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 */}
+
+
+ );
+}
\ 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: