Skip to content

Commit 920fe5a

Browse files
Merge pull request #168 from festim-dev/add-sub
Add/Sub node
2 parents f5db0c9 + 4c375d3 commit 920fe5a

File tree

5 files changed

+170
-51
lines changed

5 files changed

+170
-51
lines changed

src/components/NodeSidebar.jsx

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const makeVarName = (node) => {
1111
if (!isValidPythonIdentifier(varName)) {
1212
// add var_ prefix if it doesn't start with a letter or underscore
1313
varName = `var_${varName}`;
14-
}
14+
}
1515

1616
return varName;
1717
}
@@ -120,7 +120,7 @@ const NodeSidebar = ({
120120
{selectedNode.data.label}
121121
</h3>
122122
)}
123-
<h4 style={{
123+
<h4 style={{
124124
margin: '12px 0 8px 0',
125125
fontSize: '14px',
126126
fontWeight: '600',
@@ -130,7 +130,7 @@ const NodeSidebar = ({
130130
borderBottom: '1px solid #343556',
131131
paddingBottom: '8px'
132132
}}>TYPE: {selectedNode.type}</h4>
133-
<h4 style={{
133+
<h4 style={{
134134
margin: '12px 0 8px 0',
135135
fontSize: '14px',
136136
fontWeight: '600',
@@ -140,7 +140,7 @@ const NodeSidebar = ({
140140
borderBottom: '1px solid #343556',
141141
paddingBottom: '8px'
142142
}}>ID: {selectedNode.id}</h4>
143-
<h4 style={{
143+
<h4 style={{
144144
margin: '12px 0 8px 0',
145145
fontSize: '14px',
146146
fontWeight: '600',
@@ -213,21 +213,21 @@ const NodeSidebar = ({
213213
})()}
214214

215215
{/* Color Picker Section */}
216-
<div style={{
216+
<div style={{
217217
marginTop: '20px',
218218
marginBottom: '20px',
219219
borderTop: '1px solid #555',
220220
paddingTop: '15px'
221221
}}>
222-
<h4 style={{
222+
<h4 style={{
223223
margin: '0 0 12px 0',
224224
fontSize: '14px',
225225
fontWeight: '600',
226226
color: '#a8b3cf',
227227
textTransform: 'uppercase',
228228
letterSpacing: '0.5px'
229229
}}>Node Color</h4>
230-
230+
231231
<div style={{
232232
display: 'flex',
233233
alignItems: 'center',
@@ -261,7 +261,7 @@ const NodeSidebar = ({
261261
padding: '0'
262262
}}
263263
/>
264-
264+
265265
<input
266266
type="text"
267267
value={selectedNode.data.nodeColor || '#DDE6ED'}
@@ -292,9 +292,9 @@ const NodeSidebar = ({
292292
}}
293293
/>
294294
</div>
295-
295+
296296
{/* Color preset buttons */}
297-
<div style={{
297+
<div style={{
298298
display: 'grid',
299299
gridTemplateColumns: 'repeat(4, 1fr)',
300300
gap: '8px',
@@ -354,14 +354,14 @@ const NodeSidebar = ({
354354
>
355355
Close
356356
</button>
357-
357+
358358
{/* Documentation Section */}
359-
<div style={{
359+
<div style={{
360360
marginTop: '20px',
361361
borderTop: '1px solid #555',
362362
paddingTop: '15px'
363363
}}>
364-
<div
364+
<div
365365
style={{
366366
display: 'flex',
367367
alignItems: 'center',
@@ -391,9 +391,9 @@ const NodeSidebar = ({
391391
392392
</span>
393393
</div>
394-
394+
395395
{isDocumentationExpanded && (
396-
<div
396+
<div
397397
className="documentation-content"
398398
style={{
399399
backgroundColor: '#2a2a3e',
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import React, { useCallback, useState, useEffect } from 'react';
2+
import { Handle, useUpdateNodeInternals } from '@xyflow/react';
3+
import CustomHandle from './CustomHandle';
4+
5+
export default function AddSubNode({ id, data }) {
6+
const updateNodeInternals = useUpdateNodeInternals();
7+
const [inputHandleCount, setInputHandleCount] = useState(parseInt(data.inputCount) || 2);
8+
9+
// Handle operations string, removing surrounding quotes if they exist
10+
let operations = data.operations || Array(inputHandleCount).fill('+'); // Default to positive inputs the length of inputHandleCount
11+
if (operations.length >= 2 &&
12+
((operations[0] === '"' && operations[operations.length - 1] === '"') ||
13+
(operations[0] === "'" && operations[operations.length - 1] === "'"))) {
14+
operations = operations.slice(1, -1);
15+
}
16+
17+
useEffect(() => {
18+
if (data.inputCount !== undefined && parseInt(data.inputCount) !== inputHandleCount) {
19+
setInputHandleCount(parseInt(data.inputCount) || 2);
20+
updateNodeInternals(id);
21+
}
22+
}, [data.inputCount, inputHandleCount, id, updateNodeInternals]);
23+
24+
// Calculate node size based on number of inputs
25+
const nodeSize = Math.max(60, inputHandleCount * 15 + 30);
26+
27+
return (
28+
<div
29+
style={{
30+
width: nodeSize,
31+
height: nodeSize,
32+
background: data.nodeColor || '#DDE6ED',
33+
color: 'black',
34+
borderRadius: '50%',
35+
display: 'flex',
36+
alignItems: 'center',
37+
justifyContent: 'center',
38+
fontWeight: 'bold',
39+
fontSize: '24px',
40+
position: 'relative',
41+
cursor: 'pointer',
42+
border: '2px solid #333',
43+
}}
44+
>
45+
<div>Σ</div>
46+
47+
{/* Input Handles distributed around the left side of the circle */}
48+
{Array.from({ length: inputHandleCount }).map((_, index) => {
49+
// Distribute handles around the left semicircle
50+
const angle = inputHandleCount === 1
51+
? Math.PI // Single input at the left (180 degrees)
52+
: Math.PI * (0.5 + index / (inputHandleCount - 1)); // From top-left to bottom-left
53+
54+
const x = 50 + 50 * Math.cos(angle); // x position as percentage
55+
const y = 50 + 50 * Math.sin(angle); // y position as percentage
56+
57+
// Get the operation for this input (default to '+' if not specified)
58+
const operation = operations[index] || '?';
59+
60+
// Calculate label position at a smaller radius that scales with node size
61+
// Smaller nodes get smaller label radius to avoid overlapping with center
62+
const labelRadius = Math.max(0.6, 0.85 - (60 / nodeSize) * 0.25);
63+
const labelX = 50 + 50 * labelRadius * Math.cos(angle);
64+
const labelY = 50 + 50 * labelRadius * Math.sin(angle);
65+
66+
return (
67+
<React.Fragment key={`target-${index}`}>
68+
<CustomHandle
69+
type="target"
70+
position="left"
71+
id={`target-${index}`}
72+
style={{
73+
background: '#555',
74+
position: 'absolute',
75+
left: `${x}%`,
76+
top: `${y}%`,
77+
transform: 'translate(-50%, -50%)',
78+
}}
79+
/>
80+
{/* Operation label at consistent radius inside the circle */}
81+
<div
82+
style={{
83+
position: 'absolute',
84+
left: `${labelX}%`,
85+
top: `${labelY}%`,
86+
transform: 'translate(-50%, -50%)',
87+
fontSize: '12px',
88+
fontWeight: 'bold',
89+
color: operation === '+' ? '#555' : '#555',
90+
pointerEvents: 'none',
91+
}}
92+
>
93+
{operation}
94+
</div>
95+
</React.Fragment>
96+
);
97+
})}
98+
99+
{/* Single output handle on the right */}
100+
<Handle
101+
type="source"
102+
position="right"
103+
style={{
104+
background: '#555',
105+
right: '-6px',
106+
top: '50%',
107+
transform: 'translateY(-50%)',
108+
}}
109+
/>
110+
</div>
111+
);
112+
}

src/components/nodes/DynamicHandleNode.jsx

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,31 @@
11
import React, { useCallback, useState, useEffect } from 'react';
22
import { Handle, useUpdateNodeInternals } from '@xyflow/react';
3-
3+
44
export function DynamicHandleNode({ id, data }) {
55
const updateNodeInternals = useUpdateNodeInternals();
66
const [inputHandleCount, setInputHandleCount] = useState(parseInt(data.inputCount) || 0);
77
const [outputHandleCount, setOutputHandleCount] = useState(parseInt(data.outputCount) || 0);
8-
8+
99
useEffect(() => {
1010
let shouldUpdate = false;
11-
11+
1212
if (data.inputCount !== undefined && parseInt(data.inputCount) !== inputHandleCount) {
1313
setInputHandleCount(parseInt(data.inputCount) || 0);
1414
shouldUpdate = true;
1515
}
16-
16+
1717
if (data.outputCount !== undefined && parseInt(data.outputCount) !== outputHandleCount) {
1818
setOutputHandleCount(parseInt(data.outputCount) || 0);
1919
shouldUpdate = true;
2020
}
21-
21+
2222
if (shouldUpdate) {
2323
updateNodeInternals(id);
2424
}
2525
}, [data.inputCount, data.outputCount, inputHandleCount, outputHandleCount, id, updateNodeInternals]);
26-
2726

28-
27+
28+
2929
return (
3030
<div
3131
style={{
@@ -53,15 +53,15 @@ export function DynamicHandleNode({ id, data }) {
5353
type="target"
5454
position="left"
5555
id={`target-${index}`}
56-
style={{
56+
style={{
5757
background: '#555',
5858
top: `${topPercentage}%`
5959
}}
6060
/>
6161
{/* Input label for multiple inputs */}
6262
{inputHandleCount > 1 && (
6363
<div
64-
style={{
64+
style={{
6565
position: 'absolute',
6666
left: '8px',
6767
top: `${topPercentage}%`,
@@ -70,15 +70,15 @@ export function DynamicHandleNode({ id, data }) {
7070
fontWeight: 'normal',
7171
color: '#666',
7272
pointerEvents: 'none',
73-
}}
73+
}}
7474
>
75-
{index + 1}
75+
{index + 1}
7676
</div>
7777
)}
7878
</React.Fragment>
7979
);
8080
})}
81-
81+
8282
{/* Output Handles (right side) */}
8383
{Array.from({ length: outputHandleCount }).map((_, index) => {
8484
const topPercentage = outputHandleCount === 1 ? 50 : ((index + 1) / (outputHandleCount + 1)) * 100;
@@ -89,15 +89,15 @@ export function DynamicHandleNode({ id, data }) {
8989
type="source"
9090
position="right"
9191
id={`source-${index}`}
92-
style={{
92+
style={{
9393
background: '#555',
9494
top: `${topPercentage}%`
9595
}}
9696
/>
9797
{/* Output label for multiple outputs */}
9898
{outputHandleCount > 1 && (
9999
<div
100-
style={{
100+
style={{
101101
position: 'absolute',
102102
right: '8px',
103103
top: `${topPercentage}%`,
@@ -106,19 +106,19 @@ export function DynamicHandleNode({ id, data }) {
106106
fontWeight: 'normal',
107107
color: '#666',
108108
pointerEvents: 'none',
109-
}}
109+
}}
110110
>
111-
{index + 1}
111+
{index + 1}
112112
</div>
113113
)}
114114
</React.Fragment>
115115
);
116116
})}
117117

118118
{/* Main content */}
119-
<div style={{
120-
textAlign: 'center',
121-
wordWrap: 'break-word',
119+
<div style={{
120+
textAlign: 'center',
121+
wordWrap: 'break-word',
122122
maxWidth: '100%',
123123
display: 'flex',
124124
flexDirection: 'column',

src/nodeConfig.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import SourceNode from './components/nodes/ConstantNode';
55
import { AmplifierNode, AmplifierNodeReverse } from './components/nodes/AmplifierNode';
66
import IntegratorNode from './components/nodes/IntegratorNode';
77
import AdderNode from './components/nodes/AdderNode';
8+
import AddSubNode from './components/nodes/AddSubNode';
89
import ScopeNode from './components/nodes/ScopeNode';
910
import StepSourceNode from './components/nodes/StepSourceNode';
1011
import { createFunctionNode } from './components/nodes/FunctionNode';
@@ -37,6 +38,7 @@ export const nodeTypes = {
3738
amplifier_reverse: AmplifierNodeReverse,
3839
integrator: IntegratorNode,
3940
adder: AdderNode,
41+
addsub: AddSubNode,
4042
multiplier: MultiplierNode,
4143
scope: ScopeNode,
4244
function: DynamicHandleNode,
@@ -90,7 +92,7 @@ Object.keys(nodeMathTypes).forEach(type => {
9092
}
9193
});
9294

93-
export const nodeDynamicHandles = ['ode', 'function', 'interface'];
95+
export const nodeDynamicHandles = ['ode', 'function', 'interface', 'addsub'];
9496

9597
// Node categories for better organization
9698
export const nodeCategories = {
@@ -103,7 +105,7 @@ export const nodeCategories = {
103105
description: 'Signal processing and transformation nodes'
104106
},
105107
'Math': {
106-
nodes: ['adder', 'multiplier', 'splitter2', 'splitter3'].concat(Object.keys(nodeMathTypes)),
108+
nodes: ['adder', 'addsub', 'multiplier', 'splitter2', 'splitter3'].concat(Object.keys(nodeMathTypes)),
107109
description: 'Mathematical operation nodes'
108110
},
109111
'Control': {
@@ -153,6 +155,7 @@ export const getNodeDisplayName = (nodeType) => {
153155
'integrator': 'Integrator',
154156
'function': 'Function',
155157
'adder': 'Adder',
158+
'addsub': 'Adder/Subtractor',
156159
'ode': 'ODE',
157160
'multiplier': 'Multiplier',
158161
'splitter2': 'Splitter (1→2)',

0 commit comments

Comments
 (0)