@d34dman/flowdrop 0.0.52 → 0.0.54

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.
Files changed (40) hide show
  1. package/dist/components/App.svelte +9 -6
  2. package/dist/components/ConfigForm.svelte +1 -0
  3. package/dist/components/NodeSidebar.svelte +0 -2
  4. package/dist/components/PortCoordinateTracker.svelte +58 -0
  5. package/dist/components/PortCoordinateTracker.svelte.d.ts +12 -0
  6. package/dist/components/SettingsPanel.svelte +1 -2
  7. package/dist/components/ThemeToggle.svelte +1 -1
  8. package/dist/components/WorkflowEditor.svelte +44 -11
  9. package/dist/components/form/FormAutocomplete.svelte +3 -12
  10. package/dist/components/form/FormField.svelte +1 -1
  11. package/dist/components/form/FormFieldLight.svelte +1 -1
  12. package/dist/components/nodes/SimpleNode.svelte +0 -6
  13. package/dist/components/nodes/SquareNode.svelte +0 -4
  14. package/dist/components/nodes/TerminalNode.svelte +45 -9
  15. package/dist/components/nodes/TerminalNode.svelte.d.ts +2 -1
  16. package/dist/components/nodes/ToolNode.svelte +17 -11
  17. package/dist/components/nodes/WorkflowNode.svelte +0 -10
  18. package/dist/core/index.d.ts +2 -2
  19. package/dist/core/index.js +1 -1
  20. package/dist/editor/index.d.ts +1 -0
  21. package/dist/editor/index.js +2 -0
  22. package/dist/helpers/proximityConnect.d.ts +19 -3
  23. package/dist/helpers/proximityConnect.js +109 -7
  24. package/dist/playground/index.d.ts +1 -1
  25. package/dist/playground/index.js +1 -1
  26. package/dist/services/portConfigApi.js +0 -11
  27. package/dist/settings/index.d.ts +2 -1
  28. package/dist/settings/index.js +2 -1
  29. package/dist/stores/interruptStore.d.ts +8 -30
  30. package/dist/stores/interruptStore.js +7 -76
  31. package/dist/stores/portCoordinateStore.d.ts +60 -0
  32. package/dist/stores/portCoordinateStore.js +186 -0
  33. package/dist/stores/settingsStore.d.ts +44 -2
  34. package/dist/stores/settingsStore.js +37 -15
  35. package/dist/types/index.d.ts +22 -0
  36. package/dist/utils/fetchWithAuth.d.ts +25 -0
  37. package/dist/utils/fetchWithAuth.js +34 -0
  38. package/package.json +1 -1
  39. package/dist/stores/themeStore.d.ts +0 -68
  40. package/dist/stores/themeStore.js +0 -213
@@ -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 Euclidean distance between two nodes.
29
+ * Calculate center-to-center distance between two nodes.
30
30
  */
