ngx-vflow 1.11.0 → 1.12.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.
Files changed (49) hide show
  1. package/esm2022/lib/vflow/components/background/background.component.mjs +4 -3
  2. package/esm2022/lib/vflow/components/node/node.component.mjs +3 -1
  3. package/esm2022/lib/vflow/components/preview-flow/draw-node.mjs +100 -0
  4. package/esm2022/lib/vflow/components/preview-flow/preview-flow.component.mjs +62 -0
  5. package/esm2022/lib/vflow/components/vflow/vflow.component.mjs +26 -12
  6. package/esm2022/lib/vflow/directives/map-context.directive.mjs +28 -18
  7. package/esm2022/lib/vflow/directives/space-point-context.directive.mjs +3 -3
  8. package/esm2022/lib/vflow/interfaces/node-preview.interface.mjs +2 -0
  9. package/esm2022/lib/vflow/interfaces/node.interface.mjs +1 -1
  10. package/esm2022/lib/vflow/interfaces/optimization.interface.mjs +6 -2
  11. package/esm2022/lib/vflow/models/edge.model.mjs +42 -15
  12. package/esm2022/lib/vflow/models/handle.model.mjs +3 -1
  13. package/esm2022/lib/vflow/models/node.model.mjs +6 -1
  14. package/esm2022/lib/vflow/services/edge-rendering.service.mjs +14 -2
  15. package/esm2022/lib/vflow/services/flow-settings.service.mjs +3 -1
  16. package/esm2022/lib/vflow/services/node-rendering.service.mjs +35 -2
  17. package/esm2022/lib/vflow/services/preview-flow-render-strategy.service.mjs +21 -0
  18. package/esm2022/lib/vflow/services/viewport.service.mjs +8 -1
  19. package/esm2022/lib/vflow/utils/assert-injector.mjs +27 -0
  20. package/esm2022/lib/vflow/utils/nodes.mjs +3 -3
  21. package/esm2022/lib/vflow/utils/signals/extended-computed.mjs +15 -0
  22. package/esm2022/lib/vflow/utils/signals/to-lazy-signal.mjs +35 -0
  23. package/esm2022/lib/vflow/utils/viewport.mjs +37 -1
  24. package/esm2022/public-api.mjs +2 -1
  25. package/esm2022/testing/component-mocks/vflow-mock.component.mjs +7 -7
  26. package/fesm2022/ngx-vflow-testing.mjs +6 -6
  27. package/fesm2022/ngx-vflow-testing.mjs.map +1 -1
  28. package/fesm2022/ngx-vflow.mjs +438 -48
  29. package/fesm2022/ngx-vflow.mjs.map +1 -1
  30. package/lib/vflow/components/preview-flow/draw-node.d.ts +2 -0
  31. package/lib/vflow/components/preview-flow/preview-flow.component.d.ts +15 -0
  32. package/lib/vflow/components/vflow/vflow.component.d.ts +5 -2
  33. package/lib/vflow/directives/map-context.directive.d.ts +3 -2
  34. package/lib/vflow/interfaces/node-preview.interface.d.ts +3 -0
  35. package/lib/vflow/interfaces/node.interface.d.ts +3 -0
  36. package/lib/vflow/interfaces/optimization.interface.d.ts +17 -1
  37. package/lib/vflow/models/node.model.d.ts +3 -0
  38. package/lib/vflow/services/edge-rendering.service.d.ts +2 -0
  39. package/lib/vflow/services/flow-settings.service.d.ts +2 -0
  40. package/lib/vflow/services/node-rendering.service.d.ts +4 -0
  41. package/lib/vflow/services/preview-flow-render-strategy.service.d.ts +12 -0
  42. package/lib/vflow/services/viewport.service.d.ts +3 -0
  43. package/lib/vflow/utils/assert-injector.d.ts +44 -0
  44. package/lib/vflow/utils/signals/extended-computed.d.ts +5 -0
  45. package/lib/vflow/utils/signals/to-lazy-signal.d.ts +20 -0
  46. package/lib/vflow/utils/viewport.d.ts +19 -0
  47. package/package.json +1 -1
  48. package/public-api.d.ts +1 -0
  49. package/testing/component-mocks/vflow-mock.component.d.ts +3 -3
@@ -1,8 +1,8 @@
1
1
  import * as i0 from '@angular/core';
2
- import { signal, computed, Injectable, inject, ElementRef, Directive, effect, untracked, TemplateRef, DestroyRef, EventEmitter, OutputEmitterRef, input, NgZone, viewChild, Component, ChangeDetectionStrategy, output, HostListener, Injector, runInInjectionContext, contentChild, Input, forwardRef } from '@angular/core';
2
+ import { signal, computed, Injectable, inject, ElementRef, Directive, NgZone, effect, untracked, TemplateRef, DestroyRef, EventEmitter, OutputEmitterRef, input, assertInInjectionContext, Injector, runInInjectionContext, viewChild, Component, ChangeDetectionStrategy, output, HostListener, Renderer2, contentChild, Input, forwardRef } from '@angular/core';
3
3
  import { select } from 'd3-selection';
4
4
  import { zoomIdentity, zoom } from 'd3-zoom';
5
- import { switchMap, merge, fromEvent, tap, Subject, Observable, skip, map, pairwise, filter, distinctUntilChanged, observeOn, asyncScheduler, zip, animationFrameScheduler, share, startWith } from 'rxjs';
5
+ import { Subject, switchMap, merge, fromEvent, tap, Observable, skip, map, pairwise, filter, distinctUntilChanged, observeOn, asyncScheduler, zip, debounceTime, animationFrameScheduler, share, startWith } from 'rxjs';
6
6
  import { toObservable, takeUntilDestroyed, outputFromObservable, toSignal } from '@angular/core/rxjs-interop';
7
7
  import { drag } from 'd3-drag';
8
8
  import { __decorate } from 'tslib';
@@ -50,8 +50,8 @@ function nodeToBox(node) {
50
50
  }
51
51
  function nodeToRect(node) {
52
52
  return {
53
- x: node.point().x,
54
- y: node.point().y,
53
+ x: node.globalPoint().x,
54
+ y: node.globalPoint().y,
55
55
  width: node.width(),
56
56
  height: node.height(),
57
57
  };
@@ -177,6 +177,48 @@ function getViewportForBounds(bounds, width, height, minZoom, maxZoom, padding)
177
177
  function clamp(value, min = 0, max = 1) {
178
178
  return Math.min(Math.max(value, min), max);
179
179
  }
180
+ /**
181
+ * Calculates the visible area bounds in world coordinates based on the current viewport state
182
+ *
183
+ * @param viewport Current viewport state (x, y, zoom)
184
+ * @param flowWidth Width of the flow container
185
+ * @param flowHeight Height of the flow container
186
+ * @returns Rect representing the visible area in world coordinates
187
+ */
188
+ function getViewportBounds(viewport, flowWidth, flowHeight) {
189
+ const zoom = viewport.zoom;
190
+ return {
191
+ x: -viewport.x / zoom,
192
+ y: -viewport.y / zoom,
193
+ width: flowWidth / zoom,
194
+ height: flowHeight / zoom,
195
+ };
196
+ }
197
+ /**
198
+ * Checks if a rectangle intersects with the viewport's visible area
199
+ *
200
+ * @param rect Rectangle to check (in world coordinates)
201
+ * @param viewport Current viewport state
202
+ * @param flowWidth Width of the flow container
203
+ * @param flowHeight Height of the flow container
204
+ * @returns true if the rectangle intersects with the viewport, false otherwise
205
+ */
206
+ function isRectInViewport(rect, viewport, flowWidth, flowHeight) {
207
+ const viewportBounds = getViewportBounds(viewport, flowWidth, flowHeight);
208
+ // Check if rectangles intersect using standard rectangle intersection test
209
+ // No intersection if: rect is completely to the left, right, above, or below the viewport
210
+ const isNotIntersecting = rect.x + rect.width < viewportBounds.x || // Rect is completely to the left
211
+ rect.x > viewportBounds.x + viewportBounds.width || // Rect is completely to the right
212
+ rect.y + rect.height < viewportBounds.y || // Rect is completely above
213
+ rect.y > viewportBounds.y + viewportBounds.height; // Rect is completely below
214
+ return !isNotIntersecting;
215
+ }
216
+
217
+ const DEFAULT_OPTIMIZATION = {
218
+ detachedGroupsLayer: false,
219
+ virtualization: false,
220
+ virtualizationZoomThreshold: 0.5,
221
+ };
180
222
 
181
223
  class FlowSettingsService {
182
224
  constructor() {
@@ -199,6 +241,7 @@ class FlowSettingsService {
199
241
  this.maxZoom = signal(3);
200
242
  this.background = signal({ type: 'solid', color: '#fff' });
201
243
  this.snapGrid = signal([1, 1]);
244
+ this.optimization = signal(DEFAULT_OPTIMIZATION);
202
245
  }
203
246
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FlowSettingsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
204
247
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FlowSettingsService }); }
@@ -226,6 +269,7 @@ class ViewportService {
226
269
  * - writableViewport signal
227
270
  */
228
271
  this.readableViewport = signal(ViewportService.getDefaultViewport());
272
+ this.viewportChangeEnd$ = new Subject();
229
273
  }
