Keep hidden export roots from enlarging the exported image

Fixes #447

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
NetterSilver
2026-10-15 11:48:39 +02:00
co-authored by Claude Haiku 5.5
parent 8482c3e85b
commit dee13aefc5
2 changed files with 59 additions and 3 deletions
+12 -3
View File
@@ -765,12 +765,21 @@ pub fn export_bytes(doc: &Document, ids: &[NodeId], format: ExportFormat, scale:
}
/// The page area exporting `ids` with `setting` covers: what they paint (a clipping frame's own
/// area), or their boxes with "include bounding box" and for slices.
/// area), or their boxes with "include bounding box" and for slices. Hidden layers paint nothing,
/// so they take no room next to visible ones (a hidden-only export keeps its own area).
pub fn export_area(doc: &Document, ids: &[NodeId], setting: &ExportSetting) -> Option<figcraft_geom::Rect> {
let any_visible = ids.iter().any(|i| doc.node(*i).is_some_and(|n| n.visible));
ids.iter()
.filter_map(|i| {
let slice = doc.node(*i).is_some_and(|n| matches!(n.kind, NodeKind::Slice));
if setting.include_bounding_box || slice { doc.world_bounds(*i) } else { figcraft_render::export_bounds(doc, *i) }
let node = doc.node(*i);
let slice = node.is_some_and(|n| matches!(n.kind, NodeKind::Slice));
if setting.include_bounding_box || slice {
doc.world_bounds(*i)
} else if any_visible && node.is_some_and(|n| !n.visible) {
None
} else {
figcraft_render::export_bounds(doc, *i)
}
})
.reduce(|a, b| a.union(b))
}
+47
View File
@@ -0,0 +1,47 @@
//! Hidden top-level layers in an export take no room in the image (issue #447).
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod common;
use common::{Client, png_dims};
use serde_json::{Value, json};
fn dimensions(result: Value) -> (u32, u32) {
assert_eq!(result["isError"], false, "{result}");
let image = result["content"].as_array().unwrap().iter().find(|block| block["type"] == "image").unwrap();
png_dims(image["data"].as_str().unwrap())
}
#[test]
fn hidden_root_does_not_expand_export() {
let mut client = Client::new();
let visible = client.create(json!({
"type":"frame", "x":0, "y":0, "width":100, "height":100,
"props":{"fill":"#FF0000"}
}));
let hidden = client.create(json!({
"type":"frame", "x":900, "y":0, "width":300, "height":300,
"props":{"visible":false}
}));
assert_eq!(client.node(&hidden)["visible"], false);
let control = dimensions(client.call("export", json!({"ids":[visible], "format":"png", "size":"200w"})));
assert_eq!(control, (200, 200));
let mixed = dimensions(client.call("export", json!({"ids":[visible, hidden], "format":"png", "size":"200w"})));
assert_eq!(mixed, control, "hidden roots must not enlarge the painted area");
let natural = dimensions(client.call("export", json!({"ids":[visible, hidden], "format":"png"})));
assert_eq!(natural, (100, 100));
client.ok("select", json!({"ids":[visible, hidden]}));
let selected = dimensions(client.call("export", json!({"format":"png", "size":"200w"})));
assert_eq!(selected, (200, 200));
client.ok("select", json!({"ids":[]}));
let page = dimensions(client.call("export", json!({"format":"png"})));
assert_eq!(page, (100, 100));
// Controls: the bounding-box option and a hidden-only export keep their areas.
let boxed = dimensions(client.call("export", json!({"ids":[visible, hidden], "format":"png", "includeBoundingBox":true})));
assert_eq!(boxed, (1200, 300));
let alone = dimensions(client.call("export", json!({"ids":[hidden], "format":"png"})));
assert_eq!(alone, (300, 300));
}