mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-28 04:16:46 -06:00
- Compare changes: a ready-for-dev frame or section keeps a snapshot of itself from just before
its first change (Document::dev_snapshots, saved only while changed; dropped on resolve, re-mark,
other status or delete). `dev.compare {id?, show?}` diffs it layer by layer: added/removed
layers, and property changes formatted like Inspect (`width 120 → 136`, `fill #FF0000 →
#E03030`, text, parent, order, component, auto layout, typography, effects). The compare view
shows before / now side by side or overlaid with a slider, changed layers outlined, the list of
changes and Resolve change.
- Focus view: `dev.focus {id? | direction}` / `dev.unfocus`: one frame drawn and hit alone
(render Options::solo), a bar with name, status, Compare changes, previous / next ready frame and
exit; Escape leaves.
- Annotation categories: defaults plus the file's own with colours (`annotation.categories`,
`addCategory`, `editCategory`, `removeCategory`), `category` on annotations, coloured callouts,
a category filter and editor in the Inspect panel; stored backwards-compatibly.
- Shared widgets::icon_button / canvas_banner / dropdown_lead; has_status moved to the engine.
- Tests: devcompare_tests, devfocus_tests, annotation_categories_tests, doc handoff_tests, render
solo_tests, UI dev_mode_tests.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
79 lines
3.1 KiB
Rust
79 lines
3.1 KiB
Rust
//! Handoff snapshots: what a ready-for-dev frame or section looked like before it changed, so Dev
|
|
//! Mode can compare it with what it is now.
|
|
//!
|
|
//! A snapshot is taken (by the engine) the moment a layer marked Ready for dev first changes, from
|
|
//! the document as it was just before that edit, and dropped when the change is resolved or the
|
|
//! layer is marked again. It holds the layer and everything under it; the nodes are shared with the
|
|
//! undo history in memory, so taking one costs little. Files without snapshots load unchanged.
|
|
|
|
use std::collections::BTreeSet;
|
|
use std::sync::Arc;
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::{Document, Node, NodeId};
|
|
|
|
/// Most layers a snapshot holds (a larger frame isn't snapshotted and can't be compared).
|
|
pub const MAX_SNAPSHOT_NODES: usize = 20_000;
|
|
|
|
/// A layer and everything under it, in pre-order (the layer first), as it was.
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
pub struct DevSnapshot {
|
|
pub nodes: Vec<Arc<Node>>,
|
|
}
|
|
|
|
impl DevSnapshot {
|
|
/// The layer the snapshot is of.
|
|
pub fn root(&self) -> Option<&Node> {
|
|
self.nodes.first().map(Arc::as_ref)
|
|
}
|
|
|
|
/// The snapshot's nodes by id.
|
|
pub fn node(&self, id: NodeId) -> Option<&Node> {
|
|
self.nodes.iter().find(|n| n.id == id).map(Arc::as_ref)
|
|
}
|
|
}
|
|
|
|
impl Document {
|
|
/// Layer `root` and everything under it as they are now (`None` when it doesn't exist or holds
|
|
/// more than [`MAX_SNAPSHOT_NODES`] layers).
|
|
pub fn snapshot_subtree(&self, root: NodeId) -> Option<DevSnapshot> {
|
|
let ids = self.subtree(root);
|
|
if ids.is_empty() || ids.len() > MAX_SNAPSHOT_NODES {
|
|
return None;
|
|
}
|
|
Some(DevSnapshot { nodes: ids.iter().filter_map(|i| self.nodes.get(i).cloned()).collect() })
|
|
}
|
|
|
|
/// A copy of this document in which layer `root` is as its snapshot shows it (`None` without a
|
|
/// usable snapshot). Only `root`'s subtree is meant to be read from the copy: layers moved out
|
|
/// of it since are back inside, and layers added to it since are gone.
|
|
pub fn with_dev_snapshot(&self, root: NodeId) -> Option<Document> {
|
|
let snap = self.dev_snapshots.get(&root)?;
|
|
if snap.root().map(|n| n.id) != Some(root) || !self.contains(root) {
|
|
return None;
|
|
}
|
|
let mut d = self.clone();
|
|
let kept: BTreeSet<NodeId> = snap.nodes.iter().map(|n| n.id).collect();
|
|
for id in self.subtree(root) {
|
|
if !kept.contains(&id) {
|
|
d.nodes.remove(&id);
|
|
}
|
|
}
|
|
for n in snap.nodes.iter().take(MAX_SNAPSHOT_NODES) {
|
|
// Pages are never part of a snapshot (a damaged file could claim otherwise).
|
|
if n.is_page() || self.pages.contains(&n.id) {
|
|
continue;
|
|
}
|
|
d.nodes.insert(n.id, n.clone());
|
|
}
|
|
// The copy's root keeps its current place in the page.
|
|
if let (Some(now), Some(r)) = (self.node(root), d.nodes.get_mut(&root)) {
|
|
Arc::make_mut(r).parent = now.parent;
|
|
}
|
|
d.dev_snapshots.clear();
|
|
d.dirty = crate::Dirty::default();
|
|
Some(d)
|
|
}
|
|
}
|