diff --git a/crates/doc/src/pattern.rs b/crates/doc/src/pattern.rs index 1b587e70f..74bdf0514 100644 --- a/crates/doc/src/pattern.rs +++ b/crates/doc/src/pattern.rs @@ -572,6 +572,11 @@ impl RepeatSpec { fn kept(&self, nested: usize) -> usize { self.kind.count().min(MAX_REPEAT_INSTANCES / nested.max(1)).max(1) } + /// How many instances this Repeat draws ([`Self::transforms`]): as many as its options make, + /// fewer when the Repeats in its source would take it past [`MAX_REPEAT_INSTANCES`]. + pub fn instances(&self) -> usize { + self.kept(nested_instances(&self.source)) + } /// Instances made in all, those of the Repeats nested in the source included. fn instances_in_all(&self) -> usize { let nested = nested_instances(&self.source); diff --git a/crates/engine/src/cmd/docinfo.rs b/crates/engine/src/cmd/docinfo.rs index 647cf91c7..0a2a43e7b 100644 --- a/crates/engine/src/cmd/docinfo.rs +++ b/crates/engine/src/cmd/docinfo.rs @@ -5,7 +5,7 @@ use std::collections::{BTreeMap, BTreeSet}; use serde_json::{Value, json}; use vectorcraft_color::Paint; -use vectorcraft_doc::{ColorMode, Document, Node, NodeId, NodeKind}; +use vectorcraft_doc::{ColorMode, Document, Node, NodeId, NodeKind, RepeatKind, RepeatSpec}; use vectorcraft_geom::{Affine, Rect}; use vectorcraft_text::embed::Embedding; @@ -18,7 +18,7 @@ pub fn specs() -> Vec { "Document Info", ["Window", "Document Info"], None, - "{selectionOnly?, category?: document|objects|graphicStyles|brushes|spotColors|patterns|gradients|symbols|fonts|fontDetails|linkedImages|embeddedImages (only that one in sections and the text), format?: \"text\" (→ {text}: the report Document Info › Save… and Package write)} → {document, objects: {paths, compoundPaths, groups, …}, pathLength: pt (the paths' total length, compound paths' included, guides left out), fonts, images, swatches, graphicStyleNames (with selectionOnly: the styles the selected objects are linked to), brushNames (the Brushes panel's brushes; with selectionOnly: the brushes the selected objects' strokes use), …, sections: [{id, title, rows: [[label, value]]}] (the panel's categories; fontDetails: each font's file and whether its license lets it be embedded)}", + "{selectionOnly?, category?: document|objects|graphicStyles|brushes|spotColors|patterns|gradients|symbols|fonts|fontDetails|linkedImages|embeddedImages|repeats (only that one in sections and the text), format?: \"text\" (→ {text}: the report Document Info › Save… and Package write)} → {document, objects: {paths, compoundPaths, groups, …}, pathLength: pt (the paths' total length, compound paths' included, guides left out), fonts, images, swatches, graphicStyleNames (with selectionOnly: the styles the selected objects are linked to), brushNames (the Brushes panel's brushes; with selectionOnly: the brushes the selected objects' strokes use), repeatObjects: [{id, name, type: radial|grid|mirror, instances, rows?, cols?}] (the Repeat objects, those in groups and other repeats included; with selectionOnly: those in the selection), …, sections: [{id, title, rows: [[label, value]]}] (the panel's categories; fontDetails: each font's file and whether its license lets it be embedded)}", has_doc, info ), @@ -35,7 +35,7 @@ pub fn specs() -> Vec { } /// Document Info's categories: (id, title), in the order the panel and the report show them. -pub const CATEGORIES: [(&str, &str); 12] = [ +pub const CATEGORIES: [(&str, &str); 13] = [ ("document", "Document"), ("objects", "Objects"), ("graphicStyles", "Graphic Styles"), @@ -48,6 +48,7 @@ pub const CATEGORIES: [(&str, &str); 12] = [ ("fontDetails", "Font Details"), ("linkedImages", "Linked Images"), ("embeddedImages", "Embedded Images"), + ("repeats", "Repeat Objects"), ]; /// The object counts of `document.info` and their labels. @@ -97,9 +98,47 @@ pub fn embedding_label(fs_type: u16) -> &'static str { /// An image object as Document Info lists it: (name, width, height, linked file). type ImageRow = (String, u32, u32, Option); +/// A Repeat object as Document Info lists it: its `repeatObjects` entry, and its row, which gives +/// its name and then its kind with the number of instances it draws (for a grid, its rows and +/// columns, followed by the number it draws when that is fewer). +fn repeat_entry(n: &Node, spec: &RepeatSpec) -> (Value, (String, String)) { + let name = n.display_name(); + let count = |n: usize, one: &str, many: &str| if n == 1 { format!("1 {one}") } else { format!("{n} {many}") }; + let instances = spec.instances(); + let mut entry = json!({ "id": n.id.0, "name": name, "instances": instances }); + let kind = &spec.kind; + let detail = match kind { + RepeatKind::Radial { .. } => { + entry["type"] = json!("radial"); + count(instances, "instance", "instances") + } + RepeatKind::Mirror { .. } => { + entry["type"] = json!("mirror"); + count(instances, "instance", "instances") + } + RepeatKind::Grid { rows, cols, .. } => { + // `repeat_transforms` clamps the rows and columns the same way. + let (rows, cols) = ((*rows).clamp(1, 500), (*cols).clamp(1, 500)); + entry["type"] = json!("grid"); + entry["rows"] = json!(rows); + entry["cols"] = json!(cols); + let grid = format!("{} × {}", count(rows as usize, "row", "rows"), count(cols as usize, "column", "columns")); + if instances < (rows * cols) as usize { format!("{grid}, {}", count(instances, "instance", "instances")) } else { grid } + } + }; + (entry, (name, format!("{}, {detail}", kind.label()))) +} + /// The Document Info categories of `d` (only `category` when given) from `info` (what -/// `document.info` reports), its fonts `(family, style)`, image objects and used patterns. -fn sections(d: &Document, info: &Value, fonts: &BTreeSet, images: &[ImageRow], category: Option<&str>) -> Vec
{ +/// `document.info` reports), its fonts, its image objects and the rows of its Repeat objects. +fn sections( + d: &Document, + info: &Value, + fonts: &BTreeSet, + images: &[ImageRow], + repeats: &[(String, String)], + category: Option<&str>, +) -> Vec
{ let names = |k: &str| -> Vec<(String, String)> { info[k].as_array().into_iter().flatten().filter_map(Value::as_str).map(|n| (n.to_string(), String::new())).collect() }; @@ -176,6 +215,7 @@ fn sections(d: &Document, info: &Value, fonts: &BTreeSet "linkedImages" => { images.iter().filter_map(|(name, w, h, link)| Some((name.clone(), format!("{w} × {h} px — {}", link.as_ref()?)))).collect() } + "repeats" => repeats.to_vec(), _ => images.iter().filter(|i| i.3.is_none()).map(|(name, w, h, _)| (name.clone(), format!("{w} × {h} px"))).collect(), }; Section { id, title, rows } @@ -250,6 +290,9 @@ fn info(s: &mut Session, p: &Value) -> Result { let mut styles: Vec<&str> = if selection_only { vec![] } else { d.graphic_styles.iter().map(|g| g.name.as_str()).collect() }; // `brushes` holds the Brushes panel's brushes, or with selectionOnly it holds the brushes that the selected objects' strokes use. let mut brushes: Vec = if selection_only { vec![] } else { vectorcraft_brush::library(d).into_iter().map(|b| b.name).collect() }; + // `repeats` and `repeat_rows` hold the `repeatObjects` entries and the rows of the Repeat objects that the walk visits, in walk order. + let mut repeats: Vec = vec![]; + let mut repeat_rows: Vec<(String, String)> = vec![]; for r in roots { r.walk(&mut |n| { let k = match &n.kind { @@ -333,6 +376,11 @@ fn info(s: &mut Session, p: &Value) -> Result { } } } + if let NodeKind::Repeat(spec) = &n.kind { + let (entry, row) = repeat_entry(n, spec); + repeats.push(entry); + repeat_rows.push(row); + } }); } let font_names: Vec = fonts.iter().map(super::fonts::font_label).collect(); @@ -348,6 +396,7 @@ fn info(s: &mut Session, p: &Value) -> Result { "graphicStyles": d.graphic_styles.len(), "graphicStyleNames": styles, "brushNames": brushes, + "repeatObjects": repeats, "characterStyles": d.char_styles.len(), "paragraphStyles": d.para_styles.len(), "patterns": d.patterns.len(), @@ -355,7 +404,7 @@ fn info(s: &mut Session, p: &Value) -> Result { }); // The document's name: its file's, else its title. out["document"]["name"] = json!(st.title()); - let sections = sections(d, &out, &fonts, &image_rows, category); + let sections = sections(d, &out, &fonts, &image_rows, &repeat_rows, category); if text { return Ok(json!({ "text": report_text(&st.title(), selection_only, §ions) })); } @@ -497,6 +546,75 @@ mod tests { assert!(text.contains("\nCharcoal\n") && text.contains("\nArrow\n") && !text.contains("OBJECTS"), "{text}"); } + /// Document Info › Repeat Objects lists each Repeat object by name with its kind and the number + /// of instances it makes (for a grid, its rows and columns, followed by the number it draws when + /// that is fewer), one in a group included. With Selection Only it lists the Repeat objects in + /// the selection. + #[test] + fn info_lists_the_repeat_objects() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap(); + let square = + |s: &mut Session, x: f64| s.execute("shape.rectangle", &json!({"x": x, "y": 0, "width": 10, "height": 10})).unwrap()["id"].clone(); + let (a, b, c, d) = (square(&mut s, 0.0), square(&mut s, 100.0), square(&mut s, 200.0), square(&mut s, 300.0)); + let radial = s.execute("object.repeat.radial", &json!({"ids": [a], "instances": 6})).unwrap()["id"].clone(); + let grid = s.execute("object.repeat.grid", &json!({"ids": [b], "rows": 2, "cols": 1})).unwrap()["id"].clone(); + s.execute("layer.setProps", &json!({"id": grid, "name": "Tiles"})).unwrap(); + let mirror = s.execute("object.repeat.mirror", &json!({"ids": [c]})).unwrap()["id"].clone(); + s.execute("select.set", &json!({"ids": [mirror, d]})).unwrap(); + let group = s.execute("object.group", &json!({})).unwrap()["id"].clone(); + let i = s.execute("document.info", &json!({"category": "repeats"})).unwrap(); + assert_eq!( + i["repeatObjects"], + json!([ + {"id": radial, "name": "", "type": "radial", "instances": 6}, + {"id": grid, "name": "Tiles", "type": "grid", "rows": 2, "cols": 1, "instances": 2}, + {"id": mirror, "name": "", "type": "mirror", "instances": 2}, + ]) + ); + assert_eq!((i["sections"][0]["id"].as_str(), i["sections"][0]["title"].as_str()), (Some("repeats"), Some("Repeat Objects"))); + assert_eq!( + i["sections"][0]["rows"], + json!([ + ["", "Radial Repeat, 6 instances"], + ["Tiles", "Grid Repeat, 2 rows × 1 column"], + ["", "Mirror Repeat, 2 instances"], + ]) + ); + let names = |s: &mut Session| -> Vec { + let i = s.execute("document.info", &json!({"selectionOnly": true, "category": "repeats"})).unwrap(); + i["repeatObjects"].as_array().unwrap().iter().map(|r| r["name"].clone()).collect() + }; + s.execute("select.set", &json!({"ids": [grid, group]})).unwrap(); + assert_eq!(names(&mut s), ["Tiles", ""]); + s.execute("select.set", &json!({"ids": [radial]})).unwrap(); + assert_eq!(names(&mut s), [""]); + s.execute("select.set", &json!({"ids": [d]})).unwrap(); + assert!(names(&mut s).is_empty()); + let text = s.execute("document.info", &json!({"category": "repeats", "format": "text"})).unwrap()["text"].as_str().unwrap().to_string(); + assert!(text.contains("\nREPEAT OBJECTS\n: Radial Repeat, 6 instances\nTiles: Grid Repeat, 2 rows × 1 column\n"), "{text}"); + assert!(!text.contains("\nOBJECTS\n"), "{text}"); + // A radial of 1,000 copies of a 500 × 500 grid draws one copy within the instance budget, + // and Document Info reports 1 instance. + let e = square(&mut s, 350.0); + let big = s.execute("object.repeat.grid", &json!({"ids": [e], "rows": 500, "cols": 500})).unwrap()["id"].clone(); + let outer = s.execute("object.repeat.radial", &json!({"ids": [big], "instances": 1000})).unwrap()["id"].clone(); + s.execute("select.set", &json!({"ids": [outer]})).unwrap(); + let i = s.execute("document.info", &json!({"selectionOnly": true, "category": "repeats"})).unwrap(); + assert_eq!((i["repeatObjects"][0]["id"].clone(), i["repeatObjects"][0]["instances"].clone()), (outer, json!(1))); + assert_eq!((i["repeatObjects"][1]["id"].clone(), i["repeatObjects"][1]["instances"].clone()), (big, json!(250_000))); + assert_eq!(i["sections"][0]["rows"][0], json!(["", "Radial Repeat, 1 instance"])); + // A 500 × 500 grid of a mirror draws 125,000 copies within the budget, and its row gives + // that number after its rows and columns. + let f = square(&mut s, 380.0); + let pair = s.execute("object.repeat.mirror", &json!({"ids": [f]})).unwrap()["id"].clone(); + let tiles = s.execute("object.repeat.grid", &json!({"ids": [pair], "rows": 500, "cols": 500})).unwrap()["id"].clone(); + s.execute("select.set", &json!({"ids": [tiles]})).unwrap(); + let i = s.execute("document.info", &json!({"selectionOnly": true, "category": "repeats"})).unwrap(); + assert_eq!((i["repeatObjects"][0]["id"].clone(), i["repeatObjects"][0]["instances"].clone()), (tiles, json!(125_000))); + assert_eq!(i["sections"][0]["rows"][0], json!(["", "Grid Repeat, 500 rows × 500 columns, 125000 instances"])); + } + #[test] fn embedding_permissions_read_from_fs_type() { assert_eq!(embedding_label(0), "embedding allowed"); diff --git a/crates/engine/src/tests_package.rs b/crates/engine/src/tests_package.rs index 1f096b825..5b3947e29 100644 --- a/crates/engine/src/tests_package.rs +++ b/crates/engine/src/tests_package.rs @@ -228,6 +228,7 @@ fn the_document_info_report_has_every_category() { "FONT DETAILS", "LINKED IMAGES", "EMBEDDED IMAGES", + "REPEAT OBJECTS", ] { assert!(text.contains(&format!("\n{h}\n")), "{h}: {text}"); } @@ -239,9 +240,12 @@ fn the_document_info_report_has_every_category() { assert_eq!((i["sections"].as_array().unwrap().len(), i["sections"][0]["id"].as_str()), (1, Some("linkedImages"))); assert_eq!(i["sections"][0]["rows"].as_array().unwrap().len(), 2); let all = run(&mut s, "document.info", json!({})); - assert_eq!(all["sections"].as_array().unwrap().len(), 12); + assert_eq!(all["sections"].as_array().unwrap().len(), 13); let i = run(&mut s, "document.info", json!({"category": "brushes"})); assert_eq!((i["sections"].as_array().unwrap().len(), i["sections"][0]["id"].as_str()), (1, Some("brushes"))); + let i = run(&mut s, "document.info", json!({"category": "repeats"})); + assert_eq!((i["sections"].as_array().unwrap().len(), i["sections"][0]["id"].as_str()), (1, Some("repeats"))); + assert!(text.contains("\nREPEAT OBJECTS\nNone\n"), "{text}"); assert!(s.execute("document.info", &json!({"category": "strokes"})).is_err()); assert!(s.execute("document.info", &json!({"format": "xml"})).is_err()); // The bundled fonts allow embedding. diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index af003046f..790e5c329 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -2528,6 +2528,7 @@ Reorder Пренареждане Reorder Appearance Item Пренареждане на елемент от външния вид Repeat Повтаряне + Repeat Objects Обекти с повтаряне Repeat Options Опции за повтаряне Repeat Pathfinder Повтаряне на Pathfinder Repeat Tab Повтаряне на табулатора diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index ef3eb3188..c7f74a0fc 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -1461,6 +1461,7 @@ lighting Light Světlo {count} more rules: Export writes every one. Další pravidla ({count}): Export zapíše všechna. {count} unnamed objects left out: name them, or turn on Unnamed Objects in the panel menu. Vynecháno nepojmenovaných objektů: {count}. Pojmenujte je, nebo v nabídce panelu zapněte Nepojmenované objekty. All Categories Všechny kategorie + Repeat Objects Objekty opakování Save… Uložit… Selection Only Pouze výběr {title} (selection) {title} (výběr) diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 7776b605c..4fcdfb35d 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -1378,6 +1378,7 @@ lighting Light Lys {count} more rules: Export writes every one. {count} flere regler: Eksport skriver hver enkelt. {count} unnamed objects left out: name them, or turn on Unnamed Objects in the panel menu. {count} unavngivne objekter udeladt: giv dem navne, eller aktivér Unavngivne objekter i panelmenuen. All Categories Alle kategorier + Repeat Objects Gentagelsesobjekter Save… Gem… Selection Only Kun markering {title} (selection) {title} (markering) diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 4857200c1..0accc2e4a 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -2554,6 +2554,7 @@ mark-style Japanese Japanisch Reorder Neu anordnen Reorder Appearance Item Aussehenselement neu anordnen Repeat Wiederholung + Repeat Objects Wiederholungsobjekte Repeat Options Wiederholungsoptionen Repeat Pathfinder Pathfinder wiederholen Repeat Tab Tabulator wiederholen diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index f2e789c08..2a381b347 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -3096,6 +3096,7 @@ theme Appearance Εμφάνιση Rendering intent: Σκοπός απόδοσης: Reorder Αναδιάταξη Reorder Appearance Item Αναδιάταξη στοιχείου εμφάνισης + Repeat Objects Αντικείμενα επανάληψης Repeat Options Επιλογές επανάληψης Repeat Pathfinder Επανάληψη Pathfinder Repeat Tab Επανάληψη στηλοθέτη diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 684185882..10ef51451 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -2581,6 +2581,7 @@ Reorder Reordenar Reorder Appearance Item Reordenar elemento de apariencia Repeat Repetir + Repeat Objects Objetos de repetición Repeat Options Opciones de repetición Repeat Pathfinder Repetir Buscatrazos Repeat Tab Repetir tabulación diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 29673202e..d9ac84124 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -1435,6 +1435,7 @@ lighting Light Valo {count} more rules: Export writes every one. {count} sääntöä lisää: vienti kirjoittaa ne kaikki. {count} unnamed objects left out: name them, or turn on Unnamed Objects in the panel menu. {count} nimetöntä objektia jätetty pois: nimeä ne tai ota Nimettömät objektit käyttöön paneelin valikosta. All Categories Kaikki luokat + Repeat Objects Toisto-objektit Save… Tallenna… Selection Only Vain valinta {title} (selection) {title} (valinta) diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 284e5274c..68df48963 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -2578,6 +2578,7 @@ Reorder Réorganiser Reorder Appearance Item Réorganiser l’élément d’aspect Repeat Répétition + Repeat Objects Objets de répétition Repeat Options Options de répétition Repeat Pathfinder Répéter Pathfinder Repeat Tab Répéter la tabulation diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index e858fd451..ace31b58c 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -3104,6 +3104,7 @@ mark-style Japanese Japán Reorder Átrendezés Reorder Appearance Item Megjelenési elem átrendezése Repeat Ismétlés + Repeat Objects Ismétlésobjektumok Repeat Options Ismétlés beállításai Repeat Pathfinder Útvonalkezelő-művelet ismétlése Repeat Tab Tabulátor ismétlése diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index cbe5761d0..e2e49a046 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -1378,6 +1378,7 @@ lighting Light Cahaya {count} more rules: Export writes every one. {count} aturan lagi: Ekspor menuliskan semuanya. {count} unnamed objects left out: name them, or turn on Unnamed Objects in the panel menu. {count} objek tanpa nama tidak disertakan: beri nama, atau aktifkan Objek Tanpa Nama di menu panel. All Categories Semua kategori + Repeat Objects Objek pengulangan Save… Simpan… Selection Only Hanya seleksi {title} (selection) {title} (seleksi) diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 2b3f2c98a..d23e83b75 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -2612,6 +2612,7 @@ mark-style Japanese Giapponese Reorder Riordina Reorder Appearance Item Riordina elemento aspetto Repeat Ripetizione + Repeat Objects Oggetti di ripetizione Repeat Options Opzioni di ripetizione Repeat Pathfinder Ripeti Elaborazione tracciati Repeat Tab Ripeti tabulazione diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index ab86e069f..b3fb7bafa 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -3511,6 +3511,7 @@ file-filter Templates テンプレート Relinked {n} image(s) {n} 個の画像を再リンクしました Relinked {n} image(s); not in that folder: {} {n} 個の画像を再リンクしました。そのフォルダーに無いもの:{} Relinked {} image(s) {} 個の画像を再リンクしました + Repeat Objects リピートオブジェクト Repeat Options リピートオプション Reset with Mesh メッシュでリセット Reset with Warp ワープでリセット diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 5905dfd8e..34a0fe86f 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -1293,6 +1293,7 @@ lighting Light სინათლე {count} more rules: Export writes every one. კიდევ წესები: {count}. ექსპორტი ყველას ჩაწერს. {count} unnamed objects left out: name them, or turn on Unnamed Objects in the panel menu. გამოტოვებულია უსახელო ობიექტები: {count}. დაარქვით მათ სახელი ან პანელის მენიუში ჩართეთ „უსახელო ობიექტები“. All Categories ყველა კატეგორია + Repeat Objects გამეორების ობიექტები Save… შენახვა… Selection Only მხოლოდ მონიშნული {title} (selection) {title} (მონიშნული) diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index b9950bfe9..d70fe2ef8 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -3504,6 +3504,7 @@ file-filter Templates 템플릿 Relinked {n} image(s) 이미지 {n}개를 다시 연결했습니다 Relinked {n} image(s); not in that folder: {} 이미지 {n}개를 다시 연결했습니다. 해당 폴더에 없음: {} Relinked {} image(s) 이미지 {}개를 다시 연결했습니다 + Repeat Objects 반복 개체 Repeat Options 반복 옵션 Reset with Mesh 메시로 재설정 Reset with Warp 왜곡으로 재설정 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 4b8887f22..e4fd8c7f8 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -1392,6 +1392,7 @@ lighting Light Licht {count} more rules: Export writes every one. {count} regels meer: Export schrijft ze allemaal. {count} unnamed objects left out: name them, or turn on Unnamed Objects in the panel menu. {count} naamloze objecten weggelaten: geef ze een naam, of zet Naamloze objecten aan in het paneelmenu. All Categories Alle categorieën + Repeat Objects Herhalingsobjecten Save… Opslaan… Selection Only Alleen selectie {title} (selection) {title} (selectie) diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 42ede0d8e..53699fc66 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -1375,6 +1375,7 @@ lighting Light Światło {count} more rules: Export writes every one. Więcej reguł: {count}. Eksport zapisze wszystkie. {count} unnamed objects left out: name them, or turn on Unnamed Objects in the panel menu. Pominięto obiekty bez nazwy: {count}. Nadaj im nazwy lub włącz Obiekty bez nazwy w menu panelu. All Categories Wszystkie kategorie + Repeat Objects Obiekty powtórzenia Save… Zapisz… Selection Only Tylko zaznaczenie {title} (selection) {title} (zaznaczenie) diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 4a1fd3939..0d006f90e 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -2139,6 +2139,7 @@ Reorder Reordenar Reorder Appearance Item Reordenar Item de Aparência Repeat Repetir + Repeat Objects Objetos de Repetição Repeat Pathfinder Repetir Localizador de Caminhos Repeat Tab Repetir Parada de Tabulação Repeat {name} Repetir {name} diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index ca4ce58fa..e754190fb 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -2608,6 +2608,7 @@ mark-style Japanese Японский Reorder Изменить порядок Reorder Appearance Item Изменить порядок элемента оформления Repeat Повторить + Repeat Objects Объекты повтора Repeat Options Параметры повтора Repeat Pathfinder Повторить обработку контуров Repeat Tab Повторить табуляцию diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 4b8e64289..2b17b77d7 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -2528,6 +2528,7 @@ Reorder Ändra ordning Reorder Appearance Item Ändra ordning på utseendeobjekt Repeat Upprepa + Repeat Objects Upprepningsobjekt Repeat Options Upprepningsalternativ Repeat Pathfinder Upprepa banöverlappning Repeat Tab Upprepa tabb diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 73b9fc92b..4637d0210 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -2604,6 +2604,7 @@ mark-style Japanese Японський Reorder Змінити порядок Reorder Appearance Item Змінити порядок елемента оформлення Repeat Повторити + Repeat Objects Об’єкти повтору Repeat Options Параметри повтору Repeat Pathfinder Повторити обробку контурів Repeat Tab Повторити табуляцію diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 17740ca67..c67997cee 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -2871,6 +2871,7 @@ mark-style Japanese Kiểu Nhật Reorder Sắp xếp lại Reorder Appearance Item Sắp xếp lại mục diện mạo Repeat Lặp lại + Repeat Objects Đối tượng lặp lại Repeat Options Tùy chọn lặp lại Repeat Pathfinder Lặp lại Pathfinder Repeat Tab Tab lặp lại diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 5d9e16023..a0f513332 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -2490,6 +2490,7 @@ font-script Japanese 汉字 Reorder 重新排序 Reorder Appearance Item 重新排序外观项目 Repeat 重复 + Repeat Objects 重复对象 Repeat Options 重复选项 Repeat Pathfinder 重复路径查找器 Repeat Tab 重复制表位 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 725ab8011..879bef03c 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -3360,6 +3360,7 @@ font-script Japanese 漢字 Reorder 重新排序 Reorder Appearance Item 重新排序外觀項目 Repeat 重複 + Repeat Objects 重複物件 Repeat Options 重複選項 Repeat Pathfinder 重複路徑管理員 Repeat Tab 重複定位點 diff --git a/crates/ui-egui/src/panels/doc_info.rs b/crates/ui-egui/src/panels/doc_info.rs index 4b635f509..87f08f7f3 100644 --- a/crates/ui-egui/src/panels/doc_info.rs +++ b/crates/ui-egui/src/panels/doc_info.rs @@ -1,6 +1,6 @@ //! Document Info panel: the document's setup and what it contains (objects by kind, graphic //! styles, brushes, spot colors, patterns, gradients, symbols, fonts and their files, linked and embedded -//! images), for the whole document or the selection only. The flyout picks one category (or all) +//! images, repeat objects), for the whole document or the selection only. The flyout picks one category (or all) //! and saves the text report (`docInfo.save`, the report File → Package writes too). use egui::Ui; @@ -87,7 +87,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } /// The rows of Document Info section `id` labelled by a name (an artboard, a style, a swatch, a -/// font, an image) rather than an interface label: every row of the lists, and in Document the +/// font, an image, a repeat object) rather than an interface label: every row of the lists, and in Document the /// artboards, which follow the Artboards count. Names are shown as they are. fn named_rows(id: &str, rows: &[Value]) -> std::ops::Range { match id { @@ -183,7 +183,13 @@ mod tests { let text = crate::tests_labels::painted_text(&mut app, show); assert!(text.contains("Color Mode") && text.contains("Artboard 1"), "{text}"); let menu_text = crate::tests_labels::painted_text(&mut app, menu); - assert!(menu_text.contains("Font Details") && menu_text.contains("Brushes") && menu_text.contains("Save…"), "{menu_text}"); + assert!( + menu_text.contains("Font Details") + && menu_text.contains("Brushes") + && menu_text.contains("Repeat Objects") + && menu_text.contains("Save…"), + "{menu_text}" + ); let filtered = crate::tests_labels::painted_text(&mut app, |app, ui| { set_pstate(ui.ctx(), "docinfo-category", "fonts".to_string()); show(app, ui); @@ -199,6 +205,6 @@ mod tests { let r = app.run("docInfo.save", json!({"path": "/tmp/info.txt"})).unwrap(); assert_eq!(r["path"], "/tmp/info.txt"); let report = String::from_utf8(written.borrow().clone()).unwrap(); - assert!(report.starts_with("Document Info: ") && report.contains("\nFONT DETAILS\n"), "{report}"); + assert!(report.starts_with("Document Info: ") && report.contains("\nFONT DETAILS\n") && report.contains("\nREPEAT OBJECTS\n"), "{report}"); } } diff --git a/docs/mcp.md b/docs/mcp.md index d4d1e178e..c2dcba535 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -3196,8 +3196,11 @@ result carries the same files as a zip (`{name: ".zip", dataBase64}`, entr `document.info {selectionOnly?, category?, format?: "text"}` adds `sections` (`[{id, title, rows: [[label, value]]}]`: document, objects, graphicStyles, brushes, spotColors, patterns, gradients, symbols, fonts, fontDetails, linkedImages, -embeddedImages), `brushNames` (the Brushes panel's brushes; with `selectionOnly`, the brushes the selected objects' -strokes use) and `pathLength` (pt: the total length of the paths, compound paths' pieces included and guides left +embeddedImages, repeats), `brushNames` (the Brushes panel's brushes; with `selectionOnly`, the brushes the selected +objects' strokes use), `repeatObjects` (`[{id, name, type, instances, rows?, cols?}]`: every Repeat object, those in +groups and in other repeats included, with its type `radial`, `grid` or `mirror` and the number of instances it makes, +and a grid's `rows` and `cols`; with `selectionOnly`, the Repeat objects in the selection) +and `pathLength` (pt: the total length of the paths, compound paths' pieces included and guides left out, which objects lists as Path Length in the document's units, say for a cut line or an LED strip along the letters); `format: "text"` returns the plain-text report Document Info › Save… (`docInfo.save {path?}`) and the package report write. diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index eae65d884..893fdcc07 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -733,6 +733,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur Bristle Brush Options and `brush.new` take a bristle brush name of at most 31 characters, as the Brushes page states for the Bristle Brush Options. The Name field stops taking characters at 31, and `brush.new` and `brush.options` refuse a longer new name with an error that names the limit. The page does not say how the limit counts characters or what happens to longer names already saved; counting characters and the number added to a taken name, keeping longer saved names, and leaving `brush.duplicate` unlimited are VectorCraft's own rules. A bristle brush that a document or a saved file already holds under a longer name keeps it, and the other brush types take a name of any length. Test: `bristle_brush_names_have_at_most_31_characters` in `tests_brushsym.rs`. Public reference: [Brushes](https://web.archive.org/web/20241230205225/https://helpx.adobe.com/illustrator/using/brushes.html) (archived copy). In the Brushes panel, Shift-click selects a range of brushes and Ctrl/Cmd-click adds or removes one. Delete Brush and Duplicate Brush then act on all of them (`brush.delete {names}`, `brush.duplicate {names}`). Document Info has a Brushes category after Graphic Styles. It lists the Brushes panel's brushes, or with Selection Only it lists the brushes that the selected objects' strokes use. The Document Info report and Package's report list them too (`document.info {category: "brushes"}`, `brushNames`). + Document Info has a Repeat Objects category after Embedded Images. It lists each radial, grid and mirror Repeat object by name with its kind and the number of instances it makes. For a grid it gives the rows and columns instead, followed by the number of instances it draws when that is fewer. With Selection Only it lists the Repeat objects in the selection. The Document Info report and Package's report list them too (`document.info {category: "repeats"}`, `repeatObjects`). The Brushes panel menu's Select All Unused selects the brushes that are not used in the document, as the [Brushes](https://web.archive.org/web/20241230205225/https://helpx.adobe.com/illustrator/using/brushes.html) page (archived copy) describes. A brush counts as used when a stroke paints with it in the art (opacity masks included), in symbols, graph designs, pattern tiles or graphic styles, or in the art of a used brush (`brush.unused`). A rename in Brush Options and Delete Brush change every stroke that paints with the brush: in the layers and opacity masks, symbols, graph designs, pattern tiles, graphic styles and the art of other brushes. A rename gives those strokes the new name, and Delete Brush makes them plain strokes, in one undo step. - **Appearance:** the Appearance panel's selected row drives every paint, stroke, gradient and transparency edit (`item` on the commands); effects apply to one fill or stroke; per-item Opacity popups, whose blend-mode list and opacity slider stay open inside them (#430; the Control bar's Stroke popover too); Mixed Appearances, Layers target dots and basic-appearance rules. Effects can be reordered, moved between items and copied by dragging; Show All Hidden Attributes; clicking an effect edits it in place; the object thumbnail drags onto art; effects render on type, images, symbols, blends, envelopes, meshes and repeats, and per-item raster effects export. Groups, layers and type have their own fills, strokes and effects (Contents and Characters rows); the Layers panel's target circles target layers, groups and objects; Expand Appearance turns fills, strokes, effects and brushes into objects.