230
274
  /**
231
275
  * The default value used by d3, just copy it here
@@ -242,6 +286,11 @@ class ViewportService {
242
286
  const duration = options.duration ?? 0;
243
287
  this.writableViewport.set({ changeType: 'absolute', state, duration });
244
288
  }
289
+ triggerViewportChangeEvent(type) {
290
+ if (type === 'end') {
291
+ this.viewportChangeEnd$.next();
292
+ }
293
+ }
245
294
  getBoundsNodes(nodeIds) {
246
295
  return !nodeIds?.length
247
296
  ? // If nodes option not passed or the list is empty, then get fit the whole view
@@ -398,8 +447,9 @@ class MapContextDirective {
398
447
  this.selectionService = inject(SelectionService);
399
448
  this.viewportService = inject(ViewportService);
400
449
  this.flowSettingsService = inject(FlowSettingsService);
450
+ this.zone = inject(NgZone);
401
451
  this.rootSvgSelection = select(this.rootSvg);
402
- this.zoomableSelection = select(this.host);
452
+ this.transform = signal('');
403
453
  this.viewportForSelection = {};
404
454
  // under the hood this effect triggers handleZoom, so error throws without this flag
405
455
  this.manualViewportChangeEffect = effect(() => {
@@ -435,7 +485,7 @@ class MapContextDirective {
435
485
  this.handleZoom = ({ transform }) => {
436
486
  // update public signal for user to read
437
487
  this.viewportService.readableViewport.set(mapTransformToViewportState(transform));
438
- this.zoomableSelection.attr('transform', transform.toString());
488
+ this.transform.set(transform.toString());
439
489
  };
440
490
  this.handleZoomStart = ({ transform }) => {
441
491
  this.viewportForSelection = {
@@ -443,12 +493,16 @@ class MapContextDirective {
443
493
  };
444
494
  };
445
495
  this.handleZoomEnd = ({ transform, sourceEvent }) => {
446
- this.viewportForSelection = {
447
- ...this.viewportForSelection,
448
- end: mapTransformToViewportState(transform),
449
- target: evTarget(sourceEvent),
450
- };
451
- this.selectionService.setViewport(this.viewportForSelection);
496
+ this.zone.run(() => {
497
+ this.viewportForSelection = {
498
+ ...this.viewportForSelection,
499
+ end: mapTransformToViewportState(transform),
500
+ target: evTarget(sourceEvent),
501
+ };
502
+ this.viewportService.triggerViewportChangeEvent('end');
503
+ // TODO: maybe use triggerViewportChangeEvent instead of this method?
504
+ this.selectionService.setViewport(this.viewportForSelection);
505
+ });
452
506
  };
453
507
  this.filterCondition = (event) => {
454
508
  if (event.type === 'mousedown' || event.type === 'touchstart') {
@@ -458,22 +512,27 @@ class MapContextDirective {
458
512
  };
459
513
  }
460
514
  ngOnInit() {
461
- this.zoomBehavior = zoom()
462
- .scaleExtent([this.flowSettingsService.minZoom(), this.flowSettingsService.maxZoom()])
463
- .filter(this.filterCondition)
464
- .on('start', this.handleZoomStart)
465
- .on('zoom', this.handleZoom)
466
- .on('end', this.handleZoomEnd);
467
- this.rootSvgSelection.call(this.zoomBehavior).on('dblclick.zoom', null);
515
+ this.zone.runOutsideAngular(() => {
516
+ this.zoomBehavior = zoom()
517
+ .scaleExtent([this.flowSettingsService.minZoom(), this.flowSettingsService.maxZoom()])
518
+ .filter(this.filterCondition)
519
+ .on('start', this.handleZoomStart)
520
+ .on('zoom', this.handleZoom)
521
+ .on('end', this.handleZoomEnd);
522
+ this.rootSvgSelection.call(this.zoomBehavior).on('dblclick.zoom', null);
523
+ });
468
524
  }
469
525
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MapContextDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
470
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "17.3.12", type: MapContextDirective, isStandalone: true, selector: "g[mapContext]", ngImport: i0 }); }
526
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "17.3.12", type: MapContextDirective, isStandalone: true, selector: "g[mapContext]", host: { properties: { "attr.transform": "transform()" } }, ngImport: i0 }); }
471
527
  }
472
528
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MapContextDirective, decorators: [{
473
529
  type: Directive,
474
530
  args: [{
475
531
  standalone: true,
476
532
  selector: 'g[mapContext]',
533
+ host: {
534
+ '[attr.transform]': 'transform()',
535
+ },
477
536
  }]
478
537
  }] });
479
538
  const mapTransformToViewportState = (transform) => ({
@@ -966,6 +1025,7 @@ class NodeModel {
966
1025
  constructor(rawNode) {
967
1026
  this.rawNode = rawNode;
968
1027
  this.entitiesService = inject(FlowEntitiesService);
1028
+ this.isVisible = signal(false);
969
1029
  this.point = signal({ x: 0, y: 0 });
970
1030
  this.width = signal(NodeModel.defaultWidth);
971
1031
  this.height = signal(NodeModel.defaultHeight);
@@ -979,6 +1039,7 @@ class NodeModel {
979
1039
  this.foHeight = computed(() => this.height() + MAGIC_NUMBER_TO_FIX_GLITCH_IN_CHROME);
980
1040
  this.renderOrder = signal(0);
981
1041
  this.selected = signal(false);
1042
+ this.preview = signal({ style: {} });
982
1043
  this.globalPoint = computed(() => {
983
1044
  let parent = this.parent();
984
1045
  let x = this.point().x;
@@ -1030,6 +1091,9 @@ class NodeModel {
1030
1091
  if (internalNode.parentId) {
1031
1092
  this.parentId = internalNode.parentId;
1032
1093
  }
1094
+ if (internalNode.preview) {
1095
+ this.preview = internalNode.preview;
1096
+ }
1033
1097
  if (internalNode.type === 'default-group' && internalNode.color) {
1034
1098
  this.color = internalNode.color;
1035
1099
  }
@@ -1345,6 +1409,20 @@ function smoothStepPath({ sourcePoint, targetPoint, sourcePosition, targetPositi
1345
1409
  };
1346
1410
  }
1347
1411
 
1412
+ // MIT License
1413
+ /**
1414
+ * @todo Use `linkedSignal` after Angular update
1415
+ */
1416
+ function extendedComputed(computedCallback, options) {
1417
+ if (!options) {
1418
+ options = { equal: Object.is };
1419
+ }
1420
+ let currentValue = undefined;
1421
+ return computed(() => {
1422
+ return (currentValue = computedCallback(currentValue));
1423
+ }, options);
1424
+ }
1425
+
1348
1426
  class EdgeModel {
1349
1427
  constructor(edge) {
1350
1428
  this.edge = edge;
@@ -1405,25 +1483,51 @@ class EdgeModel {
1405
1483
  return this.curve(params);
1406
1484
  }
1407
1485
  });
