Files
vectorcraft/crates/doc/src/assets.rs
T
arhammusheerandClaude Opus 5.5 4b260c364d #1172: edit a large selection in time linear in the document, not quadratic
Every lookup by id walked the whole tree, and the edit path did one per selected id, so editing
a selection of n objects cost O(n²). Measured with the new `perf --deep` rows at 25,000 paths:

- Select All: 458 → 3.5 ms
- nudge the selection: 3,517 → 34 ms
- drag it with the Selection tool, per pointer move: 3,413 → 35 ms
- undo / redo: 407 → 3.1 ms

- vectorcraft_doc: Document::index_paths (every id's index path, in one walk; the first in tree
  order for a duplicated id, as index_path), node_at, node_mut_at (copy-on-write, as node_mut)
  and ancestry_at.
- apply_transform, roots_of, transform_patterns, doc_sane, Selection::prune and
  Document::paint_order look their ids up with one walk instead of one each.
- Selection::set dedups with a hash set instead of Vec::contains per id.
- vectorcraft-cli perf --deep: tracked rows for editing a whole selection (select, nudge, drag,
  undo, redo), live effects, every Pathfinder operation, typing in a long story, and memory.

tests_lookups checks the batched lookups against the one-id ones on nested trees with missing and
duplicated ids; storing the last duplicate instead of the first fails it. The doc and engine
suites (2,000 tests) pass unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmMmc1J8XeNLxhRihWv9cc
2026-10-11 23:26:53 +07:00

60 lines
2.4 KiB
Rust

//! Asset Export (Window → Asset Export, Object → Collect for Export): pieces of art collected to
//! export on their own, at the export settings Export for Screens shares.
//!
//! An asset names objects rather than copying them: it follows edits (a moved object moves the
//! crop, a recoloured one exports recoloured) and lets go of objects that are deleted
//! ([`Document::prune_assets`]; an asset left with none goes too). Asset ids come from the
//! document's id counter, so they never repeat within a document.
use serde::{Deserialize, Serialize};
use crate::{Document, NodeId};
/// One asset: what it is called (its files' names) and the objects it exports, cropped to their
/// visual bounds.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ExportAsset {
pub id: u64,
pub name: String,
pub nodes: Vec<NodeId>,
}
/// What the assets are named when they aren't named after their object: `Asset 1`, `Asset 2`…
const ASSET: &str = "Asset ";
impl Document {
/// Asset `id`.
pub fn asset(&self, id: u64) -> Option<&ExportAsset> {
self.assets.iter().find(|a| a.id == id)
}
/// The name of the next unnamed asset: one more than the highest `Asset N` so far.
pub fn next_asset_name(&self) -> String {
let n = self.assets.iter().filter_map(|a| a.name.strip_prefix(ASSET)?.parse::<u64>().ok()).max().unwrap_or(0);
format!("{ASSET}{}", n.saturating_add(1))
}
/// Forget the objects of assets that are no longer in the document, and the assets left with
/// none (after every edit).
pub fn prune_assets(&mut self) {
if self.assets.iter().all(|a| a.nodes.iter().all(|id| self.node(*id).is_some())) {
return;
}
let mut assets = std::mem::take(&mut self.assets);
for a in &mut assets {
a.nodes.retain(|id| self.node(*id).is_some());
}
assets.retain(|a| !a.nodes.is_empty());
self.assets = assets;
}
/// `ids` that are in the document, back to front.
pub fn paint_order(&self, ids: impl IntoIterator<Item = NodeId>) -> Vec<NodeId> {
let ids: Vec<NodeId> = ids.into_iter().collect();
let paths = self.index_paths(ids.iter().copied());
let mut v: Vec<(Vec<usize>, NodeId)> = ids.into_iter().filter_map(|id| paths.get(&id).map(|p| (p.clone(), id))).collect();
v.sort();
v.into_iter().map(|(_, id)| id).collect()
}
}