diff --git a/crates/engine/src/cmd/docmenu.rs b/crates/engine/src/cmd/docmenu.rs index 05973c9f9..dc6d2a655 100644 --- a/crates/engine/src/cmd/docmenu.rs +++ b/crates/engine/src/cmd/docmenu.rs @@ -2,7 +2,7 @@ //! brush strokes, Save/recall selections), View → Guides, and File → Close All / Document Color //! Mode / File Info. -use std::collections::BTreeSet; +use std::collections::{BTreeSet, HashMap, HashSet}; use std::sync::Arc; use serde_json::{Value, json}; @@ -11,7 +11,7 @@ use vectorcraft_geom::Vec2; use super::edit::selected_roots; use super::pathops::Beside; -use super::select::{selectable_ids, walk_selectable}; +use super::select::{is_selectable, selectable_ids, walk_selectable}; use super::{ CommandSpec, EngineError, Point, Result, Session, bad, bool_or, checked_ids_param, cmd, f64_or, f64_req, has_doc, has_selection, id_param, ok, str_param, @@ -27,6 +27,15 @@ pub fn specs() -> Vec { _ => false, } )), + cmd!( + "select.same.linkBlockSeries", + "Link Block Series", + ["Select", "Same"], + None, + "{} select every frame of the threads of the selected type objects (and of the type inside selected groups); type in no thread selects itself; type and frames that are hidden or locked, inside a hidden or locked object or layer, on a template layer, or outside the isolated group or layer neither count as selected nor are selected → {count}", + has_text_selection, + same_link_block_series + ), cmd!("select.same.fontFamily", "Font Family", ["Select", "Same"], None, "{} → {count}", has_selection, |s, _| same_text( s, "select.same.fontFamily", @@ -313,6 +322,84 @@ fn same_text(s: &mut Session, cmd: &str, eq: fn(&CharStyle, &CharStyle) -> bool) finish_same(s, cmd, ids) } +/// Call `f` with each type object among the selected objects and inside them, in selection order, +/// keeping only those [`walk_selectable`] visits: none that is hidden, locked, on a hidden, locked +/// or template layer, or outside the isolated group. A type object inside two selected objects is +/// given twice. The walk stops when `f` returns false. +fn each_selected_text(s: &Session, mut f: impl FnMut(NodeId) -> bool) { + fn inside(n: &Node, f: &mut impl FnMut(NodeId) -> bool) -> bool { + if !n.visible || n.locked { + return true; + } + if matches!(n.kind, NodeKind::Text(_)) && !f(n.id) { + return false; + } + n.children().into_iter().flatten().all(|c| inside(c, f)) + } + let Some(st) = s.active() else { return }; + let locks = st.doc.locked_artboards(); + for id in &st.selection.objects { + if is_selectable(&st.doc, st.isolation, &locks, *id) + && let Some(n) = st.doc.node(*id) + && !inside(n, &mut f) + { + return; + } + } +} + +/// The type objects [`each_selected_text`] gives, each once, in selection order. +fn selected_text(s: &Session) -> Vec { + let (mut out, mut seen) = (vec![], HashSet::new()); + each_selected_text(s, |id| { + if seen.insert(id) { + out.push(id); + } + true + }); + out +} + +/// A type object is selected, or is inside a selected object. +fn has_text_selection(s: &Session) -> std::result::Result<(), String> { + has_selection(s)?; + let mut any = false; + each_selected_text(s, |_| { + any = true; + false + }); + if any { Ok(()) } else { Err("select a text object".into()) } +} + +/// Select › Same › Link Block Series: the frames of the threads the selected type objects are +/// in, in thread order; a type object in no thread is its own series. +fn same_link_block_series(s: &mut Session, _: &Value) -> Result { + const C: &str = "select.same.linkBlockSeries"; + let seeds = selected_text(s); + if seeds.is_empty() { + return Err(bad(C, "select a text object")); + } + let st = s.doc()?; + let can = selectable_ids(&st.doc, st.isolation); + // Each frame's thread: the first thread that lists it. + let mut threads: HashMap = HashMap::new(); + for t in &st.doc.text_threads { + for id in t { + threads.entry(*id).or_insert(t.as_slice()); + } + } + let (mut ids, mut seen): (Vec, HashSet) = (vec![], HashSet::new()); + for seed in &seeds { + let series = threads.get(seed).copied().unwrap_or(std::slice::from_ref(seed)); + for id in series { + if can.contains(id) && seen.insert(*id) { + ids.push(*id); + } + } + } + finish_same(s, C, ids) +} + fn by_kind(s: &mut Session, f: impl Fn(&Node) -> bool) -> Result { let ids = candidates(s, f)?; s.select(|_, sel| sel.set(ids))?; diff --git a/crates/engine/src/cmd/select.rs b/crates/engine/src/cmd/select.rs index a98a78392..86e996068 100644 --- a/crates/engine/src/cmd/select.rs +++ b/crates/engine/src/cmd/select.rs @@ -4,7 +4,7 @@ use std::collections::BTreeSet; use serde_json::{Value, json}; use vectorcraft_color::Paint; -use vectorcraft_doc::{Document, Node, NodeId, NodeKind}; +use vectorcraft_doc::{Document, LockedArtboards, Node, NodeId, NodeKind}; use vectorcraft_tools::builder::is_live_paint; use super::wand::{fill_of, stroke_of}; @@ -249,6 +249,24 @@ pub(crate) fn selectable_ids(d: &Document, iso: Option) -> BTreeSet, locks: &LockedArtboards, id: NodeId) -> bool { + let Some(a) = d.ancestry(id) else { return false }; + if !a.iter().all(|i| d.node(*i).is_some_and(|n| n.visible && !n.locked && !n.is_template())) { + return false; + } + match iso.and_then(|i| d.node(i)) { + // Inside the isolated container, or the single object isolated. + Some(c) => a.iter().position(|i| *i == c.id).is_some_and(|p| p + 1 < a.len() || !c.is_container()), + None => { + // The object Select All takes: the outermost one that is not a layer. + let top = a.iter().filter_map(|i| d.node(*i)).find(|n| !n.is_layer()); + top.is_some_and(|t| locks.is_empty() || !locks.holds(t)) + } + } +} + fn all(s: &mut Session, _: &Value) -> Result { // While the Type tool edits text, Select All takes all of that text and leaves the art // selection as it is (Illustrator: text cursor in a text object). diff --git a/crates/engine/src/tests/menucmds.rs b/crates/engine/src/tests/menucmds.rs index 91194b273..7bac52cc9 100644 --- a/crates/engine/src/tests/menucmds.rs +++ b/crates/engine/src/tests/menucmds.rs @@ -1948,6 +1948,69 @@ fn select_same_font_size_and_fill() { assert!(s.execute("select.same.fontSize", &json!({})).is_err()); } +#[test] +fn select_same_link_block_series_selects_the_frames_of_a_thread() { + let mut s = session(); + let frame = |s: &mut Session, x: f64, y: f64| { + id_of(&s.execute("text.create", &json!({"x": x, "y": y, "text": "words ".repeat(40), "area": {"width": 80, "height": 30}})).unwrap()) + }; + let (a, b, c) = (frame(&mut s, 10.0, 10.0), frame(&mut s, 110.0, 10.0), frame(&mut s, 210.0, 10.0)); + sel(&mut s, &[a, b, c]); + s.execute("text.thread.create", &json!({})).unwrap(); + let (d, e) = (frame(&mut s, 10.0, 200.0), frame(&mut s, 110.0, 200.0)); + sel(&mut s, &[d, e]); + s.execute("text.thread.create", &json!({})).unwrap(); + let lone = text(&mut s, "alone"); + let r = rect(&mut s, 300.0, 300.0, 10.0, 10.0); + let undo = s.doc().unwrap().history.undo.len(); + // The middle frame of the first thread selects that thread's three frames, in thread order. + sel(&mut s, &[b]); + assert_eq!(s.execute("select.same.linkBlockSeries", &json!({})).unwrap()["count"], 3); + assert_eq!(selected(&s), vec![a, b, c]); + // Frames of two threads select both threads. + sel(&mut s, &[c, e]); + assert_eq!(s.execute("select.same.linkBlockSeries", &json!({})).unwrap()["count"], 5); + // Type in no thread selects itself; a path beside it is left out. + sel(&mut s, &[lone, r]); + assert_eq!(s.execute("select.same.linkBlockSeries", &json!({})).unwrap()["count"], 1); + assert_eq!(selected(&s), vec![lone]); + assert_eq!(s.doc().unwrap().history.undo.len(), undo, "selecting adds no undo step"); + // With no type selected the command is not available. + sel(&mut s, &[r]); + assert!(s.execute("select.same.linkBlockSeries", &json!({})).is_err()); + // A selected group counts the type inside it, but not hidden or locked type. + let box_ = rect(&mut s, 400.0, 400.0, 10.0, 10.0); + sel(&mut s, &[d, box_]); + let g = id_of(&s.execute("object.group", &json!({})).unwrap()); + sel(&mut s, &[g]); + assert!(find_command("select.same.linkBlockSeries").unwrap().info(&s).enabled); + assert_eq!(s.execute("select.same.linkBlockSeries", &json!({})).unwrap()["count"], 2); + assert_eq!(selected(&s), vec![d, e]); + for props in [json!({"id": d.0, "visible": false}), json!({"id": d.0, "visible": true, "locked": true})] { + s.execute("layer.setProps", &props).unwrap(); + sel(&mut s, &[g]); + assert!(!find_command("select.same.linkBlockSeries").unwrap().info(&s).enabled, "{props}"); + assert!(s.execute("select.same.linkBlockSeries", &json!({})).is_err(), "{props}"); + assert_eq!(selected(&s), vec![g], "{props}"); + } + s.execute("layer.setProps", &json!({"id": d.0, "locked": false})).unwrap(); + // A frame on a hidden layer, selected by id, is not type to start from. + let l1 = s.doc().unwrap().doc.layers[0].id; + s.execute("layer.setProps", &json!({"id": l1.0, "visible": false})).unwrap(); + sel(&mut s, &[a]); + assert!(!find_command("select.same.linkBlockSeries").unwrap().info(&s).enabled); + assert!(s.execute("select.same.linkBlockSeries", &json!({})).is_err()); + s.execute("layer.setProps", &json!({"id": l1.0, "visible": true})).unwrap(); + // In an isolated group, only the frames inside it count and are selected. + s.execute("object.isolate", &json!({"id": g.0})).unwrap(); + sel(&mut s, &[d]); + assert_eq!(s.execute("select.same.linkBlockSeries", &json!({})).unwrap()["count"], 1); + assert_eq!(selected(&s), vec![d]); + sel(&mut s, &[a]); + assert!(!find_command("select.same.linkBlockSeries").unwrap().info(&s).enabled); + assert!(s.execute("select.same.linkBlockSeries", &json!({})).is_err()); +} + /// #1737: Select > Same reads type's fill and stroke from its characters (the first run), so type /// matches type and paths both ways. #[test] diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 180c65dd3..cd7c81359 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -3270,6 +3270,7 @@ digits Malayalam Малаялам Graphic Графика Show all open documents Показване на всички отворени документи Symbol Instance Екземпляр на символ + Link Block Series Серия свързани блокове Symbol Libraries Библиотеки със символи Symbol Screener Tool Инструмент „Филтър на символи“ Symbol Scruncher Tool Инструмент „Свиване на символи“ diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 02defa9a1..e1c1eb8b7 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -418,6 +418,7 @@ Stroke Weight Debljina poteza Graphic Style Grafički stil Symbol Instance Instanca simbola + Link Block Series Niz povezanih blokova Font Family Porodica fonta Font Family & Style Porodica i stil fonta Font Family, Style & Size Porodica, stil i veličina fonta diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index d9d162bfe..f2158a683 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -4156,6 +4156,7 @@ digits Malayalam Malaiàlam Switch x/y Intercanvia x/y Symbol Símbol Symbol Instance Instància de símbol + Link Block Series Sèrie de blocs enllaçats Symbol Libraries Biblioteques de símbols Symbol Libraries Menu Menú de biblioteques de símbols Symbol Library Biblioteca de símbols diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 66db77578..19b8c43c9 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -426,6 +426,7 @@ Stroke Weight Tloušťka tahu Graphic Style Grafický styl Symbol Instance Instance symbolu + Link Block Series Řada propojených bloků Font Family Rodina písma Font Family & Style Rodina a řez písma Font Family, Style & Size Rodina, řez a velikost písma diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index e3123798e..c0bdb184b 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -385,6 +385,7 @@ Stroke Weight Stregtykkelse Graphic Style Grafisk stil Symbol Instance Symbolforekomst + Link Block Series Serie af sammenkædede blokke Font Family Skrifttypefamilie Font Family & Style Skrifttypefamilie og -stil Font Family, Style & Size Skrifttypefamilie, stil og størrelse diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 73e449299..0fec9a048 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -3298,6 +3298,7 @@ effect Points Punkte Graphic Grafik Show all open documents Alle geöffneten Dokumente anzeigen Symbol Instance Symbolinstanz + Link Block Series Reihe verketteter Blöcke Symbol Libraries Symbol-Bibliotheken Symbol Screener Tool Symbol-transparent-gestalten-Werkzeug Symbol Scruncher Tool Symbol-stauchen-Werkzeug diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index e452c1deb..9c4ba881b 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -3697,6 +3697,7 @@ digits Malayalam Μαλαγιάλαμ Graphic Γραφικό Show all open documents Εμφάνιση όλων των ανοιχτών εγγράφων Symbol Instance Στιγμιότυπο συμβόλου + Link Block Series Σειρά συνδεδεμένων μπλοκ Symbol Libraries Βιβλιοθήκες συμβόλων Symbol Screener Tool Αραίωση συμβόλων (Symbol Screener Tool) Symbol Scruncher Tool Σύμπτυξη συμβόλων (Symbol Scruncher Tool) diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 5d5656e06..90273208d 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -3354,6 +3354,7 @@ digits Malayalam Malayálam Graphic Gráfico Show all open documents Mostrar todos los documentos abiertos Symbol Instance Instancia de símbolo + Link Block Series Serie de bloques enlazados Symbol Libraries Bibliotecas de símbolos Symbol Screener Tool Herramienta Transparencia de símbolos Symbol Scruncher Tool Herramienta Concentración de símbolos diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index ccfe1b354..b56dc7e73 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -423,6 +423,7 @@ Stroke Weight Viivan paksuus Graphic Style Grafiikkatyyli Symbol Instance Symbolin ilmentymä + Link Block Series Linkitettyjen lohkojen sarja Font Family Fonttiperhe Font Family & Style Fonttiperhe ja tyyli Font Family, Style & Size Fonttiperhe, tyyli ja koko diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 37c75f40c..3f1374cf8 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -3346,6 +3346,7 @@ digits Malayalam Malayalam Graphic Graphique Show all open documents Afficher tous les documents ouverts Symbol Instance Instance de symbole + Link Block Series Série de blocs liés Symbol Libraries Bibliothèques de symboles Symbol Screener Tool Outil Transparence de symboles Symbol Scruncher Tool Outil Espacement de symboles diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 639882637..5c0099845 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -3988,6 +3988,7 @@ mark-style Japanese Japán Switch workspace Munkaterület váltása Symbol Szimbólum Symbol Instance Szimbólumpéldány + Link Block Series Csatolt blokkok sorozata Symbol Libraries Szimbólumkönyvtárak Symbol Screener Tool Szimbólumhalványító Symbol Scruncher Tool Szimbólumtömörítő diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 033f5fbcc..fabe83bc6 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -385,6 +385,7 @@ Stroke Weight Ketebalan stroke Graphic Style Gaya grafis Symbol Instance Instans simbol + Link Block Series Rangkaian blok tertaut Font Family Keluarga font Font Family & Style Keluarga & gaya font Font Family, Style & Size Keluarga, gaya & ukuran font diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index dc71df96a..3185cf883 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -3390,6 +3390,7 @@ digits Malayalam Malayalam Graphic Grafica Show all open documents Mostra tutti i documenti aperti Symbol Instance Istanza di simbolo + Link Block Series Serie di blocchi collegati Symbol Libraries Librerie simboli Symbol Screener Tool Strumento Trasparenza simboli Symbol Scruncher Tool Strumento Addensamento simboli diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index f9332e14e..928fd7055 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -455,6 +455,7 @@ digits Malayalam マラヤーラム数字 Stroke Weight 線幅 Graphic Style グラフィックスタイル Symbol Instance シンボルインスタンス + Link Block Series 連結ブロックの系列 Font Family フォントファミリー Font Family & Style フォントファミリー (スタイル) Font Family, Style & Size フォントファミリー (スタイルとサイズ) diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index f39f10da3..8a42692a8 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -343,6 +343,7 @@ Stroke Weight კონტურის სისქე Graphic Style გრაფიკული სტილი Symbol Instance სიმბოლოს ეგზემპლარი + Link Block Series დაკავშირებული ბლოკების სერია Font Family შრიფტის ოჯახი Font Family & Style შრიფტის ოჯახი და სტილი Font Family, Style & Size შრიფტის ოჯახი, სტილი და ზომა diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 00f6807c9..495c8b230 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -448,6 +448,7 @@ digits Malayalam 말라얄람어 Stroke Weight 선 두께 Graphic Style 그래픽 스타일 Symbol Instance 심볼 인스턴스 + Link Block Series 연결된 블록 시리즈 Font Family 글꼴 패밀리 Font Family & Style 글꼴 패밀리 및 스타일 Font Family, Style & Size 글꼴 패밀리, 스타일 및 크기 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index d15de7a87..698e576eb 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -399,6 +399,7 @@ Stroke Weight Contourdikte Graphic Style Grafische stijl Symbol Instance Symboolinstantie + Link Block Series Reeks gekoppelde blokken Font Family Lettertypefamilie Font Family & Style Lettertypefamilie en -stijl Font Family, Style & Size Lettertypefamilie, -stijl en -grootte diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 24d441a9e..711bf649e 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -386,6 +386,7 @@ Stroke Weight Grubość obrysu Graphic Style Styl graficzny Symbol Instance Wystąpienie symbolu + Link Block Series Seria połączonych bloków Font Family Rodzina czcionek Font Family & Style Rodzina i styl czcionki Font Family, Style & Size Rodzina, styl i rozmiar czcionki diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 879d45239..5e77e3e63 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -2757,6 +2757,7 @@ digits Malayalam Malaiala Switch Units Trocar Unidades Switch workspace Trocar espaço de trabalho Symbol Instance Instância de Símbolo + Link Block Series Série de Blocos Vinculados Symbol Libraries Bibliotecas de Símbolos Symbol Screener Tool Ferramenta Peneira de Símbolos Symbol Scruncher Tool Ferramenta Amontoar Símbolos diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 01e135647..3a52211b4 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -3385,6 +3385,7 @@ digits Malayalam Малаялам Graphic Графика Show all open documents Показать все открытые документы Symbol Instance Экземпляр символа + Link Block Series Серия связанных блоков Symbol Libraries Библиотеки символов Symbol Screener Tool Инструмент «Прозрачность символов» Symbol Scruncher Tool Инструмент «Сжатие символов» diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 9e1782d29..1e2eace01 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -3276,6 +3276,7 @@ digits Malayalam Malayalam Graphic Grafik Show all open documents Visa alla öppna dokument Symbol Instance Symbolinstans + Link Block Series Serie av länkade block Symbol Libraries Symbolbibliotek Symbol Screener Tool Verktyg för symbolopacitet Symbol Scruncher Tool Verktyg för att dra ihop symboler diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index ad6ea39b6..a9bf7050e 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -3169,6 +3169,7 @@ digits Malayalam Malayalam Graphic Grafik Show all open documents Tüm açık belgeleri göster Symbol Instance Sembol Örneği + Link Block Series Bağlantılı Blok Serisi Symbol Libraries Sembol Kitaplıkları Symbol Screener Tool Sembol Perdeleyici Aracı Symbol Scruncher Tool Sembol Ezici Aracı diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 1e9a28590..03f9ee088 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -3383,6 +3383,7 @@ effect Points Точки Graphic Графіка Show all open documents Показати всі відкриті документи Symbol Instance Екземпляр символу + Link Block Series Серія пов’язаних блоків Symbol Libraries Бібліотеки символів Symbol Screener Tool Інструмент «Прозорість символів» Symbol Scruncher Tool Інструмент «Стиск символів» diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index d39a33d34..1db2648f6 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -3706,6 +3706,7 @@ mark-style Japanese Kiểu Nhật Switch workspace Chuyển không gian làm việc Switch x/y Đổi x/y Symbol Instance Thực thể ký hiệu + Link Block Series Chuỗi khối liên kết Symbol Libraries Thư viện ký hiệu Symbol Screener Tool Công cụ Lọc ký hiệu Symbol Scruncher Tool Công cụ Thu gọn ký hiệu diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index dd17b3e73..28846a8c1 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -3222,6 +3222,7 @@ digits Malayalam 马拉雅拉姆数字 Graphic 图形 Show all open documents 显示所有打开的文档 Symbol Instance 符号实例 + Link Block Series 链接块系列 Symbol Libraries 符号库 Symbol Screener Tool 符号透明度工具 Symbol Scruncher Tool 符号紧缩工具 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 604ec3091..6f2903978 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -4234,6 +4234,7 @@ digits Malayalam 馬來亞拉姆數字 Graphic 圖形 Show all open documents 顯示所有開啟的文件 Symbol Instance 符號範例 + Link Block Series 連結區塊系列 Symbol Libraries 符號資料庫 Symbol Screener Tool 符號濾色器工具 Symbol Scruncher Tool 符號緊縮器工具 diff --git a/crates/ui-egui/src/menus/tests.rs b/crates/ui-egui/src/menus/tests.rs index a9dd3f749..04769b2c6 100644 --- a/crates/ui-egui/src/menus/tests.rs +++ b/crates/ui-egui/src/menus/tests.rs @@ -507,6 +507,26 @@ fn update_selection_follows_save_selection_and_needs_a_recall() { assert_eq!(app.run("select.recall1", json!({})).unwrap()["count"], 2); } +#[test] +fn select_same_link_block_series_selects_a_thread_from_the_menu() { + let mut app = app_with_a_rectangle(); + let entry = menu_entries(&app).into_iter().find(|e| e.command.as_deref() == Some("select.same.linkBlockSeries")).expect("menu entry"); + assert_eq!((entry.path, entry.label.as_str()), (vec!["Select".to_string(), "Same".to_string()], "Link Block Series")); + // A rectangle selected: no type, so the item is off. + assert!(!enabled(&app, "select.same.linkBlockSeries")); + let frame = |app: &mut VectorcraftApp, x: i32| { + app.run("text.create", json!({"x": x, "y": 40, "text": "words ".repeat(40), "area": {"width": 60, "height": 30}})).unwrap()["id"].clone() + }; + let (a, b) = (frame(&mut app, 0), frame(&mut app, 100)); + app.run("select.set", json!({"ids": [a.clone(), b.clone()]})).unwrap(); + app.run("text.thread.create", json!({})).unwrap(); + app.run("select.set", json!({"ids": [b.clone()]})).unwrap(); + assert!(enabled(&app, "select.same.linkBlockSeries")); + invoke(&mut app, "select.same.linkBlockSeries", json!({})); + let ids: Vec = app.session.active().unwrap().selection.objects.iter().map(|i| i.0).collect(); + assert_eq!(json!(ids), json!([a, b])); +} + #[test] fn edit_selection_opens_its_dialog_on_the_saved_selections() { let mut app = app_with_a_rectangle(); diff --git a/crates/ui-egui/src/menus/tree.rs b/crates/ui-egui/src/menus/tree.rs index e2952eac3..9eabfc681 100644 --- a/crates/ui-egui/src/menus/tree.rs +++ b/crates/ui-egui/src/menus/tree.rs @@ -462,6 +462,7 @@ pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec)> { c("Graphic Style", "select.same.graphicStyle"), c("Shape", "select.same.shapeType"), c("Symbol Instance", "select.same.symbolInstance"), + c("Link Block Series", "select.same.linkBlockSeries"), Sep, c("Font Family", "select.same.fontFamily"), c("Font Family & Style", "select.same.fontFamilyStyle"), diff --git a/docs/mcp.md b/docs/mcp.md index 5802a8144..19b072579 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -4883,6 +4883,13 @@ after it (`out`). The whole story flows into the frames before the break; the fr threaded with each other when two or more remain, and the first of them starts a paragraph. It refuses an `id` that doesn't exist, a `port` other than `in` or `out`, and a side where no frame of a thread comes before or after `id`. +`select.same.linkBlockSeries` (Select › Same › Link Block Series) selects every frame of the threads that the selected +type objects, and the type inside selected groups, are in, in thread order, and returns `{count}`. Type in no thread +selects itself, and other selected objects are left out. With frames of several threads selected, it selects the frames +of all of them. Type that is hidden or locked, inside a hidden or locked group or layer, on a template layer, or outside +the isolated group does not count as selected, and such frames of a thread are left out. It is off while no other type +is selected, and records no undo step. + In every thread, a paragraph that continues from one frame into the next has its first line, first-line indent and bullet or number in the frame where it starts. In the next frame its first line starts where its wrapped lines start, and it has no bullet or number. In a frame that is no longer threaded (after `text.thread.releaseSelection` or diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 66add4773..da63cd9be 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -888,6 +888,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur Type › Convert To Point Type on a frame of threaded text makes point type of the text that frame holds and takes it out of the thread; the story flows through the other frames without that text, so each character is in the document once. Object › Rasterize on a frame of threaded text takes it out of the thread with the text it shows, which the image draws, and the other frames keep the text they hold. When the last frame is rasterized, the text it has no room for goes after the text of the last frame that stays. Type › Create Outlines, Object › Expand and Object › Expand Appearance on a frame of threaded text take it out of the thread with the text it shows, which the outlines draw, and the other frames keep the text they hold. When the last frame is outlined, the text it has no room for goes after the text of the last frame that stays. When Expand Appearance keeps the frame as type in a group with its shadow, outer glow or Crop Marks, the type stays in the thread. The public help on threads ([Create text threads between objects](https://helpx.adobe.com/illustrator/desktop/design-with-text/add-manage-text/create-text-threads-between-objects.html), [Remove or break text threads between objects](https://helpx.adobe.com/illustrator/desktop/design-with-text/add-manage-text/remove-or-break-text-threads-between-objects.html)) does not describe outlining a threaded frame; this handling is VectorCraft's own behavior. + Select › Same › Link Block Series selects every frame of the threads of the selected type; type in no thread selects itself (`select.same.linkBlockSeries`). The menu entry is listed in [Select objects by characteristics](https://helpx.adobe.com/illustrator/desktop/manage-objects/select-objects/select-objects-by-characteristics.html); selecting several threads at once and type in no thread selecting itself are VectorCraft's own behavior. Object › Flatten Transparency takes a frame of threaded text out of the thread in the same way when Convert All Text to Outlines outlines it or when the frame becomes flat regions or an image, and the other frames keep the text they hold. New Symbol and Redefine Symbol on a frame of threaded text take it out of the thread with the text it shows, which the symbol's art holds, and the other frames keep the text they hold. When the last frame goes into the symbol, the text it has no room for goes after the text of the last frame that stays. When every frame of a thread goes into the symbol, the copy of the last frame keeps all its text. With Optical Margin Alignment on, or Roman Hanging Punctuation on in the first paragraph, the glyphs at the ends of left-to-right type on a path hang outside its brackets, measured along the path. The type hangs at the start bracket when its line is placed there, hangs at the end bracket when the line is placed there, and hangs at both when it is centered. Glyphs that hang past the end bracket do not make the type overflow, and a hang stops at an open path's end. SVG export writes such type as outlines.