Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions:
use crate::messages::portfolio::document::node_graph::utility_types::{ContextMenuData, Direction, FrontendGraphDataType, NodeGraphErrorDiagnostic};
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::misc::GroupFolderType;
use crate::messages::portfolio::document::utility_types::network_interface::{self, FlowType, InputConnector, NodeNetworkInterface, NodeTypePersistentMetadata, OutputConnector, Previewing};
use crate::messages::portfolio::document::utility_types::network_interface::{
self, FlowType, InputConnector, LayerPosition, NodeNetworkInterface, NodePosition, NodeTypePersistentMetadata, OutputConnector, Previewing,
};
use crate::messages::portfolio::document::utility_types::nodes::{CollapsedLayers, LayerPanelEntry};
use crate::messages::portfolio::document::utility_types::wires::{GraphWireStyle, WirePath, WirePathUpdate, build_vector_wire};
use crate::messages::prelude::*;
Expand Down Expand Up @@ -776,14 +778,56 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
return;
}

// Get network path of node overlay
let Some(network_metadata) = network_interface.network_metadata(breadcrumb_network_path) else {
log::error!("Could not get network metadata in InsertNodes");
return;
};

let cursor_viewport_location = ipp.mouse.position;
let cursor_to_node_graph = network_metadata
.persistent_metadata
.navigation_metadata
.node_graph_to_viewport
.inverse()
.transform_point2(cursor_viewport_location);

// Get position of the first node selected for copying that has an absolute position. Calculate paste offset from the cursor
// If no nodes with absolute position, then there is no offset from cursor
let copy_position_opt = nodes.iter().find_map(|(_, template)| match &template.persistent_node_metadata.node_type_metadata {
NodeTypePersistentMetadata::Layer(layer_metadata) => {
if let LayerPosition::Absolute(position) = &layer_metadata.position {
Some(position)
} else {
None
}
}
NodeTypePersistentMetadata::Node(node_metadata) => {
if let NodePosition::Absolute(position) = node_metadata.position() {
Some(position)
} else {
None
}
}
});

let copy_position = copy_position_opt.copied().unwrap_or_default();
let graph_delta = IVec2::new(
((cursor_to_node_graph.x / GRID_SIZE as f64).round()) as i32 - copy_position.x,
((cursor_to_node_graph.y / GRID_SIZE as f64).round()) as i32 - copy_position.y,
);

responses.add(DocumentMessage::AddTransaction);

let new_ids: HashMap<_, _> = nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect();

responses.add(NodeGraphMessage::AddNodes { nodes, new_ids: new_ids.clone() });

let nodes: Vec<_> = new_ids.values().copied().collect();
responses.add(NodeGraphMessage::SelectedNodesSet { nodes })
responses.add(NodeGraphMessage::SelectedNodesSet { nodes });

// Shift nodes based on offset
responses.add(NodeGraphMessage::ShiftSelectedNodesByAmount { graph_delta, rubber_band: true })
}
NodeGraphMessage::PointerDown {
shift_click,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ impl NodeNetworkInterface {
}

/// Creates a copy for each node by disconnecting nodes which are not connected to other copied nodes.
/// Returns an iterator of all persistent metadata for a node and their ids
/// Returns an iterator, sorted by original node id, of all persistent metadata for a node and their ids
pub fn copy_nodes<'a>(&'a mut self, new_ids: &'a HashMap<NodeId, NodeId>, network_path: &'a [NodeId]) -> impl Iterator<Item = (NodeId, NodeTemplate)> + 'a {
let mut new_nodes = new_ids
.iter()
Expand Down Expand Up @@ -442,6 +442,7 @@ impl NodeNetworkInterface {
}
}
}
new_nodes.sort_by_key(|a| a.0);
new_nodes.into_iter().map(move |(new, node_id, node)| (new, self.map_ids(node, &node_id, new_ids, network_path)))
}

