Skip to content

Copy and Paste for paths #2812

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions editor/src/messages/input_mapper/input_mappings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, molding_in_segment_edit: KeyA }),
entry!(KeyDown(MouseRight); action_dispatch=PathToolMessage::RightClick),
Expand Down
3 changes: 3 additions & 0 deletions editor/src/messages/portfolio/portfolio_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ pub enum PortfolioMessage {
PasteSerializedData {
data: String,
},
PasteSerializedVector {
data: String,
},
CenterPastedLayers {
layers: Vec<LayerNodeIdentifier>,
},
Expand Down
37 changes: 36 additions & 1 deletion editor/src/messages/portfolio/portfolio_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,29 @@ 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;
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> {
Expand Down Expand Up @@ -488,6 +492,37 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> 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::<Vec<VectorData>>(&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()]);
Expand Down
3 changes: 3 additions & 0 deletions editor/src/messages/tool/common_functionality/shape_editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ pub struct SelectedLayerState {
}

impl SelectedLayerState {
pub fn is_empty(&self) -> bool {
self.selected_points.is_empty()
}
pub fn selected_points(&self) -> impl Iterator<Item = ManipulatorPointId> + '_ {
self.selected_points.iter().copied()
}
Expand Down
73 changes: 73 additions & 0 deletions editor/src/messages/tool/tool_messages/path_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -108,6 +109,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)]
Expand Down Expand Up @@ -330,6 +337,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
DeleteAndBreakPath,
ClosePath,
PointerMove,
Copy,
Cut
),
PathToolFsmState::Dragging(_) => actions!(PathToolMessageDiscriminant;
Escape,
Expand All @@ -341,6 +350,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
BreakPath,
DeleteAndBreakPath,
SwapSelectedHandles,
Copy,
Cut,
),
PathToolFsmState::Drawing { .. } => actions!(PathToolMessageDiscriminant;
FlipSmoothSharp,
Expand Down Expand Up @@ -2072,6 +2083,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_point_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_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);
}
}

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);
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/io-managers/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
});
}
Expand Down
7 changes: 7 additions & 0 deletions frontend/wasm/src/editor_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion node-graph/gcore/src/vector/vector_data/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
24 changes: 24 additions & 0 deletions node-graph/gcore/src/vector/vector_data/modification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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]);
Expand Down
Loading