From b7327ae3c1543376b09948fbe390c58181e7111c Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Wed, 11 Mar 2026 01:23:57 -0700 Subject: [PATCH] Fix the Eyedropper tool on web with Vello and on desktop with SVG --- .../portfolio/portfolio_message_handler.rs | 5 --- .../tool/tool_messages/eyedropper_tool.rs | 44 ++++++++++--------- editor/src/node_graph_executor.rs | 15 ++++++- editor/src/node_graph_executor/runtime.rs | 4 ++ .../src/components/panels/Document.svelte | 2 +- 5 files changed, 42 insertions(+), 28 deletions(-) diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index d45f832f82..5f00696c2b 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -1191,7 +1191,6 @@ impl MessageHandler> for Portfolio Ok(message) => responses.add_front(message), } } - #[cfg(not(target_family = "wasm"))] PortfolioMessage::SubmitEyedropperPreviewRender => { use crate::consts::EYEDROPPER_PREVIEW_AREA_RESOLUTION; @@ -1223,10 +1222,6 @@ impl MessageHandler> for Portfolio Ok(message) => responses.add_front(message), } } - #[cfg(target_family = "wasm")] - PortfolioMessage::SubmitEyedropperPreviewRender => { - // TODO: Currently for Wasm, this is implemented through SVG rendering but the Eyedropper tool doesn't work at all when Vello is enabled as the renderer - } PortfolioMessage::ToggleFocusDocument => { self.focus_document = !self.focus_document; responses.add(MenuBarMessage::SendLayout); diff --git a/editor/src/messages/tool/tool_messages/eyedropper_tool.rs b/editor/src/messages/tool/tool_messages/eyedropper_tool.rs index c18abc1362..1c525b361a 100644 --- a/editor/src/messages/tool/tool_messages/eyedropper_tool.rs +++ b/editor/src/messages/tool/tool_messages/eyedropper_tool.rs @@ -1,6 +1,7 @@ use super::tool_prelude::*; use crate::messages::frontend::utility_types::EyedropperPreviewImage; use crate::messages::tool::utility_types::DocumentToolData; +use graphene_std::vector::style::RenderMode; #[derive(Default, ExtractField)] pub struct EyedropperTool { @@ -108,14 +109,19 @@ impl Fsm for EyedropperToolFsmState { fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionMessageContext, _tool_options: &(), responses: &mut VecDeque) -> Self { let ToolActionMessageContext { - global_tool_data, input, viewport, .. + document, + global_tool_data, + input, + viewport, + .. } = tool_action_data; + let render_mode = document.render_mode; let ToolMessage::Eyedropper(event) = event else { return self }; match (self, event) { // Ready -> Sampling (EyedropperToolFsmState::Ready, mouse_down) if matches!(mouse_down, EyedropperToolMessage::SamplePrimaryColorBegin | EyedropperToolMessage::SampleSecondaryColorBegin) => { - update_cursor_preview(responses, tool_data, input, global_tool_data, None); + update_cursor_preview(responses, tool_data, input, global_tool_data, None, render_mode); if mouse_down == EyedropperToolMessage::SamplePrimaryColorBegin { EyedropperToolFsmState::SamplingPrimary @@ -127,7 +133,7 @@ impl Fsm for EyedropperToolFsmState { (EyedropperToolFsmState::SamplingPrimary | EyedropperToolFsmState::SamplingSecondary, EyedropperToolMessage::PointerMove) => { let mouse_position = viewport.logical(input.mouse.position); if viewport.is_in_bounds(mouse_position + viewport.offset()) { - update_cursor_preview(responses, tool_data, input, global_tool_data, None); + update_cursor_preview(responses, tool_data, input, global_tool_data, None, render_mode); } else { disable_cursor_preview(responses, tool_data); } @@ -141,7 +147,7 @@ impl Fsm for EyedropperToolFsmState { EyedropperToolFsmState::SamplingSecondary => PrimarySecondary::Secondary, _ => unreachable!(), }; - update_cursor_preview(responses, tool_data, input, global_tool_data, Some(set_color_choice)); + update_cursor_preview(responses, tool_data, input, global_tool_data, Some(set_color_choice), render_mode); disable_cursor_preview(responses, tool_data); EyedropperToolFsmState::Ready @@ -192,31 +198,29 @@ fn disable_cursor_preview(responses: &mut VecDeque, tool_data: &mut Eye }); } -#[cfg(not(target_family = "wasm"))] -fn update_cursor_preview( - responses: &mut VecDeque, - tool_data: &mut EyedropperToolData, - _input: &InputPreprocessorMessageHandler, - _global_tool_data: &DocumentToolData, - set_color_choice: Option, -) { - tool_data.preview = true; - tool_data.color_choice = set_color_choice; - responses.add(PortfolioMessage::SubmitEyedropperPreviewRender); -} - -#[cfg(target_family = "wasm")] fn update_cursor_preview( responses: &mut VecDeque, tool_data: &mut EyedropperToolData, input: &InputPreprocessorMessageHandler, global_tool_data: &DocumentToolData, set_color_choice: Option, + render_mode: RenderMode, ) { tool_data.preview = true; - tool_data.color_choice = set_color_choice.clone(); + tool_data.color_choice = set_color_choice; - update_cursor_preview_common(responses, None, input, global_tool_data, set_color_choice); + // On web, SVG Preview mode uses the frontend's SVG rasterization to sample pixels directly + #[cfg(target_family = "wasm")] + if render_mode == RenderMode::SvgPreview { + update_cursor_preview_common(responses, None, input, global_tool_data, set_color_choice); + return; + } + + let _ = (&input, &global_tool_data, &render_mode); + + // For Vello-rendered modes (Normal, Outline, and Pixel Preview), submit a backend render request + // which will return a zoomed-in pixel preview image via the EyedropperToolMessage::PreviewImage path + responses.add(PortfolioMessage::SubmitEyedropperPreviewRender); } fn update_cursor_preview_common( diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 8fe138be44..aa63da4076 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -185,7 +185,6 @@ impl NodeGraphExecutor { } #[allow(clippy::too_many_arguments)] - #[cfg(not(target_family = "wasm"))] pub(crate) fn submit_eyedropper_preview( &mut self, document: &DocumentMessageHandler, @@ -201,13 +200,25 @@ impl NodeGraphExecutor { resolution: viewport_resolution, ..Default::default() }; + + // TODO: On desktop, SVG Preview mode cannot work with the Eyedropper tool until is implemented. + // TODO: So for now, we fall back to the Eyedropper using Normal mode (Vello) rendering, which looks similar enough to SVG Preview. + #[cfg(not(target_family = "wasm"))] + let render_mode = match document.render_mode { + graphene_std::vector::style::RenderMode::SvgPreview => graphene_std::vector::style::RenderMode::Normal, + other => other, + }; + // On web, SVG Preview is handled by the frontend's SVG rasterization path instead, producing the correct result, so we keep it enabled. + #[cfg(target_family = "wasm")] + let render_mode = document.render_mode; + let render_config = RenderConfig { viewport, scale: viewport_scale, time, pointer, export_format: graphene_std::application_io::ExportFormat::Raster, - render_mode: document.render_mode, + render_mode, hide_artboards: false, for_export: false, for_eyedropper: true, diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index b02fa23617..26102cda07 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -309,6 +309,10 @@ impl NodeRuntime { self.sender.send_eyedropper_preview(raster_cpu); continue; } + // Eyedropper render that didn't produce a texture (e.g., SVG fallback when GPU is unavailable); discard it + _ if render_config.for_eyedropper => { + continue; + } #[cfg(all(target_family = "wasm", feature = "gpu"))] Ok(TaggedValue::RenderOutput(RenderOutput { data: RenderOutputType::Texture(image_texture), diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 3dc412250e..1767c18e6c 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -237,7 +237,7 @@ const outsideArtboards = ``; const svg = ` - ${outsideArtboards}${artworkSvg} + ${outsideArtboards}${artworkSvg} `.trim(); if (!rasterizedCanvas) {