Skip to content

Clipboard: Cross-application copy-paste support - #4499

Open
VimYoung wants to merge 17 commits into
GraphiteEditor:masterfrom
VimYoung:clipboard-svg-fix
Open

Clipboard: Cross-application copy-paste support#4499
VimYoung wants to merge 17 commits into
GraphiteEditor:masterfrom
VimYoung:clipboard-svg-fix

Conversation

@VimYoung

@VimYoung VimYoung commented Sep 3, 2026

Copy link
Copy Markdown

Description

This PR aims to resolve #2373. Solution's approach has been thoroughly discussed in discord's development channel. The PR will fix this issue by introducing copy of both text and svg+xml mime types when copying a selection to the clipboard. Essentially making it possible for other apps to take the svg representation while graphite picks up the internal json representation for copy pasting.

Notes

  1. As mentioned by hypercube, image/svg+xml is not supported by firefox and safari Stable so that is handled.
  2. The intersecting by desktop still doesn't copy the svg and simply ignores it.

Following example showcase the same copy paste not working across apps (krita and inkscape shown as the other apps) in current build vs it working in this branch while still being compatible across graphite tabs.

Before(Only across graphite tabs, not across apps):

editor_before.mp4

After(Across tabs and other apps):

editor_after.mp4

@VimYoung
VimYoung marked this pull request as ready for review September 6, 2026 10:12

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

1 issue found across 8 files

Confidence score: 3/5

  • In editor/src/node_graph_executor/runtime.rs, selections containing an artboard currently discard the List<Artboard> monitor output and emit an empty SVG, so rendered previews or exports can lose the artboard’s content, position, dimensions, and clipping; render artboard outputs as SVG while preserving those properties.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="editor/src/node_graph_executor/runtime.rs">

<violation number="1" location="editor/src/node_graph_executor/runtime.rs:351">
P1: When the selection contains an artboard, this branch ignores its `List<Artboard>` monitor output and sends an empty SVG. Render artboard outputs as SVG too, preserving their location, dimensions, and clipping.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

