@bpmnkit/core 0.0.27 → 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 +39 -126
- 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 -486
- package/dist/layout/types.js +4 -0
- 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 -115
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
const ANN_WIDTH = 200; // fixed annotation width
|
|
2
|
+
const FONT_CHAR_WIDTH = 6.4; // approximate avg char width @ 12px Arial
|
|
3
|
+
const FONT_LINE_HEIGHT = 14.4; // line-height @ 12px
|
|
4
|
+
const PADDING_X = 18; // 9px each side
|
|
5
|
+
const PADDING_Y = 14; // top + bottom
|
|
6
|
+
const ANN_GAP = 20; // min gap between two annotations
|
|
7
|
+
const ELEMENT_GAP = 30; // min gap between annotation and a non-annotation shape
|
|
8
|
+
const PREFERRED_OFFSET = 50; // preferred gap to associated element
|
|
9
|
+
const MIN_HEIGHT = 30;
|
|
10
|
+
const HORIZONTAL_SHIFTS = [0, 60, -60, 120, -120, 180, -180, 240, -240];
|
|
11
|
+
function computeHeight(text, width) {
|
|
12
|
+
if (!text || !text.trim())
|
|
13
|
+
return MIN_HEIGHT;
|
|
14
|
+
const inner = Math.max(40, width - PADDING_X);
|
|
15
|
+
const cpl = Math.max(1, Math.floor(inner / FONT_CHAR_WIDTH));
|
|
16
|
+
let totalLines = 0;
|
|
17
|
+
for (const para of text.split(/\r?\n/)) {
|
|
18
|
+
const words = para.split(/\s+/).filter(Boolean);
|
|
19
|
+
if (words.length === 0) {
|
|
20
|
+
totalLines += 1;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
let lineLen = 0;
|
|
24
|
+
let lines = 1;
|
|
25
|
+
for (const w of words) {
|
|
26
|
+
let len = w.length;
|
|
27
|
+
// Hard-break very long words
|
|
28
|
+
while (len > cpl) {
|
|
29
|
+
if (lineLen > 0) {
|
|
30
|
+
lines += 1;
|
|
31
|
+
lineLen = 0;
|
|
32
|
+
}
|
|
33
|
+
lines += 1;
|
|
34
|
+
len -= cpl;
|
|
35
|
+
}
|
|
36
|
+
if (lineLen === 0)
|
|
37
|
+
lineLen = len;
|
|
38
|
+
else if (lineLen + 1 + len <= cpl)
|
|
39
|
+
lineLen += 1 + len;
|
|
40
|
+
else {
|
|
41
|
+
lines += 1;
|
|
42
|
+
lineLen = len;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
totalLines += lines;
|
|
46
|
+
}
|
|
47
|
+
return Math.max(MIN_HEIGHT, Math.ceil(totalLines * FONT_LINE_HEIGHT + PADDING_Y));
|
|
48
|
+
}
|
|
49
|
+
function center(b) {
|
|
50
|
+
return { cx: b.x + b.width / 2, cy: b.y + b.height / 2 };
|
|
51
|
+
}
|
|
52
|
+
function clampX(x, b) {
|
|
53
|
+
return Math.max(b.x, Math.min(x, b.x + b.width));
|
|
54
|
+
}
|
|
55
|
+
function clampY(y, b) {
|
|
56
|
+
return Math.max(b.y, Math.min(y, b.y + b.height));
|
|
57
|
+
}
|
|
58
|
+
/** Modal (most-common, 20px-bucketed) center-Y of large (task-sized) nodes. */
|
|
59
|
+
function computeMainFlowY(layoutNodes) {
|
|
60
|
+
const cys = layoutNodes
|
|
61
|
+
.filter((n) => n.bounds.height >= 60)
|
|
62
|
+
.map((n) => n.bounds.y + n.bounds.height / 2);
|
|
63
|
+
if (cys.length === 0)
|
|
64
|
+
return 370;
|
|
65
|
+
const buckets = new Map();
|
|
66
|
+
for (const cy of cys) {
|
|
67
|
+
const k = Math.round(cy / 20) * 20;
|
|
68
|
+
buckets.set(k, (buckets.get(k) ?? 0) + 1);
|
|
69
|
+
}
|
|
70
|
+
const sortedKeys = [...buckets.keys()].sort((a, b) => a - b);
|
|
71
|
+
let bestKey = sortedKeys[0];
|
|
72
|
+
let bestCount = -1;
|
|
73
|
+
for (const k of sortedKeys) {
|
|
74
|
+
const count = buckets.get(k);
|
|
75
|
+
if (count > bestCount) {
|
|
76
|
+
bestCount = count;
|
|
77
|
+
bestKey = k;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return bestKey;
|
|
81
|
+
}
|
|
82
|
+
/** Elements at or above the main flow keep their annotation "above"; elements clearly below get "below". */
|
|
83
|
+
function naturalSide(elemBounds, mainFlowY) {
|
|
84
|
+
const cy = elemBounds.y + elemBounds.height / 2;
|
|
85
|
+
return cy > mainFlowY + 60 ? "below" : "above";
|
|
86
|
+
}
|
|
87
|
+
function overlapsPadded(a, others, padding) {
|
|
88
|
+
for (const b of others) {
|
|
89
|
+
if (a.x - padding < b.x + b.width &&
|
|
90
|
+
a.x + a.width + padding > b.x &&
|
|
91
|
+
a.y - padding < b.y + b.height &&
|
|
92
|
+
a.y + a.height + padding > b.y)
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Sizes and packs text annotations around their linked elements without
|
|
99
|
+
* overlapping each other or any other layout node (incl. node labels).
|
|
100
|
+
* Returns final Bounds per annotation id; annotations with no resolvable
|
|
101
|
+
* association target (no association at all, or the linked element isn't
|
|
102
|
+
* in `layoutNodes`) still get an entry — they're placed at a fixed fallback
|
|
103
|
+
* origin and pushed clear of everything else already placed, mirroring the
|
|
104
|
+
* pre-port fallback in auto-layout.ts's computeAnnotationLocalBounds.
|
|
105
|
+
*/
|
|
106
|
+
export function packAnnotations(process, layoutNodes) {
|
|
107
|
+
const result = new Map();
|
|
108
|
+
if (process.textAnnotations.length === 0)
|
|
109
|
+
return result;
|
|
110
|
+
const annotationIds = new Set(process.textAnnotations.map((a) => a.id));
|
|
111
|
+
const elementForAnnotation = new Map();
|
|
112
|
+
for (const assoc of process.associations) {
|
|
113
|
+
if (annotationIds.has(assoc.targetRef))
|
|
114
|
+
elementForAnnotation.set(assoc.targetRef, assoc.sourceRef);
|
|
115
|
+
else if (annotationIds.has(assoc.sourceRef))
|
|
116
|
+
elementForAnnotation.set(assoc.sourceRef, assoc.targetRef);
|
|
117
|
+
}
|
|
118
|
+
const nodeById = new Map(layoutNodes.map((n) => [n.id, n]));
|
|
119
|
+
const mainFlowY = computeMainFlowY(layoutNodes);
|
|
120
|
+
const items = [];
|
|
121
|
+
const unlinked = [];
|
|
122
|
+
for (const ann of process.textAnnotations) {
|
|
123
|
+
const linkedId = elementForAnnotation.get(ann.id);
|
|
124
|
+
const linked = linkedId ? nodeById.get(linkedId) : undefined;
|
|
125
|
+
const width = ANN_WIDTH;
|
|
126
|
+
const height = computeHeight(ann.text ?? "", width);
|
|
127
|
+
if (!linked) {
|
|
128
|
+
unlinked.push({ id: ann.id, width, height });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const side = naturalSide(linked.bounds, mainFlowY);
|
|
132
|
+
const lc = center(linked.bounds);
|
|
133
|
+
items.push({
|
|
134
|
+
id: ann.id,
|
|
135
|
+
bounds: { x: Math.round(lc.cx - width / 2), y: 0, width, height },
|
|
136
|
+
side,
|
|
137
|
+
linked,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
const obstacles = [];
|
|
141
|
+
for (const n of layoutNodes) {
|
|
142
|
+
obstacles.push(n.bounds);
|
|
143
|
+
if (n.labelBounds)
|
|
144
|
+
obstacles.push(n.labelBounds);
|
|
145
|
+
}
|
|
146
|
+
function packSide(side) {
|
|
147
|
+
const list = items
|
|
148
|
+
.filter((it) => it.side === side)
|
|
149
|
+
.sort((a, b) => center(a.linked.bounds).cx - center(b.linked.bounds).cx);
|
|
150
|
+
const placed = [];
|
|
151
|
+
for (const item of list) {
|
|
152
|
+
const linked = item.linked;
|
|
153
|
+
const lc = center(linked.bounds);
|
|
154
|
+
const naturalX = Math.round(lc.cx - item.bounds.width / 2);
|
|
155
|
+
const naturalY = side === "above"
|
|
156
|
+
? linked.bounds.y - PREFERRED_OFFSET - item.bounds.height
|
|
157
|
+
: linked.bounds.y + linked.bounds.height + PREFERRED_OFFSET;
|
|
158
|
+
let best = { x: naturalX, y: naturalY, cost: Number.POSITIVE_INFINITY };
|
|
159
|
+
for (const dx of HORIZONTAL_SHIFTS) {
|
|
160
|
+
const candidateX = naturalX + dx;
|
|
161
|
+
const ax1 = candidateX;
|
|
162
|
+
const ax2 = candidateX + item.bounds.width;
|
|
163
|
+
const intervals = [];
|
|
164
|
+
for (const other of placed) {
|
|
165
|
+
const ox1 = other.bounds.x;
|
|
166
|
+
const ox2 = ox1 + other.bounds.width;
|
|
167
|
+
if (ax2 + ANN_GAP <= ox1 || ox2 + ANN_GAP <= ax1)
|
|
168
|
+
continue;
|
|
169
|
+
intervals.push({
|
|
170
|
+
top: other.bounds.y - ANN_GAP,
|
|
171
|
+
bottom: other.bounds.y + other.bounds.height + ANN_GAP,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
for (const sh of obstacles) {
|
|
175
|
+
const sx1 = sh.x;
|
|
176
|
+
const sx2 = sx1 + sh.width;
|
|
177
|
+
if (ax2 + ELEMENT_GAP <= sx1 || sx2 + ELEMENT_GAP <= ax1)
|
|
178
|
+
continue;
|
|
179
|
+
intervals.push({ top: sh.y - ELEMENT_GAP, bottom: sh.y + sh.height + ELEMENT_GAP });
|
|
180
|
+
}
|
|
181
|
+
let y = naturalY;
|
|
182
|
+
let changed = true;
|
|
183
|
+
while (changed) {
|
|
184
|
+
changed = false;
|
|
185
|
+
for (const iv of intervals) {
|
|
186
|
+
if (y + item.bounds.height > iv.top && y < iv.bottom) {
|
|
187
|
+
y = side === "above" ? iv.top - item.bounds.height : iv.bottom;
|
|
188
|
+
changed = true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const cost = Math.hypot(candidateX - naturalX, y - naturalY);
|
|
193
|
+
if (cost < best.cost)
|
|
194
|
+
best = { x: candidateX, y, cost };
|
|
195
|
+
}
|
|
196
|
+
item.bounds.x = Math.round(best.x);
|
|
197
|
+
item.bounds.y = Math.round(best.y);
|
|
198
|
+
placed.push(item);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
packSide("above");
|
|
202
|
+
packSide("below");
|
|
203
|
+
for (const item of items)
|
|
204
|
+
result.set(item.id, item.bounds);
|
|
205
|
+
// Fallback placement for annotations with no resolvable linked element:
|
|
206
|
+
// start at a fixed origin and push straight down until clear of every
|
|
207
|
+
// node/label obstacle and every already-placed annotation (linked or
|
|
208
|
+
// unlinked), so they never overlap anything.
|
|
209
|
+
const annObstacles = items.map((it) => it.bounds);
|
|
210
|
+
for (const u of unlinked) {
|
|
211
|
+
const bounds = { x: 0, y: 0, width: u.width, height: u.height };
|
|
212
|
+
while (overlapsPadded(bounds, obstacles, ELEMENT_GAP) ||
|
|
213
|
+
overlapsPadded(bounds, annObstacles, ANN_GAP)) {
|
|
214
|
+
bounds.y += u.height + ANN_GAP;
|
|
215
|
+
}
|
|
216
|
+
annObstacles.push(bounds);
|
|
217
|
+
result.set(u.id, bounds);
|
|
218
|
+
}
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Edge-to-edge, clamped association waypoints between a linked element and
|
|
223
|
+
* its annotation. Port of `chooseWaypoints` (tmp/01-annotation-layouting.cjs:365-389).
|
|
224
|
+
*/
|
|
225
|
+
export function associationWaypoints(elementBounds, annotationBounds) {
|
|
226
|
+
const ec = center(elementBounds);
|
|
227
|
+
const ac = center(annotationBounds);
|
|
228
|
+
if (annotationBounds.y + annotationBounds.height <= elementBounds.y) {
|
|
229
|
+
return {
|
|
230
|
+
pElem: { x: clampX(ac.cx, elementBounds), y: elementBounds.y },
|
|
231
|
+
pAnn: { x: clampX(ec.cx, annotationBounds), y: annotationBounds.y + annotationBounds.height },
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
if (annotationBounds.y >= elementBounds.y + elementBounds.height) {
|
|
235
|
+
return {
|
|
236
|
+
pElem: { x: clampX(ac.cx, elementBounds), y: elementBounds.y + elementBounds.height },
|
|
237
|
+
pAnn: { x: clampX(ec.cx, annotationBounds), y: annotationBounds.y },
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
if (annotationBounds.x >= elementBounds.x + elementBounds.width) {
|
|
241
|
+
return {
|
|
242
|
+
pElem: { x: elementBounds.x + elementBounds.width, y: clampY(ac.cy, elementBounds) },
|
|
243
|
+
pAnn: { x: annotationBounds.x, y: clampY(ec.cy, annotationBounds) },
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
pElem: { x: elementBounds.x, y: clampY(ac.cy, elementBounds) },
|
|
248
|
+
pAnn: { x: annotationBounds.x + annotationBounds.width, y: clampY(ec.cy, annotationBounds) },
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
//# sourceMappingURL=annotations.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { LayoutEdge, LayoutNode } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Collision-aware edge label placement.
|
|
4
|
+
* For each labeled edge, generates candidate positions on the longest segment
|
|
5
|
+
* and picks the first one that doesn't overlap nodes or already-placed labels.
|
|
6
|
+
*/
|
|
7
|
+
export declare function placeEdgeLabels(edges: LayoutEdge[], nodeMap: Map<string, LayoutNode>): void;
|
|
8
|
+
//# sourceMappingURL=edge-labels.d.ts.map
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { LABEL_CHAR_WIDTH, LABEL_HEIGHT, LABEL_MIN_WIDTH, LABEL_VERTICAL_OFFSET } from "../types.js";
|
|
2
|
+
/** Collision tolerance in pixels — small overlap allowed for rounding. */
|
|
3
|
+
const LABEL_COLLISION_TOLERANCE = 2;
|
|
4
|
+
/** Number of slide steps along a segment when searching for clear space. */
|
|
5
|
+
const LABEL_SLIDE_STEPS = 10;
|
|
6
|
+
function boundsOverlap(a, b) {
|
|
7
|
+
return !(a.x + a.width + LABEL_COLLISION_TOLERANCE <= b.x ||
|
|
8
|
+
b.x + b.width + LABEL_COLLISION_TOLERANCE <= a.x ||
|
|
9
|
+
a.y + a.height + LABEL_COLLISION_TOLERANCE <= b.y ||
|
|
10
|
+
b.y + b.height + LABEL_COLLISION_TOLERANCE <= a.y);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Collision-aware edge label placement.
|
|
14
|
+
* For each labeled edge, generates candidate positions on the longest segment
|
|
15
|
+
* and picks the first one that doesn't overlap nodes or already-placed labels.
|
|
16
|
+
*/
|
|
17
|
+
export function placeEdgeLabels(edges, nodeMap) {
|
|
18
|
+
const occupied = [];
|
|
19
|
+
// Collect all node bounds as obstacles
|
|
20
|
+
for (const node of nodeMap.values()) {
|
|
21
|
+
occupied.push(node.bounds);
|
|
22
|
+
if (node.labelBounds)
|
|
23
|
+
occupied.push(node.labelBounds);
|
|
24
|
+
}
|
|
25
|
+
for (const edge of edges) {
|
|
26
|
+
if (!edge.label)
|
|
27
|
+
continue;
|
|
28
|
+
const labelWidth = Math.max(edge.label.length * LABEL_CHAR_WIDTH, LABEL_MIN_WIDTH);
|
|
29
|
+
const labelHeight = LABEL_HEIGHT;
|
|
30
|
+
// Find the longest segment
|
|
31
|
+
const { segStart, segEnd } = findLongestSegment(edge.waypoints);
|
|
32
|
+
// Generate candidate positions along the segment
|
|
33
|
+
const candidates = generateLabelCandidates(segStart, segEnd, labelWidth, labelHeight);
|
|
34
|
+
// Pick the first non-overlapping candidate
|
|
35
|
+
let placed = false;
|
|
36
|
+
for (const candidate of candidates) {
|
|
37
|
+
if (!occupied.some((ob) => boundsOverlap(candidate, ob))) {
|
|
38
|
+
edge.labelBounds = candidate;
|
|
39
|
+
occupied.push(candidate);
|
|
40
|
+
placed = true;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Fallback: slide along segment to find clear space
|
|
45
|
+
if (!placed) {
|
|
46
|
+
const fallback = slideLabelAlongSegment(segStart, segEnd, labelWidth, labelHeight, occupied);
|
|
47
|
+
if (fallback) {
|
|
48
|
+
edge.labelBounds = fallback;
|
|
49
|
+
occupied.push(fallback);
|
|
50
|
+
}
|
|
51
|
+
// If no clear position exists, leave labelBounds undefined (text preserved in edge.label)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function findLongestSegment(waypoints) {
|
|
56
|
+
let bestLen = 0;
|
|
57
|
+
let bestStart = waypoints[0] ?? { x: 0, y: 0 };
|
|
58
|
+
let bestEnd = waypoints[1] ?? waypoints[0] ?? { x: 0, y: 0 };
|
|
59
|
+
for (let i = 1; i < waypoints.length; i++) {
|
|
60
|
+
const a = waypoints[i - 1];
|
|
61
|
+
const b = waypoints[i];
|
|
62
|
+
if (!a || !b)
|
|
63
|
+
continue;
|
|
64
|
+
const len = Math.abs(b.x - a.x) + Math.abs(b.y - a.y);
|
|
65
|
+
if (len > bestLen) {
|
|
66
|
+
bestLen = len;
|
|
67
|
+
bestStart = a;
|
|
68
|
+
bestEnd = b;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { segStart: bestStart, segEnd: bestEnd };
|
|
72
|
+
}
|
|
73
|
+
function generateLabelCandidates(segStart, segEnd, labelWidth, labelHeight) {
|
|
74
|
+
const candidates = [];
|
|
75
|
+
// Positions along segment: 0.5, 0.25, 0.75, 0.33, 0.67
|
|
76
|
+
const fractions = [0.5, 0.25, 0.75, 0.33, 0.67];
|
|
77
|
+
// Perpendicular offsets: above, below
|
|
78
|
+
const offsets = [-LABEL_VERTICAL_OFFSET - labelHeight, LABEL_VERTICAL_OFFSET];
|
|
79
|
+
for (const f of fractions) {
|
|
80
|
+
const px = segStart.x + (segEnd.x - segStart.x) * f;
|
|
81
|
+
const py = segStart.y + (segEnd.y - segStart.y) * f;
|
|
82
|
+
for (const offset of offsets) {
|
|
83
|
+
// Determine perpendicular direction
|
|
84
|
+
const isHorizontal = Math.abs(segEnd.y - segStart.y) < 1;
|
|
85
|
+
let lx;
|
|
86
|
+
let ly;
|
|
87
|
+
if (isHorizontal) {
|
|
88
|
+
lx = px - labelWidth / 2;
|
|
89
|
+
ly = py + offset;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
lx = px + offset;
|
|
93
|
+
ly = py - labelHeight / 2;
|
|
94
|
+
}
|
|
95
|
+
candidates.push({ x: lx, y: ly, width: labelWidth, height: labelHeight });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return candidates;
|
|
99
|
+
}
|
|
100
|
+
function slideLabelAlongSegment(segStart, segEnd, labelWidth, labelHeight, occupied) {
|
|
101
|
+
const isHorizontal = Math.abs(segEnd.y - segStart.y) < 1;
|
|
102
|
+
for (let step = 0; step <= LABEL_SLIDE_STEPS; step++) {
|
|
103
|
+
const t = step / LABEL_SLIDE_STEPS;
|
|
104
|
+
const px = segStart.x + (segEnd.x - segStart.x) * t;
|
|
105
|
+
const py = segStart.y + (segEnd.y - segStart.y) * t;
|
|
106
|
+
const candidate = isHorizontal
|
|
107
|
+
? {
|
|
108
|
+
x: px - labelWidth / 2,
|
|
109
|
+
y: py - labelHeight - LABEL_VERTICAL_OFFSET,
|
|
110
|
+
width: labelWidth,
|
|
111
|
+
height: labelHeight,
|
|
112
|
+
}
|
|
113
|
+
: {
|
|
114
|
+
x: px - labelWidth - LABEL_VERTICAL_OFFSET,
|
|
115
|
+
y: py - labelHeight / 2,
|
|
116
|
+
width: labelWidth,
|
|
117
|
+
height: labelHeight,
|
|
118
|
+
};
|
|
119
|
+
if (!occupied.some((ob) => boundsOverlap(candidate, ob))) {
|
|
120
|
+
return candidate;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// No clear position found — skip label placement rather than forcing an overlap
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=edge-labels.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { BpmnBoundaryEvent, BpmnFlowElement, BpmnSequenceFlow } from "../../bpmn/bpmn-model.js";
|
|
2
|
+
/** Resolved adjacency for one nesting level of a process. */
|
|
3
|
+
export interface FlowGraph {
|
|
4
|
+
/** Grid-placeable elements — boundary events are excluded (they ride on their host). */
|
|
5
|
+
elements: BpmnFlowElement[];
|
|
6
|
+
byId: Map<string, BpmnFlowElement>;
|
|
7
|
+
outgoing: Map<string, BpmnSequenceFlow[]>;
|
|
8
|
+
incoming: Map<string, BpmnSequenceFlow[]>;
|
|
9
|
+
/** hostId → boundary events attached to it, in document order. */
|
|
10
|
+
attachers: Map<string, BpmnBoundaryEvent[]>;
|
|
11
|
+
}
|
|
12
|
+
/** bpmn:Task subtypes — used for the "right-align before a task-only fan-out" rule. */
|
|
13
|
+
export declare function isTaskLike(type: string): boolean;
|
|
14
|
+
export declare function buildFlowGraph(flowNodes: BpmnFlowElement[], sequenceFlows: BpmnSequenceFlow[]): FlowGraph;
|
|
15
|
+
/**
|
|
16
|
+
* True iff the element has a "real" predecessor — an incoming flow that is
|
|
17
|
+
* neither a self-loop nor sourced from a boundary event / its own attacher.
|
|
18
|
+
* Elements without one become traversal starting points.
|
|
19
|
+
*/
|
|
20
|
+
export declare function hasOtherIncoming(el: BpmnFlowElement, graph: FlowGraph): boolean;
|
|
21
|
+
/** True iff el is a join (>1 incoming) with at least one not-yet-visited feeder. */
|
|
22
|
+
export declare function isFutureIncoming(el: BpmnFlowElement, visited: Set<string>, graph: FlowGraph): boolean;
|
|
23
|
+
/** True iff some unvisited feeder of el is reachable downstream FROM el (a cycle). */
|
|
24
|
+
export declare function formsLoop(el: BpmnFlowElement, visited: Set<string>, graph: FlowGraph): boolean;
|
|
25
|
+
//# sourceMappingURL=flow-graph.d.ts.map
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const TASK_TYPES = new Set([
|
|
2
|
+
"task",
|
|
3
|
+
"userTask",
|
|
4
|
+
"serviceTask",
|
|
5
|
+
"scriptTask",
|
|
6
|
+
"sendTask",
|
|
7
|
+
"receiveTask",
|
|
8
|
+
"businessRuleTask",
|
|
9
|
+
"manualTask",
|
|
10
|
+
]);
|
|
11
|
+
/** bpmn:Task subtypes — used for the "right-align before a task-only fan-out" rule. */
|
|
12
|
+
export function isTaskLike(type) {
|
|
13
|
+
return TASK_TYPES.has(type);
|
|
14
|
+
}
|
|
15
|
+
export function buildFlowGraph(flowNodes, sequenceFlows) {
|
|
16
|
+
const byId = new Map();
|
|
17
|
+
for (const n of flowNodes)
|
|
18
|
+
byId.set(n.id, n);
|
|
19
|
+
const outgoing = new Map();
|
|
20
|
+
const incoming = new Map();
|
|
21
|
+
for (const f of sequenceFlows) {
|
|
22
|
+
if (!outgoing.has(f.sourceRef))
|
|
23
|
+
outgoing.set(f.sourceRef, []);
|
|
24
|
+
outgoing.get(f.sourceRef)?.push(f);
|
|
25
|
+
if (!incoming.has(f.targetRef))
|
|
26
|
+
incoming.set(f.targetRef, []);
|
|
27
|
+
incoming.get(f.targetRef)?.push(f);
|
|
28
|
+
}
|
|
29
|
+
const attachers = new Map();
|
|
30
|
+
const elements = [];
|
|
31
|
+
for (const n of flowNodes) {
|
|
32
|
+
if (n.type === "boundaryEvent") {
|
|
33
|
+
const be = n;
|
|
34
|
+
if (!attachers.has(be.attachedToRef))
|
|
35
|
+
attachers.set(be.attachedToRef, []);
|
|
36
|
+
attachers.get(be.attachedToRef)?.push(be);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
elements.push(n);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { elements, byId, outgoing, incoming, attachers };
|
|
43
|
+
}
|
|
44
|
+
function isBoundaryAttachedTo(source, elId) {
|
|
45
|
+
return source?.type === "boundaryEvent" && source.attachedToRef === elId;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* True iff the element has a "real" predecessor — an incoming flow that is
|
|
49
|
+
* neither a self-loop nor sourced from a boundary event / its own attacher.
|
|
50
|
+
* Elements without one become traversal starting points.
|
|
51
|
+
*/
|
|
52
|
+
export function hasOtherIncoming(el, graph) {
|
|
53
|
+
const flows = graph.incoming.get(el.id) ?? [];
|
|
54
|
+
for (const f of flows) {
|
|
55
|
+
if (f.sourceRef === el.id)
|
|
56
|
+
continue;
|
|
57
|
+
const source = graph.byId.get(f.sourceRef);
|
|
58
|
+
if (source?.type !== "boundaryEvent")
|
|
59
|
+
return true;
|
|
60
|
+
if (!isBoundaryAttachedTo(source, el.id))
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
/** True iff el is a join (>1 incoming) with at least one not-yet-visited feeder. */
|
|
66
|
+
export function isFutureIncoming(el, visited, graph) {
|
|
67
|
+
const flows = graph.incoming.get(el.id) ?? [];
|
|
68
|
+
if (flows.length <= 1)
|
|
69
|
+
return false;
|
|
70
|
+
return flows.some((f) => !visited.has(f.sourceRef));
|
|
71
|
+
}
|
|
72
|
+
/** True iff some unvisited feeder of el is reachable downstream FROM el (a cycle). */
|
|
73
|
+
export function formsLoop(el, visited, graph) {
|
|
74
|
+
const unvisitedFeeders = (graph.incoming.get(el.id) ?? [])
|
|
75
|
+
.map((f) => f.sourceRef)
|
|
76
|
+
.filter((id) => !visited.has(id));
|
|
77
|
+
for (const feeder of unvisitedFeeders) {
|
|
78
|
+
if (isReachable(el.id, feeder, graph))
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
function isReachable(fromId, targetId, graph) {
|
|
84
|
+
const seen = new Set();
|
|
85
|
+
const stack = [fromId];
|
|
86
|
+
while (stack.length > 0) {
|
|
87
|
+
const id = stack.pop();
|
|
88
|
+
if (id === undefined || seen.has(id))
|
|
89
|
+
continue;
|
|
90
|
+
seen.add(id);
|
|
91
|
+
for (const f of graph.outgoing.get(id) ?? []) {
|
|
92
|
+
if (f.targetRef === targetId)
|
|
93
|
+
return true;
|
|
94
|
+
stack.push(f.targetRef);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=flow-graph.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { BpmnFlowElement, BpmnSequenceFlow } from "../../bpmn/bpmn-model.js";
|
|
2
|
+
import type { LayoutResult } from "../types.js";
|
|
3
|
+
export declare function gridLayoutFlowNodes(flowNodes: BpmnFlowElement[], sequenceFlows: BpmnSequenceFlow[]): LayoutResult;
|
|
4
|
+
//# sourceMappingURL=grid-engine.d.ts.map
|