@bpmnkit/canvas 0.0.8

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.
@@ -0,0 +1,835 @@
1
+ import { readDiColor } from "@bpmnkit/core";
2
+ // ── SVG helpers ───────────────────────────────────────────────────────────────
3
+ const NS = "http://www.w3.org/2000/svg";
4
+ function svgEl(tag) {
5
+ return document.createElementNS(NS, tag);
6
+ }
7
+ function attr(el, attrs) {
8
+ for (const [k, v] of Object.entries(attrs))
9
+ el.setAttribute(k, String(v));
10
+ }
11
+ // ── 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
+ /**
43
+ * Creates a `<text>` element (or multi-line group) centred at (`cx`, `cy`).
44
+ * When `topAlign` is true, multi-line text flows downward from `cy` rather
45
+ * than being centred around it — used for external labels below shapes so
46
+ * that long wrapped labels never extend upward into the shape.
47
+ */
48
+ function makeLabel(text, cx, cy, maxWidth, cls = "bpmnkit-label", topAlign = false) {
49
+ const lines = wrapText(text, maxWidth);
50
+ const lineH = 14;
51
+ if (lines.length === 1) {
52
+ const t = svgEl("text");
53
+ attr(t, { class: cls, x: cx, y: cy });
54
+ t.textContent = lines[0] ?? text;
55
+ return t;
56
+ }
57
+ const g = svgEl("g");
58
+ const totalH = lines.length * lineH;
59
+ const startY = topAlign ? lineH / 2 : cy - totalH / 2 + lineH / 2;
60
+ for (let i = 0; i < lines.length; i++) {
61
+ const t = svgEl("text");
62
+ attr(t, { class: cls, x: cx, y: startY + i * lineH });
63
+ t.textContent = lines[i] ?? "";
64
+ g.appendChild(t);
65
+ }
66
+ return g;
67
+ }
68
+ /**
69
+ * Converts a list of waypoints into an SVG path `d` attribute string
70
+ * with rounded corners at intermediate waypoints (quadratic bezier arcs).
71
+ */
72
+ function waypointsToRoundedPath(waypoints) {
73
+ if (waypoints.length < 2)
74
+ return "";
75
+ const r = 4; // corner radius in diagram units
76
+ const parts = [];
77
+ for (let i = 0; i < waypoints.length; i++) {
78
+ const wp = waypoints[i];
79
+ if (!wp)
80
+ continue;
81
+ if (i === 0) {
82
+ parts.push(`M${wp.x},${wp.y}`);
83
+ continue;
84
+ }
85
+ if (i === waypoints.length - 1) {
86
+ parts.push(`L${wp.x},${wp.y}`);
87
+ continue;
88
+ }
89
+ // Intermediate waypoint — round the corner with a quadratic bezier
90
+ const prev = waypoints[i - 1];
91
+ const next = waypoints[i + 1];
92
+ if (!prev || !next)
93
+ continue;
94
+ const dx1 = wp.x - prev.x;
95
+ const dy1 = wp.y - prev.y;
96
+ const d1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);
97
+ const dx2 = next.x - wp.x;
98
+ const dy2 = next.y - wp.y;
99
+ const d2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);
100
+ if (d1 < 0.01 || d2 < 0.01) {
101
+ parts.push(`L${wp.x},${wp.y}`);
102
+ continue;
103
+ }
104
+ const radius = Math.min(r, d1 / 2, d2 / 2);
105
+ // Approach point: step back from the corner along the incoming segment
106
+ const ax = wp.x - (dx1 / d1) * radius;
107
+ const ay = wp.y - (dy1 / d1) * radius;
108
+ // Departure point: step forward from the corner along the outgoing segment
109
+ const bx = wp.x + (dx2 / d2) * radius;
110
+ const by = wp.y + (dy2 / d2) * radius;
111
+ parts.push(`L${ax},${ay}`);
112
+ parts.push(`Q${wp.x},${wp.y} ${bx},${by}`);
113
+ }
114
+ return parts.join(" ");
115
+ }
116
+ // ── Task type icons (14×14, origin at 0,0) ───────────────────────────────────
117
+ /**
118
+ * Returns inner SVG markup for task-type icons positioned at the top-left
119
+ * corner of a task rectangle. Each icon fits in a 14×14 bounding box.
120
+ */
121
+ function taskIcon(type) {
122
+ switch (type) {
123
+ case "manualTask":
124
+ // Hand icon
125
+ return `<path d="M3 8V4.5a1 1 0 012 0V8M5 7V3a1 1 0 012 0v4M7 6a1 1 0 012 0v1.5M9 7.5a1 1 0 012 0V9c0 2.5-1.5 4-4.5 4H5c-2.5 0-4-1.5-4-4V8" class="bpmnkit-icon"/>`;
126
+ case "serviceTask":
127
+ // Gear: two concentric circles + 8 spokes
128
+ return `<circle cx="7" cy="7" r="5.5" class="bpmnkit-icon"/>
129
+ <circle cx="7" cy="7" r="2" class="bpmnkit-icon"/>
130
+ <path d="M7 1v2.5M7 10.5v2.5M1 7h2.5M10.5 7h2.5M2.8 2.8l1.7 1.7M9.5 9.5l1.7 1.7M11.2 2.8l-1.7 1.7M4.5 9.5l-1.7 1.7" class="bpmnkit-icon"/>`;
131
+ case "userTask":
132
+ // Person: head circle + shoulders arc
133
+ return `<circle cx="7" cy="4.5" r="2.5" class="bpmnkit-icon"/>
134
+ <path d="M1.5 14Q1.5 9 7 9Q12.5 9 12.5 14" class="bpmnkit-icon"/>`;
135
+ case "scriptTask":
136
+ // Document with three text lines
137
+ return `<rect x="2" y="0.5" width="10" height="13" rx="1" class="bpmnkit-icon"/>
138
+ <path d="M4 4h6M4 7h6M4 10h4" class="bpmnkit-icon"/>`;
139
+ case "sendTask":
140
+ // Filled envelope
141
+ return `<path d="M1.5 3.5h11v8h-11z" class="bpmnkit-icon-solid"/>
142
+ <path d="M1.5 3.5l5.5 4.5 5.5-4.5M1.5 11.5l4-3.5M12.5 11.5l-4-3.5" style="fill:none;stroke:var(--bpmnkit-shape-fill,#fff);stroke-width:1.5"/>`;
143
+ case "receiveTask":
144
+ // Outlined envelope
145
+ return `<rect x="1.5" y="3.5" width="11" height="8" class="bpmnkit-icon"/>
146
+ <path d="M1.5 3.5l5.5 4.5 5.5-4.5" class="bpmnkit-icon"/>`;
147
+ case "businessRuleTask":
148
+ // Table grid
149
+ return `<rect x="1" y="1" width="12" height="12" class="bpmnkit-icon"/>
150
+ <path d="M1 4.5h12M4 1v12" class="bpmnkit-icon"/>`;
151
+ default:
152
+ return "";
153
+ }
154
+ }
155
+ // ── Event definition markers (centred at 0,0, ~10×10) ────────────────────────
156
+ function eventMarker(defType, filled) {
157
+ const cls = filled ? "bpmnkit-icon-solid" : "bpmnkit-icon";
158
+ switch (defType) {
159
+ case "timer":
160
+ return `<circle cx="0" cy="0" r="5" class="${cls}"/>
161
+ <path d="M0 -3.5v3.5l2 2" class="bpmnkit-icon"/>`;
162
+ case "message":
163
+ return `<rect x="-5" y="-3.5" width="10" height="7" class="${cls}"/>
164
+ <path d="M-5 -3.5l5 4 5-4" class="${filled ? "bpmnkit-icon" : "bpmnkit-icon"}" style="${filled ? "stroke:var(--bpmnkit-shape-fill,#fff)" : ""}"/>`;
165
+ case "signal":
166
+ return `<path d="M0 -5.5l5.5 10h-11z" class="${cls}"/>`;
167
+ case "error":
168
+ return `<path d="M-2 -5l2.5 4.5-3.5 0.5L2 5l-2.5-4.5 3.5-0.5z" class="${cls}"/>`;
169
+ case "escalation":
170
+ return `<path d="M0 -5.5l3.5 9.5-3.5-3.5-3.5 3.5z" class="${cls}"/>`;
171
+ case "compensate":
172
+ return `<path d="M1 -3.5l-5 3.5 5 3.5zM6 -3.5l-5 3.5 5 3.5z" class="${cls}"/>`;
173
+ case "conditional":
174
+ return `<rect x="-4.5" y="-5.5" width="9" height="11" rx="1" class="${cls}"/>
175
+ <path d="M-2.5 -2.5h5M-2.5 0h5M-2.5 2.5h3" class="bpmnkit-icon"/>`;
176
+ case "link":
177
+ return `<path d="M-2 -3.5v7l5.5-3.5z" class="${cls}"/><path d="M-6 0h4" class="bpmnkit-icon"/>`;
178
+ case "cancel":
179
+ return `<path d="M-4 -4l8 8M4 -4l-8 8" class="${cls}"/>`;
180
+ case "terminate":
181
+ return `<circle cx="0" cy="0" r="5" class="bpmnkit-icon-solid"/>`;
182
+ default:
183
+ return "";
184
+ }
185
+ }
186
+ // ── Gateway markers (centred at 0,0, ~16×16) ─────────────────────────────────
187
+ function gatewayMarker(type) {
188
+ switch (type) {
189
+ case "exclusiveGateway":
190
+ return `<path d="M-6 -6l12 12M6 -6l-12 12" class="bpmnkit-gw-marker-stroke"/>`;
191
+ case "parallelGateway":
192
+ return `<path d="M0 -8v16M-8 0h16" class="bpmnkit-gw-marker-stroke"/>`;
193
+ case "inclusiveGateway":
194
+ return `<circle cx="0" cy="0" r="6" class="bpmnkit-gw-marker-stroke"/>`;
195
+ case "eventBasedGateway":
196
+ return `<circle cx="0" cy="0" r="7" class="bpmnkit-gw-marker-stroke"/>
197
+ <circle cx="0" cy="0" r="5" class="bpmnkit-gw-marker-stroke"/>
198
+ <path d="M0 -4L3.8 1.8H-3.8Z" class="bpmnkit-gw-marker"/>`;
199
+ case "complexGateway":
200
+ return `<path d="M0 -8v16M-8 0h16M-5.7 -5.7l11.4 11.4M5.7 -5.7l-11.4 11.4" class="bpmnkit-gw-marker-stroke"/>`;
201
+ default:
202
+ return "";
203
+ }
204
+ }
205
+ // ── Sub-process bottom markers ────────────────────────────────────────────────
206
+ function subProcessMarker(type) {
207
+ if (type === "adHocSubProcess") {
208
+ // Tilde (~) for ad-hoc
209
+ return `<path d="M-7 0Q-4 -4 0 0Q4 4 7 0" class="bpmnkit-icon"/>`;
210
+ }
211
+ // Standard expand marker: + in a small box
212
+ return `<rect x="-7" y="-7" width="14" height="14" rx="1" class="bpmnkit-icon"/>
213
+ <path d="M0 -4v8M-4 0h8" class="bpmnkit-icon"/>`;
214
+ }
215
+ function buildIndex(defs) {
216
+ const elements = new Map();
217
+ const flows = new Map();
218
+ const annotations = new Map();
219
+ const participants = new Map();
220
+ const lanes = new Map();
221
+ const messageFlowIds = new Set();
222
+ const defaultFlowIds = new Set();
223
+ function indexProcess(flowElements, sequenceFlows) {
224
+ for (const el of flowElements) {
225
+ elements.set(el.id, el);
226
+ if (el.type === "subProcess" ||
227
+ el.type === "adHocSubProcess" ||
228
+ el.type === "eventSubProcess" ||
229
+ el.type === "transaction") {
230
+ indexProcess(el.flowElements, el.sequenceFlows);
231
+ }
232
+ if ((el.type === "exclusiveGateway" ||
233
+ el.type === "inclusiveGateway" ||
234
+ el.type === "complexGateway") &&
235
+ el.default) {
236
+ defaultFlowIds.add(el.default);
237
+ }
238
+ }
239
+ for (const sf of sequenceFlows) {
240
+ flows.set(sf.id, sf);
241
+ }
242
+ }
243
+ function indexLaneSet(laneSet) {
244
+ for (const lane of laneSet.lanes) {
245
+ lanes.set(lane.id, lane);
246
+ if (lane.childLaneSet)
247
+ indexLaneSet(lane.childLaneSet);
248
+ }
249
+ }
250
+ for (const proc of defs.processes) {
251
+ indexProcess(proc.flowElements, proc.sequenceFlows);
252
+ for (const ta of proc.textAnnotations) {
253
+ annotations.set(ta.id, ta);
254
+ }
255
+ if (proc.laneSet)
256
+ indexLaneSet(proc.laneSet);
257
+ }
258
+ for (const collab of defs.collaborations) {
259
+ for (const p of collab.participants) {
260
+ participants.set(p.id, p);
261
+ }
262
+ for (const ta of collab.textAnnotations) {
263
+ annotations.set(ta.id, ta);
264
+ }
265
+ for (const mf of collab.messageFlows) {
266
+ messageFlowIds.add(mf.id);
267
+ }
268
+ }
269
+ return { elements, flows, annotations, participants, lanes, messageFlowIds, defaultFlowIds };
270
+ }
271
+ // ── Color helper ─────────────────────────────────────────────────────────────
272
+ /** Applies bioc/color namespace attributes as inline style on a shape body element. */
273
+ function applyColor(el, shape) {
274
+ const { fill, stroke } = readDiColor(shape.unknownAttributes);
275
+ if (!fill && !stroke)
276
+ return;
277
+ const parts = [];
278
+ if (fill)
279
+ parts.push(`fill: ${fill}`);
280
+ if (stroke)
281
+ parts.push(`stroke: ${stroke}`);
282
+ el.setAttribute("style", parts.join("; "));
283
+ }
284
+ // ── Shape renderers ───────────────────────────────────────────────────────────
285
+ function renderEvent(shape, el, instanceId) {
286
+ const { width, height } = shape.bounds;
287
+ const cx = width / 2;
288
+ const cy = height / 2;
289
+ const r = Math.min(cx, cy) - 1;
290
+ const g = svgEl("g");
291
+ const isEnd = el?.type === "endEvent";
292
+ const isIntermediate = el?.type === "intermediateCatchEvent" ||
293
+ el?.type === "intermediateThrowEvent" ||
294
+ el?.type === "boundaryEvent";
295
+ const isThrow = el?.type === "intermediateThrowEvent" ||
296
+ (el?.type === "boundaryEvent" && el.cancelActivity === false);
297
+ // Outer circle
298
+ const outer = svgEl("circle");
299
+ attr(outer, {
300
+ cx,
301
+ cy,
302
+ r,
303
+ class: isEnd ? "bpmnkit-end-body" : "bpmnkit-event-body",
304
+ });
305
+ applyColor(outer, shape);
306
+ g.appendChild(outer);
307
+ // Intermediate: inner circle (dashed for non-interrupting boundary events)
308
+ if (isIntermediate) {
309
+ const inner = svgEl("circle");
310
+ const isNonInterrupting = el?.type === "boundaryEvent" && el.cancelActivity === false;
311
+ attr(inner, {
312
+ cx,
313
+ cy,
314
+ r: r - 3,
315
+ class: isNonInterrupting ? "bpmnkit-event-inner-dashed" : "bpmnkit-event-inner",
316
+ });
317
+ g.appendChild(inner);
318
+ }
319
+ // Event definition marker
320
+ const eventDef = el && "eventDefinitions" in el && el.eventDefinitions.length > 0
321
+ ? el.eventDefinitions[0]
322
+ : undefined;
323
+ if (eventDef) {
324
+ const markerG = svgEl("g");
325
+ attr(markerG, { transform: `translate(${cx} ${cy})` });
326
+ markerG.innerHTML = eventMarker(eventDef.type, isThrow && !isEnd);
327
+ g.appendChild(markerG);
328
+ }
329
+ // Accessibility
330
+ const label = el?.name ?? el?.type ?? "event";
331
+ attr(g, {
332
+ class: "bpmnkit-shape",
333
+ tabindex: "-1",
334
+ role: "button",
335
+ "aria-label": label,
336
+ "data-bpmnkit-id": shape.bpmnElement,
337
+ "data-bpmnkit-instance": instanceId,
338
+ });
339
+ return g;
340
+ }
341
+ function renderTask(shape, el, instanceId) {
342
+ const { width, height } = shape.bounds;
343
+ const g = svgEl("g");
344
+ // Body
345
+ const body = svgEl("rect");
346
+ const bodyClass = el?.type === "callActivity"
347
+ ? "bpmnkit-callactivity-body"
348
+ : el?.type === "eventSubProcess"
349
+ ? "bpmnkit-eventsubprocess-body"
350
+ : el?.type === "subProcess" || el?.type === "adHocSubProcess"
351
+ ? "bpmnkit-shape-body"
352
+ : "bpmnkit-shape-body";
353
+ attr(body, { x: 0, y: 0, width, height, rx: 10, class: bodyClass });
354
+ applyColor(body, shape);
355
+ g.appendChild(body);
356
+ // Transaction: double inner border
357
+ if (el?.type === "transaction") {
358
+ const inner = svgEl("rect");
359
+ attr(inner, { x: 3, y: 3, width: width - 6, height: height - 6, rx: 8, class: "bpmnkit-icon" });
360
+ g.appendChild(inner);
361
+ }
362
+ // Task type icon (14×14 at position 4,4)
363
+ const templateIconUri = el?.unknownAttributes?.["zeebe:modelerTemplateIcon"];
364
+ if (templateIconUri) {
365
+ const img = svgEl("image");
366
+ attr(img, {
367
+ x: 4,
368
+ y: 4,
369
+ width: 14,
370
+ height: 14,
371
+ href: templateIconUri,
372
+ preserveAspectRatio: "xMidYMid meet",
373
+ });
374
+ g.appendChild(img);
375
+ }
376
+ else {
377
+ const iconSvg = taskIcon(el?.type ?? "");
378
+ if (iconSvg) {
379
+ const iconG = svgEl("g");
380
+ attr(iconG, { transform: "translate(4 4)" });
381
+ iconG.innerHTML = iconSvg;
382
+ g.appendChild(iconG);
383
+ }
384
+ }
385
+ // Label — centred in shape
386
+ if (el?.name) {
387
+ const labelMaxW = width - 16;
388
+ const labelEl = makeLabel(el.name, width / 2, height / 2, labelMaxW);
389
+ g.appendChild(labelEl);
390
+ }
391
+ // Sub-process expand/adHoc marker at bottom centre
392
+ if (el?.type === "subProcess" ||
393
+ el?.type === "adHocSubProcess" ||
394
+ el?.type === "eventSubProcess" ||
395
+ el?.type === "transaction") {
396
+ const markerG = svgEl("g");
397
+ attr(markerG, { transform: `translate(${width / 2} ${height - 10})` });
398
+ markerG.innerHTML = subProcessMarker(el.type);
399
+ g.appendChild(markerG);
400
+ }
401
+ const label = el?.name ?? el?.type ?? "task";
402
+ attr(g, {
403
+ class: "bpmnkit-shape",
404
+ tabindex: "-1",
405
+ role: "button",
406
+ "aria-label": label,
407
+ "data-bpmnkit-id": shape.bpmnElement,
408
+ "data-bpmnkit-instance": instanceId,
409
+ });
410
+ return g;
411
+ }
412
+ function renderGateway(shape, el, instanceId) {
413
+ const { width, height } = shape.bounds;
414
+ const cx = width / 2;
415
+ const cy = height / 2;
416
+ const g = svgEl("g");
417
+ // Diamond
418
+ const diamond = svgEl("polygon");
419
+ attr(diamond, {
420
+ points: `${cx},0 ${width},${cy} ${cx},${height} 0,${cy}`,
421
+ class: "bpmnkit-gw-body",
422
+ });
423
+ applyColor(diamond, shape);
424
+ g.appendChild(diamond);
425
+ // Gateway marker centred in diamond
426
+ const markerSvg = el ? gatewayMarker(el.type) : "";
427
+ if (markerSvg) {
428
+ const markerG = svgEl("g");
429
+ attr(markerG, { transform: `translate(${cx} ${cy})` });
430
+ markerG.innerHTML = markerSvg;
431
+ g.appendChild(markerG);
432
+ }
433
+ const label = el?.name ?? el?.type ?? "gateway";
434
+ attr(g, {
435
+ class: "bpmnkit-shape",
436
+ tabindex: "-1",
437
+ role: "button",
438
+ "aria-label": label,
439
+ "data-bpmnkit-id": shape.bpmnElement,
440
+ "data-bpmnkit-instance": instanceId,
441
+ });
442
+ return g;
443
+ }
444
+ function renderPool(shape, participant, instanceId) {
445
+ const { width, height } = shape.bounds;
446
+ const g = svgEl("g");
447
+ // Pool body
448
+ const bg = svgEl("rect");
449
+ attr(bg, { x: 0, y: 0, width, height, class: "bpmnkit-pool-body" });
450
+ g.appendChild(bg);
451
+ // Title bar (left column, 30px wide)
452
+ const titleBar = svgEl("rect");
453
+ attr(titleBar, { x: 0, y: 0, width: 30, height, class: "bpmnkit-pool-header" });
454
+ g.appendChild(titleBar);
455
+ // Pool name (rotated in title bar)
456
+ if (participant?.name) {
457
+ const text = svgEl("text");
458
+ attr(text, {
459
+ class: "bpmnkit-label",
460
+ transform: `translate(15 ${height / 2}) rotate(-90)`,
461
+ });
462
+ text.textContent = participant.name;
463
+ g.appendChild(text);
464
+ }
465
+ attr(g, {
466
+ class: "bpmnkit-shape bpmnkit-pool",
467
+ tabindex: "-1",
468
+ role: "region",
469
+ "aria-label": participant?.name ?? "Pool",
470
+ "data-bpmnkit-id": shape.bpmnElement,
471
+ "data-bpmnkit-instance": instanceId,
472
+ });
473
+ return g;
474
+ }
475
+ function renderLane(shape, lane, instanceId) {
476
+ const { width, height } = shape.bounds;
477
+ const g = svgEl("g");
478
+ // Lane body
479
+ const bg = svgEl("rect");
480
+ attr(bg, { x: 0, y: 0, width, height, class: "bpmnkit-lane-body" });
481
+ g.appendChild(bg);
482
+ // Title bar (left column, 30px wide)
483
+ const titleBar = svgEl("rect");
484
+ attr(titleBar, { x: 0, y: 0, width: 30, height, class: "bpmnkit-lane-header" });
485
+ g.appendChild(titleBar);
486
+ // Lane name (rotated in title bar)
487
+ if (lane?.name) {
488
+ const text = svgEl("text");
489
+ attr(text, {
490
+ class: "bpmnkit-label",
491
+ transform: `translate(15 ${height / 2}) rotate(-90)`,
492
+ });
493
+ text.textContent = lane.name;
494
+ g.appendChild(text);
495
+ }
496
+ attr(g, {
497
+ class: "bpmnkit-shape bpmnkit-lane",
498
+ tabindex: "-1",
499
+ role: "region",
500
+ "aria-label": lane?.name ?? "Lane",
501
+ "data-bpmnkit-id": shape.bpmnElement,
502
+ "data-bpmnkit-instance": instanceId,
503
+ });
504
+ return g;
505
+ }
506
+ function renderAnnotation(shape, text, instanceId) {
507
+ const { width, height } = shape.bounds;
508
+ const g = svgEl("g");
509
+ // Transparent hit rect so the full bounding area is clickable/draggable
510
+ const hit = svgEl("rect");
511
+ attr(hit, { x: "0", y: "0", width: String(width), height: String(height), fill: "transparent" });
512
+ g.appendChild(hit);
513
+ // Bracket (open on the right — left + top + bottom strokes only)
514
+ const path = svgEl("path");
515
+ attr(path, {
516
+ d: `M${width} 0 L0 0 L0 ${height} L${width} ${height}`,
517
+ class: "bpmnkit-icon",
518
+ });
519
+ g.appendChild(path);
520
+ // Annotation text centred in the full shape area
521
+ if (text) {
522
+ const labelEl = makeLabel(text, width / 2, height / 2, width - 8);
523
+ g.appendChild(labelEl);
524
+ }
525
+ attr(g, {
526
+ class: "bpmnkit-shape",
527
+ tabindex: "-1",
528
+ role: "note",
529
+ "data-bpmnkit-id": shape.bpmnElement,
530
+ "data-bpmnkit-instance": instanceId,
531
+ });
532
+ return g;
533
+ }
534
+ // ── External label ────────────────────────────────────────────────────────────
535
+ /**
536
+ * Renders an external label at absolute diagram coordinates.
537
+ * Used for events, gateways, and edge midpoints.
538
+ * Pass `topAlign = true` for labels below shapes — multi-line text then flows
539
+ * downward from the top of the bounds, preventing upward overlap with the shape.
540
+ */
541
+ function renderExternalLabel(absX, absY, labelW, labelH, text, topAlign = false) {
542
+ const g = svgEl("g");
543
+ attr(g, { transform: `translate(${absX} ${absY})` });
544
+ const labelEl = makeLabel(text, labelW / 2, labelH / 2, labelW - 4, "bpmnkit-label", topAlign);
545
+ g.appendChild(labelEl);
546
+ return g;
547
+ }
548
+ // ── Edge renderer ─────────────────────────────────────────────────────────────
549
+ function renderEdge(edge, flow, markerId, isDefault) {
550
+ const g = svgEl("g");
551
+ attr(g, {
552
+ class: "bpmnkit-edge",
553
+ "data-bpmnkit-id": edge.bpmnElement,
554
+ });
555
+ if (edge.waypoints.length < 2)
556
+ return g;
557
+ const path = svgEl("path");
558
+ attr(path, {
559
+ d: waypointsToRoundedPath(edge.waypoints),
560
+ class: "bpmnkit-edge-path",
561
+ "marker-end": `url(#${markerId})`,
562
+ });
563
+ g.appendChild(path);
564
+ // Wide transparent stroke so the edge is easy to click
565
+ const hitPath = svgEl("path");
566
+ attr(hitPath, {
567
+ d: waypointsToRoundedPath(edge.waypoints),
568
+ fill: "none",
569
+ stroke: "transparent",
570
+ "stroke-width": "12",
571
+ "pointer-events": "stroke",
572
+ });
573
+ g.appendChild(hitPath);
574
+ // Default-flow slash mark near the source end
575
+ if (isDefault) {
576
+ const wp0 = edge.waypoints[0];
577
+ const wp1 = edge.waypoints[1];
578
+ if (wp0 && wp1) {
579
+ const dx = wp1.x - wp0.x;
580
+ const dy = wp1.y - wp0.y;
581
+ const len = Math.sqrt(dx * dx + dy * dy);
582
+ if (len > 0) {
583
+ const nx = dx / len;
584
+ const ny = dy / len;
585
+ const t = Math.min(10, len * 0.25);
586
+ const cx = wp0.x + nx * t;
587
+ const cy = wp0.y + ny * t;
588
+ const s = 5;
589
+ const slash = svgEl("line");
590
+ attr(slash, {
591
+ x1: cx - ny * s,
592
+ y1: cy + nx * s,
593
+ x2: cx + ny * s,
594
+ y2: cy - nx * s,
595
+ class: "bpmnkit-edge-default-slash",
596
+ });
597
+ g.appendChild(slash);
598
+ }
599
+ }
600
+ }
601
+ // Edge label
602
+ if (flow?.name && edge.label?.bounds) {
603
+ const { x, y, width, height } = edge.label.bounds;
604
+ const labelEl = renderExternalLabel(x, y, width, height, flow.name);
605
+ g.appendChild(labelEl);
606
+ }
607
+ return g;
608
+ }
609
+ function renderAssociation(edge) {
610
+ const g = svgEl("g");
611
+ attr(g, { class: "bpmnkit-edge", "data-bpmnkit-id": edge.bpmnElement });
612
+ const path = svgEl("path");
613
+ attr(path, { d: waypointsToRoundedPath(edge.waypoints), class: "bpmnkit-edge-assoc" });
614
+ g.appendChild(path);
615
+ return g;
616
+ }
617
+ // ── SVG defs (arrow markers) ──────────────────────────────────────────────────
618
+ /**
619
+ * Creates the SVG `<defs>` section for this canvas instance.
620
+ * Uses `instanceId` to make marker IDs unique per canvas, avoiding
621
+ * conflicts when multiple canvases are mounted on the same page.
622
+ */
623
+ export function createDefs(svg, instanceId) {
624
+ const markerId = `bpmnkit-arrow-${instanceId}`;
625
+ const defs = svgEl("defs");
626
+ defs.innerHTML = `
627
+ <marker id="${markerId}" markerWidth="8" markerHeight="6"
628
+ refX="7" refY="3" orient="auto" markerUnits="strokeWidth">
629
+ <path d="M0,0 L8,3 L0,6 Z" class="bpmnkit-arrow-fill"/>
630
+ </marker>
631
+ `;
632
+ svg.appendChild(defs);
633
+ return markerId;
634
+ }
635
+ // ── Dot-grid background ───────────────────────────────────────────────────────
636
+ /**
637
+ * Creates and inserts an SVG dot-grid background pattern.
638
+ * The `<rect>` filling the entire viewport and the `<pattern>` definition
639
+ * are both inserted into the SVG. Returns the `<pattern>` element so the
640
+ * viewport controller can keep `patternTransform` in sync.
641
+ */
642
+ export function createGrid(svg, instanceId) {
643
+ const patternId = `bpmnkit-grid-${instanceId}`;
644
+ const defs = svg.querySelector("defs") ?? svgEl("defs");
645
+ const pattern = svgEl("pattern");
646
+ attr(pattern, {
647
+ id: patternId,
648
+ width: 20,
649
+ height: 20,
650
+ patternUnits: "userSpaceOnUse",
651
+ });
652
+ const dot = svgEl("circle");
653
+ attr(dot, { cx: 1, cy: 1, r: 1, fill: "var(--bpmnkit-grid, rgba(0,0,0,0.14))" });
654
+ pattern.appendChild(dot);
655
+ defs.appendChild(pattern);
656
+ if (!svg.contains(defs))
657
+ svg.insertBefore(defs, svg.firstChild);
658
+ const bg = svgEl("rect");
659
+ attr(bg, {
660
+ x: "-5000%",
661
+ y: "-5000%",
662
+ width: "10100%",
663
+ height: "10100%",
664
+ fill: `url(#${patternId})`,
665
+ "pointer-events": "none",
666
+ });
667
+ svg.appendChild(bg);
668
+ return pattern;
669
+ }
670
+ /**
671
+ * Renders a `BpmnDefinitions` model into SVG element groups, appending them
672
+ * to `edgesLayer` and `shapesLayer` respectively.
673
+ *
674
+ * Edges are placed below shapes (rendered first) so connection lines don't
675
+ * cover shape bodies. Shapes are rendered in DI order so container shapes
676
+ * (sub-processes) appear before their children.
677
+ */
678
+ export function render(defs, containersLayer, edgesLayer, shapesLayer, labelsLayer, markerId, instanceId) {
679
+ const index = buildIndex(defs);
680
+ const shapes = [];
681
+ const edges = [];
682
+ const plane = defs.diagrams[0]?.plane;
683
+ if (!plane)
684
+ return { shapes, edges };
685
+ // ── Edges ─────────────────────────────────────────────────────────
686
+ for (const edge of plane.edges) {
687
+ const flow = index.flows.get(edge.bpmnElement);
688
+ let g;
689
+ if (flow) {
690
+ g = renderEdge(edge, flow, markerId, index.defaultFlowIds.has(edge.bpmnElement));
691
+ }
692
+ else if (index.messageFlowIds.has(edge.bpmnElement)) {
693
+ // Message flow — dashed arrow between pools
694
+ g = svgEl("g");
695
+ attr(g, { class: "bpmnkit-edge", "data-bpmnkit-id": edge.bpmnElement });
696
+ if (edge.waypoints.length >= 2) {
697
+ const path = svgEl("path");
698
+ attr(path, {
699
+ d: waypointsToRoundedPath(edge.waypoints),
700
+ class: "bpmnkit-msgflow-path",
701
+ "marker-end": `url(#${markerId})`,
702
+ });
703
+ g.appendChild(path);
704
+ }
705
+ }
706
+ else {
707
+ // Association or unknown edge type
708
+ g = renderAssociation(edge);
709
+ }
710
+ edgesLayer.appendChild(g);
711
+ edges.push({ id: edge.bpmnElement, element: g, edge });
712
+ }
713
+ // ── Shapes ────────────────────────────────────────────────────────
714
+ for (const shape of plane.shapes) {
715
+ const el = index.elements.get(shape.bpmnElement);
716
+ const { x, y } = shape.bounds;
717
+ let g;
718
+ const type = el?.type ?? "";
719
+ if (type === "startEvent" ||
720
+ type === "endEvent" ||
721
+ type === "intermediateCatchEvent" ||
722
+ type === "intermediateThrowEvent" ||
723
+ type === "boundaryEvent") {
724
+ g = renderEvent(shape, el, instanceId);
725
+ }
726
+ else if (type === "exclusiveGateway" ||
727
+ type === "parallelGateway" ||
728
+ type === "inclusiveGateway" ||
729
+ type === "eventBasedGateway" ||
730
+ type === "complexGateway") {
731
+ g = renderGateway(shape, el, instanceId);
732
+ }
733
+ else if (type === "" && !el) {
734
+ // Could be: text annotation, pool (participant), or lane
735
+ const annotation = index.annotations.get(shape.bpmnElement);
736
+ if (annotation !== undefined) {
737
+ g = renderAnnotation(shape, annotation.text, instanceId);
738
+ attr(g, { transform: `translate(${x} ${y})` });
739
+ shapesLayer.appendChild(g);
740
+ shapes.push({ id: shape.bpmnElement, element: g, shape, flowElement: el, annotation });
741
+ continue;
742
+ }
743
+ if (index.participants.has(shape.bpmnElement)) {
744
+ g = renderPool(shape, index.participants.get(shape.bpmnElement), instanceId);
745
+ attr(g, { transform: `translate(${x} ${y})` });
746
+ containersLayer.appendChild(g);
747
+ shapes.push({ id: shape.bpmnElement, element: g, shape, flowElement: el });
748
+ continue;
749
+ }
750
+ if (index.lanes.has(shape.bpmnElement)) {
751
+ g = renderLane(shape, index.lanes.get(shape.bpmnElement), instanceId);
752
+ attr(g, { transform: `translate(${x} ${y})` });
753
+ containersLayer.appendChild(g);
754
+ shapes.push({ id: shape.bpmnElement, element: g, shape, flowElement: el });
755
+ continue;
756
+ }
757
+ // Unknown shape — invisible placeholder
758
+ g = svgEl("g");
759
+ attr(g, { "data-bpmnkit-id": shape.bpmnElement, "data-bpmnkit-instance": instanceId });
760
+ attr(g, { transform: `translate(${x} ${y})` });
761
+ shapesLayer.appendChild(g);
762
+ shapes.push({ id: shape.bpmnElement, element: g, shape, flowElement: el });
763
+ continue;
764
+ }
765
+ else {
766
+ g = renderTask(shape, el, instanceId);
767
+ }
768
+ attr(g, { transform: `translate(${x} ${y})` });
769
+ shapesLayer.appendChild(g);
770
+ shapes.push({ id: shape.bpmnElement, element: g, shape, flowElement: el });
771
+ // External labels for events and gateways — use stored bounds or default to bottom-centred
772
+ const isExternalLabelType = type === "startEvent" ||
773
+ type === "endEvent" ||
774
+ type === "intermediateCatchEvent" ||
775
+ type === "intermediateThrowEvent" ||
776
+ type === "boundaryEvent" ||
777
+ type === "exclusiveGateway" ||
778
+ type === "parallelGateway" ||
779
+ type === "inclusiveGateway" ||
780
+ type === "eventBasedGateway" ||
781
+ type === "complexGateway";
782
+ if (el?.name && isExternalLabelType) {
783
+ const lb = shape.label?.bounds ?? {
784
+ x: shape.bounds.x + shape.bounds.width / 2 - 40,
785
+ y: shape.bounds.y + shape.bounds.height + 6,
786
+ width: 80,
787
+ height: 20,
788
+ };
789
+ // topAlign=true: multi-line text flows downward from the top of the
790
+ // label bounds, so long labels never extend upward into the shape.
791
+ const labelG = renderExternalLabel(lb.x, lb.y, lb.width, lb.height, el.name, true);
792
+ labelsLayer.appendChild(labelG);
793
+ }
794
+ }
795
+ // Edge labels at their absolute label bounds
796
+ for (const edge of plane.edges) {
797
+ const flow = index.flows.get(edge.bpmnElement);
798
+ if (flow?.name && edge.label?.bounds) {
799
+ // Already rendered inside the edge group — skip duplicate
800
+ // (renderEdge adds the label when label.bounds is present)
801
+ }
802
+ }
803
+ return { shapes, edges };
804
+ }
805
+ /**
806
+ * Computes the bounding box of all shapes in the first DI diagram plane.
807
+ * Returns `null` if the diagram has no shapes.
808
+ */
809
+ export function computeDiagramBounds(defs) {
810
+ const plane = defs.diagrams[0]?.plane;
811
+ if (!plane || plane.shapes.length === 0)
812
+ return null;
813
+ let minX = Number.POSITIVE_INFINITY;
814
+ let minY = Number.POSITIVE_INFINITY;
815
+ let maxX = Number.NEGATIVE_INFINITY;
816
+ let maxY = Number.NEGATIVE_INFINITY;
817
+ for (const shape of plane.shapes) {
818
+ const { x, y, width, height } = shape.bounds;
819
+ minX = Math.min(minX, x);
820
+ minY = Math.min(minY, y);
821
+ maxX = Math.max(maxX, x + width);
822
+ maxY = Math.max(maxY, y + height);
823
+ }
824
+ // Also include waypoints from edges
825
+ for (const edge of plane.edges) {
826
+ for (const wp of edge.waypoints) {
827
+ minX = Math.min(minX, wp.x);
828
+ minY = Math.min(minY, wp.y);
829
+ maxX = Math.max(maxX, wp.x);
830
+ maxY = Math.max(maxY, wp.y);
831
+ }
832
+ }
833
+ return { minX, minY, maxX, maxY };
834
+ }
835
+ //# sourceMappingURL=renderer.js.map