Select: Link Block Series checks the selection in linear time

The type objects in the selection are deduplicated with a set, each frame's
thread is looked up in a map built once, the locked artboards are measured
once per call, and the menu check stops at the first selected type object.
This commit is contained in:
dotCooCoo
2026-11-02 15:21:51 +07:00
committed by bflatastic
parent 405cc83cae
commit 30dd4fe8f5
2 changed files with 52 additions and 30 deletions
+49 -26
View File
@@ -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};
@@ -322,37 +322,53 @@ fn same_text(s: &mut Session, cmd: &str, eq: fn(&CharStyle, &CharStyle) -> bool)
finish_same(s, cmd, ids)
}
/// The type objects 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.
fn selected_text(s: &Session) -> Vec<NodeId> {
fn inside(n: &Node, out: &mut Vec<NodeId>) {
/// 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;
}
if matches!(n.kind, NodeKind::Text(_)) && !out.contains(&n.id) {
out.push(n.id);
}
for c in n.children().into_iter().flatten() {
inside(c, out);
}
}
let Some(st) = s.active() else { return vec![] };
let mut out = vec![];
for id in &st.selection.objects {
if is_selectable(&st.doc, st.isolation, *id)
&& let Some(n) = st.doc.node(*id)
{
inside(n, &mut out);
}
/// The type objects [`each_selected_text`] gives, each once, in selection order.
fn selected_text(s: &Session) -> Vec<NodeId> {
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)?;
if selected_text(s).is_empty() { Err("select a text object".into()) } else { Ok(()) }
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
@@ -365,12 +381,19 @@ fn same_link_block_series(s: &mut Session, _: &Value) -> Result<Value> {
}
let st = s.doc()?;
let can = selectable_ids(&st.doc, st.isolation);
let mut ids: Vec<NodeId> = vec![];
for seed in seeds {
let series = st.doc.text_threads.iter().find(|t| t.contains(&seed)).cloned().unwrap_or_else(|| vec![seed]);
// Each frame's thread: the first thread that lists it.
let mut threads: HashMap<NodeId, &[NodeId]> = 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<NodeId>, HashSet<NodeId>) = (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) && !ids.contains(&id) {
ids.push(id);
if can.contains(id) && seen.insert(*id) {
ids.push(*id);
}
}
}
+3 -4
View File
@@ -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};
@@ -250,8 +250,8 @@ pub(crate) fn selectable_ids(d: &Document, iso: Option<NodeId>) -> BTreeSet<Node
}
/// Whether [`walk_selectable`] visits `id`, read from the objects that hold it instead of a walk of
/// the document.
pub(crate) fn is_selectable(d: &Document, iso: Option<NodeId>, id: NodeId) -> bool {
/// the document. `locks` is the document's [`Document::locked_artboards`].
pub(crate) fn is_selectable(d: &Document, iso: Option<NodeId>, 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;
@@ -260,7 +260,6 @@ pub(crate) fn is_selectable(d: &Document, iso: Option<NodeId>, id: NodeId) -> bo
// 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 => {
let locks = d.locked_artboards();
// 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))