Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@
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';

Check failure on line 27 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.11)

'createFunctionNode' is defined but never used. Allowed unused vars must match /^[A-Z_]/u

Check failure on line 27 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.10)

'createFunctionNode' is defined but never used. Allowed unused vars must match /^[A-Z_]/u

// * Declaring variables *

// Defining initial nodes and edges. In the data section, we have label, but also parameters specific to the node.
Expand Down Expand Up @@ -66,7 +68,7 @@
}, []);

const onDragStart = (event, nodeType) => {
setType(nodeType);

Check failure on line 71 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.11)

'setType' is not defined

Check failure on line 71 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.10)

'setType' is not defined
event.dataTransfer.setData('text/plain', nodeType);
event.dataTransfer.effectAllowed = 'move';
};
Expand Down Expand Up @@ -251,6 +253,12 @@
// 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] = '';
Expand All @@ -266,7 +274,7 @@
setNodes((nds) => [...nds, newNode]);
setNodeCounter((count) => count + 1);
},
[screenToFlowPosition, type, nodeCounter, fetchDefaultValues, setDefaultValues, setNodes, setNodeCounter],

Check warning on line 277 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.11)

React Hook useCallback has an unnecessary dependency: 'setDefaultValues'. Either exclude it or remove the dependency array

Check warning on line 277 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.10)

React Hook useCallback has an unnecessary dependency: 'setDefaultValues'. Either exclude it or remove the dependency array
);

// Function to save a graph to computer with "Save As" dialog
Expand Down Expand Up @@ -953,7 +961,6 @@
setMenu(null); // Close the context menu
}, [nodes, nodeCounter, setNodeCounter, setNodes, setMenu]);


// Keyboard event handler for deleting selected items
useEffect(() => {
const handleKeyDown = (event) => {
Expand Down Expand Up @@ -1004,7 +1011,7 @@
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [selectedEdge, selectedNode, copiedNode, duplicateNode, setCopyFeedback]);

Check warning on line 1014 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.11)

React Hook useEffect has missing dependencies: 'deleteSelectedEdge' and 'deleteSelectedNode'. Either include them or remove the dependency array

Check warning on line 1014 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.10)

React Hook useEffect has missing dependencies: 'deleteSelectedEdge' and 'deleteSelectedNode'. Either include them or remove the dependency array

return (
<div style={{ width: '100vw', height: '100vh', display: 'flex', flexDirection: 'column' }}>
Expand Down
2 changes: 1 addition & 1 deletion src/components/ContextMenu.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div
style={{ top, left, right, bottom }}
Expand Down
131 changes: 131 additions & 0 deletions src/components/nodes/DynamicHandleNode.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import React, { useCallback, useState, useEffect } from 'react';

Check failure on line 1 in src/components/nodes/DynamicHandleNode.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.11)

'useCallback' is defined but never used. Allowed unused vars must match /^[A-Z_]/u

Check failure on line 1 in src/components/nodes/DynamicHandleNode.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.10)

'useCallback' is defined but never used. Allowed unused vars must match /^[A-Z_]/u
import { Handle, useUpdateNodeInternals } from '@xyflow/react';

