Fix the Eyedropper tool on web with Vello and on desktop with SVG (#3886)

This commit is contained in:
Keavon Chambers 2026-03-11 03:26:02 -07:00 committed by GitHub
parent 116a4106c4
commit 81d0b8b8d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 42 additions and 28 deletions

View File

@ -1191,7 +1191,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
Ok(message) => responses.add_front(message), Ok(message) => responses.add_front(message),
} }
} }
#[cfg(not(target_family = "wasm"))]
PortfolioMessage::SubmitEyedropperPreviewRender => { PortfolioMessage::SubmitEyedropperPreviewRender => {
use crate::consts::EYEDROPPER_PREVIEW_AREA_RESOLUTION; use crate::consts::EYEDROPPER_PREVIEW_AREA_RESOLUTION;
@ -1223,10 +1222,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
Ok(message) => responses.add_front(message), 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 => { PortfolioMessage::ToggleFocusDocument => {
self.focus_document = !self.focus_document; self.focus_document = !self.focus_document;
responses.add(MenuBarMessage::SendLayout); responses.add(MenuBarMessage::SendLayout);

View File

@ -1,6 +1,7 @@
use super::tool_prelude::*; use super::tool_prelude::*;
use crate::messages::frontend::utility_types::EyedropperPreviewImage; use crate::messages::frontend::utility_types::EyedropperPreviewImage;
use crate::messages::tool::utility_types::DocumentToolData; use crate::messages::tool::utility_types::DocumentToolData;
use graphene_std::vector::style::RenderMode;
#[derive(Default, ExtractField)] #[derive(Default, ExtractField)]
pub struct EyedropperTool { 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<Message>) -> Self { fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionMessageContext, _tool_options: &(), responses: &mut VecDeque<Message>) -> Self {
let ToolActionMessageContext { let ToolActionMessageContext {
global_tool_data, input, viewport, .. document,
global_tool_data,
input,
viewport,
..
} = tool_action_data; } = tool_action_data;
let render_mode = document.render_mode;
let ToolMessage::Eyedropper(event) = event else { return self }; let ToolMessage::Eyedropper(event) = event else { return self };
match (self, event) { match (self, event) {
// Ready -> Sampling // Ready -> Sampling
(EyedropperToolFsmState::Ready, mouse_down) if matches!(mouse_down, EyedropperToolMessage::SamplePrimaryColorBegin | EyedropperToolMessage::SampleSecondaryColorBegin) => { (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 { if mouse_down == EyedropperToolMessage::SamplePrimaryColorBegin {
EyedropperToolFsmState::SamplingPrimary EyedropperToolFsmState::SamplingPrimary
@ -127,7 +133,7 @@ impl Fsm for EyedropperToolFsmState {
(EyedropperToolFsmState::SamplingPrimary | EyedropperToolFsmState::SamplingSecondary, EyedropperToolMessage::PointerMove) => { (EyedropperToolFsmState::SamplingPrimary | EyedropperToolFsmState::SamplingSecondary, EyedropperToolMessage::PointerMove) => {
let mouse_position = viewport.logical(input.mouse.position); let mouse_position = viewport.logical(input.mouse.position);
if viewport.is_in_bounds(mouse_position + viewport.offset()) { 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 { } else {
disable_cursor_preview(responses, tool_data); disable_cursor_preview(responses, tool_data);
} }
@ -141,7 +147,7 @@ impl Fsm for EyedropperToolFsmState {
EyedropperToolFsmState::SamplingSecondary => PrimarySecondary::Secondary, EyedropperToolFsmState::SamplingSecondary => PrimarySecondary::Secondary,
_ => unreachable!(), _ => 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); disable_cursor_preview(responses, tool_data);
EyedropperToolFsmState::Ready EyedropperToolFsmState::Ready
@ -192,31 +198,29 @@ fn disable_cursor_preview(responses: &mut VecDeque<Message>, tool_data: &mut Eye
}); });
} }
#[cfg(not(target_family = "wasm"))]
fn update_cursor_preview(
responses: &mut VecDeque<Message>,
tool_data: &mut EyedropperToolData,
_input: &InputPreprocessorMessageHandler,
_global_tool_data: &DocumentToolData,
set_color_choice: Option<PrimarySecondary>,
) {
tool_data.preview = true;
tool_data.color_choice = set_color_choice;
responses.add(PortfolioMessage::SubmitEyedropperPreviewRender);
}
#[cfg(target_family = "wasm")]
fn update_cursor_preview( fn update_cursor_preview(
responses: &mut VecDeque<Message>, responses: &mut VecDeque<Message>,
tool_data: &mut EyedropperToolData, tool_data: &mut EyedropperToolData,
input: &InputPreprocessorMessageHandler, input: &InputPreprocessorMessageHandler,
global_tool_data: &DocumentToolData, global_tool_data: &DocumentToolData,
set_color_choice: Option<PrimarySecondary>, set_color_choice: Option<PrimarySecondary>,
render_mode: RenderMode,
) { ) {
tool_data.preview = true; tool_data.preview = true;
tool_data.color_choice = set_color_choice.clone(); tool_data.color_choice = 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); 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( fn update_cursor_preview_common(

View File

@ -185,7 +185,6 @@ impl NodeGraphExecutor {
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
#[cfg(not(target_family = "wasm"))]
pub(crate) fn submit_eyedropper_preview( pub(crate) fn submit_eyedropper_preview(
&mut self, &mut self,
document: &DocumentMessageHandler, document: &DocumentMessageHandler,
@ -201,13 +200,25 @@ impl NodeGraphExecutor {
resolution: viewport_resolution, resolution: viewport_resolution,
..Default::default() ..Default::default()
}; };
// TODO: On desktop, SVG Preview mode cannot work with the Eyedropper tool until <https://github.com/GraphiteEditor/Graphite/issues/3796> 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 { let render_config = RenderConfig {
viewport, viewport,
scale: viewport_scale, scale: viewport_scale,
time, time,
pointer, pointer,
export_format: graphene_std::application_io::ExportFormat::Raster, export_format: graphene_std::application_io::ExportFormat::Raster,
render_mode: document.render_mode, render_mode,
hide_artboards: false, hide_artboards: false,
for_export: false, for_export: false,
for_eyedropper: true, for_eyedropper: true,

View File

@ -309,6 +309,10 @@ impl NodeRuntime {
self.sender.send_eyedropper_preview(raster_cpu); self.sender.send_eyedropper_preview(raster_cpu);
continue; 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"))] #[cfg(all(target_family = "wasm", feature = "gpu"))]
Ok(TaggedValue::RenderOutput(RenderOutput { Ok(TaggedValue::RenderOutput(RenderOutput {
data: RenderOutputType::Texture(image_texture), data: RenderOutputType::Texture(image_texture),

View File

@ -237,7 +237,7 @@
const outsideArtboards = `<rect x="0" y="0" width="100%" height="100%" fill="${outsideArtboardsColor}" />`; const outsideArtboards = `<rect x="0" y="0" width="100%" height="100%" fill="${outsideArtboardsColor}" />`;
const svg = ` const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">${outsideArtboards}${artworkSvg}</svg> <svg xmlns="http://www.w3.org/2000/svg" xmlns:graphite="https://graphite.art" width="${width}" height="${height}">${outsideArtboards}${artworkSvg}</svg>
`.trim(); `.trim();
if (!rasterizedCanvas) { if (!rasterizedCanvas) {