From b47b3b72cbfe6c91e81f5d644b13979a9dd3d110 Mon Sep 17 00:00:00 2001 From: Adam Date: Wed, 3 Sep 2025 11:27:53 -0700 Subject: [PATCH] Improve native UI network --- .../src/messages/frontend/frontend_message.rs | 18 +- .../document/document_message_handler.rs | 4 +- .../node_graph/generate_node_graph_overlay.rs | 165 ++++++++++------- .../node_graph/node_graph_message_handler.rs | 7 +- .../document/node_graph/utility_types.rs | 7 - editor/src/test_utils.rs | 2 +- frontend/src/components/Editor.svelte | 1 + frontend/src/messages.ts | 10 +- frontend/src/state-providers/node-graph.ts | 27 ++- .../gcore-shaders/src/color/color_types.rs | 21 +++ node-graph/gcore/src/node_graph_overlay.rs | 60 ++++-- .../gcore/src/node_graph_overlay/consts.rs | 42 +++++ .../src/node_graph_overlay/nodes_and_wires.rs | 175 ++++++++++++++++++ .../gcore/src/node_graph_overlay/types.rs | 84 +++++++-- .../src/node_graph_overlay/ui_context.rs | 26 +++ node-graph/graph-craft/src/proto.rs | 2 +- node-graph/gstd/src/wasm_application_io.rs | 23 +++ .../interpreted-executor/src/node_registry.rs | 8 + 18 files changed, 553 insertions(+), 129 deletions(-) create mode 100644 node-graph/gcore/src/node_graph_overlay/consts.rs create mode 100644 node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs create mode 100644 node-graph/gcore/src/node_graph_overlay/ui_context.rs diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 878d6c2ebc..8538930f8c 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -1,13 +1,13 @@ 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, FrontendNodeType, Transform}; +use crate::messages::portfolio::document::node_graph::utility_types::{BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendNodeType}; use crate::messages::portfolio::document::utility_types::nodes::{JsRawBuffer, LayerPanelEntry, RawBuffer}; use crate::messages::portfolio::document::utility_types::wires::WirePathInProgress; use crate::messages::prelude::*; use crate::messages::tool::utility_types::HintData; use graph_craft::document::NodeId; -use graphene_std::node_graph_overlay::types::{FrontendExports, FrontendImport, FrontendNodeToRender, FrontendXY}; +use graphene_std::node_graph_overlay::types::{FrontendExports, FrontendImport, FrontendNodeToRender, FrontendXY, NodeGraphTransform}; use graphene_std::raster::Image; use graphene_std::raster::color::Color; use graphene_std::text::{Font, TextAlign}; @@ -269,7 +269,7 @@ pub enum FrontendMessage { UpdateMouseCursor { cursor: MouseCursorIcon, }, - UpdateNodeGraphRender { + UpdateNodeGraphSvelteRender { #[serde(rename = "nodesToRender")] nodes_to_render: Vec, open: bool, @@ -280,9 +280,13 @@ pub enum FrontendMessage { #[serde(rename = "previewedNode")] previewed_node: Option, }, - UpdateNativeNodeGraphRender { - #[serde(rename = "nativeNodeGraphRender")] - native_node_graph_render: bool, + UpdateShouldRenderSvelteNodes { + #[serde(rename = "shouldRenderSvelteNodes")] + should_render_svelte_nodes: bool, + }, + UpdateNativeNodeGraphSVG { + #[serde(rename = "svgString")] + svg_string: String, }, UpdateVisibleNodes { nodes: Vec, @@ -293,7 +297,7 @@ pub enum FrontendMessage { diff: Vec, }, UpdateNodeGraphTransform { - transform: Transform, + transform: NodeGraphTransform, }, UpdateNodeThumbnail { id: NodeId, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 332efc463a..5e4d64a6dc 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1,5 +1,4 @@ use super::node_graph::document_node_definitions; -use super::node_graph::utility_types::Transform; use super::overlays::utility_types::Pivot; use super::utility_types::error::EditorError; use super::utility_types::misc::{GroupFolderType, SNAP_FUNCTIONS_FOR_BOUNDING_BOXES, SNAP_FUNCTIONS_FOR_PATHS, SnappingOptions, SnappingState}; @@ -31,6 +30,7 @@ use glam::{DAffine2, DVec2, IVec2}; use graph_craft::document::value::TaggedValue; use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork}; use graphene_std::math::quad::Quad; +use graphene_std::node_graph_overlay::types::NodeGraphTransform; use graphene_std::path_bool::{boolean_intersect, path_bool_lib}; use graphene_std::raster::BlendMode; use graphene_std::raster_types::Raster; @@ -1494,7 +1494,7 @@ impl MessageHandler> for DocumentMes responses.add(NodeGraphMessage::UpdateImportsExports); responses.add(FrontendMessage::UpdateNodeGraphTransform { - transform: Transform { + transform: NodeGraphTransform { scale: transform.matrix2.x_axis.x, x: transform.translation.x, y: transform.translation.y, diff --git a/editor/src/messages/portfolio/document/node_graph/generate_node_graph_overlay.rs b/editor/src/messages/portfolio/document/node_graph/generate_node_graph_overlay.rs index 6d2decf3d8..d3d7a9f237 100644 --- a/editor/src/messages/portfolio/document/node_graph/generate_node_graph_overlay.rs +++ b/editor/src/messages/portfolio/document/node_graph/generate_node_graph_overlay.rs @@ -3,108 +3,133 @@ use graph_craft::{ document::{DocumentNode, DocumentNodeImplementation, NodeInput, NodeNetwork, value::TaggedValue}, }; use graphene_std::{ - Context, graphic, memo, - node_graph_overlay::{self, types::NodeGraphOverlayData}, + node_graph_overlay::{types::NodeGraphOverlayData, ui_context::UIContext}, + table::Table, uuid::NodeId, }; +/// https://excalidraw.com/#json=LgKS6I4lQvGPmke06ZJyp,D9aON9vVZJAjNnZWfwy_SQ pub fn generate_node_graph_overlay(node_graph_overlay_data: NodeGraphOverlayData, opacity: f64) -> DocumentNode { - // TODO: Implement as Network and implement finer grained caching for the background, nodes, and exports + let generate_nodes_id = NodeId::new(); + let cache_nodes_id = NodeId::new(); + let transform_nodes_id = NodeId::new(); + + let generate_node_graph_bg = NodeId::new(); + let cache_node_graph_bg = NodeId::new(); + + let merge_nodes_and_bg_id = NodeId::new(); + let render_overlay_id = NodeId::new(); + let send_overlay_id = NodeId::new(); + let _cache_output_id = NodeId::new(); + // TODO: Replace with new cache node + let memo_implementation = DocumentNodeImplementation::ProtoNode(graphene_std::ops::identity::IDENTIFIER); + DocumentNode { inputs: vec![ - NodeInput::value(TaggedValue::None, true), + NodeInput::value(TaggedValue::Vector(Table::new()), true), NodeInput::value(TaggedValue::NodeGraphOverlayData(node_graph_overlay_data), true), NodeInput::value(TaggedValue::F64(opacity), true), ], + implementation: DocumentNodeImplementation::Network(NodeNetwork { - exports: vec![NodeInput::node(NodeId(0), 0)], + exports: vec![NodeInput::node(send_overlay_id, 0)], nodes: vec![ - // Merge the overlay on top of the artwork + // Create the nodes ( - NodeId(0), + generate_nodes_id, DocumentNode { - call_argument: concrete!(Context), - inputs: vec![NodeInput::node(NodeId(2), 0), NodeInput::node(NodeId(1), 0)], - implementation: DocumentNodeImplementation::ProtoNode(graphic::extend::IDENTIFIER), - ..Default::default() - }, - ), - //Wrap Artwork in a table - ( - NodeId(1), - DocumentNode { - inputs: vec![NodeInput::network(concrete!(Context), 0)], - implementation: DocumentNodeImplementation::ProtoNode(graphic::wrap_graphic::IDENTIFIER), - call_argument: concrete!(Context), - ..Default::default() - }, - ), - // Cache the full node graph so its not rerendered when the artwork changes - ( - NodeId(2), - DocumentNode { - call_argument: concrete!(Context), - inputs: vec![NodeInput::node(NodeId(3), 0)], - implementation: DocumentNodeImplementation::ProtoNode(memo::memo::IDENTIFIER), - ..Default::default() - }, - ), - // Merge the nodes on top of the dot grid background - ( - NodeId(3), - DocumentNode { - call_argument: concrete!(Context), - inputs: vec![NodeInput::node(NodeId(5), 0), NodeInput::node(NodeId(4), 0)], - implementation: DocumentNodeImplementation::ProtoNode(graphic::extend::IDENTIFIER), - ..Default::default() - }, - ), - // Generate the dot grid background - ( - NodeId(4), - DocumentNode { - inputs: vec![NodeInput::network(concrete!(Context), 2)], - implementation: DocumentNodeImplementation::ProtoNode(node_graph_overlay::dot_grid_background::IDENTIFIER), - call_argument: concrete!(Context), - ..Default::default() - }, - ), - // Transform the nodes based on the Context - ( - NodeId(5), - DocumentNode { - call_argument: concrete!(Context), - inputs: vec![NodeInput::node(NodeId(6), 0)], - implementation: DocumentNodeImplementation::ProtoNode(node_graph_overlay::transform_nodes::IDENTIFIER), + call_argument: concrete!(UIContext), + inputs: vec![NodeInput::network(concrete!(UIContext), 1)], + implementation: DocumentNodeImplementation::ProtoNode("graphene_core::node_graph_overlay::GenerateNodesNode".into()), ..Default::default() }, ), // Cache the nodes ( - NodeId(6), + cache_nodes_id, DocumentNode { - call_argument: concrete!(Context), - inputs: vec![NodeInput::node(NodeId(7), 0)], - implementation: DocumentNodeImplementation::ProtoNode(memo::memo::IDENTIFIER), + call_argument: concrete!(UIContext), + inputs: vec![NodeInput::node(generate_nodes_id, 0)], + implementation: memo_implementation.clone(), ..Default::default() }, ), - // Create the nodes + // Transform the nodes based on the Context ( - NodeId(7), + transform_nodes_id, DocumentNode { - call_argument: concrete!(Context), - inputs: vec![NodeInput::network(concrete!(Context), 1)], - implementation: DocumentNodeImplementation::ProtoNode(node_graph_overlay::generate_nodes::IDENTIFIER), + call_argument: concrete!(UIContext), + inputs: vec![NodeInput::node(cache_nodes_id, 0)], + implementation: DocumentNodeImplementation::ProtoNode("graphene_core::node_graph_overlay::TransformNodesNode".into()), ..Default::default() }, ), + // Generate the dot grid background + ( + generate_node_graph_bg, + DocumentNode { + inputs: vec![NodeInput::network(concrete!(UIContext), 2)], + implementation: DocumentNodeImplementation::ProtoNode("graphene_core::node_graph_overlay::DotGridBackgroundNode".into()), + call_argument: concrete!(UIContext), + ..Default::default() + }, + ), + // Cache the dot grid background + ( + cache_node_graph_bg, + DocumentNode { + call_argument: concrete!(UIContext), + inputs: vec![NodeInput::node(generate_node_graph_bg, 0)], + implementation: memo_implementation.clone(), + ..Default::default() + }, + ), + // Merge the nodes on top of the dot grid background + ( + merge_nodes_and_bg_id, + DocumentNode { + call_argument: concrete!(UIContext), + inputs: vec![NodeInput::node(transform_nodes_id, 0), NodeInput::node(cache_node_graph_bg, 0)], + implementation: DocumentNodeImplementation::ProtoNode("graphene_core::node_graph_overlay::NodeGraphUiExtendNode".into()), + ..Default::default() + }, + ), + // Render the node graph UI graphic to an SVG + ( + render_overlay_id, + DocumentNode { + call_argument: concrete!(UIContext), + inputs: vec![NodeInput::node(merge_nodes_and_bg_id, 0)], + implementation: DocumentNodeImplementation::ProtoNode("graphene_std::wasm_application_io::RenderNodeGraphUiNode".into()), + ..Default::default() + }, + ), + // Send the overlay to the frontend + ( + send_overlay_id, + DocumentNode { + call_argument: concrete!(UIContext), + inputs: vec![NodeInput::node(render_overlay_id, 0)], + implementation: DocumentNodeImplementation::ProtoNode("graphene_core::node_graph_overlay::SendRenderNode".into()), + ..Default::default() + }, + ), + // Cache the full node graph so its not rerendered when the artwork changes + // ( + // cache_nodes_id, + // DocumentNode { + // call_argument: concrete!(UIContext), + // inputs: vec![NodeInput::node(send_overlay_id, 0)], + // implementation: memo_implementation.clone(), + // ..Default::default() + // }, + // ), ] .into_iter() .collect(), ..Default::default() }), - call_argument: concrete!(Context), + call_argument: concrete!(UIContext), ..Default::default() } } 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 f0bdde5913..d2e2996f17 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 @@ -21,7 +21,7 @@ use crate::messages::tool::common_functionality::utility_functions::make_path_ed 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::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput}; +use graph_craft::document::{DocumentNodeImplementation, NodeId, NodeInput}; use graph_craft::proto::GraphErrors; use graphene_std::math::math_ext::QuadExt; use graphene_std::node_graph_overlay::types::{FrontendGraphDataType, FrontendXY}; @@ -1795,8 +1795,11 @@ impl<'a> MessageHandler> for NodeG } NodeGraphMessage::ToggleNativeNodeGraphRender => { self.native_node_graph_render = !self.native_node_graph_render; - self.node_graph_overlay = None; + responses.add(FrontendMessage::UpdateShouldRenderSvelteNodes { + should_render_svelte_nodes: self.native_node_graph_render, + }); responses.add(NodeGraphMessage::SendGraph); + responses.add(MenuBarMessage::SendLayout); } NodeGraphMessage::ToggleSelectedLocked => { let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else { 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 9611c79f8e..5e46218f19 100644 --- a/editor/src/messages/portfolio/document/node_graph/utility_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/utility_types.rs @@ -214,13 +214,6 @@ pub struct DragStart { pub round_y: i32, } -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] -pub struct Transform { - pub scale: f64, - pub x: f64, - pub y: f64, -} - #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] pub struct BoxSelection { #[serde(rename = "startX")] diff --git a/editor/src/test_utils.rs b/editor/src/test_utils.rs index f917a8b003..9bbb022da4 100644 --- a/editor/src/test_utils.rs +++ b/editor/src/test_utils.rs @@ -301,7 +301,7 @@ pub trait FrontendMessageTestUtils { impl FrontendMessageTestUtils for FrontendMessage { fn check_node_graph_error(&self) { - let FrontendMessage::UpdateNodeGraphRender { nodes_to_render, .. } = self else { return }; + let FrontendMessage::UpdateNodeGraphSvelteRender { nodes_to_render, .. } = self else { return }; for node in nodes_to_render { if let Some(error) = &node.metadata.errors { diff --git a/frontend/src/components/Editor.svelte b/frontend/src/components/Editor.svelte index 9a965338a2..ce1db2ed3f 100644 --- a/frontend/src/components/Editor.svelte +++ b/frontend/src/components/Editor.svelte @@ -74,6 +74,7 @@ // Replace usage of `-rgb` variants with CSS color() function to calculate alpha when browsers support it // See https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/color() and https://caniuse.com/css-color-function // Specifically, support for the relative syntax is needed: `color(from var(--color-0-black) srgb r g b / 0.5)` to convert black to 50% alpha + // Keep in sync with node_graph_overlay/consts.rs --color-0-black: #000; --color-0-black-rgb: 0, 0, 0; --color-1-nearblack: #111; diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index 618d14b150..0f656232d2 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -106,7 +106,7 @@ export class UpdateLayerWidths extends JsMessage { readonly layerWidths!: Map; } -export class UpdateNodeGraphRender extends JsMessage { +export class UpdateNodeGraphSvelteRender extends JsMessage { readonly nodesToRender!: FrontendNodeToRender[]; readonly open!: boolean; @@ -118,8 +118,12 @@ export class UpdateNodeGraphRender extends JsMessage { readonly previewedNode!: bigint | undefined; } -export class UpdateNativeNodeGraphRender extends JsMessage { - readonly nativeNodeGraphRender!: boolean; +export class UpdateShouldRenderSvelteNodes extends JsMessage { + readonly shouldRenderSvelteNodes!: boolean; +} + +export class UpdateNativeNodeGraphSVG extends JsMessage { + readonly svgString!: string; } export class UpdateVisibleNodes extends JsMessage { diff --git a/frontend/src/state-providers/node-graph.ts b/frontend/src/state-providers/node-graph.ts index 74a3584b84..7b6f3fc173 100644 --- a/frontend/src/state-providers/node-graph.ts +++ b/frontend/src/state-providers/node-graph.ts @@ -15,8 +15,9 @@ import { UpdateExportReorderIndex, UpdateImportsExports, UpdateLayerWidths, - UpdateNodeGraphRender, - UpdateNativeNodeGraphRender, + UpdateNodeGraphSvelteRender, + UpdateShouldRenderSvelteNodes, + UpdateNativeNodeGraphSVG, UpdateVisibleNodes, UpdateNodeGraphTransform, UpdateNodeThumbnail, @@ -33,6 +34,7 @@ export function createNodeGraphState(editor: Editor) { updateImportsExports: undefined as UpdateImportsExports | undefined, reorderImportIndex: undefined as number | undefined, reorderExportIndex: undefined as number | undefined, + nativeNodeGraphSVGString: "", contextMenuInformation: undefined as ContextMenuInformation | undefined, nodeTypes: [] as FrontendNodeType[], @@ -47,6 +49,7 @@ export function createNodeGraphState(editor: Editor) { // TODO: Remove these fields visibleNodes: new Set(), layerWidths: new Map(), + shouldRenderSvelteNodes: false, // Data that will be passed in the context thumbnails: new Map(), @@ -110,21 +113,27 @@ export function createNodeGraphState(editor: Editor) { return state; }); }); - editor.subscriptions.subscribeJsMessage(UpdateNodeGraphRender, (updateNodeGraphRender) => { + editor.subscriptions.subscribeJsMessage(UpdateNodeGraphSvelteRender, (updateNodeGraphSvelteRender) => { update((state) => { state.nodesToRender.clear(); - updateNodeGraphRender.nodesToRender.forEach((node) => { + updateNodeGraphSvelteRender.nodesToRender.forEach((node) => { state.nodesToRender.set(node.metadata.nodeId, node); }); - state.opacity = updateNodeGraphRender.opacity; - state.inSelectedNetwork = updateNodeGraphRender.inSelectedNetwork; - state.previewedNode = updateNodeGraphRender.previewedNode; + state.opacity = updateNodeGraphSvelteRender.opacity; + state.inSelectedNetwork = updateNodeGraphSvelteRender.inSelectedNetwork; + state.previewedNode = updateNodeGraphSvelteRender.previewedNode; return state; }); }); - editor.subscriptions.subscribeJsMessage(UpdateNativeNodeGraphRender, (updateNativeNodeGraphRender) => { + editor.subscriptions.subscribeJsMessage(UpdateShouldRenderSvelteNodes, (UpdateShouldRenderSvelteNodes) => { update((state) => { - state.nativeNodeGraphRender = updateNativeNodeGraphRender.nativeNodeGraphRender; + state.shouldRenderSvelteNodes = UpdateShouldRenderSvelteNodes.shouldRenderSvelteNodes; + return state; + }); + }); + editor.subscriptions.subscribeJsMessage(UpdateNativeNodeGraphSVG, (updateNativeNodeGraphRender) => { + update((state) => { + state.nativeNodeGraphSVGString = updateNativeNodeGraphRender.svgString; return state; }); }); diff --git a/node-graph/gcore-shaders/src/color/color_types.rs b/node-graph/gcore-shaders/src/color/color_types.rs index 5127a0c07a..b965071053 100644 --- a/node-graph/gcore-shaders/src/color/color_types.rs +++ b/node-graph/gcore-shaders/src/color/color_types.rs @@ -427,6 +427,16 @@ impl Color { Color { red, green, blue, alpha }.to_linear_srgb().map_rgb(|channel| channel * alpha) } + pub fn from_rgba8(red: u8, green: u8, blue: u8, alpha: u8) -> Color { + let map_range = |int_color| int_color as f32 / 255.; + + let red = map_range(red); + let green = map_range(green); + let blue = map_range(blue); + let alpha = map_range(alpha); + Color { red, green, blue, alpha } + } + /// Create a [Color] from a hue, saturation, lightness and alpha (all between 0 and 1) /// /// # Examples @@ -940,6 +950,17 @@ impl Color { Some(Color::from_rgb8_srgb(r, g, b)) } + pub fn from_rgba8_no_srgb(color_str: &str) -> Option { + if color_str.len() != 6 { + return None; + } + let r = u8::from_str_radix(&color_str[0..2], 16).ok()?; + let g = u8::from_str_radix(&color_str[2..4], 16).ok()?; + let b = u8::from_str_radix(&color_str[4..6], 16).ok()?; + let a = 255; + Some(Color::from_rgba8(r, g, b, a)) + } + /// Linearly interpolates between two colors based on t. /// /// T must be between 0 and 1. diff --git a/node-graph/gcore/src/node_graph_overlay.rs b/node-graph/gcore/src/node_graph_overlay.rs index 01d6e8a32e..1e878a1491 100644 --- a/node-graph/gcore/src/node_graph_overlay.rs +++ b/node-graph/gcore/src/node_graph_overlay.rs @@ -1,38 +1,55 @@ use graphene_core_shaders::{Ctx, color::Color}; use kurbo::{BezPath, Point}; -use crate::{ExtractFootprint, table::Table, vector::Vector}; +use crate::{ + node_graph_overlay::{ + nodes_and_wires::{draw_layers, draw_nodes}, + types::NodeGraphOverlayData, + ui_context::{UIContext, UIRuntimeResponse}, + }, + table::Table, + transform::ApplyTransform, + vector::Vector, +}; +pub mod consts; +pub mod nodes_and_wires; pub mod types; +pub mod ui_context; -#[node_macro::node(category(""))] -pub fn generate_nodes(_: impl Ctx, _node_graph_overlay_data: types::NodeGraphOverlayData) -> Table { - Table::new() +#[node_macro::node(skip_impl)] +pub fn generate_nodes(_: impl Ctx, node_graph_overlay_data: NodeGraphOverlayData) -> Table { + let mut nodes_and_wires = Table::new(); + let layers = draw_layers(&node_graph_overlay_data.nodes_to_render); + nodes_and_wires.extend(layers); + + let nodes = draw_nodes(&node_graph_overlay_data.nodes_to_render); + nodes_and_wires.extend(nodes); + + nodes_and_wires } -#[node_macro::node(category(""))] -pub fn transform_nodes(_ctx: impl Ctx + ExtractFootprint, nodes: Table) -> Table { +#[node_macro::node(skip_impl)] +pub fn transform_nodes(ui_context: UIContext, mut nodes: Table) -> Table { + let matrix = ui_context.transform.to_daffine2(); + nodes.apply_transform(&matrix); nodes } -#[node_macro::node(category(""))] -pub fn dot_grid_background(ctx: impl Ctx + ExtractFootprint, opacity: f64) -> Table { - let Some(footprint) = ctx.try_footprint() else { - log::error!("Could not get footprint from context in dot_grid_background"); - return Table::new(); - }; +#[node_macro::node(skip_impl)] +pub fn dot_grid_background(ui_context: UIContext, opacity: f64) -> Table { // From --color-2-mildblack: --color-2-mildblack-rgb: 34, 34, 34; let gray = (34. / 255.) as f32; - let Some(bg_color) = Color::from_rgbaf32(gray, gray, gray, opacity as f32) else { + let Some(bg_color) = Color::from_rgbaf32(gray, gray, gray, (opacity / 100.) as f32) else { log::error!("Could not create color in dot grid background"); return Table::new(); }; let mut bez_path = BezPath::new(); let p0 = Point::new(0., 0.); // bottom-left - let p1 = Point::new(footprint.resolution.x as f64, 0.); // bottom-right - let p2 = Point::new(footprint.resolution.x as f64, footprint.resolution.y as f64); // top-right - let p3 = Point::new(0., footprint.resolution.y as f64); // top-left + let p1 = Point::new(ui_context.resolution.x as f64, 0.); // bottom-right + let p2 = Point::new(ui_context.resolution.x as f64, ui_context.resolution.y as f64); // top-right + let p3 = Point::new(0., ui_context.resolution.y as f64); // top-left bez_path.move_to(p0); bez_path.line_to(p1); @@ -45,3 +62,14 @@ pub fn dot_grid_background(ctx: impl Ctx + ExtractFootprint, opacity: f64) -> Ta Table::new_from_element(vector) } + +#[node_macro::node(skip_impl)] +pub fn node_graph_ui_extend(_: impl Ctx, new: Table, mut base: Table) -> Table { + base.extend(new); + base +} + +#[node_macro::node(skip_impl)] +pub fn send_render(ui_context: UIContext, render: String) -> () { + let _ = ui_context.response_sender.send(UIRuntimeResponse::OverlaySVG(render)); +} diff --git a/node-graph/gcore/src/node_graph_overlay/consts.rs b/node-graph/gcore/src/node_graph_overlay/consts.rs new file mode 100644 index 0000000000..220d260ee8 --- /dev/null +++ b/node-graph/gcore/src/node_graph_overlay/consts.rs @@ -0,0 +1,42 @@ +pub const GRID_SIZE: f64 = 24.; +pub const BEZ_PATH_TOLERANCE: f64 = 0.1; + +// Keep in sync with colors in Editor.svelte +pub const COLOR_0_BLACK: &str = "000000"; +pub const COLOR_1_NEARBLACK: &str = "111111"; +pub const COLOR_2_MILDBLACK: &str = "222222"; +pub const COLOR_3_DARKGRAY: &str = "333333"; +pub const COLOR_4_DIMGRAY: &str = "444444"; +pub const COLOR_5_DULLGRAY: &str = "555555"; +pub const COLOR_6_LOWERGRAY: &str = "666666"; +pub const COLOR_7_MIDDLEGRAY: &str = "777777"; +pub const COLOR_8_UPPERGRAY: &str = "888888"; +pub const COLOR_9_PALEGRAY: &str = "999999"; +pub const COLOR_A_SOFTGRAY: &str = "AAAAAA"; +pub const COLOR_B_LIGHTGRAY: &str = "BBBBBB"; +pub const COLOR_C_BRIGHTGRAY: &str = "CCCCCC"; +pub const COLOR_D_MILDWHITE: &str = "DDDDDD"; +pub const COLOR_E_NEARWHITE: &str = "EEEEEE"; +pub const COLOR_F_WHITE: &str = "FFFFFF"; + +pub const COLOR_ERROR_RED: &str = "D6536E"; +pub const COLOR_WARNING_YELLOW: &str = "D5AA43"; + +pub const COLOR_DATA_GENERAL: &str = "CFCFCF"; +pub const COLOR_DATA_GENERAL_DIM: &str = "8A8A8A"; +pub const COLOR_DATA_NUMBER: &str = "C9A699"; +pub const COLOR_DATA_NUMBER_DIM: &str = "886B60"; +pub const COLOR_DATA_ARTBOARD: &str = "FBF9EB"; +pub const COLOR_DATA_ARTBOARD_DIM: &str = "B9B9A9"; +pub const COLOR_DATA_GRAPHIC: &str = "68C587"; +pub const COLOR_DATA_GRAPHIC_DIM: &str = "37754C"; +pub const COLOR_DATA_RASTER: &str = "E4BB72"; +pub const COLOR_DATA_RASTER_DIM: &str = "9A7B43"; +pub const COLOR_DATA_VECTOR: &str = "65BBE5"; +pub const COLOR_DATA_VECTOR_DIM: &str = "417892"; +pub const COLOR_DATA_COLOR: &str = "CE6EA7"; +pub const COLOR_DATA_COLOR_DIM: &str = "924071"; +pub const COLOR_DATA_GRADIENT: &str = "AF81EB"; +pub const COLOR_DATA_GRADIENT_DIM: &str = "6C489B"; +pub const COLOR_DATA_TYPOGRAPHY: &str = "EEA7A7"; +pub const COLOR_DATA_TYPOGRAPHY_DIM: &str = "955252"; 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 new file mode 100644 index 0000000000..494e782f40 --- /dev/null +++ b/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs @@ -0,0 +1,175 @@ +use graphene_core_shaders::color::{AlphaMut, Color}; +use kurbo::{BezPath, RoundedRect, Shape}; + +use crate::{ + node_graph_overlay::{ + consts::*, + types::{FrontendGraphDataType, FrontendNodeToRender}, + }, + table::{Table, TableRow}, + vector::{Vector, style::Fill}, +}; + +pub fn draw_nodes(nodes: &Vec) -> Table { + let mut node_table = Table::new(); + for node_to_render in nodes { + if let Some(frontend_node) = node_to_render.node_or_layer.node.as_ref() { + let x = frontend_node.position.x as f64 * GRID_SIZE; + let y = frontend_node.position.y as f64 * GRID_SIZE + GRID_SIZE / 2.; + let w = GRID_SIZE * 5.0; + let number_of_exposed_inputs = frontend_node.inputs.iter().skip(1).filter(|x| x.is_some()).count(); + let height = 1 + number_of_exposed_inputs; + let h = height as f64 * GRID_SIZE; + + let border_rect = RoundedRect::new(x, y, x + w, y + h, 2.); + let bez_path = border_rect.to_path(BEZ_PATH_TOLERANCE); + let mut border_vector = Vector::from_bezpath(bez_path); + let primary_output_color = frontend_node.outputs[0] + .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 node_color = if node_to_render.metadata.selected { + let mut selection_color = Color::from_rgba8_no_srgb(COLOR_F_WHITE).unwrap(); + selection_color.set_alpha(0.15); + selection_color + } else { + let mut bg_color = Color::from_rgba8_no_srgb(COLOR_0_BLACK).unwrap(); + bg_color.set_alpha(0.33); + bg_color + }; + border_vector.style.fill = crate::vector::style::Fill::Solid(node_color); + + // Make primary input brighter + if number_of_exposed_inputs == 0 { + // Draw the first row with rounded bottom corners + node_table.push(TableRow::new_from_element(node_first_row(x, y, true))); + } else { + // Draw the first row without rounded bottom corners + node_table.push(TableRow::new_from_element(node_first_row(x, y, false))); + // for node_index in 0..(number_of_exposed_inputs - 1) { + // node_table.push(TableRow::new_from_element(node_secondary_row(x, y, node_index + 1, false))); + // } + // // Draw the last row with bottom corners + // node_table.push(TableRow::new_from_element(node_secondary_row(x, y, number_of_exposed_inputs, true))); + }; + + node_table.push(TableRow::new_from_element(border_vector)); + } + } + node_table +} + +pub fn draw_layers(nodes: &Vec) -> Table { + let mut layer_table = Table::new(); + for node_to_render in nodes { + if let Some(frontend_layer) = node_to_render.node_or_layer.layer.as_ref() { + let chain_width = if frontend_layer.chain_width > 0 { + frontend_layer.chain_width as f64 * GRID_SIZE + 0.5 * GRID_SIZE + } else { + 0. + }; + + let x0 = frontend_layer.position.x as f64 * GRID_SIZE - chain_width + 0.5 * GRID_SIZE; + let y0 = frontend_layer.position.y as f64 * GRID_SIZE; + let h = 2. * GRID_SIZE; + let w = chain_width + 8. * GRID_SIZE - 0.5 * GRID_SIZE; + + let rect = RoundedRect::new(x0, y0, x0 + w, y0 + h, 8.); + let bez_path = rect.to_path(BEZ_PATH_TOLERANCE); + let mut vector = Vector::from_bezpath(bez_path); + let border_color = Color::from_rgba8_no_srgb(COLOR_5_DULLGRAY).unwrap(); + vector.style.stroke = Some(crate::vector::style::Stroke::new(Some(border_color), 1.)); + let mut background = if node_to_render.metadata.selected { + Color::from_rgba8_no_srgb(COLOR_6_LOWERGRAY).unwrap() + } else { + Color::from_rgba8_no_srgb(COLOR_0_BLACK).unwrap() + }; + background.set_alpha(0.33); + vector.style.fill = crate::vector::style::Fill::Solid(background); + layer_table.push(TableRow::new_from_element(vector)); + } + } + layer_table +} + +fn node_first_row(x0: f64, y0: f64, rounded_bottom: bool) -> Vector { + let x1 = x0 + GRID_SIZE * 5.; + let y1 = y0 + GRID_SIZE; + let r = 2.; + + let bez_path = if rounded_bottom { + let mut path = BezPath::new(); + // Start at bottom-left + path.move_to((x0, y1)); + + // Left side up + path.line_to((x0, y0 + r)); + + // Top-left corner arc + path.quad_to((x0, y0), (x0 + r, y0)); + + // Top edge + path.line_to((x1 - r, y0)); + + // Top-right corner arc + path.quad_to((x1, y0), (x1, y0 + r)); + + // Right side down + path.line_to((x1, y1)); + + // Bottom edge + path.line_to((x0, y1)); + + path.close_path(); + path + } else { + RoundedRect::new(x0, y0, x1, y1, r).to_path(BEZ_PATH_TOLERANCE) + }; + + let mut vector = Vector::from_bezpath(bez_path); + let mut color = Color::from_rgba8_no_srgb(COLOR_F_WHITE).unwrap(); + color.set_alpha(0.05); + vector.style.fill = Fill::Solid(color); + vector +} + +// fn node_secondary_row(x0: f64, y: f64, index: usize, rounded_bottom: bool) -> Vector { +// let y0 = y + index as f64 * GRID_SIZE; +// let x1 = x0 + GRID_SIZE * 5.; +// let y1 = y0 + GRID_SIZE; +// let r = 2.; +// let bez_path = if rounded_bottom { +// let mut path = BezPath::new(); +// path.move_to((x0, y0)); + +// // Top edge +// path.line_to((x1, y0)); + +// // Right side down +// path.line_to((x1, y1 - r)); + +// // Bottom-right corner arc +// path.quad_to((x1, y1), (x1 - r, y1)); + +// // Bottom edge +// path.line_to((x0 + r, y1)); + +// // Bottom-left corner arc +// path.quad_to((x0, y1), (x0, y1 - r)); + +// // Left side up +// path.line_to((x0, y0)); + +// path.close_path(); +// path +// } else { +// Rect::new(x0, y0, x1, y1).to_path(BEZ_PATH_TOLERANCE) +// }; +// let mut vector = Vector::from_bezpath(bez_path); +// let mut color = Color::from_rgba8_no_srgb(COLOR_0_BLACK).unwrap(); +// color.set_alpha(0.33); +// vector.style.fill = Fill::Solid(color); +// vector +// } diff --git a/node-graph/gcore/src/node_graph_overlay/types.rs b/node-graph/gcore/src/node_graph_overlay/types.rs index db9a293dae..8c52d02f62 100644 --- a/node-graph/gcore/src/node_graph_overlay/types.rs +++ b/node-graph/gcore/src/node_graph_overlay/types.rs @@ -1,7 +1,30 @@ -use crate::uuid::NodeId; +use glam::{DAffine2, DVec2}; + +use crate::{node_graph_overlay::consts::*, uuid::NodeId}; +use std::hash::{Hash, Hasher}; + +#[derive(Clone, Debug, Default, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)] +pub struct NodeGraphTransform { + pub scale: f64, + pub x: f64, + pub y: f64, +} + +impl NodeGraphTransform { + pub fn to_daffine2(&self) -> DAffine2 { + DAffine2::from_scale_angle_translation(DVec2::splat(self.scale), 0.0, DVec2::new(self.x, self.y)) + } +} + +impl Hash for NodeGraphTransform { + fn hash(&self, state: &mut H) { + self.scale.to_bits().hash(state); + self.x.to_bits().hash(state); + self.y.to_bits().hash(state); + } +} #[derive(Clone, Debug, Default, PartialEq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)] - pub struct NodeGraphOverlayData { pub nodes_to_render: Vec, pub open: bool, @@ -21,7 +44,6 @@ pub struct FrontendNodeToRender { // Metadata that is common to nodes and layers #[derive(Clone, Debug, Default, PartialEq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)] - pub struct FrontendNodeMetadata { #[serde(rename = "nodeId")] pub node_id: NodeId, @@ -41,7 +63,6 @@ pub struct FrontendNodeMetadata { } #[derive(Clone, Debug, Default, PartialEq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)] - pub struct FrontendNode { // pub position: FrontendNodePosition, pub position: FrontendXY, @@ -50,7 +71,6 @@ pub struct FrontendNode { } #[derive(Clone, Debug, Default, PartialEq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)] - pub struct FrontendLayer { #[serde(rename = "bottomInput")] pub bottom_input: FrontendGraphInput, @@ -71,7 +91,6 @@ pub struct FrontendLayer { } #[derive(Clone, Debug, Default, PartialEq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)] - pub struct FrontendXY { pub x: i32, pub y: i32, @@ -93,15 +112,32 @@ pub struct FrontendXY { // pub stack: Option, // } +// Should be an enum but those are hard to serialize/deserialize to TS #[derive(Clone, Debug, Default, PartialEq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)] - pub struct FrontendNodeOrLayer { pub node: Option, pub layer: Option, } -#[derive(Clone, Debug, Default, PartialEq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)] +impl FrontendNodeOrLayer { + pub fn to_enum(self) -> NodeOrLayer { + let node_or_layer = if let Some(node) = self.node { + Some(NodeOrLayer::Node(node)) + } else if let Some(layer) = self.layer { + Some(NodeOrLayer::Layer(layer)) + } else { + None + }; + node_or_layer.unwrap() + } +} +pub enum NodeOrLayer { + Node(FrontendNode), + Layer(FrontendLayer), +} + +#[derive(Clone, Debug, Default, PartialEq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)] pub struct FrontendGraphInput { #[serde(rename = "dataType")] pub data_type: FrontendGraphDataType, @@ -118,7 +154,6 @@ pub struct FrontendGraphInput { } #[derive(Clone, Debug, Default, PartialEq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)] - pub struct FrontendGraphOutput { #[serde(rename = "dataType")] pub data_type: FrontendGraphDataType, @@ -140,7 +175,6 @@ pub struct FrontendExport { } #[derive(Clone, Debug, Default, PartialEq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)] - pub struct FrontendExports { /// If the primary export is not visible, then it is None. pub exports: Vec>, @@ -149,7 +183,6 @@ pub struct FrontendExports { } #[derive(Clone, Debug, Default, PartialEq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)] - pub struct FrontendImport { pub port: FrontendGraphOutput, pub wires: Vec, @@ -168,3 +201,32 @@ pub enum FrontendGraphDataType { Gradient, Typography, } + +impl FrontendGraphDataType { + pub fn data_color(&self) -> &'static str { + match self { + FrontendGraphDataType::General => COLOR_DATA_GENERAL, + FrontendGraphDataType::Number => COLOR_DATA_NUMBER, + FrontendGraphDataType::Artboard => COLOR_DATA_ARTBOARD, + FrontendGraphDataType::Graphic => COLOR_DATA_GRAPHIC, + FrontendGraphDataType::Raster => COLOR_DATA_RASTER, + FrontendGraphDataType::Vector => COLOR_DATA_VECTOR, + FrontendGraphDataType::Color => COLOR_DATA_COLOR, + FrontendGraphDataType::Gradient => COLOR_DATA_GRADIENT, + FrontendGraphDataType::Typography => COLOR_DATA_TYPOGRAPHY, + } + } + pub fn data_color_dim(&self) -> &'static str { + match self { + FrontendGraphDataType::General => COLOR_DATA_GENERAL_DIM, + FrontendGraphDataType::Number => COLOR_DATA_NUMBER_DIM, + FrontendGraphDataType::Artboard => COLOR_DATA_ARTBOARD_DIM, + FrontendGraphDataType::Graphic => COLOR_DATA_GRAPHIC_DIM, + FrontendGraphDataType::Raster => COLOR_DATA_RASTER_DIM, + FrontendGraphDataType::Vector => COLOR_DATA_VECTOR_DIM, + FrontendGraphDataType::Color => COLOR_DATA_COLOR_DIM, + FrontendGraphDataType::Gradient => COLOR_DATA_GRADIENT_DIM, + FrontendGraphDataType::Typography => COLOR_DATA_TYPOGRAPHY_DIM, + } + } +} diff --git a/node-graph/gcore/src/node_graph_overlay/ui_context.rs b/node-graph/gcore/src/node_graph_overlay/ui_context.rs new file mode 100644 index 0000000000..62ca8aa858 --- /dev/null +++ b/node-graph/gcore/src/node_graph_overlay/ui_context.rs @@ -0,0 +1,26 @@ +use std::sync::{Arc, mpsc::Sender}; + +use glam::UVec2; +use graphene_core_shaders::{Ctx, context::ArcCtx}; + +use crate::node_graph_overlay::types::NodeGraphTransform; + +pub type UIContext = Arc; + +#[derive(Debug, Clone, dyn_any::DynAny)] +pub struct UIContextImpl { + pub transform: NodeGraphTransform, + pub resolution: UVec2, + pub response_sender: Sender, +} + +#[derive(Debug, Clone, dyn_any::DynAny)] +pub enum UIRuntimeResponse { + RuntimeReady, + OverlaySVG(String), + OverlayTexture(wgpu::Texture), + // OverlayClickTargets(NodeId, ClickTarget) +} + +impl Ctx for UIContextImpl {} +impl ArcCtx for UIContextImpl {} diff --git a/node-graph/graph-craft/src/proto.rs b/node-graph/graph-craft/src/proto.rs index de164c6cf9..a46c1b7109 100644 --- a/node-graph/graph-craft/src/proto.rs +++ b/node-graph/graph-craft/src/proto.rs @@ -672,7 +672,7 @@ impl TypingContext { // If the node has a value input we can infer the return type from it ConstructionArgs::Value(ref v) => { // TODO: This should return a reference to the value - let types = NodeIOTypes::new(concrete!(Context), Type::Future(Box::new(v.ty())), vec![]); + let types = NodeIOTypes::new(generic!(T), Type::Future(Box::new(v.ty())), vec![]); self.inferred.insert(node_id, types.clone()); return Ok(types); } diff --git a/node-graph/gstd/src/wasm_application_io.rs b/node-graph/gstd/src/wasm_application_io.rs index 91771417a4..019e60aa2e 100644 --- a/node-graph/gstd/src/wasm_application_io.rs +++ b/node-graph/gstd/src/wasm_application_io.rs @@ -1,3 +1,4 @@ +use glam::DVec2; use graph_craft::document::value::RenderOutput; pub use graph_craft::document::value::RenderOutputType; pub use graph_craft::wasm_application_io::*; @@ -6,6 +7,7 @@ use graphene_core::Artboard; use graphene_core::gradient::GradientStops; #[cfg(target_family = "wasm")] use graphene_core::math::bbox::Bbox; +use graphene_core::node_graph_overlay::ui_context::UIContext; use graphene_core::raster::image::Image; use graphene_core::raster_types::{CPU, Raster}; use graphene_core::table::Table; @@ -349,3 +351,24 @@ async fn render<'a: 'n, T: 'n + Render + WasmNotSend>( }; RenderOutput { data, metadata } } + +#[node_macro::node(skip_impl)] +async fn render_node_graph_ui( + ui_context: UIContext, + #[implementations( + UIContext -> Table, + UIContext -> Table, + UIContext -> Table, + UIContext -> Table>, + UIContext -> Table, + UIContext -> Table, + )] + data: impl Node, +) -> String { + let data = data.eval(ui_context.clone()).await; + let render_params = RenderParams::default(); + let mut render = SvgRender::new(); + data.render_svg(&mut render, &render_params); + render.format_svg(DVec2::ZERO, ui_context.resolution.as_dvec2()); + render.svg.to_svg_string() +} diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 2bd69b8b39..4bc7b8babe 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -21,6 +21,8 @@ use graphene_std::application_io::{ImageTexture, SurfaceFrame}; use graphene_std::brush::brush_cache::BrushCache; use graphene_std::brush::brush_stroke::BrushStroke; use graphene_std::gradient::GradientStops; +use graphene_std::node_graph_overlay::types::NodeGraphOverlayData; +use graphene_std::node_graph_overlay::ui_context::UIContext; use graphene_std::table::Table; use graphene_std::transform::Footprint; use graphene_std::uuid::NodeId; @@ -251,6 +253,12 @@ fn node_registry() -> HashMap, input: UIContext, fn_params: [UIContext => NodeGraphOverlayData]), + async_node!(graphene_core::node_graph_overlay::TransformNodesNode<_>, input: UIContext, fn_params: [UIContext =>Table]), + async_node!(graphene_core::node_graph_overlay::DotGridBackgroundNode<_>, input: UIContext, fn_params: [UIContext =>f64]), + async_node!(graphene_core::node_graph_overlay::NodeGraphUiExtendNode<_, _>, input: UIContext, fn_params: [UIContext =>Table, UIContext =>Table]), + async_node!(graphene_std::wasm_application_io::RenderNodeGraphUiNode<_>, input: UIContext, fn_params: [UIContext =>Table]), + async_node!(graphene_core::node_graph_overlay::SendRenderNode<_>, input: UIContext, fn_params: [UIContext => String]), ]; // ============= // CONVERT NODES