From 5100c3929ba50ea3fe97e1d79dcc73a88752695c Mon Sep 17 00:00:00 2001 From: Adesh Gupta Date: Tue, 1 Jul 2025 15:56:42 +0530 Subject: [PATCH 1/5] Copy and Paste for paths --- .../messages/input_mapper/input_mappings.rs | 2 + .../messages/portfolio/portfolio_message.rs | 3 + .../portfolio/portfolio_message_handler.rs | 37 +++++++++- .../tool/common_functionality/shape_editor.rs | 3 + .../messages/tool/tool_messages/path_tool.rs | 73 +++++++++++++++++++ frontend/src/io-managers/input.ts | 2 + frontend/wasm/src/editor_api.rs | 7 ++ .../src/vector/vector_data/attributes.rs | 2 +- .../src/vector/vector_data/modification.rs | 24 ++++++ 9 files changed, 151 insertions(+), 2 deletions(-) diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index c2465d00d2..d013af5b8f 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -211,6 +211,8 @@ pub fn input_mappings() -> 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!(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 }), entry!(KeyDown(MouseRight); action_dispatch=PathToolMessage::RightClick), diff --git a/editor/src/messages/portfolio/portfolio_message.rs b/editor/src/messages/portfolio/portfolio_message.rs index cb2fa34e3e..de2e09e859 100644 --- a/editor/src/messages/portfolio/portfolio_message.rs +++ b/editor/src/messages/portfolio/portfolio_message.rs @@ -87,6 +87,9 @@ pub enum PortfolioMessage { PasteSerializedData { data: String, }, + PasteSerializedVector { + data: String, + }, CenterPastedLayers { layers: Vec, }, diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 5abb0918b4..afa877f92c 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -3,7 +3,7 @@ use super::document::utility_types::network_interface; use super::spreadsheet::SpreadsheetMessageHandler; use super::utility_types::{PanelType, PersistentData}; use crate::application::generate_uuid; -use crate::consts::DEFAULT_DOCUMENT_NAME; +use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH}; use crate::messages::animation::TimingInformation; use crate::messages::debug::utility_types::MessageLoggingVerbosity; use crate::messages::dialog::simple_dialogs; @@ -11,17 +11,21 @@ use crate::messages::frontend::utility_types::FrontendDocumentDetails; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::DocumentMessageData; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; +use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; use crate::messages::portfolio::document::utility_types::clipboards::{Clipboard, CopyBufferEntry, INTERNAL_CLIPBOARD_COUNT}; use crate::messages::portfolio::document::utility_types::nodes::SelectedNodes; use crate::messages::portfolio::document_migration::*; 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, HintGroup, ToolType}; use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor}; use glam::{DAffine2, DVec2}; use graph_craft::document::NodeId; +use graphene_std::Color; use graphene_std::renderer::Quad; use graphene_std::text::Font; +use graphene_std::vector::{VectorData, VectorModificationType}; use std::vec; pub struct PortfolioMessageData<'a> { @@ -488,6 +492,37 @@ impl MessageHandler> for PortfolioMes } } } + // Custom paste implementation for path tool + PortfolioMessage::PasteSerializedVector { data } => { + if let Some(document) = self.active_document() { + if let Ok(data) = serde_json::from_str::>(&data) { + for new_vector in data { + let node_type = resolve_document_node_type("Path").expect("Path node does not exist"); + 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); + + // Add default fill and stroke to the layer + let fill_color = Color::WHITE; + let stroke_color = Color::BLACK; + + let fill = graphene_std::vector::style::Fill::solid(fill_color.to_gamma_srgb()); + responses.add(GraphOperationMessage::FillSet { layer, fill }); + + let stroke = graphene_std::vector::style::Stroke::new(Some(stroke_color.to_gamma_srgb()), DEFAULT_STROKE_WIDTH); + responses.add(GraphOperationMessage::StrokeSet { layer, stroke }); + + // Set the new vector data to this layer + let modification_type = VectorModificationType::SetNewVectorData { new_vector_data: new_vector }; + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + } + } + } + + // Make a new default layer for each of those vector datas and insert those layers in the node graph + } PortfolioMessage::CenterPastedLayers { layers } => { if let Some(document) = self.active_document_mut() { let viewport_bounds_quad_pixels = Quad::from_box([DVec2::ZERO, ipp.viewport_bounds.size()]); diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index c1f7e99f08..45332904d7 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -54,6 +54,9 @@ pub struct SelectedLayerState { } impl SelectedLayerState { + pub fn is_empty(&self) -> bool { + self.selected_points.is_empty() + } pub fn selected(&self) -> impl Iterator + '_ { self.selected_points.iter().copied() } diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 51c1fa579d..cb0b9ca171 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -6,6 +6,7 @@ use crate::consts::{ }; 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; @@ -106,6 +107,12 @@ pub enum PathToolMessage { UpdateSelectedPointsStatus { overlay_context: OverlayContext, }, + Copy { + clipboard: Clipboard, + }, + Cut { + clipboard: Clipboard, + }, } #[derive(PartialEq, Eq, Hash, Copy, Clone, Debug, Default, serde::Serialize, serde::Deserialize, specta::Type)] @@ -286,6 +293,8 @@ impl<'a> MessageHandler> for PathToo DeleteAndBreakPath, ClosePath, PointerMove, + Copy, + Cut ), PathToolFsmState::Dragging(_) => actions!(PathToolMessageDiscriminant; Escape, @@ -297,6 +306,8 @@ impl<'a> MessageHandler> for PathToo BreakPath, DeleteAndBreakPath, SwapSelectedHandles, + Copy, + Cut, ), PathToolFsmState::Drawing { .. } => actions!(PathToolMessageDiscriminant; FlipSmoothSharp, @@ -1928,6 +1939,68 @@ impl Fsm for PathToolFsmState { shape_editor.delete_point_and_break_path(document, responses); PathToolFsmState::Ready } + (_, PathToolMessage::Copy { clipboard }) => { + // TODO: Add support for selected segments + + let mut buffer = Vec::new(); + + for (&layer, layer_selection_state) in &shape_editor.selected_shape_state { + if layer_selection_state.is_empty() { + continue; + } + + let Some(old_vector_data) = document.network_interface.compute_modified_vector(layer) else { + continue; + }; + + let mut new_vector_data = VectorData::default(); + + // Add all the selected points + for (point, position) in old_vector_data.point_domain.iter() { + if layer_selection_state.is_selected(ManipulatorPointId::Anchor(point)) { + new_vector_data.point_domain.push(point, position); + } + } + + let find_index = |id: PointId| { + new_vector_data + .point_domain + .iter() + .enumerate() + .find(|(_, (point_id, _))| *point_id == id) + .expect("Point does not exist in point domain") + .0 + }; + + // Add segments which have selected ends + for ((segment_id, bezier, start, end), stroke) in old_vector_data.segment_bezier_iter().zip(old_vector_data.segment_domain.stroke().iter()) { + if layer_selection_state.is_selected(ManipulatorPointId::Anchor(start)) && layer_selection_state.is_selected(ManipulatorPointId::Anchor(end)) { + let start_index = find_index(start); + let end_index = find_index(end); + new_vector_data.segment_domain.push(segment_id, start_index, end_index, bezier.handles, *stroke); + } + } + + for handles in old_vector_data.colinear_manipulators { + if new_vector_data.segment_domain.ids().contains(&handles[0].segment) && new_vector_data.segment_domain.ids().contains(&handles[1].segment) { + new_vector_data.colinear_manipulators.push(handles); + } + } + + buffer.push(new_vector_data); + } + + if clipboard == Clipboard::Device { + let mut copy_text = String::from("graphite/vector: "); + copy_text += &serde_json::to_string(&buffer).expect("Could not serialize paste"); + + responses.add(FrontendMessage::TriggerTextCopy { copy_text }); + } else { + //TODO: Add implementation for internal clipboard + } + + PathToolFsmState::Ready + } (_, PathToolMessage::FlipSmoothSharp) => { // Double-clicked on a point let nearest_point = shape_editor.find_nearest_point_indices(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD); diff --git a/frontend/src/io-managers/input.ts b/frontend/src/io-managers/input.ts index bb7b035ceb..c77137a04d 100644 --- a/frontend/src/io-managers/input.ts +++ b/frontend/src/io-managers/input.ts @@ -307,6 +307,8 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli editor.handle.pasteSerializedData(text.substring(16, text.length)); } else if (text.startsWith("graphite/nodes: ")) { editor.handle.pasteSerializedNodes(text.substring(16, text.length)); + } else if (text.startsWith("graphite/vector: ")) { + editor.handle.pasteSerializedVector(text.substring(17, text.length)); } }); } diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 80bfd7fce7..9deb838018 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -504,6 +504,13 @@ impl EditorHandle { self.dispatch(message); } + /// Paste vector data into a new layer from a serialized json representation + #[wasm_bindgen(js_name = pasteSerializedVector)] + pub fn paste_serialized_vector(&self, data: String) { + let message = PortfolioMessage::PasteSerializedVector { data }; + self.dispatch(message); + } + #[wasm_bindgen(js_name = clipLayer)] pub fn clip_layer(&self, id: u64) { let id = NodeId(id); diff --git a/node-graph/gcore/src/vector/vector_data/attributes.rs b/node-graph/gcore/src/vector/vector_data/attributes.rs index bb2ca1f91a..87abd1b083 100644 --- a/node-graph/gcore/src/vector/vector_data/attributes.rs +++ b/node-graph/gcore/src/vector/vector_data/attributes.rs @@ -304,7 +304,7 @@ impl SegmentDomain { &self.stroke } - pub(crate) fn push(&mut self, id: SegmentId, start: usize, end: usize, handles: BezierHandles, stroke: StrokeId) { + pub fn push(&mut self, id: SegmentId, start: usize, end: usize, handles: BezierHandles, stroke: StrokeId) { debug_assert!(!self.id.contains(&id), "Tried to push an existing point to a point domain"); self.id.push(id); diff --git a/node-graph/gcore/src/vector/vector_data/modification.rs b/node-graph/gcore/src/vector/vector_data/modification.rs index 0f06c643a2..d49d8f1ba9 100644 --- a/node-graph/gcore/src/vector/vector_data/modification.rs +++ b/node-graph/gcore/src/vector/vector_data/modification.rs @@ -324,6 +324,8 @@ pub enum VectorModificationType { ApplyPointDelta { point: PointId, delta: DVec2 }, ApplyPrimaryDelta { segment: SegmentId, delta: DVec2 }, ApplyEndDelta { segment: SegmentId, delta: DVec2 }, + + SetNewVectorData { new_vector_data: VectorData }, } impl VectorModification { @@ -369,6 +371,28 @@ impl VectorModification { self.add_g1_continuous.remove(&[handles[1], handles[0]]); } } + VectorModificationType::SetNewVectorData { new_vector_data } => { + new_vector_data.point_domain.iter().for_each(|(id, position)| self.points.push(id, position)); + new_vector_data + .segment_bezier_iter() + .zip(new_vector_data.segment_domain.stroke().iter()) + .for_each(|((id, bezier, start, end), stroke)| { + 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)], + }; + self.segments.push(id, [start, end], handles, *stroke) + }); + + new_vector_data.colinear_manipulators.iter().for_each(|handles| { + self.add_g1_continuous.insert(*handles); + self.remove_g1_continuous.remove(handles); + }); + + self.add_g1_continuous = new_vector_data.colinear_manipulators.iter().copied().collect(); + self.remove_g1_continuous = HashSet::new(); + } VectorModificationType::SetHandles { segment, handles } => { self.segments.handle_primary.insert(*segment, handles[0]); self.segments.handle_end.insert(*segment, handles[1]); From 56e0aa1b1a977aba250cae16e9a31b39f2db201c Mon Sep 17 00:00:00 2001 From: Adesh Gupta Date: Wed, 2 Jul 2025 03:41:36 +0530 Subject: [PATCH 2/5] Fix merge --- editor/src/messages/tool/tool_messages/path_tool.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index fba54313ae..13e72baeda 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -2101,7 +2101,7 @@ impl Fsm for PathToolFsmState { // Add all the selected points for (point, position) in old_vector_data.point_domain.iter() { - if layer_selection_state.is_selected(ManipulatorPointId::Anchor(point)) { + if layer_selection_state.is_point_selected(ManipulatorPointId::Anchor(point)) { new_vector_data.point_domain.push(point, position); } } @@ -2118,7 +2118,7 @@ impl Fsm for PathToolFsmState { // Add segments which have selected ends for ((segment_id, bezier, start, end), stroke) in old_vector_data.segment_bezier_iter().zip(old_vector_data.segment_domain.stroke().iter()) { - if layer_selection_state.is_selected(ManipulatorPointId::Anchor(start)) && layer_selection_state.is_selected(ManipulatorPointId::Anchor(end)) { + if layer_selection_state.is_point_selected(ManipulatorPointId::Anchor(start)) && layer_selection_state.is_point_selected(ManipulatorPointId::Anchor(end)) { let start_index = find_index(start); let end_index = find_index(end); new_vector_data.segment_domain.push(segment_id, start_index, end_index, bezier.handles, *stroke); From 839e77c65146004c01ac4c92ef029d16a8260531 Mon Sep 17 00:00:00 2001 From: Adesh Gupta Date: Sat, 5 Jul 2025 09:22:29 +0530 Subject: [PATCH 3/5] Implement Copy, Cut and Duplicate --- .../messages/input_mapper/input_mappings.rs | 1 + .../portfolio/portfolio_message_handler.rs | 72 +++++- .../messages/tool/tool_messages/path_tool.rs | 214 +++++++++++++++++- .../src/vector/vector_data/modification.rs | 24 -- 4 files changed, 271 insertions(+), 40 deletions(-) diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index 2b63ba693b..8d60d887bb 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -213,6 +213,7 @@ pub fn input_mappings() -> Mapping { 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(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, molding_in_segment_edit: KeyA }), entry!(KeyDown(MouseRight); action_dispatch=PathToolMessage::RightClick), diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index afa877f92c..7f0c5a626c 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -20,12 +20,13 @@ use crate::messages::prelude::*; use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::utility_types::{HintData, HintGroup, ToolType}; use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor}; +use bezier_rs::BezierHandles; use glam::{DAffine2, DVec2}; use graph_craft::document::NodeId; use graphene_std::Color; use graphene_std::renderer::Quad; use graphene_std::text::Font; -use graphene_std::vector::{VectorData, VectorModificationType}; +use graphene_std::vector::{HandleId, PointId, SegmentId, VectorData, VectorModificationType}; use std::vec; pub struct PortfolioMessageData<'a> { @@ -494,15 +495,30 @@ impl MessageHandler> for PortfolioMes } // Custom paste implementation for path tool PortfolioMessage::PasteSerializedVector { data } => { - if let Some(document) = self.active_document() { - if let Ok(data) = serde_json::from_str::>(&data) { - for new_vector in data { + // If using path tool then send the operation to path tool + if *current_tool == ToolType::Path { + responses.add(PathToolMessage::Paste { data }); + } + // If not using path tool, create new layers and add paths into those + else if let Some(document) = self.active_document() { + if let Ok(data) = serde_json::from_str::>(&data) { + let mut layers = Vec::new(); + for (_layer, new_vector, transform) in data { let node_type = resolve_document_node_type("Path").expect("Path node does not exist"); 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_color = Color::WHITE; @@ -514,14 +530,52 @@ impl MessageHandler> for PortfolioMes let stroke = graphene_std::vector::style::Stroke::new(Some(stroke_color.to_gamma_srgb()), DEFAULT_STROKE_WIDTH); responses.add(GraphOperationMessage::StrokeSet { layer, stroke }); - // Set the new vector data to this layer - let modification_type = VectorModificationType::SetNewVectorData { new_vector_data: new_vector }; - responses.add(GraphOperationMessage::Vector { layer, modification_type }); + // Create new point ids and add those into the existing vector data + 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 data + 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::StartBuffer); + responses.add(PortfolioMessage::CenterPastedLayers { layers }); } } - - // Make a new default layer for each of those vector datas and insert those layers in the node graph } PortfolioMessage::CenterPastedLayers { layers } => { if let Some(document) = self.active_document_mut() { diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 13e72baeda..e30ee7c5ae 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -1,9 +1,11 @@ use super::select_tool::extend_lasso; use super::tool_prelude::*; use crate::consts::{ - COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, DRAG_THRESHOLD, HANDLE_ROTATE_SNAP_ANGLE, SEGMENT_INSERTION_DISTANCE, - SEGMENT_OVERLAY_SIZE, SELECTION_THRESHOLD, SELECTION_TOLERANCE, + COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, DEFAULT_STROKE_WIDTH, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, DRAG_THRESHOLD, HANDLE_ROTATE_SNAP_ANGLE, + SEGMENT_INSERTION_DISTANCE, SEGMENT_OVERLAY_SIZE, SELECTION_THRESHOLD, SELECTION_TOLERANCE, }; +use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; +use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_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; @@ -12,13 +14,16 @@ use crate::messages::portfolio::document::utility_types::network_interface::Node use crate::messages::portfolio::document::utility_types::transformation::Axis; use crate::messages::preferences::SelectionMode; use crate::messages::tool::common_functionality::auto_panning::AutoPanning; +use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::shape_editor::{ - ClosestSegment, ManipulatorAngle, OpposingHandleLengths, SelectedPointsInfo, SelectionChange, SelectionShape, SelectionShapeType, ShapeState, + ClosestSegment, ManipulatorAngle, OpposingHandleLengths, SelectedLayerState, SelectedPointsInfo, SelectionChange, SelectionShape, SelectionShapeType, ShapeState, }; use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager}; use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, find_two_param_best_approximate}; -use bezier_rs::{Bezier, TValue}; +use bezier_rs::{Bezier, BezierHandles, TValue}; +use graphene_std::Color; use graphene_std::renderer::Quad; +use graphene_std::uuid::NodeId; use graphene_std::vector::{HandleExt, HandleId, NoHashBuilder, SegmentId, VectorData}; use graphene_std::vector::{ManipulatorPointId, PointId, VectorModificationType}; use std::vec; @@ -115,6 +120,11 @@ pub enum PathToolMessage { Cut { clipboard: Clipboard, }, + DeleteSelected, + Paste { + data: String, + }, + Duplicate, } #[derive(PartialEq, Eq, Hash, Copy, Clone, Debug, Default, serde::Serialize, serde::Deserialize, specta::Type)] @@ -338,7 +348,10 @@ impl<'a> MessageHandler> for PathToo ClosePath, PointerMove, Copy, - Cut + Cut, + DeleteSelected, + Paste, + Duplicate ), PathToolFsmState::Dragging(_) => actions!(PathToolMessageDiscriminant; Escape, @@ -352,6 +365,9 @@ impl<'a> MessageHandler> for PathToo SwapSelectedHandles, Copy, Cut, + DeleteSelected, + Paste, + Duplicate ), PathToolFsmState::Drawing { .. } => actions!(PathToolMessageDiscriminant; FlipSmoothSharp, @@ -2097,6 +2113,9 @@ impl Fsm for PathToolFsmState { continue; }; + // Also get the transform node that is applied on the layer if it exists + let transform = document.metadata().transform_to_document(layer); + let mut new_vector_data = VectorData::default(); // Add all the selected points @@ -2118,7 +2137,9 @@ impl Fsm for PathToolFsmState { // Add segments which have selected ends for ((segment_id, bezier, start, end), stroke) in old_vector_data.segment_bezier_iter().zip(old_vector_data.segment_domain.stroke().iter()) { - if layer_selection_state.is_point_selected(ManipulatorPointId::Anchor(start)) && layer_selection_state.is_point_selected(ManipulatorPointId::Anchor(end)) { + let both_ends_selected = layer_selection_state.is_point_selected(ManipulatorPointId::Anchor(start)) && layer_selection_state.is_point_selected(ManipulatorPointId::Anchor(end)); + let segment_selected = layer_selection_state.is_segment_selected(segment_id); + if both_ends_selected || segment_selected { let start_index = find_index(start); let end_index = find_index(end); new_vector_data.segment_domain.push(segment_id, start_index, end_index, bezier.handles, *stroke); @@ -2131,7 +2152,7 @@ impl Fsm for PathToolFsmState { } } - buffer.push(new_vector_data); + buffer.push((layer, new_vector_data, transform)); } if clipboard == Clipboard::Device { @@ -2145,6 +2166,185 @@ impl Fsm for PathToolFsmState { PathToolFsmState::Ready } + (_, PathToolMessage::Cut { clipboard }) => { + responses.add(PathToolMessage::Copy { clipboard }); + // Delete the selected points/ segments + responses.add(PathToolMessage::DeleteSelected); + + PathToolFsmState::Ready + } + (_, PathToolMessage::DeleteSelected) => { + //Delete the selected points and segments + shape_editor.delete_point_and_break_path(document, responses); + shape_editor.delete_selected_segments(document, responses); + + PathToolFsmState::Ready + } + (_, PathToolMessage::Duplicate) => { + responses.add(DocumentMessage::AddTransaction); + + // Copy the existing selected geometry and paste it in the existing layers + for (layer, layer_selection_state) in shape_editor.selected_shape_state.clone() { + if layer_selection_state.is_empty() { + continue; + } + + let Some(old_vector_data) = document.network_interface.compute_modified_vector(layer) else { + continue; + }; + + // Add all the selected points + let mut points_map = HashMap::new(); + for (point, position) in old_vector_data.point_domain.iter() { + if layer_selection_state.is_point_selected(ManipulatorPointId::Anchor(point)) { + // insert the same point with new id + let new_id = PointId::generate(); + points_map.insert(point, new_id); + let modification_type = VectorModificationType::InsertPoint { id: new_id, position }; + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + } + } + + let mut segments_map = HashMap::new(); + for (segment_id, bezier, start, end) in old_vector_data.segment_bezier_iter() { + let both_ends_selected = layer_selection_state.is_point_selected(ManipulatorPointId::Anchor(start)) && layer_selection_state.is_point_selected(ManipulatorPointId::Anchor(end)); + let segment_selected = layer_selection_state.is_segment_selected(segment_id); + if both_ends_selected || segment_selected { + let new_id = SegmentId::generate(); + segments_map.insert(segment_id, new_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_id, points, handles }; + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + } + } + + for handles in old_vector_data.colinear_manipulators { + let to_new_handle = |handle: HandleId| -> HandleId { + HandleId { + ty: handle.ty, + segment: segments_map[&handle.segment], + } + }; + + if segments_map.contains_key(&handles[0].segment) && segments_map.contains_key(&handles[1].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.deselect_all_points(); + shape_editor.deselect_all_segments(); + + // Set selection to newly inserted points + let state = shape_editor.selected_shape_state.get_mut(&layer).expect("No state for layer"); + points_map.values().for_each(|point| state.select_point(ManipulatorPointId::Anchor(*point))); + } + + PathToolFsmState::Ready + } + (_, PathToolMessage::Paste { data }) => { + log::info!("yes coming here"); + + // 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 node_type = resolve_document_node_type("Path").expect("Path node does not exist"); + 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); + + let fill_color = Color::WHITE; + let stroke_color = Color::BLACK; + + let fill = graphene_std::vector::style::Fill::solid(fill_color.to_gamma_srgb()); + responses.add(GraphOperationMessage::FillSet { layer, fill }); + + let stroke = graphene_std::vector::style::Stroke::new(Some(stroke_color.to_gamma_srgb()), DEFAULT_STROKE_WIDTH); + responses.add(GraphOperationMessage::StrokeSet { layer, stroke }); + 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 data + 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 data + 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 }); + } + + if !shape_editor.selected_shape_state.contains_key(&layer) { + shape_editor.selected_shape_state.insert(layer, SelectedLayerState::default()); + } + + // Set selection to newly inserted points + let state = shape_editor.selected_shape_state.get_mut(&layer).expect("No state for layer"); + points_map.values().for_each(|point| state.select_point(ManipulatorPointId::Anchor(*point))); + } + + if !new_layers.is_empty() { + responses.add(Message::StartBuffer); + responses.add(PortfolioMessage::CenterPastedLayers { layers: new_layers }); + } + } + + PathToolFsmState::Ready + } (_, PathToolMessage::FlipSmoothSharp) => { // Double-clicked on a point let nearest_point = shape_editor.find_nearest_point_indices(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD); diff --git a/node-graph/gcore/src/vector/vector_data/modification.rs b/node-graph/gcore/src/vector/vector_data/modification.rs index d49d8f1ba9..0f06c643a2 100644 --- a/node-graph/gcore/src/vector/vector_data/modification.rs +++ b/node-graph/gcore/src/vector/vector_data/modification.rs @@ -324,8 +324,6 @@ pub enum VectorModificationType { ApplyPointDelta { point: PointId, delta: DVec2 }, ApplyPrimaryDelta { segment: SegmentId, delta: DVec2 }, ApplyEndDelta { segment: SegmentId, delta: DVec2 }, - - SetNewVectorData { new_vector_data: VectorData }, } impl VectorModification { @@ -371,28 +369,6 @@ impl VectorModification { self.add_g1_continuous.remove(&[handles[1], handles[0]]); } } - VectorModificationType::SetNewVectorData { new_vector_data } => { - new_vector_data.point_domain.iter().for_each(|(id, position)| self.points.push(id, position)); - new_vector_data - .segment_bezier_iter() - .zip(new_vector_data.segment_domain.stroke().iter()) - .for_each(|((id, bezier, start, end), stroke)| { - 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)], - }; - self.segments.push(id, [start, end], handles, *stroke) - }); - - new_vector_data.colinear_manipulators.iter().for_each(|handles| { - self.add_g1_continuous.insert(*handles); - self.remove_g1_continuous.remove(handles); - }); - - self.add_g1_continuous = new_vector_data.colinear_manipulators.iter().copied().collect(); - self.remove_g1_continuous = HashSet::new(); - } VectorModificationType::SetHandles { segment, handles } => { self.segments.handle_primary.insert(*segment, handles[0]); self.segments.handle_end.insert(*segment, handles[1]); From 9b6e0c6423d4df7b4eefc394173a0e40832909da Mon Sep 17 00:00:00 2001 From: Adesh Gupta Date: Sat, 5 Jul 2025 13:14:44 +0530 Subject: [PATCH 4/5] Fix selection of segments --- .../tool/common_functionality/shape_editor.rs | 2 +- .../messages/tool/tool_messages/path_tool.rs | 42 +++++++++++++++---- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index 82861763c6..b333481e8f 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -57,7 +57,7 @@ pub struct SelectedLayerState { impl SelectedLayerState { pub fn is_empty(&self) -> bool { - self.selected_points.is_empty() + self.selected_points.is_empty() && self.selected_segments.is_empty() } pub fn selected_points(&self) -> impl Iterator + '_ { self.selected_points.iter().copied() diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index e30ee7c5ae..a3d8d8a301 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -2118,9 +2118,18 @@ impl Fsm for PathToolFsmState { let mut new_vector_data = VectorData::default(); + let mut selected_points_by_segment = HashSet::new(); + old_vector_data + .segment_bezier_iter() + .filter(|(segment, _, _, _)| layer_selection_state.is_segment_selected(*segment)) + .for_each(|(_, _, start, end)| { + selected_points_by_segment.insert(start); + selected_points_by_segment.insert(end); + }); + // Add all the selected points for (point, position) in old_vector_data.point_domain.iter() { - if layer_selection_state.is_point_selected(ManipulatorPointId::Anchor(point)) { + if layer_selection_state.is_point_selected(ManipulatorPointId::Anchor(point)) || selected_points_by_segment.contains(&point) { new_vector_data.point_domain.push(point, position); } } @@ -2194,9 +2203,19 @@ impl Fsm for PathToolFsmState { }; // Add all the selected points + let mut selected_points_by_segment = HashSet::new(); + old_vector_data + .segment_bezier_iter() + .filter(|(segment, _, _, _)| layer_selection_state.is_segment_selected(*segment)) + .for_each(|(_, _, start, end)| { + selected_points_by_segment.insert(start); + selected_points_by_segment.insert(end); + }); let mut points_map = HashMap::new(); for (point, position) in old_vector_data.point_domain.iter() { - if layer_selection_state.is_point_selected(ManipulatorPointId::Anchor(point)) { + //TODO: Either the point is selected or it is an endpoint of a selected segment + + if layer_selection_state.is_point_selected(ManipulatorPointId::Anchor(point)) || selected_points_by_segment.contains(&point) { // insert the same point with new id let new_id = PointId::generate(); points_map.insert(point, new_id); @@ -2243,16 +2262,19 @@ impl Fsm for PathToolFsmState { shape_editor.deselect_all_points(); shape_editor.deselect_all_segments(); - // Set selection to newly inserted points + // Set selection to newly inserted points and segments let state = shape_editor.selected_shape_state.get_mut(&layer).expect("No state for layer"); - points_map.values().for_each(|point| state.select_point(ManipulatorPointId::Anchor(*point))); + if tool_options.path_editing_mode.point_editing_mode { + points_map.values().for_each(|point| state.select_point(ManipulatorPointId::Anchor(*point))); + } + if tool_options.path_editing_mode.segment_editing_mode { + segments_map.values().for_each(|segment| state.select_segment(*segment)); + } } PathToolFsmState::Ready } (_, PathToolMessage::Paste { data }) => { - log::info!("yes coming here"); - // Deserialize the data if let Ok(data) = serde_json::from_str::>(&data) { shape_editor.deselect_all_points(); @@ -2334,7 +2356,13 @@ impl Fsm for PathToolFsmState { // Set selection to newly inserted points let state = shape_editor.selected_shape_state.get_mut(&layer).expect("No state for layer"); - points_map.values().for_each(|point| state.select_point(ManipulatorPointId::Anchor(*point))); + // points_map.values().for_each(|point| state.select_point(ManipulatorPointId::Anchor(*point))); + if tool_options.path_editing_mode.point_editing_mode { + points_map.values().for_each(|point| state.select_point(ManipulatorPointId::Anchor(*point))); + } + if tool_options.path_editing_mode.segment_editing_mode { + segments_map.values().for_each(|segment| state.select_segment(*segment)); + } } if !new_layers.is_empty() { From 9bcac94a985c44fb58ed4d8fd5ef8a656fe80a99 Mon Sep 17 00:00:00 2001 From: Adesh Gupta Date: Tue, 15 Jul 2025 11:21:04 +0530 Subject: [PATCH 5/5] Fix formatting --- editor/src/messages/portfolio/portfolio_message_handler.rs | 2 +- editor/src/messages/tool/tool_messages/path_tool.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 33945de5e9..20043e23d3 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -11,8 +11,8 @@ use crate::messages::frontend::utility_types::FrontendDocumentDetails; 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::resolve_document_node_type; use crate::messages::portfolio::document::node_graph::document_node_definitions; +use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; use crate::messages::portfolio::document::utility_types::clipboards::{Clipboard, CopyBufferEntry, INTERNAL_CLIPBOARD_COUNT}; use crate::messages::portfolio::document::utility_types::network_interface::OutputConnector; use crate::messages::portfolio::document::utility_types::nodes::SelectedNodes; diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 4dea2c6541..b851fcead6 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -1,8 +1,8 @@ use super::select_tool::extend_lasso; use super::tool_prelude::*; use crate::consts::{ - COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GRAY, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, DEFAULT_STROKE_WIDTH, 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, + COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GRAY, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, DEFAULT_STROKE_WIDTH, 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::portfolio::document::graph_operation::utility_types::TransformIn; use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; @@ -24,8 +24,8 @@ use crate::messages::tool::common_functionality::utility_functions::{calculate_s use bezier_rs::{Bezier, BezierHandles, TValue}; use graphene_std::Color; use graphene_std::renderer::Quad; -use graphene_std::uuid::NodeId; use graphene_std::transform::ReferencePoint; +use graphene_std::uuid::NodeId; use graphene_std::vector::click_target::ClickTargetType; use graphene_std::vector::{HandleExt, HandleId, NoHashBuilder, SegmentId, VectorData}; use graphene_std::vector::{ManipulatorPointId, PointId, VectorModificationType};