export function DynamicHandleNode({ id, data }) {
const updateNodeInternals = useUpdateNodeInternals();
const [inputHandleCount, setInputHandleCount] = useState(parseInt(data.inputCount) || 0);
const [outputHandleCount, setOutputHandleCount] = useState(parseInt(data.outputCount) || 0);

useEffect(() => {
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 (
<div
style={{
width: Math.max(180, (data.label?.length || 8) * 8 + 40),
height: Math.max(60, Math.max(inputHandleCount, outputHandleCount) * 25 + 30),
background: '#DDE6ED',
color: 'black',
borderRadius: 0,
padding: 10,
fontWeight: 'bold',
position: 'relative',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{/* Input Handles (left side) */}
{Array.from({ length: inputHandleCount }).map((_, index) => {
const topPercentage = inputHandleCount === 1 ? 50 : ((index + 1) / (inputHandleCount + 1)) * 100;
return (
<React.Fragment key={`target-${index}`}>
<Handle
key={`target-${index}`}
type="target"
position="left"
id={`target-${index}`}
style={{
background: '#555',
top: `${topPercentage}%`
}}
/>
{/* Input label for multiple inputs */}
{inputHandleCount > 1 && (
<div
style={{
position: 'absolute',
left: '8px',
top: `${topPercentage}%`,
transform: 'translateY(-50%)',
fontSize: '10px',
fontWeight: 'normal',
color: '#666',
pointerEvents: 'none',
}}
>
{index + 1}
</div>
)}
</React.Fragment>
);
})}

{/* Output Handles (right side) */}
{Array.from({ length: outputHandleCount }).map((_, index) => {
const topPercentage = outputHandleCount === 1 ? 50 : ((index + 1) / (outputHandleCount + 1)) * 100;
return (
<React.Fragment key={`source-${index}`}>
<Handle
key={`source-${index}`}
type="source"
position="right"
id={`source-${index}`}
style={{
background: '#555',
top: `${topPercentage}%`
}}
/>
{/* Output label for multiple outputs */}
{outputHandleCount > 1 && (
<div
style={{
position: 'absolute',
right: '8px',
top: `${topPercentage}%`,
transform: 'translateY(-50%)',
fontSize: '10px',
fontWeight: 'normal',
color: '#666',
pointerEvents: 'none',
}}
>
{index + 1}
</div>
)}
</React.Fragment>
);
})}

{/* Main content */}
<div style={{
textAlign: 'center',
wordWrap: 'break-word',
maxWidth: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}>
<div>{data.label}</div>
</div>
</div>
);
}
13 changes: 8 additions & 5 deletions src/nodeConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
import AdderNode from './components/nodes/AdderNode';
import ScopeNode from './components/nodes/ScopeNode';
import StepSourceNode from './components/nodes/StepSourceNode';
import { createFunctionNode } from './components/nodes/FunctionNode';

Check failure on line 10 in src/nodeConfig.js

View workflow job for this annotation

GitHub Actions / test (20.x, 3.11)

'createFunctionNode' is defined but never used. Allowed unused vars must match /^[A-Z_]/u

Check failure on line 10 in src/nodeConfig.js

View workflow job for this annotation

GitHub Actions / test (20.x, 3.10)

'createFunctionNode' is defined but never used. Allowed unused vars must match /^[A-Z_]/u
import DefaultNode from './components/nodes/DefaultNode';
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 = {
Expand All @@ -37,8 +38,7 @@
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,
Expand All @@ -57,7 +57,8 @@
butterworthhighpass: DefaultNode,
butterworthbandpass: DefaultNode,
butterworthbandstop: DefaultNode,
fir: DefaultNode
fir: DefaultNode,
ode: DynamicHandleNode,
};

export const nodeMathTypes = {
Expand Down Expand Up @@ -86,14 +87,16 @@
}
});

export const nodeDynamicHandles = ['ode', 'function'];

// Node categories for better organization
export const nodeCategories = {
'Sources': {
nodes: ['constant', 'stepsource', 'source', 'pulsesource', 'trianglewavesource', 'sinusoidalsource', 'gaussianpulsesource', 'sinusoidalphasenoisesource', 'chirpphasenoisesource', 'chirpsource', 'clocksource', 'squarewavesource', 'rng', 'white_noise', 'pink_noise'],
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': {
Expand Down Expand Up @@ -138,8 +141,8 @@
'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)',
Expand Down
10 changes: 7 additions & 3 deletions src/python/pathsim_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
PID,
Spectrum,
Differentiator,
ODE,
Schedule,
)
import pathsim.blocks
Expand Down Expand Up @@ -73,6 +74,7 @@
"function": Function,
"function2to2": Function,
"delay": Delay,
"ode": ODE,
"bubbler": Bubbler,
"wall": FestimWall,
"white_noise": WhiteNoise,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
Loading