Pasting and duplicating nodes (#902)

* Pasting and duplicating nodes

* Rebind duplication

* Update selection on duplicate
This commit is contained in:
0HyperCube
2022-12-22 21:47:48 +00:00
committed by Keavon Chambers
parent 537dbf266a
commit 1f69b0f35f
8 changed files with 119 additions and 2 deletions
@@ -227,6 +227,9 @@ pub enum FrontendMessage {
layout_target: LayoutTarget, layout_target: LayoutTarget,
layout: SubLayout, layout: SubLayout,
}, },
UpdateNodeGraphSelection {
selected: Vec<NodeId>,
},
UpdateNodeGraphVisibility { UpdateNodeGraphVisibility {
visible: bool, visible: bool,
}, },
@@ -30,6 +30,9 @@ pub fn default_mapping() -> Mapping {
// NodeGraphMessage // NodeGraphMessage
entry!(KeyDown(Delete); action_dispatch=NodeGraphMessage::DeleteSelectedNodes), entry!(KeyDown(Delete); action_dispatch=NodeGraphMessage::DeleteSelectedNodes),
entry!(KeyDown(Backspace); 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 // TransformLayerMessage
entry!(KeyDown(Enter); action_dispatch=TransformLayerMessage::ApplyTransformOperation), entry!(KeyDown(Enter); action_dispatch=TransformLayerMessage::ApplyTransformOperation),
@@ -14,6 +14,7 @@ pub enum NodeGraphMessage {
input_node: u64, input_node: u64,
input_node_connector_index: usize, input_node_connector_index: usize,
}, },
Copy,
CreateNode { CreateNode {
// Having the caller generate the id means that we don't have to return it. This can be a random u64. // Having the caller generate the id means that we don't have to return it. This can be a random u64.
node_id: Option<NodeId>, node_id: Option<NodeId>,
@@ -21,6 +22,7 @@ pub enum NodeGraphMessage {
x: i32, x: i32,
y: i32, y: i32,
}, },
Cut,
DeleteNode { DeleteNode {
node_id: NodeId, node_id: NodeId,
}, },
@@ -32,6 +34,7 @@ pub enum NodeGraphMessage {
DoubleClickNode { DoubleClickNode {
node: NodeId, node: NodeId,
}, },
DuplicateSelectedNodes,
ExitNestedNetwork { ExitNestedNetwork {
depth_of_nesting: usize, depth_of_nesting: usize,
}, },
@@ -47,6 +50,9 @@ pub enum NodeGraphMessage {
OpenNodeGraph { OpenNodeGraph {
layer_path: Vec<document_legacy::LayerId>, layer_path: Vec<document_legacy::LayerId>,
}, },
PasteNodes {
serialized_nodes: String,
},
SelectNodes { SelectNodes {
nodes: Vec<NodeId>, nodes: Vec<NodeId>,
}, },
@@ -243,6 +243,16 @@ impl NodeGraphMessageHandler {
responses.push_back(FrontendMessage::UpdateNodeGraph { nodes, links }.into()); 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<Message>) {
responses.push_back(
FrontendMessage::UpdateNodeGraphSelection {
selected: self.selected_nodes.clone(),
}
.into(),
);
}
fn remove_references_from_network(network: &mut NodeNetwork, node_id: NodeId) -> bool { fn remove_references_from_network(network: &mut NodeNetwork, node_id: NodeId) -> bool {
if network.inputs.iter().any(|&id| id == node_id) { if network.inputs.iter().any(|&id| id == node_id) {
warn!("Deleting input node"); warn!("Deleting input node");
@@ -329,6 +339,26 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &InputPreprocessorMessageH
Self::send_graph(network, responses); Self::send_graph(network, responses);
responses.push_back(DocumentMessage::NodeGraphFrameGenerate.into()); responses.push_back(DocumentMessage::NodeGraphFrameGenerate.into());
} }
NodeGraphMessage::Copy => {
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::<Vec<_>>();
// 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 } => { NodeGraphMessage::CreateNode { node_id, node_type, x, y } => {
let node_id = node_id.unwrap_or_else(crate::application::generate_uuid); let node_id = node_id.unwrap_or_else(crate::application::generate_uuid);
let Some(network) = self.get_active_network_mut(document) else{ let Some(network) = self.get_active_network_mut(document) else{
@@ -372,6 +402,10 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &InputPreprocessorMessageH
); );
Self::send_graph(network, responses); Self::send_graph(network, responses);
} }
NodeGraphMessage::Cut => {
responses.push_back(NodeGraphMessage::Copy.into());
responses.push_back(NodeGraphMessage::DeleteSelectedNodes.into());
}
NodeGraphMessage::DeleteNode { node_id } => { NodeGraphMessage::DeleteNode { node_id } => {
if let Some(network) = self.get_active_network_mut(document) { if let Some(network) = self.get_active_network_mut(document) {
if self.remove_node(network, node_id) { if self.remove_node(network, node_id) {
@@ -419,6 +453,31 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &InputPreprocessorMessageH
Self::send_graph(network, responses); Self::send_graph(network, responses);
} }
self.collect_nested_addresses(document, responses); self.collect_nested_addresses(document, responses);
self.update_selected(responses);
}
NodeGraphMessage::DuplicateSelectedNodes => {
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 } => { NodeGraphMessage::ExitNestedNetwork { depth_of_nesting } => {
self.selected_nodes = Vec::new(); self.selected_nodes = Vec::new();
@@ -484,6 +543,32 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &InputPreprocessorMessageH
} }
self.collect_nested_addresses(document, responses); self.collect_nested_addresses(document, responses);
} }
NodeGraphMessage::PasteNodes { serialized_nodes } => {
let Some(network) = self.get_active_network_mut(document) else{
warn!("No network");
return;
};
let data = match serde_json::from_str::<Vec<DocumentNode>>(&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 } => { NodeGraphMessage::SelectNodes { nodes } => {
self.selected_nodes = nodes; self.selected_nodes = nodes;
responses.push_back(PropertiesPanelMessage::ResendActiveProperties.into()); responses.push_back(PropertiesPanelMessage::ResendActiveProperties.into());
@@ -582,7 +667,7 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &InputPreprocessorMessageH
fn actions(&self) -> ActionList { fn actions(&self) -> ActionList {
if self.layer_path.is_some() && !self.selected_nodes.is_empty() { if self.layer_path.is_some() && !self.selected_nodes.is_empty() {
actions!(NodeGraphMessageDiscriminant; DeleteSelectedNodes,) actions!(NodeGraphMessageDiscriminant; DeleteSelectedNodes, Cut, Copy, DuplicateSelectedNodes)
} else { } else {
actions!(NodeGraphMessageDiscriminant;) actions!(NodeGraphMessageDiscriminant;)
} }
+5 -1
View File
@@ -300,7 +300,7 @@ import { defineComponent, nextTick } from "vue";
import type { IconName } from "@/utility-functions/icons"; 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 LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.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 outputPort2 = document.querySelectorAll(`[data-port="output"]`)[6] as HTMLDivElement | undefined;
const inputPort2 = document.querySelectorAll(`[data-port="input"]`)[3] as HTMLDivElement | undefined; const inputPort2 = document.querySelectorAll(`[data-port="input"]`)[3] as HTMLDivElement | undefined;
if (outputPort2 && inputPort2) this.createWirePath(outputPort2, inputPort2.getBoundingClientRect(), true, false); if (outputPort2 && inputPort2) this.createWirePath(outputPort2, inputPort2.getBoundingClientRect(), true, false);
this.editor.subscriptions.subscribeJsMessage(UpdateNodeGraphSelection, (updateNodeGraphSelection) => {
this.selected = updateNodeGraphSelection.selected;
});
}, },
components: { components: {
IconLabel, IconLabel,
+2
View File
@@ -262,6 +262,8 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo
item.getAsString((text) => { item.getAsString((text) => {
if (text.startsWith("graphite/layer: ")) { if (text.startsWith("graphite/layer: ")) {
editor.instance.pasteSerializedData(text.substring(16, text.length)); editor.instance.pasteSerializedData(text.substring(16, text.length));
} else if (text.startsWith("graphite/nodes: ")) {
editor.instance.pasteSerializedNodes(text.substring(16, text.length));
} }
}); });
} }
@@ -33,11 +33,17 @@ export class UpdateNodeGraph extends JsMessage {
@Type(() => FrontendNodeLink) @Type(() => FrontendNodeLink)
readonly links!: FrontendNodeLink[]; readonly links!: FrontendNodeLink[];
} }
export class UpdateNodeTypes extends JsMessage { export class UpdateNodeTypes extends JsMessage {
@Type(() => FrontendNode) @Type(() => FrontendNode)
readonly nodeTypes!: FrontendNodeType[]; readonly nodeTypes!: FrontendNodeType[];
} }
export class UpdateNodeGraphSelection extends JsMessage {
@Type(() => BigInt)
readonly selected!: bigint[];
}
export class UpdateNodeGraphVisibility extends JsMessage { export class UpdateNodeGraphVisibility extends JsMessage {
readonly visible!: boolean; readonly visible!: boolean;
} }
@@ -1407,6 +1413,7 @@ export const messageMakers: Record<string, MessageMaker> = {
UpdateMouseCursor, UpdateMouseCursor,
UpdateNodeGraph, UpdateNodeGraph,
UpdateNodeGraphBarLayout, UpdateNodeGraphBarLayout,
UpdateNodeGraphSelection,
UpdateNodeTypes, UpdateNodeTypes,
UpdateNodeGraphVisibility, UpdateNodeGraphVisibility,
UpdateOpenDocumentsList, UpdateOpenDocumentsList,
+7
View File
@@ -627,6 +627,13 @@ impl JsEditorHandle {
self.dispatch(message); 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 /// Notifies the backend that the user double clicked a node
#[wasm_bindgen(js_name = doubleClickNode)] #[wasm_bindgen(js_name = doubleClickNode)]
pub fn double_click_node(&self, node: u64) { pub fn double_click_node(&self, node: u64) {