@bpmnkit/core 0.1.1 → 0.1.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/README.md +2 -0
- package/dist/bpmn/agentic.d.ts +121 -0
- package/dist/bpmn/agentic.js +97 -0
- package/dist/bpmn/auto-layout.d.ts +5 -5
- package/dist/bpmn/auto-layout.js +592 -36
- package/dist/bpmn/bpmn-builder.d.ts +56 -0
- package/dist/bpmn/bpmn-builder.js +148 -182
- package/dist/bpmn/bpmn-model.d.ts +4 -0
- package/dist/bpmn/bpmn-parser.js +9 -1
- package/dist/bpmn/bpmn-serializer.js +6 -0
- package/dist/bpmn/optimize/agentic.d.ts +10 -0
- package/dist/bpmn/optimize/agentic.js +88 -0
- package/dist/bpmn/optimize/deploy.d.ts +16 -0
- package/dist/bpmn/optimize/deploy.js +143 -0
- package/dist/bpmn/optimize/feel-syntax.d.ts +12 -0
- package/dist/bpmn/optimize/feel-syntax.js +87 -0
- package/dist/bpmn/optimize/feel.js +5 -2
- package/dist/bpmn/optimize/flow.js +22 -2
- package/dist/bpmn/optimize/index.js +20 -9
- package/dist/bpmn/optimize/types.d.ts +10 -1
- package/dist/bpmn/zeebe-extensions.d.ts +27 -0
- package/dist/bpmn/zeebe-extensions.js +38 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +2 -0
- package/dist/layout/annotations.js +36 -1
- package/dist/layout/collaboration/alignment.d.ts +26 -0
- package/dist/layout/collaboration/alignment.js +66 -0
- package/dist/layout/collaboration/ordering.d.ts +21 -0
- package/dist/layout/collaboration/ordering.js +102 -0
- package/dist/layout/index.d.ts +1 -0
- package/dist/layout/layout-engine.d.ts +13 -3
- package/dist/layout/layout-engine.js +9 -4
- package/dist/layout/semantic/bands.d.ts +19 -0
- package/dist/layout/semantic/bands.js +324 -0
- package/dist/layout/semantic/graph.d.ts +29 -0
- package/dist/layout/semantic/graph.js +217 -0
- package/dist/layout/semantic/index.d.ts +13 -0
- package/dist/layout/semantic/index.js +181 -0
- package/dist/layout/semantic/place.d.ts +40 -0
- package/dist/layout/semantic/place.js +271 -0
- package/dist/layout/semantic/route.d.ts +14 -0
- package/dist/layout/semantic/route.js +454 -0
- package/dist/layout/types.d.ts +17 -0
- package/dist/plan/compile.d.ts +39 -0
- package/dist/plan/compile.js +380 -0
- package/dist/plan/extract.d.ts +31 -0
- package/dist/plan/extract.js +248 -0
- package/dist/plan/index.d.ts +6 -0
- package/dist/plan/index.js +5 -0
- package/dist/plan/merge.d.ts +13 -0
- package/dist/plan/merge.js +80 -0
- package/dist/plan/slug.d.ts +5 -0
- package/dist/plan/slug.js +22 -0
- package/dist/plan/types.d.ts +225 -0
- package/dist/plan/types.js +13 -0
- package/package.json +2 -2
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import { BOUNDARY_SIZE, H_GAP } from "./place.js";
|
|
2
|
+
/** Clearance kept between a routed segment and the shapes it passes. */
|
|
3
|
+
const ROUTING_MARGIN = 20;
|
|
4
|
+
/** Stub length before a boundary-event route turns. */
|
|
5
|
+
const BOUNDARY_STEM = 20;
|
|
6
|
+
/** Shapes may be grazed by this much before a route counts as blocked. */
|
|
7
|
+
const HIT_TOLERANCE = 2;
|
|
8
|
+
/** Crossings a direct route may make before a detour is considered instead. */
|
|
9
|
+
const CROSSING_TOLERANCE = 0;
|
|
10
|
+
/** How many crossings going around has to save to be worth the extra bends. */
|
|
11
|
+
const DETOUR_PENALTY = 0;
|
|
12
|
+
/** Vertical spacing between two detours sharing the same stretch of diagram. */
|
|
13
|
+
const CORRIDOR_SPACING = 20;
|
|
14
|
+
/** How many lanes deep a corridor may stack before routes are allowed to share. */
|
|
15
|
+
const CORRIDOR_LANES = 4;
|
|
16
|
+
function centre(b) {
|
|
17
|
+
return { x: b.x + b.width / 2, y: b.y + b.height / 2 };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Route every sequence flow orthogonally.
|
|
21
|
+
*
|
|
22
|
+
* Each edge proposes candidate polylines from most to least desirable and takes
|
|
23
|
+
* the first one that clears every unrelated shape: a straight run along the
|
|
24
|
+
* spine, a turn out of the source's top or bottom for a branch, and a turn in
|
|
25
|
+
* the empty gutter in front of the target's column as the general fallback.
|
|
26
|
+
*/
|
|
27
|
+
export function routeFlows(graph, flows, bounds, bandLayout, gutterX) {
|
|
28
|
+
const ctx = { graph, bounds, gutterX, reserved: [], routed: [] };
|
|
29
|
+
const routed = new Map();
|
|
30
|
+
const pending = [];
|
|
31
|
+
for (const flow of flows) {
|
|
32
|
+
const source = bounds.get(flow.sourceRef);
|
|
33
|
+
const target = bounds.get(flow.targetRef);
|
|
34
|
+
if (!source || !target)
|
|
35
|
+
continue;
|
|
36
|
+
const fromBoundary = graph.byId.get(flow.sourceRef)?.type === "boundaryEvent";
|
|
37
|
+
const sourceRank = graph.ranks.get(fromBoundary ? hostOf(graph, flow.sourceRef) : flow.sourceRef);
|
|
38
|
+
const targetRank = graph.ranks.get(flow.targetRef);
|
|
39
|
+
const isBackEdge = graph.backEdges.has(flow.id);
|
|
40
|
+
const direct = isBackEdge
|
|
41
|
+
? []
|
|
42
|
+
: fromBoundary
|
|
43
|
+
? fromBoundaryCandidates(ctx, source, target, targetRank)
|
|
44
|
+
: forwardCandidates(ctx, source, target, sourceRank, targetRank);
|
|
45
|
+
const clear = pick(direct, ctx, flow.sourceRef, flow.targetRef);
|
|
46
|
+
if (clear && clear.crossings <= CROSSING_TOLERANCE) {
|
|
47
|
+
commit(ctx, routed, flow.id, clear.waypoints);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
pending.push({
|
|
51
|
+
flow,
|
|
52
|
+
source,
|
|
53
|
+
target,
|
|
54
|
+
sourceRank,
|
|
55
|
+
targetRank,
|
|
56
|
+
below: isBackEdge,
|
|
57
|
+
span: Math.abs(target.x - source.x),
|
|
58
|
+
direct: clear,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
// Widest detours claim the outermost corridors, so long routes nest around
|
|
62
|
+
// short ones instead of crossing them.
|
|
63
|
+
pending.sort((a, b) => b.span - a.span);
|
|
64
|
+
for (const item of pending) {
|
|
65
|
+
const around = detour(ctx, item.source, item.target, item.sourceRank, item.targetRank, item.below, item.flow.sourceRef, item.flow.targetRef);
|
|
66
|
+
// Going around costs bends and length, so it has to save more than one
|
|
67
|
+
// crossing to be worth taking.
|
|
68
|
+
const cost = crossingCount(around, ctx.routed) + DETOUR_PENALTY;
|
|
69
|
+
const waypoints = item.direct && item.direct.crossings <= cost ? item.direct.waypoints : around;
|
|
70
|
+
commit(ctx, routed, item.flow.id, waypoints);
|
|
71
|
+
}
|
|
72
|
+
// Emit in declaration order, whichever pass produced the route.
|
|
73
|
+
const edges = [];
|
|
74
|
+
for (const flow of flows) {
|
|
75
|
+
const waypoints = routed.get(flow.id);
|
|
76
|
+
if (!waypoints)
|
|
77
|
+
continue;
|
|
78
|
+
const edge = {
|
|
79
|
+
id: flow.id,
|
|
80
|
+
sourceRef: flow.sourceRef,
|
|
81
|
+
targetRef: flow.targetRef,
|
|
82
|
+
waypoints: collapse(waypoints),
|
|
83
|
+
};
|
|
84
|
+
if (flow.name)
|
|
85
|
+
edge.label = flow.name;
|
|
86
|
+
edges.push(edge);
|
|
87
|
+
}
|
|
88
|
+
return edges;
|
|
89
|
+
}
|
|
90
|
+
/** Record a chosen route so later edges can steer around it. */
|
|
91
|
+
function commit(ctx, routed, id, waypoints) {
|
|
92
|
+
routed.set(id, waypoints);
|
|
93
|
+
for (let i = 0; i + 1 < waypoints.length; i++) {
|
|
94
|
+
const a = waypoints[i];
|
|
95
|
+
const b = waypoints[i + 1];
|
|
96
|
+
if (a && b)
|
|
97
|
+
ctx.routed.push([a, b]);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function hostOf(graph, eventId) {
|
|
101
|
+
for (const [hostId, events] of graph.attachers) {
|
|
102
|
+
if (events.some((e) => e.id === eventId))
|
|
103
|
+
return hostId;
|
|
104
|
+
}
|
|
105
|
+
return eventId;
|
|
106
|
+
}
|
|
107
|
+
/** First candidate that crosses no shape other than its own endpoints. */
|
|
108
|
+
function pick(candidates, ctx, sourceId, targetId) {
|
|
109
|
+
const obstacles = [];
|
|
110
|
+
for (const [id, b] of ctx.bounds) {
|
|
111
|
+
if (id === sourceId || id === targetId)
|
|
112
|
+
continue;
|
|
113
|
+
// An expanded container legitimately holds its children's routes.
|
|
114
|
+
obstacles.push(b);
|
|
115
|
+
}
|
|
116
|
+
let best = null;
|
|
117
|
+
for (const candidate of candidates) {
|
|
118
|
+
if (blocked(candidate, obstacles))
|
|
119
|
+
continue;
|
|
120
|
+
const crossings = crossingCount(candidate, ctx.routed);
|
|
121
|
+
if (crossings === 0)
|
|
122
|
+
return { waypoints: candidate, crossings };
|
|
123
|
+
if (!best || crossings < best.crossings)
|
|
124
|
+
best = { waypoints: candidate, crossings };
|
|
125
|
+
}
|
|
126
|
+
return best;
|
|
127
|
+
}
|
|
128
|
+
/** Proper intersection between two segments — touching endpoints do not count. */
|
|
129
|
+
function intersects(p, q, r, t) {
|
|
130
|
+
const side = (o, a, b) => Math.sign((a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x));
|
|
131
|
+
return side(p, q, r) !== side(p, q, t) && side(r, t, p) !== side(r, t, q);
|
|
132
|
+
}
|
|
133
|
+
/** How many already-routed edges a candidate would cut across. */
|
|
134
|
+
function crossingCount(waypoints, routed) {
|
|
135
|
+
let count = 0;
|
|
136
|
+
for (let i = 0; i + 1 < waypoints.length; i++) {
|
|
137
|
+
const a = waypoints[i];
|
|
138
|
+
const b = waypoints[i + 1];
|
|
139
|
+
if (!a || !b)
|
|
140
|
+
continue;
|
|
141
|
+
for (const [c, d] of routed)
|
|
142
|
+
if (intersects(a, b, c, d))
|
|
143
|
+
count++;
|
|
144
|
+
}
|
|
145
|
+
return count;
|
|
146
|
+
}
|
|
147
|
+
/** How many shapes a route passes through. */
|
|
148
|
+
function hitCount(waypoints, obstacles) {
|
|
149
|
+
let count = 0;
|
|
150
|
+
for (const o of obstacles)
|
|
151
|
+
if (blocked(waypoints, [o]))
|
|
152
|
+
count++;
|
|
153
|
+
return count;
|
|
154
|
+
}
|
|
155
|
+
function blocked(waypoints, obstacles) {
|
|
156
|
+
for (let i = 0; i + 1 < waypoints.length; i++) {
|
|
157
|
+
const a = waypoints[i];
|
|
158
|
+
const b = waypoints[i + 1];
|
|
159
|
+
if (!a || !b)
|
|
160
|
+
continue;
|
|
161
|
+
const minX = Math.min(a.x, b.x) + HIT_TOLERANCE;
|
|
162
|
+
const maxX = Math.max(a.x, b.x) - HIT_TOLERANCE;
|
|
163
|
+
const minY = Math.min(a.y, b.y) + HIT_TOLERANCE;
|
|
164
|
+
const maxY = Math.max(a.y, b.y) - HIT_TOLERANCE;
|
|
165
|
+
for (const o of obstacles) {
|
|
166
|
+
if (maxX <= o.x || o.x + o.width <= minX)
|
|
167
|
+
continue;
|
|
168
|
+
if (maxY <= o.y || o.y + o.height <= minY)
|
|
169
|
+
continue;
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Candidates for a normal forward flow, best first: a straight spine segment,
|
|
177
|
+
* a branch leaving through the source's top or bottom, then the gutter turn.
|
|
178
|
+
*/
|
|
179
|
+
function forwardCandidates(ctx, source, target, sourceRank, targetRank) {
|
|
180
|
+
const from = centre(source);
|
|
181
|
+
const to = centre(target);
|
|
182
|
+
const rightOf = target.x >= source.x + source.width;
|
|
183
|
+
if (Math.abs(from.y - to.y) < 1 && rightOf) {
|
|
184
|
+
return [
|
|
185
|
+
[
|
|
186
|
+
{ x: source.x + source.width, y: from.y },
|
|
187
|
+
{ x: target.x, y: to.y },
|
|
188
|
+
],
|
|
189
|
+
];
|
|
190
|
+
}
|
|
191
|
+
// Same column, different band: drop straight between the two.
|
|
192
|
+
if (sourceRank !== undefined && sourceRank === targetRank) {
|
|
193
|
+
const down = to.y > from.y;
|
|
194
|
+
return [
|
|
195
|
+
[
|
|
196
|
+
{ x: from.x, y: down ? source.y + source.height : source.y },
|
|
197
|
+
{ x: to.x, y: down ? target.y : target.y + target.height },
|
|
198
|
+
],
|
|
199
|
+
];
|
|
200
|
+
}
|
|
201
|
+
if (!rightOf)
|
|
202
|
+
return [];
|
|
203
|
+
const down = to.y > from.y;
|
|
204
|
+
const gutter = ctx.gutterX.get(targetRank ?? 0) ?? target.x - H_GAP / 2;
|
|
205
|
+
const behind = sourceRank !== undefined
|
|
206
|
+
? (ctx.gutterX.get(sourceRank + 1) ?? source.x + source.width + H_GAP / 2)
|
|
207
|
+
: source.x + source.width + H_GAP / 2;
|
|
208
|
+
const candidates = [
|
|
209
|
+
// Branch: leave through the side facing the target band, then run in.
|
|
210
|
+
[
|
|
211
|
+
{ x: from.x, y: down ? source.y + source.height : source.y },
|
|
212
|
+
{ x: from.x, y: to.y },
|
|
213
|
+
{ x: target.x, y: to.y },
|
|
214
|
+
],
|
|
215
|
+
// Gutter turn in front of the target: out of the right edge, across, in.
|
|
216
|
+
[
|
|
217
|
+
{ x: source.x + source.width, y: from.y },
|
|
218
|
+
{ x: gutter, y: from.y },
|
|
219
|
+
{ x: gutter, y: to.y },
|
|
220
|
+
{ x: target.x, y: to.y },
|
|
221
|
+
],
|
|
222
|
+
];
|
|
223
|
+
// Turning in the gutter right behind the source instead reaches the target
|
|
224
|
+
// band early; which of the two crosses less depends on what is already
|
|
225
|
+
// routed, so both are offered.
|
|
226
|
+
if (Math.abs(behind - gutter) > 1) {
|
|
227
|
+
candidates.push([
|
|
228
|
+
{ x: source.x + source.width, y: from.y },
|
|
229
|
+
{ x: behind, y: from.y },
|
|
230
|
+
{ x: behind, y: to.y },
|
|
231
|
+
{ x: target.x, y: to.y },
|
|
232
|
+
]);
|
|
233
|
+
}
|
|
234
|
+
return candidates;
|
|
235
|
+
}
|
|
236
|
+
/** A boundary event leaves through its outward side, never into its host. */
|
|
237
|
+
function fromBoundaryCandidates(ctx, source, target, targetRank) {
|
|
238
|
+
const from = centre(source);
|
|
239
|
+
const to = centre(target);
|
|
240
|
+
const down = to.y >= from.y;
|
|
241
|
+
const exitY = down ? source.y + source.height : source.y;
|
|
242
|
+
const stem = down ? exitY + BOUNDARY_STEM : exitY - BOUNDARY_STEM;
|
|
243
|
+
if (Math.abs(from.x - to.x) < 1) {
|
|
244
|
+
return [
|
|
245
|
+
[
|
|
246
|
+
{ x: from.x, y: exitY },
|
|
247
|
+
{ x: to.x, y: down ? target.y : target.y + target.height },
|
|
248
|
+
],
|
|
249
|
+
];
|
|
250
|
+
}
|
|
251
|
+
if (target.x >= from.x) {
|
|
252
|
+
const gutter = ctx.gutterX.get(targetRank ?? 0) ?? target.x - H_GAP / 2;
|
|
253
|
+
return [
|
|
254
|
+
[
|
|
255
|
+
{ x: from.x, y: exitY },
|
|
256
|
+
{ x: from.x, y: to.y },
|
|
257
|
+
{ x: target.x, y: to.y },
|
|
258
|
+
],
|
|
259
|
+
[
|
|
260
|
+
{ x: from.x, y: exitY },
|
|
261
|
+
{ x: from.x, y: stem },
|
|
262
|
+
{ x: gutter, y: stem },
|
|
263
|
+
{ x: gutter, y: to.y },
|
|
264
|
+
{ x: target.x, y: to.y },
|
|
265
|
+
],
|
|
266
|
+
];
|
|
267
|
+
}
|
|
268
|
+
return [
|
|
269
|
+
[
|
|
270
|
+
{ x: from.x, y: exitY },
|
|
271
|
+
{ x: from.x, y: stem },
|
|
272
|
+
{ x: target.x + target.width, y: stem },
|
|
273
|
+
{ x: target.x + target.width, y: to.y },
|
|
274
|
+
],
|
|
275
|
+
];
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* The universal fallback: leave through the gutter behind the source, cross in
|
|
279
|
+
* the nearest horizontal corridor that clears every shape between the two
|
|
280
|
+
* columns, and come back through the gutter in front of the target. Used for
|
|
281
|
+
* feedback edges and for any forward edge whose direct routes are blocked.
|
|
282
|
+
*/
|
|
283
|
+
function detour(ctx, source, target, sourceRank, targetRank,
|
|
284
|
+
/** A loop reads as a loop when it runs clear of the flow it repeats. */
|
|
285
|
+
isLoop = false, sourceId = "", targetId = "") {
|
|
286
|
+
const from = centre(source);
|
|
287
|
+
const to = centre(target);
|
|
288
|
+
const dropX = sourceRank !== undefined
|
|
289
|
+
? (ctx.gutterX.get(sourceRank + 1) ?? source.x + source.width + H_GAP / 2)
|
|
290
|
+
: source.x + source.width + H_GAP / 2;
|
|
291
|
+
const riseX = ctx.gutterX.get(targetRank ?? 0) ?? target.x - H_GAP / 2;
|
|
292
|
+
const left = Math.min(dropX, riseX, from.x, to.x);
|
|
293
|
+
const right = Math.max(dropX, riseX, from.x, to.x);
|
|
294
|
+
// A loop leaves the band it repeats: below by convention, but above when the
|
|
295
|
+
// space below is busy and the space above is not. Anything else takes the
|
|
296
|
+
// nearest clear corridor, whichever side that is on.
|
|
297
|
+
const middle = (from.y + to.y) / 2;
|
|
298
|
+
const bottom = Math.max(source.y + source.height, target.y + target.height);
|
|
299
|
+
const top = Math.min(source.y, target.y);
|
|
300
|
+
const options = isLoop
|
|
301
|
+
? [
|
|
302
|
+
corridor(ctx, left, right, middle, bottom),
|
|
303
|
+
corridor(ctx, left, right, middle, undefined, top),
|
|
304
|
+
]
|
|
305
|
+
: [corridor(ctx, left, right, middle)];
|
|
306
|
+
let base = options[0] ?? middle;
|
|
307
|
+
if (isLoop) {
|
|
308
|
+
let fewest = Number.POSITIVE_INFINITY;
|
|
309
|
+
for (const option of options) {
|
|
310
|
+
const crossings = crossingCount([
|
|
311
|
+
{ x: left, y: option },
|
|
312
|
+
{ x: right, y: option },
|
|
313
|
+
], ctx.routed);
|
|
314
|
+
if (crossings < fewest) {
|
|
315
|
+
fewest = crossings;
|
|
316
|
+
base = option;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const goesDown = base > middle;
|
|
321
|
+
const corridorY = reserve(ctx, left, right, base, goesDown ? 1 : -1);
|
|
322
|
+
const exitY = goesDown ? source.y + source.height : source.y;
|
|
323
|
+
const entryY = goesDown ? target.y + target.height : target.y;
|
|
324
|
+
// Leaving through the source's own top or bottom keeps the route out of the
|
|
325
|
+
// horizontal corridor its neighbours flow along; the gutter variant is the
|
|
326
|
+
// fallback for when that vertical is occupied.
|
|
327
|
+
const straightOut = [
|
|
328
|
+
{ x: from.x, y: exitY },
|
|
329
|
+
{ x: from.x, y: corridorY },
|
|
330
|
+
{ x: to.x, y: corridorY },
|
|
331
|
+
{ x: to.x, y: entryY },
|
|
332
|
+
];
|
|
333
|
+
const viaGutter = [
|
|
334
|
+
{ x: source.x + source.width, y: from.y },
|
|
335
|
+
{ x: dropX, y: from.y },
|
|
336
|
+
{ x: dropX, y: corridorY },
|
|
337
|
+
{ x: riseX, y: corridorY },
|
|
338
|
+
{ x: riseX, y: to.y },
|
|
339
|
+
{ x: target.x, y: to.y },
|
|
340
|
+
];
|
|
341
|
+
const chosen = pick([straightOut, viaGutter], ctx, sourceId, targetId);
|
|
342
|
+
if (chosen)
|
|
343
|
+
return chosen.waypoints;
|
|
344
|
+
// Neither is clear: keep whichever grazes fewer shapes.
|
|
345
|
+
const obstacles = [];
|
|
346
|
+
for (const [id, b] of ctx.bounds) {
|
|
347
|
+
if (id !== sourceId && id !== targetId)
|
|
348
|
+
obstacles.push(b);
|
|
349
|
+
}
|
|
350
|
+
return hitCount(straightOut, obstacles) <= hitCount(viaGutter, obstacles)
|
|
351
|
+
? straightOut
|
|
352
|
+
: viaGutter;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Keep parallel detours apart: step away from the flow until this stretch of
|
|
356
|
+
* corridor is free, so two long routes stack instead of merging into one line.
|
|
357
|
+
*/
|
|
358
|
+
function reserve(ctx, left, right, base, direction) {
|
|
359
|
+
const hitsShape = (candidate) => [...ctx.bounds.values()].some((b) => b.x + b.width > left &&
|
|
360
|
+
b.x < right &&
|
|
361
|
+
b.y - HIT_TOLERANCE < candidate &&
|
|
362
|
+
candidate < b.y + b.height + HIT_TOLERANCE);
|
|
363
|
+
let y = base;
|
|
364
|
+
let free = base;
|
|
365
|
+
for (let lane = 0; lane < CORRIDOR_LANES; lane++) {
|
|
366
|
+
if (!hitsShape(y)) {
|
|
367
|
+
free = y;
|
|
368
|
+
const taken = ctx.reserved.some((r) => r.left < right && left < r.right && Math.abs(r.y - y) < CORRIDOR_SPACING);
|
|
369
|
+
if (!taken)
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
y += direction * CORRIDOR_SPACING;
|
|
373
|
+
}
|
|
374
|
+
// Sharing a corridor with another route is a lesser evil than running the
|
|
375
|
+
// route through a shape, so fall back to the last line that was clear.
|
|
376
|
+
if (hitsShape(y))
|
|
377
|
+
y = free;
|
|
378
|
+
ctx.reserved.push({ left, right, y });
|
|
379
|
+
return y;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* A horizontal line between `left` and `right` that misses every shape, chosen
|
|
383
|
+
* as close to `preferredY` as possible so detours stay local instead of
|
|
384
|
+
* sweeping around the whole diagram.
|
|
385
|
+
*/
|
|
386
|
+
function corridor(ctx, left, right, preferredY,
|
|
387
|
+
/** Only consider corridors below this line. */
|
|
388
|
+
floor,
|
|
389
|
+
/** Only consider corridors above this line. */
|
|
390
|
+
ceiling) {
|
|
391
|
+
const spans = [];
|
|
392
|
+
for (const b of ctx.bounds.values()) {
|
|
393
|
+
if (b.x + b.width > left && b.x < right) {
|
|
394
|
+
spans.push([b.y - ROUTING_MARGIN, b.y + b.height + ROUTING_MARGIN]);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (spans.length === 0)
|
|
398
|
+
return preferredY;
|
|
399
|
+
spans.sort((a, b) => a[0] - b[0]);
|
|
400
|
+
// Merge the blocked bands, then take the gap centre nearest the preference.
|
|
401
|
+
const merged = [];
|
|
402
|
+
for (const span of spans) {
|
|
403
|
+
const last = merged[merged.length - 1];
|
|
404
|
+
if (last && span[0] <= last[1])
|
|
405
|
+
last[1] = Math.max(last[1], span[1]);
|
|
406
|
+
else
|
|
407
|
+
merged.push([span[0], span[1]]);
|
|
408
|
+
}
|
|
409
|
+
const options = [];
|
|
410
|
+
const first = merged[0];
|
|
411
|
+
const last = merged[merged.length - 1];
|
|
412
|
+
if (first)
|
|
413
|
+
options.push(first[0] - ROUTING_MARGIN);
|
|
414
|
+
if (last)
|
|
415
|
+
options.push(last[1] + ROUTING_MARGIN);
|
|
416
|
+
for (let i = 0; i + 1 < merged.length; i++) {
|
|
417
|
+
const a = merged[i];
|
|
418
|
+
const b = merged[i + 1];
|
|
419
|
+
if (a && b)
|
|
420
|
+
options.push((a[1] + b[0]) / 2);
|
|
421
|
+
}
|
|
422
|
+
let allowed = floor === undefined ? options : options.filter((y) => y > floor);
|
|
423
|
+
if (ceiling !== undefined)
|
|
424
|
+
allowed = allowed.filter((y) => y < ceiling);
|
|
425
|
+
const usable = allowed.length > 0 ? allowed : options;
|
|
426
|
+
let best = usable[0] ?? preferredY;
|
|
427
|
+
for (const option of usable) {
|
|
428
|
+
if (Math.abs(option - preferredY) < Math.abs(best - preferredY))
|
|
429
|
+
best = option;
|
|
430
|
+
}
|
|
431
|
+
return best;
|
|
432
|
+
}
|
|
433
|
+
/** Drop duplicate points and mid-points that sit on a straight run. */
|
|
434
|
+
function collapse(waypoints) {
|
|
435
|
+
const out = [];
|
|
436
|
+
for (const wp of waypoints) {
|
|
437
|
+
const last = out[out.length - 1];
|
|
438
|
+
if (last && last.x === wp.x && last.y === wp.y)
|
|
439
|
+
continue;
|
|
440
|
+
out.push(wp);
|
|
441
|
+
}
|
|
442
|
+
for (let i = out.length - 2; i > 0; i--) {
|
|
443
|
+
const prev = out[i - 1];
|
|
444
|
+
const cur = out[i];
|
|
445
|
+
const next = out[i + 1];
|
|
446
|
+
if (!prev || !cur || !next)
|
|
447
|
+
continue;
|
|
448
|
+
const collinear = (prev.x === cur.x && cur.x === next.x) || (prev.y === cur.y && cur.y === next.y);
|
|
449
|
+
if (collinear)
|
|
450
|
+
out.splice(i, 1);
|
|
451
|
+
}
|
|
452
|
+
return out;
|
|
453
|
+
}
|
|
454
|
+
//# sourceMappingURL=route.js.map
|
package/dist/layout/types.d.ts
CHANGED
|
@@ -74,5 +74,22 @@ export interface SubProcessChildResult {
|
|
|
74
74
|
export interface LayoutResult {
|
|
75
75
|
nodes: LayoutNode[];
|
|
76
76
|
edges: LayoutEdge[];
|
|
77
|
+
/**
|
|
78
|
+
* Lane bands in top-to-bottom order, when the engine placed nodes by lane
|
|
79
|
+
* membership. Absent when the scope has no lanes.
|
|
80
|
+
*/
|
|
81
|
+
lanes?: Array<{
|
|
82
|
+
id: string;
|
|
83
|
+
bounds: Bounds;
|
|
84
|
+
}>;
|
|
85
|
+
/**
|
|
86
|
+
* Layouts that belong on their own plane rather than on this one: the
|
|
87
|
+
* contents of each collapsed sub-process, laid out at the origin. Nested
|
|
88
|
+
* collapsed scopes are flattened into this list.
|
|
89
|
+
*/
|
|
90
|
+
planes?: Array<{
|
|
91
|
+
elementId: string;
|
|
92
|
+
result: LayoutResult;
|
|
93
|
+
}>;
|
|
77
94
|
}
|
|
78
95
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { AdHocSubProcessOptions, BoundaryEventOptions, IntermediateCatchEventOptions, ServiceTaskOptions, StartEventOptions } from "../bpmn/bpmn-builder.js";
|
|
2
|
+
import type { BpmnDefinitions } from "../bpmn/bpmn-model.js";
|
|
3
|
+
import type { ProcessPlan } from "./types.js";
|
|
4
|
+
export interface PlanProblem {
|
|
5
|
+
/** A path into the plan, e.g. "steps[2].branches[0].condition", for pinpointing the offending field. */
|
|
6
|
+
path: string;
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
export interface SynthResult {
|
|
10
|
+
defs?: BpmnDefinitions;
|
|
11
|
+
xml?: string;
|
|
12
|
+
problems: PlanProblem[];
|
|
13
|
+
}
|
|
14
|
+
/** What a connector template resolves to — structurally matches `@bpmnkit/connectors`' `ApplyResult`. */
|
|
15
|
+
export interface ConnectorApplyResult {
|
|
16
|
+
serviceTask?: ServiceTaskOptions;
|
|
17
|
+
adHocSubProcess?: Partial<AdHocSubProcessOptions>;
|
|
18
|
+
startEvent?: Partial<StartEventOptions>;
|
|
19
|
+
boundaryEvent?: Partial<BoundaryEventOptions>;
|
|
20
|
+
intermediateEvent?: Partial<IntermediateCatchEventOptions>;
|
|
21
|
+
problems: Array<{
|
|
22
|
+
key?: string;
|
|
23
|
+
message: string;
|
|
24
|
+
}>;
|
|
25
|
+
}
|
|
26
|
+
export type ConnectorResolver = (templateId: string, values: Record<string, string>) => ConnectorApplyResult;
|
|
27
|
+
export interface CompilePlanOptions {
|
|
28
|
+
/** Resolves `PlanConnectorRef`s — pass `applyConnectorTemplate` from `@bpmnkit/connectors`. Without it, connector steps produce a problem. */
|
|
29
|
+
resolveConnector?: ConnectorResolver;
|
|
30
|
+
/** Skip auto-applying safe optimizer fixes (default false). */
|
|
31
|
+
skipAutoFix?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Compiles a `ProcessPlan` into laid-out, validated BPMN XML. Deterministic:
|
|
35
|
+
* the same plan always produces the same XML. Problems are collected, not
|
|
36
|
+
* thrown — check `result.problems` before using `result.xml`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function compilePlan(plan: ProcessPlan, opts?: CompilePlanOptions): SynthResult;
|
|
39
|
+
//# sourceMappingURL=compile.d.ts.map
|