@bpmnkit/core 0.0.26 → 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/bpmn/auto-layout.js +71 -139
- package/dist/bpmn/bpmn-builder.d.ts +30 -0
- package/dist/bpmn/bpmn-builder.js +343 -31
- package/dist/bpmn/di-check.d.ts +14 -0
- package/dist/bpmn/di-check.js +51 -0
- package/dist/bpmn/svg.js +14 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/layout/annotations.d.ts +27 -0
- package/dist/layout/annotations.js +251 -0
- package/dist/layout/grid/edge-labels.d.ts +8 -0
- package/dist/layout/grid/edge-labels.js +126 -0
- package/dist/layout/grid/flow-graph.d.ts +25 -0
- package/dist/layout/grid/flow-graph.js +99 -0
- package/dist/layout/grid/grid-engine.d.ts +4 -0
- package/dist/layout/grid/grid-engine.js +214 -0
- package/dist/layout/grid/grid-router.d.ts +36 -0
- package/dist/layout/grid/grid-router.js +190 -0
- package/dist/layout/grid/grid.d.ts +43 -0
- package/dist/layout/grid/grid.js +174 -0
- package/dist/layout/grid/walker.d.ts +11 -0
- package/dist/layout/grid/walker.js +126 -0
- package/dist/layout/index.d.ts +2 -5
- package/dist/layout/index.js +1 -4
- package/dist/layout/layout-engine.d.ts +5 -15
- package/dist/layout/layout-engine.js +8 -478
- package/dist/layout/types.d.ts +2 -2
- package/dist/layout/types.js +6 -2
- package/dist/xml/xml-parser.js +5 -0
- package/package.json +1 -1
- package/dist/layout/astar.d.ts +0 -15
- package/dist/layout/astar.js +0 -191
- package/dist/layout/block-builder.d.ts +0 -37
- package/dist/layout/block-builder.js +0 -154
- package/dist/layout/block-layout.d.ts +0 -9
- package/dist/layout/block-layout.js +0 -163
- package/dist/layout/coordinates.d.ts +0 -85
- package/dist/layout/coordinates.js +0 -1392
- package/dist/layout/crossing.d.ts +0 -8
- package/dist/layout/crossing.js +0 -60
- package/dist/layout/graph.d.ts +0 -28
- package/dist/layout/graph.js +0 -126
- package/dist/layout/layers.d.ts +0 -13
- package/dist/layout/layers.js +0 -49
- package/dist/layout/routing.d.ts +0 -33
- package/dist/layout/routing.js +0 -622
- package/dist/layout/subprocess.d.ts +0 -14
- package/dist/layout/subprocess.js +0 -77
package/dist/layout/routing.js
DELETED
|
@@ -1,622 +0,0 @@
|
|
|
1
|
-
import { LABEL_CHAR_WIDTH, LABEL_HEIGHT, LABEL_MIN_WIDTH, LABEL_VERTICAL_OFFSET } from "./types.js";
|
|
2
|
-
const GATEWAY_TYPES = new Set([
|
|
3
|
-
"exclusiveGateway",
|
|
4
|
-
"parallelGateway",
|
|
5
|
-
"inclusiveGateway",
|
|
6
|
-
"eventBasedGateway",
|
|
7
|
-
]);
|
|
8
|
-
/** Tolerance for treating two CY values as "same level" in port decisions. */
|
|
9
|
-
const PORT_SAME_Y_TOLERANCE = 25;
|
|
10
|
-
/**
|
|
11
|
-
* Determine which side of the target a forward edge should connect to.
|
|
12
|
-
* Non-gateway targets always receive edges from the left side.
|
|
13
|
-
* Split gateways (starting): incoming always from the left.
|
|
14
|
-
* Join gateways (closing): incoming based on relative position (top/bottom/left).
|
|
15
|
-
*
|
|
16
|
-
* Uses gridRow (integer row index) when available for exact comparison;
|
|
17
|
-
* falls back to pixel-Y with PORT_SAME_Y_TOLERANCE for nodes without gridRow.
|
|
18
|
-
*/
|
|
19
|
-
export function resolveTargetPort(source, target, joinGateways) {
|
|
20
|
-
if (!GATEWAY_TYPES.has(target.type)) {
|
|
21
|
-
return "left";
|
|
22
|
-
}
|
|
23
|
-
// Split/starting gateways always receive from left
|
|
24
|
-
if (!joinGateways.has(target.id)) {
|
|
25
|
-
return "left";
|
|
26
|
-
}
|
|
27
|
-
// Join/closing gateways: connect based on relative position
|
|
28
|
-
if (source.gridRow !== undefined && target.gridRow !== undefined) {
|
|
29
|
-
if (source.gridRow === target.gridRow)
|
|
30
|
-
return "left";
|
|
31
|
-
return source.gridRow < target.gridRow ? "top" : "bottom";
|
|
32
|
-
}
|
|
33
|
-
// Fallback: pixel-Y comparison with tolerance
|
|
34
|
-
const srcCy = source.bounds.y + source.bounds.height / 2;
|
|
35
|
-
const tgtCy = target.bounds.y + target.bounds.height / 2;
|
|
36
|
-
if (Math.abs(srcCy - tgtCy) <= PORT_SAME_Y_TOLERANCE) {
|
|
37
|
-
return "left";
|
|
38
|
-
}
|
|
39
|
-
return srcCy < tgtCy ? "top" : "bottom";
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Assign source ports for outgoing edges of a gateway.
|
|
43
|
-
* Uses absolute direction: target above → top, below → bottom, same level → right.
|
|
44
|
-
* Single output always exits from the right port.
|
|
45
|
-
*/
|
|
46
|
-
export function assignGatewayPorts(outgoingFlows, nodeMap) {
|
|
47
|
-
const portMap = new Map();
|
|
48
|
-
const count = outgoingFlows.length;
|
|
49
|
-
if (count === 0)
|
|
50
|
-
return portMap;
|
|
51
|
-
if (count === 1) {
|
|
52
|
-
const first = outgoingFlows[0];
|
|
53
|
-
if (first)
|
|
54
|
-
portMap.set(first.id, "right");
|
|
55
|
-
return portMap;
|
|
56
|
-
}
|
|
57
|
-
const firstFlow = outgoingFlows[0];
|
|
58
|
-
if (!firstFlow)
|
|
59
|
-
return portMap;
|
|
60
|
-
const gateway = nodeMap.get(firstFlow.sourceRef);
|
|
61
|
-
if (!gateway)
|
|
62
|
-
return portMap;
|
|
63
|
-
const gatewayCY = gateway.bounds.y + gateway.bounds.height / 2;
|
|
64
|
-
const gatewayGridRow = gateway.gridRow;
|
|
65
|
-
for (const flow of outgoingFlows) {
|
|
66
|
-
const target = nodeMap.get(flow.targetRef);
|
|
67
|
-
if (!target)
|
|
68
|
-
continue;
|
|
69
|
-
let side;
|
|
70
|
-
if (gatewayGridRow !== undefined && target.gridRow !== undefined) {
|
|
71
|
-
// Exact integer row comparison — no tolerance needed
|
|
72
|
-
if (target.gridRow < gatewayGridRow)
|
|
73
|
-
side = "top";
|
|
74
|
-
else if (target.gridRow > gatewayGridRow)
|
|
75
|
-
side = "bottom";
|
|
76
|
-
else
|
|
77
|
-
side = "right";
|
|
78
|
-
}
|
|
79
|
-
else {
|
|
80
|
-
// Fallback: pixel-Y comparison with tolerance
|
|
81
|
-
const targetCY = target.bounds.y + target.bounds.height / 2;
|
|
82
|
-
const dy = targetCY - gatewayCY;
|
|
83
|
-
if (dy < -PORT_SAME_Y_TOLERANCE)
|
|
84
|
-
side = "top";
|
|
85
|
-
else if (dy > PORT_SAME_Y_TOLERANCE)
|
|
86
|
-
side = "bottom";
|
|
87
|
-
else
|
|
88
|
-
side = "right";
|
|
89
|
-
}
|
|
90
|
-
portMap.set(flow.id, side);
|
|
91
|
-
}
|
|
92
|
-
return portMap;
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Route edges with orthogonal (horizontal + vertical) segments.
|
|
96
|
-
* Forward edges go left-to-right; back-edges route above or below.
|
|
97
|
-
* Gateway sources use port-based routing (top/right/bottom).
|
|
98
|
-
*/
|
|
99
|
-
export function routeEdges(sequenceFlows, nodeMap, backEdges) {
|
|
100
|
-
const backEdgeIds = new Set(backEdges.map((be) => be.flowId));
|
|
101
|
-
// Group forward flows by source for gateway port assignment
|
|
102
|
-
const forwardFlowsBySource = new Map();
|
|
103
|
-
const forwardIncomingCount = new Map();
|
|
104
|
-
for (const flow of sequenceFlows) {
|
|
105
|
-
if (backEdgeIds.has(flow.id))
|
|
106
|
-
continue;
|
|
107
|
-
let bucket = forwardFlowsBySource.get(flow.sourceRef);
|
|
108
|
-
if (!bucket) {
|
|
109
|
-
bucket = [];
|
|
110
|
-
forwardFlowsBySource.set(flow.sourceRef, bucket);
|
|
111
|
-
}
|
|
112
|
-
bucket.push(flow);
|
|
113
|
-
forwardIncomingCount.set(flow.targetRef, (forwardIncomingCount.get(flow.targetRef) ?? 0) + 1);
|
|
114
|
-
}
|
|
115
|
-
// Identify join gateways (gateways with multiple incoming forward edges)
|
|
116
|
-
const joinGateways = new Set();
|
|
117
|
-
for (const [targetId, count] of forwardIncomingCount) {
|
|
118
|
-
if (count >= 2) {
|
|
119
|
-
const node = nodeMap.get(targetId);
|
|
120
|
-
if (node && GATEWAY_TYPES.has(node.type)) {
|
|
121
|
-
joinGateways.add(targetId);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
// Assign ports for gateway sources
|
|
126
|
-
const portAssignments = new Map();
|
|
127
|
-
for (const [sourceId, flows] of forwardFlowsBySource) {
|
|
128
|
-
const source = nodeMap.get(sourceId);
|
|
129
|
-
if (!source || !GATEWAY_TYPES.has(source.type))
|
|
130
|
-
continue;
|
|
131
|
-
const ports = assignGatewayPorts(flows, nodeMap);
|
|
132
|
-
for (const [flowId, port] of ports) {
|
|
133
|
-
portAssignments.set(flowId, port);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
const edges = [];
|
|
137
|
-
for (const flow of sequenceFlows) {
|
|
138
|
-
const source = nodeMap.get(flow.sourceRef);
|
|
139
|
-
const target = nodeMap.get(flow.targetRef);
|
|
140
|
-
if (!source || !target)
|
|
141
|
-
continue;
|
|
142
|
-
const isBackEdge = backEdgeIds.has(flow.id);
|
|
143
|
-
let waypoints;
|
|
144
|
-
if (isBackEdge) {
|
|
145
|
-
waypoints = routeBackEdge(source, target, nodeMap);
|
|
146
|
-
}
|
|
147
|
-
else {
|
|
148
|
-
const port = portAssignments.get(flow.id);
|
|
149
|
-
waypoints = port
|
|
150
|
-
? routeFromPort(source, target, port, joinGateways)
|
|
151
|
-
: routeForwardEdge(source, target, joinGateways);
|
|
152
|
-
}
|
|
153
|
-
edges.push({
|
|
154
|
-
id: flow.id,
|
|
155
|
-
sourceRef: flow.sourceRef,
|
|
156
|
-
targetRef: flow.targetRef,
|
|
157
|
-
waypoints,
|
|
158
|
-
label: flow.name,
|
|
159
|
-
labelBounds: undefined,
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
// Resolve edges that cross through intermediate shapes
|
|
163
|
-
resolveEdgeCrossings(edges, nodeMap);
|
|
164
|
-
// Collision-aware label placement
|
|
165
|
-
placeEdgeLabels(edges, nodeMap);
|
|
166
|
-
return edges;
|
|
167
|
-
}
|
|
168
|
-
/** Route a forward edge with orthogonal segments, preferring L-shaped over Z-shaped. */
|
|
169
|
-
function routeForwardEdge(source, target, joinGateways) {
|
|
170
|
-
const targetPort = resolveTargetPort(source, target, joinGateways);
|
|
171
|
-
if (targetPort === "top" || targetPort === "bottom") {
|
|
172
|
-
const sourceRight = source.bounds.x + source.bounds.width;
|
|
173
|
-
const sourceCenterY = source.bounds.y + source.bounds.height / 2;
|
|
174
|
-
const tgtX = target.bounds.x + target.bounds.width / 2;
|
|
175
|
-
const tgtY = targetPort === "top" ? target.bounds.y : target.bounds.y + target.bounds.height;
|
|
176
|
-
return [
|
|
177
|
-
{ x: sourceRight, y: sourceCenterY },
|
|
178
|
-
{ x: tgtX, y: sourceCenterY },
|
|
179
|
-
{ x: tgtX, y: tgtY },
|
|
180
|
-
];
|
|
181
|
-
}
|
|
182
|
-
const sourceRight = source.bounds.x + source.bounds.width;
|
|
183
|
-
const sourceCenterY = source.bounds.y + source.bounds.height / 2;
|
|
184
|
-
const targetLeft = target.bounds.x;
|
|
185
|
-
const targetCenterY = target.bounds.y + target.bounds.height / 2;
|
|
186
|
-
// Same vertical position: straight horizontal line
|
|
187
|
-
if (Math.abs(sourceCenterY - targetCenterY) < 1) {
|
|
188
|
-
return [
|
|
189
|
-
{ x: sourceRight, y: sourceCenterY },
|
|
190
|
-
{ x: targetLeft, y: targetCenterY },
|
|
191
|
-
];
|
|
192
|
-
}
|
|
193
|
-
// Different vertical positions: prefer L-shaped routing
|
|
194
|
-
// L-shape option 1: horizontal to target's X, then vertical down/up
|
|
195
|
-
// L-shape option 2: vertical to target's Y, then horizontal to target
|
|
196
|
-
// For left-to-right flow, option 1 (horizontal first, then vertical into target) is cleaner
|
|
197
|
-
return [
|
|
198
|
-
{ x: sourceRight, y: sourceCenterY },
|
|
199
|
-
{ x: targetLeft, y: sourceCenterY },
|
|
200
|
-
{ x: targetLeft, y: targetCenterY },
|
|
201
|
-
];
|
|
202
|
-
}
|
|
203
|
-
/** Route a forward edge from a specific port side on the source node. */
|
|
204
|
-
function routeFromPort(source, target, port, joinGateways) {
|
|
205
|
-
if (port === "right") {
|
|
206
|
-
return routeForwardEdge(source, target, joinGateways);
|
|
207
|
-
}
|
|
208
|
-
// top/bottom ports are assigned because the target is genuinely above/below —
|
|
209
|
-
// always honour the assigned side rather than falling back to right-exit.
|
|
210
|
-
return routeFromPortDirect(source, target, port, joinGateways);
|
|
211
|
-
}
|
|
212
|
-
/** Route directly from top/bottom port, preferring L-shaped path. */
|
|
213
|
-
function routeFromPortDirect(source, target, port, joinGateways) {
|
|
214
|
-
const targetPort = resolveTargetPort(source, target, joinGateways);
|
|
215
|
-
const srcX = source.bounds.x + source.bounds.width / 2;
|
|
216
|
-
const srcY = port === "top" ? source.bounds.y : source.bounds.y + source.bounds.height;
|
|
217
|
-
if (targetPort === "top" || targetPort === "bottom") {
|
|
218
|
-
const tgtX = target.bounds.x + target.bounds.width / 2;
|
|
219
|
-
const tgtY = targetPort === "top" ? target.bounds.y : target.bounds.y + target.bounds.height;
|
|
220
|
-
if (Math.abs(srcX - tgtX) < 1) {
|
|
221
|
-
return [
|
|
222
|
-
{ x: srcX, y: srcY },
|
|
223
|
-
{ x: tgtX, y: tgtY },
|
|
224
|
-
];
|
|
225
|
-
}
|
|
226
|
-
// L-shape: vertical to target Y, then horizontal to target X
|
|
227
|
-
return [
|
|
228
|
-
{ x: srcX, y: srcY },
|
|
229
|
-
{ x: srcX, y: tgtY },
|
|
230
|
-
{ x: tgtX, y: tgtY },
|
|
231
|
-
];
|
|
232
|
-
}
|
|
233
|
-
const targetLeft = target.bounds.x;
|
|
234
|
-
const targetCenterY = target.bounds.y + target.bounds.height / 2;
|
|
235
|
-
// Same vertical position as target: straight horizontal
|
|
236
|
-
if (Math.abs(srcY - targetCenterY) < 1) {
|
|
237
|
-
return [
|
|
238
|
-
{ x: srcX, y: srcY },
|
|
239
|
-
{ x: targetLeft, y: targetCenterY },
|
|
240
|
-
];
|
|
241
|
-
}
|
|
242
|
-
// L-shape: vertical to target's center-Y, then horizontal to target
|
|
243
|
-
return [
|
|
244
|
-
{ x: srcX, y: srcY },
|
|
245
|
-
{ x: srcX, y: targetCenterY },
|
|
246
|
-
{ x: targetLeft, y: targetCenterY },
|
|
247
|
-
];
|
|
248
|
-
}
|
|
249
|
-
/**
|
|
250
|
-
* Route a back-edge (loop) above or below all nodes, choosing the shorter path.
|
|
251
|
-
* Gateway targets are entered from the right (since back-edges come from the right).
|
|
252
|
-
*/
|
|
253
|
-
function routeBackEdge(source, target, nodeMap) {
|
|
254
|
-
let minY = Number.POSITIVE_INFINITY;
|
|
255
|
-
let maxY = Number.NEGATIVE_INFINITY;
|
|
256
|
-
for (const node of nodeMap.values()) {
|
|
257
|
-
const top = node.bounds.y - (node.labelBounds ? node.labelBounds.height + 8 : 0);
|
|
258
|
-
if (top < minY)
|
|
259
|
-
minY = top;
|
|
260
|
-
const bottom = node.bounds.y + node.bounds.height;
|
|
261
|
-
if (bottom > maxY)
|
|
262
|
-
maxY = bottom;
|
|
263
|
-
}
|
|
264
|
-
const sourceRight = source.bounds.x + source.bounds.width;
|
|
265
|
-
const sourceCenterY = source.bounds.y + source.bounds.height / 2;
|
|
266
|
-
const targetCenterY = target.bounds.y + target.bounds.height / 2;
|
|
267
|
-
// Gateways: enter from right side; non-gateways: enter from left side
|
|
268
|
-
const enterRight = GATEWAY_TYPES.has(target.type);
|
|
269
|
-
const entryX = enterRight ? target.bounds.x + target.bounds.width : target.bounds.x;
|
|
270
|
-
const stemX = enterRight ? entryX + 20 : entryX - 20;
|
|
271
|
-
// Route above
|
|
272
|
-
const routeAboveY = minY - 30;
|
|
273
|
-
const aboveRoute = [
|
|
274
|
-
{ x: sourceRight, y: sourceCenterY },
|
|
275
|
-
{ x: sourceRight + 20, y: sourceCenterY },
|
|
276
|
-
{ x: sourceRight + 20, y: routeAboveY },
|
|
277
|
-
{ x: stemX, y: routeAboveY },
|
|
278
|
-
{ x: stemX, y: targetCenterY },
|
|
279
|
-
{ x: entryX, y: targetCenterY },
|
|
280
|
-
];
|
|
281
|
-
// Route below
|
|
282
|
-
const routeBelowY = maxY + 30;
|
|
283
|
-
const belowRoute = [
|
|
284
|
-
{ x: sourceRight, y: sourceCenterY },
|
|
285
|
-
{ x: sourceRight + 20, y: sourceCenterY },
|
|
286
|
-
{ x: sourceRight + 20, y: routeBelowY },
|
|
287
|
-
{ x: stemX, y: routeBelowY },
|
|
288
|
-
{ x: stemX, y: targetCenterY },
|
|
289
|
-
{ x: entryX, y: targetCenterY },
|
|
290
|
-
];
|
|
291
|
-
// Compare total path length and pick shorter
|
|
292
|
-
const aboveLen = pathLength(aboveRoute);
|
|
293
|
-
const belowLen = pathLength(belowRoute);
|
|
294
|
-
return belowLen < aboveLen ? belowRoute : aboveRoute;
|
|
295
|
-
}
|
|
296
|
-
function pathLength(waypoints) {
|
|
297
|
-
let len = 0;
|
|
298
|
-
for (let i = 1; i < waypoints.length; i++) {
|
|
299
|
-
const a = waypoints[i - 1];
|
|
300
|
-
const b = waypoints[i];
|
|
301
|
-
if (!a || !b)
|
|
302
|
-
continue;
|
|
303
|
-
len += Math.abs(b.x - a.x) + Math.abs(b.y - a.y);
|
|
304
|
-
}
|
|
305
|
-
return len;
|
|
306
|
-
}
|
|
307
|
-
/**
|
|
308
|
-
* Post-process routed edges to avoid crossing through intermediate shapes.
|
|
309
|
-
* For each segment that passes through a shape, adds detour waypoints around it.
|
|
310
|
-
*/
|
|
311
|
-
export function resolveEdgeCrossings(edges, nodeMap) {
|
|
312
|
-
const margin = 20;
|
|
313
|
-
const allShapes = [];
|
|
314
|
-
for (const [id, node] of nodeMap) {
|
|
315
|
-
allShapes.push({
|
|
316
|
-
id,
|
|
317
|
-
x: node.bounds.x,
|
|
318
|
-
y: node.bounds.y,
|
|
319
|
-
right: node.bounds.x + node.bounds.width,
|
|
320
|
-
bottom: node.bounds.y + node.bounds.height,
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
for (const edge of edges) {
|
|
324
|
-
const obstacles = allShapes.filter((s) => s.id !== edge.sourceRef && s.id !== edge.targetRef);
|
|
325
|
-
// Pass 1: Detour around obstacles crossing segments
|
|
326
|
-
for (let pass = 0; pass < 5; pass++) {
|
|
327
|
-
const fixed = fixOneCrossing(edge.waypoints, obstacles, margin);
|
|
328
|
-
if (!fixed)
|
|
329
|
-
break;
|
|
330
|
-
edge.waypoints = collapseCollinear(fixed);
|
|
331
|
-
}
|
|
332
|
-
// Pass 2: Fix corner waypoints that ended up inside obstacles
|
|
333
|
-
edge.waypoints = fixCornersInsideObstacles(edge.waypoints, obstacles, margin);
|
|
334
|
-
edge.waypoints = collapseCollinear(edge.waypoints);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
/**
|
|
338
|
-
* Find the first segment that crosses an obstacle and return a new waypoint
|
|
339
|
-
* array with a detour around it. Returns undefined if no crossing found.
|
|
340
|
-
*/
|
|
341
|
-
function fixOneCrossing(waypoints, obstacles, margin) {
|
|
342
|
-
for (let i = 0; i < waypoints.length - 1; i++) {
|
|
343
|
-
const p1 = waypoints[i];
|
|
344
|
-
const p2 = waypoints[i + 1];
|
|
345
|
-
const crossing = findCrossing(p1, p2, obstacles);
|
|
346
|
-
if (!crossing)
|
|
347
|
-
continue;
|
|
348
|
-
const detour = buildDetour(p1, p2, crossing, margin, obstacles);
|
|
349
|
-
if (!detour)
|
|
350
|
-
continue;
|
|
351
|
-
const result = [...waypoints.slice(0, i + 1), ...detour, ...waypoints.slice(i + 1)];
|
|
352
|
-
return result;
|
|
353
|
-
}
|
|
354
|
-
return undefined;
|
|
355
|
-
}
|
|
356
|
-
/** Find the first obstacle that a segment crosses through (not just touches). */
|
|
357
|
-
function findCrossing(p1, p2, obstacles) {
|
|
358
|
-
const minX = Math.min(p1.x, p2.x);
|
|
359
|
-
const maxX = Math.max(p1.x, p2.x);
|
|
360
|
-
const minY = Math.min(p1.y, p2.y);
|
|
361
|
-
const maxY = Math.max(p1.y, p2.y);
|
|
362
|
-
const shrink = 3;
|
|
363
|
-
for (const obs of obstacles) {
|
|
364
|
-
if (maxX > obs.x + shrink &&
|
|
365
|
-
minX < obs.right - shrink &&
|
|
366
|
-
maxY > obs.y + shrink &&
|
|
367
|
-
minY < obs.bottom - shrink) {
|
|
368
|
-
return obs;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
return undefined;
|
|
372
|
-
}
|
|
373
|
-
/**
|
|
374
|
-
* Build detour waypoints to route around an obstacle.
|
|
375
|
-
* For vertical segments: detour horizontally (left or right).
|
|
376
|
-
* For horizontal segments: detour vertically (above or below).
|
|
377
|
-
*/
|
|
378
|
-
function buildDetour(p1, p2, obs, margin, allObs) {
|
|
379
|
-
const isVertical = Math.abs(p1.x - p2.x) < 1;
|
|
380
|
-
const isHorizontal = Math.abs(p1.y - p2.y) < 1;
|
|
381
|
-
if (isVertical) {
|
|
382
|
-
const x = p1.x;
|
|
383
|
-
const goingDown = p2.y > p1.y;
|
|
384
|
-
const beforeY = goingDown ? obs.y - margin : obs.bottom + margin;
|
|
385
|
-
const afterY = goingDown ? obs.bottom + margin : obs.y - margin;
|
|
386
|
-
// Try both sides; pick the one with fewer new crossings
|
|
387
|
-
const leftX = obs.x - margin;
|
|
388
|
-
const rightX = obs.right + margin;
|
|
389
|
-
const leftCross = countNewCrossings([
|
|
390
|
-
{ x, y: beforeY },
|
|
391
|
-
{ x: leftX, y: beforeY },
|
|
392
|
-
{ x: leftX, y: afterY },
|
|
393
|
-
{ x, y: afterY },
|
|
394
|
-
], allObs);
|
|
395
|
-
const rightCross = countNewCrossings([
|
|
396
|
-
{ x, y: beforeY },
|
|
397
|
-
{ x: rightX, y: beforeY },
|
|
398
|
-
{ x: rightX, y: afterY },
|
|
399
|
-
{ x, y: afterY },
|
|
400
|
-
], allObs);
|
|
401
|
-
const detourX = leftCross <= rightCross ? leftX : rightX;
|
|
402
|
-
return [
|
|
403
|
-
{ x, y: beforeY },
|
|
404
|
-
{ x: detourX, y: beforeY },
|
|
405
|
-
{ x: detourX, y: afterY },
|
|
406
|
-
{ x, y: afterY },
|
|
407
|
-
];
|
|
408
|
-
}
|
|
409
|
-
if (isHorizontal) {
|
|
410
|
-
const y = p1.y;
|
|
411
|
-
const goingRight = p2.x > p1.x;
|
|
412
|
-
const beforeX = goingRight ? obs.x - margin : obs.right + margin;
|
|
413
|
-
const afterX = goingRight ? obs.right + margin : obs.x - margin;
|
|
414
|
-
const aboveY = obs.y - margin;
|
|
415
|
-
const belowY = obs.bottom + margin;
|
|
416
|
-
const aboveCross = countNewCrossings([
|
|
417
|
-
{ x: beforeX, y },
|
|
418
|
-
{ x: beforeX, y: aboveY },
|
|
419
|
-
{ x: afterX, y: aboveY },
|
|
420
|
-
{ x: afterX, y },
|
|
421
|
-
], allObs);
|
|
422
|
-
const belowCross = countNewCrossings([
|
|
423
|
-
{ x: beforeX, y },
|
|
424
|
-
{ x: beforeX, y: belowY },
|
|
425
|
-
{ x: afterX, y: belowY },
|
|
426
|
-
{ x: afterX, y },
|
|
427
|
-
], allObs);
|
|
428
|
-
const detourY = aboveCross <= belowCross ? aboveY : belowY;
|
|
429
|
-
return [
|
|
430
|
-
{ x: beforeX, y },
|
|
431
|
-
{ x: beforeX, y: detourY },
|
|
432
|
-
{ x: afterX, y: detourY },
|
|
433
|
-
{ x: afterX, y },
|
|
434
|
-
];
|
|
435
|
-
}
|
|
436
|
-
// Diagonal segment — skip (shouldn't happen in orthogonal routing)
|
|
437
|
-
return undefined;
|
|
438
|
-
}
|
|
439
|
-
/** Count how many obstacles a set of consecutive segments would cross. */
|
|
440
|
-
function countNewCrossings(points, obstacles) {
|
|
441
|
-
let count = 0;
|
|
442
|
-
for (let i = 0; i < points.length - 1; i++) {
|
|
443
|
-
const a = points[i];
|
|
444
|
-
const b = points[i + 1];
|
|
445
|
-
if (findCrossing(a, b, obstacles))
|
|
446
|
-
count++;
|
|
447
|
-
}
|
|
448
|
-
return count;
|
|
449
|
-
}
|
|
450
|
-
/** Remove collinear intermediate waypoints (same X or same Y in a row). */
|
|
451
|
-
function collapseCollinear(waypoints) {
|
|
452
|
-
if (waypoints.length <= 2)
|
|
453
|
-
return waypoints;
|
|
454
|
-
const result = [waypoints[0]];
|
|
455
|
-
for (let i = 1; i < waypoints.length - 1; i++) {
|
|
456
|
-
const prev = result[result.length - 1];
|
|
457
|
-
const curr = waypoints[i];
|
|
458
|
-
const next = waypoints[i + 1];
|
|
459
|
-
const sameX = Math.abs(prev.x - curr.x) < 0.5 && Math.abs(curr.x - next.x) < 0.5;
|
|
460
|
-
const sameY = Math.abs(prev.y - curr.y) < 0.5 && Math.abs(curr.y - next.y) < 0.5;
|
|
461
|
-
if (sameX || sameY)
|
|
462
|
-
continue;
|
|
463
|
-
result.push(curr);
|
|
464
|
-
}
|
|
465
|
-
result.push(waypoints[waypoints.length - 1]);
|
|
466
|
-
return result;
|
|
467
|
-
}
|
|
468
|
-
function isInsideRect(p, r) {
|
|
469
|
-
return p.x > r.x && p.x < r.right && p.y > r.y && p.y < r.bottom;
|
|
470
|
-
}
|
|
471
|
-
/**
|
|
472
|
-
* Fix corner waypoints that ended up inside obstacles after detours.
|
|
473
|
-
* Moves the corner below/above the obstacle while maintaining orthogonal routing.
|
|
474
|
-
*/
|
|
475
|
-
function fixCornersInsideObstacles(waypoints, obstacles, margin) {
|
|
476
|
-
const result = [...waypoints];
|
|
477
|
-
// Process backwards to maintain indices after splicing
|
|
478
|
-
for (let i = result.length - 2; i >= 1; i--) {
|
|
479
|
-
const wp = result[i];
|
|
480
|
-
const obs = obstacles.find((o) => isInsideRect(wp, o));
|
|
481
|
-
if (!obs)
|
|
482
|
-
continue;
|
|
483
|
-
const prev = result[i - 1];
|
|
484
|
-
const next = result[i + 1];
|
|
485
|
-
const isHorizToVert = Math.abs(prev.y - wp.y) < 1 && Math.abs(wp.x - next.x) < 1;
|
|
486
|
-
const isVertToHoriz = Math.abs(prev.x - wp.x) < 1 && Math.abs(wp.y - next.y) < 1;
|
|
487
|
-
if (isHorizToVert) {
|
|
488
|
-
const newY = next.y > wp.y ? obs.bottom + margin : obs.y - margin;
|
|
489
|
-
result.splice(i, 1, { x: prev.x, y: newY }, { x: wp.x, y: newY });
|
|
490
|
-
}
|
|
491
|
-
else if (isVertToHoriz) {
|
|
492
|
-
const newX = next.x > wp.x ? obs.right + margin : obs.x - margin;
|
|
493
|
-
result.splice(i, 1, { x: newX, y: prev.y }, { x: newX, y: wp.y });
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
return result;
|
|
497
|
-
}
|
|
498
|
-
/** Collision tolerance in pixels — small overlap allowed for rounding. */
|
|
499
|
-
const LABEL_COLLISION_TOLERANCE = 2;
|
|
500
|
-
/** Number of slide steps along a segment when searching for clear space. */
|
|
501
|
-
const LABEL_SLIDE_STEPS = 10;
|
|
502
|
-
function boundsOverlap(a, b) {
|
|
503
|
-
return !(a.x + a.width + LABEL_COLLISION_TOLERANCE <= b.x ||
|
|
504
|
-
b.x + b.width + LABEL_COLLISION_TOLERANCE <= a.x ||
|
|
505
|
-
a.y + a.height + LABEL_COLLISION_TOLERANCE <= b.y ||
|
|
506
|
-
b.y + b.height + LABEL_COLLISION_TOLERANCE <= a.y);
|
|
507
|
-
}
|
|
508
|
-
/**
|
|
509
|
-
* Collision-aware edge label placement.
|
|
510
|
-
* For each labeled edge, generates candidate positions on the longest segment
|
|
511
|
-
* and picks the first one that doesn't overlap nodes or already-placed labels.
|
|
512
|
-
*/
|
|
513
|
-
function placeEdgeLabels(edges, nodeMap) {
|
|
514
|
-
const occupied = [];
|
|
515
|
-
// Collect all node bounds as obstacles
|
|
516
|
-
for (const node of nodeMap.values()) {
|
|
517
|
-
occupied.push(node.bounds);
|
|
518
|
-
if (node.labelBounds)
|
|
519
|
-
occupied.push(node.labelBounds);
|
|
520
|
-
}
|
|
521
|
-
for (const edge of edges) {
|
|
522
|
-
if (!edge.label)
|
|
523
|
-
continue;
|
|
524
|
-
const labelWidth = Math.max(edge.label.length * LABEL_CHAR_WIDTH, LABEL_MIN_WIDTH);
|
|
525
|
-
const labelHeight = LABEL_HEIGHT;
|
|
526
|
-
// Find the longest segment
|
|
527
|
-
const { segStart, segEnd } = findLongestSegment(edge.waypoints);
|
|
528
|
-
// Generate candidate positions along the segment
|
|
529
|
-
const candidates = generateLabelCandidates(segStart, segEnd, labelWidth, labelHeight);
|
|
530
|
-
// Pick the first non-overlapping candidate
|
|
531
|
-
let placed = false;
|
|
532
|
-
for (const candidate of candidates) {
|
|
533
|
-
if (!occupied.some((ob) => boundsOverlap(candidate, ob))) {
|
|
534
|
-
edge.labelBounds = candidate;
|
|
535
|
-
occupied.push(candidate);
|
|
536
|
-
placed = true;
|
|
537
|
-
break;
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
// Fallback: slide along segment to find clear space
|
|
541
|
-
if (!placed) {
|
|
542
|
-
const fallback = slideLabelAlongSegment(segStart, segEnd, labelWidth, labelHeight, occupied);
|
|
543
|
-
if (fallback) {
|
|
544
|
-
edge.labelBounds = fallback;
|
|
545
|
-
occupied.push(fallback);
|
|
546
|
-
}
|
|
547
|
-
// If no clear position exists, leave labelBounds undefined (text preserved in edge.label)
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
function findLongestSegment(waypoints) {
|
|
552
|
-
let bestLen = 0;
|
|
553
|
-
let bestStart = waypoints[0] ?? { x: 0, y: 0 };
|
|
554
|
-
let bestEnd = waypoints[1] ?? waypoints[0] ?? { x: 0, y: 0 };
|
|
555
|
-
for (let i = 1; i < waypoints.length; i++) {
|
|
556
|
-
const a = waypoints[i - 1];
|
|
557
|
-
const b = waypoints[i];
|
|
558
|
-
if (!a || !b)
|
|
559
|
-
continue;
|
|
560
|
-
const len = Math.abs(b.x - a.x) + Math.abs(b.y - a.y);
|
|
561
|
-
if (len > bestLen) {
|
|
562
|
-
bestLen = len;
|
|
563
|
-
bestStart = a;
|
|
564
|
-
bestEnd = b;
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
return { segStart: bestStart, segEnd: bestEnd };
|
|
568
|
-
}
|
|
569
|
-
function generateLabelCandidates(segStart, segEnd, labelWidth, labelHeight) {
|
|
570
|
-
const candidates = [];
|
|
571
|
-
// Positions along segment: 0.5, 0.25, 0.75, 0.33, 0.67
|
|
572
|
-
const fractions = [0.5, 0.25, 0.75, 0.33, 0.67];
|
|
573
|
-
// Perpendicular offsets: above, below
|
|
574
|
-
const offsets = [-LABEL_VERTICAL_OFFSET - labelHeight, LABEL_VERTICAL_OFFSET];
|
|
575
|
-
for (const f of fractions) {
|
|
576
|
-
const px = segStart.x + (segEnd.x - segStart.x) * f;
|
|
577
|
-
const py = segStart.y + (segEnd.y - segStart.y) * f;
|
|
578
|
-
for (const offset of offsets) {
|
|
579
|
-
// Determine perpendicular direction
|
|
580
|
-
const isHorizontal = Math.abs(segEnd.y - segStart.y) < 1;
|
|
581
|
-
let lx;
|
|
582
|
-
let ly;
|
|
583
|
-
if (isHorizontal) {
|
|
584
|
-
lx = px - labelWidth / 2;
|
|
585
|
-
ly = py + offset;
|
|
586
|
-
}
|
|
587
|
-
else {
|
|
588
|
-
lx = px + offset;
|
|
589
|
-
ly = py - labelHeight / 2;
|
|
590
|
-
}
|
|
591
|
-
candidates.push({ x: lx, y: ly, width: labelWidth, height: labelHeight });
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
return candidates;
|
|
595
|
-
}
|
|
596
|
-
function slideLabelAlongSegment(segStart, segEnd, labelWidth, labelHeight, occupied) {
|
|
597
|
-
const isHorizontal = Math.abs(segEnd.y - segStart.y) < 1;
|
|
598
|
-
for (let step = 0; step <= LABEL_SLIDE_STEPS; step++) {
|
|
599
|
-
const t = step / LABEL_SLIDE_STEPS;
|
|
600
|
-
const px = segStart.x + (segEnd.x - segStart.x) * t;
|
|
601
|
-
const py = segStart.y + (segEnd.y - segStart.y) * t;
|
|
602
|
-
const candidate = isHorizontal
|
|
603
|
-
? {
|
|
604
|
-
x: px - labelWidth / 2,
|
|
605
|
-
y: py - labelHeight - LABEL_VERTICAL_OFFSET,
|
|
606
|
-
width: labelWidth,
|
|
607
|
-
height: labelHeight,
|
|
608
|
-
}
|
|
609
|
-
: {
|
|
610
|
-
x: px - labelWidth - LABEL_VERTICAL_OFFSET,
|
|
611
|
-
y: py - labelHeight / 2,
|
|
612
|
-
width: labelWidth,
|
|
613
|
-
height: labelHeight,
|
|
614
|
-
};
|
|
615
|
-
if (!occupied.some((ob) => boundsOverlap(candidate, ob))) {
|
|
616
|
-
return candidate;
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
// No clear position found — skip label placement rather than forcing an overlap
|
|
620
|
-
return undefined;
|
|
621
|
-
}
|
|
622
|
-
//# sourceMappingURL=routing.js.map
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { BpmnFlowElement } from "../bpmn/bpmn-model.js";
|
|
2
|
-
import type { LayoutNode, SubProcessChildResult } from "./types.js";
|
|
3
|
-
/**
|
|
4
|
-
* Check if a node type is a sub-process container.
|
|
5
|
-
*/
|
|
6
|
-
export declare function isSubProcess(type: string): boolean;
|
|
7
|
-
/**
|
|
8
|
-
* Perform recursive layout for sub-process containers.
|
|
9
|
-
* Lays out internal elements in local coordinates, then sizes the
|
|
10
|
-
* sub-process to fit its content with padding, and translates internal
|
|
11
|
-
* elements into the parent coordinate space.
|
|
12
|
-
*/
|
|
13
|
-
export declare function layoutSubProcesses(layoutNodes: LayoutNode[], nodeIndex: Map<string, BpmnFlowElement>): SubProcessChildResult[];
|
|
14
|
-
//# sourceMappingURL=subprocess.d.ts.map
|