M1: pixel preview (#43)

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 <noreply@anthropic.com>
This commit is contained in:
BFlat 2026-10-10 06:22:28 +07:00 committed by GitHub
parent d0a21d2a7b
commit b8a217ebba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 24 additions and 7 deletions

View File

@ -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 |

View File

@ -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 {