mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-01 02:18:12 +08:00
Pasting and duplicating nodes (#902)
* Pasting and duplicating nodes * Rebind duplication * Update selection on duplicate
This commit is contained in:
committed by
Keavon Chambers
parent
537dbf266a
commit
1f69b0f35f
@@ -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;)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user