bpmn-auto-layout 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import BPMNModdle from 'bpmn-moddle';
2
- import { assign, map, pick } from 'min-dash';
2
+ import { assign, map, pick, isFunction } from 'min-dash';
3
3
 
4
4
  function isConnection(element) {
5
5
  return !!element.sourceRef;
@@ -14,8 +14,34 @@ class Grid {
14
14
  this.grid = [];
15
15
  }
16
16
 
17
- add(element) {
18
- this._addStart(element);
17
+ add(element, position) {
18
+ if (!position) {
19
+ this._addStart(element);
20
+ return;
21
+ }
22
+
23
+ const [ row, col ] = position;
24
+ if (!row && !col) {
25
+ this._addStart(element);
26
+ }
27
+
28
+ if (!this.grid[row]) {
29
+ this.grid[row] = [];
30
+ }
31
+
32
+ if (this.grid[row][col]) {
33
+ throw new Error('Grid is occupied please ensure the place you insert at is not occupied');
34
+ }
35
+
36
+ this.grid[row][col] = element;
37
+ }
38
+
39
+ createRow(afterIndex) {
40
+ if (!afterIndex) {
41
+ this.grid.push([]);
42
+ }
43
+
44
+ this.grid.splice(afterIndex + 1, 0, []);
19
45
  }
20
46
 
21
47
  _addStart(element) {
@@ -215,7 +241,7 @@ function is(element, type) {
215
241
  }
216
242
 
217
243
  const DEFAULT_CELL_WIDTH = 150;
218
- const DEFAULT_CELL_HEIGHT = 110;
244
+ const DEFAULT_CELL_HEIGHT = 140;
219
245
 
220
246
  function getMid(bounds) {
221
247
  return {
@@ -281,18 +307,19 @@ function connectElements(source, target, layoutGrid) {
281
307
 
282
308
  // Source === Target ==> Build loop
283
309
  if (dX === 0 && dY === 0) {
310
+ const { x, y } = coordinatesToPosition(source.gridPosition.row, source.gridPosition.col);
284
311
  return [
285
312
  getDockingPoint(sourceMid, sourceBounds, 'r', dockingSource),
286
- { x: sourceMid.x + DEFAULT_CELL_WIDTH / 2, y: sourceMid.y },
287
- { x: sourceMid.x + DEFAULT_CELL_WIDTH / 2, y: sourceMid.y - DEFAULT_CELL_HEIGHT / 2 },
288
- { x: targetMid.x, y: sourceMid.y - DEFAULT_CELL_HEIGHT / 2 },
313
+ { x: x + DEFAULT_CELL_WIDTH, y: sourceMid.y },
314
+ { x: x + DEFAULT_CELL_WIDTH, y: y },
315
+ { x: targetMid.x, y: y },
289
316
  getDockingPoint(targetMid, targetBounds, 't', dockingTarget)
290
317
  ];
291
318
  }
292
319
 
293
320
  // connect horizontally
294
321
  if (dY === 0) {
295
- if (layoutGrid.getElementsInRange(source.gridPosition, target.gridPosition).length > 2) {
322
+ if (isDirectPathBlocked(source, target, layoutGrid)) {
296
323
 
297
324
  // Route on top
298
325
  return [
@@ -313,7 +340,7 @@ function connectElements(source, target, layoutGrid) {
313
340
 
314
341
  // connect vertically
315
342
  if (dX === 0) {
316
- if (layoutGrid.getElementsInRange(source.gridPosition, target.gridPosition).length > 2) {
343
+ if (isDirectPathBlocked(source, target, layoutGrid)) {
317
344
 
318
345
  // Route parallel
319
346
  const yOffset = -Math.sign(dY) * DEFAULT_CELL_HEIGHT / 2;
@@ -333,6 +360,20 @@ function connectElements(source, target, layoutGrid) {
333
360
  }
334
361
  }
335
362
 
363
+ const directManhattan = directManhattanConnect(source, target, layoutGrid);
364
+
365
+ if (directManhattan) {
366
+ const startPoint = getDockingPoint(sourceMid, sourceBounds, directManhattan[0], dockingSource);
367
+ const endPoint = getDockingPoint(targetMid, targetBounds, directManhattan[1], dockingTarget);
368
+
369
+ const midPoint = directManhattan[0] === 'h' ? { x: endPoint.x, y: startPoint.y } : { x: startPoint.x, y: endPoint.y };
370
+
371
+ return [
372
+ startPoint,
373
+ midPoint,
374
+ endPoint
375
+ ];
376
+ }
336
377
  const yOffset = -Math.sign(dY) * DEFAULT_CELL_HEIGHT / 2;
337
378
 
338
379
  return [
@@ -346,6 +387,15 @@ function connectElements(source, target, layoutGrid) {
346
387
  }
347
388
 
348
389
  // helpers /////
390
+ function coordinatesToPosition(row, col) {
391
+ return {
392
+ width: DEFAULT_CELL_WIDTH,
393
+ height: DEFAULT_CELL_HEIGHT,
394
+ x: col * DEFAULT_CELL_WIDTH,
395
+ y: row * DEFAULT_CELL_HEIGHT
396
+ };
397
+ }
398
+
349
399
  function getBounds(element, row, col, attachedTo) {
350
400
  const { width, height } = getDefaultSize(element);
351
401
 
@@ -367,10 +417,264 @@ function getBounds(element, row, col, attachedTo) {
367
417
  };
368
418
  }
369
419
 
420
+ function isDirectPathBlocked(source, target, layoutGrid) {
421
+ const { row: sourceRow, col: sourceCol } = source.gridPosition;
422
+ const { row: targetRow, col: targetCol } = target.gridPosition;
423
+
424
+ const dX = targetCol - sourceCol;
425
+ const dY = targetRow - sourceRow;
426
+
427
+ let totalElements = 0;
428
+
429
+ if (dX) {
430
+ totalElements += layoutGrid.getElementsInRange({ row: sourceRow, col: sourceCol }, { row: sourceRow, col: targetCol }).length;
431
+ }
432
+
433
+ if (dY) {
434
+ totalElements += layoutGrid.getElementsInRange({ row: sourceRow, col: targetCol }, { row: targetRow, col: targetCol }).length;
435
+ }
436
+
437
+ return totalElements > 2;
438
+ }
439
+
440
+ function directManhattanConnect(source, target, layoutGrid) {
441
+ const { row: sourceRow, col: sourceCol } = source.gridPosition;
442
+ const { row: targetRow, col: targetCol } = target.gridPosition;
443
+
444
+ const dX = targetCol - sourceCol;
445
+ const dY = targetRow - sourceRow;
446
+
447
+ // Only directly connect left-to-right flow
448
+ if (!(dX > 0 && dY !== 0)) {
449
+ return;
450
+ }
451
+
452
+ // If below, go down then horizontal
453
+ if (dY > 0) {
454
+ let totalElements = 0;
455
+ const bendPoint = { row: targetRow, col: sourceCol };
456
+ totalElements += layoutGrid.getElementsInRange({ row: sourceRow, col: sourceCol }, bendPoint).length;
457
+ totalElements += layoutGrid.getElementsInRange(bendPoint, { row: targetRow, col: targetCol }).length;
458
+
459
+ return totalElements > 2 ? false : [ 'v', 'h' ];
460
+ } else {
461
+
462
+ // If above, go horizontal than vertical
463
+ let totalElements = 0;
464
+ const bendPoint = { row: sourceRow, col: targetCol };
465
+
466
+ totalElements += layoutGrid.getElementsInRange({ row: sourceRow, col: sourceCol }, bendPoint).length;
467
+ totalElements += layoutGrid.getElementsInRange(bendPoint, { row: targetRow, col: targetCol }).length;
468
+
469
+ return totalElements > 2 ? false : [ 'h', 'v' ];
470
+ }
471
+ }
472
+
473
+ var attacherHandler = {
474
+ 'addToGrid': ({ element, grid, visited }) => {
475
+ const nextElements = [];
476
+
477
+ const attachedOutgoing = (element.attachers || [])
478
+ .map(att => att.outgoing.reverse())
479
+ .flat()
480
+ .map(out => out.targetRef);
481
+
482
+ // handle boundary events
483
+ attachedOutgoing.forEach((nextElement, index, arr) => {
484
+ if (visited.has(nextElement)) {
485
+ return;
486
+ }
487
+
488
+ // Add below and to the right of the element
489
+ insertIntoGrid(nextElement, element, grid);
490
+ nextElements.push(nextElement);
491
+ });
492
+
493
+ return nextElements;
494
+ },
495
+
496
+ 'createElementDi': ({ element, row, col, diFactory }) => {
497
+ const hostBounds = getBounds(element, row, col);
498
+
499
+ const DIs = [];
500
+ (element.attachers || []).forEach((att, i, arr) => {
501
+ att.gridPosition = { row, col };
502
+ const bounds = getBounds(att, row, col, element);
503
+
504
+ // distribute along lower edge
505
+ bounds.x = hostBounds.x + (i + 1) * (hostBounds.width / (arr.length + 1)) - bounds.width / 2;
506
+
507
+ const attacherDi = diFactory.createDiShape(att, bounds, {
508
+ id: att.id + '_di'
509
+ });
510
+ att.di = attacherDi;
511
+ att.gridPosition = { row, col };
512
+
513
+ DIs.push(attacherDi);
514
+ });
515
+
516
+ return DIs;
517
+ },
518
+
519
+ 'createConnectionDi': ({ element, row, col, layoutGrid, diFactory }) => {
520
+ const attachers = element.attachers || [];
521
+
522
+ return attachers.flatMap(att => {
523
+ const outgoing = att.outgoing || [];
524
+
525
+ return outgoing.map(out => {
526
+ const target = out.targetRef;
527
+ const waypoints = connectElements(att, target, layoutGrid);
528
+
529
+ // Correct waypoints if they don't automatically attach to the bottom
530
+ ensureExitBottom(att, waypoints, [ row, col ]);
531
+
532
+ const connectionDi = diFactory.createDiEdge(out, waypoints, {
533
+ id: out.id + '_di'
534
+ });
535
+
536
+ return connectionDi;
537
+ });
538
+ });
539
+ }
540
+ };
541
+
542
+
543
+ function insertIntoGrid(newElement, host, grid) {
544
+ const [ row, col ] = grid.find(host);
545
+
546
+ // Grid is occupied
547
+ if (grid.get(row + 1, col) || grid.get(row + 1, col + 1)) {
548
+ grid.createRow(row);
549
+ }
550
+
551
+ // Host has element directly after, add space
552
+ if (grid.get(row, col + 1)) {
553
+ grid.addAfter(host, null);
554
+ }
555
+
556
+ grid.add(newElement, [ row + 1, col + 1 ]);
557
+ }
558
+
559
+ function ensureExitBottom(source, waypoints, [ row, col ]) {
560
+
561
+ const sourceDi = source.di;
562
+ const sourceBounds = sourceDi.get('bounds');
563
+ const sourceMid = getMid(sourceBounds);
564
+
565
+ const dockingPoint = getDockingPoint(sourceMid, sourceBounds, 'b');
566
+ if (waypoints[0].x === dockingPoint.x && waypoints[0].y === dockingPoint.y) {
567
+ return;
568
+ }
569
+
570
+ if (waypoints.length === 2) {
571
+ const newStart = [
572
+ dockingPoint,
573
+ { x: dockingPoint.x, y: (row + 1) * DEFAULT_CELL_HEIGHT },
574
+ { x: (col + 1) * DEFAULT_CELL_WIDTH, y: (row + 1) * DEFAULT_CELL_HEIGHT },
575
+ { x: (col + 1) * DEFAULT_CELL_WIDTH, y: (row + 0.5) * DEFAULT_CELL_HEIGHT },
576
+ ];
577
+
578
+ waypoints.splice(0, 1, ...newStart);
579
+ return;
580
+ }
581
+
582
+ // add waypoints to exit bottom and connect to existing path
583
+ const newStart = [
584
+ dockingPoint,
585
+ { x: dockingPoint.x, y: (row + 1) * DEFAULT_CELL_HEIGHT },
586
+ { x: waypoints[1].x, y: (row + 1) * DEFAULT_CELL_HEIGHT },
587
+ ];
588
+
589
+ waypoints.splice(0, 1, ...newStart);
590
+ return;
591
+ }
592
+
593
+ var elementHandler = {
594
+ 'createElementDi': ({ element, row, col, diFactory }) => {
595
+
596
+ const bounds = getBounds(element, row, col);
597
+
598
+ const options = {
599
+ id: element.id + '_di'
600
+ };
601
+
602
+ if (is(element, 'bpmn:ExclusiveGateway')) {
603
+ options.isMarkerVisible = true;
604
+ }
605
+
606
+ const shapeDi = diFactory.createDiShape(element, bounds, options);
607
+ element.di = shapeDi;
608
+ element.gridPosition = { row, col };
609
+
610
+ return shapeDi;
611
+ }
612
+ };
613
+
614
+ var outgoingHandler = {
615
+ 'addToGrid': ({ element, grid, visited }) => {
616
+ const nextElements = [];
617
+
618
+ // Handle outgoing paths
619
+ const outgoing = (element.outgoing || [])
620
+ .map(out => out.targetRef)
621
+ .filter(el => el);
622
+
623
+ let previousElement = null;
624
+ outgoing.forEach((nextElement, index, arr) => {
625
+ if (visited.has(nextElement)) {
626
+ return;
627
+ }
628
+
629
+ if (!previousElement) {
630
+ grid.addAfter(element, nextElement);
631
+ }
632
+ else {
633
+ grid.addBelow(arr[index - 1], nextElement);
634
+ }
635
+
636
+ // Is self-looping
637
+ if (nextElement !== element) {
638
+ previousElement = nextElement;
639
+ }
640
+
641
+ nextElements.unshift(nextElement);
642
+ visited.add(nextElement);
643
+ });
644
+
645
+ return nextElements;
646
+ },
647
+ 'createConnectionDi': ({ element, row, col, layoutGrid, diFactory }) => {
648
+ const outgoing = element.outgoing || [];
649
+
650
+ return outgoing.map(out => {
651
+ const target = out.targetRef;
652
+ const waypoints = connectElements(element, target, layoutGrid);
653
+
654
+ const connectionDi = diFactory.createDiEdge(out, waypoints, {
655
+ id: out.id + '_di'
656
+ });
657
+
658
+ return connectionDi;
659
+ });
660
+
661
+ }
662
+ };
663
+
664
+ const handlers = [ elementHandler, outgoingHandler, attacherHandler ];
665
+
370
666
  class Layouter {
371
667
  constructor() {
372
668
  this.moddle = new BPMNModdle();
373
669
  this.diFactory = new DiFactory(this.moddle);
670
+ this._handlers = handlers;
671
+ }
672
+
673
+ handle(operation, options) {
674
+ return this._handlers
675
+ .filter(handler => isFunction(handler[operation]))
676
+ .map(handler => handler[operation](options));
677
+
374
678
  }
375
679
 
376
680
  async layoutProcess(xml) {
@@ -398,7 +702,6 @@ class Layouter {
398
702
  createGridLayout(root) {
399
703
  const grid = new Grid();
400
704
 
401
- // const process = this.getProcess();
402
705
  const flowElements = root.flowElements;
403
706
 
404
707
  const startingElements = flowElements.filter(el => {
@@ -429,54 +732,11 @@ class Layouter {
429
732
  this.handlePlane(currentElement);
430
733
  }
431
734
 
432
- // Handle outgoing paths
433
- const outgoing = (currentElement.outgoing || [])
434
- .map(out => out.targetRef)
435
- .filter(el => el);
436
-
437
- let previousElement = null;
438
- outgoing.forEach((nextElement, index, arr) => {
439
- if (visited.has(nextElement)) {
440
- return;
441
- }
442
-
443
- if (!previousElement) {
444
- grid.addAfter(currentElement, nextElement);
445
- }
446
- else {
447
- grid.addBelow(arr[index - 1], nextElement);
448
- }
449
-
450
- // Is self-looping
451
- if (nextElement !== currentElement) {
452
- previousElement = nextElement;
453
- }
454
- });
455
-
456
- const attachedOutgoing = (currentElement.attachers || [])
457
- .map(att => att.outgoing)
458
- .flat()
459
- .map(out => out.targetRef);
735
+ const nextElements = this.handle('addToGrid', { element: currentElement, grid, visited });
460
736
 
461
- // handle boundary events
462
- attachedOutgoing.forEach((nextElement, index, arr) => {
463
- if (visited.has(nextElement)) {
464
- return;
465
- }
466
-
467
- const below = arr[index - 1] || currentElement;
468
- grid.addBelow(below, nextElement);
469
- stack.push(nextElement);
470
- visited.add(nextElement);
471
- });
472
-
473
- // add to stack in reverse order: first element should be first of the stack
474
- outgoing.reverse().forEach(el => {
475
- if (visited.has(el)) {
476
- return;
477
- }
478
- visited.add(el);
737
+ nextElements.flat().forEach(el => {
479
738
  stack.push(el);
739
+ visited.add(el);
480
740
  });
481
741
  }
482
742
 
@@ -505,62 +765,20 @@ class Layouter {
505
765
 
506
766
  // Step 1: Create DI for all elements
507
767
  layoutGrid.elementsByPosition().forEach(({ element, row, col }) => {
508
- const bounds = getBounds(element, row, col);
768
+ const dis = this
769
+ .handle('createElementDi', { element, row, col, layoutGrid, diFactory })
770
+ .flat();
509
771
 
510
- const shapeDi = diFactory.createDiShape(element, bounds, {
511
- id: element.id + '_di'
512
- });
513
- element.di = shapeDi;
514
- element.gridPosition = { row, col };
515
-
516
- planeElement.push(shapeDi);
517
-
518
- // handle attachers
519
- (element.attachers || []).forEach(att => {
520
- att.gridPosition = { row, col };
521
- const attacherBounds = getBounds(att, row, col, element);
522
-
523
- const attacherDi = diFactory.createDiShape(att, attacherBounds, {
524
- id: att.id + '_di'
525
- });
526
- att.di = attacherDi;
527
- att.gridPosition = { row, col };
528
-
529
- planeElement.push(attacherDi);
530
- });
772
+ planeElement.push(...dis);
531
773
  });
532
774
 
533
775
  // Step 2: Create DI for all connections
534
776
  layoutGrid.elementsByPosition().forEach(({ element, row, col }) => {
535
- const outgoing = element.outgoing || [];
536
-
537
- outgoing.forEach(out => {
538
- const target = out.targetRef;
539
- const waypoints = connectElements(element, target, layoutGrid);
540
-
541
- const connectionDi = diFactory.createDiEdge(out, waypoints, {
542
- id: out.id + '_di'
543
- });
544
-
545
- planeElement.push(connectionDi);
546
- });
547
-
548
- // handle attachers
549
- (element.attachers || []).forEach(att => {
550
- const outgoing = att.outgoing || [];
551
-
552
- outgoing.forEach(out => {
553
- const target = out.targetRef;
554
- const waypoints = connectElements(att, target, layoutGrid);
555
-
556
- const connectionDi = diFactory.createDiEdge(out, waypoints, {
557
- id: out.id + '_di'
558
- });
559
-
560
- planeElement.push(connectionDi);
561
- });
562
- });
777
+ const dis = this
778
+ .handle('createConnectionDi', { element, row, col, layoutGrid, diFactory })
779
+ .flat();
563
780
 
781
+ planeElement.push(...dis);
564
782
  });
565
783
  }
566
784