diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 48cee6b4bd..e53a27123b 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -2072,6 +2072,17 @@ impl RenderState { self.drag_layers.viewport_rect = viewport_rect; + // Sort layers by global render order so the compositing loop paints + // the one visually behind first and the one in front last. Without + // this, a drag over several selected shapes would swap their + // stacking whenever the frontend sent `ids` in a different order. + if self.drag_layers.layers.len() > 1 { + let order = tree.render_order_indices(&selected_set); + self.drag_layers + .layers + .sort_by_key(|layer| order.get(&layer.shape_id).copied().unwrap_or(usize::MAX)); + } + // Restore the workspace render state exactly like `render_shape_pixels` // does. Without this the next workspace render could observe stale // render_area / focus / pending_nodes left over from our snapshots. diff --git a/render-wasm/src/state/shapes_pool.rs b/render-wasm/src/state/shapes_pool.rs index db01a5df85..6614d01d8c 100644 --- a/render-wasm/src/state/shapes_pool.rs +++ b/render-wasm/src/state/shapes_pool.rs @@ -267,6 +267,57 @@ impl ShapesPoolImpl { above } + /// Walk the tree in render order (DFS pre-order, paint-order among + /// siblings: first child = behind, last child = in front) and assign a + /// monotonically increasing index to every uuid in `targets`. The map + /// returned only contains entries for shapes that actually appear in + /// the traversal; unknown or detached ids are skipped. + /// + /// Used by the drag-layer pipeline to blit the per-shape snapshots in + /// the same stacking order the workspace would have used, regardless + /// of the order in which the frontend sent the selection ids. + pub fn render_order_indices(&self, targets: &HashSet) -> HashMap { + let mut order = HashMap::with_capacity(targets.len()); + if targets.is_empty() { + return order; + } + + let Some(root) = self.get(&Uuid::nil()) else { + return order; + }; + + // DFS with an explicit stack. Push children in reverse so the + // iteration pops them in paint order (behind first). + // `children_ids_iter_forward` returns a boxed trait object, which + // is not `DoubleEndedIterator`, so we collect first and reverse + // over the owned `Vec`. + let mut stack: Vec = { + let children: Vec = root.children_ids_iter_forward(true).copied().collect(); + children.into_iter().rev().collect() + }; + let mut counter: usize = 0; + + while let Some(id) = stack.pop() { + if targets.contains(&id) { + order.insert(id, counter); + if order.len() == targets.len() { + break; + } + } + counter += 1; + + if let Some(shape) = self.get(&id) { + let children: Vec = + shape.children_ids_iter_forward(true).copied().collect(); + for child in children.into_iter().rev() { + stack.push(child); + } + } + } + + order + } + /// Returns the raw modifier matrix currently applied to `id`, if any. /// /// The `get` method above already returns a shape with modifiers baked in,