From 560c0aecb6ead8f15db387d3dbf493030a8332b5 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Mon, 18 Aug 2025 16:47:13 -0400 Subject: [PATCH 1/4] whitespaces --- src/components/NodeSidebar.jsx | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/components/NodeSidebar.jsx b/src/components/NodeSidebar.jsx index ce091f5..52914b0 100644 --- a/src/components/NodeSidebar.jsx +++ b/src/components/NodeSidebar.jsx @@ -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; } @@ -120,7 +120,7 @@ const NodeSidebar = ({ {selectedNode.data.label} )} -

TYPE: {selectedNode.type}

-

ID: {selectedNode.id}

-

-

Node Color

- +
- +
- + {/* Color preset buttons */} -
Close - + {/* Documentation Section */} -
-
- + {isDocumentationExpanded && ( -
Date: Mon, 18 Aug 2025 17:12:58 -0400 Subject: [PATCH 2/4] added new node --- src/components/nodes/AddSubNode.jsx | 111 +++++++++++++++++++++ src/components/nodes/DynamicHandleNode.jsx | 38 +++---- src/nodeConfig.js | 7 +- src/python/pathsim_utils.py | 6 +- 4 files changed, 140 insertions(+), 22 deletions(-) create mode 100644 src/components/nodes/AddSubNode.jsx diff --git a/src/components/nodes/AddSubNode.jsx b/src/components/nodes/AddSubNode.jsx new file mode 100644 index 0000000..c41c52c --- /dev/null +++ b/src/components/nodes/AddSubNode.jsx @@ -0,0 +1,111 @@ +import React, { useCallback, useState, useEffect } from 'react'; +import { Handle, useUpdateNodeInternals } from '@xyflow/react'; + +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 || '++'; // Default to two positive inputs + 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 ( +
+
Σ
+ + {/* 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 ( + + + {/* Operation label at consistent radius inside the circle */} +
+ {operation} +
+
+ ); + })} + + {/* Single output handle on the right */} + +
+ ); +} diff --git a/src/components/nodes/DynamicHandleNode.jsx b/src/components/nodes/DynamicHandleNode.jsx index 11754f9..6b324a3 100644 --- a/src/components/nodes/DynamicHandleNode.jsx +++ b/src/components/nodes/DynamicHandleNode.jsx @@ -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 (
1 && (
- {index + 1} + {index + 1}
)} ); })} - + {/* Output Handles (right side) */} {Array.from({ length: outputHandleCount }).map((_, index) => { const topPercentage = outputHandleCount === 1 ? 50 : ((index + 1) / (outputHandleCount + 1)) * 100; @@ -89,7 +89,7 @@ export function DynamicHandleNode({ id, data }) { type="source" position="right" id={`source-${index}`} - style={{ + style={{ background: '#555', top: `${topPercentage}%` }} @@ -97,7 +97,7 @@ export function DynamicHandleNode({ id, data }) { {/* Output label for multiple outputs */} {outputHandleCount > 1 && (
- {index + 1} + {index + 1}
)} @@ -116,9 +116,9 @@ export function DynamicHandleNode({ id, data }) { })} {/* Main content */} -
{ } }); -export const nodeDynamicHandles = ['ode', 'function', 'interface']; +export const nodeDynamicHandles = ['ode', 'function', 'interface', 'addsub']; // Node categories for better organization export const nodeCategories = { @@ -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': { @@ -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)', diff --git a/src/python/pathsim_utils.py b/src/python/pathsim_utils.py index 7799bea..184d9ec 100644 --- a/src/python/pathsim_utils.py +++ b/src/python/pathsim_utils.py @@ -105,6 +105,7 @@ "splitter2": Splitter2, "splitter3": Splitter3, "adder": Adder, + "addsub": Adder, "adder_reverse": Adder, "multiplier": Multiplier, "process": Process, @@ -446,7 +447,7 @@ def get_parameters_for_block_class(block_class, node, eval_namespace): continue # Skip 'operations' for Adder, as it is handled separately # https://github.com/festim-dev/pathview/issues/73 - if k in ["operations"]: + if k in ["operations"] and node["type"] != "addsub": continue user_input = node["data"][k] if user_input == "": @@ -518,6 +519,9 @@ def get_input_index(block: Block, edge: dict, block_to_input_index: dict) -> int # TODO maybe we could directly use the targetHandle as a port alias for these: if type(block) in (Function, ODE, pathsim.blocks.Switch): return int(edge["targetHandle"].replace("target-", "")) + if isinstance(block, Adder): + if block.operations: + return int(edge["targetHandle"].replace("target-", "")) else: # make sure that the target block has only one input port (ie. that targetHandle is None) assert edge["targetHandle"] is None, ( From d6191859eb5a72079db0c056cb3e2d0c63bc6c42 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Mon, 18 Aug 2025 17:26:25 -0400 Subject: [PATCH 3/4] limit to one connection --- src/components/nodes/AddSubNode.jsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/nodes/AddSubNode.jsx b/src/components/nodes/AddSubNode.jsx index c41c52c..7093689 100644 --- a/src/components/nodes/AddSubNode.jsx +++ b/src/components/nodes/AddSubNode.jsx @@ -1,15 +1,16 @@ import React, { useCallback, useState, useEffect } from 'react'; 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 || '++'; // Default to two positive inputs - if (operations.length >= 2 && + 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[0] === "'" && operations[operations.length - 1] === "'"))) { operations = operations.slice(1, -1); } @@ -54,7 +55,7 @@ export default function AddSubNode({ id, data }) { 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] || '+'; + 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 @@ -64,7 +65,7 @@ export default function AddSubNode({ id, data }) { return ( - Date: Tue, 19 Aug 2025 08:58:03 -0400 Subject: [PATCH 4/4] fixed input/output indices --- src/python/pathsim_utils.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/python/pathsim_utils.py b/src/python/pathsim_utils.py index 184d9ec..c8ed281 100644 --- a/src/python/pathsim_utils.py +++ b/src/python/pathsim_utils.py @@ -522,13 +522,13 @@ def get_input_index(block: Block, edge: dict, block_to_input_index: dict) -> int if isinstance(block, Adder): if block.operations: return int(edge["targetHandle"].replace("target-", "")) - else: - # make sure that the target block has only one input port (ie. that targetHandle is None) - assert edge["targetHandle"] is None, ( - f"Target block {block.id} has multiple input ports, " - "but connection method hasn't been implemented." - ) - return block_to_input_index[block] + + # make sure that the target block has only one input port (ie. that targetHandle is None) + assert edge["targetHandle"] is None, ( + f"Target block {block.id} has multiple input ports, " + "but connection method hasn't been implemented." + ) + return block_to_input_index[block] # TODO here we could only pass edge and not block @@ -566,13 +566,13 @@ def get_output_index(block: Block, edge: dict) -> int: # 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: - # make sure that the source block has only one output port (ie. that sourceHandle is None) - assert edge["sourceHandle"] is None, ( - f"Source block {block.id} has multiple output ports, " - "but connection method hasn't been implemented." - ) - return 0 + + # make sure that the source block has only one output port (ie. that sourceHandle is None) + assert edge["sourceHandle"] is None, ( + f"Source block {block.id} has multiple output ports, " + "but connection method hasn't been implemented." + ) + return 0 def make_connections(nodes, edges, blocks) -> list[Connection]: