@d34dman/flowdrop 0.0.53 → 0.0.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/App.svelte +7 -6
- package/dist/components/NodeSidebar.svelte +0 -2
- package/dist/components/PortCoordinateTracker.svelte +58 -0
- package/dist/components/PortCoordinateTracker.svelte.d.ts +12 -0
- package/dist/components/WorkflowEditor.svelte +43 -9
- package/dist/components/nodes/SimpleNode.svelte +0 -6
- package/dist/components/nodes/SquareNode.svelte +0 -4
- package/dist/components/nodes/WorkflowNode.svelte +0 -10
- package/dist/editor/index.d.ts +1 -0
- package/dist/editor/index.js +2 -0
- package/dist/helpers/proximityConnect.d.ts +20 -4
- package/dist/helpers/proximityConnect.js +106 -16
- package/dist/playground/index.d.ts +1 -1
- package/dist/playground/index.js +1 -1
- package/dist/services/portConfigApi.js +0 -11
- package/dist/stores/interruptStore.d.ts +8 -30
- package/dist/stores/interruptStore.js +7 -76
- package/dist/stores/portCoordinateStore.d.ts +60 -0
- package/dist/stores/portCoordinateStore.js +186 -0
- package/dist/types/index.d.ts +20 -0
- package/package.json +1 -1
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
import { apiToasts, dismissToast } from '../services/toastService.js';
|
|
37
37
|
import { initAutoSave } from '../services/autoSaveService.js';
|
|
38
38
|
import { uiSettings } from '../stores/settingsStore.js';
|
|
39
|
+
import { initializePortCompatibility } from '../utils/connections.js';
|
|
40
|
+
import { DEFAULT_PORT_CONFIG } from '../config/defaultPortConfig.js';
|
|
39
41
|
|
|
40
42
|
/**
|
|
41
43
|
* Configuration props for runtime customization
|
|
@@ -177,9 +179,6 @@
|
|
|
177
179
|
// WorkflowEditor reference for save functionality
|
|
178
180
|
let workflowEditorRef: WorkflowEditor | null = null;
|
|
179
181
|
|
|
180
|
-
// Removed currentWorkflowState - no longer needed
|
|
181
|
-
// The global store ($workflowStore) serves as the single source of truth
|
|
182
|
-
|
|
183
182
|
/**
|
|
184
183
|
* Fetch node types from the server
|
|
185
184
|
*
|
|
@@ -383,9 +382,6 @@
|
|
|
383
382
|
}
|
|
384
383
|
}
|
|
385
384
|
|
|
386
|
-
// Removed handleWorkflowChange function - no longer needed
|
|
387
|
-
// The global store serves as the single source of truth and is already reactive
|
|
388
|
-
|
|
389
385
|
/**
|
|
390
386
|
* Save workflow - exposed API function
|
|
391
387
|
*
|
|
@@ -591,6 +587,11 @@
|
|
|
591
587
|
onMount(() => {
|
|
592
588
|
(async () => {
|
|
593
589
|
await initializeApiEndpoints();
|
|
590
|
+
|
|
591
|
+
// Ensure port compatibility checker is initialized (needed for proximity connect, etc.)
|
|
592
|
+
// mountFlowDropApp initializes this before mounting, but SvelteKit routes need it here.
|
|
593
|
+
initializePortCompatibility(DEFAULT_PORT_CONFIG);
|
|
594
|
+
|
|
594
595
|
await fetchNodeTypes();
|
|
595
596
|
|
|
596
597
|
// Initialize the workflow store if we have an initial workflow
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Port Coordinate Tracker Component
|
|
3
|
+
Bridge component that exposes SvelteFlow's getInternalNode to the parent.
|
|
4
|
+
Must be rendered inside SvelteFlowProvider context.
|
|
5
|
+
|
|
6
|
+
Uses the same pattern as EdgeRefresher - a renderless component that hooks
|
|
7
|
+
into the SvelteFlow context.
|
|
8
|
+
-->
|
|
9
|
+
|
|
10
|
+
<script lang="ts">
|
|
11
|
+
import { useSvelteFlow, type InternalNode } from '@xyflow/svelte';
|
|
12
|
+
import type { WorkflowNode as WorkflowNodeType } from '../types/index.js';
|
|
13
|
+
import {
|
|
14
|
+
rebuildAllPortCoordinates,
|
|
15
|
+
updateNodePortCoordinates
|
|
16
|
+
} from '../stores/portCoordinateStore.js';
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
/** Node to update coordinates for (e.g., during drag). Set to null when not dragging. */
|
|
20
|
+
nodeToUpdate: WorkflowNodeType | null;
|
|
21
|
+
/** Set to trigger a full rebuild of all port coordinates */
|
|
22
|
+
rebuildTrigger: number;
|
|
23
|
+
/** All workflow nodes - used for full rebuild */
|
|
24
|
+
nodes: WorkflowNodeType[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let { nodeToUpdate, rebuildTrigger, nodes }: Props = $props();
|
|
28
|
+
|
|
29
|
+
const { getInternalNode } = useSvelteFlow();
|
|
30
|
+
|
|
31
|
+
// Cast the getInternalNode function for our use
|
|
32
|
+
const getInternal = getInternalNode as (id: string) => InternalNode | undefined;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Rebuild all port coordinates when rebuildTrigger changes.
|
|
36
|
+
* Debounced to batch rapid position updates (e.g., animated auto-layout,
|
|
37
|
+
* magnetic child nodes following a parent drag).
|
|
38
|
+
*/
|
|
39
|
+
$effect(() => {
|
|
40
|
+
const _trigger = rebuildTrigger;
|
|
41
|
+
if (_trigger > 0) {
|
|
42
|
+
const timeout = setTimeout(() => {
|
|
43
|
+
rebuildAllPortCoordinates(nodes, getInternal);
|
|
44
|
+
}, 150);
|
|
45
|
+
return () => clearTimeout(timeout);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Update a single node's coordinates when nodeToUpdate changes.
|
|
51
|
+
* This is used during drag for efficient per-node updates.
|
|
52
|
+
*/
|
|
53
|
+
$effect(() => {
|
|
54
|
+
if (nodeToUpdate) {
|
|
55
|
+
updateNodePortCoordinates(nodeToUpdate, getInternal);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
</script>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { WorkflowNode as WorkflowNodeType } from '../types/index.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Node to update coordinates for (e.g., during drag). Set to null when not dragging. */
|
|
4
|
+
nodeToUpdate: WorkflowNodeType | null;
|
|
5
|
+
/** Set to trigger a full rebuild of all port coordinates */
|
|
6
|
+
rebuildTrigger: number;
|
|
7
|
+
/** All workflow nodes - used for full rebuild */
|
|
8
|
+
nodes: WorkflowNodeType[];
|
|
9
|
+
}
|
|
10
|
+
declare const PortCoordinateTracker: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type PortCoordinateTracker = ReturnType<typeof PortCoordinateTracker>;
|
|
12
|
+
export default PortCoordinateTracker;
|
|
@@ -46,10 +46,11 @@
|
|
|
46
46
|
ProximityConnectHelper,
|
|
47
47
|
type ProximityEdgeCandidate
|
|
48
48
|
} from '../helpers/proximityConnect.js';
|
|
49
|
+
import PortCoordinateTracker from './PortCoordinateTracker.svelte';
|
|
50
|
+
import { getPortCoordinateSnapshot } from '../stores/portCoordinateStore.js';
|
|
49
51
|
|
|
50
52
|
interface Props {
|
|
51
53
|
nodes?: NodeMetadata[];
|
|
52
|
-
// workflow?: Workflow; // Removed - use global store directly
|
|
53
54
|
endpointConfig?: EndpointConfig;
|
|
54
55
|
height?: string | number;
|
|
55
56
|
width?: string | number;
|
|
@@ -76,6 +77,10 @@
|
|
|
76
77
|
// Proximity connect state
|
|
77
78
|
let currentProximityCandidates = $state<ProximityEdgeCandidate[]>([]);
|
|
78
79
|
|
|
80
|
+
// Port coordinate tracker state
|
|
81
|
+
let portCoordNodeToUpdate = $state<WorkflowNodeType | null>(null);
|
|
82
|
+
let portCoordRebuildTrigger = $state(0);
|
|
83
|
+
|
|
79
84
|
// Track the workflow ID we're currently editing to detect workflow switches
|
|
80
85
|
let currentWorkflowId: string | null = null;
|
|
81
86
|
|
|
@@ -178,6 +183,14 @@
|
|
|
178
183
|
);
|
|
179
184
|
flowEdges = styledEdges;
|
|
180
185
|
|
|
186
|
+
// Trigger port coordinate rebuild after workflow load
|
|
187
|
+
// (PortCoordinateTracker will wait for SvelteFlow to render before reading handleBounds)
|
|
188
|
+
// Note: Using Date.now() instead of ++ to avoid reading the old value,
|
|
189
|
+
// which would make this effect depend on portCoordRebuildTrigger and loop.
|
|
190
|
+
if ($editorSettings.proximityConnect) {
|
|
191
|
+
portCoordRebuildTrigger = Date.now();
|
|
192
|
+
}
|
|
193
|
+
|
|
181
194
|
// Only load execution info if we have a pipelineId (pipeline status mode)
|
|
182
195
|
// and if the workflow or pipeline has changed
|
|
183
196
|
const workflowChanged = currentWorkflow.id !== previousWorkflowId;
|
|
@@ -347,7 +360,8 @@
|
|
|
347
360
|
|
|
348
361
|
/**
|
|
349
362
|
* Handle node drag - compute proximity connect preview edges
|
|
350
|
-
* Called continuously during drag if proximity connect is enabled
|
|
363
|
+
* Called continuously during drag if proximity connect is enabled.
|
|
364
|
+
* Uses port-to-port distance via the port coordinate store.
|
|
351
365
|
*/
|
|
352
366
|
function handleNodeDrag({
|
|
353
367
|
targetNode
|
|
@@ -361,19 +375,31 @@
|
|
|
361
375
|
flowEdges = ProximityConnectHelper.removePreviewEdges(flowEdges);
|
|
362
376
|
currentProximityCandidates = [];
|
|
363
377
|
}
|
|
378
|
+
portCoordNodeToUpdate = null;
|
|
364
379
|
return;
|
|
365
380
|
}
|
|
366
381
|
|
|
382
|
+
// Update the dragged node's port coordinates (position changed during drag)
|
|
383
|
+
portCoordNodeToUpdate = targetNode;
|
|
384
|
+
|
|
367
385
|
// Remove previous preview edges
|
|
368
386
|
const baseEdges = ProximityConnectHelper.removePreviewEdges(flowEdges);
|
|
369
387
|
|
|
370
|
-
// Find the best compatible edge
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
388
|
+
// Find the best compatible edge using port-to-port distance
|
|
389
|
+
const portCoordinates = getPortCoordinateSnapshot();
|
|
390
|
+
const candidates = portCoordinates.size > 0
|
|
391
|
+
? ProximityConnectHelper.findCompatibleEdgesByPortCoordinates(
|
|
392
|
+
targetNode.id,
|
|
393
|
+
portCoordinates,
|
|
394
|
+
baseEdges,
|
|
395
|
+
$editorSettings.proximityConnectDistance
|
|
396
|
+
)
|
|
397
|
+
: ProximityConnectHelper.findCompatibleEdges(
|
|
398
|
+
targetNode,
|
|
399
|
+
flowNodes,
|
|
400
|
+
baseEdges,
|
|
401
|
+
$editorSettings.proximityConnectDistance
|
|
402
|
+
);
|
|
377
403
|
|
|
378
404
|
// Create preview edges
|
|
379
405
|
const previews = ProximityConnectHelper.createPreviewEdges(candidates);
|
|
@@ -391,6 +417,7 @@
|
|
|
391
417
|
*/
|
|
392
418
|
function handleNodeDragStop(): void {
|
|
393
419
|
isDraggingNode = false;
|
|
420
|
+
portCoordNodeToUpdate = null;
|
|
394
421
|
|
|
395
422
|
// Finalize proximity connect if there are candidates
|
|
396
423
|
if ($editorSettings.proximityConnect && currentProximityCandidates.length > 0) {
|
|
@@ -699,6 +726,13 @@
|
|
|
699
726
|
<!-- EdgeRefresher component - handles updateNodeInternals calls -->
|
|
700
727
|
<EdgeRefresher {nodeIdToRefresh} onRefreshComplete={handleEdgeRefreshComplete} />
|
|
701
728
|
|
|
729
|
+
<!-- Port Coordinate Tracker - maintains port positions for proximity connect -->
|
|
730
|
+
<PortCoordinateTracker
|
|
731
|
+
nodeToUpdate={portCoordNodeToUpdate}
|
|
732
|
+
rebuildTrigger={portCoordRebuildTrigger}
|
|
733
|
+
nodes={flowNodes}
|
|
734
|
+
/>
|
|
735
|
+
|
|
702
736
|
<div class="flowdrop-workflow-editor">
|
|
703
737
|
<!-- Main Editor Area -->
|
|
704
738
|
<div class="flowdrop-workflow-editor__main">
|
|
@@ -42,8 +42,6 @@
|
|
|
42
42
|
return instanceOverride ?? typeDefault;
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
// Removed local config state - now using global ConfigSidebar
|
|
46
|
-
|
|
47
45
|
// Prioritize metadata icon over config icon for simple nodes (metadata is the node definition)
|
|
48
46
|
let nodeIcon = $derived(
|
|
49
47
|
(props.data.metadata?.icon as string) || (props.data.config?.icon as string) || 'mdi:square'
|
|
@@ -265,8 +263,6 @@
|
|
|
265
263
|
/>
|
|
266
264
|
{/each}
|
|
267
265
|
|
|
268
|
-
<!-- ConfigSidebar removed - now using global ConfigSidebar in WorkflowEditor -->
|
|
269
|
-
|
|
270
266
|
<style>
|
|
271
267
|
.flowdrop-simple-node {
|
|
272
268
|
position: relative;
|
|
@@ -379,8 +375,6 @@
|
|
|
379
375
|
color: var(--fd-node-icon);
|
|
380
376
|
}
|
|
381
377
|
|
|
382
|
-
/* Label styling removed - now using header title */
|
|
383
|
-
|
|
384
378
|
.flowdrop-simple-node__processing {
|
|
385
379
|
position: absolute;
|
|
386
380
|
top: 4px;
|
|
@@ -43,8 +43,6 @@
|
|
|
43
43
|
return instanceOverride ?? typeDefault;
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
-
// Removed local config state - now using global ConfigSidebar
|
|
47
|
-
|
|
48
46
|
/**
|
|
49
47
|
* Get icon using the same resolution as WorkflowNode
|
|
50
48
|
* Uses getNodeIcon utility with category fallback
|
|
@@ -333,8 +331,6 @@
|
|
|
333
331
|
color: var(--fd-node-icon);
|
|
334
332
|
}
|
|
335
333
|
|
|
336
|
-
/* Label styling removed - now using header title */
|
|
337
|
-
|
|
338
334
|
.flowdrop-square-node__processing {
|
|
339
335
|
position: absolute;
|
|
340
336
|
top: 4px;
|
|
@@ -124,11 +124,6 @@
|
|
|
124
124
|
allOutputPorts.filter((port) => isPortVisible(port, 'output'))
|
|
125
125
|
);
|
|
126
126
|
|
|
127
|
-
/**
|
|
128
|
-
* Handle configuration value changes - now handled by global ConfigSidebar
|
|
129
|
-
*/
|
|
130
|
-
// Removed local config handling - now using global ConfigSidebar
|
|
131
|
-
|
|
132
127
|
/**
|
|
133
128
|
* Handle node click - only handle selection, no config opening
|
|
134
129
|
*/
|
|
@@ -201,7 +196,6 @@
|
|
|
201
196
|
|
|
202
197
|
<!-- Status Indicators -->
|
|
203
198
|
<div class="flowdrop-flex flowdrop-gap--2 flowdrop-items--center">
|
|
204
|
-
<!-- Status indicators removed - using outer NodeStatusOverlay instead -->
|
|
205
199
|
</div>
|
|
206
200
|
</div>
|
|
207
201
|
<!-- Node Description - line-height 20px so header grows in steps of 10 -->
|
|
@@ -326,8 +320,6 @@
|
|
|
326
320
|
</button>
|
|
327
321
|
</div>
|
|
328
322
|
|
|
329
|
-
<!-- ConfigSidebar removed - now using global ConfigSidebar in WorkflowEditor -->
|
|
330
|
-
|
|
331
323
|
<style>
|
|
332
324
|
.flowdrop-workflow-node {
|
|
333
325
|
position: relative;
|
|
@@ -436,8 +428,6 @@
|
|
|
436
428
|
line-height: 1;
|
|
437
429
|
}
|
|
438
430
|
|
|
439
|
-
/* Status indicator styles removed - using outer NodeStatusOverlay instead */
|
|
440
|
-
|
|
441
431
|
@keyframes pulse {
|
|
442
432
|
0%,
|
|
443
433
|
100% {
|
package/dist/editor/index.d.ts
CHANGED
|
@@ -64,6 +64,7 @@ export { mountWorkflowEditor, mountFlowDropApp, unmountFlowDropApp } from '../sv
|
|
|
64
64
|
export { nodeComponentRegistry, createNamespacedType, parseNamespacedType, BUILTIN_NODE_COMPONENTS, BUILTIN_NODE_TYPES, FLOWDROP_SOURCE, registerBuiltinNodes, areBuiltinsRegistered, isBuiltinType, getBuiltinTypes, resolveBuiltinAlias, registerFlowDropPlugin, unregisterFlowDropPlugin, registerCustomNode, createPlugin, isValidNamespace, getRegisteredPlugins, getPluginNodeCount } from '../registry/index.js';
|
|
65
65
|
export { EdgeStylingHelper, NodeOperationsHelper, WorkflowOperationsHelper, ConfigurationHelper } from '../helpers/workflowEditorHelper.js';
|
|
66
66
|
export { workflowStore, workflowActions, workflowId, workflowName, workflowNodes, workflowEdges, workflowMetadata, workflowChanged, workflowValidation, workflowMetadataChanged, connectedHandles, isDirtyStore, isDirty, markAsSaved, getWorkflow as getWorkflowFromStore, setOnDirtyStateChange, setOnWorkflowChange, setHistoryEnabled, isHistoryEnabled, setRestoringFromHistory } from '../stores/workflowStore.js';
|
|
67
|
+
export { portCoordinateStore, rebuildAllPortCoordinates, updateNodePortCoordinates, removeNodePortCoordinates, getPortCoordinate, getNodePortCoordinates, getPortCoordinateSnapshot } from '../stores/portCoordinateStore.js';
|
|
67
68
|
export { historyStateStore, canUndo, canRedo, historyActions, setOnRestoreCallback, historyService, HistoryService } from '../stores/historyStore.js';
|
|
68
69
|
export type { HistoryEntry, HistoryState, PushOptions } from '../stores/historyStore.js';
|
|
69
70
|
export * from '../services/api.js';
|
package/dist/editor/index.js
CHANGED
|
@@ -101,6 +101,8 @@ export { workflowStore, workflowActions, workflowId, workflowName, workflowNodes
|
|
|
101
101
|
isDirtyStore, isDirty, markAsSaved, getWorkflow as getWorkflowFromStore, setOnDirtyStateChange, setOnWorkflowChange,
|
|
102
102
|
// History control
|
|
103
103
|
setHistoryEnabled, isHistoryEnabled, setRestoringFromHistory } from '../stores/workflowStore.js';
|
|
104
|
+
// Port Coordinate Store
|
|
105
|
+
export { portCoordinateStore, rebuildAllPortCoordinates, updateNodePortCoordinates, removeNodePortCoordinates, getPortCoordinate, getNodePortCoordinates, getPortCoordinateSnapshot } from '../stores/portCoordinateStore.js';
|
|
104
106
|
// History Store and Service
|
|
105
107
|
export { historyStateStore, canUndo, canRedo, historyActions, setOnRestoreCallback, historyService, HistoryService } from '../stores/historyStore.js';
|
|
106
108
|
// ============================================================================
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* When a node is dragged near another node, this helper finds the best
|
|
6
6
|
* compatible port pair and creates a preview/permanent edge.
|
|
7
7
|
*/
|
|
8
|
-
import type { WorkflowNode as WorkflowNodeType, WorkflowEdge, NodePort } from '../types/index.js';
|
|
8
|
+
import type { WorkflowNode as WorkflowNodeType, WorkflowEdge, NodePort, PortCoordinateMap } from '../types/index.js';
|
|
9
9
|
/** A candidate proximity edge before it is finalized */
|
|
10
10
|
export interface ProximityEdgeCandidate {
|
|
11
11
|
id: string;
|
|
@@ -26,7 +26,7 @@ export declare class ProximityConnectHelper {
|
|
|
26
26
|
*/
|
|
27
27
|
static buildHandleId(nodeId: string, direction: 'input' | 'output', portId: string): string;
|
|
28
28
|
/**
|
|
29
|
-
* Calculate center-to-center
|
|
29
|
+
* Calculate center-to-center distance between two nodes.
|
|
30
30
|
*/
|
|
31
31
|
static getNodeDistance(nodeA: {
|
|
32
32
|
position: {
|
|
@@ -51,14 +51,30 @@ export declare class ProximityConnectHelper {
|
|
|
51
51
|
* Find the single best compatible edge between a dragged node and nearby nodes.
|
|
52
52
|
*
|
|
53
53
|
* Algorithm:
|
|
54
|
-
* 1. Find the closest node within minDistance
|
|
54
|
+
* 1. Find the closest node within minDistance (edge-to-edge)
|
|
55
55
|
* 2. Check both directions (dragged->nearby and nearby->dragged)
|
|
56
56
|
* 3. Return the first exact-type match, or first compatible match
|
|
57
|
-
* 4. Skip pairs where an edge already exists
|
|
57
|
+
* 4. Skip pairs where an edge already exists
|
|
58
58
|
*
|
|
59
59
|
* @returns Array with at most ONE ProximityEdgeCandidate
|
|
60
60
|
*/
|
|
61
61
|
static findCompatibleEdges(draggedNode: WorkflowNodeType, allNodes: WorkflowNodeType[], existingEdges: WorkflowEdge[], minDistance: number): ProximityEdgeCandidate[];
|
|
62
|
+
/**
|
|
63
|
+
* Find the single best compatible edge using port-to-port distance.
|
|
64
|
+
*
|
|
65
|
+
* Unlike findCompatibleEdges() which uses node center distance,
|
|
66
|
+
* this method compares actual handle positions from the port coordinate store.
|
|
67
|
+
* This is more accurate for large nodes or nodes with many ports.
|
|
68
|
+
*
|
|
69
|
+
* Algorithm:
|
|
70
|
+
* 1. Partition ports by owner (dragged vs other) and direction (input vs output)
|
|
71
|
+
* 2. Group other-node ports by dataType for O(1) lookup of compatible groups
|
|
72
|
+
* 3. For each dragged port, only iterate compatible dataType groups
|
|
73
|
+
* 4. Return the closest compatible pair (exact type match preferred)
|
|
74
|
+
*
|
|
75
|
+
* @returns Array with at most ONE ProximityEdgeCandidate
|
|
76
|
+
*/
|
|
77
|
+
static findCompatibleEdgesByPortCoordinates(draggedNodeId: string, portCoordinates: PortCoordinateMap, existingEdges: WorkflowEdge[], maxDistance: number): ProximityEdgeCandidate[];
|
|
62
78
|
/**
|
|
63
79
|
* Convert candidates to temporary (preview) WorkflowEdge objects with dashed styling.
|
|
64
80
|
*/
|
|
@@ -48,31 +48,32 @@ export class ProximityConnectHelper {
|
|
|
48
48
|
return `${nodeId}-${direction}-${portId}`;
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
|
-
* Calculate center-to-center
|
|
51
|
+
* Calculate center-to-center distance between two nodes.
|
|
52
52
|
*/
|
|
53
53
|
static getNodeDistance(nodeA, nodeB) {
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
54
|
+
const aCenterX = nodeA.position.x + (nodeA.measured?.width ?? 0) / 2;
|
|
55
|
+
const aCenterY = nodeA.position.y + (nodeA.measured?.height ?? 0) / 2;
|
|
56
|
+
const bCenterX = nodeB.position.x + (nodeB.measured?.width ?? 0) / 2;
|
|
57
|
+
const bCenterY = nodeB.position.y + (nodeB.measured?.height ?? 0) / 2;
|
|
58
|
+
const dx = aCenterX - bCenterX;
|
|
59
|
+
const dy = aCenterY - bCenterY;
|
|
60
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
59
61
|
}
|
|
60
62
|
/**
|
|
61
63
|
* Find the single best compatible edge between a dragged node and nearby nodes.
|
|
62
64
|
*
|
|
63
65
|
* Algorithm:
|
|
64
|
-
* 1. Find the closest node within minDistance
|
|
66
|
+
* 1. Find the closest node within minDistance (edge-to-edge)
|
|
65
67
|
* 2. Check both directions (dragged->nearby and nearby->dragged)
|
|
66
68
|
* 3. Return the first exact-type match, or first compatible match
|
|
67
|
-
* 4. Skip pairs where an edge already exists
|
|
69
|
+
* 4. Skip pairs where an edge already exists
|
|
68
70
|
*
|
|
69
71
|
* @returns Array with at most ONE ProximityEdgeCandidate
|
|
70
72
|
*/
|
|
71
73
|
static findCompatibleEdges(draggedNode, allNodes, existingEdges, minDistance) {
|
|
72
74
|
const checker = getPortCompatibilityChecker();
|
|
73
|
-
// Build lookup
|
|
75
|
+
// Build lookup set for O(1) duplicate checks
|
|
74
76
|
const existingEdgeSet = new Set(existingEdges.map((e) => `${e.source}:${e.sourceHandle}->${e.target}:${e.targetHandle}`));
|
|
75
|
-
const connectedTargetHandles = new Set(existingEdges.map((e) => `${e.target}:${e.targetHandle}`));
|
|
76
77
|
// Find the closest node within distance
|
|
77
78
|
let closestNode = null;
|
|
78
79
|
let closestDistance = Infinity;
|
|
@@ -102,11 +103,8 @@ export class ProximityConnectHelper {
|
|
|
102
103
|
const sourceHandle = this.buildHandleId(draggedNode.id, 'output', outPort.id);
|
|
103
104
|
const targetHandle = this.buildHandleId(closestNode.id, 'input', inPort.id);
|
|
104
105
|
const edgeKey = `${draggedNode.id}:${sourceHandle}->${closestNode.id}:${targetHandle}`;
|
|
105
|
-
const targetHandleKey = `${closestNode.id}:${targetHandle}`;
|
|
106
106
|
if (existingEdgeSet.has(edgeKey))
|
|
107
107
|
continue;
|
|
108
|
-
if (connectedTargetHandles.has(targetHandleKey))
|
|
109
|
-
continue;
|
|
110
108
|
const candidate = {
|
|
111
109
|
id: `proximity-${uuidv4()}`,
|
|
112
110
|
source: draggedNode.id,
|
|
@@ -140,11 +138,8 @@ export class ProximityConnectHelper {
|
|
|
140
138
|
const sourceHandle = this.buildHandleId(closestNode.id, 'output', outPort.id);
|
|
141
139
|
const targetHandle = this.buildHandleId(draggedNode.id, 'input', inPort.id);
|
|
142
140
|
const edgeKey = `${closestNode.id}:${sourceHandle}->${draggedNode.id}:${targetHandle}`;
|
|
143
|
-
const targetHandleKey = `${draggedNode.id}:${targetHandle}`;
|
|
144
141
|
if (existingEdgeSet.has(edgeKey))
|
|
145
142
|
continue;
|
|
146
|
-
if (connectedTargetHandles.has(targetHandleKey))
|
|
147
|
-
continue;
|
|
148
143
|
const candidate = {
|
|
149
144
|
id: `proximity-${uuidv4()}`,
|
|
150
145
|
source: closestNode.id,
|
|
@@ -172,6 +167,101 @@ export class ProximityConnectHelper {
|
|
|
172
167
|
const best = exactMatch ?? compatibleMatch;
|
|
173
168
|
return best ? [best] : [];
|
|
174
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Find the single best compatible edge using port-to-port distance.
|
|
172
|
+
*
|
|
173
|
+
* Unlike findCompatibleEdges() which uses node center distance,
|
|
174
|
+
* this method compares actual handle positions from the port coordinate store.
|
|
175
|
+
* This is more accurate for large nodes or nodes with many ports.
|
|
176
|
+
*
|
|
177
|
+
* Algorithm:
|
|
178
|
+
* 1. Partition ports by owner (dragged vs other) and direction (input vs output)
|
|
179
|
+
* 2. Group other-node ports by dataType for O(1) lookup of compatible groups
|
|
180
|
+
* 3. For each dragged port, only iterate compatible dataType groups
|
|
181
|
+
* 4. Return the closest compatible pair (exact type match preferred)
|
|
182
|
+
*
|
|
183
|
+
* @returns Array with at most ONE ProximityEdgeCandidate
|
|
184
|
+
*/
|
|
185
|
+
static findCompatibleEdgesByPortCoordinates(draggedNodeId, portCoordinates, existingEdges, maxDistance) {
|
|
186
|
+
const checker = getPortCompatibilityChecker();
|
|
187
|
+
// Build lookup set for O(1) duplicate checks
|
|
188
|
+
const existingEdgeSet = new Set(existingEdges.map((e) => `${e.source}:${e.sourceHandle}->${e.target}:${e.targetHandle}`));
|
|
189
|
+
// Partition ports by owner and direction, group other-node ports by dataType
|
|
190
|
+
const draggedOutputs = [];
|
|
191
|
+
const draggedInputs = [];
|
|
192
|
+
const otherInputsByType = new Map();
|
|
193
|
+
const otherOutputsByType = new Map();
|
|
194
|
+
for (const coord of portCoordinates.values()) {
|
|
195
|
+
if (coord.nodeId === draggedNodeId) {
|
|
196
|
+
if (coord.direction === 'output')
|
|
197
|
+
draggedOutputs.push(coord);
|
|
198
|
+
else
|
|
199
|
+
draggedInputs.push(coord);
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
const groupMap = coord.direction === 'input' ? otherInputsByType : otherOutputsByType;
|
|
203
|
+
let group = groupMap.get(coord.dataType);
|
|
204
|
+
if (!group) {
|
|
205
|
+
group = [];
|
|
206
|
+
groupMap.set(coord.dataType, group);
|
|
207
|
+
}
|
|
208
|
+
group.push(coord);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
let bestCandidate = null;
|
|
212
|
+
let bestDistance = Infinity;
|
|
213
|
+
let bestIsExact = false;
|
|
214
|
+
const evaluatePair = (sourceCoord, targetCoord) => {
|
|
215
|
+
// Check for existing edge
|
|
216
|
+
const edgeKey = `${sourceCoord.nodeId}:${sourceCoord.handleId}->${targetCoord.nodeId}:${targetCoord.handleId}`;
|
|
217
|
+
if (existingEdgeSet.has(edgeKey))
|
|
218
|
+
return;
|
|
219
|
+
// Calculate port-to-port distance
|
|
220
|
+
const dx = sourceCoord.x - targetCoord.x;
|
|
221
|
+
const dy = sourceCoord.y - targetCoord.y;
|
|
222
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
223
|
+
if (dist > maxDistance)
|
|
224
|
+
return;
|
|
225
|
+
const isExact = sourceCoord.dataType === targetCoord.dataType;
|
|
226
|
+
// Prefer exact match, then closest distance
|
|
227
|
+
if ((isExact && !bestIsExact) || (isExact === bestIsExact && dist < bestDistance)) {
|
|
228
|
+
bestCandidate = {
|
|
229
|
+
id: `proximity-${uuidv4()}`,
|
|
230
|
+
source: sourceCoord.nodeId,
|
|
231
|
+
target: targetCoord.nodeId,
|
|
232
|
+
sourceHandle: sourceCoord.handleId,
|
|
233
|
+
targetHandle: targetCoord.handleId,
|
|
234
|
+
sourcePortDataType: sourceCoord.dataType,
|
|
235
|
+
targetPortDataType: targetCoord.dataType
|
|
236
|
+
};
|
|
237
|
+
bestDistance = dist;
|
|
238
|
+
bestIsExact = isExact;
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
// Direction A: dragged outputs → other inputs (only compatible types)
|
|
242
|
+
for (const srcPort of draggedOutputs) {
|
|
243
|
+
const compatibleTypes = checker.getCompatibleTypes(srcPort.dataType);
|
|
244
|
+
for (const targetType of compatibleTypes) {
|
|
245
|
+
const targets = otherInputsByType.get(targetType);
|
|
246
|
+
if (!targets)
|
|
247
|
+
continue;
|
|
248
|
+
for (const tgtPort of targets) {
|
|
249
|
+
evaluatePair(srcPort, tgtPort);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// Direction B: other outputs → dragged inputs (only compatible types)
|
|
254
|
+
for (const tgtPort of draggedInputs) {
|
|
255
|
+
for (const [srcType, sources] of otherOutputsByType) {
|
|
256
|
+
if (!checker.areDataTypesCompatible(srcType, tgtPort.dataType))
|
|
257
|
+
continue;
|
|
258
|
+
for (const srcPort of sources) {
|
|
259
|
+
evaluatePair(srcPort, tgtPort);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return bestCandidate ? [bestCandidate] : [];
|
|
264
|
+
}
|
|
175
265
|
/**
|
|
176
266
|
* Convert candidates to temporary (preview) WorkflowEdge objects with dashed styling.
|
|
177
267
|
*/
|
|
@@ -125,6 +125,6 @@ export type { PlaygroundSession, PlaygroundMessage, PlaygroundInputField, Playgr
|
|
|
125
125
|
export { isChatInputNode, CHAT_INPUT_PATTERNS } from '../types/playground.js';
|
|
126
126
|
export type { InterruptType, InterruptStatus, Interrupt, InterruptChoice, InterruptConfig, ConfirmationConfig, ChoiceConfig, TextConfig, FormConfig, InterruptResolution, InterruptApiResponse, InterruptListResponse, InterruptResponse, InterruptMessageMetadata, InterruptPollingConfig } from '../types/interrupt.js';
|
|
127
127
|
export { isInterruptMetadata, extractInterruptMetadata, metadataToInterrupt, defaultInterruptPollingConfig } from '../types/interrupt.js';
|
|
128
|
-
export { interrupts,
|
|
128
|
+
export { interrupts, pendingInterruptIds, pendingInterrupts, pendingInterruptCount, resolvedInterrupts, isAnySubmitting, interruptActions, getInterrupt, isInterruptPending, isInterruptSubmitting, getInterruptError, getInterruptByMessageId } from '../stores/interruptStore.js';
|
|
129
129
|
export { mountPlayground, unmountPlayground, type PlaygroundMountOptions, type MountedPlayground } from './mount.js';
|
|
130
130
|
export { createEndpointConfig, defaultEndpointConfig, buildEndpointUrl, type EndpointConfig } from '../config/endpoints.js';
|
package/dist/playground/index.js
CHANGED
|
@@ -151,7 +151,7 @@ export { isInterruptMetadata, extractInterruptMetadata, metadataToInterrupt, def
|
|
|
151
151
|
// ============================================================================
|
|
152
152
|
export {
|
|
153
153
|
// Core stores
|
|
154
|
-
interrupts,
|
|
154
|
+
interrupts,
|
|
155
155
|
// Derived stores
|
|
156
156
|
pendingInterruptIds, pendingInterrupts, pendingInterruptCount, resolvedInterrupts, isAnySubmitting,
|
|
157
157
|
// Actions
|
|
@@ -46,16 +46,5 @@ export function validatePortConfig(config) {
|
|
|
46
46
|
return false;
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
-
// Check that compatibility rules reference valid data types
|
|
50
|
-
if (config.compatibilityRules) {
|
|
51
|
-
// TODO: Fix type definition for PortCompatibilityRule - sourceType and targetType properties missing
|
|
52
|
-
// const dataTypeIds = new Set(config.dataTypes.map((dt) => dt.id));
|
|
53
|
-
// for (const rule of config.compatibilityRules) {
|
|
54
|
-
// if (!dataTypeIds.has(rule.sourceType) || !dataTypeIds.has(rule.targetType)) {
|
|
55
|
-
// console.warn("⚠️ Compatibility rule references unknown data type:", rule);
|
|
56
|
-
// return false;
|
|
57
|
-
// }
|
|
58
|
-
// }
|
|
59
|
-
}
|
|
60
49
|
return true;
|
|
61
50
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module stores/interruptStore
|
|
8
8
|
*/
|
|
9
|
-
import type { Interrupt
|
|
9
|
+
import type { Interrupt } from '../types/interrupt.js';
|
|
10
10
|
import { type InterruptState, type TransitionResult } from '../types/interruptState.js';
|
|
11
11
|
/**
|
|
12
12
|
* Extended interrupt with state machine
|
|
@@ -40,16 +40,6 @@ export declare const resolvedInterrupts: import("svelte/store").Readable<Interru
|
|
|
40
40
|
* Derived store to check if any interrupt is currently submitting
|
|
41
41
|
*/
|
|
42
42
|
export declare const isAnySubmitting: import("svelte/store").Readable<boolean>;
|
|
43
|
-
/**
|
|
44
|
-
* Legacy derived store for submitting interrupt IDs
|
|
45
|
-
* @deprecated Use interrupt.machineState.status === "submitting" instead
|
|
46
|
-
*/
|
|
47
|
-
export declare const submittingInterrupts: import("svelte/store").Readable<Set<string>>;
|
|
48
|
-
/**
|
|
49
|
-
* Legacy derived store for interrupt errors
|
|
50
|
-
* @deprecated Use interrupt.machineState.error instead
|
|
51
|
-
*/
|
|
52
|
-
export declare const interruptErrors: import("svelte/store").Readable<Map<string, string>>;
|
|
53
43
|
/**
|
|
54
44
|
* Interrupt store actions for modifying state
|
|
55
45
|
*/
|
|
@@ -111,30 +101,18 @@ export declare const interruptActions: {
|
|
|
111
101
|
*/
|
|
112
102
|
resetInterrupt: (interruptId: string) => TransitionResult;
|
|
113
103
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Mark an interrupt as resolved with the user's response (legacy)
|
|
120
|
-
* @deprecated Use startSubmit + submitSuccess instead
|
|
104
|
+
* Mark an interrupt as resolved with the user's response
|
|
105
|
+
*
|
|
106
|
+
* @param interruptId - The interrupt ID
|
|
107
|
+
* @param value - The resolved value
|
|
121
108
|
*/
|
|
122
109
|
resolveInterrupt: (interruptId: string, value: unknown) => void;
|
|
123
110
|
/**
|
|
124
|
-
* Mark an interrupt as cancelled
|
|
125
|
-
*
|
|
111
|
+
* Mark an interrupt as cancelled
|
|
112
|
+
*
|
|
113
|
+
* @param interruptId - The interrupt ID
|
|
126
114
|
*/
|
|
127
115
|
cancelInterrupt: (interruptId: string) => void;
|
|
128
|
-
/**
|
|
129
|
-
* Set submitting state for an interrupt (legacy)
|
|
130
|
-
* @deprecated State is automatically managed by startSubmit/submitSuccess
|
|
131
|
-
*/
|
|
132
|
-
setSubmitting: (interruptId: string, isSubmitting: boolean) => void;
|
|
133
|
-
/**
|
|
134
|
-
* Set error for an interrupt (legacy)
|
|
135
|
-
* @deprecated Use submitFailure() instead
|
|
136
|
-
*/
|
|
137
|
-
setError: (interruptId: string, error: string | null) => void;
|
|
138
116
|
/**
|
|
139
117
|
* Remove an interrupt from the store
|
|
140
118
|
*
|
|
@@ -70,33 +70,6 @@ export const isAnySubmitting = derived(interrupts, ($interrupts) => {
|
|
|
70
70
|
}
|
|
71
71
|
return false;
|
|
72
72
|
});
|
|
73
|
-
/**
|
|
74
|
-
* Legacy derived store for submitting interrupt IDs
|
|
75
|
-
* @deprecated Use interrupt.machineState.status === "submitting" instead
|
|
76
|
-
*/
|
|
77
|
-
export const submittingInterrupts = derived(interrupts, ($interrupts) => {
|
|
78
|
-
const submitting = new Set();
|
|
79
|
-
$interrupts.forEach((interrupt, id) => {
|
|
80
|
-
if (checkIsSubmitting(interrupt.machineState)) {
|
|
81
|
-
submitting.add(id);
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
return submitting;
|
|
85
|
-
});
|
|
86
|
-
/**
|
|
87
|
-
* Legacy derived store for interrupt errors
|
|
88
|
-
* @deprecated Use interrupt.machineState.error instead
|
|
89
|
-
*/
|
|
90
|
-
export const interruptErrors = derived(interrupts, ($interrupts) => {
|
|
91
|
-
const errors = new Map();
|
|
92
|
-
$interrupts.forEach((interrupt, id) => {
|
|
93
|
-
const errorMsg = getErrorMessage(interrupt.machineState);
|
|
94
|
-
if (errorMsg) {
|
|
95
|
-
errors.set(id, errorMsg);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
return errors;
|
|
99
|
-
});
|
|
100
73
|
// =========================================================================
|
|
101
74
|
// State Machine Actions
|
|
102
75
|
// =========================================================================
|
|
@@ -249,43 +222,22 @@ export const interruptActions = {
|
|
|
249
222
|
resetInterrupt: (interruptId) => {
|
|
250
223
|
return applyAction(interruptId, { type: 'RESET' });
|
|
251
224
|
},
|
|
252
|
-
// =========================================================================
|
|
253
|
-
// Legacy Actions (for backward compatibility)
|
|
254
|
-
// =========================================================================
|
|
255
|
-
/**
|
|
256
|
-
* Update an interrupt's status (legacy)
|
|
257
|
-
* @deprecated Use startSubmit/submitSuccess/submitFailure instead
|
|
258
|
-
*/
|
|
259
|
-
updateStatus: (interruptId, status, responseValue) => {
|
|
260
|
-
// Map legacy status to state machine actions
|
|
261
|
-
if (status === 'resolved' && responseValue !== undefined) {
|
|
262
|
-
const submitResult = applyAction(interruptId, { type: 'SUBMIT', value: responseValue });
|
|
263
|
-
if (submitResult.valid) {
|
|
264
|
-
applyAction(interruptId, { type: 'SUCCESS' });
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
else if (status === 'cancelled') {
|
|
268
|
-
const cancelResult = applyAction(interruptId, { type: 'CANCEL' });
|
|
269
|
-
if (cancelResult.valid) {
|
|
270
|
-
applyAction(interruptId, { type: 'SUCCESS' });
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
},
|
|
274
225
|
/**
|
|
275
|
-
* Mark an interrupt as resolved with the user's response
|
|
276
|
-
*
|
|
226
|
+
* Mark an interrupt as resolved with the user's response
|
|
227
|
+
*
|
|
228
|
+
* @param interruptId - The interrupt ID
|
|
229
|
+
* @param value - The resolved value
|
|
277
230
|
*/
|
|
278
231
|
resolveInterrupt: (interruptId, value) => {
|
|
279
|
-
// For backward compatibility, immediately resolve
|
|
280
|
-
// (assumes sync operation or already completed API call)
|
|
281
232
|
const submitResult = applyAction(interruptId, { type: 'SUBMIT', value });
|
|
282
233
|
if (submitResult.valid) {
|
|
283
234
|
applyAction(interruptId, { type: 'SUCCESS' });
|
|
284
235
|
}
|
|
285
236
|
},
|
|
286
237
|
/**
|
|
287
|
-
* Mark an interrupt as cancelled
|
|
288
|
-
*
|
|
238
|
+
* Mark an interrupt as cancelled
|
|
239
|
+
*
|
|
240
|
+
* @param interruptId - The interrupt ID
|
|
289
241
|
*/
|
|
290
242
|
cancelInterrupt: (interruptId) => {
|
|
291
243
|
const cancelResult = applyAction(interruptId, { type: 'CANCEL' });
|
|
@@ -293,27 +245,6 @@ export const interruptActions = {
|
|
|
293
245
|
applyAction(interruptId, { type: 'SUCCESS' });
|
|
294
246
|
}
|
|
295
247
|
},
|
|
296
|
-
/**
|
|
297
|
-
* Set submitting state for an interrupt (legacy)
|
|
298
|
-
* @deprecated State is automatically managed by startSubmit/submitSuccess
|
|
299
|
-
*/
|
|
300
|
-
setSubmitting: (interruptId, isSubmitting) => {
|
|
301
|
-
// This is now a no-op - state is managed by the state machine
|
|
302
|
-
// Kept for backward compatibility
|
|
303
|
-
if (isSubmitting) {
|
|
304
|
-
console.warn('[InterruptStore] setSubmitting(true) is deprecated. Use startSubmit() instead.');
|
|
305
|
-
}
|
|
306
|
-
},
|
|
307
|
-
/**
|
|
308
|
-
* Set error for an interrupt (legacy)
|
|
309
|
-
* @deprecated Use submitFailure() instead
|
|
310
|
-
*/
|
|
311
|
-
setError: (interruptId, error) => {
|
|
312
|
-
if (error) {
|
|
313
|
-
applyAction(interruptId, { type: 'FAILURE', error });
|
|
314
|
-
}
|
|
315
|
-
// Clearing error is not directly supported - use retry or reset
|
|
316
|
-
},
|
|
317
248
|
/**
|
|
318
249
|
* Remove an interrupt from the store
|
|
319
250
|
*
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port Coordinate Store
|
|
3
|
+
*
|
|
4
|
+
* General-purpose store that maintains absolute canvas-space coordinates
|
|
5
|
+
* for all port handles in the workflow. Built from SvelteFlow's internal
|
|
6
|
+
* handle bounds data combined with FlowDrop port metadata.
|
|
7
|
+
*
|
|
8
|
+
* Primary consumers:
|
|
9
|
+
* - Proximity connect (port-to-port distance instead of node center distance)
|
|
10
|
+
*
|
|
11
|
+
* Coordinates are derived from SvelteFlow's InternalNode.internals.handleBounds
|
|
12
|
+
* which SvelteFlow already maintains for all node types. This avoids replicating
|
|
13
|
+
* CSS positioning logic and stays automatically accurate.
|
|
14
|
+
*/
|
|
15
|
+
import type { WorkflowNode as WorkflowNodeType, PortCoordinate, PortCoordinateMap } from '../types/index.js';
|
|
16
|
+
import type { InternalNode } from '@xyflow/svelte';
|
|
17
|
+
/** Store holding all port absolute coordinates, keyed by handleId */
|
|
18
|
+
export declare const portCoordinateStore: import("svelte/store").Writable<PortCoordinateMap>;
|
|
19
|
+
/**
|
|
20
|
+
* Rebuild coordinates for ALL nodes from SvelteFlow internals.
|
|
21
|
+
* Call on initial workflow load (after render) and after bulk changes.
|
|
22
|
+
*
|
|
23
|
+
* @param nodes - All workflow nodes
|
|
24
|
+
* @param getInternalNode - SvelteFlow's getInternalNode function
|
|
25
|
+
*/
|
|
26
|
+
export declare function rebuildAllPortCoordinates(nodes: WorkflowNodeType[], getInternalNode: (id: string) => InternalNode | undefined): void;
|
|
27
|
+
/**
|
|
28
|
+
* Update coordinates for a single node (efficient for drag updates).
|
|
29
|
+
* Only recomputes ports for the specified node.
|
|
30
|
+
*
|
|
31
|
+
* @param node - The workflow node to update
|
|
32
|
+
* @param getInternalNode - SvelteFlow's getInternalNode function
|
|
33
|
+
*/
|
|
34
|
+
export declare function updateNodePortCoordinates(node: WorkflowNodeType, getInternalNode: (id: string) => InternalNode | undefined): void;
|
|
35
|
+
/**
|
|
36
|
+
* Remove all coordinates for a node (on node delete).
|
|
37
|
+
*
|
|
38
|
+
* @param nodeId - ID of the node to remove
|
|
39
|
+
*/
|
|
40
|
+
export declare function removeNodePortCoordinates(nodeId: string): void;
|
|
41
|
+
/**
|
|
42
|
+
* Get coordinates for a specific handle.
|
|
43
|
+
*
|
|
44
|
+
* @param handleId - The handle ID to look up
|
|
45
|
+
* @returns The port coordinate or undefined if not found
|
|
46
|
+
*/
|
|
47
|
+
export declare function getPortCoordinate(handleId: string): PortCoordinate | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Get all coordinates for a specific node.
|
|
50
|
+
*
|
|
51
|
+
* @param nodeId - The node ID to look up
|
|
52
|
+
* @returns Array of port coordinates for the node
|
|
53
|
+
*/
|
|
54
|
+
export declare function getNodePortCoordinates(nodeId: string): PortCoordinate[];
|
|
55
|
+
/**
|
|
56
|
+
* Get the current snapshot of all port coordinates (non-reactive).
|
|
57
|
+
*
|
|
58
|
+
* @returns Current port coordinate map
|
|
59
|
+
*/
|
|
60
|
+
export declare function getPortCoordinateSnapshot(): PortCoordinateMap;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port Coordinate Store
|
|
3
|
+
*
|
|
4
|
+
* General-purpose store that maintains absolute canvas-space coordinates
|
|
5
|
+
* for all port handles in the workflow. Built from SvelteFlow's internal
|
|
6
|
+
* handle bounds data combined with FlowDrop port metadata.
|
|
7
|
+
*
|
|
8
|
+
* Primary consumers:
|
|
9
|
+
* - Proximity connect (port-to-port distance instead of node center distance)
|
|
10
|
+
*
|
|
11
|
+
* Coordinates are derived from SvelteFlow's InternalNode.internals.handleBounds
|
|
12
|
+
* which SvelteFlow already maintains for all node types. This avoids replicating
|
|
13
|
+
* CSS positioning logic and stays automatically accurate.
|
|
14
|
+
*/
|
|
15
|
+
import { writable, get } from 'svelte/store';
|
|
16
|
+
import { ProximityConnectHelper } from '../helpers/proximityConnect.js';
|
|
17
|
+
/** Store holding all port absolute coordinates, keyed by handleId */
|
|
18
|
+
export const portCoordinateStore = writable(new Map());
|
|
19
|
+
/**
|
|
20
|
+
* Parse a handle ID to extract nodeId, direction, and portId.
|
|
21
|
+
* Handle ID format: ${nodeId}-${direction}-${portId}
|
|
22
|
+
*
|
|
23
|
+
* Note: nodeId itself can contain hyphens, so we match direction
|
|
24
|
+
* from the known suffixes (-input- or -output-).
|
|
25
|
+
*/
|
|
26
|
+
function parseHandleId(handleId) {
|
|
27
|
+
// Match the last occurrence of -input- or -output- to handle nodeIds with hyphens
|
|
28
|
+
const inputMatch = handleId.match(/^(.+)-input-(.+)$/);
|
|
29
|
+
if (inputMatch) {
|
|
30
|
+
return { nodeId: inputMatch[1], direction: 'input', portId: inputMatch[2] };
|
|
31
|
+
}
|
|
32
|
+
const outputMatch = handleId.match(/^(.+)-output-(.+)$/);
|
|
33
|
+
if (outputMatch) {
|
|
34
|
+
return { nodeId: outputMatch[1], direction: 'output', portId: outputMatch[2] };
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Build a dataType lookup map from a node's ports.
|
|
40
|
+
* Maps portId → dataType for quick lookup when processing handle bounds.
|
|
41
|
+
*/
|
|
42
|
+
function buildPortDataTypeLookup(node) {
|
|
43
|
+
const lookup = new Map();
|
|
44
|
+
const inputs = ProximityConnectHelper.getAllPorts(node, 'input');
|
|
45
|
+
for (const port of inputs) {
|
|
46
|
+
lookup.set(`input-${port.id}`, port.dataType);
|
|
47
|
+
}
|
|
48
|
+
const outputs = ProximityConnectHelper.getAllPorts(node, 'output');
|
|
49
|
+
for (const port of outputs) {
|
|
50
|
+
lookup.set(`output-${port.id}`, port.dataType);
|
|
51
|
+
}
|
|
52
|
+
return lookup;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Compute port coordinates for a single node from SvelteFlow internal data.
|
|
56
|
+
*
|
|
57
|
+
* @param node - The workflow node
|
|
58
|
+
* @param internalNode - SvelteFlow's internal node with handleBounds
|
|
59
|
+
* @returns Array of PortCoordinate entries for this node
|
|
60
|
+
*/
|
|
61
|
+
function computeNodePortCoordinates(node, internalNode) {
|
|
62
|
+
const handleBounds = internalNode.internals.handleBounds;
|
|
63
|
+
if (!handleBounds)
|
|
64
|
+
return [];
|
|
65
|
+
const posAbs = internalNode.internals.positionAbsolute;
|
|
66
|
+
const dataTypeLookup = buildPortDataTypeLookup(node);
|
|
67
|
+
const coordinates = [];
|
|
68
|
+
const allHandles = [
|
|
69
|
+
...(handleBounds.source ?? []),
|
|
70
|
+
...(handleBounds.target ?? [])
|
|
71
|
+
];
|
|
72
|
+
for (const handle of allHandles) {
|
|
73
|
+
if (!handle.id)
|
|
74
|
+
continue;
|
|
75
|
+
const parsed = parseHandleId(handle.id);
|
|
76
|
+
if (!parsed)
|
|
77
|
+
continue;
|
|
78
|
+
const lookupKey = `${parsed.direction}-${parsed.portId}`;
|
|
79
|
+
const dataType = dataTypeLookup.get(lookupKey);
|
|
80
|
+
if (!dataType)
|
|
81
|
+
continue;
|
|
82
|
+
coordinates.push({
|
|
83
|
+
x: posAbs.x + handle.x + handle.width / 2,
|
|
84
|
+
y: posAbs.y + handle.y + handle.height / 2,
|
|
85
|
+
handleId: handle.id,
|
|
86
|
+
nodeId: parsed.nodeId,
|
|
87
|
+
direction: parsed.direction,
|
|
88
|
+
dataType
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return coordinates;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Rebuild coordinates for ALL nodes from SvelteFlow internals.
|
|
95
|
+
* Call on initial workflow load (after render) and after bulk changes.
|
|
96
|
+
*
|
|
97
|
+
* @param nodes - All workflow nodes
|
|
98
|
+
* @param getInternalNode - SvelteFlow's getInternalNode function
|
|
99
|
+
*/
|
|
100
|
+
export function rebuildAllPortCoordinates(nodes, getInternalNode) {
|
|
101
|
+
const map = new Map();
|
|
102
|
+
for (const node of nodes) {
|
|
103
|
+
const internalNode = getInternalNode(node.id);
|
|
104
|
+
if (!internalNode)
|
|
105
|
+
continue;
|
|
106
|
+
const coords = computeNodePortCoordinates(node, internalNode);
|
|
107
|
+
for (const coord of coords) {
|
|
108
|
+
map.set(coord.handleId, coord);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
portCoordinateStore.set(map);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Update coordinates for a single node (efficient for drag updates).
|
|
115
|
+
* Only recomputes ports for the specified node.
|
|
116
|
+
*
|
|
117
|
+
* @param node - The workflow node to update
|
|
118
|
+
* @param getInternalNode - SvelteFlow's getInternalNode function
|
|
119
|
+
*/
|
|
120
|
+
export function updateNodePortCoordinates(node, getInternalNode) {
|
|
121
|
+
const internalNode = getInternalNode(node.id);
|
|
122
|
+
if (!internalNode)
|
|
123
|
+
return;
|
|
124
|
+
portCoordinateStore.update((map) => {
|
|
125
|
+
// Remove old entries for this node
|
|
126
|
+
for (const [key, coord] of map) {
|
|
127
|
+
if (coord.nodeId === node.id) {
|
|
128
|
+
map.delete(key);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Add new entries
|
|
132
|
+
const coords = computeNodePortCoordinates(node, internalNode);
|
|
133
|
+
for (const coord of coords) {
|
|
134
|
+
map.set(coord.handleId, coord);
|
|
135
|
+
}
|
|
136
|
+
// Return new reference for reactivity
|
|
137
|
+
return new Map(map);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Remove all coordinates for a node (on node delete).
|
|
142
|
+
*
|
|
143
|
+
* @param nodeId - ID of the node to remove
|
|
144
|
+
*/
|
|
145
|
+
export function removeNodePortCoordinates(nodeId) {
|
|
146
|
+
portCoordinateStore.update((map) => {
|
|
147
|
+
for (const [key, coord] of map) {
|
|
148
|
+
if (coord.nodeId === nodeId) {
|
|
149
|
+
map.delete(key);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return new Map(map);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Get coordinates for a specific handle.
|
|
157
|
+
*
|
|
158
|
+
* @param handleId - The handle ID to look up
|
|
159
|
+
* @returns The port coordinate or undefined if not found
|
|
160
|
+
*/
|
|
161
|
+
export function getPortCoordinate(handleId) {
|
|
162
|
+
return get(portCoordinateStore).get(handleId);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Get all coordinates for a specific node.
|
|
166
|
+
*
|
|
167
|
+
* @param nodeId - The node ID to look up
|
|
168
|
+
* @returns Array of port coordinates for the node
|
|
169
|
+
*/
|
|
170
|
+
export function getNodePortCoordinates(nodeId) {
|
|
171
|
+
const result = [];
|
|
172
|
+
for (const coord of get(portCoordinateStore).values()) {
|
|
173
|
+
if (coord.nodeId === nodeId) {
|
|
174
|
+
result.push(coord);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Get the current snapshot of all port coordinates (non-reactive).
|
|
181
|
+
*
|
|
182
|
+
* @returns Current port coordinate map
|
|
183
|
+
*/
|
|
184
|
+
export function getPortCoordinateSnapshot() {
|
|
185
|
+
return get(portCoordinateStore);
|
|
186
|
+
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -108,6 +108,26 @@ export interface NodePort {
|
|
|
108
108
|
*/
|
|
109
109
|
schema?: OutputSchema | InputSchema;
|
|
110
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Absolute position of a port handle in canvas space.
|
|
113
|
+
* Used by proximity connect and other features that need port positions.
|
|
114
|
+
*/
|
|
115
|
+
export interface PortCoordinate {
|
|
116
|
+
/** Absolute X position in canvas space */
|
|
117
|
+
x: number;
|
|
118
|
+
/** Absolute Y position in canvas space */
|
|
119
|
+
y: number;
|
|
120
|
+
/** Handle ID in format: ${nodeId}-${direction}-${portId} */
|
|
121
|
+
handleId: string;
|
|
122
|
+
/** The node this port belongs to */
|
|
123
|
+
nodeId: string;
|
|
124
|
+
/** Port direction */
|
|
125
|
+
direction: 'input' | 'output';
|
|
126
|
+
/** Port data type for compatibility checks */
|
|
127
|
+
dataType: string;
|
|
128
|
+
}
|
|
129
|
+
/** Map of handle IDs to their absolute canvas coordinates */
|
|
130
|
+
export type PortCoordinateMap = Map<string, PortCoordinate>;
|
|
111
131
|
/**
|
|
112
132
|
* Dynamic port configuration for user-defined inputs/outputs
|
|
113
133
|
* These are defined in the node's config and allow users to create
|