diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index c3324a16ff..0195180f1b 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -2,7 +2,7 @@ use super::utility_types::{DocumentDetails, MouseCursorIcon, OpenDocument}; use crate::messages::app_window::app_window_message_handler::AppWindowPlatform; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::node_graph::utility_types::{ - BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendNodeType, Transform, + BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendNodeType, NodeGraphErrorDiagnostic, Transform, }; use crate::messages::portfolio::document::utility_types::nodes::{JsRawBuffer, LayerPanelEntry, RawBuffer}; use crate::messages::portfolio::document::utility_types::wires::{WirePath, WirePathUpdate}; @@ -289,6 +289,9 @@ pub enum FrontendMessage { UpdateNodeGraphNodes { nodes: Vec, }, + UpdateNodeGraphErrorDiagnostic { + error: Option, + }, UpdateVisibleNodes { nodes: Vec, }, 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 a52fbd0765..85c7fde361 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -6,7 +6,7 @@ use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::document_message_handler::navigation_controls; use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext; use crate::messages::portfolio::document::node_graph::document_node_definitions::NodePropertiesContext; -use crate::messages::portfolio::document::node_graph::utility_types::{ContextMenuData, Direction, FrontendGraphDataType}; +use crate::messages::portfolio::document::node_graph::utility_types::{ContextMenuData, Direction, FrontendGraphDataType, NodeGraphErrorDiagnostic}; 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::{ @@ -776,8 +776,13 @@ impl<'a> MessageHandler> for NodeG } let context_menu_data = if let Some(node_id) = clicked_id { - let currently_is_node = !network_interface.is_layer(&node_id, selection_network_path); - ContextMenuData::ToggleLayer { node_id, currently_is_node } + let currently_is_node = !network_interface.is_layer(&node_id, breadcrumb_network_path); + let can_be_layer = network_interface.is_eligible_to_be_layer(&node_id, breadcrumb_network_path); + ContextMenuData::ModifyNode { + can_be_layer, + currently_is_node, + node_id, + } } else { ContextMenuData::CreateNode { compatible_type: None } }; @@ -793,10 +798,8 @@ impl<'a> MessageHandler> for NodeG DVec2::new(appear_right_of_mouse, appear_above_mouse) / network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport.matrix2.x_axis.x }; - let context_menu_coordinates = ((node_graph_point.x + node_graph_shift.x) as i32, (node_graph_point.y + node_graph_shift.y) as i32); - self.context_menu = Some(ContextMenuInformation { - context_menu_coordinates, + context_menu_coordinates: (node_graph_point + node_graph_shift).into(), context_menu_data, }); @@ -1222,7 +1225,7 @@ impl<'a> MessageHandler> for NodeG let compatible_type = network_interface.output_type(&output_connector, selection_network_path).add_node_string(); self.context_menu = Some(ContextMenuInformation { - context_menu_coordinates: ((point.x + node_graph_shift.x) as i32, (point.y + node_graph_shift.y) as i32), + context_menu_coordinates: (point + node_graph_shift).into(), context_menu_data: ContextMenuData::CreateNode { compatible_type }, }); @@ -1646,6 +1649,8 @@ impl<'a> MessageHandler> for NodeG responses.add(FrontendMessage::UpdateNodeGraphNodes { nodes }); responses.add(NodeGraphMessage::UpdateVisibleNodes); + let error = self.node_graph_error(network_interface, breadcrumb_network_path); + responses.add(FrontendMessage::UpdateNodeGraphErrorDiagnostic { error }); let (layer_widths, chain_widths, has_left_input_wire) = network_interface.collect_layer_widths(breadcrumb_network_path); responses.add(NodeGraphMessage::UpdateImportsExports); @@ -2509,8 +2514,6 @@ impl NodeGraphMessageHandler { }; 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 @@ -2552,20 +2555,6 @@ impl NodeGraphMessageHandler { let locked = network_interface.is_locked(&node_id, breadcrumb_network_path); - let errors = network_interface - .resolved_types - .node_graph_errors - .iter() - .find(|error| error.node_path == node_id_path) - .map(|error| format!("{:?}", error.error.clone())) - .or_else(|| { - if network_interface.resolved_types.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 @@ -2584,7 +2573,6 @@ impl NodeGraphMessageHandler { previewed, visible, locked, - errors, }); } @@ -2606,6 +2594,29 @@ impl NodeGraphMessageHandler { Some(subgraph_names) } + fn node_graph_error(&self, network_interface: &mut NodeNetworkInterface, breadcrumb_network_path: &[NodeId]) -> Option { + let graph_error = network_interface + .resolved_types + .node_graph_errors + .iter() + .find(|error| error.node_path.starts_with(breadcrumb_network_path) && error.node_path.len() > breadcrumb_network_path.len())?; + let error = if graph_error.node_path.len() == breadcrumb_network_path.len() + 1 { + format!("{:?}", graph_error.error) + } else { + "Node graph type error within this node".to_string() + }; + let error_node = graph_error.node_path[breadcrumb_network_path.len()]; + + let mut position = network_interface.position(&error_node, breadcrumb_network_path)?; + // Convert to graph space + position *= 24; + if network_interface.is_layer(&error_node, breadcrumb_network_path) { + position += IVec2::new(12, -12) + } + + Some(NodeGraphErrorDiagnostic { position: position.into(), error }) + } + fn update_layer_panel(network_interface: &NodeNetworkInterface, selection_network_path: &[NodeId], collapsed: &CollapsedLayers, layers_panel_open: bool, responses: &mut VecDeque) { if !layers_panel_open { return; 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 79918d2220..60869acadc 100644 --- a/editor/src/messages/portfolio/document/node_graph/utility_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/utility_types.rs @@ -1,4 +1,4 @@ -use glam::IVec2; +use glam::{DVec2, IVec2}; use graph_craft::document::NodeId; use graph_craft::document::value::TaggedValue; use graphene_std::Type; @@ -90,15 +90,14 @@ pub struct FrontendNode { 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, + #[serde(rename = "primaryOutputConnectedToLayer")] + pub primary_output_connected_to_layer: bool, pub position: IVec2, + pub previewed: bool, pub visible: bool, pub locked: bool, - pub previewed: bool, - pub errors: Option, } #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] @@ -154,16 +153,18 @@ pub struct BoxSelection { } #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +#[serde(tag = "type", content = "data")] pub enum ContextMenuData { - ToggleLayer { + ModifyNode { #[serde(rename = "nodeId")] node_id: NodeId, + #[serde(rename = "canBeLayer")] + can_be_layer: bool, #[serde(rename = "currentlyIsNode")] currently_is_node: bool, }, CreateNode { #[serde(rename = "compatibleType")] - #[serde(default)] compatible_type: Option, }, } @@ -172,11 +173,17 @@ pub enum ContextMenuData { pub struct ContextMenuInformation { // Stores whether the context menu is open and its position in graph coordinates #[serde(rename = "contextMenuCoordinates")] - pub context_menu_coordinates: (i32, i32), + pub context_menu_coordinates: FrontendXY, #[serde(rename = "contextMenuData")] pub context_menu_data: ContextMenuData, } +#[derive(Clone, Debug, PartialEq, Default, serde::Serialize, serde::Deserialize, specta::Type)] +pub struct NodeGraphErrorDiagnostic { + pub position: FrontendXY, + pub error: String, +} + #[derive(Clone, Debug, PartialEq, Default, serde::Serialize, serde::Deserialize, specta::Type)] pub struct FrontendClickTargets { #[serde(rename = "nodeClickTargets")] @@ -200,3 +207,22 @@ pub enum Direction { Left, Right, } + +/// Stores node graph coordinates which are then transformed in Svelte based on the node graph transform +#[derive(Clone, Debug, PartialEq, Default, serde::Serialize, serde::Deserialize, specta::Type)] +pub struct FrontendXY { + pub x: i32, + pub y: i32, +} + +impl From for FrontendXY { + fn from(v: DVec2) -> Self { + FrontendXY { x: v.x as i32, y: v.y as i32 } + } +} + +impl From for FrontendXY { + fn from(v: IVec2) -> Self { + FrontendXY { x: v.x, y: v.y } + } +} diff --git a/editor/src/test_utils.rs b/editor/src/test_utils.rs index 3b8a568d04..230f82121c 100644 --- a/editor/src/test_utils.rs +++ b/editor/src/test_utils.rs @@ -326,12 +326,9 @@ 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); - } + let FrontendMessage::UpdateNodeGraphErrorDiagnostic { error } = self else { return }; + if let Some(error) = error { + panic!("error: {:?}", error); } } } diff --git a/frontend/src/components/views/Graph.svelte b/frontend/src/components/views/Graph.svelte index 468d7f3fd2..509dfbd96e 100644 --- a/frontend/src/components/views/Graph.svelte +++ b/frontend/src/components/views/Graph.svelte @@ -27,9 +27,6 @@ let graph: HTMLDivElement | undefined; - // Key value is node id + input/output index - // Imports/Export are stored at a key value of 0 - $: gridSpacing = calculateGridSpacing($nodeGraph.transform.scale); $: gridDotRadius = 1 + Math.floor($nodeGraph.transform.scale - 0.5 + 0.001) / 2; @@ -115,15 +112,6 @@ return iconMap[icon] || "NodeNodes"; } - function toggleLayerDisplay(displayAsLayer: boolean, toggleId: bigint) { - let node = $nodeGraph.nodes.get(toggleId); - if (node) editor.handle.setToNodeOrLayer(node.id, displayAsLayer); - } - - function canBeToggledBetweenNodeAndLayer(toggleDisplayAsLayerNodeId: bigint) { - return $nodeGraph.nodes.get(toggleDisplayAsLayerNodeId)?.canBeLayer || false; - } - function createNode(nodeType: string) { if ($nodeGraph.contextMenuInformation === undefined) return; @@ -224,29 +212,30 @@ top: `${$nodeGraph.contextMenuInformation.contextMenuCoordinates.y * $nodeGraph.transform.scale + $nodeGraph.transform.y}px`, }} > - {#if typeof $nodeGraph.contextMenuInformation.contextMenuData === "string" && $nodeGraph.contextMenuInformation.contextMenuData === "CreateNode"} - createNode(e.detail)} /> - {:else if $nodeGraph.contextMenuInformation.contextMenuData && "compatibleType" in $nodeGraph.contextMenuInformation.contextMenuData} - createNode(e.detail)} /> - {:else} - {@const contextMenuData = $nodeGraph.contextMenuInformation.contextMenuData} + {#if $nodeGraph.contextMenuInformation.contextMenuData.type === "CreateNode"} + createNode(e.detail)} /> + {:else if $nodeGraph.contextMenuInformation.contextMenuData.type === "ModifyNode"} Display as toggleLayerDisplay(false, contextMenuData.nodeId), + action: () => + $nodeGraph.contextMenuInformation?.contextMenuData.type === "ModifyNode" && + editor.handle.setToNodeOrLayer($nodeGraph.contextMenuInformation.contextMenuData.data.nodeId, false), }, { value: "layer", label: "Layer", - action: () => toggleLayerDisplay(true, contextMenuData.nodeId), + action: () => + $nodeGraph.contextMenuInformation?.contextMenuData.type === "ModifyNode" && + editor.handle.setToNodeOrLayer($nodeGraph.contextMenuInformation.contextMenuData.data.nodeId, true), }, ]} - disabled={!canBeToggledBetweenNodeAndLayer(contextMenuData.nodeId)} + disabled={!$nodeGraph.contextMenuInformation.contextMenuData.data.canBeLayer} /> @@ -257,6 +246,17 @@ {/if} + {#if $nodeGraph.error} +
+ + {$nodeGraph.error.error} + + + {$nodeGraph.error.error} + +
+ {/if} + {#if $nodeGraph.clickTargets}
@@ -333,7 +333,7 @@ style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 8) / 24} style:--offset-top={($nodeGraph.updateImportsExports.importPosition.y - 8) / 24 + index} > - {#if editingNameImportIndex == index} + {#if editingNameImportIndex === index} - {#if node.errors} - {node.errors} - {node.errors} - {/if}
{#if $nodeGraph.thumbnails.has(node.id)} {@html $nodeGraph.thumbnails.get(node.id)} @@ -657,10 +653,6 @@ title={`${node.displayName}\n\n${description || ""}`.trim() + (editor.handle.inDevelopmentMode() ? `\n\nNode ID: ${node.id}` : "")} data-node={node.id} > - {#if node.errors} - {node.errors} - {node.errors} - {/if}
@@ -775,7 +767,6 @@
- {#if $nodeGraph.box}
{ - if (data.obj.contextMenuInformation === undefined) return undefined; - const contextMenuCoordinates = { x: data.obj.contextMenuInformation.contextMenuCoordinates[0], y: data.obj.contextMenuInformation.contextMenuCoordinates[1] }; - let contextMenuData = data.obj.contextMenuInformation.contextMenuData; - if (contextMenuData.ToggleLayer !== undefined) { - contextMenuData = { nodeId: contextMenuData.ToggleLayer.nodeId, currentlyIsNode: contextMenuData.ToggleLayer.currentlyIsNode }; - } else if (contextMenuData.CreateNode !== undefined) { - contextMenuData = { type: "CreateNode", compatibleType: contextMenuData.CreateNode.compatibleType }; - } - return { contextMenuCoordinates, contextMenuData }; -}); - -export class UpdateContextMenuInformation extends JsMessage { - @ContextTupleToVec2 - readonly contextMenuInformation!: ContextMenuInformation | undefined; -} - export class UpdateImportsExports extends JsMessage { readonly imports!: (FrontendGraphOutput | undefined)[]; @@ -94,6 +77,15 @@ export class UpdateNodeGraphNodes extends JsMessage { readonly nodes!: FrontendNode[]; } +export class UpdateNodeGraphErrorDiagnostic extends JsMessage { + readonly error!: NodeGraphError | undefined; +} + +export class NodeGraphError { + readonly position!: XY; + readonly error!: string; +} + export class UpdateVisibleNodes extends JsMessage { readonly nodes!: bigint[]; } @@ -173,9 +165,13 @@ export type FrontendClickTargets = { export type ContextMenuInformation = { contextMenuCoordinates: XY; - contextMenuData: "CreateNode" | { type: "CreateNode"; compatibleType: string } | { nodeId: bigint; currentlyIsNode: boolean }; + contextMenuData: { type: "CreateNode"; data: { compatibleType: string | undefined } } | { type: "ModifyNode"; data: { canBeLayer: boolean; currentlyIsNode: boolean; nodeId: bigint } }; }; +export class UpdateContextMenuInformation extends JsMessage { + readonly contextMenuInformation!: ContextMenuInformation | undefined; +} + export type FrontendGraphDataType = "General" | "Number" | "Artboard" | "Graphic" | "Raster" | "Vector" | "Color" | "Invalid"; export class FrontendGraphInput { @@ -205,12 +201,12 @@ export class FrontendGraphOutput { } export class FrontendNode { + readonly id!: bigint; + readonly isLayer!: boolean; readonly canBeLayer!: boolean; - readonly id!: bigint; - readonly reference!: string | undefined; readonly displayName!: string; @@ -236,9 +232,7 @@ export class FrontendNode { readonly visible!: boolean; - readonly unlocked!: boolean; - - readonly errors!: string | undefined; + readonly locked!: boolean; } export class FrontendNodeType { @@ -1700,6 +1694,7 @@ export const messageMakers: Record = { UpdateMouseCursor, UpdateNodeGraphControlBarLayout, UpdateNodeGraphNodes, + UpdateNodeGraphErrorDiagnostic, UpdateNodeGraphSelection, UpdateNodeGraphTransform, UpdateNodeGraphWires, diff --git a/frontend/src/state-providers/node-graph.ts b/frontend/src/state-providers/node-graph.ts index fd6c7c5724..80d35eadb8 100644 --- a/frontend/src/state-providers/node-graph.ts +++ b/frontend/src/state-providers/node-graph.ts @@ -1,6 +1,7 @@ import { writable } from "svelte/store"; import { type Editor } from "@graphite/editor"; +import type { NodeGraphError } from "@graphite/messages"; import { type Box, type FrontendClickTargets, @@ -25,6 +26,7 @@ import { UpdateNodeGraphTransform, UpdateNodeThumbnail, UpdateWirePathInProgress, + UpdateNodeGraphErrorDiagnostic, } from "@graphite/messages"; export function createNodeGraphState(editor: Editor) { @@ -32,6 +34,7 @@ export function createNodeGraphState(editor: Editor) { box: undefined as Box | undefined, clickTargets: undefined as FrontendClickTargets | undefined, contextMenuInformation: undefined as ContextMenuInformation | undefined, + error: undefined as NodeGraphError | undefined, layerWidths: new Map(), chainWidths: new Map(), hasLeftInputWire: new Map(), @@ -118,6 +121,12 @@ export function createNodeGraphState(editor: Editor) { return state; }); }); + editor.subscriptions.subscribeJsMessage(UpdateNodeGraphErrorDiagnostic, (updateNodeGraphErrorDiagnostic) => { + update((state) => { + state.error = updateNodeGraphErrorDiagnostic.error; + return state; + }); + }); editor.subscriptions.subscribeJsMessage(UpdateVisibleNodes, (updateVisibleNodes) => { update((state) => { state.visibleNodes = new Set(updateVisibleNodes.nodes);