From 08b25f689e5fe40971864deef5b19a23c1b8a3b5 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 7 Sep 2025 21:16:05 -0700 Subject: [PATCH 01/24] New Typography type --- Cargo.lock | 2 + .../data_panel/data_panel_message_handler.rs | 18 +++ .../document/overlays/utility_types_vello.rs | 11 +- node-graph/gcore/src/bounds.rs | 8 +- node-graph/gcore/src/consts.rs | 7 + node-graph/gcore/src/graphic.rs | 88 ++++------ node-graph/gcore/src/render_complexity.rs | 8 + node-graph/gcore/src/text.rs | 152 ++++++++++++++++++ .../src/text}/source-sans-pro-regular.ttf | Bin node-graph/gpath-bool/src/lib.rs | 1 + node-graph/graph-craft/src/document/value.rs | 19 ++- node-graph/gsvg-renderer/Cargo.toml | 2 + node-graph/gsvg-renderer/src/renderer.rs | 111 +++++++++++++ 13 files changed, 356 insertions(+), 71 deletions(-) rename {editor/src/messages/portfolio/document/overlays => node-graph/gcore/src/text}/source-sans-pro-regular.ttf (100%) diff --git a/Cargo.lock b/Cargo.lock index d90f78c7c2..1ea2396f49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2245,7 +2245,9 @@ dependencies = [ "kurbo", "log", "num-traits", + "parley", "serde", + "skrifa 0.36.0", "usvg 0.45.1", "vello", ] diff --git a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs index b9d01a5e0c..0e2229880e 100644 --- a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs @@ -12,6 +12,7 @@ use graphene_std::gradient::GradientStops; use graphene_std::memo::IORecord; use graphene_std::raster_types::{CPU, GPU, Raster}; use graphene_std::table::Table; +use graphene_std::text::Typography; use graphene_std::vector::Vector; use graphene_std::vector::style::{Fill, FillChoice}; use graphene_std::{Artboard, Graphic}; @@ -266,6 +267,7 @@ impl TableRowLayout for Graphic { Self::RasterGPU(table) => table.identifier(), Self::Color(table) => table.identifier(), Self::Gradient(table) => table.identifier(), + Self::Typography(table) => table.identifier(), } } // Don't put a breadcrumb for Graphic @@ -280,6 +282,7 @@ impl TableRowLayout for Graphic { Self::RasterGPU(table) => table.layout_with_breadcrumb(data), Self::Color(table) => table.layout_with_breadcrumb(data), Self::Gradient(table) => table.layout_with_breadcrumb(data), + Self::Typography(table) => table.layout_with_breadcrumb(data), } } } @@ -504,6 +507,21 @@ impl TableRowLayout for GradientStops { } } +impl TableRowLayout for Typography { + fn type_name() -> &'static str { + "Typography" + } + fn identifier(&self) -> String { + "Typography".to_string() + } + fn element_widget(&self, _index: usize) -> WidgetHolder { + TextLabel::new("Not supported").widget_holder() + } + fn element_page(&self, _data: &mut LayoutData) -> Vec { + vec![LayoutGroup::Row { widgets: Vec::new() }] + } +} + impl TableRowLayout for f64 { fn type_name() -> &'static str { "Number (f64)" diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs b/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs index 6cc9f37ab9..930db71c1b 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs @@ -9,6 +9,7 @@ use core::borrow::Borrow; use core::f64::consts::{FRAC_PI_2, PI, TAU}; use glam::{DAffine2, DVec2}; use graphene_std::Color; +use graphene_std::consts::SOURCE_SANS_FONT_DATA; use graphene_std::math::quad::Quad; use graphene_std::subpath::{self, Subpath}; use graphene_std::table::Table; @@ -1021,10 +1022,7 @@ impl OverlayContextInternal { align: TextAlign::Left, }; - // Load Source Sans Pro font data - // TODO: Grab this from the node_modules folder (either with `include_bytes!` or ideally at runtime) instead of checking the font file into the repo. - // TODO: And maybe use the WOFF2 version (if it's supported) for its smaller, compressed file size. - const FONT_DATA: &[u8] = include_bytes!("source-sans-pro-regular.ttf"); + const FONT_DATA: &[u8] = SOURCE_SANS_FONT_DATA; let font_blob = Some(load_font(FONT_DATA)); // Convert text to paths and calculate actual bounds @@ -1048,10 +1046,7 @@ impl OverlayContextInternal { align: TextAlign::Left, // We'll handle alignment manually via pivot }; - // Load Source Sans Pro font data - // TODO: Grab this from the node_modules folder (either with `include_bytes!` or ideally at runtime) instead of checking the font file into the repo. - // TODO: And maybe use the WOFF2 version (if it's supported) for its smaller, compressed file size. - const FONT_DATA: &[u8] = include_bytes!("source-sans-pro-regular.ttf"); + const FONT_DATA: &[u8] = SOURCE_SANS_FONT_DATA; let font_blob = Some(load_font(FONT_DATA)); // Convert text to vector paths using the existing text system diff --git a/node-graph/gcore/src/bounds.rs b/node-graph/gcore/src/bounds.rs index fb4b19cd42..084500fb1c 100644 --- a/node-graph/gcore/src/bounds.rs +++ b/node-graph/gcore/src/bounds.rs @@ -1,4 +1,4 @@ -use crate::{Color, gradient::GradientStops}; +use crate::{Color, gradient::GradientStops, text::Typography}; use glam::{DAffine2, DVec2}; #[derive(Clone, Copy, Default, Debug, PartialEq)] @@ -38,3 +38,9 @@ impl BoundingBox for GradientStops { RenderBoundingBox::Infinite } } +impl BoundingBox for Typography { + fn bounding_box(&self, transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox { + let bbox = DVec2::new(self.layout.full_width() as f64, self.layout.height() as f64); + RenderBoundingBox::Rectangle([transform.transform_point2(DVec2::ZERO), transform.transform_point2(bbox)]) + } +} diff --git a/node-graph/gcore/src/consts.rs b/node-graph/gcore/src/consts.rs index 505dc81ccd..cec018c9e5 100644 --- a/node-graph/gcore/src/consts.rs +++ b/node-graph/gcore/src/consts.rs @@ -7,3 +7,10 @@ pub const LAYER_OUTLINE_STROKE_WEIGHT: f64 = 0.5; // Fonts pub const DEFAULT_FONT_FAMILY: &str = "Cabin"; pub const DEFAULT_FONT_STYLE: &str = "Regular (400)"; + +// Load Source Sans Pro font data +// TODO: Grab this from the node_modules folder (either with `include_bytes!` or ideally at runtime) instead of checking the font file into the repo. +// TODO: And maybe use the WOFF2 version (if it's supported) for its smaller, compressed file size. +pub const SOURCE_SANS_FONT_DATA: &[u8] = include_bytes!("text/source-sans-pro-regular.ttf"); +pub const SOURCE_SANS_FONT_FAMILY: &str = "Source Sans Pro"; +pub const SOURCE_SANS_FONT_STYLE: &str = "Regular (400)"; diff --git a/node-graph/gcore/src/graphic.rs b/node-graph/gcore/src/graphic.rs index 324c93e33d..5f3892c3b1 100644 --- a/node-graph/gcore/src/graphic.rs +++ b/node-graph/gcore/src/graphic.rs @@ -3,6 +3,7 @@ use crate::bounds::{BoundingBox, RenderBoundingBox}; use crate::gradient::GradientStops; use crate::raster_types::{CPU, GPU, Raster}; use crate::table::{Table, TableRow}; +use crate::text::Typography; use crate::uuid::NodeId; use crate::vector::Vector; use crate::{Artboard, Color, Ctx}; @@ -11,7 +12,7 @@ use glam::{DAffine2, DVec2}; use std::hash::Hash; /// The possible forms of graphical content that can be rendered by the Render node into either an image or SVG syntax. -#[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Hash, PartialEq, DynAny)] pub enum Graphic { Graphic(Table), Vector(Table), @@ -19,6 +20,26 @@ pub enum Graphic { RasterGPU(Table>), Color(Table), Gradient(Table), + Typography(Table), +} + +impl serde::Serialize for Graphic { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let default: Table = Table::new(); + default.serialize(serializer) + } +} + +impl<'de> serde::Deserialize<'de> for Graphic { + fn deserialize(_deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + Ok(Graphic::Graphic(Table::new())) + } } impl Default for Graphic { @@ -232,6 +253,7 @@ impl Graphic { Graphic::RasterGPU(raster) => raster.iter().all(|row| row.alpha_blending.clip), Graphic::Color(color) => color.iter().all(|row| row.alpha_blending.clip), Graphic::Gradient(gradient) => gradient.iter().all(|row| row.alpha_blending.clip), + Graphic::Typography(typography) => typography.iter().all(|row| row.alpha_blending.clip), } } @@ -256,6 +278,7 @@ impl BoundingBox for Graphic { Graphic::Graphic(graphic) => graphic.bounding_box(transform, include_stroke), Graphic::Color(color) => color.bounding_box(transform, include_stroke), Graphic::Gradient(gradient) => gradient.bounding_box(transform, include_stroke), + Graphic::Typography(typography) => typography.bounding_box(transform, include_stroke), } } } @@ -507,34 +530,15 @@ pub fn migrate_graphic<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Res elements: Vec<(Graphic, Option)>, } - #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] - pub struct OlderTable { - id: Vec, - #[serde(alias = "instances", alias = "instance")] - element: Vec, - } - - #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] - pub struct OldTable { - id: Vec, - #[serde(alias = "instances", alias = "instance")] - element: Vec, - transform: Vec, - alpha_blending: Vec, - } - #[derive(serde::Serialize, serde::Deserialize)] #[serde(untagged)] - enum GraphicFormat { + enum EitherFormat { OldGraphicGroup(OldGraphicGroup), - OlderTableOldGraphicGroup(OlderTable), - OldTableOldGraphicGroup(OldTable), - OldTableGraphicGroup(OldTable), Table(serde_json::Value), } - Ok(match GraphicFormat::deserialize(deserializer)? { - GraphicFormat::OldGraphicGroup(old) => { + Ok(match EitherFormat::deserialize(deserializer)? { + EitherFormat::OldGraphicGroup(old) => { let mut graphic_table = Table::new(); for (graphic, source_node_id) in old.elements { graphic_table.push(TableRow { @@ -546,43 +550,7 @@ pub fn migrate_graphic<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Res } graphic_table } - GraphicFormat::OlderTableOldGraphicGroup(old) => old - .element - .into_iter() - .flat_map(|element| { - element.elements.into_iter().map(move |(graphic, source_node_id)| TableRow { - element: graphic, - transform: element.transform, - alpha_blending: element.alpha_blending, - source_node_id, - }) - }) - .collect(), - GraphicFormat::OldTableOldGraphicGroup(old) => old - .element - .into_iter() - .flat_map(|element| { - element.elements.into_iter().map(move |(graphic, source_node_id)| TableRow { - element: graphic, - transform: element.transform, - alpha_blending: element.alpha_blending, - source_node_id, - }) - }) - .collect(), - GraphicFormat::OldTableGraphicGroup(old) => old - .element - .into_iter() - .flat_map(|element| { - element.elements.into_iter().map(move |(graphic, source_node_id)| TableRow { - element: graphic, - transform: Default::default(), - alpha_blending: Default::default(), - source_node_id, - }) - }) - .collect(), - GraphicFormat::Table(value) => { + EitherFormat::Table(value) => { // Try to deserialize as either table format if let Ok(old_table) = serde_json::from_value::>(value.clone()) { let mut graphic_table = Table::new(); diff --git a/node-graph/gcore/src/render_complexity.rs b/node-graph/gcore/src/render_complexity.rs index 7920479377..3f8e1e8f5a 100644 --- a/node-graph/gcore/src/render_complexity.rs +++ b/node-graph/gcore/src/render_complexity.rs @@ -1,6 +1,7 @@ use crate::gradient::GradientStops; use crate::raster_types::{CPU, GPU, Raster}; use crate::table::Table; +use crate::text::Typography; use crate::vector::Vector; use crate::{Artboard, Color, Graphic}; @@ -31,6 +32,7 @@ impl RenderComplexity for Graphic { Self::RasterGPU(table) => table.render_complexity(), Self::Color(table) => table.render_complexity(), Self::Gradient(table) => table.render_complexity(), + Self::Typography(table) => table.render_complexity(), } } } @@ -65,3 +67,9 @@ impl RenderComplexity for GradientStops { 1 } } + +impl RenderComplexity for Typography { + fn render_complexity(&self) -> usize { + 1 + } +} diff --git a/node-graph/gcore/src/text.rs b/node-graph/gcore/src/text.rs index 3337a1488c..c3786d8309 100644 --- a/node-graph/gcore/src/text.rs +++ b/node-graph/gcore/src/text.rs @@ -1,10 +1,23 @@ mod font_cache; mod to_path; +use std::{ + borrow::Cow, + collections::{HashMap, hash_map::Entry}, + fmt, + sync::{Arc, Mutex}, +}; + use dyn_any::DynAny; pub use font_cache::*; +use graphene_core_shaders::color::Color; +use parley::{Layout, StyleProperty}; +use rustc_hash::FxHasher; +use std::hash::{Hash, Hasher}; pub use to_path::*; +use crate::{consts::*, table::Table, vector::Vector}; + /// Alignment of lines of type within a text block. #[repr(C)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] @@ -29,3 +42,142 @@ impl From for parley::Alignment { } } } + +#[derive(Clone, DynAny)] +pub struct Typography { + pub layout: Layout<()>, + pub font_family: String, + pub color: Color, + pub stroke: Option<(Color, f64)>, +} + +impl fmt::Debug for Typography { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Typography") + .field("font_family", &self.font_family) + .field("color", &self.color) + .field("stroke", &self.stroke) + .finish() + } +} + +impl PartialEq for Typography { + fn eq(&self, _other: &Self) -> bool { + true + } +} + +impl Hash for Typography { + fn hash(&self, state: &mut H) { + self.layout.len().hash(state); + } +} + +impl Typography { + pub fn to_vector(&self) -> Table { + Table::new() + } +} + +#[derive(Clone)] +pub struct NewFontCacheWrapper(pub Arc>); + +impl fmt::Debug for NewFontCacheWrapper { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("font cache").finish() + } +} + +impl PartialEq for NewFontCacheWrapper { + fn eq(&self, _other: &Self) -> bool { + log::error!("Font cache should not be compared"); + false + } +} + +unsafe impl dyn_any::StaticType for NewFontCacheWrapper { + type Static = NewFontCacheWrapper; +} + +pub struct NewFontCache { + pub font_context: parley::FontContext, + pub layout_context: parley::LayoutContext<()>, + pub font_mapping: HashMap, + pub hash: u64, +} + +impl NewFontCache { + pub fn new() -> Self { + let mut new = NewFontCache { + font_context: parley::FontContext::new(), + layout_context: parley::LayoutContext::new(), + font_mapping: HashMap::new(), + hash: 0, + }; + + let source_sans_font = Font::new(SOURCE_SANS_FONT_FAMILY.to_string(), SOURCE_SANS_FONT_STYLE.to_string()); + new.register_font(source_sans_font, SOURCE_SANS_FONT_DATA.to_vec()); + new + } + + pub fn register_font(&mut self, font: Font, data: Vec) { + match self.font_mapping.entry(font) { + Entry::Occupied(occupied_entry) => { + log::error!("Trying to register font that already is added: {:?}", occupied_entry.key()); + } + Entry::Vacant(vacant_entry) => { + let registered_font = self.font_context.collection.register_fonts(parley::fontique::Blob::from(data), None); + if registered_font.len() > 1 { + log::error!("Registered multiple fonts for {:?}. Only the first is accessible", vacant_entry.key()); + }; + match registered_font.into_iter().next() { + Some((family_id, font_info)) => { + let Some(family_name) = self.font_context.collection.family_name(family_id) else { + log::error!("Could not get family name for font: {:?}", vacant_entry.key()); + return; + }; + let Some(font_info) = font_info.into_iter().next() else { + log::error!("Could not get font info for font: {:?}", vacant_entry.key()); + return; + }; + let mut hasher = FxHasher::default(); // or FxHasher::new() + // Hash the Font for a unique id and add it to the cached hash + vacant_entry.key().hash(&mut hasher); + let hash_value = hasher.finish(); + self.hash = self.hash.wrapping_add(hash_value); + + vacant_entry.insert((family_name.to_string(), font_info)); + } + None => log::error!("Could not register font for {:?}", vacant_entry.key()), + } + } + } + } + + pub fn generate_typography(&mut self, font: &Font, font_size: f32, text: &str) -> Option { + let Some((font_family, font_info)) = self.font_mapping.get(font) else { + log::error!("Font not loaded: {:?}", font); + return None; + }; + let font_family = font_family.to_string(); + + let mut builder = self.layout_context.ranged_builder(&mut self.font_context, text, 1., false); + + builder.push_default(StyleProperty::FontStack(parley::FontStack::Single(parley::FontFamily::Named(Cow::Owned(font_family.clone()))))); + builder.push_default(StyleProperty::FontSize(font_size)); + builder.push_default(StyleProperty::FontWeight(font_info.weight())); + builder.push_default(StyleProperty::FontStyle(font_info.style())); + builder.push_default(StyleProperty::FontWidth(font_info.width())); + + let mut layout: Layout<()> = builder.build(text); + layout.break_all_lines(None); + // layout.align(None, parley::Alignment::Start, AlignmentOptions::); + + Some(Typography { + layout, + font_family, + color: Color::BLACK, + stroke: None, + }) + } +} diff --git a/editor/src/messages/portfolio/document/overlays/source-sans-pro-regular.ttf b/node-graph/gcore/src/text/source-sans-pro-regular.ttf similarity index 100% rename from editor/src/messages/portfolio/document/overlays/source-sans-pro-regular.ttf rename to node-graph/gcore/src/text/source-sans-pro-regular.ttf diff --git a/node-graph/gpath-bool/src/lib.rs b/node-graph/gpath-bool/src/lib.rs index df3a089414..7634476564 100644 --- a/node-graph/gpath-bool/src/lib.rs +++ b/node-graph/gpath-bool/src/lib.rs @@ -318,6 +318,7 @@ fn flatten_vector(graphic_table: &Table) -> Table { } }) .collect::>(), + Graphic::Typography(_) => Vec::new(), } }) .collect() diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 0e0534d9f3..2d812cc19b 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -11,6 +11,7 @@ use graphene_brush::brush_stroke::BrushStroke; use graphene_core::raster::Image; use graphene_core::raster_types::{CPU, Raster}; use graphene_core::table::Table; +use graphene_core::text::NewFontCacheWrapper; use graphene_core::transform::ReferencePoint; use graphene_core::uuid::NodeId; use graphene_core::vector::Vector; @@ -38,7 +39,9 @@ macro_rules! tagged_value { RenderOutput(RenderOutput), SurfaceFrame(SurfaceFrame), #[serde(skip)] - EditorApi(Arc) + EditorApi(Arc), + #[serde(skip)] + NewFontCache(NewFontCacheWrapper), } // We must manually implement hashing because some values are floats and so do not reproducibly hash (see FakeHash below) @@ -52,6 +55,7 @@ macro_rules! tagged_value { Self::RenderOutput(x) => x.hash(state), Self::SurfaceFrame(x) => x.hash(state), Self::EditorApi(x) => x.hash(state), + Self::NewFontCache(x) => x.hash(state), } } } @@ -64,6 +68,7 @@ macro_rules! tagged_value { Self::RenderOutput(x) => Box::new(x), Self::SurfaceFrame(x) => Box::new(x), Self::EditorApi(x) => Box::new(x), + Self::NewFontCache(x) => Box::new(x), } } /// Converts to a Arc @@ -74,6 +79,7 @@ macro_rules! tagged_value { Self::RenderOutput(x) => Arc::new(x), Self::SurfaceFrame(x) => Arc::new(x), Self::EditorApi(x) => Arc::new(x), + Self::NewFontCache(x) => Arc::new(x), } } /// Creates a graphene_core::Type::Concrete(TypeDescriptor { .. }) with the type of the value inside the tagged value @@ -83,7 +89,8 @@ macro_rules! tagged_value { $( Self::$identifier(_) => concrete!($ty), )* Self::RenderOutput(_) => concrete!(RenderOutput), Self::SurfaceFrame(_) => concrete!(SurfaceFrame), - Self::EditorApi(_) => concrete!(&WasmEditorApi) + Self::EditorApi(_) => concrete!(&WasmEditorApi), + Self::NewFontCache(_) => concrete!(NewFontCacheWrapper), } } /// Attempts to downcast the dynamic type to a tagged value @@ -517,6 +524,14 @@ mod fake_hash { self.1.hash(state) } } + impl FakeHash for NewFontCacheWrapper { + fn hash(&self, state: &mut H) { + match self.0.lock() { + Ok(inner) => inner.hash.hash(state), + Err(_) => log::error!("Could not lock font cache when hashing"), + } + } + } } #[test] diff --git a/node-graph/gsvg-renderer/Cargo.toml b/node-graph/gsvg-renderer/Cargo.toml index a6d0e395c5..d606dee915 100644 --- a/node-graph/gsvg-renderer/Cargo.toml +++ b/node-graph/gsvg-renderer/Cargo.toml @@ -19,6 +19,8 @@ log = { workspace = true } num-traits = { workspace = true } usvg = { workspace = true } kurbo = { workspace = true } +parley = { workspace = true } +skrifa = { workspace = true } # Optional workspace dependencies vello = { workspace = true, optional = true } diff --git a/node-graph/gsvg-renderer/src/renderer.rs b/node-graph/gsvg-renderer/src/renderer.rs index 3917a14c3c..cb5bfd02a6 100644 --- a/node-graph/gsvg-renderer/src/renderer.rs +++ b/node-graph/gsvg-renderer/src/renderer.rs @@ -15,6 +15,7 @@ use graphene_core::raster_types::{CPU, GPU, Raster}; use graphene_core::render_complexity::RenderComplexity; use graphene_core::subpath::Subpath; use graphene_core::table::{Table, TableRow}; +use graphene_core::text::Typography; use graphene_core::transform::{Footprint, Transform}; use graphene_core::uuid::{NodeId, generate_uuid}; use graphene_core::vector::Vector; @@ -23,11 +24,14 @@ use graphene_core::vector::style::{Fill, PaintOrder, Stroke, StrokeAlign, ViewMo use graphene_core::{Artboard, Graphic}; use kurbo::Affine; use num_traits::Zero; +use skrifa::MetadataProvider; +use skrifa::attribute::Style; use std::collections::{HashMap, HashSet}; use std::fmt::Write; use std::hash::{DefaultHasher, Hash, Hasher}; use std::ops::Deref; use std::sync::{Arc, LazyLock}; +use vello::peniko::StyleRef; #[cfg(feature = "vello")] use vello::*; @@ -115,6 +119,18 @@ impl SvgRender { self.svg.push("/>".into()); } + pub fn leaf_text(&mut self, text: impl Into, attributes: impl FnOnce(&mut SvgRenderAttrs)) { + self.indent(); + + self.svg.push("".into()); + self.svg.push(text.into()); + self.svg.push("".into()); + } + pub fn leaf_node(&mut self, content: impl Into) { self.indent(); self.svg.push(content.into()); @@ -256,6 +272,7 @@ impl Render for Graphic { Graphic::RasterGPU(_) => (), Graphic::Color(table) => table.render_svg(render, render_params), Graphic::Gradient(table) => table.render_svg(render, render_params), + Graphic::Typography(table) => table.render_svg(render, render_params), } } @@ -268,6 +285,7 @@ impl Render for Graphic { Graphic::RasterGPU(table) => table.render_to_vello(scene, transform, context, render_params), Graphic::Color(table) => table.render_to_vello(scene, transform, context, render_params), Graphic::Gradient(table) => table.render_to_vello(scene, transform, context, render_params), + Graphic::Typography(table) => table.render_to_vello(scene, transform, context, render_params), } } @@ -312,6 +330,14 @@ impl Render for Graphic { Graphic::Gradient(table) => { metadata.upstream_footprints.insert(element_id, footprint); + // TODO: Find a way to handle more than the first row + if let Some(row) = table.iter().next() { + metadata.local_transforms.insert(element_id, *row.transform); + } + } + Graphic::Typography(table) => { + metadata.upstream_footprints.insert(element_id, footprint); + // TODO: Find a way to handle more than the first row if let Some(row) = table.iter().next() { metadata.local_transforms.insert(element_id, *row.transform); @@ -327,6 +353,7 @@ impl Render for Graphic { Graphic::RasterGPU(table) => table.collect_metadata(metadata, footprint, element_id), Graphic::Color(table) => table.collect_metadata(metadata, footprint, element_id), Graphic::Gradient(table) => table.collect_metadata(metadata, footprint, element_id), + Graphic::Typography(table) => table.collect_metadata(metadata, footprint, element_id), } } @@ -338,6 +365,7 @@ impl Render for Graphic { Graphic::RasterGPU(table) => table.add_upstream_click_targets(click_targets), Graphic::Color(table) => table.add_upstream_click_targets(click_targets), Graphic::Gradient(table) => table.add_upstream_click_targets(click_targets), + Graphic::Typography(table) => table.add_upstream_click_targets(click_targets), } } @@ -349,6 +377,7 @@ impl Render for Graphic { Graphic::RasterGPU(table) => table.contains_artboard(), Graphic::Color(table) => table.contains_artboard(), Graphic::Gradient(table) => table.contains_artboard(), + Graphic::Typography(table) => table.contains_artboard(), } } @@ -360,6 +389,7 @@ impl Render for Graphic { Graphic::RasterGPU(_) => (), Graphic::Color(_) => (), Graphic::Gradient(_) => (), + Graphic::Typography(_) => (), } } } @@ -1516,6 +1546,87 @@ impl Render for Table { } } +impl Render for Table { + fn render_svg(&self, render: &mut SvgRender, _render_params: &RenderParams) { + for table_row in self.iter() { + for line in table_row.element.layout.lines() { + for item in line.items() { + match item { + parley::PositionedLayoutItem::GlyphRun(glyph_run) => { + let font = glyph_run.run().font(); + let font_ref = skrifa::FontRef::from_index(font.data.as_ref(), font.index).unwrap(); + let font_attributes = font_ref.attributes(); + let font_style = match font_attributes.style { + Style::Normal => "normal".to_string(), + Style::Italic => "italic".to_string(), + Style::Oblique(Some(angle)) => format!("oblique {}deg", angle), + Style::Oblique(None) => "oblique".to_string(), + }; + render.parent_tag( + "text", + |attributes| { + let matrix = format_transform_matrix(*table_row.transform); + if !matrix.is_empty() { + attributes.push("transform", matrix); + } + + attributes.push("font-family", table_row.element.font_family.clone()); + attributes.push("font-weight", font_attributes.weight.value().to_string()); + attributes.push("font-size", glyph_run.run().font_size().to_string()); + attributes.push("font-style", font_style); + attributes.push("fill", format!("#{}", table_row.element.color.to_rgb_hex_srgb_from_gamma())); + attributes.push("opacity", table_row.alpha_blending.opacity.to_string()); + if let Some((stroke_color, stroke_width)) = table_row.element.stroke.as_ref().cloned() { + attributes.push("stroke-color", format!("#{}", stroke_color.to_rgb_hex_srgb_from_gamma())); + attributes.push("stroke-width", format!("{stroke_width}")); + } + }, + |render| { + for glyph in glyph_run.positioned_glyphs() { + let character = font_ref.glyph_names().get(skrifa::GlyphId::new(glyph.id as u32)).unwrap().as_str().to_string(); + let character = character.replace("space", " "); + render.leaf_text(character, |attributes| { + attributes.push("x", glyph.x.to_string()); + attributes.push("y", glyph.y.to_string()); + }); + } + }, + ); + } + parley::PositionedLayoutItem::InlineBox(_positioned_inline_box) => { + log::error!("Inline box text rendering not supported"); + } + } + } + } + } + } + + fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, _context: &mut RenderContext, _render_params: &RenderParams) { + for table_row in self.iter() { + for line in table_row.element.layout.lines() { + for item in line.items() { + match item { + parley::PositionedLayoutItem::GlyphRun(glyph_run) => { + let color = table_row.element.color.clone(); + scene + .draw_glyphs(glyph_run.run().font()) + .transform(kurbo::Affine::new((transform * *table_row.transform).to_cols_array())) + .font_size(glyph_run.run().font_size()) + .brush(peniko::BrushRef::Solid(peniko::Color::new([color.r(), color.g(), color.b(), color.a()]))) + .brush_alpha(table_row.alpha_blending.opacity) + .draw(StyleRef::Fill(peniko::Fill::EvenOdd), glyph_run.glyphs().map(|g| Glyph { id: g.id as u32, x: g.x, y: g.y })); + } + parley::PositionedLayoutItem::InlineBox(_positioned_inline_box) => { + log::error!("Cannot render positioned inline box to vello"); + } + } + } + } + } + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum SvgSegment { Slice(&'static str), From e35e9886b14b65e981966cb0ce06572fa0d9f701 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 29 Aug 2025 19:43:41 -0700 Subject: [PATCH 02/24] WIP: Refactor TypeSource --- .../document/document_message_handler.rs | 11 +- .../document/graph_operation/utility_types.rs | 4 +- .../node_graph/node_graph_message_handler.rs | 45 +-- .../document/node_graph/node_properties.rs | 8 +- .../document/node_graph/utility_types.rs | 8 +- .../utility_types/network_interface.rs | 370 +++--------------- .../network_interface/resolved_types.rs | 336 ++++++++++++++++ .../graph_modification_utils.rs | 4 +- .../common_functionality/utility_functions.rs | 4 +- node-graph/gcore/src/types.rs | 9 + node-graph/graph-craft/src/document.rs | 8 + .../src/dynamic_executor.rs | 11 - node-graph/interpreted-executor/src/util.rs | 1 + 13 files changed, 429 insertions(+), 390 deletions(-) create mode 100644 editor/src/messages/portfolio/document/utility_types/network_interface/resolved_types.rs diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index e42c15a17f..b8b0c6fcd3 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -2800,14 +2800,9 @@ impl DocumentMessageHandler { .popover_layout({ // Showing only compatible types let compatible_type = selected_layer.and_then(|layer| { - let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &self.network_interface); - let node_type = graph_layer.horizontal_layer_flow().nth(1); - if let Some(node_id) = node_type { - let (output_type, _) = self.network_interface.output_type(&OutputConnector::node(node_id, 0), &self.selection_network_path); - Some(format!("type:{}", output_type.nested_type())) - } else { - None - } + self.network_interface + .upstream_output_connector(&InputConnector::node(layer.to_node(), 1), &[]) + .and_then(|upstream_output| self.network_interface.output_type(&upstream_output, &[]).add_node_string()) }); let mut node_chooser = NodeCatalog::new(); diff --git a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs index 4079fefd32..b25e7905a5 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -303,8 +303,8 @@ impl<'a> ModifyInputsContext<'a> { // If inserting a 'Path' node, insert a 'Flatten Path' node if the type is `Graphic`. // TODO: Allow the 'Path' node to operate on table data by utilizing the reference (index or ID?) for each row. if node_definition.identifier == "Path" { - let layer_input_type = self.network_interface.input_type(&InputConnector::node(output_layer.to_node(), 1), &[]).0.nested_type().clone(); - if layer_input_type == concrete!(Table) { + let layer_input_type = self.network_interface.input_type(&InputConnector::node(output_layer.to_node(), 1), &[]).into_compiled_nested_type(); + if layer_input_type == Some(concrete!(Table)) { let Some(flatten_path_definition) = resolve_document_node_type("Flatten Path") else { log::error!("Flatten Path does not exist in ModifyInputsContext::existing_node_id"); return None; diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index 6337a2566d..5e9bde5708 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -878,7 +878,7 @@ impl<'a> MessageHandler> for NodeG }; let Some(output_connector) = output_connector else { return }; self.wire_in_progress_from_connector = network_interface.output_position(&output_connector, selection_network_path); - self.wire_in_progress_type = FrontendGraphDataType::from_type(&network_interface.input_type(clicked_input, breadcrumb_network_path).0); + self.wire_in_progress_type = FrontendGraphDataType::displayed_type(&network_interface.input_type(clicked_input, breadcrumb_network_path)); return; } @@ -888,8 +888,8 @@ impl<'a> MessageHandler> for NodeG self.initial_disconnecting = false; self.wire_in_progress_from_connector = network_interface.output_position(&clicked_output, selection_network_path); - let (output_type, source) = &network_interface.output_type(&clicked_output, breadcrumb_network_path); - self.wire_in_progress_type = FrontendGraphDataType::displayed_type(output_type, source); + let output_type = network_interface.output_type(&clicked_output, breadcrumb_network_path); + self.wire_in_progress_type = FrontendGraphDataType::displayed_type(&output_type); self.update_node_graph_hints(responses); return; @@ -1207,16 +1207,12 @@ impl<'a> MessageHandler> for NodeG } // Get the output types from the network interface - let (output_type, type_source) = network_interface.output_type(&output_connector, selection_network_path); let Some(network_metadata) = network_interface.network_metadata(selection_network_path) else { warn!("No network_metadata"); return; }; - let compatible_type = match type_source { - TypeSource::RandomProtonodeImplementation | TypeSource::Error(_) => None, - _ => Some(format!("type:{}", output_type.nested_type())), - }; + let compatible_type = network_interface.output_type(&output_connector.unwrap(), selection_network_path).add_node_string(); let appear_right_of_mouse = if ipp.mouse.position.x > ipp.viewport_bounds.size().x - 173. { -173. } else { 0. }; let appear_above_mouse = if ipp.mouse.position.y > ipp.viewport_bounds.size().y - 34. { -34. } else { 0. }; @@ -1990,12 +1986,7 @@ impl<'a> MessageHandler> for NodeG responses.add(NodeGraphMessage::SendGraph); } NodeGraphMessage::UpdateTypes { resolved_types, node_graph_errors } => { - for (path, node_type) in resolved_types.add { - network_interface.resolved_types.types.insert(path.to_vec(), node_type); - } - for path in resolved_types.remove { - network_interface.resolved_types.types.remove(&path.to_vec()); - } + network_interface.resolved_types.update(resolved_types); self.node_graph_errors = node_graph_errors; } NodeGraphMessage::UpdateActionButtons => { @@ -2109,16 +2100,7 @@ impl NodeGraphMessageHandler { .popover_layout({ // Showing only compatible types let compatible_type = match (selection_includes_layers, has_multiple_selection, selected_layer) { - (true, false, Some(layer)) => { - let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, network_interface); - let node_type = graph_layer.horizontal_layer_flow().nth(1); - if let Some(node_id) = node_type { - let (output_type, _) = network_interface.output_type(&OutputConnector::node(node_id, 0), &[]); - Some(format!("type:{}", output_type.nested_type())) - } else { - None - } - } + (true, false, Some(layer)) => network_interface.output_type(&OutputConnector::node(node_id, 0), &[]).add_node_string(), _ => None, }; @@ -2431,17 +2413,10 @@ impl NodeGraphMessageHandler { .icon(Some("Node".to_string())) .tooltip("Add an operation to the end of this layer's chain of nodes") .popover_layout({ - let layer_identifier = LayerNodeIdentifier::new(layer, context.network_interface); - let compatible_type = { - let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer_identifier, context.network_interface); - let node_type = graph_layer.horizontal_layer_flow().nth(1); - if let Some(node_id) = node_type { - let (output_type, _) = context.network_interface.output_type(&OutputConnector::node(node_id, 0), &[]); - Some(format!("type:{}", output_type.nested_type())) - } else { - None - } - }; + let compatible_type = context + .network_interface + .upstream_output_connector(&InputConnector::node(layer, 1), &[]) + .and_then(|upstream_output| context.network_interface.output_type(&upstream_output, &[]).add_node_string()); let mut node_chooser = NodeCatalog::new(); node_chooser.intial_search = compatible_type.unwrap_or("".to_string()); 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 0dd2d5906c..7b5166c434 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -1610,7 +1610,11 @@ pub(crate) fn generate_node_properties(node_id: NodeId, context: &mut NodeProper input_type.clone() } - _ => context.network_interface.input_type(&InputConnector::node(node_id, input_index), context.selection_network_path).0, + _ => context + .network_interface + .input_type(&InputConnector::node(node_id, input_index), context.selection_network_path) + .into_compiled_nested_type() + .unwrap_or(concrete!(())), }; property_from_type(node_id, input_index, &input_type, number_options, unit_suffix, display_decimal_places, step, context).unwrap_or_else(|value| value) @@ -1992,7 +1996,7 @@ pub struct ParameterWidgetsInfo<'a> { impl<'a> ParameterWidgetsInfo<'a> { pub fn new(node_id: NodeId, index: usize, blank_assist: bool, context: &'a mut NodePropertiesContext) -> ParameterWidgetsInfo<'a> { let (name, description) = context.network_interface.displayed_input_name_and_description(&node_id, index, context.selection_network_path); - let input_type = FrontendGraphDataType::from_type(&context.network_interface.input_type(&InputConnector::node(node_id, index), context.selection_network_path).0); + let input_type = FrontendGraphDataType::from_type(&context.network_interface.input_type(&InputConnector::node(node_id, index), context.selection_network_path)); let document_node = context.network_interface.document_node(&node_id, context.selection_network_path); ParameterWidgetsInfo { 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 b3ed877769..08bfca30e3 100644 --- a/editor/src/messages/portfolio/document/node_graph/utility_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/utility_types.rs @@ -42,10 +42,10 @@ impl FrontendGraphDataType { } } - pub fn displayed_type(input: &Type, type_source: &TypeSource) -> Self { - match type_source { - TypeSource::Error(_) | TypeSource::RandomProtonodeImplementation => Self::General, - _ => Self::from_type(input), + pub fn displayed_type(type_source: TypeSource) -> Self { + match type_source.compiled_nested_type() { + Some(nested_type) => Self::from_type(&nested_type), + None => Self::General, } } } diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index d6c47223b9..07441df8c3 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -7,28 +7,30 @@ use crate::consts::{EXPORTS_TO_RIGHT_EDGE_PIXEL_GAP, EXPORTS_TO_TOP_EDGE_PIXEL_G use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext; use crate::messages::portfolio::document::node_graph::document_node_definitions::{DocumentNodeDefinition, resolve_document_node_type}; use crate::messages::portfolio::document::node_graph::utility_types::{Direction, FrontendClickTargets, FrontendGraphDataType, FrontendGraphInput, FrontendGraphOutput}; +use crate::messages::portfolio::document::utility_types::network_interface::resolved_types::ResolvedDocumentNodeTypes; use crate::messages::portfolio::document::utility_types::wires::{GraphWireStyle, WirePath, WirePathUpdate, build_vector_wire}; use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::tool_messages::tool_prelude::NumberInputMode; use deserialization::deserialize_node_persistent_metadata; use glam::{DAffine2, DVec2, IVec2}; use graph_craft::document::value::TaggedValue; -use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork}; -use graph_craft::{Type, concrete}; -use graphene_std::Artboard; +use graph_craft::document::{DocumentNode, DocumentNodeImplementation, InlineRust, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork}; +use graph_craft::{ProtoNodeIdentifier, Type, concrete}; use graphene_std::ContextDependencies; use graphene_std::math::quad::Quad; use graphene_std::subpath::Subpath; -use graphene_std::table::Table; use graphene_std::transform::Footprint; use graphene_std::vector::click_target::{ClickTarget, ClickTargetType}; use graphene_std::vector::{PointId, Vector, VectorModificationType}; -use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypes; use interpreted_executor::node_registry::NODE_REGISTRY; use kurbo::BezPath; use serde_json::{Value, json}; use std::collections::{HashMap, HashSet, VecDeque}; use std::hash::{DefaultHasher, Hash, Hasher}; + +mod deserialization; +mod resolved_types; +use deserialization::deserialize_node_persistent_metadata; use std::ops::Deref; /// All network modifications should be done through this API, so the fields cannot be public. However, all fields within this struct can be public since it it not possible to have a public mutable reference. @@ -454,12 +456,12 @@ impl NodeNetworkInterface { *input = NodeInput::Node { node_id: new_id, output_index }; } else { // Disconnect node input if it is not connected to another node in new_ids - let tagged_value = TaggedValue::from_type_or_none(&self.input_type(&InputConnector::node(*node_id, input_index), network_path).0); + let tagged_value = self.tagged_value_from_input(&InputConnector::node(*node_id, input_index), network_path); *input = NodeInput::value(tagged_value, true); } } else if let &mut NodeInput::Network { .. } = input { // Always disconnect network node input - let tagged_value = TaggedValue::from_type_or_none(&self.input_type(&InputConnector::node(*node_id, input_index), network_path).0); + let tagged_value = self.tagged_value_from_input(&InputConnector::node(*node_id, input_index), network_path); *input = NodeInput::value(tagged_value, true); } } @@ -467,8 +469,8 @@ impl NodeNetworkInterface { } /// Try and get the [`DocumentNodeDefinition`] for a node - pub fn get_node_definition(&self, network_path: &[NodeId], node_id: NodeId) -> Option<&DocumentNodeDefinition> { - let metadata = self.node_metadata(&node_id, network_path)?; + pub fn get_node_definition(&self, node_id: &NodeId, network_path: &[NodeId]) -> Option<&DocumentNodeDefinition> { + let metadata = self.node_metadata(node_id, network_path)?; resolve_document_node_type(metadata.persistent_metadata.reference.as_ref()?) } @@ -489,278 +491,6 @@ impl NodeNetworkInterface { } } - /// Try and get the [`Type`] for any [`InputConnector`] based on the `self.resolved_types`. - fn node_type_from_compiled(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> Option<(Type, TypeSource)> { - let (node_id, input_index) = match *input_connector { - InputConnector::Node { node_id, input_index } => (node_id, input_index), - InputConnector::Export(export_index) => { - let Some((encapsulating_node_id, encapsulating_node_id_path)) = network_path.split_last() else { - // The outermost network export defaults to a Table. - return Some((concrete!(Table), TypeSource::OuterMostExportDefault)); - }; - - let output_type = self.output_type(&OutputConnector::node(*encapsulating_node_id, export_index), encapsulating_node_id_path); - return Some(output_type); - } - }; - let Some(node) = self.document_node(&node_id, network_path) else { - log::error!("Could not get node {node_id} in input_type"); - return None; - }; - // If the input_connector is a NodeInput::Value, return the type of the tagged value. - if let Some(value) = node.inputs.get(input_index).and_then(|input| input.as_value()) { - return Some((value.ty(), TypeSource::TaggedValue)); - } - let node_id_path = [network_path, &[node_id]].concat(); - match &node.implementation { - DocumentNodeImplementation::Network(_nested_network) => { - // Attempt to resolve where this import is within the nested network (it may be connected to the node or directly to an export) - let outwards_wires = self.outward_wires(&node_id_path); - let inputs_using_import = outwards_wires.and_then(|outwards_wires| outwards_wires.get(&OutputConnector::Import(input_index))); - let first_input = inputs_using_import.and_then(|input| input.first()).copied(); - - if inputs_using_import.is_some_and(|inputs| inputs.len() > 1) { - warn!("Found multiple inputs using an import. Using the type of the first one."); - } - - if let Some(input_connector) = first_input { - self.node_type_from_compiled(&input_connector, &node_id_path) - } - // Nothing is connected to the import - else { - None - } - } - DocumentNodeImplementation::ProtoNode(_) => { - // Offset the input index by 1 since the proto node also includes the type of the input passed as a call argument. - self.resolved_types - .types - .get(node_id_path.as_slice()) - .and_then(|node_types| node_types.inputs.get(input_index + 1).cloned()) - .map(|node_types| (node_types, TypeSource::Compiled)) - } - DocumentNodeImplementation::Extract => None, - } - } - - /// Guess the type from the node based on a document node default or a random protonode definition. - fn guess_type_from_node(&mut self, network_path: &mut Vec, node_id: NodeId, input_index: usize) -> (Type, TypeSource) { - // Try and get the default value from the document node definition - if let Some(value) = self - .get_node_definition(network_path, node_id) - .and_then(|definition| definition.node_template.document_node.inputs.get(input_index)) - .and_then(|input| input.as_value()) - { - return (value.ty(), TypeSource::DocumentNodeDefault); - } - - let Some(node) = self.document_node(&node_id, network_path) else { - return (concrete!(()), TypeSource::Error("node id {node_id:?} not in network {network_path:?}")); - }; - - let node_id_path = [network_path.as_slice(), &[node_id]].concat(); - match &node.implementation { - DocumentNodeImplementation::ProtoNode(protonode) => { - let Some(node_types) = random_protonode_implementation(protonode) else { - return (concrete!(()), TypeSource::Error("could not resolve protonode")); - }; - - let skip_footprint = 1; - - let Some(input_type) = std::iter::once(node_types.call_argument.clone()).chain(node_types.inputs.clone()).nth(input_index + skip_footprint) else { - // log::warn!("Could not get type for {node_id_path:?}, input: {input_index}"); - return (concrete!(()), TypeSource::Error("could not get the protonode's input")); - }; - - (input_type, TypeSource::RandomProtonodeImplementation) - } - DocumentNodeImplementation::Network(_network) => { - // Attempt to resolve where this import is within the nested network - let outwards_wires = self.outward_wires(&node_id_path); - let inputs_using_import = outwards_wires.and_then(|outwards_wires| outwards_wires.get(&OutputConnector::Import(input_index))); - let first_input = inputs_using_import.and_then(|input| input.first()).copied(); - - if let Some(InputConnector::Node { - node_id: child_id, - input_index: child_input_index, - }) = first_input - { - network_path.push(node_id); - let result = self.guess_type_from_node(network_path, child_id, child_input_index); - network_path.pop(); - return result; - } - - // Input is disconnected - (concrete!(()), TypeSource::Error("disconnected network input")) - } - _ => (concrete!(()), TypeSource::Error("implementation is not network or protonode")), - } - } - - /// Get the [`Type`] for any InputConnector - pub fn input_type(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> (Type, TypeSource) { - if let Some(result) = self.node_type_from_compiled(input_connector, network_path) { - return result; - } - - // Resolve types from proto nodes in node_registry - let Some(node_id) = input_connector.node_id() else { - return (concrete!(()), TypeSource::Error("input connector is not a node")); - }; - - self.guess_type_from_node(&mut network_path.to_vec(), node_id, input_connector.input_index()) - } - - pub fn valid_input_types(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> Vec { - let InputConnector::Node { node_id, input_index } = input_connector else { - // An export can have any type connected to it - return vec![graph_craft::generic!(T)]; - }; - let Some(implementation) = self.implementation(node_id, network_path) else { - log::error!("Could not get node implementation in valid_input_types"); - return Vec::new(); - }; - match implementation { - DocumentNodeImplementation::Network(_) => { - let nested_path = [network_path, &[*node_id]].concat(); - let Some(outward_wires) = self.outward_wires(&nested_path) else { - log::error!("Could not get outward wires in valid_input_types"); - return Vec::new(); - }; - let Some(inputs_from_import) = outward_wires.get(&OutputConnector::Import(*input_index)) else { - log::error!("Could not get inputs from import in valid_input_types"); - return Vec::new(); - }; - - let intersection: HashSet = inputs_from_import - .clone() - .iter() - .map(|input_connector| self.valid_input_types(input_connector, &nested_path)) - .map(|vec| vec.into_iter().collect::>()) - .fold(None, |acc: Option>, set| match acc { - Some(acc_set) => Some(acc_set.intersection(&set).cloned().collect()), - None => Some(set), - }) - .unwrap_or_default(); - - intersection.into_iter().collect::>() - } - DocumentNodeImplementation::ProtoNode(proto_node_identifier) => { - let Some(implementations) = NODE_REGISTRY.get(proto_node_identifier) else { - log::error!("Protonode {proto_node_identifier:?} not found in registry"); - return Vec::new(); - }; - let number_of_inputs = self.number_of_inputs(node_id, network_path); - implementations - .iter() - .filter_map(|(node_io, _)| { - let valid_implementation = (0..number_of_inputs).filter(|iterator_index| iterator_index != input_index).all(|iterator_index| { - let input_type = self.input_type(&InputConnector::node(*node_id, iterator_index), network_path).0; - // Value inputs are stored as concrete, so they are compared to the nested type. Node inputs are stored as fn, so they are compared to the entire type. - // For example a node input of (Footprint) -> Vector would not be compatible with () -> Vector - node_io.inputs.get(iterator_index).map(|ty| ty.nested_type().clone()).as_ref() == Some(&input_type) || node_io.inputs.get(iterator_index) == Some(&input_type) - }); - if valid_implementation { node_io.inputs.get(*input_index).cloned() } else { None } - }) - .collect::>() - } - DocumentNodeImplementation::Extract => { - log::error!("Input types for extract node not supported"); - Vec::new() - } - } - } - - /// Retrieves the output types for a given document node and its exports. - /// - /// This function traverses the node and its nested network structure (if applicable) to determine - /// the types of all outputs, including the primary output and any additional exports. - /// - /// # Arguments - /// - /// * `node` - A reference to the `DocumentNode` for which to determine output types. - /// * `resolved_types` - A reference to `ResolvedDocumentNodeTypes` containing pre-resolved type information. - /// * `node_id_path` - A slice of `NodeId`s representing the path to the current node in the document graph. - /// - /// # Returns - /// - /// A `Vec>` where: - /// - The first element is the primary output type of the node. - /// - Subsequent elements are types of additional exports (if the node is a network). - /// - `None` values indicate that a type couldn't be resolved for a particular output. - /// - /// # Behavior - /// - /// 1. Retrieves the primary output type from `resolved_types`. - /// 2. If the node is a network: - /// - Iterates through its exports (skipping the first/primary export). - /// - For each export, traverses the network until reaching a protonode or terminal condition. - /// - Determines the output type based on the final node/value encountered. - /// 3. Collects and returns all resolved types. - /// - /// # Note - /// - /// This function assumes that export indices and node IDs always exist within their respective - /// collections. It will panic if these assumptions are violated. - /// - pub fn output_type(&mut self, output_connector: &OutputConnector, network_path: &[NodeId]) -> (Type, TypeSource) { - match output_connector { - OutputConnector::Node { node_id, output_index } => { - let Some(implementation) = self.implementation(node_id, network_path) else { - log::error!("Could not get output type for node {node_id} output index {output_index}. This node is no longer supported, and needs to be upgraded."); - return (concrete!(()), TypeSource::Error("Could not get implementation")); - }; - - // If the node is not a protonode, get types by traversing across exports until a proto node is reached. - match &implementation { - graph_craft::document::DocumentNodeImplementation::Network(internal_network) => { - let Some(export) = internal_network.exports.get(*output_index) else { - return (concrete!(()), TypeSource::Error("Could not get export index")); - }; - match export { - NodeInput::Node { - node_id: nested_node_id, - output_index, - .. - } => self.output_type(&OutputConnector::node(*nested_node_id, *output_index), &[network_path, &[*node_id]].concat()), - NodeInput::Value { tagged_value, .. } => (tagged_value.ty(), TypeSource::TaggedValue), - NodeInput::Network { .. } => { - // let mut encapsulating_path = network_path.to_vec(); - // let encapsulating_node = encapsulating_path.pop().expect("No imports exist in document network"); - // self.input_type(&InputConnector::node(encapsulating_node, *import_index), network_path) - (concrete!(()), TypeSource::Error("Could not type from network")) - } - NodeInput::Scope(_) => todo!(), - NodeInput::Inline(_) => todo!(), - NodeInput::Reflection(_) => todo!(), - } - } - graph_craft::document::DocumentNodeImplementation::ProtoNode(protonode) => { - let node_id_path = &[network_path, &[*node_id]].concat(); - self.resolved_types - .types - .get(node_id_path) - .map(|ty| (ty.output.clone(), TypeSource::Compiled)) - .or_else(|| { - let node_types = random_protonode_implementation(protonode)?; - Some((node_types.return_value.clone(), TypeSource::RandomProtonodeImplementation)) - }) - .unwrap_or((concrete!(()), TypeSource::Error("Could not get protonode implementation"))) - } - graph_craft::document::DocumentNodeImplementation::Extract => (concrete!(()), TypeSource::Error("extract node")), - } - } - OutputConnector::Import(import_index) => { - let Some((encapsulating_node, encapsulating_path)) = network_path.split_last() else { - log::error!("Cannot get type of import in document network"); - return (concrete!(()), TypeSource::Error("Cannot get import type in document network")); - }; - self.input_type(&InputConnector::node(*encapsulating_node, *import_index), encapsulating_path) - } - } - } - pub fn position(&mut self, node_id: &NodeId, network_path: &[NodeId]) -> Option { let top_left_position = self .node_click_targets(node_id, network_path) @@ -893,8 +623,10 @@ impl NodeNetworkInterface { if self.input_from_connector(input_connector, network_path).is_some_and(|input| !input.is_exposed()) { return None; } - let (export_type, source) = self.input_type(input_connector, network_path); - let data_type = FrontendGraphDataType::displayed_type(&export_type, &source); + let input_type = self.input_type(input_connector, network_path); + let data_type = FrontendGraphDataType::displayed_type(&input_type); + let resolved_type = input_type.resolved_type_name(); + let connected_to = self .upstream_output_connector(input_connector, network_path) .map(|output_connector| match output_connector { @@ -924,8 +656,8 @@ impl NodeNetworkInterface { let export_name = if !export_name.is_empty() { export_name - } else if *export_type.nested_type() != concrete!(()) { - export_type.nested_type().to_string() + } else if let Some(export_type_name) = input_type.compiled_nested_type_name() { + export_type_name } else { format!("Export index {}", export_index) }; @@ -933,19 +665,30 @@ impl NodeNetworkInterface { (export_name, String::new()) } }; + + // TODO: Move in separate Tooltip overlay + let valid_types = match self.valid_input_types(&input_connector, network_path) { + Ok(input_types) => input_types.iter().map(|ty| ty.to_string()).collect(), + Err(e) => { + log::error!("Error getting valid types for input {input_connector}: {e}"); + Vec::new() + } + }; + Some(FrontendGraphInput { data_type, + resolved_type, name, description, - resolved_type: format!("{export_type:?}"), - valid_types: self.valid_input_types(input_connector, network_path).iter().map(|ty| ty.to_string()).collect(), + valid_types, connected_to, }) } /// Returns None if there is an error, it is the document network, a hidden primary output or import pub fn frontend_output_from_connector(&mut self, output_connector: &OutputConnector, network_path: &[NodeId]) -> Option { - let (output_type, type_source) = self.output_type(output_connector, network_path); + let output_type = self.output_type(output_connector, network_path); + let (name, description) = match output_connector { OutputConnector::Node { node_id, output_index } => { // Do not display the primary output port for a node if it is a network node with a hidden primary export @@ -956,14 +699,7 @@ impl NodeNetworkInterface { let node_metadata = self.node_metadata(node_id, network_path)?; let output_name = node_metadata.persistent_metadata.output_names.get(*output_index).cloned().unwrap_or_default(); - let output_name = if !output_name.is_empty() { - output_name - } else if *output_type.nested_type() != concrete!(()) { - output_type.nested_type().to_string() - } else { - format!("Output {}", *output_index + 1) - }; - + let output_name = if !output_name.is_empty() { output_name } else { output_type.resolved_type_name() }; (output_name, String::new()) } OutputConnector::Import(import_index) => { @@ -978,17 +714,19 @@ impl NodeNetworkInterface { }; let (import_name, description) = self.displayed_input_name_and_description(encapsulating_node_id, *import_index, encapsulating_path); - let import_name = if *output_type.nested_type() != concrete!(()) { + let import_name = if !import_name.is_empty() { import_name + } else if let Some(import_type_name) = output_type.compiled_nested_type_name() { + import_type_name } else { - format!("Import index {}", *import_index) + format!("Import index {}", export_index) }; + (import_name, description) } }; - - let data_type = FrontendGraphDataType::displayed_type(&output_type, &type_source); - + let data_type = FrontendGraphDataType::displayed_type(&output_type); + let resolved_type = output_type.resolved_type_name(); let mut connected_to = self .outward_wires(network_path) .and_then(|outward_wires| outward_wires.get(output_connector)) @@ -1016,8 +754,8 @@ impl NodeNetworkInterface { Some(FrontendGraphOutput { data_type, + resolved_type, name, - resolved_type: format!("{:?}", output_type), description, connected_to, }) @@ -1240,7 +978,7 @@ impl NodeNetworkInterface { }; let description = input_metadata.input_description.to_string(); let name = if input_metadata.input_name.is_empty() { - self.input_type(&InputConnector::node(*node_id, input_index), network_path).0.nested_type().to_string() + self.input_type(&InputConnector::node(*node_id, input_index), network_path).resolved_type_name() } else { input_metadata.input_name.to_string() }; @@ -1276,9 +1014,9 @@ impl NodeNetworkInterface { /// Returns the description of the node, or an empty string if it is not set. pub fn description(&self, node_id: &NodeId, network_path: &[NodeId]) -> String { - self.get_node_definition(network_path, *node_id) + self.get_node_definition(node_id, network_path) .map(|node_definition| node_definition.description.to_string()) - .filter(|description| description != "TODO") + .filter(|description: &String| description != "TODO") .unwrap_or_default() } @@ -2769,7 +2507,7 @@ impl NodeNetworkInterface { let vector_wire = build_vector_wire(output_position, input_position, vertical_start, vertical_end, graph_wire_style); let path_string = vector_wire.to_svg(); - let data_type = FrontendGraphDataType::from_type(&self.input_type(&input, network_path).0); + let data_type = FrontendGraphDataType::displayed_type(&self.input_type(&input, network_path)); let wire_path_update = Some(WirePath { path_string, data_type, @@ -2807,7 +2545,7 @@ impl NodeNetworkInterface { pub fn wire_path_from_input(&mut self, input: &InputConnector, graph_wire_style: GraphWireStyle, dashed: bool, network_path: &[NodeId]) -> Option { let (vector_wire, thick) = self.vector_wire_from_input(input, graph_wire_style, network_path)?; let path_string = vector_wire.to_svg(); - let data_type = FrontendGraphDataType::from_type(&self.input_type(input, network_path).0); + let data_type = FrontendGraphDataType::displayed_type(&self.input_type(input, network_path)); Some(WirePath { path_string, data_type, @@ -4491,7 +4229,7 @@ impl NodeNetworkInterface { } } - let tagged_value = TaggedValue::from_type_or_none(&self.input_type(input_connector, network_path).0); + let tagged_value = self.tagged_value_from_input(input_connector, network_path); let value_input = NodeInput::value(tagged_value, true); @@ -6111,22 +5849,6 @@ impl Iterator for FlowIter<'_> { } } -// TODO: Refactor to be Unknown, Compiled(Type) for NodeInput::Node, or Value(Type) for NodeInput::Value -/// Represents the source of a resolved type (for debugging). -/// There will be two valid types list. One for the current valid types that will not cause a node graph error, -/// based on the other inputs to that node and returned during compilation. THe other list will be all potential -/// Valid types, based on the protonode implementation/downstream users. -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)] -pub enum TypeSource { - Compiled, - RandomProtonodeImplementation, - DocumentNodeDefault, - TaggedValue, - OuterMostExportDefault, - - Error(&'static str), -} - #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)] pub enum ImportOrExport { Import(usize), diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/resolved_types.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/resolved_types.rs new file mode 100644 index 0000000000..52a10b4bdf --- /dev/null +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/resolved_types.rs @@ -0,0 +1,336 @@ +use std::collections::HashMap; + +use graph_craft::Type; +use graphene_std::uuid::NodeId; +use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta; + +use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface, OutputConnector}; + +// This file contains utility methods for interfacing with the resolved types returned from the compiler +#[derive(Debug, Default)] +pub struct ResolvedDocumentNodeTypes { + pub types: HashMap, NodeTypes>, +} + +#[derive(Debug, Default)] +pub struct NodeTypes { + // TODO: This is currently unused. Only the output is used + pub inputs: Vec, + pub output: Type, +} + +impl ResolvedDocumentNodeTypes { + pub fn update(&mut self, delta: ResolvedDocumentNodeTypesDelta) { + for (path, node_type) in delta.add { + self.types.insert(path.to_vec(), node_type); + } + for path in delta.remove { + self.types.remove(&path.to_vec()); + } + } +} + +/// Represents the result of a type query for an input or output connector. +#[derive(Debug, Clone, PartialEq)] +pub enum TypeSource { + // A type that has been compiled based on all upstream types + Compiled(Type), + // The type of value inputs + TaggedValue(Type), + // A type that is guessed from the document node definition + DocumentNodeDefinition(Type), + // When the input is not compiled, the type is unknown and must be guessed from the valid types + Unknown, + + Error(&'static str), +} + +impl TypeSource { + pub fn into_compiled_nested_type(self) -> Option { + match self { + TypeSource::Compiled(compiled_type) => Some(compiled_type.into_nested_type()), + TypeSource::TaggedValue(value_type) => Some(value_type.into_nested_type()), + _ => None, + } + } + + pub fn compiled_nested_type(&self) -> Option<&Type> { + match self { + TypeSource::Compiled(compiled_type) => Some(compiled_type.compiled_nested_type()), + TypeSource::TaggedValue(value_type) => Some(value_type.compiled_nested_type()), + _ => None, + } + } + + // If Some, the type should be displayed in the imports/exports, if None it should be replaced with "import/export index _" + pub fn compiled_nested_type_name(self) -> Option { + self.into_compiled_nested_type().map(|ty| ty.to_string()) + } + + // Used when searching for nodes in the add Node popup + pub fn add_node_string(self) -> Option { + self.into_compiled_nested_type().map(|ty| format!("type:{}", ty.to_string())) + } + + // The type to display in the tooltip + pub fn resolved_type_name(&self) -> String { + match self { + TypeSource::Compiled(compiled_type) => compiled_type.nested_type().to_string(), + TypeSource::TaggedValue(value_type) => value_type.nested_type().to_string(), + TypeSource::DocumentNodeDefinition(_) => "Unknown".to_string(), + TypeSource::Unknown => "Unknown".to_string(), + TypeSource::Error(_) => "Error".to_string(), + } + } +} + +impl NodeNetworkInterface { + /// Get the [`TypeSource`] for any InputConnector + /// If the input is not compiled, then an Unknown or default from the definition is returned + pub fn input_type(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> TypeSource { + let Some(input) = self.input_from_connector(input_connector, network_path) else { + return TypeSource::Error("Could not get input from connector"); + }; + + match input { + NodeInput::Node { node_id, output_index } => { + let input_type = self.output_type(&OutputConnector::node(*node_id, *output_index), network_path); + if input_type == TypeSource::Unknown { + // If we are trying to get the input type of an unknown node, check if it has a reference to its definition and use that input type + if let InputConnector::Node { node_id, input_index } = input_connector { + if let Some(definition) = self.get_node_definition(node_id, network_path) { + if let Some(value) = definition.node_template.document_node.inputs.get(*input_index).cloned().and_then(|input| input.as_value()) { + return TypeSource::DocumentNodeDefinition(value.ty()); + } + } + } + } + input_type + } + NodeInput::Value { tagged_value, .. } => TypeSource::TaggedValue(tagged_value.ty()), + NodeInput::Network { import_index, .. } => { + // Get the input type of the encapsulating node input + let Some((encapsulating_node, encapsulating_path)) = network_path.split_last() else { + return TypeSource::Error("Could not get type of import in document network"); + }; + self.input_type(&InputConnector::node(*encapsulating_node, *import_index), encapsulating_path) + } + NodeInput::Scope(_) => TypeSource::Compiled(concrete!(())), + NodeInput::Reflection(document_node_metadata) => TypeSource::Compiled(document_node_metadata.ty()), + NodeInput::Inline(_) => TypeSource::Compiled(concrete!(InlineRust)), + } + } + + // Gets the default tagged value for an input. If its not compiled, then it tries to get a valid type. If there are no valid types, then it picks a random implementation + pub fn tagged_value_from_input(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> TaggedValue { + let guaranteed_type = match self.input_type(input_connector, network_path) { + TypeSource::Compiled(compiled) => compiled, + TypeSource::TaggedValue(value) => value, + TypeSource::DocumentNodeDefinition(definition) => definition, + TypeSource::Unknown => { + let mut valid_types = match self.valid_input_types(input_connector, network_path) { + Ok(types) => types, + Err(e) => { + log::error!("Error getting valid_input_types for {input_connector:?}: {e}"); + Vec::new() + } + }; + match valid_types.pop() { + Some(valid_type) => valid_type, + None => { + match self.random_downstream_type_from_connector(input_connector, network_path) { + Some(random_type) => random_type, + // If there are no connected protonodes then we give up and return the empty type + None => concrete!(()), + } + } + } + } + TypeSource::Error(e) => { + log::error!("Error getting tagged_value_from_input for {input_connector:?} {e}"); + concrete!(()) + } + }; + TaggedValue::from_type_or_none(&guaranteed_type) + } + + pub fn valid_input_types(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> Result, String> { + match input_connector { + InputConnector::Node { node_id, input_index } => { + let Some(implementation) = self.implementation(node_id, network_path) else { + return Err(format!("Could not get node implementation for {:?} {} in valid_input_types", network_path, *node_id)); + }; + match implementation { + DocumentNodeImplementation::Network(_) => self.valid_output_types(&OutputConnector::Import(input_connector.input_index()), &[network_path, &[*node_id]].concat()), + DocumentNodeImplementation::ProtoNode(proto_node_identifier) => { + let Some(implementations) = NODE_REGISTRY.get(proto_node_identifier) else { + return Err(format!("Protonode {proto_node_identifier:?} not found in registry")); + }; + let valid_output_types = match self.valid_output_types(&OutputConnector::node(*node_id, 0), network_path) { + Ok(valid_types) => valid_types, + Err(e) => return Err(e), + }; + + let valid_types = implementations + .iter() + .filter_map(|(node_io, _)| { + if !valid_output_types.contains(&node_io.return_value) { + return None; + } + let valid_inputs = (0..node_io.inputs.len()).filter(|iterator_index| iterator_index != input_index).all(|iterator_index| { + let input_type = self.input_type(&InputConnector::node(*node_id, iterator_index), network_path); + match input_type.into_compiled_nested_type() { + Some(input_type) => node_io.inputs.get(iterator_index) == Some(&input_type), + None => true, + } + }); + if valid_inputs { node_io.inputs.get(*input_index).cloned() } else { None } + }) + .collect::>(); + Ok(valid_types) + } + DocumentNodeImplementation::Extract => { + log::error!("Input types for extract node not supported"); + Ok(Vec::new()) + } + } + } + InputConnector::Export(export_index) => { + match network_path.split_last() { + Some((encapsulating_node, encapsulating_path)) => self.valid_output_types(&OutputConnector::node(*encapsulating_node, *export_index), encapsulating_path), + None => { + // Valid types for the export are all types that can be fed into the render node + // TODO: Use ::IDENTIFIER + let render_node = "graphene_std::wasm_application_io::RenderNode"; + let Some(implementations) = NODE_REGISTRY.get(&ProtoNodeIdentifier::new(render_node)) else { + return Err(format!("Protonode {render_node:?} not found in registry")); + }; + Ok(implementations.iter().map(|(types, _)| types.inputs[1]).collect()) + } + } + } + } + } + + /// Retrieves the output types for a given document node and its exports. + /// + /// This function traverses the node and its nested network structure (if applicable) to determine + /// the type of the output + /// + /// # Arguments + /// + /// * `node` - A reference to the `DocumentNode` for which to determine output types. + /// * `resolved_types` - A reference to `ResolvedDocumentNodeTypes` containing pre-resolved type information. + /// * `node_id_path` - A slice of `NodeId`s representing the path to the current node in the document graph. + /// + /// # Behavior + /// + /// 1. Retrieves the primary output type from `resolved_types`. + /// 2. If the node is a network: + /// - Iterates through its exports (skipping the first/primary export). + /// - For each export, traverses the network until reaching a protonode or terminal condition. + /// - Determines the output type based on the final node/value encountered. + /// 3. Collects and returns all resolved types. + /// + pub fn output_type(&mut self, output_connector: &OutputConnector, network_path: &[NodeId]) -> TypeSource { + match output_connector { + OutputConnector::Node { node_id, output_index } => { + // First try iterating upstream to the first protonode and try get its compiled type + let Some(implementation) = self.implementation(node_id, network_path) else { + return TypeSource::Error("Could not get implementation"); + }; + match implementation { + DocumentNodeImplementation::Network(_) => self.input_type(&InputConnector::Export(*output_index), &[network_path, &[*node_id]].concat()), + DocumentNodeImplementation::ProtoNode(_) => match self.resolved_types.types.get(&[network_path, &[*node_id]].concat()) { + Some(resolved_type) => TypeSource::Compiled(resolved_type.output.clone()), + None => TypeSource::Unknown, + }, + DocumentNodeImplementation::Extract => TypeSource::Compiled(concrete!(())), + } + } + OutputConnector::Import(import_index) => { + let Some((encapsulating_node, encapsulating_path)) = network_path.split_last() else { + return TypeSource::Error("Cannot get import type in document network"); + }; + self.input_type(&InputConnector::node(*encapsulating_node, *import_index), encapsulating_path) + } + } + } + // The valid output types are all types that are valid for each downstream connection + pub fn valid_output_types(&mut self, output_connector: &OutputConnector, network_path: &[NodeId]) -> Result, String> { + let Some(outward_wires) = self.outward_wires(&network_path) else { + return Err("Could not get outward wires in valid_input_types".to_string()); + }; + let Some(inputs_from_import) = outward_wires.get(output_connector) else { + return Err("Could not get inputs from import in valid_input_types".to_string()); + }; + + let intersection = inputs_from_import + .clone() + .iter() + .filter_map(|input_connector| match self.valid_input_types(input_connector, &network_path) { + Ok(valid_types) => Some(valid_types), + Err(e) => { + log::error!("Error getting valid types in intersection: {e}"); + None + } + }) + .map(|vec| vec.into_iter().collect::>()) + .fold(None, |acc: Option>, set| match acc { + Some(acc_set) => Some(acc_set.intersection(&set).cloned().collect()), + None => Some(set), + }) + .unwrap_or_default(); + + Ok(intersection.into_iter().collect::>()) + } + + pub fn random_downstream_type_from_connector(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> Option { + match input_connector { + InputConnector::Node { node_id, input_index } => { + let Some(implementation) = self.implementation(node_id, network_path) else { + log::error!("Could not get node {node_id} in random_downstream_protonode_from_connector"); + return None; + }; + match implementation { + DocumentNodeImplementation::Network(node_network) => { + let Some(outward_wires) = self.outward_wires(&network_path) else { + log::error!("Could not get outward wires in random_downstream_protonode_from_connector"); + return None; + }; + let Some(inputs_from_import) = outward_wires.get(&OutputConnector::Import(*input_index)) else { + log::error!("Could not get inputs from import in valid_input_types"); + return None; + }; + let Some(first_input) = inputs_from_import.first().cloned() else { + return None; + }; + self.random_downstream_type_from_connector(&first_input, &[network_path, &[node_id]].concat()) + } + DocumentNodeImplementation::ProtoNode(proto_node_identifier) => { + let Some(implementations) = NODE_REGISTRY.get(proto_node_identifier) else { + log::error!("Protonode {proto_node_identifier:?} not found in registry"); + return None; + }; + implementations.keys().next().and_then(|node_io| node_io.inputs.get(input_connector.input_index())).cloned() + } + DocumentNodeImplementation::Extract => None, + } + } + InputConnector::Export(export_index) => network_path.split_last().and_then(|(encapsulating_node, encapsulating_path)| { + let Some(outward_wires) = self.outward_wires(&encapsulating_path) else { + log::error!("Could not get outward wires in random_downstream_protonode_from_connector export"); + return None; + }; + let Some(inputs_from_import) = outward_wires.get(&OutputConnector::node(*encapsulating_node, *export_index)) else { + log::error!("Could not get inputs from import in valid_input_types"); + return None; + }; + let Some(first_input) = inputs_from_import.first().cloned() else { + return None; + }; + self.random_downstream_type_from_connector(&first_input, encapsulating_path) + }), + } + } +} diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index 3227832c9b..8a7740de43 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -483,8 +483,8 @@ impl<'a> NodeGraphLayer<'a> { /// Check if a layer is a raster layer pub fn is_raster_layer(layer: LayerNodeIdentifier, network_interface: &mut NodeNetworkInterface) -> bool { - let layer_input_type = network_interface.input_type(&InputConnector::node(layer.to_node(), 1), &[]).0.nested_type().clone(); + let layer_input_type = network_interface.input_type(&InputConnector::node(layer.to_node(), 1), &[]).into_compiled_nested_type(); - layer_input_type == concrete!(Table>) || layer_input_type == concrete!(Table>) + layer_input_type == Some(concrete!(Table>)) || layer_input_type == Some(concrete!(Table>)) } } diff --git a/editor/src/messages/tool/common_functionality/utility_functions.rs b/editor/src/messages/tool/common_functionality/utility_functions.rs index 4d0a268449..9eaa3dd1e3 100644 --- a/editor/src/messages/tool/common_functionality/utility_functions.rs +++ b/editor/src/messages/tool/common_functionality/utility_functions.rs @@ -581,8 +581,8 @@ pub fn make_path_editable_is_allowed(network_interface: &mut NodeNetworkInterfac // Must be a layer of type Table let node_id = NodeGraphLayer::new(first_layer, network_interface).horizontal_layer_flow().nth(1)?; - let (output_type, _) = network_interface.output_type(&OutputConnector::node(node_id, 0), &[]); - if output_type.nested_type() != concrete!(Table).nested_type() { + let output_type = network_interface.output_type(&OutputConnector::node(node_id, 0), &[]); + if output_type.into_compiled_nested_type() != Some(concrete!(Table)) { return None; } diff --git a/node-graph/gcore/src/types.rs b/node-graph/gcore/src/types.rs index b95b869496..133cb88c1d 100644 --- a/node-graph/gcore/src/types.rs +++ b/node-graph/gcore/src/types.rs @@ -334,6 +334,15 @@ impl Type { } } + pub fn into_nested_type(self) -> Type { + match self { + Self::Generic(_) => self, + Self::Concrete(_) => self, + Self::Fn(_, output) => output.into_nested_type(), + Self::Future(output) => output.into_nested_type(), + } + } + pub fn replace_nested(&mut self, f: impl Fn(&Type) -> Option) -> Option { if let Some(replacement) = f(self) { return Some(std::mem::replace(self, replacement)); diff --git a/node-graph/graph-craft/src/document.rs b/node-graph/graph-craft/src/document.rs index 4598737291..e8f467dc6c 100644 --- a/node-graph/graph-craft/src/document.rs +++ b/node-graph/graph-craft/src/document.rs @@ -209,6 +209,14 @@ pub enum DocumentNodeMetadata { DocumentNodePath, } +impl DocumentNodeMetadata { + pub fn ty(&self) -> Type { + match self { + DocumentNodeMetadata::DocumentNodePath => concrete!(Vec), + } + } +} + impl NodeInput { pub const fn node(node_id: NodeId, output_index: usize) -> Self { Self::Node { node_id, output_index } diff --git a/node-graph/interpreted-executor/src/dynamic_executor.rs b/node-graph/interpreted-executor/src/dynamic_executor.rs index 2f52214ed7..d97db7a77a 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor.rs @@ -33,17 +33,6 @@ impl Default for DynamicExecutor { } } -#[derive(PartialEq, Clone, Debug, Default, serde::Serialize, serde::Deserialize)] -pub struct NodeTypes { - pub inputs: Vec, - pub output: Type, -} - -#[derive(PartialEq, Clone, Debug, Default, serde::Serialize, serde::Deserialize)] -pub struct ResolvedDocumentNodeTypes { - pub types: HashMap, NodeTypes>, -} - type Path = Box<[NodeId]>; #[derive(PartialEq, Clone, Debug, Default, serde::Serialize, serde::Deserialize)] diff --git a/node-graph/interpreted-executor/src/util.rs b/node-graph/interpreted-executor/src/util.rs index cabcee148b..218e7eee21 100644 --- a/node-graph/interpreted-executor/src/util.rs +++ b/node-graph/interpreted-executor/src/util.rs @@ -44,6 +44,7 @@ pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc Date: Sat, 30 Aug 2025 02:04:51 -0700 Subject: [PATCH 03/24] Valid types --- .../document/document_message_handler.rs | 2 +- .../node_graph/node_graph_message_handler.rs | 11 ++-- .../document/node_graph/node_properties.rs | 4 +- .../document/node_graph/utility_types.rs | 5 +- .../utility_types/network_interface.rs | 47 ++++------------ .../network_interface/resolved_types.rs | 53 +++++++++++-------- frontend/src/components/views/Graph.svelte | 26 +++++---- .../src/dynamic_executor.rs | 11 ++-- 8 files changed, 78 insertions(+), 81 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index b8b0c6fcd3..d7f2257b68 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -17,7 +17,7 @@ use crate::messages::portfolio::document::overlays::utility_types::{OverlaysType use crate::messages::portfolio::document::properties_panel::properties_panel_message_handler::PropertiesPanelMessageContext; use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, DocumentMode, FlipAxis, PTZ}; -use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeTemplate, OutputConnector}; +use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeTemplate}; use crate::messages::portfolio::document::utility_types::nodes::RawBuffer; use crate::messages::portfolio::utility_types::PanelType; use crate::messages::portfolio::utility_types::PersistentData; diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index 5e9bde5708..e86870f736 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -10,13 +10,13 @@ use crate::messages::portfolio::document::node_graph::utility_types::{ContextMen use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::misc::GroupFolderType; use crate::messages::portfolio::document::utility_types::network_interface::{ - self, FlowType, InputConnector, NodeNetworkInterface, NodeTemplate, NodeTypePersistentMetadata, OutputConnector, Previewing, TypeSource, + self, FlowType, InputConnector, NodeNetworkInterface, NodeTemplate, NodeTypePersistentMetadata, OutputConnector, Previewing, }; use crate::messages::portfolio::document::utility_types::nodes::{CollapsedLayers, LayerPanelEntry}; use crate::messages::portfolio::document::utility_types::wires::{GraphWireStyle, WirePath, WirePathUpdate, build_vector_wire}; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::auto_panning::AutoPanning; -use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_clip_mode}; +use crate::messages::tool::common_functionality::graph_modification_utils::get_clip_mode; use crate::messages::tool::common_functionality::utility_functions::make_path_editable_is_allowed; use crate::messages::tool::tool_messages::tool_prelude::{Key, MouseMotion}; use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo}; @@ -1206,14 +1206,14 @@ impl<'a> MessageHandler> for NodeG return; } + let compatible_type = network_interface.output_type(&output_connector, selection_network_path).add_node_string(); + // Get the output types from the network interface let Some(network_metadata) = network_interface.network_metadata(selection_network_path) else { warn!("No network_metadata"); return; }; - let compatible_type = network_interface.output_type(&output_connector.unwrap(), selection_network_path).add_node_string(); - let appear_right_of_mouse = if ipp.mouse.position.x > ipp.viewport_bounds.size().x - 173. { -173. } else { 0. }; let appear_above_mouse = if ipp.mouse.position.y > ipp.viewport_bounds.size().y - 34. { -34. } else { 0. }; let node_graph_shift = DVec2::new(appear_right_of_mouse, appear_above_mouse) / network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport.matrix2.x_axis.x; @@ -1648,6 +1648,7 @@ impl<'a> MessageHandler> for NodeG has_left_input_wire, }); responses.add(NodeGraphMessage::SendSelectedNodes); + responses.add(NodeGraphMessage::SendWires); self.update_node_graph_hints(responses); } } @@ -2100,7 +2101,7 @@ impl NodeGraphMessageHandler { .popover_layout({ // Showing only compatible types let compatible_type = match (selection_includes_layers, has_multiple_selection, selected_layer) { - (true, false, Some(layer)) => network_interface.output_type(&OutputConnector::node(node_id, 0), &[]).add_node_string(), + (true, false, Some(layer)) => network_interface.output_type(&OutputConnector::node(layer.to_node(), 1), &[]).add_node_string(), _ => None, }; 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 7b5166c434..cbc38954bf 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -8,9 +8,9 @@ use crate::messages::prelude::*; use choice::enum_choice; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; -use graph_craft::Type; use graph_craft::document::value::TaggedValue; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput}; +use graph_craft::{Type, concrete}; use graphene_std::NodeInputDecleration; use graphene_std::animation::RealTimeMode; use graphene_std::extract_xy::XY; @@ -1996,7 +1996,7 @@ pub struct ParameterWidgetsInfo<'a> { impl<'a> ParameterWidgetsInfo<'a> { pub fn new(node_id: NodeId, index: usize, blank_assist: bool, context: &'a mut NodePropertiesContext) -> ParameterWidgetsInfo<'a> { let (name, description) = context.network_interface.displayed_input_name_and_description(&node_id, index, context.selection_network_path); - let input_type = FrontendGraphDataType::from_type(&context.network_interface.input_type(&InputConnector::node(node_id, index), context.selection_network_path)); + let input_type = FrontendGraphDataType::displayed_type(&context.network_interface.input_type(&InputConnector::node(node_id, index), context.selection_network_path)); let document_node = context.network_interface.document_node(&node_id, context.selection_network_path); ParameterWidgetsInfo { 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 08bfca30e3..5449046e3a 100644 --- a/editor/src/messages/portfolio/document/node_graph/utility_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/utility_types.rs @@ -1,10 +1,11 @@ -use crate::messages::portfolio::document::utility_types::network_interface::TypeSource; use glam::IVec2; use graph_craft::document::NodeId; use graph_craft::document::value::TaggedValue; use graphene_std::Type; use std::borrow::Cow; +use crate::messages::portfolio::document::utility_types::network_interface::resolved_types::TypeSource; + #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize, specta::Type)] pub enum FrontendGraphDataType { #[default] @@ -42,7 +43,7 @@ impl FrontendGraphDataType { } } - pub fn displayed_type(type_source: TypeSource) -> Self { + pub fn displayed_type(type_source: &TypeSource) -> Self { match type_source.compiled_nested_type() { Some(nested_type) => Self::from_type(&nested_type), None => Self::General, diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index 07441df8c3..7df2eb2eb2 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -1,5 +1,3 @@ -mod deserialization; - use super::document_metadata::{DocumentMetadata, LayerNodeIdentifier, NodeRelations}; use super::misc::PTZ; use super::nodes::SelectedNodes; @@ -13,25 +11,23 @@ use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::tool_messages::tool_prelude::NumberInputMode; use deserialization::deserialize_node_persistent_metadata; use glam::{DAffine2, DVec2, IVec2}; +use graph_craft::Type; use graph_craft::document::value::TaggedValue; -use graph_craft::document::{DocumentNode, DocumentNodeImplementation, InlineRust, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork}; -use graph_craft::{ProtoNodeIdentifier, Type, concrete}; +use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork}; use graphene_std::ContextDependencies; use graphene_std::math::quad::Quad; use graphene_std::subpath::Subpath; use graphene_std::transform::Footprint; use graphene_std::vector::click_target::{ClickTarget, ClickTargetType}; use graphene_std::vector::{PointId, Vector, VectorModificationType}; -use interpreted_executor::node_registry::NODE_REGISTRY; use kurbo::BezPath; use serde_json::{Value, json}; use std::collections::{HashMap, HashSet, VecDeque}; -use std::hash::{DefaultHasher, Hash, Hasher}; +use std::hash::Hash; +use std::ops::Deref; mod deserialization; -mod resolved_types; -use deserialization::deserialize_node_persistent_metadata; -use std::ops::Deref; +pub mod resolved_types; /// All network modifications should be done through this API, so the fields cannot be public. However, all fields within this struct can be public since it it not possible to have a public mutable reference. #[derive(Debug, Default, serde::Serialize, serde::Deserialize)] @@ -670,7 +666,7 @@ impl NodeNetworkInterface { let valid_types = match self.valid_input_types(&input_connector, network_path) { Ok(input_types) => input_types.iter().map(|ty| ty.to_string()).collect(), Err(e) => { - log::error!("Error getting valid types for input {input_connector}: {e}"); + log::error!("Error getting valid types for input {input_connector:?}: {e}"); Vec::new() } }; @@ -719,7 +715,7 @@ impl NodeNetworkInterface { } else if let Some(import_type_name) = output_type.compiled_nested_type_name() { import_type_name } else { - format!("Import index {}", export_index) + format!("Import index {}", *import_index) }; (import_name, description) @@ -1061,7 +1057,10 @@ impl NodeNetworkInterface { log::error!("Could not get downstream_connectors in primary_output_connected_to_layer"); return false; }; - let downstream_nodes = downstream_connectors.iter().filter_map(|connector| connector.node_id()).collect::>(); + let downstream_nodes = downstream_connectors + .iter() + .filter_map(|connector| if connector.input_index() == 0 { connector.node_id() } else { None }) + .collect::>(); downstream_nodes.iter().any(|node_id| self.is_layer(node_id, network_path)) } @@ -1444,30 +1443,6 @@ impl NodeNetworkInterface { } } -/// Gets the type for a random protonode implementation (used if there is no type from the compiled network) -fn random_protonode_implementation(protonode: &graph_craft::ProtoNodeIdentifier) -> Option<&graphene_std::NodeIOTypes> { - let mut protonode = protonode.clone(); - // TODO: Remove - if let Some((path, _generics)) = protonode.name.split_once('<') { - protonode = path.to_string().to_string().into(); - } - let Some(node_io_hashmap) = NODE_REGISTRY.get(&protonode) else { - log::error!("Could not get hashmap for proto node: {protonode:?}"); - return None; - }; - - let node_types = node_io_hashmap.keys().min_by_key(|node_io_types| { - let mut hasher = DefaultHasher::new(); - node_io_types.hash(&mut hasher); - hasher.finish() - }); - - if node_types.is_none() { - log::error!("Could not get node_types from hashmap"); - }; - node_types -} - // Private mutable getters for use within the network interface impl NodeNetworkInterface { fn network_mut(&mut self, network_path: &[NodeId]) -> Option<&mut NodeNetwork> { diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/resolved_types.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/resolved_types.rs index 52a10b4bdf..1e099c7ca1 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface/resolved_types.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/resolved_types.rs @@ -1,8 +1,14 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; -use graph_craft::Type; +use graph_craft::{ + ProtoNodeIdentifier, Type, concrete, + document::{DocumentNodeImplementation, InlineRust, NodeInput, value::TaggedValue}, +}; use graphene_std::uuid::NodeId; -use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta; +use interpreted_executor::{ + dynamic_executor::{NodeTypes, ResolvedDocumentNodeTypesDelta}, + node_registry::NODE_REGISTRY, +}; use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface, OutputConnector}; @@ -12,13 +18,6 @@ pub struct ResolvedDocumentNodeTypes { pub types: HashMap, NodeTypes>, } -#[derive(Debug, Default)] -pub struct NodeTypes { - // TODO: This is currently unused. Only the output is used - pub inputs: Vec, - pub output: Type, -} - impl ResolvedDocumentNodeTypes { pub fn update(&mut self, delta: ResolvedDocumentNodeTypesDelta) { for (path, node_type) in delta.add { @@ -56,20 +55,20 @@ impl TypeSource { pub fn compiled_nested_type(&self) -> Option<&Type> { match self { - TypeSource::Compiled(compiled_type) => Some(compiled_type.compiled_nested_type()), - TypeSource::TaggedValue(value_type) => Some(value_type.compiled_nested_type()), + TypeSource::Compiled(compiled_type) => Some(compiled_type.nested_type()), + TypeSource::TaggedValue(value_type) => Some(value_type.nested_type()), _ => None, } } // If Some, the type should be displayed in the imports/exports, if None it should be replaced with "import/export index _" - pub fn compiled_nested_type_name(self) -> Option { - self.into_compiled_nested_type().map(|ty| ty.to_string()) + pub fn compiled_nested_type_name(&self) -> Option { + self.compiled_nested_type().map(|ty| ty.to_string()) } // Used when searching for nodes in the add Node popup - pub fn add_node_string(self) -> Option { - self.into_compiled_nested_type().map(|ty| format!("type:{}", ty.to_string())) + pub fn add_node_string(&self) -> Option { + self.compiled_nested_type().map(|ty| format!("type:{}", ty.to_string())) } // The type to display in the tooltip @@ -99,8 +98,15 @@ impl NodeNetworkInterface { // If we are trying to get the input type of an unknown node, check if it has a reference to its definition and use that input type if let InputConnector::Node { node_id, input_index } = input_connector { if let Some(definition) = self.get_node_definition(node_id, network_path) { - if let Some(value) = definition.node_template.document_node.inputs.get(*input_index).cloned().and_then(|input| input.as_value()) { - return TypeSource::DocumentNodeDefinition(value.ty()); + if let Some(ty) = definition + .node_template + .document_node + .inputs + .get(*input_index) + .cloned() + .and_then(|input| input.as_value().map(|value| value.ty())) + { + return TypeSource::DocumentNodeDefinition(ty); } } } @@ -174,13 +180,14 @@ impl NodeNetworkInterface { let valid_types = implementations .iter() .filter_map(|(node_io, _)| { - if !valid_output_types.contains(&node_io.return_value) { + if !valid_output_types.iter().any(|output_type| output_type.nested_type() == node_io.return_value.nested_type()) { return None; } + let valid_inputs = (0..node_io.inputs.len()).filter(|iterator_index| iterator_index != input_index).all(|iterator_index| { let input_type = self.input_type(&InputConnector::node(*node_id, iterator_index), network_path); match input_type.into_compiled_nested_type() { - Some(input_type) => node_io.inputs.get(iterator_index) == Some(&input_type), + Some(input_type) => node_io.inputs.get(iterator_index).map(|input_type| input_type.nested_type()) == Some(&input_type), None => true, } }); @@ -205,7 +212,7 @@ impl NodeNetworkInterface { let Some(implementations) = NODE_REGISTRY.get(&ProtoNodeIdentifier::new(render_node)) else { return Err(format!("Protonode {render_node:?} not found in registry")); }; - Ok(implementations.iter().map(|(types, _)| types.inputs[1]).collect()) + Ok(implementations.iter().map(|(types, _)| types.inputs[1].clone()).collect()) } } } @@ -293,7 +300,7 @@ impl NodeNetworkInterface { return None; }; match implementation { - DocumentNodeImplementation::Network(node_network) => { + DocumentNodeImplementation::Network(_) => { let Some(outward_wires) = self.outward_wires(&network_path) else { log::error!("Could not get outward wires in random_downstream_protonode_from_connector"); return None; @@ -305,7 +312,7 @@ impl NodeNetworkInterface { let Some(first_input) = inputs_from_import.first().cloned() else { return None; }; - self.random_downstream_type_from_connector(&first_input, &[network_path, &[node_id]].concat()) + self.random_downstream_type_from_connector(&first_input, &[network_path, &[*node_id]].concat()) } DocumentNodeImplementation::ProtoNode(proto_node_identifier) => { let Some(implementations) = NODE_REGISTRY.get(proto_node_identifier) else { diff --git a/frontend/src/components/views/Graph.svelte b/frontend/src/components/views/Graph.svelte index 7ac2d4f162..9c8d2f8b58 100644 --- a/frontend/src/components/views/Graph.svelte +++ b/frontend/src/components/views/Graph.svelte @@ -176,6 +176,14 @@ return `M-2,-2 L${nodeWidth + 2},-2 L${nodeWidth + 2},${nodeHeight + 2} L-2,${nodeHeight + 2}z ${rectangles.join(" ")}`; } + function inputTooltip(value: FrontendGraphInput): string { + return dataTypeTooltip(value) + "\n\n" + inputConnectedToText(value) + "\n\n" + validTypesText(value); + } + + function outputTooltip(value: FrontendGraphOutput): string { + return dataTypeTooltip(value) + "\n\n" + outputConnectedToText(value); + } + function dataTypeTooltip(value: FrontendGraphInput | FrontendGraphOutput): string { return `Data Type: ${value.resolvedType}`; } @@ -317,7 +325,7 @@ style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 8) / 24} style:--offset-top={($nodeGraph.updateImportsExports.importPosition.y - 8) / 24 + index} > - {`${dataTypeTooltip(frontendOutput)}\n\n${outputConnectedToText(frontendOutput)}`} + {outputTooltip(frontendOutput)} {#if frontendOutput.connectedTo.length > 0} {:else} @@ -389,7 +397,7 @@ style:--offset-left={($nodeGraph.updateImportsExports.exportPosition.x - 8) / 24} style:--offset-top={($nodeGraph.updateImportsExports.exportPosition.y - 8) / 24 + index} > - {`${dataTypeTooltip(frontendInput)}\n\n${inputConnectedToText(frontendInput)}`} + {inputTooltip(frontendInput)} {#if frontendInput.connectedTo !== "nothing"} {:else} @@ -528,7 +536,7 @@ style:--data-color={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()})`} style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()}-dim)`} > - {`${dataTypeTooltip(node.primaryOutput)}\n\n${outputConnectedToText(node.primaryOutput)}`} + {outputTooltip(node.primaryOutput)} {#if node.primaryOutput.connectedTo.length > 0} {#if node.primaryOutputConnectedToLayer} @@ -550,7 +558,7 @@ style:--data-color-dim={`var(--color-data-${(node.primaryInput?.dataType || "General").toLowerCase()}-dim)`} > {#if node.primaryInput} - {`${dataTypeTooltip(node.primaryInput)}\n\n${validTypesText(node.primaryInput)}\n\n${inputConnectedToText(node.primaryInput)}`} + {inputTooltip(node.primaryInput)} {/if} {#if node.primaryInput?.connectedTo !== "nothing"} @@ -574,7 +582,7 @@ style:--data-color={`var(--color-data-${stackDataInput.dataType.toLowerCase()})`} style:--data-color-dim={`var(--color-data-${stackDataInput.dataType.toLowerCase()}-dim)`} > - {`${dataTypeTooltip(stackDataInput)}\n\n${validTypesText(stackDataInput)}\n\n${inputConnectedToText(stackDataInput)}`} + {inputTooltip(stackDataInput)} {#if stackDataInput.connectedTo !== undefined} {:else} @@ -692,7 +700,7 @@ style:--data-color={`var(--color-data-${node.primaryInput.dataType.toLowerCase()})`} style:--data-color-dim={`var(--color-data-${node.primaryInput.dataType.toLowerCase()}-dim)`} > - {`${dataTypeTooltip(node.primaryInput)}\n\n${validTypesText(node.primaryInput)}\n\n${inputConnectedToText(node.primaryInput)}`} + {inputTooltip(node.primaryInput)} {#if node.primaryInput.connectedTo !== undefined} {:else} @@ -711,7 +719,7 @@ style:--data-color={`var(--color-data-${secondary.dataType.toLowerCase()})`} style:--data-color-dim={`var(--color-data-${secondary.dataType.toLowerCase()}-dim)`} > - {`${dataTypeTooltip(secondary)}\n\n${validTypesText(secondary)}\n\n${inputConnectedToText(secondary)}`} + {inputTooltip(secondary)} {#if secondary.connectedTo !== undefined} {:else} @@ -733,7 +741,7 @@ style:--data-color={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()})`} style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()}-dim)`} > - {`${dataTypeTooltip(node.primaryOutput)}\n\n${outputConnectedToText(node.primaryOutput)}`} + {outputTooltip(node.primaryOutput)} {#if node.primaryOutput.connectedTo !== undefined} {:else} @@ -751,7 +759,7 @@ style:--data-color={`var(--color-data-${secondary.dataType.toLowerCase()})`} style:--data-color-dim={`var(--color-data-${secondary.dataType.toLowerCase()}-dim)`} > - {`${dataTypeTooltip(secondary)}\n\n${outputConnectedToText(secondary)}`} + {outputTooltip(secondary)} {#if secondary.connectedTo !== undefined} {:else} diff --git a/node-graph/interpreted-executor/src/dynamic_executor.rs b/node-graph/interpreted-executor/src/dynamic_executor.rs index d97db7a77a..53f23fa50f 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor.rs @@ -35,12 +35,19 @@ impl Default for DynamicExecutor { type Path = Box<[NodeId]>; -#[derive(PartialEq, Clone, Debug, Default, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)] pub struct ResolvedDocumentNodeTypesDelta { pub add: Vec<(Path, NodeTypes)>, pub remove: Vec, } +#[derive(Debug, Default, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct NodeTypes { + // This is currently unused. Only the output is used + pub inputs: Vec, + pub output: Type, +} + impl DynamicExecutor { pub async fn new(proto_network: ProtoNetwork) -> Result { let mut typing_context = TypingContext::new(&node_registry::NODE_REGISTRY); @@ -100,8 +107,6 @@ impl DynamicExecutor { pub fn document_node_types<'a>(&'a self, nodes: impl Iterator + 'a) -> impl Iterator + 'a { nodes.flat_map(|id| self.tree.source_map().get(&id).map(|(_, b)| (id, b.clone()))) - // TODO: https://github.com/GraphiteEditor/Graphite/issues/1767 - // TODO: Non exposed inputs are not added to the inputs_source_map, so they are not included in the resolved_document_node_types. The type is still available in the typing_context. This only affects the UI-only "Import" node. } } From 59d85cb53e4e76110f89efb7e6c31897999ae375 Mon Sep 17 00:00:00 2001 From: Adam Date: Sat, 30 Aug 2025 21:19:32 -0700 Subject: [PATCH 04/24] Remove valid types --- .../document/node_graph/utility_types.rs | 2 -- .../document/utility_types/network_interface.rs | 15 +++++++-------- frontend/src/components/views/Graph.svelte | 10 +++++----- frontend/src/messages.ts | 2 -- 4 files changed, 12 insertions(+), 17 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 5449046e3a..07741ee003 100644 --- a/editor/src/messages/portfolio/document/node_graph/utility_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/utility_types.rs @@ -59,8 +59,6 @@ pub struct FrontendGraphInput { pub description: String, #[serde(rename = "resolvedType")] pub resolved_type: String, - #[serde(rename = "validTypes")] - pub valid_types: Vec, #[serde(rename = "connectedTo")] /// Either "nothing", "import index {index}", or "{node name} output {output_index}". pub connected_to: String, diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index 7df2eb2eb2..27f93d5ecb 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -663,20 +663,19 @@ impl NodeNetworkInterface { }; // TODO: Move in separate Tooltip overlay - let valid_types = match self.valid_input_types(&input_connector, network_path) { - Ok(input_types) => input_types.iter().map(|ty| ty.to_string()).collect(), - Err(e) => { - log::error!("Error getting valid types for input {input_connector:?}: {e}"); - Vec::new() - } - }; + // let valid_types = match self.valid_input_types(&input_connector, network_path) { + // Ok(input_types) => input_types.iter().map(|ty| ty.to_string()).collect(), + // Err(e) => { + // log::error!("Error getting valid types for input {input_connector:?}: {e}"); + // Vec::new() + // } + // }; Some(FrontendGraphInput { data_type, resolved_type, name, description, - valid_types, connected_to, }) } diff --git a/frontend/src/components/views/Graph.svelte b/frontend/src/components/views/Graph.svelte index 9c8d2f8b58..d6cef02fec 100644 --- a/frontend/src/components/views/Graph.svelte +++ b/frontend/src/components/views/Graph.svelte @@ -177,7 +177,7 @@ } function inputTooltip(value: FrontendGraphInput): string { - return dataTypeTooltip(value) + "\n\n" + inputConnectedToText(value) + "\n\n" + validTypesText(value); + return dataTypeTooltip(value) + "\n\n" + inputConnectedToText(value) + "\n\n"; } function outputTooltip(value: FrontendGraphOutput): string { @@ -188,10 +188,10 @@ return `Data Type: ${value.resolvedType}`; } - function validTypesText(value: FrontendGraphInput): string { - const validTypes = value.validTypes.length > 0 ? value.validTypes.map((x) => `• ${x}`).join("\n") : "None"; - return `Valid Types:\n${validTypes}`; - } + // function validTypesText(value: FrontendGraphInput): string { + // const validTypes = value.validTypes.length > 0 ? value.validTypes.map((x) => `• ${x}`).join("\n") : "None"; + // return `Valid Types:\n${validTypes}`; + // } function outputConnectedToText(output: FrontendGraphOutput): string { if (output.connectedTo.length === 0) return "Connected to nothing"; diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index 83234a04d0..44d3bb5fa9 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -195,8 +195,6 @@ export class FrontendGraphInput { readonly resolvedType!: string; - readonly validTypes!: string[]; - readonly connectedTo!: string; } From 76a2cbdb2529cab176fa4f58a19813f674bb71ef Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 31 Aug 2025 15:07:24 -0700 Subject: [PATCH 05/24] Move frontend node graph functions into separate file --- .../node_graph/node_graph_message_handler.rs | 92 +--- .../utility_types/network_interface.rs | 310 +------------- .../network_interface/node_graph.rs | 404 ++++++++++++++++++ 3 files changed, 409 insertions(+), 397 deletions(-) create mode 100644 editor/src/messages/portfolio/document/utility_types/network_interface/node_graph.rs diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index e86870f736..688f455f5b 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -1,4 +1,4 @@ -use super::utility_types::{BoxSelection, ContextMenuInformation, DragStart, FrontendNode}; +use super::utility_types::{BoxSelection, ContextMenuInformation, DragStart}; use super::{document_node_definitions, node_properties}; use crate::consts::GRID_SIZE; use crate::messages::input_mapper::utility_types::macros::action_keys; @@ -1634,7 +1634,7 @@ impl<'a> MessageHandler> for NodeG responses.add(DocumentMessage::DocumentStructureChanged); responses.add(PropertiesPanelMessage::Refresh); if breadcrumb_network_path == selection_network_path && graph_view_overlay_open { - let nodes = self.collect_nodes(network_interface, breadcrumb_network_path); + let nodes = network_interface.collect_nodes(&self.node_graph_errors, breadcrumb_network_path); self.frontend_nodes = nodes.iter().map(|node| node.id).collect(); responses.add(FrontendMessage::UpdateNodeGraphNodes { nodes }); responses.add(NodeGraphMessage::UpdateVisibleNodes); @@ -2499,94 +2499,6 @@ impl NodeGraphMessageHandler { added_wires } - fn collect_nodes(&self, network_interface: &mut NodeNetworkInterface, breadcrumb_network_path: &[NodeId]) -> Vec { - let Some(network) = network_interface.nested_network(breadcrumb_network_path) else { - log::error!("Could not get nested network when collecting nodes"); - return Vec::new(); - }; - let mut nodes = Vec::new(); - for (node_id, visible) in network.nodes.iter().map(|(node_id, node)| (*node_id, node.visible)).collect::>() { - let node_id_path = [breadcrumb_network_path, &[node_id]].concat(); - - let primary_input_connector = InputConnector::node(node_id, 0); - - let primary_input = if network_interface - .input_from_connector(&primary_input_connector, breadcrumb_network_path) - .is_some_and(|input| input.is_exposed()) - { - network_interface.frontend_input_from_connector(&primary_input_connector, breadcrumb_network_path) - } else { - None - }; - let exposed_inputs = (1..network_interface.number_of_inputs(&node_id, breadcrumb_network_path)) - .filter_map(|input_index| network_interface.frontend_input_from_connector(&InputConnector::node(node_id, input_index), breadcrumb_network_path)) - .collect(); - - let primary_output = network_interface.frontend_output_from_connector(&OutputConnector::node(node_id, 0), breadcrumb_network_path); - - let exposed_outputs = (1..network_interface.number_of_outputs(&node_id, breadcrumb_network_path)) - .filter_map(|output_index| network_interface.frontend_output_from_connector(&OutputConnector::node(node_id, output_index), breadcrumb_network_path)) - .collect(); - let (primary_output_connected_to_layer, primary_input_connected_to_layer) = if network_interface.is_layer(&node_id, breadcrumb_network_path) { - ( - network_interface.primary_output_connected_to_layer(&node_id, breadcrumb_network_path), - network_interface.primary_input_connected_to_layer(&node_id, breadcrumb_network_path), - ) - } else { - (false, false) - }; - - let is_export = network_interface - .input_from_connector(&InputConnector::Export(0), breadcrumb_network_path) - .is_some_and(|export| export.as_node().is_some_and(|export_node_id| node_id == export_node_id)); - let is_root_node = network_interface.root_node(breadcrumb_network_path).is_some_and(|root_node| root_node.node_id == node_id); - - let Some(position) = network_interface.position(&node_id, breadcrumb_network_path) else { - log::error!("Could not get position for node: {node_id}"); - continue; - }; - let previewed = is_export && !is_root_node; - - let locked = network_interface.is_locked(&node_id, breadcrumb_network_path); - - let errors = self - .node_graph_errors - .iter() - .find(|error| error.node_path == node_id_path) - .map(|error| format!("{:?}", error.error.clone())) - .or_else(|| { - if self.node_graph_errors.iter().any(|error| error.node_path.starts_with(&node_id_path)) { - Some("Node graph type error within this node".to_string()) - } else { - None - } - }); - - nodes.push(FrontendNode { - id: node_id, - is_layer: network_interface - .node_metadata(&node_id, breadcrumb_network_path) - .is_some_and(|node_metadata| node_metadata.persistent_metadata.is_layer()), - can_be_layer: network_interface.is_eligible_to_be_layer(&node_id, breadcrumb_network_path), - reference: network_interface.reference(&node_id, breadcrumb_network_path).cloned().unwrap_or_default(), - display_name: network_interface.display_name(&node_id, breadcrumb_network_path), - primary_input, - exposed_inputs, - primary_output, - exposed_outputs, - primary_output_connected_to_layer, - primary_input_connected_to_layer, - position, - previewed, - visible, - locked, - errors, - }); - } - - nodes - } - fn collect_subgraph_names(network_interface: &mut NodeNetworkInterface, breadcrumb_network_path: &[NodeId]) -> Option> { let mut current_network_path = vec![]; let mut current_network = network_interface.nested_network(¤t_network_path).unwrap(); diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index 27f93d5ecb..97fcde3ac0 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -4,7 +4,7 @@ use super::nodes::SelectedNodes; use crate::consts::{EXPORTS_TO_RIGHT_EDGE_PIXEL_GAP, EXPORTS_TO_TOP_EDGE_PIXEL_GAP, GRID_SIZE, IMPORTS_TO_LEFT_EDGE_PIXEL_GAP, IMPORTS_TO_TOP_EDGE_PIXEL_GAP}; use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext; use crate::messages::portfolio::document::node_graph::document_node_definitions::{DocumentNodeDefinition, resolve_document_node_type}; -use crate::messages::portfolio::document::node_graph::utility_types::{Direction, FrontendClickTargets, FrontendGraphDataType, FrontendGraphInput, FrontendGraphOutput}; +use crate::messages::portfolio::document::node_graph::utility_types::{Direction, FrontendClickTargets, FrontendGraphDataType}; use crate::messages::portfolio::document::utility_types::network_interface::resolved_types::ResolvedDocumentNodeTypes; use crate::messages::portfolio::document::utility_types::wires::{GraphWireStyle, WirePath, WirePathUpdate, build_vector_wire}; use crate::messages::tool::common_functionality::graph_modification_utils; @@ -27,6 +27,7 @@ use std::hash::Hash; use std::ops::Deref; mod deserialization; +pub mod node_graph; pub mod resolved_types; /// All network modifications should be done through this API, so the fields cannot be public. However, all fields within this struct can be public since it it not possible to have a public mutable reference. @@ -227,31 +228,6 @@ impl NodeNetworkInterface { layers } - pub fn chain_width(&self, node_id: &NodeId, network_path: &[NodeId]) -> u32 { - if self.number_of_displayed_inputs(node_id, network_path) > 1 { - let mut last_chain_node_distance = 0u32; - // Iterate upstream from the layer, and get the number of nodes distance to the last node with Position::Chain - for (index, node_id) in self - .upstream_flow_back_from_nodes(vec![*node_id], network_path, FlowType::HorizontalPrimaryOutputFlow) - .skip(1) - .enumerate() - .collect::>() - { - // Check if the node is positioned as a chain - if self.is_chain(&node_id, network_path) { - last_chain_node_distance = (index as u32) + 1; - } else { - return last_chain_node_distance * 7 + 1; - } - } - - last_chain_node_distance * 7 + 1 - } else { - // Layer with no inputs has no chain - 0 - } - } - /// Check if the specified node id is connected to the output pub fn connected_to_output(&self, target_node_id: &NodeId, network_path: &[NodeId]) -> bool { let Some(network) = self.nested_network(network_path) else { @@ -506,256 +482,6 @@ impl NodeNetworkInterface { }) } - pub fn frontend_imports(&mut self, network_path: &[NodeId]) -> Vec> { - match network_path.split_last() { - Some((node_id, encapsulating_network_path)) => { - let Some(node) = self.document_node(node_id, encapsulating_network_path) else { - log::error!("Could not get node {node_id} in network {encapsulating_network_path:?}"); - return Vec::new(); - }; - let mut frontend_imports = (0..node.inputs.len()) - .map(|import_index| self.frontend_output_from_connector(&OutputConnector::Import(import_index), network_path)) - .collect::>(); - if frontend_imports.is_empty() { - frontend_imports.push(None); - } - frontend_imports - } - // In the document network display no imports - None => Vec::new(), - } - } - - pub fn frontend_exports(&mut self, network_path: &[NodeId]) -> Vec> { - let Some(network) = self.nested_network(network_path) else { return Vec::new() }; - let mut frontend_exports = ((0..network.exports.len()).map(|export_index| self.frontend_input_from_connector(&InputConnector::Export(export_index), network_path))).collect::>(); - if frontend_exports.is_empty() { - frontend_exports.push(None); - } - frontend_exports - } - - pub fn import_export_position(&mut self, network_path: &[NodeId]) -> Option<(IVec2, IVec2)> { - let Some(all_nodes_bounding_box) = self.all_nodes_bounding_box(network_path).cloned() else { - log::error!("Could not get all nodes bounding box in load_export_ports"); - return None; - }; - let Some(network) = self.nested_network(network_path) else { - log::error!("Could not get current network in load_export_ports"); - return None; - }; - - let Some(network_metadata) = self.network_metadata(network_path) else { - log::error!("Could not get nested network_metadata in load_export_ports"); - return None; - }; - let node_graph_to_viewport = network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport; - let target_viewport_top_left = DVec2::new(IMPORTS_TO_LEFT_EDGE_PIXEL_GAP as f64, IMPORTS_TO_TOP_EDGE_PIXEL_GAP as f64); - - let node_graph_pixel_offset_top_left = node_graph_to_viewport.inverse().transform_point2(target_viewport_top_left); - - // A 5x5 grid offset from the top left corner - let node_graph_grid_space_offset_top_left = node_graph_to_viewport.inverse().transform_point2(DVec2::ZERO) + DVec2::new(5. * GRID_SIZE as f64, 4. * GRID_SIZE as f64); - - // The inner bound of the import is the highest/furthest left of the two offsets - let top_left_inner_bound = DVec2::new( - node_graph_pixel_offset_top_left.x.min(node_graph_grid_space_offset_top_left.x), - node_graph_pixel_offset_top_left.y.min(node_graph_grid_space_offset_top_left.y), - ); - - let offset_from_top_left = if network - .exports - .first() - .is_some_and(|export| export.as_node().is_some_and(|export_node| self.is_layer(&export_node, network_path))) - { - DVec2::new(-4. * GRID_SIZE as f64, -2. * GRID_SIZE as f64) - } else { - DVec2::new(-4. * GRID_SIZE as f64, 0.) - }; - - let bounding_box_top_left = DVec2::new((all_nodes_bounding_box[0].x / 24. + 0.5).floor() * 24., (all_nodes_bounding_box[0].y / 24. + 0.5).floor() * 24.) + offset_from_top_left; - let import_top_left = DVec2::new(top_left_inner_bound.x.min(bounding_box_top_left.x), top_left_inner_bound.y.min(bounding_box_top_left.y)); - let rounded_import_top_left = DVec2::new((import_top_left.x / 24.).round() * 24., (import_top_left.y / 24.).round() * 24.); - - let viewport_top_right = network_metadata.persistent_metadata.navigation_metadata.node_graph_top_right; - let target_viewport_top_right = DVec2::new( - viewport_top_right.x - EXPORTS_TO_RIGHT_EDGE_PIXEL_GAP as f64, - viewport_top_right.y + EXPORTS_TO_TOP_EDGE_PIXEL_GAP as f64, - ); - - // An offset from the right edge in viewport pixels - let node_graph_pixel_offset_top_right = node_graph_to_viewport.inverse().transform_point2(target_viewport_top_right); - - // A 5x5 grid offset from the right corner - let node_graph_grid_space_offset_top_right = node_graph_to_viewport.inverse().transform_point2(viewport_top_right) + DVec2::new(-5. * GRID_SIZE as f64, 4. * GRID_SIZE as f64); - - // The inner bound of the export is the highest/furthest right of the two offsets - let top_right_inner_bound = DVec2::new( - node_graph_pixel_offset_top_right.x.max(node_graph_grid_space_offset_top_right.x), - node_graph_pixel_offset_top_right.y.min(node_graph_grid_space_offset_top_right.y), - ); - - let offset_from_top_right = if network - .exports - .first() - .is_some_and(|export| export.as_node().is_some_and(|export_node| self.is_layer(&export_node, network_path))) - { - DVec2::new(2. * GRID_SIZE as f64, -2. * GRID_SIZE as f64) - } else { - DVec2::new(4. * GRID_SIZE as f64, 0.) - }; - - let mut bounding_box_top_right = DVec2::new((all_nodes_bounding_box[1].x / 24. + 0.5).floor() * 24., (all_nodes_bounding_box[0].y / 24. + 0.5).floor() * 24.); - bounding_box_top_right += offset_from_top_right; - let export_top_right = DVec2::new(top_right_inner_bound.x.max(bounding_box_top_right.x), top_right_inner_bound.y.min(bounding_box_top_right.y)); - let rounded_export_top_right = DVec2::new((export_top_right.x / 24.).round() * 24., (export_top_right.y / 24.).round() * 24.); - - Some((rounded_import_top_left.as_ivec2(), rounded_export_top_right.as_ivec2())) - } - - /// Returns None if there is an error, it is a hidden primary export, or a hidden input - pub fn frontend_input_from_connector(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> Option { - // Return None if it is a hidden input - if self.input_from_connector(input_connector, network_path).is_some_and(|input| !input.is_exposed()) { - return None; - } - let input_type = self.input_type(input_connector, network_path); - let data_type = FrontendGraphDataType::displayed_type(&input_type); - let resolved_type = input_type.resolved_type_name(); - - let connected_to = self - .upstream_output_connector(input_connector, network_path) - .map(|output_connector| match output_connector { - OutputConnector::Node { node_id, output_index } => { - let mut name = self.display_name(&node_id, network_path); - if cfg!(debug_assertions) { - name.push_str(&format!(" (id: {node_id})")); - } - format!("{name} output {output_index}") - } - OutputConnector::Import(import_index) => format!("Import index {import_index}"), - }) - .unwrap_or("nothing".to_string()); - - let (name, description) = match input_connector { - InputConnector::Node { node_id, input_index } => self.displayed_input_name_and_description(node_id, *input_index, network_path), - InputConnector::Export(export_index) => { - // Get export name from parent node metadata input, which must match the number of exports. - // Empty string means to use type, or "Export + index" if type is empty determined - let export_name = if network_path.is_empty() { - "Canvas".to_string() - } else { - self.encapsulating_node_metadata(network_path) - .and_then(|encapsulating_metadata| encapsulating_metadata.persistent_metadata.output_names.get(*export_index).cloned()) - .unwrap_or_default() - }; - - let export_name = if !export_name.is_empty() { - export_name - } else if let Some(export_type_name) = input_type.compiled_nested_type_name() { - export_type_name - } else { - format!("Export index {}", export_index) - }; - - (export_name, String::new()) - } - }; - - // TODO: Move in separate Tooltip overlay - // let valid_types = match self.valid_input_types(&input_connector, network_path) { - // Ok(input_types) => input_types.iter().map(|ty| ty.to_string()).collect(), - // Err(e) => { - // log::error!("Error getting valid types for input {input_connector:?}: {e}"); - // Vec::new() - // } - // }; - - Some(FrontendGraphInput { - data_type, - resolved_type, - name, - description, - connected_to, - }) - } - - /// Returns None if there is an error, it is the document network, a hidden primary output or import - pub fn frontend_output_from_connector(&mut self, output_connector: &OutputConnector, network_path: &[NodeId]) -> Option { - let output_type = self.output_type(output_connector, network_path); - - let (name, description) = match output_connector { - OutputConnector::Node { node_id, output_index } => { - // Do not display the primary output port for a node if it is a network node with a hidden primary export - if *output_index == 0 && self.hidden_primary_output(node_id, network_path) { - return None; - }; - // Get the output name from the interior network export name - let node_metadata = self.node_metadata(node_id, network_path)?; - let output_name = node_metadata.persistent_metadata.output_names.get(*output_index).cloned().unwrap_or_default(); - - let output_name = if !output_name.is_empty() { output_name } else { output_type.resolved_type_name() }; - (output_name, String::new()) - } - OutputConnector::Import(import_index) => { - // Get the import name from the encapsulating node input metadata - let Some((encapsulating_node_id, encapsulating_path)) = network_path.split_last() else { - // Return None if it is an import in the document network - return None; - }; - // Return None if the primary input is hidden and this is the primary import - if *import_index == 0 && self.hidden_primary_import(network_path) { - return None; - }; - let (import_name, description) = self.displayed_input_name_and_description(encapsulating_node_id, *import_index, encapsulating_path); - - let import_name = if !import_name.is_empty() { - import_name - } else if let Some(import_type_name) = output_type.compiled_nested_type_name() { - import_type_name - } else { - format!("Import index {}", *import_index) - }; - - (import_name, description) - } - }; - let data_type = FrontendGraphDataType::displayed_type(&output_type); - let resolved_type = output_type.resolved_type_name(); - let mut connected_to = self - .outward_wires(network_path) - .and_then(|outward_wires| outward_wires.get(output_connector)) - .cloned() - .unwrap_or_else(|| { - log::error!("Could not get {output_connector:?} in outward wires"); - Vec::new() - }) - .iter() - .map(|input| match input { - InputConnector::Node { node_id, input_index } => { - let mut name = self.display_name(node_id, network_path); - if cfg!(debug_assertions) { - name.push_str(&format!(" (id: {node_id})")); - } - format!("{name} input {input_index}") - } - InputConnector::Export(export_index) => format!("Export index {export_index}"), - }) - .collect::>(); - - if connected_to.is_empty() { - connected_to.push("nothing".to_string()); - } - - Some(FrontendGraphOutput { - data_type, - resolved_type, - name, - description, - connected_to, - }) - } - pub fn height_from_click_target(&mut self, node_id: &NodeId, network_path: &[NodeId]) -> Option { let mut node_height: Option = self .node_click_targets(node_id, network_path) @@ -1047,28 +773,6 @@ impl NodeNetworkInterface { node_metadata.persistent_metadata.is_layer() } - pub fn primary_output_connected_to_layer(&mut self, node_id: &NodeId, network_path: &[NodeId]) -> bool { - let Some(outward_wires) = self.outward_wires(network_path) else { - log::error!("Could not get outward_wires in primary_output_connected_to_layer"); - return false; - }; - let Some(downstream_connectors) = outward_wires.get(&OutputConnector::node(*node_id, 0)) else { - log::error!("Could not get downstream_connectors in primary_output_connected_to_layer"); - return false; - }; - let downstream_nodes = downstream_connectors - .iter() - .filter_map(|connector| if connector.input_index() == 0 { connector.node_id() } else { None }) - .collect::>(); - downstream_nodes.iter().any(|node_id| self.is_layer(node_id, network_path)) - } - - pub fn primary_input_connected_to_layer(&mut self, node_id: &NodeId, network_path: &[NodeId]) -> bool { - self.input_from_connector(&InputConnector::node(*node_id, 0), network_path) - .and_then(|input| input.as_node()) - .is_some_and(|node_id| self.is_layer(&node_id, network_path)) - } - pub fn hidden_primary_export(&self, network_path: &[NodeId]) -> bool { let Some((node_id, network_path)) = network_path.split_last() else { // The document network does not have a hidden primary export @@ -3167,15 +2871,7 @@ impl NodeNetworkInterface { let chain_widths = nodes.iter().map(|node_id| (*node_id, self.chain_width(node_id, network_path))).collect::>(); let has_left_input_wire = nodes .iter() - .map(|node_id| { - ( - *node_id, - !self - .upstream_flow_back_from_nodes(vec![*node_id], network_path, FlowType::HorizontalFlow) - .skip(1) - .all(|node_id| self.is_chain(&node_id, network_path)), - ) - }) + .map(|node_id| (*node_id, self.layer_has_left_border_gap(node_id, network_path))) .collect::>(); (layer_widths, chain_widths, has_left_input_wire) diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/node_graph.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/node_graph.rs new file mode 100644 index 0000000000..b1d94d9ab8 --- /dev/null +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/node_graph.rs @@ -0,0 +1,404 @@ +use glam::{DVec2, IVec2}; +use graph_craft::proto::GraphErrors; +use graphene_std::uuid::NodeId; + +use crate::{ + consts::{EXPORTS_TO_RIGHT_EDGE_PIXEL_GAP, EXPORTS_TO_TOP_EDGE_PIXEL_GAP, GRID_SIZE, IMPORTS_TO_LEFT_EDGE_PIXEL_GAP, IMPORTS_TO_TOP_EDGE_PIXEL_GAP}, + messages::portfolio::document::{ + node_graph::utility_types::{FrontendGraphDataType, FrontendGraphInput, FrontendGraphOutput, FrontendNode}, + utility_types::network_interface::{FlowType, InputConnector, NodeNetworkInterface, OutputConnector}, + }, +}; + +// Functions used to collect data from the network interface for use in rendering the node graph +impl NodeNetworkInterface { + pub fn collect_nodes(&mut self, node_graph_errors: &GraphErrors, network_path: &[NodeId]) -> Vec { + let Some(network) = self.nested_network(network_path) else { + log::error!("Could not get nested network when collecting nodes"); + return Vec::new(); + }; + let mut nodes = Vec::new(); + for (node_id, visible) in network.nodes.iter().map(|(node_id, node)| (*node_id, node.visible)).collect::>() { + let node_id_path = [network_path, &[node_id]].concat(); + + let primary_input_connector = InputConnector::node(node_id, 0); + + let primary_input = if self.input_from_connector(&primary_input_connector, network_path).is_some_and(|input| input.is_exposed()) { + self.frontend_input_from_connector(&primary_input_connector, network_path) + } else { + None + }; + let exposed_inputs = (1..self.number_of_inputs(&node_id, network_path)) + .filter_map(|input_index| self.frontend_input_from_connector(&InputConnector::node(node_id, input_index), network_path)) + .collect(); + + let primary_output = self.frontend_output_from_connector(&OutputConnector::node(node_id, 0), network_path); + + let exposed_outputs = (1..self.number_of_outputs(&node_id, network_path)) + .filter_map(|output_index| self.frontend_output_from_connector(&OutputConnector::node(node_id, output_index), network_path)) + .collect(); + let (primary_output_connected_to_layer, primary_input_connected_to_layer) = if self.is_layer(&node_id, network_path) { + ( + self.primary_output_connected_to_layer(&node_id, network_path), + self.primary_input_connected_to_layer(&node_id, network_path), + ) + } else { + (false, false) + }; + + let Some(position) = self.position(&node_id, network_path) else { + log::error!("Could not get position for node: {node_id}"); + continue; + }; + let previewed = self.previewed_node(network_path) == Some(node_id); + + let locked = self.is_locked(&node_id, network_path); + + let errors = node_graph_errors + .iter() + .find(|error| error.node_path == node_id_path) + .map(|error| format!("{:?}", error.error.clone())) + .or_else(|| { + if node_graph_errors.iter().any(|error| error.node_path.starts_with(&node_id_path)) { + Some("Node graph type error within this node".to_string()) + } else { + None + } + }); + + nodes.push(FrontendNode { + id: node_id, + is_layer: self.node_metadata(&node_id, network_path).is_some_and(|node_metadata| node_metadata.persistent_metadata.is_layer()), + can_be_layer: self.is_eligible_to_be_layer(&node_id, network_path), + reference: self.reference(&node_id, network_path).cloned().unwrap_or_default(), + display_name: self.display_name(&node_id, network_path), + primary_input, + exposed_inputs, + primary_output, + exposed_outputs, + primary_output_connected_to_layer, + primary_input_connected_to_layer, + position, + previewed, + visible, + locked, + errors, + }); + } + + nodes + } + + /// Returns None if there is an error, it is a hidden primary export, or a hidden input + pub fn frontend_input_from_connector(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> Option { + // Return None if it is a hidden input or doesn't exist + if self.input_from_connector(input_connector, network_path).is_some_and(|input| !input.is_exposed()) { + return None; + } + let input_type = self.input_type(input_connector, network_path); + let data_type = FrontendGraphDataType::displayed_type(&input_type); + let resolved_type = input_type.resolved_type_name(); + + let connected_to = self + .upstream_output_connector(input_connector, network_path) + .map(|output_connector| match output_connector { + OutputConnector::Node { node_id, output_index } => { + let mut name = self.display_name(&node_id, network_path); + if cfg!(debug_assertions) { + name.push_str(&format!(" (id: {node_id})")); + } + format!("{name} output {output_index}") + } + OutputConnector::Import(import_index) => format!("Import index {import_index}"), + }) + .unwrap_or("nothing".to_string()); + + let (name, description) = match input_connector { + InputConnector::Node { node_id, input_index } => self.displayed_input_name_and_description(node_id, *input_index, network_path), + InputConnector::Export(export_index) => { + // Get export name from parent node metadata input, which must match the number of exports. + // Empty string means to use type, or "Export + index" if type is empty determined + let export_name = if network_path.is_empty() { + "Canvas".to_string() + } else { + self.encapsulating_node_metadata(network_path) + .and_then(|encapsulating_metadata| encapsulating_metadata.persistent_metadata.output_names.get(*export_index).cloned()) + .unwrap_or_default() + }; + + let export_name = if !export_name.is_empty() { + export_name + } else if let Some(export_type_name) = input_type.compiled_nested_type_name() { + export_type_name + } else { + format!("Export index {}", export_index) + }; + + (export_name, String::new()) + } + }; + + // TODO: Move in separate Tooltip overlay + // let valid_types = match self.valid_input_types(&input_connector, network_path) { + // Ok(input_types) => input_types.iter().map(|ty| ty.to_string()).collect(), + // Err(e) => { + // log::error!("Error getting valid types for input {input_connector:?}: {e}"); + // Vec::new() + // } + // }; + + Some(FrontendGraphInput { + data_type, + resolved_type, + name, + description, + connected_to, + }) + } + + /// Returns None if there is an error, it is the document network, a hidden primary output or import + pub fn frontend_output_from_connector(&mut self, output_connector: &OutputConnector, network_path: &[NodeId]) -> Option { + let output_type = self.output_type(output_connector, network_path); + + let (name, description) = match output_connector { + OutputConnector::Node { node_id, output_index } => { + // Do not display the primary output port for a node if it is a network node with a hidden primary export + if *output_index == 0 && self.hidden_primary_output(node_id, network_path) { + return None; + }; + // Get the output name from the interior network export name + let node_metadata = self.node_metadata(node_id, network_path)?; + let output_name = node_metadata.persistent_metadata.output_names.get(*output_index).cloned().unwrap_or_default(); + + let output_name = if !output_name.is_empty() { output_name } else { output_type.resolved_type_name() }; + (output_name, String::new()) + } + OutputConnector::Import(import_index) => { + // Get the import name from the encapsulating node input metadata + let Some((encapsulating_node_id, encapsulating_path)) = network_path.split_last() else { + // Return None if it is an import in the document network + return None; + }; + // Return None if the primary input is hidden and this is the primary import + if *import_index == 0 && self.hidden_primary_import(network_path) { + return None; + }; + let (import_name, description) = self.displayed_input_name_and_description(encapsulating_node_id, *import_index, encapsulating_path); + + let import_name = if !import_name.is_empty() { + import_name + } else if let Some(import_type_name) = output_type.compiled_nested_type_name() { + import_type_name + } else { + format!("Import index {}", *import_index) + }; + + (import_name, description) + } + }; + let data_type = FrontendGraphDataType::displayed_type(&output_type); + let resolved_type = output_type.resolved_type_name(); + let mut connected_to = self + .outward_wires(network_path) + .and_then(|outward_wires| outward_wires.get(output_connector)) + .cloned() + .unwrap_or_else(|| { + log::error!("Could not get {output_connector:?} in outward wires"); + Vec::new() + }) + .iter() + .map(|input| match input { + InputConnector::Node { node_id, input_index } => { + let mut name = self.display_name(node_id, network_path); + if cfg!(debug_assertions) { + name.push_str(&format!(" (id: {node_id})")); + } + format!("{name} input {input_index}") + } + InputConnector::Export(export_index) => format!("Export index {export_index}"), + }) + .collect::>(); + + if connected_to.is_empty() { + connected_to.push("nothing".to_string()); + } + + Some(FrontendGraphOutput { + data_type, + resolved_type, + name, + description, + connected_to, + }) + } + + pub fn chain_width(&self, node_id: &NodeId, network_path: &[NodeId]) -> u32 { + if self.number_of_displayed_inputs(node_id, network_path) > 1 { + let mut last_chain_node_distance = 0u32; + // Iterate upstream from the layer, and get the number of nodes distance to the last node with Position::Chain + for (index, node_id) in self + .upstream_flow_back_from_nodes(vec![*node_id], network_path, FlowType::HorizontalPrimaryOutputFlow) + .skip(1) + .enumerate() + .collect::>() + { + // Check if the node is positioned as a chain + if self.is_chain(&node_id, network_path) { + last_chain_node_distance = (index as u32) + 1; + } else { + return last_chain_node_distance * 7 + 1; + } + } + + last_chain_node_distance * 7 + 1 + } else { + // Layer with no inputs has no chain + 0 + } + } + + /// Checks if a layer should display a gap in its left border + pub fn layer_has_left_border_gap(&self, node_id: &NodeId, network_path: &[NodeId]) -> bool { + self.upstream_flow_back_from_nodes(vec![*node_id], network_path, FlowType::HorizontalFlow) + .skip(1) + .any(|node_id| !self.is_chain(&node_id, network_path)) + } + + /// Returns the node which should have a dashed border drawn around it + pub fn previewed_node(&self, network_path: &[NodeId]) -> Option { + self.upstream_output_connector(&InputConnector::Export(0), network_path) + .and_then(|output_connector| output_connector.node_id()) + .filter(|output_node| self.root_node(network_path).is_some_and(|root_node| root_node.node_id != *output_node)) + } + + /// If any downstream input are bottom layer inputs, then the thick cap should be displayed above the output port + fn primary_output_connected_to_layer(&mut self, node_id: &NodeId, network_path: &[NodeId]) -> bool { + let Some(outward_wires) = self.outward_wires(network_path) else { + log::error!("Could not get outward_wires in primary_output_connected_to_layer"); + return false; + }; + let Some(downstream_connectors) = outward_wires.get(&OutputConnector::node(*node_id, 0)) else { + log::error!("Could not get downstream_connectors in primary_output_connected_to_layer"); + return false; + }; + let downstream_nodes = downstream_connectors + .iter() + .filter_map(|connector| if connector.input_index() == 0 { connector.node_id() } else { None }) + .collect::>(); + downstream_nodes.iter().any(|node_id| self.is_layer(node_id, network_path)) + } + + /// If any upstream nodes are layers, then the thick cap should be displayed below the primary input port + fn primary_input_connected_to_layer(&mut self, node_id: &NodeId, network_path: &[NodeId]) -> bool { + self.input_from_connector(&InputConnector::node(*node_id, 0), network_path) + .and_then(|input| input.as_node()) + .is_some_and(|node_id| self.is_layer(&node_id, network_path)) + } + + pub fn frontend_imports(&mut self, network_path: &[NodeId]) -> Vec> { + match network_path.split_last() { + Some((node_id, encapsulatingnetwork_path)) => { + let Some(node) = self.document_node(node_id, encapsulatingnetwork_path) else { + log::error!("Could not get node {node_id} in network {encapsulatingnetwork_path:?}"); + return Vec::new(); + }; + let mut frontend_imports = (0..node.inputs.len()) + .map(|import_index| self.frontend_output_from_connector(&OutputConnector::Import(import_index), network_path)) + .collect::>(); + if frontend_imports.is_empty() { + frontend_imports.push(None); + } + frontend_imports + } + // In the document network display no imports + None => Vec::new(), + } + } + + pub fn frontend_exports(&mut self, network_path: &[NodeId]) -> Vec> { + let Some(network) = self.nested_network(network_path) else { return Vec::new() }; + let mut frontend_exports = ((0..network.exports.len()).map(|export_index| self.frontend_input_from_connector(&InputConnector::Export(export_index), network_path))).collect::>(); + if frontend_exports.is_empty() { + frontend_exports.push(None); + } + frontend_exports + } + + pub fn import_export_position(&mut self, network_path: &[NodeId]) -> Option<(IVec2, IVec2)> { + let Some(all_nodes_bounding_box) = self.all_nodes_bounding_box(network_path).cloned() else { + log::error!("Could not get all nodes bounding box in load_export_ports"); + return None; + }; + let Some(network) = self.nested_network(network_path) else { + log::error!("Could not get current network in load_export_ports"); + return None; + }; + + let Some(network_metadata) = self.network_metadata(network_path) else { + log::error!("Could not get nested network_metadata in load_export_ports"); + return None; + }; + let node_graph_to_viewport = network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport; + let target_viewport_top_left = DVec2::new(IMPORTS_TO_LEFT_EDGE_PIXEL_GAP as f64, IMPORTS_TO_TOP_EDGE_PIXEL_GAP as f64); + + let node_graph_pixel_offset_top_left = node_graph_to_viewport.inverse().transform_point2(target_viewport_top_left); + + // A 5x5 grid offset from the top left corner + let node_graph_grid_space_offset_top_left = node_graph_to_viewport.inverse().transform_point2(DVec2::ZERO) + DVec2::new(5. * GRID_SIZE as f64, 4. * GRID_SIZE as f64); + + // The inner bound of the import is the highest/furthest left of the two offsets + let top_left_inner_bound = DVec2::new( + node_graph_pixel_offset_top_left.x.min(node_graph_grid_space_offset_top_left.x), + node_graph_pixel_offset_top_left.y.min(node_graph_grid_space_offset_top_left.y), + ); + + let offset_from_top_left = if network + .exports + .first() + .is_some_and(|export| export.as_node().is_some_and(|export_node| self.is_layer(&export_node, network_path))) + { + DVec2::new(-4. * GRID_SIZE as f64, -2. * GRID_SIZE as f64) + } else { + DVec2::new(-4. * GRID_SIZE as f64, 0.) + }; + + let bounding_box_top_left = DVec2::new((all_nodes_bounding_box[0].x / 24. + 0.5).floor() * 24., (all_nodes_bounding_box[0].y / 24. + 0.5).floor() * 24.) + offset_from_top_left; + let import_top_left = DVec2::new(top_left_inner_bound.x.min(bounding_box_top_left.x), top_left_inner_bound.y.min(bounding_box_top_left.y)); + let rounded_import_top_left = DVec2::new((import_top_left.x / 24.).round() * 24., (import_top_left.y / 24.).round() * 24.); + + let viewport_top_right = network_metadata.persistent_metadata.navigation_metadata.node_graph_top_right; + let target_viewport_top_right = DVec2::new( + viewport_top_right.x - EXPORTS_TO_RIGHT_EDGE_PIXEL_GAP as f64, + viewport_top_right.y + EXPORTS_TO_TOP_EDGE_PIXEL_GAP as f64, + ); + + // An offset from the right edge in viewport pixels + let node_graph_pixel_offset_top_right = node_graph_to_viewport.inverse().transform_point2(target_viewport_top_right); + + // A 5x5 grid offset from the right corner + let node_graph_grid_space_offset_top_right = node_graph_to_viewport.inverse().transform_point2(viewport_top_right) + DVec2::new(-5. * GRID_SIZE as f64, 4. * GRID_SIZE as f64); + + // The inner bound of the export is the highest/furthest right of the two offsets + let top_right_inner_bound = DVec2::new( + node_graph_pixel_offset_top_right.x.max(node_graph_grid_space_offset_top_right.x), + node_graph_pixel_offset_top_right.y.min(node_graph_grid_space_offset_top_right.y), + ); + + let offset_from_top_right = if network + .exports + .first() + .is_some_and(|export| export.as_node().is_some_and(|export_node| self.is_layer(&export_node, network_path))) + { + DVec2::new(2. * GRID_SIZE as f64, -2. * GRID_SIZE as f64) + } else { + DVec2::new(4. * GRID_SIZE as f64, 0.) + }; + + let mut bounding_box_top_right = DVec2::new((all_nodes_bounding_box[1].x / 24. + 0.5).floor() * 24., (all_nodes_bounding_box[0].y / 24. + 0.5).floor() * 24.); + bounding_box_top_right += offset_from_top_right; + let export_top_right = DVec2::new(top_right_inner_bound.x.max(bounding_box_top_right.x), top_right_inner_bound.y.min(bounding_box_top_right.y)); + let rounded_export_top_right = DVec2::new((export_top_right.x / 24.).round() * 24., (export_top_right.y / 24.).round() * 24.); + + Some((rounded_import_top_left.as_ivec2(), rounded_export_top_right.as_ivec2())) + } +} From 1996dd205ad1b9f5877ac3ef2f5b87d491157dd1 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 31 Aug 2025 17:42:52 -0700 Subject: [PATCH 06/24] Consolidate frontend node --- editor/src/dispatcher.rs | 1 + .../src/messages/frontend/frontend_message.rs | 26 ++-- .../document/document_message_handler.rs | 2 +- .../document/node_graph/node_graph_message.rs | 2 - .../node_graph/node_graph_message_handler.rs | 49 ++++--- .../document/node_graph/utility_types.rs | 14 +- .../utility_types/network_interface.rs | 12 +- .../network_interface/node_graph.rs | 36 +++--- frontend/src/components/views/Graph.svelte | 120 +++++++----------- frontend/src/messages.ts | 34 ++--- frontend/src/state-providers/node-graph.ts | 31 ++--- 11 files changed, 136 insertions(+), 191 deletions(-) diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 7e73a54fcf..3942b3d3ab 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -41,6 +41,7 @@ impl DispatcherMessageHandlers { /// The last occurrence of the message in the message queue is sufficient to ensure correct behavior. /// In addition, these messages do not change any state in the backend (aside from caches). const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[ + MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::NodeGraph(NodeGraphMessageDiscriminant::SendGraph))), MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::PropertiesPanel( PropertiesPanelMessageDiscriminant::Refresh, ))), diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index bbba55c57a..b8969308c7 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -2,13 +2,12 @@ use super::utility_types::{DocumentDetails, MouseCursorIcon, OpenDocument}; use crate::messages::app_window::app_window_message_handler::AppWindowPlatform; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::node_graph::utility_types::{ - BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendNodeType, Transform, + BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendNodeType, FrontendXY, Transform, }; use crate::messages::portfolio::document::utility_types::nodes::{JsRawBuffer, LayerPanelEntry, RawBuffer}; use crate::messages::portfolio::document::utility_types::wires::{WirePath, WirePathUpdate}; use crate::messages::prelude::*; use crate::messages::tool::utility_types::HintData; -use glam::IVec2; use graph_craft::document::NodeId; use graphene_std::raster::Image; use graphene_std::raster::color::Color; @@ -133,18 +132,14 @@ pub enum FrontendMessage { exports: Vec>, /// The primary import location. #[serde(rename = "importPosition")] - import_position: IVec2, + import_position: FrontendXY, /// The primary export location. #[serde(rename = "exportPosition")] - export_position: IVec2, + export_position: FrontendXY, /// The document network does not have an add import or export button. #[serde(rename = "addImportExport")] add_import_export: bool, }, - UpdateInSelectedNetwork { - #[serde(rename = "inSelectedNetwork")] - in_selected_network: bool, - }, UpdateBox { #[serde(rename = "box")] box_selection: Option, @@ -185,10 +180,6 @@ pub enum FrontendMessage { UpdateLayerWidths { #[serde(rename = "layerWidths")] layer_widths: HashMap, - #[serde(rename = "chainWidths")] - chain_widths: HashMap, - #[serde(rename = "hasLeftInputWire")] - has_left_input_wire: HashMap, }, UpdateDialogButtons { #[serde(rename = "layoutTarget")] @@ -284,7 +275,13 @@ pub enum FrontendMessage { cursor: MouseCursorIcon, }, UpdateNodeGraphNodes { - nodes: Vec, + #[serde(rename = "nodesToRender")] + nodes_to_render: Vec, + #[serde(rename = "inSelectedNetwork")] + in_selected_network: bool, + // Displays a dashed border around the node + #[serde(rename = "previewedNode")] + previewed_node: Option, }, UpdateVisibleNodes { nodes: Vec, @@ -298,9 +295,6 @@ pub enum FrontendMessage { layout_target: LayoutTarget, diff: Vec, }, - UpdateNodeGraphSelection { - selected: Vec, - }, UpdateNodeGraphTransform { transform: Transform, }, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index d7f2257b68..88aeacacf1 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1191,7 +1191,7 @@ impl MessageHandler> for DocumentMes } responses.add(PropertiesPanelMessage::Refresh); responses.add(NodeGraphMessage::UpdateLayerPanel); - responses.add(NodeGraphMessage::UpdateInSelectedNetwork); + responses.add(NodeGraphMessage::SendGraph); } DocumentMessage::SetBlendModeForSelectedLayers { blend_mode } => { for layer in self.network_interface.selected_nodes().selected_layers_except_artboards(&self.network_interface) { diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs index 1432046a25..37ac29df44 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs @@ -231,7 +231,5 @@ pub enum NodeGraphMessage { }, UpdateActionButtons, UpdateGraphBarRight, - UpdateInSelectedNetwork, UpdateHints, - SendSelectedNodes, } diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index 688f455f5b..7af7cd2ee5 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -6,7 +6,7 @@ use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::document_message_handler::navigation_controls; use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext; use crate::messages::portfolio::document::node_graph::document_node_definitions::NodePropertiesContext; -use crate::messages::portfolio::document::node_graph::utility_types::{ContextMenuData, Direction, FrontendGraphDataType}; +use crate::messages::portfolio::document::node_graph::utility_types::{ContextMenuData, Direction, FrontendGraphDataType, FrontendXY}; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::misc::GroupFolderType; use crate::messages::portfolio::document::utility_types::network_interface::{ @@ -192,7 +192,7 @@ impl<'a> MessageHandler> for NodeG responses.add(MenuBarMessage::SendLayout); responses.add(NodeGraphMessage::UpdateLayerPanel); responses.add(PropertiesPanelMessage::Refresh); - responses.add(NodeGraphMessage::SendSelectedNodes); + responses.add(NodeGraphMessage::SendGraph); responses.add(ArtboardToolMessage::UpdateSelectedArtboard); responses.add(DocumentMessage::DocumentStructureChanged); responses.add(OverlaysMessage::Draw); @@ -1633,21 +1633,22 @@ impl<'a> MessageHandler> for NodeG responses.add(NodeGraphMessage::UpdateLayerPanel); responses.add(DocumentMessage::DocumentStructureChanged); responses.add(PropertiesPanelMessage::Refresh); - if breadcrumb_network_path == selection_network_path && graph_view_overlay_open { - let nodes = network_interface.collect_nodes(&self.node_graph_errors, breadcrumb_network_path); - self.frontend_nodes = nodes.iter().map(|node| node.id).collect(); - responses.add(FrontendMessage::UpdateNodeGraphNodes { nodes }); + responses.add(NodeGraphMessage::UpdateActionButtons); + if graph_view_overlay_open { + let nodes_to_render = network_interface.collect_nodes(&self.node_graph_errors, breadcrumb_network_path); + self.frontend_nodes = nodes_to_render.iter().map(|node| node.id).collect(); + let previewed_node = network_interface.previewed_node(breadcrumb_network_path); + responses.add(FrontendMessage::UpdateNodeGraphNodes { + nodes_to_render, + in_selected_network: selection_network_path == breadcrumb_network_path, + previewed_node, + }); responses.add(NodeGraphMessage::UpdateVisibleNodes); - let (layer_widths, chain_widths, has_left_input_wire) = network_interface.collect_layer_widths(breadcrumb_network_path); + let layer_widths = network_interface.collect_layer_widths(breadcrumb_network_path); responses.add(NodeGraphMessage::UpdateImportsExports); - responses.add(FrontendMessage::UpdateLayerWidths { - layer_widths, - chain_widths, - has_left_input_wire, - }); - responses.add(NodeGraphMessage::SendSelectedNodes); + responses.add(FrontendMessage::UpdateLayerWidths { layer_widths }); responses.add(NodeGraphMessage::SendWires); self.update_node_graph_hints(responses); } @@ -1956,6 +1957,15 @@ impl<'a> MessageHandler> for NodeG return; }; + let import_position = FrontendXY { + x: import_position.x, + y: import_position.y, + }; + let export_position = FrontendXY { + x: export_position.x, + y: export_position.y, + }; + // Do not show the add import or add export button in the document network; let add_import_export = !breadcrumb_network_path.is_empty(); @@ -2000,22 +2010,9 @@ impl<'a> MessageHandler> for NodeG self.update_graph_bar_right(graph_fade_artwork_percentage, network_interface, breadcrumb_network_path, navigation_handler); self.send_node_bar_layout(responses); } - NodeGraphMessage::UpdateInSelectedNetwork => responses.add(FrontendMessage::UpdateInSelectedNetwork { - in_selected_network: selection_network_path == breadcrumb_network_path, - }), NodeGraphMessage::UpdateHints => { self.update_node_graph_hints(responses); } - NodeGraphMessage::SendSelectedNodes => { - let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(breadcrumb_network_path) else { - log::error!("Could not get selected nodes in NodeGraphMessage::SendSelectedNodes"); - return; - }; - responses.add(NodeGraphMessage::UpdateActionButtons); - responses.add(FrontendMessage::UpdateNodeGraphSelection { - selected: selected_nodes.selected_nodes().cloned().collect::>(), - }); - } } let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else { log::error!("Could not get selected nodes in NodeGraphMessageHandler"); 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 07741ee003..45dd0532f2 100644 --- a/editor/src/messages/portfolio/document/node_graph/utility_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/utility_types.rs @@ -1,4 +1,3 @@ -use glam::IVec2; use graph_craft::document::NodeId; use graph_craft::document::value::TaggedValue; use graphene_std::Type; @@ -51,6 +50,12 @@ impl FrontendGraphDataType { } } +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +pub struct FrontendXY { + pub x: i32, + pub y: i32, +} + #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] pub struct FrontendGraphInput { #[serde(rename = "dataType")] @@ -85,6 +90,7 @@ pub struct FrontendNode { pub is_layer: bool, #[serde(rename = "canBeLayer")] pub can_be_layer: bool, + pub selected: bool, pub reference: Option, #[serde(rename = "displayName")] pub display_name: String, @@ -96,11 +102,15 @@ pub struct FrontendNode { pub primary_output: Option, #[serde(rename = "exposedOutputs")] pub exposed_outputs: Vec, + #[serde(rename = "chainWidth")] + pub chain_width: u32, + #[serde(rename = "layerHasLeftBorderGap")] + pub layer_has_left_border_gap: bool, #[serde(rename = "primaryOutputConnectedToLayer")] pub primary_output_connected_to_layer: bool, #[serde(rename = "primaryInputConnectedToLayer")] pub primary_input_connected_to_layer: bool, - pub position: IVec2, + pub position: FrontendXY, pub visible: bool, pub locked: bool, pub previewed: bool, diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index 97fcde3ac0..fecba7ed14 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -2853,10 +2853,11 @@ impl NodeNetworkInterface { bounding_box_subpath.bounding_box_with_transform(network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport) } - pub fn collect_layer_widths(&mut self, network_path: &[NodeId]) -> (HashMap, HashMap, HashMap) { + // TODO: Remove and get layer click targets from render output + pub fn collect_layer_widths(&mut self, network_path: &[NodeId]) -> HashMap { let Some(network_metadata) = self.network_metadata(network_path) else { log::error!("Could not get nested network_metadata in collect_layer_widths"); - return (HashMap::new(), HashMap::new(), HashMap::new()); + return HashMap::new(); }; let nodes = network_metadata .persistent_metadata @@ -2868,13 +2869,8 @@ impl NodeNetworkInterface { .iter() .filter_map(|node_id| self.layer_width(node_id, network_path).map(|layer_width| (*node_id, layer_width))) .collect::>(); - let chain_widths = nodes.iter().map(|node_id| (*node_id, self.chain_width(node_id, network_path))).collect::>(); - let has_left_input_wire = nodes - .iter() - .map(|node_id| (*node_id, self.layer_has_left_border_gap(node_id, network_path))) - .collect::>(); - (layer_widths, chain_widths, has_left_input_wire) + layer_widths } pub fn compute_modified_vector(&self, layer: LayerNodeIdentifier) -> Option { diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/node_graph.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/node_graph.rs index b1d94d9ab8..055003117e 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface/node_graph.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/node_graph.rs @@ -5,7 +5,7 @@ use graphene_std::uuid::NodeId; use crate::{ consts::{EXPORTS_TO_RIGHT_EDGE_PIXEL_GAP, EXPORTS_TO_TOP_EDGE_PIXEL_GAP, GRID_SIZE, IMPORTS_TO_LEFT_EDGE_PIXEL_GAP, IMPORTS_TO_TOP_EDGE_PIXEL_GAP}, messages::portfolio::document::{ - node_graph::utility_types::{FrontendGraphDataType, FrontendGraphInput, FrontendGraphOutput, FrontendNode}, + node_graph::utility_types::{FrontendGraphDataType, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendXY}, utility_types::network_interface::{FlowType, InputConnector, NodeNetworkInterface, OutputConnector}, }, }; @@ -17,6 +17,7 @@ impl NodeNetworkInterface { log::error!("Could not get nested network when collecting nodes"); return Vec::new(); }; + let selected_nodes = self.selected_nodes_in_nested_network(network_path).unwrap_or_default(); let mut nodes = Vec::new(); for (node_id, visible) in network.nodes.iter().map(|(node_id, node)| (*node_id, node.visible)).collect::>() { let node_id_path = [network_path, &[node_id]].concat(); @@ -37,19 +38,12 @@ impl NodeNetworkInterface { let exposed_outputs = (1..self.number_of_outputs(&node_id, network_path)) .filter_map(|output_index| self.frontend_output_from_connector(&OutputConnector::node(node_id, output_index), network_path)) .collect(); - let (primary_output_connected_to_layer, primary_input_connected_to_layer) = if self.is_layer(&node_id, network_path) { - ( - self.primary_output_connected_to_layer(&node_id, network_path), - self.primary_input_connected_to_layer(&node_id, network_path), - ) - } else { - (false, false) - }; let Some(position) = self.position(&node_id, network_path) else { log::error!("Could not get position for node: {node_id}"); continue; }; + let position = FrontendXY { x: position.x, y: position.y }; let previewed = self.previewed_node(network_path) == Some(node_id); let locked = self.is_locked(&node_id, network_path); @@ -70,19 +64,24 @@ impl NodeNetworkInterface { id: node_id, is_layer: self.node_metadata(&node_id, network_path).is_some_and(|node_metadata| node_metadata.persistent_metadata.is_layer()), can_be_layer: self.is_eligible_to_be_layer(&node_id, network_path), + selected: selected_nodes.0.contains(&node_id), reference: self.reference(&node_id, network_path).cloned().unwrap_or_default(), display_name: self.display_name(&node_id, network_path), + previewed, + visible, + errors, + primary_input, exposed_inputs, primary_output, exposed_outputs, - primary_output_connected_to_layer, - primary_input_connected_to_layer, position, - previewed, - visible, + locked, - errors, + chain_width: self.chain_width(&node_id, network_path), + layer_has_left_border_gap: self.layer_has_left_border_gap(&node_id, network_path), + primary_input_connected_to_layer: self.primary_output_connected_to_layer(&node_id, network_path), + primary_output_connected_to_layer: self.primary_input_connected_to_layer(&node_id, network_path), }); } @@ -259,9 +258,12 @@ impl NodeNetworkInterface { /// Checks if a layer should display a gap in its left border pub fn layer_has_left_border_gap(&self, node_id: &NodeId, network_path: &[NodeId]) -> bool { - self.upstream_flow_back_from_nodes(vec![*node_id], network_path, FlowType::HorizontalFlow) - .skip(1) - .any(|node_id| !self.is_chain(&node_id, network_path)) + self.upstream_flow_back_from_nodes(vec![*node_id], network_path, FlowType::HorizontalFlow).skip(1).any(|node_id| { + !self.is_chain(&node_id, network_path) + || self + .upstream_output_connector(&InputConnector::node(node_id, 0), network_path) + .is_some_and(|output_connector| matches!(output_connector, OutputConnector::Import(_))) + }) } /// Returns the node which should have a dashed border drawn around it diff --git a/frontend/src/components/views/Graph.svelte b/frontend/src/components/views/Graph.svelte index d6cef02fec..210ed69517 100644 --- a/frontend/src/components/views/Graph.svelte +++ b/frontend/src/components/views/Graph.svelte @@ -116,12 +116,11 @@ } function toggleLayerDisplay(displayAsLayer: boolean, toggleId: bigint) { - let node = $nodeGraph.nodes.get(toggleId); - if (node) editor.handle.setToNodeOrLayer(node.id, displayAsLayer); + editor.handle.setToNodeOrLayer(toggleId, displayAsLayer); } function canBeToggledBetweenNodeAndLayer(toggleDisplayAsLayerNodeId: bigint) { - return $nodeGraph.nodes.get(toggleDisplayAsLayerNodeId)?.canBeLayer || false; + return $nodeGraph.nodesToRender.get(toggleDisplayAsLayerNodeId)?.canBeLayer || false; } function createNode(nodeType: string) { @@ -148,7 +147,7 @@ return borderMask(boxes, nodeWidth, nodeHeight); } - function layerBorderMask(nodeWidthFromThumbnail: number, nodeChainAreaLeftExtension: number, hasLeftInputWire: boolean): string { + function layerBorderMask(nodeWidthFromThumbnail: number, nodeChainAreaLeftExtension: number, layerHasLeftBorderGap: boolean): string { const NODE_HEIGHT = 2 * 24; const THUMBNAIL_WIDTH = 72 + 8 * 2; const FUDGE_HEIGHT_BEYOND_LAYER_HEIGHT = 2; @@ -158,7 +157,7 @@ const boxes: { x: number; y: number; width: number; height: number }[] = []; // Left input - if (hasLeftInputWire && nodeChainAreaLeftExtension > 0) { + if (layerHasLeftBorderGap && nodeChainAreaLeftExtension > 0) { boxes.push({ x: -8, y: 16, width: 16, height: 16 }); } @@ -318,8 +317,6 @@ xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8" class="connector" - data-connector="output" - data-datatype={frontendOutput.dataType} style:--data-color={`var(--color-data-${frontendOutput.dataType.toLowerCase()})`} style:--data-color-dim={`var(--color-data-${frontendOutput.dataType.toLowerCase()}-dim)`} style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 8) / 24} @@ -390,8 +387,6 @@ xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8" class="connector" - data-connector="input" - data-datatype={frontendInput.dataType} style:--data-color={`var(--color-data-${frontendInput.dataType.toLowerCase()})`} style:--data-color-dim={`var(--color-data-${frontendInput.dataType.toLowerCase()}-dim)`} style:--offset-left={($nodeGraph.updateImportsExports.exportPosition.x - 8) / 24} @@ -492,54 +487,47 @@
- {#each Array.from($nodeGraph.nodes) - .filter(([nodeId, node]) => node.isLayer && $nodeGraph.visibleNodes.has(nodeId)) - .map(([_, node], nodeIndex) => ({ node, nodeIndex })) as { node, nodeIndex } (nodeIndex)} + {#each Array.from($nodeGraph.nodesToRender).filter(([nodeId, node]) => node.isLayer && $nodeGraph.visibleNodes.has(nodeId)) as [nodeId, layer]} {@const clipPathId = String(Math.random()).substring(2)} - {@const stackDataInput = node.exposedInputs[0]} - {@const layerAreaWidth = $nodeGraph.layerWidths.get(node.id) || 8} - {@const layerChainWidth = $nodeGraph.chainWidths.get(node.id) || 0} - {@const hasLeftInputWire = $nodeGraph.hasLeftInputWire.get(node.id) || false} - {@const description = (node.reference && $nodeGraph.nodeDescriptions.get(node.reference)) || undefined} + {@const layerAreaWidth = $nodeGraph.layerWidths.get(layer.id) || 8} + {@const layerChainWidth = layer.chainWidth !== 0 ? layer.chainWidth + 0.5 : 0} + {@const description = (layer.reference && $nodeGraph.nodeDescriptions.get(layer.reference)) || undefined}
- {#if node.errors} - {node.errors} - {node.errors} + {#if layer.errors} + {layer.errors} + {layer.errors} {/if}
- {#if $nodeGraph.thumbnails.has(node.id)} - {@html $nodeGraph.thumbnails.get(node.id)} + {#if $nodeGraph.thumbnails.has(nodeId)} + {@html $nodeGraph.thumbnails.get(nodeId)} {/if} - {#if node.primaryOutput} + {#if layer.primaryOutput} - {outputTooltip(node.primaryOutput)} - {#if node.primaryOutput.connectedTo.length > 0} + {outputTooltip(layer.primaryOutput)} + {#if layer.primaryOutput.connectedTo.length > 0} - {#if node.primaryOutputConnectedToLayer} + {#if layer.primaryOutputConnectedToLayer} {/if} {:else} @@ -552,17 +540,15 @@ xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 12" class="connector bottom" - data-connector="input" - data-datatype={node.primaryInput?.dataType} - style:--data-color={`var(--color-data-${(node.primaryInput?.dataType || "General").toLowerCase()})`} - style:--data-color-dim={`var(--color-data-${(node.primaryInput?.dataType || "General").toLowerCase()}-dim)`} + style:--data-color={`var(--color-data-${(layer.primaryInput?.dataType || "General").toLowerCase()})`} + style:--data-color-dim={`var(--color-data-${(layer.primaryInput?.dataType || "General").toLowerCase()}-dim)`} > - {#if node.primaryInput} - {inputTooltip(node.primaryInput)} + {#if layer.primaryInput} + {inputTooltip(layer.primaryInput)} {/if} - {#if node.primaryInput?.connectedTo !== "nothing"} + {#if layer.primaryInput?.connectedTo !== "nothing"} - {#if node.primaryInputConnectedToLayer} + {#if layer.primaryInputConnectedToLayer} {/if} {:else} @@ -571,19 +557,17 @@
- {#if node.exposedInputs.length > 0} + {#if layer.exposedInputs.length > 0}
- {inputTooltip(stackDataInput)} - {#if stackDataInput.connectedTo !== undefined} + {inputTooltip(layer.exposedInputs[0])} + {#if layer.exposedInputs[0].connectedTo !== undefined} {:else} @@ -593,25 +577,25 @@ {/if}
- {node.displayName} + {layer.displayName}
{ /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ }} - tooltip={node.visible ? "Visible" : "Hidden"} + tooltip={layer.visible ? "Visible" : "Hidden"} /> - + @@ -647,15 +631,13 @@
- {#each Array.from($nodeGraph.nodes) - .filter(([nodeId, node]) => !node.isLayer && $nodeGraph.visibleNodes.has(nodeId)) - .map(([_, node], nodeIndex) => ({ node, nodeIndex })) as { node, nodeIndex } (nodeIndex)} + {#each Array.from($nodeGraph.nodesToRender).filter(([nodeId, node]) => !node.isLayer && $nodeGraph.visibleNodes.has(nodeId)) as [nodeId, node]} {@const exposedInputsOutputs = zipWithUndefined(node.exposedInputs, node.exposedOutputs)} {@const clipPathId = String(Math.random()).substring(2)} {@const description = (node.reference && $nodeGraph.nodeDescriptions.get(node.reference)) || undefined}
{#if node.errors} {node.errors} @@ -695,8 +676,6 @@ xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8" class="connector primary-connector" - data-connector="input" - data-datatype={node.primaryInput?.dataType} style:--data-color={`var(--color-data-${node.primaryInput.dataType.toLowerCase()})`} style:--data-color-dim={`var(--color-data-${node.primaryInput.dataType.toLowerCase()}-dim)`} > @@ -714,8 +693,6 @@ xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8" class="connector" - data-connector="input" - data-datatype={secondary.dataType} style:--data-color={`var(--color-data-${secondary.dataType.toLowerCase()})`} style:--data-color-dim={`var(--color-data-${secondary.dataType.toLowerCase()}-dim)`} > @@ -736,8 +713,6 @@ xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8" class="connector primary-connector" - data-connector="output" - data-datatype={node.primaryOutput.dataType} style:--data-color={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()})`} style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()}-dim)`} > @@ -754,8 +729,6 @@ xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8" class="connector" - data-connector="output" - data-datatype={secondary.dataType} style:--data-color={`var(--color-data-${secondary.dataType.toLowerCase()})`} style:--data-color-dim={`var(--color-data-${secondary.dataType.toLowerCase()}-dim)`} > @@ -784,7 +757,6 @@
- {#if $nodeGraph.box}
obj.layerWidths); -const ChainWidths = Transform(({ obj }) => obj.chainWidths); -const HasLeftInputWire = Transform(({ obj }) => obj.hasLeftInputWire); export class UpdateLayerWidths extends JsMessage { @LayerWidths readonly layerWidths!: Map; - @ChainWidths - readonly chainWidths!: Map; - @HasLeftInputWire - readonly hasLeftInputWire!: Map; } export class UpdateNodeGraphNodes extends JsMessage { - @Type(() => FrontendNode) - readonly nodes!: FrontendNode[]; + readonly nodesToRender!: FrontendNode[]; + + readonly inSelectedNetwork!: boolean; + + readonly previewedNode!: bigint | undefined; } export class UpdateVisibleNodes extends JsMessage { @@ -123,11 +114,6 @@ export class UpdateNodeThumbnail extends JsMessage { readonly value!: string; } -export class UpdateNodeGraphSelection extends JsMessage { - @Type(() => BigInt) - readonly selected!: bigint[]; -} - export class UpdateOpenDocumentsList extends JsMessage { @Type(() => OpenDocument) readonly openDocuments!: OpenDocument[]; @@ -211,11 +197,13 @@ export class FrontendGraphOutput { } export class FrontendNode { + readonly id!: bigint; + readonly isLayer!: boolean; readonly canBeLayer!: boolean; - readonly id!: bigint; + readonly selected!: boolean; readonly reference!: string | undefined; @@ -229,6 +217,10 @@ export class FrontendNode { readonly exposedOutputs!: FrontendGraphOutput[]; + readonly chainWidth!: number; + + readonly layerHasLeftBorderGap!: boolean; + readonly primaryInputConnectedToLayer!: boolean; readonly primaryOutputConnectedToLayer!: boolean; @@ -1672,7 +1664,6 @@ export const messageMakers: Record = { UpdateImportReorderIndex, UpdateImportsExports, UpdateInputHints, - UpdateInSelectedNetwork, UpdateLayersPanelBottomBarLayout, UpdateLayersPanelControlBarLeftLayout, UpdateLayersPanelControlBarRightLayout, @@ -1682,7 +1673,6 @@ export const messageMakers: Record = { UpdateMouseCursor, UpdateNodeGraphControlBarLayout, UpdateNodeGraphNodes, - UpdateNodeGraphSelection, UpdateNodeGraphTransform, UpdateNodeGraphWires, UpdateNodeThumbnail, diff --git a/frontend/src/state-providers/node-graph.ts b/frontend/src/state-providers/node-graph.ts index fd6c7c5724..73e1d436a0 100644 --- a/frontend/src/state-providers/node-graph.ts +++ b/frontend/src/state-providers/node-graph.ts @@ -13,7 +13,6 @@ import { UpdateBox, UpdateClickTargets, UpdateContextMenuInformation, - UpdateInSelectedNetwork, UpdateImportReorderIndex, UpdateExportReorderIndex, UpdateImportsExports, @@ -21,7 +20,6 @@ import { UpdateNodeGraphNodes, UpdateVisibleNodes, UpdateNodeGraphWires, - UpdateNodeGraphSelection, UpdateNodeGraphTransform, UpdateNodeThumbnail, UpdateWirePathInProgress, @@ -33,10 +31,9 @@ export function createNodeGraphState(editor: Editor) { clickTargets: undefined as FrontendClickTargets | undefined, contextMenuInformation: undefined as ContextMenuInformation | undefined, layerWidths: new Map(), - chainWidths: new Map(), - hasLeftInputWire: new Map(), updateImportsExports: undefined as UpdateImportsExports | undefined, - nodes: new Map(), + nodesToRender: new Map(), + visibleNodes: new Set(), /// The index is the exposed input index. The exports have a first key value of u32::MAX. wires: new Map>(), @@ -44,9 +41,9 @@ export function createNodeGraphState(editor: Editor) { nodeDescriptions: new Map(), nodeTypes: [] as FrontendNodeType[], thumbnails: new Map(), - selected: [] as bigint[], transform: { scale: 1, x: 0, y: 0 }, inSelectedNetwork: true, + previewedNode: undefined as bigint | undefined, reorderImportIndex: undefined as number | undefined, reorderExportIndex: undefined as number | undefined, }); @@ -95,26 +92,20 @@ export function createNodeGraphState(editor: Editor) { return state; }); }); - editor.subscriptions.subscribeJsMessage(UpdateInSelectedNetwork, (updateInSelectedNetwork) => { - update((state) => { - state.inSelectedNetwork = updateInSelectedNetwork.inSelectedNetwork; - return state; - }); - }); editor.subscriptions.subscribeJsMessage(UpdateLayerWidths, (updateLayerWidths) => { update((state) => { state.layerWidths = updateLayerWidths.layerWidths; - state.chainWidths = updateLayerWidths.chainWidths; - state.hasLeftInputWire = updateLayerWidths.hasLeftInputWire; return state; }); }); editor.subscriptions.subscribeJsMessage(UpdateNodeGraphNodes, (updateNodeGraphNodes) => { update((state) => { - state.nodes.clear(); - updateNodeGraphNodes.nodes.forEach((node) => { - state.nodes.set(node.id, node); + state.nodesToRender.clear(); + updateNodeGraphNodes.nodesToRender.forEach((node) => { + state.nodesToRender.set(node.id, node); }); + state.inSelectedNetwork = updateNodeGraphNodes.inSelectedNetwork; + state.previewedNode = updateNodeGraphNodes.previewedNode; return state; }); }); @@ -148,12 +139,6 @@ export function createNodeGraphState(editor: Editor) { return state; }); }); - editor.subscriptions.subscribeJsMessage(UpdateNodeGraphSelection, (updateNodeGraphSelection) => { - update((state) => { - state.selected = updateNodeGraphSelection.selected; - return state; - }); - }); editor.subscriptions.subscribeJsMessage(UpdateNodeGraphTransform, (updateNodeGraphTransform) => { update((state) => { state.transform = updateNodeGraphTransform.transform; From 8a6aa46488052e84790f07c4b343091769756639 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 31 Aug 2025 13:54:15 -0700 Subject: [PATCH 07/24] Rewrite FrontendNode --- .../src/messages/frontend/frontend_message.rs | 4 +- .../node_graph/node_graph_message_handler.rs | 6 +- .../document/node_graph/utility_types.rs | 82 +++++++++++++------ frontend/src/components/views/Graph.svelte | 5 +- frontend/src/messages.ts | 68 ++++++++++----- frontend/src/state-providers/node-graph.ts | 6 +- 6 files changed, 118 insertions(+), 53 deletions(-) diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index b8969308c7..d77a115587 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -2,7 +2,7 @@ use super::utility_types::{DocumentDetails, MouseCursorIcon, OpenDocument}; use crate::messages::app_window::app_window_message_handler::AppWindowPlatform; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::node_graph::utility_types::{ - BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendNodeType, FrontendXY, Transform, + BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendNodeOrLayer, FrontendNodeType, FrontendXY, FrontendXY, Transform, }; use crate::messages::portfolio::document::utility_types::nodes::{JsRawBuffer, LayerPanelEntry, RawBuffer}; use crate::messages::portfolio::document::utility_types::wires::{WirePath, WirePathUpdate}; @@ -276,7 +276,7 @@ pub enum FrontendMessage { }, UpdateNodeGraphNodes { #[serde(rename = "nodesToRender")] - nodes_to_render: Vec, + nodes_to_render: Vec, #[serde(rename = "inSelectedNetwork")] in_selected_network: bool, // Displays a dashed border around the node diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index 7af7cd2ee5..8864d63f0d 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -6,7 +6,9 @@ use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::document_message_handler::navigation_controls; use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext; use crate::messages::portfolio::document::node_graph::document_node_definitions::NodePropertiesContext; -use crate::messages::portfolio::document::node_graph::utility_types::{ContextMenuData, Direction, FrontendGraphDataType, FrontendXY}; +use crate::messages::portfolio::document::node_graph::utility_types::{ + ContextMenuData, Direction, FrontendGraphDataType, FrontendLayer, FrontendNodeMetadata, FrontendNodeOrLayer, FrontendXY, FrontendXY, +}; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::misc::GroupFolderType; use crate::messages::portfolio::document::utility_types::network_interface::{ @@ -22,7 +24,7 @@ use crate::messages::tool::tool_messages::tool_prelude::{Key, MouseMotion}; use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo}; use glam::{DAffine2, DVec2, IVec2}; use graph_craft::document::{DocumentNodeImplementation, NodeId, NodeInput}; -use graph_craft::proto::GraphErrors; +use graph_craft::proto::{GraphErrors, NodeMetadata}; use graphene_std::math::math_ext::QuadExt; use graphene_std::vector::algorithms::bezpath_algorithms::bezpath_is_inside_bezpath; use graphene_std::*; 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 45dd0532f2..1adf67902e 100644 --- a/editor/src/messages/portfolio/document/node_graph/utility_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/utility_types.rs @@ -64,9 +64,12 @@ pub struct FrontendGraphInput { pub description: String, #[serde(rename = "resolvedType")] pub resolved_type: String, - #[serde(rename = "connectedTo")] /// Either "nothing", "import index {index}", or "{node name} output {output_index}". + #[serde(rename = "connectedToString")] pub connected_to: String, + /// Used to render the upstream node once this node is rendered + #[serde(rename = "connectedToNode")] + pub connected_to_node: Option, } #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] @@ -83,38 +86,71 @@ pub struct FrontendGraphOutput { pub connected_to: Vec, } +// Metadata that is common to nodes and layers #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] -pub struct FrontendNode { - pub id: graph_craft::document::NodeId, - #[serde(rename = "isLayer")] - pub is_layer: bool, +pub struct FrontendNodeMetadata { + // TODO: Remove and replace with popup manager system #[serde(rename = "canBeLayer")] pub can_be_layer: bool, - pub selected: bool, - pub reference: Option, #[serde(rename = "displayName")] pub display_name: String, - #[serde(rename = "primaryInput")] - pub primary_input: Option, - #[serde(rename = "exposedInputs")] - pub exposed_inputs: Vec, - #[serde(rename = "primaryOutput")] - pub primary_output: Option, - #[serde(rename = "exposedOutputs")] - pub exposed_outputs: Vec, + pub selected: bool, + // Used to get the description, which is stored in a global hashmap + pub reference: Option, + // Reduces opacity of node/hidden eye icon + pub visible: bool, + // The svg string for each input + // pub wires: Vec>, + pub errors: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +pub struct FrontendNode { + // pub position: FrontendNodePosition, + pub position: FrontendXY, + pub inputs: Vec>, + pub outputs: Vec>, +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +pub struct FrontendLayer { + #[serde(rename = "bottomInput")] + pub bottom_input: FrontendGraphInput, + #[serde(rename = "sideInput")] + pub side_input: Option, + pub output: FrontendGraphOutput, + // pub position: FrontendLayerPosition, + pub position: FrontendXY, + pub locked: bool, #[serde(rename = "chainWidth")] pub chain_width: u32, #[serde(rename = "layerHasLeftBorderGap")] - pub layer_has_left_border_gap: bool, - #[serde(rename = "primaryOutputConnectedToLayer")] - pub primary_output_connected_to_layer: bool, + layer_has_left_border_gap: bool, #[serde(rename = "primaryInputConnectedToLayer")] pub primary_input_connected_to_layer: bool, - pub position: FrontendXY, - pub visible: bool, - pub locked: bool, - pub previewed: bool, - pub errors: Option, + #[serde(rename = "primaryOutputConnectedToLayer")] + pub primary_output_connected_to_layer: bool, +} + +// // Should be an enum but those are hard to serialize/deserialize to TS +// #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +// pub struct FrontendNodePosition { +// pub absolute: Option, +// pub chain: Option, +// } + +// // Should be an enum but those are hard to serialize/deserialize to TS +// #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +// pub struct FrontendLayerPosition { +// pub absolute: Option, +// pub stack: Option, +// } + +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +pub struct FrontendNodeOrLayer { + pub metadata: FrontendNodeMetadata, + pub node: Option, + pub layer: Option, } #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] diff --git a/frontend/src/components/views/Graph.svelte b/frontend/src/components/views/Graph.svelte index 210ed69517..0c48a672a2 100644 --- a/frontend/src/components/views/Graph.svelte +++ b/frontend/src/components/views/Graph.svelte @@ -117,10 +117,11 @@ function toggleLayerDisplay(displayAsLayer: boolean, toggleId: bigint) { editor.handle.setToNodeOrLayer(toggleId, displayAsLayer); + editor.handle.setToNodeOrLayer(toggleId, displayAsLayer); } function canBeToggledBetweenNodeAndLayer(toggleDisplayAsLayerNodeId: bigint) { - return $nodeGraph.nodesToRender.get(toggleDisplayAsLayerNodeId)?.canBeLayer || false; + return $nodeGraph.nodesToRender.get(toggleDisplayAsLayerNodeId)?.metadata.canBeLayer || false; } function createNode(nodeType: string) { @@ -199,7 +200,7 @@ } function inputConnectedToText(input: FrontendGraphInput): string { - return `Connected to:\n${input.connectedTo}`; + return `Connected to:\n${input.connectedToString}`; } function zipWithUndefined(arr1: FrontendGraphInput[], arr2: FrontendGraphOutput[]) { diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index df532396e0..7a76acb655 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -175,13 +175,15 @@ export type FrontendGraphDataType = "General" | "Number" | "Artboard" | "Graphic export class FrontendGraphInput { readonly dataType!: FrontendGraphDataType; + readonly resolvedType!: string; + readonly name!: string; readonly description!: string; - readonly resolvedType!: string; + readonly connectedToString!: string; - readonly connectedTo!: string; + readonly connectedToNode!: bigint | undefined; } export class FrontendGraphOutput { @@ -196,26 +198,42 @@ export class FrontendGraphOutput { readonly connectedTo!: string[]; } -export class FrontendNode { - readonly id!: bigint; - - readonly isLayer!: boolean; - +export class FrontendNodeMetadata { readonly canBeLayer!: boolean; + readonly displayName!: string; + readonly selected!: boolean; readonly reference!: string | undefined; - readonly displayName!: string; + readonly visible!: boolean; + + // readonly wires!: (string | undefined)[]; + + readonly errors!: string | undefined; +} + +export class FrontendNode { + // readonly position!: FrontendNodePosition; + readonly position!: XY; + + readonly inputs!: (FrontendGraphInput | undefined)[]; - readonly primaryInput!: FrontendGraphInput | undefined; + readonly outputs!: (FrontendGraphOutput | undefined)[]; +} + +export class FrontendLayer { + // readonly position!: FrontendLayerPosition; + readonly position!: XY; - readonly exposedInputs!: FrontendGraphInput[]; + readonly bottomInput!: FrontendGraphInput; - readonly primaryOutput!: FrontendGraphOutput | undefined; + readonly sideInput!: FrontendGraphInput | undefined; - readonly exposedOutputs!: FrontendGraphOutput[]; + readonly output!: FrontendGraphOutput; + + readonly locked!: boolean; readonly chainWidth!: number; @@ -224,19 +242,27 @@ export class FrontendNode { readonly primaryInputConnectedToLayer!: boolean; readonly primaryOutputConnectedToLayer!: boolean; +} - @TupleToVec2 - readonly position!: XY; - - // TODO: Store field for the width of the left node chain - - readonly previewed!: boolean; +export class FrontendNodePosition { + readonly absolute!: XY | undefined; + readonly chain!: boolean | undefined; +} - readonly visible!: boolean; +export class FrontendLayerPosition { + readonly absolute!: XY | undefined; + readonly stack!: number | undefined; +} - readonly unlocked!: boolean; +export class FrontendNodeOrLayer { + readonly metadata!: FrontendNodeMetadata; + readonly node!: FrontendNode | undefined; + readonly layer!: FrontendLayer | undefined; +} - readonly errors!: string | undefined; +export class UpdateCentralNodeGraph extends JsMessage { + readonly nodeOrLayer!: FrontendNodeOrLayer[]; + readonly inSelectedNetwork!: boolean; } export class FrontendNodeType { diff --git a/frontend/src/state-providers/node-graph.ts b/frontend/src/state-providers/node-graph.ts index 73e1d436a0..d2f4c693f2 100644 --- a/frontend/src/state-providers/node-graph.ts +++ b/frontend/src/state-providers/node-graph.ts @@ -1,11 +1,11 @@ import { writable } from "svelte/store"; import { type Editor } from "@graphite/editor"; +import type { FrontendNodeOrLayer } from "@graphite/messages"; import { type Box, type FrontendClickTargets, type ContextMenuInformation, - type FrontendNode, type FrontendNodeType, type WirePath, ClearAllNodeGraphWires, @@ -32,8 +32,7 @@ export function createNodeGraphState(editor: Editor) { contextMenuInformation: undefined as ContextMenuInformation | undefined, layerWidths: new Map(), updateImportsExports: undefined as UpdateImportsExports | undefined, - nodesToRender: new Map(), - + nodesToRender: new Map(), visibleNodes: new Set(), /// The index is the exposed input index. The exports have a first key value of u32::MAX. wires: new Map>(), @@ -92,6 +91,7 @@ export function createNodeGraphState(editor: Editor) { return state; }); }); + editor.subscriptions.subscribeJsMessage(UpdateLayerWidths, (updateLayerWidths) => { update((state) => { state.layerWidths = updateLayerWidths.layerWidths; From 8bab555357bd814e76184b76a8805640fceffaaa Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 31 Aug 2025 14:49:02 -0700 Subject: [PATCH 08/24] Improve collect_nodes --- .../src/messages/frontend/frontend_message.rs | 2 +- .../node_graph/node_graph_message_handler.rs | 8 +- .../document/node_graph/utility_types.rs | 12 +- .../network_interface/node_graph.rs | 113 +++++++++++------- frontend/src/messages.ts | 10 +- frontend/src/state-providers/node-graph.ts | 4 +- 6 files changed, 91 insertions(+), 58 deletions(-) diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index d77a115587..e6f84f5aa1 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -2,7 +2,7 @@ use super::utility_types::{DocumentDetails, MouseCursorIcon, OpenDocument}; use crate::messages::app_window::app_window_message_handler::AppWindowPlatform; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::node_graph::utility_types::{ - BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendNodeOrLayer, FrontendNodeType, FrontendXY, FrontendXY, Transform, + BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendGraphInput, FrontendGraphOutput, FrontendNodeToRender, FrontendNodeType, FrontendXY, Transform, }; use crate::messages::portfolio::document::utility_types::nodes::{JsRawBuffer, LayerPanelEntry, RawBuffer}; use crate::messages::portfolio::document::utility_types::wires::{WirePath, WirePathUpdate}; diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index 8864d63f0d..1aa6cdf6d6 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -6,9 +6,7 @@ use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::document_message_handler::navigation_controls; use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext; use crate::messages::portfolio::document::node_graph::document_node_definitions::NodePropertiesContext; -use crate::messages::portfolio::document::node_graph::utility_types::{ - ContextMenuData, Direction, FrontendGraphDataType, FrontendLayer, FrontendNodeMetadata, FrontendNodeOrLayer, FrontendXY, FrontendXY, -}; +use crate::messages::portfolio::document::node_graph::utility_types::{ContextMenuData, Direction, FrontendGraphDataType, FrontendXY}; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::misc::GroupFolderType; use crate::messages::portfolio::document::utility_types::network_interface::{ @@ -24,7 +22,7 @@ use crate::messages::tool::tool_messages::tool_prelude::{Key, MouseMotion}; use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo}; use glam::{DAffine2, DVec2, IVec2}; use graph_craft::document::{DocumentNodeImplementation, NodeId, NodeInput}; -use graph_craft::proto::{GraphErrors, NodeMetadata}; +use graph_craft::proto::GraphErrors; use graphene_std::math::math_ext::QuadExt; use graphene_std::vector::algorithms::bezpath_algorithms::bezpath_is_inside_bezpath; use graphene_std::*; @@ -1638,7 +1636,7 @@ impl<'a> MessageHandler> for NodeG responses.add(NodeGraphMessage::UpdateActionButtons); if graph_view_overlay_open { let nodes_to_render = network_interface.collect_nodes(&self.node_graph_errors, breadcrumb_network_path); - self.frontend_nodes = nodes_to_render.iter().map(|node| node.id).collect(); + self.frontend_nodes = nodes_to_render.iter().map(|node| node.metadata.node_id).collect(); let previewed_node = network_interface.previewed_node(breadcrumb_network_path); responses.add(FrontendMessage::UpdateNodeGraphNodes { nodes_to_render, 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 1adf67902e..14a9ea5107 100644 --- a/editor/src/messages/portfolio/document/node_graph/utility_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/utility_types.rs @@ -89,6 +89,8 @@ pub struct FrontendGraphOutput { // Metadata that is common to nodes and layers #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] pub struct FrontendNodeMetadata { + #[serde(rename = "node_id")] + pub node_id: NodeId, // TODO: Remove and replace with popup manager system #[serde(rename = "canBeLayer")] pub can_be_layer: bool, @@ -125,7 +127,7 @@ pub struct FrontendLayer { #[serde(rename = "chainWidth")] pub chain_width: u32, #[serde(rename = "layerHasLeftBorderGap")] - layer_has_left_border_gap: bool, + pub layer_has_left_border_gap: bool, #[serde(rename = "primaryInputConnectedToLayer")] pub primary_input_connected_to_layer: bool, #[serde(rename = "primaryOutputConnectedToLayer")] @@ -148,11 +150,17 @@ pub struct FrontendLayer { #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] pub struct FrontendNodeOrLayer { - pub metadata: FrontendNodeMetadata, pub node: Option, pub layer: Option, } +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +pub struct FrontendNodeToRender { + pub metadata: FrontendNodeMetadata, + #[serde(rename = "nodeOrLayer")] + pub node_or_layer: FrontendNodeOrLayer, +} + #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] pub struct FrontendNodeType { pub name: Cow<'static, str>, diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/node_graph.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/node_graph.rs index 055003117e..aae024f91c 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface/node_graph.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/node_graph.rs @@ -5,14 +5,16 @@ use graphene_std::uuid::NodeId; use crate::{ consts::{EXPORTS_TO_RIGHT_EDGE_PIXEL_GAP, EXPORTS_TO_TOP_EDGE_PIXEL_GAP, GRID_SIZE, IMPORTS_TO_LEFT_EDGE_PIXEL_GAP, IMPORTS_TO_TOP_EDGE_PIXEL_GAP}, messages::portfolio::document::{ - node_graph::utility_types::{FrontendGraphDataType, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendXY}, + node_graph::utility_types::{ + FrontendGraphDataType, FrontendGraphInput, FrontendGraphOutput, FrontendLayer, FrontendNode, FrontendNodeMetadata, FrontendNodeOrLayer, FrontendNodeToRender, FrontendXY, + }, utility_types::network_interface::{FlowType, InputConnector, NodeNetworkInterface, OutputConnector}, }, }; // Functions used to collect data from the network interface for use in rendering the node graph impl NodeNetworkInterface { - pub fn collect_nodes(&mut self, node_graph_errors: &GraphErrors, network_path: &[NodeId]) -> Vec { + pub fn collect_nodes(&mut self, node_graph_errors: &GraphErrors, network_path: &[NodeId]) -> Vec { let Some(network) = self.nested_network(network_path) else { log::error!("Could not get nested network when collecting nodes"); return Vec::new(); @@ -22,32 +24,6 @@ impl NodeNetworkInterface { for (node_id, visible) in network.nodes.iter().map(|(node_id, node)| (*node_id, node.visible)).collect::>() { let node_id_path = [network_path, &[node_id]].concat(); - let primary_input_connector = InputConnector::node(node_id, 0); - - let primary_input = if self.input_from_connector(&primary_input_connector, network_path).is_some_and(|input| input.is_exposed()) { - self.frontend_input_from_connector(&primary_input_connector, network_path) - } else { - None - }; - let exposed_inputs = (1..self.number_of_inputs(&node_id, network_path)) - .filter_map(|input_index| self.frontend_input_from_connector(&InputConnector::node(node_id, input_index), network_path)) - .collect(); - - let primary_output = self.frontend_output_from_connector(&OutputConnector::node(node_id, 0), network_path); - - let exposed_outputs = (1..self.number_of_outputs(&node_id, network_path)) - .filter_map(|output_index| self.frontend_output_from_connector(&OutputConnector::node(node_id, output_index), network_path)) - .collect(); - - let Some(position) = self.position(&node_id, network_path) else { - log::error!("Could not get position for node: {node_id}"); - continue; - }; - let position = FrontendXY { x: position.x, y: position.y }; - let previewed = self.previewed_node(network_path) == Some(node_id); - - let locked = self.is_locked(&node_id, network_path); - let errors = node_graph_errors .iter() .find(|error| error.node_path == node_id_path) @@ -60,31 +36,73 @@ impl NodeNetworkInterface { } }); - nodes.push(FrontendNode { - id: node_id, - is_layer: self.node_metadata(&node_id, network_path).is_some_and(|node_metadata| node_metadata.persistent_metadata.is_layer()), + let metadata = FrontendNodeMetadata { + node_id, can_be_layer: self.is_eligible_to_be_layer(&node_id, network_path), + display_name: self.display_name(&node_id, network_path), selected: selected_nodes.0.contains(&node_id), reference: self.reference(&node_id, network_path).cloned().unwrap_or_default(), - display_name: self.display_name(&node_id, network_path), - previewed, visible, errors, + }; - primary_input, - exposed_inputs, - primary_output, - exposed_outputs, - position, - - locked, - chain_width: self.chain_width(&node_id, network_path), - layer_has_left_border_gap: self.layer_has_left_border_gap(&node_id, network_path), - primary_input_connected_to_layer: self.primary_output_connected_to_layer(&node_id, network_path), - primary_output_connected_to_layer: self.primary_input_connected_to_layer(&node_id, network_path), - }); - } + let node_or_layer = match self.is_layer(&node_id, network_path) { + true => { + let Some(position) = self.position(&node_id, network_path) else { + log::error!("Could not get position for node: {node_id}"); + continue; + }; + let position = FrontendXY { x: position.x, y: position.y }; + + let Some(bottom_input) = self.frontend_input_from_connector(&InputConnector::node(node_id, 0), network_path) else { + log::error!("Layer must have a visible primary input"); + continue; + }; + let side_input = self.frontend_input_from_connector(&InputConnector::node(node_id, 1), network_path); + let Some(output) = self.frontend_output_from_connector(&OutputConnector::node(node_id, 0), network_path) else { + log::error!("Layer must have a visible primary output"); + continue; + }; + + let layer = Some(FrontendLayer { + bottom_input, + side_input, + output, + position, + locked: self.is_locked(&node_id, network_path), + chain_width: self.chain_width(&node_id, network_path), + layer_has_left_border_gap: self.layer_has_left_border_gap(&node_id, network_path), + primary_input_connected_to_layer: self.primary_output_connected_to_layer(&node_id, network_path), + primary_output_connected_to_layer: self.primary_input_connected_to_layer(&node_id, network_path), + }); + FrontendNodeOrLayer { node: None, layer } + } + false => { + let Some(position) = self.position(&node_id, network_path) else { + log::error!("Could not get position for node: {node_id}"); + continue; + }; + + let position = FrontendXY { x: position.x, y: position.y }; + + let inputs = (0..self.number_of_inputs(&node_id, network_path)) + .map(|input_index| self.frontend_input_from_connector(&InputConnector::node(node_id, input_index), network_path)) + .collect(); + let outputs = (0..self.number_of_outputs(&node_id, network_path)) + .map(|output_index| self.frontend_output_from_connector(&OutputConnector::node(node_id, output_index), network_path)) + .collect(); + + let node = Some(FrontendNode { position, inputs, outputs }); + + FrontendNodeOrLayer { node, layer: None } + } + }; + + let frontend_node_to_render = FrontendNodeToRender { metadata, node_or_layer }; + + nodes.push(frontend_node_to_render); + } nodes } @@ -146,12 +164,15 @@ impl NodeNetworkInterface { // } // }; + let connected_to_node = self.upstream_output_connector(input_connector, network_path).and_then(|output_connector| output_connector.node_id()); + Some(FrontendGraphInput { data_type, resolved_type, name, description, connected_to, + connected_to_node, }) } diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index 7a76acb655..66092a2d3f 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -78,7 +78,7 @@ export class UpdateLayerWidths extends JsMessage { } export class UpdateNodeGraphNodes extends JsMessage { - readonly nodesToRender!: FrontendNode[]; + readonly nodesToRender!: FrontendNodeToRender[]; readonly inSelectedNetwork!: boolean; @@ -199,6 +199,8 @@ export class FrontendGraphOutput { } export class FrontendNodeMetadata { + readonly nodeId!: bigint; + readonly canBeLayer!: boolean; readonly displayName!: string; @@ -255,11 +257,15 @@ export class FrontendLayerPosition { } export class FrontendNodeOrLayer { - readonly metadata!: FrontendNodeMetadata; readonly node!: FrontendNode | undefined; readonly layer!: FrontendLayer | undefined; } +export class FrontendNodeToRender { + readonly metadata!: FrontendNodeMetadata; + readonly nodeOrLayer!: FrontendNodeOrLayer; +} + export class UpdateCentralNodeGraph extends JsMessage { readonly nodeOrLayer!: FrontendNodeOrLayer[]; readonly inSelectedNetwork!: boolean; diff --git a/frontend/src/state-providers/node-graph.ts b/frontend/src/state-providers/node-graph.ts index d2f4c693f2..9e12d4820f 100644 --- a/frontend/src/state-providers/node-graph.ts +++ b/frontend/src/state-providers/node-graph.ts @@ -1,11 +1,11 @@ import { writable } from "svelte/store"; import { type Editor } from "@graphite/editor"; -import type { FrontendNodeOrLayer } from "@graphite/messages"; import { type Box, type FrontendClickTargets, type ContextMenuInformation, + type FrontendNodeToRender, type FrontendNodeType, type WirePath, ClearAllNodeGraphWires, @@ -102,7 +102,7 @@ export function createNodeGraphState(editor: Editor) { update((state) => { state.nodesToRender.clear(); updateNodeGraphNodes.nodesToRender.forEach((node) => { - state.nodesToRender.set(node.id, node); + state.nodesToRender.set(node.metadata.nodeId, node); }); state.inSelectedNetwork = updateNodeGraphNodes.inSelectedNetwork; state.previewedNode = updateNodeGraphNodes.previewedNode; From a917c0c88d33c4c91ae8ca8aa4c13f2fb1de10b9 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 31 Aug 2025 19:41:44 -0700 Subject: [PATCH 09/24] Fix graph.svelte --- .../document/node_graph/utility_types.rs | 2 +- .../network_interface/node_graph.rs | 4 +- frontend/src/components/views/Graph.svelte | 431 ++++++++---------- 3 files changed, 205 insertions(+), 232 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 14a9ea5107..f004921ef8 100644 --- a/editor/src/messages/portfolio/document/node_graph/utility_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/utility_types.rs @@ -89,7 +89,7 @@ pub struct FrontendGraphOutput { // Metadata that is common to nodes and layers #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] pub struct FrontendNodeMetadata { - #[serde(rename = "node_id")] + #[serde(rename = "nodeId")] pub node_id: NodeId, // TODO: Remove and replace with popup manager system #[serde(rename = "canBeLayer")] diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/node_graph.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/node_graph.rs index aae024f91c..60a3357725 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface/node_graph.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/node_graph.rs @@ -72,8 +72,8 @@ impl NodeNetworkInterface { locked: self.is_locked(&node_id, network_path), chain_width: self.chain_width(&node_id, network_path), layer_has_left_border_gap: self.layer_has_left_border_gap(&node_id, network_path), - primary_input_connected_to_layer: self.primary_output_connected_to_layer(&node_id, network_path), - primary_output_connected_to_layer: self.primary_input_connected_to_layer(&node_id, network_path), + primary_input_connected_to_layer: self.primary_input_connected_to_layer(&node_id, network_path), + primary_output_connected_to_layer: self.primary_output_connected_to_layer(&node_id, network_path), }); FrontendNodeOrLayer { node: None, layer } } diff --git a/frontend/src/components/views/Graph.svelte b/frontend/src/components/views/Graph.svelte index 0c48a672a2..4e45e8e9f9 100644 --- a/frontend/src/components/views/Graph.svelte +++ b/frontend/src/components/views/Graph.svelte @@ -130,20 +130,24 @@ editor.handle.createNode(nodeType, $nodeGraph.contextMenuInformation.contextMenuCoordinates.x, $nodeGraph.contextMenuInformation.contextMenuCoordinates.y); } - function nodeBorderMask(nodeWidth: number, primaryInputExists: boolean, exposedSecondaryInputs: number, primaryOutputExists: boolean, exposedSecondaryOutputs: number): string { - const nodeHeight = Math.max(1 + exposedSecondaryInputs, 1 + exposedSecondaryOutputs) * 24; + function nodeBorderMask(nodeInputs: (FrontendGraphInput | undefined)[], nodeOutputs: (FrontendGraphOutput | undefined)[]): string { + const nodeWidth = 120; + const secondaryInputs = nodeInputs.slice(1).filter((x): x is FrontendGraphInput => x !== undefined); + const secondaryOutputs = nodeOutputs.slice(1); + + const nodeHeight = Math.max(1 + secondaryInputs.length, 1 + secondaryOutputs.length) * 24; const boxes: { x: number; y: number; width: number; height: number }[] = []; // Primary input - if (primaryInputExists) boxes.push({ x: -8, y: 4, width: 16, height: 16 }); + if (nodeInputs[0]) boxes.push({ x: -8, y: 4, width: 16, height: 16 }); // Secondary inputs - for (let i = 0; i < exposedSecondaryInputs; i++) boxes.push({ x: -8, y: 4 + (i + 1) * 24, width: 16, height: 16 }); + for (let i = 0; i < secondaryInputs.length; i++) boxes.push({ x: -8, y: 4 + (i + 1) * 24, width: 16, height: 16 }); // Primary output - if (primaryOutputExists) boxes.push({ x: nodeWidth - 8, y: 4, width: 16, height: 16 }); + if (nodeOutputs[0]) boxes.push({ x: nodeWidth - 8, y: 4, width: 16, height: 16 }); // Exposed outputs - for (let i = 0; i < exposedSecondaryOutputs; i++) boxes.push({ x: nodeWidth - 8, y: 4 + (i + 1) * 24, width: 16, height: 16 }); + for (let i = 0; i < secondaryOutputs.length; i++) boxes.push({ x: nodeWidth - 8, y: 4 + (i + 1) * 24, width: 16, height: 16 }); return borderMask(boxes, nodeWidth, nodeHeight); } @@ -203,11 +207,17 @@ return `Connected to:\n${input.connectedToString}`; } - function zipWithUndefined(arr1: FrontendGraphInput[], arr2: FrontendGraphOutput[]) { - const maxLength = Math.max(arr1.length, arr2.length); - const result = []; + function collectExposedInputsOutputs( + inputs: (FrontendGraphInput | undefined)[], + outputs: (FrontendGraphOutput | undefined)[], + ): [FrontendGraphInput | undefined, FrontendGraphOutput | undefined][] { + const secondaryInputs = inputs.slice(1).filter((x): x is FrontendGraphInput => x !== undefined); + const secondaryOutputs = outputs.slice(1); + const maxLength = Math.max(secondaryInputs.length, secondaryOutputs.length); + const result: [FrontendGraphInput | undefined, FrontendGraphOutput | undefined][] = []; + for (let i = 0; i < maxLength; i++) { - result.push([arr1[i], arr2[i]]); + result.push([secondaryInputs[i] || undefined, secondaryOutputs[i] || undefined]); } return result; } @@ -485,124 +495,118 @@ {/if}
- -
- - {#each Array.from($nodeGraph.nodesToRender).filter(([nodeId, node]) => node.isLayer && $nodeGraph.visibleNodes.has(nodeId)) as [nodeId, layer]} - {@const clipPathId = String(Math.random()).substring(2)} - {@const layerAreaWidth = $nodeGraph.layerWidths.get(layer.id) || 8} - {@const layerChainWidth = layer.chainWidth !== 0 ? layer.chainWidth + 0.5 : 0} - {@const description = (layer.reference && $nodeGraph.nodeDescriptions.get(layer.reference)) || undefined} -
- {#if layer.errors} - {layer.errors} - {layer.errors} - {/if} -
- {#if $nodeGraph.thumbnails.has(nodeId)} - {@html $nodeGraph.thumbnails.get(nodeId)} +
+ {#each Array.from($nodeGraph.nodesToRender) as [nodeId, nodeToRender]} + {#if nodeToRender.nodeOrLayer.layer !== undefined} + {@const nodeMetadata = nodeToRender.metadata} + {@const layer = nodeToRender.nodeOrLayer.layer} + {@const clipPathId = String(Math.random()).substring(2)} + {@const layerAreaWidth = $nodeGraph.layerWidths.get(nodeToRender.metadata.nodeId) || 8} + {@const layerChainWidth = layer.chainWidth !== 0 ? layer.chainWidth + 0.5 : 0} + {@const description = (nodeMetadata.reference && $nodeGraph.nodeDescriptions.get(nodeMetadata.reference)) || undefined} +
+ {#if nodeMetadata.errors} + {layer.errors} + {layer.errors} {/if} - - {#if layer.primaryOutput} +
+ {#if $nodeGraph.thumbnails.has(nodeId)} + {@html $nodeGraph.thumbnails.get(nodeId)} + {/if} + - {outputTooltip(layer.primaryOutput)} - {#if layer.primaryOutput.connectedTo.length > 0} - - {#if layer.primaryOutputConnectedToLayer} - - {/if} - {:else} - + {outputTooltip(layer.output)} + 0 ? "var(--data-color)" : "var(--data-color-dim)"} /> + + {#if layer.output.connectedTo.length > 0 && layer.primaryOutputConnectedToLayer} + {/if} - {/if} - - - {#if layer.primaryInput} - {inputTooltip(layer.primaryInput)} - {/if} - {#if layer.primaryInput?.connectedTo !== "nothing"} - - {#if layer.primaryInputConnectedToLayer} - - {/if} - {:else} - - {/if} - -
- - {#if layer.exposedInputs.length > 0} -
+ - {inputTooltip(layer.exposedInputs[0])} - {#if layer.exposedInputs[0].connectedTo !== undefined} - + {#if layer.bottomInput} + {inputTooltip(layer.bottomInput)} + {/if} + {#if layer.bottomInput?.connectedToNode !== undefined} + + {#if layer.primaryInputConnectedToLayer} + + {/if} {:else} - + {/if}
- {/if} -
- - {layer.displayName} + + {#if layer.sideInput} +
+ + {inputTooltip(layer.sideInput)} + + +
+ {/if} +
+ + {nodeMetadata.displayName} +
+
+ { + /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ + }} + tooltip={nodeMetadata.visible ? "Visible" : "Hidden"} + /> + + + + + + + + +
-
- { - /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ - }} - tooltip={layer.visible ? "Visible" : "Hidden"} - /> - - - - - - - - - -
+ {/if} {/each} -
@@ -630,129 +634,96 @@ {/if}
- - - {#each Array.from($nodeGraph.nodesToRender).filter(([nodeId, node]) => !node.isLayer && $nodeGraph.visibleNodes.has(nodeId)) as [nodeId, node]} - {@const exposedInputsOutputs = zipWithUndefined(node.exposedInputs, node.exposedOutputs)} - {@const clipPathId = String(Math.random()).substring(2)} - {@const description = (node.reference && $nodeGraph.nodeDescriptions.get(node.reference)) || undefined} -
- {#if node.errors} - {node.errors} - {node.errors} - {/if} - -
- - - {node.displayName} -
- - {#if exposedInputsOutputs.length > 0} -
- {#each exposedInputsOutputs as [input, output]} -
- - {input !== undefined ? input.name : output.name} - -
- {/each} + {#each Array.from($nodeGraph.nodesToRender) as [nodeId, nodeToRender]} + {#if nodeToRender.nodeOrLayer.node !== undefined && $nodeGraph.visibleNodes.has(nodeId)} + {@const nodeMetadata = nodeToRender.metadata} + {@const node = nodeToRender.nodeOrLayer.node} + {@const exposedInputsOutputs = collectExposedInputsOutputs(node.inputs, node.outputs)} + {@const clipPathId = String(Math.random()).substring(2)} + {@const description = (nodeMetadata.reference && $nodeGraph.nodeDescriptions.get(nodeMetadata.reference)) || undefined} +
+ {#if nodeMetadata.errors} + {node.errors} + {node.errors} + {/if} + +
+ + + {nodeMetadata.displayName}
- {/if} - -
- {#if node.primaryInput?.dataType} - - {inputTooltip(node.primaryInput)} - {#if node.primaryInput.connectedTo !== undefined} - - {:else} - - {/if} - + + {#if exposedInputsOutputs.length > 0} +
+ {#each exposedInputsOutputs as [input, output]} +
+ + {input?.name ?? output?.name ?? ""} + +
+ {/each} +
{/if} - {#each node.exposedInputs as secondary, index} - {#if index < node.exposedInputs.length} - - {inputTooltip(secondary)} - {#if secondary.connectedTo !== undefined} - - {:else} - - {/if} - - {/if} - {/each} -
- -
- {#if node.primaryOutput} - - {outputTooltip(node.primaryOutput)} - {#if node.primaryOutput.connectedTo !== undefined} - - {:else} - + +
+ {#each node.inputs as input} + {#if input !== undefined} + + {inputTooltip(input)} + + {/if} - - {/if} - {#each node.exposedOutputs as secondary} - - {outputTooltip(secondary)} - {#if secondary.connectedTo !== undefined} - - {:else} - + {/each} +
+ +
+ {#each node.outputs as output} + {#if output !== undefined} + + {outputTooltip(output)} + + {/if} - - {/each} + {/each} +
+ + + + + + +
- - - - - - - -
+ {/if} {/each}
@@ -1095,6 +1066,8 @@ // Keeps the connectors above the wires z-index: 1; + margin-top: -24px; + &.input { left: -3px; } From e2c45195ef10d7954fbd5ec4dfb03160cd1cded7 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 1 Sep 2025 11:59:49 -0700 Subject: [PATCH 10/24] Add opacity and open to the render data --- .../src/messages/frontend/frontend_message.rs | 7 +- .../document/document_message_handler.rs | 8 +- .../node_graph/node_graph_message_handler.rs | 32 +- editor/src/test_utils.rs | 8 +- .../src/components/panels/Document.svelte | 28 +- frontend/src/components/views/Graph.svelte | 965 +++++++++--------- frontend/src/messages.ts | 13 +- frontend/src/state-providers/document.ts | 9 - frontend/src/state-providers/node-graph.ts | 15 +- 9 files changed, 540 insertions(+), 545 deletions(-) diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index e6f84f5aa1..59a713b544 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -244,9 +244,6 @@ pub enum FrontendMessage { #[serde(rename = "setColorChoice")] set_color_choice: Option, }, - UpdateGraphFadeArtwork { - percentage: f64, - }, UpdateInputHints { #[serde(rename = "hintData")] hint_data: HintData, @@ -274,9 +271,11 @@ pub enum FrontendMessage { UpdateMouseCursor { cursor: MouseCursorIcon, }, - UpdateNodeGraphNodes { + UpdateNodeGraphRender { #[serde(rename = "nodesToRender")] nodes_to_render: Vec, + open: bool, + opacity: f64, #[serde(rename = "inSelectedNetwork")] in_selected_network: bool, // Displays a dashed border around the node diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 88aeacacf1..ccf12a467a 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -561,9 +561,6 @@ impl MessageHandler> for DocumentMes self.graph_view_overlay_open = open; responses.add(FrontendMessage::UpdateGraphViewOverlay { open }); - responses.add(FrontendMessage::UpdateGraphFadeArtwork { - percentage: self.graph_fade_artwork_percentage, - }); // Update the tilt menu bar buttons to be disabled when the graph is open responses.add(MenuBarMessage::SendLayout); @@ -579,12 +576,13 @@ impl MessageHandler> for DocumentMes responses.add(NavigationMessage::CanvasTiltSet { angle_radians: 0. }); responses.add(NodeGraphMessage::SetGridAlignedEdges); responses.add(NodeGraphMessage::UpdateGraphBarRight); - responses.add(NodeGraphMessage::SendGraph); responses.add(NodeGraphMessage::UpdateHints); } else { responses.add(ToolMessage::ActivateTool { tool_type: *current_tool }); responses.add(OverlaysMessage::Draw); // Redraw overlays when graph is closed } + + responses.add(NodeGraphMessage::SendGraph); } DocumentMessage::GraphViewOverlayToggle => { responses.add(DocumentMessage::GraphViewOverlay { open: !self.graph_view_overlay_open }); @@ -1200,7 +1198,7 @@ impl MessageHandler> for DocumentMes } DocumentMessage::SetGraphFadeArtwork { percentage } => { self.graph_fade_artwork_percentage = percentage; - responses.add(FrontendMessage::UpdateGraphFadeArtwork { percentage }); + responses.add(NodeGraphMessage::SendGraph); } DocumentMessage::SetNodePinned { node_id, pinned } => { responses.add(DocumentMessage::AddTransaction); diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index 1aa6cdf6d6..084f2f72cb 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -1634,24 +1634,24 @@ impl<'a> MessageHandler> for NodeG responses.add(DocumentMessage::DocumentStructureChanged); responses.add(PropertiesPanelMessage::Refresh); responses.add(NodeGraphMessage::UpdateActionButtons); - if graph_view_overlay_open { - let nodes_to_render = network_interface.collect_nodes(&self.node_graph_errors, breadcrumb_network_path); - self.frontend_nodes = nodes_to_render.iter().map(|node| node.metadata.node_id).collect(); - let previewed_node = network_interface.previewed_node(breadcrumb_network_path); - responses.add(FrontendMessage::UpdateNodeGraphNodes { - nodes_to_render, - in_selected_network: selection_network_path == breadcrumb_network_path, - previewed_node, - }); - responses.add(NodeGraphMessage::UpdateVisibleNodes); + let nodes_to_render = network_interface.collect_nodes(&self.node_graph_errors, breadcrumb_network_path); + self.frontend_nodes = nodes_to_render.iter().map(|node| node.metadata.node_id).collect(); + let previewed_node = network_interface.previewed_node(breadcrumb_network_path); + responses.add(FrontendMessage::UpdateNodeGraphRender { + nodes_to_render, + open: graph_view_overlay_open, + opacity: graph_fade_artwork_percentage, + in_selected_network: selection_network_path == breadcrumb_network_path, + previewed_node, + }); + responses.add(NodeGraphMessage::UpdateVisibleNodes); - let layer_widths = network_interface.collect_layer_widths(breadcrumb_network_path); + let layer_widths = network_interface.collect_layer_widths(breadcrumb_network_path); - responses.add(NodeGraphMessage::UpdateImportsExports); - responses.add(FrontendMessage::UpdateLayerWidths { layer_widths }); - responses.add(NodeGraphMessage::SendWires); - self.update_node_graph_hints(responses); - } + responses.add(NodeGraphMessage::UpdateImportsExports); + responses.add(FrontendMessage::UpdateLayerWidths { layer_widths }); + responses.add(NodeGraphMessage::SendWires); + self.update_node_graph_hints(responses); } NodeGraphMessage::SetGridAlignedEdges => { if graph_view_overlay_open { diff --git a/editor/src/test_utils.rs b/editor/src/test_utils.rs index addadae0c2..f917a8b003 100644 --- a/editor/src/test_utils.rs +++ b/editor/src/test_utils.rs @@ -301,11 +301,11 @@ pub trait FrontendMessageTestUtils { impl FrontendMessageTestUtils for FrontendMessage { fn check_node_graph_error(&self) { - let FrontendMessage::UpdateNodeGraphNodes { nodes, .. } = self else { return }; + let FrontendMessage::UpdateNodeGraphRender { nodes_to_render, .. } = self else { return }; - for node in nodes { - if let Some(error) = &node.errors { - panic!("error on {}: {}", node.display_name, error); + for node in nodes_to_render { + if let Some(error) = &node.metadata.errors { + panic!("error on {}: {}", node.metadata.display_name, error); } } } diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index c259c9ba74..0a58272291 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -565,9 +565,7 @@ {/if}
-
- -
+ -
- - {#if $nodeGraph.contextMenuInformation} - - {#if typeof $nodeGraph.contextMenuInformation.contextMenuData === "string" && $nodeGraph.contextMenuInformation.contextMenuData === "CreateNode"} - createNode(e.detail)} /> - {:else if $nodeGraph.contextMenuInformation.contextMenuData && "compatibleType" in $nodeGraph.contextMenuInformation.contextMenuData} - createNode(e.detail)} /> - {:else} - {@const contextMenuData = $nodeGraph.contextMenuInformation.contextMenuData} - - Display as - toggleLayerDisplay(false, contextMenuData.nodeId), - }, - { - value: "layer", - label: "Layer", - action: () => toggleLayerDisplay(true, contextMenuData.nodeId), - }, - ]} - disabled={!canBeToggledBetweenNodeAndLayer(contextMenuData.nodeId)} - /> - - - - editor.handle.mergeSelectedNodes()} /> - - {/if} - - {/if} +
+
+ + {#if $nodeGraph.contextMenuInformation} + + {#if typeof $nodeGraph.contextMenuInformation.contextMenuData === "string" && $nodeGraph.contextMenuInformation.contextMenuData === "CreateNode"} + createNode(e.detail)} /> + {:else if $nodeGraph.contextMenuInformation.contextMenuData && "compatibleType" in $nodeGraph.contextMenuInformation.contextMenuData} + createNode(e.detail)} /> + {:else} + {@const contextMenuData = $nodeGraph.contextMenuInformation.contextMenuData} + + Display as + toggleLayerDisplay(false, contextMenuData.nodeId), + }, + { + value: "layer", + label: "Layer", + action: () => toggleLayerDisplay(true, contextMenuData.nodeId), + }, + ]} + disabled={!canBeToggledBetweenNodeAndLayer(contextMenuData.nodeId)} + /> + + + + editor.handle.mergeSelectedNodes()} /> + + {/if} + + {/if} - - {#if $nodeGraph.clickTargets} -
+ + {#if $nodeGraph.clickTargets} +
+ + {#each $nodeGraph.clickTargets.nodeClickTargets as pathString} + + {/each} + {#each $nodeGraph.clickTargets.layerClickTargets as pathString} + + {/each} + {#each $nodeGraph.clickTargets.connectorClickTargets as pathString} + + {/each} + {#each $nodeGraph.clickTargets.iconClickTargets as pathString} + + {/each} + + + {#each $nodeGraph.clickTargets.modifyImportExport as pathString} + + {/each} + +
+ {/if} + + +
- {#each $nodeGraph.clickTargets.nodeClickTargets as pathString} - - {/each} - {#each $nodeGraph.clickTargets.layerClickTargets as pathString} - - {/each} - {#each $nodeGraph.clickTargets.connectorClickTargets as pathString} - - {/each} - {#each $nodeGraph.clickTargets.iconClickTargets as pathString} - - {/each} - - - {#each $nodeGraph.clickTargets.modifyImportExport as pathString} - + {#each $nodeGraph.wires.values() as map} + {#each map.values() as { pathString, dataType, thick, dashed }} + {#if thick} + + {/if} + {/each} {/each}
- {/if} - -
- - {#each $nodeGraph.wires.values() as map} - {#each map.values() as { pathString, dataType, thick, dashed }} - {#if thick} - + +
+ {#if $nodeGraph.updateImportsExports} + {#each $nodeGraph.updateImportsExports.imports as frontendOutput, index} + {#if frontendOutput} + + {outputTooltip(frontendOutput)} + {#if frontendOutput.connectedTo.length > 0} + + {:else} + + {/if} + + +
(hoveringImportIndex = index)} + on:pointerleave={() => (hoveringImportIndex = undefined)} + class="edit-import-export import" + class:separator-bottom={index === 0 && $nodeGraph.updateImportsExports.addImportExport} + class:separator-top={index === 1 && $nodeGraph.updateImportsExports.addImportExport} + style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 8) / 24} + style:--offset-top={($nodeGraph.updateImportsExports.importPosition.y - 8) / 24 + index} + > + {#if editingNameImportIndex == index} + e.key === "Enter" && setEditingImportName(e)} + /> + {:else} +

setEditingImportNameIndex(index, frontendOutput.name)}> + {frontendOutput.name} +

+ {/if} + {#if (hoveringImportIndex === index || editingNameImportIndex === index) && $nodeGraph.updateImportsExports.addImportExport} + { + /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ + }} + /> + {#if index > 0} +
+ {/if} + {/if} +
+ {:else} +
+ editor.handle.addPrimaryImport()} /> +
{/if} {/each} - {/each} - -
- - -
- {#if $nodeGraph.updateImportsExports} - {#each $nodeGraph.updateImportsExports.imports as frontendOutput, index} - {#if frontendOutput} - - {outputTooltip(frontendOutput)} - {#if frontendOutput.connectedTo.length > 0} - - {:else} - - {/if} - -
(hoveringImportIndex = index)} - on:pointerleave={() => (hoveringImportIndex = undefined)} - class="edit-import-export import" - class:separator-bottom={index === 0 && $nodeGraph.updateImportsExports.addImportExport} - class:separator-top={index === 1 && $nodeGraph.updateImportsExports.addImportExport} - style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 8) / 24} - style:--offset-top={($nodeGraph.updateImportsExports.importPosition.y - 8) / 24 + index} - > - {#if editingNameImportIndex == index} - e.key === "Enter" && setEditingImportName(e)} - /> - {:else} -

setEditingImportNameIndex(index, frontendOutput.name)}> - {frontendOutput.name} -

- {/if} - {#if (hoveringImportIndex === index || editingNameImportIndex === index) && $nodeGraph.updateImportsExports.addImportExport} - { - /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ - }} - /> - {#if index > 0} -
+ {#each $nodeGraph.updateImportsExports.exports as frontendInput, index} + {#if frontendInput} + + {inputTooltip(frontendInput)} + {#if frontendInput.connectedTo !== "nothing"} + + {:else} + {/if} - {/if} -
- {:else} + +
(hoveringExportIndex = index)} + on:pointerleave={() => (hoveringExportIndex = undefined)} + class="edit-import-export export" + class:separator-bottom={index === 0 && $nodeGraph.updateImportsExports.addImportExport} + class:separator-top={index === 1 && $nodeGraph.updateImportsExports.addImportExport} + style:--offset-left={($nodeGraph.updateImportsExports.exportPosition.x - 8) / 24} + style:--offset-top={($nodeGraph.updateImportsExports.exportPosition.y - 8) / 24 + index} + > + {#if (hoveringExportIndex === index || editingNameExportIndex === index) && $nodeGraph.updateImportsExports.addImportExport} + {#if index > 0} +
+ {/if} + { + /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ + }} + /> + {/if} + {#if editingNameExportIndex === index} + e.key === "Enter" && setEditingExportName(e)} + /> + {:else} +

setEditingExportNameIndex(index, frontendInput.name)}> + {frontendInput.name} +

+ {/if} +
+ {:else} +
+ editor.handle.addPrimaryExport()} /> +
+ {/if} + {/each} + + {#if $nodeGraph.updateImportsExports.addImportExport == true}
- editor.handle.addPrimaryImport()} /> + editor.handle.addSecondaryImport()} />
- {/if} - {/each} - - {#each $nodeGraph.updateImportsExports.exports as frontendInput, index} - {#if frontendInput} - - {inputTooltip(frontendInput)} - {#if frontendInput.connectedTo !== "nothing"} - - {:else} - - {/if} - -
(hoveringExportIndex = index)} - on:pointerleave={() => (hoveringExportIndex = undefined)} - class="edit-import-export export" - class:separator-bottom={index === 0 && $nodeGraph.updateImportsExports.addImportExport} - class:separator-top={index === 1 && $nodeGraph.updateImportsExports.addImportExport} - style:--offset-left={($nodeGraph.updateImportsExports.exportPosition.x - 8) / 24} - style:--offset-top={($nodeGraph.updateImportsExports.exportPosition.y - 8) / 24 + index} - > - {#if (hoveringExportIndex === index || editingNameExportIndex === index) && $nodeGraph.updateImportsExports.addImportExport} - {#if index > 0} -
- {/if} - { - /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ - }} - /> - {/if} - {#if editingNameExportIndex === index} - e.key === "Enter" && setEditingExportName(e)} - /> - {:else} -

setEditingExportNameIndex(index, frontendInput.name)}> - {frontendInput.name} -

- {/if} -
- {:else}
- editor.handle.addPrimaryExport()} /> + editor.handle.addSecondaryExport()} />
{/if} - {/each} - - {#if $nodeGraph.updateImportsExports.addImportExport == true} -
- editor.handle.addSecondaryImport()} /> -
-
- editor.handle.addSecondaryExport()} /> -
- {/if} - {#if $nodeGraph.reorderImportIndex !== undefined} - {@const position = { - x: Number($nodeGraph.updateImportsExports.importPosition.x), - y: Number($nodeGraph.updateImportsExports.importPosition.y) + Number($nodeGraph.reorderImportIndex) * 24, - }} -
- {/if} + {#if $nodeGraph.reorderImportIndex !== undefined} + {@const position = { + x: Number($nodeGraph.updateImportsExports.importPosition.x), + y: Number($nodeGraph.updateImportsExports.importPosition.y) + Number($nodeGraph.reorderImportIndex) * 24, + }} +
+ {/if} - {#if $nodeGraph.reorderExportIndex !== undefined} - {@const position = { - x: Number($nodeGraph.updateImportsExports.exportPosition.x), - y: Number($nodeGraph.updateImportsExports.exportPosition.y) + Number($nodeGraph.reorderExportIndex) * 24, - }} -
+ {#if $nodeGraph.reorderExportIndex !== undefined} + {@const position = { + x: Number($nodeGraph.updateImportsExports.exportPosition.x), + y: Number($nodeGraph.updateImportsExports.exportPosition.y) + Number($nodeGraph.reorderExportIndex) * 24, + }} +
+ {/if} {/if} - {/if} -
+
-
- {#each Array.from($nodeGraph.nodesToRender) as [nodeId, nodeToRender]} - {#if nodeToRender.nodeOrLayer.layer !== undefined} - {@const nodeMetadata = nodeToRender.metadata} - {@const layer = nodeToRender.nodeOrLayer.layer} - {@const clipPathId = String(Math.random()).substring(2)} - {@const layerAreaWidth = $nodeGraph.layerWidths.get(nodeToRender.metadata.nodeId) || 8} - {@const layerChainWidth = layer.chainWidth !== 0 ? layer.chainWidth + 0.5 : 0} - {@const description = (nodeMetadata.reference && $nodeGraph.nodeDescriptions.get(nodeMetadata.reference)) || undefined} -
- {#if nodeMetadata.errors} - {layer.errors} - {layer.errors} - {/if} -
- {#if $nodeGraph.thumbnails.has(nodeId)} - {@html $nodeGraph.thumbnails.get(nodeId)} +
+ {#each Array.from($nodeGraph.nodesToRender) as [nodeId, nodeToRender]} + {#if nodeToRender.nodeOrLayer.layer !== undefined} + {@const nodeMetadata = nodeToRender.metadata} + {@const layer = nodeToRender.nodeOrLayer.layer} + {@const clipPathId = String(Math.random()).substring(2)} + {@const layerAreaWidth = $nodeGraph.layerWidths.get(nodeToRender.metadata.nodeId) || 8} + {@const layerChainWidth = layer.chainWidth !== 0 ? layer.chainWidth + 0.5 : 0} + {@const description = (nodeMetadata.reference && $nodeGraph.nodeDescriptions.get(nodeMetadata.reference)) || undefined} +
+ {#if nodeMetadata.errors} + {layer.errors} + {layer.errors} {/if} - - - {outputTooltip(layer.output)} - 0 ? "var(--data-color)" : "var(--data-color-dim)"} /> - - {#if layer.output.connectedTo.length > 0 && layer.primaryOutputConnectedToLayer} - - {/if} - - - - {#if layer.bottomInput} - {inputTooltip(layer.bottomInput)} - {/if} - {#if layer.bottomInput?.connectedToNode !== undefined} - - {#if layer.primaryInputConnectedToLayer} - - {/if} - {:else} - +
+ {#if $nodeGraph.thumbnails.has(nodeId)} + {@html $nodeGraph.thumbnails.get(nodeId)} {/if} - -
- - {#if layer.sideInput} -
+ - {inputTooltip(layer.sideInput)} + {outputTooltip(layer.output)} 0 ? "var(--data-color)" : "var(--data-color-dim)"} /> + + {#if layer.output.connectedTo.length > 0 && layer.primaryOutputConnectedToLayer} + + {/if} + + + + {#if layer.bottomInput} + {inputTooltip(layer.bottomInput)} + {/if} + {#if layer.bottomInput?.connectedToNode !== undefined} + + {#if layer.primaryInputConnectedToLayer} + + {/if} + {:else} + + {/if}
- {/if} -
- - {nodeMetadata.displayName} -
-
- { - /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ - }} - tooltip={nodeMetadata.visible ? "Visible" : "Hidden"} - /> - - - - - - - - - -
- {/if} - {/each} - -
- - {#each $nodeGraph.wires.values() as map} - {#each map.values() as { pathString, dataType, thick, dashed }} - {#if !thick} - - {/if} - {/each} - {/each} - {#if $nodeGraph.wirePathInProgress} - - {/if} - -
- {#each Array.from($nodeGraph.nodesToRender) as [nodeId, nodeToRender]} - {#if nodeToRender.nodeOrLayer.node !== undefined && $nodeGraph.visibleNodes.has(nodeId)} - {@const nodeMetadata = nodeToRender.metadata} - {@const node = nodeToRender.nodeOrLayer.node} - {@const exposedInputsOutputs = collectExposedInputsOutputs(node.inputs, node.outputs)} - {@const clipPathId = String(Math.random()).substring(2)} - {@const description = (nodeMetadata.reference && $nodeGraph.nodeDescriptions.get(nodeMetadata.reference)) || undefined} -
- {#if nodeMetadata.errors} - {node.errors} - {node.errors} - {/if} - -
- - - {nodeMetadata.displayName} -
- - {#if exposedInputsOutputs.length > 0} -
- {#each exposedInputsOutputs as [input, output]} -
- - {input?.name ?? output?.name ?? ""} - -
- {/each} -
- {/if} - -
- {#each node.inputs as input} - {#if input !== undefined} - - {inputTooltip(input)} - - - {/if} - {/each} -
- -
- {#each node.outputs as output} - {#if output !== undefined} + + {#if layer.sideInput} +
- {outputTooltip(output)} + {inputTooltip(layer.sideInput)} +
+ {/if} +
+ + {nodeMetadata.displayName} +
+
+ { + /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ + }} + tooltip={nodeMetadata.visible ? "Visible" : "Hidden"} + /> + + + + + + + + + +
+ {/if} + {/each} + +
+ + {#each $nodeGraph.wires.values() as map} + {#each map.values() as { pathString, dataType, thick, dashed }} + {#if !thick} + {/if} {/each} + {/each} + {#if $nodeGraph.wirePathInProgress} + + {/if} + +
+ {#each Array.from($nodeGraph.nodesToRender) as [nodeId, nodeToRender]} + {#if nodeToRender.nodeOrLayer.node !== undefined && $nodeGraph.visibleNodes.has(nodeId)} + {@const nodeMetadata = nodeToRender.metadata} + {@const node = nodeToRender.nodeOrLayer.node} + {@const exposedInputsOutputs = collectExposedInputsOutputs(node.inputs, node.outputs)} + {@const clipPathId = String(Math.random()).substring(2)} + {@const description = (nodeMetadata.reference && $nodeGraph.nodeDescriptions.get(nodeMetadata.reference)) || undefined} +
+ {#if nodeMetadata.errors} + {node.errors} + {node.errors} + {/if} + +
+ + + {nodeMetadata.displayName} +
+ + {#if exposedInputsOutputs.length > 0} +
+ {#each exposedInputsOutputs as [input, output]} +
+ + {input?.name ?? output?.name ?? ""} + +
+ {/each} +
+ {/if} + +
+ {#each node.inputs as input} + {#if input !== undefined} + + {inputTooltip(input)} + + + {/if} + {/each} +
+ +
+ {#each node.outputs as output} + {#if output !== undefined} + + {outputTooltip(output)} + + + {/if} + {/each} +
+ + + + + + +
- - - - - - - -
- {/if} - {/each} + {/if} + {/each} +
-
- -{#if $nodeGraph.box} -
-{/if} + + {#if $nodeGraph.box} +
+ {/if} +
diff --git a/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs b/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs index 571173790c..42d3d09c65 100644 --- a/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs +++ b/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs @@ -15,7 +15,7 @@ use crate::{ transform::ApplyTransform, vector::{ Vector, - style::{Fill, Stroke}, + style::{Fill, Stroke, StrokeAlign}, }, }; @@ -102,16 +102,17 @@ pub fn draw_nodes(nodes: &Vec) -> Table { let border_mask_table = Table::new_from_row(border_mask_row); node_table.push(TableRow::new_from_element(Graphic::Vector(border_mask_table))); - // Border table is implemented as a clip mask + // Border is implemented as a clip mask let mut border_table = Table::new(); let mut border_vector = Vector::from_bezpath(node_bez_path); - let primary_output_color = frontend_node + let border_color = frontend_node .primary_output .as_ref() .map(|primary_output| primary_output.data_type.data_color_dim()) .unwrap_or(FrontendGraphDataType::General.data_color_dim()); - let border_color = Color::from_rgba8_no_srgb(primary_output_color).unwrap(); - border_vector.style.stroke = Some(crate::vector::style::Stroke::new(Some(border_color), 1.)); + let stroke = Stroke::new(Some(border_color), 1.); + // stroke.align = StrokeAlign::Inside; + border_vector.style.stroke = Some(stroke); let mut border_vector_row = TableRow::new_from_element(border_vector); border_vector_row.alpha_blending.clip = true; border_table.push(border_vector_row); @@ -182,22 +183,22 @@ pub fn draw_nodes(nodes: &Vec) -> Table { // Input and output ports let mut ports_table = Table::new(); if let Some(primary_input) = &frontend_node.primary_input { - let mut row = port_row(&primary_input.data_type); + let mut row = port_row(&primary_input.data_type, primary_input.connected_to_node.is_some()); row.transform = DAffine2::from_translation(DVec2::new(0., 12.)); ports_table.push(row); } for (index, secondary_input) in frontend_node.secondary_inputs.iter().enumerate() { - let mut row = port_row(&secondary_input.data_type); + let mut row = port_row(&secondary_input.data_type, secondary_input.connected_to_node.is_some()); row.transform = DAffine2::from_translation(DVec2::new(0., 12. + GRID_SIZE * (index + 1) as f64)); ports_table.push(row); } if let Some(primary_output) = &frontend_node.primary_output { - let mut row = port_row(&primary_output.data_type); + let mut row = port_row(&primary_output.data_type, true); row.transform = DAffine2::from_translation(DVec2::new(5. * GRID_SIZE, 12.)); ports_table.push(row); } for (index, secondary_output) in frontend_node.secondary_outputs.iter().enumerate() { - let mut row = port_row(&secondary_output.data_type); + let mut row = port_row(&secondary_output.data_type, true); row.transform = DAffine2::from_translation(DVec2::new(5. * GRID_SIZE, 12. + GRID_SIZE * (index + 1) as f64)); ports_table.push(row); } @@ -212,7 +213,7 @@ pub fn draw_nodes(nodes: &Vec) -> Table { pub fn draw_layers(nodes: &Vec) -> (Table, Table) { let mut layer_table = Table::new(); - let mut side_ports = Table::new(); + let mut side_ports_table = Table::new(); for node_to_render in nodes { if let Some(frontend_layer) = node_to_render.node_or_layer.layer.as_ref() { // The layer position is the top left of the thumbnail @@ -246,16 +247,14 @@ pub fn draw_layers(nodes: &Vec) -> (Table, Table< 0. }; - // Text starts at thumbnail + left padding - let text_start = 12. + 8.; + let text_left_padding = 8.; let right_text_edge = 8. + text_width; - let rounded_text_edge = (right_text_edge as f64 / 24.).ceil() * 24.; + // Text starts at thumbnail + left padding + let rounded_text_edge = ((12. + right_text_edge as f64) / 24.).ceil() * 24.; - let rounded_layer_width_pixels = rounded_text_edge + 24.; - // add the left thumbnail gap - let layer_right_edge_width = rounded_layer_width_pixels + 12.; + let rounded_layer_width_pixels = rounded_text_edge + 12.; - let right_layer_width = layer_right_edge_width.max(4.5 * GRID_SIZE); + let right_layer_width = rounded_layer_width_pixels.max(4.5 * GRID_SIZE); let thumbnail_width = 3. * GRID_SIZE; let full_layer_width = chain_width + thumbnail_width + right_layer_width; @@ -321,7 +320,9 @@ pub fn draw_layers(nodes: &Vec) -> (Table, Table< let bez_path = border_rect.to_path(BEZ_PATH_TOLERANCE); let mut border_vector = Vector::from_bezpath(bez_path); let border_color = Color::from_rgba8_no_srgb(COLOR_5_DULLGRAY).unwrap(); - border_vector.style.stroke = Some(crate::vector::style::Stroke::new(Some(border_color), 1.)); + let stroke = Stroke::new(Some(border_color), 1.); + // stroke.align = StrokeAlign::Inside; + border_vector.style.stroke = Some(stroke); let mut layer_border_clip = TableRow::new_from_element(border_vector); layer_border_clip.alpha_blending.clip = true; border_table.push(layer_border_clip); @@ -330,7 +331,7 @@ pub fn draw_layers(nodes: &Vec) -> (Table, Table< // The top layer contains the ports,thumbnail,text, etc for text_row in text_table.iter_mut() { text_row.element.style.fill = Fill::Solid(Color::WHITE); - *text_row.transform = DAffine2::from_translation(layer_position + DVec2::new(thumbnail_width + text_start, 16.)); + *text_row.transform = DAffine2::from_translation(layer_position + DVec2::new(thumbnail_width + text_left_padding, 16.)); } let top_layer = text_table; layer_table.push(TableRow::new_from_element(Graphic::Vector(top_layer))); @@ -338,34 +339,40 @@ pub fn draw_layers(nodes: &Vec) -> (Table, Table< // Ports let mut ports_table = Table::new(); if let Some(side_input) = &frontend_layer.side_input { - let mut port = port_row(&side_input.data_type); + let mut port: TableRow = port_row(&side_input.data_type, side_input.connected_to_node.is_some()); port.transform = DAffine2::from_translation(DVec2::new(layer_position.x - 15., layer_position.y + GRID_SIZE - 4.)); - ports_table.push(port); + side_ports_table.push(port); } let top_port = BezPath::from_svg("M0,6.953l2.521,-1.694a2.649,2.649,0,0,1,2.959,0l2.52,1.694v5.047h-8z").unwrap(); let mut vector = Vector::from_bezpath(top_port); - vector.style.fill = Fill::Solid(Color::from_rgba8_no_srgb(frontend_layer.output.data_type.data_color()).unwrap()); - let mut side_port = TableRow::new_from_element(vector); - side_port.transform = DAffine2::from_translation(DVec2::new(frontend_layer.position.x as f64 * 24. + GRID_SIZE * 2. - 4., layer_position.y - 12.)); - side_ports.push(side_port); + vector.style.fill = Fill::Solid(frontend_layer.output.data_type.data_color()); + let mut top_port = TableRow::new_from_element(vector); + top_port.transform = DAffine2::from_translation(DVec2::new(frontend_layer.position.x as f64 * 24. + GRID_SIZE * 2. - 4., layer_position.y - 12.)); + ports_table.push(top_port); + if frontend_layer.primary_output_connected_to_layer { let top_wire_cap = BezPath::from_svg("M0,-3.5h8v8l-2.521,-1.681a2.666,2.666,0,0,0,-2.959,0l-2.52,1.681z").unwrap(); let mut vector = Vector::from_bezpath(top_wire_cap); - vector.style.fill = Fill::Solid(Color::from_rgba8_no_srgb(frontend_layer.output.data_type.data_color_dim()).unwrap()); + vector.style.fill = Fill::Solid(frontend_layer.output.data_type.data_color_dim()); let mut vector_row = TableRow::new_from_element(vector); vector_row.transform = DAffine2::from_translation(DVec2::new(frontend_layer.position.x as f64 * 24. + GRID_SIZE * 2. - 4., layer_position.y - 12.)); ports_table.push(vector_row); } let bottom_port = BezPath::from_svg("M0,0H8V8L5.479,6.319a2.666,2.666,0,0,0-2.959,0L0,8Z").unwrap(); let mut vector = Vector::from_bezpath(bottom_port); - vector.style.fill = Fill::Solid(Color::from_rgba8_no_srgb(frontend_layer.bottom_input.data_type.data_color()).unwrap()); + let mut bottom_port_fill = if frontend_layer.bottom_input.connected_to_node.is_some() { + frontend_layer.bottom_input.data_type.data_color() + } else { + frontend_layer.bottom_input.data_type.data_color_dim() + }; + vector.style.fill = Fill::Solid(bottom_port_fill); let mut vector_row = TableRow::new_from_element(vector); vector_row.transform = DAffine2::from_translation(DVec2::new(frontend_layer.position.x as f64 * 24. + GRID_SIZE * 2. - 4., layer_position.y + 2. * GRID_SIZE)); ports_table.push(vector_row); if frontend_layer.primary_input_connected_to_layer { let bottom_port_cap = BezPath::from_svg("M0,10.95l2.52,-1.69c0.89,-0.6,2.06,-0.6,2.96,0l2.52,1.69v5.05h-8v-5.05z").unwrap(); let mut vector = Vector::from_bezpath(bottom_port_cap); - vector.style.fill = Fill::Solid(Color::from_rgba8_no_srgb(frontend_layer.bottom_input.data_type.data_color_dim()).unwrap()); + vector.style.fill = Fill::Solid(frontend_layer.bottom_input.data_type.data_color_dim()); let mut vector_row = TableRow::new_from_element(vector); vector_row.transform = DAffine2::from_translation(DVec2::new(frontend_layer.position.x as f64 * 24. + GRID_SIZE * 2. - 4., layer_position.y + 2. * GRID_SIZE)); ports_table.push(vector_row); @@ -406,11 +413,59 @@ pub fn draw_layers(nodes: &Vec) -> (Table, Table< icons_table.push(grip_row); } layer_table.push(TableRow::new_from_element(Graphic::Vector(icons_table))); + + // Thumbnail border/bg + let border = RoundedRect::new(layer_position.x, layer_position.y, layer_position.x + thumbnail_width, layer_position.y + 2. * GRID_SIZE, 2.); + let mut border_vec = Vector::from_bezpath(border.to_path(BEZ_PATH_TOLERANCE)); + let stroke = Stroke::new(Some(frontend_layer.output.data_type.data_color_dim()), 1.); + // stroke.align = StrokeAlign::Inside; + border_vec.style.stroke = Some(stroke); + border_vec.style.fill = Fill::Solid(Color::from_rgba8_no_srgb(COLOR_2_MILDBLACK).unwrap()); + layer_table.push(TableRow::new_from_element(Graphic::Vector(Table::new_from_element(border_vec)))); + + // Region to display thumbnail + let clip_vector = Vector::from_bezpath( + Rect::new( + layer_position.x + 2., + layer_position.y + 2., + layer_position.x + thumbnail_width - 2., + layer_position.y + GRID_SIZE * 2. - 2., + ) + .to_path(BEZ_PATH_TOLERANCE), + ); + layer_table.push(TableRow::new_from_element(Graphic::Vector(Table::new_from_row(TableRow::new_from_element(clip_vector))))); + + // Inner thumbnail + let mut inner_thumbnail_table = Table::new(); + for col in 0..9 { + for row in 0..6 { + let fill = if (col + row) % 2 == 0 { + Color::from_rgba8_no_srgb(COLOR_C_BRIGHTGRAY).unwrap() + } else { + Color::from_rgba8_no_srgb(COLOR_F_WHITE).unwrap() + }; + let mut vector = Vector::from_bezpath( + Rect::new( + 2. + 8. * col as f64 + layer_position.x, + 2. + 8. * row as f64 + layer_position.y, + 2. + 8. * col as f64 + layer_position.x + 9., + 2. + 8. * row as f64 + layer_position.y + 9., + ) + .to_path(BEZ_PATH_TOLERANCE), + ); + vector.style.fill = Fill::Solid(fill); + inner_thumbnail_table.push(TableRow::new_from_element(vector)); + } + } + let mut thumbnail_row = TableRow::new_from_element(Graphic::Vector(inner_thumbnail_table)); + thumbnail_row.alpha_blending.clip = true; + let graphic_table = Table::new_from_row(thumbnail_row); + layer_table.push(TableRow::new_from_element(Graphic::Graphic(graphic_table))); } } let mut ports_table = Table::new(); - ports_table.push(TableRow::new_from_element(Graphic::Vector(side_ports))); + ports_table.push(TableRow::new_from_element(Graphic::Vector(side_ports_table))); (layer_table, ports_table) } @@ -455,12 +510,17 @@ fn node_first_row(x0: f64, y0: f64, rounded_bottom: bool) -> Vector { vector } -fn port_row(data_type: &FrontendGraphDataType) -> TableRow { +fn port_row(data_type: &FrontendGraphDataType, full_brightness: bool) -> TableRow { let path = BezPath::from_svg("M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z").unwrap_or_else(|e| { panic!("Could not parse port svg from string: {}", e); }); let mut vector = Vector::from_bezpath(path); - vector.style.fill = Fill::Solid(Color::from_rgba8_no_srgb(data_type.data_color()).unwrap()); + let fill = if full_brightness { + Fill::Solid(data_type.data_color()) + } else { + Fill::Solid(data_type.data_color_dim()) + }; + vector.style.fill = fill; TableRow::new_from_element(vector) } @@ -470,7 +530,7 @@ pub fn draw_wires(nodes: &mut Vec) -> Table { for (wire_string, thick, data_type) in &mut node.wires { let mut wire_vector = Vector::from_bezpath(std::mem::take(wire_string)); let weight = if *thick { 8. } else { 2. }; - wire_vector.style.set_stroke(Stroke::new(Some(Color::from_rgba8_no_srgb(data_type.data_color_dim()).unwrap()), weight)); + wire_vector.style.set_stroke(Stroke::new(Some(data_type.data_color_dim()), weight)); wire_table.push(TableRow::new_from_element(wire_vector)); } } diff --git a/node-graph/gcore/src/node_graph_overlay/types.rs b/node-graph/gcore/src/node_graph_overlay/types.rs index 1acfcc3a0b..abf537b38b 100644 --- a/node-graph/gcore/src/node_graph_overlay/types.rs +++ b/node-graph/gcore/src/node_graph_overlay/types.rs @@ -1,4 +1,5 @@ use glam::{DAffine2, DVec2}; +use graphene_core_shaders::color::Color; use kurbo::BezPath; use crate::{node_graph_overlay::consts::*, uuid::NodeId}; @@ -214,8 +215,8 @@ pub enum FrontendGraphDataType { } impl FrontendGraphDataType { - pub fn data_color(&self) -> &'static str { - match self { + pub fn data_color(&self) -> Color { + let color_str = match self { FrontendGraphDataType::General => COLOR_DATA_GENERAL, FrontendGraphDataType::Number => COLOR_DATA_NUMBER, FrontendGraphDataType::Artboard => COLOR_DATA_ARTBOARD, @@ -225,10 +226,11 @@ impl FrontendGraphDataType { FrontendGraphDataType::Color => COLOR_DATA_COLOR, FrontendGraphDataType::Gradient => COLOR_DATA_GRADIENT, FrontendGraphDataType::Typography => COLOR_DATA_TYPOGRAPHY, - } + }; + Color::from_rgba8_no_srgb(color_str).unwrap() } - pub fn data_color_dim(&self) -> &'static str { - match self { + pub fn data_color_dim(&self) -> Color { + let color_str = match self { FrontendGraphDataType::General => COLOR_DATA_GENERAL_DIM, FrontendGraphDataType::Number => COLOR_DATA_NUMBER_DIM, FrontendGraphDataType::Artboard => COLOR_DATA_ARTBOARD_DIM, @@ -238,6 +240,7 @@ impl FrontendGraphDataType { FrontendGraphDataType::Color => COLOR_DATA_COLOR_DIM, FrontendGraphDataType::Gradient => COLOR_DATA_GRADIENT_DIM, FrontendGraphDataType::Typography => COLOR_DATA_TYPOGRAPHY_DIM, - } + }; + Color::from_rgba8_no_srgb(color_str).unwrap() } } From 8caf9317a55ca9ecfc728dce2cf9add96cf5b3b7 Mon Sep 17 00:00:00 2001 From: Adam Date: Sat, 6 Sep 2025 21:27:24 -0700 Subject: [PATCH 24/24] thumbnails --- .../document/node_graph/node_graph_message.rs | 5 ++ .../node_graph/node_graph_message_handler.rs | 6 ++ editor/src/node_graph_executor.rs | 37 ++++++---- editor/src/node_graph_executor/runtime.rs | 39 +++++----- node-graph/gcore/src/node_graph_overlay.rs | 2 +- .../src/node_graph_overlay/nodes_and_wires.rs | 41 ++++++++--- .../gcore/src/node_graph_overlay/types.rs | 24 ++++++- node-graph/gsvg-renderer/src/renderer.rs | 71 +++++++++++++++++++ 8 files changed, 182 insertions(+), 43 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs index 6f0b606ed0..06c2e8d2bd 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs @@ -7,6 +7,7 @@ use glam::IVec2; use graph_craft::document::value::TaggedValue; use graph_craft::document::{NodeId, NodeInput}; use graph_craft::proto::GraphErrors; +use graphene_std::Graphic; use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta; #[impl_message(Message, DocumentMessage, NodeGraph)] @@ -218,6 +219,10 @@ pub enum NodeGraphMessage { UpdateImportsExports, UpdateLayerPanel, UpdateNewNodeGraph, + UpdateThumbnail { + node_id: NodeId, + graphic: Graphic, + }, UpdateTypes { #[serde(skip)] resolved_types: ResolvedDocumentNodeTypesDelta, diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index a83056207e..5007646aa7 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -92,6 +92,8 @@ pub struct NodeGraphMessageHandler { reordering_export: Option, /// The end index of the moved connector end_index: Option, + // The rendered string for each thumbnail + pub thumbnails: HashMap, } /// NodeGraphMessageHandler always modifies the network which the selected nodes are in. No GraphOperationMessages should be added here, since those messages will always affect the document network. @@ -1924,6 +1926,9 @@ impl<'a> MessageHandler> for NodeG responses.add(NodeGraphMessage::SendGraph); } + NodeGraphMessage::UpdateThumbnail { node_id, graphic } => { + self.thumbnails.insert(node_id, graphic); + } NodeGraphMessage::UpdateTypes { resolved_types, node_graph_errors } => { network_interface.resolved_types.update(resolved_types); self.node_graph_errors = node_graph_errors; @@ -2559,6 +2564,7 @@ impl Default for NodeGraphMessageHandler { reordering_export: None, reordering_import: None, end_index: None, + thumbnails: HashMap::new(), } } } diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 8d609d34d2..19ffb07c59 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -29,10 +29,16 @@ pub struct ExecutionRequest { pub struct ExecutionResponse { execution_id: u64, result: Result, - responses: VecDeque, + execution_responses: Vec, vector_modify: HashMap, +} + +pub enum ExecutionResponseMessage { /// The resulting value from the temporary inspected during execution - inspect_result: Option, + InspectResult(Option), + UpdateNodeGraphThumbnail(NodeId, Graphic), + UpdateFrontendThumbnail(NodeId, String), + SendGraph, } #[derive(serde::Serialize, serde::Deserialize)] @@ -260,11 +266,24 @@ impl NodeGraphExecutor { let ExecutionResponse { execution_id, result, - responses: existing_responses, + execution_responses, vector_modify, - inspect_result, } = execution_response; - + for execution_response in execution_responses { + match execution_response { + ExecutionResponseMessage::InspectResult(inspect_result) => { + // Update the Data panel on the frontend using the value of the inspect result. + if let Some(inspect_result) = (self.previous_node_to_inspect.is_some()).then_some(inspect_result).flatten() { + responses.add(DataPanelMessage::UpdateLayout { inspect_result }); + } else { + responses.add(DataPanelMessage::ClearLayout); + } + } + ExecutionResponseMessage::UpdateNodeGraphThumbnail(node_id, graphic) => responses.add(NodeGraphMessage::UpdateThumbnail { node_id, graphic }), + ExecutionResponseMessage::UpdateFrontendThumbnail(node_id, string) => responses.add(FrontendMessage::UpdateNodeThumbnail { id: node_id, value: string }), + ExecutionResponseMessage::SendGraph => responses.add(NodeGraphMessage::SendGraph), + } + } responses.add(OverlaysMessage::Draw); let node_graph_output = match result { @@ -277,7 +296,6 @@ impl NodeGraphExecutor { } }; - responses.extend(existing_responses.into_iter().map(Into::into)); document.network_interface.update_vector_modify(vector_modify); let execution_context = self.futures.remove(&execution_id).ok_or_else(|| "Invalid generation ID".to_string())?; @@ -291,13 +309,6 @@ impl NodeGraphExecutor { execution_id, document_id: execution_context.document_id, }); - - // Update the Data panel on the frontend using the value of the inspect result. - if let Some(inspect_result) = (self.previous_node_to_inspect.is_some()).then_some(inspect_result).flatten() { - responses.add(DataPanelMessage::UpdateLayout { inspect_result }); - } else { - responses.add(DataPanelMessage::ClearLayout); - } } NodeGraphUpdate::CompilationResponse(execution_response) => { let CompilationResponse { node_graph_errors, result } = execution_response; diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index b89c5b2f25..ae72db8732 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -212,14 +212,14 @@ impl NodeRuntime { } GraphRuntimeRequest::ExecutionRequest(ExecutionRequest { execution_id, render_config, .. }) => { let result = self.execute_network(render_config).await; - let mut responses = VecDeque::new(); + let mut execution_responses = Vec::new(); // TODO: Only process monitor nodes if the graph has changed, not when only the Footprint changes - self.process_monitor_nodes(&mut responses, self.update_thumbnails); + self.process_monitor_nodes(&mut execution_responses, self.update_thumbnails); self.update_thumbnails = false; // Resolve the result from the inspection by accessing the monitor node let inspect_result = self.inspect_state.and_then(|state| state.access(&self.executor)); - + execution_responses.push(ExecutionResponseMessage::InspectResult(inspect_result)); let texture = if let Ok(TaggedValue::RenderOutput(RenderOutput { data: RenderOutputType::Texture(texture), .. @@ -233,9 +233,8 @@ impl NodeRuntime { self.sender.send_execution_response(ExecutionResponse { execution_id, result, - responses, + execution_responses, vector_modify: self.vector_modify.clone(), - inspect_result, }); return texture; } @@ -289,10 +288,10 @@ impl NodeRuntime { } /// Updates state data - pub fn process_monitor_nodes(&mut self, responses: &mut VecDeque, update_thumbnails: bool) { + pub fn process_monitor_nodes(&mut self, responses: &mut Vec, update_thumbnails: bool) { // TODO: Consider optimizing this since it's currently O(m*n^2), with a sort it could be made O(m * n*log(n)) self.thumbnail_renders.retain(|id, _| self.monitor_nodes.iter().any(|monitor_node_path| monitor_node_path.contains(id))); - + let mut updated_thumbnails = false; for monitor_node_path in &self.monitor_nodes { // Skip the inspect monitor node if self.inspect_state.is_some_and(|inspect_state| monitor_node_path.last().copied() == Some(inspect_state.monitor_node)) { @@ -316,13 +315,17 @@ impl NodeRuntime { // Graphic table: thumbnail 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) + Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses); + responses.push(ExecutionResponseMessage::UpdateNodeGraphThumbnail(parent_network_node_id, io.output.clone().to_graphic())); + updated_thumbnails = true; } } // 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) + Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses); + responses.push(ExecutionResponseMessage::UpdateNodeGraphThumbnail(parent_network_node_id, io.output.clone().to_graphic())); + updated_thumbnails = true; } } // Vector table: vector modifications @@ -337,18 +340,21 @@ impl NodeRuntime { log::warn!("Failed to downcast monitor node output {parent_network_node_id:?}"); } } + if updated_thumbnails { + responses.push(ExecutionResponseMessage::SendGraph); + } } /// 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) { + fn render_thumbnail(thumbnail_renders: &mut HashMap>, parent_network_node_id: NodeId, graphic: &impl Render, responses: &mut Vec) { // 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()); if old.is_none_or(|v| !v.is_empty()) { - responses.push_back(FrontendMessage::UpdateNodeThumbnail { - id: parent_network_node_id, - value: "Dense thumbnail omitted for performance".to_string(), - }); + responses.push(ExecutionResponseMessage::UpdateFrontendThumbnail( + parent_network_node_id, + "Dense thumbnail omitted for performance".to_string(), + )); } return; } @@ -382,10 +388,7 @@ impl NodeRuntime { let old_thumbnail_svg = thumbnail_renders.entry(parent_network_node_id).or_default(); if old_thumbnail_svg != &new_thumbnail_svg { - responses.push_back(FrontendMessage::UpdateNodeThumbnail { - id: parent_network_node_id, - value: new_thumbnail_svg.to_svg_string(), - }); + responses.push(ExecutionResponseMessage::UpdateFrontendThumbnail(parent_network_node_id, new_thumbnail_svg.to_svg_string())); *old_thumbnail_svg = new_thumbnail_svg; } } diff --git a/node-graph/gcore/src/node_graph_overlay.rs b/node-graph/gcore/src/node_graph_overlay.rs index 874332d638..ac234a7664 100644 --- a/node-graph/gcore/src/node_graph_overlay.rs +++ b/node-graph/gcore/src/node_graph_overlay.rs @@ -21,7 +21,7 @@ pub mod ui_context; #[node_macro::node(skip_impl)] pub fn generate_nodes(_: impl Ctx, mut node_graph_overlay_data: NodeGraphOverlayData) -> Table { let mut nodes_and_wires = Table::new(); - let (layers, side_ports) = draw_layers(&node_graph_overlay_data.nodes_to_render); + let (layers, side_ports) = draw_layers(&mut node_graph_overlay_data); nodes_and_wires.extend(layers); let wires = draw_wires(&mut node_graph_overlay_data.nodes_to_render); diff --git a/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs b/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs index 42d3d09c65..1e25b8a5cb 100644 --- a/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs +++ b/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs @@ -8,13 +8,14 @@ use crate::{ consts::SOURCE_SANS_FONT_DATA, node_graph_overlay::{ consts::*, - types::{FrontendGraphDataType, FrontendNodeToRender}, + types::{FrontendGraphDataType, FrontendNodeToRender, NodeGraphOverlayData}, }, table::{Table, TableRow}, text::{self, TextAlign, TypesettingConfig}, transform::ApplyTransform, vector::{ Vector, + style::{Fill, Stroke}, style::{Fill, Stroke, StrokeAlign}, }, }; @@ -211,10 +212,10 @@ pub fn draw_nodes(nodes: &Vec) -> Table { node_table } -pub fn draw_layers(nodes: &Vec) -> (Table, Table) { +pub fn draw_layers(nodes: &mut NodeGraphOverlayData) -> (Table, Table) { let mut layer_table = Table::new(); let mut side_ports_table = Table::new(); - for node_to_render in nodes { + for node_to_render in &nodes.nodes_to_render { if let Some(frontend_layer) = node_to_render.node_or_layer.layer.as_ref() { // The layer position is the top left of the thumbnail let layer_position = DVec2::new(frontend_layer.position.x as f64 * GRID_SIZE + 12., frontend_layer.position.y as f64 * GRID_SIZE); @@ -360,7 +361,7 @@ pub fn draw_layers(nodes: &Vec) -> (Table, Table< } let bottom_port = BezPath::from_svg("M0,0H8V8L5.479,6.319a2.666,2.666,0,0,0-2.959,0L0,8Z").unwrap(); let mut vector = Vector::from_bezpath(bottom_port); - let mut bottom_port_fill = if frontend_layer.bottom_input.connected_to_node.is_some() { + let bottom_port_fill = if frontend_layer.bottom_input.connected_to_node.is_some() { frontend_layer.bottom_input.data_type.data_color() } else { frontend_layer.bottom_input.data_type.data_color_dim() @@ -457,10 +458,34 @@ pub fn draw_layers(nodes: &Vec) -> (Table, Table< inner_thumbnail_table.push(TableRow::new_from_element(vector)); } } - let mut thumbnail_row = TableRow::new_from_element(Graphic::Vector(inner_thumbnail_table)); - thumbnail_row.alpha_blending.clip = true; - let graphic_table = Table::new_from_row(thumbnail_row); - layer_table.push(TableRow::new_from_element(Graphic::Graphic(graphic_table))); + let mut thumbnail_grid_row = TableRow::new_from_element(Graphic::Vector(inner_thumbnail_table)); + thumbnail_grid_row.alpha_blending.clip = true; + let mut clipped_thumbnail_table = Table::new(); + clipped_thumbnail_table.push(thumbnail_grid_row); + if let Some(thumbnail_graphic) = nodes.thumbnails.get_mut(&node_to_render.metadata.node_id) { + let thumbnail_graphic = std::mem::take(thumbnail_graphic); + let bbox = thumbnail_graphic.bounding_box(DAffine2::default(), false); + if let RenderBoundingBox::Rectangle(rect) = bbox { + let rect_size = rect[1] - rect[0]; + let target_size = DVec2::new(68., 44.); + // uniform scale that fits in target box + let scale_x = target_size.x / rect_size.x; + let scale_y = target_size.y / rect_size.y; + let scale = scale_x.min(scale_y); + + let translation = rect[0] * -scale; + let scaled_size = rect_size * scale; + let offset_to_center = (target_size - scaled_size) / 2.; + + let mut thumbnail_graphic_row = TableRow::new_from_element(thumbnail_graphic); + thumbnail_graphic_row.transform = DAffine2::from_translation(layer_position + offset_to_center) * DAffine2::from_scale_angle_translation(DVec2::splat(scale), 0., translation); + thumbnail_graphic_row.alpha_blending.clip = true; + + clipped_thumbnail_table.push(thumbnail_graphic_row); + } + } + + layer_table.push(TableRow::new_from_element(Graphic::Graphic(clipped_thumbnail_table))); } } diff --git a/node-graph/gcore/src/node_graph_overlay/types.rs b/node-graph/gcore/src/node_graph_overlay/types.rs index abf537b38b..8df7c0ace6 100644 --- a/node-graph/gcore/src/node_graph_overlay/types.rs +++ b/node-graph/gcore/src/node_graph_overlay/types.rs @@ -2,8 +2,11 @@ use glam::{DAffine2, DVec2}; use graphene_core_shaders::color::Color; use kurbo::BezPath; -use crate::{node_graph_overlay::consts::*, uuid::NodeId}; -use std::hash::{Hash, Hasher}; +use crate::{Graphic, node_graph_overlay::consts::*, uuid::NodeId}; +use std::{ + collections::HashMap, + hash::{Hash, Hasher}, +}; #[derive(Clone, Debug, Default, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)] pub struct NodeGraphTransform { @@ -27,13 +30,28 @@ impl NodeGraphTransform { } } -#[derive(Clone, Debug, Default, PartialEq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Default, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize)] pub struct NodeGraphOverlayData { pub nodes_to_render: Vec, pub open: bool, pub in_selected_network: bool, // Displays a dashed border around the node pub previewed_node: Option, + pub thumbnails: HashMap, +} + +impl Hash for NodeGraphOverlayData { + fn hash(&self, state: &mut H) { + self.nodes_to_render.hash(state); + self.open.hash(state); + self.in_selected_network.hash(state); + self.previewed_node.hash(state); + let mut entries: Vec<_> = self.thumbnails.iter().collect(); + entries.sort_by(|a, b| a.0.cmp(b.0)); + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + entries.hash(&mut hasher); + hasher.finish(); + } } #[derive(Clone, Debug, Default, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize)] diff --git a/node-graph/gsvg-renderer/src/renderer.rs b/node-graph/gsvg-renderer/src/renderer.rs index cb5bfd02a6..efeb98f151 100644 --- a/node-graph/gsvg-renderer/src/renderer.rs +++ b/node-graph/gsvg-renderer/src/renderer.rs @@ -9,6 +9,7 @@ use graphene_core::color::Color; use graphene_core::gradient::GradientStops; use graphene_core::gradient::GradientType; use graphene_core::math::quad::Quad; +use graphene_core::node_graph_overlay::consts::BEZ_PATH_TOLERANCE; use graphene_core::raster::BitmapMut; use graphene_core::raster::Image; use graphene_core::raster_types::{CPU, GPU, Raster}; @@ -23,6 +24,8 @@ use graphene_core::vector::click_target::{ClickTarget, FreePoint}; use graphene_core::vector::style::{Fill, PaintOrder, Stroke, StrokeAlign, ViewMode}; use graphene_core::{Artboard, Graphic}; use kurbo::Affine; +use kurbo::Rect; +use kurbo::Shape; use num_traits::Zero; use skrifa::MetadataProvider; use skrifa::attribute::Style; @@ -247,6 +250,8 @@ pub trait Render: BoundingBox + RenderComplexity { #[cfg(feature = "vello")] fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, _render_params: &RenderParams); + fn to_graphic(self) -> Graphic; + /// The upstream click targets for each layer are collected during the render so that they do not have to be calculated for each click detection. fn add_upstream_click_targets(&self, _click_targets: &mut Vec) {} @@ -289,6 +294,17 @@ impl Render for Graphic { } } + fn to_graphic(self) -> Graphic { + match self { + Graphic::Graphic(table) => table.to_graphic(), + Graphic::Vector(table) => table.to_graphic(), + Graphic::RasterCPU(table) => table.to_graphic(), + Graphic::RasterGPU(table) => table.to_graphic(), + Graphic::Color(table) => table.to_graphic(), + Graphic::Gradient(table) => table.to_graphic(), + } + } + fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option) { if let Some(element_id) = element_id { match self { @@ -467,6 +483,23 @@ impl Render for Artboard { } } + fn to_graphic(self) -> Graphic { + let bg = Rect::new( + self.location.x as f64, + self.location.y as f64, + self.location.x as f64 + self.dimensions.x as f64, + self.location.y as f64 + self.dimensions.y as f64, + ); + let mut bg_vector = Vector::from_bezpath(bg.to_path(BEZ_PATH_TOLERANCE)); + bg_vector.style.fill = Fill::Solid(self.background); + let mut graphic_table = Table::new(); + graphic_table.push(TableRow::new_from_element(Graphic::Graphic(Table::new_from_element(Graphic::Vector(Table::new_from_element( + bg_vector, + )))))); + graphic_table.push(TableRow::new_from_element(Graphic::Graphic(self.content))); + Graphic::Graphic(graphic_table) + } + fn collect_metadata(&self, metadata: &mut RenderMetadata, mut footprint: Footprint, element_id: Option) { if let Some(element_id) = element_id { let subpath = Subpath::new_rect(DVec2::ZERO, self.dimensions.as_dvec2()); @@ -505,6 +538,20 @@ impl Render for Table { } } + fn to_graphic(self) -> Graphic { + let mut graphic_table = Table::new(); + for item in self.into_iter() { + let graphic = item.element.to_graphic(); + let graphic_row = TableRow { + element: graphic, + transform: item.transform, + alpha_blending: item.alpha_blending, + source_node_id: item.source_node_id, + }; + graphic_table.push(graphic_row); + } + Graphic::Graphic(graphic_table) + } fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, _element_id: Option) { for row in self.iter() { row.element.collect_metadata(metadata, footprint, *row.source_node_id); @@ -643,6 +690,10 @@ impl Render for Table { } } + fn to_graphic(self) -> Graphic { + Graphic::Graphic(self) + } + fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option) { for row in self.iter() { if let Some(element_id) = row.source_node_id { @@ -1141,6 +1192,10 @@ impl Render for Table { } } + fn to_graphic(self) -> Graphic { + Graphic::Vector(self) + } + fn add_upstream_click_targets(&self, click_targets: &mut Vec) { for row in self.iter() { let stroke_width = row.element.style.stroke().as_ref().map_or(0., Stroke::effective_width); @@ -1318,6 +1373,10 @@ impl Render for Table> { } } + fn to_graphic(self) -> Graphic { + Graphic::RasterCPU(self) + } + fn add_upstream_click_targets(&self, click_targets: &mut Vec) { let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE); click_targets.push(ClickTarget::new_with_subpath(subpath, 0.)); @@ -1364,6 +1423,10 @@ impl Render for Table> { } } + fn to_graphic(self) -> Graphic { + Graphic::RasterGPU(self) + } + fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option) { let Some(element_id) = element_id else { return }; let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE); @@ -1444,6 +1507,10 @@ impl Render for Table { } } } + + fn to_graphic(self) -> Graphic { + Graphic::Color(self) + } } impl Render for Table { @@ -1544,6 +1611,10 @@ impl Render for Table { } } } + + fn to_graphic(self) -> Graphic { + Graphic::Gradient(self) + } } impl Render for Table {