mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-28 11:47:21 -06:00
Window > Asset Export lists the document's assets as thumbnails: art
dragged in off the canvas (or added with +) becomes one asset per object,
or one of them all with Alt; Object > Collect for Export does the same.
Assets name their objects, so they follow edits (moving the art moves the
crop) and deleting the art removes it from its asset. Click selects,
double-click renames, the bin removes. The panel's export settings are
Export for Screens' own format rows on the document's shared settings, and
Export writes each asset alone, cropped to its art, into a picked folder
(the web downloads a zip). Export for Screens' Assets tab lists the assets
with checkboxes and exports the checked ones.
Commands: assets.add, assets.remove, assets.rename, assets.list,
assets.export, assets.settings.set; document.exportForScreens takes
assets. Model: Document.assets (ExportAsset {id, name, nodes}).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
158 lines
5.3 KiB
Rust
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,
|
|
/// 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.children().into_iter().flatten().filter(|c| c.visible && !c.locked).map(|c| c.id)),
|
|
_ => 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);
|
|
}
|
|
}
|