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 |