From 3c463fa5fb03f1f7cb0c381f268b2794b5195a36 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 08:34:40 -0800 Subject: [PATCH 01/63] Brushes: strokes draw with the first of two brushes with one name When a document holds two brushes with the same name, the canvas drew a stroke that names it with the last of them, while brush.get, Brush Options, expanding, outlining and the SVG, PDF, EPS and .ai exports took the first. Document Info gave the last one's type, and every row of that name in the Brushes panel previewed and dragged the last one. The renderer, Document Info and the Brushes panel now take the first brush of a name, so the canvas, the panel and the exports agree. --- crates/engine/src/cmd/docinfo.rs | 11 ++++++---- crates/render/src/brush_fx.rs | 25 +++++++++++++++++++++- crates/ui-egui/src/panels/brushes.rs | 12 ++++++++++- crates/ui-egui/src/panels/brushes/tests.rs | 24 +++++++++++++++++++++ docs/target-app-parity.md | 2 ++ 5 files changed, 68 insertions(+), 6 deletions(-) diff --git a/crates/engine/src/cmd/docinfo.rs b/crates/engine/src/cmd/docinfo.rs index a43325becd..a58ff8e1ee 100644 --- a/crates/engine/src/cmd/docinfo.rs +++ b/crates/engine/src/cmd/docinfo.rs @@ -633,10 +633,13 @@ fn info(s: &mut Session, p: &Value) -> Result { fonts.extend(super::fonts::art_texts_of(d).into_iter().flat_map(|(_, t, _)| t.runs.iter().map(|r| super::fonts::font_of(&r.style)))); } let font_names: Vec = fonts.iter().map(super::fonts::font_label).collect(); - // Each brush the art uses with its type, found by name in the Brushes panel as the renderer finds it (the last of two + // Each brush the art uses with its type, found by name in the Brushes panel as the renderer finds it (the first of two // brushes with one name); a brush the panel does not hold draws as a plain stroke and has no type. A row names the type // as the New Brush dialog does (`Art Brush`), and the panel translates these values when it shows them. - let kinds: HashMap<&str, &vectorcraft_brush::BrushKind> = library.iter().map(|b| (b.name.as_str(), &b.kind)).collect(); + let mut kinds: HashMap<&str, &vectorcraft_brush::BrushKind> = HashMap::new(); + for b in &library { + kinds.entry(b.name.as_str()).or_insert(&b.kind); + } let brush_entries: Vec = brushes.iter().map(|name| json!({ "name": name, "type": kinds.get(name.as_str()).map(|k| k.type_id()) })).collect(); let brush_rows: Vec<(String, String)> = brushes @@ -910,7 +913,7 @@ mod tests { let d = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc); d.node_mut(NodeId(missing)).unwrap().appearance.stroke_mut().unwrap().paint = Paint::None; assert_eq!(s.execute("document.info", &json!({"category": "brushes"})).unwrap()["brushes"], json!([])); - // Two panel brushes with one name: the type is the one the renderer draws with, the last one's. + // Two panel brushes with one name: the type is the one the renderer draws with, the first one's. let twin = square(&mut s, 140.0); s.execute("brush.apply", &json!({"ids": [twin], "name": "Charcoal"})).unwrap(); let d = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc); @@ -920,7 +923,7 @@ mod tests { lib.extend(kinds.into_iter().map(|kind| vectorcraft_brush::Brush { name: "Twin".into(), kind })); vectorcraft_brush::store(d, &lib); d.node_mut(NodeId(twin)).unwrap().appearance.stroke_mut().unwrap().brush = Some("Twin".into()); - assert_eq!(s.execute("document.info", &json!({"category": "brushes"})).unwrap()["brushes"], json!([{"name": "Twin", "type": "scatter"}])); + assert_eq!(s.execute("document.info", &json!({"category": "brushes"})).unwrap()["brushes"], json!([{"name": "Twin", "type": "art"}])); // Type: a brushed stroke on drawn characters is listed, one on spaces alone is not. let blank = s.execute("text.create", &json!({"x": 10, "y": 60, "text": " "})).unwrap()["id"].as_u64().unwrap(); let word = s.execute("text.create", &json!({"x": 10, "y": 90, "text": "Hi"})).unwrap()["id"].as_u64().unwrap(); diff --git a/crates/render/src/brush_fx.rs b/crates/render/src/brush_fx.rs index b1b46b3327..3c6ea922d4 100644 --- a/crates/render/src/brush_fx.rs +++ b/crates/render/src/brush_fx.rs @@ -107,7 +107,12 @@ impl BrushCache { vectorcraft_brush::serde_json::to_string(v).unwrap_or_default().hash(&mut h); h.finish() }); - self.lib = vectorcraft_brush::library(doc).into_iter().map(|b| (b.name.clone(), Arc::new(b))).collect(); + // Of two brushes with one name, a stroke that names it draws with the first, as + // `vectorcraft_brush::find` and the exports take it. + self.lib.clear(); + for b in vectorcraft_brush::library(doc) { + self.lib.entry(b.name.clone()).or_insert_with(|| Arc::new(b)); + } } /// [`Self::key`] for the brush library `doc` holds (checked once a frame, `stamp`). @@ -388,6 +393,24 @@ mod tests { assert!(!dark(a.pixel(100, 58)) && dark(b.pixel(100, 58))); } + /// Of two brushes with one name, a stroke that names it draws with the first. + #[test] + fn a_stroke_draws_with_the_first_of_two_brushes_with_its_name() { + use vectorcraft_brush::{Brush, BrushKind, Calligraphic}; + let nib = |size: f64| Brush { name: "Twin".into(), kind: BrushKind::Calligraphic(Calligraphic { size, ..Default::default() }) }; + let doc = |lib: &[Brush]| { + let mut d = doc_with_line(Some("Twin"), 1.0); + vectorcraft_brush::store(&mut d, lib); + d + }; + let (thin, wide) = (nib(1.0), nib(20.0)); + let first = render(&mut Renderer::new(), &doc(std::slice::from_ref(&thin))).pixels; + let second = render(&mut Renderer::new(), &doc(std::slice::from_ref(&wide))).pixels; + assert_ne!(first, second, "the two nibs paint differently"); + let both = render(&mut Renderer::new(), &doc(&[thin, wide])).pixels; + assert!(both == first, "the stroke draws with the first brush named Twin"); + } + #[test] fn stained_symbol_instance() { let mut d = Document::new(100.0, 100.0); diff --git a/crates/ui-egui/src/panels/brushes.rs b/crates/ui-egui/src/panels/brushes.rs index 8f29c70475..969fa14aeb 100644 --- a/crates/ui-egui/src/panels/brushes.rs +++ b/crates/ui-egui/src/panels/brushes.rs @@ -69,6 +69,16 @@ pub(crate) fn document_brushes(st: &DocState) -> Rc> { MEMO.with(|m| m.borrow_mut().get(&st.doc, st.revision, || vectorcraft_brush::library(&st.doc).iter().map(|b| Shown::of(&st.doc, b)).collect())) } +/// The brushes of `shown` by name: of two with one name, the first, the one a stroke that names +/// it draws with and `brush.get` gives. +fn by_name(shown: &[Shown]) -> HashMap<&str, &Shown> { + let mut map = HashMap::new(); + for b in shown { + map.entry(b.name.as_str()).or_insert(b); + } + map +} + /// The cache key of the stroke preview of brush definition `def` in `doc` (`art`: its art): the /// definition and every resource of `doc` the art uses ([`art_resources`]: patterns with their /// options, symbols, swatches and colour groups, gradients, images, styles…), so the preview @@ -298,7 +308,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { // definitions and preview keys are the document version's, worked out once. let doc = app.session.active().map(|d| d.doc.clone()); let memo = app.session.active().map(document_brushes).unwrap_or_default(); - let defs: HashMap<&str, &Shown> = memo.iter().map(|b| (b.name.as_str(), b)).collect(); + let defs = by_name(&memo); let mut clicked: Option<(String, egui::Modifiers)> = None; let mut options: Option = None; let mut moved: Option = None; diff --git a/crates/ui-egui/src/panels/brushes/tests.rs b/crates/ui-egui/src/panels/brushes/tests.rs index fcab4373ce..2d9305fc52 100644 --- a/crates/ui-egui/src/panels/brushes/tests.rs +++ b/crates/ui-egui/src/panels/brushes/tests.rs @@ -821,3 +821,27 @@ fn previews_follow_the_swatches_images_and_pattern_options_their_art_uses() { assert_eq!(change(&mut app, what, f), (brushes, symbols, what.to_string())); } } + +/// Of two brushes with one name, both rows preview and drag the first, the one `brush.get` gives +/// and a stroke that names it draws with. +#[test] +fn rows_of_two_brushes_with_one_name_show_the_first() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 200, "height": 100})).unwrap(); + app.session + .edit("Brushes", |d, _| { + let mut lib = vectorcraft_brush::library(d); + let kinds: Vec<_> = ["Charcoal", "Dots"].iter().map(|n| lib.iter().find(|b| b.name == *n).unwrap().kind.clone()).collect(); + lib.extend(kinds.into_iter().map(|kind| Brush { name: "Twin".into(), kind })); + vectorcraft_brush::store(d, &lib); + Ok(()) + }) + .unwrap(); + let twins: Vec = brushes(&mut app).0.into_iter().filter(|b| b.0 == "Twin").map(|b| b.1).collect(); + assert_eq!(twins, ["art", "scatter"], "the panel lists both"); + let got = app.run("brush.get", json!({"name": "Twin"})).unwrap(); + let memo = document_brushes(app.session.active().unwrap()); + let shown = by_name(&memo)["Twin"]; + assert_eq!(shown.ty, "art"); + assert_eq!(shown.def, got, "the row shows and drags what brush.get gives"); +} diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 65b58c6dc5..4cd01e8945 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1766,6 +1766,8 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Brush art of copies:** symbol instances, inline graphics and repeat copies of a brushed path keep their own brush art between frames, as the same path on the page does. The renderer also keeps the stroke outlines of brush art and the evaluation of a live object in it, such as an envelope that a scatter brush places. The brush art is made again when the path, its stroke or the brush changes, and when the art of a symbol or pattern that the brush art can show changes. On one thread at 1000 × 1000 pixels (the median frame, with the median fastest frame in brackets), 2,000 paths on the page whose scatter brush art is an envelope take 94 ms (80 ms) a frame instead of 400 ms (306 ms), and 2,000 instances of a symbol whose art is a brushed path take 13.4 ms (12.0 ms) instead of 19.3 ms (15.6 ms), with the same pixels. +- **Two brushes with one name:** when a document's Brushes panel holds two brushes with the same name, a stroke that names it draws on the canvas with the first of them, as `brush.get`, Brush Options, expanding, outlining and the SVG, PDF, EPS and `.ai` exports take it. Document Info gives that brush's type, and both rows of the Brushes panel preview and drag the first brush. + - **Live objects in symbols and masks:** symbol instances and inline graphics whose symbol is a blend, an envelope, a repeat, an Intertwine object or a gradient mesh, or a group that holds one, draw the evaluation the renderer keeps for that object between frames, as the same object on the page does. So do opacity masks whose art is such an object, and such objects in clip groups in Outline view. The object is evaluated again when the symbols, patterns, fonts or color settings it shows change. Symbol art with effects of its own, or that holds a symbol instance, an image, a placed file, type with an inline graphic or a raster effect, is still placed and evaluated again in every frame. At 1000 × 1000 pixels, a page of 2,000 instances of a symbol that holds an envelope takes 28 ms a frame instead of 126 ms, with the same pixels. - **Live objects in documents with many symbols:** a live object, or an object with effects, that shows symbols or patterns is not walked again in a later frame to find them while the document's symbols and patterns stay the same. One that shows a placed file is walked in every frame. At 1000 × 1000 pixels, a page of 2,000 envelopes around instances of the last 2,000 of 10,000 symbols takes 30 ms a frame, and 83 ms when every frame walks them again, with the same pixels. From 453d869c984694c91e1c3af9d292ac0df3229ed4 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 10:18:09 -0800 Subject: [PATCH 02/63] Tests: brush art tests replace the default Dots brush The two brush art tests in render's memory tests added a scatter brush named Dots next to the default library's own Dots. Strokes now draw with the first brush of a name, so the stroke drew the default brush and the edits to the symbols and patterns its art shows no longer changed the frame. The tests now remove the default Dots before adding their own. --- crates/render/src/tests/memory.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/render/src/tests/memory.rs b/crates/render/src/tests/memory.rs index 011f9fd7fe..d286fa7e3c 100644 --- a/crates/render/src/tests/memory.rs +++ b/crates/render/src/tests/memory.rs @@ -931,6 +931,8 @@ fn brush_art_draws_edits_of_the_symbols_and_patterns_it_shows() { painted.transform(Affine::translate((0.0, 8.0)), true); let scatter = Node::group(d.alloc_id(), vec![Arc::new(instance), Arc::new(painted)]); let mut lib = vectorcraft_brush::library(&d); + // The default library has its own `Dots`, which a stroke would draw with first. + lib.retain(|b| b.name != "Dots"); lib.push(Brush { name: "Dots".into(), kind: BrushKind::Scatter(Scatter { art: scatter, spacing: (150.0, 150.0), ..Default::default() }) }); vectorcraft_brush::store(&mut d, &lib); let mut ap = vectorcraft_doc::Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 1.0); @@ -997,6 +999,8 @@ fn brush_art_draws_renamed_symbols_and_patterns_it_shows() { painted.transform(Affine::translate((0.0, 8.0)), true); let scatter = Node::group(d.alloc_id(), vec![Arc::new(instance), Arc::new(painted)]); let mut lib = vectorcraft_brush::library(&d); + // The default library has its own `Dots`, which a stroke would draw with first. + lib.retain(|b| b.name != "Dots"); lib.push(Brush { name: "Dots".into(), kind: BrushKind::Scatter(Scatter { art: scatter, spacing: (150.0, 150.0), ..Default::default() }) }); vectorcraft_brush::store(&mut d, &lib); let mut ap = vectorcraft_doc::Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 1.0); From 1ed7785bc5d677a8b66aa0241ebd0f277ae5fcea Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 11:37:49 -0800 Subject: [PATCH 03/63] Actions: keep folder preferences out of saved sets and confined replies A prefs.set step, recorded inside a command.batch or loaded from a .vcactions file, can hold a folder preference such as scratchPrimary. - ui.inspect under automation read roots now checks such a step's value for {key, value} and each entry of values, also in a batch step's commands at any depth, as written and trimmed, including bare names, subfolders and file: URLs. An empty value and the scratch disk choices Startup and None stay. - Any other step text written as an absolute path or a file: URL outside the read root is left out also when whitespace surrounds it. - Save Actions refuses a set with a prefs.set step that holds a folder value and names the action and the preference instead of writing the file. An empty folder and the scratch disk choices save. - prefs.export leaves out scratchPrimary and scratchSecondary, like the other folder preferences. prefs.import refuses a scratch disk set to a folder and still reads Startup, None and an empty value. The folder list, the scratch disk choices and the folder check now live in one place in the engine. Docs: docs/mcp.md, params text and a progress line in docs/target-app-parity.md. --- crates/engine/src/cmd/prefscmds.rs | 34 +++++---- crates/engine/src/tests/prefs_file.rs | 11 ++- crates/ui-egui/src/control.rs | 84 +++++++++++++++------- crates/ui-egui/src/control/tests.rs | 18 +++++ crates/ui-egui/src/i18n/bg.tsv | 1 + crates/ui-egui/src/i18n/bs.tsv | 1 + crates/ui-egui/src/i18n/ca.tsv | 1 + crates/ui-egui/src/i18n/cs.tsv | 1 + crates/ui-egui/src/i18n/da.tsv | 1 + crates/ui-egui/src/i18n/de.tsv | 1 + crates/ui-egui/src/i18n/el.tsv | 1 + crates/ui-egui/src/i18n/es.tsv | 1 + crates/ui-egui/src/i18n/fi.tsv | 1 + crates/ui-egui/src/i18n/fr.tsv | 1 + crates/ui-egui/src/i18n/hi.tsv | 1 + crates/ui-egui/src/i18n/hu.tsv | 1 + crates/ui-egui/src/i18n/id.tsv | 1 + crates/ui-egui/src/i18n/it.tsv | 1 + crates/ui-egui/src/i18n/ja.tsv | 1 + crates/ui-egui/src/i18n/ka.tsv | 1 + crates/ui-egui/src/i18n/ko.tsv | 1 + crates/ui-egui/src/i18n/nl.tsv | 1 + crates/ui-egui/src/i18n/pl.tsv | 1 + crates/ui-egui/src/i18n/pt-br.tsv | 1 + crates/ui-egui/src/i18n/ru.tsv | 1 + crates/ui-egui/src/i18n/sv.tsv | 1 + crates/ui-egui/src/i18n/tr.tsv | 1 + crates/ui-egui/src/i18n/uk.tsv | 1 + crates/ui-egui/src/i18n/vi.tsv | 1 + crates/ui-egui/src/i18n/zh-hans.tsv | 1 + crates/ui-egui/src/i18n/zh-hant.tsv | 1 + crates/ui-egui/src/menus/ui_commands.rs | 2 +- crates/ui-egui/src/panels/actions.rs | 42 +++++++++++ crates/ui-egui/src/panels/actions/files.rs | 33 ++++++++- crates/ui-egui/src/tests/automation.rs | 56 +++++++++++++++ docs/mcp.md | 23 +++--- docs/target-app-parity.md | 1 + 37 files changed, 283 insertions(+), 48 deletions(-) diff --git a/crates/engine/src/cmd/prefscmds.rs b/crates/engine/src/cmd/prefscmds.rs index 748e0a125a..8f7b90156f 100644 --- a/crates/engine/src/cmd/prefscmds.rs +++ b/crates/engine/src/cmd/prefscmds.rs @@ -792,7 +792,7 @@ pub fn specs() -> Vec { "Export Preferences", [], None, - "{path?} write the preferences as a JSON file {format: \"vectorcraft-preferences\", version: 1, prefs: {key: value, …}}: every preference prefs.list lists and the eyedropper, perspectiveWidget, pathfinder, glyphSnap and pixelSnap groups, without the folder preferences fontsFolder, pluginsFolder, recoveryFolder and templatesFolder and without saved presets; a file over 1 MB, which prefs.import would refuse, is an error and is not written → {path, count}; without path → {data: the file's text, count}", + "{path?} write the preferences as a JSON file {format: \"vectorcraft-preferences\", version: 1, prefs: {key: value, …}}: every preference prefs.list lists and the eyedropper, perspectiveWidget, pathfinder, glyphSnap and pixelSnap groups, without the folder preferences fontsFolder, pluginsFolder, scratchPrimary, scratchSecondary, recoveryFolder and templatesFolder and without saved presets; a file over 1 MB, which prefs.import would refuse, is an error and is not written → {path, count}; without path → {data: the file's text, count}", always, export ), @@ -801,7 +801,7 @@ pub fn specs() -> Vec { "Import Preferences", [], None, - "{path? | data?: the file's text} apply the preferences of a file prefs.export wrote (at most 1 MB). Every value is checked first, as prefs.set checks it: another format, a newer version, an unknown preference, a value of the wrong type or out of range, or a folder preference is an error, and nothing changes. Preferences the file leaves out keep their values; the open document's units stay as they are; no undo step; not a command.batch step → {imported: count}", + "{path? | data?: the file's text} apply the preferences of a file prefs.export wrote (at most 1 MB). Every value is checked first, as prefs.set checks it: another format, a newer version, an unknown preference, a value of the wrong type or out of range, or a folder preference (for scratchPrimary and scratchSecondary, a folder; the choices Startup and None are read) is an error, and nothing changes. Preferences the file leaves out keep their values; the open document's units stay as they are; no undo step; not a command.batch step → {imported: count}", always, import ), @@ -817,8 +817,20 @@ pub const PREFS_VERSION: u64 = 1; /// The largest preferences file `prefs.import` reads. const MAX_PREFS_FILE_BYTES: usize = 1 << 20; -/// Folder preferences, which name folders on one computer: a preferences file leaves them out. -const FOLDER_PREFS: &[&str] = &["fontsFolder", "pluginsFolder", "recoveryFolder", "templatesFolder"]; +/// Folder preferences, which name folders on one computer: a preferences file leaves them out, and +/// under automation read roots one outside them is not shown. +pub const FOLDER_PREFS: [&str; 6] = ["fontsFolder", "pluginsFolder", "scratchPrimary", "scratchSecondary", "recoveryFolder", "templatesFolder"]; + +/// The scratch disk choices that are not folders. +pub const SCRATCH_CHOICES: [&str; 2] = ["Startup", "None"]; + +/// Does `value`, set as preference `key`, name a folder: is `key` a folder preference +/// ([`FOLDER_PREFS`]) and `value`, trimmed as the app uses it, neither empty (the default folder) +/// nor a scratch disk choice ([`SCRATCH_CHOICES`])? +pub fn names_a_folder(key: &str, value: &str) -> bool { + let path = value.trim(); + FOLDER_PREFS.contains(&key) && !path.is_empty() && !(key.starts_with("scratch") && SCRATCH_CHOICES.contains(&path)) +} /// The preferences `prefs.export` writes: every [`PREF_SPECS`] key but the [`FOLDER_PREFS`], and /// the [`PREF_GROUPS`]. @@ -867,7 +879,9 @@ fn read_prefs_file(bytes: &[u8]) -> std::result::Result, Stri if prefs.is_empty() { return Err("the file holds no preferences".into()); } - if let Some(key) = prefs.keys().find(|k| FOLDER_PREFS.contains(&k.as_str())) { + // A scratch disk choice or an empty scratch disk names no folder, and is read. + let folder = |k: &str, v: &Value| FOLDER_PREFS.contains(&k) && (!k.starts_with("scratch") || v.as_str().is_some_and(|v| names_a_folder(k, v))); + if let Some(key) = prefs.iter().find(|(k, v)| folder(k, v)).map(|(k, _)| k) { return Err(format!("`{key}` names a folder, which preferences files leave out")); } Ok(prefs.clone()) @@ -892,14 +906,8 @@ fn import(s: &mut Session, p: &Value) -> Result { Ok(json!({ "imported": values.len() })) } -/// The preferences that name a folder: the [`FOLDER_PREFS`] and the scratch disks. -const PATH_PREFS: [&str; 6] = ["fontsFolder", "pluginsFolder", "scratchPrimary", "scratchSecondary", "recoveryFolder", "templatesFolder"]; - -/// The scratch disk choices that are not folders. -const SCRATCH_CHOICES: [&str; 2] = ["Startup", "None"]; - /// The preferences as `prefs.get`, `prefs.list`, `prefs.set` and `prefs.reset` show them to the -/// request being served. Under automation read roots, a folder preference ([`PATH_PREFS`]) outside +/// request being served. Under automation read roots, a folder preference ([`FOLDER_PREFS`]) outside /// them is `null`, as `ui.inspect` leaves it out, and an export preset's `folder` outside them is /// left out. An empty value (the default folder) and a scratch /// disk choice ([`SCRATCH_CHOICES`]) are shown. Without roots the preferences are shown as they are. @@ -910,7 +918,7 @@ fn shown_prefs(prefs: &Prefs) -> Value { } // The app trims a folder before it uses it. let hidden = |path: &str| !path.trim().is_empty() && crate::file_access::check_read(path.trim()).is_err(); - for key in PATH_PREFS { + for key in FOLDER_PREFS { let Some(path) = all[key].as_str() else { continue }; if hidden(path) && !(key.starts_with("scratch") && SCRATCH_CHOICES.contains(&path.trim())) { all[key] = Value::Null; diff --git a/crates/engine/src/tests/prefs_file.rs b/crates/engine/src/tests/prefs_file.rs index 7693ad0d5c..c2822ba937 100644 --- a/crates/engine/src/tests/prefs_file.rs +++ b/crates/engine/src/tests/prefs_file.rs @@ -12,7 +12,7 @@ fn is_bad_params(r: Result) -> bool { } /// Folder preferences, which a preferences file leaves out. -const FOLDERS: [&str; 4] = ["fontsFolder", "pluginsFolder", "recoveryFolder", "templatesFolder"]; +const FOLDERS: [&str; 6] = ["fontsFolder", "pluginsFolder", "scratchPrimary", "scratchSecondary", "recoveryFolder", "templatesFolder"]; /// A preferences file holding `prefs`, as `prefs.export` writes one. fn file(prefs: Value) -> String { @@ -46,7 +46,9 @@ fn in_file(s: &Session) -> Value { fn exported_preferences_import_unchanged() { let mut s = changed(); s.prefs.fonts_folder = "/fonts/here".into(); + s.prefs.scratch_primary = "/scratch/here".into(); let r = s.execute("prefs.export", &json!({})).unwrap(); + assert!(!r["data"].as_str().unwrap().contains("/scratch/here"), "a scratch disk folder is not written"); let text = r["data"].as_str().unwrap().to_string(); let written: Value = serde_json::from_str(&text).unwrap(); assert_eq!((written["format"].as_str(), written["version"].as_u64()), (Some(PREFS_FORMAT), Some(1))); @@ -71,6 +73,7 @@ fn exported_preferences_import_unchanged() { assert_eq!(t.doc().unwrap().doc.units, Unit::Millimeters, "the document's units stay as they are"); assert_eq!(t.doc().unwrap().history.undo.len(), undo, "no undo step"); assert_eq!(t.prefs.fonts_folder, "", "folders are not carried over"); + assert_eq!(t.prefs.scratch_primary, "Startup", "nor is a scratch disk"); // Through a file, too. let path = std::env::temp_dir().join(format!("vc-prefs-{}.json", std::process::id())).to_string_lossy().to_string(); @@ -96,6 +99,10 @@ fn a_hand_written_file_sets_what_it_names_and_leaves_the_rest() { assert_eq!(s.prefs.keyboard_increment, 3.0); assert_eq!(s.prefs.guide_color, "#abcdef"); assert_eq!(s.prefs.history_states, 77, "a preference the file leaves out keeps its value"); + // A scratch disk choice names no folder, and is read. + let choices = file(json!({"scratchPrimary": "None", "scratchSecondary": "Startup"})); + assert_eq!(s.execute("prefs.import", &json!({"data": choices})).unwrap(), json!({"imported": 2})); + assert_eq!((s.prefs.scratch_primary.as_str(), s.prefs.scratch_secondary.as_str()), ("None", "Startup")); } #[test] @@ -119,6 +126,8 @@ fn a_bad_preferences_file_is_refused_and_nothing_changes() { (json!({"data": file(json!({"uiAccent": "blue", "historyStates": 5000}))}), "`historyStates` must be a whole number between 5 and 1000"), (json!({"data": file(json!({"uiAccent": "blue", "eyedropper": 5}))}), "`eyedropper` must be an object"), (json!({"data": file(json!({"uiAccent": "blue", "fontsFolder": "/fonts"}))}), "`fontsFolder` names a folder"), + (json!({"data": file(json!({"uiAccent": "blue", "scratchPrimary": "/scratch"}))}), "`scratchPrimary` names a folder"), + (json!({"data": file(json!({"uiAccent": "blue", "scratchSecondary": " scratch "}))}), "`scratchSecondary` names a folder"), (json!({"data": too_large}), "larger than 1 MB"), (json!({"data": 5}), "`data` must be a string"), (json!({"path": "/a.json", "data": file(json!({"uiAccent": "blue"}))}), "give `path` or `data`"), diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index 89ab7e37b4..cc5ed91cb6 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -944,12 +944,6 @@ fn names_a_loaded_library(v: &Value) -> bool { } } -/// The preferences that hold a folder, by their keys in `ui.engine_prefs` and the Preferences dialog. -const PREF_FOLDERS: [&str; 6] = ["fontsFolder", "pluginsFolder", "scratchPrimary", "scratchSecondary", "recoveryFolder", "templatesFolder"]; - -/// The scratch disk choices that are not paths. -const SCRATCH_CHOICES: [&str; 2] = ["Startup", "None"]; - /// May the request being served read `path`? Under an automation read root (a control client's /// request), `ui.inspect` leaves out a path outside the root, as `ui.menu.list` leaves out a recent /// file or a listed script outside it. Another platform's absolute path @@ -972,11 +966,11 @@ fn unreadable_absolute(s: &str) -> bool { } /// Is `s` written as a path that the request may not read: an absolute path or a `file:` URL -/// ([`unreadable_absolute`]), `~` or a path under it, or a path that starts with `.` or `..` and a -/// separator? It is checked as the file access check reads it, relative to the working folder. Other -/// text with a separator (`a/b`) stays. +/// ([`unreadable_absolute`]), as written or with surrounding whitespace trimmed, `~` or a path under +/// it, or a path that starts with `.` or `..` and a separator? It is checked as the file access check +/// reads it, relative to the working folder. Other text with a separator (`a/b`) stays. fn unreadable_path_like(s: &str) -> bool { - if unreadable_absolute(s) { + if unreadable_absolute(s) || unreadable_absolute(s.trim()) { return true; } let t = s.trim_start(); @@ -1045,11 +1039,49 @@ fn percent_decoded(s: &str) -> String { String::from_utf8_lossy(&out).into_owned() } -/// The params of each step of each action of action set `set` (`ui.action_sets`). -fn steps_mut(set: &mut Value) -> impl Iterator> { +/// The command and params of each step of each action of action set `set` (`ui.action_sets`). +fn steps_mut(set: &mut Value) -> impl Iterator)> { let actions = set.get_mut("actions").and_then(Value::as_array_mut).into_iter().flatten(); let steps = actions.filter_map(|action| action.get_mut("steps").and_then(Value::as_array_mut)).flatten(); - steps.filter_map(|step| step.get_mut(1).and_then(Value::as_object_mut)) + steps.filter_map(|step| { + let command = step.get(0).and_then(Value::as_str).unwrap_or_default().to_string(); + step.get_mut(1).and_then(Value::as_object_mut).map(|params| (command, params)) + }) +} + +/// Takes out of the params of a recorded step running `command` the paths the request may not read: +/// the folder preferences' values [`leave_out_unreadable_pref_steps`] takes out, and what +/// [`leave_out_unreadable_params`] takes out, each in one pass over the params. +fn leave_out_unreadable_step(command: &str, params: &mut serde_json::Map) { + leave_out_unreadable_pref_steps(command, params); + leave_out_unreadable_params(params); +} + +/// Takes out of the params of a step running `command` the folder preferences' values the request +/// may not read ([`shows_folder`]): in a `prefs.set` step, given as `{key, value}` or in `values`; in +/// a `command.batch` step, the same in each of its commands, at any depth. +fn leave_out_unreadable_pref_steps(command: &str, params: &mut serde_json::Map) { + match command { + "prefs.set" => { + if let Some(Value::Object(values)) = params.get_mut("values") { + leave_out_unreadable_folders(values); + } + if let (Some(key), Some(value)) = (params.get("key").and_then(Value::as_str), params.get("value").and_then(Value::as_str)) + && !shows_folder(key, value) + { + params.remove("value"); + } + } + "command.batch" => { + for step in params.get_mut("commands").and_then(Value::as_array_mut).into_iter().flatten().filter_map(Value::as_object_mut) { + let command = step.get("command").and_then(Value::as_str).unwrap_or_default().to_string(); + if let Some(Value::Object(params)) = step.get_mut("params") { + leave_out_unreadable_pref_steps(&command, params); + } + } + } + _ => {} + } } /// Takes out of a recorded step's params, at any depth (the commands of a `command.batch` step), the @@ -1103,8 +1135,8 @@ fn leave_out_unreadable_paths(ui: &mut serde_json::Map) { } } } - for step in ui.get_mut("action_sets").and_then(Value::as_array_mut).into_iter().flatten().flat_map(steps_mut) { - leave_out_unreadable_params(step); + for (command, params) in ui.get_mut("action_sets").and_then(Value::as_array_mut).into_iter().flatten().flat_map(steps_mut) { + leave_out_unreadable_step(&command, params); } if let Some(Value::Object(prefs)) = ui.get_mut("engine_prefs") { leave_out_unreadable_folders(prefs); @@ -1117,20 +1149,24 @@ fn leave_out_unreadable_paths(ui: &mut serde_json::Map) { } } -/// Takes out of `prefs` the folder preferences ([`PREF_FOLDERS`]) the request may not read, as written -/// or trimmed as the app uses it. An empty value (no folder) and a scratch disk choice -/// ([`SCRATCH_CHOICES`]) stay. +/// Takes out of `prefs` the folder preferences +/// ([`vectorcraft_engine::cmd::prefscmds::FOLDER_PREFS`]) the request may not read ([`shows_folder`]). fn leave_out_unreadable_folders(prefs: &mut serde_json::Map) { - for key in PREF_FOLDERS { - let Some(written) = prefs.get(key).and_then(Value::as_str) else { continue }; - let path = written.trim(); - if path.is_empty() || (key.starts_with("scratch") && SCRATCH_CHOICES.contains(&path)) || (readable(written) && readable(path)) { - continue; + for key in vectorcraft_engine::cmd::prefscmds::FOLDER_PREFS { + if prefs.get(key).and_then(Value::as_str).is_some_and(|written| !shows_folder(key, written)) { + prefs.remove(key); } - prefs.remove(key); } } +/// May the request see `written` as the value of preference `key`? Yes unless it names a folder +/// ([`vectorcraft_engine::cmd::prefscmds::names_a_folder`]: an empty value and a scratch disk choice +/// do not) that the request may not read as written, or trimmed as the app uses it, also as a `file:` +/// URL ([`readable_value`]). +fn shows_folder(key: &str, written: &str) -> bool { + !vectorcraft_engine::cmd::prefscmds::names_a_folder(key, written) || (readable(written) && readable_value(written.trim())) +} + fn key_from(name: &str) -> Option { egui::Key::from_name(name).or(match name.to_ascii_lowercase().as_str() { "enter" | "return" => Some(egui::Key::Enter), diff --git a/crates/ui-egui/src/control/tests.rs b/crates/ui-egui/src/control/tests.rs index ac8fbfa3eb..4d43339a85 100644 --- a/crates/ui-egui/src/control/tests.rs +++ b/crates/ui-egui/src/control/tests.rs @@ -293,6 +293,24 @@ fn an_absolute_path_outside_the_read_root_is_unreadable() { }); } +/// Under a read root, an absolute path or a `file:` URL outside it is written as a path also with +/// whitespace around it, and a recorded step's text param loses it; a path inside the root stays. +#[test] +fn a_spaced_absolute_path_outside_the_read_root_is_left_out() { + let base = crate::tests_folder::Folder::new("unreadable-spaced"); + std::fs::create_dir_all(base.join("in")).unwrap(); + let roots = vectorcraft_engine::file_access::AutomationRoots::new(&[&base.join("in")], &[]).unwrap(); + let inside = format!(" {} ", base.join("in").join("a.svg").to_string_lossy()); + let mut params = json!({"target": " /outside/root/x", "name": "\tfile:///outside/root/x ", "keep": inside, "list": [" /outside/root/y"]}); + vectorcraft_engine::file_access::confine(roots.as_ref(), || { + assert!(super::unreadable_path_like(" /outside/root/x")); + assert!(super::unreadable_path_like("\tfile:///outside/root/x ")); + assert!(!super::unreadable_path_like(&inside), "{inside}"); + super::leave_out_unreadable_params(params.as_object_mut().unwrap()); + }); + assert_eq!(params, json!({"keep": inside, "list": []})); +} + /// Without a read root, a `file:` URL that names another host may be read, and a recorded step keeps /// it. #[test] diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 4c2785956f..a5e8b6efd0 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -5844,6 +5844,7 @@ theme Appearance Външен вид @msg no such color няма такъв цвят @msg {_1} cannot be used on Windows {_1} не може да се използва в Windows @msg more than one action is named “{_1}” повече от едно действие се казва „{_1}“ +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set действието „{_1}“ задава предпочитанието за папка {_2}; изтрийте тази стъпка, за да запазите набора @msg no symbol named `{n}` няма символ с име `{n}` Smudge Stick… Размазваща пръчка… Smudge Stick Размазваща пръчка diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 9ccc94d3a4..4cd869081f 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -5847,6 +5847,7 @@ mark-style Japanese Japanski @msg no such color nema takve boje @msg {_1} cannot be used on Windows {_1} se ne može koristiti na Windowsu @msg more than one action is named “{_1}” više od jedne akcije ima naziv „{_1}” +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set akcija „{_1}” postavlja postavku foldera {_2}; izbrišite taj korak da biste sačuvali skup @msg no symbol named `{n}` nema simbola pod nazivom `{n}` Smudge Stick… Štapić za razmazivanje… Smudge Stick Štapić za razmazivanje diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index b0159ebc14..522abdab79 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -5722,6 +5722,7 @@ digits Malayalam Malaiàlam @msg no such color no existeix aquest color @msg {_1} cannot be used on Windows {_1} no es pot fer servir al Windows @msg more than one action is named “{_1}” més d’una acció es diu “{_1}” +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set l’acció “{_1}” defineix la preferència de carpeta {_2}; suprimiu aquest pas per desar el conjunt @msg name the file poseu un nom al fitxer @msg New Window isn't available yet: a document opens in one window Finestra nova encara no està disponible: un document s’obre en una sola finestra @msg no active document no hi ha cap document actiu diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 27b2d32771..04b292ed8b 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -6153,6 +6153,7 @@ mark-style Japanese Japonské @msg no such color taková barva neexistuje @msg {_1} cannot be used on Windows Zkratku {_1} nelze ve Windows použít @msg more than one action is named “{_1}” název „{_1}“ má více než jedna akce +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set akce „{_1}“ nastavuje předvolbu složky {_2}; chcete-li sadu uložit, tento krok smažte @msg no symbol named `{n}` žádný symbol s názvem `{n}` Smudge Stick… Rozmazávací tyčinka… Smudge Stick Rozmazávací tyčinka diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 3810bb2ac8..0f3865bb2c 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -5726,6 +5726,7 @@ swatch Bright Violet Kraftig violet @msg no such color ingen sådan farve @msg {_1} cannot be used on Windows {_1} kan ikke bruges i Windows @msg more than one action is named “{_1}” mere end én handling hedder »{_1}« +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set handlingen »{_1}« angiver mappeindstillingen {_2}; slet det trin for at gemme sættet @msg no symbol named `{n}` intet symbol med navnet `{n}` Remove Color Profile… Fjern farveprofil… Color wheel Farvehjul diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 7b6cd3e8a6..93cdda7c63 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -5739,6 +5739,7 @@ effect Soft Weich @msg no such color keine solche Farbe @msg {_1} cannot be used on Windows {_1} kann unter Windows nicht verwendet werden @msg more than one action is named “{_1}” mehr als eine Aktion heißt „{_1}“ +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set die Aktion „{_1}“ legt die Ordnervoreinstellung {_2} fest; löschen Sie diesen Schritt, um den Satz zu speichern @msg no symbol named `{n}` kein Symbol namens `{n}` Smudge Stick… Wischstift… Smudge Stick Wischstift diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 8620169f21..b407274593 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -6105,6 +6105,7 @@ effect Points Σημεία @msg no such color δεν υπάρχει τέτοιο χρώμα @msg {_1} cannot be used on Windows Το {_1} δεν μπορεί να χρησιμοποιηθεί στα Windows @msg more than one action is named “{_1}” περισσότερες από μία ενέργειες ονομάζονται «{_1}» +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set η ενέργεια «{_1}» ορίζει την προτίμηση φακέλου {_2}· διαγράψτε αυτό το βήμα για να αποθηκεύσετε το σύνολο @msg no symbol named `{n}` κανένα σύμβολο με όνομα `{n}` Smudge Stick… Μουτζούρα… Smudge Stick Μουτζούρα diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index a82db20a99..e181c9a86e 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -6096,6 +6096,7 @@ mark-style Japanese Japonesas @msg no such color no existe ese color @msg {_1} cannot be used on Windows {_1} no se puede usar en Windows @msg more than one action is named “{_1}” más de una acción se llama “{_1}” +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set la acción “{_1}” establece la preferencia de carpeta {_2}; elimine ese paso para guardar el conjunto @msg no symbol named `{n}` no hay ningún símbolo llamado `{n}` Smudge Stick… Difuminar con barra… Smudge Stick Difuminar con barra diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index db90a36316..741f782599 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -6116,6 +6116,7 @@ mark-style Japanese Japanilainen @msg no such color väriä ei ole @msg {_1} cannot be used on Windows {_1} ei ole käytettävissä Windowsissa @msg more than one action is named “{_1}” useamman kuin yhden toiminnon nimi on ”{_1}” +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set toiminto ”{_1}” asettaa kansioasetuksen {_2}; poista tämä vaihe, jotta voit tallentaa sarjan @msg no symbol named `{n}` symbolia nimeltä `{n}` ei ole Smudge Stick… Sumennuskynä… Smudge Stick Sumennuskynä diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 938e43dd48..23bf37a539 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -6148,6 +6148,7 @@ theme Appearance Apparence @msg no such color couleur inexistante @msg {_1} cannot be used on Windows {_1} ne peut pas être utilisé sous Windows @msg more than one action is named “{_1}” plusieurs scripts s’appellent « {_1} » +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set le script « {_1} » définit la préférence de dossier {_2} ; supprimez cette étape pour enregistrer le jeu @msg no symbol named `{n}` aucun symbole nommé `{n}` Smudge Stick… Estompe… Smudge Stick Estompe diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index 0eacf9629b..5522cc31bd 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -7170,6 +7170,7 @@ antiAlias Sharp शार्प @msg more than 10000 linked files: package dependencies in smaller parts 10000 से अधिक लिंक्ड फ़ाइलें: पैकेज निर्भरता छोटे भागों में @msg more than 256 commands 256 से अधिक कमांड @msg more than one action is named “{_1}” एक से अधिक कार्रवाई का नाम "{_1}" रखा गया है। +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set कार्रवाई "{_1}" फ़ोल्डर प्राथमिकता {_2} सेट करती है; सेट सेव करने के लिए वह चरण हटाएँ @msg Move Panel to New Window is not available: floating panels stay in the main window नई विंडो में पैनल को स्थानांतरित करना उपलब्ध नहीं है: अस्थायी पैनल मुख्य विंडो में रहते हैं @msg move requires a destination panel move: एक गंतव्य पैनल की आवश्यकता होती है @msg move requires an existing destination panel move: चाल एक मौजूदा गंतव्य पैनल की आवश्यकता है diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 3dadc4d3b6..8361c405f3 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -6136,6 +6136,7 @@ imageTrace Outline Körvonal @msg no such color nincs ilyen szín @msg {_1} cannot be used on Windows A(z) {_1} nem használható Windows rendszeren @msg more than one action is named “{_1}” több művelet neve is „{_1}” +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set a(z) „{_1}” művelet beállítja a(z) {_2} mappabeállítást; a készlet mentéséhez törölje ezt a lépést @msg no symbol named `{n}` nincs `{n}` nevű szimbólum Smudge Stick… Elmosó rúd… Smudge Stick Elmosó rúd diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index e670840b2c..bd7a906986 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -5726,6 +5726,7 @@ swatch Bright Violet Violet cerah @msg no such color tidak ada warna seperti itu @msg {_1} cannot be used on Windows {_1} tidak dapat digunakan di Windows @msg more than one action is named “{_1}” lebih dari satu tindakan bernama “{_1}” +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set tindakan “{_1}” mengatur preferensi folder {_2}; hapus langkah itu untuk menyimpan set @msg no symbol named `{n}` tidak ada simbol bernama `{n}` Remove Color Profile… Hapus profil warna… Color wheel Roda warna diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 91d1e24bf7..765a2aa6aa 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -5774,6 +5774,7 @@ lighting Light Luce @msg no such color colore inesistente @msg {_1} cannot be used on Windows {_1} non può essere usato su Windows @msg more than one action is named “{_1}” più di un’azione si chiama “{_1}” +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set l’azione “{_1}” imposta la preferenza della cartella {_2}; elimina quel passaggio per salvare il set @msg no symbol named `{n}` nessun simbolo denominato `{n}` Smudge Stick… Sfumino… Smudge Stick Sfumino diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 628d1ef3de..7d725b5352 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -5818,6 +5818,7 @@ lighting Light ライト @msg no such color 指定されたカラーが見つかりません @msg {_1} cannot be used on Windows {_1} は Windows では使用できません @msg more than one action is named “{_1}” 「{_1}」という名前のアクションが複数あります +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set アクション「{_1}」がフォルダーの環境設定 {_2} を設定しています。セットを保存するにはそのステップを削除してください @msg no symbol named `{n}` `{n}` という名前のシンボルはありません Smudge Stick… スマッジスティック… Smudge Stick スマッジスティック diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index aa2c27c296..ea3cf5a668 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -5880,6 +5880,7 @@ imageTrace Outline კონტური @msg no such color ასეთი ფერი არ არის @msg {_1} cannot be used on Windows {_1} ვერ გამოიყენება Windows-ში @msg more than one action is named “{_1}” „{_1}“ ერთზე მეტი მოქმედების სახელია +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set მოქმედება „{_1}“ აყენებს საქაღალდის პარამეტრს {_2}; ნაკრების შესანახად წაშალეთ ეს ნაბიჯი @msg no symbol named `{n}` სიმბოლო სახელით `{n}` არ არის @msg no saved selection recalled შენახული მონიშვნა გამოძახებული არ არის @msg The last start of VectorCraft ended on {name} before its window appeared, so it started on {summary}. To try that graphics processor again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set VectorCraft-ის ბოლო გაშვება ფანჯრის გამოჩენამდე შეწყდა (გრაფიკული პროცესორი: {name}), ამიტომ VectorCraft გაეშვა: {summary}. ამ გრაფიკული პროცესორის ხელახლა საცდელად შეცვალეთ პარამეტრები › წარმადობა › გრაფიკული პროცესორი ან გაუშვით WGPU_BACKEND ცვლადის მითითებით diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index ca5c2ac8d1..98be548ced 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -5735,6 +5735,7 @@ lighting Light 광원 @msg no such color 해당 색상이 없습니다 @msg {_1} cannot be used on Windows {_1}은(는) Windows에서 사용할 수 없습니다 @msg more than one action is named “{_1}” “{_1}”(이)라는 이름의 액션이 둘 이상 있습니다 +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set 액션 “{_1}”이(가) 폴더 환경 설정 {_2}을(를) 설정합니다. 세트를 저장하려면 해당 단계를 삭제하세요 @msg no symbol named `{n}` 이름이 `{n}`인 심볼이 없습니다 Smudge Stick… 문지르기 막대… Smudge Stick 문지르기 막대 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index d46d7e8e87..441dca9c77 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -6300,6 +6300,7 @@ swatch Bright Violet Felviolet @msg no such color kleur bestaat niet @msg {_1} cannot be used on Windows {_1} kan niet worden gebruikt in Windows @msg more than one action is named “{_1}” meer dan één actie heet „{_1}” +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set de actie „{_1}” stelt de mapvoorkeur {_2} in; verwijder die stap om de set op te slaan @msg no symbol named `{n}` geen symbool met de naam `{n}` Remove Color Profile… Kleurprofiel verwijderen… Color wheel Kleurenwiel diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 38d5e2f497..9a689ba5e2 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -6008,6 +6008,7 @@ mark-style Japanese Japońskie @msg no such color brak takiego koloru @msg {_1} cannot be used on Windows Skrótu {_1} nie można używać w systemie Windows @msg more than one action is named “{_1}” więcej niż jedna operacja nazywa się „{_1}” +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set operacja „{_1}” ustawia preferencję folderu {_2}; usuń ten krok, aby zapisać zestaw @msg no symbol named `{n}` brak symbolu o nazwie `{n}` Smudge Stick… Rozmazywacz… Smudge Stick Rozmazywacz diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 2a45bc8e68..ce2a694be7 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -6068,6 +6068,7 @@ mark-style Japanese Japonesas @msg no such color cor inexistente @msg {_1} cannot be used on Windows {_1} não pode ser usado no Windows @msg more than one action is named “{_1}” mais de uma ação se chama “{_1}” +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set a ação “{_1}” define a preferência de pasta {_2}; exclua essa etapa para salvar o conjunto @msg no symbol named `{n}` nenhum símbolo chamado `{n}` Smudge Stick… Esfuminho… Smudge Stick Esfuminho diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index af93b86974..f2646a0bb2 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -5759,6 +5759,7 @@ lighting Light Свет @msg no such color такого цвета нет @msg {_1} cannot be used on Windows Сочетание {_1} нельзя использовать в Windows @msg more than one action is named “{_1}” несколько действий называются «{_1}» +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set действие «{_1}» задаёт настройку папки {_2}; удалите этот шаг, чтобы сохранить набор @msg no symbol named `{n}` нет символа с именем `{n}` Smudge Stick… Растушёвка… Smudge Stick Растушёвка diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index f26438a201..08c988a3d4 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -6107,6 +6107,7 @@ theme Appearance Utseende @msg no such color färgen finns inte @msg {_1} cannot be used on Windows {_1} kan inte användas i Windows @msg more than one action is named “{_1}” fler än en åtgärd heter ”{_1}” +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set åtgärden ”{_1}” anger mappinställningen {_2}; ta bort det steget för att spara uppsättningen @msg no symbol named `{n}` ingen symbol med namnet `{n}` Angular Vinkelmått Away from the Line Bort från linjen diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index a91aab6f0e..58d8cf1546 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -5804,6 +5804,7 @@ theme Appearance Görünüm @msg no such color öyle bir renk yok @msg {_1} cannot be used on Windows {_1} Windows'ta kullanılamaz @msg more than one action is named “{_1}” birden fazla eylemin adı “{_1}” +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set “{_1}” eylemi {_2} klasör tercihini ayarlıyor; seti kaydetmek için o adımı silin @msg no symbol named `{n}` `{n}` adında bir sembol yok Artboard Options: edit with the Artboard tool Çalışma Yüzeyi Seçenekleri: Çalışma Yüzeyi aracıyla düzenleme @msg this is a PostScript file (an .ai saved in an older format or without PDF compatibility, or an EPS file), which can't be opened yet: save it as PDF, PDF-compatible .ai or SVG bu henüz açılamayan bir PostScript dosyasıdır (eski bir formatta kaydedilmiş veya PDF uyumluluğu olmayan bir .ai veya bir EPS dosyası): PDF, PDF uyumlu .ai veya SVG olarak kaydedin diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 17ddeb2086..37c56e1881 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -5754,6 +5754,7 @@ lighting Light Світло @msg no such color такого кольору немає @msg {_1} cannot be used on Windows Сполучення {_1} не можна використовувати у Windows @msg more than one action is named “{_1}” кілька дій мають назву «{_1}» +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set дія «{_1}» задає параметр папки {_2}; видаліть цей крок, щоб зберегти набір @msg no symbol named `{n}` немає символу з назвою `{n}` Smudge Stick… Розтушовування… Smudge Stick Розтушовування diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 7d3d89a82d..4c3fff5f31 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -5755,6 +5755,7 @@ theme Appearance Giao diện @msg no such color không có màu như vậy @msg {_1} cannot be used on Windows không thể dùng {_1} trên Windows @msg more than one action is named “{_1}” có nhiều hành động tên là “{_1}” +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set hành động “{_1}” đặt tùy chọn thư mục {_2}; hãy xóa bước đó để lưu bộ @msg no symbol named `{n}` không có biểu tượng nào tên `{n}` Smudge Stick… Bút chì xóa mờ… Smudge Stick Bút chì xóa mờ diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 34ffeb5d6b..cf912fb369 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -5987,6 +5987,7 @@ lighting Light 光源 @msg no such color 没有此颜色 @msg {_1} cannot be used on Windows {_1} 在 Windows 上不能使用 @msg more than one action is named “{_1}” 有多个动作名为“{_1}” +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set 动作“{_1}”设置了文件夹首选项 {_2};请删除该步骤后再存储动作组 @msg no symbol named `{n}` 没有名为 `{n}` 的符号 Smudge Stick… 涂抹棒… Smudge Stick 涂抹棒 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 309e00346c..2fc05b8c7c 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -5818,6 +5818,7 @@ lighting Light 光源 @msg no such color 沒有這個顏色 @msg {_1} cannot be used on Windows {_1} 無法在 Windows 上使用 @msg more than one action is named “{_1}” 有多個動作名為「{_1}」 +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set 動作「{_1}」設定了資料夾偏好設定 {_2};請刪除該步驟後再儲存動作組 @msg no symbol named `{n}` 沒有名為 `{n}` 的符號 Smudge Stick… 塗抹棒… Smudge Stick 塗抹棒 diff --git a/crates/ui-egui/src/menus/ui_commands.rs b/crates/ui-egui/src/menus/ui_commands.rs index 37cdcb2d56..b29918c878 100644 --- a/crates/ui-egui/src/menus/ui_commands.rs +++ b/crates/ui-egui/src/menus/ui_commands.rs @@ -1538,7 +1538,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ crate::panels::actions::SAVE_SET, "Save Actions…", "", - "{set?: index, id?, path?} write the Actions panel's set at set (0 is the top set; default: the current set) with its actions to an action set file (.vcactions) at path, else a picked file named after the set (the web downloads it). id holds the panel's own set id, which its menu passes: the set with this id is saved wherever it is now, and set is not read. A set over 8 MiB as a file, with more than 1,000 actions or with an action of more than 10,000 steps is not written; the panel menu's Save Actions… → {path}", + "{set?: index, id?, path?} write the Actions panel's set at set (0 is the top set; default: the current set) with its actions to an action set file (.vcactions) at path, else a picked file named after the set (the web downloads it). id holds the panel's own set id, which its menu passes: the set with this id is saved wherever it is now, and set is not read. A set over 8 MiB as a file, with more than 1,000 actions or with an action of more than 10,000 steps is not written, nor is a set with a prefs.set step (also in a batch step) that sets a folder preference to a folder (an error names the action and the preference; an empty folder and the scratch disk choices Startup and None save); the panel menu's Save Actions… → {path}", ), ( crate::panels::actions::LOAD_SET, diff --git a/crates/ui-egui/src/panels/actions.rs b/crates/ui-egui/src/panels/actions.rs index 1971834a07..630d81e95f 100644 --- a/crates/ui-egui/src/panels/actions.rs +++ b/crates/ui-egui/src/panels/actions.rs @@ -3512,6 +3512,48 @@ mod tests { } } + /// `ui.actionSet.save` refuses a set with a step that sets a folder preference to a folder, as + /// `{key, value}`, in `values` or in a batch step's commands at any depth, with an error that names + /// the action and the preference, and writes no file. An empty folder, a scratch disk choice and + /// other preferences save. + #[test] + fn save_actions_refuses_a_step_that_sets_a_folder_preference() { + let mut app = app_with_sets(); + let files = with_files(&mut app, "/sets/x.vcactions"); + let step = |command: &str, params: Value| (command.to_string(), params); + let nested = json!({"commands": [{"command": "command.batch", "params": {"commands": [ + {"command": "prefs.set", "params": {"values": {"fontsFolder": "fonts"}}}, + ]}}]}); + for (steps, key) in [ + (vec![step("prefs.set", json!({"key": "scratchPrimary", "value": "scratch"}))], "scratchPrimary"), + (vec![step("prefs.set", json!({"values": {"uiAccent": "green", "templatesFolder": "/Users/me/Templates"}}))], "templatesFolder"), + ( + vec![ + step("object.move", json!({"dx": 1, "dy": 0})), + step( + "command.batch", + json!({"commands": [{"command": "prefs.set", "params": {"key": "scratchSecondary", "value": " /Volumes/Fast "}}]}), + ), + ], + "scratchSecondary", + ), + (vec![step("command.batch", nested)], "fontsFolder"), + ] { + app.ui.action_sets[1] = ActionSet::new("Other", vec![Action::new("Scratch", steps)]); + let e = app.run(SAVE_SET, json!({"set": 1, "path": "/sets/x.vcactions"})).unwrap_err(); + assert_eq!(e, format!("the action “Scratch” sets the folder preference {key}; delete that step to save the set")); + assert!(files.borrow().is_empty(), "no file is written for {key}"); + } + let kept = vec![ + step("prefs.set", json!({"values": {"scratchPrimary": "None", "scratchSecondary": " Startup ", "fontsFolder": "", "uiAccent": "green"}})), + step("prefs.set", json!({"key": "pluginsFolder", "value": ""})), + step("x.y", json!({"key": "fontsFolder", "value": "fonts"})), + ]; + app.ui.action_sets[1] = ActionSet::new("Other", vec![Action::new("Scratch", kept)]); + assert_eq!(app.run(SAVE_SET, json!({"set": 1, "path": "/sets/x.vcactions"})).unwrap(), json!({"path": "/sets/x.vcactions"})); + assert!(files.borrow().contains_key("/sets/x.vcactions")); + } + /// A file that is not an action set file, or is too large, is refused with a message, and no /// set is added. #[test] diff --git a/crates/ui-egui/src/panels/actions/files.rs b/crates/ui-egui/src/panels/actions/files.rs index a8ae79e713..b5805f9b2a 100644 --- a/crates/ui-egui/src/panels/actions/files.rs +++ b/crates/ui-egui/src/panels/actions/files.rs @@ -169,6 +169,30 @@ fn ui_steps_in<'a>(command: &str, params: &'a Value) -> Vec<&'a Value> { .collect() } +/// A folder preference whose value in a step running `command` with `params` names a folder +/// ([`vectorcraft_engine::cmd::prefscmds::names_a_folder`]): in a `prefs.set` step, given as +/// `{key, value}` or in `values`, and in a `command.batch` step, in any of its commands at any depth. +/// Every such value is found, also one under `values` that `{key, value}` replaces when the step runs, +/// because [`write_set`] writes the params as they are. +fn folder_pref_in(command: &str, params: &Value) -> Option { + use vectorcraft_engine::cmd::prefscmds::names_a_folder; + match command { + "prefs.set" => { + let names = |key: &str, value: &Value| value.as_str().is_some_and(|v| names_a_folder(key, v)); + let pair = params.get("key").and_then(Value::as_str).filter(|key| params.get("value").is_some_and(|v| names(key, v))); + let values = params.get("values").and_then(Value::as_object).into_iter().flatten(); + pair.map(str::to_string).or_else(|| values.filter(|(key, v)| names(key, v)).map(|(key, _)| key.clone()).next()) + } + "command.batch" => { + let mut steps = params.get("commands").and_then(Value::as_array).into_iter().flatten(); + steps.find_map(|step| { + folder_pref_in(step.get("command").and_then(Value::as_str).unwrap_or_default(), step.get("params").unwrap_or(&Value::Null)) + }) + } + _ => None, + } +} + /// `set` as the text of an action set file. pub fn write_set(set: &ActionSet) -> String { let actions = set @@ -281,7 +305,9 @@ pub(super) fn set_file_stem(name: &str) -> String { /// the set (the web downloads it) → `{path}`. `id` is the panel's own set id ([`ActionSet::id`]), /// which the panel menu passes: the set with this id is saved wherever it is when the file dialog /// answers, and `set` is not read. A set that [`read_set`] would refuse (larger than 8 MiB as a -/// file, more than 1,000 actions, an action with more than 10,000 steps) is not written. +/// file, more than 1,000 actions, an action with more than 10,000 steps) is not written, nor is a set +/// with a step that sets a folder preference to a folder ([`folder_pref_in`]), which names a folder +/// on this computer: the error names the action and the preference. pub fn save_set(app: &mut VectorcraftApp, p: &Value) -> Result { let len = app.ui.action_sets.len(); let set = match (p.get("id"), p.get("set")) { @@ -292,6 +318,11 @@ pub fn save_set(app: &mut VectorcraftApp, p: &Value) -> Result { (_, Some(v)) => v.as_u64().and_then(|i| usize::try_from(i).ok()).filter(|&i| i < len).ok_or("no such action set")?, }; let Some(s) = app.ui.action_sets.get(set) else { return Err("no such action set".into()) }; + let folder = + s.actions.iter().find_map(|a| a.steps.iter().find_map(|(command, params)| folder_pref_in(command, params)).map(|key| (&a.name, key))); + if let Some((action, key)) = folder { + return Err(format!("the action “{}” sets the folder preference {}; delete that step to save the set", action, key)); + } let text = write_set(s); // What Save Actions… writes, Load Actions… reads back. read_set(&text, "")?; diff --git a/crates/ui-egui/src/tests/automation.rs b/crates/ui-egui/src/tests/automation.rs index 6d7c7075c5..2364a00a56 100644 --- a/crates/ui-egui/src/tests/automation.rs +++ b/crates/ui-egui/src/tests/automation.rs @@ -891,6 +891,62 @@ fn ui_inspect_leaves_out_relative_paths_in_recorded_steps() { let _ = std::fs::remove_dir_all(base); } +/// `ui.inspect` leaves out of a recorded `prefs.set` step the value of a folder preference outside the +/// read root, given as `{key, value}` or in `values`, also in a batch step's commands at any depth: a +/// bare name or a subfolder (in the working folder, outside the root), a path with spaces around it and +/// a `file:` URL. A folder inside the root, an empty value, a scratch disk choice, other preferences and +/// another command's params stay, and the UI state is not changed. +#[test] +fn ui_inspect_leaves_out_folder_preferences_in_recorded_prefs_steps() { + let (mut app, tx, base) = confined("inspect-prefs-steps"); + let (inside, outside) = (s(base.join("in")), s(base.join("outside"))); + let url = if outside.starts_with('/') { format!("file://{outside}") } else { format!("file:///{}", outside.replace('\\', "/")) }; + let folders = ["fontsFolder", "pluginsFolder", "scratchPrimary", "scratchSecondary", "recoveryFolder", "templatesFolder"]; + let mut steps: Vec = folders.iter().map(|k| json!(["prefs.set", {"key": k, "value": "scratch"}])).collect(); + steps.push(json!(["prefs.set", {"values": folders.iter().map(|k| (k.to_string(), json!("tmp/scratch"))).collect::>()}])); + steps.push(json!(["prefs.set", {"values": { + "scratchPrimary": format!(" {outside} "), "fontsFolder": url, "pluginsFolder": format!(" {url}"), "recoveryFolder": "Startup", + "templatesFolder": inside, "scratchSecondary": " None ", "uiAccent": "green", + }}])); + steps.push(json!(["prefs.set", {"key": "scratchPrimary", "value": "Startup"}])); + steps.push(json!(["prefs.set", {"key": "recoveryFolder", "value": ""}])); + steps.push(json!(["prefs.set", {"key": "fontsFolder", "value": inside}])); + let batch = |commands: Value| json!({"commands": commands}); + let inner = batch(json!([{"command": "prefs.set", "params": {"values": {"scratchSecondary": "tmp/scratch", "uiAccent": "red"}}}])); + steps.push(json!([ + "command.batch", + batch(json!([ + {"command": "prefs.set", "params": {"key": "scratchPrimary", "value": "scratch"}}, + {"command": "command.batch", "params": inner}, + ])) + ])); + steps.push(json!(["x.y", {"key": "scratchPrimary", "value": "scratch"}])); + let set: crate::panels::actions::ActionSet = + serde_json::from_value(json!({"name": "set", "actions": [{"name": "prefs", "steps": steps}]})).unwrap(); + app.ui.action_sets = vec![set]; + let r = request(&mut app, &tx, "ui.inspect", json!({})); + let shown = &r["result"]["ui"]["action_sets"][0]["actions"][0]["steps"]; + let mut want: Vec = folders.iter().map(|k| json!(["prefs.set", {"key": k}])).collect(); + want.push(json!(["prefs.set", {"values": {}}])); + want.push(json!(["prefs.set", {"values": {"templatesFolder": inside, "scratchSecondary": " None ", "uiAccent": "green"}}])); + want.push(json!(["prefs.set", {"key": "scratchPrimary", "value": "Startup"}])); + want.push(json!(["prefs.set", {"key": "recoveryFolder", "value": ""}])); + want.push(json!(["prefs.set", {"key": "fontsFolder", "value": inside}])); + let inner = batch(json!([{"command": "prefs.set", "params": {"values": {"uiAccent": "red"}}}])); + want.push(json!([ + "command.batch", + batch(json!([ + {"command": "prefs.set", "params": {"key": "scratchPrimary"}}, + {"command": "command.batch", "params": inner}, + ])) + ])); + want.push(json!(["x.y", {"key": "scratchPrimary", "value": "scratch"}])); + assert_eq!(shown, &json!(want), "{r}"); + assert!(!r.to_string().contains("tmp/scratch") && !r.to_string().contains(&outside.replace('\\', "\\\\")), "{r}"); + assert_eq!(app.ui.action_sets[0].actions[0].steps[0].1, json!({"key": "fontsFolder", "value": "scratch"}), "the UI state is not changed"); + let _ = std::fs::remove_dir_all(base); +} + /// Two libraries loaded from outside the read root under the same file name, only the second of them /// in a library panel: `ui.inspect` and `swatch.library.list` show the second under the same id, and /// that id finds it. diff --git a/docs/mcp.md b/docs/mcp.md index 2b20cfc588..752e501e50 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -148,12 +148,14 @@ the suite) keep the agent to the folders you give it: in a recorded action's steps in `ui.action_sets`, also in a batch step's commands, the `path`, `paths`, `file`, `files` and `folder` params, a param whose name ends in `Path`, `Paths`, `File`, `Files` or `Folder` (`diffPath`, `destinationFolder`), the entries of a list under such a param, and under any other param, as a key or in any - list, an absolute path or a `file:` URL outside the roots (any case of `file:`, `%XX` escapes decoded; a URL that + list, an absolute path or a `file:` URL outside the roots, also with whitespace around it (any case of `file:`, `%XX` escapes decoded; a URL that names another host is always left out), and `~`, a path under `~` or a path that starts with `./` or `../` (either separator) that resolves outside the roots from the app's working folder; and the folder - preferences in `ui.engine_prefs` and in an open Preferences dialog's `ui.dialog.fields` - (`fontsFolder`, `pluginsFolder`, `scratchPrimary`, `scratchSecondary`, `recoveryFolder`, `templatesFolder`; an - empty folder and the scratch disk choices `Startup` and `None` stay). A library loaded from a file outside the + preferences in `ui.engine_prefs`, in an open Preferences dialog's `ui.dialog.fields` and in a recorded + `prefs.set` step (its `value` for such a `key`, and the entries of `values`), also in a batch step's commands + (`fontsFolder`, `pluginsFolder`, `scratchPrimary`, `scratchSecondary`, `recoveryFolder`, `templatesFolder`, checked + as written and trimmed, a bare name or a subfolder from the app's working folder; an empty folder and the scratch + disk choices `Startup` and `None` stay). A library loaded from a file outside the roots shows as `loaded/` wherever `ui_inspect` and `ui.docking` name it: its id, its panel's id, the Color Guide's `ui.color_guide_limit`, a closed panel's key in `ui.docking_hidden` and the `library` and `panel` params of a recorded action's steps in `ui.action_sets`, and the keys of shortcut overrides (`panel:` @@ -3152,7 +3154,11 @@ already walks Image Trace. Short agent entry points: after the set (the web downloads it), and returns `{path}`. `id` holds the panel's own set id, which the panel menu passes: the set with this id is saved wherever it is when the file dialog answers, and `set` is not read. A set that would be larger than 8 MiB as a file, has more than 1,000 actions or has an action of more than 10,000 steps is not - written, and the command returns the same error as a load of such a file. `ui.actionSet.load {path? | data?}` reads + written, and the command returns the same error as a load of such a file. Nor is a set with a `prefs.set` step, also + in a batch step's commands, that sets a folder preference (`fontsFolder`, `pluginsFolder`, `scratchPrimary`, + `scratchSecondary`, `recoveryFolder`, `templatesFolder`) to a folder, which names a folder on this computer: the + error `the action “” sets the folder preference ; delete that step to save the set` names the first such + action and preference. An empty folder and the scratch disk choices `Startup` and `None` save. `ui.actionSet.load {path? | data?}` reads the file at `path`, else a picked one, or the file's text `data`, adds its set after the other sets and returns `{set, name, actions}`. A set named like one already in the panel gets the next free number (`Print 2`), and the other sets and the current set stay as they are. An action set file (`.vcactions`) is JSON: @@ -6118,12 +6124,13 @@ Preferences. `prefs.reset` resets the preferences at once, without the question. `prefs.export {path?}` writes the preferences as a JSON file, `{"format": "vectorcraft-preferences", "version": 1, "prefs": {…}}`, and replies `{path, count}`; without `path` it replies `{data, count}` with the file's text. The file holds every preference `prefs.list` lists and the `eyedropper`, `perspectiveWidget`, `pathfinder`, `glyphSnap` and -`pixelSnap` groups. It leaves out the folder preferences (`fontsFolder`, `pluginsFolder`, `recoveryFolder`, `templatesFolder`) and -saved presets. An export over 1 MB, the most `prefs.import` reads, is an error and writes nothing. +`pixelSnap` groups. It leaves out the folder preferences (`fontsFolder`, `pluginsFolder`, `scratchPrimary`, `scratchSecondary`, +`recoveryFolder`, `templatesFolder`) and saved presets. An export over 1 MB, the most `prefs.import` reads, is an error and writes nothing. `prefs.import {path | data}` applies such a file and replies `{imported}`, the number of preferences it set. It checks every value first, as `prefs.set` does: a file over 1 MB, malformed JSON, another `format`, a newer `version`, an empty `prefs`, an unknown preference, a value of the wrong type or out of range, or a folder preference -is an error, and nothing changes. Preferences the file leaves out keep their values. An import leaves the open +is an error, and nothing changes. For `scratchPrimary` and `scratchSecondary` only a folder is an error: an empty +value and the choices `Startup` and `None` are read. Preferences the file leaves out keep their values. An import leaves the open document's units as they are, records no undo step, and is saved with the other preferences. An open Preferences dialog shows the imported values. `prefs.import` is refused as a `command.batch` step. The dialog's Export Preferences… and Import Preferences… buttons run the two commands with a save or open dialog; the web build downloads the exported file and has no Import Preferences… button. diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index f7782bdfcd..2c0b7cab1e 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -747,6 +747,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Batch…: Override Save and Override Export name each file with the extension of the format the step writes, however the step spells the format (`.svg` and `SVG` give `.svg`, `png8` gives `.png`), in `action.batch`. - Batch…: Override Export writes the files of an Export for Screens or Export Assets step into a folder for each source file or data set inside the destination folder, under the names the export gives them (`action.batch`). The folder and file names drop trailing periods and spaces (`a..svg` gives `a`). When two source files would get the same name that way (compared without case), or a later file's folder or file would take the name of an output an earlier file wrote, of a source file in the destination folder or, for data sets, of a file already there, Batch adds `-2`, `-3`… to the later one's name. - Actions panel menu: Save Actions… writes the current action set with all its actions to a file, and Load Actions… adds the set from such a file to the panel ([Manage a set of actions](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/manage-a-set-of-actions.html); `ui.actionSet.save`, `ui.actionSet.load`). The file is VectorCraft's own action set file (`.vcactions`); a loaded set named like one in the panel gets the next free number. + - Actions panel menu: Save Actions… does not write a set with a step that sets a folder preference (the Fonts, Plug-ins, Templates and Recovery folders and the two scratch disks) to a folder, and names the action and the preference instead; an empty folder and the scratch disk choices Startup and None save. Export Preferences leaves out both scratch disks, and `ui.inspect` under automation read roots leaves such a folder outside them out of recorded steps. These are VectorCraft's own rules (`ui.actionSet.save`, `prefs.export`). - Actions panel menu: Load Actions… also reads action sets saved as `.aia` files ([Actions panel overview](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/actions-panel-overview.html); `actionSet.read`, `ui.actionSet.load`). VectorCraft's own rules: the set comes in with all its actions. A step with a label and no recorded values comes in as the one menu command with the step's label, when that command is directly in a top menu, is what its menu bar item runs with no values and opens no dialog. A step that is off in the file stays excluded from playback. The other steps, recorded menu items included, are left out, and the status bar says how many. - Actions panel: the panel menu's Action Options… renames the selected action and keeps its steps ([Actions panel overview](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/actions-panel-overview.html): the menu item opens a dialog that renames the action or gives it a function key and color; `ui.action.options`). It also sets the action's function key and color. - Actions panel: Create New Action (bottom bar) and the panel menu's New Action… ask for the new action's name, set, function key and color, and Record adds the action and starts recording into it ([Create new actions](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/record-actions.html): the New Action dialog takes the action's name and the set it goes in; `ui.action.new`, `ui.action.play`). The function key (F1 to F15, with Ctrl or Command and Shift) plays the action. An action's function key plays the action even when a command has the same shortcut, and the command keeps it. VectorCraft's own rules, which the page does not cover: when several actions have the same key the first in panel order plays; a copied action has no key; New Action does not open while an action is recording. The color is kept with the action for Button Mode, which VectorCraft does not have yet. From 82bf9624bfebcb147672a8e9c6354c0110319d36 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 11:44:24 -0800 Subject: [PATCH 04/63] Smooth: the cursor shows Not Allowed when no path is selected to smooth The Smooth tool showed its own cursor with nothing selected, though a drag then changes nothing. It now shows Not Allowed until a path it can act on is selected (an editable path, not a guide, with three or more anchors on one of its subpaths); a selected path anywhere on the canvas gives the Smooth cursor wherever the pointer is. - The Pencil with Alt key toggles to Smooth Tool on and the Blob Brush show Not Allowed the same way when Alt gives them the Smooth tool. The Paintbrush keeps its closing cursor, since its release with Alt closes the stroke. - A Smooth release, from the Smooth tool or from the Pencil, Paintbrush or Blob Brush with Alt, sends path.smoothRegion only while such a path is selected. With only type or other objects selected it sends no command. - Use Precise Cursors leaves Not Allowed as it is. - The cursor catalog and docs/target-app-parity.md describe the rule. --- crates/engine/src/tests/prefs.rs | 22 +++- crates/tools/src/cursors.toml | 9 +- crates/tools/src/draw2/gesture.rs | 22 ++-- crates/tools/src/draw2/gesture/tests.rs | 131 ++++++++++++++++++++++-- crates/tools/src/lib.rs | 12 ++- docs/target-app-parity.md | 3 +- 6 files changed, 179 insertions(+), 20 deletions(-) diff --git a/crates/engine/src/tests/prefs.rs b/crates/engine/src/tests/prefs.rs index 37127f686a..121ada4a2a 100644 --- a/crates/engine/src/tests/prefs.rs +++ b/crates/engine/src/tests/prefs.rs @@ -665,7 +665,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), @@ -710,6 +709,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/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/draw2/gesture.rs b/crates/tools/src/draw2/gesture.rs index 2de44f97ba..a96cffd307 100644 --- a/crates/tools/src/draw2/gesture.rs +++ b/crates/tools/src/draw2/gesture.rs @@ -763,7 +763,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(|sp| sp.anchors.len() >= 3)) + if !found + && matches!(&child.kind, NodeKind::Path { guide: false, path, .. } if path.subpaths.iter().any(|sp| sp.anchors.len() >= 3) && path.control_bounds().is_some()) && editable(child.id) { found = true; @@ -773,6 +774,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 { @@ -1147,7 +1155,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": self.smooth_fidelity}), @@ -1199,7 +1207,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": self.fidelity}))] } "pathEraser" if has_sel => vec![Action::Exec("path.eraseSegments".into(), json!({"points": points_json(pts), "width": cx.tol(8.0)}))], @@ -1504,13 +1512,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`). @@ -1533,7 +1543,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 624adc2e98..98e154f870 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] @@ -2223,6 +2231,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/lib.rs b/crates/tools/src/lib.rs index 17e6f038e9..1447847213 100644 --- a/crates/tools/src/lib.rs +++ b/crates/tools/src/lib.rs @@ -619,8 +619,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), @@ -646,7 +654,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/docs/target-app-parity.md b/docs/target-app-parity.md index e309f72409..163b56d661 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1041,7 +1041,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Drawing:** Pen, Curvature, anchor tools, Pencil, Paintbrush, Blob Brush, Smooth, Path Eraser, Join. Direction handles dragged with Direct Selection or the Anchor Point tool snap to smart guides, and Shift keeps them at 45° steps from their anchor. The Pen continues any open path from the end clicked, and while drawing, a click on an end of another open path joins the two into one (#776). The Curvature tool edits any selected path, whatever drew it, keeping its shape except where edited (#798): a click on a point makes it current and a drag moves it, re-curving only its two segments; double-click, or Alt-click on any point but an end of an open path, toggles it smooth or corner and Delete removes it; a click on a segment adds a point without changing the shape; with an end of an open path current, clicks go on from it and its other end closes it (`path.curvatureEdit`). An Alt-click on an end of a selected open path makes that end current without changing it, so the next click goes on from it, as [the help page](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/draw-shapes-with-the-curvature-tool.html) describes for continuing a path; an Alt-click that starts a new path places a corner point, as later Alt-clicks do. Every point a drawing tool places snaps to Smart Guides through one snapper (#506): the Pen's and Curvature tool's anchors, the Pencil's ends, the shape tools' start and dragged corner or radius (Rectangle, Rounded Rectangle, Ellipse, Polygon, Star, Line Segment, Arc, Spiral, the grids), the Type tool's click and frame, a new artboard's corners. A filled Pencil path keeps a stroke of None (#1804, part of #1679). While a Pencil or Paintbrush stroke is drawn back to within Close Paths When Ends Are Within of its start, the cursor shows the closing ring and a release there closes the path, as [Draw straight lines with the Pencil tool](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/draw-straight-lines-with-the-pencil-tool.html) describes for the Pencil. The [ and ] keys reach the Blob Brush, the Eraser and the Symbolism tools in the app (#1734, part of #1661). Blob Brush and Eraser Tool Options' Size is Fixed or Pressure with a Variation (#4590, #3961): with Pressure a pen stroke is Size − Variation wide where it presses lightest and Size + Variation where it presses hardest, and a mouse stroke keeps the Size. With the Blob Brush, Shift held during a drag keeps the stroke on the horizontal or vertical line through the press point, whichever is nearer to the pointer (90° steps from the Constrain Angle preference), and the points dragged since Shift went down move to the other line when the pointer crosses to it; the points dragged while Shift is up follow the pointer. - With the Blob Brush, Alt held before the press, or pressed before the drag has moved, switches to the Smooth tool, as the reference app's [Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html) page lists: the cursor shows the Smooth tool, the drag shows a thin line and, with a path selected that it can smooth, the dashed band it smooths, without the length label, Shift does not constrain it, and the release smooths the selected paths under the drag (`path.smoothRegion`) instead of painting; with nothing selected the release changes no path. A stroke that has moved keeps painting when Alt goes down. + With the Blob Brush, Alt held before the press, or pressed before the drag has moved, switches to the Smooth tool, as the reference app's [Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html) page lists: the cursor shows the Smooth tool (Not Allowed with no path selected that it can act on), the drag shows a thin line and, with a path selected that it can smooth, the dashed band it smooths, without the length label, Shift does not constrain it, and the release smooths the selected paths under the drag (`path.smoothRegion`) instead of painting; with nothing selected the release changes no path. A stroke that has moved keeps painting when Alt goes down. Curvature point markers are circular, with no direction handles. While drawing, its own point feedback replaces generic selection markers; existing paths use the selection overlay, including partial selections and Hide Edges. Anchor Size applies, and Direct Selection and Pen retain their handles. The real egui-frame regressions in `crates/ui-egui/src/canvas.rs` verify marker positions, counts and unchanged documents (#1236, in part); the existing-path interpolation request remains open. The curve through the points (`curvature_spline` in `crates/tools/src/draw2.rs`) makes each stretch of three points follow the circle through them: a smooth point's handles lie along that circle's tangent, each `chord / (3 cos²(α/2))` long (α between handle and chord, capped at 90°), and the ends of an open path mirror their neighbour's tangent in their chord, so a J or a U curves evenly, a turn tighter than 30° comes to a point, and uneven spacing doesn't loop; the rubber band, a dragged or inserted point and `path.convertAnchor` to smooth use the same rule, while the Pencil's fitting keeps its own; `tests/draw2.rs` checks that the rubber band is the path the click makes (#1236, in part). The Smooth tool smooths only the stretch of path under the drag (#1547, fixes #1512). A path the Smooth tool changes stays selected whole, and the anchor points and segments selected on it with Direct Selection are deselected. Selected paths the drag does not change keep their selected anchors, and undo restores the earlier selection (`tests/draw2.rs`, `smoothing_a_path_drops_its_direct_selected_anchors`). While the Smooth tool drags with a path selected that it can smooth, a dashed outline shows the band it smooths: everything within its radius of the drag (12 screen pixels, VectorCraft's own value), round at both ends; Alt smoothing with the Pencil, Paintbrush and Blob Brush shows the same band. While Alt is held during a Paintbrush stroke or before a press with no path selected that the Smooth tool can act on, the Paintbrush shows a closing cursor (a crosshair with a ring), and so does the Pencil with Alt key toggles to Smooth Tool off when Alt is pressed after the drag starts; releasing the mouse button with Alt still held closes the path. Before a Paintbrush press with such a path selected, Alt shows the Smooth cursor, and holding Alt at the press smooths that path with the Smooth tool's radius and Smooth Tool Options' Fidelity. A Pencil drag started with Shift or Alt held draws a straight line instead, which stays open. With Shift held at the press, the Pencil draws a straight line from the press point to the pointer at 45° steps from the Constrain Angle, as [Draw straight lines with the Pencil tool](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/draw-straight-lines-with-the-pencil-tool.html) describes; pressed on an end of a selected open path, the line continues the path from that end (part of #3799). With Option/Alt held at the press and Alt key toggles to Smooth Tool off, the Pencil draws a straight line at any angle, whose ends snap to Smart Guides, and it stays open when Alt is still held at the release; Shift and Alt together keep the 45° steps (#3799). While Shift or Alt is held before the press, the Pencil shows a straight-line cursor (a crosshair with a short line), and over an end of a selected open path, where the line continues the path, a path-continuation cursor (the same line leaving a small square), as the page describes. @@ -1118,6 +1118,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur The Pencil's and the Paintbrush's Fidelity is in screen pixels, the same at any zoom: their Tool Options give it in px, from 0.01 px on the logarithmic slider to 20 px and up to 100 px in the field, and the tool sends `path.freehand` the value in points at the current zoom (#5729). The Smooth tool's and the Blob Brush's Fidelity stays in points. `path.freehand` and `brush.freehand` take a Fidelity of up to 100 / 0.0313 pt (about 3194.89 pt), the 100 px field maximum at the lowest zoom (3.13 %), and reject a value outside their range with an error, so a Pencil or Paintbrush stroke drawn zoomed out keeps the Fidelity set in pixels (engine tests `pencil_fit_keeps_its_anchors_on_a_stroke_drawn_zoomed_out` and `pencil_takes_fidelity_within_its_range_and_rejects_the_rest`). Each drag of the Smooth tool smooths the stretch of a selected path in its reach along its length before refitting it within Fidelity, so a wobble wider than Fidelity is smoothed out and another drag over the same place smooths it further, as [the help page](https://helpx.adobe.com/illustrator/using/tool-techniques/smooth-tool.html) describes (`path.smoothRegion`). The smoothing spread (a third of the reach) and the rule that each refit stays within twice the distance the smoothing moved the path, so repeated drags do not drift from it, are VectorCraft's own. A drag over the whole of a closed path smooths it across its first anchor too, so a bump there is smoothed like any other and no corner is left; Object › Path › Smooth smooths a closed path across its first anchor the same way. + The Smooth tool shows the Not Allowed cursor while no path is selected that it can act on (an editable path, not a guide, with three or more anchors on one of its subpaths), and the release of a drag then sends no command and changes nothing; [the help page](https://helpx.adobe.com/illustrator/using/tool-techniques/smooth-tool.html) has the path selected first. A selected path anywhere on the canvas gives the Smooth cursor wherever the pointer is. The Pencil with Alt key toggles to Smooth Tool on and the Blob Brush show Not Allowed the same way when Alt gives them the Smooth tool; the Paintbrush keeps its closing cursor, since its release with Alt closes the stroke. Use Precise Cursors leaves Not Allowed as it is. This cursor rule is VectorCraft's own (tools `smooth_shows_not_allowed_without_a_selected_path_to_smooth`, `borrowed_smooth_shows_not_allowed_without_a_selected_path_to_smooth`, `smoothing_release_sends_nothing_without_a_selected_path_to_smooth`; engine `use_precise_cursors_keeps_the_smooth_tools_not_allowed_pointer`). Before a press that draws a new path, the Pencil cursor shows a small star, as [Draw freeform paths with the Pencil tool](https://web.archive.org/web/20260926095638/https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/draw-freeform-paths-with-the-pencil-tool.html) describes; over a selected path within Edit selected paths' distance, which the press continues from an end or redraws, the star is not shown ([Reshape paths with the Pencil tool](https://web.archive.org/web/20260106004407/https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/reshape-paths-with-the-pencil-tool.html)). Keep selected in the Pencil and Paintbrush Tool Options is on by default: while it is on, a stroke selects the path it draws or continues and replaces the earlier selection. While it is off, a new stroke leaves nothing selected, the earlier selection included, as the reference application starts the stroke by dropping it (#5752); a continued or redrawn path is deselected while the rest of the selection stays (`path.freehand` `keepSelected`). With two open paths selected, a Pencil stroke from an end of one that ends within Edit selected paths' distance of an end of the other connects them into one path with the first path's appearance, in one undo step, as [Connect two paths with the Pencil tool](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/connect-two-paths-with-the-pencil-tool.html) describes (`path.freehand` `connect`). From dd3438cb7f192e1b741ad82f537d53091f75ad86 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 12:09:16 -0800 Subject: [PATCH 05/63] Live Paint Bucket: Cursor Swatch Preview and arrow keys pick swatches Live Paint Bucket Options gets a Cursor Swatch Preview checkbox (on at first, saved with the other options). With the Bucket selected, Left and Right set the active fill, or the stroke when the Stroke proxy is in front, to the color swatch before or after it in the Swatches panel, wrapping around at the ends, with the preview on or off. The selected art keeps its paint. The preview shows the active paint above the pointer between the swatches before and after it; a color that is no swatch shows alone until Right picks the first swatch or Left the last, and a paint that is not a solid color shows no chip. The chips hide during a fill drag and while Alt samples with the eyedropper. The fill and the stroke each remember the swatch picked for them, so swatches of the same color are each reached in turn. The Bucket and the Shape Builder now take the arrows for swatches only without Shift, Alt or Cmd/Ctrl, judged by the modifiers held when the key went down, so modified arrows keep their shortcuts (Tool::claims_key_with, Session::tool_claims_key_with). Docs: docs/mcp.md, docs/target-app-parity.md, tool.options params text. --- crates/engine/src/tests/build.rs | 197 ++++++++++++++++++ crates/engine/src/tooling.rs | 6 + crates/mcp/src/ui_catalog.rs | 2 +- crates/tools/src/builder.rs | 62 ++++-- crates/tools/src/builder/live_paint.rs | 75 ++++++- crates/tools/src/builder/tests.rs | 16 +- crates/tools/src/lib.rs | 5 + crates/tools/src/settings.rs | 2 +- crates/ui-egui/src/canvas/tests.rs | 8 + .../ui-egui/src/dialogs/live_paint_bucket.rs | 20 +- crates/ui-egui/src/menus/ui_commands.rs | 2 +- crates/ui-egui/src/shortcuts.rs | 19 +- crates/ui-egui/src/shortcuts/tests.rs | 31 +++ docs/mcp.md | 15 +- docs/target-app-parity.md | 1 + 15 files changed, 418 insertions(+), 43 deletions(-) diff --git a/crates/engine/src/tests/build.rs b/crates/engine/src/tests/build.rs index 6eeace1073..30956a6e4e 100644 --- a/crates/engine/src/tests/build.rs +++ b/crates/engine/src/tests/build.rs @@ -1748,6 +1748,203 @@ fn shape_builder_arrow_keys_pass_swatches_of_the_same_color() { assert_eq!(s.paint.fill, after, "the swatch after Twin B"); } +/// The colors of the cursor swatch preview's chips, left to right, and which one is ringed. +fn swatch_chips(s: &mut Session, v: ViewInfo) -> Vec<([u8; 4], bool)> { + let mut chips: Vec<(f64, [u8; 4], bool)> = s + .overlays(v) + .into_iter() + .filter_map(|o| match o { + vectorcraft_tools::Overlay::Swatch { p, color, selected } => Some((p.x, color, selected)), + _ => None, + }) + .collect(); + chips.sort_by(|a, b| a.0.total_cmp(&b.0)); + chips.into_iter().map(|(_, c, sel)| (c, sel)).collect() +} + +/// A solid paint's color as the chips draw it. +fn chip_color(p: &Paint) -> [u8; 4] { + match p { + Paint::Solid { color, .. } => color.to_rgba8(1.0), + _ => panic!("not a solid color: {p:?}"), + } +} + +/// Live Paint Bucket: Left and Right step the active paint through the document's color swatches +/// (the selected art keeps its paint), the Cursor Swatch Preview (on at first) shows the active +/// paint ringed between the swatches before and after it, and with it off the arrows still step. +#[test] +fn live_paint_bucket_arrow_keys_step_the_active_paint_through_the_swatches() { + let mut s = session(); + let (a, _) = two(&mut s); + let v = ViewInfo::default(); + s.select_tool("livePaintBucket", v).unwrap(); + assert_eq!(s.tool_options_of("livePaintBucket")["cursorSwatchPreview"], json!(true)); + assert!(s.tool_claims_key(ToolKey::Right, v) && s.tool_claims_key(ToolKey::Left, v)); + assert!(!s.tool_claims_key(ToolKey::Up, v), "Up and Down keep their own use"); + let list: Vec<(String, Paint)> = + vectorcraft_tools::builder::color_swatches(&s.doc().unwrap().doc).iter().map(|sw| (sw.name.clone(), sw.paint.clone())).collect(); + assert!(list.len() >= 4); + let n = list.len(); + let stroke = s.paint.stroke.clone(); + s.execute("paint.setFill", &json!({"swatch": list[0].0, "ids": []})).unwrap(); + s.tool_key(ToolKey::Right, Mods::default(), v).unwrap(); + s.tool_key(ToolKey::Right, Mods::default(), v).unwrap(); + assert_eq!(s.paint.fill, list[2].1); + s.tool_key(ToolKey::Left, Mods::default(), v).unwrap(); + assert_eq!(s.paint.fill, list[1].1); + assert_eq!(s.paint.stroke, stroke, "the stroke stays"); + assert_eq!(node(&s, a).appearance.fill_paint(), Paint::solid(Color::rgb(1.0, 0.0, 0.0)), "A keeps its fill"); + s.pointer(&PointerEvent::new(PointerKind::Move, 300.0, 300.0), v).unwrap(); + assert_eq!(swatch_chips(&mut s, v), vec![(chip_color(&list[0].1), false), (chip_color(&list[1].1), true), (chip_color(&list[2].1), false)]); + let alt = Mods { alt: true, ..Default::default() }; + s.pointer(&PointerEvent::new(PointerKind::Move, 300.0, 300.0).with_mods(alt), v).unwrap(); + assert!(swatch_chips(&mut s, v).is_empty(), "Alt samples with the eyedropper and shows no chips"); + s.pointer(&PointerEvent::new(PointerKind::Move, 300.0, 300.0), v).unwrap(); + // Past the first swatch, Left wraps around to the last. + s.tool_key(ToolKey::Left, Mods::default(), v).unwrap(); + s.tool_key(ToolKey::Left, Mods::default(), v).unwrap(); + assert_eq!(s.paint.fill, list[n - 1].1); + assert_eq!(swatch_chips(&mut s, v).iter().map(|c| c.0).collect::>(), [&list[n - 2].1, &list[n - 1].1, &list[0].1].map(chip_color)); + // With the Stroke proxy in front, the arrows step the stroke and the chips show it. + s.execute("paint.toggleActive", &json!({"fill": false})).unwrap(); + let fill = s.paint.fill.clone(); + s.execute("paint.setStroke", &json!({"swatch": list[1].0, "ids": []})).unwrap(); + s.tool_key(ToolKey::Right, Mods::default(), v).unwrap(); + assert_eq!((&s.paint.fill, &s.paint.stroke), (&fill, &list[2].1)); + assert!(!s.fill_active, "the Stroke proxy stays in front"); + assert_eq!(swatch_chips(&mut s, v)[1], (chip_color(&list[2].1), true)); + assert_eq!(node(&s, a).appearance.fill_paint(), Paint::solid(Color::rgb(1.0, 0.0, 0.0)), "A keeps its paint"); + // Cursor Swatch Preview off hides the chips; the arrows still step. + s.set_tool_option_cmd(&json!({"tool": "livePaintBucket", "values": {"cursorSwatchPreview": false}})).unwrap(); + assert!(swatch_chips(&mut s, v).is_empty()); + s.tool_key(ToolKey::Right, Mods::default(), v).unwrap(); + assert_eq!(s.paint.stroke, list[3].1); +} + +/// The Live Paint Bucket and the Shape Builder take Left and Right for swatches only without +/// modifiers: Shift, Alt and Cmd/Ctrl arrows are left to their shortcuts and change no paint. +#[test] +fn swatch_arrow_keys_leave_modified_arrows_to_the_shortcuts() { + let mut s = session(); + two(&mut s); + let v = ViewInfo::default(); + let first = vectorcraft_tools::builder::color_swatches(&s.doc().unwrap().doc)[0].name.clone(); + s.execute("paint.setFill", &json!({"swatch": first, "ids": []})).unwrap(); + for tool in ["livePaintBucket", "shapeBuilder"] { + s.select_tool(tool, v).unwrap(); + if tool == "shapeBuilder" { + s.set_tool_option_cmd(&json!({"tool": tool, "values": {"pickColorFrom": "swatches"}})).unwrap(); + } + let fill = s.paint.fill.clone(); + assert!(s.tool_claims_key_with(ToolKey::Right, Mods::default(), v), "{tool}"); + for m in [Mods { shift: true, ..Default::default() }, Mods { alt: true, ..Default::default() }, Mods { cmd: true, ..Default::default() }] { + for key in [ToolKey::Left, ToolKey::Right] { + assert!(!s.tool_claims_key_with(key, m, v), "{tool} {key:?} {m:?}"); + s.tool_key(key, m, v).unwrap(); + assert_eq!(s.paint.fill, fill, "{tool} {key:?} {m:?}"); + } + } + s.tool_key(ToolKey::Right, Mods::default(), v).unwrap(); + assert_ne!(s.paint.fill, fill, "{tool}: a plain Right steps"); + } +} + +/// Two swatches of the same color: the Bucket's arrows step through both, and the fill and the +/// stroke each step on from the swatch picked for them. +#[test] +fn live_paint_bucket_arrow_keys_pass_swatches_of_the_same_color_for_fill_and_stroke() { + let mut s = session(); + two(&mut s); + for name in ["Twin A", "Twin B"] { + s.execute("swatch.new", &json!({"name": name, "color": "#123456"})).unwrap(); + } + let v = ViewInfo::default(); + s.select_tool("livePaintBucket", v).unwrap(); + let names: Vec = vectorcraft_tools::builder::color_swatches(&s.doc().unwrap().doc).iter().map(|sw| sw.name.clone()).collect(); + let a = names.iter().position(|n| n == "Twin A").unwrap(); + assert_eq!(names[a + 1], "Twin B"); + let after = vectorcraft_tools::builder::color_swatches(&s.doc().unwrap().doc)[(a + 2) % names.len()].paint.clone(); + // The fill goes to Twin B. + s.execute("paint.setFill", &json!({"swatch": "Twin A", "ids": []})).unwrap(); + let twin = s.paint.fill.clone(); + s.tool_key(ToolKey::Right, Mods::default(), v).unwrap(); + assert_eq!(s.paint.fill, twin, "Twin B"); + // The stroke starts at Twin A: Right goes to Twin B, then past it. + s.execute("paint.setStroke", &json!({"swatch": "Twin A", "ids": []})).unwrap(); + assert!(!s.fill_active); + s.tool_key(ToolKey::Right, Mods::default(), v).unwrap(); + assert_eq!(s.paint.stroke, twin, "Twin B for the stroke"); + s.tool_key(ToolKey::Right, Mods::default(), v).unwrap(); + assert_eq!(s.paint.stroke, after, "the swatch after Twin B"); + // Back on the fill, which is still Twin B: Right goes past it. + s.execute("paint.toggleActive", &json!({"fill": true})).unwrap(); + s.tool_key(ToolKey::Right, Mods::default(), v).unwrap(); + assert_eq!(s.paint.fill, after, "the fill steps on from Twin B"); +} + +/// A color that is no swatch shows alone and Right picks the first swatch, Left the last; a paint +/// that is not a solid color shows no chip; one swatch steps to itself; with no color swatch the +/// tool leaves the arrows alone; during a fill drag the arrows change nothing. +#[test] +fn live_paint_bucket_arrow_keys_handle_unmatched_paint_few_swatches_and_drags() { + let mut s = session(); + live(&mut s); + let v = ViewInfo::default(); + s.select_tool("livePaintBucket", v).unwrap(); + s.pointer(&PointerEvent::new(PointerKind::Move, 300.0, 300.0), v).unwrap(); + let list: Vec = vectorcraft_tools::builder::color_swatches(&s.doc().unwrap().doc).iter().map(|sw| sw.paint.clone()).collect(); + let odd = Paint::solid(Color::rgb8(0x12, 0x34, 0x57)); + assert!(!list.contains(&odd)); + s.paint.fill = odd.clone(); + assert_eq!(swatch_chips(&mut s, v), vec![(chip_color(&odd), true)]); + s.tool_key(ToolKey::Right, Mods::default(), v).unwrap(); + assert_eq!(s.paint.fill, list[0]); + s.paint.fill = odd.clone(); + s.tool_key(ToolKey::Left, Mods::default(), v).unwrap(); + assert_eq!(s.paint.fill, list[list.len() - 1]); + s.paint.fill = Paint::None; + assert!(swatch_chips(&mut s, v).is_empty()); + + // During a fill drag the arrows are not taken and change nothing. + s.paint.fill = list[0].clone(); + s.pointer(&PointerEvent::new(PointerKind::Down, 25.0, 50.0), v).unwrap(); + assert!(s.tool_busy()); + assert!(!s.tool_claims_key(ToolKey::Right, v)); + s.tool_key(ToolKey::Right, Mods::default(), v).unwrap(); + assert_eq!(s.paint.fill, list[0]); + assert!(swatch_chips(&mut s, v).is_empty(), "no chips during a drag"); + s.pointer(&PointerEvent::new(PointerKind::Up, 25.0, 50.0), v).unwrap(); + + // One color swatch: both arrows pick it, and it stands on both sides of itself. + let one = list[0].clone(); + s.edit("t", |d, _| { + d.swatch_groups.clear(); + d.swatches.retain(|sw| sw.paint == one); + d.swatches.truncate(1); + Ok(()) + }) + .unwrap(); + assert_eq!(vectorcraft_tools::builder::color_swatches(&s.doc().unwrap().doc).len(), 1); + s.paint.fill = odd; + s.tool_key(ToolKey::Left, Mods::default(), v).unwrap(); + assert_eq!(s.paint.fill, one); + s.pointer(&PointerEvent::new(PointerKind::Move, 301.0, 300.0), v).unwrap(); + assert_eq!(swatch_chips(&mut s, v), vec![(chip_color(&one), false), (chip_color(&one), true), (chip_color(&one), false)]); + s.tool_key(ToolKey::Right, Mods::default(), v).unwrap(); + assert_eq!(s.paint.fill, one); + + // No color swatch: the arrows are left to the canvas and the paint stays. + s.edit("t", |d, _| { + d.swatches.clear(); + Ok(()) + }) + .unwrap(); + assert!(!s.tool_claims_key(ToolKey::Right, v) && !s.tool_claims_key(ToolKey::Left, v)); + s.tool_key(ToolKey::Right, Mods::default(), v).unwrap(); + assert_eq!(s.paint.fill, one); +} + #[test] fn shape_builder_tool_alt_deletes_line_pieces() { let mut s = session(); diff --git a/crates/engine/src/tooling.rs b/crates/engine/src/tooling.rs index ae0c55ac76..c974a119e2 100644 --- a/crates/engine/src/tooling.rs +++ b/crates/engine/src/tooling.rs @@ -1164,6 +1164,12 @@ impl Session { self.plane_key(key).is_some() || self.with_tool_cx(view, |t, cx| t.claims_key(cx, key)) } + /// Does the active tool take `key`, pressed with `mods` held, ahead of the shortcuts bound to it + /// (see `Tool::claims_key_with`)? + pub fn tool_claims_key_with(&mut self, key: ToolKey, mods: Mods, view: ViewInfo) -> bool { + self.plane_key(key).is_some() || self.with_tool_cx(view, |t, cx| t.claims_key_with(cx, key, mods)) + } + /// Has the active tool started something that Escape ends while no button is pressed (see /// `Tool::ends_on_escape`)? pub fn tool_ends_on_escape(&mut self, view: ViewInfo) -> bool { diff --git a/crates/mcp/src/ui_catalog.rs b/crates/mcp/src/ui_catalog.rs index dcde7c5076..db63f1c96b 100644 --- a/crates/mcp/src/ui_catalog.rs +++ b/crates/mcp/src/ui_catalog.rs @@ -925,7 +925,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "tool.options", "Tool Options…", "", - "{tool: id} what double-clicking a tool button opens: hand → fits the artboard in the window (view.fitArtboard), zoom → 100% (view.actualSize), rotate|scale|reflect|shear → that Object › Transform dialog (dialog `rotate`, `scale`, `reflect` or `shear`; error `nothing selected` without a selection), selection|directSelection|groupSelection → the Move dialog (dialog `move`; the same error), gradient|magicWand → the Gradient or Magic Wand panel (window.panel), artboard → Artboard Options of the active artboard (dialog `artboardOptions`, fields index, name, x, y, width, height, showCenterMark, showCrossHairs; OK runs artboard.setProps, and with other artboards selected with the Artboard tool a new width or height goes to each of them too, in its `artboards` list, each resized about the reference point the form used for that size, or about its top-left corner when the size was set in the fields directly), flare → Flare Tool Options (dialog `flareOptions`, fields diameter and pathLength (pt), opacity, brightness, growth, fuzziness, longest, rayFuzziness, largest (%), rays, rings, direction (°), raysOn, ringsOn; OK runs tool.setOption {tool: \"flare\", values}: the next flare dragged out uses them, and a Flare click's dialog, which also has x, y and preview (on initially), previews one there and accepts it as one Undo step; Cancel rolls back the preview), lineSegment|arc|rectangularGrid|polarGrid → that tool's Tool Options, its click dialog without x and y (dialog kind = the tool id, `__toolOptions: true`; lineSegment: length (pt), angle (°), fillLine; arc: lengthX, lengthY (pt), reference (0–8), arcType: open|closed, baseAlong: x|y, slope (−100…100), fillArc; rectangularGrid: width, height (pt, the Default Size), reference (0–8), rows, columns, rowSkew, columnSkew (%), frame, fill; polarGrid: the same with concentric, radial, concentricSkew, radialSkew and compound for rows, columns, the skews and frame; OK runs tool.setOption {tool, values} and draws nothing: the next drag uses them (Fill Line, the arc's type, base, slope and fill, the grids' dividers, skews and switches) and a click's dialog opens at them), columnGraph|stackedColumnGraph|barGraph|stackedBarGraph|lineGraph|areaGraph|scatterGraph|pieGraph|radarGraph → Graph Type for the selected graph (dialog `command` running graph.setType; an error without one), typeOnPath|verticalTypeOnPath → Type on a Path Options for the selected type on a path (dialog `command` running type.pathOptions; error `nothing selected` without a selection, and the command's error when no type on a path is selected),symbolSprayer|symbolShifter|symbolScruncher|symbolSizer|symbolSpinner|symbolStainer|symbolScreener|symbolStyler → Symbolism Tools Options (dialog `symbolismOptions`, fields tool, diameter (pt), intensity and density (1–10), shared by the eight tools; OK runs tool.setOption {tool, values}), blend → Blend Options, perspectiveGrid → Perspective Grid Options, eyedropper → Eyedropper Options (dialog `eyedropperOptions`, fields sampleSize, pickUp, apply; OK runs eyedropper.setOptions), printTiling → resets the print tiling (print.tiling.set {reset: true}), warp|twirl|pucker|bloat|scallop|crystallize|wrinkle → that tool's Tool Options (dialog `liquifyOptions`, fields tool, width, height, angle, intensity %, usePressure, detail, simplify, simplifyOn, rate, complexity, horizontal %, vertical %, affectAnchors, affectIn, affectOut, showBrush; OK runs tool.setOption {tool, values}), pencil|paintbrush|smooth|blobBrush|eraser → that tool's Tool Options (dialog `freehandOptions`, fields tool and the options the tool keeps: fidelity (px for pencil and paintbrush, the same at any zoom; pt for smooth and blobBrush), fill, keepSelected (on for pencil and paintbrush, off for blobBrush), mergeOnlySelection (off), closeWithin and editWithin (px, 0 is off), livePreview (pencil, on: off draws a thin line while the stroke is drawn, and the path's fill and stroke on release), size (pt), angle (°, −180–180; 0) and roundness (%, 1–100; 100) of the eraser's tip; OK runs tool.setOption {tool, values}), livePaintSelection → Live Paint Selection Tool Options (dialog `livePaintSelectionOptions`, fields selectFills and selectStrokes (both on), color (the highlight color, #rrggbb or a layer color's name; #ff2a2a), width (the highlight's width, 1–20 px; 4); OK runs tool.setOption {tool: \"livePaintSelection\", values}; a color with an alpha part (#rrggbbaa or #rgba) is an error and OK changes nothing), livePaintBucket → Live Paint Bucket Options (dialog `livePaintBucketOptions`, fields paintFills (on) and paintStrokes (off), highlight (on), color (the highlight color, #rrggbb or a layer color's name; #ff2a2a), width (the face highlight's width, 1–20 px; 4; an edge's highlight is half as wide); OK runs tool.setOption {tool: \"livePaintBucket\", values}; a color with an alpha part (#rrggbbaa or #rgba) is an error and OK changes nothing), shapeBuilder → Shape Builder Tool Options (dialog `shapeBuilderOptions`, fields gapDetection (off), gapLength (pt, 0–72; 3), openFilledAsClosed (on), clickStrokeSplitsPath (off), pickColorFrom: artwork|swatches (artwork), cursorSwatchPreview (off), selection: freeform|straightLine (freeform), highlightFill (on), highlightStroke (on), highlightColor (#rrggbb or a layer color's name; #ff4f4f); OK runs tool.setOption {tool: \"shapeBuilder\", values}; a highlightColor with an alpha part (#rrggbbaa or #rgba) is an error and OK changes nothing), objectsOnPath → Objects on Path Tool Options (dialog `objectsOnPathOptions`, fields pivot (0–8, row by row from the top left; 4, the center) and rotate (°, −360–360; 0); Reset puts 4 and 0 back; Save runs tool.setOption {tool: \"objectsOnPath\", values}: the objects attached next use them, groups attached before keep theirs)", + "{tool: id} what double-clicking a tool button opens: hand → fits the artboard in the window (view.fitArtboard), zoom → 100% (view.actualSize), rotate|scale|reflect|shear → that Object › Transform dialog (dialog `rotate`, `scale`, `reflect` or `shear`; error `nothing selected` without a selection), selection|directSelection|groupSelection → the Move dialog (dialog `move`; the same error), gradient|magicWand → the Gradient or Magic Wand panel (window.panel), artboard → Artboard Options of the active artboard (dialog `artboardOptions`, fields index, name, x, y, width, height, showCenterMark, showCrossHairs; OK runs artboard.setProps, and with other artboards selected with the Artboard tool a new width or height goes to each of them too, in its `artboards` list, each resized about the reference point the form used for that size, or about its top-left corner when the size was set in the fields directly), flare → Flare Tool Options (dialog `flareOptions`, fields diameter and pathLength (pt), opacity, brightness, growth, fuzziness, longest, rayFuzziness, largest (%), rays, rings, direction (°), raysOn, ringsOn; OK runs tool.setOption {tool: \"flare\", values}: the next flare dragged out uses them, and a Flare click's dialog, which also has x, y and preview (on initially), previews one there and accepts it as one Undo step; Cancel rolls back the preview), lineSegment|arc|rectangularGrid|polarGrid → that tool's Tool Options, its click dialog without x and y (dialog kind = the tool id, `__toolOptions: true`; lineSegment: length (pt), angle (°), fillLine; arc: lengthX, lengthY (pt), reference (0–8), arcType: open|closed, baseAlong: x|y, slope (−100…100), fillArc; rectangularGrid: width, height (pt, the Default Size), reference (0–8), rows, columns, rowSkew, columnSkew (%), frame, fill; polarGrid: the same with concentric, radial, concentricSkew, radialSkew and compound for rows, columns, the skews and frame; OK runs tool.setOption {tool, values} and draws nothing: the next drag uses them (Fill Line, the arc's type, base, slope and fill, the grids' dividers, skews and switches) and a click's dialog opens at them), columnGraph|stackedColumnGraph|barGraph|stackedBarGraph|lineGraph|areaGraph|scatterGraph|pieGraph|radarGraph → Graph Type for the selected graph (dialog `command` running graph.setType; an error without one), typeOnPath|verticalTypeOnPath → Type on a Path Options for the selected type on a path (dialog `command` running type.pathOptions; error `nothing selected` without a selection, and the command's error when no type on a path is selected),symbolSprayer|symbolShifter|symbolScruncher|symbolSizer|symbolSpinner|symbolStainer|symbolScreener|symbolStyler → Symbolism Tools Options (dialog `symbolismOptions`, fields tool, diameter (pt), intensity and density (1–10), shared by the eight tools; OK runs tool.setOption {tool, values}), blend → Blend Options, perspectiveGrid → Perspective Grid Options, eyedropper → Eyedropper Options (dialog `eyedropperOptions`, fields sampleSize, pickUp, apply; OK runs eyedropper.setOptions), printTiling → resets the print tiling (print.tiling.set {reset: true}), warp|twirl|pucker|bloat|scallop|crystallize|wrinkle → that tool's Tool Options (dialog `liquifyOptions`, fields tool, width, height, angle, intensity %, usePressure, detail, simplify, simplifyOn, rate, complexity, horizontal %, vertical %, affectAnchors, affectIn, affectOut, showBrush; OK runs tool.setOption {tool, values}), pencil|paintbrush|smooth|blobBrush|eraser → that tool's Tool Options (dialog `freehandOptions`, fields tool and the options the tool keeps: fidelity (px for pencil and paintbrush, the same at any zoom; pt for smooth and blobBrush), fill, keepSelected (on for pencil and paintbrush, off for blobBrush), mergeOnlySelection (off), closeWithin and editWithin (px, 0 is off), livePreview (pencil, on: off draws a thin line while the stroke is drawn, and the path's fill and stroke on release), size (pt), angle (°, −180–180; 0) and roundness (%, 1–100; 100) of the eraser's tip; OK runs tool.setOption {tool, values}), livePaintSelection → Live Paint Selection Tool Options (dialog `livePaintSelectionOptions`, fields selectFills and selectStrokes (both on), color (the highlight color, #rrggbb or a layer color's name; #ff2a2a), width (the highlight's width, 1–20 px; 4); OK runs tool.setOption {tool: \"livePaintSelection\", values}; a color with an alpha part (#rrggbbaa or #rgba) is an error and OK changes nothing), livePaintBucket → Live Paint Bucket Options (dialog `livePaintBucketOptions`, fields paintFills (on) and paintStrokes (off), cursorSwatchPreview (on: the active fill or stroke shows above the pointer between the color swatches before and after it; Left and Right without modifiers step it through the color swatches whether it is on or off), highlight (on), color (the highlight color, #rrggbb or a layer color's name; #ff2a2a), width (the face highlight's width, 1–20 px; 4; an edge's highlight is half as wide); OK runs tool.setOption {tool: \"livePaintBucket\", values}; a color with an alpha part (#rrggbbaa or #rgba) is an error and OK changes nothing), shapeBuilder → Shape Builder Tool Options (dialog `shapeBuilderOptions`, fields gapDetection (off), gapLength (pt, 0–72; 3), openFilledAsClosed (on), clickStrokeSplitsPath (off), pickColorFrom: artwork|swatches (artwork), cursorSwatchPreview (off), selection: freeform|straightLine (freeform), highlightFill (on), highlightStroke (on), highlightColor (#rrggbb or a layer color's name; #ff4f4f); OK runs tool.setOption {tool: \"shapeBuilder\", values}; a highlightColor with an alpha part (#rrggbbaa or #rgba) is an error and OK changes nothing), objectsOnPath → Objects on Path Tool Options (dialog `objectsOnPathOptions`, fields pivot (0–8, row by row from the top left; 4, the center) and rotate (°, −360–360; 0); Reset puts 4 and 0 back; Save runs tool.setOption {tool: \"objectsOnPath\", values}: the objects attached next use them, groups attached before keep theirs)", ), ( "ui.colorGuideOptions", diff --git a/crates/tools/src/builder.rs b/crates/tools/src/builder.rs index 1c5a895161..8b9157bc25 100644 --- a/crates/tools/src/builder.rs +++ b/crates/tools/src/builder.rs @@ -143,6 +143,38 @@ fn step_swatch(list: &[&Swatch], fill: &Paint, last: Option, forward: boo } } +/// Is no Shift, Alt, Cmd or Ctrl held? The arrows step swatches only then. +fn plain(mods: Mods) -> bool { + !(mods.shift || mods.alt || mods.cmd || mods.ctrl) +} + +/// The color swatch the Right (`forward`) or Left arrow key picks for the current `paint`, with its +/// index in [`color_swatches`] (see [`step_swatch`]); None when the document has no color swatch. +fn pick_swatch(doc: &Document, paint: &Paint, last: Option, forward: bool) -> Option<(usize, String)> { + let list = color_swatches(doc); + step_swatch(&list, paint, last, forward).and_then(|i| Some((i, list.get(i)?.name.clone()))) +} + +/// The cursor swatch preview for the current `paint` with the pointer at `at`: the swatch before +/// it, `paint` itself (ringed, while it is a solid color) and the swatch after it, side by side above +/// the pointer. With `matched_only`, the swatches beside it show only while `paint` is one of the +/// color swatches; otherwise the last and the first swatch stand beside a color that is none of them. +fn swatch_chips(cx: &ToolContext, at: Point, paint: &Paint, last: Option, matched_only: bool) -> Vec { + let list = color_swatches(cx.doc); + let rgba = |p: &Paint| match p { + Paint::Solid { color, .. } => Some(color.to_rgba8(1.0)), + _ => None, + }; + let beside = !matched_only || swatch_index(&list, paint, last).is_some(); + let chip = |forward| beside.then(|| step_swatch(&list, paint, last, forward)).flatten().and_then(|i| rgba(&list.get(i)?.paint)); + let (before, after) = (chip(false), chip(true)); + let y = at.y - cx.tol(22.0); + [(before, -16.0, false), (rgba(paint), 0.0, true), (after, 16.0, false)] + .into_iter() + .filter_map(|(color, dx, selected)| Some(Overlay::Swatch { p: Point::new(at.x + cx.tol(dx), y), color: color?, selected })) + .collect() +} + /// Shape Builder tool: drag across regions to merge them. Holding Alt (Option) switches to erase /// mode while it is held: the pointer shows a minus, what a click or drag would delete highlights in /// red, and the gesture deletes the regions it touches, the pieces of open paths and the edges @@ -425,20 +457,7 @@ impl ShapeBuilderTool { /// The cursor swatch preview: the swatch before the current fill, the current fill (ringed) /// and the swatch after it, side by side above the pointer. fn swatch_chips(&self, cx: &ToolContext) -> Vec { - let Some(at) = self.at else { return vec![] }; - let list = color_swatches(cx.doc); - let fill = &cx.paint.fill; - let rgba = |p: &Paint| match p { - Paint::Solid { color, .. } => Some(color.to_rgba8(1.0)), - _ => None, - }; - let chip = |forward| step_swatch(&list, fill, self.picked, forward).and_then(|i| rgba(&list.get(i)?.paint)); - let (before, after) = (chip(false), chip(true)); - let y = at.y - cx.tol(22.0); - [(before, -16.0, false), (rgba(fill), 0.0, true), (after, 16.0, false)] - .into_iter() - .filter_map(|(color, dx, selected)| Some(Overlay::Swatch { p: Point::new(at.x + cx.tol(dx), y), color: color?, selected })) - .collect() + self.at.map(|at| swatch_chips(cx, at, &cx.paint.fill, self.picked, false)).unwrap_or_default() } } @@ -712,9 +731,15 @@ impl Tool for ShapeBuilderTool { (self.opts.swatches && !self.dragging && matches!(key, ToolKey::Left | ToolKey::Right)) || (self.dragging && key == ToolKey::Escape) } - fn key(&mut self, cx: &ToolContext, key: ToolKey, _mods: Mods) -> Vec { + /// The arrows pick swatches only without modifiers; Shift, Alt and Cmd/Ctrl arrows keep their + /// shortcuts. + fn claims_key_with(&self, cx: &ToolContext, key: ToolKey, mods: Mods) -> bool { + self.claims_key(cx, key) && (key == ToolKey::Escape || plain(mods)) + } + + fn key(&mut self, cx: &ToolContext, key: ToolKey, mods: Mods) -> Vec { // While a merge is being computed, the arrows leave the current fill it takes as it was. - if !self.claims_key(cx, key) || self.editing() { + if !self.claims_key_with(cx, key, mods) || self.editing() { return vec![]; } if key == ToolKey::Escape { @@ -726,13 +751,12 @@ impl Tool for ShapeBuilderTool { (self.marquee, self.hover, self.far) = (None, None, false); return vec![]; } - let list = color_swatches(cx.doc); - let Some((i, s)) = step_swatch(&list, &cx.paint.fill, self.picked, key == ToolKey::Right).and_then(|i| Some((i, list.get(i)?))) else { + let Some((i, name)) = pick_swatch(cx.doc, &cx.paint.fill, self.picked, key == ToolKey::Right) else { return vec![]; }; self.picked = Some(i); // The current fill only (no `ids`): the selected art keeps its own. - vec![Action::Exec("paint.setFill".into(), json!({ "swatch": s.name, "ids": [] }))] + vec![Action::Exec("paint.setFill".into(), json!({ "swatch": name, "ids": [] }))] } fn options(&self) -> Value { diff --git a/crates/tools/src/builder/live_paint.rs b/crates/tools/src/builder/live_paint.rs index e2d6c9f77e..885d82526a 100644 --- a/crates/tools/src/builder/live_paint.rs +++ b/crates/tools/src/builder/live_paint.rs @@ -3,7 +3,7 @@ use super::lookup::{is_live_paint, live_paint_at, live_paint_at_where, visible_e use super::map::{HIGHLIGHT_RED, sorted_roots}; use super::{ Action, Arc, BezPath, CLICK_TOL_PX, Cursor, Document, EDGE_NAME, FACE_NAME, FillRule, Mods, Node, NodeId, Overlay, Point, PointerEvent, - PointerKind, Rect, Tool, ToolContext, ToolKey, Value, json, json_ids, path_contains, + PointerKind, Rect, Tool, ToolContext, ToolKey, Value, color_swatches, json, json_ids, path_contains, pick_swatch, plain, swatch_chips, }; // ---------- Live Paint Bucket ---------- @@ -171,6 +171,14 @@ enum LpPaint { /// click paints nothing. With Highlight off, the face or edge under the pointer is not /// highlighted. The cursor is a filled bucket where a click fills a face, a brush where it paints /// an edge and an empty bucket where it paints nothing; with Alt it is the Eyedropper. +/// +/// The Left and Right arrow keys without Shift, Alt or Cmd/Ctrl set the active paint (the fill, +/// or the stroke when the Stroke proxy is in front) to the color swatch before or after it in the +/// Swatches panel, wrapping around; from a color that is no swatch, Right picks the first swatch +/// and Left the last. The selected art keeps its paint. With `cursorSwatchPreview` (on), the +/// active paint shows as a chip above the pointer between the swatches before and after it, +/// except during a fill drag or with Alt held; a solid color that is no swatch shows alone, and a +/// paint that is not a solid color shows no chip. pub struct LivePaintBucketTool { pub(super) cache: RegionCache, pub(super) hover: LpHover, @@ -183,6 +191,11 @@ pub struct LivePaintBucketTool { highlight: bool, color: [u8; 3], width: f64, + /// Cursor Swatch Preview. + preview: bool, + /// The index in [`color_swatches`] of the swatch the arrow keys picked last for the fill and for + /// the stroke. + picked: [Option; 2], } /// Maximum points in one Live Paint fill path chunk. @@ -200,11 +213,24 @@ impl Default for LivePaintBucketTool { highlight: true, color: HIGHLIGHT_RED, width: LP_BUCKET_WIDTH, + preview: true, + picked: [None; 2], } } } impl LivePaintBucketTool { + /// The active paint the arrow keys step and the preview shows: the fill, or the stroke when the + /// Stroke proxy is in front. + fn active_paint<'a>(cx: &'a ToolContext) -> &'a vectorcraft_color::Paint { + if cx.fill_active { &cx.paint.fill } else { &cx.paint.stroke } + } + + /// The slot of [`Self::picked`] for the active paint. + fn picked_slot(cx: &ToolContext) -> usize { + usize::from(!cx.fill_active) + } + fn begin_fill(&mut self, params: Value, p: Point) -> Vec { self.fill_drag = Some((params.clone(), vec![vec![p]])); vec![Action::Begin("Live Paint Bucket".into()), Action::Preview("livePaint.fill".into(), params)] @@ -387,24 +413,57 @@ impl Tool for LivePaintBucketTool { } } - fn key(&mut self, _cx: &ToolContext, key: ToolKey, _mods: Mods) -> Vec { - if key == ToolKey::Escape && self.fill_drag.take().is_some() { vec![Action::Cancel] } else { vec![] } + /// Escape during a fill drag cancels it. Left and Right pick the color swatch before or after + /// the active paint, while no drag is on. + fn key(&mut self, cx: &ToolContext, key: ToolKey, mods: Mods) -> Vec { + if key == ToolKey::Escape { + return if self.fill_drag.take().is_some() { vec![Action::Cancel] } else { vec![] }; + } + if !self.claims_key_with(cx, key, mods) { + return vec![]; + } + let slot = Self::picked_slot(cx); + let Some((i, name)) = pick_swatch(cx.doc, Self::active_paint(cx), self.picked[slot], key == ToolKey::Right) else { + return vec![]; + }; + self.picked[slot] = Some(i); + // The active paint only (no `ids`): the selected art keeps its own. + let cmd = if cx.fill_active { "paint.setFill" } else { "paint.setStroke" }; + vec![Action::Exec(cmd.into(), json!({ "swatch": name, "ids": [] }))] } fn busy(&self) -> bool { self.fill_drag.is_some() } - fn claims_key(&self, _cx: &ToolContext, key: ToolKey) -> bool { - key == ToolKey::Escape && self.busy() + fn claims_key(&self, cx: &ToolContext, key: ToolKey) -> bool { + match key { + ToolKey::Escape => self.busy(), + ToolKey::Left | ToolKey::Right => !self.busy() && !color_swatches(cx.doc).is_empty(), + _ => false, + } + } + + /// The arrows pick swatches only without modifiers; Shift, Alt and Cmd/Ctrl arrows keep their + /// shortcuts. + fn claims_key_with(&self, cx: &ToolContext, key: ToolKey, mods: Mods) -> bool { + self.claims_key(cx, key) && (key == ToolKey::Escape || plain(mods)) } fn deactivate(&mut self, _cx: &ToolContext) -> Vec { if self.fill_drag.take().is_some() { vec![Action::Commit] } else { vec![] } } - fn overlays(&self, _cx: &ToolContext) -> Vec { - lp_overlays(&self.hover, &self.cache, self.color, self.width as f32, (self.width * LP_BUCKET_EDGE_FRACTION) as f32) + fn overlays(&self, cx: &ToolContext) -> Vec { + let mut out = lp_overlays(&self.hover, &self.cache, self.color, self.width as f32, (self.width * LP_BUCKET_EDGE_FRACTION) as f32); + // No chips during a fill drag, or while Alt makes the tool sample with the eyedropper. + if self.preview + && !self.busy() + && let Some((at, _, false)) = self.at + { + out.extend(swatch_chips(cx, at, Self::active_paint(cx), self.picked[Self::picked_slot(cx)], true)); + } + out } fn cursor(&self, cx: &ToolContext, p: Point, m: Mods) -> Cursor { @@ -416,6 +475,7 @@ impl Tool for LivePaintBucketTool { json!({ "paintFills": self.fills, "paintStrokes": self.strokes, + "cursorSwatchPreview": self.preview, "highlight": self.highlight, "color": format!("#{r:02x}{g:02x}{b:02x}"), "width": self.width, @@ -426,6 +486,7 @@ impl Tool for LivePaintBucketTool { match key { "paintFills" => self.fills = v.as_bool().unwrap_or(self.fills), "paintStrokes" => self.strokes = v.as_bool().unwrap_or(self.strokes), + "cursorSwatchPreview" => self.preview = v.as_bool().unwrap_or(self.preview), "highlight" => self.highlight = v.as_bool().unwrap_or(self.highlight), "color" => self.color = lp_color(v).unwrap_or(self.color), "width" => { diff --git a/crates/tools/src/builder/tests.rs b/crates/tools/src/builder/tests.rs index 298e6a1907..14976f03c9 100644 --- a/crates/tools/src/builder/tests.rs +++ b/crates/tools/src/builder/tests.rs @@ -1113,7 +1113,12 @@ fn bucket_options_choose_what_a_click_paints() { }; let (face, edge) = (Some(Rect::new(50.0, 0.0, 100.0, 50.0)), Some(Rect::new(50.0, 0.0, 50.0, 50.0))); let mut t = LivePaintBucketTool::default(); - assert_eq!(t.options(), json!({"paintFills": true, "paintStrokes": false, "highlight": true, "color": "#ff2a2a", "width": 4.0})); + assert_eq!( + t.options(), + json!({"paintFills": true, "paintStrokes": false, "cursorSwatchPreview": true, "highlight": true, "color": "#ff2a2a", "width": 4.0}) + ); + // The highlight alone, without the swatch chips. + t.set_option("cursorSwatchPreview", &json!(false)); // Paint Fills alone: 1 pt from the edge, a click fills the face and Shift-click strokes the edge. assert_eq!((click(&mut t, 51.0, Mods::default()), hovered(&mut t, 51.0, Mods::default())), (fill(51.0), face)); assert_eq!((click(&mut t, 51.0, shift), hovered(&mut t, 51.0, shift)), (stroke(51.0), edge)); @@ -1231,6 +1236,7 @@ fn bucket_fills_a_region_of_a_selection_that_mixes_a_group_and_paths() { s.set([g]); let live = cx(&d, &s, &p); let mut t = LivePaintBucketTool::default(); + t.set_option("cursorSwatchPreview", &json!(false)); t.pointer(&live, &ev(PointerKind::Move, pos.x, pos.y)); t.poll(&live, true); assert!(t.overlays(&live).is_empty()); @@ -1245,6 +1251,7 @@ fn bucket_strokes_alone_paints_and_highlights_no_region_of_the_selection() { let p = paint(); let cx = cx(&d, &s, &p); let mut t = LivePaintBucketTool::default(); + t.set_option("cursorSwatchPreview", &json!(false)); t.pointer(&cx, &ev(PointerKind::Move, 75.0, 50.0)); t.poll(&cx, true); assert!(!t.overlays(&cx).is_empty(), "Paint Fills highlights the region"); @@ -1263,6 +1270,7 @@ fn bucket_options_set_the_highlight() { let p = paint(); let cx = cx(&d, &s, &p); let mut t = LivePaintBucketTool::default(); + t.set_option("cursorSwatchPreview", &json!(false)); let highlight = |t: &mut LivePaintBucketTool| { t.pointer(&cx, &ev(PointerKind::Move, 75.0, 25.0)); match t.overlays(&cx).as_slice() { @@ -1299,6 +1307,7 @@ fn bucket_highlights_an_edge_thinner_than_a_face() { let p = paint(); let cx = cx(&d, &s, &p); let mut t = LivePaintBucketTool::default(); + t.set_option("cursorSwatchPreview", &json!(false)); let width = |t: &mut LivePaintBucketTool, x: f64, shift: bool| { t.pointer(&cx, &ev(PointerKind::Move, x, 25.0).with_mods(Mods { shift, ..Default::default() })); match t.overlays(&cx).as_slice() { @@ -1517,7 +1526,7 @@ fn bucket_hover_builds_a_large_map_on_another_thread() { poll_until_ready(&mut t, &cx); assert!(matches!(t.hover, LpHover::Region(_)), "{:?}", t.hover); assert_eq!((t.cache.builds, t.cache.background), (1, 1)); - assert_eq!(t.overlays(&cx).len(), 1); + assert_eq!(t.overlays(&cx).iter().filter(|o| matches!(o, Overlay::Path { .. })).count(), 1); } #[test] @@ -1883,6 +1892,7 @@ fn live_paint_edges_outside_the_faces_come_from_the_nearest_visible_group() { }; assert_eq!(lp_click(&mut sel, &cx, ev(PointerKind::Down, x, y).with_mods(alt)), vec![picked]); let mut bucket = LivePaintBucketTool::default(); + bucket.set_option("cursorSwatchPreview", &json!(false)); bucket.pointer(&cx, &ev(PointerKind::Move, x, y).with_mods(shift)); assert_eq!(edge_x(&bucket.overlays(&cx)), want.map(|w| w.2)); let stroked = want.map(|(g, ..)| Action::Exec("livePaint.strokeEdge".into(), json!({ "group": g.0, "point": [x, y], "tolerance": 4.0 }))); @@ -1926,6 +1936,7 @@ fn live_paint_tools_skip_hidden_edges() { }; assert_eq!(lp_click(&mut sel, &cx, ev(PointerKind::Down, x, 50.0).with_mods(alt)), vec![picked], "x {x}"); let mut bucket = LivePaintBucketTool::default(); + bucket.set_option("cursorSwatchPreview", &json!(false)); bucket.pointer(&cx, &ev(PointerKind::Move, x, 50.0).with_mods(shift)); assert_eq!(edge_x(&bucket.overlays(&cx)), want.map(|w| w.2), "x {x}"); let stroked = want.map(|(g, ..)| Action::Exec("livePaint.strokeEdge".into(), json!({ "group": g.0, "point": [x, 50.0], "tolerance": 4.0 }))); @@ -1953,6 +1964,7 @@ fn live_paint_tools_skip_hidden_faces() { }; // Where the faces overlap, both tools take the back group's face under the hidden one. let mut bucket = LivePaintBucketTool::default(); + bucket.set_option("cursorSwatchPreview", &json!(false)); bucket.pointer(&cx, &ev(PointerKind::Move, 75.0, 50.0)); assert_eq!(part(&bucket.overlays(&cx)), Some(Rect::new(0.0, 0.0, 100.0, 100.0))); let fill = Action::Preview("livePaint.fill".into(), json!({ "group": back.0, "point": [75.0, 50.0] })); diff --git a/crates/tools/src/lib.rs b/crates/tools/src/lib.rs index 17e6f038e9..33f2b7bd3c 100644 --- a/crates/tools/src/lib.rs +++ b/crates/tools/src/lib.rs @@ -182,6 +182,11 @@ pub trait Tool: Send { fn claims_key(&self, _cx: &ToolContext, _key: ToolKey) -> bool { false } + /// [`Self::claims_key`] for a press with `mods` held. By default the modifiers do not matter; + /// a tool that takes a key only without modifiers leaves the modified presses to the shortcuts. + fn claims_key_with(&self, cx: &ToolContext, key: ToolKey, _mods: Mods) -> bool { + self.claims_key(cx, key) + } /// Has the tool started something that Escape ends while no button is pressed (a path being /// drawn, the Blend tool's first pick)? That Escape ends nothing else, such as isolation mode or /// a rotated view. diff --git a/crates/tools/src/settings.rs b/crates/tools/src/settings.rs index e7b9f9a5c4..a4d1b1a64f 100644 --- a/crates/tools/src/settings.rs +++ b/crates/tools/src/settings.rs @@ -46,7 +46,7 @@ const ROWS: &[Row] = &[ // Live Paint Selection Tool Options (`builder::LivePaintSelectionTool`). (&["livePaintSelection"], None, &["selectFills", "selectStrokes", "color", "width"]), // Live Paint Bucket Options (`builder::LivePaintBucketTool`). - (&["livePaintBucket"], None, &["paintFills", "paintStrokes", "highlight", "color", "width"]), + (&["livePaintBucket"], None, &["paintFills", "paintStrokes", "cursorSwatchPreview", "highlight", "color", "width"]), (&["polygon"], None, &["sides"]), (&["star"], None, &["points"]), (&["freeTransform"], None, &["constrain"]), diff --git a/crates/ui-egui/src/canvas/tests.rs b/crates/ui-egui/src/canvas/tests.rs index d6df378c87..c4d324ad2b 100644 --- a/crates/ui-egui/src/canvas/tests.rs +++ b/crates/ui-egui/src/canvas/tests.rs @@ -680,9 +680,17 @@ fn bucket_modifier_only_hover_updates_the_visible_cursor() { frame(app, &ctx, events); app.canvas.hover_cursor.map(|(_, _, cursor)| cursor) }; + // The cursor swatch preview's chips go and come back with Alt too. + let chips = |app: &mut VectorcraftApp| { + let view = app.view_info(); + app.session.overlays(view).iter().filter(|o| matches!(o, vectorcraft_tools::Overlay::Swatch { .. })).count() + }; assert_eq!(shown(&mut app, vec![Event::PointerMoved(at)]), Some(Cursor::LivePaintBucket)); + assert!(chips(&mut app) > 0); assert_eq!(shown(&mut app, vec![Event::ModifiersChanged(Modifiers::ALT)]), Some(Cursor::Eyedropper)); + assert_eq!(chips(&mut app), 0); assert_eq!(shown(&mut app, vec![Event::ModifiersChanged(Modifiers::NONE)]), Some(Cursor::LivePaintBucket)); + assert!(chips(&mut app) > 0); } /// The Image Trace panel's eye-button hold ends when the pointer is released, even when the diff --git a/crates/ui-egui/src/dialogs/live_paint_bucket.rs b/crates/ui-egui/src/dialogs/live_paint_bucket.rs index c040cdcc31..f515c3d904 100644 --- a/crates/ui-egui/src/dialogs/live_paint_bucket.rs +++ b/crates/ui-egui/src/dialogs/live_paint_bucket.rs @@ -4,7 +4,8 @@ //! `tool.setOption {tool: "livePaintBucket", values}`. //! //! Fields: `paintFills` and `paintStrokes` (the faces and the edges of Live Paint groups), -//! `highlight` (whether the face or edge under the pointer is outlined), `color` (the highlight +//! `cursorSwatchPreview` (whether the active paint and the color swatches beside it show above the +//! pointer), `highlight` (whether the face or edge under the pointer is outlined), `color` (the highlight //! color: `#rrggbb`, or a layer color's name such as "Red") and `width` (the width of a face's //! highlight in screen pixels; an edge's highlight is half as wide). @@ -25,7 +26,7 @@ pub const KIND: &str = "livePaintBucketOptions"; const TOOL: &str = "livePaintBucket"; /// The options the dialog edits. -const KEYS: [&str; 5] = ["paintFills", "paintStrokes", "highlight", "color", "width"]; +const KEYS: [&str; 6] = ["paintFills", "paintStrokes", "cursorSwatchPreview", "highlight", "color", "width"]; /// Width of the label column. const LABEL_W: f32 = 60.0; @@ -48,6 +49,7 @@ pub fn open(app: &mut VectorcraftApp) -> Result { fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { form::check(ui, d, "paintFills", tl!("Paint Fills")); form::check(ui, d, "paintStrokes", tl!("Paint Strokes")); + form::check(ui, d, "cursorSwatchPreview", tl!("Cursor Swatch Preview")); ui.add_space(6.0); form::check(ui, d, "highlight", tl!("Highlight")); let color = resolve(&d.str("color")).unwrap_or(Color::BLACK); @@ -120,17 +122,18 @@ mod tests { crate::toolbar::open_options(&mut app, TOOL).unwrap(); let text = painted(&mut app); - for label in ["Live Paint Bucket Options", "Paint Fills", "Paint Strokes", "Highlight", "Color:", "Width:"] { + for label in ["Live Paint Bucket Options", "Paint Fills", "Paint Strokes", "Cursor Swatch Preview", "Highlight", "Color:", "Width:"] { assert!(text.contains(label), "{label} in {text}"); } let d = app.ui.dialog.as_mut().unwrap(); assert_eq!(d.kind, KIND); assert_eq!( - (d.bool("paintFills"), d.bool("paintStrokes"), d.bool("highlight"), d.str("color"), d.f64("width", 0.0)), - (true, false, true, "#ff2a2a".to_string(), 4.0) + (d.bool("paintFills"), d.bool("paintStrokes"), d.bool("cursorSwatchPreview"), d.bool("highlight"), d.str("color"), d.f64("width", 0.0)), + (true, false, true, true, "#ff2a2a".to_string(), 4.0) ); d.fields.insert("paintFills".into(), json!(false)); d.fields.insert("paintStrokes".into(), json!(true)); + d.fields.insert("cursorSwatchPreview".into(), json!(false)); d.fields.insert("color".into(), json!("Green")); d.fields.insert("width".into(), json!(8)); super::super::confirm(&mut app).unwrap(); @@ -141,10 +144,13 @@ mod tests { // They last while another tool is chosen, and the dialog opens on them again. app.select_tool("selection"); let o = app.session.tool_options_of(TOOL); - assert_eq!((o["paintFills"].as_bool(), o["paintStrokes"].as_bool(), o["color"].as_str()), (Some(false), Some(true), Some("#4fff4f"))); + assert_eq!( + (o["paintFills"].as_bool(), o["paintStrokes"].as_bool(), o["cursorSwatchPreview"].as_bool(), o["color"].as_str()), + (Some(false), Some(true), Some(false), Some("#4fff4f")) + ); app.run("tool.options", json!({"tool": TOOL})).unwrap(); let d = app.ui.dialog.as_mut().unwrap(); - assert_eq!((d.bool("paintStrokes"), d.str("color")), (true, "#4fff4f".to_string())); + assert_eq!((d.bool("paintStrokes"), d.bool("cursorSwatchPreview"), d.str("color")), (true, false, "#4fff4f".to_string())); // Highlight off: the tool outlines nothing under the pointer. d.fields.insert("highlight".into(), json!(false)); super::super::confirm(&mut app).unwrap(); diff --git a/crates/ui-egui/src/menus/ui_commands.rs b/crates/ui-egui/src/menus/ui_commands.rs index 925d7adf3b..a565bcaaf2 100644 --- a/crates/ui-egui/src/menus/ui_commands.rs +++ b/crates/ui-egui/src/menus/ui_commands.rs @@ -926,7 +926,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "tool.options", "Tool Options…", "", - "{tool: id} what double-clicking a tool button opens: hand → fits the artboard in the window (view.fitArtboard), zoom → 100% (view.actualSize), rotate|scale|reflect|shear → that Object › Transform dialog (dialog `rotate`, `scale`, `reflect` or `shear`; error `nothing selected` without a selection), selection|directSelection|groupSelection → the Move dialog (dialog `move`; the same error), gradient|magicWand → the Gradient or Magic Wand panel (window.panel), artboard → Artboard Options of the active artboard (dialog `artboardOptions`, fields index, name, x, y, width, height, showCenterMark, showCrossHairs; OK runs artboard.setProps, and with other artboards selected with the Artboard tool a new width or height goes to each of them too, in its `artboards` list, each resized about the reference point the form used for that size, or about its top-left corner when the size was set in the fields directly), flare → Flare Tool Options (dialog `flareOptions`, fields diameter and pathLength (pt), opacity, brightness, growth, fuzziness, longest, rayFuzziness, largest (%), rays, rings, direction (°), raysOn, ringsOn; OK runs tool.setOption {tool: \"flare\", values}: the next flare dragged out uses them, and a Flare click's dialog, which also has x, y and preview (on initially), previews one there and accepts it as one Undo step; Cancel rolls back the preview), lineSegment|arc|rectangularGrid|polarGrid → that tool's Tool Options, its click dialog without x and y (dialog kind = the tool id, `__toolOptions: true`; lineSegment: length (pt), angle (°), fillLine; arc: lengthX, lengthY (pt), reference (0–8), arcType: open|closed, baseAlong: x|y, slope (−100…100), fillArc; rectangularGrid: width, height (pt, the Default Size), reference (0–8), rows, columns, rowSkew, columnSkew (%), frame, fill; polarGrid: the same with concentric, radial, concentricSkew, radialSkew and compound for rows, columns, the skews and frame; OK runs tool.setOption {tool, values} and draws nothing: the next drag uses them (Fill Line, the arc's type, base, slope and fill, the grids' dividers, skews and switches) and a click's dialog opens at them), columnGraph|stackedColumnGraph|barGraph|stackedBarGraph|lineGraph|areaGraph|scatterGraph|pieGraph|radarGraph → Graph Type for the selected graph (dialog `command` running graph.setType; an error without one), typeOnPath|verticalTypeOnPath → Type on a Path Options for the selected type on a path (dialog `command` running type.pathOptions; error `nothing selected` without a selection, and the command's error when no type on a path is selected),symbolSprayer|symbolShifter|symbolScruncher|symbolSizer|symbolSpinner|symbolStainer|symbolScreener|symbolStyler → Symbolism Tools Options (dialog `symbolismOptions`, fields tool, diameter (pt), intensity and density (1–10), shared by the eight tools; OK runs tool.setOption {tool, values}), blend → Blend Options, perspectiveGrid → Perspective Grid Options, eyedropper → Eyedropper Options (dialog `eyedropperOptions`, fields sampleSize, pickUp, apply; OK runs eyedropper.setOptions), printTiling → resets the print tiling (print.tiling.set {reset: true}), warp|twirl|pucker|bloat|scallop|crystallize|wrinkle → that tool's Tool Options (dialog `liquifyOptions`, fields tool, width, height, angle, intensity %, usePressure, detail, simplify, simplifyOn, rate, complexity, horizontal %, vertical %, affectAnchors, affectIn, affectOut, showBrush; OK runs tool.setOption {tool, values}), pencil|paintbrush|smooth|blobBrush|eraser → that tool's Tool Options (dialog `freehandOptions`, fields tool and the options the tool keeps: fidelity (px for pencil and paintbrush, the same at any zoom; pt for smooth and blobBrush), fill, keepSelected (on for pencil and paintbrush, off for blobBrush), mergeOnlySelection (off), closeWithin and editWithin (px, 0 is off), livePreview (pencil, on: off draws a thin line while the stroke is drawn, and the path's fill and stroke on release), size (pt), angle (°, −180–180; 0) and roundness (%, 1–100; 100) of the eraser's tip; OK runs tool.setOption {tool, values}), livePaintSelection → Live Paint Selection Tool Options (dialog `livePaintSelectionOptions`, fields selectFills and selectStrokes (both on), color (the highlight color, #rrggbb or a layer color's name; #ff2a2a), width (the highlight's width, 1–20 px; 4); OK runs tool.setOption {tool: \"livePaintSelection\", values}; a color with an alpha part (#rrggbbaa or #rgba) is an error and OK changes nothing), livePaintBucket → Live Paint Bucket Options (dialog `livePaintBucketOptions`, fields paintFills (on) and paintStrokes (off), highlight (on), color (the highlight color, #rrggbb or a layer color's name; #ff2a2a), width (the face highlight's width, 1–20 px; 4; an edge's highlight is half as wide); OK runs tool.setOption {tool: \"livePaintBucket\", values}; a color with an alpha part (#rrggbbaa or #rgba) is an error and OK changes nothing), shapeBuilder → Shape Builder Tool Options (dialog `shapeBuilderOptions`, fields gapDetection (off), gapLength (pt, 0–72; 3), openFilledAsClosed (on), clickStrokeSplitsPath (off), pickColorFrom: artwork|swatches (artwork), cursorSwatchPreview (off), selection: freeform|straightLine (freeform), highlightFill (on), highlightStroke (on), highlightColor (#rrggbb or a layer color's name; #ff4f4f); OK runs tool.setOption {tool: \"shapeBuilder\", values}; a highlightColor with an alpha part (#rrggbbaa or #rgba) is an error and OK changes nothing), objectsOnPath → Objects on Path Tool Options (dialog `objectsOnPathOptions`, fields pivot (0–8, row by row from the top left; 4, the center) and rotate (°, −360–360; 0); Reset puts 4 and 0 back; Save runs tool.setOption {tool: \"objectsOnPath\", values}: the objects attached next use them, groups attached before keep theirs)", + "{tool: id} what double-clicking a tool button opens: hand → fits the artboard in the window (view.fitArtboard), zoom → 100% (view.actualSize), rotate|scale|reflect|shear → that Object › Transform dialog (dialog `rotate`, `scale`, `reflect` or `shear`; error `nothing selected` without a selection), selection|directSelection|groupSelection → the Move dialog (dialog `move`; the same error), gradient|magicWand → the Gradient or Magic Wand panel (window.panel), artboard → Artboard Options of the active artboard (dialog `artboardOptions`, fields index, name, x, y, width, height, showCenterMark, showCrossHairs; OK runs artboard.setProps, and with other artboards selected with the Artboard tool a new width or height goes to each of them too, in its `artboards` list, each resized about the reference point the form used for that size, or about its top-left corner when the size was set in the fields directly), flare → Flare Tool Options (dialog `flareOptions`, fields diameter and pathLength (pt), opacity, brightness, growth, fuzziness, longest, rayFuzziness, largest (%), rays, rings, direction (°), raysOn, ringsOn; OK runs tool.setOption {tool: \"flare\", values}: the next flare dragged out uses them, and a Flare click's dialog, which also has x, y and preview (on initially), previews one there and accepts it as one Undo step; Cancel rolls back the preview), lineSegment|arc|rectangularGrid|polarGrid → that tool's Tool Options, its click dialog without x and y (dialog kind = the tool id, `__toolOptions: true`; lineSegment: length (pt), angle (°), fillLine; arc: lengthX, lengthY (pt), reference (0–8), arcType: open|closed, baseAlong: x|y, slope (−100…100), fillArc; rectangularGrid: width, height (pt, the Default Size), reference (0–8), rows, columns, rowSkew, columnSkew (%), frame, fill; polarGrid: the same with concentric, radial, concentricSkew, radialSkew and compound for rows, columns, the skews and frame; OK runs tool.setOption {tool, values} and draws nothing: the next drag uses them (Fill Line, the arc's type, base, slope and fill, the grids' dividers, skews and switches) and a click's dialog opens at them), columnGraph|stackedColumnGraph|barGraph|stackedBarGraph|lineGraph|areaGraph|scatterGraph|pieGraph|radarGraph → Graph Type for the selected graph (dialog `command` running graph.setType; an error without one), typeOnPath|verticalTypeOnPath → Type on a Path Options for the selected type on a path (dialog `command` running type.pathOptions; error `nothing selected` without a selection, and the command's error when no type on a path is selected),symbolSprayer|symbolShifter|symbolScruncher|symbolSizer|symbolSpinner|symbolStainer|symbolScreener|symbolStyler → Symbolism Tools Options (dialog `symbolismOptions`, fields tool, diameter (pt), intensity and density (1–10), shared by the eight tools; OK runs tool.setOption {tool, values}), blend → Blend Options, perspectiveGrid → Perspective Grid Options, eyedropper → Eyedropper Options (dialog `eyedropperOptions`, fields sampleSize, pickUp, apply; OK runs eyedropper.setOptions), printTiling → resets the print tiling (print.tiling.set {reset: true}), warp|twirl|pucker|bloat|scallop|crystallize|wrinkle → that tool's Tool Options (dialog `liquifyOptions`, fields tool, width, height, angle, intensity %, usePressure, detail, simplify, simplifyOn, rate, complexity, horizontal %, vertical %, affectAnchors, affectIn, affectOut, showBrush; OK runs tool.setOption {tool, values}), pencil|paintbrush|smooth|blobBrush|eraser → that tool's Tool Options (dialog `freehandOptions`, fields tool and the options the tool keeps: fidelity (px for pencil and paintbrush, the same at any zoom; pt for smooth and blobBrush), fill, keepSelected (on for pencil and paintbrush, off for blobBrush), mergeOnlySelection (off), closeWithin and editWithin (px, 0 is off), livePreview (pencil, on: off draws a thin line while the stroke is drawn, and the path's fill and stroke on release), size (pt), angle (°, −180–180; 0) and roundness (%, 1–100; 100) of the eraser's tip; OK runs tool.setOption {tool, values}), livePaintSelection → Live Paint Selection Tool Options (dialog `livePaintSelectionOptions`, fields selectFills and selectStrokes (both on), color (the highlight color, #rrggbb or a layer color's name; #ff2a2a), width (the highlight's width, 1–20 px; 4); OK runs tool.setOption {tool: \"livePaintSelection\", values}; a color with an alpha part (#rrggbbaa or #rgba) is an error and OK changes nothing), livePaintBucket → Live Paint Bucket Options (dialog `livePaintBucketOptions`, fields paintFills (on) and paintStrokes (off), cursorSwatchPreview (on: the active fill or stroke shows above the pointer between the color swatches before and after it; Left and Right without modifiers step it through the color swatches whether it is on or off), highlight (on), color (the highlight color, #rrggbb or a layer color's name; #ff2a2a), width (the face highlight's width, 1–20 px; 4; an edge's highlight is half as wide); OK runs tool.setOption {tool: \"livePaintBucket\", values}; a color with an alpha part (#rrggbbaa or #rgba) is an error and OK changes nothing), shapeBuilder → Shape Builder Tool Options (dialog `shapeBuilderOptions`, fields gapDetection (off), gapLength (pt, 0–72; 3), openFilledAsClosed (on), clickStrokeSplitsPath (off), pickColorFrom: artwork|swatches (artwork), cursorSwatchPreview (off), selection: freeform|straightLine (freeform), highlightFill (on), highlightStroke (on), highlightColor (#rrggbb or a layer color's name; #ff4f4f); OK runs tool.setOption {tool: \"shapeBuilder\", values}; a highlightColor with an alpha part (#rrggbbaa or #rgba) is an error and OK changes nothing), objectsOnPath → Objects on Path Tool Options (dialog `objectsOnPathOptions`, fields pivot (0–8, row by row from the top left; 4, the center) and rotate (°, −360–360; 0); Reset puts 4 and 0 back; Save runs tool.setOption {tool: \"objectsOnPath\", values}: the objects attached next use them, groups attached before keep theirs)", ), ( "ui.colorGuideOptions", diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs index 0b5794b615..1dd2ef1d96 100644 --- a/crates/ui-egui/src/shortcuts.rs +++ b/crates/ui-egui/src/shortcuts.rs @@ -878,8 +878,23 @@ fn claimed_tool_keys(app: &mut VectorcraftApp, ctx: &egui::Context, view: ViewIn // The Smart Pen Tool's Tab while it draws a path (the key otherwise hides the panels). (Key::Tab, ToolKey::Tab), ] { - if !rows_take(k) && ctx.input(|i| i.key_pressed(k)) && app.session.tool_claims_key(tk, view) && ctx.input_mut(|i| i.consume_key(m, k)) { - let r = app.session.tool_key(tk, crate::canvas::mods(m, false), view); + // The modifiers held when the key went down, which can differ from the frame's at its end. + let pm = ctx.input(|i| { + i.events + .iter() + .rev() + .find_map(|e| match e { + egui::Event::Key { key, pressed: true, modifiers, .. } if *key == k => Some(*modifiers), + _ => None, + }) + .unwrap_or(m) + }); + if !rows_take(k) + && ctx.input(|i| i.key_pressed(k)) + && app.session.tool_claims_key_with(tk, crate::canvas::mods(pm, false), view) + && ctx.input_mut(|i| i.consume_key(pm, k)) + { + let r = app.session.tool_key(tk, crate::canvas::mods(pm, false), view); // Keyboard actions can change the Artboard tool's active board too. Keep the // navigator aligned with its valid survivor, as pointer dispatch does. if r.is_ok() diff --git a/crates/ui-egui/src/shortcuts/tests.rs b/crates/ui-egui/src/shortcuts/tests.rs index 60b8c5eacd..ae1f966696 100644 --- a/crates/ui-egui/src/shortcuts/tests.rs +++ b/crates/ui-egui/src/shortcuts/tests.rs @@ -2245,6 +2245,37 @@ fn alt_arrows_step_selected_type_object() { assert_ne!(selected[0], vectorcraft_doc::NodeId(rect), "a non-type object is still copied by Alt+arrow"); } +/// With the Live Paint Bucket, a plain Right picks the next color swatch, while Alt+Right keeps +/// copying and nudging the selection, and a Shift+Right released within the same frame is still a +/// Shift+Right that picks no swatch. +#[test] +fn live_paint_bucket_takes_only_plain_arrows_for_swatches() { + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 200, "height": 200})).unwrap(); + let rect = app.session.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap()["id"].as_u64().unwrap(); + app.session.execute("select.set", &json!({"ids": [rect]})).unwrap(); + let first = vectorcraft_tools::builder::color_swatches(&app.session.active().unwrap().doc)[0].name.clone(); + app.session.execute("paint.setFill", &json!({"swatch": first, "ids": []})).unwrap(); + app.select_tool("livePaintBucket"); + let fill = app.session.paint.fill.clone(); + + frame(&mut app, press(Key::ArrowRight, Modifiers::ALT)); + assert_eq!(app.session.paint.fill, fill, "Alt+Right picks no swatch"); + let selected = &app.session.active().unwrap().selection.objects; + assert_ne!(selected.as_slice(), [vectorcraft_doc::NodeId(rect)], "Alt+Right copies and nudges the selection"); + + let shift_then_released = vec![ + egui::Event::ModifiersChanged(Modifiers::SHIFT), + egui::Event::Key { key: Key::ArrowRight, physical_key: Some(Key::ArrowRight), pressed: true, repeat: false, modifiers: Modifiers::SHIFT }, + egui::Event::ModifiersChanged(Modifiers::NONE), + ]; + frame(&mut app, shift_then_released); + assert_eq!(app.session.paint.fill, fill, "Shift+Right picks no swatch"); + + frame(&mut app, press(Key::ArrowRight, Modifiers::NONE)); + assert_ne!(app.session.paint.fill, fill, "a plain Right picks the next swatch"); +} + /// #793: with a layout without Latin letters the one-key shortcuts go by the key's position: /// the P key typing Persian `ح` chooses the Pen, the V key typing Russian `м` the Selection /// tool. A Latin layout keeps the letter typed: AZERTY's A (the Q key's place) is A. diff --git a/docs/mcp.md b/docs/mcp.md index cb5c40501c..4f843939a8 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -3437,7 +3437,8 @@ already walks Image Trace. Short agent entry points: at the pointer selects under these rules: the arrow with a filled square for a face, with a short line for an edge, and with an x for nothing. It keeps the look it had at the press until the release. The Live Paint Bucket's options (`tool.setOption {tool: "livePaintBucket", values}`; double-clicking the tool opens - them, dialog `livePaintBucketOptions`) are `paintFills` (on), `paintStrokes` (off), `highlight` (on), `color` (the + them, dialog `livePaintBucketOptions`) are `paintFills` (on), `paintStrokes` (off), `cursorSwatchPreview` (on), + `highlight` (on), `color` (the highlight color, `#rrggbb` or a layer color's name; `#ff2a2a`) and `width` (the face highlight's width, 1 to 20 screen pixels; 4; an edge's highlight is half as wide). The dialog's OK gives an error for a `color` with an alpha part (`#rrggbbaa` or `#rgba`) and changes nothing. With `paintFills` alone on, a click runs `livePaint.fill` and Shift-click runs `livePaint.strokeEdge`. @@ -3446,6 +3447,14 @@ already walks Image Trace. Short agent entry points: `livePaint.strokeEdge` when an edge is within 4 screen pixels of the pointer and `livePaint.fill` otherwise, and Shift-click runs `livePaint.fill`. With both off, a click runs nothing. With `highlight` off, the face or edge under the pointer is not outlined. + With the Live Paint Bucket selected and no drag in progress, Left and Right without Shift, Alt or Cmd/Ctrl (which + keep their shortcuts) run `paint.setFill {swatch, ids: []}` (or + `paint.setStroke` when the Stroke proxy is in front) with the color swatch before or after the active paint in the + Swatches panel, wrapping around at the ends; from a color that is no swatch, Right picks the first swatch and Left + the last, and with no color swatch the tool does not take the keys. The selected art keeps its paint. With + `cursorSwatchPreview` on, the active paint shows as a ringed chip above the pointer between the swatches before and + after it, except during a fill drag or with Alt held; a solid color that is no swatch shows alone, and a paint that is not a solid color shows no chip. Left and + Right step the swatches with `cursorSwatchPreview` off too. `livePaint.merge` keeps the bottom Live Paint group's id, name, lock, visibility, transparency, appearance, opacity mask and graphic style link. With a Live Paint group among the objects, `livePaint.make` adds the others to it as `livePaint.merge` does, and so does `livePaint.fill {ids}` when `point` is outside that group's faces. @@ -9703,8 +9712,8 @@ The Shape Builder tool's options are its Shape Builder Tool Options (dialog `sha `openFilledAsClosed` (on: a filled open path bounds a region as if an invisible edge closed it; off: it cuts regions as an unfilled path does), `pickColorFrom` (`artwork`, the default: a merged shape takes the appearance, opacity and blending mode of the front-most selected object under the drag's first point, else of the one under its last point, -else of the front-most selected object; `swatches`: the current fill, and the Left and Right arrow keys step the -current fill through the document's color swatches), `cursorSwatchPreview` (off; with `swatches`, chips above the +else of the front-most selected object; `swatches`: the current fill, and the Left and Right arrow keys without +Shift, Alt or Cmd/Ctrl step the current fill through the document's color swatches), `cursorSwatchPreview` (off; with `swatches`, chips above the pointer show the current fill and the swatches before and after it), `selection` (`freeform`, the default, or `straightLine`: a drag touches what the line from its first point to the pointer crosses), `highlightFill` (on by default; when it is off, merge mode shades no regions), `gapDetection` (off) with `gapLength` (pt, 0 to 72; 3), `clickStrokeSplitsPath` (In Merge Mode, diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index e309f72409..e0b61377f0 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1401,6 +1401,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur Object › Live Paint › Merge adds the other selected paths and Live Paint groups to the bottom selected Live Paint group ([Live Paint groups](https://web.archive.org/web/20241230205225/https://helpx.adobe.com/illustrator/using/live-paint-groups.html)), which keeps its id, the paint of its faces and edges, and its name, lock, visibility, transparency, appearance, opacity mask and graphic style link. Object › Live Paint › Make with a Live Paint group selected, and a Live Paint Bucket click on a selected path beside a selected Live Paint group, add the paths to that group the same way. Make, Merge and the Live Paint Bucket take paths, compound paths and groups of them; with type, an image or another object among the selection they report that it has to be converted to paths first and change nothing, as type and raster images can't go into a Live Paint group directly ([Create Live Paint groups](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/create-live-paint-groups.html)); before, such an object went among the group's hidden sources and vanished from the canvas. Evidence: `tests_build::live_paint_refuses_type_and_images_and_changes_nothing`. With the Live Paint Bucket selected, Alt-click on vector art samples its fill, stroke and stroke weight into the paint defaults ([Paint with the Live Paint Bucket tool](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/paint-with-the-live-paint-bucket-tool.html)). On a placed image it samples the image pixel as a solid fill with no stroke. Alt+Shift-click samples only the color under the pointer, including an intermediate gradient color or image pixels, into the active fill or stroke ([Default keyboard shortcuts](https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)). Neither click paints or changes the selection or selected art, and the next Bucket click paints with the sampled paint. Evidence: `tests_build::live_paint_bucket_alt_sample_paints_next_face_without_changing_selection`. Double-clicking the Live Paint Bucket opens Live Paint Bucket Options ([Paint with the Live Paint Bucket tool](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/paint-with-the-live-paint-bucket-tool.html)): Paint Fills (on) and Paint Strokes (off) set whether a click paints faces, edges or both, Highlight turns the outline of the face or edge under the pointer on or off, and Color and Width set it (red and 4 px by default). Faces are outlined at the Width and edges at half of it, as the page says faces are highlighted with a thick line and edges with a thin line. With both on, a click paints the edge within 4 px of the pointer, else the face, and Shift switches to Paint Fills only; with one on, Shift switches to the other. The options last across tool switches and are saved with the preferences (`tool.setOption {tool: "livePaintBucket"}`). Evidence: `builder::tests::{bucket_options_choose_what_a_click_paints, bucket_options_set_the_highlight, bucket_highlights_an_edge_thinner_than_a_face}`, `dialogs::live_paint_bucket::tests::double_clicking_the_tool_opens_its_options_and_ok_sets_them`. + Live Paint Bucket Options has Cursor Swatch Preview, as the [help page](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/paint-with-the-live-paint-bucket-tool.html) lists: with the Bucket selected, the active fill or stroke shows above the pointer between the color swatches before and after it in the Swatches panel, and the Left and Right arrow keys pick the swatch before or after it, as the page describes. The selected art keeps its paint. As VectorCraft's own rules, the option is on at first, the arrows step the stroke when the Stroke proxy is in front, they wrap around at the ends and still step with the preview off, a color that is no swatch shows alone until Right picks the first swatch or Left the last, and the arrows do nothing during a fill drag or with no color swatch. Evidence: `tests::build::{live_paint_bucket_arrow_keys_step_the_active_paint_through_the_swatches, live_paint_bucket_arrow_keys_handle_unmatched_paint_few_swatches_and_drags, live_paint_bucket_arrow_keys_pass_swatches_of_the_same_color_for_fill_and_stroke, swatch_arrow_keys_leave_modified_arrows_to_the_shortcuts}`. Live Paint Make preserves the front-most covering source's fill when compound paths share a curved boundary, and Expand keeps the face fills. Regression evidence: `planar::tests::coincident_curves_keep_their_source_faces` checks source order, winding, fill rules and nearby boundaries; `tests::build::live_paint_make_and_expand_keep_abutting_compound_colors` checks native compound paths, Make, Expand and Undo/Redo. This preserves VectorCraft's source-fill rule without changing arrangement precision. Live Paint Gap Options (Object › Live Paint › Gap Options…, `livePaint.gapOptions`): with Gap Detection on, paint stops at the gaps that run from a loose end of a path to the nearest path, up to Small (3 pt), Medium (6 pt), Large (12 pt) or a custom size; Close Gaps With Paths adds an unpainted path across each gap; Preview applies the options live and draws the gaps in the Gap Preview Color. The options are saved, copied and undone with the group, and Merge keeps the bottom group's options when its Gap Detection is on, or else turns Gap Detection on with Small when another merged group has it on. When Gap Detection is off in every merged group, it stays off. View › Show Live Paint Gaps (`view.livePaintGaps`) draws the gaps of the selected Live Paint groups up to their Paint Stops At size, each in its group's Gap Preview Color, until Hide Live Paint Gaps or Hide Edges hides them ([Live Paint gaps](https://web.archive.org/web/20260105111201/https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/find-and-close-gaps-in-live-paint-groups.html)). From 8d0e45ef5291893ca87a98f61456d0c8005e47e8 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 12:15:49 -0800 Subject: [PATCH 06/63] Actions: keep empty path values in ui.inspect's copy of recorded steps Under a read root, an empty or blank value under a path param, or in a path param's list, names no path. ui.inspect now keeps it, so a recorded prefs.set step that clears a folder preference shows the reset. --- crates/ui-egui/src/control.rs | 7 ++++--- crates/ui-egui/src/control/tests.rs | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index cc5ed91cb6..a840ae19e2 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -1088,13 +1088,14 @@ fn leave_out_unreadable_pref_steps(command: &str, params: &mut serde_json::Map) { use vectorcraft_engine::file_access::is_path_key; + let shown = |p: &str| p.trim().is_empty() || readable_value(p); params.retain(|key, v| { !unreadable_path_like(key) && match v.as_str() { - Some(p) => !unreadable_path_like(p) && !(is_path_key(key) && !readable_value(p)), + Some(p) => !unreadable_path_like(p) && !(is_path_key(key) && !shown(p)), None => true, } }); @@ -1102,7 +1103,7 @@ fn leave_out_unreadable_params(params: &mut serde_json::Map) { if let Value::Array(paths) = v && is_path_key(key) { - paths.retain(|p| p.as_str().is_none_or(readable_value)); + paths.retain(|p| p.as_str().is_none_or(shown)); } leave_out_unreadable_values(v); } diff --git a/crates/ui-egui/src/control/tests.rs b/crates/ui-egui/src/control/tests.rs index 4d43339a85..009998a8fd 100644 --- a/crates/ui-egui/src/control/tests.rs +++ b/crates/ui-egui/src/control/tests.rs @@ -311,6 +311,24 @@ fn a_spaced_absolute_path_outside_the_read_root_is_left_out() { assert_eq!(params, json!({"keep": inside, "list": []})); } +/// Under a read root, an empty or blank value names no path, so a recorded step keeps it under a path +/// param and in a path param's list, a folder preference reset in a `prefs.set` step's `values` +/// included; a path outside the root is still left out. +#[test] +fn an_empty_path_param_stays_under_a_read_root() { + let base = crate::tests_folder::Folder::new("unreadable-empty"); + std::fs::create_dir_all(base.join("in")).unwrap(); + let roots = vectorcraft_engine::file_access::AutomationRoots::new(&[&base.join("in")], &[]).unwrap(); + let mut params = json!({ + "values": {"fontsFolder": "", "templatesFolder": " ", "recoveryFolder": "/outside/root/x"}, + "path": "", "destinationFolder": " ", "files": ["", "/outside/root/y"], + }); + vectorcraft_engine::file_access::confine(roots.as_ref(), || { + super::leave_out_unreadable_step("prefs.set", params.as_object_mut().unwrap()); + }); + assert_eq!(params, json!({"values": {"fontsFolder": "", "templatesFolder": " "}, "path": "", "destinationFolder": " ", "files": [""]})); +} + /// Without a read root, a `file:` URL that names another host may be read, and a recorded step keeps /// it. #[test] From 3a10022dcf56b3c1ef2325a8953f765439c9d332 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 13:00:44 -0800 Subject: [PATCH 07/63] Contextual Task Bar: actions wrap onto more rows on a narrow canvas On a canvas narrower than one row of the bar, the action buttons ran past its right edge: only the font controls moved to a second row, and keep_in moved the bar without shrinking it. - The actions now wrap, in order, onto rows under the row with the handle, the chips and More Options. Their widths are measured with the button's font, and the bar's place on the canvas uses the same layout from the first frame and after a resize. - An action wider than a row, or one that would need a row past the canvas's height, is listed in More Options. - When the handle, the chips and More Options don't fit one row, the chips take rows after the actions. Without a row to spare, the font controls are left out. A canvas too small for the handle and More Options shows no bar. Docs: docs/target-app-parity.md. --- crates/ui-egui/src/canvas/task_bar.rs | 426 +++++++++++++++++++------- crates/ui-egui/src/tests/taskbar.rs | 94 ++++++ docs/target-app-parity.md | 1 + 3 files changed, 410 insertions(+), 111 deletions(-) diff --git a/crates/ui-egui/src/canvas/task_bar.rs b/crates/ui-egui/src/canvas/task_bar.rs index e908640c25..d606f630d5 100644 --- a/crates/ui-egui/src/canvas/task_bar.rs +++ b/crates/ui-egui/src/canvas/task_bar.rs @@ -73,8 +73,8 @@ pub(crate) fn task_bar_shows_type(app: &VectorcraftApp) -> bool { /// A basic shape selected with the Selection tool shows a Shape Properties button in place of Lock. /// Selected type also shows the font family, font style and Font Size controls before its Fill. /// While a Type tool edits type, or has type selected, the bar shows only those controls and the -/// Fill. The font controls take a second row when one row would be wider than the canvas, and -/// narrow there to fit it. +/// Fill. When one row would be wider than the canvas, the font controls take a row of their own, +/// narrowed to fit it, and the actions wrap onto rows under the handle's ([`bar_layout`]). /// Its handle drags it anywhere on the canvas; unpinned it keeps that offset as it follows the /// selection, pinned it stays put ([`crate::state::TaskBarPlace`]). pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) { @@ -231,7 +231,7 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) { } items.push((tl!("Duplicate"), "copy", "edit.duplicate", none())); } - let (fill, stroke, fill_mixed, stroke_mixed) = chip_paints(app, ui.ctx()); + let paints = chip_paints(app, ui.ctx()); let colored = has_colors(&st.doc, &st.selection.objects); let doc = st.uid; // Under the selection's lowest corner on screen, centered on the selection (the view may be @@ -239,23 +239,47 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) { let lowest = xf.quad(b).iter().map(|c| c.y).fold(f32::NEG_INFINITY, f32::max); let anchor = pos2(xf.to_screen(b.center()).x, lowest); let bounds = xf.rect.shrink(8.0); + // The canvas's width less the frame's margins and stroke. + let room = bounds.width() - BAR_FRAME; // On a canvas too narrow for the type controls at their narrowest, the bar leaves them out. - let fonts = fonts && bounds.width() - 18.0 >= crate::panels::character::task_bar_min_room(); + let fonts = fonts && room >= crate::panels::character::task_bar_min_room(); + // The selected objects' Fill, Stroke, Recolor Artwork (when they have colours to recolour) and + // Lock (a basic shape's properties instead), left out with the Artboard tool. A Type tool's bar + // has the Fill alone. + let chips: Vec = [ + (!on_artboards, Chip::Fill), + (!on_artboards && !typing, Chip::Stroke), + (!on_artboards && !typing && colored, Chip::Recolor), + (!on_artboards && !typing, Chip::Lock), + ] + .into_iter() + .filter_map(|(shown, chip)| shown.then_some(chip)) + .collect(); + let widths: Vec = items.iter().map(|(label, ..)| action_width(ui, label)).collect(); + let font_w = fonts.then(|| { + let w = crate::panels::character::TASK_BAR_WIDTHS; + w.iter().sum::() + BAR_GAP * (w.len() - 1) as f32 + }); + let spacing = ui.spacing().item_spacing.y; + let chip_widths: Vec = chips.iter().map(|c| c.width()).collect(); + let layout = bar_layout(&widths, &chip_widths, font_w, room, bar_max_rows(bounds.height(), spacing)); + // On a canvas too small for its handle and More Options: no bar, rather than one that runs off + // the canvas. + let fonts = layout.as_ref().is_some_and(|l| l.fonts); ui.ctx().data_mut(|d| d.insert_temp(task_bar_id().with("fonts"), fonts)); - // The width of the bar's frame (margins and stroke), its handle, Fill and Stroke chips, - // Recolor Artwork, Lock or Shape Properties, and More Options, each after a 6-point gap. - let chips: f32 = - [(!on_artboards, 26.0), (!on_artboards && !typing, 26.0), (!on_artboards && !typing && colored, 30.0), (!on_artboards && !typing, 30.0)] - .iter() - .filter(|(shown, _)| *shown) - .map(|(_, w)| w + 6.0) - .sum(); - let (font_row, est) = size_estimate(&items, fonts, 18.0 + 4.0 + chips + 36.0, bounds.width()); + let Some(layout) = layout else { return }; + let est = layout.size(spacing); // Its place under the selection, then where the handle moved it, kept on the canvas (with the - // bar's size last frame, the estimate before it first shows). + // bar's size last frame, the layout's size before it first shows and whenever the layout + // changed since, as after the canvas or the selection changed). let under = pos2(anchor.x - est.x / 2.0, anchor.y + 28.0); let id = task_bar_id(); - let size = egui::AreaState::load(ui.ctx(), id).and_then(|s| s.size).unwrap_or(est); + let unchanged = ui.ctx().data_mut(|d| { + let last = d.get_temp::(id.with("layout")); + d.insert_temp(id.with("layout"), est); + last == Some(est) + }); + let size = egui::AreaState::load(ui.ctx(), id).and_then(|s| s.size).filter(|_| unchanged).unwrap_or(est); let keep_in = |p: Pos2| p.clamp(bounds.min, (bounds.max - size).max(bounds.min)); let place = &mut app.ui.task_bar_place; // Just unpinned: follow the selection from the pinned spot. @@ -285,95 +309,88 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) { .inner_margin(egui::Margin::symmetric(8, 6)) .shadow(egui::epaint::Shadow { offset: [0, 3], blur: 10, spread: 0, color: Color32::from_black_alpha(70) }) .show(ui, |ui| { - ui.horizontal(|ui| { - ui.spacing_mut().item_spacing.x = 6.0; - let (g, grip) = ui.allocate_exact_size(vec2(4.0, 28.0), Sense::drag()); - let active = grip.hovered() || grip.dragged(); - ui.painter().rect_filled(g.shrink2(vec2(0.5, 4.0)), CornerRadius::same(2), if active { t.text_dim } else { t.button_border }); - if grip.dragged() { - ui.ctx().set_cursor_icon(egui::CursorIcon::Grabbing); - moved = grip.drag_delta(); - } else if grip.hovered() { - ui.ctx().set_cursor_icon(egui::CursorIcon::Grab); - } - for (label, icon, cmd, params) in &items { - let galley = ui.painter().layout_no_wrap(label.to_string(), egui::FontId::proportional(13.0), t.text_strong); - let (r, resp) = ui.allocate_exact_size(vec2(galley.size().x + 38.0, 30.0), Sense::click()); - resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, *label)); - if resp.hovered() { - ui.painter().rect_filled(r, CornerRadius::same(3), t.hover); - } - ui.painter().rect_stroke(r, CornerRadius::same(3), Stroke::new(1.0, t.button_border), StrokeKind::Inside); - crate::icons::paint(ui, icon, egui::Rect::from_min_size(r.min + vec2(8.0, 7.0), vec2(16.0, 16.0)), t.icon); - ui.painter().galley(pos2(r.left() + 30.0, r.center().y - galley.size().y / 2.0), galley, t.text_strong); - if resp.clicked() { - run = Some((*cmd, params.clone())); - } - } - // Type's font family, font style and Font Size, which act as the Character - // panel's do: on the characters a Type tool selects, else on the selected type. - if fonts && !font_row { - crate::panels::character::task_bar(app, ui, f32::INFINITY); - } - // The selected objects' Fill, Stroke, Recolor Artwork and Lock (a basic shape's - // properties instead), left out with the Artboard tool. A Type tool's bar has - // the Fill alone. - if !on_artboards && paint_chip(ui, &t, &fill, fill_mixed, None, tl!("Fill")) { - app.session.fill_active = true; - show_swatches = true; - } - if !on_artboards && !typing { - // The Stroke chip, drawn as a ring as the Control bar's is. - if paint_chip(ui, &t, &stroke, stroke_mixed, Some(4.0), tl!("Stroke")) { - app.session.fill_active = false; - show_swatches = true; - } - // Recolor Artwork, when the selection has colours to recolour. - if colored && widgets::icon_button(ui, "palette", tl!("Recolor Artwork"), false, 30.0).clicked() { - run = Some(("ui.recolorDialog", json!({}))); - } - if let Some(shape) = &shape - && let NodeKind::Path { live: Some(live), .. } = &shape.kind - { - // The shape's properties in a popover that stays open while its fields and - // its Corner dropdown are used. - let props = widgets::icon_button(ui, "shapes", tl!("Shape Properties"), false, 30.0); - widgets::popover(&props, props.clicked(), |ui| { - ui.set_min_width(250.0); - crate::panels::transform::live_shape_rows(app, ui, shape, live, "task-bar-shape", true); - }); - } else { - // The Lock button is off in isolation mode, as Object › Lock › Selection is. - let lockable = crate::menus::enabled(app, "object.lock"); - if widgets::icon_button_enabled(ui, "lock", tl!("Lock (⌘2)"), false, lockable, 30.0).clicked() { - run = Some(("object.lock", json!({}))); - } - } - } - let more = widgets::icon_button(ui, "ellipsis", tl!("More Options"), false, 30.0); - egui::Popup::menu(&more).show(|ui| { - ui.set_min_width(170.0); - let bar = [ - (tl!("Hide Bar"), "window.taskBar", false), - (tl!("Pin Bar Position"), "window.taskBar.pin", pinned), - (tl!("Reset Bar Position"), "window.taskBar.reset", false), - ]; - for (label, cmd, checked) in bar { - if widgets::menu_item(ui, label, true, checked) { - run = Some((cmd, json!({}))); - } - } - ui.separator(); - if widgets::menu_item(ui, tl!("Show Properties Panel"), true, false) { - run = Some(("window.panel", json!({"panel": "properties"}))); - } - }); - }); - if font_row { + // The first row holds the handle and More Options; actions that don't fit beside + // them go on the rows after it, in order. + for row in 0..layout.rows { ui.horizontal(|ui| { - ui.spacing_mut().item_spacing.x = 6.0; - // The canvas's width less the frame's margins and stroke. - crate::panels::character::task_bar(app, ui, bounds.width() - 18.0); + ui.spacing_mut().item_spacing.x = BAR_GAP; + if row == 0 { + let (g, grip) = ui.allocate_exact_size(vec2(BAR_HANDLE, 28.0), Sense::drag()); + let active = grip.hovered() || grip.dragged(); + ui.painter().rect_filled( + g.shrink2(vec2(0.5, 4.0)), + CornerRadius::same(2), + if active { t.text_dim } else { t.button_border }, + ); + if grip.dragged() { + ui.ctx().set_cursor_icon(egui::CursorIcon::Grabbing); + moved = grip.drag_delta(); + } else if grip.hovered() { + ui.ctx().set_cursor_icon(egui::CursorIcon::Grab); + } + } + for ((label, icon, cmd, params), at) in items.iter().zip(&layout.actions) { + if *at == Some(row) && action_button(ui, &t, label, icon) { + run = Some((*cmd, params.clone())); + } + } + // Type's font family, font style and Font Size, which act as the Character + // panel's do: on the characters a Type tool selects, else on the selected type. + if row == 0 && fonts && !layout.font_row { + crate::panels::character::task_bar(app, ui, f32::INFINITY); + } + for (chip, at) in chips.iter().zip(&layout.chips) { + if *at != row { + continue; + } + match paint_or_object_chip(app, ui, &t, *chip, &paints, shape.as_ref()) { + Some(ChipClick::Fill(fill)) => { + app.session.fill_active = fill; + show_swatches = true; + } + Some(ChipClick::Run(cmd)) => run = Some((cmd, json!({}))), + None => {} + } + } + if row != 0 { + return; + } + let more = widgets::icon_button(ui, "ellipsis", tl!("More Options"), false, BAR_MORE); + egui::Popup::menu(&more).show(|ui| { + ui.set_min_width(170.0); + // The actions too wide for any row of the bar. + let listed = items.iter().zip(&layout.actions).filter(|(_, at)| at.is_none()); + let mut any = false; + for ((label, _, cmd, params), _) in listed { + any = true; + if widgets::menu_item_name(ui, label, true, false) { + run = Some((*cmd, params.clone())); + } + } + if any { + ui.separator(); + } + let bar = [ + (tl!("Hide Bar"), "window.taskBar", false), + (tl!("Pin Bar Position"), "window.taskBar.pin", pinned), + (tl!("Reset Bar Position"), "window.taskBar.reset", false), + ]; + for (label, cmd, checked) in bar { + if widgets::menu_item(ui, label, true, checked) { + run = Some((cmd, json!({}))); + } + } + ui.separator(); + if widgets::menu_item(ui, tl!("Show Properties Panel"), true, false) { + run = Some(("window.panel", json!({"panel": "properties"}))); + } + }); + }); + } + if layout.font_row { + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = BAR_GAP; + crate::panels::character::task_bar(app, ui, room); }); } }); @@ -413,14 +430,201 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) { } } -/// Whether the type controls go on a second row, and the bar's estimated size before it first -/// shows: `items` and, when `fonts`, the font family, font style and Font Size controls, which take -/// a second row when one row, with the bar's other controls `fixed` points wide, would be wider than -/// `room` (and narrow to fit it there). -fn size_estimate(items: &[(&str, &str, &str, serde_json::Value)], fonts: bool, fixed: f32, room: f32) -> (bool, Vec2) { - let font_w: f32 = crate::panels::character::TASK_BAR_WIDTHS.iter().filter(|_| fonts).map(|w| w + 6.0).sum(); - let items_w = if fonts { fixed } else { 150.0 } + items.iter().map(|(l, _, _, _)| l.len() as f32 * 7.2 + 44.0).sum::(); - if fonts && items_w + font_w > room { (true, vec2(items_w.max((font_w + 16.0).min(room)), 80.0)) } else { (false, vec2(items_w + font_w, 44.0)) } +/// The width of the Contextual Task Bar's frame: its margins and stroke on both sides. +const BAR_FRAME: f32 = 18.0; +/// The gap between the bar's controls. +const BAR_GAP: f32 = 6.0; +/// The width of the bar's handle. +const BAR_HANDLE: f32 = 4.0; +/// The width of the bar's More Options button. +const BAR_MORE: f32 = 30.0; +/// The height of a row of the bar's controls. +const BAR_ROW: f32 = 30.0; + +/// The controls the Contextual Task Bar shows after its actions: the Fill and Stroke chips, +/// Recolor Artwork, and Lock or a basic shape's Shape Properties. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Chip { + Fill, + Stroke, + Recolor, + Lock, +} + +impl Chip { + fn width(self) -> f32 { + match self { + Chip::Fill | Chip::Stroke => 26.0, + Chip::Recolor | Chip::Lock => 30.0, + } + } +} + +/// What a click on one of the bar's [`Chip`]s does: make the Fill (`true`) or the Stroke active and +/// bring up Swatches, or run a command. +enum ChipClick { + Fill(bool), + Run(&'static str), +} + +/// Where each of the Contextual Task Bar's controls goes. +#[derive(Debug, PartialEq)] +struct BarLayout { + /// Each action's row (0 is the row with the handle and More Options), or `None` for an action + /// wider than a row or past the rows the canvas's height allows, which More Options lists + /// instead. + actions: Vec>, + /// Each chip's row. + chips: Vec, + /// Whether the bar shows the font controls it was given. + fonts: bool, + /// Whether the font controls take a row of their own, after the others. + font_row: bool, + /// The number of rows before the font controls' row. + rows: usize, + /// The bar's width, its frame included. + width: f32, +} + +impl BarLayout { + /// The bar's size, with `spacing` points between its rows. + fn size(&self, spacing: f32) -> Vec2 { + let rows = (self.rows + usize::from(self.font_row)) as f32; + vec2(self.width, rows * BAR_ROW + (rows - 1.0) * spacing + 14.0) + } +} + +/// How many rows of the Contextual Task Bar fit in `height` points, with `spacing` points between +/// them. +fn bar_max_rows(height: f32, spacing: f32) -> usize { + ((height - 14.0 + spacing) / (BAR_ROW + spacing)).floor().max(0.0) as usize +} + +/// The Contextual Task Bar's layout in `room` points (the canvas's width less the frame) and at +/// most `max_rows` rows, for actions and chips the given widths wide, and the font controls `fonts` +/// wide when shown. One row holds everything when it fits. Otherwise the font controls take a row +/// of their own, narrowed to the room, and the actions wrap: the first row keeps the handle, the +/// chips and More Options, and each action goes on the last row when it fits there, else on a new +/// row. When the handle, the chips and More Options don't fit in one row, the chips take rows of +/// their own after the actions. An action wider than a row, or one that would need a row past +/// `max_rows`, goes in More Options; without a row to spare the font controls are left out. `None` +/// when not even the handle and More Options, and the chips' rows, fit. +fn bar_layout(actions: &[f32], chips: &[f32], fonts: Option, room: f32, max_rows: usize) -> Option { + // Puts a control `w` wide on the last row when it fits there, else on a new row. + fn place(rows: &mut Vec, w: f32, room: f32) -> usize { + match rows.last_mut() { + Some(last) if *last + BAR_GAP + w <= room => *last += BAR_GAP + w, + _ => rows.push(w), + } + rows.len() - 1 + } + let ends = BAR_HANDLE + BAR_GAP + BAR_MORE; + if room < ends || max_rows == 0 { + return None; + } + let chips_w: f32 = chips.iter().map(|w| w + BAR_GAP).sum(); + let actions_w: f32 = actions.iter().map(|w| w + BAR_GAP).sum(); + let one_row = ends + chips_w + actions_w + fonts.map_or(0.0, |w| w + BAR_GAP); + if one_row <= room { + return Some(BarLayout { + actions: vec![Some(0); actions.len()], + chips: vec![0; chips.len()], + fonts: fonts.is_some(), + font_row: false, + rows: 1, + width: one_row + BAR_FRAME, + }); + } + let chips_first = ends + chips_w <= room; + let mut chip_rows = vec![]; + let chips: Vec = chips.iter().map(|w| if chips_first { 0 } else { place(&mut chip_rows, *w, room) }).collect(); + if 1 + chip_rows.len() > max_rows { + return None; + } + let font_row = fonts.is_some() && 1 + chip_rows.len() < max_rows; + let action_rows = max_rows - chip_rows.len() - usize::from(font_row); + let mut rows = vec![if chips_first { ends + chips_w } else { ends }]; + // Once an action needs a row past the last one, it and the actions after it go in More Options, + // keeping their order. + let mut full = false; + let actions: Vec> = actions + .iter() + .map(|w| { + if *w > room || full { + return None; + } + let mut next = rows.clone(); + let row = place(&mut next, *w, room); + full = row >= action_rows; + (!full).then(|| { + rows = next; + row + }) + }) + .collect(); + let chips = chips.into_iter().map(|row| if chips_first { 0 } else { rows.len() + row }).collect(); + let font_w = if font_row { fonts.map_or(0.0, |w| w.min(room)) } else { 0.0 }; + let width = rows.iter().chain(&chip_rows).fold(font_w, |a, b| a.max(*b)) + BAR_FRAME; + Some(BarLayout { actions, chips, fonts: font_row, font_row, rows: rows.len() + chip_rows.len(), width }) +} + +/// The width of the Contextual Task Bar's action button for `label`. +fn action_width(ui: &Ui, label: &str) -> f32 { + ui.painter().layout_no_wrap(label.to_string(), egui::FontId::proportional(13.0), Color32::PLACEHOLDER).size().x + 38.0 +} + +/// One of the Contextual Task Bar's action buttons: `icon` and `label`, named for screen readers. +/// Returns whether it was clicked. +fn action_button(ui: &mut Ui, t: &Tokens, label: &str, icon: &str) -> bool { + let galley = ui.painter().layout_no_wrap(label.to_string(), egui::FontId::proportional(13.0), t.text_strong); + let (r, resp) = ui.allocate_exact_size(vec2(galley.size().x + 38.0, BAR_ROW), Sense::click()); + resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, label)); + if resp.hovered() { + ui.painter().rect_filled(r, CornerRadius::same(3), t.hover); + } + ui.painter().rect_stroke(r, CornerRadius::same(3), Stroke::new(1.0, t.button_border), StrokeKind::Inside); + crate::icons::paint(ui, icon, egui::Rect::from_min_size(r.min + vec2(8.0, 7.0), vec2(16.0, 16.0)), t.icon); + ui.painter().galley(pos2(r.left() + 30.0, r.center().y - galley.size().y / 2.0), galley, t.text_strong); + resp.clicked() +} + +/// One of the Contextual Task Bar's chips, with the Fill and Stroke `paints` ([`chip_paints`]) and +/// the basic `shape` whose properties take Lock's place. Returns what a click on it does. +fn paint_or_object_chip( + app: &mut VectorcraftApp, + ui: &mut Ui, + t: &Tokens, + chip: Chip, + paints: &(vectorcraft_color::Paint, vectorcraft_color::Paint, bool, bool), + shape: Option<&vectorcraft_doc::Node>, +) -> Option { + let (fill, stroke, fill_mixed, stroke_mixed) = paints; + match chip { + Chip::Fill => paint_chip(ui, t, fill, *fill_mixed, None, tl!("Fill")).then_some(ChipClick::Fill(true)), + // The Stroke chip, drawn as a ring as the Control bar's is. + Chip::Stroke => paint_chip(ui, t, stroke, *stroke_mixed, Some(4.0), tl!("Stroke")).then_some(ChipClick::Fill(false)), + Chip::Recolor => { + widgets::icon_button(ui, "palette", tl!("Recolor Artwork"), false, 30.0).clicked().then_some(ChipClick::Run("ui.recolorDialog")) + } + Chip::Lock => { + if let Some(shape) = shape + && let NodeKind::Path { live: Some(live), .. } = &shape.kind + { + // The shape's properties in a popover that stays open while its fields and its + // Corner dropdown are used. + let props = widgets::icon_button(ui, "shapes", tl!("Shape Properties"), false, 30.0); + widgets::popover(&props, props.clicked(), |ui| { + ui.set_min_width(250.0); + crate::panels::transform::live_shape_rows(app, ui, shape, live, "task-bar-shape", true); + }); + None + } else { + // The Lock button is off in isolation mode, as Object › Lock › Selection is. + let lockable = crate::menus::enabled(app, "object.lock"); + widgets::icon_button_enabled(ui, "lock", tl!("Lock (⌘2)"), false, lockable, 30.0).clicked().then_some(ChipClick::Run("object.lock")) + } + } + } } /// The paints the Fill and Stroke chips show, and whether each is mixed ("?"): the proxies', or diff --git a/crates/ui-egui/src/tests/taskbar.rs b/crates/ui-egui/src/tests/taskbar.rs index 68ef8a3f15..9768813a70 100644 --- a/crates/ui-egui/src/tests/taskbar.rs +++ b/crates/ui-egui/src/tests/taskbar.rs @@ -767,6 +767,100 @@ fn selected_types_font_controls_take_a_second_row_on_a_narrow_canvas() { assert!(canvas.contains_rect(r) && r.height() < 60.0, "no font controls on a 160 px screen: {r:?} in {canvas:?}"); } +/// The labels drawn inside the task bar after `frames` frames on a screen `size` points in `ctx`, +/// with the bar's rect and the canvas's. +fn bar_texts_on_screen(app: &mut VectorcraftApp, ctx: &egui::Context, size: egui::Vec2, frames: usize) -> (Vec<(String, Rect)>, Rect, Rect) { + let screen = Rect::from_min_size(Pos2::ZERO, size); + let mut texts = vec![]; + for _ in 0..frames { + let mut out = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), ..Default::default() }, |ui| canvas::show(app, ui)); + out.textures_delta.clear(); + texts = shapes_text(&out.shapes.iter().map(|c| c.shape.clone()).collect::>()); + } + let r = canvas::task_bar_rect(ctx).expect("the task bar shows"); + (texts.into_iter().filter(|(_, t)| r.contains(t.center())).collect(), r, app.canvas_rect.expect("the canvas is laid out")) +} + +/// On a canvas narrower than one row of the bar's actions, the actions wrap onto more rows, in +/// order, and the bar stays whole on the canvas, also on the first frame after the canvas narrows. +#[test] +fn a_task_bars_actions_wrap_on_a_narrow_canvas() { + let mut app = app(); + app.run("tool.select", json!({"tool": "selection"})).unwrap(); + let circle = id(app.run("path.create", json!({"d": "M100 300 A100 100 0 1 1 300 300 A100 100 0 1 1 100 300 Z"})).unwrap()); + let t = id(app.run("text.createInPath", json!({"path": circle, "mode": "onPath", "text": "Round", "at": [90, 300]})).unwrap()); + select(&mut app, &[t]); + let actions = ["Create Outlines", "Type on a Path Options", "Center on Top", "Center on Bottom", "Dynamic Text", "Duplicate"]; + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let (texts, r, canvas) = bar_texts_on_screen(&mut app, &ctx, vec2(1800.0, 700.0), 3); + assert!(canvas.contains_rect(r) && r.height() < 60.0, "one row where it fits: {r:?} in {canvas:?}"); + // The first frame after the canvas narrows, then once the bar settles. + for frames in [1, 3] { + let (texts, r, canvas) = bar_texts_on_screen(&mut app, &ctx, vec2(760.0, 700.0), frames); + assert!(canvas.contains_rect(r), "{frames} frames: {r:?} in {canvas:?}"); + let ys: Vec = actions.iter().map(|label| at(&texts, label).y).collect(); + assert!(ys.windows(2).all(|w| w[0] <= w[1]), "{frames} frames: the actions keep their order: {ys:?}"); + assert!(ys[0] < ys[ys.len() - 1], "{frames} frames: the actions take more than one row: {texts:?}"); + } + assert!(actions.iter().all(|label| has(&texts, label)), "{texts:?}"); +} + +/// On a canvas too narrow for an action's button, or too short for the rows the actions would +/// wrap onto, More Options lists those actions, and the chips take rows after the actions when they +/// don't fit beside the handle and More Options. Narrower than the handle and More Options, the bar +/// doesn't show. +#[test] +fn a_task_bar_action_wider_than_the_canvas_goes_in_more_options() { + let mut app = app(); + app.run("tool.select", json!({"tool": "selection"})).unwrap(); + let a = id(app.run("shape.rectangle", json!({"x": 40, "y": 40, "width": 120, "height": 100})).unwrap()); + let b = id(app.run("shape.ellipse", json!({"x": 70, "y": 60, "width": 50, "height": 50})).unwrap()); + select(&mut app, &[a, b]); + app.run("object.compoundPath.make", json!({})).unwrap(); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let (texts, r, canvas) = bar_texts_on_screen(&mut app, &ctx, vec2(160.0, 700.0), 3); + assert!(canvas.contains_rect(r), "{r:?} in {canvas:?}"); + assert!(has(&texts, "Edit Path") && has(&texts, "Duplicate") && !has(&texts, "Release Compound Path"), "{texts:?}"); + // More Options, after the frame's margin and the handle on the first row. + let more = r.min + vec2(9.0 + 4.0 + 6.0 + 15.0, 7.0 + 15.0); + let click = |app: &mut VectorcraftApp, p: Pos2| { + let press = |pressed| Event::PointerButton { pos: p, button: PointerButton::Primary, pressed, modifiers: Default::default() }; + let screen = Rect::from_min_size(Pos2::ZERO, vec2(160.0, 700.0)); + let mut texts = vec![]; + for events in [vec![Event::PointerMoved(p)], vec![press(true)], vec![press(false)], vec![]] { + let mut out = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), events, ..Default::default() }, |ui| canvas::show(app, ui)); + out.textures_delta.clear(); + texts = shapes_text(&out.shapes.iter().map(|c| c.shape.clone()).collect::>()); + } + texts + }; + let texts = click(&mut app, more); + // Menu items are drawn after a space for the check mark. + let item = |label: &str| texts.iter().find(|(t, _)| t.trim() == label).map(|(_, r)| r.center()); + assert!(item("Hide Bar").is_some(), "More Options opens: {texts:?}"); + let release = item("Release Compound Path").unwrap_or_else(|| panic!("More Options lists the action: {texts:?}")); + click(&mut app, release); + let st = app.session.active().unwrap(); + assert_eq!(st.selection.objects.len(), 2, "the paths come apart, selected"); + assert!(st.selection.objects.iter().all(|i| matches!(st.doc.node(*i).unwrap().kind, NodeKind::Path { .. }))); + + // On a canvas too short for the rows the actions would wrap onto, More Options lists them, in + // order: the bar keeps at most the first. + let (texts, r, canvas) = bar_texts_on_screen(&mut app, &ctx, vec2(160.0, 150.0), 3); + assert!(canvas.contains_rect(r), "{r:?} in {canvas:?}"); + let shown = ["Group", "Unite", "Duplicate"].map(|label| has(&texts, label)); + assert!(!shown[1] && !shown[2], "{texts:?}"); + + let screen = Rect::from_min_size(Pos2::ZERO, vec2(60.0, 700.0)); + for _ in 0..3 { + let mut out = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), ..Default::default() }, |ui| canvas::show(&mut app, ui)); + out.textures_delta.clear(); + } + assert!(canvas::task_bar_rect(&ctx).is_none(), "no room for the handle and More Options: no bar"); +} + /// When the canvas narrows so far that the bar leaves its font controls out, the font family /// field it drew before no longer takes the mouse wheel. #[test] diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 9a9b0d06ed..494d0465bc 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -845,6 +845,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - With the Artboard tool, the contextual task bar sits under the selected artboards and offers Resize (Artboard Options for the active artboard), Duplicate (each selected artboard, in one undo step, the copies then selected), Rearrange All (with two or more artboards) and Export (Export for Screens with the selected artboards checked). This follows Illustrator's bar: while the Artboard tool is in use, it has options for resizing, duplicating and exporting artboards (the [Contextual Task Bar announcement](https://community.adobe.com/questions-652/contextual-task-bar-in-illustrator-2025-quick-actions-when-you-need-them-1546580); Rearrange and Export on the bar: [Managing Artboards in Illustrator](https://creativepro.com/managing-artboards-in-illustrator/)). - The contextual task bar offers more next steps by selection (): an open path Join and Edit Path (the Direct Selection tool), a closed path Edit Path, a compound path Edit Path and Release Compound Path, a clipping group Edit Clipping Path and Release Clipping Mask instead of Ungroup and Isolate, an image selected with vector art Make Clipping Mask instead of Unite, point or area type Convert To Area Type or Convert To Point Type beside Create Outlines, and type on a path, for which the overview names an item to add an effect and rearrange the text, Type on a Path Options (`type.pathOptions`; its options: ) beside Create Outlines. Evidence: `crates/ui-egui/src/tests/taskbar.rs`. - While a Type tool (Type, Area Type, Type on a Path and their vertical versions) adds or edits type, the contextual task bar shows the font family menu, the font style menu, the Font Size combo and the Fill chip (); the Touch Type tool shows no bar, as the same page describes. Type selected with a selection tool shows the same three font controls beside its Create Outlines, conversion and Type on a Path items (). The controls act as the Character panel's do: on the characters a Type tool selects, else on every selected type object, with the size left blank where the sizes differ. VectorCraft's own rules: a Type tool's bar leaves out the object actions, the Stroke chip, Recolor Artwork and Lock; a Type tool shows no bar before it adds type or with other art selected; type selected with other art shows no font controls; the font controls go on a second row when one row would be wider than the canvas, and narrow to fit a canvas narrower than they are; while a Type tool edits type, the Fill chip shows the edited characters' fill ("?" where it differs). Evidence: `the_type_tools_show_font_and_fill_controls`, `the_type_tools_show_no_bar_without_type`, `selected_type_shows_font_controls_beside_its_actions`, `the_task_bars_font_size_sets_what_the_character_panel_would`, `the_type_tools_fill_chip_activates_the_fill`, `the_type_tools_bar_is_named_and_stays_on_the_canvas`, `selected_types_font_controls_take_a_second_row_on_a_narrow_canvas` and `the_type_tools_fill_chip_shows_the_edited_characters_fill` in `crates/ui-egui/src/tests/taskbar.rs`. + - The help page describes the contextual task bar as one horizontal row () and gives no rule for a canvas narrower than that row. VectorCraft's own rule: the bar stays whole on the canvas. When its actions don't fit in one row, they wrap, in order, onto rows under the row with the handle, the chips and More Options; an action wider than any row, or one that would need a row past the canvas's height, is listed in More Options instead; when the chips don't fit beside the handle and More Options, they take rows after the actions; with no row to spare for the font controls, the bar leaves them out; a canvas too small for the handle and More Options shows no bar. Evidence: `a_task_bars_actions_wrap_on_a_narrow_canvas` and `a_task_bar_action_wider_than_the_canvas_goes_in_more_options` in `crates/ui-egui/src/tests/taskbar.rs`. - With a live rectangle, rounded rectangle, ellipse or polygon selected with the Selection tool, the contextual task bar shows a Shape Properties button in place of Lock. It opens a popup with the shape's rows from the Transform panel (corner radius, pie angles, or sides, angle, corner and radius), which edit it through `object.setLiveShape`. The overview describes this: when a shape drawn with a shape tool (Rectangle, Star) is selected, the bar offers to change its properties where Lock Object would be ([Contextual Task Bar overview](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/contextual-task-bar-overview.html)). Object › Lock and ⌘2 still lock the shape. Not done: stars, which the app draws as plain paths. Evidence: `a_basic_shape_shows_its_properties_in_place_of_lock`, `the_shape_properties_popup_edits_the_shape` and `other_selections_keep_lock` in `crates/ui-egui/src/tests/taskbar.rs`. - The contextual task bar has a Stroke chip beside the Fill chip, drawn with the paint the Stroke proxy shows (a "?" when the selected objects' strokes differ, as on the Fill chip); a click brings the Stroke proxy forward and shows the Swatches panel wherever it is docked, floating or closed, as a click on the Fill chip does for the Fill proxy. The reference app's bar has Fill and Stroke controls for changing an object's colors ([Contextual Task Bar overview](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/contextual-task-bar-overview.html)). Evidence: `the_task_bar_shows_a_stroke_chip_beside_fill` and `the_task_bar_chips_follow_the_proxies_and_show_swatches_anywhere` in `crates/ui-egui/src/canvas/tests.rs`. - With anchor points selected with Direct Selection, the contextual task bar offers Remove Anchor Points, Connect (`path.join`), Cut Path, Corner and Smooth (`path.convertAnchors` with `to` set to `corner` or `smooth`), the commands of the Control bar's anchor buttons. As in Illustrator, whose bar has items that delete the selected anchors, join selected end points, split the path at the selected anchors, and make the selected anchors corner or smooth points ([Contextual Task Bar overview](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/contextual-task-bar-overview.html)). Not done: Simplify, which the bar offers only while no anchors are selected, and the bar's Smooth item that adjusts a path's curvature, a command the app doesn't have (Object › Path › Smooth). From 2fc344cb8c438fbb8afa3f70db6e215925dd40d9 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 14:15:03 -0800 Subject: [PATCH 08/63] PDF/.ai import: graphs keep their data and settings when their records read whole A legacy .ai graph (%_Gs ... %_GS) opened as plain art, with its table, type, axes and settings left out. A grouped column, stacked column, line or area graph now keeps them on its group when every record has the shape and values the version 7 specification's appendix A gives, the table holds at least one number, the drawn art strokes the plot's left and bottom edges, and each series group is tagged once as a whole column, holds only paths and paints them with one fill and one stroke. The saved art stays as it is on open; Graph Data redraws the graph from its data afterward, with each series' fill and stroke kept. Any other graph (pie and scatter graphs, designs, customizations, record shapes of later versions, tables past 10,000 categories, 256 series or 2,600,000 cells) keeps its art as plain objects with the existing note. Saving a graph to .ai, in either form, now warns that its data and settings are not written. --- crates/engine/src/cmd/graph/tests.rs | 63 ++ crates/engine/tests/import_fuzz.rs | 30 + crates/eps/src/ai_editing/writer.rs | 4 + crates/eps/src/import/ai/read.rs | 9 +- crates/eps/src/import/ai/read/document.rs | 4 +- crates/eps/src/import/ai/read/graph.rs | 681 ++++++++++++++++++++++ crates/eps/src/import/ai/read/ops.rs | 45 +- crates/eps/src/import/ai/read/types.rs | 16 +- crates/eps/src/import/ai/tests.rs | 218 +++++++ crates/eps/src/legacy_ai.rs | 1 + crates/eps/src/legacy_ai/writer.rs | 5 +- docs/file-format-parity.md | 9 +- docs/target-app-parity.md | 1 + 13 files changed, 1030 insertions(+), 56 deletions(-) create mode 100644 crates/eps/src/import/ai/read/graph.rs diff --git a/crates/engine/src/cmd/graph/tests.rs b/crates/engine/src/cmd/graph/tests.rs index b1a491424e..c8580549aa 100644 --- a/crates/engine/src/cmd/graph/tests.rs +++ b/crates/engine/src/cmd/graph/tests.rs @@ -5280,3 +5280,66 @@ fn graph_commands_refuse_params_of_the_wrong_type() { s.execute("graph.setData", &json!({"id": id.0, "rows": [[1, null], [2, 3]], "series": null, "transpose": null})).unwrap(); assert_eq!(spec(&s).cells(), vec![vec![Some(1.0), None], vec![Some(2.0), Some(3.0)]]); } + +/// A `.ai` in PostScript form whose graph's records read whole (version 7 specification, appendix A) +/// opens as a graph whose data can be edited: Graph Data redraws it on its plot. Saving it as `.ai` +/// says its data and settings aren't written, in either form. +#[test] +fn a_graph_opened_from_ai_keeps_its_data_for_editing() { + let graph = "%_Gs\n%_100 200 300 100 Gb\n%_5 0 0 14 0 1 0 0 6 44 Gy\n%_10 2 0 0 90 80 0 Gd\n\ + %_1 () Ga\n%_0 13 0 0 0 0 0 () GA\n%_2 () Ga\n%_0 14 0 0 0 0 0 () GA\n%_4 () Ga\n%_0 14 0 0 0 0 0 () GA\n\ + %_2 2 1 1 Gz\n%_() Gc (East) Gc (Q1) Gc 5 Gc GC\n\ + u\nu\n0.6 0 1 0 k\n110 100 m\n110 150 L\n130 150 L\n130 100 L\nf\nU\n%_2 1 0 0 Go\n\ + u\n0 0 0 1 K\n100 200 m\n100 100 L\n300 100 L\nS\nU\nU\n%_GS\n"; + let text = format!( + "%!PS-Adobe-3.0\n%%Creator: a script\n%%BoundingBox: 0 0 400 400\n%AI5_FileFormat 3\n%AI3_Cropmarks: 0 0 400 400\n%%EndComments\n\ + %%BeginProlog\n%%EndProlog\n%%BeginSetup\n%%EndSetup\n%AI5_BeginLayer\n1 1 1 1 0 0 0 79 128 255 Lb\n(Graph) Ln\n0 A\n{graph}LB\n\ + %AI5_EndLayer--\n%%PageTrailer\n%%Trailer\n%%EOF\n" + ); + let dir = vectorcraft_testkit::temp_dir("graph-ai-open"); + let path = dir.join("graph.ai"); + std::fs::write(&path, text).unwrap(); + let mut s = Session::new(); + s.execute("document.open", &json!({"path": path})).unwrap(); + let mut found = vec![]; + s.doc().unwrap().doc.walk(|n| { + if n.extras().graph.is_some() { + found.push(n.id); + } + }); + assert_eq!(found.len(), 1); + let id = found[0]; + let before = spec(&s, id); + assert_eq!( + (before.series.as_slice(), before.categories.as_slice(), before.rows.as_slice()), + (&["East".to_string()][..], &["Q1".to_string()][..], &[vec![5.0]][..]) + ); + // The fill of the series' first mark. + let series_fill = |s: &Session| { + let mut fill = None; + s.doc().unwrap().doc.walk(|n| { + if n.extras().series_index == Some(0) && fill.is_none() { + let first = n.children().and_then(|c| c.first().cloned()); + fill = first.and_then(|m| m.appearance.items.iter().find(|i| i.is_fill()).map(|i| i.paint().clone())); + } + }); + fill + }; + let painted = series_fill(&s); + assert!(painted.is_some()); + s.execute("select.set", &json!({"ids": [id.0]})).unwrap(); + s.execute("graph.setData", &json!({"id": id.0, "csv": ",East\nQ1,8\nQ2,2"})).unwrap(); + let after = spec(&s, id); + assert_eq!(after.rows, [[8.0], [2.0]]); + assert_eq!(after.rect, before.rect, "the graph is redrawn on its plot"); + assert_eq!(series_fill(&s), painted, "the series keeps the file's paint"); + for (name, version) in [("v7.ai", Some("7")), ("pdf.ai", None)] { + let mut p = json!({"path": dir.join(name), "format": "ai"}); + if let Some(v) = version { + p["aiVersion"] = json!(v); + } + let r = s.execute("file.saveAs", &p).unwrap(); + let notes = r["warnings"].as_array().cloned().unwrap_or_default(); + assert!(notes.iter().any(|w| w.as_str().is_some_and(|w| w.starts_with("graphs are written as their art"))), "{name}: {r}"); + } +} diff --git a/crates/engine/tests/import_fuzz.rs b/crates/engine/tests/import_fuzz.rs index 04e7094f9e..79178d6233 100644 --- a/crates/engine/tests/import_fuzz.rs +++ b/crates/engine/tests/import_fuzz.rs @@ -7488,3 +7488,33 @@ proptest! { prop_assert!(!notes.iter().any(|n| n.contains("art this can't read")), "the records aren't art: {:?}", notes); } } + +/// A graph in `.ai` editing data whose records read whole (version 7 specification, appendix A), of +/// a table `rows` × `cols` (its label row and column included): a series group tagged by its +/// column, and the axes on the plot's edges. +fn live_graph_text(rows: &str, cols: &str) -> String { + format!( + "%_Gs\n%_10 90 110 10 Gb\n%_5 0 0 14 0 1 0 0 6 44 Gy\n%_10 2 0 0 90 80 0 Gd\n\ + %_1 () Ga\n%_0 13 0 0 0 0 0 () GA\n%_2 () Ga\n%_0 14 0 0 0 0 0 () GA\n%_4 () Ga\n%_0 14 0 0 0 0 0 () GA\n\ + %_{rows} {cols} 1 1 Gz\n%_() Gc (East) Gc (Q1) Gc 5 Gc GC\n\ + u\nu\n0 0 0 1 k\n20 10 m\n20 50 L\n40 50 L\n40 10 L\nf\nU\n%_2 1 0 0 Go\nu\n0 0 0 1 K\n10 90 m\n10 10 L\n110 10 L\nS\nU\nU\n%_GS\n" + ) +} + +proptest! { + #![proptest_config(config())] + + /// A `.ai` graph that reads whole, then cut or damaged, or with a table of any size (zero, past + /// the limits, past what memory holds), never panics, and its drawn art always comes in. + #[test] + fn ai_live_graph_never_panics(cut in 0usize..800, edits in prop::collection::vec((0usize..800, prop::sample::select(vec!['0', '9', '-', '.', ' ', '\n', '(', ')', '/', '%', '_', 'G', 'c', 'C', 'o', 's', 'S', 'u', 'U'])), 0..12), rows in prop_oneof![Just(2u64), 0u64..20_000, Just(u64::MAX)], cols in prop_oneof![Just(2u64), 0u64..400, Just(4_000_000_000u64)]) { + let text = mutate_text(&live_graph_text("2", "2"), cut, &edits); + survive("mutated live .ai graph", || Some(vectorcraft_eps::layered_ai(&ai_private(&text), Document::new(100.0, 100.0), vec![], false, true, 1.0).0))?; + let sized = live_graph_text(&rows.to_string(), &cols.to_string()); + let (doc, notes, read) = vectorcraft_eps::layered_ai(&ai_private(&sized), Document::new(4000.0, 200.0), vec![], false, true, 1.0); + prop_assert!(read, "{:?}", notes); + let mut paths = 0; + doc.walk(|n| paths += usize::from(matches!(n.kind, vectorcraft_doc::NodeKind::Path { .. }))); + prop_assert_eq!(paths, 2, "{:?}", notes); + } +} diff --git a/crates/eps/src/ai_editing/writer.rs b/crates/eps/src/ai_editing/writer.rs index 7db5d11896..967d1f7ed5 100644 --- a/crates/eps/src/ai_editing/writer.rs +++ b/crates/eps/src/ai_editing/writer.rs @@ -56,6 +56,7 @@ pub(super) const CLIPPING: &str = "clipping groups and clipping layers whose cli pub(super) const CLIP_PAINT: &str = "the fills and strokes of clipping paths that a plain fill and line can't draw are written as paths of their own inside the clipping (fills under the clipped art, strokes over it) in the editing data for other apps"; pub(super) const CLIP_PAINT_MASKS: &str = "the opacity masks of clipping paths whose fills and strokes are written as paths of their own stay on the clipping path, not on those paths, in the editing data for other apps"; pub(super) const LIVE: &str = "blends, meshes, repeats, placed documents and intertwines are written as their art in the editing data for other apps"; +pub(super) const GRAPHS: &str = "graphs are written as their art, without their data or settings, in the editing data for other apps"; pub(super) const STROKES: &str = "strokes a plain line can't draw (inside or outside the path, variable width, dots, dashes fitted to corners, gradients, patterns, arrowheads) are written as filled outlines, and brushed strokes as their brush's art, in the editing data for other apps"; pub(super) const ITEM_TRANSPARENCY: &str = "the opacity and blending modes of single fills and strokes are left out of the editing data for other apps"; @@ -613,6 +614,9 @@ impl<'a> Writer<'a> { if self.in_symbol { self.wrote_fixed |= symbols::redrawn(n); } + if n.extras().graph.is_some() && !self.in_look { + self.warn(GRAPHS); + } // On the layers, outside art cut to a clipping object: with its art style, when it has one. if !self.in_look && self.hidden == 0 diff --git a/crates/eps/src/import/ai/read.rs b/crates/eps/src/import/ai/read.rs index 4b2936f56e..b77bc32199 100644 --- a/crates/eps/src/import/ai/read.rs +++ b/crates/eps/src/import/ai/read.rs @@ -78,7 +78,8 @@ //! //! - Graphs (version 7 specification §17): `%_Gs`, the graph's data and settings on `%_` lines, //! its drawn art as ordinary groups, paths and type (each object followed by `%_… Go`), `%_GS`. -//! The drawn art is read; the data and settings are left out, with a note. +//! The drawn art is read as it is. Its group keeps the data and settings when they read whole +//! ([`graph`]); otherwise they are left out, with a note. //! //! - Swatches: the palette (`%AI5_BeginPalette`), read by [`super::palette`], replaces the //! default swatches. @@ -133,6 +134,7 @@ mod art; mod containers; mod directives; mod document; +mod graph; mod masks; mod on_path; mod ops; @@ -146,12 +148,13 @@ pub use document::{ symbols, variables, }; pub(crate) use document::{slot_frame, slot_of}; +use graph::Graph; pub(crate) use ops::{read_art_style, read_style_list, style_moves_with}; #[cfg(test)] pub(crate) use raster::with_image_limits; pub(crate) use types::MAX_NODES; pub use types::PlacedPage; -use types::{BLEND_BUDGET, BrushArtColors, FLIP, Frame, GState, Graph, Kind, PatternHead, PendingEnvelope, Place, PlacedTail, Raster, SymbolHead}; +use types::{BLEND_BUDGET, BrushArtColors, FLIP, Frame, GState, Kind, PatternHead, PendingEnvelope, Place, PlacedTail, Raster, SymbolHead}; #[cfg(test)] pub use types::{GRAPH_PLAIN, GRAPH_UNENDED, LINKED_PLACEHOLDERS, NON_NATIVE_ART, NON_NATIVE_ART_UNREAD, VERSIONED_UNENDED}; pub(super) use types::{blend_mode, blend_name}; @@ -670,7 +673,7 @@ struct Reader<'a> { brushes_kept: BrushesKept, /// Brush objects that weren't read. brushes_unread: usize, - /// The graph being read (see [`Reader::graph_record`]). + /// The graph being read (see [`graph`]). graph: Option, /// The multi-layer mask being read (`Mb` … `MB`), which goes on past a layer's end. multi_mask: Option, diff --git a/crates/eps/src/import/ai/read/document.rs b/crates/eps/src/import/ai/read/document.rs index 0bcae74b32..ded114bc67 100644 --- a/crates/eps/src/import/ai/read/document.rs +++ b/crates/eps/src/import/ai/read/document.rs @@ -1165,7 +1165,9 @@ impl<'a> Reader<'a> { } pub(super) fn finish(mut self) -> Result { - if self.graph.take().is_some() { + if self.graph.is_some() { + self.graph_plain(); + self.graph = None; self.warn(super::types::GRAPH_UNENDED); } // Envelopes whose plugin never closed them keep their drawn art. diff --git a/crates/eps/src/import/ai/read/graph.rs b/crates/eps/src/import/ai/read/graph.rs new file mode 100644 index 0000000000..759eb4e1cd --- /dev/null +++ b/crates/eps/src/import/ai/read/graph.rs @@ -0,0 +1,681 @@ +//! A graph's records (version 7 specification §17 and appendix A) and the live graph they give. +//! +//! A graph is `%_Gs`, its functional records on `%_` lines (`Gb`, `Gy`, `Gd`, any `Gj`, three +//! `Ga` … `GA` axes, any `Gw`, `Gz`, `Gc` cells and `GC`), any `version Gt` … `GT` customizations, its +//! drawn art in one group (objects followed by `%_… Go`), then `%_GS`. The drawn art is always +//! read as it is. The group also keeps the graph's data and settings ([`GraphSpec`]) when every +//! record has the shape and values appendix A gives and the model holds each setting, the table +//! has data, the art draws the plot's left and bottom edges, and each series' group is tagged, is +//! one of the graph's own groups and paints its paths with one fill and one stroke, which the +//! graph keeps as the series' paint. +//! Any other graph stays plain art, with a note. + +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; + +use kurbo::{PathSeg, Point}; +use vectorcraft_doc::{Appearance, GraphKind, GraphSpec, MAX_GRAPH_CATEGORIES, MAX_GRAPH_SERIES, SeriesPaint, TickLength}; + +use super::types::{GRAPH_OPS, GRAPH_PLAIN, GRAPH_UNENDED, Kind}; +use super::{Affine, AppearanceItem, BlendMode, Node, NodeId, NodeKind, Paint, Reader, Rect, V, obj}; + +/// Most cells of a graph's table, its label row and column included. +const MAX_CELLS: usize = 2_600_000; + +/// Longest cell string (appendix A, `Gc`). +const MAX_CELL_TEXT: usize = 255; + +/// Most characters of an axis label's text before or after its numbers (appendix A, `Ga`, `GA`). +const MAX_AFFIX: usize = 9; + +/// Most tick marks per division a graph draws. +const MAX_TICK_MARKS: usize = 20; + +/// Most divisions of a value axis with set values. +const MAX_DIVISIONS: f64 = 100.0; + +/// How far an axis line may be from the plot's edge, in points. +const EDGE: f64 = 0.01; + +/// The axes in the order the records give them: bottom, left, right. +const AXES: [i64; 3] = [1, 2, 4]; + +/// The `Go` targets appendix A lists. +const TARGETS: [i64; 11] = [1, 2, 4, 5, 6, 8, 9, 10, 15, 20, 22]; + +/// The version of a graph's customizations (appendix A, `Gt`). +const CUSTOM_VERSION: i64 = 2; + +/// `Go`'s target for one series, including its legend. +const SERIES: i64 = 2; + +/// A graph being read (`%_Gs` … `%_GS`). +#[derive(Debug)] +pub(super) struct Graph { + /// Between `Gt` and `GT`, where the graph's settings may use paint and type operators on `%_` + /// lines too. + pub(super) settings: bool, + /// How many frames were open at its `Gs`: a layer that ends with fewer ends the graph. + pub(super) frames: usize, + /// How many objects the innermost frame held at `Gs`: the drawn art is the one after them. + siblings: usize, + /// The records read so far, while they can still give a live graph. + live: Option>, +} + +/// Where the records are, in the order appendix A gives. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +enum Stage { + #[default] + Bounds, + Style, + Dialog, + /// Before the `Ga` of axis `n`. + Axis(usize), + /// Before the `GA` of axis `n`. + AxisEnd(usize), + Widths, + Cells, + /// After `GC`: customizations, then the drawn art. + Done, + /// Between `Gt` and `GT`. + Custom, +} + +/// One axis: `whichAxis beforeString Ga` and `useManualValues tickMarkType minimumValue maximumValue +/// betweenValue smallTicksPerValue drawMarksBetweenLabels afterString GA`. +#[derive(Debug, Default)] +struct Axis { + before: Vec, + manual: bool, + tick: TickLength, + min: f64, + max: f64, + between: f64, + small: usize, + between_labels: bool, + after: Vec, +} + +/// A cell of the table (`Gc`). +#[derive(Debug)] +enum Cell { + Num(f64), + Text(Vec), +} + +/// The records of a graph read so far. +#[derive(Debug, Default)] +struct Records { + stage: Stage, + spec: GraphSpec, + axes: [Axis; 3], + /// `Gw`: the first column and how many widths. + widths: Vec<(usize, usize)>, + /// `Gz`: rows, columns, first data row and first data column. + size: Option<[usize; 4]>, + cells: Vec, + /// The groups `Go` tags as one series, and the column each names. + series: Vec<(NodeId, usize)>, +} + +/// A whole number. +fn int(v: &V) -> Option { + v.num().filter(|x| x.is_finite() && x.fract() == 0.0 && x.abs() < 1e15).map(|x| x as i64) +} + +/// 0 or 1. +fn flag(v: &V) -> Option { + match int(v)? { + 0 => Some(false), + 1 => Some(true), + _ => None, + } +} + +fn finite(v: &V) -> Option { + v.num().filter(|x| x.is_finite()) +} + +/// A count from 0 to `max`. +fn count(v: &V, max: usize) -> Option { + usize::try_from(int(v)?).ok().filter(|n| *n <= max) +} + +/// `tickMarkType`: 13 none, 14 short, 15 the length of the plot. +fn tick(v: &V) -> Option { + match int(v)? { + 13 => Some(TickLength::None), + 14 => Some(TickLength::Short), + 15 => Some(TickLength::Full), + _ => None, + } +} + +/// `graphType`: 5 grouped column, 6 stacked column, 7 line, 10 area. Pie (8) and scatter (9) graphs +/// keep settings the records don't give the model, and other values are not graph types. +fn kind(v: &V) -> Option { + match int(v)? { + 5 => Some(GraphKind::Column), + 6 => Some(GraphKind::StackedColumn), + 7 => Some(GraphKind::Line), + 10 => Some(GraphKind::Area), + _ => None, + } +} + +/// A percentage of the available width, 1 to 1000. +fn percent(v: &V) -> Option { + finite(v).filter(|p| (1.0..=1000.0).contains(p)) +} + +/// The tick count and values of an axis: with set values (`useManualValues` 1), the divisions +/// between its lowest and highest value, a whole number of `betweenValue` steps from the lowest up. +fn values(a: &Axis) -> Option<(usize, Option, Option)> { + if !a.manual { + return Some((0, None, None)); + } + let n = (a.max - a.min) / a.between; + let whole = n.round(); + let exact = (n - whole).abs() <= 1e-6 * whole.max(1.0); + (a.between > 0.0 && a.max > a.min && exact && (1.0..=MAX_DIVISIONS).contains(&whole)).then_some((whole as usize, Some(a.min), Some(a.max))) +} + +impl Records { + /// Take the record `w` with its operands `vals`; `None` when its shape, values or place aren't + /// those appendix A gives, or the model doesn't hold a value it sets. `to_doc` takes art space + /// to the document. + fn step(&mut self, w: &str, vals: &[V], to_doc: Affine) -> Option<()> { + match (w, self.stage) { + ("Gb", Stage::Bounds) => { + let [l, t, r, b] = vals else { return None }; + let [l, t, r, b] = [finite(l)?, finite(t)?, finite(r)?, finite(b)?]; + let plot = to_doc.transform_rect_bbox(Rect::new(l, b, r, t)); + let sound = r > l && t > b && plot.width() > 0.0 && plot.height() > 0.0 && plot.width().is_finite() && plot.height().is_finite(); + sound.then_some(())?; + self.spec.rect = plot; + self.stage = Stage::Style; + } + ("Gy", Stage::Style) => { + self.style(vals)?; + self.stage = Stage::Dialog; + } + ("Gd", Stage::Dialog) => { + let [col_width, precision, series_order, both_axes, bar, group, edge] = vals else { return None }; + count(col_width, 20).filter(|w| *w >= 3)?; + count(precision, 10)?; + // `useBothAxis` copies one axis' values to the other, which the model doesn't do. + (!flag(both_axes)?).then_some(())?; + self.spec.first_column_in_front = flag(series_order)?; + self.spec.column_width = percent(bar)?; + self.spec.cluster_width = percent(group)?; + self.spec.edge_to_edge = flag(edge)?; + self.stage = Stage::Axis(0); + } + ("Ga", Stage::Axis(n)) if n < AXES.len() => { + let [axis, V::Str(before)] = vals else { return None }; + (int(axis)? == *AXES.get(n)?).then_some(())?; + self.axes.get_mut(n)?.before = before.clone(); + self.stage = Stage::AxisEnd(n); + } + ("GA", Stage::AxisEnd(n)) => { + let [manual, ticks, min, max, between, small, between_labels, V::Str(after)] = vals else { return None }; + let a = self.axes.get_mut(n)?; + (a.manual, a.tick) = (flag(manual)?, tick(ticks)?); + (a.min, a.max, a.between) = (finite(min)?, finite(max)?, finite(between)?); + a.small = count(small, 1000)?; + a.between_labels = flag(between_labels)?; + a.after = after.clone(); + self.stage = Stage::Axis(n + 1); + } + ("Gw", Stage::Axis(3) | Stage::Widths) => { + // `col width1 … widthX numParams Gw`: numParams counts `col` and the 1 to 14 widths. + let (params, rest) = vals.split_last()?; + let params = count(params, 15).filter(|p| *p >= 2 && *p == rest.len())?; + let (col, widths) = rest.split_first()?; + let col = count(col, MAX_CELLS)?; + widths.iter().all(|w| int(w).is_some_and(|w| w == -1 || (3..=20).contains(&w))).then_some(())?; + self.widths.push((col, params - 1)); + self.stage = Stage::Widths; + } + ("Gz", Stage::Axis(3) | Stage::Widths) => { + let [rows, cols, first_row, first_col] = vals else { return None }; + let (rows, cols) = (count(rows, MAX_CELLS)?, count(cols, MAX_CELLS)?); + let (first_row, first_col) = (usize::from(flag(first_row)?), usize::from(flag(first_col)?)); + rows.checked_sub(first_row).filter(|c| (1..=MAX_GRAPH_CATEGORIES).contains(c))?; + cols.checked_sub(first_col).filter(|s| (1..=MAX_GRAPH_SERIES).contains(s))?; + rows.checked_mul(cols).filter(|n| *n <= MAX_CELLS)?; + self.widths.iter().all(|(col, n)| col.checked_add(*n).is_some_and(|end| end <= cols)).then_some(())?; + self.size = Some([rows, cols, first_row, first_col]); + self.stage = Stage::Cells; + } + ("Gc", Stage::Cells) => { + // One to fifteen values, at most one of them a string. + let strings = vals.iter().filter(|v| matches!(v, V::Str(_))).count(); + ((1..=15).contains(&vals.len()) && strings <= 1).then_some(())?; + let [rows, cols, ..] = self.size?; + (self.cells.len() + vals.len() <= rows * cols).then_some(())?; + for v in vals { + self.cells.push(match v { + V::Str(s) if s.len() <= MAX_CELL_TEXT => Cell::Text(s.clone()), + _ => Cell::Num(finite(v)?), + }); + } + } + ("GC", Stage::Cells) if vals.is_empty() => self.stage = Stage::Done, + // `version Gt`, of version 2. Customizations are edits made after the graph was built, which + // the model can't replay: only an empty list keeps the graph live. + ("Gt", Stage::Done) => { + let [version] = vals else { return None }; + (int(version)? == CUSTOM_VERSION).then_some(())?; + self.stage = Stage::Custom; + } + ("GT", Stage::Custom) if vals.is_empty() => self.stage = Stage::Done, + _ => return None, + } + Some(()) + } + + /// `graphType shadow dataPointOrder pieLegendStyle drawMarks drawLines drawLinesAsShapes + /// drawLegendsAcrossTop lineShapeWidth whichAxis [piePercentage piePctDigits] Gy`. + fn style(&mut self, vals: &[V]) -> Option<()> { + let (base, pie) = match vals.len() { + 10 => (vals, None), + 12 => vals.split_at_checked(10).map(|(b, p)| (b, Some(p)))?, + _ => return None, + }; + let [ty, shadow, order, legend, marks, lines, shapes, across, width, axis] = base else { return None }; + self.spec.kind = kind(ty)?; + self.spec.drop_shadow = flag(shadow)?; + self.spec.first_row_in_front = flag(order)?; + // 14: legends as in bar and line graphs; 15 (in wedges) and 16 (none) are pie styles. + (int(legend)? == 14).then_some(())?; + self.spec.legend = true; + self.spec.mark_points = flag(marks)?; + self.spec.connect_points = flag(lines)?; + // Lines drawn as shapes have a width the model doesn't keep. + (!flag(shapes)?).then_some(())?; + self.spec.filled_lines = false; + self.spec.legend_across_top = flag(across)?; + finite(width).filter(|w| (0.0..=100.0).contains(w))?; + // 44: the series are measured against the left axis; 45 (right) isn't kept per series. + (int(axis)? == 44).then_some(())?; + if let Some([percentage, digits]) = pie { + // Pie percentages, which only pie graphs show; shown (1) isn't kept. + (!flag(percentage)?).then_some(())?; + int(digits).filter(|d| *d >= 0)?; + } + Some(()) + } + + /// The graph's data and settings, its strings read as names in `names`; `None` when the table + /// or an axis holds what the model can't. + fn finish(&self, names: &mut obj::Names) -> Option { + let [rows, cols, first_row, first_col] = self.size?; + let mut spec = self.spec.clone(); + let text = |names: &mut obj::Names, b: &[u8], max: usize| { + let t = names.quiet(b); + (t.chars().count() <= max && !t.chars().any(char::is_control)).then(|| names.bytes(b)) + }; + // Axes: the bottom one is the category axis, the left one the value axis; the right one keeps + // its settings for when it is shown. + let [bottom, left, right] = &self.axes; + (bottom.before.is_empty() && bottom.after.is_empty() && !bottom.manual).then_some(())?; + (!left.between_labels && !right.between_labels).then_some(())?; + (spec.category_tick_length, spec.category_tick_marks) = (bottom.tick, bottom.small.max(1)); + spec.ticks_between_labels = bottom.between_labels; + (spec.tick_length, spec.tick_marks) = (left.tick, left.small.max(1)); + (spec.right_tick_length, spec.right_tick_marks) = (right.tick, right.small.max(1)); + (spec.category_tick_marks <= MAX_TICK_MARKS && spec.tick_marks <= MAX_TICK_MARKS && spec.right_tick_marks <= MAX_TICK_MARKS).then_some(())?; + (spec.ticks, spec.axis_min, spec.axis_max) = values(left)?; + (spec.right_ticks, spec.right_axis_min, spec.right_axis_max) = values(right)?; + spec.prefix = text(names, &left.before, MAX_AFFIX)?; + spec.suffix = text(names, &left.after, MAX_AFFIX)?; + spec.right_prefix = text(names, &right.before, MAX_AFFIX)?; + spec.right_suffix = text(names, &right.after, MAX_AFFIX)?; + // The table, row by row; cells left out at its end are empty. + let cell = |r: usize, c: usize| self.cells.get(r * cols + c); + let label = |names: &mut obj::Names, r: usize, c: usize| match cell(r, c) { + None => Some(String::new()), + Some(Cell::Text(t)) => text(names, t, MAX_CELL_TEXT), + Some(Cell::Num(_)) => None, + }; + // The corner of a table with both label row and label column holds nothing. + let corner_empty = match cell(0, 0) { + None => true, + Some(Cell::Text(t)) => t.is_empty(), + Some(Cell::Num(_)) => false, + }; + if first_row == 1 && first_col == 1 && !corner_empty { + return None; + } + spec.series = if first_row == 1 { (first_col..cols).map(|c| label(names, 0, c)).collect::>()? } else { vec![] }; + spec.categories = if first_col == 1 { (first_row..rows).map(|r| label(names, r, 0)).collect::>()? } else { vec![] }; + let mut data = Vec::with_capacity(rows - first_row); + for r in first_row..rows { + let row = (first_col..cols) + .map(|c| match cell(r, c) { + None => Some(None), + Some(Cell::Text(t)) if t.is_empty() => Some(None), + Some(Cell::Text(_)) => None, + Some(Cell::Num(v)) => Some(Some(*v)), + }) + .collect::>>()?; + data.push(row); + } + data.iter().flatten().any(Option::is_some).then_some(())?; + spec.set_cells(data); + Some(spec) + } + + /// The series index of each group `Go` tagged as one series, by id: every series once, its + /// columns counted in the table or among the series alike. `None` when the tags are missing, + /// repeated or out of range: the series' paint is read from its group. + fn series_tags(&self) -> Option> { + let [_, cols, _, first_col] = self.size?; + let n = cols - first_col; + let ids: HashSet = self.series.iter().map(|(id, _)| *id).collect(); + (ids.len() == self.series.len() && self.series.len() == n).then_some(())?; + let indexes = |offset: usize| -> Option> { + let mut seen = HashSet::new(); + let tags: HashMap = self + .series + .iter() + .map(|(id, col)| { + let s = col.checked_sub(offset).filter(|s| *s < n && seen.insert(*s))?; + Some((*id, u32::try_from(s).ok()?)) + }) + .collect::>()?; + Some(tags) + }; + // With a label column, the columns read as the table's or as the series' are told apart by the + // set they make; without one, they are the same. + match (indexes(0), indexes(first_col)) { + (Some(a), Some(b)) if a == b => Some(a), + (Some(a), None) | (None, Some(a)) => Some(a), + _ => None, + } + } +} + +/// Whether `art` draws the plot `plot`'s left edge and its bottom edge (the document's y runs down) +/// as straight stroked lines: the value axis and the category axis, drawn on the plot's edges. +fn draws_axes(art: &Node, plot: Rect) -> bool { + let near = |a: Point, b: Point| (a - b).hypot() <= EDGE; + let on = |s: &PathSeg, a: Point, b: Point| match s { + PathSeg::Line(l) => (near(l.p0, a) && near(l.p1, b)) || (near(l.p0, b) && near(l.p1, a)), + _ => false, + }; + let (top_left, bottom_left, bottom_right) = (Point::new(plot.x0, plot.y0), Point::new(plot.x0, plot.y1), Point::new(plot.x1, plot.y1)); + let (mut left, mut bottom) = (false, false); + let mut stack = vec![art]; + while let Some(n) = stack.pop() { + // Only shown objects draw the axes, and a path only with a stroke. + if !n.visible || n.opacity <= 0.0 || (left && bottom) { + continue; + } + let stroked = n.appearance.items.iter().any(|it| !it.is_fill() && it.visible() && it.opacity() > 0.0 && !it.paint().is_none()); + if let NodeKind::Path { path, .. } = &n.kind + && stroked + { + for s in path.to_bezpath().segments() { + left |= on(&s, top_left, bottom_left); + bottom |= on(&s, bottom_left, bottom_right); + } + } + stack.extend(n.children().into_iter().flatten().map(|c| &**c)); + } + left && bottom +} + +/// Give the groups among `n`'s children that `tags` holds their series index. +fn tag_series(n: &mut Node, tags: &HashMap) { + for c in n.children_mut().into_iter().flatten() { + if let Some(i) = tags.get(&c.id) { + Arc::make_mut(c).extras_mut().series_index = Some(*i); + } + } +} + +/// Whether every node `tags` holds is a group among `n`'s children: the graph draws its series +/// groups again as its own children. +fn tags_groups(n: &Node, tags: &HashMap) -> bool { + let found = n.children().into_iter().flatten().filter(|c| tags.contains_key(&c.id) && matches!(c.kind, NodeKind::Group { .. })).count(); + found == tags.len() +} + +/// The paint of the shown fill (`fill`) or stroke of `p`, with a stroke's weight; `None` when it +/// has none. `Err` when it has more than one, or that fill or stroke has an opacity, a blend mode +/// or effects of its own, which a series' paint doesn't hold. +fn shown(p: &Node, fill: bool) -> Result, ()> { + let mut items = p.appearance.items.iter().filter(|it| it.is_fill() == fill && it.visible()); + let Some(item) = items.next() else { return Ok(None) }; + if items.next().is_some() || item.opacity() != 1.0 || item.blend() != BlendMode::Normal || !item.effects().is_empty() { + return Err(()); + } + if item.paint().is_none() { + return Ok(None); + } + let width = match item { + AppearanceItem::Stroke(s) => s.width, + AppearanceItem::Fill(_) => 0.0, + }; + Ok(Some((item.paint().clone(), width))) +} + +/// Whether `n` is shown, fully opaque, in Normal mode, not isolated and without an opacity mask: +/// the graph draws its series groups and their marks so. +fn as_drawn(n: &Node) -> bool { + n.visible && n.opacity == 1.0 && n.blend == BlendMode::Normal && !n.isolate && n.extras().mask.is_none() +} + +/// Each series' fill and stroke, read from its group (`tags`), which holds paths only, one at +/// least: the paths with a fill share it, and every path has the same stroke or none. Only a line +/// graph (`line`) has paths without a fill: its lines. `None` when they differ, or the group, its +/// own appearance or a path is drawn otherwise than [`as_drawn`]. +fn series_paints(art: &Node, tags: &HashMap, line: bool) -> Option> { + let mut paints = vec![SeriesPaint::default(); tags.len()]; + for group in art.children().into_iter().flatten() { + let Some(slot) = tags.get(&group.id).and_then(|i| paints.get_mut(usize::try_from(*i).ok()?)) else { continue }; + (as_drawn(group) && *group.appearance == Appearance::default()).then_some(())?; + let paths: Vec<&Arc> = group.children().into_iter().flatten().collect(); + (!paths.is_empty() && paths.iter().all(|p| matches!(p.kind, NodeKind::Path { .. }))).then_some(())?; + let (mut fills, mut strokes) = (vec![], vec![]); + for p in &paths { + as_drawn(p).then_some(())?; + fills.extend(shown(p, true).ok()?); + strokes.push(shown(p, false).ok()?); + } + let fill = fills.first().map(|(paint, _)| paint.clone()); + fills.iter().all(|(paint, _)| Some(paint) == fill.as_ref()).then_some(())?; + (line || fills.len() == paths.len()).then_some(())?; + let stroke = strokes.first().cloned().flatten(); + strokes.iter().all(|s| *s == stroke).then_some(())?; + slot.fill = fill; + (slot.stroke, slot.stroke_width) = match stroke { + Some((paint, width)) => (Some(paint), Some(width)), + None => (Some(Paint::None), None), + }; + } + Some(paints) +} + +impl<'a> Reader<'a> { + /// A graph's records: `%_Gs` starts the graph on a layer, `GS` ends it, and its data and settings + /// (the operators in [`GRAPH_OPS`], and any operator on a `%_` line between `Gt` and `GT`) are + /// taken with their operands `vals`. Its drawn art, and anything else, is read as usual. `true` + /// when `w` was one of them. `ended`: whether a group and a mesh had just ended before `w`, which + /// a `Go` tag after them leaves so. + pub(super) fn graph_record(&mut self, w: &str, vals: &[V], hidden: bool, ended: (bool, bool)) -> bool { + let depth = self.frames.len(); + let Some(g) = self.graph.as_mut() else { + // Outside a graph, its operators stay ones the reader doesn't know. + if w == "Gs" && hidden && !self.frames.is_empty() { + let siblings = self.frames.last().map_or(0, |f| f.children.len()); + // A graph is live only where its art is in the document's space, in a layer or a group. + let placed = self.pattern.is_none() + && self.symbol.is_none() + && vals.is_empty() + && matches!(self.frames.last().map(|f| &f.kind), Some(Kind::Layer(_) | Kind::Group | Kind::Clip)); + self.graph = Some(Graph { settings: false, frames: depth, siblings, live: placed.then(Box::default) }); + return true; + } + return false; + }; + // The frame the graph started in closed: its art isn't where it was. + if depth < g.frames { + self.graph_plain(); + } + let Some(g) = self.graph.as_mut() else { return false }; + let stage = g.live.as_ref().map(|r| r.stage); + match w { + // A graph doesn't start inside another: the one being read goes on. + "Gs" => { + self.graph_plain(); + self.warn(GRAPH_UNENDED); + } + "GS" => self.graph_end(vals.is_empty() && hidden), + "Gt" | "GT" => { + g.settings = w == "Gt"; + self.graph_step(w, vals, hidden); + } + "Go" => { + self.graph_tag(vals, hidden); + (self.group_ended, self.mesh_ended) = ended; + } + _ if GRAPH_OPS.contains(&w) => self.graph_step(w, vals, hidden), + _ if hidden && g.settings => self.graph_plain(), + _ => { + // Before its table ends, a graph has only its records. + if !matches!(stage, None | Some(Stage::Done)) { + self.graph_plain(); + } + return false; + } + } + true + } + + /// The graph being read can't be live: its art stays plain, with a note. + pub(super) fn graph_plain(&mut self) { + if let Some(g) = self.graph.as_mut() { + g.live = None; + } + self.warn(GRAPH_PLAIN); + } + + /// Take a functional record or customization on a `%_` line. + fn graph_step(&mut self, w: &str, vals: &[V], hidden: bool) { + if self.graph.as_ref().is_none_or(|g| g.live.is_none()) { + return; + } + let to_doc = if w == "Gb" { self.space() } else { Affine::IDENTITY }; + let ok = hidden && self.graph.as_mut().and_then(|g| g.live.as_mut()).and_then(|r| r.step(w, vals, to_doc)).is_some(); + if !ok { + self.graph_plain(); + } + } + + /// `target column row whichAxis Go` after an object of the drawn art: a group tagged as one + /// series is kept with the column it names. + fn graph_tag(&mut self, vals: &[V], hidden: bool) { + let depth = self.frames.len(); + let last = self + .after_object + .then(|| self.frames.last().and_then(|f| f.children.last())) + .flatten() + .map(|n| (n.id, matches!(n.kind, NodeKind::Group { .. }))); + let Some(g) = self.graph.as_mut() else { return }; + let inside = depth > g.frames; + let Some(r) = g.live.as_mut() else { return }; + let tag = match vals { + [target, column, row, axis] if hidden && r.stage == Stage::Done => { + let (row, axis) = (int(row), int(axis)); + // A series is the whole column, measured on the left axis (2) or with no axis given (0): + // the model keeps no series on a row or on the right axis. + let whole_column = row == Some(0) && matches!(axis, Some(0 | 2)); + int(target).filter(|t| TARGETS.contains(t) && row.is_some() && axis.is_some()).zip(int(column)).map(|(t, c)| (t, c, whole_column)) + } + _ => None, + }; + let ok = match (tag, last) { + (Some((SERIES, column, true)), Some((id, true))) if inside => usize::try_from(column).ok().map(|c| r.series.push((id, c))).is_some(), + (Some((SERIES, ..)), _) => false, + (Some(_), Some(_)) => true, + _ => false, + }; + if !ok { + self.graph_plain(); + } + } + + /// `GS`: the graph ends. Its drawn art is the one object its frame gained, a group; it keeps the + /// graph's data and settings when they read whole (`whole`) and the art is registered on its plot. + fn graph_end(&mut self, whole: bool) { + let Some(g) = self.graph.take() else { return }; + let depth = self.frames.len(); + let live = g.live.filter(|r| whole && r.stage == Stage::Done && depth == g.frames); + let spec = live.as_ref().and_then(|r| r.finish(&mut self.names).zip(r.series_tags())); + let attached = spec.is_some_and(|(mut spec, tags)| { + let Some(art) = self.frames.last_mut().filter(|f| f.children.len() == g.siblings + 1).and_then(|f| f.children.last_mut()) else { + return false; + }; + if !matches!(art.kind, NodeKind::Group { clip: false, .. }) || !tags_groups(art, &tags) || !draws_axes(art, spec.rect) { + return false; + } + let Some(placed) = art.geometric_bounds().filter(|b| b.width() > 0.0 && b.height() > 0.0) else { return false }; + let r = spec.rect; + if r.x0 < placed.x0 - EDGE || r.y0 < placed.y0 - EDGE || r.x1 > placed.x1 + EDGE || r.y1 > placed.y1 + EDGE { + return false; + } + let Some(paints) = series_paints(art, &tags, spec.kind == GraphKind::Line) else { return false }; + spec.series_paints = paints; + spec.placed = Some(placed); + tag_series(art, &tags); + art.extras_mut().graph = Some(Box::new(spec)); + true + }); + if !attached { + self.warn(GRAPH_PLAIN); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use vectorcraft_color::Color; + use vectorcraft_geom::shapes; + + /// A graph's art: one series group (id 2) holding one mark painted `ap`, the group changed by + /// `group`. + fn art(ap: Appearance, group: impl Fn(&mut Node)) -> Node { + let mark = Arc::new(Node::path(NodeId(3), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), ap)); + let mut series = Node::group(NodeId(2), vec![mark]); + group(&mut series); + Node::group(NodeId(1), vec![Arc::new(series)]) + } + + /// A series whose mark has two fills has no one paint, so the graph can't draw it again. + #[test] + fn a_series_mark_with_two_fills_has_no_series_paint() { + let red = Paint::solid(Color::rgb(1.0, 0.0, 0.0)); + let one = Appearance::basic(red.clone(), Paint::None, 0.0); + let mut two = one.clone(); + two.items.extend(one.items.iter().find(|i| i.is_fill()).cloned()); + let tags = HashMap::from([(NodeId(2), 0)]); + let kept = series_paints(&art(one, |_| {}), &tags, false).expect("one fill"); + assert_eq!((kept[0].fill.as_ref(), kept[0].stroke.as_ref()), (Some(&red), Some(&Paint::None))); + assert!(series_paints(&art(two, |_| {}), &tags, false).is_none()); + } + + /// A series group that is hidden, see-through or in another blending mode is not drawn again so. + #[test] + fn a_series_group_drawn_otherwise_has_no_series_paint() { + let ap = || Appearance::basic(Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0); + let tags = HashMap::from([(NodeId(2), 0)]); + assert!(series_paints(&art(ap(), |g| g.visible = false), &tags, false).is_none(), "hidden"); + assert!(series_paints(&art(ap(), |g| g.opacity = 0.5), &tags, false).is_none(), "50% opaque"); + assert!(series_paints(&art(ap(), |g| g.blend = BlendMode::Multiply), &tags, false).is_none(), "Multiply"); + } +} diff --git a/crates/eps/src/import/ai/read/ops.rs b/crates/eps/src/import/ai/read/ops.rs index 83d0f71d40..3c642c5a02 100644 --- a/crates/eps/src/import/ai/read/ops.rs +++ b/crates/eps/src/import/ai/read/ops.rs @@ -1,12 +1,9 @@ use super::containers::close; use super::document::slot_of; -use super::types::{ - BLENDS, FLIP, Frame, GRAPH_OPS, GRAPH_PLAIN, GRAPH_UNENDED, IGNORED, Kind, MAX_DEPTH, MAX_NODES, MAX_TAG_TEXT, MAX_TAGS, Place, URL_TAG, - line_width, -}; +use super::types::{BLENDS, FLIP, Frame, GRAPH_UNENDED, IGNORED, Kind, MAX_DEPTH, MAX_NODES, MAX_TAG_TEXT, MAX_TAGS, Place, URL_TAG, line_width}; use super::{ - Affine, Appearance, AppearanceItem, Arc, BlendMode, BrushesKept, Dash, FillLayer, FillRule, Global, Gradient, GradientDef, Graph, HashMap, - HashSet, Instance, Knockout, LayerColor, LineCap, LineJoin, Node, NodeId, NodeKind, Paint, Reader, Rect, ShownNames, StrokeLayer, V, obj, paint, + Affine, Appearance, AppearanceItem, Arc, BlendMode, BrushesKept, Dash, FillLayer, FillRule, Global, Gradient, GradientDef, HashMap, HashSet, + Instance, Knockout, LayerColor, LineCap, LineJoin, Node, NodeId, NodeKind, Paint, Reader, Rect, ShownNames, StrokeLayer, V, obj, paint, }; impl<'a> Reader<'a> { @@ -69,6 +66,7 @@ impl<'a> Reader<'a> { /// `LB`: a graph started in this layer that hasn't ended doesn't go on past it; the layer ends. fn layer_end(&mut self) -> Result<(), String> { if self.graph.as_ref().is_some_and(|g| g.frames >= self.frames.len()) { + self.graph_plain(); self.graph = None; self.warn(GRAPH_UNENDED); } @@ -158,7 +156,7 @@ impl<'a> Reader<'a> { let mesh_ended = std::mem::take(&mut self.mesh_ended); let vals = self.operands(); let vals = self.take_head(vals); - if self.graph_record(w, hidden) { + if self.graph_record(w, &vals, hidden, (group_ended, mesh_ended)) { return Ok(()); } // Only `Bh` reads an undefined number. Anywhere else it would reach a point or a size, so @@ -425,33 +423,6 @@ impl<'a> Reader<'a> { Ok(()) } - /// A graph's records (version 7 specification §17 and appendix A): `%_Gs` starts the graph on - /// a layer, `GS` ends it, and its data and settings (the operators in [`GRAPH_OPS`], and any - /// operator on a `%_` line between `Gt` and `GT`) are left out with their operands. Its drawn - /// art, and anything else, is read as usual. `true` when `w` was one of them. - fn graph_record(&mut self, w: &str, hidden: bool) -> bool { - let Some(g) = self.graph.as_mut() else { - // Outside a graph, its operators stay ones the reader doesn't know. - if w == "Gs" && hidden && !self.frames.is_empty() { - self.graph = Some(Graph { settings: false, frames: self.frames.len() }); - self.warn(GRAPH_PLAIN); - return true; - } - return false; - }; - match w { - // A graph doesn't start inside another: the one being read goes on. - "Gs" => self.warn(GRAPH_UNENDED), - "GS" => self.graph = None, - "Gt" => g.settings = true, - "GT" => g.settings = false, - _ if GRAPH_OPS.contains(&w) => {} - _ if hidden && g.settings => {} - _ => return false, - } - true - } - /// `… Lb`: a layer's options, `visible preview unlocked printing dimmed`, then the colour index /// and its RGB. The colour index is at 5 with fewer than ten operands, 6 with ten to twelve /// (`masks` before it) and 7 with thirteen or more (one more flag before it). Four places after @@ -585,9 +556,9 @@ impl<'a> Reader<'a> { return; } self.image_tail = false; - // An object's tags (`XT`) follow it one after another, and an image's follow the `XH` that - // ends its data. - if matches!(w, "Xy" | "Xd" | "XW" | "BB" | "LB" | "XT" | "XH") { + // An object's tags (`XT`, and a graph's `Go`) follow it one after another, and an image's + // follow the `XH` that ends its data. + if matches!(w, "Xy" | "Xd" | "XW" | "BB" | "LB" | "XT" | "XH" | "Go") { return; } self.after_object = false; diff --git a/crates/eps/src/import/ai/read/types.rs b/crates/eps/src/import/ai/read/types.rs index 6457c1cde1..83a838a6cb 100644 --- a/crates/eps/src/import/ai/read/types.rs +++ b/crates/eps/src/import/ai/read/types.rs @@ -436,8 +436,8 @@ pub const NON_NATIVE_ART_UNREAD: &str = "non-native art (a placed PDF's content) /// The name of the group a `/ForeignObject`'s art comes in as. pub const NON_NATIVE_ART: &str = "Non-native art"; -/// The note for a file with a graph (`%_Gs` … `%_GS`): its drawn art comes in, its data and -/// settings don't. +/// The note for a file with a graph (`%_Gs` … `%_GS`) whose data and settings can't be kept: its +/// drawn art comes in as plain art. pub const GRAPH_PLAIN: &str = "graphs come in as plain art, without their data or settings"; /// The note for a graph that starts inside another or doesn't end before its layer does: the art @@ -445,18 +445,8 @@ pub const GRAPH_PLAIN: &str = "graphs come in as plain art, without their data o pub const GRAPH_UNENDED: &str = "a graph's records don't end where they should: its art was read as it is"; /// A graph's records (the version 7 specification's chapter 17 and appendix A): two-letter -/// operators starting with `G`, read and left out with their operands. +/// operators starting with `G`, read with their operands. pub(super) const GRAPH_OPS: &[&str] = &[ "G1", "G+", "Ga", "GA", "Gb", "Gc", "GC", "Gd", "GD", "Ge", "Gf", "Gg", "Gi", "Gj", "Gl", "Gm", "Go", "Gp", "Gr", "Gs", "GS", "Gt", "GT", "Gv", "Gw", "GX", "Gx", "Gy", "Gz", ]; - -/// A graph being read (`%_Gs` … `%_GS`). -#[derive(Debug)] -pub(super) struct Graph { - /// Between `Gt` and `GT`, where the graph's settings may use paint and type operators on `%_` - /// lines too. - pub(super) settings: bool, - /// How many frames were open at its `Gs`: a layer that ends with fewer ends the graph. - pub(super) frames: usize, -} diff --git a/crates/eps/src/import/ai/tests.rs b/crates/eps/src/import/ai/tests.rs index 23d2536adc..346f31210e 100644 --- a/crates/eps/src/import/ai/tests.rs +++ b/crates/eps/src/import/ai/tests.rs @@ -10434,6 +10434,7 @@ fn type_with_an_art_brush_keeps_its_drawn_look() { } use read::{GRAPH_PLAIN, GRAPH_UNENDED}; +use vectorcraft_doc::{GraphKind, TickLength}; /// Editing data in the legacy format (version 7 specification, chapter 2): `%AI5_FileFormat 2.0`, /// the art size and the template box in the header, then `body`. @@ -10575,6 +10576,223 @@ fn a_graphs_records_take_their_operands() { assert_eq!(r.warnings, [GRAPH_PLAIN]); } +/// A graph's style record (`Gy`) for a grouped column graph: no shadow, legends as in column +/// graphs, lines without marks, measured against the left axis. +const LIVE_STYLE: &str = "%_5 0 0 14 0 1 0 0 6 44 Gy\n"; + +/// A graph's dialog record (`Gd`): columns 90% wide in clusters 80% wide. +const LIVE_DIALOG: &str = "%_10 2 0 0 90 80 0 Gd\n"; + +/// A graph's axes, bottom, left and right: the bottom one with no tick marks, between its labels; +/// the left one labeled `$` … ` units`, with two tick marks a division. +const LIVE_AXES: &str = "%_1 () Ga\n%_0 13 0 0 0 0 1 () GA\n%_2 ($) Ga\n%_0 14 0 0 0 2 0 ( units) GA\n%_4 () Ga\n%_0 14 0 0 0 0 0 () GA\n"; + +/// A graph's table: series East and West, categories Q1 to Q3, and an empty cell (West in Q2). +const LIVE_TABLE: &str = "%_4 3 1 1 Gz\n%_() Gc (East) Gc (West) Gc\n%_(Q1) Gc 5 3 (Q2) Gc 7 () Gc (Q3) Gc 4 6 Gc GC\n"; + +/// A column of a graph's series at `x`, tagged as one data bar. +fn series_bar(x: f64) -> String { + format!("0 0 0 1 k\n{}f\n%_4 1 0 0 Go\n", rect_path(x, 100.0, x + 20.0, 150.0)) +} + +/// The first series' mark in [`live_art`]. +fn series_mark() -> String { + format!("0 0 0 1 k\n{}f\n", rect_path(110.0, 100.0, 130.0, 150.0)) +} + +/// [`series_mark`] filled and stroked, then an outline stroked alike with no fill. +fn series_mark_and_outline() -> String { + format!("0 0 0 1 k\n0 0 0 1 K\n{}B\n{}S\n", rect_path(110.0, 100.0, 130.0, 150.0), rect_path(112.0, 100.0, 118.0, 120.0)) +} + +/// The drawn art of the graph [`live_graph`] gives: two series groups, each tagged as one series by +/// its table column, and the axes on the plot's left and bottom edges. +fn live_art() -> String { + format!( + "u\n{}U\n%_2 1 0 0 Go\nu\n{}U\n%_2 2 0 0 Go\nu\n0 0 0 1 K\n100 200 m\n100 100 L\n300 100 L\nS\nU\n%_9 0 0 2 Go\n", + series_bar(110.0), + series_bar(140.0) + ) +} + +/// A graph whose records read whole, written from the version 7 specification's appendix A, on the +/// plot 100 100 300 200 (art space). +fn live_graph() -> String { + format!("%_Gs\n%_100 200 300 100 Gb\n{LIVE_STYLE}{LIVE_DIALOG}{LIVE_AXES}{LIVE_TABLE}u\n{}U\n%_GS\n", live_art()) +} + +/// The graphs in `doc`'s art, and the series groups tagged in them. +fn live_graphs(doc: &Document) -> (Vec, usize) { + let (mut graphs, mut tagged) = (vec![], 0); + doc.walk(|n| { + if n.extras().graph.is_some() { + graphs.push(n.clone()); + } + tagged += usize::from(n.extras().series_index.is_some()); + }); + (graphs, tagged) +} + +fn same_rect(a: Rect, b: Rect) -> bool { + (a.x0 - b.x0).abs() + (a.y0 - b.y0).abs() + (a.x1 - b.x1).abs() + (a.y1 - b.y1).abs() < 1e-9 +} + +/// A graph whose records read whole keeps its data and settings on its group, which holds its +/// drawn art as it is, each series group tagged with its index. +#[test] +fn a_graph_whose_records_read_whole_keeps_its_data_and_settings() { + let r = read(stream7(&layer7("Graph", &live_graph())).as_bytes()).unwrap(); + assert!(r.warnings.is_empty(), "{:?}", r.warnings); + let a = art(&r.doc); + assert_eq!(a.len(), 1); + let g = a[0].extras().graph.clone().expect("a live graph"); + assert_eq!(g.kind, GraphKind::Column); + assert_eq!(g.series, ["East", "West"]); + assert_eq!(g.categories, ["Q1", "Q2", "Q3"]); + assert_eq!(g.cells(), [[Some(5.0), Some(3.0)], [Some(7.0), None], [Some(4.0), Some(6.0)]]); + assert_eq!((g.column_width, g.cluster_width), (90.0, 80.0)); + assert_eq!((g.prefix.as_str(), g.suffix.as_str(), g.tick_length, g.tick_marks), ("$", " units", TickLength::Short, 2)); + assert_eq!((g.category_tick_length, g.ticks_between_labels), (TickLength::None, true)); + assert_eq!((g.ticks, g.axis_min, g.axis_max), (0, None, None)); + assert!(g.legend && !g.legend_across_top && !g.mark_points && g.connect_points && !g.drop_shadow && !g.edge_to_edge, "{g:?}"); + // The drawn art: two series groups and the axes, as written. + let kids = a[0].children().unwrap(); + assert_eq!(kids.len(), 3); + let index: Vec<_> = kids.iter().map(|k| k.extras().series_index).collect(); + assert_eq!(index, [Some(0), Some(1), None]); + assert!(kids[..2].iter().all(|k| k.children().is_some_and(|c| c.len() == 1))); + // Each series keeps its marks' fill, and no stroke, for when the graph is drawn again. + assert_eq!(g.series_paints.len(), 2); + let fill = kids[0].children().unwrap()[0].appearance.items.iter().find(|i| i.is_fill()).map(|i| i.paint().clone()); + assert!(g.series_paints.iter().all(|p| p.fill == fill && p.stroke.as_ref().is_some_and(|s| s.is_none())), "{:?}", g.series_paints); + let axes = kids[2].children().unwrap()[0].geometric_bounds().unwrap(); + assert!(same_rect(g.rect, axes), "the plot is where the axes are: {:?} {axes:?}", g.rect); + assert_eq!(g.placed, a[0].geometric_bounds()); +} + +/// The graph types the records give: 5 grouped column, 6 stacked column, 7 line, 10 area; and the +/// style and dialog settings, each kept as the graph's. +#[test] +fn a_graphs_type_and_settings_come_from_its_records() { + let live = |style: &str, dialog: &str, axes: &str| { + let text = live_graph().replace(LIVE_STYLE, style).replace(LIVE_DIALOG, dialog).replace(LIVE_AXES, axes); + let r = read(stream7(&layer7("Graph", &text)).as_bytes()).unwrap(); + assert!(r.warnings.is_empty(), "{style}{dialog}: {:?}", r.warnings); + art(&r.doc)[0].extras().graph.clone().expect("a live graph") + }; + for (ty, kind) in [(5, GraphKind::Column), (6, GraphKind::StackedColumn), (7, GraphKind::Line), (10, GraphKind::Area)] { + let g = live(&format!("%_{ty} 0 0 14 0 1 0 0 6 44 Gy\n"), LIVE_DIALOG, LIVE_AXES); + assert_eq!(g.kind, kind, "{ty}"); + } + let axes = LIVE_AXES.replace("%_0 14 0 0 0 2 0 ( units) GA", "%_1 15 0 100 25 0 0 () GA"); + let g = live("%_7 1 1 14 1 0 0 1 6 44 0 2 Gy\n", "%_10 2 1 0 120 50 1 Gd\n", &axes); + assert!(g.drop_shadow && g.first_row_in_front && g.mark_points && !g.connect_points && g.legend_across_top, "{g:?}"); + assert!(g.first_column_in_front && g.edge_to_edge, "{g:?}"); + assert_eq!((g.column_width, g.cluster_width), (120.0, 50.0)); + assert_eq!((g.ticks, g.axis_min, g.axis_max, g.tick_length, g.tick_marks), (4, Some(0.0), Some(100.0), TickLength::Full, 1)); + // A line graph's series draws its line unfilled beside its filled marks. + let text = live_graph().replace(LIVE_STYLE, "%_7 0 0 14 0 1 0 0 6 44 Gy\n").replacen(&series_mark(), &series_mark_and_outline(), 1); + let r = read(stream7(&layer7("Graph", &text)).as_bytes()).unwrap(); + assert!(r.warnings.is_empty(), "{:?}", r.warnings); + assert!(art(&r.doc)[0].extras().graph.is_some()); + // An empty list of customizations, of the version appendix A gives (2), sets nothing. + let text = live_graph().replace("GC\n", "GC\n%_2 Gt\n%_GT\n"); + let r = read(stream7(&layer7("Graph", &text)).as_bytes()).unwrap(); + assert!(r.warnings.is_empty(), "{:?}", r.warnings); + assert!(art(&r.doc)[0].extras().graph.is_some()); +} + +/// A graph keeps its drawn art and stays plain, with one note, when a record has a shape or value +/// appendix A doesn't give, sets what the model doesn't keep, or the table, the series tags or the +/// art don't fit the graph. +#[test] +fn graphs_the_model_cant_keep_stay_plain_art() { + let cases: Vec<(&str, String)> = vec![ + ("an eleventh style value", live_graph().replace(LIVE_STYLE, "%_5 0 0 14 0 1 0 0 6 44 0 Gy\n")), + ("eleven dialog values", live_graph().replace(LIVE_DIALOG, "%_10 2 0 0 90 80 0 0 0 0 0 Gd\n")), + ("a fourth axis", live_graph().replace(LIVE_AXES, &format!("{LIVE_AXES}%_8 () Ga\n%_0 14 0 0 0 0 0 () GA\n"))), + ("ten axis values", live_graph().replace("%_0 14 0 0 0 0 0 () GA", "%_0 14 0 0 0 0 0 () 0 0 GA")), + ("a pie graph", live_graph().replace("%_5 0 0 14", "%_8 0 0 14")), + ("a scatter graph", live_graph().replace("%_5 0 0 14", "%_9 0 0 14")), + ("type 11", live_graph().replace("%_5 0 0 14", "%_11 0 0 14")), + ("legends in wedges", live_graph().replace("%_5 0 0 14", "%_5 0 0 15")), + ("the right axis", live_graph().replace("6 44 Gy", "6 45 Gy")), + ("lines as shapes", live_graph().replace("%_5 0 0 14 0 1 0", "%_5 0 0 14 0 1 1")), + ("both axes alike", live_graph().replace(LIVE_DIALOG, "%_10 2 0 1 90 80 0 Gd\n")), + ("a subscription", live_graph().replace(LIVE_DIALOG, &format!("{LIVE_DIALOG}%_0 0 1 1 1 Gj\n"))), + ("a customization", live_graph().replace("GC\n", "GC\n%_2 Gt\n%_(Bars) GD\n%_GT\n")), + ("customizations without their version", live_graph().replace("GC\n", "GC\n%_Gt\n%_GT\n")), + ("customizations of another version", live_graph().replace("GC\n", "GC\n%_3 Gt\n%_GT\n")), + ("bounds upside down", live_graph().replace("%_100 200 300 100 Gb", "%_100 100 300 200 Gb")), + ("a plot the axes aren't on", live_graph().replace("%_100 200 300 100 Gb", "%_50 200 300 100 Gb")), + ("a text data cell", live_graph().replace("7 () Gc", "(seven) Gc () Gc")), + ("a table of empty cells", live_graph().replace(LIVE_TABLE, "%_4 3 1 1 Gz\n%_() Gc (East) Gc (West) Gc\n%_GC\n")), + ("a cell past the table", live_graph().replace("4 6 Gc GC", "4 6 1 Gc GC")), + ("too many categories", live_graph().replace("%_4 3 1 1 Gz", "%_10002 3 1 1 Gz")), + ("a table too large", live_graph().replace("%_4 3 1 1 Gz", "%_4000000000 4000000000 1 1 Gz")), + ("a number as a label", live_graph().replace("(West) Gc", "2025 Gc")), + ("a series untagged", live_graph().replace("%_2 2 0 0 Go", "%_4 2 0 0 Go")), + ("a series tagged twice", live_graph().replace("%_2 2 0 0 Go", "%_2 1 0 0 Go")), + ("a path tagged as a series", live_graph().replacen("%_4 1 0 0 Go", "%_2 1 0 0 Go", 1)), + ("a target the specification doesn't list", live_graph().replace("%_9 0 0 2 Go", "%_3 0 0 2 Go")), + ("art before its group", live_graph().replace("GC\nu\n", &format!("GC\n{}f\nu\n", rect_path(0.0, 0.0, 5.0, 5.0)))), + ("a record on a line that shows", live_graph().replace("%_10 2 0 0 90 80 0 Gd", "10 2 0 0 90 80 0 Gd")), + ("series groups inside another group", live_graph().replace("GC\nu\n", "GC\nu\nu\n").replace("U\n%_GS", "U\nU\n%_GS")), + ( + "two fills in one series", + live_graph().replacen( + "f\n%_4 1 0 0 Go\nU\n", + &format!("f\n%_4 1 0 0 Go\n0 0 0 0.5 k\n{}f\nU\n", rect_path(112.0, 100.0, 118.0, 120.0)), + 1, + ), + ), + ("axes filled, not stroked", live_graph().replace("300 100 L\nS\n", "300 100 L\nf\n")), + ("a series tagged on a row", live_graph().replace("%_2 2 0 0 Go", "%_2 2 1 0 Go")), + ("a series tagged on the right axis", live_graph().replace("%_2 2 0 0 Go", "%_2 2 0 4 Go")), + ("an unfilled mark in a column series", live_graph().replacen(&series_mark(), &series_mark_and_outline(), 1)), + ("a series' mark in a group of its own", live_graph().replacen(&series_mark(), &format!("u\n{}U\n", series_mark()), 1)), + ( + "an untagged graph of one series", + live_graph() + .replace(LIVE_TABLE, "%_2 2 1 1 Gz\n%_() Gc (East) Gc (Q1) Gc 5 Gc GC\n") + .replace("%_2 2 0 0 Go", "%_4 1 0 0 Go") + .replace("%_2 1 0 0 Go", "%_4 1 0 0 Go"), + ), + ]; + for (what, text) in cases { + let r = read(stream7(&layer7("Graph", &text)).as_bytes()).unwrap_or_else(|e| panic!("{what}: {e}")); + assert_eq!(r.warnings, [GRAPH_PLAIN], "{what}"); + assert_eq!(live_graphs(&r.doc), (vec![], 0), "{what}"); + let mut kept = false; + r.doc.walk(|n| kept |= n.children().is_some_and(|c| c.len() == 3)); + assert!(kept, "{what}: the drawn art is kept"); + } +} + +/// A graph in a group or a clipping group keeps its data on its own group, not the container's; of +/// two graphs in one layer, each is kept or left plain on its own, and the note is given once. +#[test] +fn graphs_are_kept_one_by_one_wherever_they_are() { + let clip = format!("q\n{}W n\n{}Q\n", rect_path(0.0, 0.0, 400.0, 400.0), live_graph()); + for (what, text) in [("in a group", format!("u\n{}U\n", live_graph())), ("in a clipping group", clip)] { + let r = read(stream7(&layer7("Graph", &text)).as_bytes()).unwrap(); + assert!(r.warnings.is_empty(), "{what}: {:?}", r.warnings); + let outer = &art(&r.doc)[0]; + assert!(outer.extras().graph.is_none(), "{what}"); + let (graphs, tagged) = live_graphs(&r.doc); + assert_eq!((graphs.len(), tagged), (1, 2), "{what}"); + assert!(outer.children().unwrap().iter().any(|c| c.id == graphs[0].id), "{what}: the graph's own group"); + } + let pie = live_graph().replace("%_5 0 0 14", "%_8 0 0 14"); + let body = format!("{}{pie}{}", live_graph(), live_graph()); + let r = read(stream7(&layer7("Graph", &body)).as_bytes()).unwrap(); + assert_eq!(r.warnings, [GRAPH_PLAIN]); + let a = art(&r.doc); + assert_eq!(a.len(), 3); + let live: Vec = a.iter().map(|n| n.extras().graph.is_some()).collect(); + assert_eq!(live, [true, false, true]); +} + /// Editing data in the legacy format (`%AI5_FileFormat 2.0`, ten `Lb` operands), then `body`. fn mask_stream7(body: &str) -> String { format!( diff --git a/crates/eps/src/legacy_ai.rs b/crates/eps/src/legacy_ai.rs index 00bb29d12f..71648f955a 100644 --- a/crates/eps/src/legacy_ai.rs +++ b/crates/eps/src/legacy_ai.rs @@ -122,6 +122,7 @@ const STROKES: &str = "strokes a plain line can't draw (inside or outside the path, variable width, gradients, brushes, arrowheads) are written as filled outlines"; const IMAGE_ALPHA: &str = "transparent image pixels are written over white"; const LIVE: &str = "symbols, blends, envelopes, meshes, repeats, placed documents and intertwines are written as their art"; +const GRAPHS: &str = "graphs are written as their art, without their data or settings"; /// The warning for an image left out because it can't be decoded: the note raster exports give /// (`vectorcraft-engine`'s `rasterfx::undecoded_warning`). const UNDECODED: &str = "an image couldn't be decoded and is missing from the picture"; diff --git a/crates/eps/src/legacy_ai/writer.rs b/crates/eps/src/legacy_ai/writer.rs index 20f8b32b96..c6f99eee79 100644 --- a/crates/eps/src/legacy_ai/writer.rs +++ b/crates/eps/src/legacy_ai/writer.rs @@ -9,7 +9,7 @@ use super::{ ROTATED_TYPE, Rect, STROKES, SUBLAYERS, Shape, StrokeLayer, SubPath, TITLE_AND_COLOR_NAMES, TRANSPARENCY, TYPE, TYPE_ATTRIBUTES, TextKind, TextObject, UNDECODED, Vec2, Write, ai_ops, latin, ps, stroke, }; -use super::{CONIC, CONIC_WEDGES, DIAMOND, HUE_STOPS, SPREAD, SPREAD_CAPPED}; +use super::{CONIC, CONIC_WEDGES, DIAMOND, GRAPHS, HUE_STOPS, SPREAD, SPREAD_CAPPED}; pub(super) struct Writer<'a> { doc: &'a Document, @@ -254,6 +254,9 @@ impl<'a> Writer<'a> { if n.name.as_deref().is_some_and(|s| !s.is_empty()) && !n.is_layer() { self.warn(NAMES); } + if n.extras().graph.is_some() { + self.warn(GRAPHS); + } let locked = locked || n.locked; match &n.kind { NodeKind::Layer { children, clip, .. } => { diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index c5cb2a78c1..26a9cbc90b 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -232,7 +232,14 @@ private editing data. VectorCraft: from 0 to 9 leaves the mode as it was. Legacy `.ai` placed EPS files in the version 7 form (`` ` `` … `~` with no `%AI5_BeginPlace`) are read like other placed files without a preview: as the page's art in their box. Graphs (`%_Gs` … `%_GS`) come in as their - drawn art, as plain groups, paths and type, without their data or settings, with a note. + drawn art. A grouped column, stacked column, line or area graph also keeps its table, type, axes and settings + when every record has the shape and values the version 7 specification's appendix A gives + ([the specification](https://13thmonkey.org/documentation/fonts/AI7FileFormat.pdf), printed pp. 109–112 + and 146–158), its table holds at least one number, its art draws the plot's left and bottom edges, and each series' + group is tagged once and paints its marks with one fill and one stroke, which the graph keeps as that series' + paint; Graph Data then edits it. Other graphs (pie and scatter graphs, graphs with designs or + customizations, records of later versions) come in as plain groups, paths and type, with a note. Saving a graph to + `.ai`, in either form, writes its art without its data or settings, with a warning. Legacy `.ai` patterns in the version 7 form (a list of `@` and `&` layers, `_ &` for the tile's background) open as pattern swatches, with a fill's offset, scale, rotation, reflection and shear applied before its matrix. Legacy `.ai` linked images in diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index e7c1b28726..2d22d74e36 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1619,6 +1619,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur A `.ai` opened from its editing data opens its Objects on Path groups live, with their path, their objects in order along it, Pivot and Rotate, and their art where the file draws it; a group whose spacing has been dragged, or with options an Objects on Path group doesn't hold, opens as a plain group (`crates/eps/src/import/ai/tests.rs`). `.ai` symbol definitions and placed instances open as live symbols, in the Symbols panel's order; an instance of a symbol that isn't read, or whose appearance differs from its symbol's, comes in as the page draws it (`crates/eps/src/import/ai/tests.rs`). In a `.ai` opened from its editing data, an object in a pattern's art or a brush's art with an art style VectorCraft reads opens as itself with its effects live, as an object on a layer does, and an object in a pattern's art keeps the id of a graphic style in the file's list (`crates/eps/src/import/ai/tests.rs`). + A graph in a `.ai` whose records have the shape and values the published version 7 specification gives ([the specification](https://13thmonkey.org/documentation/fonts/AI7FileFormat.pdf), printed pp. 109–112 and 146–158) opens as a live grouped column, stacked column, line or area graph with its saved art, table, axes and settings, and Graph Data edits it ([the help page](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/visualize-data/add-graph-data.html)); other graphs keep their art as plain objects with a note, and saving a graph to `.ai` warns that its data and settings aren't written (`crates/eps/src/import/ai/tests.rs`, `crates/engine/src/cmd/graph/tests.rs`). Blends and envelopes in the definitions of a `.ai`'s symbols, patterns and brushes' art count against the same per-file limits on blend layout and envelope checks as the layers' art, so a file with many definitions does not get those limits again for each one (`crates/eps/src/import/ai/read/directives.rs`). A `.ai` or EPS whose editing data has brushes opens with the file's Calligraphic, Scatter, Art and Bristle brushes in the Brushes panel in place of the default ones, in the file's order: the Calligraphic size, roundness and angle with their variation, the Scatter size, spacing, scatter and rotation, the Bristle settings, and the Scatter and Art brushes' art (Art brushes in the older and newer forms of their objects). A brush library `.ai` (brushes and no layers) opens from its PDF part with its brushes, so Other Library lists them. An Art brush in Tints keeps that colorization. Pattern brushes and brushes that can't be read are left out with a note. Paths with a Calligraphic brush, and paths with an Art brush in the newer form at 100% in Tints that stretches art of solid-filled paths unflipped, open as the path with the brush on its stroke. Paths with an Art brush in the older form, and other brushed paths, still come in as their drawn look (part of #372 and #637; `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`). In a `.ai` opened from its editing data, a path or type object with Offset Path, Outline Object or Outline Stroke on the whole object opens as itself with those effects live, in their order and shown or hidden; on a fill or stroke of type they stay on that fill or stroke. Offset Path keeps its Offset, Joins and Miter Limit, and Outline Stroke takes its width from the stroke. An Offset Path with a setting missing, out of range or not read keeps the drawn art, as does a path with a Path effect on one fill or stroke (`crates/eps/src/import/ai/style/tests.rs`, `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`). From 193ed34794c86e419ac9f5b07e5e1a0fb4d4fd2e Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 14:16:51 -0800 Subject: [PATCH 09/63] i18n: German has a row for every message the untranslated report lists The German catalog lacked a row for 1,842 of the status and error messages that the untranslated report finds in the sources, so the status bar showed those messages in English. - de.tsv gains 1,842 `@msg` rows: the errors of commands and their parameter checks, scripts, file import and export, printing and plug-ins. The translations are VectorCraft's own, written from the English meaning with the catalog's conventions and its existing feature names. Placeholders, parameter names, command ids and literal values stay as the English writes them. - The untranslated report now lists no German messages. Menu strings, `tl!` literals, undo labels and swatch names are unchanged. - Two of the messages have two placeholders in a row. The catalog does not load such a row, so those two are still shown in English. - The German row of docs/localization-parity.md gives the catalog's current message counts. Docs: docs/target-app-parity.md, docs/localization-parity.md. --- crates/ui-egui/src/i18n/de.tsv | 1843 ++++++++++++++++++++++++++++++++ docs/localization-parity.md | 2 +- docs/target-app-parity.md | 1 + 3 files changed, 1845 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 8fe1734165..bca5343ac8 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -7047,3 +7047,1846 @@ breakText Glyphs Glyphen Margin Rand Box Rahmen Swatch Farbfeld +# 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' und 'arguments' können in Funktionen im Strict-Modus nicht verwendet werden +@msg Boolean.prototype.{name} was called on a value that is not a Boolean Boolean.prototype.{name} wurde für einen Wert aufgerufen, der kein Boolean ist +@msg CMYK images whose samples couldn't be read in CMYK were imported in RGB CMYK-Bilder, deren Samples nicht in CMYK gelesen werden konnten, wurden in RGB importiert +@msg Cannot write to the location of “{name}”; choose where to save it Am Speicherort von „{name}“ kann nicht geschrieben werden; wählen Sie aus, wo die Datei gespeichert werden soll +@msg DCS files can only be placed linked: use File › Place with Link on DCS-Dateien können nur verknüpft platziert werden: Verwenden Sie Datei › Platzieren mit aktivierter Option „Verknüpfen“ +@msg Date.prototype method called on a value that is not a Date object Date.prototype-Methode wurde für einen Wert aufgerufen, der kein Date-Objekt ist +@msg Error.prototype.toString was called on a value that is not an object Error.prototype.toString wurde für einen Wert aufgerufen, der kein Objekt ist +@msg Function.prototype.toString was called on a value that is not a function Function.prototype.toString wurde für einen Wert aufgerufen, der keine Funktion ist +@msg Import SVG Filter reads SVG files, not SVGZ SVG-Filter importieren liest SVG-Dateien, keine SVGZ-Dateien +@msg JSON.parse: {why} (at offset {at}) JSON.parse: {why} (bei Offset {at}) +@msg JSON.stringify cannot write a structure that contains itself JSON.stringify kann keine Struktur schreiben, die sich selbst enthält +@msg Namespace.prototype.toString needs a Namespace Namespace.prototype.toString braucht einen Namespace +@msg New Document profiles need the preferences folder, which this run doesn't have Profile für neue Dokumente brauchen den Voreinstellungsordner, den diese Sitzung nicht hat +@msg Number.prototype.{name} was called on a value that is not a number Number.prototype.{name} wurde für einen Wert aufgerufen, der keine Zahl ist +@msg Object.create's prototype must be an object or null der Prototyp für Object.create muss ein Objekt oder null sein +@msg Object.{function} was given a value that is not an object Object.{function} hat einen Wert erhalten, der kein Objekt ist +@msg QName.prototype.toString needs a QName QName.prototype.toString braucht einen QName +@msg RegExp.prototype.{name} was called on a value that is not a RegExp object RegExp.prototype.{name} wurde für einen Wert aufgerufen, der kein RegExp-Objekt ist +@msg Stencil Bridges: the selected art is too large or too detailed to bridge Schablonenstege: Das ausgewählte Bildmaterial ist zu groß oder zu detailliert für Stege +@msg String.prototype.toString was called on a value that is not a string String.prototype.toString wurde für einen Wert aufgerufen, der kein String ist +@msg The {name} profile could not be read, so the new document has the default settings: {reason} Das Profil {name} konnte nicht gelesen werden, daher hat das neue Dokument die Standardeinstellungen: {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 Überfüllen: Mehr als {_1} Objektpaare liegen nah genug zum Überfüllen, oder zu viele Objekte teilen sich einen horizontalen Bereich; überfüllen Sie weniger Objekte auf einmal +@msg Weed Box and Lines: no weeding line fits outside the art, and the weed box is off Entgitterrahmen und -linien: Keine Entgitterlinie passt außerhalb des Bildmaterials, und der Entgitterrahmen ist deaktiviert +@msg \\ at end of pattern \\ am Ende des Musters +@msg `alignOn` cannot be empty `alignOn` darf nicht leer sein +@msg `alignOn` must be a string `alignOn` muss ein String sein +@msg `align` must be a string `align` muss ein String sein +@msg `answer` for this {kind} dialog must be one of: {_1} `answer` für dieses {kind}-Dialogfeld muss eines der folgenden sein: {_1} +@msg `answers` has more than {MAX_ANSWERS} answers `answers` hat mehr als {MAX_ANSWERS} Antworten +@msg `answers` is larger than 1 MiB `answers` ist größer als 1 MiB +@msg `answers` must be a list of answers `answers` muss eine Liste von Antworten sein +@msg `artboard` must be the index of an artboard of the document `artboard` muss der Index einer Zeichenfläche des Dokuments sein +@msg `artboardsAt` must be [x, y] `artboardsAt` muss [x, y] sein +@msg `artboards` names an artboard the document does not have `artboards` nennt eine Zeichenfläche, die das Dokument nicht hat +@msg `artboards` names an artboard twice `artboards` nennt eine Zeichenfläche zweimal +@msg `at` must be [x, y] (got {at}) `at` muss [x, y] sein (erhalten: {at}) +@msg `beside` needs `side` `beside` braucht `side` +@msg `code` must be a whole number `code` muss eine ganze Zahl sein +@msg `colorMode` must be one of rgb, cmyk `colorMode` muss rgb oder cmyk sein +@msg `colors` does not apply to the {name} reduction `colors` gilt nicht für die Reduzierung {name} +@msg `colors` must be a whole number from 2 to 256 `colors` muss eine ganze Zahl von 2 bis 256 sein +@msg `colors` must be at least 9 for the uniform reduction `colors` muss für die einheitliche Reduzierung mindestens 9 sein +@msg `colors` must be between 1 and {MAX_THEME_COLORS} (got {n}) `colors` muss zwischen 1 und {MAX_THEME_COLORS} liegen (erhalten: {n}) +@msg `count` must be a whole number from 0 to {MAX_COUNT} `count` muss eine ganze Zahl von 0 bis {MAX_COUNT} sein +@msg `ditherAmount` must be a number from 0 to 100 `ditherAmount` muss eine Zahl von 0 bis 100 sein +@msg `fidelity` must be from {lo} to {hi} pt `fidelity` muss zwischen {lo} und {hi} pt liegen +@msg `focal` and `radius` cannot be used together `focal` und `radius` können nicht zusammen verwendet werden +@msg `focal` must be an array of [subpath, anchor] pairs `focal` muss ein Array aus Paaren [subpath, anchor] sein +@msg `focal` names [{si}, {ai}], which is not an anchor of the path `focal` nennt [{si}, {ai}], das kein Ankerpunkt des Pfads ist +@msg `glyphAlternate` takes only null, which takes a glyph choice away (text.setGlyphAlternate makes one) `glyphAlternate` nimmt nur null, was eine Glyphenauswahl entfernt (text.setGlyphAlternate legt eine fest) +@msg `group` must be one of {_1} `group` muss eines der folgenden sein: {_1} +@msg `group` must be the Objects on Path group of `member` and `target` `group` muss die Gruppe „Objekte auf Pfad“ von `member` und `target` sein +@msg `id` is an object id, not {v} `id` ist eine Objekt-ID, nicht {v} +@msg `item` needs `index` `item` braucht `index` +@msg `kerningBefore` belongs to a line's first character, not to a character style (text.setRangeStyle sets it) `kerningBefore` gehört zum ersten Zeichen einer Zeile, nicht zu einem Zeichenformat (text.setRangeStyle legt es fest) +@msg `kinsokuType` must be "pushInFirst", "pushOutFirst" or "pushOutOnly" `kinsokuType` muss „pushInFirst“, „pushOutFirst“ oder „pushOutOnly“ sein +@msg `leader` must be a string `leader` muss ein String sein +@msg `max` must be 1 to {MAX_SUGGESTIONS} `max` muss zwischen 1 und {MAX_SUGGESTIONS} liegen +@msg `name` must be a name that is not empty `name` muss ein nicht leerer Name sein +@msg `name` must not be empty `name` darf nicht leer sein +@msg `origin` must be [x, y] in points `origin` muss [x, y] in Punkt sein +@msg `picks` must be an array (got {v}) `picks` muss ein Array sein (erhalten: {v}) +@msg `picks` must hold 1 to {MAX_PICKS} picks (got {_1}) `picks` muss 1 bis {MAX_PICKS} Auswahlen enthalten (erhalten: {_1}) +@msg `radius` must be 0 or more `radius` muss 0 oder größer sein +@msg `rect` must be [x0, y0, x1, y1] (got {r}) `rect` muss [x0, y0, x1, y1] sein (erhalten: {r}) +@msg `replace` must be a string `replace` muss ein String sein +@msg `rows` must be lists of numbers or nulls, not {r} `rows` müssen Listen aus Zahlen oder null-Werten sein, nicht {r} +@msg `scale` must be a number from 0.001 to 1000 `scale` muss eine Zahl von 0,001 bis 1000 sein +@msg `scope.{key}` must be a non-empty array `scope.{key}` muss ein nicht leeres Array sein +@msg `scope` must be "document", {artboards} or {layers} `scope` muss „document“, {artboards} oder {layers} sein +@msg `side` must be "left", "right", "top" or "bottom" `side` muss „left“, „right“, „top“ oder „bottom“ sein +@msg `side` places the artboard: leave out x, y, width and height `side` platziert die Zeichenfläche: Lassen Sie x, y, width und height weg +@msg `source` is the only path in series group `target` `source` ist der einzige Pfad in der Reihengruppe `target` +@msg `spacing` makes more than {MAX_COUNT} lines across the box `spacing` ergibt mehr als {MAX_COUNT} Linien über den Rahmen +@msg `spacing` must be more than 0 and at most {MAX_DISTANCE} pt `spacing` muss größer als 0 und höchstens {MAX_DISTANCE} pt sein +@msg `target` must be an object of the same Objects on Path group as `member` `target` muss ein Objekt derselben Gruppe „Objekte auf Pfad“ wie `member` sein +@msg `version` must be 0 or {_1} `version` muss 0 oder {_1} sein +@msg `version` must be 0 or {newest} `version` muss 0 oder {newest} sein +@msg `weight` must be from 0.01 to 1000 pt `weight` muss zwischen 0,01 und 1000 pt liegen +@msg `weights` must be a list of {_1} numbers, one per row (got {v}) `weights` muss eine Liste aus {_1} Zahlen sein, eine pro Zeile (erhalten: {v}) +@msg `xml` must be a string `xml` muss ein String sein +@msg `{KEY}` entry {n} gives the paragraph at byte {start} of {_1} other indents than an entry before it `{KEY}`-Eintrag {n} gibt dem Absatz bei Byte {start} von {_1} andere Einzüge als ein Eintrag davor +@msg `{KEY}` entry {n} must be an object `{KEY}`-Eintrag {n} muss ein Objekt sein +@msg `{KEY}` entry {n} sets neither `leftIndent` nor `firstLineIndent` `{KEY}`-Eintrag {n} legt weder `leftIndent` noch `firstLineIndent` fest +@msg `{KEY}` entry {n}: `id` must be an object id `{KEY}`-Eintrag {n}: `id` muss eine Objekt-ID sein +@msg `{KEY}` entry {n}: `start` must be a byte offset `{KEY}`-Eintrag {n}: `start` muss ein Byte-Offset sein +@msg `{KEY}` entry {n}: `{k}` must be a number from -{MAX_PARA} to {MAX_PARA} pt `{KEY}`-Eintrag {n}: `{k}` muss eine Zahl von -{MAX_PARA} bis {MAX_PARA} pt sein +@msg `{KEY}` entry {n}: no paragraph of {_1} starts at byte {start} `{KEY}`-Eintrag {n}: Kein Absatz von {_1} beginnt bei Byte {start} +@msg `{KEY}` entry {n}: unknown key `{k}` `{KEY}`-Eintrag {n}: unbekannter Schlüssel `{k}` +@msg `{KEY}` entry {n}: {_1} is not a type object `{KEY}`-Eintrag {n}: {_1} ist kein Textobjekt +@msg `{KEY}` is empty `{KEY}` ist leer +@msg `{KEY}` must be a list `{KEY}` muss eine Liste sein +@msg `{KEY}` takes at most {MAX_PARAGRAPH_INDENTS} entries (got {_1}) `{KEY}` nimmt höchstens {MAX_PARAGRAPH_INDENTS} Einträge (erhalten: {_1}) +@msg `{KEY}` takes no `{other}` beside it `{KEY}` nimmt kein `{other}` daneben +@msg `{_1}` can't name a composite font `{_1}` kann keine zusammengesetzte Schrift benennen +@msg `{_1}` is the name of an installed font `{_1}` ist der Name einer installierten Schrift +@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} ist in `{font}` gesetzt, einer zusammengesetzten Schrift +@msg `{base}` is larger than {_1} MB `{base}` ist größer als {_1} MB +@msg `{base}` is not in the DCS file's folder `{base}` ist nicht im Ordner der DCS-Datei +@msg `{key}` holds more than {MAX_EDIT_WORDS} words `{key}` enthält mehr als {MAX_EDIT_WORDS} Wörter +@msg `{key}` must be 0 or more `{key}` muss 0 oder größer sein +@msg `{key}` must be [x, y] numbers `{key}` muss aus den Zahlen [x, y] bestehen +@msg `{key}` must be a list of words `{key}` muss eine Liste von Wörtern sein +@msg `{key}` must be a list, not {v} `{key}` muss eine Liste sein, nicht {v} +@msg `{key}` must be a number or null, not {v} `{key}` muss eine Zahl oder null sein, nicht {v} +@msg `{key}` must be a whole number of 0 or more `{key}` muss eine ganze Zahl von 0 oder größer sein +@msg `{key}` must be from 0 to {MAX_DISTANCE} pt `{key}` muss zwischen 0 und {MAX_DISTANCE} pt liegen +@msg `{key}` must be from {lo} to {hi}, not {v} `{key}` muss zwischen {lo} und {hi} liegen, nicht {v} +@msg `{key}` must be one of auto, always, never `{key}` muss auto, always oder never sein +@msg `{key}` must be one of miter, round, bevel `{key}` muss miter, round oder bevel sein +@msg `{key}` must be one word of at most {_1} characters `{key}` muss ein Wort mit höchstens {_1} Zeichen sein +@msg `{key}` must hold single words of at most {_1} characters `{key}` muss einzelne Wörter mit höchstens {_1} Zeichen enthalten +@msg `{key}` must name an object attached to the path of an Objects on Path group `{key}` muss ein Objekt nennen, das an den Pfad einer Gruppe „Objekte auf Pfad“ angehängt ist +@msg `{key}` names an object that is hidden or locked `{key}` nennt ein Objekt, das ausgeblendet oder gesperrt ist +@msg `{name}` is a file of its own, which can only be found next to the DCS file's path `{name}` ist eine eigene Datei, die nur neben dem Pfad der DCS-Datei gefunden werden kann +@msg `{name}`: give the kanji part `{name}`: Geben Sie den Kanji-Teil an +@msg `{name}`: parts must be an object `{name}`: parts muss ein Objekt sein +@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: unbekannte Klasse `{k}`; die Klassen sind kanji, kana, punctuation, symbols, roman und numerals +@msg `{name}`: unknown key `{k}` in {_1} `{name}`: unbekannter Schlüssel `{k}` in {_1} +@msg `{name}`: {_1} must be an object `{name}`: {_1} muss ein Objekt sein +@msg `{name}`: {_1}: {e} `{name}`: {_1}: {e} +@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` kann keine zusammengesetzte Schrift benennen (1 bis {COMPOSITE_NAME_MAX} Zeichen, keine Steuerzeichen) +@msg `{u}` is no Unicode character `{u}` ist kein Unicode-Zeichen +@msg `{word}` isn't in the custom dictionary `{word}` ist nicht im Benutzerwörterbuch +@msg a CMap has too many mappings eine CMap hat zu viele Zuordnungen +@msg a \\u escape needs four hexadecimal digits ein \\u-Escape braucht vier Hexadezimalziffern +@msg a comma in an XPath expression must separate function arguments ein Komma in einem XPath-Ausdruck muss Funktionsargumente trennen +@msg a document holds at most {_1} SVG filters ein Dokument enthält höchstens {_1} SVG-Filter +@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts ein Dokument enthält höchstens {COMPOSITE_FONTS_MAX} zusammengesetzte Schriften +@msg a document with unsaved changes has no recovery copy ein Dokument mit nicht gespeicherten Änderungen hat keine Wiederherstellungskopie +@msg a font's data is too large die Daten einer Schrift sind zu groß +@msg a font's subroutines are too large die Subroutinen einer Schrift sind zu groß +@msg a function declaration without a name eine Funktionsdeklaration ohne Namen +@msg a host value has too many entries ein Host-Wert hat zu viele Einträge +@msg a host value is nested too deep ein Host-Wert ist zu tief verschachtelt +@msg a namespace declaration uses a reserved prefix or URI eine Namespace-Deklaration verwendet ein reserviertes Präfix oder einen reservierten URI +@msg a namespace qualifier is undefined ein Namespace-Qualifizierer ist nicht definiert +@msg a namespace with an empty URI cannot have a prefix ein Namespace mit leerem URI kann kein Präfix haben +@msg a number has no digits eine Zahl hat keine Ziffern +@msg a number's exponent has no digits der Exponent einer Zahl hat keine Ziffern +@msg a number's fraction has no digits der Nachkommateil einer Zahl hat keine Ziffern +@msg a property descriptor cannot have both a value or writable and a get or set ein Eigenschaftsdeskriptor kann nicht zugleich value oder writable und get oder set haben +@msg a property descriptor must be an object ein Eigenschaftsdeskriptor muss ein Objekt sein +@msg a property's {name} must be a function or undefined {name} einer Eigenschaft muss eine Funktion oder undefined sein +@msg a stream in it would decode to more than 256 MB, or its decode parameters can't be applied ein Datenstrom darin würde zu mehr als 256 MB decodiert, oder seine Decodierparameter können nicht angewendet werden +@msg a string holds a control character ein String enthält ein Steuerzeichen +@msg a string holds an escape sequence JSON does not have ein String enthält eine Escape-Sequenz, die JSON nicht kennt +@msg a string is not closed ein String ist nicht geschlossen +@msg an SVG filter is named by its id: give `xml` with `name` ein SVG-Filter wird über seine ID benannt: Geben Sie `xml` mit `name` an +@msg an XML element has two attributes with the same name ein XML-Element hat zwei Attribute mit demselben Namen +@msg an XML value cannot be inserted into itself or its descendants ein XML-Wert kann nicht in sich selbst oder seine Nachfahren eingefügt werden +@msg an XML value's children cannot be assigned by position die Kinder eines XML-Werts können nicht über ihre Position zugewiesen werden +@msg an XML value's children cannot be deleted by position die Kinder eines XML-Werts können nicht über ihre Position gelöscht werden +@msg an XMLList converts to XML only when it has one item eine XMLList wird nur in XML umgewandelt, wenn sie genau ein Element hat +@msg an XPath node test is missing ein XPath-Knotentest fehlt +@msg an XPath operand is missing ein XPath-Operand fehlt +@msg an XPath operator is missing ein XPath-Operator fehlt +@msg an XPath predicate or step cannot follow here ein XPath-Prädikat oder -Schritt kann hier nicht folgen +@msg an XPath predicate or step follows a value that is not a node-set ein XPath-Prädikat oder -Schritt folgt auf einen Wert, der keine Knotenmenge ist +@msg an XPath step is missing ein XPath-Schritt fehlt +@msg an image couldn't be decoded and is missing from the picture ein Bild konnte nicht decodiert werden und fehlt in der Darstellung +@msg an image of {w} × {h} pixels is too large to convert (at most {_1} pixels) ein Bild mit {w} × {h} Pixeln ist zu groß zum Konvertieren (höchstens {_1} Pixel) +@msg apply was called on a value that is not a function apply wurde für einen Wert aufgerufen, der keine Funktion ist +@msg area type in a thread cannot use Dynamic Text: {frames} Flächentext in verkettetem Text kann keinen dynamischen Text verwenden: {frames} +@msg array literal too long Array-Literal zu lang +@msg assignment to a constant binding Zuweisung an eine konstante Bindung +@msg at least one row that isn't excluded needs a weight above 0 mindestens eine nicht ausgeschlossene Zeile braucht eine Gewichtung über 0 +@msg blend must be a mode name blend muss der Name eines Modus sein +@msg both sides of an XPath | must select nodes beide Seiten eines XPath-| müssen Knoten auswählen +@msg canceled abgebrochen +@msg cannot convert the object to a primitive value das Objekt kann nicht in einen primitiven Wert umgewandelt werden +@msg cannot convert undefined or null to an object undefined oder null kann nicht in ein Objekt umgewandelt werden +@msg character dimensions cannot be represented with the source text transform Zeichenmaße können mit der Texttransformation der Quelle nicht dargestellt werden +@msg character dimensions cannot be represented with the target text transform Zeichenmaße können mit der Texttransformation des Ziels nicht dargestellt werden +@msg continuation must be an array of path chunks continuation muss ein Array aus Pfadstücken sein +@msg continuation requires points continuation erfordert Punkte +@msg dataBase64 is not base64 dataBase64 ist kein Base64 +@msg each font must be {name, parts} jede Schrift muss {name, parts} sein +@msg each font needs a name jede Schrift braucht einen Namen +@msg each path chunk must contain 1 to 4096 points jedes Pfadstück muss 1 bis 4096 Punkte enthalten +@msg each pick is {{at: [x, y]}} or {{rect: [x0, y0, x1, y1]}} (got {v}) jede Auswahl ist {{at: [x, y]}} oder {{rect: [x0, y0, x1, y1]}} (erhalten: {v}) +@msg each weight must be a number of 0 or more (got {v}) jede Gewichtung muss eine Zahl von 0 oder größer sein (erhalten: {v}) +@msg finally table {t} out of range finally-Tabelle {t} außerhalb des gültigen Bereichs +@msg flags cannot be given with a RegExp object mit einem RegExp-Objekt können keine Flags angegeben werden +@msg fonts must be a list of composite fonts fonts muss eine Liste zusammengesetzter Schriften sein +@msg function declaration index out of range Index der Funktionsdeklaration außerhalb des gültigen Bereichs +@msg give `char`, `text`, `code` or `unicode` geben Sie `char`, `text`, `code` oder `unicode` an +@msg give `count` or `spacing`, not both geben Sie `count` oder `spacing` an, nicht beides +@msg give `name`, an SVG filter's name geben Sie `name` an, den Namen eines SVG-Filters +@msg give `path`, an SVG file, or `svg`, SVG text geben Sie `path` an, eine SVG-Datei, oder `svg`, SVG-Text +@msg give `xml`, one SVG element geben Sie `xml` an, ein SVG-Element +@msg give exactly one of `path`, `data` and `dataBase64` geben Sie genau eines von `path`, `data` und `dataBase64` an +@msg give fonts: [{name, parts}…] geben Sie fonts: [{name, parts}…] an +@msg give index or indexes, not both geben Sie index oder indexes an, nicht beides +@msg give one element geben Sie ein einzelnes Element an +@msg give one of path, data or dataBase64 geben Sie eines von path, data oder dataBase64 an +@msg give opacity or blend geben Sie opacity oder blend an +@msg handler at {_1} expects {_2} values but its range starts with {d} der Handler bei {_1} erwartet {_2} Werte, aber sein Bereich beginnt mit {d} +@msg handler depth too large Handler-Tiefe zu groß +@msg handler range {_1}..{_2} out of range Handler-Bereich {_1}..{_2} außerhalb des gültigen Bereichs +@msg hiddenLayers must be true or false hiddenLayers muss true oder false sein +@msg hvcC hvcC +@msg images in a spot ink whose samples couldn't be read were imported in RGB Bilder in einer Volltonfarbe, deren Samples nicht gelesen werden konnten, wurden in RGB importiert +@msg images past the most pixels one file may hold were left out Bilder über der Höchstzahl an Pixeln, die eine Datei enthalten darf, wurden ausgelassen +@msg images too large to read were left out Bilder, die zum Lesen zu groß sind, wurden ausgelassen +@msg indexes may hold at most {MAX_GUIDE_INDEXES} entries indexes darf höchstens {MAX_GUIDE_INDEXES} Einträge enthalten +@msg indexes must be an array of guide indexes indexes muss ein Array aus Hilfslinienindizes sein +@msg instruction out of range Anweisung außerhalb des gültigen Bereichs +@msg instruction {pc} ({op}) pops below the stack Anweisung {pc} ({op}) entnimmt mehr Werte, als der Stapel enthält +@msg instruction {pc} reached with stack depths {seen} and {d} Anweisung {pc} wird mit den Stapeltiefen {seen} und {d} erreicht +@msg invalid array length ungültige Array-Länge +@msg invalid group ungültige Gruppe +@msg invalid object id {v}: expected a non-negative integer ungültige Objekt-ID {v}: erwartet wird eine nicht negative ganze Zahl +@msg invalid regular expression flag '{_1}' ungültiges Flag '{_1}' für regulären Ausdruck +@msg invalid regular expression: {m} ungültiger regulärer Ausdruck: {m} +@msg invalid time value ungültiger Zeitwert +@msg is a confirm's answer, whose `value` must be true or false ist die Antwort auf eine Bestätigung, deren `value` true oder false sein muss +@msg is a prompt's answer, whose `value` must be a string or null ist die Antwort auf eine Eingabeaufforderung, deren `value` ein String oder null sein muss +@msg is an alert's answer, whose `value` must be "ok" ist die Antwort auf eine Warnung, deren `value` „ok“ sein muss +@msg is longer than 16 KiB ist länger als 16 KiB +@msg it draws its image through a clipping path es zeichnet sein Bild durch einen Beschneidungspfad +@msg it draws no image es zeichnet kein Bild +@msg it is larger than {MAX_DICTIONARY_FILE_BYTES} bytes sie ist größer als {MAX_DICTIONARY_FILE_BYTES} Byte +@msg it isn't a file es ist keine Datei +@msg it names {_1} plates, and at most {MAX_PLATES} are read sie nennt {_1} Platten, und höchstens {MAX_PLATES} werden gelesen +@msg its art is too long or too detailed for a head seine Grafik ist zu lang oder zu detailliert für eine Spitze +@msg its art paints nothing seine Grafik malt nichts +@msg its image is too large to read sein Bild ist zu groß zum Lesen +@msg its plates are too large: more than {MAX_SAMPLES} pixels together seine Platten sind zu groß: zusammen mehr als {MAX_SAMPLES} Pixel +@msg its plates are too large: {_1} plates of {w} × {h} pixels seine Platten sind zu groß: {_1} Platten mit {w} × {h} Pixeln +@msg its plates can't be kept together seine Platten können nicht zusammengehalten werden +@msg its shapes could not be combined into one seine Formen konnten nicht zu einer zusammengefasst werden +@msg jump to {pc}, past the end Sprung zu {pc}, hinter das Ende +@msg legacyNameEncoding must be one of {_1} legacyNameEncoding muss eines der folgenden sein: {_1} +@msg localize: the first argument must be a localization object or a string localize: Das erste Argument muss ein Lokalisierungsobjekt oder ein String sein +@msg missing `picks` `picks` fehlt +@msg missing `word` `word` fehlt +@msg must be an object with `kind` and `value` muss ein Objekt mit `kind` und `value` sein +@msg must have `kind` alert, confirm or prompt muss `kind` alert, confirm oder prompt haben +@msg must have exactly `kind` and `value` muss genau `kind` und `value` haben +@msg names must be a list of names names muss eine Liste von Namen sein +@msg names must be strings names müssen Strings sein +@msg needs a closing quote after the name braucht ein schließendes Anführungszeichen nach dem Namen +@msg needs a name braucht einen Namen +@msg needs a name of at most 64 bytes braucht einen Namen mit höchstens 64 Byte +@msg needs a name of letters, digits and _, or a quoted name braucht einen Namen aus Buchstaben, Ziffern und _ oder einen Namen in Anführungszeichen +@msg needs a name without control characters braucht einen Namen ohne Steuerzeichen +@msg nesting too deep zu tief verschachtelt +@msg no HP-GL format kein HP-GL-Format +@msg no New Document profile called `{shown}` ({_1}) kein Profil für neue Dokumente namens `{shown}` ({_1}) +@msg no SVG filter at index {k} kein SVG-Filter bei Index {k} +@msg no arrowheads file here hier ist keine Pfeilspitzendatei +@msg no character has code {code} kein Zeichen hat den Code {code} +@msg no composite font keine zusammengesetzte Schrift +@msg no composite font named `{n}` keine zusammengesetzte Schrift namens `{n}` +@msg no composite fonts to export keine zusammengesetzten Schriften zum Exportieren +@msg no custom arrowhead {name} (see stroke.arrowheads.list) keine eigene Pfeilspitze {name} (siehe stroke.arrowheads.list) +@msg no file to open keine Datei zum Öffnen +@msg no instructions keine Anweisungen +@msg no layer to rename keine Ebene zum Umbenennen +@msg no random numbers ({e}) keine Zufallszahlen ({e}) +@msg no selected or given object has a visible 3D effect kein ausgewähltes oder angegebenes Objekt hat einen sichtbaren 3D-Effekt +@msg no such artboard {v} keine solche Zeichenfläche {v} +@msg no symbol named `{_1}` kein Symbol namens `{_1}` +@msg node {_1} already has the text variable `{_2}` Knoten {_1} hat bereits die Textvariable `{_2}` +@msg not a DCS file keine DCS-Datei +@msg not a RegExp object kein RegExp-Objekt +@msg not a composite fonts file: no compositeFonts list keine Datei mit zusammengesetzten Schriften: keine compositeFonts-Liste +@msg not a composite fonts file: {e} keine Datei mit zusammengesetzten Schriften: {e} +@msg not a regular expression kein regulärer Ausdruck +@msg not an action set: the text doesn't start with /version and a number kein Aktionssatz: Der Text beginnt nicht mit /version und einer Zahl +@msg nothing to repeat nichts zu wiederholen +@msg numbers out of order in {_1} quantifier Zahlen in falscher Reihenfolge im Quantifizierer {_1} +@msg object {_1} needs a 3D effect Objekt {_1} braucht einen 3D-Effekt +@msg object {_1}: a hollow Extrude & Bevel has no caps: turn on Cap to map a graphic Objekt {_1}: Ein hohles „Extrudieren und abgeflachte Kante“ hat keine Deckflächen: Aktivieren Sie „Deckflächen“, um eine Grafik zuzuweisen +@msg object {_1}: with a depth of 0 the object has one face: map its graphics onto the front Objekt {_1}: Bei einer Tiefe von 0 hat das Objekt nur eine Fläche: Weisen Sie seine Grafiken der Vorderseite zu +@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} bei Index {index} hat keine Beleuchtungsvorgaben; legen Sie seine Lichter mit effect.setParams fest +@msg opacity must be a number opacity muss eine Zahl sein +@msg operand too large Operand zu groß +@msg order is a whole number from 0 to {others}, not `{v}` order ist eine ganze Zahl von 0 bis {others}, nicht `{v}` +@msg path chunks must share their boundary point Pfadstücke müssen ihren Grenzpunkt teilen +@msg path is empty der Pfad ist leer +@msg pdfCompatible: false needs the artboards to keep the editable artwork when useArtboards is false pdfCompatible: false braucht die Zeichenflächen, um die bearbeitbare Grafik zu behalten, wenn useArtboards false ist +@msg point x must be a number Punkt x muss eine Zahl sein +@msg point y must be a number Punkt y muss eine Zahl sein +@msg points must be [x,y] pairs Punkte müssen [x,y]-Paare sein +@msg points must be finite canvas points Punkte müssen endliche Punkte auf der Arbeitsfläche sein +@msg range out of order in character class Bereich in falscher Reihenfolge in der Zeichenklasse +@msg reduce of an empty array with no initial value reduce eines leeren Arrays ohne Startwert +@msg regular expression flag '{_1}' given twice Flag '{_1}' des regulären Ausdrucks zweimal angegeben +@msg sampled text would exceed the canvas (coordinates out of range) der Mustertext würde über die Arbeitsfläche hinausgehen (Koordinaten außerhalb des gültigen Bereichs) +@msg select an SVG filter wählen Sie einen SVG-Filter aus +@msg select an object with a 3D effect wählen Sie ein Objekt mit einem 3D-Effekt aus +@msg select an object with a 3D effect, or give its id in ids wählen Sie ein Objekt mit einem 3D-Effekt aus, oder geben Sie seine ID in ids an +@msg select one layer, group or object to rename wählen Sie eine Ebene, Gruppe oder ein Objekt zum Umbenennen aus +@msg select the art to weed wählen Sie das zu entgitternde Bildmaterial aus +@msg select type on a path or area type wählen Sie Pfadtext oder Flächentext aus +@msg side is left or right, not `{v}` side ist left oder right, nicht `{v}` +@msg source text transform is collapsed or unrepresentable die Texttransformation der Quelle ist zusammengefallen oder nicht darstellbar +@msg table must be true or false table muss true oder false sein +@msg takes one name nimmt einen Namen +@msg target text transform is collapsed or unrepresentable die Texttransformation des Ziels ist zusammengefallen oder nicht darstellbar +@msg that makes too many pieces (at most 100000) or dash steps: simplify the dash pattern or make the dashes longer das ergibt zu viele Teile (höchstens 100000) oder Strichschritte: Vereinfachen Sie das Strichmuster oder verlängern Sie die Striche +@msg the Layers panel is not shown das Bedienfeld „Ebenen“ wird nicht angezeigt +@msg the Layers panel's search, filter or Show Layers Only hides its row die Suche, der Filter oder „Nur Ebenen einblenden“ im Bedienfeld „Ebenen“ blendet seine Zeile aus +@msg the Objects on Path group is hidden or locked die Gruppe „Objekte auf Pfad“ ist ausgeblendet oder gesperrt +@msg the SVG file is larger than {MAX_IMPORT_BYTES} bytes die SVG-Datei ist größer als {MAX_IMPORT_BYTES} Byte +@msg the XML operation needs an XML or XMLList value die XML-Operation braucht einen XML- oder XMLList-Wert +@msg the XPath expression does not evaluate to a node-set der XPath-Ausdruck ergibt keine Knotenmenge +@msg the XPath expression ends early der XPath-Ausdruck endet zu früh +@msg the XPath expression has an unmatched ) der XPath-Ausdruck hat eine ) ohne Gegenstück +@msg the XPath expression has an unmatched ] der XPath-Ausdruck hat eine ] ohne Gegenstück +@msg the XPath expression has an unmatched closing bracket der XPath-Ausdruck hat eine schließende Klammer ohne Gegenstück +@msg the XPath expression selects a namespace node, which is not an XML value der XPath-Ausdruck wählt einen Namespace-Knoten aus, der kein XML-Wert ist +@msg the XPath expression selects the root node, which is not an XML value der XPath-Ausdruck wählt den Wurzelknoten aus, der kein XML-Wert ist +@msg the XPath function {name} needs a node-set die XPath-Funktion {name} braucht eine Knotenmenge +@msg the XPath function {name} takes the wrong number of arguments die XPath-Funktion {name} erhält die falsche Anzahl an Argumenten +@msg the XPath name functions need a node-set die XPath-Namensfunktionen brauchen eine Knotenmenge +@msg the XPath prefix {shown} is not bound to a namespace das XPath-Präfix {shown} ist an keinen Namespace gebunden +@msg the XPath variable ${shown} is not bound die XPath-Variable ${shown} ist nicht gebunden +@msg the argument list is not an object die Argumentliste ist kein Objekt +@msg the artwork is too large for EPS: what follows 256 MB of PostScript was left out die Grafik ist zu groß für EPS: Was nach 256 MB PostScript folgt, wurde ausgelassen +@msg the bitmaps hold more pixels than one file may die Bitmaps enthalten mehr Pixel, als eine Datei enthalten darf +@msg the code runs past its last instruction der Code läuft über seine letzte Anweisung hinaus +@msg the curve tolerance must be from 0.01 to 10 points die Kurventoleranz muss zwischen 0,01 und 10 Punkt liegen +@msg the custom dictionary couldn't be saved to {_1}: {e} das Benutzerwörterbuch konnte nicht unter {_1} gespeichert werden: {e} +@msg the custom dictionary {_1} can't be read ({why}), so it is left as it is das Benutzerwörterbuch {_1} kann nicht gelesen werden ({why}), daher bleibt es unverändert +@msg the document already has an SVG filter named {name} das Dokument hat bereits einen SVG-Filter namens {name} +@msg the document has another SVG filter named {name}: use svgFilter.apply, or svgFilter.define with `replace` to edit it das Dokument hat einen anderen SVG-Filter namens {name}: Verwenden Sie svgFilter.apply oder svgFilter.define mit `replace`, um ihn zu bearbeiten +@msg the document has no SVG filter named {name} das Dokument hat keinen SVG-Filter namens {name} +@msg the document has no SVG filter named {r} das Dokument hat keinen SVG-Filter namens {r} +@msg the document has no color swatches das Dokument hat keine Farbfelder +@msg the document's swatches hold {_1} colors; the document reduction takes at most 255 die Farbfelder des Dokuments enthalten {_1} Farben; die Reduzierung auf Dokumentfarben nimmt höchstens 255 +@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts die Datei enthält mehr als {COMPOSITE_FONTS_MAX} zusammengesetzte Schriften +@msg the file holds no composite fonts die Datei enthält keine zusammengesetzten Schriften +@msg the file holds two composite fonts named `{_1}` die Datei enthält zwei zusammengesetzte Schriften namens `{_1}` +@msg the file is larger than 4 MB die Datei ist größer als 4 MB +@msg the file isn't valid SVG: {e} die Datei ist kein gültiges SVG: {e} +@msg the file names no plate die Datei nennt keine Platte +@msg the function's prototype is not an object der Prototyp der Funktion ist kein Objekt +@msg the guide is hidden or locked die Hilfslinie ist ausgeblendet oder gesperrt +@msg the library's file is outside the read roots die Datei der Bibliothek liegt außerhalb der Lesestammordner +@msg the method needs an XML value die Methode braucht einen XML-Wert +@msg the paint is not a freeform gradient (apply one with paint.editGradient {kind}) die Farbe ist kein Freiformverlauf (wenden Sie einen mit paint.editGradient {kind} an) +@msg the parameters or body do not form one function die Parameter oder der Rumpf bilden keine einzelne Funktion +@msg the pasted SVG filter {name} draws differently from the document's filter of that name, and no free name fits it der eingefügte SVG-Filter {name} zeichnet anders als der gleichnamige Filter des Dokuments, und kein freier Name passt zu ihm +@msg the pen table holds more than 255 pens die Stifttabelle enthält mehr als 255 Stifte +@msg the plates can't be kept die Platten können nicht behalten werden +@msg the position table does not match the instructions die Positionstabelle passt nicht zu den Anweisungen +@msg the prompt's text is longer than {MAX_PROMPT_BYTES} bytes der Text der Eingabeaufforderung ist länger als {MAX_PROMPT_BYTES} Byte +@msg the radix must be between 2 and 36 die Basis muss zwischen 2 und 36 liegen +@msg the right side of 'in' is not an object die rechte Seite von 'in' ist kein Objekt +@msg the right side of 'instanceof' is not a function die rechte Seite von 'instanceof' ist keine Funktion +@msg the selected art is nested more than {MAX_DEPTH} levels deep das ausgewählte Bildmaterial ist mehr als {MAX_DEPTH} Ebenen tief verschachtelt +@msg the selected art is too large to measure das ausgewählte Bildmaterial ist zu groß zum Messen +@msg the selection has no outlines to weed around die Auswahl hat keine Umrisse, um die herum entgittert werden kann +@msg the value bind was called on is not a function der Wert, für den bind aufgerufen wurde, ist keine Funktion +@msg the value is not a constructor der Wert ist kein Konstruktor +@msg the value is not a function der Wert ist keine Funktion +@msg the value is not a valid XML name der Wert ist kein gültiger XML-Name +@msg the value is not an XML attribute name der Wert ist kein XML-Attributname +@msg the value is not an XML name der Wert ist kein XML-Name +@msg the weeding lines cross the art's outlines too many times to measure; use fewer lines die Entgitterlinien kreuzen die Umrisse des Bildmaterials zu oft zum Messen; verwenden Sie weniger Linien +@msg the weeding lines pass too close to too many outline segments to measure; use fewer lines or a smaller gap die Entgitterlinien verlaufen zu nah an zu vielen Umrisssegmenten zum Messen; verwenden Sie weniger Linien oder einen kleineren Abstand +@msg the weeding lines would be more than {max_pieces} pieces die Entgitterlinien würden mehr als {max_pieces} Teile ergeben +@msg the {_1} plate lies outside the file die Platte {_1} liegt außerhalb der Datei +@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 dies ist eine binäre DXF-Datei, die nicht gelesen werden kann: Speichern Sie sie in der CAD-Anwendung als ASCII-DXF und öffnen Sie diese +@msg this warp's points can't be edited: use Reset with Mesh first die Punkte dieser Verkrümmung können nicht bearbeitet werden: Verwenden Sie zuerst „Mit Gitter zurücksetzen“ +@msg toExponential() digits must be between 0 and 20 die Stellen für toExponential() müssen zwischen 0 und 20 liegen +@msg toFixed() digits must be between 0 and 20 die Stellen für toFixed() müssen zwischen 0 und 20 liegen +@msg toISOString is not a function toISOString ist keine Funktion +@msg toPrecision() argument must be between 1 and 21 das Argument von toPrecision() muss zwischen 1 und 21 liegen +@msg toSource: a getter cannot be written as source toSource: Ein Getter kann nicht als Quelltext geschrieben werden +@msg toSource: an array with properties besides its elements, a property that is not enumerable or another prototype cannot be written as source toSource: Ein Array mit Eigenschaften neben seinen Elementen, einer nicht aufzählbaren Eigenschaft oder einem anderen Prototyp kann nicht als Quelltext geschrieben werden +@msg toSource: an object with a property that is not enumerable, or whose prototype is not Object.prototype, cannot be written as source toSource: Ein Objekt mit einer nicht aufzählbaren Eigenschaft oder mit einem anderen Prototyp als Object.prototype kann nicht als Quelltext geschrieben werden +@msg toSource: the source is too long toSource: Der Quelltext ist zu lang +@msg toSource: the value contains itself toSource: Der Wert enthält sich selbst +@msg toSource: the value has too many entries toSource: Der Wert hat zu viele Einträge +@msg toSource: the value is nested too deep toSource: Der Wert ist zu tief verschachtelt +@msg toString is not a function toString ist keine Funktion +@msg too many arguments zu viele Argumente +@msg too many quantifiers zu viele Quantifizierer +@msg two composite fonts are named `{_1}` zwei zusammengesetzte Schriften heißen `{_1}` +@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`) unbekannte OpenType-Funktion `{t}` (liga, calt, dlig, smcp, frac, onum, tnum, ordn, swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom, aalt; mit vorangestelltem - ausschalten; swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom und aalt nehmen die Nummer einer Alternative als `tag=N` mit N von 0 bis {_1}, etwa `salt=2`) +@msg unknown dither `{d}` (diffusion, pattern, noise or none) unbekanntes Dithering `{d}` (diffusion, pattern, noise oder none) +@msg unknown key `{k}`; a font takes name and parts unbekannter Schlüssel `{k}`; eine Schrift nimmt name und parts +@msg unknown panel `{panel}` unbekanntes Bedienfeld `{panel}` +@msg unknown parameter `{key}` unbekannter Parameter `{key}` +@msg unknown parameter `{within}settings.{format}`; {why} unbekannter Parameter `{within}settings.{format}`; {why} +@msg unknown parameter `{within}{key}`; {why} unbekannter Parameter `{within}{key}`; {why} +@msg unknown reduction `{reduction}` (selective, perceptual, adaptive, web, gray, blackWhite, uniform or document) unbekannte Reduzierung `{reduction}` (selective, perceptual, adaptive, web, gray, blackWhite, uniform oder document) +@msg unmatched ')' ')' ohne Gegenstück +@msg unterminated character class nicht abgeschlossene Zeichenklasse +@msg unterminated group nicht abgeschlossene Gruppe +@msg wordGap and lineGap need area type in the selection wordGap und lineGap brauchen Flächentext in der Auswahl +@msg xpath needs its variables as an object xpath braucht seine Variablen als Objekt +@msg xpath needs the expression as a string xpath braucht den Ausdruck als String +@msg {GRANT_NOT_REMOVED}, and its settings and cache can't be moved back from {_1} to {_2}: {e} {GRANT_NOT_REMOVED}, und seine Einstellungen und sein Cache können nicht von {_1} nach {_2} zurückverschoben werden: {e} +@msg {_1} is not a folder: {e} {_1} ist kein Ordner: {e} +@msg {_1}: {_2} and {_3} are in the way {_1}: {_2} und {_3} sind im Weg +@msg {code}: {0} {code}: {0} +@msg {dir}: {e} {dir}: {e} +@msg {file} names more than one linked JavaScript file {file} nennt mehr als eine verknüpfte JavaScript-Datei +@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}: Der Pfad einer eigenen Pfeilspitze braucht eine zu füllende Fläche und höchstens {most} Ankerpunkte und {most} Teilpfade, jeder Ankerpunkt höchstens {far} Spitzeneinheiten von ihrer Spitze entfernt +@msg {k}: bad custom arrowhead path: {e} {k}: ungültiger Pfad einer eigenen Pfeilspitze: {e} +@msg {k}: bad custom arrowhead: {e} {k}: ungültige eigene Pfeilspitze: {e} +@msg {k}: baseline must be {_1} to {_2}% {k}: baseline muss {_1} bis {_2} % sein +@msg {k}: custom must be a name {k}: custom muss ein Name sein +@msg {k}: give a font {k}: Geben Sie eine Schrift an +@msg {k}: only kana can be scaled at the center {k}: nur Kana können an der Mitte skaliert werden +@msg {k}: only kana, roman and numerals can be scaled {k}: nur Kana, lateinische Zeichen und Ziffern können skaliert werden +@msg {k}: scale must be {_1} to {_2}% {k}: scale muss {_1} bis {_2} % sein +@msg {k}: size must be {_1} to {_2}% {k}: size muss {_1} bis {_2} % sein +@msg {path}: copy takes a DCS file only when its plates are inside it; relink to it where its plate files are {path}: Kopieren nimmt eine DCS-Datei nur, wenn ihre Platten darin enthalten sind; verknüpfen Sie sie dort neu, wo ihre Plattendateien liegen +@msg {path}: larger than {_1} MB {path}: größer als {_1} MB +@msg {shown} is not an XPath 1.0 function {shown} ist keine XPath-1.0-Funktion +@msg {v} names no layer or sublayer {v} nennt keine Ebene oder Unterebene +@msg {what} is not a function {what} ist keine Funktion +@msg {what} would print more than {MAX_COPIES_PER_SHEET} copies on a sheet: scale it up {what} würde mehr als {MAX_COPIES_PER_SHEET} Kopien auf einem Bogen drucken: Vergrößern Sie es +@msg {what} {i} out of range {what} {i} außerhalb des gültigen Bereichs +@msg A profile supports one Revolve effect. Edit the existing effect's options instead. Ein Profil unterstützt einen Kreiseln-Effekt. Bearbeiten Sie stattdessen die Optionen des vorhandenen Effekts. +@msg Add Points at Crossings: a path went missing Punkte an Kreuzungen hinzufügen: Ein Pfad ist verloren gegangen +@msg CSS options: {e} CSS-Optionen: {e} +@msg CSV has a header but no data rows die CSV-Datei hat eine Kopfzeile, aber keine Datenzeilen +@msg CSV holds NUL bytes: save it as UTF-8, or as UTF-16 with a byte-order mark die CSV-Datei enthält NUL-Bytes: Speichern Sie sie als UTF-8 oder als UTF-16 mit Byte-Order-Mark +@msg CSV is empty die CSV-Datei ist leer +@msg CSV is larger than {MAX_CSV_BYTES} bytes die CSV-Datei ist größer als {MAX_CSV_BYTES} Byte +@msg CSV is not UTF-16 die CSV-Datei ist nicht UTF-16 +@msg CSV is not UTF-8 die CSV-Datei ist nicht UTF-8 +@msg CSV would be larger than {MAX_CSV_BYTES} bytes die CSV-Datei wäre größer als {MAX_CSV_BYTES} Byte +@msg Choose original artwork, not a dimension's generated paths Wählen Sie die ursprüngliche Grafik, nicht die erzeugten Pfade einer Bemaßung +@msg Contour: select paths or shapes Umriss: Wählen Sie Pfade oder Formen aus +@msg Contour: the Offset Path effect is not available Umriss: Der Effekt „Pfad verschieben“ ist nicht verfügbar +@msg Create needs fills, strokes or both Erstellen braucht Flächen, Konturen oder beides +@msg DXF options: {e} DXF-Optionen: {e} +@msg DXF scale must be a positive number up to {_1}, not {scale} der DXF-Maßstab muss eine positive Zahl bis {_1} sein, nicht {scale} +@msg DXF {field} `{s}`: {allowed} DXF {field} `{s}`: {allowed} +@msg Dimension is locked or hidden Die Bemaßung ist gesperrt oder ausgeblendet +@msg Dimension mode does not match its source Der Bemaßungsmodus passt nicht zu seiner Quelle +@msg Draw Inside needs exactly one selected path, compound path or text „Innen zeichnen“ braucht genau einen ausgewählten Pfad, zusammengesetzten Pfad oder Text +@msg EPS Save writes all artwork: use document.export for selected-only EPS Speichern als EPS schreibt die gesamte Grafik: Verwenden Sie document.export für ein EPS nur mit der Auswahl +@msg EPS flattener: {e} EPS-Reduzierung: {e} +@msg EPS options: {e} EPS-Optionen: {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 Für Bildschirme exportieren schreibt png, png8, jpg, webp, gif, tiff, bmp, tga, psd, svg, svgz, pdf, eps, ai oder vectorcraft +@msg Flare click coordinates must be finite numbers Die Klickkoordinaten für Blendenflecke müssen endliche Zahlen sein +@msg HTML Text applies to object slices of type only HTML-Text gilt nur für Objekt-Slices aus Text +@msg Identify Objects By is XML ID: the name must be a valid XML ID (start with a letter, underscore or colon; no spaces) „Objekte identifizieren nach“ ist XML-ID: Der Name muss eine gültige XML-ID sein (beginnt mit einem Buchstaben, Unterstrich oder Doppelpunkt; keine Leerzeichen) +@msg Image Trace failed: internal error: its thread stopped (please report this bug) Bildnachzeichner fehlgeschlagen: interner Fehler: Sein Thread wurde beendet (bitte melden Sie diesen Fehler) +@msg Image Trace failed: {e} Bildnachzeichner fehlgeschlagen: {e} +@msg Image Trace finished, but its document was closed Bildnachzeichner abgeschlossen, aber sein Dokument wurde geschlossen +@msg Image Trace finished, but the image or its settings changed meanwhile: trace it again Bildnachzeichner abgeschlossen, aber das Bild oder seine Einstellungen haben sich inzwischen geändert: Zeichnen Sie es erneut nach +@msg Invalid dimension offset Ungültiger Bemaßungsversatz +@msg Invalid point Ungültiger Punkt +@msg Join: a subpath went missing Zusammenfügen: Ein Teilpfad ist verloren gegangen +@msg Lock Workspace is not available in VectorCraft Arbeitsbereich sperren ist in VectorCraft nicht verfügbar +@msg Move Panel to New Window is not available: floating panels stay in the main window Bedienfeld in neues Fenster verschieben ist nicht verfügbar: Schwebende Bedienfelder bleiben im Hauptfenster +@msg Offset Path: the path is too complex to offset Pfad verschieben: Der Pfad ist zu komplex zum Verschieben +@msg Offset Path: {e} Pfad verschieben: {e} +@msg PDF content is incomplete or exceeds the reader's limits; no document was imported der PDF-Inhalt ist unvollständig oder überschreitet die Grenzen des Lesers; es wurde kein Dokument importiert +@msg PDF flattener: {e} PDF-Reduzierung: {e} +@msg PDF options: {e} PDF-Optionen: {e} +@msg Recolor Artwork is no longer open „Grafik neu färben“ ist nicht mehr geöffnet +@msg SVG is not UTF-8, UTF-16 or ISO 8859-1 SVG ist nicht UTF-8, UTF-16 oder ISO 8859-1 +@msg Save writes one artboard: name one, or export several with document.export Speichern schreibt eine Zeichenfläche: Nennen Sie eine, oder exportieren Sie mehrere mit document.export +@msg Select a dimension first Wählen Sie zuerst eine Bemaßung aus +@msg Select dimensions to update Wählen Sie die zu aktualisierenden Bemaßungen aus +@msg Select only dimensions Wählen Sie nur Bemaßungen aus +@msg Select two or more objects, or a group Wählen Sie zwei oder mehr Objekte oder eine Gruppe aus +@msg Shaper needs 1–64 source paths Shaper braucht 1–64 Quellpfade +@msg Switch x/y is for scatter graphs x/y vertauschen gilt für Streudiagramme +@msg Text options: {e} Textoptionen: {e} +@msg `align` is start, center or end `align` ist start, center oder end +@msg `anchors` must be an array of [subpath, anchor] pairs `anchors` muss ein Array aus Paaren [subpath, anchor] sein +@msg `anchors` must list anchor indices (0, 1, 2…), not {k} `anchors` muss Ankerpunktindizes (0, 1, 2…) auflisten, nicht {k} +@msg `anchors` must list anchor indices (0, 1, 2…), not {v} `anchors` muss Ankerpunktindizes (0, 1, 2…) auflisten, nicht {v} +@msg `artboards` must be a list of artboard indices `artboards` muss eine Liste von Zeichenflächenindizes sein +@msg `asset` is the asset id (see assets.list) `asset` ist die Asset-ID (siehe assets.list) +@msg `at` must be a byte offset `at` muss ein Byte-Offset sein +@msg `autoLeading` must be a number `autoLeading` muss eine Zahl sein +@msg `background` must be "", "matte" or #rrggbb `background` muss "", „matte“ oder #rrggbb sein +@msg `burasagari` must be "none", "standard" or "forced" `burasagari` muss „none“, „standard“ oder „forced“ sein +@msg `center` must be [x, y] `center` muss [x, y] sein +@msg `charAlign` must be "romanBaseline", "emBoxTop", "emBoxCenter", "emBoxBottom", "icfTop" or "icfBottom" `charAlign` muss „romanBaseline“, „emBoxTop“, „emBoxCenter“, „emBoxBottom“, „icfTop“ oder „icfBottom“ sein +@msg `charDirection` must be "default", "leftToRight" or "rightToLeft" `charDirection` muss „default“, „leftToRight“ oder „rightToLeft“ sein +@msg `clearStroke` requires `stroke: false`, `ids: []` and no `stop` `clearStroke` erfordert `stroke: false`, `ids: []` und kein `stop` +@msg `clear` must be a list of type object ids, not {v} `clear` muss eine Liste von Textobjekt-IDs sein, nicht {v} +@msg `clear` must list type objects; {_1} is not one `clear` muss Textobjekte auflisten; {_1} ist keines +@msg `clear` must not list `id` `clear` darf `id` nicht auflisten +@msg `color` needs a `rule` to make the theme from `color` braucht eine `rule`, aus der das Farbthema erstellt wird +@msg `colors` is empty `colors` ist leer +@msg `columnType` is vertical, uniform, repeating or sliding `columnType` ist vertical, uniform, repeating oder sliding +@msg `columns` must be a whole number 1..50 `columns` muss eine ganze Zahl von 1..50 sein +@msg `commands` must not be empty `commands` darf nicht leer sein +@msg `contents` goes with `to` `contents` gehört zu `to` +@msg `corners` must be sharp, round or square, not `{other}` `corners` muss sharp, round oder square sein, nicht `{other}` +@msg `corners` must list anchor indices (0, 1, 2…), not {v} `corners` muss Ankerpunktindizes (0, 1, 2…) auflisten, nicht {v} +@msg `count` must be a whole number from 1 to 100 `count` muss eine ganze Zahl von 1 bis 100 sein +@msg `dataBase64` is larger than the module limit `dataBase64` ist größer als die Modulgrenze +@msg `dataBase64` is not valid base64 `dataBase64` ist kein gültiges Base64 +@msg `defaultsOnly` cannot be combined with `ids`, `reverse` or `append` `defaultsOnly` kann nicht mit `ids`, `reverse` oder `append` kombiniert werden +@msg `depth` and `childLimit` slice the summary: pass `summary: true` `depth` und `childLimit` begrenzen die Zusammenfassung: Übergeben Sie `summary: true` +@msg `design` is a design name, or null for plain columns `design` ist ein Designname oder null für einfache Säulen +@msg `design` is a design name, or null for the default marker `design` ist ein Designname oder null für die Standardmarkierung +@msg `destinationFolder` is required when destination is `folder` `destinationFolder` ist erforderlich, wenn destination `folder` ist +@msg `destination` must be `none`, `saveAndClose` or `folder` `destination` muss `none`, `saveAndClose` oder `folder` sein +@msg `diameter` must be from 0.1 to 2500 pt `diameter` muss zwischen 0,1 und 2500 pt liegen +@msg `digits` must be one of {_1} `digits` muss eines der folgenden sein: {_1} +@msg `dimImages` is a percentage or false `dimImages` ist ein Prozentwert oder false +@msg `direction` must be "auto", "leftToRight" or "rightToLeft" `direction` muss „auto“, „leftToRight“ oder „rightToLeft“ sein +@msg `each` is the value one design stands for, more than 0 `each` ist der Wert, für den ein Design steht, größer als 0 +@msg `end` must be "start" or "end" `end` muss „start“ oder „end“ sein +@msg `ends` applies to joining exactly two paths `ends` gilt für das Zusammenfügen von genau zwei Pfaden +@msg `event` must be one of {_1} `event` muss eines der folgenden sein: {_1} +@msg `every` must be "all", "odd" or "even", not {v} `every` muss „all“, „odd“ oder „even“ sein, nicht {v} +@msg `expand` must be true or false `expand` muss true oder false sein +@msg `files` must be 1 to 1000 paths of font files the last text.findFontFiles search found `files` muss 1 bis 1000 Pfade von Schriftdateien sein, die die letzte Suche mit text.findFontFiles gefunden hat +@msg `folder` must be an absolute folder path `folder` muss ein absoluter Ordnerpfad sein +@msg `fontAxes` must be an object of axis tags and values, such as {"wght"} `fontAxes` muss ein Objekt aus Achsen-Tags und Werten sein, etwa {"wght"} +@msg `format` must be a string `format` muss ein String sein +@msg `format` must be one of {_1} `format` muss eines der folgenden sein: {_1} +@msg `fractions` is chop or scale `fractions` ist chop oder scale +@msg `from` and `to` must differ `from` und `to` müssen sich unterscheiden +@msg `from` has an empty frame `from` hat einen leeren Rahmen +@msg `from` must be a non-negative integer `from` muss eine nicht negative ganze Zahl sein +@msg `from` must be area type or type on a path `from` muss Flächentext oder Pfadtext sein +@msg `gid` must be a glyph id, not {v} `gid` muss eine Glyphen-ID sein, nicht {v} +@msg `glyphAlternate` belongs to one character, not to a character style (text.setGlyphAlternate sets it) `glyphAlternate` gehört zu einem einzelnen Zeichen, nicht zu einem Zeichenformat (text.setGlyphAlternate legt es fest) +@msg `guide` must be a guide index (guide.list) `guide` muss ein Hilfslinienindex sein (guide.list) +@msg `hAlign` must be default, left, center or right `hAlign` muss default, left, center oder right sein +@msg `id` must be a node id `id` muss eine Knoten-ID sein +@msg `id` must be an object id, not {v} `id` muss eine Objekt-ID sein, nicht {v} +@msg `ids` must name Objects on Path groups or objects in them `ids` muss Gruppen „Objekte auf Pfad“ oder Objekte darin nennen +@msg `ids` takes at most {MAX_INSPECT_IDS} ids `ids` nimmt höchstens {MAX_INSPECT_IDS} IDs +@msg `imageMap` must be none, rectangle or polygon `imageMap` muss none, rectangle oder polygon sein +@msg `includeArtboardFill` must be true or false `includeArtboardFill` muss true oder false sein +@msg `includeSubdirs` must be true or false `includeSubdirs` muss true oder false sein +@msg `index` must be a point index `index` muss ein Punktindex sein +@msg `index` must be a whole number or null `index` muss eine ganze Zahl oder null sein +@msg `indices` must be a non-empty array of item indices `indices` muss ein nicht leeres Array aus Elementindizes sein +@msg `items` is a list of {kind, item} `items` ist eine Liste aus {kind, item} +@msg `items` is empty `items` ist leer +@msg `items` must be a list of {id, points} `items` muss eine Liste aus {id, points} sein +@msg `items` must be a list of {{id, anchors?}}, not {v} `items` muss eine Liste aus {{id, anchors?}} sein, nicht {v} +@msg `items` must be an array of {id, anchors} `items` muss ein Array aus {id, anchors} sein +@msg `items` must be an array of {{id, corners?}}, not {v} `items` muss ein Array aus {{id, corners?}} sein, nicht {v} +@msg `kashida` must be "none", "short", "medium", "long" or "stylistic" `kashida` muss „none“, „short“, „medium“, „long“ oder „stylistic“ sein +@msg `kind` is one of {_1} `kind` ist eines der folgenden: {_1} +@msg `kind` must be "round", "invertedRound" or "chamfer", not {v} `kind` muss „round“, „invertedRound“ oder „chamfer“ sein, nicht {v} +@msg `kind` must be `text`, `visibility`, `image` or `graph` `kind` muss `text`, `visibility`, `image` oder `graph` sein +@msg `kind` must be a string `kind` muss ein String sein +@msg `kind` must be image, noImage or htmlText `kind` muss image, noImage oder htmlText sein +@msg `kinsoku` must be "none", "hard" or "soft" `kinsoku` muss „none“, „hard“ oder „soft“ sein +@msg `label` must be a non-empty name of at most 200 characters `label` muss ein nicht leerer Name mit höchstens 200 Zeichen sein +@msg `language` must be a language name or tag, such as "Turkish" or "tr" `language` muss ein Sprachname oder -Tag sein, etwa „Turkish“ oder „tr“ +@msg `leadingModel` must be "romanBaseline" or "emBoxTop" `leadingModel` muss „romanBaseline“ oder „emBoxTop“ sein +@msg `leading` must be from 0.1 to 5000, or null for Auto `leading` muss zwischen 0,1 und 5000 liegen oder null für Automatisch sein +@msg `length` must be a number of at most 1000000 pt either way `length` muss eine Zahl von höchstens 1000000 pt in beide Richtungen sein +@msg `line` must be true or false `line` muss true oder false sein +@msg `minAngle` ({min_angle}°) can't be more than `maxAngle` ({max_angle}°) `minAngle` ({min_angle}°) darf nicht größer als `maxAngle` ({max_angle}°) sein +@msg `mode` must be "handles", "straight" or "smooth" `mode` muss „handles“, „straight“ oder „smooth“ sein +@msg `mode` must be "straight", "bezier" or "arc" `mode` muss „straight“, „bezier“ oder „arc“ sein +@msg `mode` must be one of {_1} `mode` muss eines der folgenden sein: {_1} +@msg `mojikumi` must be "none", "yakumonoHankaku", "gyomatsuYakumonoHankaku", "gyomatsuYakumonoZenkaku", "yakumonoZenkaku" or "lineEndHalf" `mojikumi` muss „none“, „yakumonoHankaku“, „gyomatsuYakumonoHankaku“, „gyomatsuYakumonoZenkaku“, „yakumonoZenkaku“ oder „lineEndHalf“ sein +@msg `names` is a list of preset names `names` ist eine Liste von Vorgabennamen +@msg `names` lists preset names `names` listet Vorgabennamen auf +@msg `names` must be a list of names `names` muss eine Liste von Namen sein +@msg `names` must be a list of swatch names `names` muss eine Liste von Farbfeldnamen sein +@msg `names` must be an array of style names `names` muss ein Array aus Formatnamen sein +@msg `new` is a design name, or true `new` ist ein Designname oder true +@msg `path` must be a folder `path` muss ein Ordner sein +@msg `path` must be a path object `path` muss ein Pfadobjekt sein +@msg `pivot` must be a whole number from 0 to 8 `pivot` muss eine ganze Zahl von 0 bis 8 sein +@msg `points` must be a list of [x, y] points `points` muss eine Liste von Punkten [x, y] sein +@msg `points` must be a list of point indexes `points` muss eine Liste von Punktindizes sein +@msg `points` must be an array of point indices `points` muss ein Array aus Punktindizes sein +@msg `points` must be point indexes `points` müssen Punktindizes sein +@msg `port` must be "in" or "out" `port` muss „in“ oder „out“ sein +@msg `position` is above or below `position` ist above oder below +@msg `preset` must be a string `preset` muss ein String sein +@msg `ratio` must be from 0.1 to 0.5 `ratio` muss zwischen 0,1 und 0,5 liegen +@msg `rect` must be [x, y, width, height] with a positive size `rect` muss [x, y, width, height] mit einer positiven Größe sein +@msg `relation` must be overlap, inside or outside `relation` muss overlap, inside oder outside sein +@msg `reversed` must be true or false `reversed` muss true oder false sein +@msg `rotate` must be a finite number `rotate` muss eine endliche Zahl sein +@msg `rule` must be nonZero or evenOdd `rule` muss nonZero oder evenOdd sein +@msg `scale` must be a number in (0, 100] `scale` muss eine Zahl in (0, 100] sein +@msg `scope` must be "selection" or "document", not {other} `scope` muss „selection“ oder „document“ sein, nicht {other} +@msg `script` must be a string `script` muss ein String sein +@msg `seriesIndexes` is a non-empty list of series indexes `seriesIndexes` ist eine nicht leere Liste von Reihenindizes +@msg `seriesIndexes` needs a `type` or a `valueAxis` `seriesIndexes` braucht einen `type` oder eine `valueAxis` +@msg `shift` must be a number of points `shift` muss eine Anzahl Punkt sein +@msg `showCenter` must be true or false `showCenter` muss true oder false sein +@msg `singleWordJustification` must be "fullJustify", "alignLeft", "alignCenter" or "alignRight" `singleWordJustification` muss „fullJustify“, „alignLeft“, „alignCenter“ oder „alignRight“ sein +@msg `size` is a percentage of the base text's size, 1 to 100 `size` ist ein Prozentwert der Größe des Grundtexts, 1 bis 100 +@msg `source` and `target` are the same object `source` und `target` sind dasselbe Objekt +@msg `source` must be `folder` or `datasets` `source` muss `folder` oder `datasets` sein +@msg `spacing` must be a number `spacing` muss eine Zahl sein +@msg `spacing` must be a number of points, 0 or more, or "auto" `spacing` muss eine Anzahl Punkt sein, 0 oder größer, oder „auto“ +@msg `start` must be a finite number `start` muss eine endliche Zahl sein +@msg `starts` must be an array of anchor indices (or nulls) `starts` muss ein Array aus Ankerpunktindizes (oder null-Werten) sein +@msg `status` must be linked, embedded, missing or modified, or a list of them `status` muss linked, embedded, missing oder modified oder eine Liste davon sein +@msg `stop` must be a stop index `stop` muss ein Farbstoppindex sein +@msg `story` styles the whole story: give no `start` or `end` `story` formatiert den gesamten Textabschnitt: Geben Sie kein `start` oder `end` an +@msg `story` takes the whole story: give no `start` or `end` `story` nimmt den gesamten Textabschnitt: Geben Sie kein `start` oder `end` an +@msg `strokeMode` applies to strokes (stroke: true) `strokeMode` gilt für Konturen (stroke: true) +@msg `style` must be roman or japanese `style` muss roman oder japanese sein +@msg `subpaths` must be two integer indices `subpaths` müssen zwei ganzzahlige Indizes sein +@msg `symbol` must be a symbol name `symbol` muss ein Symbolname sein +@msg `target` must be "object" or "contents", not {v} `target` muss „object“ oder „contents“ sein, nicht {v} +@msg `tint` applies to global and spot colours `tint` gilt für globale Farben und Volltonfarben +@msg `tint` goes with a global or spot `swatch` `tint` gehört zu einem globalen oder Volltonfarben-`swatch` +@msg `tint` must be a number (%) `tint` muss eine Zahl sein (%) +@msg `tint` needs a global or spot colour `tint` braucht eine globale Farbe oder Volltonfarbe +@msg `to` is already threaded `to` ist bereits verkettet +@msg `to` is the design's new name `to` ist der neue Name des Designs +@msg `to` must be [x, y] `to` muss [x, y] sein +@msg `to` must be a closed path, area type or type on a path other than `from` `to` muss ein geschlossener Pfad, Flächentext oder Pfadtext außer `from` sein +@msg `to` must be a whole number `to` muss eine ganze Zahl sein +@msg `to` takes the rows of one object's stack, not aligned rows `to` nimmt die Zeilen des Stapels eines Objekts, keine ausgerichteten Zeilen +@msg `useLab` must be true or false `useLab` muss true oder false sein +@msg `vAlign` must be default, top, middle, baseline or bottom `vAlign` muss default, top, middle, baseline oder bottom sein +@msg `values` must be a list `values` muss eine Liste sein +@msg `values` must be an object `values` muss ein Objekt sein +@msg `values` must name hex, rgb, cmyk, lab or gray `values` muss hex, rgb, cmyk, lab oder gray nennen +@msg `values` names a kind twice `values` nennt eine Art zweimal +@msg `{_1}` can't be edited or removed `{_1}` kann nicht bearbeitet oder entfernt werden +@msg `{_1}` is built in and always listed `{_1}` ist integriert und wird immer aufgeführt +@msg `{_1}` is not a font file the last text.findFontFiles search found `{_1}` ist keine Schriftdatei, die die letzte Suche mit text.findFontFiles gefunden hat +@msg `{_1}` would be overwritten by the package: choose a different folder or name `{_1}` würde durch das Paket überschrieben: Wählen Sie einen anderen Ordner oder Namen +@msg `{file}` has no art to place `{file}` hat keine Grafik zum Platzieren +@msg `{file}` is over {_1} MB `{file}` ist größer als {_1} MB +@msg `{f}` belongs to a VectorCraft that is running `{f}` gehört zu einem laufenden VectorCraft +@msg `{f}` is the copy of a document open now `{f}` ist die Kopie eines gerade geöffneten Dokuments +@msg `{id}` is no writable format (see document.formats) `{id}` ist kein beschreibbares Format (siehe document.formats) +@msg `{id}` isn't a loaded library `{id}` ist keine geladene Bibliothek +@msg `{key}` has no colours `{key}` hat keine Farben +@msg `{key}` is a design name `{key}` ist ein Designname +@msg `{key}` is a design name or a list of them `{key}` ist ein Designname oder eine Liste davon +@msg `{key}` is longer than {MAX_NAME} characters `{key}` ist länger als {MAX_NAME} Zeichen +@msg `{key}` is out of range `{key}` liegt außerhalb des gültigen Bereichs +@msg `{key}` must be [min, desired, max] or {{min?, desired?, max?}}, in percent `{key}` muss [min, desired, max] oder {{min?, desired?, max?}} in Prozent sein +@msg `{key}` must be a non-negative integer `{key}` muss eine nicht negative ganze Zahl sein +@msg `{key}` must be a number {_1}..{_2} `{key}` muss eine Zahl von {_1}..{_2} sein +@msg `{key}` must be a whole number of at least 0, not {n} `{key}` muss eine ganze Zahl von mindestens 0 sein, nicht {n} +@msg `{key}` must be an appearance item index (paint order) or null `{key}` muss ein Index eines Aussehenselements (Malreihenfolge) oder null sein +@msg `{key}` must be an array of asset ids (see assets.list) `{key}` muss ein Array aus Asset-IDs sein (siehe assets.list) +@msg `{key}` must be from 0 to 180 degrees, not {a} `{key}` muss zwischen 0 und 180 Grad liegen, nicht {a} +@msg `{key}` must be from {_1} to {shift_max} `{key}` muss zwischen {_1} und {shift_max} liegen +@msg `{key}`: not a colour: {v} `{key}`: keine Farbe: {v} +@msg `{k}.family` is required `{k}.family` ist erforderlich +@msg `{k}` is a number 0..1 `{k}` ist eine Zahl von 0..1 +@msg `{k}` is true or false `{k}` ist true oder false +@msg `{k}` isn't an export setting: {_1} `{k}` ist keine Exporteinstellung: {_1} +@msg `{k}` must be a byte offset `{k}` muss ein Byte-Offset sein +@msg `{k}` must be a number `{k}` muss eine Zahl sein +@msg `{k}` must be a number of points `{k}` muss eine Anzahl Punkt sein +@msg `{k}` must be a string `{k}` muss ein String sein +@msg `{k}` must be a whole number `{k}` muss eine ganze Zahl sein +@msg `{k}` must be from {lo} to {hi} for {key} `{k}` muss für {key} zwischen {lo} und {hi} liegen +@msg `{k}` must be {what}, not {v} `{k}` muss {what} sein, nicht {v} +@msg `{l}` isn't a global or spot colour `{l}` ist keine globale Farbe und keine Volltonfarbe +@msg `{m}` isn't a bitmap: use clipboard.importSvg, importPdf or importText `{m}` ist keine Bitmap: Verwenden Sie clipboard.importSvg, importPdf oder importText +@msg `{name}` can't be deleted `{name}` kann nicht gelöscht werden +@msg `{name}` can't be duplicated `{name}` kann nicht dupliziert werden +@msg `{name}` can't be edited `{name}` kann nicht bearbeitet werden +@msg `{name}` can't be edited this way `{name}` kann nicht auf diese Weise bearbeitet werden +@msg `{name}` can't be renamed `{name}` kann nicht umbenannt werden +@msg `{name}` can't name a folder `{name}` kann keinen Ordner benennen +@msg `{name}` has no art to place `{name}` hat keine Grafik zum Platzieren +@msg `{name}` has no text to place `{name}` hat keinen Text zum Platzieren +@msg `{name}` in the Word document is not UTF-8 or UTF-16 text `{name}` im Word-Dokument ist kein UTF-8- oder UTF-16-Text +@msg `{name}` in the Word document is not well-formed XML: {e} `{name}` im Word-Dokument ist kein wohlgeformtes XML: {e} +@msg `{name}` in the Word document nests elements more than {MAX_NESTING} deep `{name}` im Word-Dokument verschachtelt Elemente mehr als {MAX_NESTING} Ebenen tief +@msg `{name}` is a built-in preset and is read-only: save it under another name `{name}` ist eine integrierte Vorgabe und schreibgeschützt: Speichern Sie sie unter einem anderen Namen +@msg `{name}` is a built-in preset: save under another name `{name}` ist eine integrierte Vorgabe: Speichern Sie unter einem anderen Namen +@msg `{name}` is a built-in profile and stays `{name}` ist ein integriertes Profil und bleibt erhalten +@msg `{name}` is a tint of `{base}`: edit `{base}` (a tint swatch only takes a new name) `{name}` ist ein Tonwert von `{base}`: Bearbeiten Sie `{base}` (ein Tonwertfeld nimmt nur einen neuen Namen an) +@msg `{name}` is a working space: choose another with edit.colorSettings first `{name}` ist ein Arbeitsfarbraum: Wählen Sie zuerst mit edit.colorSettings einen anderen +@msg `{name}` is built in and stays `{name}` ist integriert und bleibt erhalten +@msg `{name}` takes a paint of its own kind (colour, gradient or pattern) `{name}` nimmt eine Farbe seiner eigenen Art (Farbe, Verlauf oder Muster) +@msg `{name}`: Minimum must not exceed Desired, nor Desired Maximum `{name}`: Minimum darf Optimal nicht übersteigen und Optimal nicht Maximum +@msg `{n}` can't be deleted `{n}` kann nicht gelöscht werden +@msg `{n}` can't be merged `{n}` kann nicht zusammengeführt werden +@msg `{n}` can't be moved `{n}` kann nicht verschoben werden +@msg `{n}` is not a colour swatch `{n}` ist kein Farbfeld +@msg `{n}` isn't a solid colour `{n}` ist keine Vollfarbe +@msg `{n}`: colour groups hold solid colours only `{n}`: Farbgruppen enthalten nur Vollfarben +@msg `{other}` doesn't trace an image `{other}` zeichnet kein Bild nach +@msg `{s}`: the step's references copy in more than {_1} MB of values `{s}`: Die Verweise des Schritts kopieren mehr als {_1} MB an Werten +@msg `{tag}` is not an axis tag: a tag is four characters, such as "wght" `{tag}` ist kein Achsen-Tag: Ein Tag hat vier Zeichen, etwa „wght“ +@msg `{to}` is empty or already used `{to}` ist leer oder wird bereits verwendet +@msg `{what}` is longer than {MAX_TEXT} bytes `{what}` ist länger als {MAX_TEXT} Byte +@msg `{what}` must be text `{what}` muss Text sein +@msg a DXF drawing has one origin: pass useArtboards: true for one drawing per artboard eine DXF-Zeichnung hat einen Ursprung: Übergeben Sie useArtboards: true für eine Zeichnung pro Zeichenfläche +@msg a PDF presets file with over {_1} presets eine PDF-Vorgabendatei mit über {_1} Vorgaben +@msg a batch preview cannot switch documents eine Stapelvorschau kann nicht zwischen Dokumenten wechseln +@msg a brush named `{nn}` exists ein Pinsel namens `{nn}` existiert bereits +@msg a container of the listed objects is gone ein Container der aufgelisteten Objekte ist nicht mehr vorhanden +@msg a content stream can't be read ein Inhaltsdatenstrom kann nicht gelesen werden +@msg a content stream isn't a stream ein Inhaltsdatenstrom ist kein Datenstrom +@msg a design holds at most {_1} objects ein Design enthält höchstens {_1} Objekte +@msg a design needs a name ein Design braucht einen Namen +@msg a dictionary grew too large ein Wörterbuch ist zu groß geworden +@msg a document keeps at most {_1} graph designs ein Dokument enthält höchstens {_1} Diagrammdesigns +@msg a document keeps at most {_1} saved selections ein Dokument enthält höchstens {_1} gespeicherte Auswahlen +@msg a document keeps at most {_1} views ein Dokument enthält höchstens {_1} Ansichten +@msg a document links at most {MAX_SVG_SCRIPTS} JavaScript files ein Dokument verknüpft höchstens {MAX_SVG_SCRIPTS} JavaScript-Dateien +@msg a feConvolveMatrix filter's order is {x}×{y}, which overflows the kernel-size multiply usvg makes die Ordnung eines feConvolveMatrix-Filters ist {x}×{y}, was bei der Multiplikation der Kernelgröße in usvg überläuft +@msg a gradient preset is already called {n} eine Verlaufsvorgabe heißt bereits {n} +@msg a graph has at most {_1} series, so it transposes with at most that many categories ein Diagramm hat höchstens {_1} Reihen, daher lässt es sich nur mit höchstens so vielen Kategorien transponieren +@msg a layer can't be merged into its own sublayer eine Ebene kann nicht mit ihrer eigenen Unterebene zusammengeführt werden +@msg a layer's form box isn't an array der Formularrahmen einer Ebene ist kein Array +@msg a layer's form can't be read das Formular einer Ebene kann nicht gelesen werden +@msg a layer's form has no box das Formular einer Ebene hat keinen Rahmen +@msg a layer's form has no dictionary das Formular einer Ebene hat kein Wörterbuch +@msg a legend draws at most {MAX_CHIPS} swatches eine Legende zeichnet höchstens {MAX_CHIPS} Farbfelder +@msg a name names one asset: give multiple: false, or one object ein Name benennt ein einzelnes Asset: Geben Sie multiple: false oder ein Objekt an +@msg a page can't be read eine Seite kann nicht gelesen werden +@msg a page object is used by something else ein Seitenobjekt wird von etwas anderem verwendet +@msg a page thumbnail must be RGB pixels, 1 to {THUMBNAIL_SIZE} a side (got {w} × {h}) eine Seitenminiatur muss aus RGB-Pixeln bestehen, 1 bis {THUMBNAIL_SIZE} pro Seite (erhalten: {w} × {h}) +@msg a pattern layer doesn't end what it starts eine Musterebene beendet nicht, was sie beginnt +@msg a pattern layer's colors hold something else die Farben einer Musterebene enthalten etwas anderes +@msg a pattern named `{n}` exists ein Muster namens `{n}` existiert bereits +@msg a plug-in with id {_1} is already installed ein Plug-in mit der ID {_1} ist bereits installiert +@msg a preset named `{n}` exists eine Vorgabe namens `{n}` existiert bereits +@msg a print presets file with over {_1} presets eine Druckvorgabendatei mit über {_1} Vorgaben +@msg a profile is already called {n} ein Profil heißt bereits {n} +@msg a row can't go into itself eine Zeile kann nicht in sich selbst eingefügt werden +@msg a series goes on the left or the right value axis eine Reihe gehört auf die linke oder die rechte Werteachse +@msg a start must be an anchor index ≥ 0 or null ein Anfang muss ein Ankerpunktindex ≥ 0 oder null sein +@msg a style named `{name}` already exists ein Format namens `{name}` existiert bereits +@msg a style named `{to}` already exists ein Format namens `{to}` existiert bereits +@msg a swatch needs a colour, gradient or pattern ein Farbfeld braucht eine Farbe, einen Verlauf oder ein Muster +@msg a symbol named `{n}` already exists ein Symbol namens `{n}` existiert bereits +@msg a theme has 1 to {THEME_SIZE} colours ein Farbthema hat 1 bis {THEME_SIZE} Farben +@msg a theme is already called {name} (replace: "{name}" overwrites it) ein Farbthema heißt bereits {name} (replace: „{name}“ überschreibt es) +@msg a toolbar name has at most {MAX_NAME} characters ein Werkzeugleistenname hat höchstens {MAX_NAME} Zeichen +@msg a view named `{_1}` exists eine Ansicht namens `{_1}` existiert bereits +@msg a view named `{name}` exists eine Ansicht namens `{name}` existiert bereits +@msg a workspace keeps at most {MAX} toolbars ein Arbeitsbereich enthält höchstens {MAX} Werkzeugleisten +@msg a {_1} series can't go in a {_2} graph eine Reihe vom Typ {_1} kann nicht in ein Diagramm vom Typ {_2} +@msg aiVersion {v}: "pdf" (PDF-compatible, the default) or "7" (legacy .ai, version 7) aiVersion {v}: „pdf“ (PDF-kompatibel, Standard) oder „7“ (älteres .ai, Version 7) +@msg already editing an opacity mask eine Deckkraftmaske wird bereits bearbeitet +@msg already editing another object's opacity mask die Deckkraftmaske eines anderen Objekts wird bereits bearbeitet +@msg already in pattern editing mode bereits im Musterbearbeitungsmodus +@msg an artboard is listed twice eine Zeichenfläche ist zweimal aufgeführt +@msg an empty artboard has no width or height to scale to eine leere Zeichenfläche hat keine Breite oder Höhe, auf die skaliert werden kann +@msg an image is too large for DXF and was left out ein Bild ist zu groß für DXF und wurde ausgelassen +@msg an image of {_1} MB, at most {_2} MB ein Bild mit {_1} MB, höchstens {_2} MB +@msg an image of {declared_w} × {declared_h} pixels is too large to draw ein Bild mit {declared_w} × {declared_h} Pixeln ist zu groß zum Zeichnen +@msg an image of {width} × {height} pixels is too large to draw ein Bild mit {width} × {height} Pixeln ist zu groß zum Zeichnen +@msg an index must be a whole number ein Index muss eine ganze Zahl sein +@msg an inline graphic has no glyphs eine eingebundene Grafik hat keine Glyphen +@msg an invisible text form can't be read ein unsichtbares Textformular kann nicht gelesen werden +@msg an object is missing ein Objekt fehlt +@msg an object's header is missing der Kopf eines Objekts fehlt +@msg an overprinting form can't be read ein überdruckendes Formular kann nicht gelesen werden +@msg an unread object has no readable bounds ein nicht gelesenes Objekt hat keine lesbaren Begrenzungen +@msg an unread object holds a frame of threaded type ein nicht gelesenes Objekt enthält einen Rahmen mit verkettetem Text +@msg an unread object is missing from its layer ein nicht gelesenes Objekt fehlt in seiner Ebene +@msg anchor `smooth` must be true or false Ankerpunkt-`smooth` muss true oder false sein +@msg anchor `{key}` must be [x, y] Ankerpunkt-`{key}` muss [x, y] sein +@msg anchor `{key}` must be a number Ankerpunkt-`{key}` muss eine Zahl sein +@msg angle and roundness must be finite angle und roundness müssen endlich sein +@msg angles must be a list as long as pins (degrees or null) angles muss eine Liste so lang wie pins sein (Grad oder null) +@msg appearance must be flat|center|edge appearance muss flat|center|edge sein +@msg apply is "fill" or "stroke", not `{other}` apply ist „fill“ oder „stroke“, nicht `{other}` +@msg area must be a closed polygon of 3 to 100000 [x, y] points area muss ein geschlossenes Polygon aus 3 bis 100000 Punkten [x, y] sein +@msg area type needs a path that encloses an area Flächentext braucht einen Pfad, der eine Fläche umschließt +@msg area type {_1} is named twice Flächentext {_1} ist zweimal genannt +@msg arrowAlign must be extend|tip, got {o} arrowAlign muss extend|tip sein, erhalten: {o} +@msg arrowScale must be [start, end] arrowScale muss [start, end] sein +@msg artboard is a 0-based index artboard ist ein nullbasierter Index +@msg artboard must be a 0-based index artboard muss ein nullbasierter Index sein +@msg artboard {i} doesn't print Zeichenfläche {i} wird nicht gedruckt +@msg artboardLayout must be {layout?, columns?, spacing?, rightToLeft?} artboardLayout muss {layout?, columns?, spacing?, rightToLeft?} sein +@msg artboardLayout.columns must be 1 or more artboardLayout.columns muss 1 oder größer sein +@msg artboardLayout.layout must be gridByRow, gridByColumn, row or column artboardLayout.layout muss gridByRow, gridByColumn, row oder column sein +@msg artboardLayout.rightToLeft must be true or false artboardLayout.rightToLeft muss true oder false sein +@msg artboardLayout.spacing must be 0–{MAX_SPACING} pt artboardLayout.spacing muss 0–{MAX_SPACING} pt sein +@msg artboards must be a non-empty list artboards muss eine nicht leere Liste sein +@msg artboards must be a number artboards muss eine Zahl sein +@msg asset `{_1}` has no art to export Asset `{_1}` hat keine Grafik zum Exportieren +@msg at most {MAX_KEYWORDS} keywords höchstens {MAX_KEYWORDS} Stichwörter +@msg at most {MAX_MATCHES} matches are kept höchstens {MAX_MATCHES} Treffer werden behalten +@msg at most {MAX_POINTS} points höchstens {MAX_POINTS} Punkte +@msg at most {MAX_QUEUE} files höchstens {MAX_QUEUE} Dateien +@msg at must be [x, y] at muss [x, y] sein +@msg at/rect lie outside the canvas at/rect liegen außerhalb der Arbeitsfläche +@msg attribute must be size|leading|tracking|kerning|baselineShift attribute muss size|leading|tracking|kerning|baselineShift sein +@msg attrs must be an object attrs muss ein Objekt sein +@msg automation path rejected: the folder's name isn't valid text Automatisierungspfad abgelehnt: Der Name des Ordners ist kein gültiger Text +@msg automation path rejected: {_1} isn't valid text Automatisierungspfad abgelehnt: {_1} ist kein gültiger Text +@msg automation path rejected: {why}: {path} Automatisierungspfad abgelehnt: {why}: {path} +@msg axis must be "vertical", "horizontal" or an angle in degrees, not `{given}` axis muss „vertical“, „horizontal“ oder ein Winkel in Grad sein, nicht `{given}` +@msg backgroundContents must be transparent or white backgroundContents muss transparent oder white sein +@msg bad AES key ungültiger AES-Schlüssel +@msg bad `runs`: {e} ungültige `runs`: {e} +@msg bad arrowScale ungültige arrowScale +@msg bad base64 ungültiges Base64 +@msg bad color {c} ungültige Farbe {c} +@msg bad colour ungültige Farbe +@msg bad colour in `colors` ungültige Farbe in `colors` +@msg bad colour {v} ungültige Farbe {v} +@msg bad colour {v} in groupColors ungültige Farbe {v} in groupColors +@msg bad cross-reference entry ungültiger Querverweiseintrag +@msg bad cross-reference table ungültige Querverweistabelle +@msg bad current colour {v} ungültige aktuelle Farbe {v} +@msg bad dataBase64 ungültiges dataBase64 +@msg bad edit ungültige Bearbeitung +@msg bad fill {v} ungültige Fläche {v} +@msg bad fit: {e} ungültige Einpassung: {e} +@msg bad model {other} (keep, new or document) ungültiges Modell {other} (keep, new oder document) +@msg bad new colour {v} ungültige neue Farbe {v} +@msg bad object ungültiges Objekt +@msg bad object number ungültige Objektnummer +@msg bad offset ungültiger Offset +@msg bad path data: {e} ungültige Pfaddaten: {e} +@msg bad point {v}: expected [x, y] or [x, y, pressure] ungültiger Punkt {v}: erwartet wird [x, y] oder [x, y, pressure] +@msg bad reference ungültiger Verweis +@msg bad trailer size ungültige Trailer-Größe +@msg bad {k} colour ungültige {k}-Farbe +@msg batches cannot nest Stapel können nicht verschachtelt werden +@msg bind something to a variable first binden Sie zuerst etwas an eine Variable +@msg bleed must be 0–{MAX_BLEED} pt: a number, [top, bottom, left, right] or {{top, bottom, left, right}} bleed muss 0–{MAX_BLEED} pt sein: eine Zahl, [top, bottom, left, right] oder {{top, bottom, left, right}} +@msg brush must be a name or null brush muss ein Name oder null sein +@msg by must be a number by muss eine Zahl sein +@msg can't add page thumbnails: {why} Seitenminiaturen können nicht hinzugefügt werden: {why} +@msg can't add the editing data for other apps: {why} die Bearbeitungsdaten für andere Anwendungen können nicht hinzugefügt werden: {why} +@msg can't edit the written PDF: {why} das geschriebene PDF kann nicht bearbeitet werden: {why} +@msg can't encrypt the PDF: {why} das PDF kann nicht verschlüsselt werden: {why} +@msg can't linearise the PDF: {why} das PDF kann nicht linearisiert werden: {why} +@msg can't open `{_1}`: {_2} `{_1}` kann nicht geöffnet werden: {_2} +@msg can't open `{_1}`: {e} `{_1}` kann nicht geöffnet werden: {e} +@msg can't open `{name}`: not a format VectorCraft reads (see document.formats) `{name}` kann nicht geöffnet werden: kein Format, das VectorCraft liest (siehe document.formats) +@msg can't place `{name}`: not a format VectorCraft reads (see document.formats) `{name}` kann nicht platziert werden: kein Format, das VectorCraft liest (siehe document.formats) +@msg can't read back the written PDF: {e} das geschriebene PDF kann nicht zurückgelesen werden: {e} +@msg can't read this Word document: {why} dieses Word-Dokument kann nicht gelesen werden: {why} +@msg can't write .{e} images: use .png, .jpg, .gif, .webp, .tif or .bmp .{e}-Bilder können nicht geschrieben werden: Verwenden Sie .png, .jpg, .gif, .webp, .tif oder .bmp +@msg can't write PDF layers, overprint or invisible text: {why} PDF-Ebenen, Überdrucken oder unsichtbarer Text können nicht geschrieben werden: {why} +@msg can't write the file's copyright: {why} das Copyright der Datei kann nicht geschrieben werden: {why} +@msg can't write the file's modification date: {why} das Änderungsdatum der Datei kann nicht geschrieben werden: {why} +@msg can't write the pages' user units: {why} die Benutzereinheiten der Seiten können nicht geschrieben werden: {why} +@msg cannot distort a zero-size or non-finite bounding box ein Begrenzungsrahmen der Größe null oder mit nicht endlichen Werten kann nicht verzerrt werden +@msg cannot move an object into itself ein Objekt kann nicht in sich selbst verschoben werden +@msg case must be upper|lower|title|sentence case muss upper|lower|title|sentence sein +@msg category `{c}`: one of {_1} Kategorie `{c}`: eines von {_1} +@msg center must be [x, y] center muss [x, y] sein +@msg choose at least one artboard wählen Sie mindestens eine Zeichenfläche +@msg choose at least one status wählen Sie mindestens einen Status +@msg clip must be true or false clip muss true oder false sein +@msg clippingMask must be true or false clippingMask muss true oder false sein +@msg closed must be a boolean closed muss ein boolescher Wert sein +@msg collapsed must be true or false collapsed muss true oder false sein +@msg color.profile: no RGB or CMYK profile is called “{profile}” color.profile: Kein RGB- oder CMYK-Profil heißt „{profile}“ +@msg colorMode must be rgb or cmyk, not `{m}` colorMode muss rgb oder cmyk sein, nicht `{m}` +@msg colorModel must be {own} (the document's mode), grayscale or bitmap colorModel muss {own} (der Modus des Dokuments), grayscale oder bitmap sein +@msg colour groups can't go into colour groups Farbgruppen können nicht in Farbgruppen eingefügt werden +@msg colour, mode and spot apply to solid-colour swatches only colour, mode und spot gelten nur für Vollfarbfelder +@msg column {column}: `{cell}` is a `%` column of graph data files, which are not read Spalte {column}: `{cell}` ist eine `%`-Spalte von Diagrammdatendateien, die nicht gelesen werden +@msg column {column}: `{cell}` names `{name}`, which is a {_1} variable Spalte {column}: `{cell}` nennt `{name}`, eine Variable vom Typ {_1} +@msg column {column}: `{cell}` names no variable Spalte {column}: `{cell}` nennt keine Variable +@msg column {column}: the variable name is longer than {MAX_NAME} characters Spalte {column}: Der Variablenname ist länger als {MAX_NAME} Zeichen +@msg column: true floats the whole column and takes no onto, above or below column: true macht die ganze Spalte schwebend und nimmt kein onto, above oder below +@msg command must be a shape.* command command muss ein shape.*-Befehl sein +@msg commands[{i}]: missing `command` commands[{i}]: `command` fehlt +@msg composer must be "singleLine" or "everyLine" composer muss „singleLine“ oder „everyLine“ sein +@msg compound shapes take paths, compound paths, groups, compound shapes, type, blends and envelopes zusammengesetzte Formen nehmen Pfade, zusammengesetzte Pfade, Gruppen, zusammengesetzte Formen, Text, Angleichungen und Verzerrungshüllen +@msg compound {_1} must contain exactly one open path to join zusammengesetzter Pfad {_1} muss genau einen offenen Pfad zum Zusammenfügen enthalten +@msg connect end must be "start" or "end" connect end muss „start“ oder „end“ sein +@msg connect needs a path other than the one extended connect braucht einen anderen Pfad als den verlängerten +@msg connect needs id connect braucht id +@msg connect path has no open end der connect-Pfad hat kein offenes Ende +@msg copy needs path: the file to copy into the document's folder copy braucht path: die Datei, die in den Ordner des Dokuments kopiert werden soll +@msg copy takes images only: relink placed documents where their files are copy nimmt nur Bilder: Verknüpfen Sie platzierte Dokumente dort neu, wo ihre Dateien liegen +@msg copying a file isn't available on the web das Kopieren einer Datei ist im Web nicht verfügbar +@msg copyrightStatus must be unknown, copyrighted or publicDomain copyrightStatus muss unknown, copyrighted oder publicDomain sein +@msg copyrightUrl must be a URL (no spaces, at most 2048 characters) copyrightUrl muss eine URL sein (keine Leerzeichen, höchstens 2048 Zeichen) +@msg corners must be 4 [x, y] points (TL, TR, BR, BL) corners müssen 4 Punkte [x, y] sein (TL, TR, BR, BL) +@msg counterPer `{c}`: item or file counterPer `{c}`: item oder file +@msg crop `{_1}`: only PDF pages have art, trim, bleed and media boxes; use "crop" or "bounding" crop `{_1}`: Nur PDF-Seiten haben Art-, Trim-, Bleed- und Media-Boxen; verwenden Sie „crop“ oder „bounding“ +@msg crop rectangle doesn't overlap the image das Freistellungsrechteck überlappt das Bild nicht +@msg cross-reference and object streams aren't supported Querverweis- und Objektdatenströme werden nicht unterstützt +@msg damaged compressed data: {e} beschädigte komprimierte Daten: {e} +@msg dash lengths must be numbers, got {v} Strichlängen müssen Zahlen sein, erhalten: {v} +@msg dash must be a list of lengths or null, got {v} dash muss eine Liste von Längen oder null sein, erhalten: {v} +@msg data key `{k}`: letters, digits, -, _ and . (not name or vc-…) Datenschlüssel `{k}`: Buchstaben, Ziffern, -, _ und . (nicht name oder vc-…) +@msg data must be an object of key: value, not {v} data muss ein Objekt aus key: value sein, nicht {v} +@msg data set `{name}` holds a value for `{var}`, which the library doesn't declare Datensatz `{name}` enthält einen Wert für `{var}`, den die Bibliothek nicht deklariert +@msg data set `{name}` holds two values for `{var}` Datensatz `{name}` enthält zwei Werte für `{var}` +@msg data set `{name}` is declared twice Datensatz `{name}` ist zweimal deklariert +@msg data takes at most {MAX_DATA} keys at once data nimmt höchstens {MAX_DATA} Schlüssel auf einmal +@msg dataset `{name}` already exists Datensatz `{name}` existiert bereits +@msg dataset `{to}` already exists Datensatz `{to}` existiert bereits +@msg date `{d}`: YYYY-MM-DD Datum `{d}`: YYYY-MM-DD +@msg decoded data is too large die decodierten Daten sind zu groß +@msg define a variable first definieren Sie zuerst eine Variable +@msg detect must be true or false detect muss true oder false sein +@msg direct-select anchor points of a path wählen Sie Ankerpunkte eines Pfads direkt aus +@msg distance must be > 0 distance muss > 0 sein +@msg dx and dy give the arrow's direction dx und dy geben die Richtung des Pfeils an +@msg dx and dy must be numbers dx und dy müssen Zahlen sein +@msg dxf fitTo must be [width, height], positive dxf fitTo muss [width, height] sein, positiv +@msg dxf must be an object of DXF import options dxf muss ein Objekt aus DXF-Importoptionen sein +@msg dxf scale must be a positive number up to {_1}, not {s} dxf scale muss eine positive Zahl bis {_1} sein, nicht {s} +@msg dxf unit `{u}`: a unit such as mm, cm, in or pt dxf unit `{u}`: eine Einheit wie mm, cm, in oder pt +@msg dxf: {e} dxf: {e} +@msg each format is an object {format, scale?, suffix?} jedes Format ist ein Objekt {format, scale?, suffix?} +@msg each item needs an id jedes Element braucht eine id +@msg each match needs `kind` jeder Treffer braucht `kind` +@msg each match needs a `name` jeder Treffer braucht einen `name` +@msg each of `fonts` is {family, style?}; 1 to 1000 of them jedes Element von `fonts` ist {family, style?}; 1 bis 1000 davon +@msg each of `items` is {kind, item} jedes Element von `items` ist {kind, item} +@msg each of `items` is {{id, corners?}} with an object id, not {it} jedes Element von `items` ist {{id, corners?}} mit einer Objekt-ID, nicht {it} +@msg each row needs `from` jede Zeile braucht `from` +@msg each stop needs a position jeder Farbstopp braucht eine Position +@msg each subpath needs an anchors array jeder Teilpfad braucht ein anchors-Array +@msg easingType must be easeIn, easeOut, easeInOut or easeOutIn easingType muss easeIn, easeOut, easeInOut oder easeOutIn sein +@msg editingData must be true or false editingData muss true oder false sein +@msg editingData must be true, false or "always" editingData muss true, false oder „always“ sein +@msg edits overlap Bearbeitungen überlappen sich +@msg effects of objects whose appearances differ can't be moved together Effekte von Objekten mit unterschiedlichem Aussehen können nicht zusammen verschoben werden +@msg empty header in column {_1} leere Kopfzeile in Spalte {_1} +@msg encoding `{_1}`: utf8 or utf16 Codierung `{_1}`: utf8 oder utf16 +@msg ends must be two of "first" and "last" ends muss zwei von „first“ und „last“ sein +@msg every numbered name for the result is taken jeder nummerierte Name für das Ergebnis ist bereits vergeben +@msg expanded SVG styles exceed the import size limit die erweiterten SVG-Stile überschreiten die Importgrößengrenze +@msg export presets file is larger than {MAX_PRESET_FILE_BYTES} bytes die Exportvorgabendatei ist größer als {MAX_PRESET_FILE_BYTES} Byte +@msg exportText must be editable or appearance exportText muss editable oder appearance sein +@msg exportType must be movieClip or graphic, not `{s}` exportType muss movieClip oder graphic sein, nicht `{s}` +@msg extend needs id extend braucht id +@msg featureChanges lists at most {MAX_FEATURE_CHANGES} tags featureChanges listet höchstens {MAX_FEATURE_CHANGES} Tags auf +@msg featureChanges must be a list of tags featureChanges muss eine Liste von Tags sein +@msg features must be a list of tags features muss eine Liste von Tags sein +@msg fidelity must be 0..100 fidelity muss 0..100 sein +@msg files must be {name, dataBase64} files muss {name, dataBase64} sein +@msg fit must be none|autoHeight|shrinkText|block, got {id} fit muss none|autoHeight|shrinkText|block sein, erhalten: {id} +@msg flattener: {e} Reduzierung: {e} +@msg flattenerPreset `{name}`: high, medium, low or a saved one (see flattener.presets.list) flattenerPreset `{name}`: high, medium, low oder eine gespeicherte (siehe flattener.presets.list) +@msg flipProfile must be along|across, got {o} flipProfile muss along|across sein, erhalten: {o} +@msg folders `{f}`: none, rows, format, item or itemFormat folders `{f}`: none, rows, format, item oder itemFormat +@msg folders deeper than {MAX_BATCH_DEPTH} levels Ordner tiefer als {MAX_BATCH_DEPTH} Ebenen +@msg folders deeper than {MAX_DEPTH} levels Ordner tiefer als {MAX_DEPTH} Ebenen +@msg font family `{family}` is not available die Schriftfamilie `{family}` ist nicht verfügbar +@msg font family `{tf}` is not available die Schriftfamilie `{tf}` ist nicht verfügbar +@msg format `{f}`: pdf or postscript Format `{f}`: pdf oder postscript +@msg format `{f}`: text or json Format `{f}`: text oder json +@msg format is `{format}` das Format ist `{format}` +@msg formats must be an array of rows, not {v} formats muss ein Array aus Zeilen sein, nicht {v} +@msg frequency must be between 1 and 300 lines per inch frequency muss zwischen 1 und 300 Linien pro Zoll liegen +@msg from and to must be finite from und to müssen endlich sein +@msg from must be [x0, y0, x1, y1] from muss [x0, y0, x1, y1] sein +@msg give `before` or `after`, not both geben Sie `before` oder `after` an, nicht beides +@msg give `colors`, or `color` and `rule` geben Sie `colors` an, oder `color` und `rule` +@msg give `event`, such as onclick geben Sie `event` an, etwa onclick +@msg give `file`, the JavaScript file's path or URL geben Sie `file` an, den Pfad oder die URL der JavaScript-Datei +@msg give `fill` and/or `stroke` geben Sie `fill` und/oder `stroke` an +@msg give `fill`, `stroke` or both geben Sie `fill`, `stroke` oder beides an +@msg give `folder` geben Sie `folder` an +@msg give `fonts`, or open a document geben Sie `fonts` an, oder öffnen Sie ein Dokument +@msg give `from` or `indices`, not both geben Sie `from` oder `indices` an, nicht beides +@msg give `id` (no text is being edited) geben Sie `id` an (es wird kein Text bearbeitet) +@msg give `id` and `anchors`, or `items`, not both geben Sie `id` und `anchors` oder `items` an, nicht beides +@msg give `id` or `guide`, not both geben Sie `id` oder `guide` an, nicht beides +@msg give `length` or `to` geben Sie `length` oder `to` an +@msg give `matches`: [{kind, name, target}] geben Sie `matches` an: [{kind, name, target}] +@msg give `name` (or edit a pattern) geben Sie `name` an (oder bearbeiten Sie ein Muster) +@msg give `name` or `names` geben Sie `name` oder `names` an +@msg give `path` (a .wasm file) or `dataBase64` (the module) geben Sie `path` (eine .wasm-Datei) oder `dataBase64` (das Modul) an +@msg give `path`, `data` or `dataBase64` geben Sie `path`, `data` oder `dataBase64` an +@msg give `path`, `data`, `dataBase64` or `csv` geben Sie `path`, `data`, `dataBase64` oder `csv` an +@msg give `path`, `xml` or `dataBase64` geben Sie `path`, `xml` oder `dataBase64` an +@msg give `script`, the JavaScript the event runs ("" removes the event) geben Sie `script` an, das JavaScript, das das Ereignis ausführt ("" entfernt das Ereignis) +@msg give `text` (the reading) or `remove` geben Sie `text` (die Lesung) oder `remove` an +@msg give `x` and `y` (and a `tolerance` of 0 or more) geben Sie `x` und `y` an (und eine `tolerance` von 0 oder größer) +@msg give a mode or r/g/b, c/m/y/k, gray or tint adjustments geben Sie einen Modus oder Anpassungen für r/g/b, c/m/y/k, gray oder tint an +@msg give a name, or paint the active proxy with a saved preset geben Sie einen Namen an, oder färben Sie den aktiven Proxy mit einer gespeicherten Vorgabe +@msg give a name, or select a stroke with a saved profile geben Sie einen Namen an, oder wählen Sie eine Kontur mit einem gespeicherten Profil aus +@msg give a pattern name or select an object painted with a pattern geben Sie einen Musternamen an, oder wählen Sie ein mit einem Muster gefärbtes Objekt aus +@msg give at least one of clip, invert, disabled, linked geben Sie mindestens eines von clip, invert, disabled, linked an +@msg give color, none, swatch or gradient geben Sie color, none, swatch oder gradient an +@msg give dataBase64 geben Sie dataBase64 an +@msg give either at (a click) or area (a path drawn around the overlaps) geben Sie entweder at (einen Klick) oder area (einen um die Überlappungen gezeichneten Pfad) an +@msg give formats or a preset, not both geben Sie formats oder eine Vorgabe an, nicht beides +@msg give newName or delete geben Sie newName oder delete an +@msg give offset or by geben Sie offset oder by an +@msg give one of `to`, `rect` or `at` geben Sie eines von `to`, `rect` oder `at` an +@msg give one of assets, layers, selection or fullDocument geben Sie eines von assets, layers, selection oder fullDocument an +@msg give only one of onto, above, below, icon and a column place geben Sie nur eines von onto, above, below, icon und einem Spaltenplatz an +@msg give origin: [x, y], or reset: true geben Sie origin: [x, y] oder reset: true an +@msg give overprintFill, overprintStroke, showCenter, imageMap, url or note geben Sie overprintFill, overprintStroke, showCenter, imageMap, url oder note an +@msg give path (a file) or folder geben Sie path (eine Datei) oder folder an +@msg give path, or name and dataBase64 geben Sie path an, oder name und dataBase64 +@msg give paths or files geben Sie paths oder files an +@msg give rows, rowHeight, columns or columnWidth geben Sie rows, rowHeight, columns oder columnWidth an +@msg give svg, or dataBase64 of an SVG or SVGZ file geben Sie svg an, oder dataBase64 einer SVG- oder SVGZ-Datei +@msg give text geben Sie text an +@msg give the asset a name geben Sie dem Asset einen Namen +@msg give the preset a `name` geben Sie der Vorgabe einen `name` +@msg give the preset a name geben Sie der Vorgabe einen Namen +@msg give the preset a name (1–64 characters) geben Sie der Vorgabe einen Namen (1–64 Zeichen) +@msg give the settings to change, e.g. {"formats"}]} geben Sie die zu ändernden Einstellungen an, z. B. {"formats"}]} +@msg give the style a name geben Sie dem Format einen Namen +@msg give two or more style `names` geben Sie zwei oder mehr Formatnamen in `names` an +@msg give two or more swatches geben Sie zwei oder mehr Farbfelder an +@msg give x, y, or a handle's length or angle geben Sie x, y oder die Länge oder den Winkel eines Griffs an +@msg give {key, value} or {values} geben Sie {key, value} oder {values} an +@msg global must be true or false global muss true oder false sein +@msg glyph {_1} is not drawn for {ch} with the character's style (such as All Caps, Small Caps or a numeral system) Glyphe {_1} wird für {ch} mit dem Format des Zeichens nicht gezeichnet (etwa Großbuchstaben, Kapitälchen oder ein Zahlensystem) +@msg glyph {g} is not an alternate of {ch} in {_1} {_2} Glyphe {g} ist keine Alternative zu {ch} in {_1} {_2} +@msg gradient must be "objects" or "mesh" gradient muss „objects“ oder „mesh“ sein +@msg grays must be "profile" or "black", not `{g}` grays muss „profile“ oder „black“ sein, nicht `{g}` +@msg gridColors must be a preset name or two colours gridColors muss ein Vorgabenname oder zwei Farben sein +@msg gridSize must be small, medium or large gridSize muss small, medium oder large sein +@msg guide {g} is not selected Hilfslinie {g} ist nicht ausgewählt +@msg handle must be 0..3 handle muss 0..3 sein +@msg hiddenObjects {v}: "layer" (the default), "omit" or "visible" hiddenObjects {v}: „layer“ (Standard), „omit“ oder „visible“ +@msg highlight a layer to merge into markieren Sie eine Ebene, in die zusammengeführt werden soll +@msg highlight two or more rows in one layer or group markieren Sie zwei oder mehr Zeilen in einer Ebene oder Gruppe +@msg highlight two or more rows to merge markieren Sie zwei oder mehr Zeilen zum Zusammenführen +@msg icon docks a panel in the icon column: use window.panel.dock icon dockt ein Bedienfeld in der Symbolspalte an: Verwenden Sie window.panel.dock +@msg id must be a dimension id id muss eine Bemaßungs-ID sein +@msg image has no bounds das Bild hat keine Begrenzungen +@msg image {_1} is linked already Bild {_1} ist bereits verknüpft +@msg imageMap `{m}`: none, client or server imageMap `{m}`: none, client oder server +@msg index out of range Index außerhalb des gültigen Bereichs +@msg instances must be 1..1000 instances muss 1..1000 sein +@msg invalid `color` ungültige `color` +@msg invalid anchor index {v}: expected a non-negative integer ungültiger Ankerpunktindex {v}: erwartet wird eine nicht negative ganze Zahl +@msg invalid anchor reference {v}: expected [subpath, anchor] ungültiger Ankerpunktverweis {v}: erwartet wird [subpath, anchor] +@msg invalid angle ungültiger Winkel +@msg invalid brush: {e} ungültiger Pinsel: {e} +@msg invalid color ungültige Farbe +@msg invalid colour {c} ungültige Farbe {c} +@msg invalid delta ungültiges Delta +@msg invalid point ungültiger Punkt +@msg invalid size ungültige Größe +@msg invalid t ungültiges t +@msg invalid tolerance ungültige Toleranz +@msg its artboards all lie beyond the canvas (more than {_1} pt from the origin) seine Zeichenflächen liegen alle außerhalb der Arbeitsfläche (mehr als {_1} pt vom Ursprung entfernt) +@msg its clipping masks are nested too deeply seine Schnittmasken sind zu tief verschachtelt +@msg its metadata has no description seine Metadaten haben keine Beschreibung +@msg its metadata has no direct length seine Metadaten haben keine direkte Länge +@msg join two different paths fügen Sie zwei verschiedene Pfade zusammen +@msg justify must be auto|left|center|right|justifyAll justify muss auto|left|center|right|justifyAll sein +@msg kbps must be a positive number, not {kbps} kbps muss eine positive Zahl sein, nicht {kbps} +@msg keywords must be a list of words or a comma-separated string keywords muss eine Liste von Wörtern oder ein durch Kommas getrennter String sein +@msg keywords must be at most {MAX_KEYWORD} characters, without control characters keywords darf höchstens {MAX_KEYWORD} Zeichen ohne Steuerzeichen haben +@msg kind must be 1, 2 or 3, or give a preset `name` (see perspective.presets.list) kind muss 1, 2 oder 3 sein, oder geben Sie einen Vorgaben-`name` an (siehe perspective.presets.list) +@msg kind must be graphic, fillColor, strokeColor, fillGradient, strokeGradient, fillPattern, strokePattern, charStyle or paraStyle kind muss graphic, fillColor, strokeColor, fillGradient, strokeGradient, fillPattern, strokePattern, charStyle oder paraStyle sein +@msg knockout must be "on", "off", "neutral" or a bool, not {v} knockout muss „on“, „off“, „neutral“ oder ein bool sein, nicht {v} +@msg layer `{name}` has no art to export Ebene `{name}` hat keine Grafik zum Exportieren +@msg layer must name a layer or sublayer layer muss eine Ebene oder Unterebene nennen +@msg layers can't be intertwined: select the objects in them Ebenen können nicht verflochten werden: Wählen Sie die Objekte darin aus +@msg layers go only at the top level or in layers, objects in layers and groups Ebenen gehören nur auf die oberste Stufe oder in Ebenen, Objekte in Ebenen und Gruppen +@msg layers must be true or false layers muss true oder false sein +@msg layout must be gridByRow, gridByColumn, row or column layout muss gridByRow, gridByColumn, row oder column sein +@msg leading must be a number or "auto" leading muss eine Zahl oder „auto“ sein +@msg left/right must be non-negative widths left/right müssen nicht negative Breiten sein +@msg level `{t}`: 2 or 3 Stufe `{t}`: 2 oder 3 +@msg line {line}: more than {MAX_CSV_COLS} columns Zeile {line}: mehr als {MAX_CSV_COLS} Spalten +@msg line {line}: unclosed quote Zeile {line}: nicht geschlossenes Anführungszeichen +@msg line {line}: value for `{var}` must match its variable kind Zeile {line}: Der Wert für `{var}` muss zur Art seiner Variable passen +@msg lineEndings `{other}`: lf or crlf lineEndings `{other}`: lf oder crlf +@msg loading profiles from disk isn't available in the browser das Laden von Profilen vom Datenträger ist im Browser nicht verfügbar +@msg margin: a {m} pt margin leaves no imageable area on the paper margin: Ein Rand von {m} pt lässt keinen druckbaren Bereich auf dem Papier +@msg matrix must be finite and invertible, depth finite matrix muss endlich und invertierbar sein, depth endlich +@msg matte: {e} matte: {e} +@msg member is for order front, forward, backward or back member gilt für order front, forward, backward oder back +@msg missing `artboards` `artboards` fehlt +@msg missing `at` [x, y] `at` [x, y] fehlt +@msg missing `attrs` `attrs` fehlt +@msg missing `color` or `opacity` `color` oder `opacity` fehlt +@msg missing `colors` `colors` fehlt +@msg missing `commands` `commands` fehlt +@msg missing `destinationFolder` `destinationFolder` fehlt +@msg missing `effect` id `effect`-ID fehlt +@msg missing `family` `family` fehlt +@msg missing `find` `find` fehlt +@msg missing `gid` (a glyph id, or null for the default glyph) `gid` fehlt (eine Glyphen-ID oder null für die Standardglyphe) +@msg missing `group` `group` fehlt +@msg missing `id` `id` fehlt +@msg missing `id`: give one, or select one type object `id` fehlt: Geben Sie eine an, oder wählen Sie ein Textobjekt aus +@msg missing `index` (a point index, or null to clear) `index` fehlt (ein Punktindex oder null zum Löschen) +@msg missing `index` (a stop index, or null to clear) `index` fehlt (ein Farbstoppindex oder null zum Löschen) +@msg missing `index` (an item index or null) `index` fehlt (ein Elementindex oder null) +@msg missing `item` `item` fehlt +@msg missing `library` `library` fehlt +@msg missing `library` (see graphicStyle.libraries) `library` fehlt (siehe graphicStyle.libraries) +@msg missing `library` (see swatch.library.list) `library` fehlt (siehe swatch.library.list) +@msg missing `map` (rows [{from, to}] or {current}) `map` fehlt (Zeilen [{from, to}] oder {current}) +@msg missing `matrix` [a,b,c,d,e,f] `matrix` [a,b,c,d,e,f] fehlt +@msg missing `name` (see gradient.presets.list) `name` fehlt (siehe gradient.presets.list) +@msg missing `offset` (a distance in points) `offset` fehlt (ein Abstand in Punkt) +@msg missing `path` `path` fehlt +@msg missing `path` (a path object id) `path` fehlt (eine Pfadobjekt-ID) +@msg missing `rule` `rule` fehlt +@msg missing `source` id `source`-ID fehlt +@msg missing `start` (the byte offset of a character) `start` fehlt (der Byte-Offset eines Zeichens) +@msg missing `status`: linked, embedded, missing or modified `status` fehlt: linked, embedded, missing oder modified +@msg missing `target` id `target`-ID fehlt +@msg missing `text` `text` fehlt +@msg missing `to` `to` fehlt +@msg missing `to` (an index) `to` fehlt (ein Index) +@msg missing `to`: a non-negative integer `to` fehlt: eine nicht negative ganze Zahl +@msg missing `type` `type` fehlt +@msg missing `value` `value` fehlt +@msg missing `{key}` `{key}` fehlt +@msg missing `{k}` `{k}` fehlt +@msg missing anchor Ankerpunkt fehlt +@msg missing anchors Ankerpunkte fehlen +@msg missing anchors [[subpath, anchor]…] Ankerpunkte fehlen [[subpath, anchor]…] +@msg missing array `{key}` Array `{key}` fehlt +@msg missing case case fehlt +@msg missing color Farbe fehlt +@msg missing commands Befehle fehlen +@msg missing destination folder Zielordner fehlt +@msg missing from from fehlt +@msg missing group Gruppe fehlt +@msg missing id or ids id oder ids fehlt +@msg missing ids ids fehlt +@msg missing indexes Indizes fehlen +@msg missing matrix [a,b,c,d,e,f] Matrix [a,b,c,d,e,f] fehlt +@msg missing name Name fehlt +@msg missing op op fehlt +@msg missing or invalid `color` `color` fehlt oder ist ungültig +@msg missing or unknown `tool` `tool` fehlt oder ist unbekannt +@msg missing point Punkt fehlt +@msg missing point [x,y] Punkt [x,y] fehlt +@msg missing point `{key}` ([x, y]) Punkt `{key}` fehlt ([x, y]) +@msg missing points Punkte fehlen +@msg missing points [[x,y]…] Punkte fehlen [[x,y]…] +@msg missing segment Segment fehlt +@msg missing stops Farbstopps fehlen +@msg missing subpaths Teilpfade fehlen +@msg missing target Ziel fehlt +@msg missing to to fehlt +@msg missing x, y x, y fehlen +@msg mode is set, add or toggle, not `{other}` mode ist set, add oder toggle, nicht `{other}` +@msg mode must be "cmyk" or "rgb" mode muss „cmyk“ oder „rgb“ sein +@msg mode must be area|onPath, got {o} mode muss area|onPath sein, erhalten: {o} +@msg more than 10000 linked files: package dependencies in smaller parts mehr als 10000 verknüpfte Dateien: Verpacken Sie Abhängigkeiten in kleineren Teilen +@msg more than 256 commands mehr als 256 Befehle +@msg more than {MAX_BATCH_FILES} data sets mehr als {MAX_BATCH_FILES} Datensätze +@msg more than {MAX_BATCH_FILES} files mehr als {MAX_BATCH_FILES} Dateien +@msg more than {MAX_CSV_ROWS} data rows mehr als {MAX_CSV_ROWS} Datenzeilen +@msg more than {MAX_FILES} images mehr als {MAX_FILES} Bilder +@msg more than {MAX_IMPORT} presets mehr als {MAX_IMPORT} Vorgaben +@msg moved must be [[x,y]…] moved muss [[x,y]…] sein +@msg name (a toolbar's name) is required name (der Name einer Werkzeugleiste) ist erforderlich +@msg name (a toolbar's name) must be a string name (der Name einer Werkzeugleiste) muss ein String sein +@msg name must be a toolbar's name name muss der Name einer Werkzeugleiste sein +@msg names is empty names ist leer +@msg names must be an array of symbol names names muss ein Array aus Symbolnamen sein +@msg need `anchor` or `segment` + `t` `anchor` oder `segment` + `t` erforderlich +@msg need at least one anchor mindestens ein Ankerpunkt erforderlich +@msg need at least one point mindestens ein Punkt erforderlich +@msg need at least two points mindestens zwei Punkte erforderlich +@msg needs points braucht Punkte +@msg nineSlice must be true or false nineSlice muss true oder false sein +@msg nineSliceGuides must be [left, top, right, bottom], each -16..16, with left + right < 1 and top + bottom < 1 nineSliceGuides muss [left, top, right, bottom] sein, jeweils -16..16, mit left + right < 1 und top + bottom < 1 +@msg no PDF preset named `{n}` keine PDF-Vorgabe namens `{n}` +@msg no action set is current kein Aktionssatz ist aktuell +@msg no active data set kein aktiver Datensatz +@msg no appearance item {index} kein Aussehenselement {index} +@msg no artboard {_1} keine Zeichenfläche {_1} +@msg no artboard {artboard} (0-based; the document has {_1}) keine Zeichenfläche {artboard} (nullbasiert; das Dokument hat {_1}) +@msg no asset {id} kein Asset {id} +@msg no asset {id} (see assets.list) kein Asset {id} (siehe assets.list) +@msg no assets to export keine Assets zum Exportieren +@msg no assets yet: collect art for export first (assets.add) noch keine Assets: Sammeln Sie zuerst Grafiken für den Export (assets.add) +@msg no brush named `{n}` kein Pinsel namens `{n}` +@msg no character at byte {start} kein Zeichen bei Byte {start} +@msg no colour keine Farbe +@msg no colour group `{group}` keine Farbgruppe `{group}` +@msg no colour group `{name}` keine Farbgruppe `{name}` +@msg no data sets keine Datensätze +@msg no dataset named `{from}` kein Datensatz namens `{from}` +@msg no dataset named `{name}` kein Datensatz namens `{name}` +@msg no datasets keine Datensätze +@msg no design named `{name}` kein Design namens `{name}` +@msg no design named `{n}` kein Design namens `{n}` +@msg no design named `{old}` kein Design namens `{old}` +@msg no encoder for {_1} yet noch kein Encoder für {_1} +@msg no export preset `{name}` (see document.exportPresets.list) keine Exportvorgabe `{name}` (siehe document.exportPresets.list) +@msg no folder: pass `path` or set Preferences › Plug-ins & Scratch Disks › Additional Plug-ins Folder kein Ordner: Übergeben Sie `path` oder legen Sie Voreinstellungen › Plug-ins & Arbeitsvolumes › Zusätzlicher Plug-in-Ordner fest +@msg no fonts to look for: they are all available keine Schriften zu suchen: Alle sind verfügbar +@msg no formats to export keine Formate zum Exportieren +@msg no gradient preset `{name}` (see gradient.presets.list) keine Verlaufsvorgabe `{name}` (siehe gradient.presets.list) +@msg no graphic style `{n}` in `{_1}` kein Grafikstil `{n}` in `{_1}` +@msg no graphic style library `{key}` (see graphicStyle.libraries) keine Grafikstil-Bibliothek `{key}` (siehe graphicStyle.libraries) +@msg no guide selected keine Hilfslinie ausgewählt +@msg no guide selected (or give an index) keine Hilfslinie ausgewählt (oder geben Sie einen Index an) +@msg no guide {i} keine Hilfslinie {i} +@msg no images in the folder keine Bilder im Ordner +@msg no item at index {_1} kein Element bei Index {_1} +@msg no item at index {from} kein Element bei Index {from} +@msg no item at one of the indices {idx} kein Element bei einem der Indizes {idx} +@msg no layer keine Ebene +@msg no layer {id} (see layer.list) keine Ebene {id} (siehe layer.list) +@msg no layers to export keine Ebenen zum Exportieren +@msg no library `{key}` keine Bibliothek `{key}` +@msg no metafile format kein Metadateiformat +@msg no object {_1} kein Objekt {_1} +@msg no objects keine Objekte +@msg no parent layer keine übergeordnete Ebene +@msg no perspective grid preset named `{b}` (see perspective.presets.list) keine Perspektivenrastervorgabe namens `{b}` (siehe perspective.presets.list) +@msg no perspective grid preset named `{n}` keine Perspektivenrastervorgabe namens `{n}` +@msg no point selected (give `index`) kein Punkt ausgewählt (geben Sie `index` an) +@msg no preset called `{from}` (see file.newPresets) keine Vorgabe namens `{from}` (siehe file.newPresets) +@msg no preset called `{name}` (see file.newPresets) keine Vorgabe namens `{name}` (siehe file.newPresets) +@msg no preset named `{n}` keine Vorgabe namens `{n}` +@msg no print preset named `{b}` (see print.presets.list) keine Druckvorgabe namens `{b}` (siehe print.presets.list) +@msg no print preset named `{name}` (presets: {_1}) keine Druckvorgabe namens `{name}` (Vorgaben: {_1}) +@msg no print preset named `{n}` keine Druckvorgabe namens `{n}` +@msg no profile named `{name}` kein Profil namens `{name}` +@msg no readable files in the folder keine lesbaren Dateien im Ordner +@msg no recovery copy `{f}` (see file.recovery.list) keine Wiederherstellungskopie `{f}` (siehe file.recovery.list) +@msg no rows keine Zeilen +@msg no saved PDF preset named `{name}` keine gespeicherte PDF-Vorgabe namens `{name}` +@msg no saved export preset `{name}` keine gespeicherte Exportvorgabe `{name}` +@msg no saved gradient preset called {name} keine gespeicherte Verlaufsvorgabe namens {name} +@msg no saved gradient presets keine gespeicherten Verlaufsvorgaben +@msg no saved preset called `{name}` keine gespeicherte Vorgabe namens `{name}` +@msg no saved presets to export (name [Default] in `names` to export it) keine gespeicherten Vorgaben zum Exportieren (nennen Sie [Default] in `names`, um sie zu exportieren) +@msg no saved presets to export (name built-in ones in `names`) keine gespeicherten Vorgaben zum Exportieren (nennen Sie integrierte in `names`) +@msg no saved print preset named `{name}` keine gespeicherte Druckvorgabe namens `{name}` +@msg no saved profile called {name} kein gespeichertes Profil namens {name} +@msg no saved selection `{name}` keine gespeicherte Auswahl `{name}` +@msg no saved theme called {name} (see colorTheme.list) kein gespeichertes Farbthema namens {name} (siehe colorTheme.list) +@msg no saved views keine gespeicherten Ansichten +@msg no series {i} keine Reihe {i} +@msg no slice {n} in the image (see slice.list) kein Slice {n} im Bild (siehe slice.list) +@msg no source art keine Quellgrafik +@msg no stop {i} (the gradient has {n}) kein Farbstopp {i} (der Verlauf hat {n}) +@msg no style `{name}` kein Format `{name}` +@msg no such assets (see assets.list) keine solchen Assets (siehe assets.list) +@msg no such frame anchor, or the frame would reach past the canvas kein solcher Rahmenankerpunkt, oder der Rahmen würde über die Arbeitsfläche hinausreichen +@msg no such objects keine solchen Objekte +@msg no such path anchor, or the path would reach past the canvas kein solcher Pfadankerpunkt, oder der Pfad würde über die Arbeitsfläche hinausreichen +@msg no such rows keine solchen Zeilen +@msg no swatch `{keep}` kein Farbfeld `{keep}` +@msg no swatch `{n}` kein Farbfeld `{n}` +@msg no swatch `{n}` (swatches and colour groups move separately) kein Farbfeld `{n}` (Farbfelder und Farbgruppen werden getrennt verschoben) +@msg no swatch library `{key}` (see swatch.library.list) keine Farbfeldbibliothek `{key}` (siehe swatch.library.list) +@msg no swatch library `{key}` (see swatch.library.list; "document": the document's swatches) keine Farbfeldbibliothek `{key}` (siehe swatch.library.list; „document“: die Farbfelder des Dokuments) +@msg no swatch library `{key}` (see swatch.library.list; "{DOCUMENT_SWATCHES}": the document's swatches) keine Farbfeldbibliothek `{key}` (siehe swatch.library.list; „{DOCUMENT_SWATCHES}“: die Farbfelder des Dokuments) +@msg no swatch named `{n}` kein Farbfeld namens `{n}` +@msg no swatch or colour group `{n}` kein Farbfeld und keine Farbgruppe `{n}` +@msg no swatch or colour group `{n}` in `{_1}` kein Farbfeld und keine Farbgruppe `{n}` in `{_1}` +@msg no symbol named `{name}` kein Symbol namens `{name}` +@msg no text objects selected keine Textobjekte ausgewählt +@msg no toolbar named `{_1}` keine Werkzeugleiste namens `{_1}` +@msg no toolbar named `{name}` keine Werkzeugleiste namens `{name}` +@msg no tracing preset `{preset}` (see imageTrace.presets) keine Nachzeichnervorgabe `{preset}` (siehe imageTrace.presets) +@msg no user library folder here (save with a path, or without one to get the data) hier ist kein Benutzerbibliotheksordner (speichern Sie mit einem Pfad oder ohne Pfad, um die Daten zu erhalten) +@msg no variable named `{from}` keine Variable namens `{from}` +@msg no variable named `{name}` keine Variable namens `{name}` +@msg no variable named `{var}` keine Variable namens `{var}` +@msg no variable named `{v}` keine Variable namens `{v}` +@msg no view named `{name}` keine Ansicht namens `{name}` +@msg node {_1} is not a graph Knoten {_1} ist kein Diagramm +@msg node {_1} is not an image Knoten {_1} ist kein Bild +@msg node {_1} is not type Knoten {_1} ist kein Text +@msg not a PDF presets file (format `{_1}`) keine PDF-Vorgabendatei (Format `{_1}`) +@msg not a PDF presets file: {e} keine PDF-Vorgabendatei: {e} +@msg not a Word document: its WordDocument stream doesn't start with a file information block kein Word-Dokument: Sein WordDocument-Datenstrom beginnt nicht mit einem File Information Block +@msg not a Word document: its compound file has no WordDocument stream kein Word-Dokument: Seine Verbunddatei hat keinen WordDocument-Datenstrom +@msg not a Word document: its main part is not a WordprocessingML document kein Word-Dokument: Sein Hauptteil ist kein WordprocessingML-Dokument +@msg not a Word document: its package has no main document part kein Word-Dokument: Sein Paket hat keinen Hauptdokumentteil +@msg not a closed path kein geschlossener Pfad +@msg not a flattener presets file (format `{_1}`) keine Reduzierungsvorgabendatei (Format `{_1}`) +@msg not a flattener presets file: {e} keine Reduzierungsvorgabendatei: {e} +@msg not a layer or group keine Ebene oder Gruppe +@msg not a library file: {e} keine Bibliotheksdatei: {e} +@msg not a mesh envelope point kein Punkt eines Hüllengitters +@msg not a perspective grid presets file (format `{_1}`) keine Perspektivenrastervorgabendatei (Format `{_1}`) +@msg not a perspective grid presets file: {e} keine Perspektivenrastervorgabendatei: {e} +@msg not a print presets file (format `{_1}`) keine Druckvorgabendatei (Format `{_1}`) +@msg not a print presets file: {e} keine Druckvorgabendatei: {e} +@msg not a stored or deflated central record kein gespeicherter oder Deflate-komprimierter zentraler Eintrag +@msg not a valid SVGZ file: {e} keine gültige SVGZ-Datei: {e} +@msg not an RTF file: it doesn't start with {\\rtf keine RTF-Datei: Sie beginnt nicht mit {\\rtf +@msg not an image VectorCraft reads (see document.formats) kein Bild, das VectorCraft liest (siehe document.formats) +@msg not area type (text in a frame) outside perspective kein Flächentext (Text in einem Rahmen) außerhalb der Perspektive +@msg not editing an opacity mask es wird keine Deckkraftmaske bearbeitet +@msg not type on a path outside perspective kein Pfadtext außerhalb der Perspektive +@msg nothing prints nichts wird gedruckt +@msg nothing to bind nichts zu binden +@msg nothing to change nichts zu ändern +@msg nothing to change: give dx, dy, hScale, vScale or rotation nichts zu ändern: Geben Sie dx, dy, hScale, vScale oder rotation an +@msg nothing to crop nichts zuzuschneiden +@msg nothing to export nichts zu exportieren +@msg nothing to export: the document has no visible art nichts zu exportieren: Das Dokument hat keine sichtbare Grafik +@msg nothing to mark nichts zu markieren +@msg nothing to print: every artboard picked is blank nichts zu drucken: Jede gewählte Zeichenfläche ist leer +@msg nothing to print: the document has no art that prints nichts zu drucken: Das Dokument hat keine Grafik, die gedruckt wird +@msg object is not a path das Objekt ist kein Pfad +@msg object transparency cannot be sampled onto highlighted characters; disable Transparency in Eyedropper Options or select the whole object Objekttransparenz kann nicht auf markierte Zeichen übertragen werden; deaktivieren Sie „Transparenz“ in den Pipettenoptionen oder wählen Sie das ganze Objekt aus +@msg object {_1} can't be set as point type Objekt {_1} kann nicht als Punkttext festgelegt werden +@msg object {_1} has lost its file's contents: relink it Objekt {_1} hat den Inhalt seiner Datei verloren: Verknüpfen Sie es neu +@msg object {_1} has no path Objekt {_1} hat keinen Pfad +@msg object {_1} has no size Objekt {_1} hat keine Größe +@msg object {_1} is hidden or locked, or on a locked artboard Objekt {_1} ist ausgeblendet oder gesperrt oder liegt auf einer gesperrten Zeichenfläche +@msg object {_1} is not a placed document Objekt {_1} ist kein platziertes Dokument +@msg object {_1} is not an Intertwine group Objekt {_1} ist keine Verflechtungsgruppe +@msg object {_1} is not an image Objekt {_1} ist kein Bild +@msg object {_1} is not an image or a linked file Objekt {_1} ist kein Bild und keine verknüpfte Datei +@msg object {_1} is not an image or a placed document Objekt {_1} ist kein Bild und kein platziertes Dokument +@msg object {_1} is not in this Intertwine group Objekt {_1} ist nicht in dieser Verflechtungsgruppe +@msg object {_1} needs a 3D effect at index {index} Objekt {_1} braucht einen 3D-Effekt bei Index {index} +@msg object {id} has anchors 0–{_1} for `corners`, not {past} Objekt {id} hat die Ankerpunkte 0–{_1} für `corners`, nicht {past} +@msg object {id} has {n} anchors, so no anchor {past} Objekt {id} hat {n} Ankerpunkte, also keinen Ankerpunkt {past} +@msg objects must be none, move or copy objects muss none, move oder copy sein +@msg objects were left out Objekte wurden ausgelassen +@msg offset out of range Offset außerhalb des gültigen Bereichs +@msg on must be true or false on muss true oder false sein +@msg only groups, layers, type and images have a Contents (Characters) row nur Gruppen, Ebenen, Text und Bilder haben eine Zeile „Inhalt (Zeichen)“ +@msg only interior mesh lines can be deleted nur innere Gitterlinien können gelöscht werden +@msg only solid colours can be added (not gradients or patterns) nur Vollfarben können hinzugefügt werden (keine Verläufe oder Muster) +@msg only solid-colour swatches can be spot colours nur Vollfarbfelder können Volltonfarben sein +@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 nur die eingebettete Vorschau dieser Affinity-Datei konnte gelesen werden: Verwenden Sie Datei › Öffnen, um sie mit ihrer Warnung zu sehen, oder exportieren Sie zuerst SVG oder PDF aus Affinity +@msg only this Affinity file's embedded preview could be read; use File › Open to see it with its warning nur die eingebettete Vorschau dieser Affinity-Datei konnte gelesen werden; verwenden Sie Datei › Öffnen, um sie mit ihrer Warnung zu sehen +@msg onto, above, below, icon and a column place belong to window.panel.float and window.panel.dock onto, above, below, icon und ein Spaltenplatz gehören zu window.panel.float und window.panel.dock +@msg onto, above, below, icon and a column place take no anchor, side, zone or before onto, above, below, icon und ein Spaltenplatz nehmen kein anchor, side, zone oder before +@msg onto, above, below, icon and a column place take no x, y, width or height onto, above, below, icon und ein Spaltenplatz nehmen kein x, y, width oder height +@msg open a document to look for its linked files öffnen Sie ein Dokument, um nach seinen verknüpften Dateien zu suchen +@msg options must be an object (see file.formatOptions) options muss ein Objekt sein (siehe file.formatOptions) +@msg order must be leftToRight or rightToLeft order muss leftToRight oder rightToLeft sein +@msg orientation must be portrait or landscape, not `{o}` orientation muss portrait oder landscape sein, nicht `{o}` +@msg outlineText {v}: true or false outlineText {v}: true oder false +@msg output intent profile: {e} Profil der Ausgabemethode: {e} +@msg output.destination: no RGB or CMYK profile is called “{name}” output.destination: Kein RGB- oder CMYK-Profil heißt „{name}“ +@msg output.destination: {_1} files are CMYK, and “{name}” is an RGB profile output.destination: {_1}-Dateien sind CMYK, und „{name}“ ist ein RGB-Profil +@msg output.destination: {e} output.destination: {e} +@msg output.inks: no ink is set to print output.inks: Keine Druckfarbe ist zum Drucken eingestellt +@msg output.outputIntent: no profile is called “{name}”, and a {std} output intent embeds its profile output.outputIntent: Kein Profil heißt „{name}“, und eine {std}-Ausgabemethode bettet ihr Profil ein +@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: Kein Profil heißt „{name}“: Eine {std}-Ausgabemethode bettet ein Profil ein oder nennt eine registrierte Druckbedingung (outputConditionId und registry) +@msg output.outputIntent: {std} files print in CMYK, and “{name}” is an RGB profile output.outputIntent: {std}-Dateien drucken in CMYK, und „{name}“ ist ein RGB-Profil +@msg output.outputIntent: “{name}” is a grey profile, which can't be a {std} output intent output.outputIntent: „{name}“ ist ein Graustufenprofil, das keine {std}-Ausgabemethode sein kann +@msg overlap: {o} pt is half a tile or more overlap: {o} pt ist eine halbe Kachel oder mehr +@msg overlapping Shaper paths must be in the same layer or group überlappende Shaper-Pfade müssen in derselben Ebene oder Gruppe liegen +@msg overlapping edits überlappende Bearbeitungen +@msg page art conflicts with image data for {key} die Seitengrafik widerspricht den Bilddaten für {key} +@msg page art conflicts with pattern {name} die Seitengrafik widerspricht dem Muster {name} +@msg page art conflicts with swatch {name} die Seitengrafik widerspricht dem Farbfeld {name} +@msg page art is missing image data for {key} der Seitengrafik fehlen die Bilddaten für {key} +@msg page art is missing pattern {name} der Seitengrafik fehlt das Muster {name} +@msg page art is missing swatch {name} der Seitengrafik fehlt das Farbfeld {name} +@msg page must be a whole number from 1 page muss eine ganze Zahl ab 1 sein +@msg page thumbnail: {e} Seitenminiatur: {e} +@msg page {page} of the PDF has no objects Seite {page} des PDF hat keine Objekte +@msg page {page}: `{file}` has {n} {artboards} Seite {page}: `{file}` hat {n} {artboards} +@msg page {page}: `{name}` has {n} {pages} Seite {page}: `{name}` hat {n} {pages} +@msg pages must be a range such as "2-3" or a page number, not {v} pages muss ein Bereich wie „2-3“ oder eine Seitenzahl sein, nicht {v} +@msg pages: {e} pages: {e} +@msg panel position must be finite die Bedienfeldposition muss endlich sein +@msg parameters must be an object parameters muss ein Objekt sein +@msg params must be an object params muss ein Objekt sein +@msg passwords can have up to {MAX_PASSWORD} bytes Kennwörter können bis zu {MAX_PASSWORD} Byte haben +@msg path has no open end der Pfad hat kein offenes Ende +@msg paths must be strings paths müssen Strings sein +@msg pdfCompatible: false needs every artboard to keep the editable artwork pdfCompatible: false braucht jede Zeichenfläche, um die bearbeitbare Grafik zu behalten +@msg pdfs `{m}`: single or multiple pdfs `{m}`: single oder multiple +@msg pickColorFrom is "artwork" or "swatches", not {v} pickColorFrom ist „artwork“ oder „swatches“, nicht {v} +@msg pins and moved must be non-empty lists of the same length pins und moved müssen nicht leere Listen gleicher Länge sein +@msg pins must be [[x,y]…] pins muss [[x,y]…] sein +@msg pins must be finite pins müssen endlich sein +@msg place is above, below or inside, not `{other}` place ist above, below oder inside, nicht `{other}` +@msg plane must be left|right|ground plane muss left|right|ground sein +@msg plane must be left|right|ground|none plane muss left|right|ground|none sein +@msg point is outside the mesh der Punkt liegt außerhalb des Gitters +@msg points must be [t, left, right] with non-negative widths points muss [t, left, right] mit nicht negativen Breiten sein +@msg points must be a non-empty [[x,y,pressure?]…] list points muss eine nicht leere Liste [[x,y,pressure?]…] sein +@msg points must be a non-empty list of finite [x,y] points muss eine nicht leere Liste endlicher [x,y] sein +@msg points must be an array or null points muss ein Array oder null sein +@msg points must be finite points müssen endlich sein +@msg points must be finite [x,y] pairs points müssen endliche [x,y]-Paare sein +@msg points must contain 1–4096 coordinates points muss 1–4096 Koordinaten enthalten +@msg position must be normal, superscript or subscript position muss normal, superscript oder subscript sein +@msg position must be topLeft, topRight, bottomLeft or bottomRight position muss topLeft, topRight, bottomLeft oder bottomRight sein +@msg positions must be between 0 and 100000 pt positions müssen zwischen 0 und 100000 pt liegen +@msg ppi must be a positive number, not {ppi} ppi muss eine positive Zahl sein, nicht {ppi} +@msg ppi must be between 1 and 2400 ppi muss zwischen 1 und 2400 liegen +@msg preserve `{v}` is not transforms|bounds|fileDimensions|fit|fill preserve `{v}` ist nicht transforms|bounds|fileDimensions|fit|fill +@msg preserveShape must be clippingMask|transparency preserveShape muss clippingMask|transparency sein +@msg preset `{id}`: mobile or density Vorgabe `{id}`: mobile oder density +@msg preset must be standard, diffuse, topLeft or right preset muss standard, diffuse, topLeft oder right sein +@msg preset must be the name of a print preset preset muss der Name einer Druckvorgabe sein +@msg previewMode must be default, pixel or overprint previewMode muss default, pixel oder overprint sein +@msg print settings: {e} Druckeinstellungen: {e} +@msg procedures nest too deeply Prozeduren sind zu tief verschachtelt +@msg procedures nested too deeply Prozeduren zu tief verschachtelt +@msg quotes must be {double?, single?} quotes muss {double?, single?} sein +@msg quotes.{k} must be an opening and a closing quote, e.g. "“”" quotes.{k} muss ein öffnendes und ein schließendes Anführungszeichen sein, z. B. "“”" +@msg rasterEffectsPpi must be 1–2400 (72 screen, 150 medium, 300 high) rasterEffectsPpi muss 1–2400 sein (72 Bildschirm, 150 mittel, 300 hoch) +@msg rating must be a whole number from 0 to {MAX_RATING} rating muss eine ganze Zahl von 0 bis {MAX_RATING} sein +@msg rect and {k} contradict each other: give the area or the artboards rect und {k} widersprechen sich: Geben Sie den Bereich oder die Zeichenflächen an +@msg rect applies to raster formats, not {_1} rect gilt für Rasterformate, nicht für {_1} +@msg rect is [x, y, width, height] in points rect ist [x, y, width, height] in Punkt +@msg rect must be [x, y, width, height] rect muss [x, y, width, height] sein +@msg rect must be [x, y, width, height] with a positive width and height rect muss [x, y, width, height] mit positiver Breite und Höhe sein +@msg rect needs finite numbers and a positive width and height rect braucht endliche Zahlen und eine positive Breite und Höhe +@msg redraw needs id redraw braucht id +@msg redraw within must be a finite distance of 0 or more redraw within muss ein endlicher Abstand von 0 oder größer sein +@msg registration must be 0..8 registration muss 0..8 sein +@msg release a layer or a group wandeln Sie eine Ebene oder eine Gruppe zurück +@msg replace keeps the replaced object's place and transform: drop template, at and rect replace behält Platz und Transformation des ersetzten Objekts: Lassen Sie template, at und rect weg +@msg replace: select the one object to replace replace: Wählen Sie das eine zu ersetzende Objekt aus +@msg resolution must be a number or screen|medium|high resolution muss eine Zahl oder screen|medium|high sein +@msg resolution must be between 1 and 2400 ppi resolution muss zwischen 1 und 2400 ppi liegen +@msg rotation must be a number of degrees rotation muss eine Gradzahl sein +@msg rotation must be finite rotation muss endlich sein +@msg rows and cols must be 1..50 rows und cols müssen 1..50 sein +@msg rows and columns must be 1..500 rows und columns müssen 1..500 sein +@msg rows of objects whose appearances differ can't be reordered together Zeilen von Objekten mit unterschiedlichem Aussehen können nicht zusammen neu angeordnet werden +@msg save the document first: Package collects a saved document speichern Sie zuerst das Dokument: Verpacken sammelt ein gespeichertes Dokument +@msg save the document first: copy puts the file in its folder speichern Sie zuerst das Dokument: copy legt die Datei in seinen Ordner +@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}`: ein Faktor wie 2x, eine Breite (100w), eine Höhe (100h) oder eine Auflösung (72ppi) +@msg scale must be a number above 0, at most 64 scale muss eine Zahl über 0 sein, höchstens 64 +@msg scale must be non-zero scale darf nicht null sein +@msg scope must be selection or all (got {other}) scope muss selection oder all sein (erhalten: {other}) +@msg scribble is too long die Kritzelei ist zu lang +@msg segment does not exist das Segment existiert nicht +@msg select a Shaper group wählen Sie eine Shaper-Gruppe aus +@msg select a blend to set easing or color shift wählen Sie eine Angleichung aus, um Beschleunigung oder Farbverschiebung festzulegen +@msg select a compound shape wählen Sie eine zusammengesetzte Form aus +@msg select a graph wählen Sie ein Diagramm aus +@msg select a group or layer with visible, unlocked members wählen Sie eine Gruppe oder Ebene mit sichtbaren, nicht gesperrten Elementen aus +@msg select a layer wählen Sie eine Ebene aus +@msg select a path wählen Sie einen Pfad aus +@msg select a text object wählen Sie ein Textobjekt aus +@msg select an image (or an Image Trace group) wählen Sie ein Bild (oder eine Bildnachzeichner-Gruppe) aus +@msg select an image, or pass id wählen Sie ein Bild aus, oder übergeben Sie id +@msg select an object (or pass `id`) wählen Sie ein Objekt aus (oder übergeben Sie `id`) +@msg select an object with a freeform gradient wählen Sie ein Objekt mit einem Freiformverlauf aus +@msg select an object with an opacity mask wählen Sie ein Objekt mit einer Deckkraftmaske aus +@msg select area type wählen Sie Flächentext aus +@msg select area type (text in a frame) wählen Sie Flächentext (Text in einem Rahmen) aus +@msg select area type and closed paths (at least one area type object) wählen Sie Flächentext und geschlossene Pfade aus (mindestens ein Flächentextobjekt) +@msg select area type whose first line can fit its frame wählen Sie Flächentext aus, dessen erste Zeile in seinen Rahmen passt +@msg select artwork to add its colours wählen Sie Grafik aus, um ihre Farben hinzuzufügen +@msg select artwork to make a group of its colours wählen Sie Grafik aus, um aus ihren Farben eine Gruppe zu erstellen +@msg select artwork to recolour, or give a colour `group` wählen Sie die neu zu färbende Grafik aus, oder geben Sie eine Farbgruppe in `group` an +@msg select exactly two path ends to join wählen Sie genau zwei Pfadenden zum Zusammenfügen aus +@msg select members of a compound shape wählen Sie Elemente einer zusammengesetzten Form aus +@msg select objects or give ids wählen Sie Objekte aus, oder geben Sie ids an +@msg select objects to align to the guide wählen Sie Objekte aus, die an der Hilfslinie ausgerichtet werden sollen +@msg select one Intertwine group wählen Sie eine Verflechtungsgruppe aus +@msg select one image or pass id wählen Sie ein Bild aus, oder übergeben Sie id +@msg select paths wählen Sie Pfade aus +@msg select paths or compound paths wählen Sie Pfade oder zusammengesetzte Pfade aus +@msg select rows or objects to collect wählen Sie Zeilen oder Objekte zum Sammeln aus +@msg select something to export wählen Sie etwas zum Exportieren aus +@msg select something to export (or turn off selectedOnly) wählen Sie etwas zum Exportieren aus (oder deaktivieren Sie selectedOnly) +@msg select text to redefine the style from wählen Sie den Text aus, aus dem das Format neu definiert werden soll +@msg select the art and, on top of it, the mask object wählen Sie das Bildmaterial und darüber das Maskenobjekt aus +@msg select the art for the design wählen Sie das Bildmaterial für das Design aus +@msg select the art for the pattern tile wählen Sie das Bildmaterial für die Musterkachel aus +@msg select the art to add first wählen Sie zuerst das hinzuzufügende Bildmaterial aus +@msg select the art to collect (or give its ids) wählen Sie das zu sammelnde Bildmaterial aus (oder geben Sie seine IDs an) +@msg select the content and a path on top wählen Sie den Inhalt und einen darüberliegenden Pfad aus +@msg select the images to relink, or pass ids wählen Sie die neu zu verknüpfenden Bilder aus, oder übergeben Sie ids +@msg select the images, or pass ids wählen Sie die Bilder aus, oder übergeben Sie ids +@msg select the linked images or placed documents, or pass ids wählen Sie die verknüpften Bilder oder platzierten Dokumente aus, oder übergeben Sie ids +@msg select the object to copy from wählen Sie das Objekt aus, aus dem kopiert werden soll +@msg select the objects (or give their ids) wählen Sie die Objekte aus (oder geben Sie ihre IDs an) +@msg select the objects to attach to the path wählen Sie die Objekte aus, die an den Pfad angehängt werden sollen +@msg select the objects to bind wählen Sie die zu bindenden Objekte aus +@msg select two or more objects to intertwine wählen Sie zwei oder mehr Objekte zum Verflechten aus +@msg select two or more slices to combine wählen Sie zwei oder mehr Slices zum Kombinieren aus +@msg select type on a path wählen Sie Pfadtext aus +@msg selection `{m}`: one or each selection `{m}`: one oder each +@msg settings is an object of document.exportForScreens params settings ist ein Objekt aus Parametern von document.exportForScreens +@msg settings must be an object settings muss ein Objekt sein +@msg show `{show}`: all, missing, modified or embedded show `{show}`: all, missing, modified oder embedded +@msg show must be true or false show muss true oder false sein +@msg size must be "small", "medium", "large" or a number of points over 0 up to {_1} size muss „small“, „medium“, „large“ oder eine Anzahl Punkt über 0 bis {_1} sein +@msg size must be positive size muss positiv sein +@msg sizeMode is "fixed" or "pressure", not `{other}` sizeMode ist „fixed“ oder „pressure“, nicht `{other}` +@msg sizeMode is "fixed" or "pressure", not {other} sizeMode ist „fixed“ oder „pressure“, nicht {other} +@msg sizeVariation must be 0 or more sizeVariation muss 0 oder größer sein +@msg smallCaps must be true or false smallCaps muss true oder false sein +@msg sort `{x}`: name, kind or status sort `{x}`: name, kind oder status +@msg source paths are too large for Shaper die Quellpfade sind zu groß für Shaper +@msg spot, mode and group apply to solid colours only spot, mode und group gelten nur für Vollfarben +@msg status `{s}`: linked, embedded, missing or modified status `{s}`: linked, embedded, missing oder modified +@msg steps must be 1..1000 steps muss 1..1000 sein +@msg steps must be 1..{MAX_STEPS} steps muss 1..{MAX_STEPS} sein +@msg stop {i}: `plates` must be four numbers from 0 to 1 (C, M, Y, K) Farbstopp {i}: `plates` muss aus vier Zahlen von 0 bis 1 bestehen (C, M, Y, K) +@msg stopped before the archive was finished angehalten, bevor das Archiv fertig war +@msg strokeMode must be within|along|across, got {m} strokeMode muss within|along|across sein, erhalten: {m} +@msg strokeOptions must be an object of stroke.set options strokeOptions muss ein Objekt aus Optionen von stroke.set sein +@msg strokeWidth must be a positive number of pixels strokeWidth muss eine positive Anzahl Pixel sein +@msg subpath {si} is not an open subpath Teilpfad {si} ist kein offener Teilpfad +@msg subpaths required when joining the same path subpaths ist beim Zusammenfügen desselben Pfads erforderlich +@msg swatchConflict must be "merge" or "add", not {v} swatchConflict muss „merge“ oder „add“ sein, nicht {v} +@msg text must be {characterSet?, platform?, removeLineReturns?, removeParagraphReturns?, replaceSpaces?} text muss {characterSet?, platform?, removeLineReturns?, removeParagraphReturns?, replaceSpaces?} sein +@msg text transform is collapsed or unrepresentable die Texttransformation ist zusammengefallen oder nicht darstellbar +@msg text.replaceSpaces must be a whole number from 2 to 100 text.replaceSpaces muss eine ganze Zahl von 2 bis 100 sein +@msg text.{key} `{v}`: use "{_1}" or "{_2}" text.{key} `{v}`: Verwenden Sie „{_1}“ oder „{_2}“ +@msg textureImage: give {path} or {name, dataBase64} of an image file, or a texture as effect.list gives it textureImage: Geben Sie {path} oder {name, dataBase64} einer Bilddatei an, oder eine Textur, wie effect.list sie angibt +@msg textureImage: {e} textureImage: {e} +@msg that makes more than {MAX_PIECES} slices das ergibt mehr als {MAX_PIECES} Slices +@msg that object is not in the overlap dieses Objekt liegt nicht in der Überlappung +@msg the Affinity document has no page with a size das Affinity-Dokument hat keine Seite mit einer Größe +@msg the CMYK image couldn't be written das CMYK-Bild konnte nicht geschrieben werden +@msg the PDF security settings were saved without their passwords: give openPassword or permissionsPassword again, or security without passwordRequired to export unprotected die PDF-Sicherheitseinstellungen wurden ohne ihre Kennwörter gespeichert: Geben Sie openPassword oder permissionsPassword erneut an, oder security ohne passwordRequired, um ungeschützt zu exportieren +@msg the RTF file has too many paragraphs or formatting changes to place as type (at most {MAX_PARAGRAPHS} paragraphs and {MAX_RUNS} runs) die RTF-Datei hat zu viele Absätze oder Formatierungswechsel, um als Text platziert zu werden (höchstens {MAX_PARAGRAPHS} Absätze und {MAX_RUNS} Textläufe) +@msg the RTF file is too large to place as type (at most {_1} MB) die RTF-Datei ist zu groß, um als Text platziert zu werden (höchstens {_1} MB) +@msg the SVG expands to more than {0} nested SVG images das SVG wird zu mehr als {0} verschachtelten SVG-Bildern erweitert +@msg the SVG expands to more than {0} objects das SVG wird zu mehr als {0} Objekten erweitert +@msg the SVG has no drawable objects das SVG hat keine zeichenbaren Objekte +@msg the SVGZ file unpacks to more than {_1} MB die SVGZ-Datei wird auf mehr als {_1} MB entpackt +@msg the Word document has more than {MAX_PAGES} pages of properties, too many to place das Word-Dokument hat mehr als {MAX_PAGES} Seiten an Eigenschaften, zu viele zum Platzieren +@msg the Word document has more than {MAX_RUNS} runs of character or paragraph properties, too many to place das Word-Dokument hat mehr als {MAX_RUNS} Läufe mit Zeichen- oder Absatzeigenschaften, zu viele zum Platzieren +@msg the Word document has more than {_1} MB of formatting properties, too many to place das Word-Dokument hat mehr als {_1} MB an Formatierungseigenschaften, zu viele zum Platzieren +@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) das Word-Dokument hat zu viel Text oder Formatierung, um als Text platziert zu werden (höchstens {MAX_PARAGRAPHS} Absätze, {MAX_RUNS} Textläufe und {_1} MB Text) +@msg the Word document has too much text to place as type (at most {_1} MB) das Word-Dokument hat zu viel Text, um als Text platziert zu werden (höchstens {_1} MB) +@msg the Word document is encrypted or protected with a password; save it without one to place it das Word-Dokument ist verschlüsselt oder mit einem Kennwort geschützt; speichern Sie es ohne Kennwort, um es zu platzieren +@msg the Word document is too large to place (its {name} stream holds more than {_1} MB) das Word-Dokument ist zu groß zum Platzieren (sein Datenstrom {name} enthält mehr als {_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 das Word-Dokument wurde zuletzt mit Schnellspeicherung gespeichert, die VectorCraft nicht liest; speichern Sie es erneut mit deaktivierter Schnellspeicherung +@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 das Word-Dokument wurde mit Word 6.0 oder Word 95 gespeichert, das VectorCraft nicht liest; speichern Sie es als Word-97-2003-Dokument +@msg the Word document's text is made of more than {MAX_PIECES} pieces, too many to place der Text des Word-Dokuments besteht aus mehr als {MAX_PIECES} Teilen, zu viele zum Platzieren +@msg the action set ids are not a list die IDs der Aktionssätze sind keine Liste +@msg the active paint is not a gradient die aktive Farbe ist kein Verlauf +@msg the artboards don't fit on the canvas: fewer artboards, less spacing or a smaller size die Zeichenflächen passen nicht auf die Arbeitsfläche: weniger Zeichenflächen, weniger Abstand oder eine kleinere Größe +@msg the character's size can't be measured die Größe des Zeichens kann nicht gemessen werden +@msg the compound shape is too complex to expand die zusammengesetzte Form ist zu komplex zum Umwandeln +@msg the container of the listed objects is gone der Container der aufgelisteten Objekte ist nicht mehr vorhanden +@msg the copy `{user_id}` can't be read as a library die Kopie `{user_id}` kann nicht als Bibliothek gelesen werden +@msg the copy would leave the canvas die Kopie würde die Arbeitsfläche verlassen +@msg the cut cannot be made on this art: {e} der Schnitt kann an diesem Bildmaterial nicht ausgeführt werden: {e} +@msg the data isn't an EMF or WMF picture die Daten sind kein EMF- oder WMF-Bild +@msg the data isn't an EPS or PostScript picture die Daten sind kein EPS- oder PostScript-Bild +@msg the deferred paste is no longer available das verzögerte Einfügen ist nicht mehr verfügbar +@msg the design is too big to draw at every data point of this graph das Design ist zu groß, um an jedem Datenpunkt dieses Diagramms gezeichnet zu werden +@msg the design is too big to draw in every column of this graph das Design ist zu groß, um in jeder Säule dieses Diagramms gezeichnet zu werden +@msg the dialog it was opened over is no longer open das Dialogfeld, über dem es geöffnet wurde, ist nicht mehr geöffnet +@msg the dictionary stack overflowed der Wörterbuchstapel ist übergelaufen +@msg the editing data's hash has no place der Hash der Bearbeitungsdaten hat keinen Platz +@msg the entities the document declares expand to too much text die vom Dokument deklarierten Entitäten werden zu zu viel Text erweitert +@msg the envelope's content has no bounds der Inhalt der Verzerrungshülle hat keine Begrenzungen +@msg the envelope's mesh is malformed das Gitter der Verzerrungshülle ist fehlerhaft +@msg the file holds no `variableSets` element die Datei enthält kein `variableSets`-Element +@msg the file is larger than {_1} MB die Datei ist größer als {_1} MB +@msg the file is too large die Datei ist zu groß +@msg the file was updated or encrypted already die Datei wurde bereits aktualisiert oder verschlüsselt +@msg the first character of a line has no character before it to kern: it moves only up and down (dy) das erste Zeichen einer Zeile hat kein Zeichen davor zum Unterschneiden: Es bewegt sich nur nach oben und unten (dy) +@msg the font {_1} is not available die Schrift {_1} ist nicht verfügbar +@msg the format has no file extension das Format hat keine Dateierweiterung +@msg the graphic can't be read: {e} die Grafik kann nicht gelesen werden: {e} +@msg the graphic is empty die Grafik ist leer +@msg the graphic's data is damaged die Daten der Grafik sind beschädigt +@msg the graphic's document has no artboard das Dokument der Grafik hat keine Zeichenfläche +@msg the header `{cell}` for `{name}` would read back as the variable `{other}`: rename one of them die Kopfzeile `{cell}` für `{name}` würde als Variable `{other}` zurückgelesen: Benennen Sie eine von ihnen um +@msg the image `{path}` isn't followed: {r} dem Bild `{path}` wird nicht gefolgt: {r} +@msg the image has no embedded pixels das Bild hat keine eingebetteten Pixel +@msg the image is empty das Bild ist leer +@msg the image is fully transparent das Bild ist vollständig transparent +@msg the image is not on an artboard; pass rect das Bild liegt auf keiner Zeichenfläche; übergeben Sie rect +@msg the image's pixels are missing die Pixel des Bilds fehlen +@msg the image's pixels aren't in the document die Pixel des Bilds sind nicht im Dokument +@msg the image's pixels can't be read: {e} die Pixel des Bilds können nicht gelesen werden: {e} +@msg the image's transparency doesn't fit it die Transparenz des Bilds passt nicht zu ihm +@msg the job would print more than {MAX_PAGES} pages der Auftrag würde mehr als {MAX_PAGES} Seiten drucken +@msg the job would print more than {_1} pages der Auftrag würde mehr als {_1} Seiten drucken +@msg the layers hold no art to mask die Ebenen enthalten keine Grafik zum Maskieren +@msg the layout moved das Layout hat sich verschoben +@msg the library has a group `{name}` already die Bibliothek hat bereits eine Gruppe `{name}` +@msg the library has no group `{name}` die Bibliothek hat keine Gruppe `{name}` +@msg the library has no item `{item}` die Bibliothek hat kein Element `{item}` +@msg the library has no item `{other}` die Bibliothek hat kein Element `{other}` +@msg the library holds {MAX_GROUPS} groups already die Bibliothek enthält bereits {MAX_GROUPS} Gruppen +@msg the library holds {MAX_ITEMS} items of this kind already die Bibliothek enthält bereits {MAX_ITEMS} Elemente dieser Art +@msg the name is empty der Name ist leer +@msg the object has no fill, stroke or effect das Objekt hat keine Fläche, Kontur oder Effekt +@msg the object has no graphic style (pass `name`) das Objekt hat keinen Grafikstil (übergeben Sie `name`) +@msg the objects don't match the plan die Objekte passen nicht zum Plan +@msg the open and permissions passwords must differ das Kennwort zum Öffnen und das Berechtigungskennwort müssen sich unterscheiden +@msg the operand stack overflowed der Operandenstapel ist übergelaufen +@msg the page has no area die Seite hat keine Fläche +@msg the page's type in an unread object's box isn't in its page art der Text der Seite im Rahmen eines nicht gelesenen Objekts ist nicht in seiner Seitengrafik +@msg the paint behind the proxy is not a gradient die Farbe hinter dem Proxy ist kein Verlauf +@msg the parent must be a layer or sublayer das übergeordnete Element muss eine Ebene oder Unterebene sein +@msg the path can't be extended der Pfad kann nicht verlängert werden +@msg the path can't be inside one of the objects to attach der Pfad darf nicht in einem der anzuhängenden Objekte liegen +@msg the path has no segments to measure der Pfad hat keine Segmente zum Messen +@msg the path is empty der Pfad ist leer +@msg the path is too short to shorten that much der Pfad ist zu kurz, um ihn so stark zu kürzen +@msg the picture has no objects das Bild hat keine Objekte +@msg the program runs too long das Programm läuft zu lange +@msg the program uses too much memory das Programm verwendet zu viel Speicher +@msg the rectangle must have a width and a height das Rechteck muss eine Breite und eine Höhe haben +@msg the rectangle needs a size and must lie on the canvas das Rechteck braucht eine Größe und muss auf der Arbeitsfläche liegen +@msg the selected art has no width das ausgewählte Bildmaterial hat keine Breite +@msg the selection draws nothing die Auswahl zeichnet nichts +@msg the selection has no area die Auswahl hat keine Fläche +@msg the selection has no art to export die Auswahl hat keine Grafik zum Exportieren +@msg the selection has no bounds die Auswahl hat keine Begrenzungen +@msg the selection has no graphic style die Auswahl hat keinen Grafikstil +@msg the slice would leave the canvas der Slice würde die Arbeitsfläche verlassen +@msg the spot colour couldn't be added die Volltonfarbe konnte nicht hinzugefügt werden +@msg the subpath has no ends to extend (it is closed or a single point) der Teilpfad hat keine Enden zum Verlängern (er ist geschlossen oder ein einzelner Punkt) +@msg the symbols are nested too deeply to expand their instances die Symbole sind zu tief verschachtelt, um ihre Instanzen umzuwandeln +@msg the text file is too large to place as type (at most {_1} MB) die Textdatei ist zu groß, um als Text platziert zu werden (höchstens {_1} MB) +@msg the text is empty der Text ist leer +@msg the text is over {_1} MB der Text ist größer als {_1} MB +@msg the top object must be a path das oberste Objekt muss ein Pfad sein +@msg the trace was cancelled das Nachzeichnen wurde abgebrochen +@msg the trailer has no size der Trailer hat keine Größe +@msg the type object is flattened to nothing das Textobjekt wird zu nichts reduziert +@msg the value of axis `{tag}` must be a number or null der Wert der Achse `{tag}` muss eine Zahl oder null sein +@msg the variable library declares more than {MAX_LIBRARY_DATASETS} data sets die Variablenbibliothek deklariert mehr als {MAX_LIBRARY_DATASETS} Datensätze +@msg the variable library declares more than {MAX_LIBRARY_ENTITIES} entities die Variablenbibliothek deklariert mehr als {MAX_LIBRARY_ENTITIES} Entitäten +@msg the variable library declares more than {MAX_LIBRARY_VARIABLES} variables die Variablenbibliothek deklariert mehr als {MAX_LIBRARY_VARIABLES} Variablen +@msg the variable library declares no variables die Variablenbibliothek deklariert keine Variablen +@msg the variable library is larger than {MAX_LIBRARY_BYTES} bytes die Variablenbibliothek ist größer als {MAX_LIBRARY_BYTES} Byte +@msg the variable library is not UTF-8 die Variablenbibliothek ist nicht UTF-8 +@msg the variable library is not well-formed XML: {e} die Variablenbibliothek ist kein wohlgeformtes XML: {e} +@msg the variable library nests elements more than {MAX_LIBRARY_NESTING} deep die Variablenbibliothek verschachtelt Elemente mehr als {MAX_LIBRARY_NESTING} Ebenen tief +@msg the variable library's document type declaration holds something other than entities of plain text die Dokumenttypdeklaration der Variablenbibliothek enthält etwas anderes als Entitäten aus reinem Text +@msg the variable library's entities expand to more than {MAX_LIBRARY_BYTES} bytes die Entitäten der Variablenbibliothek werden zu mehr als {MAX_LIBRARY_BYTES} Byte erweitert +@msg the window has no dock das Fenster hat kein Dock +@msg the written PDF has no catalog das geschriebene PDF hat keinen Katalog +@msg the written PDF has no cross-reference table das geschriebene PDF hat keine Querverweistabelle +@msg the written PDF's metadata can't be edited die Metadaten des geschriebenen PDF können nicht bearbeitet werden +@msg there are no colour swatches to draw es gibt keine Farbfelder zum Zeichnen +@msg there is already a design named `{name}` es gibt bereits ein Design namens `{name}` +@msg there is already a design named `{to}` es gibt bereits ein Design namens `{to}` +@msg there is already a toolbar named `{name}` es gibt bereits eine Werkzeugleiste namens `{name}` +@msg there is already a toolbar named `{to}` es gibt bereits eine Werkzeugleiste namens `{to}` +@msg there is no library: create one first es gibt keine Bibliothek: Erstellen Sie zuerst eine +@msg these paths are too degenerate for Shaper diese Pfade sind zu entartet für Shaper +@msg thumbnail: no page {v} (the {what} has {count}) Miniatur: keine Seite {v} ({what} hat {count}) +@msg tileRange of {what}: {e} tileRange von {what}: {e} +@msg title can't be empty title darf nicht leer sein +@msg to (the new name) is required to (der neue Name) ist erforderlich +@msg to must be fill or stroke to muss fill oder stroke sein +@msg to must be smooth or corner to muss smooth oder corner sein +@msg too large for a zip archive: package to a folder zu groß für ein ZIP-Archiv: Verpacken Sie in einen Ordner +@msg too many Shaper edits zu viele Shaper-Bearbeitungen +@msg too many `points` (at most 100000) zu viele `points` (höchstens 100000) +@msg too many characters: at most {MAX_CHARS} zu viele Zeichen: höchstens {MAX_CHARS} +@msg too many dots for this size: lower the frequency zu viele Punkte für diese Größe: Verringern Sie die Frequenz +@msg too many filters over one file zu viele Filter über einer Datei +@msg too many overlapping paths for Shaper zu viele überlappende Pfade für Shaper +@msg too many saved clips zu viele gespeicherte Beschneidungen +@msg too many saved graphics states zu viele gespeicherte Grafikzustände +@msg toolbar (a toolbar's name) must be a string toolbar (der Name einer Werkzeugleiste) muss ein String sein +@msg toolbar name can't be empty der Werkzeugleistenname darf nicht leer sein +@msg transparentImages {v}: "flatten" (the default) or "omit" transparentImages {v}: „flatten“ (Standard) oder „omit“ +@msg two saved selections would be named `{_1}` zwei gespeicherte Auswahlen würden `{_1}` heißen +@msg type dimension cannot be represented with this text transform das Textmaß kann mit dieser Texttransformation nicht dargestellt werden +@msg type dimension must be finite das Textmaß muss endlich sein +@msg unexpected resources unerwartete Ressourcen +@msg unknown CMYK profile `{name}` unbekanntes CMYK-Profil `{name}` +@msg unknown File Info field `{k}` unbekanntes Dateiinformationen-Feld `{k}` +@msg unknown OpenType feature `{t}` in featureChanges (the tags `features` takes) unbekannte OpenType-Funktion `{t}` in featureChanges (die Tags, die `features` nimmt) +@msg unknown PDF preset `{name}` (presets: {_1}) unbekannte PDF-Vorgabe `{name}` (Vorgaben: {_1}) +@msg unknown Warp style unbekannter Verkrümmungsstil +@msg unknown alignToPath `{a}` unbekanntes alignToPath `{a}` +@msg unknown arrowhead {v} unbekannte Pfeilspitze {v} +@msg unknown artboardLayout key `{k}` (layout, columns, spacing, rightToLeft) unbekannter artboardLayout-Schlüssel `{k}` (layout, columns, spacing, rightToLeft) +@msg unknown attribute `{attribute}` unbekanntes Attribut `{attribute}` +@msg unknown attribute `{k}` unbekanntes Attribut `{k}` +@msg unknown axis `{other}` (vertical|horizontal|free) unbekannte Achse `{other}` (vertical|horizontal|free) +@msg unknown blend mode `{b}` unbekannte Füllmethode `{b}` +@msg unknown blend mode {b} unbekannte Füllmethode {b} +@msg unknown brush type `{o}` unbekannter Pinseltyp `{o}` +@msg unknown category `{c}` unbekannte Kategorie `{c}` +@msg unknown category `{c}` ({_1}) unbekannte Kategorie `{c}` ({_1}) +@msg unknown character `{c}` unbekanntes Zeichen `{c}` +@msg unknown character style number field unbekanntes Zahlenfeld im Zeichenformat +@msg unknown effect `{e}` unbekannter Effekt `{e}` +@msg unknown end {e} unbekanntes Ende {e} +@msg unknown flattenerPreset `{name}` (High Resolution, Medium Resolution, Low Resolution or a saved one: see flattener.presets.list) unbekanntes flattenerPreset `{name}` (High Resolution, Medium Resolution, Low Resolution oder eine gespeicherte: siehe flattener.presets.list) +@msg unknown format `{f}` (vcswatches, gpl, ase or css) unbekanntes Format `{f}` (vcswatches, gpl, ase oder css) +@msg unknown from {f} unbekanntes from {f} +@msg unknown graph type `{t}` unbekannter Diagrammtyp `{t}` +@msg unknown grid colours `{name}` (one of {_1}) unbekannte Rasterfarben `{name}` (eines von {_1}) +@msg unknown gridType `{t}` unbekannter gridType `{t}` +@msg unknown highlight `{v}` unbekannte Hervorhebung `{v}` +@msg unknown intent `{i}` unbekannte Renderpriorität `{i}` +@msg unknown issue kind {id} unbekannte Problemart {id} +@msg unknown issue kind {v} unbekannte Problemart {v} +@msg unknown knockout state {k} unbekannter Aussparungszustand {k} +@msg unknown language `{language}` (one of {_1}) unbekannte Sprache `{language}` (eine von {_1}) +@msg unknown language `{l}` (one of {_1}) unbekannte Sprache `{l}` (eine von {_1}) +@msg unknown method `{m}` ({_1}) unbekannte Methode `{m}` ({_1}) +@msg unknown mode `{other}` (gray, rgb, hsb, lab, cmyk or web) unbekannter Modus `{other}` (gray, rgb, hsb, lab, cmyk oder web) +@msg unknown mode `{other}` (mono|cmyk) unbekannter Modus `{other}` (mono|cmyk) +@msg unknown mode `{other}` (rgb|cmyk|gray|global) unbekannter Modus `{other}` (rgb|cmyk|gray|global) +@msg unknown mode `{other}` (set, add, subtract) unbekannter Modus `{other}` (set, add, subtract) +@msg unknown mode `{o}` unbekannter Modus `{o}` +@msg unknown mode {m} unbekannter Modus {m} +@msg unknown model `{other}` unbekanntes Modell `{other}` +@msg unknown model `{other}` (keep, new or document) unbekanntes Modell `{other}` (keep, new oder document) +@msg unknown op {o} unbekannte Operation {o} +@msg unknown order `{o}`: toggle, front, forward, backward or back unbekannte Reihenfolge `{o}`: toggle, front, forward, backward oder back +@msg unknown overprints `{v}` (preserve, simulate or discard) unbekanntes Überdrucken `{v}` (preserve, simulate oder discard) +@msg unknown panel unbekanntes Bedienfeld +@msg unknown pieLegend `{v}` (none, standard or wedges) unbekannte pieLegend `{v}` (none, standard oder wedges) +@msg unknown piePosition `{v}` (even, ratio or stacked) unbekannte piePosition `{v}` (even, ratio oder stacked) +@msg unknown pieSort `{v}` (none, all or first) unbekannte pieSort `{v}` (none, all oder first) +@msg unknown plate `{n}` unbekannte Platte `{n}` +@msg unknown preference `{k}` unbekannte Voreinstellung `{k}` +@msg unknown profile {id} (see stroke.widthProfile.list) unbekanntes Profil {id} (siehe stroke.widthProfile.list) +@msg unknown proof target `{t}` unbekanntes Proof-Ziel `{t}` +@msg unknown quotes key `{k}` (double, single) unbekannter quotes-Schlüssel `{k}` (double, single) +@msg unknown resolution `{v}` unbekannte Auflösung `{v}` +@msg unknown rule `{rule}` ({_1}) unbekannte Regel `{rule}` ({_1}) +@msg unknown rule `{r}` (see color.harmony) unbekannte Regel `{r}` (siehe color.harmony) +@msg unknown setting `{k}` unbekannte Einstellung `{k}` +@msg unknown shape `{v}` (circle|ellipse|square|diamond|line) unbekannte Form `{v}` (circle|ellipse|square|diamond|line) +@msg unknown spacing `{o}` (smooth|steps|distance) unbekannter Abstand `{o}` (smooth|steps|distance) +@msg unknown tileType `{t}` ({_1}) unbekannter tileType `{t}` ({_1}) +@msg unknown units unbekannte Einheiten +@msg unknown units `{u}` unbekannte Einheiten `{u}` +@msg unknown value axis `{v}` (left, right or both) unbekannte Werteachse `{v}` (left, right oder both) +@msg unknown variation `{v}` (tintsShades|warmCool|vividMuted) unbekannte Variante `{v}` (tintsShades|warmCool|vividMuted) +@msg unknown warp style `{s}` unbekannter Verkrümmungsstil `{s}` +@msg unknown {key} `{v}` (none, short or full) unbekanntes {key} `{v}` (none, short oder full) +@msg unknown {key} profile `{n}` unbekanntes {key}-Profil `{n}` +@msg unknown {k} key `{key}` (size, position) unbekannter {k}-Schlüssel `{key}` (size, position) +@msg unreadable object nicht lesbares Objekt +@msg unsafe destination file name unsicherer Zieldateiname +@msg use paths or compound paths verwenden Sie Pfade oder zusammengesetzte Pfade +@msg useArtboards must be true or false, not {v} useArtboards muss true oder false sein, nicht {v} +@msg value for `{var}` must match its variable kind der Wert für `{var}` muss zur Art seiner Variable passen +@msg variable `{_1}` is already defined Variable `{_1}` ist bereits definiert +@msg variable `{name}` has no `trait` Variable `{name}` hat kein `trait` +@msg variable `{name}` is already defined Variable `{name}` ist bereits definiert +@msg variable `{name}` is declared twice Variable `{name}` ist zweimal deklariert +@msg variable `{to}` already exists Variable `{to}` existiert bereits +@msg view is required view ist erforderlich +@msg view must be one of {_1} view muss eines der folgenden sein: {_1} +@msg warpStyle can only edit an applied Warp effect warpStyle kann nur einen angewendeten Verkrümmungseffekt bearbeiten +@msg width and height must be more than 0 and at most {limit} width und height müssen größer als 0 und höchstens {limit} sein +@msg width factors must be finite Breitenfaktoren müssen endlich sein +@msg width must be positive width muss positiv sein +@msg width out of range Breite außerhalb des gültigen Bereichs +@msg width/height must be > 0 width/height müssen > 0 sein +@msg x and y must be finite numbers x und y müssen endliche Zahlen sein +@msg x and y must be given together x und y müssen zusammen angegeben werden +@msg zone needs an anchor: dock beside a panel with anchor, or in a new dock column with side (Start or End) zone braucht einen Anker: Docken Sie mit anchor neben einem Bedienfeld an oder mit side (Start oder End) in einer neuen Dockspalte +@msg zoom must be positive zoom muss positiv sein +@msg {C}: no effect at index {list} {C}: kein Effekt bei Index {list} +@msg {DEFAULT_PRESET} is protected and stays {DEFAULT_PRESET} ist geschützt und bleibt erhalten +@msg {DEFAULT_PRESET} is protected: save the settings under another name {DEFAULT_PRESET} ist geschützt: Speichern Sie die Einstellungen unter einem anderen Namen +@msg {LABEL} produced an empty result {LABEL} hat ein leeres Ergebnis erzeugt +@msg {_1} MB of document, at most {_2} MB {_1} MB Dokument, höchstens {_2} MB +@msg {_1} MB of images, at most {_2} MB {_1} MB Bilder, höchstens {_2} MB +@msg {_1} bytes of RGBA data for a {width} × {height} image {_1} Byte RGBA-Daten für ein Bild mit {width} × {height} +@msg {_1} can't be read, or changed since it was read: relink or update it first {_1} kann nicht gelesen werden oder hat sich seit dem Lesen geändert: Verknüpfen Sie es zuerst neu oder aktualisieren Sie es +@msg {_1} can't go into {into} {_1} kann nicht in {into} eingefügt werden +@msg {_1} can't limit printing to low resolution: choose PDF 1.4 or later, or allow high-resolution printing {_1} kann das Drucken nicht auf niedrige Auflösung beschränken: Wählen Sie PDF 1.4 oder höher, oder erlauben Sie das Drucken in hoher Auflösung +@msg {_1} files can't be opened yet {_1}-Dateien können noch nicht geöffnet werden +@msg {_1} files can't be password-protected: remove the passwords or choose no standard {_1}-Dateien können nicht mit einem Kennwort geschützt werden: Entfernen Sie die Kennwörter oder wählen Sie keinen Standard +@msg {_1} files can't be {_2} {_1}-Dateien können nicht {_2} sein +@msg {_1} files can't carry editing data: turn off preserveEditing {_1}-Dateien können keine Bearbeitungsdaten enthalten: Deaktivieren Sie preserveEditing +@msg {_1} files can't have PDF layers: turn off createLayers {_1}-Dateien können keine PDF-Ebenen haben: Deaktivieren Sie createLayers +@msg {_1} files need a creation date, and this platform has no clock to give one {_1}-Dateien brauchen ein Erstellungsdatum, und diese Plattform hat keine Uhr, die eines liefert +@msg {_1} isn't a path {_1} ist kein Pfad +@msg {_1} options: {e} {_1}-Optionen: {e} +@msg {_1} paints nothing to rasterize {_1} malt nichts, das gerastert werden kann +@msg {_1} passwords can have up to {MAX_LEGACY_PASSWORD} ASCII characters (PDF 1.7 and later take any) {_1}-Kennwörter können bis zu {MAX_LEGACY_PASSWORD} ASCII-Zeichen haben (PDF 1.7 und höher nehmen beliebige) +@msg {_1} with linked images or an image map writes several files: use document.export {_1} mit verknüpften Bildern oder einer Imagemap schreibt mehrere Dateien: Verwenden Sie document.export +@msg {_1} writes one file per artboard here: use document.export, or name one artboard {_1} schreibt hier eine Datei pro Zeichenfläche: Verwenden Sie document.export, oder nennen Sie eine Zeichenfläche +@msg {_1} × {_2} pixels is too large for {_3} (at most {max} pixels a side): lower the scale {_1} × {_2} Pixel ist zu groß für {_3} (höchstens {max} Pixel pro Seite): Verringern Sie die Skalierung +@msg {_1}: belongs to another account {_1}: gehört einem anderen Konto +@msg {_1}: not a folder of its own {_1}: kein eigener Ordner +@msg {_1}: not a plain file {_1}: keine normale Datei +@msg {dest}: {e} {dest}: {e} +@msg {doc} has no folder {doc} hat keinen Ordner +@msg {file} isn't linked {file} ist nicht verknüpft +@msg {folder} is not a package folder {folder} ist kein Paketordner +@msg {folder}: more than {max} files and folders {folder}: mehr als {max} Dateien und Ordner +@msg {folder}: {e} {folder}: {e} +@msg {g} is not a Live Paint group {g} ist keine Gruppe „Interaktiv malen“ +@msg {id} is not a slice {id} ist kein Slice +@msg {key} must be 0–{MAX_ADD_AROUND} pt {key} muss 0–{MAX_ADD_AROUND} pt sein +@msg {key} must be [x,y] {key} muss [x,y] sein +@msg {key} must be a percentage {min}..100 {key} muss ein Prozentwert von {min}..100 sein +@msg {key} must be a positive number, not {n} {key} muss eine positive Zahl sein, nicht {n} +@msg {key} must be a positive number, not {v} {key} muss eine positive Zahl sein, nicht {v} +@msg {key} must be one of {_1} {key} muss eines der folgenden sein: {_1} +@msg {key} must be {_1}, got {s} {key} muss {_1} sein, erhalten: {s} +@msg {key} must name a floating panel {key} muss ein schwebendes Bedienfeld nennen +@msg {key} must name a panel in another group {key} muss ein Bedienfeld in einer anderen Gruppe nennen +@msg {key} must name a panel that shows {key} muss ein angezeigtes Bedienfeld nennen +@msg {key} must name a registered panel {key} muss ein registriertes Bedienfeld nennen +@msg {kind} files have no transparency, and the file would have {what}: flatten it first (object.flattenTransparency) {kind}-Dateien haben keine Transparenz, und die Datei hätte {what}: Reduzieren Sie sie zuerst (object.flattenTransparency) +@msg {kind} images can't be opened (see document.formats) {kind}-Bilder können nicht geöffnet werden (siehe document.formats) +@msg {k} `{v}` is not one of the choices {k} `{v}` ist keine der Auswahlmöglichkeiten +@msg {k} has control characters{_1} {k} enthält Steuerzeichen{_1} +@msg {k} is too long (at most {MAX_TEXT} characters) {k} ist zu lang (höchstens {MAX_TEXT} Zeichen) +@msg {k} must be > 0 {k} muss > 0 sein +@msg {k} must be a number (pt) or a length such as "210 mm" {k} muss eine Zahl (pt) oder eine Länge wie „210 mm“ sein +@msg {k} must be a number from {min} to {max} {k} muss eine Zahl von {min} bis {max} sein +@msg {k} must be a number or "auto" {k} muss eine Zahl oder „auto“ sein +@msg {k} must be a string {k} muss ein String sein +@msg {k} must be text {k} muss Text sein +@msg {k} must be true or false {k} muss true oder false sein +@msg {k} must be {{size?, position?}} {k} muss {{size?, position?}} sein +@msg {k}: can't read `{s}` as a length {k}: `{s}` kann nicht als Länge gelesen werden +@msg {label} must be a number at most {max} {label} muss eine Zahl von höchstens {max} sein +@msg {label} must be a number from -1296 to 1296 {label} muss eine Zahl von -1296 bis 1296 sein +@msg {label} must be a number from 1 to 10000 {label} muss eine Zahl von 1 bis 10000 sein +@msg {name} `{s}`: {ids} {name} `{s}`: {ids} +@msg {name} is built in and can't be deleted {name} ist integriert und kann nicht gelöscht werden +@msg {name} must be {lo}–{hi}{unit} (got {v}) {name} muss {lo}–{hi}{unit} sein (erhalten: {v}) +@msg {name}: CRC or size mismatch {name}: CRC oder Größe stimmt nicht überein +@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}: nur die eingebettete Vorschau dieser Affinity-Datei konnte gelesen werden; verwenden Sie Datei › Öffnen, um sie mit ihrer Warnung zu sehen, oder exportieren Sie vor dem Platzieren SVG oder PDF aus Affinity +@msg {name}: {e} {name}: {e} +@msg {native}; its embedded preview can't be read either ({e}) {native}; seine eingebettete Vorschau kann ebenfalls nicht gelesen werden ({e}) +@msg {std} files are CMYK, and the file would have colours in {_1} {std}-Dateien sind CMYK, und die Datei hätte Farben in {_1} +@msg {std} files embed their fonts, and the file would have one that isn't {std}-Dateien betten ihre Schriften ein, und die Datei hätte eine, die nicht eingebettet ist +@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} ist die Datei, aus der dieses Dokument gelesen wurde, und beim Lesen wurde etwas ausgelassen ({what}): Ein Überschreiben würde es endgültig verlieren. Schreiben Sie stattdessen eine andere Datei, oder übergeben Sie acknowledgeLoss: true, um sie trotzdem zu ersetzen +@msg {ty} brushes need selected art (or params.{k}) {ty}-Pinsel brauchen ausgewähltes Bildmaterial (oder params.{k}) +@msg {v} is not an asset id {v} ist keine Asset-ID +@msg {what} can't be intertwined {what} kann nicht verflochten werden +@msg {what} doesn't fit the paper with its bleed and marks {what} passt mit Anschnitt und Marken nicht auf das Papier +@msg {what} would print on more than {MAX_TILES} tiles: scale it down {what} würde auf mehr als {MAX_TILES} Kacheln gedruckt: Verkleinern Sie es +@msg Alignment must be one of the choices Ausrichtung muss eine der Auswahlmöglichkeiten sein +@msg Line Gap must be a number Zeilenabstand muss eine Zahl sein +@msg `at` must be a point [x, y] `at` muss ein Punkt [x, y] sein +@msg `points` must hold from 2 to 100000 points `points` muss 2 bis 100000 Punkte enthalten +@msg `rect` must be [x, y, width, height] `rect` muss [x, y, width, height] sein +@msg `tolerance` must be from 0 to 100 pt `tolerance` muss zwischen 0 und 100 pt liegen +@msg `warichuAlign` must be one of {_1} `warichuAlign` muss eines der folgenden sein: {_1} +@msg `warichuLines` must be a whole number from {_1} to {_2} `warichuLines` muss eine ganze Zahl von {_1} bis {_2} sein +@msg bad point {v}: expected [x, y] ungültiger Punkt {v}: erwartet wird [x, y] +@msg give exactly one of `at`, `rect` and `points` geben Sie genau eines von `at`, `rect` und `points` an +@msg the line is too long, or the paths it passes too many, to check die Linie ist zu lang, oder sie kreuzt zu viele Pfade, um sie zu prüfen +@msg the paths are too many or too tangled to find where they cross die Pfade sind zu zahlreich oder zu verschlungen, um ihre Kreuzungen zu finden +@msg the rectangle or line reaches too many paths to check das Rechteck oder die Linie erreicht zu viele Pfade, um sie zu prüfen +@msg {name} must be a number {name} muss eine Zahl sein diff --git a/docs/localization-parity.md b/docs/localization-parity.md index cd8dc33f30..a6078c1ba2 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -35,7 +35,7 @@ Chinese by `imachus`). Only Georgian is recorded as reviewed by a native speaker | Portuguese | `pt-br` | 6,106 catalog entries (counted 2026-10-31 on `main` at `d26dd933a`): menus, `tl!` literals and messages complete when counted; one/other plurals | all | Latin | contributor is a Brazilian Portuguese speaker; further native review welcome | full | — (review) | | Indonesian | `id` | 5,279 rows, 750 of them `@msg` (counted 2026-10-21): menus and `tl!` literals complete when counted; its messages are partial; one plural form | dialogs and tooltips all; 750 message rows (French has 796) | Latin | no | full | — (review) | | Japanese | `ja` | 4,004 / 4,020 (99.6%) | all | CJK UI font as above; Japanese IME; vertical type partial; Japanese crop marks and composition settings | no | full | — (review) | -| German | `de` | 3,952 / 4,020 (98%; menus, literals and messages complete when counted) | all (602 of 604 message rows; landed 2026-10-10) | Latin | contributor-written, not reviewed | full | — (review) | +| German | `de` | 3,952 / 4,020 (98%; menus, literals and messages complete when counted) | all (3,463 `@msg` and 48 `@msgplural` rows, counted 2026-11-13: the untranslated report lists no missing messages for it) | Latin | contributor-written, not reviewed | full | — (review) | | Korean | `ko` | 4,870 rows (counted 2026-10-21): menus, `tl!` literals and messages complete when counted; one plural form | all | Hangul from an installed system font on the desktop (Apple SD Gothic Neo, Malgun Gothic, Noto Sans CJK KR…); the web build has no Korean UI font yet (craft-fonts); IME path exists | no (AI-drafted) | full | — (review; web font) | | Vietnamese | `vi` | 4,886 rows (counted 2026-10-21): menus, `tl!` literals and messages complete when counted; one plural form; 4,046 plain + 18 `@plural` + 28 contextual + 794 `@msg` | all | Latin with Vietnamese diacritics (stacked marks attach in precomposed, decomposed and keyboard forms, #4491; glyph tests pass) | contributor is a Vietnamese speaker; AI-assisted draft, further native review welcome | full | — (review) | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index e7c1b28726..70b73024d8 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -882,6 +882,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur The status bar's messages for documents opened or saved with notes and for discarded or restored recovered documents, and the Global Edit options' Results line, do the same (`open_save_and_recovery_messages_take_the_plural_form` in `crates/ui-egui/src/i18n/tests.rs`); the warnings for linked images and placed documents output as their previews and the errors for a page past a placed file's last page or artboard name their counts in the singular or the plural in English. - Edit › Undo and Edit › Redo, in the in-window and native menus, name the step in the interface language as the History panel lists it (`panels::history::step_name`), so when the catalog has no plain row for a step named after an effect, such as Feather, the menu shows the translation of the effect's menu row without its ellipsis (`undo_and_redo_name_the_step_as_the_history_panel_does`). - The History panel translates the undo step names the engine, the tools and the shell write as literals: the untranslated report (`untranslated_report`) lists any a complete catalog lacks, and a step whose catalog row exists only with an ellipsis (such as an effect's menu name) shows that row without the ellipsis. + - The German catalog (`crates/ui-egui/src/i18n/de.tsv`) has an `@msg` row for every status and error message the untranslated report (`untranslated_report`) finds in the sources, 3,463 `@msg` rows in all: the errors of commands, scripts, file import and export and plug-ins. The report's German section lists no messages. A message with two placeholders in a row (`Template::parse` in `crates/ui-egui/src/i18n/catalog.rs`) is still shown in English. - The History panel menu has Step Backward and Step Forward, which run Edit › Undo and Edit › Redo and are disabled when there is no step to undo or redo, and Set History Limit…, which opens Preferences at Performance, where History States is set. - The History panel menu's Clear History (`edit.clearHistory`) removes every step to undo and to redo and leaves the document as it is; it is not an undo step itself, and it is disabled when there is no step to clear. - The New Document from State button at the bottom of the History panel (`edit.newDocumentFromState`) makes a new document, which becomes active, from the state the panel has selected ([public source](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/undo-edits-and-manage-design-history.html)). VectorCraft's own rules: the new document takes the next Untitled-N name, is marked unsaved, has nothing selected and has no steps to undo or redo, and the source document and its steps stay as they are. From 15b75be8602fcf8a5370f3c06e60cf34effbe8f6 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 14:35:40 -0800 Subject: [PATCH 10/63] i18n: Italian translates 1,802 more status and error messages it is in COMPLETE_MESSAGES, but the untranslated report listed 1,842 status and error messages with no Italian row, so with the interface in Italian those messages showed in English. - it.tsv gains 1,802 `@msg` rows. The translations are VectorCraft's own, written from the English meaning with the catalog's terms, and await native review. Placeholders, backticked names, command ids, double-quoted values and bare true, false and null stay as the English writes them. - The untranslated report now lists 40 Italian messages. 38 of those keys get Italian rows from open pull requests 7211, 7214 and 7116, so they are left out here. Menu strings, `tl!` literals, undo labels and swatch names are unchanged. - The Italian row of docs/localization-parity.md gives the catalog's current counts, and docs/target-app-parity.md has a line for the change. --- crates/ui-egui/src/i18n/it.tsv | 1804 ++++++++++++++++++++++++++++++++ docs/localization-parity.md | 2 +- docs/target-app-parity.md | 1 + 3 files changed, 1806 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index fee7dbd594..d86c040533 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -7081,3 +7081,1807 @@ breakText Glyphs Glifi Margin Margine Box Riquadro Swatch Campione + +# Status and error messages from across the app, script engines and file readers. +@msg 'caller', 'callee' and 'arguments' cannot be used on strict mode functions 'caller', 'callee' e 'arguments' non possono essere usati nelle funzioni in modalità strict +@msg Alignment must be one of the choices L'allineamento deve essere una delle scelte disponibili +@msg Boolean.prototype.{name} was called on a value that is not a Boolean Boolean.prototype.{name} è stato chiamato su un valore che non è un Boolean +@msg CMYK images whose samples couldn't be read in CMYK were imported in RGB Le immagini CMYK i cui campioni non è stato possibile leggere in CMYK sono state importate in RGB +@msg Cannot write to the location of “{name}”; choose where to save it Impossibile scrivere nella posizione di “{name}”; scegli dove salvarlo +@msg DCS files can only be placed linked: use File › Place with Link on I file DCS possono essere inseriti solo come collegati: usa File › Inserisci con Collega attivato +@msg Date.prototype method called on a value that is not a Date object Metodo di Date.prototype chiamato su un valore che non è un oggetto Date +@msg Error.prototype.toString was called on a value that is not an object Error.prototype.toString è stato chiamato su un valore che non è un oggetto +@msg Function.prototype.toString was called on a value that is not a function Function.prototype.toString è stato chiamato su un valore che non è una funzione +@msg Import SVG Filter reads SVG files, not SVGZ Importa filtro SVG legge file SVG, non SVGZ +@msg JSON.parse: {why} (at offset {at}) JSON.parse: {why} (all'offset {at}) +@msg JSON.stringify cannot write a structure that contains itself JSON.stringify non può scrivere una struttura che contiene se stessa +@msg Line Gap must be a number La distanza tra le righe deve essere un numero +@msg Namespace.prototype.toString needs a Namespace Namespace.prototype.toString richiede un Namespace +@msg New Document profiles need the preferences folder, which this run doesn't have I profili di nuovo documento richiedono la cartella delle preferenze, che questa esecuzione non ha +@msg Number.prototype.{name} was called on a value that is not a number Number.prototype.{name} è stato chiamato su un valore che non è un numero +@msg Object.create's prototype must be an object or null Il prototipo di Object.create deve essere un oggetto o null +@msg Object.{function} was given a value that is not an object Object.{function} ha ricevuto un valore che non è un oggetto +@msg QName.prototype.toString needs a QName QName.prototype.toString richiede un QName +@msg RegExp.prototype.{name} was called on a value that is not a RegExp object RegExp.prototype.{name} è stato chiamato su un valore che non è un oggetto RegExp +@msg Stencil Bridges: the selected art is too large or too detailed to bridge Ponti per stencil: la grafica selezionata è troppo grande o troppo dettagliata per essere collegata +@msg String.prototype.toString was called on a value that is not a string String.prototype.toString è stato chiamato su un valore che non è una stringa +@msg The {name} profile could not be read, so the new document has the default settings: {reason} Non è stato possibile leggere il profilo {name}, quindi il nuovo documento ha le impostazioni predefinite: {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 Trapping: più di {_1} coppie di oggetti sono abbastanza vicine da richiedere il trapping, oppure troppi oggetti condividono un intervallo orizzontale; applica il trapping a meno oggetti alla volta +@msg Weed Box and Lines: no weeding line fits outside the art, and the weed box is off Riquadro e linee di spellicolatura: nessuna linea di spellicolatura entra fuori dalla grafica e il riquadro di spellicolatura è disattivato +@msg \\ at end of pattern \\ alla fine del pattern +@msg `alignOn` cannot be empty `alignOn` non può essere vuoto +@msg `alignOn` must be a string `alignOn` deve essere una stringa +@msg `align` must be a string `align` deve essere una stringa +@msg `answer` for this {kind} dialog must be one of: {_1} `answer` per questa finestra di dialogo {kind} deve essere uno tra: {_1} +@msg `answers` has more than {MAX_ANSWERS} answers `answers` ha più di {MAX_ANSWERS} risposte +@msg `answers` is larger than 1 MiB `answers` supera 1 MiB +@msg `answers` must be a list of answers `answers` deve essere un elenco di risposte +@msg `artboard` must be the index of an artboard of the document `artboard` deve essere l'indice di una tavola da disegno del documento +@msg `artboardsAt` must be [x, y] `artboardsAt` deve essere [x, y] +@msg `artboards` names an artboard the document does not have `artboards` indica una tavola da disegno che il documento non ha +@msg `artboards` names an artboard twice `artboards` indica due volte la stessa tavola da disegno +@msg `at` must be [x, y] (got {at}) `at` deve essere [x, y] (ricevuto {at}) +@msg `at` must be a point [x, y] `at` deve essere un punto [x, y] +@msg `beside` needs `side` `beside` richiede `side` +@msg `code` must be a whole number `code` deve essere un numero intero +@msg `colorMode` must be one of rgb, cmyk `colorMode` deve essere uno tra rgb, cmyk +@msg `colors` does not apply to the {name} reduction `colors` non si applica alla riduzione {name} +@msg `colors` must be a whole number from 2 to 256 `colors` deve essere un numero intero compreso tra 2 e 256 +@msg `colors` must be at least 9 for the uniform reduction `colors` deve essere almeno 9 per la riduzione uniforme +@msg `colors` must be between 1 and {MAX_THEME_COLORS} (got {n}) `colors` deve essere compreso tra 1 e {MAX_THEME_COLORS} (ricevuto {n}) +@msg `count` must be a whole number from 0 to {MAX_COUNT} `count` deve essere un numero intero compreso tra 0 e {MAX_COUNT} +@msg `ditherAmount` must be a number from 0 to 100 `ditherAmount` deve essere un numero compreso tra 0 e 100 +@msg `fidelity` must be from {lo} to {hi} pt `fidelity` deve essere compreso tra {lo} e {hi} pt +@msg `focal` and `radius` cannot be used together `focal` e `radius` non possono essere usati insieme +@msg `focal` must be an array of [subpath, anchor] pairs `focal` deve essere un array di coppie [subpath, anchor] +@msg `focal` names [{si}, {ai}], which is not an anchor of the path `focal` indica [{si}, {ai}], che non è un punto di ancoraggio del tracciato +@msg `glyphAlternate` takes only null, which takes a glyph choice away (text.setGlyphAlternate makes one) `glyphAlternate` accetta solo null, che rimuove una scelta di glifo (text.setGlyphAlternate ne imposta una) +@msg `group` must be one of {_1} `group` deve essere uno tra {_1} +@msg `group` must be the Objects on Path group of `member` and `target` `group` deve essere il gruppo Oggetti su tracciato di `member` e `target` +@msg `id` is an object id, not {v} `id` è un id di oggetto, non {v} +@msg `item` needs `index` `item` richiede `index` +@msg `kerningBefore` belongs to a line's first character, not to a character style (text.setRangeStyle sets it) `kerningBefore` riguarda il primo carattere di una riga, non uno stile di carattere (lo imposta text.setRangeStyle) +@msg `kinsokuType` must be "pushInFirst", "pushOutFirst" or "pushOutOnly" `kinsokuType` deve essere "pushInFirst", "pushOutFirst" o "pushOutOnly" +@msg `leader` must be a string `leader` deve essere una stringa +@msg `max` must be 1 to {MAX_SUGGESTIONS} `max` deve essere compreso tra 1 e {MAX_SUGGESTIONS} +@msg `name` must be a name that is not empty `name` deve essere un nome non vuoto +@msg `name` must not be empty `name` non deve essere vuoto +@msg `origin` must be [x, y] in points `origin` deve essere [x, y] in punti +@msg `picks` must be an array (got {v}) `picks` deve essere un array (ricevuto {v}) +@msg `picks` must hold 1 to {MAX_PICKS} picks (got {_1}) `picks` deve contenere da 1 a {MAX_PICKS} scelte (ricevute {_1}) +@msg `points` must hold from 2 to 100000 points `points` deve contenere da 2 a 100000 punti +@msg `radius` must be 0 or more `radius` deve essere 0 o maggiore +@msg `rect` must be [x, y, width, height] `rect` deve essere [x, y, width, height] +@msg `rect` must be [x0, y0, x1, y1] (got {r}) `rect` deve essere [x0, y0, x1, y1] (ricevuto {r}) +@msg `replace` must be a string `replace` deve essere una stringa +@msg `rows` must be lists of numbers or nulls, not {r} `rows` deve essere un elenco di liste di numeri o null, non {r} +@msg `scale` must be a number from 0.001 to 1000 `scale` deve essere un numero compreso tra 0.001 e 1000 +@msg `scope.{key}` must be a non-empty array `scope.{key}` deve essere un array non vuoto +@msg `scope` must be "document", {artboards} or {layers} `scope` deve essere "document", {artboards} o {layers} +@msg `side` must be "left", "right", "top" or "bottom" `side` deve essere "left", "right", "top" o "bottom" +@msg `side` places the artboard: leave out x, y, width and height `side` posiziona la tavola da disegno: ometti x, y, width e height +@msg `source` is the only path in series group `target` `source` è l'unico tracciato nel gruppo in serie `target` +@msg `spacing` makes more than {MAX_COUNT} lines across the box `spacing` produce più di {MAX_COUNT} linee nel riquadro +@msg `spacing` must be more than 0 and at most {MAX_DISTANCE} pt `spacing` deve essere maggiore di 0 e al massimo {MAX_DISTANCE} pt +@msg `target` must be an object of the same Objects on Path group as `member` `target` deve essere un oggetto dello stesso gruppo Oggetti su tracciato di `member` +@msg `tolerance` must be from 0 to 100 pt `tolerance` deve essere compreso tra 0 e 100 pt +@msg `version` must be 0 or {_1} `version` deve essere 0 o {_1} +@msg `version` must be 0 or {newest} `version` deve essere 0 o {newest} +@msg `warichuAlign` must be one of {_1} `warichuAlign` deve essere uno tra {_1} +@msg `warichuLines` must be a whole number from {_1} to {_2} `warichuLines` deve essere un numero intero compreso tra {_1} e {_2} +@msg `weight` must be from 0.01 to 1000 pt `weight` deve essere compreso tra 0.01 e 1000 pt +@msg `weights` must be a list of {_1} numbers, one per row (got {v}) `weights` deve essere un elenco di {_1} numeri, uno per riga (ricevuto {v}) +@msg `xml` must be a string `xml` deve essere una stringa +@msg `{KEY}` entry {n} gives the paragraph at byte {start} of {_1} other indents than an entry before it la voce {n} di `{KEY}` assegna al paragrafo al byte {start} di {_1} rientri diversi da quelli di una voce precedente +@msg `{KEY}` entry {n} must be an object la voce {n} di `{KEY}` deve essere un oggetto +@msg `{KEY}` entry {n} sets neither `leftIndent` nor `firstLineIndent` la voce {n} di `{KEY}` non imposta né `leftIndent` né `firstLineIndent` +@msg `{KEY}` entry {n}: `id` must be an object id `{KEY}` voce {n}: `id` deve essere un id di oggetto +@msg `{KEY}` entry {n}: `start` must be a byte offset `{KEY}` voce {n}: `start` deve essere un offset in byte +@msg `{KEY}` entry {n}: `{k}` must be a number from -{MAX_PARA} to {MAX_PARA} pt `{KEY}` voce {n}: `{k}` deve essere un numero compreso tra -{MAX_PARA} e {MAX_PARA} pt +@msg `{KEY}` entry {n}: no paragraph of {_1} starts at byte {start} `{KEY}` voce {n}: nessun paragrafo di {_1} inizia al byte {start} +@msg `{KEY}` entry {n}: unknown key `{k}` `{KEY}` voce {n}: chiave sconosciuta `{k}` +@msg `{KEY}` entry {n}: {_1} is not a type object `{KEY}` voce {n}: {_1} non è un oggetto di testo +@msg `{KEY}` is empty `{KEY}` è vuoto +@msg `{KEY}` must be a list `{KEY}` deve essere un elenco +@msg `{KEY}` takes at most {MAX_PARAGRAPH_INDENTS} entries (got {_1}) `{KEY}` accetta al massimo {MAX_PARAGRAPH_INDENTS} voci (ricevute {_1}) +@msg `{KEY}` takes no `{other}` beside it `{KEY}` non accetta `{other}` insieme +@msg `{base}` is larger than {_1} MB `{base}` supera {_1} MB +@msg `{base}` is not in the DCS file's folder `{base}` non si trova nella cartella del file DCS +@msg `{key}` holds more than {MAX_EDIT_WORDS} words `{key}` contiene più di {MAX_EDIT_WORDS} parole +@msg `{key}` must be 0 or more `{key}` deve essere 0 o maggiore +@msg `{key}` must be [x, y] numbers `{key}` deve essere [x, y] numerici +@msg `{key}` must be a list of words `{key}` deve essere un elenco di parole +@msg `{key}` must be a list, not {v} `{key}` deve essere un elenco, non {v} +@msg `{key}` must be a number or null, not {v} `{key}` deve essere un numero o null, non {v} +@msg `{key}` must be a whole number of 0 or more `{key}` deve essere un numero intero pari a 0 o maggiore +@msg `{key}` must be from 0 to {MAX_DISTANCE} pt `{key}` deve essere compreso tra 0 e {MAX_DISTANCE} pt +@msg `{key}` must be from {lo} to {hi}, not {v} `{key}` deve essere compreso tra {lo} e {hi}, non {v} +@msg `{key}` must be one of auto, always, never `{key}` deve essere uno tra auto, always, never +@msg `{key}` must be one of miter, round, bevel `{key}` deve essere uno tra miter, round, bevel +@msg `{key}` must be one word of at most {_1} characters `{key}` deve essere una sola parola di al massimo {_1} caratteri +@msg `{key}` must hold single words of at most {_1} characters `{key}` deve contenere singole parole di al massimo {_1} caratteri +@msg `{key}` must name an object attached to the path of an Objects on Path group `{key}` deve indicare un oggetto collegato al tracciato di un gruppo Oggetti su tracciato +@msg `{key}` names an object that is hidden or locked `{key}` indica un oggetto nascosto o bloccato +@msg `{name}` is a file of its own, which can only be found next to the DCS file's path `{name}` è un file a sé, che può trovarsi solo accanto al percorso del file DCS +@msg `{name}`: {_1}: {e} `{name}`: {_1}: {e} +@msg `{u}` is no Unicode character `{u}` non è un carattere Unicode +@msg `{word}` isn't in the custom dictionary `{word}` non è nel dizionario personalizzato +@msg a CMap has too many mappings una CMap ha troppe corrispondenze +@msg a \\u escape needs four hexadecimal digits un escape \\u richiede quattro cifre esadecimali +@msg a comma in an XPath expression must separate function arguments una virgola in un'espressione XPath deve separare gli argomenti di una funzione +@msg a document holds at most {_1} SVG filters un documento contiene al massimo {_1} filtri SVG +@msg a document with unsaved changes has no recovery copy un documento con modifiche non salvate non ha una copia di ripristino +@msg a font's data is too large i dati di un font sono troppo grandi +@msg a font's subroutines are too large le subroutine di un font sono troppo grandi +@msg a function declaration without a name una dichiarazione di funzione senza nome +@msg a host value has too many entries un valore host ha troppe voci +@msg a host value is nested too deep un valore host è annidato troppo in profondità +@msg a namespace declaration uses a reserved prefix or URI una dichiarazione di namespace usa un prefisso o un URI riservato +@msg a namespace qualifier is undefined un qualificatore di namespace non è definito +@msg a namespace with an empty URI cannot have a prefix un namespace con URI vuoto non può avere un prefisso +@msg a number has no digits un numero non ha cifre +@msg a number's exponent has no digits l'esponente di un numero non ha cifre +@msg a number's fraction has no digits la parte frazionaria di un numero non ha cifre +@msg a property descriptor cannot have both a value or writable and a get or set un descrittore di proprietà non può avere sia value o writable sia get o set +@msg a property descriptor must be an object un descrittore di proprietà deve essere un oggetto +@msg a property's {name} must be a function or undefined il {name} di una proprietà deve essere una funzione o undefined +@msg a stream in it would decode to more than 256 MB, or its decode parameters can't be applied uno stream al suo interno verrebbe decodificato in più di 256 MB, oppure i suoi parametri di decodifica non possono essere applicati +@msg a string holds a control character una stringa contiene un carattere di controllo +@msg a string holds an escape sequence JSON does not have una stringa contiene una sequenza di escape che JSON non prevede +@msg a string is not closed una stringa non è chiusa +@msg an SVG filter is named by its id: give `xml` with `name` un filtro SVG prende il nome dal suo id: indica `xml` con `name` +@msg an XML element has two attributes with the same name un elemento XML ha due attributi con lo stesso nome +@msg an XML value cannot be inserted into itself or its descendants un valore XML non può essere inserito in se stesso o nei suoi discendenti +@msg an XML value's children cannot be assigned by position i figli di un valore XML non possono essere assegnati per posizione +@msg an XML value's children cannot be deleted by position i figli di un valore XML non possono essere eliminati per posizione +@msg an XMLList converts to XML only when it has one item un XMLList si converte in XML solo quando ha un solo elemento +@msg an XPath node test is missing manca un test di nodo XPath +@msg an XPath operand is missing manca un operando XPath +@msg an XPath operator is missing manca un operatore XPath +@msg an XPath predicate or step cannot follow here qui non può seguire un predicato o un passo XPath +@msg an XPath predicate or step follows a value that is not a node-set un predicato o un passo XPath segue un valore che non è un node-set +@msg an XPath step is missing manca un passo XPath +@msg an image couldn't be decoded and is missing from the picture non è stato possibile decodificare un'immagine, che manca dal disegno +@msg an image of {w} × {h} pixels is too large to convert (at most {_1} pixels) un'immagine di {w} × {h} pixel è troppo grande per essere convertita (al massimo {_1} pixel) +@msg apply was called on a value that is not a function apply è stato chiamato su un valore che non è una funzione +@msg area type in a thread cannot use Dynamic Text: {frames} il testo in area in una concatenazione non può usare il testo dinamico: {frames} +@msg array literal too long letterale array troppo lungo +@msg assignment to a constant binding assegnazione a un binding costante +@msg at least one row that isn't excluded needs a weight above 0 almeno una riga non esclusa deve avere un peso maggiore di 0 +@msg bad point {v}: expected [x, y] punto non valido {v}: previsto [x, y] +@msg blend must be a mode name blend deve essere il nome di un metodo +@msg both sides of an XPath | must select nodes entrambi i lati di un | XPath devono selezionare nodi +@msg canceled annullato +@msg cannot convert the object to a primitive value impossibile convertire l'oggetto in un valore primitivo +@msg cannot convert undefined or null to an object impossibile convertire undefined o null in un oggetto +@msg character dimensions cannot be represented with the source text transform le dimensioni dei caratteri non possono essere rappresentate con la trasformazione del testo di origine +@msg character dimensions cannot be represented with the target text transform le dimensioni dei caratteri non possono essere rappresentate con la trasformazione del testo di destinazione +@msg continuation must be an array of path chunks continuation deve essere un array di segmenti di tracciato +@msg continuation requires points continuation richiede dei punti +@msg each path chunk must contain 1 to 4096 points ogni segmento di tracciato deve contenere da 1 a 4096 punti +@msg each pick is {{at: [x, y]}} or {{rect: [x0, y0, x1, y1]}} (got {v}) ogni scelta è {{at: [x, y]}} o {{rect: [x0, y0, x1, y1]}} (ricevuto {v}) +@msg each weight must be a number of 0 or more (got {v}) ogni peso deve essere un numero pari a 0 o maggiore (ricevuto {v}) +@msg finally table {t} out of range tabella finally {t} fuori intervallo +@msg flags cannot be given with a RegExp object i flag non possono essere indicati con un oggetto RegExp +@msg function declaration index out of range indice di dichiarazione di funzione fuori intervallo +@msg give `char`, `text`, `code` or `unicode` indica `char`, `text`, `code` o `unicode` +@msg give `count` or `spacing`, not both indica `count` o `spacing`, non entrambi +@msg give `name`, an SVG filter's name indica `name`, il nome di un filtro SVG +@msg give `path`, an SVG file, or `svg`, SVG text indica `path`, un file SVG, o `svg`, testo SVG +@msg give `xml`, one SVG element indica `xml`, un solo elemento SVG +@msg give exactly one of `at`, `rect` and `points` indica esattamente uno tra `at`, `rect` e `points` +@msg give exactly one of `path`, `data` and `dataBase64` indica esattamente uno tra `path`, `data` e `dataBase64` +@msg give index or indexes, not both indica index o indexes, non entrambi +@msg give one element indica un solo elemento +@msg give opacity or blend indica opacity o blend +@msg handler at {_1} expects {_2} values but its range starts with {d} il gestore a {_1} prevede {_2} valori, ma il suo intervallo inizia con {d} +@msg handler depth too large profondità del gestore troppo grande +@msg handler range {_1}..{_2} out of range intervallo del gestore {_1}..{_2} fuori intervallo +@msg hiddenLayers must be true or false hiddenLayers deve essere true o false +@msg hvcC hvcC +@msg images in a spot ink whose samples couldn't be read were imported in RGB le immagini in un inchiostro spot i cui campioni non è stato possibile leggere sono state importate in RGB +@msg images past the most pixels one file may hold were left out le immagini oltre il numero massimo di pixel che un file può contenere sono state escluse +@msg images too large to read were left out le immagini troppo grandi da leggere sono state escluse +@msg indexes may hold at most {MAX_GUIDE_INDEXES} entries indexes può contenere al massimo {MAX_GUIDE_INDEXES} voci +@msg indexes must be an array of guide indexes indexes deve essere un array di indici di guide +@msg instruction out of range istruzione fuori intervallo +@msg instruction {pc} ({op}) pops below the stack l'istruzione {pc} ({op}) estrae oltre il fondo dello stack +@msg instruction {pc} reached with stack depths {seen} and {d} istruzione {pc} raggiunta con profondità dello stack {seen} e {d} +@msg invalid array length lunghezza dell'array non valida +@msg invalid group gruppo non valido +@msg invalid object id {v}: expected a non-negative integer id di oggetto non valido {v}: previsto un intero non negativo +@msg invalid regular expression flag '{_1}' flag di espressione regolare non valido '{_1}' +@msg invalid regular expression: {m} espressione regolare non valida: {m} +@msg invalid time value valore di tempo non valido +@msg is a confirm's answer, whose `value` must be true or false è la risposta a una conferma, il cui `value` deve essere true o false +@msg is a prompt's answer, whose `value` must be a string or null è la risposta a una richiesta di input, il cui `value` deve essere una stringa o null +@msg is an alert's answer, whose `value` must be "ok" è la risposta a un avviso, il cui `value` deve essere "ok" +@msg is longer than 16 KiB supera 16 KiB +@msg it draws its image through a clipping path disegna la sua immagine attraverso un tracciato di ritaglio +@msg it draws no image non disegna alcuna immagine +@msg it is larger than {MAX_DICTIONARY_FILE_BYTES} bytes supera {MAX_DICTIONARY_FILE_BYTES} byte +@msg it isn't a file non è un file +@msg it names {_1} plates, and at most {MAX_PLATES} are read indica {_1} lastre, e ne vengono lette al massimo {MAX_PLATES} +@msg its art is too long or too detailed for a head la sua grafica è troppo lunga o troppo dettagliata per una testa +@msg its art paints nothing la sua grafica non dipinge nulla +@msg its image is too large to read la sua immagine è troppo grande da leggere +@msg its plates are too large: more than {MAX_SAMPLES} pixels together le sue lastre sono troppo grandi: più di {MAX_SAMPLES} pixel in totale +@msg its plates are too large: {_1} plates of {w} × {h} pixels le sue lastre sono troppo grandi: {_1} lastre di {w} × {h} pixel +@msg its plates can't be kept together le sue lastre non possono essere tenute insieme +@msg its shapes could not be combined into one non è stato possibile combinare le sue forme in una sola +@msg jump to {pc}, past the end salto a {pc}, oltre la fine +@msg legacyNameEncoding must be one of {_1} legacyNameEncoding deve essere uno tra {_1} +@msg localize: the first argument must be a localization object or a string localize: il primo argomento deve essere un oggetto di localizzazione o una stringa +@msg missing `picks` `picks` mancante +@msg missing `word` `word` mancante +@msg must be an object with `kind` and `value` deve essere un oggetto con `kind` e `value` +@msg must have `kind` alert, confirm or prompt deve avere `kind` alert, confirm o prompt +@msg must have exactly `kind` and `value` deve avere esattamente `kind` e `value` +@msg needs a closing quote after the name richiede una virgoletta di chiusura dopo il nome +@msg needs a name richiede un nome +@msg needs a name of at most 64 bytes richiede un nome di al massimo 64 byte +@msg needs a name of letters, digits and _, or a quoted name richiede un nome di lettere, cifre e _, oppure un nome tra virgolette +@msg needs a name without control characters richiede un nome senza caratteri di controllo +@msg nesting too deep annidamento troppo profondo +@msg no HP-GL format nessun formato HP-GL +@msg no New Document profile called `{shown}` ({_1}) nessun profilo di nuovo documento chiamato `{shown}` ({_1}) +@msg no SVG filter at index {k} nessun filtro SVG all'indice {k} +@msg no arrowheads file here nessun file di punte di freccia qui +@msg no character has code {code} nessun carattere ha il codice {code} +@msg no custom arrowhead {name} (see stroke.arrowheads.list) nessuna punta di freccia personalizzata {name} (vedi stroke.arrowheads.list) +@msg no file to open nessun file da aprire +@msg no instructions nessuna istruzione +@msg no layer to rename nessun livello da rinominare +@msg no random numbers ({e}) nessun numero casuale ({e}) +@msg no selected or given object has a visible 3D effect nessun oggetto selezionato o indicato ha un effetto 3D visibile +@msg no such artboard {v} tavola da disegno {v} inesistente +@msg no symbol named `{_1}` nessun simbolo chiamato `{_1}` +@msg node {_1} already has the text variable `{_2}` il nodo {_1} ha già la variabile di testo `{_2}` +@msg not a DCS file non è un file DCS +@msg not a RegExp object non è un oggetto RegExp +@msg not a regular expression non è un'espressione regolare +@msg not an action set: the text doesn't start with /version and a number non è un set di azioni: il testo non inizia con /version e un numero +@msg nothing to repeat niente da ripetere +@msg numbers out of order in {_1} quantifier numeri non in ordine nel quantificatore {_1} +@msg object {_1} needs a 3D effect l'oggetto {_1} richiede un effetto 3D +@msg object {_1}: a hollow Extrude & Bevel has no caps: turn on Cap to map a graphic oggetto {_1}: un'estrusione e smussatura cava non ha tappi: attiva Tappo per applicare una grafica +@msg object {_1}: with a depth of 0 the object has one face: map its graphics onto the front oggetto {_1}: con una profondità di 0 l'oggetto ha una sola faccia: applica le sue grafiche sulla faccia anteriore +@msg object {_1}: {_2} oggetto {_1}: {_2} +@msg object {_1}: {e} oggetto {_1}: {e} +@msg object {_1}: {name} at index {index} has no lighting presets; set its lights with effect.setParams oggetto {_1}: {name} all'indice {index} non ha predefiniti di illuminazione; imposta le sue luci con effect.setParams +@msg opacity must be a number opacity deve essere un numero +@msg operand too large operando troppo grande +@msg order is a whole number from 0 to {others}, not `{v}` order è un numero intero compreso tra 0 e {others}, non `{v}` +@msg path chunks must share their boundary point i segmenti di tracciato devono condividere il punto di confine +@msg pdfCompatible: false needs the artboards to keep the editable artwork when useArtboards is false pdfCompatible: false richiede le tavole da disegno per conservare la grafica modificabile quando useArtboards è false +@msg point x must be a number la x del punto deve essere un numero +@msg point y must be a number la y del punto deve essere un numero +@msg points must be [x,y] pairs points deve contenere coppie [x,y] +@msg points must be finite canvas points points deve contenere punti finiti dell'area di lavoro +@msg range out of order in character class intervallo non in ordine nella classe di caratteri +@msg reduce of an empty array with no initial value reduce di un array vuoto senza valore iniziale +@msg regular expression flag '{_1}' given twice flag di espressione regolare '{_1}' indicato due volte +@msg sampled text would exceed the canvas (coordinates out of range) il testo campionato supererebbe l'area di lavoro (coordinate fuori intervallo) +@msg select an SVG filter seleziona un filtro SVG +@msg select an object with a 3D effect seleziona un oggetto con un effetto 3D +@msg select an object with a 3D effect, or give its id in ids seleziona un oggetto con un effetto 3D, o indica il suo id in ids +@msg select one layer, group or object to rename seleziona un livello, un gruppo o un oggetto da rinominare +@msg select the art to weed seleziona la grafica da spellicolare +@msg select type on a path or area type seleziona un testo su tracciato o un testo in area +@msg side is left or right, not `{v}` side è left o right, non `{v}` +@msg source text transform is collapsed or unrepresentable la trasformazione del testo di origine è degenere o non rappresentabile +@msg table must be true or false table deve essere true o false +@msg takes one name accetta un solo nome +@msg target text transform is collapsed or unrepresentable la trasformazione del testo di destinazione è degenere o non rappresentabile +@msg that makes too many pieces (at most 100000) or dash steps: simplify the dash pattern or make the dashes longer questo produce troppi pezzi (al massimo 100000) o passi del tratteggio: semplifica lo schema del tratteggio o allunga i trattini +@msg the Layers panel is not shown il pannello Livelli non è visualizzato +@msg the Layers panel's search, filter or Show Layers Only hides its row la ricerca, il filtro o Mostra solo livelli del pannello Livelli ne nasconde la riga +@msg the Objects on Path group is hidden or locked il gruppo Oggetti su tracciato è nascosto o bloccato +@msg the SVG file is larger than {MAX_IMPORT_BYTES} bytes il file SVG supera {MAX_IMPORT_BYTES} byte +@msg the XML operation needs an XML or XMLList value l'operazione XML richiede un valore XML o XMLList +@msg the XPath expression does not evaluate to a node-set l'espressione XPath non restituisce un node-set +@msg the XPath expression ends early l'espressione XPath termina prematuramente +@msg the XPath expression has an unmatched ) l'espressione XPath ha una ) senza corrispondenza +@msg the XPath expression has an unmatched ] l'espressione XPath ha una ] senza corrispondenza +@msg the XPath expression has an unmatched closing bracket l'espressione XPath ha una parentesi di chiusura senza corrispondenza +@msg the XPath expression selects a namespace node, which is not an XML value l'espressione XPath seleziona un nodo namespace, che non è un valore XML +@msg the XPath expression selects the root node, which is not an XML value l'espressione XPath seleziona il nodo radice, che non è un valore XML +@msg the XPath function {name} needs a node-set la funzione XPath {name} richiede un node-set +@msg the XPath function {name} takes the wrong number of arguments la funzione XPath {name} riceve un numero errato di argomenti +@msg the XPath name functions need a node-set le funzioni XPath sui nomi richiedono un node-set +@msg the XPath prefix {shown} is not bound to a namespace il prefisso XPath {shown} non è associato a un namespace +@msg the XPath variable ${shown} is not bound la variabile XPath ${shown} non è associata +@msg the argument list is not an object l'elenco degli argomenti non è un oggetto +@msg the artwork is too large for EPS: what follows 256 MB of PostScript was left out la grafica è troppo grande per EPS: ciò che segue i 256 MB di PostScript è stato escluso +@msg the bitmaps hold more pixels than one file may le bitmap contengono più pixel di quanti ne possa contenere un file +@msg the code runs past its last instruction il codice prosegue oltre la sua ultima istruzione +@msg the curve tolerance must be from 0.01 to 10 points la tolleranza della curva deve essere compresa tra 0.01 e 10 punti +@msg the custom dictionary couldn't be saved to {_1}: {e} impossibile salvare il dizionario personalizzato in {_1}: {e} +@msg the custom dictionary {_1} can't be read ({why}), so it is left as it is impossibile leggere il dizionario personalizzato {_1} ({why}), quindi viene lasciato com'è +@msg the document already has an SVG filter named {name} il documento ha già un filtro SVG chiamato {name} +@msg the document has another SVG filter named {name}: use svgFilter.apply, or svgFilter.define with `replace` to edit it il documento ha un altro filtro SVG chiamato {name}: usa svgFilter.apply, oppure svgFilter.define con `replace` per modificarlo +@msg the document has no SVG filter named {name} il documento non ha alcun filtro SVG chiamato {name} +@msg the document has no SVG filter named {r} il documento non ha alcun filtro SVG chiamato {r} +@msg the document has no color swatches il documento non ha campioni di colore +@msg the document's swatches hold {_1} colors; the document reduction takes at most 255 i campioni del documento contengono {_1} colori; la riduzione al documento ne accetta al massimo 255 +@msg the file isn't valid SVG: {e} il file non è un SVG valido: {e} +@msg the file names no plate il file non indica alcuna lastra +@msg the function's prototype is not an object il prototipo della funzione non è un oggetto +@msg the guide is hidden or locked la guida è nascosta o bloccata +@msg the library's file is outside the read roots il file della libreria è fuori dalle cartelle di lettura consentite +@msg the line is too long, or the paths it passes too many, to check la linea è troppo lunga, o i tracciati che attraversa sono troppi, per poterla verificare +@msg the method needs an XML value il metodo richiede un valore XML +@msg the paint is not a freeform gradient (apply one with paint.editGradient {kind}) la pittura non è una sfumatura a forma libera (applicane una con paint.editGradient {kind}) +@msg the parameters or body do not form one function i parametri o il corpo non formano un'unica funzione +@msg the pasted SVG filter {name} draws differently from the document's filter of that name, and no free name fits it il filtro SVG incollato {name} disegna in modo diverso dal filtro del documento con quel nome, e non c'è un nome libero adatto +@msg the paths are too many or too tangled to find where they cross i tracciati sono troppi o troppo intrecciati per trovare dove si incrociano +@msg the pen table holds more than 255 pens la tabella delle penne contiene più di 255 penne +@msg the plates can't be kept le lastre non possono essere conservate +@msg the position table does not match the instructions la tabella delle posizioni non corrisponde alle istruzioni +@msg the prompt's text is longer than {MAX_PROMPT_BYTES} bytes il testo della richiesta supera {MAX_PROMPT_BYTES} byte +@msg the radix must be between 2 and 36 la base deve essere compresa tra 2 e 36 +@msg the rectangle or line reaches too many paths to check il rettangolo o la linea raggiunge troppi tracciati da verificare +@msg the right side of 'in' is not an object il lato destro di 'in' non è un oggetto +@msg the right side of 'instanceof' is not a function il lato destro di 'instanceof' non è una funzione +@msg the selected art is nested more than {MAX_DEPTH} levels deep la grafica selezionata è annidata per più di {MAX_DEPTH} livelli +@msg the selected art is too large to measure la grafica selezionata è troppo grande da misurare +@msg the selection has no outlines to weed around la selezione non ha contorni attorno a cui spellicolare +@msg the value bind was called on is not a function il valore su cui è stato chiamato bind non è una funzione +@msg the value is not a constructor il valore non è un costruttore +@msg the value is not a function il valore non è una funzione +@msg the value is not a valid XML name il valore non è un nome XML valido +@msg the value is not an XML attribute name il valore non è un nome di attributo XML +@msg the value is not an XML name il valore non è un nome XML +@msg the weeding lines cross the art's outlines too many times to measure; use fewer lines le linee di spellicolatura incrociano i contorni della grafica troppe volte per poterle misurare; usa meno linee +@msg the weeding lines pass too close to too many outline segments to measure; use fewer lines or a smaller gap le linee di spellicolatura passano troppo vicino a troppi segmenti di contorno per poterle misurare; usa meno linee o una distanza minore +@msg the weeding lines would be more than {max_pieces} pieces le linee di spellicolatura sarebbero più di {max_pieces} pezzi +@msg the {_1} plate lies outside the file la lastra {_1} si trova fuori dal file +@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 questo è un file DXF binario, che non può essere letto: salvalo dall'app CAD come DXF ASCII e apri quello +@msg this warp's points can't be edited: use Reset with Mesh first i punti di questa distorsione non possono essere modificati: usa prima Reimposta con trama +@msg toExponential() digits must be between 0 and 20 le cifre di toExponential() devono essere comprese tra 0 e 20 +@msg toFixed() digits must be between 0 and 20 le cifre di toFixed() devono essere comprese tra 0 e 20 +@msg toISOString is not a function toISOString non è una funzione +@msg toPrecision() argument must be between 1 and 21 l'argomento di toPrecision() deve essere compreso tra 1 e 21 +@msg toSource: a getter cannot be written as source toSource: un getter non può essere scritto come sorgente +@msg toSource: an array with properties besides its elements, a property that is not enumerable or another prototype cannot be written as source toSource: un array con proprietà oltre ai suoi elementi, una proprietà non enumerabile o un altro prototipo non può essere scritto come sorgente +@msg toSource: an object with a property that is not enumerable, or whose prototype is not Object.prototype, cannot be written as source toSource: un oggetto con una proprietà non enumerabile, o il cui prototipo non è Object.prototype, non può essere scritto come sorgente +@msg toSource: the source is too long toSource: il sorgente è troppo lungo +@msg toSource: the value contains itself toSource: il valore contiene se stesso +@msg toSource: the value has too many entries toSource: il valore ha troppe voci +@msg toSource: the value is nested too deep toSource: il valore è annidato troppo in profondità +@msg toString is not a function toString non è una funzione +@msg too many arguments troppi argomenti +@msg too many quantifiers troppi quantificatori +@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`) funzione OpenType sconosciuta `{t}` (liga, calt, dlig, smcp, frac, onum, tnum, ordn, swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom, aalt; anteponi - per disattivarla; swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom e aalt accettano il numero di un'alternativa come `tag=N` con N da 0 a {_1}, ad esempio `salt=2`) +@msg unknown dither `{d}` (diffusion, pattern, noise or none) dithering sconosciuto `{d}` (diffusion, pattern, noise o none) +@msg unknown panel `{panel}` pannello sconosciuto `{panel}` +@msg unknown parameter `{key}` parametro sconosciuto `{key}` +@msg unknown parameter `{within}settings.{format}`; {why} parametro sconosciuto `{within}settings.{format}`; {why} +@msg unknown reduction `{reduction}` (selective, perceptual, adaptive, web, gray, blackWhite, uniform or document) riduzione sconosciuta `{reduction}` (selective, perceptual, adaptive, web, gray, blackWhite, uniform o document) +@msg unmatched ')' ')' senza corrispondenza +@msg unterminated character class classe di caratteri non terminata +@msg unterminated group gruppo non terminato +@msg wordGap and lineGap need area type in the selection wordGap e lineGap richiedono un testo in area nella selezione +@msg xpath needs its variables as an object xpath richiede le sue variabili come oggetto +@msg xpath needs the expression as a string xpath richiede l'espressione come stringa +@msg {GRANT_NOT_REMOVED}, and its settings and cache can't be moved back from {_1} to {_2}: {e} {GRANT_NOT_REMOVED}, e le sue impostazioni e la sua cache non possono essere spostate di nuovo da {_1} a {_2}: {e} +@msg {_1} is not a folder: {e} {_1} non è una cartella: {e} +@msg {_1}: {_2} and {_3} are in the way {_1}: {_2} e {_3} sono d'intralcio +@msg {code}: {0} {code}: {0} +@msg {dir}: {e} {dir}: {e} +@msg {file} names more than one linked JavaScript file {file} indica più di un file JavaScript collegato +@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}: il tracciato di una punta di freccia personalizzata richiede un'area da riempire e al massimo {most} punti di ancoraggio e {most} sottotracciati, ciascun punto entro {far} unità della punta dal suo vertice +@msg {k}: bad custom arrowhead path: {e} {k}: tracciato della punta di freccia personalizzata non valido: {e} +@msg {k}: bad custom arrowhead: {e} {k}: punta di freccia personalizzata non valida: {e} +@msg {k}: custom must be a name {k}: custom deve essere un nome +@msg {name} must be a number {name} deve essere un numero +@msg {path}: copy takes a DCS file only when its plates are inside it; relink to it where its plate files are {path}: copy accetta un file DCS solo quando le sue lastre sono al suo interno; ricollegalo dove si trovano i file delle sue lastre +@msg {path}: larger than {_1} MB {path}: supera {_1} MB +@msg {shown} is not an XPath 1.0 function {shown} non è una funzione XPath 1.0 +@msg {v} names no layer or sublayer {v} non indica alcun livello o sottolivello +@msg {what} is not a function {what} non è una funzione +@msg {what} would print more than {MAX_COPIES_PER_SHEET} copies on a sheet: scale it up {what} stamperebbe più di {MAX_COPIES_PER_SHEET} copie su un foglio: ingrandiscilo +@msg {what} {i} out of range {what} {i} fuori intervallo +@msg A profile supports one Revolve effect. Edit the existing effect's options instead. Un profilo supporta un solo effetto Rivoluzione. Modifica invece le opzioni dell'effetto esistente. +@msg Add Points at Crossings: a path went missing Aggiungi punti agli incroci: un tracciato è andato perso +@msg CSS options: {e} Opzioni CSS: {e} +@msg CSV has a header but no data rows Il CSV ha un'intestazione ma nessuna riga di dati +@msg CSV holds NUL bytes: save it as UTF-8, or as UTF-16 with a byte-order mark Il CSV contiene byte NUL: salvalo come UTF-8, o come UTF-16 con un byte-order mark +@msg CSV is empty Il CSV è vuoto +@msg CSV is larger than {MAX_CSV_BYTES} bytes Il CSV supera {MAX_CSV_BYTES} byte +@msg CSV is not UTF-16 Il CSV non è in UTF-16 +@msg CSV is not UTF-8 Il CSV non è in UTF-8 +@msg CSV would be larger than {MAX_CSV_BYTES} bytes Il CSV supererebbe {MAX_CSV_BYTES} byte +@msg Choose original artwork, not a dimension's generated paths Scegli la grafica originale, non i tracciati generati da una quota +@msg Contour: select paths or shapes Contorno: seleziona tracciati o forme +@msg Contour: the Offset Path effect is not available Contorno: l'effetto Sposta tracciato non è disponibile +@msg Create needs fills, strokes or both Crea richiede riempimenti, tracce o entrambi +@msg DXF options: {e} Opzioni DXF: {e} +@msg DXF scale must be a positive number up to {_1}, not {scale} La scala DXF deve essere un numero positivo fino a {_1}, non {scale} +@msg DXF {field} `{s}`: {allowed} DXF {field} `{s}`: {allowed} +@msg Dimension is locked or hidden La quota è bloccata o nascosta +@msg Dimension mode does not match its source La modalità della quota non corrisponde alla sua origine +@msg Draw Inside needs exactly one selected path, compound path or text Disegna all'interno richiede esattamente un tracciato, un tracciato composto o un testo selezionato +@msg EPS Save writes all artwork: use document.export for selected-only EPS Il salvataggio EPS scrive tutta la grafica: usa document.export per un EPS con la sola selezione +@msg EPS flattener: {e} Appiattimento EPS: {e} +@msg EPS options: {e} Opzioni 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 Esporta per schermi scrive png, png8, jpg, webp, gif, tiff, bmp, tga, psd, svg, svgz, pdf, eps, ai o vectorcraft +@msg Flare click coordinates must be finite numbers Le coordinate del clic per Riflesso devono essere numeri finiti +@msg HTML Text applies to object slices of type only Testo HTML si applica solo alle sezioni oggetto di testo +@msg Identify Objects By is XML ID: the name must be a valid XML ID (start with a letter, underscore or colon; no spaces) Identifica oggetti per è impostato su ID XML: il nome deve essere un ID XML valido (inizia con una lettera, un trattino basso o due punti; senza spazi) +@msg Image Trace failed: internal error: its thread stopped (please report this bug) Ricalco immagine non riuscito: errore interno: il suo thread si è fermato (segnala questo bug) +@msg Image Trace failed: {e} Ricalco immagine non riuscito: {e} +@msg Image Trace finished, but its document was closed Ricalco immagine terminato, ma il suo documento è stato chiuso +@msg Image Trace finished, but the image or its settings changed meanwhile: trace it again Ricalco immagine terminato, ma nel frattempo l'immagine o le sue impostazioni sono cambiate: ricalcala di nuovo +@msg Invalid dimension offset Distanza della quota non valida +@msg Invalid point Punto non valido +@msg Join: a subpath went missing Unisci: un sottotracciato è andato perso +@msg Lock Workspace is not available in VectorCraft Blocca area di lavoro non è disponibile in VectorCraft +@msg Move Panel to New Window is not available: floating panels stay in the main window Sposta pannello in una nuova finestra non è disponibile: i pannelli mobili restano nella finestra principale +@msg Offset Path: the path is too complex to offset Sposta tracciato: il tracciato è troppo complesso da spostare +@msg Offset Path: {e} Sposta tracciato: {e} +@msg PDF content is incomplete or exceeds the reader's limits; no document was imported Il contenuto PDF è incompleto o supera i limiti del lettore; nessun documento è stato importato +@msg PDF flattener: {e} Appiattimento PDF: {e} +@msg PDF options: {e} Opzioni PDF: {e} +@msg Recolor Artwork is no longer open Ricolora grafica non è più aperto +@msg SVG is not UTF-8, UTF-16 or ISO 8859-1 L'SVG non è in UTF-8, UTF-16 o ISO 8859-1 +@msg Save writes one artboard: name one, or export several with document.export Salva scrive una sola tavola da disegno: indicane una, o esportane diverse con document.export +@msg Select a dimension first Seleziona prima una quota +@msg Select dimensions to update Seleziona le quote da aggiornare +@msg Select only dimensions Seleziona solo quote +@msg Select two or more objects, or a group Seleziona due o più oggetti, o un gruppo +@msg Shaper needs 1–64 source paths Shaper richiede da 1 a 64 tracciati di origine +@msg Switch x/y is for scatter graphs Scambia x/y è per i grafici a dispersione +@msg Text options: {e} Opzioni testo: {e} +@msg `align` is start, center or end `align` è start, center o end +@msg `anchors` must be an array of [subpath, anchor] pairs `anchors` deve essere un array di coppie [subpath, anchor] +@msg `anchors` must list anchor indices (0, 1, 2…), not {k} `anchors` deve elencare indici di punti di ancoraggio (0, 1, 2…), non {k} +@msg `anchors` must list anchor indices (0, 1, 2…), not {v} `anchors` deve elencare indici di punti di ancoraggio (0, 1, 2…), non {v} +@msg `artboards` must be a list of artboard indices `artboards` deve essere un elenco di indici di tavole da disegno +@msg `asset` is the asset id (see assets.list) `asset` è l'id della risorsa (vedi assets.list) +@msg `at` must be a byte offset `at` deve essere un offset in byte +@msg `autoLeading` must be a number `autoLeading` deve essere un numero +@msg `background` must be "", "matte" or #rrggbb `background` deve essere "", "matte" o #rrggbb +@msg `burasagari` must be "none", "standard" or "forced" `burasagari` deve essere "none", "standard" o "forced" +@msg `center` must be [x, y] `center` deve essere [x, y] +@msg `charAlign` must be "romanBaseline", "emBoxTop", "emBoxCenter", "emBoxBottom", "icfTop" or "icfBottom" `charAlign` deve essere "romanBaseline", "emBoxTop", "emBoxCenter", "emBoxBottom", "icfTop" o "icfBottom" +@msg `charDirection` must be "default", "leftToRight" or "rightToLeft" `charDirection` deve essere "default", "leftToRight" o "rightToLeft" +@msg `clearStroke` requires `stroke: false`, `ids: []` and no `stop` `clearStroke` richiede `stroke: false`, `ids: []` e nessun `stop` +@msg `clear` must be a list of type object ids, not {v} `clear` deve essere un elenco di id di oggetti di testo, non {v} +@msg `clear` must list type objects; {_1} is not one `clear` deve elencare oggetti di testo; {_1} non lo è +@msg `clear` must not list `id` `clear` non deve elencare `id` +@msg `color` needs a `rule` to make the theme from `color` richiede una `rule` da cui creare il tema +@msg `colors` is empty `colors` è vuoto +@msg `columnType` is vertical, uniform, repeating or sliding `columnType` è vertical, uniform, repeating o sliding +@msg `columns` must be a whole number 1..50 `columns` deve essere un numero intero 1..50 +@msg `commands` must not be empty `commands` non deve essere vuoto +@msg `contents` goes with `to` `contents` va usato con `to` +@msg `corners` must be sharp, round or square, not `{other}` `corners` deve essere sharp, round o square, non `{other}` +@msg `corners` must list anchor indices (0, 1, 2…), not {v} `corners` deve elencare indici di punti di ancoraggio (0, 1, 2…), non {v} +@msg `count` must be a whole number from 1 to 100 `count` deve essere un numero intero compreso tra 1 e 100 +@msg `dataBase64` is larger than the module limit `dataBase64` supera il limite del modulo +@msg `dataBase64` is not valid base64 `dataBase64` non è un base64 valido +@msg `defaultsOnly` cannot be combined with `ids`, `reverse` or `append` `defaultsOnly` non può essere combinato con `ids`, `reverse` o `append` +@msg `depth` and `childLimit` slice the summary: pass `summary: true` `depth` e `childLimit` riducono il riepilogo: passa `summary: true` +@msg `design` is a design name, or null for plain columns `design` è il nome di un design, o null per colonne semplici +@msg `design` is a design name, or null for the default marker `design` è il nome di un design, o null per il marcatore predefinito +@msg `destinationFolder` is required when destination is `folder` `destinationFolder` è obbligatorio quando la destinazione è `folder` +@msg `destination` must be `none`, `saveAndClose` or `folder` `destination` deve essere `none`, `saveAndClose` o `folder` +@msg `diameter` must be from 0.1 to 2500 pt `diameter` deve essere compreso tra 0.1 e 2500 pt +@msg `digits` must be one of {_1} `digits` deve essere uno tra {_1} +@msg `dimImages` is a percentage or false `dimImages` è una percentuale o false +@msg `direction` must be "auto", "leftToRight" or "rightToLeft" `direction` deve essere "auto", "leftToRight" o "rightToLeft" +@msg `each` is the value one design stands for, more than 0 `each` è il valore rappresentato da un design, maggiore di 0 +@msg `end` must be "start" or "end" `end` deve essere "start" o "end" +@msg `ends` applies to joining exactly two paths `ends` si applica all'unione di esattamente due tracciati +@msg `event` must be one of {_1} `event` deve essere uno tra {_1} +@msg `every` must be "all", "odd" or "even", not {v} `every` deve essere "all", "odd" o "even", non {v} +@msg `expand` must be true or false `expand` deve essere true o false +@msg `files` must be 1 to 1000 paths of font files the last text.findFontFiles search found `files` deve contenere da 1 a 1000 percorsi di file di font trovati dall'ultima ricerca text.findFontFiles +@msg `folder` must be an absolute folder path `folder` deve essere il percorso assoluto di una cartella +@msg `fontAxes` must be an object of axis tags and values, such as {"wght"} `fontAxes` deve essere un oggetto di tag di assi e valori, ad esempio {"wght"} +@msg `format` must be a string `format` deve essere una stringa +@msg `format` must be one of {_1} `format` deve essere uno tra {_1} +@msg `fractions` is chop or scale `fractions` è chop o scale +@msg `from` and `to` must differ `from` e `to` devono essere diversi +@msg `from` has an empty frame `from` ha una cornice vuota +@msg `from` must be a non-negative integer `from` deve essere un intero non negativo +@msg `from` must be area type or type on a path `from` deve essere un testo in area o un testo su tracciato +@msg `gid` must be a glyph id, not {v} `gid` deve essere un id di glifo, non {v} +@msg `glyphAlternate` belongs to one character, not to a character style (text.setGlyphAlternate sets it) `glyphAlternate` riguarda un singolo carattere, non uno stile di carattere (lo imposta text.setGlyphAlternate) +@msg `guide` must be a guide index (guide.list) `guide` deve essere un indice di guida (guide.list) +@msg `hAlign` must be default, left, center or right `hAlign` deve essere default, left, center o right +@msg `id` must be a node id `id` deve essere un id di nodo +@msg `id` must be an object id, not {v} `id` deve essere un id di oggetto, non {v} +@msg `ids` must name Objects on Path groups or objects in them `ids` deve indicare gruppi Oggetti su tracciato o oggetti al loro interno +@msg `ids` takes at most {MAX_INSPECT_IDS} ids `ids` accetta al massimo {MAX_INSPECT_IDS} id +@msg `imageMap` must be none, rectangle or polygon `imageMap` deve essere none, rectangle o polygon +@msg `includeArtboardFill` must be true or false `includeArtboardFill` deve essere true o false +@msg `includeSubdirs` must be true or false `includeSubdirs` deve essere true o false +@msg `index` must be a point index `index` deve essere un indice di punto +@msg `index` must be a whole number or null `index` deve essere un numero intero o null +@msg `indices` must be a non-empty array of item indices `indices` deve essere un array non vuoto di indici di elementi +@msg `items` is a list of {kind, item} `items` è un elenco di {kind, item} +@msg `items` is empty `items` è vuoto +@msg `items` must be a list of {id, points} `items` deve essere un elenco di {id, points} +@msg `items` must be a list of {{id, anchors?}}, not {v} `items` deve essere un elenco di {{id, anchors?}}, non {v} +@msg `items` must be an array of {id, anchors} `items` deve essere un array di {id, anchors} +@msg `items` must be an array of {{id, corners?}}, not {v} `items` deve essere un array di {{id, corners?}}, non {v} +@msg `kashida` must be "none", "short", "medium", "long" or "stylistic" `kashida` deve essere "none", "short", "medium", "long" o "stylistic" +@msg `kind` is one of {_1} `kind` è uno tra {_1} +@msg `kind` must be "round", "invertedRound" or "chamfer", not {v} `kind` deve essere "round", "invertedRound" o "chamfer", non {v} +@msg `kind` must be `text`, `visibility`, `image` or `graph` `kind` deve essere `text`, `visibility`, `image` o `graph` +@msg `kind` must be a string `kind` deve essere una stringa +@msg `kind` must be image, noImage or htmlText `kind` deve essere image, noImage o htmlText +@msg `kinsoku` must be "none", "hard" or "soft" `kinsoku` deve essere "none", "hard" o "soft" +@msg `label` must be a non-empty name of at most 200 characters `label` deve essere un nome non vuoto di al massimo 200 caratteri +@msg `language` must be a language name or tag, such as "Turkish" or "tr" `language` deve essere il nome o il tag di una lingua, ad esempio "Turkish" o "tr" +@msg `leadingModel` must be "romanBaseline" or "emBoxTop" `leadingModel` deve essere "romanBaseline" o "emBoxTop" +@msg `leading` must be from 0.1 to 5000, or null for Auto `leading` deve essere compreso tra 0.1 e 5000, o null per Auto +@msg `length` must be a number of at most 1000000 pt either way `length` deve essere un numero di al massimo 1000000 pt in entrambe le direzioni +@msg `line` must be true or false `line` deve essere true o false +@msg `minAngle` ({min_angle}°) can't be more than `maxAngle` ({max_angle}°) `minAngle` ({min_angle}°) non può essere maggiore di `maxAngle` ({max_angle}°) +@msg `mode` must be "handles", "straight" or "smooth" `mode` deve essere "handles", "straight" o "smooth" +@msg `mode` must be "straight", "bezier" or "arc" `mode` deve essere "straight", "bezier" o "arc" +@msg `mode` must be one of {_1} `mode` deve essere uno tra {_1} +@msg `mojikumi` must be "none", "yakumonoHankaku", "gyomatsuYakumonoHankaku", "gyomatsuYakumonoZenkaku", "yakumonoZenkaku" or "lineEndHalf" `mojikumi` deve essere "none", "yakumonoHankaku", "gyomatsuYakumonoHankaku", "gyomatsuYakumonoZenkaku", "yakumonoZenkaku" o "lineEndHalf" +@msg `names` is a list of preset names `names` è un elenco di nomi di predefiniti +@msg `names` lists preset names `names` elenca nomi di predefiniti +@msg `names` must be a list of names `names` deve essere un elenco di nomi +@msg `names` must be a list of swatch names `names` deve essere un elenco di nomi di campioni +@msg `names` must be an array of style names `names` deve essere un array di nomi di stili +@msg `new` is a design name, or true `new` è il nome di un design, o true +@msg `path` must be a folder `path` deve essere una cartella +@msg `path` must be a path object `path` deve essere un oggetto tracciato +@msg `pivot` must be a whole number from 0 to 8 `pivot` deve essere un numero intero compreso tra 0 e 8 +@msg `points` must be a list of [x, y] points `points` deve essere un elenco di punti [x, y] +@msg `points` must be a list of point indexes `points` deve essere un elenco di indici di punti +@msg `points` must be an array of point indices `points` deve essere un array di indici di punti +@msg `points` must be point indexes `points` deve contenere indici di punti +@msg `port` must be "in" or "out" `port` deve essere "in" o "out" +@msg `position` is above or below `position` è above o below +@msg `preset` must be a string `preset` deve essere una stringa +@msg `ratio` must be from 0.1 to 0.5 `ratio` deve essere compreso tra 0.1 e 0.5 +@msg `rect` must be [x, y, width, height] with a positive size `rect` deve essere [x, y, width, height] con una dimensione positiva +@msg `relation` must be overlap, inside or outside `relation` deve essere overlap, inside o outside +@msg `reversed` must be true or false `reversed` deve essere true o false +@msg `rotate` must be a finite number `rotate` deve essere un numero finito +@msg `rule` must be nonZero or evenOdd `rule` deve essere nonZero o evenOdd +@msg `scale` must be a number in (0, 100] `scale` deve essere un numero in (0, 100] +@msg `scope` must be "selection" or "document", not {other} `scope` deve essere "selection" o "document", non {other} +@msg `script` must be a string `script` deve essere una stringa +@msg `seriesIndexes` is a non-empty list of series indexes `seriesIndexes` è un elenco non vuoto di indici di serie +@msg `seriesIndexes` needs a `type` or a `valueAxis` `seriesIndexes` richiede un `type` o un `valueAxis` +@msg `shift` must be a number of points `shift` deve essere un numero di punti +@msg `showCenter` must be true or false `showCenter` deve essere true o false +@msg `singleWordJustification` must be "fullJustify", "alignLeft", "alignCenter" or "alignRight" `singleWordJustification` deve essere "fullJustify", "alignLeft", "alignCenter" o "alignRight" +@msg `size` is a percentage of the base text's size, 1 to 100 `size` è una percentuale della dimensione del testo di base, da 1 a 100 +@msg `source` and `target` are the same object `source` e `target` sono lo stesso oggetto +@msg `source` must be `folder` or `datasets` `source` deve essere `folder` o `datasets` +@msg `spacing` must be a number `spacing` deve essere un numero +@msg `spacing` must be a number of points, 0 or more, or "auto" `spacing` deve essere un numero di punti, pari a 0 o maggiore, o "auto" +@msg `start` must be a finite number `start` deve essere un numero finito +@msg `starts` must be an array of anchor indices (or nulls) `starts` deve essere un array di indici di punti di ancoraggio (o null) +@msg `status` must be linked, embedded, missing or modified, or a list of them `status` deve essere linked, embedded, missing o modified, o un elenco di questi +@msg `stop` must be a stop index `stop` deve essere un indice di interruzione +@msg `story` styles the whole story: give no `start` or `end` `story` applica lo stile all'intero brano: non indicare `start` o `end` +@msg `story` takes the whole story: give no `start` or `end` `story` prende l'intero brano: non indicare `start` o `end` +@msg `strokeMode` applies to strokes (stroke: true) `strokeMode` si applica alle tracce (stroke: true) +@msg `style` must be roman or japanese `style` deve essere roman o japanese +@msg `subpaths` must be two integer indices `subpaths` deve essere due indici interi +@msg `symbol` must be a symbol name `symbol` deve essere il nome di un simbolo +@msg `target` must be "object" or "contents", not {v} `target` deve essere "object" o "contents", non {v} +@msg `tint` applies to global and spot colours `tint` si applica ai colori globali e spot +@msg `tint` goes with a global or spot `swatch` `tint` va usato con un `swatch` globale o spot +@msg `tint` must be a number (%) `tint` deve essere un numero (%) +@msg `tint` needs a global or spot colour `tint` richiede un colore globale o spot +@msg `to` is already threaded `to` è già concatenato +@msg `to` is the design's new name `to` è il nuovo nome del design +@msg `to` must be [x, y] `to` deve essere [x, y] +@msg `to` must be a closed path, area type or type on a path other than `from` `to` deve essere un tracciato chiuso, un testo in area o un testo su tracciato diverso da `from` +@msg `to` must be a whole number `to` deve essere un numero intero +@msg `to` takes the rows of one object's stack, not aligned rows `to` accetta le righe della pila di un solo oggetto, non righe allineate +@msg `useLab` must be true or false `useLab` deve essere true o false +@msg `vAlign` must be default, top, middle, baseline or bottom `vAlign` deve essere default, top, middle, baseline o bottom +@msg `values` must be a list `values` deve essere un elenco +@msg `values` must be an object `values` deve essere un oggetto +@msg `values` must name hex, rgb, cmyk, lab or gray `values` deve indicare hex, rgb, cmyk, lab o gray +@msg `values` names a kind twice `values` indica due volte lo stesso tipo +@msg `{_1}` can't be edited or removed `{_1}` non può essere modificato o rimosso +@msg `{_1}` is built in and always listed `{_1}` è incorporato ed è sempre elencato +@msg `{_1}` is not a font file the last text.findFontFiles search found `{_1}` non è un file di font trovato dall'ultima ricerca text.findFontFiles +@msg `{_1}` would be overwritten by the package: choose a different folder or name `{_1}` verrebbe sovrascritto dal pacchetto: scegli una cartella o un nome diverso +@msg `{file}` has no art to place `{file}` non ha grafica da inserire +@msg `{file}` is over {_1} MB `{file}` supera {_1} MB +@msg `{f}` belongs to a VectorCraft that is running `{f}` appartiene a un VectorCraft in esecuzione +@msg `{f}` is the copy of a document open now `{f}` è la copia di un documento aperto in questo momento +@msg `{id}` is no writable format (see document.formats) `{id}` non è un formato scrivibile (vedi document.formats) +@msg `{id}` isn't a loaded library `{id}` non è una libreria caricata +@msg `{key}` has no colours `{key}` non ha colori +@msg `{key}` is a design name `{key}` è il nome di un design +@msg `{key}` is a design name or a list of them `{key}` è il nome di un design o un elenco di nomi +@msg `{key}` is longer than {MAX_NAME} characters `{key}` supera {MAX_NAME} caratteri +@msg `{key}` is out of range `{key}` è fuori intervallo +@msg `{key}` must be [min, desired, max] or {{min?, desired?, max?}}, in percent `{key}` deve essere [min, desired, max] o {{min?, desired?, max?}}, in percentuale +@msg `{key}` must be a non-negative integer `{key}` deve essere un intero non negativo +@msg `{key}` must be a number {_1}..{_2} `{key}` deve essere un numero {_1}..{_2} +@msg `{key}` must be a whole number of at least 0, not {n} `{key}` deve essere un numero intero pari almeno a 0, non {n} +@msg `{key}` must be an appearance item index (paint order) or null `{key}` deve essere un indice di voce dell'aspetto (ordine di pittura) o null +@msg `{key}` must be an array of asset ids (see assets.list) `{key}` deve essere un array di id di risorse (vedi assets.list) +@msg `{key}` must be from 0 to 180 degrees, not {a} `{key}` deve essere compreso tra 0 e 180 gradi, non {a} +@msg `{key}` must be from {_1} to {shift_max} `{key}` deve essere compreso tra {_1} e {shift_max} +@msg `{key}`: not a colour: {v} `{key}`: non è un colore: {v} +@msg `{k}.family` is required `{k}.family` è obbligatorio +@msg `{k}` is a number 0..1 `{k}` è un numero 0..1 +@msg `{k}` is true or false `{k}` è true o false +@msg `{k}` isn't an export setting: {_1} `{k}` non è un'impostazione di esportazione: {_1} +@msg `{k}` must be a byte offset `{k}` deve essere un offset in byte +@msg `{k}` must be a number `{k}` deve essere un numero +@msg `{k}` must be a number of points `{k}` deve essere un numero di punti +@msg `{k}` must be a string `{k}` deve essere una stringa +@msg `{k}` must be a whole number `{k}` deve essere un numero intero +@msg `{k}` must be from {lo} to {hi} for {key} `{k}` deve essere compreso tra {lo} e {hi} per {key} +@msg `{k}` must be {what}, not {v} `{k}` deve essere {what}, non {v} +@msg `{l}` isn't a global or spot colour `{l}` non è un colore globale o spot +@msg `{m}` isn't a bitmap: use clipboard.importSvg, importPdf or importText `{m}` non è una bitmap: usa clipboard.importSvg, importPdf o importText +@msg `{name}` can't be deleted `{name}` non può essere eliminato +@msg `{name}` can't be duplicated `{name}` non può essere duplicato +@msg `{name}` can't be edited `{name}` non può essere modificato +@msg `{name}` can't be edited this way `{name}` non può essere modificato in questo modo +@msg `{name}` can't be renamed `{name}` non può essere rinominato +@msg `{name}` can't name a folder `{name}` non può essere il nome di una cartella +@msg `{name}` has no art to place `{name}` non ha grafica da inserire +@msg `{name}` has no text to place `{name}` non ha testo da inserire +@msg `{name}` in the Word document is not UTF-8 or UTF-16 text `{name}` nel documento Word non è testo UTF-8 o UTF-16 +@msg `{name}` in the Word document is not well-formed XML: {e} `{name}` nel documento Word non è XML ben formato: {e} +@msg `{name}` in the Word document nests elements more than {MAX_NESTING} deep `{name}` nel documento Word annida elementi a più di {MAX_NESTING} livelli di profondità +@msg `{name}` is a built-in preset and is read-only: save it under another name `{name}` è un predefinito incorporato ed è di sola lettura: salvalo con un altro nome +@msg `{name}` is a built-in preset: save under another name `{name}` è un predefinito incorporato: salva con un altro nome +@msg `{name}` is a built-in profile and stays `{name}` è un profilo incorporato e rimane +@msg `{name}` is a tint of `{base}`: edit `{base}` (a tint swatch only takes a new name) `{name}` è una tinta di `{base}`: modifica `{base}` (un campione di tinta accetta solo un nuovo nome) +@msg `{name}` is a working space: choose another with edit.colorSettings first `{name}` è uno spazio di lavoro: scegline prima un altro con edit.colorSettings +@msg `{name}` is built in and stays `{name}` è incorporato e rimane +@msg `{name}` takes a paint of its own kind (colour, gradient or pattern) `{name}` accetta una pittura del proprio tipo (colore, sfumatura o motivo) +@msg `{n}` can't be deleted `{n}` non può essere eliminato +@msg `{n}` can't be merged `{n}` non può essere unito +@msg `{n}` can't be moved `{n}` non può essere spostato +@msg `{n}` is not a colour swatch `{n}` non è un campione di colore +@msg `{n}` isn't a solid colour `{n}` non è un colore pieno +@msg `{n}`: colour groups hold solid colours only `{n}`: i gruppi di colori contengono solo colori pieni +@msg `{other}` doesn't trace an image `{other}` non ricalca un'immagine +@msg `{s}`: the step's references copy in more than {_1} MB of values `{s}`: i riferimenti del passo copiano più di {_1} MB di valori +@msg `{tag}` is not an axis tag: a tag is four characters, such as "wght" `{tag}` non è un tag di asse: un tag è di quattro caratteri, ad esempio "wght" +@msg `{to}` is empty or already used `{to}` è vuoto o già usato +@msg `{what}` is longer than {MAX_TEXT} bytes `{what}` supera {MAX_TEXT} byte +@msg `{what}` must be text `{what}` deve essere testo +@msg a DXF drawing has one origin: pass useArtboards: true for one drawing per artboard un disegno DXF ha una sola origine: passa useArtboards: true per un disegno per tavola da disegno +@msg a PDF presets file with over {_1} presets un file di predefiniti PDF con più di {_1} predefiniti +@msg a batch preview cannot switch documents un'anteprima batch non può cambiare documento +@msg a brush named `{nn}` exists esiste un pennello chiamato `{nn}` +@msg a container of the listed objects is gone un contenitore degli oggetti elencati non esiste più +@msg a content stream can't be read impossibile leggere uno stream di contenuto +@msg a content stream isn't a stream uno stream di contenuto non è uno stream +@msg a design holds at most {_1} objects un design contiene al massimo {_1} oggetti +@msg a design needs a name un design richiede un nome +@msg a dictionary grew too large un dizionario è diventato troppo grande +@msg a document keeps at most {_1} graph designs un documento conserva al massimo {_1} design di grafico +@msg a document keeps at most {_1} saved selections un documento conserva al massimo {_1} selezioni salvate +@msg a document keeps at most {_1} views un documento conserva al massimo {_1} viste +@msg a document links at most {MAX_SVG_SCRIPTS} JavaScript files un documento collega al massimo {MAX_SVG_SCRIPTS} file JavaScript +@msg a feConvolveMatrix filter's order is {x}×{y}, which overflows the kernel-size multiply usvg makes l'ordine di un filtro feConvolveMatrix è {x}×{y}, che fa traboccare la moltiplicazione della dimensione del kernel eseguita da usvg +@msg a gradient preset is already called {n} esiste già un predefinito di sfumatura chiamato {n} +@msg a graph has at most {_1} series, so it transposes with at most that many categories un grafico ha al massimo {_1} serie, quindi si traspone con al massimo altrettante categorie +@msg a layer can't be merged into its own sublayer un livello non può essere unito a un proprio sottolivello +@msg a layer's form box isn't an array il riquadro del form di un livello non è un array +@msg a layer's form can't be read impossibile leggere il form di un livello +@msg a layer's form has no box il form di un livello non ha un riquadro +@msg a layer's form has no dictionary il form di un livello non ha un dizionario +@msg a legend draws at most {MAX_CHIPS} swatches una legenda disegna al massimo {MAX_CHIPS} campioni +@msg a name names one asset: give multiple: false, or one object un nome indica una sola risorsa: indica multiple: false, o un solo oggetto +@msg a page can't be read impossibile leggere una pagina +@msg a page object is used by something else un oggetto pagina è usato da qualcos'altro +@msg a page thumbnail must be RGB pixels, 1 to {THUMBNAIL_SIZE} a side (got {w} × {h}) una miniatura di pagina deve essere in pixel RGB, da 1 a {THUMBNAIL_SIZE} per lato (ricevuto {w} × {h}) +@msg a pattern layer doesn't end what it starts un livello di motivo non chiude ciò che apre +@msg a pattern layer's colors hold something else i colori di un livello di motivo contengono qualcos'altro +@msg a pattern named `{n}` exists esiste un motivo chiamato `{n}` +@msg a plug-in with id {_1} is already installed un plug-in con id {_1} è già installato +@msg a preset named `{n}` exists esiste un predefinito chiamato `{n}` +@msg a print presets file with over {_1} presets un file di predefiniti di stampa con più di {_1} predefiniti +@msg a profile is already called {n} esiste già un profilo chiamato {n} +@msg a row can't go into itself una riga non può entrare in se stessa +@msg a series goes on the left or the right value axis una serie va sull'asse dei valori sinistro o destro +@msg a start must be an anchor index ≥ 0 or null un inizio deve essere un indice di punto di ancoraggio ≥ 0 o null +@msg a style named `{name}` already exists esiste già uno stile chiamato `{name}` +@msg a style named `{to}` already exists esiste già uno stile chiamato `{to}` +@msg a swatch needs a colour, gradient or pattern un campione richiede un colore, una sfumatura o un motivo +@msg a symbol named `{n}` already exists esiste già un simbolo chiamato `{n}` +@msg a theme has 1 to {THEME_SIZE} colours un tema ha da 1 a {THEME_SIZE} colori +@msg a theme is already called {name} (replace: "{name}" overwrites it) esiste già un tema chiamato {name} (replace: "{name}" lo sovrascrive) +@msg a toolbar name has at most {MAX_NAME} characters il nome di una barra degli strumenti ha al massimo {MAX_NAME} caratteri +@msg a view named `{_1}` exists esiste una vista chiamata `{_1}` +@msg a view named `{name}` exists esiste una vista chiamata `{name}` +@msg a workspace keeps at most {MAX} toolbars un'area di lavoro conserva al massimo {MAX} barre degli strumenti +@msg a {_1} series can't go in a {_2} graph una serie {_1} non può andare in un grafico {_2} +@msg aiVersion {v}: "pdf" (PDF-compatible, the default) or "7" (legacy .ai, version 7) aiVersion {v}: "pdf" (compatibile PDF, il valore predefinito) o "7" (.ai legacy, versione 7) +@msg already editing an opacity mask stai già modificando una maschera di opacità +@msg already editing another object's opacity mask stai già modificando la maschera di opacità di un altro oggetto +@msg already in pattern editing mode già in modalità di modifica motivo +@msg an artboard is listed twice una tavola da disegno è elencata due volte +@msg an empty artboard has no width or height to scale to una tavola da disegno vuota non ha larghezza o altezza a cui adattarsi +@msg an image is too large for DXF and was left out un'immagine è troppo grande per DXF ed è stata esclusa +@msg an image of {_1} MB, at most {_2} MB un'immagine di {_1} MB, al massimo {_2} MB +@msg an image of {declared_w} × {declared_h} pixels is too large to draw un'immagine di {declared_w} × {declared_h} pixel è troppo grande da disegnare +@msg an image of {width} × {height} pixels is too large to draw un'immagine di {width} × {height} pixel è troppo grande da disegnare +@msg an index must be a whole number un indice deve essere un numero intero +@msg an inline graphic has no glyphs una grafica in linea non ha glifi +@msg an invisible text form can't be read impossibile leggere un form di testo invisibile +@msg an object is missing manca un oggetto +@msg an object's header is missing manca l'intestazione di un oggetto +@msg an overprinting form can't be read impossibile leggere un form in sovrastampa +@msg an unread object has no readable bounds un oggetto non letto non ha limiti leggibili +@msg an unread object holds a frame of threaded type un oggetto non letto contiene una cornice di testo concatenato +@msg an unread object is missing from its layer un oggetto non letto manca dal suo livello +@msg anchor `smooth` must be true or false `smooth` del punto di ancoraggio deve essere true o false +@msg anchor `{key}` must be [x, y] `{key}` del punto di ancoraggio deve essere [x, y] +@msg anchor `{key}` must be a number `{key}` del punto di ancoraggio deve essere un numero +@msg angle and roundness must be finite angle e roundness devono essere finiti +@msg angles must be a list as long as pins (degrees or null) angles deve essere un elenco lungo quanto pins (gradi o null) +@msg appearance must be flat|center|edge appearance deve essere flat|center|edge +@msg apply is "fill" or "stroke", not `{other}` apply è "fill" o "stroke", non `{other}` +@msg area must be a closed polygon of 3 to 100000 [x, y] points area deve essere un poligono chiuso da 3 a 100000 punti [x, y] +@msg area type needs a path that encloses an area il testo in area richiede un tracciato che racchiuda un'area +@msg area type {_1} is named twice il testo in area {_1} è indicato due volte +@msg arrowAlign must be extend|tip, got {o} arrowAlign deve essere extend|tip, ricevuto {o} +@msg arrowScale must be [start, end] arrowScale deve essere [start, end] +@msg artboard is a 0-based index artboard è un indice a partire da 0 +@msg artboard must be a 0-based index artboard deve essere un indice a partire da 0 +@msg artboard {i} doesn't print la tavola da disegno {i} non viene stampata +@msg artboardLayout must be {layout?, columns?, spacing?, rightToLeft?} artboardLayout deve essere {layout?, columns?, spacing?, rightToLeft?} +@msg artboardLayout.columns must be 1 or more artboardLayout.columns deve essere 1 o maggiore +@msg artboardLayout.layout must be gridByRow, gridByColumn, row or column artboardLayout.layout deve essere gridByRow, gridByColumn, row o column +@msg artboardLayout.rightToLeft must be true or false artboardLayout.rightToLeft deve essere true o false +@msg artboardLayout.spacing must be 0–{MAX_SPACING} pt artboardLayout.spacing deve essere 0–{MAX_SPACING} pt +@msg artboards must be a non-empty list artboards deve essere un elenco non vuoto +@msg artboards must be a number artboards deve essere un numero +@msg asset `{_1}` has no art to export la risorsa `{_1}` non ha grafica da esportare +@msg at most {MAX_KEYWORDS} keywords al massimo {MAX_KEYWORDS} parole chiave +@msg at most {MAX_MATCHES} matches are kept vengono conservate al massimo {MAX_MATCHES} corrispondenze +@msg at most {MAX_POINTS} points al massimo {MAX_POINTS} punti +@msg at most {MAX_QUEUE} files al massimo {MAX_QUEUE} file +@msg at must be [x, y] at deve essere [x, y] +@msg at/rect lie outside the canvas at/rect si trovano fuori dall'area di lavoro +@msg attribute must be size|leading|tracking|kerning|baselineShift attribute deve essere size|leading|tracking|kerning|baselineShift +@msg attrs must be an object attrs deve essere un oggetto +@msg automation path rejected: the folder's name isn't valid text percorso di automazione rifiutato: il nome della cartella non è testo valido +@msg automation path rejected: {_1} isn't valid text percorso di automazione rifiutato: {_1} non è testo valido +@msg automation path rejected: {why}: {path} percorso di automazione rifiutato: {why}: {path} +@msg axis must be "vertical", "horizontal" or an angle in degrees, not `{given}` axis deve essere "vertical", "horizontal" o un angolo in gradi, non `{given}` +@msg backgroundContents must be transparent or white backgroundContents deve essere transparent o white +@msg bad AES key chiave AES non valida +@msg bad `runs`: {e} `runs` non valido: {e} +@msg bad arrowScale arrowScale non valido +@msg bad base64 base64 non valido +@msg bad color {c} colore non valido {c} +@msg bad colour colore non valido +@msg bad colour in `colors` colore non valido in `colors` +@msg bad colour {v} colore non valido {v} +@msg bad colour {v} in groupColors colore non valido {v} in groupColors +@msg bad cross-reference entry voce di riferimento incrociato non valida +@msg bad cross-reference table tabella dei riferimenti incrociati non valida +@msg bad current colour {v} colore corrente non valido {v} +@msg bad dataBase64 dataBase64 non valido +@msg bad edit modifica non valida +@msg bad fill {v} riempimento non valido {v} +@msg bad fit: {e} adattamento non valido: {e} +@msg bad model {other} (keep, new or document) modello non valido {other} (keep, new o document) +@msg bad new colour {v} nuovo colore non valido {v} +@msg bad object oggetto non valido +@msg bad object number numero di oggetto non valido +@msg bad offset offset non valido +@msg bad path data: {e} dati del tracciato non validi: {e} +@msg bad point {v}: expected [x, y] or [x, y, pressure] punto non valido {v}: previsto [x, y] o [x, y, pressure] +@msg bad reference riferimento non valido +@msg bad trailer size dimensione del trailer non valida +@msg bad {k} colour colore {k} non valido +@msg batches cannot nest i batch non possono essere annidati +@msg bind something to a variable first prima collega qualcosa a una variabile +@msg bleed must be 0–{MAX_BLEED} pt: a number, [top, bottom, left, right] or {{top, bottom, left, right}} bleed deve essere 0–{MAX_BLEED} pt: un numero, [top, bottom, left, right] o {{top, bottom, left, right}} +@msg brush must be a name or null brush deve essere un nome o null +@msg by must be a number by deve essere un numero +@msg can't add page thumbnails: {why} impossibile aggiungere le miniature delle pagine: {why} +@msg can't add the editing data for other apps: {why} impossibile aggiungere i dati di modifica per altre app: {why} +@msg can't edit the written PDF: {why} impossibile modificare il PDF scritto: {why} +@msg can't encrypt the PDF: {why} impossibile crittografare il PDF: {why} +@msg can't linearise the PDF: {why} impossibile linearizzare il PDF: {why} +@msg can't open `{_1}`: {_2} impossibile aprire `{_1}`: {_2} +@msg can't open `{_1}`: {e} impossibile aprire `{_1}`: {e} +@msg can't open `{name}`: not a format VectorCraft reads (see document.formats) impossibile aprire `{name}`: non è un formato che VectorCraft legge (vedi document.formats) +@msg can't place `{name}`: not a format VectorCraft reads (see document.formats) impossibile inserire `{name}`: non è un formato che VectorCraft legge (vedi document.formats) +@msg can't read back the written PDF: {e} impossibile rileggere il PDF scritto: {e} +@msg can't read this Word document: {why} impossibile leggere questo documento Word: {why} +@msg can't write .{e} images: use .png, .jpg, .gif, .webp, .tif or .bmp impossibile scrivere immagini .{e}: usa .png, .jpg, .gif, .webp, .tif o .bmp +@msg can't write PDF layers, overprint or invisible text: {why} impossibile scrivere livelli PDF, sovrastampa o testo invisibile: {why} +@msg can't write the file's copyright: {why} impossibile scrivere il copyright del file: {why} +@msg can't write the file's modification date: {why} impossibile scrivere la data di modifica del file: {why} +@msg can't write the pages' user units: {why} impossibile scrivere le unità utente delle pagine: {why} +@msg cannot distort a zero-size or non-finite bounding box impossibile distorcere un riquadro di delimitazione di dimensione zero o non finito +@msg cannot move an object into itself impossibile spostare un oggetto in se stesso +@msg case must be upper|lower|title|sentence case deve essere upper|lower|title|sentence +@msg category `{c}`: one of {_1} categoria `{c}`: una tra {_1} +@msg center must be [x, y] center deve essere [x, y] +@msg choose at least one artboard scegli almeno una tavola da disegno +@msg choose at least one status scegli almeno uno stato +@msg clip must be true or false clip deve essere true o false +@msg clippingMask must be true or false clippingMask deve essere true o false +@msg closed must be a boolean closed deve essere un booleano +@msg collapsed must be true or false collapsed deve essere true o false +@msg color.profile: no RGB or CMYK profile is called “{profile}” color.profile: nessun profilo RGB o CMYK si chiama “{profile}” +@msg colorMode must be rgb or cmyk, not `{m}` colorMode deve essere rgb o cmyk, non `{m}` +@msg colorModel must be {own} (the document's mode), grayscale or bitmap colorModel deve essere {own} (il metodo del documento), grayscale o bitmap +@msg colour groups can't go into colour groups i gruppi di colori non possono entrare in altri gruppi di colori +@msg colour, mode and spot apply to solid-colour swatches only colore, metodo e spot si applicano solo ai campioni di colore pieno +@msg column {column}: `{cell}` is a `%` column of graph data files, which are not read colonna {column}: `{cell}` è una colonna `%` dei file di dati dei grafici, che non vengono letti +@msg column {column}: `{cell}` names `{name}`, which is a {_1} variable colonna {column}: `{cell}` indica `{name}`, che è una variabile {_1} +@msg column {column}: `{cell}` names no variable colonna {column}: `{cell}` non indica alcuna variabile +@msg column {column}: the variable name is longer than {MAX_NAME} characters colonna {column}: il nome della variabile supera {MAX_NAME} caratteri +@msg column: true floats the whole column and takes no onto, above or below column: true rende mobile l'intera colonna e non accetta onto, above o below +@msg command must be a shape.* command command deve essere un comando shape.* +@msg commands[{i}]: missing `command` commands[{i}]: `command` mancante +@msg composer must be "singleLine" or "everyLine" composer deve essere "singleLine" o "everyLine" +@msg compound shapes take paths, compound paths, groups, compound shapes, type, blends and envelopes le forme composte accettano tracciati, tracciati composti, gruppi, forme composte, testo, fusioni e involucri +@msg compound {_1} must contain exactly one open path to join il composto {_1} deve contenere esattamente un tracciato aperto da unire +@msg connect end must be "start" or "end" end di connect deve essere "start" o "end" +@msg connect needs a path other than the one extended connect richiede un tracciato diverso da quello esteso +@msg connect needs id connect richiede id +@msg connect path has no open end il tracciato di connect non ha un'estremità aperta +@msg copy needs path: the file to copy into the document's folder copy richiede path: il file da copiare nella cartella del documento +@msg copy takes images only: relink placed documents where their files are copy accetta solo immagini: ricollega i documenti inseriti dove si trovano i loro file +@msg copying a file isn't available on the web la copia di un file non è disponibile sul web +@msg copyrightStatus must be unknown, copyrighted or publicDomain copyrightStatus deve essere unknown, copyrighted o publicDomain +@msg copyrightUrl must be a URL (no spaces, at most 2048 characters) copyrightUrl deve essere un URL (senza spazi, al massimo 2048 caratteri) +@msg corners must be 4 [x, y] points (TL, TR, BR, BL) corners deve essere 4 punti [x, y] (TL, TR, BR, BL) +@msg counterPer `{c}`: item or file counterPer `{c}`: item o file +@msg crop `{_1}`: only PDF pages have art, trim, bleed and media boxes; use "crop" or "bounding" crop `{_1}`: solo le pagine PDF hanno i riquadri art, trim, bleed e media; usa "crop" o "bounding" +@msg crop rectangle doesn't overlap the image il rettangolo di ritaglio non si sovrappone all'immagine +@msg cross-reference and object streams aren't supported i riferimenti incrociati e gli stream di oggetti non sono supportati +@msg damaged compressed data: {e} dati compressi danneggiati: {e} +@msg dash lengths must be numbers, got {v} le lunghezze dei trattini devono essere numeri, ricevuto {v} +@msg dash must be a list of lengths or null, got {v} dash deve essere un elenco di lunghezze o null, ricevuto {v} +@msg data key `{k}`: letters, digits, -, _ and . (not name or vc-…) chiave di dati `{k}`: lettere, cifre, -, _ e . (non name o vc-…) +@msg data must be an object of key: value, not {v} data deve essere un oggetto chiave: valore, non {v} +@msg data set `{name}` holds a value for `{var}`, which the library doesn't declare il set di dati `{name}` contiene un valore per `{var}`, che la libreria non dichiara +@msg data set `{name}` holds two values for `{var}` il set di dati `{name}` contiene due valori per `{var}` +@msg data set `{name}` is declared twice il set di dati `{name}` è dichiarato due volte +@msg data takes at most {MAX_DATA} keys at once data accetta al massimo {MAX_DATA} chiavi alla volta +@msg dataset `{name}` already exists il set di dati `{name}` esiste già +@msg dataset `{to}` already exists il set di dati `{to}` esiste già +@msg date `{d}`: YYYY-MM-DD data `{d}`: YYYY-MM-DD +@msg decoded data is too large i dati decodificati sono troppo grandi +@msg define a variable first definisci prima una variabile +@msg detect must be true or false detect deve essere true o false +@msg direct-select anchor points of a path seleziona con Selezione diretta i punti di ancoraggio di un tracciato +@msg distance must be > 0 distance deve essere > 0 +@msg dx and dy give the arrow's direction dx e dy indicano la direzione della freccia +@msg dx and dy must be numbers dx e dy devono essere numeri +@msg dxf fitTo must be [width, height], positive fitTo di dxf deve essere [width, height], positivi +@msg dxf must be an object of DXF import options dxf deve essere un oggetto di opzioni di importazione DXF +@msg dxf scale must be a positive number up to {_1}, not {s} la scala di dxf deve essere un numero positivo fino a {_1}, non {s} +@msg dxf unit `{u}`: a unit such as mm, cm, in or pt unità dxf `{u}`: un'unità come mm, cm, in o pt +@msg dxf: {e} dxf: {e} +@msg each format is an object {format, scale?, suffix?} ogni formato è un oggetto {format, scale?, suffix?} +@msg each item needs an id ogni elemento richiede un id +@msg each match needs `kind` ogni corrispondenza richiede `kind` +@msg each match needs a `name` ogni corrispondenza richiede un `name` +@msg each of `fonts` is {family, style?}; 1 to 1000 of them ogni voce di `fonts` è {family, style?}; da 1 a 1000 voci +@msg each of `items` is {kind, item} ogni voce di `items` è {kind, item} +@msg each of `items` is {{id, corners?}} with an object id, not {it} ogni voce di `items` è {{id, corners?}} con un id di oggetto, non {it} +@msg each row needs `from` ogni riga richiede `from` +@msg each stop needs a position ogni interruzione richiede una posizione +@msg each subpath needs an anchors array ogni sottotracciato richiede un array anchors +@msg easingType must be easeIn, easeOut, easeInOut or easeOutIn easingType deve essere easeIn, easeOut, easeInOut o easeOutIn +@msg editingData must be true or false editingData deve essere true o false +@msg editingData must be true, false or "always" editingData deve essere true, false o "always" +@msg edits overlap le modifiche si sovrappongono +@msg effects of objects whose appearances differ can't be moved together gli effetti di oggetti con aspetti diversi non possono essere spostati insieme +@msg empty header in column {_1} intestazione vuota nella colonna {_1} +@msg encoding `{_1}`: utf8 or utf16 codifica `{_1}`: utf8 o utf16 +@msg ends must be two of "first" and "last" ends deve essere due tra "first" e "last" +@msg every numbered name for the result is taken tutti i nomi numerati per il risultato sono già usati +@msg expanded SVG styles exceed the import size limit gli stili SVG espansi superano il limite di dimensione dell'importazione +@msg export presets file is larger than {MAX_PRESET_FILE_BYTES} bytes il file dei predefiniti di esportazione supera {MAX_PRESET_FILE_BYTES} byte +@msg exportText must be editable or appearance exportText deve essere editable o appearance +@msg exportType must be movieClip or graphic, not `{s}` exportType deve essere movieClip o graphic, non `{s}` +@msg extend needs id extend richiede id +@msg featureChanges lists at most {MAX_FEATURE_CHANGES} tags featureChanges elenca al massimo {MAX_FEATURE_CHANGES} tag +@msg featureChanges must be a list of tags featureChanges deve essere un elenco di tag +@msg features must be a list of tags features deve essere un elenco di tag +@msg fidelity must be 0..100 fidelity deve essere 0..100 +@msg files must be {name, dataBase64} files deve essere {name, dataBase64} +@msg fit must be none|autoHeight|shrinkText|block, got {id} fit deve essere none|autoHeight|shrinkText|block, ricevuto {id} +@msg flattener: {e} appiattimento: {e} +@msg flattenerPreset `{name}`: high, medium, low or a saved one (see flattener.presets.list) flattenerPreset `{name}`: high, medium, low o uno salvato (vedi flattener.presets.list) +@msg flipProfile must be along|across, got {o} flipProfile deve essere along|across, ricevuto {o} +@msg folders `{f}`: none, rows, format, item or itemFormat folders `{f}`: none, rows, format, item o itemFormat +@msg folders deeper than {MAX_BATCH_DEPTH} levels cartelle più profonde di {MAX_BATCH_DEPTH} livelli +@msg folders deeper than {MAX_DEPTH} levels cartelle più profonde di {MAX_DEPTH} livelli +@msg font family `{family}` is not available la famiglia di font `{family}` non è disponibile +@msg font family `{tf}` is not available la famiglia di font `{tf}` non è disponibile +@msg format `{f}`: pdf or postscript formato `{f}`: pdf o postscript +@msg format `{f}`: text or json formato `{f}`: text o json +@msg format is `{format}` il formato è `{format}` +@msg formats must be an array of rows, not {v} formats deve essere un array di righe, non {v} +@msg frequency must be between 1 and 300 lines per inch frequency deve essere compreso tra 1 e 300 linee per pollice +@msg from and to must be finite from e to devono essere finiti +@msg from must be [x0, y0, x1, y1] from deve essere [x0, y0, x1, y1] +@msg give `before` or `after`, not both indica `before` o `after`, non entrambi +@msg give `colors`, or `color` and `rule` indica `colors`, oppure `color` e `rule` +@msg give `event`, such as onclick indica `event`, ad esempio onclick +@msg give `file`, the JavaScript file's path or URL indica `file`, il percorso o l'URL del file JavaScript +@msg give `fill` and/or `stroke` indica `fill` e/o `stroke` +@msg give `fill`, `stroke` or both indica `fill`, `stroke` o entrambi +@msg give `folder` indica `folder` +@msg give `fonts`, or open a document indica `fonts`, o apri un documento +@msg give `from` or `indices`, not both indica `from` o `indices`, non entrambi +@msg give `id` (no text is being edited) indica `id` (nessun testo è in modifica) +@msg give `id` and `anchors`, or `items`, not both indica `id` e `anchors`, oppure `items`, non entrambi +@msg give `id` or `guide`, not both indica `id` o `guide`, non entrambi +@msg give `length` or `to` indica `length` o `to` +@msg give `matches`: [{kind, name, target}] indica `matches`: [{kind, name, target}] +@msg give `name` (or edit a pattern) indica `name` (o modifica un motivo) +@msg give `name` or `names` indica `name` o `names` +@msg give `path` (a .wasm file) or `dataBase64` (the module) indica `path` (un file .wasm) o `dataBase64` (il modulo) +@msg give `path`, `data` or `dataBase64` indica `path`, `data` o `dataBase64` +@msg give `path`, `data`, `dataBase64` or `csv` indica `path`, `data`, `dataBase64` o `csv` +@msg give `path`, `xml` or `dataBase64` indica `path`, `xml` o `dataBase64` +@msg give `script`, the JavaScript the event runs ("" removes the event) indica `script`, il JavaScript eseguito dall'evento ("" rimuove l'evento) +@msg give `text` (the reading) or `remove` indica `text` (la lettura) o `remove` +@msg give `x` and `y` (and a `tolerance` of 0 or more) indica `x` e `y` (e una `tolerance` pari a 0 o maggiore) +@msg give a mode or r/g/b, c/m/y/k, gray or tint adjustments indica un metodo o regolazioni r/g/b, c/m/y/k, gray o tint +@msg give a name, or paint the active proxy with a saved preset indica un nome, o dipingi il proxy attivo con un predefinito salvato +@msg give a name, or select a stroke with a saved profile indica un nome, o seleziona una traccia con un profilo salvato +@msg give a pattern name or select an object painted with a pattern indica il nome di un motivo o seleziona un oggetto dipinto con un motivo +@msg give at least one of clip, invert, disabled, linked indica almeno uno tra clip, invert, disabled, linked +@msg give color, none, swatch or gradient indica color, none, swatch o gradient +@msg give dataBase64 indica dataBase64 +@msg give either at (a click) or area (a path drawn around the overlaps) indica at (un clic) o area (un tracciato disegnato attorno alle sovrapposizioni) +@msg give formats or a preset, not both indica formats o un predefinito, non entrambi +@msg give newName or delete indica newName o delete +@msg give offset or by indica offset o by +@msg give one of `to`, `rect` or `at` indica uno tra `to`, `rect` o `at` +@msg give one of assets, layers, selection or fullDocument indica uno tra assets, layers, selection o fullDocument +@msg give only one of onto, above, below, icon and a column place indica solo uno tra onto, above, below, icon e una posizione in colonna +@msg give origin: [x, y], or reset: true indica origin: [x, y], o reset: true +@msg give overprintFill, overprintStroke, showCenter, imageMap, url or note indica overprintFill, overprintStroke, showCenter, imageMap, url o note +@msg give path (a file) or folder indica path (un file) o folder +@msg give path, or name and dataBase64 indica path, oppure name e dataBase64 +@msg give paths or files indica paths o files +@msg give rows, rowHeight, columns or columnWidth indica rows, rowHeight, columns o columnWidth +@msg give svg, or dataBase64 of an SVG or SVGZ file indica svg, o dataBase64 di un file SVG o SVGZ +@msg give text indica text +@msg give the asset a name assegna un nome alla risorsa +@msg give the preset a `name` assegna un `name` al predefinito +@msg give the preset a name assegna un nome al predefinito +@msg give the preset a name (1–64 characters) assegna un nome al predefinito (1–64 caratteri) +@msg give the settings to change, e.g. {"formats"}]} indica le impostazioni da modificare, ad es. {"formats"}]} +@msg give the style a name assegna un nome allo stile +@msg give two or more style `names` indica due o più `names` di stili +@msg give two or more swatches indica due o più campioni +@msg give x, y, or a handle's length or angle indica x, y, o la lunghezza o l'angolo di una maniglia +@msg give {key, value} or {values} indica {key, value} o {values} +@msg global must be true or false global deve essere true o false +@msg glyph {_1} is not drawn for {ch} with the character's style (such as All Caps, Small Caps or a numeral system) il glifo {_1} non viene disegnato per {ch} con lo stile del carattere (ad esempio Tutto maiuscole, Maiuscoletto o un sistema numerico) +@msg glyph {g} is not an alternate of {ch} in {_1} {_2} il glifo {g} non è un'alternativa di {ch} in {_1} {_2} +@msg gradient must be "objects" or "mesh" gradient deve essere "objects" o "mesh" +@msg grays must be "profile" or "black", not `{g}` grays deve essere "profile" o "black", non `{g}` +@msg gridColors must be a preset name or two colours gridColors deve essere il nome di un predefinito o due colori +@msg gridSize must be small, medium or large gridSize deve essere small, medium o large +@msg guide {g} is not selected la guida {g} non è selezionata +@msg handle must be 0..3 handle deve essere 0..3 +@msg hiddenObjects {v}: "layer" (the default), "omit" or "visible" hiddenObjects {v}: "layer" (il valore predefinito), "omit" o "visible" +@msg highlight a layer to merge into evidenzia un livello in cui unire +@msg highlight two or more rows in one layer or group evidenzia due o più righe in un unico livello o gruppo +@msg highlight two or more rows to merge evidenzia due o più righe da unire +@msg icon docks a panel in the icon column: use window.panel.dock icon ancora un pannello nella colonna delle icone: usa window.panel.dock +@msg id must be a dimension id id deve essere l'id di una quota +@msg image has no bounds l'immagine non ha limiti +@msg image {_1} is linked already l'immagine {_1} è già collegata +@msg imageMap `{m}`: none, client or server imageMap `{m}`: none, client o server +@msg index out of range indice fuori intervallo +@msg instances must be 1..1000 instances deve essere 1..1000 +@msg invalid `color` `color` non valido +@msg invalid anchor index {v}: expected a non-negative integer indice di punto di ancoraggio non valido {v}: previsto un intero non negativo +@msg invalid anchor reference {v}: expected [subpath, anchor] riferimento a punto di ancoraggio non valido {v}: previsto [subpath, anchor] +@msg invalid angle angolo non valido +@msg invalid brush: {e} pennello non valido: {e} +@msg invalid color colore non valido +@msg invalid colour {c} colore non valido {c} +@msg invalid delta delta non valido +@msg invalid point punto non valido +@msg invalid size dimensione non valida +@msg invalid t t non valido +@msg invalid tolerance tolleranza non valida +@msg its artboards all lie beyond the canvas (more than {_1} pt from the origin) le sue tavole da disegno si trovano tutte oltre l'area di lavoro (a più di {_1} pt dall'origine) +@msg its clipping masks are nested too deeply le sue maschere di ritaglio sono annidate troppo in profondità +@msg its metadata has no description i suoi metadati non hanno una descrizione +@msg its metadata has no direct length i suoi metadati non hanno una lunghezza diretta +@msg join two different paths unisci due tracciati diversi +@msg justify must be auto|left|center|right|justifyAll justify deve essere auto|left|center|right|justifyAll +@msg kbps must be a positive number, not {kbps} kbps deve essere un numero positivo, non {kbps} +@msg keywords must be a list of words or a comma-separated string keywords deve essere un elenco di parole o una stringa separata da virgole +@msg keywords must be at most {MAX_KEYWORD} characters, without control characters keywords deve avere al massimo {MAX_KEYWORD} caratteri, senza caratteri di controllo +@msg kind must be 1, 2 or 3, or give a preset `name` (see perspective.presets.list) kind deve essere 1, 2 o 3, oppure indica un `name` di predefinito (vedi perspective.presets.list) +@msg kind must be graphic, fillColor, strokeColor, fillGradient, strokeGradient, fillPattern, strokePattern, charStyle or paraStyle kind deve essere graphic, fillColor, strokeColor, fillGradient, strokeGradient, fillPattern, strokePattern, charStyle o paraStyle +@msg knockout must be "on", "off", "neutral" or a bool, not {v} knockout deve essere "on", "off", "neutral" o un bool, non {v} +@msg layer `{name}` has no art to export il livello `{name}` non ha grafica da esportare +@msg layer must name a layer or sublayer layer deve indicare un livello o un sottolivello +@msg layers can't be intertwined: select the objects in them i livelli non possono essere intrecciati: seleziona gli oggetti al loro interno +@msg layers go only at the top level or in layers, objects in layers and groups i livelli vanno solo al primo livello o in altri livelli, gli oggetti in livelli e gruppi +@msg layers must be true or false layers deve essere true o false +@msg layout must be gridByRow, gridByColumn, row or column layout deve essere gridByRow, gridByColumn, row o column +@msg leading must be a number or "auto" leading deve essere un numero o "auto" +@msg left/right must be non-negative widths left/right devono essere larghezze non negative +@msg level `{t}`: 2 or 3 livello `{t}`: 2 o 3 +@msg line {line}: more than {MAX_CSV_COLS} columns riga {line}: più di {MAX_CSV_COLS} colonne +@msg line {line}: unclosed quote riga {line}: virgolette non chiuse +@msg line {line}: value for `{var}` must match its variable kind riga {line}: il valore per `{var}` deve corrispondere al tipo della sua variabile +@msg lineEndings `{other}`: lf or crlf lineEndings `{other}`: lf o crlf +@msg loading profiles from disk isn't available in the browser il caricamento dei profili dal disco non è disponibile nel browser +@msg margin: a {m} pt margin leaves no imageable area on the paper margin: un margine di {m} pt non lascia alcuna area stampabile sulla carta +@msg matrix must be finite and invertible, depth finite matrix deve essere finita e invertibile, depth finito +@msg matte: {e} matte: {e} +@msg member is for order front, forward, backward or back member è per order front, forward, backward o back +@msg missing `artboards` `artboards` mancante +@msg missing `at` [x, y] `at` [x, y] mancante +@msg missing `attrs` `attrs` mancante +@msg missing `color` or `opacity` `color` o `opacity` mancante +@msg missing `colors` `colors` mancante +@msg missing `commands` `commands` mancante +@msg missing `destinationFolder` `destinationFolder` mancante +@msg missing `effect` id id `effect` mancante +@msg missing `family` `family` mancante +@msg missing `find` `find` mancante +@msg missing `gid` (a glyph id, or null for the default glyph) `gid` mancante (un id di glifo, o null per il glifo predefinito) +@msg missing `group` `group` mancante +@msg missing `id` `id` mancante +@msg missing `id`: give one, or select one type object `id` mancante: indicane uno, o seleziona un oggetto di testo +@msg missing `index` (a point index, or null to clear) `index` mancante (un indice di punto, o null per cancellare) +@msg missing `index` (a stop index, or null to clear) `index` mancante (un indice di interruzione, o null per cancellare) +@msg missing `index` (an item index or null) `index` mancante (un indice di voce o null) +@msg missing `item` `item` mancante +@msg missing `library` `library` mancante +@msg missing `library` (see graphicStyle.libraries) `library` mancante (vedi graphicStyle.libraries) +@msg missing `library` (see swatch.library.list) `library` mancante (vedi swatch.library.list) +@msg missing `map` (rows [{from, to}] or {current}) `map` mancante (righe [{from, to}] o {current}) +@msg missing `matrix` [a,b,c,d,e,f] `matrix` [a,b,c,d,e,f] mancante +@msg missing `name` (see gradient.presets.list) `name` mancante (vedi gradient.presets.list) +@msg missing `offset` (a distance in points) `offset` mancante (una distanza in punti) +@msg missing `path` `path` mancante +@msg missing `path` (a path object id) `path` mancante (l'id di un oggetto tracciato) +@msg missing `rule` `rule` mancante +@msg missing `source` id id `source` mancante +@msg missing `start` (the byte offset of a character) `start` mancante (l'offset in byte di un carattere) +@msg missing `status`: linked, embedded, missing or modified `status` mancante: linked, embedded, missing o modified +@msg missing `target` id id `target` mancante +@msg missing `text` `text` mancante +@msg missing `to` `to` mancante +@msg missing `to` (an index) `to` mancante (un indice) +@msg missing `to`: a non-negative integer `to` mancante: un intero non negativo +@msg missing `type` `type` mancante +@msg missing `value` `value` mancante +@msg missing `{key}` `{key}` mancante +@msg missing `{k}` `{k}` mancante +@msg missing anchor punto di ancoraggio mancante +@msg missing anchors punti di ancoraggio mancanti +@msg missing anchors [[subpath, anchor]…] anchors mancanti [[subpath, anchor]…] +@msg missing array `{key}` array `{key}` mancante +@msg missing case case mancante +@msg missing color colore mancante +@msg missing commands comandi mancanti +@msg missing destination folder cartella di destinazione mancante +@msg missing from from mancante +@msg missing group gruppo mancante +@msg missing id or ids id o ids mancante +@msg missing ids ids mancante +@msg missing indexes indici mancanti +@msg missing matrix [a,b,c,d,e,f] matrice [a,b,c,d,e,f] mancante +@msg missing name nome mancante +@msg missing op op mancante +@msg missing or invalid `color` `color` mancante o non valido +@msg missing or unknown `tool` `tool` mancante o sconosciuto +@msg missing point punto mancante +@msg missing point [x,y] punto [x,y] mancante +@msg missing point `{key}` ([x, y]) punto `{key}` mancante ([x, y]) +@msg missing points punti mancanti +@msg missing points [[x,y]…] punti mancanti [[x,y]…] +@msg missing segment segmento mancante +@msg missing stops interruzioni mancanti +@msg missing subpaths sottotracciati mancanti +@msg missing target destinazione mancante +@msg missing to to mancante +@msg missing x, y x, y mancanti +@msg mode is set, add or toggle, not `{other}` mode è set, add o toggle, non `{other}` +@msg mode must be "cmyk" or "rgb" mode deve essere "cmyk" o "rgb" +@msg mode must be area|onPath, got {o} mode deve essere area|onPath, ricevuto {o} +@msg more than 10000 linked files: package dependencies in smaller parts più di 10000 file collegati: crea il pacchetto delle dipendenze in parti più piccole +@msg more than 256 commands più di 256 comandi +@msg more than {MAX_BATCH_FILES} data sets più di {MAX_BATCH_FILES} set di dati +@msg more than {MAX_BATCH_FILES} files più di {MAX_BATCH_FILES} file +@msg more than {MAX_CSV_ROWS} data rows più di {MAX_CSV_ROWS} righe di dati +@msg more than {MAX_FILES} images più di {MAX_FILES} immagini +@msg more than {MAX_IMPORT} presets più di {MAX_IMPORT} predefiniti +@msg moved must be [[x,y]…] moved deve essere [[x,y]…] +@msg name (a toolbar's name) is required name (il nome di una barra degli strumenti) è obbligatorio +@msg name (a toolbar's name) must be a string name (il nome di una barra degli strumenti) deve essere una stringa +@msg name must be a toolbar's name name deve essere il nome di una barra degli strumenti +@msg names is empty names è vuoto +@msg names must be an array of symbol names names deve essere un array di nomi di simboli +@msg need `anchor` or `segment` + `t` serve `anchor` o `segment` + `t` +@msg need at least one anchor serve almeno un punto di ancoraggio +@msg need at least one point serve almeno un punto +@msg need at least two points servono almeno due punti +@msg needs points richiede dei punti +@msg nineSlice must be true or false nineSlice deve essere true o false +@msg nineSliceGuides must be [left, top, right, bottom], each -16..16, with left + right < 1 and top + bottom < 1 nineSliceGuides deve essere [left, top, right, bottom], ciascuno -16..16, con left + right < 1 e top + bottom < 1 +@msg no PDF preset named `{n}` nessun predefinito PDF chiamato `{n}` +@msg no action set is current nessun set di azioni è corrente +@msg no active data set nessun set di dati attivo +@msg no appearance item {index} nessuna voce dell'aspetto {index} +@msg no artboard {_1} nessuna tavola da disegno {_1} +@msg no artboard {artboard} (0-based; the document has {_1}) nessuna tavola da disegno {artboard} (a partire da 0; il documento ne ha {_1}) +@msg no asset {id} nessuna risorsa {id} +@msg no asset {id} (see assets.list) nessuna risorsa {id} (vedi assets.list) +@msg no assets to export nessuna risorsa da esportare +@msg no assets yet: collect art for export first (assets.add) ancora nessuna risorsa: raccogli prima la grafica da esportare (assets.add) +@msg no brush named `{n}` nessun pennello chiamato `{n}` +@msg no character at byte {start} nessun carattere al byte {start} +@msg no colour nessun colore +@msg no colour group `{group}` nessun gruppo di colori `{group}` +@msg no colour group `{name}` nessun gruppo di colori `{name}` +@msg no data sets nessun set di dati +@msg no dataset named `{from}` nessun set di dati chiamato `{from}` +@msg no dataset named `{name}` nessun set di dati chiamato `{name}` +@msg no datasets nessun set di dati +@msg no design named `{name}` nessun design chiamato `{name}` +@msg no design named `{n}` nessun design chiamato `{n}` +@msg no design named `{old}` nessun design chiamato `{old}` +@msg no encoder for {_1} yet ancora nessun codificatore per {_1} +@msg no export preset `{name}` (see document.exportPresets.list) nessun predefinito di esportazione `{name}` (vedi document.exportPresets.list) +@msg no folder: pass `path` or set Preferences › Plug-ins & Scratch Disks › Additional Plug-ins Folder nessuna cartella: passa `path` o imposta Preferenze › Plug-in & Dischi di memoria virtuale › Cartella plug-in aggiuntivi +@msg no fonts to look for: they are all available nessun font da cercare: sono tutti disponibili +@msg no formats to export nessun formato da esportare +@msg no gradient preset `{name}` (see gradient.presets.list) nessun predefinito di sfumatura `{name}` (vedi gradient.presets.list) +@msg no graphic style `{n}` in `{_1}` nessuno stile grafico `{n}` in `{_1}` +@msg no graphic style library `{key}` (see graphicStyle.libraries) nessuna libreria di stili grafici `{key}` (vedi graphicStyle.libraries) +@msg no guide selected nessuna guida selezionata +@msg no guide selected (or give an index) nessuna guida selezionata (o indica un indice) +@msg no guide {i} nessuna guida {i} +@msg no images in the folder nessuna immagine nella cartella +@msg no item at index {_1} nessun elemento all'indice {_1} +@msg no item at index {from} nessun elemento all'indice {from} +@msg no item at one of the indices {idx} nessun elemento in uno degli indici {idx} +@msg no layer nessun livello +@msg no layer {id} (see layer.list) nessun livello {id} (vedi layer.list) +@msg no layers to export nessun livello da esportare +@msg no library `{key}` nessuna libreria `{key}` +@msg no metafile format nessun formato metafile +@msg no object {_1} nessun oggetto {_1} +@msg no objects nessun oggetto +@msg no parent layer nessun livello padre +@msg no perspective grid preset named `{b}` (see perspective.presets.list) nessun predefinito di griglia prospettica chiamato `{b}` (vedi perspective.presets.list) +@msg no perspective grid preset named `{n}` nessun predefinito di griglia prospettica chiamato `{n}` +@msg no point selected (give `index`) nessun punto selezionato (indica `index`) +@msg no preset called `{from}` (see file.newPresets) nessun predefinito chiamato `{from}` (vedi file.newPresets) +@msg no preset called `{name}` (see file.newPresets) nessun predefinito chiamato `{name}` (vedi file.newPresets) +@msg no preset named `{n}` nessun predefinito chiamato `{n}` +@msg no print preset named `{b}` (see print.presets.list) nessun predefinito di stampa chiamato `{b}` (vedi print.presets.list) +@msg no print preset named `{name}` (presets: {_1}) nessun predefinito di stampa chiamato `{name}` (predefiniti: {_1}) +@msg no print preset named `{n}` nessun predefinito di stampa chiamato `{n}` +@msg no profile named `{name}` nessun profilo chiamato `{name}` +@msg no readable files in the folder nessun file leggibile nella cartella +@msg no recovery copy `{f}` (see file.recovery.list) nessuna copia di ripristino `{f}` (vedi file.recovery.list) +@msg no rows nessuna riga +@msg no saved PDF preset named `{name}` nessun predefinito PDF salvato chiamato `{name}` +@msg no saved export preset `{name}` nessun predefinito di esportazione salvato `{name}` +@msg no saved gradient preset called {name} nessun predefinito di sfumatura salvato chiamato {name} +@msg no saved gradient presets nessun predefinito di sfumatura salvato +@msg no saved preset called `{name}` nessun predefinito salvato chiamato `{name}` +@msg no saved presets to export (name [Default] in `names` to export it) nessun predefinito salvato da esportare (indica [Default] in `names` per esportarlo) +@msg no saved presets to export (name built-in ones in `names`) nessun predefinito salvato da esportare (indica quelli incorporati in `names`) +@msg no saved print preset named `{name}` nessun predefinito di stampa salvato chiamato `{name}` +@msg no saved profile called {name} nessun profilo salvato chiamato {name} +@msg no saved selection `{name}` nessuna selezione salvata `{name}` +@msg no saved theme called {name} (see colorTheme.list) nessun tema salvato chiamato {name} (vedi colorTheme.list) +@msg no saved views nessuna vista salvata +@msg no series {i} nessuna serie {i} +@msg no slice {n} in the image (see slice.list) nessuna sezione {n} nell'immagine (vedi slice.list) +@msg no source art nessuna grafica di origine +@msg no stop {i} (the gradient has {n}) nessuna interruzione {i} (la sfumatura ne ha {n}) +@msg no style `{name}` nessuno stile `{name}` +@msg no such assets (see assets.list) risorse inesistenti (vedi assets.list) +@msg no such frame anchor, or the frame would reach past the canvas punto di ancoraggio della cornice inesistente, oppure la cornice supererebbe l'area di lavoro +@msg no such objects oggetti inesistenti +@msg no such path anchor, or the path would reach past the canvas punto di ancoraggio del tracciato inesistente, oppure il tracciato supererebbe l'area di lavoro +@msg no such rows righe inesistenti +@msg no swatch `{keep}` nessun campione `{keep}` +@msg no swatch `{n}` nessun campione `{n}` +@msg no swatch `{n}` (swatches and colour groups move separately) nessun campione `{n}` (campioni e gruppi di colori si spostano separatamente) +@msg no swatch library `{key}` (see swatch.library.list) nessuna libreria campioni `{key}` (vedi swatch.library.list) +@msg no swatch library `{key}` (see swatch.library.list; "document": the document's swatches) nessuna libreria campioni `{key}` (vedi swatch.library.list; "document": i campioni del documento) +@msg no swatch library `{key}` (see swatch.library.list; "{DOCUMENT_SWATCHES}": the document's swatches) nessuna libreria campioni `{key}` (vedi swatch.library.list; "{DOCUMENT_SWATCHES}": i campioni del documento) +@msg no swatch named `{n}` nessun campione chiamato `{n}` +@msg no swatch or colour group `{n}` nessun campione o gruppo di colori `{n}` +@msg no swatch or colour group `{n}` in `{_1}` nessun campione o gruppo di colori `{n}` in `{_1}` +@msg no symbol named `{name}` nessun simbolo chiamato `{name}` +@msg no text objects selected nessun oggetto di testo selezionato +@msg no toolbar named `{_1}` nessuna barra degli strumenti chiamata `{_1}` +@msg no toolbar named `{name}` nessuna barra degli strumenti chiamata `{name}` +@msg no tracing preset `{preset}` (see imageTrace.presets) nessun predefinito di ricalco `{preset}` (vedi imageTrace.presets) +@msg no user library folder here (save with a path, or without one to get the data) nessuna cartella delle librerie utente qui (salva con un percorso, o senza per ottenere i dati) +@msg no variable named `{from}` nessuna variabile chiamata `{from}` +@msg no variable named `{name}` nessuna variabile chiamata `{name}` +@msg no variable named `{var}` nessuna variabile chiamata `{var}` +@msg no variable named `{v}` nessuna variabile chiamata `{v}` +@msg no view named `{name}` nessuna vista chiamata `{name}` +@msg node {_1} is not a graph il nodo {_1} non è un grafico +@msg node {_1} is not an image il nodo {_1} non è un'immagine +@msg node {_1} is not type il nodo {_1} non è testo +@msg not a PDF presets file (format `{_1}`) non è un file di predefiniti PDF (formato `{_1}`) +@msg not a PDF presets file: {e} non è un file di predefiniti PDF: {e} +@msg not a Word document: its WordDocument stream doesn't start with a file information block non è un documento Word: il suo stream WordDocument non inizia con un blocco di informazioni sul file +@msg not a Word document: its compound file has no WordDocument stream non è un documento Word: il suo file composto non ha uno stream WordDocument +@msg not a Word document: its main part is not a WordprocessingML document non è un documento Word: la sua parte principale non è un documento WordprocessingML +@msg not a Word document: its package has no main document part non è un documento Word: il suo pacchetto non ha una parte di documento principale +@msg not a closed path non è un tracciato chiuso +@msg not a flattener presets file (format `{_1}`) non è un file di predefiniti di appiattimento (formato `{_1}`) +@msg not a flattener presets file: {e} non è un file di predefiniti di appiattimento: {e} +@msg not a layer or group non è un livello o un gruppo +@msg not a library file: {e} non è un file di libreria: {e} +@msg not a mesh envelope point non è un punto di un involucro a trama +@msg not a perspective grid presets file (format `{_1}`) non è un file di predefiniti di griglia prospettica (formato `{_1}`) +@msg not a perspective grid presets file: {e} non è un file di predefiniti di griglia prospettica: {e} +@msg not a print presets file (format `{_1}`) non è un file di predefiniti di stampa (formato `{_1}`) +@msg not a print presets file: {e} non è un file di predefiniti di stampa: {e} +@msg not a stored or deflated central record non è un record centrale archiviato o compresso con deflate +@msg not a valid SVGZ file: {e} non è un file SVGZ valido: {e} +@msg not an RTF file: it doesn't start with {\\rtf non è un file RTF: non inizia con {\\rtf +@msg not an image VectorCraft reads (see document.formats) non è un'immagine che VectorCraft legge (vedi document.formats) +@msg not area type (text in a frame) outside perspective non è testo in area (testo in una cornice) fuori dalla prospettiva +@msg not editing an opacity mask non stai modificando una maschera di opacità +@msg not type on a path outside perspective non è testo su tracciato fuori dalla prospettiva +@msg nothing prints non viene stampato nulla +@msg nothing to bind niente da collegare +@msg nothing to change niente da modificare +@msg nothing to change: give dx, dy, hScale, vScale or rotation niente da modificare: indica dx, dy, hScale, vScale o rotation +@msg nothing to crop niente da ritagliare +@msg nothing to export niente da esportare +@msg nothing to export: the document has no visible art niente da esportare: il documento non ha grafica visibile +@msg nothing to mark niente da contrassegnare +@msg nothing to print: every artboard picked is blank niente da stampare: tutte le tavole da disegno scelte sono vuote +@msg nothing to print: the document has no art that prints niente da stampare: il documento non ha grafica stampabile +@msg object is not a path l'oggetto non è un tracciato +@msg object transparency cannot be sampled onto highlighted characters; disable Transparency in Eyedropper Options or select the whole object la trasparenza dell'oggetto non può essere campionata sui caratteri evidenziati; disattiva Trasparenza in Opzioni contagocce o seleziona l'intero oggetto +@msg object {_1} can't be set as point type l'oggetto {_1} non può essere impostato come testo con punto +@msg object {_1} has lost its file's contents: relink it l'oggetto {_1} ha perso il contenuto del suo file: ricollegalo +@msg object {_1} has no path l'oggetto {_1} non ha un tracciato +@msg object {_1} has no size l'oggetto {_1} non ha dimensioni +@msg object {_1} is hidden or locked, or on a locked artboard l'oggetto {_1} è nascosto o bloccato, o si trova su una tavola da disegno bloccata +@msg object {_1} is not a placed document l'oggetto {_1} non è un documento inserito +@msg object {_1} is not an Intertwine group l'oggetto {_1} non è un gruppo Intreccia +@msg object {_1} is not an image l'oggetto {_1} non è un'immagine +@msg object {_1} is not an image or a linked file l'oggetto {_1} non è un'immagine o un file collegato +@msg object {_1} is not an image or a placed document l'oggetto {_1} non è un'immagine o un documento inserito +@msg object {_1} is not in this Intertwine group l'oggetto {_1} non è in questo gruppo Intreccia +@msg object {_1} needs a 3D effect at index {index} l'oggetto {_1} richiede un effetto 3D all'indice {index} +@msg object {id} has anchors 0–{_1} for `corners`, not {past} l'oggetto {id} ha i punti di ancoraggio 0–{_1} per `corners`, non {past} +@msg object {id} has {n} anchors, so no anchor {past} l'oggetto {id} ha {n} punti di ancoraggio, quindi nessun punto di ancoraggio {past} +@msg objects must be none, move or copy objects deve essere none, move o copy +@msg objects were left out alcuni oggetti sono stati esclusi +@msg offset out of range offset fuori intervallo +@msg on must be true or false on deve essere true o false +@msg only groups, layers, type and images have a Contents (Characters) row solo gruppi, livelli, testo e immagini hanno una riga Contenuto (Caratteri) +@msg only interior mesh lines can be deleted solo le linee interne della trama possono essere eliminate +@msg only solid colours can be added (not gradients or patterns) solo i colori pieni possono essere aggiunti (non sfumature o motivi) +@msg only solid-colour swatches can be spot colours solo i campioni di colore pieno possono essere colori spot +@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 è stato possibile leggere solo l'anteprima incorporata di questo file Affinity: usa File › Apri per vederla con il relativo avviso, oppure esporta prima un SVG o un PDF da Affinity +@msg only this Affinity file's embedded preview could be read; use File › Open to see it with its warning è stato possibile leggere solo l'anteprima incorporata di questo file Affinity; usa File › Apri per vederla con il relativo avviso +@msg onto, above, below, icon and a column place belong to window.panel.float and window.panel.dock onto, above, below, icon e una posizione in colonna appartengono a window.panel.float e window.panel.dock +@msg onto, above, below, icon and a column place take no anchor, side, zone or before onto, above, below, icon e una posizione in colonna non accettano anchor, side, zone o before +@msg onto, above, below, icon and a column place take no x, y, width or height onto, above, below, icon e una posizione in colonna non accettano x, y, width o height +@msg open a document to look for its linked files apri un documento per cercarne i file collegati +@msg options must be an object (see file.formatOptions) options deve essere un oggetto (vedi file.formatOptions) +@msg order must be leftToRight or rightToLeft order deve essere leftToRight o rightToLeft +@msg orientation must be portrait or landscape, not `{o}` orientation deve essere portrait o landscape, non `{o}` +@msg outlineText {v}: true or false outlineText {v}: true o false +@msg output intent profile: {e} profilo dell'intento di output: {e} +@msg output.destination: no RGB or CMYK profile is called “{name}” output.destination: nessun profilo RGB o CMYK si chiama “{name}” +@msg output.destination: {_1} files are CMYK, and “{name}” is an RGB profile output.destination: i file {_1} sono CMYK, e “{name}” è un profilo RGB +@msg output.destination: {e} output.destination: {e} +@msg output.inks: no ink is set to print output.inks: nessun inchiostro è impostato per la stampa +@msg output.outputIntent: no profile is called “{name}”, and a {std} output intent embeds its profile output.outputIntent: nessun profilo si chiama “{name}”, e un intento di output {std} incorpora il suo profilo +@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: nessun profilo si chiama “{name}”: un intento di output {std} incorpora un profilo, o indica una condizione di stampa registrata (outputConditionId e registry) +@msg output.outputIntent: {std} files print in CMYK, and “{name}” is an RGB profile output.outputIntent: i file {std} si stampano in CMYK, e “{name}” è un profilo RGB +@msg output.outputIntent: “{name}” is a grey profile, which can't be a {std} output intent output.outputIntent: “{name}” è un profilo in scala di grigi, che non può essere un intento di output {std} +@msg overlap: {o} pt is half a tile or more overlap: {o} pt è metà di un riquadro o più +@msg overlapping Shaper paths must be in the same layer or group i tracciati Shaper sovrapposti devono essere nello stesso livello o gruppo +@msg overlapping edits modifiche sovrapposte +@msg page art conflicts with image data for {key} la grafica della pagina è in conflitto con i dati dell'immagine per {key} +@msg page art conflicts with pattern {name} la grafica della pagina è in conflitto con il motivo {name} +@msg page art conflicts with swatch {name} la grafica della pagina è in conflitto con il campione {name} +@msg page art is missing image data for {key} nella grafica della pagina mancano i dati dell'immagine per {key} +@msg page art is missing pattern {name} nella grafica della pagina manca il motivo {name} +@msg page art is missing swatch {name} nella grafica della pagina manca il campione {name} +@msg page must be a whole number from 1 page deve essere un numero intero a partire da 1 +@msg page thumbnail: {e} miniatura della pagina: {e} +@msg page {page} of the PDF has no objects la pagina {page} del PDF non ha oggetti +@msg page {page}: `{file}` has {n} {artboards} pagina {page}: `{file}` ha {n} {artboards} +@msg page {page}: `{name}` has {n} {pages} pagina {page}: `{name}` ha {n} {pages} +@msg pages must be a range such as "2-3" or a page number, not {v} pages deve essere un intervallo come "2-3" o un numero di pagina, non {v} +@msg pages: {e} pagine: {e} +@msg panel position must be finite la posizione del pannello deve essere finita +@msg parameters must be an object i parametri devono essere un oggetto +@msg params must be an object params deve essere un oggetto +@msg passwords can have up to {MAX_PASSWORD} bytes le password possono avere fino a {MAX_PASSWORD} byte +@msg path has no open end il tracciato non ha un'estremità aperta +@msg paths must be strings paths deve contenere stringhe +@msg pdfCompatible: false needs every artboard to keep the editable artwork pdfCompatible: false richiede che ogni tavola da disegno conservi la grafica modificabile +@msg pdfs `{m}`: single or multiple pdfs `{m}`: single o multiple +@msg pickColorFrom is "artwork" or "swatches", not {v} pickColorFrom è "artwork" o "swatches", non {v} +@msg pins and moved must be non-empty lists of the same length pins e moved devono essere elenchi non vuoti della stessa lunghezza +@msg pins must be [[x,y]…] pins deve essere [[x,y]…] +@msg pins must be finite pins deve essere finito +@msg place is above, below or inside, not `{other}` place è above, below o inside, non `{other}` +@msg plane must be left|right|ground plane deve essere left|right|ground +@msg plane must be left|right|ground|none plane deve essere left|right|ground|none +@msg point is outside the mesh il punto è fuori dalla trama +@msg points must be [t, left, right] with non-negative widths points deve essere [t, left, right] con larghezze non negative +@msg points must be a non-empty [[x,y,pressure?]…] list points deve essere un elenco [[x,y,pressure?]…] non vuoto +@msg points must be a non-empty list of finite [x,y] points deve essere un elenco non vuoto di [x,y] finiti +@msg points must be an array or null points deve essere un array o null +@msg points must be finite points deve essere finito +@msg points must be finite [x,y] pairs points deve contenere coppie [x,y] finite +@msg points must contain 1–4096 coordinates points deve contenere 1–4096 coordinate +@msg position must be normal, superscript or subscript position deve essere normal, superscript o subscript +@msg position must be topLeft, topRight, bottomLeft or bottomRight position deve essere topLeft, topRight, bottomLeft o bottomRight +@msg positions must be between 0 and 100000 pt le posizioni devono essere comprese tra 0 e 100000 pt +@msg ppi must be a positive number, not {ppi} ppi deve essere un numero positivo, non {ppi} +@msg ppi must be between 1 and 2400 ppi deve essere compreso tra 1 e 2400 +@msg preserve `{v}` is not transforms|bounds|fileDimensions|fit|fill preserve `{v}` non è transforms|bounds|fileDimensions|fit|fill +@msg preserveShape must be clippingMask|transparency preserveShape deve essere clippingMask|transparency +@msg preset `{id}`: mobile or density preset `{id}`: mobile o density +@msg preset must be standard, diffuse, topLeft or right preset deve essere standard, diffuse, topLeft o right +@msg preset must be the name of a print preset preset deve essere il nome di un predefinito di stampa +@msg previewMode must be default, pixel or overprint previewMode deve essere default, pixel o overprint +@msg print settings: {e} impostazioni di stampa: {e} +@msg procedures nest too deeply le procedure sono annidate troppo in profondità +@msg procedures nested too deeply procedure annidate troppo in profondità +@msg quotes must be {double?, single?} quotes deve essere {double?, single?} +@msg quotes.{k} must be an opening and a closing quote, e.g. "“”" quotes.{k} deve essere una virgoletta di apertura e una di chiusura, ad es. "“”" +@msg rasterEffectsPpi must be 1–2400 (72 screen, 150 medium, 300 high) rasterEffectsPpi deve essere 1–2400 (72 schermo, 150 media, 300 alta) +@msg rating must be a whole number from 0 to {MAX_RATING} rating deve essere un numero intero compreso tra 0 e {MAX_RATING} +@msg rect and {k} contradict each other: give the area or the artboards rect e {k} si contraddicono: indica l'area o le tavole da disegno +@msg rect applies to raster formats, not {_1} rect si applica ai formati raster, non a {_1} +@msg rect is [x, y, width, height] in points rect è [x, y, width, height] in punti +@msg rect must be [x, y, width, height] rect deve essere [x, y, width, height] +@msg rect must be [x, y, width, height] with a positive width and height rect deve essere [x, y, width, height] con larghezza e altezza positive +@msg rect needs finite numbers and a positive width and height rect richiede numeri finiti e larghezza e altezza positive +@msg redraw needs id redraw richiede id +@msg redraw within must be a finite distance of 0 or more within di redraw deve essere una distanza finita pari a 0 o maggiore +@msg registration must be 0..8 registration deve essere 0..8 +@msg release a layer or a group rilascia un livello o un gruppo +@msg replace keeps the replaced object's place and transform: drop template, at and rect replace mantiene la posizione e la trasformazione dell'oggetto sostituito: rimuovi template, at e rect +@msg replace: select the one object to replace replace: seleziona l'unico oggetto da sostituire +@msg resolution must be a number or screen|medium|high resolution deve essere un numero o screen|medium|high +@msg resolution must be between 1 and 2400 ppi resolution deve essere compreso tra 1 e 2400 ppi +@msg rotation must be a number of degrees rotation deve essere un numero di gradi +@msg rotation must be finite rotation deve essere finito +@msg rows and cols must be 1..50 rows e cols devono essere 1..50 +@msg rows and columns must be 1..500 rows e columns devono essere 1..500 +@msg rows of objects whose appearances differ can't be reordered together le righe di oggetti con aspetti diversi non possono essere riordinate insieme +@msg save the document first: Package collects a saved document salva prima il documento: Crea pacchetto raccoglie un documento salvato +@msg save the document first: copy puts the file in its folder salva prima il documento: copy mette il file nella sua cartella +@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}`: un fattore come 2x, una larghezza (100w), un'altezza (100h) o una risoluzione (72ppi) +@msg scale must be a number above 0, at most 64 scale deve essere un numero maggiore di 0, al massimo 64 +@msg scale must be non-zero scale deve essere diverso da zero +@msg scope must be selection or all (got {other}) scope deve essere selection o all (ricevuto {other}) +@msg scribble is too long lo scarabocchio è troppo lungo +@msg segment does not exist il segmento non esiste +@msg select a Shaper group seleziona un gruppo Shaper +@msg select a blend to set easing or color shift seleziona una fusione per impostare l'andamento o lo spostamento del colore +@msg select a compound shape seleziona una forma composta +@msg select a graph seleziona un grafico +@msg select a group or layer with visible, unlocked members seleziona un gruppo o un livello con membri visibili e sbloccati +@msg select a layer seleziona un livello +@msg select a text object seleziona un oggetto di testo +@msg select an image (or an Image Trace group) seleziona un'immagine (o un gruppo di Ricalco immagine) +@msg select an image, or pass id seleziona un'immagine, o passa id +@msg select an object (or pass `id`) seleziona un oggetto (o passa `id`) +@msg select an object with a freeform gradient seleziona un oggetto con una sfumatura a forma libera +@msg select an object with an opacity mask seleziona un oggetto con una maschera di opacità +@msg select area type seleziona un testo in area +@msg select area type (text in a frame) seleziona un testo in area (testo in una cornice) +@msg select area type and closed paths (at least one area type object) seleziona testo in area e tracciati chiusi (almeno un oggetto di testo in area) +@msg select area type whose first line can fit its frame seleziona un testo in area la cui prima riga può entrare nella sua cornice +@msg select artwork to add its colours seleziona la grafica di cui aggiungere i colori +@msg select artwork to make a group of its colours seleziona la grafica con i cui colori creare un gruppo +@msg select artwork to recolour, or give a colour `group` seleziona la grafica da ricolorare, o indica un `group` di colori +@msg select exactly two path ends to join seleziona esattamente due estremità di tracciato da unire +@msg select members of a compound shape seleziona i membri di una forma composta +@msg select objects or give ids seleziona degli oggetti o indica gli ids +@msg select objects to align to the guide seleziona gli oggetti da allineare alla guida +@msg select one Intertwine group seleziona un gruppo Intreccia +@msg select one image or pass id seleziona un'immagine o passa id +@msg select paths seleziona dei tracciati +@msg select paths or compound paths seleziona tracciati o tracciati composti +@msg select rows or objects to collect seleziona righe o oggetti da raccogliere +@msg select something to export seleziona qualcosa da esportare +@msg select something to export (or turn off selectedOnly) seleziona qualcosa da esportare (o disattiva selectedOnly) +@msg select text to redefine the style from seleziona il testo da cui ridefinire lo stile +@msg select the art and, on top of it, the mask object seleziona la grafica e, sopra di essa, l'oggetto maschera +@msg select the art for the design seleziona la grafica per il design +@msg select the art for the pattern tile seleziona la grafica per la porzione del motivo +@msg select the art to add first seleziona prima la grafica da aggiungere +@msg select the art to collect (or give its ids) seleziona la grafica da raccogliere (o indica i suoi ids) +@msg select the content and a path on top seleziona il contenuto e un tracciato sopra di esso +@msg select the images to relink, or pass ids seleziona le immagini da ricollegare, o passa ids +@msg select the images, or pass ids seleziona le immagini, o passa ids +@msg select the linked images or placed documents, or pass ids seleziona le immagini collegate o i documenti inseriti, o passa ids +@msg select the object to copy from seleziona l'oggetto da cui copiare +@msg select the objects (or give their ids) seleziona gli oggetti (o indica i loro ids) +@msg select the objects to attach to the path seleziona gli oggetti da collegare al tracciato +@msg select the objects to bind seleziona gli oggetti da collegare +@msg select two or more objects to intertwine seleziona due o più oggetti da intrecciare +@msg select two or more slices to combine seleziona due o più sezioni da combinare +@msg select type on a path seleziona un testo su tracciato +@msg selection `{m}`: one or each selection `{m}`: one o each +@msg settings is an object of document.exportForScreens params settings è un oggetto di parametri di document.exportForScreens +@msg settings must be an object settings deve essere un oggetto +@msg show `{show}`: all, missing, modified or embedded show `{show}`: all, missing, modified o embedded +@msg show must be true or false show deve essere true o false +@msg size must be "small", "medium", "large" or a number of points over 0 up to {_1} size deve essere "small", "medium", "large" o un numero di punti maggiore di 0 fino a {_1} +@msg size must be positive size deve essere positivo +@msg sizeMode is "fixed" or "pressure", not `{other}` sizeMode è "fixed" o "pressure", non `{other}` +@msg sizeMode is "fixed" or "pressure", not {other} sizeMode è "fixed" o "pressure", non {other} +@msg sizeVariation must be 0 or more sizeVariation deve essere 0 o maggiore +@msg smallCaps must be true or false smallCaps deve essere true o false +@msg sort `{x}`: name, kind or status sort `{x}`: name, kind o status +@msg source paths are too large for Shaper i tracciati di origine sono troppo grandi per Shaper +@msg spot, mode and group apply to solid colours only spot, mode e group si applicano solo ai colori pieni +@msg status `{s}`: linked, embedded, missing or modified status `{s}`: linked, embedded, missing o modified +@msg steps must be 1..1000 steps deve essere 1..1000 +@msg steps must be 1..{MAX_STEPS} steps deve essere 1..{MAX_STEPS} +@msg stop {i}: `plates` must be four numbers from 0 to 1 (C, M, Y, K) interruzione {i}: `plates` deve essere quattro numeri compresi tra 0 e 1 (C, M, Y, K) +@msg stopped before the archive was finished interrotto prima del completamento dell'archivio +@msg strokeMode must be within|along|across, got {m} strokeMode deve essere within|along|across, ricevuto {m} +@msg strokeOptions must be an object of stroke.set options strokeOptions deve essere un oggetto di opzioni di stroke.set +@msg strokeWidth must be a positive number of pixels strokeWidth deve essere un numero positivo di pixel +@msg subpath {si} is not an open subpath il sottotracciato {si} non è un sottotracciato aperto +@msg subpaths required when joining the same path subpaths è obbligatorio quando si unisce lo stesso tracciato +@msg swatchConflict must be "merge" or "add", not {v} swatchConflict deve essere "merge" o "add", non {v} +@msg text must be {characterSet?, platform?, removeLineReturns?, removeParagraphReturns?, replaceSpaces?} text deve essere {characterSet?, platform?, removeLineReturns?, removeParagraphReturns?, replaceSpaces?} +@msg text transform is collapsed or unrepresentable la trasformazione del testo è degenere o non rappresentabile +@msg text.replaceSpaces must be a whole number from 2 to 100 text.replaceSpaces deve essere un numero intero compreso tra 2 e 100 +@msg text.{key} `{v}`: use "{_1}" or "{_2}" text.{key} `{v}`: usa "{_1}" o "{_2}" +@msg textureImage: give {path} or {name, dataBase64} of an image file, or a texture as effect.list gives it textureImage: indica {path} o {name, dataBase64} di un file immagine, o una texture come la restituisce effect.list +@msg textureImage: {e} textureImage: {e} +@msg that makes more than {MAX_PIECES} slices questo produce più di {MAX_PIECES} sezioni +@msg that object is not in the overlap quell'oggetto non è nella sovrapposizione +@msg the Affinity document has no page with a size il documento Affinity non ha una pagina con una dimensione +@msg the CMYK image couldn't be written impossibile scrivere l'immagine CMYK +@msg the PDF security settings were saved without their passwords: give openPassword or permissionsPassword again, or security without passwordRequired to export unprotected le impostazioni di sicurezza PDF sono state salvate senza le relative password: indica di nuovo openPassword o permissionsPassword, oppure security senza passwordRequired per esportare senza protezione +@msg the RTF file has too many paragraphs or formatting changes to place as type (at most {MAX_PARAGRAPHS} paragraphs and {MAX_RUNS} runs) il file RTF ha troppi paragrafi o cambi di formattazione per essere inserito come testo (al massimo {MAX_PARAGRAPHS} paragrafi e {MAX_RUNS} sequenze) +@msg the RTF file is too large to place as type (at most {_1} MB) il file RTF è troppo grande per essere inserito come testo (al massimo {_1} MB) +@msg the SVG expands to more than {0} nested SVG images l'SVG si espande in più di {0} immagini SVG annidate +@msg the SVG expands to more than {0} objects l'SVG si espande in più di {0} oggetti +@msg the SVG has no drawable objects l'SVG non ha oggetti disegnabili +@msg the SVGZ file unpacks to more than {_1} MB il file SVGZ si decomprime in più di {_1} MB +@msg the Word document has more than {MAX_PAGES} pages of properties, too many to place il documento Word ha più di {MAX_PAGES} pagine di proprietà, troppe per essere inserito +@msg the Word document has more than {MAX_RUNS} runs of character or paragraph properties, too many to place il documento Word ha più di {MAX_RUNS} sequenze di proprietà di carattere o di paragrafo, troppe per essere inserito +@msg the Word document has more than {_1} MB of formatting properties, too many to place il documento Word ha più di {_1} MB di proprietà di formattazione, troppe per essere inserito +@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) il documento Word ha troppo testo o troppa formattazione per essere inserito come testo (al massimo {MAX_PARAGRAPHS} paragrafi, {MAX_RUNS} sequenze e {_1} MB di testo) +@msg the Word document has too much text to place as type (at most {_1} MB) il documento Word ha troppo testo per essere inserito come testo (al massimo {_1} MB) +@msg the Word document is encrypted or protected with a password; save it without one to place it il documento Word è crittografato o protetto da password; salvalo senza password per inserirlo +@msg the Word document is too large to place (its {name} stream holds more than {_1} MB) il documento Word è troppo grande per essere inserito (il suo stream {name} contiene più di {_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 il documento Word è stato salvato l'ultima volta con il salvataggio rapido, che VectorCraft non legge; salvalo di nuovo con il salvataggio rapido disattivato +@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 il documento Word è stato salvato da Word 6.0 o Word 95, che VectorCraft non legge; salvalo come documento Word 97-2003 +@msg the Word document's text is made of more than {MAX_PIECES} pieces, too many to place il testo del documento Word è composto da più di {MAX_PIECES} parti, troppe per essere inserito +@msg the action set ids are not a list gli id dei set di azioni non sono un elenco +@msg the active paint is not a gradient la pittura attiva non è una sfumatura +@msg the artboards don't fit on the canvas: fewer artboards, less spacing or a smaller size le tavole da disegno non entrano nell'area di lavoro: usa meno tavole da disegno, una spaziatura minore o una dimensione più piccola +@msg the character's size can't be measured impossibile misurare la dimensione del carattere +@msg the compound shape is too complex to expand la forma composta è troppo complessa da espandere +@msg the container of the listed objects is gone il contenitore degli oggetti elencati non esiste più +@msg the copy `{user_id}` can't be read as a library la copia `{user_id}` non può essere letta come libreria +@msg the copy would leave the canvas la copia uscirebbe dall'area di lavoro +@msg the cut cannot be made on this art: {e} impossibile eseguire il taglio su questa grafica: {e} +@msg the data isn't an EMF or WMF picture i dati non sono un'immagine EMF o WMF +@msg the data isn't an EPS or PostScript picture i dati non sono un'immagine EPS o PostScript +@msg the deferred paste is no longer available l'incolla differito non è più disponibile +@msg the design is too big to draw at every data point of this graph il design è troppo grande da disegnare in ogni punto dati di questo grafico +@msg the design is too big to draw in every column of this graph il design è troppo grande da disegnare in ogni colonna di questo grafico +@msg the dialog it was opened over is no longer open la finestra di dialogo sopra la quale è stata aperta non è più aperta +@msg the dictionary stack overflowed lo stack dei dizionari ha superato il limite +@msg the editing data's hash has no place l'hash dei dati di modifica non ha una posizione +@msg the entities the document declares expand to too much text le entità dichiarate dal documento si espandono in troppo testo +@msg the envelope's content has no bounds il contenuto dell'involucro non ha limiti +@msg the envelope's mesh is malformed la trama dell'involucro non è valida +@msg the file holds no `variableSets` element il file non contiene alcun elemento `variableSets` +@msg the file is larger than {_1} MB il file supera {_1} MB +@msg the file is too large il file è troppo grande +@msg the file was updated or encrypted already il file è già stato aggiornato o crittografato +@msg the first character of a line has no character before it to kern: it moves only up and down (dy) il primo carattere di una riga non ha un carattere precedente con cui applicare la crenatura: si sposta solo in alto e in basso (dy) +@msg the font {_1} is not available il font {_1} non è disponibile +@msg the format has no file extension il formato non ha un'estensione di file +@msg the graphic can't be read: {e} impossibile leggere la grafica: {e} +@msg the graphic is empty la grafica è vuota +@msg the graphic's data is damaged i dati della grafica sono danneggiati +@msg the graphic's document has no artboard il documento della grafica non ha tavole da disegno +@msg the header `{cell}` for `{name}` would read back as the variable `{other}`: rename one of them l'intestazione `{cell}` per `{name}` verrebbe riletta come la variabile `{other}`: rinomina una delle due +@msg the image `{path}` isn't followed: {r} l'immagine `{path}` non viene seguita: {r} +@msg the image has no embedded pixels l'immagine non ha pixel incorporati +@msg the image is empty l'immagine è vuota +@msg the image is fully transparent l'immagine è completamente trasparente +@msg the image is not on an artboard; pass rect l'immagine non è su una tavola da disegno; passa rect +@msg the image's pixels are missing mancano i pixel dell'immagine +@msg the image's pixels aren't in the document i pixel dell'immagine non sono nel documento +@msg the image's pixels can't be read: {e} impossibile leggere i pixel dell'immagine: {e} +@msg the image's transparency doesn't fit it la trasparenza dell'immagine non corrisponde all'immagine +@msg the job would print more than {MAX_PAGES} pages il lavoro stamperebbe più di {MAX_PAGES} pagine +@msg the job would print more than {_1} pages il lavoro stamperebbe più di {_1} pagine +@msg the layers hold no art to mask i livelli non contengono grafica da mascherare +@msg the layout moved il layout si è spostato +@msg the library has a group `{name}` already la libreria ha già un gruppo `{name}` +@msg the library has no group `{name}` la libreria non ha alcun gruppo `{name}` +@msg the library has no item `{item}` la libreria non ha alcun elemento `{item}` +@msg the library has no item `{other}` la libreria non ha alcun elemento `{other}` +@msg the library holds {MAX_GROUPS} groups already la libreria contiene già {MAX_GROUPS} gruppi +@msg the library holds {MAX_ITEMS} items of this kind already la libreria contiene già {MAX_ITEMS} elementi di questo tipo +@msg the name is empty il nome è vuoto +@msg the object has no fill, stroke or effect l'oggetto non ha riempimento, traccia o effetto +@msg the object has no graphic style (pass `name`) l'oggetto non ha uno stile grafico (passa `name`) +@msg the objects don't match the plan gli oggetti non corrispondono al piano +@msg the open and permissions passwords must differ le password di apertura e di autorizzazione devono essere diverse +@msg the operand stack overflowed lo stack degli operandi ha superato il limite +@msg the page has no area la pagina non ha area +@msg the page's type in an unread object's box isn't in its page art il testo della pagina nel riquadro di un oggetto non letto non è nella grafica della pagina +@msg the paint behind the proxy is not a gradient la pittura dietro il proxy non è una sfumatura +@msg the parent must be a layer or sublayer il padre deve essere un livello o un sottolivello +@msg the path can't be extended il tracciato non può essere esteso +@msg the path can't be inside one of the objects to attach il tracciato non può trovarsi all'interno di uno degli oggetti da collegare +@msg the path has no segments to measure il tracciato non ha segmenti da misurare +@msg the path is empty il tracciato è vuoto +@msg the path is too short to shorten that much il tracciato è troppo corto per accorciarlo così tanto +@msg the picture has no objects l'immagine non ha oggetti +@msg the program runs too long il programma dura troppo a lungo +@msg the program uses too much memory il programma usa troppa memoria +@msg the rectangle must have a width and a height il rettangolo deve avere una larghezza e un'altezza +@msg the rectangle needs a size and must lie on the canvas il rettangolo richiede una dimensione e deve trovarsi nell'area di lavoro +@msg the selected art has no width la grafica selezionata non ha larghezza +@msg the selection draws nothing la selezione non disegna nulla +@msg the selection has no area la selezione non ha area +@msg the selection has no art to export la selezione non ha grafica da esportare +@msg the selection has no bounds la selezione non ha limiti +@msg the selection has no graphic style la selezione non ha uno stile grafico +@msg the slice would leave the canvas la sezione uscirebbe dall'area di lavoro +@msg the spot colour couldn't be added impossibile aggiungere il colore spot +@msg the subpath has no ends to extend (it is closed or a single point) il sottotracciato non ha estremità da estendere (è chiuso o è un singolo punto) +@msg the symbols are nested too deeply to expand their instances i simboli sono annidati troppo in profondità per espanderne le istanze +@msg the text file is too large to place as type (at most {_1} MB) il file di testo è troppo grande per essere inserito come testo (al massimo {_1} MB) +@msg the text is empty il testo è vuoto +@msg the text is over {_1} MB il testo supera {_1} MB +@msg the top object must be a path l'oggetto in primo piano deve essere un tracciato +@msg the trace was cancelled il ricalco è stato annullato +@msg the trailer has no size il trailer non ha una dimensione +@msg the type object is flattened to nothing l'oggetto di testo viene appiattito nel nulla +@msg the value of axis `{tag}` must be a number or null il valore dell'asse `{tag}` deve essere un numero o null +@msg the variable library declares more than {MAX_LIBRARY_DATASETS} data sets la libreria di variabili dichiara più di {MAX_LIBRARY_DATASETS} set di dati +@msg the variable library declares more than {MAX_LIBRARY_ENTITIES} entities la libreria di variabili dichiara più di {MAX_LIBRARY_ENTITIES} entità +@msg the variable library declares more than {MAX_LIBRARY_VARIABLES} variables la libreria di variabili dichiara più di {MAX_LIBRARY_VARIABLES} variabili +@msg the variable library declares no variables la libreria di variabili non dichiara alcuna variabile +@msg the variable library is larger than {MAX_LIBRARY_BYTES} bytes la libreria di variabili supera {MAX_LIBRARY_BYTES} byte +@msg the variable library is not UTF-8 la libreria di variabili non è in UTF-8 +@msg the variable library is not well-formed XML: {e} la libreria di variabili non è XML ben formato: {e} +@msg the variable library nests elements more than {MAX_LIBRARY_NESTING} deep la libreria di variabili annida elementi a più di {MAX_LIBRARY_NESTING} livelli di profondità +@msg the variable library's document type declaration holds something other than entities of plain text la dichiarazione del tipo di documento della libreria di variabili contiene qualcosa di diverso da entità di testo semplice +@msg the variable library's entities expand to more than {MAX_LIBRARY_BYTES} bytes le entità della libreria di variabili si espandono in più di {MAX_LIBRARY_BYTES} byte +@msg the window has no dock la finestra non ha un'area di ancoraggio +@msg the written PDF has no catalog il PDF scritto non ha un catalogo +@msg the written PDF has no cross-reference table il PDF scritto non ha una tabella dei riferimenti incrociati +@msg the written PDF's metadata can't be edited i metadati del PDF scritto non possono essere modificati +@msg there are no colour swatches to draw non ci sono campioni di colore da disegnare +@msg there is already a design named `{name}` esiste già un design chiamato `{name}` +@msg there is already a design named `{to}` esiste già un design chiamato `{to}` +@msg there is already a toolbar named `{name}` esiste già una barra degli strumenti chiamata `{name}` +@msg there is already a toolbar named `{to}` esiste già una barra degli strumenti chiamata `{to}` +@msg there is no library: create one first non c'è alcuna libreria: creane prima una +@msg these paths are too degenerate for Shaper questi tracciati sono troppo degeneri per Shaper +@msg thumbnail: no page {v} (the {what} has {count}) miniatura: nessuna pagina {v} ({what} ne ha {count}) +@msg tileRange of {what}: {e} tileRange di {what}: {e} +@msg title can't be empty title non può essere vuoto +@msg to (the new name) is required to (il nuovo nome) è obbligatorio +@msg to must be fill or stroke to deve essere fill o stroke +@msg to must be smooth or corner to deve essere smooth o corner +@msg too large for a zip archive: package to a folder troppo grande per un archivio zip: crea il pacchetto in una cartella +@msg too many Shaper edits troppe modifiche Shaper +@msg too many `points` (at most 100000) troppi `points` (al massimo 100000) +@msg too many characters: at most {MAX_CHARS} troppi caratteri: al massimo {MAX_CHARS} +@msg too many dots for this size: lower the frequency troppi punti per questa dimensione: riduci la frequenza +@msg too many filters over one file troppi filtri su un solo file +@msg too many overlapping paths for Shaper troppi tracciati sovrapposti per Shaper +@msg too many saved clips troppi ritagli salvati +@msg too many saved graphics states troppi stati grafici salvati +@msg toolbar (a toolbar's name) must be a string toolbar (il nome di una barra degli strumenti) deve essere una stringa +@msg toolbar name can't be empty il nome della barra degli strumenti non può essere vuoto +@msg transparentImages {v}: "flatten" (the default) or "omit" transparentImages {v}: "flatten" (il valore predefinito) o "omit" +@msg two saved selections would be named `{_1}` due selezioni salvate si chiamerebbero `{_1}` +@msg type dimension cannot be represented with this text transform la dimensione del testo non può essere rappresentata con questa trasformazione del testo +@msg type dimension must be finite la dimensione del testo deve essere finita +@msg unexpected resources risorse impreviste +@msg unknown CMYK profile `{name}` profilo CMYK sconosciuto `{name}` +@msg unknown File Info field `{k}` campo di Info file sconosciuto `{k}` +@msg unknown OpenType feature `{t}` in featureChanges (the tags `features` takes) funzione OpenType sconosciuta `{t}` in featureChanges (i tag accettati da `features`) +@msg unknown PDF preset `{name}` (presets: {_1}) predefinito PDF sconosciuto `{name}` (predefiniti: {_1}) +@msg unknown Warp style stile Altera sconosciuto +@msg unknown alignToPath `{a}` alignToPath sconosciuto `{a}` +@msg unknown arrowhead {v} punta di freccia sconosciuta {v} +@msg unknown artboardLayout key `{k}` (layout, columns, spacing, rightToLeft) chiave di artboardLayout sconosciuta `{k}` (layout, columns, spacing, rightToLeft) +@msg unknown attribute `{attribute}` attributo sconosciuto `{attribute}` +@msg unknown attribute `{k}` attributo sconosciuto `{k}` +@msg unknown axis `{other}` (vertical|horizontal|free) asse sconosciuto `{other}` (vertical|horizontal|free) +@msg unknown blend mode `{b}` metodo di fusione sconosciuto `{b}` +@msg unknown blend mode {b} metodo di fusione sconosciuto {b} +@msg unknown brush type `{o}` tipo di pennello sconosciuto `{o}` +@msg unknown category `{c}` categoria sconosciuta `{c}` +@msg unknown category `{c}` ({_1}) categoria sconosciuta `{c}` ({_1}) +@msg unknown character `{c}` carattere sconosciuto `{c}` +@msg unknown character style number field campo numerico dello stile di carattere sconosciuto +@msg unknown effect `{e}` effetto sconosciuto `{e}` +@msg unknown end {e} estremità sconosciuta {e} +@msg unknown flattenerPreset `{name}` (High Resolution, Medium Resolution, Low Resolution or a saved one: see flattener.presets.list) flattenerPreset sconosciuto `{name}` (High Resolution, Medium Resolution, Low Resolution o uno salvato: vedi flattener.presets.list) +@msg unknown format `{f}` (vcswatches, gpl, ase or css) formato sconosciuto `{f}` (vcswatches, gpl, ase o css) +@msg unknown from {f} from sconosciuto {f} +@msg unknown graph type `{t}` tipo di grafico sconosciuto `{t}` +@msg unknown grid colours `{name}` (one of {_1}) colori della griglia sconosciuti `{name}` (uno tra {_1}) +@msg unknown gridType `{t}` gridType sconosciuto `{t}` +@msg unknown highlight `{v}` evidenziazione sconosciuta `{v}` +@msg unknown intent `{i}` intento sconosciuto `{i}` +@msg unknown issue kind {id} tipo di problema sconosciuto {id} +@msg unknown issue kind {v} tipo di problema sconosciuto {v} +@msg unknown knockout state {k} stato di foratura sconosciuto {k} +@msg unknown language `{language}` (one of {_1}) lingua sconosciuta `{language}` (una tra {_1}) +@msg unknown language `{l}` (one of {_1}) lingua sconosciuta `{l}` (una tra {_1}) +@msg unknown method `{m}` ({_1}) metodo sconosciuto `{m}` ({_1}) +@msg unknown mode `{other}` (gray, rgb, hsb, lab, cmyk or web) metodo sconosciuto `{other}` (gray, rgb, hsb, lab, cmyk o web) +@msg unknown mode `{other}` (mono|cmyk) metodo sconosciuto `{other}` (mono|cmyk) +@msg unknown mode `{other}` (rgb|cmyk|gray|global) metodo sconosciuto `{other}` (rgb|cmyk|gray|global) +@msg unknown mode `{other}` (set, add, subtract) modalità sconosciuta `{other}` (set, add, subtract) +@msg unknown mode `{o}` modalità sconosciuta `{o}` +@msg unknown mode {m} modalità sconosciuta {m} +@msg unknown model `{other}` modello sconosciuto `{other}` +@msg unknown model `{other}` (keep, new or document) modello sconosciuto `{other}` (keep, new o document) +@msg unknown op {o} op sconosciuto {o} +@msg unknown order `{o}`: toggle, front, forward, backward or back ordine sconosciuto `{o}`: toggle, front, forward, backward o back +@msg unknown overprints `{v}` (preserve, simulate or discard) overprints sconosciuto `{v}` (preserve, simulate o discard) +@msg unknown panel pannello sconosciuto +@msg unknown pieLegend `{v}` (none, standard or wedges) pieLegend sconosciuto `{v}` (none, standard o wedges) +@msg unknown piePosition `{v}` (even, ratio or stacked) piePosition sconosciuto `{v}` (even, ratio o stacked) +@msg unknown pieSort `{v}` (none, all or first) pieSort sconosciuto `{v}` (none, all o first) +@msg unknown plate `{n}` lastra sconosciuta `{n}` +@msg unknown preference `{k}` preferenza sconosciuta `{k}` +@msg unknown profile {id} (see stroke.widthProfile.list) profilo sconosciuto {id} (vedi stroke.widthProfile.list) +@msg unknown proof target `{t}` destinazione di prova sconosciuta `{t}` +@msg unknown quotes key `{k}` (double, single) chiave di quotes sconosciuta `{k}` (double, single) +@msg unknown resolution `{v}` risoluzione sconosciuta `{v}` +@msg unknown rule `{rule}` ({_1}) regola sconosciuta `{rule}` ({_1}) +@msg unknown rule `{r}` (see color.harmony) regola sconosciuta `{r}` (vedi color.harmony) +@msg unknown setting `{k}` impostazione sconosciuta `{k}` +@msg unknown shape `{v}` (circle|ellipse|square|diamond|line) forma sconosciuta `{v}` (circle|ellipse|square|diamond|line) +@msg unknown spacing `{o}` (smooth|steps|distance) spaziatura sconosciuta `{o}` (smooth|steps|distance) +@msg unknown tileType `{t}` ({_1}) tileType sconosciuto `{t}` ({_1}) +@msg unknown units unità sconosciute +@msg unknown units `{u}` unità sconosciute `{u}` +@msg unknown value axis `{v}` (left, right or both) asse dei valori sconosciuto `{v}` (left, right o both) +@msg unknown variation `{v}` (tintsShades|warmCool|vividMuted) variazione sconosciuta `{v}` (tintsShades|warmCool|vividMuted) +@msg unknown warp style `{s}` stile Altera sconosciuto `{s}` +@msg unknown {key} `{v}` (none, short or full) {key} sconosciuto `{v}` (none, short o full) +@msg unknown {key} profile `{n}` profilo {key} sconosciuto `{n}` +@msg unknown {k} key `{key}` (size, position) chiave di {k} sconosciuta `{key}` (size, position) +@msg unreadable object oggetto illeggibile +@msg unsafe destination file name nome del file di destinazione non sicuro +@msg use paths or compound paths usa tracciati o tracciati composti +@msg useArtboards must be true or false, not {v} useArtboards deve essere true o false, non {v} +@msg value for `{var}` must match its variable kind il valore per `{var}` deve corrispondere al tipo della sua variabile +@msg variable `{_1}` is already defined la variabile `{_1}` è già definita +@msg variable `{name}` has no `trait` la variabile `{name}` non ha un `trait` +@msg variable `{name}` is already defined la variabile `{name}` è già definita +@msg variable `{name}` is declared twice la variabile `{name}` è dichiarata due volte +@msg variable `{to}` already exists la variabile `{to}` esiste già +@msg view is required view è obbligatorio +@msg view must be one of {_1} view deve essere uno tra {_1} +@msg warpStyle can only edit an applied Warp effect warpStyle può modificare solo un effetto Altera applicato +@msg width and height must be more than 0 and at most {limit} width e height devono essere maggiori di 0 e al massimo {limit} +@msg width factors must be finite i fattori di larghezza devono essere finiti +@msg width must be positive width deve essere positivo +@msg width out of range width fuori intervallo +@msg width/height must be > 0 width/height devono essere > 0 +@msg x and y must be finite numbers x e y devono essere numeri finiti +@msg x and y must be given together x e y devono essere indicati insieme +@msg zone needs an anchor: dock beside a panel with anchor, or in a new dock column with side (Start or End) zone richiede un ancoraggio: ancora accanto a un pannello con anchor, o in una nuova colonna di ancoraggio con side (Start o End) +@msg zoom must be positive zoom deve essere positivo +@msg {C}: no effect at index {list} {C}: nessun effetto all'indice {list} +@msg {DEFAULT_PRESET} is protected and stays {DEFAULT_PRESET} è protetto e rimane +@msg {DEFAULT_PRESET} is protected: save the settings under another name {DEFAULT_PRESET} è protetto: salva le impostazioni con un altro nome +@msg {LABEL} produced an empty result {LABEL} ha prodotto un risultato vuoto +@msg {_1} MB of document, at most {_2} MB {_1} MB di documento, al massimo {_2} MB +@msg {_1} MB of images, at most {_2} MB {_1} MB di immagini, al massimo {_2} MB +@msg {_1} bytes of RGBA data for a {width} × {height} image {_1} byte di dati RGBA per un'immagine di {width} × {height} +@msg {_1} can't be read, or changed since it was read: relink or update it first {_1} non può essere letto, o è cambiato da quando è stato letto: prima ricollegalo o aggiornalo +@msg {_1} can't go into {into} {_1} non può entrare in {into} +@msg {_1} can't limit printing to low resolution: choose PDF 1.4 or later, or allow high-resolution printing {_1} non può limitare la stampa alla bassa risoluzione: scegli PDF 1.4 o successivo, o consenti la stampa ad alta risoluzione +@msg {_1} files can't be opened yet i file {_1} non possono ancora essere aperti +@msg {_1} files can't be password-protected: remove the passwords or choose no standard i file {_1} non possono essere protetti da password: rimuovi le password o non scegliere alcuno standard +@msg {_1} files can't be {_2} i file {_1} non possono essere {_2} +@msg {_1} files can't carry editing data: turn off preserveEditing i file {_1} non possono contenere dati di modifica: disattiva preserveEditing +@msg {_1} files can't have PDF layers: turn off createLayers i file {_1} non possono avere livelli PDF: disattiva createLayers +@msg {_1} files need a creation date, and this platform has no clock to give one i file {_1} richiedono una data di creazione, e questa piattaforma non ha un orologio che la fornisca +@msg {_1} isn't a path {_1} non è un tracciato +@msg {_1} options: {e} opzioni {_1}: {e} +@msg {_1} paints nothing to rasterize {_1} non dipinge nulla da rasterizzare +@msg {_1} passwords can have up to {MAX_LEGACY_PASSWORD} ASCII characters (PDF 1.7 and later take any) le password {_1} possono avere fino a {MAX_LEGACY_PASSWORD} caratteri ASCII (PDF 1.7 e successivi accettano qualsiasi carattere) +@msg {_1} with linked images or an image map writes several files: use document.export {_1} con immagini collegate o una mappa immagine scrive più file: usa document.export +@msg {_1} writes one file per artboard here: use document.export, or name one artboard {_1} qui scrive un file per tavola da disegno: usa document.export, o indica una sola tavola da disegno +@msg {_1} × {_2} pixels is too large for {_3} (at most {max} pixels a side): lower the scale {_1} × {_2} pixel è troppo grande per {_3} (al massimo {max} pixel per lato): riduci la scala +@msg {_1}: belongs to another account {_1}: appartiene a un altro account +@msg {_1}: not a folder of its own {_1}: non è una cartella propria +@msg {_1}: not a plain file {_1}: non è un file normale +@msg {dest}: {e} {dest}: {e} +@msg {doc} has no folder {doc} non ha una cartella +@msg {file} isn't linked {file} non è collegato +@msg {folder} is not a package folder {folder} non è una cartella di pacchetto +@msg {folder}: more than {max} files and folders {folder}: più di {max} file e cartelle +@msg {folder}: {e} {folder}: {e} +@msg {g} is not a Live Paint group {g} non è un gruppo Pittura dinamica +@msg {id} is not a slice {id} non è una sezione +@msg {key} must be 0–{MAX_ADD_AROUND} pt {key} deve essere 0–{MAX_ADD_AROUND} pt +@msg {key} must be [x,y] {key} deve essere [x,y] +@msg {key} must be a percentage {min}..100 {key} deve essere una percentuale {min}..100 +@msg {key} must be a positive number, not {n} {key} deve essere un numero positivo, non {n} +@msg {key} must be a positive number, not {v} {key} deve essere un numero positivo, non {v} +@msg {key} must be one of {_1} {key} deve essere uno tra {_1} +@msg {key} must be {_1}, got {s} {key} deve essere {_1}, ricevuto {s} +@msg {key} must name a floating panel {key} deve indicare un pannello mobile +@msg {key} must name a panel in another group {key} deve indicare un pannello in un altro gruppo +@msg {key} must name a panel that shows {key} deve indicare un pannello visibile +@msg {key} must name a registered panel {key} deve indicare un pannello registrato +@msg {kind} files have no transparency, and the file would have {what}: flatten it first (object.flattenTransparency) i file {kind} non hanno trasparenza, e il file avrebbe {what}: appiattiscila prima (object.flattenTransparency) +@msg {kind} images can't be opened (see document.formats) le immagini {kind} non possono essere aperte (vedi document.formats) +@msg {k} `{v}` is not one of the choices {k} `{v}` non è una delle scelte disponibili +@msg {k} has control characters{_1} {k} contiene caratteri di controllo{_1} +@msg {k} is too long (at most {MAX_TEXT} characters) {k} è troppo lungo (al massimo {MAX_TEXT} caratteri) +@msg {k} must be > 0 {k} deve essere > 0 +@msg {k} must be a number (pt) or a length such as "210 mm" {k} deve essere un numero (pt) o una lunghezza come "210 mm" +@msg {k} must be a number from {min} to {max} {k} deve essere un numero compreso tra {min} e {max} +@msg {k} must be a number or "auto" {k} deve essere un numero o "auto" +@msg {k} must be a string {k} deve essere una stringa +@msg {k} must be text {k} deve essere testo +@msg {k} must be true or false {k} deve essere true o false +@msg {k} must be {{size?, position?}} {k} deve essere {{size?, position?}} +@msg {k}: can't read `{s}` as a length {k}: impossibile leggere `{s}` come lunghezza +@msg {label} must be a number at most {max} {label} deve essere un numero non superiore a {max} +@msg {label} must be a number from -1296 to 1296 {label} deve essere un numero compreso tra -1296 e 1296 +@msg {label} must be a number from 1 to 10000 {label} deve essere un numero compreso tra 1 e 10000 +@msg {name} `{s}`: {ids} {name} `{s}`: {ids} +@msg {name} is built in and can't be deleted {name} è incorporato e non può essere eliminato +@msg {name}: CRC or size mismatch {name}: CRC o dimensione non corrispondente +@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}: è stato possibile leggere solo l'anteprima incorporata di questo file Affinity; usa File › Apri per vederla con il relativo avviso, oppure esporta un SVG o un PDF da Affinity prima di inserirlo +@msg {name}: {e} {name}: {e} +@msg {native}; its embedded preview can't be read either ({e}) {native}; nemmeno la sua anteprima incorporata può essere letta ({e}) +@msg {std} files are CMYK, and the file would have colours in {_1} i file {std} sono CMYK, e il file avrebbe colori in {_1} +@msg {std} files embed their fonts, and the file would have one that isn't i file {std} incorporano i loro font, e il file ne avrebbe uno non incorporato +@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} è il file da cui è stato letto questo documento, e la lettura ha escluso alcuni elementi ({what}): sovrascriverlo li perderebbe definitivamente. Scrivi invece un altro file, o passa acknowledgeLoss: true per sostituirlo comunque +@msg {ty} brushes need selected art (or params.{k}) i pennelli {ty} richiedono grafica selezionata (o params.{k}) +@msg {v} is not an asset id {v} non è un id di risorsa +@msg {what} can't be intertwined {what} non può essere intrecciato +@msg {what} doesn't fit the paper with its bleed and marks {what} non entra nella carta con l'abbondanza e i segni +@msg {what} would print on more than {MAX_TILES} tiles: scale it down {what} verrebbe stampato su più di {MAX_TILES} riquadri: riducilo diff --git a/docs/localization-parity.md b/docs/localization-parity.md index cd8dc33f30..25c65d7943 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -50,7 +50,7 @@ plus review. Native-speaker review is human work and isn't in the hours. | Language | Code | Translated | Status | |---|---|---|---| | Traditional Chinese (Taiwan) | `zh-hant` | 4,892 rows, 790 of them `@msg` (counted 2026-10-21): menus, `tl!` literals and messages complete when counted (80% overall) | partial | -| Italian | `it` | 4,011 / 4,020 | full | +| Italian | `it` | 8,757 rows, 3,422 of them `@msg` and 48 `@msgplural` (counted 2026-11-13): the untranslated report lists 40 missing messages for it | full | | Dutch | `nl` | 7,299 rows, 1,609 of them `@msg` and 47 `@msgplural` (counted 2026-11-13 on `main` at `9a1a158a3`): in `COMPLETE_MESSAGES`, and the untranslated report lists the same missing messages for it as for German; one/other plurals | full | | Danish | `da` | 5,279 rows, 750 of them `@msg` (counted 2026-10-21): menus and `tl!` literals complete when counted; its messages are partial; one/other plurals | full | | Russian | `ru` | 4,009 / 4,020 | full | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index e7c1b28726..5d298b065e 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -870,6 +870,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Hindi (2026-11-06): Hindi (`hi`): 7,877-row machine-assisted catalog, zero/one singular plurals, desktop font fallback and optional Devanagari craft-fonts fallback; all 55 translation tests pass; native review and a web font remain; scores and estimates unchanged. [Terminology and limitations](localization-hi.md). - Hindi wording (2026-11-13): rows outside the plug-in messages keep their English's meaning, quantities and clauses, and use the catalog's save, swatch, crop, resolution, path and type terms; a test checks those terms and the point unit in every row; native review remains; scores and estimates unchanged. - Hindi terms (2026-11-13): every row that names a variable (data merge, variable fonts and widths, environment variables) says वेरिएबल, and joining paths, anchor points, point type and lines says जोड़ें instead of शामिल हों, which stays for joining the community; a test checks both terms in every row; scores and estimates unchanged. + - Italian messages (2026-11-13): the Italian catalog (`it`) has 1,802 more status and error message rows, and the untranslated report's missing Italian messages go from 1,842 to 40; the translations are VectorCraft's own and await native review; scores and estimates unchanged. See [localization-parity.md](localization-parity.md). - Window › Help Bar shows or hides the tool-help strip at the bottom of the window apart from the status bar (`window.helpBar`). Each setting is kept with the UI preferences and in saved workspaces, and Presentation Mode hides both bars. Layouts saved before the setting existed show the Help Bar where they show the status bar (`help_bar_toggle_is_independent_and_saved_in_workspaces`, `help_bar_and_status_bar_render_independently`, `legacy_help_bar_visibility_follows_each_layouts_status_bar`, `a_help_bar_setting_the_file_lacks_is_saved`). - Czech render-site labels: desktop/web file filters, diagnostics, suggested preset names and contributor tooltips/plurals now use catalog lookups; gradients have Czech adjective agreement and printer marks have a separate context (PDF/Print and Summary). Search, Home, toolbar/panel tooltips and Preferences already look up their labels. Every complete catalog has rows for these labels and for the Roman and Japanese printer mark styles. Evidence: `i18n::tests::czech_render_site_follow_up`, `i18n::tests::complete_catalogs_translate_render_site_labels`, `dialogs::tests_modal::printer_mark_labels_are_distinct_from_language_names`; see [localization-parity.md](localization-parity.md). - The History panel, Edit › Undo and Edit › Redo translate the names of the steps that apply or redefine a character or paragraph style and of the step that redefines a graphic style: each such step keeps its template (`Apply {name}`, `Redefine {name}`, `Redefine Graphic Style “{name}”`) and the style's name, every complete catalog has a row for each template, and the panel and the menu show the style's name as the document holds it, except the Normal styles' names, which they show in the interface language (`steps_named_after_a_style_list_the_template_around_the_name`, `undo_and_redo_name_a_style_step_around_the_style_name`). From aa18c36039234ec2f96d7b1dd99fe81524cccecb Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 15:11:53 -0800 Subject: [PATCH 11/63] UI commands: catalog row for Save Actions with folder preferences crates/mcp/src/ui_catalog.rs now carries the ui.actionSet.save params text from crates/ui-egui/src/menus/ui_commands.rs, so `cargo xtask ui-commands --check` passes. --- crates/mcp/src/ui_catalog.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/mcp/src/ui_catalog.rs b/crates/mcp/src/ui_catalog.rs index 8836b72ac2..92694697d6 100644 --- a/crates/mcp/src/ui_catalog.rs +++ b/crates/mcp/src/ui_catalog.rs @@ -1537,7 +1537,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "ui.actionSet.save", "Save Actions…", "", - "{set?: index, id?, path?} write the Actions panel's set at set (0 is the top set; default: the current set) with its actions to an action set file (.vcactions) at path, else a picked file named after the set (the web downloads it). id holds the panel's own set id, which its menu passes: the set with this id is saved wherever it is now, and set is not read. A set over 8 MiB as a file, with more than 1,000 actions or with an action of more than 10,000 steps is not written; the panel menu's Save Actions… → {path}", + "{set?: index, id?, path?} write the Actions panel's set at set (0 is the top set; default: the current set) with its actions to an action set file (.vcactions) at path, else a picked file named after the set (the web downloads it). id holds the panel's own set id, which its menu passes: the set with this id is saved wherever it is now, and set is not read. A set over 8 MiB as a file, with more than 1,000 actions or with an action of more than 10,000 steps is not written, nor is a set with a prefs.set step (also in a batch step) that sets a folder preference to a folder (an error names the action and the preference; an empty folder and the scratch disk choices Startup and None save); the panel menu's Save Actions… → {path}", ), ( "ui.actionSet.load", From 9594d39bd5aadf9a3dfff97fca03b2b98c5dc9f0 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 14:37:58 -0800 Subject: [PATCH 12/63] Edit Colors: no undo step when a recolor changes nothing Invert Colors, the Convert to commands, Saturate, Adjust Color Balance, the Blend commands and Recolor Artwork recorded an undo step even when they changed no color, for example Invert Colors with fills and strokes off, Saturate at 0 or a second Blend Front to Back. The empty step also cleared the redo steps. paint.invert and paint.complement on whole objects, Apply as Filter (addon.color.filter) and Apply Nearest Swatch run the same pass and did the same. These commands now run on a copy of the document and record the step only when something changed. Convert to Indexed Color uses the same helper. Recolor Artwork also compares the documents when it counted no recolored paint, so rewriting a color group's swatches still records its step. --- crates/engine/src/cmd/addons/color.rs | 4 +- crates/engine/src/cmd/colorcmds.rs | 78 ++++++++++++++++----------- crates/engine/src/cmd/paint.rs | 4 +- crates/engine/src/cmd/recolor.rs | 11 ++-- crates/engine/src/tests/editcolors.rs | 42 +++++++++++++++ crates/ui-egui/src/dialogs/recolor.rs | 4 +- docs/mcp.md | 7 ++- 7 files changed, 108 insertions(+), 42 deletions(-) diff --git a/crates/engine/src/cmd/addons/color.rs b/crates/engine/src/cmd/addons/color.rs index ced10c9912..54923a8440 100644 --- a/crates/engine/src/cmd/addons/color.rs +++ b/crates/engine/src/cmd/addons/color.rs @@ -43,7 +43,7 @@ pub fn specs() -> Vec { "Apply as Filter", ["Object", "Plug-ins", "Color Effects"], None, - "{effect: a colour adjustment's effect id (adjust.brightnessContrast, adjust.curves, adjust.hueSaturation, adjust.levels, adjust.shiftToColor, adjust.temperatureTint, addon.color.exposure, addon.color.desaturate, addon.color.duotone or addon.color.invert), params?: {…} (as effect.apply; missing keys take the dialog defaults), fill?: true, stroke?: true, includeImages?: true, includePatterns?: true, ids?: [id…] (default: the selection)} recolour the objects and everything inside them as the live effect would draw them, but in their colours, as edit.colors.invert does (fills, strokes, type, gradient meshes, embedded images and pattern tiles), as one undo step; their effects stay. Duotone links a colour to its one tone's swatch as the live effect does. On a path directly in a graph series, the graph also stores the new paint of the topmost visible fill and stroke (docs/mcp.md) → {changed}", + "{effect: a colour adjustment's effect id (adjust.brightnessContrast, adjust.curves, adjust.hueSaturation, adjust.levels, adjust.shiftToColor, adjust.temperatureTint, addon.color.exposure, addon.color.desaturate, addon.color.duotone or addon.color.invert), params?: {…} (as effect.apply; missing keys take the dialog defaults), fill?: true, stroke?: true, includeImages?: true, includePatterns?: true, ids?: [id…] (default: the selection)} recolour the objects and everything inside them as the live effect would draw them, but in their colours, as edit.colors.invert does (fills, strokes, type, gradient meshes, embedded images and pattern tiles), as one undo step (none when nothing changes); their effects stay. Duotone links a colour to its one tone's swatch as the live effect does. On a path directly in a graph series, the graph also stores the new paint of the topmost visible fill and stroke (docs/mcp.md) → {changed}", has_selection, filter ), @@ -70,7 +70,7 @@ pub fn specs() -> Vec { "Apply Nearest Swatch", ["Object", "Plug-ins", "Color Effects"], None, - "{library?: as addon.color.nearestSwatch (default: \"document\"), fill?: true, stroke?: true, ids?: [id…] (default: the selection)} give each solid colour and gradient stop of the selected objects' fills and strokes (their type's and those of the objects inside them; gradient meshes too) the colour of its nearest swatch in the library, as addon.color.nearestSwatch finds it, as one undo step: a global or spot swatch's colour links to it. A library's swatches the colours take are added to the document's first, as swatch.library.add adds them (a spot colour stays spot); images and patterns keep their colours. On a path directly in a graph series, the graph also stores the new paint of the topmost visible fill and stroke (docs/mcp.md) → {changed: paints changed, swatches: [the document swatches the colours took], added: [swatches added to the document]}", + "{library?: as addon.color.nearestSwatch (default: \"document\"), fill?: true, stroke?: true, ids?: [id…] (default: the selection)} give each solid colour and gradient stop of the selected objects' fills and strokes (their type's and those of the objects inside them; gradient meshes too) the colour of its nearest swatch in the library, as addon.color.nearestSwatch finds it, as one undo step (none when nothing changes): a global or spot swatch's colour links to it. A library's swatches the colours take are added to the document's first, as swatch.library.add adds them (a spot colour stays spot); images and patterns keep their colours. On a path directly in a graph series, the graph also stores the new paint of the topmost visible fill and stroke (docs/mcp.md) → {changed: paints changed, swatches: [the document swatches the colours took], added: [swatches added to the document]}", has_selection, nearest::apply ), diff --git a/crates/engine/src/cmd/colorcmds.rs b/crates/engine/src/cmd/colorcmds.rs index 6de2fbf9a2..2395c51f8e 100644 --- a/crates/engine/src/cmd/colorcmds.rs +++ b/crates/engine/src/cmd/colorcmds.rs @@ -36,7 +36,7 @@ pub fn specs() -> Vec { "Invert Colors", ["Edit", "Edit Colors"], None, - "{fill?: true, stroke?: true, includeImages?: true, includePatterns?: true, ids?} replace each colour by its RGB inverse (keeps the colour model) in fills, strokes, text, gradient meshes (with fills), colorized image fills (their pixels stay unchanged), other embedded images (a recoloured copy) and pattern tiles (a new pattern swatch; the original stays), as one undo step. On a path directly in a graph series, the graph also stores the new paint of the topmost visible fill and stroke (docs/mcp.md) → {changed, notChanged: {locked, hidden} (when the selection is everything select.all takes: the locked and hidden objects it left out whose colors would have changed; an object both hidden and locked counts as hidden), warning?: set when notChanged counts any object}", + "{fill?: true, stroke?: true, includeImages?: true, includePatterns?: true, ids?} replace each colour by its RGB inverse (keeps the colour model) in fills, strokes, text, gradient meshes (with fills), colorized image fills (their pixels stay unchanged), other embedded images (a recoloured copy) and pattern tiles (a new pattern swatch; the original stays), as one undo step (none when nothing changes). On a path directly in a graph series, the graph also stores the new paint of the topmost visible fill and stroke (docs/mcp.md) → {changed, notChanged: {locked, hidden} (when the selection is everything select.all takes: the locked and hidden objects it left out whose colors would have changed; an object both hidden and locked counts as hidden), warning?: set when notChanged counts any object}", has_selection, |s, p| recolor(s, p, "edit.colors.invert", "Invert Colors", &invert) ), @@ -104,7 +104,7 @@ pub fn specs() -> Vec { "Blend Front to Back", ["Edit", "Edit Colors"], None, - "{} ≥3 filled objects (solid fills or gradient meshes, which keep their shading): intermediate fills graded between the frontmost and backmost fill. On a path directly in a graph series, the graph also stores the new fill, as for the other Blend commands (docs/mcp.md) → {changed}", + "{} ≥3 filled objects (solid fills or gradient meshes, which keep their shading): intermediate fills graded between the frontmost and backmost fill. On a path directly in a graph series, the graph also stores the new fill, as for the other Blend commands (docs/mcp.md); no undo step when no fill changes → {changed}", has_selection, |s, _| blend(s, BlendOrder::Stack) ), @@ -113,7 +113,7 @@ pub fn specs() -> Vec { "Blend Horizontally", ["Edit", "Edit Colors"], None, - "{} ≥3 filled objects or meshes: fills graded between the leftmost and rightmost → {changed}", + "{} ≥3 filled objects or meshes: fills graded between the leftmost and rightmost; no undo step when no fill changes → {changed}", has_selection, |s, _| blend(s, BlendOrder::Horizontal) ), @@ -122,7 +122,7 @@ pub fn specs() -> Vec { "Blend Vertically", ["Edit", "Edit Colors"], None, - "{} ≥3 filled objects or meshes: fills graded between the topmost and bottommost → {changed}", + "{} ≥3 filled objects or meshes: fills graded between the topmost and bottommost; no undo step when no fill changes → {changed}", has_selection, |s, _| blend(s, BlendOrder::Vertical) ), @@ -457,8 +457,8 @@ pub(crate) fn recolor_links(s: &mut Session, p: &Value, cmd: &str, label: &str, run_pass(s, p, cmd, label, Recolor::with_links(Scope::of(p, cmd)?, f, links)) } -/// Run `pass` on the `ids` param's objects (default: the selection) as one undo step named `label`; -/// errors name the command `cmd`. The reply's `notChanged` counts the locked and hidden objects the +/// Run `pass` on the `ids` param's objects (default: the selection) as one undo step named `label`, +/// or none when the pass changes nothing; errors name the command `cmd`. The reply's `notChanged` counts the locked and hidden objects the /// selection left out ([`left_out`]) that the pass would have changed. fn run_pass(s: &mut Session, p: &Value, cmd: &str, label: &str, pass: Recolor) -> Result { let (ids, left) = match ids_param(p, "ids", cmd)? { @@ -471,20 +471,47 @@ fn run_pass(s: &mut Session, p: &Value, cmd: &str, label: &str, pass: Recolor) - } }; let colorized_ok = super::paint::check_colorized(&s.doc()?.doc).is_ok(); - let (n, not_changed) = s.edit(label, |d, _| { + let (n, not_changed) = edit_if_changed(s, label, |_, d| { let not_changed = NotChanged::of(pass.again().would_change(d, &left).into_iter().map(|(_, hidden)| hidden)); let n = keeping_series_paint(d, &ids, |d| { let n = pass.run(d, &ids); super::paint::check_colorized_kept(colorized_ok, d).map_err(|e| bad(cmd, e))?; Ok(n) })?; - Ok((n, not_changed)) + Ok(((n, not_changed), n > 0)) })?; let mut out = json!({ "changed": n }); not_changed.report(&mut out); Ok(out) } +/// [`Session::edit`] for an Edit Colors pass, which may change nothing: `f` runs on a copy of the +/// document (it gets the document as it is too) and returns its result with whether it changed the +/// copy. Only a changed copy becomes the document, as one undo step named `label`; otherwise the +/// undo and redo steps and the saved state stay as they were. +pub(crate) fn edit_if_changed(s: &mut Session, label: &str, f: impl FnOnce(&Document, &mut Document) -> Result<(T, bool)>) -> Result { + if s.script_running() { + return Err(EngineError::Busy); + } + // A typing session ends here as `s.edit` would end it, before the copy is taken, so that the + // copy holds what was typed. + if !s.previewing && super::typing_in_progress(s) { + super::settle_interaction(s)?; + } + let before = s.doc()?.doc.clone(); + let mut doc = (*before).clone(); + let (v, changed) = f(&before, &mut doc)?; + // The document is shared with `before` no longer, so `s.edit` does not copy it again. + drop(before); + if changed { + s.edit(label, |d, _| { + *d = doc; + Ok(()) + })?; + } + Ok(v) +} + /// Run `recolor` on `d` (it returns how many objects it changed) over `roots`, then store on their graphs what it changed /// on the graph series marks among `roots` and inside them, so the marks keep their new paint when the graph is drawn /// again. @@ -597,11 +624,6 @@ fn pass_targets(s: &Session, p: &Value, cmd: &str) -> Result { fn to_indexed(s: &mut Session, p: &Value) -> Result { const C: &str = "edit.colors.toIndexed"; let indexing = Indexing::of(s, p)?; - // A typing session ends here as `s.edit` would end it, before the copy is taken, so that the - // copy holds what was typed. - if !s.previewing && super::typing_in_progress(s) { - super::settle_interaction(s)?; - } let remake = |b: &ImageBlob| indexing.convert(b); let images = Scope { fill: false, stroke: false, images: true, patterns: false }; let pass = Recolor { remake: Some(&remake), ..Recolor::new(images, &same) }; @@ -609,22 +631,18 @@ fn to_indexed(s: &mut Session, p: &Value) -> Result { // indexed PNG yet; it is not converted to find out. The empty PNG only gives it a new key. let would = |b: &ImageBlob| (!b.indexed_png() && b.pixel_size().is_some()).then(|| ImageBlob::png(vec![])); let (ids, left) = pass_targets(s, p, C)?; - let st = s.doc()?; - let colorized_ok = super::paint::check_colorized(&st.doc).is_ok(); - let not_changed = NotChanged::of( - Recolor { remake: Some(&would), ..Recolor::new(images, &same) }.would_change(&st.doc, &left).into_iter().map(|(_, hidden)| hidden), - ); - let mut doc = (*st.doc).clone(); - let n = pass.run(&mut doc, &ids); - if let Some((w, h)) = indexing.too_large.get() { - return Err(bad(C, format!("an image of {w} × {h} pixels is too large to convert (at most {} pixels)", Indexing::MAX_PIXELS))); - } - if n > 0 { - s.edit("Convert to Indexed Color", |d, _| { - *d = doc; - super::paint::check_colorized_kept(colorized_ok, d).map_err(|e| bad(C, e)) - })?; - } + let (n, not_changed) = edit_if_changed(s, "Convert to Indexed Color", |now, doc| { + let colorized_ok = super::paint::check_colorized(now).is_ok(); + let not_changed = NotChanged::of( + Recolor { remake: Some(&would), ..Recolor::new(images, &same) }.would_change(now, &left).into_iter().map(|(_, hidden)| hidden), + ); + let n = pass.run(doc, &ids); + if let Some((w, h)) = indexing.too_large.get() { + return Err(bad(C, format!("an image of {w} × {h} pixels is too large to convert (at most {} pixels)", Indexing::MAX_PIXELS))); + } + super::paint::check_colorized_kept(colorized_ok, doc).map_err(|e| bad(C, e))?; + Ok(((n, not_changed), n > 0)) + })?; let mut out = json!({ "changed": n }); not_changed.report(&mut out); Ok(out) @@ -923,7 +941,7 @@ fn blend(s: &mut Session, order: BlendOrder) -> Result { super::paint::check_colorized_kept(colorized_ok, d).map_err(|e| bad(label, e))?; Ok(changed) }; - let changed = s.edit(label, |d, _| keeping_series_paint(d, &ids, run))?; + let changed = edit_if_changed(s, label, |_, d| keeping_series_paint(d, &ids, run).map(|n| (n, n > 0)))?; Ok(json!({ "changed": changed })) } diff --git a/crates/engine/src/cmd/paint.rs b/crates/engine/src/cmd/paint.rs index edab9cfe22..cfd6b89b4f 100644 --- a/crates/engine/src/cmd/paint.rs +++ b/crates/engine/src/cmd/paint.rs @@ -88,7 +88,7 @@ pub fn proxy_specs() -> Vec { "Invert", [], None, - "{stroke?: bool (default: the active proxy), ids?|id?, item?: appearance item index|null, target?: \"object\"|\"contents\"} invert the active proxy's colours of the selection (or ids; groups recolour their contents) keeping each colour's model, as edit.colors.invert does; the Appearance panel's active fill/stroke item (appearance.setActiveItem) is inverted instead when it is of that kind and no ids are given; with an object of a dynamic symbol instance selected by Direct Selection and no ids, id or item, that object's solid color changes for this instance only, and a gradient or pattern, or type whose characters differ in color, is refused; with nothing selected, invert the default. On a path directly in a graph series, the graph also stores the new color of the topmost visible fill or stroke (docs/mcp.md) → {changed}", + "{stroke?: bool (default: the active proxy), ids?|id?, item?: appearance item index|null, target?: \"object\"|\"contents\"} invert the active proxy's colours of the selection (or ids; groups recolour their contents) keeping each colour's model, as edit.colors.invert does; the Appearance panel's active fill/stroke item (appearance.setActiveItem) is inverted instead when it is of that kind and no ids are given; with an object of a dynamic symbol instance selected by Direct Selection and no ids, id or item, that object's solid color changes for this instance only, and a gradient or pattern, or type whose characters differ in color, is refused; with nothing selected, invert the default. On a path directly in a graph series, the graph also stores the new color of the topmost visible fill or stroke (docs/mcp.md); no undo step when no color changes → {changed}", has_doc, |s, p| proxy_recolor(s, p, "paint.invert", "Invert", &super::colorcmds::invert) ), @@ -97,7 +97,7 @@ pub fn proxy_specs() -> Vec { "Complement", [], None, - "{stroke?: bool (default: the active proxy), ids?|id?, item?: appearance item index|null, target?: \"object\"|\"contents\"} replace the active proxy's colours by their complements ((highest + lowest) − each component, over RGB or CMY; grey unchanged) keeping each colour's model; honours the Appearance panel's active item and a selected object of a dynamic symbol instance as paint.invert does; with nothing selected, the default → {changed}", + "{stroke?: bool (default: the active proxy), ids?|id?, item?: appearance item index|null, target?: \"object\"|\"contents\"} replace the active proxy's colours by their complements ((highest + lowest) − each component, over RGB or CMY; grey unchanged) keeping each colour's model; honours the Appearance panel's active item and a selected object of a dynamic symbol instance as paint.invert does; with nothing selected, the default; no undo step when no color changes → {changed}", has_doc, |s, p| proxy_recolor(s, p, "paint.complement", "Complement", &|c: Color| c.complement_keep_model()) ), diff --git a/crates/engine/src/cmd/recolor.rs b/crates/engine/src/cmd/recolor.rs index db0c0ac367..ede74545b6 100644 --- a/crates/engine/src/cmd/recolor.rs +++ b/crates/engine/src/cmd/recolor.rs @@ -15,7 +15,7 @@ use vectorcraft_doc::swatches::{node_colors, node_paints, override_paints}; use vectorcraft_doc::{Document, Node, NodeId, NodeKind}; use vectorcraft_geom::{Point, Rect}; -use super::colorcmds::{NotChanged, Recolor, Scope, keeping_series_paint, left_out}; +use super::colorcmds::{NotChanged, Recolor, Scope, edit_if_changed, keeping_series_paint, left_out}; use super::{CommandSpec, Result, Session, always, bad, checked_ids_param, cmd, color_value, has_doc, index, str_param, switch_or, text_param}; pub fn specs() -> Vec { @@ -34,7 +34,7 @@ pub fn specs() -> Vec { "Recolor Artwork", ["Edit", "Edit Colors"], None, - "{map: rows [{from: [colour keys (recolor.colors) or \"#rrggbb\" (every colour shown as that hex)], to: colour (hex, key, {c,m,y,k} or {l,a,b}) | null (in from, to and groupColors, #rrggbbaa and #rgba are refused), exclude?: false (the row's colours are kept), tint?: 100 (percent; with limitTo, art linked to the global or spot swatch the new color is takes this share of its tint)}] | {\"key or #rrggbb\": colour, …} (one row per colour), method?: \"exact\" (default)|\"preserveTints\"|\"scaleTints\"|\"tintsShades\"|\"hueShift\" (how a row's colours take its new colour, relative to the row's darkest, average or most saturated colour), limitTo?: swatch library id or name; or \"document\" (the document's swatches): new colours snap to its nearest colour, group?: colour group to rewrite, groupColors?: [colour] (what `group` is rewritten with; default: the rows' new colours, in order, excluded rows left out), rename?: the group's new name, includeImages?: true, includePatterns?: true, globalAdjust?: {saturation?: 0, brightness?: 0, temperature?: 0, luminosity?: 0} (Global Adjust: percent from -100 to 100, 0 changes nothing; every colour a row that isn't excluded recolours, and the group's new colours, take the same change after the row's new colour: saturation scales HSB saturation by 1 + saturation/100, brightness moves HSB brightness that share of the way to full or to black, temperature adds 0.2 × temperature/100 to red and takes it from blue, luminosity moves Lab lightness that share of the way to 100 or 0 keeping a and b; with limitTo each row's new colour takes the change before it snaps to the library)} recolour the selection (gradients, text, meshes, image pixels and pattern tiles included; each new colour keeps the model of the one it replaces (model?: \"keep\" default | \"new\" (the new colour's own model, so a CMYK colour replacing RGB art gives CMYK art) | \"document\" (the document's colour mode)), except a library swatch's with limitTo: art takes the swatch itself, linked to a global or spot colour at its tints, and the swatch joins the document's swatches; art linked to a global swatch of `group` that is rewritten with its row's new color stays linked to it, at its tint, unless the row converts the color into the art's model; other recolored art, and art in a row linked to a global swatch of `group` that is rewritten with another color, keeps its row's new color and loses its swatch link) and the group, as one undo step. On a path directly in a graph series, the graph also stores the new paint of the topmost visible fill and stroke (docs/mcp.md) → {changed, swatches: [names added to the document's swatches], relinked?: with group, the other paints linked to its global swatches that took their new colors, note?: set when a colour kept a model other than its new colour's (see model), notChanged: {locked, hidden} (as edit.colors.invert), warning?: set when notChanged counts any object}", + "{map: rows [{from: [colour keys (recolor.colors) or \"#rrggbb\" (every colour shown as that hex)], to: colour (hex, key, {c,m,y,k} or {l,a,b}) | null (in from, to and groupColors, #rrggbbaa and #rgba are refused), exclude?: false (the row's colours are kept), tint?: 100 (percent; with limitTo, art linked to the global or spot swatch the new color is takes this share of its tint)}] | {\"key or #rrggbb\": colour, …} (one row per colour), method?: \"exact\" (default)|\"preserveTints\"|\"scaleTints\"|\"tintsShades\"|\"hueShift\" (how a row's colours take its new colour, relative to the row's darkest, average or most saturated colour), limitTo?: swatch library id or name; or \"document\" (the document's swatches): new colours snap to its nearest colour, group?: colour group to rewrite, groupColors?: [colour] (what `group` is rewritten with; default: the rows' new colours, in order, excluded rows left out), rename?: the group's new name, includeImages?: true, includePatterns?: true, globalAdjust?: {saturation?: 0, brightness?: 0, temperature?: 0, luminosity?: 0} (Global Adjust: percent from -100 to 100, 0 changes nothing; every colour a row that isn't excluded recolours, and the group's new colours, take the same change after the row's new colour: saturation scales HSB saturation by 1 + saturation/100, brightness moves HSB brightness that share of the way to full or to black, temperature adds 0.2 × temperature/100 to red and takes it from blue, luminosity moves Lab lightness that share of the way to 100 or 0 keeping a and b; with limitTo each row's new colour takes the change before it snaps to the library)} recolour the selection (gradients, text, meshes, image pixels and pattern tiles included; each new colour keeps the model of the one it replaces (model?: \"keep\" default | \"new\" (the new colour's own model, so a CMYK colour replacing RGB art gives CMYK art) | \"document\" (the document's colour mode)), except a library swatch's with limitTo: art takes the swatch itself, linked to a global or spot colour at its tints, and the swatch joins the document's swatches; art linked to a global swatch of `group` that is rewritten with its row's new color stays linked to it, at its tint, unless the row converts the color into the art's model; other recolored art, and art in a row linked to a global swatch of `group` that is rewritten with another color, keeps its row's new color and loses its swatch link) and the group, as one undo step (none when nothing changes). On a path directly in a graph series, the graph also stores the new paint of the topmost visible fill and stroke (docs/mcp.md) → {changed, swatches: [names added to the document's swatches], relinked?: with group, the other paints linked to its global swatches that took their new colors, note?: set when a colour kept a model other than its new colour's (see model), notChanged: {locked, hidden} (as edit.colors.invert), warning?: set when notChanged counts any object}", has_doc, apply ), @@ -1661,7 +1661,7 @@ fn apply(s: &mut Session, p: &Value) -> Result { }; let rename = text_param(p, "rename", C)?; let colorized_ok = super::paint::check_colorized(&s.doc()?.doc).is_ok(); - let (changed, edit, added, converted, not_changed) = s.edit(label, |d, _| { + let (changed, edit, added, converted, not_changed) = edit_if_changed(s, label, |now, d| { // The rows' library swatches join the document (one it has with that name and colour is // used as it is). let mut added = vec![]; @@ -1716,7 +1716,10 @@ fn apply(s: &mut Session, p: &Value) -> Result { let not_changed = NotChanged::of( before.iter().filter(|(id, colors, _)| d.node(*id).is_some_and(|now| shown_colors(d, now) == *colors)).map(|(_, _, hidden)| *hidden), ); - Ok((changed, edit, added, map.converted.get(), not_changed)) + // Rewriting the group's swatches and relinking its art are not counted in `changed`, so a + // pass that counts nothing compares the documents. + let any = changed > 0 || !added.is_empty() || *d != *now; + Ok(((changed, edit, added, map.converted.get(), not_changed), any)) })?; let mut out = json!({ "changed": changed, "swatches": added }); not_changed.report(&mut out); diff --git a/crates/engine/src/tests/editcolors.rs b/crates/engine/src/tests/editcolors.rs index e3aa59368a..a1abfeb259 100644 --- a/crates/engine/src/tests/editcolors.rs +++ b/crates/engine/src/tests/editcolors.rs @@ -109,6 +109,48 @@ fn saturate_keeps_a_color_it_does_not_change() { assert_eq!(paint(&s, b), cmyk); } +/// An Edit Colors pass, a Blend or Recolor Artwork that changes nothing records no undo step, and the +/// step that redo would bring back stays; one that changes a color still records one. +#[test] +fn edit_colors_that_change_nothing_record_no_undo_step() { + let mut s = session(); + let gray = rect(&mut s, json!("#808080")); + let a = rect(&mut s, json!("#ff0000")); + let b = rect(&mut s, json!("#808080")); + let c = rect(&mut s, json!("#0000ff")); + s.execute("select.set", &json!({"ids": [a.0, b.0, c.0]})).unwrap(); + s.execute("edit.colors.blendFrontToBack", &json!({})).unwrap(); + // A step for redo to bring back. + s.execute("edit.colors.invert", &json!({"ids": [a.0]})).unwrap(); + s.execute("edit.undo", &json!({})).unwrap(); + let history = |s: &Session| { + let h = &s.doc().unwrap().history; + (h.undo.len(), h.redo.len()) + }; + let before = history(&s); + assert_eq!(before.1, 1, "one step to redo"); + let doc = s.doc().unwrap().doc.clone(); + let cases = [ + ("edit.colors.invert", json!({"fill": false, "stroke": false})), + ("edit.colors.saturate", json!({"intensity": 0})), + ("edit.colors.toRGB", json!({})), + ("edit.colors.adjustBalance", json!({"mode": "rgb", "r": 0})), + ("edit.colors.blendFrontToBack", json!({})), + ("recolor.apply", json!({"map": {"#ff0000": "#ff0000"}})), + // The complement of a gray is the same gray. + ("paint.complement", json!({"ids": [gray.0], "stroke": false})), + ("addon.color.filter", json!({"effect": "addon.color.invert", "fill": false, "stroke": false})), + ]; + for (id, p) in cases { + let v = s.execute(id, &p).unwrap(); + assert_eq!(v["changed"], 0, "{id}"); + assert_eq!(history(&s), before, "{id}: no undo step, and redo is kept"); + assert_eq!(s.doc().unwrap().doc, doc, "{id}: the document is unchanged"); + } + s.execute("edit.colors.invert", &json!({})).unwrap(); + assert_eq!(history(&s), (before.0 + 1, 0), "a pass that changes colors is one undo step"); +} + /// Adjust Color Balance refuses a channel that is not a number, which it would otherwise read as 0. #[test] fn adjust_balance_refuses_a_channel_that_is_not_a_number() { diff --git a/crates/ui-egui/src/dialogs/recolor.rs b/crates/ui-egui/src/dialogs/recolor.rs index ccdb4e8ca1..d2bbeb3013 100644 --- a/crates/ui-egui/src/dialogs/recolor.rs +++ b/crates/ui-egui/src/dialogs/recolor.rs @@ -1783,7 +1783,7 @@ mod tests { /// The Color Theme Picker's picks (the `themePicks` field) give the rows that aren't excluded /// the palette of the art picked, in row order, repeating it when the rows outnumber its /// colors, and the art previews them. Picks that touch no art leave the rows as they are, and - /// OK keeps the recolor as one undo step. + /// OK with rows that give the art its own colors back records no undo step. #[test] fn theme_picks_give_the_rows_the_palette_of_the_art_picked() { let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); @@ -1822,7 +1822,7 @@ mod tests { assert_eq!(app.session.doc().unwrap().history.undo.len(), undo, "nothing is committed before OK"); crate::dialogs::confirm(&mut app).unwrap(); assert_eq!((fill_hex(&app, red), fill_hex(&app, blue)), ("#ff0000".into(), "#0000ff".into()), "the excluded row kept red"); - assert_eq!(app.session.doc().unwrap().history.undo.len(), undo + 1, "one undo step"); + assert_eq!(app.session.doc().unwrap().history.undo.len(), undo, "the art is unchanged: no undo step"); } const RED: &str = "rgb 255 0 0"; diff --git a/docs/mcp.md b/docs/mcp.md index 707a414d0c..a37f88055b 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -3878,6 +3878,9 @@ For `edit.colors.*`, `fill: false` leaves colorized image fills unchanged. `reco `edit.colors.toGrayscale` and `edit.colors.adjustBalance` with `mode: "gray"` and `convert: true` store an embedded image with one gray channel, so a PDF writes it in gray. `includePatterns: false` leaves patterns out. The Blend commands also grade gradient meshes, which keep their shading. +The `edit.colors.*` commands, including the Blend commands, and `recolor.apply`, `paint.invert`, `paint.complement`, +`addon.color.filter` and `addon.color.applyNearestSwatch` record no undo step when they change nothing, for example +`edit.colors.invert` with `fill: false` and `stroke: false`, or a Blend whose objects already have the graded fills. `edit.colors.saturate` needs `intensity`, a number from -100 to 100 (a number outside the range counts as its nearest end). Without one, or with `null`, it answers `` missing number `intensity` ``; with a value that is not a number, such as `"40"`, it answers `` `intensity` must be a number, not "40" ``. Either way it changes nothing. @@ -10985,7 +10988,7 @@ the other colour adjustments (Effect › Color Adjustments); each colour keeps i `addon.color.filter {effect, params?, fill?, stroke?, includeImages?, includePatterns?, ids?}` applies any colour adjustment, the add-on's or one of Effect › Color Adjustments', to the colours themselves instead of as a live effect: fills, strokes, type, gradient meshes, embedded images and pattern tiles, as `edit.colors.invert` reaches them, as one -undo step. Missing `params` take the effect's dialog defaults; the objects' own effects stay as they are. +undo step (none when nothing changes). Missing `params` take the effect's dialog defaults; the objects' own effects stay as they are. ```json {"name":"run_command","arguments":{"command":"effect.apply","params":{"effect":"addon.color.exposure","params":{"exposure":1.5}}}} @@ -11152,7 +11155,7 @@ spot, deltaE}]}]}` (`values` as a default swatch name reads them: `C=10 M=20 Y=3 `addon.color.applyNearestSwatch {library?, fill?, stroke?, ids?}` (Object › Plug-ins › Color Effects › Find Nearest Swatch…, whose dialog lists each colour's three closest swatches with their difference; Apply runs it) gives each solid colour and gradient stop of the selected objects' fills and strokes (and gradient meshes) the colour of its nearest -swatch, as one undo step. A global or spot swatch's colour links to it. The library's swatches the colours take are +swatch, as one undo step (none when nothing changes). A global or spot swatch's colour links to it. The library's swatches the colours take are added to the document's first, as `swatch.library.add` adds them, so a spot colour from a colour book stays spot; images and patterns keep their colours. → `{changed, swatches: [the document swatches the colours took], added}` From 9484b116ab03e2d14ac98329c7728c87c92e8c11 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 15:31:04 -0800 Subject: [PATCH 13/63] Tests: screen-mode drags and popups start clear of the task bar In Full Screen Mode with Menu Bar and Full Screen Mode the canvas is about 390 pt wide, so the Contextual Task Bar under the selected rectangle now wraps onto a second row and covers the canvas's center. The middle-button pan, hand, zoom and context menu tests pressed there and reached the bar instead of the canvas. They now press low on the canvas, at a point the helper checks is clear of the bar. --- crates/ui-egui/src/tests/screenmode.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/tests/screenmode.rs b/crates/ui-egui/src/tests/screenmode.rs index 3154ccb6d9..181212bd61 100644 --- a/crates/ui-egui/src/tests/screenmode.rs +++ b/crates/ui-egui/src/tests/screenmode.rs @@ -80,6 +80,14 @@ fn in_screen_mode(ctx: &egui::Context, mode: u8) -> VectorcraftApp { app } +/// A point on the empty canvas, low enough to miss the Contextual Task Bar under the selection +/// (on a narrow canvas its actions wrap onto more rows and can reach the canvas's center). +fn open_canvas_point(app: &VectorcraftApp, ctx: &egui::Context) -> egui::Pos2 { + let p = app.canvas_rect.unwrap().lerp_inside(egui::vec2(0.5, 0.85)); + assert!(crate::canvas::task_bar_rect(ctx).is_none_or(|bar| !bar.expand(40.0).contains(p)), "the task bar covers {p:?}"); + p +} + fn begin_tools_move(app: &mut VectorcraftApp, ctx: &egui::Context) -> (Option<[f32; 2]>, egui::Pos2) { app.run("window.panel.float", json!({"panel": "tools", "x": 120, "y": 140})).unwrap(); frame(app, ctx, vec![]); @@ -241,7 +249,7 @@ fn navigation_drag_keeps_screen_mode(kind: &str) { _ => panic!("unknown navigation drag"), }; frame(&mut app, &ctx, vec![]); - let start = app.canvas_rect.unwrap().center(); + let start = open_canvas_point(&app, &ctx); let end = start + egui::vec2(35.0, 20.0); let pointer = |pos, pressed| egui::Event::PointerButton { pos, button, pressed, modifiers: Default::default() }; frame(&mut app, &ctx, vec![egui::Event::PointerMoved(start), pointer(start, true)]); @@ -551,7 +559,7 @@ fn held_escape_that_cancels_a_dialog_or_leaves_presentation_skips_a_command_boun fn a_popup_takes_escape_before_full_screen_exits() { let ctx = egui::Context::default(); let mut app = in_screen_mode(&ctx, 2); - let pos = app.canvas_rect.unwrap().center(); + let pos = open_canvas_point(&app, &ctx); let button = |pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Secondary, pressed, modifiers: Default::default() }; frame(&mut app, &ctx, vec![egui::Event::PointerMoved(pos), button(true)]); frame(&mut app, &ctx, vec![button(false)]); From 3429bbc76a6a9ee363756969e8d19aa4a5d335f9 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 14:16:15 -0800 Subject: [PATCH 14/63] Save, Export for Screens: warnings name an option the file doesn't read The save commands' warnings now follow an export's .ai rules: a legacy .ai file (aiVersion "7") reads only its own options, and the other .ai and .ait files don't read outlineText, hiddenObjects, transparentImages or preserveEditing. They name any option given beside the path that the file doesn't read, not only compress, version, preview and precision, and any such key in options. The bytes a save writes don't change. The Save Options dialog sends a .ai save only the options the chosen version reads, so a save from the dialog gets none of these warnings. When the document's own file is already a .ai, the .ai options that a save doesn't give and its file doesn't read keep their remembered values, so the dialog still shows them; the save doesn't write them, and a retry in another folder doesn't send them. Export for Screens and Export Assets already accepted a key in a format row or in a format's settings that only another format takes. Their warnings now name it as given (formats[0].quality, settings.png.quality). A key in a format's settings is named only when no row of that format, as the export writes it, reads it. The options of an exportPreset, and for assets.export the settings the document keeps, get no warning. Docs: params text of document.save, document.exportForScreens and assets.export; docs/mcp.md; docs/target-app-parity.md. --- crates/engine/src/cmd/assets.rs | 4 +- crates/engine/src/cmd/fileio.rs | 2 +- crates/engine/src/cmd/fileio/commands.rs | 2 +- crates/engine/src/cmd/fileio/export.rs | 27 +++-- crates/engine/src/cmd/fileio/save.rs | 79 +++++++++++++-- crates/engine/src/cmd/fileio/screens.rs | 94 ++++++++++++++++-- crates/engine/src/cmd/fileio/tests.rs | 4 +- .../engine/src/cmd/fileio/tests_exportas.rs | 99 +++++++++++++++++++ crates/engine/src/cmd/fileio/tests_screens.rs | 90 +++++++++++++++++ crates/engine/src/cmd/recovery.rs | 1 + crates/mcp/src/tests/fileio.rs | 10 +- crates/ui-egui/src/dialogs/save_options.rs | 10 +- .../ui-egui/src/dialogs/save_options/tests.rs | 27 +++++ crates/ui-egui/src/dialogs/tests_screens.rs | 33 +++++++ docs/mcp.md | 18 +++- docs/target-app-parity.md | 2 +- 16 files changed, 452 insertions(+), 50 deletions(-) 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/fileio.rs b/crates/engine/src/cmd/fileio.rs index 9154ce7ee5..97f957ed0e 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 5abe3e879c..e7656bb6d0 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, ,