Files
figcraft/crates/render/src/retained_edit_tests.rs
T
BFlatandClaude Opus 5.5 4904303fc2 M8: retained canvas correctness (#89)
* M8: retained canvas correctness

Drags now show live on the canvas. The canvas used to drop every finished
render whose request had been superseded, and it asks for the next render
before it polls. So while an edit lands every frame (any drag), no image was
ever shown until the pointer was released. Holding the button still made it
worse: the same move ran the gesture again each frame, built a new document
and asked for a new render, which kept the canvas stuck on the pre-drag image.

- Canvas: show every finished render (renders finish in order; the image is
  re-projected to the current view). The generation filter is gone.
- Engine: Session::pointer skips a drag move identical to the last one, and
  Session::preview keeps the document when the preview changes nothing (a
  handle held still).
- Render: clip and layer rectangles snap edges that are within float noise of
  a pixel boundary before rounding outwards. An edge exactly on a boundary came
  out a hair to either side depending on the render's origin, so a clip let a
  whole column through in some renders and not others.
- Tests: a seeded random walk compares the retained render with a full render
  after every step: edits, undo/redo, reparenting, masks, groups, effects,
  strokes, text, auto layout, instances, variables, styles, pans and zooms. It
  found the clip-edge divergence; a minimised regression test covers it. Also
  a headless UI test for a live drag, and engine tests for skipped repeated
  moves and unchanged previews.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M8: retained canvas tests follow the new blur effect type

Layer blurs are `LayerBlur(Blur)` since the progressive blur landed; the random
walk now also tries progressive layer blurs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:40:26 +07:00

68 lines
3.4 KiB
Rust

//! Retained renders after edits inside transformed and clipping containers: every render must match
//! a full render of the same view (the engine's random walk drives whole documents through it).
use std::sync::Arc;
use figcraft_color::{Color, Paint};
use figcraft_doc::{Blur, Document, Effect, EffectKind, NodeId, NodeKind};
use figcraft_geom::Affine;
use crate::retained_tests::assert_same;
use crate::{Options, Retained, View, Work, render_view};
fn check(r: &mut Retained, doc: &Arc<Document>, page: NodeId, v: &View, what: &str) {
let (shown, pix, work) = r.render(doc, page, v, &Options::default()).unwrap();
let pix = pix.clone();
let full = render_view(doc, page, &shown, &Options::default()).unwrap();
assert_same(&pix, &full, &format!("{what} ({work:?})"));
}
#[test]
fn moving_a_child_of_a_rotated_frame() {
let mut d = Document::new("rot");
let page = d.first_page().unwrap();
let f = d.create(page, NodeKind::Frame, "F", 100.0, 100.0, 300.0, 200.0).unwrap();
d.get_mut(f).unwrap().transform = figcraft_geom::set_rotation_about_center(d.node(f).unwrap().transform, 300.0, 200.0, 25.0);
let c = d.create(f, NodeKind::Rectangle, "C", 40.0, 40.0, 80.0, 60.0).unwrap();
d.get_mut(c).unwrap().fills = vec![Paint::solid(Color::from_hex("E03030").unwrap())];
let doc = Arc::new(d);
let v = View { origin: (0.0, 0.0), scale: 1.0, width: 500, height: 450 };
let mut r = Retained::default();
check(&mut r, &doc, page, &v, "first");
let mut d2 = (*doc).clone();
let pw = d2.world_transform(f);
let n = d2.get_mut(c).unwrap();
n.transform = pw.inverse() * Affine::translate((150.0, 60.0)) * pw * n.transform;
check(&mut r, &Arc::new(d2), page, &v, "moved");
}
/// A clipping frame whose right edge lands exactly on a pixel boundary at 40 %: float noise in
/// one render's origin and not another's put the edge a hair past the boundary, and the clip,
/// rounded outwards, let a whole column of the child through.
#[test]
fn a_clip_edge_on_a_pixel_boundary_clips_the_same_in_every_render() {
let mut d = Document::new("clip");
let page = d.first_page().unwrap();
let f = d.create(page, NodeKind::Frame, "F", 212.0, 198.0, 220.0, 70.0).unwrap();
d.get_mut(f).unwrap().fills = vec![Paint::solid(Color::from_hex("FFE0B0").unwrap())];
let c = d.create(f, NodeKind::Rectangle, "C", 58.0, 10.0, 40.0, 40.0).unwrap();
d.get_mut(c).unwrap().fills = vec![Paint::solid(Color::from_hex("3070E0").unwrap())];
d.get_mut(c).unwrap().effects.push(Effect { kind: EffectKind::LayerBlur(Blur { radius: 3.0, ..Blur::default() }), visible: true });
let doc = Arc::new(d);
// The frame's new right edge (x 280) is device pixel 172 exactly.
let v = View { origin: (-150.0, 132.8), scale: 0.4, width: 320, height: 240 };
for lod in [false, true] {
let opts = Options { lod, ..Default::default() };
let mut r = Retained::default();
r.render(&doc, page, &v, &opts).unwrap();
let mut smaller = (*doc).clone();
smaller.get_mut(f).unwrap().width = 68.0;
smaller.get_mut(f).unwrap().height = 54.0;
let smaller = Arc::new(smaller);
let (shown, pix, work) = r.render(&smaller, page, &v, &opts).unwrap();
assert!(matches!(work, Work::Partial { .. }), "{work:?}");
let pix = pix.clone();
assert_same(&pix, &render_view(&smaller, page, &shown, &opts).unwrap(), &format!("lod {lod}"));
}
}