31
31
  static getNodeDistance(nodeA: {
32
32
  position: {
@@ -51,7 +51,7 @@ 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
57
  * 4. Skip pairs where an edge already exists or input handle is already connected
@@ -59,6 +59,22 @@ export declare class ProximityConnectHelper {
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,20 +48,22 @@ export class ProximityConnectHelper {
48
48
  return `${nodeId}-${direction}-${portId}`;
49
49
  }
50
50
  /**
51
- * Calculate center-to-center Euclidean distance between two nodes.
51
+ * Calculate center-to-center distance between two nodes.
52
52
  */
53
53
  static getNodeDistance(nodeA, nodeB) {
54
- const ax = nodeA.position.x + (nodeA.measured?.width ?? 0) / 2;
55
- const ay = nodeA.position.y + (nodeA.measured?.height ?? 0) / 2;
56
- const bx = nodeB.position.x + (nodeB.measured?.width ?? 0) / 2;
57
- const by = nodeB.position.y + (nodeB.measured?.height ?? 0) / 2;
58
- return Math.sqrt((ax - bx) ** 2 + (ay - by) ** 2);
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
69
  * 4. Skip pairs where an edge already exists or input handle is already connected
@@ -172,6 +174,106 @@ export class ProximityConnectHelper {
172
174
  const best = exactMatch ?? compatibleMatch;
173
175
  return best ? [best] : [];
174
176
  }
177
+ /**
178
+ * Find the single best compatible edge using port-to-port distance.
179
+ *
180
+ * Unlike findCompatibleEdges() which uses node center distance,
181
+ * this method compares actual handle positions from the port coordinate store.
182
+ * This is more accurate for large nodes or nodes with many ports.
183
+ *
184
+ * Algorithm:
185
+ * 1. Partition ports by owner (dragged vs other) and direction (input vs output)
186
+ * 2. Group other-node ports by dataType for O(1) lookup of compatible groups
187
+ * 3. For each dragged port, only iterate compatible dataType groups
188
+ * 4. Return the closest compatible pair (exact type match preferred)
189
+ *
190
+ * @returns Array with at most ONE ProximityEdgeCandidate
191
+ */
192
+ static findCompatibleEdgesByPortCoordinates(draggedNodeId, portCoordinates, existingEdges, maxDistance) {
193
+ const checker = getPortCompatibilityChecker();
194
+ // Build lookup sets for O(1) duplicate/connected checks
195
+ const existingEdgeSet = new Set(existingEdges.map((e) => `${e.source}:${e.sourceHandle}->${e.target}:${e.targetHandle}`));
196
+ const connectedTargetHandles = new Set(existingEdges.map((e) => `${e.target}:${e.targetHandle}`));
197
+ // Partition ports by owner and direction, group other-node ports by dataType
198
+ const draggedOutputs = [];
199
+ const draggedInputs = [];
200
+ const otherInputsByType = new Map();
201
+ const otherOutputsByType = new Map();
202
+ for (const coord of portCoordinates.values()) {
203
+ if (coord.nodeId === draggedNodeId) {
204
+ if (coord.direction === 'output')
205
+ draggedOutputs.push(coord);
206
+ else
207
+ draggedInputs.push(coord);
208
+ }
209
+ else {
210
+ const groupMap = coord.direction === 'input' ? otherInputsByType : otherOutputsByType;
211
+ let group = groupMap.get(coord.dataType);
212
+ if (!group) {
213
+ group = [];
214
+ groupMap.set(coord.dataType, group);
215
+ }
216
+ group.push(coord);
217
+ }
218
+ }
219
+ let bestCandidate = null;
220
+ let bestDistance = Infinity;
221
+ let bestIsExact = false;
222
+ const evaluatePair = (sourceCoord, targetCoord) => {
223
+ // Check for existing edge
224
+ const edgeKey = `${sourceCoord.nodeId}:${sourceCoord.handleId}->${targetCoord.nodeId}:${targetCoord.handleId}`;
225
+ if (existingEdgeSet.has(edgeKey))
226
+ return;
227
+ // Check target handle not already connected (single-input rule)
228
+ const targetHandleKey = `${targetCoord.nodeId}:${targetCoord.handleId}`;
229
+ if (connectedTargetHandles.has(targetHandleKey))
230
+ return;
231
+ // Calculate port-to-port distance
232
+ const dx = sourceCoord.x - targetCoord.x;
233
+ const dy = sourceCoord.y - targetCoord.y;
234
+ const dist = Math.sqrt(dx * dx + dy * dy);
235
+ if (dist > maxDistance)
236
+ return;
237
+ const isExact = sourceCoord.dataType === targetCoord.dataType;
238
+ // Prefer exact match, then closest distance
239
+ if ((isExact && !bestIsExact) || (isExact === bestIsExact && dist < bestDistance)) {
240
+ bestCandidate = {
241
+ id: `proximity-${uuidv4()}`,
242
+ source: sourceCoord.nodeId,
243
+ target: targetCoord.nodeId,
244
+ sourceHandle: sourceCoord.handleId,
245
+ targetHandle: targetCoord.handleId,
246
+ sourcePortDataType: sourceCoord.dataType,
247
+ targetPortDataType: targetCoord.dataType
248
+ };
249
+ bestDistance = dist;
250
+ bestIsExact = isExact;
251
+ }
252
+ };
253
+ // Direction A: dragged outputs → other inputs (only compatible types)
254
+ for (const srcPort of draggedOutputs) {
255
+ const compatibleTypes = checker.getCompatibleTypes(srcPort.dataType);
256
+ for (const targetType of compatibleTypes) {
257
+ const targets = otherInputsByType.get(targetType);
258
+ if (!targets)
259
+ continue;
260
+ for (const tgtPort of targets) {
261
+ evaluatePair(srcPort, tgtPort);
262
+ }
263
+ }
264
+ }
265
+ // Direction B: other outputs → dragged inputs (only compatible types)
266
+ for (const tgtPort of draggedInputs) {
267
+ for (const [srcType, sources] of otherOutputsByType) {
268
+ if (!checker.areDataTypesCompatible(srcType, tgtPort.dataType))
269
+ continue;
270
+ for (const srcPort of sources) {
271
+ evaluatePair(srcPort, tgtPort);
272
+ }
273
+ }
274
+ }
275
+ return bestCandidate ? [bestCandidate] : [];
276
+ }
175
277
  /**
176
278
  * Convert candidates to temporary (preview) WorkflowEdge objects with dashed styling.
177
279
  */
@@ -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, submittingInterrupts, interruptErrors, pendingInterruptIds, pendingInterrupts, pendingInterruptCount, resolvedInterrupts, isAnySubmitting, interruptActions, getInterrupt, isInterruptPending, isInterruptSubmitting, getInterruptError, getInterruptByMessageId } from '../stores/interruptStore.js';
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';
@@ -151,7 +151,7 @@ export { isInterruptMetadata, extractInterruptMetadata, metadataToInterrupt, def
151
151
  // ============================================================================
152
152
  export {
153
153
  // Core stores
154
- interrupts, submittingInterrupts, interruptErrors,
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
  }
@@ -5,7 +5,8 @@
5
5
  * preferences with hybrid persistence (localStorage + optional API sync).
6
6
  *
7
7
  * Theme stores and functions (theme, resolvedTheme, setTheme, toggleTheme,
8
- * cycleTheme, initializeTheme) are exported from `@d34dman/flowdrop/core`.
8
+ * cycleTheme, initializeTheme) are exported from `@d34dman/flowdrop/core`
9
+ * (sourced from settingsStore).
9
10
  *
10
11
  * @module settings
11
12
  *
@@ -5,7 +5,8 @@
5
5
  * preferences with hybrid persistence (localStorage + optional API sync).
6
6
  *
7
7
  * Theme stores and functions (theme, resolvedTheme, setTheme, toggleTheme,
8
- * cycleTheme, initializeTheme) are exported from `@d34dman/flowdrop/core`.
8
+ * cycleTheme, initializeTheme) are exported from `@d34dman/flowdrop/core`
9
+ * (sourced from settingsStore).
9
10
  *
10
11
  * @module settings
11
12
  *
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @module stores/interruptStore
8
8
  */
9
- import type { Interrupt, InterruptStatus } from '../types/interrupt.js';
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
- * Update an interrupt's status (legacy)
115
- * @deprecated Use startSubmit/submitSuccess/submitFailure instead
116
- */
117
- updateStatus: (interruptId: string, status: InterruptStatus, responseValue?: unknown) => void;
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 (legacy)
125
- * @deprecated Use startCancel + submitSuccess instead
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 (legacy)
276
- * @deprecated Use startSubmit + submitSuccess instead
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 (legacy)
288
- * @deprecated Use startCancel + submitSuccess instead
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
+ }