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
30 changes: 15 additions & 15 deletions src/components/NodeSidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const makeVarName = (node) => {
if (!isValidPythonIdentifier(varName)) {
// add var_ prefix if it doesn't start with a letter or underscore
varName = `var_${varName}`;
}
}

return varName;
}
Expand Down Expand Up @@ -120,7 +120,7 @@ const NodeSidebar = ({
{selectedNode.data.label}
</h3>
)}
<h4 style={{
<h4 style={{
margin: '12px 0 8px 0',
fontSize: '14px',
fontWeight: '600',
Expand All @@ -130,7 +130,7 @@ const NodeSidebar = ({
borderBottom: '1px solid #343556',
paddingBottom: '8px'
}}>TYPE: {selectedNode.type}</h4>
<h4 style={{
<h4 style={{
margin: '12px 0 8px 0',
fontSize: '14px',
fontWeight: '600',
Expand All @@ -140,7 +140,7 @@ const NodeSidebar = ({
borderBottom: '1px solid #343556',
paddingBottom: '8px'
}}>ID: {selectedNode.id}</h4>
<h4 style={{
<h4 style={{
margin: '12px 0 8px 0',
fontSize: '14px',
fontWeight: '600',
Expand Down Expand Up @@ -213,21 +213,21 @@ const NodeSidebar = ({
})()}

{/* Color Picker Section */}
<div style={{
<div style={{
marginTop: '20px',
marginBottom: '20px',
borderTop: '1px solid #555',
paddingTop: '15px'
}}>
<h4 style={{
<h4 style={{
margin: '0 0 12px 0',
fontSize: '14px',
fontWeight: '600',
color: '#a8b3cf',
textTransform: 'uppercase',
letterSpacing: '0.5px'
}}>Node Color</h4>

<div style={{
display: 'flex',
alignItems: 'center',
Expand Down Expand Up @@ -261,7 +261,7 @@ const NodeSidebar = ({
padding: '0'
}}
/>

<input
type="text"
value={selectedNode.data.nodeColor || '#DDE6ED'}
Expand Down Expand Up @@ -292,9 +292,9 @@ const NodeSidebar = ({
}}
/>
</div>

{/* Color preset buttons */}
<div style={{
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)',
gap: '8px',
Expand Down Expand Up @@ -354,14 +354,14 @@ const NodeSidebar = ({
>
Close
</button>

{/* Documentation Section */}
<div style={{
<div style={{
marginTop: '20px',
borderTop: '1px solid #555',
paddingTop: '15px'
}}>
<div
<div
style={{
display: 'flex',
alignItems: 'center',
Expand Down Expand Up @@ -391,9 +391,9 @@ const NodeSidebar = ({
</span>
</div>

{isDocumentationExpanded && (
<div
<div
className="documentation-content"
style={{
backgroundColor: '#2a2a3e',
Expand Down
112 changes: 112 additions & 0 deletions src/components/nodes/AddSubNode.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React, { useCallback, useState, useEffect } from 'react';

Check failure on line 1 in src/components/nodes/AddSubNode.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/AddSubNode.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';
import CustomHandle from './CustomHandle';

export default function AddSubNode({ id, data }) {
const updateNodeInternals = useUpdateNodeInternals();
const [inputHandleCount, setInputHandleCount] = useState(parseInt(data.inputCount) || 2);

// Handle operations string, removing surrounding quotes if they exist
let operations = data.operations || Array(inputHandleCount).fill('+'); // Default to positive inputs the length of inputHandleCount
if (operations.length >= 2 &&
((operations[0] === '"' && operations[operations.length - 1] === '"') ||
(operations[0] === "'" && operations[operations.length - 1] === "'"))) {
operations = operations.slice(1, -1);
}

useEffect(() => {
if (data.inputCount !== undefined && parseInt(data.inputCount) !== inputHandleCount) {
setInputHandleCount(parseInt(data.inputCount) || 2);
updateNodeInternals(id);
}
}, [data.inputCount, inputHandleCount, id, updateNodeInternals]);

// Calculate node size based on number of inputs
const nodeSize = Math.max(60, inputHandleCount * 15 + 30);

return (
<div
style={{
width: nodeSize,
height: nodeSize,
background: data.nodeColor || '#DDE6ED',
color: 'black',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 'bold',
fontSize: '24px',
position: 'relative',
cursor: 'pointer',
border: '2px solid #333',
}}
>
<div>Σ</div>

{/* Input Handles distributed around the left side of the circle */}
{Array.from({ length: inputHandleCount }).map((_, index) => {
// Distribute handles around the left semicircle
const angle = inputHandleCount === 1
? Math.PI // Single input at the left (180 degrees)
: Math.PI * (0.5 + index / (inputHandleCount - 1)); // From top-left to bottom-left

const x = 50 + 50 * Math.cos(angle); // x position as percentage
const y = 50 + 50 * Math.sin(angle); // y position as percentage

// Get the operation for this input (default to '+' if not specified)
const operation = operations[index] || '?';

// Calculate label position at a smaller radius that scales with node size
// Smaller nodes get smaller label radius to avoid overlapping with center
const labelRadius = Math.max(0.6, 0.85 - (60 / nodeSize) * 0.25);
const labelX = 50 + 50 * labelRadius * Math.cos(angle);
const labelY = 50 + 50 * labelRadius * Math.sin(angle);

return (
<React.Fragment key={`target-${index}`}>
<CustomHandle
type="target"
position="left"
id={`target-${index}`}
style={{
background: '#555',
position: 'absolute',
left: `${x}%`,
top: `${y}%`,
transform: 'translate(-50%, -50%)',
}}
/>
{/* Operation label at consistent radius inside the circle */}
<div
style={{
position: 'absolute',
left: `${labelX}%`,
top: `${labelY}%`,
transform: 'translate(-50%, -50%)',
fontSize: '12px',
fontWeight: 'bold',
color: operation === '+' ? '#555' : '#555',
pointerEvents: 'none',
}}
>
{operation}
</div>
</React.Fragment>
);
})}

{/* Single output handle on the right */}
<Handle
type="source"
position="right"
style={{
background: '#555',
right: '-6px',
top: '50%',
transform: 'translateY(-50%)',
}}
/>
</div>
);
}
38 changes: 19 additions & 19 deletions src/components/nodes/DynamicHandleNode.jsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
import React, { useCallback, useState, useEffect } from 'react';
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={{
Expand Down Expand Up @@ -53,15 +53,15 @@ export function DynamicHandleNode({ id, data }) {
type="target"
position="left"
id={`target-${index}`}
style={{
style={{
background: '#555',
top: `${topPercentage}%`
}}
/>
{/* Input label for multiple inputs */}
{inputHandleCount > 1 && (
<div
style={{
style={{
position: 'absolute',
left: '8px',
top: `${topPercentage}%`,
Expand All @@ -70,15 +70,15 @@ export function DynamicHandleNode({ id, data }) {
fontWeight: 'normal',
color: '#666',
pointerEvents: 'none',
}}
}}
>
{index + 1}
{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;
Expand All @@ -89,15 +89,15 @@ export function DynamicHandleNode({ id, data }) {
type="source"
position="right"
id={`source-${index}`}
style={{
style={{
background: '#555',
top: `${topPercentage}%`
}}
/>
{/* Output label for multiple outputs */}
{outputHandleCount > 1 && (
<div
style={{
style={{
position: 'absolute',
right: '8px',
top: `${topPercentage}%`,
Expand All @@ -106,19 +106,19 @@ export function DynamicHandleNode({ id, data }) {
fontWeight: 'normal',
color: '#666',
pointerEvents: 'none',
}}
}}
>
{index + 1}
{index + 1}
</div>
)}
</React.Fragment>
);
})}

{/* Main content */}
<div style={{
textAlign: 'center',
wordWrap: 'break-word',
<div style={{
textAlign: 'center',
wordWrap: 'break-word',
maxWidth: '100%',
display: 'flex',
flexDirection: 'column',
Expand Down
7 changes: 5 additions & 2 deletions src/nodeConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import SourceNode from './components/nodes/ConstantNode';
import { AmplifierNode, AmplifierNodeReverse } from './components/nodes/AmplifierNode';
import IntegratorNode from './components/nodes/IntegratorNode';
import AdderNode from './components/nodes/AdderNode';
import AddSubNode from './components/nodes/AddSubNode';
import ScopeNode from './components/nodes/ScopeNode';
import StepSourceNode from './components/nodes/StepSourceNode';
import { createFunctionNode } from './components/nodes/FunctionNode';
Expand Down Expand Up @@ -37,6 +38,7 @@ export const nodeTypes = {
amplifier_reverse: AmplifierNodeReverse,
integrator: IntegratorNode,
adder: AdderNode,
addsub: AddSubNode,
multiplier: MultiplierNode,
scope: ScopeNode,
function: DynamicHandleNode,
Expand Down Expand Up @@ -90,7 +92,7 @@ Object.keys(nodeMathTypes).forEach(type => {
}
});

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

// Node categories for better organization
export const nodeCategories = {
Expand All @@ -103,7 +105,7 @@ export const nodeCategories = {
description: 'Signal processing and transformation nodes'
},
'Math': {
nodes: ['adder', 'multiplier', 'splitter2', 'splitter3'].concat(Object.keys(nodeMathTypes)),
nodes: ['adder', 'addsub', 'multiplier', 'splitter2', 'splitter3'].concat(Object.keys(nodeMathTypes)),
description: 'Mathematical operation nodes'
},
'Control': {
Expand Down Expand Up @@ -153,6 +155,7 @@ export const getNodeDisplayName = (nodeType) => {
'integrator': 'Integrator',
'function': 'Function',
'adder': 'Adder',
'addsub': 'Adder/Subtractor',
'ode': 'ODE',
'multiplier': 'Multiplier',
'splitter2': 'Splitter (1→2)',
Expand Down
Loading
Loading