@ship-ui/core 0.15.18 → 0.15.19

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.
@@ -1,6 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, ElementRef, Renderer2, ChangeDetectionStrategy, Component, signal, DestroyRef, InjectionToken, input, computed, viewChild, effect, HostListener, NgModule, Injectable, DOCUMENT, model, output, ApplicationRef, createComponent, isSignal, OutputEmitterRef, contentChild, contentChildren, afterNextRender, assertInInjectionContext, Injector, HostBinding, TemplateRef, runInInjectionContext, Directive, ChangeDetectorRef, viewChildren, ViewContainerRef, EnvironmentInjector } from '@angular/core';
3
- import { DatePipe, NgTemplateOutlet } from '@angular/common';
2
+ import { inject, ElementRef, Renderer2, ChangeDetectionStrategy, Component, signal, DestroyRef, InjectionToken, input, computed, viewChild, effect, HostListener, NgModule, Injectable, DOCUMENT, model, output, ApplicationRef, createComponent, isSignal, OutputEmitterRef, PLATFORM_ID, ViewChild, contentChild, contentChildren, afterNextRender, assertInInjectionContext, Injector, HostBinding, TemplateRef, runInInjectionContext, Directive, ChangeDetectorRef, viewChildren, ViewContainerRef, EnvironmentInjector } from '@angular/core';
3
+ import { isPlatformBrowser, JsonPipe, DatePipe, NgTemplateOutlet } from '@angular/common';
4
+ import { ShipCardComponent as ShipCardComponent$1, ShipIconComponent as ShipIconComponent$1, ShipButtonComponent as ShipButtonComponent$1 } from 'ship-ui';
4
5
  import { NgModel } from '@angular/forms';
5
6
  import { SIGNAL } from '@angular/core/primitives/signals';
6
7
 
@@ -35,8 +36,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImpor
35
36
  }]
36
37
  }] });
37
38
 
38
- function classMutationSignal() {
39
- const element = inject(ElementRef).nativeElement;
39
+ function classMutationSignal(_element = null) {
40
+ const element = _element ?? inject(ElementRef).nativeElement;
40
41
  if (!element)
41
42
  return signal('');
42
43
  const classListSignal = signal(element.className, ...(ngDevMode ? [{ debugName: "classListSignal" }] : []));
@@ -559,6 +560,1041 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImpor
559
560
  }]
560
561
  }] });
561
562
 
