From 2f4aef34e507a7b4b8dbb7a0c7c613bf9b8929af Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sun, 10 Aug 2025 01:34:33 -0700 Subject: [PATCH] Add Table as a graphical type (#3033) * Reduce code duplication in bounding box impls on Table * Working Table rendering in the graph * Implement color and fix other rendering with Vello and polish --- .../document/node_graph/utility_types.rs | 14 +- .../spreadsheet_message_handler.rs | 7 + editor/src/node_graph_executor.rs | 54 +++--- editor/src/node_graph_executor/runtime.rs | 68 +++---- frontend/src/components/Editor.svelte | 16 +- frontend/src/messages.ts | 2 +- node-graph/gbrush/src/brush.rs | 5 +- node-graph/gcore/src/artboard.rs | 26 ++- node-graph/gcore/src/blending_nodes.rs | 31 ++++ node-graph/gcore/src/bounds.rs | 24 ++- node-graph/gcore/src/graphic.rs | 76 ++++++-- node-graph/gcore/src/raster_types.rs | 20 +-- node-graph/gcore/src/render_complexity.rs | 7 + node-graph/gcore/src/table.rs | 25 ++- node-graph/gcore/src/transform.rs | 8 +- node-graph/gcore/src/transform_nodes.rs | 6 +- .../gcore/src/vector/algorithms/instance.rs | 7 +- node-graph/gcore/src/vector/vector_nodes.rs | 35 ++-- node-graph/gcore/src/vector/vector_types.rs | 14 +- node-graph/gpath-bool/src/lib.rs | 15 ++ node-graph/graph-craft/src/document/value.rs | 1 + node-graph/gstd/src/wasm_application_io.rs | 22 ++- node-graph/gsvg-renderer/src/renderer.rs | 169 ++++++++++++++---- .../interpreted-executor/src/node_registry.rs | 8 +- 24 files changed, 462 insertions(+), 198 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/utility_types.rs b/editor/src/messages/portfolio/document/node_graph/utility_types.rs index 80a802d4..eaac2371 100644 --- a/editor/src/messages/portfolio/document/node_graph/utility_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/utility_types.rs @@ -8,18 +8,17 @@ use std::borrow::Cow; pub enum FrontendGraphDataType { #[default] General, + Number, + Artboard, + Graphic, Raster, Vector, - Number, - Graphic, - Artboard, + Color, } impl FrontendGraphDataType { pub fn from_type(input: &Type) -> Self { match TaggedValue::from_type_or_none(input) { - TaggedValue::Raster(_) => Self::Raster, - TaggedValue::Vector(_) => Self::Vector, TaggedValue::U32(_) | TaggedValue::U64(_) | TaggedValue::F64(_) @@ -28,8 +27,11 @@ impl FrontendGraphDataType { | TaggedValue::VecF64(_) | TaggedValue::VecDVec2(_) | TaggedValue::DAffine2(_) => Self::Number, - TaggedValue::Graphic(_) => Self::Graphic, TaggedValue::Artboard(_) => Self::Artboard, + TaggedValue::Graphic(_) => Self::Graphic, + TaggedValue::Raster(_) => Self::Raster, + TaggedValue::Vector(_) => Self::Vector, + TaggedValue::ColorTable(_) | TaggedValue::Color(_) | TaggedValue::OptionalColor(_) => Self::Color, _ => Self::General, } } diff --git a/editor/src/messages/portfolio/spreadsheet/spreadsheet_message_handler.rs b/editor/src/messages/portfolio/spreadsheet/spreadsheet_message_handler.rs index 64b82500..7a218be9 100644 --- a/editor/src/messages/portfolio/spreadsheet/spreadsheet_message_handler.rs +++ b/editor/src/messages/portfolio/spreadsheet/spreadsheet_message_handler.rs @@ -158,6 +158,7 @@ impl TableRowLayout for Graphic { Self::Vector(vector) => vector.identifier(), Self::RasterCPU(_) => "Raster (on CPU)".to_string(), Self::RasterGPU(_) => "Raster (on GPU)".to_string(), + Self::Color(_) => "Color".to_string(), } } // Don't put a breadcrumb for Graphic @@ -170,6 +171,12 @@ impl TableRowLayout for Graphic { Self::Vector(table) => table.layout_with_breadcrumb(data), Self::RasterCPU(_) => label("Raster is not supported"), Self::RasterGPU(_) => label("Raster is not supported"), + Self::Color(color) => { + let rows = vec![vec![ + TextLabel::new(format!("Colors:\n{}", color.iter().map(|color| color.element.to_rgba_hex_srgb()).collect::>().join("\n"))).widget_holder(), + ]]; + vec![LayoutGroup::Table { rows }] + } } } } diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index c395ae0d..e3aa0c40 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -15,6 +15,7 @@ use graphene_std::text::FontCache; use graphene_std::transform::Footprint; use graphene_std::vector::Vector; use graphene_std::vector::style::ViewMode; +use graphene_std::wasm_application_io::RenderOutputType; use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta; mod runtime_io; @@ -33,7 +34,7 @@ pub struct ExecutionResponse { execution_id: u64, result: Result, responses: VecDeque, - transform: DAffine2, + footprint: Footprint, vector_modify: HashMap, /// The resulting value from the temporary inspected during execution inspect_result: Option, @@ -223,7 +224,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: RenderOutputType::Svg { svg, .. }, .. }) = node_graph_output else { @@ -263,7 +264,7 @@ impl NodeGraphExecutor { execution_id, result, responses: existing_responses, - transform, + footprint, vector_modify, inspect_result, } = execution_response; @@ -286,9 +287,9 @@ impl NodeGraphExecutor { let execution_context = self.futures.remove(&execution_id).ok_or_else(|| "Invalid generation ID".to_string())?; if let Some(export_config) = execution_context.export_config { // Special handling for exporting the artwork - self.export(node_graph_output, export_config, responses)? + self.export(node_graph_output, export_config, responses)?; } else { - self.process_node_graph_output(node_graph_output, transform, responses)? + self.process_node_graph_output(node_graph_output, footprint, responses)?; } responses.add_front(DeferMessage::TriggerGraphRun(execution_id, execution_context.document_id)); @@ -332,12 +333,12 @@ impl NodeGraphExecutor { Ok(()) } - fn debug_render(render_object: impl Render, transform: DAffine2, responses: &mut VecDeque) { + fn debug_render(render_object: impl Render, footprint: Footprint, responses: &mut VecDeque) { // Setup rendering let mut render = SvgRender::new(); let render_params = RenderParams { view_mode: ViewMode::Normal, - culling_bounds: None, + footprint, thumbnail: false, hide_artboards: false, for_export: false, @@ -349,24 +350,25 @@ impl NodeGraphExecutor { render_object.render_svg(&mut render, &render_params); // Concatenate the defs and the SVG into one string - render.wrap_with_transform(transform, None); + render.wrap_with_transform(footprint.transform, None); let svg = render.svg.to_svg_string(); // Send to frontend responses.add(FrontendMessage::UpdateDocumentArtwork { svg }); } - fn process_node_graph_output(&mut self, node_graph_output: TaggedValue, transform: DAffine2, responses: &mut VecDeque) -> Result<(), String> { + fn process_node_graph_output(&mut self, node_graph_output: TaggedValue, footprint: Footprint, responses: &mut VecDeque) -> Result<(), String> { let mut render_output_metadata = RenderMetadata::default(); + match node_graph_output { TaggedValue::RenderOutput(render_output) => { match render_output.data { - graphene_std::wasm_application_io::RenderOutputType::Svg { svg, image_data } => { + RenderOutputType::Svg { svg, image_data } => { // Send to frontend responses.add(FrontendMessage::UpdateImageData { image_data }); responses.add(FrontendMessage::UpdateDocumentArtwork { svg }); } - graphene_std::wasm_application_io::RenderOutputType::CanvasFrame(frame) => { + RenderOutputType::CanvasFrame(frame) => { let matrix = format_transform_matrix(frame.transform); let transform = if matrix.is_empty() { String::new() } else { format!(" transform=\"{matrix}\"") }; let svg = format!( @@ -375,29 +377,23 @@ impl NodeGraphExecutor { ); responses.add(FrontendMessage::UpdateDocumentArtwork { svg }); } - graphene_std::wasm_application_io::RenderOutputType::Texture { .. } => {} - _ => { - return Err(format!("Invalid node graph output type: {:#?}", render_output.data)); - } + RenderOutputType::Texture { .. } => {} + _ => return Err(format!("Invalid node graph output type: {:#?}", render_output.data)), } render_output_metadata = render_output.metadata; } - TaggedValue::Bool(render_object) => Self::debug_render(render_object, transform, responses), - TaggedValue::String(render_object) => Self::debug_render(render_object, transform, responses), - TaggedValue::F64(render_object) => Self::debug_render(render_object, transform, responses), - TaggedValue::DVec2(render_object) => Self::debug_render(render_object, transform, responses), - TaggedValue::OptionalColor(render_object) => Self::debug_render(render_object, transform, responses), - TaggedValue::Vector(render_object) => Self::debug_render(render_object, transform, responses), - TaggedValue::Graphic(render_object) => Self::debug_render(render_object, transform, responses), - TaggedValue::Raster(render_object) => Self::debug_render(render_object, transform, responses), - TaggedValue::Palette(render_object) => Self::debug_render(render_object, transform, responses), - _ => { - return Err(format!("Invalid node graph output type: {node_graph_output:#?}")); - } + TaggedValue::Bool(render_object) => Self::debug_render(render_object, footprint, responses), + TaggedValue::F64(render_object) => Self::debug_render(render_object, footprint, responses), + TaggedValue::DVec2(render_object) => Self::debug_render(render_object, footprint, responses), + TaggedValue::String(render_object) => Self::debug_render(render_object, footprint, responses), + TaggedValue::OptionalColor(render_object) => Self::debug_render(render_object, footprint, responses), + TaggedValue::Palette(render_object) => Self::debug_render(render_object, footprint, responses), + _ => return Err(format!("Invalid node graph output type: {node_graph_output:#?}")), }; + let graphene_std::renderer::RenderMetadata { - upstream_footprints: footprints, + upstream_footprints, local_transforms, first_element_source_id, click_targets, @@ -406,7 +402,7 @@ impl NodeGraphExecutor { // Run these update state messages immediately responses.add(DocumentMessage::UpdateUpstreamTransforms { - upstream_footprints: footprints, + upstream_footprints, local_transforms, first_element_source_id, }); diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 052b26e8..f5c6ba04 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -8,11 +8,13 @@ use graph_craft::proto::GraphErrors; use graph_craft::wasm_application_io::EditorPreferences; use graph_craft::{ProtoNodeIdentifier, concrete}; use graphene_std::application_io::{ImageTexture, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig}; +use graphene_std::bounds::RenderBoundingBox; use graphene_std::memo::IORecord; use graphene_std::renderer::{Render, RenderParams, SvgRender}; use graphene_std::renderer::{RenderSvgSegmentList, SvgSegment}; use graphene_std::table::{Table, TableRow}; use graphene_std::text::FontCache; +use graphene_std::transform::RenderQuality; use graphene_std::vector::Vector; use graphene_std::vector::style::ViewMode; use graphene_std::wasm_application_io::{RenderOutputType, WasmApplicationIo, WasmEditorApi}; @@ -202,8 +204,6 @@ impl NodeRuntime { }); } GraphRuntimeRequest::ExecutionRequest(ExecutionRequest { execution_id, render_config, .. }) => { - let transform = render_config.viewport.transform; - let result = self.execute_network(render_config).await; let mut responses = VecDeque::new(); // TODO: Only process monitor nodes if the graph has changed, not when only the Footprint changes @@ -227,7 +227,7 @@ impl NodeRuntime { execution_id, result, responses, - transform, + footprint: render_config.viewport, vector_modify: self.vector_modify.clone(), inspect_result, }); @@ -292,51 +292,49 @@ impl NodeRuntime { if self.inspect_state.is_some_and(|inspect_state| monitor_node_path.last().copied() == Some(inspect_state.monitor_node)) { continue; } + // The monitor nodes are located within a document node, and are thus children in that network, so this gets the parent document node's ID let Some(parent_network_node_id) = monitor_node_path.len().checked_sub(2).and_then(|index| monitor_node_path.get(index)).copied() else { warn!("Monitor node has invalid node id"); - continue; }; - // Extract the monitor node's stored `Graphic` data. + // Extract the monitor node's stored `Graphic` data let Ok(introspected_data) = self.executor.introspect(monitor_node_path) else { // TODO: Fix the root of the issue causing the spam of this warning (this at least temporarily disables it in release builds) #[cfg(debug_assertions)] warn!("Failed to introspect monitor node {}", self.executor.introspect(monitor_node_path).unwrap_err()); - continue; }; + // Graphic table: thumbnail if let Some(io) = introspected_data.downcast_ref::>>() { - Self::process_graphic(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses, update_thumbnails) - } else if let Some(io) = introspected_data.downcast_ref::>>() { - Self::process_graphic(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses, update_thumbnails) - // Insert the vector modify if we are dealing with vector data - } else if let Some(record) = introspected_data.downcast_ref::>>() { + if update_thumbnails { + Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses) + } + } + // Artboard table: thumbnail + else if let Some(io) = introspected_data.downcast_ref::>>() { + if update_thumbnails { + Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses) + } + } + // Vector table: vector modifications + else if let Some(io) = introspected_data.downcast_ref::>>() { + // Insert the vector modify let default = TableRow::default(); self.vector_modify - .insert(parent_network_node_id, record.output.iter().next().unwrap_or_else(|| default.as_ref()).element.clone()); - } else { + .insert(parent_network_node_id, io.output.iter().next().unwrap_or_else(|| default.as_ref()).element.clone()); + } + // Other + else { log::warn!("Failed to downcast monitor node output {parent_network_node_id:?}"); } } } - // If this is `Graphic` data, regenerate click targets and thumbnails for the layers in the graph, modifying the state and updating the UI. - fn process_graphic( - thumbnail_renders: &mut HashMap>, - parent_network_node_id: NodeId, - graphic: &impl Render, - responses: &mut VecDeque, - update_thumbnails: bool, - ) { - // RENDER THUMBNAIL - - if !update_thumbnails { - return; - } - + /// If this is `Graphic` data, regenerate click targets and thumbnails for the layers in the graph, modifying the state and updating the UI. + fn render_thumbnail(thumbnail_renders: &mut HashMap>, parent_network_node_id: NodeId, graphic: &impl Render, responses: &mut VecDeque) { // Skip thumbnails if the layer is too complex (for performance) if graphic.render_complexity() > 1000 { let old = thumbnail_renders.insert(parent_network_node_id, Vec::new()); @@ -349,12 +347,21 @@ impl NodeRuntime { return; } - let bounds = graphic.bounding_box(DAffine2::IDENTITY, true); + let bounds = match graphic.bounding_box(DAffine2::IDENTITY, true) { + RenderBoundingBox::None => return, + RenderBoundingBox::Infinite => [DVec2::ZERO, DVec2::new(300., 200.)], + RenderBoundingBox::Rectangle(bounds) => bounds, + }; + let footprint = Footprint { + transform: DAffine2::from_translation(DVec2::new(bounds[0].x, bounds[0].y)), + resolution: UVec2::new((bounds[1].x - bounds[0].x).abs() as u32, (bounds[1].y - bounds[0].y).abs() as u32), + quality: RenderQuality::Full, + }; // Render the thumbnail from a `Graphic` into an SVG string let render_params = RenderParams { view_mode: ViewMode::Normal, - culling_bounds: bounds, + footprint, thumbnail: true, hide_artboards: false, for_export: false, @@ -365,8 +372,7 @@ impl NodeRuntime { graphic.render_svg(&mut render, &render_params); // And give the SVG a viewbox and outer ... wrapper tag - let [min, max] = bounds.unwrap_or_default(); - render.format_svg(min, max); + render.format_svg(bounds[0], bounds[1]); // UPDATE FRONTEND THUMBNAIL diff --git a/frontend/src/components/Editor.svelte b/frontend/src/components/Editor.svelte index 043c6dd4..68aed47c 100644 --- a/frontend/src/components/Editor.svelte +++ b/frontend/src/components/Editor.svelte @@ -113,16 +113,18 @@ --color-data-general: #cfcfcf; --color-data-general-dim: #8a8a8a; + --color-data-number: #c9a699; + --color-data-number-dim: #886b60; + --color-data-artboard: #fbf9eb; + --color-data-artboard-dim: #b9b9a9; + --color-data-graphic: #66b195; + --color-data-graphic-dim: #3d725e; --color-data-raster: #e4bb72; --color-data-raster-dim: #8b7752; --color-data-vector: #65bbe5; - --color-data-vector-dim: #4b778c; - --color-data-graphic: #66b195; - --color-data-graphic-dim: #3d725e; - --color-data-artboard: #fbf9eb; - --color-data-artboard-dim: #b9b9a9; - --color-data-number: #c9a699; - --color-data-number-dim: #886b60; + --color-data-vector-dim: #417892; + --color-data-color: #af81eb; + --color-data-color-dim: #6c489b; --color-none: white; --color-none-repeat: no-repeat; diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index c68bc115..157c46d0 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -192,7 +192,7 @@ export type ContextMenuInformation = { contextMenuData: "CreateNode" | { type: "CreateNode"; compatibleType: string } | { nodeId: bigint; currentlyIsNode: boolean }; }; -export type FrontendGraphDataType = "General" | "Raster" | "Vector" | "Number" | "Graphic" | "Artboard"; +export type FrontendGraphDataType = "General" | "Number" | "Artboard" | "Graphic" | "Raster" | "Vector" | "Color"; export class Node { readonly index!: bigint; diff --git a/node-graph/gbrush/src/brush.rs b/node-graph/gbrush/src/brush.rs index 114f2b44..23368453 100644 --- a/node-graph/gbrush/src/brush.rs +++ b/node-graph/gbrush/src/brush.rs @@ -2,7 +2,7 @@ use crate::brush_cache::BrushCache; use crate::brush_stroke::{BrushStroke, BrushStyle}; use glam::{DAffine2, DVec2}; use graphene_core::blending::BlendMode; -use graphene_core::bounds::BoundingBox; +use graphene_core::bounds::{BoundingBox, RenderBoundingBox}; use graphene_core::color::{Alpha, Color, Pixel, Sample}; use graphene_core::generic::FnNode; use graphene_core::math::bbox::{AxisAlignedBbox, Bbox}; @@ -186,7 +186,8 @@ async fn brush(_: impl Ctx, mut image_frame_table: Table>, strokes: // TODO: Find a way to handle more than one row let table_row = image_frame_table.iter().next().expect("Expected the one row we just pushed").into_cloned(); - let [start, end] = Table::new_from_row(table_row.clone()).bounding_box(DAffine2::IDENTITY, false).unwrap_or([DVec2::ZERO, DVec2::ZERO]); + let bounds = Table::new_from_row(table_row.clone()).bounding_box(DAffine2::IDENTITY, false); + let [start, end] = if let RenderBoundingBox::Rectangle(rect) = bounds { rect } else { [DVec2::ZERO, DVec2::ZERO] }; let image_bbox = AxisAlignedBbox { start, end }; let stroke_bbox = strokes.iter().map(|s| s.bounding_box()).reduce(|a, b| a.union(&b)).unwrap_or(AxisAlignedBbox::ZERO); let bbox = if image_bbox.size().length() < 0.1 { stroke_bbox } else { stroke_bbox.union(&image_bbox) }; diff --git a/node-graph/gcore/src/artboard.rs b/node-graph/gcore/src/artboard.rs index c8e3edb6..f3350b4c 100644 --- a/node-graph/gcore/src/artboard.rs +++ b/node-graph/gcore/src/artboard.rs @@ -1,5 +1,5 @@ use crate::blending::AlphaBlending; -use crate::bounds::BoundingBox; +use crate::bounds::{BoundingBox, RenderBoundingBox}; use crate::math::quad::Quad; use crate::raster_types::{CPU, GPU, Raster}; use crate::table::{Table, TableRow}; @@ -42,15 +42,16 @@ impl Artboard { } impl BoundingBox for Artboard { - fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]> { - let artboard_bounds = (transform * Quad::from_box([self.location.as_dvec2(), self.location.as_dvec2() + self.dimensions.as_dvec2()])).bounding_box(); + fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox { + let artboard_bounds = || (transform * Quad::from_box([self.location.as_dvec2(), self.location.as_dvec2() + self.dimensions.as_dvec2()])).bounding_box(); + if self.clip { - Some(artboard_bounds) - } else { - [self.content.bounding_box(transform, include_stroke), Some(artboard_bounds)] - .into_iter() - .flatten() - .reduce(Quad::combine_bounds) + return RenderBoundingBox::Rectangle(artboard_bounds()); + } + + match self.content.bounding_box(transform, include_stroke) { + RenderBoundingBox::Rectangle(content_bounds) => RenderBoundingBox::Rectangle(Quad::combine_bounds(content_bounds, artboard_bounds())), + other => other, } } } @@ -88,12 +89,6 @@ pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Re }) } -impl BoundingBox for Table { - fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]> { - self.iter().filter_map(|row| row.element.bounding_box(transform, include_stroke)).reduce(Quad::combine_bounds) - } -} - #[node_macro::node(category(""))] async fn create_artboard> + 'n>( ctx: impl ExtractAll + CloneVarArgs + Ctx, @@ -102,6 +97,7 @@ async fn create_artboard> + 'n>( Context -> Table, Context -> Table>, Context -> Table>, + Context -> Table, Context -> DAffine2, )] content: impl Node, Output = T>, diff --git a/node-graph/gcore/src/blending_nodes.rs b/node-graph/gcore/src/blending_nodes.rs index 105c44a8..6e123c24 100644 --- a/node-graph/gcore/src/blending_nodes.rs +++ b/node-graph/gcore/src/blending_nodes.rs @@ -34,6 +34,13 @@ impl MultiplyAlpha for Table> { } } } +impl MultiplyAlpha for Table { + fn multiply_alpha(&mut self, factor: f64) { + for row in self.iter_mut() { + row.alpha_blending.opacity *= factor as f32; + } + } +} pub(super) trait MultiplyFill { fn multiply_fill(&mut self, factor: f64); @@ -64,6 +71,13 @@ impl MultiplyFill for Table> { } } } +impl MultiplyFill for Table { + fn multiply_fill(&mut self, factor: f64) { + for row in self.iter_mut() { + row.alpha_blending.fill *= factor as f32; + } + } +} trait SetBlendMode { fn set_blend_mode(&mut self, blend_mode: BlendMode); @@ -90,6 +104,13 @@ impl SetBlendMode for Table> { } } } +impl SetBlendMode for Table { + fn set_blend_mode(&mut self, blend_mode: BlendMode) { + for row in self.iter_mut() { + row.alpha_blending.blend_mode = blend_mode; + } + } +} trait SetClip { fn set_clip(&mut self, clip: bool); @@ -116,6 +137,13 @@ impl SetClip for Table> { } } } +impl SetClip for Table { + fn set_clip(&mut self, clip: bool) { + for row in self.iter_mut() { + row.alpha_blending.clip = clip; + } + } +} #[node_macro::node(category("Style"))] fn blend_mode( @@ -124,6 +152,7 @@ fn blend_mode( Table, Table, Table>, + Table, )] mut value: T, blend_mode: BlendMode, @@ -140,6 +169,7 @@ fn opacity( Table, Table, Table>, + Table, )] mut value: T, #[default(100.)] opacity: Percentage, @@ -156,6 +186,7 @@ fn blending( Table, Table, Table>, + Table, )] mut value: T, blend_mode: BlendMode, diff --git a/node-graph/gcore/src/bounds.rs b/node-graph/gcore/src/bounds.rs index ccc373d4..6523d5f3 100644 --- a/node-graph/gcore/src/bounds.rs +++ b/node-graph/gcore/src/bounds.rs @@ -1,15 +1,23 @@ use crate::Color; use glam::{DAffine2, DVec2}; +#[derive(Clone, Copy, Default, Debug, PartialEq)] +pub enum RenderBoundingBox { + #[default] + None, + Infinite, + Rectangle([DVec2; 2]), +} + pub trait BoundingBox { - fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]>; + fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox; } macro_rules! none_impl { ($t:path) => { impl BoundingBox for $t { - fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> { - None + fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox { + RenderBoundingBox::None } } }; @@ -20,5 +28,11 @@ none_impl!(bool); none_impl!(f32); none_impl!(f64); none_impl!(DVec2); -none_impl!(Option); -none_impl!(Vec); +none_impl!(Option); // TODO: Remove this? +none_impl!(Vec); // TODO: Remove this? + +impl BoundingBox for Color { + fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox { + RenderBoundingBox::Infinite + } +} diff --git a/node-graph/gcore/src/graphic.rs b/node-graph/gcore/src/graphic.rs index da621712..38fbbb60 100644 --- a/node-graph/gcore/src/graphic.rs +++ b/node-graph/gcore/src/graphic.rs @@ -1,6 +1,5 @@ use crate::blending::AlphaBlending; -use crate::bounds::BoundingBox; -use crate::math::quad::Quad; +use crate::bounds::{BoundingBox, RenderBoundingBox}; use crate::raster_types::{CPU, GPU, Raster}; use crate::table::{Table, TableRow}; use crate::uuid::NodeId; @@ -17,11 +16,12 @@ pub enum Graphic { Vector(Table), RasterCPU(Table>), RasterGPU(Table>), + Color(Table), } impl Default for Graphic { fn default() -> Self { - Self::Graphic(Default::default()) + Self::Graphic(Table::new()) } } @@ -98,6 +98,48 @@ impl From>> for Table { } } +// Color +impl From for Graphic { + fn from(color: Color) -> Self { + Graphic::Color(Table::new_from_element(color)) + } +} +impl From> for Graphic { + fn from(color: Table) -> Self { + Graphic::Color(color) + } +} +impl From for Table { + fn from(color: Color) -> Self { + Table::new_from_element(Graphic::Color(Table::new_from_element(color))) + } +} +impl From> for Table { + fn from(color: Table) -> Self { + Table::new_from_element(Graphic::Color(color)) + } +} + +// Option +impl From> for Graphic { + fn from(color: Option) -> Self { + if let Some(color) = color { + Graphic::Color(Table::new_from_element(color)) + } else { + Graphic::default() + } + } +} +impl From> for Table { + fn from(color: Option) -> Self { + if let Some(color) = color { + Table::new_from_element(Graphic::Color(Table::new_from_element(color))) + } else { + Table::new() + } + } +} + // DAffine2 impl From for Graphic { fn from(_: DAffine2) -> Self { @@ -159,6 +201,7 @@ impl Graphic { Graphic::Graphic(graphic) => graphic.iter().all(|row| row.alpha_blending.clip), Graphic::RasterCPU(raster) => raster.iter().all(|row| row.alpha_blending.clip), Graphic::RasterGPU(raster) => raster.iter().all(|row| row.alpha_blending.clip), + Graphic::Color(color) => color.iter().all(|row| row.alpha_blending.clip), } } @@ -175,28 +218,21 @@ impl Graphic { } impl BoundingBox for Graphic { - fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]> { + fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox { match self { Graphic::Vector(vector) => vector.bounding_box(transform, include_stroke), Graphic::RasterCPU(raster) => raster.bounding_box(transform, include_stroke), Graphic::RasterGPU(raster) => raster.bounding_box(transform, include_stroke), Graphic::Graphic(graphic) => graphic.bounding_box(transform, include_stroke), + Graphic::Color(color) => color.bounding_box(transform, include_stroke), } } } -impl BoundingBox for Table { - fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]> { - self.iter() - .filter_map(|element| element.element.bounding_box(transform * *element.transform, include_stroke)) - .reduce(Quad::combine_bounds) - } -} - #[node_macro::node(category(""))] async fn source_node_id( _: impl Ctx, - #[implementations(Table, Table, Table, Table>, Table>)] content: Table, + #[implementations(Table, Table, Table, Table>, Table>, Table)] content: Table, node_path: Vec, ) -> Table { // Get the penultimate element of the node path, or None if the path is too short @@ -216,11 +252,11 @@ async fn source_node_id( async fn extend( _: impl Ctx, /// The table whose rows will appear at the start of the extended table. - #[implementations(Table, Table, Table, Table>, Table>)] + #[implementations(Table, Table, Table, Table>, Table>, Table)] base: Table, /// The table whose rows will appear at the end of the extended table. #[expose] - #[implementations(Table, Table, Table, Table>, Table>)] + #[implementations(Table, Table, Table, Table>, Table>, Table)] new: Table, ) -> Table { let mut base = base; @@ -233,9 +269,9 @@ async fn extend( #[node_macro::node(category(""))] async fn legacy_layer_extend( _: impl Ctx, - #[implementations(Table, Table, Table, Table>, Table>)] base: Table, + #[implementations(Table, Table, Table, Table>, Table>, Table)] base: Table, #[expose] - #[implementations(Table, Table, Table, Table>, Table>)] + #[implementations(Table, Table, Table, Table>, Table>, Table)] new: Table, nested_node_path: Vec, ) -> Table { @@ -260,6 +296,9 @@ async fn wrap_graphic + 'n>( Table, Table>, Table>, + Table, + Color, + Option, DAffine2, )] content: T, @@ -277,6 +316,9 @@ async fn to_graphic> + 'n>( Table, Table>, Table>, + Table, + Color, + Option, )] content: T, ) -> Table { diff --git a/node-graph/gcore/src/raster_types.rs b/node-graph/gcore/src/raster_types.rs index 3e8fe1bb..97dd1381 100644 --- a/node-graph/gcore/src/raster_types.rs +++ b/node-graph/gcore/src/raster_types.rs @@ -1,8 +1,7 @@ use crate::Color; -use crate::bounds::BoundingBox; +use crate::bounds::{BoundingBox, RenderBoundingBox}; use crate::math::quad::Quad; use crate::raster::Image; -use crate::table::Table; use core::ops::Deref; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; @@ -199,17 +198,16 @@ mod gpu_common { } } -impl BoundingBox for Table> +impl BoundingBox for Raster where Raster: Storage, { - fn bounding_box(&self, transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> { - self.iter() - .filter(|row| !row.element.is_empty()) // Eliminate empty images - .flat_map(|row| { - let transform = transform * *row.transform; - (transform.matrix2.determinant() != 0.).then(|| (transform * Quad::from_box([DVec2::ZERO, DVec2::ONE])).bounding_box()) - }) - .reduce(Quad::combine_bounds) + fn bounding_box(&self, transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox { + if self.is_empty() || transform.matrix2.determinant() == 0. { + return RenderBoundingBox::None; + } + + let unit_rectangle = Quad::from_box([DVec2::ZERO, DVec2::ONE]); + RenderBoundingBox::Rectangle((transform * unit_rectangle).bounding_box()) } } diff --git a/node-graph/gcore/src/render_complexity.rs b/node-graph/gcore/src/render_complexity.rs index 882322cf..267abd0f 100644 --- a/node-graph/gcore/src/render_complexity.rs +++ b/node-graph/gcore/src/render_complexity.rs @@ -29,6 +29,7 @@ impl RenderComplexity for Graphic { Self::Vector(table) => table.render_complexity(), Self::RasterCPU(table) => table.render_complexity(), Self::RasterGPU(table) => table.render_complexity(), + Self::Color(table) => table.render_complexity(), } } } @@ -52,6 +53,12 @@ impl RenderComplexity for Raster { } } +impl RenderComplexity for Color { + fn render_complexity(&self) -> usize { + 1 + } +} + impl RenderComplexity for String {} impl RenderComplexity for bool {} impl RenderComplexity for f32 {} diff --git a/node-graph/gcore/src/table.rs b/node-graph/gcore/src/table.rs index 72be7591..9d3fdffa 100644 --- a/node-graph/gcore/src/table.rs +++ b/node-graph/gcore/src/table.rs @@ -1,6 +1,7 @@ -use crate::AlphaBlending; +use crate::bounds::{BoundingBox, RenderBoundingBox}; use crate::transform::ApplyTransform; use crate::uuid::NodeId; +use crate::{AlphaBlending, math::quad::Quad}; use dyn_any::StaticType; use glam::DAffine2; use std::hash::Hash; @@ -125,6 +126,28 @@ impl Table { } } +impl BoundingBox for Table { + fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox { + let mut combined_bounds = None; + + for row in self.iter() { + match row.element.bounding_box(transform * *row.transform, include_stroke) { + RenderBoundingBox::None => continue, + RenderBoundingBox::Infinite => return RenderBoundingBox::Infinite, + RenderBoundingBox::Rectangle(bounds) => match combined_bounds { + Some(existing) => combined_bounds = Some(Quad::combine_bounds(existing, bounds)), + None => combined_bounds = Some(bounds), + }, + } + } + + match combined_bounds { + Some(bounds) => RenderBoundingBox::Rectangle(bounds), + None => RenderBoundingBox::None, + } + } +} + impl IntoIterator for Table { type Item = TableRow; type IntoIter = TableRowIter; diff --git a/node-graph/gcore/src/transform.rs b/node-graph/gcore/src/transform.rs index 4d5f5b5d..141fa69c 100644 --- a/node-graph/gcore/src/transform.rs +++ b/node-graph/gcore/src/transform.rs @@ -2,7 +2,7 @@ use crate::Artboard; use crate::math::bbox::AxisAlignedBbox; pub use crate::vector::ReferencePoint; use core::f64; -use glam::{DAffine2, DMat2, DVec2}; +use glam::{DAffine2, DMat2, DVec2, UVec2}; pub trait Transform { fn transform(&self) -> DAffine2; @@ -89,7 +89,7 @@ pub struct Footprint { /// Inverse of the transform which will be applied to the node output during the rendering process pub transform: DAffine2, /// Resolution of the target output area in pixels - pub resolution: glam::UVec2, + pub resolution: UVec2, /// Quality of the render, this may be used by caching nodes to decide if the cached render is sufficient pub quality: RenderQuality, } @@ -103,7 +103,7 @@ impl Default for Footprint { impl Footprint { pub const DEFAULT: Self = Self { transform: DAffine2::IDENTITY, - resolution: glam::UVec2::new(1920, 1080), + resolution: UVec2::new(1920, 1080), quality: RenderQuality::Full, }; @@ -112,7 +112,7 @@ impl Footprint { matrix2: DMat2::from_diagonal(DVec2::splat(f64::INFINITY)), translation: DVec2::ZERO, }, - resolution: glam::UVec2::new(0, 0), + resolution: UVec2::ZERO, quality: RenderQuality::Full, }; diff --git a/node-graph/gcore/src/transform_nodes.rs b/node-graph/gcore/src/transform_nodes.rs index 67313930..71cdbd9f 100644 --- a/node-graph/gcore/src/transform_nodes.rs +++ b/node-graph/gcore/src/transform_nodes.rs @@ -5,6 +5,7 @@ use crate::vector::Vector; use crate::{CloneVarArgs, Context, Ctx, ExtractAll, Graphic, OwnedContextImpl}; use core::f64; use glam::{DAffine2, DVec2}; +use graphene_core_shaders::color::Color; #[node_macro::node(category(""))] async fn transform( @@ -43,7 +44,7 @@ async fn transform( #[node_macro::node(category(""))] fn replace_transform( _: impl Ctx, - #[implementations(Table, Table>, Table)] mut data: Table, + #[implementations(Table, Table>, Table, Table)] mut data: Table, #[implementations(DAffine2)] transform: TransformInput, ) -> Table { for data_transform in data.iter_mut() { @@ -60,6 +61,7 @@ async fn extract_transform( Table, Table>, Table>, + Table, )] vector: Table, ) -> DAffine2 { @@ -94,6 +96,7 @@ async fn boundless_footprint( Context -> Table, Context -> Table>, Context -> Table>, + Context -> Table, Context -> String, Context -> f64, )] @@ -112,6 +115,7 @@ async fn freeze_real_time( Context -> Table, Context -> Table>, Context -> Table>, + Context -> Table, Context -> String, Context -> f64, )] diff --git a/node-graph/gcore/src/vector/algorithms/instance.rs b/node-graph/gcore/src/vector/algorithms/instance.rs index cf2e4b73..f72aefd6 100644 --- a/node-graph/gcore/src/vector/algorithms/instance.rs +++ b/node-graph/gcore/src/vector/algorithms/instance.rs @@ -3,6 +3,7 @@ use crate::table::{Table, TableRowRef}; use crate::vector::Vector; use crate::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractIndex, ExtractVarArgs, Graphic, OwnedContextImpl}; use glam::DVec2; +use graphene_core_shaders::color::Color; #[node_macro::node(name("Instance on Points"), category("Instancing"), path(graphene_core::vector))] async fn instance_on_points + Default + Send + Clone + 'static>( @@ -11,7 +12,8 @@ async fn instance_on_points + Default + Send + Clone + 'static> #[implementations( Context -> Table, Context -> Table, - Context -> Table> + Context -> Table>, + Context -> Table, )] instance: impl Node<'n, Context<'static>, Output = Table>, reverse: bool, @@ -52,7 +54,8 @@ async fn instance_repeat + Default + Send + Clone + 'static>( #[implementations( Context -> Table, Context -> Table, - Context -> Table> + Context -> Table>, + Context -> Table, )] instance: impl Node<'n, Context<'static>, Output = Table>, #[default(1)] count: u64, diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index e130d7db..9b6a9088 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -4,7 +4,7 @@ use super::algorithms::spline::{solve_spline_first_handle_closed, solve_spline_f use super::misc::{CentroidType, bezpath_from_manipulator_groups, bezpath_to_manipulator_groups, point_to_dvec2}; use super::style::{Fill, Gradient, GradientStops, Stroke}; use super::{PointId, SegmentDomain, SegmentId, StrokeId, Vector, VectorExt}; -use crate::bounds::BoundingBox; +use crate::bounds::{BoundingBox, RenderBoundingBox}; use crate::raster_types::{CPU, GPU, Raster}; use crate::registry::types::{Angle, Fraction, IntegerCount, Length, Multiplier, Percentage, PixelLength, PixelSize, SeedValue}; use crate::table::{Table, TableRow, TableRowMut}; @@ -106,7 +106,7 @@ where } #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("fill_properties"))] -async fn fill + 'n + Send, V>( +async fn fill + 'n + Send, V: VectorTableIterMut + 'n + Send>( _: impl Ctx, #[implementations( Table, @@ -116,7 +116,7 @@ async fn fill + 'n + Send, V>( Table, Table, Table, - Table + Table, )] /// The content with vector paths to apply the fill style to. mut content: V, @@ -135,10 +135,7 @@ async fn fill + 'n + Send, V>( fill: F, _backup_color: Option, _backup_gradient: Gradient, -) -> V -where - V: VectorTableIterMut + 'n + Send, -{ +) -> V { let fill: Fill = fill.into(); for vector in content.vector_iter_mut() { let mut fill = fill.clone(); @@ -219,7 +216,7 @@ where async fn repeat( _: impl Ctx, // TODO: Implement other graphical types. - #[implementations(Table, Table, Table>)] instance: Table, + #[implementations(Table, Table, Table>, Table)] instance: Table, #[default(100., 100.)] // TODO: When using a custom Properties panel layout in document_node_definitions.rs and this default is set, the widget weirdly doesn't show up in the Properties panel. Investigation is needed. direction: PixelSize, @@ -255,7 +252,7 @@ async fn repeat( async fn circular_repeat( _: impl Ctx, // TODO: Implement other graphical types. - #[implementations(Table, Table, Table>)] instance: Table, + #[implementations(Table, Table, Table>, Table)] instance: Table, angle_offset: Angle, #[unit(" px")] #[default(5)] @@ -291,7 +288,7 @@ async fn copy_to_points( points: Table, #[expose] /// Artwork to be copied and placed at each point. - #[implementations(Table, Table, Table>)] + #[implementations(Table, Table, Table>, Table)] instance: Table, /// Minimum range of randomized sizes given to each instance. #[default(1)] @@ -366,7 +363,7 @@ async fn copy_to_points( #[node_macro::node(category("Instancing"), path(graphene_core::vector))] async fn mirror( _: impl Ctx, - #[implementations(Table, Table, Table>)] instance: Table, + #[implementations(Table, Table, Table>, Table)] content: Table, #[default(ReferencePoint::Center)] relative_to_bounds: ReferencePoint, #[unit(" px")] offset: f64, #[range((-90., 90.))] angle: Angle, @@ -375,14 +372,12 @@ async fn mirror( where Table: BoundingBox, { - let mut result_table = Table::new(); - // Normalize the direction vector let normal = DVec2::from_angle(angle.to_radians()); - // The mirror reference is based on the bounding box (at least for now, until we have proper local layer origins) - let Some(bounding_box) = instance.bounding_box(DAffine2::IDENTITY, false) else { - return result_table; + // The mirror reference may be based on the bounding box if an explicit reference point is chosen + let RenderBoundingBox::Rectangle(bounding_box) = content.bounding_box(DAffine2::IDENTITY, false) else { + return content; }; let reference_point_location = relative_to_bounds.point_in_bounding_box((bounding_box[0], bounding_box[1]).into()); @@ -404,15 +399,17 @@ where reflection * DAffine2::from_translation(DVec2::from_angle(angle.to_radians()) * DVec2::splat(-offset)) }; + let mut result_table = Table::new(); + // Add original instance depending on the keep_original flag if keep_original { - for instance in instance.clone().into_iter() { + for instance in content.clone().into_iter() { result_table.push(instance); } } // Create and add mirrored instance - for mut row in instance.into_iter() { + for mut row in content.into_iter() { row.transform = reflected_transform * row.transform; result_table.push(row); } @@ -1901,7 +1898,7 @@ fn point_inside(_: impl Ctx, source: Table, point: DVec2) -> bool { } #[node_macro::node(category("General"), path(graphene_core::vector))] -async fn count_elements(_: impl Ctx, #[implementations(Table, Table, Table>, Table>)] source: Table) -> u64 { +async fn count_elements(_: impl Ctx, #[implementations(Table, Table, Table>, Table>, Table)] source: Table) -> u64 { source.len() as u64 } diff --git a/node-graph/gcore/src/vector/vector_types.rs b/node-graph/gcore/src/vector/vector_types.rs index bd63d00a..e3a7b098 100644 --- a/node-graph/gcore/src/vector/vector_types.rs +++ b/node-graph/gcore/src/vector/vector_types.rs @@ -2,7 +2,7 @@ use super::misc::dvec2_to_point; use super::style::{PathStyle, Stroke}; pub use super::vector_attributes::*; pub use super::vector_modification::*; -use crate::bounds::BoundingBox; +use crate::bounds::{BoundingBox, RenderBoundingBox}; use crate::math::quad::Quad; use crate::table::Table; use crate::transform::Transform; @@ -437,8 +437,9 @@ impl Vector { } impl BoundingBox for Table { - fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]> { - self.iter() + fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox { + let bounds = self + .iter() .flat_map(|row| { if !include_stroke { return row.element.bounding_box_with_transform(transform * *row.transform); @@ -455,7 +456,12 @@ impl BoundingBox for Table { row.element.bounding_box_with_transform(transform * *row.transform).map(|[a, b]| [a - offset, b + offset]) }) - .reduce(Quad::combine_bounds) + .reduce(Quad::combine_bounds); + + match bounds { + Some(bounds) => RenderBoundingBox::Rectangle(bounds), + None => RenderBoundingBox::None, + } } } diff --git a/node-graph/gpath-bool/src/lib.rs b/node-graph/gpath-bool/src/lib.rs index f16c1b75..f9931b30 100644 --- a/node-graph/gpath-bool/src/lib.rs +++ b/node-graph/gpath-bool/src/lib.rs @@ -280,6 +280,21 @@ fn flatten_vector(graphic_table: &Table) -> Table { unioned.into_iter().collect::>() } + Graphic::Color(color) => color + .into_iter() + .map(|row| { + let mut element = Vector::default(); + element.style.set_fill(Fill::Solid(row.element)); + element.style.set_stroke_transform(DAffine2::IDENTITY); + + TableRow { + element, + transform: row.transform, + alpha_blending: row.alpha_blending, + source_node_id: row.source_node_id, + } + }) + .collect::>(), } }) .collect() diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index f5feab1d..e12330a0 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -193,6 +193,7 @@ tagged_value! { #[cfg_attr(target_family = "wasm", serde(deserialize_with = "graphene_core::artboard::migrate_artboard"))] // TODO: Eventually remove this migration document upgrade code #[serde(alias = "ArtboardGroup")] Artboard(Table), + ColorTable(Table), // TODO: Rename to Color // ============ // STRUCT TYPES // ============ diff --git a/node-graph/gstd/src/wasm_application_io.rs b/node-graph/gstd/src/wasm_application_io.rs index 16d366af..f4769567 100644 --- a/node-graph/gstd/src/wasm_application_io.rs +++ b/node-graph/gstd/src/wasm_application_io.rs @@ -8,6 +8,7 @@ use graphene_core::math::bbox::Bbox; use graphene_core::raster::image::Image; use graphene_core::raster_types::{CPU, Raster}; use graphene_core::table::Table; +#[cfg(target_family = "wasm")] use graphene_core::transform::Footprint; use graphene_core::vector::Vector; use graphene_core::{Color, Context, Ctx, ExtractFootprint, Graphic, OwnedContextImpl, WasmNotSend}; @@ -137,7 +138,9 @@ fn decode_image(_: impl Ctx, data: Arc<[u8]>) -> Table> { Table::new_from_element(Raster::new_cpu(image)) } -fn render_svg(data: impl Render, mut render: SvgRender, render_params: RenderParams, footprint: Footprint) -> RenderOutputType { +fn render_svg(data: impl Render, mut render: SvgRender, render_params: RenderParams) -> RenderOutputType { + let footprint = render_params.footprint; + if !data.contains_artboard() && !render_params.hide_artboards { render.leaf_tag("rect", |attributes| { attributes.push("x", "0"); @@ -217,6 +220,7 @@ async fn rasterize( Table, Table>, Table, + Table, )] mut data: Table, footprint: Footprint, @@ -237,7 +241,7 @@ where let size = aabb.size(); let resolution = footprint.resolution; let render_params = RenderParams { - culling_bounds: None, + footprint, for_export: true, ..Default::default() }; @@ -282,11 +286,11 @@ async fn render<'a: 'n, T: 'n + Render + WasmNotSend>( render_config: RenderConfig, editor_api: impl Node, Output = &'a WasmEditorApi>, #[implementations( + Context -> Table, + Context -> Table, Context -> Table, Context -> Table>, - Context -> Table, - Context -> Table, - Context -> Artboard, + Context -> Table, Context -> Option, Context -> Vec, Context -> bool, @@ -308,7 +312,7 @@ async fn render<'a: 'n, T: 'n + Render + WasmNotSend>( let RenderConfig { hide_artboards, for_export, .. } = render_config; let render_params = RenderParams { view_mode: render_config.view_mode, - culling_bounds: None, + footprint, thumbnail: false, hide_artboards, for_export, @@ -333,7 +337,7 @@ async fn render<'a: 'n, T: 'n + Render + 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), ExportFormat::Canvas => { if use_vello && editor_api.application_io.as_ref().unwrap().gpu_executor().is_some() { #[cfg(all(feature = "vello", not(test)))] @@ -342,9 +346,9 @@ async fn render<'a: 'n, T: 'n + Render + WasmNotSend>( metadata, }; #[cfg(any(not(feature = "vello"), test))] - render_svg(data, SvgRender::new(), render_params, footprint) + render_svg(data, SvgRender::new(), render_params) } else { - render_svg(data, SvgRender::new(), render_params, footprint) + render_svg(data, SvgRender::new(), render_params) } } _ => 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 5bfdbef6..c7f19faa 100644 --- a/node-graph/gsvg-renderer/src/renderer.rs +++ b/node-graph/gsvg-renderer/src/renderer.rs @@ -5,8 +5,10 @@ use dyn_any::DynAny; use glam::{DAffine2, DVec2}; use graphene_core::blending::BlendMode; use graphene_core::bounds::BoundingBox; +use graphene_core::bounds::RenderBoundingBox; use graphene_core::color::Color; use graphene_core::math::quad::Quad; +use graphene_core::raster::BitmapMut; use graphene_core::raster::Image; use graphene_core::raster_types::{CPU, GPU, Raster}; use graphene_core::render_complexity::RenderComplexity; @@ -155,7 +157,7 @@ pub struct RenderContext { #[derive(Default)] pub struct RenderParams { pub view_mode: ViewMode, - pub culling_bounds: Option<[DVec2; 2]>, + pub footprint: Footprint, pub thumbnail: bool, /// Don't render the rectangle for an artboard to allow exporting with a transparent background. pub hide_artboards: bool, @@ -301,13 +303,13 @@ impl Render for Table { ViewMode::Outline => peniko::Mix::Normal, _ => alpha_blending.blend_mode.to_peniko(), }; - let mut bounds = None; + let mut bounds = RenderBoundingBox::None; let opacity = row.alpha_blending.opacity(render_params.for_mask); if opacity < 1. || (render_params.view_mode != ViewMode::Outline && alpha_blending.blend_mode != BlendMode::default()) { bounds = row.element.bounding_box(transform, true); - if let Some(bounds) = bounds { + if let RenderBoundingBox::Rectangle(bounds) = bounds { scene.push_layer( peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver), opacity, @@ -331,7 +333,7 @@ impl Render for Table { bounds = row.element.bounding_box(transform, true); } - if let Some(bounds) = bounds { + if let RenderBoundingBox::Rectangle(bounds) = bounds { let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y); scene.push_layer(peniko::Mix::Normal, 1., kurbo::Affine::IDENTITY, &rect); @@ -341,7 +343,7 @@ impl Render for Table { row.element.render_to_vello(scene, transform, context, render_params); - if bounds.is_some() { + if matches!(bounds, RenderBoundingBox::Rectangle(_)) { scene.pop_layer(); scene.pop_layer(); } @@ -956,7 +958,9 @@ impl Render for Table> { state.finish() }); if !render.image_data.iter().any(|(old_id, _)| *old_id == id) { - render.image_data.push((id, image.data().clone())); + let mut image = image.data().clone(); + image.map_pixels(|p| p.to_unassociated_alpha()); + render.image_data.push((id, image)); } render.parent_tag( "foreignObject", @@ -1043,7 +1047,7 @@ impl Render for Table> { let mut layer = false; if opacity < 1. || alpha_blending.blend_mode != BlendMode::default() { - if let Some(bounds) = self.bounding_box(transform, false) { + if let RenderBoundingBox::Rectangle(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); @@ -1095,7 +1099,7 @@ impl Render for Table> { let blend_mode = *row.alpha_blending; let mut layer = false; if blend_mode != Default::default() { - if let Some(bounds) = self.bounding_box(transform, true) { + if let RenderBoundingBox::Rectangle(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); @@ -1145,6 +1149,7 @@ impl Render for Graphic { Graphic::Vector(vector) => vector.render_svg(render, render_params), Graphic::RasterCPU(raster) => raster.render_svg(render, render_params), Graphic::RasterGPU(_) => (), + Graphic::Color(color) => color.render_svg(render, render_params), } } @@ -1155,6 +1160,7 @@ impl Render for Graphic { Graphic::Vector(vector) => vector.render_to_vello(scene, transform, context, render_params), Graphic::RasterCPU(raster) => raster.render_to_vello(scene, transform, context, render_params), Graphic::RasterGPU(raster) => raster.render_to_vello(scene, transform, context, render_params), + Graphic::Color(color) => color.render_to_vello(scene, transform, context, render_params), } } @@ -1172,20 +1178,28 @@ impl Render for Graphic { metadata.local_transforms.insert(element_id, *vector.transform); } } - Graphic::RasterCPU(raster_frame) => { + Graphic::RasterCPU(raster) => { metadata.upstream_footprints.insert(element_id, footprint); // TODO: Find a way to handle more than one row of images - if let Some(image) = raster_frame.iter().next() { - metadata.local_transforms.insert(element_id, *image.transform); + if let Some(raster) = raster.iter().next() { + metadata.local_transforms.insert(element_id, *raster.transform); } } - Graphic::RasterGPU(raster_frame) => { + Graphic::RasterGPU(raster) => { metadata.upstream_footprints.insert(element_id, footprint); // TODO: Find a way to handle more than one row of images - if let Some(image) = raster_frame.iter().next() { - metadata.local_transforms.insert(element_id, *image.transform); + if let Some(raster) = raster.iter().next() { + metadata.local_transforms.insert(element_id, *raster.transform); + } + } + Graphic::Color(color) => { + metadata.upstream_footprints.insert(element_id, footprint); + + // TODO: Find a way to handle more than one row of images + if let Some(color) = color.iter().next() { + metadata.local_transforms.insert(element_id, *color.transform); } } } @@ -1196,6 +1210,7 @@ impl Render for Graphic { Graphic::Vector(vector) => vector.collect_metadata(metadata, footprint, element_id), Graphic::RasterCPU(raster) => raster.collect_metadata(metadata, footprint, element_id), Graphic::RasterGPU(raster) => raster.collect_metadata(metadata, footprint, element_id), + Graphic::Color(color) => color.collect_metadata(metadata, footprint, element_id), } } @@ -1205,6 +1220,7 @@ impl Render for Graphic { Graphic::Vector(vector) => vector.add_upstream_click_targets(click_targets), Graphic::RasterCPU(raster) => raster.add_upstream_click_targets(click_targets), Graphic::RasterGPU(raster) => raster.add_upstream_click_targets(click_targets), + Graphic::Color(color) => color.add_upstream_click_targets(click_targets), } } @@ -1214,6 +1230,7 @@ impl Render for Graphic { Graphic::Vector(vector) => vector.contains_artboard(), Graphic::RasterCPU(raster) => raster.contains_artboard(), Graphic::RasterGPU(raster) => raster.contains_artboard(), + Graphic::Color(color) => color.contains_artboard(), } } @@ -1223,6 +1240,71 @@ impl Render for Graphic { Graphic::Vector(vector) => vector.new_ids_from_hash(reference), Graphic::RasterCPU(_) => (), Graphic::RasterGPU(_) => (), + Graphic::Color(_) => (), + } + } +} + +impl Render for Table { + fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { + for row in self.iter() { + render.leaf_tag("rect", |attributes| { + attributes.push("width", render_params.footprint.resolution.x.to_string()); + attributes.push("height", render_params.footprint.resolution.y.to_string()); + + let matrix = format_transform_matrix(render_params.footprint.transform.inverse()); + if !matrix.is_empty() { + attributes.push("transform", matrix); + } + + let color = row.element; + attributes.push("fill", format!("#{}", color.to_rgb_hex_srgb_from_gamma())); + if color.a() < 1. { + attributes.push("fill-opacity", ((color.a() * 1000.).round() / 1000.).to_string()); + } + + let opacity = row.alpha_blending.opacity(render_params.for_mask); + if opacity < 1. { + attributes.push("opacity", opacity.to_string()); + } + + if row.alpha_blending.blend_mode != BlendMode::default() { + attributes.push("style", row.alpha_blending.blend_mode.render()); + } + }); + } + } + + #[cfg(feature = "vello")] + fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) { + use vello::peniko; + + for row in self.iter() { + let alpha_blending = *row.alpha_blending; + let blend_mode = alpha_blending.blend_mode.to_peniko(); + let opacity = alpha_blending.opacity(render_params.for_mask); + + let transform = parent_transform * render_params.footprint.transform.inverse(); + let color = row.element; + let vello_color = peniko::Color::new([color.r(), color.g(), color.b(), color.a()]); + + let rect = kurbo::Rect::from_origin_size( + kurbo::Point::ZERO, + kurbo::Size::new(render_params.footprint.resolution.x as f64, render_params.footprint.resolution.y as f64), + ); + + let mut layer = false; + if opacity < 1. || alpha_blending.blend_mode != BlendMode::default() { + let blending = peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver); + scene.push_layer(blending, opacity, kurbo::Affine::IDENTITY, &rect); + layer = true; + } + + scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(transform.to_cols_array()), vello_color, None, &rect); + + if layer { + scene.pop_layer(); + } } } } @@ -1251,27 +1333,50 @@ impl Render for P { } impl Render for Option { - fn render_svg(&self, render: &mut SvgRender, _render_params: &RenderParams) { - let Some(color) = self else { - render.parent_tag("text", |_| {}, |render| render.leaf_node("Empty color")); - return; - }; - let color_info = format!("{:?} #{} {:?}", color, color.to_rgba_hex_srgb(), color.to_rgba8_srgb()); - - render.leaf_tag("rect", |attributes| { - attributes.push("width", "100"); - attributes.push("height", "100"); - attributes.push("y", "40"); - attributes.push("fill", format!("#{}", color.to_rgb_hex_srgb_from_gamma())); - if color.a() < 1. { - attributes.push("fill-opacity", ((color.a() * 1000.).round() / 1000.).to_string()); - } - }); - render.parent_tag("text", text_attributes, |render| render.leaf_node(color_info)) + fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { + if let Some(color) = self { + color.render_svg(render, render_params); + } } #[cfg(feature = "vello")] - fn render_to_vello(&self, _scene: &mut Scene, _transform: DAffine2, _context: &mut RenderContext, _render_params: &RenderParams) {} + fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) { + if let Some(color) = self { + color.render_to_vello(scene, parent_transform, _context, render_params); + } + } +} + +impl Render for Color { + fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { + render.leaf_tag("rect", |attributes| { + attributes.push("width", render_params.footprint.resolution.x.to_string()); + attributes.push("height", render_params.footprint.resolution.y.to_string()); + + let matrix = format_transform_matrix(render_params.footprint.transform.inverse()); + if !matrix.is_empty() { + attributes.push("transform", matrix); + } + + attributes.push("fill", format!("#{}", self.to_rgb_hex_srgb_from_gamma())); + if self.a() < 1. { + attributes.push("fill-opacity", ((self.a() * 1000.).round() / 1000.).to_string()); + } + }); + } + + #[cfg(feature = "vello")] + fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) { + let transform = parent_transform * render_params.footprint.transform.inverse(); + let vello_color = peniko::Color::new([self.r(), self.g(), self.b(), self.a()]); + + let rect = kurbo::Rect::from_origin_size( + kurbo::Point::ZERO, + kurbo::Size::new(render_params.footprint.resolution.x as f64, render_params.footprint.resolution.y as f64), + ); + + scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(transform.to_cols_array()), vello_color, None, &rect); + } } impl Render for Vec { diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 4b8d17fc..45d0082e 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -56,6 +56,7 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => Table>]), #[cfg(feature = "gpu")] async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table>]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Color]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Option]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => String]), @@ -86,10 +87,12 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => Image]), + async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table]), + async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table>]), - async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table]), + async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table]), + async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Image]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec]), #[cfg(feature = "gpu")] async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Arc]), @@ -113,6 +116,7 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => Table>]), #[cfg(feature = "gpu")] async_node!(graphene_core::memo::ImpureMemoNode<_, _, _>, input: Context, fn_params: [Context => Table>]), + async_node!(graphene_core::memo::ImpureMemoNode<_, _, _>, input: Context, fn_params: [Context => Table]), async_node!(graphene_core::memo::ImpureMemoNode<_, _, _>, input: Context, fn_params: [Context => WgpuSurface]), async_node!(graphene_core::memo::ImpureMemoNode<_, _, _>, input: Context, fn_params: [Context => Option]), async_node!(graphene_core::memo::ImpureMemoNode<_, _, _>, input: Context, fn_params: [Context => ImageTexture]),