ngx-vflow 0.15.0 → 0.16.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.
@@ -1213,6 +1213,176 @@ function getPointOnBezier(sourcePoint, targetPoint, sourceControl, targetControl
1213
1213
  return getPointOnLineByRatio(getPointOnLineByRatio(fromSourceToFirstControl, fromFirstControlToSecond, ratio), getPointOnLineByRatio(fromFirstControlToSecond, fromSecondControlToTarget, ratio), ratio);
1214
1214
  }
1215
1215
 
1216
+ const handleDirections = {
1217
+ left: { x: -1, y: 0 },
1218
+ right: { x: 1, y: 0 },
1219
+ top: { x: 0, y: -1 },
1220
+ bottom: { x: 0, y: 1 },
1221
+ };
1222
+ function getEdgeCenter(source, target) {
1223
+ const xOffset = Math.abs(target.x - source.x) / 2;
1224
+ const centerX = target.x < source.x ? target.x + xOffset : target.x - xOffset;
1225
+ const yOffset = Math.abs(target.y - source.y) / 2;
1226
+ const centerY = target.y < source.y ? target.y + yOffset : target.y - yOffset;
1227
+ return [centerX, centerY, xOffset, yOffset];
1228
+ }
1229
+ const getDirection = ({ source, sourcePosition = 'bottom', target, }) => {
1230
+ if (sourcePosition === 'left' || sourcePosition === 'right') {
1231
+ return source.x < target.x ? { x: 1, y: 0 } : { x: -1, y: 0 };
1232
+ }
1233
+ return source.y < target.y ? { x: 0, y: 1 } : { x: 0, y: -1 };
1234
+ };
1235
+ const distance = (a, b) => Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2));
1236
+ // ith this function we try to mimic a orthogonal edge routing behaviour
1237
+ // It's not as good as a real orthogonal edge routing but it's faster and good enough as a default for step and smooth step edges
1238
+ function getPoints({ source, sourcePosition = 'bottom', target, targetPosition = 'top', offset, }) {
1239
+ const sourceDir = handleDirections[sourcePosition];
1240
+ const targetDir = handleDirections[targetPosition];
1241
+ const sourceGapped = { x: source.x + sourceDir.x * offset, y: source.y + sourceDir.y * offset };
1242
+ const targetGapped = { x: target.x + targetDir.x * offset, y: target.y + targetDir.y * offset };
1243
+ const dir = getDirection({
1244
+ source: sourceGapped,
1245
+ sourcePosition,
1246
+ target: targetGapped,
1247
+ });
1248
+ const dirAccessor = dir.x !== 0 ? 'x' : 'y';
1249
+ const currDir = dir[dirAccessor];
1250
+ let points = [];
1251
+ let centerX, centerY;
1252
+ const sourceGapOffset = { x: 0, y: 0 };
1253
+ const targetGapOffset = { x: 0, y: 0 };
1254
+ const [defaultCenterX, defaultCenterY] = getEdgeCenter(source, target);
1255
+ // opposite handle positions, default case
1256
+ if (sourceDir[dirAccessor] * targetDir[dirAccessor] === -1) {
1257
+ centerX = defaultCenterX;
1258
+ centerY = defaultCenterY;
1259
+ // --->
1260
+ // |
1261
+ // >---
1262
+ const verticalSplit = [
1263
+ { x: centerX, y: sourceGapped.y },
1264
+ { x: centerX, y: targetGapped.y },
1265
+ ];
1266
+ // |
1267
+ // ---
1268
+ // |
1269
+ const horizontalSplit = [
1270
+ { x: sourceGapped.x, y: centerY },
1271
+ { x: targetGapped.x, y: centerY },
1272
+ ];
1273
+ if (sourceDir[dirAccessor] === currDir) {
1274
+ points = dirAccessor === 'x' ? verticalSplit : horizontalSplit;
1275
+ }
1276
+ else {
1277
+ points = dirAccessor === 'x' ? horizontalSplit : verticalSplit;
1278
+ }
1279
+ }
1280
+ else {
1281
+ // sourceTarget means we take x from source and y from target, targetSource is the opposite
1282
+ const sourceTarget = [{ x: sourceGapped.x, y: targetGapped.y }];
1283
+ const targetSource = [{ x: targetGapped.x, y: sourceGapped.y }];
1284
+ // this handles edges with same handle positions
1285
+ if (dirAccessor === 'x') {
1286
+ points = sourceDir.x === currDir ? targetSource : sourceTarget;
1287
+ }
1288
+ else {
1289
+ points = sourceDir.y === currDir ? sourceTarget : targetSource;
1290
+ }
1291
+ if (sourcePosition === targetPosition) {
1292
+ const diff = Math.abs(source[dirAccessor] - target[dirAccessor]);
1293
+ // if an edge goes from right to right for example (sourcePosition === targetPosition) and the distance between source.x and target.x is less than the offset, the added point and the gapped source/target will overlap. This leads to a weird edge path. To avoid this we add a gapOffset to the source/target
1294
+ if (diff <= offset) {
1295
+ const gapOffset = Math.min(offset - 1, offset - diff);
1296
+ if (sourceDir[dirAccessor] === currDir) {
1297
+ sourceGapOffset[dirAccessor] = (sourceGapped[dirAccessor] > source[dirAccessor] ? -1 : 1) * gapOffset;
1298
+ }
1299
+ else {
1300
+ targetGapOffset[dirAccessor] = (targetGapped[dirAccessor] > target[dirAccessor] ? -1 : 1) * gapOffset;
1301
+ }
1302
+ }
1303
+ }
1304
+ // these are conditions for handling mixed handle positions like Right -> Bottom for example
1305
+ if (sourcePosition !== targetPosition) {
1306
+ const dirAccessorOpposite = dirAccessor === 'x' ? 'y' : 'x';
1307
+ const isSameDir = sourceDir[dirAccessor] === targetDir[dirAccessorOpposite];
1308
+ const sourceGtTargetOppo = sourceGapped[dirAccessorOpposite] > targetGapped[dirAccessorOpposite];
1309
+ const sourceLtTargetOppo = sourceGapped[dirAccessorOpposite] < targetGapped[dirAccessorOpposite];
1310
+ const flipSourceTarget = (sourceDir[dirAccessor] === 1 && ((!isSameDir && sourceGtTargetOppo) || (isSameDir && sourceLtTargetOppo))) ||
1311
+ (sourceDir[dirAccessor] !== 1 && ((!isSameDir && sourceLtTargetOppo) || (isSameDir && sourceGtTargetOppo)));
1312
+ if (flipSourceTarget) {
1313
+ points = dirAccessor === 'x' ? sourceTarget : targetSource;
1314
+ }
1315
+ }
1316
+ const sourceGapPoint = { x: sourceGapped.x + sourceGapOffset.x, y: sourceGapped.y + sourceGapOffset.y };
1317
+ const targetGapPoint = { x: targetGapped.x + targetGapOffset.x, y: targetGapped.y + targetGapOffset.y };
1318
+ const maxXDistance = Math.max(Math.abs(sourceGapPoint.x - points[0].x), Math.abs(targetGapPoint.x - points[0].x));
1319
+ const maxYDistance = Math.max(Math.abs(sourceGapPoint.y - points[0].y), Math.abs(targetGapPoint.y - points[0].y));
1320
+ // we want to place the label on the longest segment of the edge
1321
+ if (maxXDistance >= maxYDistance) {
1322
+ centerX = (sourceGapPoint.x + targetGapPoint.x) / 2;
1323
+ centerY = points[0].y;
1324
+ }
1325
+ else {
1326
+ centerX = points[0].x;
1327
+ centerY = (sourceGapPoint.y + targetGapPoint.y) / 2;
1328
+ }
1329
+ }
1330
+ const pathPoints = [
1331
+ source,
1332
+ { x: sourceGapped.x + sourceGapOffset.x, y: sourceGapped.y + sourceGapOffset.y },
1333
+ ...points,
1334
+ { x: targetGapped.x + targetGapOffset.x, y: targetGapped.y + targetGapOffset.y },
1335
+ target,
1336
+ ];
1337
+ return [pathPoints, centerX, centerY];
1338
+ }
1339
+ function getBend(a, b, c, size) {
1340
+ const bendSize = Math.min(distance(a, b) / 2, distance(b, c) / 2, size);
1341
+ const { x, y } = b;
1342
+ // no bend
1343
+ if ((a.x === x && x === c.x) || (a.y === y && y === c.y)) {
1344
+ return `L${x} ${y}`;
1345
+ }
1346
+ // first segment is horizontal
1347
+ if (a.y === y) {
1348
+ const xDir = a.x < c.x ? -1 : 1;
1349
+ const yDir = a.y < c.y ? 1 : -1;
1350
+ return `L ${x + bendSize * xDir},${y}Q ${x},${y} ${x},${y + bendSize * yDir}`;
1351
+ }
1352
+ const xDir = a.x < c.x ? 1 : -1;
1353
+ const yDir = a.y < c.y ? -1 : 1;
1354
+ return `L ${x},${y + bendSize * yDir}Q ${x},${y} ${x + bendSize * xDir},${y}`;
1355
+ }
1356
+ function smoothStepPath(source, target, sourcePosition, targetPosition, borderRadius = 5) {
1357
+ const [points, labelX, labelY] = getPoints({
1358
+ source,
1359
+ sourcePosition,
1360
+ target,
1361
+ targetPosition,
1362
+ offset: 20
1363
+ });
1364
+ const path = points.reduce((res, p, i) => {
1365
+ let segment = '';
1366
+ if (i > 0 && i < points.length - 1) {
1367
+ segment = getBend(points[i - 1], p, points[i + 1], borderRadius);
1368
+ }
1369
+ else {
1370
+ segment = `${i === 0 ? 'M' : 'L'}${p.x} ${p.y}`;
1371
+ }
1372
+ res += segment;
1373
+ return res;
1374
+ }, '');
1375
+ return {
1376
+ path,
1377
+ points: {
1378
+ // TODO start and end points temporary unavailable for this path
1379
+ start: { x: labelX, y: labelY },
1380
+ center: { x: labelX, y: labelY },
1381
+ end: { x: labelX, y: labelY },
1382
+ }
1383
+ };
1384
+ }
1385
+
1216
1386
  class EdgeModel {
1217
1387
  constructor(edge) {
1218
1388
  this.edge = edge;
@@ -1282,6 +1452,10 @@ class EdgeModel {
1282
1452
  return straightPath(source.pointAbsolute(), target.pointAbsolute(), this.usingPoints);
1283
1453
  case 'bezier':
1284
1454
  return bezierPath(source.pointAbsolute(), target.pointAbsolute(), source.rawHandle.position, target.rawHandle.position, this.usingPoints);
1455
+ case 'smooth-step':
1456
+ return smoothStepPath(source.pointAbsolute(), target.pointAbsolute(), source.rawHandle.position, target.rawHandle.position);
1457
+ case 'step':
1458
+ return smoothStepPath(source.pointAbsolute(), target.pointAbsolute(), source.rawHandle.position, target.rawHandle.position, 0);
1285
1459
  }
1286
1460
  });