Expand Down Expand Up @@ -6931,6 +6932,7 @@ impl NodePersistentMetadata {
pub fn new(position: NodePosition) -> Self {
Self { position }
}

pub fn position(&self) -> &NodePosition {
&self.position
}
Expand Down Expand Up @@ -7074,7 +7076,10 @@ pub fn collect_node_resources(node: &DocumentNode, out: &mut HashSet<ResourceId>

#[cfg(test)]
mod network_interface_tests {
use crate::test_utils::test_prelude::*;
use crate::{
messages::portfolio::document::utility_types::network_interface::{LayerPosition, NodePosition, NodeTypePersistentMetadata},
test_utils::test_prelude::*,
};
#[tokio::test]
async fn copy_isolated_node() {
let mut editor = EditorTestUtils::create();
Expand Down Expand Up @@ -7104,4 +7109,82 @@ mod network_interface_tests {
"duplicated node should exist\nother nodes: {nodes:#?}\norignal {orignal:#?}"
);
}

#[tokio::test]
async fn copy_and_paste_nodes() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
let rectangle = editor
.create_node_by_name_and_coordinates(DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::rectangle::IDENTIFIER), (0, 0))
.await;
let circle = editor
.create_node_by_name_and_coordinates(DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::circle::IDENTIFIER), (5, 5))
.await;
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![circle, rectangle] }).await;
let frontend_messages = editor.handle_message(NodeGraphMessage::Copy).await;
let clipboard = frontend_messages
.into_iter()
.find_map(|msg| match msg {
FrontendMessage::TriggerClipboardWrite { content } => Some(content),
_ => None,
})
.expect("copy message should be dispatched");
println!("Clipboard: {clipboard}");
// Move mouse then paste at new location
let mouse_pos = (24.0, 24.0);

editor.move_mouse(mouse_pos.0, mouse_pos.1, ModifierKeys::empty(), MouseKeys::empty()).await;
editor.left_mousedown(mouse_pos.0, mouse_pos.1, ModifierKeys::empty()).await;
editor.left_mouseup(mouse_pos.0, mouse_pos.1, ModifierKeys::empty()).await;
editor
.handle_message(ClipboardMessage::ReadClipboard {
content: ClipboardContentRaw::Text(clipboard),
})
.await;

// For each pasted node, check that the change in position is based on the delta of the mouse position and original circle. The mouse position in grid coordinates is (1,1). Since the original
// circle is at (5,5), the new circle should be pasted at (1,1), and the new square should be pasted at (-4, -4). Because we preserve selection order, the first selected pasted node should be
// a circle, the second a rectangle.
let new_positions: Vec<&IVec2> = editor
.active_document()
.network_interface
.selected_nodes()
.selected_nodes()
.map(|node_id| {
let node_metadata = &editor
.active_document()
.network_interface
.document_network_metadata()
.persistent_metadata
.node_metadata
.get(node_id)
.expect("Node should exist")
.persistent_metadata
.node_type_metadata;
let new_position = match node_metadata {
NodeTypePersistentMetadata::Layer(layer_metadata) => {
if let LayerPosition::Absolute(position) = &layer_metadata.position {
Some(position)
} else {
None
}
}
NodeTypePersistentMetadata::Node(node_metadata) => {
if let NodePosition::Absolute(position) = node_metadata.position() {
Some(position)
} else {
None
}
}
}
.expect("Pasted node should have a position");
new_position
})
.collect();
println!("positions of pasted nodes: {:?}", new_positions);
// Check position of pasted circle
assert_eq!(new_positions[0], &IVec2::new(1, 1));
// Check position of pasted rectangle
assert_eq!(new_positions[1], &IVec2::new(-4, -4));
}
}
12 changes: 12 additions & 0 deletions editor/src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,18 @@ impl EditorTestUtils {
.await;
node_id
}

pub async fn create_node_by_name_and_coordinates(&mut self, node_type: DefinitionIdentifier, xy: (i32, i32)) -> NodeId {
let node_id = NodeId::new();
self.handle_message(NodeGraphMessage::CreateNodeFromContextMenu {
node_id: Some(node_id),
node_type,
xy: Some(xy),
add_transaction: true,
})
.await;
node_id
}
}

pub trait FrontendMessageTestUtils {
Expand Down