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
33 changes: 10 additions & 23 deletions src/lib/components/nodes/TerminalNode.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -316,13 +317,14 @@
<div class="flowdrop-terminal-node__circle-wrapper">
<!-- Input Handles (for end/exit variants) -->
{#if showInputs}
{#each inputPorts as port (port.id)}
{#each inputPorts as port, index (port.id)}
{@const pos = getCircleHandlePosition(index, inputPorts.length, 'left')}
<Handle
type="target"
position={Position.Left}
style="background-color: {getDataTypeColor(
port.dataType
)}; border-color: var(--fd-handle-border); top: 50%; transform: translateY(-50%); z-index: 30;"
)}; border-color: var(--fd-handle-border); left: {pos.left}px; top: {pos.top}px; transform: translate(-50%, -50%); z-index: 30;"
id={`${props.data.nodeId}-input-${port.id}`}
/>
{/each}
Expand All @@ -343,14 +345,15 @@

<!-- Output Handles (for start variant) -->
{#if showOutputs}
{#each outputPorts as port (port.id)}
{#each outputPorts as port, index (port.id)}
{@const pos = getCircleHandlePosition(index, outputPorts.length, 'right')}
<Handle
type="source"
position={Position.Right}
id={`${props.data.nodeId}-output-${port.id}`}
style="background-color: {getDataTypeColor(
port.dataType
)}; border-color: var(--fd-handle-border); top: 50%; transform: translateY(-50%); z-index: 30;"
)}; border-color: var(--fd-handle-border); left: {pos.left}px; top: {pos.top}px; transform: translate(-50%, -50%); z-index: 30;"
/>
{/each}
{/if}
Expand Down Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down
48 changes: 48 additions & 0 deletions src/lib/utils/handlePositioning.ts
Original file line number Diff line number Diff line change
@@ -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)
};
}
139 changes: 139 additions & 0 deletions src/mocks/data/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {}
}
}
];

Expand Down