@bpmnkit/canvas 0.0.28 → 0.0.29
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/canvas.d.ts +126 -1
- package/dist/canvas.js +528 -30
- package/dist/css.d.ts +1 -1
- package/dist/css.js +83 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +3 -1
- package/dist/measure.d.ts +8 -0
- package/dist/measure.js +114 -0
- package/dist/overlays.d.ts +86 -0
- package/dist/overlays.js +140 -0
- package/dist/renderer.d.ts +71 -8
- package/dist/renderer.js +525 -249
- package/dist/scene.d.ts +51 -0
- package/dist/scene.js +140 -0
- package/dist/types.d.ts +94 -3
- package/package.json +2 -2
package/dist/renderer.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readDiColor } from "@bpmnkit/core";
|
|
2
|
+
import { wrapText } from "./measure.js";
|
|
2
3
|
// ── SVG helpers ───────────────────────────────────────────────────────────────
|
|
3
4
|
const NS = "http://www.w3.org/2000/svg";
|
|
4
5
|
function svgEl(tag) {
|
|
@@ -9,36 +10,6 @@ function attr(el, attrs) {
|
|
|
9
10
|
el.setAttribute(k, String(v));
|
|
10
11
|
}
|
|
11
12
|
// ── Text helpers ──────────────────────────────────────────────────────────────
|
|
12
|
-
const AVG_CHAR_PX = 6.5; // approximate width at 11px system-ui
|
|
13
|
-
/**
|
|
14
|
-
* Splits `text` into lines that fit within `maxPx` pixels.
|
|
15
|
-
* Uses an average character-width estimate rather than actual text measurement
|
|
16
|
-
* to keep this dependency-free and synchronous.
|
|
17
|
-
*/
|
|
18
|
-
function wrapText(text, maxPx) {
|
|
19
|
-
if (!text.trim())
|
|
20
|
-
return [];
|
|
21
|
-
const words = text.split(/\s+/);
|
|
22
|
-
const lines = [];
|
|
23
|
-
let line = "";
|
|
24
|
-
for (const word of words) {
|
|
25
|
-
const candidate = line ? `${line} ${word}` : word;
|
|
26
|
-
if (candidate.length * AVG_CHAR_PX <= maxPx) {
|
|
27
|
-
line = candidate;
|
|
28
|
-
}
|
|
29
|
-
else if (line) {
|
|
30
|
-
lines.push(line);
|
|
31
|
-
line = word;
|
|
32
|
-
}
|
|
33
|
-
else {
|
|
34
|
-
// Single word wider than maxPx — use as-is
|
|
35
|
-
line = word;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
if (line)
|
|
39
|
-
lines.push(line);
|
|
40
|
-
return lines.length > 0 ? lines : [text];
|
|
41
|
-
}
|
|
42
13
|
/**
|
|
43
14
|
* Creates a `<text>` element (or multi-line group) centred at (`cx`, `cy`).
|
|
44
15
|
* When `topAlign` is true, multi-line text flows downward from `cy` rather
|
|
@@ -113,6 +84,69 @@ function waypointsToRoundedPath(waypoints) {
|
|
|
113
84
|
}
|
|
114
85
|
return parts.join(" ");
|
|
115
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Returns the point on `geom`'s outline along the ray from its centre toward
|
|
89
|
+
* `toward`. Used to dock connection endpoints onto the true shape outline
|
|
90
|
+
* (circle for events, diamond for gateways) instead of the bounding box.
|
|
91
|
+
*/
|
|
92
|
+
function dockPoint(geom, toward) {
|
|
93
|
+
const cx = geom.x + geom.width / 2;
|
|
94
|
+
const cy = geom.y + geom.height / 2;
|
|
95
|
+
const dx = toward.x - cx;
|
|
96
|
+
const dy = toward.y - cy;
|
|
97
|
+
if (dx === 0 && dy === 0)
|
|
98
|
+
return { x: cx, y: cy };
|
|
99
|
+
if (geom.kind === "circle") {
|
|
100
|
+
const r = Math.min(geom.width, geom.height) / 2;
|
|
101
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
102
|
+
return { x: cx + (dx / len) * r, y: cy + (dy / len) * r };
|
|
103
|
+
}
|
|
104
|
+
if (geom.kind === "diamond") {
|
|
105
|
+
// Ray/diamond intersection: |t·dx|/(w/2) + |t·dy|/(h/2) = 1
|
|
106
|
+
const t = 1 / (Math.abs(dx) / (geom.width / 2) + Math.abs(dy) / (geom.height / 2));
|
|
107
|
+
return { x: cx + dx * t, y: cy + dy * t };
|
|
108
|
+
}
|
|
109
|
+
// Rectangle: scale the ray to the nearest border.
|
|
110
|
+
const tx = dx !== 0 ? geom.width / 2 / Math.abs(dx) : Number.POSITIVE_INFINITY;
|
|
111
|
+
const ty = dy !== 0 ? geom.height / 2 / Math.abs(dy) : Number.POSITIVE_INFINITY;
|
|
112
|
+
const t = Math.min(tx, ty);
|
|
113
|
+
return { x: cx + dx * t, y: cy + dy * t };
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Crops the first and last segments of `waypoints` to the source and target
|
|
117
|
+
* shape outlines. Display-only — the DI waypoints are never mutated.
|
|
118
|
+
*/
|
|
119
|
+
function cropWaypoints(waypoints, source, target) {
|
|
120
|
+
const out = waypoints.map((w) => ({ x: w.x, y: w.y }));
|
|
121
|
+
if (out.length < 2)
|
|
122
|
+
return out;
|
|
123
|
+
if (source) {
|
|
124
|
+
const next = out[1];
|
|
125
|
+
const first = out[0];
|
|
126
|
+
if (next && first)
|
|
127
|
+
out[0] = dockPoint(source, next);
|
|
128
|
+
}
|
|
129
|
+
if (target) {
|
|
130
|
+
const prev = out[out.length - 2];
|
|
131
|
+
const last = out[out.length - 1];
|
|
132
|
+
if (prev && last)
|
|
133
|
+
out[out.length - 1] = dockPoint(target, prev);
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
/** Classifies a shape's docking outline from its BPMN element type. */
|
|
138
|
+
function geomKind(type) {
|
|
139
|
+
if (type === "startEvent" ||
|
|
140
|
+
type === "endEvent" ||
|
|
141
|
+
type === "intermediateCatchEvent" ||
|
|
142
|
+
type === "intermediateThrowEvent" ||
|
|
143
|
+
type === "boundaryEvent") {
|
|
144
|
+
return "circle";
|
|
145
|
+
}
|
|
146
|
+
if (type?.endsWith("Gateway"))
|
|
147
|
+
return "diamond";
|
|
148
|
+
return "rect";
|
|
149
|
+
}
|
|
116
150
|
// ── Task type icons (14×14, origin at 0,0) ───────────────────────────────────
|
|
117
151
|
/**
|
|
118
152
|
* Returns inner SVG markup for task-type icons positioned at the top-left
|
|
@@ -202,25 +236,61 @@ function gatewayMarker(type) {
|
|
|
202
236
|
return "";
|
|
203
237
|
}
|
|
204
238
|
}
|
|
205
|
-
// ──
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
239
|
+
// ── Activity bottom markers ───────────────────────────────────────────────────
|
|
240
|
+
/** Glyphs (each centred at 0,0, ~14×14) for the activity marker row. */
|
|
241
|
+
const MARKER_GLYPHS = {
|
|
242
|
+
miParallel: `<path d="M-4 -5v10M0 -5v10M4 -5v10" class="bpmnkit-icon"/>`,
|
|
243
|
+
miSequential: `<path d="M-5 -4h10M-5 0h10M-5 4h10" class="bpmnkit-icon"/>`,
|
|
244
|
+
// Two left-pointing triangles (compensation "rewind")
|
|
245
|
+
compensation: `<path d="M1 -3.5l-5 3.5 5 3.5zM6 -3.5l-5 3.5 5 3.5z" class="bpmnkit-icon"/>`,
|
|
246
|
+
// Tilde (ad-hoc)
|
|
247
|
+
adHoc: `<path d="M-7 1Q-4 -3 0 1Q4 5 7 1" class="bpmnkit-icon"/>`,
|
|
248
|
+
// Box with a plus (collapsed activity)
|
|
249
|
+
collapsed: `<rect x="-7" y="-7" width="14" height="14" rx="1" class="bpmnkit-icon"/><path d="M0 -4v8M-4 0h8" class="bpmnkit-icon"/>`,
|
|
250
|
+
};
|
|
251
|
+
/**
|
|
252
|
+
* Returns the inner SVG markup for an activity's bottom marker row (loop
|
|
253
|
+
* characteristics, compensation, ad-hoc, and the collapsed `+` marker),
|
|
254
|
+
* laid out as a horizontal row centred at the origin.
|
|
255
|
+
*
|
|
256
|
+
* Callers translate the returned group to the bottom-centre of the activity.
|
|
257
|
+
*/
|
|
258
|
+
function activityMarkers(el, shape, skipCollapsed = false) {
|
|
259
|
+
const glyphs = [];
|
|
260
|
+
const type = el.type;
|
|
261
|
+
// Multi-instance loop
|
|
262
|
+
const lc = "loopCharacteristics" in el ? el.loopCharacteristics : undefined;
|
|
263
|
+
if (lc)
|
|
264
|
+
glyphs.push(lc.isSequential ? MARKER_GLYPHS.miSequential : MARKER_GLYPHS.miParallel);
|
|
265
|
+
// Compensation
|
|
266
|
+
if (el.isForCompensation)
|
|
267
|
+
glyphs.push(MARKER_GLYPHS.compensation);
|
|
268
|
+
// Ad-hoc
|
|
269
|
+
if (type === "adHocSubProcess")
|
|
270
|
+
glyphs.push(MARKER_GLYPHS.adHoc);
|
|
271
|
+
// Collapsed marker: call activities always show it; sub-process variants
|
|
272
|
+
// show it only when collapsed (not expanded into visible children). Skipped
|
|
273
|
+
// when the caller renders an interactive drill-down button instead.
|
|
274
|
+
const isSubProcessType = type === "subProcess" ||
|
|
275
|
+
type === "adHocSubProcess" ||
|
|
276
|
+
type === "eventSubProcess" ||
|
|
277
|
+
type === "transaction";
|
|
278
|
+
if (!skipCollapsed &&
|
|
279
|
+
(type === "callActivity" || (isSubProcessType && shape.isExpanded !== true))) {
|
|
280
|
+
glyphs.push(MARKER_GLYPHS.collapsed);
|
|
222
281
|
}
|
|
223
|
-
|
|
282
|
+
if (glyphs.length === 0)
|
|
283
|
+
return "";
|
|
284
|
+
const step = 16;
|
|
285
|
+
const start = -((glyphs.length - 1) * step) / 2;
|
|
286
|
+
return glyphs
|
|
287
|
+
.map((glyph, i) => `<g transform="translate(${start + i * step} 0)">${glyph}</g>`)
|
|
288
|
+
.join("");
|
|
289
|
+
}
|
|
290
|
+
/** Marker for an event with multiple event definitions (pentagon). */
|
|
291
|
+
function multipleEventMarker(filled) {
|
|
292
|
+
const cls = filled ? "bpmnkit-icon-solid" : "bpmnkit-icon";
|
|
293
|
+
return `<path d="M0 -5.5L5.2 -1.7L3.2 4.5H-3.2L-5.2 -1.7Z" class="${cls}"/>`;
|
|
224
294
|
}
|
|
225
295
|
function buildIndex(defs) {
|
|
226
296
|
const elements = new Map();
|
|
@@ -228,7 +298,9 @@ function buildIndex(defs) {
|
|
|
228
298
|
const annotations = new Map();
|
|
229
299
|
const participants = new Map();
|
|
230
300
|
const lanes = new Map();
|
|
231
|
-
const
|
|
301
|
+
const associations = new Map();
|
|
302
|
+
const groups = new Map();
|
|
303
|
+
const messageFlows = new Map();
|
|
232
304
|
const defaultFlowIds = new Set();
|
|
233
305
|
function indexProcess(flowElements, sequenceFlows) {
|
|
234
306
|
for (const el of flowElements) {
|
|
@@ -262,6 +334,12 @@ function buildIndex(defs) {
|
|
|
262
334
|
for (const ta of proc.textAnnotations) {
|
|
263
335
|
annotations.set(ta.id, ta);
|
|
264
336
|
}
|
|
337
|
+
for (const assoc of proc.associations) {
|
|
338
|
+
associations.set(assoc.id, assoc);
|
|
339
|
+
}
|
|
340
|
+
for (const group of proc.groups) {
|
|
341
|
+
groups.set(group.id, group);
|
|
342
|
+
}
|
|
265
343
|
if (proc.laneSet)
|
|
266
344
|
indexLaneSet(proc.laneSet);
|
|
267
345
|
}
|
|
@@ -272,11 +350,27 @@ function buildIndex(defs) {
|
|
|
272
350
|
for (const ta of collab.textAnnotations) {
|
|
273
351
|
annotations.set(ta.id, ta);
|
|
274
352
|
}
|
|
353
|
+
for (const assoc of collab.associations) {
|
|
354
|
+
associations.set(assoc.id, assoc);
|
|
355
|
+
}
|
|
356
|
+
for (const group of collab.groups) {
|
|
357
|
+
groups.set(group.id, group);
|
|
358
|
+
}
|
|
275
359
|
for (const mf of collab.messageFlows) {
|
|
276
|
-
|
|
360
|
+
messageFlows.set(mf.id, mf);
|
|
277
361
|
}
|
|
278
362
|
}
|
|
279
|
-
return {
|
|
363
|
+
return {
|
|
364
|
+
elements,
|
|
365
|
+
flows,
|
|
366
|
+
annotations,
|
|
367
|
+
participants,
|
|
368
|
+
lanes,
|
|
369
|
+
associations,
|
|
370
|
+
groups,
|
|
371
|
+
messageFlows,
|
|
372
|
+
defaultFlowIds,
|
|
373
|
+
};
|
|
280
374
|
}
|
|
281
375
|
// ── Color helper ─────────────────────────────────────────────────────────────
|
|
282
376
|
/** Applies bioc/color namespace attributes as inline style on a shape body element. */
|
|
@@ -326,14 +420,16 @@ function renderEvent(shape, el, instanceId) {
|
|
|
326
420
|
});
|
|
327
421
|
g.appendChild(inner);
|
|
328
422
|
}
|
|
329
|
-
// Event definition marker
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
: undefined;
|
|
333
|
-
if (eventDef) {
|
|
423
|
+
// Event definition marker(s)
|
|
424
|
+
const eventDefs = el && "eventDefinitions" in el ? el.eventDefinitions : [];
|
|
425
|
+
if (eventDefs.length > 0) {
|
|
334
426
|
const markerG = svgEl("g");
|
|
335
427
|
attr(markerG, { transform: `translate(${cx} ${cy})` });
|
|
336
|
-
|
|
428
|
+
// Multiple event definitions render as a single "multiple" pentagon.
|
|
429
|
+
markerG.innerHTML =
|
|
430
|
+
eventDefs.length > 1
|
|
431
|
+
? multipleEventMarker(isThrow && !isEnd)
|
|
432
|
+
: eventMarker(eventDefs[0].type, isThrow && !isEnd);
|
|
337
433
|
g.appendChild(markerG);
|
|
338
434
|
}
|
|
339
435
|
// Accessibility
|
|
@@ -348,7 +444,7 @@ function renderEvent(shape, el, instanceId) {
|
|
|
348
444
|
});
|
|
349
445
|
return g;
|
|
350
446
|
}
|
|
351
|
-
function renderTask(shape, el, instanceId) {
|
|
447
|
+
function renderTask(shape, el, instanceId, drillable = false) {
|
|
352
448
|
const { width, height } = shape.bounds;
|
|
353
449
|
const g = svgEl("g");
|
|
354
450
|
// Body
|
|
@@ -398,17 +494,33 @@ function renderTask(shape, el, instanceId) {
|
|
|
398
494
|
const labelEl = makeLabel(el.name, width / 2, height / 2, labelMaxW);
|
|
399
495
|
g.appendChild(labelEl);
|
|
400
496
|
}
|
|
401
|
-
//
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
el
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
497
|
+
// Activity markers (multi-instance, compensation, ad-hoc, collapsed `+`)
|
|
498
|
+
// at bottom centre. Drillable sub-processes render the collapsed `+` via the
|
|
499
|
+
// interactive drill-down button below, so it is skipped here.
|
|
500
|
+
if (el) {
|
|
501
|
+
const markers = activityMarkers(el, shape, drillable);
|
|
502
|
+
if (markers) {
|
|
503
|
+
const markerG = svgEl("g");
|
|
504
|
+
attr(markerG, { transform: `translate(${width / 2} ${height - 10})` });
|
|
505
|
+
markerG.innerHTML = markers;
|
|
506
|
+
g.appendChild(markerG);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// Drill-down affordance: a clickable `+` button at bottom centre that opens
|
|
510
|
+
// the sub-process's own plane.
|
|
511
|
+
if (drillable) {
|
|
512
|
+
const btn = svgEl("g");
|
|
513
|
+
attr(btn, {
|
|
514
|
+
class: "bpmnkit-drilldown",
|
|
515
|
+
transform: `translate(${width / 2} ${height - 10})`,
|
|
516
|
+
"data-bpmnkit-drilldown": shape.bpmnElement,
|
|
517
|
+
"aria-label": "Open sub-process",
|
|
518
|
+
role: "button",
|
|
519
|
+
});
|
|
520
|
+
btn.innerHTML =
|
|
521
|
+
`<rect x="-8" y="-8" width="16" height="16" rx="2" class="bpmnkit-drilldown-box"/>` +
|
|
522
|
+
`<path d="M0 -4v8M-4 0h8" class="bpmnkit-icon"/>`;
|
|
523
|
+
g.appendChild(btn);
|
|
412
524
|
}
|
|
413
525
|
const label = el?.name ?? el?.type ?? "task";
|
|
414
526
|
attr(g, {
|
|
@@ -419,6 +531,16 @@ function renderTask(shape, el, instanceId) {
|
|
|
419
531
|
"data-bpmnkit-id": shape.bpmnElement,
|
|
420
532
|
"data-bpmnkit-instance": instanceId,
|
|
421
533
|
});
|
|
534
|
+
// Expandable containers announce their collapsed/expanded state to AT.
|
|
535
|
+
const type = el?.type;
|
|
536
|
+
const isContainer = type === "subProcess" ||
|
|
537
|
+
type === "adHocSubProcess" ||
|
|
538
|
+
type === "eventSubProcess" ||
|
|
539
|
+
type === "transaction" ||
|
|
540
|
+
type === "callActivity";
|
|
541
|
+
if (isContainer) {
|
|
542
|
+
g.setAttribute("aria-expanded", type === "callActivity" ? "false" : String(shape.isExpanded === true));
|
|
543
|
+
}
|
|
422
544
|
return g;
|
|
423
545
|
}
|
|
424
546
|
function renderGateway(shape, el, instanceId) {
|
|
@@ -453,67 +575,50 @@ function renderGateway(shape, el, instanceId) {
|
|
|
453
575
|
});
|
|
454
576
|
return g;
|
|
455
577
|
}
|
|
456
|
-
|
|
578
|
+
const TITLE_BAR = 30;
|
|
579
|
+
/**
|
|
580
|
+
* Renders a swimlane (pool or lane). The title bar sits on the left with
|
|
581
|
+
* rotated text for horizontal lanes (the default), or across the top with
|
|
582
|
+
* upright text when `shape.isHorizontal === false` (vertical pools/lanes).
|
|
583
|
+
*/
|
|
584
|
+
function renderSwimlane(shape, name, kind, instanceId) {
|
|
457
585
|
const { width, height } = shape.bounds;
|
|
586
|
+
const vertical = shape.isHorizontal === false;
|
|
458
587
|
const g = svgEl("g");
|
|
459
|
-
// Pool body
|
|
460
588
|
const bg = svgEl("rect");
|
|
461
|
-
attr(bg, { x: 0, y: 0, width, height, class:
|
|
589
|
+
attr(bg, { x: 0, y: 0, width, height, class: `bpmnkit-${kind}-body` });
|
|
462
590
|
g.appendChild(bg);
|
|
463
|
-
// Title bar (left column, 30px wide)
|
|
464
591
|
const titleBar = svgEl("rect");
|
|
465
|
-
attr(titleBar,
|
|
592
|
+
attr(titleBar, vertical
|
|
593
|
+
? { x: 0, y: 0, width, height: TITLE_BAR, class: `bpmnkit-${kind}-header` }
|
|
594
|
+
: { x: 0, y: 0, width: TITLE_BAR, height, class: `bpmnkit-${kind}-header` });
|
|
466
595
|
g.appendChild(titleBar);
|
|
467
|
-
|
|
468
|
-
if (participant?.name) {
|
|
596
|
+
if (name) {
|
|
469
597
|
const text = svgEl("text");
|
|
470
|
-
attr(text,
|
|
471
|
-
class: "bpmnkit-label",
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
598
|
+
attr(text, vertical
|
|
599
|
+
? { class: "bpmnkit-label", x: width / 2, y: TITLE_BAR / 2 }
|
|
600
|
+
: {
|
|
601
|
+
class: "bpmnkit-label",
|
|
602
|
+
transform: `translate(${TITLE_BAR / 2} ${height / 2}) rotate(-90)`,
|
|
603
|
+
});
|
|
604
|
+
text.textContent = name;
|
|
475
605
|
g.appendChild(text);
|
|
476
606
|
}
|
|
477
607
|
attr(g, {
|
|
478
|
-
class:
|
|
608
|
+
class: `bpmnkit-shape bpmnkit-${kind}`,
|
|
479
609
|
tabindex: "-1",
|
|
480
610
|
role: "region",
|
|
481
|
-
"aria-label":
|
|
611
|
+
"aria-label": name ?? (kind === "pool" ? "Pool" : "Lane"),
|
|
482
612
|
"data-bpmnkit-id": shape.bpmnElement,
|
|
483
613
|
"data-bpmnkit-instance": instanceId,
|
|
484
614
|
});
|
|
485
615
|
return g;
|
|
486
616
|
}
|
|
617
|
+
function renderPool(shape, participant, instanceId) {
|
|
618
|
+
return renderSwimlane(shape, participant?.name, "pool", instanceId);
|
|
619
|
+
}
|
|
487
620
|
function renderLane(shape, lane, instanceId) {
|
|
488
|
-
|
|
489
|
-
const g = svgEl("g");
|
|
490
|
-
// Lane body
|
|
491
|
-
const bg = svgEl("rect");
|
|
492
|
-
attr(bg, { x: 0, y: 0, width, height, class: "bpmnkit-lane-body" });
|
|
493
|
-
g.appendChild(bg);
|
|
494
|
-
// Title bar (left column, 30px wide)
|
|
495
|
-
const titleBar = svgEl("rect");
|
|
496
|
-
attr(titleBar, { x: 0, y: 0, width: 30, height, class: "bpmnkit-lane-header" });
|
|
497
|
-
g.appendChild(titleBar);
|
|
498
|
-
// Lane name (rotated in title bar)
|
|
499
|
-
if (lane?.name) {
|
|
500
|
-
const text = svgEl("text");
|
|
501
|
-
attr(text, {
|
|
502
|
-
class: "bpmnkit-label",
|
|
503
|
-
transform: `translate(15 ${height / 2}) rotate(-90)`,
|
|
504
|
-
});
|
|
505
|
-
text.textContent = lane.name;
|
|
506
|
-
g.appendChild(text);
|
|
507
|
-
}
|
|
508
|
-
attr(g, {
|
|
509
|
-
class: "bpmnkit-shape bpmnkit-lane",
|
|
510
|
-
tabindex: "-1",
|
|
511
|
-
role: "region",
|
|
512
|
-
"aria-label": lane?.name ?? "Lane",
|
|
513
|
-
"data-bpmnkit-id": shape.bpmnElement,
|
|
514
|
-
"data-bpmnkit-instance": instanceId,
|
|
515
|
-
});
|
|
516
|
-
return g;
|
|
621
|
+
return renderSwimlane(shape, lane?.name, "lane", instanceId);
|
|
517
622
|
}
|
|
518
623
|
function renderAnnotation(shape, text, instanceId) {
|
|
519
624
|
const { width, height } = shape.bounds;
|
|
@@ -543,6 +648,96 @@ function renderAnnotation(shape, text, instanceId) {
|
|
|
543
648
|
});
|
|
544
649
|
return g;
|
|
545
650
|
}
|
|
651
|
+
function renderDataObjectReference(shape, el, instanceId) {
|
|
652
|
+
const { width, height } = shape.bounds;
|
|
653
|
+
const fold = Math.min(14, width / 3);
|
|
654
|
+
const g = svgEl("g");
|
|
655
|
+
// Document outline with a folded top-right corner.
|
|
656
|
+
const body = svgEl("path");
|
|
657
|
+
attr(body, {
|
|
658
|
+
d: `M0 0 H${width - fold} L${width} ${fold} V${height} H0 Z`,
|
|
659
|
+
class: "bpmnkit-data-body",
|
|
660
|
+
});
|
|
661
|
+
applyColor(body, shape);
|
|
662
|
+
g.appendChild(body);
|
|
663
|
+
const foldPath = svgEl("path");
|
|
664
|
+
attr(foldPath, { d: `M${width - fold} 0 V${fold} H${width}`, class: "bpmnkit-icon" });
|
|
665
|
+
g.appendChild(foldPath);
|
|
666
|
+
// Collection marker (three vertical bars) at bottom centre.
|
|
667
|
+
if (el?.type === "dataObjectReference" && el.isCollection) {
|
|
668
|
+
const marker = svgEl("g");
|
|
669
|
+
attr(marker, { transform: `translate(${width / 2} ${height - 6})` });
|
|
670
|
+
marker.innerHTML = `<path d="M-3 -5v9M0 -5v9M3 -5v9" class="bpmnkit-icon"/>`;
|
|
671
|
+
g.appendChild(marker);
|
|
672
|
+
}
|
|
673
|
+
attr(g, {
|
|
674
|
+
class: "bpmnkit-shape",
|
|
675
|
+
tabindex: "-1",
|
|
676
|
+
role: "img",
|
|
677
|
+
"aria-label": el?.name ?? "Data object",
|
|
678
|
+
"data-bpmnkit-id": shape.bpmnElement,
|
|
679
|
+
"data-bpmnkit-instance": instanceId,
|
|
680
|
+
});
|
|
681
|
+
return g;
|
|
682
|
+
}
|
|
683
|
+
function renderDataStoreReference(shape, el, instanceId) {
|
|
684
|
+
const { width, height } = shape.bounds;
|
|
685
|
+
const ry = Math.min(8, height / 6);
|
|
686
|
+
const g = svgEl("g");
|
|
687
|
+
// Cylinder body.
|
|
688
|
+
const body = svgEl("path");
|
|
689
|
+
attr(body, {
|
|
690
|
+
d: `M0 ${ry} A ${width / 2} ${ry} 0 0 0 ${width} ${ry} V ${height - ry} A ${width / 2} ${ry} 0 0 1 0 ${height - ry} Z`,
|
|
691
|
+
class: "bpmnkit-datastore-body",
|
|
692
|
+
});
|
|
693
|
+
applyColor(body, shape);
|
|
694
|
+
g.appendChild(body);
|
|
695
|
+
// Top ellipse + a couple of stacked-disk arcs.
|
|
696
|
+
const top = svgEl("ellipse");
|
|
697
|
+
attr(top, { cx: width / 2, cy: ry, rx: width / 2, ry, class: "bpmnkit-datastore-body" });
|
|
698
|
+
applyColor(top, shape);
|
|
699
|
+
g.appendChild(top);
|
|
700
|
+
const disks = svgEl("path");
|
|
701
|
+
attr(disks, {
|
|
702
|
+
d: `M0 ${ry * 2} A ${width / 2} ${ry} 0 0 0 ${width} ${ry * 2} M0 ${ry * 3.4} A ${width / 2} ${ry} 0 0 0 ${width} ${ry * 3.4}`,
|
|
703
|
+
class: "bpmnkit-icon",
|
|
704
|
+
});
|
|
705
|
+
g.appendChild(disks);
|
|
706
|
+
attr(g, {
|
|
707
|
+
class: "bpmnkit-shape",
|
|
708
|
+
tabindex: "-1",
|
|
709
|
+
role: "img",
|
|
710
|
+
"aria-label": el?.name ?? "Data store",
|
|
711
|
+
"data-bpmnkit-id": shape.bpmnElement,
|
|
712
|
+
"data-bpmnkit-instance": instanceId,
|
|
713
|
+
});
|
|
714
|
+
return g;
|
|
715
|
+
}
|
|
716
|
+
function renderGroup(shape, instanceId) {
|
|
717
|
+
const { width, height } = shape.bounds;
|
|
718
|
+
const g = svgEl("g");
|
|
719
|
+
// Dashed rounded rectangle; border-only hit target so the interior is
|
|
720
|
+
// click-through (elements inside a group stay selectable).
|
|
721
|
+
const body = svgEl("rect");
|
|
722
|
+
attr(body, {
|
|
723
|
+
x: 0,
|
|
724
|
+
y: 0,
|
|
725
|
+
width,
|
|
726
|
+
height,
|
|
727
|
+
rx: 8,
|
|
728
|
+
class: "bpmnkit-group-body",
|
|
729
|
+
"pointer-events": "stroke",
|
|
730
|
+
});
|
|
731
|
+
g.appendChild(body);
|
|
732
|
+
attr(g, {
|
|
733
|
+
class: "bpmnkit-shape bpmnkit-group",
|
|
734
|
+
tabindex: "-1",
|
|
735
|
+
role: "group",
|
|
736
|
+
"data-bpmnkit-id": shape.bpmnElement,
|
|
737
|
+
"data-bpmnkit-instance": instanceId,
|
|
738
|
+
});
|
|
739
|
+
return g;
|
|
740
|
+
}
|
|
546
741
|
// ── External label ────────────────────────────────────────────────────────────
|
|
547
742
|
/**
|
|
548
743
|
* Renders an external label at absolute diagram coordinates.
|
|
@@ -558,25 +753,31 @@ function renderExternalLabel(absX, absY, labelW, labelH, text, topAlign = false)
|
|
|
558
753
|
return g;
|
|
559
754
|
}
|
|
560
755
|
// ── Edge renderer ─────────────────────────────────────────────────────────────
|
|
561
|
-
function renderEdge(edge, flow,
|
|
756
|
+
function renderEdge(edge, flow, instanceId, isDefault, isConditional, waypoints) {
|
|
757
|
+
const ids = markerIds(instanceId);
|
|
562
758
|
const g = svgEl("g");
|
|
563
759
|
attr(g, {
|
|
564
760
|
class: "bpmnkit-edge",
|
|
565
761
|
"data-bpmnkit-id": edge.bpmnElement,
|
|
566
762
|
});
|
|
567
|
-
if (
|
|
763
|
+
if (waypoints.length < 2)
|
|
568
764
|
return g;
|
|
569
765
|
const path = svgEl("path");
|
|
570
|
-
|
|
571
|
-
d: waypointsToRoundedPath(
|
|
766
|
+
const pathAttrs = {
|
|
767
|
+
d: waypointsToRoundedPath(waypoints),
|
|
572
768
|
class: "bpmnkit-edge-path",
|
|
573
|
-
"marker-end": `url(#${
|
|
574
|
-
}
|
|
769
|
+
"marker-end": `url(#${ids.arrow})`,
|
|
770
|
+
};
|
|
771
|
+
// Conditional sequence flow → diamond at the source (mutually exclusive
|
|
772
|
+
// with the default-flow slash: a flow is never both).
|
|
773
|
+
if (isConditional)
|
|
774
|
+
pathAttrs["marker-start"] = `url(#${ids.conditional})`;
|
|
775
|
+
attr(path, pathAttrs);
|
|
575
776
|
g.appendChild(path);
|
|
576
777
|
// Wide transparent stroke so the edge is easy to click
|
|
577
778
|
const hitPath = svgEl("path");
|
|
578
779
|
attr(hitPath, {
|
|
579
|
-
d: waypointsToRoundedPath(
|
|
780
|
+
d: waypointsToRoundedPath(waypoints),
|
|
580
781
|
fill: "none",
|
|
581
782
|
stroke: "transparent",
|
|
582
783
|
"stroke-width": "12",
|
|
@@ -585,8 +786,8 @@ function renderEdge(edge, flow, markerId, isDefault) {
|
|
|
585
786
|
g.appendChild(hitPath);
|
|
586
787
|
// Default-flow slash mark near the source end
|
|
587
788
|
if (isDefault) {
|
|
588
|
-
const wp0 =
|
|
589
|
-
const wp1 =
|
|
789
|
+
const wp0 = waypoints[0];
|
|
790
|
+
const wp1 = waypoints[1];
|
|
590
791
|
if (wp0 && wp1) {
|
|
591
792
|
const dx = wp1.x - wp0.x;
|
|
592
793
|
const dy = wp1.y - wp0.y;
|
|
@@ -618,31 +819,67 @@ function renderEdge(edge, flow, markerId, isDefault) {
|
|
|
618
819
|
}
|
|
619
820
|
return g;
|
|
620
821
|
}
|
|
621
|
-
function renderAssociation(edge) {
|
|
822
|
+
function renderAssociation(edge, association, instanceId, waypoints) {
|
|
823
|
+
const ids = markerIds(instanceId);
|
|
622
824
|
const g = svgEl("g");
|
|
623
825
|
attr(g, { class: "bpmnkit-edge", "data-bpmnkit-id": edge.bpmnElement });
|
|
624
826
|
const path = svgEl("path");
|
|
625
|
-
|
|
827
|
+
const pathAttrs = {
|
|
828
|
+
d: waypointsToRoundedPath(waypoints),
|
|
829
|
+
class: "bpmnkit-edge-assoc",
|
|
830
|
+
};
|
|
831
|
+
// Directed associations get an open arrowhead; "Both" is bidirectional.
|
|
832
|
+
const direction = association?.associationDirection;
|
|
833
|
+
if (direction === "One" || direction === "Both") {
|
|
834
|
+
pathAttrs["marker-end"] = `url(#${ids.openArrow})`;
|
|
835
|
+
}
|
|
836
|
+
if (direction === "Both") {
|
|
837
|
+
pathAttrs["marker-start"] = `url(#${ids.openArrow})`;
|
|
838
|
+
}
|
|
839
|
+
attr(path, pathAttrs);
|
|
626
840
|
g.appendChild(path);
|
|
627
841
|
return g;
|
|
628
842
|
}
|
|
629
|
-
|
|
843
|
+
/** Derives this canvas instance's per-marker element IDs. */
|
|
844
|
+
function markerIds(instanceId) {
|
|
845
|
+
return {
|
|
846
|
+
arrow: `bpmnkit-arrow-${instanceId}`,
|
|
847
|
+
openArrow: `bpmnkit-open-arrow-${instanceId}`,
|
|
848
|
+
conditional: `bpmnkit-conditional-${instanceId}`,
|
|
849
|
+
messageStart: `bpmnkit-msgstart-${instanceId}`,
|
|
850
|
+
};
|
|
851
|
+
}
|
|
630
852
|
/**
|
|
631
853
|
* Creates the SVG `<defs>` section for this canvas instance.
|
|
632
854
|
* Uses `instanceId` to make marker IDs unique per canvas, avoiding
|
|
633
855
|
* conflicts when multiple canvases are mounted on the same page.
|
|
856
|
+
*
|
|
857
|
+
* @returns the filled-arrowhead marker ID (sequence-flow terminus). Other
|
|
858
|
+
* marker IDs are derived from `instanceId` via {@link markerIds}.
|
|
634
859
|
*/
|
|
635
860
|
export function createDefs(svg, instanceId) {
|
|
636
|
-
const
|
|
861
|
+
const ids = markerIds(instanceId);
|
|
637
862
|
const defs = svgEl("defs");
|
|
638
863
|
defs.innerHTML = `
|
|
639
|
-
<marker id="${
|
|
864
|
+
<marker id="${ids.arrow}" markerWidth="8" markerHeight="6"
|
|
640
865
|
refX="7" refY="3" orient="auto" markerUnits="strokeWidth">
|
|
641
866
|
<path d="M0,0 L8,3 L0,6 Z" class="bpmnkit-arrow-fill"/>
|
|
642
867
|
</marker>
|
|
868
|
+
<marker id="${ids.openArrow}" markerWidth="14" markerHeight="10"
|
|
869
|
+
refX="10" refY="5" orient="auto" markerUnits="userSpaceOnUse">
|
|
870
|
+
<path d="M1,1 L10,5 L1,9" class="bpmnkit-open-arrow"/>
|
|
871
|
+
</marker>
|
|
872
|
+
<marker id="${ids.conditional}" markerWidth="16" markerHeight="10"
|
|
873
|
+
refX="0" refY="5" orient="auto" markerUnits="userSpaceOnUse">
|
|
874
|
+
<path d="M0,5 L7,1 L14,5 L7,9 Z" class="bpmnkit-conditional-marker"/>
|
|
875
|
+
</marker>
|
|
876
|
+
<marker id="${ids.messageStart}" markerWidth="12" markerHeight="10"
|
|
877
|
+
refX="0" refY="5" orient="auto" markerUnits="userSpaceOnUse">
|
|
878
|
+
<circle cx="5" cy="5" r="4" class="bpmnkit-msg-marker"/>
|
|
879
|
+
</marker>
|
|
643
880
|
`;
|
|
644
881
|
svg.appendChild(defs);
|
|
645
|
-
return
|
|
882
|
+
return ids.arrow;
|
|
646
883
|
}
|
|
647
884
|
// ── Dot-grid background ───────────────────────────────────────────────────────
|
|
648
885
|
/**
|
|
@@ -679,147 +916,186 @@ export function createGrid(svg, instanceId) {
|
|
|
679
916
|
svg.appendChild(bg);
|
|
680
917
|
return pattern;
|
|
681
918
|
}
|
|
919
|
+
/** Builds a {@link RenderContext} for a plane (index + docking geometry). */
|
|
920
|
+
export function buildRenderContext(defs, plane, drillableIds, instanceId) {
|
|
921
|
+
const index = buildIndex(defs);
|
|
922
|
+
const geomById = new Map();
|
|
923
|
+
for (const shape of plane.shapes) {
|
|
924
|
+
const { x, y, width, height } = shape.bounds;
|
|
925
|
+
geomById.set(shape.bpmnElement, {
|
|
926
|
+
x,
|
|
927
|
+
y,
|
|
928
|
+
width,
|
|
929
|
+
height,
|
|
930
|
+
kind: geomKind(index.elements.get(shape.bpmnElement)?.type),
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
return { index, geomById, drillableIds, instanceId };
|
|
934
|
+
}
|
|
935
|
+
/** Renders a single edge's `<g>` (no DOM insertion). */
|
|
936
|
+
export function renderEdgeGroup(edge, ctx) {
|
|
937
|
+
const { index, geomById, instanceId } = ctx;
|
|
938
|
+
const flow = index.flows.get(edge.bpmnElement);
|
|
939
|
+
if (flow) {
|
|
940
|
+
const isDefault = index.defaultFlowIds.has(edge.bpmnElement);
|
|
941
|
+
// A conditional flow shows a diamond at its source, but only when the
|
|
942
|
+
// source is an activity (not a gateway) and it is not the default flow.
|
|
943
|
+
const source = index.elements.get(flow.sourceRef);
|
|
944
|
+
const sourceIsGateway = source?.type.endsWith("Gateway") ?? false;
|
|
945
|
+
const isConditional = !!flow.conditionExpression && !isDefault && !sourceIsGateway;
|
|
946
|
+
const wps = cropWaypoints(edge.waypoints, geomById.get(flow.sourceRef), geomById.get(flow.targetRef));
|
|
947
|
+
return renderEdge(edge, flow, instanceId, isDefault, isConditional, wps);
|
|
948
|
+
}
|
|
949
|
+
if (index.messageFlows.has(edge.bpmnElement)) {
|
|
950
|
+
// Message flow — dashed line with a hollow source circle and an open
|
|
951
|
+
// arrowhead at the target.
|
|
952
|
+
const g = svgEl("g");
|
|
953
|
+
attr(g, { class: "bpmnkit-edge", "data-bpmnkit-id": edge.bpmnElement });
|
|
954
|
+
if (edge.waypoints.length >= 2) {
|
|
955
|
+
const mf = index.messageFlows.get(edge.bpmnElement);
|
|
956
|
+
const wps = cropWaypoints(edge.waypoints, mf ? geomById.get(mf.sourceRef) : undefined, mf ? geomById.get(mf.targetRef) : undefined);
|
|
957
|
+
const ids = markerIds(instanceId);
|
|
958
|
+
const path = svgEl("path");
|
|
959
|
+
attr(path, {
|
|
960
|
+
d: waypointsToRoundedPath(wps),
|
|
961
|
+
class: "bpmnkit-msgflow-path",
|
|
962
|
+
"marker-start": `url(#${ids.messageStart})`,
|
|
963
|
+
"marker-end": `url(#${ids.openArrow})`,
|
|
964
|
+
});
|
|
965
|
+
g.appendChild(path);
|
|
966
|
+
}
|
|
967
|
+
return g;
|
|
968
|
+
}
|
|
969
|
+
// Association or unknown edge type
|
|
970
|
+
const assoc = index.associations.get(edge.bpmnElement);
|
|
971
|
+
const wps = cropWaypoints(edge.waypoints, assoc ? geomById.get(assoc.sourceRef) : undefined, assoc ? geomById.get(assoc.targetRef) : undefined);
|
|
972
|
+
return renderAssociation(edge, assoc, instanceId, wps);
|
|
973
|
+
}
|
|
974
|
+
/** Renders a single shape's `<g>` (+ optional external label), without DOM insertion. */
|
|
975
|
+
export function renderShapeGroup(shape, ctx) {
|
|
976
|
+
const { index, drillableIds, instanceId } = ctx;
|
|
977
|
+
const el = index.elements.get(shape.bpmnElement);
|
|
978
|
+
const { x, y } = shape.bounds;
|
|
979
|
+
const type = el?.type ?? "";
|
|
980
|
+
const place = (group, layer, annotation) => {
|
|
981
|
+
attr(group, { transform: `translate(${x} ${y})` });
|
|
982
|
+
return {
|
|
983
|
+
group,
|
|
984
|
+
layer,
|
|
985
|
+
label: null,
|
|
986
|
+
rendered: { id: shape.bpmnElement, element: group, shape, flowElement: el, annotation },
|
|
987
|
+
};
|
|
988
|
+
};
|
|
989
|
+
if (type === "startEvent" ||
|
|
990
|
+
type === "endEvent" ||
|
|
991
|
+
type === "intermediateCatchEvent" ||
|
|
992
|
+
type === "intermediateThrowEvent" ||
|
|
993
|
+
type === "boundaryEvent") {
|
|
994
|
+
return withExternalLabel(place(renderEvent(shape, el, instanceId), "shapes"), shape, el);
|
|
995
|
+
}
|
|
996
|
+
if (type === "exclusiveGateway" ||
|
|
997
|
+
type === "parallelGateway" ||
|
|
998
|
+
type === "inclusiveGateway" ||
|
|
999
|
+
type === "eventBasedGateway" ||
|
|
1000
|
+
type === "complexGateway") {
|
|
1001
|
+
return withExternalLabel(place(renderGateway(shape, el, instanceId), "shapes"), shape, el);
|
|
1002
|
+
}
|
|
1003
|
+
if (type === "dataObjectReference") {
|
|
1004
|
+
return withExternalLabel(place(renderDataObjectReference(shape, el, instanceId), "shapes"), shape, el);
|
|
1005
|
+
}
|
|
1006
|
+
if (type === "dataStoreReference") {
|
|
1007
|
+
return withExternalLabel(place(renderDataStoreReference(shape, el, instanceId), "shapes"), shape, el);
|
|
1008
|
+
}
|
|
1009
|
+
if (type === "" && !el) {
|
|
1010
|
+
if (index.groups.has(shape.bpmnElement))
|
|
1011
|
+
return place(renderGroup(shape, instanceId), "containers");
|
|
1012
|
+
const annotation = index.annotations.get(shape.bpmnElement);
|
|
1013
|
+
if (annotation !== undefined) {
|
|
1014
|
+
return place(renderAnnotation(shape, annotation.text, instanceId), "shapes", annotation);
|
|
1015
|
+
}
|
|
1016
|
+
if (index.participants.has(shape.bpmnElement)) {
|
|
1017
|
+
return place(renderPool(shape, index.participants.get(shape.bpmnElement), instanceId), "containers");
|
|
1018
|
+
}
|
|
1019
|
+
if (index.lanes.has(shape.bpmnElement)) {
|
|
1020
|
+
return place(renderLane(shape, index.lanes.get(shape.bpmnElement), instanceId), "containers");
|
|
1021
|
+
}
|
|
1022
|
+
// Unknown shape — invisible placeholder
|
|
1023
|
+
const g = svgEl("g");
|
|
1024
|
+
attr(g, { "data-bpmnkit-id": shape.bpmnElement, "data-bpmnkit-instance": instanceId });
|
|
1025
|
+
return place(g, "shapes");
|
|
1026
|
+
}
|
|
1027
|
+
return withExternalLabel(place(renderTask(shape, el, instanceId, drillableIds.has(shape.bpmnElement)), "shapes"), shape, el);
|
|
1028
|
+
}
|
|
1029
|
+
const EXTERNAL_LABEL_TYPES = new Set([
|
|
1030
|
+
"startEvent",
|
|
1031
|
+
"endEvent",
|
|
1032
|
+
"intermediateCatchEvent",
|
|
1033
|
+
"intermediateThrowEvent",
|
|
1034
|
+
"boundaryEvent",
|
|
1035
|
+
"exclusiveGateway",
|
|
1036
|
+
"parallelGateway",
|
|
1037
|
+
"inclusiveGateway",
|
|
1038
|
+
"eventBasedGateway",
|
|
1039
|
+
"complexGateway",
|
|
1040
|
+
"dataObjectReference",
|
|
1041
|
+
"dataStoreReference",
|
|
1042
|
+
]);
|
|
1043
|
+
/** Attaches an external label (below the shape) to a rendered shape group, if applicable. */
|
|
1044
|
+
function withExternalLabel(result, shape, el) {
|
|
1045
|
+
if (el?.name && EXTERNAL_LABEL_TYPES.has(el.type)) {
|
|
1046
|
+
const lb = shape.label?.bounds ?? {
|
|
1047
|
+
x: shape.bounds.x + shape.bounds.width / 2 - 40,
|
|
1048
|
+
y: shape.bounds.y + shape.bounds.height + 6,
|
|
1049
|
+
width: 80,
|
|
1050
|
+
height: 20,
|
|
1051
|
+
};
|
|
1052
|
+
// topAlign=true: multi-line text flows downward from the top of the label
|
|
1053
|
+
// bounds, so long labels never extend upward into the shape.
|
|
1054
|
+
result.label = renderExternalLabel(lb.x, lb.y, lb.width, lb.height, el.name, true);
|
|
1055
|
+
}
|
|
1056
|
+
return result;
|
|
1057
|
+
}
|
|
682
1058
|
/**
|
|
683
|
-
* Renders a `BpmnDefinitions` model into SVG element groups, appending them
|
|
684
|
-
*
|
|
1059
|
+
* Renders a `BpmnDefinitions` model into SVG element groups, appending them to
|
|
1060
|
+
* the provided layers. Thin wrapper over {@link buildRenderContext} +
|
|
1061
|
+
* {@link renderEdgeGroup}/{@link renderShapeGroup}, retained for existing
|
|
1062
|
+
* callers (the editor). New code should prefer the {@link Scene} class.
|
|
685
1063
|
*
|
|
686
1064
|
* Edges are placed below shapes (rendered first) so connection lines don't
|
|
687
|
-
* cover shape bodies.
|
|
1065
|
+
* cover shape bodies. Shapes are rendered in DI order so container shapes
|
|
688
1066
|
* (sub-processes) appear before their children.
|
|
689
1067
|
*/
|
|
690
|
-
export function render(defs, containersLayer, edgesLayer, shapesLayer, labelsLayer,
|
|
691
|
-
|
|
1068
|
+
export function render(defs, containersLayer, edgesLayer, shapesLayer, labelsLayer, _markerId, instanceId,
|
|
1069
|
+
/** The DI plane to render. Defaults to the first diagram's plane. */
|
|
1070
|
+
targetPlane,
|
|
1071
|
+
/** Element ids that own their own plane and can be drilled into. */
|
|
1072
|
+
drillableIds = new Set()) {
|
|
692
1073
|
const shapes = [];
|
|
693
1074
|
const edges = [];
|
|
694
|
-
const plane = defs.diagrams[0]?.plane;
|
|
1075
|
+
const plane = targetPlane ?? defs.diagrams[0]?.plane;
|
|
695
1076
|
if (!plane)
|
|
696
1077
|
return { shapes, edges };
|
|
697
|
-
|
|
1078
|
+
const ctx = buildRenderContext(defs, plane, drillableIds, instanceId);
|
|
698
1079
|
for (const edge of plane.edges) {
|
|
699
|
-
const
|
|
700
|
-
let g;
|
|
701
|
-
if (flow) {
|
|
702
|
-
g = renderEdge(edge, flow, markerId, index.defaultFlowIds.has(edge.bpmnElement));
|
|
703
|
-
}
|
|
704
|
-
else if (index.messageFlowIds.has(edge.bpmnElement)) {
|
|
705
|
-
// Message flow — dashed arrow between pools
|
|
706
|
-
g = svgEl("g");
|
|
707
|
-
attr(g, { class: "bpmnkit-edge", "data-bpmnkit-id": edge.bpmnElement });
|
|
708
|
-
if (edge.waypoints.length >= 2) {
|
|
709
|
-
const path = svgEl("path");
|
|
710
|
-
attr(path, {
|
|
711
|
-
d: waypointsToRoundedPath(edge.waypoints),
|
|
712
|
-
class: "bpmnkit-msgflow-path",
|
|
713
|
-
"marker-end": `url(#${markerId})`,
|
|
714
|
-
});
|
|
715
|
-
g.appendChild(path);
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
else {
|
|
719
|
-
// Association or unknown edge type
|
|
720
|
-
g = renderAssociation(edge);
|
|
721
|
-
}
|
|
1080
|
+
const g = renderEdgeGroup(edge, ctx);
|
|
722
1081
|
edgesLayer.appendChild(g);
|
|
723
1082
|
edges.push({ id: edge.bpmnElement, element: g, edge });
|
|
724
1083
|
}
|
|
725
|
-
// ── Shapes ────────────────────────────────────────────────────────
|
|
726
1084
|
for (const shape of plane.shapes) {
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
type === "endEvent" ||
|
|
733
|
-
type === "intermediateCatchEvent" ||
|
|
734
|
-
type === "intermediateThrowEvent" ||
|
|
735
|
-
type === "boundaryEvent") {
|
|
736
|
-
g = renderEvent(shape, el, instanceId);
|
|
737
|
-
}
|
|
738
|
-
else if (type === "exclusiveGateway" ||
|
|
739
|
-
type === "parallelGateway" ||
|
|
740
|
-
type === "inclusiveGateway" ||
|
|
741
|
-
type === "eventBasedGateway" ||
|
|
742
|
-
type === "complexGateway") {
|
|
743
|
-
g = renderGateway(shape, el, instanceId);
|
|
744
|
-
}
|
|
745
|
-
else if (type === "" && !el) {
|
|
746
|
-
// Could be: text annotation, pool (participant), or lane
|
|
747
|
-
const annotation = index.annotations.get(shape.bpmnElement);
|
|
748
|
-
if (annotation !== undefined) {
|
|
749
|
-
g = renderAnnotation(shape, annotation.text, instanceId);
|
|
750
|
-
attr(g, { transform: `translate(${x} ${y})` });
|
|
751
|
-
shapesLayer.appendChild(g);
|
|
752
|
-
shapes.push({ id: shape.bpmnElement, element: g, shape, flowElement: el, annotation });
|
|
753
|
-
continue;
|
|
754
|
-
}
|
|
755
|
-
if (index.participants.has(shape.bpmnElement)) {
|
|
756
|
-
g = renderPool(shape, index.participants.get(shape.bpmnElement), instanceId);
|
|
757
|
-
attr(g, { transform: `translate(${x} ${y})` });
|
|
758
|
-
containersLayer.appendChild(g);
|
|
759
|
-
shapes.push({ id: shape.bpmnElement, element: g, shape, flowElement: el });
|
|
760
|
-
continue;
|
|
761
|
-
}
|
|
762
|
-
if (index.lanes.has(shape.bpmnElement)) {
|
|
763
|
-
g = renderLane(shape, index.lanes.get(shape.bpmnElement), instanceId);
|
|
764
|
-
attr(g, { transform: `translate(${x} ${y})` });
|
|
765
|
-
containersLayer.appendChild(g);
|
|
766
|
-
shapes.push({ id: shape.bpmnElement, element: g, shape, flowElement: el });
|
|
767
|
-
continue;
|
|
768
|
-
}
|
|
769
|
-
// Unknown shape — invisible placeholder
|
|
770
|
-
g = svgEl("g");
|
|
771
|
-
attr(g, { "data-bpmnkit-id": shape.bpmnElement, "data-bpmnkit-instance": instanceId });
|
|
772
|
-
attr(g, { transform: `translate(${x} ${y})` });
|
|
773
|
-
shapesLayer.appendChild(g);
|
|
774
|
-
shapes.push({ id: shape.bpmnElement, element: g, shape, flowElement: el });
|
|
775
|
-
continue;
|
|
776
|
-
}
|
|
777
|
-
else {
|
|
778
|
-
g = renderTask(shape, el, instanceId);
|
|
779
|
-
}
|
|
780
|
-
attr(g, { transform: `translate(${x} ${y})` });
|
|
781
|
-
shapesLayer.appendChild(g);
|
|
782
|
-
shapes.push({ id: shape.bpmnElement, element: g, shape, flowElement: el });
|
|
783
|
-
// External labels for events and gateways — use stored bounds or default to bottom-centred
|
|
784
|
-
const isExternalLabelType = type === "startEvent" ||
|
|
785
|
-
type === "endEvent" ||
|
|
786
|
-
type === "intermediateCatchEvent" ||
|
|
787
|
-
type === "intermediateThrowEvent" ||
|
|
788
|
-
type === "boundaryEvent" ||
|
|
789
|
-
type === "exclusiveGateway" ||
|
|
790
|
-
type === "parallelGateway" ||
|
|
791
|
-
type === "inclusiveGateway" ||
|
|
792
|
-
type === "eventBasedGateway" ||
|
|
793
|
-
type === "complexGateway";
|
|
794
|
-
if (el?.name && isExternalLabelType) {
|
|
795
|
-
const lb = shape.label?.bounds ?? {
|
|
796
|
-
x: shape.bounds.x + shape.bounds.width / 2 - 40,
|
|
797
|
-
y: shape.bounds.y + shape.bounds.height + 6,
|
|
798
|
-
width: 80,
|
|
799
|
-
height: 20,
|
|
800
|
-
};
|
|
801
|
-
// topAlign=true: multi-line text flows downward from the top of the
|
|
802
|
-
// label bounds, so long labels never extend upward into the shape.
|
|
803
|
-
const labelG = renderExternalLabel(lb.x, lb.y, lb.width, lb.height, el.name, true);
|
|
804
|
-
labelsLayer.appendChild(labelG);
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
// Edge labels at their absolute label bounds
|
|
808
|
-
for (const edge of plane.edges) {
|
|
809
|
-
const flow = index.flows.get(edge.bpmnElement);
|
|
810
|
-
if (flow?.name && edge.label?.bounds) {
|
|
811
|
-
// Already rendered inside the edge group — skip duplicate
|
|
812
|
-
// (renderEdge adds the label when label.bounds is present)
|
|
813
|
-
}
|
|
1085
|
+
const { group, layer, label, rendered } = renderShapeGroup(shape, ctx);
|
|
1086
|
+
(layer === "containers" ? containersLayer : shapesLayer).appendChild(group);
|
|
1087
|
+
if (label)
|
|
1088
|
+
labelsLayer.appendChild(label);
|
|
1089
|
+
shapes.push(rendered);
|
|
814
1090
|
}
|
|
815
1091
|
return { shapes, edges };
|
|
816
1092
|
}
|
|
817
1093
|
/**
|
|
818
|
-
* Computes the bounding box of all shapes in the first
|
|
819
|
-
* Returns `null` if the
|
|
1094
|
+
* Computes the bounding box of all shapes in a DI plane (the first diagram's
|
|
1095
|
+
* plane by default). Returns `null` if the plane has no shapes.
|
|
820
1096
|
*/
|
|
821
|
-
export function computeDiagramBounds(defs) {
|
|
822
|
-
const plane = defs.diagrams[0]?.plane;
|
|
1097
|
+
export function computeDiagramBounds(defs, targetPlane) {
|
|
1098
|
+
const plane = targetPlane ?? defs.diagrams[0]?.plane;
|
|
823
1099
|
if (!plane || plane.shapes.length === 0)
|
|
824
1100
|
return null;
|
|
825
1101
|
let minX = Number.POSITIVE_INFINITY;
|