From 74d05f96befd1757cdd22d64730e18bca1d47091 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Fri, 8 Aug 2025 21:04:34 -0400 Subject: [PATCH 1/8] first example of dynamic handle number --- src/App.jsx | 44 +++++++++++++++++++++++++++++++++- src/components/ContextMenu.jsx | 14 +++++++++++ src/nodeConfig.js | 6 +++-- 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index a17d5aa6..78457b7d 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -23,6 +23,8 @@ import GlobalVariablesTab from './components/GlobalVariablesTab.jsx'; import { makeEdge } from './components/CustomEdge'; import { nodeTypes } from './nodeConfig.js'; +import { createFunctionNode } from './components/nodes/FunctionNode.jsx'; + // * Declaring variables * // Defining initial nodes and edges. In the data section, we have label, but also parameters specific to the node. @@ -934,6 +936,46 @@ const DnDFlow = () => { }, [nodes, nodeCounter, setNodeCounter, setNodes, setMenu]); + // Function to add input to a node + const addInputToNode = useCallback((nodeId) => { + setNodes((nds) => + nds.map((node) => { + if (node.id === nodeId) { + const currentInputCount = node.data.inputCount || 0; + return { + ...node, + data: { + ...node.data, + inputCount: currentInputCount + 1 + } + }; + } + return node; + }) + ); + setMenu(null); // Close the context menu + }, [setNodes, setMenu]); + + // Function to add output to a node + const addOutputToNode = useCallback((nodeId) => { + setNodes((nds) => + nds.map((node) => { + if (node.id === nodeId) { + const currentOutputCount = node.data.outputCount || 0; + return { + ...node, + data: { + ...node.data, + outputCount: currentOutputCount + 1 + } + }; + } + return node; + }) + ); + setMenu(null); // Close the context menu + }, [setNodes, setMenu]); + // Keyboard event handler for deleting selected items useEffect(() => { const handleKeyDown = (event) => { @@ -1109,7 +1151,7 @@ const DnDFlow = () => { - {menu && } + {menu && } {copyFeedback && (
{ + onAddInput && onAddInput(id); + onClick && onClick(); // Close menu after action + }, [id, onAddInput, onClick]); + + const addOutput = useCallback(() => { + onAddOutput && onAddOutput(id); + onClick && onClick(); // Close menu after action + }, [id, onAddOutput, onClick]); + return (
+ +
); } \ No newline at end of file diff --git a/src/nodeConfig.js b/src/nodeConfig.js index db823b83..25a0fdf0 100644 --- a/src/nodeConfig.js +++ b/src/nodeConfig.js @@ -13,6 +13,7 @@ import MultiplierNode from './components/nodes/MultiplierNode'; import { Splitter2Node, Splitter3Node } from './components/nodes/Splitters'; import BubblerNode from './components/nodes/BubblerNode'; import WallNode from './components/nodes/WallNode'; +import { RandomHandleNode } from './components/nodes/arbitraryNode'; // Node types mapping export const nodeTypes = { @@ -57,7 +58,8 @@ export const nodeTypes = { butterworthhighpass: DefaultNode, butterworthbandpass: DefaultNode, butterworthbandstop: DefaultNode, - fir: DefaultNode + fir: DefaultNode, + arbitrary: RandomHandleNode, }; export const nodeMathTypes = { @@ -113,7 +115,7 @@ export const nodeCategories = { description: 'Fuel cycle specific nodes' }, 'Others': { - nodes: ['samplehold', 'comparator'], + nodes: ['samplehold', 'comparator', 'arbitrary'], description: 'Miscellaneous nodes' }, 'Output': { From 7e3f72c9822c63428ac799a35d0a118f303817f1 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Mon, 11 Aug 2025 20:07:35 -0400 Subject: [PATCH 2/8] added node --- src/components/nodes/arbitraryNode.jsx | 141 +++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 src/components/nodes/arbitraryNode.jsx diff --git a/src/components/nodes/arbitraryNode.jsx b/src/components/nodes/arbitraryNode.jsx new file mode 100644 index 00000000..f91569e8 --- /dev/null +++ b/src/components/nodes/arbitraryNode.jsx @@ -0,0 +1,141 @@ +import React, { useCallback, useState, useEffect } from 'react'; +import { Handle, useUpdateNodeInternals } from '@xyflow/react'; + +export function RandomHandleNode({ id, data }) { + const updateNodeInternals = useUpdateNodeInternals(); + const [inputHandleCount, setInputHandleCount] = useState(data.inputCount || 0); + const [outputHandleCount, setOutputHandleCount] = useState(data.outputCount || 0); + + useEffect(() => { + let shouldUpdate = false; + + if (data.inputCount !== undefined && data.inputCount !== inputHandleCount) { + setInputHandleCount(data.inputCount); + shouldUpdate = true; + } + + if (data.outputCount !== undefined && data.outputCount !== outputHandleCount) { + setOutputHandleCount(data.outputCount); + shouldUpdate = true; + } + + if (shouldUpdate) { + updateNodeInternals(id); + } + }, [data.inputCount, data.outputCount, inputHandleCount, outputHandleCount, id, updateNodeInternals]); + + + + return ( +
+ {/* Input Handles (left side) */} + {Array.from({ length: inputHandleCount }).map((_, index) => { + const topPercentage = inputHandleCount === 1 ? 50 : ((index + 1) / (inputHandleCount + 1)) * 100; + return ( + + + {/* Input label for multiple inputs */} + {inputHandleCount > 1 && ( +
+ {index + 1} +
+ )} +
+ ); + })} + + {/* Output Handles (right side) */} + {Array.from({ length: outputHandleCount }).map((_, index) => { + const topPercentage = outputHandleCount === 1 ? 50 : ((index + 1) / (outputHandleCount + 1)) * 100; + return ( + + + {/* Output label for multiple outputs */} + {outputHandleCount > 1 && ( +
+ {index + 1} +
+ )} +
+ ); + })} + + {/* Main content */} +
+
{data.label}
+ {(inputHandleCount > 0 || outputHandleCount > 0) && ( + + Right-click to add handles + + )} +
+
+ ); +} \ No newline at end of file From 5a19d1fc4e145514ba79471053b66b4d1819d08b Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Mon, 11 Aug 2025 20:34:29 -0400 Subject: [PATCH 3/8] removed from context menu + makes sure it's always an int --- src/App.jsx | 51 ++++---------------------- src/components/ContextMenu.jsx | 14 ------- src/components/nodes/arbitraryNode.jsx | 36 +++++++----------- src/nodeConfig.js | 6 ++- 4 files changed, 25 insertions(+), 82 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 3c4f55f3..03dfbe26 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -21,7 +21,7 @@ import ContextMenu from './components/ContextMenu.jsx'; import EventsTab from './components/EventsTab.jsx'; import GlobalVariablesTab from './components/GlobalVariablesTab.jsx'; import { makeEdge } from './components/CustomEdge'; -import { nodeTypes } from './nodeConfig.js'; +import { nodeTypes, nodeDynamicHandles } from './nodeConfig.js'; import LogDock from './components/LogDock.jsx'; import { createFunctionNode } from './components/nodes/FunctionNode.jsx'; @@ -253,6 +253,12 @@ const DnDFlow = () => { // Create node data with label and initialize all expected fields as empty strings let nodeData = { label: `${type} ${newNodeId}` }; + // if node in nodeDynamicHandles, ensure add outputCount and inputCount to data + if (nodeDynamicHandles.includes(type)) { + nodeData.inputCount = 1; + nodeData.outputCount = 1; + } + // Initialize all expected parameters as empty strings Object.keys(defaults).forEach(key => { nodeData[key] = ''; @@ -955,47 +961,6 @@ const DnDFlow = () => { setMenu(null); // Close the context menu }, [nodes, nodeCounter, setNodeCounter, setNodes, setMenu]); - - // Function to add input to a node - const addInputToNode = useCallback((nodeId) => { - setNodes((nds) => - nds.map((node) => { - if (node.id === nodeId) { - const currentInputCount = node.data.inputCount || 0; - return { - ...node, - data: { - ...node.data, - inputCount: currentInputCount + 1 - } - }; - } - return node; - }) - ); - setMenu(null); // Close the context menu - }, [setNodes, setMenu]); - - // Function to add output to a node - const addOutputToNode = useCallback((nodeId) => { - setNodes((nds) => - nds.map((node) => { - if (node.id === nodeId) { - const currentOutputCount = node.data.outputCount || 0; - return { - ...node, - data: { - ...node.data, - outputCount: currentOutputCount + 1 - } - }; - } - return node; - }) - ); - setMenu(null); // Close the context menu - }, [setNodes, setMenu]); - // Keyboard event handler for deleting selected items useEffect(() => { const handleKeyDown = (event) => { @@ -1171,7 +1136,7 @@ const DnDFlow = () => { - {menu && } + {menu && } {copyFeedback && (
edges.filter((edge) => edge.source !== id && edge.target !== id)); onClick && onClick(); // Close menu after action }, [id, setNodes, setEdges, onClick]); - - const addInput = useCallback(() => { - onAddInput && onAddInput(id); - onClick && onClick(); // Close menu after action - }, [id, onAddInput, onClick]); - - const addOutput = useCallback(() => { - onAddOutput && onAddOutput(id); - onClick && onClick(); // Close menu after action - }, [id, onAddOutput, onClick]); return (
- -
); } \ No newline at end of file diff --git a/src/components/nodes/arbitraryNode.jsx b/src/components/nodes/arbitraryNode.jsx index f91569e8..84bdefee 100644 --- a/src/components/nodes/arbitraryNode.jsx +++ b/src/components/nodes/arbitraryNode.jsx @@ -1,21 +1,21 @@ import React, { useCallback, useState, useEffect } from 'react'; import { Handle, useUpdateNodeInternals } from '@xyflow/react'; -export function RandomHandleNode({ id, data }) { +export function DynamicHandleNode({ id, data }) { const updateNodeInternals = useUpdateNodeInternals(); - const [inputHandleCount, setInputHandleCount] = useState(data.inputCount || 0); - const [outputHandleCount, setOutputHandleCount] = useState(data.outputCount || 0); + const [inputHandleCount, setInputHandleCount] = useState(parseInt(data.inputCount) || 0); + const [outputHandleCount, setOutputHandleCount] = useState(parseInt(data.outputCount) || 0); useEffect(() => { let shouldUpdate = false; - if (data.inputCount !== undefined && data.inputCount !== inputHandleCount) { - setInputHandleCount(data.inputCount); + if (data.inputCount !== undefined && parseInt(data.inputCount) !== inputHandleCount) { + setInputHandleCount(parseInt(data.inputCount) || 0); shouldUpdate = true; } - if (data.outputCount !== undefined && data.outputCount !== outputHandleCount) { - setOutputHandleCount(data.outputCount); + if (data.outputCount !== undefined && parseInt(data.outputCount) !== outputHandleCount) { + setOutputHandleCount(parseInt(data.outputCount) || 0); shouldUpdate = true; } @@ -61,7 +61,7 @@ export function RandomHandleNode({ id, data }) { {/* Input label for multiple inputs */} {inputHandleCount > 1 && (
- {index + 1} + {index + 1}
)} @@ -97,7 +97,7 @@ export function RandomHandleNode({ id, data }) { {/* Output label for multiple outputs */} {outputHandleCount > 1 && (
- {index + 1} + {index + 1}
)} @@ -125,16 +125,6 @@ export function RandomHandleNode({ id, data }) { alignItems: 'center' }}>
{data.label}
- {(inputHandleCount > 0 || outputHandleCount > 0) && ( - - Right-click to add handles - - )}
); diff --git a/src/nodeConfig.js b/src/nodeConfig.js index 25a0fdf0..6ae7ed51 100644 --- a/src/nodeConfig.js +++ b/src/nodeConfig.js @@ -13,7 +13,7 @@ import MultiplierNode from './components/nodes/MultiplierNode'; import { Splitter2Node, Splitter3Node } from './components/nodes/Splitters'; import BubblerNode from './components/nodes/BubblerNode'; import WallNode from './components/nodes/WallNode'; -import { RandomHandleNode } from './components/nodes/arbitraryNode'; +import { DynamicHandleNode } from './components/nodes/arbitraryNode'; // Node types mapping export const nodeTypes = { @@ -59,7 +59,7 @@ export const nodeTypes = { butterworthbandpass: DefaultNode, butterworthbandstop: DefaultNode, fir: DefaultNode, - arbitrary: RandomHandleNode, + arbitrary: DynamicHandleNode, }; export const nodeMathTypes = { @@ -88,6 +88,8 @@ Object.keys(nodeMathTypes).forEach(type => { } }); +export const nodeDynamicHandles = ['arbitrary']; + // Node categories for better organization export const nodeCategories = { 'Sources': { From a8713d095a94f993c6bdaa84062e3fc171a7693d Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Mon, 11 Aug 2025 21:02:33 -0400 Subject: [PATCH 4/8] working for ODE and Function --- src/components/nodes/arbitraryNode.jsx | 12 ++++++------ src/nodeConfig.js | 11 ++++++----- src/python/pathsim_utils.py | 10 +++++++--- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/components/nodes/arbitraryNode.jsx b/src/components/nodes/arbitraryNode.jsx index 84bdefee..a4daeec3 100644 --- a/src/components/nodes/arbitraryNode.jsx +++ b/src/components/nodes/arbitraryNode.jsx @@ -47,12 +47,12 @@ export function DynamicHandleNode({ id, data }) { {Array.from({ length: inputHandleCount }).map((_, index) => { const topPercentage = inputHandleCount === 1 ? 50 : ((index + 1) / (inputHandleCount + 1)) * 100; return ( - + { const topPercentage = outputHandleCount === 1 ? 50 : ((index + 1) / (outputHandleCount + 1)) * 100; return ( - + { } }); -export const nodeDynamicHandles = ['arbitrary']; +export const nodeDynamicHandles = ['ode', 'function']; // Node categories for better organization export const nodeCategories = { @@ -97,7 +97,7 @@ export const nodeCategories = { description: 'Signal and data source nodes' }, 'Processing': { - nodes: ['delay', 'amplifier', 'amplifier_reverse', 'integrator', 'differentiator', 'function', 'function2to2'], + nodes: ['delay', 'amplifier', 'amplifier_reverse', 'integrator', 'differentiator', 'function', 'function2to2', 'ode'], description: 'Signal processing and transformation nodes' }, 'Math': { @@ -117,7 +117,7 @@ export const nodeCategories = { description: 'Fuel cycle specific nodes' }, 'Others': { - nodes: ['samplehold', 'comparator', 'arbitrary'], + nodes: ['samplehold', 'comparator'], description: 'Miscellaneous nodes' }, 'Output': { @@ -144,6 +144,7 @@ export const getNodeDisplayName = (nodeType) => { 'function': 'Function', 'function2to2': 'Function (2→2)', 'adder': 'Adder', + 'ode': 'ODE', 'multiplier': 'Multiplier', 'splitter2': 'Splitter (1→2)', 'splitter3': 'Splitter (1→3)', diff --git a/src/python/pathsim_utils.py b/src/python/pathsim_utils.py index c2df81db..ddc73a3e 100644 --- a/src/python/pathsim_utils.py +++ b/src/python/pathsim_utils.py @@ -19,6 +19,7 @@ PID, Spectrum, Differentiator, + ODE, Schedule, ) import pathsim.blocks @@ -73,6 +74,7 @@ "function": Function, "function2to2": Function, "delay": Delay, + "ode": ODE, "bubbler": Bubbler, "wall": FestimWall, "white_noise": WhiteNoise, @@ -374,7 +376,8 @@ def get_input_index(block: Block, edge: dict, block_to_input_index: dict) -> int if block._port_map_in: return edge["targetHandle"] - if isinstance(block, Function): + # TODO maybe we could directly use the targetHandle as a port alias for these: + if isinstance(block, (Function, ODE)): return int(edge["targetHandle"].replace("target-", "")) else: # make sure that the target block has only one input port (ie. that targetHandle is None) @@ -410,8 +413,9 @@ def get_output_index(block: Block, edge: dict) -> int: f"Invalid source handle '{edge['sourceHandle']}' for {edge}." ) return output_index - elif isinstance(block, Function): - # Function outputs are always in order, so we can use the handle directly + # TODO maybe we could directly use the targetHandle as a port alias for these: + elif isinstance(block, (Function, ODE)): + # Function and ODE outputs are always in order, so we can use the handle directly assert edge["sourceHandle"], edge return int(edge["sourceHandle"].replace("source-", "")) else: From 0b40f1ac69a6dcae22a5f81be331cdf5420088ec Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Mon, 11 Aug 2025 21:03:01 -0400 Subject: [PATCH 5/8] removed Function 2 to 2 --- src/nodeConfig.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/nodeConfig.js b/src/nodeConfig.js index 01b82af4..43cd8319 100644 --- a/src/nodeConfig.js +++ b/src/nodeConfig.js @@ -38,8 +38,7 @@ export const nodeTypes = { adder: AdderNode, multiplier: MultiplierNode, scope: ScopeNode, - function: DynamicHandleNode, // Default FunctionNode with 1 input and 1 output - function2to2: createFunctionNode(2, 2), // FunctionNode with 2 inputs and 2 outputs + function: DynamicHandleNode, rng: SourceNode, pid: DefaultNode, antiwinduppid: DefaultNode, @@ -97,7 +96,7 @@ export const nodeCategories = { description: 'Signal and data source nodes' }, 'Processing': { - nodes: ['delay', 'amplifier', 'amplifier_reverse', 'integrator', 'differentiator', 'function', 'function2to2', 'ode'], + nodes: ['delay', 'amplifier', 'amplifier_reverse', 'integrator', 'differentiator', 'function', 'ode'], description: 'Signal processing and transformation nodes' }, 'Math': { @@ -142,7 +141,6 @@ export const getNodeDisplayName = (nodeType) => { 'amplifier_reverse': 'Amplifier (Reverse)', 'integrator': 'Integrator', 'function': 'Function', - 'function2to2': 'Function (2→2)', 'adder': 'Adder', 'ode': 'ODE', 'multiplier': 'Multiplier', From 2104228a2e0575fbf69e83730a62e039fa4d1cc5 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Mon, 11 Aug 2025 21:16:40 -0400 Subject: [PATCH 6/8] renamed file --- .../nodes/{arbitraryNode.jsx => DynamicHandleNode.jsx} | 0 src/nodeConfig.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/components/nodes/{arbitraryNode.jsx => DynamicHandleNode.jsx} (100%) diff --git a/src/components/nodes/arbitraryNode.jsx b/src/components/nodes/DynamicHandleNode.jsx similarity index 100% rename from src/components/nodes/arbitraryNode.jsx rename to src/components/nodes/DynamicHandleNode.jsx diff --git a/src/nodeConfig.js b/src/nodeConfig.js index 43cd8319..006f9d38 100644 --- a/src/nodeConfig.js +++ b/src/nodeConfig.js @@ -13,7 +13,7 @@ import MultiplierNode from './components/nodes/MultiplierNode'; import { Splitter2Node, Splitter3Node } from './components/nodes/Splitters'; import BubblerNode from './components/nodes/BubblerNode'; import WallNode from './components/nodes/WallNode'; -import { DynamicHandleNode } from './components/nodes/arbitraryNode'; +import { DynamicHandleNode } from './components/nodes/DynamicHandleNode'; // Node types mapping export const nodeTypes = { From 58b800495d88a60d8a94676a35aae0a88769eedb Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Mon, 11 Aug 2025 21:25:39 -0400 Subject: [PATCH 7/8] check for type --- src/python/pathsim_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/python/pathsim_utils.py b/src/python/pathsim_utils.py index ddc73a3e..f963a771 100644 --- a/src/python/pathsim_utils.py +++ b/src/python/pathsim_utils.py @@ -377,7 +377,7 @@ def get_input_index(block: Block, edge: dict, block_to_input_index: dict) -> int return edge["targetHandle"] # TODO maybe we could directly use the targetHandle as a port alias for these: - if isinstance(block, (Function, ODE)): + elif type(block) in (Function, ODE): return int(edge["targetHandle"].replace("target-", "")) else: # make sure that the target block has only one input port (ie. that targetHandle is None) @@ -414,7 +414,7 @@ def get_output_index(block: Block, edge: dict) -> int: ) return output_index # TODO maybe we could directly use the targetHandle as a port alias for these: - elif isinstance(block, (Function, ODE)): + elif type(block) in (Function, ODE): # Function and ODE outputs are always in order, so we can use the handle directly assert edge["sourceHandle"], edge return int(edge["sourceHandle"].replace("source-", "")) From 43d0f3d01aabce77e45df1b437dff13d607f2c22 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Mon, 11 Aug 2025 21:33:50 -0400 Subject: [PATCH 8/8] if instead of elif --- src/python/pathsim_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/python/pathsim_utils.py b/src/python/pathsim_utils.py index f963a771..4804791c 100644 --- a/src/python/pathsim_utils.py +++ b/src/python/pathsim_utils.py @@ -377,7 +377,7 @@ def get_input_index(block: Block, edge: dict, block_to_input_index: dict) -> int return edge["targetHandle"] # TODO maybe we could directly use the targetHandle as a port alias for these: - elif type(block) in (Function, ODE): + if type(block) in (Function, ODE): return int(edge["targetHandle"].replace("target-", "")) else: # make sure that the target block has only one input port (ie. that targetHandle is None) @@ -414,7 +414,7 @@ def get_output_index(block: Block, edge: dict) -> int: ) return output_index # TODO maybe we could directly use the targetHandle as a port alias for these: - elif type(block) in (Function, ODE): + if type(block) in (Function, ODE): # Function and ODE outputs are always in order, so we can use the handle directly assert edge["sourceHandle"], edge return int(edge["sourceHandle"].replace("source-", ""))