aurea-eden 1.41.1 → 1.42.2

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.
@@ -3,7 +3,7 @@
3
3
  </template>
4
4
 
5
5
  <script>
6
- import { BpmnDiagram } from '../notations/BpmnDiagram.js';
6
+ import { BpmnDiagram } from '../notations/bpmn/BpmnDiagram.js';
7
7
  import { StarShape } from '../shapes/solids/StarShape.js';
8
8
  import { shallowRef, onMounted, onUnmounted, watch } from 'vue';
9
9
  import starUrl from '../../assets/star_gold.gif';
@@ -304,3 +304,4 @@ export default {
304
304
  }
305
305
  };
306
306
  </script>
307
+
@@ -1,47 +1,481 @@
1
- import * as THREE from 'three';
2
- import { RoundedCornerOrthogonalConnectorShape } from '../shapes/connector/RoundedCornerOrthogonalConnectorShape.js';
3
-
4
- class Connector extends THREE.Mesh {
5
- constructor(elementId,
6
- shape = new RoundedCornerOrthogonalConnectorShape(),
7
- sourceElement = null,
8
- targetElement = null,
9
- sourcePoint = { x: 0, y: 0 },
10
- targetPoint = { x: 2, y: 2 },
11
- waypoints = [{ x: 0, y: 1 }, { x: 2, y: 1 }],
12
- properties = {}) {
13
- super(shape.geometry, shape.material);
14
- this.elementId = elementId;
15
- this.sourceElement = sourceElement;
16
- this.targetElement = targetElement;
17
- this.shape = shape;
18
- this.points = [sourcePoint, ...waypoints, targetPoint];
19
- this.properties = properties;
20
- }
21
-
22
- setDiagram(diagram) {
23
- this.diagram = diagram;
24
- }
25
-
26
- static determinePoints(sourcePoint, targetPoint, sourcePosition, targetPosition) { //TODO: rozbudować o sourcePosition i targetPosition (np. trzyelementowe konektory)
27
- let waypoints = [];
28
-
29
- if (sourcePoint.x !== targetPoint.x && sourcePoint.y !== targetPoint.y) {
30
- // if source position starts with W or E then use the y coordinate of the source point
31
- // and the x coordinate of the target point
32
- if (sourcePosition.startsWith('W') || sourcePosition.startsWith('E')) {
33
- waypoints.push({ x: targetPoint.x, y: sourcePoint.y });
34
- }
35
- // if source position starts with N or S then use the x coordinate of the source point
36
- // and the y coordinate of the target point
37
- if (sourcePosition.startsWith('N') || sourcePosition.startsWith('S')) {
38
- waypoints.push({ x: sourcePoint.x, y: targetPoint.y });
39
- }
40
- }
41
-
42
- return [sourcePoint, ...waypoints, targetPoint];
43
- }
44
-
45
- }
46
-
1
+ import * as THREE from 'three';
2
+ import { RoundedCornerOrthogonalConnectorShape } from '../shapes/connector/RoundedCornerOrthogonalConnectorShape.js';
3
+ import { Element } from '../elements/Element.js';
4
+ import { CircleShape } from '../shapes/paths/CircleShape.js';
5
+ import { DiagramDimensions } from '../diagrams/DiagramConstants.js';
6
+
7
+ class Connector extends THREE.Mesh {
8
+ constructor(elementId,
9
+ shape = new RoundedCornerOrthogonalConnectorShape(),
10
+ sourceElement = null,
11
+ targetElement = null,
12
+ sourcePosition = 'auto',
13
+ targetPosition = 'auto',
14
+ label = null,
15
+ type = 'sequence',
16
+ properties = {}) {
17
+ super(shape.geometry, shape.material);
18
+ this.elementId = elementId;
19
+ this.sourceElement = sourceElement;
20
+ this.targetElement = targetElement;
21
+ this.sourcePosition = sourcePosition;
22
+ this.targetPosition = targetPosition;
23
+ this.label = label;
24
+ this.type = type;
25
+ this.properties = properties;
26
+
27
+ // Extract points from shape if provided, or expect them to be set later
28
+ this.points = shape.points || [];
29
+ this.labelElement = null; // Store reference to assigned label
30
+
31
+ /**
32
+ * The semantic role of the connector (e.g., 'sequence-flow', 'message-flow', 'association').
33
+ * Used for granular theming.
34
+ * @type {string}
35
+ */
36
+ this.semanticType = 'sequence-flow';
37
+ }
38
+
39
+ setDiagram(diagram) {
40
+ this.diagram = diagram;
41
+ if (this.labelElement) {
42
+ this.diagram.addElement(this.labelElement);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Recalculates the connector path and updates its geometry.
48
+ * Triggered when source or target elements move.
49
+ */
50
+ update() {
51
+ if (!this.sourceElement || !this.targetElement) return;
52
+
53
+ // Use the static points determination logic
54
+ // Properties can pass 'waypoints' (array of {x, y}) which will force route through them
55
+ const points = Connector.determinePoints(
56
+ this.sourceElement,
57
+ this.targetElement,
58
+ this.sourcePosition,
59
+ this.targetPosition,
60
+ this.properties?.waypoints,
61
+ this.properties?.waypointPorts
62
+ );
63
+
64
+ this.points = points;
65
+
66
+ // Re-generate geometry
67
+ let newShape;
68
+ if (this.type === 'association') {
69
+ // We need to import StraightDottedConnectorShape here or solve the circular dependency
70
+ // For now, assume most are sequence flows in this context
71
+ // TODO: Handle other types if necessary
72
+ } else {
73
+ newShape = new RoundedCornerOrthogonalConnectorShape(points);
74
+ }
75
+
76
+ if (newShape) {
77
+ if (this.geometry) this.geometry.dispose();
78
+ this.geometry = newShape.geometry;
79
+ }
80
+
81
+ // Update label position if the label element already exists
82
+ if (this.label && this.labelElement) {
83
+ this._updateLabelPosition();
84
+ }
85
+ // If a label was requested (setLabel was called) but failed because points
86
+ // were empty at the time (e.g. deferred pending connections in arrange()),
87
+ // retry creating the label now that points are available.
88
+ else if (this.label && !this.labelElement && this.points.length >= 2) {
89
+ this.setLabel(this.label);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Internal helper to update label anchor position without re-creating the Element.
95
+ */
96
+ _updateLabelPosition() {
97
+ if (!this.labelElement || this.points.length < 2) return;
98
+
99
+ // Calculate the longest segment of the connector to place the label
100
+ let longestSegment = { p1: this.points[0], p2: this.points[1], length: 0 };
101
+ for (let i = 1; i < this.points.length; i++) {
102
+ const p1 = this.points[i - 1];
103
+ const p2 = this.points[i];
104
+ const dist = Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
105
+ if (dist > longestSegment.length) {
106
+ longestSegment = { p1, p2, length: dist };
107
+ }
108
+ }
109
+
110
+ const midX = (longestSegment.p1.x + longestSegment.p2.x) / 2;
111
+ const midY = (longestSegment.p1.y + longestSegment.p2.y) / 2;
112
+
113
+ const isVertical = Math.abs(longestSegment.p1.x - longestSegment.p2.x) < 0.01;
114
+ let finalOffsetX = 0;
115
+ let finalOffsetY = 0;
116
+
117
+ if (isVertical) {
118
+ finalOffsetX = 15;
119
+ finalOffsetY = 5;
120
+ } else {
121
+ // Standard offset from setLabel
122
+ finalOffsetY = 20;
123
+ }
124
+
125
+ this.labelElement.positionAt({ x: midX + finalOffsetX, y: midY + finalOffsetY, z: 2 });
126
+ }
127
+
128
+ /**
129
+ * Natively assigns a text label to the connector by automatically calculating the optimal midpoint.
130
+ * @param {string} text - The label text to display
131
+ * @param {number} [offsetY=20] - Vertical offset from the longest line segment
132
+ */
133
+ setLabel(text, offsetY = 20) {
134
+ if (!this.diagram) {
135
+ console.warn("Connector.setLabel called before diagram was set. The label will be added when setDiagram is called.");
136
+ }
137
+
138
+ // Safety guard: need at least 2 points to calculate a segment midpoint.
139
+ // If points aren't available yet (e.g. connector was just created as a deferred
140
+ // pending connection in arrange()), silently store the label text and return.
141
+ // Connector.update() will call setLabel again once points have been computed.
142
+ if (!this.points || this.points.length < 2) {
143
+ this.label = text; // ensure update() retry can find the text
144
+ return this;
145
+ }
146
+
147
+ // Calculate the longest segment of the connector to place the label
148
+ let longestSegment = { p1: this.points[0], p2: this.points[1], length: 0 };
149
+ for (let i = 1; i < this.points.length; i++) {
150
+ const p1 = this.points[i - 1];
151
+ const p2 = this.points[i];
152
+ const dist = Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
153
+ if (dist > longestSegment.length) {
154
+ longestSegment = { p1, p2, length: dist };
155
+ }
156
+ }
157
+
158
+ const midX = (longestSegment.p1.x + longestSegment.p2.x) / 2;
159
+ const midY = (longestSegment.p1.y + longestSegment.p2.y) / 2;
160
+
161
+ const anchorGeo = new CircleShape(1, 1);
162
+ this.labelElement = new Element(`${this.elementId}-label`, anchorGeo);
163
+ this.labelElement.semanticType = 'flow';
164
+ this.labelElement.themable = true;
165
+
166
+ if (this.diagram) {
167
+ this.diagram.addElement(this.labelElement);
168
+ }
169
+
170
+ const isVertical = Math.abs(longestSegment.p1.x - longestSegment.p2.x) < 0.01;
171
+
172
+ let wrapWidth;
173
+ let finalOffsetX = 0;
174
+ let finalOffsetY = 0;
175
+
176
+ if (isVertical) {
177
+ // Text flows horizontally, so we don't constrain width based on the vertical segment length.
178
+ // Give it a safe static width to prevent overflowing adjacent objects
179
+ wrapWidth = 100;
180
+ // Shift the text horizontally to the right so it doesn't straddle the vertical line
181
+ finalOffsetX = 15;
182
+ // Center the text vertically relative to the midpoint
183
+ finalOffsetY = 5;
184
+ } else {
185
+ // For horizontal segments, constrain text to the segment length so it doesn't overhang
186
+ wrapWidth = Math.max(60, Math.min(longestSegment.length * 0.9, 150));
187
+ // Shift the text vertically so it rests above the line
188
+ finalOffsetY = offsetY; // Default is +20
189
+ }
190
+
191
+ this.labelElement.positionAt({ x: midX + finalOffsetX, y: midY + finalOffsetY, z: 2 });
192
+ this.labelElement.visible = false; // Hide the anchor mesh itself
193
+
194
+ // Attach text to the invisible anchor
195
+ this.labelElement.addWrappedText(text, new THREE.Vector3(0, 0, 3), 7, 'center', wrapWidth, 0, 'top');
196
+
197
+ return this;
198
+ }
199
+
200
+ static determinePoints(sourceElement, targetElement, sourcePosition, targetPosition, waypointCoords = null, waypointPorts = null) {
201
+
202
+ // --- WAYPOINT MERGING LOGIC ---
203
+ // If a waypoint is provided (e.g. from an absorbed AnchorPoint), we treat this
204
+ // as two separate routes (Source -> Waypoint, and Waypoint -> Target) and splice them together.
205
+ if (waypointCoords && waypointCoords.length > 0) {
206
+ const anchor = waypointCoords[0];
207
+ const ports = waypointPorts || ['auto', 'auto'];
208
+
209
+ // Support either a static {x, y} coordinate, or an actual Element reference (AnchorPoint)
210
+ let anchorSource, anchorTarget;
211
+
212
+ if (anchor instanceof Element) {
213
+ // If it's a real element, use it directly!
214
+ anchorSource = anchor;
215
+ anchorTarget = anchor;
216
+ } else {
217
+ // Create dummy Element objects for pathfinding to/from the anchor coordinate
218
+ anchorSource = new Element('temp_AnchorSrc', new CircleShape(1, 1));
219
+ anchorSource.positionAt({ x: anchor.x, y: anchor.y, z: 0 });
220
+
221
+ anchorTarget = new Element('temp_AnchorTgt', new CircleShape(1, 1));
222
+ anchorTarget.positionAt({ x: anchor.x, y: anchor.y, z: 0 });
223
+ }
224
+
225
+ // Path 1: Source to Anchor
226
+ // We use the explicit inbound port provided, or fallback to 'auto'
227
+ const path1 = Connector.determinePoints(sourceElement, anchorTarget, sourcePosition, ports[0]);
228
+
229
+ // Path 2: Anchor to Target
230
+ // We use the explicit outbound port provided, or fallback to 'auto'
231
+ const path2 = Connector.determinePoints(anchorSource, targetElement, ports[1], targetPosition);
232
+
233
+ // Merge paths, drop the overlapping exact anchor coordinate point from path2
234
+ const merged = [...path1, ...path2.slice(1)];
235
+
236
+ // Clean up: The two individual paths might have generated identical overlapping points
237
+ // at the handoff coordinate, or created a strictly collinear pass-through.
238
+ // We must filter them or Shape curves will tear.
239
+ const filtered = [merged[0]];
240
+ for (let i = 1; i < merged.length - 1; i++) {
241
+ const prev = merged[i - 1];
242
+ const curr = merged[i];
243
+ const next = merged[i + 1];
244
+
245
+ const collinearH = (Math.abs(prev.y - curr.y) < 0.1 && Math.abs(curr.y - next.y) < 0.1);
246
+ const collinearV = (Math.abs(prev.x - curr.x) < 0.1 && Math.abs(curr.x - next.x) < 0.1);
247
+
248
+ // If the point is NOT collinear with its neighbours, it's a true corner. Keep it.
249
+ if (!collinearH && !collinearV) {
250
+ // Also skip duplicate consecutive points
251
+ if (Math.abs(curr.x - prev.x) > 0.1 || Math.abs(curr.y - prev.y) > 0.1) {
252
+ filtered.push(curr);
253
+ }
254
+ }
255
+ }
256
+ // Ensure the very last target point is included and not a duplicate
257
+ const lastMerged = merged[merged.length - 1];
258
+ const lastFiltered = filtered[filtered.length - 1];
259
+ if (Math.abs(lastFiltered.x - lastMerged.x) > 0.1 || Math.abs(lastFiltered.y - lastMerged.y) > 0.1) {
260
+ filtered.push(lastMerged);
261
+ }
262
+
263
+ return filtered;
264
+ }
265
+
266
+ let waypoints = [];
267
+
268
+ const sourcePoint = sourceElement.getPointPosition(sourcePosition);
269
+ const targetPoint = targetElement.getPointPosition(targetPosition);
270
+
271
+ // Helper to get raw direction from position string (e.g. 'E', 'top-left' -> 'N')
272
+ const getDir = (pos) => {
273
+ if (!pos) return null;
274
+ if (pos.startsWith('E')) return 'E';
275
+ if (pos.startsWith('W')) return 'W';
276
+ if (pos.startsWith('N') || pos.startsWith('top')) return 'N';
277
+ if (pos.startsWith('S') || pos.startsWith('bottom')) return 'S';
278
+ return null;
279
+ };
280
+
281
+ const srcDir = getDir(sourcePosition);
282
+ const tgtDir = getDir(targetPosition);
283
+
284
+ // Define a safe fallback distance for C-curves and routing around elements
285
+ const safeMargin = DiagramDimensions.SAFE_MARGIN;
286
+
287
+ // If either direction is missing, fallback to simple L-curve
288
+ if (!srcDir || !tgtDir) {
289
+ if (sourcePoint.x !== targetPoint.x && sourcePoint.y !== targetPoint.y) {
290
+ if (srcDir === 'W' || srcDir === 'E') waypoints.push({ x: targetPoint.x, y: sourcePoint.y });
291
+ else if (srcDir === 'N' || srcDir === 'S') waypoints.push({ x: sourcePoint.x, y: targetPoint.y });
292
+ else waypoints.push({ x: targetPoint.x, y: sourcePoint.y }); // Default fallback
293
+ }
294
+ return [sourcePoint, ...waypoints, targetPoint];
295
+ }
296
+
297
+ const isHorizontal = (dir) => dir === 'E' || dir === 'W';
298
+ const isVertical = (dir) => dir === 'N' || dir === 'S';
299
+
300
+ if (isHorizontal(srcDir) && isHorizontal(tgtDir)) {
301
+ if (srcDir !== tgtDir) {
302
+ // Opposite Faces (East to West or West to East) -> S-Curve (2 elbows)
303
+ const isNaturallyOrdered = (srcDir === 'E' && sourcePoint.x < targetPoint.x) || (srcDir === 'W' && sourcePoint.x > targetPoint.x);
304
+ if (isNaturallyOrdered) {
305
+ if (Math.abs(sourcePoint.y - targetPoint.y) > 0.1) {
306
+ const midX = (sourcePoint.x + targetPoint.x) / 2;
307
+ waypoints.push({ x: midX, y: sourcePoint.y });
308
+ waypoints.push({ x: midX, y: targetPoint.y });
309
+ }
310
+ } else {
311
+ // U-Curve (4 elbows) via bounding box avoidance
312
+ const topEdgeY = Math.max(
313
+ sourceElement.position.y + sourceElement.getSize().y / 2,
314
+ targetElement.position.y + targetElement.getSize().y / 2
315
+ );
316
+ const avoidY = topEdgeY + safeMargin;
317
+ const margin1 = sourcePoint.x + (srcDir === 'E' ? safeMargin : -safeMargin);
318
+ const margin2 = targetPoint.x + (tgtDir === 'E' ? safeMargin : -safeMargin);
319
+
320
+ waypoints.push({ x: margin1, y: sourcePoint.y });
321
+ waypoints.push({ x: margin1, y: avoidY });
322
+ waypoints.push({ x: margin2, y: avoidY });
323
+ waypoints.push({ x: margin2, y: targetPoint.y });
324
+ }
325
+ } else {
326
+ // Same Faces (East to East or West to West) -> C-Curve (2 elbows)
327
+ // When connecting identical faces, we must route the connector outwards.
328
+ // To avoid cutting through the elements if they are staggered, we must project to the *furthest* edge.
329
+ let marginX;
330
+ if (srcDir === 'E') {
331
+ // Go further East than the rightmost element
332
+ marginX = Math.max(sourcePoint.x, targetPoint.x) + safeMargin;
333
+ } else {
334
+ // Go further West than the leftmost element
335
+ marginX = Math.min(sourcePoint.x, targetPoint.x) - safeMargin;
336
+ }
337
+ waypoints.push({ x: marginX, y: sourcePoint.y });
338
+ waypoints.push({ x: marginX, y: targetPoint.y });
339
+ }
340
+ }
341
+ else if (isVertical(srcDir) && isVertical(tgtDir)) {
342
+ if (srcDir !== tgtDir) {
343
+ // Opposite Faces (North to South or South to North) -> S-Curve (2 elbows)
344
+ // In THREE.js ++Y is North (Up), --Y is South (Down)
345
+ const isNaturallyOrdered = (srcDir === 'N' && sourcePoint.y < targetPoint.y) || (srcDir === 'S' && sourcePoint.y > targetPoint.y);
346
+ if (isNaturallyOrdered) {
347
+ if (Math.abs(sourcePoint.x - targetPoint.x) > 0.1) {
348
+ const midY = (sourcePoint.y + targetPoint.y) / 2;
349
+ waypoints.push({ x: sourcePoint.x, y: midY });
350
+ waypoints.push({ x: targetPoint.x, y: midY });
351
+ }
352
+ } else {
353
+ // U-Curve (4 elbows) via bounding box avoidance
354
+ const rightEdgeX = Math.max(
355
+ sourceElement.position.x + sourceElement.getSize().x / 2,
356
+ targetElement.position.x + targetElement.getSize().x / 2
357
+ );
358
+ const avoidX = rightEdgeX + safeMargin;
359
+ const margin1 = sourcePoint.y + (srcDir === 'N' ? safeMargin : -safeMargin);
360
+ const margin2 = targetPoint.y + (tgtDir === 'N' ? safeMargin : -safeMargin);
361
+
362
+ waypoints.push({ x: sourcePoint.x, y: margin1 });
363
+ waypoints.push({ x: avoidX, y: margin1 });
364
+ waypoints.push({ x: avoidX, y: margin2 });
365
+ waypoints.push({ x: targetPoint.x, y: margin2 });
366
+ }
367
+ } else {
368
+ // Same Faces (North to North or South to South) -> C-Curve (2 elbows)
369
+ let marginY;
370
+ if (srcDir === 'N') {
371
+ // Go further North (Up/Higher +Y) than the highest element
372
+ marginY = Math.max(sourcePoint.y, targetPoint.y) + safeMargin;
373
+ } else {
374
+ // Go further South (Down/Lower -Y) than the lowest element
375
+ marginY = Math.min(sourcePoint.y, targetPoint.y) - safeMargin;
376
+ }
377
+ waypoints.push({ x: sourcePoint.x, y: marginY });
378
+ waypoints.push({ x: targetPoint.x, y: marginY });
379
+ }
380
+ }
381
+ else {
382
+ // Orthogonal Faces (e.g., East to North, West to South, etc.)
383
+ const dx = targetPoint.x - sourcePoint.x;
384
+ const dy = targetPoint.y - sourcePoint.y;
385
+
386
+ // Check if we can use a simple 1-elbow L-curve.
387
+ // A simple L-curve is valid ONLY if it arrives at the target face from the correct direction.
388
+ // Directional rules for sequence flows:
389
+ // - E: Enter from right (moving left) <- Impossible for L-curve starting E or W
390
+ // - W: Enter from left (moving right) <- Impossible for L-curve starting E or W
391
+ // - N: Enter from top (moving down) <- Impossible for L-curve starting N or S
392
+ // - S: Enter from bottom (moving up) <- Impossible for L-curve starting N or S
393
+
394
+ // This means an L-curve is ONLY possible between a Horizontal start and a Vertical end (or vice versa)
395
+ // AND the target must be positioned such that the natural corner doesn't go "through" the target box.
396
+
397
+ let canUseSimpleL = false;
398
+ // If starting Horizontal (E/W), the last segment is Vertical. So it must enter a N or S port.
399
+ if (srcDir === 'E' && dx >= 0) {
400
+ if (tgtDir === 'S' && dy >= 0) canUseSimpleL = true; // Right then Up into South
401
+ if (tgtDir === 'N' && dy <= 0) canUseSimpleL = true; // Right then Down into North
402
+ }
403
+ if (srcDir === 'W' && dx <= 0) {
404
+ if (tgtDir === 'S' && dy >= 0) canUseSimpleL = true; // Left then Up into South
405
+ if (tgtDir === 'N' && dy <= 0) canUseSimpleL = true; // Left then Down into North
406
+ }
407
+ // If starting Vertical (N/S), the last segment is Horizontal. So it must enter an E or W port.
408
+ if (srcDir === 'N' && dy >= 0) {
409
+ if (tgtDir === 'W' && dx >= 0) canUseSimpleL = true; // Up then Right into West
410
+ if (tgtDir === 'E' && dx <= 0) canUseSimpleL = true; // Up then Left into East
411
+ }
412
+ if (srcDir === 'S' && dy <= 0) {
413
+ if (tgtDir === 'W' && dx >= 0) canUseSimpleL = true; // Down then Right into West
414
+ if (tgtDir === 'E' && dx <= 0) canUseSimpleL = true; // Down then Left into East
415
+ }
416
+
417
+ if (canUseSimpleL) {
418
+ // 1 elbow (L-curve)
419
+ if (isHorizontal(srcDir)) waypoints.push({ x: targetPoint.x, y: sourcePoint.y });
420
+ else waypoints.push({ x: sourcePoint.x, y: targetPoint.y });
421
+ } else {
422
+ // Complex orthogonal routing (3 elbows / 4 segments)
423
+ // This ensures we loop around and enter the port from the correct exterior side.
424
+ if (isHorizontal(srcDir)) {
425
+ // Start Horizontally (E/W), must enter Vertically (N/S)
426
+ const marginX = sourcePoint.x + (srcDir === 'E' ? safeMargin : -safeMargin);
427
+ const marginY = targetPoint.y + (tgtDir === 'N' ? safeMargin : -safeMargin);
428
+ waypoints.push({ x: marginX, y: sourcePoint.y });
429
+ waypoints.push({ x: marginX, y: marginY });
430
+ waypoints.push({ x: targetPoint.x, y: marginY });
431
+ } else {
432
+ // Start Vertically (N/S), must enter Horizontally (E/W)
433
+ const marginY = sourcePoint.y + (srcDir === 'N' ? safeMargin : -safeMargin);
434
+ const marginX = targetPoint.x + (tgtDir === 'E' ? safeMargin : -safeMargin);
435
+ waypoints.push({ x: sourcePoint.x, y: marginY });
436
+ waypoints.push({ x: marginX, y: marginY });
437
+ waypoints.push({ x: marginX, y: targetPoint.y });
438
+ }
439
+ }
440
+ }
441
+ // Ensure points are strictly orthogonal. If not, fallback.
442
+ const rawPoints = [sourcePoint, ...waypoints, targetPoint];
443
+ if (rawPoints.length <= 2) return rawPoints;
444
+
445
+ // -----------------------------------------
446
+ // CLEANUP: Filter collinear points
447
+ // RoundedCornerOrthogonalConnectorShape builds a rounded corner at EVERY interior waypoint.
448
+ // Therefore, any collinear "pass-through" points will cause rendering bugs (wobbles, broken geometry).
449
+ // We must ensure the final array only contains points where a 90-degree turn actually happens.
450
+ // -----------------------------------------
451
+ const filteredPoints = [rawPoints[0]]; // Always keep source
452
+
453
+ for (let i = 1; i < rawPoints.length - 1; i++) {
454
+ const prev = rawPoints[i - 1];
455
+ const curr = rawPoints[i];
456
+ const next = rawPoints[i + 1];
457
+
458
+ const collinearH = (Math.abs(prev.y - curr.y) < 0.1 && Math.abs(curr.y - next.y) < 0.1);
459
+ const collinearV = (Math.abs(prev.x - curr.x) < 0.1 && Math.abs(curr.x - next.x) < 0.1);
460
+
461
+ // If the point is NOT collinear with its neighbours, it's a true corner. Keep it.
462
+ if (!collinearH && !collinearV) {
463
+ // Also skip duplicate consecutive points just in case
464
+ if (Math.abs(curr.x - prev.x) > 0.1 || Math.abs(curr.y - prev.y) > 0.1) {
465
+ filteredPoints.push(curr);
466
+ }
467
+ }
468
+ }
469
+
470
+ // Ensure the last target point isn't accidentally removed
471
+ const lastFiltered = filteredPoints[filteredPoints.length - 1];
472
+ if (Math.abs(lastFiltered.x - targetPoint.x) > 0.1 || Math.abs(lastFiltered.y - targetPoint.y) > 0.1) {
473
+ filteredPoints.push(targetPoint);
474
+ }
475
+
476
+ return filteredPoints;
477
+ }
478
+
479
+ }
480
+
47
481
  export { Connector };