diff --git a/src/App.jsx b/src/App.jsx index 7af9104b..03dfbe26 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -21,9 +21,11 @@ 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'; + // * Declaring variables * // Defining initial nodes and edges. In the data section, we have label, but also parameters specific to the node. @@ -251,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] = ''; @@ -953,7 +961,6 @@ const DnDFlow = () => { setMenu(null); // Close the context menu }, [nodes, nodeCounter, setNodeCounter, setNodes, setMenu]); - // Keyboard event handler for deleting selected items useEffect(() => { const handleKeyDown = (event) => { diff --git a/src/components/ContextMenu.jsx b/src/components/ContextMenu.jsx index b23e2b8b..449192c4 100644 --- a/src/components/ContextMenu.jsx +++ b/src/components/ContextMenu.jsx @@ -22,7 +22,7 @@ export default function ContextMenu({ setEdges((edges) => edges.filter((edge) => edge.source !== id && edge.target !== id)); onClick && onClick(); // Close menu after action }, [id, setNodes, setEdges, onClick]); - + return (
{ + let shouldUpdate = false; + + if (data.inputCount !== undefined && parseInt(data.inputCount) !== inputHandleCount) { + setInputHandleCount(parseInt(data.inputCount) || 0); + shouldUpdate = true; + } + + if (data.outputCount !== undefined && parseInt(data.outputCount) !== outputHandleCount) { + setOutputHandleCount(parseInt(data.outputCount) || 0); + 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}
+
+
+ ); +} \ No newline at end of file diff --git a/src/nodeConfig.js b/src/nodeConfig.js index db823b83..006f9d38 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 { DynamicHandleNode } from './components/nodes/DynamicHandleNode'; // Node types mapping export const nodeTypes = { @@ -37,8 +38,7 @@ export const nodeTypes = { adder: AdderNode, multiplier: MultiplierNode, scope: ScopeNode, - function: createFunctionNode(1, 1), // 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, @@ -57,7 +57,8 @@ export const nodeTypes = { butterworthhighpass: DefaultNode, butterworthbandpass: DefaultNode, butterworthbandstop: DefaultNode, - fir: DefaultNode + fir: DefaultNode, + ode: DynamicHandleNode, }; export const nodeMathTypes = { @@ -86,6 +87,8 @@ Object.keys(nodeMathTypes).forEach(type => { } }); +export const nodeDynamicHandles = ['ode', 'function']; + // Node categories for better organization export const nodeCategories = { 'Sources': { @@ -93,7 +96,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', 'ode'], description: 'Signal processing and transformation nodes' }, 'Math': { @@ -138,8 +141,8 @@ export const getNodeDisplayName = (nodeType) => { 'amplifier_reverse': 'Amplifier (Reverse)', 'integrator': 'Integrator', '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..4804791c 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 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) @@ -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: + 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-", "")) else: