diff --git a/editor/src/application.rs b/editor/src/application.rs index e3c7c24d09..5b3e64d275 100644 --- a/editor/src/application.rs +++ b/editor/src/application.rs @@ -1,5 +1,8 @@ use crate::dispatcher::Dispatcher; +use crate::messages::portfolio::document::node_graph::generate_node_graph_overlay::generate_node_graph_overlay; use crate::messages::prelude::*; +use graph_craft::document::{NodeInput, NodeNetwork}; +use graphene_std::node_graph_overlay::types::NodeGraphOverlayData; pub use graphene_std::uuid::*; // TODO: serialize with serde to save the current editor state @@ -30,6 +33,32 @@ impl Editor { pub fn poll_node_graph_evaluation(&mut self, responses: &mut VecDeque) -> Result<(), String> { self.dispatcher.poll_node_graph_evaluation(responses) } + + pub fn generate_node_graph_overlay_network(&mut self) -> Option { + let Some(active_document) = self.dispatcher.message_handlers.portfolio_message_handler.active_document_mut() else { + return None; + }; + let breadcrumb_network_path = &active_document.breadcrumb_network_path; + let nodes_to_render = active_document.network_interface.collect_nodes( + &active_document.node_graph_handler.node_graph_errors, + self.dispatcher.message_handlers.preferences_message_handler.graph_wire_style, + breadcrumb_network_path, + ); + let previewed_node = active_document.network_interface.previewed_node(breadcrumb_network_path); + let node_graph_render_data = NodeGraphOverlayData { + nodes_to_render, + open: active_document.graph_view_overlay_open, + in_selected_network: &active_document.selection_network_path == breadcrumb_network_path, + previewed_node, + }; + let opacity = active_document.graph_fade_artwork_percentage; + let node_graph_overlay_node = generate_node_graph_overlay(node_graph_render_data, opacity); + Some(NodeNetwork { + exports: vec![NodeInput::node(NodeId(0), 0)], + nodes: vec![(NodeId(0), node_graph_overlay_node)].into_iter().collect(), + ..Default::default() + }) + } } impl Default for Editor { diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 3942b3d3ab..dea15382f8 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -20,11 +20,11 @@ pub struct DispatcherMessageHandlers { defer_message_handler: DeferMessageHandler, dialog_message_handler: DialogMessageHandler, globals_message_handler: GlobalsMessageHandler, - input_preprocessor_message_handler: InputPreprocessorMessageHandler, + pub input_preprocessor_message_handler: InputPreprocessorMessageHandler, key_mapping_message_handler: KeyMappingMessageHandler, layout_message_handler: LayoutMessageHandler, pub portfolio_message_handler: PortfolioMessageHandler, - preferences_message_handler: PreferencesMessageHandler, + pub preferences_message_handler: PreferencesMessageHandler, tool_message_handler: ToolMessageHandler, } diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 59a713b544..1f8aafcf44 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -1,14 +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, FrontendGraphInput, FrontendGraphOutput, FrontendNodeToRender, FrontendNodeType, FrontendXY, 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::{WirePath, WirePathUpdate}; +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, FrontendXY, NodeGraphTransform}; use graphene_std::raster::Image; use graphene_std::raster::color::Color; use graphene_std::text::{Font, TextAlign}; @@ -127,9 +126,8 @@ pub enum FrontendMessage { }, UpdateImportsExports { /// If the primary import is not visible, then it is None. - imports: Vec>, - /// If the primary export is not visible, then it is None. - exports: Vec>, + imports: Vec>, + exports: FrontendExports, /// The primary import location. #[serde(rename = "importPosition")] import_position: FrontendXY, @@ -140,7 +138,7 @@ pub enum FrontendMessage { #[serde(rename = "addImportExport")] add_import_export: bool, }, - UpdateBox { + UpdateNodeGraphSelectionBox { #[serde(rename = "box")] box_selection: Option, }, @@ -271,31 +269,18 @@ pub enum FrontendMessage { UpdateMouseCursor { cursor: MouseCursorIcon, }, - 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 - #[serde(rename = "previewedNode")] - previewed_node: Option, - }, - UpdateVisibleNodes { - nodes: Vec, - }, - UpdateNodeGraphWires { - wires: Vec, + RequestNativeNodeGraphRender, + UpdateNativeNodeGraphSVG { + #[serde(rename = "svgString")] + svg_string: String, }, - ClearAllNodeGraphWires, UpdateNodeGraphControlBarLayout { #[serde(rename = "layoutTarget")] layout_target: LayoutTarget, diff: Vec, }, UpdateNodeGraphTransform { - transform: Transform, + transform: NodeGraphTransform, }, UpdateNodeThumbnail { id: NodeId, @@ -321,8 +306,8 @@ pub enum FrontendMessage { diff: Vec, }, UpdateWirePathInProgress { - #[serde(rename = "wirePath")] - wire_path: Option, + #[serde(rename = "wirePathInProgress")] + wire_path_in_progress: Option, }, UpdateWorkingColorsLayout { #[serde(rename = "layoutTarget")] diff --git a/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs b/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs index 52e20d1d7b..a7bcfa37b9 100644 --- a/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs +++ b/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs @@ -34,7 +34,6 @@ impl MessageHandler f self.viewport_bounds = bounds; responses.add(NavigationMessage::CanvasPan { delta: DVec2::ZERO }); - responses.add(NodeGraphMessage::SetGridAlignedEdges); } responses.add(DeferMessage::AfterGraphRun { messages: vec![ diff --git a/editor/src/messages/layout/utility_types/widgets/button_widgets.rs b/editor/src/messages/layout/utility_types/widgets/button_widgets.rs index 3698bf3bc0..5f401d08dd 100644 --- a/editor/src/messages/layout/utility_types/widgets/button_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/button_widgets.rs @@ -1,8 +1,8 @@ use crate::messages::input_mapper::utility_types::misc::ActionKeys; use crate::messages::layout::utility_types::widget_prelude::*; -use crate::messages::portfolio::document::node_graph::utility_types::FrontendGraphDataType; use crate::messages::tool::tool_messages::tool_prelude::WidgetCallback; use derivative::*; +use graphene_std::node_graph_overlay::types::FrontendGraphDataType; use graphene_std::vector::style::FillChoice; use graphite_proc_macros::WidgetBuilder; diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index ccf12a467a..71373d0ebb 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; @@ -119,10 +119,10 @@ pub struct DocumentMessageHandler { pub(crate) path: Option, /// Path to network currently viewed in the node graph overlay. This will eventually be stored in each panel, so that multiple panels can refer to different networks #[serde(skip)] - breadcrumb_network_path: Vec, + pub breadcrumb_network_path: Vec, /// Path to network that is currently selected. Updated based on the most recently clicked panel. #[serde(skip)] - selection_network_path: Vec, + pub selection_network_path: Vec, /// Stack of document network snapshots for previous history states. #[serde(skip)] document_undo_history: VecDeque, @@ -474,10 +474,8 @@ impl MessageHandler> for DocumentMes DocumentMessage::EnterNestedNetwork { node_id } => { self.breadcrumb_network_path.push(node_id); self.selection_network_path.clone_from(&self.breadcrumb_network_path); - responses.add(NodeGraphMessage::UnloadWires); responses.add(NodeGraphMessage::SendGraph); responses.add(DocumentMessage::ZoomCanvasToFitAll); - responses.add(NodeGraphMessage::SetGridAlignedEdges); } DocumentMessage::Escape => { if self.node_graph_handler.drag_start.is_some() { @@ -494,7 +492,7 @@ impl MessageHandler> for DocumentMes responses.add(FrontendMessage::UpdateContextMenuInformation { context_menu_information: None }); self.node_graph_handler.wire_in_progress_from_connector = None; self.node_graph_handler.wire_in_progress_to_connector = None; - responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path: None }); + responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path_in_progress: None }); } else { responses.add(DocumentMessage::GraphViewOverlay { open: false }); } @@ -504,10 +502,8 @@ impl MessageHandler> for DocumentMes self.breadcrumb_network_path.pop(); self.selection_network_path.clone_from(&self.breadcrumb_network_path); } - responses.add(NodeGraphMessage::UnloadWires); responses.add(NodeGraphMessage::SendGraph); responses.add(DocumentMessage::PTZUpdate); - responses.add(NodeGraphMessage::SetGridAlignedEdges); } DocumentMessage::FlipSelectedLayers { flip_axis } => { let scale = match flip_axis { @@ -557,7 +553,6 @@ impl MessageHandler> for DocumentMes } } DocumentMessage::GraphViewOverlay { open } => { - let opened = !self.graph_view_overlay_open && open; self.graph_view_overlay_open = open; responses.add(FrontendMessage::UpdateGraphViewOverlay { open }); @@ -567,14 +562,10 @@ impl MessageHandler> for DocumentMes responses.add(DocumentMessage::RenderRulers); responses.add(DocumentMessage::RenderScrollbars); - if opened { - responses.add(NodeGraphMessage::UnloadWires); - } if open { responses.add(ToolMessage::DeactivateTools); responses.add(OverlaysMessage::Draw); // Clear the overlays responses.add(NavigationMessage::CanvasTiltSet { angle_radians: 0. }); - responses.add(NodeGraphMessage::SetGridAlignedEdges); responses.add(NodeGraphMessage::UpdateGraphBarRight); responses.add(NodeGraphMessage::UpdateHints); } else { @@ -1495,12 +1486,10 @@ impl MessageHandler> for DocumentMes responses.add(DocumentMessage::RenderRulers); responses.add(DocumentMessage::RenderScrollbars); - responses.add(NodeGraphMessage::UpdateEdges); responses.add(NodeGraphMessage::UpdateBoxSelection); 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, @@ -1948,10 +1937,6 @@ impl DocumentMessageHandler { responses.add(NodeGraphMessage::SelectedNodesUpdated); responses.add(NodeGraphMessage::ForceRunDocumentGraph); - // TODO: Remove once the footprint is used to load the imports/export distances from the edge - responses.add(NodeGraphMessage::UnloadWires); - responses.add(NodeGraphMessage::SetGridAlignedEdges); - Some(previous_network) } pub fn redo_with_history(&mut self, ipp: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { @@ -1981,8 +1966,6 @@ impl DocumentMessageHandler { responses.add(PortfolioMessage::UpdateOpenDocumentsList); responses.add(NodeGraphMessage::SelectedNodesUpdated); responses.add(NodeGraphMessage::ForceRunDocumentGraph); - responses.add(NodeGraphMessage::UnloadWires); - responses.add(NodeGraphMessage::SendWires); Some(previous_network) } diff --git a/editor/src/messages/portfolio/document/navigation/navigation_message_handler.rs b/editor/src/messages/portfolio/document/navigation/navigation_message_handler.rs index 259b60e995..63f8817511 100644 --- a/editor/src/messages/portfolio/document/navigation/navigation_message_handler.rs +++ b/editor/src/messages/portfolio/document/navigation/navigation_message_handler.rs @@ -178,7 +178,6 @@ impl MessageHandler> for Navigat NavigationMessage::CanvasPanMouseWheel { use_y_as_x } => { let delta = if use_y_as_x { (-ipp.mouse.scroll_delta.y, 0.).into() } else { -ipp.mouse.scroll_delta.as_dvec2() } * VIEWPORT_SCROLL_RATE; responses.add(NavigationMessage::CanvasPan { delta }); - responses.add(NodeGraphMessage::SetGridAlignedEdges); } NavigationMessage::CanvasTiltResetAndZoomTo100Percent => { let Some(ptz) = get_ptz_mut(document_ptz, network_interface, graph_view_overlay_open, breadcrumb_network_path) else { @@ -193,7 +192,6 @@ impl MessageHandler> for Navigat responses.add(PortfolioMessage::UpdateDocumentWidgets); } responses.add(DocumentMessage::PTZUpdate); - responses.add(NodeGraphMessage::SetGridAlignedEdges); } NavigationMessage::CanvasTiltSet { angle_radians } => { let Some(ptz) = get_ptz_mut(document_ptz, network_interface, graph_view_overlay_open, breadcrumb_network_path) else { @@ -272,7 +270,6 @@ impl MessageHandler> for Navigat responses.add(PortfolioMessage::UpdateDocumentWidgets); } responses.add(DocumentMessage::PTZUpdate); - responses.add(NodeGraphMessage::SetGridAlignedEdges); } NavigationMessage::CanvasFlip => { if graph_view_overlay_open { @@ -320,7 +317,6 @@ impl MessageHandler> for Navigat } else { responses.add(PortfolioMessage::UpdateDocumentWidgets); } - responses.add(NodeGraphMessage::SetGridAlignedEdges); // Reset the navigation operation now that it's done self.navigation_operation = NavigationOperation::None; @@ -382,7 +378,6 @@ impl MessageHandler> for Navigat responses.add(PortfolioMessage::UpdateDocumentWidgets); } responses.add(DocumentMessage::PTZUpdate); - responses.add(NodeGraphMessage::SetGridAlignedEdges); } // Fully zooms in on the selected NavigationMessage::FitViewportToSelection => { @@ -476,7 +471,6 @@ impl MessageHandler> for Navigat }; responses.add(NavigationMessage::CanvasZoomSet { zoom_factor: ptz.zoom() }); - responses.add(NodeGraphMessage::SetGridAlignedEdges); } } 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 new file mode 100644 index 0000000000..2d69ae1dba --- /dev/null +++ b/editor/src/messages/portfolio/document/node_graph/generate_node_graph_overlay.rs @@ -0,0 +1,136 @@ +use graph_craft::{ + concrete, + document::{DocumentNode, DocumentNodeImplementation, NodeInput, NodeNetwork, value::TaggedValue}, +}; +use graphene_std::{ + 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 { + 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 identity_implementation = DocumentNodeImplementation::ProtoNode(graphene_std::ops::identity::IDENTIFIER); + let memo_implementation = DocumentNodeImplementation::ProtoNode(graphene_std::memo::memo::IDENTIFIER); + + DocumentNode { + inputs: vec![ + 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(cache_output_id, 0)], + nodes: vec![ + // Create the nodes + ( + generate_nodes_id, + DocumentNode { + 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 + ( + cache_nodes_id, + DocumentNode { + call_argument: concrete!(UIContext), + inputs: vec![NodeInput::node(generate_nodes_id, 0)], + implementation: identity_implementation.clone(), + ..Default::default() + }, + ), + // Transform the nodes based on the Context + ( + transform_nodes_id, + DocumentNode { + 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: identity_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 nothing changes + ( + cache_output_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!(UIContext), + ..Default::default() + } +} diff --git a/editor/src/messages/portfolio/document/node_graph/mod.rs b/editor/src/messages/portfolio/document/node_graph/mod.rs index bd25016438..30874339f3 100644 --- a/editor/src/messages/portfolio/document/node_graph/mod.rs +++ b/editor/src/messages/portfolio/document/node_graph/mod.rs @@ -1,4 +1,5 @@ pub mod document_node_definitions; +pub mod generate_node_graph_overlay; mod node_graph_message; mod node_graph_message_handler; pub mod node_properties; 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 37ac29df44..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)] @@ -139,11 +140,7 @@ pub enum NodeGraphMessage { }, SendClickTargets, EndSendClickTargets, - UnloadWires, - SendWires, - UpdateVisibleNodes, SendGraph, - SetGridAlignedEdges, SetInputValue { node_id: NodeId, input_index: usize, @@ -218,11 +215,14 @@ pub enum NodeGraphMessage { SetLockedOrVisibilitySideEffects { node_ids: Vec, }, - UpdateEdges, UpdateBoxSelection, 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 084f2f72cb..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 @@ -6,14 +6,14 @@ 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}; 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, }; 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::portfolio::document::utility_types::wires::{GraphWireStyle, WirePathInProgress, 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::get_clip_mode; @@ -24,6 +24,7 @@ use glam::{DAffine2, DVec2, IVec2}; 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}; use graphene_std::vector::algorithms::bezpath_algorithms::bezpath_is_inside_bezpath; use graphene_std::*; use kurbo::{DEFAULT_ACCURACY, Shape}; @@ -91,10 +92,8 @@ pub struct NodeGraphMessageHandler { reordering_export: Option, /// The end index of the moved connector end_index: Option, - /// Used to keep track of what nodes are sent to the front end so that only visible ones are sent to the frontend - frontend_nodes: Vec, - /// Used to keep track of what wires are sent to the front end so the old ones can be removed - frontend_wires: HashSet<(NodeId, usize)>, + // 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. @@ -297,7 +296,7 @@ impl<'a> MessageHandler> for NodeG self.wire_in_progress_type = FrontendGraphDataType::General; self.wire_in_progress_to_connector = None; } - responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path: None }); + responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path_in_progress: None }); responses.add(FrontendMessage::UpdateContextMenuInformation { context_menu_information: self.context_menu.clone(), }); @@ -470,7 +469,6 @@ impl<'a> MessageHandler> for NodeG } responses.add(NodeGraphMessage::UpdateImportsExports); - responses.add(NodeGraphMessage::SendWires); } NodeGraphMessage::ExposePrimaryExport { exposed } => { let export_connector: InputConnector = InputConnector::Export(0); @@ -761,7 +759,7 @@ impl<'a> MessageHandler> for NodeG responses.add(NodeGraphMessage::SelectedNodesSet { nodes: self.selection_before_pointer_down.clone(), }); - responses.add(FrontendMessage::UpdateBox { box_selection: None }); + responses.add(FrontendMessage::UpdateNodeGraphSelectionBox { box_selection: None }); return; } // Abort dragging a wire @@ -770,7 +768,7 @@ impl<'a> MessageHandler> for NodeG self.wire_in_progress_type = FrontendGraphDataType::General; self.wire_in_progress_to_connector = None; responses.add(DocumentMessage::AbortTransaction); - responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path: None }); + responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path_in_progress: None }); return; } @@ -851,7 +849,7 @@ impl<'a> MessageHandler> for NodeG responses.add(FrontendMessage::UpdateContextMenuInformation { context_menu_information: self.context_menu.clone(), }); - responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path: None }); + responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path_in_progress: None }); } // Toggle visibility of clicked node and return @@ -878,7 +876,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::displayed_type(&network_interface.input_type(clicked_input, breadcrumb_network_path)); + self.wire_in_progress_type = network_interface.input_type(clicked_input, breadcrumb_network_path).displayed_type(); return; } @@ -889,7 +887,7 @@ impl<'a> MessageHandler> for NodeG self.wire_in_progress_from_connector = network_interface.output_position(&clicked_output, selection_network_path); let output_type = network_interface.output_type(&clicked_output, breadcrumb_network_path); - self.wire_in_progress_type = FrontendGraphDataType::displayed_type(&output_type); + self.wire_in_progress_type = output_type.displayed_type(); self.update_node_graph_hints(responses); return; @@ -1058,14 +1056,14 @@ impl<'a> MessageHandler> for NodeG to_connector_is_layer, GraphWireStyle::Direct, ); - let path_string = vector_wire.to_svg(); - let wire_path = WirePath { - path_string, + let wire_path = WirePathInProgress { + wire: vector_wire.to_svg(), data_type: self.wire_in_progress_type, thick: false, - dashed: false, }; - responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path: Some(wire_path) }); + responses.add(FrontendMessage::UpdateWirePathInProgress { + wire_path_in_progress: Some(wire_path), + }); } } else if let Some((drag_start, dragged)) = &mut self.drag_start { if drag_start.start_x != point.x || drag_start.start_y != point.y { @@ -1315,7 +1313,8 @@ impl<'a> MessageHandler> for NodeG return None; } - let (wire, is_stack) = network_interface.vector_wire_from_input(&input, preferences.graph_wire_style, selection_network_path)?; + let wire = network_interface.wire_from_input(&input, preferences.graph_wire_style, selection_network_path)?; + let thick = network_interface.wire_is_thick(&input, selection_network_path); let node_bbox = kurbo::Rect::new(node_bbox[0].x, node_bbox[0].y, node_bbox[1].x, node_bbox[1].y).to_path(DEFAULT_ACCURACY); let inside = bezpath_is_inside_bezpath(&wire, &node_bbox, None, None); @@ -1324,7 +1323,7 @@ impl<'a> MessageHandler> for NodeG .segments() .any(|segment| node_bbox.segments().filter_map(|segment| segment.as_line()).any(|line| !segment.intersect_line(line).is_empty())); - (intersect || inside).then_some((input, is_stack)) + (intersect || inside).then_some((input, thick)) }) .collect::>(); @@ -1391,8 +1390,8 @@ impl<'a> MessageHandler> for NodeG self.reordering_export = None; self.reordering_import = None; responses.add(DocumentMessage::EndTransaction); - responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path: None }); - responses.add(FrontendMessage::UpdateBox { box_selection: None }); + responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path_in_progress: None }); + responses.add(FrontendMessage::UpdateNodeGraphSelectionBox { box_selection: None }); responses.add(FrontendMessage::UpdateImportReorderIndex { index: None }); responses.add(FrontendMessage::UpdateExportReorderIndex { index: None }); self.update_node_graph_hints(responses); @@ -1591,75 +1590,16 @@ impl<'a> MessageHandler> for NodeG click_targets: Some(network_interface.collect_frontend_click_targets(breadcrumb_network_path)), }), NodeGraphMessage::EndSendClickTargets => responses.add(FrontendMessage::UpdateClickTargets { click_targets: None }), - NodeGraphMessage::UnloadWires => { - for input in network_interface.node_graph_input_connectors(breadcrumb_network_path) { - network_interface.unload_wire(&input, breadcrumb_network_path); - } - - responses.add(FrontendMessage::ClearAllNodeGraphWires); - } - NodeGraphMessage::SendWires => { - let wires = self.collect_wires(network_interface, preferences.graph_wire_style, breadcrumb_network_path); - responses.add(FrontendMessage::UpdateNodeGraphWires { wires }); - } - NodeGraphMessage::UpdateVisibleNodes => { - let Some(network_metadata) = network_interface.network_metadata(breadcrumb_network_path) else { - return; - }; - - let viewport_bbox = ipp.document_bounds(); - let document_bbox: [DVec2; 2] = viewport_bbox.map(|p| network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport.inverse().transform_point2(p)); - - let mut nodes = Vec::new(); - for node_id in &self.frontend_nodes { - let Some(node_bbox) = network_interface.node_bounding_box(node_id, breadcrumb_network_path) else { - log::error!("Could not get bbox for node: {node_id:?}"); - continue; - }; - - if node_bbox[1].x >= document_bbox[0].x && node_bbox[0].x <= document_bbox[1].x && node_bbox[1].y >= document_bbox[0].y && node_bbox[0].y <= document_bbox[1].y { - nodes.push(*node_id); - } - for error in &self.node_graph_errors { - if error.node_path.contains(node_id) { - nodes.push(*node_id); - } - } - } - - responses.add(FrontendMessage::UpdateVisibleNodes { nodes }); - } NodeGraphMessage::SendGraph => { responses.add(NodeGraphMessage::UpdateLayerPanel); responses.add(DocumentMessage::DocumentStructureChanged); responses.add(PropertiesPanelMessage::Refresh); responses.add(NodeGraphMessage::UpdateActionButtons); - 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); + responses.add(FrontendMessage::RequestNativeNodeGraphRender); 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 { - network_interface.set_grid_aligned_edges(DVec2::new(ipp.viewport_bounds.bottom_right.x - ipp.viewport_bounds.top_left.x, 0.), breadcrumb_network_path); - // Send the new edges to the frontend - responses.add(NodeGraphMessage::UpdateImportsExports); - } - } NodeGraphMessage::SetInputValue { node_id, input_index, value } => { let input = NodeInput::value(value, false); responses.add(NodeGraphMessage::SetInput { @@ -1725,8 +1665,6 @@ impl<'a> MessageHandler> for NodeG Ordering::Equal => {} } } - - responses.add(NodeGraphMessage::SendWires); } NodeGraphMessage::ToggleSelectedAsLayersOrNodes => { let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else { @@ -1746,8 +1684,6 @@ impl<'a> MessageHandler> for NodeG } NodeGraphMessage::ShiftNodePosition { node_id, x, y } => { network_interface.shift_absolute_node_position(&node_id, IVec2::new(x, y), selection_network_path); - - responses.add(NodeGraphMessage::SendWires); } NodeGraphMessage::SetToNodeOrLayer { node_id, is_layer } => { if is_layer && !network_interface.is_eligible_to_be_layer(&node_id, selection_network_path) { @@ -1761,7 +1697,6 @@ impl<'a> MessageHandler> for NodeG }); responses.add(NodeGraphMessage::RunDocumentGraph); responses.add(NodeGraphMessage::SendGraph); - responses.add(NodeGraphMessage::SendWires); } NodeGraphMessage::SetDisplayName { node_id, @@ -1945,12 +1880,12 @@ impl<'a> MessageHandler> for NodeG nodes: nodes.into_iter().collect::>(), }); } - responses.add(FrontendMessage::UpdateBox { box_selection }) + responses.add(FrontendMessage::UpdateNodeGraphSelectionBox { box_selection }) } } NodeGraphMessage::UpdateImportsExports => { - let imports = network_interface.frontend_imports(breadcrumb_network_path); - let exports = network_interface.frontend_exports(breadcrumb_network_path); + let imports = network_interface.frontend_imports(preferences.graph_wire_style, breadcrumb_network_path); + let exports = network_interface.frontend_exports(preferences.graph_wire_style, breadcrumb_network_path); let Some((import_position, export_position)) = network_interface.import_export_position(breadcrumb_network_path) else { log::error!("Could not get import export positions"); @@ -1969,8 +1904,6 @@ impl<'a> MessageHandler> for NodeG // Do not show the add import or add export button in the document network; let add_import_export = !breadcrumb_network_path.is_empty(); - responses.add(NodeGraphMessage::UpdateVisibleNodes); - responses.add(NodeGraphMessage::SendWires); responses.add(FrontendMessage::UpdateImportsExports { imports, exports, @@ -1983,9 +1916,6 @@ impl<'a> MessageHandler> for NodeG NodeGraphMessage::UpdateLayerPanel => { Self::update_layer_panel(network_interface, selection_network_path, collapsed, layers_panel_open, responses); } - NodeGraphMessage::UpdateEdges => { - // Update the import/export UI edges whenever the PTZ changes or the bounding box of all nodes changes - } NodeGraphMessage::UpdateNewNodeGraph => { let Some(selected_nodes) = network_interface.selected_nodes_mut(selection_network_path) else { log::error!("Could not get selected nodes in NodeGraphMessage::UpdateNewNodeGraph"); @@ -1996,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; @@ -2461,41 +2394,6 @@ impl NodeGraphMessageHandler { } } - fn collect_wires(&mut self, network_interface: &mut NodeNetworkInterface, graph_wire_style: GraphWireStyle, breadcrumb_network_path: &[NodeId]) -> Vec { - let mut added_wires = network_interface - .node_graph_input_connectors(breadcrumb_network_path) - .iter() - .filter_map(|connector| network_interface.newly_loaded_input_wire(connector, graph_wire_style, breadcrumb_network_path)) - .collect::>(); - - let changed_wire_inputs = added_wires.iter().map(|update| (update.id, update.input_index)).collect::>(); - self.frontend_wires.extend(changed_wire_inputs); - - let mut orphaned_wire_inputs = self.frontend_wires.clone(); - self.frontend_wires = network_interface - .node_graph_wire_inputs(breadcrumb_network_path) - .iter() - .filter_map(|visible_wire_input| orphaned_wire_inputs.take(visible_wire_input)) - .collect::>(); - added_wires.extend(orphaned_wire_inputs.into_iter().map(|(id, input_index)| WirePathUpdate { - id, - input_index, - wire_path_update: None, - })); - - if let Some(wire_to_root) = network_interface.wire_to_root(graph_wire_style, breadcrumb_network_path) { - added_wires.push(wire_to_root); - } else { - added_wires.push(WirePathUpdate { - id: NodeId(u64::MAX), - input_index: u32::MAX as usize, - wire_path_update: None, - }) - } - - added_wires - } - 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(); @@ -2666,8 +2564,7 @@ impl Default for NodeGraphMessageHandler { reordering_export: None, reordering_import: None, end_index: None, - frontend_nodes: Vec::new(), - frontend_wires: HashSet::new(), + thumbnails: HashMap::new(), } } } 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 cbc38954bf..2754afa8d4 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -1,7 +1,6 @@ #![allow(clippy::too_many_arguments)] use super::document_node_definitions::{NODE_OVERRIDES, NodePropertiesContext}; -use super::utility_types::FrontendGraphDataType; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::utility_types::network_interface::InputConnector; use crate::messages::prelude::*; @@ -14,6 +13,7 @@ use graph_craft::{Type, concrete}; use graphene_std::NodeInputDecleration; use graphene_std::animation::RealTimeMode; use graphene_std::extract_xy::XY; +use graphene_std::node_graph_overlay::types::FrontendGraphDataType; use graphene_std::path_bool::BooleanOperation; use graphene_std::raster::curve::Curve; use graphene_std::raster::{ @@ -1996,7 +1996,10 @@ 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::displayed_type(&context.network_interface.input_type(&InputConnector::node(node_id, index), context.selection_network_path)); + let input_type = context + .network_interface + .input_type(&InputConnector::node(node_id, index), context.selection_network_path) + .displayed_type(); 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 f004921ef8..1a637758a1 100644 --- a/editor/src/messages/portfolio/document/node_graph/utility_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/utility_types.rs @@ -1,6 +1,4 @@ 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; @@ -60,10 +58,10 @@ pub struct FrontendXY { pub struct FrontendGraphInput { #[serde(rename = "dataType")] pub data_type: FrontendGraphDataType, - pub name: String, - pub description: String, #[serde(rename = "resolvedType")] pub resolved_type: String, + pub name: String, + pub description: String, /// Either "nothing", "import index {index}", or "{node name} output {output_index}". #[serde(rename = "connectedToString")] pub connected_to: String, @@ -86,6 +84,26 @@ pub struct FrontendGraphOutput { pub connected_to: Vec, } +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +pub struct FrontendExport { + pub port: FrontendGraphInput, + pub wire: Option, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +pub struct FrontendExports { + /// If the primary export is not visible, then it is None. + pub exports: Vec>, + #[serde(rename = "previewWire")] + pub preview_wire: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +pub struct FrontendImport { + pub port: FrontendGraphOutput, + pub wires: Vec, +} + // Metadata that is common to nodes and layers #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] pub struct FrontendNodeMetadata { @@ -159,6 +177,8 @@ pub struct FrontendNodeToRender { pub metadata: FrontendNodeMetadata, #[serde(rename = "nodeOrLayer")] pub node_or_layer: FrontendNodeOrLayer, + //TODO: Remove + pub wires: Vec<(String, bool, FrontendGraphDataType)>, } #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] @@ -186,7 +206,7 @@ impl FrontendNodeType { } } } -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct DragStart { pub start_x: f64, pub start_y: f64, @@ -194,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")] @@ -249,8 +262,6 @@ pub struct FrontendClickTargets { pub icon_click_targets: Vec, #[serde(rename = "allNodesBoundingBox")] pub all_nodes_bounding_box: String, - #[serde(rename = "importExportsBoundingBox")] - pub import_exports_bounding_box: String, #[serde(rename = "modifyImportExport")] pub modify_import_export: Vec, } 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 930db71c1b..8cabb7250b 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs @@ -1022,8 +1022,7 @@ impl OverlayContextInternal { align: TextAlign::Left, }; - const FONT_DATA: &[u8] = SOURCE_SANS_FONT_DATA; - let font_blob = Some(load_font(FONT_DATA)); + let font_blob = Some(load_font(SOURCE_SANS_FONT_DATA)); // Convert text to paths and calculate actual bounds let text_table = to_path(text, font_blob, typesetting, false); 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 fecba7ed14..0a0728df77 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -1,12 +1,11 @@ use super::document_metadata::{DocumentMetadata, LayerNodeIdentifier, NodeRelations}; use super::misc::PTZ; 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::consts::GRID_SIZE; 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}; +use crate::messages::portfolio::document::node_graph::utility_types::{Direction, FrontendClickTargets}; 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; @@ -20,7 +19,6 @@ 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 kurbo::BezPath; use serde_json::{Value, json}; use std::collections::{HashMap, HashSet, VecDeque}; use std::hash::Hash; @@ -684,13 +682,6 @@ impl NodeNetworkInterface { Some(&metadata.persistent_metadata) } - fn transient_input_metadata(&self, node_id: &NodeId, index: usize, network_path: &[NodeId]) -> Option<&InputTransientMetadata> { - let metadata = self - .node_metadata(node_id, network_path) - .and_then(|node_metadata| node_metadata.persistent_metadata.input_metadata.get(index))?; - Some(&metadata.transient_metadata) - } - /// Returns the input name to display in the properties panel. If the name is empty then the type is used. pub fn displayed_input_name_and_description(&mut self, node_id: &NodeId, input_index: usize, network_path: &[NodeId]) -> (String, String) { let Some(input_metadata) = self.persistent_input_metadata(node_id, input_index, network_path) else { @@ -1538,30 +1529,6 @@ impl NodeNetworkInterface { return; }; network_metadata.transient_metadata.import_export_ports.unload(); - - // Always unload all wires connected to them as well - let number_of_imports = self.number_of_imports(network_path); - let Some(outward_wires) = self.outward_wires(network_path) else { - log::error!("Could not get outward wires in remove_import"); - return; - }; - let mut input_connectors = Vec::new(); - for import_index in 0..number_of_imports { - let Some(outward_wires_for_import) = outward_wires.get(&OutputConnector::Import(import_index)).cloned() else { - log::error!("Could not get outward wires for import in remove_import"); - return; - }; - input_connectors.extend(outward_wires_for_import); - } - let Some(network) = self.nested_network(network_path) else { - return; - }; - for export_index in 0..network.exports.len() { - input_connectors.push(InputConnector::Export(export_index)); - } - for input in &input_connectors { - self.unload_wire(input, network_path); - } } pub fn modify_import_export(&mut self, network_path: &[NodeId]) -> Option<&ModifyImportExportClickTarget> { @@ -1653,68 +1620,6 @@ impl NodeNetworkInterface { network_metadata.transient_metadata.modify_import_export.unload(); } - pub fn rounded_network_edge_distance(&mut self, network_path: &[NodeId]) -> Option<&NetworkEdgeDistance> { - let Some(network_metadata) = self.network_metadata(network_path) else { - log::error!("Could not get nested network_metadata in rounded_network_edge_distance"); - return None; - }; - if !network_metadata.transient_metadata.rounded_network_edge_distance.is_loaded() { - self.load_rounded_network_edge_distance(network_path); - } - let Some(network_metadata) = self.network_metadata(network_path) else { - log::error!("Could not get nested network_metadata in rounded_network_edge_distance"); - return None; - }; - let TransientMetadata::Loaded(rounded_network_edge_distance) = &network_metadata.transient_metadata.rounded_network_edge_distance else { - log::error!("could not load import rounded_network_edge_distance"); - return None; - }; - Some(rounded_network_edge_distance) - } - - fn load_rounded_network_edge_distance(&mut self, network_path: &[NodeId]) { - let Some(network_metadata) = self.network_metadata_mut(network_path) else { - log::error!("Could not get nested network in set_grid_aligned_edges"); - return; - }; - // When setting the edges to be grid aligned, update the pixel offset to ensure the next pan starts from the snapped import/export position - let node_graph_to_viewport = network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport; - // TODO: Eventually replace node graph top right with the footprint when trying to get the network edge distance - let node_graph_top_right = network_metadata.persistent_metadata.navigation_metadata.node_graph_top_right; - let target_exports_distance = node_graph_to_viewport.inverse().transform_point2(DVec2::new( - node_graph_top_right.x - EXPORTS_TO_RIGHT_EDGE_PIXEL_GAP as f64, - node_graph_top_right.y + EXPORTS_TO_TOP_EDGE_PIXEL_GAP as f64, - )); - - let target_imports_distance = node_graph_to_viewport - .inverse() - .transform_point2(DVec2::new(IMPORTS_TO_LEFT_EDGE_PIXEL_GAP as f64, IMPORTS_TO_TOP_EDGE_PIXEL_GAP as f64)); - - let rounded_exports_distance = DVec2::new((target_exports_distance.x / 24. + 0.5).floor() * 24., (target_exports_distance.y / 24. + 0.5).floor() * 24.); - let rounded_imports_distance = DVec2::new((target_imports_distance.x / 24. + 0.5).floor() * 24., (target_imports_distance.y / 24. + 0.5).floor() * 24.); - - let rounded_viewport_exports_distance = node_graph_to_viewport.transform_point2(rounded_exports_distance); - let rounded_viewport_imports_distance = node_graph_to_viewport.transform_point2(rounded_imports_distance); - - let network_edge_distance = NetworkEdgeDistance { - exports_to_edge_distance: rounded_viewport_exports_distance, - imports_to_edge_distance: rounded_viewport_imports_distance, - }; - let Some(network_metadata) = self.network_metadata_mut(network_path) else { - log::error!("Could not get current network in load_export_ports"); - return; - }; - network_metadata.transient_metadata.rounded_network_edge_distance = TransientMetadata::Loaded(network_edge_distance); - } - - fn unload_rounded_network_edge_distance(&mut self, network_path: &[NodeId]) { - let Some(network_metadata) = self.network_metadata_mut(network_path) else { - log::error!("Could not get nested network_metadata in unload_export_ports"); - return; - }; - network_metadata.transient_metadata.rounded_network_edge_distance.unload(); - } - fn owned_nodes(&self, node_id: &NodeId, network_path: &[NodeId]) -> Option<&HashSet> { let layer_node = self.node_metadata(node_id, network_path)?; let NodeTypePersistentMetadata::Layer(LayerPersistentMetadata { owned_nodes, .. }) = &layer_node.persistent_metadata.node_type_metadata else { @@ -1980,99 +1885,6 @@ impl NodeNetworkInterface { .find_map(|(input_index, click_target)| if index == input_index { click_target.bounding_box_center() } else { None }) } - pub fn newly_loaded_input_wire(&mut self, input: &InputConnector, graph_wire_style: GraphWireStyle, network_path: &[NodeId]) -> Option { - if !self.wire_is_loaded(input, network_path) { - self.load_wire(input, graph_wire_style, network_path); - } else { - return None; - } - - let wire = match input { - InputConnector::Node { node_id, input_index } => { - let input_metadata = self.transient_input_metadata(node_id, *input_index, network_path)?; - let TransientMetadata::Loaded(wire) = &input_metadata.wire else { - log::error!("Could not load wire for input: {input:?}"); - return None; - }; - wire.clone() - } - InputConnector::Export(export_index) => { - let network_metadata = self.network_metadata(network_path)?; - let Some(TransientMetadata::Loaded(wire)) = network_metadata.transient_metadata.wires.get(*export_index) else { - log::error!("Could not load wire for input: {input:?}"); - return None; - }; - wire.clone() - } - }; - Some(wire) - } - - pub fn wire_is_loaded(&mut self, input: &InputConnector, network_path: &[NodeId]) -> bool { - match input { - InputConnector::Node { node_id, input_index } => { - let Some(input_metadata) = self.transient_input_metadata(node_id, *input_index, network_path) else { - log::error!("Input metadata should always exist for input"); - return false; - }; - input_metadata.wire.is_loaded() - } - InputConnector::Export(export_index) => { - let Some(network_metadata) = self.network_metadata(network_path) else { - return false; - }; - match network_metadata.transient_metadata.wires.get(*export_index) { - Some(wire) => wire.is_loaded(), - None => false, - } - } - } - } - - fn load_wire(&mut self, input: &InputConnector, graph_wire_style: GraphWireStyle, network_path: &[NodeId]) { - let dashed = match self.previewing(network_path) { - Previewing::Yes { .. } => match input { - InputConnector::Node { .. } => false, - InputConnector::Export(export_index) => *export_index == 0, - }, - Previewing::No => false, - }; - let Some(wire) = self.wire_path_from_input(input, graph_wire_style, dashed, network_path) else { - log::error!("Could not load wire path from input"); - return; - }; - match input { - InputConnector::Node { node_id, input_index } => { - let Some(node_metadata) = self.node_metadata_mut(node_id, network_path) else { return }; - let Some(input_metadata) = node_metadata.persistent_metadata.input_metadata.get_mut(*input_index) else { - // log::warn!("Node metadata must exist on node: {input:?}"); - return; - }; - let wire_update = WirePathUpdate { - id: *node_id, - input_index: *input_index, - wire_path_update: Some(wire), - }; - input_metadata.transient_metadata.wire = TransientMetadata::Loaded(wire_update); - } - InputConnector::Export(export_index) => { - let Some(network_metadata) = self.network_metadata_mut(network_path) else { return }; - if *export_index >= network_metadata.transient_metadata.wires.len() { - network_metadata.transient_metadata.wires.resize(export_index + 1, TransientMetadata::Unloaded); - } - let Some(input_metadata) = network_metadata.transient_metadata.wires.get_mut(*export_index) else { - return; - }; - let wire_update = WirePathUpdate { - id: NodeId(u64::MAX), - input_index: *export_index, - wire_path_update: Some(wire), - }; - *input_metadata = TransientMetadata::Loaded(wire_update); - } - } - } - pub fn all_input_connectors(&self, network_path: &[NodeId]) -> Vec { let mut input_connectors = Vec::new(); let Some(network) = self.nested_network(network_path) else { @@ -2097,141 +1909,6 @@ impl NodeNetworkInterface { .collect() } - /// Maps to the frontend representation of a wire start. Includes disconnected value wire inputs. - pub fn node_graph_wire_inputs(&self, network_path: &[NodeId]) -> Vec<(NodeId, usize)> { - self.node_graph_input_connectors(network_path) - .iter() - .map(|input| match input { - InputConnector::Node { node_id, input_index } => (*node_id, *input_index), - InputConnector::Export(export_index) => (NodeId(u64::MAX), *export_index), - }) - .chain(std::iter::once((NodeId(u64::MAX), u32::MAX as usize))) - .collect() - } - - fn unload_wires_for_node(&mut self, node_id: &NodeId, network_path: &[NodeId]) { - let number_of_outputs = self.number_of_outputs(node_id, network_path); - let Some(outward_wires) = self.outward_wires(network_path) else { - log::error!("Could not get outward wires in reorder_export"); - return; - }; - let mut input_connectors = Vec::new(); - for output_index in 0..number_of_outputs { - let Some(inputs) = outward_wires.get(&OutputConnector::node(*node_id, output_index)) else { - continue; - }; - input_connectors.extend(inputs.clone()) - } - for input_index in 0..self.number_of_inputs(node_id, network_path) { - input_connectors.push(InputConnector::node(*node_id, input_index)); - } - for input in input_connectors { - self.unload_wire(&input, network_path); - } - } - - pub fn unload_wire(&mut self, input: &InputConnector, network_path: &[NodeId]) { - match input { - InputConnector::Node { node_id, input_index } => { - let Some(node_metadata) = self.node_metadata_mut(node_id, network_path) else { - return; - }; - let Some(input_metadata) = node_metadata.persistent_metadata.input_metadata.get_mut(*input_index) else { - // log::warn!("Node metadata must exist on node: {input:?}"); - return; - }; - input_metadata.transient_metadata.wire = TransientMetadata::Unloaded; - } - InputConnector::Export(export_index) => { - let Some(network_metadata) = self.network_metadata_mut(network_path) else { - return; - }; - if *export_index >= network_metadata.transient_metadata.wires.len() { - network_metadata.transient_metadata.wires.resize(export_index + 1, TransientMetadata::Unloaded); - } - let Some(input_metadata) = network_metadata.transient_metadata.wires.get_mut(*export_index) else { - return; - }; - *input_metadata = TransientMetadata::Unloaded; - } - } - } - - /// When previewing, there may be a second path to the root node. - pub fn wire_to_root(&mut self, graph_wire_style: GraphWireStyle, network_path: &[NodeId]) -> Option { - let input = InputConnector::Export(0); - let current_export = self.upstream_output_connector(&input, network_path)?; - - let root_node = match self.previewing(network_path) { - Previewing::Yes { root_node_to_restore } => root_node_to_restore, - Previewing::No => None, - }?; - - if Some(root_node.node_id) == current_export.node_id() { - return None; - } - let Some(input_position) = self.get_input_center(&input, network_path) else { - log::error!("Could not get input position for wire end in root node: {input:?}"); - return None; - }; - let upstream_output = OutputConnector::node(root_node.node_id, root_node.output_index); - let Some(output_position) = self.get_output_center(&upstream_output, network_path) else { - log::error!("Could not get output position for wire start in root node: {upstream_output:?}"); - return None; - }; - let vertical_end = input.node_id().is_some_and(|node_id| self.is_layer(&node_id, network_path) && input.input_index() == 0); - let vertical_start: bool = upstream_output.node_id().is_some_and(|node_id| self.is_layer(&node_id, network_path)); - let thick = vertical_end && vertical_start; - 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::displayed_type(&self.input_type(&input, network_path)); - let wire_path_update = Some(WirePath { - path_string, - data_type, - thick, - dashed: false, - }); - - Some(WirePathUpdate { - id: NodeId(u64::MAX), - input_index: u32::MAX as usize, - wire_path_update, - }) - } - - /// Returns the vector subpath and a boolean of whether the wire should be thick. - pub fn vector_wire_from_input(&mut self, input: &InputConnector, wire_style: GraphWireStyle, network_path: &[NodeId]) -> Option<(BezPath, bool)> { - let Some(input_position) = self.get_input_center(input, network_path) else { - log::error!("Could not get dom rect for wire end: {input:?}"); - return None; - }; - // An upstream output could not be found, so the wire does not exist, but it should still be loaded as as empty vector - let Some(upstream_output) = self.upstream_output_connector(input, network_path) else { - return Some((BezPath::new(), false)); - }; - let Some(output_position) = self.get_output_center(&upstream_output, network_path) else { - log::error!("Could not get output port for wire start: {:?}", upstream_output); - return None; - }; - let vertical_end = input.node_id().is_some_and(|node_id| self.is_layer(&node_id, network_path) && input.input_index() == 0); - let vertical_start = upstream_output.node_id().is_some_and(|node_id| self.is_layer(&node_id, network_path)); - let thick = vertical_end && vertical_start; - Some((build_vector_wire(output_position, input_position, vertical_start, vertical_end, wire_style), thick)) - } - - 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::displayed_type(&self.input_type(input, network_path)); - Some(WirePath { - path_string, - data_type, - thick, - dashed, - }) - } - pub fn node_click_targets(&mut self, node_id: &NodeId, network_path: &[NodeId]) -> Option<&DocumentNodeClickTargets> { self.try_load_node_click_targets(node_id, network_path); self.try_get_node_click_targets(node_id, network_path) @@ -2469,7 +2146,6 @@ impl NodeNetworkInterface { return; }; node_metadata.transient_metadata.click_targets.unload(); - self.unload_wires_for_node(node_id, network_path); } pub fn unload_upstream_node_click_targets(&mut self, node_ids: Vec, network_path: &[NodeId]) { @@ -2561,33 +2237,6 @@ impl NodeNetworkInterface { let rect = Subpath::::new_rect(bounds[0], bounds[1]); let all_nodes_bounding_box = rect.to_bezpath().to_svg(); - let Some(rounded_network_edge_distance) = self.rounded_network_edge_distance(network_path).cloned() else { - log::error!("Could not get rounded_network_edge_distance in collect_frontend_click_targets"); - return FrontendClickTargets::default(); - }; - let Some(network_metadata) = self.network_metadata(network_path) else { - log::error!("Could not get nested network_metadata in collect_frontend_click_targets"); - return FrontendClickTargets::default(); - }; - let import_exports_viewport_top_left = rounded_network_edge_distance.imports_to_edge_distance; - let import_exports_viewport_bottom_right = rounded_network_edge_distance.exports_to_edge_distance; - - let node_graph_top_left = network_metadata - .persistent_metadata - .navigation_metadata - .node_graph_to_viewport - .inverse() - .transform_point2(import_exports_viewport_top_left); - let node_graph_bottom_right = network_metadata - .persistent_metadata - .navigation_metadata - .node_graph_to_viewport - .inverse() - .transform_point2(import_exports_viewport_bottom_right); - - let import_exports_target = Subpath::::new_rect(node_graph_top_left, node_graph_bottom_right); - let import_exports_bounding_box = import_exports_target.to_bezpath().to_svg(); - let mut modify_import_export = Vec::new(); if let Some(modify_import_export_click_targets) = self.modify_import_export(network_path) { for click_target in modify_import_export_click_targets @@ -2606,7 +2255,6 @@ impl NodeNetworkInterface { connector_click_targets, icon_click_targets, all_nodes_bounding_box, - import_exports_bounding_box, modify_import_export, } } @@ -2853,26 +2501,6 @@ impl NodeNetworkInterface { bounding_box_subpath.bounding_box_with_transform(network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport) } - // 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(); - }; - let nodes = network_metadata - .persistent_metadata - .node_metadata - .iter() - .filter_map(|(node_id, _)| if self.is_layer(node_id, network_path) { Some(*node_id) } else { None }) - .collect::>(); - let layer_widths = nodes - .iter() - .filter_map(|node_id| self.layer_width(node_id, network_path).map(|layer_width| (*node_id, layer_width))) - .collect::>(); - - layer_widths - } - pub fn compute_modified_vector(&self, layer: LayerNodeIdentifier) -> Option { let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, self); @@ -3047,18 +2675,6 @@ impl NodeNetworkInterface { self.unload_modify_import_export(network_path); } - // This should be run whenever the pan ends, a zoom occurs, or the network is opened - pub fn set_grid_aligned_edges(&mut self, node_graph_top_right: DVec2, network_path: &[NodeId]) { - let Some(network_metadata) = self.network_metadata_mut(network_path) else { - log::error!("Could not get nested network_metadata in set_grid_aligned_edges"); - return; - }; - network_metadata.persistent_metadata.navigation_metadata.node_graph_top_right = node_graph_top_right; - self.unload_rounded_network_edge_distance(network_path); - self.unload_import_export_ports(network_path); - self.unload_modify_import_export(network_path); - } - pub fn vector_modify(&mut self, node_id: &NodeId, modification_type: VectorModificationType) { let Some(node) = self.network_mut(&[]).unwrap().nodes.get_mut(node_id) else { log::error!("Could not get node in vector_modification"); @@ -3792,17 +3408,14 @@ impl NodeNetworkInterface { // If a connection is made to the imports (NodeInput::Value { .. } | NodeInput::Scope { .. } | NodeInput::Inline { .. }, NodeInput::Network { .. }) => { self.unload_outward_wires(network_path); - self.unload_wire(input_connector, network_path); } // If a connection to the imports is disconnected (NodeInput::Network { .. }, NodeInput::Value { .. } | NodeInput::Scope { .. } | NodeInput::Inline { .. }) => { self.unload_outward_wires(network_path); - self.unload_wire(input_connector, network_path); } // If a node is disconnected. (NodeInput::Node { .. }, NodeInput::Value { .. } | NodeInput::Scope { .. } | NodeInput::Inline { .. }) => { self.unload_outward_wires(network_path); - self.unload_wire(input_connector, network_path); if let Some((old_upstream_node_id, previous_position)) = previous_metadata { let old_upstream_node_is_layer = self.is_layer(&old_upstream_node_id, network_path); @@ -5562,7 +5175,7 @@ impl InputConnector { } /// Represents an output connector -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)] +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum OutputConnector { #[serde(rename = "node")] Node { @@ -5846,11 +5459,6 @@ pub struct NodeNetworkTransientMetadata { pub import_export_ports: TransientMetadata, /// Click targets for adding, removing, and moving import/export ports pub modify_import_export: TransientMetadata, - // Distance to the edges of the network, where the import/export ports are displayed. Rounded to nearest grid space when the panning ends. - pub rounded_network_edge_distance: TransientMetadata, - - // Wires from the exports - pub wires: Vec>, } #[derive(Debug, Clone)] @@ -6029,12 +5637,11 @@ impl InputPersistentMetadata { } } -#[derive(Debug, Clone, Default)] -struct InputTransientMetadata { - wire: TransientMetadata, - // downstream_protonode: populated for all inputs after each compile - // types: populated for each protonode after each -} +// #[derive(Debug, Clone, Default)] +// struct InputTransientMetadata { +// // downstream_protonode: populated for all inputs after each compile +// // types: populated for each protonode after each +// } /// Persistent metadata for each node in the network, which must be included when creating, serializing, and deserializing saving a node. #[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] @@ -6073,15 +5680,15 @@ impl DocumentNodePersistentMetadata { #[derive(Debug, Default, serde::Serialize, serde::Deserialize)] pub struct InputMetadata { pub persistent_metadata: InputPersistentMetadata, - #[serde(skip)] - transient_metadata: InputTransientMetadata, + // #[serde(skip)] + // transient_metadata: InputTransientMetadata, } impl Clone for InputMetadata { fn clone(&self) -> Self { InputMetadata { persistent_metadata: self.persistent_metadata.clone(), - transient_metadata: Default::default(), + // transient_metadata: Default::default(), } } } 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 60a3357725..5d459fcf8a 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 @@ -1,20 +1,25 @@ use glam::{DVec2, IVec2}; use graph_craft::proto::GraphErrors; use graphene_std::uuid::NodeId; +use kurbo::BezPath; 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, FrontendLayer, FrontendNode, FrontendNodeMetadata, FrontendNodeOrLayer, FrontendNodeToRender, FrontendXY, + FrontendExport, FrontendExports, FrontendGraphDataType, FrontendGraphInput, FrontendGraphOutput, FrontendImport, FrontendLayer, FrontendNode, FrontendNodeMetadata, FrontendNodeOrLayer, + FrontendNodeToRender, FrontendXY, + }, + utility_types::{ + network_interface::{FlowType, InputConnector, NodeNetworkInterface, OutputConnector, Previewing}, + wires::{GraphWireStyle, build_vector_wire}, }, - 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, wire_style: GraphWireStyle, 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(); @@ -85,21 +90,46 @@ impl NodeNetworkInterface { 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)) + let primary_input = self.frontend_input_from_connector(&InputConnector::node(node_id, 0), network_path); + let secondary_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 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)) + let primary_output = self.frontend_output_from_connector(&OutputConnector::node(node_id, 0), network_path); + let secondary_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 node = Some(FrontendNode { position, inputs, outputs }); + let node = Some(FrontendNode { + position, + primary_input, + primary_output, + secondary_inputs, + secondary_outputs, + }); FrontendNodeOrLayer { node, layer: None } } }; - let frontend_node_to_render = FrontendNodeToRender { metadata, node_or_layer }; + let wires = (0..self.number_of_displayed_inputs(&node_id, network_path)) + .filter_map(|input_index| { + self.wire_from_input(&InputConnector::node(node_id, input_index), wire_style, network_path) + .filter(|_| { + self.upstream_output_connector(&InputConnector::node(node_id, input_index), network_path) + .is_some_and(|output| !matches!(output, OutputConnector::Import(_))) + }) + .map(|wire| { + ( + wire, + self.wire_is_thick(&InputConnector::node(node_id, input_index), network_path), + FrontendGraphDataType::displayed_type(&self.input_type(&InputConnector::node(node_id, input_index), network_path)), + ) + }) + }) + .collect(); + + let frontend_node_to_render = FrontendNodeToRender { metadata, node_or_layer, wires }; nodes.push(frontend_node_to_render); } @@ -113,7 +143,7 @@ impl NodeNetworkInterface { return None; } let input_type = self.input_type(input_connector, network_path); - let data_type = FrontendGraphDataType::displayed_type(&input_type); + let data_type = input_type.displayed_type(); let resolved_type = input_type.resolved_type_name(); let connected_to = self @@ -216,7 +246,7 @@ impl NodeNetworkInterface { (import_name, description) } }; - let data_type = FrontendGraphDataType::displayed_type(&output_type); + let data_type = output_type.displayed_type(); let resolved_type = output_type.resolved_type_name(); let mut connected_to = self .outward_wires(network_path) @@ -318,16 +348,35 @@ impl NodeNetworkInterface { .is_some_and(|node_id| self.is_layer(&node_id, network_path)) } - pub fn frontend_imports(&mut self, network_path: &[NodeId]) -> Vec> { + /// The imports contain both the output port and the outward wires + pub fn frontend_imports(&mut self, graph_wire_style: GraphWireStyle, 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:?}"); + 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)) + .map(|import_index| { + let port = self.frontend_output_from_connector(&OutputConnector::Import(import_index), network_path); + port.and_then(|port| { + let outward_wires = self.outward_wires(network_path)?; + let downstream_inputs = outward_wires.get(&OutputConnector::Import(import_index)).cloned()?; + let wires = downstream_inputs + .iter() + .filter_map(|input_connector| { + let Some(wire) = self.wire_from_input(&input_connector, graph_wire_style, network_path) else { + log::error!("Could not get wire path for import input: {input_connector:?}"); + return None; + }; + Some(wire.to_svg()) + }) + .collect::>(); + Some(FrontendImport { port, wires }) + }) + }) .collect::>(); + if frontend_imports.is_empty() { frontend_imports.push(None); } @@ -338,13 +387,29 @@ impl NodeNetworkInterface { } } - 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); + /// The imports contain the export port, the outward wires, and the preview wire if it exists + pub fn frontend_exports(&mut self, graph_wire_style: GraphWireStyle, network_path: &[NodeId]) -> FrontendExports { + let Some(network) = self.nested_network(network_path) else { + log::error!("Could not get nested network in frontend exports"); + return FrontendExports::default(); + }; + let mut exports = (0..network.exports.len()) + .map(|export_index| { + let export_connector = InputConnector::Export(export_index); + let frontend_export = self.frontend_input_from_connector(&export_connector, network_path); + + frontend_export.and_then(|export| { + let wire = self.wire_from_input(&export_connector, graph_wire_style, network_path).map(|path| path.to_svg()); + Some(FrontendExport { port: export, wire }) + }) + }) + .collect::>(); + + if exports.is_empty() { + exports.push(None); } - frontend_exports + let preview_wire = self.wire_to_root(graph_wire_style, network_path).map(|wire| wire.to_svg()); + FrontendExports { exports, preview_wire } } pub fn import_export_position(&mut self, network_path: &[NodeId]) -> Option<(IVec2, IVec2)> { @@ -424,4 +489,60 @@ impl NodeNetworkInterface { Some((rounded_import_top_left.as_ivec2(), rounded_export_top_right.as_ivec2())) } + + pub fn wire_is_thick(&self, input: &InputConnector, network_path: &[NodeId]) -> bool { + let Some(upstream_output) = self.upstream_output_connector(input, network_path) else { + return false; + }; + let vertical_end = input.node_id().is_some_and(|node_id| self.is_layer(&node_id, network_path) && input.input_index() == 0); + let vertical_start = upstream_output.node_id().is_some_and(|node_id| self.is_layer(&node_id, network_path)); + vertical_end && vertical_start + } + + /// Returns the vector subpath and a boolean of whether the wire should be thick. + pub fn wire_from_input(&mut self, input: &InputConnector, wire_style: GraphWireStyle, network_path: &[NodeId]) -> Option { + let Some(input_position) = self.get_input_center(input, network_path) else { + log::error!("Could not get dom rect for wire end: {input:?}"); + return None; + }; + // An upstream output could not be found + let Some(upstream_output) = self.upstream_output_connector(input, network_path) else { + return None; + }; + let Some(output_position) = self.get_output_center(&upstream_output, network_path) else { + log::error!("Could not get output port for wire start: {:?}", upstream_output); + return None; + }; + let vertical_end = input.node_id().is_some_and(|node_id| self.is_layer(&node_id, network_path) && input.input_index() == 0); + let vertical_start = upstream_output.node_id().is_some_and(|node_id| self.is_layer(&node_id, network_path)); + Some(build_vector_wire(output_position, input_position, vertical_start, vertical_end, wire_style)) + } + + /// When previewing, there may be a second path to the root node. + pub fn wire_to_root(&mut self, graph_wire_style: GraphWireStyle, network_path: &[NodeId]) -> Option { + let input = InputConnector::Export(0); + let current_export = self.upstream_output_connector(&input, network_path)?; + + let root_node = match self.previewing(network_path) { + Previewing::Yes { root_node_to_restore } => root_node_to_restore, + Previewing::No => None, + }?; + + if Some(root_node.node_id) == current_export.node_id() { + return None; + } + let Some(input_position) = self.get_input_center(&input, network_path) else { + log::error!("Could not get input position for wire end in root node: {input:?}"); + return None; + }; + let upstream_output = OutputConnector::node(root_node.node_id, root_node.output_index); + let Some(output_position) = self.get_output_center(&upstream_output, network_path) else { + log::error!("Could not get output position for wire start in root node: {upstream_output:?}"); + return None; + }; + let vertical_start = upstream_output.node_id().is_some_and(|node_id| self.is_layer(&node_id, network_path)); + let vector_wire = build_vector_wire(output_position, input_position, vertical_start, false, graph_wire_style); + + Some(vector_wire) + } } 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 1e099c7ca1..bc04dc1bc7 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 @@ -4,7 +4,7 @@ use graph_craft::{ ProtoNodeIdentifier, Type, concrete, document::{DocumentNodeImplementation, InlineRust, NodeInput, value::TaggedValue}, }; -use graphene_std::uuid::NodeId; +use graphene_std::{node_graph_overlay::types::FrontendGraphDataType, uuid::NodeId}; use interpreted_executor::{ dynamic_executor::{NodeTypes, ResolvedDocumentNodeTypesDelta}, node_registry::NODE_REGISTRY, @@ -45,6 +45,31 @@ pub enum TypeSource { } impl TypeSource { + pub fn displayed_type(&self) -> FrontendGraphDataType { + match self.compiled_nested_type() { + Some(nested_type) => match TaggedValue::from_type_or_none(nested_type) { + TaggedValue::U32(_) + | TaggedValue::U64(_) + | TaggedValue::F32(_) + | TaggedValue::F64(_) + | TaggedValue::DVec2(_) + | TaggedValue::F64Array4(_) + | TaggedValue::VecF64(_) + | TaggedValue::VecDVec2(_) + | TaggedValue::DAffine2(_) => FrontendGraphDataType::Number, + TaggedValue::Artboard(_) => FrontendGraphDataType::Artboard, + TaggedValue::Graphic(_) => FrontendGraphDataType::Graphic, + TaggedValue::Raster(_) => FrontendGraphDataType::Raster, + TaggedValue::Vector(_) => FrontendGraphDataType::Vector, + TaggedValue::Color(_) => FrontendGraphDataType::Color, + TaggedValue::Gradient(_) | TaggedValue::GradientStops(_) | TaggedValue::GradientTable(_) => FrontendGraphDataType::Gradient, + TaggedValue::String(_) => FrontendGraphDataType::Typography, + _ => FrontendGraphDataType::General, + }, + None => FrontendGraphDataType::General, + } + } + pub fn into_compiled_nested_type(self) -> Option { match self { TypeSource::Compiled(compiled_type) => Some(compiled_type.into_nested_type()), diff --git a/editor/src/messages/portfolio/document/utility_types/nodes.rs b/editor/src/messages/portfolio/document/utility_types/nodes.rs index c120938a80..f262e4bdb5 100644 --- a/editor/src/messages/portfolio/document/utility_types/nodes.rs +++ b/editor/src/messages/portfolio/document/utility_types/nodes.rs @@ -62,7 +62,7 @@ pub struct LayerPanelEntry { } /// IMPORTANT: the same node may appear multiple times. -#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, PartialEq, Eq, specta::Type)] +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, PartialEq, Eq)] pub struct SelectedNodes(pub Vec); impl SelectedNodes { @@ -172,5 +172,5 @@ impl SelectedNodes { } } -#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, PartialEq, Eq, specta::Type)] +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, PartialEq, Eq)] pub struct CollapsedLayers(pub Vec); diff --git a/editor/src/messages/portfolio/document/utility_types/wires.rs b/editor/src/messages/portfolio/document/utility_types/wires.rs index ad6ab32843..a29c84c8e8 100644 --- a/editor/src/messages/portfolio/document/utility_types/wires.rs +++ b/editor/src/messages/portfolio/document/utility_types/wires.rs @@ -1,26 +1,13 @@ -use crate::messages::portfolio::document::node_graph::utility_types::FrontendGraphDataType; use glam::{DVec2, IVec2}; -use graphene_std::{uuid::NodeId, vector::misc::dvec2_to_point}; +use graphene_std::vector::misc::dvec2_to_point; use kurbo::{BezPath, DEFAULT_ACCURACY, Line, Point, Shape}; #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] -pub struct WirePath { - #[serde(rename = "pathString")] - pub path_string: String, +pub struct WirePathInProgress { + pub wire: String, #[serde(rename = "dataType")] pub data_type: FrontendGraphDataType, pub thick: bool, - pub dashed: bool, -} - -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] -pub struct WirePathUpdate { - pub id: NodeId, - #[serde(rename = "inputIndex")] - pub input_index: usize, - // If none, then remove the wire from the map - #[serde(rename = "wirePathUpdate")] - pub wire_path_update: Option, } #[derive(Copy, Clone, Debug, PartialEq, Default, serde::Serialize, serde::Deserialize, specta::Type)] @@ -67,8 +54,19 @@ pub fn build_vector_wire(output_position: DVec2, input_position: DVec2, vertical let horizontal_curve = horizontal_curve_amount * curve_length; let vertical_curve = vertical_curve_amount * curve_length; + let wire_start = if vertical_in && vertical_out { output_position + DVec2::new(0., -4.) } else { output_position }; + let wire_end = if vertical_in && vertical_in { + DVec2::new(input_position.x, input_position.y) + DVec2::new(0., 4.) + } else { + DVec2::new(input_position.x, input_position.y) + }; + + if vertical_in && vertical_in && wire_end.y + 5. > wire_start.y { + return BezPath::new(); + } + let locations = [ - output_position, + wire_start, DVec2::new( if vertical_out { output_position.x } else { output_position.x + horizontal_curve }, if vertical_out { output_position.y - vertical_curve } else { output_position.y }, @@ -77,7 +75,7 @@ pub fn build_vector_wire(output_position: DVec2, input_position: DVec2, vertical if vertical_in { input_position.x } else { input_position.x - horizontal_curve }, if vertical_in { input_position.y + vertical_curve } else { input_position.y }, ), - DVec2::new(input_position.x, input_position.y), + wire_end, ]; let smoothing = 0.5; diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index c83346c885..39dec1e08a 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -909,8 +909,6 @@ impl MessageHandler> for Portfolio responses.add(DocumentMessage::GraphViewOverlay { open: node_graph_open }); if node_graph_open { responses.add(NodeGraphMessage::UpdateGraphBarRight); - responses.add(NodeGraphMessage::UnloadWires); - responses.add(NodeGraphMessage::SendWires) } else { responses.add(PortfolioMessage::UpdateDocumentWidgets); } diff --git a/editor/src/messages/preferences/preferences_message_handler.rs b/editor/src/messages/preferences/preferences_message_handler.rs index 214903b9b9..9081376669 100644 --- a/editor/src/messages/preferences/preferences_message_handler.rs +++ b/editor/src/messages/preferences/preferences_message_handler.rs @@ -87,8 +87,7 @@ impl MessageHandler for PreferencesMessageHandler { } PreferencesMessage::GraphWireStyle { style } => { self.graph_wire_style = style; - responses.add(NodeGraphMessage::UnloadWires); - responses.add(NodeGraphMessage::SendWires); + responses.add(NodeGraphMessage::SendGraph); } PreferencesMessage::ViewportZoomWheelRate { rate } => { self.viewport_zoom_wheel_rate = rate; diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 84b0975ac4..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)] @@ -115,10 +121,17 @@ impl NodeGraphExecutor { /// Update the cached network if necessary. fn update_node_graph(&mut self, document: &mut DocumentMessageHandler, node_to_inspect: Option, ignore_hash: bool) -> Result<(), String> { - let network_hash = document.network_interface.document_network().current_hash(); + let mut network = document.network_interface.document_network().clone(); + if let Some(mut node_graph_overlay_node) = document.node_graph_handler.node_graph_overlay.clone() { + let node_graph_overlay_id = NodeId::new(); + let new_export = NodeInput::node(node_graph_overlay_id, 0); + let old_export = std::mem::replace(&mut network.exports[0], new_export); + node_graph_overlay_node.inputs[0] = old_export; + network.nodes.insert(node_graph_overlay_id, node_graph_overlay_node); + } + let network_hash = network.current_hash(); // Refresh the graph when it changes or the inspect node changes if network_hash != self.node_graph_hash || self.previous_node_to_inspect != node_to_inspect || ignore_hash { - let network = document.network_interface.document_network().clone(); self.previous_node_to_inspect = node_to_inspect; self.node_graph_hash = network_hash; @@ -253,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 { @@ -270,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())?; @@ -284,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/editor/src/test_utils.rs b/editor/src/test_utils.rs index f917a8b003..32ce75e835 100644 --- a/editor/src/test_utils.rs +++ b/editor/src/test_utils.rs @@ -301,13 +301,7 @@ pub trait FrontendMessageTestUtils { impl FrontendMessageTestUtils for FrontendMessage { fn check_node_graph_error(&self) { - let FrontendMessage::UpdateNodeGraphRender { nodes_to_render, .. } = self else { return }; - - for node in nodes_to_render { - if let Some(error) = &node.metadata.errors { - panic!("error on {}: {}", node.metadata.display_name, error); - } - } + // TODO: Implement } } 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/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 0a58272291..5079189ce7 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -564,8 +564,9 @@ {/if} - - +
+ +
editor.handle.panCanvasByFraction(detail, 0)} on:thumbPosition={({ detail }) => panCanvasX(detail)} - on:thumbDragEnd={() => editor.handle.setGridAlignedEdges()} on:thumbDragStart={() => editor.handle.panCanvasAbortPrepare(true)} on:thumbDragAbort={() => editor.handle.panCanvasAbort(true)} /> @@ -823,6 +823,18 @@ } } + .graph-view { + pointer-events: none; + transition: opacity 0.2s; + opacity: 0; + + &.open { + cursor: auto; + pointer-events: auto; + opacity: 1; + } + } + .fade-artwork, .graph { position: absolute; diff --git a/frontend/src/components/views/Graph.svelte b/frontend/src/components/views/Graph.svelte index 7bb48019f0..73da5c4f48 100644 --- a/frontend/src/components/views/Graph.svelte +++ b/frontend/src/components/views/Graph.svelte @@ -223,577 +223,527 @@ } -
-
- - {#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} -
- - {#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} +
{@html $nodeGraph.nativeNodeGraphSVGString}
+ +
+ + {#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} +
- {#each $nodeGraph.wires.values() as map} - {#each map.values() as { pathString, dataType, thick, dashed }} - {#if thick} - - {/if} - {/each} + {#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} - -
- {#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 $nodeGraph.updateImportsExports.exports as frontendInput, index} - {#if frontendInput} - - {inputTooltip(frontendInput)} - {#if frontendInput.connectedTo !== "nothing"} - - {:else} - - {/if} + + + {#if $nodeGraph.wirePathInProgress} + + {/if} + + + +
+ {#if $nodeGraph.updateImportsExports} + {#each $nodeGraph.updateImportsExports.imports as frontendImport, index} + {#if frontendImport} + {@const frontendOutput = frontendImport.port} + + {#each frontendImport.wires as wire} + + -
(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} + {/each} + + {outputTooltip(frontendOutput)} + {#if frontendOutput.connectedTo.length > 0} + + {:else} + + {/if} + - {#if $nodeGraph.updateImportsExports.addImportExport == true}
(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} > - editor.handle.addSecondaryImport()} /> + {#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.addSecondaryExport()} /> + editor.handle.addPrimaryImport()} />
{/if} + {/each} - {#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} - {/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} + - {#if nodeMetadata.errors} - {layer.errors} - {layer.errors} + {inputTooltip(frontendInput)} + {#if frontendInput.connectedTo !== "nothing"} + + {:else} + {/if} -
- {#if $nodeGraph.thumbnails.has(nodeId)} - {@html $nodeGraph.thumbnails.get(nodeId)} + +
(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} - - - {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 layer.sideInput} -
- - {inputTooltip(layer.sideInput)} - - -
+ { + /* 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} -
- - {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} + {:else}
- {#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} -
- - - - - - - + editor.handle.addPrimaryExport()} />
{/if} {/each} -
-
- - {#if $nodeGraph.box} -
- {/if} + {#if $nodeGraph.updateImportsExports.exports.previewWire && $nodeGraph.updateImportsExports.exports.exports[0] !== undefined} + + + + {/if} + + {#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.reorderExportIndex !== undefined} + {@const position = { + x: Number($nodeGraph.updateImportsExports.exportPosition.x), + y: Number($nodeGraph.updateImportsExports.exportPosition.y) + Number($nodeGraph.reorderExportIndex) * 24, + }} +
+ {/if} + {/if} +
- diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index 598727737b..656cb9863c 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -26,13 +26,36 @@ export type XY = { x: number; y: number }; // ============================================================================ export class UpdateBox extends JsMessage { - readonly box!: Box | undefined; + readonly box!: FrontendSelectionBox | undefined; +} + +export class FrontendClickTargets { + readonly nodeClickTargets!: string[]; + readonly layerClickTargets!: string[]; + readonly connectorClickTargets!: string[]; + readonly iconClickTargets!: string[]; + readonly allNodesBoundingBox!: string; + readonly modifyImportExport!: string[]; } export class UpdateClickTargets extends JsMessage { readonly clickTargets!: FrontendClickTargets | undefined; } +export class FrontendSelectionBox { + readonly startX!: number; + + readonly startY!: number; + + readonly endX!: number; + + readonly endY!: number; +} + +export class UpdateNodeGraphSelectionBox extends JsMessage { + readonly box!: FrontendSelectionBox | undefined; +} + const ContextTupleToVec2 = Transform((data) => { if (data.obj.contextMenuInformation === undefined) return undefined; const contextMenuCoordinates = { x: data.obj.contextMenuInformation.contextMenuCoordinates[0], y: data.obj.contextMenuInformation.contextMenuCoordinates[1] }; @@ -45,15 +68,20 @@ const ContextTupleToVec2 = Transform((data) => { return { contextMenuCoordinates, contextMenuData }; }); +export class ContextMenuInformation { + readonly contextMenuCoordinates!: XY; + readonly contextMenuData!: "CreateNode" | { type: "CreateNode"; compatibleType: string } | { nodeId: bigint; currentlyIsNode: boolean }; +} + export class UpdateContextMenuInformation extends JsMessage { @ContextTupleToVec2 readonly contextMenuInformation!: ContextMenuInformation | undefined; } export class UpdateImportsExports extends JsMessage { - readonly imports!: (FrontendGraphOutput | undefined)[]; + readonly imports!: (FrontendImport | undefined)[]; - readonly exports!: (FrontendGraphInput | undefined)[]; + readonly exports!: FrontendExports; readonly importPosition!: XY; @@ -77,28 +105,10 @@ export class UpdateLayerWidths extends JsMessage { readonly layerWidths!: Map; } -export class UpdateNodeGraphRender extends JsMessage { - readonly nodesToRender!: FrontendNodeToRender[]; - - readonly open!: boolean; - - readonly opacity!: number; - - readonly inSelectedNetwork!: boolean; - - readonly previewedNode!: bigint | undefined; -} - -export class UpdateVisibleNodes extends JsMessage { - readonly nodes!: bigint[]; -} - -export class UpdateNodeGraphWires extends JsMessage { - readonly wires!: WireUpdate[]; +export class UpdateNativeNodeGraphSVG extends JsMessage { + readonly svgString!: string; } -export class ClearAllNodeGraphWires extends JsMessage {} - export class UpdateNodeGraphTransform extends JsMessage { readonly transform!: NodeGraphTransform; } @@ -123,8 +133,14 @@ export class UpdateOpenDocumentsList extends JsMessage { readonly openDocuments!: OpenDocument[]; } +export class WirePathInProgress { + readonly wire!: string; + readonly thick!: boolean; + readonly dataType!: FrontendGraphDataType; +} + export class UpdateWirePathInProgress extends JsMessage { - readonly wirePath!: WirePath | undefined; + readonly wirePathInProgress!: WirePathInProgress | undefined; } export class OpenDocument { @@ -149,6 +165,7 @@ export class DocumentDetails { } } +<<<<<<< HEAD export class Box { readonly startX!: number; @@ -174,6 +191,12 @@ export type ContextMenuInformation = { contextMenuData: "CreateNode" | { type: "CreateNode"; compatibleType: string } | { nodeId: bigint; currentlyIsNode: boolean }; }; +======= +export class FrontendDocumentDetails extends DocumentDetails { + readonly id!: bigint; +} + +>>>>>>> 17a1a3d5 (Complete separating node rendering from imports/exports) export type FrontendGraphDataType = "General" | "Number" | "Artboard" | "Graphic" | "Raster" | "Vector" | "Color"; export class FrontendGraphInput { @@ -202,6 +225,21 @@ export class FrontendGraphOutput { readonly connectedTo!: string[]; } +export class FrontendExport { + readonly port!: FrontendGraphInput; + readonly wire!: string | undefined; +} + +export class FrontendExports { + readonly exports!: (FrontendExport | undefined)[]; + readonly previewWire!: string | undefined; +} + +export class FrontendImport { + readonly port!: FrontendGraphOutput; + readonly wires!: string[]; +} + export class FrontendNodeMetadata { readonly nodeId!: bigint; @@ -268,6 +306,8 @@ export class FrontendNodeOrLayer { export class FrontendNodeToRender { readonly metadata!: FrontendNodeMetadata; readonly nodeOrLayer!: FrontendNodeOrLayer; + // TODO: Remove + readonly wires!: [string, boolean, FrontendGraphDataType][]; } export class UpdateCentralNodeGraph extends JsMessage { @@ -1647,7 +1687,6 @@ type JSMessageFactory = (data: any, wasm: WebAssembly.Memory, handle: EditorHand type MessageMaker = typeof JsMessage | JSMessageFactory; export const messageMakers: Record = { - ClearAllNodeGraphWires, DisplayDialog, DisplayDialogDismiss, DisplayDialogPanic, @@ -1676,7 +1715,6 @@ export const messageMakers: Record = { TriggerTextCopy, TriggerVisitLink, UpdateActiveDocument, - UpdateBox, UpdateClickTargets, UpdateContextMenuInformation, UpdateDialogButtons, @@ -1703,9 +1741,9 @@ export const messageMakers: Record = { UpdateMenuBarLayout, UpdateMouseCursor, UpdateNodeGraphControlBarLayout, - UpdateNodeGraphRender, + UpdateNativeNodeGraphSVG, UpdateNodeGraphTransform, - UpdateNodeGraphWires, + UpdateNodeGraphSelectionBox, UpdateNodeThumbnail, UpdateOpenDocumentsList, UpdatePlatform, @@ -1717,7 +1755,6 @@ export const messageMakers: Record = { UpdateToolOptionsLayout, UpdateToolShelfLayout, UpdateViewportHolePunch, - UpdateVisibleNodes, UpdateWirePathInProgress, UpdateWorkingColorsLayout, } as const; diff --git a/frontend/src/state-providers/node-graph.ts b/frontend/src/state-providers/node-graph.ts index 0ac31d467a..74762825df 100644 --- a/frontend/src/state-providers/node-graph.ts +++ b/frontend/src/state-providers/node-graph.ts @@ -2,52 +2,50 @@ import { writable } from "svelte/store"; import { type Editor } from "@graphite/editor"; import { - type Box, + type FrontendSelectionBox, type FrontendClickTargets, type ContextMenuInformation, type FrontendNodeToRender, type FrontendNodeType, - type WirePath, - ClearAllNodeGraphWires, + type WirePathInProgress, SendUIMetadata, - UpdateBox, UpdateClickTargets, UpdateContextMenuInformation, UpdateImportReorderIndex, UpdateExportReorderIndex, UpdateImportsExports, UpdateLayerWidths, - UpdateNodeGraphRender, - UpdateVisibleNodes, - UpdateNodeGraphWires, - UpdateNodeGraphTransform, + UpdateNativeNodeGraphSVG, UpdateNodeThumbnail, UpdateWirePathInProgress, + UpdateNodeGraphSelectionBox, + UpdateNodeGraphTransform, } from "@graphite/messages"; export function createNodeGraphState(editor: Editor) { const { subscribe, update } = writable({ - box: undefined as Box | undefined, + // Data that will continue to be rendered in Svelte for now + selectionBox: undefined as FrontendSelectionBox | undefined, clickTargets: undefined as FrontendClickTargets | undefined, - contextMenuInformation: undefined as ContextMenuInformation | undefined, - layerWidths: new Map(), + wirePathInProgress: undefined as WirePathInProgress | undefined, updateImportsExports: undefined as UpdateImportsExports | undefined, + reorderImportIndex: undefined as number | undefined, + reorderExportIndex: undefined as number | undefined, + nativeNodeGraphSVGString: "", + + contextMenuInformation: undefined as ContextMenuInformation | undefined, + nodeTypes: [] as FrontendNodeType[], + nodeDescriptions: new Map(), + + // Data that will be moved into the node graph to be rendered natively nodesToRender: new Map(), - open: false, opacity: 0.8, + inSelectedNetwork: true, + previewedNode: undefined as bigint | undefined, - visibleNodes: new Set(), - /// The index is the exposed input index. The exports have a first key value of u32::MAX. - wires: new Map>(), - wirePathInProgress: undefined as WirePath | undefined, - nodeDescriptions: new Map(), - nodeTypes: [] as FrontendNodeType[], + // Data that will be passed in the context thumbnails: new Map(), transform: { scale: 1, x: 0, y: 0 }, - inSelectedNetwork: true, - previewedNode: undefined as bigint | undefined, - reorderImportIndex: undefined as number | undefined, - reorderExportIndex: undefined as number | undefined, }); // Set up message subscriptions on creation @@ -58,15 +56,21 @@ export function createNodeGraphState(editor: Editor) { return state; }); }); - editor.subscriptions.subscribeJsMessage(UpdateBox, (updateBox) => { + editor.subscriptions.subscribeJsMessage(UpdateNodeGraphSelectionBox, (updateBox) => { + update((state) => { + state.selectionBox = updateBox.box; + return state; + }); + }); + editor.subscriptions.subscribeJsMessage(UpdateClickTargets, (updateClickTargets) => { update((state) => { - state.box = updateBox.box; + state.clickTargets = updateClickTargets.clickTargets; return state; }); }); - editor.subscriptions.subscribeJsMessage(UpdateClickTargets, (UpdateClickTargets) => { + editor.subscriptions.subscribeJsMessage(UpdateWirePathInProgress, (updateWirePathInProgress) => { update((state) => { - state.clickTargets = UpdateClickTargets.clickTargets; + state.wirePathInProgress = updateWirePathInProgress.wirePathInProgress; return state; }); }); @@ -95,52 +99,9 @@ export function createNodeGraphState(editor: Editor) { }); }); - editor.subscriptions.subscribeJsMessage(UpdateLayerWidths, (updateLayerWidths) => { - update((state) => { - state.layerWidths = updateLayerWidths.layerWidths; - return state; - }); - }); - editor.subscriptions.subscribeJsMessage(UpdateNodeGraphRender, (updateNodeGraphRender) => { - update((state) => { - state.nodesToRender.clear(); - updateNodeGraphRender.nodesToRender.forEach((node) => { - state.nodesToRender.set(node.metadata.nodeId, node); - }); - state.open = updateNodeGraphRender.open; - state.opacity = updateNodeGraphRender.opacity; - state.inSelectedNetwork = updateNodeGraphRender.inSelectedNetwork; - state.previewedNode = updateNodeGraphRender.previewedNode; - return state; - }); - }); - editor.subscriptions.subscribeJsMessage(UpdateVisibleNodes, (updateVisibleNodes) => { - update((state) => { - state.visibleNodes = new Set(updateVisibleNodes.nodes); - return state; - }); - }); - editor.subscriptions.subscribeJsMessage(UpdateNodeGraphWires, (updateNodeWires) => { - update((state) => { - updateNodeWires.wires.forEach((wireUpdate) => { - let inputMap = state.wires.get(wireUpdate.id); - // If it doesn't exist, create it and set it in the outer map - if (!inputMap) { - inputMap = new Map(); - state.wires.set(wireUpdate.id, inputMap); - } - if (wireUpdate.wirePathUpdate !== undefined) { - inputMap.set(wireUpdate.inputIndex, wireUpdate.wirePathUpdate); - } else { - inputMap.delete(wireUpdate.inputIndex); - } - }); - return state; - }); - }); - editor.subscriptions.subscribeJsMessage(ClearAllNodeGraphWires, (_) => { + editor.subscriptions.subscribeJsMessage(UpdateNativeNodeGraphSVG, (updateNativeNodeGraphRender) => { update((state) => { - state.wires.clear(); + state.nativeNodeGraphSVGString = updateNativeNodeGraphRender.svgString; return state; }); }); @@ -156,12 +117,6 @@ export function createNodeGraphState(editor: Editor) { return state; }); }); - editor.subscriptions.subscribeJsMessage(UpdateWirePathInProgress, (updateWirePathInProgress) => { - update((state) => { - state.wirePathInProgress = updateWirePathInProgress.wirePath; - return state; - }); - }); return { subscribe, diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index da1901815d..30e18ba831 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -785,13 +785,6 @@ impl EditorHandle { self.dispatch(message); } - /// Snaps the import/export edges to a grid space when the scroll bar is released - #[wasm_bindgen(js_name = setGridAlignedEdges)] - pub fn set_grid_aligned_edges(&self) { - let message = NodeGraphMessage::SetGridAlignedEdges; - self.dispatch(message); - } - /// Merge the selected nodes into a subnetwork #[wasm_bindgen(js_name = mergeSelectedNodes)] pub fn merge_nodes(&self) { 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/lib.rs b/node-graph/gcore/src/lib.rs index 586a76e0b6..c91e7e4fb4 100644 --- a/node-graph/gcore/src/lib.rs +++ b/node-graph/gcore/src/lib.rs @@ -17,6 +17,7 @@ pub mod logic; pub mod math; pub mod memo; pub mod misc; +pub mod node_graph_overlay; pub mod ops; pub mod raster; pub mod raster_types; diff --git a/node-graph/gcore/src/node_graph_overlay.rs b/node-graph/gcore/src/node_graph_overlay.rs new file mode 100644 index 0000000000..ac234a7664 --- /dev/null +++ b/node-graph/gcore/src/node_graph_overlay.rs @@ -0,0 +1,59 @@ +use graphene_core_shaders::Ctx; + +use crate::{ + Graphic, + node_graph_overlay::{ + background::generate_background, + nodes_and_wires::{draw_layers, draw_nodes, draw_wires}, + types::NodeGraphOverlayData, + ui_context::{UIContext, UIRuntimeResponse}, + }, + table::Table, + transform::ApplyTransform, +}; + +pub mod background; +pub mod consts; +pub mod nodes_and_wires; +pub mod types; +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(&mut node_graph_overlay_data); + nodes_and_wires.extend(layers); + + let wires = draw_wires(&mut node_graph_overlay_data.nodes_to_render); + nodes_and_wires.extend(wires); + + nodes_and_wires.extend(side_ports); + + let nodes = draw_nodes(&node_graph_overlay_data.nodes_to_render); + nodes_and_wires.extend(nodes); + + nodes_and_wires +} + +#[node_macro::node(skip_impl)] +pub fn transform_nodes(ui_context: UIContext, mut nodes: Table) -> Table { + let matrix = ui_context.transform.to_daffine2(); + nodes.left_apply_transform(&matrix); + nodes +} + +#[node_macro::node(skip_impl)] +pub fn dot_grid_background(ui_context: UIContext, opacity: f64) -> Table { + Table::new_from_element(Graphic::Vector(generate_background(ui_context, opacity))) +} + +#[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/background.rs b/node-graph/gcore/src/node_graph_overlay/background.rs new file mode 100644 index 0000000000..ccc7c49e5e --- /dev/null +++ b/node-graph/gcore/src/node_graph_overlay/background.rs @@ -0,0 +1,97 @@ +use graphene_core_shaders::color::Color; +use kurbo::{BezPath, Point}; + +use crate::{ + node_graph_overlay::{consts::*, ui_context::UIContext}, + table::{Table, TableRow}, + vector::{ + Vector, + style::{Fill, Stroke, StrokeCap}, + }, +}; + +pub fn generate_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 / 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(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); + bez_path.line_to(p2); + bez_path.line_to(p3); + bez_path.close_path(); + + let mut vector = Vector::from_bezpath(bez_path); + vector.style.fill = Fill::Solid(bg_color); + + let mut bg_table = Table::new_from_element(vector); + + let mut grid_spacing = ui_context.transform.scale * GRID_SIZE; + while grid_spacing > 0. && grid_spacing < GRID_COLLAPSE_SPACING { + grid_spacing *= 2.; + } + let grid_dot_radius = 1. + (ui_context.transform.scale - 0.5 + 0.001).floor() / 2.; + let grid_offset_left = (ui_context.transform.x % grid_spacing + grid_spacing) % grid_spacing; + let grid_offset_top = (ui_context.transform.y % grid_spacing + grid_spacing) % grid_spacing; + + // make sure we cover full screen (+1 avoids missing last col/row) + let number_of_rows = (ui_context.resolution.y as f64 / grid_spacing).ceil() as u32 + 1; + // for col in 0..number_of_cols { + for row in 0..number_of_rows { + let circle_color = Color::from_rgba8_no_srgb(COLOR_7_MIDDLEGRAY).unwrap(); + let line_y = (row as f64 - 1.) * grid_spacing + grid_offset_top; + let mut line = BezPath::new(); + line.move_to(Point::new(grid_offset_left, line_y)); + line.line_to(Point::new(grid_offset_left + ui_context.resolution.x as f64, line_y)); + let mut line_vector = Vector::from_bezpath(line); + let dash_gap = grid_spacing - 0.00001; + let stroke_cap = StrokeCap::Round; + line_vector.style.stroke = Some( + Stroke::new(Some(circle_color), grid_dot_radius * 2.) + .with_dash_lengths(vec![0.00001, dash_gap]) + .with_stroke_cap(stroke_cap), + ); + bg_table.push(TableRow::new_from_element(line_vector)); + } + // } + bg_table +} + +// fn circle_bezpath(center: Point, radius: f64) -> BezPath { +// // "magic constant" for approximating a circle with 4 cubic Beziers +// let k = 0.5522847498307936; + +// let cx = center.x; +// let cy = center.y; +// let r = radius; +// let c = k * r; + +// let mut path = BezPath::new(); + +// // start at rightmost point +// path.move_to((cx + r, cy)); + +// // top-right quadrant +// path.curve_to((cx + r, cy + c), (cx + c, cy + r), (cx, cy + r)); + +// // top-left quadrant +// path.curve_to((cx - c, cy + r), (cx - r, cy + c), (cx - r, cy)); + +// // bottom-left quadrant +// path.curve_to((cx - r, cy - c), (cx - c, cy - r), (cx, cy - r)); + +// // bottom-right quadrant +// path.curve_to((cx + c, cy - r), (cx + r, cy - c), (cx + r, cy)); + +// path.close_path(); +// path +// } 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..1e3fedc7c0 --- /dev/null +++ b/node-graph/gcore/src/node_graph_overlay/consts.rs @@ -0,0 +1,43 @@ +pub const GRID_SIZE: f64 = 24.; +pub const GRID_COLLAPSE_SPACING: f64 = 10.; +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..1e25b8a5cb --- /dev/null +++ b/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs @@ -0,0 +1,563 @@ +use glam::{DAffine2, DVec2}; +use graphene_core_shaders::color::{AlphaMut, Color}; +use kurbo::{BezPath, Circle, Rect, RoundedRect, Shape}; + +use crate::{ + Graphic, + bounds::{BoundingBox, RenderBoundingBox}, + consts::SOURCE_SANS_FONT_DATA, + node_graph_overlay::{ + consts::*, + types::{FrontendGraphDataType, FrontendNodeToRender, NodeGraphOverlayData}, + }, + table::{Table, TableRow}, + text::{self, TextAlign, TypesettingConfig}, + transform::ApplyTransform, + vector::{ + Vector, + style::{Fill, Stroke}, + style::{Fill, Stroke, StrokeAlign}, + }, +}; + +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 node_width = GRID_SIZE * 5.0; + let number_of_secondary_inputs = frontend_node.secondary_inputs.len(); + let number_of_secondary_outputs = frontend_node.secondary_outputs.len(); + let number_of_rows = 1 + number_of_secondary_inputs.max(number_of_secondary_outputs); + let node_height = number_of_rows as f64 * GRID_SIZE; + + let node_rect = RoundedRect::new(x, y, x + node_width, y + node_height, 2.); + let node_bez_path = node_rect.to_path(BEZ_PATH_TOLERANCE); + + // Background table + let mut bg_table = Table::new(); + let mut bg_vector = Vector::from_bezpath(node_bez_path.clone()); + 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 + }; + bg_vector.style.fill = crate::vector::style::Fill::Solid(node_color.clone()); + bg_table.push(TableRow::new_from_element(bg_vector)); + // Make primary input brighter + if number_of_secondary_inputs == 0 { + // Draw the first row with rounded bottom corners + bg_table.push(TableRow::new_from_element(node_first_row(x, y, false))); + } else { + // Draw the first row without rounded bottom corners + bg_table.push(TableRow::new_from_element(node_first_row(x, y, false))); + }; + node_table.push(TableRow::new_from_element(Graphic::Vector(bg_table))); + + // Border mask table is the region where to display the border + let mut border_mask_path = BezPath::new(); + border_mask_path.move_to((-2., -2.)); + border_mask_path.line_to((node_width + 2., -2.)); + if frontend_node.primary_output.is_some() { + border_mask_path.line_to((node_width + 2., 4.)); + border_mask_path.line_to((node_width - 2., 4.)); + border_mask_path.line_to((node_width - 2., 20.)); + } + border_mask_path.line_to((node_width + 2., 20.)); + for row in 1..number_of_rows { + border_mask_path.line_to((node_width + 2., row as f64 * GRID_SIZE + 4.)); + if row <= number_of_secondary_outputs { + border_mask_path.line_to((node_width - 2., row as f64 * GRID_SIZE + 4.)); + border_mask_path.line_to((node_width - 2., row as f64 * GRID_SIZE + 20.)); + border_mask_path.line_to((node_width + 2., row as f64 * GRID_SIZE + 20.)); + } + } + border_mask_path.line_to((node_width + 2., number_of_rows as f64 * GRID_SIZE + 2.)); + border_mask_path.line_to((-2., number_of_rows as f64 * GRID_SIZE + 2.)); + for row in (1..number_of_rows).rev() { + border_mask_path.line_to((-2., row as f64 * GRID_SIZE + 20.)); + if row <= number_of_secondary_inputs { + border_mask_path.line_to((2., row as f64 * GRID_SIZE + 20.)); + border_mask_path.line_to((2., row as f64 * GRID_SIZE + 4.)); + border_mask_path.line_to((2., row as f64 * GRID_SIZE + 4.)); + } + } + if frontend_node.primary_input.is_some() { + border_mask_path.line_to((-2., 20.)); + border_mask_path.line_to((2., 20.)); + border_mask_path.line_to((2., 4.)); + border_mask_path.line_to((-2., 4.)); + } + border_mask_path.line_to((-2., -2.)); + border_mask_path.close_path(); + let mut border_mask_vector = Vector::from_bezpath(border_mask_path); + border_mask_vector.style.fill = Fill::Solid(Color::WHITE); + let mut border_mask_row = TableRow::new_from_element(border_mask_vector); + border_mask_row.alpha_blending.fill = 0.; + border_mask_row.transform = DAffine2::from_translation(DVec2::new(x, y)); + let border_mask_table = Table::new_from_row(border_mask_row); + node_table.push(TableRow::new_from_element(Graphic::Vector(border_mask_table))); + + // Border is implemented as a clip mask + let mut border_table = Table::new(); + let mut border_vector = Vector::from_bezpath(node_bez_path); + 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 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); + node_table.push(TableRow::new_from_element(Graphic::Vector(border_table))); + + let typesetting = TypesettingConfig { + font_size: 14., + line_height_ratio: 1.2, + character_spacing: 0.0, + max_width: None, + max_height: None, + tilt: 0.0, + align: TextAlign::Left, + }; + + // Names for each row + let font_blob = Some(text::load_font(SOURCE_SANS_FONT_DATA)); + let mut node_text = crate::text::to_path(&node_to_render.metadata.display_name, font_blob, typesetting, false); + for text_row in node_text.iter_mut() { + *text_row.transform = DAffine2::from_translation(DVec2::new(x + 8., y + 3.)); + } + + for row in 1..=number_of_rows { + if let Some(input) = frontend_node.secondary_inputs.get(row - 1) { + let font_blob = Some(text::load_font(SOURCE_SANS_FONT_DATA)); + let mut input_row_text = crate::text::to_path(&input.name, font_blob, typesetting, false); + for text_row in input_row_text.iter_mut() { + *text_row.transform = DAffine2::from_translation(DVec2::new(x + 8., y + 24. * row as f64 + 3.)); + } + node_text.extend(input_row_text); + } else if let Some(output) = frontend_node.secondary_outputs.get(row - 1) { + let font_blob = Some(text::load_font(SOURCE_SANS_FONT_DATA)); + let mut output_row_text = crate::text::to_path(&output.name, font_blob, typesetting, false); + // Find width to right align text + let full_text_width = if let RenderBoundingBox::Rectangle(bbox) = output_row_text.bounding_box(DAffine2::default(), true) { + bbox[1].x - bbox[0].x + } else { + 0. + }; + // Account for clipping + let text_width = full_text_width.min(5. * GRID_SIZE - 16.); + let left_offset = 5. * GRID_SIZE - 8. - text_width; + for text_row in output_row_text.iter_mut() { + *text_row.transform = DAffine2::from_translation(DVec2::new(x + 8. + left_offset, y + 24. * row as f64 + 3.)); + } + node_text.extend(output_row_text); + } + } + + // for text_row in node_text.iter_mut() { + // text_row.element.style.fill = Fill::Solid(Color::WHITE); + // } + + let node_text_row = TableRow::new_from_element(Graphic::Vector(node_text)); + // node_text_row.transform.left_apply_transform(&DAffine2::from_translation(DVec2::new(x + 8., y + 8.))); + // log::debug!("node_text_row {:?}", node_text_row.transform); + node_table.push(node_text_row); + + // Add black clipping path to view text in node + let text_area = Rect::new(x + 8., y, x + node_width - 8., y + node_height); + let mut text_area_vector = Vector::from_bezpath(text_area.to_path(BEZ_PATH_TOLERANCE)); + text_area_vector.style.fill = Fill::Solid(Color::WHITE); + let mut text_area_row = TableRow::new_from_element(text_area_vector); + text_area_row.alpha_blending.clip = true; + let text_area_table = Table::new_from_row(text_area_row); + node_table.push(TableRow::new_from_element(Graphic::Vector(text_area_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, 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, 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, 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, true); + row.transform = DAffine2::from_translation(DVec2::new(5. * GRID_SIZE, 12. + GRID_SIZE * (index + 1) as f64)); + ports_table.push(row); + } + let mut graphic_ports_row = TableRow::new_from_element(Graphic::Vector(ports_table)); + graphic_ports_row.transform = DAffine2::from_translation(DVec2::new(x - 3., y - 4.)); + node_table.push(graphic_ports_row); + } + } + + node_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.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); + + // Width from the left of the thumbnail to the left border + let chain_width = if frontend_layer.chain_width > 0 { + frontend_layer.chain_width as f64 * GRID_SIZE + 0.5 * GRID_SIZE + } else { + 0. + }; + + // First render the text to get the layer width + // Create typesetting configuration + let typesetting = TypesettingConfig { + font_size: 14., + line_height_ratio: 1.2, + character_spacing: 0.0, + max_width: None, + max_height: None, + tilt: 0.0, + align: TextAlign::Left, + }; + + let font_blob = Some(text::load_font(SOURCE_SANS_FONT_DATA)); + let mut text_table = crate::text::to_path(&node_to_render.metadata.display_name, font_blob, typesetting, false); + + let text_width = if let RenderBoundingBox::Rectangle(bbox) = text_table.bounding_box(DAffine2::default(), true) { + bbox[1].x - bbox[0].x + } else { + 0. + }; + + let text_left_padding = 8.; + let right_text_edge = 8. + text_width; + // 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 + 12.; + + 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; + + let x0 = layer_position.x - chain_width; + let y0 = layer_position.y; + let h = 2. * GRID_SIZE; + + // Background + let mut background_table = Table::new(); + let bg_rect = RoundedRect::new(x0, y0, x0 + full_layer_width, y0 + h, 8.); + let bez_path = bg_rect.to_path(BEZ_PATH_TOLERANCE); + let mut bg_vector = Vector::from_bezpath(bez_path); + 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); + bg_vector.style.fill = Fill::Solid(background.clone()); + background_table.push(TableRow::new_from_element(bg_vector)); + layer_table.push(TableRow::new_from_element(Graphic::Vector(background_table))); + + // Border mask is a transparent region for where to draw the border + let mut border_mask_table = Table::new(); + let mut border_mask = BezPath::new(); + border_mask.move_to((-2., -2.)); + border_mask.line_to((chain_width - 8., -2.)); + border_mask.line_to((chain_width - 8., 2.)); + border_mask.line_to((chain_width + GRID_SIZE * 3. + 8., 2.)); + border_mask.line_to((chain_width + GRID_SIZE * 3. + 8., -2.)); + border_mask.line_to((full_layer_width + 2., -2.)); + border_mask.line_to((full_layer_width + 2., 12.)); + border_mask.line_to((full_layer_width - 2., 12.)); + border_mask.line_to((full_layer_width - 2., 36.)); + border_mask.line_to((full_layer_width + 2., 36.)); + border_mask.line_to((full_layer_width + 2., 50.)); + border_mask.line_to((full_layer_width + 2., 50.)); + border_mask.line_to((chain_width + GRID_SIZE * 3. + 8., 50.)); + border_mask.line_to((chain_width + GRID_SIZE * 3. + 8., 46.)); + border_mask.line_to((chain_width - 8., 46.)); + border_mask.line_to((chain_width - 8., 50.)); + border_mask.line_to((-2., 50.)); + border_mask.line_to((-2., 32.)); + if frontend_layer.layer_has_left_border_gap && chain_width > 0.1 { + border_mask.line_to((2., 32.)); + border_mask.line_to((2., 16.)); + } + border_mask.line_to((-2., 16.)); + border_mask.line_to((-2., -2.)); + border_mask.close_path(); + + let mut border_mask_vector = Vector::from_bezpath(border_mask); + border_mask_vector.style.fill = Fill::Solid(Color::WHITE); + let mut border_mask_row = TableRow::new_from_element(border_mask_vector); + border_mask_row.alpha_blending.fill = 0.; + border_mask_row.transform.left_apply_transform(&DAffine2::from_translation(DVec2::new(x0, y0))); + border_mask_table.push(border_mask_row); + layer_table.push(TableRow::new_from_element(Graphic::Vector(border_mask_table))); + + // Border is implemented as a mask + let mut border_table = Table::new(); + let border_rect = RoundedRect::new(x0, y0, x0 + full_layer_width, y0 + h, 8.); + 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(); + 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); + layer_table.push(TableRow::new_from_element(Graphic::Vector(border_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_left_padding, 16.)); + } + let top_layer = text_table; + layer_table.push(TableRow::new_from_element(Graphic::Vector(top_layer))); + + // Ports + let mut ports_table = Table::new(); + if let Some(side_input) = &frontend_layer.side_input { + 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.)); + 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(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(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); + 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() + }; + 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(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); + } + layer_table.push(TableRow::new_from_element(Graphic::Vector(ports_table))); + + // Eye and grip icon + let mut icons_table = Table::new(); + let icon_svg = if node_to_render.metadata.visible { + BezPath::from_svg("M8,3C3,3,0,8,0,8s3,5,8,5s8-5,8-5S13,3,8,3z M8,12c-2.2,0-4-1.8-4-4s1.8-4,4-4s4,1.8,4,4S10.2,12,8,12z").unwrap() + } else { + BezPath::from_svg("M8,4c3.5,0,5.9,2.8,6.8,4c-0.9,1.2-3.3,4-6.8,4S2.1,9.2,1.2,8C2.1,6.8,4.5,4,8,4 M8,3C3,3,0,8,0,8s3,5,8,5s8-5,8-5S13,3,8,3L8,3z").unwrap() + }; + let mut icon_vector = Vector::from_bezpath(icon_svg); + icon_vector.style.fill = Fill::Solid(Color::WHITE); + let mut icon_row = TableRow::new_from_element(icon_vector); + icon_row.transform = DAffine2::from_translation(layer_position + DVec2::new(thumbnail_width + right_layer_width - 8., 16.)); + icons_table.push(icon_row); + + if node_to_render.metadata.selected { + let mut grip_path = BezPath::new(); + let circle = Circle::new((0.5, 1.5), 0.5); + grip_path.extend(circle.to_path(BEZ_PATH_TOLERANCE)); + let circle = Circle::new((0.5, 4.5), 0.5); + grip_path.extend(circle.to_path(BEZ_PATH_TOLERANCE)); + let circle = Circle::new((0.5, 7.5), 0.5); + grip_path.extend(circle.to_path(BEZ_PATH_TOLERANCE)); + let circle = Circle::new((3.5, 1.5), 0.5); + grip_path.extend(circle.to_path(BEZ_PATH_TOLERANCE)); + let circle = Circle::new((3.5, 4.5), 0.5); + grip_path.extend(circle.to_path(BEZ_PATH_TOLERANCE)); + let circle = Circle::new((3.5, 7.5), 0.5); + grip_path.extend(circle.to_path(BEZ_PATH_TOLERANCE)); + let mut grip_vector = Vector::from_bezpath(grip_path); + grip_vector.style.fill = Fill::Solid(Color::from_rgba8_no_srgb(COLOR_E_NEARWHITE).unwrap()); + let mut grip_row = TableRow::new_from_element(grip_vector); + grip_row.transform = DAffine2::from_translation(layer_position + DVec2::new(thumbnail_width + right_layer_width + 6. - GRID_SIZE, 19.5)); + 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_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))); + } + } + + let mut ports_table = Table::new(); + ports_table.push(TableRow::new_from_element(Graphic::Vector(side_ports_table))); + (layer_table, ports_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 { + RoundedRect::new(x0, y0, x1, y1, r).to_path(BEZ_PATH_TOLERANCE) + } else { + 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 + }; + + 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 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); + 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) +} + +pub fn draw_wires(nodes: &mut Vec) -> Table { + let mut wire_table = Table::new(); + for node in nodes { + 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(data_type.data_color_dim()), weight)); + wire_table.push(TableRow::new_from_element(wire_vector)); + } + } + Table::new_from_element(Graphic::Vector(wire_table)) +} diff --git a/node-graph/gcore/src/node_graph_overlay/types.rs b/node-graph/gcore/src/node_graph_overlay/types.rs new file mode 100644 index 0000000000..8df7c0ace6 --- /dev/null +++ b/node-graph/gcore/src/node_graph_overlay/types.rs @@ -0,0 +1,264 @@ +use glam::{DAffine2, DVec2}; +use graphene_core_shaders::color::Color; +use kurbo::BezPath; + +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 { + pub scale: f64, + pub x: f64, + pub y: f64, +} + +impl Hash for NodeGraphTransform { + fn hash(&self, state: &mut H) { + // Convert f64 to u64 bit pattern for hashing + self.scale.to_bits().hash(state); + self.x.to_bits().hash(state); + self.y.to_bits().hash(state); + } +} + +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)) + } +} + +#[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)] +pub struct FrontendNodeToRender { + pub metadata: FrontendNodeMetadata, + #[serde(rename = "nodeOrLayer")] + pub node_or_layer: FrontendNodeOrLayer, + //TODO: Remove + pub wires: Vec<(BezPath, bool, FrontendGraphDataType)>, +} + +impl Hash for FrontendNodeToRender { + fn hash(&self, state: &mut H) { + self.metadata.hash(state); + self.node_or_layer.hash(state); + } +} + +// 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, + // TODO: Remove and replace with popup manager system + #[serde(rename = "canBeLayer")] + pub can_be_layer: bool, + #[serde(rename = "displayName")] + pub display_name: String, + 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, Default, PartialEq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)] +pub struct FrontendNode { + // pub position: FrontendNodePosition, + pub position: FrontendXY, + pub primary_output: Option, + pub primary_input: Option, + pub secondary_inputs: Vec, + pub secondary_outputs: Vec, +} + +#[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, + #[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 = "primaryInputConnectedToLayer")] + pub primary_input_connected_to_layer: bool, + #[serde(rename = "primaryOutputConnectedToLayer")] + pub primary_output_connected_to_layer: bool, +} + +#[derive(Clone, Debug, Default, PartialEq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)] +pub struct FrontendXY { + pub x: i32, + pub y: i32, +} + +// // 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 FrontendNodePosition { +// pub absolute: Option, +// pub chain: 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 FrontendLayerPosition { +// pub absolute: Option, +// 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, +} + +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, + #[serde(rename = "resolvedType")] + pub resolved_type: String, + pub name: String, + pub description: String, + /// 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, Default, PartialEq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)] +pub struct FrontendGraphOutput { + #[serde(rename = "dataType")] + pub data_type: FrontendGraphDataType, + pub name: String, + #[serde(rename = "resolvedType")] + pub resolved_type: String, + pub description: String, + /// If connected to an export, it is "export index {index}". + /// If connected to a node, it is "{node name} input {input_index}". + #[serde(rename = "connectedTo")] + pub connected_to: Vec, +} + +#[derive(Clone, Debug, Default, PartialEq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)] + +pub struct FrontendExport { + pub port: FrontendGraphInput, + pub wire: Option, +} + +#[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>, + #[serde(rename = "previewWire")] + pub preview_wire: Option, +} + +#[derive(Clone, Debug, Default, PartialEq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)] +pub struct FrontendImport { + pub port: FrontendGraphOutput, + pub wires: Vec, +} + +#[derive(Copy, Clone, Debug, Default, PartialEq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)] +pub enum FrontendGraphDataType { + #[default] + General, + Number, + Artboard, + Graphic, + Raster, + Vector, + Color, + Gradient, + Typography, +} + +impl FrontendGraphDataType { + 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, + 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, + }; + Color::from_rgba8_no_srgb(color_str).unwrap() + } + 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, + 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, + }; + Color::from_rgba8_no_srgb(color_str).unwrap() + } +} 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..a5a22cc958 --- /dev/null +++ b/node-graph/gcore/src/node_graph_overlay/ui_context.rs @@ -0,0 +1,40 @@ +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, +} + +use std::hash::{Hash, Hasher}; +impl Hash for UIContextImpl { + fn hash(&self, state: &mut H) { + self.transform.hash(state); + self.resolution.hash(state); + } +} + +impl PartialEq for UIContextImpl { + fn eq(&self, other: &Self) -> bool { + self.transform == other.transform && self.resolution == other.resolution + } +} + +#[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/gcore/src/types.rs b/node-graph/gcore/src/types.rs index 133cb88c1d..741f1f88b9 100644 --- a/node-graph/gcore/src/types.rs +++ b/node-graph/gcore/src/types.rs @@ -126,7 +126,7 @@ impl std::fmt::Debug for NodeIOTypes { } } -#[derive(Clone, Debug, PartialEq, Eq, Hash, specta::Type, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub struct ProtoNodeIdentifier { pub name: Cow<'static, str>, } @@ -230,7 +230,7 @@ impl PartialEq for TypeDescriptor { } /// Graph runtime type information used for type inference. -#[derive(Clone, PartialEq, Eq, Hash, specta::Type, serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub enum Type { /// A wrapper for some type variable used within the inference system. Resolved at inference time and replaced with a concrete type. Generic(Cow<'static, str>), diff --git a/node-graph/gcore/src/vector/style.rs b/node-graph/gcore/src/vector/style.rs index 2850401270..4da0a11498 100644 --- a/node-graph/gcore/src/vector/style.rs +++ b/node-graph/gcore/src/vector/style.rs @@ -416,7 +416,7 @@ impl Stroke { self } - pub fn with_dash_lengths(mut self, dash_lengths: &str) -> Option { + pub fn with_dash_lengths_str(mut self, dash_lengths: &str) -> Option { dash_lengths .split(&[',', ' ']) .filter(|x| !x.is_empty()) @@ -429,6 +429,11 @@ impl Stroke { }) } + pub fn with_dash_lengths(mut self, dash_lengths: Vec) -> Self { + self.dash_lengths = dash_lengths; + self + } + pub fn with_dash_offset(mut self, dash_offset: f64) -> Self { self.dash_offset = dash_offset; self diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 2d812cc19b..db57aedbc1 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -262,6 +262,7 @@ tagged_value! { CentroidType(graphene_core::vector::misc::CentroidType), BooleanOperation(graphene_path_bool::BooleanOperation), TextAlign(graphene_core::text::TextAlign), + NodeGraphOverlayData(graphene_core::node_graph_overlay::types::NodeGraphOverlayData), } impl TaggedValue { 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/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 { diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 2bd69b8b39..58af7eb576 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,13 @@ fn node_registry() -> HashMap, input: UIContext, fn_params: [UIContext => ()]), + async_node!(graphene_core::node_graph_overlay::GenerateNodesNode<_>, 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