@procaaso/alphinity-ui-components 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -36,6 +36,7 @@ __export(index_exports, {
36
36
  CardValue: () => CardValue,
37
37
  CircularGauge: () => CircularGauge,
38
38
  ControlPanel: () => ControlPanel,
39
+ DEFAULT_THRESHOLDS: () => DEFAULT_THRESHOLDS,
39
40
  DashboardCard: () => DashboardCard,
40
41
  DisplayModeToggle: () => DisplayModeToggle,
41
42
  EquipmentIndicator: () => EquipmentIndicator,
@@ -47,6 +48,8 @@ __export(index_exports, {
47
48
  LeftPanel: () => LeftPanel,
48
49
  LeftToggleButton: () => LeftToggleButton,
49
50
  LegacyValueEntry: () => LegacyValueEntry,
51
+ NodeConfigPanel: () => NodeConfigPanel,
52
+ PIDCanvas: () => PIDCanvas,
50
53
  PanelContent: () => PanelContent,
51
54
  PanelTabs: () => PanelTabs,
52
55
  RightPanel: () => RightPanel,
@@ -57,14 +60,32 @@ __export(index_exports, {
57
60
  Selector: () => Selector,
58
61
  Sparkline: () => Sparkline,
59
62
  StatusIndicator: () => StatusIndicator,
63
+ SymbolLibrary: () => SymbolLibrary,
60
64
  ThemeProvider: () => ThemeProvider,
61
65
  ThemeToggle: () => ThemeToggle,
62
66
  TrendLine: () => TrendLine,
63
67
  ValueEntry: () => ValueEntry,
64
68
  ZoomButton: () => ZoomButton,
65
69
  ZoomControls: () => ZoomControls,
70
+ calculateThresholdStatus: () => calculateThresholdStatus,
71
+ exampleDiagram: () => exampleDiagram,
72
+ exportDiagram: () => exportDiagram,
73
+ generateRoutePoints: () => generateRoutePoints,
74
+ getAvailableSymbols: () => getAvailableSymbols,
75
+ getPortWorldPosition: () => getPortWorldPosition,
76
+ getSymbolDefinition: () => getSymbolDefinition,
77
+ importDiagram: () => importDiagram,
78
+ mockSymbolLibrary: () => mockSymbolLibrary,
79
+ nodeHasPort: () => nodeHasPort,
66
80
  overlayStyles: () => overlayStyles,
67
- useTheme: () => useTheme
81
+ useDrag: () => useDrag,
82
+ useKeyboardShortcuts: () => useKeyboardShortcuts,
83
+ useSelection: () => useSelection,
84
+ useSimulation: () => useSimulation,
85
+ useTelemetry: () => useTelemetry,
86
+ useTheme: () => useTheme,
87
+ useViewBox: () => useViewBox,
88
+ validateDiagram: () => validateDiagram
68
89
  });
69
90
  module.exports = __toCommonJS(index_exports);
70
91
 
@@ -703,6 +724,24 @@ var ControlPanel = ({
703
724
 
704
725
  // src/components/StatusIndicator/StatusIndicator.tsx
705
726
  var import_jsx_runtime6 = require("react/jsx-runtime");
727
+ var getStatusDescription = (status) => {
728
+ switch (status) {
729
+ case "alarm":
730
+ return "Critical alarm condition requiring attention";
731
+ case "warning":
732
+ return "Warning condition that should be monitored";
733
+ case "fault":
734
+ return "Equipment fault detected";
735
+ case "active":
736
+ return "Equipment is actively running";
737
+ case "normal":
738
+ return "Operating normally";
739
+ case "off":
740
+ return "Equipment is off";
741
+ default:
742
+ return "";
743
+ }
744
+ };
706
745
  var StatusIndicator = ({
707
746
  status,
708
747
  size = "medium",
@@ -739,6 +778,10 @@ var StatusIndicator = ({
739
778
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
740
779
  "span",
741
780
  {
781
+ role: "status",
782
+ "aria-label": label || `Status: ${status}`,
783
+ "aria-description": getStatusDescription(status),
784
+ "data-status": status,
742
785
  className: `
743
786
  ${baseClass}
744
787
  ${statusClass}
@@ -1740,6 +1783,3117 @@ var ThemeToggle = ({
1740
1783
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { style: { fontSize: "0.75rem", opacity: 0.8 }, children: theme === "modern" ? "Industrial Design" : "High Performance" })
1741
1784
  ] });
1742
1785
  };
1786
+
1787
+ // src/components/PIDCanvas/PIDCanvas.tsx
1788
+ var import_react13 = require("react");
1789
+
1790
+ // src/diagram/symbols/mockSymbolLibrary.ts
1791
+ var mockSymbolLibrary = {
1792
+ "valve-ball": {
1793
+ id: "valve-ball",
1794
+ name: "Ball Valve",
1795
+ category: "valve",
1796
+ viewBox: { x: 0, y: 0, width: 50, height: 50 },
1797
+ ports: [
1798
+ { id: "inlet", x: 0, y: 25, type: "inlet", direction: "in" },
1799
+ { id: "outlet", x: 50, y: 25, type: "outlet", direction: "out" }
1800
+ ],
1801
+ svgContent: `
1802
+ <g>
1803
+ <!-- Valve body -->
1804
+ <rect x="15" y="10" width="20" height="30" fill="#546e7a" stroke="#263238" stroke-width="2" rx="2"/>
1805
+ <!-- Ball indicator -->
1806
+ <circle cx="25" cy="25" r="6" fill="#ffa726" stroke="#e65100" stroke-width="1.5"/>
1807
+ <!-- Inlet/outlet lines -->
1808
+ <line x1="0" y1="25" x2="15" y2="25" stroke="#263238" stroke-width="3" stroke-linecap="round"/>
1809
+ <line x1="35" y1="25" x2="50" y2="25" stroke="#263238" stroke-width="3" stroke-linecap="round"/>
1810
+ </g>
1811
+ `,
1812
+ metadata: {
1813
+ description: "Ball valve for flow control",
1814
+ tags: ["valve", "control", "shutoff"]
1815
+ }
1816
+ },
1817
+ "valve-gate": {
1818
+ id: "valve-gate",
1819
+ name: "Gate Valve (Powered)",
1820
+ category: "valve",
1821
+ viewBox: { x: 0, y: 0, width: 52, height: 52 },
1822
+ ports: [
1823
+ { id: "inlet", x: 0, y: 36, type: "inlet", direction: "in" },
1824
+ { id: "outlet", x: 52, y: 36, type: "outlet", direction: "out" }
1825
+ ],
1826
+ svgContent: `
1827
+ <g>
1828
+ <!-- Actuator box -->
1829
+ <rect x="16.74" y="0" width="18.02" height="18.02" fill="#edeeef" stroke="#666666" pointer-events="all"/>
1830
+ <!-- Actuator stem -->
1831
+ <path d="M 25.75 18.02 L 25.75 36.05" fill="none" stroke="#666666" stroke-linejoin="round" stroke-miterlimit="10" pointer-events="all"/>
1832
+ <!-- Valve body (two triangles forming diamond) -->
1833
+ <path d="M 0 20.6 L 25.75 36.05 L 0 51.5 Z M 51.5 20.6 L 25.75 36.05 L 51.5 51.5 Z" fill="#edeeef" stroke="#666666" stroke-linejoin="round" stroke-miterlimit="10" pointer-events="all"/>
1834
+ </g>
1835
+ `,
1836
+ metadata: {
1837
+ description: "Gate valve with powered actuator",
1838
+ tags: ["valve", "gate", "powered", "actuator"]
1839
+ }
1840
+ },
1841
+ "pump-centrifugal": {
1842
+ id: "pump-centrifugal",
1843
+ name: "Centrifugal Pump",
1844
+ category: "pump",
1845
+ viewBox: { x: 0, y: 0, width: 80, height: 60 },
1846
+ ports: [
1847
+ { id: "inlet", x: 10, y: 30, type: "inlet", direction: "in" },
1848
+ { id: "outlet", x: 70, y: 30, type: "outlet", direction: "out" }
1849
+ ],
1850
+ svgContent: `
1851
+ <g>
1852
+ <!-- Pump casing -->
1853
+ <circle cx="40" cy="30" r="22" fill="#4fc3f7" stroke="#0277bd" stroke-width="2.5"/>
1854
+ <!-- Impeller -->
1855
+ <path d="M 40 30 L 50 20 M 40 30 L 50 40 M 40 30 L 30 40 M 40 30 L 30 20"
1856
+ stroke="#01579b" stroke-width="2.5" stroke-linecap="round"/>
1857
+ <!-- Center hub -->
1858
+ <circle cx="40" cy="30" r="5" fill="#0277bd" stroke="#01579b" stroke-width="1.5"/>
1859
+ <!-- Inlet pipe -->
1860
+ <line x1="0" y1="30" x2="18" y2="30" stroke="#0277bd" stroke-width="4" stroke-linecap="round"/>
1861
+ <!-- Outlet pipe -->
1862
+ <line x1="62" y1="30" x2="80" y2="30" stroke="#0277bd" stroke-width="4" stroke-linecap="round"/>
1863
+ </g>
1864
+ `,
1865
+ metadata: {
1866
+ description: "Centrifugal pump for fluid transfer",
1867
+ tags: ["pump", "centrifugal", "transfer"]
1868
+ }
1869
+ },
1870
+ "vessel-tank": {
1871
+ id: "vessel-tank",
1872
+ name: "Storage Tank",
1873
+ category: "vessel",
1874
+ viewBox: { x: 0, y: 0, width: 60, height: 80 },
1875
+ ports: [
1876
+ { id: "inlet", x: 30, y: 10, type: "inlet", direction: "in" },
1877
+ { id: "outlet", x: 30, y: 70, type: "outlet", direction: "out" }
1878
+ ],
1879
+ svgContent: `
1880
+ <g>
1881
+ <!-- Tank body -->
1882
+ <rect x="10" y="15" width="40" height="50" fill="#66bb6a" stroke="#2e7d32" stroke-width="2.5" rx="3"/>
1883
+ <!-- Tank bottom (rounded) -->
1884
+ <ellipse cx="30" cy="65" rx="20" ry="8" fill="#66bb6a" stroke="#2e7d32" stroke-width="2.5"/>
1885
+ <!-- Liquid level indicator -->
1886
+ <rect x="12" y="35" width="36" height="28" fill="#4dd0e1" opacity="0.6"/>
1887
+ <!-- Inlet pipe -->
1888
+ <line x1="30" y1="0" x2="30" y2="15" stroke="#2e7d32" stroke-width="3" stroke-linecap="round"/>
1889
+ <!-- Outlet pipe -->
1890
+ <line x1="30" y1="65" x2="30" y2="80" stroke="#2e7d32" stroke-width="3" stroke-linecap="round"/>
1891
+ </g>
1892
+ `,
1893
+ metadata: {
1894
+ description: "Storage vessel for liquids",
1895
+ tags: ["vessel", "tank", "storage"]
1896
+ }
1897
+ },
1898
+ "instrument-indicator": {
1899
+ id: "instrument-indicator",
1900
+ name: "Instrument Indicator",
1901
+ category: "instrument",
1902
+ viewBox: { x: 0, y: 0, width: 40, height: 40 },
1903
+ ports: [
1904
+ { id: "signal", x: 20, y: 40, type: "signal", direction: "bi" }
1905
+ ],
1906
+ svgContent: `
1907
+ <g>
1908
+ <!-- Instrument circle -->
1909
+ <circle cx="20" cy="20" r="15" fill="#fff" stroke="#616161" stroke-width="2"/>
1910
+ <!-- Inner circle -->
1911
+ <circle cx="20" cy="20" r="12" fill="none" stroke="#616161" stroke-width="1"/>
1912
+ <!-- Signal line -->
1913
+ <line x1="20" y1="35" x2="20" y2="40" stroke="#616161" stroke-width="2" stroke-linecap="round"/>
1914
+ </g>
1915
+ `,
1916
+ metadata: {
1917
+ description: "Generic instrument indicator",
1918
+ tags: ["instrument", "indicator", "measurement"]
1919
+ }
1920
+ }
1921
+ };
1922
+ function getSymbolDefinition(symbolId) {
1923
+ return mockSymbolLibrary[symbolId];
1924
+ }
1925
+ function getAvailableSymbols() {
1926
+ return Object.keys(mockSymbolLibrary);
1927
+ }
1928
+
1929
+ // src/components/PIDCanvas/NodeRenderer.tsx
1930
+ var import_jsx_runtime13 = require("react/jsx-runtime");
1931
+ function NodeRenderer({
1932
+ nodes: nodes2,
1933
+ selectedNodeIds,
1934
+ onNodeClick,
1935
+ onNodeDoubleClick,
1936
+ onNodeDragStart,
1937
+ draggingNodeId,
1938
+ dragOffset = { x: 0, y: 0 },
1939
+ enableDrag = false
1940
+ }) {
1941
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("g", { id: "layer-nodes", children: nodes2.filter((node) => node.visible !== false).map((node) => {
1942
+ const symbol = getSymbolDefinition(node.symbolId);
1943
+ const isSelected = selectedNodeIds?.has(node.id) ?? false;
1944
+ const isDragging = draggingNodeId === node.id;
1945
+ if (!symbol) {
1946
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("g", { children: [
1947
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1948
+ "circle",
1949
+ {
1950
+ cx: node.transform.x,
1951
+ cy: node.transform.y,
1952
+ r: "15",
1953
+ fill: "#ff0000",
1954
+ opacity: "0.3",
1955
+ stroke: "#ff0000",
1956
+ strokeWidth: "2"
1957
+ }
1958
+ ),
1959
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1960
+ "text",
1961
+ {
1962
+ x: node.transform.x,
1963
+ y: node.transform.y,
1964
+ textAnchor: "middle",
1965
+ dominantBaseline: "middle",
1966
+ fontSize: "10",
1967
+ fill: "#ff0000",
1968
+ children: "?"
1969
+ }
1970
+ )
1971
+ ] }, node.id);
1972
+ }
1973
+ const transforms = [];
1974
+ const effectiveX = isDragging ? node.transform.x + dragOffset.x : node.transform.x;
1975
+ const effectiveY = isDragging ? node.transform.y + dragOffset.y : node.transform.y;
1976
+ transforms.push(`translate(${effectiveX}, ${effectiveY})`);
1977
+ if (node.transform.rotation) {
1978
+ const centerX = symbol.viewBox.width / 2;
1979
+ const centerY = symbol.viewBox.height / 2;
1980
+ transforms.push(`rotate(${node.transform.rotation}, ${centerX}, ${centerY})`);
1981
+ }
1982
+ if (node.transform.scale && node.transform.scale !== 1) {
1983
+ const centerX = symbol.viewBox.width / 2;
1984
+ const centerY = symbol.viewBox.height / 2;
1985
+ transforms.push(
1986
+ `translate(${centerX}, ${centerY}) scale(${node.transform.scale}) translate(${-centerX}, ${-centerY})`
1987
+ );
1988
+ }
1989
+ if (node.transform.flipX || node.transform.flipY) {
1990
+ const scaleX = node.transform.flipX ? -1 : 1;
1991
+ const scaleY = node.transform.flipY ? -1 : 1;
1992
+ const centerX = symbol.viewBox.width / 2;
1993
+ const centerY = symbol.viewBox.height / 2;
1994
+ transforms.push(
1995
+ `translate(${centerX}, ${centerY}) scale(${scaleX}, ${scaleY}) translate(${-centerX}, ${-centerY})`
1996
+ );
1997
+ }
1998
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
1999
+ "g",
2000
+ {
2001
+ "data-node-id": node.id,
2002
+ "data-symbol-id": node.symbolId,
2003
+ "data-selected": isSelected,
2004
+ "data-dragging": isDragging,
2005
+ transform: transforms.join(" "),
2006
+ onClick: (e) => {
2007
+ e.stopPropagation();
2008
+ onNodeClick?.(node.id, e);
2009
+ },
2010
+ onDoubleClick: (e) => {
2011
+ e.stopPropagation();
2012
+ onNodeDoubleClick?.(node.id, e);
2013
+ },
2014
+ onMouseDown: (e) => {
2015
+ if (enableDrag) {
2016
+ onNodeDragStart?.(node.id, e);
2017
+ }
2018
+ },
2019
+ onTouchStart: (e) => {
2020
+ if (enableDrag) {
2021
+ e.stopPropagation();
2022
+ onNodeDragStart?.(node.id, e);
2023
+ }
2024
+ },
2025
+ style: {
2026
+ cursor: enableDrag ? isDragging ? "grabbing" : "grab" : onNodeClick ? "pointer" : "default"
2027
+ },
2028
+ children: [
2029
+ isSelected && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2030
+ "rect",
2031
+ {
2032
+ x: "-5",
2033
+ y: "-5",
2034
+ width: symbol.viewBox.width + 10,
2035
+ height: symbol.viewBox.height + 10,
2036
+ fill: "none",
2037
+ stroke: isDragging ? "#10b981" : "#3b82f6",
2038
+ strokeWidth: isDragging ? "3" : "2",
2039
+ strokeDasharray: "4 2",
2040
+ rx: "2",
2041
+ pointerEvents: "none"
2042
+ }
2043
+ ),
2044
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2045
+ "g",
2046
+ {
2047
+ dangerouslySetInnerHTML: { __html: symbol.svgContent },
2048
+ "data-symbol-content": symbol.id,
2049
+ style: { pointerEvents: "auto" }
2050
+ }
2051
+ ),
2052
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2053
+ "text",
2054
+ {
2055
+ x: symbol.viewBox.width / 2 + (node.labelOffset?.x ?? 0),
2056
+ y: symbol.viewBox.height + 12 + (node.labelOffset?.y ?? 0),
2057
+ textAnchor: "middle",
2058
+ fontSize: node.labelFontSize ?? 10,
2059
+ fontWeight: "600",
2060
+ fill: isSelected ? "#3b82f6" : "#1f2937",
2061
+ transform: node.transform.rotation ? `rotate(${-node.transform.rotation}, ${symbol.viewBox.width / 2 + (node.labelOffset?.x ?? 0)}, ${symbol.viewBox.height + 12 + (node.labelOffset?.y ?? 0)})` : void 0,
2062
+ children: node.label || node.id
2063
+ }
2064
+ )
2065
+ ]
2066
+ },
2067
+ node.id
2068
+ );
2069
+ }) });
2070
+ }
2071
+
2072
+ // src/components/PIDCanvas/PipeRenderer.tsx
2073
+ var import_jsx_runtime14 = require("react/jsx-runtime");
2074
+ function PipeRenderer({
2075
+ pipes,
2076
+ selectedPipeIds,
2077
+ onPipeClick,
2078
+ enableWaypointEditing = false,
2079
+ onWaypointDragStart,
2080
+ editingPipeId,
2081
+ draggingWaypointIndex,
2082
+ waypointDragOffset,
2083
+ onPipeSegmentClick
2084
+ }) {
2085
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("g", { id: "layer-pipes", children: pipes.filter((pipe) => pipe.visible !== false).map((pipe) => {
2086
+ const isSelected = selectedPipeIds?.has(pipe.id) ?? false;
2087
+ const isEditing = enableWaypointEditing && editingPipeId === pipe.id;
2088
+ const showEditingControls = enableWaypointEditing && isSelected;
2089
+ const displayPoints = pipe.routePoints.map((point, idx) => {
2090
+ if (isEditing && draggingWaypointIndex === idx && waypointDragOffset) {
2091
+ return {
2092
+ x: point.x + waypointDragOffset.x,
2093
+ y: point.y + waypointDragOffset.y
2094
+ };
2095
+ }
2096
+ return point;
2097
+ });
2098
+ const pointsString = displayPoints.map((point) => `${point.x},${point.y}`).join(" ");
2099
+ const pipeStyle = pipe.style || {};
2100
+ const stroke = isSelected ? "#3b82f6" : pipeStyle.stroke || "#263238";
2101
+ const strokeWidth = isSelected ? (pipeStyle.strokeWidth || 3) + 2 : pipeStyle.strokeWidth || 3;
2102
+ const strokeDasharray = pipeStyle.strokeDasharray;
2103
+ const opacity = pipeStyle.opacity !== void 0 ? pipeStyle.opacity : 1;
2104
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
2105
+ "g",
2106
+ {
2107
+ "data-pipe-id": pipe.id,
2108
+ "data-selected": isSelected,
2109
+ onClick: (e) => {
2110
+ e.stopPropagation();
2111
+ onPipeClick?.(pipe.id, e);
2112
+ },
2113
+ style: {
2114
+ cursor: onPipeClick ? "pointer" : "default"
2115
+ },
2116
+ children: [
2117
+ !enableWaypointEditing && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
2118
+ onPipeClick && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2119
+ "polyline",
2120
+ {
2121
+ points: pointsString,
2122
+ fill: "none",
2123
+ stroke: "transparent",
2124
+ strokeWidth: strokeWidth + 8,
2125
+ strokeLinecap: "round",
2126
+ strokeLinejoin: "round",
2127
+ pointerEvents: "stroke"
2128
+ }
2129
+ ),
2130
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2131
+ "polyline",
2132
+ {
2133
+ points: pointsString,
2134
+ fill: "none",
2135
+ stroke,
2136
+ strokeWidth,
2137
+ strokeDasharray,
2138
+ strokeLinecap: "round",
2139
+ strokeLinejoin: "round",
2140
+ opacity,
2141
+ "data-from": pipe.fromNodeId,
2142
+ "data-to": pipe.toNodeId,
2143
+ pointerEvents: onPipeClick ? "none" : "auto"
2144
+ }
2145
+ )
2146
+ ] }),
2147
+ displayPoints.map((point, idx) => {
2148
+ const isEndpoint = idx === 0 || idx === displayPoints.length - 1;
2149
+ const isDragging = isEditing && draggingWaypointIndex === idx;
2150
+ const waypointSize = isDragging ? 8 : showEditingControls ? 6 : isSelected ? 3 : 2;
2151
+ const waypointOpacity = isDragging ? 1 : showEditingControls ? 0.9 : isSelected ? 0.8 : 0.5;
2152
+ const waypointColor = isEndpoint && showEditingControls ? "#f59e0b" : isDragging ? "#10b981" : stroke;
2153
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2154
+ "circle",
2155
+ {
2156
+ cx: point.x,
2157
+ cy: point.y,
2158
+ r: waypointSize,
2159
+ fill: waypointColor,
2160
+ stroke: showEditingControls ? "#ffffff" : "none",
2161
+ strokeWidth: showEditingControls ? 2 : 0,
2162
+ opacity: waypointOpacity,
2163
+ className: "pipe-route-point",
2164
+ style: {
2165
+ cursor: showEditingControls ? isDragging ? "grabbing" : "grab" : "default"
2166
+ },
2167
+ pointerEvents: showEditingControls ? "all" : "none",
2168
+ onMouseDown: (e) => {
2169
+ if (showEditingControls) {
2170
+ e.stopPropagation();
2171
+ onWaypointDragStart?.(pipe.id, idx, e);
2172
+ }
2173
+ },
2174
+ onTouchStart: (e) => {
2175
+ if (showEditingControls) {
2176
+ e.stopPropagation();
2177
+ onWaypointDragStart?.(pipe.id, idx, e);
2178
+ }
2179
+ }
2180
+ },
2181
+ `${pipe.id}-point-${idx}`
2182
+ );
2183
+ }),
2184
+ showEditingControls && onPipeSegmentClick && displayPoints.length >= 2 && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_jsx_runtime14.Fragment, { children: displayPoints.slice(0, -1).map((point, idx) => {
2185
+ const nextPoint = displayPoints[idx + 1];
2186
+ const midX = (point.x + nextPoint.x) / 2;
2187
+ const midY = (point.y + nextPoint.y) / 2;
2188
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("g", { children: [
2189
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2190
+ "circle",
2191
+ {
2192
+ cx: midX,
2193
+ cy: midY,
2194
+ r: 8,
2195
+ fill: "transparent",
2196
+ className: "pipe-segment-add-area",
2197
+ style: { cursor: "crosshair" },
2198
+ pointerEvents: "all",
2199
+ onClick: (e) => {
2200
+ e.stopPropagation();
2201
+ onPipeSegmentClick?.(pipe.id, { x: midX, y: midY }, idx);
2202
+ }
2203
+ }
2204
+ ),
2205
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2206
+ "circle",
2207
+ {
2208
+ cx: midX,
2209
+ cy: midY,
2210
+ r: 4,
2211
+ fill: "#3b82f6",
2212
+ stroke: "#ffffff",
2213
+ strokeWidth: 2,
2214
+ opacity: 0.6,
2215
+ className: "pipe-segment-add",
2216
+ pointerEvents: "none"
2217
+ }
2218
+ )
2219
+ ] }, `${pipe.id}-segment-${idx}`);
2220
+ }) }),
2221
+ pipe.showLabel !== false && pipe.label && pipe.routePoints.length >= 2 && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2222
+ "text",
2223
+ {
2224
+ x: pipe.routePoints[Math.floor(pipe.routePoints.length / 2)].x + (pipe.labelOffset?.x ?? 0),
2225
+ y: pipe.routePoints[Math.floor(pipe.routePoints.length / 2)].y - 8 + (pipe.labelOffset?.y ?? 0),
2226
+ textAnchor: "middle",
2227
+ fontSize: "10",
2228
+ fontWeight: isSelected ? "600" : "500",
2229
+ fill: stroke,
2230
+ opacity: opacity * 0.8,
2231
+ pointerEvents: "none",
2232
+ children: pipe.label
2233
+ }
2234
+ )
2235
+ ]
2236
+ },
2237
+ pipe.id
2238
+ );
2239
+ }) });
2240
+ }
2241
+
2242
+ // src/components/PIDCanvas/DataOverlay.tsx
2243
+ var import_jsx_runtime15 = require("react/jsx-runtime");
2244
+ function DataOverlay({ nodeId, x, y, telemetry }) {
2245
+ const { value, display, history } = telemetry;
2246
+ const showValue = display?.showValue ?? true;
2247
+ const showStatus = display?.showStatus ?? true;
2248
+ const showSparkline = display?.showSparkline ?? false;
2249
+ const label = display?.label;
2250
+ const precision = display?.precision;
2251
+ const sparklineColor = display?.sparklineColor;
2252
+ const textColor = display?.textColor;
2253
+ const backgroundColor = display?.backgroundColor;
2254
+ const offsetX = display?.offsetX ?? 0;
2255
+ const offsetY = display?.offsetY ?? 0;
2256
+ if (!showValue) {
2257
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_jsx_runtime15.Fragment, {});
2258
+ }
2259
+ const formatValue = (val) => {
2260
+ if (typeof val === "number") {
2261
+ return precision !== void 0 ? val.toFixed(precision) : val.toString();
2262
+ }
2263
+ if (typeof val === "boolean") {
2264
+ return val ? "ON" : "OFF";
2265
+ }
2266
+ return val.toString();
2267
+ };
2268
+ const defaultStatusColors = {
2269
+ normal: { bg: "#10b981", text: "#ffffff", border: "#059669" },
2270
+ warning: { bg: "#f59e0b", text: "#ffffff", border: "#d97706" },
2271
+ alarm: { bg: "#ef4444", text: "#ffffff", border: "#dc2626" },
2272
+ fault: { bg: "#dc2626", text: "#ffffff", border: "#b91c1c" },
2273
+ off: { bg: "#6b7280", text: "#ffffff", border: "#4b5563" }
2274
+ };
2275
+ const customColor = telemetry.statusColors?.[value.status];
2276
+ const defaultColorSet = defaultStatusColors[value.status] || defaultStatusColors.normal;
2277
+ const statusColors = customColor ? { bg: customColor, text: "#ffffff", border: customColor } : defaultColorSet;
2278
+ const colors = backgroundColor && backgroundColor !== "status" ? { bg: backgroundColor, text: textColor || "#ffffff", border: backgroundColor } : statusColors;
2279
+ const finalTextColor = textColor || colors.text;
2280
+ const formattedValue = formatValue(value.value);
2281
+ const displayText = value.unit ? `${formattedValue} ${value.unit}` : formattedValue;
2282
+ const sparklinePath = (() => {
2283
+ if (!showSparkline || !history || history.length < 2) return "";
2284
+ const width = 70;
2285
+ const height = 20;
2286
+ const padding = 5;
2287
+ const effectiveWidth = width - padding * 2;
2288
+ const effectiveHeight = height - padding * 2;
2289
+ const min = Math.min(...history);
2290
+ const max = Math.max(...history);
2291
+ const range = max - min || 1;
2292
+ const points = history.map((val, i) => {
2293
+ const xPos = i / (history.length - 1) * effectiveWidth + padding;
2294
+ const yPos = height - padding - (val - min) / range * effectiveHeight;
2295
+ return `${xPos.toFixed(2)},${yPos.toFixed(2)}`;
2296
+ });
2297
+ return `M ${points.join(" L ")}`;
2298
+ })();
2299
+ const hasSparkline = showSparkline && sparklinePath;
2300
+ const overlayHeight = label ? hasSparkline ? 55 : 30 : hasSparkline ? 47 : 22;
2301
+ const displayX = x + offsetX;
2302
+ const displayY = y + offsetY;
2303
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("g", { "data-overlay-node": nodeId, "data-status": value.status, children: [
2304
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2305
+ "rect",
2306
+ {
2307
+ x: displayX - 40,
2308
+ y: displayY - 25,
2309
+ width: "80",
2310
+ height: overlayHeight,
2311
+ rx: "4",
2312
+ fill: colors.bg,
2313
+ stroke: colors.border,
2314
+ strokeWidth: "1",
2315
+ opacity: "0.95",
2316
+ filter: "drop-shadow(0 2px 4px rgba(0,0,0,0.2))"
2317
+ }
2318
+ ),
2319
+ label && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2320
+ "text",
2321
+ {
2322
+ x: displayX,
2323
+ y: displayY - 15,
2324
+ textAnchor: "middle",
2325
+ fontSize: "8",
2326
+ fontWeight: "600",
2327
+ fill: finalTextColor,
2328
+ opacity: "0.9",
2329
+ children: label
2330
+ }
2331
+ ),
2332
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2333
+ "text",
2334
+ {
2335
+ x: displayX,
2336
+ y: label ? displayY - 3 : displayY - 9,
2337
+ textAnchor: "middle",
2338
+ fontSize: "11",
2339
+ fontWeight: "700",
2340
+ fill: finalTextColor,
2341
+ letterSpacing: "0.02em",
2342
+ children: displayText
2343
+ }
2344
+ ),
2345
+ showStatus && value.status !== "normal" && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("circle", { cx: displayX + 35, cy: displayY - 18, r: "3", fill: finalTextColor, opacity: "0.9", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2346
+ "animate",
2347
+ {
2348
+ attributeName: "opacity",
2349
+ values: "0.9;0.3;0.9",
2350
+ dur: "1.5s",
2351
+ repeatCount: "indefinite"
2352
+ }
2353
+ ) }),
2354
+ value.quality !== void 0 && value.quality < 100 && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
2355
+ "text",
2356
+ {
2357
+ x: displayX,
2358
+ y: displayY + 5,
2359
+ textAnchor: "middle",
2360
+ fontSize: "7",
2361
+ fontWeight: "500",
2362
+ fill: finalTextColor,
2363
+ opacity: "0.7",
2364
+ children: [
2365
+ "Q: ",
2366
+ value.quality,
2367
+ "%"
2368
+ ]
2369
+ }
2370
+ ),
2371
+ hasSparkline && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("g", { transform: `translate(${displayX - 35}, ${displayY + (label ? 5 : 0)})`, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2372
+ "path",
2373
+ {
2374
+ d: sparklinePath,
2375
+ fill: "none",
2376
+ stroke: sparklineColor || finalTextColor,
2377
+ strokeWidth: "1.5",
2378
+ strokeLinecap: "round",
2379
+ strokeLinejoin: "round",
2380
+ opacity: "0.9"
2381
+ }
2382
+ ) })
2383
+ ] });
2384
+ }
2385
+
2386
+ // src/diagram/hooks/useViewBox.ts
2387
+ var import_react8 = require("react");
2388
+ function useViewBox(options) {
2389
+ const {
2390
+ initialViewBox,
2391
+ enablePan = true,
2392
+ enableZoom = true,
2393
+ minZoom = 0.1,
2394
+ maxZoom = 10,
2395
+ zoomSensitivity = 1e-3,
2396
+ allowLeftClickPan = true
2397
+ } = options;
2398
+ const [viewBox, setViewBox] = (0, import_react8.useState)(initialViewBox);
2399
+ const svgRef = (0, import_react8.useRef)(null);
2400
+ const isPanningRef = (0, import_react8.useRef)(false);
2401
+ const lastMousePosRef = (0, import_react8.useRef)({ x: 0, y: 0 });
2402
+ const spaceKeyDownRef = (0, import_react8.useRef)(false);
2403
+ const lastTouchDistanceRef = (0, import_react8.useRef)(null);
2404
+ const resetViewBox = (0, import_react8.useCallback)(() => {
2405
+ setViewBox(initialViewBox);
2406
+ }, [initialViewBox]);
2407
+ const zoomIn = (0, import_react8.useCallback)(() => {
2408
+ setViewBox((prev) => {
2409
+ const currentZoom = initialViewBox.width / prev.width;
2410
+ const newZoom = Math.min(1 / minZoom, currentZoom * 1.25);
2411
+ const scale = initialViewBox.width / newZoom;
2412
+ const centerX = prev.x + prev.width / 2;
2413
+ const centerY = prev.y + prev.height / 2;
2414
+ const newWidth = scale;
2415
+ const newHeight = prev.height / prev.width * scale;
2416
+ return {
2417
+ x: centerX - newWidth / 2,
2418
+ y: centerY - newHeight / 2,
2419
+ width: newWidth,
2420
+ height: newHeight
2421
+ };
2422
+ });
2423
+ }, [initialViewBox, minZoom]);
2424
+ const zoomOut = (0, import_react8.useCallback)(() => {
2425
+ setViewBox((prev) => {
2426
+ const currentZoom = initialViewBox.width / prev.width;
2427
+ const newZoom = Math.max(1 / maxZoom, currentZoom * 0.8);
2428
+ const scale = initialViewBox.width / newZoom;
2429
+ const centerX = prev.x + prev.width / 2;
2430
+ const centerY = prev.y + prev.height / 2;
2431
+ const newWidth = scale;
2432
+ const newHeight = prev.height / prev.width * scale;
2433
+ return {
2434
+ x: centerX - newWidth / 2,
2435
+ y: centerY - newHeight / 2,
2436
+ width: newWidth,
2437
+ height: newHeight
2438
+ };
2439
+ });
2440
+ }, [initialViewBox, maxZoom]);
2441
+ (0, import_react8.useEffect)(() => {
2442
+ if (!enableZoom || !svgRef.current) return;
2443
+ const svg = svgRef.current;
2444
+ const handleWheel = (e) => {
2445
+ e.preventDefault();
2446
+ const rect = svg.getBoundingClientRect();
2447
+ const mouseX = e.clientX - rect.left;
2448
+ const mouseY = e.clientY - rect.top;
2449
+ setViewBox((prev) => {
2450
+ const svgX = prev.x + mouseX / rect.width * prev.width;
2451
+ const svgY = prev.y + mouseY / rect.height * prev.height;
2452
+ const delta = -e.deltaY * zoomSensitivity;
2453
+ const zoom = Math.exp(delta);
2454
+ let newWidth = prev.width / zoom;
2455
+ let newHeight = prev.height / zoom;
2456
+ const currentZoom = initialViewBox.width / prev.width;
2457
+ const newZoom = currentZoom * zoom;
2458
+ if (newZoom < minZoom || newZoom > 1 / minZoom) {
2459
+ return prev;
2460
+ }
2461
+ const newX = svgX - mouseX / rect.width * newWidth;
2462
+ const newY = svgY - mouseY / rect.height * newHeight;
2463
+ return {
2464
+ x: newX,
2465
+ y: newY,
2466
+ width: newWidth,
2467
+ height: newHeight
2468
+ };
2469
+ });
2470
+ };
2471
+ svg.addEventListener("wheel", handleWheel, { passive: false });
2472
+ return () => svg.removeEventListener("wheel", handleWheel);
2473
+ }, [enableZoom, zoomSensitivity, minZoom, initialViewBox]);
2474
+ (0, import_react8.useEffect)(() => {
2475
+ if (!enablePan || !svgRef.current) return;
2476
+ const svg = svgRef.current;
2477
+ const handleMouseDown = (e) => {
2478
+ const target = e.target;
2479
+ const isDraggableNode = target.closest("[data-node-id]");
2480
+ const isWaypoint = target.classList.contains("pipe-route-point");
2481
+ const isPipeSegment = target.classList.contains("pipe-segment-add") || target.classList.contains("pipe-segment-add-area");
2482
+ if (isDraggableNode || isWaypoint || isPipeSegment) {
2483
+ return;
2484
+ }
2485
+ const isLeftClick = e.button === 0 && allowLeftClickPan;
2486
+ const isMiddleClick = e.button === 1;
2487
+ const isSpaceLeftClick = e.button === 0 && spaceKeyDownRef.current;
2488
+ if (isLeftClick || isMiddleClick || isSpaceLeftClick) {
2489
+ e.preventDefault();
2490
+ isPanningRef.current = true;
2491
+ lastMousePosRef.current = { x: e.clientX, y: e.clientY };
2492
+ svg.style.cursor = "grabbing";
2493
+ }
2494
+ };
2495
+ const handleMouseMove = (e) => {
2496
+ if (!isPanningRef.current) return;
2497
+ const dx = e.clientX - lastMousePosRef.current.x;
2498
+ const dy = e.clientY - lastMousePosRef.current.y;
2499
+ lastMousePosRef.current = { x: e.clientX, y: e.clientY };
2500
+ setViewBox((prev) => {
2501
+ const rect = svg.getBoundingClientRect();
2502
+ const scaleX = prev.width / rect.width;
2503
+ const scaleY = prev.height / rect.height;
2504
+ return {
2505
+ ...prev,
2506
+ x: prev.x - dx * scaleX,
2507
+ y: prev.y - dy * scaleY
2508
+ };
2509
+ });
2510
+ };
2511
+ const handleMouseUp = () => {
2512
+ if (isPanningRef.current) {
2513
+ isPanningRef.current = false;
2514
+ svg.style.cursor = allowLeftClickPan || spaceKeyDownRef.current ? "grab" : "";
2515
+ }
2516
+ };
2517
+ const handleKeyDown = (e) => {
2518
+ if (e.code === "Space" && !spaceKeyDownRef.current) {
2519
+ e.preventDefault();
2520
+ spaceKeyDownRef.current = true;
2521
+ if (!isPanningRef.current && !allowLeftClickPan) {
2522
+ svg.style.cursor = "grab";
2523
+ }
2524
+ }
2525
+ };
2526
+ const handleKeyUp = (e) => {
2527
+ if (e.code === "Space") {
2528
+ spaceKeyDownRef.current = false;
2529
+ if (!isPanningRef.current && !allowLeftClickPan) {
2530
+ svg.style.cursor = "";
2531
+ }
2532
+ }
2533
+ };
2534
+ if (allowLeftClickPan && !isPanningRef.current) {
2535
+ svg.style.cursor = "grab";
2536
+ }
2537
+ svg.addEventListener("mousedown", handleMouseDown);
2538
+ window.addEventListener("mousemove", handleMouseMove);
2539
+ window.addEventListener("mouseup", handleMouseUp);
2540
+ window.addEventListener("keydown", handleKeyDown);
2541
+ window.addEventListener("keyup", handleKeyUp);
2542
+ return () => {
2543
+ svg.removeEventListener("mousedown", handleMouseDown);
2544
+ window.removeEventListener("mousemove", handleMouseMove);
2545
+ window.removeEventListener("mouseup", handleMouseUp);
2546
+ window.removeEventListener("keydown", handleKeyDown);
2547
+ window.removeEventListener("keyup", handleKeyUp);
2548
+ };
2549
+ }, [enablePan, allowLeftClickPan]);
2550
+ (0, import_react8.useEffect)(() => {
2551
+ if (!svgRef.current) return;
2552
+ if (!enablePan && !enableZoom) return;
2553
+ const svg = svgRef.current;
2554
+ const handleTouchStart = (e) => {
2555
+ const target = e.target;
2556
+ const isDraggableNode = target.closest("[data-node-id]");
2557
+ const isWaypoint = target.classList.contains("pipe-route-point");
2558
+ const isPipeSegment = target.classList.contains("pipe-segment-add") || target.classList.contains("pipe-segment-add-area");
2559
+ if ((isDraggableNode || isWaypoint || isPipeSegment) && e.touches.length === 1) {
2560
+ return;
2561
+ }
2562
+ if (e.touches.length === 1 && enablePan) {
2563
+ isPanningRef.current = true;
2564
+ lastMousePosRef.current = {
2565
+ x: e.touches[0].clientX,
2566
+ y: e.touches[0].clientY
2567
+ };
2568
+ } else if (e.touches.length === 2 && enableZoom) {
2569
+ const touch1 = e.touches[0];
2570
+ const touch2 = e.touches[1];
2571
+ const distance = Math.hypot(
2572
+ touch2.clientX - touch1.clientX,
2573
+ touch2.clientY - touch1.clientY
2574
+ );
2575
+ lastTouchDistanceRef.current = distance;
2576
+ }
2577
+ };
2578
+ const handleTouchMove = (e) => {
2579
+ e.preventDefault();
2580
+ if (e.touches.length === 1 && isPanningRef.current && enablePan) {
2581
+ const touch = e.touches[0];
2582
+ const dx = touch.clientX - lastMousePosRef.current.x;
2583
+ const dy = touch.clientY - lastMousePosRef.current.y;
2584
+ lastMousePosRef.current = { x: touch.clientX, y: touch.clientY };
2585
+ setViewBox((prev) => {
2586
+ const rect = svg.getBoundingClientRect();
2587
+ const scaleX = prev.width / rect.width;
2588
+ const scaleY = prev.height / rect.height;
2589
+ return {
2590
+ ...prev,
2591
+ x: prev.x - dx * scaleX,
2592
+ y: prev.y - dy * scaleY
2593
+ };
2594
+ });
2595
+ } else if (e.touches.length === 2 && enableZoom && lastTouchDistanceRef.current) {
2596
+ const touch1 = e.touches[0];
2597
+ const touch2 = e.touches[1];
2598
+ const distance = Math.hypot(
2599
+ touch2.clientX - touch1.clientX,
2600
+ touch2.clientY - touch1.clientY
2601
+ );
2602
+ const scale = distance / lastTouchDistanceRef.current;
2603
+ lastTouchDistanceRef.current = distance;
2604
+ const centerX = (touch1.clientX + touch2.clientX) / 2;
2605
+ const centerY = (touch1.clientY + touch2.clientY) / 2;
2606
+ const rect = svg.getBoundingClientRect();
2607
+ setViewBox((prev) => {
2608
+ const svgX = prev.x + (centerX - rect.left) / rect.width * prev.width;
2609
+ const svgY = prev.y + (centerY - rect.top) / rect.height * prev.height;
2610
+ let newWidth = prev.width / scale;
2611
+ let newHeight = prev.height / scale;
2612
+ const currentZoom = initialViewBox.width / prev.width;
2613
+ const newZoom = currentZoom * scale;
2614
+ if (newZoom < minZoom || newZoom > 1 / minZoom) {
2615
+ return prev;
2616
+ }
2617
+ const newX = svgX - (centerX - rect.left) / rect.width * newWidth;
2618
+ const newY = svgY - (centerY - rect.top) / rect.height * newHeight;
2619
+ return {
2620
+ x: newX,
2621
+ y: newY,
2622
+ width: newWidth,
2623
+ height: newHeight
2624
+ };
2625
+ });
2626
+ }
2627
+ };
2628
+ const handleTouchEnd = () => {
2629
+ isPanningRef.current = false;
2630
+ lastTouchDistanceRef.current = null;
2631
+ };
2632
+ svg.addEventListener("touchstart", handleTouchStart);
2633
+ svg.addEventListener("touchmove", handleTouchMove, { passive: false });
2634
+ svg.addEventListener("touchend", handleTouchEnd);
2635
+ return () => {
2636
+ svg.removeEventListener("touchstart", handleTouchStart);
2637
+ svg.removeEventListener("touchmove", handleTouchMove);
2638
+ svg.removeEventListener("touchend", handleTouchEnd);
2639
+ };
2640
+ }, [enablePan, enableZoom, minZoom, initialViewBox]);
2641
+ const screenToWorld = (0, import_react8.useCallback)(
2642
+ (screenX, screenY) => {
2643
+ const svg = svgRef.current;
2644
+ if (!svg) return { x: screenX, y: screenY };
2645
+ const rect = svg.getBoundingClientRect();
2646
+ const svgAspect = rect.width / rect.height;
2647
+ const viewBoxAspect = viewBox.width / viewBox.height;
2648
+ let actualWidth = rect.width;
2649
+ let actualHeight = rect.height;
2650
+ let offsetX = 0;
2651
+ let offsetY = 0;
2652
+ if (svgAspect > viewBoxAspect) {
2653
+ actualWidth = rect.height * viewBoxAspect;
2654
+ offsetX = (rect.width - actualWidth) / 2;
2655
+ } else {
2656
+ actualHeight = rect.width / viewBoxAspect;
2657
+ offsetY = (rect.height - actualHeight) / 2;
2658
+ }
2659
+ const relativeX = (screenX - rect.left - offsetX) / actualWidth;
2660
+ const relativeY = (screenY - rect.top - offsetY) / actualHeight;
2661
+ const worldX = viewBox.x + relativeX * viewBox.width;
2662
+ const worldY = viewBox.y + relativeY * viewBox.height;
2663
+ return { x: worldX, y: worldY };
2664
+ },
2665
+ [viewBox]
2666
+ );
2667
+ return {
2668
+ viewBox,
2669
+ resetViewBox,
2670
+ zoomIn,
2671
+ zoomOut,
2672
+ screenToWorld,
2673
+ svgRef
2674
+ };
2675
+ }
2676
+
2677
+ // src/diagram/hooks/useSelection.ts
2678
+ var import_react9 = require("react");
2679
+ function useSelection(options = {}) {
2680
+ const {
2681
+ multiSelect = true,
2682
+ initialSelection = { selectedNodeIds: /* @__PURE__ */ new Set(), selectedPipeIds: /* @__PURE__ */ new Set() },
2683
+ onSelectionChange
2684
+ } = options;
2685
+ const [selection, setSelection] = (0, import_react9.useState)(initialSelection);
2686
+ const notifyChange = (0, import_react9.useCallback)(
2687
+ (newSelection) => {
2688
+ if (onSelectionChange) {
2689
+ onSelectionChange(newSelection);
2690
+ }
2691
+ },
2692
+ [onSelectionChange]
2693
+ );
2694
+ const isNodeSelected = (0, import_react9.useCallback)(
2695
+ (nodeId) => selection.selectedNodeIds.has(nodeId),
2696
+ [selection.selectedNodeIds]
2697
+ );
2698
+ const isPipeSelected = (0, import_react9.useCallback)(
2699
+ (pipeId) => selection.selectedPipeIds.has(pipeId),
2700
+ [selection.selectedPipeIds]
2701
+ );
2702
+ const selectNode = (0, import_react9.useCallback)(
2703
+ (nodeId, isMultiSelect = false) => {
2704
+ setSelection((prev) => {
2705
+ const newNodeIds = new Set(isMultiSelect && multiSelect ? prev.selectedNodeIds : []);
2706
+ if (newNodeIds.has(nodeId)) {
2707
+ newNodeIds.delete(nodeId);
2708
+ } else {
2709
+ newNodeIds.add(nodeId);
2710
+ }
2711
+ const newSelection = {
2712
+ selectedNodeIds: newNodeIds,
2713
+ selectedPipeIds: isMultiSelect && multiSelect ? prev.selectedPipeIds : /* @__PURE__ */ new Set()
2714
+ };
2715
+ notifyChange(newSelection);
2716
+ return newSelection;
2717
+ });
2718
+ },
2719
+ [multiSelect, notifyChange]
2720
+ );
2721
+ const selectPipe = (0, import_react9.useCallback)(
2722
+ (pipeId, isMultiSelect = false) => {
2723
+ setSelection((prev) => {
2724
+ const newPipeIds = new Set(isMultiSelect && multiSelect ? prev.selectedPipeIds : []);
2725
+ if (newPipeIds.has(pipeId)) {
2726
+ newPipeIds.delete(pipeId);
2727
+ } else {
2728
+ newPipeIds.add(pipeId);
2729
+ }
2730
+ const newSelection = {
2731
+ selectedNodeIds: isMultiSelect && multiSelect ? prev.selectedNodeIds : /* @__PURE__ */ new Set(),
2732
+ selectedPipeIds: newPipeIds
2733
+ };
2734
+ notifyChange(newSelection);
2735
+ return newSelection;
2736
+ });
2737
+ },
2738
+ [multiSelect, notifyChange]
2739
+ );
2740
+ const clearSelection = (0, import_react9.useCallback)(() => {
2741
+ const newSelection = {
2742
+ selectedNodeIds: /* @__PURE__ */ new Set(),
2743
+ selectedPipeIds: /* @__PURE__ */ new Set()
2744
+ };
2745
+ setSelection(newSelection);
2746
+ notifyChange(newSelection);
2747
+ }, [notifyChange]);
2748
+ const selectNodes = (0, import_react9.useCallback)(
2749
+ (nodeIds) => {
2750
+ const newSelection = {
2751
+ selectedNodeIds: new Set(nodeIds),
2752
+ selectedPipeIds: /* @__PURE__ */ new Set()
2753
+ };
2754
+ setSelection(newSelection);
2755
+ notifyChange(newSelection);
2756
+ },
2757
+ [notifyChange]
2758
+ );
2759
+ const selectPipes = (0, import_react9.useCallback)(
2760
+ (pipeIds) => {
2761
+ const newSelection = {
2762
+ selectedNodeIds: /* @__PURE__ */ new Set(),
2763
+ selectedPipeIds: new Set(pipeIds)
2764
+ };
2765
+ setSelection(newSelection);
2766
+ notifyChange(newSelection);
2767
+ },
2768
+ [notifyChange]
2769
+ );
2770
+ return {
2771
+ selection,
2772
+ isNodeSelected,
2773
+ isPipeSelected,
2774
+ selectNode,
2775
+ selectPipe,
2776
+ clearSelection,
2777
+ selectNodes,
2778
+ selectPipes
2779
+ };
2780
+ }
2781
+
2782
+ // src/diagram/hooks/useTelemetry.ts
2783
+ var import_react10 = require("react");
2784
+ function useTelemetry(options = {}) {
2785
+ const {
2786
+ initialBindings = [],
2787
+ onTelemetryChange,
2788
+ refreshInterval = 0
2789
+ } = options;
2790
+ const [telemetry, setTelemetry] = (0, import_react10.useState)(() => {
2791
+ const map = /* @__PURE__ */ new Map();
2792
+ initialBindings.forEach((binding) => {
2793
+ map.set(binding.nodeId, binding);
2794
+ });
2795
+ return map;
2796
+ });
2797
+ const telemetryRef = (0, import_react10.useRef)(telemetry);
2798
+ telemetryRef.current = telemetry;
2799
+ const notifyChange = (0, import_react10.useCallback)(
2800
+ (newTelemetry) => {
2801
+ if (onTelemetryChange) {
2802
+ onTelemetryChange(newTelemetry);
2803
+ }
2804
+ },
2805
+ [onTelemetryChange]
2806
+ );
2807
+ const updateTelemetry = (0, import_react10.useCallback)(
2808
+ (nodeId, value) => {
2809
+ setTelemetry((prev) => {
2810
+ const newMap = new Map(prev);
2811
+ const existing = newMap.get(nodeId);
2812
+ if (existing) {
2813
+ newMap.set(nodeId, {
2814
+ ...existing,
2815
+ value: {
2816
+ ...value,
2817
+ timestamp: value.timestamp ?? Date.now()
2818
+ }
2819
+ });
2820
+ } else {
2821
+ newMap.set(nodeId, {
2822
+ nodeId,
2823
+ tag: nodeId,
2824
+ // Default tag to nodeId
2825
+ value: {
2826
+ ...value,
2827
+ timestamp: value.timestamp ?? Date.now()
2828
+ }
2829
+ });
2830
+ }
2831
+ notifyChange(newMap);
2832
+ return newMap;
2833
+ });
2834
+ },
2835
+ [notifyChange]
2836
+ );
2837
+ const updateTelemetryBatch = (0, import_react10.useCallback)(
2838
+ (updates) => {
2839
+ setTelemetry((prev) => {
2840
+ const newMap = new Map(prev);
2841
+ let hasChanges = false;
2842
+ updates.forEach(({ nodeId, tag, value, history, display, thresholds, statusColors, remove }) => {
2843
+ if (remove) {
2844
+ if (newMap.has(nodeId)) {
2845
+ newMap.delete(nodeId);
2846
+ hasChanges = true;
2847
+ }
2848
+ return;
2849
+ }
2850
+ const existing = newMap.get(nodeId);
2851
+ if (existing) {
2852
+ newMap.set(nodeId, {
2853
+ ...existing,
2854
+ ...tag !== void 0 && { tag },
2855
+ ...value !== void 0 && {
2856
+ value: {
2857
+ ...value,
2858
+ timestamp: value.timestamp ?? Date.now()
2859
+ }
2860
+ },
2861
+ ...history !== void 0 && { history },
2862
+ ...display !== void 0 && { display },
2863
+ ...thresholds !== void 0 && { thresholds },
2864
+ ...statusColors !== void 0 && { statusColors }
2865
+ });
2866
+ hasChanges = true;
2867
+ } else if (value) {
2868
+ newMap.set(nodeId, {
2869
+ nodeId,
2870
+ tag: tag ?? nodeId,
2871
+ value: {
2872
+ ...value,
2873
+ timestamp: value.timestamp ?? Date.now()
2874
+ },
2875
+ ...history !== void 0 && { history },
2876
+ ...display !== void 0 && { display },
2877
+ ...thresholds !== void 0 && { thresholds },
2878
+ ...statusColors !== void 0 && { statusColors }
2879
+ });
2880
+ hasChanges = true;
2881
+ }
2882
+ });
2883
+ if (hasChanges) {
2884
+ notifyChange(newMap);
2885
+ }
2886
+ return newMap;
2887
+ });
2888
+ },
2889
+ [notifyChange]
2890
+ );
2891
+ const getTelemetry = (0, import_react10.useCallback)(
2892
+ (nodeId) => telemetryRef.current.get(nodeId),
2893
+ []
2894
+ );
2895
+ const clearTelemetry = (0, import_react10.useCallback)(() => {
2896
+ const newMap = /* @__PURE__ */ new Map();
2897
+ setTelemetry(newMap);
2898
+ notifyChange(newMap);
2899
+ }, [notifyChange]);
2900
+ const setBindings = (0, import_react10.useCallback)(
2901
+ (bindings) => {
2902
+ const newMap = /* @__PURE__ */ new Map();
2903
+ bindings.forEach((binding) => {
2904
+ newMap.set(binding.nodeId, binding);
2905
+ });
2906
+ setTelemetry(newMap);
2907
+ notifyChange(newMap);
2908
+ },
2909
+ [notifyChange]
2910
+ );
2911
+ (0, import_react10.useEffect)(() => {
2912
+ if (refreshInterval <= 0) return;
2913
+ const intervalId = setInterval(() => {
2914
+ notifyChange(telemetryRef.current);
2915
+ }, refreshInterval);
2916
+ return () => clearInterval(intervalId);
2917
+ }, [refreshInterval, notifyChange]);
2918
+ return {
2919
+ telemetry,
2920
+ updateTelemetry,
2921
+ updateTelemetryBatch,
2922
+ getTelemetry,
2923
+ clearTelemetry,
2924
+ setBindings
2925
+ };
2926
+ }
2927
+
2928
+ // src/diagram/hooks/useDrag.ts
2929
+ var import_react11 = require("react");
2930
+ function useDrag(options = {}) {
2931
+ const {
2932
+ onDragStart,
2933
+ onDragMove,
2934
+ onDragEnd,
2935
+ snapToGrid = 0,
2936
+ dragThreshold = 3,
2937
+ screenToWorld = (x, y) => ({ x, y })
2938
+ } = options;
2939
+ const [dragState, setDragState] = (0, import_react11.useState)({
2940
+ isDragging: false,
2941
+ startPosition: null,
2942
+ offset: { x: 0, y: 0 },
2943
+ draggedId: null
2944
+ });
2945
+ const [isListening, setIsListening] = (0, import_react11.useState)(false);
2946
+ const dragStartRef = (0, import_react11.useRef)(null);
2947
+ const hasMovedRef = (0, import_react11.useRef)(false);
2948
+ const applySnap = (0, import_react11.useCallback)(
2949
+ (value) => {
2950
+ if (snapToGrid <= 0) return value;
2951
+ return Math.round(value / snapToGrid) * snapToGrid;
2952
+ },
2953
+ [snapToGrid]
2954
+ );
2955
+ const handleMouseMove = (0, import_react11.useCallback)(
2956
+ (event) => {
2957
+ if (!dragStartRef.current) return;
2958
+ const { id, screenStart: _screenStart, itemPosition, clickOffset } = dragStartRef.current;
2959
+ const currentWorld = screenToWorld(event.clientX, event.clientY);
2960
+ const newX = currentWorld.x - clickOffset.x;
2961
+ const newY = currentWorld.y - clickOffset.y;
2962
+ const rawOffset = {
2963
+ x: newX - itemPosition.x,
2964
+ y: newY - itemPosition.y
2965
+ };
2966
+ const distance = Math.sqrt(rawOffset.x ** 2 + rawOffset.y ** 2);
2967
+ if (!hasMovedRef.current && distance < dragThreshold) {
2968
+ return;
2969
+ }
2970
+ if (!hasMovedRef.current) {
2971
+ hasMovedRef.current = true;
2972
+ setDragState({
2973
+ isDragging: true,
2974
+ startPosition: itemPosition,
2975
+ offset: { x: 0, y: 0 },
2976
+ draggedId: id
2977
+ });
2978
+ onDragStart?.(id, itemPosition);
2979
+ }
2980
+ const snappedOffset = {
2981
+ x: applySnap(rawOffset.x),
2982
+ y: applySnap(rawOffset.y)
2983
+ };
2984
+ const newPosition = {
2985
+ x: applySnap(itemPosition.x + rawOffset.x),
2986
+ y: applySnap(itemPosition.y + rawOffset.y)
2987
+ };
2988
+ setDragState((prev) => ({
2989
+ ...prev,
2990
+ offset: snappedOffset
2991
+ }));
2992
+ onDragMove?.(id, snappedOffset, newPosition);
2993
+ },
2994
+ [screenToWorld, dragThreshold, applySnap, onDragStart, onDragMove]
2995
+ );
2996
+ const handleMouseUp = (0, import_react11.useCallback)(
2997
+ (event) => {
2998
+ if (!dragStartRef.current || !hasMovedRef.current) {
2999
+ dragStartRef.current = null;
3000
+ hasMovedRef.current = false;
3001
+ setIsListening(false);
3002
+ return;
3003
+ }
3004
+ const { id, itemPosition, clickOffset } = dragStartRef.current;
3005
+ const currentWorld = screenToWorld(event.clientX, event.clientY);
3006
+ const newX = currentWorld.x - clickOffset.x;
3007
+ const newY = currentWorld.y - clickOffset.y;
3008
+ const rawOffset = {
3009
+ x: newX - itemPosition.x,
3010
+ y: newY - itemPosition.y
3011
+ };
3012
+ const snappedOffset = {
3013
+ x: applySnap(rawOffset.x),
3014
+ y: applySnap(rawOffset.y)
3015
+ };
3016
+ const finalPosition = {
3017
+ x: applySnap(itemPosition.x + rawOffset.x),
3018
+ y: applySnap(itemPosition.y + rawOffset.y)
3019
+ };
3020
+ onDragEnd?.(id, snappedOffset, finalPosition);
3021
+ setDragState({
3022
+ isDragging: false,
3023
+ startPosition: null,
3024
+ offset: { x: 0, y: 0 },
3025
+ draggedId: null
3026
+ });
3027
+ dragStartRef.current = null;
3028
+ hasMovedRef.current = false;
3029
+ setIsListening(false);
3030
+ },
3031
+ [screenToWorld, applySnap, onDragEnd]
3032
+ );
3033
+ const handleTouchMove = (0, import_react11.useCallback)(
3034
+ (event) => {
3035
+ if (!dragStartRef.current || event.touches.length !== 1) return;
3036
+ const touch = event.touches[0];
3037
+ const { id, screenStart: _screenStart2, itemPosition, clickOffset } = dragStartRef.current;
3038
+ const currentWorld = screenToWorld(touch.clientX, touch.clientY);
3039
+ const newX = currentWorld.x - clickOffset.x;
3040
+ const newY = currentWorld.y - clickOffset.y;
3041
+ const rawOffset = {
3042
+ x: newX - itemPosition.x,
3043
+ y: newY - itemPosition.y
3044
+ };
3045
+ const distance = Math.sqrt(rawOffset.x ** 2 + rawOffset.y ** 2);
3046
+ if (!hasMovedRef.current && distance < dragThreshold) {
3047
+ return;
3048
+ }
3049
+ event.preventDefault();
3050
+ if (!hasMovedRef.current) {
3051
+ hasMovedRef.current = true;
3052
+ setDragState({
3053
+ isDragging: true,
3054
+ startPosition: itemPosition,
3055
+ offset: { x: 0, y: 0 },
3056
+ draggedId: id
3057
+ });
3058
+ onDragStart?.(id, itemPosition);
3059
+ }
3060
+ const snappedOffset = {
3061
+ x: applySnap(rawOffset.x),
3062
+ y: applySnap(rawOffset.y)
3063
+ };
3064
+ const newPosition = {
3065
+ x: applySnap(itemPosition.x + rawOffset.x),
3066
+ y: applySnap(itemPosition.y + rawOffset.y)
3067
+ };
3068
+ setDragState((prev) => ({
3069
+ ...prev,
3070
+ offset: snappedOffset
3071
+ }));
3072
+ onDragMove?.(id, snappedOffset, newPosition);
3073
+ },
3074
+ [screenToWorld, dragThreshold, applySnap, onDragStart, onDragMove]
3075
+ );
3076
+ const handleTouchEnd = (0, import_react11.useCallback)(
3077
+ (event) => {
3078
+ if (!dragStartRef.current || !hasMovedRef.current) {
3079
+ dragStartRef.current = null;
3080
+ hasMovedRef.current = false;
3081
+ setIsListening(false);
3082
+ return;
3083
+ }
3084
+ const { id, itemPosition, clickOffset } = dragStartRef.current;
3085
+ const touch = event.changedTouches[0];
3086
+ const currentWorld = screenToWorld(touch.clientX, touch.clientY);
3087
+ const newX = currentWorld.x - clickOffset.x;
3088
+ const newY = currentWorld.y - clickOffset.y;
3089
+ const rawOffset = {
3090
+ x: newX - itemPosition.x,
3091
+ y: newY - itemPosition.y
3092
+ };
3093
+ const snappedOffset = {
3094
+ x: applySnap(rawOffset.x),
3095
+ y: applySnap(rawOffset.y)
3096
+ };
3097
+ const finalPosition = {
3098
+ x: applySnap(itemPosition.x + rawOffset.x),
3099
+ y: applySnap(itemPosition.y + rawOffset.y)
3100
+ };
3101
+ onDragEnd?.(id, snappedOffset, finalPosition);
3102
+ setDragState({
3103
+ isDragging: false,
3104
+ startPosition: null,
3105
+ offset: { x: 0, y: 0 },
3106
+ draggedId: null
3107
+ });
3108
+ dragStartRef.current = null;
3109
+ hasMovedRef.current = false;
3110
+ setIsListening(false);
3111
+ },
3112
+ [screenToWorld, applySnap, onDragEnd]
3113
+ );
3114
+ (0, import_react11.useEffect)(() => {
3115
+ if (!isListening) return;
3116
+ document.addEventListener("mousemove", handleMouseMove);
3117
+ document.addEventListener("mouseup", handleMouseUp);
3118
+ document.addEventListener("touchmove", handleTouchMove, { passive: false });
3119
+ document.addEventListener("touchend", handleTouchEnd);
3120
+ document.addEventListener("touchcancel", handleTouchEnd);
3121
+ return () => {
3122
+ document.removeEventListener("mousemove", handleMouseMove);
3123
+ document.removeEventListener("mouseup", handleMouseUp);
3124
+ document.removeEventListener("touchmove", handleTouchMove);
3125
+ document.removeEventListener("touchend", handleTouchEnd);
3126
+ document.removeEventListener("touchcancel", handleTouchEnd);
3127
+ };
3128
+ }, [isListening, handleMouseMove, handleMouseUp, handleTouchMove, handleTouchEnd]);
3129
+ const startDrag = (0, import_react11.useCallback)(
3130
+ (id, event, itemPosition) => {
3131
+ event.stopPropagation();
3132
+ let clientX;
3133
+ let clientY;
3134
+ if ("touches" in event) {
3135
+ if (event.touches.length !== 1) return;
3136
+ clientX = event.touches[0].clientX;
3137
+ clientY = event.touches[0].clientY;
3138
+ } else {
3139
+ clientX = event.clientX;
3140
+ clientY = event.clientY;
3141
+ }
3142
+ const worldStart = screenToWorld(clientX, clientY);
3143
+ const clickOffset = {
3144
+ x: worldStart.x - itemPosition.x,
3145
+ y: worldStart.y - itemPosition.y
3146
+ };
3147
+ dragStartRef.current = {
3148
+ id,
3149
+ screenStart: { x: clientX, y: clientY },
3150
+ worldStart,
3151
+ itemPosition,
3152
+ clickOffset
3153
+ };
3154
+ hasMovedRef.current = false;
3155
+ setIsListening(true);
3156
+ },
3157
+ [screenToWorld]
3158
+ );
3159
+ const cancelDrag = (0, import_react11.useCallback)(() => {
3160
+ dragStartRef.current = null;
3161
+ hasMovedRef.current = false;
3162
+ setIsListening(false);
3163
+ setDragState({
3164
+ isDragging: false,
3165
+ startPosition: null,
3166
+ offset: { x: 0, y: 0 },
3167
+ draggedId: null
3168
+ });
3169
+ }, []);
3170
+ const isDraggingItem = (0, import_react11.useCallback)(
3171
+ (id) => dragState.isDragging && dragState.draggedId === id,
3172
+ [dragState.isDragging, dragState.draggedId]
3173
+ );
3174
+ return {
3175
+ dragState,
3176
+ startDrag,
3177
+ cancelDrag,
3178
+ isDraggingItem
3179
+ };
3180
+ }
3181
+
3182
+ // src/diagram/hooks/useKeyboardShortcuts.ts
3183
+ var import_react12 = require("react");
3184
+ function useKeyboardShortcuts(options) {
3185
+ const {
3186
+ onDelete,
3187
+ onCopy,
3188
+ onPaste,
3189
+ onUndo,
3190
+ onRedo,
3191
+ onSelectAll,
3192
+ enabled = true
3193
+ } = options;
3194
+ (0, import_react12.useEffect)(() => {
3195
+ if (!enabled) return;
3196
+ const handleKeyDown = (event) => {
3197
+ const target = event.target;
3198
+ if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) {
3199
+ return;
3200
+ }
3201
+ const isCtrl = event.ctrlKey || event.metaKey;
3202
+ const isShift = event.shiftKey;
3203
+ if ((event.key === "Delete" || event.key === "Backspace") && !isCtrl) {
3204
+ event.preventDefault();
3205
+ onDelete?.();
3206
+ return;
3207
+ }
3208
+ if (isCtrl && event.key === "c") {
3209
+ event.preventDefault();
3210
+ onCopy?.();
3211
+ return;
3212
+ }
3213
+ if (isCtrl && event.key === "v") {
3214
+ event.preventDefault();
3215
+ onPaste?.();
3216
+ return;
3217
+ }
3218
+ if (isCtrl && event.key === "z" && !isShift) {
3219
+ event.preventDefault();
3220
+ onUndo?.();
3221
+ return;
3222
+ }
3223
+ if (isCtrl && (event.key === "y" || event.key === "z" && isShift)) {
3224
+ event.preventDefault();
3225
+ onRedo?.();
3226
+ return;
3227
+ }
3228
+ if (isCtrl && event.key === "a") {
3229
+ event.preventDefault();
3230
+ onSelectAll?.();
3231
+ return;
3232
+ }
3233
+ };
3234
+ window.addEventListener("keydown", handleKeyDown);
3235
+ return () => window.removeEventListener("keydown", handleKeyDown);
3236
+ }, [enabled, onDelete, onCopy, onPaste, onUndo, onRedo, onSelectAll]);
3237
+ }
3238
+
3239
+ // src/diagram/utils/portCalculations.ts
3240
+ function getPortWorldPosition(node, portId) {
3241
+ const symbol = getSymbolDefinition(node.symbolId);
3242
+ if (!symbol) return null;
3243
+ const port = symbol.ports.find((p) => p.id === portId);
3244
+ if (!port) return null;
3245
+ const { x: nodeX, y: nodeY, rotation = 0, scale = 1, flipX = false, flipY = false } = node.transform;
3246
+ let portX = port.x;
3247
+ let portY = port.y;
3248
+ const centerX = symbol.viewBox.width / 2;
3249
+ const centerY = symbol.viewBox.height / 2;
3250
+ if (flipX) {
3251
+ portX = symbol.viewBox.width - portX;
3252
+ }
3253
+ if (flipY) {
3254
+ portY = symbol.viewBox.height - portY;
3255
+ }
3256
+ portX = centerX + (portX - centerX) * scale;
3257
+ portY = centerY + (portY - centerY) * scale;
3258
+ if (rotation !== 0) {
3259
+ const rad = rotation * Math.PI / 180;
3260
+ const cos = Math.cos(rad);
3261
+ const sin = Math.sin(rad);
3262
+ const dx = portX - centerX;
3263
+ const dy = portY - centerY;
3264
+ portX = centerX + dx * cos - dy * sin;
3265
+ portY = centerY + dx * sin + dy * cos;
3266
+ }
3267
+ return {
3268
+ x: nodeX + portX,
3269
+ y: nodeY + portY
3270
+ };
3271
+ }
3272
+ function generateRoutePoints(fromNode, fromPortId, toNode, toPortId, options) {
3273
+ const startPoint = getPortWorldPosition(fromNode, fromPortId);
3274
+ const endPoint = getPortWorldPosition(toNode, toPortId);
3275
+ if (!startPoint || !endPoint) {
3276
+ return [];
3277
+ }
3278
+ const style = options?.style || "direct";
3279
+ if (style === "direct") {
3280
+ return [startPoint, endPoint];
3281
+ }
3282
+ if (style === "orthogonal" && options?.waypoints) {
3283
+ return [startPoint, ...options.waypoints, endPoint];
3284
+ }
3285
+ if (style === "orthogonal") {
3286
+ const midX = (startPoint.x + endPoint.x) / 2;
3287
+ return [
3288
+ startPoint,
3289
+ { x: midX, y: startPoint.y },
3290
+ { x: midX, y: endPoint.y },
3291
+ endPoint
3292
+ ];
3293
+ }
3294
+ return [startPoint, endPoint];
3295
+ }
3296
+ function nodeHasPort(node, portId) {
3297
+ const symbol = getSymbolDefinition(node.symbolId);
3298
+ if (!symbol) return false;
3299
+ return symbol.ports.some((p) => p.id === portId);
3300
+ }
3301
+
3302
+ // src/components/PIDCanvas/PIDCanvas.tsx
3303
+ var import_jsx_runtime16 = require("react/jsx-runtime");
3304
+ function PIDCanvas({
3305
+ diagram,
3306
+ className = "",
3307
+ style,
3308
+ features = {
3309
+ pan: true,
3310
+ zoom: true,
3311
+ selection: false,
3312
+ multiSelect: true,
3313
+ telemetry: false,
3314
+ dragNodes: false
3315
+ },
3316
+ onSelectionChange,
3317
+ onNodeClick,
3318
+ onPipeClick,
3319
+ onNodeMove,
3320
+ onDiagramChange,
3321
+ onDelete,
3322
+ onItemsCopied,
3323
+ onPaste,
3324
+ onSymbolDrop,
3325
+ onPipeCreated,
3326
+ telemetry,
3327
+ ...props
3328
+ }) {
3329
+ const [localDiagram, setLocalDiagram] = (0, import_react13.useState)(diagram);
3330
+ const [dragOverPosition, setDragOverPosition] = (0, import_react13.useState)(null);
3331
+ const [isConnecting, setIsConnecting] = (0, import_react13.useState)(false);
3332
+ const [connectionSource, setConnectionSource] = (0, import_react13.useState)(null);
3333
+ const [connectionSourcePort, setConnectionSourcePort] = (0, import_react13.useState)(null);
3334
+ const [connectionCursor, setConnectionCursor] = (0, import_react13.useState)(null);
3335
+ const [hoveredNode, setHoveredNode] = (0, import_react13.useState)(null);
3336
+ const [hoveredPort, setHoveredPort] = (0, import_react13.useState)(null);
3337
+ (0, import_react13.useEffect)(() => {
3338
+ setLocalDiagram(diagram);
3339
+ }, [diagram]);
3340
+ const {
3341
+ viewBox: controlledViewBox,
3342
+ svgRef,
3343
+ screenToWorld
3344
+ } = useViewBox({
3345
+ initialViewBox: diagram.viewBox,
3346
+ enablePan: features.pan ?? true,
3347
+ enableZoom: features.zoom ?? true
3348
+ });
3349
+ const { selection, selectNode, selectPipe, clearSelection } = useSelection({
3350
+ multiSelect: features.multiSelect ?? true,
3351
+ onSelectionChange
3352
+ });
3353
+ const dragEnabled = features.dragNodes ?? false;
3354
+ const selectionEnabled = features.selection ?? false;
3355
+ const handleDragEnd = (0, import_react13.useCallback)(
3356
+ (nodeId, offset, finalPosition) => {
3357
+ const dragDistance = Math.sqrt(offset.x ** 2 + offset.y ** 2);
3358
+ if (dragDistance < 2) {
3359
+ if (selectionEnabled) {
3360
+ selectNode(nodeId, false);
3361
+ }
3362
+ return;
3363
+ }
3364
+ const updatedNodes = localDiagram.nodes.map(
3365
+ (node) => node.id === nodeId ? {
3366
+ ...node,
3367
+ transform: {
3368
+ ...node.transform,
3369
+ x: finalPosition.x,
3370
+ y: finalPosition.y
3371
+ }
3372
+ } : node
3373
+ );
3374
+ const affectedPipes = localDiagram.pipes.filter(
3375
+ (pipe) => pipe.fromNodeId === nodeId || pipe.toNodeId === nodeId
3376
+ );
3377
+ const updatedPipes = localDiagram.pipes.map((pipe) => {
3378
+ if (!affectedPipes.find((p) => p.id === pipe.id)) return pipe;
3379
+ const sourceNode = updatedNodes.find((n) => n.id === pipe.fromNodeId);
3380
+ const targetNode = updatedNodes.find((n) => n.id === pipe.toNodeId);
3381
+ if (!sourceNode || !targetNode) return pipe;
3382
+ if (pipe.routePoints.length > 2) {
3383
+ return pipe;
3384
+ }
3385
+ const newRoutePoints = generateRoutePoints(
3386
+ sourceNode,
3387
+ pipe.fromPortId ?? "output",
3388
+ targetNode,
3389
+ pipe.toPortId ?? "input"
3390
+ );
3391
+ return {
3392
+ ...pipe,
3393
+ routePoints: newRoutePoints
3394
+ };
3395
+ });
3396
+ const updatedDiagram = {
3397
+ ...localDiagram,
3398
+ nodes: updatedNodes,
3399
+ pipes: updatedPipes
3400
+ };
3401
+ setLocalDiagram(updatedDiagram);
3402
+ onDiagramChange?.(updatedDiagram);
3403
+ onNodeMove?.(nodeId, finalPosition.x, finalPosition.y);
3404
+ },
3405
+ [localDiagram, onDiagramChange, onNodeMove, selectionEnabled, selectNode]
3406
+ );
3407
+ const { dragState, startDrag } = useDrag({
3408
+ snapToGrid: features.snapToGrid ?? 0,
3409
+ screenToWorld,
3410
+ onDragEnd: handleDragEnd
3411
+ });
3412
+ const { dragState: waypointDragState, startDrag: startWaypointDrag } = useDrag({
3413
+ snapToGrid: features.snapToGrid ?? 0,
3414
+ screenToWorld,
3415
+ onDragEnd: (pipeId, _offset, finalPosition) => {
3416
+ const [actualPipeId, pointIndex] = pipeId.split(":");
3417
+ const idx = parseInt(pointIndex, 10);
3418
+ const updatedPipes = localDiagram.pipes.map((pipe) => {
3419
+ if (pipe.id !== actualPipeId) return pipe;
3420
+ const updatedRoutePoints = pipe.routePoints.map(
3421
+ (point, i) => i === idx ? { x: finalPosition.x, y: finalPosition.y } : point
3422
+ );
3423
+ return { ...pipe, routePoints: updatedRoutePoints };
3424
+ });
3425
+ const updatedDiagram = { ...localDiagram, pipes: updatedPipes };
3426
+ setLocalDiagram(updatedDiagram);
3427
+ onDiagramChange?.(updatedDiagram);
3428
+ }
3429
+ });
3430
+ const handleNodeDragStart = (0, import_react13.useCallback)(
3431
+ (nodeId, event) => {
3432
+ const node = localDiagram.nodes.find((n) => n.id === nodeId);
3433
+ if (!node) return;
3434
+ startDrag(nodeId, event, { x: node.transform.x, y: node.transform.y });
3435
+ },
3436
+ [localDiagram.nodes, startDrag]
3437
+ );
3438
+ const handleWaypointDragStart = (0, import_react13.useCallback)(
3439
+ (pipeId, pointIndex, event) => {
3440
+ const pipe = localDiagram.pipes.find((p) => p.id === pipeId);
3441
+ if (!pipe || pointIndex >= pipe.routePoints.length) return;
3442
+ const point = pipe.routePoints[pointIndex];
3443
+ startWaypointDrag(`${pipeId}:${pointIndex}`, event, { x: point.x, y: point.y });
3444
+ },
3445
+ [localDiagram.pipes, startWaypointDrag]
3446
+ );
3447
+ const handlePipeSegmentClick = (0, import_react13.useCallback)(
3448
+ (pipeId, position, segmentIndex) => {
3449
+ const updatedPipes = localDiagram.pipes.map((pipe) => {
3450
+ if (pipe.id !== pipeId) return pipe;
3451
+ const updatedRoutePoints = [
3452
+ ...pipe.routePoints.slice(0, segmentIndex + 1),
3453
+ position,
3454
+ ...pipe.routePoints.slice(segmentIndex + 1)
3455
+ ];
3456
+ return { ...pipe, routePoints: updatedRoutePoints };
3457
+ });
3458
+ const updatedDiagram = { ...localDiagram, pipes: updatedPipes };
3459
+ setLocalDiagram(updatedDiagram);
3460
+ onDiagramChange?.(updatedDiagram);
3461
+ },
3462
+ [localDiagram, onDiagramChange]
3463
+ );
3464
+ const telemetryEnabled = features.telemetry ?? false;
3465
+ const handleDelete = (0, import_react13.useCallback)(() => {
3466
+ if (!selectionEnabled) return;
3467
+ const { selectedNodeIds, selectedPipeIds } = selection;
3468
+ if (selectedNodeIds.size === 0 && selectedPipeIds.size === 0) return;
3469
+ const updatedNodes = localDiagram.nodes.filter((node) => !selectedNodeIds.has(node.id));
3470
+ const updatedPipes = localDiagram.pipes.filter(
3471
+ (pipe) => !selectedPipeIds.has(pipe.id) && !selectedNodeIds.has(pipe.fromNodeId) && !selectedNodeIds.has(pipe.toNodeId)
3472
+ );
3473
+ const updatedDiagram = {
3474
+ ...localDiagram,
3475
+ nodes: updatedNodes,
3476
+ pipes: updatedPipes
3477
+ };
3478
+ setLocalDiagram(updatedDiagram);
3479
+ clearSelection();
3480
+ onDiagramChange?.(updatedDiagram);
3481
+ onDelete?.(Array.from(selectedNodeIds), Array.from(selectedPipeIds));
3482
+ }, [selection, localDiagram, selectionEnabled, clearSelection, onDiagramChange, onDelete]);
3483
+ const handleCopy = (0, import_react13.useCallback)(() => {
3484
+ if (!selectionEnabled) return;
3485
+ const { selectedNodeIds, selectedPipeIds } = selection;
3486
+ if (selectedNodeIds.size === 0 && selectedPipeIds.size === 0) return;
3487
+ onItemsCopied?.(Array.from(selectedNodeIds), Array.from(selectedPipeIds));
3488
+ }, [selection, selectionEnabled, onItemsCopied]);
3489
+ const handlePaste = (0, import_react13.useCallback)(() => {
3490
+ if (!selectionEnabled) return;
3491
+ onPaste?.();
3492
+ }, [selectionEnabled, onPaste]);
3493
+ const handleNodeDoubleClick = (nodeId, event) => {
3494
+ if (!selectionEnabled) return;
3495
+ console.log("Node double-clicked:", nodeId);
3496
+ selectNode(nodeId, event.ctrlKey || event.metaKey);
3497
+ };
3498
+ useKeyboardShortcuts({
3499
+ enabled: features.keyboardShortcuts ?? false,
3500
+ onDelete: handleDelete,
3501
+ onCopy: handleCopy,
3502
+ onPaste: handlePaste
3503
+ });
3504
+ const handleNodeClick = (nodeId, event) => {
3505
+ const connectionModeEnabled = features.connectionMode ?? false;
3506
+ if (connectionModeEnabled && isConnecting) {
3507
+ if (connectionSource && connectionSource !== nodeId) {
3508
+ onPipeCreated?.(
3509
+ connectionSource,
3510
+ nodeId,
3511
+ connectionSourcePort || void 0,
3512
+ hoveredPort || void 0
3513
+ );
3514
+ setIsConnecting(false);
3515
+ setConnectionSource(null);
3516
+ setConnectionSourcePort(null);
3517
+ setConnectionCursor(null);
3518
+ setHoveredPort(null);
3519
+ }
3520
+ return;
3521
+ }
3522
+ if (connectionModeEnabled && !isConnecting) {
3523
+ setIsConnecting(true);
3524
+ setConnectionSource(nodeId);
3525
+ setConnectionSourcePort(hoveredPort);
3526
+ return;
3527
+ }
3528
+ if (selectionEnabled) {
3529
+ selectNode(nodeId, event.ctrlKey || event.metaKey);
3530
+ }
3531
+ onNodeClick?.(nodeId);
3532
+ };
3533
+ const handlePipeClick = (pipeId, event) => {
3534
+ if (selectionEnabled) {
3535
+ selectPipe(pipeId, event.ctrlKey || event.metaKey);
3536
+ }
3537
+ onPipeClick?.(pipeId);
3538
+ };
3539
+ const handleCanvasClick = () => {
3540
+ if (selectionEnabled) {
3541
+ clearSelection();
3542
+ }
3543
+ };
3544
+ const handleDragOver = (0, import_react13.useCallback)(
3545
+ (event) => {
3546
+ if (!features.allowSymbolDrop) return;
3547
+ event.preventDefault();
3548
+ event.dataTransfer.dropEffect = "copy";
3549
+ if (!svgRef.current) return;
3550
+ const worldPos = screenToWorld(event.clientX, event.clientY);
3551
+ setDragOverPosition(worldPos);
3552
+ },
3553
+ [features.allowSymbolDrop, screenToWorld]
3554
+ );
3555
+ const handleDrop = (0, import_react13.useCallback)(
3556
+ (event) => {
3557
+ if (!features.allowSymbolDrop) return;
3558
+ event.preventDefault();
3559
+ setDragOverPosition(null);
3560
+ const symbolId = event.dataTransfer.getData("application/symbol-id");
3561
+ if (!symbolId) return;
3562
+ if (!svgRef.current) return;
3563
+ const worldPos = screenToWorld(event.clientX, event.clientY);
3564
+ onSymbolDrop?.(symbolId, worldPos);
3565
+ },
3566
+ [features.allowSymbolDrop, screenToWorld, onSymbolDrop]
3567
+ );
3568
+ const handleDragLeave = (0, import_react13.useCallback)(() => {
3569
+ setDragOverPosition(null);
3570
+ }, []);
3571
+ const handleMouseMove = (0, import_react13.useCallback)(
3572
+ (event) => {
3573
+ const connectionModeEnabled = features.connectionMode ?? false;
3574
+ if (connectionModeEnabled) {
3575
+ const worldPos = screenToWorld(event.clientX, event.clientY);
3576
+ if (isConnecting && connectionSource) {
3577
+ setConnectionCursor(worldPos);
3578
+ }
3579
+ let foundPort = null;
3580
+ let foundNode = null;
3581
+ for (const node of localDiagram.nodes) {
3582
+ const symbol = getSymbolDefinition(node.symbolId);
3583
+ if (!symbol || !symbol.ports) continue;
3584
+ for (const port of symbol.ports) {
3585
+ const portPos = getPortWorldPosition(node, port.id);
3586
+ if (!portPos) continue;
3587
+ const distance = Math.sqrt(
3588
+ (worldPos.x - portPos.x) ** 2 + (worldPos.y - portPos.y) ** 2
3589
+ );
3590
+ if (distance < 10) {
3591
+ foundPort = port.id;
3592
+ foundNode = node.id;
3593
+ break;
3594
+ }
3595
+ }
3596
+ if (foundPort) break;
3597
+ }
3598
+ setHoveredPort(foundPort);
3599
+ setHoveredNode(foundNode);
3600
+ }
3601
+ },
3602
+ [isConnecting, connectionSource, screenToWorld, localDiagram.nodes, features.connectionMode]
3603
+ );
3604
+ const { name } = diagram;
3605
+ const viewBox = controlledViewBox;
3606
+ const displayDiagram = localDiagram;
3607
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3608
+ "div",
3609
+ {
3610
+ className: `pid-canvas ${className}`.trim(),
3611
+ style: {
3612
+ width: "100%",
3613
+ height: "100%",
3614
+ position: "relative",
3615
+ overflow: "hidden",
3616
+ backgroundColor: "#f5f5f5",
3617
+ ...style
3618
+ },
3619
+ ...props,
3620
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
3621
+ "svg",
3622
+ {
3623
+ ref: svgRef,
3624
+ width: "100%",
3625
+ height: "100%",
3626
+ viewBox: `${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}`,
3627
+ preserveAspectRatio: "xMidYMid meet",
3628
+ style: {
3629
+ display: "block"
3630
+ },
3631
+ "aria-label": `P&ID diagram: ${name}`,
3632
+ role: "img",
3633
+ onClick: handleCanvasClick,
3634
+ onMouseMove: handleMouseMove,
3635
+ onDragOver: handleDragOver,
3636
+ onDrop: handleDrop,
3637
+ onDragLeave: handleDragLeave,
3638
+ children: [
3639
+ features.showGrid !== false ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
3640
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("defs", { children: [
3641
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("pattern", { id: "grid-minor", width: "5", height: "5", patternUnits: "userSpaceOnUse", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("path", { d: "M 5 0 L 0 0 0 5", fill: "none", stroke: "#e5e7eb", strokeWidth: "0.5" }) }),
3642
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("pattern", { id: "grid-major", width: "25", height: "25", patternUnits: "userSpaceOnUse", children: [
3643
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("rect", { width: "25", height: "25", fill: "url(#grid-minor)" }),
3644
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("path", { d: "M 25 0 L 0 0 0 25", fill: "none", stroke: "#d1d5db", strokeWidth: "1" })
3645
+ ] })
3646
+ ] }),
3647
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3648
+ "rect",
3649
+ {
3650
+ x: Math.floor(viewBox.x / 25) * 25 - viewBox.width,
3651
+ y: Math.floor(viewBox.y / 25) * 25 - viewBox.height,
3652
+ width: Math.ceil(viewBox.width * 3 / 25) * 25,
3653
+ height: Math.ceil(viewBox.height * 3 / 25) * 25,
3654
+ fill: "url(#grid-major)"
3655
+ }
3656
+ )
3657
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3658
+ "rect",
3659
+ {
3660
+ x: Math.floor(viewBox.x / 25) * 25 - viewBox.width,
3661
+ y: Math.floor(viewBox.y / 25) * 25 - viewBox.height,
3662
+ width: Math.ceil(viewBox.width * 3 / 25) * 25,
3663
+ height: Math.ceil(viewBox.height * 3 / 25) * 25,
3664
+ fill: features.backgroundColor ?? "#ffffff"
3665
+ }
3666
+ ),
3667
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3668
+ PipeRenderer,
3669
+ {
3670
+ pipes: displayDiagram.pipes,
3671
+ selectedPipeIds: selectionEnabled ? selection.selectedPipeIds : void 0,
3672
+ onPipeClick: selectionEnabled ? handlePipeClick : void 0,
3673
+ enableWaypointEditing: false,
3674
+ onWaypointDragStart: void 0,
3675
+ editingPipeId: null,
3676
+ draggingWaypointIndex: null,
3677
+ waypointDragOffset: void 0,
3678
+ onPipeSegmentClick: void 0
3679
+ }
3680
+ ),
3681
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3682
+ NodeRenderer,
3683
+ {
3684
+ nodes: displayDiagram.nodes,
3685
+ selectedNodeIds: selectionEnabled ? selection.selectedNodeIds : void 0,
3686
+ onNodeClick: selectionEnabled || features.connectionMode ? handleNodeClick : void 0,
3687
+ onNodeDoubleClick: selectionEnabled ? handleNodeDoubleClick : void 0,
3688
+ enableDrag: dragEnabled && !isConnecting,
3689
+ onNodeDragStart: dragEnabled && !isConnecting ? handleNodeDragStart : void 0,
3690
+ draggingNodeId: dragState.draggedId,
3691
+ dragOffset: dragState.offset
3692
+ }
3693
+ ),
3694
+ (features.editPipeRoutes ?? false) && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3695
+ PipeRenderer,
3696
+ {
3697
+ pipes: displayDiagram.pipes,
3698
+ selectedPipeIds: selectionEnabled ? selection.selectedPipeIds : void 0,
3699
+ onPipeClick: void 0,
3700
+ enableWaypointEditing: true,
3701
+ onWaypointDragStart: handleWaypointDragStart,
3702
+ editingPipeId: waypointDragState.draggedId?.split(":")[0] || null,
3703
+ draggingWaypointIndex: waypointDragState.draggedId ? parseInt(waypointDragState.draggedId.split(":")[1], 10) : null,
3704
+ waypointDragOffset: waypointDragState.offset,
3705
+ onPipeSegmentClick: handlePipeSegmentClick
3706
+ }
3707
+ ),
3708
+ telemetryEnabled && telemetry && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("g", { id: "layer-telemetry", children: displayDiagram.nodes.map((node) => {
3709
+ const binding = telemetry.get(node.id);
3710
+ if (!binding) return null;
3711
+ const symbol = getSymbolDefinition(node.symbolId);
3712
+ if (!symbol) return null;
3713
+ const overlayY = node.transform.y - symbol.viewBox.height / 2 - 10;
3714
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3715
+ DataOverlay,
3716
+ {
3717
+ nodeId: node.id,
3718
+ x: node.transform.x,
3719
+ y: overlayY,
3720
+ telemetry: binding
3721
+ },
3722
+ `telemetry-${node.id}`
3723
+ );
3724
+ }) }),
3725
+ dragOverPosition && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3726
+ "circle",
3727
+ {
3728
+ cx: dragOverPosition.x,
3729
+ cy: dragOverPosition.y,
3730
+ r: 20,
3731
+ fill: "#3b82f6",
3732
+ opacity: 0.3,
3733
+ stroke: "#3b82f6",
3734
+ strokeWidth: 2,
3735
+ pointerEvents: "none"
3736
+ }
3737
+ ),
3738
+ isConnecting && connectionSource && connectionCursor && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("g", { id: "connection-preview", pointerEvents: "none", children: (() => {
3739
+ const sourceNode = displayDiagram.nodes.find((n) => n.id === connectionSource);
3740
+ if (!sourceNode) return null;
3741
+ const symbol = getSymbolDefinition(sourceNode.symbolId);
3742
+ let lineStartX = sourceNode.transform.x;
3743
+ let lineStartY = sourceNode.transform.y;
3744
+ if (connectionSourcePort && symbol?.ports) {
3745
+ const port = symbol.ports.find((p) => p.id === connectionSourcePort);
3746
+ if (port) {
3747
+ const portPos = getPortWorldPosition(sourceNode, connectionSourcePort);
3748
+ if (portPos) {
3749
+ lineStartX = portPos.x;
3750
+ lineStartY = portPos.y;
3751
+ }
3752
+ }
3753
+ }
3754
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
3755
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3756
+ "circle",
3757
+ {
3758
+ cx: sourceNode.transform.x,
3759
+ cy: sourceNode.transform.y,
3760
+ r: 30,
3761
+ fill: "none",
3762
+ stroke: "#10b981",
3763
+ strokeWidth: 3,
3764
+ opacity: 0.6
3765
+ }
3766
+ ),
3767
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3768
+ "line",
3769
+ {
3770
+ x1: lineStartX,
3771
+ y1: lineStartY,
3772
+ x2: connectionCursor.x,
3773
+ y2: connectionCursor.y,
3774
+ stroke: "#10b981",
3775
+ strokeWidth: 2,
3776
+ strokeDasharray: "5,5",
3777
+ opacity: 0.8
3778
+ }
3779
+ ),
3780
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3781
+ "circle",
3782
+ {
3783
+ cx: connectionCursor.x,
3784
+ cy: connectionCursor.y,
3785
+ r: 8,
3786
+ fill: "#10b981",
3787
+ opacity: 0.6
3788
+ }
3789
+ )
3790
+ ] });
3791
+ })() }),
3792
+ features.connectionMode && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("g", { id: "port-indicators", pointerEvents: "none", children: displayDiagram.nodes.map((node) => {
3793
+ const symbol = getSymbolDefinition(node.symbolId);
3794
+ if (!symbol?.ports) return null;
3795
+ return symbol.ports.map((port) => {
3796
+ const portPos = getPortWorldPosition(node, port.id);
3797
+ if (!portPos) return null;
3798
+ const isHovered = hoveredPort === port.id && hoveredNode === node.id;
3799
+ const isSourcePort = node.id === connectionSource && port.id === connectionSourcePort;
3800
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3801
+ "circle",
3802
+ {
3803
+ cx: portPos.x,
3804
+ cy: portPos.y,
3805
+ r: isHovered ? 8 : 5,
3806
+ fill: isSourcePort ? "#10b981" : isHovered ? "#3b82f6" : "#94a3b8",
3807
+ stroke: isSourcePort ? "#059669" : isHovered ? "#2563eb" : "#64748b",
3808
+ strokeWidth: isHovered || isSourcePort ? 2 : 1,
3809
+ opacity: isHovered || isSourcePort ? 1 : 0.6
3810
+ },
3811
+ `${node.id}-${port.id}`
3812
+ );
3813
+ });
3814
+ }) })
3815
+ ]
3816
+ }
3817
+ )
3818
+ }
3819
+ );
3820
+ }
3821
+
3822
+ // src/components/SymbolLibrary/SymbolLibrary.tsx
3823
+ var import_react14 = require("react");
3824
+ var import_jsx_runtime17 = require("react/jsx-runtime");
3825
+ function SymbolLibrary({
3826
+ className = "",
3827
+ onSymbolDragStart,
3828
+ showCategories = true,
3829
+ categories = ["Valves", "Pumps", "Tanks", "Instruments", "Displays"]
3830
+ }) {
3831
+ const [selectedCategory, setSelectedCategory] = (0, import_react14.useState)("all");
3832
+ const [searchQuery, setSearchQuery] = (0, import_react14.useState)("");
3833
+ const symbolIds = getAvailableSymbols();
3834
+ const allSymbols = symbolIds.map((id) => getSymbolDefinition(id)).filter((symbol) => symbol !== void 0);
3835
+ const filteredSymbols = allSymbols.filter((symbol) => {
3836
+ const matchesCategory = selectedCategory === "all" || symbol.category === selectedCategory;
3837
+ const matchesSearch = searchQuery === "" || symbol.name.toLowerCase().includes(searchQuery.toLowerCase()) || symbol.id.toLowerCase().includes(searchQuery.toLowerCase());
3838
+ return matchesCategory && matchesSearch;
3839
+ });
3840
+ const handleDragStart = (symbol, event) => {
3841
+ event.dataTransfer.setData("application/symbol-id", symbol.id);
3842
+ event.dataTransfer.effectAllowed = "copy";
3843
+ onSymbolDragStart?.(symbol.id, event);
3844
+ };
3845
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: `symbol-library ${className}`.trim(), style: styles.container, children: [
3846
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { style: styles.header, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("h3", { style: styles.title, children: "Symbol Library" }) }),
3847
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { style: styles.searchContainer, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
3848
+ "input",
3849
+ {
3850
+ type: "text",
3851
+ placeholder: "Search symbols...",
3852
+ value: searchQuery,
3853
+ onChange: (e) => setSearchQuery(e.target.value),
3854
+ style: styles.searchInput
3855
+ }
3856
+ ) }),
3857
+ showCategories && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { style: styles.categories, children: [
3858
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
3859
+ "button",
3860
+ {
3861
+ onClick: () => setSelectedCategory("all"),
3862
+ style: {
3863
+ ...styles.categoryButton,
3864
+ ...selectedCategory === "all" ? styles.categoryButtonActive : {}
3865
+ },
3866
+ children: "All"
3867
+ }
3868
+ ),
3869
+ categories.map((category) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
3870
+ "button",
3871
+ {
3872
+ onClick: () => setSelectedCategory(category),
3873
+ style: {
3874
+ ...styles.categoryButton,
3875
+ ...selectedCategory === category ? styles.categoryButtonActive : {}
3876
+ },
3877
+ children: category
3878
+ },
3879
+ category
3880
+ ))
3881
+ ] }),
3882
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { style: styles.symbolGrid, children: filteredSymbols.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { style: styles.emptyState, children: "No symbols found" }) : filteredSymbols.map((symbol) => /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
3883
+ "div",
3884
+ {
3885
+ draggable: true,
3886
+ onDragStart: (e) => handleDragStart(symbol, e),
3887
+ style: styles.symbolCard,
3888
+ title: symbol.metadata?.description || symbol.name,
3889
+ children: [
3890
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { style: styles.symbolPreview, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
3891
+ "svg",
3892
+ {
3893
+ viewBox: `${symbol.viewBox.x} ${symbol.viewBox.y} ${symbol.viewBox.width} ${symbol.viewBox.height}`,
3894
+ style: styles.symbolSvg,
3895
+ children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("g", { dangerouslySetInnerHTML: { __html: symbol.svgContent } })
3896
+ }
3897
+ ) }),
3898
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { style: styles.symbolName, children: symbol.name })
3899
+ ]
3900
+ },
3901
+ symbol.id
3902
+ )) })
3903
+ ] });
3904
+ }
3905
+ var styles = {
3906
+ container: {
3907
+ display: "flex",
3908
+ flexDirection: "column",
3909
+ height: "100%",
3910
+ backgroundColor: "#ffffff",
3911
+ border: "1px solid #e0e0e0",
3912
+ borderRadius: "8px",
3913
+ overflow: "hidden"
3914
+ },
3915
+ header: {
3916
+ padding: "16px",
3917
+ borderBottom: "1px solid #e0e0e0",
3918
+ backgroundColor: "#f5f5f5"
3919
+ },
3920
+ title: {
3921
+ margin: 0,
3922
+ fontSize: "16px",
3923
+ fontWeight: 600,
3924
+ color: "#263238"
3925
+ },
3926
+ searchContainer: {
3927
+ padding: "12px 16px",
3928
+ borderBottom: "1px solid #e0e0e0"
3929
+ },
3930
+ searchInput: {
3931
+ width: "100%",
3932
+ padding: "8px 12px",
3933
+ border: "1px solid #e0e0e0",
3934
+ borderRadius: "4px",
3935
+ fontSize: "14px",
3936
+ outline: "none"
3937
+ },
3938
+ categories: {
3939
+ display: "flex",
3940
+ gap: "8px",
3941
+ padding: "12px 16px",
3942
+ borderBottom: "1px solid #e0e0e0",
3943
+ flexWrap: "wrap"
3944
+ },
3945
+ categoryButton: {
3946
+ padding: "6px 12px",
3947
+ border: "1px solid #e0e0e0",
3948
+ borderRadius: "4px",
3949
+ backgroundColor: "#ffffff",
3950
+ fontSize: "13px",
3951
+ cursor: "pointer",
3952
+ transition: "all 0.2s"
3953
+ },
3954
+ categoryButtonActive: {
3955
+ backgroundColor: "#3b82f6",
3956
+ color: "#ffffff",
3957
+ borderColor: "#3b82f6"
3958
+ },
3959
+ symbolGrid: {
3960
+ flex: 1,
3961
+ padding: "16px",
3962
+ display: "grid",
3963
+ gridTemplateColumns: "repeat(auto-fill, minmax(100px, 1fr))",
3964
+ gap: "12px",
3965
+ overflowY: "auto",
3966
+ alignContent: "start"
3967
+ },
3968
+ emptyState: {
3969
+ gridColumn: "1 / -1",
3970
+ textAlign: "center",
3971
+ padding: "32px",
3972
+ color: "#999"
3973
+ },
3974
+ symbolCard: {
3975
+ display: "flex",
3976
+ flexDirection: "column",
3977
+ alignItems: "center",
3978
+ padding: "12px",
3979
+ border: "1px solid #e0e0e0",
3980
+ borderRadius: "6px",
3981
+ backgroundColor: "#ffffff",
3982
+ cursor: "grab",
3983
+ transition: "all 0.2s"
3984
+ },
3985
+ symbolPreview: {
3986
+ width: "60px",
3987
+ height: "60px",
3988
+ display: "flex",
3989
+ alignItems: "center",
3990
+ justifyContent: "center",
3991
+ marginBottom: "8px"
3992
+ },
3993
+ symbolSvg: {
3994
+ maxWidth: "100%",
3995
+ maxHeight: "100%"
3996
+ },
3997
+ symbolName: {
3998
+ fontSize: "12px",
3999
+ color: "#263238",
4000
+ textAlign: "center",
4001
+ wordBreak: "break-word",
4002
+ lineHeight: "1.3"
4003
+ }
4004
+ };
4005
+
4006
+ // src/components/NodeConfigPanel/NodeConfigPanel.tsx
4007
+ var import_jsx_runtime18 = require("react/jsx-runtime");
4008
+ function NodeConfigPanel({
4009
+ node,
4010
+ binding,
4011
+ onUpdateNode,
4012
+ onUpdateTelemetry,
4013
+ onAddTelemetry,
4014
+ onRemove,
4015
+ className = "",
4016
+ style = {}
4017
+ }) {
4018
+ const defaultStyle = {
4019
+ width: "320px",
4020
+ backgroundColor: "#f8fafc",
4021
+ borderLeft: "1px solid #e2e8f0",
4022
+ overflow: "auto",
4023
+ padding: "20px",
4024
+ ...style
4025
+ };
4026
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className, style: defaultStyle, children: [
4027
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("h2", { style: { margin: "0 0 16px 0", fontSize: "1rem", fontWeight: 600 }, children: [
4028
+ "Configure: ",
4029
+ node.id
4030
+ ] }),
4031
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: { marginBottom: "20px" }, children: [
4032
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("h3", { style: { fontSize: "0.875rem", fontWeight: 600, marginBottom: "8px" }, children: "Node Settings" }),
4033
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("label", { style: { fontSize: "0.75rem", display: "block", marginBottom: "4px" }, children: "Label:" }),
4034
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4035
+ "input",
4036
+ {
4037
+ type: "text",
4038
+ value: node.label || node.id,
4039
+ onChange: (e) => onUpdateNode({ label: e.target.value }),
4040
+ style: {
4041
+ width: "100%",
4042
+ padding: "6px 8px",
4043
+ borderRadius: "4px",
4044
+ border: "1px solid #cbd5e1",
4045
+ fontSize: "0.875rem",
4046
+ marginBottom: "8px"
4047
+ }
4048
+ }
4049
+ ),
4050
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: "8px" }, children: [
4051
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { children: [
4052
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("label", { style: { fontSize: "0.75rem", display: "block", marginBottom: "4px" }, children: "Label Offset X:" }),
4053
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4054
+ "input",
4055
+ {
4056
+ type: "number",
4057
+ value: node.labelOffset?.x ?? 0,
4058
+ onChange: (e) => onUpdateNode({
4059
+ labelOffset: {
4060
+ x: parseFloat(e.target.value) || 0,
4061
+ y: node.labelOffset?.y ?? 0
4062
+ }
4063
+ }),
4064
+ style: {
4065
+ width: "100%",
4066
+ padding: "6px 8px",
4067
+ borderRadius: "4px",
4068
+ border: "1px solid #cbd5e1",
4069
+ fontSize: "0.875rem"
4070
+ }
4071
+ }
4072
+ )
4073
+ ] }),
4074
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { children: [
4075
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("label", { style: { fontSize: "0.75rem", display: "block", marginBottom: "4px" }, children: "Label Offset Y:" }),
4076
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4077
+ "input",
4078
+ {
4079
+ type: "number",
4080
+ value: node.labelOffset?.y ?? 0,
4081
+ onChange: (e) => onUpdateNode({
4082
+ labelOffset: {
4083
+ x: node.labelOffset?.x ?? 0,
4084
+ y: parseFloat(e.target.value) || 0
4085
+ }
4086
+ }),
4087
+ style: {
4088
+ width: "100%",
4089
+ padding: "6px 8px",
4090
+ borderRadius: "4px",
4091
+ border: "1px solid #cbd5e1",
4092
+ fontSize: "0.875rem"
4093
+ }
4094
+ }
4095
+ )
4096
+ ] })
4097
+ ] }),
4098
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { children: [
4099
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("label", { style: { fontSize: "0.75rem", display: "block", marginBottom: "4px" }, children: "Label Font Size:" }),
4100
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4101
+ "input",
4102
+ {
4103
+ type: "number",
4104
+ min: "6",
4105
+ max: "24",
4106
+ value: node.labelFontSize ?? 10,
4107
+ onChange: (e) => onUpdateNode({
4108
+ labelFontSize: parseFloat(e.target.value) || 10
4109
+ }),
4110
+ style: {
4111
+ width: "100%",
4112
+ padding: "6px 8px",
4113
+ borderRadius: "4px",
4114
+ border: "1px solid #cbd5e1",
4115
+ fontSize: "0.875rem"
4116
+ }
4117
+ }
4118
+ )
4119
+ ] }),
4120
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { children: [
4121
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("label", { style: { fontSize: "0.75rem", display: "block", marginBottom: "4px" }, children: "Rotation (degrees):" }),
4122
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4123
+ "input",
4124
+ {
4125
+ type: "number",
4126
+ min: "0",
4127
+ max: "360",
4128
+ step: "15",
4129
+ value: node.transform.rotation ?? 0,
4130
+ onChange: (e) => onUpdateNode({
4131
+ transform: {
4132
+ ...node.transform,
4133
+ rotation: parseFloat(e.target.value) || 0
4134
+ }
4135
+ }),
4136
+ style: {
4137
+ width: "100%",
4138
+ padding: "6px 8px",
4139
+ borderRadius: "4px",
4140
+ border: "1px solid #cbd5e1",
4141
+ fontSize: "0.875rem"
4142
+ }
4143
+ }
4144
+ )
4145
+ ] })
4146
+ ] }),
4147
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: { marginBottom: "20px" }, children: [
4148
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("h3", { style: { fontSize: "0.875rem", fontWeight: 600, marginBottom: "8px" }, children: "Telemetry Data" }),
4149
+ !binding ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4150
+ "button",
4151
+ {
4152
+ onClick: () => {
4153
+ if (onAddTelemetry) {
4154
+ onAddTelemetry();
4155
+ } else {
4156
+ onUpdateTelemetry({
4157
+ nodeId: node.id,
4158
+ tag: node.id,
4159
+ value: {
4160
+ value: 50,
4161
+ status: "normal",
4162
+ unit: "PSI",
4163
+ timestamp: Date.now()
4164
+ },
4165
+ history: [45, 47, 48, 50, 52, 51, 49, 50],
4166
+ display: {
4167
+ showValue: true,
4168
+ showSparkline: false,
4169
+ precision: 1,
4170
+ backgroundColor: "status"
4171
+ },
4172
+ thresholds: {
4173
+ highHigh: 85,
4174
+ high: 70,
4175
+ low: 30,
4176
+ lowLow: 15
4177
+ }
4178
+ });
4179
+ }
4180
+ },
4181
+ style: {
4182
+ padding: "8px 16px",
4183
+ backgroundColor: "#3b82f6",
4184
+ color: "#fff",
4185
+ border: "none",
4186
+ borderRadius: "4px",
4187
+ cursor: "pointer",
4188
+ width: "100%"
4189
+ },
4190
+ children: "+ Add Telemetry"
4191
+ }
4192
+ ) : /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_jsx_runtime18.Fragment, { children: [
4193
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("label", { style: { fontSize: "0.75rem", display: "block", marginBottom: "4px" }, children: "Initial Value:" }),
4194
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4195
+ "input",
4196
+ {
4197
+ type: "number",
4198
+ value: binding.value.value,
4199
+ onChange: (e) => onUpdateTelemetry({
4200
+ nodeId: node.id,
4201
+ value: {
4202
+ ...binding.value,
4203
+ value: parseFloat(e.target.value) || 0
4204
+ }
4205
+ }),
4206
+ style: {
4207
+ width: "100%",
4208
+ padding: "6px 8px",
4209
+ borderRadius: "4px",
4210
+ border: "1px solid #cbd5e1",
4211
+ fontSize: "0.875rem",
4212
+ marginBottom: "8px"
4213
+ }
4214
+ }
4215
+ ),
4216
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("label", { style: { fontSize: "0.75rem", display: "block", marginBottom: "4px" }, children: "Unit:" }),
4217
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4218
+ "input",
4219
+ {
4220
+ type: "text",
4221
+ value: binding.value.unit || "",
4222
+ onChange: (e) => onUpdateTelemetry({
4223
+ nodeId: node.id,
4224
+ value: {
4225
+ ...binding.value,
4226
+ unit: e.target.value
4227
+ }
4228
+ }),
4229
+ style: {
4230
+ width: "100%",
4231
+ padding: "6px 8px",
4232
+ borderRadius: "4px",
4233
+ border: "1px solid #cbd5e1",
4234
+ fontSize: "0.875rem",
4235
+ marginBottom: "8px"
4236
+ }
4237
+ }
4238
+ ),
4239
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("label", { style: { fontSize: "0.75rem", display: "block", marginBottom: "4px" }, children: [
4240
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4241
+ "input",
4242
+ {
4243
+ type: "checkbox",
4244
+ checked: binding.display?.showSparkline ?? false,
4245
+ onChange: (e) => onUpdateTelemetry({
4246
+ nodeId: node.id,
4247
+ tag: binding.tag,
4248
+ value: binding.value,
4249
+ history: binding.history,
4250
+ thresholds: binding.thresholds,
4251
+ statusColors: binding.statusColors,
4252
+ display: {
4253
+ ...binding.display,
4254
+ showSparkline: e.target.checked
4255
+ }
4256
+ }),
4257
+ style: { marginRight: "6px" }
4258
+ }
4259
+ ),
4260
+ "Show Sparkline"
4261
+ ] }),
4262
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: { marginTop: "12px" }, children: [
4263
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4264
+ "label",
4265
+ {
4266
+ style: {
4267
+ fontSize: "0.75rem",
4268
+ fontWeight: 600,
4269
+ display: "block",
4270
+ marginBottom: "8px"
4271
+ },
4272
+ children: "Telemetry Position Offset:"
4273
+ }
4274
+ ),
4275
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: "8px" }, children: [
4276
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { children: [
4277
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("label", { style: { fontSize: "0.7rem", display: "block", marginBottom: "4px" }, children: "X Offset:" }),
4278
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4279
+ "input",
4280
+ {
4281
+ type: "number",
4282
+ value: binding.display?.offsetX ?? 0,
4283
+ onChange: (e) => onUpdateTelemetry({
4284
+ nodeId: node.id,
4285
+ tag: binding.tag,
4286
+ value: binding.value,
4287
+ history: binding.history,
4288
+ thresholds: binding.thresholds,
4289
+ statusColors: binding.statusColors,
4290
+ display: {
4291
+ ...binding.display,
4292
+ offsetX: parseFloat(e.target.value) || 0
4293
+ }
4294
+ }),
4295
+ style: {
4296
+ width: "100%",
4297
+ padding: "4px 6px",
4298
+ borderRadius: "4px",
4299
+ border: "1px solid #cbd5e1",
4300
+ fontSize: "0.75rem"
4301
+ }
4302
+ }
4303
+ )
4304
+ ] }),
4305
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { children: [
4306
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("label", { style: { fontSize: "0.7rem", display: "block", marginBottom: "4px" }, children: "Y Offset:" }),
4307
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4308
+ "input",
4309
+ {
4310
+ type: "number",
4311
+ value: binding.display?.offsetY ?? 0,
4312
+ onChange: (e) => onUpdateTelemetry({
4313
+ nodeId: node.id,
4314
+ tag: binding.tag,
4315
+ value: binding.value,
4316
+ history: binding.history,
4317
+ thresholds: binding.thresholds,
4318
+ statusColors: binding.statusColors,
4319
+ display: {
4320
+ ...binding.display,
4321
+ offsetY: parseFloat(e.target.value) || 0
4322
+ }
4323
+ }),
4324
+ style: {
4325
+ width: "100%",
4326
+ padding: "4px 6px",
4327
+ borderRadius: "4px",
4328
+ border: "1px solid #cbd5e1",
4329
+ fontSize: "0.75rem"
4330
+ }
4331
+ }
4332
+ )
4333
+ ] })
4334
+ ] }),
4335
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: { marginTop: "16px" }, children: [
4336
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4337
+ "label",
4338
+ {
4339
+ style: {
4340
+ fontSize: "0.75rem",
4341
+ fontWeight: 600,
4342
+ display: "block",
4343
+ marginBottom: "8px"
4344
+ },
4345
+ children: "Display Colors:"
4346
+ }
4347
+ ),
4348
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: { marginBottom: "8px" }, children: [
4349
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("label", { style: { fontSize: "0.7rem", display: "block", marginBottom: "4px" }, children: "Text Color:" }),
4350
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: { display: "flex", gap: "8px", alignItems: "center" }, children: [
4351
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4352
+ "input",
4353
+ {
4354
+ type: "color",
4355
+ value: binding.display?.textColor ?? "#000000",
4356
+ onChange: (e) => onUpdateTelemetry({
4357
+ nodeId: node.id,
4358
+ tag: binding.tag,
4359
+ value: binding.value,
4360
+ history: binding.history,
4361
+ thresholds: binding.thresholds,
4362
+ statusColors: binding.statusColors,
4363
+ display: {
4364
+ ...binding.display,
4365
+ textColor: e.target.value
4366
+ }
4367
+ }),
4368
+ style: {
4369
+ width: "40px",
4370
+ height: "28px",
4371
+ border: "1px solid #cbd5e1",
4372
+ borderRadius: "4px",
4373
+ cursor: "pointer"
4374
+ }
4375
+ }
4376
+ ),
4377
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4378
+ "input",
4379
+ {
4380
+ type: "text",
4381
+ value: binding.display?.textColor ?? "#000000",
4382
+ onChange: (e) => onUpdateTelemetry({
4383
+ nodeId: node.id,
4384
+ tag: binding.tag,
4385
+ value: binding.value,
4386
+ history: binding.history,
4387
+ thresholds: binding.thresholds,
4388
+ statusColors: binding.statusColors,
4389
+ display: {
4390
+ ...binding.display,
4391
+ textColor: e.target.value
4392
+ }
4393
+ }),
4394
+ placeholder: "#000000",
4395
+ style: {
4396
+ flex: 1,
4397
+ padding: "4px 6px",
4398
+ borderRadius: "4px",
4399
+ border: "1px solid #cbd5e1",
4400
+ fontSize: "0.7rem",
4401
+ fontFamily: "monospace"
4402
+ }
4403
+ }
4404
+ )
4405
+ ] })
4406
+ ] }),
4407
+ binding.display?.showSparkline && /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: { marginBottom: "8px" }, children: [
4408
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("label", { style: { fontSize: "0.7rem", display: "block", marginBottom: "4px" }, children: "Sparkline Color:" }),
4409
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: { display: "flex", gap: "8px", alignItems: "center" }, children: [
4410
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4411
+ "input",
4412
+ {
4413
+ type: "color",
4414
+ value: binding.display?.sparklineColor ?? "#3b82f6",
4415
+ onChange: (e) => onUpdateTelemetry({
4416
+ nodeId: node.id,
4417
+ tag: binding.tag,
4418
+ value: binding.value,
4419
+ history: binding.history,
4420
+ thresholds: binding.thresholds,
4421
+ statusColors: binding.statusColors,
4422
+ display: {
4423
+ ...binding.display,
4424
+ sparklineColor: e.target.value
4425
+ }
4426
+ }),
4427
+ style: {
4428
+ width: "40px",
4429
+ height: "28px",
4430
+ border: "1px solid #cbd5e1",
4431
+ borderRadius: "4px",
4432
+ cursor: "pointer"
4433
+ }
4434
+ }
4435
+ ),
4436
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4437
+ "input",
4438
+ {
4439
+ type: "text",
4440
+ value: binding.display?.sparklineColor ?? "#3b82f6",
4441
+ onChange: (e) => onUpdateTelemetry({
4442
+ nodeId: node.id,
4443
+ tag: binding.tag,
4444
+ value: binding.value,
4445
+ history: binding.history,
4446
+ thresholds: binding.thresholds,
4447
+ statusColors: binding.statusColors,
4448
+ display: {
4449
+ ...binding.display,
4450
+ sparklineColor: e.target.value
4451
+ }
4452
+ }),
4453
+ placeholder: "#3b82f6",
4454
+ style: {
4455
+ flex: 1,
4456
+ padding: "4px 6px",
4457
+ borderRadius: "4px",
4458
+ border: "1px solid #cbd5e1",
4459
+ fontSize: "0.7rem",
4460
+ fontFamily: "monospace"
4461
+ }
4462
+ }
4463
+ )
4464
+ ] })
4465
+ ] }),
4466
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: { marginBottom: "8px" }, children: [
4467
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("label", { style: { fontSize: "0.7rem", display: "block", marginBottom: "4px" }, children: "Background:" }),
4468
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4469
+ "input",
4470
+ {
4471
+ type: "text",
4472
+ value: binding.display?.backgroundColor ?? "status",
4473
+ onChange: (e) => onUpdateTelemetry({
4474
+ nodeId: node.id,
4475
+ tag: binding.tag,
4476
+ value: binding.value,
4477
+ history: binding.history,
4478
+ thresholds: binding.thresholds,
4479
+ statusColors: binding.statusColors,
4480
+ display: {
4481
+ ...binding.display,
4482
+ backgroundColor: e.target.value
4483
+ }
4484
+ }),
4485
+ placeholder: "status or #ffffff",
4486
+ style: {
4487
+ width: "100%",
4488
+ padding: "4px 6px",
4489
+ borderRadius: "4px",
4490
+ border: "1px solid #cbd5e1",
4491
+ fontSize: "0.7rem",
4492
+ fontFamily: "monospace"
4493
+ }
4494
+ }
4495
+ ),
4496
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { style: { fontSize: "0.65rem", color: "#6b7280", marginTop: "2px" }, children: "Use 'status' for status-based color or hex code" })
4497
+ ] })
4498
+ ] }),
4499
+ " "
4500
+ ] })
4501
+ ] })
4502
+ ] }),
4503
+ binding && /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: { marginBottom: "20px" }, children: [
4504
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("h3", { style: { fontSize: "0.875rem", fontWeight: 600, marginBottom: "8px" }, children: "Thresholds" }),
4505
+ ["highHigh", "high", "low", "lowLow"].map((key) => {
4506
+ const label = key === "highHigh" ? "High-High (\u2265)" : key === "high" ? "High (\u2265)" : key === "low" ? "Low (\u2264)" : "Low-Low (\u2264)";
4507
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: { marginBottom: "8px" }, children: [
4508
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("label", { style: { fontSize: "0.75rem", display: "block", marginBottom: "4px" }, children: label }),
4509
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4510
+ "input",
4511
+ {
4512
+ type: "number",
4513
+ value: binding.thresholds?.[key] ?? (key === "highHigh" ? 85 : key === "high" ? 70 : key === "low" ? 30 : 15),
4514
+ onChange: (e) => onUpdateTelemetry({
4515
+ nodeId: node.id,
4516
+ thresholds: {
4517
+ ...binding.thresholds,
4518
+ [key]: parseFloat(e.target.value) || 0
4519
+ }
4520
+ }),
4521
+ style: {
4522
+ width: "100%",
4523
+ padding: "6px 8px",
4524
+ borderRadius: "4px",
4525
+ border: "1px solid #cbd5e1",
4526
+ fontSize: "0.875rem"
4527
+ }
4528
+ }
4529
+ )
4530
+ ] }, key);
4531
+ })
4532
+ ] }),
4533
+ binding && /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: { marginBottom: "20px" }, children: [
4534
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("h3", { style: { fontSize: "0.875rem", fontWeight: 600, marginBottom: "8px" }, children: "Status Colors" }),
4535
+ ["normal", "warning", "alarm", "fault", "off"].map((status) => {
4536
+ const defaultColors = {
4537
+ normal: "#10b981",
4538
+ warning: "#f59e0b",
4539
+ alarm: "#ef4444",
4540
+ fault: "#8b5cf6",
4541
+ off: "#6b7280"
4542
+ };
4543
+ const label = status === "normal" ? "Normal" : status === "warning" ? "Warning" : status === "alarm" ? "Alarm" : status === "fault" ? "Fault" : "Off";
4544
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: { marginBottom: "8px" }, children: [
4545
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("label", { style: { fontSize: "0.75rem", display: "block", marginBottom: "4px" }, children: [
4546
+ label,
4547
+ ":"
4548
+ ] }),
4549
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: { display: "flex", gap: "8px", alignItems: "center" }, children: [
4550
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4551
+ "input",
4552
+ {
4553
+ type: "color",
4554
+ value: binding.statusColors?.[status] ?? defaultColors[status],
4555
+ onChange: (e) => onUpdateTelemetry({
4556
+ nodeId: node.id,
4557
+ statusColors: {
4558
+ ...binding.statusColors,
4559
+ [status]: e.target.value
4560
+ }
4561
+ }),
4562
+ style: {
4563
+ width: "50px",
4564
+ height: "32px",
4565
+ border: "1px solid #cbd5e1",
4566
+ borderRadius: "4px",
4567
+ cursor: "pointer"
4568
+ }
4569
+ }
4570
+ ),
4571
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4572
+ "input",
4573
+ {
4574
+ type: "text",
4575
+ value: binding.statusColors?.[status] ?? defaultColors[status],
4576
+ onChange: (e) => onUpdateTelemetry({
4577
+ nodeId: node.id,
4578
+ statusColors: {
4579
+ ...binding.statusColors,
4580
+ [status]: e.target.value
4581
+ }
4582
+ }),
4583
+ placeholder: defaultColors[status],
4584
+ style: {
4585
+ flex: 1,
4586
+ padding: "6px 8px",
4587
+ borderRadius: "4px",
4588
+ border: "1px solid #cbd5e1",
4589
+ fontSize: "0.75rem",
4590
+ fontFamily: "monospace"
4591
+ }
4592
+ }
4593
+ )
4594
+ ] })
4595
+ ] }, status);
4596
+ })
4597
+ ] }),
4598
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { style: { marginTop: "20px", paddingTop: "20px", borderTop: "1px solid #cbd5e1" }, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4599
+ "button",
4600
+ {
4601
+ onClick: onRemove,
4602
+ style: {
4603
+ padding: "8px 16px",
4604
+ backgroundColor: "#ef4444",
4605
+ color: "#fff",
4606
+ border: "none",
4607
+ borderRadius: "4px",
4608
+ cursor: "pointer",
4609
+ width: "100%"
4610
+ },
4611
+ children: "Remove Node"
4612
+ }
4613
+ ) })
4614
+ ] });
4615
+ }
4616
+
4617
+ // src/diagram/utils/serialization.ts
4618
+ function validateDiagram(diagram) {
4619
+ const errors = [];
4620
+ if (!diagram || typeof diagram !== "object") {
4621
+ errors.push("Diagram must be an object");
4622
+ return { valid: false, errors };
4623
+ }
4624
+ const d = diagram;
4625
+ if (!d.id || typeof d.id !== "string") {
4626
+ errors.push("Diagram must have a valid id (string)");
4627
+ }
4628
+ if (!d.name || typeof d.name !== "string") {
4629
+ errors.push("Diagram must have a valid name (string)");
4630
+ }
4631
+ if (!d.version || typeof d.version !== "string") {
4632
+ errors.push("Diagram must have a valid version (string)");
4633
+ }
4634
+ if (!d.viewBox) {
4635
+ errors.push("Diagram must have a viewBox");
4636
+ } else if (typeof d.viewBox.x !== "number" || typeof d.viewBox.y !== "number" || typeof d.viewBox.width !== "number" || typeof d.viewBox.height !== "number") {
4637
+ errors.push("ViewBox must have numeric x, y, width, height properties");
4638
+ }
4639
+ if (!Array.isArray(d.nodes)) {
4640
+ errors.push("Diagram.nodes must be an array");
4641
+ }
4642
+ if (!Array.isArray(d.pipes)) {
4643
+ errors.push("Diagram.pipes must be an array");
4644
+ }
4645
+ if (!Array.isArray(d.overlays)) {
4646
+ errors.push("Diagram.overlays must be an array");
4647
+ }
4648
+ return {
4649
+ valid: errors.length === 0,
4650
+ errors
4651
+ };
4652
+ }
4653
+ function exportDiagram(diagram) {
4654
+ return JSON.stringify(diagram, null, 2);
4655
+ }
4656
+ function importDiagram(json) {
4657
+ try {
4658
+ const parsed = JSON.parse(json);
4659
+ const validation = validateDiagram(parsed);
4660
+ if (!validation.valid) {
4661
+ throw new Error(`Invalid diagram: ${validation.errors.join(", ")}`);
4662
+ }
4663
+ return parsed;
4664
+ } catch (error) {
4665
+ if (error instanceof SyntaxError) {
4666
+ throw new Error(`Invalid JSON: ${error.message}`);
4667
+ }
4668
+ throw error;
4669
+ }
4670
+ }
4671
+
4672
+ // src/diagram/examples/exampleDiagram.ts
4673
+ var nodes = [
4674
+ {
4675
+ id: "V-101",
4676
+ symbolId: "valve-ball",
4677
+ transform: {
4678
+ x: 100,
4679
+ y: 150,
4680
+ rotation: 0
4681
+ },
4682
+ ports: [
4683
+ { id: "inlet", x: -25, y: 0, type: "inlet" },
4684
+ { id: "outlet", x: 25, y: 0, type: "outlet" }
4685
+ ]
4686
+ },
4687
+ {
4688
+ id: "P-101",
4689
+ symbolId: "pump-centrifugal",
4690
+ transform: {
4691
+ x: 250,
4692
+ y: 150,
4693
+ rotation: 0
4694
+ },
4695
+ ports: [
4696
+ { id: "inlet", x: -40, y: 0, type: "inlet" },
4697
+ { id: "outlet", x: 40, y: 0, type: "outlet" }
4698
+ ]
4699
+ },
4700
+ {
4701
+ id: "T-101",
4702
+ symbolId: "vessel-tank",
4703
+ transform: {
4704
+ x: 450,
4705
+ y: 120,
4706
+ rotation: 0
4707
+ },
4708
+ ports: [
4709
+ { id: "inlet", x: 0, y: -40, type: "inlet" },
4710
+ { id: "outlet", x: 0, y: 40, type: "outlet" }
4711
+ ]
4712
+ }
4713
+ ];
4714
+ var exampleDiagram = {
4715
+ id: "example-001",
4716
+ name: "Simple Process Flow",
4717
+ version: "1.0.0",
4718
+ viewBox: {
4719
+ x: -100,
4720
+ y: -50,
4721
+ width: 1200,
4722
+ height: 600
4723
+ },
4724
+ nodes,
4725
+ pipes: [
4726
+ {
4727
+ id: "pipe-001",
4728
+ fromNodeId: "V-101",
4729
+ fromPortId: "outlet",
4730
+ toNodeId: "P-101",
4731
+ toPortId: "inlet",
4732
+ routePoints: generateRoutePoints(nodes[0], "outlet", nodes[1], "inlet"),
4733
+ style: {
4734
+ stroke: "#0277bd",
4735
+ strokeWidth: 4
4736
+ }
4737
+ },
4738
+ {
4739
+ id: "pipe-002",
4740
+ fromNodeId: "P-101",
4741
+ fromPortId: "outlet",
4742
+ toNodeId: "T-101",
4743
+ toPortId: "inlet",
4744
+ routePoints: generateRoutePoints(nodes[1], "outlet", nodes[2], "inlet", {
4745
+ style: "orthogonal"
4746
+ }),
4747
+ style: {
4748
+ stroke: "#2e7d32",
4749
+ strokeWidth: 4
4750
+ }
4751
+ }
4752
+ ],
4753
+ overlays: [
4754
+ {
4755
+ id: "overlay-v101",
4756
+ type: "value",
4757
+ anchorType: "node",
4758
+ anchorId: "V-101",
4759
+ offset: { x: 0, y: -40 },
4760
+ binding: {
4761
+ sourceType: "tag",
4762
+ source: "V101.position",
4763
+ format: "0.0%"
4764
+ },
4765
+ content: {
4766
+ text: "Valve Position"
4767
+ }
4768
+ },
4769
+ {
4770
+ id: "overlay-p101",
4771
+ type: "value",
4772
+ anchorType: "node",
4773
+ anchorId: "P-101",
4774
+ offset: { x: 0, y: -50 },
4775
+ binding: {
4776
+ sourceType: "tag",
4777
+ source: "P101.flow",
4778
+ format: "0.0 LPM"
4779
+ },
4780
+ content: {
4781
+ text: "Flow Rate"
4782
+ }
4783
+ },
4784
+ {
4785
+ id: "overlay-t101",
4786
+ type: "gauge",
4787
+ anchorType: "node",
4788
+ anchorId: "T-101",
4789
+ offset: { x: 60, y: 0 },
4790
+ binding: {
4791
+ sourceType: "tag",
4792
+ source: "T101.level"
4793
+ },
4794
+ content: {
4795
+ component: "CircularGauge",
4796
+ props: {
4797
+ max: 100,
4798
+ unit: "%",
4799
+ label: "Level"
4800
+ }
4801
+ }
4802
+ }
4803
+ ],
4804
+ metadata: {
4805
+ author: "System",
4806
+ created: "2025-12-31T00:00:00Z",
4807
+ description: "Example P&ID diagram demonstrating schema structure",
4808
+ tags: ["example", "tutorial"]
4809
+ }
4810
+ };
4811
+
4812
+ // src/utils/thresholds.ts
4813
+ function calculateThresholdStatus(value, thresholds) {
4814
+ if (!thresholds) return "normal";
4815
+ const { highHigh, high, low, lowLow } = thresholds;
4816
+ if (highHigh !== void 0 && value >= highHigh) {
4817
+ return "alarm";
4818
+ }
4819
+ if (lowLow !== void 0 && value <= lowLow) {
4820
+ return "fault";
4821
+ }
4822
+ if (high !== void 0 && value >= high) {
4823
+ return "warning";
4824
+ }
4825
+ if (low !== void 0 && value <= low) {
4826
+ return "warning";
4827
+ }
4828
+ return "normal";
4829
+ }
4830
+ var DEFAULT_THRESHOLDS = {
4831
+ highHigh: 85,
4832
+ high: 70,
4833
+ low: 30,
4834
+ lowLow: 15
4835
+ };
4836
+
4837
+ // src/diagram/hooks/useSimulation.ts
4838
+ var import_react15 = require("react");
4839
+ function useSimulation(telemetry, updateTelemetryBatch, options = {}) {
4840
+ const {
4841
+ interval = 2e3,
4842
+ enabled = true,
4843
+ maxChange = 10,
4844
+ minValue = 0,
4845
+ maxValue = 100,
4846
+ historyLength = 10,
4847
+ generateValue
4848
+ } = options;
4849
+ (0, import_react15.useEffect)(() => {
4850
+ if (!enabled) return;
4851
+ const intervalId = setInterval(() => {
4852
+ const updates = [];
4853
+ telemetry.forEach((binding, nodeId) => {
4854
+ if (typeof binding.value.value !== "number") return;
4855
+ const currentValue = binding.value.value;
4856
+ let newValue;
4857
+ if (generateValue) {
4858
+ newValue = generateValue(currentValue, nodeId);
4859
+ } else {
4860
+ const change = (Math.random() - 0.5) * maxChange;
4861
+ newValue = Math.max(minValue, Math.min(maxValue, currentValue + change));
4862
+ }
4863
+ const status = calculateThresholdStatus(newValue, binding.thresholds);
4864
+ const newHistory = [...binding.history || [], newValue].slice(-historyLength);
4865
+ updates.push({
4866
+ nodeId,
4867
+ tag: binding.tag,
4868
+ value: {
4869
+ ...binding.value,
4870
+ value: newValue,
4871
+ status,
4872
+ timestamp: Date.now()
4873
+ },
4874
+ history: newHistory,
4875
+ display: binding.display,
4876
+ thresholds: binding.thresholds,
4877
+ statusColors: binding.statusColors
4878
+ });
4879
+ });
4880
+ if (updates.length > 0) {
4881
+ updateTelemetryBatch(updates);
4882
+ }
4883
+ }, interval);
4884
+ return () => clearInterval(intervalId);
4885
+ }, [
4886
+ enabled,
4887
+ interval,
4888
+ maxChange,
4889
+ minValue,
4890
+ maxValue,
4891
+ historyLength,
4892
+ generateValue,
4893
+ telemetry,
4894
+ updateTelemetryBatch
4895
+ ]);
4896
+ }
1743
4897
  // Annotate the CommonJS export names for ESM import in node:
1744
4898
  0 && (module.exports = {
1745
4899
  Button,
@@ -1748,6 +4902,7 @@ var ThemeToggle = ({
1748
4902
  CardValue,
1749
4903
  CircularGauge,
1750
4904
  ControlPanel,
4905
+ DEFAULT_THRESHOLDS,
1751
4906
  DashboardCard,
1752
4907
  DisplayModeToggle,
1753
4908
  EquipmentIndicator,
@@ -1759,6 +4914,8 @@ var ThemeToggle = ({
1759
4914
  LeftPanel,
1760
4915
  LeftToggleButton,
1761
4916
  LegacyValueEntry,
4917
+ NodeConfigPanel,
4918
+ PIDCanvas,
1762
4919
  PanelContent,
1763
4920
  PanelTabs,
1764
4921
  RightPanel,
@@ -1769,13 +4926,31 @@ var ThemeToggle = ({
1769
4926
  Selector,
1770
4927
  Sparkline,
1771
4928
  StatusIndicator,
4929
+ SymbolLibrary,
1772
4930
  ThemeProvider,
1773
4931
  ThemeToggle,
1774
4932
  TrendLine,
1775
4933
  ValueEntry,
1776
4934
  ZoomButton,
1777
4935
  ZoomControls,
4936
+ calculateThresholdStatus,
4937
+ exampleDiagram,
4938
+ exportDiagram,
4939
+ generateRoutePoints,
4940
+ getAvailableSymbols,
4941
+ getPortWorldPosition,
4942
+ getSymbolDefinition,
4943
+ importDiagram,
4944
+ mockSymbolLibrary,
4945
+ nodeHasPort,
1778
4946
  overlayStyles,
1779
- useTheme
4947
+ useDrag,
4948
+ useKeyboardShortcuts,
4949
+ useSelection,
4950
+ useSimulation,
4951
+ useTelemetry,
4952
+ useTheme,
4953
+ useViewBox,
4954
+ validateDiagram
1780
4955
  });
1781
4956
  //# sourceMappingURL=index.cjs.map