@pyreon/flow 0.5.0

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/src/edges.ts ADDED
@@ -0,0 +1,369 @@
1
+ import type { EdgePathResult, FlowNode, XYPosition } from './types'
2
+ import { Position } from './types'
3
+
4
+ /**
5
+ * Auto-detect the best handle position based on relative node positions.
6
+ * If the node has configured handles, uses those. Otherwise picks the
7
+ * closest edge (top/right/bottom/left) based on direction to the other node.
8
+ */
9
+ export function getSmartHandlePositions(
10
+ sourceNode: FlowNode,
11
+ targetNode: FlowNode,
12
+ ): { sourcePosition: Position; targetPosition: Position } {
13
+ const sw = sourceNode.width ?? 150
14
+ const sh = sourceNode.height ?? 40
15
+ const tw = targetNode.width ?? 150
16
+ const th = targetNode.height ?? 40
17
+
18
+ const dx = targetNode.position.x + tw / 2 - (sourceNode.position.x + sw / 2)
19
+ const dy = targetNode.position.y + th / 2 - (sourceNode.position.y + sh / 2)
20
+
21
+ const sourceHandle = sourceNode.sourceHandles?.[0]
22
+ const targetHandle = targetNode.targetHandles?.[0]
23
+
24
+ const sourcePosition = sourceHandle
25
+ ? sourceHandle.position
26
+ : Math.abs(dx) > Math.abs(dy)
27
+ ? dx > 0
28
+ ? Position.Right
29
+ : Position.Left
30
+ : dy > 0
31
+ ? Position.Bottom
32
+ : Position.Top
33
+
34
+ const targetPosition = targetHandle
35
+ ? targetHandle.position
36
+ : Math.abs(dx) > Math.abs(dy)
37
+ ? dx > 0
38
+ ? Position.Left
39
+ : Position.Right
40
+ : dy > 0
41
+ ? Position.Top
42
+ : Position.Bottom
43
+
44
+ return { sourcePosition, targetPosition }
45
+ }
46
+
47
+ /**
48
+ * Get the center point between source and target positions.
49
+ */
50
+ function getCenter(source: XYPosition, target: XYPosition): XYPosition {
51
+ return {
52
+ x: (source.x + target.x) / 2,
53
+ y: (source.y + target.y) / 2,
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Get the handle position offset for a given position (top/right/bottom/left).
59
+ */
60
+ export function getHandlePosition(
61
+ position: Position,
62
+ nodeX: number,
63
+ nodeY: number,
64
+ nodeWidth: number,
65
+ nodeHeight: number,
66
+ _handleId?: string,
67
+ ): XYPosition {
68
+ switch (position) {
69
+ case Position.Top:
70
+ return { x: nodeX + nodeWidth / 2, y: nodeY }
71
+ case Position.Right:
72
+ return { x: nodeX + nodeWidth, y: nodeY + nodeHeight / 2 }
73
+ case Position.Bottom:
74
+ return { x: nodeX + nodeWidth / 2, y: nodeY + nodeHeight }
75
+ case Position.Left:
76
+ return { x: nodeX, y: nodeY + nodeHeight / 2 }
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Calculate a cubic bezier edge path between two points.
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * const { path, labelX, labelY } = getBezierPath({
86
+ * sourceX: 0, sourceY: 0, sourcePosition: Position.Right,
87
+ * targetX: 200, targetY: 100, targetPosition: Position.Left,
88
+ * })
89
+ * // path = "M0,0 C100,0 100,100 200,100"
90
+ * ```
91
+ */
92
+ export function getBezierPath(params: {
93
+ sourceX: number
94
+ sourceY: number
95
+ sourcePosition?: Position
96
+ targetX: number
97
+ targetY: number
98
+ targetPosition?: Position
99
+ curvature?: number
100
+ }): EdgePathResult {
101
+ const {
102
+ sourceX,
103
+ sourceY,
104
+ sourcePosition = Position.Bottom,
105
+ targetX,
106
+ targetY,
107
+ targetPosition = Position.Top,
108
+ curvature = 0.25,
109
+ } = params
110
+
111
+ const distX = Math.abs(targetX - sourceX)
112
+ const distY = Math.abs(targetY - sourceY)
113
+ const dist = Math.sqrt(distX * distX + distY * distY)
114
+ const offset = dist * curvature
115
+
116
+ let sourceControlX = sourceX
117
+ let sourceControlY = sourceY
118
+ let targetControlX = targetX
119
+ let targetControlY = targetY
120
+
121
+ switch (sourcePosition) {
122
+ case Position.Top:
123
+ sourceControlY = sourceY - offset
124
+ break
125
+ case Position.Bottom:
126
+ sourceControlY = sourceY + offset
127
+ break
128
+ case Position.Left:
129
+ sourceControlX = sourceX - offset
130
+ break
131
+ case Position.Right:
132
+ sourceControlX = sourceX + offset
133
+ break
134
+ }
135
+
136
+ switch (targetPosition) {
137
+ case Position.Top:
138
+ targetControlY = targetY - offset
139
+ break
140
+ case Position.Bottom:
141
+ targetControlY = targetY + offset
142
+ break
143
+ case Position.Left:
144
+ targetControlX = targetX - offset
145
+ break
146
+ case Position.Right:
147
+ targetControlX = targetX + offset
148
+ break
149
+ }
150
+
151
+ const center = getCenter(
152
+ { x: sourceX, y: sourceY },
153
+ { x: targetX, y: targetY },
154
+ )
155
+
156
+ return {
157
+ path: `M${sourceX},${sourceY} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetX},${targetY}`,
158
+ labelX: center.x,
159
+ labelY: center.y,
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Calculate a smoothstep edge path — horizontal/vertical segments with rounded corners.
165
+ */
166
+ export function getSmoothStepPath(params: {
167
+ sourceX: number
168
+ sourceY: number
169
+ sourcePosition?: Position
170
+ targetX: number
171
+ targetY: number
172
+ targetPosition?: Position
173
+ borderRadius?: number
174
+ offset?: number
175
+ }): EdgePathResult {
176
+ const {
177
+ sourceX,
178
+ sourceY,
179
+ sourcePosition = Position.Bottom,
180
+ targetX,
181
+ targetY,
182
+ targetPosition = Position.Top,
183
+ borderRadius = 5,
184
+ offset = 20,
185
+ } = params
186
+
187
+ const isHorizontalSource =
188
+ sourcePosition === Position.Left || sourcePosition === Position.Right
189
+ const isHorizontalTarget =
190
+ targetPosition === Position.Left || targetPosition === Position.Right
191
+
192
+ // Calculate offset points
193
+ const sourceOffsetX =
194
+ sourcePosition === Position.Right
195
+ ? offset
196
+ : sourcePosition === Position.Left
197
+ ? -offset
198
+ : 0
199
+ const sourceOffsetY =
200
+ sourcePosition === Position.Bottom
201
+ ? offset
202
+ : sourcePosition === Position.Top
203
+ ? -offset
204
+ : 0
205
+ const targetOffsetX =
206
+ targetPosition === Position.Right
207
+ ? offset
208
+ : targetPosition === Position.Left
209
+ ? -offset
210
+ : 0
211
+ const targetOffsetY =
212
+ targetPosition === Position.Bottom
213
+ ? offset
214
+ : targetPosition === Position.Top
215
+ ? -offset
216
+ : 0
217
+
218
+ const sX = sourceX + sourceOffsetX
219
+ const sY = sourceY + sourceOffsetY
220
+ const tX = targetX + targetOffsetX
221
+ const tY = targetY + targetOffsetY
222
+
223
+ const center = getCenter(
224
+ { x: sourceX, y: sourceY },
225
+ { x: targetX, y: targetY },
226
+ )
227
+
228
+ // Simple smoothstep: source → midpoint → target with rounded corners
229
+ const midX = (sX + tX) / 2
230
+ const midY = (sY + tY) / 2
231
+ const r = borderRadius
232
+
233
+ let path: string
234
+
235
+ if (isHorizontalSource && !isHorizontalTarget) {
236
+ // Horizontal source → vertical target
237
+ const cornerY = tY
238
+ path = `M${sourceX},${sourceY} L${sX},${sY} L${sX},${cornerY > sY ? cornerY - r : cornerY + r} Q${sX},${cornerY} ${sX + (tX > sX ? r : -r)},${cornerY} L${tX},${cornerY} L${targetX},${targetY}`
239
+ } else if (!isHorizontalSource && isHorizontalTarget) {
240
+ // Vertical source → horizontal target
241
+ const cornerX = tX
242
+ path = `M${sourceX},${sourceY} L${sX},${sY} L${cornerX > sX ? cornerX - r : cornerX + r},${sY} Q${cornerX},${sY} ${cornerX},${sY + (tY > sY ? r : -r)} L${cornerX},${tY} L${targetX},${targetY}`
243
+ } else if (isHorizontalSource && isHorizontalTarget) {
244
+ // Both horizontal — go through middle Y
245
+ path = `M${sourceX},${sourceY} L${sX},${sourceY} L${midX},${sourceY} Q${midX},${sourceY} ${midX},${midY} L${midX},${targetY} L${tX},${targetY} L${targetX},${targetY}`
246
+ } else {
247
+ // Both vertical — go through middle X
248
+ path = `M${sourceX},${sourceY} L${sourceX},${sY} L${sourceX},${midY} Q${sourceX},${midY} ${midX},${midY} L${targetX},${midY} L${targetX},${tY} L${targetX},${targetY}`
249
+ }
250
+
251
+ return { path, labelX: center.x, labelY: center.y }
252
+ }
253
+
254
+ /**
255
+ * Calculate a straight edge path — direct line between two points.
256
+ */
257
+ export function getStraightPath(params: {
258
+ sourceX: number
259
+ sourceY: number
260
+ targetX: number
261
+ targetY: number
262
+ }): EdgePathResult {
263
+ const { sourceX, sourceY, targetX, targetY } = params
264
+ const center = getCenter(
265
+ { x: sourceX, y: sourceY },
266
+ { x: targetX, y: targetY },
267
+ )
268
+
269
+ return {
270
+ path: `M${sourceX},${sourceY} L${targetX},${targetY}`,
271
+ labelX: center.x,
272
+ labelY: center.y,
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Calculate a step edge path — right-angle segments with no rounding.
278
+ */
279
+ export function getStepPath(params: {
280
+ sourceX: number
281
+ sourceY: number
282
+ sourcePosition?: Position
283
+ targetX: number
284
+ targetY: number
285
+ targetPosition?: Position
286
+ }): EdgePathResult {
287
+ return getSmoothStepPath({ ...params, borderRadius: 0 })
288
+ }
289
+
290
+ /**
291
+ * Calculate an edge path that passes through waypoints.
292
+ * Uses line segments with optional smoothing.
293
+ */
294
+ export function getWaypointPath(params: {
295
+ sourceX: number
296
+ sourceY: number
297
+ targetX: number
298
+ targetY: number
299
+ waypoints: XYPosition[]
300
+ }): EdgePathResult {
301
+ const { sourceX, sourceY, targetX, targetY, waypoints } = params
302
+
303
+ if (waypoints.length === 0) {
304
+ return getStraightPath({ sourceX, sourceY, targetX, targetY })
305
+ }
306
+
307
+ const allPoints = [
308
+ { x: sourceX, y: sourceY },
309
+ ...waypoints,
310
+ { x: targetX, y: targetY },
311
+ ]
312
+
313
+ const segments = allPoints.map((p) => `${p.x},${p.y}`)
314
+ const path = `M${segments.join(' L')}`
315
+
316
+ // Label at the middle waypoint
317
+ const midIdx = Math.floor(waypoints.length / 2)
318
+ const midPoint = waypoints[midIdx] ?? {
319
+ x: (sourceX + targetX) / 2,
320
+ y: (sourceY + targetY) / 2,
321
+ }
322
+
323
+ return { path, labelX: midPoint.x, labelY: midPoint.y }
324
+ }
325
+
326
+ /**
327
+ * Get the edge path for a given edge type.
328
+ */
329
+ export function getEdgePath(
330
+ type: string,
331
+ sourceX: number,
332
+ sourceY: number,
333
+ sourcePosition: Position,
334
+ targetX: number,
335
+ targetY: number,
336
+ targetPosition: Position,
337
+ ): EdgePathResult {
338
+ switch (type) {
339
+ case 'smoothstep':
340
+ return getSmoothStepPath({
341
+ sourceX,
342
+ sourceY,
343
+ sourcePosition,
344
+ targetX,
345
+ targetY,
346
+ targetPosition,
347
+ })
348
+ case 'straight':
349
+ return getStraightPath({ sourceX, sourceY, targetX, targetY })
350
+ case 'step':
351
+ return getStepPath({
352
+ sourceX,
353
+ sourceY,
354
+ sourcePosition,
355
+ targetX,
356
+ targetY,
357
+ targetPosition,
358
+ })
359
+ default:
360
+ return getBezierPath({
361
+ sourceX,
362
+ sourceY,
363
+ sourcePosition,
364
+ targetX,
365
+ targetY,
366
+ targetPosition,
367
+ })
368
+ }
369
+ }