mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-25 10:56:48 -06:00
Large documents stay interactive: the canvas re-renders only what a pan uncovers or an edit touches, the layers panel lays out only the rows in view, and per-frame work no longer walks the whole page. - Benchmark: `cargo xtask bench [SIZES]` builds synthetic pages (`figcraft_engine::synthetic`: screens with an auto layout header, a card group, button instances and shapes/text) at 1k/10k/50k layers and prints engine/render timings (`figcraft-cli perf --layers 1000,10000`) and headless whole-UI frame timings (an ignored ui-egui test). - Retained canvas rendering (`figcraft_render::Retained`, used by the render worker): views snap to whole device pixels, so a pan shifts the previous image in place and renders only the uncovered strips; an edit renders only where changed layers were and are (`Document::changed_ids` diffs the two versions skipping shared structure; ancestors that paint from their children's pixels - booleans, effects, masks - count whole; background blur reach and noise are allowed for). Zoom, page, options, font or page-level changes, or large changes, render the whole view as before. `ui.inspect` reports `canvas.renderWork`. - The texture image is converted on the render thread, not the UI thread. - Layers panel: only visible rows are laid out (stable ids while scrolling); rows are cached per document version, page, expanded set and filter. - Frame labels and layout grids of off-screen top-level frames are skipped; the Prototype tab's connection sources are cached per document version. - Text: the layout cache keys by a cheap fingerprint confirmed by `TextData` equality (no Debug formatting per lookup, 32k entries) and hands out shared layouts; a layout generation marks renders stale when fonts arrive. Overflowing text is culled by its glyphs, so it no longer vanishes while its box is off screen. Before -> after (release, Windows, 1440x900 view, ms; 10k / 50k layers): - move one layer, re-render @100%: 7.65 / 6.49 -> 0.18 / 0.81 - move one layer, re-render zoomed out: 11.95 / 24.10 -> 0.16 / 0.77 - pan 40 px @100%: 7.53 / 6.82 -> 0.69 / 0.81 - pan 40 px zoomed out: 12.07 / 24.30 -> 1.62 / 4.17 - full render @100% / zoomed to fit: 7.78 / 44.22 -> 6.56 / 38.83 (10k @100%, 50k fit) - UI frame, every screen expanded in layers: 6.16 / 39.07 -> 0.24 / 0.57 - UI frame, scrolling that panel: 6.11 / 40.18 -> 0.43 / 0.40 - UI frame, layers filtered by name: 1.03 / 6.92 -> 0.17 / 0.36 - UI frame, Prototype tab: 0.89 / 6.45 -> 0.18 / 0.80 - UI frame, idle: 0.31 / 1.43 -> 0.11 / 0.39 Unchanged and within budget: single-layer edits through the engine ~0.01 ms, normalize after a one-node edit < 0.01 ms, hit test 26 / 93 us. Left: save/load at 50k (~0.7 s each), zoomed-out renders at 50k (~40 ms). Tests: retained renders match full renders after pans, moves, recolours, hide/remove/add, undo, masks, overflowing text, edits under a background blur and an inner-shadow/noise layer, a drop- shadowed group, and a blur panned in from the view's edge; zoom/page/options/page background render full; junk views don't panic. Layer-row and prototype-source caches follow edits, undo, page switches, expansion and the filter. `changed_ids`, the text cache, and the perf CLI. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
28 lines
1.4 KiB
Rust
28 lines
1.4 KiB
Rust
//! `Document::changed_ids`: what differs between two versions of a document.
|
|
|
|
use crate::{Document, NodeKind};
|
|
|
|
#[test]
|
|
fn changed_ids_lists_added_removed_and_edited_nodes() {
|
|
let mut a = Document::new("a");
|
|
let page = a.first_page().unwrap();
|
|
let keep = a.create(page, NodeKind::Rectangle, "Keep", 0.0, 0.0, 10.0, 10.0).unwrap();
|
|
let edit = a.create(page, NodeKind::Rectangle, "Edit", 0.0, 0.0, 10.0, 10.0).unwrap();
|
|
let gone = a.create(page, NodeKind::Rectangle, "Gone", 0.0, 0.0, 10.0, 10.0).unwrap();
|
|
assert_eq!(a.changed_ids(&a, 10), Some(vec![]), "a version against itself");
|
|
let mut b = a.clone();
|
|
assert_eq!(b.changed_ids(&a, 10), Some(vec![]), "a copy shares everything");
|
|
b.get_mut(edit).unwrap().opacity = 0.5;
|
|
b.remove(gone).unwrap();
|
|
let new = b.create(page, NodeKind::Ellipse { start: 0.0, sweep: 1.0, inner: 0.0 }, "New", 0.0, 0.0, 5.0, 5.0).unwrap();
|
|
// The page changed too (its children).
|
|
assert_eq!(b.changed_ids(&a, 10), Some(vec![page, edit, gone, new]));
|
|
assert_eq!(a.changed_ids(&b, 10), Some(vec![page, edit, gone, new]), "symmetric");
|
|
assert!(!b.changed_ids(&a, 10).unwrap().contains(&keep));
|
|
// A node copied on write but equal in value isn't a change.
|
|
let mut c = a.clone();
|
|
let _ = c.get_mut(keep).unwrap();
|
|
assert_eq!(c.changed_ids(&a, 10), Some(vec![]));
|
|
assert_eq!(b.changed_ids(&a, 3), None, "more than the limit");
|
|
}
|