1408
- this.sourceHandle = computed(() => {
1486
+ this.sourceHandle = extendedComputed((previousHandle) => {
1487
+ let handle = null;
1409
1488
  if (this.edge.sourceHandle) {
1410
- return (this.source()
1411
- ?.handles()
1412
- .find((handle) => handle.rawHandle.id === this.edge.sourceHandle) ?? null);
1489
+ handle =
1490
+ this.source()
1491
+ ?.handles()
1492
+ .find((handle) => handle.rawHandle.id === this.edge.sourceHandle) ?? null;
1413
1493
  }
1414
- return (this.source()
1415
- ?.handles()
1416
- .find((handle) => handle.rawHandle.type === 'source') ?? null);
1494
+ else {
1495
+ handle =
1496
+ this.source()
1497
+ ?.handles()
1498
+ .find((handle) => handle.rawHandle.type === 'source') ?? null;
1499
+ }
1500
+ // In case of virtual scrolling, if the node is scrolled out of view the handle may disappear
1501
+ // which could lead to the edge not being rendered
1502
+ // so we return the previous handle if the current one is null
1503
+ // TODO: check if this breaks anything
1504
+ if (handle === null) {
1505
+ return previousHandle;
1506
+ }
1507
+ return handle;
1417
1508
  });
1418
- this.targetHandle = computed(() => {
1509
+ this.targetHandle = extendedComputed((previousHandle) => {
1510
+ let handle = null;
1419
1511
  if (this.edge.targetHandle) {
1420
- return (this.target()
1421
- ?.handles()
1422
- .find((handle) => handle.rawHandle.id === this.edge.targetHandle) ?? null);
1512
+ handle =
1513
+ this.target()
1514
+ ?.handles()
1515
+ .find((handle) => handle.rawHandle.id === this.edge.targetHandle) ?? null;
1423
1516
  }
1424
- return (this.target()
1425
- ?.handles()
1426
- .find((handle) => handle.rawHandle.type === 'target') ?? null);
1517
+ else {
1518
+ handle =
1519
+ this.target()
1520
+ ?.handles()
1521
+ .find((handle) => handle.rawHandle.type === 'target') ?? null;
1522
+ }
1523
+ // In case of virtual scrolling, if the node is scrolled out of view the handle may disappear
1524
+ // which could lead to the edge not being rendered
1525
+ // so we return the previous handle if the current one is null
1526
+ // TODO: check if this breaks anything
1527
+ if (handle === null) {
1528
+ return previousHandle;
1529
+ }
1530
+ return handle;
1427
1531
  });
1428
1532
  /**
1429
1533
  * TODO: not reactive
@@ -1737,11 +1841,74 @@ function isGroupNode(node) {
1737
1841
  return node.rawNode.type === 'default-group' || node.rawNode.type === 'template-group';
1738
1842
  }
1739
1843
 
1844
+ // MIT License
1845
+ // Copyright (c) 2023 Chau Tran
1846
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
1847
+ // of this software and associated documentation files (the "Software"), to deal
1848
+ // in the Software without restriction, including without limitation the rights
1849
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1850
+ // copies of the Software, and to permit persons to whom the Software is
1851
+ // furnished to do so, subject to the following conditions:
1852
+ // The above copyright notice and this permission notice shall be included in all
1853
+ // copies or substantial portions of the Software.
1854
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1855
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1856
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1857
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1858
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1859
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1860
+ // SOFTWARE.
1861
+ /* eslint-disable @typescript-eslint/ban-types */
1862
+ function assertInjector(fn, injector, runner) {
1863
+ !injector && assertInInjectionContext(fn);
1864
+ const assertedInjector = injector ?? inject(Injector);
1865
+ if (!runner)
1866
+ return assertedInjector;
1867
+ return runInInjectionContext(assertedInjector, runner);
1868
+ }
1869
+
1870
+ // MIT License
1871
+ // Copyright (c) 2023 Chau Tran
1872
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
1873
+ // of this software and associated documentation files (the "Software"), to deal
1874
+ // in the Software without restriction, including without limitation the rights
1875
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1876
+ // copies of the Software, and to permit persons to whom the Software is
1877
+ // furnished to do so, subject to the following conditions:
1878
+ // The above copyright notice and this permission notice shall be included in all
1879
+ // copies or substantial portions of the Software.
1880
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1881
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1882
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1883
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1884
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1885
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1886
+ // SOFTWARE.
1887
+ /**
1888
+ * Function `toLazySignal()` is a proxy function that will call the original
1889
+ * `toSignal()` function when the returned signal is read for the first time.
1890
+ */
1891
+ function toLazySignal(source, options) {
1892
+ const injector = assertInjector(toLazySignal, options?.injector);
1893
+ let s;
1894
+ return computed(() => {
1895
+ if (!s) {
1896
+ s = untracked(() => toSignal(source, { ...options, injector }));
1897
+ }
1898
+ return s();
1899
+ });
1900
+ }
1901
+
1740
1902
  class NodeRenderingService {
1741
1903
  constructor() {
1742
1904
  this.flowEntitiesService = inject(FlowEntitiesService);
1905
+ this.flowSettingsService = inject(FlowSettingsService);
1906
+ this.viewportService = inject(ViewportService);
1743
1907
  this.nodes = computed(() => {
1744
- return [...this.flowEntitiesService.nodes().sort((aNode, bNode) => aNode.renderOrder() - bNode.renderOrder())];
1908
+ if (!this.flowSettingsService.optimization().virtualization) {
1909
+ return [...this.flowEntitiesService.nodes()].sort((aNode, bNode) => aNode.renderOrder() - bNode.renderOrder());
1910
+ }
1911
+ return this.viewportNodesAfterInteraction().sort((aNode, bNode) => aNode.renderOrder() - bNode.renderOrder());
1745
1912
  });
1746
1913
  this.groups = computed(() => {
1747
1914
  return this.nodes().filter((n) => isGroupNode(n));
@@ -1749,6 +1916,28 @@ class NodeRenderingService {
1749
1916
  this.nonGroups = computed(() => {
1750
1917
  return this.nodes().filter((n) => !isGroupNode(n));
1751
1918
  });
1919
+ this.viewportNodes = computed(() => {
1920
+ const nodes = this.flowEntitiesService.nodes();
1921
+ const viewport = this.viewportService.readableViewport();
1922
+ const flowWidth = this.flowSettingsService.computedFlowWidth();
1923
+ const flowHeight = this.flowSettingsService.computedFlowHeight();
1924
+ return nodes.filter((n) => {
1925
+ const { x, y } = n.globalPoint();
1926
+ const width = n.width();
1927
+ const height = n.height();
1928
+ return isRectInViewport({ x, y, width, height }, viewport, flowWidth, flowHeight);
1929
+ });
1930
+ });
1931
+ this.viewportNodesAfterInteraction = toLazySignal(merge(
1932
+ // TODO: maybe there is a better way wait when viewport is ready?
1933
+ // (to correctly calculate viewport nodes on first render)
1934
+ toObservable(this.flowEntitiesService.nodes).pipe(observeOn(asyncScheduler), filter((nodes) => !!nodes.length)), this.viewportService.viewportChangeEnd$.pipe(debounceTime(300))).pipe(map(() => {
1935
+ const viewport = this.viewportService.readableViewport();
1936
+ const zoomThreshold = this.flowSettingsService.optimization().virtualizationZoomThreshold;
1937
+ return viewport.zoom < zoomThreshold ? [] : this.viewportNodes();
1938
+ })), {
1939
+ initialValue: [],
1940
+ });
1752
1941
  this.maxOrder = computed(() => {
1753
1942
  return Math.max(...this.flowEntitiesService.nodes().map((n) => n.renderOrder()));
1754
1943
  });
@@ -1839,7 +2028,7 @@ class SpacePointContextDirective {
1839
2028
  y: movement.y,
1840
2029
  });
1841
2030
  });
1842
- this.pointerMovement = toSignal(this.pointerMovementDirective.pointerMovement$);
2031
+ this.pointerMovement = toLazySignal(this.pointerMovementDirective.pointerMovement$);
1843
2032
  }
1844
2033
  documentPointToFlowPoint(documentPoint) {
1845
2034
  const point = this.rootSvg.createSVGPoint();
@@ -2144,8 +2333,19 @@ function statusToConnection(status, isStrictMode) {
2144
2333
  class EdgeRenderingService {
2145
2334
  constructor() {
2146
2335
  this.flowEntitiesService = inject(FlowEntitiesService);
2336
+ this.flowSettingsService = inject(FlowSettingsService);
2147
2337
  this.edges = computed(() => {
2148
- return this.flowEntitiesService.validEdges().sort((aNode, bNode) => aNode.renderOrder() - bNode.renderOrder());
2338
+ if (!this.flowSettingsService.optimization().virtualization) {
2339
+ return [...this.flowEntitiesService.validEdges()].sort((aEdge, bEdge) => aEdge.renderOrder() - bEdge.renderOrder());
2340
+ }
2341
+ return this.viewportEdges().sort((aEdge, bEdge) => aEdge.renderOrder() - bEdge.renderOrder());
2342
+ });
2343
+ this.viewportEdges = computed(() => {
2344
+ return this.flowEntitiesService.validEdges().filter((e) => {
2345
+ const sourceHandle = e.sourceHandle();
2346
+ const targetHandle = e.targetHandle();
2347
+ return sourceHandle && targetHandle;
2348
+ });
2149
2349
  });
2150
2350
  this.maxOrder = computed(() => {
2151
2351
  return Math.max(...this.flowEntitiesService.validEdges().map((n) => n.renderOrder()));
@@ -2645,9 +2845,11 @@ class HandleModel {
2645
2845
  });
2646
2846
  this.state = signal('idle');
2647
2847
  this.updateHostSizeAndPosition$ = new Subject();
2848
+ // TODO: for some reason toLazySignal breaks unit tests, so we use toSignal here
2648
2849
  this.hostSize = toSignal(this.updateHostSizeAndPosition$.pipe(map(() => this.getHostSize())), {
2649
2850
  initialValue: { width: 0, height: 0 },
2650
2851
  });
2852
+ // TODO: for some reason toLazySignal breaks unit tests, so we use toSignal here
2651
2853
  this.hostPosition = toSignal(this.updateHostSizeAndPosition$.pipe(map(() => ({
2652
2854
  x: this.hostReference instanceof HTMLElement ? this.hostReference.offsetLeft : 0, // for now just 0 for group nodes
2653
2855
  y: this.hostReference instanceof HTMLElement ? this.hostReference.offsetTop : 0, // for now just 0 for group nodes
@@ -2846,6 +3048,7 @@ class NodeComponent {
2846
3048
  this.toolbars = computed(() => this.overlaysService.nodeToolbarsMap().get(this.model()));
2847
3049
  }
2848
3050
  ngOnInit() {
3051
+ this.model().isVisible.set(true);
2849
3052
  this.nodeAccessor.model.set(this.model());
2850
3053
  this.handleService.node.set(this.model());
2851
3054
  effect(() => {
@@ -2858,6 +3061,7 @@ class NodeComponent {
2858
3061
  }, { injector: this.injector });
2859
3062
  }
2860
3063
  ngOnDestroy() {
3064
+ this.model().isVisible.set(false);
2861
3065
  this.draggableService.destroy(this.hostRef.nativeElement);
2862
3066
  }
2863
3067
  startConnection(event, handle) {
@@ -3098,7 +3302,7 @@ class BackgroundComponent {
3098
3302
  const background = this.backgroundSignal();
3099
3303
  return background.type === 'image' ? background.src : '';
3100
3304
  });
3101
- this.imageSize = toSignal(toObservable(this.backgroundSignal).pipe(switchMap(() => createImage(this.bgImageSrc())), map((image) => ({ width: image.naturalWidth, height: image.naturalHeight }))), { initialValue: { width: 0, height: 0 } });
3305
+ this.imageSize = toLazySignal(toObservable(this.backgroundSignal).pipe(switchMap(() => createImage(this.bgImageSrc())), map((image) => ({ width: image.naturalWidth, height: image.naturalHeight }))), { initialValue: { width: 0, height: 0 } });
3102
3306
  this.scaledImageWidth = computed(() => {
3103
3307
  const background = this.backgroundSignal();
3104
3308
  if (background.type === 'image') {
@@ -3272,6 +3476,181 @@ function getSpacePoints(point, groups) {
3272
3476
  return result;
3273
3477
  }
3274
3478
 
3479
+ class PreviewFlowRenderStrategyService {
3480
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PreviewFlowRenderStrategyService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3481
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PreviewFlowRenderStrategyService }); }
3482
+ }
3483
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PreviewFlowRenderStrategyService, decorators: [{
3484
+ type: Injectable
3485
+ }] });
3486
+ class ViewportPreviewFlowRenderStrategyService extends PreviewFlowRenderStrategyService {
3487
+ shouldRenderNode(node) {
3488
+ // Do not render preview node if the real node is visible
3489
+ return !node.isVisible();
3490
+ }
3491
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ViewportPreviewFlowRenderStrategyService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
3492
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ViewportPreviewFlowRenderStrategyService }); }
3493
+ }
3494
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ViewportPreviewFlowRenderStrategyService, decorators: [{
3495
+ type: Injectable
3496
+ }] });
3497
+
3498
+ function drawNode(ctx, node) {
3499
+ if (Object.keys(node.preview().style).length) {
3500
+ drawStyledNode(ctx, node);
3501
+ return;
3502
+ }
3503
+ if (node.rawNode.type === 'default') {
3504
+ drawDefaultNode(ctx, node);
3505
+ return;
3506
+ }
3507
+ if (node.rawNode.type === 'default-group') {
3508
+ drawDefaultGroupNode(ctx, node);
3509
+ return;
3510
+ }
3511
+ drawUnknownNode(ctx, node);
3512
+ }
3513
+ function drawDefaultNode(ctx, node) {
3514
+ const point = node.globalPoint();
3515
+ const width = node.width();
3516
+ const height = node.height();
3517
+ borderRadius(ctx, node, 5);
3518
+ // Draw background (background-color: white)
3519
+ ctx.fillStyle = 'white';
3520
+ ctx.fill();
3521
+ // Draw border (border: 1.5px solid #1b262c)
3522
+ ctx.strokeStyle = '#1b262c';
3523
+ ctx.lineWidth = 1.5;
3524
+ ctx.stroke();
3525
+ // Draw centered text (color: black, justify-content: center)
3526
+ ctx.fillStyle = 'black';
3527
+ // TODO: use as in default node
3528
+ ctx.font = '14px Arial';
3529
+ ctx.textAlign = 'center';
3530
+ ctx.textBaseline = 'middle';
3531
+ const centerX = point.x + width / 2;
3532
+ const centerY = point.y + height / 2;
3533
+ ctx.fillText(node.text(), centerX, centerY);
3534
+ }
3535
+ function drawDefaultGroupNode(ctx, node) {
3536
+ const point = node.globalPoint();
3537
+ const width = node.width();
3538
+ const height = node.height();
3539
+ ctx.globalAlpha = 0.05;
3540
+ ctx.fillStyle = node.color();
3541
+ ctx.fillRect(point.x, point.y, width, height);
3542
+ ctx.globalAlpha = 1;
3543
+ ctx.strokeStyle = node.color();
3544
+ ctx.lineWidth = 1.5;
3545
+ ctx.strokeRect(point.x, point.y, width, height);
3546
+ }
3547
+ function drawStyledNode(ctx, node) {
3548
+ const point = node.globalPoint();
3549
+ const width = node.width();
3550
+ const height = node.height();
3551
+ const style = node.preview().style;
3552
+ if (style.borderRadius) {
3553
+ const radius = parseFloat(style.borderRadius);
3554
+ borderRadius(ctx, node, radius);
3555
+ }
3556
+ else {
3557
+ ctx.beginPath();
3558
+ ctx.rect(point.x, point.y, width, height);
3559
+ ctx.closePath();
3560
+ }
3561
+ if (style.backgroundColor) {
3562
+ ctx.fillStyle = style.backgroundColor;
3563
+ }
3564
+ if (style.borderColor) {
3565
+ ctx.strokeStyle = style.borderColor;
3566
+ }
3567
+ if (style.borderWidth) {
3568
+ ctx.lineWidth = parseFloat(style.borderWidth);
3569
+ }
3570
+ ctx.fill();
3571
+ ctx.stroke();
3572
+ }
3573
+ function drawUnknownNode(ctx, node) {
3574
+ const point = node.globalPoint();
3575
+ const width = node.width();
3576
+ const height = node.height();
3577
+ ctx.fillStyle = 'rgb(0 0 0 / 10%)';
3578
+ ctx.fillRect(point.x, point.y, width, height);
3579
+ }
3580
+ function borderRadius(ctx, node, radius) {
3581
+ const point = node.globalPoint();
3582
+ const width = node.width();
3583
+ const height = node.height();
3584
+ // Create rounded rectangle path
3585
+ ctx.beginPath();
3586
+ ctx.moveTo(point.x + radius, point.y);
3587
+ ctx.lineTo(point.x + width - radius, point.y);
3588
+ ctx.quadraticCurveTo(point.x + width, point.y, point.x + width, point.y + radius);
3589
+ ctx.lineTo(point.x + width, point.y + height - radius);
3590
+ ctx.quadraticCurveTo(point.x + width, point.y + height, point.x + width - radius, point.y + height);
3591
+ ctx.lineTo(point.x + radius, point.y + height);
3592
+ ctx.quadraticCurveTo(point.x, point.y + height, point.x, point.y + height - radius);
3593
+ ctx.lineTo(point.x, point.y + radius);
3594
+ ctx.quadraticCurveTo(point.x, point.y, point.x + radius, point.y);
3595
+ ctx.closePath();
3596
+ }
3597
+
3598
+ class PreviewFlowComponent {
3599
+ constructor() {
3600
+ this.viewportService = inject(ViewportService);
3601
+ this.renderStrategy = inject(PreviewFlowRenderStrategyService);
3602
+ this.nodeRenderingService = inject(NodeRenderingService);
3603
+ this.renderer2 = inject(Renderer2);
3604
+ this.element = inject(ElementRef).nativeElement;
3605
+ this.ctx = this.element.getContext('2d');
3606
+ this.width = input(0);
3607
+ this.height = input(0);
3608
+ this.dpr = window.devicePixelRatio;
3609
+ effect(() => {
3610
+ // Set the "actual" size of the canvas
3611
+ this.renderer2.setProperty(this.element, 'width', this.width() * this.dpr);
3612
+ this.renderer2.setProperty(this.element, 'height', this.height() * this.dpr);
3613
+ // Set the "drawn" size of the canvas
3614
+ this.renderer2.setStyle(this.element, 'width', `${this.width()}px`);
3615
+ this.renderer2.setStyle(this.element, 'height', `${this.height()}px`);
3616
+ // Scale the context to match device pixel ratio
3617
+ this.ctx.scale(this.dpr, this.dpr);
3618
+ });
3619
+ effect(() => {
3620
+ const viewport = this.viewportService.readableViewport();
3621
+ this.ctx.clearRect(0, 0, this.width(), this.height());
3622
+ // Save the current context state
3623
+ this.ctx.save();
3624
+ // Apply viewport transformations (zoom and pan)
3625
+ this.ctx.setTransform(viewport.zoom * this.dpr, // horizontal scaling with DPR
3626
+ 0, // horizontal skewing
3627
+ 0, // vertical skewing
3628
+ viewport.zoom * this.dpr, // vertical scaling with DPR
3629
+ viewport.x * this.dpr, // horizontal translation with DPR
3630
+ viewport.y * this.dpr);
3631
+ for (let i = 0; i < this.nodeRenderingService.viewportNodes().length; i++) {
3632
+ const node = this.nodeRenderingService.viewportNodes()[i];
3633
+ if (this.renderStrategy.shouldRenderNode(node)) {
3634
+ drawNode(this.ctx, node);
3635
+ }
3636
+ }
3637
+ // Restore the context state
3638
+ this.ctx.restore();
3639
+ });
3640
+ }
3641
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PreviewFlowComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3642
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "17.3.12", type: PreviewFlowComponent, isStandalone: true, selector: "canvas[previewFlow]", inputs: { width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
3643
+ }
3644
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PreviewFlowComponent, decorators: [{
3645
+ type: Component,
3646
+ args: [{
3647
+ standalone: true,
3648
+ changeDetection: ChangeDetectionStrategy.OnPush,
3649
+ selector: 'canvas[previewFlow]',
3650
+ template: '',
3651
+ }]
3652
+ }], ctorParameters: () => [] });
3653
+
3275
3654
  const changesControllerHostDirective = {
3276
3655
  directive: ChangesControllerDirective,
3277
3656
  outputs: [
@@ -3319,9 +3698,6 @@ class VflowComponent {
3319
3698
  this.componentEventBusService = inject(ComponentEventBusService);
3320
3699
  this.keyboardService = inject(KeyboardService);
3321
3700
  this.injector = inject(Injector);
3322
- this.optimization = input({
3323
- detachedGroupsLayer: false,
3324
- });
3325
3701
  this.nodeModels = this.nodeRenderingService.nodes;
3326
3702
  this.groups = this.nodeRenderingService.groups;
3327
3703
  this.nonGroups = this.nodeRenderingService.nonGroups;
@@ -3355,13 +3731,13 @@ class VflowComponent {
3355
3731
  /**
3356
3732
  * Signal for reading nodes change
3357
3733
  */
3358
- this.nodesChange = toSignal(this.nodesChangeService.changes$, {
3734
+ this.nodesChange = toLazySignal(this.nodesChangeService.changes$, {
3359
3735
  initialValue: [],
3360
3736
  });
3361
3737
  /**
3362
3738
  * Signal to reading edges change
3363
3739
  */
3364
- this.edgesChange = toSignal(this.edgesChangeService.changes$, {
3740
+ this.edgesChange = toLazySignal(this.edgesChangeService.changes$, {
3365
3741
  initialValue: [],
3366
3742
  });
3367
3743
  // #endregion
@@ -3381,6 +3757,9 @@ class VflowComponent {
3381
3757
  // #endregion
3382
3758
  this.markers = this.flowEntitiesService.markers;
3383
3759
  this.minimap = this.flowEntitiesService.minimap;
3760
+ this.flowOptimization = this.flowSettingsService.optimization;
3761
+ this.flowWidth = this.flowSettingsService.computedFlowWidth;
3762
+ this.flowHeight = this.flowSettingsService.computedFlowHeight;
3384
3763
  }
3385
3764
  // #endregion
3386
3765
  // #region SETTINGS
@@ -3412,6 +3791,12 @@ class VflowComponent {
3412
3791
  set background(value) {
3413
3792
  this.flowSettingsService.background.set(transformBackground(value));
3414
3793
  }
3794
+ set optimization(newOptimization) {
3795
+ this.flowSettingsService.optimization.update((optimization) => ({
3796
+ ...optimization,
3797
+ ...newOptimization,
3798
+ }));
3799
+ }
3415
3800
  /**
3416
3801
  * Global rule if you can or can't select entities
3417
3802
  */
@@ -3469,7 +3854,7 @@ class VflowComponent {
3469
3854
  set edges(newEdges) {
3470
3855
  const newModels = runInInjectionContext(this.injector, () => ReferenceIdentityChecker.edges(newEdges, this.flowEntitiesService.edges()));
3471
3856
  // quick and dirty binding nodes to edges
3472
- addNodesToEdges(this.nodeModels(), newModels);
3857
+ addNodesToEdges(this.flowEntitiesService.nodes(), newModels);
3473
3858
  this.flowEntitiesService.edges.set(newModels);
3474
3859
  }
3475
3860
  // #region METHODS_API
@@ -3572,7 +3957,7 @@ class VflowComponent {
3572
3957
  return edge;
3573
3958
  }
3574
3959
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: VflowComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3575
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: VflowComponent, isStandalone: true, selector: "vflow", inputs: { view: { classPropertyName: "view", publicName: "view", isSignal: false, isRequired: false, transformFunction: null }, minZoom: { classPropertyName: "minZoom", publicName: "minZoom", isSignal: false, isRequired: false, transformFunction: null }, maxZoom: { classPropertyName: "maxZoom", publicName: "maxZoom", isSignal: false, isRequired: false, transformFunction: null }, background: { classPropertyName: "background", publicName: "background", isSignal: false, isRequired: false, transformFunction: null }, optimization: { classPropertyName: "optimization", publicName: "optimization", isSignal: true, isRequired: false, transformFunction: null }, entitiesSelectable: { classPropertyName: "entitiesSelectable", publicName: "entitiesSelectable", isSignal: false, isRequired: false, transformFunction: null }, keyboardShortcuts: { classPropertyName: "keyboardShortcuts", publicName: "keyboardShortcuts", isSignal: false, isRequired: false, transformFunction: null }, connection: { classPropertyName: "connection", publicName: "connection", isSignal: false, isRequired: false, transformFunction: (settings) => new ConnectionModel(settings) }, snapGrid: { classPropertyName: "snapGrid", publicName: "snapGrid", isSignal: false, isRequired: false, transformFunction: null }, elevateNodesOnSelect: { classPropertyName: "elevateNodesOnSelect", publicName: "elevateNodesOnSelect", isSignal: false, isRequired: false, transformFunction: null }, elevateEdgesOnSelect: { classPropertyName: "elevateEdgesOnSelect", publicName: "elevateEdgesOnSelect", isSignal: false, isRequired: false, transformFunction: null }, nodes: { classPropertyName: "nodes", publicName: "nodes", isSignal: false, isRequired: true, transformFunction: null }, edges: { classPropertyName: "edges", publicName: "edges", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { onComponentNodeEvent: "onComponentNodeEvent" }, providers: [
3960
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: VflowComponent, isStandalone: true, selector: "vflow", inputs: { view: "view", minZoom: "minZoom", maxZoom: "maxZoom", background: "background", optimization: "optimization", entitiesSelectable: "entitiesSelectable", keyboardShortcuts: "keyboardShortcuts", connection: ["connection", "connection", (settings) => new ConnectionModel(settings)], snapGrid: "snapGrid", elevateNodesOnSelect: "elevateNodesOnSelect", elevateEdgesOnSelect: "elevateEdgesOnSelect", nodes: "nodes", edges: "edges" }, outputs: { onComponentNodeEvent: "onComponentNodeEvent" }, providers: [
3576
3961
  DraggableService,
3577
3962
  ViewportService,
3578
3963
  FlowStatusService,
@@ -3586,7 +3971,8 @@ class VflowComponent {
3586
3971
  ComponentEventBusService,
3587
3972
  KeyboardService,
3588
3973
  OverlaysService,
3589
- ], queries: [{ propertyName: "nodeTemplateDirective", first: true, predicate: NodeHtmlTemplateDirective, descendants: true, isSignal: true }, { propertyName: "nodeSvgTemplateDirective", first: true, predicate: NodeSvgTemplateDirective, descendants: true, isSignal: true }, { propertyName: "groupNodeTemplateDirective", first: true, predicate: GroupNodeTemplateDirective, descendants: true, isSignal: true }, { propertyName: "edgeTemplateDirective", first: true, predicate: EdgeTemplateDirective, descendants: true, isSignal: true }, { propertyName: "edgeLabelHtmlDirective", first: true, predicate: EdgeLabelHtmlTemplateDirective, descendants: true, isSignal: true }, { propertyName: "connectionTemplateDirective", first: true, predicate: ConnectionTemplateDirective, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "mapContext", first: true, predicate: MapContextDirective, descendants: true, isSignal: true }, { propertyName: "spacePointContext", first: true, predicate: SpacePointContextDirective, descendants: true, isSignal: true }], hostDirectives: [{ 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 #flow rootSvgRef rootSvgContext rootPointer flowSizeController class=\"root-svg\">\n <defs flowDefs [markers]=\"markers()\" />\n\n <g background />\n\n <svg:g mapContext spacePointContext>\n <!-- Connection -->\n <svg:g connection [model]=\"connection\" [template]=\"connectionTemplateDirective()?.templateRef\" />\n\n @if (optimization().detachedGroupsLayer) {\n <!-- Groups -->\n @for (model of groups(); track trackNodes($index, model)) {\n <svg:g\n node\n [model]=\"model\"\n [groupNodeTemplate]=\"groupNodeTemplateDirective()?.templateRef\"\n [attr.transform]=\"model.pointTransform()\" />\n }\n <!-- Edges -->\n @for (model of edgeModels(); track trackEdges($index, model)) {\n <svg:g\n edge\n [model]=\"model\"\n [edgeTemplate]=\"edgeTemplateDirective()?.templateRef\"\n [edgeLabelHtmlTemplate]=\"edgeLabelHtmlDirective()?.templateRef\" />\n }\n <!-- Nodes -->\n @for (model of nonGroups(); track trackNodes($index, model)) {\n <svg:g\n node\n [model]=\"model\"\n [nodeTemplate]=\"nodeTemplateDirective()?.templateRef\"\n [nodeSvgTemplate]=\"nodeSvgTemplateDirective()?.templateRef\"\n [attr.transform]=\"model.pointTransform()\" />\n }\n }\n\n @if (!optimization().detachedGroupsLayer) {\n <!-- Edges -->\n @for (model of edgeModels(); track trackEdges($index, model)) {\n <svg:g\n edge\n [model]=\"model\"\n [edgeTemplate]=\"edgeTemplateDirective()?.templateRef\"\n [edgeLabelHtmlTemplate]=\"edgeLabelHtmlDirective()?.templateRef\" />\n }\n\n <!-- Nodes -->\n @for (model of nodeModels(); track trackNodes($index, model)) {\n <svg:g\n node\n [model]=\"model\"\n [nodeTemplate]=\"nodeTemplateDirective()?.templateRef\"\n [nodeSvgTemplate]=\"nodeSvgTemplateDirective()?.templateRef\"\n [groupNodeTemplate]=\"groupNodeTemplateDirective()?.templateRef\"\n [attr.transform]=\"model.pointTransform()\" />\n }\n }\n </svg:g>\n\n <!-- Minimap -->\n @if (minimap(); as minimap) {\n <ng-container [ngTemplateOutlet]=\"minimap.template()\" />\n }\n</svg:svg>\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: 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]" }, { kind: "component", type: DefsComponent, selector: "defs[flowDefs]", inputs: ["markers"] }, { kind: "component", type: BackgroundComponent, selector: "g[background]" }, { kind: "directive", type: MapContextDirective, selector: "g[mapContext]" }, { kind: "directive", type: SpacePointContextDirective, selector: "g[spacePointContext]" }, { kind: "component", type: ConnectionComponent, selector: "g[connection]", inputs: ["model", "template"] }, { kind: "component", type: NodeComponent, selector: "g[node]", inputs: ["model", "nodeTemplate", "nodeSvgTemplate", "groupNodeTemplate"] }, { kind: "component", type: EdgeComponent, selector: "g[edge]", inputs: ["model", "edgeTemplate", "edgeLabelHtmlTemplate"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
3974
+ { provide: PreviewFlowRenderStrategyService, useClass: ViewportPreviewFlowRenderStrategyService },
3975
+ ], queries: [{ propertyName: "nodeTemplateDirective", first: true, predicate: NodeHtmlTemplateDirective, descendants: true, isSignal: true }, { propertyName: "nodeSvgTemplateDirective", first: true, predicate: NodeSvgTemplateDirective, descendants: true, isSignal: true }, { propertyName: "groupNodeTemplateDirective", first: true, predicate: GroupNodeTemplateDirective, descendants: true, isSignal: true }, { propertyName: "edgeTemplateDirective", first: true, predicate: EdgeTemplateDirective, descendants: true, isSignal: true }, { propertyName: "edgeLabelHtmlDirective", first: true, predicate: EdgeLabelHtmlTemplateDirective, descendants: true, isSignal: true }, { propertyName: "connectionTemplateDirective", first: true, predicate: ConnectionTemplateDirective, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "mapContext", first: true, predicate: MapContextDirective, descendants: true, isSignal: true }, { propertyName: "spacePointContext", first: true, predicate: SpacePointContextDirective, descendants: true, isSignal: true }], hostDirectives: [{ 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 #flow rootSvgRef rootSvgContext rootPointer flowSizeController class=\"root-svg\">\n <defs flowDefs [markers]=\"markers()\" />\n\n <g background />\n\n <svg:g mapContext spacePointContext>\n <!-- Connection -->\n <svg:g connection [model]=\"connection\" [template]=\"connectionTemplateDirective()?.templateRef\" />\n\n @if (flowOptimization().detachedGroupsLayer) {\n <!-- Groups -->\n @for (model of groups(); track trackNodes($index, model)) {\n <svg:g\n node\n [model]=\"model\"\n [groupNodeTemplate]=\"groupNodeTemplateDirective()?.templateRef\"\n [attr.transform]=\"model.pointTransform()\" />\n }\n <!-- Edges -->\n @for (model of edgeModels(); track trackEdges($index, model)) {\n <svg:g\n edge\n [model]=\"model\"\n [edgeTemplate]=\"edgeTemplateDirective()?.templateRef\"\n [edgeLabelHtmlTemplate]=\"edgeLabelHtmlDirective()?.templateRef\" />\n }\n <!-- Nodes -->\n @for (model of nonGroups(); track trackNodes($index, model)) {\n <svg:g\n node\n [model]=\"model\"\n [nodeTemplate]=\"nodeTemplateDirective()?.templateRef\"\n [nodeSvgTemplate]=\"nodeSvgTemplateDirective()?.templateRef\"\n [attr.transform]=\"model.pointTransform()\" />\n }\n }\n\n @if (!flowOptimization().detachedGroupsLayer) {\n <!-- Edges -->\n @for (model of edgeModels(); track trackEdges($index, model)) {\n <svg:g\n edge\n [model]=\"model\"\n [edgeTemplate]=\"edgeTemplateDirective()?.templateRef\"\n [edgeLabelHtmlTemplate]=\"edgeLabelHtmlDirective()?.templateRef\" />\n }\n\n @for (model of nodeModels(); track trackNodes($index, model)) {\n <svg:g\n node\n [model]=\"model\"\n [nodeTemplate]=\"nodeTemplateDirective()?.templateRef\"\n [nodeSvgTemplate]=\"nodeSvgTemplateDirective()?.templateRef\"\n [groupNodeTemplate]=\"groupNodeTemplateDirective()?.templateRef\"\n [attr.transform]=\"model.pointTransform()\" />\n }\n }\n </svg:g>\n\n <!-- Minimap -->\n @if (minimap(); as minimap) {\n <ng-container [ngTemplateOutlet]=\"minimap.template()\" />\n }\n</svg:svg>\n\n@if (flowOptimization().virtualization) {\n <canvas previewFlow class=\"preview-flow\" [width]=\"flowWidth()\" [height]=\"flowHeight()\"></canvas>\n}\n", styles: [":host{display:grid;grid-template-columns:1fr;width:100%;height:100%;-webkit-user-select:none;user-select:none}:host ::ng-deep *{box-sizing:border-box}.root-svg{grid-row-start:1;grid-column-start:1}.preview-flow{pointer-events:none;grid-row-start:1;grid-column-start:1}\n"], dependencies: [{ 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]" }, { kind: "component", type: DefsComponent, selector: "defs[flowDefs]", inputs: ["markers"] }, { kind: "component", type: BackgroundComponent, selector: "g[background]" }, { kind: "directive", type: MapContextDirective, selector: "g[mapContext]" }, { kind: "directive", type: SpacePointContextDirective, selector: "g[spacePointContext]" }, { kind: "component", type: ConnectionComponent, selector: "g[connection]", inputs: ["model", "template"] }, { kind: "component", type: NodeComponent, selector: "g[node]", inputs: ["model", "nodeTemplate", "nodeSvgTemplate", "groupNodeTemplate"] }, { kind: "component", type: EdgeComponent, selector: "g[edge]", inputs: ["model", "edgeTemplate", "edgeLabelHtmlTemplate"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: PreviewFlowComponent, selector: "canvas[previewFlow]", inputs: ["width", "height"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
3590
3976
  }
3591
3977
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: VflowComponent, decorators: [{
3592
3978
  type: Component,
@@ -3604,6 +3990,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
3604
3990
  ComponentEventBusService,
3605
3991
  KeyboardService,
3606
3992
  OverlaysService,
3993
+ { provide: PreviewFlowRenderStrategyService, useClass: ViewportPreviewFlowRenderStrategyService },
3607
3994
  ], hostDirectives: [changesControllerHostDirective], imports: [
3608
3995
  RootSvgReferenceDirective,
3609
3996
  RootSvgContextDirective,
@@ -3617,7 +4004,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
3617
4004
  NodeComponent,
3618
4005
  EdgeComponent,
3619
4006
  NgTemplateOutlet,
3620
- ], template: "<svg:svg #flow rootSvgRef rootSvgContext rootPointer flowSizeController class=\"root-svg\">\n <defs flowDefs [markers]=\"markers()\" />\n\n <g background />\n\n <svg:g mapContext spacePointContext>\n <!-- Connection -->\n <svg:g connection [model]=\"connection\" [template]=\"connectionTemplateDirective()?.templateRef\" />\n\n @if (optimization().detachedGroupsLayer) {\n <!-- Groups -->\n @for (model of groups(); track trackNodes($index, model)) {\n <svg:g\n node\n [model]=\"model\"\n [groupNodeTemplate]=\"groupNodeTemplateDirective()?.templateRef\"\n [attr.transform]=\"model.pointTransform()\" />\n }\n <!-- Edges -->\n @for (model of edgeModels(); track trackEdges($index, model)) {\n <svg:g\n edge\n [model]=\"model\"\n [edgeTemplate]=\"edgeTemplateDirective()?.templateRef\"\n [edgeLabelHtmlTemplate]=\"edgeLabelHtmlDirective()?.templateRef\" />\n }\n <!-- Nodes -->\n @for (model of nonGroups(); track trackNodes($index, model)) {\n <svg:g\n node\n [model]=\"model\"\n [nodeTemplate]=\"nodeTemplateDirective()?.templateRef\"\n [nodeSvgTemplate]=\"nodeSvgTemplateDirective()?.templateRef\"\n [attr.transform]=\"model.pointTransform()\" />\n }\n }\n\n @if (!optimization().detachedGroupsLayer) {\n <!-- Edges -->\n @for (model of edgeModels(); track trackEdges($index, model)) {\n <svg:g\n edge\n [model]=\"model\"\n [edgeTemplate]=\"edgeTemplateDirective()?.templateRef\"\n [edgeLabelHtmlTemplate]=\"edgeLabelHtmlDirective()?.templateRef\" />\n }\n\n <!-- Nodes -->\n @for (model of nodeModels(); track trackNodes($index, model)) {\n <svg:g\n node\n [model]=\"model\"\n [nodeTemplate]=\"nodeTemplateDirective()?.templateRef\"\n [nodeSvgTemplate]=\"nodeSvgTemplateDirective()?.templateRef\"\n [groupNodeTemplate]=\"groupNodeTemplateDirective()?.templateRef\"\n [attr.transform]=\"model.pointTransform()\" />\n }\n }\n </svg:g>\n\n <!-- Minimap -->\n @if (minimap(); as minimap) {\n <ng-container [ngTemplateOutlet]=\"minimap.template()\" />\n }\n</svg:svg>\n", styles: [":host{display:block;width:100%;height:100%;-webkit-user-select:none;user-select:none}:host ::ng-deep *{box-sizing:border-box}\n"] }]
4007
+ PreviewFlowComponent,
4008
+ ], template: "<svg:svg #flow rootSvgRef rootSvgContext rootPointer flowSizeController class=\"root-svg\">\n <defs flowDefs [markers]=\"markers()\" />\n\n <g background />\n\n <svg:g mapContext spacePointContext>\n <!-- Connection -->\n <svg:g connection [model]=\"connection\" [template]=\"connectionTemplateDirective()?.templateRef\" />\n\n @if (flowOptimization().detachedGroupsLayer) {\n <!-- Groups -->\n @for (model of groups(); track trackNodes($index, model)) {\n <svg:g\n node\n [model]=\"model\"\n [groupNodeTemplate]=\"groupNodeTemplateDirective()?.templateRef\"\n [attr.transform]=\"model.pointTransform()\" />\n }\n <!-- Edges -->\n @for (model of edgeModels(); track trackEdges($index, model)) {\n <svg:g\n edge\n [model]=\"model\"\n [edgeTemplate]=\"edgeTemplateDirective()?.templateRef\"\n [edgeLabelHtmlTemplate]=\"edgeLabelHtmlDirective()?.templateRef\" />\n }\n <!-- Nodes -->\n @for (model of nonGroups(); track trackNodes($index, model)) {\n <svg:g\n node\n [model]=\"model\"\n [nodeTemplate]=\"nodeTemplateDirective()?.templateRef\"\n [nodeSvgTemplate]=\"nodeSvgTemplateDirective()?.templateRef\"\n [attr.transform]=\"model.pointTransform()\" />\n }\n }\n\n @if (!flowOptimization().detachedGroupsLayer) {\n <!-- Edges -->\n @for (model of edgeModels(); track trackEdges($index, model)) {\n <svg:g\n edge\n [model]=\"model\"\n [edgeTemplate]=\"edgeTemplateDirective()?.templateRef\"\n [edgeLabelHtmlTemplate]=\"edgeLabelHtmlDirective()?.templateRef\" />\n }\n\n @for (model of nodeModels(); track trackNodes($index, model)) {\n <svg:g\n node\n [model]=\"model\"\n [nodeTemplate]=\"nodeTemplateDirective()?.templateRef\"\n [nodeSvgTemplate]=\"nodeSvgTemplateDirective()?.templateRef\"\n [groupNodeTemplate]=\"groupNodeTemplateDirective()?.templateRef\"\n [attr.transform]=\"model.pointTransform()\" />\n }\n }\n </svg:g>\n\n <!-- Minimap -->\n @if (minimap(); as minimap) {\n <ng-container [ngTemplateOutlet]=\"minimap.template()\" />\n }\n</svg:svg>\n\n@if (flowOptimization().virtualization) {\n <canvas previewFlow class=\"preview-flow\" [width]=\"flowWidth()\" [height]=\"flowHeight()\"></canvas>\n}\n", styles: [":host{display:grid;grid-template-columns:1fr;width:100%;height:100%;-webkit-user-select:none;user-select:none}:host ::ng-deep *{box-sizing:border-box}.root-svg{grid-row-start:1;grid-column-start:1}.preview-flow{pointer-events:none;grid-row-start:1;grid-column-start:1}\n"] }]
3621
4009
  }], propDecorators: { view: [{
3622
4010
  type: Input
3623
4011
  }], minZoom: [{
@@ -3626,6 +4014,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
3626
4014
  type: Input
3627
4015
  }], background: [{
3628
4016
  type: Input
4017
+ }], optimization: [{
4018
+ type: Input
3629
4019
  }], entitiesSelectable: [{
3630
4020
  type: Input
3631
4021
  }], keyboardShortcuts: [{
@@ -3966,5 +4356,5 @@ const Vflow = [
3966
4356
  * Generated bundle index. Do not edit.
3967
4357
  */
3968
4358
 
3969
- export { ChangesControllerDirective, ConnectionControllerDirective, ConnectionTemplateDirective, CustomDynamicNodeComponent, CustomNodeComponent, CustomTemplateEdgeComponent, DragHandleDirective, EdgeLabelHtmlTemplateDirective, EdgeTemplateDirective, GroupNodeTemplateDirective, HandleComponent, HandleTemplateDirective, MiniMapComponent, NodeHtmlTemplateDirective, NodeSvgTemplateDirective, NodeToolbarComponent, NodeToolbarWrapperDirective, ResizableComponent, SelectableDirective, Vflow, VflowComponent, isComponentDynamicNode, isComponentStaticNode, isDefaultDynamicGroupNode, isDefaultDynamicNode, isDefaultStaticGroupNode, isDefaultStaticNode, isDynamicNode, isStaticNode, isSvgTemplateDynamicNode, isSvgTemplateStaticNode, isTemplateDynamicGroupNode, isTemplateDynamicNode, isTemplateStaticGroupNode, isTemplateStaticNode, ComponentEventBusService as ɵComponentEventBusService, ConnectionModel as ɵConnectionModel, FlowEntitiesService as ɵFlowEntitiesService, FlowSettingsService as ɵFlowSettingsService, HandleModel as ɵHandleModel, HandleService as ɵHandleService, NodeAccessorService as ɵNodeAccessorService, NodeModel as ɵNodeModel, RootPointerDirective as ɵRootPointerDirective, SelectionService as ɵSelectionService, SpacePointContextDirective as ɵSpacePointContextDirective, ViewportService as ɵViewportService };
4359
+ export { ChangesControllerDirective, ConnectionControllerDirective, ConnectionTemplateDirective, CustomDynamicNodeComponent, CustomNodeComponent, CustomTemplateEdgeComponent, DEFAULT_OPTIMIZATION, DragHandleDirective, EdgeLabelHtmlTemplateDirective, EdgeTemplateDirective, GroupNodeTemplateDirective, HandleComponent, HandleTemplateDirective, MiniMapComponent, NodeHtmlTemplateDirective, NodeSvgTemplateDirective, NodeToolbarComponent, NodeToolbarWrapperDirective, ResizableComponent, SelectableDirective, Vflow, VflowComponent, isComponentDynamicNode, isComponentStaticNode, isDefaultDynamicGroupNode, isDefaultDynamicNode, isDefaultStaticGroupNode, isDefaultStaticNode, isDynamicNode, isStaticNode, isSvgTemplateDynamicNode, isSvgTemplateStaticNode, isTemplateDynamicGroupNode, isTemplateDynamicNode, isTemplateStaticGroupNode, isTemplateStaticNode, ComponentEventBusService as ɵComponentEventBusService, ConnectionModel as ɵConnectionModel, FlowEntitiesService as ɵFlowEntitiesService, FlowSettingsService as ɵFlowSettingsService, HandleModel as ɵHandleModel, HandleService as ɵHandleService, NodeAccessorService as ɵNodeAccessorService, NodeModel as ɵNodeModel, RootPointerDirective as ɵRootPointerDirective, SelectionService as ɵSelectionService, SpacePointContextDirective as ɵSpacePointContextDirective, ViewportService as ɵViewportService };
3970
4360
  //# sourceMappingURL=ngx-vflow.mjs.map