Skip to content
Closed
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
11 changes: 11 additions & 0 deletions editor/src/messages/portfolio/document/utility_types/clipboards.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::network_interface::NodeTemplate;
use graph_craft::application_io::resource::{DataSource, ResourceHash, ResourceId};
use graph_craft::document::NodeId;

#[repr(u8)]
Expand All @@ -20,4 +21,14 @@ pub struct CopyBufferEntry {
pub visible: bool,
pub locked: bool,
pub collapsed: bool,
#[serde(default)]
pub resources: Vec<ClipboardResource>,
}

/// A snapshot of a document's resource registry entry, carried in the clipboard so a paste can re-register it.
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ClipboardResource {
pub id: ResourceId,
pub sources: Vec<DataSource>,
pub hash: Option<ResourceHash>,
}
90 changes: 83 additions & 7 deletions editor/src/messages/portfolio/portfolio_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ 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::utility_types::clipboards::{Clipboard, ClipboardResource, CopyBufferEntry, INTERNAL_CLIPBOARD_COUNT};
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::*;
Expand All @@ -26,8 +26,9 @@ use crate::messages::tool::utility_types::{HintData, ToolType};
use crate::messages::viewport::ToPhysical;
use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor};
use glam::{DAffine2, DVec2};
use graph_craft::application_io::resource::{DataSource, ResourceHash};
use graph_craft::document::NodeId;
use graph_craft::application_io::resource::{DataSource, ResourceHash, ResourceId};
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput};
use graphene_std::Color;
use graphene_std::raster_types::Image;
use graphene_std::renderer::Quad;
Expand Down Expand Up @@ -319,12 +320,32 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
copy_ids.insert(node_id, NodeId((index + 1) as u64));
});

let nodes: Vec<_> = active_document.network_interface.copy_nodes(&copy_ids, &[]).collect();

// Snapshot the registry entries for resources these nodes reference so a cross-document paste can re-register them
let mut resource_ids = HashSet::new();
for (_, template) in &nodes {
collect_document_node_resources(&template.document_node, &mut resource_ids);
}
let resources = resource_ids
.into_iter()
.filter_map(|id| {
let info = active_document.resources.registry.info(&id)?;
Some(ClipboardResource {
id,
sources: info.sources.to_vec(),
hash: info.hash.copied(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Device clipboard resource snapshots omit embedded resource bytes, so embedded-only images/fonts can paste as registry entries that cannot render in another session. Include embedded payloads or avoid marking them resolved unless bytes are present in target storage.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/portfolio/portfolio_message_handler.rs, line 337:

<comment>Device clipboard resource snapshots omit embedded resource bytes, so embedded-only images/fonts can paste as registry entries that cannot render in another session. Include embedded payloads or avoid marking them resolved unless bytes are present in target storage.</comment>

<file context>
@@ -319,12 +320,32 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
+								Some(ClipboardResource {
+									id,
+									sources: info.sources.to_vec(),
+									hash: info.hash.copied(),
+								})
+							})
</file context>

})
})
.collect();

buffer.push(CopyBufferEntry {
nodes: active_document.network_interface.copy_nodes(&copy_ids, &[]).collect(),
nodes,
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,
resources,
});
}
};
Expand Down Expand Up @@ -1039,6 +1060,12 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
// TODO: Unused except by tests, remove?
PortfolioMessage::PasteIntoFolder { clipboard, parent, insert_index } => {
let mut all_new_ids = Vec::new();

let clipboard_resources: Vec<ClipboardResource> = self.copy_buffer[clipboard as usize].iter().flat_map(|entry| entry.resources.iter().cloned()).collect();
if let Some(document) = self.active_document_mut() {
register_clipboard_resources(document, &clipboard_resources);
}
Comment on lines +1064 to +1067

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Instead of collecting and cloning all clipboard resources into a temporary Vec, we can check if any resources exist using .any() and pass a lazy iterator to register_clipboard_resources. This avoids unnecessary allocations and clones, matching the pattern used in PasteSerializedData.

Suggested change
let clipboard_resources: Vec<ClipboardResource> = self.copy_buffer[clipboard as usize].iter().flat_map(|entry| entry.resources.iter().cloned()).collect();
if let Some(document) = self.active_document_mut() {
register_clipboard_resources(document, &clipboard_resources);
}
let has_resources = self.copy_buffer[clipboard as usize].iter().any(|entry| !entry.resources.is_empty());
if has_resources && let Some(document) = self.active_document_mut() {
register_clipboard_resources(document, self.copy_buffer[clipboard as usize].iter().flat_map(|entry| &entry.resources));
}


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}");
Expand All @@ -1058,11 +1085,23 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
}
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: all_new_ids });

if !clipboard_resources.is_empty()
&& let Some(document_id) = self.active_document_id
{
responses.add(PortfolioMessage::ResolveDocumentResources { document_id });
}
Comment on lines +1089 to +1093

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Use the has_resources boolean flag defined at the top of the match arm instead of checking !clipboard_resources.is_empty() on the collected vector.

				if has_resources && let Some(document_id) = self.active_document_id {
					responses.add(PortfolioMessage::ResolveDocumentResources { document_id });
				}

}
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) {
if let Ok(data) = serde_json::from_str::<Vec<CopyBufferEntry>>(&data) {
let has_resources = data.iter().any(|entry| !entry.resources.is_empty());

if has_resources && let Some(document) = self.active_document_mut() {
register_clipboard_resources(document, data.iter().flat_map(|entry| &entry.resources));
}

if let Some(document) = self.active_document() {
let mut all_new_ids = Vec::new();
let parent = document.new_layer_parent(false);
let mut layers = Vec::new();

Expand Down Expand Up @@ -1090,6 +1129,10 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
messages: vec![PortfolioMessage::CenterPastedLayers { layers }.into()],
});
}

if has_resources && let Some(document_id) = self.active_document_id {
responses.add(PortfolioMessage::ResolveDocumentResources { document_id });
}
}
}
// Custom paste implementation for Path tool
Expand Down Expand Up @@ -2216,3 +2259,36 @@ fn build_recovery_zip(entries: &[(String, Vec<u8>)]) -> Result<Vec<u8>, String>
writer.finish().map_err(|e| format!("finish: {e}"))?;
Ok(buffer.into_inner())
}

/// Recursively gathers the IDs of resources referenced by `TaggedValue::Resource` inputs within a node and its nested networks.
fn collect_document_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);
}
}
Comment on lines +2265 to +2271

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Extracted or cloned nodes can be wrapped inside a TaggedValue::DocumentNode variant. Recursively traversing TaggedValue::DocumentNode ensures that resources nested inside these nodes are also collected and registered correctly during copy-paste operations.

	for input in &node.inputs {
		if let NodeInput::Value { tagged_value, .. } = input {
			if let TaggedValue::Resource(id) = &**tagged_value {
				out.insert(*id);
			} else if let TaggedValue::DocumentNode(inner_node) = &**tagged_value {
				collect_document_node_resources(inner_node, out);
			}
		}
	}


if let DocumentNodeImplementation::Network(nested) = &node.implementation {
for nested_node in nested.nodes.values() {
collect_document_node_resources(nested_node, out);
}
}
}

/// Re-registers clipboard resource snapshots into a document's registry, skipping IDs it already knows.
fn register_clipboard_resources<'a>(document: &mut DocumentMessageHandler, resources: impl IntoIterator<Item = &'a ClipboardResource>) {
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.clone());
}
if let Some(hash) = resource.hash {
document.resources.registry.resolve(&resource.id, hash);
}
}
}
Loading