diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index b7d6e12309..276f4b9882 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -186,7 +186,14 @@ impl Dispatcher { self.message_handlers.future_message_handler.process_message(message, &mut queue, FutureMessageContext {}); } Message::Broadcast(message) => self.message_handlers.broadcast_message_handler.process_message(message, &mut queue, ()), - Message::Clipboard(message) => self.message_handlers.clipboard_message_handler.process_message(message, &mut queue, ()), + Message::Clipboard(message) => { + let context = ClipboardMessageContext { + portfolio: &mut self.message_handlers.portfolio_message_handler, + current_tool: &self.message_handlers.tool_message_handler.tool_state.tool_data.active_tool_type, + resource_storage: &self.message_handlers.resource_storage_message_handler, + }; + self.message_handlers.clipboard_message_handler.process_message(message, &mut queue, context); + } Message::ColorPicker(message) => self.message_handlers.color_picker_message_handler.process_message(message, &mut queue, ()), Message::Debug(message) => { self.message_handlers.debug_message_handler.process_message(message, &mut queue, ()); @@ -419,141 +426,6 @@ impl Dispatcher { mod test { pub use crate::test_utils::test_prelude::*; - /// Create an editor with three layers - /// 1. A red rectangle - /// 2. A blue shape - /// 3. A green ellipse - async fn create_editor_with_three_layers() -> EditorTestUtils { - let mut editor = EditorTestUtils::create(); - - editor.new_document().await; - - editor.select_primary_color(Color::RED).await; - editor.draw_rect(100., 200., 300., 400.).await; - - editor.select_primary_color(Color::BLUE).await; - editor.draw_polygon(10., 1200., 1300., 400.).await; - - editor.select_primary_color(Color::GREEN).await; - editor.draw_ellipse(104., 1200., 1300., 400.).await; - - editor - } - - /// - create rect, shape and ellipse - /// - copy - /// - paste - /// - assert that ellipse was copied - #[tokio::test] - async fn copy_paste_single_layer() { - let mut editor = create_editor_with_three_layers().await; - - let layers_before_copy = editor.active_document().metadata().all_layers().collect::>(); - editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::Internal }).await; - editor - .handle_message(PortfolioMessage::PasteIntoFolder { - clipboard: Clipboard::Internal, - parent: LayerNodeIdentifier::ROOT_PARENT, - insert_index: 0, - }) - .await; - - let layers_after_copy = editor.active_document().metadata().all_layers().collect::>(); - - assert_eq!(layers_before_copy.len(), 3); - assert_eq!(layers_after_copy.len(), 4); - - // Existing layers are unaffected - for i in 0..=2 { - assert_eq!(layers_before_copy[i], layers_after_copy[i + 1]); - } - } - - #[cfg_attr(miri, ignore)] - /// - create rect, shape and ellipse - /// - select shape - /// - copy - /// - paste - /// - assert that shape was copied - #[tokio::test] - async fn copy_paste_single_layer_from_middle() { - let mut editor = create_editor_with_three_layers().await; - - let layers_before_copy = editor.active_document().metadata().all_layers().collect::>(); - let shape_id = editor.active_document().metadata().all_layers().nth(1).unwrap(); - - editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![shape_id.to_node()] }).await; - editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::Internal }).await; - editor - .handle_message(PortfolioMessage::PasteIntoFolder { - clipboard: Clipboard::Internal, - parent: LayerNodeIdentifier::ROOT_PARENT, - insert_index: 0, - }) - .await; - - let layers_after_copy = editor.active_document().metadata().all_layers().collect::>(); - - assert_eq!(layers_before_copy.len(), 3); - assert_eq!(layers_after_copy.len(), 4); - - // Existing layers are unaffected - for i in 0..=2 { - assert_eq!(layers_before_copy[i], layers_after_copy[i + 1]); - } - } - - #[cfg_attr(miri, ignore)] - /// - create rect, shape and ellipse - /// - select ellipse and rect - /// - copy - /// - delete - /// - create another rect - /// - paste - /// - paste - #[tokio::test] - async fn copy_paste_deleted_layers() { - let mut editor = create_editor_with_three_layers().await; - assert_eq!(editor.active_document().metadata().all_layers().count(), 3); - - let layers_before_copy = editor.active_document().metadata().all_layers().collect::>(); - let rect_id = layers_before_copy[0]; - let shape_id = layers_before_copy[1]; - let ellipse_id = layers_before_copy[2]; - - editor - .handle_message(NodeGraphMessage::SelectedNodesSet { - nodes: vec![rect_id.to_node(), ellipse_id.to_node()], - }) - .await; - editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::Internal }).await; - editor.handle_message(NodeGraphMessage::DeleteSelectedNodes { delete_children: true }).await; - editor.draw_rect(0., 800., 12., 200.).await; - editor - .handle_message(PortfolioMessage::PasteIntoFolder { - clipboard: Clipboard::Internal, - parent: LayerNodeIdentifier::ROOT_PARENT, - insert_index: 0, - }) - .await; - editor - .handle_message(PortfolioMessage::PasteIntoFolder { - clipboard: Clipboard::Internal, - parent: LayerNodeIdentifier::ROOT_PARENT, - insert_index: 0, - }) - .await; - - let layers_after_copy = editor.active_document().metadata().all_layers().collect::>(); - - assert_eq!(layers_before_copy.len(), 3); - assert_eq!(layers_after_copy.len(), 6); - - println!("{layers_after_copy:?} {layers_before_copy:?}"); - - assert_eq!(layers_after_copy[5], shape_id); - } - #[tokio::test] /// This test will fail when you make changes to the underlying serialization format for a document. async fn check_if_demo_art_opens() { diff --git a/editor/src/messages/clipboard/clipboard_message.rs b/editor/src/messages/clipboard/clipboard_message.rs index 920028a748..423bd539a2 100644 --- a/editor/src/messages/clipboard/clipboard_message.rs +++ b/editor/src/messages/clipboard/clipboard_message.rs @@ -1,4 +1,4 @@ -use crate::messages::clipboard::utility_types::{ClipboardContent, ClipboardContentRaw}; +use crate::messages::clipboard::utility_types::{ClipboardContent, ClipboardContentRaw, ClipboardItem, ClipboardLayer, ClipboardVectorEntry}; use crate::messages::prelude::*; #[impl_message(Message, Clipboard)] @@ -10,4 +10,11 @@ pub enum ClipboardMessage { ReadClipboard { content: ClipboardContentRaw }, ReadSelection { content: Option, cut: bool }, Write { content: ClipboardContent }, + + CopyLayers, + CutLayers, + WriteItems { items: Vec }, + PasteItems { data: String }, + PasteLayers { entries: Vec }, + PasteVectors { paths: Vec }, } diff --git a/editor/src/messages/clipboard/clipboard_message_handler.rs b/editor/src/messages/clipboard/clipboard_message_handler.rs index 57d6f31a4a..3e3481e7ec 100644 --- a/editor/src/messages/clipboard/clipboard_message_handler.rs +++ b/editor/src/messages/clipboard/clipboard_message_handler.rs @@ -1,36 +1,58 @@ -use crate::messages::clipboard::utility_types::{ClipboardContent, ClipboardContentRaw}; +use crate::consts::DEFAULT_STROKE_WIDTH; +use crate::messages::clipboard::utility_types::{ClipboardContent, ClipboardContentRaw, ClipboardItem, ClipboardLayer, ClipboardResource, ResourceData}; +use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; +use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_network_node_type; +use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; +use crate::messages::portfolio::document::utility_types::network_interface; +use crate::messages::portfolio::document::utility_types::nodes::SelectedNodes; use crate::messages::prelude::*; +use crate::messages::tool::common_functionality::graph_modification_utils; +use crate::messages::tool::utility_types::ToolType; +use graph_craft::application_io::resource::{DataSource, ResourceHash}; +use graph_craft::document::NodeId; +use graphene_std::Color; use graphene_std::raster::Image; +use graphene_std::subpath::BezierHandles; +use graphene_std::vector::misc::HandleId; +use graphene_std::vector::{PointId, SegmentId, VectorModificationType}; use graphite_proc_macros::{ExtractField, message_handler_data}; +use std::sync::Arc; -const CLIPBOARD_PREFIX_LAYER: &str = "graphite/layer: "; -const CLIPBOARD_PREFIX_NODES: &str = "graphite/nodes: "; -const CLIPBOARD_PREFIX_VECTOR: &str = "graphite/vector: "; +const CLIPBOARD_PREFIX: &str = "graphite: "; + +#[derive(ExtractField)] +pub struct ClipboardMessageContext<'a> { + pub portfolio: &'a mut PortfolioMessageHandler, + pub current_tool: &'a ToolType, + pub resource_storage: &'a ResourceStorageMessageHandler, +} #[derive(Debug, Clone, Default, ExtractField)] pub struct ClipboardMessageHandler {} #[message_handler_data] -impl MessageHandler for ClipboardMessageHandler { - fn process_message(&mut self, message: ClipboardMessage, responses: &mut std::collections::VecDeque, _: ()) { +impl MessageHandler> for ClipboardMessageHandler { + fn process_message(&mut self, message: ClipboardMessage, responses: &mut std::collections::VecDeque, context: ClipboardMessageContext) { + let ClipboardMessageContext { + portfolio, + current_tool, + resource_storage, + } = context; + match message { ClipboardMessage::Cut => responses.add(FrontendMessage::TriggerSelectionRead { cut: true }), ClipboardMessage::Copy => responses.add(FrontendMessage::TriggerSelectionRead { cut: false }), ClipboardMessage::Paste => responses.add(FrontendMessage::TriggerClipboardRead), ClipboardMessage::ReadClipboard { content } => match content { ClipboardContentRaw::Text(text) => { - if let Some(layer) = text.strip_prefix(CLIPBOARD_PREFIX_LAYER) { - responses.add(PortfolioMessage::PasteSerializedData { data: layer.to_string() }); - } else if let Some(nodes) = text.strip_prefix(CLIPBOARD_PREFIX_NODES) { - responses.add(NodeGraphMessage::PasteNodes { serialized_nodes: nodes.to_string() }); - } else if let Some(vector) = text.strip_prefix(CLIPBOARD_PREFIX_VECTOR) { - responses.add(PortfolioMessage::PasteSerializedVector { data: vector.to_string() }); + if let Some(graphite) = text.strip_prefix(CLIPBOARD_PREFIX) { + responses.add(ClipboardMessage::PasteItems { data: graphite.to_string() }); } else { responses.add(FrontendMessage::TriggerSelectionWrite { content: text }); } } ClipboardContentRaw::Svg(svg) => { - responses.add(PortfolioMessage::PasteSvg { + responses.add(PortfolioMessage::InsertSvg { svg, name: None, mouse: None, @@ -38,7 +60,7 @@ impl MessageHandler for ClipboardMessageHandler { }); } ClipboardContentRaw::Image { data, width, height } => { - responses.add(PortfolioMessage::PasteImage { + responses.add(PortfolioMessage::InsertImage { image: Image::from_image_data(&data, width, height), name: None, mouse: None, @@ -47,15 +69,14 @@ impl MessageHandler for ClipboardMessageHandler { } }, ClipboardMessage::ReadSelection { content, cut } => { - use crate::messages::portfolio::document::utility_types::clipboards::Clipboard; if let Some(text) = content { responses.add(ClipboardMessage::Write { content: ClipboardContent::Text(text), }); } else if cut { - responses.add(PortfolioMessage::Cut { clipboard: Clipboard::Device }); + responses.add(ClipboardMessage::CutLayers); } else { - responses.add(PortfolioMessage::Copy { clipboard: Clipboard::Device }); + responses.add(ClipboardMessage::CopyLayers); } } ClipboardMessage::Write { content } => { @@ -68,13 +89,381 @@ impl MessageHandler for ClipboardMessageHandler { log::error!("Image copying is not yet supported"); return; } - ClipboardContent::Layer(layer) => format!("{CLIPBOARD_PREFIX_LAYER}{layer}"), - ClipboardContent::Nodes(nodes) => format!("{CLIPBOARD_PREFIX_NODES}{nodes}"), - ClipboardContent::Vector(vector) => format!("{CLIPBOARD_PREFIX_VECTOR}{vector}"), + ClipboardContent::Graphite(graphite) => format!("{CLIPBOARD_PREFIX}{graphite}"), ClipboardContent::Text(text) => text, }; responses.add(FrontendMessage::TriggerClipboardWrite { content: text }); } + + ClipboardMessage::CopyLayers => { + if current_tool == &ToolType::Path { + responses.add(PathToolMessage::Copy); + return; + } + + let Some(active_document) = portfolio.active_document_id.and_then(|id| portfolio.documents.get_mut(&id)) else { + return; + }; + + if active_document.graph_view_overlay_open() { + responses.add(NodeGraphMessage::Copy); + return; + } + + let mut buffer = Vec::new(); + + let mut ordered_last_elements = active_document.network_interface.shallowest_unique_layers(&[]).collect::>(); + ordered_last_elements.sort_by_key(|layer| { + let Some(parent) = layer.parent(active_document.metadata()) else { return usize::MAX }; + DocumentMessageHandler::get_calculated_insert_index(active_document.metadata(), &SelectedNodes(vec![layer.to_node()]), parent) + }); + + for layer in ordered_last_elements.into_iter() { + let layer_node_id = layer.to_node(); + + let mut copy_ids = HashMap::new(); + copy_ids.insert(layer_node_id, NodeId(0)); + + active_document + .network_interface + .upstream_flow_back_from_nodes(vec![layer_node_id], &[], network_interface::FlowType::LayerChildrenUpstreamFlow) + .enumerate() + .for_each(|(index, node_id)| { + copy_ids.insert(node_id, NodeId((index + 1) as u64)); + }); + + // The layer panel keys collapse state on the top-down path of node IDs down to the layer + let mut tree_path = layer + .ancestors(active_document.metadata()) + .filter(|ancestor| *ancestor != LayerNodeIdentifier::ROOT_PARENT) + .map(|ancestor| ancestor.to_node()) + .collect::>(); + tree_path.reverse(); + + buffer.push(ClipboardLayer { + nodes: active_document.network_interface.copy_nodes(©_ids, &[]).collect(), + visible: active_document.network_interface.selected_nodes().layer_visible(layer, &active_document.network_interface), + locked: active_document.network_interface.selected_nodes().layer_locked(layer, &active_document.network_interface), + collapsed: active_document.collapsed.0.contains(&tree_path), + }); + } + + responses.add(ClipboardMessage::WriteItems { + items: buffer.into_iter().map(ClipboardItem::Layer).collect(), + }); + } + ClipboardMessage::CutLayers => { + if current_tool == &ToolType::Path { + responses.add(PathToolMessage::Cut); + return; + } + + if let Some(active_document) = portfolio.active_document() + && active_document.graph_view_overlay_open() + { + responses.add(NodeGraphMessage::Cut); + return; + } + + responses.add(ClipboardMessage::CopyLayers); + responses.add(DocumentMessage::DeleteSelectedLayers); + } + ClipboardMessage::WriteItems { items } => { + let has_content = items.iter().any(|item| match item { + ClipboardItem::Layer(entry) => !entry.nodes.is_empty(), + ClipboardItem::Nodes(nodes) => !nodes.is_empty(), + ClipboardItem::Vector(vector) => !vector.is_empty(), + ClipboardItem::Resource(_) => true, + }); + if !has_content { + return; + } + + let mut resource_ids = HashSet::new(); + for item in &items { + match item { + ClipboardItem::Layer(entry) => entry + .nodes + .iter() + .for_each(|(_, template)| network_interface::collect_node_resources(&template.document_node, &mut resource_ids)), + ClipboardItem::Nodes(nodes) => nodes + .iter() + .for_each(|(_, template)| network_interface::collect_node_resources(&template.document_node, &mut resource_ids)), + ClipboardItem::Vector(_) | ClipboardItem::Resource(_) => {} + } + } + + // Snapshot each resource's registry entry; embedded ones also need their bytes + let mut resources = Vec::new(); + let mut bytes_to_load = Vec::new(); + if let Some(document) = portfolio.active_document() { + for id in resource_ids { + let Some(info) = document.resources.registry.info(&id) else { continue }; + if let Some(hash) = info.hash + && info.sources.contains(&DataSource::Embedded) + { + bytes_to_load.push((resources.len(), *hash)); + } + resources.push(ClipboardResource { + id, + hash: info.hash.copied(), + sources: info.sources.to_vec(), + data: None, + }); + } + } + + if bytes_to_load.is_empty() { + let mut items = items; + items.extend(resources.into_iter().map(ClipboardItem::Resource)); + if let Some(content) = serialize_clipboard(&items) { + responses.add(ClipboardMessage::Write { content }); + } + } else { + // Load the embedded bytes from the resource storage, then write + let load_handle = resource_storage.resources(); + responses.add(async move { + let mut resources = resources; + for (index, hash) in bytes_to_load { + if let Some(resource) = load_handle.load(hash).await { + resources[index].data = Some(ResourceData(resource.as_ref().to_vec())); + } + } + let mut items = items; + items.extend(resources.into_iter().map(ClipboardItem::Resource)); + match serialize_clipboard(&items) { + Some(content) => ClipboardMessage::Write { content }.into(), + None => Message::NoOp, + } + }); + } + } + ClipboardMessage::PasteItems { data } => { + let items = match serde_json::from_str::>(&data) { + Ok(items) => items, + Err(error) => { + log::error!("Failed to deserialize clipboard payload: {error}"); + return; + } + }; + + let mut layers = Vec::new(); + let mut node_groups = Vec::new(); + let mut vectors = Vec::new(); + let mut resources = Vec::new(); + for item in items { + match item { + ClipboardItem::Layer(entry) => layers.push(entry), + ClipboardItem::Nodes(nodes) => node_groups.push(nodes), + ClipboardItem::Vector(vector) => vectors.push(vector), + ClipboardItem::Resource(resource) => resources.push(resource), + } + } + + // Re-register the carried resources and store their bytes + let mut needs_resolve = false; + if !resources.is_empty() + && let Some(document) = portfolio.active_document_mut() + { + for resource in resources { + if document.resources.registry.contains(&resource.id) { + continue; + } + for source in resource.sources { + document.resources.registry.push_source_back(&resource.id, source); + } + match resource.data { + Some(data) => match resource.hash { + Some(hash) if ResourceHash::from(data.0.as_slice()) == hash => { + document.resources.registry.resolve(&resource.id, hash); + responses.add(ResourceStorageMessage::Store { data: Arc::from(data.0) }); + } + _ => warn!("Discarding pasted resource {:?}: embedded bytes do not match its advertised hash", resource.id), + }, + None => needs_resolve = true, + } + } + } + + // Paste the content through the existing per-type handlers + if !layers.is_empty() { + responses.add(ClipboardMessage::PasteLayers { entries: layers }); + } + for nodes in node_groups { + responses.add(NodeGraphMessage::InsertNodes { nodes }); + } + for paths in vectors { + responses.add(ClipboardMessage::PasteVectors { paths }); + } + + // Re-resolve resources that carried no bytes (URL- or font-backed) + if needs_resolve && let Some(document_id) = portfolio.active_document_id { + responses.add(PortfolioMessage::ResolveDocumentResources { document_id }); + } + } + ClipboardMessage::PasteLayers { entries } => { + if let Some(document) = portfolio.active_document() { + let parent = document.new_layer_parent(false); + + // Top-down node ID path of the destination parent, used to derive each pasted layer's collapse path + let mut parent_tree_path = parent + .ancestors(document.metadata()) + .filter(|ancestor| *ancestor != LayerNodeIdentifier::ROOT_PARENT) + .map(|ancestor| ancestor.to_node()) + .collect::>(); + parent_tree_path.reverse(); + + let mut all_new_ids = Vec::new(); + let mut layers = Vec::new(); + + let mut added_nodes = false; + + for entry in entries.into_iter().rev() { + let new_ids: HashMap<_, _> = entry.nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect(); + let Some(&root_id) = new_ids.get(&NodeId(0)) else { + warn!("Skipping pasted layer missing its root node"); + continue; + }; + let layer = LayerNodeIdentifier::new_unchecked(root_id); + + if !added_nodes { + responses.add(DocumentMessage::DeselectAllLayers); + responses.add(DocumentMessage::AddTransaction); + added_nodes = true; + } + + all_new_ids.extend(new_ids.values().copied()); + + responses.add(NodeGraphMessage::AddNodes { nodes: entry.nodes, new_ids }); + responses.add(NodeGraphMessage::MoveLayerToStack { layer, parent, insert_index: 0 }); + + // Restore the copied layer's visibility, lock, and collapse state + if !entry.visible { + responses.add(NodeGraphMessage::SetVisibility { + node_id: root_id, + network_path: Vec::new(), + visible: false, + }); + } + if entry.locked { + responses.add(NodeGraphMessage::SetLocked { + node_id: root_id, + network_path: Vec::new(), + locked: true, + }); + } + if entry.collapsed { + let mut tree_path = parent_tree_path.clone(); + tree_path.push(root_id); + responses.add(DocumentMessage::ToggleLayerExpansion { tree_path, recursive: false }); + } + + layers.push(layer); + } + + responses.add(NodeGraphMessage::RunDocumentGraph); + responses.add(NodeGraphMessage::SelectedNodesSet { nodes: all_new_ids }); + responses.add(DeferMessage::AfterGraphRun { + messages: vec![PortfolioMessage::CenterLayers { layers }.into()], + }); + } + } + ClipboardMessage::PasteVectors { paths } => { + // If using Path tool then send the operation to Path tool + // TODO: Consider if this is actually the correct place to put this logic + // TODO: Consider making paste in general go through the current tool, so that the tool can decide what to do + if *current_tool == ToolType::Path { + responses.add(PathToolMessage::Paste { paths }); + return; + } + + // If not using Path tool, create new layers and add paths into those + if let Some(document) = portfolio.active_document() { + let mut layers = Vec::new(); + + for (_, new_vector, transform) in paths { + let Some(node_type) = resolve_network_node_type("Path") else { + error!("Path node does not exist"); + continue; + }; + let nodes = vec![(NodeId(0), node_type.default_node_template())]; + + let parent = document.new_layer_parent(false); + + let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses); + layers.push(layer); + + // Adding the transform back into the layer + responses.add(GraphOperationMessage::TransformSet { + layer, + transform, + transform_in: TransformIn::Local, + skip_rerender: false, + }); + + // Add default fill and stroke to the layer + let fill = graphene_std::vector::style::Fill::solid(Color::WHITE); + responses.add(GraphOperationMessage::FillSet { layer, fill }); + + let stroke = graphene_std::vector::style::Stroke::new(Some(Color::BLACK), DEFAULT_STROKE_WIDTH); + responses.add(GraphOperationMessage::StrokeSet { layer, stroke }); + + // Create new point ids and add those into the existing Vector path + let mut points_map = HashMap::new(); + for (point, position) in new_vector.point_domain.iter() { + let new_point_id = PointId::generate(); + points_map.insert(point, new_point_id); + let modification_type = VectorModificationType::InsertPoint { id: new_point_id, position }; + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + } + + // Create new segment ids and add the segments into the existing Vector path + let mut segments_map = HashMap::new(); + for (segment_id, bezier, start, end) in new_vector.segment_bezier_iter() { + let (Some(&start_point), Some(&end_point)) = (points_map.get(&start), points_map.get(&end)) else { + warn!("Skipping pasted vector segment with an unknown endpoint"); + continue; + }; + + let new_segment_id = SegmentId::generate(); + segments_map.insert(segment_id, new_segment_id); + + let handles = match bezier.handles { + BezierHandles::Linear => [None, None], + BezierHandles::Quadratic { handle } => [Some(handle - bezier.start), None], + BezierHandles::Cubic { handle_start, handle_end } => [Some(handle_start - bezier.start), Some(handle_end - bezier.end)], + }; + + let points = [start_point, end_point]; + let modification_type = VectorModificationType::InsertSegment { id: new_segment_id, points, handles }; + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + } + + // Set G1 continuity + for handles in new_vector.colinear_manipulators { + let to_new_handle = |handle: HandleId| -> Option { + Some(HandleId { + ty: handle.ty, + segment: *segments_map.get(&handle.segment)?, + }) + }; + let (Some(first), Some(second)) = (to_new_handle(handles[0]), to_new_handle(handles[1])) else { + continue; + }; + let modification_type = VectorModificationType::SetG1Continuous { + handles: [first, second], + enabled: true, + }; + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + } + } + + responses.add(NodeGraphMessage::RunDocumentGraph); + responses.add(Message::Defer(DeferMessage::AfterGraphRun { + messages: vec![PortfolioMessage::CenterLayers { layers }.into()], + })); + } + } } } advertise_actions!(ClipboardMessageDiscriminant; @@ -83,3 +472,183 @@ impl MessageHandler for ClipboardMessageHandler { Paste, ); } + +/// Serialize the clipboard items, logging on failure. +fn serialize_clipboard(items: &[ClipboardItem]) -> Option { + match serde_json::to_string(items) { + Ok(data) => Some(ClipboardContent::Graphite(data)), + Err(error) => { + log::error!("Failed to serialize clipboard payload: {error}"); + None + } + } +} + +#[cfg(test)] +mod test { + use crate::messages::clipboard::utility_types::{ClipboardItem, ClipboardResource, ResourceData}; + use crate::test_utils::test_prelude::*; + use graph_craft::application_io::resource::{DataSource, ResourceHash, ResourceId}; + + /// Create an editor with three layers + /// 1. A red rectangle + /// 2. A blue shape + /// 3. A green ellipse + async fn create_editor_with_three_layers() -> EditorTestUtils { + let mut editor = EditorTestUtils::create(); + + editor.new_document().await; + + editor.select_primary_color(Color::RED).await; + editor.draw_rect(100., 200., 300., 400.).await; + + editor.select_primary_color(Color::BLUE).await; + editor.draw_polygon(10., 1200., 1300., 400.).await; + + editor.select_primary_color(Color::GREEN).await; + editor.draw_ellipse(104., 1200., 1300., 400.).await; + + editor + } + + /// Copies the layer selection and returns the written clipboard payload. + async fn copy_layers_to_clipboard(editor: &mut EditorTestUtils) -> String { + editor + .handle_message(ClipboardMessage::CopyLayers) + .await + .into_iter() + .find_map(|message| match message { + FrontendMessage::TriggerClipboardWrite { content } => Some(content), + _ => None, + }) + .expect("copying layers should write a payload to the clipboard") + } + + /// Pastes a clipboard string as if read from the system clipboard. + async fn paste_from_clipboard(editor: &mut EditorTestUtils, clipboard: &str) { + editor + .handle_message(ClipboardMessage::ReadClipboard { + content: ClipboardContentRaw::Text(clipboard.to_string()), + }) + .await; + } + + /// - create rect, shape and ellipse + /// - copy + /// - paste + /// - assert that ellipse was copied + #[tokio::test] + async fn copy_paste_single_layer() { + let mut editor = create_editor_with_three_layers().await; + + let layers_before_copy = editor.active_document().metadata().all_layers().collect::>(); + let clipboard = copy_layers_to_clipboard(&mut editor).await; + paste_from_clipboard(&mut editor, &clipboard).await; + + let layers_after_copy = editor.active_document().metadata().all_layers().collect::>(); + + assert_eq!(layers_before_copy.len(), 3); + assert_eq!(layers_after_copy.len(), 4); + + // Existing layers are unaffected + for i in 0..=2 { + assert_eq!(layers_before_copy[i], layers_after_copy[i + 1]); + } + } + + #[cfg_attr(miri, ignore)] + /// - create rect, shape and ellipse + /// - select shape + /// - copy + /// - paste + /// - assert that shape was copied + #[tokio::test] + async fn copy_paste_single_layer_from_middle() { + let mut editor = create_editor_with_three_layers().await; + + let layers_before_copy = editor.active_document().metadata().all_layers().collect::>(); + let shape_id = editor.active_document().metadata().all_layers().nth(1).unwrap(); + + editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![shape_id.to_node()] }).await; + let clipboard = copy_layers_to_clipboard(&mut editor).await; + paste_from_clipboard(&mut editor, &clipboard).await; + + let layers_after_copy = editor.active_document().metadata().all_layers().collect::>(); + + assert_eq!(layers_before_copy.len(), 3); + assert_eq!(layers_after_copy.len(), 4); + + // Existing layers are unaffected + for i in 0..=2 { + assert_eq!(layers_before_copy[i], layers_after_copy[i + 1]); + } + } + + #[cfg_attr(miri, ignore)] + /// - create rect, shape and ellipse + /// - select ellipse and rect + /// - copy + /// - delete + /// - create another rect + /// - paste + /// - paste + #[tokio::test] + async fn copy_paste_deleted_layers() { + let mut editor = create_editor_with_three_layers().await; + assert_eq!(editor.active_document().metadata().all_layers().count(), 3); + + let layers_before_copy = editor.active_document().metadata().all_layers().collect::>(); + let rect_id = layers_before_copy[0]; + let shape_id = layers_before_copy[1]; + let ellipse_id = layers_before_copy[2]; + + editor + .handle_message(NodeGraphMessage::SelectedNodesSet { + nodes: vec![rect_id.to_node(), ellipse_id.to_node()], + }) + .await; + let clipboard = copy_layers_to_clipboard(&mut editor).await; + editor.handle_message(NodeGraphMessage::DeleteSelectedNodes { delete_children: true }).await; + editor.draw_rect(0., 800., 12., 200.).await; + paste_from_clipboard(&mut editor, &clipboard).await; + paste_from_clipboard(&mut editor, &clipboard).await; + + let layers_after_copy = editor.active_document().metadata().all_layers().collect::>(); + + assert_eq!(layers_before_copy.len(), 3); + assert_eq!(layers_after_copy.len(), 6); + + assert_eq!(layers_after_copy[5], shape_id); + } + + /// A pasted `graphite:` payload re-registers the resources it carries into the active document. + #[tokio::test] + async fn paste_carries_embedded_resource() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + + let id = ResourceId::new(); + assert!(!editor.active_document().resources.registry.contains(&id)); + + // Carry the resource and its bytes through the clipboard + let bytes = b"a pretend png".to_vec(); + let hash = ResourceHash::from(bytes.as_slice()); + let items = vec![ClipboardItem::Resource(ClipboardResource { + id, + hash: Some(hash), + sources: vec![DataSource::Embedded], + data: Some(ResourceData(bytes)), + })]; + let payload = format!("graphite: {}", serde_json::to_string(&items).unwrap()); + + editor + .handle_message(ClipboardMessage::ReadClipboard { + content: ClipboardContentRaw::Text(payload), + }) + .await; + + let registry = &editor.active_document().resources.registry; + assert!(registry.contains(&id), "the carried resource should be registered after paste"); + assert_eq!(registry.hash(&id), Some(hash), "the carried resource should resolve to the carried bytes' hash"); + } +} diff --git a/editor/src/messages/clipboard/mod.rs b/editor/src/messages/clipboard/mod.rs index e090860708..5d48836c07 100644 --- a/editor/src/messages/clipboard/mod.rs +++ b/editor/src/messages/clipboard/mod.rs @@ -5,4 +5,4 @@ pub mod utility_types; #[doc(inline)] pub use clipboard_message::{ClipboardMessage, ClipboardMessageDiscriminant}; #[doc(inline)] -pub use clipboard_message_handler::ClipboardMessageHandler; +pub use clipboard_message_handler::{ClipboardMessageContext, ClipboardMessageHandler}; diff --git a/editor/src/messages/clipboard/utility_types.rs b/editor/src/messages/clipboard/utility_types.rs index d32319098e..a4298222ca 100644 --- a/editor/src/messages/clipboard/utility_types.rs +++ b/editor/src/messages/clipboard/utility_types.rs @@ -1,3 +1,12 @@ +use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; +use crate::messages::portfolio::document::utility_types::network_interface::NodeTemplate; +use base64::Engine; +use base64::engine::general_purpose::STANDARD as BASE64; +use glam::DAffine2; +use graph_craft::application_io::resource::{DataSource, ResourceHash, ResourceId}; +use graph_craft::document::NodeId; +use graphene_std::vector::Vector; + #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum ClipboardContentRaw { Text(String), @@ -7,10 +16,55 @@ pub enum ClipboardContentRaw { #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum ClipboardContent { - Layer(String), - Nodes(String), - Vector(String), + Graphite(String), Text(String), Svg(String), Image { data: Vec, width: u32, height: u32 }, } + +pub type ClipboardVectorEntry = (LayerNodeIdentifier, Vector, DAffine2); + +/// An entry in the `graphite:` clipboard payload. +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] +pub enum ClipboardItem { + Layer(ClipboardLayer), + Nodes(Vec<(NodeId, NodeTemplate)>), + Vector(Vec), + Resource(ClipboardResource), +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct ClipboardLayer { + pub nodes: Vec<(NodeId, NodeTemplate)>, + pub visible: bool, + pub locked: bool, + pub collapsed: bool, +} + +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct ClipboardResource { + pub id: ResourceId, + pub hash: Option, + pub sources: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub data: Option, +} + +#[derive(PartialEq, Clone)] +pub struct ResourceData(pub Vec); +impl std::fmt::Debug for ResourceData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ResourceData").field("len", &self.0.len()).finish() + } +} +impl serde::Serialize for ResourceData { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_str(&BASE64.encode(&self.0)) + } +} +impl<'de> serde::Deserialize<'de> for ResourceData { + fn deserialize>(deserializer: D) -> Result { + let encoded = String::deserialize(deserializer)?; + Ok(Self(BASE64.decode(&encoded).map_err(serde::de::Error::custom)?)) + } +} diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index 2e93961f1e..8023d0f926 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -7,7 +7,6 @@ use crate::messages::input_mapper::utility_types::macros::*; use crate::messages::input_mapper::utility_types::misc::MappingEntry; use crate::messages::input_mapper::utility_types::misc::{KeyMappingEntries, Mapping}; use crate::messages::portfolio::document::node_graph::utility_types::Direction; -use crate::messages::portfolio::document::utility_types::clipboards::Clipboard; use crate::messages::portfolio::document::utility_types::misc::GroupFolderType; use crate::messages::prelude::*; use crate::messages::tool::tool_messages::brush_tool::BrushToolMessageOptionsUpdate; @@ -224,8 +223,8 @@ pub fn input_mappings(zoom_with_scroll: bool) -> Mapping { entry!(KeyDown(Backspace); modifiers=[Accel], action_dispatch=PathToolMessage::DeleteAndBreakPath), entry!(KeyDown(Delete); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath), entry!(KeyDown(Backspace); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath), - entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=PathToolMessage::Cut { clipboard: Clipboard::Device }), - entry!(KeyDown(KeyC); modifiers=[Accel], action_dispatch=PathToolMessage::Copy { clipboard: Clipboard::Device }), + entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=PathToolMessage::Cut), + entry!(KeyDown(KeyC); modifiers=[Accel], action_dispatch=PathToolMessage::Copy), entry!(KeyDown(KeyD); modifiers=[Accel], action_dispatch=PathToolMessage::Duplicate), entry!(KeyDownNoRepeat(Tab); action_dispatch=PathToolMessage::SwapSelectedHandles), entry!(KeyDown(MouseLeft); action_dispatch=PathToolMessage::MouseDown { extend_selection: Shift, lasso_select: Control, handle_drag_from_anchor: Alt, drag_restore_handle: Control, segment_editing_modifier: Control }), @@ -447,8 +446,6 @@ pub fn input_mappings(zoom_with_scroll: bool) -> Mapping { entry!(KeyDown(KeyW); modifiers=[Accel, Alt], action_dispatch=PortfolioMessage::CloseAllDocumentsWithConfirmation), entry!(KeyDown(KeyO); modifiers=[Accel], action_dispatch=PortfolioMessage::Open), entry!(KeyDown(KeyI); modifiers=[Accel], action_dispatch=PortfolioMessage::Import), - entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=PortfolioMessage::Cut { clipboard: Clipboard::Device }), - entry!(KeyDown(KeyC); modifiers=[Accel], action_dispatch=PortfolioMessage::Copy { clipboard: Clipboard::Device }), entry!(KeyDown(KeyR); modifiers=[Alt], action_dispatch=PortfolioMessage::ToggleRulers), entry!(KeyDown(KeyD); modifiers=[Alt], action_dispatch=PortfolioMessage::ToggleDataPanelOpen), entry!(KeyDown(Enter); modifiers=[Alt], action_dispatch=PortfolioMessage::ToggleFocusDocument), diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index 203287fb58..e43494d283 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -112,7 +112,7 @@ pub enum DocumentMessage { resize: Key, resize_opposite: Key, }, - PasteImage { + InsertImage { name: Option, image: Image, mouse: Option<(f64, f64)>, @@ -121,7 +121,7 @@ pub enum DocumentMessage { /// can wrap it without a content Transform node. When false, place at the cursor or viewport center. place_at_origin: bool, }, - PasteSvg { + InsertSvg { name: Option, svg: String, mouse: Option<(f64, f64)>, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index dcffdac794..98f6c90062 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -753,7 +753,7 @@ impl MessageHandler> for DocumentMes let resize_opposite = ipp.keyboard.key(resize_opposite); self.nudge_selected_layers(delta_x, delta_y, resize, resize_opposite, responses); } - DocumentMessage::PasteImage { + DocumentMessage::InsertImage { name, image, mouse, @@ -817,7 +817,7 @@ impl MessageHandler> for DocumentMes // Force chosen tool to be Select Tool after importing image. responses.add(ToolMessage::ActivateTool { tool_type: ToolType::Select }); } - DocumentMessage::PasteSvg { + DocumentMessage::InsertSvg { name, svg, mouse, 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 8e864f66a2..3b7aa266ba 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 @@ -97,8 +97,8 @@ pub enum NodeGraphMessage { SetChainPosition { node_id: NodeId, }, - PasteNodes { - serialized_nodes: String, + InsertNodes { + nodes: Vec<(NodeId, NodeTemplate)>, }, PointerDown { shift_click: bool, 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 c9023c732d..f2a5211e11 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 @@ -1,7 +1,7 @@ use super::node_properties; use super::utility_types::{BoxSelection, ContextMenuInformation, DragStart, FrontendNode}; use crate::consts::GRID_SIZE; -use crate::messages::clipboard::utility_types::ClipboardContent; +use crate::messages::clipboard::utility_types::ClipboardItem; use crate::messages::input_mapper::utility_types::macros::{action_shortcut, action_shortcut_manual}; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::document_message_handler::navigation_controls; @@ -12,9 +12,7 @@ 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, NodeTemplate, NodeTypePersistentMetadata, OutputConnector, Previewing, -}; +use crate::messages::portfolio::document::utility_types::network_interface::{self, FlowType, InputConnector, NodeNetworkInterface, 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::*; @@ -248,12 +246,8 @@ impl<'a> MessageHandler> for NodeG let new_ids = &all_selected_nodes.iter().enumerate().map(|(new, old)| (*old, NodeId(new as u64))).collect(); let copied_nodes = network_interface.copy_nodes(new_ids, selection_network_path).collect::>(); - let Ok(data) = serde_json::to_string(&copied_nodes) else { - log::error!("Failed to serialize nodes for clipboard"); - return; - }; - responses.add(ClipboardMessage::Write { - content: ClipboardContent::Nodes(data), + responses.add(ClipboardMessage::WriteItems { + items: vec![ClipboardItem::Nodes(copied_nodes)], }); } NodeGraphMessage::CreateNodeInLayerNoTransaction { node_type, layer } => { @@ -762,26 +756,18 @@ impl<'a> MessageHandler> for NodeG NodeGraphMessage::SetChainPosition { node_id } => { network_interface.set_chain_position(&node_id, selection_network_path); } - NodeGraphMessage::PasteNodes { serialized_nodes } => { - let data = match serde_json::from_str::>(&serialized_nodes) { - Ok(d) => d, - Err(e) => { - warn!("Invalid node data {e:?}"); - return; - } - }; - if data.is_empty() { + NodeGraphMessage::InsertNodes { nodes } => { + if nodes.is_empty() { return; } responses.add(DocumentMessage::AddTransaction); - let new_ids: HashMap<_, _> = data.iter().map(|(id, _)| (*id, NodeId::new())).collect(); + 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::AddNodes { - nodes: data, - new_ids: new_ids.clone(), - }); responses.add(NodeGraphMessage::SelectedNodesSet { nodes }) } NodeGraphMessage::PointerDown { diff --git a/editor/src/messages/portfolio/document/utility_types/clipboards.rs b/editor/src/messages/portfolio/document/utility_types/clipboards.rs deleted file mode 100644 index 12d9b54654..0000000000 --- a/editor/src/messages/portfolio/document/utility_types/clipboards.rs +++ /dev/null @@ -1,23 +0,0 @@ -use super::network_interface::NodeTemplate; -use graph_craft::document::NodeId; - -#[repr(u8)] -#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, PartialEq, Eq, Debug)] -pub enum Clipboard { - Internal, - Device, - - _InternalClipboardCount, // Keep this as the last entry of **internal** clipboards since it is used for counting the number of enum variants -} - -pub const INTERNAL_CLIPBOARD_COUNT: u8 = Clipboard::_InternalClipboardCount as u8; - -#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] -pub struct CopyBufferEntry { - pub nodes: Vec<(NodeId, NodeTemplate)>, - pub selected: bool, - pub visible: bool, - pub locked: bool, - pub collapsed: bool, -} diff --git a/editor/src/messages/portfolio/document/utility_types/mod.rs b/editor/src/messages/portfolio/document/utility_types/mod.rs index 8bed0dbb85..1fcf5c60ca 100644 --- a/editor/src/messages/portfolio/document/utility_types/mod.rs +++ b/editor/src/messages/portfolio/document/utility_types/mod.rs @@ -1,4 +1,3 @@ -pub mod clipboards; pub mod document_metadata; pub mod error; pub mod misc; diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index cdd8ef06a7..4f1adc60c2 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -6832,17 +6832,22 @@ pub enum TransactionStatus { fn collect_network_resources(network: &NodeNetwork, out: &mut HashSet) { for node in network.nodes.values() { - for input in &node.inputs { - if let NodeInput::Value { tagged_value, .. } = input - && let TaggedValue::Resource(id) = &**tagged_value - { - out.insert(*id); - } - } - if let DocumentNodeImplementation::Network(nested) = &node.implementation { - collect_network_resources(nested, out); + collect_node_resources(node, out); + } +} + +/// Collects resource IDs referenced by a node and its nested networks. +pub fn collect_node_resources(node: &DocumentNode, out: &mut HashSet) { + for input in &node.inputs { + if let NodeInput::Value { tagged_value, .. } = input + && let TaggedValue::Resource(id) = &**tagged_value + { + out.insert(*id); } } + if let DocumentNodeImplementation::Network(nested) = &node.implementation { + collect_network_resources(nested, out); + } } #[cfg(test)] @@ -6857,18 +6862,19 @@ mod network_interface_tests { .await; editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![rectangle] }).await; let frontend_messages = editor.handle_message(NodeGraphMessage::Copy).await; - let serialized_nodes = frontend_messages + let clipboard = frontend_messages .into_iter() .find_map(|msg| match msg { FrontendMessage::TriggerClipboardWrite { content } => Some(content), _ => None, }) - .expect("copy message should be dispatched") - .strip_prefix("graphite/nodes: ") - .expect("should start with magic string") - .to_string(); - println!("Serialized: {serialized_nodes}"); - editor.handle_message(NodeGraphMessage::PasteNodes { serialized_nodes }).await; + .expect("copy message should be dispatched"); + println!("Clipboard: {clipboard}"); + editor + .handle_message(ClipboardMessage::ReadClipboard { + content: ClipboardContentRaw::Text(clipboard), + }) + .await; let nodes = &mut editor.active_document_mut().network_interface.network_mut(&[]).unwrap().nodes; let orignal = nodes.remove(&rectangle).expect("original node should exist"); assert!( diff --git a/editor/src/messages/portfolio/portfolio_message.rs b/editor/src/messages/portfolio/portfolio_message.rs index 596afae458..196ba4d468 100644 --- a/editor/src/messages/portfolio/portfolio_message.rs +++ b/editor/src/messages/portfolio/portfolio_message.rs @@ -2,7 +2,6 @@ use super::document::utility_types::document_metadata::LayerNodeIdentifier; use super::persistent_state::PersistentStateMessage; use super::utility_types::{DockingSplitDirection, PanelGroupId, PanelType}; use crate::messages::frontend::utility_types::{ExportBounds, FileType, PersistedState}; -use crate::messages::portfolio::document::utility_types::clipboards::Clipboard; use crate::messages::prelude::*; use graphene_std::Color; use graphene_std::raster::Image; @@ -39,12 +38,6 @@ pub enum PortfolioMessage { CloseDocumentWithConfirmation { document_id: DocumentId, }, - Copy { - clipboard: Clipboard, - }, - Cut { - clipboard: Clipboard, - }, DeleteDocument { document_id: DocumentId, }, @@ -113,31 +106,19 @@ pub enum PortfolioMessage { name: Option, svg: String, }, - PasteSerializedData { - data: String, - }, - PasteSerializedVector { - data: String, - }, - PasteImage { + InsertImage { name: Option, image: Image, mouse: Option<(f64, f64)>, parent_and_insert_index: Option<(LayerNodeIdentifier, usize)>, }, - PasteSvg { + InsertSvg { name: Option, svg: String, mouse: Option<(f64, f64)>, parent_and_insert_index: Option<(LayerNodeIdentifier, usize)>, }, - // TODO: Unused except by tests, remove? - PasteIntoFolder { - clipboard: Clipboard, - parent: LayerNodeIdentifier, - insert_index: usize, - }, - CenterPastedLayers { + CenterLayers { layers: Vec, }, PrevDocument, diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 98b3012e61..5e848aded9 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -1,11 +1,9 @@ use super::document::utility_types::document_metadata::LayerNodeIdentifier; -use super::document::utility_types::network_interface; use super::persistent_state::{PersistentStateMessage, PersistentStateMessageContext, PersistentStateMessageHandler}; use super::utility_types::{PanelLayoutSubdivision, PanelType, WorkspacePanelLayout}; use crate::application::{Editor, generate_uuid}; -use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH, FILE_EXTENSION}; +use crate::consts::{DEFAULT_DOCUMENT_NAME, FILE_EXTENSION}; use crate::messages::animation::TimingInformation; -use crate::messages::clipboard::utility_types::ClipboardContent; use crate::messages::dialog::simple_dialogs; use crate::messages::frontend::utility_types::{DocumentInfo, PersistedState}; use crate::messages::input_mapper::utility_types::input_keyboard::Key; @@ -13,15 +11,12 @@ use crate::messages::input_mapper::utility_types::macros::{action_shortcut, acti use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::DocumentMessageContext; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; -use crate::messages::portfolio::document::node_graph::document_node_definitions::{self, resolve_network_node_type}; -use crate::messages::portfolio::document::utility_types::clipboards::{Clipboard, CopyBufferEntry, INTERNAL_CLIPBOARD_COUNT}; +use crate::messages::portfolio::document::node_graph::document_node_definitions; use crate::messages::portfolio::document::utility_types::network_interface::OutputConnector; -use crate::messages::portfolio::document::utility_types::nodes::SelectedNodes; use crate::messages::portfolio::document_migration::*; use crate::messages::portfolio::utility_types::FileContent; use crate::messages::preferences::SelectionMode; use crate::messages::prelude::*; -use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::utility_types::{HintData, ToolType}; use crate::messages::viewport::ToPhysical; use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor}; @@ -31,9 +26,6 @@ use graph_craft::document::NodeId; use graphene_std::Color; use graphene_std::raster_types::Image; use graphene_std::renderer::Quad; -use graphene_std::subpath::BezierHandles; -use graphene_std::vector::misc::HandleId; -use graphene_std::vector::{PointId, SegmentId, Vector, VectorModificationType}; use std::path::PathBuf; use std::sync::Arc; use std::vec; @@ -68,7 +60,6 @@ pub struct PortfolioMessageHandler { pub(crate) active_document_id: Option, persistent_state: PersistentStateMessageHandler, pub fonts: FontsMessageHandler, - copy_buffer: [Vec; INTERNAL_CLIPBOARD_COUNT as usize], pub executor: NodeGraphExecutor, pub selection_mode: SelectionMode, pub reset_node_definitions_on_open: bool, @@ -281,86 +272,6 @@ impl MessageHandler> for Portfolio responses.add(PortfolioMessage::SelectDocument { document_id }); } } - PortfolioMessage::Copy { clipboard } => { - if context.current_tool == &ToolType::Path { - responses.add(PathToolMessage::Copy { clipboard }); - return; - } - - // We can't use `self.active_document()` because it counts as an immutable borrow of the entirety of `self` - let Some(active_document) = self.active_document_id.and_then(|id| self.documents.get_mut(&id)) else { - return; - }; - - if active_document.graph_view_overlay_open() { - responses.add(NodeGraphMessage::Copy); - return; - } - - let mut copy_val = |buffer: &mut Vec| { - let mut ordered_last_elements = active_document.network_interface.shallowest_unique_layers(&[]).collect::>(); - - ordered_last_elements.sort_by_key(|layer| { - let Some(parent) = layer.parent(active_document.metadata()) else { return usize::MAX }; - DocumentMessageHandler::get_calculated_insert_index(active_document.metadata(), &SelectedNodes(vec![layer.to_node()]), parent) - }); - - for layer in ordered_last_elements.into_iter() { - let layer_node_id = layer.to_node(); - - let mut copy_ids = HashMap::new(); - copy_ids.insert(layer_node_id, NodeId(0)); - - active_document - .network_interface - .upstream_flow_back_from_nodes(vec![layer_node_id], &[], network_interface::FlowType::LayerChildrenUpstreamFlow) - .enumerate() - .for_each(|(index, node_id)| { - copy_ids.insert(node_id, NodeId((index + 1) as u64)); - }); - - buffer.push(CopyBufferEntry { - nodes: active_document.network_interface.copy_nodes(©_ids, &[]).collect(), - selected: active_document.network_interface.selected_nodes().selected_layers_contains(layer, active_document.metadata()), - visible: active_document.network_interface.selected_nodes().layer_visible(layer, &active_document.network_interface), - locked: active_document.network_interface.selected_nodes().layer_locked(layer, &active_document.network_interface), - collapsed: false, - }); - } - }; - - if clipboard == Clipboard::Device { - let mut buffer = Vec::new(); - copy_val(&mut buffer); - let Ok(data) = serde_json::to_string(&buffer) else { - log::error!("Failed to serialize nodes for clipboard"); - return; - }; - responses.add(ClipboardMessage::Write { - content: ClipboardContent::Layer(data), - }); - } else { - let copy_buffer = &mut self.copy_buffer; - copy_buffer[clipboard as usize].clear(); - copy_val(&mut copy_buffer[clipboard as usize]); - } - } - PortfolioMessage::Cut { clipboard } => { - if context.current_tool == &ToolType::Path { - responses.add(PathToolMessage::Cut { clipboard }); - return; - } - - if let Some(active_document) = self.active_document() - && active_document.graph_view_overlay_open() - { - responses.add(NodeGraphMessage::Cut); - return; - } - - responses.add(PortfolioMessage::Copy { clipboard }); - responses.add(DocumentMessage::DeleteSelectedLayers); - } PortfolioMessage::DeleteDocument { document_id } => { let document_index = self.document_index(document_id); self.documents.remove(&document_id); @@ -759,7 +670,7 @@ impl MessageHandler> for Portfolio }); } FileContent::Svg(svg) => { - responses.add(PortfolioMessage::PasteSvg { + responses.add(PortfolioMessage::InsertSvg { name, svg, mouse: None, @@ -767,7 +678,7 @@ impl MessageHandler> for Portfolio }); } FileContent::Image(image) => { - responses.add(PortfolioMessage::PasteImage { + responses.add(PortfolioMessage::InsertImage { name, image, mouse: None, @@ -959,7 +870,7 @@ impl MessageHandler> for Portfolio name: name.clone().unwrap_or_default(), }); - responses.add(DocumentMessage::PasteImage { + responses.add(DocumentMessage::InsertImage { name, image, mouse: None, @@ -1014,7 +925,7 @@ impl MessageHandler> for Portfolio }) }); - responses.add(DocumentMessage::PasteSvg { + responses.add(DocumentMessage::InsertSvg { name, svg, mouse: None, @@ -1036,153 +947,7 @@ impl MessageHandler> for Portfolio messages: vec![DocumentMessage::ZoomCanvasToFitAll.into()], }); } - // TODO: Unused except by tests, remove? - PortfolioMessage::PasteIntoFolder { clipboard, parent, insert_index } => { - let mut all_new_ids = Vec::new(); - let paste = |entry: &CopyBufferEntry, responses: &mut VecDeque<_>, all_new_ids: &mut Vec| { - if self.active_document().is_some() { - trace!("Pasting into folder {parent:?} as index: {insert_index}"); - let nodes = entry.clone().nodes; - let new_ids: HashMap<_, _> = nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect(); - let layer = LayerNodeIdentifier::new_unchecked(new_ids[&NodeId(0)]); - all_new_ids.extend(new_ids.values().cloned()); - responses.add(NodeGraphMessage::AddNodes { nodes, new_ids: new_ids.clone() }); - responses.add(NodeGraphMessage::MoveLayerToStack { layer, parent, insert_index }); - } - }; - - responses.add(DocumentMessage::DeselectAllLayers); - - for entry in self.copy_buffer[clipboard as usize].iter().rev() { - paste(entry, responses, &mut all_new_ids) - } - responses.add(NodeGraphMessage::RunDocumentGraph); - responses.add(NodeGraphMessage::SelectedNodesSet { nodes: all_new_ids }); - } - PortfolioMessage::PasteSerializedData { data } => { - if let Some(document) = self.active_document() { - let mut all_new_ids = Vec::new(); - if let Ok(data) = serde_json::from_str::>(&data) { - let parent = document.new_layer_parent(false); - let mut layers = Vec::new(); - - let mut added_nodes = false; - - for entry in data.into_iter().rev() { - if !added_nodes { - responses.add(DocumentMessage::DeselectAllLayers); - responses.add(DocumentMessage::AddTransaction); - added_nodes = true; - } - - let new_ids: HashMap<_, _> = entry.nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect(); - let layer = LayerNodeIdentifier::new_unchecked(new_ids[&NodeId(0)]); - all_new_ids.extend(new_ids.values().cloned()); - - responses.add(NodeGraphMessage::AddNodes { nodes: entry.nodes, new_ids }); - responses.add(NodeGraphMessage::MoveLayerToStack { layer, parent, insert_index: 0 }); - layers.push(layer); - } - - responses.add(NodeGraphMessage::RunDocumentGraph); - responses.add(NodeGraphMessage::SelectedNodesSet { nodes: all_new_ids }); - responses.add(DeferMessage::AfterGraphRun { - messages: vec![PortfolioMessage::CenterPastedLayers { layers }.into()], - }); - } - } - } - // Custom paste implementation for Path tool - PortfolioMessage::PasteSerializedVector { data } => { - // If using Path tool then send the operation to Path tool - if *current_tool == ToolType::Path { - responses.add(PathToolMessage::Paste { data }); - return; - } - - // If not using Path tool, create new layers and add paths into those - if let Some(document) = self.active_document() { - let Ok(data) = serde_json::from_str::>(&data) else { - return; - }; - - let mut layers = Vec::new(); - - for (_, new_vector, transform) in data { - let Some(node_type) = resolve_network_node_type("Path") else { - error!("Path node does not exist"); - continue; - }; - let nodes = vec![(NodeId(0), node_type.default_node_template())]; - - let parent = document.new_layer_parent(false); - - let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses); - layers.push(layer); - - // Adding the transform back into the layer - responses.add(GraphOperationMessage::TransformSet { - layer, - transform, - transform_in: TransformIn::Local, - skip_rerender: false, - }); - - // Add default fill and stroke to the layer - let fill = graphene_std::vector::style::Fill::solid(Color::WHITE); - responses.add(GraphOperationMessage::FillSet { layer, fill }); - - let stroke = graphene_std::vector::style::Stroke::new(Some(Color::BLACK), DEFAULT_STROKE_WIDTH); - responses.add(GraphOperationMessage::StrokeSet { layer, stroke }); - - // Create new point ids and add those into the existing Vector path - let mut points_map = HashMap::new(); - for (point, position) in new_vector.point_domain.iter() { - let new_point_id = PointId::generate(); - points_map.insert(point, new_point_id); - let modification_type = VectorModificationType::InsertPoint { id: new_point_id, position }; - responses.add(GraphOperationMessage::Vector { layer, modification_type }); - } - - // Create new segment ids and add the segments into the existing Vector path - let mut segments_map = HashMap::new(); - for (segment_id, bezier, start, end) in new_vector.segment_bezier_iter() { - let new_segment_id = SegmentId::generate(); - - segments_map.insert(segment_id, new_segment_id); - - let handles = match bezier.handles { - BezierHandles::Linear => [None, None], - BezierHandles::Quadratic { handle } => [Some(handle - bezier.start), None], - BezierHandles::Cubic { handle_start, handle_end } => [Some(handle_start - bezier.start), Some(handle_end - bezier.end)], - }; - - let points = [points_map[&start], points_map[&end]]; - let modification_type = VectorModificationType::InsertSegment { id: new_segment_id, points, handles }; - responses.add(GraphOperationMessage::Vector { layer, modification_type }); - } - - // Set G1 continuity - for handles in new_vector.colinear_manipulators { - let to_new_handle = |handle: HandleId| -> HandleId { - HandleId { - ty: handle.ty, - segment: segments_map[&handle.segment], - } - }; - let new_handles = [to_new_handle(handles[0]), to_new_handle(handles[1])]; - let modification_type = VectorModificationType::SetG1Continuous { handles: new_handles, enabled: true }; - responses.add(GraphOperationMessage::Vector { layer, modification_type }); - } - } - - responses.add(NodeGraphMessage::RunDocumentGraph); - responses.add(Message::Defer(DeferMessage::AfterGraphRun { - messages: vec![PortfolioMessage::CenterPastedLayers { layers }.into()], - })); - } - } - PortfolioMessage::CenterPastedLayers { layers } => { + PortfolioMessage::CenterLayers { layers } => { if let Some(document) = self.active_document_mut() { let viewport_bounds_quad_pixels = Quad::from_box([DVec2::ZERO, viewport.size().into_dvec2()]); // In viewport pixel coordinates let viewport_center_pixels = viewport_bounds_quad_pixels.center(); // In viewport pixel coordinates @@ -1287,7 +1052,7 @@ impl MessageHandler> for Portfolio responses.add(NodeGraphMessage::RunDocumentGraph); } } - PortfolioMessage::PasteImage { + PortfolioMessage::InsertImage { name, image, mouse, @@ -1296,7 +1061,7 @@ impl MessageHandler> for Portfolio if self.document_ids.is_empty() { responses.add(PortfolioMessage::OpenImage { name, image }); } else { - responses.add(DocumentMessage::PasteImage { + responses.add(DocumentMessage::InsertImage { name, image, mouse, @@ -1305,7 +1070,7 @@ impl MessageHandler> for Portfolio }); } } - PortfolioMessage::PasteSvg { + PortfolioMessage::InsertSvg { name, svg, mouse, @@ -1314,7 +1079,7 @@ impl MessageHandler> for Portfolio if self.document_ids.is_empty() { responses.add(PortfolioMessage::OpenSvg { name, svg }); } else { - responses.add(DocumentMessage::PasteSvg { + responses.add(DocumentMessage::InsertSvg { name, svg, mouse, @@ -1816,14 +1581,6 @@ impl MessageHandler> for Portfolio PrevDocument, Import, )); - - // Extend with actions that must have a selected layer - if document.network_interface.selected_nodes().selected_layers(document.metadata()).next().is_some() { - common.extend(actions!(PortfolioMessageDiscriminant; - Copy, - Cut, - )); - } } // Extend with actions that are disabled when focusing the document diff --git a/editor/src/messages/prelude.rs b/editor/src/messages/prelude.rs index cded44b1c2..3b99ad1afb 100644 --- a/editor/src/messages/prelude.rs +++ b/editor/src/messages/prelude.rs @@ -7,7 +7,7 @@ pub use crate::messages::animation::{AnimationMessage, AnimationMessageDiscrimin pub use crate::messages::app_window::{AppWindowMessage, AppWindowMessageDiscriminant, AppWindowMessageHandler}; pub use crate::messages::broadcast::event::{EventMessage, EventMessageContext, EventMessageDiscriminant, EventMessageHandler}; pub use crate::messages::broadcast::{BroadcastMessage, BroadcastMessageDiscriminant, BroadcastMessageHandler}; -pub use crate::messages::clipboard::{ClipboardMessage, ClipboardMessageDiscriminant, ClipboardMessageHandler}; +pub use crate::messages::clipboard::{ClipboardMessage, ClipboardMessageContext, ClipboardMessageDiscriminant, ClipboardMessageHandler}; pub use crate::messages::color_picker::{ColorPickerMessage, ColorPickerMessageDiscriminant, ColorPickerMessageHandler}; pub use crate::messages::debug::{DebugMessage, DebugMessageDiscriminant, DebugMessageHandler}; pub use crate::messages::defer::{DeferMessage, DeferMessageDiscriminant, DeferMessageHandler}; diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index b3dd093cb2..72aa7a1c9a 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -5,13 +5,12 @@ use crate::consts::{ DOUBLE_CLICK_MILLISECONDS, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, DRAG_THRESHOLD, DRILL_THROUGH_THRESHOLD, HANDLE_ROTATE_SNAP_ANGLE, SEGMENT_INSERTION_DISTANCE, SEGMENT_OVERLAY_SIZE, SELECTION_THRESHOLD, SELECTION_TOLERANCE, }; -use crate::messages::clipboard::utility_types::ClipboardContent; +use crate::messages::clipboard::utility_types::{ClipboardItem, ClipboardVectorEntry}; use crate::messages::input_mapper::utility_types::macros::action_shortcut_manual; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_network_node_type; use crate::messages::portfolio::document::overlays::utility_functions::{path_overlays, selected_segments}; use crate::messages::portfolio::document::overlays::utility_types::{DrawHandles, OverlayContext}; -use crate::messages::portfolio::document::utility_types::clipboards::Clipboard; use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface; use crate::messages::portfolio::document::utility_types::transformation::Axis; @@ -141,14 +140,11 @@ pub enum PathToolMessage { overlay_context: OverlayContext, }, StartSlidingPoint, - Copy { - clipboard: Clipboard, - }, - Cut { - clipboard: Clipboard, - }, + Copy, + Cut, Paste { - data: String, + #[cfg_attr(feature = "wasm", tsify(type = "unknown"))] + paths: Vec, }, DeleteSelected, Duplicate, @@ -2749,7 +2745,7 @@ impl Fsm for PathToolFsmState { PathToolFsmState::Ready } } - (_, PathToolMessage::Copy { clipboard }) => { + (_, PathToolMessage::Copy) => { // TODO: Add support for selected segments let mut buffer = Vec::new(); @@ -2808,131 +2804,121 @@ impl Fsm for PathToolFsmState { buffer.push((layer, new_vector, transform)); } - if clipboard == Clipboard::Device { - if let Ok(data) = serde_json::to_string(&buffer) { - responses.add(ClipboardMessage::Write { - content: ClipboardContent::Vector(data), - }); - } else { - log::error!("Failed to serialize nodes for clipboard"); - } - } - // TODO: Add implementation for internal clipboard + responses.add(ClipboardMessage::WriteItems { + items: vec![ClipboardItem::Vector(buffer)], + }); PathToolFsmState::Ready } - (_, PathToolMessage::Cut { clipboard }) => { - responses.add(PathToolMessage::Copy { clipboard }); + (_, PathToolMessage::Cut) => { + responses.add(PathToolMessage::Copy); // Delete the selected points/segments responses.add(PathToolMessage::DeleteSelected); PathToolFsmState::Ready } - (_, PathToolMessage::Paste { data }) => { - // Deserialize the data - if let Ok(data) = serde_json::from_str::>(&data) { - shape_editor.deselect_all_points(); - responses.add(DocumentMessage::AddTransaction); - let mut new_layers = Vec::new(); - for (layer, new_vector, transform) in data { - // If layer is not selected then create a new selected layer - let layer = if shape_editor.selected_shape_state.contains_key(&layer) { - layer - } else { - let Some(node_type) = resolve_network_node_type("Path") else { - error!("Could not resolve node type for Path"); - continue; - }; - let nodes = vec![(NodeId(0), node_type.default_node_template())]; - - let parent = document.new_layer_parent(false); - - let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses); - - // Defaults chosen because the pasted geometry has no inherent associated style - let stroke = graphene_std::vector::style::Stroke::new(Some(Color::BLACK), DEFAULT_STROKE_WIDTH); - responses.add(GraphOperationMessage::StrokeSet { layer, stroke }); - - let fill = graphene_std::vector::style::Fill::solid(Color::WHITE); - responses.add(GraphOperationMessage::FillSet { layer, fill }); - - new_layers.push(layer); - - responses.add(GraphOperationMessage::TransformSet { - layer, - transform, - transform_in: TransformIn::Local, - skip_rerender: false, - }); - - layer - }; - - // Create new point ids and add those into the existing vector content - let mut points_map = HashMap::new(); - for (point, position) in new_vector.point_domain.iter() { - let new_point_id = PointId::generate(); - points_map.insert(point, new_point_id); - - let modification_type = VectorModificationType::InsertPoint { id: new_point_id, position }; - - responses.add(GraphOperationMessage::Vector { layer, modification_type }); - } - - // Create new segment ids and add the segments into the existing vector content - let mut segments_map = HashMap::new(); - for (segment_id, bezier, start, end) in new_vector.segment_iter() { - let new_segment_id = SegmentId::generate(); - - segments_map.insert(segment_id, new_segment_id); - - let points = pathseg_points(bezier); - let handles = [points.p1.map(|handle| handle - points.p0), points.p2.map(|handle| handle - points.p3)]; - - let points = [points_map[&start], points_map[&end]]; - let modification_type = VectorModificationType::InsertSegment { id: new_segment_id, points, handles }; - - responses.add(GraphOperationMessage::Vector { layer, modification_type }); - } - - // Set G1 continuity - for handles in new_vector.colinear_manipulators { - let to_new_handle = |handle: HandleId| -> HandleId { - HandleId { - ty: handle.ty, - segment: segments_map[&handle.segment], - } - }; - let new_handles = [to_new_handle(handles[0]), to_new_handle(handles[1])]; - let modification_type = VectorModificationType::SetG1Continuous { handles: new_handles, enabled: true }; - - responses.add(GraphOperationMessage::Vector { layer, modification_type }); - } - - shape_editor.selected_shape_state.entry(layer).or_insert(Default::default()); - - // Set selection to newly inserted points - let Some(state) = shape_editor.selected_shape_state.get_mut(&layer) else { - error!("No state for layer: {layer:?}"); + (_, PathToolMessage::Paste { paths }) => { + shape_editor.deselect_all_points(); + responses.add(DocumentMessage::AddTransaction); + let mut new_layers = Vec::new(); + for (layer, new_vector, transform) in paths { + // If layer is not selected then create a new selected layer + let layer = if shape_editor.selected_shape_state.contains_key(&layer) { + layer + } else { + let Some(node_type) = resolve_network_node_type("Path") else { + error!("Could not resolve node type for Path"); continue; }; + let nodes = vec![(NodeId(0), node_type.default_node_template())]; - // If point editing mode is enabled, select all the pasted points - if tool_options.path_editing_mode.point_editing_mode { - points_map.values().for_each(|point| state.select_point(ManipulatorPointId::Anchor(*point))); - } - // If segment editing mode is enabled, select all the pasted segments - if tool_options.path_editing_mode.segment_editing_mode { - segments_map.values().for_each(|segment| state.select_segment(*segment)); - } + let parent = document.new_layer_parent(false); + + let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses); + + // Defaults chosen because the pasted geometry has no inherent associated style + let stroke = graphene_std::vector::style::Stroke::new(Some(Color::BLACK), DEFAULT_STROKE_WIDTH); + responses.add(GraphOperationMessage::StrokeSet { layer, stroke }); + + let fill = graphene_std::vector::style::Fill::solid(Color::WHITE); + responses.add(GraphOperationMessage::FillSet { layer, fill }); + + new_layers.push(layer); + + responses.add(GraphOperationMessage::TransformSet { + layer, + transform, + transform_in: TransformIn::Local, + skip_rerender: false, + }); + + layer + }; + + // Create new point ids and add those into the existing vector content + let mut points_map = HashMap::new(); + for (point, position) in new_vector.point_domain.iter() { + let new_point_id = PointId::generate(); + points_map.insert(point, new_point_id); + + let modification_type = VectorModificationType::InsertPoint { id: new_point_id, position }; + + responses.add(GraphOperationMessage::Vector { layer, modification_type }); } - // If there are new layers created, we need to center them in the viewport - if !new_layers.is_empty() { - responses.add(Message::Defer(DeferMessage::AfterGraphRun { - messages: vec![PortfolioMessage::CenterPastedLayers { layers: new_layers }.into()], - })); + // Create new segment ids and add the segments into the existing vector content + let mut segments_map = HashMap::new(); + for (segment_id, bezier, start, end) in new_vector.segment_iter() { + let new_segment_id = SegmentId::generate(); + + segments_map.insert(segment_id, new_segment_id); + + let points = pathseg_points(bezier); + let handles = [points.p1.map(|handle| handle - points.p0), points.p2.map(|handle| handle - points.p3)]; + + let points = [points_map[&start], points_map[&end]]; + let modification_type = VectorModificationType::InsertSegment { id: new_segment_id, points, handles }; + + responses.add(GraphOperationMessage::Vector { layer, modification_type }); } + + // Set G1 continuity + for handles in new_vector.colinear_manipulators { + let to_new_handle = |handle: HandleId| -> HandleId { + HandleId { + ty: handle.ty, + segment: segments_map[&handle.segment], + } + }; + let new_handles = [to_new_handle(handles[0]), to_new_handle(handles[1])]; + let modification_type = VectorModificationType::SetG1Continuous { handles: new_handles, enabled: true }; + + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + } + + shape_editor.selected_shape_state.entry(layer).or_insert(Default::default()); + + // Set selection to newly inserted points + let Some(state) = shape_editor.selected_shape_state.get_mut(&layer) else { + error!("No state for layer: {layer:?}"); + continue; + }; + + // If point editing mode is enabled, select all the pasted points + if tool_options.path_editing_mode.point_editing_mode { + points_map.values().for_each(|point| state.select_point(ManipulatorPointId::Anchor(*point))); + } + // If segment editing mode is enabled, select all the pasted segments + if tool_options.path_editing_mode.segment_editing_mode { + segments_map.values().for_each(|segment| state.select_segment(*segment)); + } + } + + // If there are new layers created, we need to center them in the viewport + if !new_layers.is_empty() { + responses.add(Message::Defer(DeferMessage::AfterGraphRun { + messages: vec![PortfolioMessage::CenterLayers { layers: new_layers }.into()], + })); } PathToolFsmState::Ready diff --git a/editor/src/test_utils.rs b/editor/src/test_utils.rs index 23552601e6..d6078b1f10 100644 --- a/editor/src/test_utils.rs +++ b/editor/src/test_utils.rs @@ -245,7 +245,7 @@ impl EditorTestUtils { } pub async fn create_raster_image(&mut self, image: graphene_std::raster::Image, mouse: Option<(f64, f64)>) { - self.handle_message(PortfolioMessage::PasteImage { + self.handle_message(PortfolioMessage::InsertImage { name: None, image, mouse, @@ -346,10 +346,10 @@ pub mod test_prelude { pub use super::FrontendMessageTestUtils; pub use crate::application::Editor; pub use crate::float_eq; + pub use crate::messages::clipboard::utility_types::ClipboardContentRaw; pub use crate::messages::input_mapper::utility_types::input_keyboard::{Key, ModifierKeys}; pub use crate::messages::input_mapper::utility_types::input_mouse::MouseKeys; pub use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier; - pub use crate::messages::portfolio::document::utility_types::clipboards::Clipboard; pub use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; pub use crate::messages::prelude::*; pub use crate::messages::tool::common_functionality::graph_modification_utils::{NodeGraphLayer, is_layer_fed_by_node_of_name}; diff --git a/frontend/wrapper/src/editor_wrapper.rs b/frontend/wrapper/src/editor_wrapper.rs index 2b91cf1125..478252bfaf 100644 --- a/frontend/wrapper/src/editor_wrapper.rs +++ b/frontend/wrapper/src/editor_wrapper.rs @@ -877,7 +877,7 @@ impl EditorWrapper { None }; - let message = PortfolioMessage::PasteImage { + let message = PortfolioMessage::InsertImage { name, image, mouse, @@ -899,7 +899,7 @@ impl EditorWrapper { None }; - let message = PortfolioMessage::PasteSvg { + let message = PortfolioMessage::InsertSvg { name, svg, mouse,