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