perf(canvas): render coarse zoom previews before refinement (M8)

A new zoom can't reuse the retained canvas image, and the previous one,
re-projected, covers little of the view after a zoom out (1/64 of it
from 800% to 100%), so the page background showed until the full render
finished.

When renders from scratch have been slow (a frame or more), the worker
first renders the whole requested view with a quarter of the pixels
each way, in a Retained of its own so pans and edits still build on the
full image, then refines it unless a newer request is already waiting
(that one gets its own preview). The web build renders the preview and
the refined image in consecutive frames. Pixel preview gets none.

Finished images carry the file, page and whether they are coarse: the
canvas never shows an image of another file or page, and a late preview
never replaces a refined image already at the zoom asked for. ui.inspect
reports the canvas quality and the latency from request to the frame
showing the preview and the refined image.

Refs #295
This commit is contained in:
tylerjenningsw
2026-10-14 11:08:55 -04:00
parent 833f0d2c27
commit 4c23cb71c0
6 changed files with 413 additions and 26 deletions
+52 -7
View File
@@ -11,16 +11,26 @@ use serde_json::json;
use crate::FigApp;
use crate::i18n::{tr, trf, trn};
use crate::render_worker::{Job, Worker};
use crate::render_worker::{Done, Job, Worker};
use crate::theme::{ACCENT, COMPONENT, Tokens, doc_c32, regular, semibold};
pub struct CanvasState {
pub rect: Rect,
texture: Option<egui::TextureHandle>,
tex_view: Option<figcraft_render::View>,
/// The file ([`figcraft_engine::DocState::uid`]) and page the texture shows: it is drawn only
/// on them.
tex_of: Option<(u64, NodeId)>,
/// The texture is a coarse preview (see [`crate::render_worker`]).
pub tex_coarse: bool,
submitted: Option<JobKey>,
/// The scale (device pixels per page unit) of the last render asked for.
want_scale: f64,
/// What the last canvas render had to do (shown by `ui.inspect`).
pub render_work: Option<figcraft_render::Work>,
/// From asking for a render to the frame that shows its preview, and its refined image (ms).
pub preview_latency_ms: Option<f64>,
pub refine_latency_ms: Option<f64>,
dragging: bool,
space_pan: bool,
middle_pan: Option<(Pos2, f64, f64)>,
@@ -45,8 +55,13 @@ impl Default for CanvasState {
rect: Rect::NOTHING,
texture: None,
tex_view: None,
tex_of: None,
tex_coarse: false,
submitted: None,
want_scale: 0.0,
render_work: None,
preview_latency_ms: None,
refine_latency_ms: None,
dragging: false,
space_pan: false,
middle_pan: None,
@@ -169,10 +184,22 @@ pub fn show(app: &mut FigApp, ui: &mut Ui, worker: &mut Worker) {
let own_cursor = input(app, ui, &resp);
crate::live::track_pointer(app, &ctx);
app.sync_preedit();
request_render(app, worker, ppp);
let now = ctx.input(|i| i.time);
request_render(app, worker, ppp, now);
// Every finished render is shown, even when a newer one is already on its way: renders finish
// in order, and an edit every frame (a drag) would otherwise never show until it stopped.
if let Some(done) = worker.poll() {
let shown = app.session.ds().map(|ds| (ds.uid, ds.page)).ok();
if let Some(done) = worker.poll()
&& shows(&app.canvas, &done, shown)
{
let latency = Some((now - done.at).max(0.0) * 1000.0);
if done.coarse {
app.canvas.preview_latency_ms = latency;
} else {
app.canvas.refine_latency_ms = latency;
app.canvas.render_work = Some(done.work);
app.ui.render_ms = done.ms;
}
// A pixel preview render is magnified with hard pixel edges.
let filter = if app.session.ds().ok().and_then(|d| d.view.pixel_preview_scale()) == Some(done.view.scale) {
egui::TextureOptions::NEAREST
@@ -184,8 +211,8 @@ pub fn show(app: &mut FigApp, ui: &mut Ui, worker: &mut Worker) {
None => app.canvas.texture = Some(ctx.load_texture("figcraft-canvas", done.image, filter)),
}
app.canvas.tex_view = Some(done.view);
app.canvas.render_work = Some(done.work);
app.ui.render_ms = done.ms;
app.canvas.tex_of = Some((done.uid, done.page));
app.canvas.tex_coarse = done.coarse;
}
let painter = ui.painter_at(rect);
let t = Tokens::get(&ctx);
@@ -200,7 +227,7 @@ pub fn show(app: &mut FigApp, ui: &mut Ui, worker: &mut Worker) {
})
.unwrap_or(t.canvas);
painter.rect_filled(rect, CornerRadius::ZERO, bg);
if let (Some(tex), Some(v)) = (&app.canvas.texture, &app.canvas.tex_view) {
if let (Some(tex), Some(v), true) = (&app.canvas.texture, &app.canvas.tex_view, shown.is_some() && app.canvas.tex_of == shown) {
// Re-project the last render onto the current view.
let tl = app.to_screen(v.origin.0, v.origin.1);
let zoom = app.session.ds().map(|d| d.view.zoom).unwrap_or(1.0);
@@ -268,7 +295,17 @@ fn describe_box([x0, y0, x1, y1]: [f64; 4]) -> String {
)
}
fn request_render(app: &mut FigApp, worker: &mut Worker, ppp: f32) {
/// Whether a finished render goes on screen: an image of the file and page shown (`shown`), and
/// not a coarse preview over a refined image already at the zoom asked for.
fn shows(c: &CanvasState, done: &Done, shown: Option<(u64, NodeId)>) -> bool {
if shown != Some((done.uid, done.page)) {
return false;
}
let refined_here = !c.tex_coarse && c.tex_of == shown && c.tex_view.as_ref().is_some_and(|v| v.scale.to_bits() == c.want_scale.to_bits());
!(done.coarse && refined_here)
}
fn request_render(app: &mut FigApp, worker: &mut Worker, ppp: f32, now: f64) {
let Ok(ds) = app.session.ds() else { return };
let rect = app.canvas.rect;
let (mut ox, mut oy) = ds.view.origin();
@@ -313,7 +350,11 @@ fn request_render(app: &mut FigApp, worker: &mut Worker, ppp: f32) {
skip: vec![],
solo: key.solo,
proof,
uid: ds.uid,
zoom_preview: ds.view.pixel_preview_scale().is_none(),
at: now,
};
app.canvas.want_scale = scale;
worker.submit(job);
app.canvas.submitted = Some(key);
}
@@ -1342,3 +1383,7 @@ pub fn fmt(v: f64) -> String {
let r = (v * 100.0).round() / 100.0;
if (r - r.round()).abs() < 1e-9 { format!("{}", r.round() as i64) } else { format!("{r}") }
}
#[cfg(test)]
#[path = "zoom_preview_tests.rs"]
mod zoom_preview_tests;
+3
View File
@@ -62,6 +62,9 @@ fn big_job(doc: &Arc<figcraft_doc::Document>, page: figcraft_doc::NodeId, scale:
skip: vec![],
solo: None,
proof: false,
uid: 0,
zoom_preview: false,
at: 0.0,
}
}
+3
View File
@@ -212,6 +212,9 @@ fn a_finished_background_render_wakes_the_ui() {
skip: vec![],
solo: None,
proof: false,
uid: 0,
zoom_preview: false,
at: 0.0,
});
let start = Instant::now();
while wakes.load(Ordering::SeqCst) == 0 {
+3
View File
@@ -1181,6 +1181,9 @@ impl FigApp {
figcraft_render::Work::Partial { pixels } => json!({"kind": "partial", "pixels": pixels}),
figcraft_render::Work::Full => json!({"kind": "full"}),
}),
"quality": if self.canvas.tex_coarse { "preview" } else { "refined" },
"previewLatencyMs": self.canvas.preview_latency_ms,
"refineLatencyMs": self.canvas.refine_latency_ms,
},
"live": live::inspect(self),
"window": self.windows.current,
+133 -19
View File
@@ -3,6 +3,11 @@
//! until the next one arrives.
//! The renderer keeps its last image ([`figcraft_render::Retained`]): a pan renders only the
//! uncovered strips and an edit only the areas of the layers that changed.
//! A new zoom can't reuse that image, and the old one, re-projected, covers little of the view
//! after a zoom out (an eighth of the width from 800% to 100%). When full renders are slow, the
//! whole view is first rendered coarsely ([`PREVIEW_FACTOR`] times fewer pixels each way, kept
//! apart from the full image so pans and edits still build on it), then refined unless a newer
//! request is already waiting.
use std::sync::Arc;
@@ -10,6 +15,11 @@ use figcraft_color::ColorProfile;
use figcraft_doc::{Document, NodeId};
use figcraft_render::{Pixmap, Retained, View, Work};
/// A preview has this many times fewer pixels each way than the view it stands in for.
pub const PREVIEW_FACTOR: f64 = 4.0;
/// Full renders quicker than this (about a frame) show without a preview first.
const PREVIEW_AFTER_MS: f64 = 12.0;
#[derive(Clone)]
pub struct Job {
pub doc: Arc<Document>,
@@ -25,6 +35,13 @@ pub struct Job {
pub solo: Option<NodeId>,
/// Soft-proof sRGB: stripe the colours sRGB can't show.
pub proof: bool,
/// The document's [`figcraft_engine::DocState::uid`]: images of other files are never shown.
pub uid: u64,
/// A coarse preview may come first when the zoom changes (not in pixel preview, whose pixels
/// are the point).
pub zoom_preview: bool,
/// When the UI asked for it (`egui` input time, seconds): how long the image took to show.
pub at: f64,
}
pub struct Done {
@@ -34,6 +51,13 @@ pub struct Done {
pub view: View,
pub ms: f64,
pub work: Work,
/// What it is an image of ([`Job::uid`], [`Job::page`]).
pub uid: u64,
pub page: NodeId,
/// A coarse preview, refined by a later image unless a newer request replaced it.
pub coarse: bool,
/// [`Job::at`].
pub at: f64,
}
/// A render of a document in `profile` as a texture image for the screen (sRGB).
@@ -58,7 +82,8 @@ fn mark_outside_srgb(image: &mut egui::ColorImage, pix: &Pixmap, profile: ColorP
}
}
fn run(job: &Job, retained: &mut Retained) -> Option<Done> {
/// Render `view` of the job (its own view, or the coarse one standing in for it).
fn run(job: &Job, view: &View, retained: &mut Retained, coarse: bool) -> Option<Done> {
#[cfg(not(target_arch = "wasm32"))]
let t0 = std::time::Instant::now();
// The canvas is a preview: what's too small to see in detail at this zoom is approximated.
@@ -71,7 +96,7 @@ fn run(job: &Job, retained: &mut Retained) -> Option<Done> {
lod: true,
..Default::default()
};
let (view, pix, work) = retained.render(&job.doc, job.page, &job.view, &opts)?;
let (view, pix, work) = retained.render(&job.doc, job.page, view, &opts)?;
let mut image = screen_image(pix, job.doc.color_profile);
if job.proof {
mark_outside_srgb(&mut image, pix, job.doc.color_profile);
@@ -80,7 +105,80 @@ fn run(job: &Job, retained: &mut Retained) -> Option<Done> {
let ms = t0.elapsed().as_secs_f64() * 1000.0;
#[cfg(target_arch = "wasm32")]
let ms = 0.0;
Some(Done { image, view, ms, work })
Some(Done { image, view, ms, work, uid: job.uid, page: job.page, coarse, at: job.at })
}
/// `view` with [`PREVIEW_FACTOR`] times fewer pixels each way, showing at least the same area.
pub fn coarse_view(view: &View) -> View {
let shrink = |n: u32| (f64::from(n) / PREVIEW_FACTOR).ceil().max(1.0) as u32;
View { origin: view.origin, scale: view.scale / PREVIEW_FACTOR, width: shrink(view.width), height: shrink(view.height) }
}
/// What the render thread keeps between jobs: the canvas image, the previews standing in for it
/// while the zoom changes, and what the last full render drew and took.
pub struct Renders {
full: Retained,
preview: Retained,
/// The last full render's document, page and scale.
last: Option<(u64, NodeId, f64)>,
/// How long the last render from scratch took.
full_ms: f64,
/// Previews come first only when renders from scratch take at least this long.
preview_after_ms: f64,
}
impl Default for Renders {
fn default() -> Self {
Self::with_threshold(PREVIEW_AFTER_MS)
}
}
impl Renders {
/// Previews when renders from scratch take at least `ms` (tests: 0, always).
pub fn with_threshold(ms: f64) -> Self {
Self { full: Retained::default(), preview: Retained::default(), last: None, full_ms: 0.0, preview_after_ms: ms }
}
/// Whether `job` gets a preview first: the kept image is of another zoom, page or file (so the
/// full render starts from scratch) and those have been slow. The first render has no
/// measure yet and comes without one.
fn wants_preview(&self, job: &Job) -> bool {
job.zoom_preview
&& self.full_ms >= self.preview_after_ms
&& self.last.is_some_and(|(uid, page, scale)| uid != job.uid || page != job.page || scale.to_bits() != job.view.scale.to_bits())
}
/// The coarse preview of the whole of `job`'s view, when one should come first.
pub fn preview(&mut self, job: &Job) -> Option<Done> {
if !self.wants_preview(job) {
return None;
}
let view = coarse_view(&job.view);
guarded(&mut self.preview, |r| run(job, &view, r, true))
}
/// The image of `job`'s view, built on the kept one where it can be.
pub fn refine(&mut self, job: &Job) -> Option<Done> {
let done = guarded(&mut self.full, |r| run(job, &job.view, r, false));
if let Some(d) = &done {
self.last = Some((job.uid, job.page, job.view.scale));
if d.work == Work::Full {
self.full_ms = d.ms;
}
}
done
}
}
/// Run `f` on `retained`; after a panic, forget the image it may have left half drawn.
fn guarded(retained: &mut Retained, f: impl FnOnce(&mut Retained) -> Option<Done>) -> Option<Done> {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(retained))) {
Ok(d) => d,
Err(_) => {
retained.clear();
None
}
}
}
#[cfg(not(target_arch = "wasm32"))]
@@ -104,22 +202,26 @@ impl Worker {
// The window that renders with it (each window has its own worker).
let viewport = ctx.viewport_id();
let spawned = std::thread::Builder::new().name("figcraft-render".into()).spawn(move || {
let mut retained = Retained::default();
while let Ok((mut seq, mut job)) = jobs.recv() {
let mut renders = Renders::default();
let mut next = None;
loop {
let Some((mut seq, mut job)) = next.take().or_else(|| jobs.recv().ok()) else { break };
// Skip to the newest queued job.
while let Ok(newer) = jobs.try_recv() {
(seq, job) = newer;
}
let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| run(&job, &mut retained)));
let done = match res {
Ok(d) => d,
Err(_) => {
// Don't build on an image a panic may have left half drawn.
retained.clear();
None
if let Some(d) = renders.preview(&job) {
if done_tx.send(d).is_err() {
break;
}
};
if let Some(d) = done
ctx.request_repaint_of(viewport);
// A newer request makes refining this one moot: it gets its own preview.
if let Ok(newer) = jobs.try_recv() {
next = Some(newer);
continue;
}
}
if let Some(d) = renders.refine(&job)
&& done_tx.send(d).is_err()
{
break;
@@ -155,30 +257,42 @@ impl Worker {
}
}
/// Renders inline, one image a frame: a preview first when one is due, the refined image in the
/// next frame unless a newer request replaced the job.
#[cfg(target_arch = "wasm32")]
pub struct Worker {
ctx: egui::Context,
pending: Option<Job>,
retained: Retained,
previewed: bool,
renders: Renders,
}
#[cfg(target_arch = "wasm32")]
impl Worker {
pub fn new(_ctx: egui::Context) -> Self {
Self { pending: None, retained: Retained::default() }
pub fn new(ctx: egui::Context) -> Self {
Self { ctx, pending: None, previewed: false, renders: Renders::default() }
}
pub fn submit(&mut self, job: Job) {
self.pending = Some(job);
self.previewed = false;
}
pub fn in_flight(&self) -> bool {
self.pending.is_some()
}
pub fn poll(&mut self) -> Option<Done> {
if !self.previewed {
self.previewed = true;
if let Some(d) = self.pending.as_ref().and_then(|job| self.renders.preview(job)) {
self.ctx.request_repaint();
return Some(d);
}
}
let job = self.pending.take()?;
run(&job, &mut self.retained)
self.renders.refine(&job)
}
}
/// Render synchronously from scratch (tests, screenshots).
pub fn render_now(job: &Job) -> Option<Done> {
run(job, &mut Retained::default())
run(job, &job.view, &mut Retained::default(), false)
}
+219
View File
@@ -0,0 +1,219 @@
//! Coarse previews while the zoom changes (#295): a new zoom first shows the whole view coarsely,
//! then the refined image, which matches a render from scratch; pans and edits still build on the
//! kept image; previews never replace a refined image already at the zoom asked for, and no image
//! of another file or page goes on screen.
use std::sync::Arc;
use figcraft_doc::{Document, NodeId};
use figcraft_engine::Session;
use figcraft_engine::params::id_of;
use figcraft_render::{View, Work};
use serde_json::json;
use super::{CanvasState, shows};
use crate::render_worker::{Done, Job, PREVIEW_FACTOR, Renders, coarse_view, render_now};
/// A page of ellipses with a rectangle on top; the session and the rectangle.
fn document() -> (Session, NodeId) {
let mut s = Session::new();
for k in 0..12 {
let k = f64::from(k);
s.execute("shape.ellipse", &json!({"x": k * 60.0, "y": k * 40.0, "width": 120, "height": 90})).unwrap();
}
let made = s.execute("shape.rectangle", &json!({"x": 100, "y": 50, "width": 300, "height": 200})).unwrap();
let rect = id_of(&made["id"]).unwrap();
(s, rect)
}
/// A `w` × `h` device-pixel view at `scale` centred on page point `c`.
fn view_at(c: (f64, f64), scale: f64, w: u32, h: u32) -> View {
View { origin: (c.0 - f64::from(w) / scale / 2.0, c.1 - f64::from(h) / scale / 2.0), scale, width: w, height: h }
}
fn job(doc: &Arc<Document>, page: NodeId, uid: u64, view: View) -> Job {
Job {
doc: doc.clone(),
page,
view,
outline: false,
outline_extras: Default::default(),
outlined: vec![],
skip: vec![],
solo: None,
proof: false,
uid,
zoom_preview: true,
at: 0.0,
}
}
/// Every pixel of `a` and `b` alike.
fn same_pixels(a: &Done, b: &Done) -> bool {
a.image.size == b.image.size && a.image.pixels == b.image.pixels
}
fn covers(outer: &View, inner: &View) -> bool {
let (o, i) = (outer.page_rect(), inner.page_rect());
let eps = 1e-6;
o.x0 <= i.x0 + eps && o.y0 <= i.y0 + eps && o.x1 >= i.x1 - eps && o.y1 >= i.y1 - eps
}
#[test]
fn a_zoom_out_previews_the_whole_view_then_refines_to_a_fresh_render() {
let (s, _) = document();
let ds = s.ds().unwrap();
let mut r = Renders::with_threshold(0.0);
let zoomed_in = job(&ds.doc, ds.page, ds.uid, view_at((300.0, 200.0), 8.0, 400, 300));
assert!(r.preview(&zoomed_in).is_none(), "the first render has nothing to measure previews by");
assert!(r.refine(&zoomed_in).is_some());
// 800% → 100% about the same centre: the old image covers 1/64 of the new view.
let out = job(&ds.doc, ds.page, ds.uid, view_at((300.0, 200.0), 1.0, 400, 300));
let p = r.preview(&out).expect("a preview first");
assert!(p.coarse && (p.uid, p.page) == (ds.uid, ds.page));
assert!(covers(&p.view, &out.view), "the preview shows the whole view asked for");
let pixels = |d: &Done| d.image.size[0] * d.image.size[1];
let fresh = render_now(&out).unwrap();
assert!(
(pixels(&p) as f64) * PREVIEW_FACTOR * PREVIEW_FACTOR < 1.5 * pixels(&fresh) as f64,
"a sixteenth of the pixels, give or take the snapping"
);
let refined = r.refine(&out).unwrap();
assert!(!refined.coarse && refined.work == Work::Full);
assert!(same_pixels(&refined, &fresh), "the settled image is a render from scratch");
}
/// Zoom, pan and edit with `r`; whether each step was previewed, and the pan's and the edit's
/// images.
fn zoom_pan_edit(r: &mut Renders) -> (Vec<bool>, Done, Done) {
let (mut s, rect) = document();
let (doc, page, uid) = {
let ds = s.ds().unwrap();
(ds.doc.clone(), ds.page, ds.uid)
};
r.refine(&job(&doc, page, uid, view_at((300.0, 200.0), 4.0, 1280, 960))).unwrap();
let mut previewed = vec![];
// A zoom; the preview is kept apart from the full image.
let zoomed = job(&doc, page, uid, view_at((300.0, 200.0), 2.0, 1280, 960));
previewed.push(r.preview(&zoomed).is_some());
r.refine(&zoomed).unwrap();
// A pan at that zoom.
let mut pan = zoomed;
pan.view.origin.0 += 10.0;
previewed.push(r.preview(&pan).is_some());
let panned = r.refine(&pan).unwrap();
// An edit there.
s.execute("node.set", &json!({"id": rect, "props": {"x": 120}})).unwrap();
let edit = job(&s.ds().unwrap().doc, page, uid, pan.view);
previewed.push(r.preview(&edit).is_some());
let edited = r.refine(&edit).unwrap();
(previewed, panned, edited)
}
#[test]
fn pans_and_edits_get_no_preview_and_build_on_the_kept_image() {
let (previewed, panned, edited) = zoom_pan_edit(&mut Renders::with_threshold(0.0));
assert_eq!(previewed, [true, false, false], "the zoom only");
assert!(matches!(panned.work, Work::Partial { .. }), "a pan after a previewed zoom renders strips only: {:?}", panned.work);
assert!(matches!(edited.work, Work::Partial { .. }), "an edit renders its area only: {:?}", edited.work);
// The same steps without previews: the same work and the same pixels.
let (none, panned_alone, edited_alone) = zoom_pan_edit(&mut Renders::with_threshold(f64::INFINITY));
assert_eq!(none, [false, false, false]);
assert_eq!((panned.work, edited.work), (panned_alone.work, edited_alone.work));
assert!(same_pixels(&panned, &panned_alone) && same_pixels(&edited, &edited_alone));
}
#[test]
fn previews_come_for_other_pages_and_files_but_not_in_pixel_preview_or_when_renders_are_quick() {
let (mut s, _) = document();
let (doc, page, uid) = {
let ds = s.ds().unwrap();
(ds.doc.clone(), ds.page, ds.uid)
};
let v = view_at((300.0, 200.0), 1.0, 200, 150);
let mut r = Renders::with_threshold(0.0);
r.refine(&job(&doc, page, uid, v.clone())).unwrap();
s.execute("page.add", &json!({})).unwrap();
let ds = s.ds().unwrap();
assert!(r.preview(&job(&ds.doc, ds.page, uid, v.clone())).is_some(), "another page");
assert!(r.preview(&job(&doc, page, uid + 1, v.clone())).is_some(), "another file");
let zoom = view_at((300.0, 200.0), 2.0, 200, 150);
let mut pixel_preview = job(&doc, page, uid, zoom.clone());
pixel_preview.zoom_preview = false;
assert!(r.preview(&pixel_preview).is_none(), "pixel preview shows real pixels only");
let mut quick = Renders::with_threshold(f64::INFINITY);
quick.refine(&job(&doc, page, uid, v)).unwrap();
assert!(quick.preview(&job(&doc, page, uid, zoom)).is_none(), "quick renders show without one");
}
#[test]
fn previews_cover_the_view_for_any_zoom_and_density() {
let (s, _) = document();
let ds = s.ds().unwrap();
let mut r = Renders::with_threshold(0.0);
r.refine(&job(&ds.doc, ds.page, ds.uid, view_at((0.0, 0.0), 0.37, 64, 64))).unwrap();
let mut n = 0;
// Zooms from 2% to 6400% at 1x, 1.5x and 2x displays, odd sizes, centres off the pixel grid.
for zoom in [0.02, 0.1, 0.333, 1.0, 2.5, 8.0, 64.0] {
for ppp in [1.0, 1.5, 2.0] {
for (w, h) in [(1, 1), (3, 7), (401, 299), (1280, 720)] {
let v = view_at((123.456, -78.9), zoom * ppp, w, h);
let c = coarse_view(&v);
assert!(covers(&c, &v), "coarse view of {v:?}");
let p = r.preview(&job(&ds.doc, ds.page, ds.uid, v.clone())).expect("a preview");
assert!(covers(&p.view, &v), "preview of {v:?} shows {:?}", p.view);
n += 1;
}
}
}
assert_eq!(n, 84);
}
#[test]
fn outline_mode_proofing_and_glass_preview_and_settle_like_fresh_renders() {
let (mut s, rect) = document();
s.execute("effect.add", &json!({"ids": [rect], "type": "GLASS"})).unwrap();
let ds = s.ds().unwrap();
for (outline, proof) in [(false, false), (true, false), (false, true)] {
let mut r = Renders::with_threshold(0.0);
let mut first = job(&ds.doc, ds.page, ds.uid, view_at((250.0, 150.0), 3.0, 300, 200));
(first.outline, first.proof) = (outline, proof);
r.refine(&first).unwrap();
let mut next = first.clone();
next.view = view_at((250.0, 150.0), 0.75, 300, 200);
let p = r.preview(&next).expect("a preview");
assert!(p.coarse && covers(&p.view, &next.view));
let d = r.refine(&next).unwrap();
assert!(same_pixels(&d, &render_now(&next).unwrap()), "outline {outline}, proof {proof}");
}
}
#[test]
fn the_canvas_shows_images_of_its_own_page_and_never_a_coarser_one_at_its_zoom() {
let (s, _) = document();
let ds = s.ds().unwrap();
let here = Some((ds.uid, ds.page));
let mut r = Renders::with_threshold(0.0);
let near = job(&ds.doc, ds.page, ds.uid, view_at((300.0, 200.0), 2.0, 200, 150));
let refined = r.refine(&near).unwrap();
let far = job(&ds.doc, ds.page, ds.uid, view_at((300.0, 200.0), 0.5, 200, 150));
let preview = r.preview(&far).unwrap();
let mut c = CanvasState { want_scale: near.view.scale, ..Default::default() };
assert!(shows(&c, &refined, here));
assert!(!shows(&c, &refined, Some((ds.uid + 1, ds.page))), "another file");
assert!(!shows(&c, &refined, Some((ds.uid, NodeId(ds.page.0 + 1)))), "another page");
assert!(!shows(&c, &refined, None), "no file open");
// Showing the refined image at the zoom asked for: a preview arriving late stays off.
(c.tex_view, c.tex_of, c.tex_coarse) = (Some(refined.view.clone()), here, false);
assert!(!shows(&c, &preview, here));
// Once the zoom asked for moves on, the preview is the better match.
c.want_scale = far.view.scale;
assert!(shows(&c, &preview, here));
// Over another preview, or a refined image of another page, it goes on.
c.want_scale = near.view.scale;
c.tex_coarse = true;
assert!(shows(&c, &preview, here));
(c.tex_coarse, c.tex_of) = (false, Some((ds.uid, NodeId(ds.page.0 + 1))));
assert!(shows(&c, &preview, here));
}