perf(engine): keep the symbol part check on Selection::prune fast paths (#4502)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
el-j
2026-11-15 11:04:24 +01:00
co-authored by Claude Sonnet 5.5
parent db4eff146d
commit 3ed9d333b0
2 changed files with 36 additions and 2 deletions
+5 -2
View File
@@ -253,13 +253,16 @@ impl Selection {
}
/// Drop ids that no longer exist or are no longer editable.
pub fn prune(&mut self, doc: &Document) {
if self.objects.is_empty() && self.anchors.is_empty() && self.segments.is_empty() && self.key.is_none() && self.target.is_none() {
// The fast paths below don't look at a picked symbol part (it needs the symbol's art as well
// as the instance), so they are only taken while none is picked.
let plain = self.symbol_part.is_none();
if plain && self.objects.is_empty() && self.anchors.is_empty() && self.segments.is_empty() && self.key.is_none() && self.target.is_none() {
self.guides.retain(|i| doc.guides.get(*i).is_some_and(|g| doc.guide_editable(g)));
self.slices.retain(|id| doc.is_slice(*id));
self.key_guide = None;
return;
}
if self.anchors.is_empty() && self.segments.is_empty() && self.key.is_none() && self.target.is_none() {
if plain && self.anchors.is_empty() && self.segments.is_empty() && self.key.is_none() && self.target.is_none() {
if doc.has_lookup_cache() || self.objects.len() <= 16 {
self.objects.retain(|id| doc.node(*id).is_some());
} else {
+31
View File
@@ -410,6 +410,25 @@ fn baseline_prune(s: &mut Selection, doc: &Document) {
if s.key.is_some() || s.objects.is_empty() || s.key_guide.is_some_and(|k| !s.guides.contains(&k)) {
s.key_guide = None;
}
// A picked symbol part stays only while its instance is selected alone, with no guides, and
// the part is still editable in a dynamic symbol's art.
let editable_part = |art: &crate::Node, path: &[u32]| {
crate::symbol_overrides::part(art, path).is_some_and(crate::symbol_overrides::takes_part_paint)
&& (0..=path.len()).all(|k| crate::symbol_overrides::part(art, &path[..k]).is_some_and(|n| n.visible && !n.locked && !n.is_template()))
};
let part_kept = s.symbol_part.as_ref().is_some_and(|sp| {
s.objects == [sp.instance]
&& s.guides.is_empty()
&& found.get(&sp.instance).and_then(|p| doc.node_at(p)).is_some_and(|n| match &n.kind {
NodeKind::SymbolInstance { symbol, .. } => {
doc.symbols.iter().find(|x| x.name == *symbol).is_some_and(|x| x.dynamic && editable_part(&x.art, &sp.path))
}
_ => false,
})
});
if !part_kept {
s.symbol_part = None;
}
}
#[test]
@@ -457,5 +476,17 @@ fn prune_matches_baseline_for_mixed_objects_anchors_and_guides() {
let mut actual_empty = empty_sel.clone();
actual_empty.prune(&d);
assert_eq!(actual_empty, expected_empty, "empty selection mismatch on seed {seed}");
// 4. A picked symbol part must not slip through the object-only and empty fast paths.
for objects in [obj_only.objects.clone(), Vec::new()] {
let mut with_part = obj_only.clone();
with_part.objects = objects;
with_part.set_symbol_part(with_part.objects.first().copied().unwrap_or(NodeId(9999)), vec![0]);
let mut expected_part = with_part.clone();
baseline_prune(&mut expected_part, &d);
let mut actual_part = with_part.clone();
actual_part.prune(&d);
assert_eq!(actual_part, expected_part, "symbol part selection mismatch on seed {seed}");
}
}
}