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 8f4f7b3cf1
commit af001f8db6
8 changed files with 119 additions and 2 deletions

View File

@ -227,6 +227,9 @@ pub enum FrontendMessage {
layout_target: LayoutTarget,
layout: SubLayout,
},
UpdateNodeGraphSelection {
selected: Vec<NodeId>,
},
UpdateNodeGraphVisibility {
visible: bool,
},

View File

@ -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),

View File

@ -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<NodeId>,
@ -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<document_legacy::LayerId>,
},
PasteNodes {
serialized_nodes: String,
},
SelectNodes {
nodes: Vec<NodeId>,
},

View File

@ -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<Message>) {
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<NodeGraphMessage, (&mut Document, &InputPreprocessorMessageH
Self::send_graph(network, responses);
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 } => {
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<NodeGraphMessage, (&mut Document, &InputPreprocessorMessageH
);
Self::send_graph(network, responses);
}
NodeGraphMessage::Cut => {
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<NodeGraphMessage, (&mut Document, &InputPreprocessorMessageH
Self::send_graph(network, 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 } => {
self.selected_nodes = Vec::new();
@ -484,6 +543,32 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &InputPreprocessorMessageH
}
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 } => {
self.selected_nodes = nodes;
responses.push_back(PropertiesPanelMessage::ResendActiveProperties.into());
@ -582,7 +667,7 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &InputPreprocessorMessageH
fn actions(&self) -> ActionList {
if self.layer_path.is_some() && !self.selected_nodes.is_empty() {
actions!(NodeGraphMessageDiscriminant; DeleteSelectedNodes,)
actions!(NodeGraphMessageDiscriminant; DeleteSelectedNodes, Cut, Copy, DuplicateSelectedNodes)
} else {
actions!(NodeGraphMessageDiscriminant;)
}

View File

@ -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,

View File

@ -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));
}
});
}

View File

@ -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<string, MessageMaker> = {
UpdateMouseCursor,
UpdateNodeGraph,
UpdateNodeGraphBarLayout,
UpdateNodeGraphSelection,
UpdateNodeTypes,
UpdateNodeGraphVisibility,
UpdateOpenDocumentsList,

View File

@ -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) {