@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/bpmn/auto-layout.js
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
+
import { associationWaypoints, packAnnotations } from "../layout/annotations.js";
|
|
1
2
|
import { layoutProcess } from "../layout/layout-engine.js";
|
|
2
|
-
import { resolveEdgeCrossings } from "../layout/routing.js";
|
|
3
3
|
const POOL_HEADER = 30;
|
|
4
4
|
const LANE_HEADER = 30;
|
|
5
5
|
const PADDING = 20;
|
|
6
6
|
const POOL_GAP = 30;
|
|
7
|
-
const ANN_H = 50;
|
|
8
|
-
const ANN_GAP = 60;
|
|
9
|
-
const ANN_PADDING = 20;
|
|
10
7
|
function contentBbox(nodes, extra) {
|
|
11
8
|
let minX = Number.POSITIVE_INFINITY;
|
|
12
9
|
let minY = Number.POSITIVE_INFINITY;
|
|
@@ -34,97 +31,6 @@ function contentBbox(nodes, extra) {
|
|
|
34
31
|
}
|
|
35
32
|
return { minX, minY, maxX, maxY };
|
|
36
33
|
}
|
|
37
|
-
/** Pre-compute annotation positions in layout space (before dx/dy shift). */
|
|
38
|
-
function computeAnnotationLocalBounds(process, layoutNodes) {
|
|
39
|
-
const nodeById = new Map(layoutNodes.map((n) => [n.id, n]));
|
|
40
|
-
const placements = new Map();
|
|
41
|
-
// Occupied regions for overlap checks (node bounds + label bounds)
|
|
42
|
-
const occupied = [];
|
|
43
|
-
for (const n of layoutNodes) {
|
|
44
|
-
occupied.push({ ...n.bounds });
|
|
45
|
-
if (n.labelBounds)
|
|
46
|
-
occupied.push({ ...n.labelBounds });
|
|
47
|
-
}
|
|
48
|
-
// Static obstacles for crossing detection (nodes + labels only, not annotations)
|
|
49
|
-
const obstacles = [...occupied];
|
|
50
|
-
for (const ta of process.textAnnotations) {
|
|
51
|
-
const assoc = process.associations.find((a) => a.sourceRef === ta.id || a.targetRef === ta.id);
|
|
52
|
-
const connId = assoc
|
|
53
|
-
? assoc.sourceRef === ta.id
|
|
54
|
-
? assoc.targetRef
|
|
55
|
-
: assoc.sourceRef
|
|
56
|
-
: undefined;
|
|
57
|
-
const connNode = connId ? nodeById.get(connId) : undefined;
|
|
58
|
-
const annW = Math.min(200, Math.max(100, (ta.text?.length ?? 10) * 5));
|
|
59
|
-
if (!connNode) {
|
|
60
|
-
const candidate = { x: 0, y: 0, width: annW, height: ANN_H };
|
|
61
|
-
occupied.push({ ...candidate });
|
|
62
|
-
placements.set(ta.id, candidate);
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
const localX = connNode.bounds.x + connNode.bounds.width / 2 - annW / 2;
|
|
66
|
-
const anchorX = connNode.bounds.x + connNode.bounds.width / 2;
|
|
67
|
-
const pushStep = ANN_H + ANN_PADDING * 2 + 10;
|
|
68
|
-
// Try below: start below connected element, push down for overlaps
|
|
69
|
-
const belowY = connNode.bounds.y + connNode.bounds.height + ANN_GAP;
|
|
70
|
-
const below = { x: localX, y: belowY, width: annW, height: ANN_H };
|
|
71
|
-
for (let i = 0; i < 30 && hasOverlapPadded(below, occupied, ANN_PADDING); i++)
|
|
72
|
-
below.y += pushStep;
|
|
73
|
-
// Try above: gap scales with text length so longer annotations have more breathing room
|
|
74
|
-
const aboveGap = ANN_GAP + Math.round(annW * 0.2);
|
|
75
|
-
const aboveY = connNode.bounds.y - aboveGap - ANN_H;
|
|
76
|
-
const above = { x: localX, y: aboveY, width: annW, height: ANN_H };
|
|
77
|
-
for (let i = 0; i < 30 && hasOverlapPadded(above, occupied, ANN_PADDING); i++)
|
|
78
|
-
above.y -= pushStep;
|
|
79
|
-
// Count how many obstacles the association line would cross for each candidate
|
|
80
|
-
const belowCrossings = countLineCrossings(anchorX, connNode.bounds, below, obstacles);
|
|
81
|
-
const aboveCrossings = countLineCrossings(anchorX, connNode.bounds, above, obstacles);
|
|
82
|
-
const candidate = belowCrossings <= aboveCrossings ? below : above;
|
|
83
|
-
occupied.push({ ...candidate });
|
|
84
|
-
obstacles.push({ ...candidate });
|
|
85
|
-
placements.set(ta.id, candidate);
|
|
86
|
-
}
|
|
87
|
-
return placements;
|
|
88
|
-
}
|
|
89
|
-
/** Count how many obstacles the vertical association line from connNode to annotation crosses. */
|
|
90
|
-
function countLineCrossings(lineX, connBounds, annBounds, obstacles) {
|
|
91
|
-
const annCY = annBounds.y + annBounds.height / 2;
|
|
92
|
-
const connCY = connBounds.y + connBounds.height / 2;
|
|
93
|
-
const top = Math.min(annCY, connCY);
|
|
94
|
-
const bottom = Math.max(annCY, connCY);
|
|
95
|
-
const tolerance = 20;
|
|
96
|
-
let crossings = 0;
|
|
97
|
-
for (const b of obstacles) {
|
|
98
|
-
// Skip the connected element itself
|
|
99
|
-
if (b.x === connBounds.x && b.y === connBounds.y && b.width === connBounds.width)
|
|
100
|
-
continue;
|
|
101
|
-
// Obstacle must overlap with the line's X corridor
|
|
102
|
-
if (b.x + b.width < lineX - tolerance || b.x > lineX + tolerance)
|
|
103
|
-
continue;
|
|
104
|
-
// Obstacle must be between connNode and annotation vertically
|
|
105
|
-
if (b.y + b.height <= top || b.y >= bottom)
|
|
106
|
-
continue;
|
|
107
|
-
crossings++;
|
|
108
|
-
}
|
|
109
|
-
return crossings;
|
|
110
|
-
}
|
|
111
|
-
function hasOverlap(a, others) {
|
|
112
|
-
for (const b of others) {
|
|
113
|
-
if (a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y)
|
|
114
|
-
return true;
|
|
115
|
-
}
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
function hasOverlapPadded(a, others, padding) {
|
|
119
|
-
for (const b of others) {
|
|
120
|
-
if (a.x - padding < b.x + b.width &&
|
|
121
|
-
a.x + a.width + padding > b.x &&
|
|
122
|
-
a.y - padding < b.y + b.height &&
|
|
123
|
-
a.y + a.height + padding > b.y)
|
|
124
|
-
return true;
|
|
125
|
-
}
|
|
126
|
-
return false;
|
|
127
|
-
}
|
|
128
34
|
function nodeToShape(node, dx, dy) {
|
|
129
35
|
const shape = {
|
|
130
36
|
id: `${node.id}_di`,
|
|
@@ -201,19 +107,38 @@ function buildLaneShapes(lanes, nodes, dx, dy, poolY, poolHeaderWidth, laneConte
|
|
|
201
107
|
const mB = accB && accB.count > 0 ? accB.sum / accB.count : Number.POSITIVE_INFINITY;
|
|
202
108
|
return mA - mB;
|
|
203
109
|
});
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
110
|
+
// Compute proportional weight per lane: node count × row height, minimum 1 row
|
|
111
|
+
const MIN_LANE_H = 80;
|
|
112
|
+
const weights = sortedLanes.map((lane) => {
|
|
113
|
+
const count = nodes.filter((n) => elemToLane.get(n.id) === lane.id).length;
|
|
114
|
+
return Math.max(count, 1);
|
|
115
|
+
});
|
|
116
|
+
const totalWeight = weights.reduce((a, b) => a + b, 0);
|
|
117
|
+
// Scale proportionally to poolHeight so all lanes fill the pool exactly
|
|
118
|
+
const scaledHeights = weights.map((w) => Math.round((w / totalWeight) * poolHeight));
|
|
119
|
+
// Fix last lane for rounding drift
|
|
120
|
+
if (scaledHeights.length > 0) {
|
|
121
|
+
scaledHeights[scaledHeights.length - 1] =
|
|
122
|
+
poolHeight - scaledHeights.slice(0, -1).reduce((a, b) => a + b, 0);
|
|
123
|
+
}
|
|
124
|
+
let cumulativeY = 0;
|
|
125
|
+
return sortedLanes.map((lane, i) => {
|
|
126
|
+
const laneH = scaledHeights[i] ?? MIN_LANE_H;
|
|
127
|
+
const shape = {
|
|
128
|
+
id: `${lane.id}_di`,
|
|
129
|
+
bpmnElement: lane.id,
|
|
130
|
+
isHorizontal: true,
|
|
131
|
+
bounds: {
|
|
132
|
+
x: Math.round(poolHeaderWidth),
|
|
133
|
+
y: Math.round(poolY + cumulativeY),
|
|
134
|
+
width: Math.round(laneContentWidth),
|
|
135
|
+
height: Math.round(laneH),
|
|
136
|
+
},
|
|
137
|
+
unknownAttributes: {},
|
|
138
|
+
};
|
|
139
|
+
cumulativeY += laneH;
|
|
140
|
+
return shape;
|
|
141
|
+
});
|
|
217
142
|
}
|
|
218
143
|
function addAnnotationShapes(process, layoutNodes, annLocalBounds, allShapes, allEdges, dx, dy) {
|
|
219
144
|
if (process.textAnnotations.length === 0 && process.associations.length === 0)
|
|
@@ -244,33 +169,17 @@ function addAnnotationShapes(process, layoutNodes, annLocalBounds, allShapes, al
|
|
|
244
169
|
const elId = annId === assoc.sourceRef ? assoc.targetRef : assoc.sourceRef;
|
|
245
170
|
const annB = annId ? annLocalBounds.get(annId) : undefined;
|
|
246
171
|
const elNode = nodeById.get(elId);
|
|
247
|
-
if (!annB || !elNode)
|
|
172
|
+
if (!annB || !annId || !elNode)
|
|
248
173
|
continue;
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
];
|
|
259
|
-
}
|
|
260
|
-
else if (annB.y + annB.height <= elB.y) {
|
|
261
|
-
// annotation above: element top-center → annotation bottom-center
|
|
262
|
-
waypoints = [
|
|
263
|
-
{ x: elCx, y: Math.round(elB.y + dy) },
|
|
264
|
-
{ x: annCx, y: Math.round(annB.y + annB.height + dy) },
|
|
265
|
-
];
|
|
266
|
-
}
|
|
267
|
-
else {
|
|
268
|
-
// side-by-side: center-to-center
|
|
269
|
-
waypoints = [
|
|
270
|
-
{ x: Math.round(elB.x + elB.width / 2 + dx), y: Math.round(elB.y + elB.height / 2 + dy) },
|
|
271
|
-
{ x: annCx, y: Math.round(annB.y + annB.height / 2 + dy) },
|
|
272
|
-
];
|
|
273
|
-
}
|
|
174
|
+
const { pElem, pAnn } = associationWaypoints(elNode.bounds, annB);
|
|
175
|
+
// Shift into diagram space and honour the original sourceRef→targetRef order.
|
|
176
|
+
const shifted = {
|
|
177
|
+
pElem: { x: Math.round(pElem.x + dx), y: Math.round(pElem.y + dy) },
|
|
178
|
+
pAnn: { x: Math.round(pAnn.x + dx), y: Math.round(pAnn.y + dy) },
|
|
179
|
+
};
|
|
180
|
+
const wp1 = assoc.sourceRef === annId ? shifted.pAnn : shifted.pElem;
|
|
181
|
+
const wp2 = assoc.sourceRef === annId ? shifted.pElem : shifted.pAnn;
|
|
182
|
+
const waypoints = [wp1, wp2];
|
|
274
183
|
allEdges.push({
|
|
275
184
|
id: `${assoc.id}_di`,
|
|
276
185
|
bpmnElement: assoc.id,
|
|
@@ -305,15 +214,11 @@ export function applyAutoLayout(defs) {
|
|
|
305
214
|
const participantId = processToParticipant.get(process.id);
|
|
306
215
|
const lanes = process.laneSet?.lanes ?? [];
|
|
307
216
|
const hasLanes = lanes.length > 0;
|
|
308
|
-
// layoutProcess now handles boundary event repositioning internally.
|
|
309
217
|
const result = layoutProcess(process);
|
|
310
|
-
// Re-resolve edge crossings after boundary events moved shapes
|
|
311
|
-
const nodeMap = new Map(result.nodes.map((n) => [n.id, n]));
|
|
312
|
-
resolveEdgeCrossings(result.edges, nodeMap);
|
|
313
218
|
if (result.nodes.length === 0)
|
|
314
219
|
continue;
|
|
315
220
|
// Pre-compute annotation positions in layout space so they're included in the bbox
|
|
316
|
-
const annBounds =
|
|
221
|
+
const annBounds = packAnnotations(process, result.nodes);
|
|
317
222
|
const { minX, minY, maxX, maxY } = contentBbox(result.nodes, annBounds.values());
|
|
318
223
|
const contentW = maxX - minX;
|
|
319
224
|
const contentH = maxY - minY;
|
|
@@ -352,6 +257,33 @@ export function applyAutoLayout(defs) {
|
|
|
352
257
|
poolY += innerH + POOL_GAP;
|
|
353
258
|
}
|
|
354
259
|
}
|
|
260
|
+
if (collab && collab.messageFlows.length > 0) {
|
|
261
|
+
const shapeByElement = new Map(allShapes.map((s) => [s.bpmnElement, s.bounds]));
|
|
262
|
+
for (const mf of collab.messageFlows) {
|
|
263
|
+
const src = shapeByElement.get(mf.sourceRef);
|
|
264
|
+
const tgt = shapeByElement.get(mf.targetRef);
|
|
265
|
+
if (!src || !tgt)
|
|
266
|
+
continue;
|
|
267
|
+
const srcBelow = src.y + src.height / 2 > tgt.y + tgt.height / 2;
|
|
268
|
+
const sx = Math.round(src.x + src.width / 2);
|
|
269
|
+
const tx = Math.round(tgt.x + tgt.width / 2);
|
|
270
|
+
const sy = srcBelow ? src.y : src.y + src.height;
|
|
271
|
+
const ty = srcBelow ? tgt.y + tgt.height : tgt.y;
|
|
272
|
+
const midY = Math.round((sy + ty) / 2);
|
|
273
|
+
const waypoints = sx === tx
|
|
274
|
+
? [
|
|
275
|
+
{ x: sx, y: sy },
|
|
276
|
+
{ x: tx, y: ty },
|
|
277
|
+
]
|
|
278
|
+
: [
|
|
279
|
+
{ x: sx, y: sy },
|
|
280
|
+
{ x: sx, y: midY },
|
|
281
|
+
{ x: tx, y: midY },
|
|
282
|
+
{ x: tx, y: ty },
|
|
283
|
+
];
|
|
284
|
+
allEdges.push({ id: `${mf.id}_di`, bpmnElement: mf.id, waypoints, unknownAttributes: {} });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
355
287
|
const planeBpmnElement = collab?.id ?? defs.processes[0]?.id ?? "plane";
|
|
356
288
|
const existingDiagram = defs.diagrams[0];
|
|
357
289
|
return {
|
|
@@ -321,6 +321,12 @@ export declare class BranchBuilder {
|
|
|
321
321
|
* @param handler - Callback that chains elements from the boundary event.
|
|
322
322
|
*/
|
|
323
323
|
withBoundary(id: string, options: Omit<BoundaryEventOptions, "attachedTo">, handler: (b: BranchBuilder) => void): this;
|
|
324
|
+
/** Add a sub-process (mirrors `ProcessBuilder.subProcess`). */
|
|
325
|
+
subProcess(id: string, content: (b: SubProcessContentBuilder) => void, options?: SubProcessOptions): this;
|
|
326
|
+
/** Add an ad-hoc sub-process (mirrors `ProcessBuilder.adHocSubProcess`). */
|
|
327
|
+
adHocSubProcess(id: string, content: (b: SubProcessContentBuilder) => void, options?: AdHocSubProcessOptions): this;
|
|
328
|
+
/** Add an event sub-process. Triggered by its start event — no incoming or outgoing sequence flows. */
|
|
329
|
+
eventSubProcess(id: string, content: (b: SubProcessContentBuilder) => void, options?: ElementOptions): this;
|
|
324
330
|
/**
|
|
325
331
|
* Create a named branch from the last gateway added inside this branch.
|
|
326
332
|
*
|
|
@@ -372,6 +378,30 @@ export declare class SubProcessContentBuilder {
|
|
|
372
378
|
branch(name: string, callback: (b: BranchBuilder) => void): this;
|
|
373
379
|
connectTo(targetId: string): this;
|
|
374
380
|
element(elementId: string): this;
|
|
381
|
+
/**
|
|
382
|
+
* Add a boundary event attached to an existing activity in this sub-process.
|
|
383
|
+
*
|
|
384
|
+
* The boundary event is NOT connected by a sequence flow — it attaches via
|
|
385
|
+
* `attachedToRef`. The builder cursor advances to the boundary event so
|
|
386
|
+
* subsequent elements chain from it. Use `withBoundary()` if you want the
|
|
387
|
+
* cursor to return to the task afterward.
|
|
388
|
+
*/
|
|
389
|
+
boundaryEvent(id: string, options: BoundaryEventOptions): this;
|
|
390
|
+
/**
|
|
391
|
+
* Attach a boundary event to the preceding task and build its outgoing path,
|
|
392
|
+
* then restore the builder cursor to the preceding task so the main flow continues.
|
|
393
|
+
*
|
|
394
|
+
* @param id - ID for the boundary event element.
|
|
395
|
+
* @param options - Boundary event options (without `attachedTo` — inferred from cursor).
|
|
396
|
+
* @param handler - Callback that chains elements from the boundary event.
|
|
397
|
+
*/
|
|
398
|
+
withBoundary(id: string, options: Omit<BoundaryEventOptions, "attachedTo">, handler: (b: SubProcessContentBuilder) => void): this;
|
|
399
|
+
/** Add a sub-process nested inside this sub-process's content. */
|
|
400
|
+
subProcess(id: string, content: (b: SubProcessContentBuilder) => void, options?: SubProcessOptions): this;
|
|
401
|
+
/** Add an ad-hoc sub-process nested inside this sub-process's content. */
|
|
402
|
+
adHocSubProcess(id: string, content: (b: SubProcessContentBuilder) => void, options?: AdHocSubProcessOptions): this;
|
|
403
|
+
/** Add an event sub-process. Triggered by its start event — no incoming or outgoing sequence flows. */
|
|
404
|
+
eventSubProcess(id: string, content: (b: SubProcessContentBuilder) => void, options?: ElementOptions): this;
|
|
375
405
|
}
|
|
376
406
|
/** Fluent builder for constructing BPMN processes. */
|
|
377
407
|
export declare class ProcessBuilder {
|