@bpmnkit/core 0.0.25 → 0.0.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bpmn/auto-layout.js +32 -13
- package/dist/bpmn/bpmn-builder.d.ts +48 -8
- package/dist/bpmn/bpmn-builder.js +193 -36
- package/dist/bpmn/bpmn-model.d.ts +2 -0
- package/dist/bpmn/bpmn-parser.js +9 -2
- package/dist/bpmn/bpmn-serializer.js +6 -2
- package/dist/layout/layout-engine.js +127 -5
- package/dist/layout/subprocess.js +38 -0
- package/dist/layout/types.d.ts +2 -2
- package/dist/layout/types.js +2 -2
- package/package.json +1 -1
package/dist/bpmn/auto-layout.js
CHANGED
|
@@ -201,19 +201,38 @@ function buildLaneShapes(lanes, nodes, dx, dy, poolY, poolHeaderWidth, laneConte
|
|
|
201
201
|
const mB = accB && accB.count > 0 ? accB.sum / accB.count : Number.POSITIVE_INFINITY;
|
|
202
202
|
return mA - mB;
|
|
203
203
|
});
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
204
|
+
// Compute proportional weight per lane: node count × row height, minimum 1 row
|
|
205
|
+
const MIN_LANE_H = 80;
|
|
206
|
+
const weights = sortedLanes.map((lane) => {
|
|
207
|
+
const count = nodes.filter((n) => elemToLane.get(n.id) === lane.id).length;
|
|
208
|
+
return Math.max(count, 1);
|
|
209
|
+
});
|
|
210
|
+
const totalWeight = weights.reduce((a, b) => a + b, 0);
|
|
211
|
+
// Scale proportionally to poolHeight so all lanes fill the pool exactly
|
|
212
|
+
const scaledHeights = weights.map((w) => Math.round((w / totalWeight) * poolHeight));
|
|
213
|
+
// Fix last lane for rounding drift
|
|
214
|
+
if (scaledHeights.length > 0) {
|
|
215
|
+
scaledHeights[scaledHeights.length - 1] =
|
|
216
|
+
poolHeight - scaledHeights.slice(0, -1).reduce((a, b) => a + b, 0);
|
|
217
|
+
}
|
|
218
|
+
let cumulativeY = 0;
|
|
219
|
+
return sortedLanes.map((lane, i) => {
|
|
220
|
+
const laneH = scaledHeights[i] ?? MIN_LANE_H;
|
|
221
|
+
const shape = {
|
|
222
|
+
id: `${lane.id}_di`,
|
|
223
|
+
bpmnElement: lane.id,
|
|
224
|
+
isHorizontal: true,
|
|
225
|
+
bounds: {
|
|
226
|
+
x: Math.round(poolHeaderWidth),
|
|
227
|
+
y: Math.round(poolY + cumulativeY),
|
|
228
|
+
width: Math.round(laneContentWidth),
|
|
229
|
+
height: Math.round(laneH),
|
|
230
|
+
},
|
|
231
|
+
unknownAttributes: {},
|
|
232
|
+
};
|
|
233
|
+
cumulativeY += laneH;
|
|
234
|
+
return shape;
|
|
235
|
+
});
|
|
217
236
|
}
|
|
218
237
|
function addAnnotationShapes(process, layoutNodes, annLocalBounds, allShapes, allEdges, dx, dy) {
|
|
219
238
|
if (process.textAnnotations.length === 0 && process.associations.length === 0)
|
|
@@ -5,6 +5,11 @@ export interface ElementOptions {
|
|
|
5
5
|
name?: string;
|
|
6
6
|
isForCompensation?: boolean;
|
|
7
7
|
}
|
|
8
|
+
/** Options for send/receive task elements. */
|
|
9
|
+
export interface MessageTaskOptions extends ElementOptions {
|
|
10
|
+
/** Message name — generates or reuses a root <bpmn:message> and sets messageRef. */
|
|
11
|
+
messageName?: string;
|
|
12
|
+
}
|
|
8
13
|
/** Options for creating a start event. */
|
|
9
14
|
export interface StartEventOptions extends ElementOptions {
|
|
10
15
|
/** Timer duration (ISO 8601) — creates a timer start event. */
|
|
@@ -252,6 +257,10 @@ export declare class BranchBuilder {
|
|
|
252
257
|
readonly _textAnnotations: BpmnTextAnnotation[];
|
|
253
258
|
/** @internal */
|
|
254
259
|
readonly _associations: BpmnAssociation[];
|
|
260
|
+
/** @internal – ID of the last gateway added in this branch (for nested branch() support). */
|
|
261
|
+
private currentGatewayId;
|
|
262
|
+
/** @internal – Open ends of nested branches waiting to auto-connect to the next element. */
|
|
263
|
+
private openBranchEnds;
|
|
255
264
|
private readonly _annCounters;
|
|
256
265
|
private readonly rootErrors;
|
|
257
266
|
private readonly rootMessages;
|
|
@@ -269,8 +278,10 @@ export declare class BranchBuilder {
|
|
|
269
278
|
* Supports forward references (element created later) and backward references (loops).
|
|
270
279
|
*/
|
|
271
280
|
connectTo(targetId: string): this;
|
|
272
|
-
/** @internal – ID of the last element added (or
|
|
273
|
-
get _lastNodeId(): string;
|
|
281
|
+
/** @internal – ID of the last element added (or undefined if branches are open). */
|
|
282
|
+
get _lastNodeId(): string | undefined;
|
|
283
|
+
/** @internal – Open ends of nested branches that have not yet been connected. */
|
|
284
|
+
get _openBranchEnds(): string[];
|
|
274
285
|
/** Attach a text annotation to the element at the current cursor position. */
|
|
275
286
|
textAnnotation(text: string): this;
|
|
276
287
|
/** Attach a text annotation to an element by explicit ID. */
|
|
@@ -278,8 +289,8 @@ export declare class BranchBuilder {
|
|
|
278
289
|
serviceTask(id: string, options: ServiceTaskOptions): this;
|
|
279
290
|
userTask(id: string, options?: UserTaskOptions): this;
|
|
280
291
|
scriptTask(id: string, options: ScriptTaskOptions): this;
|
|
281
|
-
sendTask(id: string, options?:
|
|
282
|
-
receiveTask(id: string, options?:
|
|
292
|
+
sendTask(id: string, options?: MessageTaskOptions): this;
|
|
293
|
+
receiveTask(id: string, options?: MessageTaskOptions): this;
|
|
283
294
|
businessRuleTask(id: string, options?: BusinessRuleTaskOptions): this;
|
|
284
295
|
callActivity(id: string, options: CallActivityOptions): this;
|
|
285
296
|
/** Add an abstract task with no Zeebe extensions. */
|
|
@@ -292,6 +303,32 @@ export declare class BranchBuilder {
|
|
|
292
303
|
parallelGateway(id: string, options?: ElementOptions): this;
|
|
293
304
|
inclusiveGateway(id: string, options?: GatewayOptions): this;
|
|
294
305
|
eventBasedGateway(id: string, options?: ElementOptions): this;
|
|
306
|
+
/**
|
|
307
|
+
* Add a boundary event attached to an existing activity in this branch.
|
|
308
|
+
*
|
|
309
|
+
* The boundary event is NOT connected by a sequence flow — it attaches via
|
|
310
|
+
* `attachedToRef`. The builder cursor advances to the boundary event so
|
|
311
|
+
* subsequent elements chain from it. Use `withBoundary()` if you want the
|
|
312
|
+
* cursor to return to the task afterward.
|
|
313
|
+
*/
|
|
314
|
+
boundaryEvent(id: string, options: BoundaryEventOptions): this;
|
|
315
|
+
/**
|
|
316
|
+
* Attach a boundary event to the preceding task and build its outgoing path,
|
|
317
|
+
* then restore the branch cursor to the preceding task so the main branch flow continues.
|
|
318
|
+
*
|
|
319
|
+
* @param id - ID for the boundary event element.
|
|
320
|
+
* @param options - Boundary event options (without `attachedTo` — inferred from cursor).
|
|
321
|
+
* @param handler - Callback that chains elements from the boundary event.
|
|
322
|
+
*/
|
|
323
|
+
withBoundary(id: string, options: Omit<BoundaryEventOptions, "attachedTo">, handler: (b: BranchBuilder) => void): this;
|
|
324
|
+
/**
|
|
325
|
+
* Create a named branch from the last gateway added inside this branch.
|
|
326
|
+
*
|
|
327
|
+
* Works identically to the top-level `ProcessBuilder.branch()` — use
|
|
328
|
+
* `.condition(expr)` or `.defaultFlow()` inside the callback, finish with
|
|
329
|
+
* `.connectTo(id)` or an `.endEvent()` to terminate the nested branch.
|
|
330
|
+
*/
|
|
331
|
+
branch(name: string, callback: (b: BranchBuilder) => void): this;
|
|
295
332
|
}
|
|
296
333
|
/** Builder for the contents of a sub-process or ad-hoc sub-process. */
|
|
297
334
|
export declare class SubProcessContentBuilder {
|
|
@@ -307,6 +344,9 @@ export declare class SubProcessContentBuilder {
|
|
|
307
344
|
private lastNodeId;
|
|
308
345
|
private currentGatewayId;
|
|
309
346
|
private openBranchEnds;
|
|
347
|
+
private readonly rootMessages;
|
|
348
|
+
/** @internal */
|
|
349
|
+
constructor(rootMessages?: BpmnMessage[]);
|
|
310
350
|
private addElement;
|
|
311
351
|
startEvent(id?: string, options?: StartEventOptions): this;
|
|
312
352
|
endEvent(id?: string, options?: EndEventOptions): this;
|
|
@@ -317,8 +357,8 @@ export declare class SubProcessContentBuilder {
|
|
|
317
357
|
userTask(id: string, options?: UserTaskOptions): this;
|
|
318
358
|
businessRuleTask(id: string, options?: BusinessRuleTaskOptions): this;
|
|
319
359
|
callActivity(id: string, options: CallActivityOptions): this;
|
|
320
|
-
sendTask(id: string, options?:
|
|
321
|
-
receiveTask(id: string, options?:
|
|
360
|
+
sendTask(id: string, options?: MessageTaskOptions): this;
|
|
361
|
+
receiveTask(id: string, options?: MessageTaskOptions): this;
|
|
322
362
|
/** Add an abstract task with no Zeebe extensions. */
|
|
323
363
|
task(id: string, options?: ElementOptions): this;
|
|
324
364
|
exclusiveGateway(id: string, options?: GatewayOptions): this;
|
|
@@ -419,9 +459,9 @@ export declare class ProcessBuilder {
|
|
|
419
459
|
/** Add a user task with optional form reference. */
|
|
420
460
|
userTask(id: string, options?: UserTaskOptions): this;
|
|
421
461
|
/** Add a send task (aspirational). */
|
|
422
|
-
sendTask(id: string, options?:
|
|
462
|
+
sendTask(id: string, options?: MessageTaskOptions): this;
|
|
423
463
|
/** Add a receive task (aspirational). */
|
|
424
|
-
receiveTask(id: string, options?:
|
|
464
|
+
receiveTask(id: string, options?: MessageTaskOptions): this;
|
|
425
465
|
/** Add a business rule task. */
|
|
426
466
|
businessRuleTask(id: string, options?: BusinessRuleTaskOptions): this;
|
|
427
467
|
/** Add a call activity referencing another process. */
|
|
@@ -7,6 +7,14 @@ const EXPORTER_VERSION = "0.0.23";
|
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
8
|
// Internal helpers
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
|
+
function resolveMessage(messageName, rootMessages) {
|
|
11
|
+
let existing = rootMessages.find((m) => m.name === messageName);
|
|
12
|
+
if (!existing) {
|
|
13
|
+
existing = { id: generateId("Message"), name: messageName, unknownAttributes: {} };
|
|
14
|
+
rootMessages.push(existing);
|
|
15
|
+
}
|
|
16
|
+
return existing.id;
|
|
17
|
+
}
|
|
10
18
|
function buildEventDefinitions(opts, rootErrors, rootMessages, rootSignals, rootEscalations) {
|
|
11
19
|
const defs = [];
|
|
12
20
|
if (opts.timerDuration || opts.timerDate || opts.timerCycle) {
|
|
@@ -34,15 +42,9 @@ function buildEventDefinitions(opts, rootErrors, rootMessages, rootSignals, root
|
|
|
34
42
|
defs.push({ type: "error", errorRef });
|
|
35
43
|
}
|
|
36
44
|
if (opts.messageName !== undefined) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (!existing) {
|
|
41
|
-
existing = { id: generateId("Message"), name: opts.messageName, unknownAttributes: {} };
|
|
42
|
-
rootMessages.push(existing);
|
|
43
|
-
}
|
|
44
|
-
messageRef = existing.id;
|
|
45
|
-
}
|
|
45
|
+
const messageRef = rootMessages
|
|
46
|
+
? resolveMessage(opts.messageName, rootMessages)
|
|
47
|
+
: opts.messageName;
|
|
46
48
|
defs.push({ type: "message", messageRef });
|
|
47
49
|
}
|
|
48
50
|
if (opts.signalName !== undefined) {
|
|
@@ -454,6 +456,10 @@ export class BranchBuilder {
|
|
|
454
456
|
_textAnnotations = [];
|
|
455
457
|
/** @internal */
|
|
456
458
|
_associations = [];
|
|
459
|
+
/** @internal – ID of the last gateway added in this branch (for nested branch() support). */
|
|
460
|
+
currentGatewayId;
|
|
461
|
+
/** @internal – Open ends of nested branches waiting to auto-connect to the next element. */
|
|
462
|
+
openBranchEnds = [];
|
|
457
463
|
_annCounters = new Map();
|
|
458
464
|
rootErrors;
|
|
459
465
|
rootMessages;
|
|
@@ -481,22 +487,34 @@ export class BranchBuilder {
|
|
|
481
487
|
}
|
|
482
488
|
addElement(element) {
|
|
483
489
|
this._elements.push(element);
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
this.
|
|
490
|
+
if (this.lastNodeId) {
|
|
491
|
+
const flowId = generateId("Flow");
|
|
492
|
+
const flow = {
|
|
493
|
+
id: flowId,
|
|
494
|
+
sourceRef: this.lastNodeId,
|
|
495
|
+
targetRef: element.id,
|
|
496
|
+
name: this.isFirstElement ? this.branchName : undefined,
|
|
497
|
+
conditionExpression: this.isFirstElement && this.pendingCondition
|
|
498
|
+
? makeConditionExpression(this.pendingCondition)
|
|
499
|
+
: undefined,
|
|
500
|
+
extensionElements: [],
|
|
501
|
+
unknownAttributes: {},
|
|
502
|
+
};
|
|
503
|
+
this._flows.push(flow);
|
|
504
|
+
if (this.isFirstElement && this.pendingDefault) {
|
|
505
|
+
this._defaultFlowId = flowId;
|
|
506
|
+
}
|
|
499
507
|
}
|
|
508
|
+
for (const branchEnd of this.openBranchEnds) {
|
|
509
|
+
this._flows.push({
|
|
510
|
+
id: generateId("Flow"),
|
|
511
|
+
sourceRef: branchEnd,
|
|
512
|
+
targetRef: element.id,
|
|
513
|
+
extensionElements: [],
|
|
514
|
+
unknownAttributes: {},
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
this.openBranchEnds = [];
|
|
500
518
|
this.isFirstElement = false;
|
|
501
519
|
this.lastNodeId = element.id;
|
|
502
520
|
return this;
|
|
@@ -509,6 +527,7 @@ export class BranchBuilder {
|
|
|
509
527
|
const flowId = generateId("Flow");
|
|
510
528
|
const flow = {
|
|
511
529
|
id: flowId,
|
|
530
|
+
// biome-ignore lint/style/noNonNullAssertion: lastNodeId starts as gatewayId and is always defined in pre-branch context
|
|
512
531
|
sourceRef: this.lastNodeId,
|
|
513
532
|
targetRef: targetId,
|
|
514
533
|
name: this.isFirstElement ? this.branchName : undefined,
|
|
@@ -527,13 +546,18 @@ export class BranchBuilder {
|
|
|
527
546
|
this._connected = true;
|
|
528
547
|
return this;
|
|
529
548
|
}
|
|
530
|
-
/** @internal – ID of the last element added (or
|
|
549
|
+
/** @internal – ID of the last element added (or undefined if branches are open). */
|
|
531
550
|
get _lastNodeId() {
|
|
532
551
|
return this.lastNodeId;
|
|
533
552
|
}
|
|
553
|
+
/** @internal – Open ends of nested branches that have not yet been connected. */
|
|
554
|
+
get _openBranchEnds() {
|
|
555
|
+
return this.openBranchEnds;
|
|
556
|
+
}
|
|
534
557
|
// ---- Annotations ----
|
|
535
558
|
/** Attach a text annotation to the element at the current cursor position. */
|
|
536
559
|
textAnnotation(text) {
|
|
560
|
+
// biome-ignore lint/style/noNonNullAssertion: lastNodeId starts as gatewayId and is always defined in pre-branch context
|
|
537
561
|
return this.annotate(this.lastNodeId, text);
|
|
538
562
|
}
|
|
539
563
|
/** Attach a text annotation to an element by explicit ID. */
|
|
@@ -565,12 +589,16 @@ export class BranchBuilder {
|
|
|
565
589
|
const el = makeFlowElement(id, "sendTask", options);
|
|
566
590
|
if (options?.isForCompensation)
|
|
567
591
|
el.isForCompensation = true;
|
|
592
|
+
if (options?.messageName)
|
|
593
|
+
el.messageRef = resolveMessage(options.messageName, this.rootMessages);
|
|
568
594
|
return this.addElement(el);
|
|
569
595
|
}
|
|
570
596
|
receiveTask(id, options) {
|
|
571
597
|
const el = makeFlowElement(id, "receiveTask", options);
|
|
572
598
|
if (options?.isForCompensation)
|
|
573
599
|
el.isForCompensation = true;
|
|
600
|
+
if (options?.messageName)
|
|
601
|
+
el.messageRef = resolveMessage(options.messageName, this.rootMessages);
|
|
574
602
|
return this.addElement(el);
|
|
575
603
|
}
|
|
576
604
|
businessRuleTask(id, options) {
|
|
@@ -615,17 +643,121 @@ export class BranchBuilder {
|
|
|
615
643
|
return this.addElement(el);
|
|
616
644
|
}
|
|
617
645
|
exclusiveGateway(id, options) {
|
|
646
|
+
this.currentGatewayId = id;
|
|
618
647
|
return this.addElement(makeExclusiveGatewayEl(id, options));
|
|
619
648
|
}
|
|
620
649
|
parallelGateway(id, options) {
|
|
650
|
+
this.currentGatewayId = id;
|
|
621
651
|
return this.addElement(makeFlowElement(id, "parallelGateway", options));
|
|
622
652
|
}
|
|
623
653
|
inclusiveGateway(id, options) {
|
|
654
|
+
this.currentGatewayId = id;
|
|
624
655
|
return this.addElement(makeInclusiveGatewayEl(id, options));
|
|
625
656
|
}
|
|
626
657
|
eventBasedGateway(id, options) {
|
|
658
|
+
this.currentGatewayId = id;
|
|
627
659
|
return this.addElement(makeFlowElement(id, "eventBasedGateway", options));
|
|
628
660
|
}
|
|
661
|
+
/**
|
|
662
|
+
* Add a boundary event attached to an existing activity in this branch.
|
|
663
|
+
*
|
|
664
|
+
* The boundary event is NOT connected by a sequence flow — it attaches via
|
|
665
|
+
* `attachedToRef`. The builder cursor advances to the boundary event so
|
|
666
|
+
* subsequent elements chain from it. Use `withBoundary()` if you want the
|
|
667
|
+
* cursor to return to the task afterward.
|
|
668
|
+
*/
|
|
669
|
+
boundaryEvent(id, options) {
|
|
670
|
+
const element = makeFlowElement(id, "boundaryEvent", options);
|
|
671
|
+
if (element.type === "boundaryEvent") {
|
|
672
|
+
element.attachedToRef = options.attachedTo;
|
|
673
|
+
element.cancelActivity = options.cancelActivity;
|
|
674
|
+
element.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
|
|
675
|
+
}
|
|
676
|
+
// Push directly — no sequence flow, boundary events attach via attachedToRef
|
|
677
|
+
this._elements.push(element);
|
|
678
|
+
this.lastNodeId = element.id;
|
|
679
|
+
this.isFirstElement = false;
|
|
680
|
+
return this;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Attach a boundary event to the preceding task and build its outgoing path,
|
|
684
|
+
* then restore the branch cursor to the preceding task so the main branch flow continues.
|
|
685
|
+
*
|
|
686
|
+
* @param id - ID for the boundary event element.
|
|
687
|
+
* @param options - Boundary event options (without `attachedTo` — inferred from cursor).
|
|
688
|
+
* @param handler - Callback that chains elements from the boundary event.
|
|
689
|
+
*/
|
|
690
|
+
withBoundary(id, options, handler) {
|
|
691
|
+
const attachedTo = this.lastNodeId;
|
|
692
|
+
if (!attachedTo || attachedTo === this.gatewayId) {
|
|
693
|
+
throw new Error("withBoundary() must follow a task element inside the branch. Current builder position has no active task.");
|
|
694
|
+
}
|
|
695
|
+
const attachedEl = this._elements.find((n) => n.id === attachedTo);
|
|
696
|
+
if (attachedEl?.type === "boundaryEvent") {
|
|
697
|
+
throw new Error("withBoundary() cannot attach to a boundary event. It must follow a task or activity element.");
|
|
698
|
+
}
|
|
699
|
+
const savedLast = this.lastNodeId;
|
|
700
|
+
const savedGateway = this.currentGatewayId;
|
|
701
|
+
const savedConnected = this._connected;
|
|
702
|
+
const savedOpenEnds = [...this.openBranchEnds];
|
|
703
|
+
this.openBranchEnds = [];
|
|
704
|
+
// Create and push the boundary event (no sequence flow)
|
|
705
|
+
this.boundaryEvent(id, { ...options, attachedTo });
|
|
706
|
+
// Build the boundary event's outgoing path
|
|
707
|
+
handler(this);
|
|
708
|
+
// Restore cursor to the task so the branch main flow continues
|
|
709
|
+
this.lastNodeId = savedLast;
|
|
710
|
+
this.currentGatewayId = savedGateway;
|
|
711
|
+
this._connected = savedConnected;
|
|
712
|
+
this.openBranchEnds = savedOpenEnds;
|
|
713
|
+
return this;
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Create a named branch from the last gateway added inside this branch.
|
|
717
|
+
*
|
|
718
|
+
* Works identically to the top-level `ProcessBuilder.branch()` — use
|
|
719
|
+
* `.condition(expr)` or `.defaultFlow()` inside the callback, finish with
|
|
720
|
+
* `.connectTo(id)` or an `.endEvent()` to terminate the nested branch.
|
|
721
|
+
*/
|
|
722
|
+
branch(name, callback) {
|
|
723
|
+
if (!this.currentGatewayId) {
|
|
724
|
+
throw new Error("branch() must be called after a gateway element");
|
|
725
|
+
}
|
|
726
|
+
const b = new BranchBuilder(this.currentGatewayId, name, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
|
|
727
|
+
callback(b);
|
|
728
|
+
for (const el of b._elements) {
|
|
729
|
+
if (this._elements.some((n) => n.id === el.id)) {
|
|
730
|
+
throw new Error(`Duplicate element ID "${el.id}"`);
|
|
731
|
+
}
|
|
732
|
+
this._elements.push(el);
|
|
733
|
+
}
|
|
734
|
+
for (const fl of b._flows)
|
|
735
|
+
this._flows.push(fl);
|
|
736
|
+
for (const ann of b._textAnnotations)
|
|
737
|
+
this._textAnnotations.push(ann);
|
|
738
|
+
for (const assoc of b._associations)
|
|
739
|
+
this._associations.push(assoc);
|
|
740
|
+
if (b._defaultFlowId) {
|
|
741
|
+
const gw = this._elements.find((n) => n.id === this.currentGatewayId);
|
|
742
|
+
if (gw && (gw.type === "exclusiveGateway" || gw.type === "inclusiveGateway")) {
|
|
743
|
+
gw.default = b._defaultFlowId;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
if (!b._connected) {
|
|
747
|
+
const allEnds = [
|
|
748
|
+
...(b._lastNodeId !== undefined ? [b._lastNodeId] : []),
|
|
749
|
+
...b._openBranchEnds,
|
|
750
|
+
];
|
|
751
|
+
for (const endId of allEnds) {
|
|
752
|
+
const endEl = this._elements.find((n) => n.id === endId);
|
|
753
|
+
if (endEl && endEl.type !== "endEvent") {
|
|
754
|
+
this.openBranchEnds.push(endId);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
this.lastNodeId = undefined;
|
|
759
|
+
return this;
|
|
760
|
+
}
|
|
629
761
|
}
|
|
630
762
|
// ---------------------------------------------------------------------------
|
|
631
763
|
// Sub-process content builder
|
|
@@ -644,6 +776,11 @@ export class SubProcessContentBuilder {
|
|
|
644
776
|
lastNodeId;
|
|
645
777
|
currentGatewayId;
|
|
646
778
|
openBranchEnds = [];
|
|
779
|
+
rootMessages;
|
|
780
|
+
/** @internal */
|
|
781
|
+
constructor(rootMessages = []) {
|
|
782
|
+
this.rootMessages = rootMessages;
|
|
783
|
+
}
|
|
647
784
|
addElement(element) {
|
|
648
785
|
if (this._elements.some((n) => n.id === element.id)) {
|
|
649
786
|
throw new Error(`Duplicate element ID "${element.id}" in sub-process`);
|
|
@@ -719,12 +856,16 @@ export class SubProcessContentBuilder {
|
|
|
719
856
|
const el = makeFlowElement(id, "sendTask", options);
|
|
720
857
|
if (options?.isForCompensation)
|
|
721
858
|
el.isForCompensation = true;
|
|
859
|
+
if (options?.messageName)
|
|
860
|
+
el.messageRef = resolveMessage(options.messageName, this.rootMessages);
|
|
722
861
|
return this.addElement(el);
|
|
723
862
|
}
|
|
724
863
|
receiveTask(id, options) {
|
|
725
864
|
const el = makeFlowElement(id, "receiveTask", options);
|
|
726
865
|
if (options?.isForCompensation)
|
|
727
866
|
el.isForCompensation = true;
|
|
867
|
+
if (options?.messageName)
|
|
868
|
+
el.messageRef = resolveMessage(options.messageName, this.rootMessages);
|
|
728
869
|
return this.addElement(el);
|
|
729
870
|
}
|
|
730
871
|
/** Add an abstract task with no Zeebe extensions. */
|
|
@@ -799,10 +940,16 @@ export class SubProcessContentBuilder {
|
|
|
799
940
|
gw.default = b._defaultFlowId;
|
|
800
941
|
}
|
|
801
942
|
}
|
|
802
|
-
if (!b._connected
|
|
803
|
-
const
|
|
804
|
-
|
|
805
|
-
|
|
943
|
+
if (!b._connected) {
|
|
944
|
+
const allEnds = [
|
|
945
|
+
...(b._lastNodeId !== undefined ? [b._lastNodeId] : []),
|
|
946
|
+
...b._openBranchEnds,
|
|
947
|
+
];
|
|
948
|
+
for (const endId of allEnds) {
|
|
949
|
+
const endEl = this._elements.find((n) => n.id === endId);
|
|
950
|
+
if (endEl && endEl.type !== "endEvent") {
|
|
951
|
+
this.openBranchEnds.push(endId);
|
|
952
|
+
}
|
|
806
953
|
}
|
|
807
954
|
}
|
|
808
955
|
this.lastNodeId = undefined;
|
|
@@ -1075,6 +1222,8 @@ export class ProcessBuilder {
|
|
|
1075
1222
|
const el = makeFlowElement(id, "sendTask", options);
|
|
1076
1223
|
if (options?.isForCompensation)
|
|
1077
1224
|
el.isForCompensation = true;
|
|
1225
|
+
if (options?.messageName)
|
|
1226
|
+
el.messageRef = resolveMessage(options.messageName, this.rootMessages);
|
|
1078
1227
|
this.addFlowElement(el);
|
|
1079
1228
|
return this;
|
|
1080
1229
|
}
|
|
@@ -1083,6 +1232,8 @@ export class ProcessBuilder {
|
|
|
1083
1232
|
const el = makeFlowElement(id, "receiveTask", options);
|
|
1084
1233
|
if (options?.isForCompensation)
|
|
1085
1234
|
el.isForCompensation = true;
|
|
1235
|
+
if (options?.messageName)
|
|
1236
|
+
el.messageRef = resolveMessage(options.messageName, this.rootMessages);
|
|
1086
1237
|
this.addFlowElement(el);
|
|
1087
1238
|
return this;
|
|
1088
1239
|
}
|
|
@@ -1175,10 +1326,16 @@ export class ProcessBuilder {
|
|
|
1175
1326
|
}
|
|
1176
1327
|
// Track the branch's open end so the next element auto-connects from it.
|
|
1177
1328
|
// Skip branches that terminated at an end event (those are intentional dead-ends).
|
|
1178
|
-
if (!b._connected
|
|
1179
|
-
const
|
|
1180
|
-
|
|
1181
|
-
|
|
1329
|
+
if (!b._connected) {
|
|
1330
|
+
const allEnds = [
|
|
1331
|
+
...(b._lastNodeId !== undefined ? [b._lastNodeId] : []),
|
|
1332
|
+
...b._openBranchEnds,
|
|
1333
|
+
];
|
|
1334
|
+
for (const endId of allEnds) {
|
|
1335
|
+
const endEl = this.flowElements.find((n) => n.id === endId);
|
|
1336
|
+
if (endEl && endEl.type !== "endEvent") {
|
|
1337
|
+
this.openBranchEnds.push(endId);
|
|
1338
|
+
}
|
|
1182
1339
|
}
|
|
1183
1340
|
}
|
|
1184
1341
|
this.lastNodeId = undefined;
|
|
@@ -1217,7 +1374,7 @@ export class ProcessBuilder {
|
|
|
1217
1374
|
// ---- Sub-processes ----
|
|
1218
1375
|
/** Add an ad-hoc sub-process with optional AI agent or multi-instance configuration. */
|
|
1219
1376
|
adHocSubProcess(id, content, options) {
|
|
1220
|
-
const sub = new SubProcessContentBuilder();
|
|
1377
|
+
const sub = new SubProcessContentBuilder(this.rootMessages);
|
|
1221
1378
|
content(sub);
|
|
1222
1379
|
insertJoinGateways(sub._elements, sub._flows);
|
|
1223
1380
|
recomputeIncomingOutgoing(sub._elements, sub._flows);
|
|
@@ -1285,7 +1442,7 @@ export class ProcessBuilder {
|
|
|
1285
1442
|
}
|
|
1286
1443
|
/** Add a sub-process (aspirational). */
|
|
1287
1444
|
subProcess(id, content, options) {
|
|
1288
|
-
const sub = new SubProcessContentBuilder();
|
|
1445
|
+
const sub = new SubProcessContentBuilder(this.rootMessages);
|
|
1289
1446
|
content(sub);
|
|
1290
1447
|
insertJoinGateways(sub._elements, sub._flows);
|
|
1291
1448
|
recomputeIncomingOutgoing(sub._elements, sub._flows);
|
|
@@ -1304,7 +1461,7 @@ export class ProcessBuilder {
|
|
|
1304
1461
|
}
|
|
1305
1462
|
/** Add an event sub-process. Triggered by its start event — no incoming or outgoing sequence flows. */
|
|
1306
1463
|
eventSubProcess(id, content, options) {
|
|
1307
|
-
const sub = new SubProcessContentBuilder();
|
|
1464
|
+
const sub = new SubProcessContentBuilder(this.rootMessages);
|
|
1308
1465
|
content(sub);
|
|
1309
1466
|
insertJoinGateways(sub._elements, sub._flows);
|
|
1310
1467
|
recomputeIncomingOutgoing(sub._elements, sub._flows);
|
|
@@ -152,10 +152,12 @@ export interface BpmnCallActivity extends BpmnFlowNodeBase {
|
|
|
152
152
|
}
|
|
153
153
|
export interface BpmnSendTask extends BpmnFlowNodeBase {
|
|
154
154
|
type: "sendTask";
|
|
155
|
+
messageRef?: string;
|
|
155
156
|
loopCharacteristics?: BpmnMultiInstanceLoopCharacteristics;
|
|
156
157
|
}
|
|
157
158
|
export interface BpmnReceiveTask extends BpmnFlowNodeBase {
|
|
158
159
|
type: "receiveTask";
|
|
160
|
+
messageRef?: string;
|
|
159
161
|
loopCharacteristics?: BpmnMultiInstanceLoopCharacteristics;
|
|
160
162
|
}
|
|
161
163
|
export interface BpmnAdHocSubProcess extends BpmnFlowNodeBase {
|
package/dist/bpmn/bpmn-parser.js
CHANGED
|
@@ -303,8 +303,6 @@ function parseFlowElement(element) {
|
|
|
303
303
|
case "serviceTask":
|
|
304
304
|
case "scriptTask":
|
|
305
305
|
case "userTask":
|
|
306
|
-
case "sendTask":
|
|
307
|
-
case "receiveTask":
|
|
308
306
|
case "businessRuleTask":
|
|
309
307
|
case "manualTask":
|
|
310
308
|
case "callActivity":
|
|
@@ -314,6 +312,15 @@ function parseFlowElement(element) {
|
|
|
314
312
|
loopCharacteristics: parseLoopCharacteristics(element),
|
|
315
313
|
isForCompensation: attr(element, "isForCompensation") === "true" ? true : undefined,
|
|
316
314
|
};
|
|
315
|
+
case "sendTask":
|
|
316
|
+
case "receiveTask":
|
|
317
|
+
return {
|
|
318
|
+
...base,
|
|
319
|
+
type: ln,
|
|
320
|
+
messageRef: attr(element, "messageRef"),
|
|
321
|
+
loopCharacteristics: parseLoopCharacteristics(element),
|
|
322
|
+
isForCompensation: attr(element, "isForCompensation") === "true" ? true : undefined,
|
|
323
|
+
};
|
|
317
324
|
case "adHocSubProcess":
|
|
318
325
|
return {
|
|
319
326
|
...base,
|
|
@@ -190,13 +190,17 @@ function serializeFlowElement(fe, ns) {
|
|
|
190
190
|
case "serviceTask":
|
|
191
191
|
case "scriptTask":
|
|
192
192
|
case "userTask":
|
|
193
|
-
case "sendTask":
|
|
194
|
-
case "receiveTask":
|
|
195
193
|
case "businessRuleTask":
|
|
196
194
|
case "manualTask":
|
|
197
195
|
case "callActivity":
|
|
198
196
|
children.push(...serializeLoopCharacteristics(fe.loopCharacteristics, bp));
|
|
199
197
|
break;
|
|
198
|
+
case "sendTask":
|
|
199
|
+
case "receiveTask":
|
|
200
|
+
if (fe.messageRef)
|
|
201
|
+
attrs.messageRef = fe.messageRef;
|
|
202
|
+
children.push(...serializeLoopCharacteristics(fe.loopCharacteristics, bp));
|
|
203
|
+
break;
|
|
200
204
|
case "adHocSubProcess":
|
|
201
205
|
children.push(...serializeLoopCharacteristics(fe.loopCharacteristics, bp));
|
|
202
206
|
children.push(...serializeProcessContents(fe, ns));
|
|
@@ -36,10 +36,21 @@ function repositionBoundaryEvents(flowElements, result) {
|
|
|
36
36
|
ps.add(edge.sourceRef);
|
|
37
37
|
predIds.set(edge.targetRef, ps);
|
|
38
38
|
}
|
|
39
|
+
const allChainNodes = new Set();
|
|
39
40
|
for (const [hostId, beIds] of boundaryMap) {
|
|
40
41
|
const hostNode = nodeById.get(hostId);
|
|
41
42
|
if (!hostNode)
|
|
42
43
|
continue;
|
|
44
|
+
// Pre-compute distribution parameters (all boundary events share the same fixed size).
|
|
45
|
+
// Distribute events evenly along the bottom edge, centered on the task.
|
|
46
|
+
// effectiveSpacing guarantees events don't overlap (min bW + 4px gap).
|
|
47
|
+
const firstBeNode = nodeById.get(beIds[0] ?? "");
|
|
48
|
+
const bW = firstBeNode?.bounds.width ?? 36;
|
|
49
|
+
const bH = firstBeNode?.bounds.height ?? 36;
|
|
50
|
+
const n = beIds.length;
|
|
51
|
+
const effectiveSpacing = Math.max(Math.round(hostNode.bounds.width / (n + 1)), bW + 4);
|
|
52
|
+
const groupWidth = Math.max(0, n - 1) * effectiveSpacing;
|
|
53
|
+
const groupStartCenterX = Math.round(hostNode.bounds.x + hostNode.bounds.width / 2 - groupWidth / 2);
|
|
43
54
|
for (let i = 0; i < beIds.length; i++) {
|
|
44
55
|
const beId = beIds[i];
|
|
45
56
|
if (!beId)
|
|
@@ -47,11 +58,9 @@ function repositionBoundaryEvents(flowElements, result) {
|
|
|
47
58
|
const beNode = nodeById.get(beId);
|
|
48
59
|
if (!beNode)
|
|
49
60
|
continue;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const rightEdge = hostNode.bounds.x + hostNode.bounds.width;
|
|
54
|
-
beNode.bounds.x = Math.round(rightEdge - bW / 2 - i * (bW + 4));
|
|
61
|
+
// bW / bH come from the pre-loop computation (all BEs are fixed 36×36)
|
|
62
|
+
// Center-bottom distribution: single event → task center; multiple → even spread
|
|
63
|
+
beNode.bounds.x = Math.round(groupStartCenterX + i * effectiveSpacing - bW / 2);
|
|
55
64
|
beNode.bounds.y = Math.round(hostNode.bounds.y + hostNode.bounds.height - bH / 2);
|
|
56
65
|
if (beNode.labelBounds) {
|
|
57
66
|
beNode.labelBounds.x = beNode.bounds.x + Math.round(bW / 2 - beNode.labelBounds.width / 2);
|
|
@@ -72,6 +81,9 @@ function repositionBoundaryEvents(flowElements, result) {
|
|
|
72
81
|
queue.push(...(succIds.get(id) ?? []));
|
|
73
82
|
}
|
|
74
83
|
}
|
|
84
|
+
// Record all chain members so the forward pass can identify them.
|
|
85
|
+
for (const cid of chainSet)
|
|
86
|
+
allChainNodes.add(cid);
|
|
75
87
|
// Each boundary event's chain gets its own vertical lane
|
|
76
88
|
let maxChainH = 0;
|
|
77
89
|
for (const id of chainOrder) {
|
|
@@ -133,6 +145,116 @@ function repositionBoundaryEvents(flowElements, result) {
|
|
|
133
145
|
}
|
|
134
146
|
}
|
|
135
147
|
}
|
|
148
|
+
// Forward-placement pass: any node not in any chain but whose predecessor
|
|
149
|
+
// has been relocated further right must be pushed rightward.
|
|
150
|
+
// Process in topological order (Kahn's algorithm over the sequenceFlow graph).
|
|
151
|
+
const inDegree = new Map();
|
|
152
|
+
for (const id of nodeById.keys()) {
|
|
153
|
+
inDegree.set(id, (predIds.get(id) ?? new Set()).size);
|
|
154
|
+
}
|
|
155
|
+
const topoQueue = [];
|
|
156
|
+
for (const [id, deg] of inDegree) {
|
|
157
|
+
if (deg === 0)
|
|
158
|
+
topoQueue.push(id);
|
|
159
|
+
}
|
|
160
|
+
const topoOrder = [];
|
|
161
|
+
while (topoQueue.length > 0) {
|
|
162
|
+
const id = topoQueue.shift();
|
|
163
|
+
if (!id)
|
|
164
|
+
break;
|
|
165
|
+
topoOrder.push(id);
|
|
166
|
+
for (const succId of succIds.get(id) ?? []) {
|
|
167
|
+
const newDeg = (inDegree.get(succId) ?? 1) - 1;
|
|
168
|
+
inDegree.set(succId, newDeg);
|
|
169
|
+
if (newDeg === 0)
|
|
170
|
+
topoQueue.push(succId);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const movedInPass = new Set();
|
|
174
|
+
for (const id of topoOrder) {
|
|
175
|
+
if (allChainNodes.has(id))
|
|
176
|
+
continue;
|
|
177
|
+
const node = nodeById.get(id);
|
|
178
|
+
if (!node)
|
|
179
|
+
continue;
|
|
180
|
+
const preds = predIds.get(id) ?? new Set();
|
|
181
|
+
if (preds.size === 0)
|
|
182
|
+
continue;
|
|
183
|
+
let maxPredRight = 0;
|
|
184
|
+
for (const predId of preds) {
|
|
185
|
+
const pred = nodeById.get(predId);
|
|
186
|
+
if (pred)
|
|
187
|
+
maxPredRight = Math.max(maxPredRight, pred.bounds.x + pred.bounds.width);
|
|
188
|
+
}
|
|
189
|
+
const minX = maxPredRight + CHAIN_GAP;
|
|
190
|
+
if (minX > node.bounds.x) {
|
|
191
|
+
const delta = minX - node.bounds.x;
|
|
192
|
+
node.bounds.x = minX;
|
|
193
|
+
if (node.labelBounds)
|
|
194
|
+
node.labelBounds.x += delta;
|
|
195
|
+
movedInPass.add(id);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Spatial bump: if a moved node now overlaps a non-chain node on a parallel
|
|
199
|
+
// path (no predecessor/successor relationship), push it clear and cascade.
|
|
200
|
+
let bumped = true;
|
|
201
|
+
while (bumped) {
|
|
202
|
+
bumped = false;
|
|
203
|
+
for (const movedId of movedInPass) {
|
|
204
|
+
const moved = nodeById.get(movedId);
|
|
205
|
+
if (!moved)
|
|
206
|
+
continue;
|
|
207
|
+
const movedRight = moved.bounds.x + moved.bounds.width;
|
|
208
|
+
for (const [otherId, other] of nodeById) {
|
|
209
|
+
if (otherId === movedId)
|
|
210
|
+
continue;
|
|
211
|
+
if (allChainNodes.has(otherId))
|
|
212
|
+
continue;
|
|
213
|
+
if (movedInPass.has(otherId))
|
|
214
|
+
continue;
|
|
215
|
+
// Check y overlap
|
|
216
|
+
if (other.bounds.y + other.bounds.height <= moved.bounds.y)
|
|
217
|
+
continue;
|
|
218
|
+
if (other.bounds.y >= moved.bounds.y + moved.bounds.height)
|
|
219
|
+
continue;
|
|
220
|
+
// Check x overlap (moved node intrudes into other's space)
|
|
221
|
+
if (other.bounds.x >= movedRight)
|
|
222
|
+
continue;
|
|
223
|
+
if (other.bounds.x + other.bounds.width <= moved.bounds.x)
|
|
224
|
+
continue;
|
|
225
|
+
// Push other right of moved
|
|
226
|
+
const newX = movedRight + CHAIN_GAP;
|
|
227
|
+
if (newX > other.bounds.x) {
|
|
228
|
+
const delta = newX - other.bounds.x;
|
|
229
|
+
other.bounds.x = newX;
|
|
230
|
+
if (other.labelBounds)
|
|
231
|
+
other.labelBounds.x += delta;
|
|
232
|
+
movedInPass.add(otherId);
|
|
233
|
+
bumped = true;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Re-route edges where a chain source now points at a moved target,
|
|
239
|
+
// or where the source itself was moved by the forward pass.
|
|
240
|
+
for (const edge of result.edges) {
|
|
241
|
+
const srcMoved = movedInPass.has(edge.sourceRef);
|
|
242
|
+
const tgtMoved = movedInPass.has(edge.targetRef);
|
|
243
|
+
if (!srcMoved && !tgtMoved)
|
|
244
|
+
continue;
|
|
245
|
+
const src = nodeById.get(edge.sourceRef);
|
|
246
|
+
const tgt = nodeById.get(edge.targetRef);
|
|
247
|
+
if (!src || !tgt)
|
|
248
|
+
continue;
|
|
249
|
+
const srcX = Math.round(src.bounds.x + src.bounds.width);
|
|
250
|
+
const srcY = Math.round(src.bounds.y + src.bounds.height / 2);
|
|
251
|
+
const tgtX = Math.round(tgt.bounds.x);
|
|
252
|
+
const tgtY = Math.round(tgt.bounds.y + tgt.bounds.height / 2);
|
|
253
|
+
edge.waypoints = [
|
|
254
|
+
{ x: srcX, y: srcY },
|
|
255
|
+
{ x: tgtX, y: tgtY },
|
|
256
|
+
];
|
|
257
|
+
}
|
|
136
258
|
}
|
|
137
259
|
/**
|
|
138
260
|
* Auto-layout a BPMN process using the Sugiyama/layered algorithm.
|
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
import { layoutFlowNodes } from "./layout-engine.js";
|
|
2
2
|
import { SUBPROCESS_PADDING } from "./types.js";
|
|
3
|
+
const ADHOC_MAX_COLS = 4;
|
|
4
|
+
const ADHOC_H_GAP = 50;
|
|
5
|
+
const ADHOC_V_GAP = 80;
|
|
6
|
+
/**
|
|
7
|
+
* Rearrange disconnected adHocSubProcess tool nodes into a grid.
|
|
8
|
+
* Nodes start at (0, 0) so the subprocess padding offset applies cleanly.
|
|
9
|
+
*/
|
|
10
|
+
function applyAdHocGridLayout(nodes) {
|
|
11
|
+
if (nodes.length === 0)
|
|
12
|
+
return;
|
|
13
|
+
const cols = Math.min(nodes.length, ADHOC_MAX_COLS);
|
|
14
|
+
const cellW = nodes.reduce((max, n) => Math.max(max, n.bounds.width), 0);
|
|
15
|
+
const cellH = nodes.reduce((max, n) => Math.max(max, n.bounds.height), 0);
|
|
16
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
17
|
+
const col = i % cols;
|
|
18
|
+
const row = Math.floor(i / cols);
|
|
19
|
+
const n = nodes[i];
|
|
20
|
+
if (!n)
|
|
21
|
+
continue;
|
|
22
|
+
const newX = col * (cellW + ADHOC_H_GAP) + Math.round((cellW - n.bounds.width) / 2);
|
|
23
|
+
const newY = row * (cellH + ADHOC_V_GAP) + Math.round((cellH - n.bounds.height) / 2);
|
|
24
|
+
const dx = newX - n.bounds.x;
|
|
25
|
+
const dy = newY - n.bounds.y;
|
|
26
|
+
n.bounds.x = newX;
|
|
27
|
+
n.bounds.y = newY;
|
|
28
|
+
if (n.labelBounds) {
|
|
29
|
+
n.labelBounds.x += dx;
|
|
30
|
+
n.labelBounds.y += dy;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
3
34
|
/**
|
|
4
35
|
* Check if a node type is a sub-process container.
|
|
5
36
|
*/
|
|
@@ -22,6 +53,13 @@ export function layoutSubProcesses(layoutNodes, nodeIndex) {
|
|
|
22
53
|
if (!subProcess.flowElements || subProcess.flowElements.length === 0)
|
|
23
54
|
continue;
|
|
24
55
|
const childResult = layoutFlowNodes(subProcess.flowElements, subProcess.sequenceFlows ?? []);
|
|
56
|
+
// For adHocSubProcess with no sequence flows, rearrange into a compact grid
|
|
57
|
+
// instead of a single long horizontal row.
|
|
58
|
+
if (bpmnNode.type === "adHocSubProcess" &&
|
|
59
|
+
(subProcess.sequenceFlows?.length ?? 0) === 0 &&
|
|
60
|
+
childResult.nodes.length > 0) {
|
|
61
|
+
applyAdHocGridLayout(childResult.nodes);
|
|
62
|
+
}
|
|
25
63
|
if (childResult.nodes.length === 0)
|
|
26
64
|
continue;
|
|
27
65
|
// Compute bounding box of child elements
|
package/dist/layout/types.d.ts
CHANGED
|
@@ -5,13 +5,13 @@ export declare const ELEMENT_SIZES: Record<string, {
|
|
|
5
5
|
height: number;
|
|
6
6
|
}>;
|
|
7
7
|
/** Virtual grid cell dimensions for element placement. */
|
|
8
|
-
export declare const GRID_CELL_WIDTH =
|
|
8
|
+
export declare const GRID_CELL_WIDTH = 150;
|
|
9
9
|
export declare const GRID_CELL_HEIGHT = 140;
|
|
10
10
|
/** Minimum spacing between elements (derived from grid). */
|
|
11
11
|
export declare const HORIZONTAL_SPACING: number;
|
|
12
12
|
export declare const VERTICAL_SPACING: number;
|
|
13
13
|
/** Padding inside sub-process containers. */
|
|
14
|
-
export declare const SUBPROCESS_PADDING =
|
|
14
|
+
export declare const SUBPROCESS_PADDING = 50;
|
|
15
15
|
/** Edge-label sizing constants (used for placement & collision detection). */
|
|
16
16
|
export declare const LABEL_CHAR_WIDTH = 7;
|
|
17
17
|
export declare const LABEL_MIN_WIDTH = 40;
|
package/dist/layout/types.js
CHANGED
|
@@ -22,13 +22,13 @@ export const ELEMENT_SIZES = {
|
|
|
22
22
|
eventSubProcess: { width: 100, height: 80 },
|
|
23
23
|
};
|
|
24
24
|
/** Virtual grid cell dimensions for element placement. */
|
|
25
|
-
export const GRID_CELL_WIDTH =
|
|
25
|
+
export const GRID_CELL_WIDTH = 150;
|
|
26
26
|
export const GRID_CELL_HEIGHT = 140;
|
|
27
27
|
/** Minimum spacing between elements (derived from grid). */
|
|
28
28
|
export const HORIZONTAL_SPACING = GRID_CELL_WIDTH - 100; // 100 = max element width
|
|
29
29
|
export const VERTICAL_SPACING = GRID_CELL_HEIGHT - 80; // 80 = max element height
|
|
30
30
|
/** Padding inside sub-process containers. */
|
|
31
|
-
export const SUBPROCESS_PADDING =
|
|
31
|
+
export const SUBPROCESS_PADDING = 50;
|
|
32
32
|
/** Edge-label sizing constants (used for placement & collision detection). */
|
|
33
33
|
export const LABEL_CHAR_WIDTH = 7;
|
|
34
34
|
export const LABEL_MIN_WIDTH = 40;
|