mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-19 10:58:04 +08:00
Rework clipboard handling to carry embedded resources across documents (#4296)
* Move clipboard code to its dedicated handler * Adapt clipboard format to work with resources * Restore visibility, lock, and collapse state when pasting layers --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
@@ -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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
|
||||
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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
|
||||
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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
|
||||
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() {
|
||||
|
||||
@@ -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<String>, cut: bool },
|
||||
Write { content: ClipboardContent },
|
||||
|
||||
CopyLayers,
|
||||
CutLayers,
|
||||
WriteItems { items: Vec<ClipboardItem> },
|
||||
PasteItems { data: String },
|
||||
PasteLayers { entries: Vec<ClipboardLayer> },
|
||||
PasteVectors { paths: Vec<ClipboardVectorEntry> },
|
||||
}
|
||||
|
||||
@@ -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<ClipboardMessage, ()> for ClipboardMessageHandler {
|
||||
fn process_message(&mut self, message: ClipboardMessage, responses: &mut std::collections::VecDeque<Message>, _: ()) {
|
||||
impl MessageHandler<ClipboardMessage, ClipboardMessageContext<'_>> for ClipboardMessageHandler {
|
||||
fn process_message(&mut self, message: ClipboardMessage, responses: &mut std::collections::VecDeque<Message>, 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<ClipboardMessage, ()> 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<ClipboardMessage, ()> 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<ClipboardMessage, ()> 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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
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::<Vec<ClipboardItem>>(&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::<Vec<_>>();
|
||||
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<HandleId> {
|
||||
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<ClipboardMessage, ()> for ClipboardMessageHandler {
|
||||
Paste,
|
||||
);
|
||||
}
|
||||
|
||||
/// Serialize the clipboard items, logging on failure.
|
||||
fn serialize_clipboard(items: &[ClipboardItem]) -> Option<ClipboardContent> {
|
||||
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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
|
||||
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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
|
||||
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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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<u8>, 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<ClipboardVectorEntry>),
|
||||
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<ResourceHash>,
|
||||
pub sources: Vec<DataSource>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub data: Option<ResourceData>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone)]
|
||||
pub struct ResourceData(pub Vec<u8>);
|
||||
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<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
serializer.serialize_str(&BASE64.encode(&self.0))
|
||||
}
|
||||
}
|
||||
impl<'de> serde::Deserialize<'de> for ResourceData {
|
||||
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
let encoded = String::deserialize(deserializer)?;
|
||||
Ok(Self(BASE64.decode(&encoded).map_err(serde::de::Error::custom)?))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -112,7 +112,7 @@ pub enum DocumentMessage {
|
||||
resize: Key,
|
||||
resize_opposite: Key,
|
||||
},
|
||||
PasteImage {
|
||||
InsertImage {
|
||||
name: Option<String>,
|
||||
image: Image<Color>,
|
||||
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<String>,
|
||||
svg: String,
|
||||
mouse: Option<(f64, f64)>,
|
||||
|
||||
@@ -753,7 +753,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> 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<DocumentMessage, DocumentMessageContext<'_>> 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,
|
||||
|
||||
@@ -97,8 +97,8 @@ pub enum NodeGraphMessage {
|
||||
SetChainPosition {
|
||||
node_id: NodeId,
|
||||
},
|
||||
PasteNodes {
|
||||
serialized_nodes: String,
|
||||
InsertNodes {
|
||||
nodes: Vec<(NodeId, NodeTemplate)>,
|
||||
},
|
||||
PointerDown {
|
||||
shift_click: bool,
|
||||
|
||||
@@ -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<NodeGraphMessage, NodeGraphMessageContext<'a>> 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::<Vec<_>>();
|
||||
|
||||
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<NodeGraphMessage, NodeGraphMessageContext<'a>> 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::<Vec<(NodeId, NodeTemplate)>>(&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 {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod clipboards;
|
||||
pub mod document_metadata;
|
||||
pub mod error;
|
||||
pub mod misc;
|
||||
|
||||
@@ -6832,17 +6832,22 @@ pub enum TransactionStatus {
|
||||
|
||||
fn collect_network_resources(network: &NodeNetwork, out: &mut HashSet<ResourceId>) {
|
||||
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<ResourceId>) {
|
||||
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!(
|
||||
|
||||
@@ -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<String>,
|
||||
svg: String,
|
||||
},
|
||||
PasteSerializedData {
|
||||
data: String,
|
||||
},
|
||||
PasteSerializedVector {
|
||||
data: String,
|
||||
},
|
||||
PasteImage {
|
||||
InsertImage {
|
||||
name: Option<String>,
|
||||
image: Image<Color>,
|
||||
mouse: Option<(f64, f64)>,
|
||||
parent_and_insert_index: Option<(LayerNodeIdentifier, usize)>,
|
||||
},
|
||||
PasteSvg {
|
||||
InsertSvg {
|
||||
name: Option<String>,
|
||||
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<LayerNodeIdentifier>,
|
||||
},
|
||||
PrevDocument,
|
||||
|
||||
@@ -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<DocumentId>,
|
||||
persistent_state: PersistentStateMessageHandler,
|
||||
pub fonts: FontsMessageHandler,
|
||||
copy_buffer: [Vec<CopyBufferEntry>; 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<PortfolioMessage, PortfolioMessageContext<'_>> 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<CopyBufferEntry>| {
|
||||
let mut ordered_last_elements = active_document.network_interface.shallowest_unique_layers(&[]).collect::<Vec<_>>();
|
||||
|
||||
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<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
});
|
||||
}
|
||||
FileContent::Svg(svg) => {
|
||||
responses.add(PortfolioMessage::PasteSvg {
|
||||
responses.add(PortfolioMessage::InsertSvg {
|
||||
name,
|
||||
svg,
|
||||
mouse: None,
|
||||
@@ -767,7 +678,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
});
|
||||
}
|
||||
FileContent::Image(image) => {
|
||||
responses.add(PortfolioMessage::PasteImage {
|
||||
responses.add(PortfolioMessage::InsertImage {
|
||||
name,
|
||||
image,
|
||||
mouse: None,
|
||||
@@ -959,7 +870,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> 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<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
})
|
||||
});
|
||||
|
||||
responses.add(DocumentMessage::PasteSvg {
|
||||
responses.add(DocumentMessage::InsertSvg {
|
||||
name,
|
||||
svg,
|
||||
mouse: None,
|
||||
@@ -1036,153 +947,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> 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<NodeId>| {
|
||||
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::<Vec<CopyBufferEntry>>(&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::<Vec<(LayerNodeIdentifier, Vector, DAffine2)>>(&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<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
}
|
||||
PortfolioMessage::PasteImage {
|
||||
PortfolioMessage::InsertImage {
|
||||
name,
|
||||
image,
|
||||
mouse,
|
||||
@@ -1296,7 +1061,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> 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<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
});
|
||||
}
|
||||
}
|
||||
PortfolioMessage::PasteSvg {
|
||||
PortfolioMessage::InsertSvg {
|
||||
name,
|
||||
svg,
|
||||
mouse,
|
||||
@@ -1314,7 +1079,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> 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<PortfolioMessage, PortfolioMessageContext<'_>> 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
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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<ClipboardVectorEntry>,
|
||||
},
|
||||
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::<Vec<(LayerNodeIdentifier, Vector, DAffine2)>>(&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
|
||||
|
||||
@@ -245,7 +245,7 @@ impl EditorTestUtils {
|
||||
}
|
||||
|
||||
pub async fn create_raster_image(&mut self, image: graphene_std::raster::Image<Color>, 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};
|
||||
|
||||
Reference in New Issue
Block a user