Files
XplorePlane/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs
T
2026-04-21 01:49:09 +08:00

470 lines
21 KiB
C#

using Microsoft.Win32;
using Prism.Commands;
using Prism.Events;
using Prism.Mvvm;
using System;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using XP.Common.Logging.Interfaces;
using XplorePlane.Events;
using XplorePlane.Models;
using XplorePlane.Services.AppState;
using XplorePlane.Services.Cnc;
namespace XplorePlane.ViewModels.Cnc
{
/// <summary>
/// CNC 编辑器 ViewModel,管理 CNC 程序的节点列表、编辑操作和文件操作
/// CNC editor ViewModel that manages the node list, editing operations and file operations
/// </summary>
public class CncEditorViewModel : BindableBase
{
private readonly ICncProgramService _cncProgramService;
private readonly IAppStateService _appStateService;
private readonly IEventAggregator _eventAggregator;
private readonly ILoggerService _logger;
// 当前 CNC 程序 | Current CNC program
private CncProgram _currentProgram;
private ObservableCollection<CncNodeViewModel> _nodes;
private CncNodeViewModel _selectedNode;
private bool _isModified;
private string _programName;
/// <summary>
/// 构造函数 | Constructor
/// </summary>
public CncEditorViewModel(
ICncProgramService cncProgramService,
IAppStateService appStateService,
IEventAggregator eventAggregator,
ILoggerService logger)
{
_cncProgramService = cncProgramService ?? throw new ArgumentNullException(nameof(cncProgramService));
_appStateService = appStateService ?? throw new ArgumentNullException(nameof(appStateService));
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule<CncEditorViewModel>();
_nodes = new ObservableCollection<CncNodeViewModel>();
// ── 节点插入命令 | Node insertion commands ──
InsertReferencePointCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.ReferencePoint));
InsertSaveNodeWithImageCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNodeWithImage));
InsertSaveNodeCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNode));
InsertSavePositionCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SavePosition));
InsertInspectionModuleCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.InspectionModule));
InsertInspectionMarkerCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.InspectionMarker));
InsertPauseDialogCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.PauseDialog));
InsertWaitDelayCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.WaitDelay));
InsertCompleteProgramCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.CompleteProgram));
// ── 节点编辑命令 | Node editing commands ──
DeleteNodeCommand = new DelegateCommand(ExecuteDeleteNode, CanExecuteDeleteNode)
.ObservesProperty(() => SelectedNode);
MoveNodeUpCommand = new DelegateCommand<CncNodeViewModel>(ExecuteMoveNodeUp);
MoveNodeDownCommand = new DelegateCommand<CncNodeViewModel>(ExecuteMoveNodeDown);
// ── 文件操作命令 | File operation commands ──
SaveProgramCommand = new DelegateCommand(async () => await ExecuteSaveProgramAsync());
LoadProgramCommand = new DelegateCommand(async () => await ExecuteLoadProgramAsync());
NewProgramCommand = new DelegateCommand(ExecuteNewProgram);
ExportCsvCommand = new DelegateCommand(ExecuteExportCsv);
_logger.Info("CncEditorViewModel 已初始化 | CncEditorViewModel initialized");
}
// ── 属性 | Properties ──────────────────────────────────────────
/// <summary>节点列表 | Node list</summary>
public ObservableCollection<CncNodeViewModel> Nodes
{
get => _nodes;
set => SetProperty(ref _nodes, value);
}
/// <summary>当前选中的节点 | Currently selected node</summary>
public CncNodeViewModel SelectedNode
{
get => _selectedNode;
set => SetProperty(ref _selectedNode, value);
}
/// <summary>程序是否已修改 | Whether the program has been modified</summary>
public bool IsModified
{
get => _isModified;
set => SetProperty(ref _isModified, value);
}
/// <summary>当前程序名称 | Current program name</summary>
public string ProgramName
{
get => _programName;
set => SetProperty(ref _programName, value);
}
// ── 节点插入命令 | Node insertion commands ──────────────────────
/// <summary>插入参考点命令 | Insert reference point command</summary>
public DelegateCommand InsertReferencePointCommand { get; }
/// <summary>插入保存节点(含图像)命令 | Insert save node with image command</summary>
public DelegateCommand InsertSaveNodeWithImageCommand { get; }
/// <summary>插入保存节点命令 | Insert save node command</summary>
public DelegateCommand InsertSaveNodeCommand { get; }
/// <summary>插入保存位置命令 | Insert save position command</summary>
public DelegateCommand InsertSavePositionCommand { get; }
/// <summary>插入检测模块命令 | Insert inspection module command</summary>
public DelegateCommand InsertInspectionModuleCommand { get; }
/// <summary>插入检测标记命令 | Insert inspection marker command</summary>
public DelegateCommand InsertInspectionMarkerCommand { get; }
/// <summary>插入停顿对话框命令 | Insert pause dialog command</summary>
public DelegateCommand InsertPauseDialogCommand { get; }
/// <summary>插入等待延时命令 | Insert wait delay command</summary>
public DelegateCommand InsertWaitDelayCommand { get; }
/// <summary>插入完成程序命令 | Insert complete program command</summary>
public DelegateCommand InsertCompleteProgramCommand { get; }
// ── 节点编辑命令 | Node editing commands ────────────────────────
/// <summary>删除选中节点命令 | Delete selected node command</summary>
public DelegateCommand DeleteNodeCommand { get; }
/// <summary>上移节点命令 | Move node up command</summary>
public DelegateCommand<CncNodeViewModel> MoveNodeUpCommand { get; }
/// <summary>下移节点命令 | Move node down command</summary>
public DelegateCommand<CncNodeViewModel> MoveNodeDownCommand { get; }
// ── 文件操作命令 | File operation commands ──────────────────────
/// <summary>保存程序命令 | Save program command</summary>
public DelegateCommand SaveProgramCommand { get; }
/// <summary>加载程序命令 | Load program command</summary>
public DelegateCommand LoadProgramCommand { get; }
/// <summary>新建程序命令 | New program command</summary>
public DelegateCommand NewProgramCommand { get; }
/// <summary>导出 CSV 命令 | Export CSV command</summary>
public DelegateCommand ExportCsvCommand { get; }
// ── 命令执行方法 | Command execution methods ────────────────────
/// <summary>
/// 插入指定类型的节点到选中节点之后
/// Insert a node of the specified type after the selected node
/// </summary>
private void ExecuteInsertNode(CncNodeType nodeType)
{
if (_currentProgram == null)
{
_logger.Warn("无法插入节点:当前无程序 | Cannot insert node: no current program");
ExecuteNewProgram();
}
try
{
var node = _cncProgramService.CreateNode(nodeType);
int afterIndex = SelectedNode?.Index ?? _currentProgram.Nodes.Count - 1;
_currentProgram = _cncProgramService.InsertNode(_currentProgram, afterIndex, node);
OnProgramEdited();
_logger.Info("已插入节点 | Inserted node: Type={NodeType}", nodeType);
}
catch (InvalidOperationException ex)
{
// 重复插入 CompleteProgram 等业务规则异常 | Business rule exceptions like duplicate CompleteProgram
_logger.Warn("插入节点被阻止 | Node insertion blocked: {Message}", ex.Message);
}
}
/// <summary>
/// 删除选中节点 | Delete the selected node
/// </summary>
private void ExecuteDeleteNode()
{
if (_currentProgram == null || SelectedNode == null)
return;
try
{
_currentProgram = _cncProgramService.RemoveNode(_currentProgram, SelectedNode.Index);
OnProgramEdited();
_logger.Info("已删除节点 | Deleted node at index: {Index}", SelectedNode.Index);
}
catch (ArgumentOutOfRangeException ex)
{
_logger.Warn("删除节点失败 | Delete node failed: {Message}", ex.Message);
}
}
/// <summary>
/// 判断是否可以删除节点(至少保留 1 个节点)
/// Determines whether delete is allowed (at least 1 node must remain)
/// </summary>
private bool CanExecuteDeleteNode()
{
return SelectedNode != null
&& _currentProgram != null
&& _currentProgram.Nodes.Count > 1;
}
/// <summary>
/// 上移节点 | Move node up
/// </summary>
private void ExecuteMoveNodeUp(CncNodeViewModel nodeVm)
{
if (_currentProgram == null || nodeVm == null || nodeVm.Index <= 0)
return;
try
{
_currentProgram = _cncProgramService.MoveNode(_currentProgram, nodeVm.Index, nodeVm.Index - 1);
OnProgramEdited();
_logger.Info("已上移节点 | Moved node up: {OldIndex} -> {NewIndex}", nodeVm.Index, nodeVm.Index - 1);
}
catch (ArgumentOutOfRangeException ex)
{
_logger.Warn("上移节点失败 | Move node up failed: {Message}", ex.Message);
}
}
/// <summary>
/// 下移节点 | Move node down
/// </summary>
private void ExecuteMoveNodeDown(CncNodeViewModel nodeVm)
{
if (_currentProgram == null || nodeVm == null || nodeVm.Index >= _currentProgram.Nodes.Count - 1)
return;
try
{
_currentProgram = _cncProgramService.MoveNode(_currentProgram, nodeVm.Index, nodeVm.Index + 1);
OnProgramEdited();
_logger.Info("已下移节点 | Moved node down: {OldIndex} -> {NewIndex}", nodeVm.Index, nodeVm.Index + 1);
}
catch (ArgumentOutOfRangeException ex)
{
_logger.Warn("下移节点失败 | Move node down failed: {Message}", ex.Message);
}
}
/// <summary>
/// 保存当前程序到文件 | Save current program to file
/// </summary>
private async Task ExecuteSaveProgramAsync()
{
if (_currentProgram == null)
{
_logger.Warn("无法保存:当前无程序 | Cannot save: no current program");
return;
}
try
{
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}", dlg.FileName);
}
catch (Exception ex)
{
_logger.Error(ex, "保存程序失败 | Failed to save program");
}
}
/// <summary>
/// 从文件加载程序 | Load program from file
/// </summary>
private async Task ExecuteLoadProgramAsync()
{
try
{
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();
_logger.Info("程序已加载 | Program loaded: {ProgramName}", _currentProgram.Name);
}
catch (Exception ex)
{
_logger.Error(ex, "加载程序失败 | Failed to load program");
}
}
/// <summary>
/// 创建新程序 | Create a new program
/// </summary>
private void ExecuteNewProgram()
{
var name = string.IsNullOrWhiteSpace(ProgramName) ? "新程序" : ProgramName;
_currentProgram = _cncProgramService.CreateProgram(name);
ProgramName = _currentProgram.Name;
IsModified = false;
RefreshNodes();
_logger.Info("已创建新程序 | Created new program: {ProgramName}", name);
}
/// <summary>
/// 导出当前程序为 CSV 文件 | Export current program to CSV file
/// </summary>
private void ExecuteExportCsv()
{
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,Current_uA,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)},{rp.Voltage.ToString(inv)},{rp.Current.ToString(inv)},,{rp.IsRayOn},,,,,,,,,,",
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");
}
}
/// <summary>
/// CSV 字段转义:含逗号、引号或换行时用双引号包裹
/// Escape CSV field: wrap with double quotes if it contains comma, quote, or newline
/// </summary>
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 ───────────────────────────────────
/// <summary>
/// 编辑操作后的统一处理:刷新节点、标记已修改、发布变更事件
/// Unified post-edit handling: refresh nodes, mark modified, publish change event
/// </summary>
private void OnProgramEdited()
{
IsModified = true;
RefreshNodes();
PublishProgramChanged();
}
/// <summary>
/// 从 _currentProgram.Nodes 重建 Nodes 集合
/// Rebuild the Nodes collection from _currentProgram.Nodes
/// </summary>
private void RefreshNodes()
{
Nodes.Clear();
if (_currentProgram?.Nodes == null)
return;
foreach (var node in _currentProgram.Nodes)
{
Nodes.Add(new CncNodeViewModel(node));
}
// 尝试保持选中状态 | Try to preserve selection
if (SelectedNode != null)
{
var match = Nodes.FirstOrDefault(n => n.Index == SelectedNode.Index);
SelectedNode = match ?? Nodes.LastOrDefault();
}
}
/// <summary>
/// 通过 IEventAggregator 发布 CNC 程序变更事件
/// Publish CNC program changed event via IEventAggregator
/// </summary>
private void PublishProgramChanged()
{
_eventAggregator
.GetEvent<CncProgramChangedEvent>()
.Publish(new CncProgramChangedPayload(ProgramName ?? string.Empty, IsModified));
}
}
}