From 7fe9efbf67e42c1104af5bfa9df3dcf35139ad35 Mon Sep 17 00:00:00 2001 From: afrdbaig7 Date: Sat, 17 Jan 2026 20:10:25 +0530 Subject: [PATCH] layers: add Alt+Drag duplication support in Layers panel --- frontend/src/components/panels/Layers.svelte | 54 ++++++++++++++++++++ frontend/wrapper/src/editor_wrapper.rs | 7 +++ 2 files changed, 61 insertions(+) diff --git a/frontend/src/components/panels/Layers.svelte b/frontend/src/components/panels/Layers.svelte index 14c022f955..abeb2f5773 100644 --- a/frontend/src/components/panels/Layers.svelte +++ b/frontend/src/components/panels/Layers.svelte @@ -67,6 +67,11 @@ let justFinishedDrag = false; // Used to prevent click events after a drag let dragInPanel = false; + // Alt+Drag duplication + let altKeyPressedDuringDrag = false; + let originalLayersBeforeDuplication: bigint[] | undefined = undefined; + let duplicatedLayerIds: bigint[] | undefined = undefined; + // Interactive clipping let layerToClipUponClick: LayerListingInfo | undefined = undefined; let layerToClipAltKeyPressed = false; @@ -108,6 +113,8 @@ addEventListener("mousedown", draggingMouseDown); addEventListener("keydown", draggingKeyDown); addEventListener("keydown", handleLayerPanelKeyDown); + addEventListener("keyup", draggingKeyUp); + addEventListener("blur", () => internalDragState?.active && abortDrag()); addEventListener("pointermove", clippingHover); addEventListener("keydown", clippingKeyPress); @@ -126,6 +133,7 @@ removeEventListener("mousedown", draggingMouseDown); removeEventListener("keydown", draggingKeyDown); removeEventListener("keydown", handleLayerPanelKeyDown); + removeEventListener("keyup", draggingKeyUp); removeEventListener("pointermove", clippingHover); removeEventListener("keydown", clippingKeyPress); @@ -415,11 +423,45 @@ abortDrag(); } + async function startDuplicates() { + if (!internalDragState?.active) return; + + originalLayersBeforeDuplication = [...$nodeGraph.selected]; + editor.duplicateSelectedLayers(); + + await tick(); + duplicatedLayerIds = [...$nodeGraph.selected]; + } + + function stopDuplicates() { + if (!originalLayersBeforeDuplication || !duplicatedLayerIds) return; + + duplicatedLayerIds.forEach((layerId) => { + editor.deleteNode(layerId); + }); + + editor.deselectAllLayers(); + originalLayersBeforeDuplication.forEach((layerId, index) => { + const ctrl = index > 0; + editor.selectLayer(layerId, ctrl, false); + }); + + originalLayersBeforeDuplication = undefined; + duplicatedLayerIds = undefined; + } + function abortDrag() { + if (altKeyPressedDuringDrag && originalLayersBeforeDuplication) { + stopDuplicates(); + } + internalDragState = undefined; draggingData = undefined; fakeHighlightOfNotYetSelectedLayerBeingDragged = undefined; dragInPanel = false; + altKeyPressedDuringDrag = false; + originalLayersBeforeDuplication = undefined; + duplicatedLayerIds = undefined; } function draggingMouseDown(e: MouseEvent) { @@ -435,6 +477,18 @@ justFinishedDrag = true; abortDrag(); } + + if (e.key === "Alt" && internalDragState?.active && !altKeyPressedDuringDrag) { + altKeyPressedDuringDrag = true; + startDuplicates(); + } + } + + function draggingKeyUp(e: KeyboardEvent) { + if (e.key === "Alt" && internalDragState?.active && altKeyPressedDuringDrag) { + altKeyPressedDuringDrag = false; + stopDuplicates(); + } } function handleLayerPanelKeyDown(e: KeyboardEvent) { diff --git a/frontend/wrapper/src/editor_wrapper.rs b/frontend/wrapper/src/editor_wrapper.rs index 74a76d7c3e..815fb9436c 100644 --- a/frontend/wrapper/src/editor_wrapper.rs +++ b/frontend/wrapper/src/editor_wrapper.rs @@ -753,6 +753,13 @@ impl EditorWrapper { self.dispatch(message); } + /// Duplicate the currently selected layers + #[wasm_bindgen(js_name = duplicateSelectedLayers)] + pub fn duplicate_selected_layers(&self) { + let message = DocumentMessage::DuplicateSelectedLayers; + self.dispatch(message); + } + /// Move a layer to within a folder and placed down at the given index. /// If the folder is `None`, it is inserted into the document root. /// If the insert index is `None`, it is inserted at the start of the folder.