From ad9894a90fabf079a23aea1afdd7c0dcc8f092b7 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Wed, 4 Nov 2026 04:31:52 +0000 Subject: [PATCH] fix(doc): give render bounds a visit budget so a repeating child list ends --- crates/doc/src/lib.rs | 18 +++++++++++------- crates/doc/src/tests.rs | 16 ++++++++++++++++ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs index 0181ffd21..f3ee25c53 100644 --- a/crates/doc/src/lib.rs +++ b/crates/doc/src/lib.rs @@ -760,16 +760,20 @@ impl Document { /// children only within their mask). A group, or an unclipped frame past its box, is what its /// children show grown by its effects, which apply to all of it. pub fn world_render_bounds(&self, id: NodeId) -> Option { - self.render_bounds_at(id, 0) + // A tree visits each node once; a child list deserialized with repeats (a graph) is cut off + // after twice the document's node count rather than walked in full. + self.render_bounds_at(id, 0, &std::cell::Cell::new(self.nodes.len().saturating_mul(2).saturating_add(8))) } - fn render_bounds_at(&self, id: NodeId, depth: usize) -> Option { + fn render_bounds_at(&self, id: NodeId, depth: usize, left: &std::cell::Cell) -> Option { 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()) || depth >= MAX_RENDER_DEPTH { + let visit = left.get(); + left.set(visit.saturating_sub(1)); + if (n.clip && n.kind.is_frame_like() && !n.kind.bounds_follow_children()) || depth >= MAX_RENDER_DEPTH || visit == 0 { return Some(own); } - Some(match (self.children_render_bounds(n, depth + 1), &n.kind) { + Some(match (self.children_render_bounds(n, depth + 1, left), &n.kind) { (Some(kids), NodeKind::Group) => n.grow_by_effects(kids), // An unclipped frame's effects apply to its children too, past its box included. (Some(kids), _) => own.union(n.grow_by_effects(kids)), @@ -778,15 +782,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, depth: usize) -> Option { + fn children_render_bounds(&self, n: &Node, depth: usize, left: &std::cell::Cell) -> Option { let shown = |i: &NodeId| self.node(*i).is_some_and(|c| c.visible && !c.is_mask); let mut out: Option = 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.render_bounds_at(*i, depth)).reduce(|a, b| a.union(b)); + let r = ids.iter().filter(|i| shown(i)).filter_map(|i| self.render_bounds_at(*i, depth, left)).reduce(|a, b| a.union(b)); let r = match mask { - 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), + Some(m) => r.zip(self.render_bounds_at(m, depth, left)).map(|(a, b)| a.intersect(b)).filter(|x| x.width() > 0.0 && x.height() > 0.0), None => r, }; out = match (out, r) { diff --git a/crates/doc/src/tests.rs b/crates/doc/src/tests.rs index a5c8e8e7a..75de3541e 100644 --- a/crates/doc/src/tests.rs +++ b/crates/doc/src/tests.rs @@ -582,6 +582,22 @@ fn is_ancestor_holds_for_very_deep_chains() { assert!(matches!(d.reparent(root, leaf, None), Err(DocError::Cycle(..)))); } +#[test] +fn render_bounds_of_a_doubled_child_list_end_at_once() { + // A file can list a child twice at every level: 2^40 visits if each is walked. + let mut d = Document::new("x"); + let mut parent = page(&d); + let mut groups = Vec::new(); + for _ in 0..40 { + parent = d.create(parent, NodeKind::Group, "G", 0.0, 0.0, 10.0, 10.0).unwrap(); + groups.push(parent); + } + for w in groups.windows(2) { + d.node_mut(w[0]).unwrap().children.push(w[1]); + } + assert!(d.world_render_bounds(groups[0]).is_some()); +} + #[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