From dde193cbf44ebe1bdd74af043ed728a7cd17d9c3 Mon Sep 17 00:00:00 2001 From: ahmed-226 Date: Wed, 14 Oct 2026 09:36:52 +0300 Subject: [PATCH] M7: a slice's export preview shows the page content under it A slice paints nothing itself, so thumbnail() rendered an empty pixmap of the slice's own box: expanding Preview for a slice showed only the transparency checkerboard while exporting the same slice wrote the artwork correctly. Give the preview the export's slice rule (drawn_layers + export_area in cmd/file.rs): draw every top-level layer of the slice's page over its world bounds. Regression tests cover a slice over artwork, a slice with nothing under it, and a plain layer. Signed-off-by: Ahmed Mahmoud --- crates/ui-egui/src/assets_panel.rs | 20 +++++++-- crates/ui-egui/src/export_preview_tests.rs | 52 ++++++++++++++++++++++ crates/ui-egui/src/lib.rs | 2 + 3 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 crates/ui-egui/src/export_preview_tests.rs diff --git a/crates/ui-egui/src/assets_panel.rs b/crates/ui-egui/src/assets_panel.rs index 37485568..5ea19375 100644 --- a/crates/ui-egui/src/assets_panel.rs +++ b/crates/ui-egui/src/assets_panel.rs @@ -190,15 +190,27 @@ fn same_layers(a: &Document, b: &Document, id: NodeId) -> bool { } pub(crate) fn thumbnail(ctx: &egui::Context, doc: &Document, id: NodeId, px: f32) -> Option { - let b = figcraft_render::export_bounds(doc, id)?; + let pix = thumbnail_pixmap(doc, id, px)?; + let img = crate::render_worker::screen_image(&pix, doc.color_profile); + Some(ctx.load_texture(format!("asset-thumb-{}", id.0), img, egui::TextureOptions::LINEAR)) +} + +/// The pixels a thumbnail shows. A slice paints nothing itself, so it shows the page content +/// under it, exactly as exporting one does (`figcraft_engine::cmd::file::drawn_layers`): without +/// this a slice's preview is a blank checkerboard next to a correct export. +pub(crate) fn thumbnail_pixmap(doc: &Document, id: NodeId, px: f32) -> Option { + let slice = doc.node(id).is_some_and(|n| matches!(n.kind, NodeKind::Slice)); + let b = if slice { doc.world_bounds(id)? } else { figcraft_render::export_bounds(doc, id)? }; let side = b.width().max(b.height()); if !(side.is_finite() && side > 0.0) { return None; } let scale = (f64::from(px) / side).min(2.0); - let pix = figcraft_render::render_node(doc, id, scale)?; - let img = crate::render_worker::screen_image(&pix, doc.color_profile); - Some(ctx.load_texture(format!("asset-thumb-{}", id.0), img, egui::TextureOptions::LINEAR)) + if slice { + let layers = doc.page_of(id).and_then(|p| doc.node(p)).map(|p| p.children.clone()).unwrap_or_default(); + return figcraft_render::render_area(doc, &layers, b, scale); + } + figcraft_render::render_node(doc, id, scale) } fn matches(name: &str, q: &str) -> bool { diff --git a/crates/ui-egui/src/export_preview_tests.rs b/crates/ui-egui/src/export_preview_tests.rs new file mode 100644 index 00000000..3d536a01 --- /dev/null +++ b/crates/ui-egui/src/export_preview_tests.rs @@ -0,0 +1,52 @@ +//! The Export section's Preview row: a slice paints nothing itself, so its preview must show the +//! page content under it — the same thing exporting the slice writes — and not a blank +//! transparency checkerboard next to a correct export. + +use figcraft_doc::{Document, NodeId}; +use figcraft_engine::Session; +use serde_json::{Value, json}; + +use crate::assets_panel::thumbnail_pixmap; + +/// Run `commands`, returning the document and the id each one made. +fn doc_after(commands: &[(&str, Value)]) -> (Document, Vec) { + let mut s = Session::new(); + let ids = commands.iter().map(|(cmd, params)| NodeId(s.execute(cmd, params).unwrap()["id"].as_u64().unwrap())).collect(); + (s.doc().unwrap().clone(), ids) +} + +/// Whether the pixmap paints anything (any pixel not fully transparent). +fn paints(pix: &figcraft_render::Pixmap) -> bool { + figcraft_render::to_rgba8(pix).as_chunks::<4>().0.iter().any(|p| p.get(3).copied().unwrap_or(0) > 0) +} + +#[test] +fn a_slice_previews_the_content_under_it() { + let (doc, ids) = doc_after(&[ + ("shape.rectangle", json!({"x": 0, "y": 0, "width": 200, "height": 100})), + ("node.create", json!({"type": "slice", "x": 0, "y": 0, "width": 200, "height": 100})), + ]); + let slice = ids[1]; + let pix = thumbnail_pixmap(&doc, slice, 200.0).expect("the slice previews"); + assert_eq!([pix.width(), pix.height()], [200, 100], "the slice's area, not an empty node"); + assert!(paints(&pix), "the rectangle under the slice shows"); +} + +#[test] +fn a_slice_with_nothing_under_it_is_blank() { + let (doc, ids) = doc_after(&[("node.create", json!({"type": "slice", "x": 0, "y": 0, "width": 200, "height": 100}))]); + let pix = thumbnail_pixmap(&doc, ids[0], 200.0).expect("the preview still has the slice's area"); + assert_eq!([pix.width(), pix.height()], [200, 100]); + assert!(!paints(&pix), "nothing under it stays transparent"); +} + +#[test] +fn other_layers_preview_themselves() { + let (doc, ids) = doc_after(&[ + ("shape.rectangle", json!({"x": 0, "y": 0, "width": 200, "height": 100})), + ("node.create", json!({"type": "slice", "x": 0, "y": 0, "width": 200, "height": 100})), + ]); + let pix = thumbnail_pixmap(&doc, ids[0], 200.0).expect("the rectangle previews"); + assert_eq!([pix.width(), pix.height()], [200, 100], "the layer's own area"); + assert!(paints(&pix)); +} diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 613f18f9..0da9a576 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -78,6 +78,8 @@ mod cursors_tests; #[cfg(test)] mod dev_mode_tests; #[cfg(test)] +mod export_preview_tests; +#[cfg(test)] mod field_tests; #[cfg(test)] mod frame_label_tests;