563
+ function layoutNodes(nodes) {
564
+ if (!nodes || nodes.length === 0)
565
+ return nodes;
566
+ const layoutedNodes = structuredClone(nodes);
567
+ const visited = new Set();
568
+ const unconnectedNodes = [];
569
+ const connectedGraphs = [];
570
+ const allToNodes = new Set(layoutedNodes.flatMap((node) => node.connections).map((conn) => conn.toNode));
571
+ const rootNodes = layoutedNodes.filter((node) => !allToNodes.has(node.id));
572
+ for (const node of layoutedNodes) {
573
+ if (!node.connections.length) {
574
+ unconnectedNodes.push(node);
575
+ }
576
+ }
577
+ for (const root of rootNodes) {
578
+ if (!visited.has(root.id)) {
579
+ const graph = [];
580
+ traverseGraph(root, graph, visited, layoutedNodes);
581
+ if (graph.length > 0) {
582
+ connectedGraphs.push(graph);
583
+ }
584
+ }
585
+ }
586
+ positionUnconnectedNodes(unconnectedNodes);
587
+ let currentX = 20;
588
+ let currentY = 20;
589
+ connectedGraphs.forEach((graph) => {
590
+ const layoutedGraph = traverseAndLayout(graph);
591
+ positionGraph(layoutedGraph, currentX, currentY);
592
+ currentY += getGraphHeight(layoutedGraph) + 200;
593
+ });
594
+ return layoutedNodes;
595
+ }
596
+ function traverseGraph(startNode, graph, visited, allNodes) {
597
+ const queue = [startNode];
598
+ visited.add(startNode.id);
599
+ let head = 0;
600
+ while (head < queue.length) {
601
+ const node = queue[head++];
602
+ graph.push(node);
603
+ node.connections.forEach((conn) => {
604
+ const toNode = allNodes.find((n) => n.id === conn.toNode);
605
+ if (toNode && !visited.has(toNode.id)) {
606
+ visited.add(toNode.id);
607
+ queue.push(toNode);
608
+ }
609
+ });
610
+ }
611
+ }
612
+ function positionUnconnectedNodes(nodes) {
613
+ nodes.forEach((node, index) => {
614
+ node.coordinates[0] = 20 + index * 200;
615
+ node.coordinates[1] = 20;
616
+ });
617
+ }
618
+ function traverseAndLayout(graph) {
619
+ const graphNodeMap = new Map(graph.map((n) => [n.id, n]));
620
+ const incomingConnectionCount = new Map();
621
+ graph.forEach((node) => {
622
+ incomingConnectionCount.set(node.id, 0);
623
+ });
624
+ graph
625
+ .flatMap((n) => n.connections)
626
+ .forEach((conn) => {
627
+ if (graphNodeMap.has(conn.toNode)) {
628
+ incomingConnectionCount.set(conn.toNode, (incomingConnectionCount.get(conn.toNode) || 0) + 1);
629
+ }
630
+ });
631
+ const roots = graph.filter((node) => (incomingConnectionCount.get(node.id) || 0) === 0);
632
+ const layoutedNodes = new Set();
633
+ const queue = [...roots];
634
+ let head = 0;
635
+ while (head < queue.length) {
636
+ const currentNode = queue[head++];
637
+ layoutedNodes.add(currentNode.id);
638
+ let outputConnectionIndex = 0;
639
+ const connectedNodes = [];
640
+ currentNode.connections.forEach((conn) => {
641
+ const toNode = graphNodeMap.get(conn.toNode);
642
+ if (toNode && !layoutedNodes.has(toNode.id)) {
643
+ connectedNodes.push(toNode);
644
+ }
645
+ });
646
+ connectedNodes.forEach((toNode) => {
647
+ toNode.coordinates[0] = currentNode.coordinates[0] + 200;
648
+ if (connectedNodes.length > 1) {
649
+ const verticalOffset = (outputConnectionIndex - (connectedNodes.length - 1) / 2) * 200;
650
+ toNode.coordinates[1] = currentNode.coordinates[1] + verticalOffset;
651
+ }
652
+ else {
653
+ toNode.coordinates[1] = currentNode.coordinates[1];
654
+ }
655
+ queue.push(toNode);
656
+ outputConnectionIndex++;
657
+ });
658
+ }
659
+ return graph;
660
+ }
661
+ function positionGraph(graph, startX, startY) {
662
+ const minX = Math.min(...graph.map((n) => n.coordinates[0]));
663
+ const minY = Math.min(...graph.map((n) => n.coordinates[1]));
664
+ graph.forEach((node) => {
665
+ node.coordinates[0] += startX - minX;
666
+ node.coordinates[1] += startY - minY;
667
+ });
668
+ }
669
+ function getGraphHeight(graph) {
670
+ if (!graph.length)
671
+ return 0;
672
+ const maxY = Math.max(...graph.map((n) => n.coordinates[1]));
673
+ const minY = Math.min(...graph.map((n) => n.coordinates[1]));
674
+ return maxY - minY;
675
+ }
676
+
677
+ function findDuplicatePortIDs(nodes) {
678
+ const errors = [];
679
+ for (const node of nodes) {
680
+ const portIdCounts = new Map();
681
+ const allPorts = [...node.inputs, ...node.outputs];
682
+ for (const port of allPorts) {
683
+ const count = portIdCounts.get(port.id) || 0;
684
+ portIdCounts.set(port.id, count + 1);
685
+ }
686
+ const duplicatePortIds = [];
687
+ for (const [id, count] of portIdCounts.entries()) {
688
+ if (count > 1) {
689
+ duplicatePortIds.push(id);
690
+ }
691
+ }
692
+ if (duplicatePortIds.length > 0) {
693
+ errors.push({
694
+ nodeId: node.id,
695
+ duplicatePortIds: duplicatePortIds,
696
+ });
697
+ }
698
+ }
699
+ return errors;
700
+ }
701
+ function findDuplicateNodeIDs(nodes) {
702
+ const nodeIdCounts = new Map();
703
+ for (const node of nodes) {
704
+ const count = nodeIdCounts.get(node.id) || 0;
705
+ nodeIdCounts.set(node.id, count + 1);
706
+ }
707
+ const duplicateNodeIds = [];
708
+ for (const [id, count] of nodeIdCounts.entries()) {
709
+ if (count > 1) {
710
+ duplicateNodeIds.push(id);
711
+ }
712
+ }
713
+ return duplicateNodeIds;
714
+ }
715
+
716
+ const TEST_NODES = [
717
+ {
718
+ id: 'a1',
719
+ coordinates: [0, 0],
720
+ inputs: [],
721
+ outputs: [{ id: 'out-1', name: 'Start Output' }],
722
+ connections: [{ fromNode: 'a1', fromPort: 'out-1', toNode: 'b1', toPort: 'in-1' }],
723
+ },
724
+ {
725
+ id: 'b1',
726
+ coordinates: [0, 0],
727
+ inputs: [
728
+ { id: 'in-1', name: 'Input A' },
729
+ { id: 'in-1', name: 'Another Input A' },
730
+ ],
731
+ outputs: [{ id: 'out-1', name: 'Output B' }],
732
+ connections: [
733
+ { fromNode: 'b1', fromPort: 'out-1', toNode: 'c6', toPort: 'in-1' },
734
+ { fromNode: 'a1', fromPort: 'out-1', toNode: 'b1', toPort: 'in-1' },
735
+ ],
736
+ },
737
+ {
738
+ id: 'c6',
739
+ coordinates: [0, 0],
740
+ inputs: [{ id: 'in-1', name: 'Input C' }],
741
+ outputs: [{ id: 'out-1', name: 'Output D' }],
742
+ connections: [{ fromNode: 'b1', fromPort: 'out-1', toNode: 'c6', toPort: 'in-1' }],
743
+ },
744
+ ];
745
+ class ShipBlueprintComponent {
746
+ #ZOOM_SPEED;
747
+ #MAX_ZOOM;
748
+ #MIN_ZOOM;
749
+ #document;
750
+ #platformId;
751
+ #selfRef;
752
+ #currentClass;
753
+ #htmlClass;
754
+ #currentGridColor;
755
+ #draggingNodeCoordinates;
756
+ #isDragging;
757
+ #lastMouseX;
758
+ #lastMouseY;
759
+ #initialPinchDistance;
760
+ #isNodeDragging;
761
+ #draggedNodeId;
762
+ #dragOffset;
763
+ #connections;
764
+ #ctx;
765
+ #resizeObserver;
766
+ constructor() {
767
+ this.#ZOOM_SPEED = 0.01;
768
+ this.#MAX_ZOOM = 1.5;
769
+ this.#MIN_ZOOM = 0.5;
770
+ this.#document = inject(DOCUMENT);
771
+ this.#platformId = inject(PLATFORM_ID);
772
+ this.#selfRef = inject((ElementRef));
773
+ this.#currentClass = classMutationSignal();
774
+ this.#htmlClass = classMutationSignal(this.#document.documentElement);
775
+ this.asDots = computed(() => this.#currentClass().includes('dots'), ...(ngDevMode ? [{ debugName: "asDots" }] : []));
776
+ this.lightMode = computed(() => this.#htmlClass().includes('light'), ...(ngDevMode ? [{ debugName: "lightMode" }] : []));
777
+ this.forceUnique = input(true, ...(ngDevMode ? [{ debugName: "forceUnique" }] : []));
778
+ this.autoLayout = input(false, ...(ngDevMode ? [{ debugName: "autoLayout" }] : []));
779
+ this.gridSize = input(20, ...(ngDevMode ? [{ debugName: "gridSize" }] : []));
780
+ this.snapToGrid = input(true, ...(ngDevMode ? [{ debugName: "snapToGrid" }] : []));
781
+ this.gridColor = input(['#d8d8d8', '#2c2c2c'], ...(ngDevMode ? [{ debugName: "gridColor" }] : []));
782
+ this.nodes = model([], ...(ngDevMode ? [{ debugName: "nodes" }] : []));
783
+ this.#currentGridColor = computed(() => this.gridColor()[this.lightMode() ? 0 : 1], ...(ngDevMode ? [{ debugName: "#currentGridColor" }] : []));
784
+ this.panX = signal(0, ...(ngDevMode ? [{ debugName: "panX" }] : []));
785
+ this.panY = signal(0, ...(ngDevMode ? [{ debugName: "panY" }] : []));
786
+ this.zoomLevel = signal(1, ...(ngDevMode ? [{ debugName: "zoomLevel" }] : []));
787
+ this.gridSnapSize = signal(20, ...(ngDevMode ? [{ debugName: "gridSnapSize" }] : []));
788
+ this.isHoveringNode = signal(false, ...(ngDevMode ? [{ debugName: "isHoveringNode" }] : []));
789
+ this.midpointDivPosition = signal(null, ...(ngDevMode ? [{ debugName: "midpointDivPosition" }] : []));
790
+ this.showMidpointDiv = signal(false, ...(ngDevMode ? [{ debugName: "showMidpointDiv" }] : []));
791
+ this.isLocked = signal(false, ...(ngDevMode ? [{ debugName: "isLocked" }] : []));
792
+ this.draggingConnection = signal(null, ...(ngDevMode ? [{ debugName: "draggingConnection" }] : []));
793
+ this.validationErrors = signal(null, ...(ngDevMode ? [{ debugName: "validationErrors" }] : []));
794
+ this.highlightedConnection = signal(null, ...(ngDevMode ? [{ debugName: "highlightedConnection" }] : []));
795
+ this.#draggingNodeCoordinates = signal(null, ...(ngDevMode ? [{ debugName: "#draggingNodeCoordinates" }] : []));
796
+ this.#isDragging = signal(false, ...(ngDevMode ? [{ debugName: "#isDragging" }] : []));
797
+ this.#lastMouseX = signal(0, ...(ngDevMode ? [{ debugName: "#lastMouseX" }] : []));
798
+ this.#lastMouseY = signal(0, ...(ngDevMode ? [{ debugName: "#lastMouseY" }] : []));
799
+ this.#initialPinchDistance = signal(0, ...(ngDevMode ? [{ debugName: "#initialPinchDistance" }] : []));
800
+ this.#isNodeDragging = signal(false, ...(ngDevMode ? [{ debugName: "#isNodeDragging" }] : []));
801
+ this.#draggedNodeId = signal(null, ...(ngDevMode ? [{ debugName: "#draggedNodeId" }] : []));
802
+ this.#dragOffset = signal(null, ...(ngDevMode ? [{ debugName: "#dragOffset" }] : []));
803
+ this.#connections = signal([], ...(ngDevMode ? [{ debugName: "#connections" }] : []));
804
+ if (isPlatformBrowser(this.#platformId)) {
805
+ effect(() => {
806
+ this.asDots();
807
+ this.panX();
808
+ this.panY();
809
+ this.zoomLevel();
810
+ this.nodes();
811
+ this.#connections();
812
+ this.draggingConnection();
813
+ this.#currentGridColor();
814
+ this.highlightedConnection();
815
+ this.#draggingNodeCoordinates();
816
+ requestAnimationFrame(() => this.drawCanvas());
817
+ });
818
+ effect(() => {
819
+ const nodes = this.nodes();
820
+ const connectionsFromNodes = nodes.flatMap((node) => node.connections);
821
+ const uniqueConnections = connectionsFromNodes.filter((conn, index, self) => index ===
822
+ self.findIndex((c) => c.fromNode === conn.fromNode &&
823
+ c.fromPort === conn.fromPort &&
824
+ c.toNode === conn.toNode &&
825
+ c.toPort === conn.toPort));
826
+ this.#connections.set(uniqueConnections);
827
+ });
828
+ }
829
+ }
830
+ ngAfterViewInit() {
831
+ if (isPlatformBrowser(this.#platformId)) {
832
+ const canvas = this.canvasRef.nativeElement;
833
+ this.#ctx = canvas.getContext('2d');
834
+ this.#resizeObserver = new ResizeObserver(() => this.updateCanvasSize());
835
+ this.#resizeObserver.observe(this.#selfRef.nativeElement);
836
+ this.updateCanvasSize();
837
+ if (this.autoLayout()) {
838
+ this.applyAutolayout();
839
+ }
840
+ if (this.forceUnique()) {
841
+ this.#removeDuplicatesAndReinitiate();
842
+ }
843
+ else {
844
+ this.#validateNodes();
845
+ }
846
+ }
847
+ }
848
+ ngOnDestroy() {
849
+ if (this.#resizeObserver) {
850
+ this.#resizeObserver.disconnect();
851
+ }
852
+ }
853
+ #validateNodes() {
854
+ const duplicatePortIds = findDuplicatePortIDs(this.nodes());
855
+ const duplicateNodeIds = findDuplicateNodeIDs(this.nodes());
856
+ this.validationErrors.set({
857
+ duplicateNodeIds,
858
+ duplicatePortIds,
859
+ });
860
+ }
861
+ applyAutolayout() {
862
+ const newNodes = layoutNodes(this.nodes());
863
+ this.nodes.set(newNodes);
864
+ }
865
+ updateCanvasSize() {
866
+ const canvas = this.canvasRef.nativeElement;
867
+ const rect = this.#selfRef.nativeElement.getBoundingClientRect();
868
+ const dpr = window.devicePixelRatio || 1;
869
+ canvas.width = rect.width * dpr;
870
+ canvas.height = rect.height * dpr;
871
+ this.#ctx.scale(dpr, dpr);
872
+ canvas.style.width = `${rect.width}px`;
873
+ canvas.style.height = `${rect.height}px`;
874
+ this.drawCanvas();
875
+ }
876
+ drawCanvas() {
877
+ if (!this.#ctx)
878
+ return;
879
+ const ctx = this.#ctx;
880
+ const { width, height } = this.canvasRef.nativeElement;
881
+ const dpr = window.devicePixelRatio || 1;
882
+ ctx.clearRect(0, 0, width / dpr, height / dpr);
883
+ ctx.save();
884
+ ctx.translate(this.panX(), this.panY());
885
+ ctx.scale(this.zoomLevel(), this.zoomLevel());
886
+ this.drawGrid(ctx);
887
+ this.drawConnections(ctx);
888
+ if (this.draggingConnection()) {
889
+ this.drawDraggingPath(ctx);
890
+ }
891
+ ctx.restore();
892
+ }
893
+ drawGrid(ctx) {
894
+ const { width, height } = this.canvasRef.nativeElement;
895
+ const dpr = window.devicePixelRatio || 1;
896
+ const zoom = this.zoomLevel();
897
+ const panX = this.panX();
898
+ const panY = this.panY();
899
+ const gridSize = this.gridSize();
900
+ const gridColor = this.#currentGridColor();
901
+ const scaledGridSize = gridSize * zoom;
902
+ const dynamicGridSize = scaledGridSize < 20 ? gridSize * 4 : gridSize;
903
+ const startX = Math.floor(-panX / zoom / dynamicGridSize) * dynamicGridSize;
904
+ const startY = Math.floor(-panY / zoom / dynamicGridSize) * dynamicGridSize;
905
+ const endX = startX + width / dpr / zoom + dynamicGridSize;
906
+ const endY = startY + height / dpr / zoom + dynamicGridSize;
907
+ if (this.asDots()) {
908
+ const dotRadius = 1 / zoom;
909
+ ctx.fillStyle = gridColor;
910
+ for (let x = startX; x < endX; x += dynamicGridSize) {
911
+ for (let y = startY; y < endY; y += dynamicGridSize) {
912
+ ctx.beginPath();
913
+ ctx.arc(x, y, dotRadius, 0, 2 * Math.PI);
914
+ ctx.fill();
915
+ }
916
+ }
917
+ }
918
+ else {
919
+ ctx.beginPath();
920
+ ctx.strokeStyle = gridColor;
921
+ ctx.lineWidth = 1 / zoom;
922
+ for (let x = startX; x < endX; x += dynamicGridSize) {
923
+ ctx.moveTo(x, startY);
924
+ ctx.lineTo(x, endY);
925
+ }
926
+ for (let y = startY; y < endY; y += dynamicGridSize) {
927
+ ctx.moveTo(startX, y);
928
+ ctx.lineTo(endX, y);
929
+ }
930
+ ctx.stroke();
931
+ }
932
+ }
933
+ drawConnections(ctx) {
934
+ const highlighted = this.highlightedConnection();
935
+ ctx.lineWidth = 2 / this.zoomLevel();
936
+ for (const conn of this.#connections()) {
937
+ const isHighlighted = highlighted?.fromNode === conn.fromNode &&
938
+ highlighted?.fromPort === conn.fromPort &&
939
+ highlighted?.toNode === conn.toNode &&
940
+ highlighted?.toPort === conn.toPort;
941
+ ctx.strokeStyle = isHighlighted ? '#ffc107' : '#888';
942
+ ctx.beginPath();
943
+ const startPos = this.getNodePortPosition(conn.fromNode, conn.fromPort);
944
+ const endPos = this.getNodePortPosition(conn.toNode, conn.toPort);
945
+ this.drawCurvedPath(ctx, startPos, endPos);
946
+ ctx.stroke();
947
+ }
948
+ }
949
+ drawDraggingPath(ctx) {
950
+ const conn = this.draggingConnection();
951
+ const startPos = this.getNodePortPosition(conn.fromNode, conn.fromPort);
952
+ const endPos = [conn.x2, conn.y2];
953
+ ctx.strokeStyle = '#5a9cf8';
954
+ ctx.lineWidth = 2 / this.zoomLevel();
955
+ ctx.beginPath();
956
+ this.drawCurvedPath(ctx, startPos, endPos);
957
+ ctx.stroke();
958
+ }
959
+ drawCurvedPath(ctx, start, end) {
960
+ const [x1, y1] = start;
961
+ const [x2, y2] = end;
962
+ const dx = Math.abs(x1 - x2) * 0.7;
963
+ ctx.moveTo(x1, y1);
964
+ ctx.bezierCurveTo(x1 + dx, y1, x2 - dx, y2, x2, y2);
965
+ }
966
+ onMouseUp(event) {
967
+ if (this.isLocked())
968
+ return;
969
+ this.endPan();
970
+ this.endNodeDrag();
971
+ }
972
+ onClick(event) {
973
+ const target = event.target;
974
+ if (this.draggingConnection()) {
975
+ if (!target.closest('.port')) {
976
+ this.cancelPortDrag();
977
+ }
978
+ }
979
+ else if (this.isLocked() && !target.closest('.midpoint-div')) {
980
+ this.closeMidpointDiv();
981
+ }
982
+ else {
983
+ this.#handleConnectionClick(event);
984
+ }
985
+ }
986
+ onEscape(event) {
987
+ if (this.draggingConnection()) {
988
+ this.cancelPortDrag();
989
+ }
990
+ else if (this.isLocked()) {
991
+ this.closeMidpointDiv();
992
+ }
993
+ }
994
+ onMouseMove(event) {
995
+ if (this.isLocked())
996
+ return;
997
+ if (this.#isNodeDragging()) {
998
+ this.nodeDrag(event);
999
+ }
1000
+ else if (this.draggingConnection()) {
1001
+ this.updatePathOnMove(event);
1002
+ }
1003
+ else {
1004
+ this.pan(event);
1005
+ if (this.isHoveringNode()) {
1006
+ this.highlightedConnection.set(null);
1007
+ }
1008
+ else {
1009
+ this.#checkConnectionHover(event);
1010
+ }
1011
+ }
1012
+ }
1013
+ onTouchMove(event) {
1014
+ if (this.isLocked())
1015
+ return;
1016
+ if (this.#isNodeDragging()) {
1017
+ this.nodeDrag(event.touches[0]);
1018
+ }
1019
+ else if (this.draggingConnection()) {
1020
+ this.updatePathOnMove(event.touches[0]);
1021
+ }
1022
+ else {
1023
+ this.handleTouchMove(event);
1024
+ }
1025
+ }
1026
+ startNodeDrag(event, nodeId) {
1027
+ event.stopPropagation();
1028
+ event.preventDefault();
1029
+ this.#isNodeDragging.set(true);
1030
+ this.#draggedNodeId.set(nodeId);
1031
+ const clientX = event instanceof MouseEvent ? event.clientX : event.touches[0].clientX;
1032
+ const clientY = event instanceof MouseEvent ? event.clientY : event.touches[0].clientY;
1033
+ const blueprintRect = this.#selfRef.nativeElement.getBoundingClientRect();
1034
+ const worldX = (clientX - blueprintRect.left - this.panX()) / this.zoomLevel();
1035
+ const worldY = (clientY - blueprintRect.top - this.panY()) / this.zoomLevel();
1036
+ const draggedNode = this.nodes().find((n) => n.id === nodeId);
1037
+ if (draggedNode) {
1038
+ this.#dragOffset.set([worldX - draggedNode.coordinates[0], worldY - draggedNode.coordinates[1]]);
1039
+ }
1040
+ this.#lastMouseX.set(clientX);
1041
+ this.#lastMouseY.set(clientY);
1042
+ }
1043
+ endNodeDrag() {
1044
+ const draggedId = this.#draggedNodeId();
1045
+ const finalCoords = this.#draggingNodeCoordinates();
1046
+ if (draggedId && finalCoords) {
1047
+ this.nodes.update((nodes) => {
1048
+ const node = nodes.find((n) => n.id === draggedId);
1049
+ if (node) {
1050
+ node.coordinates = finalCoords;
1051
+ }
1052
+ return [...nodes];
1053
+ });
1054
+ }
1055
+ this.#isNodeDragging.set(false);
1056
+ this.#draggedNodeId.set(null);
1057
+ this.#dragOffset.set(null);
1058
+ this.#draggingNodeCoordinates.set(null);
1059
+ }
1060
+ nodeDrag(event) {
1061
+ const draggedId = this.#draggedNodeId();
1062
+ if (!this.#isNodeDragging() || !draggedId)
1063
+ return;
1064
+ const { clientX, clientY } = event;
1065
+ const blueprintRect = this.#selfRef.nativeElement.getBoundingClientRect();
1066
+ const worldX = (clientX - blueprintRect.left - this.panX()) / this.zoomLevel();
1067
+ const worldY = (clientY - blueprintRect.top - this.panY()) / this.zoomLevel();
1068
+ let newX = worldX - this.#dragOffset()[0];
1069
+ let newY = worldY - this.#dragOffset()[1];
1070
+ if (this.snapToGrid() || (event instanceof MouseEvent && event.shiftKey)) {
1071
+ const grid = this.gridSnapSize();
1072
+ newX = Math.round(newX / grid) * grid;
1073
+ newY = Math.round(newY / grid) * grid;
1074
+ }
1075
+ this.#draggingNodeCoordinates.set([newX, newY]);
1076
+ this.#lastMouseX.set(clientX);
1077
+ this.#lastMouseY.set(clientY);
1078
+ }
1079
+ startPortDrag(event, nodeId, portId) {
1080
+ event.stopPropagation();
1081
+ if (this.draggingConnection())
1082
+ this.cancelPortDrag();
1083
+ const node = this.nodes().find((n) => n.id === nodeId);
1084
+ if (node) {
1085
+ this.draggingConnection.set({
1086
+ fromNode: nodeId,
1087
+ fromPort: portId,
1088
+ x2: node.coordinates[0],
1089
+ y2: node.coordinates[1],
1090
+ });
1091
+ }
1092
+ this.updatePathOnMove(event);
1093
+ }
1094
+ endPortDrag(_, toNodeId, toPortId) {
1095
+ if (!this.draggingConnection())
1096
+ return;
1097
+ const from = this.draggingConnection();
1098
+ if (from.fromNode === toNodeId) {
1099
+ this.cancelPortDrag();
1100
+ return;
1101
+ }
1102
+ const newConnection = {
1103
+ fromNode: from.fromNode,
1104
+ fromPort: from.fromPort,
1105
+ toNode: toNodeId,
1106
+ toPort: toPortId,
1107
+ };
1108
+ this.nodes.update((nodes) => {
1109
+ const fromNode = nodes.find((n) => n.id === newConnection.fromNode);
1110
+ const toNode = nodes.find((n) => n.id === newConnection.toNode);
1111
+ if (fromNode && toNode) {
1112
+ const isDuplicateFrom = fromNode.connections.some((c) => c.fromNode === newConnection.fromNode &&
1113
+ c.fromPort === newConnection.fromPort &&
1114
+ c.toNode === newConnection.toNode &&
1115
+ c.toPort === newConnection.toPort);
1116
+ if (!isDuplicateFrom) {
1117
+ fromNode.connections = [...fromNode.connections, newConnection];
1118
+ }
1119
+ const isDuplicateTo = toNode.connections.some((c) => c.fromNode === newConnection.fromNode &&
1120
+ c.fromPort === newConnection.fromPort &&
1121
+ c.toNode === newConnection.toNode &&
1122
+ c.toPort === newConnection.toPort);
1123
+ if (!isDuplicateTo) {
1124
+ toNode.connections = [...toNode.connections, newConnection];
1125
+ }
1126
+ }
1127
+ return [...nodes];
1128
+ });
1129
+ this.cancelPortDrag();
1130
+ }
1131
+ cancelPortDrag() {
1132
+ this.draggingConnection.set(null);
1133
+ }
1134
+ updatePathOnMove(event) {
1135
+ if (!this.draggingConnection())
1136
+ return;
1137
+ const blueprintRect = this.#selfRef.nativeElement.getBoundingClientRect();
1138
+ const x2 = (event.clientX - blueprintRect.left - this.panX()) / this.zoomLevel();
1139
+ const y2 = (event.clientY - blueprintRect.top - this.panY()) / this.zoomLevel();
1140
+ this.draggingConnection.update((conn) => (conn ? { ...conn, x2, y2 } : conn));
1141
+ }
1142
+ getNodePortPosition(nodeId, portId) {
1143
+ const node = this.nodes().find((n) => n.id === nodeId);
1144
+ if (!node)
1145
+ return [0, 0];
1146
+ const portEl = this.#selfRef.nativeElement.querySelector(`[data-node-id="${nodeId}"][data-port-id="${portId}"]`);
1147
+ if (!portEl)
1148
+ return node.coordinates;
1149
+ const nodeWrapper = this.#selfRef.nativeElement.querySelector('.nodes-wrapper');
1150
+ const wrapperRect = nodeWrapper.getBoundingClientRect();
1151
+ const portRect = portEl.getBoundingClientRect();
1152
+ const portCenterX = portRect.left + portRect.width / 2;
1153
+ const portCenterY = portRect.top + portRect.height / 2;
1154
+ const worldX = (portCenterX - wrapperRect.left) / this.zoomLevel();
1155
+ const worldY = (portCenterY - wrapperRect.top) / this.zoomLevel();
1156
+ return [worldX, worldY];
1157
+ }
1158
+ startPan(event) {
1159
+ if (this.isLocked())
1160
+ return;
1161
+ if (event.target instanceof HTMLElement && event.target.closest('.node'))
1162
+ return;
1163
+ event.preventDefault();
1164
+ this.#isDragging.set(true);
1165
+ this.#lastMouseX.set(event.clientX);
1166
+ this.#lastMouseY.set(event.clientY);
1167
+ }
1168
+ endPan() {
1169
+ this.#isDragging.set(false);
1170
+ }
1171
+ pan(event) {
1172
+ if (!this.#isDragging() || this.isLocked())
1173
+ return;
1174
+ const dx = event.clientX - this.#lastMouseX();
1175
+ const dy = event.clientY - this.#lastMouseY();
1176
+ this.panX.update((x) => x + dx);
1177
+ this.panY.update((y) => y + dy);
1178
+ this.#lastMouseX.set(event.clientX);
1179
+ this.#lastMouseY.set(event.clientY);
1180
+ }
1181
+ zoom(event) {
1182
+ event.preventDefault();
1183
+ const oldZoom = this.zoomLevel();
1184
+ const newZoom = this.#clamp(oldZoom * (1 - event.deltaY * this.#ZOOM_SPEED), this.#MIN_ZOOM, this.#MAX_ZOOM);
1185
+ const panRatio = newZoom / oldZoom;
1186
+ const newPanX = event.clientX - (event.clientX - this.panX()) * panRatio;
1187
+ const newPanY = event.clientY - (event.clientY - this.panY()) * panRatio;
1188
+ this.zoomLevel.set(newZoom);
1189
+ this.panX.set(newPanX);
1190
+ this.panY.set(newPanY);
1191
+ }
1192
+ handleTouchStart(event) {
1193
+ if (event.target instanceof HTMLElement && event.target.closest('.node'))
1194
+ return;
1195
+ event.preventDefault();
1196
+ if (event.touches.length === 1) {
1197
+ this.#isDragging.set(true);
1198
+ this.#lastMouseX.set(event.touches[0].clientX);
1199
+ this.#lastMouseY.set(event.touches[0].clientY);
1200
+ }
1201
+ else if (event.touches.length === 2) {
1202
+ this.#isDragging.set(false);
1203
+ this.#initialPinchDistance.set(this.#getDistance(event.touches[0], event.touches[1]));
1204
+ }
1205
+ }
1206
+ handleTouchMove(event) {
1207
+ if (event.touches.length === 1 && this.#isDragging()) {
1208
+ const dx = event.touches[0].clientX - this.#lastMouseX();
1209
+ const dy = event.touches[0].clientY - this.#lastMouseY();
1210
+ this.panX.update((x) => x + dx);
1211
+ this.panY.update((y) => y + dy);
1212
+ this.#lastMouseX.set(event.touches[0].clientX);
1213
+ this.#lastMouseY.set(event.touches[0].clientY);
1214
+ }
1215
+ else if (event.touches.length === 2) {
1216
+ const newPinchDistance = this.#getDistance(event.touches[0], event.touches[1]);
1217
+ const pinchRatio = newPinchDistance / this.#initialPinchDistance();
1218
+ const oldZoom = this.zoomLevel();
1219
+ const newZoom = this.#clamp(oldZoom * pinchRatio, this.#MIN_ZOOM, this.#MAX_ZOOM);
1220
+ const pinchCenterX = (event.touches[0].clientX + event.touches[1].clientX) / 2;
1221
+ const pinchCenterY = (event.touches[0].clientY + event.touches[1].clientY) / 2;
1222
+ const panRatio = newZoom / oldZoom;
1223
+ const newPanX = pinchCenterX - (pinchCenterX - this.panX()) * panRatio;
1224
+ const newPanY = pinchCenterY - (pinchCenterY - this.panY()) * panRatio;
1225
+ this.zoomLevel.set(newZoom);
1226
+ this.panX.set(newPanX);
1227
+ this.panY.set(newPanY);
1228
+ this.#initialPinchDistance.set(newPinchDistance);
1229
+ }
1230
+ }
1231
+ handleTouchEnd() {
1232
+ this.#isDragging.set(false);
1233
+ }
1234
+ closeMidpointDiv() {
1235
+ this.showMidpointDiv.set(false);
1236
+ this.midpointDivPosition.set(null);
1237
+ this.isLocked.set(false);
1238
+ }
1239
+ getDisplayCoordinates(node) {
1240
+ const draggedId = this.#draggedNodeId();
1241
+ const draggingCoords = this.#draggingNodeCoordinates();
1242
+ if (draggedId === node.id && draggingCoords) {
1243
+ return `translate(${draggingCoords[0]}px, ${draggingCoords[1]}px)`;
1244
+ }
1245
+ return `translate(${node.coordinates[0]}px, ${node.coordinates[1]}px)`;
1246
+ }
1247
+ removeConnection() {
1248
+ const conn = this.highlightedConnection();
1249
+ if (!conn)
1250
+ return;
1251
+ this.#connections.update((conns) => conns.filter((c) => !(c.fromNode === conn.fromNode &&
1252
+ c.fromPort === conn.fromPort &&
1253
+ c.toNode === conn.toNode &&
1254
+ c.toPort === conn.toPort)));
1255
+ this.nodes.update((nodes) => nodes.map((n) => ({
1256
+ ...n,
1257
+ connections: n.connections.filter((c) => !(c.fromNode === conn.fromNode &&
1258
+ c.fromPort === conn.fromPort &&
1259
+ c.toNode === conn.toNode &&
1260
+ c.toPort === conn.toPort)),
1261
+ })));
1262
+ this.highlightedConnection.set(null);
1263
+ this.closeMidpointDiv();
1264
+ }
1265
+ #removeDuplicatesAndReinitiate() {
1266
+ const nodes = this.nodes();
1267
+ const uniqueNodesMap = new Map();
1268
+ const uniqueNodes = [];
1269
+ for (const node of nodes) {
1270
+ if (!uniqueNodesMap.has(node.id)) {
1271
+ uniqueNodesMap.set(node.id, node);
1272
+ uniqueNodes.push(node);
1273
+ }
1274
+ }
1275
+ const nodesWithUniquePorts = uniqueNodes.map((node) => {
1276
+ const uniqueInputs = new Map();
1277
+ const uniqueOutputs = new Map();
1278
+ const uniqueNodeInputs = node.inputs.filter((port) => {
1279
+ if (!uniqueInputs.has(port.id)) {
1280
+ uniqueInputs.set(port.id, port);
1281
+ return true;
1282
+ }
1283
+ return false;
1284
+ });
1285
+ const uniqueNodeOutputs = node.outputs.filter((port) => {
1286
+ if (!uniqueOutputs.has(port.id)) {
1287
+ uniqueOutputs.set(port.id, port);
1288
+ return true;
1289
+ }
1290
+ return false;
1291
+ });
1292
+ return {
1293
+ ...node,
1294
+ inputs: uniqueNodeInputs,
1295
+ outputs: uniqueNodeOutputs,
1296
+ };
1297
+ });
1298
+ const finalNodes = nodesWithUniquePorts.map((node) => {
1299
+ const validConnections = node.connections.filter((conn) => {
1300
+ const fromNodeExists = nodesWithUniquePorts.some((n) => n.id === conn.fromNode);
1301
+ const toNodeExists = nodesWithUniquePorts.some((n) => n.id === conn.toNode);
1302
+ const fromPortExists = nodesWithUniquePorts
1303
+ .find((n) => n.id === conn.fromNode)
1304
+ ?.outputs.some((p) => p.id === conn.fromPort);
1305
+ const toPortExists = nodesWithUniquePorts
1306
+ .find((n) => n.id === conn.toNode)
1307
+ ?.inputs.some((p) => p.id === conn.toPort);
1308
+ return fromNodeExists && toNodeExists && fromPortExists && toPortExists;
1309
+ });
1310
+ return { ...node, connections: validConnections };
1311
+ });
1312
+ this.nodes.set(finalNodes);
1313
+ }
1314
+ #handleConnectionClick(event) {
1315
+ if (this.highlightedConnection()) {
1316
+ const conn = this.highlightedConnection();
1317
+ const startPos = this.getNodePortPosition(conn.fromNode, conn.fromPort);
1318
+ const endPos = this.getNodePortPosition(conn.toNode, conn.toPort);
1319
+ const midpoint = this.#getBezierMidpoint(startPos, endPos);
1320
+ this.midpointDivPosition.set({ x: midpoint[0], y: midpoint[1] });
1321
+ this.showMidpointDiv.set(true);
1322
+ this.isLocked.set(true);
1323
+ }
1324
+ }
1325
+ #getBezierMidpoint(start, end) {
1326
+ const t = 0.5;
1327
+ const [x1, y1] = start;
1328
+ const [x2, y2] = end;
1329
+ const dx = Math.abs(x1 - x2) * 0.7;
1330
+ const cp1x = x1 + dx;
1331
+ const cp1y = y1;
1332
+ const cp2x = x2 - dx;
1333
+ const cp2y = y2;
1334
+ const mx = Math.pow(1 - t, 3) * x1 +
1335
+ 3 * Math.pow(1 - t, 2) * t * cp1x +
1336
+ 3 * (1 - t) * Math.pow(t, 2) * cp2x +
1337
+ Math.pow(t, 3) * x2;
1338
+ const my = Math.pow(1 - t, 3) * y1 +
1339
+ 3 * Math.pow(1 - t, 2) * t * cp1y +
1340
+ 3 * (1 - t) * Math.pow(t, 2) * cp2y +
1341
+ Math.pow(t, 3) * y2;
1342
+ return [mx, my];
1343
+ }
1344
+ #checkConnectionHover(event) {
1345
+ const blueprintRect = this.#selfRef.nativeElement.getBoundingClientRect();
1346
+ const worldX = (event.clientX - blueprintRect.left - this.panX()) / this.zoomLevel();
1347
+ const worldY = (event.clientY - blueprintRect.top - this.panY()) / this.zoomLevel();
1348
+ const cursorPoint = [worldX, worldY];
1349
+ let hoveredConn = null;
1350
+ const tolerance = 10 / this.zoomLevel();
1351
+ for (const conn of this.#connections()) {
1352
+ const startPos = this.getNodePortPosition(conn.fromNode, conn.fromPort);
1353
+ const endPos = this.getNodePortPosition(conn.toNode, conn.toPort);
1354
+ if (this.#isPointNearBezierCurve(cursorPoint, startPos, endPos, tolerance)) {
1355
+ hoveredConn = conn;
1356
+ break;
1357
+ }
1358
+ }
1359
+ this.highlightedConnection.set(hoveredConn);
1360
+ }
1361
+ #isPointNearBezierCurve(point, start, end, tolerance = 10) {
1362
+ const [x1, y1] = start;
1363
+ const [x2, y2] = end;
1364
+ const dx = Math.abs(x1 - x2) * 0.7;
1365
+ const cp1x = x1 + dx;
1366
+ const cp1y = y1;
1367
+ const cp2x = x2 - dx;
1368
+ const cp2y = y2;
1369
+ const steps = 20;
1370
+ for (let i = 0; i <= steps; i++) {
1371
+ const t = i / steps;
1372
+ const x = Math.pow(1 - t, 3) * x1 +
1373
+ 3 * Math.pow(1 - t, 2) * t * cp1x +
1374
+ 3 * (1 - t) * Math.pow(t, 2) * cp2x +
1375
+ Math.pow(t, 3) * x2;
1376
+ const y = Math.pow(1 - t, 3) * y1 +
1377
+ 3 * Math.pow(1 - t, 2) * t * cp1y +
1378
+ 3 * (1 - t) * Math.pow(t, 2) * cp2y +
1379
+ Math.pow(t, 3) * y2;
1380
+ const distance = Math.sqrt(Math.pow(point[0] - x, 2) + Math.pow(point[1] - y, 2));
1381
+ if (distance < tolerance) {
1382
+ return true;
1383
+ }
1384
+ }
1385
+ return false;
1386
+ }
1387
+ #getDistance(touch1, touch2) {
1388
+ const dx = touch1.clientX - touch2.clientX;
1389
+ const dy = touch1.clientY - touch2.clientY;
1390
+ return Math.sqrt(dx * dx + dy * dy);
1391
+ }
1392
+ #clamp(value, min, max) {
1393
+ return Math.max(min, Math.min(max, value));
1394
+ }
1395
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ShipBlueprintComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1396
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.0", type: ShipBlueprintComponent, isStandalone: true, selector: "sh-blueprint", inputs: { forceUnique: { classPropertyName: "forceUnique", publicName: "forceUnique", isSignal: true, isRequired: false, transformFunction: null }, autoLayout: { classPropertyName: "autoLayout", publicName: "autoLayout", isSignal: true, isRequired: false, transformFunction: null }, gridSize: { classPropertyName: "gridSize", publicName: "gridSize", isSignal: true, isRequired: false, transformFunction: null }, snapToGrid: { classPropertyName: "snapToGrid", publicName: "snapToGrid", isSignal: true, isRequired: false, transformFunction: null }, gridColor: { classPropertyName: "gridColor", publicName: "gridColor", isSignal: true, isRequired: false, transformFunction: null }, nodes: { classPropertyName: "nodes", publicName: "nodes", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { nodes: "nodesChange" }, host: { listeners: { "document:mouseup": "onMouseUp($event)", "document:click": "onClick($event)", "document:keydown.escape": "onEscape($event)", "document:mousemove": "onMouseMove($event)", "document:touchmove": "onTouchMove($event)" } }, viewQueries: [{ propertyName: "canvasRef", first: true, predicate: ["blueprintCanvas"], descendants: true, static: true }], ngImport: i0, template: `
1397
+ <div
1398
+ class="canvas-container"
1399
+ [class.locked]="isLocked()"
1400
+ [class.hovering-connection]="highlightedConnection()"
1401
+ (mousedown)="startPan($event)"
1402
+ (mousemove)="pan($event)"
1403
+ (wheel)="zoom($event)"
1404
+ (touchstart)="handleTouchStart($event)"
1405
+ (touchmove)="handleTouchMove($event)"
1406
+ (touchend)="handleTouchEnd()">
1407
+ <div class="action-panel">
1408
+ <button shButton (click)="applyAutolayout()" class="small">
1409
+ Autolayout
1410
+ <sh-icon>tree-structure</sh-icon>
1411
+ </button>
1412
+
1413
+ <ng-content />
1414
+ </div>
1415
+
1416
+ <canvas #blueprintCanvas></canvas>
1417
+
1418
+ @if (validationErrors()) {
1419
+ <sh-card class="validation-errors type-c">
1420
+ Something went wrong you have
1421
+ <br />
1422
+ duplicate node IDs or port IDs
1423
+ <pre>{{ validationErrors() | json }}</pre>
1424
+ </sh-card>
1425
+ } @else {
1426
+ <div
1427
+ class="nodes-wrapper"
1428
+ [style.transform]="'translate(' + panX() + 'px, ' + panY() + 'px) scale(' + zoomLevel() + ')'"
1429
+ [style.transform-origin]="'0 0'">
1430
+ @for (node of nodes(); track node.id) {
1431
+ <sh-card
1432
+ class="node type-c"
1433
+ [style.transform]="getDisplayCoordinates(node)"
1434
+ (mouseenter)="isHoveringNode.set(true)"
1435
+ (mouseleave)="isHoveringNode.set(false)">
1436
+ <header (mousedown)="startNodeDrag($event, node.id)" (touchstart)="startNodeDrag($event, node.id)">
1437
+ <span class="node-title">{{ node.id }}</span>
1438
+ <sh-icon>list</sh-icon>
1439
+ </header>
1440
+ <div class="ports">
1441
+ <div class="inputs">
1442
+ @for (inputPort of node.inputs; track inputPort.id) {
1443
+ <div class="port-wrap">
1444
+ <div
1445
+ class="port input-port"
1446
+ [attr.data-node-id]="node.id"
1447
+ [attr.data-port-id]="inputPort.id"
1448
+ (click)="endPortDrag($event, node.id, inputPort.id)"></div>
1449
+ <span class="port-name">{{ inputPort.name }}</span>
1450
+ </div>
1451
+ }
1452
+ </div>
1453
+ <div class="outputs">
1454
+ @for (outputPort of node.outputs; track outputPort.id) {
1455
+ <div class="port-wrap">
1456
+ <span class="port-name">{{ outputPort.name }}</span>
1457
+ <div
1458
+ class="port output-port"
1459
+ [attr.data-node-id]="node.id"
1460
+ [attr.data-port-id]="outputPort.id"
1461
+ (click)="startPortDrag($event, node.id, outputPort.id)"></div>
1462
+ </div>
1463
+ }
1464
+ </div>
1465
+ </div>
1466
+ </sh-card>
1467
+ }
1468
+
1469
+ @if (showMidpointDiv()) {
1470
+ <div
1471
+ class="midpoint-div"
1472
+ (click)="removeConnection()"
1473
+ [style.left.px]="midpointDivPosition()?.x"
1474
+ [style.top.px]="midpointDivPosition()?.y">
1475
+ Remove connection
1476
+ <sh-icon>trash</sh-icon>
1477
+ </div>
1478
+ }
1479
+ </div>
1480
+ }
1481
+ </div>
1482
+ `, isInline: true, dependencies: [{ kind: "component", type: ShipCardComponent$1, selector: "sh-card" }, { kind: "component", type: ShipIconComponent$1, selector: "sh-icon" }, { kind: "component", type: ShipButtonComponent$1, selector: "[shButton]" }, { kind: "pipe", type: JsonPipe, name: "json" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1483
+ }
1484
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ShipBlueprintComponent, decorators: [{
1485
+ type: Component,
1486
+ args: [{
1487
+ selector: 'sh-blueprint',
1488
+ imports: [ShipCardComponent$1, ShipIconComponent$1, JsonPipe, ShipButtonComponent$1],
1489
+ template: `
1490
+ <div
1491
+ class="canvas-container"
1492
+ [class.locked]="isLocked()"
1493
+ [class.hovering-connection]="highlightedConnection()"
1494
+ (mousedown)="startPan($event)"
1495
+ (mousemove)="pan($event)"
1496
+ (wheel)="zoom($event)"
1497
+ (touchstart)="handleTouchStart($event)"
1498
+ (touchmove)="handleTouchMove($event)"
1499
+ (touchend)="handleTouchEnd()">
1500
+ <div class="action-panel">
1501
+ <button shButton (click)="applyAutolayout()" class="small">
1502
+ Autolayout
1503
+ <sh-icon>tree-structure</sh-icon>
1504
+ </button>
1505
+
1506
+ <ng-content />
1507
+ </div>
1508
+
1509
+ <canvas #blueprintCanvas></canvas>
1510
+
1511
+ @if (validationErrors()) {
1512
+ <sh-card class="validation-errors type-c">
1513
+ Something went wrong you have
1514
+ <br />
1515
+ duplicate node IDs or port IDs
1516
+ <pre>{{ validationErrors() | json }}</pre>
1517
+ </sh-card>
1518
+ } @else {
1519
+ <div
1520
+ class="nodes-wrapper"
1521
+ [style.transform]="'translate(' + panX() + 'px, ' + panY() + 'px) scale(' + zoomLevel() + ')'"
1522
+ [style.transform-origin]="'0 0'">
1523
+ @for (node of nodes(); track node.id) {
1524
+ <sh-card
1525
+ class="node type-c"
1526
+ [style.transform]="getDisplayCoordinates(node)"
1527
+ (mouseenter)="isHoveringNode.set(true)"
1528
+ (mouseleave)="isHoveringNode.set(false)">
1529
+ <header (mousedown)="startNodeDrag($event, node.id)" (touchstart)="startNodeDrag($event, node.id)">
1530
+ <span class="node-title">{{ node.id }}</span>
1531
+ <sh-icon>list</sh-icon>
1532
+ </header>
1533
+ <div class="ports">
1534
+ <div class="inputs">
1535
+ @for (inputPort of node.inputs; track inputPort.id) {
1536
+ <div class="port-wrap">
1537
+ <div
1538
+ class="port input-port"
1539
+ [attr.data-node-id]="node.id"
1540
+ [attr.data-port-id]="inputPort.id"
1541
+ (click)="endPortDrag($event, node.id, inputPort.id)"></div>
1542
+ <span class="port-name">{{ inputPort.name }}</span>
1543
+ </div>
1544
+ }
1545
+ </div>
1546
+ <div class="outputs">
1547
+ @for (outputPort of node.outputs; track outputPort.id) {
1548
+ <div class="port-wrap">
1549
+ <span class="port-name">{{ outputPort.name }}</span>
1550
+ <div
1551
+ class="port output-port"
1552
+ [attr.data-node-id]="node.id"
1553
+ [attr.data-port-id]="outputPort.id"
1554
+ (click)="startPortDrag($event, node.id, outputPort.id)"></div>
1555
+ </div>
1556
+ }
1557
+ </div>
1558
+ </div>
1559
+ </sh-card>
1560
+ }
1561
+
1562
+ @if (showMidpointDiv()) {
1563
+ <div
1564
+ class="midpoint-div"
1565
+ (click)="removeConnection()"
1566
+ [style.left.px]="midpointDivPosition()?.x"
1567
+ [style.top.px]="midpointDivPosition()?.y">
1568
+ Remove connection
1569
+ <sh-icon>trash</sh-icon>
1570
+ </div>
1571
+ }
1572
+ </div>
1573
+ }
1574
+ </div>
1575
+ `,
1576
+ changeDetection: ChangeDetectionStrategy.OnPush,
1577
+ }]
1578
+ }], ctorParameters: () => [], propDecorators: { canvasRef: [{
1579
+ type: ViewChild,
1580
+ args: ['blueprintCanvas', { static: true }]
1581
+ }], onMouseUp: [{
1582
+ type: HostListener,
1583
+ args: ['document:mouseup', ['$event']]
1584
+ }], onClick: [{
1585
+ type: HostListener,
1586
+ args: ['document:click', ['$event']]
1587
+ }], onEscape: [{
1588
+ type: HostListener,
1589
+ args: ['document:keydown.escape', ['$event']]
1590
+ }], onMouseMove: [{
1591
+ type: HostListener,
1592
+ args: ['document:mousemove', ['$event']]
1593
+ }], onTouchMove: [{
1594
+ type: HostListener,
1595
+ args: ['document:touchmove', ['$event']]
1596
+ }] } });
1597
+
562
1598
  class ShipButtonGroupComponent {
563
1599
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ShipButtonGroupComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
564
1600
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.0", type: ShipButtonGroupComponent, isStandalone: true, selector: "sh-button-group", ngImport: i0, template: `
@@ -5696,5 +6732,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImpor
5696
6732
  * Generated bundle index. Do not edit.
