@procaaso/alphinity-ui-components 1.0.1 → 1.0.3

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