From ebf67eaa8282e4a6010672d44caa8f61612375f8 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Wed, 3 May 2023 02:09:07 -0700 Subject: [PATCH] Implement download/copy ImageFrame layer output (#1194) * Implement download/copy ImageFrame layer output * Add note about black transparency when copying * Introspect node graph output type to conditionally disable the download button --------- Co-authored-by: Dennis Kobert --- document-legacy/src/layers/layer_info.rs | 6 +-- .../src/messages/frontend/frontend_message.rs | 24 ++++++++---- .../portfolio/document/document_message.rs | 6 +++ .../document/document_message_handler.rs | 29 ++++++++++++-- .../graph_operation_message_handler.rs | 2 +- .../transform_utils.rs | 2 +- .../node_graph/node_graph_message_handler.rs | 9 +++-- .../document_node_types.rs | 2 +- .../node_properties.rs | 34 +++++++++++++++- .../messages/tool/tool_messages/brush_tool.rs | 2 +- .../tool/tool_messages/select_tool.rs | 2 +- .../messages/tool/tool_messages/text_tool.rs | 4 +- editor/src/node_graph_executor.rs | 10 ++++- frontend/src/io-managers/clipboard.ts | 18 +++++++++ frontend/src/state-providers/portfolio.ts | 39 +++++++++++++------ .../src/utility-functions/rasterization.ts | 22 ++++++++++- frontend/src/wasm-communication/messages.ts | 38 +++++++++++------- 17 files changed, 197 insertions(+), 52 deletions(-) diff --git a/document-legacy/src/layers/layer_info.rs b/document-legacy/src/layers/layer_info.rs index 10ee572f..3a84685e 100644 --- a/document-legacy/src/layers/layer_info.rs +++ b/document-legacy/src/layers/layer_info.rs @@ -449,7 +449,7 @@ impl Layer { /// Get a mutable reference to the NodeNetwork /// This operation will fail if the [Layer type](Layer::data) is not `LayerDataType::Layer`. - pub fn as_node_graph_mut(&mut self) -> Result<&mut graph_craft::document::NodeNetwork, DocumentError> { + pub fn as_layer_network_mut(&mut self) -> Result<&mut graph_craft::document::NodeNetwork, DocumentError> { match &mut self.data { LayerDataType::Layer(layer) => Ok(&mut layer.network), _ => Err(DocumentError::NotNodeGraph), @@ -458,14 +458,14 @@ impl Layer { /// Get a reference to the NodeNetwork /// This operation will fail if the [Layer type](Layer::data) is not `LayerDataType::Layer`. - pub fn as_node_graph(&self) -> Result<&graph_craft::document::NodeNetwork, DocumentError> { + pub fn as_layer_network(&self) -> Result<&graph_craft::document::NodeNetwork, DocumentError> { match &self.data { LayerDataType::Layer(layer) => Ok(&layer.network), _ => Err(DocumentError::NotNodeGraph), } } - pub fn as_graph_frame(&self) -> Result<&LayerLayer, DocumentError> { + pub fn as_layer(&self) -> Result<&LayerLayer, DocumentError> { match &self.data { LayerDataType::Layer(layer) => Ok(layer), _ => Err(DocumentError::NotNodeGraph), diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index e4a5ae2a..1ef3cf1f 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -50,7 +50,23 @@ pub enum FrontendMessage { #[serde(rename = "commitDate")] commit_date: String, }, - TriggerFileDownload { + TriggerCopyToClipboardBlobUrl { + #[serde(rename = "blobUrl")] + blob_url: String, + }, + TriggerDownloadBlobUrl { + #[serde(rename = "layerName")] + layer_name: String, + #[serde(rename = "blobUrl")] + blob_url: String, + }, + TriggerDownloadRaster { + svg: String, + name: String, + mime: String, + size: (f64, f64), + }, + TriggerDownloadTextFile { document: String, name: String, }, @@ -107,12 +123,6 @@ pub enum FrontendMessage { TriggerLoadPreferences, TriggerOpenDocument, TriggerPaste, - TriggerRasterDownload { - svg: String, - name: String, - mime: String, - size: (f64, f64), - }, TriggerRasterizeRegionBelowLayer { #[serde(rename = "documentId")] document_id: u64, diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index 498316d7..fa8314f0 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -58,6 +58,9 @@ pub enum DocumentMessage { BooleanOperation(BooleanOperationType), ClearLayerTree, CommitTransaction, + CopyToClipboardLayerImageOutput { + layer_path: Vec, + }, CreateEmptyFolder { container_path: Vec, }, @@ -72,6 +75,9 @@ pub enum DocumentMessage { DocumentHistoryBackward, DocumentHistoryForward, DocumentStructureChanged, + DownloadLayerImageOutput { + layer_path: Vec, + }, DuplicateSelectedLayers, ExportDocument { file_name: String, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 8245bf19..44fce3ca 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -278,6 +278,15 @@ impl MessageHandler (), + CopyToClipboardLayerImageOutput { layer_path } => { + let layer = self.document_legacy.layer(&layer_path).ok(); + + let blob_url = layer.and_then(|layer| layer.as_layer().ok()).and_then(|layer_layer| layer_layer.as_blob_url()).cloned(); + + if let Some(blob_url) = blob_url { + responses.add(FrontendMessage::TriggerCopyToClipboardBlobUrl { blob_url }); + } + } CreateEmptyFolder { mut container_path } => { let id = generate_uuid(); container_path.push(id); @@ -326,6 +335,17 @@ impl MessageHandler { + let layer = self.document_legacy.layer(&layer_path).ok(); + + let layer_name = layer.map(|layer| layer.name.clone().unwrap_or_else(|| "Untitled Layer".to_string())); + + let blob_url = layer.and_then(|layer| layer.as_layer().ok()).and_then(|layer_layer| layer_layer.as_blob_url()).cloned(); + + if let (Some(layer_name), Some(blob_url)) = (layer_name, blob_url) { + responses.add(FrontendMessage::TriggerDownloadBlobUrl { layer_name, blob_url }); + } + } DuplicateSelectedLayers => { self.backup(responses); responses.add_front(SetSelectedLayers { replacement_selected_layers: vec![] }); @@ -353,6 +373,7 @@ impl MessageHandler { @@ -702,7 +723,7 @@ impl MessageHandler self.name.clone(), false => self.name.clone() + FILE_SAVE_SUFFIX, }; - responses.add(FrontendMessage::TriggerFileDownload { + responses.add(FrontendMessage::TriggerDownloadTextFile { document: self.serialize_document(), name, }) @@ -968,7 +989,7 @@ impl DocumentMessageHandler { ) -> Option { // Prepare the node graph input image - let Some(node_network) = self.document_legacy.layer(&layer_path).ok().and_then(|layer| layer.as_node_graph().ok()) else { + let Some(node_network) = self.document_legacy.layer(&layer_path).ok().and_then(|layer| layer.as_layer_network().ok()) else { return None; }; diff --git a/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs index ec202d56..8181b407 100644 --- a/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs @@ -24,7 +24,7 @@ struct ModifyInputsContext<'a> { impl<'a> ModifyInputsContext<'a> { /// Get the node network from the document fn new(layer: &'a [LayerId], document: &'a mut Document, node_graph: &'a mut NodeGraphMessageHandler, responses: &'a mut VecDeque) -> Option { - document.layer_mut(layer).ok().and_then(|layer| layer.as_node_graph_mut().ok()).map(|network| Self { + document.layer_mut(layer).ok().and_then(|layer| layer.as_layer_network_mut().ok()).map(|network| Self { network, node_graph, responses, diff --git a/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler/transform_utils.rs b/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler/transform_utils.rs index e139bbc0..57938d83 100644 --- a/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler/transform_utils.rs +++ b/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler/transform_utils.rs @@ -53,7 +53,7 @@ impl LayerBounds { pub fn new(document: &Document, layer_path: &[u64]) -> Self { let layer = document.layer(layer_path).ok(); let bounds = layer - .and_then(|layer| layer.as_graph_frame().ok()) + .and_then(|layer| layer.as_layer().ok()) .and_then(|frame| frame.as_vector_data().as_ref().map(|vector| vector.nonzero_bounding_box())) .unwrap_or([DVec2::ZERO, DVec2::ONE]); let bounds_transform = DAffine2::IDENTITY; diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index db348d6e..ee7094ce 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -118,11 +118,14 @@ pub struct NodeGraphMessageHandler { impl NodeGraphMessageHandler { fn get_root_network<'a>(&self, document: &'a Document) -> Option<&'a graph_craft::document::NodeNetwork> { - self.layer_path.as_ref().and_then(|path| document.layer(path).ok()).and_then(|layer| layer.as_node_graph().ok()) + self.layer_path.as_ref().and_then(|path| document.layer(path).ok()).and_then(|layer| layer.as_layer_network().ok()) } fn get_root_network_mut<'a>(&self, document: &'a mut Document) -> Option<&'a mut graph_craft::document::NodeNetwork> { - self.layer_path.as_ref().and_then(|path| document.layer_mut(path).ok()).and_then(|layer| layer.as_node_graph_mut().ok()) + self.layer_path + .as_ref() + .and_then(|path| document.layer_mut(path).ok()) + .and_then(|layer| layer.as_layer_network_mut().ok()) } /// Get the active graph_craft NodeNetwork struct @@ -726,7 +729,7 @@ impl MessageHandler Vec { default: NodeInput::value(TaggedValue::ImageFrame(ImageFrame::empty()), true), }], outputs: vec![], - properties: |_document_node, _node_id, _context| node_properties::string_properties("The graph's output is drawn in the layer"), + properties: node_properties::output_properties, }, DocumentNodeType { name: "Image Frame", diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs index 964fafcf..4d55515e 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs @@ -7,11 +7,12 @@ use document_legacy::Operation; use glam::DVec2; use graph_craft::document::value::TaggedValue; use graph_craft::document::{DocumentNode, NodeId, NodeInput}; -use graph_craft::imaginate_input::*; +use graph_craft::{concrete, imaginate_input::*}; use graphene_core::raster::{BlendMode, Color, ImageFrame, LuminanceCalculation, RedGreenBlue, RelativeAbsolute, SelectiveColorChoice}; use graphene_core::text::Font; use graphene_core::vector::style::{FillType, GradientType, LineCap, LineJoin}; use graphene_core::EditorApi; +use graphene_core::{Cow, Type, TypeDescriptor}; use super::document_node_types::NodePropertiesContext; use super::{FrontendGraphDataType, IMAGINATE_NODE}; @@ -512,6 +513,37 @@ pub fn blend_properties(document_node: &DocumentNode, node_id: NodeId, _context: vec![backdrop, blend_mode, LayoutGroup::Row { widgets: opacity }] } +pub fn output_properties(_document_node: &DocumentNode, _node_id: NodeId, context: &mut NodePropertiesContext) -> Vec { + let output_type = context.executor.previous_output_type(context.layer_path); + let raster_output_type = concrete!(ImageFrame); + let disabled = match output_type { + Some(output_type) => output_type != raster_output_type, + None => true, + }; + + let layer_path_1 = context.layer_path.to_vec(); + let layer_path_2 = context.layer_path.to_vec(); + + let label = TextLabel::new("The graph's output is drawn in the layer").widget_holder(); + let download_button = TextButton::new("Download Render Output") + .tooltip("Download the rendered image output as a PNG file") + .disabled(disabled) + .on_update(move |_| DocumentMessage::DownloadLayerImageOutput { layer_path: layer_path_1.clone() }.into()) + .widget_holder(); + let copy_button = TextButton::new("Copy Render Output") + .tooltip("Copy the rendered image output to the clipboard") + .disabled(disabled) + .on_update(move |_| DocumentMessage::CopyToClipboardLayerImageOutput { layer_path: layer_path_2.clone() }.into()) + .widget_holder(); + + vec![ + LayoutGroup::Row { widgets: vec![label] }, + LayoutGroup::Row { + widgets: vec![download_button, WidgetHolder::related_separator(), copy_button], + }, + ] +} + pub fn mask_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { let mask = color_widget(document_node, node_id, 1, "Stencil", ColorInput::default(), true); diff --git a/editor/src/messages/tool/tool_messages/brush_tool.rs b/editor/src/messages/tool/tool_messages/brush_tool.rs index c7e1f396..bac88194 100644 --- a/editor/src/messages/tool/tool_messages/brush_tool.rs +++ b/editor/src/messages/tool/tool_messages/brush_tool.rs @@ -322,7 +322,7 @@ fn load_existing_points(document: &DocumentMessageHandler) -> Option<(Vec>, responses: &mut VecDeque, render_data: &RenderData) { let get_bounds = |layer: &Layer, path: &[LayerId], document: &DocumentMessageHandler, render_data: &RenderData| { - let node_graph = layer.as_node_graph().ok()?; + let node_graph = layer.as_layer_network().ok()?; let node_id = get_text_node_id(node_graph)?; let document_node = node_graph.nodes.get(&node_id)?; let (text, font, font_size) = TextToolData::extract_text_node_inputs(document_node)?; @@ -410,7 +410,7 @@ fn update_overlays(document: &DocumentMessageHandler, tool_data: &mut TextToolDa fn get_network<'a>(layer_path: &[LayerId], document: &'a DocumentMessageHandler) -> Option<&'a NodeNetwork> { let layer = document.document_legacy.layer(layer_path).ok()?; - layer.as_node_graph().ok() + layer.as_layer_network().ok() } fn get_text_node_id(network: &NodeNetwork) -> Option { diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index b7f57426..aee73dd0 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -20,7 +20,9 @@ use std::borrow::Cow; #[derive(Debug, Clone, Default)] pub struct NodeGraphExecutor { - executor: DynamicExecutor, + pub(crate) executor: DynamicExecutor, + // TODO: This is a memory leak since layers are never removed + pub(crate) last_output_type: HashMap, Option>, } impl NodeGraphExecutor { @@ -55,6 +57,10 @@ impl NodeGraphExecutor { self.executor.introspect(path).flatten() } + pub fn previous_output_type(&self, path: &[LayerId]) -> Option { + self.last_output_type.get(path).cloned().flatten() + } + /// Computes an input for a node in the graph pub fn compute_input(&mut self, old_network: &NodeNetwork, node_path: &[NodeId], mut input_index: usize, editor_api: Cow>) -> Result { let mut network = old_network.clone(); @@ -272,11 +278,13 @@ impl NodeGraphExecutor { // Update the cached vector data on the layer let vector_data: VectorData = dyn_any::downcast(boxed_node_graph_output).map(|v| *v)?; let transform = vector_data.transform.to_cols_array(); + self.last_output_type.insert(layer_path.clone(), Some(concrete!(VectorData))); responses.add(Operation::SetLayerTransform { path: layer_path.clone(), transform }); responses.add(Operation::SetVectorData { path: layer_path, vector_data }); } else { // Attempt to downcast to an image frame let ImageFrame { image, transform } = dyn_any::downcast(boxed_node_graph_output).map(|image_frame| *image_frame)?; + self.last_output_type.insert(layer_path.clone(), Some(concrete!(ImageFrame))); // Don't update the frame's transform if the new transform is DAffine2::ZERO. let transform = (!transform.abs_diff_eq(DAffine2::ZERO, f64::EPSILON)).then_some(transform.to_cols_array()); diff --git a/frontend/src/io-managers/clipboard.ts b/frontend/src/io-managers/clipboard.ts index 1d94d30e..e7bf0aae 100644 --- a/frontend/src/io-managers/clipboard.ts +++ b/frontend/src/io-managers/clipboard.ts @@ -1,5 +1,6 @@ import { type Editor } from "@graphite/wasm-communication/editor"; import { TriggerTextCopy } from "@graphite/wasm-communication/messages"; +import { imageToPNG } from "~src/utility-functions/rasterization"; export function createClipboardManager(editor: Editor): void { // Subscribe to process backend event @@ -8,3 +9,20 @@ export function createClipboardManager(editor: Editor): void { navigator.clipboard?.writeText?.(triggerTextCopy.copyText); }); } + +export async function copyToClipboardFileURL(url: string): Promise { + const response = await fetch(url); + const blob = await response.blob(); + + // TODO: Remove this if/when we end up returning PNG directly from the backend + const pngBlob = await imageToPNG(blob); + + const clipboardItem: Record = {}; + clipboardItem[pngBlob.type] = pngBlob; + const data = [new ClipboardItem(clipboardItem)]; + + // Note: if this image has transparency, it will be lost and appear as black due to limitations of the way browsers handle copying transparent images + // This even happens if you just open a regular transparent PNG file in a browser tab, right click > copy, and paste it somewhere (the transparency will show up as black) + // This is true, at least, on Windows (it's worth checking on other OSs though) + navigator.clipboard.write(data); +} diff --git a/frontend/src/state-providers/portfolio.ts b/frontend/src/state-providers/portfolio.ts index 9948aa97..a3961541 100644 --- a/frontend/src/state-providers/portfolio.ts +++ b/frontend/src/state-providers/portfolio.ts @@ -2,25 +2,28 @@ import {writable} from "svelte/store"; -import { downloadFileText, downloadFileBlob, upload } from "@graphite/utility-functions/files"; +import { downloadFileText, downloadFileBlob, upload, downloadFileURL } from "@graphite/utility-functions/files"; import { imaginateGenerate, imaginateCheckConnection, imaginateTerminate, updateBackendImage } from "@graphite/utility-functions/imaginate"; -import { extractPixelData, rasterizeSVG, rasterizeSVGCanvas } from "@graphite/utility-functions/rasterization"; +import { extractPixelData, imageToPNG, rasterizeSVG, rasterizeSVGCanvas } from "@graphite/utility-functions/rasterization"; import { type Editor } from "@graphite/wasm-communication/editor"; import { type FrontendDocumentDetails, - TriggerFileDownload, - TriggerImport, - TriggerOpenDocument, - TriggerRasterDownload, + TriggerCopyToClipboardBlobUrl, + TriggerDownloadBlobUrl, + TriggerDownloadRaster, + TriggerDownloadTextFile, + TriggerImaginateCheckServerStatus, TriggerImaginateGenerate, TriggerImaginateTerminate, - TriggerImaginateCheckServerStatus, + TriggerImport, + TriggerOpenDocument, TriggerRasterizeRegionBelowLayer, - UpdateActiveDocument, - UpdateOpenDocumentsList, - UpdateImageData, TriggerRevokeBlobUrl, + UpdateActiveDocument, + UpdateImageData, + UpdateOpenDocumentsList, } from "@graphite/wasm-communication/messages"; +import { copyToClipboardFileURL } from "~src/io-managers/clipboard"; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function createPortfolioState(editor: Editor) { @@ -55,10 +58,22 @@ export function createPortfolioState(editor: Editor) { const imageData = await extractPixelData(new Blob([data.content], { type: data.type })); editor.instance.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height); }); - editor.subscriptions.subscribeJsMessage(TriggerFileDownload, (triggerFileDownload) => { + editor.subscriptions.subscribeJsMessage(TriggerDownloadTextFile, (triggerFileDownload) => { downloadFileText(triggerFileDownload.name, triggerFileDownload.document); }); - editor.subscriptions.subscribeJsMessage(TriggerRasterDownload, async (triggerRasterDownload) => { + editor.subscriptions.subscribeJsMessage(TriggerDownloadBlobUrl, async (triggerDownloadBlobUrl) => { + const data = await fetch(triggerDownloadBlobUrl.blobUrl); + const blob = await data.blob(); + + // TODO: Remove this if/when we end up returning PNG directly from the backend + const pngBlob = await imageToPNG(blob); + + downloadFileBlob(triggerDownloadBlobUrl.layerName, pngBlob); + }); + editor.subscriptions.subscribeJsMessage(TriggerCopyToClipboardBlobUrl, (triggerDownloadBlobUrl) => { + copyToClipboardFileURL(triggerDownloadBlobUrl.blobUrl); + }); + editor.subscriptions.subscribeJsMessage(TriggerDownloadRaster, async (triggerRasterDownload) => { const { svg, name, mime, size } = triggerRasterDownload; // Fill the canvas with white if it'll be a JPEG (which does not support transparency and defaults to black) diff --git a/frontend/src/utility-functions/rasterization.ts b/frontend/src/utility-functions/rasterization.ts index 7f803b05..29d02c94 100644 --- a/frontend/src/utility-functions/rasterization.ts +++ b/frontend/src/utility-functions/rasterization.ts @@ -56,6 +56,26 @@ export async function rasterizeSVG(svg: string, width: number, height: number, m /// Convert an image source (e.g. PNG document) into pixel data, a width, and a height export async function extractPixelData(imageData: ImageBitmapSource): Promise { + const canvasContext = await imageToCanvasContext(imageData); + const width = canvasContext.canvas.width; + const height = canvasContext.canvas.height; + + return canvasContext.getImageData(0, 0, width, height); +} + +/// Convert an image source (e.g. BMP document) into a PNG blob +export async function imageToPNG(imageData: ImageBitmapSource): Promise { + const canvasContext = await imageToCanvasContext(imageData); + + return new Promise((resolve, reject) => { + canvasContext.canvas.toBlob((pngBlob) => { + if (pngBlob) resolve(pngBlob); + else reject("Converting canvas to blob data failed in imageToPNG()"); + }, "image/png"); + }); +} + +export async function imageToCanvasContext(imageData: ImageBitmapSource): Promise { // Special handling to rasterize an SVG file let svgImageData; if (imageData instanceof File && imageData.type === "image/svg+xml") { @@ -92,5 +112,5 @@ export async function extractPixelData(imageData: ImageBitmapSource): Promise = { DisplayEditableTextboxTransform, DisplayRemoveEditableTextbox, TriggerAboutGraphiteLocalizedCommitDate, + TriggerCopyToClipboardBlobUrl, + TriggerDownloadBlobUrl, + TriggerDownloadRaster, + TriggerDownloadTextFile, + TriggerFontLoad, TriggerImaginateCheckServerStatus, TriggerImaginateGenerate, TriggerImaginateTerminate, - TriggerRasterizeRegionBelowLayer, - TriggerFileDownload, - TriggerFontLoad, TriggerImport, TriggerIndexedDbRemoveDocument, TriggerIndexedDbWriteDocument, @@ -1397,7 +1409,7 @@ export const messageMakers: Record = { TriggerLoadPreferences, TriggerOpenDocument, TriggerPaste, - TriggerRasterDownload, + TriggerRasterizeRegionBelowLayer, TriggerRefreshBoundsOfViewports, TriggerRevokeBlobUrl, TriggerSavePreferences, @@ -1415,8 +1427,8 @@ export const messageMakers: Record = { UpdateDocumentModeLayout, UpdateDocumentOverlays, UpdateDocumentRulers, - UpdateEyedropperSamplingState, UpdateDocumentScrollbars, + UpdateEyedropperSamplingState, UpdateImageData, UpdateInputHints, UpdateLayerTreeOptionsLayout, @@ -1429,9 +1441,9 @@ export const messageMakers: Record = { UpdateOpenDocumentsList, UpdatePropertyPanelOptionsLayout, UpdatePropertyPanelSectionsLayout, - UpdateZoomWithScroll, UpdateToolOptionsLayout, UpdateToolShelfLayout, UpdateWorkingColorsLayout, + UpdateZoomWithScroll, } as const; export type JsMessageType = keyof typeof messageMakers;