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