diff --git a/src/components/NodeSidebar.jsx b/src/components/NodeSidebar.jsx index ce091f5c..52914b0b 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 && ( -
= 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 11754f96..6b324a35 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 7799beaa..c8ed2819 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,13 +519,16 @@ 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-", "")) - 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] + if isinstance(block, Adder): + if block.operations: + return int(edge["targetHandle"].replace("target-", "")) + + # 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 @@ -562,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]: