From 26fa8d967e97e7644edcadee1f23656a1bedf376 Mon Sep 17 00:00:00 2001 From: Nitish Itankar <134285523+Stargazer10101@users.noreply.github.com> Date: Thu, 13 Feb 2025 13:28:43 +0530 Subject: [PATCH] Add the style of right-angle grid-aligned wires in the graph (#2182) * Verticle and horizontal lines achieved(#2170) * vertical lines alligned with grid dots * fixed vertical lines positioning * Deals with cases 5 and 6 * Fixed case 5 and other problematic zones * edge cases solved * edge cases fixed: HorizontalOut & HorizontalIn * added comments * Changed midX and midY * Clean up if/else statements * Consolidate code * Consolidate further * Add preference for wire style --------- Co-authored-by: Keavon Chambers --- .../preferences_dialog_message_handler.rs | 66 +++- .../src/messages/frontend/frontend_message.rs | 2 + .../document/document_message_handler.rs | 3 + .../node_graph/node_graph_message_handler.rs | 10 +- .../document/node_graph/utility_types.rs | 29 ++ .../portfolio/portfolio_message_handler.rs | 2 + .../preferences/preferences_message.rs | 2 + .../preferences_message_handler.rs | 8 + frontend/src/components/views/Graph.svelte | 325 +++++++++++++++--- frontend/src/messages.ts | 2 + frontend/src/state-providers/node-graph.ts | 2 + 11 files changed, 393 insertions(+), 58 deletions(-) diff --git a/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs b/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs index a4b13e70..8c0df6d4 100644 --- a/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs +++ b/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs @@ -1,4 +1,5 @@ use crate::messages::layout::utility_types::widget_prelude::*; +use crate::messages::portfolio::document::node_graph::utility_types::GraphWireStyle; use crate::messages::preferences::SelectionMode; use crate::messages::prelude::*; @@ -32,6 +33,29 @@ impl PreferencesDialogMessageHandler { const TITLE: &'static str = "Editor Preferences"; fn layout(&self, preferences: &PreferencesMessageHandler) -> Layout { + // ===== + // INPUT + // ===== + + let zoom_with_scroll_tooltip = "Use the scroll wheel for zooming instead of vertically panning (not recommended for trackpads)"; + let input_section = vec![TextLabel::new("Input").italic(true).widget_holder()]; + let zoom_with_scroll = vec![ + CheckboxInput::new(preferences.zoom_with_scroll) + .tooltip(zoom_with_scroll_tooltip) + .on_update(|checkbox_input: &CheckboxInput| { + PreferencesMessage::ModifyLayout { + zoom_with_scroll: checkbox_input.checked, + } + .into() + }) + .widget_holder(), + TextLabel::new("Zoom with Scroll").table_align(true).tooltip(zoom_with_scroll_tooltip).widget_holder(), + ]; + + // ========= + // SELECTION + // ========= + let selection_section = vec![TextLabel::new("Selection").italic(true).widget_holder()]; let selection_mode = RadioInput::new(vec![ RadioEntryData::new(SelectionMode::Touched.to_string()) @@ -65,20 +89,28 @@ impl PreferencesDialogMessageHandler { .selected_index(Some(preferences.selection_mode as u32)) .widget_holder(); - let zoom_with_scroll_tooltip = "Use the scroll wheel for zooming instead of vertically panning (not recommended for trackpads)"; - let input_section = vec![TextLabel::new("Input").italic(true).widget_holder()]; - let zoom_with_scroll = vec![ - CheckboxInput::new(preferences.zoom_with_scroll) - .tooltip(zoom_with_scroll_tooltip) - .on_update(|checkbox_input: &CheckboxInput| { - PreferencesMessage::ModifyLayout { - zoom_with_scroll: checkbox_input.checked, - } - .into() - }) - .widget_holder(), - TextLabel::new("Zoom with Scroll").table_align(true).tooltip(zoom_with_scroll_tooltip).widget_holder(), - ]; + // ================ + // NODE GRAPH WIRES + // ================ + + let node_graph_section_tooltip = "Appearance of the wires running between node connections in the graph"; + let node_graph_section = vec![TextLabel::new("Node Graph Wires").tooltip(node_graph_section_tooltip).italic(true).widget_holder()]; + let graph_wire_style = RadioInput::new(vec![ + RadioEntryData::new(GraphWireStyle::GridAligned.to_string()) + .label(GraphWireStyle::GridAligned.to_string()) + .tooltip(GraphWireStyle::GridAligned.tooltip_description()) + .on_update(move |_| PreferencesMessage::GraphWireStyle { style: GraphWireStyle::GridAligned }.into()), + RadioEntryData::new(GraphWireStyle::Direct.to_string()) + .label(GraphWireStyle::Direct.to_string()) + .tooltip(GraphWireStyle::Direct.tooltip_description()) + .on_update(move |_| PreferencesMessage::GraphWireStyle { style: GraphWireStyle::Direct }.into()), + ]) + .selected_index(Some(preferences.graph_wire_style as u32)) + .widget_holder(); + + // ============ + // EXPERIMENTAL + // ============ let vello_tooltip = "Use the experimental Vello renderer (your browser must support WebGPU)"; let renderer_section = vec![TextLabel::new("Experimental").italic(true).widget_holder()]; @@ -126,10 +158,12 @@ impl PreferencesDialogMessageHandler { // ]; Layout::WidgetLayout(WidgetLayout::new(vec![ - LayoutGroup::Row { widgets: selection_section }, - LayoutGroup::Row { widgets: vec![selection_mode] }, LayoutGroup::Row { widgets: input_section }, LayoutGroup::Row { widgets: zoom_with_scroll }, + LayoutGroup::Row { widgets: selection_section }, + LayoutGroup::Row { widgets: vec![selection_mode] }, + LayoutGroup::Row { widgets: node_graph_section }, + LayoutGroup::Row { widgets: vec![graph_wire_style] }, LayoutGroup::Row { widgets: renderer_section }, LayoutGroup::Row { widgets: use_vello }, LayoutGroup::Row { widgets: vector_meshes }, diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 3bbf2791..89306b21 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -250,6 +250,8 @@ pub enum FrontendMessage { UpdateNodeGraph { nodes: Vec, wires: Vec, + #[serde(rename = "wiresDirectNotGridAligned")] + wires_direct_not_grid_aligned: bool, }, UpdateNodeGraphControlBarLayout { #[serde(rename = "layoutTarget")] diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index f60d19be..8cef1fb9 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -42,6 +42,7 @@ pub struct DocumentMessageData<'a> { pub persistent_data: &'a PersistentData, pub executor: &'a mut NodeGraphExecutor, pub current_tool: &'a ToolType, + pub preferences: &'a PreferencesMessageHandler, } #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] @@ -172,6 +173,7 @@ impl MessageHandler> for DocumentMessag persistent_data, executor, current_tool, + preferences, } = data; let selected_nodes_bounding_box_viewport = self.network_interface.selected_nodes_bounding_box_viewport(&self.breadcrumb_network_path); @@ -222,6 +224,7 @@ impl MessageHandler> for DocumentMessag graph_view_overlay_open: self.graph_view_overlay_open, graph_fade_artwork_percentage: self.graph_fade_artwork_percentage, navigation_handler: &self.navigation_handler, + preferences, }, ); } 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 4a452864..b38429f7 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 @@ -35,6 +35,7 @@ pub struct NodeGraphHandlerData<'a> { pub graph_view_overlay_open: bool, pub graph_fade_artwork_percentage: f64, pub navigation_handler: &'a NavigationMessageHandler, + pub preferences: &'a PreferencesMessageHandler, } #[derive(Debug, Clone)] @@ -93,6 +94,7 @@ impl<'a> MessageHandler> for NodeGrap graph_view_overlay_open, graph_fade_artwork_percentage, navigation_handler, + preferences, } = data; match message { @@ -1293,8 +1295,14 @@ impl<'a> MessageHandler> for NodeGrap let wires = Self::collect_wires(network_interface, breadcrumb_network_path); let nodes = self.collect_nodes(network_interface, breadcrumb_network_path); let (layer_widths, chain_widths, has_left_input_wire) = network_interface.collect_layer_widths(breadcrumb_network_path); + let wires_direct_not_grid_aligned = preferences.graph_wire_style.is_direct(); + responses.add(NodeGraphMessage::UpdateImportsExports); - responses.add(FrontendMessage::UpdateNodeGraph { nodes, wires }); + responses.add(FrontendMessage::UpdateNodeGraph { + nodes, + wires, + wires_direct_not_grid_aligned, + }); responses.add(FrontendMessage::UpdateLayerWidths { layer_widths, chain_widths, 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 24e1ee41..c3cca338 100644 --- a/editor/src/messages/portfolio/document/node_graph/utility_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/utility_types.rs @@ -200,3 +200,32 @@ pub enum Direction { Left, Right, } + +#[derive(Copy, Clone, Debug, PartialEq, Default, serde::Serialize, serde::Deserialize, specta::Type)] +pub enum GraphWireStyle { + #[default] + GridAligned = 0, + Direct = 1, +} + +impl std::fmt::Display for GraphWireStyle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GraphWireStyle::GridAligned => write!(f, "Grid-Aligned"), + GraphWireStyle::Direct => write!(f, "Direct"), + } + } +} + +impl GraphWireStyle { + pub fn tooltip_description(&self) -> &'static str { + match self { + GraphWireStyle::GridAligned => "Wires follow the grid, running in straight lines between nodes", + GraphWireStyle::Direct => "Wires bend to run at an angle directly between nodes", + } + } + + pub fn is_direct(&self) -> bool { + *self == GraphWireStyle::Direct + } +} diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 89497669..6afd3683 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -106,6 +106,7 @@ impl MessageHandler> for PortfolioMes persistent_data: &self.persistent_data, executor: &mut self.executor, current_tool, + preferences, }; document.process_message(message, responses, document_inputs) } @@ -121,6 +122,7 @@ impl MessageHandler> for PortfolioMes persistent_data: &self.persistent_data, executor: &mut self.executor, current_tool, + preferences, }; document.process_message(message, responses, document_inputs) } diff --git a/editor/src/messages/preferences/preferences_message.rs b/editor/src/messages/preferences/preferences_message.rs index d060267a..63f7d59a 100644 --- a/editor/src/messages/preferences/preferences_message.rs +++ b/editor/src/messages/preferences/preferences_message.rs @@ -1,3 +1,4 @@ +use crate::messages::portfolio::document::node_graph::utility_types::GraphWireStyle; use crate::messages::preferences::SelectionMode; use crate::messages::prelude::*; @@ -13,6 +14,7 @@ pub enum PreferencesMessage { SelectionMode { selection_mode: SelectionMode }, VectorMeshes { enabled: bool }, ModifyLayout { zoom_with_scroll: bool }, + GraphWireStyle { style: GraphWireStyle }, // ImaginateRefreshFrequency { seconds: f64 }, // ImaginateServerHostname { hostname: String }, } diff --git a/editor/src/messages/preferences/preferences_message_handler.rs b/editor/src/messages/preferences/preferences_message_handler.rs index f52b109b..2b51aa28 100644 --- a/editor/src/messages/preferences/preferences_message_handler.rs +++ b/editor/src/messages/preferences/preferences_message_handler.rs @@ -1,4 +1,5 @@ use crate::messages::input_mapper::key_mapping::MappingVariant; +use crate::messages::portfolio::document::node_graph::utility_types::GraphWireStyle; use crate::messages::preferences::SelectionMode; use crate::messages::prelude::*; @@ -12,6 +13,7 @@ pub struct PreferencesMessageHandler { pub zoom_with_scroll: bool, pub use_vello: bool, pub vector_meshes: bool, + pub graph_wire_style: GraphWireStyle, } impl PreferencesMessageHandler { @@ -37,6 +39,7 @@ impl Default for PreferencesMessageHandler { imaginate_hostname: host_name, use_vello, } = Default::default(); + Self { imaginate_server_hostname: host_name, imaginate_refresh_frequency: 1., @@ -44,6 +47,7 @@ impl Default for PreferencesMessageHandler { zoom_with_scroll: matches!(MappingVariant::default(), MappingVariant::ZoomWithScroll), use_vello, vector_meshes: false, + graph_wire_style: GraphWireStyle::default(), } } } @@ -95,6 +99,10 @@ impl MessageHandler for PreferencesMessageHandler { PreferencesMessage::SelectionMode { selection_mode } => { self.selection_mode = selection_mode; } + PreferencesMessage::GraphWireStyle { style } => { + self.graph_wire_style = style; + responses.add(NodeGraphMessage::SendGraph); + } } // TODO: Reenable when Imaginate is restored (and move back up one line since the auto-formatter doesn't like it in that block) // PreferencesMessage::ImaginateRefreshFrequency { seconds } => { diff --git a/frontend/src/components/views/Graph.svelte b/frontend/src/components/views/Graph.svelte index 0186f605..589a8411 100644 --- a/frontend/src/components/views/Graph.svelte +++ b/frontend/src/components/views/Graph.svelte @@ -159,11 +159,13 @@ return { nodeOutput, nodeInput }; } - function createWirePath(outputPort: SVGSVGElement, inputPort: SVGSVGElement, verticalOut: boolean, verticalIn: boolean, dashed: boolean): WirePath { + function createWirePath(outputPort: SVGSVGElement, inputPort: SVGSVGElement, verticalOut: boolean, verticalIn: boolean, dashed: boolean, directNotGridAligned: boolean): WirePath { const inputPortRect = inputPort.getBoundingClientRect(); const outputPortRect = outputPort.getBoundingClientRect(); - const pathString = buildWirePathString(outputPortRect, inputPortRect, verticalOut, verticalIn); + const pathString = directNotGridAligned + ? buildCurvedWirePathString(outputPortRect, inputPortRect, verticalOut, verticalIn) + : buildStraightWirePathString(outputPortRect, inputPortRect, verticalOut, verticalIn); const dataType = (outputPort.getAttribute("data-datatype") as FrontendGraphDataType) || "General"; const thick = verticalIn && verticalOut; @@ -184,7 +186,7 @@ const wireEndNode = wire.wireEnd.nodeId !== undefined ? $nodeGraph.nodes.get(wire.wireEnd.nodeId) : undefined; const wireEnd = (wireEndNode?.isLayer && Number(wire.wireEnd.index) === 0) || false; - return [createWirePath(nodeOutput, nodeInput, wireStart, wireEnd, wire.dashed)]; + return [createWirePath(nodeOutput, nodeInput, wireStart, wireEnd, wire.dashed, $nodeGraph.wiresDirectNotGridAligned)]; }); } @@ -198,7 +200,270 @@ return iconMap[icon] || "NodeNodes"; } - function buildWirePathLocations(outputBounds: DOMRect, inputBounds: DOMRect, verticalOut: boolean, verticalIn: boolean): { x: number; y: number }[] { + function buildStraightWirePathLocations(outputBounds: DOMRect, inputBounds: DOMRect, verticalOut: boolean, verticalIn: boolean): { x: number; y: number }[] { + if (!nodesContainer) return []; + + const VERTICAL_WIRE_OVERLAP_ON_SHAPED_CAP = 1; + const LINE_WIDTH = 2; + + // Calculate coordinates for input and output connectors + const inX = verticalIn ? inputBounds.x + inputBounds.width / 2 : inputBounds.x + 1; + const inY = verticalIn ? inputBounds.y + inputBounds.height - VERTICAL_WIRE_OVERLAP_ON_SHAPED_CAP : inputBounds.y + inputBounds.height / 2; + const outX = verticalOut ? outputBounds.x + outputBounds.width / 2 : outputBounds.x + outputBounds.width - 1; + const outY = verticalOut ? outputBounds.y + VERTICAL_WIRE_OVERLAP_ON_SHAPED_CAP : outputBounds.y + outputBounds.height / 2; + + // Adjust for scale + const containerBounds = nodesContainer.getBoundingClientRect(); + const scale = $nodeGraph.transform.scale; + const inConnectorX = Math.round((inX - containerBounds.x) / scale); + const inConnectorY = Math.round((inY - containerBounds.y) / scale); + const outConnectorX = Math.round((outX - containerBounds.x) / scale); + const outConnectorY = Math.round((outY - containerBounds.y) / scale); + + // Helper functions for calculating coordinates + const calculateMidX = () => (inConnectorX + outConnectorX) / 2 + (((inConnectorX + outConnectorX) / 2) % gridSpacing); + const calculateMidY = () => (inConnectorY + outConnectorY) / 2 + (((inConnectorY + outConnectorY) / 2) % gridSpacing); + const calculateMidYAlternate = () => (inConnectorY + outConnectorY) / 2 - (((inConnectorY + outConnectorY) / 2) % gridSpacing); + + // Define X coordinate calculations + const x1 = () => outConnectorX; + const x2 = () => outConnectorX + gridSpacing; + const x3 = () => inConnectorX - 2 * gridSpacing; + const x4 = () => inConnectorX; + const x5 = () => inConnectorX - 2 * gridSpacing + LINE_WIDTH; + const x6 = () => outConnectorX + gridSpacing + LINE_WIDTH; + const x7 = () => outConnectorX + 2 * gridSpacing + LINE_WIDTH; + const x8 = () => inConnectorX + LINE_WIDTH; + const x9 = () => outConnectorX + 2 * gridSpacing; + const x10 = () => calculateMidX() + LINE_WIDTH; + const x11 = () => outConnectorX - gridSpacing; + const x12 = () => outConnectorX - 4 * gridSpacing; + const x13 = () => calculateMidX(); + const x14 = () => inConnectorX + gridSpacing; + const x15 = () => inConnectorX - 4 * gridSpacing; + const x16 = () => inConnectorX + 8 * gridSpacing; + const x17 = () => calculateMidX() - 2 * LINE_WIDTH; + const x18 = () => outConnectorX + gridSpacing - 2 * LINE_WIDTH; + const x19 = () => outConnectorX - 2 * LINE_WIDTH; + const x20 = () => calculateMidX() - LINE_WIDTH; + + // Define Y coordinate calculations + const y1 = () => outConnectorY; + const y2 = () => outConnectorY - gridSpacing; + const y3 = () => inConnectorY; + const y4 = () => outConnectorY - gridSpacing + 5.5 * LINE_WIDTH; + const y5 = () => inConnectorY - 2 * gridSpacing; + const y6 = () => outConnectorY + 4 * LINE_WIDTH; + const y7 = () => outConnectorY + 5 * LINE_WIDTH; + const y8 = () => outConnectorY - 2 * gridSpacing + 5.5 * LINE_WIDTH; + const y9 = () => outConnectorY + 6 * LINE_WIDTH; + const y10 = () => inConnectorY + 2 * gridSpacing; + const y111 = () => inConnectorY + gridSpacing + 6.5 * LINE_WIDTH; + const y12 = () => inConnectorY + gridSpacing - 5.5 * LINE_WIDTH; + const y13 = () => inConnectorY - gridSpacing; + const y14 = () => inConnectorY + gridSpacing; + const y15 = () => calculateMidY(); + const y16 = () => calculateMidYAlternate(); + + // Helper function for constructing coordinate pairs + const construct = (...coords: [() => number, () => number][]) => coords.map(([x, y]) => ({ x: x(), y: y() })); + + // Define wire path shapes that get used more than once + const wire1 = () => construct([x1, y1], [x1, y4], [x5, y4], [x5, y3], [x4, y3]); + const wire2 = () => construct([x1, y1], [x1, y16], [x3, y16], [x3, y3], [x4, y3]); + const wire3 = () => construct([x1, y1], [x1, y4], [x12, y4], [x12, y10], [x3, y10], [x3, y3], [x4, y3]); + const wire4 = () => construct([x1, y1], [x1, y4], [x13, y4], [x13, y10], [x3, y10], [x3, y3], [x4, y3]); + + // `outConnector` point and `inConnector` point lying on the same horizontal grid line and `outConnector` point lies to the right of `inConnector` point + if (outConnectorY === inConnectorY && outConnectorX > inConnectorX && (verticalOut || !verticalIn)) return construct([x1, y1], [x2, y1], [x2, y2], [x3, y2], [x3, y3], [x4, y3]); + + // Handle straight lines + if (outConnectorY === inConnectorY || (outConnectorX === inConnectorX && verticalOut)) return construct([x1, y1], [x4, y3]); + + // Handle standard right-angle paths + // Start vertical, then horizontal + + // `outConnector` point lies to the left of `inConnector` point + if (verticalOut && inConnectorX > outConnectorX) { + // `outConnector` point lies above `inConnector` point + if (outConnectorY < inConnectorY) { + // `outConnector` point lies on the vertical grid line 4 units to the left of `inConnector` point point + if (-4 * gridSpacing <= outConnectorX - inConnectorX && outConnectorX - inConnectorX < -3 * gridSpacing) return wire1(); + + // `outConnector` point lying on vertical grid lines 3 and 2 units to the left of `inConnector` point + if (-3 * gridSpacing <= outConnectorX - inConnectorX && outConnectorX - inConnectorX <= -1 * gridSpacing) { + if (-2 * gridSpacing <= outConnectorY - inConnectorY && outConnectorY - inConnectorY <= -1 * gridSpacing) return construct([x1, y1], [x1, y2], [x2, y2], [x2, y3], [x4, y3]); + + if (-1 * gridSpacing <= outConnectorY - inConnectorY && outConnectorY - inConnectorY <= 0 * gridSpacing) return construct([x1, y1], [x1, y4], [x6, y4], [x6, y3], [x4, y3]); + + return construct([x1, y1], [x1, y4], [x7, y4], [x7, y5], [x3, y5], [x3, y3], [x4, y3]); + } + + // `outConnector` point lying on vertical grid line 1 units to the left of `inConnector` point + if (-1 * gridSpacing < outConnectorX - inConnectorX && outConnectorX - inConnectorX <= 0 * gridSpacing) { + // `outConnector` point lying on horizontal grid line 1 unit above `inConnector` point + if (-2 * gridSpacing <= outConnectorY - inConnectorY && outConnectorY - inConnectorY <= -1 * gridSpacing) return construct([x1, y6], [x2, y6], [x8, y3]); + + // `outConnector` point lying on the same horizontal grid line as `inConnector` point + if (-1 * gridSpacing <= outConnectorY - inConnectorY && outConnectorY - inConnectorY <= 0 * gridSpacing) return construct([x1, y7], [x4, y3]); + + return construct([x1, y1], [x1, y2], [x9, y2], [x9, y5], [x3, y5], [x3, y3], [x4, y3]); + } + + return construct([x1, y1], [x1, y4], [x10, y4], [x10, y3], [x4, y3]); + } + + // `outConnector` point lies below `inConnector` point + // `outConnector` point lying on vertical grid line 1 unit to the left of `inConnector` point + if (-1 * gridSpacing <= outConnectorX - inConnectorX && outConnectorX - inConnectorX <= 0 * gridSpacing) { + // `outConnector` point lying on the horizontal grid lines 1 and 2 units below the `inConnector` point + if (0 * gridSpacing <= outConnectorY - inConnectorY && outConnectorY - inConnectorY <= 2 * gridSpacing) construct([x1, y6], [x11, y6], [x11, y3], [x4, y3]); + + return wire2(); + } + + return construct([x1, y1], [x1, y3], [x4, y3]); + } + + // `outConnector` point lies to the right of `inConnector` point + if (verticalOut && inConnectorX <= outConnectorX) { + // `outConnector` point lying on any horizontal grid line above `inConnector` point + if (outConnectorY < inConnectorY) { + // `outConnector` point lying on horizontal grid line 1 unit above `inConnector` point + if (-2 * gridSpacing < outConnectorY - inConnectorY && outConnectorY - inConnectorY <= -1 * gridSpacing) return wire1(); + + // `outConnector` point lying on the same horizontal grid line as `inConnector` point + if (-1 * gridSpacing < outConnectorY - inConnectorY && outConnectorY - inConnectorY <= 0 * gridSpacing) return construct([x1, y1], [x1, y8], [x5, y8], [x5, y3], [x4, y3]); + + // `outConnector` point lying on vertical grid lines 1 and 2 units to the right of `inConnector` point + if (gridSpacing <= outConnectorX - inConnectorX && outConnectorX - inConnectorX <= 3 * gridSpacing) { + return construct([x1, y1], [x1, y4], [x9, y4], [x9, y5], [x3, y5], [x3, y3], [x4, y3]); + } + + return construct([x1, y1], [x1, y4], [x10, y4], [x10, y5], [x5, y5], [x5, y3], [x4, y3]); + } + + // `outConnector` point lies below `inConnector` point + if (outConnectorY - inConnectorY <= gridSpacing) { + // `outConnector` point lies on the horizontal grid line 1 unit below the `inConnector` Point + if (0 <= outConnectorX - inConnectorX && outConnectorX - inConnectorX <= 13 * gridSpacing) return construct([x1, y9], [x3, y9], [x3, y3], [x4, y3]); + + if (13 < outConnectorX - inConnectorX && outConnectorX - inConnectorX <= 18 * gridSpacing) return wire3(); + + return wire4(); + } + + // `outConnector` point lies on the horizontal grid line 2 units below `outConnector` point + if (gridSpacing <= outConnectorY - inConnectorY && outConnectorY - inConnectorY <= 2 * gridSpacing) { + if (0 <= outConnectorX - inConnectorX && outConnectorX - inConnectorX <= 13 * gridSpacing) return construct([x1, y7], [x5, y7], [x5, y3], [x4, y3]); + + if (13 < outConnectorX - inConnectorX && outConnectorX - inConnectorX <= 18 * gridSpacing) return wire3(); + + return wire4(); + } + + // 0 to 4 units below the `outConnector` Point + if (outConnectorY - inConnectorY <= 4 * gridSpacing) return wire1(); + + return wire2(); + } + + // Start horizontal, then vertical + if (verticalIn) { + // when `outConnector` lies below `inConnector` + if (outConnectorY > inConnectorY) { + // `outConnectorX` lies to the left of `inConnectorX` + if (outConnectorX < inConnectorX) return construct([x1, y1], [x4, y1], [x4, y3]); + + // `outConnectorX` lies to the right of `inConnectorX` + if (outConnectorY - inConnectorY <= gridSpacing) { + // `outConnector` point directly below `inConnector` point + if (0 <= outConnectorX - inConnectorX && outConnectorX - inConnectorX <= gridSpacing) return construct([x1, y1], [x14, y1], [x14, y2], [x4, y2], [x4, y3]); + + // `outConnector` point lies below `inConnector` point and strictly to the right of `inConnector` point + return construct([x1, y1], [x2, y1], [x2, y111], [x4, y111], [x4, y3]); + } + + return construct([x1, y1], [x2, y1], [x2, y2], [x4, y2], [x4, y3]); + } + + // `outConnectorY` lies on or above the `inConnectorY` point + if (-6 * gridSpacing < inConnectorX - outConnectorX && inConnectorX - outConnectorX < 4 * gridSpacing) { + // edge case: `outConnector` point lying on vertical grid lines ranging from 4 units to left to 5 units to right of `inConnector` point + if (-1 * gridSpacing < inConnectorX - outConnectorX && inConnectorX - outConnectorX < 4 * gridSpacing) { + return construct([x1, y1], [x2, y1], [x2, y2], [x15, y2], [x15, y12], [x4, y12], [x4, y3]); + } + + return construct([x1, y1], [x16, y1], [x16, y12], [x4, y12], [x4, y3]); + } + + // left of edge case: `outConnector` point lying on vertical grid lines more than 4 units to left of `inConnector` point + if (4 * gridSpacing < inConnectorX - outConnectorX) return construct([x1, y1], [x17, y1], [x17, y12], [x4, y12], [x4, y3]); + + // right of edge case: `outConnector` point lying on the vertical grid lines more than 5 units to right of `inConnector` point + if (6 * gridSpacing > inConnectorX - outConnectorX) return construct([x1, y1], [x18, y1], [x18, y12], [x4, y12], [x4, y3]); + } + + // Both horizontal - use horizontal middle point + // When `inConnector` point is one of the two closest diagonally opposite points + if (0 <= inConnectorX - outConnectorX && inConnectorX - outConnectorX <= gridSpacing && inConnectorY - outConnectorY >= -1 * gridSpacing && inConnectorY - outConnectorY <= gridSpacing) { + return construct([x19, y1], [x19, y3], [x4, y3]); + } + + // When `inConnector` point lies on the horizontal line 1 unit above and below the `outConnector` point + if (-1 * gridSpacing <= outConnectorY - inConnectorY && outConnectorY - inConnectorY <= gridSpacing && outConnectorX > inConnectorX) { + // Horizontal line above `outConnectorY` + if (inConnectorY < outConnectorY) return construct([x1, y1], [x2, y1], [x2, y13], [x3, y13], [x3, y3], [x4, y3]); + + // Horizontal line below `outConnectorY` + return construct([x1, y1], [x2, y1], [x2, y14], [x3, y14], [x3, y3], [x4, y3]); + } + + // `outConnector` point to the right of `inConnector` point + if (outConnectorX > inConnectorX - gridSpacing) return construct([x1, y1], [x18, y1], [x18, y15], [x5, y15], [x5, y3], [x4, y3]); + + // When `inConnector` point lies on the vertical grid line two units to the right of `outConnector` point + if (gridSpacing <= inConnectorX - outConnectorX && inConnectorX - outConnectorX <= 2 * gridSpacing) return construct([x1, y1], [x18, y1], [x18, y3], [x4, y3]); + + return construct([x1, y1], [x20, y1], [x20, y3], [x4, y3]); + } + + function buildStraightWirePathString(outputBounds: DOMRect, inputBounds: DOMRect, verticalOut: boolean, verticalIn: boolean): string { + const locations = buildStraightWirePathLocations(outputBounds, inputBounds, verticalOut, verticalIn); + if (locations.length === 0) return "[error]"; + if (locations.length === 2) return `M${locations[0].x},${locations[0].y} L${locations[1].x},${locations[1].y}`; + + const CORNER_RADIUS = 10; + + // Create path with rounded corners + let path = `M${locations[0].x},${locations[0].y}`; + + for (let i = 1; i < locations.length - 1; i++) { + const prev = locations[i - 1]; + const curr = locations[i]; + const next = locations[i + 1]; + + // Calculate corner points + const isVertical = curr.x === prev.x; + const cornerStart = { + x: curr.x + (isVertical ? 0 : prev.x < curr.x ? -CORNER_RADIUS : CORNER_RADIUS), + y: curr.y + (isVertical ? (prev.y < curr.y ? -CORNER_RADIUS : CORNER_RADIUS) : 0), + }; + const cornerEnd = { + x: curr.x + (isVertical ? (next.x < curr.x ? -CORNER_RADIUS : CORNER_RADIUS) : 0), + y: curr.y + (isVertical ? 0 : next.y < curr.y ? -CORNER_RADIUS : CORNER_RADIUS), + }; + + // Add line to corner start, quadratic curve for corner, then continue to next point + path += ` L${cornerStart.x},${cornerStart.y}`; + path += ` Q${curr.x},${curr.y} ${cornerEnd.x},${cornerEnd.y}`; + } + + path += ` L${locations[locations.length - 1].x},${locations[locations.length - 1].y}`; + return path; + } + + function buildCurvedWirePathLocations(outputBounds: DOMRect, inputBounds: DOMRect, verticalOut: boolean, verticalIn: boolean): { x: number; y: number }[] { if (!nodesContainer) return []; const VERTICAL_WIRE_OVERLAP_ON_SHAPED_CAP = 1; @@ -217,19 +482,6 @@ const horizontalGap = Math.abs(outConnectorX - inConnectorX); const verticalGap = Math.abs(outConnectorY - inConnectorY); - // TODO: Finish this commented out code replacement for the code below it based on this diagram: - // // Straight: stacking lines which are always straight, or a straight horizontal wire between two aligned nodes - // if ((verticalOut && verticalIn) || (!verticalOut && !verticalIn && verticalGap === 0)) { - // return [ - // { x: outConnectorX, y: outConnectorY }, - // { x: inConnectorX, y: inConnectorY }, - // ]; - // } - - // // L-shape bend - // if (verticalOut !== verticalIn) { - // } - const curveLength = 24; const curveFalloffRate = curveLength * Math.PI * 2; @@ -246,12 +498,14 @@ ]; } - function buildWirePathString(outputBounds: DOMRect, inputBounds: DOMRect, verticalOut: boolean, verticalIn: boolean): string { - const locations = buildWirePathLocations(outputBounds, inputBounds, verticalOut, verticalIn); + function buildCurvedWirePathString(outputBounds: DOMRect, inputBounds: DOMRect, verticalOut: boolean, verticalIn: boolean): string { + const locations = buildCurvedWirePathLocations(outputBounds, inputBounds, verticalOut, verticalIn); if (locations.length === 0) return "[error]"; + const SMOOTHING = 0.5; const delta01 = { x: (locations[1].x - locations[0].x) * SMOOTHING, y: (locations[1].y - locations[0].y) * SMOOTHING }; const delta23 = { x: (locations[3].x - locations[2].x) * SMOOTHING, y: (locations[3].y - locations[2].y) * SMOOTHING }; + return ` M${locations[0].x},${locations[0].y} L${locations[1].x},${locations[1].y} @@ -335,31 +589,20 @@ } function outputConnectedToText(output: FrontendGraphOutput): string { - if (output.connectedTo.length === 0) { - return "Connected to nothing"; - } else { - return output.connectedTo - .map((inputConnector) => { - if ((inputConnector as Node).nodeId === undefined) { - return `Connected to export index ${inputConnector.index}`; - } else { - return `Connected to ${(inputConnector as Node).nodeId}, port index ${inputConnector.index}`; - } - }) - .join("\n"); - } + if (output.connectedTo.length === 0) return "Connected to nothing"; + + return output.connectedTo + .map((inputConnector) => { + if ((inputConnector as Node).nodeId === undefined) return `Connected to export index ${inputConnector.index}`; + return `Connected to ${(inputConnector as Node).nodeId}, port index ${inputConnector.index}`; + }) + .join("\n"); } function inputConnectedToText(input: FrontendGraphInput): string { - if (input.connectedTo === undefined) { - return "Connected to nothing"; - } else { - if ((input.connectedTo as Node).nodeId === undefined) { - return `Connected to import index ${input.connectedTo.index}`; - } else { - return `Connected to ${(input.connectedTo as Node).nodeId}, port index ${input.connectedTo.index}`; - } - } + if (input.connectedTo === undefined) return "Connected to nothing"; + if ((input.connectedTo as Node).nodeId === undefined) return `Connected to import index ${input.connectedTo.index}`; + return `Connected to ${(input.connectedTo as Node).nodeId}, port index ${input.connectedTo.index}`; } function primaryOutputConnectedToLayer(node: FrontendNode): boolean { diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index ca0da8f9..f6e7188d 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -100,6 +100,8 @@ export class UpdateNodeGraph extends JsMessage { @Type(() => FrontendNodeWire) readonly wires!: FrontendNodeWire[]; + + readonly wiresDirectNotGridAligned!: boolean; } export class UpdateNodeGraphTransform extends JsMessage { diff --git a/frontend/src/state-providers/node-graph.ts b/frontend/src/state-providers/node-graph.ts index 25e9c519..266dcf18 100644 --- a/frontend/src/state-providers/node-graph.ts +++ b/frontend/src/state-providers/node-graph.ts @@ -41,6 +41,7 @@ export function createNodeGraphState(editor: Editor) { addExport: undefined as { x: number; y: number } | undefined, nodes: new Map(), wires: [] as FrontendNodeWire[], + wiresDirectNotGridAligned: false, wirePathInProgress: undefined as WirePath | undefined, inputTypeDescriptions: new Map(), nodeDescriptions: new Map(), @@ -123,6 +124,7 @@ export function createNodeGraphState(editor: Editor) { state.nodes.set(node.id, node); }); state.wires = updateNodeGraph.wires; + state.wiresDirectNotGridAligned = updateNodeGraph.wiresDirectNotGridAligned; return state; }); });