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.
- package/lib/components/AureaEdenBpmnDiagram.vue +2 -1
- package/lib/connectors/Connector.js +480 -46
- package/lib/diagrams/Diagram.js +329 -1
- package/lib/diagrams/DiagramConstants.js +2 -1
- package/lib/elements/Element.js +430 -19
- package/lib/notations/{BpmnDiagram.js → bpmn/BpmnDiagram.js} +651 -80
- package/lib/notations/bpmn/BpmnExporter.js +328 -0
- package/lib/notations/bpmn/BpmnToFluentConverter.js +1046 -0
- package/lib/notations/bpmn/CONVERTER_ALGORITHM.md +89 -0
- package/lib/notations/{MyCustomNotationDiagram.js → custom/MyCustomNotationDiagram.js} +5 -5
- package/lib/shapes/connector/RoundedCornerOrthogonalConnectorShape.js +248 -228
- package/lib/shapes/connector/StraightArrowConnectorShape.js +108 -0
- package/lib/shapes/connector/StraightDottedConnectorShape.js +2 -2
- package/lib/shapes/icon/IconShape.js +1 -1
- package/package.json +4 -2
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
this.
|
|
19
|
-
this.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
this.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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 };
|