From 6278828736cb9dd70a0a7237f52943a087ff472b Mon Sep 17 00:00:00 2001 From: sneax Date: Wed, 9 Jul 2025 16:51:05 +0530 Subject: [PATCH 1/2] Add duplicate layer functionality --- .../messages/portfolio/portfolio_message.rs | 4 ++ .../portfolio/portfolio_message_handler.rs | 41 +++++++++++++++++++ frontend/src/components/panels/Layers.svelte | 15 +++++-- frontend/wasm/src/editor_api.rs | 7 ++++ 4 files changed, 63 insertions(+), 4 deletions(-) diff --git a/editor/src/messages/portfolio/portfolio_message.rs b/editor/src/messages/portfolio/portfolio_message.rs index cb2fa34e3e..1e953f412c 100644 --- a/editor/src/messages/portfolio/portfolio_message.rs +++ b/editor/src/messages/portfolio/portfolio_message.rs @@ -84,6 +84,10 @@ pub enum PortfolioMessage { parent: LayerNodeIdentifier, insert_index: usize, }, + DuplicateSelectedLayers { + parent: LayerNodeIdentifier, + insert_index: usize, + }, PasteSerializedData { data: String, }, diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 195768bd4b..e0b7dd1013 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -473,6 +473,47 @@ impl MessageHandler> for PortfolioMes // Load the document into the portfolio so it opens in the editor self.load_document(document, document_id, responses, to_front); } + PortfolioMessage::DuplicateSelectedLayers { parent, insert_index } => { + let Some(document) = self.active_document_mut() else { + return; + }; + + let mut all_new_ids = Vec::new(); + let selected_layers = document.network_interface.selected_nodes().selected_layers(document.metadata()).collect::>(); + + responses.add(DocumentMessage::DeselectAllLayers); + responses.add(DocumentMessage::AddTransaction); + + for layer in selected_layers { + let layer_node_id = layer.to_node(); + + let mut copy_ids = HashMap::new(); + copy_ids.insert(layer_node_id, NodeId(0)); + + document + .network_interface + .upstream_flow_back_from_nodes(vec![layer_node_id], &[], network_interface::FlowType::LayerChildrenUpstreamFlow) + .enumerate() + .for_each(|(index, node_id)| { + copy_ids.insert(node_id, NodeId((index + 1) as u64)); + }); + + let nodes: Vec<_> = document.network_interface.copy_nodes(©_ids, &[]).collect(); + let new_ids: HashMap<_, _> = nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect(); + let new_layer = LayerNodeIdentifier::new_unchecked(new_ids[&NodeId(0)]); + all_new_ids.extend(new_ids.values().cloned()); + + responses.add(NodeGraphMessage::AddNodes { nodes, new_ids: new_ids.clone() }); + responses.add(NodeGraphMessage::MoveLayerToStack { + layer: new_layer, + parent, + insert_index, + }); + } + + responses.add(NodeGraphMessage::RunDocumentGraph); + responses.add(NodeGraphMessage::SelectedNodesSet { nodes: all_new_ids }); + } PortfolioMessage::PasteIntoFolder { clipboard, parent, insert_index } => { let mut all_new_ids = Vec::new(); let paste = |entry: &CopyBufferEntry, responses: &mut VecDeque<_>, all_new_ids: &mut Vec| { diff --git a/frontend/src/components/panels/Layers.svelte b/frontend/src/components/panels/Layers.svelte index 092a83fbac..136f6e19af 100644 --- a/frontend/src/components/panels/Layers.svelte +++ b/frontend/src/components/panels/Layers.svelte @@ -54,6 +54,7 @@ let draggingData: undefined | DraggingData = undefined; let fakeHighlightOfNotYetSelectedLayerBeingDragged: undefined | bigint = undefined; let dragInPanel = false; + let isDuplicating = false; // Interactive clipping let layerToClipUponClick: LayerListingInfo | undefined = undefined; @@ -382,8 +383,9 @@ // Set style of cursor for drag if (event.dataTransfer) { - event.dataTransfer.dropEffect = "move"; - event.dataTransfer.effectAllowed = "move"; + isDuplicating = event.altKey; + event.dataTransfer.dropEffect = isDuplicating ? "copy" : "move"; + event.dataTransfer.effectAllowed = isDuplicating ? "copy" : "move"; } if (list) draggingData = calculateDragIndex(list, event.clientY, select); @@ -406,11 +408,15 @@ e.preventDefault(); if (e.dataTransfer) { - // Moving layers + // Moving or duplicating layers if (e.dataTransfer.items.length === 0) { if (draggable && dragInPanel) { select?.(); - editor.handle.moveLayerInTree(insertParentId, insertIndex); + if (isDuplicating) { + editor.handle.duplicateLayer(insertParentId, insertIndex); + } else { + editor.handle.moveLayerInTree(insertParentId, insertIndex); + } } } // Importing files @@ -444,6 +450,7 @@ draggingData = undefined; fakeHighlightOfNotYetSelectedLayerBeingDragged = undefined; dragInPanel = false; + isDuplicating = false; } function rebuildLayerHierarchy(updateDocumentLayerStructure: DocumentLayerStructure) { diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 20f2f1ae96..9c7ab01ff7 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -669,6 +669,13 @@ impl EditorHandle { }; self.dispatch(message); } + /// Duplicate the selected layers + #[wasm_bindgen(js_name = duplicateLayer)] + pub fn duplicate_layer(&self, parent_id: Option, insert_index: usize) { + let parent = parent_id.map(|id| LayerNodeIdentifier::new_unchecked(NodeId(id))).unwrap_or_default(); + let message = PortfolioMessage::DuplicateSelectedLayers { parent, insert_index }; + self.dispatch(message); + } /// Toggle visibility of a layer or node given its node ID #[wasm_bindgen(js_name = toggleNodeVisibilityLayerPanel)] From c6e77827a5bcb8a8216c736cc04941de260310c8 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 25 Jul 2025 23:17:43 -0700 Subject: [PATCH 2/2] Consolidate messages --- .../portfolio/document/document_message.rs | 1 + .../document/document_message_handler.rs | 84 ++++++++++++++++--- .../messages/portfolio/portfolio_message.rs | 4 - .../portfolio/portfolio_message_handler.rs | 41 --------- frontend/src/components/panels/Layers.svelte | 8 +- frontend/wasm/src/editor_api.rs | 19 ++--- 6 files changed, 80 insertions(+), 77 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index ae3576d2a1..94e16b183f 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -76,6 +76,7 @@ pub enum DocumentMessage { MoveSelectedLayersTo { parent: LayerNodeIdentifier, insert_index: usize, + as_duplicate: bool, }, MoveSelectedLayersToGroup { parent: LayerNodeIdentifier, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 422d5bb111..85caca5092 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -616,12 +616,12 @@ impl MessageHandler> for DocumentMes responses.add(NodeGraphMessage::SelectedNodesSet { nodes: new_folders }); } } - DocumentMessage::MoveSelectedLayersTo { parent, insert_index } => { + DocumentMessage::MoveSelectedLayersTo { parent, insert_index, as_duplicate } => { + // Exit early if we have been called with an empty selection. if !self.selection_network_path.is_empty() { log::error!("Moving selected layers is only supported for the Document Network"); return; } - // Disallow trying to insert into self. if self .network_interface @@ -640,22 +640,54 @@ impl MessageHandler> for DocumentMes if any_artboards && parent != LayerNodeIdentifier::ROOT_PARENT { return; } - - // Non-artboards cannot be put at the top level if artboards also exist there + // Non-artboards cannot be put at the top level if artboards also exist there. let selected_any_non_artboards = self .network_interface .selected_nodes() .selected_layers(self.metadata()) .any(|layer| !self.network_interface.is_artboard(&layer.to_node(), &self.selection_network_path)); - let top_level_artboards = LayerNodeIdentifier::ROOT_PARENT .children(self.metadata()) .any(|layer| self.network_interface.is_artboard(&layer.to_node(), &self.selection_network_path)); - if selected_any_non_artboards && parent == LayerNodeIdentifier::ROOT_PARENT && top_level_artboards { return; } + if as_duplicate { + let mut all_new_ids = Vec::new(); + let selected_layers = self.network_interface.selected_nodes().selected_layers(self.metadata()).collect::>(); + + responses.add(DocumentMessage::DeselectAllLayers); + responses.add(DocumentMessage::AddTransaction); + + for selected_layer in selected_layers { + let layer_node_id = selected_layer.to_node(); + + let mut copy_ids = HashMap::new(); + copy_ids.insert(layer_node_id, NodeId(0)); + + self.network_interface + .upstream_flow_back_from_nodes(vec![layer_node_id], &[], network_interface::FlowType::LayerChildrenUpstreamFlow) + .enumerate() + .for_each(|(index, node_id)| { + copy_ids.insert(node_id, NodeId((index + 1) as u64)); + }); + + let nodes: Vec<_> = self.network_interface.copy_nodes(©_ids, &[]).collect(); + let new_ids: HashMap<_, _> = nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect(); + let layer = LayerNodeIdentifier::new_unchecked(new_ids[&NodeId(0)]); + all_new_ids.extend(new_ids.values().cloned()); + + responses.add(NodeGraphMessage::AddNodes { nodes, new_ids: new_ids.clone() }); + responses.add(NodeGraphMessage::MoveLayerToStack { layer, parent, insert_index }); + } + + responses.add(NodeGraphMessage::RunDocumentGraph); + responses.add(NodeGraphMessage::SelectedNodesSet { nodes: all_new_ids }); + + return; + } + let layers_to_move = self.network_interface.shallowest_unique_layers_sorted(&self.selection_network_path); // Offset the index for layers to move that are below another layer to move. For example when moving 1 and 2 between 3 and 4, 2 should be inserted at the same index as 1 since 1 is moved first. let layers_to_move_with_insert_offset = layers_to_move @@ -2893,8 +2925,11 @@ impl DocumentMessageHandler { }; // If moving down, insert below this layer. If moving up, insert above this layer. - let insert_index = if relative_index_offset < 0 { neighbor_index } else { neighbor_index + 1 }; - responses.add(DocumentMessage::MoveSelectedLayersTo { parent, insert_index }); + responses.add(DocumentMessage::MoveSelectedLayersTo { + parent, + insert_index: if relative_index_offset < 0 { neighbor_index } else { neighbor_index + 1 }, + as_duplicate: false, + }); } pub fn graph_view_overlay_open(&self) -> bool { @@ -3260,6 +3295,7 @@ mod document_message_handler_tests { .handle_message(DocumentMessage::MoveSelectedLayersTo { parent: child_folder, insert_index: 0, + as_duplicate: false, }) .await; @@ -3285,7 +3321,13 @@ mod document_message_handler_tests { // First move rectangle into folder1 editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![rect_layer.to_node()] }).await; - editor.handle_message(DocumentMessage::MoveSelectedLayersTo { parent: folder1, insert_index: 0 }).await; + editor + .handle_message(DocumentMessage::MoveSelectedLayersTo { + parent: folder1, + insert_index: 0, + as_duplicate: false, + }) + .await; // Verifying rectagle is now in folder1 let rect_parent = rect_layer.parent(editor.active_document().metadata()).unwrap(); @@ -3293,7 +3335,13 @@ mod document_message_handler_tests { // Moving folder1 into folder2 editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![folder1.to_node()] }).await; - editor.handle_message(DocumentMessage::MoveSelectedLayersTo { parent: folder2, insert_index: 0 }).await; + editor + .handle_message(DocumentMessage::MoveSelectedLayersTo { + parent: folder2, + insert_index: 0, + as_duplicate: false, + }) + .await; // Verifing hirarchy: folder2 > folder1 > rectangle let document = editor.active_document(); @@ -3352,7 +3400,13 @@ mod document_message_handler_tests { // Moving the rectangle to folder1 to ensure it's inside editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![rect_layer.to_node()] }).await; - editor.handle_message(DocumentMessage::MoveSelectedLayersTo { parent: folder1, insert_index: 0 }).await; + editor + .handle_message(DocumentMessage::MoveSelectedLayersTo { + parent: folder1, + insert_index: 0, + as_duplicate: false, + }) + .await; editor.handle_message(TransformLayerMessage::BeginGrab).await; editor.move_mouse(50., 25., ModifierKeys::empty(), MouseKeys::NONE).await; @@ -3369,7 +3423,13 @@ mod document_message_handler_tests { let rect_bbox_before = document.metadata().bounding_box_viewport(rect_layer).unwrap(); // Moving rectangle from folder1 to folder2 - editor.handle_message(DocumentMessage::MoveSelectedLayersTo { parent: folder2, insert_index: 0 }).await; + editor + .handle_message(DocumentMessage::MoveSelectedLayersTo { + parent: folder2, + insert_index: 0, + as_duplicate: false, + }) + .await; // Rectangle's viewport position after moving let document = editor.active_document(); diff --git a/editor/src/messages/portfolio/portfolio_message.rs b/editor/src/messages/portfolio/portfolio_message.rs index 89d4f07969..4fb756fc4a 100644 --- a/editor/src/messages/portfolio/portfolio_message.rs +++ b/editor/src/messages/portfolio/portfolio_message.rs @@ -85,10 +85,6 @@ pub enum PortfolioMessage { parent: LayerNodeIdentifier, insert_index: usize, }, - DuplicateSelectedLayers { - parent: LayerNodeIdentifier, - insert_index: usize, - }, PasteSerializedData { data: String, }, diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 5bfd2288b1..71de86b90f 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -518,47 +518,6 @@ impl MessageHandler> for Portfolio // Load the document into the portfolio so it opens in the editor self.load_document(document, document_id, responses, to_front); } - PortfolioMessage::DuplicateSelectedLayers { parent, insert_index } => { - let Some(document) = self.active_document_mut() else { - return; - }; - - let mut all_new_ids = Vec::new(); - let selected_layers = document.network_interface.selected_nodes().selected_layers(document.metadata()).collect::>(); - - responses.add(DocumentMessage::DeselectAllLayers); - responses.add(DocumentMessage::AddTransaction); - - for layer in selected_layers { - let layer_node_id = layer.to_node(); - - let mut copy_ids = HashMap::new(); - copy_ids.insert(layer_node_id, NodeId(0)); - - document - .network_interface - .upstream_flow_back_from_nodes(vec![layer_node_id], &[], network_interface::FlowType::LayerChildrenUpstreamFlow) - .enumerate() - .for_each(|(index, node_id)| { - copy_ids.insert(node_id, NodeId((index + 1) as u64)); - }); - - let nodes: Vec<_> = document.network_interface.copy_nodes(©_ids, &[]).collect(); - let new_ids: HashMap<_, _> = nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect(); - let new_layer = LayerNodeIdentifier::new_unchecked(new_ids[&NodeId(0)]); - all_new_ids.extend(new_ids.values().cloned()); - - responses.add(NodeGraphMessage::AddNodes { nodes, new_ids: new_ids.clone() }); - responses.add(NodeGraphMessage::MoveLayerToStack { - layer: new_layer, - parent, - insert_index, - }); - } - - responses.add(NodeGraphMessage::RunDocumentGraph); - responses.add(NodeGraphMessage::SelectedNodesSet { nodes: all_new_ids }); - } PortfolioMessage::PasteIntoFolder { clipboard, parent, insert_index } => { let mut all_new_ids = Vec::new(); let paste = |entry: &CopyBufferEntry, responses: &mut VecDeque<_>, all_new_ids: &mut Vec| { diff --git a/frontend/src/components/panels/Layers.svelte b/frontend/src/components/panels/Layers.svelte index 136f6e19af..787ed87085 100644 --- a/frontend/src/components/panels/Layers.svelte +++ b/frontend/src/components/panels/Layers.svelte @@ -410,13 +410,9 @@ if (e.dataTransfer) { // Moving or duplicating layers if (e.dataTransfer.items.length === 0) { - if (draggable && dragInPanel) { + if (draggable && dragInPanel && insertIndex !== undefined) { select?.(); - if (isDuplicating) { - editor.handle.duplicateLayer(insertParentId, insertIndex); - } else { - editor.handle.moveLayerInTree(insertParentId, insertIndex); - } + editor.handle.moveSelectedLayersInTree(insertParentId, insertIndex, isDuplicating); } } // Importing files diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 2c912bad4f..4a673a95d0 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -633,18 +633,16 @@ impl EditorHandle { self.dispatch(message); } - /// Move a layer to within a folder and placed down at the given index. + /// Move the selected layers to within a given folder and placed within it at a 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. - #[wasm_bindgen(js_name = moveLayerInTree)] - pub fn move_layer_in_tree(&self, insert_parent_id: Option, insert_index: Option) { + #[wasm_bindgen(js_name = moveSelectedLayersInTree)] + pub fn move_selected_layers_in_tree(&self, insert_parent_id: Option, insert_index: Option, as_duplicate: bool) { let insert_parent_id = insert_parent_id.map(NodeId); let parent = insert_parent_id.map(LayerNodeIdentifier::new_unchecked).unwrap_or_default(); + let insert_index = insert_index.unwrap_or_default(); - let message = DocumentMessage::MoveSelectedLayersTo { - parent, - insert_index: insert_index.unwrap_or_default(), - }; + let message = DocumentMessage::MoveSelectedLayersTo { parent, insert_index, as_duplicate }; self.dispatch(message); } @@ -774,13 +772,6 @@ impl EditorHandle { }; self.dispatch(message); } - /// Duplicate the selected layers - #[wasm_bindgen(js_name = duplicateLayer)] - pub fn duplicate_layer(&self, parent_id: Option, insert_index: usize) { - let parent = parent_id.map(|id| LayerNodeIdentifier::new_unchecked(NodeId(id))).unwrap_or_default(); - let message = PortfolioMessage::DuplicateSelectedLayers { parent, insert_index }; - self.dispatch(message); - } /// Toggle visibility of a layer or node given its node ID #[wasm_bindgen(js_name = toggleNodeVisibilityLayerPanel)]