diff --git a/src/lib/components/nodes/TerminalNode.svelte b/src/lib/components/nodes/TerminalNode.svelte
index ad89177..f854115 100644
--- a/src/lib/components/nodes/TerminalNode.svelte
+++ b/src/lib/components/nodes/TerminalNode.svelte
@@ -14,6 +14,7 @@
import Icon from '@iconify/svelte';
import { getDataTypeColor, getCategoryColorToken } from '$lib/utils/colors.js';
import { getNodeIcon } from '../../utils/icons.js';
+ import { getCircleHandlePosition } from '$lib/utils/handlePositioning.js';
/**
* Terminal node variant types
@@ -316,13 +317,14 @@
{#if showInputs}
- {#each inputPorts as port (port.id)}
+ {#each inputPorts as port, index (port.id)}
+ {@const pos = getCircleHandlePosition(index, inputPorts.length, 'left')}
{/each}
@@ -343,14 +345,15 @@
{#if showOutputs}
- {#each outputPorts as port (port.id)}
+ {#each outputPorts as port, index (port.id)}
+ {@const pos = getCircleHandlePosition(index, outputPorts.length, 'right')}
{/each}
{/if}
@@ -614,7 +617,7 @@
}
}
- /* Handle styles - positioned relative to circle wrapper */
+ /* Handle styles - positioned along circle arc using cos/sin */
:global(.flowdrop-terminal-node__circle-wrapper .svelte-flow__handle) {
width: 16px !important;
height: 16px !important;
@@ -626,16 +629,8 @@
pointer-events: auto !important;
}
- :global(.flowdrop-terminal-node__circle-wrapper .svelte-flow__handle-left) {
- left: -8px !important;
- }
-
- :global(.flowdrop-terminal-node__circle-wrapper .svelte-flow__handle-right) {
- right: -8px !important;
- }
-
:global(.flowdrop-terminal-node__circle-wrapper .svelte-flow__handle:hover) {
- transform: translateY(-50%) scale(1.2) !important;
+ transform: translate(-50%, -50%) scale(1.2) !important;
}
:global(.flowdrop-terminal-node__circle-wrapper .svelte-flow__handle:focus) {
@@ -655,16 +650,8 @@
pointer-events: auto !important;
}
- :global(.svelte-flow__node-terminal .svelte-flow__handle-left) {
- left: -8px !important;
- }
-
- :global(.svelte-flow__node-terminal .svelte-flow__handle-right) {
- right: -8px !important;
- }
-
:global(.svelte-flow__node-terminal .svelte-flow__handle:hover) {
- transform: translateY(-50%) scale(1.2) !important;
+ transform: translate(-50%, -50%) scale(1.2) !important;
}
:global(.svelte-flow__node-terminal .svelte-flow__handle:focus) {
diff --git a/src/lib/utils/handlePositioning.ts b/src/lib/utils/handlePositioning.ts
new file mode 100644
index 0000000..a0f1577
--- /dev/null
+++ b/src/lib/utils/handlePositioning.ts
@@ -0,0 +1,48 @@
+/**
+ * Utility functions for calculating handle positions on nodes
+ */
+
+export interface HandlePosition {
+ left: number;
+ top: number;
+}
+
+/**
+ * Calculate handle position along a circle arc using cos/sin
+ *
+ * Distributes handles evenly along an arc on the left or right side of a circle.
+ * For N handles, they are positioned at angles calculated as:
+ * angle = centerAngle - arcSpan/2 + arcSpan * (index + 1) / (count + 1)
+ *
+ * @param index - The index of the handle (0-based)
+ * @param count - Total number of handles on this side
+ * @param side - 'left' for inputs, 'right' for outputs
+ * @param radius - The radius of the circle (default: 36px for 72px diameter)
+ * @param arcSpan - The arc span in radians (default: 5π/6 = 150°)
+ * @returns Object with left and top pixel values relative to the circle's bounding box
+ *
+ * @example
+ * // Single handle on left side - positioned at center (180°)
+ * getCircleHandlePosition(0, 1, 'left') // { left: 0, top: 36 }
+ *
+ * @example
+ * // Two handles on left side - positioned at 150° and 210°
+ * getCircleHandlePosition(0, 2, 'left') // { left: ~4.8, top: ~18 }
+ * getCircleHandlePosition(1, 2, 'left') // { left: ~4.8, top: ~54 }
+ */
+export function getCircleHandlePosition(
+ index: number,
+ count: number,
+ side: 'left' | 'right',
+ radius: number = 36,
+ arcSpan: number = (Math.PI * 5) / 6
+): HandlePosition {
+ const centerAngle = side === 'left' ? Math.PI : 0; // 180° for left, 0° for right
+ const angle = centerAngle - arcSpan / 2 + (arcSpan * (index + 1)) / (count + 1);
+ const centerOffset = radius; // center of the circle (assuming square bounding box)
+
+ return {
+ left: centerOffset + radius * Math.cos(angle),
+ top: centerOffset + radius * Math.sin(angle)
+ };
+}
diff --git a/src/mocks/data/nodes.ts b/src/mocks/data/nodes.ts
index 8a8c9ed..6823521 100644
--- a/src/mocks/data/nodes.ts
+++ b/src/mocks/data/nodes.ts
@@ -4904,6 +4904,145 @@ export const mockNodes: NodeMetadata[] = [
},
required: ['recipient']
}
+ },
+ // Test terminal nodes for multi-handle positioning
+ {
+ id: 'test_terminal_multi_output',
+ name: 'Test Multi-Output Start',
+ type: 'terminal',
+ supportedTypes: ['terminal'],
+ description: 'Test terminal node with multiple outputs for handle positioning',
+ category: 'helpers',
+ icon: 'mdi:play-circle',
+ color: '#10b981',
+ version: '1.0.0',
+ tags: ['test', 'start', 'multi-output'],
+ inputs: [],
+ outputs: [
+ {
+ id: 'trigger1',
+ name: 'Trigger 1',
+ type: 'output',
+ dataType: 'trigger',
+ required: false,
+ description: 'First trigger output'
+ },
+ {
+ id: 'trigger2',
+ name: 'Trigger 2',
+ type: 'output',
+ dataType: 'trigger',
+ required: false,
+ description: 'Second trigger output'
+ },
+ {
+ id: 'data',
+ name: 'Data',
+ type: 'output',
+ dataType: 'json',
+ required: false,
+ description: 'Data output'
+ }
+ ],
+ config: {
+ variant: 'start'
+ },
+ configSchema: {
+ type: 'object',
+ properties: {}
+ }
+ },
+ {
+ id: 'test_terminal_multi_input',
+ name: 'Test Multi-Input End',
+ type: 'terminal',
+ supportedTypes: ['terminal'],
+ description: 'Test terminal node with multiple inputs for handle positioning',
+ category: 'helpers',
+ icon: 'mdi:stop-circle',
+ color: '#6b7280',
+ version: '1.0.0',
+ tags: ['test', 'end', 'multi-input'],
+ inputs: [
+ {
+ id: 'trigger1',
+ name: 'Trigger 1',
+ type: 'input',
+ dataType: 'trigger',
+ required: false,
+ description: 'First trigger input'
+ },
+ {
+ id: 'trigger2',
+ name: 'Trigger 2',
+ type: 'input',
+ dataType: 'trigger',
+ required: false,
+ description: 'Second trigger input'
+ }
+ ],
+ outputs: [],
+ config: {
+ variant: 'end'
+ },
+ configSchema: {
+ type: 'object',
+ properties: {}
+ }
+ },
+ {
+ id: 'test_terminal_four_inputs',
+ name: 'Test 4-Input End',
+ type: 'terminal',
+ supportedTypes: ['terminal'],
+ description: 'Test terminal node with four inputs for handle positioning',
+ category: 'helpers',
+ icon: 'mdi:stop-circle',
+ color: '#ef4444',
+ version: '1.0.0',
+ tags: ['test', 'exit', 'multi-input'],
+ inputs: [
+ {
+ id: 'input1',
+ name: 'Input 1',
+ type: 'input',
+ dataType: 'trigger',
+ required: false,
+ description: 'First input'
+ },
+ {
+ id: 'input2',
+ name: 'Input 2',
+ type: 'input',
+ dataType: 'json',
+ required: false,
+ description: 'Second input'
+ },
+ {
+ id: 'input3',
+ name: 'Input 3',
+ type: 'input',
+ dataType: 'string',
+ required: false,
+ description: 'Third input'
+ },
+ {
+ id: 'input4',
+ name: 'Input 4',
+ type: 'input',
+ dataType: 'number',
+ required: false,
+ description: 'Fourth input'
+ }
+ ],
+ outputs: [],
+ config: {
+ variant: 'exit'
+ },
+ configSchema: {
+ type: 'object',
+ properties: {}
+ }
}
];