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 — 附加属性,在禁用控件上显示“当前角色无权访问此功能”提示
1096 lines
44 KiB
C#
1096 lines
44 KiB
C#
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";
|
||
}
|
||
}
|
||
}
|