Merge pull request #1275 from Ridzzz0Alam/fix/1062-render-bounds-depth

fix: Issue (#1062) render bounds stop at the depth the renderer draws.
This commit is contained in:
BFlat
2026-10-26 10:25:45 +07:00
committed by GitHub
2 changed files with 31 additions and 5 deletions
+13 -5
View File
@@ -44,6 +44,10 @@ use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::sync::Arc;
/// Deepest descendant [`Document::world_render_bounds`] follows: as deep as the renderer draws
/// (`figcraft_render::MAX_EXPORT_DEPTH`), so a crafted chain can't overflow the stack (#1062).
const MAX_RENDER_DEPTH: usize = 256;
/// Stable node identifier, unique within a document.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
@@ -703,12 +707,16 @@ impl Document {
/// a clipping frame's own area, else that and what its visible children show (masked
/// children only within their mask). A group is what its children show, grown by its effects.
pub fn world_render_bounds(&self, id: NodeId) -> Option<Rect> {
self.render_bounds_at(id, 0)
}
fn render_bounds_at(&self, id: NodeId, depth: usize) -> Option<Rect> {
let n = self.node(id)?;
let own = n.own_paint_bounds(self.world_transform(id));
if n.clip && n.kind.is_frame_like() && !n.kind.bounds_follow_children() {
if (n.clip && n.kind.is_frame_like() && !n.kind.bounds_follow_children()) || depth >= MAX_RENDER_DEPTH {
return Some(own);
}
Some(match (self.children_render_bounds(n), &n.kind) {
Some(match (self.children_render_bounds(n, depth + 1), &n.kind) {
(Some(kids), NodeKind::Group) => n.grow_by_effects(kids),
(Some(kids), _) => own.union(kids),
(None, _) => own,
@@ -716,15 +724,15 @@ impl Document {
}
/// What `n`'s visible children show, together: each mask's run within the mask's bounds.
fn children_render_bounds(&self, n: &Node) -> Option<Rect> {
fn children_render_bounds(&self, n: &Node, depth: usize) -> Option<Rect> {
let shown = |i: &NodeId| self.node(*i).is_some_and(|c| c.visible && !c.is_mask);
let mut out: Option<Rect> = None;
for run in self.paint_runs(n) {
let ids = n.children.get(run).unwrap_or(&[]);
let mask = ids.first().copied().filter(|m| self.node(*m).is_some_and(|c| c.is_mask && c.visible));
let r = ids.iter().filter(|i| shown(i)).filter_map(|i| self.world_render_bounds(*i)).reduce(|a, b| a.union(b));
let r = ids.iter().filter(|i| shown(i)).filter_map(|i| self.render_bounds_at(*i, depth)).reduce(|a, b| a.union(b));
let r = match mask {
Some(m) => r.zip(self.world_render_bounds(m)).map(|(a, b)| a.intersect(b)).filter(|x| x.width() > 0.0 && x.height() > 0.0),
Some(m) => r.zip(self.render_bounds_at(m, depth)).map(|(a, b)| a.intersect(b)).filter(|x| x.width() > 0.0 && x.height() > 0.0),
None => r,
};
out = match (out, r) {
+18
View File
@@ -560,6 +560,24 @@ fn is_ancestor_holds_for_very_deep_chains() {
assert!(matches!(d.reparent(root, leaf, None), Err(DocError::Cycle(..))));
}
#[test]
fn render_bounds_stop_where_drawing_stops_on_very_deep_chains() {
// A crafted file nests groups far deeper than anything draws (#1062). The leaf counts while
// it's within the renderer's depth and is left out past it, as the renderer leaves it out.
let chain = |depth: usize| {
let mut d = Document::new("x");
let mut parent = page(&d);
for _ in 0..depth {
parent = d.create(parent, NodeKind::Group, "G", 0.0, 0.0, 10.0, 10.0).unwrap();
}
d.create(parent, NodeKind::Rectangle, "R", 100.0, 100.0, 10.0, 10.0).unwrap();
let top = d.node(page(&d)).unwrap().children[0];
d.world_render_bounds(top).unwrap()
};
assert_eq!(chain(MAX_RENDER_DEPTH).x1, 110.0);
assert_eq!(chain(20_000).x1, 10.0);
}
#[test]
fn text_insert_at_run_boundary_keeps_runs_disjoint() {
let mut t = TextData::new("0123456789");