@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.
- package/README.md +32 -1
- 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 +265 -3
- package/dist/bpmn/bpmn-builder.js +603 -197
- package/dist/bpmn/bpmn-model.d.ts +114 -0
- package/dist/bpmn/bpmn-parser.js +1414 -521
- package/dist/bpmn/bpmn-serializer.js +107 -19
- package/dist/bpmn/compact.d.ts +17 -2
- package/dist/bpmn/compact.js +3 -3
- package/dist/bpmn/full-operations.d.ts +89 -0
- package/dist/bpmn/full-operations.js +478 -0
- package/dist/bpmn/index.d.ts +19 -0
- package/dist/bpmn/index.js +21 -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 +7 -4
- package/dist/bpmn/optimize/flow.js +22 -2
- package/dist/bpmn/optimize/index.js +20 -9
- package/dist/bpmn/optimize/patterns.js +23 -16
- package/dist/bpmn/optimize/tasks.js +30 -7
- package/dist/bpmn/optimize/types.d.ts +10 -1
- package/dist/bpmn/optimize/utils.js +2 -4
- package/dist/bpmn/optimize/variable-flow.js +58 -67
- package/dist/bpmn/semantic-hash.d.ts +93 -0
- package/dist/bpmn/semantic-hash.js +155 -0
- package/dist/bpmn/sha256.d.ts +17 -0
- package/dist/bpmn/sha256.js +95 -0
- package/dist/bpmn/zeebe-extensions.d.ts +83 -0
- package/dist/bpmn/zeebe-extensions.js +117 -0
- package/dist/bpmn/zeebe-placement.d.ts +12 -0
- package/dist/bpmn/zeebe-placement.js +140 -0
- package/dist/errors.d.ts +40 -1
- package/dist/errors.js +41 -0
- package/dist/index.d.ts +16 -5
- package/dist/index.js +9 -3
- 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 +37 -0
- package/dist/layout/semantic/graph.js +242 -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 +514 -0
- package/dist/layout/types.d.ts +17 -0
- package/dist/node/index.d.ts +10 -0
- package/dist/node/index.js +9 -0
- package/dist/node/write.d.ts +81 -0
- package/dist/node/write.js +167 -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/dist/types/id-generator.js +11 -3
- package/dist/xml/index.d.ts +3 -1
- package/dist/xml/index.js +2 -1
- package/dist/xml/xml-parser.d.ts +32 -0
- package/dist/xml/xml-parser.js +394 -143
- package/package.json +9 -2
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vertical order of the pools in a collaboration.
|
|
3
|
+
*
|
|
4
|
+
* Message flows read best when the pools they connect sit next to each other,
|
|
5
|
+
* so the order is chosen to minimise how far messages travel vertically rather
|
|
6
|
+
* than left as declared. Small collaborations can afford an exhaustive search;
|
|
7
|
+
* larger ones use repeated remove-and-reinsert, which reaches the same answer on
|
|
8
|
+
* every realistic diagram and never depends on iteration order.
|
|
9
|
+
*/
|
|
10
|
+
/** Above this many pools, permuting every order costs more than it is worth. */
|
|
11
|
+
const EXHAUSTIVE_LIMIT = 8;
|
|
12
|
+
/**
|
|
13
|
+
* How far the messages travel in this order, weighted by how many there are.
|
|
14
|
+
* The declaration-order term is a tie-break: with nothing to gain from moving,
|
|
15
|
+
* the pools stay where the author put them.
|
|
16
|
+
*/
|
|
17
|
+
function cost(order, links) {
|
|
18
|
+
const position = new Map();
|
|
19
|
+
for (let i = 0; i < order.length; i++) {
|
|
20
|
+
const id = order[i];
|
|
21
|
+
if (id !== undefined)
|
|
22
|
+
position.set(id, i);
|
|
23
|
+
}
|
|
24
|
+
let total = 0;
|
|
25
|
+
for (const link of links) {
|
|
26
|
+
const from = position.get(link.from);
|
|
27
|
+
const to = position.get(link.to);
|
|
28
|
+
if (from === undefined || to === undefined)
|
|
29
|
+
continue;
|
|
30
|
+
total += link.weight * Math.abs(from - to);
|
|
31
|
+
}
|
|
32
|
+
let drift = 0;
|
|
33
|
+
for (let i = 0; i < order.length; i++)
|
|
34
|
+
drift += Math.abs((order[i] ?? i) - i);
|
|
35
|
+
return total + drift / (order.length * order.length + 1);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Order pools by their message-flow relationships. Returns indices into the
|
|
39
|
+
* input order; an input with no message flows comes back unchanged.
|
|
40
|
+
*/
|
|
41
|
+
export function orderPools(count, links) {
|
|
42
|
+
const identity = Array.from({ length: count }, (_, i) => i);
|
|
43
|
+
if (count < 3 || links.length === 0)
|
|
44
|
+
return identity;
|
|
45
|
+
return count <= EXHAUSTIVE_LIMIT ? exhaustive(identity, links) : refine(identity, links);
|
|
46
|
+
}
|
|
47
|
+
/** Every order, best first-found wins — so declaration order survives a tie. */
|
|
48
|
+
function exhaustive(identity, links) {
|
|
49
|
+
let best = identity;
|
|
50
|
+
let bestCost = cost(identity, links);
|
|
51
|
+
const permute = (prefix, rest) => {
|
|
52
|
+
if (rest.length === 0) {
|
|
53
|
+
const candidate = cost(prefix, links);
|
|
54
|
+
if (candidate < bestCost) {
|
|
55
|
+
best = [...prefix];
|
|
56
|
+
bestCost = candidate;
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
for (let i = 0; i < rest.length; i++) {
|
|
61
|
+
const next = rest[i];
|
|
62
|
+
if (next === undefined)
|
|
63
|
+
continue;
|
|
64
|
+
permute([...prefix, next], [...rest.slice(0, i), ...rest.slice(i + 1)]);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
permute([], identity);
|
|
68
|
+
return best;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Take each pool out and put it back wherever it fits best, repeating until a
|
|
72
|
+
* full sweep changes nothing.
|
|
73
|
+
*/
|
|
74
|
+
function refine(identity, links) {
|
|
75
|
+
let order = [...identity];
|
|
76
|
+
let current = cost(order, links);
|
|
77
|
+
for (let sweep = 0; sweep < order.length; sweep++) {
|
|
78
|
+
let improved = false;
|
|
79
|
+
for (let from = 0; from < order.length; from++) {
|
|
80
|
+
const pool = order[from];
|
|
81
|
+
if (pool === undefined)
|
|
82
|
+
continue;
|
|
83
|
+
const without = [...order.slice(0, from), ...order.slice(from + 1)];
|
|
84
|
+
for (let to = 0; to <= without.length; to++) {
|
|
85
|
+
if (to === from)
|
|
86
|
+
continue;
|
|
87
|
+
const candidate = [...without.slice(0, to), pool, ...without.slice(to)];
|
|
88
|
+
const candidateCost = cost(candidate, links);
|
|
89
|
+
if (candidateCost < current) {
|
|
90
|
+
order = candidate;
|
|
91
|
+
current = candidateCost;
|
|
92
|
+
improved = true;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!improved)
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
return order;
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=ordering.js.map
|
package/dist/layout/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { layoutProcess, layoutFlowNodes } from "./layout-engine.js";
|
|
2
|
+
export type { LayoutEngine } from "./layout-engine.js";
|
|
2
3
|
export { benchmarkLayout, compareLayouts, formatBenchmarkResult, generateAutoLayout, parseReferenceLayout, } from "./bench.js";
|
|
3
4
|
export type { BenchmarkResult, BoundingBox, ElementComparison, ElementPosition, FlowOrderViolation, } from "./bench.js";
|
|
4
5
|
export { assertNoOverlap } from "./overlap.js";
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import type { BpmnFlowElement, BpmnProcess, BpmnSequenceFlow } from "../bpmn/bpmn-model.js";
|
|
2
2
|
import type { LayoutResult } from "./types.js";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Which layout algorithm to run.
|
|
5
|
+
*
|
|
6
|
+
* `semantic` places nodes by rank and narrative band and honours lane
|
|
7
|
+
* membership. `grid` is the older cell-grid walk, kept for collaborations,
|
|
8
|
+
* where pool and message-flow geometry is still decided by the caller.
|
|
9
|
+
*/
|
|
10
|
+
export type LayoutEngine = "semantic" | "grid";
|
|
11
|
+
/**
|
|
12
|
+
* Layout a full process. Boundary events, expanded subprocesses and edge
|
|
13
|
+
* labels are handled inside the engine.
|
|
6
14
|
* Never throws on residual label overlap — call assertNoOverlap yourself
|
|
7
15
|
* in tests that validate known-good fixtures.
|
|
8
16
|
*/
|
|
9
|
-
export declare function layoutProcess(process: BpmnProcess
|
|
17
|
+
export declare function layoutProcess(process: BpmnProcess, engine?: LayoutEngine,
|
|
18
|
+
/** Sub-processes the diagram draws collapsed; their contents get their own plane. */
|
|
19
|
+
collapsed?: ReadonlySet<string>): LayoutResult;
|
|
10
20
|
/** Layout a set of flow nodes and sequence flows (used by ascii, proxy, compact). */
|
|
11
21
|
export declare function layoutFlowNodes(flowNodes: BpmnFlowElement[], sequenceFlows: BpmnSequenceFlow[]): LayoutResult;
|
|
12
22
|
//# sourceMappingURL=layout-engine.d.ts.map
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { gridLayoutFlowNodes } from "./grid/grid-engine.js";
|
|
2
|
+
import { semanticLayoutProcess } from "./semantic/index.js";
|
|
2
3
|
/**
|
|
3
|
-
* Layout a full process
|
|
4
|
-
*
|
|
4
|
+
* Layout a full process. Boundary events, expanded subprocesses and edge
|
|
5
|
+
* labels are handled inside the engine.
|
|
5
6
|
* Never throws on residual label overlap — call assertNoOverlap yourself
|
|
6
7
|
* in tests that validate known-good fixtures.
|
|
7
8
|
*/
|
|
8
|
-
export function layoutProcess(process
|
|
9
|
-
|
|
9
|
+
export function layoutProcess(process, engine = "semantic",
|
|
10
|
+
/** Sub-processes the diagram draws collapsed; their contents get their own plane. */
|
|
11
|
+
collapsed) {
|
|
12
|
+
if (engine === "grid")
|
|
13
|
+
return gridLayoutFlowNodes(process.flowElements, process.sequenceFlows);
|
|
14
|
+
return semanticLayoutProcess(process, collapsed);
|
|
10
15
|
}
|
|
11
16
|
/** Layout a set of flow nodes and sequence flows (used by ascii, proxy, compact). */
|
|
12
17
|
export function layoutFlowNodes(flowNodes, sequenceFlows) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { SemanticGraph } from "./graph.js";
|
|
2
|
+
/**
|
|
3
|
+
* Vertical narrative roles for one process scope.
|
|
4
|
+
*
|
|
5
|
+
* Band 0 is the spine — the primary path from the start event to an end event.
|
|
6
|
+
* Positive bands sit below it, negative bands above. Branch meaning decides the
|
|
7
|
+
* side: error handlers go down, escalation handlers go up, and plain
|
|
8
|
+
* alternatives alternate down/up so neither side runs away from the spine.
|
|
9
|
+
*/
|
|
10
|
+
export interface BandLayout {
|
|
11
|
+
/** node id → band. 0 is the spine, > 0 below it, < 0 above it. */
|
|
12
|
+
bands: Map<string, number>;
|
|
13
|
+
/** Ids of the nodes on the primary path. */
|
|
14
|
+
spine: Set<string>;
|
|
15
|
+
/** Flow ids that continue the spine and should be routed as one segment. */
|
|
16
|
+
straightFlows: Set<string>;
|
|
17
|
+
}
|
|
18
|
+
export declare function assignBands(graph: SemanticGraph): BandLayout;
|
|
19
|
+
//# sourceMappingURL=bands.d.ts.map
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { reachesEnd } from "./graph.js";
|
|
2
|
+
function isDefaultFlow(source, flow) {
|
|
3
|
+
if (!source)
|
|
4
|
+
return false;
|
|
5
|
+
const gateway = source;
|
|
6
|
+
return gateway.default !== undefined && gateway.default === flow.id;
|
|
7
|
+
}
|
|
8
|
+
/** Escalation handlers read as "upward" exceptions; everything else reads downward. */
|
|
9
|
+
function boundarySide(event) {
|
|
10
|
+
return event.eventDefinitions.some((d) => d.type === "escalation") ? -1 : 1;
|
|
11
|
+
}
|
|
12
|
+
export function assignBands(graph) {
|
|
13
|
+
const bands = new Map();
|
|
14
|
+
const spine = new Set();
|
|
15
|
+
const straightFlows = new Set();
|
|
16
|
+
const branches = [];
|
|
17
|
+
for (const start of graph.starts) {
|
|
18
|
+
if (!start)
|
|
19
|
+
continue;
|
|
20
|
+
traceSpine(graph, start, spine, straightFlows);
|
|
21
|
+
}
|
|
22
|
+
for (const id of spine)
|
|
23
|
+
bands.set(id, 0);
|
|
24
|
+
// Branch off the spine first, then off already-placed branches, so a nested
|
|
25
|
+
// alternative always fans farther from the spine than its parent.
|
|
26
|
+
const sources = [];
|
|
27
|
+
for (const node of graph.nodes) {
|
|
28
|
+
if (spine.has(node.id))
|
|
29
|
+
sources.push({ id: node.id, depth: 0, side: 1 });
|
|
30
|
+
}
|
|
31
|
+
const assigned = new Set(spine);
|
|
32
|
+
for (let i = 0; i < sources.length; i++) {
|
|
33
|
+
const source = sources[i];
|
|
34
|
+
if (!source)
|
|
35
|
+
continue;
|
|
36
|
+
for (const branch of branchesFrom(graph, source, assigned)) {
|
|
37
|
+
branches.push(branch);
|
|
38
|
+
for (const id of branch.nodes) {
|
|
39
|
+
assigned.add(id);
|
|
40
|
+
sources.push({ id, depth: branch.depth, side: branch.side });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for (const [id, band] of compact(branches))
|
|
45
|
+
bands.set(id, band);
|
|
46
|
+
placeUnassigned(graph, bands, assigned);
|
|
47
|
+
reduceCrossings(graph, bands);
|
|
48
|
+
return { bands, spine, straightFlows };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Two edges cross when their rank spans overlap and their endpoints swap order
|
|
52
|
+
* vertically on the way across. Counting those inversions estimates the
|
|
53
|
+
* crossings a band assignment will produce, without routing anything.
|
|
54
|
+
*/
|
|
55
|
+
function inversions(graph, bands) {
|
|
56
|
+
const edges = [];
|
|
57
|
+
for (const [host, flows] of graph.outgoing) {
|
|
58
|
+
for (const flow of flows) {
|
|
59
|
+
if (graph.backEdges.has(flow.id))
|
|
60
|
+
continue;
|
|
61
|
+
// A boundary event has no rank of its own; it travels with its host.
|
|
62
|
+
edges.push({
|
|
63
|
+
x1: graph.ranks.get(host) ?? 0,
|
|
64
|
+
y1: bands.get(host) ?? 0,
|
|
65
|
+
x2: graph.ranks.get(flow.targetRef) ?? 0,
|
|
66
|
+
y2: bands.get(flow.targetRef) ?? 0,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
let count = 0;
|
|
71
|
+
for (let i = 0; i < edges.length; i++) {
|
|
72
|
+
const a = edges[i];
|
|
73
|
+
if (!a)
|
|
74
|
+
continue;
|
|
75
|
+
for (let j = i + 1; j < edges.length; j++) {
|
|
76
|
+
const b = edges[j];
|
|
77
|
+
if (!b)
|
|
78
|
+
continue;
|
|
79
|
+
// Only edges travelling over the same ranks can cross.
|
|
80
|
+
if (Math.min(a.x1, a.x2) >= Math.max(b.x1, b.x2))
|
|
81
|
+
continue;
|
|
82
|
+
if (Math.min(b.x1, b.x2) >= Math.max(a.x1, a.x2))
|
|
83
|
+
continue;
|
|
84
|
+
const left = Math.sign(a.y1 - b.y1);
|
|
85
|
+
const right = Math.sign(a.y2 - b.y2);
|
|
86
|
+
if (left !== 0 && right !== 0 && left !== right)
|
|
87
|
+
count++;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return count;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Swap neighbouring bands where doing so untangles the edges between them.
|
|
94
|
+
*
|
|
95
|
+
* Compaction picks a band from where a branch starts and how far from the spine
|
|
96
|
+
* it belongs, which says nothing about the edges running past it: two branches
|
|
97
|
+
* that leave in one order and rejoin in the other end up crossing. Trading two
|
|
98
|
+
* neighbouring bands keeps each on its own side of the spine, so the narrative
|
|
99
|
+
* survives while the crossings drop.
|
|
100
|
+
*/
|
|
101
|
+
function reduceCrossings(graph, bands) {
|
|
102
|
+
const levels = [...new Set(bands.values())].filter((band) => band !== 0).sort((a, b) => a - b);
|
|
103
|
+
if (levels.length < 2)
|
|
104
|
+
return;
|
|
105
|
+
let best = inversions(graph, bands);
|
|
106
|
+
for (let sweep = 0; sweep < levels.length; sweep++) {
|
|
107
|
+
let improved = false;
|
|
108
|
+
for (let i = 0; i + 1 < levels.length; i++) {
|
|
109
|
+
const lower = levels[i];
|
|
110
|
+
const upper = levels[i + 1];
|
|
111
|
+
// Only bands on the same side of the spine may trade places.
|
|
112
|
+
if (lower === undefined || upper === undefined)
|
|
113
|
+
continue;
|
|
114
|
+
if (Math.sign(lower) !== Math.sign(upper))
|
|
115
|
+
continue;
|
|
116
|
+
const moved = new Map();
|
|
117
|
+
for (const [id, band] of bands) {
|
|
118
|
+
if (band === lower)
|
|
119
|
+
moved.set(id, upper);
|
|
120
|
+
else if (band === upper)
|
|
121
|
+
moved.set(id, lower);
|
|
122
|
+
}
|
|
123
|
+
if (moved.size === 0)
|
|
124
|
+
continue;
|
|
125
|
+
const previous = new Map();
|
|
126
|
+
for (const [id, band] of moved) {
|
|
127
|
+
previous.set(id, bands.get(id) ?? 0);
|
|
128
|
+
bands.set(id, band);
|
|
129
|
+
}
|
|
130
|
+
const candidate = inversions(graph, bands);
|
|
131
|
+
if (candidate < best) {
|
|
132
|
+
best = candidate;
|
|
133
|
+
improved = true;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
for (const [id, band] of previous)
|
|
137
|
+
bands.set(id, band);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (!improved)
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Give the nodes no traversal reached — a scope entered only through a loop, say
|
|
146
|
+
* — a band of their own rather than dropping them on the spine, where they would
|
|
147
|
+
* land on top of whatever already occupies their rank.
|
|
148
|
+
*/
|
|
149
|
+
function placeUnassigned(graph, bands, assigned) {
|
|
150
|
+
const taken = new Set();
|
|
151
|
+
for (const node of graph.nodes) {
|
|
152
|
+
if (!assigned.has(node.id))
|
|
153
|
+
continue;
|
|
154
|
+
taken.add(`${graph.ranks.get(node.id) ?? 0}:${bands.get(node.id) ?? 0}`);
|
|
155
|
+
}
|
|
156
|
+
for (const node of graph.nodes) {
|
|
157
|
+
if (assigned.has(node.id))
|
|
158
|
+
continue;
|
|
159
|
+
const rank = graph.ranks.get(node.id) ?? 0;
|
|
160
|
+
// Start from whatever this node is connected to, so it lands near it.
|
|
161
|
+
let preferred = 0;
|
|
162
|
+
const neighbours = [
|
|
163
|
+
...(graph.outgoing.get(node.id) ?? []).map((f) => f.targetRef),
|
|
164
|
+
...(graph.incoming.get(node.id) ?? []).map((f) => f.sourceRef),
|
|
165
|
+
];
|
|
166
|
+
for (const neighbour of neighbours) {
|
|
167
|
+
if (!assigned.has(neighbour))
|
|
168
|
+
continue;
|
|
169
|
+
preferred = bands.get(neighbour) ?? 0;
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
let band = preferred;
|
|
173
|
+
for (let step = 0; step <= graph.nodes.length; step++) {
|
|
174
|
+
band = preferred + (step % 2 === 0 ? step / 2 : -(step + 1) / 2);
|
|
175
|
+
if (!taken.has(`${rank}:${band}`))
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
bands.set(node.id, band);
|
|
179
|
+
taken.add(`${rank}:${band}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* The spine is picked one edge at a time: prefer a target that can still reach
|
|
184
|
+
* an end event, then the gateway's default flow, then declaration order. That
|
|
185
|
+
* stops a dead-end alternative from becoming the main narrative just because it
|
|
186
|
+
* was declared first.
|
|
187
|
+
*/
|
|
188
|
+
function traceSpine(graph, start, spine, straightFlows) {
|
|
189
|
+
let current = start;
|
|
190
|
+
while (!spine.has(current)) {
|
|
191
|
+
spine.add(current);
|
|
192
|
+
const candidates = (graph.outgoing.get(current) ?? []).filter((f) => !graph.backEdges.has(f.id) && !spine.has(f.targetRef));
|
|
193
|
+
if (candidates.length === 0)
|
|
194
|
+
return;
|
|
195
|
+
const source = graph.byId.get(current);
|
|
196
|
+
const scored = candidates.map((flow, index) => ({
|
|
197
|
+
flow,
|
|
198
|
+
index,
|
|
199
|
+
// A handler path leaving a boundary event is an exception, never the
|
|
200
|
+
// narrative — it only continues the spine if nothing else can.
|
|
201
|
+
handler: flow.sourceRef === current ? 0 : 1,
|
|
202
|
+
ends: reachesEnd(graph, flow.targetRef) ? 0 : 1,
|
|
203
|
+
isDefault: isDefaultFlow(source, flow) ? 0 : 1,
|
|
204
|
+
}));
|
|
205
|
+
scored.sort((a, b) => a.handler - b.handler || a.ends - b.ends || a.isDefault - b.isDefault || a.index - b.index);
|
|
206
|
+
const next = scored[0];
|
|
207
|
+
if (!next)
|
|
208
|
+
return;
|
|
209
|
+
straightFlows.add(next.flow.id);
|
|
210
|
+
current = next.flow.targetRef;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Collect the alternatives leaving one node: its non-spine sequence flows plus
|
|
215
|
+
* the handler paths of any boundary event attached to it.
|
|
216
|
+
*/
|
|
217
|
+
function branchesFrom(graph, source, assigned) {
|
|
218
|
+
const out = [];
|
|
219
|
+
const node = graph.byId.get(source.id);
|
|
220
|
+
const alternatives = (graph.outgoing.get(source.id) ?? []).filter((f) => !graph.backEdges.has(f.id) && !assigned.has(f.targetRef));
|
|
221
|
+
// A gateway with a default flow keeps its alternatives on one side; without
|
|
222
|
+
// one they alternate below / above / farther below / farther above.
|
|
223
|
+
const hasDefault = alternatives.some((f) => isDefaultFlow(node, f));
|
|
224
|
+
let below = 0;
|
|
225
|
+
let above = 0;
|
|
226
|
+
for (let i = 0; i < alternatives.length; i++) {
|
|
227
|
+
const flow = alternatives[i];
|
|
228
|
+
if (!flow)
|
|
229
|
+
continue;
|
|
230
|
+
const handler = handlerSideOf(graph, source.id, flow);
|
|
231
|
+
let side;
|
|
232
|
+
if (handler !== undefined)
|
|
233
|
+
side = handler;
|
|
234
|
+
else if (hasDefault)
|
|
235
|
+
side = 1;
|
|
236
|
+
else
|
|
237
|
+
side = i % 2 === 0 ? 1 : -1;
|
|
238
|
+
const step = side === 1 ? ++below : ++above;
|
|
239
|
+
const branch = follow(graph, flow.targetRef, side, source.depth + step, assigned);
|
|
240
|
+
if (branch)
|
|
241
|
+
out.push(branch);
|
|
242
|
+
}
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
/** The side a flow inherits when it leaves a boundary event of this host. */
|
|
246
|
+
function handlerSideOf(graph, hostId, flow) {
|
|
247
|
+
if (flow.sourceRef === hostId)
|
|
248
|
+
return undefined;
|
|
249
|
+
const event = (graph.attachers.get(hostId) ?? []).find((e) => e.id === flow.sourceRef);
|
|
250
|
+
return event ? boundarySide(event) : undefined;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Follow a branch forward until it rejoins placed flow or runs out.
|
|
254
|
+
*
|
|
255
|
+
* A branch reserves its complete span — out to the rank it rejoins at, not just
|
|
256
|
+
* the ranks its own nodes occupy — because the edge back to the join still has
|
|
257
|
+
* to travel along that band. Reserving only the nodes lets a second branch share
|
|
258
|
+
* the band and be crossed by the first one's last edge.
|
|
259
|
+
*/
|
|
260
|
+
function follow(graph, entry, side, depth, assigned) {
|
|
261
|
+
const nodes = [];
|
|
262
|
+
const local = new Set();
|
|
263
|
+
let current = entry;
|
|
264
|
+
let minRank = Number.POSITIVE_INFINITY;
|
|
265
|
+
let maxRank = Number.NEGATIVE_INFINITY;
|
|
266
|
+
while (current !== undefined && !assigned.has(current) && !local.has(current)) {
|
|
267
|
+
nodes.push(current);
|
|
268
|
+
local.add(current);
|
|
269
|
+
const rank = graph.ranks.get(current) ?? 0;
|
|
270
|
+
minRank = Math.min(minRank, rank);
|
|
271
|
+
maxRank = Math.max(maxRank, rank);
|
|
272
|
+
const next = (graph.outgoing.get(current) ?? []).find((f) => !graph.backEdges.has(f.id) && !assigned.has(f.targetRef) && !local.has(f.targetRef));
|
|
273
|
+
current = next?.targetRef;
|
|
274
|
+
}
|
|
275
|
+
if (nodes.length === 0)
|
|
276
|
+
return null;
|
|
277
|
+
// The walk stops before the node the branch rejoins at, because that node is
|
|
278
|
+
// already placed. Reserve out to it anyway: the edge back into it still
|
|
279
|
+
// travels along this band.
|
|
280
|
+
const last = nodes[nodes.length - 1];
|
|
281
|
+
if (last !== undefined) {
|
|
282
|
+
for (const flow of graph.outgoing.get(last) ?? []) {
|
|
283
|
+
if (graph.backEdges.has(flow.id))
|
|
284
|
+
continue;
|
|
285
|
+
maxRank = Math.max(maxRank, graph.ranks.get(flow.targetRef) ?? maxRank);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return { nodes, side, depth, minRank, maxRank };
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Pack branches into physical bands. A band reservation covers the ranks the
|
|
292
|
+
* branch spans, so two branches that never overlap horizontally can share one
|
|
293
|
+
* band; overlapping narratives cannot.
|
|
294
|
+
*/
|
|
295
|
+
function compact(branches) {
|
|
296
|
+
const bands = new Map();
|
|
297
|
+
for (const side of [1, -1]) {
|
|
298
|
+
const mine = branches
|
|
299
|
+
.filter((b) => b.side === side)
|
|
300
|
+
.sort((a, b) => a.minRank - b.minRank || a.depth - b.depth);
|
|
301
|
+
/** physical level (1-based) → rank intervals already reserved on it. */
|
|
302
|
+
const reserved = [];
|
|
303
|
+
for (const branch of mine) {
|
|
304
|
+
let level = 0;
|
|
305
|
+
while (true) {
|
|
306
|
+
const slots = reserved[level];
|
|
307
|
+
if (!slots) {
|
|
308
|
+
reserved[level] = [[branch.minRank, branch.maxRank]];
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
const overlaps = slots.some(([from, to]) => branch.minRank <= to && from <= branch.maxRank);
|
|
312
|
+
if (!overlaps) {
|
|
313
|
+
slots.push([branch.minRank, branch.maxRank]);
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
level++;
|
|
317
|
+
}
|
|
318
|
+
for (const id of branch.nodes)
|
|
319
|
+
bands.set(id, side * (level + 1));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return bands;
|
|
323
|
+
}
|
|
324
|
+
//# sourceMappingURL=bands.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { BpmnBoundaryEvent, BpmnFlowElement, BpmnSequenceFlow } from "../../bpmn/bpmn-model.js";
|
|
2
|
+
/**
|
|
3
|
+
* Adjacency for one process scope, with cycles broken and ranks assigned.
|
|
4
|
+
*
|
|
5
|
+
* Boundary events are not ranked themselves: they dock onto their host, and an
|
|
6
|
+
* implicit host → handler edge carries their successors forward instead. That
|
|
7
|
+
* keeps a handler path strictly to the right of the activity it guards.
|
|
8
|
+
*/
|
|
9
|
+
export interface SemanticGraph {
|
|
10
|
+
/** Rankable flow nodes, in declaration order. Boundary events excluded. */
|
|
11
|
+
nodes: BpmnFlowElement[];
|
|
12
|
+
byId: Map<string, BpmnFlowElement>;
|
|
13
|
+
outgoing: Map<string, BpmnSequenceFlow[]>;
|
|
14
|
+
incoming: Map<string, BpmnSequenceFlow[]>;
|
|
15
|
+
/** hostId → boundary events attached to it, in declaration order. */
|
|
16
|
+
attachers: Map<string, BpmnBoundaryEvent[]>;
|
|
17
|
+
/** Ids of sequence flows that close a cycle; excluded from ranking. */
|
|
18
|
+
backEdges: Set<string>;
|
|
19
|
+
/** Weakly connected components, each a node-id list in declaration order. */
|
|
20
|
+
components: string[][];
|
|
21
|
+
/** Semantic start node id per component, index-aligned with `components`. */
|
|
22
|
+
starts: string[];
|
|
23
|
+
/** Longest-path rank per node id. */
|
|
24
|
+
ranks: Map<string, number>;
|
|
25
|
+
/** Lazily built: ids from which an end (or terminal) node is reachable. See {@link reachesEnd}. */
|
|
26
|
+
endReach?: Set<string>;
|
|
27
|
+
}
|
|
28
|
+
export declare function buildSemanticGraph(flowElements: BpmnFlowElement[], sequenceFlows: BpmnSequenceFlow[]): SemanticGraph;
|
|
29
|
+
/**
|
|
30
|
+
* True when an end event is reachable from `id` without traversing a back edge.
|
|
31
|
+
* A node other than `id` itself that has no outgoing flow also ends the path.
|
|
32
|
+
*
|
|
33
|
+
* The reverse reachability set is computed once per graph, so repeated calls
|
|
34
|
+
* from the spine tracer stay O(1) instead of re-walking the graph each time.
|
|
35
|
+
*/
|
|
36
|
+
export declare function reachesEnd(graph: SemanticGraph, id: string): boolean;
|
|
37
|
+
//# sourceMappingURL=graph.d.ts.map
|