return texture;
}
GraphRuntimeRequest::CopySvgTextClipboard(text_string_clipboard, selected_node_ids) => {
let mut combined_graphics = List::<Graphic>::new();

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: When the selection contains an artboard, this branch ignores its List<Artboard> monitor output and sends an empty SVG. Render artboard outputs as SVG too, preserving their location, dimensions, and clipping.

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

<comment>When the selection contains an artboard, this branch ignores its `List<Artboard>` monitor output and sends an empty SVG. Render artboard outputs as SVG too, preserving their location, dimensions, and clipping.</comment>

<file context>
@@ -340,6 +347,59 @@ impl NodeRuntime {
 					return texture;
 				}
+				GraphRuntimeRequest::CopySvgTextClipboard(text_string_clipboard, selected_node_ids) => {
+					let mut combined_graphics = List::<Graphic>::new();
+
+					for monitor_node_path in &self.monitor_nodes {
</file context>

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I don't think one can copy the artboard as is, even if they copy every content in the artboard, the copying type would still be graphic (of all selected nodes/layers) ig.

Comment thread editor/src/node_graph_executor/runtime.rs Outdated
Comment thread editor/src/messages/clipboard/clipboard_message_handler.rs Outdated
Comment thread frontend/src/managers/clipboard.ts Outdated
Comment thread editor/src/node_graph_executor.rs

@0HyperCube 0HyperCube left a comment

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.

Works well; thanks for your work so far.

Comment thread desktop/wrapper/src/intercept_frontend_message.rs
Comment thread editor/src/messages/clipboard/clipboard_message_handler.rs Outdated
Comment thread editor/src/messages/frontend/frontend_message.rs Outdated
Comment thread editor/src/node_graph_executor.rs Outdated
Comment thread editor/src/messages/frontend/frontend_message.rs Outdated
Comment thread editor/src/node_graph_executor/runtime.rs Outdated
Comment thread editor/src/node_graph_executor/runtime.rs Outdated
Comment thread editor/src/node_graph_executor/runtime.rs Outdated
Comment thread editor/src/node_graph_executor/runtime.rs Outdated
Comment thread frontend/src/managers/clipboard.ts Outdated
@timon-schelling

Copy link
Copy Markdown
Member

I would like to wait with this until winit update is merged. And we might want to make this API lazy.

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

2 issues found across 11 files (changes from recent commits).

Confidence score: 3/5

  • In editor/src/node_graph_executor/runtime.rs, coalescing copy and evaluation can inspect monitor nodes before execution refreshes them, leaving the clipboard with stale or empty SVG; ensure the pending execution refreshes monitor state before clipboard introspection.
  • In editor/src/node_graph_executor/runtime.rs, moving svg_clipboard ahead of execution can let CopySvgTextClipboard return early when combined_graphics is empty, skipping a pending render; preserve the required execution ordering or avoid short-circuiting the render.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="editor/src/node_graph_executor/runtime.rs">

<violation number="1" location="editor/src/node_graph_executor/runtime.rs:203">
P2: When a copy request and a graph evaluation are coalesced in one runtime pass, this line introspects monitor nodes before the pending execution refreshes them, so the clipboard can contain stale or empty SVG. Ensure the copy path reads output from the relevant completed execution, and avoid relying only on request reordering because the execution branch currently returns before later requests.</violation>

<violation number="2" location="editor/src/node_graph_executor/runtime.rs:203">
P2: Moving `svg_clipboard` ahead of `execution` lets a pending `CopySvgTextClipboard` request short-circuit a pending render. The `CopySvgTextClipboard` branch returns `None` early when `combined_graphics.is_empty()` (the case for `copy_svg_clipboard(graphite_json, Vec::new())` in portfolio_message_handler.rs:1697 and for an empty selection). Because the loop processes `svg_clipboard` first, that `return None` now skips the `ExecutionRequest` branch entirely, so the queued viewport render is dropped and its `ExecutionResponse` is never sent. Before this reorder the execution ran first and the copy was skipped instead. Consider processing the SVG copy without the early `return None` (e.g., `continue` after sending the clipboard) so a concurrent execution request still runs.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread editor/src/node_graph_executor.rs Outdated
}

let requests = [preferences, graph, eyedropper, execution].into_iter().flatten();
let requests = [preferences, graph, eyedropper, svg_clipboard, execution].into_iter().flatten();

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.

P2: When a copy request and a graph evaluation are coalesced in one runtime pass, this line introspects monitor nodes before the pending execution refreshes them, so the clipboard can contain stale or empty SVG. Ensure the copy path reads output from the relevant completed execution, and avoid relying only on request reordering because the execution branch currently returns before later requests.

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

<comment>When a copy request and a graph evaluation are coalesced in one runtime pass, this line introspects monitor nodes before the pending execution refreshes them, so the clipboard can contain stale or empty SVG. Ensure the copy path reads output from the relevant completed execution, and avoid relying only on request reordering because the execution branch currently returns before later requests.</comment>

<file context>
@@ -200,7 +200,7 @@ impl NodeRuntime {
 		}
 
-		let requests = [preferences, graph, eyedropper, execution, svg_clipboard].into_iter().flatten();
+		let requests = [preferences, graph, eyedropper, svg_clipboard, execution].into_iter().flatten();
 
 		for request in requests {
</file context>

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.

This is technically true however it probably isn't very relevant since the user would be copying an SVG that is not yet rendered into the viewport.

You could fix this by having a result variable instead of an early return:

let mut result = None;
for request in requests {
	match request {
		// ...
		GraphRuntimeRequest::ExecutionRequest(ExecutionRequest { execution_id, mut render_config, .. }) => {
			// ...
			result = Some(texture);
		}
	}
}
result

}

let requests = [preferences, graph, eyedropper, execution].into_iter().flatten();
let requests = [preferences, graph, eyedropper, svg_clipboard, execution].into_iter().flatten();

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.

P2: Moving svg_clipboard ahead of execution lets a pending CopySvgTextClipboard request short-circuit a pending render. The CopySvgTextClipboard branch returns None early when combined_graphics.is_empty() (the case for copy_svg_clipboard(graphite_json, Vec::new()) in portfolio_message_handler.rs:1697 and for an empty selection). Because the loop processes svg_clipboard first, that return None now skips the ExecutionRequest branch entirely, so the queued viewport render is dropped and its ExecutionResponse is never sent. Before this reorder the execution ran first and the copy was skipped instead. Consider processing the SVG copy without the early return None (e.g., continue after sending the clipboard) so a concurrent execution request still runs.

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

<comment>Moving `svg_clipboard` ahead of `execution` lets a pending `CopySvgTextClipboard` request short-circuit a pending render. The `CopySvgTextClipboard` branch returns `None` early when `combined_graphics.is_empty()` (the case for `copy_svg_clipboard(graphite_json, Vec::new())` in portfolio_message_handler.rs:1697 and for an empty selection). Because the loop processes `svg_clipboard` first, that `return None` now skips the `ExecutionRequest` branch entirely, so the queued viewport render is dropped and its `ExecutionResponse` is never sent. Before this reorder the execution ran first and the copy was skipped instead. Consider processing the SVG copy without the early `return None` (e.g., `continue` after sending the clipboard) so a concurrent execution request still runs.</comment>

<file context>
@@ -200,7 +200,7 @@ impl NodeRuntime {
 		}
 
-		let requests = [preferences, graph, eyedropper, execution, svg_clipboard].into_iter().flatten();
+		let requests = [preferences, graph, eyedropper, svg_clipboard, execution].into_iter().flatten();
 
 		for request in requests {
</file context>

@VimYoung

VimYoung commented Sep 7, 2026

Copy link
Copy Markdown
Author

Hey @timon-schelling , we can wait for the API to be merged and then do the changes, what do you mean for the API to be lazy here?
Also can you assign the issue to me as this PR is opened and in review?

@timon-schelling

Copy link
Copy Markdown
Member

Hey @timon-schelling , we can wait for the API to be merged and then do the changes, what do you mean for the API to be lazy here?

See the top level API definitions in rust-windowing/winit#4658

Both the DnD and Clipboard APIs are lazy in winit, meaning data only needs to be constructed once the other application accepts (based on data type).

I think we should model it in a similar way.
I'm not completely decided on how to model this with messages though.

Also can you assign the issue to me as this PR is opened and in review?

We usually do that when something is merged.

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

1 issue found across 6 files (changes from recent commits).

Confidence score: 3/5

  • In editor/src/messages/clipboard/clipboard_message_handler.rs, Chrome/Edge text copying can fail because Option::None becomes JSON null, which passes the frontend’s svg_string !== undefined check and leads to invalid clipboard construction; align the null/undefined handling before merging.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="editor/src/messages/clipboard/clipboard_message_handler.rs">

<violation number="1" location="editor/src/messages/clipboard/clipboard_message_handler.rs:91">
P1: Copying a text selection breaks on Chrome/Edge: the frontend receives `svg_string` as `null` (serde serializes Rust `Option::None` as JSON null), so `data.svg_string !== undefined` is true, and it builds `new ClipboardItem({ "image/svg+xml": null, ... })`, which throws a TypeError and never writes the text. The guard must treat `null` as absent too (`data.svg_string != null`), or the text case must not be routed through `TriggerClipboardSvgAndJsonWrite`.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

responses.add(PortfolioMessage::RequestSvgTextCopy { graphite_json });
}
ClipboardContent::Text(graphite_json) => {
responses.add(FrontendMessage::TriggerClipboardSvgAndJsonWrite { svg_string: None, graphite_json });

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: Copying a text selection breaks on Chrome/Edge: the frontend receives svg_string as null (serde serializes Rust Option::None as JSON null), so data.svg_string !== undefined is true, and it builds new ClipboardItem({ "image/svg+xml": null, ... }), which throws a TypeError and never writes the text. The guard must treat null as absent too (data.svg_string != null), or the text case must not be routed through TriggerClipboardSvgAndJsonWrite.

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

<comment>Copying a text selection breaks on Chrome/Edge: the frontend receives `svg_string` as `null` (serde serializes Rust `Option::None` as JSON null), so `data.svg_string !== undefined` is true, and it builds `new ClipboardItem({ "image/svg+xml": null, ... })`, which throws a TypeError and never writes the text. The guard must treat `null` as absent too (`data.svg_string != null`), or the text case must not be routed through `TriggerClipboardSvgAndJsonWrite`.</comment>

<file context>
@@ -87,8 +87,8 @@ impl MessageHandler<ClipboardMessage, ClipboardMessageContext<'_>> for Clipboard
-				ClipboardContent::Text(text) => {
-					responses.add(FrontendMessage::TriggerClipboardWrite { content: text });
+				ClipboardContent::Text(graphite_json) => {
+					responses.add(FrontendMessage::TriggerClipboardSvgAndJsonWrite { svg_string: None, graphite_json });
 				}
 			},
</file context>

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.

The None does become an undefined in js.

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

1 existing issue remains and 3 new issues found across 8 files (changes from recent commits).

Confidence score: 2/5

  • editor/src/node_graph_executor/runtime.rs in collect_graphics drops rank-0 Item<Graphic> monitor output, causing CopySvgTextClipboard to send an empty SVG for selected layers; preserve the rank-0 output alongside list graphics.
  • frontend/src/managers/clipboard.ts builds a ClipboardItem with a null SVG for text-only copies because the new guard does not trigger, so clipboard writes throw instead of falling back to writeText; fix the SVG presence check and fallback path.
  • editor/src/messages/clipboard/clipboard_message_handler.rs labels plain caret text from ReadSelection as if it were serialized Graphite content, which can mislead downstream clipboard handling; keep the binding aligned with the actual payload.
  • node-graph/libraries/core-types/src/transform.rs applies absolute resolution to reversed bounds while translating from bounds[0], potentially placing transformed content at the wrong corner; make translation consistent with reversed-bound handling.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="editor/src/messages/clipboard/clipboard_message_handler.rs">

<violation number="1" location="editor/src/messages/clipboard/clipboard_message_handler.rs:90">
P3: `ClipboardContent::Text` is only ever constructed from `ReadSelection { content }`, which carries the plain text read at the caret (`readAtCaret`), not a serialized Graphite payload. Renaming the binding to `graphite_json` and passing it through the `graphite_json` field of `TriggerClipboardSvgAndJsonWrite` is misleading: the value written to the system clipboard here is plain text (the frontend's else branch does `writeText` when `svg_string` is None). A future maintainer will reasonably assume this field holds a `graphite:`-prefixed JSON payload. Use a name that reflects the value, e.g. `text`, and note clearly that plain text copy intentionally sends `svg_string: None` with the text in the graphite_json slot.</violation>
</file>

<file name="node-graph/libraries/core-types/src/transform.rs">

<violation number="1" location="node-graph/libraries/core-types/src/transform.rs:192">
P3: The resolution clamps with `.abs()` while the translation always uses `bounds[0]`, so reversed bounds would render at the wrong corner. The function's `.abs()` implies it handles reversed bounds but the translation does not. Since callers pass normalized `[min, max]` bounds, either drop the `.abs()` (documenting the `[min, max]` contract) or normalize both corners consistently.</violation>
</file>

<file name="editor/src/node_graph_executor/runtime.rs">

<violation number="1" location="editor/src/node_graph_executor/runtime.rs:568">
P1: When a selected layer's monitor output is `Item<Graphic>`, `collect_graphics` drops it and `CopySvgTextClipboard` sends an empty SVG. Preserve the rank-0 branch by pushing `io.output` alongside the existing `List<Graphic>` handling.</violation>
</file>

Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.
Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment on lines +568 to +574
if let Ok(introspected_data) = self.executor.introspect(monitor_node_path)
&& let Some(io) = introspected_data.downcast_ref::<IORecord<Context, List<Graphic>>>()
{
combined_graphics.extend(io.output.clone());
} else {
warn!("No graphic type is matched while extracting svg");
}

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: When a selected layer's monitor output is Item<Graphic>, collect_graphics drops it and CopySvgTextClipboard sends an empty SVG. Preserve the rank-0 branch by pushing io.output alongside the existing List<Graphic> handling.

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

<comment>When a selected layer's monitor output is `Item<Graphic>`, `collect_graphics` drops it and `CopySvgTextClipboard` sends an empty SVG. Preserve the rank-0 branch by pushing `io.output` alongside the existing `List<Graphic>` handling.</comment>

<file context>
@@ -589,6 +550,36 @@ impl NodeRuntime {
+
+			if selected_node_ids.contains(&parent_network_node_id) {
+				// Introspect using the full monitor node path
+				if let Ok(introspected_data) = self.executor.introspect(monitor_node_path)
+					&& let Some(io) = introspected_data.downcast_ref::<IORecord<Context, List<Graphic>>>()
+				{
</file context>
Suggested change
if let Ok(introspected_data) = self.executor.introspect(monitor_node_path)
&& let Some(io) = introspected_data.downcast_ref::<IORecord<Context, List<Graphic>>>()
{
combined_graphics.extend(io.output.clone());
} else {
warn!("No graphic type is matched while extracting svg");
}
if let Ok(introspected_data) = self.executor.introspect(monitor_node_path) {
if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, List<Graphic>>>() {
combined_graphics.extend(io.output.clone());
} else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Item<Graphic>>>() {
combined_graphics.push(io.output.clone());
} else {
warn!("No graphic type is matched while extracting svg");
}
} else {
warn!("No graphic type is matched while extracting svg");
}

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.

This doesn't happen as it will always be made into a group.

Comment on lines +90 to +92
ClipboardContent::Text(graphite_json) => {
responses.add(FrontendMessage::TriggerClipboardSvgAndJsonWrite { svg_string: None, graphite_json });
}

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.

P3: ClipboardContent::Text is only ever constructed from ReadSelection { content }, which carries the plain text read at the caret (readAtCaret), not a serialized Graphite payload. Renaming the binding to graphite_json and passing it through the graphite_json field of TriggerClipboardSvgAndJsonWrite is misleading: the value written to the system clipboard here is plain text (the frontend's else branch does writeText when svg_string is None). A future maintainer will reasonably assume this field holds a graphite:-prefixed JSON payload. Use a name that reflects the value, e.g. text, and note clearly that plain text copy intentionally sends svg_string: None with the text in the graphite_json slot.

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

<comment>`ClipboardContent::Text` is only ever constructed from `ReadSelection { content }`, which carries the plain text read at the caret (`readAtCaret`), not a serialized Graphite payload. Renaming the binding to `graphite_json` and passing it through the `graphite_json` field of `TriggerClipboardSvgAndJsonWrite` is misleading: the value written to the system clipboard here is plain text (the frontend's else branch does `writeText` when `svg_string` is None). A future maintainer will reasonably assume this field holds a `graphite:`-prefixed JSON payload. Use a name that reflects the value, e.g. `text`, and note clearly that plain text copy intentionally sends `svg_string: None` with the text in the graphite_json slot.</comment>

<file context>
@@ -87,8 +87,8 @@ impl MessageHandler<ClipboardMessage, ClipboardMessageContext<'_>> for Clipboard
 				}
-				ClipboardContent::Text(text) => {
-					responses.add(FrontendMessage::TriggerClipboardWrite { content: text });
+				ClipboardContent::Text(graphite_json) => {
+					responses.add(FrontendMessage::TriggerClipboardSvgAndJsonWrite { svg_string: None, graphite_json });
 				}
</file context>
Suggested change
ClipboardContent::Text(graphite_json) => {
responses.add(FrontendMessage::TriggerClipboardSvgAndJsonWrite { svg_string: None, graphite_json });
}
ClipboardContent::Text(text) => {
responses.add(FrontendMessage::TriggerClipboardSvgAndJsonWrite { svg_string: None, graphite_json: text });
}

Comment on lines +192 to +197
pub fn from_bounds(bounds: [DVec2; 2], quality: RenderQuality) -> Self {
Footprint {
transform: DAffine2::from_translation(DVec2::new(bounds[0].x, bounds[0].y)),
resolution: UVec2::new((bounds[1].x - bounds[0].x).abs().ceil() as u32, (bounds[1].y - bounds[0].y).abs().ceil() as u32).max(UVec2::ONE),
quality,
}

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.

P3: The resolution clamps with .abs() while the translation always uses bounds[0], so reversed bounds would render at the wrong corner. The function's .abs() implies it handles reversed bounds but the translation does not. Since callers pass normalized [min, max] bounds, either drop the .abs() (documenting the [min, max] contract) or normalize both corners consistently.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node-graph/libraries/core-types/src/transform.rs, line 192:

<comment>The resolution clamps with `.abs()` while the translation always uses `bounds[0]`, so reversed bounds would render at the wrong corner. The function's `.abs()` implies it handles reversed bounds but the translation does not. Since callers pass normalized `[min, max]` bounds, either drop the `.abs()` (documenting the `[min, max]` contract) or normalize both corners consistently.</comment>

<file context>
@@ -189,6 +189,14 @@ impl Footprint {
 		quality: RenderQuality::Full,
 	};
 
+	pub fn from_bounds(bounds: [DVec2; 2], quality: RenderQuality) -> Self {
+		Footprint {
+			transform: DAffine2::from_translation(DVec2::new(bounds[0].x, bounds[0].y)),
</file context>
Suggested change
pub fn from_bounds(bounds: [DVec2; 2], quality: RenderQuality) -> Self {
Footprint {
transform: DAffine2::from_translation(DVec2::new(bounds[0].x, bounds[0].y)),
resolution: UVec2::new((bounds[1].x - bounds[0].x).abs().ceil() as u32, (bounds[1].y - bounds[0].y).abs().ceil() as u32).max(UVec2::ONE),
quality,
}
pub fn from_bounds(bounds: [DVec2; 2], quality: RenderQuality) -> Self {
Footprint {
transform: DAffine2::from_translation(bounds[0]),
resolution: UVec2::new((bounds[1].x - bounds[0].x).ceil() as u32, (bounds[1].y - bounds[0].y).ceil() as u32).max(UVec2::ONE),
quality,
}
}

pub fn from_bounds(bounds: [DVec2; 2], quality: RenderQuality) -> Self {
Footprint {
transform: DAffine2::from_translation(DVec2::new(bounds[0].x, bounds[0].y)),
resolution: UVec2::new((bounds[1].x - bounds[0].x).abs().ceil() as u32, (bounds[1].y - bounds[0].y).abs().ceil() as u32).max(UVec2::ONE),

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.

Suggested change
resolution: UVec2::new((bounds[1].x - bounds[0].x).abs().ceil() as u32, (bounds[1].y - bounds[0].y).abs().ceil() as u32).max(UVec2::ONE),
transform: DAffine2::from_translation(bounds[0].min(bounds[1])),
resolution: (bounds[1] - bounds[0]).abs().ceil().as_uvec2().max(UVec2::ONE),

Comment on lines +568 to +574
if let Ok(introspected_data) = self.executor.introspect(monitor_node_path)
&& let Some(io) = introspected_data.downcast_ref::<IORecord<Context, List<Graphic>>>()
{
combined_graphics.extend(io.output.clone());
} else {
warn!("No graphic type is matched while extracting svg");
}

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.

This doesn't happen as it will always be made into a group.

responses.add(PortfolioMessage::RequestSvgTextCopy { graphite_json });
}
ClipboardContent::Text(graphite_json) => {
responses.add(FrontendMessage::TriggerClipboardSvgAndJsonWrite { svg_string: None, graphite_json });

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.

The None does become an undefined in js.


if combined_graphics.is_empty() {
self.sender.send_svg_text_clipboard(String::new(), text_string_clipboard);
return None;

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.

continue don't return.

let requests = [preferences, graph, eyedropper, svg_clipboard, execution].into_iter().flatten();

for request in requests {
match request {

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.

I think the control flow would be much easier if these were made into functions:

match request {
	GraphRuntimeRequest::EditorPreferencesUpdate(preferences) => self.editor_preferences_update(preferences),
	GraphRuntimeRequest::GraphUpdate(graph_update) => self.graph_update(graph_update),
	...
}

}

let requests = [preferences, graph, eyedropper, execution].into_iter().flatten();
let requests = [preferences, graph, eyedropper, svg_clipboard, execution].into_iter().flatten();

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.

This is technically true however it probably isn't very relevant since the user would be copying an SVG that is not yet rendered into the viewport.

You could fix this by having a result variable instead of an early return:

let mut result = None;
for request in requests {
	match request {
		// ...
		GraphRuntimeRequest::ExecutionRequest(ExecutionRequest { execution_id, mut render_config, .. }) => {
			// ...
			result = Some(texture);
		}
	}
}
result

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Copy/pasting vector and raster content between other graphics apps

3 participants