From f8790a02b1e869819a877079c45e6b270f0bd16f Mon Sep 17 00:00:00 2001 From: mtvare6 Date: Fri, 18 Jul 2025 08:02:04 +0530 Subject: [PATCH 01/21] add: move images as rendered canvases to node_graph_executor --- editor/src/node_graph_executor.rs | 47 ++++++++- node-graph/gcore/src/raster.rs | 1 + node-graph/gcore/src/raster/image.rs | 8 ++ node-graph/graph-craft/src/document/value.rs | 48 ++++++++- .../graph-craft/src/wasm_application_io.rs | 21 ++++ node-graph/gstd/src/wasm_application_io.rs | 43 ++------ node-graph/gsvg-renderer/src/renderer.rs | 99 +++++++++++++------ 7 files changed, 197 insertions(+), 70 deletions(-) diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 8c6a7d13a3..83a178f9d7 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -213,7 +213,7 @@ impl NodeGraphExecutor { fn export(&self, node_graph_output: TaggedValue, export_config: ExportConfig, responses: &mut VecDeque) -> Result<(), String> { let TaggedValue::RenderOutput(RenderOutput { - data: graphene_std::wasm_application_io::RenderOutputType::Svg(svg), + data: graphene_std::wasm_application_io::RenderOutputType::Svg { svg, .. }, .. }) = node_graph_output else { @@ -350,7 +350,7 @@ impl NodeGraphExecutor { match node_graph_output { TaggedValue::RenderOutput(render_output) => { match render_output.data { - graphene_std::wasm_application_io::RenderOutputType::Svg(svg) => { + graphene_std::wasm_application_io::RenderOutputType::Svg { svg, image_data, canvas } => { // Send to frontend responses.add(FrontendMessage::UpdateDocumentArtwork { svg }); } @@ -393,6 +393,49 @@ impl NodeGraphExecutor { } } +#[cfg(target_arch = "wasm32")] +use graph_craft::wasm_application_io::WasmSurfaceHandle; +#[cfg(target_arch = "wasm32")] +use graphene_std::Color; +#[cfg(target_arch = "wasm32")] +use graphene_std::application_io; +#[cfg(target_arch = "wasm32")] +use graphene_std::raster::{Image, TransformImage}; +#[cfg(target_arch = "wasm32")] +use std::sync::Arc; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::Clamped; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::JsCast; +#[cfg(target_arch = "wasm32")] +use web_sys::CanvasRenderingContext2d; +#[cfg(target_arch = "wasm32")] +use web_sys::HtmlCanvasElement; + +#[cfg(target_arch = "wasm32")] +use bytemuck; + +#[cfg(target_arch = "wasm32")] +async fn draw_image_frame(images: Vec<(u64, Image, TransformImage)>, surface_handle: Arc) -> Vec> { + let mut canves = Vec::new(); + for (id, image, transform) in images { + let image_data = image.data; + let array: Clamped<&[u8]> = Clamped(bytemuck::cast_slice(image_data.as_slice())); + if image.width > 0 && image.height > 0 { + let canvas = &surface_handle.surface; + canvas.set_width(image.width); + canvas.set_height(image.height); + let context = canvas.get_context("2d").unwrap().unwrap().dyn_into::().unwrap(); + let image_data = web_sys::ImageData::new_with_u8_clamped_array_and_sh(array, image.width, image.height).expect("Failed to construct ImageData"); + context.put_image_data(&image_data, 0., 0.).unwrap(); + } + let transform = transform.0; + let canvas = application_io::SurfaceHandleFrame { surface_handle, transform }; + canves.push(canvas); + } + canves +} + // Re-export for usage by tests in other modules #[cfg(test)] pub use test::Instrumented; diff --git a/node-graph/gcore/src/raster.rs b/node-graph/gcore/src/raster.rs index 60106bdc70..d38fbf467b 100644 --- a/node-graph/gcore/src/raster.rs +++ b/node-graph/gcore/src/raster.rs @@ -15,6 +15,7 @@ pub mod color { pub mod image; pub use self::image::Image; +pub use self::image::TransformImage; pub trait Bitmap { type Pixel: Pixel; diff --git a/node-graph/gcore/src/raster/image.rs b/node-graph/gcore/src/raster/image.rs index e93fe60ba2..8916954e4d 100644 --- a/node-graph/gcore/src/raster/image.rs +++ b/node-graph/gcore/src/raster/image.rs @@ -50,6 +50,14 @@ pub struct Image { // TODO: Currently it is always anchored at the top left corner at (0, 0). The bottom right corner of the new origin field would correspond to (1, 1). } +#[derive(Debug, Clone, dyn_any::DynAny, Default, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct TransformImage(pub DAffine2); + +impl Hash for TransformImage { + fn hash(&self, _: &mut H) {} +} + + impl Debug for Image

{ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let length = self.data.len(); diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index c8c896290c..a162ab72d1 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -1,12 +1,13 @@ use super::DocumentNode; use crate::proto::{Any as DAny, FutureAny}; -use crate::wasm_application_io::WasmEditorApi; +use crate::wasm_application_io::{WasmCanvas, WasmEditorApi}; use dyn_any::DynAny; pub use dyn_any::StaticType; pub use glam::{DAffine2, DVec2, IVec2, UVec2}; use graphene_application_io::SurfaceFrame; use graphene_brush::brush_cache::BrushCache; use graphene_brush::brush_stroke::BrushStroke; +use graphene_core::raster::{Image, TransformImage}; use graphene_core::raster_types::CPU; use graphene_core::transform::ReferencePoint; use graphene_core::uuid::NodeId; @@ -425,13 +426,54 @@ pub struct RenderOutput { pub metadata: RenderMetadata, } -#[derive(Debug, Clone, PartialEq, dyn_any::DynAny, Hash, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Hash, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize)] pub enum RenderOutputType { CanvasFrame(SurfaceFrame), - Svg(String), + Svg { + svg: String, + image_data: Vec<(u64, Image, TransformImage)>, + #[serde(skip)] + canvas: WasmCanvas, + }, Image(Vec), } +// impl Hash for RenderOutputType { +// fn hash(&self, state: &mut H) { +// std::mem::discriminant(self).hash(state); +// match self { +// RenderOutputType::CanvasFrame(frame) => { +// frame.hash(state); +// } +// RenderOutputType::Svg { svg, image_data, .. } => { +// svg.hash(state); +// image_data.hash(state); +// } +// RenderOutputType::Image(data) => { +// data.hash(state); +// } +// } +// } +// } +// +// impl PartialEq for RenderOutputType { +// fn eq(&self, other: &Self) -> bool { +// match (self, other) { +// (Self::CanvasFrame(l0), Self::CanvasFrame(r0)) => l0 == r0, +// ( +// Self::Svg { +// svg: l_svg, image_data: l_image_data, .. +// }, +// Self::Svg { +// svg: r_svg, image_data: r_image_data, .. +// }, +// ) => l_svg == r_svg && l_image_data == r_image_data, +// (Self::Image(l0), Self::Image(r0)) => l0 == r0, +// _ => false, +// } +// } +// } + impl Hash for RenderOutput { fn hash(&self, state: &mut H) { self.data.hash(state) diff --git a/node-graph/graph-craft/src/wasm_application_io.rs b/node-graph/graph-craft/src/wasm_application_io.rs index 1d11744aa4..f89fd4e7cc 100644 --- a/node-graph/graph-craft/src/wasm_application_io.rs +++ b/node-graph/graph-craft/src/wasm_application_io.rs @@ -3,6 +3,7 @@ use graphene_application_io::{ApplicationError, ApplicationIo, ResourceFuture, S #[cfg(target_arch = "wasm32")] use js_sys::{Object, Reflect}; use std::collections::HashMap; +use std::hash::Hash; use std::sync::Arc; #[cfg(target_arch = "wasm32")] use std::sync::atomic::AtomicU64; @@ -313,13 +314,33 @@ impl ApplicationIo for WasmApplicationIo { #[cfg(feature = "wgpu")] pub type WasmSurfaceHandle = SurfaceHandle; #[cfg(feature = "wgpu")] +#[derive(Debug, Clone, dyn_any::DynAny, Default)] +pub struct WasmCanvas(pub Option>); +#[cfg(feature = "wgpu")] pub type WasmSurfaceHandleFrame = graphene_application_io::SurfaceHandleFrame; +#[cfg(feature = "wgpu")] +unsafe impl Send for WasmCanvas {} +#[cfg(feature = "wgpu")] +unsafe impl Sync for WasmCanvas {} + #[derive(Clone, Debug, PartialEq, Hash, specta::Type, serde::Serialize, serde::Deserialize)] pub struct EditorPreferences { pub use_vello: bool, } +#[cfg(feature = "wgpu")] +impl Hash for WasmCanvas { + fn hash(&self, _: &mut H) {} +} + +#[cfg(feature = "wgpu")] +impl PartialEq for WasmCanvas { + fn eq(&self, _: &Self) -> bool { + true + } +} + impl graphene_application_io::GetEditorPreferences for EditorPreferences { fn use_vello(&self) -> bool { self.use_vello diff --git a/node-graph/gstd/src/wasm_application_io.rs b/node-graph/gstd/src/wasm_application_io.rs index ae03edd425..fb9a901c0f 100644 --- a/node-graph/gstd/src/wasm_application_io.rs +++ b/node-graph/gstd/src/wasm_application_io.rs @@ -30,35 +30,6 @@ async fn create_surface<'a: 'n>(_: impl Ctx, editor: &'a WasmEditorApi) -> Arc, -// surface_handle: Arc, -// ) -> graphene_core::application_io::SurfaceHandleFrame { -// let image = image.instance_ref_iter().next().unwrap().instance; -// let image_data = image.image.data; -// let array: Clamped<&[u8]> = Clamped(bytemuck::cast_slice(image_data.as_slice())); -// if image.image.width > 0 && image.image.height > 0 { -// let canvas = &surface_handle.surface; -// canvas.set_width(image.image.width); -// canvas.set_height(image.image.height); -// // TODO: replace "2d" with "bitmaprenderer" once we switch to ImageBitmap (lives on gpu) from RasterData (lives on cpu) -// let context = canvas.get_context("2d").unwrap().unwrap().dyn_into::().unwrap(); -// let image_data = web_sys::ImageData::new_with_u8_clamped_array_and_sh(array, image.image.width, image.image.height).expect("Failed to construct RasterData"); -// context.put_image_data(&image_data, 0., 0.).unwrap(); -// } -// graphene_core::application_io::SurfaceHandleFrame { -// surface_handle, -// transform: image.transform, -// } -// } - #[node_macro::node(category("Web Request"))] async fn load_resource<'a: 'n>(_: impl Ctx, _primary: (), #[scope("editor-api")] editor: &'a WasmEditorApi, #[name("URL")] url: String) -> Arc<[u8]> { let Some(api) = editor.application_io.as_ref() else { @@ -93,7 +64,7 @@ fn decode_image(_: impl Ctx, data: Arc<[u8]>) -> RasterDataTable { RasterDataTable::new(Raster::new_cpu(image)) } -fn render_svg(data: impl GraphicElementRendered, mut render: SvgRender, render_params: RenderParams, footprint: Footprint) -> RenderOutputType { +fn render_svg(data: impl GraphicElementRendered, mut render: SvgRender, render_params: RenderParams, footprint: Footprint, editor: &WasmEditorApi) -> RenderOutputType { if !data.contains_artboard() && !render_params.hide_artboards { render.leaf_tag("rect", |attributes| { attributes.push("x", "0"); @@ -112,7 +83,11 @@ fn render_svg(data: impl GraphicElementRendered, mut render: SvgRender, render_p render.wrap_with_transform(footprint.transform, Some(footprint.resolution.as_dvec2())); - RenderOutputType::Svg(render.svg.to_svg_string()) + let svg = render.svg.to_svg_string(); + let image_data = render.image_data; + let canvas = (!image_data.is_empty()).then_some(Arc::new(editor.application_io.as_ref().unwrap().create_window())); + let canvas = WasmCanvas(canvas); + RenderOutputType::Svg { svg, image_data, canvas } } #[cfg(feature = "vello")] @@ -282,7 +257,7 @@ async fn render<'a: 'n, T: 'n + GraphicElementRendered + WasmNotSend>( let output_format = render_config.export_format; let data = match output_format { - ExportFormat::Svg => render_svg(data, SvgRender::new(), render_params, footprint), + ExportFormat::Svg => render_svg(data, SvgRender::new(), render_params, footprint, editor_api), ExportFormat::Canvas => { if use_vello && editor_api.application_io.as_ref().unwrap().gpu_executor().is_some() { #[cfg(all(feature = "vello", not(test)))] @@ -291,9 +266,9 @@ async fn render<'a: 'n, T: 'n + GraphicElementRendered + WasmNotSend>( metadata, }; #[cfg(any(not(feature = "vello"), test))] - render_svg(data, SvgRender::new(), render_params, footprint) + render_svg(data, SvgRender::new(), render_params, footprint, editor_api) } else { - render_svg(data, SvgRender::new(), render_params, footprint) + render_svg(data, SvgRender::new(), render_params, footprint, editor_api) } } _ => todo!("Non-SVG render output for {output_format:?}"), diff --git a/node-graph/gsvg-renderer/src/renderer.rs b/node-graph/gsvg-renderer/src/renderer.rs index 216a9b666f..4da5dd61b4 100644 --- a/node-graph/gsvg-renderer/src/renderer.rs +++ b/node-graph/gsvg-renderer/src/renderer.rs @@ -8,7 +8,7 @@ use graphene_core::bounds::BoundingBox; use graphene_core::color::Color; use graphene_core::instances::Instance; use graphene_core::math::quad::Quad; -use graphene_core::raster::Image; +use graphene_core::raster::{Image, TransformImage}; use graphene_core::raster_types::{CPU, GPU, RasterDataTable}; use graphene_core::render_complexity::RenderComplexity; use graphene_core::transform::{Footprint, Transform}; @@ -51,7 +51,7 @@ pub struct SvgRender { pub svg: Vec, pub svg_defs: String, pub transform: DAffine2, - pub image_data: Vec<(u64, Image)>, + pub image_data: Vec<(u64, Image, TransformImage)>, indent: usize, } @@ -173,6 +173,10 @@ impl RenderParams { let alignment_parent_transform = Some(transform); Self { alignment_parent_transform, ..*self } } + + pub fn to_canvas(&self) -> bool { + !self.for_export && !self.thumbnail + } } pub fn format_transform_matrix(transform: DAffine2) -> String { @@ -937,40 +941,73 @@ impl GraphicElementRendered for RasterDataTable { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { for instance in self.instance_ref_iter() { let transform = *instance.transform; - let image = &instance.instance; + if image.data.is_empty() { - return; + continue; } - let base64_string = image.base64_string.clone().unwrap_or_else(|| { - use base64::Engine; + if render_params.to_canvas() { + let id = generate_uuid(); + render.image_data.push((id, image.data().clone(), TransformImage(transform))); + render.parent_tag( + "foreignObject", + |attributes| { + attributes.push("width", "1"); + attributes.push("height", "1"); + + let matrix = format_transform_matrix(transform); + if !matrix.is_empty() { + attributes.push("transform", matrix); + } - let output = image.to_png(); - let preamble = "data:image/png;base64,"; - let mut base64_string = String::with_capacity(preamble.len() + output.len() * 4); - base64_string.push_str(preamble); - base64::engine::general_purpose::STANDARD.encode_string(output, &mut base64_string); - base64_string - }); - render.leaf_tag("image", |attributes| { - attributes.push("width", 1.to_string()); - attributes.push("height", 1.to_string()); - attributes.push("preserveAspectRatio", "none"); - attributes.push("href", base64_string); - let matrix = format_transform_matrix(transform); - if !matrix.is_empty() { - attributes.push("transform", matrix); - } - let factor = if render_params.for_mask { 1. } else { instance.alpha_blending.fill }; - let opacity = instance.alpha_blending.opacity * factor; - if opacity < 1. { - attributes.push("opacity", opacity.to_string()); - } - if instance.alpha_blending.blend_mode != BlendMode::default() { - attributes.push("style", instance.alpha_blending.blend_mode.render()); - } - }); + let factor = if render_params.for_mask { 1. } else { instance.alpha_blending.fill }; + let opacity = instance.alpha_blending.opacity * factor; + if opacity < 1. { + attributes.push("opacity", opacity.to_string()); + } + if instance.alpha_blending.blend_mode != BlendMode::default() { + attributes.push("style", instance.alpha_blending.blend_mode.render()); + } + }, + |render| { + render.leaf_tag("div", |attributes| { + attributes.push("data-canvas-placeholder", format!("canvas{}", id)); + attributes.push("style", "width: 100%; height: 100%;".to_string()); + }) + }, + ); + } else { + let base64_string = image.base64_string.clone().unwrap_or_else(|| { + use base64::Engine; + + let output = image.to_png(); + let preamble = "data:image/png;base64,"; + let mut base64_string = String::with_capacity(preamble.len() + output.len() * 4); + base64_string.push_str(preamble); + base64::engine::general_purpose::STANDARD.encode_string(output, &mut base64_string); + base64_string + }); + + render.leaf_tag("image", |attributes| { + attributes.push("width", "1"); + attributes.push("height", "1"); + attributes.push("preserveAspectRatio", "none"); + attributes.push("href", base64_string); + let matrix = format_transform_matrix(transform); + if !matrix.is_empty() { + attributes.push("transform", matrix); + } + let factor = if render_params.for_mask { 1. } else { instance.alpha_blending.fill }; + let opacity = instance.alpha_blending.opacity * factor; + if opacity < 1. { + attributes.push("opacity", opacity.to_string()); + } + if instance.alpha_blending.blend_mode != BlendMode::default() { + attributes.push("style", instance.alpha_blending.blend_mode.render()); + } + }); + } } } From 6a9724a789e69de3486c766a230ea94d2b1caab3 Mon Sep 17 00:00:00 2001 From: mtvare6 Date: Fri, 18 Jul 2025 10:00:48 +0530 Subject: [PATCH 02/21] add: added the frontend message --- Cargo.lock | 1 + editor/Cargo.toml | 1 + editor/src/messages/frontend/frontend_message.rs | 12 ++++++++++++ editor/src/messages/frontend/mod.rs | 2 +- editor/src/messages/prelude.rs | 1 + editor/src/node_graph_executor.rs | 15 ++++++++------- 6 files changed, 24 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5487e27d28..46f97214fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2439,6 +2439,7 @@ version = "0.0.0" dependencies = [ "bezier-rs", "bitflags 2.9.1", + "bytemuck", "derivative", "dyn-any", "env_logger", diff --git a/editor/Cargo.toml b/editor/Cargo.toml index 1f6af599ca..5b8dbd425b 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -46,6 +46,7 @@ num_enum = { workspace = true } usvg = { workspace = true } once_cell = { workspace = true } web-sys = { workspace = true } +bytemuck = { workspace = true } # Required dependencies spin = "0.9.8" diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index c24ebc405c..63d9ad2378 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -8,8 +8,16 @@ use crate::messages::portfolio::document::utility_types::wires::{WirePath, WireP use crate::messages::prelude::*; use crate::messages::tool::utility_types::HintData; use graph_craft::document::NodeId; +use graphene_std::application_io; use graphene_std::raster::color::Color; use graphene_std::text::Font; +use web_sys::HtmlCanvasElement; + +#[derive(Debug, Clone, dyn_any::DynAny, Default, PartialEq)] +pub struct FrontendHtmlCanvases(pub Vec>); + +unsafe impl Send for FrontendHtmlCanvases {} +unsafe impl Sync for FrontendHtmlCanvases {} #[impl_message(Message, Frontend)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] @@ -179,6 +187,10 @@ pub enum FrontendMessage { UpdateDocumentArtwork { svg: String, }, + UpdateCanvasImage { + #[serde(skip)] + canvases: FrontendHtmlCanvases, + }, UpdateDocumentBarLayout { #[serde(rename = "layoutTarget")] layout_target: LayoutTarget, diff --git a/editor/src/messages/frontend/mod.rs b/editor/src/messages/frontend/mod.rs index 81d963f56a..6fd58c38af 100644 --- a/editor/src/messages/frontend/mod.rs +++ b/editor/src/messages/frontend/mod.rs @@ -3,4 +3,4 @@ mod frontend_message; pub mod utility_types; #[doc(inline)] -pub use frontend_message::{FrontendMessage, FrontendMessageDiscriminant}; +pub use frontend_message::{FrontendMessage, FrontendMessageDiscriminant, FrontendHtmlCanvases}; diff --git a/editor/src/messages/prelude.rs b/editor/src/messages/prelude.rs index 72a6cb9ba7..070287303c 100644 --- a/editor/src/messages/prelude.rs +++ b/editor/src/messages/prelude.rs @@ -49,6 +49,7 @@ pub use crate::messages::tool::tool_messages::text_tool::{TextToolMessage, TextT // Helper pub use crate::messages::globals::global_variables::*; pub use crate::messages::portfolio::document::utility_types::misc::DocumentId; +pub use crate::messages::frontend::FrontendHtmlCanvases; pub use graphite_proc_macros::*; pub use std::collections::{HashMap, HashSet, VecDeque}; diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 83a178f9d7..5e258dfb2f 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -352,6 +352,11 @@ impl NodeGraphExecutor { match render_output.data { graphene_std::wasm_application_io::RenderOutputType::Svg { svg, image_data, canvas } => { // Send to frontend + #[cfg(target_arch = "wasm32")] + { + let canvases = draw_image_frame(image_data, canvas.0.unwrap()); + responses.add(FrontendMessage::UpdateCanvasImage { canvases }); + } responses.add(FrontendMessage::UpdateDocumentArtwork { svg }); } graphene_std::wasm_application_io::RenderOutputType::CanvasFrame(frame) => { @@ -409,14 +414,9 @@ use wasm_bindgen::Clamped; use wasm_bindgen::JsCast; #[cfg(target_arch = "wasm32")] use web_sys::CanvasRenderingContext2d; -#[cfg(target_arch = "wasm32")] -use web_sys::HtmlCanvasElement; - -#[cfg(target_arch = "wasm32")] -use bytemuck; #[cfg(target_arch = "wasm32")] -async fn draw_image_frame(images: Vec<(u64, Image, TransformImage)>, surface_handle: Arc) -> Vec> { +fn draw_image_frame(images: Vec<(u64, Image, TransformImage)>, surface_handle: Arc) -> FrontendHtmlCanvases { let mut canves = Vec::new(); for (id, image, transform) in images { let image_data = image.data; @@ -430,10 +430,11 @@ async fn draw_image_frame(images: Vec<(u64, Image, TransformImage)>, surf context.put_image_data(&image_data, 0., 0.).unwrap(); } let transform = transform.0; + let surface_handle = surface_handle.clone(); let canvas = application_io::SurfaceHandleFrame { surface_handle, transform }; canves.push(canvas); } - canves + FrontendHtmlCanvases(canves) } // Re-export for usage by tests in other modules From d05e650c23b2d35caf6d9505fa7c901972eb4309 Mon Sep 17 00:00:00 2001 From: mtvare6 Date: Fri, 18 Jul 2025 12:02:10 +0530 Subject: [PATCH 03/21] fix: bytemuck stuff --- editor/src/node_graph_executor.rs | 13 +++---- node-graph/graph-craft/src/document/value.rs | 36 -------------------- 2 files changed, 7 insertions(+), 42 deletions(-) diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 5e258dfb2f..d8f0d17978 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -353,7 +353,7 @@ impl NodeGraphExecutor { graphene_std::wasm_application_io::RenderOutputType::Svg { svg, image_data, canvas } => { // Send to frontend #[cfg(target_arch = "wasm32")] - { + if !image_data.is_empty() { let canvases = draw_image_frame(image_data, canvas.0.unwrap()); responses.add(FrontendMessage::UpdateCanvasImage { canvases }); } @@ -417,10 +417,10 @@ use web_sys::CanvasRenderingContext2d; #[cfg(target_arch = "wasm32")] fn draw_image_frame(images: Vec<(u64, Image, TransformImage)>, surface_handle: Arc) -> FrontendHtmlCanvases { - let mut canves = Vec::new(); + let mut canvases = Vec::new(); for (id, image, transform) in images { - let image_data = image.data; - let array: Clamped<&[u8]> = Clamped(bytemuck::cast_slice(image_data.as_slice())); + let image_data_rgba8: Vec = image.data.iter().flat_map(|color| color.to_rgba8_srgb()).collect(); + let array: Clamped<&[u8]> = Clamped(&image_data_rgba8); if image.width > 0 && image.height > 0 { let canvas = &surface_handle.surface; canvas.set_width(image.width); @@ -429,12 +429,13 @@ fn draw_image_frame(images: Vec<(u64, Image, TransformImage)>, surface_ha let image_data = web_sys::ImageData::new_with_u8_clamped_array_and_sh(array, image.width, image.height).expect("Failed to construct ImageData"); context.put_image_data(&image_data, 0., 0.).unwrap(); } + let transform = transform.0; let surface_handle = surface_handle.clone(); let canvas = application_io::SurfaceHandleFrame { surface_handle, transform }; - canves.push(canvas); + canvases.push(canvas); } - FrontendHtmlCanvases(canves) + FrontendHtmlCanvases(canvases) } // Re-export for usage by tests in other modules diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index a162ab72d1..a353a2b7f2 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -438,42 +438,6 @@ pub enum RenderOutputType { Image(Vec), } -// impl Hash for RenderOutputType { -// fn hash(&self, state: &mut H) { -// std::mem::discriminant(self).hash(state); -// match self { -// RenderOutputType::CanvasFrame(frame) => { -// frame.hash(state); -// } -// RenderOutputType::Svg { svg, image_data, .. } => { -// svg.hash(state); -// image_data.hash(state); -// } -// RenderOutputType::Image(data) => { -// data.hash(state); -// } -// } -// } -// } -// -// impl PartialEq for RenderOutputType { -// fn eq(&self, other: &Self) -> bool { -// match (self, other) { -// (Self::CanvasFrame(l0), Self::CanvasFrame(r0)) => l0 == r0, -// ( -// Self::Svg { -// svg: l_svg, image_data: l_image_data, .. -// }, -// Self::Svg { -// svg: r_svg, image_data: r_image_data, .. -// }, -// ) => l_svg == r_svg && l_image_data == r_image_data, -// (Self::Image(l0), Self::Image(r0)) => l0 == r0, -// _ => false, -// } -// } -// } - impl Hash for RenderOutput { fn hash(&self, state: &mut H) { self.data.hash(state) From acf0efd6046b6616212841cd7670db3ad4c24083 Mon Sep 17 00:00:00 2001 From: mtvare6 Date: Fri, 18 Jul 2025 16:09:27 +0530 Subject: [PATCH 04/21] fix: canvas element breaking --- editor/src/node_graph_executor.rs | 39 ++++++++++++++++++---- frontend/src/messages.ts | 55 ++++++++++++++++--------------- 2 files changed, 62 insertions(+), 32 deletions(-) diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index d8f0d17978..c3c38434c0 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -407,33 +407,60 @@ use graphene_std::application_io; #[cfg(target_arch = "wasm32")] use graphene_std::raster::{Image, TransformImage}; #[cfg(target_arch = "wasm32")] +use js_sys::{Object, Reflect}; +#[cfg(target_arch = "wasm32")] use std::sync::Arc; #[cfg(target_arch = "wasm32")] use wasm_bindgen::Clamped; #[cfg(target_arch = "wasm32")] use wasm_bindgen::JsCast; #[cfg(target_arch = "wasm32")] +use wasm_bindgen::JsValue; +#[cfg(target_arch = "wasm32")] use web_sys::CanvasRenderingContext2d; +#[cfg(target_arch = "wasm32")] +use web_sys::HtmlCanvasElement; #[cfg(target_arch = "wasm32")] fn draw_image_frame(images: Vec<(u64, Image, TransformImage)>, surface_handle: Arc) -> FrontendHtmlCanvases { let mut canvases = Vec::new(); + let window = web_sys::window().expect("No window object exists"); + let image_canvases_key = JsValue::from_str("imageCanvases"); + let image_canvases_obj = match Reflect::get(&window, &image_canvases_key) { + Ok(obj) if !obj.is_undefined() => obj.dyn_into::().expect("window.imageCanvases is not an object"), + _ => { + let new_obj = Object::new(); + Reflect::set(&window, &image_canvases_key, &new_obj).expect("Failed to set window.imageCanvases"); + new_obj + } + }; + for (id, image, transform) in images { let image_data_rgba8: Vec = image.data.iter().flat_map(|color| color.to_rgba8_srgb()).collect(); + let array: Clamped<&[u8]> = Clamped(&image_data_rgba8); if image.width > 0 && image.height > 0 { - let canvas = &surface_handle.surface; - canvas.set_width(image.width); - canvas.set_height(image.height); - let context = canvas.get_context("2d").unwrap().unwrap().dyn_into::().unwrap(); + let document = window.document().expect("should have a document on window"); + let canvas_element = document + .create_element("canvas") + .expect("failed to create canvas element") + .dyn_into::() + .expect("failed to cast to canvas element"); + + canvas_element.set_width(image.width); + canvas_element.set_height(image.height); + let context = canvas_element.get_context("2d").unwrap().unwrap().dyn_into::().unwrap(); + let image_data = web_sys::ImageData::new_with_u8_clamped_array_and_sh(array, image.width, image.height).expect("Failed to construct ImageData"); context.put_image_data(&image_data, 0., 0.).unwrap(); + let canvas_name = format!("canvas{}", id); + Reflect::set(&image_canvases_obj, &JsValue::from_str(&canvas_name), &canvas_element).expect("Failed to set canvas on imageCanvases object"); } let transform = transform.0; let surface_handle = surface_handle.clone(); - let canvas = application_io::SurfaceHandleFrame { surface_handle, transform }; - canvases.push(canvas); + let canvas_frame = application_io::SurfaceHandleFrame { surface_handle, transform }; + canvases.push(canvas_frame); } FrontendHtmlCanvases(canvases) } diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index 28b777f806..a560a05bda 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -109,7 +109,7 @@ export class UpdateNodeGraphWires extends JsMessage { readonly wires!: WireUpdate[]; } -export class ClearAllNodeGraphWires extends JsMessage {} +export class ClearAllNodeGraphWires extends JsMessage { } export class UpdateNodeGraphTransform extends JsMessage { readonly transform!: NodeGraphTransform; @@ -689,6 +689,8 @@ export class UpdateDocumentArtwork extends JsMessage { readonly svg!: string; } +export class UpdateCanvasImage extends JsMessage {} + export class UpdateDocumentScrollbars extends JsMessage { @TupleToVec2 readonly position!: XY; @@ -759,10 +761,10 @@ export class UpdateMouseCursor extends JsMessage { readonly cursor!: MouseCursorIcon; } -export class TriggerLoadFirstAutoSaveDocument extends JsMessage {} -export class TriggerLoadRestAutoSaveDocuments extends JsMessage {} +export class TriggerLoadFirstAutoSaveDocument extends JsMessage { } +export class TriggerLoadRestAutoSaveDocuments extends JsMessage { } -export class TriggerLoadPreferences extends JsMessage {} +export class TriggerLoadPreferences extends JsMessage { } export class TriggerFetchAndOpenDocument extends JsMessage { readonly name!: string; @@ -770,13 +772,13 @@ export class TriggerFetchAndOpenDocument extends JsMessage { readonly filename!: string; } -export class TriggerOpenDocument extends JsMessage {} +export class TriggerOpenDocument extends JsMessage { } -export class TriggerImport extends JsMessage {} +export class TriggerImport extends JsMessage { } -export class TriggerPaste extends JsMessage {} +export class TriggerPaste extends JsMessage { } -export class TriggerDelayedZoomCanvasToFitAll extends JsMessage {} +export class TriggerDelayedZoomCanvasToFitAll extends JsMessage { } export class TriggerDownloadImage extends JsMessage { readonly svg!: string; @@ -803,7 +805,7 @@ export class TriggerSaveActiveDocument extends JsMessage { readonly documentId!: bigint; } -export class DocumentChanged extends JsMessage {} +export class DocumentChanged extends JsMessage { } export type DataBuffer = { pointer: bigint; @@ -837,7 +839,7 @@ export class DisplayEditableTextboxTransform extends JsMessage { readonly transform!: number[]; } -export class DisplayRemoveEditableTextbox extends JsMessage {} +export class DisplayRemoveEditableTextbox extends JsMessage { } export class UpdateDocumentLayerDetails extends JsMessage { @Type(() => LayerPanelEntry) @@ -886,7 +888,7 @@ export class LayerPanelEntry { clippable!: boolean; } -export class DisplayDialogDismiss extends JsMessage {} +export class DisplayDialogDismiss extends JsMessage { } export class Font { fontFamily!: string; @@ -903,7 +905,7 @@ export class TriggerVisitLink extends JsMessage { url!: string; } -export class TriggerTextCommit extends JsMessage {} +export class TriggerTextCommit extends JsMessage { } export class TriggerTextCopy extends JsMessage { readonly copyText!: string; @@ -1558,21 +1560,21 @@ function createLayoutGroup(layoutGroup: any): LayoutGroup { } // WIDGET LAYOUTS -export class UpdateDialogButtons extends WidgetDiffUpdate {} +export class UpdateDialogButtons extends WidgetDiffUpdate { } -export class UpdateDialogColumn1 extends WidgetDiffUpdate {} +export class UpdateDialogColumn1 extends WidgetDiffUpdate { } -export class UpdateDialogColumn2 extends WidgetDiffUpdate {} +export class UpdateDialogColumn2 extends WidgetDiffUpdate { } -export class UpdateDocumentBarLayout extends WidgetDiffUpdate {} +export class UpdateDocumentBarLayout extends WidgetDiffUpdate { } -export class UpdateDocumentModeLayout extends WidgetDiffUpdate {} +export class UpdateDocumentModeLayout extends WidgetDiffUpdate { } -export class UpdateLayersPanelControlBarLeftLayout extends WidgetDiffUpdate {} +export class UpdateLayersPanelControlBarLeftLayout extends WidgetDiffUpdate { } -export class UpdateLayersPanelControlBarRightLayout extends WidgetDiffUpdate {} +export class UpdateLayersPanelControlBarRightLayout extends WidgetDiffUpdate { } -export class UpdateLayersPanelBottomBarLayout extends WidgetDiffUpdate {} +export class UpdateLayersPanelBottomBarLayout extends WidgetDiffUpdate { } // Extends JsMessage instead of WidgetDiffUpdate because the menu bar isn't diffed export class UpdateMenuBarLayout extends JsMessage { @@ -1584,17 +1586,17 @@ export class UpdateMenuBarLayout extends JsMessage { layout!: MenuBarEntry[]; } -export class UpdateNodeGraphControlBarLayout extends WidgetDiffUpdate {} +export class UpdateNodeGraphControlBarLayout extends WidgetDiffUpdate { } -export class UpdatePropertyPanelSectionsLayout extends WidgetDiffUpdate {} +export class UpdatePropertyPanelSectionsLayout extends WidgetDiffUpdate { } -export class UpdateSpreadsheetLayout extends WidgetDiffUpdate {} +export class UpdateSpreadsheetLayout extends WidgetDiffUpdate { } -export class UpdateToolOptionsLayout extends WidgetDiffUpdate {} +export class UpdateToolOptionsLayout extends WidgetDiffUpdate { } -export class UpdateToolShelfLayout extends WidgetDiffUpdate {} +export class UpdateToolShelfLayout extends WidgetDiffUpdate { } -export class UpdateWorkingColorsLayout extends WidgetDiffUpdate {} +export class UpdateWorkingColorsLayout extends WidgetDiffUpdate { } // eslint-disable-next-line @typescript-eslint/no-explicit-any function createMenuLayout(menuBarEntry: any[]): MenuBarEntry[] { @@ -1656,6 +1658,7 @@ export const messageMakers: Record = { UpdateDialogColumn1, UpdateDialogColumn2, UpdateDocumentArtwork, + UpdateCanvasImage, UpdateDocumentBarLayout, UpdateDocumentLayerDetails, UpdateDocumentLayerStructureJs, From bff06908221b2e5937de3dd78f9ba4a838327281 Mon Sep 17 00:00:00 2001 From: mtvare6 Date: Fri, 18 Jul 2025 16:28:49 +0530 Subject: [PATCH 05/21] fix: width issues --- editor/src/node_graph_executor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index c3c38434c0..a01cc0b765 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -447,8 +447,8 @@ fn draw_image_frame(images: Vec<(u64, Image, TransformImage)>, surface_ha .dyn_into::() .expect("failed to cast to canvas element"); - canvas_element.set_width(image.width); - canvas_element.set_height(image.height); + canvas_element.set_width(1); + canvas_element.set_height(1); let context = canvas_element.get_context("2d").unwrap().unwrap().dyn_into::().unwrap(); let image_data = web_sys::ImageData::new_with_u8_clamped_array_and_sh(array, image.width, image.height).expect("Failed to construct ImageData"); From f5e4f256a841820d37f31cb5c4a9a3babddd1eec Mon Sep 17 00:00:00 2001 From: mtvare6 Date: Sat, 19 Jul 2025 02:00:21 +0530 Subject: [PATCH 06/21] fix: remove the old message --- .../src/messages/frontend/frontend_message.rs | 15 +--- editor/src/messages/frontend/mod.rs | 2 +- editor/src/messages/prelude.rs | 9 +-- editor/src/node_graph_executor.rs | 75 +------------------ frontend/src/messages.ts | 3 - node-graph/gcore/src/raster/image.rs | 2 +- node-graph/graph-craft/src/document/value.rs | 9 +-- .../graph-craft/src/wasm_application_io.rs | 20 ----- node-graph/gstd/src/wasm_application_io.rs | 12 ++- 9 files changed, 16 insertions(+), 131 deletions(-) diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 63d9ad2378..cc722ffad7 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -8,16 +8,10 @@ use crate::messages::portfolio::document::utility_types::wires::{WirePath, WireP use crate::messages::prelude::*; use crate::messages::tool::utility_types::HintData; use graph_craft::document::NodeId; -use graphene_std::application_io; +use graphene_std::raster::Image; +use graphene_std::raster::TransformImage; use graphene_std::raster::color::Color; use graphene_std::text::Font; -use web_sys::HtmlCanvasElement; - -#[derive(Debug, Clone, dyn_any::DynAny, Default, PartialEq)] -pub struct FrontendHtmlCanvases(pub Vec>); - -unsafe impl Send for FrontendHtmlCanvases {} -unsafe impl Sync for FrontendHtmlCanvases {} #[impl_message(Message, Frontend)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] @@ -187,9 +181,8 @@ pub enum FrontendMessage { UpdateDocumentArtwork { svg: String, }, - UpdateCanvasImage { - #[serde(skip)] - canvases: FrontendHtmlCanvases, + UpdateImageData { + image_data: Vec<(u64, Image, TransformImage)>, }, UpdateDocumentBarLayout { #[serde(rename = "layoutTarget")] diff --git a/editor/src/messages/frontend/mod.rs b/editor/src/messages/frontend/mod.rs index 6fd58c38af..81d963f56a 100644 --- a/editor/src/messages/frontend/mod.rs +++ b/editor/src/messages/frontend/mod.rs @@ -3,4 +3,4 @@ mod frontend_message; pub mod utility_types; #[doc(inline)] -pub use frontend_message::{FrontendMessage, FrontendMessageDiscriminant, FrontendHtmlCanvases}; +pub use frontend_message::{FrontendMessage, FrontendMessageDiscriminant}; diff --git a/editor/src/messages/prelude.rs b/editor/src/messages/prelude.rs index 070287303c..5a57f48bd9 100644 --- a/editor/src/messages/prelude.rs +++ b/editor/src/messages/prelude.rs @@ -15,13 +15,7 @@ pub use crate::messages::input_mapper::key_mapping::{KeyMappingMessage, KeyMappi pub use crate::messages::input_mapper::{InputMapperMessage, InputMapperMessageContext, InputMapperMessageDiscriminant, InputMapperMessageHandler}; pub use crate::messages::input_preprocessor::{InputPreprocessorMessage, InputPreprocessorMessageContext, InputPreprocessorMessageDiscriminant, InputPreprocessorMessageHandler}; pub use crate::messages::layout::{LayoutMessage, LayoutMessageDiscriminant, LayoutMessageHandler}; -pub use crate::messages::portfolio::document::graph_operation::{GraphOperationMessage, GraphOperationMessageContext, GraphOperationMessageDiscriminant, GraphOperationMessageHandler}; -pub use crate::messages::portfolio::document::navigation::{NavigationMessage, NavigationMessageContext, NavigationMessageDiscriminant, NavigationMessageHandler}; -pub use crate::messages::portfolio::document::node_graph::{NodeGraphMessage, NodeGraphMessageDiscriminant, NodeGraphMessageHandler}; -pub use crate::messages::portfolio::document::overlays::{OverlaysMessage, OverlaysMessageContext, OverlaysMessageDiscriminant, OverlaysMessageHandler}; -pub use crate::messages::portfolio::document::properties_panel::{PropertiesPanelMessage, PropertiesPanelMessageDiscriminant, PropertiesPanelMessageHandler}; -pub use crate::messages::portfolio::document::{DocumentMessage, DocumentMessageContext, DocumentMessageDiscriminant, DocumentMessageHandler}; -pub use crate::messages::portfolio::menu_bar::{MenuBarMessage, MenuBarMessageDiscriminant, MenuBarMessageHandler}; +pub use crate::messages::portfolio::document::graph_operation::{GraphOperationMessage, GraphOperationMessageContext, GraphOperationMessageDiscriminant, GraphOperationMessageHandler}; pub use crate::messages::portfolio::document::navigation::{NavigationMessage, NavigationMessageContext, NavigationMessageDiscriminant, NavigationMessageHandler}; pub use crate::messages::portfolio::document::node_graph::{NodeGraphMessage, NodeGraphMessageDiscriminant, NodeGraphMessageHandler}; pub use crate::messages::portfolio::document::overlays::{OverlaysMessage, OverlaysMessageContext, OverlaysMessageDiscriminant, OverlaysMessageHandler}; pub use crate::messages::portfolio::document::properties_panel::{PropertiesPanelMessage, PropertiesPanelMessageDiscriminant, PropertiesPanelMessageHandler}; pub use crate::messages::portfolio::document::{DocumentMessage, DocumentMessageContext, DocumentMessageDiscriminant, DocumentMessageHandler}; pub use crate::messages::portfolio::menu_bar::{MenuBarMessage, MenuBarMessageDiscriminant, MenuBarMessageHandler}; pub use crate::messages::portfolio::spreadsheet::{SpreadsheetMessage, SpreadsheetMessageDiscriminant}; pub use crate::messages::portfolio::{PortfolioMessage, PortfolioMessageContext, PortfolioMessageDiscriminant, PortfolioMessageHandler}; pub use crate::messages::preferences::{PreferencesMessage, PreferencesMessageDiscriminant, PreferencesMessageHandler}; @@ -49,7 +43,6 @@ pub use crate::messages::tool::tool_messages::text_tool::{TextToolMessage, TextT // Helper pub use crate::messages::globals::global_variables::*; pub use crate::messages::portfolio::document::utility_types::misc::DocumentId; -pub use crate::messages::frontend::FrontendHtmlCanvases; pub use graphite_proc_macros::*; pub use std::collections::{HashMap, HashSet, VecDeque}; diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index a01cc0b765..a3067b436b 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -350,13 +350,9 @@ impl NodeGraphExecutor { match node_graph_output { TaggedValue::RenderOutput(render_output) => { match render_output.data { - graphene_std::wasm_application_io::RenderOutputType::Svg { svg, image_data, canvas } => { + graphene_std::wasm_application_io::RenderOutputType::Svg { svg, image_data } => { // Send to frontend - #[cfg(target_arch = "wasm32")] - if !image_data.is_empty() { - let canvases = draw_image_frame(image_data, canvas.0.unwrap()); - responses.add(FrontendMessage::UpdateCanvasImage { canvases }); - } + responses.add(FrontendMessage::UpdateImageData { image_data }); responses.add(FrontendMessage::UpdateDocumentArtwork { svg }); } graphene_std::wasm_application_io::RenderOutputType::CanvasFrame(frame) => { @@ -398,73 +394,6 @@ impl NodeGraphExecutor { } } -#[cfg(target_arch = "wasm32")] -use graph_craft::wasm_application_io::WasmSurfaceHandle; -#[cfg(target_arch = "wasm32")] -use graphene_std::Color; -#[cfg(target_arch = "wasm32")] -use graphene_std::application_io; -#[cfg(target_arch = "wasm32")] -use graphene_std::raster::{Image, TransformImage}; -#[cfg(target_arch = "wasm32")] -use js_sys::{Object, Reflect}; -#[cfg(target_arch = "wasm32")] -use std::sync::Arc; -#[cfg(target_arch = "wasm32")] -use wasm_bindgen::Clamped; -#[cfg(target_arch = "wasm32")] -use wasm_bindgen::JsCast; -#[cfg(target_arch = "wasm32")] -use wasm_bindgen::JsValue; -#[cfg(target_arch = "wasm32")] -use web_sys::CanvasRenderingContext2d; -#[cfg(target_arch = "wasm32")] -use web_sys::HtmlCanvasElement; - -#[cfg(target_arch = "wasm32")] -fn draw_image_frame(images: Vec<(u64, Image, TransformImage)>, surface_handle: Arc) -> FrontendHtmlCanvases { - let mut canvases = Vec::new(); - let window = web_sys::window().expect("No window object exists"); - let image_canvases_key = JsValue::from_str("imageCanvases"); - let image_canvases_obj = match Reflect::get(&window, &image_canvases_key) { - Ok(obj) if !obj.is_undefined() => obj.dyn_into::().expect("window.imageCanvases is not an object"), - _ => { - let new_obj = Object::new(); - Reflect::set(&window, &image_canvases_key, &new_obj).expect("Failed to set window.imageCanvases"); - new_obj - } - }; - - for (id, image, transform) in images { - let image_data_rgba8: Vec = image.data.iter().flat_map(|color| color.to_rgba8_srgb()).collect(); - - let array: Clamped<&[u8]> = Clamped(&image_data_rgba8); - if image.width > 0 && image.height > 0 { - let document = window.document().expect("should have a document on window"); - let canvas_element = document - .create_element("canvas") - .expect("failed to create canvas element") - .dyn_into::() - .expect("failed to cast to canvas element"); - - canvas_element.set_width(1); - canvas_element.set_height(1); - let context = canvas_element.get_context("2d").unwrap().unwrap().dyn_into::().unwrap(); - - let image_data = web_sys::ImageData::new_with_u8_clamped_array_and_sh(array, image.width, image.height).expect("Failed to construct ImageData"); - context.put_image_data(&image_data, 0., 0.).unwrap(); - let canvas_name = format!("canvas{}", id); - Reflect::set(&image_canvases_obj, &JsValue::from_str(&canvas_name), &canvas_element).expect("Failed to set canvas on imageCanvases object"); - } - - let transform = transform.0; - let surface_handle = surface_handle.clone(); - let canvas_frame = application_io::SurfaceHandleFrame { surface_handle, transform }; - canvases.push(canvas_frame); - } - FrontendHtmlCanvases(canvases) -} - // Re-export for usage by tests in other modules #[cfg(test)] pub use test::Instrumented; diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index a560a05bda..ac00d15a6d 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -689,8 +689,6 @@ export class UpdateDocumentArtwork extends JsMessage { readonly svg!: string; } -export class UpdateCanvasImage extends JsMessage {} - export class UpdateDocumentScrollbars extends JsMessage { @TupleToVec2 readonly position!: XY; @@ -1658,7 +1656,6 @@ export const messageMakers: Record = { UpdateDialogColumn1, UpdateDialogColumn2, UpdateDocumentArtwork, - UpdateCanvasImage, UpdateDocumentBarLayout, UpdateDocumentLayerDetails, UpdateDocumentLayerStructureJs, diff --git a/node-graph/gcore/src/raster/image.rs b/node-graph/gcore/src/raster/image.rs index 8916954e4d..92b49ee462 100644 --- a/node-graph/gcore/src/raster/image.rs +++ b/node-graph/gcore/src/raster/image.rs @@ -50,7 +50,7 @@ pub struct Image { // TODO: Currently it is always anchored at the top left corner at (0, 0). The bottom right corner of the new origin field would correspond to (1, 1). } -#[derive(Debug, Clone, dyn_any::DynAny, Default, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, dyn_any::DynAny, Default, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] pub struct TransformImage(pub DAffine2); impl Hash for TransformImage { diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index a353a2b7f2..4d04d9553b 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -1,6 +1,6 @@ use super::DocumentNode; use crate::proto::{Any as DAny, FutureAny}; -use crate::wasm_application_io::{WasmCanvas, WasmEditorApi}; +use crate::wasm_application_io::WasmEditorApi; use dyn_any::DynAny; pub use dyn_any::StaticType; pub use glam::{DAffine2, DVec2, IVec2, UVec2}; @@ -429,12 +429,7 @@ pub struct RenderOutput { #[derive(Debug, Clone, Hash, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize)] pub enum RenderOutputType { CanvasFrame(SurfaceFrame), - Svg { - svg: String, - image_data: Vec<(u64, Image, TransformImage)>, - #[serde(skip)] - canvas: WasmCanvas, - }, + Svg { svg: String, image_data: Vec<(u64, Image, TransformImage)> }, Image(Vec), } diff --git a/node-graph/graph-craft/src/wasm_application_io.rs b/node-graph/graph-craft/src/wasm_application_io.rs index f89fd4e7cc..f4398aef54 100644 --- a/node-graph/graph-craft/src/wasm_application_io.rs +++ b/node-graph/graph-craft/src/wasm_application_io.rs @@ -314,33 +314,13 @@ impl ApplicationIo for WasmApplicationIo { #[cfg(feature = "wgpu")] pub type WasmSurfaceHandle = SurfaceHandle; #[cfg(feature = "wgpu")] -#[derive(Debug, Clone, dyn_any::DynAny, Default)] -pub struct WasmCanvas(pub Option>); -#[cfg(feature = "wgpu")] pub type WasmSurfaceHandleFrame = graphene_application_io::SurfaceHandleFrame; -#[cfg(feature = "wgpu")] -unsafe impl Send for WasmCanvas {} -#[cfg(feature = "wgpu")] -unsafe impl Sync for WasmCanvas {} - #[derive(Clone, Debug, PartialEq, Hash, specta::Type, serde::Serialize, serde::Deserialize)] pub struct EditorPreferences { pub use_vello: bool, } -#[cfg(feature = "wgpu")] -impl Hash for WasmCanvas { - fn hash(&self, _: &mut H) {} -} - -#[cfg(feature = "wgpu")] -impl PartialEq for WasmCanvas { - fn eq(&self, _: &Self) -> bool { - true - } -} - impl graphene_application_io::GetEditorPreferences for EditorPreferences { fn use_vello(&self) -> bool { self.use_vello diff --git a/node-graph/gstd/src/wasm_application_io.rs b/node-graph/gstd/src/wasm_application_io.rs index fb9a901c0f..daa083041b 100644 --- a/node-graph/gstd/src/wasm_application_io.rs +++ b/node-graph/gstd/src/wasm_application_io.rs @@ -64,7 +64,7 @@ fn decode_image(_: impl Ctx, data: Arc<[u8]>) -> RasterDataTable { RasterDataTable::new(Raster::new_cpu(image)) } -fn render_svg(data: impl GraphicElementRendered, mut render: SvgRender, render_params: RenderParams, footprint: Footprint, editor: &WasmEditorApi) -> RenderOutputType { +fn render_svg(data: impl GraphicElementRendered, mut render: SvgRender, render_params: RenderParams, footprint: Footprint) -> RenderOutputType { if !data.contains_artboard() && !render_params.hide_artboards { render.leaf_tag("rect", |attributes| { attributes.push("x", "0"); @@ -85,9 +85,7 @@ fn render_svg(data: impl GraphicElementRendered, mut render: SvgRender, render_p let svg = render.svg.to_svg_string(); let image_data = render.image_data; - let canvas = (!image_data.is_empty()).then_some(Arc::new(editor.application_io.as_ref().unwrap().create_window())); - let canvas = WasmCanvas(canvas); - RenderOutputType::Svg { svg, image_data, canvas } + RenderOutputType::Svg { svg, image_data } } #[cfg(feature = "vello")] @@ -257,7 +255,7 @@ async fn render<'a: 'n, T: 'n + GraphicElementRendered + WasmNotSend>( let output_format = render_config.export_format; let data = match output_format { - ExportFormat::Svg => render_svg(data, SvgRender::new(), render_params, footprint, editor_api), + ExportFormat::Svg => render_svg(data, SvgRender::new(), render_params, footprint), ExportFormat::Canvas => { if use_vello && editor_api.application_io.as_ref().unwrap().gpu_executor().is_some() { #[cfg(all(feature = "vello", not(test)))] @@ -266,9 +264,9 @@ async fn render<'a: 'n, T: 'n + GraphicElementRendered + WasmNotSend>( metadata, }; #[cfg(any(not(feature = "vello"), test))] - render_svg(data, SvgRender::new(), render_params, footprint, editor_api) + render_svg(data, SvgRender::new(), render_params, footprint) } else { - render_svg(data, SvgRender::new(), render_params, footprint, editor_api) + render_svg(data, SvgRender::new(), render_params, footprint) } } _ => todo!("Non-SVG render output for {output_format:?}"), From 81467292f506dc16168f5899f77a36b1a958ad94 Mon Sep 17 00:00:00 2001 From: mtvare6 Date: Sat, 19 Jul 2025 02:54:10 +0530 Subject: [PATCH 07/21] npm: run lint-fix --- frontend/src/messages.ts | 52 ++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index ac00d15a6d..28b777f806 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -109,7 +109,7 @@ export class UpdateNodeGraphWires extends JsMessage { readonly wires!: WireUpdate[]; } -export class ClearAllNodeGraphWires extends JsMessage { } +export class ClearAllNodeGraphWires extends JsMessage {} export class UpdateNodeGraphTransform extends JsMessage { readonly transform!: NodeGraphTransform; @@ -759,10 +759,10 @@ export class UpdateMouseCursor extends JsMessage { readonly cursor!: MouseCursorIcon; } -export class TriggerLoadFirstAutoSaveDocument extends JsMessage { } -export class TriggerLoadRestAutoSaveDocuments extends JsMessage { } +export class TriggerLoadFirstAutoSaveDocument extends JsMessage {} +export class TriggerLoadRestAutoSaveDocuments extends JsMessage {} -export class TriggerLoadPreferences extends JsMessage { } +export class TriggerLoadPreferences extends JsMessage {} export class TriggerFetchAndOpenDocument extends JsMessage { readonly name!: string; @@ -770,13 +770,13 @@ export class TriggerFetchAndOpenDocument extends JsMessage { readonly filename!: string; } -export class TriggerOpenDocument extends JsMessage { } +export class TriggerOpenDocument extends JsMessage {} -export class TriggerImport extends JsMessage { } +export class TriggerImport extends JsMessage {} -export class TriggerPaste extends JsMessage { } +export class TriggerPaste extends JsMessage {} -export class TriggerDelayedZoomCanvasToFitAll extends JsMessage { } +export class TriggerDelayedZoomCanvasToFitAll extends JsMessage {} export class TriggerDownloadImage extends JsMessage { readonly svg!: string; @@ -803,7 +803,7 @@ export class TriggerSaveActiveDocument extends JsMessage { readonly documentId!: bigint; } -export class DocumentChanged extends JsMessage { } +export class DocumentChanged extends JsMessage {} export type DataBuffer = { pointer: bigint; @@ -837,7 +837,7 @@ export class DisplayEditableTextboxTransform extends JsMessage { readonly transform!: number[]; } -export class DisplayRemoveEditableTextbox extends JsMessage { } +export class DisplayRemoveEditableTextbox extends JsMessage {} export class UpdateDocumentLayerDetails extends JsMessage { @Type(() => LayerPanelEntry) @@ -886,7 +886,7 @@ export class LayerPanelEntry { clippable!: boolean; } -export class DisplayDialogDismiss extends JsMessage { } +export class DisplayDialogDismiss extends JsMessage {} export class Font { fontFamily!: string; @@ -903,7 +903,7 @@ export class TriggerVisitLink extends JsMessage { url!: string; } -export class TriggerTextCommit extends JsMessage { } +export class TriggerTextCommit extends JsMessage {} export class TriggerTextCopy extends JsMessage { readonly copyText!: string; @@ -1558,21 +1558,21 @@ function createLayoutGroup(layoutGroup: any): LayoutGroup { } // WIDGET LAYOUTS -export class UpdateDialogButtons extends WidgetDiffUpdate { } +export class UpdateDialogButtons extends WidgetDiffUpdate {} -export class UpdateDialogColumn1 extends WidgetDiffUpdate { } +export class UpdateDialogColumn1 extends WidgetDiffUpdate {} -export class UpdateDialogColumn2 extends WidgetDiffUpdate { } +export class UpdateDialogColumn2 extends WidgetDiffUpdate {} -export class UpdateDocumentBarLayout extends WidgetDiffUpdate { } +export class UpdateDocumentBarLayout extends WidgetDiffUpdate {} -export class UpdateDocumentModeLayout extends WidgetDiffUpdate { } +export class UpdateDocumentModeLayout extends WidgetDiffUpdate {} -export class UpdateLayersPanelControlBarLeftLayout extends WidgetDiffUpdate { } +export class UpdateLayersPanelControlBarLeftLayout extends WidgetDiffUpdate {} -export class UpdateLayersPanelControlBarRightLayout extends WidgetDiffUpdate { } +export class UpdateLayersPanelControlBarRightLayout extends WidgetDiffUpdate {} -export class UpdateLayersPanelBottomBarLayout extends WidgetDiffUpdate { } +export class UpdateLayersPanelBottomBarLayout extends WidgetDiffUpdate {} // Extends JsMessage instead of WidgetDiffUpdate because the menu bar isn't diffed export class UpdateMenuBarLayout extends JsMessage { @@ -1584,17 +1584,17 @@ export class UpdateMenuBarLayout extends JsMessage { layout!: MenuBarEntry[]; } -export class UpdateNodeGraphControlBarLayout extends WidgetDiffUpdate { } +export class UpdateNodeGraphControlBarLayout extends WidgetDiffUpdate {} -export class UpdatePropertyPanelSectionsLayout extends WidgetDiffUpdate { } +export class UpdatePropertyPanelSectionsLayout extends WidgetDiffUpdate {} -export class UpdateSpreadsheetLayout extends WidgetDiffUpdate { } +export class UpdateSpreadsheetLayout extends WidgetDiffUpdate {} -export class UpdateToolOptionsLayout extends WidgetDiffUpdate { } +export class UpdateToolOptionsLayout extends WidgetDiffUpdate {} -export class UpdateToolShelfLayout extends WidgetDiffUpdate { } +export class UpdateToolShelfLayout extends WidgetDiffUpdate {} -export class UpdateWorkingColorsLayout extends WidgetDiffUpdate { } +export class UpdateWorkingColorsLayout extends WidgetDiffUpdate {} // eslint-disable-next-line @typescript-eslint/no-explicit-any function createMenuLayout(menuBarEntry: any[]): MenuBarEntry[] { From 0d181460a91ef126c7b9a9b9d08ddb163e0f9df7 Mon Sep 17 00:00:00 2001 From: mtvare6 Date: Sat, 19 Jul 2025 06:59:58 +0530 Subject: [PATCH 08/21] fix --- frontend/wasm/src/editor_api.rs | 76 +++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index c8056e4efc..a52bcd5df3 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -17,12 +17,16 @@ use editor::messages::prelude::*; use editor::messages::tool::tool_messages::tool_prelude::WidgetId; use graph_craft::document::NodeId; use graphene_std::raster::color::Color; +use graphene_std::raster::{Image, TransformImage}; +use js_sys::{Object, Reflect}; use serde::Serialize; use serde_wasm_bindgen::{self, from_value}; use std::cell::RefCell; use std::sync::atomic::Ordering; use std::time::Duration; +use wasm_bindgen::JsCast; use wasm_bindgen::prelude::*; +use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, ImageData, window}; /// Set the random seed used by the editor by calling this from JS upon initialization. /// This is necessary because WASM doesn't have a random number generator. @@ -37,6 +41,73 @@ pub fn wasm_memory() -> JsValue { wasm_bindgen::memory() } +fn render_image_data_to_canvases(image_data: &[(u64, Image, TransformImage)]) { + let window = match window() { + Some(window) => window, + None => { + error!("Cannot render canvas: window object not found"); + return; + } + }; + let document = window.document().expect("window should have a document"); + let window_obj = Object::from(window); + let image_canvases_key = JsValue::from_str("imageCanvases"); + + let canvases_obj = match Reflect::get(&window_obj, &image_canvases_key) { + Ok(obj) if !obj.is_undefined() && !obj.is_null() => obj, + _ => { + let new_obj = Object::new(); + if Reflect::set(&window_obj, &image_canvases_key, &new_obj).is_err() { + error!("Failed to create and set imageCanvases object on window"); + return; + } + new_obj.into() + } + }; + let canvases_obj = Object::from(canvases_obj); + + for (placeholder_id, image, _) in image_data.iter() { + if image.width == 0 || image.height == 0 { + continue; + } + + let canvas: HtmlCanvasElement = document + .create_element("canvas") + .expect("Failed to create canvas element") + .dyn_into::() + .expect("Failed to cast element to HtmlCanvasElement"); + + canvas.set_width(1); + canvas.set_height(1); + let context: CanvasRenderingContext2d = canvas + .get_context("2d") + .expect("Failed to get 2d context") + .expect("2d context was not found") + .dyn_into::() + .expect("Failed to cast context to CanvasRenderingContext2d"); + let u8_data: Vec = image.data.iter().flat_map(|color| color.to_rgba8_srgb()).collect(); + let clamped_u8_data = wasm_bindgen::Clamped(&u8_data[..]); + match ImageData::new_with_u8_clamped_array_and_sh(clamped_u8_data, image.width, image.height) { + Ok(image_data_obj) => { + if context.put_image_data(&image_data_obj, 0.0, 0.0).is_err() { + error!("Failed to put image data on canvas for id: {}", placeholder_id); + } + } + Err(e) => { + error!("Failed to create ImageData for id: {}: {:?}", placeholder_id, e); + } + } + + let canvas_name = format!("canvas{}", placeholder_id); + let js_key = JsValue::from_str(&canvas_name); + let js_value = JsValue::from(canvas); + + if Reflect::set(&canvases_obj, &js_key, &js_value).is_err() { + error!("Failed to set canvas '{}' on imageCanvases object", canvas_name); + } + } +} + // ============================================================================ /// This struct is, via wasm-bindgen, used by JS to interact with the editor backend. It does this by calling functions, which are `impl`ed @@ -88,6 +159,11 @@ impl EditorHandle { // Sends a FrontendMessage to JavaScript fn send_frontend_message_to_js(&self, mut message: FrontendMessage) { + if let FrontendMessage::UpdateImageData { ref image_data } = message { + render_image_data_to_canvases(image_data.as_slice()); + return; + } + if let FrontendMessage::UpdateDocumentLayerStructure { data_buffer } = message { message = FrontendMessage::UpdateDocumentLayerStructureJs { data_buffer: data_buffer.into() }; } From 44f9b8fd67a63269c8510a2f6b9ad6bbf708b36e Mon Sep 17 00:00:00 2001 From: mtvare6 Date: Sat, 19 Jul 2025 09:43:47 +0530 Subject: [PATCH 09/21] works finally --- frontend/wasm/src/editor_api.rs | 4 ++-- node-graph/gsvg-renderer/src/renderer.rs | 16 +++++++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index a52bcd5df3..7c7676e355 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -77,8 +77,8 @@ fn render_image_data_to_canvases(image_data: &[(u64, Image, TransformImag .dyn_into::() .expect("Failed to cast element to HtmlCanvasElement"); - canvas.set_width(1); - canvas.set_height(1); + canvas.set_width(image.width); + canvas.set_height(image.height); let context: CanvasRenderingContext2d = canvas .get_context("2d") .expect("Failed to get 2d context") diff --git a/node-graph/gsvg-renderer/src/renderer.rs b/node-graph/gsvg-renderer/src/renderer.rs index 4da5dd61b4..18ab60c576 100644 --- a/node-graph/gsvg-renderer/src/renderer.rs +++ b/node-graph/gsvg-renderer/src/renderer.rs @@ -949,17 +949,24 @@ impl GraphicElementRendered for RasterDataTable { if render_params.to_canvas() { let id = generate_uuid(); + let scale = DVec2::new(transform.x_axis.x, transform.y_axis.y); render.image_data.push((id, image.data().clone(), TransformImage(transform))); + log::debug!("{transform} {} {}", image.width, image.height); render.parent_tag( "foreignObject", |attributes| { - attributes.push("width", "1"); - attributes.push("height", "1"); - - let matrix = format_transform_matrix(transform); + let size = DVec2::new(image.width as f64, image.height as f64); + let scale = scale / size; + let scale = DAffine2::from_scale(scale); + let matrix = format_transform_matrix(scale); if !matrix.is_empty() { attributes.push("transform", matrix); + attributes.push("transform-origin", "top-left"); } + attributes.push("width", transform.matrix2.x_axis.x.to_string()); + attributes.push("height", transform.matrix2.y_axis.y.to_string()); + attributes.push("x", transform.translation.x.to_string()); + attributes.push("y", transform.translation.y.to_string()); let factor = if render_params.for_mask { 1. } else { instance.alpha_blending.fill }; let opacity = instance.alpha_blending.opacity * factor; @@ -973,7 +980,6 @@ impl GraphicElementRendered for RasterDataTable { |render| { render.leaf_tag("div", |attributes| { attributes.push("data-canvas-placeholder", format!("canvas{}", id)); - attributes.push("style", "width: 100%; height: 100%;".to_string()); }) }, ); From 139f7030476772a05107e4d7149361c3cb6884c5 Mon Sep 17 00:00:00 2001 From: mtvare6 Date: Sun, 20 Jul 2025 14:49:55 +0530 Subject: [PATCH 10/21] fix transforms --- node-graph/gsvg-renderer/src/renderer.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/node-graph/gsvg-renderer/src/renderer.rs b/node-graph/gsvg-renderer/src/renderer.rs index 18ab60c576..762fee6897 100644 --- a/node-graph/gsvg-renderer/src/renderer.rs +++ b/node-graph/gsvg-renderer/src/renderer.rs @@ -949,24 +949,20 @@ impl GraphicElementRendered for RasterDataTable { if render_params.to_canvas() { let id = generate_uuid(); - let scale = DVec2::new(transform.x_axis.x, transform.y_axis.y); + let mut transform_values = transform.to_scale_angle_translation(); render.image_data.push((id, image.data().clone(), TransformImage(transform))); - log::debug!("{transform} {} {}", image.width, image.height); render.parent_tag( "foreignObject", |attributes| { let size = DVec2::new(image.width as f64, image.height as f64); - let scale = scale / size; - let scale = DAffine2::from_scale(scale); - let matrix = format_transform_matrix(scale); + transform_values.0 /= size; + let matrix = DAffine2::from_scale_angle_translation(transform_values.0, transform_values.1, transform_values.2); + let matrix = format_transform_matrix(matrix); if !matrix.is_empty() { attributes.push("transform", matrix); - attributes.push("transform-origin", "top-left"); } - attributes.push("width", transform.matrix2.x_axis.x.to_string()); - attributes.push("height", transform.matrix2.y_axis.y.to_string()); - attributes.push("x", transform.translation.x.to_string()); - attributes.push("y", transform.translation.y.to_string()); + attributes.push("width", size.x.to_string()); + attributes.push("height", size.y.to_string()); let factor = if render_params.for_mask { 1. } else { instance.alpha_blending.fill }; let opacity = instance.alpha_blending.opacity * factor; From 1d6970aab05409e189f88403be7375cd32ae9863 Mon Sep 17 00:00:00 2001 From: hypercube <0hypercube@gmail.com> Date: Sun, 20 Jul 2025 12:57:24 +0100 Subject: [PATCH 11/21] Fix self closing tag --- node-graph/gsvg-renderer/src/renderer.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/node-graph/gsvg-renderer/src/renderer.rs b/node-graph/gsvg-renderer/src/renderer.rs index 762fee6897..efa8570720 100644 --- a/node-graph/gsvg-renderer/src/renderer.rs +++ b/node-graph/gsvg-renderer/src/renderer.rs @@ -974,9 +974,12 @@ impl GraphicElementRendered for RasterDataTable { } }, |render| { - render.leaf_tag("div", |attributes| { - attributes.push("data-canvas-placeholder", format!("canvas{}", id)); - }) + render.leaf_tag( + "img", // Must be a self-closing tag + |attributes| { + attributes.push("data-canvas-placeholder", format!("canvas{}", id)); + }, + ) }, ); } else { From 88357bb6d2623edba13c95683cbe4a7357bd8081 Mon Sep 17 00:00:00 2001 From: mtvare6 Date: Wed, 23 Jul 2025 22:15:04 +0530 Subject: [PATCH 12/21] fix: reuse id --- .../src/messages/frontend/frontend_message.rs | 3 +-- frontend/wasm/src/editor_api.rs | 6 +++--- node-graph/graph-craft/src/document/value.rs | 4 ++-- node-graph/gsvg-renderer/src/renderer.rs | 17 ++++++++++++----- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index cc722ffad7..55fa2c71f1 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -9,7 +9,6 @@ use crate::messages::prelude::*; use crate::messages::tool::utility_types::HintData; use graph_craft::document::NodeId; use graphene_std::raster::Image; -use graphene_std::raster::TransformImage; use graphene_std::raster::color::Color; use graphene_std::text::Font; @@ -182,7 +181,7 @@ pub enum FrontendMessage { svg: String, }, UpdateImageData { - image_data: Vec<(u64, Image, TransformImage)>, + image_data: Vec<(u64, Image)>, }, UpdateDocumentBarLayout { #[serde(rename = "layoutTarget")] diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 7c7676e355..44e8949815 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -16,8 +16,8 @@ use editor::messages::portfolio::utility_types::Platform; use editor::messages::prelude::*; use editor::messages::tool::tool_messages::tool_prelude::WidgetId; use graph_craft::document::NodeId; +use graphene_std::raster::Image; use graphene_std::raster::color::Color; -use graphene_std::raster::{Image, TransformImage}; use js_sys::{Object, Reflect}; use serde::Serialize; use serde_wasm_bindgen::{self, from_value}; @@ -41,7 +41,7 @@ pub fn wasm_memory() -> JsValue { wasm_bindgen::memory() } -fn render_image_data_to_canvases(image_data: &[(u64, Image, TransformImage)]) { +fn render_image_data_to_canvases(image_data: &[(u64, Image)]) { let window = match window() { Some(window) => window, None => { @@ -66,7 +66,7 @@ fn render_image_data_to_canvases(image_data: &[(u64, Image, TransformImag }; let canvases_obj = Object::from(canvases_obj); - for (placeholder_id, image, _) in image_data.iter() { + for (placeholder_id, image) in image_data.iter() { if image.width == 0 || image.height == 0 { continue; } diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 4d04d9553b..363d033435 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -7,7 +7,7 @@ pub use glam::{DAffine2, DVec2, IVec2, UVec2}; use graphene_application_io::SurfaceFrame; use graphene_brush::brush_cache::BrushCache; use graphene_brush::brush_stroke::BrushStroke; -use graphene_core::raster::{Image, TransformImage}; +use graphene_core::raster::Image; use graphene_core::raster_types::CPU; use graphene_core::transform::ReferencePoint; use graphene_core::uuid::NodeId; @@ -429,7 +429,7 @@ pub struct RenderOutput { #[derive(Debug, Clone, Hash, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize)] pub enum RenderOutputType { CanvasFrame(SurfaceFrame), - Svg { svg: String, image_data: Vec<(u64, Image, TransformImage)> }, + Svg { svg: String, image_data: Vec<(u64, Image)> }, Image(Vec), } diff --git a/node-graph/gsvg-renderer/src/renderer.rs b/node-graph/gsvg-renderer/src/renderer.rs index efa8570720..a1e9208773 100644 --- a/node-graph/gsvg-renderer/src/renderer.rs +++ b/node-graph/gsvg-renderer/src/renderer.rs @@ -8,7 +8,7 @@ use graphene_core::bounds::BoundingBox; use graphene_core::color::Color; use graphene_core::instances::Instance; use graphene_core::math::quad::Quad; -use graphene_core::raster::{Image, TransformImage}; +use graphene_core::raster::Image; use graphene_core::raster_types::{CPU, GPU, RasterDataTable}; use graphene_core::render_complexity::RenderComplexity; use graphene_core::transform::{Footprint, Transform}; @@ -20,6 +20,7 @@ use graphene_core::{AlphaBlending, Artboard, ArtboardGroupTable, GraphicElement, use num_traits::Zero; use std::collections::{HashMap, HashSet}; use std::fmt::Write; +use std::hash::{DefaultHasher, Hash, Hasher}; #[cfg(feature = "vello")] use vello::*; @@ -51,7 +52,7 @@ pub struct SvgRender { pub svg: Vec, pub svg_defs: String, pub transform: DAffine2, - pub image_data: Vec<(u64, Image, TransformImage)>, + pub image_data: Vec<(u64, Image)>, indent: usize, } @@ -948,12 +949,18 @@ impl GraphicElementRendered for RasterDataTable { } if render_params.to_canvas() { - let id = generate_uuid(); - let mut transform_values = transform.to_scale_angle_translation(); - render.image_data.push((id, image.data().clone(), TransformImage(transform))); + let id = instance.source_node_id.map(|x| x.0).unwrap_or_else(|| { + let mut state = DefaultHasher::new(); + image.data().hash(&mut state); + state.finish() + }); + // if !render.image_data.iter().any(|(old_id, _)| *old_id == id) { + render.image_data.push((id, image.data().clone())); + // } render.parent_tag( "foreignObject", |attributes| { + let mut transform_values = transform.to_scale_angle_translation(); let size = DVec2::new(image.width as f64, image.height as f64); transform_values.0 /= size; let matrix = DAffine2::from_scale_angle_translation(transform_values.0, transform_values.1, transform_values.2); From 9488cfc4728296d0d4bf42e714fe3fb8342b41a5 Mon Sep 17 00:00:00 2001 From: mtvare6 Date: Thu, 24 Jul 2025 03:26:50 +0530 Subject: [PATCH 13/21] fix: have it working with repeat instance --- .../src/components/panels/Document.svelte | 17 +++++++++-- frontend/wasm/src/editor_api.rs | 28 +++++++++++++++---- node-graph/gsvg-renderer/src/renderer.rs | 4 +-- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 70d4ea99ba..37ad82f65b 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -192,12 +192,25 @@ const placeholders = window.document.querySelectorAll("[data-viewport] [data-canvas-placeholder]"); // Replace the placeholders with the actual canvas elements - placeholders.forEach((placeholder) => { + Array.from(placeholders).forEach((placeholder) => { const canvasName = placeholder.getAttribute("data-canvas-placeholder"); if (!canvasName) return; // Get the canvas element from the global storage // eslint-disable-next-line @typescript-eslint/no-explicit-any - const canvas = (window as any).imageCanvases[canvasName]; + let canvas = (window as any).imageCanvases[canvasName]; + + if (canvas.parentElement) { + var newCanvas = window.document.createElement("canvas"); + var context = newCanvas.getContext("2d"); + + newCanvas.width = canvas.width; + newCanvas.height = canvas.height; + + context?.drawImage(canvas, 0, 0); + + canvas = newCanvas; + } + placeholder.replaceWith(canvas); }); } diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 44e8949815..692e6c2423 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -22,12 +22,22 @@ use js_sys::{Object, Reflect}; use serde::Serialize; use serde_wasm_bindgen::{self, from_value}; use std::cell::RefCell; -use std::sync::atomic::Ordering; +use std::sync::atomic::{AtomicU64, Ordering}; use std::time::Duration; use wasm_bindgen::JsCast; use wasm_bindgen::prelude::*; use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, ImageData, window}; +static IMAGE_DATA_HASH: AtomicU64 = AtomicU64::new(0); + +fn calculate_hash(t: &T) -> u64 { + use std::collections::hash_map::DefaultHasher; + use std::hash::Hasher; + let mut hasher = DefaultHasher::new(); + t.hash(&mut hasher); + hasher.finish() +} + /// Set the random seed used by the editor by calling this from JS upon initialization. /// This is necessary because WASM doesn't have a random number generator. #[wasm_bindgen(js_name = setRandomSeed)] @@ -67,7 +77,10 @@ fn render_image_data_to_canvases(image_data: &[(u64, Image)]) { let canvases_obj = Object::from(canvases_obj); for (placeholder_id, image) in image_data.iter() { - if image.width == 0 || image.height == 0 { + let canvas_name = format!("canvas{}", placeholder_id); + let js_key = JsValue::from_str(&canvas_name); + + if Reflect::has(&canvases_obj, &js_key).unwrap_or(false) || image.width == 0 || image.height == 0 { continue; } @@ -79,6 +92,7 @@ fn render_image_data_to_canvases(image_data: &[(u64, Image)]) { canvas.set_width(image.width); canvas.set_height(image.height); + let context: CanvasRenderingContext2d = canvas .get_context("2d") .expect("Failed to get 2d context") @@ -98,8 +112,6 @@ fn render_image_data_to_canvases(image_data: &[(u64, Image)]) { } } - let canvas_name = format!("canvas{}", placeholder_id); - let js_key = JsValue::from_str(&canvas_name); let js_value = JsValue::from(canvas); if Reflect::set(&canvases_obj, &js_key, &js_value).is_err() { @@ -160,7 +172,13 @@ impl EditorHandle { // Sends a FrontendMessage to JavaScript fn send_frontend_message_to_js(&self, mut message: FrontendMessage) { if let FrontendMessage::UpdateImageData { ref image_data } = message { - render_image_data_to_canvases(image_data.as_slice()); + let new_hash = calculate_hash(image_data); + let prev_hash = IMAGE_DATA_HASH.load(Ordering::Relaxed); + + if new_hash != prev_hash { + render_image_data_to_canvases(image_data.as_slice()); + IMAGE_DATA_HASH.store(new_hash, Ordering::Relaxed); + } return; } diff --git a/node-graph/gsvg-renderer/src/renderer.rs b/node-graph/gsvg-renderer/src/renderer.rs index a1e9208773..c2a33d409c 100644 --- a/node-graph/gsvg-renderer/src/renderer.rs +++ b/node-graph/gsvg-renderer/src/renderer.rs @@ -954,9 +954,9 @@ impl GraphicElementRendered for RasterDataTable { image.data().hash(&mut state); state.finish() }); - // if !render.image_data.iter().any(|(old_id, _)| *old_id == id) { + if !render.image_data.iter().any(|(old_id, _)| *old_id == id) { render.image_data.push((id, image.data().clone())); - // } + } render.parent_tag( "foreignObject", |attributes| { From c9cd494ed20472fd2f8cb940ac985342d445c5ba Mon Sep 17 00:00:00 2001 From: mtvare6 Date: Thu, 24 Jul 2025 03:49:00 +0530 Subject: [PATCH 14/21] cargo: fmt --- editor/src/messages/prelude.rs | 8 +++++++- node-graph/gcore/src/raster/image.rs | 1 - node-graph/gcore/src/vector/vector_nodes.rs | 8 ++------ node-graph/graster-nodes/src/dehaze.rs | 1 - node-graph/graster-nodes/src/filter.rs | 1 - node-graph/graster-nodes/src/std_nodes.rs | 6 ++---- 6 files changed, 11 insertions(+), 14 deletions(-) diff --git a/editor/src/messages/prelude.rs b/editor/src/messages/prelude.rs index 5a57f48bd9..72a6cb9ba7 100644 --- a/editor/src/messages/prelude.rs +++ b/editor/src/messages/prelude.rs @@ -15,7 +15,13 @@ pub use crate::messages::input_mapper::key_mapping::{KeyMappingMessage, KeyMappi pub use crate::messages::input_mapper::{InputMapperMessage, InputMapperMessageContext, InputMapperMessageDiscriminant, InputMapperMessageHandler}; pub use crate::messages::input_preprocessor::{InputPreprocessorMessage, InputPreprocessorMessageContext, InputPreprocessorMessageDiscriminant, InputPreprocessorMessageHandler}; pub use crate::messages::layout::{LayoutMessage, LayoutMessageDiscriminant, LayoutMessageHandler}; -pub use crate::messages::portfolio::document::graph_operation::{GraphOperationMessage, GraphOperationMessageContext, GraphOperationMessageDiscriminant, GraphOperationMessageHandler}; pub use crate::messages::portfolio::document::navigation::{NavigationMessage, NavigationMessageContext, NavigationMessageDiscriminant, NavigationMessageHandler}; pub use crate::messages::portfolio::document::node_graph::{NodeGraphMessage, NodeGraphMessageDiscriminant, NodeGraphMessageHandler}; pub use crate::messages::portfolio::document::overlays::{OverlaysMessage, OverlaysMessageContext, OverlaysMessageDiscriminant, OverlaysMessageHandler}; pub use crate::messages::portfolio::document::properties_panel::{PropertiesPanelMessage, PropertiesPanelMessageDiscriminant, PropertiesPanelMessageHandler}; pub use crate::messages::portfolio::document::{DocumentMessage, DocumentMessageContext, DocumentMessageDiscriminant, DocumentMessageHandler}; pub use crate::messages::portfolio::menu_bar::{MenuBarMessage, MenuBarMessageDiscriminant, MenuBarMessageHandler}; +pub use crate::messages::portfolio::document::graph_operation::{GraphOperationMessage, GraphOperationMessageContext, GraphOperationMessageDiscriminant, GraphOperationMessageHandler}; +pub use crate::messages::portfolio::document::navigation::{NavigationMessage, NavigationMessageContext, NavigationMessageDiscriminant, NavigationMessageHandler}; +pub use crate::messages::portfolio::document::node_graph::{NodeGraphMessage, NodeGraphMessageDiscriminant, NodeGraphMessageHandler}; +pub use crate::messages::portfolio::document::overlays::{OverlaysMessage, OverlaysMessageContext, OverlaysMessageDiscriminant, OverlaysMessageHandler}; +pub use crate::messages::portfolio::document::properties_panel::{PropertiesPanelMessage, PropertiesPanelMessageDiscriminant, PropertiesPanelMessageHandler}; +pub use crate::messages::portfolio::document::{DocumentMessage, DocumentMessageContext, DocumentMessageDiscriminant, DocumentMessageHandler}; +pub use crate::messages::portfolio::menu_bar::{MenuBarMessage, MenuBarMessageDiscriminant, MenuBarMessageHandler}; pub use crate::messages::portfolio::spreadsheet::{SpreadsheetMessage, SpreadsheetMessageDiscriminant}; pub use crate::messages::portfolio::{PortfolioMessage, PortfolioMessageContext, PortfolioMessageDiscriminant, PortfolioMessageHandler}; pub use crate::messages::preferences::{PreferencesMessage, PreferencesMessageDiscriminant, PreferencesMessageHandler}; diff --git a/node-graph/gcore/src/raster/image.rs b/node-graph/gcore/src/raster/image.rs index 92b49ee462..d6bfa3ec0d 100644 --- a/node-graph/gcore/src/raster/image.rs +++ b/node-graph/gcore/src/raster/image.rs @@ -57,7 +57,6 @@ impl Hash for TransformImage { fn hash(&self, _: &mut H) {} } - impl Debug for Image

{ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let length = self.data.len(); diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index ca2349feed..275377c47e 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -417,7 +417,6 @@ where // Create and add mirrored instance for mut instance in instance.instance_iter() { instance.transform = reflected_transform * instance.transform; - instance.source_node_id = None; result_table.push(instance); } @@ -448,6 +447,7 @@ async fn round_corners( .map(|source| { let source_transform = *source.transform; let source_transform_inverse = source_transform.inverse(); + let source_node_id = source.source_node_id; let source = source.instance; let upstream_graphic_group = source.upstream_graphic_group.clone(); @@ -542,7 +542,7 @@ async fn round_corners( instance: result, transform: source_transform, alpha_blending: Default::default(), - source_node_id: None, + source_node_id: *source_node_id, } }) .collect() @@ -645,7 +645,6 @@ async fn box_warp(_: impl Ctx, vector_data: VectorDataTable, #[expose] rectangle // Add this to the table and reset the transform since we've applied it directly to the points vector_data_instance.instance = result; vector_data_instance.transform = DAffine2::IDENTITY; - vector_data_instance.source_node_id = None; vector_data_instance }) .collect() @@ -917,7 +916,6 @@ async fn bounding_box(_: impl Ctx, vector_data: VectorDataTable) -> VectorDataTa result.style.set_stroke_transform(DAffine2::IDENTITY); vector_data_instance.instance = result; - vector_data_instance.source_node_id = None; vector_data_instance }) .collect() @@ -1013,7 +1011,6 @@ async fn offset_path(_: impl Ctx, vector_data: VectorDataTable, distance: f64, j } vector_data_instance.instance = result; - vector_data_instance.source_node_id = None; vector_data_instance }) .collect() @@ -1068,7 +1065,6 @@ async fn solidify_stroke(_: impl Ctx, vector_data: VectorDataTable) -> VectorDat } vector_data_instance.instance = result; - vector_data_instance.source_node_id = None; vector_data_instance }) .collect() diff --git a/node-graph/graster-nodes/src/dehaze.rs b/node-graph/graster-nodes/src/dehaze.rs index 6c6246b964..32fc6b20b8 100644 --- a/node-graph/graster-nodes/src/dehaze.rs +++ b/node-graph/graster-nodes/src/dehaze.rs @@ -31,7 +31,6 @@ async fn dehaze(_: impl Ctx, image_frame: RasterDataTable, strength: Percen }; image_frame_instance.instance = Raster::new_cpu(dehazed_image); - image_frame_instance.source_node_id = None; image_frame_instance }) .collect() diff --git a/node-graph/graster-nodes/src/filter.rs b/node-graph/graster-nodes/src/filter.rs index b5c4bbf9b4..80891e0170 100644 --- a/node-graph/graster-nodes/src/filter.rs +++ b/node-graph/graster-nodes/src/filter.rs @@ -36,7 +36,6 @@ async fn blur( }; image_instance.instance = blurred_image; - image_instance.source_node_id = None; image_instance }) .collect() diff --git a/node-graph/graster-nodes/src/std_nodes.rs b/node-graph/graster-nodes/src/std_nodes.rs index 3df24c364c..9fcc96839d 100644 --- a/node-graph/graster-nodes/src/std_nodes.rs +++ b/node-graph/graster-nodes/src/std_nodes.rs @@ -88,7 +88,6 @@ pub fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: Rast let new_transform = image_frame_transform * DAffine2::from_translation(offset) * DAffine2::from_scale(size); image_frame_instance.transform = new_transform; - image_frame_instance.source_node_id = None; image_frame_instance.instance = Raster::new_cpu(image); Some(image_frame_instance) }) @@ -121,7 +120,7 @@ pub fn combine_channels( let alpha = alpha.filter(|i| i.instance.width > 0 && i.instance.height > 0); // Get this instance's transform and alpha blending mode from the first non-empty channel - let Some((transform, alpha_blending)) = [&red, &green, &blue, &alpha].iter().find_map(|i| i.as_ref()).map(|i| (i.transform, i.alpha_blending)) else { + let Some((transform, alpha_blending, source_node_id)) = [&red, &green, &blue, &alpha].iter().find_map(|i| i.as_ref()).map(|i| (i.transform, i.alpha_blending, i.source_node_id)) else { return None; }; @@ -179,7 +178,7 @@ pub fn combine_channels( instance: Raster::new_cpu(image), transform, alpha_blending, - source_node_id: None, + source_node_id, }) }) .collect() @@ -276,7 +275,6 @@ pub fn extend_image_to_bounds(_: impl Ctx, image: RasterDataTable, bounds: image_instance.instance = Raster::new_cpu(new_image); image_instance.transform = new_texture_to_layer_space; - image_instance.source_node_id = None; image_instance }) .collect() From 8fa25f62385fe9f80c86cc0ecf4d1bb2a706b7f4 Mon Sep 17 00:00:00 2001 From: mtvare6 Date: Thu, 24 Jul 2025 07:37:18 +0530 Subject: [PATCH 15/21] fix --- frontend/src/components/panels/Document.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 37ad82f65b..5f579b83dd 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -199,7 +199,7 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any let canvas = (window as any).imageCanvases[canvasName]; - if (canvas.parentElement) { + if (canvasName != "canvas0" && canvas.parentElement) { var newCanvas = window.document.createElement("canvas"); var context = newCanvas.getContext("2d"); From df33a022849fd978df6790c14f4ccadbbdc66094 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Wed, 23 Jul 2025 21:56:28 -0700 Subject: [PATCH 16/21] Avoid "canvas" prefix to IDs --- editor/src/node_graph_executor.rs | 4 ++-- frontend/src/components/panels/Document.svelte | 2 +- frontend/wasm/src/editor_api.rs | 8 ++++---- node-graph/graph-craft/src/wasm_application_io.rs | 6 +++--- node-graph/gsvg-renderer/src/renderer.rs | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index a3067b436b..65d73271a3 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -357,9 +357,9 @@ impl NodeGraphExecutor { } graphene_std::wasm_application_io::RenderOutputType::CanvasFrame(frame) => { let matrix = format_transform_matrix(frame.transform); - let transform = if matrix.is_empty() { String::new() } else { format!(" transform=\"{}\"", matrix) }; + let transform = if matrix.is_empty() { String::new() } else { format!(" transform=\"{matrix}\"") }; let svg = format!( - r#"
"#, + r#"
"#, frame.resolution.x, frame.resolution.y, frame.surface_id.0 ); responses.add(FrontendMessage::UpdateDocumentArtwork { svg }); diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 5f579b83dd..bd36152f04 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -199,7 +199,7 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any let canvas = (window as any).imageCanvases[canvasName]; - if (canvasName != "canvas0" && canvas.parentElement) { + if (canvasName !== "0" && canvas.parentElement) { var newCanvas = window.document.createElement("canvas"); var context = newCanvas.getContext("2d"); diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 4b9bb64fc9..1943add2da 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -77,7 +77,7 @@ fn render_image_data_to_canvases(image_data: &[(u64, Image)]) { let canvases_obj = Object::from(canvases_obj); for (placeholder_id, image) in image_data.iter() { - let canvas_name = format!("canvas{}", placeholder_id); + let canvas_name = placeholder_id.to_string(); let js_key = JsValue::from_str(&canvas_name); if Reflect::has(&canvases_obj, &js_key).unwrap_or(false) || image.width == 0 || image.height == 0 { @@ -104,18 +104,18 @@ fn render_image_data_to_canvases(image_data: &[(u64, Image)]) { match ImageData::new_with_u8_clamped_array_and_sh(clamped_u8_data, image.width, image.height) { Ok(image_data_obj) => { if context.put_image_data(&image_data_obj, 0.0, 0.0).is_err() { - error!("Failed to put image data on canvas for id: {}", placeholder_id); + error!("Failed to put image data on canvas for id: {placeholder_id}"); } } Err(e) => { - error!("Failed to create ImageData for id: {}: {:?}", placeholder_id, e); + error!("Failed to create ImageData for id: {placeholder_id}: {e:?}"); } } let js_value = JsValue::from(canvas); if Reflect::set(&canvases_obj, &js_key, &js_value).is_err() { - error!("Failed to set canvas '{}' on imageCanvases object", canvas_name); + error!("Failed to set canvas '{canvas_name}' on imageCanvases object"); } } } diff --git a/node-graph/graph-craft/src/wasm_application_io.rs b/node-graph/graph-craft/src/wasm_application_io.rs index d3aa4c4d88..b52d4e0a80 100644 --- a/node-graph/graph-craft/src/wasm_application_io.rs +++ b/node-graph/graph-craft/src/wasm_application_io.rs @@ -40,7 +40,7 @@ impl Drop for WindowWrapper { let wrapper = || { if let Ok(canvases) = Reflect::get(&window, &image_canvases_key) { // Convert key and value to JsValue - let js_key = JsValue::from_str(format!("canvas{}", self.window.window_id).as_str()); + let js_key = JsValue::from_str(self.window.window_id.to_string().as_str()); // Use Reflect API to set property Reflect::delete_property(&canvases.into(), &js_key)?; @@ -201,7 +201,7 @@ impl ApplicationIo for WasmApplicationIo { } // Convert key and value to JsValue - let js_key = JsValue::from_str(format!("canvas{}", id).as_str()); + let js_key = JsValue::from_str(id.to_string().as_str()); let js_value = JsValue::from(canvas.clone()); let canvases = Object::from(canvases.unwrap()); @@ -254,7 +254,7 @@ impl ApplicationIo for WasmApplicationIo { let wrapper = || { if let Ok(canvases) = Reflect::get(&window, &image_canvases_key) { // Convert key and value to JsValue - let js_key = JsValue::from_str(format!("canvas{}", surface_id.0).as_str()); + let js_key = JsValue::from_str(surface_id.0.to_string().as_str()); // Use Reflect API to set property Reflect::delete_property(&canvases.into(), &js_key)?; diff --git a/node-graph/gsvg-renderer/src/renderer.rs b/node-graph/gsvg-renderer/src/renderer.rs index 9f328fc1e1..440b9e7b47 100644 --- a/node-graph/gsvg-renderer/src/renderer.rs +++ b/node-graph/gsvg-renderer/src/renderer.rs @@ -990,7 +990,7 @@ impl GraphicElementRendered for RasterDataTable { render.leaf_tag( "img", // Must be a self-closing tag |attributes| { - attributes.push("data-canvas-placeholder", format!("canvas{}", id)); + attributes.push("data-canvas-placeholder", id.to_string()); }, ) }, From 810e6815349ddd030be45009dc3c7a49cef15e7a Mon Sep 17 00:00:00 2001 From: mtvare6 Date: Thu, 24 Jul 2025 12:07:14 +0530 Subject: [PATCH 17/21] fix --- node-graph/gsvg-renderer/src/renderer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node-graph/gsvg-renderer/src/renderer.rs b/node-graph/gsvg-renderer/src/renderer.rs index 440b9e7b47..85a67524f5 100644 --- a/node-graph/gsvg-renderer/src/renderer.rs +++ b/node-graph/gsvg-renderer/src/renderer.rs @@ -176,7 +176,7 @@ impl RenderParams { } pub fn to_canvas(&self) -> bool { - !self.for_export && !self.thumbnail + !self.for_export && !self.thumbnail && !self.for_mask } } From 295c7b25703c59fcda8d47e43679041db4b16f35 Mon Sep 17 00:00:00 2001 From: mtvare6 Date: Fri, 25 Jul 2025 03:31:16 +0530 Subject: [PATCH 18/21] fix: vello issue from 6111440 --- node-graph/gsvg-renderer/src/renderer.rs | 29 ++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/node-graph/gsvg-renderer/src/renderer.rs b/node-graph/gsvg-renderer/src/renderer.rs index 4160a8dd10..107c68f23b 100644 --- a/node-graph/gsvg-renderer/src/renderer.rs +++ b/node-graph/gsvg-renderer/src/renderer.rs @@ -1032,18 +1032,39 @@ impl GraphicElementRendered for RasterDataTable { } #[cfg(feature = "vello")] - fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, _: &mut RenderContext, _render_params: &RenderParams) { + fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, _: &mut RenderContext, render_params: &RenderParams) { use vello::peniko; for instance in self.instance_ref_iter() { let image = &instance.instance; if image.data.is_empty() { - return; + continue; } + + let alpha_blending = *instance.alpha_blending; + let blend_mode = alpha_blending.blend_mode.to_peniko(); + let factor = if render_params.for_mask { 1. } else { alpha_blending.fill }; + let opacity = alpha_blending.opacity * factor; + + let mut layer = false; + + if opacity < 1. || alpha_blending.blend_mode != BlendMode::default() { + if let Some(bounds) = self.bounding_box(transform, false) { + let blending = peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver); + let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y); + scene.push_layer(blending, opacity, kurbo::Affine::IDENTITY, &rect); + layer = true; + } + } + let image = peniko::Image::new(image.to_flat_u8().0.into(), peniko::ImageFormat::Rgba8, image.width, image.height).with_extend(peniko::Extend::Repeat); - let transform = transform * *instance.transform * DAffine2::from_scale(1. / DVec2::new(image.width as f64, image.height as f64)); + let image_transform = transform * *instance.transform * DAffine2::from_scale(1. / DVec2::new(image.width as f64, image.height as f64)); - scene.draw_image(&image, kurbo::Affine::new(transform.to_cols_array())); + scene.draw_image(&image, kurbo::Affine::new(image_transform.to_cols_array())); + + if layer { + scene.pop_layer(); + } } } From 7fb1450fb2bbda1e2e907e629da3628764e7c38e Mon Sep 17 00:00:00 2001 From: mtvare6 Date: Fri, 25 Jul 2025 03:44:36 +0530 Subject: [PATCH 19/21] fix: gpu stuff --- node-graph/gcore/src/blending.rs | 4 ++++ node-graph/gsvg-renderer/src/renderer.rs | 23 +++++++++-------------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/node-graph/gcore/src/blending.rs b/node-graph/gcore/src/blending.rs index 921fb0d0ae..44404bd502 100644 --- a/node-graph/gcore/src/blending.rs +++ b/node-graph/gcore/src/blending.rs @@ -56,6 +56,10 @@ impl AlphaBlending { clip: if t < 0.5 { self.clip } else { other.clip }, } } + + pub fn opacity(&self, mask: bool) -> f32 { + self.opacity * if mask { 1. } else { self.fill } + } } #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] diff --git a/node-graph/gsvg-renderer/src/renderer.rs b/node-graph/gsvg-renderer/src/renderer.rs index 107c68f23b..8953cf22e3 100644 --- a/node-graph/gsvg-renderer/src/renderer.rs +++ b/node-graph/gsvg-renderer/src/renderer.rs @@ -248,8 +248,7 @@ impl GraphicElementRendered for GraphicGroupTable { attributes.push("transform", matrix); } - let factor = if render_params.for_mask { 1. } else { instance.alpha_blending.fill }; - let opacity = instance.alpha_blending.opacity * factor; + let opacity = instance.alpha_blending.opacity(render_params.for_mask); if opacity < 1. { attributes.push("opacity", opacity.to_string()); } @@ -304,8 +303,7 @@ impl GraphicElementRendered for GraphicGroupTable { }; let mut bounds = None; - let factor = if render_params.for_mask { 1. } else { alpha_blending.fill }; - let opacity = alpha_blending.opacity * factor; + let opacity = instance.alpha_blending.opacity(render_params.for_mask); if opacity < 1. || (render_params.view_mode != ViewMode::Outline && alpha_blending.blend_mode != BlendMode::default()) { bounds = self .instance_ref_iter() @@ -508,8 +506,7 @@ impl GraphicElementRendered for VectorDataTable { } attributes.push_val(fill_and_stroke); - let factor = if render_params.for_mask { 1. } else { instance.alpha_blending.fill }; - let opacity = instance.alpha_blending.opacity * factor; + let opacity = instance.alpha_blending.opacity(render_params.for_mask); if opacity < 1. { attributes.push("opacity", opacity.to_string()); } @@ -550,8 +547,8 @@ impl GraphicElementRendered for VectorDataTable { _ => instance.alpha_blending.blend_mode.to_peniko(), }; let mut layer = false; - let factor = if render_params.for_mask { 1. } else { instance.alpha_blending.fill }; - let opacity = instance.alpha_blending.opacity * factor; + + let opacity = instance.alpha_blending.opacity(render_params.for_mask); if opacity < 1. || instance.alpha_blending.blend_mode != BlendMode::default() { layer = true; scene.push_layer( @@ -979,8 +976,7 @@ impl GraphicElementRendered for RasterDataTable { attributes.push("width", size.x.to_string()); attributes.push("height", size.y.to_string()); - let factor = if render_params.for_mask { 1. } else { instance.alpha_blending.fill }; - let opacity = instance.alpha_blending.opacity * factor; + let opacity = instance.alpha_blending.opacity(render_params.for_mask); if opacity < 1. { attributes.push("opacity", opacity.to_string()); } @@ -1018,8 +1014,8 @@ impl GraphicElementRendered for RasterDataTable { if !matrix.is_empty() { attributes.push("transform", matrix); } - let factor = if render_params.for_mask { 1. } else { instance.alpha_blending.fill }; - let opacity = instance.alpha_blending.opacity * factor; + + let opacity = instance.alpha_blending.opacity(render_params.for_mask); if opacity < 1. { attributes.push("opacity", opacity.to_string()); } @@ -1043,9 +1039,8 @@ impl GraphicElementRendered for RasterDataTable { let alpha_blending = *instance.alpha_blending; let blend_mode = alpha_blending.blend_mode.to_peniko(); - let factor = if render_params.for_mask { 1. } else { alpha_blending.fill }; - let opacity = alpha_blending.opacity * factor; + let opacity = alpha_blending.opacity(render_params.for_mask); let mut layer = false; if opacity < 1. || alpha_blending.blend_mode != BlendMode::default() { From dc1fe735c736c92a34195d5660ee6415f8f3a403 Mon Sep 17 00:00:00 2001 From: mtvare6 Date: Fri, 25 Jul 2025 04:08:32 +0530 Subject: [PATCH 20/21] fix: vello bbox --- node-graph/gsvg-renderer/src/renderer.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/node-graph/gsvg-renderer/src/renderer.rs b/node-graph/gsvg-renderer/src/renderer.rs index 8953cf22e3..ff96e260b0 100644 --- a/node-graph/gsvg-renderer/src/renderer.rs +++ b/node-graph/gsvg-renderer/src/renderer.rs @@ -551,6 +551,9 @@ impl GraphicElementRendered for VectorDataTable { let opacity = instance.alpha_blending.opacity(render_params.for_mask); if opacity < 1. || instance.alpha_blending.blend_mode != BlendMode::default() { layer = true; + let weight = instance.instance.style.stroke().unwrap().weight; + let quad = Quad::from_box(layer_bounds).inflate(weight * element_transform.matrix2.determinant()); + let layer_bounds = quad.bounding_box(); scene.push_layer( peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver), opacity, @@ -1094,12 +1097,14 @@ impl GraphicElementRendered for RasterDataTable { for instance in self.instance_ref_iter() { let blend_mode = *instance.alpha_blending; - let layer = blend_mode != Default::default(); - if layer { - let Some(bounds) = self.bounding_box(transform, true) else { return }; - let blending = peniko::BlendMode::new(blend_mode.blend_mode.to_peniko(), peniko::Compose::SrcOver); - let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y); - scene.push_layer(blending, blend_mode.opacity, kurbo::Affine::IDENTITY, &rect); + let mut layer = false; + if blend_mode != Default::default() { + if let Some(bounds) = self.bounding_box(transform, true) { + let blending = peniko::BlendMode::new(blend_mode.blend_mode.to_peniko(), peniko::Compose::SrcOver); + let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y); + scene.push_layer(blending, blend_mode.opacity, kurbo::Affine::IDENTITY, &rect); + layer = true; + } } let image = peniko::Image::new( From a8115336194ac2a9c079a329f1ba2b11d5c5663a Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Thu, 24 Jul 2025 16:04:39 -0700 Subject: [PATCH 21/21] Code review --- frontend/wasm/src/editor_api.rs | 2 +- node-graph/gcore/src/raster.rs | 19 ++++++++----------- node-graph/graster-nodes/src/std_nodes.rs | 11 +++++------ node-graph/gstd/src/wasm_application_io.rs | 7 ++++--- node-graph/gsvg-renderer/src/renderer.rs | 5 ++++- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 1943add2da..c649ee17ac 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -103,7 +103,7 @@ fn render_image_data_to_canvases(image_data: &[(u64, Image)]) { let clamped_u8_data = wasm_bindgen::Clamped(&u8_data[..]); match ImageData::new_with_u8_clamped_array_and_sh(clamped_u8_data, image.width, image.height) { Ok(image_data_obj) => { - if context.put_image_data(&image_data_obj, 0.0, 0.0).is_err() { + if context.put_image_data(&image_data_obj, 0., 0.).is_err() { error!("Failed to put image data on canvas for id: {placeholder_id}"); } } diff --git a/node-graph/gcore/src/raster.rs b/node-graph/gcore/src/raster.rs index d38fbf467b..617441b8f9 100644 --- a/node-graph/gcore/src/raster.rs +++ b/node-graph/gcore/src/raster.rs @@ -1,12 +1,3 @@ -use crate::GraphicGroupTable; -pub use crate::color::*; -use crate::raster_types::{CPU, RasterDataTable}; -use crate::vector::VectorDataTable; -use std::fmt::Debug; - -#[cfg(target_arch = "spirv")] -use spirv_std::num_traits::float::Float; - /// as to not yet rename all references pub mod color { pub use super::*; @@ -14,8 +5,14 @@ pub mod color { pub mod image; -pub use self::image::Image; -pub use self::image::TransformImage; +pub use self::image::{Image, TransformImage}; +use crate::GraphicGroupTable; +pub use crate::color::*; +use crate::raster_types::{CPU, RasterDataTable}; +use crate::vector::VectorDataTable; +#[cfg(target_arch = "spirv")] +use spirv_std::num_traits::float::Float; +use std::fmt::Debug; pub trait Bitmap { type Pixel: Pixel; diff --git a/node-graph/graster-nodes/src/std_nodes.rs b/node-graph/graster-nodes/src/std_nodes.rs index 9fcc96839d..8c32c02a47 100644 --- a/node-graph/graster-nodes/src/std_nodes.rs +++ b/node-graph/graster-nodes/src/std_nodes.rs @@ -120,9 +120,10 @@ pub fn combine_channels( let alpha = alpha.filter(|i| i.instance.width > 0 && i.instance.height > 0); // Get this instance's transform and alpha blending mode from the first non-empty channel - let Some((transform, alpha_blending, source_node_id)) = [&red, &green, &blue, &alpha].iter().find_map(|i| i.as_ref()).map(|i| (i.transform, i.alpha_blending, i.source_node_id)) else { - return None; - }; + let (transform, alpha_blending, source_node_id) = [&red, &green, &blue, &alpha] + .iter() + .find_map(|i| i.as_ref()) + .map(|i| (i.transform, i.alpha_blending, i.source_node_id))?; // Get the common width and height of the channels, which must have equal dimensions let channel_dimensions = [ @@ -139,9 +140,7 @@ pub fn combine_channels( { return None; } - let Some(&(width, height)) = channel_dimensions.iter().flatten().next() else { - return None; - }; + let &(width, height) = channel_dimensions.iter().flatten().next()?; // Create a new image for this instance output let mut image = Image::new(width, height, Color::TRANSPARENT); diff --git a/node-graph/gstd/src/wasm_application_io.rs b/node-graph/gstd/src/wasm_application_io.rs index bfc1057e69..23f12bb59e 100644 --- a/node-graph/gstd/src/wasm_application_io.rs +++ b/node-graph/gstd/src/wasm_application_io.rs @@ -158,9 +158,10 @@ fn render_svg(data: impl GraphicElementRendered, mut render: SvgRender, render_p render.wrap_with_transform(footprint.transform, Some(footprint.resolution.as_dvec2())); - let svg = render.svg.to_svg_string(); - let image_data = render.image_data; - RenderOutputType::Svg { svg, image_data } + RenderOutputType::Svg { + svg: render.svg.to_svg_string(), + image_data: render.image_data, + } } #[cfg(feature = "vello")] diff --git a/node-graph/gsvg-renderer/src/renderer.rs b/node-graph/gsvg-renderer/src/renderer.rs index ff96e260b0..9751a27212 100644 --- a/node-graph/gsvg-renderer/src/renderer.rs +++ b/node-graph/gsvg-renderer/src/renderer.rs @@ -971,11 +971,13 @@ impl GraphicElementRendered for RasterDataTable { let mut transform_values = transform.to_scale_angle_translation(); let size = DVec2::new(image.width as f64, image.height as f64); transform_values.0 /= size; + let matrix = DAffine2::from_scale_angle_translation(transform_values.0, transform_values.1, transform_values.2); let matrix = format_transform_matrix(matrix); if !matrix.is_empty() { attributes.push("transform", matrix); } + attributes.push("width", size.x.to_string()); attributes.push("height", size.y.to_string()); @@ -983,13 +985,14 @@ impl GraphicElementRendered for RasterDataTable { if opacity < 1. { attributes.push("opacity", opacity.to_string()); } + if instance.alpha_blending.blend_mode != BlendMode::default() { attributes.push("style", instance.alpha_blending.blend_mode.render()); } }, |render| { render.leaf_tag( - "img", // Must be a self-closing tag + "img", // Must be a self-closing (void element) tag, so we can't use `div` or `span`, for example |attributes| { attributes.push("data-canvas-placeholder", id.to_string()); },