@flowgram.ai/free-snap-plugin 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1070 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var __decorateClass = (decorators, target, key, kind) => {
30
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
31
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
32
+ if (decorator = decorators[i])
33
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
34
+ if (kind && result) __defProp(target, key, result);
35
+ return result;
36
+ };
37
+
38
+ // src/index.ts
39
+ var src_exports = {};
40
+ __export(src_exports, {
41
+ WorkflowSnapService: () => WorkflowSnapService,
42
+ createFreeSnapPlugin: () => createFreeSnapPlugin
43
+ });
44
+ module.exports = __toCommonJS(src_exports);
45
+
46
+ // src/create-plugin.ts
47
+ var import_core3 = require("@flowgram.ai/core");
48
+
49
+ // src/service.ts
50
+ var import_inversify = require("inversify");
51
+ var import_document = require("@flowgram.ai/document");
52
+ var import_document2 = require("@flowgram.ai/document");
53
+ var import_core = require("@flowgram.ai/core");
54
+ var import_free_layout_core = require("@flowgram.ai/free-layout-core");
55
+ var import_free_layout_core2 = require("@flowgram.ai/free-layout-core");
56
+ var import_utils = require("@flowgram.ai/utils");
57
+
58
+ // src/constant.ts
59
+ var SnapDefaultOptions = {
60
+ enableEdgeSnapping: true,
61
+ edgeThreshold: 7,
62
+ enableGridSnapping: false,
63
+ gridSize: 20,
64
+ enableMultiSnapping: false,
65
+ enableOnlyViewportSnapping: true,
66
+ edgeColor: "#4E40E5",
67
+ alignColor: "#4E40E5",
68
+ edgeLineWidth: 2,
69
+ alignLineWidth: 2,
70
+ alignCrossWidth: 16
71
+ };
72
+ var Epsilon = 1e-5;
73
+
74
+ // src/utils.ts
75
+ var isEqual = (a, b) => {
76
+ if (a === void 0 || b === void 0) {
77
+ return false;
78
+ }
79
+ return Math.abs(a - b) < Epsilon;
80
+ };
81
+ var isLessThan = (a, b) => {
82
+ if (a === void 0 || b === void 0) {
83
+ return false;
84
+ }
85
+ return b - a > Epsilon;
86
+ };
87
+ var isGreaterThan = (a, b) => {
88
+ if (a === void 0 || b === void 0) {
89
+ return false;
90
+ }
91
+ return a - b > Epsilon;
92
+ };
93
+ var isLessThanOrEqual = (a, b) => isEqual(a, b) || isLessThan(a, b);
94
+ var isNumber = (value) => typeof value === "number" && !isNaN(value);
95
+
96
+ // src/service.ts
97
+ var WorkflowSnapService = class {
98
+ constructor() {
99
+ this.disposers = [];
100
+ this.snapEmitter = new import_utils.Emitter();
101
+ this.onSnap = this.snapEmitter.event;
102
+ }
103
+ init(params = {}) {
104
+ this.options = {
105
+ ...SnapDefaultOptions,
106
+ ...params
107
+ };
108
+ this.mountListener();
109
+ }
110
+ dispose() {
111
+ this.disposers.forEach((disposer) => disposer.dispose());
112
+ }
113
+ mountListener() {
114
+ const dragAdjusterDisposer = this.dragService.registerPosAdjuster(
115
+ (params) => this.snapping({
116
+ targetNodes: params.selectedNodes,
117
+ position: params.position
118
+ })
119
+ );
120
+ const dragEndDisposer = this.dragService.onNodesDrag((event) => {
121
+ if (event.type !== "onDragEnd") {
122
+ return;
123
+ }
124
+ if (this.options.enableGridSnapping) {
125
+ this.gridSnapping({
126
+ targetNodes: event.nodes,
127
+ gridSize: this.options.gridSize
128
+ });
129
+ }
130
+ if (this.options.enableEdgeSnapping) {
131
+ this.clear();
132
+ }
133
+ });
134
+ this.disposers.push(dragAdjusterDisposer, dragEndDisposer);
135
+ }
136
+ snapping(params) {
137
+ const { targetNodes, position } = params;
138
+ const isMultiSnapping = this.options.enableMultiSnapping ? false : targetNodes.length !== 1;
139
+ if (!this.options.enableEdgeSnapping || isMultiSnapping) {
140
+ return {
141
+ x: 0,
142
+ y: 0
143
+ };
144
+ }
145
+ const selectedBounds = this.getBounds(targetNodes);
146
+ const targetRect = new import_utils.Rectangle(
147
+ position.x,
148
+ position.y,
149
+ selectedBounds.width,
150
+ selectedBounds.height
151
+ );
152
+ const snapNodeRects = this.getSnapNodeRects({
153
+ targetNodes,
154
+ targetRect
155
+ });
156
+ const { alignOffset, alignRects, alignSpacing } = this.calcAlignOffset({
157
+ targetRect,
158
+ alignThreshold: this.options.edgeThreshold,
159
+ snapNodeRects
160
+ });
161
+ const { snapOffset, snapEdgeLines } = this.calcSnapOffset({
162
+ targetRect,
163
+ edgeThreshold: this.options.edgeThreshold,
164
+ snapNodeRects
165
+ });
166
+ const offset = {
167
+ x: snapOffset.x || alignOffset.x,
168
+ y: snapOffset.y || alignOffset.y
169
+ };
170
+ const snapRect = new import_utils.Rectangle(
171
+ position.x + offset.x,
172
+ position.y + offset.y,
173
+ targetRect.width,
174
+ targetRect.height
175
+ );
176
+ this.snapEmitter.fire({
177
+ snapRect,
178
+ snapEdgeLines,
179
+ alignRects,
180
+ alignSpacing
181
+ });
182
+ return offset;
183
+ }
184
+ calcSnapOffset(params) {
185
+ const { snapNodeRects, edgeThreshold, targetRect } = params;
186
+ const snapLines = this.getSnapLines({
187
+ snapNodeRects
188
+ });
189
+ const topYClosestLine = snapLines.horizontal.find(
190
+ (line) => isLessThanOrEqual(Math.abs(line.y - targetRect.top), edgeThreshold)
191
+ );
192
+ const bottomYClosestLine = snapLines.horizontal.find(
193
+ (line) => isLessThanOrEqual(Math.abs(line.y - targetRect.bottom), edgeThreshold)
194
+ );
195
+ const leftXClosestLine = snapLines.vertical.find(
196
+ (line) => isLessThanOrEqual(Math.abs(line.x - targetRect.left), edgeThreshold)
197
+ );
198
+ const rightXClosestLine = snapLines.vertical.find(
199
+ (line) => isLessThanOrEqual(Math.abs(line.x - targetRect.right), edgeThreshold)
200
+ );
201
+ const midYClosestLine = snapLines.midHorizontal.find(
202
+ (line) => isLessThanOrEqual(Math.abs(line.y - targetRect.center.y), edgeThreshold)
203
+ );
204
+ const midXClosestLine = snapLines.midVertical.find(
205
+ (line) => isLessThanOrEqual(Math.abs(line.x - targetRect.center.x), edgeThreshold)
206
+ );
207
+ const topYClosest = topYClosestLine?.y;
208
+ const bottomYClosest = isNumber(bottomYClosestLine?.y) ? bottomYClosestLine.y - targetRect.height : void 0;
209
+ const leftXClosest = leftXClosestLine?.x;
210
+ const rightXClosest = isNumber(rightXClosestLine?.x) ? rightXClosestLine.x - targetRect.width : void 0;
211
+ const midYClosest = isNumber(midYClosestLine?.y) ? midYClosestLine.y - targetRect.height / 2 : void 0;
212
+ const midXClosest = isNumber(midXClosestLine?.x) ? midXClosestLine.x - targetRect.width / 2 : void 0;
213
+ const snappingPosition = {
214
+ x: midXClosest ?? leftXClosest ?? rightXClosest ?? targetRect.x,
215
+ y: midYClosest ?? topYClosest ?? bottomYClosest ?? targetRect.y
216
+ };
217
+ const snapOffset = {
218
+ x: snappingPosition.x - targetRect.x,
219
+ y: snappingPosition.y - targetRect.y
220
+ };
221
+ const snapEdgeLines = {
222
+ top: isEqual(topYClosest, snappingPosition.y) ? topYClosestLine : void 0,
223
+ bottom: isEqual(bottomYClosest, snappingPosition.y) ? bottomYClosestLine : void 0,
224
+ left: isEqual(leftXClosest, snappingPosition.x) ? leftXClosestLine : void 0,
225
+ right: isEqual(rightXClosest, snappingPosition.x) ? rightXClosestLine : void 0,
226
+ midVertical: isEqual(midXClosest, snappingPosition.x) ? midXClosestLine : void 0,
227
+ midHorizontal: isEqual(midYClosest, snappingPosition.y) ? midYClosestLine : void 0
228
+ };
229
+ return { snapOffset, snapEdgeLines };
230
+ }
231
+ gridSnapping(params) {
232
+ const { gridSize, targetNodes } = params;
233
+ const rect = this.getBounds(targetNodes);
234
+ const snap = (value) => Math.round(value / gridSize) * gridSize;
235
+ const snappedPosition = {
236
+ x: snap(rect.x),
237
+ y: snap(rect.y)
238
+ };
239
+ const offset = {
240
+ x: snappedPosition.x - rect.x,
241
+ y: snappedPosition.y - rect.y
242
+ };
243
+ targetNodes.forEach(
244
+ (node) => this.updateNodePositionWithOffset({
245
+ node,
246
+ offset
247
+ })
248
+ );
249
+ }
250
+ clear() {
251
+ this.snapEmitter.fire({
252
+ snapEdgeLines: {},
253
+ snapRect: import_utils.Rectangle.EMPTY,
254
+ alignRects: {
255
+ top: [],
256
+ bottom: [],
257
+ left: [],
258
+ right: []
259
+ },
260
+ alignSpacing: {}
261
+ });
262
+ }
263
+ getSnapLines(params) {
264
+ const { snapNodeRects } = params;
265
+ const horizontalLines = [];
266
+ const verticalLines = [];
267
+ const midHorizontalLines = [];
268
+ const midVerticalLines = [];
269
+ snapNodeRects.forEach((snapNodeRect) => {
270
+ const nodeBounds = snapNodeRect.rect;
271
+ const nodeCenter = nodeBounds.center;
272
+ const top = {
273
+ y: nodeBounds.top,
274
+ sourceNodeId: snapNodeRect.id
275
+ };
276
+ const bottom = {
277
+ y: nodeBounds.bottom,
278
+ sourceNodeId: snapNodeRect.id
279
+ };
280
+ const left = {
281
+ x: nodeBounds.left,
282
+ sourceNodeId: snapNodeRect.id
283
+ };
284
+ const right = {
285
+ x: nodeBounds.right,
286
+ sourceNodeId: snapNodeRect.id
287
+ };
288
+ const midHorizontal = {
289
+ y: nodeCenter.y,
290
+ sourceNodeId: snapNodeRect.id
291
+ };
292
+ const midVertical = {
293
+ x: nodeCenter.x,
294
+ sourceNodeId: snapNodeRect.id
295
+ };
296
+ horizontalLines.push(top, bottom);
297
+ verticalLines.push(left, right);
298
+ midHorizontalLines.push(midHorizontal);
299
+ midVerticalLines.push(midVertical);
300
+ });
301
+ return {
302
+ horizontal: horizontalLines,
303
+ vertical: verticalLines,
304
+ midHorizontal: midHorizontalLines,
305
+ midVertical: midVerticalLines
306
+ };
307
+ }
308
+ getAvailableNodes(params) {
309
+ const { targetNodes, targetRect } = params;
310
+ const targetCenter = targetRect.center;
311
+ const targetContainerId = targetNodes[0].parent?.id ?? this.document.root.id;
312
+ const disabledNodeIds = targetNodes.map((n) => n.id);
313
+ disabledNodeIds.push(import_document2.FlowNodeBaseType.ROOT);
314
+ const availableNodes = this.nodes.filter((n) => n.parent?.id === targetContainerId).filter((n) => !disabledNodeIds.includes(n.id)).sort((nodeA, nodeB) => {
315
+ const nodeCenterA = nodeA.getData(import_document.FlowNodeTransformData).bounds.center;
316
+ const nodeCenterB = nodeB.getData(import_document.FlowNodeTransformData).bounds.center;
317
+ const distanceA = Math.abs(nodeCenterA.x - targetCenter.x) + Math.abs(nodeCenterA.y - targetCenter.y);
318
+ const distanceB = Math.abs(nodeCenterB.x - targetCenter.x) + Math.abs(nodeCenterB.y - targetCenter.y);
319
+ return distanceA - distanceB;
320
+ });
321
+ return availableNodes;
322
+ }
323
+ viewRect() {
324
+ const { width, height, scrollX, scrollY, zoom } = this.playgroundConfig.config;
325
+ return new import_utils.Rectangle(scrollX / zoom, scrollY / zoom, width / zoom, height / zoom);
326
+ }
327
+ getSnapNodeRects(params) {
328
+ const availableNodes = this.getAvailableNodes(params);
329
+ const viewRect = this.viewRect();
330
+ return availableNodes.map((node) => {
331
+ const snapNodeRect = {
332
+ id: node.id,
333
+ rect: node.getData(import_document.FlowNodeTransformData).bounds,
334
+ entity: node
335
+ };
336
+ if (this.options.enableOnlyViewportSnapping && node.parent?.flowNodeType === import_document2.FlowNodeBaseType.ROOT && !import_utils.Rectangle.intersects(viewRect, snapNodeRect.rect)) {
337
+ return;
338
+ }
339
+ return snapNodeRect;
340
+ }).filter(Boolean);
341
+ }
342
+ get nodes() {
343
+ return this.entityManager.getEntities(import_free_layout_core.WorkflowNodeEntity);
344
+ }
345
+ getBounds(nodes) {
346
+ if (nodes.length === 0) {
347
+ return import_utils.Rectangle.EMPTY;
348
+ }
349
+ return import_utils.Rectangle.enlarge(nodes.map((n) => n.getData(import_document.FlowNodeTransformData).bounds));
350
+ }
351
+ updateNodePositionWithOffset(params) {
352
+ const { node, offset } = params;
353
+ const transform = node.getData(import_core.TransformData);
354
+ const positionWithOffset = {
355
+ x: transform.position.x + offset.x,
356
+ y: transform.position.y + offset.y
357
+ };
358
+ if (node.collapsedChildren?.length > 0) {
359
+ node.collapsedChildren.forEach((childNode) => {
360
+ const childNodeTransformData = childNode.getData(import_document.FlowNodeTransformData);
361
+ childNodeTransformData.fireChange();
362
+ });
363
+ }
364
+ transform.update({
365
+ position: positionWithOffset
366
+ });
367
+ }
368
+ calcAlignOffset(params) {
369
+ const { snapNodeRects, targetRect, alignThreshold } = params;
370
+ const alignRects = this.getAlignRects({
371
+ targetRect,
372
+ snapNodeRects
373
+ });
374
+ const alignSpacing = this.calcAlignSpacing({
375
+ targetRect,
376
+ alignRects
377
+ });
378
+ let topY;
379
+ let bottomY;
380
+ let leftX;
381
+ let rightX;
382
+ let midY;
383
+ let midX;
384
+ if (alignSpacing.top) {
385
+ const topAlignY = alignRects.top[0].rect.bottom + alignSpacing.top;
386
+ const isAlignTop = isLessThanOrEqual(Math.abs(targetRect.top - topAlignY), alignThreshold);
387
+ if (isAlignTop) {
388
+ topY = topAlignY;
389
+ } else {
390
+ alignSpacing.top = void 0;
391
+ }
392
+ }
393
+ if (alignSpacing.bottom) {
394
+ const bottomAlignY = alignRects.bottom[0].rect.top - alignSpacing.bottom;
395
+ const isAlignBottom = isLessThan(Math.abs(targetRect.bottom - bottomAlignY), alignThreshold);
396
+ if (isAlignBottom) {
397
+ bottomY = bottomAlignY - targetRect.height;
398
+ } else {
399
+ alignSpacing.bottom = void 0;
400
+ }
401
+ }
402
+ if (alignSpacing.left) {
403
+ const leftAlignX = alignRects.left[0].rect.right + alignSpacing.left;
404
+ const isAlignLeft = isLessThanOrEqual(Math.abs(targetRect.left - leftAlignX), alignThreshold);
405
+ if (isAlignLeft) {
406
+ leftX = leftAlignX;
407
+ } else {
408
+ alignSpacing.left = void 0;
409
+ }
410
+ }
411
+ if (alignSpacing.right) {
412
+ const rightAlignX = alignRects.right[0].rect.left - alignSpacing.right;
413
+ const isAlignRight = isLessThanOrEqual(
414
+ Math.abs(targetRect.right - rightAlignX),
415
+ alignThreshold
416
+ );
417
+ if (isAlignRight) {
418
+ rightX = rightAlignX - targetRect.width;
419
+ } else {
420
+ alignSpacing.right = void 0;
421
+ }
422
+ }
423
+ if (alignSpacing.midHorizontal) {
424
+ const leftAlignX = alignRects.left[0].rect.right + alignSpacing.midHorizontal;
425
+ const isAlignMidHorizontal = isLessThanOrEqual(
426
+ Math.abs(targetRect.left - leftAlignX),
427
+ alignThreshold
428
+ );
429
+ if (isAlignMidHorizontal) {
430
+ midX = leftAlignX;
431
+ } else {
432
+ alignSpacing.midHorizontal = void 0;
433
+ }
434
+ }
435
+ if (alignSpacing.midVertical) {
436
+ const topAlignY = alignRects.top[0].rect.bottom + alignSpacing.midVertical;
437
+ const isAlignMidVertical = isLessThanOrEqual(
438
+ Math.abs(targetRect.top - topAlignY),
439
+ alignThreshold
440
+ );
441
+ if (isAlignMidVertical) {
442
+ midY = topAlignY;
443
+ } else {
444
+ alignSpacing.midVertical = void 0;
445
+ }
446
+ }
447
+ const alignPosition = {
448
+ x: midX ?? leftX ?? rightX ?? targetRect.x,
449
+ y: midY ?? topY ?? bottomY ?? targetRect.y
450
+ };
451
+ const alignOffset = {
452
+ x: alignPosition.x - targetRect.x,
453
+ y: alignPosition.y - targetRect.y
454
+ };
455
+ return { alignOffset, alignRects, alignSpacing };
456
+ }
457
+ calcAlignSpacing(params) {
458
+ const { targetRect, alignRects } = params;
459
+ const topSpacing = this.getDirectionAlignSpacing({
460
+ rects: alignRects.top,
461
+ isHorizontal: false
462
+ });
463
+ const bottomSpacing = this.getDirectionAlignSpacing({
464
+ rects: alignRects.bottom,
465
+ isHorizontal: false
466
+ });
467
+ const leftSpacing = this.getDirectionAlignSpacing({
468
+ rects: alignRects.left,
469
+ isHorizontal: true
470
+ });
471
+ const rightSpacing = this.getDirectionAlignSpacing({
472
+ rects: alignRects.right,
473
+ isHorizontal: true
474
+ });
475
+ const midHorizontalSpacing = this.getMidAlignSpacing({
476
+ rectA: alignRects.left[0]?.rect,
477
+ rectB: alignRects.right[0]?.rect,
478
+ targetRect,
479
+ isHorizontal: true
480
+ });
481
+ const midVerticalSpacing = this.getMidAlignSpacing({
482
+ rectA: alignRects.top[0]?.rect,
483
+ rectB: alignRects.bottom[0]?.rect,
484
+ targetRect,
485
+ isHorizontal: false
486
+ });
487
+ return {
488
+ top: topSpacing,
489
+ bottom: bottomSpacing,
490
+ left: leftSpacing,
491
+ right: rightSpacing,
492
+ midHorizontal: midHorizontalSpacing,
493
+ midVertical: midVerticalSpacing
494
+ };
495
+ }
496
+ getAlignRects(params) {
497
+ const { targetRect, snapNodeRects } = params;
498
+ const topVerticalRects = [];
499
+ const bottomVerticalRects = [];
500
+ const leftHorizontalRects = [];
501
+ const rightHorizontalRects = [];
502
+ snapNodeRects.forEach((snapNodeRect) => {
503
+ const nodeRect = snapNodeRect.rect;
504
+ const { isVerticalIntersection, isHorizontalIntersection, isIntersection } = this.intersection(nodeRect, targetRect);
505
+ if (isIntersection) {
506
+ return;
507
+ } else if (isVerticalIntersection) {
508
+ if (isGreaterThan(nodeRect.center.y, targetRect.center.y)) {
509
+ bottomVerticalRects.push({
510
+ rect: nodeRect,
511
+ sourceNodeId: snapNodeRect.id
512
+ });
513
+ } else {
514
+ topVerticalRects.push({
515
+ rect: nodeRect,
516
+ sourceNodeId: snapNodeRect.id
517
+ });
518
+ }
519
+ } else if (isHorizontalIntersection) {
520
+ if (isGreaterThan(nodeRect.center.x, targetRect.center.x)) {
521
+ rightHorizontalRects.push({
522
+ rect: nodeRect,
523
+ sourceNodeId: snapNodeRect.id
524
+ });
525
+ } else {
526
+ leftHorizontalRects.push({
527
+ rect: nodeRect,
528
+ sourceNodeId: snapNodeRect.id
529
+ });
530
+ }
531
+ }
532
+ });
533
+ return {
534
+ top: topVerticalRects,
535
+ bottom: bottomVerticalRects,
536
+ left: leftHorizontalRects,
537
+ right: rightHorizontalRects
538
+ };
539
+ }
540
+ getMidAlignSpacing(params) {
541
+ const { rectA, rectB, targetRect, isHorizontal } = params;
542
+ if (!rectA || !rectB) {
543
+ return;
544
+ }
545
+ const { isVerticalIntersection, isHorizontalIntersection, isIntersection } = this.intersection(
546
+ rectA,
547
+ rectB
548
+ );
549
+ if (isIntersection) {
550
+ return;
551
+ }
552
+ if (isHorizontal && isHorizontalIntersection && !isVerticalIntersection) {
553
+ const betweenSpacing = Math.min(
554
+ Math.abs(rectA.left - rectB.right),
555
+ Math.abs(rectA.right - rectB.left)
556
+ );
557
+ return (betweenSpacing - targetRect.width) / 2;
558
+ } else if (!isHorizontal && isVerticalIntersection && !isHorizontalIntersection) {
559
+ const betweenSpacing = Math.min(
560
+ Math.abs(rectA.top - rectB.bottom),
561
+ Math.abs(rectA.bottom - rectB.top)
562
+ );
563
+ return (betweenSpacing - targetRect.height) / 2;
564
+ }
565
+ }
566
+ getDirectionAlignSpacing(params) {
567
+ const { rects, isHorizontal } = params;
568
+ if (rects.length < 2) {
569
+ return;
570
+ }
571
+ const rectA = rects[0].rect;
572
+ const rectB = rects[1].rect;
573
+ const { isVerticalIntersection, isHorizontalIntersection, isIntersection } = this.intersection(
574
+ rectA,
575
+ rectB
576
+ );
577
+ if (isIntersection) {
578
+ return;
579
+ }
580
+ if (isHorizontal && isHorizontalIntersection && !isVerticalIntersection) {
581
+ return Math.min(Math.abs(rectA.left - rectB.right), Math.abs(rectA.right - rectB.left));
582
+ } else if (!isHorizontal && isVerticalIntersection && !isHorizontalIntersection) {
583
+ return Math.min(Math.abs(rectA.top - rectB.bottom), Math.abs(rectA.bottom - rectB.top));
584
+ }
585
+ return;
586
+ }
587
+ intersection(rectA, rectB) {
588
+ const isVerticalIntersection = isLessThan(rectA.left, rectB.right) && isGreaterThan(rectA.right, rectB.left);
589
+ const isHorizontalIntersection = isLessThan(rectA.top, rectB.bottom) && isGreaterThan(rectA.bottom, rectB.top);
590
+ const isIntersection = isHorizontalIntersection && isVerticalIntersection;
591
+ return {
592
+ isHorizontalIntersection,
593
+ isVerticalIntersection,
594
+ isIntersection
595
+ };
596
+ }
597
+ };
598
+ __decorateClass([
599
+ (0, import_inversify.inject)(import_free_layout_core.WorkflowDocument)
600
+ ], WorkflowSnapService.prototype, "document", 2);
601
+ __decorateClass([
602
+ (0, import_inversify.inject)(import_core.EntityManager)
603
+ ], WorkflowSnapService.prototype, "entityManager", 2);
604
+ __decorateClass([
605
+ (0, import_inversify.inject)(import_free_layout_core2.WorkflowDragService)
606
+ ], WorkflowSnapService.prototype, "dragService", 2);
607
+ __decorateClass([
608
+ (0, import_inversify.inject)(import_core.PlaygroundConfigEntity)
609
+ ], WorkflowSnapService.prototype, "playgroundConfig", 2);
610
+ WorkflowSnapService = __decorateClass([
611
+ (0, import_inversify.injectable)()
612
+ ], WorkflowSnapService);
613
+
614
+ // src/layer.tsx
615
+ var import_react = __toESM(require("react"));
616
+ var import_inversify2 = require("inversify");
617
+ var import_document3 = require("@flowgram.ai/document");
618
+ var import_core2 = require("@flowgram.ai/core");
619
+ var import_free_layout_core3 = require("@flowgram.ai/free-layout-core");
620
+ var import_utils3 = require("@flowgram.ai/utils");
621
+ var WorkflowSnapLayer = class extends import_core2.Layer {
622
+ constructor() {
623
+ super(...arguments);
624
+ this.node = import_utils3.domUtils.createDivWithClass(
625
+ "gedit-playground-layer gedit-flow-snap-layer"
626
+ );
627
+ this.edgeLines = [];
628
+ this.alignLines = [];
629
+ }
630
+ onReady() {
631
+ this.node.style.zIndex = "9999";
632
+ this.toDispose.pushAll([
633
+ this.service.onSnap((event) => {
634
+ this.edgeLines = this.calcEdgeLines(event);
635
+ this.alignLines = this.calcAlignLines(event);
636
+ this.render();
637
+ })
638
+ ]);
639
+ }
640
+ render() {
641
+ return /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, this.alignLines.length > 0 && /* @__PURE__ */ import_react.default.createElement("div", { className: "workflow-snap-align-lines" }, this.renderAlignLines()), this.edgeLines.length > 0 && /* @__PURE__ */ import_react.default.createElement("div", { className: "workflow-snap-edge-lines" }, this.renderEdgeLines()));
642
+ }
643
+ onZoom(scale) {
644
+ this.node.style.transform = `scale(${scale})`;
645
+ }
646
+ renderEdgeLines() {
647
+ return this.edgeLines.map((renderLine) => {
648
+ const { className, sourceNode, top, left, width, height, dashed } = renderLine;
649
+ const id = `${className}-${sourceNode}-${top}-${left}-${width}-${height}`;
650
+ const isHorizontal = width < height;
651
+ const border = `${this.options.edgeLineWidth}px ${dashed ? "dashed" : "solid"} ${this.options.edgeColor}`;
652
+ return /* @__PURE__ */ import_react.default.createElement(
653
+ "div",
654
+ {
655
+ className: `workflow-snap-edge-line ${className}`,
656
+ "data-testid": "sdk.workflow.canvas.snap.edgeLine",
657
+ "data-snap-line-id": id,
658
+ "data-snap-line-sourceNode": sourceNode,
659
+ key: id,
660
+ style: {
661
+ top,
662
+ left,
663
+ width,
664
+ height,
665
+ position: "absolute",
666
+ borderLeft: isHorizontal ? border : "none",
667
+ borderTop: !isHorizontal ? border : "none"
668
+ }
669
+ }
670
+ );
671
+ });
672
+ }
673
+ renderAlignLines() {
674
+ return this.alignLines.map((renderLine) => {
675
+ const id = `${renderLine.className}-${renderLine.sourceNode}-${renderLine.top}-${renderLine.left}-${renderLine.width}-${renderLine.height}`;
676
+ const isHorizontal = isGreaterThan(renderLine.width, renderLine.height);
677
+ const alignLineWidth = this.options.alignLineWidth;
678
+ const alignCrossWidth = this.options.alignCrossWidth;
679
+ const adjustedTop = isHorizontal ? renderLine.top - alignLineWidth / 2 : renderLine.top;
680
+ const adjustedLeft = isHorizontal ? renderLine.left : renderLine.left - alignLineWidth / 2;
681
+ return /* @__PURE__ */ import_react.default.createElement(
682
+ "div",
683
+ {
684
+ className: `workflow-snap-align-line ${renderLine.className}`,
685
+ "data-testid": "sdk.workflow.canvas.snap.alignLine",
686
+ "data-snap-line-id": id,
687
+ "data-snap-line-sourceNode": renderLine.sourceNode,
688
+ key: id,
689
+ style: {
690
+ position: "absolute"
691
+ }
692
+ },
693
+ /* @__PURE__ */ import_react.default.createElement(
694
+ "div",
695
+ {
696
+ style: {
697
+ position: "absolute",
698
+ top: adjustedTop,
699
+ left: adjustedLeft,
700
+ width: isHorizontal ? renderLine.width : alignLineWidth,
701
+ height: isHorizontal ? alignLineWidth : renderLine.height,
702
+ backgroundColor: this.options.alignColor
703
+ }
704
+ }
705
+ ),
706
+ /* @__PURE__ */ import_react.default.createElement(
707
+ "div",
708
+ {
709
+ style: {
710
+ position: "absolute",
711
+ top: isHorizontal ? adjustedTop - (alignCrossWidth - alignLineWidth) / 2 : adjustedTop,
712
+ left: isHorizontal ? adjustedLeft : adjustedLeft - (alignCrossWidth - alignLineWidth) / 2,
713
+ width: isHorizontal ? alignLineWidth : alignCrossWidth,
714
+ height: isHorizontal ? alignCrossWidth : alignLineWidth,
715
+ backgroundColor: this.options.alignColor
716
+ }
717
+ }
718
+ ),
719
+ /* @__PURE__ */ import_react.default.createElement(
720
+ "div",
721
+ {
722
+ style: {
723
+ position: "absolute",
724
+ top: isHorizontal ? adjustedTop - (alignCrossWidth - alignLineWidth) / 2 : adjustedTop + renderLine.height - alignLineWidth,
725
+ left: isHorizontal ? adjustedLeft + renderLine.width - alignLineWidth : adjustedLeft - (alignCrossWidth - alignLineWidth) / 2,
726
+ width: isHorizontal ? alignLineWidth : alignCrossWidth,
727
+ height: isHorizontal ? alignCrossWidth : alignLineWidth,
728
+ backgroundColor: this.options.alignColor
729
+ }
730
+ }
731
+ )
732
+ );
733
+ });
734
+ }
735
+ calcEdgeLines(event) {
736
+ const { alignRects, snapRect, snapEdgeLines } = event;
737
+ const edgeLines = [];
738
+ const topFullAlign = this.directionFullAlign({
739
+ alignRects: alignRects.top,
740
+ targetRect: snapRect,
741
+ isVertical: true
742
+ });
743
+ const bottomFullAlign = this.directionFullAlign({
744
+ alignRects: alignRects.bottom,
745
+ targetRect: snapRect,
746
+ isVertical: true
747
+ });
748
+ const leftFullAlign = this.directionFullAlign({
749
+ alignRects: alignRects.left,
750
+ targetRect: snapRect,
751
+ isVertical: false
752
+ });
753
+ const rightFullAlign = this.directionFullAlign({
754
+ alignRects: alignRects.right,
755
+ targetRect: snapRect,
756
+ isVertical: false
757
+ });
758
+ if (topFullAlign) {
759
+ const top = topFullAlign.rect.top;
760
+ const height = bottomFullAlign ? snapRect.bottom - snapRect.height / 2 - top : snapRect.bottom - top;
761
+ const width = this.options.edgeLineWidth;
762
+ const lineData = {
763
+ top,
764
+ width,
765
+ height
766
+ };
767
+ edgeLines.push({
768
+ className: "edge-full-top-left",
769
+ sourceNode: topFullAlign.sourceNodeId,
770
+ left: snapRect.left,
771
+ ...lineData
772
+ });
773
+ edgeLines.push({
774
+ className: "edge-full-top-right",
775
+ sourceNode: topFullAlign.sourceNodeId,
776
+ left: snapRect.right,
777
+ ...lineData
778
+ });
779
+ edgeLines.push({
780
+ className: "edge-full-top-mid",
781
+ sourceNode: topFullAlign.sourceNodeId,
782
+ left: snapRect.left + snapRect.width / 2,
783
+ dashed: true,
784
+ ...lineData
785
+ });
786
+ }
787
+ if (bottomFullAlign) {
788
+ const top = topFullAlign ? snapRect.top + snapRect.height / 2 : snapRect.top;
789
+ const height = bottomFullAlign.rect.bottom - top;
790
+ const width = this.options.edgeLineWidth;
791
+ const lineData = {
792
+ top,
793
+ width,
794
+ height
795
+ };
796
+ edgeLines.push({
797
+ className: "edge-full-bottom-left",
798
+ sourceNode: bottomFullAlign.sourceNodeId,
799
+ left: snapRect.left,
800
+ ...lineData
801
+ });
802
+ edgeLines.push({
803
+ className: "edge-full-bottom-right",
804
+ sourceNode: bottomFullAlign.sourceNodeId,
805
+ left: snapRect.right,
806
+ ...lineData
807
+ });
808
+ edgeLines.push({
809
+ className: "edge-full-bottom-mid",
810
+ sourceNode: bottomFullAlign.sourceNodeId,
811
+ left: snapRect.left + snapRect.width / 2,
812
+ dashed: true,
813
+ ...lineData
814
+ });
815
+ }
816
+ if (leftFullAlign) {
817
+ const left = leftFullAlign.rect.left;
818
+ const width = rightFullAlign ? snapRect.right - snapRect.width / 2 - left : snapRect.right - left;
819
+ const height = this.options.edgeLineWidth;
820
+ const lineData = {
821
+ left,
822
+ width,
823
+ height
824
+ };
825
+ edgeLines.push({
826
+ className: "edge-full-left-top",
827
+ sourceNode: leftFullAlign.sourceNodeId,
828
+ top: snapRect.top,
829
+ ...lineData
830
+ });
831
+ edgeLines.push({
832
+ className: "edge-full-left-bottom",
833
+ sourceNode: leftFullAlign.sourceNodeId,
834
+ top: snapRect.bottom,
835
+ ...lineData
836
+ });
837
+ edgeLines.push({
838
+ className: "edge-full-left-mid",
839
+ sourceNode: leftFullAlign.sourceNodeId,
840
+ top: snapRect.top + snapRect.height / 2,
841
+ dashed: true,
842
+ ...lineData
843
+ });
844
+ }
845
+ if (rightFullAlign) {
846
+ const left = leftFullAlign ? snapRect.left + snapRect.width / 2 : snapRect.left;
847
+ const width = rightFullAlign.rect.right - left;
848
+ const height = this.options.edgeLineWidth;
849
+ const lineData = {
850
+ left,
851
+ width,
852
+ height
853
+ };
854
+ edgeLines.push({
855
+ className: "edge-full-right-top",
856
+ sourceNode: rightFullAlign.sourceNodeId,
857
+ top: snapRect.top,
858
+ ...lineData
859
+ });
860
+ edgeLines.push({
861
+ className: "edge-full-right-bottom",
862
+ sourceNode: rightFullAlign.sourceNodeId,
863
+ top: snapRect.bottom,
864
+ ...lineData
865
+ });
866
+ edgeLines.push({
867
+ className: "edge-full-right-mid",
868
+ sourceNode: rightFullAlign.sourceNodeId,
869
+ top: snapRect.top + snapRect.height / 2,
870
+ dashed: true,
871
+ ...lineData
872
+ });
873
+ }
874
+ const snappedEdgeLines = Object.entries(snapEdgeLines).map(([direction, snapLine]) => {
875
+ if (!snapLine) {
876
+ return;
877
+ }
878
+ const sourceNode = this.document.getNode(snapLine.sourceNodeId);
879
+ if (!sourceNode) {
880
+ return;
881
+ }
882
+ const nodeRect = sourceNode.getData(import_document3.FlowNodeTransformData).bounds;
883
+ if (isNumber(snapLine.x)) {
884
+ const top = Math.min(nodeRect.top, snapRect.top);
885
+ const bottom = Math.max(nodeRect.bottom, snapRect.bottom);
886
+ const height = bottom - top;
887
+ const left = snapLine.x;
888
+ const width = this.options.edgeLineWidth;
889
+ const isMidX = direction === "midVertical";
890
+ const lineData = {
891
+ className: `edge-snapped-${direction}`,
892
+ sourceNode: snapLine.sourceNodeId,
893
+ top,
894
+ left,
895
+ width,
896
+ height,
897
+ dashed: isMidX
898
+ };
899
+ const onTop = top === nodeRect.top;
900
+ if (onTop && topFullAlign) {
901
+ return;
902
+ }
903
+ if (!onTop && bottomFullAlign) {
904
+ return;
905
+ }
906
+ return lineData;
907
+ } else if (isNumber(snapLine.y)) {
908
+ const left = Math.min(nodeRect.left, snapRect.left);
909
+ const right = Math.max(nodeRect.right, snapRect.right);
910
+ const width = right - left;
911
+ const top = snapLine.y;
912
+ const height = this.options.edgeLineWidth;
913
+ const isMidY = direction === "midHorizontal";
914
+ const lineData = {
915
+ className: `edge-snapped-${direction}`,
916
+ sourceNode: snapLine.sourceNodeId,
917
+ top,
918
+ left,
919
+ width,
920
+ height,
921
+ dashed: isMidY
922
+ };
923
+ const onLeft = left === nodeRect.left;
924
+ if (onLeft && leftFullAlign) {
925
+ return;
926
+ }
927
+ if (!onLeft && rightFullAlign) {
928
+ return;
929
+ }
930
+ return lineData;
931
+ }
932
+ }).filter(Boolean);
933
+ edgeLines.push(...snappedEdgeLines);
934
+ return edgeLines;
935
+ }
936
+ directionFullAlign(params) {
937
+ const { alignRects, targetRect, isVertical } = params;
938
+ let fullAlignIndex = -1;
939
+ for (let i = 0; i < alignRects.length; i++) {
940
+ const alignRect = alignRects[i];
941
+ const prevRect = alignRects[i - 1]?.rect ?? targetRect;
942
+ const isFullAlign = this.rectFullAlign(alignRect.rect, prevRect, isVertical);
943
+ if (!isFullAlign) {
944
+ break;
945
+ }
946
+ fullAlignIndex = i;
947
+ }
948
+ const fullAlignRect = alignRects[fullAlignIndex];
949
+ return fullAlignRect;
950
+ }
951
+ rectFullAlign(rectA, rectB, isVertical) {
952
+ if (isVertical) {
953
+ return isEqual(rectA.left, rectB.left) && isEqual(rectA.right, rectB.right);
954
+ } else {
955
+ return isEqual(rectA.top, rectB.top) && isEqual(rectA.bottom, rectB.bottom);
956
+ }
957
+ }
958
+ calcAlignLines(event) {
959
+ const { alignRects, alignSpacing, snapRect } = event;
960
+ const topAlignLines = this.calcDirectionAlignLines({
961
+ alignRects: alignRects.top,
962
+ targetRect: snapRect,
963
+ isVertical: true,
964
+ spacing: alignSpacing.midVertical ?? alignSpacing.top
965
+ });
966
+ const bottomAlignLines = this.calcDirectionAlignLines({
967
+ alignRects: alignRects.bottom,
968
+ targetRect: snapRect,
969
+ isVertical: true,
970
+ spacing: alignSpacing.midVertical ?? alignSpacing.bottom
971
+ });
972
+ const leftAlignLines = this.calcDirectionAlignLines({
973
+ alignRects: alignRects.left,
974
+ targetRect: snapRect,
975
+ isVertical: false,
976
+ spacing: alignSpacing.midHorizontal ?? alignSpacing.left
977
+ });
978
+ const rightAlignLines = this.calcDirectionAlignLines({
979
+ alignRects: alignRects.right,
980
+ targetRect: snapRect,
981
+ isVertical: false,
982
+ spacing: alignSpacing.midHorizontal ?? alignSpacing.right
983
+ });
984
+ return [...topAlignLines, ...bottomAlignLines, ...leftAlignLines, ...rightAlignLines];
985
+ }
986
+ calcDirectionAlignLines(params) {
987
+ const { alignRects, targetRect, isVertical, spacing } = params;
988
+ const alignLines = [];
989
+ if (!spacing) {
990
+ return alignLines;
991
+ }
992
+ for (let i = 0; i < alignRects.length; i++) {
993
+ const alignRect = alignRects[i];
994
+ const rect = alignRect.rect;
995
+ const prevRect = alignRects[i - 1]?.rect ?? targetRect;
996
+ const betweenSpacing = isVertical ? Math.min(Math.abs(prevRect.top - rect.bottom), Math.abs(prevRect.bottom - rect.top)) : Math.min(Math.abs(prevRect.left - rect.right), Math.abs(prevRect.right - rect.left));
997
+ if (!isEqual(betweenSpacing, spacing)) {
998
+ break;
999
+ }
1000
+ if (isVertical) {
1001
+ const centerX = this.calcHorizontalIntersectionCenter(rect, targetRect);
1002
+ alignLines.push({
1003
+ className: "align-vertical",
1004
+ sourceNode: alignRect.sourceNodeId,
1005
+ top: Math.min(rect.bottom, prevRect.bottom),
1006
+ left: centerX,
1007
+ width: 1,
1008
+ height: spacing
1009
+ });
1010
+ } else {
1011
+ const centerY = this.calcVerticalIntersectionCenter(rect, targetRect);
1012
+ alignLines.push({
1013
+ className: "align-horizontal",
1014
+ sourceNode: alignRect.sourceNodeId,
1015
+ top: centerY,
1016
+ left: Math.min(rect.right, prevRect.right),
1017
+ width: spacing,
1018
+ height: 1
1019
+ });
1020
+ }
1021
+ }
1022
+ return alignLines;
1023
+ }
1024
+ calcVerticalIntersectionCenter(rectA, rectB) {
1025
+ const top = Math.max(rectA.top, rectB.top);
1026
+ const bottom = Math.min(rectA.bottom, rectB.bottom);
1027
+ return (top + bottom) / 2;
1028
+ }
1029
+ calcHorizontalIntersectionCenter(rectA, rectB) {
1030
+ const left = Math.max(rectA.left, rectB.left);
1031
+ const right = Math.min(rectA.right, rectB.right);
1032
+ return (left + right) / 2;
1033
+ }
1034
+ };
1035
+ WorkflowSnapLayer.type = "WorkflowSnapLayer";
1036
+ __decorateClass([
1037
+ (0, import_inversify2.inject)(import_free_layout_core3.WorkflowDocument)
1038
+ ], WorkflowSnapLayer.prototype, "document", 2);
1039
+ __decorateClass([
1040
+ (0, import_inversify2.inject)(WorkflowSnapService)
1041
+ ], WorkflowSnapLayer.prototype, "service", 2);
1042
+ WorkflowSnapLayer = __decorateClass([
1043
+ (0, import_inversify2.injectable)()
1044
+ ], WorkflowSnapLayer);
1045
+
1046
+ // src/create-plugin.ts
1047
+ var createFreeSnapPlugin = (0, import_core3.definePluginCreator)({
1048
+ onBind({ bind }) {
1049
+ bind(WorkflowSnapService).toSelf().inSingletonScope();
1050
+ },
1051
+ onInit(ctx, opts) {
1052
+ const options = {
1053
+ ...SnapDefaultOptions,
1054
+ ...opts
1055
+ };
1056
+ ctx.playground.registerLayer(WorkflowSnapLayer, options);
1057
+ const snapService = ctx.get(WorkflowSnapService);
1058
+ snapService.init(options);
1059
+ },
1060
+ onDispose(ctx) {
1061
+ const snapService = ctx.get(WorkflowSnapService);
1062
+ snapService.dispose();
1063
+ }
1064
+ });
1065
+ // Annotate the CommonJS export names for ESM import in node:
1066
+ 0 && (module.exports = {
1067
+ WorkflowSnapService,
1068
+ createFreeSnapPlugin
1069
+ });
1070
+ //# sourceMappingURL=index.js.map