From 69985f49980ccc2d0252695d84ab0eabffb76292 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sun, 1 Nov 2026 01:49:54 +0700 Subject: [PATCH 1/4] Fix #4928: Arrange Tools: Group Overlapping Objects addon.arrange.groupOverlapping groups the selected top-level objects whose visual bounds overlap or touch, chains included (union-find over a sweep along x, at most 20000 objects), only within one layer or group. Each group takes its frontmost member's stacking place; one undo step, all groups made in one pass over the tree. Menu item, tests, sweep case, catalog rows and docs. --- ROADMAP.md | 4 +- crates/engine/src/cmd/addons/arrange.rs | 90 ++++++++++++++++++- crates/engine/src/cmd/addons/arrange/tests.rs | 77 +++++++++++++++- crates/engine/tests/command_sweep.rs | 1 + crates/ui-egui/src/addons/arrange.rs | 3 +- crates/ui-egui/src/addons/arrange/tests.rs | 14 +++ crates/ui-egui/src/i18n/bg.tsv | 3 + crates/ui-egui/src/i18n/bs.tsv | 3 + crates/ui-egui/src/i18n/ca.tsv | 3 + crates/ui-egui/src/i18n/cs.tsv | 3 + crates/ui-egui/src/i18n/da.tsv | 3 + crates/ui-egui/src/i18n/de.tsv | 3 + crates/ui-egui/src/i18n/el.tsv | 3 + crates/ui-egui/src/i18n/es.tsv | 3 + crates/ui-egui/src/i18n/fi.tsv | 3 + crates/ui-egui/src/i18n/fr.tsv | 3 + crates/ui-egui/src/i18n/hu.tsv | 3 + crates/ui-egui/src/i18n/id.tsv | 3 + crates/ui-egui/src/i18n/it.tsv | 3 + crates/ui-egui/src/i18n/ja.tsv | 3 + crates/ui-egui/src/i18n/ka.tsv | 3 + crates/ui-egui/src/i18n/ko.tsv | 3 + crates/ui-egui/src/i18n/nl.tsv | 3 + crates/ui-egui/src/i18n/pl.tsv | 3 + crates/ui-egui/src/i18n/pt-br.tsv | 3 + crates/ui-egui/src/i18n/ru.tsv | 3 + crates/ui-egui/src/i18n/sv.tsv | 3 + crates/ui-egui/src/i18n/tr.tsv | 3 + crates/ui-egui/src/i18n/uk.tsv | 3 + crates/ui-egui/src/i18n/vi.tsv | 3 + crates/ui-egui/src/i18n/zh-hans.tsv | 3 + crates/ui-egui/src/i18n/zh-hant.tsv | 3 + docs/mcp.md | 1 + docs/target-app-parity.md | 7 +- docs/ui-parity.md | 4 +- 35 files changed, 272 insertions(+), 7 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 3511fe3f2..c943bbb02 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -9,7 +9,7 @@ **Stage: alpha** · next: beta, ~13 points (ready for real work ~62% → ~75%, and reliable `.ai` exchange) and ~170–270 h away -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** minor (Progress log rows for the Arrange Tools add-on, Export for Screens as the reference app's dialog and the 2026-10-31 community landings; headline numbers unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Progress log row for Group Overlapping Objects in the Arrange Tools add-on; headline numbers unchanged) · **Target:** Adobe Illustrator 2026 (30.x) VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adobe Illustrator workflow. It runs on macOS, Windows, Linux, FreeBSD and the web (WASM), and agents can drive all of it over MCP, the CLI and a JSON @@ -129,6 +129,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap ## Progress log +- 2026-11-01: Group Overlapping Objects joins the Arrange Tools add-on (Object › Plug-ins › Arrange Tools): the selected objects whose visual bounds overlap or touch, chains included, go into one group each, within their own layer or group, at the stacking place of their frontmost member; objects that overlap nothing stay as they are. One undo step, a sweep along x so thousands of loose pieces group quickly (at most 20000 objects). A VectorCraft addition: headline numbers unchanged. - 2026-10-31: Export for Screens works as the reference app's dialog: Artboards and Assets tabs with a grid or list of thumbnails, Clear Selection and Prefix, Create Sub-folders by Scale or Format (on by default), Export PDFs As Single File (the chosen artboards as one PDF's pages, `pdfs: "single"`) or Multiple Files, iOS and Android rows, format presets in a ☰ menu, and a count of the files the export writes (#4895). About 55 community PRs and fixes landed the same day: Find Font, Replace Font, Outline Stroke and Expand leave hidden and locked art alone (with a warning); DXF export writes LINE and ELLIPSE entities and outlines only the strokes a CAD line can't draw; Blob Brush Angle and Roundness; Shift-constrained Perspective Selection; Grid Repeat Options Preview; a gradient mesh from a file budgeted at about a million quads; width-profile strokes keep their shape zoomed out; `.ai`/EPS import fidelity (area type, envelopes, transparency in dictionaries, character styles); PDF import finds hyphenated and CMap-suffixed font names; Windows renders follow the process's core affinity; Enter or Escape in a floating panel's field no longer reaches the canvas (#4803); Properties shows Blend Options for any selected blend (#4817); Artwork Cleanup finds doubled type (#4835) and joins stacked lines into one object (Join Point Text, #4826). Headline numbers unchanged. - 2026-10-31: Arrange Tools, a built-in add-on (Object › Plug-ins › Arrange Tools): Arrange in Grid lays the selected objects out in rows and columns, a row or a column with gaps, from the selection's top-left or centred on the artboard; Swap Objects swaps two objects' places (and, with Swap Objects and Sizes, their sizes); Replace with Top Object puts a copy of the topmost selected object in place of each of the others, kept, stretched or fitted in proportion; Set Size of Each gives every selected object an exact width and/or height about a reference point; Zoom to Selection fits the view to the selection. Each is one undo step with a previewed dialog where it has options. A VectorCraft addition: headline numbers unchanged. - 2026-10-30: Gradient panel presets: a Presets menu applies one of the built-in gradient libraries' presets (grouped by theme) or a saved one to the gradient behind the active proxy, keeping its placement, as one undo step; the panel menu saves the current gradient as a preset, kept with the preferences, and deletes saved ones (`gradient.presets.list/apply/save/delete`). @@ -204,6 +205,7 @@ The full earlier record of what landed is kept in [docs/roadmap.md](docs/roadmap | Date | Change | Summary | |---|---|---| +| 2026-11-01 | minor | Progress log row for Group Overlapping Objects (Arrange Tools add-on, #4928); headline numbers unchanged. | | 2026-10-31 | minor | Progress log row for Export for Screens laid out as the reference app's (#4895) and the 2026-10-31 community landings; headline numbers unchanged. | | 2026-10-31 | minor | Progress log row for the Arrange Tools add-on; headline numbers unchanged. | | 2026-10-29 | minor | Progress log row for the Linux Flatpak clipboard's core Wayland protocol text fallback; headline numbers unchanged. | diff --git a/crates/engine/src/cmd/addons/arrange.rs b/crates/engine/src/cmd/addons/arrange.rs index 2011559c3..986016213 100644 --- a/crates/engine/src/cmd/addons/arrange.rs +++ b/crates/engine/src/cmd/addons/arrange.rs @@ -2,14 +2,17 @@ //! out in rows and columns, a row or a column; Swap Objects swaps the places (and sizes) of two; //! Replace with Top Object puts a copy of the topmost selected object in place of each of the //! others; and Set Size of Each gives every selected object an exact width and height. Places and -//! sizes are measured and set as `object.setBounds` does. Its submenu (Object › Plug-ins › Arrange +//! sizes are measured and set as `object.setBounds` does. Group Overlapping Objects groups the +//! objects whose visual bounds overlap. Its submenu (Object › Plug-ins › Arrange //! Tools), its dialogs and Zoom to Selection (a view command) are the UI's. #[cfg(test)] mod tests; +use std::sync::Arc; + use serde_json::{Value, json}; -use vectorcraft_doc::{NodeId, OrientedBox}; +use vectorcraft_doc::{Node, NodeId, OrientedBox}; use vectorcraft_geom::{Affine, Point, Rect}; use vectorcraft_render::effects; @@ -25,6 +28,8 @@ const LIMIT: f64 = 1e6; const LEAST: f64 = 0.001; /// The most columns Arrange in Grid takes. const MAX_COLUMNS: u64 = 10_000; +/// The most objects Group Overlapping Objects looks at. +const MAX_OBJECTS: usize = 20_000; pub fn specs() -> Vec { vec![ @@ -64,6 +69,15 @@ pub fn specs() -> Vec { has_selection, size ), + cmd!( + "addon.arrange.groupOverlapping", + "Group Overlapping Objects", + ["Object", "Plug-ins", "Arrange Tools"], + None, + "{} group the selected objects (the top-level ones, at most 20000) wherever their visual bounds overlap or touch, chains included (when A overlaps B and B overlaps C, all three go in one group), as one undo step. Only objects in the same layer or group are grouped together, objects that overlap nothing stay as they are, and each new group goes where its frontmost member was in the stacking order; the new groups and the objects left alone stay selected → {groups: [id…], grouped (how many objects went into groups)}", + has_multi, + group_overlapping + ), ] } @@ -270,3 +284,75 @@ fn size(s: &mut Session, p: &Value) -> Result { transform_each(s, "Set Size of Each", strokes, &xfs)?; Ok(json!({ "count": xfs.len() })) } + +/// The sets of two or more of `items` (each in the order given) whose rects overlap or touch, +/// chains included, among items of the same key (parent). A sweep along x: only the rects still +/// open where each one starts are compared with it. +fn overlapping(items: &[(K, Rect)]) -> Vec> { + fn root(up: &mut [usize], mut i: usize) -> usize { + while up[i] != i { + up[i] = up[up[i]]; + i = up[i]; + } + i + } + let mut up: Vec = (0..items.len()).collect(); + let mut order: Vec = (0..items.len()).collect(); + order.sort_by(|a, b| items[*a].1.x0.total_cmp(&items[*b].1.x0)); + let mut open: Vec = vec![]; + for i in order { + let (key, r) = (&items[i].0, items[i].1); + open.retain(|&j| items[j].1.x1 >= r.x0); + for &j in &open { + let (k, o) = (&items[j].0, items[j].1); + if k == key && o.y0 <= r.y1 && r.y0 <= o.y1 { + let (a, b) = (root(&mut up, i), root(&mut up, j)); + up[a] = b; + } + } + open.push(i); + } + let mut sets: Vec> = vec![vec![]; items.len()]; + for i in 0..items.len() { + let r = root(&mut up, i); + sets[r].push(i); + } + sets.retain(|s| s.len() > 1); + sets.sort_by_key(|s| s.first().copied()); + sets +} + +fn group_overlapping(s: &mut Session, _: &Value) -> Result { + const C: &str = "addon.arrange.groupOverlapping"; + let roots = selected_roots(s)?; + if roots.len() > MAX_OBJECTS { + return Err(bad(C, "too many objects selected (at most 20000)")); + } + let d = &s.doc()?.doc; + let items: Vec<(NodeId, (Option, Rect))> = + roots.iter().filter_map(|id| Some((*id, (d.parent_of(*id), effects::preview::bounds_of(d, &[*id], true)?)))).collect(); + let keyed: Vec<(Option, Rect)> = items.iter().map(|(_, k)| *k).collect(); + let sets: Vec> = overlapping(&keyed).into_iter().map(|set| set.iter().filter_map(|i| Some(items.get(*i)?.0)).collect()).collect(); + if sets.is_empty() { + return Ok(json!({ "groups": [], "grouped": 0 })); + } + let grouped: std::collections::HashSet = sets.iter().flatten().copied().collect(); + let groups = s.edit("Group Overlapping Objects", |d, sel| { + // Every group at once: the members behind each front one go, then each front one is + // replaced by its group (one pass over the tree each, however many groups). + let (mut places, mut behind, mut groups) = (vec![], vec![], vec![]); + for set in &sets { + let Some((&front, rest)) = set.split_last() else { continue }; + let children: Vec> = set.iter().filter_map(|id| d.node(*id).cloned().map(Arc::new)).collect(); + let gid = d.alloc_id(); + places.push((front, vec![Node::group(gid, children)])); + behind.extend_from_slice(rest); + groups.push(gid); + } + d.remove_many(&behind)?; + d.insert_beside(places, true)?; + sel.set(groups.iter().copied().chain(roots.iter().copied().filter(|id| !grouped.contains(id)))); + Ok(groups) + })?; + Ok(json!({ "groups": groups.iter().map(|i| i.0).collect::>(), "grouped": grouped.len() })) +} diff --git a/crates/engine/src/cmd/addons/arrange/tests.rs b/crates/engine/src/cmd/addons/arrange/tests.rs index db19cc1e9..48f9ae4ec 100644 --- a/crates/engine/src/cmd/addons/arrange/tests.rs +++ b/crates/engine/src/cmd/addons/arrange/tests.rs @@ -256,8 +256,83 @@ fn off_without_a_selection() { let mut s = session(); rect(&mut s, 0.0, 0.0, 20.0, 10.0); s.execute("select.none", &json!({})).unwrap(); - for c in ["addon.arrange.grid", "addon.arrange.swap", "addon.arrange.replace", "addon.arrange.size"] { + for c in ["addon.arrange.grid", "addon.arrange.swap", "addon.arrange.replace", "addon.arrange.size", "addon.arrange.groupOverlapping"] { assert!(!enabled(&s, c), "{c}"); } assert!(!enabled(&Session::new(), "addon.arrange.swap")); } + +fn ids_of(v: &Value) -> Vec { + v.as_array().unwrap().iter().map(|i| NodeId(i.as_u64().unwrap())).collect() +} + +fn children(s: &Session, id: NodeId) -> Vec { + s.doc().unwrap().doc.node(id).unwrap().children().unwrap().iter().map(|n| n.id).collect() +} + +#[test] +fn group_overlapping_groups_chains_and_leaves_loners() { + let mut s = session(); + // a–b–c a chain (c only touches b), d alone, e over f. + let a = rect(&mut s, 0.0, 0.0, 20.0, 20.0); + let d = rect(&mut s, 200.0, 0.0, 10.0, 10.0); + let b = rect(&mut s, 15.0, 15.0, 20.0, 20.0); + let e = rect(&mut s, 100.0, 100.0, 30.0, 30.0); + let c = rect(&mut s, 35.0, 0.0, 10.0, 20.0); + let f = rect(&mut s, 110.0, 110.0, 5.0, 5.0); + select(&mut s, &[a, b, c, d, e, f]); + let n = steps(&s); + let r = s.execute("addon.arrange.groupOverlapping", &json!({})).unwrap(); + assert_eq!(steps(&s), n + 1, "one undo step"); + assert_eq!(r["grouped"], json!(5)); + let groups = ids_of(&r["groups"]); + assert_eq!(groups.len(), 2); + assert_eq!(children(&s, groups[0]), [a, b, c], "members keep their stacking order"); + assert_eq!(children(&s, groups[1]), [e, f]); + // Each group where its frontmost member was: a d [abc] [ef] → d, group(abc) above it, then (ef). + let layer: Vec = s.doc().unwrap().doc.layers[0].children().unwrap().iter().map(|n| n.id).collect(); + assert_eq!(layer, [d, groups[0], groups[1]]); + let sel = &s.doc().unwrap().selection.objects; + assert!(sel.contains(&groups[0]) && sel.contains(&groups[1]) && sel.contains(&d) && sel.len() == 3, "{sel:?}"); + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(s.doc().unwrap().doc.layers[0].children().unwrap().len(), 6); +} + +#[test] +fn group_overlapping_keeps_layers_apart_and_measures_visual_bounds() { + let mut s = session(); + let a = rect(&mut s, 0.0, 0.0, 20.0, 20.0); + let gap = rect(&mut s, 22.0, 0.0, 20.0, 20.0); + s.execute("layer.new", &json!({})).unwrap(); + let other = rect(&mut s, 10.0, 10.0, 20.0, 20.0); + select(&mut s, &[a, gap, other]); + // 2 pt apart: nothing overlaps within a layer, and a and `other` are in different layers. + let n = steps(&s); + let r = s.execute("addon.arrange.groupOverlapping", &json!({})).unwrap(); + assert_eq!((r["grouped"].clone(), steps(&s)), (json!(0), n), "no groups, no step"); + // A 4 pt stroke reaches 2 pt out: the two in the first layer now touch. + select(&mut s, &[a, gap]); + s.execute("paint.setStroke", &json!({"color": "#000000"})).unwrap(); + s.execute("stroke.set", &json!({"weight": 4})).unwrap(); + select(&mut s, &[a, gap, other]); + let r = s.execute("addon.arrange.groupOverlapping", &json!({})).unwrap(); + let groups = ids_of(&r["groups"]); + assert_eq!(groups.len(), 1); + assert_eq!(children(&s, groups[0]), [a, gap]); + let doc = &s.doc().unwrap().doc; + assert_ne!(doc.layer_of(other), doc.layer_of(a), "left in its own layer"); +} + +#[test] +fn group_overlapping_is_off_with_one_object_and_scales_to_many() { + let mut s = session(); + let a = rect(&mut s, 0.0, 0.0, 20.0, 20.0); + select(&mut s, &[a]); + assert!(!enabled(&s, "addon.arrange.groupOverlapping")); + // A long row of pairs: each pair overlaps, neighbouring pairs don't. + let rects: Vec<(usize, Rect)> = + (0..4000).map(|i| (0, Rect::new(i as f64 * 10.0, 0.0, i as f64 * 10.0 + if i % 2 == 0 { 12.0 } else { 3.0 }, 5.0))).collect(); + let sets = super::overlapping(&rects); + assert_eq!(sets.len(), 2000); + assert!(sets.iter().all(|s| s.len() == 2 && s[1] == s[0] + 1)); +} diff --git a/crates/engine/tests/command_sweep.rs b/crates/engine/tests/command_sweep.rs index 69c69097e..14d7f9559 100644 --- a/crates/engine/tests/command_sweep.rs +++ b/crates/engine/tests/command_sweep.rs @@ -588,6 +588,7 @@ fn structured_junk() { ("addon.arrange.replace", json!({"fit": "bounds", "scaleStrokes": true})), ("addon.arrange.size", json!({"width": 1e6, "height": 0.001, "reference": 8, "scaleStrokes": true})), ("addon.arrange.size", json!({"width": 0.001, "proportional": true, "reference": u64::MAX})), + ("addon.arrange.groupOverlapping", json!({"ids": "x", "junk": f64::MAX})), // Smart Pen add-on. ("addon.pen.append", json!({"id": 2, "x": 1e308, "y": 0, "in": [1e308, -1e308]})), ("addon.pen.append", json!({"id": 2, "subpath": u64::MAX, "x": 0, "y": 0, "segment": "curved"})), diff --git a/crates/ui-egui/src/addons/arrange.rs b/crates/ui-egui/src/addons/arrange.rs index 8435483cb..f33757cee 100644 --- a/crates/ui-egui/src/addons/arrange.rs +++ b/crates/ui-egui/src/addons/arrange.rs @@ -1,6 +1,6 @@ //! Arrange Tools in the interface: its submenu (Object › Plug-ins › Arrange Tools); the dialogs of //! Arrange in Grid, Replace with Top Object and Set Size of Each, which preview on the canvas and -//! run their `addon.arrange.*` command on OK; and Zoom to Selection ([`ZOOM`]), which fits the +//! run their `addon.arrange.*` command on OK; Group Overlapping Objects; and Zoom to Selection ([`ZOOM`]), which fits the //! active view to the selection. use egui::Ui; @@ -38,6 +38,7 @@ pub fn menu() -> Vec { Item::Cmd("Swap Objects and Sizes", "addon.arrange.swap", json!({ "size": true })), Item::Cmd("Replace with Top Object…", "addon.arrange.replace", Value::Null), Item::Cmd("Set Size of Each…", "addon.arrange.size", Value::Null), + Item::Cmd("Group Overlapping Objects", "addon.arrange.groupOverlapping", json!({})), Item::Sep, Item::Cmd("Zoom to Selection", ZOOM, json!({})), ] diff --git a/crates/ui-egui/src/addons/arrange/tests.rs b/crates/ui-egui/src/addons/arrange/tests.rs index 63e58448b..72411f388 100644 --- a/crates/ui-egui/src/addons/arrange/tests.rs +++ b/crates/ui-egui/src/addons/arrange/tests.rs @@ -40,6 +40,7 @@ fn the_plugins_submenu_lists_the_tools() { ("Swap Objects and Sizes", "addon.arrange.swap"), ("Replace with Top Object…", "addon.arrange.replace"), ("Set Size of Each…", "addon.arrange.size"), + ("Group Overlapping Objects", "addon.arrange.groupOverlapping"), ("Zoom to Selection", ZOOM), ] { assert_eq!(under(label).and_then(|e| e.command.as_deref()), Some(id), "{label}"); @@ -125,3 +126,16 @@ fn zoom_to_selection_fits_the_selection_in_the_view() { assert!(!menus::enabled(&app, ZOOM)); assert!(app.run(ZOOM, json!({})).is_err()); } + +#[test] +fn group_overlapping_objects_runs_directly() { + let (mut app, a, b) = app(); + let c = NodeId(app.run("shape.rectangle", json!({"x": 10, "y": 5, "width": 20, "height": 20})).unwrap()["id"].as_u64().unwrap()); + app.run("select.all", json!({})).unwrap(); + click(&mut app, "Group Overlapping Objects"); + assert!(app.ui.dialog.is_none()); + let doc = &app.session.doc().unwrap().doc; + let g = doc.parent_of(a).unwrap(); + assert_eq!(doc.parent_of(c), Some(g), "a and c overlap: grouped"); + assert_ne!(doc.parent_of(b), Some(g), "b is left alone"); +} diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index d33addad4..a960e7dd2 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -5926,6 +5926,9 @@ breakText Glyphs Глифи Set Size of Each Задаване на размер на всеки @msg select exactly two objects изберете точно два обекта @msg give `width` or `height` задайте `width` или `height` +# Arrange Tools add-on: Group Overlapping Objects. + Group Overlapping Objects Групиране на припокриващи се обекти +@msg too many objects selected (at most 20000) избрани са твърде много обекти (най-много 20000) Marathi Маратхи Gujarati Гуджарати Bengali Бенгалски diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index f527ca255..6dba3afdf 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -5960,6 +5960,9 @@ shape Open Otvoren Set Size of Each Postavi veličinu svakog @msg select exactly two objects odaberite tačno dva objekta @msg give `width` or `height` zadajte `width` ili `height` +# Arrange Tools add-on: Group Overlapping Objects. + Group Overlapping Objects Grupiši preklapajuće objekte +@msg too many objects selected (at most 20000) odabrano je previše objekata (najviše 20000) Marathi Marathi Gujarati Gudžarati Bengali Bengalski diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 5873fbada..a102f7a7c 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -6033,6 +6033,9 @@ breakText Glyphs Glifs Set Size of Each Defineix la mida de cadascun @msg select exactly two objects selecciona exactament dos objectes @msg give `width` or `height` indica `width` o `height` +# Arrange Tools add-on: Group Overlapping Objects. + Group Overlapping Objects Agrupa els objectes superposats +@msg too many objects selected (at most 20000) s’han seleccionat massa objectes (com a màxim 20000) Marathi Marathi Gujarati Gujarati Bengali Bengalí diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 8f7a60710..b0c04ffe2 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -6043,6 +6043,9 @@ breakText Glyphs Glyfy Set Size of Each Nastavit velikost jednotlivě @msg select exactly two objects vyberte právě dva objekty @msg give `width` or `height` zadejte `width` nebo `height` +# Arrange Tools add-on: Group Overlapping Objects. + Group Overlapping Objects Seskupit překrývající se objekty +@msg too many objects selected (at most 20000) vybráno příliš mnoho objektů (nejvýše 20000) Marathi Maráthština Gujarati Gudžarátština Bengali Bengálština diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 8b67b907a..d85c865bf 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -5940,6 +5940,9 @@ breakText Glyphs Glyffer Set Size of Each Angiv størrelse for hver @msg select exactly two objects markér præcis to objekter @msg give `width` or `height` angiv `width` eller `height` +# Arrange Tools add-on: Group Overlapping Objects. + Group Overlapping Objects Gruppér overlappende objekter +@msg too many objects selected (at most 20000) for mange objekter markeret (højst 20000) Marathi Marathi Gujarati Gujarati Bengali Bengali diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 7400e6f73..b1e187de8 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -5628,6 +5628,9 @@ breakText Glyphs Glyphen Set Size of Each Größe einzeln festlegen @msg select exactly two objects wählen Sie genau zwei Objekte aus @msg give `width` or `height` geben Sie `width` oder `height` an +# Arrange Tools add-on: Group Overlapping Objects. + Group Overlapping Objects Überlappende Objekte gruppieren +@msg too many objects selected (at most 20000) zu viele Objekte ausgewählt (höchstens 20000) Marathi Marathi Gujarati Gujarati Bengali Bengalisch diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 19ba17dd7..76d29ca75 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -5995,6 +5995,9 @@ breakText Glyphs Γλύφοι Set Size of Each Ορισμός μεγέθους καθενός @msg select exactly two objects επιλέξτε ακριβώς δύο αντικείμενα @msg give `width` or `height` δώστε `width` ή `height` +# Arrange Tools add-on: Group Overlapping Objects. + Group Overlapping Objects Ομαδοποίηση επικαλυπτόμενων αντικειμένων +@msg too many objects selected (at most 20000) επιλέχθηκαν πάρα πολλά αντικείμενα (το πολύ 20000) Marathi Μαραθικά Gujarati Γκουγιαράτι Bengali Βεγγαλικά diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 884ee8e7e..f1d2645dc 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -5985,6 +5985,9 @@ breakText Glyphs Glifos Set Size of Each Definir el tamaño de cada uno @msg select exactly two objects selecciona exactamente dos objetos @msg give `width` or `height` indica `width` o `height` +# Arrange Tools add-on: Group Overlapping Objects. + Group Overlapping Objects Agrupar objetos superpuestos +@msg too many objects selected (at most 20000) demasiados objetos seleccionados (como máximo 20000) Marathi Maratí Gujarati Guyaratí Bengali Bengalí diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 1b3528602..65c7df126 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -6073,6 +6073,9 @@ breakText Glyphs Merkit Set Size of Each Aseta kunkin koko @msg select exactly two objects valitse täsmälleen kaksi objektia @msg give `width` or `height` anna `width` tai `height` +# Arrange Tools add-on: Group Overlapping Objects. + Group Overlapping Objects Ryhmitä päällekkäiset objektit +@msg too many objects selected (at most 20000) liian monta objektia valittu (enintään 20000) Marathi Marathi Gujarati Gudžarati Bengali Bengali diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 46f1c08db..f0d74bcc7 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -6040,6 +6040,9 @@ breakText Glyphs Glyphes Set Size of Each Définir la taille de chaque objet @msg select exactly two objects sélectionnez exactement deux objets @msg give `width` or `height` indiquez `width` ou `height` +# Arrange Tools add-on: Group Overlapping Objects. + Group Overlapping Objects Grouper les objets qui se chevauchent +@msg too many objects selected (at most 20000) trop d’objets sélectionnés (20000 au plus) Marathi Marathi Gujarati Goudjarati Bengali Bengali diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index bf9ec10b2..bd2b64965 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -6029,6 +6029,9 @@ breakText Glyphs Glifák Set Size of Each Egyenkénti méret beállítása @msg select exactly two objects jelölj ki pontosan két objektumot @msg give `width` or `height` add meg a `width` vagy a `height` értéket +# Arrange Tools add-on: Group Overlapping Objects. + Group Overlapping Objects Átfedő objektumok csoportosítása +@msg too many objects selected (at most 20000) túl sok objektum van kijelölve (legfeljebb 20000) Marathi Maráthi Gujarati Gudzsaráti Bengali Bengáli diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 99b747111..c7a2f0021 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -5940,6 +5940,9 @@ breakText Glyphs Glif Set Size of Each Atur ukuran masing-masing @msg select exactly two objects pilih tepat dua objek @msg give `width` or `height` berikan `width` atau `height` +# Arrange Tools add-on: Group Overlapping Objects. + Group Overlapping Objects Grupkan objek yang tumpang tindih +@msg too many objects selected (at most 20000) terlalu banyak objek dipilih (paling banyak 20000) Marathi Marathi Gujarati Gujarati Bengali Bengali diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index f3cbd7558..5f340b02b 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -5662,6 +5662,9 @@ breakText Glyphs Glifi Set Size of Each Imposta dimensione di ciascuno @msg select exactly two objects seleziona esattamente due oggetti @msg give `width` or `height` indica `width` o `height` +# Arrange Tools add-on: Group Overlapping Objects. + Group Overlapping Objects Raggruppa oggetti sovrapposti +@msg too many objects selected (at most 20000) troppi oggetti selezionati (al massimo 20000) Marathi Marathi Gujarati Gujarati Bengali Bengalese diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 5363c5d06..da7fd34f0 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -5729,6 +5729,9 @@ breakText Glyphs 字形 Set Size of Each 個別にサイズを設定 @msg select exactly two objects オブジェクトをちょうど 2 つ選択してください @msg give `width` or `height` `width` または `height` を指定してください +# Arrange Tools add-on: Group Overlapping Objects. + Group Overlapping Objects 重なり合うオブジェクトをグループ化 +@msg too many objects selected (at most 20000) 選択されたオブジェクトが多すぎます(最大 20000) Marathi マラーティー語 Gujarati グジャラート語 Bengali ベンガル語 diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 8f2c407c4..d71ee8b9b 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -6032,6 +6032,9 @@ breakText Glyphs გლიფები Set Size of Each თითოეულის ზომის დაყენება @msg select exactly two objects მონიშნეთ ზუსტად ორი ობიექტი @msg give `width` or `height` მიუთითეთ `width` ან `height` +# Arrange Tools add-on: Group Overlapping Objects. + Group Overlapping Objects გადაფარვადი ობიექტების დაჯგუფება +@msg too many objects selected (at most 20000) არჩეულია ძალიან ბევრი ობიექტი (მაქსიმუმ 20000) Marathi მარათჰი Gujarati გუჯარათი Bengali ბენგალური diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index acf8135c3..6b9ef0524 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -5625,6 +5625,9 @@ breakText Glyphs 글리프 Set Size of Each 각각 크기 설정 @msg select exactly two objects 개체를 정확히 두 개 선택하세요 @msg give `width` or `height` `width` 또는 `height`를 지정하세요 +# Arrange Tools add-on: Group Overlapping Objects. + Group Overlapping Objects 겹치는 개체 그룹화 +@msg too many objects selected (at most 20000) 선택한 개체가 너무 많습니다(최대 20000개) Marathi 마라티어 Gujarati 구자라트어 Bengali 벵골어 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 51dd8b37e..c4d9c535e 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -5984,6 +5984,9 @@ breakText Glyphs Glyphs Set Size of Each Formaat van elk instellen @msg select exactly two objects selecteer precies twee objecten @msg give `width` or `height` geef `width` of `height` op +# Arrange Tools add-on: Group Overlapping Objects. + Group Overlapping Objects Overlappende objecten groeperen +@msg too many objects selected (at most 20000) te veel objecten geselecteerd (hoogstens 20000) Marathi Marathi Gujarati Gujarati Bengali Bengaals diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 30203b1a1..2418eae3c 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -5898,6 +5898,9 @@ breakText Glyphs Glify Set Size of Each Ustaw rozmiar każdego @msg select exactly two objects zaznacz dokładnie dwa obiekty @msg give `width` or `height` podaj `width` lub `height` +# Arrange Tools add-on: Group Overlapping Objects. + Group Overlapping Objects Grupuj nakładające się obiekty +@msg too many objects selected (at most 20000) zaznaczono za dużo obiektów (najwyżej 20000) Marathi Marathi Gujarati Gudżarati Bengali Bengalski diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 6ede5c351..34b223428 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -5962,6 +5962,9 @@ breakText Glyphs Glifos Set Size of Each Definir Tamanho de Cada Um @msg select exactly two objects selecione exatamente dois objetos @msg give `width` or `height` informe `width` ou `height` +# Arrange Tools add-on: Group Overlapping Objects. + Group Overlapping Objects Agrupar objetos sobrepostos +@msg too many objects selected (at most 20000) objetos demais selecionados (no máximo 20000) Marathi Marati Gujarati Guzerate Bengali Bengali diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 2bf406c74..747c91585 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -6020,6 +6020,9 @@ breakText Glyphs Глифы Set Size of Each Задать размер каждого @msg select exactly two objects выделите ровно два объекта @msg give `width` or `height` укажите `width` или `height` +# Arrange Tools add-on: Group Overlapping Objects. + Group Overlapping Objects Сгруппировать перекрывающиеся объекты +@msg too many objects selected (at most 20000) выделено слишком много объектов (не более 20000) Marathi Маратхи Gujarati Гуджарати Bengali Бенгальский diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 788d45c8a..26b3a1d59 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -6095,6 +6095,9 @@ breakText Glyphs Glyfer Set Size of Each Ange storlek för vart och ett @msg select exactly two objects markera exakt två objekt @msg give `width` or `height` ange `width` eller `height` +# Arrange Tools add-on: Group Overlapping Objects. + Group Overlapping Objects Gruppera överlappande objekt +@msg too many objects selected (at most 20000) för många objekt markerade (högst 20000) Marathi Marathi Gujarati Gujarati Bengali Bengali diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 50b7f213b..a4851f2e3 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -6000,6 +6000,9 @@ breakText Glyphs Glifler Set Size of Each Her Birinin Boyutunu Ayarla @msg select exactly two objects tam olarak iki nesne seç @msg give `width` or `height` `width` veya `height` belirt +# Arrange Tools add-on: Group Overlapping Objects. + Group Overlapping Objects Çakışan Nesneleri Grupla +@msg too many objects selected (at most 20000) çok fazla nesne seçili (en fazla 20000) Marathi Marathi dili Gujarati Gücerat dili Bengali Bengalce diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 03d912f32..a4c04383a 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -5642,6 +5642,9 @@ breakText Glyphs Гліфи Set Size of Each Задати розмір кожного @msg select exactly two objects виділіть рівно два об’єкти @msg give `width` or `height` укажіть `width` або `height` +# Arrange Tools add-on: Group Overlapping Objects. + Group Overlapping Objects Згрупувати об’єкти, що перекриваються +@msg too many objects selected (at most 20000) виділено забагато об’єктів (не більше 20000) Marathi Маратхі Gujarati Гуджараті Bengali Бенгальська diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index cdea7542b..ab593787b 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -5645,6 +5645,9 @@ breakText Glyphs Glyph Set Size of Each Đặt kích thước từng cái @msg select exactly two objects hãy chọn đúng hai đối tượng @msg give `width` or `height` hãy cho `width` hoặc `height` +# Arrange Tools add-on: Group Overlapping Objects. + Group Overlapping Objects Nhóm các đối tượng chồng lên nhau +@msg too many objects selected (at most 20000) chọn quá nhiều đối tượng (tối đa 20000) Marathi Tiếng Marathi Gujarati Tiếng Gujarati Bengali Tiếng Bengali diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index c485086b1..9b506a128 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -5885,6 +5885,9 @@ breakText Glyphs 字形 Set Size of Each 分别设置大小 @msg select exactly two objects 请恰好选择两个对象 @msg give `width` or `height` 请提供 `width` 或 `height` +# Arrange Tools add-on: Group Overlapping Objects. + Group Overlapping Objects 编组重叠对象 +@msg too many objects selected (at most 20000) 选择的对象过多(最多 20000 个) Marathi 马拉地语 Gujarati 古吉拉特语 Bengali 孟加拉语 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 06ea60498..3e645ca09 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -5682,6 +5682,9 @@ breakText Glyphs 字符 Set Size of Each 個別設定大小 @msg select exactly two objects 請選取正好兩個物件 @msg give `width` or `height` 請提供 `width` 或 `height` +# Arrange Tools add-on: Group Overlapping Objects. + Group Overlapping Objects 群組重疊物件 +@msg too many objects selected (at most 20000) 選取的物件過多(最多 20000 個) Marathi 馬拉地文 Gujarati 古吉拉特文 Bengali 孟加拉文 diff --git a/docs/mcp.md b/docs/mcp.md index baf64c4e2..705b9d54b 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -6251,6 +6251,7 @@ as `object.setBounds` measures them (Use Preview Bounds), except Arrange in Grid | `addon.arrange.swap` | `size` (false); `scaleStrokes` (the Scale Strokes & Effects preference) | Swaps the bounds centres of exactly two selected objects (off otherwise); with `size`, each also takes the other's width and height. | | `addon.arrange.replace` | `fit`: `keep`, `bounds` (the replaced object's width and height), `proportional` (fits inside them) (`keep`); `scaleStrokes`; `keep` (false) | Puts a copy of the topmost selected object centred on each of the other selected objects, in that object's place in the stacking order; the replaced objects are deleted unless `keep` (the copy then goes right above each). Needs two objects or more; the copies are selected. → `{ids}` | | `addon.arrange.size` | `width`, `height` pt, 0.001…1000000 (at least one); `proportional` (false: a side not given stays; true: it follows the given one in proportion); `reference` 0…8 (4, the centre); `scaleStrokes` | Gives every selected object that width and height, keeping its reference point in place (the 9-point grid row by row from the top-left). | +| `addon.arrange.groupOverlapping` | none | Groups the selected objects (at most 20000) wherever their visual bounds overlap or touch, chains included: when A overlaps B and B overlaps C, all three go in one group. Only objects in the same layer or group are grouped together; objects that overlap nothing stay as they are. Each new group goes where its frontmost member was in the stacking order, and the groups and the objects left alone stay selected; with nothing overlapping it makes no step. Needs two objects or more. → `{groups: [id…], grouped}` (how many objects went into groups) | Zoom to Selection is the UI command `addon.arrange.zoomSelection {}` (remote mode only): it zooms and centres the active view so the selection's visual bounds fill the window with a margin, as `view.fitArtboard` fits the artboard, diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 6dd6db0b2..1a0fe7d61 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with Adobe Illustrator -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Type core: text shaped in its language, a Language per character, Indian languages, #4940 #4946 #4943; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Arrange Tools add-on: Group Overlapping Objects, #4928; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) ## Feature submission evidence @@ -1399,10 +1399,15 @@ scaling, replace with each fit and kept originals in stacking order, size with p undo step, off states, bad params refused), `addons::arrange::tests` in `vectorcraft-ui-egui` (the submenu, the dialogs, Preview and Cancel, Zoom to Selection) and `command_sweep`. Commands: `docs/mcp.md` › Arrange Tools add-on. +## Arrange Tools add-on: Group Overlapping Objects (2026-11-01) + +A VectorCraft addition, not a feature of the reference app, so no area score moves. Object › Plug-ins › Arrange Tools › Group Overlapping Objects groups the top-level selected objects wherever their visual bounds overlap or touch, chains included (A overlapping B and B overlapping C makes one group of three), only among objects of the same layer or group; objects that overlap nothing stay as they are, each new group takes its frontmost member's place in the stacking order, and the groups and the loose objects stay selected. One undo step; at most 20000 objects, compared by a sweep along x with union-find. Tests: `group_overlapping_*` in `cmd::addons::arrange::tests` in `vectorcraft-engine` (chains, loners, stacking order, layers kept apart, visual bounds, no step when nothing overlaps, many objects), `group_overlapping_objects_runs_directly` in `vectorcraft-ui-egui` and `command_sweep`. Commands: `docs/mcp.md` › Arrange Tools add-on. + ## Revision history | Date | Change | Summary | |---|---|---| +| 2026-11-01 | minor | Arrange Tools add-on (a VectorCraft addition): Group Overlapping Objects (#4928); evidence added; scores unchanged. | | 2026-11-01 | minor | Type core: a Language per character (Character panel › Language, `text.setFormat` / `text.setRangeStyle {language}`, character styles), new type in the document's language; text is shaped in its language so fonts use their language forms (OpenType `locl`: Turkish `fi`, #4940, #4946); Hindi, Marathi, Gujarati, Bengali, Punjabi, Tamil, Telugu, Kannada, Malayalam, Odia and Urdu in the language list (#4943); scores unchanged | | 2026-10-31 | minor | Selection tools: with Alt held at the press, a Direct Selection marquee selects every path it touches whole and keeps the Group Selection pointer (#4890); a press on a selected object's centre point selects the object whole and a drag from it moves it (#4891); regression tests in `direct/tests.rs`; scores and estimates unchanged. | | 2026-10-31 | minor | Arrange Tools add-on (a VectorCraft addition): evidence added; scores unchanged. | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index a135d587c..6672e0f94 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Character panel › Language, Indian languages in Document Setup › Language, #4946 #4943) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Arrange Tools add-on: Group Overlapping Objects, #4928) · **Target:** Adobe Illustrator 2026 (30.x) ## Feature submission evidence @@ -329,6 +329,7 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po | Tabs panel: a stop drags from where the press landed, a stop just added included | done | #1458 | | Outline view keeps template layers in Preview, dimmed | done | #1486; a picture placed to be traced stays visible under the outlines | | Arrange Tools add-on (VectorCraft addition): Object › Plug-ins › Arrange Tools runs Arrange in Grid… (grid by row, one row or one column; columns, H and V spacing, Center on Artboard; Preview), Swap Objects, Swap Objects and Sizes, Replace with Top Object… (Actual Size, Bounds or Bounds (Fit Proportionally); Keep Original; Scale Strokes & Effects; Preview), Set Size of Each… (Width and Height each ticked on or off, Constrain Width and Height Proportions with one side, the reference point, Scale Strokes & Effects; Preview) and Zoom to Selection, each one undo step | done | `addon.arrange.*`, `crates/engine/src/cmd/addons/arrange.rs`, `crates/ui-egui/src/addons/arrange.rs` and their tests; not a feature of the reference app, so it is not scored | +| Arrange Tools add-on (VectorCraft addition): Object › Plug-ins › Arrange Tools › Group Overlapping Objects groups the selected objects whose visual bounds overlap or touch (chains included, within one layer or group), each group at its frontmost member's stacking place, one undo step | done | #4928; `addon.arrange.groupOverlapping`, `crates/engine/src/cmd/addons/arrange.rs` and its tests; not a feature of the reference app, so it is not scored | | Smart Pen add-on (VectorCraft addition): Object › Plug-ins › Smart Pen selects the Smart Pen Tool, which the All Tools drawer lists under its own heading but neither toolbar shows as shipped (no shortcut), and opens the Smart Pen panel (next segment straight or curved; Remove Points Keep Shape, Make Smooth, Make Corner, Retract Handles, Close or Join). The tool draws (click: straight or curved segment, a curve after a straight one tangent; drag: smooth anchor, Alt breaks the handle, Shift 45°, Space moves it; Tab switches a segment; rubber band; close, join, continue) and edits without switching tools (click selects a point, drag moves it, Alt-click removes it keeping the shape, Shift+Alt-click deletes it, double-click toggles smooth/corner; handle drags with Shift and Alt; segment drags reshape, Alt-click adds a point) | done | `addon.pen.*`, `crates/tools/src/addons/smart_pen.rs`, `crates/ui-egui/src/addons/pen.rs` and their tests; not a feature of the reference app, so it is not scored | | Path Precision Tools add-on (VectorCraft addition): Object › Plug-ins › Path Precision Tools selects its seven tools (Anchor Eraser Brush, Extend Path, Measure & Inspect, Parametric Shape, Corner Tool, Slide Anchor Along Path, Shared Edge Merge Tool), which the All Tools drawer lists under their own heading but neither toolbar shows as shipped, opens the Anchor Inspector panel (the selected anchor's X/Y and handle lengths and angles as fields; Make Smooth, Make Corner, Equalize Handles, Remove Points Keep Shape, Add Points, the three ways to close) and the Parametric Shape panel (kind, count, W/H, rotation, the kind's own values and corner radius of the selected parametric shape, or of the tool's next one), and runs Add Points at / Split at Crossings and Convert to Parametric Shape; the Parametric Shape Tool draws ten re-editable kinds from the centre (↑/↓ and ←/→ change their numbers while dragging) and edits a selected one with handles; the Corner Tool cuts editable corners on any path (hover shows the anchor, a click uses its radius and kind, a drag sizes the corner, Shift whole values; a corner's marker selects it and loads its values, drags to resize, to 0 or double-clicked removes it; never past what fits; Alt-marquee cuts every anchor inside) with a Corners section in the Anchor Inspector (radius, kind, selected count, Apply to Selected Anchors); Slide Anchor Along Path drags an anchor along its curve with the segments beside it refitted, Shift snapping where the tangent is a multiple of 45°; the Shared Edge Merge Tool shows, over two paths touching at two coincident anchors, the two stretches and the edge merging them, a click merges them (half way; Shift the upper path, Alt the lower), and near anchors meet or an anchor one path lacks is added to match (Shift+Alt asks for this inside a stretch), with a Shared Edges section in the Anchor Inspector (brush diameter, coincidence 0 to 3 pt, Keep Shared Points Together, which takes coincident anchors along with the add-on's own anchor moves); a double-click on the brush or Extend Path tool opens its options | done | `addon.pathtools.*`, `crates/tools/src/addons/`, `crates/ui-egui/src/addons/pathtools.rs`, `crates/ui-egui/src/addons/pathtools/` and their tests; not a feature of the reference app, so it is not scored | | Path Precision Tools add-on (VectorCraft addition): Effect › Path Effects lists its live effects (Dashes to Geometry, Add Anchors, Extend Path, Close Path, Smooth Anchors, Retract Handles, Reverse Direction, Simplify Anchors, Split at Anchors), each with the effect dialog's live Preview except Reverse Direction, which has no options, and listed in the Appearance panel like any effect | done | `addon.path.*`, `crates/effects/src/addons/path.rs`, `crates/ui-egui/src/addons/pathtools.rs` and their tests; not a feature of the reference app, so it is not scored | @@ -381,6 +382,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development | Date | Change | Summary | |---|---|---| +| 2026-11-01 | minor | Arrange Tools add-on (a VectorCraft addition): Group Overlapping Objects (#4928); scores and estimates unchanged | | 2026-11-01 | minor | Character panel › Language: the selected characters' language, from Document Setup's list, which gains Hindi, Marathi, Gujarati, Bengali, Punjabi, Tamil, Telugu, Kannada, Malayalam, Odia and Urdu (#4946, #4943); the Digits menu's numeral names keep their own translations; scores unchanged | | 2026-11-01 | minor | VectorCraft Options' Version is a menu of the versions VectorCraft writes, each named by the releases that read it, and an uncompressible version turns Use Compression off (#4931); a save refused before writing shows the Can't save alert with the reason (#4933); About › Contributors' table keeps its header row above the scrolling rows (#4944); the Gradient panel's freeform point swatch opens the point's popover (#4953); scores unchanged | | 2026-10-31 | minor | A file opened from the file manager while VectorCraft runs opens as a new tab in that window, which comes to the front, on Windows and Linux (#4892); `--new-instance` opts out; scores unchanged | From 9e3f8dccec3621c70d1cddb44f2bb1e6d0ade57d Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sun, 1 Nov 2026 01:56:23 +0700 Subject: [PATCH 2/4] Fix #4950: Path Precision Tools: Divide into Dashes addon.pathtools.divideDashes cuts the selected paths (or only their direct-selected segments) into separate open paths of a dash length measured along the curve, with an optional gap (gap pieces dropped unless removeGaps is false) and Fit to Corners and Ends; pieces keep the path's appearance, one undo step, at most 100000 pieces. Geometry in vectorcraft-pathops; dialog with unit fields and Preview; tests, sweep cases, catalog rows and docs. --- ROADMAP.md | 4 +- crates/engine/src/cmd/addons/pathtools.rs | 90 ++++++- .../engine/src/cmd/addons/pathtools/tests.rs | 74 +++++ crates/engine/tests/command_sweep.rs | 3 + crates/pathops/src/addons/pathtools.rs | 5 +- crates/pathops/src/addons/pathtools/dashes.rs | 252 ++++++++++++++++++ crates/ui-egui/src/addons/pathtools.rs | 12 +- crates/ui-egui/src/addons/pathtools/dashes.rs | 91 +++++++ crates/ui-egui/src/dialogs.rs | 1 + crates/ui-egui/src/i18n/bg.tsv | 9 + crates/ui-egui/src/i18n/bs.tsv | 9 + crates/ui-egui/src/i18n/ca.tsv | 9 + crates/ui-egui/src/i18n/cs.tsv | 9 + crates/ui-egui/src/i18n/da.tsv | 9 + crates/ui-egui/src/i18n/de.tsv | 9 + crates/ui-egui/src/i18n/el.tsv | 9 + crates/ui-egui/src/i18n/es.tsv | 9 + crates/ui-egui/src/i18n/fi.tsv | 9 + crates/ui-egui/src/i18n/fr.tsv | 9 + crates/ui-egui/src/i18n/hu.tsv | 9 + crates/ui-egui/src/i18n/id.tsv | 9 + crates/ui-egui/src/i18n/it.tsv | 9 + crates/ui-egui/src/i18n/ja.tsv | 9 + crates/ui-egui/src/i18n/ka.tsv | 9 + crates/ui-egui/src/i18n/ko.tsv | 9 + crates/ui-egui/src/i18n/nl.tsv | 9 + crates/ui-egui/src/i18n/pl.tsv | 9 + crates/ui-egui/src/i18n/pt-br.tsv | 9 + crates/ui-egui/src/i18n/ru.tsv | 9 + crates/ui-egui/src/i18n/sv.tsv | 9 + crates/ui-egui/src/i18n/tr.tsv | 9 + crates/ui-egui/src/i18n/uk.tsv | 9 + crates/ui-egui/src/i18n/vi.tsv | 9 + crates/ui-egui/src/i18n/zh-hans.tsv | 9 + crates/ui-egui/src/i18n/zh-hant.tsv | 9 + crates/ui-egui/src/menus/invoke.rs | 1 + docs/mcp.md | 10 + docs/target-app-parity.md | 7 +- docs/ui-parity.md | 4 +- 39 files changed, 770 insertions(+), 18 deletions(-) create mode 100644 crates/pathops/src/addons/pathtools/dashes.rs create mode 100644 crates/ui-egui/src/addons/pathtools/dashes.rs diff --git a/ROADMAP.md b/ROADMAP.md index c943bbb02..b8365d0f0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -9,7 +9,7 @@ **Stage: alpha** · next: beta, ~13 points (ready for real work ~62% → ~75%, and reliable `.ai` exchange) and ~170–270 h away -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Progress log row for Group Overlapping Objects in the Arrange Tools add-on; headline numbers unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Progress log row for Divide into Dashes in the Path Precision Tools add-on; headline numbers unchanged) · **Target:** Adobe Illustrator 2026 (30.x) VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adobe Illustrator workflow. It runs on macOS, Windows, Linux, FreeBSD and the web (WASM), and agents can drive all of it over MCP, the CLI and a JSON @@ -129,6 +129,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap ## Progress log +- 2026-11-01: Divide into Dashes joins the Path Precision Tools add-on (Object › Plug-ins › Path Precision Tools): the selected paths, or only their direct-selected segments, are cut into separate open paths of a dash length measured along the curve, with an optional gap (left empty, or kept as pieces), each keeping the path's appearance; Fit to Corners and Ends stretches or squeezes the lengths so every run starts and ends on a dash. A dialog with Preview, one undo step, at most 100000 pieces. A VectorCraft addition: headline numbers unchanged. - 2026-11-01: Group Overlapping Objects joins the Arrange Tools add-on (Object › Plug-ins › Arrange Tools): the selected objects whose visual bounds overlap or touch, chains included, go into one group each, within their own layer or group, at the stacking place of their frontmost member; objects that overlap nothing stay as they are. One undo step, a sweep along x so thousands of loose pieces group quickly (at most 20000 objects). A VectorCraft addition: headline numbers unchanged. - 2026-10-31: Export for Screens works as the reference app's dialog: Artboards and Assets tabs with a grid or list of thumbnails, Clear Selection and Prefix, Create Sub-folders by Scale or Format (on by default), Export PDFs As Single File (the chosen artboards as one PDF's pages, `pdfs: "single"`) or Multiple Files, iOS and Android rows, format presets in a ☰ menu, and a count of the files the export writes (#4895). About 55 community PRs and fixes landed the same day: Find Font, Replace Font, Outline Stroke and Expand leave hidden and locked art alone (with a warning); DXF export writes LINE and ELLIPSE entities and outlines only the strokes a CAD line can't draw; Blob Brush Angle and Roundness; Shift-constrained Perspective Selection; Grid Repeat Options Preview; a gradient mesh from a file budgeted at about a million quads; width-profile strokes keep their shape zoomed out; `.ai`/EPS import fidelity (area type, envelopes, transparency in dictionaries, character styles); PDF import finds hyphenated and CMap-suffixed font names; Windows renders follow the process's core affinity; Enter or Escape in a floating panel's field no longer reaches the canvas (#4803); Properties shows Blend Options for any selected blend (#4817); Artwork Cleanup finds doubled type (#4835) and joins stacked lines into one object (Join Point Text, #4826). Headline numbers unchanged. - 2026-10-31: Arrange Tools, a built-in add-on (Object › Plug-ins › Arrange Tools): Arrange in Grid lays the selected objects out in rows and columns, a row or a column with gaps, from the selection's top-left or centred on the artboard; Swap Objects swaps two objects' places (and, with Swap Objects and Sizes, their sizes); Replace with Top Object puts a copy of the topmost selected object in place of each of the others, kept, stretched or fitted in proportion; Set Size of Each gives every selected object an exact width and/or height about a reference point; Zoom to Selection fits the view to the selection. Each is one undo step with a previewed dialog where it has options. A VectorCraft addition: headline numbers unchanged. @@ -205,6 +206,7 @@ The full earlier record of what landed is kept in [docs/roadmap.md](docs/roadmap | Date | Change | Summary | |---|---|---| +| 2026-11-01 | minor | Progress log row for Divide into Dashes (Path Precision Tools add-on, #4950); headline numbers unchanged. | | 2026-11-01 | minor | Progress log row for Group Overlapping Objects (Arrange Tools add-on, #4928); headline numbers unchanged. | | 2026-10-31 | minor | Progress log row for Export for Screens laid out as the reference app's (#4895) and the 2026-10-31 community landings; headline numbers unchanged. | | 2026-10-31 | minor | Progress log row for the Arrange Tools add-on; headline numbers unchanged. | diff --git a/crates/engine/src/cmd/addons/pathtools.rs b/crates/engine/src/cmd/addons/pathtools.rs index b78eb0976..94114a59c 100644 --- a/crates/engine/src/cmd/addons/pathtools.rs +++ b/crates/engine/src/cmd/addons/pathtools.rs @@ -1,8 +1,8 @@ //! Path Precision Tools (`addon.pathtools.*`), a built-in add-on: the Anchor Eraser Brush's stroke, //! the Extend Path tool's extension, the Measure & Inspect tool's readouts, the Anchor Inspector's //! anchor edits, Split / Add Points at Crossings, Slide Anchor Along Path, the Corner Tool's corners -//! ([`corners`]), the Parametric Shape Tool's shapes ([`shape`]), and the Shared Edge Merge Tool's -//! merges with Keep Shared Points Together ([`shared_edge`]). +//! ([`corners`]), the Parametric Shape Tool's shapes ([`shape`]), the Shared Edge Merge Tool's +//! merges with Keep Shared Points Together ([`shared_edge`]), and Divide into Dashes. //! Its tools, panels and submenu (Object › Plug-ins › Path Precision Tools) are the tools crate's //! and the UI's. @@ -40,6 +40,8 @@ const MIN_RADIUS: f64 = 0.05; const MAX_DIAMETER: f64 = 2500.0; /// The farthest an end is extended at once (pt). const MAX_EXTEND: f64 = 1e6; +/// Most pieces Divide into Dashes makes at once. +const MAX_PIECES: usize = 100_000; /// Most anchors Add Points puts on each segment. const MAX_ADD: u64 = pt::MAX_ADDED_PER_SEGMENT as u64; @@ -153,6 +155,15 @@ pub fn specs() -> Vec { has_doc, slide_anchor ), + cmd!( + "addon.pathtools.divideDashes", + "Divide into Dashes", + ["Object", "Plug-ins", "Path Precision Tools"], + None, + "{dash?: pt 0.01..1000000 (10), gap?: pt 0..1000000 (0: the pieces touch), fit?: bool (false: exact lengths, the last piece of each run what is left; true: fit to corners and ends, every run between corners and ends holding whole dashes, all stretched or squeezed alike so it starts and ends on a dash), removeGaps?: bool (true: the gaps are left empty; false: they stay as pieces too)} cut the selected paths into separate open paths along the curve, each dash long with gap between them and keeping the path's appearance, as one undo step. Only the direct-selected segments (or those between two direct-selected anchors) are divided when there are any, the dash pattern running on across segments that follow one another; the rest of such a path stays, as pieces of its own. The pieces are selected; at most 100000 → {ids, dashes}", + has_selection, + divide_dashes + ), ]; v.extend(shape::specs()); v.extend(corners::specs()); @@ -583,6 +594,16 @@ fn remove_keep_shape(s: &mut Session, _: &Value) -> Result { Ok(json!({ "removed": removed })) } +/// The segments of subpath `si` (`sp`) picked: the direct-selected segments `segments` of its +/// path, else those between two of its selected `anchors`. +fn picked_segments(segments: Option<&BTreeSet<(usize, usize)>>, si: usize, sp: &SubPath, anchors: &BTreeSet) -> BTreeSet { + let n = sp.anchors.len().max(1); + match segments.filter(|set| !set.is_empty()) { + Some(set) => set.iter().filter(|(s, _)| *s == si).map(|(_, k)| *k).collect(), + None => (0..sp.segment_count()).filter(|k| anchors.contains(k) && anchors.contains(&((k + 1) % n))).collect(), + } +} + fn add_points(s: &mut Session, p: &Value) -> Result { const C: &str = "addon.pathtools.addPoints"; let count = number_or(p, "count", 1.0, C)?; @@ -597,16 +618,10 @@ fn add_points(s: &mut Session, p: &Value) -> Result { let mut changed = vec![]; for (id, by_sub) in &picked { let Some(path) = d.node(*id).and_then(|n| n.path_data()) else { continue }; - let picked_segments = st.selection.segments.get(id); let mut np = path.clone(); for (si, sp) in np.subpaths.iter_mut().enumerate() { let Some(anchors) = by_sub.get(&si) else { continue }; - let n = sp.anchors.len(); - // Picked segments, else those between two selected anchors. - let segs: BTreeSet = match picked_segments { - Some(set) => set.iter().filter(|(s, _)| *s == si).map(|(_, k)| *k).collect(), - None => (0..sp.segment_count()).filter(|k| anchors.contains(k) && anchors.contains(&((k + 1) % n.max(1)))).collect(), - }; + let segs = picked_segments(st.selection.segments.get(id), si, sp, anchors); let (out, marks) = pt::add_points(sp, Some(&segs), count as usize); added += marks.len(); *sp = out; @@ -721,3 +736,60 @@ fn crossings(s: &mut Session, p: &Value) -> Result { })?; Ok(json!({ "ids": out.iter().map(|i| i.0).collect::>(), "crossings": count })) } + +// ---------- Divide into Dashes ---------- + +fn divide_dashes(s: &mut Session, p: &Value) -> Result { + const C: &str = "addon.pathtools.divideDashes"; + let spec = pt::DashSpec { + dash: given(p, "dash", Some((0.01, 1e6)), C)?.unwrap_or(10.0), + gap: given(p, "gap", Some((0.0, 1e6)), C)?.unwrap_or(0.0), + fit: switch_or(p, "fit", false, C)?, + }; + let remove_gaps = switch_or(p, "removeGaps", true, C)?; + let picked = picked_anchors(s, true)?; + let st = s.doc()?; + let d = &st.doc; + check_size(picked.iter().filter_map(|(id, _)| d.node(*id)?.path_data()), C)?; + let (mut made, mut dashes) = (0, 0); + // Each path divided: what stays of it (the subpaths not divided, as one path) and its pieces. + let mut changed: Vec<(NodeId, Vec)> = vec![]; + for (id, by_sub) in &picked { + let Some(path) = d.node(*id).and_then(|n| n.path_data()) else { continue }; + let (mut kept, mut pieces) = (vec![], vec![]); + for (si, sp) in path.subpaths.iter().enumerate() { + let segs = by_sub.get(&si).map(|anchors| picked_segments(st.selection.segments.get(id), si, sp, anchors)).unwrap_or_default(); + if segs.is_empty() { + kept.push(sp.clone()); + continue; + } + let only = (segs.len() < sp.segment_count()).then_some(&segs); + let out = pt::divide_dashes(sp, only, spec, MAX_PIECES.saturating_sub(made)) + .ok_or_else(|| bad(C, "that makes too many pieces (at most 100000): make the dashes longer"))?; + made += out.len(); + for (piece, kind) in out.into_iter().filter(|(_, k)| !(remove_gaps && *k == pt::Piece::Gap)) { + dashes += usize::from(kind == pt::Piece::Dash); + pieces.push(PathData::single(piece)); + } + } + if !pieces.is_empty() { + changed.push((*id, (!kept.is_empty()).then(|| PathData::new(kept)).into_iter().chain(pieces).collect())); + } + } + if changed.is_empty() { + return Err(bad(C, "select paths, or segments, to divide")); + } + let ids = s.edit("Divide into Dashes", |d, sel| { + let mut all = vec![]; + let mut beside = Beside::replacing(d, &changed.iter().map(|(id, _)| *id).collect::>()); + for (id, pieces) in changed { + all.extend(replace_with_pieces(d, &mut beside, id, pieces)?); + } + beside.finish(d)?; + // The pieces join the objects selected (whole, in one go: there may be many). + let before = std::mem::take(&mut sel.objects); + sel.set(before.into_iter().chain(all.iter().copied())); + Ok(all) + })?; + Ok(json!({ "ids": ids.iter().map(|i| i.0).collect::>(), "dashes": dashes })) +} diff --git a/crates/engine/src/cmd/addons/pathtools/tests.rs b/crates/engine/src/cmd/addons/pathtools/tests.rs index 52f0c7078..cc745385c 100644 --- a/crates/engine/src/cmd/addons/pathtools/tests.rs +++ b/crates/engine/src/cmd/addons/pathtools/tests.rs @@ -674,3 +674,77 @@ fn the_slide_tool_previews_and_commits_one_step() { s.execute("edit.undo", &json!({})).unwrap(); assert_eq!(at(&s), Point::new(200.0, 100.0)); } + +fn ids(v: &Value) -> Vec { + v["ids"].as_array().unwrap().iter().map(|i| NodeId(i.as_u64().unwrap())).collect() +} + +#[test] +fn divide_into_dashes_cuts_a_path_into_open_pieces_keeping_its_look() { + let mut s = session(); + let l = line(&mut s, &[(0.0, 0.0), (100.0, 0.0)], false); + select(&mut s, &[l]); + s.execute("stroke.set", &json!({"weight": 3})).unwrap(); + let n = steps(&s); + let r = s.execute("addon.pathtools.divideDashes", &json!({"dash": 30, "gap": 10})).unwrap(); + assert_eq!(steps(&s), n + 1, "one undo step"); + // 30 + 10 + 30 + 10 + 20: the gaps are left empty. + let pieces = ids(&r); + assert_eq!((pieces.len(), r["dashes"].clone()), (3, json!(3))); + let st = s.doc().unwrap(); + let spans: Vec<(f64, f64)> = pieces.iter().map(|id| path(&s, *id).bounds().map(|b| (b.x0, b.x1)).unwrap()).collect(); + let want = [(0.0, 30.0), (40.0, 70.0), (80.0, 100.0)]; + assert!(spans.iter().zip(want).all(|(a, b)| (a.0 - b.0).abs() < 1e-6 && (a.1 - b.1).abs() < 1e-6), "{spans:?}"); + assert!(pieces.iter().all(|id| st.doc.node(*id).unwrap().appearance.stroke_width() == 3.0), "the stroke stays"); + assert_eq!(st.selection.objects, pieces); + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(path(&s, l).anchor_count(), 2); + // Kept gaps; fitted 25s with no gap. + let r = s.execute("addon.pathtools.divideDashes", &json!({"dash": 30, "gap": 10, "removeGaps": false})).unwrap(); + assert_eq!((ids(&r).len(), r["dashes"].clone()), (5, json!(3))); + s.execute("edit.undo", &json!({})).unwrap(); + let r = s.execute("addon.pathtools.divideDashes", &json!({"dash": 30, "fit": true})).unwrap(); + let spans: Vec = ids(&r).iter().map(|id| path(&s, *id).bounds().unwrap().width()).collect(); + assert_eq!(spans.len(), 3); + assert!(spans.iter().all(|w| (w - 100.0 / 3.0).abs() < 1e-6), "{spans:?}"); +} + +#[test] +fn divide_into_dashes_divides_only_the_selected_segments() { + let mut s = session(); + let u = line(&mut s, &[(0.0, 0.0), (40.0, 0.0), (40.0, 40.0), (0.0, 40.0)], false); + // The middle side, between anchors 1 and 2. + select_anchors(&mut s, u, &[1, 2]); + let r = s.execute("addon.pathtools.divideDashes", &json!({"dash": 10})).unwrap(); + let pieces = ids(&r); + assert_eq!((pieces.len(), r["dashes"].clone()), (6, json!(4)), "the first side, four dashes, the last side"); + assert_eq!(path(&s, pieces[0]).bounds().unwrap().width(), 40.0); + let layer: Vec = s.doc().unwrap().doc.layers[0].children().unwrap().iter().map(|n| n.id).collect(); + assert_eq!(layer, pieces, "in the path's place, in order"); +} + +#[test] +fn divide_into_dashes_refuses_bad_params_and_too_many_pieces() { + let mut s = session(); + let l = line(&mut s, &[(0.0, 0.0), (1e5, 0.0)], false); + select(&mut s, &[l]); + refuses( + &mut s, + "addon.pathtools.divideDashes", + &[ + json!({"dash": 0}), + json!({"dash": -5}), + json!({"dash": 1e308}), + json!({"dash": "10"}), + json!({"gap": -1}), + json!({"gap": 2e6}), + json!({"fit": "yes"}), + json!({"removeGaps": 1}), + json!({"dash": 0.5}), + ], + ); + // 200000 pieces refused above; 1000 is fine. + assert_eq!(s.execute("addon.pathtools.divideDashes", &json!({"dash": 100})).unwrap()["dashes"], json!(1000)); + s.execute("select.none", &json!({})).unwrap(); + refuses(&mut s, "addon.pathtools.divideDashes", &[json!({})]); +} diff --git a/crates/engine/tests/command_sweep.rs b/crates/engine/tests/command_sweep.rs index 14d7f9559..36fe6401d 100644 --- a/crates/engine/tests/command_sweep.rs +++ b/crates/engine/tests/command_sweep.rs @@ -589,6 +589,9 @@ fn structured_junk() { ("addon.arrange.size", json!({"width": 1e6, "height": 0.001, "reference": 8, "scaleStrokes": true})), ("addon.arrange.size", json!({"width": 0.001, "proportional": true, "reference": u64::MAX})), ("addon.arrange.groupOverlapping", json!({"ids": "x", "junk": f64::MAX})), + ("addon.pathtools.divideDashes", json!({"dash": 1e-308, "gap": -1e308, "fit": "x"})), + ("addon.pathtools.divideDashes", json!({"dash": 0.01, "gap": 1e6, "fit": true, "removeGaps": false})), + ("addon.pathtools.divideDashes", json!({"dash": f64::MAX, "gap": [0], "removeGaps": null})), // Smart Pen add-on. ("addon.pen.append", json!({"id": 2, "x": 1e308, "y": 0, "in": [1e308, -1e308]})), ("addon.pen.append", json!({"id": 2, "subpath": u64::MAX, "x": 0, "y": 0, "segment": "curved"})), diff --git a/crates/pathops/src/addons/pathtools.rs b/crates/pathops/src/addons/pathtools.rs index b98778cc5..1de342ffc 100644 --- a/crates/pathops/src/addons/pathtools.rs +++ b/crates/pathops/src/addons/pathtools.rs @@ -3,9 +3,11 @@ //! ([`add_anchors`], [`smooth_anchors`], [`split_at_anchors`]), the readouts the Measure & Inspect //! tool shows over a path, open paths lengthened or shortened at an end ([`extend`]), the places //! where paths cross ([`crossings`]), anchors slid along their path ([`slide`]), touching edges -//! merged ([`shared_edge`]) and the Parametric Shape Tool's shapes ([`shapes`]). +//! merged ([`shared_edge`]), paths divided into dashes ([`divide_dashes`]) and the Parametric Shape +//! Tool's shapes ([`shapes`]). mod crossings; +mod dashes; mod extend; pub mod shapes; mod shared_edge; @@ -19,6 +21,7 @@ use kurbo::{CubicBez, ParamCurve, ParamCurveArclen, ParamCurveNearest, Point, Ve use vectorcraft_geom::{Anchor, AnchorKind, PathData, SubPath}; pub use crossings::{MAX_CROSSING_SEGMENTS, crossings, insert_at}; +pub use dashes::{DashSpec, Piece, divide_dashes}; pub use extend::{ExtendMode, extend_length_to, extend_open}; pub use shared_edge::{ COINCIDENCE, EndSide, Follow, MAX_COINCIDENCE, MergePlan, MergePreview, Stretch, apply_merge, ends_at, merge_preview, stretches_at, diff --git a/crates/pathops/src/addons/pathtools/dashes.rs b/crates/pathops/src/addons/pathtools/dashes.rs new file mode 100644 index 000000000..cec805d84 --- /dev/null +++ b/crates/pathops/src/addons/pathtools/dashes.rs @@ -0,0 +1,252 @@ +//! Divide into Dashes: a subpath (or some of its segments) cut into open pieces of a given length +//! measured along the curve, with gaps of a given length between them. + +use std::collections::BTreeSet; + +use kurbo::{CubicBez, ParamCurve, ParamCurveArclen}; +use vectorcraft_geom::{Anchor, AnchorKind, SubPath}; + +use super::{ACCURACY, TINY}; + +/// What a piece [`divide_dashes`] cuts is. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Piece { + Dash, + /// The stretch between two dashes. + Gap, + /// A stretch of segments that weren't to be divided, as it was. + Rest, +} + +/// How [`divide_dashes`] measures. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct DashSpec { + /// Each dash's length (pt), more than 0. + pub dash: f64, + /// Each gap's length (pt), 0 or more (0: the dashes touch). + pub gap: f64, + /// Fit to corners and ends: every run between corners and ends holds a whole number of dashes + /// (and the gaps between them), all stretched or squeezed alike so it starts and ends on a + /// dash; a closed subpath with no corner holds whole periods. Otherwise the lengths are exact + /// and the last piece of each run is what is left. + pub fit: bool, +} + +/// `sp` cut into open pieces in order along it: the segments `segments` picks (all when `None`) +/// cut into dashes and gaps as `spec` says, the dash pattern running on across the picked +/// segments that follow one another, and each stretch of segments not picked as one piece. `None` +/// when that would make more than `room` pieces, or `spec` is no pattern (lengths not finite, a +/// dash not more than 0, a negative gap). +pub fn divide_dashes(sp: &SubPath, segments: Option<&BTreeSet>, spec: DashSpec, room: usize) -> Option> { + let DashSpec { dash, gap, fit } = spec; + if !(dash.is_finite() && gap.is_finite() && dash > TINY && gap >= 0.0) { + return None; + } + let n = sp.segment_count(); + let picked = |k: usize| segments.is_none_or(|s| s.contains(&k)); + if !(0..n).any(picked) { + return Some(vec![(sp.clone(), Piece::Rest)]); + } + // An anchor between two segments where the outline turns: a corner a fitted run ends at. + let corner = |i: usize| sp.anchors.get(i).is_some_and(|a| Anchor::with_handles(a.p, a.h_in, a.h_out).kind != AnchorKind::Smooth); + // A closed subpath starts where picked and unpicked segments meet, else (fitting) at a corner. + let start = if sp.closed { + (0..n).find(|&k| picked(k) != picked((k + n - 1) % n)).or_else(|| (0..n).find(|&k| fit && corner(k))).unwrap_or(0) + } else { + 0 + }; + // Runs of segments alike; a fitted picked run also ends at each corner. + let mut runs: Vec<(bool, Vec)> = vec![]; + for k in (0..n).map(|i| (start + i) % n) { + let p = picked(k); + match runs.last_mut() { + Some((q, segs)) if *q == p && !(p && fit && corner(k)) => segs.push(k), + _ => runs.push((p, vec![k])), + } + } + let whole_loop = sp.closed && runs.len() == 1 && !(fit && corner(start)); + let mut out = vec![]; + for (p, segs) in runs { + let curves: Vec = segs.iter().map(|&k| sp.segment(k)).collect(); + if !p { + out.extend(from_cubics(&curves).map(|s| (s, Piece::Rest))); + continue; + } + let lens: Vec = curves.iter().map(|c| c.arclen(ACCURACY)).collect(); + let len: f64 = lens.iter().sum(); + if !(len.is_finite() && len > TINY) { + continue; + } + let cuts = spans(len, spec, whole_loop, room.checked_sub(out.len())?)?; + let mut cursor = 0; + let mut from = 0.0; + let starts: Vec = lens + .iter() + .map(|l| { + let s = from; + from += l; + s + }) + .collect(); + for (a, b, kind) in cuts { + let mut piece = vec![]; + let mut j = cursor; + while let (Some(&s0), Some(&l), Some(c)) = (starts.get(j), lens.get(j), curves.get(j)) { + if s0 >= b - TINY { + break; + } + let (t0, t1) = ((a - s0).max(0.0), (b - s0).min(l)); + if t1 - t0 > TINY { + let at = |d: f64| { + if d <= 0.0 { + 0.0 + } else if d >= l { + 1.0 + } else { + c.inv_arclen(d, ACCURACY) + } + }; + piece.push(c.subsegment(at(t0)..at(t1))); + } + if s0 + l > b + TINY { + break; + } + j += 1; + } + cursor = j; + out.extend(from_cubics(&piece).map(|s| (s, kind))); + } + } + Some(out) +} + +/// The pieces (start, end along the run, kind) of a run `len` long; `None` past `room` pieces. +fn spans(len: f64, DashSpec { dash, gap, fit }: DashSpec, whole_loop: bool, room: usize) -> Option> { + let period = dash + gap; + // A loop holds whole periods; an open run one gap fewer than dashes. + let scale = if !fit { + 1.0 + } else if whole_loop { + len / ((len / period).round().max(1.0) * period) + } else { + let count = ((len + gap) / period).round().max(1.0); + len / (count * dash + (count - 1.0) * gap) + }; + let (dash, gap) = (dash * scale, gap * scale); + // Rounding (of the length measured, over many pieces): what is left past `end` is no piece. + let end = len - (len * 1e-9).max(TINY); + let most = (end / (dash + gap)).ceil() * if gap > 0.0 { 2.0 } else { 1.0 }; + if !(most.is_finite() && most <= room as f64) { + return None; + } + let mut out = vec![]; + let (mut at, mut on) = (0.0, true); + while at < end { + let step = if on { dash } else { gap }; + if step > 0.0 { + let to = (at + step).min(len); + out.push((at, if to >= end { len } else { to }, if on { Piece::Dash } else { Piece::Gap })); + at = to; + } + on = !on; + } + Some(out) +} + +/// An open subpath through cubic segments that follow one another; `None` for none. +fn from_cubics(cs: &[CubicBez]) -> Option { + let (first, last) = (cs.first()?, cs.last()?); + let mut anchors = vec![Anchor::with_handles(first.p0, first.p0, first.p1)]; + anchors.extend(cs.windows(2).filter_map(|w| match w { + [a, b] => Some(Anchor::with_handles(b.p0, a.p2, b.p1)), + _ => None, + })); + anchors.push(Anchor::with_handles(last.p3, last.p2, last.p3)); + Some(SubPath::new(anchors, false)) +} + +#[cfg(test)] +mod tests { + use kurbo::Point; + + use super::*; + + fn line(len: f64) -> SubPath { + SubPath::polyline(&[Point::new(0.0, 0.0), Point::new(len, 0.0)], false) + } + + fn length(sp: &SubPath) -> f64 { + (0..sp.segment_count()).map(|k| sp.segment(k).arclen(ACCURACY)).sum() + } + + fn lengths(pieces: &[(SubPath, Piece)]) -> Vec<(i64, Piece)> { + pieces.iter().map(|(s, k)| ((length(s) * 1000.0).round() as i64, *k)).collect() + } + + const EXACT: DashSpec = DashSpec { dash: 10.0, gap: 0.0, fit: false }; + + #[test] + fn a_line_is_cut_into_dashes_and_gaps() { + let p = divide_dashes(&line(35.0), None, EXACT, 100).unwrap(); + assert_eq!(lengths(&p), [(10_000, Piece::Dash), (10_000, Piece::Dash), (10_000, Piece::Dash), (5_000, Piece::Dash)]); + assert_eq!(p[0].0.anchors[1].p, p[1].0.anchors[0].p, "touching"); + assert!(p[1].0.anchors[0].p.distance(Point::new(10.0, 0.0)) < 1e-6); + let p = divide_dashes(&line(35.0), None, DashSpec { gap: 5.0, ..EXACT }, 100).unwrap(); + let kinds: Vec = p.iter().map(|(_, k)| *k).collect(); + assert_eq!(kinds, [Piece::Dash, Piece::Gap, Piece::Dash, Piece::Gap, Piece::Dash]); + assert_eq!(lengths(&p)[4], (5_000, Piece::Dash)); + } + + #[test] + fn fitting_stretches_the_pattern_to_start_and_end_on_a_dash() { + // 35 with 10 + 5: three dashes and two gaps, squeezed from 40 to 35. + let p = divide_dashes(&line(35.0), None, DashSpec { dash: 10.0, gap: 5.0, fit: true }, 100).unwrap(); + let (d, g) = ((8_750, Piece::Dash), (4_375, Piece::Gap)); + assert_eq!(lengths(&p), [d, g, d, g, d]); + // A square: each side its own run, each starting and ending on a dash. + let sq = SubPath::polyline(&[Point::new(0.0, 0.0), Point::new(30.0, 0.0), Point::new(30.0, 30.0), Point::new(0.0, 30.0)], true); + let p = divide_dashes(&sq, None, DashSpec { dash: 7.0, gap: 0.0, fit: true }, 100).unwrap(); + assert_eq!(p.len(), 16, "four dashes of 7.5 on each side"); + assert!(lengths(&p).iter().all(|(l, k)| *l == 7_500 && *k == Piece::Dash)); + } + + #[test] + fn a_curve_is_measured_along_it() { + let arc = SubPath::new( + vec![ + Anchor::with_handles(Point::new(0.0, 0.0), Point::new(0.0, 0.0), Point::new(0.0, 55.0)), + Anchor::with_handles(Point::new(100.0, 100.0), Point::new(45.0, 100.0), Point::new(100.0, 100.0)), + ], + false, + ); + let total = length(&arc); + let p = divide_dashes(&arc, None, EXACT, 1000).unwrap(); + assert_eq!(p.len(), (total / 10.0).ceil() as usize); + assert!(p.iter().rev().skip(1).all(|(s, _)| (length(s) - 10.0).abs() < 1e-4)); + assert!((p.iter().map(|(s, _)| length(s)).sum::() - total).abs() < 1e-4); + } + + #[test] + fn only_the_picked_segments_are_divided() { + // Three sides of 20; the middle one picked. + let u = SubPath::polyline(&[Point::new(0.0, 0.0), Point::new(20.0, 0.0), Point::new(20.0, 20.0), Point::new(0.0, 20.0)], false); + let p = divide_dashes(&u, Some(&BTreeSet::from([1])), EXACT, 100).unwrap(); + assert_eq!(lengths(&p), [(20_000, Piece::Rest), (10_000, Piece::Dash), (10_000, Piece::Dash), (20_000, Piece::Rest)]); + // Closed: the unpicked rest is one piece round the start. + let sq = SubPath::polyline(&[Point::new(0.0, 0.0), Point::new(20.0, 0.0), Point::new(20.0, 20.0), Point::new(0.0, 20.0)], true); + let p = divide_dashes(&sq, Some(&BTreeSet::from([1])), EXACT, 100).unwrap(); + assert_eq!(lengths(&p), [(10_000, Piece::Dash), (10_000, Piece::Dash), (60_000, Piece::Rest)]); + assert!(p.iter().all(|(s, _)| !s.closed)); + } + + #[test] + fn too_many_pieces_or_no_pattern_is_none() { + assert!(divide_dashes(&line(1000.0), None, EXACT, 99).is_none()); + assert!(divide_dashes(&line(1000.0), None, EXACT, 100).is_some()); + for bad in [0.0, -1.0, f64::NAN, f64::INFINITY] { + assert!(divide_dashes(&line(10.0), None, DashSpec { dash: bad, ..EXACT }, 100).is_none()); + } + assert!(divide_dashes(&line(10.0), None, DashSpec { gap: -1.0, ..EXACT }, 100).is_none()); + assert!(divide_dashes(&line(1e6), None, DashSpec { dash: 1e-6, ..EXACT }, 100_000).is_none()); + } +} diff --git a/crates/ui-egui/src/addons/pathtools.rs b/crates/ui-egui/src/addons/pathtools.rs index b55c38ccd..9503a35e4 100644 --- a/crates/ui-egui/src/addons/pathtools.rs +++ b/crates/ui-egui/src/addons/pathtools.rs @@ -1,12 +1,13 @@ //! Path Precision Tools in the interface: its submenu (Object › Plug-ins › Path Precision Tools), -//! which selects its tools, opens its panels and runs Split / Add Points at Crossings and Convert -//! to Parametric Shape; the Anchor Inspector panel (the selected anchor's position and handles as -//! numbers, the add-on's anchor edits, the Corner Tool's values, [`corner`], and the Shared Edge -//! Merge Tool's, [`shared_edge`]); the Parametric Shape panel ([`shape`]); and its tools' options -//! dialog (a double-click on the tool). Everything acts through `addon.pathtools.*` and +//! which selects its tools, opens its panels and runs Split / Add Points at Crossings, Divide into +//! Dashes ([`dashes`]) and Convert to Parametric Shape; the Anchor Inspector panel (the selected +//! anchor's position and handles as numbers, the add-on's anchor edits, the Corner Tool's values, +//! [`corner`], and the Shared Edge Merge Tool's, [`shared_edge`]); the Parametric Shape panel +//! ([`shape`]); and its tools' options dialog (a double-click on the tool). Everything acts through `addon.pathtools.*` and //! `tool.setOption`. pub mod corner; +pub mod dashes; pub mod shape; pub mod shared_edge; @@ -63,6 +64,7 @@ pub fn menu() -> Vec { Item::Sep, Item::Cmd("Add Points at Crossings", "addon.pathtools.crossings", json!({})), Item::Cmd("Split at Crossings", "addon.pathtools.crossings", json!({ "cut": true })), + Item::Cmd("Divide into Dashes…", dashes::COMMAND, Value::Null), Item::Cmd("Convert to Parametric Shape", "addon.pathtools.shape.convert", json!({})), ] } diff --git a/crates/ui-egui/src/addons/pathtools/dashes.rs b/crates/ui-egui/src/addons/pathtools/dashes.rs new file mode 100644 index 000000000..aa798d03e --- /dev/null +++ b/crates/ui-egui/src/addons/pathtools/dashes.rs @@ -0,0 +1,91 @@ +//! Divide into Dashes in the interface: its dialog (dash and gap lengths, Fit to Corners and Ends, +//! Remove Gaps), which previews on the canvas and runs `addon.pathtools.divideDashes` on OK. + +use egui::Ui; +use serde_json::{Value, json}; + +use crate::VectorcraftApp; +use crate::dialogs::{DialogSpec, form}; +use crate::state::Dialog; +use crate::widgets; + +/// The command, and its dialog's kind. +pub const COMMAND: &str = "addon.pathtools.divideDashes"; +pub const KIND: &str = "divideDashes"; +/// The dash and gap lengths the command takes (pt). +const DASHES: std::ops::RangeInclusive = 0.01..=1e6; +const GAPS: std::ops::RangeInclusive = 0.0..=1e6; +const LABEL_W: f32 = 60.0; +const FIELD_W: f32 = 90.0; + +/// The dialog's fields to start with: the command's defaults. +pub(crate) fn fields() -> Value { + json!({ "dash": 10, "gap": 0, "fit": false, "removeGaps": true }) +} + +pub(crate) const SPEC: DialogSpec = + DialogSpec { heading: |_| tl!("Divide into Dashes").to_string(), body, confirm, preview: true, min_width: 280.0, ..DialogSpec::FORM }; + +fn body(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog) -> bool { + let unit = app.session.general_unit(); + widgets::label_row(ui, tl!("Dash:"), LABEL_W, |ui| { + form::length_within(ui, d, "dash", unit, FIELD_W, Some(DASHES)); + }); + widgets::label_row(ui, tl!("Gap:"), LABEL_W, |ui| { + form::length_within(ui, d, "gap", unit, FIELD_W, Some(GAPS)); + }); + form::check(ui, d, "fit", tl!("Fit to Corners and Ends")); + // Without a gap there are no gap pieces to remove. + let gaps = d.f64("gap", 0.0) > 0.0; + ui.add_enabled_ui(gaps, |ui| form::check(ui, d, "removeGaps", tl!("Remove Gaps"))); + form::preview(app, ui, d, "Divide into Dashes", COMMAND, form::params(d)); + false +} + +/// OK: run the command (keep its preview) and close. +fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { + form::commit_preview(app, COMMAND, form::params(d)) +} + +#[cfg(test)] +mod tests { + use serde_json::json; + use vectorcraft_engine::Session; + + use crate::tests_labels::painted_text; + use crate::{VectorcraftApp, menus}; + + #[test] + fn divide_into_dashes_opens_its_dialog_previews_and_runs_on_ok() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 400, "height": 300})).unwrap(); + let anchors = json!([{"x": 0, "y": 0}, {"x": 100, "y": 0}]); + let id = app.run("path.create", json!({"anchors": anchors})).unwrap()["id"].as_u64().unwrap(); + app.run("select.set", json!({"ids": [id]})).unwrap(); + let entries = menus::menu_entries(&app); + let item = entries.iter().find(|e| e.path == ["Object", "Plug-ins", "Path Precision Tools"] && e.label == "Divide into Dashes…").unwrap(); + let (cmd, p) = menus::click_target(&item.label, item.command.as_deref().unwrap(), &item.params); + menus::invoke(&mut app, &cmd, p); + assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(super::KIND)); + let shown = painted_text(&mut app, |app, ui| crate::dialogs::show(app, ui.ctx())); + for s in ["Divide into Dashes", "Dash:", "Gap:", "Fit to Corners and Ends", "Remove Gaps", "Preview"] { + assert!(shown.contains(s), "{s} in {shown}"); + } + let count = |app: &VectorcraftApp| app.session.doc().unwrap().doc.layers[0].children().unwrap().len(); + let d = app.ui.dialog.as_mut().unwrap(); + d.fields.insert("dash".into(), json!(25)); + d.fields.insert("preview".into(), json!(true)); + painted_text(&mut app, |app, ui| crate::dialogs::show(app, ui.ctx())); + assert_eq!(count(&app), 4, "previewed"); + crate::dialogs::cancel(&mut app); + assert_eq!(count(&app), 1, "rolled back"); + menus::invoke(&mut app, &cmd, serde_json::Value::Null); + let n = app.session.doc().unwrap().history.undo.len(); + app.ui.dialog.as_mut().unwrap().fields.insert("gap".into(), json!(10)); + crate::dialogs::confirm(&mut app).unwrap(); + assert!(app.ui.dialog.is_none()); + assert_eq!(app.session.doc().unwrap().history.undo.len(), n + 1); + // 10 + 10 + … over 100: five dashes, the gaps left empty. + assert_eq!(count(&app), 5); + } +} diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index a4eac7041..0f23c2bc4 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -397,6 +397,7 @@ registry! { PathfinderOptions: [pathfinder_options::KIND] => pathfinder_options::SPEC, RearrangeArtboards: [rearrange_artboards::KIND] => rearrange_artboards::SPEC, PathToolOptions: [crate::addons::pathtools::KIND] => crate::addons::pathtools::OPTIONS_SPEC, + DivideDashes: [crate::addons::pathtools::dashes::KIND] => crate::addons::pathtools::dashes::SPEC, ArrangeTools: [crate::addons::arrange::GRID, crate::addons::arrange::REPLACE, crate::addons::arrange::SIZE] => crate::addons::arrange::SPEC, ShortcutImport: [shortcut_import::KIND] => DialogSpec::window(shortcut_import::show, shortcut_import::confirm), } diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index a960e7dd2..e92164cde 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -5929,6 +5929,15 @@ breakText Glyphs Глифи # Arrange Tools add-on: Group Overlapping Objects. Group Overlapping Objects Групиране на припокриващи се обекти @msg too many objects selected (at most 20000) избрани са твърде много обекти (най-много 20000) +# Path Precision Tools add-on: Divide into Dashes. + Divide into Dashes… Разделяне на тирета… + Divide into Dashes Разделяне на тирета + Dash: Тире: + Gap: Разстояние: + Fit to Corners and Ends Напасване към ъгли и краища + Remove Gaps Премахване на разстоянията +@msg that makes too many pieces (at most 100000): make the dashes longer така се получават твърде много части (най-много 100000): удължете тиретата +@msg select paths, or segments, to divide изберете пътища или сегменти за разделяне Marathi Маратхи Gujarati Гуджарати Bengali Бенгалски diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 6dba3afdf..68745fa1a 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -5963,6 +5963,15 @@ shape Open Otvoren # Arrange Tools add-on: Group Overlapping Objects. Group Overlapping Objects Grupiši preklapajuće objekte @msg too many objects selected (at most 20000) odabrano je previše objekata (najviše 20000) +# Path Precision Tools add-on: Divide into Dashes. + Divide into Dashes… Podijeli na crtice… + Divide into Dashes Podijeli na crtice + Dash: Crtica: + Gap: Razmak: + Fit to Corners and Ends Uklopi u uglove i krajeve + Remove Gaps Ukloni razmake +@msg that makes too many pieces (at most 100000): make the dashes longer to daje previše dijelova (najviše 100000): produžite crtice +@msg select paths, or segments, to divide odaberite putanje ili segmente za dijeljenje Marathi Marathi Gujarati Gudžarati Bengali Bengalski diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index a102f7a7c..318609343 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -6036,6 +6036,15 @@ breakText Glyphs Glifs # Arrange Tools add-on: Group Overlapping Objects. Group Overlapping Objects Agrupa els objectes superposats @msg too many objects selected (at most 20000) s’han seleccionat massa objectes (com a màxim 20000) +# Path Precision Tools add-on: Divide into Dashes. + Divide into Dashes… Divideix en guions… + Divide into Dashes Divideix en guions + Dash: Guió: + Gap: Separació: + Fit to Corners and Ends Ajusta a les cantonades i els extrems + Remove Gaps Elimina les separacions +@msg that makes too many pieces (at most 100000): make the dashes longer això fa massa trossos (com a màxim 100000): allargueu els guions +@msg select paths, or segments, to divide seleccioneu camins o segments per dividir Marathi Marathi Gujarati Gujarati Bengali Bengalí diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index b0c04ffe2..838906c88 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -6046,6 +6046,15 @@ breakText Glyphs Glyfy # Arrange Tools add-on: Group Overlapping Objects. Group Overlapping Objects Seskupit překrývající se objekty @msg too many objects selected (at most 20000) vybráno příliš mnoho objektů (nejvýše 20000) +# Path Precision Tools add-on: Divide into Dashes. + Divide into Dashes… Rozdělit na čárky… + Divide into Dashes Rozdělit na čárky + Dash: Pomlčka: + Gap: Mezera: + Fit to Corners and Ends Přizpůsobit rohům a koncům + Remove Gaps Odstranit mezery +@msg that makes too many pieces (at most 100000): make the dashes longer tím vznikne příliš mnoho dílů (nejvýše 100000): prodlužte čárky +@msg select paths, or segments, to divide vyberte cesty nebo segmenty k rozdělení Marathi Maráthština Gujarati Gudžarátština Bengali Bengálština diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index d85c865bf..bdde0358b 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -5943,6 +5943,15 @@ breakText Glyphs Glyffer # Arrange Tools add-on: Group Overlapping Objects. Group Overlapping Objects Gruppér overlappende objekter @msg too many objects selected (at most 20000) for mange objekter markeret (højst 20000) +# Path Precision Tools add-on: Divide into Dashes. + Divide into Dashes… Opdel i streger… + Divide into Dashes Opdel i streger + Dash: Tankestreg: + Gap: Afstand: + Fit to Corners and Ends Tilpas til hjørner og ender + Remove Gaps Fjern mellemrum +@msg that makes too many pieces (at most 100000): make the dashes longer det giver for mange stykker (højst 100000): gør stregerne længere +@msg select paths, or segments, to divide vælg stier eller segmenter, der skal opdeles Marathi Marathi Gujarati Gujarati Bengali Bengali diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index b1e187de8..a4d2ec778 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -5631,6 +5631,15 @@ breakText Glyphs Glyphen # Arrange Tools add-on: Group Overlapping Objects. Group Overlapping Objects Überlappende Objekte gruppieren @msg too many objects selected (at most 20000) zu viele Objekte ausgewählt (höchstens 20000) +# Path Precision Tools add-on: Divide into Dashes. + Divide into Dashes… In Striche aufteilen… + Divide into Dashes In Striche aufteilen + Dash: Strich: + Gap: Abstand: + Fit to Corners and Ends An Ecken und Enden anpassen + Remove Gaps Lücken entfernen +@msg that makes too many pieces (at most 100000): make the dashes longer das ergibt zu viele Teile (höchstens 100000): machen Sie die Striche länger +@msg select paths, or segments, to divide wählen Sie Pfade oder Segmente zum Aufteilen aus Marathi Marathi Gujarati Gujarati Bengali Bengalisch diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 76d29ca75..e60204139 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -5998,6 +5998,15 @@ breakText Glyphs Γλύφοι # Arrange Tools add-on: Group Overlapping Objects. Group Overlapping Objects Ομαδοποίηση επικαλυπτόμενων αντικειμένων @msg too many objects selected (at most 20000) επιλέχθηκαν πάρα πολλά αντικείμενα (το πολύ 20000) +# Path Precision Tools add-on: Divide into Dashes. + Divide into Dashes… Διαίρεση σε παύλες… + Divide into Dashes Διαίρεση σε παύλες + Dash: Παύλα: + Gap: Απόσταση: + Fit to Corners and Ends Προσαρμογή σε γωνίες και άκρα + Remove Gaps Αφαίρεση κενών +@msg that makes too many pieces (at most 100000): make the dashes longer αυτό δίνει πάρα πολλά κομμάτια (το πολύ 100000): κάντε τις παύλες μεγαλύτερες +@msg select paths, or segments, to divide επιλέξτε διαδρομές ή τμήματα για διαίρεση Marathi Μαραθικά Gujarati Γκουγιαράτι Bengali Βεγγαλικά diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index f1d2645dc..8ce984b1d 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -5988,6 +5988,15 @@ breakText Glyphs Glifos # Arrange Tools add-on: Group Overlapping Objects. Group Overlapping Objects Agrupar objetos superpuestos @msg too many objects selected (at most 20000) demasiados objetos seleccionados (como máximo 20000) +# Path Precision Tools add-on: Divide into Dashes. + Divide into Dashes… Dividir en guiones… + Divide into Dashes Dividir en guiones + Dash: Guion: + Gap: Separación: + Fit to Corners and Ends Ajustar a esquinas y extremos + Remove Gaps Quitar separaciones +@msg that makes too many pieces (at most 100000): make the dashes longer eso da demasiados trozos (como máximo 100000): alargue los guiones +@msg select paths, or segments, to divide seleccione trazados o segmentos para dividir Marathi Maratí Gujarati Guyaratí Bengali Bengalí diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 65c7df126..038074659 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -6076,6 +6076,15 @@ breakText Glyphs Merkit # Arrange Tools add-on: Group Overlapping Objects. Group Overlapping Objects Ryhmitä päällekkäiset objektit @msg too many objects selected (at most 20000) liian monta objektia valittu (enintään 20000) +# Path Precision Tools add-on: Divide into Dashes. + Divide into Dashes… Jaa viivoiksi… + Divide into Dashes Jaa viivoiksi + Dash: Ajatusviiva: + Gap: Väli: + Fit to Corners and Ends Sovita kulmiin ja päihin + Remove Gaps Poista välit +@msg that makes too many pieces (at most 100000): make the dashes longer siitä tulee liian monta osaa (enintään 100000): pidennä viivoja +@msg select paths, or segments, to divide valitse jaettavat polut tai segmentit Marathi Marathi Gujarati Gudžarati Bengali Bengali diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index f0d74bcc7..135abca40 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -6043,6 +6043,15 @@ breakText Glyphs Glyphes # Arrange Tools add-on: Group Overlapping Objects. Group Overlapping Objects Grouper les objets qui se chevauchent @msg too many objects selected (at most 20000) trop d’objets sélectionnés (20000 au plus) +# Path Precision Tools add-on: Divide into Dashes. + Divide into Dashes… Diviser en tirets… + Divide into Dashes Diviser en tirets + Dash: Tiret : + Gap: Écart : + Fit to Corners and Ends Ajuster aux angles et aux extrémités + Remove Gaps Supprimer les écarts +@msg that makes too many pieces (at most 100000): make the dashes longer cela donne trop de morceaux (100000 au plus) : allongez les tirets +@msg select paths, or segments, to divide sélectionnez des tracés ou des segments à diviser Marathi Marathi Gujarati Goudjarati Bengali Bengali diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index bd2b64965..9e64f5f39 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -6032,6 +6032,15 @@ breakText Glyphs Glifák # Arrange Tools add-on: Group Overlapping Objects. Group Overlapping Objects Átfedő objektumok csoportosítása @msg too many objects selected (at most 20000) túl sok objektum van kijelölve (legfeljebb 20000) +# Path Precision Tools add-on: Divide into Dashes. + Divide into Dashes… Felosztás szaggatásokra… + Divide into Dashes Felosztás szaggatásokra + Dash: Gondolatjel: + Gap: Hézag: + Fit to Corners and Ends Illesztés a sarkokhoz és végekhez + Remove Gaps Hézagok eltávolítása +@msg that makes too many pieces (at most 100000): make the dashes longer ez túl sok darabot adna (legfeljebb 100000): hosszabbítsa meg a szaggatásokat +@msg select paths, or segments, to divide jelöljön ki felosztandó útvonalakat vagy szakaszokat Marathi Maráthi Gujarati Gudzsaráti Bengali Bengáli diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index c7a2f0021..0ec84f994 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -5943,6 +5943,15 @@ breakText Glyphs Glif # Arrange Tools add-on: Group Overlapping Objects. Group Overlapping Objects Grupkan objek yang tumpang tindih @msg too many objects selected (at most 20000) terlalu banyak objek dipilih (paling banyak 20000) +# Path Precision Tools add-on: Divide into Dashes. + Divide into Dashes… Bagi menjadi garis putus… + Divide into Dashes Bagi menjadi garis putus + Dash: Tanda pisah: + Gap: Celah: + Fit to Corners and Ends Paskan ke sudut dan ujung + Remove Gaps Hapus celah +@msg that makes too many pieces (at most 100000): make the dashes longer itu menghasilkan terlalu banyak potongan (paling banyak 100000): perpanjang garisnya +@msg select paths, or segments, to divide pilih jalur atau segmen yang akan dibagi Marathi Marathi Gujarati Gujarati Bengali Bengali diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 5f340b02b..863fa08e8 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -5665,6 +5665,15 @@ breakText Glyphs Glifi # Arrange Tools add-on: Group Overlapping Objects. Group Overlapping Objects Raggruppa oggetti sovrapposti @msg too many objects selected (at most 20000) troppi oggetti selezionati (al massimo 20000) +# Path Precision Tools add-on: Divide into Dashes. + Divide into Dashes… Dividi in trattini… + Divide into Dashes Dividi in trattini + Dash: Trattino: + Gap: Distanza: + Fit to Corners and Ends Adatta ad angoli ed estremità + Remove Gaps Rimuovi le distanze +@msg that makes too many pieces (at most 100000): make the dashes longer così si ottengono troppi pezzi (al massimo 100000): allunga i trattini +@msg select paths, or segments, to divide seleziona tracciati o segmenti da dividere Marathi Marathi Gujarati Gujarati Bengali Bengalese diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index da7fd34f0..c6e9c62fb 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -5732,6 +5732,15 @@ breakText Glyphs 字形 # Arrange Tools add-on: Group Overlapping Objects. Group Overlapping Objects 重なり合うオブジェクトをグループ化 @msg too many objects selected (at most 20000) 選択されたオブジェクトが多すぎます(最大 20000) +# Path Precision Tools add-on: Divide into Dashes. + Divide into Dashes… ダッシュに分割… + Divide into Dashes ダッシュに分割 + Dash: ダッシュ: + Gap: 間隔: + Fit to Corners and Ends コーナーと端に合わせる + Remove Gaps 間隔を削除 +@msg that makes too many pieces (at most 100000): make the dashes longer 分割数が多すぎます(最大 100000):ダッシュを長くしてください +@msg select paths, or segments, to divide 分割するパスまたはセグメントを選択してください Marathi マラーティー語 Gujarati グジャラート語 Bengali ベンガル語 diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index d71ee8b9b..c37dceb8e 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -6035,6 +6035,15 @@ breakText Glyphs გლიფები # Arrange Tools add-on: Group Overlapping Objects. Group Overlapping Objects გადაფარვადი ობიექტების დაჯგუფება @msg too many objects selected (at most 20000) არჩეულია ძალიან ბევრი ობიექტი (მაქსიმუმ 20000) +# Path Precision Tools add-on: Divide into Dashes. + Divide into Dashes… ტირეებად დაყოფა… + Divide into Dashes ტირეებად დაყოფა + Dash: ტირე: + Gap: ღრეჩო: + Fit to Corners and Ends კუთხეებსა და ბოლოებზე მორგება + Remove Gaps ღრეჩოების მოცილება +@msg that makes too many pieces (at most 100000): make the dashes longer ეს ძალიან ბევრ ნაწილს იძლევა (მაქსიმუმ 100000): გაზარდეთ ტირეების სიგრძე +@msg select paths, or segments, to divide აირჩიეთ დასაყოფი კონტურები ან სეგმენტები Marathi მარათჰი Gujarati გუჯარათი Bengali ბენგალური diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 6b9ef0524..0d03c7496 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -5628,6 +5628,15 @@ breakText Glyphs 글리프 # Arrange Tools add-on: Group Overlapping Objects. Group Overlapping Objects 겹치는 개체 그룹화 @msg too many objects selected (at most 20000) 선택한 개체가 너무 많습니다(최대 20000개) +# Path Precision Tools add-on: Divide into Dashes. + Divide into Dashes… 대시로 나누기… + Divide into Dashes 대시로 나누기 + Dash: 대시: + Gap: 간격: + Fit to Corners and Ends 모퉁이와 끝에 맞추기 + Remove Gaps 간격 제거 +@msg that makes too many pieces (at most 100000): make the dashes longer 조각이 너무 많아집니다(최대 100000개). 대시를 더 길게 하십시오 +@msg select paths, or segments, to divide 나눌 패스나 세그먼트를 선택하십시오 Marathi 마라티어 Gujarati 구자라트어 Bengali 벵골어 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index c4d9c535e..acecce2dc 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -5987,6 +5987,15 @@ breakText Glyphs Glyphs # Arrange Tools add-on: Group Overlapping Objects. Group Overlapping Objects Overlappende objecten groeperen @msg too many objects selected (at most 20000) te veel objecten geselecteerd (hoogstens 20000) +# Path Precision Tools add-on: Divide into Dashes. + Divide into Dashes… Verdelen in streepjes… + Divide into Dashes Verdelen in streepjes + Dash: Streepje: + Gap: Tussenruimte: + Fit to Corners and Ends Passend maken op hoeken en uiteinden + Remove Gaps Tussenruimten verwijderen +@msg that makes too many pieces (at most 100000): make the dashes longer dat geeft te veel stukken (hoogstens 100000): maak de streepjes langer +@msg select paths, or segments, to divide selecteer paden of segmenten om te verdelen Marathi Marathi Gujarati Gujarati Bengali Bengaals diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 2418eae3c..4b42f1e2d 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -5901,6 +5901,15 @@ breakText Glyphs Glify # Arrange Tools add-on: Group Overlapping Objects. Group Overlapping Objects Grupuj nakładające się obiekty @msg too many objects selected (at most 20000) zaznaczono za dużo obiektów (najwyżej 20000) +# Path Precision Tools add-on: Divide into Dashes. + Divide into Dashes… Podziel na kreski… + Divide into Dashes Podziel na kreski + Dash: Myślnik: + Gap: Odstęp: + Fit to Corners and Ends Dopasuj do narożników i końców + Remove Gaps Usuń odstępy +@msg that makes too many pieces (at most 100000): make the dashes longer to daje za dużo części (najwyżej 100000): wydłuż kreski +@msg select paths, or segments, to divide zaznacz ścieżki lub segmenty do podzielenia Marathi Marathi Gujarati Gudżarati Bengali Bengalski diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 34b223428..86518877a 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -5965,6 +5965,15 @@ breakText Glyphs Glifos # Arrange Tools add-on: Group Overlapping Objects. Group Overlapping Objects Agrupar objetos sobrepostos @msg too many objects selected (at most 20000) objetos demais selecionados (no máximo 20000) +# Path Precision Tools add-on: Divide into Dashes. + Divide into Dashes… Dividir em traços… + Divide into Dashes Dividir em traços + Dash: Travessão: + Gap: Distância: + Fit to Corners and Ends Ajustar aos cantos e extremidades + Remove Gaps Remover distâncias +@msg that makes too many pieces (at most 100000): make the dashes longer isso gera peças demais (no máximo 100000): aumente os traços +@msg select paths, or segments, to divide selecione demarcadores ou segmentos para dividir Marathi Marati Gujarati Guzerate Bengali Bengali diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 747c91585..2b0b85161 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -6023,6 +6023,15 @@ breakText Glyphs Глифы # Arrange Tools add-on: Group Overlapping Objects. Group Overlapping Objects Сгруппировать перекрывающиеся объекты @msg too many objects selected (at most 20000) выделено слишком много объектов (не более 20000) +# Path Precision Tools add-on: Divide into Dashes. + Divide into Dashes… Разделить на штрихи… + Divide into Dashes Разделить на штрихи + Dash: Тире: + Gap: Зазор: + Fit to Corners and Ends Подогнать к углам и концам + Remove Gaps Убрать зазоры +@msg that makes too many pieces (at most 100000): make the dashes longer так получится слишком много частей (не более 100000): удлините штрихи +@msg select paths, or segments, to divide выделите контуры или сегменты для разделения Marathi Маратхи Gujarati Гуджарати Bengali Бенгальский diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 26b3a1d59..e6ab91380 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -6098,6 +6098,15 @@ breakText Glyphs Glyfer # Arrange Tools add-on: Group Overlapping Objects. Group Overlapping Objects Gruppera överlappande objekt @msg too many objects selected (at most 20000) för många objekt markerade (högst 20000) +# Path Precision Tools add-on: Divide into Dashes. + Divide into Dashes… Dela upp i streck… + Divide into Dashes Dela upp i streck + Dash: Tankstreck: + Gap: Mellanrum: + Fit to Corners and Ends Anpassa till hörn och ändar + Remove Gaps Ta bort mellanrum +@msg that makes too many pieces (at most 100000): make the dashes longer det blir för många delar (högst 100000): gör strecken längre +@msg select paths, or segments, to divide markera banor eller segment att dela upp Marathi Marathi Gujarati Gujarati Bengali Bengali diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index a4851f2e3..c4b924a01 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -6003,6 +6003,15 @@ breakText Glyphs Glifler # Arrange Tools add-on: Group Overlapping Objects. Group Overlapping Objects Çakışan Nesneleri Grupla @msg too many objects selected (at most 20000) çok fazla nesne seçili (en fazla 20000) +# Path Precision Tools add-on: Divide into Dashes. + Divide into Dashes… Tirelere Böl… + Divide into Dashes Tirelere Böl + Dash: Tire: + Gap: Boşluk: + Fit to Corners and Ends Köşelere ve Uçlara Uydur + Remove Gaps Boşlukları Kaldır +@msg that makes too many pieces (at most 100000): make the dashes longer bu çok fazla parça oluşturur (en fazla 100000): tireleri uzatın +@msg select paths, or segments, to divide bölünecek yolları veya bölümleri seçin Marathi Marathi dili Gujarati Gücerat dili Bengali Bengalce diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index a4c04383a..d22468de3 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -5645,6 +5645,15 @@ breakText Glyphs Гліфи # Arrange Tools add-on: Group Overlapping Objects. Group Overlapping Objects Згрупувати об’єкти, що перекриваються @msg too many objects selected (at most 20000) виділено забагато об’єктів (не більше 20000) +# Path Precision Tools add-on: Divide into Dashes. + Divide into Dashes… Розділити на штрихи… + Divide into Dashes Розділити на штрихи + Dash: Тире: + Gap: Проміжок: + Fit to Corners and Ends Припасувати до кутів і кінців + Remove Gaps Прибрати проміжки +@msg that makes too many pieces (at most 100000): make the dashes longer так вийде забагато частин (не більше 100000): подовжте штрихи +@msg select paths, or segments, to divide виділіть контури або сегменти для розділення Marathi Маратхі Gujarati Гуджараті Bengali Бенгальська diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index ab593787b..f6b12617a 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -5648,6 +5648,15 @@ breakText Glyphs Glyph # Arrange Tools add-on: Group Overlapping Objects. Group Overlapping Objects Nhóm các đối tượng chồng lên nhau @msg too many objects selected (at most 20000) chọn quá nhiều đối tượng (tối đa 20000) +# Path Precision Tools add-on: Divide into Dashes. + Divide into Dashes… Chia thành nét đứt… + Divide into Dashes Chia thành nét đứt + Dash: Gạch ngang: + Gap: Khoảng hở: + Fit to Corners and Ends Khớp với góc và đầu mút + Remove Gaps Gỡ các khoảng hở +@msg that makes too many pieces (at most 100000): make the dashes longer như vậy tạo ra quá nhiều mảnh (tối đa 100000): hãy kéo dài nét +@msg select paths, or segments, to divide chọn các đường dẫn hoặc đoạn để chia Marathi Tiếng Marathi Gujarati Tiếng Gujarati Bengali Tiếng Bengali diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 9b506a128..9381523e5 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -5888,6 +5888,15 @@ breakText Glyphs 字形 # Arrange Tools add-on: Group Overlapping Objects. Group Overlapping Objects 编组重叠对象 @msg too many objects selected (at most 20000) 选择的对象过多(最多 20000 个) +# Path Precision Tools add-on: Divide into Dashes. + Divide into Dashes… 分割为短划线… + Divide into Dashes 分割为短划线 + Dash: 短划线: + Gap: 间隙: + Fit to Corners and Ends 适合转角和端点 + Remove Gaps 移除间隙 +@msg that makes too many pieces (at most 100000): make the dashes longer 这会产生过多片段(最多 100000 个):请加长短划线 +@msg select paths, or segments, to divide 请选择要分割的路径或线段 Marathi 马拉地语 Gujarati 古吉拉特语 Bengali 孟加拉语 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 3e645ca09..0d6ae8b2b 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -5685,6 +5685,15 @@ breakText Glyphs 字符 # Arrange Tools add-on: Group Overlapping Objects. Group Overlapping Objects 群組重疊物件 @msg too many objects selected (at most 20000) 選取的物件過多(最多 20000 個) +# Path Precision Tools add-on: Divide into Dashes. + Divide into Dashes… 分割為短破折號… + Divide into Dashes 分割為短破折號 + Dash: 短破折號: + Gap: 間隙: + Fit to Corners and Ends 符合轉角和端點 + Remove Gaps 移除間隙 +@msg that makes too many pieces (at most 100000): make the dashes longer 這會產生過多片段(最多 100000 個):請加長短破折號 +@msg select paths, or segments, to divide 請選取要分割的路徑或線段 Marathi 馬拉地文 Gujarati 古吉拉特文 Bengali 孟加拉文 diff --git a/crates/ui-egui/src/menus/invoke.rs b/crates/ui-egui/src/menus/invoke.rs index 70c863971..77dd67f44 100644 --- a/crates/ui-egui/src/menus/invoke.rs +++ b/crates/ui-egui/src/menus/invoke.rs @@ -68,6 +68,7 @@ pub(super) fn menu_dialog(app: &VectorcraftApp, id: &str) -> Option<(&'static st ("splitIntoGrid", crate::dialogs::path_ops::split_fields(size.0, size.1)) } "object.vectorHalftone" => (crate::dialogs::halftone::KIND, crate::dialogs::halftone::fields()), + crate::addons::pathtools::dashes::COMMAND => (crate::addons::pathtools::dashes::KIND, crate::addons::pathtools::dashes::fields()), "artboard.rearrange" => (crate::dialogs::rearrange_artboards::KIND, crate::dialogs::rearrange_artboards::open_fields(app)), _ => return None, }) diff --git a/docs/mcp.md b/docs/mcp.md index 705b9d54b..8c1d1f907 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -5966,6 +5966,16 @@ Commands: - `addon.pathtools.crossings {cut?, selfCrossings?, ids?}` adds anchors where the selected paths cross one another (and where each crosses itself, with `selfCrossings`), or with `cut` also cuts them there: a path of one subpath becomes a path per piece. Paths that meet at a shared anchor don't cross there; at most 10,000 segments at once. +- `addon.pathtools.divideDashes {dash?, gap?, fit?, removeGaps?}` cuts the selected paths into separate open paths + along the curve, each `dash` long (0.01 to 1,000,000 pt, 10 by default) with `gap` between them (0 to 1,000,000 pt, + 0 by default: the pieces touch), every piece keeping the path's appearance, as one undo step. The gap pieces are left + out unless `removeGaps` is false. With direct-selected segments (or anchors) only those runs are divided, the + pattern running on across segments that follow one another; the rest of such a path stays as pieces of its own. + With `fit`, every run between corners and ends holds a whole number of dashes, all stretched or squeezed alike so it + starts and ends on a dash (a closed path with no corner holds whole periods); otherwise the lengths are exact and the + last piece of each run is what is left. The pieces are selected; more than 100,000 is refused. → `{ids, dashes}`. + Divide into Dashes… opens a dialog (`ui.menuDialog {command}`, kind `divideDashes`) whose fields are these + parameters, with Preview. - `addon.pathtools.cornerSet {ids?, anchors?, items?, radius, kind?}` cuts live corners, the ones Direct Selection's corner widgets and the Corners dialog edit, as one undo step. `ids` (default: the selection) are the paths and `anchors` the anchor indices of each path's outline with no corners cut; without `anchors`, a path's direct-selected diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 1a0fe7d61..2f2a22642 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with Adobe Illustrator -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Arrange Tools add-on: Group Overlapping Objects, #4928; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Path Precision Tools add-on: Divide into Dashes, #4950; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) ## Feature submission evidence @@ -1403,10 +1403,15 @@ dialogs, Preview and Cancel, Zoom to Selection) and `command_sweep`. Commands: ` A VectorCraft addition, not a feature of the reference app, so no area score moves. Object › Plug-ins › Arrange Tools › Group Overlapping Objects groups the top-level selected objects wherever their visual bounds overlap or touch, chains included (A overlapping B and B overlapping C makes one group of three), only among objects of the same layer or group; objects that overlap nothing stay as they are, each new group takes its frontmost member's place in the stacking order, and the groups and the loose objects stay selected. One undo step; at most 20000 objects, compared by a sweep along x with union-find. Tests: `group_overlapping_*` in `cmd::addons::arrange::tests` in `vectorcraft-engine` (chains, loners, stacking order, layers kept apart, visual bounds, no step when nothing overlaps, many objects), `group_overlapping_objects_runs_directly` in `vectorcraft-ui-egui` and `command_sweep`. Commands: `docs/mcp.md` › Arrange Tools add-on. +## Path Precision Tools add-on: Divide into Dashes (2026-11-01) + +A VectorCraft addition, not a feature of the reference app, so no area score moves. Object › Plug-ins › Path Precision Tools › Divide into Dashes… cuts the selected paths into separate open paths along the curve (arc length, not straight distance): each `dash` long (0.01 to 1000000 pt, 10 by default) with `gap` between them (0 by default: the pieces touch); the gap pieces are left out unless Remove Gaps is off. With direct-selected segments (or anchors) only those runs are divided, the pattern running on across segments that follow one another, and the rest of such a path stays as pieces of its own. Fit to Corners and Ends gives every run between corners and ends a whole number of dashes, all stretched or squeezed alike so it starts and ends on a dash (a closed path without corners holds whole periods); otherwise the lengths are exact and each run's last piece is what is left. Every piece keeps the path's appearance and is selected; one undo step; at most 100000 pieces (more is refused). The dialog takes lengths in any unit and previews on the canvas. Tests: `dashes::tests` in `vectorcraft-pathops` (exact and fitted lengths, curves measured along them, picked segments on open and closed paths, the piece cap and bad patterns), `divide_into_dashes_*` in `cmd::addons::pathtools::tests` in `vectorcraft-engine` (gaps removed or kept, fitted lengths, the stroke kept, one undo step, selected segments only, bad params and too many pieces refused), `divide_into_dashes_opens_its_dialog_previews_and_runs_on_ok` in `vectorcraft-ui-egui` and `command_sweep`. Commands: `docs/mcp.md` › Path Precision Tools add-on. + ## Revision history | Date | Change | Summary | |---|---|---| +| 2026-11-01 | minor | Path Precision Tools add-on (a VectorCraft addition): Divide into Dashes (#4950); evidence added; scores unchanged. | | 2026-11-01 | minor | Arrange Tools add-on (a VectorCraft addition): Group Overlapping Objects (#4928); evidence added; scores unchanged. | | 2026-11-01 | minor | Type core: a Language per character (Character panel › Language, `text.setFormat` / `text.setRangeStyle {language}`, character styles), new type in the document's language; text is shaped in its language so fonts use their language forms (OpenType `locl`: Turkish `fi`, #4940, #4946); Hindi, Marathi, Gujarati, Bengali, Punjabi, Tamil, Telugu, Kannada, Malayalam, Odia and Urdu in the language list (#4943); scores unchanged | | 2026-10-31 | minor | Selection tools: with Alt held at the press, a Direct Selection marquee selects every path it touches whole and keeps the Group Selection pointer (#4890); a press on a selected object's centre point selects the object whole and a drag from it moves it (#4891); regression tests in `direct/tests.rs`; scores and estimates unchanged. | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 6672e0f94..6c31deb0a 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Arrange Tools add-on: Group Overlapping Objects, #4928) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Path Precision Tools add-on: Divide into Dashes, #4950) · **Target:** Adobe Illustrator 2026 (30.x) ## Feature submission evidence @@ -332,6 +332,7 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po | Arrange Tools add-on (VectorCraft addition): Object › Plug-ins › Arrange Tools › Group Overlapping Objects groups the selected objects whose visual bounds overlap or touch (chains included, within one layer or group), each group at its frontmost member's stacking place, one undo step | done | #4928; `addon.arrange.groupOverlapping`, `crates/engine/src/cmd/addons/arrange.rs` and its tests; not a feature of the reference app, so it is not scored | | Smart Pen add-on (VectorCraft addition): Object › Plug-ins › Smart Pen selects the Smart Pen Tool, which the All Tools drawer lists under its own heading but neither toolbar shows as shipped (no shortcut), and opens the Smart Pen panel (next segment straight or curved; Remove Points Keep Shape, Make Smooth, Make Corner, Retract Handles, Close or Join). The tool draws (click: straight or curved segment, a curve after a straight one tangent; drag: smooth anchor, Alt breaks the handle, Shift 45°, Space moves it; Tab switches a segment; rubber band; close, join, continue) and edits without switching tools (click selects a point, drag moves it, Alt-click removes it keeping the shape, Shift+Alt-click deletes it, double-click toggles smooth/corner; handle drags with Shift and Alt; segment drags reshape, Alt-click adds a point) | done | `addon.pen.*`, `crates/tools/src/addons/smart_pen.rs`, `crates/ui-egui/src/addons/pen.rs` and their tests; not a feature of the reference app, so it is not scored | | Path Precision Tools add-on (VectorCraft addition): Object › Plug-ins › Path Precision Tools selects its seven tools (Anchor Eraser Brush, Extend Path, Measure & Inspect, Parametric Shape, Corner Tool, Slide Anchor Along Path, Shared Edge Merge Tool), which the All Tools drawer lists under their own heading but neither toolbar shows as shipped, opens the Anchor Inspector panel (the selected anchor's X/Y and handle lengths and angles as fields; Make Smooth, Make Corner, Equalize Handles, Remove Points Keep Shape, Add Points, the three ways to close) and the Parametric Shape panel (kind, count, W/H, rotation, the kind's own values and corner radius of the selected parametric shape, or of the tool's next one), and runs Add Points at / Split at Crossings and Convert to Parametric Shape; the Parametric Shape Tool draws ten re-editable kinds from the centre (↑/↓ and ←/→ change their numbers while dragging) and edits a selected one with handles; the Corner Tool cuts editable corners on any path (hover shows the anchor, a click uses its radius and kind, a drag sizes the corner, Shift whole values; a corner's marker selects it and loads its values, drags to resize, to 0 or double-clicked removes it; never past what fits; Alt-marquee cuts every anchor inside) with a Corners section in the Anchor Inspector (radius, kind, selected count, Apply to Selected Anchors); Slide Anchor Along Path drags an anchor along its curve with the segments beside it refitted, Shift snapping where the tangent is a multiple of 45°; the Shared Edge Merge Tool shows, over two paths touching at two coincident anchors, the two stretches and the edge merging them, a click merges them (half way; Shift the upper path, Alt the lower), and near anchors meet or an anchor one path lacks is added to match (Shift+Alt asks for this inside a stretch), with a Shared Edges section in the Anchor Inspector (brush diameter, coincidence 0 to 3 pt, Keep Shared Points Together, which takes coincident anchors along with the add-on's own anchor moves); a double-click on the brush or Extend Path tool opens its options | done | `addon.pathtools.*`, `crates/tools/src/addons/`, `crates/ui-egui/src/addons/pathtools.rs`, `crates/ui-egui/src/addons/pathtools/` and their tests; not a feature of the reference app, so it is not scored | +| Path Precision Tools add-on (VectorCraft addition): Object › Plug-ins › Path Precision Tools › Divide into Dashes… (Dash and Gap in any unit, Fit to Corners and Ends, Remove Gaps; Preview) cuts the selected paths, or only their direct-selected segments, into open pieces along the curve, keeping their appearance, one undo step | done | #4950; `addon.pathtools.divideDashes`, `crates/pathops/src/addons/pathtools/dashes.rs`, `crates/ui-egui/src/addons/pathtools/dashes.rs` and their tests; not a feature of the reference app, so it is not scored | | Path Precision Tools add-on (VectorCraft addition): Effect › Path Effects lists its live effects (Dashes to Geometry, Add Anchors, Extend Path, Close Path, Smooth Anchors, Retract Handles, Reverse Direction, Simplify Anchors, Split at Anchors), each with the effect dialog's live Preview except Reverse Direction, which has no options, and listed in the Appearance panel like any effect | done | `addon.path.*`, `crates/effects/src/addons/path.rs`, `crates/ui-egui/src/addons/pathtools.rs` and their tests; not a feature of the reference app, so it is not scored | | Path Precision Tools add-on (VectorCraft addition): Object › Plug-ins › Path Precision Tools selects its four tools (Anchor Eraser Brush, Extend Path, Measure & Inspect, Parametric Shape), which the All Tools drawer lists under their own heading but neither toolbar shows as shipped, opens the Anchor Inspector panel (the selected anchor's X/Y and handle lengths and angles as fields; Make Smooth, Make Corner, Equalize Handles, Remove Points Keep Shape, Add Points, the three ways to close) and the Parametric Shape panel (kind, count, W/H, rotation, the kind's own values and corner radius of the selected parametric shape, or of the tool's next one), and runs Add Points at / Split at Crossings and Convert to Parametric Shape; the Parametric Shape Tool draws ten re-editable kinds from the centre (↑/↓ and ←/→ change their numbers while dragging) and edits a selected one with handles; a double-click on the brush or Extend Path tool opens its options | done | `addon.pathtools.*`, `crates/tools/src/addons/`, `crates/ui-egui/src/addons/pathtools.rs`, `crates/ui-egui/src/addons/pathtools/shape.rs` and their tests; not a feature of the reference app, so it is not scored | | Path Precision Tools add-on (VectorCraft addition): Object › Plug-ins › Path Precision Tools selects its six tools (Anchor Eraser Brush, Extend Path, Measure & Inspect, Parametric Shape, Corner Tool, Slide Anchor Along Path), which the All Tools drawer lists under their own heading but neither toolbar shows as shipped, opens the Anchor Inspector panel (the selected anchor's X/Y and handle lengths and angles as fields; Make Smooth, Make Corner, Equalize Handles, Remove Points Keep Shape, Add Points, the three ways to close) and the Parametric Shape panel (kind, count, W/H, rotation, the kind's own values and corner radius of the selected parametric shape, or of the tool's next one), and runs Add Points at / Split at Crossings and Convert to Parametric Shape; the Parametric Shape Tool draws ten re-editable kinds from the centre (↑/↓ and ←/→ change their numbers while dragging) and edits a selected one with handles; the Corner Tool cuts editable corners on any path (hover shows the anchor, a click uses its radius and kind, a drag sizes the corner, Shift whole values; a corner's marker selects it and loads its values, drags to resize, to 0 or double-clicked removes it; never past what fits; Alt-marquee cuts every anchor inside) with a Corners section in the Anchor Inspector (radius, kind, selected count, Apply to Selected Anchors); Slide Anchor Along Path drags an anchor along its curve with the segments beside it refitted, Shift snapping where the tangent is a multiple of 45°; a double-click on the brush or Extend Path tool opens its options | done | `addon.pathtools.*`, `crates/tools/src/addons/`, `crates/ui-egui/src/addons/pathtools.rs`, `crates/ui-egui/src/addons/pathtools/` and their tests; not a feature of the reference app, so it is not scored | @@ -382,6 +383,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development | Date | Change | Summary | |---|---|---| +| 2026-11-01 | minor | Path Precision Tools add-on (a VectorCraft addition): Divide into Dashes (#4950); scores and estimates unchanged | | 2026-11-01 | minor | Arrange Tools add-on (a VectorCraft addition): Group Overlapping Objects (#4928); scores and estimates unchanged | | 2026-11-01 | minor | Character panel › Language: the selected characters' language, from Document Setup's list, which gains Hindi, Marathi, Gujarati, Bengali, Punjabi, Tamil, Telugu, Kannada, Malayalam, Odia and Urdu (#4946, #4943); the Digits menu's numeral names keep their own translations; scores unchanged | | 2026-11-01 | minor | VectorCraft Options' Version is a menu of the versions VectorCraft writes, each named by the releases that read it, and an uncompressible version turns Use Compression off (#4931); a save refused before writing shows the Can't save alert with the reason (#4933); About › Contributors' table keeps its header row above the scrolling rows (#4944); the Gradient panel's freeform point swatch opens the point's popover (#4953); scores unchanged | From 916b60898a5f5de5179e37239d4637a829b0cc8d Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sun, 1 Nov 2026 02:42:19 +0700 Subject: [PATCH 3/4] Fix #4927: Save a Timestamped Copy --- ROADMAP.md | 4 +- crates/engine/src/cmd/addons.rs | 2 + crates/engine/src/cmd/addons/document.rs | 73 ++++++++++++++ .../engine/src/cmd/addons/document/tests.rs | 98 +++++++++++++++++++ crates/engine/src/cmd/fileio.rs | 1 + crates/engine/src/cmd/fileio/save.rs | 2 +- crates/engine/tests/command_sweep.rs | 3 + crates/ui-egui/src/addons.rs | 2 + crates/ui-egui/src/addons/document.rs | 30 ++++++ crates/ui-egui/src/addons/document/tests.rs | 63 ++++++++++++ crates/ui-egui/src/i18n/bg.tsv | 7 ++ crates/ui-egui/src/i18n/bs.tsv | 7 ++ crates/ui-egui/src/i18n/ca.tsv | 7 ++ crates/ui-egui/src/i18n/cs.tsv | 7 ++ crates/ui-egui/src/i18n/da.tsv | 7 ++ crates/ui-egui/src/i18n/de.tsv | 7 ++ crates/ui-egui/src/i18n/el.tsv | 7 ++ crates/ui-egui/src/i18n/es.tsv | 7 ++ crates/ui-egui/src/i18n/fi.tsv | 7 ++ crates/ui-egui/src/i18n/fr.tsv | 7 ++ crates/ui-egui/src/i18n/hu.tsv | 7 ++ crates/ui-egui/src/i18n/id.tsv | 7 ++ crates/ui-egui/src/i18n/it.tsv | 7 ++ crates/ui-egui/src/i18n/ja.tsv | 7 ++ crates/ui-egui/src/i18n/ka.tsv | 7 ++ crates/ui-egui/src/i18n/ko.tsv | 7 ++ crates/ui-egui/src/i18n/nl.tsv | 7 ++ crates/ui-egui/src/i18n/pl.tsv | 7 ++ crates/ui-egui/src/i18n/pt-br.tsv | 7 ++ crates/ui-egui/src/i18n/ru.tsv | 7 ++ crates/ui-egui/src/i18n/sv.tsv | 7 ++ crates/ui-egui/src/i18n/tr.tsv | 7 ++ crates/ui-egui/src/i18n/uk.tsv | 7 ++ crates/ui-egui/src/i18n/vi.tsv | 7 ++ crates/ui-egui/src/i18n/zh-hans.tsv | 7 ++ crates/ui-egui/src/i18n/zh-hant.tsv | 7 ++ crates/ui-egui/src/menus/run.rs | 1 + docs/mcp.md | 18 ++++ docs/target-app-parity.md | 7 +- docs/ui-parity.md | 4 +- 40 files changed, 486 insertions(+), 4 deletions(-) create mode 100644 crates/engine/src/cmd/addons/document.rs create mode 100644 crates/engine/src/cmd/addons/document/tests.rs create mode 100644 crates/ui-egui/src/addons/document.rs create mode 100644 crates/ui-egui/src/addons/document/tests.rs diff --git a/ROADMAP.md b/ROADMAP.md index b8365d0f0..d61922cbe 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -9,7 +9,7 @@ **Stage: alpha** · next: beta, ~13 points (ready for real work ~62% → ~75%, and reliable `.ai` exchange) and ~170–270 h away -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Progress log row for Divide into Dashes in the Path Precision Tools add-on; headline numbers unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Progress log row for the Document Tools add-on's Save a Timestamped Copy; headline numbers unchanged) · **Target:** Adobe Illustrator 2026 (30.x) VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adobe Illustrator workflow. It runs on macOS, Windows, Linux, FreeBSD and the web (WASM), and agents can drive all of it over MCP, the CLI and a JSON @@ -129,6 +129,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap ## Progress log +- 2026-11-01: Document Tools, a built-in add-on (Object › Plug-ins › Document Tools), starts with Save a Timestamped Copy: a copy of the document named `_YYYY-MM-DD_HH-MM` (UTC; `-2`, `-3`… when that name is taken) goes next to it, or into a folder picked for a document never saved, written as File › Save a Copy writes it, so the document keeps its path, title and modified state. Off on the web, which has no clock. A VectorCraft addition: headline numbers unchanged. - 2026-11-01: Divide into Dashes joins the Path Precision Tools add-on (Object › Plug-ins › Path Precision Tools): the selected paths, or only their direct-selected segments, are cut into separate open paths of a dash length measured along the curve, with an optional gap (left empty, or kept as pieces), each keeping the path's appearance; Fit to Corners and Ends stretches or squeezes the lengths so every run starts and ends on a dash. A dialog with Preview, one undo step, at most 100000 pieces. A VectorCraft addition: headline numbers unchanged. - 2026-11-01: Group Overlapping Objects joins the Arrange Tools add-on (Object › Plug-ins › Arrange Tools): the selected objects whose visual bounds overlap or touch, chains included, go into one group each, within their own layer or group, at the stacking place of their frontmost member; objects that overlap nothing stay as they are. One undo step, a sweep along x so thousands of loose pieces group quickly (at most 20000 objects). A VectorCraft addition: headline numbers unchanged. - 2026-10-31: Export for Screens works as the reference app's dialog: Artboards and Assets tabs with a grid or list of thumbnails, Clear Selection and Prefix, Create Sub-folders by Scale or Format (on by default), Export PDFs As Single File (the chosen artboards as one PDF's pages, `pdfs: "single"`) or Multiple Files, iOS and Android rows, format presets in a ☰ menu, and a count of the files the export writes (#4895). About 55 community PRs and fixes landed the same day: Find Font, Replace Font, Outline Stroke and Expand leave hidden and locked art alone (with a warning); DXF export writes LINE and ELLIPSE entities and outlines only the strokes a CAD line can't draw; Blob Brush Angle and Roundness; Shift-constrained Perspective Selection; Grid Repeat Options Preview; a gradient mesh from a file budgeted at about a million quads; width-profile strokes keep their shape zoomed out; `.ai`/EPS import fidelity (area type, envelopes, transparency in dictionaries, character styles); PDF import finds hyphenated and CMap-suffixed font names; Windows renders follow the process's core affinity; Enter or Escape in a floating panel's field no longer reaches the canvas (#4803); Properties shows Blend Options for any selected blend (#4817); Artwork Cleanup finds doubled type (#4835) and joins stacked lines into one object (Join Point Text, #4826). Headline numbers unchanged. @@ -206,6 +207,7 @@ The full earlier record of what landed is kept in [docs/roadmap.md](docs/roadmap | Date | Change | Summary | |---|---|---| +| 2026-11-01 | minor | Progress log row for the Document Tools add-on's Save a Timestamped Copy (#4926); headline numbers unchanged. | | 2026-11-01 | minor | Progress log row for Divide into Dashes (Path Precision Tools add-on, #4950); headline numbers unchanged. | | 2026-11-01 | minor | Progress log row for Group Overlapping Objects (Arrange Tools add-on, #4928); headline numbers unchanged. | | 2026-10-31 | minor | Progress log row for Export for Screens laid out as the reference app's (#4895) and the 2026-10-31 community landings; headline numbers unchanged. | diff --git a/crates/engine/src/cmd/addons.rs b/crates/engine/src/cmd/addons.rs index 2e153491b..21d73cc4f 100644 --- a/crates/engine/src/cmd/addons.rs +++ b/crates/engine/src/cmd/addons.rs @@ -4,6 +4,7 @@ mod arrange; mod cleanup; mod color; +mod document; mod pathtools; mod pen; @@ -14,6 +15,7 @@ pub fn specs() -> Vec { let mut v = arrange::specs(); v.extend(cleanup::specs()); v.extend(color::specs()); + v.extend(document::specs()); v.extend(pathtools::specs()); v.extend(pen::specs()); v diff --git a/crates/engine/src/cmd/addons/document.rs b/crates/engine/src/cmd/addons/document.rs new file mode 100644 index 000000000..b4911e475 --- /dev/null +++ b/crates/engine/src/cmd/addons/document.rs @@ -0,0 +1,73 @@ +//! Document Tools (`addon.document.*`), a built-in add-on: Save a Timestamped Copy writes a copy of +//! the document named with the date and time through File › Save a Copy's save. Its submenu +//! (Object › Plug-ins › Document Tools) is the UI's. + +#[cfg(test)] +mod tests; + +use serde_json::{Value, json}; + +use super::super::fileio::{self, SaveMode}; +use super::super::{CommandSpec, bad, clock_date, cmd, date_param, has_doc, str_param}; +use crate::{Result, Session}; + +/// The most numbers (`-2`, `-3`…) tried after a timestamped name that is taken. +const MAX_NUMBER: u32 = 999; + +pub fn specs() -> Vec { + vec![cmd!( + "addon.document.saveTimestampedCopy", + "Save a Timestamped Copy", + ["Object", "Plug-ins", "Document Tools"], + None, + "{folder? (default: the document's own folder; needed for a document never saved), format?: vectorcraft|pdf|svg|svgz|ai|eps (default: the document's own, else vectorcraft), time?: Unix seconds (default: now)} write a copy of the document named _YYYY-MM-DD_HH-MM. (the date and time in UTC; -2, -3… added when that name is taken) as File › Save a Copy writes it: the document keeps its path, title and modified state, and no undo step is made. Off where there is no clock (the web) → {path, format, bytes, warnings}", + has_clock, + save_timestamped_copy + )] +} + +/// A document is open and the time can be told. +fn has_clock(s: &Session) -> std::result::Result<(), String> { + has_doc(s)?; + vectorcraft_doc::metadata::output_time().map(|_| ()).ok_or_else(|| "there is no clock here".into()) +} + +/// `.`, then `-2.`… in `folder`: the first that `taken` doesn't hold. +fn free_name(folder: &str, base: &str, ext: &str, taken: impl Fn(&str) -> bool) -> Option { + (1..=MAX_NUMBER) + .map(|i| { + let name = if i == 1 { format!("{base}.{ext}") } else { format!("{base}-{i}.{ext}") }; + std::path::Path::new(folder).join(name).to_string_lossy().into_owned() + }) + .find(|path| !taken(path)) +} + +fn save_timestamped_copy(s: &mut Session, p: &Value) -> Result { + const C: &str = "addon.document.saveTimestampedCopy"; + let st = s.doc()?; + let format = match str_param(p, "format") { + Some(f) => fileio::save_format(Some(f), None), + // A document opened from a format Save doesn't write (a picture, a drawing) copies as native. + None => fileio::save_format(Some(st.format), None).or_else(|_| fileio::save_format(None, None)), + } + .map_err(|e| bad(C, e))?; + let folder = match str_param(p, "folder").filter(|f| !f.trim().is_empty()) { + Some(f) => f.to_string(), + None => st + .path + .as_deref() + .and_then(|path| std::path::Path::new(path).parent()) + .map(|f| f.to_string_lossy().into_owned()) + .filter(|f| !f.is_empty()) + .ok_or_else(|| bad(C, "the document has never been saved: give `folder`"))?, + }; + let stem = fileio::file_stem(st.path.as_deref().unwrap_or(&st.doc.title)); + let given = date_param(p, "time", C)?; + let t = clock_date(s, "time", given).ok_or_else(|| bad(C, "there is no clock here: give `time` (Unix seconds)"))?; + let [y, mo, d, h, mi, _] = vectorcraft_doc::metadata::civil(t); + let base = format!("{stem}_{y:04}-{mo:02}-{d:02}_{h:02}-{mi:02}"); + // Only whether something is there: the write itself goes through the automation roots. + let path = free_name(&folder, &base, format.extensions[0], |p| std::path::Path::new(p).exists()) + .ok_or_else(|| bad(C, "every numbered name for the copy is taken"))?; + fileio::save_document(s, SaveMode::Copy, &json!({ "path": path, "format": format.id })) +} diff --git a/crates/engine/src/cmd/addons/document/tests.rs b/crates/engine/src/cmd/addons/document/tests.rs new file mode 100644 index 000000000..362ea1a43 --- /dev/null +++ b/crates/engine/src/cmd/addons/document/tests.rs @@ -0,0 +1,98 @@ +//! Document Tools' commands: the copy's name and place, the document left as it was, and bad +//! params refused. + +use std::path::{Path, PathBuf}; + +use serde_json::{Value, json}; + +use crate::Session; + +/// 2026-10-05 14:03:00 UTC. +const TIME: i64 = 1_791_208_980; + +/// A new, empty folder of its own for test `name`. +fn folder(name: &str) -> PathBuf { + let dir = std::env::temp_dir().join(format!("vc-addon-document-{name}-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + dir +} + +fn session() -> Session { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); + s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 100, "height": 50})).unwrap(); + s +} + +fn stamp(s: &mut Session, p: Value) -> crate::Result { + s.execute("addon.document.saveTimestampedCopy", &p) +} + +#[test] +fn a_timestamped_copy_goes_next_to_the_document_which_stays_as_it_was() { + let dir = folder("next-to"); + let mut s = session(); + let own = dir.join("poster.vectorcraft"); + s.execute("document.save", &json!({"path": own})).unwrap(); + s.execute("shape.ellipse", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap(); + let (steps, title) = (s.doc().unwrap().history.undo.len(), s.doc().unwrap().doc.title.clone()); + let r = stamp(&mut s, json!({"time": TIME})).unwrap(); + let copy = dir.join("poster_2026-10-05_14-03.vectorcraft"); + assert_eq!(Path::new(r["path"].as_str().unwrap()), copy); + assert!(copy.is_file()); + let st = s.doc().unwrap(); + assert_eq!(st.path.as_deref().map(Path::new), Some(own.as_path()), "the document keeps its path"); + assert_eq!((st.history.undo.len(), &st.doc.title, st.is_dirty()), (steps, &title, true), "no step; still modified"); + // The copy holds the document as it is now: the ellipse too. + let mut other = Session::new(); + other.execute("document.open", &json!({"path": copy})).unwrap(); + assert_eq!(other.doc().unwrap().doc.layers[0].children().unwrap().len(), 2); + // The same minute again: numbered. + let r = stamp(&mut s, json!({"time": TIME})).unwrap(); + assert!(r["path"].as_str().unwrap().ends_with("poster_2026-10-05_14-03-2.vectorcraft"), "{r}"); + let _ = std::fs::remove_dir_all(&dir); +} + +#[test] +fn an_unsaved_document_needs_a_folder_and_other_formats_take_their_extension() { + let dir = folder("unsaved"); + let mut s = session(); + let before = s.doc().unwrap().doc.clone(); + assert!(stamp(&mut s, json!({"time": TIME})).is_err(), "no folder to put it in"); + let r = stamp(&mut s, json!({"time": TIME, "folder": dir, "format": "svg"})).unwrap(); + let name = Path::new(r["path"].as_str().unwrap()).file_name().unwrap().to_string_lossy().into_owned(); + assert!(name.ends_with("_2026-10-05_14-03.svg") && dir.join(&name).is_file(), "{name}"); + let st = s.doc().unwrap(); + assert!(st.path.is_none() && *st.doc == *before, "the document is unchanged"); + let _ = std::fs::remove_dir_all(&dir); +} + +#[test] +fn bad_params_are_refused_and_writes_stay_inside_the_automation_roots() { + let dir = folder("bad"); + let mut s = session(); + for p in [ + json!({"folder": dir, "format": "png"}), + json!({"folder": dir, "format": "dxf"}), + json!({"folder": dir, "time": "now"}), + json!({"folder": dir, "time": 1.5}), + json!({"folder": dir.join("missing").join("deeper"), "time": TIME}), + ] { + assert!(stamp(&mut s, p.clone()).is_err(), "{p}"); + } + assert_eq!(std::fs::read_dir(&dir).unwrap().count(), 0, "nothing written"); + let allowed = dir.join("allowed"); + std::fs::create_dir_all(&allowed).unwrap(); + let roots = crate::file_access::AutomationRoots::new(&[&dir], &[&allowed]).unwrap(); + let outside = crate::file_access::confine(roots.as_ref(), || stamp(&mut s, json!({"folder": dir, "time": TIME}))); + assert!(outside.is_err(), "outside the write root"); + let inside = crate::file_access::confine(roots.as_ref(), || stamp(&mut s, json!({"folder": allowed, "time": TIME}))); + assert!(inside.is_ok(), "{inside:?}"); + assert!(!enabled_without_document(), "off with no document"); + let _ = std::fs::remove_dir_all(&dir); +} + +fn enabled_without_document() -> bool { + Session::new().commands().into_iter().find(|c| c.id == "addon.document.saveTimestampedCopy").unwrap().enabled +} diff --git a/crates/engine/src/cmd/fileio.rs b/crates/engine/src/cmd/fileio.rs index 7471ffb56..e41bdd9b2 100644 --- a/crates/engine/src/cmd/fileio.rs +++ b/crates/engine/src/cmd/fileio.rs @@ -75,6 +75,7 @@ pub(crate) use native::preview_png; pub use native::with_compression_pref; 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, export_folder, 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, diff --git a/crates/engine/src/cmd/fileio/save.rs b/crates/engine/src/cmd/fileio/save.rs index 4a8d56421..a38c4b93c 100644 --- a/crates/engine/src/cmd/fileio/save.rs +++ b/crates/engine/src/cmd/fileio/save.rs @@ -897,7 +897,7 @@ pub fn save_with(s: &mut Session, plan: SavePlan, write: impl FnMut(&str, &[u8]) } /// A save command: plan, then write with the file system (a native file as it is encoded). -fn save(s: &mut Session, mode: SaveMode, p: &Value) -> Result { +pub(crate) fn save(s: &mut Session, mode: SaveMode, p: &Value) -> Result { let plan = save_plan(s, mode, p)?; let job = save_job(s, plan)?; let uid = s.doc()?.uid; diff --git a/crates/engine/tests/command_sweep.rs b/crates/engine/tests/command_sweep.rs index 36fe6401d..c53f51b2a 100644 --- a/crates/engine/tests/command_sweep.rs +++ b/crates/engine/tests/command_sweep.rs @@ -592,6 +592,9 @@ fn structured_junk() { ("addon.pathtools.divideDashes", json!({"dash": 1e-308, "gap": -1e308, "fit": "x"})), ("addon.pathtools.divideDashes", json!({"dash": 0.01, "gap": 1e6, "fit": true, "removeGaps": false})), ("addon.pathtools.divideDashes", json!({"dash": f64::MAX, "gap": [0], "removeGaps": null})), + // Refused before anything is written: no format of that name, a time that is no time. + ("addon.document.saveTimestampedCopy", json!({"format": "png", "folder": 7})), + ("addon.document.saveTimestampedCopy", json!({"format": "svg", "time": [1e308], "folder": null})), // Smart Pen add-on. ("addon.pen.append", json!({"id": 2, "x": 1e308, "y": 0, "in": [1e308, -1e308]})), ("addon.pen.append", json!({"id": 2, "subpath": u64::MAX, "x": 0, "y": 0, "segment": "curved"})), diff --git a/crates/ui-egui/src/addons.rs b/crates/ui-egui/src/addons.rs index 4d148ff58..65210c6d2 100644 --- a/crates/ui-egui/src/addons.rs +++ b/crates/ui-egui/src/addons.rs @@ -4,6 +4,7 @@ pub mod arrange; pub mod cleanup; pub mod color; +pub mod document; pub mod pathtools; pub mod pen; @@ -15,6 +16,7 @@ pub fn plugin_menu() -> Vec { Item::Sub("Arrange Tools", arrange::menu()), Item::Sub("Artwork Cleanup", cleanup::menu()), Item::Sub("Color Effects", color::menu()), + Item::Sub("Document Tools", document::menu()), Item::Sub("Path Precision Tools", pathtools::menu()), Item::Sub("Smart Pen", pen::menu()), Item::Sep, diff --git a/crates/ui-egui/src/addons/document.rs b/crates/ui-egui/src/addons/document.rs new file mode 100644 index 000000000..a94874969 --- /dev/null +++ b/crates/ui-egui/src/addons/document.rs @@ -0,0 +1,30 @@ +//! Document Tools in the interface: its submenu (Object › Plug-ins › Document Tools). Save a +//! Timestamped Copy of a document never saved asks for the folder to put it in ([`save_copy`]). + +use serde_json::{Value, json}; + +use crate::VectorcraftApp; +use crate::menus::Item; + +/// Save a Timestamped Copy. +pub const SAVE_COPY: &str = "addon.document.saveTimestampedCopy"; + +/// Object › Plug-ins › Document Tools. +pub fn menu() -> Vec { + vec![Item::Cmd("Save a Timestamped Copy", SAVE_COPY, json!({}))] +} + +/// Save a Timestamped Copy with params `p`: a document never saved, given no `folder`, goes in the +/// folder picked. The status bar says where the copy went. +pub(crate) fn save_copy(app: &mut VectorcraftApp, p: &Value) -> Result { + let mut p = if p.is_object() { p.clone() } else { json!({}) }; + if p.get("folder").is_none() && app.session.active().is_some_and(|st| st.path.is_none()) { + p["folder"] = json!(crate::picks::folder(app).ok_or("cancelled")?); + } + let r = app.session.execute(SAVE_COPY, &p).map_err(|e| e.to_string())?; + crate::io::report_saved(app, r["path"].as_str().unwrap_or_default(), &r); + Ok(r) +} + +#[cfg(test)] +mod tests; diff --git a/crates/ui-egui/src/addons/document/tests.rs b/crates/ui-egui/src/addons/document/tests.rs new file mode 100644 index 000000000..cf43d59e1 --- /dev/null +++ b/crates/ui-egui/src/addons/document/tests.rs @@ -0,0 +1,63 @@ +use std::path::PathBuf; + +use serde_json::json; +use vectorcraft_engine::Session; + +use super::SAVE_COPY; +use crate::{VectorcraftApp, menus}; + +/// A new, empty folder of its own for test `name`. +fn folder(name: &str) -> PathBuf { + let dir = std::env::temp_dir().join(format!("vc-ui-document-tools-{name}-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + dir +} + +fn app() -> VectorcraftApp { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 400, "height": 300})).unwrap(); + app +} + +/// Click the Document Tools item `label`. +fn click(app: &mut VectorcraftApp, label: &str) { + let entries = menus::menu_entries(app); + let item = entries.iter().find(|e| e.path == ["Object", "Plug-ins", "Document Tools"] && e.label == label).expect(label); + let (id, p) = menus::click_target(&item.label, item.command.as_deref().unwrap(), &item.params); + menus::invoke(app, &id, p); +} + +fn files(dir: &PathBuf) -> Vec { + std::fs::read_dir(dir).unwrap().map(|e| e.unwrap().file_name().to_string_lossy().into_owned()).collect() +} + +#[test] +fn save_a_timestamped_copy_writes_beside_the_document() { + let dir = folder("beside"); + let mut app = app(); + app.run("document.save", json!({"path": dir.join("card.vectorcraft")})).unwrap(); + assert_eq!(menus::menu_entries(&app).iter().find(|e| e.label == "Save a Timestamped Copy").and_then(|e| e.command.as_deref()), Some(SAVE_COPY)); + click(&mut app, "Save a Timestamped Copy"); + let names = files(&dir); + assert_eq!(names.len(), 2, "{names:?}"); + assert!(names.iter().any(|n| n.starts_with("card_20") && n.ends_with(".vectorcraft")), "{names:?}"); + assert!(app.ui.status.starts_with("Saved "), "{}", app.ui.status); + let _ = std::fs::remove_dir_all(&dir); +} + +#[test] +fn an_unsaved_document_asks_for_a_folder() { + let dir = folder("picked"); + let mut app = app(); + let picked = dir.to_string_lossy().into_owned(); + app.services.pick_folder = Some(Box::new(move || Some(picked.clone()))); + click(&mut app, "Save a Timestamped Copy"); + assert_eq!(files(&dir).len(), 1); + assert!(app.session.doc().unwrap().path.is_none(), "the document stays unsaved"); + // No folder picked: nothing written. + app.services.pick_folder = Some(Box::new(|| None)); + assert_eq!(app.run(SAVE_COPY, json!({})).unwrap_err(), "cancelled"); + assert_eq!(files(&dir).len(), 1); + let _ = std::fs::remove_dir_all(&dir); +} diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index e92164cde..519247e09 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -5938,6 +5938,13 @@ breakText Glyphs Глифи Remove Gaps Премахване на разстоянията @msg that makes too many pieces (at most 100000): make the dashes longer така се получават твърде много части (най-много 100000): удължете тиретата @msg select paths, or segments, to divide изберете пътища или сегменти за разделяне +# Document Tools add-on (Object › Plug-ins › Document Tools): Save a Timestamped Copy. + Document Tools Инструменти за документи + Save a Timestamped Copy Запазване на копие с дата и час +@msg there is no clock here тук няма часовник +@msg there is no clock here: give `time` (Unix seconds) тук няма часовник: задайте `time` (Unix секунди) +@msg the document has never been saved: give `folder` документът никога не е запазван: задайте `folder` +@msg every numbered name for the copy is taken всички номерирани имена за копието са заети Marathi Маратхи Gujarati Гуджарати Bengali Бенгалски diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 68745fa1a..5910e5904 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -5972,6 +5972,13 @@ shape Open Otvoren Remove Gaps Ukloni razmake @msg that makes too many pieces (at most 100000): make the dashes longer to daje previše dijelova (najviše 100000): produžite crtice @msg select paths, or segments, to divide odaberite putanje ili segmente za dijeljenje +# Document Tools add-on (Object › Plug-ins › Document Tools): Save a Timestamped Copy. + Document Tools Alati za dokument + Save a Timestamped Copy Sačuvaj kopiju s datumom i vremenom +@msg there is no clock here ovdje nema sata +@msg there is no clock here: give `time` (Unix seconds) ovdje nema sata: navedite `time` (Unix sekunde) +@msg the document has never been saved: give `folder` dokument nikad nije sačuvan: navedite `folder` +@msg every numbered name for the copy is taken sva numerisana imena za kopiju su zauzeta Marathi Marathi Gujarati Gudžarati Bengali Bengalski diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 318609343..6c2ccc29a 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -6045,6 +6045,13 @@ breakText Glyphs Glifs Remove Gaps Elimina les separacions @msg that makes too many pieces (at most 100000): make the dashes longer això fa massa trossos (com a màxim 100000): allargueu els guions @msg select paths, or segments, to divide seleccioneu camins o segments per dividir +# Document Tools add-on (Object › Plug-ins › Document Tools): Save a Timestamped Copy. + Document Tools Eines del document + Save a Timestamped Copy Desa una còpia amb data i hora +@msg there is no clock here aquí no hi ha rellotge +@msg there is no clock here: give `time` (Unix seconds) aquí no hi ha rellotge: indiqueu `time` (segons Unix) +@msg the document has never been saved: give `folder` el document no s’ha desat mai: indiqueu `folder` +@msg every numbered name for the copy is taken tots els noms numerats per a la còpia estan ocupats Marathi Marathi Gujarati Gujarati Bengali Bengalí diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 838906c88..cc0fbfa1b 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -6055,6 +6055,13 @@ breakText Glyphs Glyfy Remove Gaps Odstranit mezery @msg that makes too many pieces (at most 100000): make the dashes longer tím vznikne příliš mnoho dílů (nejvýše 100000): prodlužte čárky @msg select paths, or segments, to divide vyberte cesty nebo segmenty k rozdělení +# Document Tools add-on (Object › Plug-ins › Document Tools): Save a Timestamped Copy. + Document Tools Nástroje dokumentu + Save a Timestamped Copy Uložit kopii s datem a časem +@msg there is no clock here nejsou k dispozici hodiny +@msg there is no clock here: give `time` (Unix seconds) nejsou k dispozici hodiny: zadejte `time` (sekundy Unix) +@msg the document has never been saved: give `folder` dokument nebyl nikdy uložen: zadejte `folder` +@msg every numbered name for the copy is taken všechny číslované názvy kopie jsou obsazené Marathi Maráthština Gujarati Gudžarátština Bengali Bengálština diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index bdde0358b..e77983fcb 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -5952,6 +5952,13 @@ breakText Glyphs Glyffer Remove Gaps Fjern mellemrum @msg that makes too many pieces (at most 100000): make the dashes longer det giver for mange stykker (højst 100000): gør stregerne længere @msg select paths, or segments, to divide vælg stier eller segmenter, der skal opdeles +# Document Tools add-on (Object › Plug-ins › Document Tools): Save a Timestamped Copy. + Document Tools Dokumentværktøjer + Save a Timestamped Copy Gem en kopi med dato og klokkeslæt +@msg there is no clock here der er intet ur her +@msg there is no clock here: give `time` (Unix seconds) der er intet ur her: angiv `time` (Unix-sekunder) +@msg the document has never been saved: give `folder` dokumentet er aldrig blevet gemt: angiv `folder` +@msg every numbered name for the copy is taken alle nummererede navne til kopien er optaget Marathi Marathi Gujarati Gujarati Bengali Bengali diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index a4d2ec778..5a3bf8ef5 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -5640,6 +5640,13 @@ breakText Glyphs Glyphen Remove Gaps Lücken entfernen @msg that makes too many pieces (at most 100000): make the dashes longer das ergibt zu viele Teile (höchstens 100000): machen Sie die Striche länger @msg select paths, or segments, to divide wählen Sie Pfade oder Segmente zum Aufteilen aus +# Document Tools add-on (Object › Plug-ins › Document Tools): Save a Timestamped Copy. + Document Tools Dokumentwerkzeuge + Save a Timestamped Copy Kopie mit Zeitstempel speichern +@msg there is no clock here hier gibt es keine Uhr +@msg there is no clock here: give `time` (Unix seconds) hier gibt es keine Uhr: geben Sie `time` an (Unix-Sekunden) +@msg the document has never been saved: give `folder` das Dokument wurde nie gespeichert: geben Sie `folder` an +@msg every numbered name for the copy is taken alle nummerierten Namen für die Kopie sind vergeben Marathi Marathi Gujarati Gujarati Bengali Bengalisch diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index e60204139..686376775 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -6007,6 +6007,13 @@ breakText Glyphs Γλύφοι Remove Gaps Αφαίρεση κενών @msg that makes too many pieces (at most 100000): make the dashes longer αυτό δίνει πάρα πολλά κομμάτια (το πολύ 100000): κάντε τις παύλες μεγαλύτερες @msg select paths, or segments, to divide επιλέξτε διαδρομές ή τμήματα για διαίρεση +# Document Tools add-on (Object › Plug-ins › Document Tools): Save a Timestamped Copy. + Document Tools Εργαλεία εγγράφου + Save a Timestamped Copy Αποθήκευση αντιγράφου με ημερομηνία και ώρα +@msg there is no clock here δεν υπάρχει ρολόι εδώ +@msg there is no clock here: give `time` (Unix seconds) δεν υπάρχει ρολόι εδώ: δώστε `time` (δευτερόλεπτα Unix) +@msg the document has never been saved: give `folder` το έγγραφο δεν έχει αποθηκευτεί ποτέ: δώστε `folder` +@msg every numbered name for the copy is taken όλα τα αριθμημένα ονόματα για το αντίγραφο είναι πιασμένα Marathi Μαραθικά Gujarati Γκουγιαράτι Bengali Βεγγαλικά diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 8ce984b1d..e99c15f65 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -5997,6 +5997,13 @@ breakText Glyphs Glifos Remove Gaps Quitar separaciones @msg that makes too many pieces (at most 100000): make the dashes longer eso da demasiados trozos (como máximo 100000): alargue los guiones @msg select paths, or segments, to divide seleccione trazados o segmentos para dividir +# Document Tools add-on (Object › Plug-ins › Document Tools): Save a Timestamped Copy. + Document Tools Herramientas de documento + Save a Timestamped Copy Guardar una copia con fecha y hora +@msg there is no clock here aquí no hay reloj +@msg there is no clock here: give `time` (Unix seconds) aquí no hay reloj: indique `time` (segundos Unix) +@msg the document has never been saved: give `folder` el documento nunca se ha guardado: indique `folder` +@msg every numbered name for the copy is taken todos los nombres numerados para la copia están ocupados Marathi Maratí Gujarati Guyaratí Bengali Bengalí diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 038074659..7a4f5e656 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -6085,6 +6085,13 @@ breakText Glyphs Merkit Remove Gaps Poista välit @msg that makes too many pieces (at most 100000): make the dashes longer siitä tulee liian monta osaa (enintään 100000): pidennä viivoja @msg select paths, or segments, to divide valitse jaettavat polut tai segmentit +# Document Tools add-on (Object › Plug-ins › Document Tools): Save a Timestamped Copy. + Document Tools Asiakirjatyökalut + Save a Timestamped Copy Tallenna kopio aikaleimalla +@msg there is no clock here täällä ei ole kelloa +@msg there is no clock here: give `time` (Unix seconds) täällä ei ole kelloa: anna `time` (Unix-sekunteina) +@msg the document has never been saved: give `folder` asiakirjaa ei ole koskaan tallennettu: anna `folder` +@msg every numbered name for the copy is taken kaikki kopion numeroidut nimet ovat varattuja Marathi Marathi Gujarati Gudžarati Bengali Bengali diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 135abca40..81326659d 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -6052,6 +6052,13 @@ breakText Glyphs Glyphes Remove Gaps Supprimer les écarts @msg that makes too many pieces (at most 100000): make the dashes longer cela donne trop de morceaux (100000 au plus) : allongez les tirets @msg select paths, or segments, to divide sélectionnez des tracés ou des segments à diviser +# Document Tools add-on (Object › Plug-ins › Document Tools): Save a Timestamped Copy. + Document Tools Outils de document + Save a Timestamped Copy Enregistrer une copie horodatée +@msg there is no clock here il n’y a pas d’horloge ici +@msg there is no clock here: give `time` (Unix seconds) il n’y a pas d’horloge ici : indiquez `time` (secondes Unix) +@msg the document has never been saved: give `folder` le document n’a jamais été enregistré : indiquez `folder` +@msg every numbered name for the copy is taken tous les noms numérotés pour la copie sont pris Marathi Marathi Gujarati Goudjarati Bengali Bengali diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 9e64f5f39..498ed78e7 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -6041,6 +6041,13 @@ breakText Glyphs Glifák Remove Gaps Hézagok eltávolítása @msg that makes too many pieces (at most 100000): make the dashes longer ez túl sok darabot adna (legfeljebb 100000): hosszabbítsa meg a szaggatásokat @msg select paths, or segments, to divide jelöljön ki felosztandó útvonalakat vagy szakaszokat +# Document Tools add-on (Object › Plug-ins › Document Tools): Save a Timestamped Copy. + Document Tools Dokumentumeszközök + Save a Timestamped Copy Másolat mentése időbélyeggel +@msg there is no clock here itt nincs óra +@msg there is no clock here: give `time` (Unix seconds) itt nincs óra: adja meg a `time` értéket (Unix-másodperc) +@msg the document has never been saved: give `folder` a dokumentum még nem volt mentve: adja meg a `folder` értéket +@msg every numbered name for the copy is taken a másolat összes számozott neve foglalt Marathi Maráthi Gujarati Gudzsaráti Bengali Bengáli diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 0ec84f994..197b69455 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -5952,6 +5952,13 @@ breakText Glyphs Glif Remove Gaps Hapus celah @msg that makes too many pieces (at most 100000): make the dashes longer itu menghasilkan terlalu banyak potongan (paling banyak 100000): perpanjang garisnya @msg select paths, or segments, to divide pilih jalur atau segmen yang akan dibagi +# Document Tools add-on (Object › Plug-ins › Document Tools): Save a Timestamped Copy. + Document Tools Alat dokumen + Save a Timestamped Copy Simpan salinan bercap waktu +@msg there is no clock here tidak ada jam di sini +@msg there is no clock here: give `time` (Unix seconds) tidak ada jam di sini: berikan `time` (detik Unix) +@msg the document has never been saved: give `folder` dokumen belum pernah disimpan: berikan `folder` +@msg every numbered name for the copy is taken semua nama bernomor untuk salinan sudah dipakai Marathi Marathi Gujarati Gujarati Bengali Bengali diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 863fa08e8..54d16513f 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -5674,6 +5674,13 @@ breakText Glyphs Glifi Remove Gaps Rimuovi le distanze @msg that makes too many pieces (at most 100000): make the dashes longer così si ottengono troppi pezzi (al massimo 100000): allunga i trattini @msg select paths, or segments, to divide seleziona tracciati o segmenti da dividere +# Document Tools add-on (Object › Plug-ins › Document Tools): Save a Timestamped Copy. + Document Tools Strumenti documento + Save a Timestamped Copy Salva una copia con data e ora +@msg there is no clock here qui non c’è un orologio +@msg there is no clock here: give `time` (Unix seconds) qui non c’è un orologio: indica `time` (secondi Unix) +@msg the document has never been saved: give `folder` il documento non è mai stato salvato: indica `folder` +@msg every numbered name for the copy is taken tutti i nomi numerati per la copia sono già usati Marathi Marathi Gujarati Gujarati Bengali Bengalese diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index c6e9c62fb..f94365ca3 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -5741,6 +5741,13 @@ breakText Glyphs 字形 Remove Gaps 間隔を削除 @msg that makes too many pieces (at most 100000): make the dashes longer 分割数が多すぎます(最大 100000):ダッシュを長くしてください @msg select paths, or segments, to divide 分割するパスまたはセグメントを選択してください +# Document Tools add-on (Object › Plug-ins › Document Tools): Save a Timestamped Copy. + Document Tools ドキュメントツール + Save a Timestamped Copy 日時付きのコピーを保存 +@msg there is no clock here ここには時計がありません +@msg there is no clock here: give `time` (Unix seconds) ここには時計がありません:`time`(Unix 秒)を指定してください +@msg the document has never been saved: give `folder` ドキュメントは一度も保存されていません:`folder` を指定してください +@msg every numbered name for the copy is taken コピー用の番号付きの名前はすべて使用されています Marathi マラーティー語 Gujarati グジャラート語 Bengali ベンガル語 diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index c37dceb8e..e948b44ed 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -6044,6 +6044,13 @@ breakText Glyphs გლიფები Remove Gaps ღრეჩოების მოცილება @msg that makes too many pieces (at most 100000): make the dashes longer ეს ძალიან ბევრ ნაწილს იძლევა (მაქსიმუმ 100000): გაზარდეთ ტირეების სიგრძე @msg select paths, or segments, to divide აირჩიეთ დასაყოფი კონტურები ან სეგმენტები +# Document Tools add-on (Object › Plug-ins › Document Tools): Save a Timestamped Copy. + Document Tools დოკუმენტის ხელსაწყოები + Save a Timestamped Copy ასლის შენახვა თარიღითა და დროით +@msg there is no clock here აქ საათი არ არის +@msg there is no clock here: give `time` (Unix seconds) აქ საათი არ არის: მიუთითეთ `time` (Unix წამები) +@msg the document has never been saved: give `folder` დოკუმენტი არასოდეს შენახულა: მიუთითეთ `folder` +@msg every numbered name for the copy is taken ასლის ყველა დანომრილი სახელი დაკავებულია Marathi მარათჰი Gujarati გუჯარათი Bengali ბენგალური diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 0d03c7496..e3b4f2401 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -5637,6 +5637,13 @@ breakText Glyphs 글리프 Remove Gaps 간격 제거 @msg that makes too many pieces (at most 100000): make the dashes longer 조각이 너무 많아집니다(최대 100000개). 대시를 더 길게 하십시오 @msg select paths, or segments, to divide 나눌 패스나 세그먼트를 선택하십시오 +# Document Tools add-on (Object › Plug-ins › Document Tools): Save a Timestamped Copy. + Document Tools 문서 도구 + Save a Timestamped Copy 날짜와 시간이 붙은 사본 저장 +@msg there is no clock here 여기에는 시계가 없습니다 +@msg there is no clock here: give `time` (Unix seconds) 여기에는 시계가 없습니다. `time`(Unix 초)을 지정하십시오 +@msg the document has never been saved: give `folder` 문서를 저장한 적이 없습니다. `folder`를 지정하십시오 +@msg every numbered name for the copy is taken 사본에 쓸 번호 붙은 이름이 모두 사용 중입니다 Marathi 마라티어 Gujarati 구자라트어 Bengali 벵골어 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index acecce2dc..e68a49a8e 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -5996,6 +5996,13 @@ breakText Glyphs Glyphs Remove Gaps Tussenruimten verwijderen @msg that makes too many pieces (at most 100000): make the dashes longer dat geeft te veel stukken (hoogstens 100000): maak de streepjes langer @msg select paths, or segments, to divide selecteer paden of segmenten om te verdelen +# Document Tools add-on (Object › Plug-ins › Document Tools): Save a Timestamped Copy. + Document Tools Documentgereedschappen + Save a Timestamped Copy Een kopie met tijdstempel opslaan +@msg there is no clock here er is hier geen klok +@msg there is no clock here: give `time` (Unix seconds) er is hier geen klok: geef `time` op (Unix-seconden) +@msg the document has never been saved: give `folder` het document is nooit opgeslagen: geef `folder` op +@msg every numbered name for the copy is taken alle genummerde namen voor de kopie zijn bezet Marathi Marathi Gujarati Gujarati Bengali Bengaals diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 4b42f1e2d..aa7f7b878 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -5910,6 +5910,13 @@ breakText Glyphs Glify Remove Gaps Usuń odstępy @msg that makes too many pieces (at most 100000): make the dashes longer to daje za dużo części (najwyżej 100000): wydłuż kreski @msg select paths, or segments, to divide zaznacz ścieżki lub segmenty do podzielenia +# Document Tools add-on (Object › Plug-ins › Document Tools): Save a Timestamped Copy. + Document Tools Narzędzia dokumentu + Save a Timestamped Copy Zapisz kopię z datą i godziną +@msg there is no clock here brak tu zegara +@msg there is no clock here: give `time` (Unix seconds) brak tu zegara: podaj `time` (sekundy Unix) +@msg the document has never been saved: give `folder` dokument nigdy nie został zapisany: podaj `folder` +@msg every numbered name for the copy is taken wszystkie numerowane nazwy kopii są zajęte Marathi Marathi Gujarati Gudżarati Bengali Bengalski diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 86518877a..3305725b5 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -5974,6 +5974,13 @@ breakText Glyphs Glifos Remove Gaps Remover distâncias @msg that makes too many pieces (at most 100000): make the dashes longer isso gera peças demais (no máximo 100000): aumente os traços @msg select paths, or segments, to divide selecione demarcadores ou segmentos para dividir +# Document Tools add-on (Object › Plug-ins › Document Tools): Save a Timestamped Copy. + Document Tools Ferramentas de Documento + Save a Timestamped Copy Salvar uma Cópia com Data e Hora +@msg there is no clock here não há relógio aqui +@msg there is no clock here: give `time` (Unix seconds) não há relógio aqui: informe `time` (segundos Unix) +@msg the document has never been saved: give `folder` o documento nunca foi salvo: informe `folder` +@msg every numbered name for the copy is taken todos os nomes numerados para a cópia estão em uso Marathi Marati Gujarati Guzerate Bengali Bengali diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 2b0b85161..d27267088 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -6032,6 +6032,13 @@ breakText Glyphs Глифы Remove Gaps Убрать зазоры @msg that makes too many pieces (at most 100000): make the dashes longer так получится слишком много частей (не более 100000): удлините штрихи @msg select paths, or segments, to divide выделите контуры или сегменты для разделения +# Document Tools add-on (Object › Plug-ins › Document Tools): Save a Timestamped Copy. + Document Tools Инструменты документа + Save a Timestamped Copy Сохранить копию с датой и временем +@msg there is no clock here здесь нет часов +@msg there is no clock here: give `time` (Unix seconds) здесь нет часов: укажите `time` (секунды Unix) +@msg the document has never been saved: give `folder` документ ни разу не сохранялся: укажите `folder` +@msg every numbered name for the copy is taken все нумерованные имена для копии заняты Marathi Маратхи Gujarati Гуджарати Bengali Бенгальский diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index e6ab91380..754962d2e 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -6107,6 +6107,13 @@ breakText Glyphs Glyfer Remove Gaps Ta bort mellanrum @msg that makes too many pieces (at most 100000): make the dashes longer det blir för många delar (högst 100000): gör strecken längre @msg select paths, or segments, to divide markera banor eller segment att dela upp +# Document Tools add-on (Object › Plug-ins › Document Tools): Save a Timestamped Copy. + Document Tools Dokumentverktyg + Save a Timestamped Copy Spara en kopia med tidsstämpel +@msg there is no clock here det finns ingen klocka här +@msg there is no clock here: give `time` (Unix seconds) det finns ingen klocka här: ange `time` (Unix-sekunder) +@msg the document has never been saved: give `folder` dokumentet har aldrig sparats: ange `folder` +@msg every numbered name for the copy is taken alla numrerade namn för kopian är upptagna Marathi Marathi Gujarati Gujarati Bengali Bengali diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index c4b924a01..8d682e785 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -6012,6 +6012,13 @@ breakText Glyphs Glifler Remove Gaps Boşlukları Kaldır @msg that makes too many pieces (at most 100000): make the dashes longer bu çok fazla parça oluşturur (en fazla 100000): tireleri uzatın @msg select paths, or segments, to divide bölünecek yolları veya bölümleri seçin +# Document Tools add-on (Object › Plug-ins › Document Tools): Save a Timestamped Copy. + Document Tools Belge Araçları + Save a Timestamped Copy Zaman Damgalı Bir Kopya Kaydet +@msg there is no clock here burada saat yok +@msg there is no clock here: give `time` (Unix seconds) burada saat yok: `time` verin (Unix saniyesi) +@msg the document has never been saved: give `folder` belge hiç kaydedilmedi: `folder` verin +@msg every numbered name for the copy is taken kopya için tüm numaralı adlar kullanılıyor Marathi Marathi dili Gujarati Gücerat dili Bengali Bengalce diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index d22468de3..b5ac2dc5a 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -5654,6 +5654,13 @@ breakText Glyphs Гліфи Remove Gaps Прибрати проміжки @msg that makes too many pieces (at most 100000): make the dashes longer так вийде забагато частин (не більше 100000): подовжте штрихи @msg select paths, or segments, to divide виділіть контури або сегменти для розділення +# Document Tools add-on (Object › Plug-ins › Document Tools): Save a Timestamped Copy. + Document Tools Інструменти документа + Save a Timestamped Copy Зберегти копію з датою і часом +@msg there is no clock here тут немає годинника +@msg there is no clock here: give `time` (Unix seconds) тут немає годинника: укажіть `time` (секунди Unix) +@msg the document has never been saved: give `folder` документ жодного разу не зберігався: укажіть `folder` +@msg every numbered name for the copy is taken усі нумеровані назви для копії зайняті Marathi Маратхі Gujarati Гуджараті Bengali Бенгальська diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index f6b12617a..49885e57d 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -5657,6 +5657,13 @@ breakText Glyphs Glyph Remove Gaps Gỡ các khoảng hở @msg that makes too many pieces (at most 100000): make the dashes longer như vậy tạo ra quá nhiều mảnh (tối đa 100000): hãy kéo dài nét @msg select paths, or segments, to divide chọn các đường dẫn hoặc đoạn để chia +# Document Tools add-on (Object › Plug-ins › Document Tools): Save a Timestamped Copy. + Document Tools Công cụ tài liệu + Save a Timestamped Copy Lưu bản sao có ngày giờ +@msg there is no clock here ở đây không có đồng hồ +@msg there is no clock here: give `time` (Unix seconds) ở đây không có đồng hồ: hãy cho `time` (giây Unix) +@msg the document has never been saved: give `folder` tài liệu chưa từng được lưu: hãy cho `folder` +@msg every numbered name for the copy is taken mọi tên đánh số cho bản sao đều đã dùng Marathi Tiếng Marathi Gujarati Tiếng Gujarati Bengali Tiếng Bengali diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 9381523e5..2c767f639 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -5897,6 +5897,13 @@ breakText Glyphs 字形 Remove Gaps 移除间隙 @msg that makes too many pieces (at most 100000): make the dashes longer 这会产生过多片段(最多 100000 个):请加长短划线 @msg select paths, or segments, to divide 请选择要分割的路径或线段 +# Document Tools add-on (Object › Plug-ins › Document Tools): Save a Timestamped Copy. + Document Tools 文档工具 + Save a Timestamped Copy 保存带时间戳的副本 +@msg there is no clock here 此处没有时钟 +@msg there is no clock here: give `time` (Unix seconds) 此处没有时钟:请提供 `time`(Unix 秒) +@msg the document has never been saved: give `folder` 文档从未保存过:请提供 `folder` +@msg every numbered name for the copy is taken 副本的所有编号名称均已被占用 Marathi 马拉地语 Gujarati 古吉拉特语 Bengali 孟加拉语 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 0d6ae8b2b..651a1293c 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -5694,6 +5694,13 @@ breakText Glyphs 字符 Remove Gaps 移除間隙 @msg that makes too many pieces (at most 100000): make the dashes longer 這會產生過多片段(最多 100000 個):請加長短破折號 @msg select paths, or segments, to divide 請選取要分割的路徑或線段 +# Document Tools add-on (Object › Plug-ins › Document Tools): Save a Timestamped Copy. + Document Tools 文件工具 + Save a Timestamped Copy 儲存附時間戳記的拷貝 +@msg there is no clock here 此處沒有時鐘 +@msg there is no clock here: give `time` (Unix seconds) 此處沒有時鐘:請提供 `time`(Unix 秒) +@msg the document has never been saved: give `folder` 文件從未儲存過:請提供 `folder` +@msg every numbered name for the copy is taken 拷貝的所有編號名稱均已被佔用 Marathi 馬拉地文 Gujarati 古吉拉特文 Bengali 孟加拉文 diff --git a/crates/ui-egui/src/menus/run.rs b/crates/ui-egui/src/menus/run.rs index 5b83dc25b..f85b25d70 100644 --- a/crates/ui-egui/src/menus/run.rs +++ b/crates/ui-egui/src/menus/run.rs @@ -530,6 +530,7 @@ fn run_ui_part6(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option crate::addons::pathtools::corner::open(app), crate::addons::pathtools::shared_edge::COMMAND => crate::addons::pathtools::shared_edge::open(app), crate::addons::arrange::ZOOM => crate::addons::arrange::zoom(app), + crate::addons::document::SAVE_COPY => crate::addons::document::save_copy(app, p), "tool.setOption" => app.session.set_tool_option_cmd(p), "effect.dialog" => crate::dialogs::open_effect_dialog(app, p), "ui.recolorDialog" => crate::dialogs::recolor::open(app, p), diff --git a/docs/mcp.md b/docs/mcp.md index 8c1d1f907..ec441b736 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -6274,3 +6274,21 @@ command's parameters, with Preview; Set Size of Each's `__width` and `__height` {"name":"run_command","arguments":{"command":"addon.arrange.replace","params":{"fit":"proportional"}}} {"name":"run_command","arguments":{"command":"addon.arrange.size","params":{"width":72,"proportional":true}}} ``` + +## Document Tools add-on + +Document Tools is a built-in add-on for whole-document chores: its commands are `addon.document.*`, and in the desktop +app it lives in Object › Plug-ins › Document Tools. + +`addon.document.saveTimestampedCopy {folder?, format?, time?}` writes a copy of the document named +`_YYYY-MM-DD_HH-MM.`, with `-2`, `-3`… added when that name is taken. The date and time are `time` (Unix +seconds; now by default) in UTC. The copy goes in the document's own folder, or in `folder`, which a document never +saved needs (the desktop app asks for one with the folder picker); `format` is a save format (`vectorcraft`, `pdf`, +`svg`, `svgz`, `ai`, `eps`; the document's own by default). It is written as `file.saveCopy` writes, inside the +automation roots in force: the document keeps its path, title and modified state, and no undo step is made. The +command is off where there is no clock (the web). → `{path, format, bytes, warnings}` + +```json +{"name":"run_command","arguments":{"command":"addon.document.saveTimestampedCopy","params":{}}} +{"name":"run_command","arguments":{"command":"addon.document.saveTimestampedCopy","params":{"folder":"/Users/me/Versions","format":"pdf"}}} +``` diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 2f2a22642..99e2d7b2b 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with Adobe Illustrator -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Path Precision Tools add-on: Divide into Dashes, #4950; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Document Tools add-on: Save a Timestamped Copy, #4926; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) ## Feature submission evidence @@ -1407,10 +1407,15 @@ A VectorCraft addition, not a feature of the reference app, so no area score mov A VectorCraft addition, not a feature of the reference app, so no area score moves. Object › Plug-ins › Path Precision Tools › Divide into Dashes… cuts the selected paths into separate open paths along the curve (arc length, not straight distance): each `dash` long (0.01 to 1000000 pt, 10 by default) with `gap` between them (0 by default: the pieces touch); the gap pieces are left out unless Remove Gaps is off. With direct-selected segments (or anchors) only those runs are divided, the pattern running on across segments that follow one another, and the rest of such a path stays as pieces of its own. Fit to Corners and Ends gives every run between corners and ends a whole number of dashes, all stretched or squeezed alike so it starts and ends on a dash (a closed path without corners holds whole periods); otherwise the lengths are exact and each run's last piece is what is left. Every piece keeps the path's appearance and is selected; one undo step; at most 100000 pieces (more is refused). The dialog takes lengths in any unit and previews on the canvas. Tests: `dashes::tests` in `vectorcraft-pathops` (exact and fitted lengths, curves measured along them, picked segments on open and closed paths, the piece cap and bad patterns), `divide_into_dashes_*` in `cmd::addons::pathtools::tests` in `vectorcraft-engine` (gaps removed or kept, fitted lengths, the stroke kept, one undo step, selected segments only, bad params and too many pieces refused), `divide_into_dashes_opens_its_dialog_previews_and_runs_on_ok` in `vectorcraft-ui-egui` and `command_sweep`. Commands: `docs/mcp.md` › Path Precision Tools add-on. +## Document Tools add-on: Save a Timestamped Copy (2026-11-01) + +A VectorCraft addition, not a feature of the reference app, so no area score moves. Object › Plug-ins › Document Tools › Save a Timestamped Copy writes a copy of the document named `_YYYY-MM-DD_HH-MM.` (the date and time in UTC, through chrono; `-2`, `-3`… added when that name is taken) next to the document, or into a folder picked (the folder picker) when it was never saved, in the document's own format or another save format. It writes through File › Save a Copy's save, so the document keeps its path, title and modified state, no undo step is made, and the write stays inside the automation roots in force. It is off on the web, which has no clock. Tests: `cmd::addons::document::tests` in `vectorcraft-engine` (the name and place, numbered names, the document unchanged and its content in the copy, an unsaved document needing a folder, other formats, bad params refused, automation roots), `addons::document::tests` in `vectorcraft-ui-egui` (the submenu, beside the document, the folder picked or cancelled) and `command_sweep`. Commands: `docs/mcp.md` › Document Tools add-on. + ## Revision history | Date | Change | Summary | |---|---|---| +| 2026-11-01 | minor | Document Tools add-on (a VectorCraft addition): Save a Timestamped Copy (#4926); evidence added; scores unchanged. | | 2026-11-01 | minor | Path Precision Tools add-on (a VectorCraft addition): Divide into Dashes (#4950); evidence added; scores unchanged. | | 2026-11-01 | minor | Arrange Tools add-on (a VectorCraft addition): Group Overlapping Objects (#4928); evidence added; scores unchanged. | | 2026-11-01 | minor | Type core: a Language per character (Character panel › Language, `text.setFormat` / `text.setRangeStyle {language}`, character styles), new type in the document's language; text is shaped in its language so fonts use their language forms (OpenType `locl`: Turkish `fi`, #4940, #4946); Hindi, Marathi, Gujarati, Bengali, Punjabi, Tamil, Telugu, Kannada, Malayalam, Odia and Urdu in the language list (#4943); scores unchanged | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 6c31deb0a..51578bb92 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Path Precision Tools add-on: Divide into Dashes, #4950) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Document Tools add-on: Save a Timestamped Copy, #4926) · **Target:** Adobe Illustrator 2026 (30.x) ## Feature submission evidence @@ -330,6 +330,7 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po | Outline view keeps template layers in Preview, dimmed | done | #1486; a picture placed to be traced stays visible under the outlines | | Arrange Tools add-on (VectorCraft addition): Object › Plug-ins › Arrange Tools runs Arrange in Grid… (grid by row, one row or one column; columns, H and V spacing, Center on Artboard; Preview), Swap Objects, Swap Objects and Sizes, Replace with Top Object… (Actual Size, Bounds or Bounds (Fit Proportionally); Keep Original; Scale Strokes & Effects; Preview), Set Size of Each… (Width and Height each ticked on or off, Constrain Width and Height Proportions with one side, the reference point, Scale Strokes & Effects; Preview) and Zoom to Selection, each one undo step | done | `addon.arrange.*`, `crates/engine/src/cmd/addons/arrange.rs`, `crates/ui-egui/src/addons/arrange.rs` and their tests; not a feature of the reference app, so it is not scored | | Arrange Tools add-on (VectorCraft addition): Object › Plug-ins › Arrange Tools › Group Overlapping Objects groups the selected objects whose visual bounds overlap or touch (chains included, within one layer or group), each group at its frontmost member's stacking place, one undo step | done | #4928; `addon.arrange.groupOverlapping`, `crates/engine/src/cmd/addons/arrange.rs` and its tests; not a feature of the reference app, so it is not scored | +| Document Tools add-on (VectorCraft addition): Object › Plug-ins › Document Tools › Save a Timestamped Copy writes `_YYYY-MM-DD_HH-MM.` next to the document (a folder picked for one never saved), which keeps its path, title and modified state; off on the web | done | #4926; `addon.document.saveTimestampedCopy`, `crates/engine/src/cmd/addons/document.rs`, `crates/ui-egui/src/addons/document.rs` and their tests; not a feature of the reference app, so it is not scored | | Smart Pen add-on (VectorCraft addition): Object › Plug-ins › Smart Pen selects the Smart Pen Tool, which the All Tools drawer lists under its own heading but neither toolbar shows as shipped (no shortcut), and opens the Smart Pen panel (next segment straight or curved; Remove Points Keep Shape, Make Smooth, Make Corner, Retract Handles, Close or Join). The tool draws (click: straight or curved segment, a curve after a straight one tangent; drag: smooth anchor, Alt breaks the handle, Shift 45°, Space moves it; Tab switches a segment; rubber band; close, join, continue) and edits without switching tools (click selects a point, drag moves it, Alt-click removes it keeping the shape, Shift+Alt-click deletes it, double-click toggles smooth/corner; handle drags with Shift and Alt; segment drags reshape, Alt-click adds a point) | done | `addon.pen.*`, `crates/tools/src/addons/smart_pen.rs`, `crates/ui-egui/src/addons/pen.rs` and their tests; not a feature of the reference app, so it is not scored | | Path Precision Tools add-on (VectorCraft addition): Object › Plug-ins › Path Precision Tools selects its seven tools (Anchor Eraser Brush, Extend Path, Measure & Inspect, Parametric Shape, Corner Tool, Slide Anchor Along Path, Shared Edge Merge Tool), which the All Tools drawer lists under their own heading but neither toolbar shows as shipped, opens the Anchor Inspector panel (the selected anchor's X/Y and handle lengths and angles as fields; Make Smooth, Make Corner, Equalize Handles, Remove Points Keep Shape, Add Points, the three ways to close) and the Parametric Shape panel (kind, count, W/H, rotation, the kind's own values and corner radius of the selected parametric shape, or of the tool's next one), and runs Add Points at / Split at Crossings and Convert to Parametric Shape; the Parametric Shape Tool draws ten re-editable kinds from the centre (↑/↓ and ←/→ change their numbers while dragging) and edits a selected one with handles; the Corner Tool cuts editable corners on any path (hover shows the anchor, a click uses its radius and kind, a drag sizes the corner, Shift whole values; a corner's marker selects it and loads its values, drags to resize, to 0 or double-clicked removes it; never past what fits; Alt-marquee cuts every anchor inside) with a Corners section in the Anchor Inspector (radius, kind, selected count, Apply to Selected Anchors); Slide Anchor Along Path drags an anchor along its curve with the segments beside it refitted, Shift snapping where the tangent is a multiple of 45°; the Shared Edge Merge Tool shows, over two paths touching at two coincident anchors, the two stretches and the edge merging them, a click merges them (half way; Shift the upper path, Alt the lower), and near anchors meet or an anchor one path lacks is added to match (Shift+Alt asks for this inside a stretch), with a Shared Edges section in the Anchor Inspector (brush diameter, coincidence 0 to 3 pt, Keep Shared Points Together, which takes coincident anchors along with the add-on's own anchor moves); a double-click on the brush or Extend Path tool opens its options | done | `addon.pathtools.*`, `crates/tools/src/addons/`, `crates/ui-egui/src/addons/pathtools.rs`, `crates/ui-egui/src/addons/pathtools/` and their tests; not a feature of the reference app, so it is not scored | | Path Precision Tools add-on (VectorCraft addition): Object › Plug-ins › Path Precision Tools › Divide into Dashes… (Dash and Gap in any unit, Fit to Corners and Ends, Remove Gaps; Preview) cuts the selected paths, or only their direct-selected segments, into open pieces along the curve, keeping their appearance, one undo step | done | #4950; `addon.pathtools.divideDashes`, `crates/pathops/src/addons/pathtools/dashes.rs`, `crates/ui-egui/src/addons/pathtools/dashes.rs` and their tests; not a feature of the reference app, so it is not scored | @@ -383,6 +384,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development | Date | Change | Summary | |---|---|---| +| 2026-11-01 | minor | Document Tools add-on (a VectorCraft addition): Save a Timestamped Copy (#4926); scores and estimates unchanged | | 2026-11-01 | minor | Path Precision Tools add-on (a VectorCraft addition): Divide into Dashes (#4950); scores and estimates unchanged | | 2026-11-01 | minor | Arrange Tools add-on (a VectorCraft addition): Group Overlapping Objects (#4928); scores and estimates unchanged | | 2026-11-01 | minor | Character panel › Language: the selected characters' language, from Document Setup's list, which gains Hindi, Marathi, Gujarati, Bengali, Punjabi, Tamil, Telugu, Kannada, Malayalam, Odia and Urdu (#4946, #4943); the Digits menu's numeral names keep their own translations; scores unchanged | From 68391bdde4ffbd06feaa0f8d98740203bef6cf19 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sun, 1 Nov 2026 02:42:20 +0700 Subject: [PATCH 4/4] Fix #4926: New Symbols from Layers --- ROADMAP.md | 4 +- crates/engine/src/cmd/addons/document.rs | 119 ++++++++++++++++-- .../engine/src/cmd/addons/document/tests.rs | 102 ++++++++++++++- crates/engine/src/cmd/brushsym.rs | 2 +- crates/engine/src/cmd/brushsym/symbols.rs | 34 +++-- crates/engine/tests/command_sweep.rs | 3 + crates/ui-egui/src/addons/document.rs | 15 ++- crates/ui-egui/src/addons/document/tests.rs | 21 ++++ crates/ui-egui/src/i18n/bg.tsv | 4 + crates/ui-egui/src/i18n/bs.tsv | 4 + crates/ui-egui/src/i18n/ca.tsv | 4 + crates/ui-egui/src/i18n/cs.tsv | 4 + crates/ui-egui/src/i18n/da.tsv | 4 + crates/ui-egui/src/i18n/de.tsv | 4 + crates/ui-egui/src/i18n/el.tsv | 4 + crates/ui-egui/src/i18n/es.tsv | 4 + crates/ui-egui/src/i18n/fi.tsv | 4 + crates/ui-egui/src/i18n/fr.tsv | 4 + crates/ui-egui/src/i18n/hu.tsv | 4 + crates/ui-egui/src/i18n/id.tsv | 4 + crates/ui-egui/src/i18n/it.tsv | 4 + crates/ui-egui/src/i18n/ja.tsv | 4 + crates/ui-egui/src/i18n/ka.tsv | 4 + crates/ui-egui/src/i18n/ko.tsv | 4 + crates/ui-egui/src/i18n/nl.tsv | 4 + crates/ui-egui/src/i18n/pl.tsv | 4 + crates/ui-egui/src/i18n/pt-br.tsv | 4 + crates/ui-egui/src/i18n/ru.tsv | 4 + crates/ui-egui/src/i18n/sv.tsv | 4 + crates/ui-egui/src/i18n/tr.tsv | 4 + crates/ui-egui/src/i18n/uk.tsv | 4 + crates/ui-egui/src/i18n/vi.tsv | 4 + crates/ui-egui/src/i18n/zh-hans.tsv | 4 + crates/ui-egui/src/i18n/zh-hant.tsv | 4 + docs/mcp.md | 12 ++ docs/target-app-parity.md | 7 +- docs/ui-parity.md | 4 +- 37 files changed, 393 insertions(+), 34 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index d61922cbe..0206168ac 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -9,7 +9,7 @@ **Stage: alpha** · next: beta, ~13 points (ready for real work ~62% → ~75%, and reliable `.ai` exchange) and ~170–270 h away -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Progress log row for the Document Tools add-on's Save a Timestamped Copy; headline numbers unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Progress log row for the Document Tools add-on's New Symbols from Layers; headline numbers unchanged) · **Target:** Adobe Illustrator 2026 (30.x) VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adobe Illustrator workflow. It runs on macOS, Windows, Linux, FreeBSD and the web (WASM), and agents can drive all of it over MCP, the CLI and a JSON @@ -129,6 +129,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap ## Progress log +- 2026-11-01: New Symbols from Layers joins the Document Tools add-on (Object › Plug-ins › Document Tools): one symbol of each top-level layer's art (the layers highlighted in the Layers panel when two or more are, else every layer), named after the layer, made as New Symbol makes one; Convert Layers to Symbols also puts an instance of each in place of its layer's art. Empty layers are skipped and reported; one undo step. A VectorCraft addition: headline numbers unchanged. - 2026-11-01: Document Tools, a built-in add-on (Object › Plug-ins › Document Tools), starts with Save a Timestamped Copy: a copy of the document named `_YYYY-MM-DD_HH-MM` (UTC; `-2`, `-3`… when that name is taken) goes next to it, or into a folder picked for a document never saved, written as File › Save a Copy writes it, so the document keeps its path, title and modified state. Off on the web, which has no clock. A VectorCraft addition: headline numbers unchanged. - 2026-11-01: Divide into Dashes joins the Path Precision Tools add-on (Object › Plug-ins › Path Precision Tools): the selected paths, or only their direct-selected segments, are cut into separate open paths of a dash length measured along the curve, with an optional gap (left empty, or kept as pieces), each keeping the path's appearance; Fit to Corners and Ends stretches or squeezes the lengths so every run starts and ends on a dash. A dialog with Preview, one undo step, at most 100000 pieces. A VectorCraft addition: headline numbers unchanged. - 2026-11-01: Group Overlapping Objects joins the Arrange Tools add-on (Object › Plug-ins › Arrange Tools): the selected objects whose visual bounds overlap or touch, chains included, go into one group each, within their own layer or group, at the stacking place of their frontmost member; objects that overlap nothing stay as they are. One undo step, a sweep along x so thousands of loose pieces group quickly (at most 20000 objects). A VectorCraft addition: headline numbers unchanged. @@ -207,6 +208,7 @@ The full earlier record of what landed is kept in [docs/roadmap.md](docs/roadmap | Date | Change | Summary | |---|---|---| +| 2026-11-01 | minor | Progress log row for the Document Tools add-on's New Symbols from Layers (#4927); headline numbers unchanged. | | 2026-11-01 | minor | Progress log row for the Document Tools add-on's Save a Timestamped Copy (#4926); headline numbers unchanged. | | 2026-11-01 | minor | Progress log row for Divide into Dashes (Path Precision Tools add-on, #4950); headline numbers unchanged. | | 2026-11-01 | minor | Progress log row for Group Overlapping Objects (Arrange Tools add-on, #4928); headline numbers unchanged. | diff --git a/crates/engine/src/cmd/addons/document.rs b/crates/engine/src/cmd/addons/document.rs index b4911e475..38aa8fab3 100644 --- a/crates/engine/src/cmd/addons/document.rs +++ b/crates/engine/src/cmd/addons/document.rs @@ -1,29 +1,45 @@ //! Document Tools (`addon.document.*`), a built-in add-on: Save a Timestamped Copy writes a copy of -//! the document named with the date and time through File › Save a Copy's save. Its submenu -//! (Object › Plug-ins › Document Tools) is the UI's. +//! the document named with the date and time through File › Save a Copy's save; New Symbols from +//! Layers makes a symbol of each layer's art as New Symbol does. Its submenu (Object › Plug-ins › +//! Document Tools) is the UI's. #[cfg(test)] mod tests; -use serde_json::{Value, json}; +use std::sync::Arc; +use serde_json::{Value, json}; +use vectorcraft_doc::{Node, NodeId, NodeKind, SymbolRegistration}; + +use super::super::brushsym::{instance_in_place, normalize, place_xf, store_symbol}; use super::super::fileio::{self, SaveMode}; -use super::super::{CommandSpec, bad, clock_date, cmd, date_param, has_doc, str_param}; +use super::super::{CommandSpec, bad, checked_ids_param, clock_date, cmd, date_param, has_doc, str_param, switch_or}; use crate::{Result, Session}; /// The most numbers (`-2`, `-3`…) tried after a timestamped name that is taken. const MAX_NUMBER: u32 = 999; pub fn specs() -> Vec { - vec![cmd!( - "addon.document.saveTimestampedCopy", - "Save a Timestamped Copy", - ["Object", "Plug-ins", "Document Tools"], - None, - "{folder? (default: the document's own folder; needed for a document never saved), format?: vectorcraft|pdf|svg|svgz|ai|eps (default: the document's own, else vectorcraft), time?: Unix seconds (default: now)} write a copy of the document named _YYYY-MM-DD_HH-MM. (the date and time in UTC; -2, -3… added when that name is taken) as File › Save a Copy writes it: the document keeps its path, title and modified state, and no undo step is made. Off where there is no clock (the web) → {path, format, bytes, warnings}", - has_clock, - save_timestamped_copy - )] + vec![ + cmd!( + "addon.document.saveTimestampedCopy", + "Save a Timestamped Copy", + ["Object", "Plug-ins", "Document Tools"], + None, + "{folder? (default: the document's own folder; needed for a document never saved), format?: vectorcraft|pdf|svg|svgz|ai|eps (default: the document's own, else vectorcraft), time?: Unix seconds (default: now)} write a copy of the document named _YYYY-MM-DD_HH-MM. (the date and time in UTC; -2, -3… added when that name is taken) as File › Save a Copy writes it: the document keeps its path, title and modified state, and no undo step is made. Off where there is no clock (the web) → {path, format, bytes, warnings}", + has_clock, + save_timestamped_copy + ), + cmd!( + "addon.document.symbolsFromLayers", + "New Symbols from Layers", + ["Object", "Plug-ins", "Document Tools"], + None, + "{layers?: [layer id…] (top-level layers; default: those highlighted in the Layers panel when two or more are, else every one), replace?: bool (false: the art stays; true: each layer's art is replaced by an instance of its symbol in the same place)} make one symbol of each layer's art as symbol.new makes one (registration in the centre), named after the layer (with a number added when a symbol has that name), as one undo step. Layers with no art are skipped, and with replace so are locked layers; with replace the instances are selected → {symbols: [name…], ids: [instance id…] (with replace), skipped: [{layer, name, reason: \"empty\"|\"locked\"}…]}", + has_doc, + symbols_from_layers + ), + ] } /// A document is open and the time can be told. @@ -71,3 +87,80 @@ fn save_timestamped_copy(s: &mut Session, p: &Value) -> Result { .ok_or_else(|| bad(C, "every numbered name for the copy is taken"))?; fileio::save_document(s, SaveMode::Copy, &json!({ "path": path, "format": format.id })) } + +/// `n` with each layer in it, and itself, made a group of the same art and clipping (a symbol holds +/// no layers). +fn without_layers(n: &Node) -> Node { + let mut n = n.clone(); + if let NodeKind::Layer { children, clip, .. } = &n.kind { + n.kind = NodeKind::Group { children: children.clone(), clip: *clip }; + } + if let Some(children) = n.children_mut() { + for c in children.iter_mut().filter(|c| matches!(c.kind, NodeKind::Layer { .. })) { + *c = Arc::new(without_layers(c)); + } + } + n +} + +fn symbols_from_layers(s: &mut Session, p: &Value) -> Result { + const C: &str = "addon.document.symbolsFromLayers"; + let replace = switch_or(p, "replace", false, C)?; + let st = s.doc()?; + let d = &st.doc; + let layers: Vec = match p.get("layers").filter(|v| !v.is_null()) { + // The layers highlighted in the Layers panel when two or more are (one is mostly just the + // current layer), else every one. + None => { + let rows = st.highlighted_rows(); + let picked: Vec = d.layers.iter().map(|l| l.id).filter(|id| rows.contains(id)).collect(); + if picked.len() > 1 { picked } else { d.layers.iter().map(|l| l.id).collect() } + } + Some(_) => { + let ids = checked_ids_param(s, p, "layers", C)?; + if let Some(NodeId(id)) = ids.iter().find(|id| !d.layers.iter().any(|l| l.id == **id)) { + return Err(bad(C, format!("{id} is no top-level layer"))); + } + d.layers.iter().map(|l| l.id).filter(|id| ids.contains(id)).collect() + } + }; + // Each layer with art: its id and name, the art's roots, and the art ready for a symbol. + let (mut made, mut skipped) = (vec![], vec![]); + for layer in layers.iter().filter_map(|id| d.layers.iter().find(|l| l.id == *id)) { + let name = layer.display_name(); + let roots: Vec = layer.children().map(|c| c.iter().map(|n| n.id).collect()).unwrap_or_default(); + let art = match layer.children().map(Vec::as_slice).unwrap_or_default() { + [] => None, + [one] => Some(without_layers(one)), + many => Some(without_layers(&Node::group(NodeId(0), many.to_vec()))), + }; + let reason = if replace && layer.locked { Some("locked") } else { None }; + let ready = art.and_then(|mut art| { + crate::cmd::threads::keep_shown_text(d, &mut art); + normalize(art).ok() + }); + match (reason, ready) { + (None, Some(ready)) => made.push((name, roots, ready)), + (reason, _) => skipped.push(json!({ "layer": layer.id.0, "name": name, "reason": reason.unwrap_or("empty") })), + } + } + if made.is_empty() { + return Ok(json!({ "symbols": [], "ids": [], "skipped": skipped })); + } + let (names, ids) = s.edit("New Symbols from Layers", |d, sel| { + let (mut names, mut ids) = (vec![], vec![]); + for (name, roots, (art, size, center)) in &made { + let name = d.unique_symbol_name(name); + store_symbol(d, art, &name, *size); + if replace { + ids.push(instance_in_place(d, roots, &name, place_xf(*center, *size, SymbolRegistration::CENTER))?); + } + names.push(name); + } + if replace { + sel.set(ids.iter().copied()); + } + Ok((names, ids)) + })?; + Ok(json!({ "symbols": names, "ids": ids.iter().map(|i| i.0).collect::>(), "skipped": skipped })) +} diff --git a/crates/engine/src/cmd/addons/document/tests.rs b/crates/engine/src/cmd/addons/document/tests.rs index 362ea1a43..a67fcd5ae 100644 --- a/crates/engine/src/cmd/addons/document/tests.rs +++ b/crates/engine/src/cmd/addons/document/tests.rs @@ -1,9 +1,10 @@ -//! Document Tools' commands: the copy's name and place, the document left as it was, and bad -//! params refused. +//! Document Tools' commands: the timestamped copy's name and place with the document left as it +//! was, symbols made of layers, and bad params refused. use std::path::{Path, PathBuf}; use serde_json::{Value, json}; +use vectorcraft_doc::{NodeId, NodeKind}; use crate::Session; @@ -96,3 +97,100 @@ fn bad_params_are_refused_and_writes_stay_inside_the_automation_roots() { fn enabled_without_document() -> bool { Session::new().commands().into_iter().find(|c| c.id == "addon.document.saveTimestampedCopy").unwrap().enabled } + +fn layer_ids(s: &Session) -> Vec { + s.doc().unwrap().doc.layers.iter().map(|l| l.id.0).collect() +} + +fn symbol_names(s: &Session) -> Vec { + s.doc().unwrap().doc.symbols.iter().map(|x| x.name.clone()).collect() +} + +/// A document of three layers: `Layer 1` with two rectangles, an empty one, and `Icons` with one +/// ellipse. +fn layered() -> Session { + let mut s = session(); + s.execute("shape.rectangle", &json!({"x": 200, "y": 10, "width": 50, "height": 50})).unwrap(); + s.execute("layer.new", &json!({})).unwrap(); + s.execute("layer.new", &json!({"name": "Icons"})).unwrap(); + s.execute("shape.ellipse", &json!({"x": 0, "y": 100, "width": 40, "height": 20})).unwrap(); + s +} + +#[test] +fn a_symbol_is_made_of_each_layer_and_empty_layers_are_reported() { + let mut s = layered(); + let before = symbol_names(&s).len(); + let n = s.doc().unwrap().history.undo.len(); + let r = s.execute("addon.document.symbolsFromLayers", &json!({})).unwrap(); + assert_eq!(s.doc().unwrap().history.undo.len(), n + 1, "one undo step"); + let names: Vec = serde_json::from_value(r["symbols"].clone()).unwrap(); + assert_eq!(names.len(), 2, "{r}"); + assert!(names.contains(&"Icons".to_string())); + assert_eq!(symbol_names(&s).len(), before + 2); + assert_eq!(r["skipped"].as_array().unwrap().len(), 1, "the empty layer: {r}"); + assert_eq!(r["skipped"][0]["reason"], json!("empty")); + assert_eq!(r["ids"], json!([]), "without replace the art stays"); + assert_eq!(s.doc().unwrap().doc.layers[0].children().unwrap().len(), 2); + // Again: the names are taken, so they get numbers. + let r = s.execute("addon.document.symbolsFromLayers", &json!({})).unwrap(); + assert!(r["symbols"].as_array().unwrap().contains(&json!("Icons 2")), "{r}"); + s.execute("edit.undo", &json!({})).unwrap(); + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(symbol_names(&s).len(), before); +} + +#[test] +fn replace_puts_an_instance_where_the_layers_art_was() { + let mut s = layered(); + let first = layer_ids(&s)[0]; + let bounds = |s: &Session, id: NodeId| s.doc().unwrap().doc.bounds_of(&[id], false).unwrap(); + let layer = NodeId(first); + let art = bounds(&s, layer); + let r = s.execute("addon.document.symbolsFromLayers", &json!({"layers": [first], "replace": true})).unwrap(); + let ids = r["ids"].as_array().unwrap(); + assert_eq!(ids.len(), 1); + let inst = NodeId(ids[0].as_u64().unwrap()); + let st = s.doc().unwrap(); + let kids = st.doc.layers[0].children().unwrap(); + assert_eq!(kids.len(), 1); + assert!(matches!(&kids[0].kind, NodeKind::SymbolInstance { .. })); + assert_eq!(st.selection.objects, [inst]); + let now = bounds(&s, layer); + assert!((now.x0 - art.x0).abs() < 1e-6 && (now.y1 - art.y1).abs() < 1e-6 && (now.width() - art.width()).abs() < 1e-6, "{art:?} {now:?}"); + assert_eq!(s.doc().unwrap().doc.layers.len(), 3, "the other layers untouched"); +} + +#[test] +fn locked_layers_keep_their_art_and_bad_layers_are_refused() { + let mut s = layered(); + let ids = layer_ids(&s); + s.execute("layer.setProps", &json!({"id": ids[2], "locked": true})).unwrap(); + let r = s.execute("addon.document.symbolsFromLayers", &json!({"replace": true})).unwrap(); + let skipped: Vec<&str> = r["skipped"].as_array().unwrap().iter().map(|k| k["reason"].as_str().unwrap()).collect(); + assert_eq!(skipped, ["empty", "locked"]); + let before = (s.doc().unwrap().doc.clone(), s.doc().unwrap().history.undo.len()); + let rect = s.doc().unwrap().doc.layers[0].children().unwrap()[0].id.0; + for p in [json!({"layers": [rect]}), json!({"layers": [u64::MAX]}), json!({"layers": "all"}), json!({"layers": [-1]}), json!({"replace": "yes"})] + { + assert!(s.execute("addon.document.symbolsFromLayers", &p).is_err(), "{p}"); + } + assert!(*s.doc().unwrap().doc == *before.0 && s.doc().unwrap().history.undo.len() == before.1); + // Only an empty layer: nothing made, no step. + let r = s.execute("addon.document.symbolsFromLayers", &json!({"layers": [ids[1]]})).unwrap(); + assert_eq!((r["symbols"].clone(), s.doc().unwrap().history.undo.len()), (json!([]), before.1)); +} + +#[test] +fn the_layers_highlighted_in_the_layers_panel_are_the_default() { + let mut s = layered(); + let ids = layer_ids(&s); + // One highlighted row is just the current layer: every layer. + s.execute("layer.setCurrent", &json!({"id": ids[0]})).unwrap(); + let r = s.execute("addon.document.symbolsFromLayers", &json!({})).unwrap(); + assert_eq!(r["symbols"].as_array().unwrap().len(), 2); + s.execute("edit.undo", &json!({})).unwrap(); + s.execute("layer.highlight", &json!({"ids": [ids[1], ids[2]]})).unwrap(); + let r = s.execute("addon.document.symbolsFromLayers", &json!({})).unwrap(); + assert_eq!((r["symbols"].clone(), r["skipped"].as_array().unwrap().len()), (json!(["Icons"]), 1)); +} diff --git a/crates/engine/src/cmd/brushsym.rs b/crates/engine/src/cmd/brushsym.rs index 3b9a476d4..2124667ce 100644 --- a/crates/engine/src/cmd/brushsym.rs +++ b/crates/engine/src/cmd/brushsym.rs @@ -32,7 +32,7 @@ use brushes::{ brush_apply, brush_delete, brush_duplicate, brush_freehand, brush_get, brush_list, brush_move, brush_new, brush_options, brush_remove, brush_selection_art, brush_set_current, brush_unused, expand_brush, }; -pub(crate) use symbols::{natural_size, normalize, place_xf, set_natural_size}; +pub(crate) use symbols::{instance_in_place, natural_size, normalize, place_xf, set_natural_size, store_symbol}; use symbols::{ symbol_adjust, symbol_break_link, symbol_delete, symbol_duplicate, symbol_edit, symbol_list, symbol_new, symbol_options, symbol_place, symbol_replace, symbol_select_instances, symbol_set_current, symbol_spray, symbol_update, diff --git a/crates/engine/src/cmd/brushsym/symbols.rs b/crates/engine/src/cmd/brushsym/symbols.rs index 8e8ba163e..d1ce429a0 100644 --- a/crates/engine/src/cmd/brushsym/symbols.rs +++ b/crates/engine/src/cmd/brushsym/symbols.rs @@ -134,6 +134,25 @@ pub(super) fn symbol_list(s: &mut Session, _: &Value) -> Result { Ok(json!({ "symbols": list, "current": current_symbol(d).or_else(|| symbol_names(d).into_iter().next()) })) } +/// Store `art` ([`normalize`]d, of natural size `size`) as symbol `name`, which becomes the +/// current symbol. +pub(crate) fn store_symbol(d: &mut Document, art: &Node, name: &str, size: (f64, f64)) { + let art = d.reid(art); + d.symbols.push(Symbol { name: name.into(), art: Arc::new(art) }); + set_natural_size(d, name, Some(size)); + d.unknown.insert(CURRENT_SYMBOL.into(), json!(name)); +} + +/// Replace the root nodes `ids` with one instance of symbol `name` (already stored) placed by +/// `xf`. Frames of threaded text among them leave their threads with the text they show. Returns +/// the instance's id. +pub(crate) fn instance_in_place(d: &mut Document, ids: &[NodeId], name: &str, xf: Affine) -> Result { + let threaded = crate::cmd::threads::frames(d); + let id = replace_with_instance(d, ids, name, xf)?; + crate::cmd::threads::leave_threads(d, &threaded); + Ok(id) +} + /// Replace the root nodes `ids` with one instance of `name` (art already stored). Returns the id. fn replace_with_instance(d: &mut Document, ids: &[NodeId], name: &str, xf: Affine) -> Result { let top = *ids.last().ok_or_else(|| EngineError::Other("nothing selected".into()))?; @@ -163,17 +182,11 @@ pub(super) fn symbol_new(s: &mut Session, p: &Value) -> Result { // Keep the art where it was: the geometric centre stays at `c`, whatever the registration is. let place = place_xf(c + reg.offset_from_center(size.0, size.1), size, reg); let id = s.edit("New Symbol", |d, sel| { - let art = d.reid(&art); - d.symbols.push(Symbol { name: name.clone(), art: Arc::new(art) }); - set_natural_size(d, &name, Some(size)); + store_symbol(d, &art, &name, size); d.set_symbol_registration(&name, reg); d.set_symbol_export_type(&name, export); d.set_symbol_nine_slice(&name, nine); - d.unknown.insert(CURRENT_SYMBOL.into(), json!(name)); - let threaded = crate::cmd::threads::frames(d); - let id = replace_with_instance(d, &ids, &name, place)?; - // Frames of threaded text in the symbol leave their threads with the text they show. - crate::cmd::threads::leave_threads(d, &threaded); + let id = instance_in_place(d, &ids, &name, place)?; sel.set([id]); Ok(id) })?; @@ -370,10 +383,7 @@ pub(super) fn symbol_update(s: &mut Session, p: &Value) -> Result { } } let reg = d.symbol_registration(&name); - let threaded = crate::cmd::threads::frames(d); - let id = replace_with_instance(d, &ids, &name, place_xf(c + reg.offset_from_center(size.0, size.1), size, reg))?; - // Frames of threaded text in the symbol leave their threads with the text they show. - crate::cmd::threads::leave_threads(d, &threaded); + let id = instance_in_place(d, &ids, &name, place_xf(c + reg.offset_from_center(size.0, size.1), size, reg))?; sel.set([id]); Ok(id) })?; diff --git a/crates/engine/tests/command_sweep.rs b/crates/engine/tests/command_sweep.rs index c53f51b2a..ae69993f7 100644 --- a/crates/engine/tests/command_sweep.rs +++ b/crates/engine/tests/command_sweep.rs @@ -595,6 +595,9 @@ fn structured_junk() { // Refused before anything is written: no format of that name, a time that is no time. ("addon.document.saveTimestampedCopy", json!({"format": "png", "folder": 7})), ("addon.document.saveTimestampedCopy", json!({"format": "svg", "time": [1e308], "folder": null})), + ("addon.document.symbolsFromLayers", json!({"layers": "x", "replace": 3})), + ("addon.document.symbolsFromLayers", json!({"layers": [1e308, -1, null, u64::MAX], "replace": true})), + ("addon.document.symbolsFromLayers", json!({"layers": [], "replace": true})), // Smart Pen add-on. ("addon.pen.append", json!({"id": 2, "x": 1e308, "y": 0, "in": [1e308, -1e308]})), ("addon.pen.append", json!({"id": 2, "subpath": u64::MAX, "x": 0, "y": 0, "segment": "curved"})), diff --git a/crates/ui-egui/src/addons/document.rs b/crates/ui-egui/src/addons/document.rs index a94874969..47ea442f4 100644 --- a/crates/ui-egui/src/addons/document.rs +++ b/crates/ui-egui/src/addons/document.rs @@ -1,5 +1,7 @@ -//! Document Tools in the interface: its submenu (Object › Plug-ins › Document Tools). Save a -//! Timestamped Copy of a document never saved asks for the folder to put it in ([`save_copy`]). +//! Document Tools in the interface: its submenu (Object › Plug-ins › Document Tools), which runs +//! Save a Timestamped Copy, New Symbols from Layers and Convert Layers to Symbols (the same, with +//! each layer's art replaced by an instance). Save a Timestamped Copy of a document never saved +//! asks for the folder to put it in ([`save_copy`]). use serde_json::{Value, json}; @@ -8,10 +10,17 @@ use crate::menus::Item; /// Save a Timestamped Copy. pub const SAVE_COPY: &str = "addon.document.saveTimestampedCopy"; +/// New Symbols from Layers (Convert Layers to Symbols: with their art replaced by instances). +const SYMBOLS: &str = "addon.document.symbolsFromLayers"; /// Object › Plug-ins › Document Tools. pub fn menu() -> Vec { - vec![Item::Cmd("Save a Timestamped Copy", SAVE_COPY, json!({}))] + vec![ + Item::Cmd("Save a Timestamped Copy", SAVE_COPY, json!({})), + Item::Sep, + Item::Cmd("New Symbols from Layers", SYMBOLS, json!({})), + Item::Cmd("Convert Layers to Symbols", SYMBOLS, json!({ "replace": true })), + ] } /// Save a Timestamped Copy with params `p`: a document never saved, given no `folder`, goes in the diff --git a/crates/ui-egui/src/addons/document/tests.rs b/crates/ui-egui/src/addons/document/tests.rs index cf43d59e1..5221c8414 100644 --- a/crates/ui-egui/src/addons/document/tests.rs +++ b/crates/ui-egui/src/addons/document/tests.rs @@ -61,3 +61,24 @@ fn an_unsaved_document_asks_for_a_folder() { assert_eq!(files(&dir).len(), 1); let _ = std::fs::remove_dir_all(&dir); } + +#[test] +fn new_symbols_from_layers_and_convert_layers_to_symbols() { + let mut app = app(); + app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 30})).unwrap(); + app.run("layer.new", json!({"name": "Badges"})).unwrap(); + app.run("shape.ellipse", json!({"x": 100, "y": 10, "width": 40, "height": 40})).unwrap(); + let symbols = |app: &VectorcraftApp| app.session.doc().unwrap().doc.symbols.iter().map(|s| s.name.clone()).collect::>(); + let before = symbols(&app).len(); + click(&mut app, "New Symbols from Layers"); + assert_eq!(symbols(&app).len(), before + 2); + assert!(symbols(&app).contains(&"Badges".to_string())); + app.run("edit.undo", json!({})).unwrap(); + click(&mut app, "Convert Layers to Symbols"); + let doc = &app.session.doc().unwrap().doc; + assert!( + doc.layers + .iter() + .all(|l| matches!(l.children().unwrap()[..], [ref one] if matches!(one.kind, vectorcraft_doc::NodeKind::SymbolInstance { .. }))) + ); +} diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 519247e09..177f87058 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -5945,6 +5945,10 @@ breakText Glyphs Глифи @msg there is no clock here: give `time` (Unix seconds) тук няма часовник: задайте `time` (Unix секунди) @msg the document has never been saved: give `folder` документът никога не е запазван: задайте `folder` @msg every numbered name for the copy is taken всички номерирани имена за копието са заети +# Document Tools add-on: New Symbols from Layers and Convert Layers to Symbols. + New Symbols from Layers Нови символи от слоевете + Convert Layers to Symbols Превръщане на слоевете в символи +@msg {id} is no top-level layer {id} не е слой от най-горно ниво Marathi Маратхи Gujarati Гуджарати Bengali Бенгалски diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 5910e5904..1cdf807f0 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -5979,6 +5979,10 @@ shape Open Otvoren @msg there is no clock here: give `time` (Unix seconds) ovdje nema sata: navedite `time` (Unix sekunde) @msg the document has never been saved: give `folder` dokument nikad nije sačuvan: navedite `folder` @msg every numbered name for the copy is taken sva numerisana imena za kopiju su zauzeta +# Document Tools add-on: New Symbols from Layers and Convert Layers to Symbols. + New Symbols from Layers Novi simboli iz slojeva + Convert Layers to Symbols Pretvori slojeve u simbole +@msg {id} is no top-level layer {id} nije sloj najvišeg nivoa Marathi Marathi Gujarati Gudžarati Bengali Bengalski diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 6c2ccc29a..c2c363ebd 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -6052,6 +6052,10 @@ breakText Glyphs Glifs @msg there is no clock here: give `time` (Unix seconds) aquí no hi ha rellotge: indiqueu `time` (segons Unix) @msg the document has never been saved: give `folder` el document no s’ha desat mai: indiqueu `folder` @msg every numbered name for the copy is taken tots els noms numerats per a la còpia estan ocupats +# Document Tools add-on: New Symbols from Layers and Convert Layers to Symbols. + New Symbols from Layers Símbols nous a partir de les capes + Convert Layers to Symbols Converteix les capes en símbols +@msg {id} is no top-level layer {id} no és una capa de primer nivell Marathi Marathi Gujarati Gujarati Bengali Bengalí diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index cc0fbfa1b..42f1c3977 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -6062,6 +6062,10 @@ breakText Glyphs Glyfy @msg there is no clock here: give `time` (Unix seconds) nejsou k dispozici hodiny: zadejte `time` (sekundy Unix) @msg the document has never been saved: give `folder` dokument nebyl nikdy uložen: zadejte `folder` @msg every numbered name for the copy is taken všechny číslované názvy kopie jsou obsazené +# Document Tools add-on: New Symbols from Layers and Convert Layers to Symbols. + New Symbols from Layers Nové symboly z vrstev + Convert Layers to Symbols Převést vrstvy na symboly +@msg {id} is no top-level layer {id} není vrstva nejvyšší úrovně Marathi Maráthština Gujarati Gudžarátština Bengali Bengálština diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index e77983fcb..5b64d2758 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -5959,6 +5959,10 @@ breakText Glyphs Glyffer @msg there is no clock here: give `time` (Unix seconds) der er intet ur her: angiv `time` (Unix-sekunder) @msg the document has never been saved: give `folder` dokumentet er aldrig blevet gemt: angiv `folder` @msg every numbered name for the copy is taken alle nummererede navne til kopien er optaget +# Document Tools add-on: New Symbols from Layers and Convert Layers to Symbols. + New Symbols from Layers Nye symboler fra lag + Convert Layers to Symbols Konvertér lag til symboler +@msg {id} is no top-level layer {id} er ikke et lag på øverste niveau Marathi Marathi Gujarati Gujarati Bengali Bengali diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 5a3bf8ef5..32f8ee3f8 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -5647,6 +5647,10 @@ breakText Glyphs Glyphen @msg there is no clock here: give `time` (Unix seconds) hier gibt es keine Uhr: geben Sie `time` an (Unix-Sekunden) @msg the document has never been saved: give `folder` das Dokument wurde nie gespeichert: geben Sie `folder` an @msg every numbered name for the copy is taken alle nummerierten Namen für die Kopie sind vergeben +# Document Tools add-on: New Symbols from Layers and Convert Layers to Symbols. + New Symbols from Layers Neue Symbole aus Ebenen + Convert Layers to Symbols Ebenen in Symbole umwandeln +@msg {id} is no top-level layer {id} ist keine Ebene der obersten Stufe Marathi Marathi Gujarati Gujarati Bengali Bengalisch diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 686376775..2ce1d6b85 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -6014,6 +6014,10 @@ breakText Glyphs Γλύφοι @msg there is no clock here: give `time` (Unix seconds) δεν υπάρχει ρολόι εδώ: δώστε `time` (δευτερόλεπτα Unix) @msg the document has never been saved: give `folder` το έγγραφο δεν έχει αποθηκευτεί ποτέ: δώστε `folder` @msg every numbered name for the copy is taken όλα τα αριθμημένα ονόματα για το αντίγραφο είναι πιασμένα +# Document Tools add-on: New Symbols from Layers and Convert Layers to Symbols. + New Symbols from Layers Νέα σύμβολα από επίπεδα + Convert Layers to Symbols Μετατροπή επιπέδων σε σύμβολα +@msg {id} is no top-level layer το {id} δεν είναι επίπεδο ανώτατου επιπέδου Marathi Μαραθικά Gujarati Γκουγιαράτι Bengali Βεγγαλικά diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index e99c15f65..277af453f 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -6004,6 +6004,10 @@ breakText Glyphs Glifos @msg there is no clock here: give `time` (Unix seconds) aquí no hay reloj: indique `time` (segundos Unix) @msg the document has never been saved: give `folder` el documento nunca se ha guardado: indique `folder` @msg every numbered name for the copy is taken todos los nombres numerados para la copia están ocupados +# Document Tools add-on: New Symbols from Layers and Convert Layers to Symbols. + New Symbols from Layers Nuevos símbolos a partir de capas + Convert Layers to Symbols Convertir capas en símbolos +@msg {id} is no top-level layer {id} no es una capa de nivel superior Marathi Maratí Gujarati Guyaratí Bengali Bengalí diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 7a4f5e656..5ff14b7d9 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -6092,6 +6092,10 @@ breakText Glyphs Merkit @msg there is no clock here: give `time` (Unix seconds) täällä ei ole kelloa: anna `time` (Unix-sekunteina) @msg the document has never been saved: give `folder` asiakirjaa ei ole koskaan tallennettu: anna `folder` @msg every numbered name for the copy is taken kaikki kopion numeroidut nimet ovat varattuja +# Document Tools add-on: New Symbols from Layers and Convert Layers to Symbols. + New Symbols from Layers Uudet symbolit tasoista + Convert Layers to Symbols Muunna tasot symboleiksi +@msg {id} is no top-level layer {id} ei ole ylimmän tason taso Marathi Marathi Gujarati Gudžarati Bengali Bengali diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 81326659d..76717dd00 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -6059,6 +6059,10 @@ breakText Glyphs Glyphes @msg there is no clock here: give `time` (Unix seconds) il n’y a pas d’horloge ici : indiquez `time` (secondes Unix) @msg the document has never been saved: give `folder` le document n’a jamais été enregistré : indiquez `folder` @msg every numbered name for the copy is taken tous les noms numérotés pour la copie sont pris +# Document Tools add-on: New Symbols from Layers and Convert Layers to Symbols. + New Symbols from Layers Nouveaux symboles à partir des calques + Convert Layers to Symbols Convertir les calques en symboles +@msg {id} is no top-level layer {id} n’est pas un calque de premier niveau Marathi Marathi Gujarati Goudjarati Bengali Bengali diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 498ed78e7..c391405c4 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -6048,6 +6048,10 @@ breakText Glyphs Glifák @msg there is no clock here: give `time` (Unix seconds) itt nincs óra: adja meg a `time` értéket (Unix-másodperc) @msg the document has never been saved: give `folder` a dokumentum még nem volt mentve: adja meg a `folder` értéket @msg every numbered name for the copy is taken a másolat összes számozott neve foglalt +# Document Tools add-on: New Symbols from Layers and Convert Layers to Symbols. + New Symbols from Layers Új szimbólumok rétegekből + Convert Layers to Symbols Rétegek átalakítása szimbólumokká +@msg {id} is no top-level layer a(z) {id} nem legfelső szintű réteg Marathi Maráthi Gujarati Gudzsaráti Bengali Bengáli diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 197b69455..a49fcb3e3 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -5959,6 +5959,10 @@ breakText Glyphs Glif @msg there is no clock here: give `time` (Unix seconds) tidak ada jam di sini: berikan `time` (detik Unix) @msg the document has never been saved: give `folder` dokumen belum pernah disimpan: berikan `folder` @msg every numbered name for the copy is taken semua nama bernomor untuk salinan sudah dipakai +# Document Tools add-on: New Symbols from Layers and Convert Layers to Symbols. + New Symbols from Layers Simbol baru dari lapisan + Convert Layers to Symbols Ubah lapisan menjadi simbol +@msg {id} is no top-level layer {id} bukan lapisan tingkat atas Marathi Marathi Gujarati Gujarati Bengali Bengali diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 54d16513f..0816ef0c7 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -5681,6 +5681,10 @@ breakText Glyphs Glifi @msg there is no clock here: give `time` (Unix seconds) qui non c’è un orologio: indica `time` (secondi Unix) @msg the document has never been saved: give `folder` il documento non è mai stato salvato: indica `folder` @msg every numbered name for the copy is taken tutti i nomi numerati per la copia sono già usati +# Document Tools add-on: New Symbols from Layers and Convert Layers to Symbols. + New Symbols from Layers Nuovi simboli dai livelli + Convert Layers to Symbols Converti livelli in simboli +@msg {id} is no top-level layer {id} non è un livello di primo livello Marathi Marathi Gujarati Gujarati Bengali Bengalese diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index f94365ca3..795b48730 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -5748,6 +5748,10 @@ breakText Glyphs 字形 @msg there is no clock here: give `time` (Unix seconds) ここには時計がありません:`time`(Unix 秒)を指定してください @msg the document has never been saved: give `folder` ドキュメントは一度も保存されていません:`folder` を指定してください @msg every numbered name for the copy is taken コピー用の番号付きの名前はすべて使用されています +# Document Tools add-on: New Symbols from Layers and Convert Layers to Symbols. + New Symbols from Layers レイヤーから新規シンボル + Convert Layers to Symbols レイヤーをシンボルに変換 +@msg {id} is no top-level layer {id} は最上位のレイヤーではありません Marathi マラーティー語 Gujarati グジャラート語 Bengali ベンガル語 diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index e948b44ed..2eec1b66f 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -6051,6 +6051,10 @@ breakText Glyphs გლიფები @msg there is no clock here: give `time` (Unix seconds) აქ საათი არ არის: მიუთითეთ `time` (Unix წამები) @msg the document has never been saved: give `folder` დოკუმენტი არასოდეს შენახულა: მიუთითეთ `folder` @msg every numbered name for the copy is taken ასლის ყველა დანომრილი სახელი დაკავებულია +# Document Tools add-on: New Symbols from Layers and Convert Layers to Symbols. + New Symbols from Layers ახალი სიმბოლოები შრეებიდან + Convert Layers to Symbols შრეების სიმბოლოებად გარდაქმნა +@msg {id} is no top-level layer {id} არ არის ზედა დონის შრე Marathi მარათჰი Gujarati გუჯარათი Bengali ბენგალური diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index e3b4f2401..05f3a02a1 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -5644,6 +5644,10 @@ breakText Glyphs 글리프 @msg there is no clock here: give `time` (Unix seconds) 여기에는 시계가 없습니다. `time`(Unix 초)을 지정하십시오 @msg the document has never been saved: give `folder` 문서를 저장한 적이 없습니다. `folder`를 지정하십시오 @msg every numbered name for the copy is taken 사본에 쓸 번호 붙은 이름이 모두 사용 중입니다 +# Document Tools add-on: New Symbols from Layers and Convert Layers to Symbols. + New Symbols from Layers 레이어에서 새 심볼 만들기 + Convert Layers to Symbols 레이어를 심볼로 변환 +@msg {id} is no top-level layer {id}은(는) 최상위 레이어가 아닙니다 Marathi 마라티어 Gujarati 구자라트어 Bengali 벵골어 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index e68a49a8e..082877247 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -6003,6 +6003,10 @@ breakText Glyphs Glyphs @msg there is no clock here: give `time` (Unix seconds) er is hier geen klok: geef `time` op (Unix-seconden) @msg the document has never been saved: give `folder` het document is nooit opgeslagen: geef `folder` op @msg every numbered name for the copy is taken alle genummerde namen voor de kopie zijn bezet +# Document Tools add-on: New Symbols from Layers and Convert Layers to Symbols. + New Symbols from Layers Nieuwe symbolen van lagen + Convert Layers to Symbols Lagen omzetten in symbolen +@msg {id} is no top-level layer {id} is geen laag op het hoogste niveau Marathi Marathi Gujarati Gujarati Bengali Bengaals diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index aa7f7b878..38f394971 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -5917,6 +5917,10 @@ breakText Glyphs Glify @msg there is no clock here: give `time` (Unix seconds) brak tu zegara: podaj `time` (sekundy Unix) @msg the document has never been saved: give `folder` dokument nigdy nie został zapisany: podaj `folder` @msg every numbered name for the copy is taken wszystkie numerowane nazwy kopii są zajęte +# Document Tools add-on: New Symbols from Layers and Convert Layers to Symbols. + New Symbols from Layers Nowe symbole z warstw + Convert Layers to Symbols Konwertuj warstwy na symbole +@msg {id} is no top-level layer {id} nie jest warstwą najwyższego poziomu Marathi Marathi Gujarati Gudżarati Bengali Bengalski diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 3305725b5..ed4568edc 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -5981,6 +5981,10 @@ breakText Glyphs Glifos @msg there is no clock here: give `time` (Unix seconds) não há relógio aqui: informe `time` (segundos Unix) @msg the document has never been saved: give `folder` o documento nunca foi salvo: informe `folder` @msg every numbered name for the copy is taken todos os nomes numerados para a cópia estão em uso +# Document Tools add-on: New Symbols from Layers and Convert Layers to Symbols. + New Symbols from Layers Novos Símbolos a partir de Camadas + Convert Layers to Symbols Converter Camadas em Símbolos +@msg {id} is no top-level layer {id} não é uma camada de nível superior Marathi Marati Gujarati Guzerate Bengali Bengali diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index d27267088..a829cbf0d 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -6039,6 +6039,10 @@ breakText Glyphs Глифы @msg there is no clock here: give `time` (Unix seconds) здесь нет часов: укажите `time` (секунды Unix) @msg the document has never been saved: give `folder` документ ни разу не сохранялся: укажите `folder` @msg every numbered name for the copy is taken все нумерованные имена для копии заняты +# Document Tools add-on: New Symbols from Layers and Convert Layers to Symbols. + New Symbols from Layers Новые символы из слоёв + Convert Layers to Symbols Преобразовать слои в символы +@msg {id} is no top-level layer {id} — не слой верхнего уровня Marathi Маратхи Gujarati Гуджарати Bengali Бенгальский diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 754962d2e..182dee7b5 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -6114,6 +6114,10 @@ breakText Glyphs Glyfer @msg there is no clock here: give `time` (Unix seconds) det finns ingen klocka här: ange `time` (Unix-sekunder) @msg the document has never been saved: give `folder` dokumentet har aldrig sparats: ange `folder` @msg every numbered name for the copy is taken alla numrerade namn för kopian är upptagna +# Document Tools add-on: New Symbols from Layers and Convert Layers to Symbols. + New Symbols from Layers Nya symboler från lager + Convert Layers to Symbols Konvertera lager till symboler +@msg {id} is no top-level layer {id} är inget lager på översta nivån Marathi Marathi Gujarati Gujarati Bengali Bengali diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 8d682e785..ae2a8b933 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -6019,6 +6019,10 @@ breakText Glyphs Glifler @msg there is no clock here: give `time` (Unix seconds) burada saat yok: `time` verin (Unix saniyesi) @msg the document has never been saved: give `folder` belge hiç kaydedilmedi: `folder` verin @msg every numbered name for the copy is taken kopya için tüm numaralı adlar kullanılıyor +# Document Tools add-on: New Symbols from Layers and Convert Layers to Symbols. + New Symbols from Layers Katmanlardan Yeni Semboller + Convert Layers to Symbols Katmanları Sembollere Dönüştür +@msg {id} is no top-level layer {id} üst düzey bir katman değil Marathi Marathi dili Gujarati Gücerat dili Bengali Bengalce diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index b5ac2dc5a..f96a15dc6 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -5661,6 +5661,10 @@ breakText Glyphs Гліфи @msg there is no clock here: give `time` (Unix seconds) тут немає годинника: укажіть `time` (секунди Unix) @msg the document has never been saved: give `folder` документ жодного разу не зберігався: укажіть `folder` @msg every numbered name for the copy is taken усі нумеровані назви для копії зайняті +# Document Tools add-on: New Symbols from Layers and Convert Layers to Symbols. + New Symbols from Layers Нові символи з шарів + Convert Layers to Symbols Перетворити шари на символи +@msg {id} is no top-level layer {id} — не шар верхнього рівня Marathi Маратхі Gujarati Гуджараті Bengali Бенгальська diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 49885e57d..559dbe099 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -5664,6 +5664,10 @@ breakText Glyphs Glyph @msg there is no clock here: give `time` (Unix seconds) ở đây không có đồng hồ: hãy cho `time` (giây Unix) @msg the document has never been saved: give `folder` tài liệu chưa từng được lưu: hãy cho `folder` @msg every numbered name for the copy is taken mọi tên đánh số cho bản sao đều đã dùng +# Document Tools add-on: New Symbols from Layers and Convert Layers to Symbols. + New Symbols from Layers Ký hiệu mới từ các lớp + Convert Layers to Symbols Chuyển các lớp thành ký hiệu +@msg {id} is no top-level layer {id} không phải là lớp cấp cao nhất Marathi Tiếng Marathi Gujarati Tiếng Gujarati Bengali Tiếng Bengali diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 2c767f639..cc619b472 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -5904,6 +5904,10 @@ breakText Glyphs 字形 @msg there is no clock here: give `time` (Unix seconds) 此处没有时钟:请提供 `time`(Unix 秒) @msg the document has never been saved: give `folder` 文档从未保存过:请提供 `folder` @msg every numbered name for the copy is taken 副本的所有编号名称均已被占用 +# Document Tools add-on: New Symbols from Layers and Convert Layers to Symbols. + New Symbols from Layers 从图层新建符号 + Convert Layers to Symbols 将图层转换为符号 +@msg {id} is no top-level layer {id} 不是顶层图层 Marathi 马拉地语 Gujarati 古吉拉特语 Bengali 孟加拉语 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 651a1293c..d2134afdc 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -5701,6 +5701,10 @@ breakText Glyphs 字符 @msg there is no clock here: give `time` (Unix seconds) 此處沒有時鐘:請提供 `time`(Unix 秒) @msg the document has never been saved: give `folder` 文件從未儲存過:請提供 `folder` @msg every numbered name for the copy is taken 拷貝的所有編號名稱均已被佔用 +# Document Tools add-on: New Symbols from Layers and Convert Layers to Symbols. + New Symbols from Layers 從圖層新增符號 + Convert Layers to Symbols 將圖層轉換為符號 +@msg {id} is no top-level layer {id} 不是最上層圖層 Marathi 馬拉地文 Gujarati 古吉拉特文 Bengali 孟加拉文 diff --git a/docs/mcp.md b/docs/mcp.md index ec441b736..ee1f9a923 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -6292,3 +6292,15 @@ command is off where there is no clock (the web). → `{path, format, bytes, war {"name":"run_command","arguments":{"command":"addon.document.saveTimestampedCopy","params":{}}} {"name":"run_command","arguments":{"command":"addon.document.saveTimestampedCopy","params":{"folder":"/Users/me/Versions","format":"pdf"}}} ``` + +`addon.document.symbolsFromLayers {layers?, replace?}` makes one symbol of each top-level layer's art, as `symbol.new` +makes one (registration in the centre), named after the layer with a number added when a symbol has that name, as +one undo step. `layers` lists layer ids (top-level layers only; anything else is refused); by default it takes the +layers highlighted in the Layers panel when two or more are, else every layer. With `replace` (Convert Layers to +Symbols in the menu) each layer's art is replaced by an instance of its symbol at the same place and size, the +instances are selected, and locked layers are skipped. Layers with no art are skipped too; with nothing to make +there is no step. → `{symbols: [name…], ids: [instance id…], skipped: [{layer, name, reason: "empty" | "locked"}…]}` + +```json +{"name":"run_command","arguments":{"command":"addon.document.symbolsFromLayers","params":{"replace":true}}} +``` diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 99e2d7b2b..8e145f25a 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with Adobe Illustrator -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Document Tools add-on: Save a Timestamped Copy, #4926; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Document Tools add-on: New Symbols from Layers, #4927; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) ## Feature submission evidence @@ -1411,10 +1411,15 @@ A VectorCraft addition, not a feature of the reference app, so no area score mov A VectorCraft addition, not a feature of the reference app, so no area score moves. Object › Plug-ins › Document Tools › Save a Timestamped Copy writes a copy of the document named `_YYYY-MM-DD_HH-MM.` (the date and time in UTC, through chrono; `-2`, `-3`… added when that name is taken) next to the document, or into a folder picked (the folder picker) when it was never saved, in the document's own format or another save format. It writes through File › Save a Copy's save, so the document keeps its path, title and modified state, no undo step is made, and the write stays inside the automation roots in force. It is off on the web, which has no clock. Tests: `cmd::addons::document::tests` in `vectorcraft-engine` (the name and place, numbered names, the document unchanged and its content in the copy, an unsaved document needing a folder, other formats, bad params refused, automation roots), `addons::document::tests` in `vectorcraft-ui-egui` (the submenu, beside the document, the folder picked or cancelled) and `command_sweep`. Commands: `docs/mcp.md` › Document Tools add-on. +## Document Tools add-on: New Symbols from Layers (2026-11-01) + +A VectorCraft addition, not a feature of the reference app, so no area score moves. Object › Plug-ins › Document Tools › New Symbols from Layers makes one symbol of each top-level layer's art (the layers given, else those highlighted in the Layers panel when two or more are, else every layer), named after the layer (a number added when a symbol has that name), with New Symbol's own steps: the art normalized to its natural size, registration in the centre, threaded text keeping the text it shows, and sublayers held as groups. Convert Layers to Symbols does the same and puts an instance of each symbol in place of its layer's art, at the same place and size, selecting the instances; locked layers are skipped then. Empty layers are skipped and reported, all in one undo step (none when nothing is made). Tests: `cmd::addons::document::tests` in `vectorcraft-engine` (a symbol per layer, unique names, empty and locked layers reported, replacement in place, the highlighted layers as the default, bad layer ids refused, one undo step), `new_symbols_from_layers_and_convert_layers_to_symbols` in `vectorcraft-ui-egui` and `command_sweep`. Commands: `docs/mcp.md` › Document Tools add-on. + ## Revision history | Date | Change | Summary | |---|---|---| +| 2026-11-01 | minor | Document Tools add-on (a VectorCraft addition): New Symbols from Layers (#4927); evidence added; scores unchanged. | | 2026-11-01 | minor | Document Tools add-on (a VectorCraft addition): Save a Timestamped Copy (#4926); evidence added; scores unchanged. | | 2026-11-01 | minor | Path Precision Tools add-on (a VectorCraft addition): Divide into Dashes (#4950); evidence added; scores unchanged. | | 2026-11-01 | minor | Arrange Tools add-on (a VectorCraft addition): Group Overlapping Objects (#4928); evidence added; scores unchanged. | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 51578bb92..e05eeafa8 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Document Tools add-on: Save a Timestamped Copy, #4926) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Document Tools add-on: New Symbols from Layers, #4927) · **Target:** Adobe Illustrator 2026 (30.x) ## Feature submission evidence @@ -331,6 +331,7 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po | Arrange Tools add-on (VectorCraft addition): Object › Plug-ins › Arrange Tools runs Arrange in Grid… (grid by row, one row or one column; columns, H and V spacing, Center on Artboard; Preview), Swap Objects, Swap Objects and Sizes, Replace with Top Object… (Actual Size, Bounds or Bounds (Fit Proportionally); Keep Original; Scale Strokes & Effects; Preview), Set Size of Each… (Width and Height each ticked on or off, Constrain Width and Height Proportions with one side, the reference point, Scale Strokes & Effects; Preview) and Zoom to Selection, each one undo step | done | `addon.arrange.*`, `crates/engine/src/cmd/addons/arrange.rs`, `crates/ui-egui/src/addons/arrange.rs` and their tests; not a feature of the reference app, so it is not scored | | Arrange Tools add-on (VectorCraft addition): Object › Plug-ins › Arrange Tools › Group Overlapping Objects groups the selected objects whose visual bounds overlap or touch (chains included, within one layer or group), each group at its frontmost member's stacking place, one undo step | done | #4928; `addon.arrange.groupOverlapping`, `crates/engine/src/cmd/addons/arrange.rs` and its tests; not a feature of the reference app, so it is not scored | | Document Tools add-on (VectorCraft addition): Object › Plug-ins › Document Tools › Save a Timestamped Copy writes `_YYYY-MM-DD_HH-MM.` next to the document (a folder picked for one never saved), which keeps its path, title and modified state; off on the web | done | #4926; `addon.document.saveTimestampedCopy`, `crates/engine/src/cmd/addons/document.rs`, `crates/ui-egui/src/addons/document.rs` and their tests; not a feature of the reference app, so it is not scored | +| Document Tools add-on (VectorCraft addition): Object › Plug-ins › Document Tools › New Symbols from Layers makes a symbol of each layer's art named after the layer (the highlighted layers when two or more are, else all); Convert Layers to Symbols also replaces each layer's art with an instance in place; empty layers skipped, one undo step | done | #4927; `addon.document.symbolsFromLayers`, `crates/engine/src/cmd/addons/document.rs`, `crates/ui-egui/src/addons/document.rs` and their tests; not a feature of the reference app, so it is not scored | | Smart Pen add-on (VectorCraft addition): Object › Plug-ins › Smart Pen selects the Smart Pen Tool, which the All Tools drawer lists under its own heading but neither toolbar shows as shipped (no shortcut), and opens the Smart Pen panel (next segment straight or curved; Remove Points Keep Shape, Make Smooth, Make Corner, Retract Handles, Close or Join). The tool draws (click: straight or curved segment, a curve after a straight one tangent; drag: smooth anchor, Alt breaks the handle, Shift 45°, Space moves it; Tab switches a segment; rubber band; close, join, continue) and edits without switching tools (click selects a point, drag moves it, Alt-click removes it keeping the shape, Shift+Alt-click deletes it, double-click toggles smooth/corner; handle drags with Shift and Alt; segment drags reshape, Alt-click adds a point) | done | `addon.pen.*`, `crates/tools/src/addons/smart_pen.rs`, `crates/ui-egui/src/addons/pen.rs` and their tests; not a feature of the reference app, so it is not scored | | Path Precision Tools add-on (VectorCraft addition): Object › Plug-ins › Path Precision Tools selects its seven tools (Anchor Eraser Brush, Extend Path, Measure & Inspect, Parametric Shape, Corner Tool, Slide Anchor Along Path, Shared Edge Merge Tool), which the All Tools drawer lists under their own heading but neither toolbar shows as shipped, opens the Anchor Inspector panel (the selected anchor's X/Y and handle lengths and angles as fields; Make Smooth, Make Corner, Equalize Handles, Remove Points Keep Shape, Add Points, the three ways to close) and the Parametric Shape panel (kind, count, W/H, rotation, the kind's own values and corner radius of the selected parametric shape, or of the tool's next one), and runs Add Points at / Split at Crossings and Convert to Parametric Shape; the Parametric Shape Tool draws ten re-editable kinds from the centre (↑/↓ and ←/→ change their numbers while dragging) and edits a selected one with handles; the Corner Tool cuts editable corners on any path (hover shows the anchor, a click uses its radius and kind, a drag sizes the corner, Shift whole values; a corner's marker selects it and loads its values, drags to resize, to 0 or double-clicked removes it; never past what fits; Alt-marquee cuts every anchor inside) with a Corners section in the Anchor Inspector (radius, kind, selected count, Apply to Selected Anchors); Slide Anchor Along Path drags an anchor along its curve with the segments beside it refitted, Shift snapping where the tangent is a multiple of 45°; the Shared Edge Merge Tool shows, over two paths touching at two coincident anchors, the two stretches and the edge merging them, a click merges them (half way; Shift the upper path, Alt the lower), and near anchors meet or an anchor one path lacks is added to match (Shift+Alt asks for this inside a stretch), with a Shared Edges section in the Anchor Inspector (brush diameter, coincidence 0 to 3 pt, Keep Shared Points Together, which takes coincident anchors along with the add-on's own anchor moves); a double-click on the brush or Extend Path tool opens its options | done | `addon.pathtools.*`, `crates/tools/src/addons/`, `crates/ui-egui/src/addons/pathtools.rs`, `crates/ui-egui/src/addons/pathtools/` and their tests; not a feature of the reference app, so it is not scored | | Path Precision Tools add-on (VectorCraft addition): Object › Plug-ins › Path Precision Tools › Divide into Dashes… (Dash and Gap in any unit, Fit to Corners and Ends, Remove Gaps; Preview) cuts the selected paths, or only their direct-selected segments, into open pieces along the curve, keeping their appearance, one undo step | done | #4950; `addon.pathtools.divideDashes`, `crates/pathops/src/addons/pathtools/dashes.rs`, `crates/ui-egui/src/addons/pathtools/dashes.rs` and their tests; not a feature of the reference app, so it is not scored | @@ -384,6 +385,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development | Date | Change | Summary | |---|---|---| +| 2026-11-01 | minor | Document Tools add-on (a VectorCraft addition): New Symbols from Layers and Convert Layers to Symbols (#4927); scores and estimates unchanged | | 2026-11-01 | minor | Document Tools add-on (a VectorCraft addition): Save a Timestamped Copy (#4926); scores and estimates unchanged | | 2026-11-01 | minor | Path Precision Tools add-on (a VectorCraft addition): Divide into Dashes (#4950); scores and estimates unchanged | | 2026-11-01 | minor | Arrange Tools add-on (a VectorCraft addition): Group Overlapping Objects (#4928); scores and estimates unchanged |