@procaaso/alphinity-ui-components 1.0.1 → 1.0.2

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