@bpmnkit/editor 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,1473 @@
1
+ import { BIOC_NS, COLOR_NS, writeDiColor } from "@bpmnkit/core";
2
+ import { computeWaypoints, computeWaypointsAvoiding, computeWaypointsWithPorts, portFromWaypoint, routeEntersShape, routeOrthogonal, waypointsIntersectObstacles, } from "./geometry.js";
3
+ import { genId } from "./id.js";
4
+ // ── Empty definitions ─────────────────────────────────────────────────────────
5
+ /** Creates a minimal valid BpmnDefinitions with one process and one diagram. */
6
+ export function createEmptyDefinitions() {
7
+ const processId = genId("Process");
8
+ const planeId = genId("BPMNPlane");
9
+ return {
10
+ id: genId("Definitions"),
11
+ targetNamespace: "http://bpmn.io/schema/bpmn",
12
+ namespaces: {
13
+ bpmn: "http://www.omg.org/spec/BPMN/20100524/MODEL",
14
+ bpmndi: "http://www.omg.org/spec/BPMN/20100524/DI",
15
+ dc: "http://www.omg.org/spec/DD/20100524/DC",
16
+ di: "http://www.omg.org/spec/DD/20100524/DI",
17
+ },
18
+ unknownAttributes: {},
19
+ errors: [],
20
+ escalations: [],
21
+ messages: [],
22
+ collaborations: [],
23
+ processes: [
24
+ {
25
+ id: processId,
26
+ extensionElements: [],
27
+ flowElements: [],
28
+ sequenceFlows: [],
29
+ textAnnotations: [],
30
+ associations: [],
31
+ unknownAttributes: {},
32
+ },
33
+ ],
34
+ diagrams: [
35
+ {
36
+ id: genId("BPMNDiagram"),
37
+ plane: {
38
+ id: planeId,
39
+ bpmnElement: processId,
40
+ shapes: [],
41
+ edges: [],
42
+ },
43
+ },
44
+ ],
45
+ };
46
+ }
47
+ // ── Helper: build a new flow element ─────────────────────────────────────────
48
+ function makeFlowElement(type, id, name) {
49
+ const base = {
50
+ id,
51
+ name,
52
+ incoming: [],
53
+ outgoing: [],
54
+ extensionElements: [],
55
+ unknownAttributes: {},
56
+ };
57
+ switch (type) {
58
+ case "startEvent":
59
+ return { ...base, type: "startEvent", eventDefinitions: [] };
60
+ case "messageStartEvent":
61
+ return { ...base, type: "startEvent", eventDefinitions: [{ type: "message" }] };
62
+ case "timerStartEvent":
63
+ return { ...base, type: "startEvent", eventDefinitions: [{ type: "timer" }] };
64
+ case "conditionalStartEvent":
65
+ return { ...base, type: "startEvent", eventDefinitions: [{ type: "conditional" }] };
66
+ case "signalStartEvent":
67
+ return { ...base, type: "startEvent", eventDefinitions: [{ type: "signal" }] };
68
+ case "endEvent":
69
+ return { ...base, type: "endEvent", eventDefinitions: [] };
70
+ case "messageEndEvent":
71
+ return { ...base, type: "endEvent", eventDefinitions: [{ type: "message" }] };
72
+ case "escalationEndEvent":
73
+ return { ...base, type: "endEvent", eventDefinitions: [{ type: "escalation" }] };
74
+ case "errorEndEvent":
75
+ return { ...base, type: "endEvent", eventDefinitions: [{ type: "error" }] };
76
+ case "compensationEndEvent":
77
+ return { ...base, type: "endEvent", eventDefinitions: [{ type: "compensate" }] };
78
+ case "signalEndEvent":
79
+ return { ...base, type: "endEvent", eventDefinitions: [{ type: "signal" }] };
80
+ case "terminateEndEvent":
81
+ return { ...base, type: "endEvent", eventDefinitions: [{ type: "terminate" }] };
82
+ case "intermediateThrowEvent":
83
+ return { ...base, type: "intermediateThrowEvent", eventDefinitions: [] };
84
+ case "intermediateCatchEvent":
85
+ return { ...base, type: "intermediateCatchEvent", eventDefinitions: [] };
86
+ case "messageCatchEvent":
87
+ return { ...base, type: "intermediateCatchEvent", eventDefinitions: [{ type: "message" }] };
88
+ case "messageThrowEvent":
89
+ return { ...base, type: "intermediateThrowEvent", eventDefinitions: [{ type: "message" }] };
90
+ case "timerCatchEvent":
91
+ return { ...base, type: "intermediateCatchEvent", eventDefinitions: [{ type: "timer" }] };
92
+ case "escalationThrowEvent":
93
+ return {
94
+ ...base,
95
+ type: "intermediateThrowEvent",
96
+ eventDefinitions: [{ type: "escalation" }],
97
+ };
98
+ case "conditionalCatchEvent":
99
+ return {
100
+ ...base,
101
+ type: "intermediateCatchEvent",
102
+ eventDefinitions: [{ type: "conditional" }],
103
+ };
104
+ case "linkCatchEvent":
105
+ return { ...base, type: "intermediateCatchEvent", eventDefinitions: [{ type: "link" }] };
106
+ case "linkThrowEvent":
107
+ return { ...base, type: "intermediateThrowEvent", eventDefinitions: [{ type: "link" }] };
108
+ case "compensationThrowEvent":
109
+ return {
110
+ ...base,
111
+ type: "intermediateThrowEvent",
112
+ eventDefinitions: [{ type: "compensate" }],
113
+ };
114
+ case "signalCatchEvent":
115
+ return { ...base, type: "intermediateCatchEvent", eventDefinitions: [{ type: "signal" }] };
116
+ case "signalThrowEvent":
117
+ return { ...base, type: "intermediateThrowEvent", eventDefinitions: [{ type: "signal" }] };
118
+ case "task":
119
+ return { ...base, type: "task" };
120
+ case "serviceTask":
121
+ return { ...base, type: "serviceTask" };
122
+ case "userTask":
123
+ return { ...base, type: "userTask" };
124
+ case "scriptTask":
125
+ return { ...base, type: "scriptTask" };
126
+ case "sendTask":
127
+ return { ...base, type: "sendTask" };
128
+ case "receiveTask":
129
+ return { ...base, type: "receiveTask" };
130
+ case "businessRuleTask":
131
+ return { ...base, type: "businessRuleTask" };
132
+ case "manualTask":
133
+ return { ...base, type: "manualTask" };
134
+ case "callActivity":
135
+ return { ...base, type: "callActivity" };
136
+ case "subProcess":
137
+ return {
138
+ ...base,
139
+ type: "subProcess",
140
+ flowElements: [],
141
+ sequenceFlows: [],
142
+ textAnnotations: [],
143
+ associations: [],
144
+ };
145
+ case "adHocSubProcess":
146
+ return {
147
+ ...base,
148
+ type: "adHocSubProcess",
149
+ flowElements: [],
150
+ sequenceFlows: [],
151
+ textAnnotations: [],
152
+ associations: [],
153
+ };
154
+ case "transaction":
155
+ return {
156
+ ...base,
157
+ type: "transaction",
158
+ flowElements: [],
159
+ sequenceFlows: [],
160
+ textAnnotations: [],
161
+ associations: [],
162
+ };
163
+ case "exclusiveGateway":
164
+ return { ...base, type: "exclusiveGateway" };
165
+ case "parallelGateway":
166
+ return { ...base, type: "parallelGateway" };
167
+ case "inclusiveGateway":
168
+ return { ...base, type: "inclusiveGateway" };
169
+ case "eventBasedGateway":
170
+ return { ...base, type: "eventBasedGateway" };
171
+ case "complexGateway":
172
+ return { ...base, type: "complexGateway" };
173
+ case "textAnnotation":
174
+ throw new Error("textAnnotation is not a flow element — use createAnnotation()");
175
+ }
176
+ }
177
+ // ── Container subprocess helpers ──────────────────────────────────────────────
178
+ /** Recursively collects all descendant flow element IDs and sequence flow IDs. */
179
+ function collectDescendantIds(el, out) {
180
+ if (el.type !== "subProcess" &&
181
+ el.type !== "adHocSubProcess" &&
182
+ el.type !== "eventSubProcess" &&
183
+ el.type !== "transaction")
184
+ return;
185
+ for (const child of el.flowElements) {
186
+ out.add(child.id);
187
+ collectDescendantIds(child, out);
188
+ }
189
+ for (const sf of el.sequenceFlows) {
190
+ out.add(sf.id);
191
+ }
192
+ }
193
+ /** Flattens all sequence flows from all nesting levels into a Map. */
194
+ function collectAllSequenceFlows(flowElements, sequenceFlows, out) {
195
+ for (const sf of sequenceFlows) {
196
+ out.set(sf.id, sf);
197
+ }
198
+ for (const el of flowElements) {
199
+ if (el.type === "subProcess" ||
200
+ el.type === "adHocSubProcess" ||
201
+ el.type === "eventSubProcess" ||
202
+ el.type === "transaction") {
203
+ collectAllSequenceFlows(el.flowElements, el.sequenceFlows, out);
204
+ }
205
+ }
206
+ }
207
+ /** Returns the innermost container element ID whose bounds contain (cx, cy), or null. */
208
+ function findContainerForPoint(defs, cx, cy) {
209
+ const process = defs.processes[0];
210
+ const diagram = defs.diagrams[0];
211
+ if (!process || !diagram)
212
+ return null;
213
+ const containerIds = new Set();
214
+ const gatherContainers = (elements) => {
215
+ for (const el of elements) {
216
+ if (el.type === "subProcess" ||
217
+ el.type === "adHocSubProcess" ||
218
+ el.type === "eventSubProcess" ||
219
+ el.type === "transaction") {
220
+ containerIds.add(el.id);
221
+ gatherContainers(el.flowElements);
222
+ }
223
+ }
224
+ };
225
+ gatherContainers(process.flowElements);
226
+ let bestId = null;
227
+ let bestArea = Number.POSITIVE_INFINITY;
228
+ for (const shape of diagram.plane.shapes) {
229
+ if (!containerIds.has(shape.bpmnElement))
230
+ continue;
231
+ const { x, y, width, height } = shape.bounds;
232
+ if (cx >= x && cx <= x + width && cy >= y && cy <= y + height) {
233
+ const area = width * height;
234
+ if (area < bestArea) {
235
+ bestArea = area;
236
+ bestId = shape.bpmnElement;
237
+ }
238
+ }
239
+ }
240
+ return bestId;
241
+ }
242
+ /** Recursively finds the container by ID and appends newElement to its flowElements. */
243
+ function addToContainer(flowElements, containerId, newElement) {
244
+ return flowElements.map((el) => {
245
+ if (el.type !== "subProcess" &&
246
+ el.type !== "adHocSubProcess" &&
247
+ el.type !== "eventSubProcess" &&
248
+ el.type !== "transaction")
249
+ return el;
250
+ if (el.id === containerId) {
251
+ return { ...el, flowElements: [...el.flowElements, newElement] };
252
+ }
253
+ return { ...el, flowElements: addToContainer(el.flowElements, containerId, newElement) };
254
+ });
255
+ }
256
+ /** Recursively searches all levels for an element/flow by ID and updates its name. */
257
+ function updateNameInElements(flowElements, sequenceFlows, id, name) {
258
+ const elIndex = flowElements.findIndex((el) => el.id === id);
259
+ if (elIndex >= 0) {
260
+ const newElements = [...flowElements];
261
+ const el = newElements[elIndex];
262
+ if (el)
263
+ newElements[elIndex] = { ...el, name };
264
+ return { elements: newElements, flows: sequenceFlows, updated: true };
265
+ }
266
+ const sfIndex = sequenceFlows.findIndex((sf) => sf.id === id);
267
+ if (sfIndex >= 0) {
268
+ const newFlows = [...sequenceFlows];
269
+ const sf = newFlows[sfIndex];
270
+ if (sf)
271
+ newFlows[sfIndex] = { ...sf, name };
272
+ return { elements: flowElements, flows: newFlows, updated: true };
273
+ }
274
+ for (let i = 0; i < flowElements.length; i++) {
275
+ const el = flowElements[i];
276
+ if (el &&
277
+ (el.type === "subProcess" ||
278
+ el.type === "adHocSubProcess" ||
279
+ el.type === "eventSubProcess" ||
280
+ el.type === "transaction")) {
281
+ const r = updateNameInElements(el.flowElements, el.sequenceFlows, id, name);
282
+ if (r.updated) {
283
+ const newElements = [...flowElements];
284
+ newElements[i] = { ...el, flowElements: r.elements, sequenceFlows: r.flows };
285
+ return { elements: newElements, flows: sequenceFlows, updated: true };
286
+ }
287
+ }
288
+ }
289
+ return { elements: flowElements, flows: sequenceFlows, updated: false };
290
+ }
291
+ /**
292
+ * Recursively removes deleted elements from all nesting levels.
293
+ * Mutates allRemovedIds (adds removed flow IDs) for DI edge cleanup.
294
+ */
295
+ function removeFromContainers(flowElements, sequenceFlows, idSet, allRemovedIds) {
296
+ const flowsToRemove = new Set(sequenceFlows
297
+ .filter((sf) => idSet.has(sf.id) || idSet.has(sf.sourceRef) || idSet.has(sf.targetRef))
298
+ .map((sf) => sf.id));
299
+ for (const fid of flowsToRemove)
300
+ allRemovedIds.add(fid);
301
+ const newSequenceFlows = sequenceFlows.filter((sf) => !flowsToRemove.has(sf.id));
302
+ const newFlowElements = flowElements
303
+ .filter((el) => !idSet.has(el.id))
304
+ .map((el) => {
305
+ const cleaned = {
306
+ ...el,
307
+ incoming: el.incoming.filter((ref) => !allRemovedIds.has(ref)),
308
+ outgoing: el.outgoing.filter((ref) => !allRemovedIds.has(ref)),
309
+ };
310
+ if (cleaned.type === "subProcess" ||
311
+ cleaned.type === "adHocSubProcess" ||
312
+ cleaned.type === "eventSubProcess" ||
313
+ cleaned.type === "transaction") {
314
+ const r = removeFromContainers(cleaned.flowElements, cleaned.sequenceFlows, idSet, allRemovedIds);
315
+ return { ...cleaned, flowElements: r.flowElements, sequenceFlows: r.sequenceFlows };
316
+ }
317
+ return cleaned;
318
+ });
319
+ return { flowElements: newFlowElements, sequenceFlows: newSequenceFlows };
320
+ }
321
+ /** Recursively finds element by ID and applies updateFn to it. */
322
+ function updateRefInElements(flowElements, id, updateFn) {
323
+ return flowElements.map((el) => {
324
+ if (el.id === id)
325
+ return updateFn(el);
326
+ if (el.type === "subProcess" ||
327
+ el.type === "adHocSubProcess" ||
328
+ el.type === "eventSubProcess" ||
329
+ el.type === "transaction") {
330
+ return { ...el, flowElements: updateRefInElements(el.flowElements, id, updateFn) };
331
+ }
332
+ return el;
333
+ });
334
+ }
335
+ // ── Create shape ─────────────────────────────────────────────────────────────
336
+ export function createShape(defs, type, bounds, name) {
337
+ const id = genId(type);
338
+ const shapeId = genId(`${type}_di`);
339
+ const flowElement = makeFlowElement(type, id, name);
340
+ const process = defs.processes[0];
341
+ if (!process)
342
+ return { defs, id };
343
+ const diagram = defs.diagrams[0];
344
+ if (!diagram)
345
+ return { defs, id };
346
+ const diShape = {
347
+ id: shapeId,
348
+ bpmnElement: id,
349
+ bounds,
350
+ unknownAttributes: {},
351
+ };
352
+ const cx = bounds.x + bounds.width / 2;
353
+ const cy = bounds.y + bounds.height / 2;
354
+ const containerId = findContainerForPoint(defs, cx, cy);
355
+ const newDefs = {
356
+ ...defs,
357
+ processes: [
358
+ {
359
+ ...process,
360
+ flowElements: containerId !== null
361
+ ? addToContainer(process.flowElements, containerId, flowElement)
362
+ : [...process.flowElements, flowElement],
363
+ },
364
+ ...defs.processes.slice(1),
365
+ ],
366
+ diagrams: [
367
+ {
368
+ ...diagram,
369
+ plane: {
370
+ ...diagram.plane,
371
+ shapes: [...diagram.plane.shapes, diShape],
372
+ },
373
+ },
374
+ ...defs.diagrams.slice(1),
375
+ ],
376
+ };
377
+ return { defs: newDefs, id };
378
+ }
379
+ // ── Create boundary event ─────────────────────────────────────────────────────
380
+ export function createBoundaryEvent(defs, hostId, eventDefType, bounds, cancelActivity = true) {
381
+ const id = genId("BoundaryEvent");
382
+ const shapeId = genId("BoundaryEvent_di");
383
+ const process = defs.processes[0];
384
+ if (!process)
385
+ return { defs, id };
386
+ const diagram = defs.diagrams[0];
387
+ if (!diagram)
388
+ return { defs, id };
389
+ const eventDefs = eventDefType ? [{ type: eventDefType }] : [];
390
+ const boundaryEvent = {
391
+ type: "boundaryEvent",
392
+ id,
393
+ attachedToRef: hostId,
394
+ cancelActivity,
395
+ eventDefinitions: eventDefs,
396
+ incoming: [],
397
+ outgoing: [],
398
+ extensionElements: [],
399
+ unknownAttributes: {},
400
+ };
401
+ const diShape = {
402
+ id: shapeId,
403
+ bpmnElement: id,
404
+ bounds,
405
+ unknownAttributes: {},
406
+ };
407
+ return {
408
+ defs: {
409
+ ...defs,
410
+ processes: [
411
+ {
412
+ ...process,
413
+ flowElements: [...process.flowElements, boundaryEvent],
414
+ },
415
+ ...defs.processes.slice(1),
416
+ ],
417
+ diagrams: [
418
+ {
419
+ ...diagram,
420
+ plane: {
421
+ ...diagram.plane,
422
+ shapes: [...diagram.plane.shapes, diShape],
423
+ },
424
+ },
425
+ ...defs.diagrams.slice(1),
426
+ ],
427
+ },
428
+ id,
429
+ };
430
+ }
431
+ // ── Create connection ─────────────────────────────────────────────────────────
432
+ export function createConnection(defs, sourceId, targetId, waypoints) {
433
+ const id = genId("Flow");
434
+ const edgeId = genId("Flow_di");
435
+ const process = defs.processes[0];
436
+ if (!process)
437
+ return { defs, id };
438
+ const diagram = defs.diagrams[0];
439
+ if (!diagram)
440
+ return { defs, id };
441
+ const sf = {
442
+ id,
443
+ sourceRef: sourceId,
444
+ targetRef: targetId,
445
+ extensionElements: [],
446
+ unknownAttributes: {},
447
+ };
448
+ const edge = {
449
+ id: edgeId,
450
+ bpmnElement: id,
451
+ waypoints,
452
+ unknownAttributes: {},
453
+ };
454
+ // Update source.outgoing and target.incoming (recursive — handles subprocess children)
455
+ let updatedElements = updateRefInElements(process.flowElements, sourceId, (el) => ({ ...el, outgoing: [...el.outgoing, id] }));
456
+ updatedElements = updateRefInElements(updatedElements, targetId, (el) => ({ ...el, incoming: [...el.incoming, id] }));
457
+ const newDefs = {
458
+ ...defs,
459
+ processes: [
460
+ {
461
+ ...process,
462
+ flowElements: updatedElements,
463
+ sequenceFlows: [...process.sequenceFlows, sf],
464
+ },
465
+ ...defs.processes.slice(1),
466
+ ],
467
+ diagrams: [
468
+ {
469
+ ...diagram,
470
+ plane: {
471
+ ...diagram.plane,
472
+ edges: [...diagram.plane.edges, edge],
473
+ },
474
+ },
475
+ ...defs.diagrams.slice(1),
476
+ ],
477
+ };
478
+ return { defs: newDefs, id };
479
+ }
480
+ // ── Move shapes ───────────────────────────────────────────────────────────────
481
+ /** Returns true if the boundary event center is within the host bounds (with a margin). */
482
+ function isOnHostBoundary(eventBounds, hostBounds) {
483
+ const margin = 24;
484
+ const cx = eventBounds.x + eventBounds.width / 2;
485
+ const cy = eventBounds.y + eventBounds.height / 2;
486
+ return (cx >= hostBounds.x - margin &&
487
+ cx <= hostBounds.x + hostBounds.width + margin &&
488
+ cy >= hostBounds.y - margin &&
489
+ cy <= hostBounds.y + hostBounds.height + margin);
490
+ }
491
+ export function moveShapes(defs, moves) {
492
+ if (moves.length === 0)
493
+ return defs;
494
+ const moveMap = new Map(moves.map((m) => [m.id, m]));
495
+ const process = defs.processes[0];
496
+ const diagram = defs.diagrams[0];
497
+ if (!process || !diagram)
498
+ return defs;
499
+ // Cascade: boundary events attached to moved shapes also move, but only if they
500
+ // are currently positioned on/near the host boundary (not moved away by the user).
501
+ const extendedMoves = [...moves];
502
+ for (const el of process.flowElements) {
503
+ if (el.type === "boundaryEvent" && moveMap.has(el.attachedToRef) && !moveMap.has(el.id)) {
504
+ const hostShape = diagram.plane.shapes.find((s) => s.bpmnElement === el.attachedToRef);
505
+ const eventShape = diagram.plane.shapes.find((s) => s.bpmnElement === el.id);
506
+ if (hostShape && eventShape && isOnHostBoundary(eventShape.bounds, hostShape.bounds)) {
507
+ const hostMove = moveMap.get(el.attachedToRef);
508
+ if (hostMove)
509
+ extendedMoves.push({ id: el.id, dx: hostMove.dx, dy: hostMove.dy });
510
+ }
511
+ }
512
+ }
513
+ // Cascade: descendants of moving container elements (subprocesses) also move.
514
+ const seenIds = new Set(extendedMoves.map((m) => m.id));
515
+ const cascadeDescendants = (elements) => {
516
+ for (const el of elements) {
517
+ if (el.type === "subProcess" ||
518
+ el.type === "adHocSubProcess" ||
519
+ el.type === "eventSubProcess" ||
520
+ el.type === "transaction") {
521
+ if (moveMap.has(el.id)) {
522
+ const move = moveMap.get(el.id);
523
+ if (move) {
524
+ const descIds = new Set();
525
+ collectDescendantIds(el, descIds);
526
+ for (const descId of descIds) {
527
+ if (!seenIds.has(descId)) {
528
+ seenIds.add(descId);
529
+ extendedMoves.push({ id: descId, dx: move.dx, dy: move.dy });
530
+ }
531
+ }
532
+ }
533
+ }
534
+ cascadeDescendants(el.flowElements);
535
+ }
536
+ }
537
+ };
538
+ cascadeDescendants(process.flowElements);
539
+ const extendedMoveMap = new Map(extendedMoves.map((m) => [m.id, m]));
540
+ // Update DI shape bounds (and label bounds, if present)
541
+ const newShapes = diagram.plane.shapes.map((s) => {
542
+ const m = extendedMoveMap.get(s.bpmnElement);
543
+ if (!m)
544
+ return s;
545
+ return {
546
+ ...s,
547
+ bounds: {
548
+ ...s.bounds,
549
+ x: s.bounds.x + m.dx,
550
+ y: s.bounds.y + m.dy,
551
+ },
552
+ label: s.label?.bounds !== undefined
553
+ ? {
554
+ ...s.label,
555
+ bounds: {
556
+ x: s.label.bounds.x + m.dx,
557
+ y: s.label.bounds.y + m.dy,
558
+ width: s.label.bounds.width,
559
+ height: s.label.bounds.height,
560
+ },
561
+ }
562
+ : s.label,
563
+ };
564
+ });
565
+ // Update edge waypoints
566
+ // After each move, ensure sequence flows:
567
+ // 1. Never pass behind/through other elements (obstacle avoidance)
568
+ // 2. Never share the same connection point on an element (port deconfliction)
569
+ const allFlows = new Map();
570
+ collectAllSequenceFlows(process.flowElements, process.sequenceFlows, allFlows);
571
+ // Build a lookup of post-move shape bounds
572
+ const shapeBoundsMap = new Map();
573
+ for (const s of newShapes) {
574
+ shapeBoundsMap.set(s.bpmnElement, s.bounds);
575
+ }
576
+ const newEdges = diagram.plane.edges.map((edge) => {
577
+ // Handle sequence flows (search all nesting levels)
578
+ const flow = allFlows.get(edge.bpmnElement);
579
+ if (flow) {
580
+ if (edge.waypoints.length < 2)
581
+ return edge;
582
+ const srcMove = extendedMoveMap.get(flow.sourceRef);
583
+ const tgtMove = extendedMoveMap.get(flow.targetRef);
584
+ const srcShape = newShapes.find((s) => s.bpmnElement === flow.sourceRef);
585
+ const tgtShape = newShapes.find((s) => s.bpmnElement === flow.targetRef);
586
+ if (!srcShape || !tgtShape)
587
+ return edge;
588
+ // Obstacles = all shapes except this edge's source and target
589
+ const obstacles = newShapes
590
+ .filter((s) => s.bpmnElement !== flow.sourceRef && s.bpmnElement !== flow.targetRef)
591
+ .map((s) => s.bounds);
592
+ if (srcMove || tgtMove) {
593
+ // At least one endpoint moved — always re-route from port midpoints so that
594
+ // connection points are centered on the side and obstacles are avoided.
595
+ return {
596
+ ...edge,
597
+ waypoints: computeWaypointsAvoiding(srcShape.bounds, tgtShape.bounds, obstacles),
598
+ };
599
+ }
600
+ // Neither endpoint moves — validate the existing waypoints fully.
601
+ // This catches both: (a) a moved shape now blocking the path, and
602
+ // (b) pre-existing invalid paths loaded from XML that pass through
603
+ // a shape's interior. Any invalid path is re-routed.
604
+ if (waypointsIntersectObstacles(edge.waypoints, obstacles) ||
605
+ routeEntersShape(edge.waypoints, srcShape.bounds) ||
606
+ routeEntersShape(edge.waypoints, tgtShape.bounds)) {
607
+ return {
608
+ ...edge,
609
+ waypoints: computeWaypointsAvoiding(srcShape.bounds, tgtShape.bounds, obstacles),
610
+ };
611
+ }
612
+ return edge;
613
+ }
614
+ // Handle association edges
615
+ const assoc = process.associations.find((a) => a.id === edge.bpmnElement);
616
+ if (assoc) {
617
+ const srcMove = extendedMoveMap.get(assoc.sourceRef);
618
+ const tgtMove = extendedMoveMap.get(assoc.targetRef);
619
+ if (!srcMove && !tgtMove)
620
+ return edge;
621
+ if (srcMove && tgtMove) {
622
+ return {
623
+ ...edge,
624
+ waypoints: edge.waypoints.map((wp) => ({ x: wp.x + srcMove.dx, y: wp.y + srcMove.dy })),
625
+ };
626
+ }
627
+ const srcShape = newShapes.find((s) => s.bpmnElement === assoc.sourceRef);
628
+ const tgtShape = newShapes.find((s) => s.bpmnElement === assoc.targetRef);
629
+ if (srcShape && tgtShape) {
630
+ return { ...edge, waypoints: computeWaypoints(srcShape.bounds, tgtShape.bounds) };
631
+ }
632
+ }
633
+ return edge;
634
+ });
635
+ // Port deconfliction: spread edges that share the same connection point on a shape
636
+ const deconflictedEdges = deconflictPorts(newEdges, shapeBoundsMap, allFlows);
637
+ return {
638
+ ...defs,
639
+ diagrams: [
640
+ {
641
+ ...diagram,
642
+ plane: { ...diagram.plane, shapes: newShapes, edges: deconflictedEdges },
643
+ },
644
+ ...defs.diagrams.slice(1),
645
+ ],
646
+ };
647
+ }
648
+ // ── Port deconfliction ────────────────────────────────────────────────────────
649
+ /**
650
+ * Spreads edge connection points when multiple flows share the same port
651
+ * midpoint on a shape. Groups edges by (shapeId, port-side) and offsets
652
+ * them evenly along that side so no two flows touch the exact same point.
653
+ */
654
+ function deconflictPorts(edges, shapeBoundsMap, allFlows) {
655
+ const groups = new Map();
656
+ for (let i = 0; i < edges.length; i++) {
657
+ const edge = edges[i];
658
+ if (!edge || edge.waypoints.length < 2)
659
+ continue;
660
+ const flow = allFlows.get(edge.bpmnElement);
661
+ if (!flow)
662
+ continue;
663
+ const srcBounds = shapeBoundsMap.get(flow.sourceRef);
664
+ const tgtBounds = shapeBoundsMap.get(flow.targetRef);
665
+ if (!srcBounds || !tgtBounds)
666
+ continue;
667
+ const firstWp = edge.waypoints[0];
668
+ const lastWp = edge.waypoints[edge.waypoints.length - 1];
669
+ if (!firstWp || !lastWp)
670
+ continue;
671
+ const srcPort = portFromWaypoint(firstWp, srcBounds);
672
+ const tgtPort = portFromWaypoint(lastWp, tgtBounds);
673
+ const srcKey = `${flow.sourceRef}:${srcPort}`;
674
+ const tgtKey = `${flow.targetRef}:${tgtPort}`;
675
+ const sg = groups.get(srcKey) ?? [];
676
+ sg.push({ edgeIdx: i, isSource: true, port: srcPort });
677
+ groups.set(srcKey, sg);
678
+ const tg = groups.get(tgtKey) ?? [];
679
+ tg.push({ edgeIdx: i, isSource: false, port: tgtPort });
680
+ groups.set(tgtKey, tg);
681
+ }
682
+ // Compute spread waypoints for groups with collisions
683
+ const newSrcTerminals = new Map();
684
+ const newSrcPorts = new Map();
685
+ const newTgtTerminals = new Map();
686
+ const newTgtPorts = new Map();
687
+ for (const [key, group] of groups) {
688
+ if (group.length <= 1)
689
+ continue;
690
+ const colonIdx = key.indexOf(":");
691
+ const shapeId = key.slice(0, colonIdx);
692
+ const portStr = key.slice(colonIdx + 1);
693
+ if (portStr !== "top" && portStr !== "right" && portStr !== "bottom" && portStr !== "left")
694
+ continue;
695
+ const port = portStr;
696
+ const bounds = shapeBoundsMap.get(shapeId);
697
+ if (!bounds)
698
+ continue;
699
+ const n = group.length;
700
+ const isH = port === "left" || port === "right";
701
+ const available = isH ? bounds.height - 30 : bounds.width - 30;
702
+ const spacing = Math.min(25, available / Math.max(n - 1, 1));
703
+ for (let i = 0; i < n; i++) {
704
+ const term = group[i];
705
+ if (!term)
706
+ continue;
707
+ const offset = (i - (n - 1) / 2) * spacing;
708
+ let newPt;
709
+ if (port === "right") {
710
+ newPt = { x: bounds.x + bounds.width, y: bounds.y + bounds.height / 2 + offset };
711
+ }
712
+ else if (port === "left") {
713
+ newPt = { x: bounds.x, y: bounds.y + bounds.height / 2 + offset };
714
+ }
715
+ else if (port === "bottom") {
716
+ newPt = { x: bounds.x + bounds.width / 2 + offset, y: bounds.y + bounds.height };
717
+ }
718
+ else {
719
+ newPt = { x: bounds.x + bounds.width / 2 + offset, y: bounds.y };
720
+ }
721
+ if (term.isSource) {
722
+ newSrcTerminals.set(term.edgeIdx, newPt);
723
+ newSrcPorts.set(term.edgeIdx, port);
724
+ }
725
+ else {
726
+ newTgtTerminals.set(term.edgeIdx, newPt);
727
+ newTgtPorts.set(term.edgeIdx, port);
728
+ }
729
+ }
730
+ }
731
+ if (newSrcTerminals.size === 0 && newTgtTerminals.size === 0)
732
+ return edges;
733
+ const result = [...edges];
734
+ for (let i = 0; i < result.length; i++) {
735
+ const newSrcPt = newSrcTerminals.get(i);
736
+ const newTgtPt = newTgtTerminals.get(i);
737
+ if (!newSrcPt && !newTgtPt)
738
+ continue;
739
+ const edge = result[i];
740
+ if (!edge || edge.waypoints.length < 2)
741
+ continue;
742
+ const flow = allFlows.get(edge.bpmnElement);
743
+ if (!flow)
744
+ continue;
745
+ const srcBounds = shapeBoundsMap.get(flow.sourceRef);
746
+ const tgtBounds = shapeBoundsMap.get(flow.targetRef);
747
+ if (!srcBounds || !tgtBounds)
748
+ continue;
749
+ const firstWp = edge.waypoints[0];
750
+ const lastWp = edge.waypoints[edge.waypoints.length - 1];
751
+ if (!firstWp || !lastWp)
752
+ continue;
753
+ const srcPt = newSrcPt ?? firstWp;
754
+ const tgtPt = newTgtPt ?? lastWp;
755
+ const srcPort = newSrcPorts.get(i) ?? portFromWaypoint(srcPt, srcBounds);
756
+ const tgtPort = newTgtPorts.get(i) ?? portFromWaypoint(tgtPt, tgtBounds);
757
+ // Compute obstacles for this edge (all shapes except its src and tgt)
758
+ const obstacles = [];
759
+ for (const [shapeId, bounds] of shapeBoundsMap) {
760
+ if (shapeId !== flow.sourceRef && shapeId !== flow.targetRef) {
761
+ obstacles.push(bounds);
762
+ }
763
+ }
764
+ const candidate = routeOrthogonal(srcPt, srcPort, tgtPt, tgtPort);
765
+ // If the spread route passes through an obstacle, fall back to the obstacle-avoiding
766
+ // route (which exits from the port midpoint, but never goes behind an element).
767
+ result[i] = {
768
+ ...edge,
769
+ waypoints: waypointsIntersectObstacles(candidate, obstacles)
770
+ ? computeWaypointsAvoiding(srcBounds, tgtBounds, obstacles)
771
+ : candidate,
772
+ };
773
+ }
774
+ return result;
775
+ }
776
+ // ── Resize shape ──────────────────────────────────────────────────────────────
777
+ export function resizeShape(defs, id, newBounds) {
778
+ const process = defs.processes[0];
779
+ const diagram = defs.diagrams[0];
780
+ if (!process || !diagram)
781
+ return defs;
782
+ const newShapes = diagram.plane.shapes.map((s) => s.bpmnElement === id ? { ...s, bounds: newBounds } : s);
783
+ // Recompute terminal waypoints for connected edges
784
+ const newEdges = diagram.plane.edges.map((edge) => {
785
+ const flow = process.sequenceFlows.find((sf) => sf.id === edge.bpmnElement);
786
+ if (!flow)
787
+ return edge;
788
+ const isSource = flow.sourceRef === id;
789
+ const isTarget = flow.targetRef === id;
790
+ if (!isSource && !isTarget)
791
+ return edge;
792
+ // Find the other shape's bounds
793
+ const otherId = isSource ? flow.targetRef : flow.sourceRef;
794
+ const otherShape = diagram.plane.shapes.find((s) => s.bpmnElement === otherId);
795
+ if (!otherShape)
796
+ return edge;
797
+ const wps = isSource
798
+ ? computeWaypoints(newBounds, otherShape.bounds)
799
+ : computeWaypoints(otherShape.bounds, newBounds);
800
+ return { ...edge, waypoints: wps };
801
+ });
802
+ return {
803
+ ...defs,
804
+ diagrams: [
805
+ {
806
+ ...diagram,
807
+ plane: { ...diagram.plane, shapes: newShapes, edges: newEdges },
808
+ },
809
+ ...defs.diagrams.slice(1),
810
+ ],
811
+ };
812
+ }
813
+ // ── Delete elements ───────────────────────────────────────────────────────────
814
+ export function deleteElements(defs, ids) {
815
+ if (ids.length === 0)
816
+ return defs;
817
+ const idSet = new Set(ids);
818
+ // Cascade: boundary events whose host is deleted are also deleted
819
+ const process0 = defs.processes[0];
820
+ if (process0) {
821
+ for (const el of process0.flowElements) {
822
+ if (el.type === "boundaryEvent" && idSet.has(el.attachedToRef)) {
823
+ idSet.add(el.id);
824
+ }
825
+ }
826
+ }
827
+ // Cascade: descendants of deleted container elements are also deleted
828
+ if (process0) {
829
+ for (const el of process0.flowElements) {
830
+ if (idSet.has(el.id))
831
+ collectDescendantIds(el, idSet);
832
+ }
833
+ }
834
+ const process = defs.processes[0];
835
+ const diagram = defs.diagrams[0];
836
+ if (!process || !diagram)
837
+ return defs;
838
+ // Find associations to remove (directly specified, or whose source/target is deleted)
839
+ const assocsToRemove = new Set(process.associations
840
+ .filter((a) => idSet.has(a.id) || idSet.has(a.sourceRef) || idSet.has(a.targetRef))
841
+ .map((a) => a.id));
842
+ // Recursively remove elements and collect all removed flow IDs into allRemovedIds
843
+ const allRemovedIds = new Set(idSet);
844
+ const { flowElements: newFlowElements, sequenceFlows: newSequenceFlows } = removeFromContainers(process.flowElements, process.sequenceFlows, idSet, allRemovedIds);
845
+ // Add association IDs for DI edge cleanup
846
+ for (const aid of assocsToRemove)
847
+ allRemovedIds.add(aid);
848
+ // Remove text annotations and associations
849
+ const newTextAnnotations = process.textAnnotations.filter((ta) => !idSet.has(ta.id));
850
+ const newAssociations = process.associations.filter((a) => !assocsToRemove.has(a.id));
851
+ // Remove DI shapes and edges
852
+ const newDiShapes = diagram.plane.shapes.filter((s) => !idSet.has(s.bpmnElement));
853
+ const newDiEdges = diagram.plane.edges.filter((e) => !allRemovedIds.has(e.bpmnElement));
854
+ return {
855
+ ...defs,
856
+ processes: [
857
+ {
858
+ ...process,
859
+ flowElements: newFlowElements,
860
+ sequenceFlows: newSequenceFlows,
861
+ textAnnotations: newTextAnnotations,
862
+ associations: newAssociations,
863
+ },
864
+ ...defs.processes.slice(1),
865
+ ],
866
+ diagrams: [
867
+ {
868
+ ...diagram,
869
+ plane: { ...diagram.plane, shapes: newDiShapes, edges: newDiEdges },
870
+ },
871
+ ...defs.diagrams.slice(1),
872
+ ],
873
+ };
874
+ }
875
+ // ── Update label ──────────────────────────────────────────────────────────────
876
+ export function updateLabel(defs, id, name) {
877
+ const process = defs.processes[0];
878
+ if (!process)
879
+ return defs;
880
+ // Check flow elements and sequence flows (recursive — handles subprocess children)
881
+ const r = updateNameInElements(process.flowElements, process.sequenceFlows, id, name);
882
+ if (r.updated) {
883
+ return {
884
+ ...defs,
885
+ processes: [
886
+ { ...process, flowElements: r.elements, sequenceFlows: r.flows },
887
+ ...defs.processes.slice(1),
888
+ ],
889
+ };
890
+ }
891
+ // Check text annotations (text field, not name)
892
+ const taIndex = process.textAnnotations.findIndex((ta) => ta.id === id);
893
+ if (taIndex >= 0) {
894
+ const newAnnotations = [...process.textAnnotations];
895
+ const ta = newAnnotations[taIndex];
896
+ if (ta) {
897
+ newAnnotations[taIndex] = { ...ta, text: name };
898
+ }
899
+ return {
900
+ ...defs,
901
+ processes: [{ ...process, textAnnotations: newAnnotations }, ...defs.processes.slice(1)],
902
+ };
903
+ }
904
+ return defs;
905
+ }
906
+ // ── Update label position ─────────────────────────────────────────────────────
907
+ /** Updates the DI label bounds for a shape (sets explicit external label position). */
908
+ export function updateLabelPosition(defs, shapeId, labelBounds) {
909
+ const diagram = defs.diagrams[0];
910
+ if (!diagram)
911
+ return defs;
912
+ const newShapes = diagram.plane.shapes.map((s) => s.bpmnElement === shapeId ? { ...s, label: { bounds: labelBounds } } : s);
913
+ return {
914
+ ...defs,
915
+ diagrams: [
916
+ { ...diagram, plane: { ...diagram.plane, shapes: newShapes } },
917
+ ...defs.diagrams.slice(1),
918
+ ],
919
+ };
920
+ }
921
+ // ── Update edge endpoint ──────────────────────────────────────────────────────
922
+ /**
923
+ * Reconnects one endpoint of an edge to a different port on the same
924
+ * source or target shape, recomputing the orthogonal route.
925
+ */
926
+ export function updateEdgeEndpoint(defs, edgeId, isStart, newPort) {
927
+ const process = defs.processes[0];
928
+ const diagram = defs.diagrams[0];
929
+ if (!process || !diagram)
930
+ return defs;
931
+ const edge = diagram.plane.edges.find((e) => e.bpmnElement === edgeId);
932
+ if (!edge || edge.waypoints.length < 2)
933
+ return defs;
934
+ const flow = process.sequenceFlows.find((sf) => sf.id === edgeId);
935
+ if (!flow)
936
+ return defs;
937
+ const srcShape = diagram.plane.shapes.find((s) => s.bpmnElement === flow.sourceRef);
938
+ const tgtShape = diagram.plane.shapes.find((s) => s.bpmnElement === flow.targetRef);
939
+ if (!srcShape || !tgtShape)
940
+ return defs;
941
+ const first = edge.waypoints[0];
942
+ const last = edge.waypoints[edge.waypoints.length - 1];
943
+ if (!first || !last)
944
+ return defs;
945
+ const srcPort = isStart ? newPort : portFromWaypoint(first, srcShape.bounds);
946
+ const tgtPort = isStart ? portFromWaypoint(last, tgtShape.bounds) : newPort;
947
+ const newWaypoints = computeWaypointsWithPorts(srcShape.bounds, srcPort, tgtShape.bounds, tgtPort);
948
+ const newEdges = diagram.plane.edges.map((e) => e.bpmnElement === edgeId ? { ...e, waypoints: newWaypoints } : e);
949
+ return {
950
+ ...defs,
951
+ diagrams: [
952
+ { ...diagram, plane: { ...diagram.plane, edges: newEdges } },
953
+ ...defs.diagrams.slice(1),
954
+ ],
955
+ };
956
+ }
957
+ // ── Edge segment / waypoint manipulation ──────────────────────────────────────
958
+ /**
959
+ * Moves an orthogonal edge segment perpendicularly by `delta` units.
960
+ * For horizontal segments delta shifts Y; for vertical segments delta shifts X.
961
+ * Both waypoints of the segment move together, stretching adjacent segments.
962
+ */
963
+ export function moveEdgeSegment(defs, edgeId, segIdx, isHoriz, delta) {
964
+ const diagram = defs.diagrams[0];
965
+ if (!diagram)
966
+ return defs;
967
+ const edge = diagram.plane.edges.find((e) => e.bpmnElement === edgeId);
968
+ if (!edge || segIdx >= edge.waypoints.length - 1)
969
+ return defs;
970
+ const newWaypoints = edge.waypoints.map((wp, i) => {
971
+ if (i === segIdx || i === segIdx + 1) {
972
+ return isHoriz ? { ...wp, y: wp.y + delta } : { ...wp, x: wp.x + delta };
973
+ }
974
+ return wp;
975
+ });
976
+ const newEdges = diagram.plane.edges.map((e) => e.bpmnElement === edgeId ? { ...e, waypoints: newWaypoints } : e);
977
+ return {
978
+ ...defs,
979
+ diagrams: [
980
+ { ...diagram, plane: { ...diagram.plane, edges: newEdges } },
981
+ ...defs.diagrams.slice(1),
982
+ ],
983
+ };
984
+ }
985
+ /**
986
+ * Inserts a new waypoint between waypoints[segIdx] and waypoints[segIdx+1].
987
+ * Used for free-form bend creation (diagonal movement allowed).
988
+ */
989
+ export function insertEdgeWaypoint(defs, edgeId, segIdx, pt) {
990
+ const diagram = defs.diagrams[0];
991
+ if (!diagram)
992
+ return defs;
993
+ const edge = diagram.plane.edges.find((e) => e.bpmnElement === edgeId);
994
+ if (!edge)
995
+ return defs;
996
+ const newWaypoints = [
997
+ ...edge.waypoints.slice(0, segIdx + 1),
998
+ pt,
999
+ ...edge.waypoints.slice(segIdx + 1),
1000
+ ];
1001
+ const newEdges = diagram.plane.edges.map((e) => e.bpmnElement === edgeId ? { ...e, waypoints: newWaypoints } : e);
1002
+ return {
1003
+ ...defs,
1004
+ diagrams: [
1005
+ { ...diagram, plane: { ...diagram.plane, edges: newEdges } },
1006
+ ...defs.diagrams.slice(1),
1007
+ ],
1008
+ };
1009
+ }
1010
+ /**
1011
+ * Moves a single intermediate waypoint (by index) to a new position.
1012
+ * Start (0) and end (last) waypoints are not moveable via this function.
1013
+ */
1014
+ export function moveEdgeWaypoint(defs, edgeId, wpIdx, pt) {
1015
+ const diagram = defs.diagrams[0];
1016
+ if (!diagram)
1017
+ return defs;
1018
+ const edge = diagram.plane.edges.find((e) => e.bpmnElement === edgeId);
1019
+ if (!edge || wpIdx <= 0 || wpIdx >= edge.waypoints.length - 1)
1020
+ return defs;
1021
+ const newWaypoints = edge.waypoints.map((wp, i) => (i === wpIdx ? { ...pt } : wp));
1022
+ const newEdges = diagram.plane.edges.map((e) => e.bpmnElement === edgeId ? { ...e, waypoints: newWaypoints } : e);
1023
+ return {
1024
+ ...defs,
1025
+ diagrams: [
1026
+ { ...diagram, plane: { ...diagram.plane, edges: newEdges } },
1027
+ ...defs.diagrams.slice(1),
1028
+ ],
1029
+ };
1030
+ }
1031
+ /**
1032
+ * Removes intermediate waypoints that lie exactly on the straight line between
1033
+ * their neighbours. Runs iteratively until no more collinear waypoints remain.
1034
+ */
1035
+ export function removeCollinearWaypoints(defs, edgeId) {
1036
+ const diagram = defs.diagrams[0];
1037
+ if (!diagram)
1038
+ return defs;
1039
+ const edge = diagram.plane.edges.find((e) => e.bpmnElement === edgeId);
1040
+ if (!edge || edge.waypoints.length < 3)
1041
+ return defs;
1042
+ const EPS = 1.0;
1043
+ let waypoints = [...edge.waypoints];
1044
+ let changed = true;
1045
+ while (changed) {
1046
+ changed = false;
1047
+ const filtered = [waypoints[0]];
1048
+ for (let i = 1; i < waypoints.length - 1; i++) {
1049
+ const a = waypoints[i - 1];
1050
+ const b = waypoints[i];
1051
+ const c = waypoints[i + 1];
1052
+ const cross = Math.abs((c.x - a.x) * (b.y - a.y) - (b.x - a.x) * (c.y - a.y));
1053
+ if (cross < EPS) {
1054
+ changed = true;
1055
+ }
1056
+ else {
1057
+ filtered.push(b);
1058
+ }
1059
+ }
1060
+ filtered.push(waypoints[waypoints.length - 1]);
1061
+ waypoints = filtered;
1062
+ }
1063
+ const newEdges = diagram.plane.edges.map((e) => e.bpmnElement === edgeId ? { ...e, waypoints } : e);
1064
+ return {
1065
+ ...defs,
1066
+ diagrams: [
1067
+ { ...diagram, plane: { ...diagram.plane, edges: newEdges } },
1068
+ ...defs.diagrams.slice(1),
1069
+ ],
1070
+ };
1071
+ }
1072
+ // ── Change element type ───────────────────────────────────────────────────────
1073
+ /**
1074
+ * Replaces a flow element's type while preserving its id, name, and connections.
1075
+ * Use this for gateway type-switching (exclusive ↔ parallel) and task type-switching.
1076
+ */
1077
+ export function changeElementType(defs, id, newType) {
1078
+ const process = defs.processes[0];
1079
+ if (!process)
1080
+ return defs;
1081
+ const elIndex = process.flowElements.findIndex((el) => el.id === id);
1082
+ if (elIndex < 0)
1083
+ return defs;
1084
+ const el = process.flowElements[elIndex];
1085
+ if (!el)
1086
+ return defs;
1087
+ const base = {
1088
+ id: el.id,
1089
+ name: el.name,
1090
+ incoming: el.incoming,
1091
+ outgoing: el.outgoing,
1092
+ extensionElements: el.extensionElements,
1093
+ unknownAttributes: el.unknownAttributes,
1094
+ };
1095
+ let newEl;
1096
+ switch (newType) {
1097
+ case "startEvent":
1098
+ newEl = { ...base, type: "startEvent", eventDefinitions: [] };
1099
+ break;
1100
+ case "messageStartEvent":
1101
+ newEl = { ...base, type: "startEvent", eventDefinitions: [{ type: "message" }] };
1102
+ break;
1103
+ case "timerStartEvent":
1104
+ newEl = { ...base, type: "startEvent", eventDefinitions: [{ type: "timer" }] };
1105
+ break;
1106
+ case "conditionalStartEvent":
1107
+ newEl = { ...base, type: "startEvent", eventDefinitions: [{ type: "conditional" }] };
1108
+ break;
1109
+ case "signalStartEvent":
1110
+ newEl = { ...base, type: "startEvent", eventDefinitions: [{ type: "signal" }] };
1111
+ break;
1112
+ case "endEvent":
1113
+ newEl = { ...base, type: "endEvent", eventDefinitions: [] };
1114
+ break;
1115
+ case "messageEndEvent":
1116
+ newEl = { ...base, type: "endEvent", eventDefinitions: [{ type: "message" }] };
1117
+ break;
1118
+ case "escalationEndEvent":
1119
+ newEl = { ...base, type: "endEvent", eventDefinitions: [{ type: "escalation" }] };
1120
+ break;
1121
+ case "errorEndEvent":
1122
+ newEl = { ...base, type: "endEvent", eventDefinitions: [{ type: "error" }] };
1123
+ break;
1124
+ case "compensationEndEvent":
1125
+ newEl = { ...base, type: "endEvent", eventDefinitions: [{ type: "compensate" }] };
1126
+ break;
1127
+ case "signalEndEvent":
1128
+ newEl = { ...base, type: "endEvent", eventDefinitions: [{ type: "signal" }] };
1129
+ break;
1130
+ case "terminateEndEvent":
1131
+ newEl = { ...base, type: "endEvent", eventDefinitions: [{ type: "terminate" }] };
1132
+ break;
1133
+ case "intermediateThrowEvent":
1134
+ newEl = { ...base, type: "intermediateThrowEvent", eventDefinitions: [] };
1135
+ break;
1136
+ case "intermediateCatchEvent":
1137
+ newEl = { ...base, type: "intermediateCatchEvent", eventDefinitions: [] };
1138
+ break;
1139
+ case "messageCatchEvent":
1140
+ newEl = { ...base, type: "intermediateCatchEvent", eventDefinitions: [{ type: "message" }] };
1141
+ break;
1142
+ case "messageThrowEvent":
1143
+ newEl = { ...base, type: "intermediateThrowEvent", eventDefinitions: [{ type: "message" }] };
1144
+ break;
1145
+ case "timerCatchEvent":
1146
+ newEl = { ...base, type: "intermediateCatchEvent", eventDefinitions: [{ type: "timer" }] };
1147
+ break;
1148
+ case "escalationThrowEvent":
1149
+ newEl = {
1150
+ ...base,
1151
+ type: "intermediateThrowEvent",
1152
+ eventDefinitions: [{ type: "escalation" }],
1153
+ };
1154
+ break;
1155
+ case "conditionalCatchEvent":
1156
+ newEl = {
1157
+ ...base,
1158
+ type: "intermediateCatchEvent",
1159
+ eventDefinitions: [{ type: "conditional" }],
1160
+ };
1161
+ break;
1162
+ case "linkCatchEvent":
1163
+ newEl = { ...base, type: "intermediateCatchEvent", eventDefinitions: [{ type: "link" }] };
1164
+ break;
1165
+ case "linkThrowEvent":
1166
+ newEl = { ...base, type: "intermediateThrowEvent", eventDefinitions: [{ type: "link" }] };
1167
+ break;
1168
+ case "compensationThrowEvent":
1169
+ newEl = {
1170
+ ...base,
1171
+ type: "intermediateThrowEvent",
1172
+ eventDefinitions: [{ type: "compensate" }],
1173
+ };
1174
+ break;
1175
+ case "signalCatchEvent":
1176
+ newEl = { ...base, type: "intermediateCatchEvent", eventDefinitions: [{ type: "signal" }] };
1177
+ break;
1178
+ case "signalThrowEvent":
1179
+ newEl = { ...base, type: "intermediateThrowEvent", eventDefinitions: [{ type: "signal" }] };
1180
+ break;
1181
+ case "task":
1182
+ newEl = { ...base, type: "task" };
1183
+ break;
1184
+ case "serviceTask":
1185
+ newEl = { ...base, type: "serviceTask" };
1186
+ break;
1187
+ case "userTask":
1188
+ newEl = { ...base, type: "userTask" };
1189
+ break;
1190
+ case "scriptTask":
1191
+ newEl = { ...base, type: "scriptTask" };
1192
+ break;
1193
+ case "sendTask":
1194
+ newEl = { ...base, type: "sendTask" };
1195
+ break;
1196
+ case "receiveTask":
1197
+ newEl = { ...base, type: "receiveTask" };
1198
+ break;
1199
+ case "businessRuleTask":
1200
+ newEl = { ...base, type: "businessRuleTask" };
1201
+ break;
1202
+ case "manualTask":
1203
+ newEl = { ...base, type: "manualTask" };
1204
+ break;
1205
+ case "callActivity":
1206
+ newEl = { ...base, type: "callActivity" };
1207
+ break;
1208
+ case "subProcess":
1209
+ newEl = {
1210
+ ...base,
1211
+ type: "subProcess",
1212
+ flowElements: el.type === "subProcess" ? el.flowElements : [],
1213
+ sequenceFlows: el.type === "subProcess" ? el.sequenceFlows : [],
1214
+ textAnnotations: el.type === "subProcess" ? el.textAnnotations : [],
1215
+ associations: el.type === "subProcess" ? el.associations : [],
1216
+ };
1217
+ break;
1218
+ case "adHocSubProcess":
1219
+ newEl = {
1220
+ ...base,
1221
+ type: "adHocSubProcess",
1222
+ flowElements: el.type === "adHocSubProcess" ? el.flowElements : [],
1223
+ sequenceFlows: el.type === "adHocSubProcess" ? el.sequenceFlows : [],
1224
+ textAnnotations: el.type === "adHocSubProcess" ? el.textAnnotations : [],
1225
+ associations: el.type === "adHocSubProcess" ? el.associations : [],
1226
+ };
1227
+ break;
1228
+ case "transaction":
1229
+ newEl = {
1230
+ ...base,
1231
+ type: "transaction",
1232
+ flowElements: el.type === "transaction" ? el.flowElements : [],
1233
+ sequenceFlows: el.type === "transaction" ? el.sequenceFlows : [],
1234
+ textAnnotations: el.type === "transaction" ? el.textAnnotations : [],
1235
+ associations: el.type === "transaction" ? el.associations : [],
1236
+ };
1237
+ break;
1238
+ case "exclusiveGateway":
1239
+ newEl = { ...base, type: "exclusiveGateway" };
1240
+ break;
1241
+ case "parallelGateway":
1242
+ newEl = { ...base, type: "parallelGateway" };
1243
+ break;
1244
+ case "inclusiveGateway":
1245
+ newEl = { ...base, type: "inclusiveGateway" };
1246
+ break;
1247
+ case "eventBasedGateway":
1248
+ newEl = { ...base, type: "eventBasedGateway" };
1249
+ break;
1250
+ case "complexGateway":
1251
+ newEl = { ...base, type: "complexGateway" };
1252
+ break;
1253
+ case "textAnnotation":
1254
+ throw new Error("textAnnotation is not a flow element — use createAnnotation()");
1255
+ }
1256
+ const newElements = [...process.flowElements];
1257
+ newElements[elIndex] = newEl;
1258
+ return {
1259
+ ...defs,
1260
+ processes: [{ ...process, flowElements: newElements }, ...defs.processes.slice(1)],
1261
+ };
1262
+ }
1263
+ // ── Insert shape on edge ──────────────────────────────────────────────────────
1264
+ /**
1265
+ * Splits an existing sequence flow by inserting a shape between its source and
1266
+ * target: removes the original edge and creates two new connections
1267
+ * (source → shapeId and shapeId → target).
1268
+ */
1269
+ export function insertShapeOnEdge(defs, edgeId, shapeId) {
1270
+ const process = defs.processes[0];
1271
+ const diagram = defs.diagrams[0];
1272
+ if (!process || !diagram)
1273
+ return defs;
1274
+ const flow = process.sequenceFlows.find((sf) => sf.id === edgeId);
1275
+ if (!flow)
1276
+ return defs;
1277
+ const srcDi = diagram.plane.shapes.find((s) => s.bpmnElement === flow.sourceRef);
1278
+ const tgtDi = diagram.plane.shapes.find((s) => s.bpmnElement === flow.targetRef);
1279
+ const newDi = diagram.plane.shapes.find((s) => s.bpmnElement === shapeId);
1280
+ if (!srcDi || !tgtDi || !newDi)
1281
+ return defs;
1282
+ const withoutEdge = deleteElements(defs, [edgeId]);
1283
+ const r1 = createConnection(withoutEdge, flow.sourceRef, shapeId, computeWaypoints(srcDi.bounds, newDi.bounds));
1284
+ const r2 = createConnection(r1.defs, shapeId, flow.targetRef, computeWaypoints(newDi.bounds, tgtDi.bounds));
1285
+ return r2.defs;
1286
+ }
1287
+ export function copyElements(defs, ids) {
1288
+ const idSet = new Set(ids);
1289
+ const process = defs.processes[0];
1290
+ const diagram = defs.diagrams[0];
1291
+ if (!process || !diagram) {
1292
+ return { elements: [], flows: [], shapes: [], edges: [] };
1293
+ }
1294
+ const elements = process.flowElements.filter((el) => idSet.has(el.id));
1295
+ const flows = process.sequenceFlows.filter((sf) => idSet.has(sf.sourceRef) && idSet.has(sf.targetRef));
1296
+ const flowIds = new Set(flows.map((sf) => sf.id));
1297
+ const shapes = diagram.plane.shapes.filter((s) => idSet.has(s.bpmnElement));
1298
+ const edges = diagram.plane.edges.filter((e) => flowIds.has(e.bpmnElement));
1299
+ return { elements, flows, shapes, edges };
1300
+ }
1301
+ export function pasteElements(defs, clipboard, offsetX, offsetY) {
1302
+ const newIds = new Map();
1303
+ // Generate new IDs for elements
1304
+ for (const el of clipboard.elements) {
1305
+ newIds.set(el.id, genId(el.type));
1306
+ }
1307
+ for (const sf of clipboard.flows) {
1308
+ newIds.set(sf.id, genId("Flow"));
1309
+ }
1310
+ const process = defs.processes[0];
1311
+ const diagram = defs.diagrams[0];
1312
+ if (!process || !diagram)
1313
+ return { defs, newIds };
1314
+ // Create new flow elements with new IDs, offset positions handled via DI
1315
+ const newElements = clipboard.elements.map((el) => {
1316
+ const newId = newIds.get(el.id) ?? genId(el.type);
1317
+ const newIncoming = el.incoming
1318
+ .map((ref) => newIds.get(ref))
1319
+ .filter((r) => r !== undefined);
1320
+ const newOutgoing = el.outgoing
1321
+ .map((ref) => newIds.get(ref))
1322
+ .filter((r) => r !== undefined);
1323
+ return { ...el, id: newId, incoming: newIncoming, outgoing: newOutgoing };
1324
+ });
1325
+ // Create new sequence flows
1326
+ const newFlows = clipboard.flows.map((sf) => {
1327
+ const newId = newIds.get(sf.id) ?? genId("Flow");
1328
+ const newSrc = newIds.get(sf.sourceRef) ?? sf.sourceRef;
1329
+ const newTgt = newIds.get(sf.targetRef) ?? sf.targetRef;
1330
+ return { ...sf, id: newId, sourceRef: newSrc, targetRef: newTgt };
1331
+ });
1332
+ // Create new DI shapes with offset
1333
+ const newDiShapes = clipboard.shapes.map((s) => {
1334
+ const newElId = newIds.get(s.bpmnElement) ?? s.bpmnElement;
1335
+ return {
1336
+ ...s,
1337
+ id: genId(`${newElId}_di`),
1338
+ bpmnElement: newElId,
1339
+ bounds: {
1340
+ ...s.bounds,
1341
+ x: s.bounds.x + offsetX,
1342
+ y: s.bounds.y + offsetY,
1343
+ },
1344
+ };
1345
+ });
1346
+ // Create new DI edges with offset waypoints
1347
+ const newDiEdges = clipboard.edges.map((e) => {
1348
+ const newFlowId = newIds.get(e.bpmnElement) ?? e.bpmnElement;
1349
+ return {
1350
+ ...e,
1351
+ id: genId(`${newFlowId}_di`),
1352
+ bpmnElement: newFlowId,
1353
+ waypoints: e.waypoints.map((wp) => ({
1354
+ x: wp.x + offsetX,
1355
+ y: wp.y + offsetY,
1356
+ })),
1357
+ };
1358
+ });
1359
+ const newDefs = {
1360
+ ...defs,
1361
+ processes: [
1362
+ {
1363
+ ...process,
1364
+ flowElements: [...process.flowElements, ...newElements],
1365
+ sequenceFlows: [...process.sequenceFlows, ...newFlows],
1366
+ },
1367
+ ...defs.processes.slice(1),
1368
+ ],
1369
+ diagrams: [
1370
+ {
1371
+ ...diagram,
1372
+ plane: {
1373
+ ...diagram.plane,
1374
+ shapes: [...diagram.plane.shapes, ...newDiShapes],
1375
+ edges: [...diagram.plane.edges, ...newDiEdges],
1376
+ },
1377
+ },
1378
+ ...defs.diagrams.slice(1),
1379
+ ],
1380
+ };
1381
+ return { defs: newDefs, newIds };
1382
+ }
1383
+ // ── Create text annotation ────────────────────────────────────────────────────
1384
+ export function createAnnotation(defs, bounds, text) {
1385
+ const id = genId("TextAnnotation");
1386
+ const shapeId = genId("TextAnnotation_di");
1387
+ const annotation = { id, text, unknownAttributes: {} };
1388
+ const diShape = { id: shapeId, bpmnElement: id, bounds, unknownAttributes: {} };
1389
+ const process = defs.processes[0];
1390
+ if (!process)
1391
+ return { defs, id };
1392
+ const diagram = defs.diagrams[0];
1393
+ if (!diagram)
1394
+ return { defs, id };
1395
+ return {
1396
+ defs: {
1397
+ ...defs,
1398
+ processes: [
1399
+ { ...process, textAnnotations: [...process.textAnnotations, annotation] },
1400
+ ...defs.processes.slice(1),
1401
+ ],
1402
+ diagrams: [
1403
+ {
1404
+ ...diagram,
1405
+ plane: { ...diagram.plane, shapes: [...diagram.plane.shapes, diShape] },
1406
+ },
1407
+ ...defs.diagrams.slice(1),
1408
+ ],
1409
+ },
1410
+ id,
1411
+ };
1412
+ }
1413
+ export function createAnnotationWithLink(defs, bounds, sourceId, sourceBounds, text) {
1414
+ const annotResult = createAnnotation(defs, bounds, text);
1415
+ const annotationId = annotResult.id;
1416
+ const assocId = genId("Association");
1417
+ const edgeId = genId("Association_di");
1418
+ const assoc = {
1419
+ id: assocId,
1420
+ sourceRef: sourceId,
1421
+ targetRef: annotationId,
1422
+ associationDirection: "None",
1423
+ unknownAttributes: {},
1424
+ };
1425
+ const waypoints = computeWaypoints(sourceBounds, bounds);
1426
+ const edge = { id: edgeId, bpmnElement: assocId, waypoints, unknownAttributes: {} };
1427
+ const d = annotResult.defs;
1428
+ const process = d.processes[0];
1429
+ const diagram = d.diagrams[0];
1430
+ if (!process || !diagram)
1431
+ return { defs: d, annotationId, associationId: assocId };
1432
+ return {
1433
+ defs: {
1434
+ ...d,
1435
+ processes: [
1436
+ { ...process, associations: [...process.associations, assoc] },
1437
+ ...d.processes.slice(1),
1438
+ ],
1439
+ diagrams: [
1440
+ {
1441
+ ...diagram,
1442
+ plane: { ...diagram.plane, edges: [...diagram.plane.edges, edge] },
1443
+ },
1444
+ ...d.diagrams.slice(1),
1445
+ ],
1446
+ },
1447
+ annotationId,
1448
+ associationId: assocId,
1449
+ };
1450
+ }
1451
+ // ── Update shape color ────────────────────────────────────────────────────────
1452
+ export function updateShapeColor(defs, id, color) {
1453
+ const diagram = defs.diagrams[0];
1454
+ if (!diagram)
1455
+ return defs;
1456
+ const newShapes = diagram.plane.shapes.map((s) => s.bpmnElement === id
1457
+ ? { ...s, unknownAttributes: writeDiColor(s.unknownAttributes, color) }
1458
+ : s);
1459
+ // Add color namespaces when any color is set
1460
+ const needsNs = !!(color.fill ?? color.stroke);
1461
+ const newNamespaces = needsNs
1462
+ ? { ...defs.namespaces, bioc: BIOC_NS, color: COLOR_NS }
1463
+ : defs.namespaces;
1464
+ return {
1465
+ ...defs,
1466
+ namespaces: newNamespaces,
1467
+ diagrams: [
1468
+ { ...diagram, plane: { ...diagram.plane, shapes: newShapes } },
1469
+ ...defs.diagrams.slice(1),
1470
+ ],
1471
+ };
1472
+ }
1473
+ //# sourceMappingURL=modeling.js.map