From 51bdb28e2acd825882c7de519e9d1a65442b423c Mon Sep 17 00:00:00 2001 From: Nixon <43715558+nixonyh@users.noreply.github.com> Date: Fri, 5 Jun 2026 15:30:05 +0800 Subject: [PATCH 1/2] Skip positioned subtrees only when parent translation changed Gate the early-return in `propagate_translation` on whether the parent's world translation actually changed, and track per-node whether the translation changed before recursing into children. This avoids incorrectly skipping subtrees whose ancestor moved while still pruning work when nothing changed. --- crates/rectree/src/lib.rs | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/crates/rectree/src/lib.rs b/crates/rectree/src/lib.rs index 1f4a6d4..856f935 100644 --- a/crates/rectree/src/lib.rs +++ b/crates/rectree/src/lib.rs @@ -228,7 +228,13 @@ pub fn layout< .and_then(|node| node.parent_id) .and_then(|parent| nodes.get_node(&parent)) .map_or(Vec2::ZERO, |node| node.world_translation); - propagate_translation(tree, nodes, &bubbled_id, parent_world); + propagate_translation( + tree, + nodes, + &bubbled_id, + parent_world, + false, + ); } /// Propagates a constraint top-down through the subtree rooted @@ -417,25 +423,36 @@ pub fn propagate_translation< nodes: &mut N, id: &T::Id, parent_world: Vec2, + parent_translation_changed: bool, ) { let node = nodes .get_node(id) .expect("propagate_translation: Id is invalid!"); // Already up-to-date; skip this entire subtree. - if node.state.is_positioned() { + if !parent_translation_changed && node.state.is_positioned() { return; } let world = parent_world + node.translation; + let mut translation_changed = false; if let Some(n) = nodes.get_node_mut(id) { - n.world_translation = world; + translation_changed = n.world_translation != world; + if translation_changed { + n.world_translation = world; + } n.state.has_repositioned(); } tree.for_each_child(id, nodes, |child, nodes| { - propagate_translation(tree, nodes, child, world); + propagate_translation( + tree, + nodes, + child, + world, + translation_changed, + ); }); } @@ -598,6 +615,7 @@ mod tests { &mut wt.nodes, &0, Vec2::ZERO, + false, ); assert_eq!(wt.nodes.0[&0].world_translation, Vec2::ZERO); @@ -621,6 +639,7 @@ mod tests { &mut wt.nodes, &0, Vec2::ZERO, + false, ); assert_eq!(wt.nodes.0[&1].world_translation, Vec2::ZERO); @@ -641,6 +660,7 @@ mod tests { &mut wt.nodes, &0, Vec2::ZERO, + false, ); let calls = wt.tree.for_each_child_calls.get(); @@ -650,6 +670,7 @@ mod tests { &mut wt.nodes, &0, Vec2::ZERO, + false, ); assert_eq!(wt.tree.for_each_child_calls.get(), calls); } From 4aa9a9752899fe559c2314a0be6683475cc4852d Mon Sep 17 00:00:00 2001 From: Nixon <43715558+nixonyh@users.noreply.github.com> Date: Fri, 5 Jun 2026 16:30:20 +0800 Subject: [PATCH 2/2] Trigger reposition on every `set_translation` calls --- crates/rectree/src/lib.rs | 5 ++-- crates/rectree/src/node.rs | 56 ++++++++++++++++++++------------------ 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/crates/rectree/src/lib.rs b/crates/rectree/src/lib.rs index 856f935..3e79033 100644 --- a/crates/rectree/src/lib.rs +++ b/crates/rectree/src/lib.rs @@ -122,6 +122,7 @@ impl NodeContext for N { fn set_translation(&mut self, id: &Self::Id, translation: Vec2) { if let Some(n) = self.get_node_mut(id) { n.translation = translation; + n.state.needs_reposition(); } } } @@ -220,9 +221,7 @@ pub fn layout< } // 3. Propagate translation, seeding from `bubbled_id`'s parent - // world translation. Seeding from `bubbled_id`'s own translation - // would offset a non-root node by its own translation each pass; - // the root has no parent and seeds from zero. + // world translation. let parent_world = nodes .get_node(&bubbled_id) .and_then(|node| node.parent_id) diff --git a/crates/rectree/src/node.rs b/crates/rectree/src/node.rs index 68abcb9..f3f9636 100644 --- a/crates/rectree/src/node.rs +++ b/crates/rectree/src/node.rs @@ -84,17 +84,17 @@ bitflags! { /// | Translation propagated | `POSITIONED` set | #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] pub struct NodeState: u8 { - /// Set by [`crate::propagate_translation`] when - /// `world_translation` is current. - const POSITIONED = 1; - /// Set by [`crate::constrain`] when the stored constraint /// matches the value last propagated from the parent. - const CONSTRAINED = 1 << 1; + const CONSTRAINED = 1; /// Set by [`crate::build`] or [`crate::build_up`] when /// `size` and child translations are current. - const BUILT = 1 << 2; + const BUILT = 1 << 1; + + /// Set by [`crate::propagate_translation`] when + /// `world_translation` is current. + const POSITIONED = 1 << 2; } } @@ -113,11 +113,6 @@ impl NodeState { *self == Self::all() } - /// Returns `true` if the `POSITIONED` flag is set. - pub fn is_positioned(&self) -> bool { - self.intersects(Self::POSITIONED) - } - /// Returns `true` if the `CONSTRAINED` flag is set. pub fn is_constrained(&self) -> bool { self.intersects(Self::CONSTRAINED) @@ -128,9 +123,9 @@ impl NodeState { self.intersects(Self::BUILT) } - /// Clears `POSITIONED`, marking translation as stale. - pub fn needs_reposition(&mut self) { - self.remove(Self::POSITIONED); + /// Returns `true` if the `POSITIONED` flag is set. + pub fn is_positioned(&self) -> bool { + self.intersects(Self::POSITIONED) } /// Clears `CONSTRAINED`, marking constraint as stale. @@ -143,9 +138,9 @@ impl NodeState { self.remove(Self::BUILT); } - /// Sets `POSITIONED`, marking translation as current. - pub fn has_repositioned(&mut self) { - self.insert(Self::POSITIONED); + /// Clears `POSITIONED`, marking translation as stale. + pub fn needs_reposition(&mut self) { + self.remove(Self::POSITIONED); } /// Sets `CONSTRAINED`, marking constraint as current. @@ -157,6 +152,11 @@ impl NodeState { pub fn has_rebuilt(&mut self) { self.insert(Self::BUILT); } + + /// Sets `POSITIONED`, marking translation as current. + pub fn has_repositioned(&mut self) { + self.insert(Self::POSITIONED); + } } #[cfg(test)] @@ -186,6 +186,10 @@ mod tests { #[test] fn test_reset_clears_all_flags() { let mut s = NodeState::all(); + s.has_reconstrained(); + s.has_rebuilt(); + s.has_repositioned(); + assert!(s.is_ready()); s.reset(); assert!(!s.is_constrained()); assert!(!s.is_built()); @@ -193,6 +197,15 @@ mod tests { assert!(!s.is_ready()); } + #[test] + fn test_needs_reconstrain_clears_constrained() { + let mut s = NodeState::default(); + s.has_reconstrained(); + assert!(s.is_constrained()); + s.needs_reconstrain(); + assert!(!s.is_constrained()); + } + #[test] fn test_needs_rebuild_clears_built() { let mut s = NodeState::default(); @@ -210,13 +223,4 @@ mod tests { s.needs_reposition(); assert!(!s.is_positioned()); } - - #[test] - fn test_needs_reconstrain_clears_constrained() { - let mut s = NodeState::default(); - s.has_reconstrained(); - assert!(s.is_constrained()); - s.needs_reconstrain(); - assert!(!s.is_constrained()); - } }