@bpmnkit/core 0.1.1 → 0.2.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.
Files changed (83) hide show
  1. package/README.md +32 -1
  2. package/dist/bpmn/agentic.d.ts +121 -0
  3. package/dist/bpmn/agentic.js +97 -0
  4. package/dist/bpmn/auto-layout.d.ts +5 -5
  5. package/dist/bpmn/auto-layout.js +592 -36
  6. package/dist/bpmn/bpmn-builder.d.ts +265 -3
  7. package/dist/bpmn/bpmn-builder.js +603 -197
  8. package/dist/bpmn/bpmn-model.d.ts +114 -0
  9. package/dist/bpmn/bpmn-parser.js +1414 -521
  10. package/dist/bpmn/bpmn-serializer.js +107 -19
  11. package/dist/bpmn/compact.d.ts +17 -2
  12. package/dist/bpmn/compact.js +3 -3
  13. package/dist/bpmn/full-operations.d.ts +89 -0
  14. package/dist/bpmn/full-operations.js +478 -0
  15. package/dist/bpmn/index.d.ts +19 -0
  16. package/dist/bpmn/index.js +21 -0
  17. package/dist/bpmn/optimize/agentic.d.ts +10 -0
  18. package/dist/bpmn/optimize/agentic.js +88 -0
  19. package/dist/bpmn/optimize/deploy.d.ts +16 -0
  20. package/dist/bpmn/optimize/deploy.js +143 -0
  21. package/dist/bpmn/optimize/feel-syntax.d.ts +12 -0
  22. package/dist/bpmn/optimize/feel-syntax.js +87 -0
  23. package/dist/bpmn/optimize/feel.js +7 -4
  24. package/dist/bpmn/optimize/flow.js +22 -2
  25. package/dist/bpmn/optimize/index.js +20 -9
  26. package/dist/bpmn/optimize/patterns.js +23 -16
  27. package/dist/bpmn/optimize/tasks.js +30 -7
  28. package/dist/bpmn/optimize/types.d.ts +10 -1
  29. package/dist/bpmn/optimize/utils.js +2 -4
  30. package/dist/bpmn/optimize/variable-flow.js +58 -67
  31. package/dist/bpmn/semantic-hash.d.ts +93 -0
  32. package/dist/bpmn/semantic-hash.js +155 -0
  33. package/dist/bpmn/sha256.d.ts +17 -0
  34. package/dist/bpmn/sha256.js +95 -0
  35. package/dist/bpmn/zeebe-extensions.d.ts +83 -0
  36. package/dist/bpmn/zeebe-extensions.js +117 -0
  37. package/dist/bpmn/zeebe-placement.d.ts +12 -0
  38. package/dist/bpmn/zeebe-placement.js +140 -0
  39. package/dist/errors.d.ts +40 -1
  40. package/dist/errors.js +41 -0
  41. package/dist/index.d.ts +16 -5
  42. package/dist/index.js +9 -3
  43. package/dist/layout/annotations.js +36 -1
  44. package/dist/layout/collaboration/alignment.d.ts +26 -0
  45. package/dist/layout/collaboration/alignment.js +66 -0
  46. package/dist/layout/collaboration/ordering.d.ts +21 -0
  47. package/dist/layout/collaboration/ordering.js +102 -0
  48. package/dist/layout/index.d.ts +1 -0
  49. package/dist/layout/layout-engine.d.ts +13 -3
  50. package/dist/layout/layout-engine.js +9 -4
  51. package/dist/layout/semantic/bands.d.ts +19 -0
  52. package/dist/layout/semantic/bands.js +324 -0
  53. package/dist/layout/semantic/graph.d.ts +37 -0
  54. package/dist/layout/semantic/graph.js +242 -0
  55. package/dist/layout/semantic/index.d.ts +13 -0
  56. package/dist/layout/semantic/index.js +181 -0
  57. package/dist/layout/semantic/place.d.ts +40 -0
  58. package/dist/layout/semantic/place.js +271 -0
  59. package/dist/layout/semantic/route.d.ts +14 -0
  60. package/dist/layout/semantic/route.js +514 -0
  61. package/dist/layout/types.d.ts +17 -0
  62. package/dist/node/index.d.ts +10 -0
  63. package/dist/node/index.js +9 -0
  64. package/dist/node/write.d.ts +81 -0
  65. package/dist/node/write.js +167 -0
  66. package/dist/plan/compile.d.ts +39 -0
  67. package/dist/plan/compile.js +380 -0
  68. package/dist/plan/extract.d.ts +31 -0
  69. package/dist/plan/extract.js +248 -0
  70. package/dist/plan/index.d.ts +6 -0
  71. package/dist/plan/index.js +5 -0
  72. package/dist/plan/merge.d.ts +13 -0
  73. package/dist/plan/merge.js +80 -0
  74. package/dist/plan/slug.d.ts +5 -0
  75. package/dist/plan/slug.js +22 -0
  76. package/dist/plan/types.d.ts +225 -0
  77. package/dist/plan/types.js +13 -0
  78. package/dist/types/id-generator.js +11 -3
  79. package/dist/xml/index.d.ts +3 -1
  80. package/dist/xml/index.js +2 -1
  81. package/dist/xml/xml-parser.d.ts +32 -0
  82. package/dist/xml/xml-parser.js +394 -143
  83. package/package.json +9 -2