5697
6733
  */
5698
6734
 
5699
- export { GridSortableDirective, SHIP_CONFIG, ShipAlertComponent, ShipAlertContainerComponent, ShipAlertModule, ShipAlertService, ShipButtonComponent, ShipButtonGroupComponent, ShipCardComponent, ShipCheckboxComponent, ShipChipComponent, ShipColorPickerComponent, ShipDatepickerComponent, ShipDatepickerInputComponent, ShipDaterangeInputComponent, ShipDialogComponent, ShipDialogService, ShipDividerComponent, ShipEventCardComponent, ShipFileDragDropDirective, ShipFileUploadComponent, ShipFormFieldComponent, ShipIconComponent, ShipListComponent, ShipMenuComponent, ShipPopoverComponent, ShipPreventWheelDirective, ShipProgressBarComponent, ShipRadioComponent, ShipRangeSliderComponent, ShipResizeDirective, ShipSelectComponent, ShipSidenavComponent, ShipSortDirective, ShipSortableComponent, ShipSortableDirective, ShipSpinnerComponent, ShipStepperComponent, ShipStickyColumnsDirective, ShipTableComponent, ShipTabsComponent, ShipToggleCardComponent, ShipToggleComponent, ShipTooltipComponent, ShipTooltipDirective, ShipTooltipWrapper, ShipVirtualScrollComponent, moveIndex, watchHostClass };
6735
+ export { GridSortableDirective, SHIP_CONFIG, ShipAlertComponent, ShipAlertContainerComponent, ShipAlertModule, ShipAlertService, ShipBlueprintComponent, ShipButtonComponent, ShipButtonGroupComponent, ShipCardComponent, ShipCheckboxComponent, ShipChipComponent, ShipColorPickerComponent, ShipDatepickerComponent, ShipDatepickerInputComponent, ShipDaterangeInputComponent, ShipDialogComponent, ShipDialogService, ShipDividerComponent, ShipEventCardComponent, ShipFileDragDropDirective, ShipFileUploadComponent, ShipFormFieldComponent, ShipIconComponent, ShipListComponent, ShipMenuComponent, ShipPopoverComponent, ShipPreventWheelDirective, ShipProgressBarComponent, ShipRadioComponent, ShipRangeSliderComponent, ShipResizeDirective, ShipSelectComponent, ShipSidenavComponent, ShipSortDirective, ShipSortableComponent, ShipSortableDirective, ShipSpinnerComponent, ShipStepperComponent, ShipStickyColumnsDirective, ShipTableComponent, ShipTabsComponent, ShipToggleCardComponent, ShipToggleComponent, ShipTooltipComponent, ShipTooltipDirective, ShipTooltipWrapper, ShipVirtualScrollComponent, TEST_NODES, moveIndex, watchHostClass };
5700
6736
  //# sourceMappingURL=ship-ui-core.mjs.map