diff --git a/Cargo.lock b/Cargo.lock index d90f78c7c2..1ea2396f49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2245,7 +2245,9 @@ dependencies = [ "kurbo", "log", "num-traits", + "parley", "serde", + "skrifa 0.36.0", "usvg 0.45.1", "vello", ] diff --git a/editor/src/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 7e73a54fcf..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, } @@ -41,6 +41,7 @@ impl DispatcherMessageHandlers { /// The last occurrence of the message in the message queue is sufficient to ensure correct behavior. /// In addition, these messages do not change any state in the backend (aside from caches). const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[ + MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::NodeGraph(NodeGraphMessageDiscriminant::SendGraph))), MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::PropertiesPanel( PropertiesPanelMessageDiscriminant::Refresh, ))), diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index bbba55c57a..1f8aafcf44 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -1,15 +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, FrontendNode, FrontendNodeType, Transform, -}; +use crate::messages::portfolio::document::node_graph::utility_types::{BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendNodeType}; use crate::messages::portfolio::document::utility_types::nodes::{JsRawBuffer, LayerPanelEntry, RawBuffer}; -use crate::messages::portfolio::document::utility_types::wires::{WirePath, WirePathUpdate}; +use crate::messages::portfolio::document::utility_types::wires::WirePathInProgress; use crate::messages::prelude::*; use crate::messages::tool::utility_types::HintData; -use glam::IVec2; 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}; @@ -128,24 +126,19 @@ 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: IVec2, + import_position: FrontendXY, /// The primary export location. #[serde(rename = "exportPosition")] - export_position: IVec2, + export_position: FrontendXY, /// The document network does not have an add import or export button. #[serde(rename = "addImportExport")] add_import_export: bool, }, - UpdateInSelectedNetwork { - #[serde(rename = "inSelectedNetwork")] - in_selected_network: bool, - }, - UpdateBox { + UpdateNodeGraphSelectionBox { #[serde(rename = "box")] box_selection: Option, }, @@ -185,10 +178,6 @@ pub enum FrontendMessage { UpdateLayerWidths { #[serde(rename = "layerWidths")] layer_widths: HashMap, - #[serde(rename = "chainWidths")] - chain_widths: HashMap, - #[serde(rename = "hasLeftInputWire")] - has_left_input_wire: HashMap, }, UpdateDialogButtons { #[serde(rename = "layoutTarget")] @@ -253,9 +242,6 @@ pub enum FrontendMessage { #[serde(rename = "setColorChoice")] set_color_choice: Option, }, - UpdateGraphFadeArtwork { - percentage: f64, - }, UpdateInputHints { #[serde(rename = "hintData")] hint_data: HintData, @@ -283,26 +269,18 @@ pub enum FrontendMessage { UpdateMouseCursor { cursor: MouseCursorIcon, }, - UpdateNodeGraphNodes { - nodes: Vec, - }, - UpdateVisibleNodes { - nodes: Vec, + RequestNativeNodeGraphRender, + UpdateNativeNodeGraphSVG { + #[serde(rename = "svgString")] + svg_string: String, }, - UpdateNodeGraphWires { - wires: Vec, - }, - ClearAllNodeGraphWires, UpdateNodeGraphControlBarLayout { #[serde(rename = "layoutTarget")] layout_target: LayoutTarget, diff: Vec, }, - UpdateNodeGraphSelection { - selected: Vec, - }, UpdateNodeGraphTransform { - transform: Transform, + transform: NodeGraphTransform, }, UpdateNodeThumbnail { id: NodeId, @@ -328,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/data_panel/data_panel_message_handler.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs index b9d01a5e0c..0e2229880e 100644 --- a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs @@ -12,6 +12,7 @@ use graphene_std::gradient::GradientStops; use graphene_std::memo::IORecord; use graphene_std::raster_types::{CPU, GPU, Raster}; use graphene_std::table::Table; +use graphene_std::text::Typography; use graphene_std::vector::Vector; use graphene_std::vector::style::{Fill, FillChoice}; use graphene_std::{Artboard, Graphic}; @@ -266,6 +267,7 @@ impl TableRowLayout for Graphic { Self::RasterGPU(table) => table.identifier(), Self::Color(table) => table.identifier(), Self::Gradient(table) => table.identifier(), + Self::Typography(table) => table.identifier(), } } // Don't put a breadcrumb for Graphic @@ -280,6 +282,7 @@ impl TableRowLayout for Graphic { Self::RasterGPU(table) => table.layout_with_breadcrumb(data), Self::Color(table) => table.layout_with_breadcrumb(data), Self::Gradient(table) => table.layout_with_breadcrumb(data), + Self::Typography(table) => table.layout_with_breadcrumb(data), } } } @@ -504,6 +507,21 @@ impl TableRowLayout for GradientStops { } } +impl TableRowLayout for Typography { + fn type_name() -> &'static str { + "Typography" + } + fn identifier(&self) -> String { + "Typography".to_string() + } + fn element_widget(&self, _index: usize) -> WidgetHolder { + TextLabel::new("Not supported").widget_holder() + } + fn element_page(&self, _data: &mut LayoutData) -> Vec { + vec![LayoutGroup::Row { widgets: Vec::new() }] + } +} + impl TableRowLayout for f64 { fn type_name() -> &'static str { "Number (f64)" diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index e42c15a17f..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}; @@ -17,7 +16,7 @@ use crate::messages::portfolio::document::overlays::utility_types::{OverlaysType use crate::messages::portfolio::document::properties_panel::properties_panel_message_handler::PropertiesPanelMessageContext; use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, DocumentMode, FlipAxis, PTZ}; -use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeTemplate, OutputConnector}; +use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeTemplate}; use crate::messages::portfolio::document::utility_types::nodes::RawBuffer; use crate::messages::portfolio::utility_types::PanelType; use crate::messages::portfolio::utility_types::PersistentData; @@ -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,34 +553,27 @@ 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 }); - responses.add(FrontendMessage::UpdateGraphFadeArtwork { - percentage: self.graph_fade_artwork_percentage, - }); // Update the tilt menu bar buttons to be disabled when the graph is open responses.add(MenuBarMessage::SendLayout); 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::SendGraph); responses.add(NodeGraphMessage::UpdateHints); } else { responses.add(ToolMessage::ActivateTool { tool_type: *current_tool }); responses.add(OverlaysMessage::Draw); // Redraw overlays when graph is closed } + + responses.add(NodeGraphMessage::SendGraph); } DocumentMessage::GraphViewOverlayToggle => { responses.add(DocumentMessage::GraphViewOverlay { open: !self.graph_view_overlay_open }); @@ -1191,7 +1180,7 @@ impl MessageHandler> for DocumentMes } responses.add(PropertiesPanelMessage::Refresh); responses.add(NodeGraphMessage::UpdateLayerPanel); - responses.add(NodeGraphMessage::UpdateInSelectedNetwork); + responses.add(NodeGraphMessage::SendGraph); } DocumentMessage::SetBlendModeForSelectedLayers { blend_mode } => { for layer in self.network_interface.selected_nodes().selected_layers_except_artboards(&self.network_interface) { @@ -1200,7 +1189,7 @@ impl MessageHandler> for DocumentMes } DocumentMessage::SetGraphFadeArtwork { percentage } => { self.graph_fade_artwork_percentage = percentage; - responses.add(FrontendMessage::UpdateGraphFadeArtwork { percentage }); + responses.add(NodeGraphMessage::SendGraph); } DocumentMessage::SetNodePinned { node_id, pinned } => { responses.add(DocumentMessage::AddTransaction); @@ -1497,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, @@ -1950,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) { @@ -1983,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) } @@ -2800,14 +2781,9 @@ impl DocumentMessageHandler { .popover_layout({ // Showing only compatible types let compatible_type = selected_layer.and_then(|layer| { - let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &self.network_interface); - let node_type = graph_layer.horizontal_layer_flow().nth(1); - if let Some(node_id) = node_type { - let (output_type, _) = self.network_interface.output_type(&OutputConnector::node(node_id, 0), &self.selection_network_path); - Some(format!("type:{}", output_type.nested_type())) - } else { - None - } + self.network_interface + .upstream_output_connector(&InputConnector::node(layer.to_node(), 1), &[]) + .and_then(|upstream_output| self.network_interface.output_type(&upstream_output, &[]).add_node_string()) }); let mut node_chooser = NodeCatalog::new(); diff --git a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs index 4079fefd32..b25e7905a5 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -303,8 +303,8 @@ impl<'a> ModifyInputsContext<'a> { // If inserting a 'Path' node, insert a 'Flatten Path' node if the type is `Graphic`. // TODO: Allow the 'Path' node to operate on table data by utilizing the reference (index or ID?) for each row. if node_definition.identifier == "Path" { - let layer_input_type = self.network_interface.input_type(&InputConnector::node(output_layer.to_node(), 1), &[]).0.nested_type().clone(); - if layer_input_type == concrete!(Table) { + let layer_input_type = self.network_interface.input_type(&InputConnector::node(output_layer.to_node(), 1), &[]).into_compiled_nested_type(); + if layer_input_type == Some(concrete!(Table)) { let Some(flatten_path_definition) = resolve_document_node_type("Flatten Path") else { log::error!("Flatten Path does not exist in ModifyInputsContext::existing_node_id"); return None; diff --git a/editor/src/messages/portfolio/document/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 1432046a25..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, @@ -231,7 +231,5 @@ pub enum NodeGraphMessage { }, UpdateActionButtons, UpdateGraphBarRight, - UpdateInSelectedNetwork, UpdateHints, - SendSelectedNodes, } diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index 6337a2566d..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 @@ -1,4 +1,4 @@ -use super::utility_types::{BoxSelection, ContextMenuInformation, DragStart, FrontendNode}; +use super::utility_types::{BoxSelection, ContextMenuInformation, DragStart}; use super::{document_node_definitions, node_properties}; use crate::consts::GRID_SIZE; use crate::messages::input_mapper::utility_types::macros::action_keys; @@ -6,17 +6,17 @@ use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::document_message_handler::navigation_controls; use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext; use crate::messages::portfolio::document::node_graph::document_node_definitions::NodePropertiesContext; -use crate::messages::portfolio::document::node_graph::utility_types::{ContextMenuData, Direction, FrontendGraphDataType}; +use crate::messages::portfolio::document::node_graph::utility_types::{ContextMenuData, Direction}; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::misc::GroupFolderType; use crate::messages::portfolio::document::utility_types::network_interface::{ - self, FlowType, InputConnector, NodeNetworkInterface, NodeTemplate, NodeTypePersistentMetadata, OutputConnector, Previewing, TypeSource, + self, FlowType, InputConnector, NodeNetworkInterface, NodeTemplate, NodeTypePersistentMetadata, OutputConnector, Previewing, }; use crate::messages::portfolio::document::utility_types::nodes::{CollapsedLayers, LayerPanelEntry}; -use crate::messages::portfolio::document::utility_types::wires::{GraphWireStyle, WirePath, WirePathUpdate, build_vector_wire}; +use crate::messages::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::{self, get_clip_mode}; +use crate::messages::tool::common_functionality::graph_modification_utils::get_clip_mode; use crate::messages::tool::common_functionality::utility_functions::make_path_editable_is_allowed; use crate::messages::tool::tool_messages::tool_prelude::{Key, MouseMotion}; use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo}; @@ -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. @@ -192,7 +191,7 @@ impl<'a> MessageHandler> for NodeG responses.add(MenuBarMessage::SendLayout); responses.add(NodeGraphMessage::UpdateLayerPanel); responses.add(PropertiesPanelMessage::Refresh); - responses.add(NodeGraphMessage::SendSelectedNodes); + responses.add(NodeGraphMessage::SendGraph); responses.add(ArtboardToolMessage::UpdateSelectedArtboard); responses.add(DocumentMessage::DocumentStructureChanged); responses.add(OverlaysMessage::Draw); @@ -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::from_type(&network_interface.input_type(clicked_input, breadcrumb_network_path).0); + self.wire_in_progress_type = network_interface.input_type(clicked_input, breadcrumb_network_path).displayed_type(); return; } @@ -888,8 +886,8 @@ impl<'a> MessageHandler> for NodeG self.initial_disconnecting = false; self.wire_in_progress_from_connector = network_interface.output_position(&clicked_output, selection_network_path); - let (output_type, source) = &network_interface.output_type(&clicked_output, breadcrumb_network_path); - self.wire_in_progress_type = FrontendGraphDataType::displayed_type(output_type, source); + let output_type = network_interface.output_type(&clicked_output, breadcrumb_network_path); + self.wire_in_progress_type = 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 { @@ -1206,18 +1204,14 @@ impl<'a> MessageHandler> for NodeG return; } + let compatible_type = network_interface.output_type(&output_connector, selection_network_path).add_node_string(); + // Get the output types from the network interface - let (output_type, type_source) = network_interface.output_type(&output_connector, selection_network_path); let Some(network_metadata) = network_interface.network_metadata(selection_network_path) else { warn!("No network_metadata"); return; }; - let compatible_type = match type_source { - TypeSource::RandomProtonodeImplementation | TypeSource::Error(_) => None, - _ => Some(format!("type:{}", output_type.nested_type())), - }; - let appear_right_of_mouse = if ipp.mouse.position.x > ipp.viewport_bounds.size().x - 173. { -173. } else { 0. }; let appear_above_mouse = if ipp.mouse.position.y > ipp.viewport_bounds.size().y - 34. { -34. } else { 0. }; let node_graph_shift = DVec2::new(appear_right_of_mouse, appear_above_mouse) / network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport.matrix2.x_axis.x; @@ -1319,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); @@ -1328,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::>(); @@ -1395,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); @@ -1595,72 +1590,15 @@ 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); - if breadcrumb_network_path == selection_network_path && graph_view_overlay_open { - let nodes = self.collect_nodes(network_interface, breadcrumb_network_path); - self.frontend_nodes = nodes.iter().map(|node| node.id).collect(); - responses.add(FrontendMessage::UpdateNodeGraphNodes { nodes }); - responses.add(NodeGraphMessage::UpdateVisibleNodes); - - let (layer_widths, chain_widths, has_left_input_wire) = network_interface.collect_layer_widths(breadcrumb_network_path); - - responses.add(NodeGraphMessage::UpdateImportsExports); - responses.add(FrontendMessage::UpdateLayerWidths { - layer_widths, - chain_widths, - has_left_input_wire, - }); - responses.add(NodeGraphMessage::SendSelectedNodes); - 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); - } + responses.add(NodeGraphMessage::UpdateActionButtons); + + responses.add(FrontendMessage::RequestNativeNodeGraphRender); + responses.add(NodeGraphMessage::UpdateImportsExports); + self.update_node_graph_hints(responses); } NodeGraphMessage::SetInputValue { node_id, input_index, value } => { let input = NodeInput::value(value, false); @@ -1727,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 { @@ -1748,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) { @@ -1763,7 +1697,6 @@ impl<'a> MessageHandler> for NodeG }); responses.add(NodeGraphMessage::RunDocumentGraph); responses.add(NodeGraphMessage::SendGraph); - responses.add(NodeGraphMessage::SendWires); } NodeGraphMessage::SetDisplayName { node_id, @@ -1947,23 +1880,30 @@ 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"); return; }; + let import_position = FrontendXY { + x: import_position.x, + y: import_position.y, + }; + let export_position = FrontendXY { + x: export_position.x, + y: export_position.y, + }; + // Do not show the add import or add export button in the document network; let add_import_export = !breadcrumb_network_path.is_empty(); - responses.add(NodeGraphMessage::UpdateVisibleNodes); - responses.add(NodeGraphMessage::SendWires); responses.add(FrontendMessage::UpdateImportsExports { imports, exports, @@ -1976,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"); @@ -1989,13 +1926,11 @@ 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 } => { - for (path, node_type) in resolved_types.add { - network_interface.resolved_types.types.insert(path.to_vec(), node_type); - } - for path in resolved_types.remove { - network_interface.resolved_types.types.remove(&path.to_vec()); - } + network_interface.resolved_types.update(resolved_types); self.node_graph_errors = node_graph_errors; } NodeGraphMessage::UpdateActionButtons => { @@ -2008,22 +1943,9 @@ impl<'a> MessageHandler> for NodeG self.update_graph_bar_right(graph_fade_artwork_percentage, network_interface, breadcrumb_network_path, navigation_handler); self.send_node_bar_layout(responses); } - NodeGraphMessage::UpdateInSelectedNetwork => responses.add(FrontendMessage::UpdateInSelectedNetwork { - in_selected_network: selection_network_path == breadcrumb_network_path, - }), NodeGraphMessage::UpdateHints => { self.update_node_graph_hints(responses); } - NodeGraphMessage::SendSelectedNodes => { - let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(breadcrumb_network_path) else { - log::error!("Could not get selected nodes in NodeGraphMessage::SendSelectedNodes"); - return; - }; - responses.add(NodeGraphMessage::UpdateActionButtons); - responses.add(FrontendMessage::UpdateNodeGraphSelection { - selected: selected_nodes.selected_nodes().cloned().collect::>(), - }); - } } let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else { log::error!("Could not get selected nodes in NodeGraphMessageHandler"); @@ -2109,16 +2031,7 @@ impl NodeGraphMessageHandler { .popover_layout({ // Showing only compatible types let compatible_type = match (selection_includes_layers, has_multiple_selection, selected_layer) { - (true, false, Some(layer)) => { - let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, network_interface); - let node_type = graph_layer.horizontal_layer_flow().nth(1); - if let Some(node_id) = node_type { - let (output_type, _) = network_interface.output_type(&OutputConnector::node(node_id, 0), &[]); - Some(format!("type:{}", output_type.nested_type())) - } else { - None - } - } + (true, false, Some(layer)) => network_interface.output_type(&OutputConnector::node(layer.to_node(), 1), &[]).add_node_string(), _ => None, }; @@ -2431,17 +2344,10 @@ impl NodeGraphMessageHandler { .icon(Some("Node".to_string())) .tooltip("Add an operation to the end of this layer's chain of nodes") .popover_layout({ - let layer_identifier = LayerNodeIdentifier::new(layer, context.network_interface); - let compatible_type = { - let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer_identifier, context.network_interface); - let node_type = graph_layer.horizontal_layer_flow().nth(1); - if let Some(node_id) = node_type { - let (output_type, _) = context.network_interface.output_type(&OutputConnector::node(node_id, 0), &[]); - Some(format!("type:{}", output_type.nested_type())) - } else { - None - } - }; + let compatible_type = context + .network_interface + .upstream_output_connector(&InputConnector::node(layer, 1), &[]) + .and_then(|upstream_output| context.network_interface.output_type(&upstream_output, &[]).add_node_string()); let mut node_chooser = NodeCatalog::new(); node_chooser.intial_search = compatible_type.unwrap_or("".to_string()); @@ -2488,129 +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_nodes(&self, network_interface: &mut NodeNetworkInterface, breadcrumb_network_path: &[NodeId]) -> Vec { - let Some(network) = network_interface.nested_network(breadcrumb_network_path) else { - log::error!("Could not get nested network when collecting nodes"); - return Vec::new(); - }; - let mut nodes = Vec::new(); - for (node_id, visible) in network.nodes.iter().map(|(node_id, node)| (*node_id, node.visible)).collect::>() { - let node_id_path = [breadcrumb_network_path, &[node_id]].concat(); - - let primary_input_connector = InputConnector::node(node_id, 0); - - let primary_input = if network_interface - .input_from_connector(&primary_input_connector, breadcrumb_network_path) - .is_some_and(|input| input.is_exposed()) - { - network_interface.frontend_input_from_connector(&primary_input_connector, breadcrumb_network_path) - } else { - None - }; - let exposed_inputs = (1..network_interface.number_of_inputs(&node_id, breadcrumb_network_path)) - .filter_map(|input_index| network_interface.frontend_input_from_connector(&InputConnector::node(node_id, input_index), breadcrumb_network_path)) - .collect(); - - let primary_output = network_interface.frontend_output_from_connector(&OutputConnector::node(node_id, 0), breadcrumb_network_path); - - let exposed_outputs = (1..network_interface.number_of_outputs(&node_id, breadcrumb_network_path)) - .filter_map(|output_index| network_interface.frontend_output_from_connector(&OutputConnector::node(node_id, output_index), breadcrumb_network_path)) - .collect(); - let (primary_output_connected_to_layer, primary_input_connected_to_layer) = if network_interface.is_layer(&node_id, breadcrumb_network_path) { - ( - network_interface.primary_output_connected_to_layer(&node_id, breadcrumb_network_path), - network_interface.primary_input_connected_to_layer(&node_id, breadcrumb_network_path), - ) - } else { - (false, false) - }; - - let is_export = network_interface - .input_from_connector(&InputConnector::Export(0), breadcrumb_network_path) - .is_some_and(|export| export.as_node().is_some_and(|export_node_id| node_id == export_node_id)); - let is_root_node = network_interface.root_node(breadcrumb_network_path).is_some_and(|root_node| root_node.node_id == node_id); - - let Some(position) = network_interface.position(&node_id, breadcrumb_network_path) else { - log::error!("Could not get position for node: {node_id}"); - continue; - }; - let previewed = is_export && !is_root_node; - - let locked = network_interface.is_locked(&node_id, breadcrumb_network_path); - - let errors = self - .node_graph_errors - .iter() - .find(|error| error.node_path == node_id_path) - .map(|error| format!("{:?}", error.error.clone())) - .or_else(|| { - if self.node_graph_errors.iter().any(|error| error.node_path.starts_with(&node_id_path)) { - Some("Node graph type error within this node".to_string()) - } else { - None - } - }); - - nodes.push(FrontendNode { - id: node_id, - is_layer: network_interface - .node_metadata(&node_id, breadcrumb_network_path) - .is_some_and(|node_metadata| node_metadata.persistent_metadata.is_layer()), - can_be_layer: network_interface.is_eligible_to_be_layer(&node_id, breadcrumb_network_path), - reference: network_interface.reference(&node_id, breadcrumb_network_path).cloned().unwrap_or_default(), - display_name: network_interface.display_name(&node_id, breadcrumb_network_path), - primary_input, - exposed_inputs, - primary_output, - exposed_outputs, - primary_output_connected_to_layer, - primary_input_connected_to_layer, - position, - previewed, - visible, - locked, - errors, - }); - } - - nodes - } - fn collect_subgraph_names(network_interface: &mut NodeNetworkInterface, breadcrumb_network_path: &[NodeId]) -> Option> { let mut current_network_path = vec![]; let mut current_network = network_interface.nested_network(¤t_network_path).unwrap(); @@ -2781,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 0dd2d5906c..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,19 +1,19 @@ #![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::*; use choice::enum_choice; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; -use graph_craft::Type; use graph_craft::document::value::TaggedValue; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput}; +use graph_craft::{Type, concrete}; use graphene_std::NodeInputDecleration; use graphene_std::animation::RealTimeMode; use graphene_std::extract_xy::XY; +use graphene_std::node_graph_overlay::types::FrontendGraphDataType; use graphene_std::path_bool::BooleanOperation; use graphene_std::raster::curve::Curve; use graphene_std::raster::{ @@ -1610,7 +1610,11 @@ pub(crate) fn generate_node_properties(node_id: NodeId, context: &mut NodeProper input_type.clone() } - _ => context.network_interface.input_type(&InputConnector::node(node_id, input_index), context.selection_network_path).0, + _ => context + .network_interface + .input_type(&InputConnector::node(node_id, input_index), context.selection_network_path) + .into_compiled_nested_type() + .unwrap_or(concrete!(())), }; property_from_type(node_id, input_index, &input_type, number_options, unit_suffix, display_decimal_places, step, context).unwrap_or_else(|value| value) @@ -1992,7 +1996,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::from_type(&context.network_interface.input_type(&InputConnector::node(node_id, index), context.selection_network_path).0); + 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 b3ed877769..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,10 +1,8 @@ -use crate::messages::portfolio::document::utility_types::network_interface::TypeSource; -use glam::IVec2; use graph_craft::document::NodeId; -use graph_craft::document::value::TaggedValue; -use graphene_std::Type; use std::borrow::Cow; +use crate::messages::portfolio::document::utility_types::network_interface::resolved_types::TypeSource; + #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize, specta::Type)] pub enum FrontendGraphDataType { #[default] @@ -42,27 +40,34 @@ impl FrontendGraphDataType { } } - pub fn displayed_type(input: &Type, type_source: &TypeSource) -> Self { - match type_source { - TypeSource::Error(_) | TypeSource::RandomProtonodeImplementation => Self::General, - _ => Self::from_type(input), + pub fn displayed_type(type_source: &TypeSource) -> Self { + match type_source.compiled_nested_type() { + Some(nested_type) => Self::from_type(&nested_type), + None => Self::General, } } } +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +pub struct FrontendXY { + pub x: i32, + pub y: i32, +} + #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] pub struct FrontendGraphInput { #[serde(rename = "dataType")] pub data_type: FrontendGraphDataType, - pub name: String, - pub description: String, #[serde(rename = "resolvedType")] pub resolved_type: String, - #[serde(rename = "validTypes")] - pub valid_types: Vec, - #[serde(rename = "connectedTo")] + 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, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] @@ -80,34 +85,102 @@ pub struct FrontendGraphOutput { } #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] -pub struct FrontendNode { - pub id: graph_craft::document::NodeId, - #[serde(rename = "isLayer")] - pub is_layer: bool, +pub struct 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 { + #[serde(rename = "nodeId")] + pub node_id: NodeId, + // TODO: Remove and replace with popup manager system #[serde(rename = "canBeLayer")] pub can_be_layer: bool, - pub reference: Option, #[serde(rename = "displayName")] pub display_name: String, - #[serde(rename = "primaryInput")] - pub primary_input: Option, - #[serde(rename = "exposedInputs")] - pub exposed_inputs: Vec, - #[serde(rename = "primaryOutput")] - pub primary_output: Option, - #[serde(rename = "exposedOutputs")] - pub exposed_outputs: Vec, - #[serde(rename = "primaryOutputConnectedToLayer")] - pub primary_output_connected_to_layer: bool, - #[serde(rename = "primaryInputConnectedToLayer")] - pub primary_input_connected_to_layer: bool, - pub position: IVec2, + 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, - pub locked: bool, - pub previewed: bool, + // The svg string for each input + // pub wires: Vec>, pub errors: Option, } +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +pub struct FrontendNode { + // pub position: FrontendNodePosition, + pub position: FrontendXY, + pub inputs: Vec>, + pub outputs: Vec>, +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +pub struct FrontendLayer { + #[serde(rename = "bottomInput")] + pub bottom_input: FrontendGraphInput, + #[serde(rename = "sideInput")] + pub side_input: Option, + pub output: FrontendGraphOutput, + // pub position: FrontendLayerPosition, + pub position: FrontendXY, + pub locked: bool, + #[serde(rename = "chainWidth")] + pub chain_width: u32, + #[serde(rename = "layerHasLeftBorderGap")] + pub layer_has_left_border_gap: bool, + #[serde(rename = "primaryInputConnectedToLayer")] + pub primary_input_connected_to_layer: bool, + #[serde(rename = "primaryOutputConnectedToLayer")] + pub primary_output_connected_to_layer: bool, +} + +// // Should be an enum but those are hard to serialize/deserialize to TS +// #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +// pub struct FrontendNodePosition { +// pub absolute: Option, +// pub chain: Option, +// } + +// // Should be an enum but those are hard to serialize/deserialize to TS +// #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +// pub struct FrontendLayerPosition { +// pub absolute: Option, +// pub stack: Option, +// } + +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +pub struct FrontendNodeOrLayer { + pub node: Option, + pub layer: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +pub struct FrontendNodeToRender { + pub metadata: FrontendNodeMetadata, + #[serde(rename = "nodeOrLayer")] + pub node_or_layer: FrontendNodeOrLayer, + //TODO: Remove + pub wires: Vec<(String, bool, FrontendGraphDataType)>, +} + #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] pub struct FrontendNodeType { pub name: Cow<'static, str>, @@ -133,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, @@ -141,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")] @@ -196,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 6cc9f37ab9..8cabb7250b 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs @@ -9,6 +9,7 @@ use core::borrow::Borrow; use core::f64::consts::{FRAC_PI_2, PI, TAU}; use glam::{DAffine2, DVec2}; use graphene_std::Color; +use graphene_std::consts::SOURCE_SANS_FONT_DATA; use graphene_std::math::quad::Quad; use graphene_std::subpath::{self, Subpath}; use graphene_std::table::Table; @@ -1021,11 +1022,7 @@ impl OverlayContextInternal { align: TextAlign::Left, }; - // Load Source Sans Pro font data - // TODO: Grab this from the node_modules folder (either with `include_bytes!` or ideally at runtime) instead of checking the font file into the repo. - // TODO: And maybe use the WOFF2 version (if it's supported) for its smaller, compressed file size. - const FONT_DATA: &[u8] = include_bytes!("source-sans-pro-regular.ttf"); - 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); @@ -1048,10 +1045,7 @@ impl OverlayContextInternal { align: TextAlign::Left, // We'll handle alignment manually via pivot }; - // Load Source Sans Pro font data - // TODO: Grab this from the node_modules folder (either with `include_bytes!` or ideally at runtime) instead of checking the font file into the repo. - // TODO: And maybe use the WOFF2 version (if it's supported) for its smaller, compressed file size. - const FONT_DATA: &[u8] = include_bytes!("source-sans-pro-regular.ttf"); + const FONT_DATA: &[u8] = SOURCE_SANS_FONT_DATA; let font_blob = Some(load_font(FONT_DATA)); // Convert text to vector paths using the existing text system diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index d6c47223b9..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,36 +1,33 @@ -mod deserialization; - 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, FrontendGraphInput, FrontendGraphOutput}; -use crate::messages::portfolio::document::utility_types::wires::{GraphWireStyle, WirePath, WirePathUpdate, build_vector_wire}; +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::tool::common_functionality::graph_modification_utils; use crate::messages::tool::tool_messages::tool_prelude::NumberInputMode; use deserialization::deserialize_node_persistent_metadata; use glam::{DAffine2, DVec2, IVec2}; +use graph_craft::Type; use graph_craft::document::value::TaggedValue; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork}; -use graph_craft::{Type, concrete}; -use graphene_std::Artboard; use graphene_std::ContextDependencies; use graphene_std::math::quad::Quad; use graphene_std::subpath::Subpath; -use graphene_std::table::Table; use graphene_std::transform::Footprint; use graphene_std::vector::click_target::{ClickTarget, ClickTargetType}; use graphene_std::vector::{PointId, Vector, VectorModificationType}; -use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypes; -use interpreted_executor::node_registry::NODE_REGISTRY; -use kurbo::BezPath; use serde_json::{Value, json}; use std::collections::{HashMap, HashSet, VecDeque}; -use std::hash::{DefaultHasher, Hash, Hasher}; +use std::hash::Hash; use std::ops::Deref; +mod deserialization; +pub mod node_graph; +pub mod resolved_types; + /// All network modifications should be done through this API, so the fields cannot be public. However, all fields within this struct can be public since it it not possible to have a public mutable reference. #[derive(Debug, Default, serde::Serialize, serde::Deserialize)] pub struct NodeNetworkInterface { @@ -229,31 +226,6 @@ impl NodeNetworkInterface { layers } - pub fn chain_width(&self, node_id: &NodeId, network_path: &[NodeId]) -> u32 { - if self.number_of_displayed_inputs(node_id, network_path) > 1 { - let mut last_chain_node_distance = 0u32; - // Iterate upstream from the layer, and get the number of nodes distance to the last node with Position::Chain - for (index, node_id) in self - .upstream_flow_back_from_nodes(vec![*node_id], network_path, FlowType::HorizontalPrimaryOutputFlow) - .skip(1) - .enumerate() - .collect::>() - { - // Check if the node is positioned as a chain - if self.is_chain(&node_id, network_path) { - last_chain_node_distance = (index as u32) + 1; - } else { - return last_chain_node_distance * 7 + 1; - } - } - - last_chain_node_distance * 7 + 1 - } else { - // Layer with no inputs has no chain - 0 - } - } - /// Check if the specified node id is connected to the output pub fn connected_to_output(&self, target_node_id: &NodeId, network_path: &[NodeId]) -> bool { let Some(network) = self.nested_network(network_path) else { @@ -454,12 +426,12 @@ impl NodeNetworkInterface { *input = NodeInput::Node { node_id: new_id, output_index }; } else { // Disconnect node input if it is not connected to another node in new_ids - let tagged_value = TaggedValue::from_type_or_none(&self.input_type(&InputConnector::node(*node_id, input_index), network_path).0); + let tagged_value = self.tagged_value_from_input(&InputConnector::node(*node_id, input_index), network_path); *input = NodeInput::value(tagged_value, true); } } else if let &mut NodeInput::Network { .. } = input { // Always disconnect network node input - let tagged_value = TaggedValue::from_type_or_none(&self.input_type(&InputConnector::node(*node_id, input_index), network_path).0); + let tagged_value = self.tagged_value_from_input(&InputConnector::node(*node_id, input_index), network_path); *input = NodeInput::value(tagged_value, true); } } @@ -467,8 +439,8 @@ impl NodeNetworkInterface { } /// Try and get the [`DocumentNodeDefinition`] for a node - pub fn get_node_definition(&self, network_path: &[NodeId], node_id: NodeId) -> Option<&DocumentNodeDefinition> { - let metadata = self.node_metadata(&node_id, network_path)?; + pub fn get_node_definition(&self, node_id: &NodeId, network_path: &[NodeId]) -> Option<&DocumentNodeDefinition> { + let metadata = self.node_metadata(node_id, network_path)?; resolve_document_node_type(metadata.persistent_metadata.reference.as_ref()?) } @@ -489,278 +461,6 @@ impl NodeNetworkInterface { } } - /// Try and get the [`Type`] for any [`InputConnector`] based on the `self.resolved_types`. - fn node_type_from_compiled(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> Option<(Type, TypeSource)> { - let (node_id, input_index) = match *input_connector { - InputConnector::Node { node_id, input_index } => (node_id, input_index), - InputConnector::Export(export_index) => { - let Some((encapsulating_node_id, encapsulating_node_id_path)) = network_path.split_last() else { - // The outermost network export defaults to a Table. - return Some((concrete!(Table), TypeSource::OuterMostExportDefault)); - }; - - let output_type = self.output_type(&OutputConnector::node(*encapsulating_node_id, export_index), encapsulating_node_id_path); - return Some(output_type); - } - }; - let Some(node) = self.document_node(&node_id, network_path) else { - log::error!("Could not get node {node_id} in input_type"); - return None; - }; - // If the input_connector is a NodeInput::Value, return the type of the tagged value. - if let Some(value) = node.inputs.get(input_index).and_then(|input| input.as_value()) { - return Some((value.ty(), TypeSource::TaggedValue)); - } - let node_id_path = [network_path, &[node_id]].concat(); - match &node.implementation { - DocumentNodeImplementation::Network(_nested_network) => { - // Attempt to resolve where this import is within the nested network (it may be connected to the node or directly to an export) - let outwards_wires = self.outward_wires(&node_id_path); - let inputs_using_import = outwards_wires.and_then(|outwards_wires| outwards_wires.get(&OutputConnector::Import(input_index))); - let first_input = inputs_using_import.and_then(|input| input.first()).copied(); - - if inputs_using_import.is_some_and(|inputs| inputs.len() > 1) { - warn!("Found multiple inputs using an import. Using the type of the first one."); - } - - if let Some(input_connector) = first_input { - self.node_type_from_compiled(&input_connector, &node_id_path) - } - // Nothing is connected to the import - else { - None - } - } - DocumentNodeImplementation::ProtoNode(_) => { - // Offset the input index by 1 since the proto node also includes the type of the input passed as a call argument. - self.resolved_types - .types - .get(node_id_path.as_slice()) - .and_then(|node_types| node_types.inputs.get(input_index + 1).cloned()) - .map(|node_types| (node_types, TypeSource::Compiled)) - } - DocumentNodeImplementation::Extract => None, - } - } - - /// Guess the type from the node based on a document node default or a random protonode definition. - fn guess_type_from_node(&mut self, network_path: &mut Vec, node_id: NodeId, input_index: usize) -> (Type, TypeSource) { - // Try and get the default value from the document node definition - if let Some(value) = self - .get_node_definition(network_path, node_id) - .and_then(|definition| definition.node_template.document_node.inputs.get(input_index)) - .and_then(|input| input.as_value()) - { - return (value.ty(), TypeSource::DocumentNodeDefault); - } - - let Some(node) = self.document_node(&node_id, network_path) else { - return (concrete!(()), TypeSource::Error("node id {node_id:?} not in network {network_path:?}")); - }; - - let node_id_path = [network_path.as_slice(), &[node_id]].concat(); - match &node.implementation { - DocumentNodeImplementation::ProtoNode(protonode) => { - let Some(node_types) = random_protonode_implementation(protonode) else { - return (concrete!(()), TypeSource::Error("could not resolve protonode")); - }; - - let skip_footprint = 1; - - let Some(input_type) = std::iter::once(node_types.call_argument.clone()).chain(node_types.inputs.clone()).nth(input_index + skip_footprint) else { - // log::warn!("Could not get type for {node_id_path:?}, input: {input_index}"); - return (concrete!(()), TypeSource::Error("could not get the protonode's input")); - }; - - (input_type, TypeSource::RandomProtonodeImplementation) - } - DocumentNodeImplementation::Network(_network) => { - // Attempt to resolve where this import is within the nested network - let outwards_wires = self.outward_wires(&node_id_path); - let inputs_using_import = outwards_wires.and_then(|outwards_wires| outwards_wires.get(&OutputConnector::Import(input_index))); - let first_input = inputs_using_import.and_then(|input| input.first()).copied(); - - if let Some(InputConnector::Node { - node_id: child_id, - input_index: child_input_index, - }) = first_input - { - network_path.push(node_id); - let result = self.guess_type_from_node(network_path, child_id, child_input_index); - network_path.pop(); - return result; - } - - // Input is disconnected - (concrete!(()), TypeSource::Error("disconnected network input")) - } - _ => (concrete!(()), TypeSource::Error("implementation is not network or protonode")), - } - } - - /// Get the [`Type`] for any InputConnector - pub fn input_type(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> (Type, TypeSource) { - if let Some(result) = self.node_type_from_compiled(input_connector, network_path) { - return result; - } - - // Resolve types from proto nodes in node_registry - let Some(node_id) = input_connector.node_id() else { - return (concrete!(()), TypeSource::Error("input connector is not a node")); - }; - - self.guess_type_from_node(&mut network_path.to_vec(), node_id, input_connector.input_index()) - } - - pub fn valid_input_types(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> Vec { - let InputConnector::Node { node_id, input_index } = input_connector else { - // An export can have any type connected to it - return vec![graph_craft::generic!(T)]; - }; - let Some(implementation) = self.implementation(node_id, network_path) else { - log::error!("Could not get node implementation in valid_input_types"); - return Vec::new(); - }; - match implementation { - DocumentNodeImplementation::Network(_) => { - let nested_path = [network_path, &[*node_id]].concat(); - let Some(outward_wires) = self.outward_wires(&nested_path) else { - log::error!("Could not get outward wires in valid_input_types"); - return Vec::new(); - }; - let Some(inputs_from_import) = outward_wires.get(&OutputConnector::Import(*input_index)) else { - log::error!("Could not get inputs from import in valid_input_types"); - return Vec::new(); - }; - - let intersection: HashSet = inputs_from_import - .clone() - .iter() - .map(|input_connector| self.valid_input_types(input_connector, &nested_path)) - .map(|vec| vec.into_iter().collect::>()) - .fold(None, |acc: Option>, set| match acc { - Some(acc_set) => Some(acc_set.intersection(&set).cloned().collect()), - None => Some(set), - }) - .unwrap_or_default(); - - intersection.into_iter().collect::>() - } - DocumentNodeImplementation::ProtoNode(proto_node_identifier) => { - let Some(implementations) = NODE_REGISTRY.get(proto_node_identifier) else { - log::error!("Protonode {proto_node_identifier:?} not found in registry"); - return Vec::new(); - }; - let number_of_inputs = self.number_of_inputs(node_id, network_path); - implementations - .iter() - .filter_map(|(node_io, _)| { - let valid_implementation = (0..number_of_inputs).filter(|iterator_index| iterator_index != input_index).all(|iterator_index| { - let input_type = self.input_type(&InputConnector::node(*node_id, iterator_index), network_path).0; - // Value inputs are stored as concrete, so they are compared to the nested type. Node inputs are stored as fn, so they are compared to the entire type. - // For example a node input of (Footprint) -> Vector would not be compatible with () -> Vector - node_io.inputs.get(iterator_index).map(|ty| ty.nested_type().clone()).as_ref() == Some(&input_type) || node_io.inputs.get(iterator_index) == Some(&input_type) - }); - if valid_implementation { node_io.inputs.get(*input_index).cloned() } else { None } - }) - .collect::>() - } - DocumentNodeImplementation::Extract => { - log::error!("Input types for extract node not supported"); - Vec::new() - } - } - } - - /// Retrieves the output types for a given document node and its exports. - /// - /// This function traverses the node and its nested network structure (if applicable) to determine - /// the types of all outputs, including the primary output and any additional exports. - /// - /// # Arguments - /// - /// * `node` - A reference to the `DocumentNode` for which to determine output types. - /// * `resolved_types` - A reference to `ResolvedDocumentNodeTypes` containing pre-resolved type information. - /// * `node_id_path` - A slice of `NodeId`s representing the path to the current node in the document graph. - /// - /// # Returns - /// - /// A `Vec>` where: - /// - The first element is the primary output type of the node. - /// - Subsequent elements are types of additional exports (if the node is a network). - /// - `None` values indicate that a type couldn't be resolved for a particular output. - /// - /// # Behavior - /// - /// 1. Retrieves the primary output type from `resolved_types`. - /// 2. If the node is a network: - /// - Iterates through its exports (skipping the first/primary export). - /// - For each export, traverses the network until reaching a protonode or terminal condition. - /// - Determines the output type based on the final node/value encountered. - /// 3. Collects and returns all resolved types. - /// - /// # Note - /// - /// This function assumes that export indices and node IDs always exist within their respective - /// collections. It will panic if these assumptions are violated. - /// - pub fn output_type(&mut self, output_connector: &OutputConnector, network_path: &[NodeId]) -> (Type, TypeSource) { - match output_connector { - OutputConnector::Node { node_id, output_index } => { - let Some(implementation) = self.implementation(node_id, network_path) else { - log::error!("Could not get output type for node {node_id} output index {output_index}. This node is no longer supported, and needs to be upgraded."); - return (concrete!(()), TypeSource::Error("Could not get implementation")); - }; - - // If the node is not a protonode, get types by traversing across exports until a proto node is reached. - match &implementation { - graph_craft::document::DocumentNodeImplementation::Network(internal_network) => { - let Some(export) = internal_network.exports.get(*output_index) else { - return (concrete!(()), TypeSource::Error("Could not get export index")); - }; - match export { - NodeInput::Node { - node_id: nested_node_id, - output_index, - .. - } => self.output_type(&OutputConnector::node(*nested_node_id, *output_index), &[network_path, &[*node_id]].concat()), - NodeInput::Value { tagged_value, .. } => (tagged_value.ty(), TypeSource::TaggedValue), - NodeInput::Network { .. } => { - // let mut encapsulating_path = network_path.to_vec(); - // let encapsulating_node = encapsulating_path.pop().expect("No imports exist in document network"); - // self.input_type(&InputConnector::node(encapsulating_node, *import_index), network_path) - (concrete!(()), TypeSource::Error("Could not type from network")) - } - NodeInput::Scope(_) => todo!(), - NodeInput::Inline(_) => todo!(), - NodeInput::Reflection(_) => todo!(), - } - } - graph_craft::document::DocumentNodeImplementation::ProtoNode(protonode) => { - let node_id_path = &[network_path, &[*node_id]].concat(); - self.resolved_types - .types - .get(node_id_path) - .map(|ty| (ty.output.clone(), TypeSource::Compiled)) - .or_else(|| { - let node_types = random_protonode_implementation(protonode)?; - Some((node_types.return_value.clone(), TypeSource::RandomProtonodeImplementation)) - }) - .unwrap_or((concrete!(()), TypeSource::Error("Could not get protonode implementation"))) - } - graph_craft::document::DocumentNodeImplementation::Extract => (concrete!(()), TypeSource::Error("extract node")), - } - } - OutputConnector::Import(import_index) => { - let Some((encapsulating_node, encapsulating_path)) = network_path.split_last() else { - log::error!("Cannot get type of import in document network"); - return (concrete!(()), TypeSource::Error("Cannot get import type in document network")); - }; - self.input_type(&InputConnector::node(*encapsulating_node, *import_index), encapsulating_path) - } - } - } - pub fn position(&mut self, node_id: &NodeId, network_path: &[NodeId]) -> Option { let top_left_position = self .node_click_targets(node_id, network_path) @@ -780,249 +480,6 @@ impl NodeNetworkInterface { }) } - pub fn frontend_imports(&mut self, network_path: &[NodeId]) -> Vec> { - match network_path.split_last() { - Some((node_id, encapsulating_network_path)) => { - let Some(node) = self.document_node(node_id, encapsulating_network_path) else { - log::error!("Could not get node {node_id} in network {encapsulating_network_path:?}"); - return Vec::new(); - }; - let mut frontend_imports = (0..node.inputs.len()) - .map(|import_index| self.frontend_output_from_connector(&OutputConnector::Import(import_index), network_path)) - .collect::>(); - if frontend_imports.is_empty() { - frontend_imports.push(None); - } - frontend_imports - } - // In the document network display no imports - None => Vec::new(), - } - } - - pub fn frontend_exports(&mut self, network_path: &[NodeId]) -> Vec> { - let Some(network) = self.nested_network(network_path) else { return Vec::new() }; - let mut frontend_exports = ((0..network.exports.len()).map(|export_index| self.frontend_input_from_connector(&InputConnector::Export(export_index), network_path))).collect::>(); - if frontend_exports.is_empty() { - frontend_exports.push(None); - } - frontend_exports - } - - pub fn import_export_position(&mut self, network_path: &[NodeId]) -> Option<(IVec2, IVec2)> { - let Some(all_nodes_bounding_box) = self.all_nodes_bounding_box(network_path).cloned() else { - log::error!("Could not get all nodes bounding box in load_export_ports"); - return None; - }; - let Some(network) = self.nested_network(network_path) else { - log::error!("Could not get current network in load_export_ports"); - return None; - }; - - let Some(network_metadata) = self.network_metadata(network_path) else { - log::error!("Could not get nested network_metadata in load_export_ports"); - return None; - }; - let node_graph_to_viewport = network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport; - let target_viewport_top_left = DVec2::new(IMPORTS_TO_LEFT_EDGE_PIXEL_GAP as f64, IMPORTS_TO_TOP_EDGE_PIXEL_GAP as f64); - - let node_graph_pixel_offset_top_left = node_graph_to_viewport.inverse().transform_point2(target_viewport_top_left); - - // A 5x5 grid offset from the top left corner - let node_graph_grid_space_offset_top_left = node_graph_to_viewport.inverse().transform_point2(DVec2::ZERO) + DVec2::new(5. * GRID_SIZE as f64, 4. * GRID_SIZE as f64); - - // The inner bound of the import is the highest/furthest left of the two offsets - let top_left_inner_bound = DVec2::new( - node_graph_pixel_offset_top_left.x.min(node_graph_grid_space_offset_top_left.x), - node_graph_pixel_offset_top_left.y.min(node_graph_grid_space_offset_top_left.y), - ); - - let offset_from_top_left = if network - .exports - .first() - .is_some_and(|export| export.as_node().is_some_and(|export_node| self.is_layer(&export_node, network_path))) - { - DVec2::new(-4. * GRID_SIZE as f64, -2. * GRID_SIZE as f64) - } else { - DVec2::new(-4. * GRID_SIZE as f64, 0.) - }; - - let bounding_box_top_left = DVec2::new((all_nodes_bounding_box[0].x / 24. + 0.5).floor() * 24., (all_nodes_bounding_box[0].y / 24. + 0.5).floor() * 24.) + offset_from_top_left; - let import_top_left = DVec2::new(top_left_inner_bound.x.min(bounding_box_top_left.x), top_left_inner_bound.y.min(bounding_box_top_left.y)); - let rounded_import_top_left = DVec2::new((import_top_left.x / 24.).round() * 24., (import_top_left.y / 24.).round() * 24.); - - let viewport_top_right = network_metadata.persistent_metadata.navigation_metadata.node_graph_top_right; - let target_viewport_top_right = DVec2::new( - viewport_top_right.x - EXPORTS_TO_RIGHT_EDGE_PIXEL_GAP as f64, - viewport_top_right.y + EXPORTS_TO_TOP_EDGE_PIXEL_GAP as f64, - ); - - // An offset from the right edge in viewport pixels - let node_graph_pixel_offset_top_right = node_graph_to_viewport.inverse().transform_point2(target_viewport_top_right); - - // A 5x5 grid offset from the right corner - let node_graph_grid_space_offset_top_right = node_graph_to_viewport.inverse().transform_point2(viewport_top_right) + DVec2::new(-5. * GRID_SIZE as f64, 4. * GRID_SIZE as f64); - - // The inner bound of the export is the highest/furthest right of the two offsets - let top_right_inner_bound = DVec2::new( - node_graph_pixel_offset_top_right.x.max(node_graph_grid_space_offset_top_right.x), - node_graph_pixel_offset_top_right.y.min(node_graph_grid_space_offset_top_right.y), - ); - - let offset_from_top_right = if network - .exports - .first() - .is_some_and(|export| export.as_node().is_some_and(|export_node| self.is_layer(&export_node, network_path))) - { - DVec2::new(2. * GRID_SIZE as f64, -2. * GRID_SIZE as f64) - } else { - DVec2::new(4. * GRID_SIZE as f64, 0.) - }; - - let mut bounding_box_top_right = DVec2::new((all_nodes_bounding_box[1].x / 24. + 0.5).floor() * 24., (all_nodes_bounding_box[0].y / 24. + 0.5).floor() * 24.); - bounding_box_top_right += offset_from_top_right; - let export_top_right = DVec2::new(top_right_inner_bound.x.max(bounding_box_top_right.x), top_right_inner_bound.y.min(bounding_box_top_right.y)); - let rounded_export_top_right = DVec2::new((export_top_right.x / 24.).round() * 24., (export_top_right.y / 24.).round() * 24.); - - Some((rounded_import_top_left.as_ivec2(), rounded_export_top_right.as_ivec2())) - } - - /// Returns None if there is an error, it is a hidden primary export, or a hidden input - pub fn frontend_input_from_connector(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> Option { - // Return None if it is a hidden input - if self.input_from_connector(input_connector, network_path).is_some_and(|input| !input.is_exposed()) { - return None; - } - let (export_type, source) = self.input_type(input_connector, network_path); - let data_type = FrontendGraphDataType::displayed_type(&export_type, &source); - let connected_to = self - .upstream_output_connector(input_connector, network_path) - .map(|output_connector| match output_connector { - OutputConnector::Node { node_id, output_index } => { - let mut name = self.display_name(&node_id, network_path); - if cfg!(debug_assertions) { - name.push_str(&format!(" (id: {node_id})")); - } - format!("{name} output {output_index}") - } - OutputConnector::Import(import_index) => format!("Import index {import_index}"), - }) - .unwrap_or("nothing".to_string()); - - let (name, description) = match input_connector { - InputConnector::Node { node_id, input_index } => self.displayed_input_name_and_description(node_id, *input_index, network_path), - InputConnector::Export(export_index) => { - // Get export name from parent node metadata input, which must match the number of exports. - // Empty string means to use type, or "Export + index" if type is empty determined - let export_name = if network_path.is_empty() { - "Canvas".to_string() - } else { - self.encapsulating_node_metadata(network_path) - .and_then(|encapsulating_metadata| encapsulating_metadata.persistent_metadata.output_names.get(*export_index).cloned()) - .unwrap_or_default() - }; - - let export_name = if !export_name.is_empty() { - export_name - } else if *export_type.nested_type() != concrete!(()) { - export_type.nested_type().to_string() - } else { - format!("Export index {}", export_index) - }; - - (export_name, String::new()) - } - }; - Some(FrontendGraphInput { - data_type, - name, - description, - resolved_type: format!("{export_type:?}"), - valid_types: self.valid_input_types(input_connector, network_path).iter().map(|ty| ty.to_string()).collect(), - connected_to, - }) - } - - /// Returns None if there is an error, it is the document network, a hidden primary output or import - pub fn frontend_output_from_connector(&mut self, output_connector: &OutputConnector, network_path: &[NodeId]) -> Option { - let (output_type, type_source) = self.output_type(output_connector, network_path); - let (name, description) = match output_connector { - OutputConnector::Node { node_id, output_index } => { - // Do not display the primary output port for a node if it is a network node with a hidden primary export - if *output_index == 0 && self.hidden_primary_output(node_id, network_path) { - return None; - }; - // Get the output name from the interior network export name - let node_metadata = self.node_metadata(node_id, network_path)?; - let output_name = node_metadata.persistent_metadata.output_names.get(*output_index).cloned().unwrap_or_default(); - - let output_name = if !output_name.is_empty() { - output_name - } else if *output_type.nested_type() != concrete!(()) { - output_type.nested_type().to_string() - } else { - format!("Output {}", *output_index + 1) - }; - - (output_name, String::new()) - } - OutputConnector::Import(import_index) => { - // Get the import name from the encapsulating node input metadata - let Some((encapsulating_node_id, encapsulating_path)) = network_path.split_last() else { - // Return None if it is an import in the document network - return None; - }; - // Return None if the primary input is hidden and this is the primary import - if *import_index == 0 && self.hidden_primary_import(network_path) { - return None; - }; - let (import_name, description) = self.displayed_input_name_and_description(encapsulating_node_id, *import_index, encapsulating_path); - - let import_name = if *output_type.nested_type() != concrete!(()) { - import_name - } else { - format!("Import index {}", *import_index) - }; - (import_name, description) - } - }; - - let data_type = FrontendGraphDataType::displayed_type(&output_type, &type_source); - - let mut connected_to = self - .outward_wires(network_path) - .and_then(|outward_wires| outward_wires.get(output_connector)) - .cloned() - .unwrap_or_else(|| { - log::error!("Could not get {output_connector:?} in outward wires"); - Vec::new() - }) - .iter() - .map(|input| match input { - InputConnector::Node { node_id, input_index } => { - let mut name = self.display_name(node_id, network_path); - if cfg!(debug_assertions) { - name.push_str(&format!(" (id: {node_id})")); - } - format!("{name} input {input_index}") - } - InputConnector::Export(export_index) => format!("Export index {export_index}"), - }) - .collect::>(); - - if connected_to.is_empty() { - connected_to.push("nothing".to_string()); - } - - Some(FrontendGraphOutput { - data_type, - name, - resolved_type: format!("{:?}", output_type), - description, - connected_to, - }) - } - pub fn height_from_click_target(&mut self, node_id: &NodeId, network_path: &[NodeId]) -> Option { let mut node_height: Option = self .node_click_targets(node_id, network_path) @@ -1225,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 { @@ -1240,7 +690,7 @@ impl NodeNetworkInterface { }; let description = input_metadata.input_description.to_string(); let name = if input_metadata.input_name.is_empty() { - self.input_type(&InputConnector::node(*node_id, input_index), network_path).0.nested_type().to_string() + self.input_type(&InputConnector::node(*node_id, input_index), network_path).resolved_type_name() } else { input_metadata.input_name.to_string() }; @@ -1276,9 +726,9 @@ impl NodeNetworkInterface { /// Returns the description of the node, or an empty string if it is not set. pub fn description(&self, node_id: &NodeId, network_path: &[NodeId]) -> String { - self.get_node_definition(network_path, *node_id) + self.get_node_definition(node_id, network_path) .map(|node_definition| node_definition.description.to_string()) - .filter(|description| description != "TODO") + .filter(|description: &String| description != "TODO") .unwrap_or_default() } @@ -1314,25 +764,6 @@ impl NodeNetworkInterface { node_metadata.persistent_metadata.is_layer() } - pub fn primary_output_connected_to_layer(&mut self, node_id: &NodeId, network_path: &[NodeId]) -> bool { - let Some(outward_wires) = self.outward_wires(network_path) else { - log::error!("Could not get outward_wires in primary_output_connected_to_layer"); - return false; - }; - let Some(downstream_connectors) = outward_wires.get(&OutputConnector::node(*node_id, 0)) else { - log::error!("Could not get downstream_connectors in primary_output_connected_to_layer"); - return false; - }; - let downstream_nodes = downstream_connectors.iter().filter_map(|connector| connector.node_id()).collect::>(); - downstream_nodes.iter().any(|node_id| self.is_layer(node_id, network_path)) - } - - pub fn primary_input_connected_to_layer(&mut self, node_id: &NodeId, network_path: &[NodeId]) -> bool { - self.input_from_connector(&InputConnector::node(*node_id, 0), network_path) - .and_then(|input| input.as_node()) - .is_some_and(|node_id| self.is_layer(&node_id, network_path)) - } - pub fn hidden_primary_export(&self, network_path: &[NodeId]) -> bool { let Some((node_id, network_path)) = network_path.split_last() else { // The document network does not have a hidden primary export @@ -1706,30 +1137,6 @@ impl NodeNetworkInterface { } } -/// Gets the type for a random protonode implementation (used if there is no type from the compiled network) -fn random_protonode_implementation(protonode: &graph_craft::ProtoNodeIdentifier) -> Option<&graphene_std::NodeIOTypes> { - let mut protonode = protonode.clone(); - // TODO: Remove - if let Some((path, _generics)) = protonode.name.split_once('<') { - protonode = path.to_string().to_string().into(); - } - let Some(node_io_hashmap) = NODE_REGISTRY.get(&protonode) else { - log::error!("Could not get hashmap for proto node: {protonode:?}"); - return None; - }; - - let node_types = node_io_hashmap.keys().min_by_key(|node_io_types| { - let mut hasher = DefaultHasher::new(); - node_io_types.hash(&mut hasher); - hasher.finish() - }); - - if node_types.is_none() { - log::error!("Could not get node_types from hashmap"); - }; - node_types -} - // Private mutable getters for use within the network interface impl NodeNetworkInterface { fn network_mut(&mut self, network_path: &[NodeId]) -> Option<&mut NodeNetwork> { @@ -2122,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> { @@ -2237,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 { @@ -2564,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 { @@ -2681,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::from_type(&self.input_type(&input, network_path).0); - 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::from_type(&self.input_type(input, network_path).0); - 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) @@ -3053,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]) { @@ -3145,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 @@ -3190,7 +2255,6 @@ impl NodeNetworkInterface { connector_click_targets, icon_click_targets, all_nodes_bounding_box, - import_exports_bounding_box, modify_import_export, } } @@ -3437,38 +2501,6 @@ impl NodeNetworkInterface { bounding_box_subpath.bounding_box_with_transform(network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport) } - pub fn collect_layer_widths(&mut self, network_path: &[NodeId]) -> (HashMap, HashMap, HashMap) { - let Some(network_metadata) = self.network_metadata(network_path) else { - log::error!("Could not get nested network_metadata in collect_layer_widths"); - return (HashMap::new(), HashMap::new(), HashMap::new()); - }; - 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::>(); - let chain_widths = nodes.iter().map(|node_id| (*node_id, self.chain_width(node_id, network_path))).collect::>(); - let has_left_input_wire = nodes - .iter() - .map(|node_id| { - ( - *node_id, - !self - .upstream_flow_back_from_nodes(vec![*node_id], network_path, FlowType::HorizontalFlow) - .skip(1) - .all(|node_id| self.is_chain(&node_id, network_path)), - ) - }) - .collect::>(); - - (layer_widths, chain_widths, has_left_input_wire) - } - pub fn compute_modified_vector(&self, layer: LayerNodeIdentifier) -> Option { let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, self); @@ -3643,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"); @@ -4388,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); @@ -4491,7 +3508,7 @@ impl NodeNetworkInterface { } } - let tagged_value = TaggedValue::from_type_or_none(&self.input_type(input_connector, network_path).0); + let tagged_value = self.tagged_value_from_input(input_connector, network_path); let value_input = NodeInput::value(tagged_value, true); @@ -6111,22 +5128,6 @@ impl Iterator for FlowIter<'_> { } } -// TODO: Refactor to be Unknown, Compiled(Type) for NodeInput::Node, or Value(Type) for NodeInput::Value -/// Represents the source of a resolved type (for debugging). -/// There will be two valid types list. One for the current valid types that will not cause a node graph error, -/// based on the other inputs to that node and returned during compilation. THe other list will be all potential -/// Valid types, based on the protonode implementation/downstream users. -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)] -pub enum TypeSource { - Compiled, - RandomProtonodeImplementation, - DocumentNodeDefault, - TaggedValue, - OuterMostExportDefault, - - Error(&'static str), -} - #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)] pub enum ImportOrExport { Import(usize), @@ -6174,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 { @@ -6458,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)] @@ -6641,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)] @@ -6685,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 new file mode 100644 index 0000000000..5d459fcf8a --- /dev/null +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/node_graph.rs @@ -0,0 +1,548 @@ +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::{ + 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}, + }, + }, +}; + +// 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, 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(); + }; + let selected_nodes = self.selected_nodes_in_nested_network(network_path).unwrap_or_default(); + let mut nodes = Vec::new(); + for (node_id, visible) in network.nodes.iter().map(|(node_id, node)| (*node_id, node.visible)).collect::>() { + let node_id_path = [network_path, &[node_id]].concat(); + + let errors = node_graph_errors + .iter() + .find(|error| error.node_path == node_id_path) + .map(|error| format!("{:?}", error.error.clone())) + .or_else(|| { + if node_graph_errors.iter().any(|error| error.node_path.starts_with(&node_id_path)) { + Some("Node graph type error within this node".to_string()) + } else { + None + } + }); + + let metadata = FrontendNodeMetadata { + node_id, + can_be_layer: self.is_eligible_to_be_layer(&node_id, network_path), + display_name: self.display_name(&node_id, network_path), + selected: selected_nodes.0.contains(&node_id), + reference: self.reference(&node_id, network_path).cloned().unwrap_or_default(), + visible, + errors, + }; + + let node_or_layer = match self.is_layer(&node_id, network_path) { + true => { + let Some(position) = self.position(&node_id, network_path) else { + log::error!("Could not get position for node: {node_id}"); + continue; + }; + let position = FrontendXY { x: position.x, y: position.y }; + + let Some(bottom_input) = self.frontend_input_from_connector(&InputConnector::node(node_id, 0), network_path) else { + log::error!("Layer must have a visible primary input"); + continue; + }; + let side_input = self.frontend_input_from_connector(&InputConnector::node(node_id, 1), network_path); + let Some(output) = self.frontend_output_from_connector(&OutputConnector::node(node_id, 0), network_path) else { + log::error!("Layer must have a visible primary output"); + continue; + }; + + let layer = Some(FrontendLayer { + bottom_input, + side_input, + output, + position, + locked: self.is_locked(&node_id, network_path), + chain_width: self.chain_width(&node_id, network_path), + layer_has_left_border_gap: self.layer_has_left_border_gap(&node_id, network_path), + primary_input_connected_to_layer: self.primary_input_connected_to_layer(&node_id, network_path), + primary_output_connected_to_layer: self.primary_output_connected_to_layer(&node_id, network_path), + }); + FrontendNodeOrLayer { node: None, layer } + } + false => { + let Some(position) = self.position(&node_id, network_path) else { + log::error!("Could not get position for node: {node_id}"); + continue; + }; + + let position = FrontendXY { x: position.x, y: position.y }; + + let 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 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, + primary_input, + primary_output, + secondary_inputs, + secondary_outputs, + }); + + FrontendNodeOrLayer { node, layer: None } + } + }; + + 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); + } + nodes + } + + /// Returns None if there is an error, it is a hidden primary export, or a hidden input + pub fn frontend_input_from_connector(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> Option { + // Return None if it is a hidden input or doesn't exist + if self.input_from_connector(input_connector, network_path).is_some_and(|input| !input.is_exposed()) { + return None; + } + let input_type = self.input_type(input_connector, network_path); + let data_type = input_type.displayed_type(); + let resolved_type = input_type.resolved_type_name(); + + let connected_to = self + .upstream_output_connector(input_connector, network_path) + .map(|output_connector| match output_connector { + OutputConnector::Node { node_id, output_index } => { + let mut name = self.display_name(&node_id, network_path); + if cfg!(debug_assertions) { + name.push_str(&format!(" (id: {node_id})")); + } + format!("{name} output {output_index}") + } + OutputConnector::Import(import_index) => format!("Import index {import_index}"), + }) + .unwrap_or("nothing".to_string()); + + let (name, description) = match input_connector { + InputConnector::Node { node_id, input_index } => self.displayed_input_name_and_description(node_id, *input_index, network_path), + InputConnector::Export(export_index) => { + // Get export name from parent node metadata input, which must match the number of exports. + // Empty string means to use type, or "Export + index" if type is empty determined + let export_name = if network_path.is_empty() { + "Canvas".to_string() + } else { + self.encapsulating_node_metadata(network_path) + .and_then(|encapsulating_metadata| encapsulating_metadata.persistent_metadata.output_names.get(*export_index).cloned()) + .unwrap_or_default() + }; + + let export_name = if !export_name.is_empty() { + export_name + } else if let Some(export_type_name) = input_type.compiled_nested_type_name() { + export_type_name + } else { + format!("Export index {}", export_index) + }; + + (export_name, String::new()) + } + }; + + // TODO: Move in separate Tooltip overlay + // let valid_types = match self.valid_input_types(&input_connector, network_path) { + // Ok(input_types) => input_types.iter().map(|ty| ty.to_string()).collect(), + // Err(e) => { + // log::error!("Error getting valid types for input {input_connector:?}: {e}"); + // Vec::new() + // } + // }; + + let connected_to_node = self.upstream_output_connector(input_connector, network_path).and_then(|output_connector| output_connector.node_id()); + + Some(FrontendGraphInput { + data_type, + resolved_type, + name, + description, + connected_to, + connected_to_node, + }) + } + + /// Returns None if there is an error, it is the document network, a hidden primary output or import + pub fn frontend_output_from_connector(&mut self, output_connector: &OutputConnector, network_path: &[NodeId]) -> Option { + let output_type = self.output_type(output_connector, network_path); + + let (name, description) = match output_connector { + OutputConnector::Node { node_id, output_index } => { + // Do not display the primary output port for a node if it is a network node with a hidden primary export + if *output_index == 0 && self.hidden_primary_output(node_id, network_path) { + return None; + }; + // Get the output name from the interior network export name + let node_metadata = self.node_metadata(node_id, network_path)?; + let output_name = node_metadata.persistent_metadata.output_names.get(*output_index).cloned().unwrap_or_default(); + + let output_name = if !output_name.is_empty() { output_name } else { output_type.resolved_type_name() }; + (output_name, String::new()) + } + OutputConnector::Import(import_index) => { + // Get the import name from the encapsulating node input metadata + let Some((encapsulating_node_id, encapsulating_path)) = network_path.split_last() else { + // Return None if it is an import in the document network + return None; + }; + // Return None if the primary input is hidden and this is the primary import + if *import_index == 0 && self.hidden_primary_import(network_path) { + return None; + }; + let (import_name, description) = self.displayed_input_name_and_description(encapsulating_node_id, *import_index, encapsulating_path); + + let import_name = if !import_name.is_empty() { + import_name + } else if let Some(import_type_name) = output_type.compiled_nested_type_name() { + import_type_name + } else { + format!("Import index {}", *import_index) + }; + + (import_name, description) + } + }; + let data_type = output_type.displayed_type(); + let resolved_type = output_type.resolved_type_name(); + let mut connected_to = self + .outward_wires(network_path) + .and_then(|outward_wires| outward_wires.get(output_connector)) + .cloned() + .unwrap_or_else(|| { + log::error!("Could not get {output_connector:?} in outward wires"); + Vec::new() + }) + .iter() + .map(|input| match input { + InputConnector::Node { node_id, input_index } => { + let mut name = self.display_name(node_id, network_path); + if cfg!(debug_assertions) { + name.push_str(&format!(" (id: {node_id})")); + } + format!("{name} input {input_index}") + } + InputConnector::Export(export_index) => format!("Export index {export_index}"), + }) + .collect::>(); + + if connected_to.is_empty() { + connected_to.push("nothing".to_string()); + } + + Some(FrontendGraphOutput { + data_type, + resolved_type, + name, + description, + connected_to, + }) + } + + pub fn chain_width(&self, node_id: &NodeId, network_path: &[NodeId]) -> u32 { + if self.number_of_displayed_inputs(node_id, network_path) > 1 { + let mut last_chain_node_distance = 0u32; + // Iterate upstream from the layer, and get the number of nodes distance to the last node with Position::Chain + for (index, node_id) in self + .upstream_flow_back_from_nodes(vec![*node_id], network_path, FlowType::HorizontalPrimaryOutputFlow) + .skip(1) + .enumerate() + .collect::>() + { + // Check if the node is positioned as a chain + if self.is_chain(&node_id, network_path) { + last_chain_node_distance = (index as u32) + 1; + } else { + return last_chain_node_distance * 7 + 1; + } + } + + last_chain_node_distance * 7 + 1 + } else { + // Layer with no inputs has no chain + 0 + } + } + + /// Checks if a layer should display a gap in its left border + pub fn layer_has_left_border_gap(&self, node_id: &NodeId, network_path: &[NodeId]) -> bool { + self.upstream_flow_back_from_nodes(vec![*node_id], network_path, FlowType::HorizontalFlow).skip(1).any(|node_id| { + !self.is_chain(&node_id, network_path) + || self + .upstream_output_connector(&InputConnector::node(node_id, 0), network_path) + .is_some_and(|output_connector| matches!(output_connector, OutputConnector::Import(_))) + }) + } + + /// Returns the node which should have a dashed border drawn around it + pub fn previewed_node(&self, network_path: &[NodeId]) -> Option { + self.upstream_output_connector(&InputConnector::Export(0), network_path) + .and_then(|output_connector| output_connector.node_id()) + .filter(|output_node| self.root_node(network_path).is_some_and(|root_node| root_node.node_id != *output_node)) + } + + /// If any downstream input are bottom layer inputs, then the thick cap should be displayed above the output port + fn primary_output_connected_to_layer(&mut self, node_id: &NodeId, network_path: &[NodeId]) -> bool { + let Some(outward_wires) = self.outward_wires(network_path) else { + log::error!("Could not get outward_wires in primary_output_connected_to_layer"); + return false; + }; + let Some(downstream_connectors) = outward_wires.get(&OutputConnector::node(*node_id, 0)) else { + log::error!("Could not get downstream_connectors in primary_output_connected_to_layer"); + return false; + }; + let downstream_nodes = downstream_connectors + .iter() + .filter_map(|connector| if connector.input_index() == 0 { connector.node_id() } else { None }) + .collect::>(); + downstream_nodes.iter().any(|node_id| self.is_layer(node_id, network_path)) + } + + /// If any upstream nodes are layers, then the thick cap should be displayed below the primary input port + fn primary_input_connected_to_layer(&mut self, node_id: &NodeId, network_path: &[NodeId]) -> bool { + self.input_from_connector(&InputConnector::node(*node_id, 0), network_path) + .and_then(|input| input.as_node()) + .is_some_and(|node_id| self.is_layer(&node_id, network_path)) + } + + /// 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, 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| { + 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); + } + frontend_imports + } + // In the document network display no imports + None => Vec::new(), + } + } + + /// 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); + } + 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)> { + let Some(all_nodes_bounding_box) = self.all_nodes_bounding_box(network_path).cloned() else { + log::error!("Could not get all nodes bounding box in load_export_ports"); + return None; + }; + let Some(network) = self.nested_network(network_path) else { + log::error!("Could not get current network in load_export_ports"); + return None; + }; + + let Some(network_metadata) = self.network_metadata(network_path) else { + log::error!("Could not get nested network_metadata in load_export_ports"); + return None; + }; + let node_graph_to_viewport = network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport; + let target_viewport_top_left = DVec2::new(IMPORTS_TO_LEFT_EDGE_PIXEL_GAP as f64, IMPORTS_TO_TOP_EDGE_PIXEL_GAP as f64); + + let node_graph_pixel_offset_top_left = node_graph_to_viewport.inverse().transform_point2(target_viewport_top_left); + + // A 5x5 grid offset from the top left corner + let node_graph_grid_space_offset_top_left = node_graph_to_viewport.inverse().transform_point2(DVec2::ZERO) + DVec2::new(5. * GRID_SIZE as f64, 4. * GRID_SIZE as f64); + + // The inner bound of the import is the highest/furthest left of the two offsets + let top_left_inner_bound = DVec2::new( + node_graph_pixel_offset_top_left.x.min(node_graph_grid_space_offset_top_left.x), + node_graph_pixel_offset_top_left.y.min(node_graph_grid_space_offset_top_left.y), + ); + + let offset_from_top_left = if network + .exports + .first() + .is_some_and(|export| export.as_node().is_some_and(|export_node| self.is_layer(&export_node, network_path))) + { + DVec2::new(-4. * GRID_SIZE as f64, -2. * GRID_SIZE as f64) + } else { + DVec2::new(-4. * GRID_SIZE as f64, 0.) + }; + + let bounding_box_top_left = DVec2::new((all_nodes_bounding_box[0].x / 24. + 0.5).floor() * 24., (all_nodes_bounding_box[0].y / 24. + 0.5).floor() * 24.) + offset_from_top_left; + let import_top_left = DVec2::new(top_left_inner_bound.x.min(bounding_box_top_left.x), top_left_inner_bound.y.min(bounding_box_top_left.y)); + let rounded_import_top_left = DVec2::new((import_top_left.x / 24.).round() * 24., (import_top_left.y / 24.).round() * 24.); + + let viewport_top_right = network_metadata.persistent_metadata.navigation_metadata.node_graph_top_right; + let target_viewport_top_right = DVec2::new( + viewport_top_right.x - EXPORTS_TO_RIGHT_EDGE_PIXEL_GAP as f64, + viewport_top_right.y + EXPORTS_TO_TOP_EDGE_PIXEL_GAP as f64, + ); + + // An offset from the right edge in viewport pixels + let node_graph_pixel_offset_top_right = node_graph_to_viewport.inverse().transform_point2(target_viewport_top_right); + + // A 5x5 grid offset from the right corner + let node_graph_grid_space_offset_top_right = node_graph_to_viewport.inverse().transform_point2(viewport_top_right) + DVec2::new(-5. * GRID_SIZE as f64, 4. * GRID_SIZE as f64); + + // The inner bound of the export is the highest/furthest right of the two offsets + let top_right_inner_bound = DVec2::new( + node_graph_pixel_offset_top_right.x.max(node_graph_grid_space_offset_top_right.x), + node_graph_pixel_offset_top_right.y.min(node_graph_grid_space_offset_top_right.y), + ); + + let offset_from_top_right = if network + .exports + .first() + .is_some_and(|export| export.as_node().is_some_and(|export_node| self.is_layer(&export_node, network_path))) + { + DVec2::new(2. * GRID_SIZE as f64, -2. * GRID_SIZE as f64) + } else { + DVec2::new(4. * GRID_SIZE as f64, 0.) + }; + + let mut bounding_box_top_right = DVec2::new((all_nodes_bounding_box[1].x / 24. + 0.5).floor() * 24., (all_nodes_bounding_box[0].y / 24. + 0.5).floor() * 24.); + bounding_box_top_right += offset_from_top_right; + let export_top_right = DVec2::new(top_right_inner_bound.x.max(bounding_box_top_right.x), top_right_inner_bound.y.min(bounding_box_top_right.y)); + let rounded_export_top_right = DVec2::new((export_top_right.x / 24.).round() * 24., (export_top_right.y / 24.).round() * 24.); + + Some((rounded_import_top_left.as_ivec2(), rounded_export_top_right.as_ivec2())) + } + + 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 new file mode 100644 index 0000000000..bc04dc1bc7 --- /dev/null +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/resolved_types.rs @@ -0,0 +1,368 @@ +use std::collections::{HashMap, HashSet}; + +use graph_craft::{ + ProtoNodeIdentifier, Type, concrete, + document::{DocumentNodeImplementation, InlineRust, NodeInput, value::TaggedValue}, +}; +use graphene_std::{node_graph_overlay::types::FrontendGraphDataType, uuid::NodeId}; +use interpreted_executor::{ + dynamic_executor::{NodeTypes, ResolvedDocumentNodeTypesDelta}, + node_registry::NODE_REGISTRY, +}; + +use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface, OutputConnector}; + +// This file contains utility methods for interfacing with the resolved types returned from the compiler +#[derive(Debug, Default)] +pub struct ResolvedDocumentNodeTypes { + pub types: HashMap, NodeTypes>, +} + +impl ResolvedDocumentNodeTypes { + pub fn update(&mut self, delta: ResolvedDocumentNodeTypesDelta) { + for (path, node_type) in delta.add { + self.types.insert(path.to_vec(), node_type); + } + for path in delta.remove { + self.types.remove(&path.to_vec()); + } + } +} + +/// Represents the result of a type query for an input or output connector. +#[derive(Debug, Clone, PartialEq)] +pub enum TypeSource { + // A type that has been compiled based on all upstream types + Compiled(Type), + // The type of value inputs + TaggedValue(Type), + // A type that is guessed from the document node definition + DocumentNodeDefinition(Type), + // When the input is not compiled, the type is unknown and must be guessed from the valid types + Unknown, + + Error(&'static str), +} + +impl TypeSource { + pub fn 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()), + TypeSource::TaggedValue(value_type) => Some(value_type.into_nested_type()), + _ => None, + } + } + + pub fn compiled_nested_type(&self) -> Option<&Type> { + match self { + TypeSource::Compiled(compiled_type) => Some(compiled_type.nested_type()), + TypeSource::TaggedValue(value_type) => Some(value_type.nested_type()), + _ => None, + } + } + + // If Some, the type should be displayed in the imports/exports, if None it should be replaced with "import/export index _" + pub fn compiled_nested_type_name(&self) -> Option { + self.compiled_nested_type().map(|ty| ty.to_string()) + } + + // Used when searching for nodes in the add Node popup + pub fn add_node_string(&self) -> Option { + self.compiled_nested_type().map(|ty| format!("type:{}", ty.to_string())) + } + + // The type to display in the tooltip + pub fn resolved_type_name(&self) -> String { + match self { + TypeSource::Compiled(compiled_type) => compiled_type.nested_type().to_string(), + TypeSource::TaggedValue(value_type) => value_type.nested_type().to_string(), + TypeSource::DocumentNodeDefinition(_) => "Unknown".to_string(), + TypeSource::Unknown => "Unknown".to_string(), + TypeSource::Error(_) => "Error".to_string(), + } + } +} + +impl NodeNetworkInterface { + /// Get the [`TypeSource`] for any InputConnector + /// If the input is not compiled, then an Unknown or default from the definition is returned + pub fn input_type(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> TypeSource { + let Some(input) = self.input_from_connector(input_connector, network_path) else { + return TypeSource::Error("Could not get input from connector"); + }; + + match input { + NodeInput::Node { node_id, output_index } => { + let input_type = self.output_type(&OutputConnector::node(*node_id, *output_index), network_path); + if input_type == TypeSource::Unknown { + // If we are trying to get the input type of an unknown node, check if it has a reference to its definition and use that input type + if let InputConnector::Node { node_id, input_index } = input_connector { + if let Some(definition) = self.get_node_definition(node_id, network_path) { + if let Some(ty) = definition + .node_template + .document_node + .inputs + .get(*input_index) + .cloned() + .and_then(|input| input.as_value().map(|value| value.ty())) + { + return TypeSource::DocumentNodeDefinition(ty); + } + } + } + } + input_type + } + NodeInput::Value { tagged_value, .. } => TypeSource::TaggedValue(tagged_value.ty()), + NodeInput::Network { import_index, .. } => { + // Get the input type of the encapsulating node input + let Some((encapsulating_node, encapsulating_path)) = network_path.split_last() else { + return TypeSource::Error("Could not get type of import in document network"); + }; + self.input_type(&InputConnector::node(*encapsulating_node, *import_index), encapsulating_path) + } + NodeInput::Scope(_) => TypeSource::Compiled(concrete!(())), + NodeInput::Reflection(document_node_metadata) => TypeSource::Compiled(document_node_metadata.ty()), + NodeInput::Inline(_) => TypeSource::Compiled(concrete!(InlineRust)), + } + } + + // Gets the default tagged value for an input. If its not compiled, then it tries to get a valid type. If there are no valid types, then it picks a random implementation + pub fn tagged_value_from_input(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> TaggedValue { + let guaranteed_type = match self.input_type(input_connector, network_path) { + TypeSource::Compiled(compiled) => compiled, + TypeSource::TaggedValue(value) => value, + TypeSource::DocumentNodeDefinition(definition) => definition, + TypeSource::Unknown => { + let mut valid_types = match self.valid_input_types(input_connector, network_path) { + Ok(types) => types, + Err(e) => { + log::error!("Error getting valid_input_types for {input_connector:?}: {e}"); + Vec::new() + } + }; + match valid_types.pop() { + Some(valid_type) => valid_type, + None => { + match self.random_downstream_type_from_connector(input_connector, network_path) { + Some(random_type) => random_type, + // If there are no connected protonodes then we give up and return the empty type + None => concrete!(()), + } + } + } + } + TypeSource::Error(e) => { + log::error!("Error getting tagged_value_from_input for {input_connector:?} {e}"); + concrete!(()) + } + }; + TaggedValue::from_type_or_none(&guaranteed_type) + } + + pub fn valid_input_types(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> Result, String> { + match input_connector { + InputConnector::Node { node_id, input_index } => { + let Some(implementation) = self.implementation(node_id, network_path) else { + return Err(format!("Could not get node implementation for {:?} {} in valid_input_types", network_path, *node_id)); + }; + match implementation { + DocumentNodeImplementation::Network(_) => self.valid_output_types(&OutputConnector::Import(input_connector.input_index()), &[network_path, &[*node_id]].concat()), + DocumentNodeImplementation::ProtoNode(proto_node_identifier) => { + let Some(implementations) = NODE_REGISTRY.get(proto_node_identifier) else { + return Err(format!("Protonode {proto_node_identifier:?} not found in registry")); + }; + let valid_output_types = match self.valid_output_types(&OutputConnector::node(*node_id, 0), network_path) { + Ok(valid_types) => valid_types, + Err(e) => return Err(e), + }; + + let valid_types = implementations + .iter() + .filter_map(|(node_io, _)| { + if !valid_output_types.iter().any(|output_type| output_type.nested_type() == node_io.return_value.nested_type()) { + return None; + } + + let valid_inputs = (0..node_io.inputs.len()).filter(|iterator_index| iterator_index != input_index).all(|iterator_index| { + let input_type = self.input_type(&InputConnector::node(*node_id, iterator_index), network_path); + match input_type.into_compiled_nested_type() { + Some(input_type) => node_io.inputs.get(iterator_index).map(|input_type| input_type.nested_type()) == Some(&input_type), + None => true, + } + }); + if valid_inputs { node_io.inputs.get(*input_index).cloned() } else { None } + }) + .collect::>(); + Ok(valid_types) + } + DocumentNodeImplementation::Extract => { + log::error!("Input types for extract node not supported"); + Ok(Vec::new()) + } + } + } + InputConnector::Export(export_index) => { + match network_path.split_last() { + Some((encapsulating_node, encapsulating_path)) => self.valid_output_types(&OutputConnector::node(*encapsulating_node, *export_index), encapsulating_path), + None => { + // Valid types for the export are all types that can be fed into the render node + // TODO: Use ::IDENTIFIER + let render_node = "graphene_std::wasm_application_io::RenderNode"; + let Some(implementations) = NODE_REGISTRY.get(&ProtoNodeIdentifier::new(render_node)) else { + return Err(format!("Protonode {render_node:?} not found in registry")); + }; + Ok(implementations.iter().map(|(types, _)| types.inputs[1].clone()).collect()) + } + } + } + } + } + + /// Retrieves the output types for a given document node and its exports. + /// + /// This function traverses the node and its nested network structure (if applicable) to determine + /// the type of the output + /// + /// # Arguments + /// + /// * `node` - A reference to the `DocumentNode` for which to determine output types. + /// * `resolved_types` - A reference to `ResolvedDocumentNodeTypes` containing pre-resolved type information. + /// * `node_id_path` - A slice of `NodeId`s representing the path to the current node in the document graph. + /// + /// # Behavior + /// + /// 1. Retrieves the primary output type from `resolved_types`. + /// 2. If the node is a network: + /// - Iterates through its exports (skipping the first/primary export). + /// - For each export, traverses the network until reaching a protonode or terminal condition. + /// - Determines the output type based on the final node/value encountered. + /// 3. Collects and returns all resolved types. + /// + pub fn output_type(&mut self, output_connector: &OutputConnector, network_path: &[NodeId]) -> TypeSource { + match output_connector { + OutputConnector::Node { node_id, output_index } => { + // First try iterating upstream to the first protonode and try get its compiled type + let Some(implementation) = self.implementation(node_id, network_path) else { + return TypeSource::Error("Could not get implementation"); + }; + match implementation { + DocumentNodeImplementation::Network(_) => self.input_type(&InputConnector::Export(*output_index), &[network_path, &[*node_id]].concat()), + DocumentNodeImplementation::ProtoNode(_) => match self.resolved_types.types.get(&[network_path, &[*node_id]].concat()) { + Some(resolved_type) => TypeSource::Compiled(resolved_type.output.clone()), + None => TypeSource::Unknown, + }, + DocumentNodeImplementation::Extract => TypeSource::Compiled(concrete!(())), + } + } + OutputConnector::Import(import_index) => { + let Some((encapsulating_node, encapsulating_path)) = network_path.split_last() else { + return TypeSource::Error("Cannot get import type in document network"); + }; + self.input_type(&InputConnector::node(*encapsulating_node, *import_index), encapsulating_path) + } + } + } + // The valid output types are all types that are valid for each downstream connection + pub fn valid_output_types(&mut self, output_connector: &OutputConnector, network_path: &[NodeId]) -> Result, String> { + let Some(outward_wires) = self.outward_wires(&network_path) else { + return Err("Could not get outward wires in valid_input_types".to_string()); + }; + let Some(inputs_from_import) = outward_wires.get(output_connector) else { + return Err("Could not get inputs from import in valid_input_types".to_string()); + }; + + let intersection = inputs_from_import + .clone() + .iter() + .filter_map(|input_connector| match self.valid_input_types(input_connector, &network_path) { + Ok(valid_types) => Some(valid_types), + Err(e) => { + log::error!("Error getting valid types in intersection: {e}"); + None + } + }) + .map(|vec| vec.into_iter().collect::>()) + .fold(None, |acc: Option>, set| match acc { + Some(acc_set) => Some(acc_set.intersection(&set).cloned().collect()), + None => Some(set), + }) + .unwrap_or_default(); + + Ok(intersection.into_iter().collect::>()) + } + + pub fn random_downstream_type_from_connector(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> Option { + match input_connector { + InputConnector::Node { node_id, input_index } => { + let Some(implementation) = self.implementation(node_id, network_path) else { + log::error!("Could not get node {node_id} in random_downstream_protonode_from_connector"); + return None; + }; + match implementation { + DocumentNodeImplementation::Network(_) => { + let Some(outward_wires) = self.outward_wires(&network_path) else { + log::error!("Could not get outward wires in random_downstream_protonode_from_connector"); + return None; + }; + let Some(inputs_from_import) = outward_wires.get(&OutputConnector::Import(*input_index)) else { + log::error!("Could not get inputs from import in valid_input_types"); + return None; + }; + let Some(first_input) = inputs_from_import.first().cloned() else { + return None; + }; + self.random_downstream_type_from_connector(&first_input, &[network_path, &[*node_id]].concat()) + } + DocumentNodeImplementation::ProtoNode(proto_node_identifier) => { + let Some(implementations) = NODE_REGISTRY.get(proto_node_identifier) else { + log::error!("Protonode {proto_node_identifier:?} not found in registry"); + return None; + }; + implementations.keys().next().and_then(|node_io| node_io.inputs.get(input_connector.input_index())).cloned() + } + DocumentNodeImplementation::Extract => None, + } + } + InputConnector::Export(export_index) => network_path.split_last().and_then(|(encapsulating_node, encapsulating_path)| { + let Some(outward_wires) = self.outward_wires(&encapsulating_path) else { + log::error!("Could not get outward wires in random_downstream_protonode_from_connector export"); + return None; + }; + let Some(inputs_from_import) = outward_wires.get(&OutputConnector::node(*encapsulating_node, *export_index)) else { + log::error!("Could not get inputs from import in valid_input_types"); + return None; + }; + let Some(first_input) = inputs_from_import.first().cloned() else { + return None; + }; + self.random_downstream_type_from_connector(&first_input, encapsulating_path) + }), + } + } +} diff --git a/editor/src/messages/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/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index 3227832c9b..8a7740de43 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -483,8 +483,8 @@ impl<'a> NodeGraphLayer<'a> { /// Check if a layer is a raster layer pub fn is_raster_layer(layer: LayerNodeIdentifier, network_interface: &mut NodeNetworkInterface) -> bool { - let layer_input_type = network_interface.input_type(&InputConnector::node(layer.to_node(), 1), &[]).0.nested_type().clone(); + let layer_input_type = network_interface.input_type(&InputConnector::node(layer.to_node(), 1), &[]).into_compiled_nested_type(); - layer_input_type == concrete!(Table>) || layer_input_type == concrete!(Table>) + layer_input_type == Some(concrete!(Table>)) || layer_input_type == Some(concrete!(Table>)) } } diff --git a/editor/src/messages/tool/common_functionality/utility_functions.rs b/editor/src/messages/tool/common_functionality/utility_functions.rs index 4d0a268449..9eaa3dd1e3 100644 --- a/editor/src/messages/tool/common_functionality/utility_functions.rs +++ b/editor/src/messages/tool/common_functionality/utility_functions.rs @@ -581,8 +581,8 @@ pub fn make_path_editable_is_allowed(network_interface: &mut NodeNetworkInterfac // Must be a layer of type Table let node_id = NodeGraphLayer::new(first_layer, network_interface).horizontal_layer_flow().nth(1)?; - let (output_type, _) = network_interface.output_type(&OutputConnector::node(node_id, 0), &[]); - if output_type.nested_type() != concrete!(Table).nested_type() { + let output_type = network_interface.output_type(&OutputConnector::node(node_id, 0), &[]); + if output_type.into_compiled_nested_type() != Some(concrete!(Table)) { return None; } diff --git a/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 addadae0c2..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::UpdateNodeGraphNodes { nodes, .. } = self else { return }; - - for node in nodes { - if let Some(error) = &node.errors { - panic!("error on {}: {}", node.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 c259c9ba74..5079189ce7 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -564,8 +564,7 @@ {/if} - -
+
@@ -588,7 +587,6 @@ thumbPosition={scrollbarPos.x} on:trackShift={({ detail }) => 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)} /> @@ -835,18 +833,6 @@ pointer-events: auto; opacity: 1; } - - &::before { - content: ""; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: var(--color-2-mildblack); - opacity: var(--fade-artwork); - pointer-events: none; - } } .fade-artwork, diff --git a/frontend/src/components/views/Graph.svelte b/frontend/src/components/views/Graph.svelte index 7ac2d4f162..73da5c4f48 100644 --- a/frontend/src/components/views/Graph.svelte +++ b/frontend/src/components/views/Graph.svelte @@ -4,7 +4,7 @@ import { fade } from "svelte/transition"; import type { Editor } from "@graphite/editor"; - import type { FrontendGraphInput, FrontendGraphOutput } from "@graphite/messages"; + import { type FrontendGraphInput, type FrontendGraphOutput } from "@graphite/messages"; import type { NodeGraphState } from "@graphite/state-providers/node-graph"; import type { IconName } from "@graphite/utility-functions/icons"; @@ -116,12 +116,12 @@ } function toggleLayerDisplay(displayAsLayer: boolean, toggleId: bigint) { - let node = $nodeGraph.nodes.get(toggleId); - if (node) editor.handle.setToNodeOrLayer(node.id, displayAsLayer); + editor.handle.setToNodeOrLayer(toggleId, displayAsLayer); + editor.handle.setToNodeOrLayer(toggleId, displayAsLayer); } function canBeToggledBetweenNodeAndLayer(toggleDisplayAsLayerNodeId: bigint) { - return $nodeGraph.nodes.get(toggleDisplayAsLayerNodeId)?.canBeLayer || false; + return $nodeGraph.nodesToRender.get(toggleDisplayAsLayerNodeId)?.metadata.canBeLayer || false; } function createNode(nodeType: string) { @@ -130,25 +130,29 @@ editor.handle.createNode(nodeType, $nodeGraph.contextMenuInformation.contextMenuCoordinates.x, $nodeGraph.contextMenuInformation.contextMenuCoordinates.y); } - function nodeBorderMask(nodeWidth: number, primaryInputExists: boolean, exposedSecondaryInputs: number, primaryOutputExists: boolean, exposedSecondaryOutputs: number): string { - const nodeHeight = Math.max(1 + exposedSecondaryInputs, 1 + exposedSecondaryOutputs) * 24; + function nodeBorderMask(nodeInputs: (FrontendGraphInput | undefined)[], nodeOutputs: (FrontendGraphOutput | undefined)[]): string { + const nodeWidth = 120; + const secondaryInputs = nodeInputs.slice(1).filter((x): x is FrontendGraphInput => x !== undefined); + const secondaryOutputs = nodeOutputs.slice(1); + + const nodeHeight = Math.max(1 + secondaryInputs.length, 1 + secondaryOutputs.length) * 24; const boxes: { x: number; y: number; width: number; height: number }[] = []; // Primary input - if (primaryInputExists) boxes.push({ x: -8, y: 4, width: 16, height: 16 }); + if (nodeInputs[0]) boxes.push({ x: -8, y: 4, width: 16, height: 16 }); // Secondary inputs - for (let i = 0; i < exposedSecondaryInputs; i++) boxes.push({ x: -8, y: 4 + (i + 1) * 24, width: 16, height: 16 }); + for (let i = 0; i < secondaryInputs.length; i++) boxes.push({ x: -8, y: 4 + (i + 1) * 24, width: 16, height: 16 }); // Primary output - if (primaryOutputExists) boxes.push({ x: nodeWidth - 8, y: 4, width: 16, height: 16 }); + if (nodeOutputs[0]) boxes.push({ x: nodeWidth - 8, y: 4, width: 16, height: 16 }); // Exposed outputs - for (let i = 0; i < exposedSecondaryOutputs; i++) boxes.push({ x: nodeWidth - 8, y: 4 + (i + 1) * 24, width: 16, height: 16 }); + for (let i = 0; i < secondaryOutputs.length; i++) boxes.push({ x: nodeWidth - 8, y: 4 + (i + 1) * 24, width: 16, height: 16 }); return borderMask(boxes, nodeWidth, nodeHeight); } - function layerBorderMask(nodeWidthFromThumbnail: number, nodeChainAreaLeftExtension: number, hasLeftInputWire: boolean): string { + function layerBorderMask(nodeWidthFromThumbnail: number, nodeChainAreaLeftExtension: number, layerHasLeftBorderGap: boolean): string { const NODE_HEIGHT = 2 * 24; const THUMBNAIL_WIDTH = 72 + 8 * 2; const FUDGE_HEIGHT_BEYOND_LAYER_HEIGHT = 2; @@ -158,7 +162,7 @@ const boxes: { x: number; y: number; width: number; height: number }[] = []; // Left input - if (hasLeftInputWire && nodeChainAreaLeftExtension > 0) { + if (layerHasLeftBorderGap && nodeChainAreaLeftExtension > 0) { boxes.push({ x: -8, y: 16, width: 16, height: 16 }); } @@ -176,14 +180,22 @@ return `M-2,-2 L${nodeWidth + 2},-2 L${nodeWidth + 2},${nodeHeight + 2} L-2,${nodeHeight + 2}z ${rectangles.join(" ")}`; } + function inputTooltip(value: FrontendGraphInput): string { + return dataTypeTooltip(value) + "\n\n" + inputConnectedToText(value) + "\n\n"; + } + + function outputTooltip(value: FrontendGraphOutput): string { + return dataTypeTooltip(value) + "\n\n" + outputConnectedToText(value); + } + function dataTypeTooltip(value: FrontendGraphInput | FrontendGraphOutput): string { return `Data Type: ${value.resolvedType}`; } - function validTypesText(value: FrontendGraphInput): string { - const validTypes = value.validTypes.length > 0 ? value.validTypes.map((x) => `• ${x}`).join("\n") : "None"; - return `Valid Types:\n${validTypes}`; - } + // function validTypesText(value: FrontendGraphInput): string { + // const validTypes = value.validTypes.length > 0 ? value.validTypes.map((x) => `• ${x}`).join("\n") : "None"; + // return `Valid Types:\n${validTypes}`; + // } function outputConnectedToText(output: FrontendGraphOutput): string { if (output.connectedTo.length === 0) return "Connected to nothing"; @@ -192,28 +204,28 @@ } function inputConnectedToText(input: FrontendGraphInput): string { - return `Connected to:\n${input.connectedTo}`; + return `Connected to:\n${input.connectedToString}`; } - function zipWithUndefined(arr1: FrontendGraphInput[], arr2: FrontendGraphOutput[]) { - const maxLength = Math.max(arr1.length, arr2.length); - const result = []; + function collectExposedInputsOutputs( + inputs: (FrontendGraphInput | undefined)[], + outputs: (FrontendGraphOutput | undefined)[], + ): [FrontendGraphInput | undefined, FrontendGraphOutput | undefined][] { + const secondaryInputs = inputs.slice(1).filter((x): x is FrontendGraphInput => x !== undefined); + const secondaryOutputs = outputs.slice(1); + const maxLength = Math.max(secondaryInputs.length, secondaryOutputs.length); + const result: [FrontendGraphInput | undefined, FrontendGraphOutput | undefined][] = []; + for (let i = 0; i < maxLength; i++) { - result.push([arr1[i], arr2[i]]); + result.push([secondaryInputs[i] || undefined, secondaryOutputs[i] || undefined]); } return result; } -
+
{@html $nodeGraph.nativeNodeGraphSVGString}
+ +
{#if $nodeGraph.contextMenuInformation} {#if $nodeGraph.clickTargets} -
+
{#each $nodeGraph.clickTargets.nodeClickTargets as pathString} @@ -274,7 +286,6 @@ {/each} - {#each $nodeGraph.clickTargets.modifyImportExport as pathString} {/each} @@ -282,42 +293,40 @@
{/if} - -
- - {#each $nodeGraph.wires.values() as map} - {#each map.values() as { pathString, dataType, thick, dashed }} - {#if thick} - - {/if} - {/each} - {/each} - -
+ + + {#if $nodeGraph.wirePathInProgress} + + {/if} + -
+
{#if $nodeGraph.updateImportsExports} - {#each $nodeGraph.updateImportsExports.imports as frontendOutput, index} - {#if frontendOutput} + {#each $nodeGraph.updateImportsExports.imports as frontendImport, index} + {#if frontendImport} + {@const frontendOutput = frontendImport.port} + + {#each frontendImport.wires as wire} + + + + {/each} - {`${dataTypeTooltip(frontendOutput)}\n\n${outputConnectedToText(frontendOutput)}`} + {outputTooltip(frontendOutput)} {#if frontendOutput.connectedTo.length > 0} {:else} @@ -352,7 +361,7 @@ {#if (hoveringImportIndex === index || editingNameImportIndex === index) && $nodeGraph.updateImportsExports.addImportExport} + + + {/if} - {`${dataTypeTooltip(frontendInput)}\n\n${inputConnectedToText(frontendInput)}`} + {inputTooltip(frontendInput)} {#if frontendInput.connectedTo !== "nothing"} {:else} @@ -411,7 +429,7 @@ {/if} + + + {/if} + {#if $nodeGraph.updateImportsExports.addImportExport == true}
- editor.handle.addSecondaryExport()} /> + editor.handle.addSecondaryExport()} />
{/if} @@ -480,320 +509,28 @@ {/if} {/if}
- - -
- - {#each Array.from($nodeGraph.nodes) - .filter(([nodeId, node]) => node.isLayer && $nodeGraph.visibleNodes.has(nodeId)) - .map(([_, node], nodeIndex) => ({ node, nodeIndex })) as { node, nodeIndex } (nodeIndex)} - {@const clipPathId = String(Math.random()).substring(2)} - {@const stackDataInput = node.exposedInputs[0]} - {@const layerAreaWidth = $nodeGraph.layerWidths.get(node.id) || 8} - {@const layerChainWidth = $nodeGraph.chainWidths.get(node.id) || 0} - {@const hasLeftInputWire = $nodeGraph.hasLeftInputWire.get(node.id) || false} - {@const description = (node.reference && $nodeGraph.nodeDescriptions.get(node.reference)) || undefined} -
- {#if node.errors} - {node.errors} - {node.errors} - {/if} -
- {#if $nodeGraph.thumbnails.has(node.id)} - {@html $nodeGraph.thumbnails.get(node.id)} - {/if} - - {#if node.primaryOutput} - - {`${dataTypeTooltip(node.primaryOutput)}\n\n${outputConnectedToText(node.primaryOutput)}`} - {#if node.primaryOutput.connectedTo.length > 0} - - {#if node.primaryOutputConnectedToLayer} - - {/if} - {:else} - - {/if} - - {/if} - - - {#if node.primaryInput} - {`${dataTypeTooltip(node.primaryInput)}\n\n${validTypesText(node.primaryInput)}\n\n${inputConnectedToText(node.primaryInput)}`} - {/if} - {#if node.primaryInput?.connectedTo !== "nothing"} - - {#if node.primaryInputConnectedToLayer} - - {/if} - {:else} - - {/if} - -
- - {#if node.exposedInputs.length > 0} -
- - {`${dataTypeTooltip(stackDataInput)}\n\n${validTypesText(stackDataInput)}\n\n${inputConnectedToText(stackDataInput)}`} - {#if stackDataInput.connectedTo !== undefined} - - {:else} - - {/if} - -
- {/if} -
- - {node.displayName} -
-
- { - /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ - }} - tooltip={node.visible ? "Visible" : "Hidden"} - /> - - - - - - - - - -
- {/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.nodes) - .filter(([nodeId, node]) => !node.isLayer && $nodeGraph.visibleNodes.has(nodeId)) - .map(([_, node], nodeIndex) => ({ node, nodeIndex })) as { node, nodeIndex } (nodeIndex)} - {@const exposedInputsOutputs = zipWithUndefined(node.exposedInputs, node.exposedOutputs)} - {@const clipPathId = String(Math.random()).substring(2)} - {@const description = (node.reference && $nodeGraph.nodeDescriptions.get(node.reference)) || undefined} -
- {#if node.errors} - {node.errors} - {node.errors} - {/if} - -
- - - {node.displayName} -
- - {#if exposedInputsOutputs.length > 0} -
- {#each exposedInputsOutputs as [input, output]} -
- - {input !== undefined ? input.name : output.name} - -
- {/each} -
- {/if} - -
- {#if node.primaryInput?.dataType} - - {`${dataTypeTooltip(node.primaryInput)}\n\n${validTypesText(node.primaryInput)}\n\n${inputConnectedToText(node.primaryInput)}`} - {#if node.primaryInput.connectedTo !== undefined} - - {:else} - - {/if} - - {/if} - {#each node.exposedInputs as secondary, index} - {#if index < node.exposedInputs.length} - - {`${dataTypeTooltip(secondary)}\n\n${validTypesText(secondary)}\n\n${inputConnectedToText(secondary)}`} - {#if secondary.connectedTo !== undefined} - - {:else} - - {/if} - - {/if} - {/each} -
- -
- {#if node.primaryOutput} - - {`${dataTypeTooltip(node.primaryOutput)}\n\n${outputConnectedToText(node.primaryOutput)}`} - {#if node.primaryOutput.connectedTo !== undefined} - - {:else} - - {/if} - - {/if} - {#each node.exposedOutputs as secondary} - - {`${dataTypeTooltip(secondary)}\n\n${outputConnectedToText(secondary)}`} - {#if secondary.connectedTo !== undefined} - - {:else} - - {/if} - - {/each} -
- - - - - - - -
- {/each} -
- -{#if $nodeGraph.box} +{#if $nodeGraph.selectionBox}
{/if} diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index 83234a04d0..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,29 +68,28 @@ 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; - @TupleToVec2 readonly importPosition!: XY; - @TupleToVec2 readonly exportPosition!: XY; readonly addImportExport!: boolean; } -export class UpdateInSelectedNetwork extends JsMessage { - readonly inSelectedNetwork!: boolean; -} - export class UpdateImportReorderIndex extends JsMessage { readonly importIndex!: number | undefined; } @@ -77,33 +99,16 @@ export class UpdateExportReorderIndex extends JsMessage { } const LayerWidths = Transform(({ obj }) => obj.layerWidths); -const ChainWidths = Transform(({ obj }) => obj.chainWidths); -const HasLeftInputWire = Transform(({ obj }) => obj.hasLeftInputWire); export class UpdateLayerWidths extends JsMessage { @LayerWidths readonly layerWidths!: Map; - @ChainWidths - readonly chainWidths!: Map; - @HasLeftInputWire - readonly hasLeftInputWire!: Map; } -export class UpdateNodeGraphNodes extends JsMessage { - @Type(() => FrontendNode) - readonly nodes!: FrontendNode[]; -} - -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,18 +128,19 @@ export class UpdateNodeThumbnail extends JsMessage { readonly value!: string; } -export class UpdateNodeGraphSelection extends JsMessage { - @Type(() => BigInt) - readonly selected!: bigint[]; -} - export class UpdateOpenDocumentsList extends JsMessage { @Type(() => OpenDocument) readonly openDocuments!: OpenDocument[]; } +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 { @@ -159,6 +165,7 @@ export class DocumentDetails { } } +<<<<<<< HEAD export class Box { readonly startX!: number; @@ -184,20 +191,26 @@ 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 { readonly dataType!: FrontendGraphDataType; + readonly resolvedType!: string; + readonly name!: string; readonly description!: string; - readonly resolvedType!: string; - - readonly validTypes!: string[]; + readonly connectedToString!: string; - readonly connectedTo!: string; + readonly connectedToNode!: bigint | undefined; } export class FrontendGraphOutput { @@ -212,41 +225,94 @@ export class FrontendGraphOutput { readonly connectedTo!: string[]; } -export class FrontendNode { - readonly isLayer!: boolean; +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; readonly canBeLayer!: boolean; - readonly id!: bigint; + readonly displayName!: string; + + readonly selected!: boolean; readonly reference!: string | undefined; - readonly displayName!: string; + readonly visible!: boolean; + + // readonly wires!: (string | undefined)[]; + + readonly errors!: string | undefined; +} - readonly primaryInput!: FrontendGraphInput | undefined; +export class FrontendNode { + // readonly position!: FrontendNodePosition; + readonly position!: XY; - readonly exposedInputs!: FrontendGraphInput[]; + readonly inputs!: (FrontendGraphInput | undefined)[]; - readonly primaryOutput!: FrontendGraphOutput | undefined; + readonly outputs!: (FrontendGraphOutput | undefined)[]; +} - readonly exposedOutputs!: FrontendGraphOutput[]; +export class FrontendLayer { + // readonly position!: FrontendLayerPosition; + readonly position!: XY; + + readonly bottomInput!: FrontendGraphInput; + + readonly sideInput!: FrontendGraphInput | undefined; + + readonly output!: FrontendGraphOutput; + + readonly locked!: boolean; + + readonly chainWidth!: number; + + readonly layerHasLeftBorderGap!: boolean; readonly primaryInputConnectedToLayer!: boolean; readonly primaryOutputConnectedToLayer!: boolean; +} - @TupleToVec2 - readonly position!: XY; - - // TODO: Store field for the width of the left node chain +export class FrontendNodePosition { + readonly absolute!: XY | undefined; + readonly chain!: boolean | undefined; +} - readonly previewed!: boolean; +export class FrontendLayerPosition { + readonly absolute!: XY | undefined; + readonly stack!: number | undefined; +} - readonly visible!: boolean; +export class FrontendNodeOrLayer { + readonly node!: FrontendNode | undefined; + readonly layer!: FrontendLayer | undefined; +} - readonly unlocked!: boolean; +export class FrontendNodeToRender { + readonly metadata!: FrontendNodeMetadata; + readonly nodeOrLayer!: FrontendNodeOrLayer; + // TODO: Remove + readonly wires!: [string, boolean, FrontendGraphDataType][]; +} - readonly errors!: string | undefined; +export class UpdateCentralNodeGraph extends JsMessage { + readonly nodeOrLayer!: FrontendNodeOrLayer[]; + readonly inSelectedNetwork!: boolean; } export class FrontendNodeType { @@ -708,10 +774,6 @@ export class UpdateGraphViewOverlay extends JsMessage { open!: boolean; } -export class UpdateGraphFadeArtwork extends JsMessage { - readonly percentage!: number; -} - export class UpdateDataPanelState extends JsMessage { readonly open!: boolean; } @@ -1625,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, @@ -1654,7 +1715,6 @@ export const messageMakers: Record = { TriggerTextCopy, TriggerVisitLink, UpdateActiveDocument, - UpdateBox, UpdateClickTargets, UpdateContextMenuInformation, UpdateDialogButtons, @@ -1669,12 +1729,10 @@ export const messageMakers: Record = { UpdateDocumentScrollbars, UpdateExportReorderIndex, UpdateEyedropperSamplingState, - UpdateGraphFadeArtwork, UpdateGraphViewOverlay, UpdateImportReorderIndex, UpdateImportsExports, UpdateInputHints, - UpdateInSelectedNetwork, UpdateLayersPanelBottomBarLayout, UpdateLayersPanelControlBarLeftLayout, UpdateLayersPanelControlBarRightLayout, @@ -1683,10 +1741,9 @@ export const messageMakers: Record = { UpdateMenuBarLayout, UpdateMouseCursor, UpdateNodeGraphControlBarLayout, - UpdateNodeGraphNodes, - UpdateNodeGraphSelection, + UpdateNativeNodeGraphSVG, UpdateNodeGraphTransform, - UpdateNodeGraphWires, + UpdateNodeGraphSelectionBox, UpdateNodeThumbnail, UpdateOpenDocumentsList, UpdatePlatform, @@ -1698,7 +1755,6 @@ export const messageMakers: Record = { UpdateToolOptionsLayout, UpdateToolShelfLayout, UpdateViewportHolePunch, - UpdateVisibleNodes, UpdateWirePathInProgress, UpdateWorkingColorsLayout, } as const; diff --git a/frontend/src/state-providers/document.ts b/frontend/src/state-providers/document.ts index 0ed98ec4ae..8db5446d3e 100644 --- a/frontend/src/state-providers/document.ts +++ b/frontend/src/state-providers/document.ts @@ -13,7 +13,6 @@ import { UpdateWorkingColorsLayout, UpdateNodeGraphControlBarLayout, UpdateGraphViewOverlay, - UpdateGraphFadeArtwork, } from "@graphite/messages"; export function createDocumentState(editor: Editor) { @@ -25,19 +24,11 @@ export function createDocumentState(editor: Editor) { toolShelfLayout: defaultWidgetLayout(), workingColorsLayout: defaultWidgetLayout(), nodeGraphControlBarLayout: defaultWidgetLayout(), - // Graph view overlay graphViewOverlayOpen: false, - fadeArtwork: 100, }); const { subscribe, update } = state; // Update layouts - editor.subscriptions.subscribeJsMessage(UpdateGraphFadeArtwork, (updateGraphFadeArtwork) => { - update((state) => { - state.fadeArtwork = updateGraphFadeArtwork.percentage; - return state; - }); - }); editor.subscriptions.subscribeJsMessage(UpdateDocumentModeLayout, async (updateDocumentModeLayout) => { await tick(); diff --git a/frontend/src/state-providers/node-graph.ts b/frontend/src/state-providers/node-graph.ts index fd6c7c5724..74762825df 100644 --- a/frontend/src/state-providers/node-graph.ts +++ b/frontend/src/state-providers/node-graph.ts @@ -2,53 +2,50 @@ import { writable } from "svelte/store"; import { type Editor } from "@graphite/editor"; import { - type Box, + type FrontendSelectionBox, type FrontendClickTargets, type ContextMenuInformation, - type FrontendNode, + type FrontendNodeToRender, type FrontendNodeType, - type WirePath, - ClearAllNodeGraphWires, + type WirePathInProgress, SendUIMetadata, - UpdateBox, UpdateClickTargets, UpdateContextMenuInformation, - UpdateInSelectedNetwork, UpdateImportReorderIndex, UpdateExportReorderIndex, UpdateImportsExports, UpdateLayerWidths, - UpdateNodeGraphNodes, - UpdateVisibleNodes, - UpdateNodeGraphWires, - UpdateNodeGraphSelection, - 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(), - chainWidths: new Map(), - hasLeftInputWire: new Map(), + wirePathInProgress: undefined as WirePathInProgress | undefined, updateImportsExports: undefined as UpdateImportsExports | undefined, - nodes: new Map(), - visibleNodes: new Set(), - /// The index is the exposed input index. The exports have a first key value of u32::MAX. - wires: new Map>(), - wirePathInProgress: undefined as WirePath | undefined, - nodeDescriptions: new Map(), + 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(), + opacity: 0.8, + inSelectedNetwork: true, + previewedNode: undefined as bigint | undefined, + + // Data that will be passed in the context thumbnails: new Map(), - selected: [] as bigint[], transform: { scale: 1, x: 0, y: 0 }, - inSelectedNetwork: true, - reorderImportIndex: undefined as number | undefined, - reorderExportIndex: undefined as number | undefined, }); // Set up message subscriptions on creation @@ -59,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,62 +98,10 @@ export function createNodeGraphState(editor: Editor) { return state; }); }); - editor.subscriptions.subscribeJsMessage(UpdateInSelectedNetwork, (updateInSelectedNetwork) => { - update((state) => { - state.inSelectedNetwork = updateInSelectedNetwork.inSelectedNetwork; - return state; - }); - }); - editor.subscriptions.subscribeJsMessage(UpdateLayerWidths, (updateLayerWidths) => { - update((state) => { - state.layerWidths = updateLayerWidths.layerWidths; - state.chainWidths = updateLayerWidths.chainWidths; - state.hasLeftInputWire = updateLayerWidths.hasLeftInputWire; - return state; - }); - }); - editor.subscriptions.subscribeJsMessage(UpdateNodeGraphNodes, (updateNodeGraphNodes) => { - update((state) => { - state.nodes.clear(); - updateNodeGraphNodes.nodes.forEach((node) => { - state.nodes.set(node.id, node); - }); - 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, (_) => { - update((state) => { - state.wires.clear(); - return state; - }); - }); - editor.subscriptions.subscribeJsMessage(UpdateNodeGraphSelection, (updateNodeGraphSelection) => { + + editor.subscriptions.subscribeJsMessage(UpdateNativeNodeGraphSVG, (updateNativeNodeGraphRender) => { update((state) => { - state.selected = updateNodeGraphSelection.selected; + state.nativeNodeGraphSVGString = updateNativeNodeGraphRender.svgString; return state; }); }); @@ -166,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/bounds.rs b/node-graph/gcore/src/bounds.rs index fb4b19cd42..084500fb1c 100644 --- a/node-graph/gcore/src/bounds.rs +++ b/node-graph/gcore/src/bounds.rs @@ -1,4 +1,4 @@ -use crate::{Color, gradient::GradientStops}; +use crate::{Color, gradient::GradientStops, text::Typography}; use glam::{DAffine2, DVec2}; #[derive(Clone, Copy, Default, Debug, PartialEq)] @@ -38,3 +38,9 @@ impl BoundingBox for GradientStops { RenderBoundingBox::Infinite } } +impl BoundingBox for Typography { + fn bounding_box(&self, transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox { + let bbox = DVec2::new(self.layout.full_width() as f64, self.layout.height() as f64); + RenderBoundingBox::Rectangle([transform.transform_point2(DVec2::ZERO), transform.transform_point2(bbox)]) + } +} diff --git a/node-graph/gcore/src/consts.rs b/node-graph/gcore/src/consts.rs index 505dc81ccd..cec018c9e5 100644 --- a/node-graph/gcore/src/consts.rs +++ b/node-graph/gcore/src/consts.rs @@ -7,3 +7,10 @@ pub const LAYER_OUTLINE_STROKE_WEIGHT: f64 = 0.5; // Fonts pub const DEFAULT_FONT_FAMILY: &str = "Cabin"; pub const DEFAULT_FONT_STYLE: &str = "Regular (400)"; + +// Load Source Sans Pro font data +// TODO: Grab this from the node_modules folder (either with `include_bytes!` or ideally at runtime) instead of checking the font file into the repo. +// TODO: And maybe use the WOFF2 version (if it's supported) for its smaller, compressed file size. +pub const SOURCE_SANS_FONT_DATA: &[u8] = include_bytes!("text/source-sans-pro-regular.ttf"); +pub const SOURCE_SANS_FONT_FAMILY: &str = "Source Sans Pro"; +pub const SOURCE_SANS_FONT_STYLE: &str = "Regular (400)"; diff --git a/node-graph/gcore/src/graphic.rs b/node-graph/gcore/src/graphic.rs index 324c93e33d..5f3892c3b1 100644 --- a/node-graph/gcore/src/graphic.rs +++ b/node-graph/gcore/src/graphic.rs @@ -3,6 +3,7 @@ use crate::bounds::{BoundingBox, RenderBoundingBox}; use crate::gradient::GradientStops; use crate::raster_types::{CPU, GPU, Raster}; use crate::table::{Table, TableRow}; +use crate::text::Typography; use crate::uuid::NodeId; use crate::vector::Vector; use crate::{Artboard, Color, Ctx}; @@ -11,7 +12,7 @@ use glam::{DAffine2, DVec2}; use std::hash::Hash; /// The possible forms of graphical content that can be rendered by the Render node into either an image or SVG syntax. -#[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Hash, PartialEq, DynAny)] pub enum Graphic { Graphic(Table), Vector(Table), @@ -19,6 +20,26 @@ pub enum Graphic { RasterGPU(Table>), Color(Table), Gradient(Table), + Typography(Table), +} + +impl serde::Serialize for Graphic { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let default: Table = Table::new(); + default.serialize(serializer) + } +} + +impl<'de> serde::Deserialize<'de> for Graphic { + fn deserialize(_deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + Ok(Graphic::Graphic(Table::new())) + } } impl Default for Graphic { @@ -232,6 +253,7 @@ impl Graphic { Graphic::RasterGPU(raster) => raster.iter().all(|row| row.alpha_blending.clip), Graphic::Color(color) => color.iter().all(|row| row.alpha_blending.clip), Graphic::Gradient(gradient) => gradient.iter().all(|row| row.alpha_blending.clip), + Graphic::Typography(typography) => typography.iter().all(|row| row.alpha_blending.clip), } } @@ -256,6 +278,7 @@ impl BoundingBox for Graphic { Graphic::Graphic(graphic) => graphic.bounding_box(transform, include_stroke), Graphic::Color(color) => color.bounding_box(transform, include_stroke), Graphic::Gradient(gradient) => gradient.bounding_box(transform, include_stroke), + Graphic::Typography(typography) => typography.bounding_box(transform, include_stroke), } } } @@ -507,34 +530,15 @@ pub fn migrate_graphic<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Res elements: Vec<(Graphic, Option)>, } - #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] - pub struct OlderTable { - id: Vec, - #[serde(alias = "instances", alias = "instance")] - element: Vec, - } - - #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] - pub struct OldTable { - id: Vec, - #[serde(alias = "instances", alias = "instance")] - element: Vec, - transform: Vec, - alpha_blending: Vec, - } - #[derive(serde::Serialize, serde::Deserialize)] #[serde(untagged)] - enum GraphicFormat { + enum EitherFormat { OldGraphicGroup(OldGraphicGroup), - OlderTableOldGraphicGroup(OlderTable), - OldTableOldGraphicGroup(OldTable), - OldTableGraphicGroup(OldTable), Table(serde_json::Value), } - Ok(match GraphicFormat::deserialize(deserializer)? { - GraphicFormat::OldGraphicGroup(old) => { + Ok(match EitherFormat::deserialize(deserializer)? { + EitherFormat::OldGraphicGroup(old) => { let mut graphic_table = Table::new(); for (graphic, source_node_id) in old.elements { graphic_table.push(TableRow { @@ -546,43 +550,7 @@ pub fn migrate_graphic<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Res } graphic_table } - GraphicFormat::OlderTableOldGraphicGroup(old) => old - .element - .into_iter() - .flat_map(|element| { - element.elements.into_iter().map(move |(graphic, source_node_id)| TableRow { - element: graphic, - transform: element.transform, - alpha_blending: element.alpha_blending, - source_node_id, - }) - }) - .collect(), - GraphicFormat::OldTableOldGraphicGroup(old) => old - .element - .into_iter() - .flat_map(|element| { - element.elements.into_iter().map(move |(graphic, source_node_id)| TableRow { - element: graphic, - transform: element.transform, - alpha_blending: element.alpha_blending, - source_node_id, - }) - }) - .collect(), - GraphicFormat::OldTableGraphicGroup(old) => old - .element - .into_iter() - .flat_map(|element| { - element.elements.into_iter().map(move |(graphic, source_node_id)| TableRow { - element: graphic, - transform: Default::default(), - alpha_blending: Default::default(), - source_node_id, - }) - }) - .collect(), - GraphicFormat::Table(value) => { + EitherFormat::Table(value) => { // Try to deserialize as either table format if let Ok(old_table) = serde_json::from_value::>(value.clone()) { let mut graphic_table = Table::new(); diff --git a/node-graph/gcore/src/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/render_complexity.rs b/node-graph/gcore/src/render_complexity.rs index 7920479377..3f8e1e8f5a 100644 --- a/node-graph/gcore/src/render_complexity.rs +++ b/node-graph/gcore/src/render_complexity.rs @@ -1,6 +1,7 @@ use crate::gradient::GradientStops; use crate::raster_types::{CPU, GPU, Raster}; use crate::table::Table; +use crate::text::Typography; use crate::vector::Vector; use crate::{Artboard, Color, Graphic}; @@ -31,6 +32,7 @@ impl RenderComplexity for Graphic { Self::RasterGPU(table) => table.render_complexity(), Self::Color(table) => table.render_complexity(), Self::Gradient(table) => table.render_complexity(), + Self::Typography(table) => table.render_complexity(), } } } @@ -65,3 +67,9 @@ impl RenderComplexity for GradientStops { 1 } } + +impl RenderComplexity for Typography { + fn render_complexity(&self) -> usize { + 1 + } +} diff --git a/node-graph/gcore/src/text.rs b/node-graph/gcore/src/text.rs index 3337a1488c..c3786d8309 100644 --- a/node-graph/gcore/src/text.rs +++ b/node-graph/gcore/src/text.rs @@ -1,10 +1,23 @@ mod font_cache; mod to_path; +use std::{ + borrow::Cow, + collections::{HashMap, hash_map::Entry}, + fmt, + sync::{Arc, Mutex}, +}; + use dyn_any::DynAny; pub use font_cache::*; +use graphene_core_shaders::color::Color; +use parley::{Layout, StyleProperty}; +use rustc_hash::FxHasher; +use std::hash::{Hash, Hasher}; pub use to_path::*; +use crate::{consts::*, table::Table, vector::Vector}; + /// Alignment of lines of type within a text block. #[repr(C)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] @@ -29,3 +42,142 @@ impl From for parley::Alignment { } } } + +#[derive(Clone, DynAny)] +pub struct Typography { + pub layout: Layout<()>, + pub font_family: String, + pub color: Color, + pub stroke: Option<(Color, f64)>, +} + +impl fmt::Debug for Typography { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Typography") + .field("font_family", &self.font_family) + .field("color", &self.color) + .field("stroke", &self.stroke) + .finish() + } +} + +impl PartialEq for Typography { + fn eq(&self, _other: &Self) -> bool { + true + } +} + +impl Hash for Typography { + fn hash(&self, state: &mut H) { + self.layout.len().hash(state); + } +} + +impl Typography { + pub fn to_vector(&self) -> Table { + Table::new() + } +} + +#[derive(Clone)] +pub struct NewFontCacheWrapper(pub Arc>); + +impl fmt::Debug for NewFontCacheWrapper { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("font cache").finish() + } +} + +impl PartialEq for NewFontCacheWrapper { + fn eq(&self, _other: &Self) -> bool { + log::error!("Font cache should not be compared"); + false + } +} + +unsafe impl dyn_any::StaticType for NewFontCacheWrapper { + type Static = NewFontCacheWrapper; +} + +pub struct NewFontCache { + pub font_context: parley::FontContext, + pub layout_context: parley::LayoutContext<()>, + pub font_mapping: HashMap, + pub hash: u64, +} + +impl NewFontCache { + pub fn new() -> Self { + let mut new = NewFontCache { + font_context: parley::FontContext::new(), + layout_context: parley::LayoutContext::new(), + font_mapping: HashMap::new(), + hash: 0, + }; + + let source_sans_font = Font::new(SOURCE_SANS_FONT_FAMILY.to_string(), SOURCE_SANS_FONT_STYLE.to_string()); + new.register_font(source_sans_font, SOURCE_SANS_FONT_DATA.to_vec()); + new + } + + pub fn register_font(&mut self, font: Font, data: Vec) { + match self.font_mapping.entry(font) { + Entry::Occupied(occupied_entry) => { + log::error!("Trying to register font that already is added: {:?}", occupied_entry.key()); + } + Entry::Vacant(vacant_entry) => { + let registered_font = self.font_context.collection.register_fonts(parley::fontique::Blob::from(data), None); + if registered_font.len() > 1 { + log::error!("Registered multiple fonts for {:?}. Only the first is accessible", vacant_entry.key()); + }; + match registered_font.into_iter().next() { + Some((family_id, font_info)) => { + let Some(family_name) = self.font_context.collection.family_name(family_id) else { + log::error!("Could not get family name for font: {:?}", vacant_entry.key()); + return; + }; + let Some(font_info) = font_info.into_iter().next() else { + log::error!("Could not get font info for font: {:?}", vacant_entry.key()); + return; + }; + let mut hasher = FxHasher::default(); // or FxHasher::new() + // Hash the Font for a unique id and add it to the cached hash + vacant_entry.key().hash(&mut hasher); + let hash_value = hasher.finish(); + self.hash = self.hash.wrapping_add(hash_value); + + vacant_entry.insert((family_name.to_string(), font_info)); + } + None => log::error!("Could not register font for {:?}", vacant_entry.key()), + } + } + } + } + + pub fn generate_typography(&mut self, font: &Font, font_size: f32, text: &str) -> Option { + let Some((font_family, font_info)) = self.font_mapping.get(font) else { + log::error!("Font not loaded: {:?}", font); + return None; + }; + let font_family = font_family.to_string(); + + let mut builder = self.layout_context.ranged_builder(&mut self.font_context, text, 1., false); + + builder.push_default(StyleProperty::FontStack(parley::FontStack::Single(parley::FontFamily::Named(Cow::Owned(font_family.clone()))))); + builder.push_default(StyleProperty::FontSize(font_size)); + builder.push_default(StyleProperty::FontWeight(font_info.weight())); + builder.push_default(StyleProperty::FontStyle(font_info.style())); + builder.push_default(StyleProperty::FontWidth(font_info.width())); + + let mut layout: Layout<()> = builder.build(text); + layout.break_all_lines(None); + // layout.align(None, parley::Alignment::Start, AlignmentOptions::); + + Some(Typography { + layout, + font_family, + color: Color::BLACK, + stroke: None, + }) + } +} diff --git a/editor/src/messages/portfolio/document/overlays/source-sans-pro-regular.ttf b/node-graph/gcore/src/text/source-sans-pro-regular.ttf similarity index 100% rename from editor/src/messages/portfolio/document/overlays/source-sans-pro-regular.ttf rename to node-graph/gcore/src/text/source-sans-pro-regular.ttf diff --git a/node-graph/gcore/src/types.rs b/node-graph/gcore/src/types.rs index b95b869496..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>), @@ -334,6 +334,15 @@ impl Type { } } + pub fn into_nested_type(self) -> Type { + match self { + Self::Generic(_) => self, + Self::Concrete(_) => self, + Self::Fn(_, output) => output.into_nested_type(), + Self::Future(output) => output.into_nested_type(), + } + } + pub fn replace_nested(&mut self, f: impl Fn(&Type) -> Option) -> Option { if let Some(replacement) = f(self) { return Some(std::mem::replace(self, replacement)); diff --git a/node-graph/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/gpath-bool/src/lib.rs b/node-graph/gpath-bool/src/lib.rs index df3a089414..7634476564 100644 --- a/node-graph/gpath-bool/src/lib.rs +++ b/node-graph/gpath-bool/src/lib.rs @@ -318,6 +318,7 @@ fn flatten_vector(graphic_table: &Table) -> Table { } }) .collect::>(), + Graphic::Typography(_) => Vec::new(), } }) .collect() diff --git a/node-graph/graph-craft/src/document.rs b/node-graph/graph-craft/src/document.rs index 4598737291..e8f467dc6c 100644 --- a/node-graph/graph-craft/src/document.rs +++ b/node-graph/graph-craft/src/document.rs @@ -209,6 +209,14 @@ pub enum DocumentNodeMetadata { DocumentNodePath, } +impl DocumentNodeMetadata { + pub fn ty(&self) -> Type { + match self { + DocumentNodeMetadata::DocumentNodePath => concrete!(Vec), + } + } +} + impl NodeInput { pub const fn node(node_id: NodeId, output_index: usize) -> Self { Self::Node { node_id, output_index } diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 0e0534d9f3..db57aedbc1 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -11,6 +11,7 @@ use graphene_brush::brush_stroke::BrushStroke; use graphene_core::raster::Image; use graphene_core::raster_types::{CPU, Raster}; use graphene_core::table::Table; +use graphene_core::text::NewFontCacheWrapper; use graphene_core::transform::ReferencePoint; use graphene_core::uuid::NodeId; use graphene_core::vector::Vector; @@ -38,7 +39,9 @@ macro_rules! tagged_value { RenderOutput(RenderOutput), SurfaceFrame(SurfaceFrame), #[serde(skip)] - EditorApi(Arc) + EditorApi(Arc), + #[serde(skip)] + NewFontCache(NewFontCacheWrapper), } // We must manually implement hashing because some values are floats and so do not reproducibly hash (see FakeHash below) @@ -52,6 +55,7 @@ macro_rules! tagged_value { Self::RenderOutput(x) => x.hash(state), Self::SurfaceFrame(x) => x.hash(state), Self::EditorApi(x) => x.hash(state), + Self::NewFontCache(x) => x.hash(state), } } } @@ -64,6 +68,7 @@ macro_rules! tagged_value { Self::RenderOutput(x) => Box::new(x), Self::SurfaceFrame(x) => Box::new(x), Self::EditorApi(x) => Box::new(x), + Self::NewFontCache(x) => Box::new(x), } } /// Converts to a Arc @@ -74,6 +79,7 @@ macro_rules! tagged_value { Self::RenderOutput(x) => Arc::new(x), Self::SurfaceFrame(x) => Arc::new(x), Self::EditorApi(x) => Arc::new(x), + Self::NewFontCache(x) => Arc::new(x), } } /// Creates a graphene_core::Type::Concrete(TypeDescriptor { .. }) with the type of the value inside the tagged value @@ -83,7 +89,8 @@ macro_rules! tagged_value { $( Self::$identifier(_) => concrete!($ty), )* Self::RenderOutput(_) => concrete!(RenderOutput), Self::SurfaceFrame(_) => concrete!(SurfaceFrame), - Self::EditorApi(_) => concrete!(&WasmEditorApi) + Self::EditorApi(_) => concrete!(&WasmEditorApi), + Self::NewFontCache(_) => concrete!(NewFontCacheWrapper), } } /// Attempts to downcast the dynamic type to a tagged value @@ -255,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 { @@ -517,6 +525,14 @@ mod fake_hash { self.1.hash(state) } } + impl FakeHash for NewFontCacheWrapper { + fn hash(&self, state: &mut H) { + match self.0.lock() { + Ok(inner) => inner.hash.hash(state), + Err(_) => log::error!("Could not lock font cache when hashing"), + } + } + } } #[test] diff --git a/node-graph/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/Cargo.toml b/node-graph/gsvg-renderer/Cargo.toml index a6d0e395c5..d606dee915 100644 --- a/node-graph/gsvg-renderer/Cargo.toml +++ b/node-graph/gsvg-renderer/Cargo.toml @@ -19,6 +19,8 @@ log = { workspace = true } num-traits = { workspace = true } usvg = { workspace = true } kurbo = { workspace = true } +parley = { workspace = true } +skrifa = { workspace = true } # Optional workspace dependencies vello = { workspace = true, optional = true } diff --git a/node-graph/gsvg-renderer/src/renderer.rs b/node-graph/gsvg-renderer/src/renderer.rs index 3917a14c3c..efeb98f151 100644 --- a/node-graph/gsvg-renderer/src/renderer.rs +++ b/node-graph/gsvg-renderer/src/renderer.rs @@ -9,12 +9,14 @@ 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}; use graphene_core::render_complexity::RenderComplexity; use graphene_core::subpath::Subpath; use graphene_core::table::{Table, TableRow}; +use graphene_core::text::Typography; use graphene_core::transform::{Footprint, Transform}; use graphene_core::uuid::{NodeId, generate_uuid}; use graphene_core::vector::Vector; @@ -22,12 +24,17 @@ 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; use std::collections::{HashMap, HashSet}; use std::fmt::Write; use std::hash::{DefaultHasher, Hash, Hasher}; use std::ops::Deref; use std::sync::{Arc, LazyLock}; +use vello::peniko::StyleRef; #[cfg(feature = "vello")] use vello::*; @@ -115,6 +122,18 @@ impl SvgRender { self.svg.push("/>".into()); } + pub fn leaf_text(&mut self, text: impl Into, attributes: impl FnOnce(&mut SvgRenderAttrs)) { + self.indent(); + + self.svg.push("".into()); + self.svg.push(text.into()); + self.svg.push("".into()); + } + pub fn leaf_node(&mut self, content: impl Into) { self.indent(); self.svg.push(content.into()); @@ -231,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) {} @@ -256,6 +277,7 @@ impl Render for Graphic { Graphic::RasterGPU(_) => (), Graphic::Color(table) => table.render_svg(render, render_params), Graphic::Gradient(table) => table.render_svg(render, render_params), + Graphic::Typography(table) => table.render_svg(render, render_params), } } @@ -268,6 +290,18 @@ impl Render for Graphic { Graphic::RasterGPU(table) => table.render_to_vello(scene, transform, context, render_params), Graphic::Color(table) => table.render_to_vello(scene, transform, context, render_params), Graphic::Gradient(table) => table.render_to_vello(scene, transform, context, render_params), + Graphic::Typography(table) => table.render_to_vello(scene, transform, context, render_params), + } + } + + 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(), } } @@ -312,6 +346,14 @@ impl Render for Graphic { Graphic::Gradient(table) => { metadata.upstream_footprints.insert(element_id, footprint); + // TODO: Find a way to handle more than the first row + if let Some(row) = table.iter().next() { + metadata.local_transforms.insert(element_id, *row.transform); + } + } + Graphic::Typography(table) => { + metadata.upstream_footprints.insert(element_id, footprint); + // TODO: Find a way to handle more than the first row if let Some(row) = table.iter().next() { metadata.local_transforms.insert(element_id, *row.transform); @@ -327,6 +369,7 @@ impl Render for Graphic { Graphic::RasterGPU(table) => table.collect_metadata(metadata, footprint, element_id), Graphic::Color(table) => table.collect_metadata(metadata, footprint, element_id), Graphic::Gradient(table) => table.collect_metadata(metadata, footprint, element_id), + Graphic::Typography(table) => table.collect_metadata(metadata, footprint, element_id), } } @@ -338,6 +381,7 @@ impl Render for Graphic { Graphic::RasterGPU(table) => table.add_upstream_click_targets(click_targets), Graphic::Color(table) => table.add_upstream_click_targets(click_targets), Graphic::Gradient(table) => table.add_upstream_click_targets(click_targets), + Graphic::Typography(table) => table.add_upstream_click_targets(click_targets), } } @@ -349,6 +393,7 @@ impl Render for Graphic { Graphic::RasterGPU(table) => table.contains_artboard(), Graphic::Color(table) => table.contains_artboard(), Graphic::Gradient(table) => table.contains_artboard(), + Graphic::Typography(table) => table.contains_artboard(), } } @@ -360,6 +405,7 @@ impl Render for Graphic { Graphic::RasterGPU(_) => (), Graphic::Color(_) => (), Graphic::Gradient(_) => (), + Graphic::Typography(_) => (), } } } @@ -437,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()); @@ -475,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); @@ -613,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 { @@ -1111,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); @@ -1288,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.)); @@ -1334,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); @@ -1414,6 +1507,10 @@ impl Render for Table { } } } + + fn to_graphic(self) -> Graphic { + Graphic::Color(self) + } } impl Render for Table { @@ -1514,6 +1611,91 @@ impl Render for Table { } } } + + fn to_graphic(self) -> Graphic { + Graphic::Gradient(self) + } +} + +impl Render for Table { + fn render_svg(&self, render: &mut SvgRender, _render_params: &RenderParams) { + for table_row in self.iter() { + for line in table_row.element.layout.lines() { + for item in line.items() { + match item { + parley::PositionedLayoutItem::GlyphRun(glyph_run) => { + let font = glyph_run.run().font(); + let font_ref = skrifa::FontRef::from_index(font.data.as_ref(), font.index).unwrap(); + let font_attributes = font_ref.attributes(); + let font_style = match font_attributes.style { + Style::Normal => "normal".to_string(), + Style::Italic => "italic".to_string(), + Style::Oblique(Some(angle)) => format!("oblique {}deg", angle), + Style::Oblique(None) => "oblique".to_string(), + }; + render.parent_tag( + "text", + |attributes| { + let matrix = format_transform_matrix(*table_row.transform); + if !matrix.is_empty() { + attributes.push("transform", matrix); + } + + attributes.push("font-family", table_row.element.font_family.clone()); + attributes.push("font-weight", font_attributes.weight.value().to_string()); + attributes.push("font-size", glyph_run.run().font_size().to_string()); + attributes.push("font-style", font_style); + attributes.push("fill", format!("#{}", table_row.element.color.to_rgb_hex_srgb_from_gamma())); + attributes.push("opacity", table_row.alpha_blending.opacity.to_string()); + if let Some((stroke_color, stroke_width)) = table_row.element.stroke.as_ref().cloned() { + attributes.push("stroke-color", format!("#{}", stroke_color.to_rgb_hex_srgb_from_gamma())); + attributes.push("stroke-width", format!("{stroke_width}")); + } + }, + |render| { + for glyph in glyph_run.positioned_glyphs() { + let character = font_ref.glyph_names().get(skrifa::GlyphId::new(glyph.id as u32)).unwrap().as_str().to_string(); + let character = character.replace("space", " "); + render.leaf_text(character, |attributes| { + attributes.push("x", glyph.x.to_string()); + attributes.push("y", glyph.y.to_string()); + }); + } + }, + ); + } + parley::PositionedLayoutItem::InlineBox(_positioned_inline_box) => { + log::error!("Inline box text rendering not supported"); + } + } + } + } + } + } + + fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, _context: &mut RenderContext, _render_params: &RenderParams) { + for table_row in self.iter() { + for line in table_row.element.layout.lines() { + for item in line.items() { + match item { + parley::PositionedLayoutItem::GlyphRun(glyph_run) => { + let color = table_row.element.color.clone(); + scene + .draw_glyphs(glyph_run.run().font()) + .transform(kurbo::Affine::new((transform * *table_row.transform).to_cols_array())) + .font_size(glyph_run.run().font_size()) + .brush(peniko::BrushRef::Solid(peniko::Color::new([color.r(), color.g(), color.b(), color.a()]))) + .brush_alpha(table_row.alpha_blending.opacity) + .draw(StyleRef::Fill(peniko::Fill::EvenOdd), glyph_run.glyphs().map(|g| Glyph { id: g.id as u32, x: g.x, y: g.y })); + } + parley::PositionedLayoutItem::InlineBox(_positioned_inline_box) => { + log::error!("Cannot render positioned inline box to vello"); + } + } + } + } + } + } } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/node-graph/interpreted-executor/src/dynamic_executor.rs b/node-graph/interpreted-executor/src/dynamic_executor.rs index 2f52214ed7..53f23fa50f 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor.rs @@ -33,25 +33,21 @@ impl Default for DynamicExecutor { } } -#[derive(PartialEq, Clone, Debug, Default, serde::Serialize, serde::Deserialize)] -pub struct NodeTypes { - pub inputs: Vec, - pub output: Type, -} - -#[derive(PartialEq, Clone, Debug, Default, serde::Serialize, serde::Deserialize)] -pub struct ResolvedDocumentNodeTypes { - pub types: HashMap, NodeTypes>, -} - type Path = Box<[NodeId]>; -#[derive(PartialEq, Clone, Debug, Default, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)] pub struct ResolvedDocumentNodeTypesDelta { pub add: Vec<(Path, NodeTypes)>, pub remove: Vec, } +#[derive(Debug, Default, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct NodeTypes { + // This is currently unused. Only the output is used + pub inputs: Vec, + pub output: Type, +} + impl DynamicExecutor { pub async fn new(proto_network: ProtoNetwork) -> Result { let mut typing_context = TypingContext::new(&node_registry::NODE_REGISTRY); @@ -111,8 +107,6 @@ impl DynamicExecutor { pub fn document_node_types<'a>(&'a self, nodes: impl Iterator + 'a) -> impl Iterator + 'a { nodes.flat_map(|id| self.tree.source_map().get(&id).map(|(_, b)| (id, b.clone()))) - // TODO: https://github.com/GraphiteEditor/Graphite/issues/1767 - // TODO: Non exposed inputs are not added to the inputs_source_map, so they are not included in the resolved_document_node_types. The type is still available in the typing_context. This only affects the UI-only "Import" node. } } 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 diff --git a/node-graph/interpreted-executor/src/util.rs b/node-graph/interpreted-executor/src/util.rs index cabcee148b..218e7eee21 100644 --- a/node-graph/interpreted-executor/src/util.rs +++ b/node-graph/interpreted-executor/src/util.rs @@ -44,6 +44,7 @@ pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc