Files
vectorcraft/crates/doc/src/selection.rs
T
BFlatandClaude Opus 5.5 27a1a48176 Layers: sublayers are layers, not objects, and every row has a command
Art in a sublayer is now selected as objects (clicks, marquee, Select All, the
Select menu), sublayers select in their own colour, and hidden, locked or
template sublayers block clicks. Layer Options gain Preview (per-layer outline)
and Dim Images. New and fixed layer.* commands for the Layers panel: setCurrent
and highlight (rows at any depth), selectAll, move (reparent with cycle and
nesting checks, Alt copies), delete/duplicate rows, merge, flatten, collect,
release to layers, reverse, template, hide/outline/lock others and their
show/preview/unlock all, locate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 22:29:15 +07:00

158 lines
5.3 KiB
Rust

//! Selection state (objects and, for direct selection, individual anchors).
use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
use crate::{Document, NodeId};
/// (subpath index, anchor index) inside a path.
pub type AnchorRef = (usize, usize);
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct Selection {
/// Selected objects in selection order.
pub objects: Vec<NodeId>,
/// Direct-selected anchors per path. A path in `objects` with no entry here is fully selected.
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub anchors: BTreeMap<NodeId, BTreeSet<AnchorRef>>,
/// Key object for Align.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub key: Option<NodeId>,
/// The object, group or layer targeted through its target circle in the Layers panel, which
/// appearance, transparency and opacity-mask commands then act on (a targeted layer has its
/// art selected). Any other selection change clears it.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target: Option<NodeId>,
/// Slices selected with the Slice Selection tool: user slice ids and the ids of objects whose
/// object slice is selected (see [`crate::slices`]). Any change to the object selection
/// clears them.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub slices: Vec<NodeId>,
}
impl Selection {
pub fn is_empty(&self) -> bool {
self.objects.is_empty()
}
pub fn len(&self) -> usize {
self.objects.len()
}
pub fn contains(&self, id: NodeId) -> bool {
self.objects.contains(&id)
}
pub fn clear(&mut self) {
self.objects.clear();
self.anchors.clear();
self.key = None;
self.target = None;
self.slices.clear();
}
pub fn set(&mut self, ids: impl IntoIterator<Item = NodeId>) {
self.clear();
for id in ids {
self.add(id);
}
}
pub fn add(&mut self, id: NodeId) {
self.target = None;
self.slices.clear();
if !self.objects.contains(&id) {
self.objects.push(id);
}
}
pub fn remove(&mut self, id: NodeId) {
self.target = None;
self.slices.clear();
self.objects.retain(|x| *x != id);
self.anchors.remove(&id);
if self.key == Some(id) {
self.key = None;
}
}
/// Select slices `ids` (see [`Selection::slices`]) and nothing else.
pub fn set_slices(&mut self, ids: impl IntoIterator<Item = NodeId>) {
self.clear();
for id in ids {
if !self.slices.contains(&id) {
self.slices.push(id);
}
}
}
pub fn toggle(&mut self, id: NodeId) {
if self.contains(id) { self.remove(id) } else { self.add(id) }
}
/// Are some (not all) anchors of `id` direct-selected?
pub fn partial(&self, id: NodeId) -> Option<&BTreeSet<AnchorRef>> {
self.anchors.get(&id)
}
/// Drop ids that no longer exist or are no longer editable.
pub fn prune(&mut self, doc: &Document) {
self.objects.retain(|id| doc.node(*id).is_some());
self.anchors.retain(|id, _| doc.node(*id).is_some());
if self.key.is_some_and(|k| doc.node(k).is_none()) {
self.key = None;
}
if self.target.is_some_and(|t| doc.node(t).is_none()) {
self.target = None;
}
self.slices.retain(|id| doc.is_slice(*id));
}
/// Target `id` (see [`Selection::target`]): a layer gets its visible, unlocked art selected
/// (the art of its sublayers too), anything else is selected itself.
pub fn set_target(&mut self, doc: &Document, id: NodeId) {
match doc.node(id) {
Some(n) if n.is_layer() => self.set(n.layer_art(true)),
_ => self.set([id]),
}
self.target = Some(id);
}
/// What appearance and transparency edits act on: the targeted object, group or layer, else
/// the selected objects.
pub fn subjects(&self) -> &[NodeId] {
match &self.target {
Some(t) => std::slice::from_ref(t),
None => &self.objects,
}
}
/// Top-level ordering: selected ids sorted by paint order (bottom first).
pub fn in_paint_order(&self, doc: &Document) -> Vec<NodeId> {
doc.paint_order(self.objects.iter().copied())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_ops() {
let mut s = Selection::default();
s.add(NodeId(1));
s.add(NodeId(1));
s.toggle(NodeId(2));
assert_eq!(s.objects, vec![NodeId(1), NodeId(2)]);
s.toggle(NodeId(1));
assert_eq!(s.objects, vec![NodeId(2)]);
s.clear();
assert!(s.is_empty());
}
#[test]
fn selection_changes_clear_the_target() {
let mut s = Selection::default();
s.set([NodeId(2), NodeId(3)]);
s.target = Some(NodeId(1));
assert_eq!(s.subjects(), &[NodeId(1)]);
s.add(NodeId(4));
assert_eq!(s.target, None);
assert_eq!(s.subjects(), &[NodeId(2), NodeId(3), NodeId(4)]);
s.target = Some(NodeId(1));
s.remove(NodeId(2));
assert_eq!(s.target, None);
s.target = Some(NodeId(1));
s.set([NodeId(3)]);
assert_eq!(s.target, None);
}
}