From c9a33e44bd554bd3aa04409d2abdd9a1cbe90d36 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Thu, 27 Jun 2024 10:24:09 +0200 Subject: [PATCH] New node: Rasterize (#1755) * Add RasterizeVector node * Add document node definition for RasterizeVectorNode * Add dummy node properties * Add prototype of footprint widget * Fix types * Fix footprint widget to use document space * Fix aspect ratio issues by making resolution a multiplier * Fix rebase errors * Fix rasterization bounds and node definition * UI and bug fixes but still issues with scaling in the frame * Rename node to Raster * Fix RasterizeNode * Reenable resolution multiplier * Remove unused variable * UI fixes * Fix Footprint default and resolution updates --------- Co-authored-by: Keavon Chambers --- .../node_graph/document_node_types.rs | 124 +++++++++++---- .../document/node_graph/node_properties.rs | 148 +++++++++++++++++- .../gcore/src/graphic_element/renderer.rs | 3 + node-graph/gcore/src/transform.rs | 10 ++ node-graph/gstd/src/raster.rs | 5 +- node-graph/gstd/src/wasm_application_io.rs | 60 ++++++- .../interpreted-executor/src/node_registry.rs | 24 +-- 7 files changed, 330 insertions(+), 44 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_types.rs b/editor/src/messages/portfolio/document/node_graph/document_node_types.rs index 3c0d7acac..98ccf9441 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_types.rs @@ -483,6 +483,70 @@ fn static_nodes() -> Vec { }], ..Default::default() }, + DocumentNodeDefinition { + name: "Rasterize", + category: "Raster", + implementation: DocumentNodeImplementation::Network(NodeNetwork { + exports: vec![NodeInput::node(NodeId(2), 0)], + nodes: [ + DocumentNode { + name: "Create Canvas".to_string(), + inputs: vec![NodeInput::network(concrete!(WasmEditorApi), 2)], + implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_std::wasm_application_io::CreateSurfaceNode")), + skip_deduplication: true, + ..Default::default() + }, + DocumentNode { + name: "Cache".to_string(), + manual_composition: Some(concrete!(())), + inputs: vec![NodeInput::node(NodeId(0), 0)], + implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::memo::MemoNode<_, _>")), + ..Default::default() + }, + DocumentNode { + name: "Rasterize".to_string(), + inputs: vec![NodeInput::network(generic!(T), 0), NodeInput::network(concrete!(Footprint), 1), NodeInput::node(NodeId(1), 0)], + implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_std::wasm_application_io::RasterizeNode<_, _>")), + ..Default::default() + }, + ] + .into_iter() + .enumerate() + .map(|(id, node)| (NodeId(id as u64), node)) + .collect(), + ..Default::default() + }), + inputs: vec![ + DocumentInputType { + name: "Artwork", + data_type: FrontendGraphDataType::Raster, + default: NodeInput::value(TaggedValue::VectorData(VectorData::default()), true), + }, + DocumentInputType { + name: "Footprint", + data_type: FrontendGraphDataType::General, + default: NodeInput::value( + TaggedValue::Footprint(Footprint { + transform: DAffine2::from_scale_angle_translation(DVec2::new(100., 100.), 0., DVec2::new(0., 0.)), + resolution: UVec2::new(100, 100), + ..Default::default() + }), + false, + ), + }, + DocumentInputType { + name: "In", + data_type: FrontendGraphDataType::General, + default: NodeInput::network(concrete!(WasmEditorApi), 0), + }, + ], + properties: node_properties::rasterize_properties, + outputs: vec![DocumentOutputType { + name: "Canvas", + data_type: FrontendGraphDataType::General, + }], + ..Default::default() + }, DocumentNodeDefinition { // This essentially builds the concept of a closure where we store variables (`let` bindings) so they can be accessed within this scope. name: "Begin Scope", @@ -720,7 +784,7 @@ fn static_nodes() -> Vec { // TODO: This needs to work with resolution-aware (raster with footprint, post-Cull node) data. DocumentNodeDefinition { name: "Mask", - category: "Image Adjustments", + category: "Raster", implementation: DocumentNodeImplementation::proto("graphene_std::raster::MaskImageNode<_, _, _>"), inputs: vec![ DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true), @@ -733,7 +797,7 @@ fn static_nodes() -> Vec { // TODO: This needs to work with resolution-aware (raster with footprint, post-Cull node) data. DocumentNodeDefinition { name: "Insert Channel", - category: "Image Adjustments", + category: "Raster", implementation: DocumentNodeImplementation::proto("graphene_std::raster::InsertChannelNode<_, _, _, _>"), inputs: vec![ DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true), @@ -747,7 +811,7 @@ fn static_nodes() -> Vec { // TODO: This needs to work with resolution-aware (raster with footprint, post-Cull node) data. DocumentNodeDefinition { name: "Combine Channels", - category: "Image Adjustments", + category: "Raster", implementation: DocumentNodeImplementation::proto("graphene_std::raster::CombineChannelsNode"), inputs: vec![ DocumentInputType::value("None", TaggedValue::None, false), @@ -765,7 +829,7 @@ fn static_nodes() -> Vec { // TODO: This needs to work with resolution-aware (raster with footprint, post-Cull node) data. DocumentNodeDefinition { name: "Blend", - category: "Image Adjustments", + category: "Raster", implementation: DocumentNodeImplementation::proto("graphene_core::raster::BlendNode<_, _, _, _>"), inputs: vec![ DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true), @@ -779,7 +843,7 @@ fn static_nodes() -> Vec { }, DocumentNodeDefinition { name: "Levels", - category: "Image Adjustments", + category: "Raster", implementation: DocumentNodeImplementation::proto("graphene_core::raster::LevelsNode<_, _, _, _, _>"), inputs: vec![ DocumentInputType { @@ -819,7 +883,7 @@ fn static_nodes() -> Vec { }, DocumentNodeDefinition { name: "Black & White", - category: "Image Adjustments", + category: "Raster", implementation: DocumentNodeImplementation::proto("graphene_core::raster::BlackAndWhiteNode<_, _, _, _, _, _, _>"), inputs: vec![ DocumentInputType { @@ -869,7 +933,7 @@ fn static_nodes() -> Vec { }, DocumentNodeDefinition { name: "Color Channel", - category: "Image Adjustments", + category: "Raster", implementation: DocumentNodeImplementation::proto("graphene_core::ops::IdentityNode"), inputs: vec![DocumentInputType::value("Channel", TaggedValue::RedGreenBlue(RedGreenBlue::Red), false)], outputs: vec![DocumentOutputType::new("Out", FrontendGraphDataType::General)], @@ -887,7 +951,7 @@ fn static_nodes() -> Vec { }, DocumentNodeDefinition { name: "Luminance", - category: "Image Adjustments", + category: "Raster", implementation: DocumentNodeImplementation::proto("graphene_core::raster::LuminanceNode<_>"), inputs: vec![ DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true), @@ -899,7 +963,7 @@ fn static_nodes() -> Vec { }, DocumentNodeDefinition { name: "Extract Channel", - category: "Image Adjustments", + category: "Raster", implementation: DocumentNodeImplementation::proto("graphene_core::raster::ExtractChannelNode<_>"), inputs: vec![ DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true), @@ -911,7 +975,7 @@ fn static_nodes() -> Vec { }, DocumentNodeDefinition { name: "Extract Opaque", - category: "Image Adjustments", + category: "Raster", implementation: DocumentNodeImplementation::proto("graphene_core::raster::ExtractOpaqueNode<>"), inputs: vec![DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true)], outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], @@ -919,7 +983,7 @@ fn static_nodes() -> Vec { }, DocumentNodeDefinition { name: "Split Channels", - category: "Image Adjustments", + category: "Raster", implementation: DocumentNodeImplementation::Network(NodeNetwork { exports: vec![ NodeInput::node(NodeId(0), 0), @@ -1584,7 +1648,7 @@ fn static_nodes() -> Vec { #[cfg(feature = "gpu")] DocumentNodeDefinition { name: "GpuImage", - category: "Image Adjustments", + category: "Raster", implementation: DocumentNodeImplementation::proto("graphene_std::executor::MapGpuSingleImageNode<_>"), inputs: vec![ DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true), @@ -1605,7 +1669,7 @@ fn static_nodes() -> Vec { #[cfg(feature = "gpu")] DocumentNodeDefinition { name: "Blend (GPU)", - category: "Image Adjustments", + category: "Raster", implementation: DocumentNodeImplementation::proto("graphene_std::executor::BlendGpuImageNode<_, _, _>"), inputs: vec![ DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true), @@ -1699,7 +1763,7 @@ fn static_nodes() -> Vec { }, DocumentNodeDefinition { name: "Invert RGB", - category: "Image Adjustments", + category: "Raster", implementation: DocumentNodeImplementation::proto("graphene_core::raster::InvertRGBNode"), inputs: vec![DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true)], outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], @@ -1707,7 +1771,7 @@ fn static_nodes() -> Vec { }, DocumentNodeDefinition { name: "Hue/Saturation", - category: "Image Adjustments", + category: "Raster", implementation: DocumentNodeImplementation::proto("graphene_core::raster::HueSaturationNode<_, _, _>"), inputs: vec![ DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true), @@ -1721,7 +1785,7 @@ fn static_nodes() -> Vec { }, DocumentNodeDefinition { name: "Brightness/Contrast", - category: "Image Adjustments", + category: "Raster", implementation: DocumentNodeImplementation::proto("graphene_core::raster::BrightnessContrastNode<_, _, _>"), inputs: vec![ DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true), @@ -1735,7 +1799,7 @@ fn static_nodes() -> Vec { }, DocumentNodeDefinition { name: "Curves", - category: "Image Adjustments", + category: "Raster", implementation: DocumentNodeImplementation::proto("graphene_core::raster::CurvesNode<_>"), inputs: vec![ DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true), @@ -1747,7 +1811,7 @@ fn static_nodes() -> Vec { }, DocumentNodeDefinition { name: "Threshold", - category: "Image Adjustments", + category: "Raster", implementation: DocumentNodeImplementation::proto("graphene_core::raster::ThresholdNode<_, _, _>"), inputs: vec![ DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true), @@ -1761,7 +1825,7 @@ fn static_nodes() -> Vec { }, DocumentNodeDefinition { name: "Vibrance", - category: "Image Adjustments", + category: "Raster", implementation: DocumentNodeImplementation::proto("graphene_core::raster::VibranceNode<_>"), inputs: vec![ DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true), @@ -1773,7 +1837,7 @@ fn static_nodes() -> Vec { }, DocumentNodeDefinition { name: "Channel Mixer", - category: "Image Adjustments", + category: "Raster", implementation: DocumentNodeImplementation::proto("graphene_core::raster::ChannelMixerNode<_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _>"), inputs: vec![ DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true), @@ -1808,7 +1872,7 @@ fn static_nodes() -> Vec { }, DocumentNodeDefinition { name: "Selective Color", - category: "Image Adjustments", + category: "Raster", implementation: DocumentNodeImplementation::proto( "graphene_core::raster::SelectiveColorNode<_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _>", ), @@ -1870,7 +1934,7 @@ fn static_nodes() -> Vec { }, DocumentNodeDefinition { name: "Opacity", - category: "Image Adjustments", + category: "Raster", implementation: DocumentNodeImplementation::proto("graphene_core::raster::OpacityNode<_>"), inputs: vec![ DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true), @@ -1882,7 +1946,7 @@ fn static_nodes() -> Vec { }, DocumentNodeDefinition { name: "Blend Mode", - category: "Image Adjustments", + category: "Raster", implementation: DocumentNodeImplementation::proto("graphene_core::raster::BlendModeNode<_>"), inputs: vec![ DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true), @@ -1894,7 +1958,7 @@ fn static_nodes() -> Vec { }, DocumentNodeDefinition { name: "Posterize", - category: "Image Adjustments", + category: "Raster", implementation: DocumentNodeImplementation::proto("graphene_core::raster::PosterizeNode<_>"), inputs: vec![ DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true), @@ -1906,7 +1970,7 @@ fn static_nodes() -> Vec { }, DocumentNodeDefinition { name: "Exposure", - category: "Image Adjustments", + category: "Raster", implementation: DocumentNodeImplementation::proto("graphene_core::raster::ExposureNode<_, _, _>"), inputs: vec![ DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true), @@ -2645,7 +2709,7 @@ fn static_nodes() -> Vec { // TODO: This needs to work with resolution-aware (raster with footprint, post-Cull node) data. DocumentNodeDefinition { name: "Image Segmentation", - category: "Image Adjustments", + category: "Raster", implementation: DocumentNodeImplementation::proto("graphene_std::image_segmentation::ImageSegmentationNode<_>"), inputs: vec![ DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true), @@ -2656,7 +2720,7 @@ fn static_nodes() -> Vec { }, DocumentNodeDefinition { name: "Index", - category: "Image Adjustments", + category: "Raster", implementation: DocumentNodeImplementation::proto("graphene_core::raster::IndexNode<_>"), inputs: vec![ DocumentInputType::value("Segmentation", TaggedValue::Segments(vec![ImageFrame::empty()]), true), @@ -2669,7 +2733,7 @@ fn static_nodes() -> Vec { // Applies the given color to each pixel of an image but maintains the alpha value DocumentNodeDefinition { name: "Color Fill", - category: "Image Adjustments", + category: "Raster", implementation: DocumentNodeImplementation::proto("graphene_core::raster::adjustments::ColorFillNode<_>"), inputs: vec![ DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true), @@ -2681,7 +2745,7 @@ fn static_nodes() -> Vec { }, DocumentNodeDefinition { name: "Color Overlay", - category: "Image Adjustments", + category: "Raster", implementation: DocumentNodeImplementation::proto("graphene_core::raster::adjustments::ColorOverlayNode<_, _, _>"), inputs: vec![ DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true), @@ -2695,7 +2759,7 @@ fn static_nodes() -> Vec { }, DocumentNodeDefinition { name: "Image Color Palette", - category: "Image Adjustments", + category: "Raster", implementation: DocumentNodeImplementation::proto("graphene_std::image_color_palette::ImageColorPaletteNode<_>"), inputs: vec![ DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true), diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index a0535668e..89d65fbe6 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -18,7 +18,8 @@ use graphene_core::vector::misc::CentroidType; use graphene_core::vector::style::{GradientType, LineCap, LineJoin}; use graphene_std::vector::style::{Fill, FillChoice}; -use glam::{DVec2, IVec2, UVec2}; +use glam::{DAffine2, DVec2, IVec2, UVec2}; +use graphene_std::transform::Footprint; use graphene_std::vector::misc::BooleanOperation; pub fn string_properties(text: impl Into) -> Vec { @@ -137,6 +138,148 @@ fn bool_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name widgets } +fn footprint_widget(document_node: &DocumentNode, node_id: NodeId, index: usize) -> Vec { + let mut location_widgets = start_widgets(document_node, node_id, index, "Footprint", FrontendGraphDataType::General, true); + location_widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); + + let mut scale_widgets = vec![TextLabel::new("").widget_holder()]; + add_blank_assist(&mut scale_widgets); + scale_widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); + + let mut resolution_widgets = vec![TextLabel::new("").widget_holder()]; + add_blank_assist(&mut resolution_widgets); + resolution_widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); + + if let NodeInput::Value { + tagged_value: TaggedValue::Footprint(footprint), + exposed: false, + } = &document_node.inputs[index] + { + let footprint = *footprint; + let top_left = footprint.transform.transform_point2(DVec2::ZERO); + let bounds = footprint.scale(); + let oversample = footprint.resolution.as_dvec2() / bounds; + + location_widgets.extend_from_slice(&[ + NumberInput::new(Some(top_left.x)) + .label("X") + .unit(" px") + .on_update(update_value( + move |x: &NumberInput| { + let (offset, scale) = (move |x: f64| -> (DVec2, DVec2) { + let diff = DVec2::new(top_left.x - x, 0.); + (top_left - diff, bounds) + })(x.value.unwrap_or_default()); + + let footprint = Footprint { + transform: DAffine2::from_scale_angle_translation(scale.into(), 0., offset.into()), + resolution: (oversample * scale).as_uvec2(), + ..footprint + }; + + TaggedValue::Footprint(footprint) + }, + node_id, + index, + )) + .on_commit(commit_value) + .widget_holder(), + Separator::new(SeparatorType::Related).widget_holder(), + NumberInput::new(Some(top_left.y)) + .label("Y") + .unit(" px") + .on_update(update_value( + move |x: &NumberInput| { + let (offset, scale) = (move |y: f64| -> (DVec2, DVec2) { + let diff = DVec2::new(0., top_left.y - y); + (top_left - diff, bounds) + })(x.value.unwrap_or_default()); + + let footprint = Footprint { + transform: DAffine2::from_scale_angle_translation(scale.into(), 0., offset.into()), + resolution: (oversample * scale).as_uvec2(), + ..footprint + }; + + TaggedValue::Footprint(footprint) + }, + node_id, + index, + )) + .on_commit(commit_value) + .widget_holder(), + ]); + + scale_widgets.extend_from_slice(&[ + NumberInput::new(Some(bounds.x)) + .label("W") + .unit(" px") + .on_update(update_value( + move |x: &NumberInput| { + let (offset, scale) = (move |x: f64| -> (DVec2, DVec2) { (top_left, DVec2::new(x, bounds.y)) })(x.value.unwrap_or_default()); + + let footprint = Footprint { + transform: DAffine2::from_scale_angle_translation(scale.into(), 0., offset.into()), + resolution: (oversample * scale).as_uvec2(), + ..footprint + }; + + TaggedValue::Footprint(footprint) + }, + node_id, + index, + )) + .on_commit(commit_value) + .widget_holder(), + Separator::new(SeparatorType::Related).widget_holder(), + NumberInput::new(Some(bounds.y)) + .label("H") + .unit(" px") + .on_update(update_value( + move |x: &NumberInput| { + let (offset, scale) = (move |y: f64| -> (DVec2, DVec2) { (top_left, DVec2::new(bounds.x, y)) })(x.value.unwrap_or_default()); + + let footprint = Footprint { + transform: DAffine2::from_scale_angle_translation(scale.into(), 0., offset.into()), + resolution: (oversample * scale).as_uvec2(), + ..footprint + }; + + TaggedValue::Footprint(footprint) + }, + node_id, + index, + )) + .on_commit(commit_value) + .widget_holder(), + ]); + + resolution_widgets.push( + NumberInput::new(Some((footprint.resolution.as_dvec2() / bounds).x as f64 * 100.)) + .label("Resolution") + .unit("%") + .on_update(update_value( + move |x: &NumberInput| { + let resolution = (bounds * x.value.unwrap_or(100.) / 100.).as_uvec2().max((1, 1).into()).min((4000, 4000).into()); + + let footprint = Footprint { resolution, ..footprint }; + TaggedValue::Footprint(footprint) + }, + node_id, + index, + )) + .on_commit(commit_value) + .widget_holder(), + ); + } + + vec![ + LayoutGroup::Row { widgets: location_widgets }, + LayoutGroup::Row { widgets: scale_widgets }, + LayoutGroup::Row { widgets: resolution_widgets }, + ] +} + fn vec2_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, x: &str, y: &str, unit: &str, min: Option, mut assist: impl FnMut(&mut Vec)) -> LayoutGroup { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::Number, false); @@ -1640,6 +1783,9 @@ pub fn transform_properties(document_node: &DocumentNode, node_id: NodeId, _cont vec![translation, rotation, scale] } +pub fn rasterize_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { + footprint_widget(document_node, node_id, 1) +} pub fn node_section_font(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { let text = text_area_widget(document_node, node_id, 1, "Text", true); diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs index 51155ca58..5ef5d8289 100644 --- a/node-graph/gcore/src/graphic_element/renderer.rs +++ b/node-graph/gcore/src/graphic_element/renderer.rs @@ -155,11 +155,14 @@ impl Default for SvgRender { } } +#[derive(Default)] pub enum ImageRenderMode { + #[default] Base64, } /// Static state used whilst rendering +#[derive(Default)] pub struct RenderParams { pub view_mode: crate::vector::style::ViewMode, pub image_render_mode: ImageRenderMode, diff --git a/node-graph/gcore/src/transform.rs b/node-graph/gcore/src/transform.rs index 01e4396bf..8ea0a14c9 100644 --- a/node-graph/gcore/src/transform.rs +++ b/node-graph/gcore/src/transform.rs @@ -200,6 +200,16 @@ impl Footprint { let end = inverse.transform_point2(self.resolution.as_dvec2()); AxisAlignedBbox { start, end } } + + pub fn scale(&self) -> DVec2 { + let x = self.transform.transform_vector2((1., 0.).into()).length(); + let y = self.transform.transform_vector2((0., 1.).into()).length(); + DVec2::new(x, y) + } + + pub fn offset(&self) -> DVec2 { + self.transform.transform_point2(DVec2::ZERO) + } } #[derive(Debug, Clone, Copy)] diff --git a/node-graph/gstd/src/raster.rs b/node-graph/gstd/src/raster.rs index 8342fcb24..025f90290 100644 --- a/node-graph/gstd/src/raster.rs +++ b/node-graph/gstd/src/raster.rs @@ -733,8 +733,9 @@ fn mandelbrot_node(footprint: Footprint) -> ImageFrame { let offset = (intersection.start - image_bounds.start).max(DVec2::ZERO); - let width = footprint.transform.transform_vector2(DVec2::X * size.x).length() as u32; - let height = footprint.transform.transform_vector2(DVec2::Y * size.y).length() as u32; + let scale = footprint.scale(); + let width = (size.x * scale.x) as u32; + let height = (size.y * scale.y) as u32; let mut data = Vec::with_capacity(width as usize * height as usize); let max_iter = 255; diff --git a/node-graph/gstd/src/wasm_application_io.rs b/node-graph/gstd/src/wasm_application_io.rs index aef39b1f7..550715845 100644 --- a/node-graph/gstd/src/wasm_application_io.rs +++ b/node-graph/gstd/src/wasm_application_io.rs @@ -1,14 +1,18 @@ use dyn_any::StaticType; use graphene_core::application_io::{ApplicationError, ApplicationIo, ExportFormat, RenderConfig, ResourceFuture, SurfaceHandle, SurfaceHandleFrame, SurfaceId}; +use graphene_core::raster::bbox::Bbox; use graphene_core::raster::Image; use graphene_core::raster::{color::SRGBA8, ImageFrame}; use graphene_core::renderer::{format_transform_matrix, GraphicElementRendered, ImageRenderMode, RenderParams, RenderSvgSegmentList, SvgRender}; -use graphene_core::transform::Footprint; +use graphene_core::transform::{Footprint, TransformMut}; use graphene_core::Color; use graphene_core::Node; #[cfg(feature = "wgpu")] use wgpu_executor::WgpuExecutor; +use base64::Engine; +use glam::DAffine2; + use core::future::Future; #[cfg(target_arch = "wasm32")] use js_sys::{Object, Reflect}; @@ -362,6 +366,60 @@ fn render_canvas( RenderOutput::CanvasFrame(frame.into()) } +pub struct RasterizeNode { + footprint: Footprint, + surface_handle: Surface, +} + +#[node_macro::node_fn(RasterizeNode)] +async fn rasterize<_T: GraphicElementRendered + TransformMut>(mut data: _T, footprint: Footprint, surface_handle: Arc>) -> ImageFrame { + let mut render = SvgRender::new(); + + if footprint.transform.matrix2.determinant() == 0. { + log::trace!("Invalid footprint received for rasterization"); + return ImageFrame::default(); + } + let aabb = Bbox::from_transform(footprint.transform).to_axis_aligned_bbox(); + let size = aabb.size(); + let resolution = footprint.resolution; + let render_params = RenderParams { + culling_bounds: None, + ..Default::default() + }; + + *data.transform_mut() = DAffine2::from_translation(-aabb.start) * data.transform(); + data.render_svg(&mut render, &render_params); + render.format_svg(glam::DVec2::ZERO, size); + let svg_string = render.svg.to_svg_string(); + + let canvas = &surface_handle.surface; + canvas.set_width(resolution.x); + canvas.set_height(resolution.y); + + let context = canvas.get_context("2d").unwrap().unwrap().dyn_into::().unwrap(); + + let preamble = "data:image/svg+xml;base64,"; + let mut base64_string = String::with_capacity(preamble.len() + svg_string.len() * 4); + base64_string.push_str(preamble); + base64::engine::general_purpose::STANDARD.encode_string(svg_string, &mut base64_string); + + let image_data = web_sys::HtmlImageElement::new().unwrap(); + image_data.set_src(base64_string.as_str()); + wasm_bindgen_futures::JsFuture::from(image_data.decode()).await.unwrap(); + context + .draw_image_with_html_image_element_and_dw_and_dh(&image_data, 0., 0., resolution.x as f64, resolution.y as f64) + .unwrap(); + + let rasterized = context.get_image_data(0., 0., resolution.x as f64, resolution.y as f64).unwrap(); + + let image = Image::from_image_data(&rasterized.data().0, resolution.x as u32, resolution.y as u32); + ImageFrame { + image, + transform: footprint.transform, + ..Default::default() + } +} + // Render with the data node taking in Footprint. impl<'input, 'a: 'input, T: 'input + GraphicElementRendered, F: 'input + Future, Data: 'input, Surface: 'input, SurfaceFuture: 'input> Node<'input, WasmEditorApi<'a>> for RenderNode diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index e8310384d..2112a0a7f 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -80,6 +80,8 @@ macro_rules! register_node { macro_rules! async_node { // TODO: we currently need to annotate the type here because the compiler would otherwise (correctly) // assign a Pin>> type to the node, which is not what we want for now. + // + // This `params` variant of the macro wraps the normal `fn_params` variant and is used as a shorthand for writing `T` instead of `() => T` ($path:ty, input: $input:ty, output: $output:ty, params: [ $($type:ty),*]) => { async_node!($path, input: $input, output: $output, fn_params: [ $(() => $type),*]) }; @@ -686,18 +688,20 @@ fn node_registry() -> HashMap, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => GraphicGroup, () => Arc]), async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => Artboard, () => Arc]), async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => ArtboardGroup, () => Arc]), - async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [() => ImageFrame, () => Arc]), - async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [() => VectorData, () => Arc]), - async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [() => GraphicGroup, () => Arc]), - async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [() => Artboard, () => Arc]), - async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [() => bool, () => Arc]), - async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [() => f32, () => Arc]), - async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [() => f64, () => Arc]), - async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [() => String, () => Arc]), - async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [() => Option, () => Arc]), async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => Option, () => Arc]), - async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [() => Vec, () => Arc]), async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => Vec, () => Arc]), + async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, params: [ImageFrame, Arc]), + async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, params: [VectorData, Arc]), + async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, params: [GraphicGroup, Arc]), + async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, params: [Artboard, Arc]), + async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, params: [bool, Arc]), + async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, params: [f32, Arc]), + async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, params: [f64, Arc]), + async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, params: [String, Arc]), + async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, params: [Option, Arc]), + async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, params: [Vec, Arc]), + async_node!(graphene_std::wasm_application_io::RasterizeNode<_, _>, input: VectorData, output: ImageFrame, params: [Footprint, Arc]), + async_node!(graphene_std::wasm_application_io::RasterizeNode<_, _>, input: GraphicGroup, output: ImageFrame, params: [Footprint, Arc]), async_node!(graphene_core::transform::TransformNode<_, _, _, _, _, _>, input: Footprint, output: VectorData, fn_params: [Footprint => VectorData, () => DVec2, () => f64, () => DVec2, () => DVec2, () => DVec2]), async_node!(graphene_core::transform::TransformNode<_, _, _, _, _, _>, input: Footprint, output: WasmSurfaceHandleFrame, fn_params: [Footprint => WasmSurfaceHandleFrame, () => DVec2, () => f64, () => DVec2, () => DVec2, () => DVec2]), async_node!(graphene_core::transform::TransformNode<_, _, _, _, _, _>, input: Footprint, output: ImageFrame, fn_params: [Footprint => ImageFrame, () => DVec2, () => f64, () => DVec2, () => DVec2, () => DVec2]),