@@ -0,0 +1,514 @@
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
+ /** Width of one bucket in the x-axis indexes below. */
17
+ const INDEX_CELL = 128;
18
+ /**
19
+ * Items bucketed by the x-range they cover, so a query for a segment's x-span
20
+ * visits only the shapes and routes near it instead of every one in the
21
+ * diagram. Orthogonal routes are mostly short, so this turns the per-edge
22
+ * obstacle and crossing checks from O(n) into near-constant work.
23
+ */
24
+ class XIndex {
25
+ cells = new Map();
26
+ stamp = 0;
27
+ insert(minX, maxX, item) {
28
+ const entry = { item, stamp: 0 };
29
+ const lo = Math.floor(Math.min(minX, maxX) / INDEX_CELL);
30
+ const hi = Math.floor(Math.max(minX, maxX) / INDEX_CELL);
31
+ for (let c = lo; c <= hi; c++) {
32
+ const cell = this.cells.get(c);
33
+ if (cell)
34
+ cell.push(entry);
35
+ else
36
+ this.cells.set(c, [entry]);
37
+ }
38
+ }
39
+ /** Visit every item whose x-range may overlap [minX, maxX]; stop early when `visit` returns true. */
40
+ query(minX, maxX, visit) {
41
+ const stamp = ++this.stamp;
42
+ const lo = Math.floor(Math.min(minX, maxX) / INDEX_CELL);
43
+ const hi = Math.floor(Math.max(minX, maxX) / INDEX_CELL);
44
+ for (let c = lo; c <= hi; c++) {
45
+ const cell = this.cells.get(c);
46
+ if (!cell)
47
+ continue;
48
+ for (const entry of cell) {
49
+ if (entry.stamp === stamp)
50
+ continue;
51
+ entry.stamp = stamp;
52
+ if (visit(entry.item))
53
+ return true;
54
+ }
55
+ }
56
+ return false;
57
+ }
58
+ }
59
+ function centre(b) {
60
+ return { x: b.x + b.width / 2, y: b.y + b.height / 2 };
61
+ }
62
+ /**
63
+ * Route every sequence flow orthogonally.
64
+ *
65
+ * Each edge proposes candidate polylines from most to least desirable and takes
66
+ * the first one that clears every unrelated shape: a straight run along the
67
+ * spine, a turn out of the source's top or bottom for a branch, and a turn in
68
+ * the empty gutter in front of the target's column as the general fallback.
69
+ */
70
+ export function routeFlows(graph, flows, bounds, bandLayout, gutterX) {
71
+ const obstacles = new XIndex();
72
+ for (const [id, b] of bounds)
73
+ obstacles.insert(b.x, b.x + b.width, { id, b });
74
+ const ctx = {
75
+ graph,
76
+ bounds,
77
+ obstacles,
78
+ gutterX,
79
+ reserved: [],
80
+ routed: new XIndex(),
81
+ };
82
+ const routed = new Map();
83
+ const pending = [];
84
+ for (const flow of flows) {
85
+ const source = bounds.get(flow.sourceRef);
86
+ const target = bounds.get(flow.targetRef);
87
+ if (!source || !target)
88
+ continue;
89
+ const fromBoundary = graph.byId.get(flow.sourceRef)?.type === "boundaryEvent";
90
+ const sourceRank = graph.ranks.get(fromBoundary ? hostOf(graph, flow.sourceRef) : flow.sourceRef);
91
+ const targetRank = graph.ranks.get(flow.targetRef);
92
+ const isBackEdge = graph.backEdges.has(flow.id);
93
+ const direct = isBackEdge
94
+ ? []
95
+ : fromBoundary
96
+ ? fromBoundaryCandidates(ctx, source, target, targetRank)
97
+ : forwardCandidates(ctx, source, target, sourceRank, targetRank);
98
+ const clear = pick(direct, ctx, flow.sourceRef, flow.targetRef);
99
+ if (clear && clear.crossings <= CROSSING_TOLERANCE) {
100
+ commit(ctx, routed, flow.id, clear.waypoints);
101
+ continue;
102
+ }
103
+ pending.push({
104
+ flow,
105
+ source,
106
+ target,
107
+ sourceRank,
108
+ targetRank,
109
+ below: isBackEdge,
110
+ span: Math.abs(target.x - source.x),
111
+ direct: clear,
112
+ });
113
+ }
114
+ // Widest detours claim the outermost corridors, so long routes nest around
115
+ // short ones instead of crossing them.
116
+ pending.sort((a, b) => b.span - a.span);
117
+ for (const item of pending) {
118
+ const around = detour(ctx, item.source, item.target, item.sourceRank, item.targetRank, item.below, item.flow.sourceRef, item.flow.targetRef);
119
+ // Going around costs bends and length, so it has to save more than one
120
+ // crossing to be worth taking.
121
+ const cost = crossingCount(around, ctx) + DETOUR_PENALTY;
122
+ const waypoints = item.direct && item.direct.crossings <= cost ? item.direct.waypoints : around;
123
+ commit(ctx, routed, item.flow.id, waypoints);
124
+ }
125
+ // Emit in declaration order, whichever pass produced the route.
126
+ const edges = [];
127
+ for (const flow of flows) {
128
+ const waypoints = routed.get(flow.id);
129
+ if (!waypoints)
130
+ continue;
131
+ const edge = {
132
+ id: flow.id,
133
+ sourceRef: flow.sourceRef,
134
+ targetRef: flow.targetRef,
135
+ waypoints: collapse(waypoints),
136
+ };
137
+ if (flow.name)
138
+ edge.label = flow.name;
139
+ edges.push(edge);
140
+ }
141
+ return edges;
142
+ }
143
+ /** Record a chosen route so later edges can steer around it. */
144
+ function commit(ctx, routed, id, waypoints) {
145
+ routed.set(id, waypoints);
146
+ for (let i = 0; i + 1 < waypoints.length; i++) {
147
+ const a = waypoints[i];
148
+ const b = waypoints[i + 1];
149
+ if (a && b)
150
+ ctx.routed.insert(Math.min(a.x, b.x), Math.max(a.x, b.x), [a, b]);
151
+ }
152
+ }
153
+ function hostOf(graph, eventId) {
154
+ for (const [hostId, events] of graph.attachers) {
155
+ if (events.some((e) => e.id === eventId))
156
+ return hostId;
157
+ }
158
+ return eventId;
159
+ }
160
+ /** First candidate that crosses no shape other than its own endpoints. */
161
+ function pick(candidates, ctx, sourceId, targetId) {
162
+ let best = null;
163
+ for (const candidate of candidates) {
164
+ // An expanded container legitimately holds its children's routes, so only
165
+ // the two endpoints are exempt from the obstacle check.
166
+ if (blocked(ctx, candidate, sourceId, targetId))
167
+ continue;
168
+ const crossings = crossingCount(candidate, ctx);
169
+ if (crossings === 0)
170
+ return { waypoints: candidate, crossings };
171
+ if (!best || crossings < best.crossings)
172
+ best = { waypoints: candidate, crossings };
173
+ }
174
+ return best;
175
+ }
176
+ /** Proper intersection between two segments — touching endpoints do not count. */
177
+ function intersects(p, q, r, t) {
178
+ const side = (o, a, b) => Math.sign((a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x));
179
+ return side(p, q, r) !== side(p, q, t) && side(r, t, p) !== side(r, t, q);
180
+ }
181
+ /** How many already-routed edges a candidate would cut across. */
182
+ function crossingCount(waypoints, ctx) {
183
+ let count = 0;
184
+ for (let i = 0; i + 1 < waypoints.length; i++) {
185
+ const a = waypoints[i];
186
+ const b = waypoints[i + 1];
187
+ if (!a || !b)
188
+ continue;
189
+ ctx.routed.query(Math.min(a.x, b.x), Math.max(a.x, b.x), ([c, d]) => {
190
+ if (intersects(a, b, c, d))
191
+ count++;
192
+ return false;
193
+ });
194
+ }
195
+ return count;
196
+ }
197
+ /** True when the segment a→b passes through `o`, grazing allowed up to HIT_TOLERANCE. */
198
+ function segmentHits(a, b, o) {
199
+ const minX = Math.min(a.x, b.x) + HIT_TOLERANCE;
200
+ const maxX = Math.max(a.x, b.x) - HIT_TOLERANCE;
201
+ const minY = Math.min(a.y, b.y) + HIT_TOLERANCE;
202
+ const maxY = Math.max(a.y, b.y) - HIT_TOLERANCE;
203
+ if (maxX <= o.x || o.x + o.width <= minX)
204
+ return false;
205
+ if (maxY <= o.y || o.y + o.height <= minY)
206
+ return false;
207
+ return true;
208
+ }
209
+ /** How many shapes a route passes through, its own endpoints excepted. */
210
+ function hitCount(ctx, waypoints, sourceId, targetId) {
211
+ const hit = new Set();
212
+ for (let i = 0; i + 1 < waypoints.length; i++) {
213
+ const a = waypoints[i];
214
+ const b = waypoints[i + 1];
215
+ if (!a || !b)
216
+ continue;
217
+ ctx.obstacles.query(Math.min(a.x, b.x), Math.max(a.x, b.x), (o) => {
218
+ if (o.id !== sourceId && o.id !== targetId && segmentHits(a, b, o.b))
219
+ hit.add(o);
220
+ return false;
221
+ });
222
+ }
223
+ return hit.size;
224
+ }
225
+ /** True when any segment of the route passes through a shape other than its endpoints. */
226
+ function blocked(ctx, waypoints, sourceId, targetId) {
227
+ for (let i = 0; i + 1 < waypoints.length; i++) {
228
+ const a = waypoints[i];
229
+ const b = waypoints[i + 1];
230
+ if (!a || !b)
231
+ continue;
232
+ const hit = ctx.obstacles.query(Math.min(a.x, b.x), Math.max(a.x, b.x), (o) => o.id !== sourceId && o.id !== targetId && segmentHits(a, b, o.b));
233
+ if (hit)
234
+ return true;
235
+ }
236
+ return false;
237
+ }
238
+ /**
239
+ * Candidates for a normal forward flow, best first: a straight spine segment,
240
+ * a branch leaving through the source's top or bottom, then the gutter turn.
241
+ */
242
+ function forwardCandidates(ctx, source, target, sourceRank, targetRank) {
243
+ const from = centre(source);
244
+ const to = centre(target);
245
+ const rightOf = target.x >= source.x + source.width;
246
+ if (Math.abs(from.y - to.y) < 1 && rightOf) {
247
+ return [
248
+ [
249
+ { x: source.x + source.width, y: from.y },
250
+ { x: target.x, y: to.y },
251
+ ],
252
+ ];
253
+ }
254
+ // Same column, different band: drop straight between the two.
255
+ if (sourceRank !== undefined && sourceRank === targetRank) {
256
+ const down = to.y > from.y;
257
+ return [
258
+ [
259
+ { x: from.x, y: down ? source.y + source.height : source.y },
260
+ { x: to.x, y: down ? target.y : target.y + target.height },
261
+ ],
262
+ ];
263
+ }
264
+ if (!rightOf)
265
+ return [];
266
+ const down = to.y > from.y;
267
+ const gutter = ctx.gutterX.get(targetRank ?? 0) ?? target.x - H_GAP / 2;
268
+ const behind = sourceRank !== undefined
269
+ ? (ctx.gutterX.get(sourceRank + 1) ?? source.x + source.width + H_GAP / 2)
270
+ : source.x + source.width + H_GAP / 2;
271
+ const candidates = [
272
+ // Branch: leave through the side facing the target band, then run in.
273
+ [
274
+ { x: from.x, y: down ? source.y + source.height : source.y },
275
+ { x: from.x, y: to.y },
276
+ { x: target.x, y: to.y },
277
+ ],
278
+ // Gutter turn in front of the target: out of the right edge, across, in.
279
+ [
280
+ { x: source.x + source.width, y: from.y },
281
+ { x: gutter, y: from.y },
282
+ { x: gutter, y: to.y },
283
+ { x: target.x, y: to.y },
284
+ ],
285
+ ];
286
+ // Turning in the gutter right behind the source instead reaches the target
287
+ // band early; which of the two crosses less depends on what is already
288
+ // routed, so both are offered.
289
+ if (Math.abs(behind - gutter) > 1) {
290
+ candidates.push([
291
+ { x: source.x + source.width, y: from.y },
292
+ { x: behind, y: from.y },
293
+ { x: behind, y: to.y },
294
+ { x: target.x, y: to.y },
295
+ ]);
296
+ }
297
+ return candidates;
298
+ }
299
+ /** A boundary event leaves through its outward side, never into its host. */
300
+ function fromBoundaryCandidates(ctx, source, target, targetRank) {
301
+ const from = centre(source);
302
+ const to = centre(target);
303
+ const down = to.y >= from.y;
304
+ const exitY = down ? source.y + source.height : source.y;
305
+ const stem = down ? exitY + BOUNDARY_STEM : exitY - BOUNDARY_STEM;
306
+ if (Math.abs(from.x - to.x) < 1) {
307
+ return [
308
+ [
309
+ { x: from.x, y: exitY },
310
+ { x: to.x, y: down ? target.y : target.y + target.height },
311
+ ],
312
+ ];
313
+ }
314
+ if (target.x >= from.x) {
315
+ const gutter = ctx.gutterX.get(targetRank ?? 0) ?? target.x - H_GAP / 2;
316
+ return [
317
+ [
318
+ { x: from.x, y: exitY },
319
+ { x: from.x, y: to.y },
320
+ { x: target.x, y: to.y },
321
+ ],
322
+ [
323
+ { x: from.x, y: exitY },
324
+ { x: from.x, y: stem },
325
+ { x: gutter, y: stem },
326
+ { x: gutter, y: to.y },
327
+ { x: target.x, y: to.y },
328
+ ],
329
+ ];
330
+ }
331
+ return [
332
+ [
333
+ { x: from.x, y: exitY },
334
+ { x: from.x, y: stem },
335
+ { x: target.x + target.width, y: stem },
336
+ { x: target.x + target.width, y: to.y },
337
+ ],
338
+ ];
339
+ }
340
+ /**
341
+ * The universal fallback: leave through the gutter behind the source, cross in
342
+ * the nearest horizontal corridor that clears every shape between the two
343
+ * columns, and come back through the gutter in front of the target. Used for
344
+ * feedback edges and for any forward edge whose direct routes are blocked.
345
+ */
346
+ function detour(ctx, source, target, sourceRank, targetRank,
347
+ /** A loop reads as a loop when it runs clear of the flow it repeats. */
348
+ isLoop = false, sourceId = "", targetId = "") {
349
+ const from = centre(source);
350
+ const to = centre(target);
351
+ const dropX = sourceRank !== undefined
352
+ ? (ctx.gutterX.get(sourceRank + 1) ?? source.x + source.width + H_GAP / 2)
353
+ : source.x + source.width + H_GAP / 2;
354
+ const riseX = ctx.gutterX.get(targetRank ?? 0) ?? target.x - H_GAP / 2;
355
+ const left = Math.min(dropX, riseX, from.x, to.x);
356
+ const right = Math.max(dropX, riseX, from.x, to.x);
357
+ // A loop leaves the band it repeats: below by convention, but above when the
358
+ // space below is busy and the space above is not. Anything else takes the
359
+ // nearest clear corridor, whichever side that is on.
360
+ const middle = (from.y + to.y) / 2;
361
+ const bottom = Math.max(source.y + source.height, target.y + target.height);
362
+ const top = Math.min(source.y, target.y);
363
+ const options = isLoop
364
+ ? [
365
+ corridor(ctx, left, right, middle, bottom),
366
+ corridor(ctx, left, right, middle, undefined, top),
367
+ ]
368
+ : [corridor(ctx, left, right, middle)];
369
+ let base = options[0] ?? middle;
370
+ if (isLoop) {
371
+ let fewest = Number.POSITIVE_INFINITY;
372
+ for (const option of options) {
373
+ const crossings = crossingCount([
374
+ { x: left, y: option },
375
+ { x: right, y: option },
376
+ ], ctx);
377
+ if (crossings < fewest) {
378
+ fewest = crossings;
379
+ base = option;
380
+ }
381
+ }
382
+ }
383
+ const goesDown = base > middle;
384
+ const corridorY = reserve(ctx, left, right, base, goesDown ? 1 : -1);
385
+ const exitY = goesDown ? source.y + source.height : source.y;
386
+ const entryY = goesDown ? target.y + target.height : target.y;
387
+ // Leaving through the source's own top or bottom keeps the route out of the
388
+ // horizontal corridor its neighbours flow along; the gutter variant is the
389
+ // fallback for when that vertical is occupied.
390
+ const straightOut = [
391
+ { x: from.x, y: exitY },
392
+ { x: from.x, y: corridorY },
393
+ { x: to.x, y: corridorY },
394
+ { x: to.x, y: entryY },
395
+ ];
396
+ const viaGutter = [
397
+ { x: source.x + source.width, y: from.y },
398
+ { x: dropX, y: from.y },
399
+ { x: dropX, y: corridorY },
400
+ { x: riseX, y: corridorY },
401
+ { x: riseX, y: to.y },
402
+ { x: target.x, y: to.y },
403
+ ];
404
+ const chosen = pick([straightOut, viaGutter], ctx, sourceId, targetId);
405
+ if (chosen)
406
+ return chosen.waypoints;
407
+ // Neither is clear: keep whichever grazes fewer shapes.
408
+ return hitCount(ctx, straightOut, sourceId, targetId) <=
409
+ hitCount(ctx, viaGutter, sourceId, targetId)
410
+ ? straightOut
411
+ : viaGutter;
412
+ }
413
+ /**
414
+ * Keep parallel detours apart: step away from the flow until this stretch of
415
+ * corridor is free, so two long routes stack instead of merging into one line.
416
+ */
417
+ function reserve(ctx, left, right, base, direction) {
418
+ const hitsShape = (candidate) => ctx.obstacles.query(left, right, ({ b }) => b.x + b.width > left &&
419
+ b.x < right &&
420
+ b.y - HIT_TOLERANCE < candidate &&
421
+ candidate < b.y + b.height + HIT_TOLERANCE);
422
+ let y = base;
423
+ let free = base;
424
+ for (let lane = 0; lane < CORRIDOR_LANES; lane++) {
425
+ if (!hitsShape(y)) {
426
+ free = y;
427
+ const taken = ctx.reserved.some((r) => r.left < right && left < r.right && Math.abs(r.y - y) < CORRIDOR_SPACING);
428
+ if (!taken)
429
+ break;
430
+ }
431
+ y += direction * CORRIDOR_SPACING;
432
+ }
433
+ // Sharing a corridor with another route is a lesser evil than running the
434
+ // route through a shape, so fall back to the last line that was clear.
435
+ if (hitsShape(y))
436
+ y = free;
437
+ ctx.reserved.push({ left, right, y });
438
+ return y;
439
+ }
440
+ /**
441
+ * A horizontal line between `left` and `right` that misses every shape, chosen
442
+ * as close to `preferredY` as possible so detours stay local instead of
443
+ * sweeping around the whole diagram.
444
+ */
445
+ function corridor(ctx, left, right, preferredY,
446
+ /** Only consider corridors below this line. */
447
+ floor,
448
+ /** Only consider corridors above this line. */
449
+ ceiling) {
450
+ const spans = [];
451
+ ctx.obstacles.query(left, right, ({ b }) => {
452
+ if (b.x + b.width > left && b.x < right) {
453
+ spans.push([b.y - ROUTING_MARGIN, b.y + b.height + ROUTING_MARGIN]);
454
+ }
455
+ return false;
456
+ });
457
+ if (spans.length === 0)
458
+ return preferredY;
459
+ spans.sort((a, b) => a[0] - b[0]);
460
+ // Merge the blocked bands, then take the gap centre nearest the preference.
461
+ const merged = [];
462
+ for (const span of spans) {
463
+ const last = merged[merged.length - 1];
464
+ if (last && span[0] <= last[1])
465
+ last[1] = Math.max(last[1], span[1]);
466
+ else
467
+ merged.push([span[0], span[1]]);
468
+ }
469
+ const options = [];
470
+ const first = merged[0];
471
+ const last = merged[merged.length - 1];
472
+ if (first)
473
+ options.push(first[0] - ROUTING_MARGIN);
474
+ if (last)
475
+ options.push(last[1] + ROUTING_MARGIN);
476
+ for (let i = 0; i + 1 < merged.length; i++) {
477
+ const a = merged[i];
478
+ const b = merged[i + 1];
479
+ if (a && b)
480
+ options.push((a[1] + b[0]) / 2);
481
+ }
482
+ let allowed = floor === undefined ? options : options.filter((y) => y > floor);
483
+ if (ceiling !== undefined)
484
+ allowed = allowed.filter((y) => y < ceiling);
485
+ const usable = allowed.length > 0 ? allowed : options;
486
+ let best = usable[0] ?? preferredY;
487
+ for (const option of usable) {
488
+ if (Math.abs(option - preferredY) < Math.abs(best - preferredY))
489
+ best = option;
490
+ }
491
+ return best;
492
+ }
493
+ /** Drop duplicate points and mid-points that sit on a straight run. */
494
+ function collapse(waypoints) {
495
+ const out = [];
496
+ for (const wp of waypoints) {
497
+ const last = out[out.length - 1];
498
+ if (last && last.x === wp.x && last.y === wp.y)
499
+ continue;
500
+ out.push(wp);
501
+ }
502
+ for (let i = out.length - 2; i > 0; i--) {
503
+ const prev = out[i - 1];
504
+ const cur = out[i];
505
+ const next = out[i + 1];
506
+ if (!prev || !cur || !next)
507
+ continue;
508
+ const collinear = (prev.x === cur.x && cur.x === next.x) || (prev.y === cur.y && cur.y === next.y);
509
+ if (collinear)
510
+ out.splice(i, 1);
511
+ }
512
+ return out;
513
+ }
514
+ //# sourceMappingURL=route.js.map
@@ -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,10 @@
1
+ /**
2
+ * Node-only entry point.
3
+ *
4
+ * Everything here touches the filesystem, so it lives behind the
5
+ * `@bpmnkit/core/node` subpath. Importing `@bpmnkit/core` itself stays free of
6
+ * `node:` builtins and keeps working in browsers, workers and edge runtimes.
7
+ */
8
+ export { writeBpmn } from "./write.js";
9
+ export type { WriteBpmnOptions, WriteBpmnResult } from "./write.js";
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Node-only entry point.
3
+ *
4
+ * Everything here touches the filesystem, so it lives behind the
5
+ * `@bpmnkit/core/node` subpath. Importing `@bpmnkit/core` itself stays free of
6
+ * `node:` builtins and keeps working in browsers, workers and edge runtimes.
7
+ */
8
+ export { writeBpmn } from "./write.js";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,81 @@
1
+ import type { BpmnDefinitions } from "../bpmn/bpmn-model.js";
2
+ import { type SemanticDiff } from "../bpmn/semantic-hash.js";
3
+ /**
4
+ * The only place in this package that writes a BPMN file, and the only one that
5
+ * checks what it wrote.
6
+ *
7
+ * Node-only — it is reached through the `@bpmnkit/core/node` subpath so that
8
+ * importing `@bpmnkit/core` in a browser never pulls `node:fs` in.
9
+ *
10
+ * **What the verification does and does not cover.** Before anything reaches
11
+ * disk, the model is serialised, parsed back, and the two semantic hashes are
12
+ * compared. That catches the serialiser dropping or mangling something. It
13
+ * cannot catch the *parser* dropping something on the way in: content the
14
+ * parser never saw is absent from both sides and the hashes agree. Guarding
15
+ * that is the round-trip corpus gate's job (`tests/roundtrip-corpus.test.ts`),
16
+ * not this function's.
17
+ *
18
+ * There is deliberately no option to skip verification. Turning it off would
19
+ * only ever be used to get past the bug it exists to report; callers who want
20
+ * unchecked serialisation can still use `Bpmn.export()` and write it
21
+ * themselves.
22
+ */
23
+ export interface WriteBpmnOptions {
24
+ /** Path to write to. */
25
+ output: string;
26
+ /**
27
+ * Replace `output` if it already exists. Default `false`, which refuses
28
+ * rather than overwrite.
29
+ */
30
+ force?: boolean;
31
+ /**
32
+ * `"preserve"` (default) writes the diagram the model already carries.
33
+ * `"auto"` regenerates it first — the model is unchanged either way, which
34
+ * the verification step proves.
35
+ */
36
+ layout?: "preserve" | "auto";
37
+ }
38
+ export interface WriteBpmnResult {
39
+ /** Absolute path written. */
40
+ destination: string;
41
+ /** Size of the written file in bytes. */
42
+ bytes: number;
43
+ /** SHA-256 of the exact bytes written. */
44
+ outputSha256: string;
45
+ /** Semantic hash of the model, as verified after reading it back. */
46
+ semanticHash: string;
47
+ /**
48
+ * What this write changed about the file that was already there, or
49
+ * `undefined` when the destination was newly created or the previous
50
+ * contents could not be parsed.
51
+ */
52
+ changes?: SemanticDiff;
53
+ }
54
+ /**
55
+ * Serialises a model, verifies it survives a round trip, and writes it
56
+ * atomically.
57
+ *
58
+ * The file appears complete or not at all: the contents go to a temporary file
59
+ * in the destination's own directory and are then renamed into place, so an
60
+ * interrupted write cannot leave a half-written model behind. Without `force`
61
+ * the final step is a hard link, which fails if the destination appeared in the
62
+ * meantime rather than silently replacing it.
63
+ *
64
+ * @param definitions - The model to write.
65
+ * @param options - Destination and write behaviour.
66
+ * @returns Where it went, what it hashes to, and what it changed.
67
+ * @throws {WriteVerificationError} If reading the output back does not
68
+ * reproduce the model. Nothing is written.
69
+ * @throws {WriteError} If the destination exists and `force` was not given, or
70
+ * the filesystem refused the write.
71
+ *
72
+ * @example
73
+ * ```typescript
74
+ * import { writeBpmn } from "@bpmnkit/core/node"
75
+ *
76
+ * const result = await writeBpmn(definitions, { output: "flow.bpmn" })
77
+ * console.log(result.semanticHash, result.changes?.changed.length ?? 0)
78
+ * ```
79
+ */
80
+ export declare function writeBpmn(definitions: BpmnDefinitions, options: WriteBpmnOptions): Promise<WriteBpmnResult>;
81
+ //# sourceMappingURL=write.d.ts.map