From b8a217ebba1d7bb7bba9732a33ffe45fd5bd6d4c Mon Sep 17 00:00:00 2001 From: BFlat Date: Sat, 10 Oct 2026 06:22:28 +0700 Subject: [PATCH] M1: pixel preview (#43) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit View ▸ Pixel preview now shows the canvas as it would export at 1x when zoomed in: the view is rendered one pixel per page unit from a whole-pixel origin and magnified without smoothing, so anti-aliasing and sub-pixel edges show as they really are (and line up with the pixel grid). Co-authored-by: Claude Opus 5.5 --- ROADMAP.md | 2 +- crates/ui-egui/src/canvas.rs | 29 +++++++++++++++++++++++------ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 7017de2..0a0b59c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -150,7 +150,7 @@ Scores by behaviour, 0–100%. Hours are what remains (one agent). | Area | Weight | Score | Remaining work | Hours | |---|---|---|---|---| -| Canvas & navigation | 6 | 65% | pixel preview, frame guides, perf at scale | 6–10 | +| Canvas & navigation | 6 | 72% | frame guides, 2x pixel preview | 3–5 | | Selection & transform | 8 | 64% | scale tool polish, measure hover polish | 8–12 | | Layers panel | 5 | 76% | keyboard row navigation, drag between pages | 2–4 | | Shapes & vector | 8 | 63% | variable width, multi-point transform box, point snapping, live boolean editing, arc handles on canvas, shape builder on strokes and open paths | 10–15 | diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 8bf14fd..69d45f8 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -139,9 +139,15 @@ pub fn show(app: &mut FigApp, ui: &mut Ui, worker: &mut Worker) { if let Some(done) = worker.poll() && done.generation == app.canvas.generation { + // A pixel preview render is magnified with hard pixel edges. + let filter = if done.view.scale == 1.0 && app.session.ds().is_ok_and(pixel_preview) { + egui::TextureOptions::NEAREST + } else { + egui::TextureOptions::LINEAR + }; match &mut app.canvas.texture { - Some(t) => t.set(done.image, egui::TextureOptions::LINEAR), - None => app.canvas.texture = Some(ctx.load_texture("figcraft-canvas", done.image, egui::TextureOptions::LINEAR)), + Some(t) => t.set(done.image, filter), + 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); @@ -205,13 +211,24 @@ pub fn show(app: &mut FigApp, ui: &mut Ui, worker: &mut Worker) { } } +/// Pixel preview is on and zoomed in past one screen pixel per page pixel. +fn pixel_preview(ds: &figcraft_engine::DocState) -> bool { + ds.view.pixel_preview && ds.view.zoom > 1.0 +} + fn request_render(app: &mut FigApp, worker: &mut Worker, ppp: f32) { let Ok(ds) = app.session.ds() else { return }; let rect = app.canvas.rect; - let scale = ds.view.zoom * f64::from(ppp); - let (ox, oy) = ds.view.origin(); - let w = (rect.width() * ppp).ceil().max(1.0) as u32; - let h = (rect.height() * ppp).ceil().max(1.0) as u32; + let (mut ox, mut oy) = ds.view.origin(); + let (scale, w, h) = if pixel_preview(ds) { + // Pixel preview: render one pixel per page unit from a whole-pixel origin, shown magnified + // without smoothing. + (ox, oy) = (ox.floor(), oy.floor()); + let z = ds.view.zoom.max(1e-9); + (1.0, (f64::from(rect.width()) / z).ceil() as u32 + 2, (f64::from(rect.height()) / z).ceil() as u32 + 2) + } else { + (ds.view.zoom * f64::from(ppp), (rect.width() * ppp).ceil().max(1.0) as u32, (rect.height() * ppp).ceil().max(1.0) as u32) + }; // The edited text is drawn live (an IME composition included); the caret is an overlay. let doc = app.preedit_view().map(|p| p.doc.clone()).unwrap_or_else(|| ds.doc.clone()); let key = JobKey {