Files
XplorePlane/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs
T
zhengxuan.zhang 741874e85d 基于角色的权限控制
1、用户角色枚举、权限枚举、结果记录和密码存储模型
IPermissionService 接口及包含认证、权限检查、密码管理和登出功能的 PermissionService 单例
2、支持层级化角色-权限映射的权限矩阵(SuperAdmin ⊇ Admin ⊇ User)
密码持久化至 passwords.json 文件,并提供工厂默认值回退机制
3、UI 层
LoginDialog — 启动时弹出模态登录对话框,支持密码掩码输入、错误提示以及取消退出功能
RibbonStatusAreaView — 在Ribbon右侧区域始终显示角色标签和“切换用户”按钮
权限感知的CncEditorViewModel — 用户角色无法使用CNC编辑控件
权限感知的CncInspectionModulePipelineViewModel — 用户角色无法进行流程编辑
设置导航可见性 — Admin/User角色隐藏Factory_Settings,User角色隐藏Report_Settings
PasswordManagementView — 仅SuperAdmin可访问的修改角色密码对话框
PermissionTooltipHelper — 附加属性,在禁用控件上显示“当前角色无权访问此功能”提示
2026-06-01 17:15:59 +08:00

1096 lines
44 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Microsoft.Win32;
using Prism.Commands;
using Prism.Events;
using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using XP.Common.Logging.Interfaces;
using XplorePlane.Events;
using XplorePlane.Models;
using XplorePlane.Services.AppState;
using XplorePlane.Services.Cnc;
using XplorePlane.Services;
using XplorePlane.Services.Permission;
using XplorePlane.Services.Storage;
using PermissionEnum = XplorePlane.Models.Permission;
namespace XplorePlane.ViewModels.Cnc
{
/// <summary>
/// CNC editor ViewModel that manages the node tree, editing operations and file operations.
/// </summary>
public class CncEditorViewModel : BindableBase
{
private readonly ICncProgramService _cncProgramService;
private readonly IEventAggregator _eventAggregator;
private readonly ILoggerService _logger;
private readonly ICncExecutionService _cncExecutionService;
private readonly IXpDataPathService _dataPathService;
private readonly IPipelinePersistenceService _pipelinePersistenceService;
private readonly IImageProcessingService _imageProcessingService;
private readonly IPermissionService _permissionService;
private CncProgram _currentProgram;
private ObservableCollection<CncNodeViewModel> _nodes;
private ObservableCollection<CncNodeViewModel> _treeNodes;
private ObservableCollection<CncProgramTreeRootViewModel> _programTreeRoots;
private CncNodeViewModel _selectedNode;
private bool _isModified;
private string _programName;
private string _programDisplayName = "新建检测程序.xp";
private Guid? _preferredSelectedNodeId;
private Guid? _pendingInsertAnchorNodeId;
private bool _pendingInsertAfterAnchor;
private CancellationTokenSource _cts;
private bool _isRunning;
private string _statusMessage = "就绪";
private string _executionError;
private bool _hasExecutionError;
public CncEditorViewModel(
ICncProgramService cncProgramService,
IAppStateService appStateService,
IEventAggregator eventAggregator,
ILoggerService logger,
ICncExecutionService cncExecutionService,
IXpDataPathService dataPathService,
IPipelinePersistenceService pipelinePersistenceService,
IPermissionService permissionService,
IImageProcessingService imageProcessingService = null)
{
_cncProgramService = cncProgramService ?? throw new ArgumentNullException(nameof(cncProgramService));
ArgumentNullException.ThrowIfNull(appStateService);
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule<CncEditorViewModel>();
_cncExecutionService = cncExecutionService ?? throw new ArgumentNullException(nameof(cncExecutionService));
_dataPathService = dataPathService ?? throw new ArgumentNullException(nameof(dataPathService));
_pipelinePersistenceService = pipelinePersistenceService ?? throw new ArgumentNullException(nameof(pipelinePersistenceService));
_permissionService = permissionService ?? throw new ArgumentNullException(nameof(permissionService));
_imageProcessingService = imageProcessingService; // optional — used for pipeline step display names
_nodes = new ObservableCollection<CncNodeViewModel>();
_treeNodes = new ObservableCollection<CncNodeViewModel>();
_programTreeRoots = new ObservableCollection<CncProgramTreeRootViewModel>
{
new CncProgramTreeRootViewModel(_programDisplayName, _treeNodes)
};
InsertReferencePointCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.ReferencePoint), () => !IsRunning && CanEditCncProgram);
InsertSaveNodeWithImageCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNodeWithImage), () => !IsRunning && CanEditCncProgram);
InsertSaveNodeCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNode), () => !IsRunning && CanEditCncProgram);
InsertSavePositionCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SavePosition), () => !IsRunning && CanEditCncProgram);
InsertInspectionModuleCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.InspectionModule), () => !IsRunning && CanEditCncProgram);
InsertInspectionMarkerCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.InspectionMarker), () => !IsRunning && CanEditCncProgram);
InsertPauseDialogCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.PauseDialog), () => !IsRunning && CanEditCncProgram);
InsertWaitDelayCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.WaitDelay), () => !IsRunning && CanEditCncProgram);
InsertCompleteProgramCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.CompleteProgram), () => !IsRunning && CanEditCncProgram);
DeleteNodeCommand = new DelegateCommand(ExecuteDeleteNode, CanExecuteDeleteNode)
.ObservesProperty(() => SelectedNode);
MoveNodeUpCommand = new DelegateCommand<CncNodeViewModel>(ExecuteMoveNodeUp);
MoveNodeDownCommand = new DelegateCommand<CncNodeViewModel>(ExecuteMoveNodeDown);
PrepareInsertAboveCommand = new DelegateCommand<CncNodeViewModel>(nodeVm => SetPendingInsertAnchor(nodeVm, insertAfter: false));
PrepareInsertBelowCommand = new DelegateCommand<CncNodeViewModel>(nodeVm => SetPendingInsertAnchor(nodeVm, insertAfter: true));
SaveProgramCommand = new DelegateCommand(async () => await ExecuteSaveProgramAsync(), () => CanEditCncProgram);
LoadProgramCommand = new DelegateCommand(async () => await ExecuteLoadProgramAsync());
NewProgramCommand = new DelegateCommand(ExecuteNewProgram, () => CanEditCncProgram);
ExportCsvCommand = new DelegateCommand(ExecuteExportCsv);
RunCncCommand = new DelegateCommand(async () => await ExecuteRunAsync(), CanExecuteRun);
StopCncCommand = new DelegateCommand(ExecuteStop, CanExecuteStop);
// Subscribe to role changes to refresh permission-dependent properties
_eventAggregator.GetEvent<RoleChangedEvent>().Subscribe(OnRoleChanged);
_logger.Info("CncEditorViewModel initialized");
}
public ObservableCollection<CncNodeViewModel> Nodes
{
get => _nodes;
private set => SetProperty(ref _nodes, value);
}
public ObservableCollection<CncNodeViewModel> TreeNodes
{
get => _treeNodes;
private set => SetProperty(ref _treeNodes, value);
}
public ObservableCollection<CncProgramTreeRootViewModel> ProgramTreeRoots
{
get => _programTreeRoots;
private set => SetProperty(ref _programTreeRoots, value);
}
public CncNodeViewModel SelectedNode
{
get => _selectedNode;
set
{
if (SetProperty(ref _selectedNode, value))
{
RaisePropertyChanged(nameof(HasSelection));
}
}
}
public bool HasSelection => SelectedNode != null;
public bool IsModified
{
get => _isModified;
set => SetProperty(ref _isModified, value);
}
public string ProgramName
{
get => _programName;
set => SetProperty(ref _programName, value);
}
public string ProgramDisplayName
{
get => _programDisplayName;
private set
{
if (SetProperty(ref _programDisplayName, value) && ProgramTreeRoots?.Count > 0)
ProgramTreeRoots[0].DisplayName = value;
}
}
public bool IsRunning
{
get => _isRunning;
private set
{
if (SetProperty(ref _isRunning, value))
{
RunCncCommand.RaiseCanExecuteChanged();
StopCncCommand.RaiseCanExecuteChanged();
RaiseEditCommandsCanExecuteChanged();
}
}
}
public string StatusMessage
{
get => _statusMessage;
private set => SetProperty(ref _statusMessage, value);
}
public string ExecutionError
{
get => _executionError;
private set => SetProperty(ref _executionError, value);
}
public bool HasExecutionError
{
get => _hasExecutionError;
private set => SetProperty(ref _hasExecutionError, value);
}
/// <summary>当前角色是否允许编辑 CNC 程序(插入、删除、重命名、重排序、新建、保存、删除文件)。</summary>
public bool CanEditCncProgram => _permissionService.HasPermission(PermissionEnum.CncInsertNode);
/// <summary>当前角色是否允许编辑检测模块(添加、删除、重排序、启用/禁用、编辑参数)。</summary>
public bool CanEditInspectionModule => _permissionService.HasPermission(PermissionEnum.InspectionAddOperator);
public DelegateCommand InsertReferencePointCommand { get; }
public DelegateCommand InsertSaveNodeWithImageCommand { get; }
public DelegateCommand InsertSaveNodeCommand { get; }
public DelegateCommand InsertSavePositionCommand { get; }
public DelegateCommand InsertInspectionModuleCommand { get; }
public DelegateCommand InsertInspectionMarkerCommand { get; }
public DelegateCommand InsertPauseDialogCommand { get; }
public DelegateCommand InsertWaitDelayCommand { get; }
public DelegateCommand InsertCompleteProgramCommand { get; }
public DelegateCommand DeleteNodeCommand { get; }
public DelegateCommand<CncNodeViewModel> MoveNodeUpCommand { get; }
public DelegateCommand<CncNodeViewModel> MoveNodeDownCommand { get; }
public DelegateCommand<CncNodeViewModel> PrepareInsertAboveCommand { get; }
public DelegateCommand<CncNodeViewModel> PrepareInsertBelowCommand { get; }
public DelegateCommand SaveProgramCommand { get; }
public DelegateCommand LoadProgramCommand { get; }
public DelegateCommand NewProgramCommand { get; }
public DelegateCommand ExportCsvCommand { get; }
public DelegateCommand RunCncCommand { get; }
public DelegateCommand StopCncCommand { get; }
private void ExecuteInsertNode(CncNodeType nodeType)
{
if (IsRunning)
return;
if (_currentProgram == null)
{
ExecuteNewProgram();
}
try
{
var node = _cncProgramService.CreateNode(nodeType);
int afterIndex = ResolveInsertAfterIndex(nodeType);
_currentProgram = _cncProgramService.InsertNode(_currentProgram, afterIndex, node);
_preferredSelectedNodeId = node.Id;
ClearPendingInsertAnchor();
OnProgramEdited();
_logger.Info("Inserted node: Type={NodeType}", nodeType);
}
catch (InvalidOperationException ex)
{
_logger.Warn("Node insertion blocked: {Message}", ex.Message);
}
}
private void ExecuteDeleteNode()
{
if (_currentProgram == null || SelectedNode == null)
return;
try
{
int deletedIndex = SelectedNode.Index;
if (SelectedNode.IsSavePosition)
{
var nodes = _currentProgram.Nodes.ToList();
int startIndex = deletedIndex;
int endIndex = GetSavePositionBlockEndIndex(startIndex);
nodes.RemoveRange(startIndex, endIndex - startIndex + 1);
_currentProgram = ReplaceProgramNodes(nodes);
}
else
{
_currentProgram = _cncProgramService.RemoveNode(_currentProgram, deletedIndex);
}
OnProgramEdited();
ClearPendingInsertAnchorIfMissing();
_logger.Info("Deleted node at index: {Index}", deletedIndex);
}
catch (ArgumentOutOfRangeException ex)
{
_logger.Warn("Delete node failed: {Message}", ex.Message);
}
}
private bool CanExecuteDeleteNode()
{
return !IsRunning
&& CanEditCncProgram
&& SelectedNode != null
&& _currentProgram != null
&& _currentProgram.Nodes.Count > 1;
}
private void ExecuteMoveNodeUp(CncNodeViewModel nodeVm)
{
if (IsRunning || !CanEditCncProgram || _currentProgram == null || nodeVm == null || nodeVm.Index <= 0)
return;
try
{
if (IsSavePositionChild(nodeVm.NodeType))
{
MoveSavePositionChild(nodeVm, moveDown: false);
}
else
{
MoveRootBlock(nodeVm, moveDown: false);
}
OnProgramEdited();
}
catch (Exception ex) when (ex is ArgumentOutOfRangeException or InvalidOperationException)
{
_logger.Warn("Move node up failed: {Message}", ex.Message);
}
}
private void ExecuteMoveNodeDown(CncNodeViewModel nodeVm)
{
if (IsRunning || !CanEditCncProgram || _currentProgram == null || nodeVm == null || nodeVm.Index >= _currentProgram.Nodes.Count - 1)
return;
try
{
if (IsSavePositionChild(nodeVm.NodeType))
{
MoveSavePositionChild(nodeVm, moveDown: true);
}
else
{
MoveRootBlock(nodeVm, moveDown: true);
}
OnProgramEdited();
}
catch (Exception ex) when (ex is ArgumentOutOfRangeException or InvalidOperationException)
{
_logger.Warn("Move node down failed: {Message}", ex.Message);
}
}
/// <summary>供外部直接 await 的保存方法</summary>
public Task SaveAsync() => ExecuteSaveProgramAsync();
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,
InitialDirectory = GetPlanDirectory()
};
if (dlg.ShowDialog() != true)
return;
await _cncProgramService.SaveAsync(_currentProgram, dlg.FileName);
ProgramDisplayName = Path.GetFileName(dlg.FileName);
IsModified = false;
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to save program");
}
}
private async Task ExecuteLoadProgramAsync()
{
try
{
var dlg = new OpenFileDialog
{
Title = "加载 CNC 程序",
Filter = "CNC 程序文件 (*.xp)|*.xp|所有文件 (*.*)|*.*",
DefaultExt = ".xp",
InitialDirectory = GetPlanDirectory()
};
if (dlg.ShowDialog() != true)
return;
_currentProgram = await _cncProgramService.LoadAsync(dlg.FileName);
ProgramName = _currentProgram.Name;
ProgramDisplayName = Path.GetFileName(dlg.FileName);
IsModified = false;
ClearPendingInsertAnchor();
RefreshNodes();
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to load program");
}
}
private void ExecuteNewProgram()
{
const string name = "新建检测程序";
_currentProgram = _cncProgramService.CreateProgram(name);
ProgramName = _currentProgram.Name;
ProgramDisplayName = FormatProgramDisplayName(_currentProgram.Name);
IsModified = false;
ClearPendingInsertAnchor();
RefreshNodes();
}
private void ExecuteExportCsv()
{
if (_currentProgram == null || _currentProgram.Nodes.Count == 0)
{
_logger.Warn("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();
sb.AppendLine("Index,NodeType,Name,SourceZ,DetectorZ,StageX,StageY,DetectorSwing,StageRotation,FixtureRotation,FOD,FDD,Magnification,Voltage_kV,Current_uA,Power_W,RayOn,DetectorConnected,FrameRate,Resolution,ImageFile,SaveImage,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.SourceZ.ToString(inv)},{rp.DetectorZ.ToString(inv)},{rp.StageX.ToString(inv)},{rp.StageY.ToString(inv)},{rp.DetectorSwing.ToString(inv)},{rp.StageRotation.ToString(inv)},{rp.FixtureRotation.ToString(inv)},{rp.FOD.ToString(inv)},{rp.FDD.ToString(inv)},{rp.Magnification.ToString(inv)},{rp.Voltage.ToString(inv)},{rp.Current.ToString(inv)},,{rp.IsRayOn},,,,,,,,,,",
SaveNodeWithImageNode sni => $"{sni.Index},{sni.NodeType},{Esc(sni.Name)},{sni.MotionState.SourceZ.ToString(inv)},{sni.MotionState.DetectorZ.ToString(inv)},{sni.MotionState.StageX.ToString(inv)},{sni.MotionState.StageY.ToString(inv)},{sni.MotionState.DetectorSwing.ToString(inv)},{sni.MotionState.StageRotation.ToString(inv)},{sni.MotionState.FixtureRotation.ToString(inv)},{sni.MotionState.FOD.ToString(inv)},{sni.MotionState.FDD.ToString(inv)},{sni.MotionState.Magnification.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.SourceZ.ToString(inv)},{sn.MotionState.DetectorZ.ToString(inv)},{sn.MotionState.StageX.ToString(inv)},{sn.MotionState.StageY.ToString(inv)},{sn.MotionState.DetectorSwing.ToString(inv)},{sn.MotionState.StageRotation.ToString(inv)},{sn.MotionState.FixtureRotation.ToString(inv)},{sn.MotionState.FOD.ToString(inv)},{sn.MotionState.FDD.ToString(inv)},{sn.MotionState.Magnification.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.SourceZ.ToString(inv)},{sp.MotionState.DetectorZ.ToString(inv)},{sp.MotionState.StageX.ToString(inv)},{sp.MotionState.StageY.ToString(inv)},{sp.MotionState.DetectorSwing.ToString(inv)},{sp.MotionState.StageRotation.ToString(inv)},{sp.MotionState.FixtureRotation.ToString(inv)},{sp.MotionState.FOD.ToString(inv)},{sp.MotionState.FDD.ToString(inv)},{sp.MotionState.Magnification.ToString(inv)},,,,,,,,{sp.SaveImage},,,,,,,",
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);
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to export CSV");
}
}
public async Task InsertInspectionModuleFromPipelineFileAsync(string filePath)
{
if (IsRunning)
return;
if (string.IsNullOrWhiteSpace(filePath))
throw new ArgumentException("检测模块文件路径不能为空。", nameof(filePath));
if (!File.Exists(filePath))
throw new FileNotFoundException("检测模块文件不存在。", filePath);
if (_currentProgram == null)
{
ExecuteNewProgram();
}
var pipeline = await _pipelinePersistenceService.LoadAsync(filePath);
try
{
var node = _cncProgramService.CreateNode(CncNodeType.InspectionModule);
if (node is not InspectionModuleNode inspectionModuleNode)
throw new InvalidOperationException("无法创建检测模块节点。");
var pipelineName = string.IsNullOrWhiteSpace(pipeline.Name)
? Path.GetFileNameWithoutExtension(filePath)
: pipeline.Name;
pipeline.Name = pipelineName;
var configuredNode = inspectionModuleNode with
{
Pipeline = pipeline,
Name = pipelineName
};
int afterIndex = ResolveInsertAfterIndex(CncNodeType.InspectionModule);
_currentProgram = _cncProgramService.InsertNode(_currentProgram, afterIndex, configuredNode);
_preferredSelectedNodeId = configuredNode.Id;
ClearPendingInsertAnchor();
OnProgramEdited();
StatusMessage = $"已插入检测模块:{pipelineName}";
_logger.Info("Inserted built-in inspection module from file: {FilePath}", filePath);
}
catch (InvalidOperationException ex)
{
_logger.Warn("Built-in inspection module insertion blocked: {Message}", ex.Message);
throw;
}
}
private string GetPlanDirectory()
{
var directory = _dataPathService.PlanPath;
Directory.CreateDirectory(directory);
return directory;
}
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;
}
private bool CanExecuteRun()
=> !IsRunning && _currentProgram?.Nodes?.Count > 0;
private bool CanExecuteStop()
=> IsRunning;
private async Task ExecuteRunAsync()
{
_cts = new CancellationTokenSource();
ResetAllNodeStates();
IsRunning = true;
HasExecutionError = false;
ExecutionError = null;
StatusMessage = $"正在执行:{_currentProgram?.Name ?? ""}(共 {_currentProgram?.Nodes?.Count ?? 0} 个节点)";
try
{
var progress = new Progress<CncNodeExecutionProgress>(OnExecutionProgress);
await _cncExecutionService.ExecuteAsync(_currentProgram, progress, _cts.Token);
if (_cts.IsCancellationRequested)
StatusMessage = "执行已停止";
else
StatusMessage = $"执行完成:{_currentProgram?.Name}";
}
catch (OperationCanceledException)
{
StatusMessage = "执行已取消";
}
catch (Exception ex)
{
_logger.Error(ex, "CNC execution failed");
ExecutionError = ex.Message;
HasExecutionError = true;
StatusMessage = $"执行失败:{ex.Message}";
}
finally
{
IsRunning = false;
ResetAllNodeStates();
_cts?.Dispose();
_cts = null;
}
}
private void ExecuteStop()
{
_cts?.Cancel();
}
private void OnExecutionProgress(CncNodeExecutionProgress progress)
{
var nodeVm = Nodes.FirstOrDefault(n => n.Id == progress.NodeId);
if (nodeVm != null)
{
nodeVm.ExecutionState = progress.State;
nodeVm.ExecutionProgressPercent = progress.ProgressPercent ?? (progress.State == NodeExecutionState.Succeeded ? 100d : 0d);
if (progress.State == NodeExecutionState.Running)
StatusMessage = $"正在执行节点:{nodeVm.Name}{nodeVm.Index + 1}/{_currentProgram?.Nodes?.Count ?? 0}";
else if (progress.State == NodeExecutionState.Succeeded)
{
// 缓存结果图像到节点,供切换节点时使用
if (progress.ResultImage != null)
nodeVm.ResultImage = progress.ResultImage;
}
else if (progress.State == NodeExecutionState.Failed)
{
HasExecutionError = true;
ExecutionError = $"节点 [{nodeVm.Name}] 执行失败";
StatusMessage = $"错误:节点 [{nodeVm.Name}] 执行失败";
}
}
}
private void ResetAllNodeStates()
{
foreach (var node in Nodes)
{
node.ExecutionState = NodeExecutionState.Idle;
node.ExecutionProgressPercent = 0;
}
}
private void RaiseEditCommandsCanExecuteChanged()
{
InsertReferencePointCommand.RaiseCanExecuteChanged();
InsertSaveNodeWithImageCommand.RaiseCanExecuteChanged();
InsertSaveNodeCommand.RaiseCanExecuteChanged();
InsertSavePositionCommand.RaiseCanExecuteChanged();
InsertInspectionModuleCommand.RaiseCanExecuteChanged();
InsertInspectionMarkerCommand.RaiseCanExecuteChanged();
InsertPauseDialogCommand.RaiseCanExecuteChanged();
InsertWaitDelayCommand.RaiseCanExecuteChanged();
InsertCompleteProgramCommand.RaiseCanExecuteChanged();
DeleteNodeCommand.RaiseCanExecuteChanged();
MoveNodeUpCommand.RaiseCanExecuteChanged();
MoveNodeDownCommand.RaiseCanExecuteChanged();
SaveProgramCommand.RaiseCanExecuteChanged();
NewProgramCommand.RaiseCanExecuteChanged();
RunCncCommand.RaiseCanExecuteChanged();
StopCncCommand.RaiseCanExecuteChanged();
}
private void OnRoleChanged(RoleChangedPayload payload)
{
RaisePropertyChanged(nameof(CanEditCncProgram));
RaisePropertyChanged(nameof(CanEditInspectionModule));
RaiseEditCommandsCanExecuteChanged();
}
private void OnProgramEdited()
{
IsModified = true;
RefreshNodes();
PublishProgramChanged();
}
private void HandleNodeModelChanged(CncNodeViewModel nodeVm, CncNode updatedNode)
{
if (_currentProgram == null)
return;
_logger.Debug("[CNC-ROI][HandleNodeModelChanged] 节点={Name}(idx={Idx})updatedNode类型={Type},调用栈:{Stack}",
nodeVm.Name, nodeVm.Index, updatedNode.GetType().Name,
new System.Diagnostics.StackTrace(1, false).ToString().Split('\n')[0].Trim());
_currentProgram = _cncProgramService.UpdateNode(_currentProgram, nodeVm.Index, updatedNode);
IsModified = true;
ProgramName = _currentProgram.Name;
PublishProgramChanged();
}
private void RefreshNodes()
{
_logger.Debug("[CNC-ROI][RefreshNodes] 触发,调用栈:{Stack}",
new System.Diagnostics.StackTrace(1, false).ToString().Split('\n')[0].Trim());
NormalizeDefaultNodeNamesInCurrentProgram();
var selectedId = _preferredSelectedNodeId ?? SelectedNode?.Id;
var expansionState = Nodes.ToDictionary(node => node.Id, node => node.IsExpanded);
var flatNodes = new List<CncNodeViewModel>();
var rootNodes = new List<CncNodeViewModel>();
CncNodeViewModel currentSavePosition = null;
if (_currentProgram?.Nodes != null)
{
foreach (var node in _currentProgram.Nodes)
{
var vm = new CncNodeViewModel(node, HandleNodeModelChanged)
{
IsExpanded = expansionState.TryGetValue(node.Id, out var isExpanded) ? isExpanded : true
};
// 为检测模块节点填充流水线步骤(三级树节点)
if (node is InspectionModuleNode imNode)
{
vm.SyncPipelineSteps(imNode.Pipeline, _imageProcessingService);
}
flatNodes.Add(vm);
if (vm.IsSavePosition)
{
rootNodes.Add(vm);
currentSavePosition = vm;
continue;
}
if (currentSavePosition != null && IsSavePositionChild(vm.NodeType))
{
currentSavePosition.Children.Add(vm);
continue;
}
rootNodes.Add(vm);
currentSavePosition = null;
}
}
Nodes = new ObservableCollection<CncNodeViewModel>(flatNodes);
TreeNodes = new ObservableCollection<CncNodeViewModel>(rootNodes);
ProgramTreeRoots = new ObservableCollection<CncProgramTreeRootViewModel>
{
new CncProgramTreeRootViewModel(ProgramDisplayName, TreeNodes)
};
SelectedNode = selectedId.HasValue
? Nodes.FirstOrDefault(node => node.Id == selectedId.Value) ?? Nodes.LastOrDefault()
: Nodes.LastOrDefault();
_preferredSelectedNodeId = null;
RaiseEditCommandsCanExecuteChanged();
}
private void NormalizeDefaultNodeNamesInCurrentProgram()
{
if (_currentProgram?.Nodes == null || _currentProgram.Nodes.Count == 0)
{
return;
}
var normalizedNodes = ApplyDefaultNodeNames(_currentProgram.Nodes);
bool changed = false;
for (int i = 0; i < normalizedNodes.Count; i++)
{
if (!Equals(normalizedNodes[i], _currentProgram.Nodes[i]))
{
changed = true;
break;
}
}
if (changed)
{
_currentProgram = _currentProgram with { Nodes = normalizedNodes };
}
}
private int ResolveInsertAfterIndex(CncNodeType nodeType)
{
if (_currentProgram == null || _currentProgram.Nodes.Count == 0)
{
return -1;
}
if (TryResolvePendingInsertAfterIndex(nodeType, out int pendingAfterIndex))
{
return pendingAfterIndex;
}
if (!IsSavePositionChild(nodeType))
{
int? currentSavePositionIndex = FindOwningSavePositionIndex(SelectedNode?.Index);
if (currentSavePositionIndex.HasValue)
{
return GetSavePositionBlockEndIndex(currentSavePositionIndex.Value);
}
return SelectedNode?.Index ?? _currentProgram.Nodes.Count - 1;
}
int? savePositionIndex = FindOwningSavePositionIndex(SelectedNode?.Index);
if (!savePositionIndex.HasValue)
{
throw new InvalidOperationException("请先选择一个“保存位置”节点,再插入标记点或检测模块。");
}
return GetSavePositionBlockEndIndex(savePositionIndex.Value);
}
private void SetPendingInsertAnchor(CncNodeViewModel nodeVm, bool insertAfter)
{
if (_currentProgram == null || nodeVm == null)
{
return;
}
_pendingInsertAnchorNodeId = nodeVm.Id;
_pendingInsertAfterAnchor = insertAfter;
SelectedNode = nodeVm;
}
private bool TryResolvePendingInsertAfterIndex(CncNodeType nodeType, out int afterIndex)
{
afterIndex = -1;
if (!_pendingInsertAnchorNodeId.HasValue || _currentProgram == null || IsSavePositionChild(nodeType))
{
return false;
}
int anchorIndex = FindNodeIndexById(_pendingInsertAnchorNodeId.Value);
if (anchorIndex < 0)
{
ClearPendingInsertAnchor();
return false;
}
afterIndex = _pendingInsertAfterAnchor ? anchorIndex : anchorIndex - 1;
return true;
}
private int FindNodeIndexById(Guid nodeId)
{
if (_currentProgram?.Nodes == null)
{
return -1;
}
for (int i = 0; i < _currentProgram.Nodes.Count; i++)
{
if (_currentProgram.Nodes[i].Id == nodeId)
{
return i;
}
}
return -1;
}
private void ClearPendingInsertAnchor()
{
_pendingInsertAnchorNodeId = null;
_pendingInsertAfterAnchor = false;
}
private void ClearPendingInsertAnchorIfMissing()
{
if (!_pendingInsertAnchorNodeId.HasValue)
{
return;
}
if (FindNodeIndexById(_pendingInsertAnchorNodeId.Value) < 0)
{
ClearPendingInsertAnchor();
}
}
private void MoveSavePositionChild(CncNodeViewModel nodeVm, bool moveDown)
{
int? parentIndex = FindOwningSavePositionIndex(nodeVm.Index);
if (!parentIndex.HasValue)
{
throw new InvalidOperationException("当前子节点未归属于任何保存位置,无法移动。");
}
int childStartIndex = parentIndex.Value + 1;
int childEndIndex = GetSavePositionBlockEndIndex(parentIndex.Value);
int targetIndex = moveDown ? nodeVm.Index + 1 : nodeVm.Index - 1;
if (targetIndex < childStartIndex || targetIndex > childEndIndex)
{
return;
}
_currentProgram = _cncProgramService.MoveNode(_currentProgram, nodeVm.Index, targetIndex);
_preferredSelectedNodeId = nodeVm.Id;
}
private void MoveRootBlock(CncNodeViewModel nodeVm, bool moveDown)
{
var blocks = BuildRootBlocks(_currentProgram.Nodes);
int blockIndex = blocks.FindIndex(block => block.Start == nodeVm.Index);
if (blockIndex < 0)
{
return;
}
if (!moveDown && blockIndex == 0)
{
return;
}
if (moveDown && blockIndex >= blocks.Count - 1)
{
return;
}
var currentBlock = blocks[blockIndex];
var targetBlock = blocks[moveDown ? blockIndex + 1 : blockIndex - 1];
var nodes = _currentProgram.Nodes.ToList();
var movingNodes = nodes.GetRange(currentBlock.Start, currentBlock.End - currentBlock.Start + 1);
nodes.RemoveRange(currentBlock.Start, movingNodes.Count);
int insertAt = moveDown
? targetBlock.End - movingNodes.Count + 1
: targetBlock.Start;
nodes.InsertRange(insertAt, movingNodes);
_currentProgram = ReplaceProgramNodes(nodes);
_preferredSelectedNodeId = nodeVm.Id;
}
private int? FindOwningSavePositionIndex(int? startIndex)
{
if (!startIndex.HasValue || _currentProgram == null)
{
return null;
}
int index = startIndex.Value;
if (index < 0 || index >= _currentProgram.Nodes.Count)
{
return null;
}
var selectedType = _currentProgram.Nodes[index].NodeType;
if (_currentProgram.Nodes[index].NodeType == CncNodeType.SavePosition)
{
return index;
}
if (!IsSavePositionChild(selectedType))
{
return null;
}
for (int i = index - 1; i >= 0; i--)
{
var type = _currentProgram.Nodes[i].NodeType;
if (type == CncNodeType.SavePosition)
{
return i;
}
if (!IsSavePositionChild(type))
{
break;
}
}
return null;
}
private int GetSavePositionBlockEndIndex(int savePositionIndex)
{
if (_currentProgram == null)
{
return savePositionIndex;
}
int endIndex = savePositionIndex;
for (int i = savePositionIndex + 1; i < _currentProgram.Nodes.Count; i++)
{
if (!IsSavePositionChild(_currentProgram.Nodes[i].NodeType))
{
break;
}
endIndex = i;
}
return endIndex;
}
private static List<(int Start, int End)> BuildRootBlocks(IReadOnlyList<CncNode> nodes)
{
var blocks = new List<(int Start, int End)>();
for (int i = 0; i < nodes.Count; i++)
{
if (IsSavePositionChild(nodes[i].NodeType))
{
continue;
}
int end = i;
if (nodes[i].NodeType == CncNodeType.SavePosition)
{
for (int j = i + 1; j < nodes.Count; j++)
{
if (!IsSavePositionChild(nodes[j].NodeType))
{
break;
}
end = j;
}
}
blocks.Add((i, end));
}
return blocks;
}
private CncProgram ReplaceProgramNodes(List<CncNode> nodes)
{
var renumberedNodes = ApplyDefaultNodeNames(nodes);
return _currentProgram with
{
Nodes = renumberedNodes,
UpdatedAt = DateTime.UtcNow
};
}
private static IReadOnlyList<CncNode> ApplyDefaultNodeNames(IReadOnlyList<CncNode> nodes)
{
var result = new List<CncNode>(nodes.Count);
int referencePointNumber = 0;
int savePositionNumber = 0;
int inspectionModuleNumber = 0;
for (int i = 0; i < nodes.Count; i++)
{
var indexedNode = nodes[i] with { Index = i };
result.Add(indexedNode switch
{
ReferencePointNode referencePointNode => referencePointNode with { Name = $"\u53C2\u8003\u70B9_{referencePointNumber++}" },
// 保存位置:用户自定义名称时保留,否则用"位置N"1-based
SavePositionNode savePositionNode => IsDefaultSavePositionName(savePositionNode.Name)
? savePositionNode with { Name = $"\u4F4D\u7F6E{++savePositionNumber}" }
: (CncNode)(savePositionNode with { Index = i }),
// 检测模块:用户自定义名称时保留,否则用"模块N"1-based
InspectionModuleNode inspectionModuleNode => IsDefaultInspectionModuleName(inspectionModuleNode.Name)
? inspectionModuleNode with { Name = $"\u6A21\u5757{++inspectionModuleNumber}" }
: (CncNode)(inspectionModuleNode with { Index = i }),
_ => indexedNode
});
// 无论是否重命名,计数器都要递增以保持后续编号连续
if (indexedNode is SavePositionNode sp && !IsDefaultSavePositionName(sp.Name))
savePositionNumber++;
if (indexedNode is InspectionModuleNode im && !IsDefaultInspectionModuleName(im.Name))
inspectionModuleNumber++;
}
return result.AsReadOnly();
}
/// <summary>判断是否为系统默认的保存位置名称("位置N" 格式)</summary>
private static bool IsDefaultSavePositionName(string name)
{
if (string.IsNullOrWhiteSpace(name)) return true;
if (name.StartsWith("\u4F4D\u7F6E", StringComparison.Ordinal))
return int.TryParse(name[2..], out _);
// 兼容旧格式 "保存位置_N"
if (name.StartsWith("\u4FDD\u5B58\u4F4D\u7F6E_", StringComparison.Ordinal))
return true;
return false;
}
/// <summary>判断是否为系统默认的检测模块名称("模块N" 或旧格式 "检测模块_N"</summary>
private static bool IsDefaultInspectionModuleName(string name)
{
if (string.IsNullOrWhiteSpace(name)) return true;
if (name.StartsWith("\u6A21\u5757", StringComparison.Ordinal))
return int.TryParse(name[2..], out _);
// 兼容旧格式 "检测模块_N"
if (name.StartsWith("\u68C0\u6D4B\u6A21\u5757_", StringComparison.Ordinal))
return true;
return false;
}
private static bool IsSavePositionChild(CncNodeType type)
{
return type is CncNodeType.InspectionMarker
or CncNodeType.InspectionModule;
}
private void PublishProgramChanged()
{
_eventAggregator
.GetEvent<CncProgramChangedEvent>()
.Publish(new CncProgramChangedPayload(ProgramName ?? string.Empty, IsModified));
}
private static string FormatProgramDisplayName(string programName)
{
var name = string.IsNullOrWhiteSpace(programName) ? "新建检测程序" : programName;
return name.EndsWith(".xp", StringComparison.OrdinalIgnoreCase) ? name : $"{name}.xp";
}
}
}