1287
1461
  this.edgeLabels = {};
@@ -1558,6 +1732,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
1558
1732
  args: ['onEdgesChange.select.many']
1559
1733
  }] } });
1560
1734
 
1735
+ function isGroupNode(node) {
1736
+ return node.node.type === 'default-group' || node.node.type === 'template-group';
1737
+ }
1738
+
1561
1739
  class NodeRenderingService {
1562
1740
  constructor() {
1563
1741
  this.flowEntitiesService = inject(FlowEntitiesService);
@@ -1565,6 +1743,12 @@ class NodeRenderingService {
1565
1743
  return this.flowEntitiesService.nodes()
1566
1744
  .sort((aNode, bNode) => aNode.renderOrder() - bNode.renderOrder());
1567
1745
  });
1746
+ this.groups = computed(() => {
1747
+ return this.nodes().filter(n => isGroupNode(n));
1748
+ });
1749
+ this.nonGroups = computed(() => {
1750
+ return this.nodes().filter(n => !isGroupNode(n));
1751
+ });
1568
1752
  this.maxOrder = computed(() => {
1569
1753
  return Math.max(...this.flowEntitiesService.nodes().map((n) => n.renderOrder()));
1570
1754
  });
@@ -2470,6 +2654,8 @@ class ConnectionComponent {
2470
2654
  switch (this.model.curve) {
2471
2655
  case 'straight': return straightPath(sourcePoint, targetPoint).path;
2472
2656
  case 'bezier': return bezierPath(sourcePoint, targetPoint, sourcePosition, targetPosition).path;
2657
+ case 'smooth-step': return smoothStepPath(sourcePoint, targetPoint, sourcePosition, targetPosition).path;
2658
+ case 'step': return smoothStepPath(sourcePoint, targetPoint, sourcePosition, targetPosition, 0).path;
2473
2659
  }
2474
2660
  }
2475
2661
  if (status.state === 'connection-validation') {
@@ -2487,6 +2673,8 @@ class ConnectionComponent {
2487
2673
  switch (this.model.curve) {
2488
2674
  case 'straight': return straightPath(sourcePoint, targetPoint).path;
2489
2675
  case 'bezier': return bezierPath(sourcePoint, targetPoint, sourcePosition, targetPosition).path;
2676
+ case 'smooth-step': return smoothStepPath(sourcePoint, targetPoint, sourcePosition, targetPosition).path;
2677
+ case 'step': return smoothStepPath(sourcePoint, targetPoint, sourcePosition, targetPosition, 0).path;
2490
2678
  }
2491
2679
  }
2492
2680
  return null;
@@ -2592,12 +2780,15 @@ const defaultBg = '#fff';
2592
2780
  const defaultGap = 20;
2593
2781
  const defaultDotSize = 2;
2594
2782
  const defaultDotColor = 'rgb(177, 177, 183)';
2783
+ const defaultImageScale = 0.1;
2784
+ const defaultRepeated = true;
2595
2785
  class BackgroundComponent {
2596
2786
  constructor() {
2597
2787
  this.viewportService = inject(ViewportService);
2598
2788
  this.rootSvg = inject(RootSvgReferenceDirective).element;
2599
2789
  this.settingsService = inject(FlowSettingsService);
2600
2790
  this.backgroundSignal = this.settingsService.background;
2791
+ // DOTS PATTERN
2601
2792
  this.scaledGap = computed(() => {
2602
2793
  const background = this.backgroundSignal();
2603
2794
  if (background.type === 'dots') {
@@ -2608,7 +2799,13 @@ class BackgroundComponent {
2608
2799
  });
2609
2800
  this.x = computed(() => this.viewportService.readableViewport().x % this.scaledGap());
2610
2801
  this.y = computed(() => this.viewportService.readableViewport().y % this.scaledGap());
2611
- this.patternColor = computed(() => this.backgroundSignal().color ?? defaultDotColor);
2802
+ this.patternColor = computed(() => {
2803
+ const bg = this.backgroundSignal();
2804
+ if (bg.type === 'dots') {
2805
+ return bg.color ?? defaultDotColor;
2806
+ }
2807
+ return defaultDotColor;
2808
+ });
2612
2809
  this.patternSize = computed(() => {
2613
2810
  const background = this.backgroundSignal();
2614
2811
  if (background.type === 'dots') {
@@ -2616,6 +2813,56 @@ class BackgroundComponent {
2616
2813
  }
2617
2814
  return 0;
2618
2815
  });
2816
+ // IMAGE PATTERN
2817
+ this.bgImageSrc = computed(() => {
2818
+ const background = this.backgroundSignal();
2819
+ return background.type === 'image' ? background.src : '';
2820
+ });
2821
+ this.imageSize = toSignal(toObservable(this.backgroundSignal).pipe(switchMap(() => createImage(this.bgImageSrc())), map((image) => ({ width: image.naturalWidth, height: image.naturalHeight }))), { initialValue: { width: 0, height: 0 } });
2822
+ this.scaledImageWidth = computed(() => {
2823
+ const background = this.backgroundSignal();
2824
+ if (background.type === 'image') {
2825
+ const zoom = background.fixed ? 1 : this.viewportService.readableViewport().zoom;
2826
+ return this.imageSize().width * zoom * (background.scale ?? defaultImageScale);
2827
+ }
2828
+ return 0;
2829
+ });
2830
+ this.scaledImageHeight = computed(() => {
2831
+ const background = this.backgroundSignal();
2832
+ if (background.type === 'image') {
2833
+ const zoom = background.fixed ? 1 : this.viewportService.readableViewport().zoom;
2834
+ return this.imageSize().height * zoom * (background.scale ?? defaultImageScale);
2835
+ }
2836
+ return 0;
2837
+ });
2838
+ this.imageX = computed(() => {
2839
+ const background = this.backgroundSignal();
2840
+ if (background.type === 'image') {
2841
+ if (!background.repeat) {
2842
+ return background.fixed ? 0 : this.viewportService.readableViewport().x;
2843
+ }
2844
+ return background.fixed
2845
+ ? 0
2846
+ : this.viewportService.readableViewport().x % this.scaledImageWidth();
2847
+ }
2848
+ return 0;
2849
+ });
2850
+ this.imageY = computed(() => {
2851
+ const background = this.backgroundSignal();
2852
+ if (background.type === 'image') {
2853
+ if (!background.repeat) {
2854
+ return background.fixed ? 0 : this.viewportService.readableViewport().y;
2855
+ }
2856
+ return background.fixed
2857
+ ? 0
2858
+ : this.viewportService.readableViewport().y % this.scaledImageHeight();
2859
+ }
2860
+ return 0;
2861
+ });
2862
+ this.repeated = computed(() => {
2863
+ const background = this.backgroundSignal();
2864
+ return background.type === 'image' && (background.repeat ?? defaultRepeated);
2865
+ });
2619
2866
  // Without ID there will be pattern collision for several flows on the page
2620
2867
  // Later pattern ID may be exposed to API
2621
2868
  this.patternId = id();
@@ -2631,12 +2878,19 @@ class BackgroundComponent {
2631
2878
  });
2632
2879
  }
2633
2880
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: BackgroundComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2634
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: BackgroundComponent, selector: "g[background]", ngImport: i0, template: "<ng-container *ngIf=\"backgroundSignal().type === 'dots'\">\n <svg:pattern\n [attr.id]=\"patternId\"\n [attr.x]=\"x()\"\n [attr.y]=\"y()\"\n [attr.width]=\"scaledGap()\"\n [attr.height]=\"scaledGap()\"\n patternUnits=\"userSpaceOnUse\"\n >\n <svg:circle\n [attr.cx]=\"patternSize()\"\n [attr.cy]=\"patternSize()\"\n [attr.r]=\"patternSize()\"\n [attr.fill]=\"patternColor()\"\n />\n </svg:pattern>\n\n <svg:rect\n x=\"0\"\n y=\"0\"\n width=\"100%\"\n height=\"100%\"\n [attr.fill]=\"patternUrl\"\n />\n</ng-container>\n", dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2881
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: BackgroundComponent, selector: "g[background]", ngImport: i0, template: "<ng-container *ngIf=\"backgroundSignal().type === 'dots'\">\n <svg:pattern\n [attr.id]=\"patternId\"\n [attr.x]=\"x()\"\n [attr.y]=\"y()\"\n [attr.width]=\"scaledGap()\"\n [attr.height]=\"scaledGap()\"\n patternUnits=\"userSpaceOnUse\"\n >\n <svg:circle\n [attr.cx]=\"patternSize()\"\n [attr.cy]=\"patternSize()\"\n [attr.r]=\"patternSize()\"\n [attr.fill]=\"patternColor()\"\n />\n </svg:pattern>\n\n <svg:rect\n x=\"0\"\n y=\"0\"\n width=\"100%\"\n height=\"100%\"\n [attr.fill]=\"patternUrl\"\n />\n</ng-container>\n\n<ng-container *ngIf=\"backgroundSignal().type === 'image'\">\n <ng-container *ngIf=\"repeated()\">\n <svg:pattern\n [attr.id]=\"patternId\"\n [attr.x]=\"imageX()\"\n [attr.y]=\"imageY()\"\n [attr.width]=\"scaledImageWidth()\"\n [attr.height]=\"scaledImageHeight()\"\n patternUnits=\"userSpaceOnUse\"\n >\n <svg:image\n [attr.href]=\"bgImageSrc()\"\n [attr.width]=\"scaledImageWidth()\"\n [attr.height]=\"scaledImageHeight()\"\n />\n </svg:pattern>\n\n <svg:rect\n x=\"0\"\n y=\"0\"\n width=\"100%\"\n height=\"100%\"\n [attr.fill]=\"patternUrl\"\n />\n </ng-container>\n\n <ng-container *ngIf=\"!repeated()\">\n <svg:image\n [attr.x]=\"imageX()\"\n [attr.y]=\"imageY()\"\n [attr.width]=\"scaledImageWidth()\"\n [attr.height]=\"scaledImageHeight()\"\n [attr.href]=\"bgImageSrc()\"\n />\n </ng-container>\n</ng-container>\n", dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2635
2882
  }
2636
2883
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: BackgroundComponent, decorators: [{
2637
2884
  type: Component,
2638
- args: [{ selector: 'g[background]', changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *ngIf=\"backgroundSignal().type === 'dots'\">\n <svg:pattern\n [attr.id]=\"patternId\"\n [attr.x]=\"x()\"\n [attr.y]=\"y()\"\n [attr.width]=\"scaledGap()\"\n [attr.height]=\"scaledGap()\"\n patternUnits=\"userSpaceOnUse\"\n >\n <svg:circle\n [attr.cx]=\"patternSize()\"\n [attr.cy]=\"patternSize()\"\n [attr.r]=\"patternSize()\"\n [attr.fill]=\"patternColor()\"\n />\n </svg:pattern>\n\n <svg:rect\n x=\"0\"\n y=\"0\"\n width=\"100%\"\n height=\"100%\"\n [attr.fill]=\"patternUrl\"\n />\n</ng-container>\n" }]
2885
+ args: [{ selector: 'g[background]', changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *ngIf=\"backgroundSignal().type === 'dots'\">\n <svg:pattern\n [attr.id]=\"patternId\"\n [attr.x]=\"x()\"\n [attr.y]=\"y()\"\n [attr.width]=\"scaledGap()\"\n [attr.height]=\"scaledGap()\"\n patternUnits=\"userSpaceOnUse\"\n >\n <svg:circle\n [attr.cx]=\"patternSize()\"\n [attr.cy]=\"patternSize()\"\n [attr.r]=\"patternSize()\"\n [attr.fill]=\"patternColor()\"\n />\n </svg:pattern>\n\n <svg:rect\n x=\"0\"\n y=\"0\"\n width=\"100%\"\n height=\"100%\"\n [attr.fill]=\"patternUrl\"\n />\n</ng-container>\n\n<ng-container *ngIf=\"backgroundSignal().type === 'image'\">\n <ng-container *ngIf=\"repeated()\">\n <svg:pattern\n [attr.id]=\"patternId\"\n [attr.x]=\"imageX()\"\n [attr.y]=\"imageY()\"\n [attr.width]=\"scaledImageWidth()\"\n [attr.height]=\"scaledImageHeight()\"\n patternUnits=\"userSpaceOnUse\"\n >\n <svg:image\n [attr.href]=\"bgImageSrc()\"\n [attr.width]=\"scaledImageWidth()\"\n [attr.height]=\"scaledImageHeight()\"\n />\n </svg:pattern>\n\n <svg:rect\n x=\"0\"\n y=\"0\"\n width=\"100%\"\n height=\"100%\"\n [attr.fill]=\"patternUrl\"\n />\n </ng-container>\n\n <ng-container *ngIf=\"!repeated()\">\n <svg:image\n [attr.x]=\"imageX()\"\n [attr.y]=\"imageY()\"\n [attr.width]=\"scaledImageWidth()\"\n [attr.height]=\"scaledImageHeight()\"\n [attr.href]=\"bgImageSrc()\"\n />\n </ng-container>\n</ng-container>\n" }]
2639
2886
  }], ctorParameters: function () { return []; } });
2887
+ function createImage(url) {
2888
+ const image = new Image();
2889
+ image.src = url;
2890
+ return new Promise(resolve => {
2891
+ image.onload = () => resolve(image);
2892
+ });
2893
+ }
2640
2894
 
2641
2895
  // TODO: too general purpose nane
2642
2896
  class RootSvgContextDirective {
@@ -2746,9 +3000,12 @@ class VflowComponent {
2746
3000
  this.keyboardService = inject(KeyboardService);
2747
3001
  this.injector = inject(Injector);
2748
3002
  this.optimization = {
2749
- computeLayersOnInit: true
3003
+ computeLayersOnInit: true,
3004
+ detachedGroupsLayer: false
2750
3005
  };
2751
3006
  this.nodeModels = computed(() => this.nodeRenderingService.nodes());
3007
+ this.groups = computed(() => this.nodeRenderingService.groups());
3008
+ this.nonGroups = computed(() => this.nodeRenderingService.nonGroups());
2752
3009
  this.edgeModels = computed(() => this.flowEntitiesService.validEdges());
2753
3010
  // #endregion
2754
3011
  // #region OUTPUTS
@@ -2952,7 +3209,7 @@ class VflowComponent {
2952
3209
  ComponentEventBusService,
2953
3210
  KeyboardService,
2954
3211
  OverlaysService
2955
- ], queries: [{ propertyName: "nodeTemplateDirective", first: true, predicate: NodeHtmlTemplateDirective, descendants: true }, { propertyName: "groupNodeTemplateDirective", first: true, predicate: GroupNodeTemplateDirective, descendants: true }, { propertyName: "edgeTemplateDirective", first: true, predicate: EdgeTemplateDirective, descendants: true }, { propertyName: "edgeLabelHtmlDirective", first: true, predicate: EdgeLabelHtmlTemplateDirective, descendants: true }, { propertyName: "connectionTemplateDirective", first: true, predicate: ConnectionTemplateDirective, descendants: true }], viewQueries: [{ propertyName: "mapContext", first: true, predicate: MapContextDirective, descendants: true }, { propertyName: "spacePointContext", first: true, predicate: SpacePointContextDirective, descendants: true }], hostDirectives: [{ directive: ConnectionControllerDirective, outputs: ["onConnect", "onConnect"] }, { directive: ChangesControllerDirective, outputs: ["onNodesChange", "onNodesChange", "onNodesChange.position", "onNodesChange.position", "onNodesChange.position.single", "onNodesChange.position.single", "onNodesChange.position.many", "onNodesChange.position.many", "onNodesChange.size", "onNodesChange.size", "onNodesChange.size.single", "onNodesChange.size.single", "onNodesChange.size.many", "onNodesChange.size.many", "onNodesChange.add", "onNodesChange.add", "onNodesChange.add.single", "onNodesChange.add.single", "onNodesChange.add.many", "onNodesChange.add.many", "onNodesChange.remove", "onNodesChange.remove", "onNodesChange.remove.single", "onNodesChange.remove.single", "onNodesChange.remove.many", "onNodesChange.remove.many", "onNodesChange.select", "onNodesChange.select", "onNodesChange.select.single", "onNodesChange.select.single", "onNodesChange.select.many", "onNodesChange.select.many", "onEdgesChange", "onEdgesChange", "onEdgesChange.detached", "onEdgesChange.detached", "onEdgesChange.detached.single", "onEdgesChange.detached.single", "onEdgesChange.detached.many", "onEdgesChange.detached.many", "onEdgesChange.add", "onEdgesChange.add", "onEdgesChange.add.single", "onEdgesChange.add.single", "onEdgesChange.add.many", "onEdgesChange.add.many", "onEdgesChange.remove", "onEdgesChange.remove", "onEdgesChange.remove.single", "onEdgesChange.remove.single", "onEdgesChange.remove.many", "onEdgesChange.remove.many", "onEdgesChange.select", "onEdgesChange.select", "onEdgesChange.select.single", "onEdgesChange.select.single", "onEdgesChange.select.many", "onEdgesChange.select.many"] }], ngImport: i0, template: "<svg:svg\n rootSvgRef\n rootSvgContext\n rootPointer\n flowSizeController\n class=\"root-svg\"\n #flow\n>\n <defs [markers]=\"markers()\" flowDefs />\n\n <g background />\n\n <svg:g\n mapContext\n spacePointContext\n >\n <!-- Connection -->\n <svg:g\n connection\n [model]=\"connection\"\n [template]=\"connectionTemplateDirective?.templateRef\"\n />\n\n <!-- Edges -->\n <svg:g\n *ngFor=\"let model of edgeModels(); trackBy: trackEdges\"\n edge\n [model]=\"model\"\n [edgeTemplate]=\"edgeTemplateDirective?.templateRef\"\n [edgeLabelHtmlTemplate]=\"edgeLabelHtmlDirective?.templateRef\"\n />\n\n <!-- Nodes -->\n <svg:g\n *ngFor=\"let model of nodeModels(); trackBy: trackNodes\"\n node\n [nodeModel]=\"model\"\n [nodeTemplate]=\"nodeTemplateDirective?.templateRef\"\n [groupNodeTemplate]=\"groupNodeTemplateDirective?.templateRef\"\n [attr.transform]=\"model.pointTransform()\"\n />\n </svg:g>\n\n <!-- Minimap -->\n <ng-container *ngIf=\"minimap() as minimap\">\n <ng-container [ngTemplateOutlet]=\"minimap.template()\" />\n </ng-container>\n</svg:svg>\n\n", styles: [":host{display:block;width:100%;height:100%;-webkit-user-select:none;user-select:none}:host ::ng-deep *{box-sizing:border-box}\n"], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: NodeComponent, selector: "g[node]", inputs: ["nodeModel", "nodeTemplate", "groupNodeTemplate"] }, { kind: "component", type: EdgeComponent, selector: "g[edge]", inputs: ["model", "edgeTemplate", "edgeLabelHtmlTemplate"] }, { kind: "component", type: ConnectionComponent, selector: "g[connection]", inputs: ["model", "template"] }, { kind: "component", type: DefsComponent, selector: "defs[flowDefs]", inputs: ["markers"] }, { kind: "component", type: BackgroundComponent, selector: "g[background]" }, { kind: "directive", type: SpacePointContextDirective, selector: "g[spacePointContext]" }, { kind: "directive", type: MapContextDirective, selector: "g[mapContext]" }, { kind: "directive", type: RootSvgReferenceDirective, selector: "svg[rootSvgRef]" }, { kind: "directive", type: RootSvgContextDirective, selector: "svg[rootSvgContext]" }, { kind: "directive", type: RootPointerDirective, selector: "svg[rootPointer]" }, { kind: "directive", type: FlowSizeControllerDirective, selector: "svg[flowSizeController]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
3212
+ ], queries: [{ propertyName: "nodeTemplateDirective", first: true, predicate: NodeHtmlTemplateDirective, descendants: true }, { propertyName: "groupNodeTemplateDirective", first: true, predicate: GroupNodeTemplateDirective, descendants: true }, { propertyName: "edgeTemplateDirective", first: true, predicate: EdgeTemplateDirective, descendants: true }, { propertyName: "edgeLabelHtmlDirective", first: true, predicate: EdgeLabelHtmlTemplateDirective, descendants: true }, { propertyName: "connectionTemplateDirective", first: true, predicate: ConnectionTemplateDirective, descendants: true }], viewQueries: [{ propertyName: "mapContext", first: true, predicate: MapContextDirective, descendants: true }, { propertyName: "spacePointContext", first: true, predicate: SpacePointContextDirective, descendants: true }], hostDirectives: [{ directive: ConnectionControllerDirective, outputs: ["onConnect", "onConnect"] }, { directive: ChangesControllerDirective, outputs: ["onNodesChange", "onNodesChange", "onNodesChange.position", "onNodesChange.position", "onNodesChange.position.single", "onNodesChange.position.single", "onNodesChange.position.many", "onNodesChange.position.many", "onNodesChange.size", "onNodesChange.size", "onNodesChange.size.single", "onNodesChange.size.single", "onNodesChange.size.many", "onNodesChange.size.many", "onNodesChange.add", "onNodesChange.add", "onNodesChange.add.single", "onNodesChange.add.single", "onNodesChange.add.many", "onNodesChange.add.many", "onNodesChange.remove", "onNodesChange.remove", "onNodesChange.remove.single", "onNodesChange.remove.single", "onNodesChange.remove.many", "onNodesChange.remove.many", "onNodesChange.select", "onNodesChange.select", "onNodesChange.select.single", "onNodesChange.select.single", "onNodesChange.select.many", "onNodesChange.select.many", "onEdgesChange", "onEdgesChange", "onEdgesChange.detached", "onEdgesChange.detached", "onEdgesChange.detached.single", "onEdgesChange.detached.single", "onEdgesChange.detached.many", "onEdgesChange.detached.many", "onEdgesChange.add", "onEdgesChange.add", "onEdgesChange.add.single", "onEdgesChange.add.single", "onEdgesChange.add.many", "onEdgesChange.add.many", "onEdgesChange.remove", "onEdgesChange.remove", "onEdgesChange.remove.single", "onEdgesChange.remove.single", "onEdgesChange.remove.many", "onEdgesChange.remove.many", "onEdgesChange.select", "onEdgesChange.select", "onEdgesChange.select.single", "onEdgesChange.select.single", "onEdgesChange.select.many", "onEdgesChange.select.many"] }], ngImport: i0, template: "<svg:svg\n rootSvgRef\n rootSvgContext\n rootPointer\n flowSizeController\n class=\"root-svg\"\n #flow\n>\n <defs [markers]=\"markers()\" flowDefs />\n\n <g background />\n\n <svg:g\n mapContext\n spacePointContext\n >\n <!-- Connection -->\n <svg:g\n connection\n [model]=\"connection\"\n [template]=\"connectionTemplateDirective?.templateRef\"\n />\n\n <ng-container *ngIf=\"optimization.detachedGroupsLayer\">\n <!-- Groups -->\n <svg:g\n *ngFor=\"let model of groups(); trackBy: trackNodes\"\n node\n [nodeModel]=\"model\"\n [groupNodeTemplate]=\"groupNodeTemplateDirective?.templateRef\"\n [attr.transform]=\"model.pointTransform()\"\n />\n\n <!-- Edges -->\n <svg:g\n *ngFor=\"let model of edgeModels(); trackBy: trackEdges\"\n edge\n [model]=\"model\"\n [edgeTemplate]=\"edgeTemplateDirective?.templateRef\"\n [edgeLabelHtmlTemplate]=\"edgeLabelHtmlDirective?.templateRef\"\n />\n\n <!-- Nodes -->\n <svg:g\n *ngFor=\"let model of nonGroups(); trackBy: trackNodes\"\n node\n [nodeModel]=\"model\"\n [nodeTemplate]=\"nodeTemplateDirective?.templateRef\"\n [attr.transform]=\"model.pointTransform()\"\n />\n </ng-container>\n\n <ng-container *ngIf=\"!optimization.detachedGroupsLayer\">\n <!-- Edges -->\n <svg:g\n *ngFor=\"let model of edgeModels(); trackBy: trackEdges\"\n edge\n [model]=\"model\"\n [edgeTemplate]=\"edgeTemplateDirective?.templateRef\"\n [edgeLabelHtmlTemplate]=\"edgeLabelHtmlDirective?.templateRef\"\n />\n\n <!-- Nodes -->\n <svg:g\n *ngFor=\"let model of nodeModels(); trackBy: trackNodes\"\n node\n [nodeModel]=\"model\"\n [nodeTemplate]=\"nodeTemplateDirective?.templateRef\"\n [groupNodeTemplate]=\"groupNodeTemplateDirective?.templateRef\"\n [attr.transform]=\"model.pointTransform()\"\n />\n </ng-container>\n\n </svg:g>\n\n <!-- Minimap -->\n <ng-container *ngIf=\"minimap() as minimap\">\n <ng-container [ngTemplateOutlet]=\"minimap.template()\" />\n </ng-container>\n</svg:svg>\n\n", styles: [":host{display:block;width:100%;height:100%;-webkit-user-select:none;user-select:none}:host ::ng-deep *{box-sizing:border-box}\n"], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: NodeComponent, selector: "g[node]", inputs: ["nodeModel", "nodeTemplate", "groupNodeTemplate"] }, { kind: "component", type: EdgeComponent, selector: "g[edge]", inputs: ["model", "edgeTemplate", "edgeLabelHtmlTemplate"] }, { kind: "component", type: ConnectionComponent, selector: "g[connection]", inputs: ["model", "template"] }, { kind: "component", type: DefsComponent, selector: "defs[flowDefs]", inputs: ["markers"] }, { kind: "component", type: BackgroundComponent, selector: "g[background]" }, { kind: "directive", type: SpacePointContextDirective, selector: "g[spacePointContext]" }, { kind: "directive", type: MapContextDirective, selector: "g[mapContext]" }, { kind: "directive", type: RootSvgReferenceDirective, selector: "svg[rootSvgRef]" }, { kind: "directive", type: RootSvgContextDirective, selector: "svg[rootSvgContext]" }, { kind: "directive", type: RootPointerDirective, selector: "svg[rootPointer]" }, { kind: "directive", type: FlowSizeControllerDirective, selector: "svg[flowSizeController]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2956
3213
  }
2957
3214
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: VflowComponent, decorators: [{
2958
3215
  type: Component,
@@ -2972,7 +3229,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
2972
3229
  ], hostDirectives: [
2973
3230
  connectionControllerHostDirective,
2974
3231
  changesControllerHostDirective
2975
- ], template: "<svg:svg\n rootSvgRef\n rootSvgContext\n rootPointer\n flowSizeController\n class=\"root-svg\"\n #flow\n>\n <defs [markers]=\"markers()\" flowDefs />\n\n <g background />\n\n <svg:g\n mapContext\n spacePointContext\n >\n <!-- Connection -->\n <svg:g\n connection\n [model]=\"connection\"\n [template]=\"connectionTemplateDirective?.templateRef\"\n />\n\n <!-- Edges -->\n <svg:g\n *ngFor=\"let model of edgeModels(); trackBy: trackEdges\"\n edge\n [model]=\"model\"\n [edgeTemplate]=\"edgeTemplateDirective?.templateRef\"\n [edgeLabelHtmlTemplate]=\"edgeLabelHtmlDirective?.templateRef\"\n />\n\n <!-- Nodes -->\n <svg:g\n *ngFor=\"let model of nodeModels(); trackBy: trackNodes\"\n node\n [nodeModel]=\"model\"\n [nodeTemplate]=\"nodeTemplateDirective?.templateRef\"\n [groupNodeTemplate]=\"groupNodeTemplateDirective?.templateRef\"\n [attr.transform]=\"model.pointTransform()\"\n />\n </svg:g>\n\n <!-- Minimap -->\n <ng-container *ngIf=\"minimap() as minimap\">\n <ng-container [ngTemplateOutlet]=\"minimap.template()\" />\n </ng-container>\n</svg:svg>\n\n", styles: [":host{display:block;width:100%;height:100%;-webkit-user-select:none;user-select:none}:host ::ng-deep *{box-sizing:border-box}\n"] }]
3232
+ ], template: "<svg:svg\n rootSvgRef\n rootSvgContext\n rootPointer\n flowSizeController\n class=\"root-svg\"\n #flow\n>\n <defs [markers]=\"markers()\" flowDefs />\n\n <g background />\n\n <svg:g\n mapContext\n spacePointContext\n >\n <!-- Connection -->\n <svg:g\n connection\n [model]=\"connection\"\n [template]=\"connectionTemplateDirective?.templateRef\"\n />\n\n <ng-container *ngIf=\"optimization.detachedGroupsLayer\">\n <!-- Groups -->\n <svg:g\n *ngFor=\"let model of groups(); trackBy: trackNodes\"\n node\n [nodeModel]=\"model\"\n [groupNodeTemplate]=\"groupNodeTemplateDirective?.templateRef\"\n [attr.transform]=\"model.pointTransform()\"\n />\n\n <!-- Edges -->\n <svg:g\n *ngFor=\"let model of edgeModels(); trackBy: trackEdges\"\n edge\n [model]=\"model\"\n [edgeTemplate]=\"edgeTemplateDirective?.templateRef\"\n [edgeLabelHtmlTemplate]=\"edgeLabelHtmlDirective?.templateRef\"\n />\n\n <!-- Nodes -->\n <svg:g\n *ngFor=\"let model of nonGroups(); trackBy: trackNodes\"\n node\n [nodeModel]=\"model\"\n [nodeTemplate]=\"nodeTemplateDirective?.templateRef\"\n [attr.transform]=\"model.pointTransform()\"\n />\n </ng-container>\n\n <ng-container *ngIf=\"!optimization.detachedGroupsLayer\">\n <!-- Edges -->\n <svg:g\n *ngFor=\"let model of edgeModels(); trackBy: trackEdges\"\n edge\n [model]=\"model\"\n [edgeTemplate]=\"edgeTemplateDirective?.templateRef\"\n [edgeLabelHtmlTemplate]=\"edgeLabelHtmlDirective?.templateRef\"\n />\n\n <!-- Nodes -->\n <svg:g\n *ngFor=\"let model of nodeModels(); trackBy: trackNodes\"\n node\n [nodeModel]=\"model\"\n [nodeTemplate]=\"nodeTemplateDirective?.templateRef\"\n [groupNodeTemplate]=\"groupNodeTemplateDirective?.templateRef\"\n [attr.transform]=\"model.pointTransform()\"\n />\n </ng-container>\n\n </svg:g>\n\n <!-- Minimap -->\n <ng-container *ngIf=\"minimap() as minimap\">\n <ng-container [ngTemplateOutlet]=\"minimap.template()\" />\n </ng-container>\n</svg:svg>\n\n", styles: [":host{display:block;width:100%;height:100%;-webkit-user-select:none;user-select:none}:host ::ng-deep *{box-sizing:border-box}\n"] }]
2976
3233
  }], propDecorators: { view: [{
2977
3234
  type: Input
2978
3235
  }], minZoom: [{
@@ -3085,7 +3342,13 @@ class MiniMapComponent {
3085
3342
  }
3086
3343
  return 0.2;
3087
3344
  });
3088
- this.viewportColor = computed(() => this.flowSettingsService.background().color ?? '#fff');
3345
+ this.viewportColor = computed(() => {
3346
+ const bg = this.flowSettingsService.background();
3347
+ if (bg.type === 'dots' || bg.type === 'solid') {
3348
+ return bg.color ?? '#fff';
3349
+ }
3350
+ return '#fff';
3351
+ });
3089
3352
  this.hovered = signal(false);
3090
3353
  this.minimapPoint = computed(() => {
3091
3354
  switch (this.minimapPosition()) {