diff --git a/Doc/RAYSOURCE_VIEW_INTEGRATION_TECHNICAL.md b/XplorePlane/Doc/硬件层及UI集成技术路线.md
similarity index 100%
rename from Doc/RAYSOURCE_VIEW_INTEGRATION_TECHNICAL.md
rename to XplorePlane/Doc/硬件层及UI集成技术路线.md
diff --git a/XplorePlane/Libs/Hardware/XP.Hardware.Detector.dll b/XplorePlane/Libs/Hardware/XP.Hardware.Detector.dll
new file mode 100644
index 0000000..a9c88df
Binary files /dev/null and b/XplorePlane/Libs/Hardware/XP.Hardware.Detector.dll differ
diff --git a/XplorePlane/Libs/Hardware/XP.Hardware.PLC.dll b/XplorePlane/Libs/Hardware/XP.Hardware.PLC.dll
new file mode 100644
index 0000000..dbef651
Binary files /dev/null and b/XplorePlane/Libs/Hardware/XP.Hardware.PLC.dll differ
diff --git a/XplorePlane/Libs/Hardware/XP.Hardware.RaySource.Comet.Messages.dll b/XplorePlane/Libs/Hardware/XP.Hardware.RaySource.Comet.Messages.dll
new file mode 100644
index 0000000..6adcfb0
Binary files /dev/null and b/XplorePlane/Libs/Hardware/XP.Hardware.RaySource.Comet.Messages.dll differ
diff --git a/XplorePlane/Libs/Hardware/XP.Hardware.RaySource_Framework.dll b/XplorePlane/Libs/Hardware/XP.Hardware.RaySource_Framework.dll
new file mode 100644
index 0000000..eb1c1c6
Binary files /dev/null and b/XplorePlane/Libs/Hardware/XP.Hardware.RaySource_Framework.dll differ
diff --git a/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs b/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs
index ae03585..ffc9073 100644
--- a/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs
+++ b/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs
@@ -1,7 +1,11 @@
using System;
using System.Collections.ObjectModel;
+using System.Globalization;
+using System.IO;
using System.Linq;
+using System.Text;
using System.Threading.Tasks;
+using Microsoft.Win32;
using Prism.Commands;
using Prism.Events;
using Prism.Mvvm;
@@ -272,11 +276,20 @@ namespace XplorePlane.ViewModels.Cnc
try
{
- // 使用程序名称作为文件名 | Use program name as file name
- var filePath = $"{_currentProgram.Name}.xp";
- await _cncProgramService.SaveAsync(_currentProgram, filePath);
+ var dlg = new SaveFileDialog
+ {
+ Title = "保存 CNC 程序",
+ Filter = "CNC 程序文件 (*.xp)|*.xp|所有文件 (*.*)|*.*",
+ DefaultExt = ".xp",
+ FileName = _currentProgram.Name
+ };
+
+ if (dlg.ShowDialog() != true)
+ return;
+
+ await _cncProgramService.SaveAsync(_currentProgram, dlg.FileName);
IsModified = false;
- _logger.Info("程序已保存 | Program saved: {FilePath}", filePath);
+ _logger.Info("程序已保存 | Program saved: {FilePath}", dlg.FileName);
}
catch (Exception ex)
{
@@ -291,9 +304,17 @@ namespace XplorePlane.ViewModels.Cnc
{
try
{
- // 占位:实际应通过文件对话框选择路径 | Placeholder: should use file dialog to select path
- var filePath = $"{ProgramName}.xp";
- _currentProgram = await _cncProgramService.LoadAsync(filePath);
+ var dlg = new OpenFileDialog
+ {
+ Title = "加载 CNC 程序",
+ Filter = "CNC 程序文件 (*.xp)|*.xp|所有文件 (*.*)|*.*",
+ DefaultExt = ".xp"
+ };
+
+ if (dlg.ShowDialog() != true)
+ return;
+
+ _currentProgram = await _cncProgramService.LoadAsync(dlg.FileName);
ProgramName = _currentProgram.Name;
IsModified = false;
RefreshNodes();
@@ -319,12 +340,82 @@ namespace XplorePlane.ViewModels.Cnc
}
///
- /// 导出 CSV(占位实现)| Export CSV (placeholder)
+ /// 导出当前程序为 CSV 文件 | Export current program to CSV file
///
private void ExecuteExportCsv()
{
- // TODO: 实现 CSV 导出功能 | Implement CSV export functionality
- _logger.Info("CSV 导出功能尚未实现 | CSV export not yet implemented");
+ if (_currentProgram == null || _currentProgram.Nodes.Count == 0)
+ {
+ _logger.Warn("无法导出 CSV:当前无程序或节点为空 | Cannot export CSV: no program or empty nodes");
+ return;
+ }
+
+ try
+ {
+ var dlg = new SaveFileDialog
+ {
+ Title = "导出 CSV",
+ Filter = "CSV 文件 (*.csv)|*.csv|所有文件 (*.*)|*.*",
+ DefaultExt = ".csv",
+ FileName = _currentProgram.Name
+ };
+
+ if (dlg.ShowDialog() != true)
+ return;
+
+ var sb = new StringBuilder();
+ // CSV 表头 | CSV header
+ sb.AppendLine("Index,NodeType,Name,XM,YM,ZT,ZD,TiltD,Dist,Voltage_kV,Power_W,RayOn,DetectorConnected,FrameRate,Resolution,ImageFile,MarkerType,MarkerX,MarkerY,DialogTitle,DialogMessage,DelayMs,Pipeline");
+
+ var inv = CultureInfo.InvariantCulture;
+
+ foreach (var node in _currentProgram.Nodes)
+ {
+ var row = node switch
+ {
+ ReferencePointNode rp => $"{rp.Index},{rp.NodeType},{Esc(rp.Name)},{rp.XM.ToString(inv)},{rp.YM.ToString(inv)},{rp.ZT.ToString(inv)},{rp.ZD.ToString(inv)},{rp.TiltD.ToString(inv)},{rp.Dist.ToString(inv)},,,,,,,,,,,,,,",
+
+ SaveNodeWithImageNode sni => $"{sni.Index},{sni.NodeType},{Esc(sni.Name)},{sni.MotionState.XM.ToString(inv)},{sni.MotionState.YM.ToString(inv)},{sni.MotionState.ZT.ToString(inv)},{sni.MotionState.ZD.ToString(inv)},{sni.MotionState.TiltD.ToString(inv)},{sni.MotionState.Dist.ToString(inv)},{sni.RaySourceState.Voltage.ToString(inv)},{sni.RaySourceState.Power.ToString(inv)},{sni.RaySourceState.IsOn},{sni.DetectorState.IsConnected},{sni.DetectorState.FrameRate.ToString(inv)},{Esc(sni.DetectorState.Resolution)},{Esc(sni.ImageFileName)},,,,,,",
+
+ SaveNodeNode sn => $"{sn.Index},{sn.NodeType},{Esc(sn.Name)},{sn.MotionState.XM.ToString(inv)},{sn.MotionState.YM.ToString(inv)},{sn.MotionState.ZT.ToString(inv)},{sn.MotionState.ZD.ToString(inv)},{sn.MotionState.TiltD.ToString(inv)},{sn.MotionState.Dist.ToString(inv)},{sn.RaySourceState.Voltage.ToString(inv)},{sn.RaySourceState.Power.ToString(inv)},{sn.RaySourceState.IsOn},{sn.DetectorState.IsConnected},{sn.DetectorState.FrameRate.ToString(inv)},{Esc(sn.DetectorState.Resolution)},,,,,,,",
+
+ SavePositionNode sp => $"{sp.Index},{sp.NodeType},{Esc(sp.Name)},{sp.MotionState.XM.ToString(inv)},{sp.MotionState.YM.ToString(inv)},{sp.MotionState.ZT.ToString(inv)},{sp.MotionState.ZD.ToString(inv)},{sp.MotionState.TiltD.ToString(inv)},{sp.MotionState.Dist.ToString(inv)},,,,,,,,,,,,,,",
+
+ InspectionModuleNode im => $"{im.Index},{im.NodeType},{Esc(im.Name)},,,,,,,,,,,,,,,,,,,{Esc(im.Pipeline?.Name ?? string.Empty)}",
+
+ InspectionMarkerNode mk => $"{mk.Index},{mk.NodeType},{Esc(mk.Name)},,,,,,,,,,,,,,{Esc(mk.MarkerType)},{mk.MarkerX.ToString(inv)},{mk.MarkerY.ToString(inv)},,,",
+
+ PauseDialogNode pd => $"{pd.Index},{pd.NodeType},{Esc(pd.Name)},,,,,,,,,,,,,,,,,{Esc(pd.DialogTitle)},{Esc(pd.DialogMessage)},,",
+
+ WaitDelayNode wd => $"{wd.Index},{wd.NodeType},{Esc(wd.Name)},,,,,,,,,,,,,,,,,,,{wd.DelayMilliseconds},",
+
+ CompleteProgramNode cp => $"{cp.Index},{cp.NodeType},{Esc(cp.Name)},,,,,,,,,,,,,,,,,,,,",
+
+ _ => $"{node.Index},{node.NodeType},{Esc(node.Name)},,,,,,,,,,,,,,,,,,,,"
+ };
+
+ sb.AppendLine(row);
+ }
+
+ File.WriteAllText(dlg.FileName, sb.ToString(), Encoding.UTF8);
+ _logger.Info("CSV 已导出 | CSV exported: {FilePath}, 节点数={Count}", dlg.FileName, _currentProgram.Nodes.Count);
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "导出 CSV 失败 | Failed to export CSV");
+ }
+ }
+
+ ///
+ /// CSV 字段转义:含逗号、引号或换行时用双引号包裹
+ /// Escape CSV field: wrap with double quotes if it contains comma, quote, or newline
+ ///
+ private static string Esc(string value)
+ {
+ if (string.IsNullOrEmpty(value)) return string.Empty;
+ if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
+ return $"\"{value.Replace("\"", "\"\"")}\"";
+ return value;
}
// ── 辅助方法 | Helper methods ───────────────────────────────────
diff --git a/XplorePlane/Views/Cnc/WindowIconHelper.cs b/XplorePlane/Views/Cnc/WindowIconHelper.cs
new file mode 100644
index 0000000..f5dbc15
--- /dev/null
+++ b/XplorePlane/Views/Cnc/WindowIconHelper.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Windows;
+using System.Windows.Interop;
+
+namespace XplorePlane.Views.Cnc
+{
+ ///
+ /// 通过 Win32 API 移除 WPF 窗口标题栏图标。
+ ///
+ internal static class WindowIconHelper
+ {
+ private const int GWL_EXSTYLE = -20;
+ private const int WS_EX_DLGMODALFRAME = 0x0001;
+ private const int SWP_NOSIZE = 0x0001;
+ private const int SWP_NOMOVE = 0x0002;
+ private const int SWP_NOZORDER = 0x0004;
+ private const int SWP_FRAMECHANGED = 0x0020;
+ private const uint WM_SETICON = 0x0080;
+
+ [DllImport("user32.dll")]
+ private static extern int GetWindowLong(IntPtr hwnd, int index);
+
+ [DllImport("user32.dll")]
+ private static extern int SetWindowLong(IntPtr hwnd, int index, int newStyle);
+
+ [DllImport("user32.dll")]
+ private static extern bool SetWindowPos(IntPtr hwnd, IntPtr hwndInsertAfter,
+ int x, int y, int cx, int cy, uint flags);
+
+ [DllImport("user32.dll")]
+ private static extern IntPtr SendMessage(IntPtr hwnd, uint msg, IntPtr wParam, IntPtr lParam);
+
+ public static void RemoveIcon(Window window)
+ {
+ var hwnd = new WindowInteropHelper(window).Handle;
+
+ // 添加 WS_EX_DLGMODALFRAME 扩展样式以移除图标
+ int extendedStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
+ SetWindowLong(hwnd, GWL_EXSTYLE, extendedStyle | WS_EX_DLGMODALFRAME);
+
+ // 清除大小图标
+ SendMessage(hwnd, WM_SETICON, IntPtr.Zero, IntPtr.Zero);
+ SendMessage(hwnd, WM_SETICON, new IntPtr(1), IntPtr.Zero);
+
+ // 刷新窗口非客户区
+ SetWindowPos(hwnd, IntPtr.Zero, 0, 0, 0, 0,
+ SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
+ }
+ }
+}