From af001f8db6d82a425c3abd00083beb5a3e4b47e0 Mon Sep 17 00:00:00 2001 From: 0HyperCube <78500760+0HyperCube@users.noreply.github.com> Date: Thu, 22 Dec 2022 21:47:48 +0000 Subject: [PATCH] Pasting and duplicating nodes (#902) * Pasting and duplicating nodes * Rebind duplication * Update selection on duplicate --- .../src/messages/frontend/frontend_message.rs | 3 + .../messages/input_mapper/default_mapping.rs | 3 + .../document/node_graph/node_graph_message.rs | 6 ++ .../node_graph/node_graph_message_handler.rs | 87 ++++++++++++++++++- frontend/src/components/panels/NodeGraph.vue | 6 +- frontend/src/io-managers/input.ts | 2 + frontend/src/wasm-communication/messages.ts | 7 ++ frontend/wasm/src/editor_api.rs | 7 ++ 8 files changed, 119 insertions(+), 2 deletions(-) diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 69d2932d..8246fd24 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -227,6 +227,9 @@ pub enum FrontendMessage { layout_target: LayoutTarget, layout: SubLayout, }, + UpdateNodeGraphSelection { + selected: Vec, + }, UpdateNodeGraphVisibility { visible: bool, }, diff --git a/editor/src/messages/input_mapper/default_mapping.rs b/editor/src/messages/input_mapper/default_mapping.rs index cce31b02..ffc787c0 100644 --- a/editor/src/messages/input_mapper/default_mapping.rs +++ b/editor/src/messages/input_mapper/default_mapping.rs @@ -30,6 +30,9 @@ pub fn default_mapping() -> Mapping { // NodeGraphMessage entry!(KeyDown(Delete); action_dispatch=NodeGraphMessage::DeleteSelectedNodes), entry!(KeyDown(Backspace); action_dispatch=NodeGraphMessage::DeleteSelectedNodes), + entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=NodeGraphMessage::Cut), + entry!(KeyDown(KeyC); modifiers=[Accel], action_dispatch=NodeGraphMessage::Copy), + entry!(KeyDown(KeyD); modifiers=[Accel], action_dispatch=NodeGraphMessage::DuplicateSelectedNodes), // // TransformLayerMessage entry!(KeyDown(Enter); action_dispatch=TransformLayerMessage::ApplyTransformOperation), diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs index 955a9d4c..ef7d3cee 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs @@ -14,6 +14,7 @@ pub enum NodeGraphMessage { input_node: u64, input_node_connector_index: usize, }, + Copy, CreateNode { // Having the caller generate the id means that we don't have to return it. This can be a random u64. node_id: Option, @@ -21,6 +22,7 @@ pub enum NodeGraphMessage { x: i32, y: i32, }, + Cut, DeleteNode { node_id: NodeId, }, @@ -32,6 +34,7 @@ pub enum NodeGraphMessage { DoubleClickNode { node: NodeId, }, + DuplicateSelectedNodes, ExitNestedNetwork { depth_of_nesting: usize, }, @@ -47,6 +50,9 @@ pub enum NodeGraphMessage { OpenNodeGraph { layer_path: Vec, }, + PasteNodes { + serialized_nodes: String, + }, SelectNodes { nodes: Vec, }, diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index 175580ad..0fe33c6d 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -243,6 +243,16 @@ impl NodeGraphMessageHandler { responses.push_back(FrontendMessage::UpdateNodeGraph { nodes, links }.into()); } + /// Updates the frontend's selection state inline with the backend + fn update_selected(&self, responses: &mut VecDeque) { + responses.push_back( + FrontendMessage::UpdateNodeGraphSelection { + selected: self.selected_nodes.clone(), + } + .into(), + ); + } + fn remove_references_from_network(network: &mut NodeNetwork, node_id: NodeId) -> bool { if network.inputs.iter().any(|&id| id == node_id) { warn!("Deleting input node"); @@ -329,6 +339,26 @@ impl MessageHandler { + let Some(network) = self.get_active_network_mut(document) else { + error!("No network"); + return; + }; + + // Collect the selected nodes + let selected_nodes = self + .selected_nodes + .iter() + .filter(|&&id| !network.inputs.contains(&id) && network.output != id) // Don't copy input or output nodes + .filter_map(|id| network.nodes.get(id)) + .collect::>(); + + // Prefix to show that this is nodes + let mut copy_text = String::from("graphite/nodes: "); + copy_text += &serde_json::to_string(&selected_nodes).expect("Could not serialize paste"); + + responses.push_back(FrontendMessage::TriggerTextCopy { copy_text }.into()); + } NodeGraphMessage::CreateNode { node_id, node_type, x, y } => { let node_id = node_id.unwrap_or_else(crate::application::generate_uuid); let Some(network) = self.get_active_network_mut(document) else{ @@ -372,6 +402,10 @@ impl MessageHandler { + responses.push_back(NodeGraphMessage::Copy.into()); + responses.push_back(NodeGraphMessage::DeleteSelectedNodes.into()); + } NodeGraphMessage::DeleteNode { node_id } => { if let Some(network) = self.get_active_network_mut(document) { if self.remove_node(network, node_id) { @@ -419,6 +453,31 @@ impl MessageHandler { + if let Some(network) = self.get_active_network_mut(document) { + let mut new_selected = Vec::new(); + for &id in &self.selected_nodes { + // Don't allow copying input or output nodes. + if id != network.output && !network.inputs.contains(&id) { + if let Some(node) = network.nodes.get(&id) { + let new_id = crate::application::generate_uuid(); + let mut node = node.clone(); + + // Shift duplicated nodes + node.metadata.position.0 += 2; + node.metadata.position.1 += 2; + + network.nodes.insert(new_id, node); + new_selected.push(new_id); + } + } + } + self.selected_nodes = new_selected; + Self::send_graph(network, responses); + self.update_selected(responses); + } } NodeGraphMessage::ExitNestedNetwork { depth_of_nesting } => { self.selected_nodes = Vec::new(); @@ -484,6 +543,32 @@ impl MessageHandler { + let Some(network) = self.get_active_network_mut(document) else{ + warn!("No network"); + return; + }; + + let data = match serde_json::from_str::>(&serialized_nodes) { + Ok(d) => d, + Err(e) => { + warn!("Invalid node data {e:?}"); + return; + } + }; + + self.selected_nodes.clear(); + for node in data { + let id = crate::application::generate_uuid(); + network.nodes.insert(id, node); + + // Select the newly pasted node + self.selected_nodes.push(id); + } + + Self::send_graph(network, responses); + self.update_selected(responses); + } NodeGraphMessage::SelectNodes { nodes } => { self.selected_nodes = nodes; responses.push_back(PropertiesPanelMessage::ResendActiveProperties.into()); @@ -582,7 +667,7 @@ impl MessageHandler ActionList { if self.layer_path.is_some() && !self.selected_nodes.is_empty() { - actions!(NodeGraphMessageDiscriminant; DeleteSelectedNodes,) + actions!(NodeGraphMessageDiscriminant; DeleteSelectedNodes, Cut, Copy, DuplicateSelectedNodes) } else { actions!(NodeGraphMessageDiscriminant;) } diff --git a/frontend/src/components/panels/NodeGraph.vue b/frontend/src/components/panels/NodeGraph.vue index 7cdc5c00..02e611a2 100644 --- a/frontend/src/components/panels/NodeGraph.vue +++ b/frontend/src/components/panels/NodeGraph.vue @@ -300,7 +300,7 @@ import { defineComponent, nextTick } from "vue"; import type { IconName } from "@/utility-functions/icons"; -import type { FrontendNodeLink } from "@/wasm-communication/messages"; +import { UpdateNodeGraphSelection, FrontendNodeLink } from "@/wasm-communication/messages"; import LayoutCol from "@/components/layout/LayoutCol.vue"; import LayoutRow from "@/components/layout/LayoutRow.vue"; @@ -721,6 +721,10 @@ export default defineComponent({ const outputPort2 = document.querySelectorAll(`[data-port="output"]`)[6] as HTMLDivElement | undefined; const inputPort2 = document.querySelectorAll(`[data-port="input"]`)[3] as HTMLDivElement | undefined; if (outputPort2 && inputPort2) this.createWirePath(outputPort2, inputPort2.getBoundingClientRect(), true, false); + + this.editor.subscriptions.subscribeJsMessage(UpdateNodeGraphSelection, (updateNodeGraphSelection) => { + this.selected = updateNodeGraphSelection.selected; + }); }, components: { IconLabel, diff --git a/frontend/src/io-managers/input.ts b/frontend/src/io-managers/input.ts index 70653f64..ab8cd701 100644 --- a/frontend/src/io-managers/input.ts +++ b/frontend/src/io-managers/input.ts @@ -262,6 +262,8 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo item.getAsString((text) => { if (text.startsWith("graphite/layer: ")) { editor.instance.pasteSerializedData(text.substring(16, text.length)); + } else if (text.startsWith("graphite/nodes: ")) { + editor.instance.pasteSerializedNodes(text.substring(16, text.length)); } }); } diff --git a/frontend/src/wasm-communication/messages.ts b/frontend/src/wasm-communication/messages.ts index b430c530..f78b5249 100644 --- a/frontend/src/wasm-communication/messages.ts +++ b/frontend/src/wasm-communication/messages.ts @@ -33,11 +33,17 @@ export class UpdateNodeGraph extends JsMessage { @Type(() => FrontendNodeLink) readonly links!: FrontendNodeLink[]; } + export class UpdateNodeTypes extends JsMessage { @Type(() => FrontendNode) readonly nodeTypes!: FrontendNodeType[]; } +export class UpdateNodeGraphSelection extends JsMessage { + @Type(() => BigInt) + readonly selected!: bigint[]; +} + export class UpdateNodeGraphVisibility extends JsMessage { readonly visible!: boolean; } @@ -1407,6 +1413,7 @@ export const messageMakers: Record = { UpdateMouseCursor, UpdateNodeGraph, UpdateNodeGraphBarLayout, + UpdateNodeGraphSelection, UpdateNodeTypes, UpdateNodeGraphVisibility, UpdateOpenDocumentsList, diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 54ed2fdf..b35d5630 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -627,6 +627,13 @@ impl JsEditorHandle { self.dispatch(message); } + /// Pastes the nodes based on serialized data + #[wasm_bindgen(js_name = pasteSerializedNodes)] + pub fn paste_serialized_nodes(&self, serialized_nodes: String) { + let message = NodeGraphMessage::PasteNodes { serialized_nodes }; + self.dispatch(message); + } + /// Notifies the backend that the user double clicked a node #[wasm_bindgen(js_name = doubleClickNode)] pub fn double_click_node(&self, node: u64) {