From 08c2d0543c98aafcd444cb36938d28073d76b034 Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 17 Jul 2025 00:31:09 -0700 Subject: [PATCH] Animated thumbnails --- editor/src/dispatcher.rs | 2 - .../document/document_message_handler.rs | 1 - .../node_graph/document_node_definitions.rs | 2 +- .../portfolio/portfolio_message_handler.rs | 40 +++++---- node-graph/gcore/src/bounds.rs | 84 +++++++++++++++-- node-graph/gcore/src/context.rs | 4 +- node-graph/gcore/src/gradient.rs | 26 +++++- node-graph/gcore/src/render_complexity.rs | 5 +- node-graph/gcore/src/vector/vector_data.rs | 34 ++++++- node-graph/graph-craft/src/document/value.rs | 6 ++ node-graph/gstd/src/wasm_application_io.rs | 5 +- node-graph/gsvg-renderer/src/renderer.rs | 90 ++++++++++++++----- 12 files changed, 239 insertions(+), 60 deletions(-) diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 871803f8c1..5dd5df6c8e 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -6,9 +6,7 @@ use crate::messages::prelude::*; #[derive(Debug, Default)] pub struct Dispatcher { evaluation_queue: Vec, - introspection_queue: Vec, queueing_evaluation_messages: bool, - queueing_introspection_messages: bool, message_queues: Vec>, pub responses: Vec, pub message_handlers: DispatcherMessageHandlers, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 66a4da3a79..57f7de89cb 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -17,7 +17,6 @@ use crate::messages::portfolio::document::utility_types::document_metadata::{Doc use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, DocumentMode, FlipAxis, PTZ}; use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeTemplate}; use crate::messages::portfolio::document::utility_types::nodes::RawBuffer; -use crate::messages::portfolio::utility_types::PersistentData; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_blend_mode, get_fill, get_opacity}; use crate::messages::tool::tool_messages::select_tool::SelectToolPointerKeys; diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index 3eb27ee150..fab5a3d4dd 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -1096,7 +1096,7 @@ fn static_nodes() -> Vec { implementation: DocumentNodeImplementation::ProtoNode(text::text::IDENTIFIER), manual_composition: Some(concrete!(Context)), inputs: vec![ - NodeInput::scope("editor-api"), + NodeInput::scope("font-cache"), NodeInput::value(TaggedValue::String("Lorem ipsum".to_string()), false), NodeInput::value( TaggedValue::Font(Font::new(graphene_std::consts::DEFAULT_FONT_FAMILY.into(), graphene_std::consts::DEFAULT_FONT_STYLE.into())), diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 19da88c3af..85a667a7c7 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -796,7 +796,7 @@ impl MessageHandler> for Portfolio // Remove all thumbnails cleared_thumbnails.push(sni); } - + document.node_graph_handler.node_graph_errors = Vec::new(); self.thumbnails_to_clear.extend(cleared_thumbnails); } PortfolioMessage::EvaluateActiveDocumentWithThumbnails => { @@ -869,7 +869,9 @@ impl MessageHandler> for Portfolio let evaluated_data = match monitor_result { MonitorIntrospectResult::Error => continue, MonitorIntrospectResult::Disabled => continue, - MonitorIntrospectResult::NotEvaluated => continue, + MonitorIntrospectResult::NotEvaluated => { + continue; + } MonitorIntrospectResult::Evaluated((data, changed)) => { // If the evaluated value is the same as the previous, then just remap the ID if !changed { @@ -1253,30 +1255,30 @@ impl PortfolioMessageHandler { .network_interface .viewport_loaded_thumbnail_position(&input_connector, graph_wire_style, &document.breadcrumb_network_path) { - let in_view = viewport_position.x > 0.0 && viewport_position.y > 0.0 && viewport_position.x < ipp.viewport_bounds()[1].x && viewport_position.y < ipp.viewport_bounds()[1].y; - if in_view { - let Some(protonode) = document.network_interface.protonode_from_input(&input_connector, &document.breadcrumb_network_path) else { - // The input is not connected to the export, which occurs if inside a disconnected node - wire_stack = Vec::new(); - nodes_to_render.clear(); - continue; - }; - nodes_to_render.insert(protonode); - } + // let in_view = viewport_position.x > 0.0 && viewport_position.y > 0.0 && viewport_position.x < ipp.viewport_bounds()[1].x && viewport_position.y < ipp.viewport_bounds()[1].y; + // if in_view { + let Some(protonode) = document.network_interface.protonode_from_input(&input_connector, &document.breadcrumb_network_path) else { + // The input is not connected to the export, which occurs if inside a disconnected node + wire_stack = Vec::new(); + nodes_to_render.clear(); + continue; + }; + nodes_to_render.insert(protonode); + // } } } }; - // Get thumbnails for all visible layer - for visible_node in &document.node_graph_handler.visible_nodes(&mut document.network_interface, &document.breadcrumb_network_path, ipp) { - if document.network_interface.is_layer(&visible_node, &document.breadcrumb_network_path) { - let Some(protonode) = document + // Get thumbnails for all visible layer ouputs + // for visible_node in &document.node_graph_handler.visible_nodes(&mut document.network_interface, &document.breadcrumb_network_path, ipp) { + for visible_node in document.network_interface.nested_network(&document.breadcrumb_network_path).unwrap().nodes.keys() { + if document.network_interface.is_layer(visible_node, &document.breadcrumb_network_path) { + if let Some(protonode) = document .network_interface .protonode_from_output(&OutputConnector::node(*visible_node, 0), &document.breadcrumb_network_path) - else { - continue; + { + nodes_to_render.insert(protonode); }; - nodes_to_render.insert(protonode); } } diff --git a/node-graph/gcore/src/bounds.rs b/node-graph/gcore/src/bounds.rs index ccc373d4cf..cafe2433fb 100644 --- a/node-graph/gcore/src/bounds.rs +++ b/node-graph/gcore/src/bounds.rs @@ -1,5 +1,5 @@ use crate::Color; -use glam::{DAffine2, DVec2}; +use glam::{DAffine2, DVec2, IVec2, UVec2}; pub trait BoundingBox { fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]>; @@ -15,10 +15,80 @@ macro_rules! none_impl { }; } -none_impl!(String); -none_impl!(bool); -none_impl!(f32); -none_impl!(f64); -none_impl!(DVec2); -none_impl!(Option); none_impl!(Vec); + +impl BoundingBox for u32 { + fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> { + text_bbox(i32_width(*self as i32)) + } +} + +impl BoundingBox for f64 { + fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> { + text_bbox(f64_width(*self)) + } +} + +impl BoundingBox for DVec2 { + fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> { + let width_x = f64_width(self.x); + let width_y = f64_width(self.y); + let total_width = width_x + width_y + 50.; + text_bbox(total_width) + } +} + +impl BoundingBox for IVec2 { + fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> { + let width_x = i32_width(self.x); + let width_y = i32_width(self.y); + let total_width = width_x + width_y + 50.; + text_bbox(total_width) + } +} + +impl BoundingBox for UVec2 { + fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> { + let width_x = i32_width(self.x as i32); + let width_y = i32_width(self.y as i32); + let total_width = width_x + width_y + 50.; + text_bbox(total_width) + } +} + +impl BoundingBox for bool { + fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> { + text_bbox(60.) + } +} + +impl BoundingBox for String { + fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> { + let width = self.len() * 16; + text_bbox(width as f64) + } +} + +impl BoundingBox for Option { + fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> { + Some([(0., -5.).into(), (150., 110.).into()]) + } +} + +fn f64_width(f64: f64) -> f64 { + let left_of_decimal_width = i32_width(f64 as i32); + left_of_decimal_width + 5. + 2. * 16. +} + +fn i32_width(i32: i32) -> f64 { + let number_of_digits = (i32.abs()).checked_ilog10().unwrap_or(0) + 1; + let mut width = number_of_digits * 16; + if i32 < 0 { + width += 20; + } + width.into() +} + +fn text_bbox(width: f64) -> Option<[DVec2; 2]> { + Some([(-width / 2., 0.).into(), (width / 2., 30.).into()]) +} diff --git a/node-graph/gcore/src/context.rs b/node-graph/gcore/src/context.rs index 5691b69cb3..a4a9d1c193 100644 --- a/node-graph/gcore/src/context.rs +++ b/node-graph/gcore/src/context.rs @@ -630,8 +630,8 @@ fn get_animation_time(ctx: impl Ctx + ExtractAnimationTime) -> Option { } #[node_macro::node(category("Context Getter"))] -fn get_index(ctx: impl Ctx + ExtractIndex) -> Option { - ctx.try_index() +fn get_index(ctx: impl Ctx + ExtractIndex) -> Option { + ctx.try_index().map(|index| index as u32) } // #[node_macro::node(category("Loop"))] diff --git a/node-graph/gcore/src/gradient.rs b/node-graph/gcore/src/gradient.rs index 8034cc9876..381201d53e 100644 --- a/node-graph/gcore/src/gradient.rs +++ b/node-graph/gcore/src/gradient.rs @@ -1,4 +1,4 @@ -use crate::Color; +use crate::{Color, bounds::BoundingBox, vector::VectorDataTable}; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; @@ -32,6 +32,12 @@ impl Default for GradientStops { } } +impl BoundingBox for GradientStops { + fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> { + Into::::into(Into::::into(self.clone())).bounding_box(DAffine2::default(), false) + } +} + impl IntoIterator for GradientStops { type Item = (f64, Color); type IntoIter = std::vec::IntoIter<(f64, Color)>; @@ -169,6 +175,24 @@ impl std::fmt::Display for Gradient { } } +impl From for Gradient { + fn from(gradient_stops: GradientStops) -> Gradient { + Gradient { + stops: gradient_stops.clone(), + gradient_type: GradientType::Linear, + start: (0., 0.).into(), + end: (1., 0.).into(), + transform: DAffine2::IDENTITY, + } + } +} + +impl BoundingBox for Gradient { + fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> { + Into::::into(self.clone()).bounding_box(DAffine2::default(), false) + } +} + impl Gradient { /// Constructs a new gradient with the colors at 0 and 1 specified. pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, transform: DAffine2, gradient_type: GradientType) -> Self { diff --git a/node-graph/gcore/src/render_complexity.rs b/node-graph/gcore/src/render_complexity.rs index 3c137c87e8..72e57e0396 100644 --- a/node-graph/gcore/src/render_complexity.rs +++ b/node-graph/gcore/src/render_complexity.rs @@ -1,3 +1,4 @@ +use crate::gradient::{Gradient, GradientStops}; use crate::instances::Instances; use crate::raster_types::{CPU, GPU, Raster}; use crate::vector::VectorData; @@ -54,8 +55,10 @@ impl RenderComplexity for Raster { impl RenderComplexity for String {} impl RenderComplexity for bool {} -impl RenderComplexity for f32 {} +impl RenderComplexity for u32 {} impl RenderComplexity for f64 {} impl RenderComplexity for DVec2 {} impl RenderComplexity for Option {} impl RenderComplexity for Vec {} +impl RenderComplexity for GradientStops {} +impl RenderComplexity for Gradient {} diff --git a/node-graph/gcore/src/vector/vector_data.rs b/node-graph/gcore/src/vector/vector_data.rs index 3523784545..cda364b59b 100644 --- a/node-graph/gcore/src/vector/vector_data.rs +++ b/node-graph/gcore/src/vector/vector_data.rs @@ -5,13 +5,15 @@ mod modification; use super::misc::{dvec2_to_point, point_to_dvec2}; use super::style::{PathStyle, Stroke}; use crate::bounds::BoundingBox; +use crate::gradient::{Gradient, GradientType}; use crate::instances::Instances; use crate::math::quad::Quad; use crate::transform::Transform; use crate::vector::click_target::{ClickTargetType, FreePoint}; +use crate::vector::style::Fill; use crate::{AlphaBlending, Color, GraphicGroupTable}; pub use attributes::*; -use bezier_rs::{BezierHandles, ManipulatorGroup}; +use bezier_rs::{BezierHandles, ManipulatorGroup, Subpath}; use core::borrow::Borrow; use core::hash::Hash; use dyn_any::DynAny; @@ -511,6 +513,36 @@ impl BoundingBox for VectorDataTable { } } +/// Convert a Gradient/GradientStops into VectorDataTable for rendering thumbnails +impl From for VectorDataTable { + fn from(mut gradient: Gradient) -> VectorDataTable { + match gradient.gradient_type { + GradientType::Linear => { + let mut rectangle = VectorData::from_subpath(Subpath::new_rect((0., 0.).into(), (150., 100.).into())); + // Handle vertical gradients + let intersection = if gradient.start.x == gradient.end.x { + DVec2::new(0., 100.) + } else { + let slope = (gradient.start.y - gradient.end.y) / (gradient.start.x - gradient.end.x); + if slope > 100. / 150. { DVec2::new(100. / slope, 100.) } else { DVec2::new(150., slope * 150.) } + }; + gradient.start = (0., 0.).into(); + gradient.end = intersection; + rectangle.style.fill = Fill::Gradient(gradient); + Instances::new(rectangle) + } + GradientType::Radial => { + let mut circle = VectorData::from_subpath(Subpath::new_ellipse((-100., -100.).into(), (100., 100.).into())); + gradient.start = (0., 0.).into(); + gradient.end = (100., 0.).into(); + gradient.transform = DAffine2::IDENTITY; + circle.style.fill = Fill::Gradient(gradient); + Instances::new(circle) + } + } + } +} + /// A selectable part of a curve, either an anchor (start or end of a bézier) or a handle (doesn't necessarily go through the bézier but influences curvature). #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, DynAny, serde::Serialize, serde::Deserialize)] pub enum ManipulatorPointId { diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 92b17e5ffa..5b4ed8befa 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -7,6 +7,7 @@ pub use glam::{DAffine2, DVec2, IVec2, UVec2}; use graphene_application_io::SurfaceFrame; use graphene_brush::brush_cache::BrushCache; use graphene_brush::brush_stroke::BrushStroke; +use graphene_core::gradient::GradientStops; use graphene_core::raster_types::{CPU, GPU}; use graphene_core::transform::ReferencePoint; use graphene_core::uuid::NodeId; @@ -562,8 +563,13 @@ thumbnail_render! { graphene_core::raster_types::RasterDataTable, graphene_core::GraphicElement, Option, + GradientStops, Vec, + u32, f64, + DVec2, + bool, + String, } pub enum ThumbnailRenderResult { diff --git a/node-graph/gstd/src/wasm_application_io.rs b/node-graph/gstd/src/wasm_application_io.rs index 37c7bc55c0..4f998b6f45 100644 --- a/node-graph/gstd/src/wasm_application_io.rs +++ b/node-graph/gstd/src/wasm_application_io.rs @@ -1,7 +1,9 @@ +use glam::DVec2; pub use graph_craft::document::value::RenderOutputType; use graph_craft::document::value::{EditorMetadata, RenderOutput}; pub use graph_craft::wasm_application_io::*; use graphene_application_io::ApplicationIo; +use graphene_core::gradient::GradientStops; #[cfg(target_arch = "wasm32")] use graphene_core::instances::Instances; #[cfg(target_arch = "wasm32")] @@ -240,10 +242,11 @@ async fn render<'a: 'n, T: 'n + GraphicElementRendered + WasmNotSend>( graphene_core::Artboard, graphene_core::ArtboardGroupTable, Option, + GradientStops, Vec, bool, - f32, f64, + DVec2, String, )] data: T, diff --git a/node-graph/gsvg-renderer/src/renderer.rs b/node-graph/gsvg-renderer/src/renderer.rs index c472ab425f..4930e4e148 100644 --- a/node-graph/gsvg-renderer/src/renderer.rs +++ b/node-graph/gsvg-renderer/src/renderer.rs @@ -6,6 +6,7 @@ use glam::{DAffine2, DVec2}; use graphene_core::blending::BlendMode; use graphene_core::bounds::BoundingBox; use graphene_core::color::Color; +use graphene_core::gradient::{Gradient, GradientStops}; use graphene_core::instances::Instance; use graphene_core::math::quad::Quad; use graphene_core::raster::Image; @@ -1185,23 +1186,36 @@ impl GraphicElementRendered for GraphicElement { } } -/// Used to stop rust complaining about upstream traits adding display implementations to `Option`. This would not be an issue as we control that crate. -trait Primitive: std::fmt::Display + BoundingBox + RenderComplexity {} -impl Primitive for String {} +trait Primitive: std::fmt::Display + BoundingBox + RenderComplexity { + fn precision() -> bool { + false + } +} +impl Primitive for u32 {} +impl Primitive for f64 { + fn precision() -> bool { + true + } +} +impl Primitive for DVec2 { + fn precision() -> bool { + true + } +} impl Primitive for bool {} -impl Primitive for f32 {} -impl Primitive for f64 {} -impl Primitive for DVec2 {} +impl Primitive for String {} fn text_attributes(attributes: &mut SvgRenderAttrs) { attributes.push("fill", "black"); attributes.push("font-size", "30"); + attributes.push("dominant-baseline", "hanging"); + attributes.push("text-anchor", "middle"); } impl GraphicElementRendered for P { fn render_svg(&self, render: &mut SvgRender, _render_params: &RenderParams) { - log::debug!("Rendering svg for primative: {}", self); - render.parent_tag("text", text_attributes, |render| render.leaf_node(format!("{self}"))); + let text = if P::precision() { format!("{:.2}", self) } else { format!("{self}") }; + render.parent_tag("text", text_attributes, |render| render.leaf_node(text)); } #[cfg(feature = "vello")] @@ -1210,22 +1224,33 @@ impl GraphicElementRendered for P { impl GraphicElementRendered 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()); + match self { + Some(color) => { + render.leaf_tag("rect", |attributes| { + attributes.push("width", "150"); + attributes.push("height", "100"); + 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)) + None => { + render.leaf_tag("rect", |attributes| { + attributes.push("width", "150"); + attributes.push("height", "100"); + attributes.push("fill", format!("#ffffff")); + }); + render.leaf_tag("line", |attributes| { + attributes.push("x1", "0"); + attributes.push("y1", "100"); + attributes.push("x2", "150"); + attributes.push("y2", "0"); + attributes.push("stroke", "red"); + attributes.push("stroke-width", "5"); + }); + } + } } #[cfg(feature = "vello")] @@ -1236,7 +1261,7 @@ impl GraphicElementRendered for Vec { fn render_svg(&self, render: &mut SvgRender, _render_params: &RenderParams) { for (index, &color) in self.iter().enumerate() { render.leaf_tag("rect", |attributes| { - attributes.push("width", "100"); + attributes.push("width", "150"); attributes.push("height", "100"); attributes.push("x", (index * 120).to_string()); attributes.push("y", "40"); @@ -1252,6 +1277,23 @@ impl GraphicElementRendered for Vec { fn render_to_vello(&self, _scene: &mut Scene, _transform: DAffine2, _context: &mut RenderContext, _render_params: &RenderParams) {} } +impl GraphicElementRendered for GradientStops { + fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { + // Gradient stops -> Gradient -> Vector data table + Into::::into(Into::::into(self.clone())).render_svg(render, render_params); + } + + #[cfg(feature = "vello")] + fn render_to_vello(&self, _scene: &mut Scene, _transform: DAffine2, _context: &mut RenderContext, _render_params: &RenderParams) {} +} + +impl GraphicElementRendered for Gradient { + fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { + Into::::into(self.clone()).render_svg(render, render_params); + } + #[cfg(feature = "vello")] + fn render_to_vello(&self, _scene: &mut Scene, _transform: DAffine2, _context: &mut RenderContext, _render_params: &RenderParams) {} +} #[derive(Debug, Clone, PartialEq, Eq)] pub enum SvgSegment { Slice(&'static str),