diff --git a/XplorePlane/Services/Cnc/CncProgramService.cs b/XplorePlane/Services/Cnc/CncProgramService.cs index eabad59..f4fa478 100644 --- a/XplorePlane/Services/Cnc/CncProgramService.cs +++ b/XplorePlane/Services/Cnc/CncProgramService.cs @@ -205,6 +205,32 @@ namespace XplorePlane.Services.Cnc return updated; } + /// + public CncProgram UpdateNode(CncProgram program, int index, CncNode node) + { + ArgumentNullException.ThrowIfNull(program); + ArgumentNullException.ThrowIfNull(node); + + if (index < 0 || index >= program.Nodes.Count) + throw new ArgumentOutOfRangeException(nameof(index), + $"Index out of range: {index}, Count={program.Nodes.Count}"); + + var nodes = new List(program.Nodes) + { + [index] = node with { Index = index } + }; + + var updated = program with + { + Nodes = nodes.AsReadOnly(), + UpdatedAt = DateTime.UtcNow + }; + + _logger.Info("Updated node: Index={Index}, Type={NodeType}, Program={ProgramName}", + index, node.NodeType, program.Name); + return updated; + } + /// public async Task SaveAsync(CncProgram program, string filePath) { diff --git a/XplorePlane/Services/Cnc/ICncProgramService.cs b/XplorePlane/Services/Cnc/ICncProgramService.cs index 8429f96..db54c03 100644 --- a/XplorePlane/Services/Cnc/ICncProgramService.cs +++ b/XplorePlane/Services/Cnc/ICncProgramService.cs @@ -4,36 +4,28 @@ using XplorePlane.Models; namespace XplorePlane.Services.Cnc { /// - /// CNC 程序管理服务接口,负责程序的创建、节点编辑、序列化/反序列化和文件读写 - /// CNC program management service interface for creation, node editing, serialization and file I/O + /// CNC program management service interface. /// public interface ICncProgramService { - /// 创建空的 CNC 程序 | Create an empty CNC program CncProgram CreateProgram(string name); - /// 根据节点类型创建节点(从 IAppStateService 捕获设备状态)| Create a node by type (captures device state from IAppStateService) CncNode CreateNode(CncNodeType type); - /// 在指定索引之后插入节点并重新编号 | Insert a node after the given index and renumber CncProgram InsertNode(CncProgram program, int afterIndex, CncNode node); - /// 移除指定索引的节点并重新编号 | Remove the node at the given index and renumber CncProgram RemoveNode(CncProgram program, int index); - /// 将节点从旧索引移动到新索引并重新编号 | Move a node from old index to new index and renumber CncProgram MoveNode(CncProgram program, int oldIndex, int newIndex); - /// 将 CNC 程序保存到 .xp 文件 | Save CNC program to .xp file + CncProgram UpdateNode(CncProgram program, int index, CncNode node); + Task SaveAsync(CncProgram program, string filePath); - /// 从 .xp 文件加载 CNC 程序 | Load CNC program from .xp file Task LoadAsync(string filePath); - /// 将 CNC 程序序列化为 JSON 字符串 | Serialize CNC program to JSON string string Serialize(CncProgram program); - /// 从 JSON 字符串反序列化 CNC 程序 | Deserialize CNC program from JSON string CncProgram Deserialize(string json); } -} \ No newline at end of file +} diff --git a/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs b/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs index c1cdc9c..0b3590f 100644 --- a/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs +++ b/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs @@ -3,6 +3,7 @@ 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; @@ -18,27 +19,21 @@ using XplorePlane.Services.Cnc; namespace XplorePlane.ViewModels.Cnc { /// - /// CNC 编辑器 ViewModel,管理 CNC 程序的节点列表、编辑操作和文件操作 - /// CNC editor ViewModel that manages the node list, editing operations and file operations + /// CNC editor ViewModel that manages the node tree, editing operations and file operations. /// 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 _nodes; + private ObservableCollection _treeNodes; private CncNodeViewModel _selectedNode; private bool _isModified; private string _programName; - /// - /// 构造函数 | Constructor - /// public CncEditorViewModel( ICncProgramService cncProgramService, IAppStateService appStateService, @@ -46,13 +41,13 @@ namespace XplorePlane.ViewModels.Cnc ILoggerService logger) { _cncProgramService = cncProgramService ?? throw new ArgumentNullException(nameof(cncProgramService)); - _appStateService = appStateService ?? throw new ArgumentNullException(nameof(appStateService)); + ArgumentNullException.ThrowIfNull(appStateService); _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); _logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule(); _nodes = new ObservableCollection(); + _treeNodes = new ObservableCollection(); - // ── 节点插入命令 | Node insertion commands ── InsertReferencePointCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.ReferencePoint)); InsertSaveNodeWithImageCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNodeWithImage)); InsertSaveNodeCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNode)); @@ -63,116 +58,78 @@ namespace XplorePlane.ViewModels.Cnc 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(ExecuteMoveNodeUp); MoveNodeDownCommand = new DelegateCommand(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"); + _logger.Info("CncEditorViewModel initialized"); } - // ── 属性 | Properties ────────────────────────────────────────── - - /// 节点列表 | Node list public ObservableCollection Nodes { get => _nodes; - set => SetProperty(ref _nodes, value); + private set => SetProperty(ref _nodes, value); + } + + public ObservableCollection TreeNodes + { + get => _treeNodes; + private set => SetProperty(ref _treeNodes, value); } - /// 当前选中的节点 | Currently selected node public CncNodeViewModel SelectedNode { get => _selectedNode; - set => SetProperty(ref _selectedNode, value); + set + { + if (SetProperty(ref _selectedNode, value)) + { + RaisePropertyChanged(nameof(HasSelection)); + } + } } - /// 程序是否已修改 | Whether the program has been modified + public bool HasSelection => SelectedNode != null; + public bool IsModified { get => _isModified; set => SetProperty(ref _isModified, value); } - /// 当前程序名称 | Current program name public string ProgramName { get => _programName; set => SetProperty(ref _programName, value); } - // ── 节点插入命令 | Node insertion commands ────────────────────── - - /// 插入参考点命令 | Insert reference point command public DelegateCommand InsertReferencePointCommand { get; } - - /// 插入保存节点(含图像)命令 | Insert save node with image command public DelegateCommand InsertSaveNodeWithImageCommand { get; } - - /// 插入保存节点命令 | Insert save node command public DelegateCommand InsertSaveNodeCommand { get; } - - /// 插入保存位置命令 | Insert save position command public DelegateCommand InsertSavePositionCommand { get; } - - /// 插入检测模块命令 | Insert inspection module command public DelegateCommand InsertInspectionModuleCommand { get; } - - /// 插入检测标记命令 | Insert inspection marker command public DelegateCommand InsertInspectionMarkerCommand { get; } - - /// 插入停顿对话框命令 | Insert pause dialog command public DelegateCommand InsertPauseDialogCommand { get; } - - /// 插入等待延时命令 | Insert wait delay command public DelegateCommand InsertWaitDelayCommand { get; } - - /// 插入完成程序命令 | Insert complete program command public DelegateCommand InsertCompleteProgramCommand { get; } - - // ── 节点编辑命令 | Node editing commands ──────────────────────── - - /// 删除选中节点命令 | Delete selected node command public DelegateCommand DeleteNodeCommand { get; } - - /// 上移节点命令 | Move node up command public DelegateCommand MoveNodeUpCommand { get; } - - /// 下移节点命令 | Move node down command public DelegateCommand MoveNodeDownCommand { get; } - - // ── 文件操作命令 | File operation commands ────────────────────── - - /// 保存程序命令 | Save program command public DelegateCommand SaveProgramCommand { get; } - - /// 加载程序命令 | Load program command public DelegateCommand LoadProgramCommand { get; } - - /// 新建程序命令 | New program command public DelegateCommand NewProgramCommand { get; } - - /// 导出 CSV 命令 | Export CSV command public DelegateCommand ExportCsvCommand { get; } - // ── 命令执行方法 | Command execution methods ──────────────────── - - /// - /// 插入指定类型的节点到选中节点之后 - /// Insert a node of the specified type after the selected node - /// private void ExecuteInsertNode(CncNodeType nodeType) { if (_currentProgram == null) { - _logger.Warn("无法插入节点:当前无程序 | Cannot insert node: no current program"); ExecuteNewProgram(); } @@ -183,18 +140,14 @@ namespace XplorePlane.ViewModels.Cnc _currentProgram = _cncProgramService.InsertNode(_currentProgram, afterIndex, node); OnProgramEdited(); - _logger.Info("已插入节点 | Inserted node: Type={NodeType}", nodeType); + _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); + _logger.Warn("Node insertion blocked: {Message}", ex.Message); } } - /// - /// 删除选中节点 | Delete the selected node - /// private void ExecuteDeleteNode() { if (_currentProgram == null || SelectedNode == null) @@ -204,18 +157,14 @@ namespace XplorePlane.ViewModels.Cnc { _currentProgram = _cncProgramService.RemoveNode(_currentProgram, SelectedNode.Index); OnProgramEdited(); - _logger.Info("已删除节点 | Deleted node at index: {Index}", SelectedNode.Index); + _logger.Info("Deleted node at index: {Index}", SelectedNode.Index); } catch (ArgumentOutOfRangeException ex) { - _logger.Warn("删除节点失败 | Delete node failed: {Message}", ex.Message); + _logger.Warn("Delete node failed: {Message}", ex.Message); } } - /// - /// 判断是否可以删除节点(至少保留 1 个节点) - /// Determines whether delete is allowed (at least 1 node must remain) - /// private bool CanExecuteDeleteNode() { return SelectedNode != null @@ -223,9 +172,6 @@ namespace XplorePlane.ViewModels.Cnc && _currentProgram.Nodes.Count > 1; } - /// - /// 上移节点 | Move node up - /// private void ExecuteMoveNodeUp(CncNodeViewModel nodeVm) { if (_currentProgram == null || nodeVm == null || nodeVm.Index <= 0) @@ -235,17 +181,13 @@ namespace XplorePlane.ViewModels.Cnc { _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); + _logger.Warn("Move node up failed: {Message}", ex.Message); } } - /// - /// 下移节点 | Move node down - /// private void ExecuteMoveNodeDown(CncNodeViewModel nodeVm) { if (_currentProgram == null || nodeVm == null || nodeVm.Index >= _currentProgram.Nodes.Count - 1) @@ -255,22 +197,18 @@ namespace XplorePlane.ViewModels.Cnc { _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); + _logger.Warn("Move node down failed: {Message}", ex.Message); } } - /// - /// 保存当前程序到文件 | Save current program to file - /// private async Task ExecuteSaveProgramAsync() { if (_currentProgram == null) { - _logger.Warn("无法保存:当前无程序 | Cannot save: no current program"); + _logger.Warn("Cannot save: no current program"); return; } @@ -289,17 +227,13 @@ namespace XplorePlane.ViewModels.Cnc 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"); + _logger.Error(ex, "Failed to save program"); } } - /// - /// 从文件加载程序 | Load program from file - /// private async Task ExecuteLoadProgramAsync() { try @@ -318,35 +252,27 @@ namespace XplorePlane.ViewModels.Cnc ProgramName = _currentProgram.Name; IsModified = false; RefreshNodes(); - _logger.Info("程序已加载 | Program loaded: {ProgramName}", _currentProgram.Name); } catch (Exception ex) { - _logger.Error(ex, "加载程序失败 | Failed to load program"); + _logger.Error(ex, "Failed to load program"); } } - /// - /// 创建新程序 | Create a new program - /// private void ExecuteNewProgram() { - var name = string.IsNullOrWhiteSpace(ProgramName) ? "新程序" : ProgramName; + var name = string.IsNullOrWhiteSpace(ProgramName) ? "NewCncProgram" : ProgramName; _currentProgram = _cncProgramService.CreateProgram(name); ProgramName = _currentProgram.Name; IsModified = false; RefreshNodes(); - _logger.Info("已创建新程序 | Created new program: {ProgramName}", name); } - /// - /// 导出当前程序为 CSV 文件 | Export current program to CSV file - /// private void ExecuteExportCsv() { if (_currentProgram == null || _currentProgram.Nodes.Count == 0) { - _logger.Warn("无法导出 CSV:当前无程序或节点为空 | Cannot export CSV: no program or empty nodes"); + _logger.Warn("Cannot export CSV: no program or empty nodes"); return; } @@ -364,7 +290,6 @@ namespace XplorePlane.ViewModels.Cnc 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; @@ -374,23 +299,14 @@ namespace XplorePlane.ViewModels.Cnc 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)},,,,,,,,,,,,,,,,,,,," }; @@ -398,18 +314,13 @@ namespace XplorePlane.ViewModels.Cnc } 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"); + _logger.Error(ex, "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; @@ -418,12 +329,6 @@ namespace XplorePlane.ViewModels.Cnc return value; } - // ── 辅助方法 | Helper methods ─────────────────────────────────── - - /// - /// 编辑操作后的统一处理:刷新节点、标记已修改、发布变更事件 - /// Unified post-edit handling: refresh nodes, mark modified, publish change event - /// private void OnProgramEdited() { IsModified = true; @@ -431,34 +336,70 @@ namespace XplorePlane.ViewModels.Cnc PublishProgramChanged(); } - /// - /// 从 _currentProgram.Nodes 重建 Nodes 集合 - /// Rebuild the Nodes collection from _currentProgram.Nodes - /// - private void RefreshNodes() + private void HandleNodeModelChanged(CncNodeViewModel nodeVm, CncNode updatedNode) { - Nodes.Clear(); - - if (_currentProgram?.Nodes == null) + if (_currentProgram == 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(); - } + _currentProgram = _cncProgramService.UpdateNode(_currentProgram, nodeVm.Index, updatedNode); + IsModified = true; + ProgramName = _currentProgram.Name; + PublishProgramChanged(); + } + + private void RefreshNodes() + { + var selectedId = SelectedNode?.Id; + var expansionState = Nodes.ToDictionary(node => node.Id, node => node.IsExpanded); + + var flatNodes = new List(); + var rootNodes = new List(); + CncNodeViewModel currentModule = 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 + }; + + flatNodes.Add(vm); + + if (vm.IsInspectionModule) + { + rootNodes.Add(vm); + currentModule = vm; + continue; + } + + if (currentModule != null && IsModuleChild(vm.NodeType)) + { + currentModule.Children.Add(vm); + continue; + } + + rootNodes.Add(vm); + currentModule = null; + } + } + + Nodes = new ObservableCollection(flatNodes); + TreeNodes = new ObservableCollection(rootNodes); + + SelectedNode = selectedId.HasValue + ? Nodes.FirstOrDefault(node => node.Id == selectedId.Value) ?? Nodes.LastOrDefault() + : Nodes.LastOrDefault(); + } + + private static bool IsModuleChild(CncNodeType type) + { + return type is CncNodeType.InspectionMarker + or CncNodeType.PauseDialog + or CncNodeType.WaitDelay; } - /// - /// 通过 IEventAggregator 发布 CNC 程序变更事件 - /// Publish CNC program changed event via IEventAggregator - /// private void PublishProgramChanged() { _eventAggregator diff --git a/XplorePlane/ViewModels/Cnc/CncNodeViewModel.cs b/XplorePlane/ViewModels/Cnc/CncNodeViewModel.cs index 4fa752b..886ba15 100644 --- a/XplorePlane/ViewModels/Cnc/CncNodeViewModel.cs +++ b/XplorePlane/ViewModels/Cnc/CncNodeViewModel.cs @@ -1,89 +1,530 @@ using Prism.Mvvm; +using System; +using System.Collections.ObjectModel; using XplorePlane.Models; namespace XplorePlane.ViewModels.Cnc { /// - /// CNC 节点 ViewModel,将 CncNode 模型封装为可绑定的 WPF ViewModel - /// CNC node ViewModel that wraps a CncNode model into a bindable WPF ViewModel + /// CNC node ViewModel with editable properties and tree children. /// public class CncNodeViewModel : BindableBase { - private int _index; - private string _name; - private CncNodeType _nodeType; + private readonly Action _modelChangedCallback; + private CncNode _model; private string _icon; + private bool _isExpanded = true; - /// - /// 构造函数,从 CncNode 模型初始化 ViewModel - /// Constructor that initializes the ViewModel from a CncNode model - /// - public CncNodeViewModel(CncNode model) + public CncNodeViewModel(CncNode model, Action modelChangedCallback) { - Model = model; - _index = model.Index; - _name = model.Name; - _nodeType = model.NodeType; + _model = model ?? throw new ArgumentNullException(nameof(model)); + _modelChangedCallback = modelChangedCallback ?? throw new ArgumentNullException(nameof(modelChangedCallback)); _icon = GetIconForNodeType(model.NodeType); + Children = new ObservableCollection(); } - /// 底层 CNC 节点模型(只读)| Underlying CNC node model (read-only) - public CncNode Model { get; } + public ObservableCollection Children { get; } - /// 节点在程序中的索引 | Node index in the program - public int Index - { - get => _index; - set => SetProperty(ref _index, value); - } + public CncNode Model => _model; + + public Guid Id => _model.Id; + + public int Index => _model.Index; - /// 节点显示名称 | Node display name public string Name { - get => _name; - set => SetProperty(ref _name, value); + get => _model.Name; + set => UpdateModel(_model with { Name = value ?? string.Empty }); } - /// 节点类型 | Node type - public CncNodeType NodeType + public CncNodeType NodeType => _model.NodeType; + + public string NodeTypeDisplay => NodeType.ToString(); + + public string Icon { - get => _nodeType; + get => _icon; + private set => SetProperty(ref _icon, value); + } + + public bool IsExpanded + { + get => _isExpanded; + set => SetProperty(ref _isExpanded, value); + } + + public bool HasChildren => Children.Count > 0; + + public bool IsReferencePoint => _model is ReferencePointNode; + public bool IsSaveNode => _model is SaveNodeNode; + public bool IsSaveNodeWithImage => _model is SaveNodeWithImageNode; + public bool IsSavePosition => _model is SavePositionNode; + public bool IsInspectionModule => _model is InspectionModuleNode; + public bool IsInspectionMarker => _model is InspectionMarkerNode; + public bool IsPauseDialog => _model is PauseDialogNode; + public bool IsWaitDelay => _model is WaitDelayNode; + public bool IsCompleteProgram => _model is CompleteProgramNode; + public bool IsMotionSnapshotNode => _model is ReferencePointNode or SaveNodeNode or SaveNodeWithImageNode or SavePositionNode; + + public double XM + { + get => _model switch + { + ReferencePointNode rp => rp.XM, + SaveNodeNode sn => sn.MotionState.XM, + SaveNodeWithImageNode sni => sni.MotionState.XM, + SavePositionNode sp => sp.MotionState.XM, + _ => 0d + }; + set => UpdateMotion(value, MotionAxis.XM); + } + + public double YM + { + get => _model switch + { + ReferencePointNode rp => rp.YM, + SaveNodeNode sn => sn.MotionState.YM, + SaveNodeWithImageNode sni => sni.MotionState.YM, + SavePositionNode sp => sp.MotionState.YM, + _ => 0d + }; + set => UpdateMotion(value, MotionAxis.YM); + } + + public double ZT + { + get => _model switch + { + ReferencePointNode rp => rp.ZT, + SaveNodeNode sn => sn.MotionState.ZT, + SaveNodeWithImageNode sni => sni.MotionState.ZT, + SavePositionNode sp => sp.MotionState.ZT, + _ => 0d + }; + set => UpdateMotion(value, MotionAxis.ZT); + } + + public double ZD + { + get => _model switch + { + ReferencePointNode rp => rp.ZD, + SaveNodeNode sn => sn.MotionState.ZD, + SaveNodeWithImageNode sni => sni.MotionState.ZD, + SavePositionNode sp => sp.MotionState.ZD, + _ => 0d + }; + set => UpdateMotion(value, MotionAxis.ZD); + } + + public double TiltD + { + get => _model switch + { + ReferencePointNode rp => rp.TiltD, + SaveNodeNode sn => sn.MotionState.TiltD, + SaveNodeWithImageNode sni => sni.MotionState.TiltD, + SavePositionNode sp => sp.MotionState.TiltD, + _ => 0d + }; + set => UpdateMotion(value, MotionAxis.TiltD); + } + + public double Dist + { + get => _model switch + { + ReferencePointNode rp => rp.Dist, + SaveNodeNode sn => sn.MotionState.Dist, + SaveNodeWithImageNode sni => sni.MotionState.Dist, + SavePositionNode sp => sp.MotionState.Dist, + _ => 0d + }; + set => UpdateMotion(value, MotionAxis.Dist); + } + + public bool IsRayOn + { + get => _model switch + { + ReferencePointNode rp => rp.IsRayOn, + SaveNodeNode sn => sn.RaySourceState.IsOn, + SaveNodeWithImageNode sni => sni.RaySourceState.IsOn, + _ => false + }; + set => UpdateRaySource(isOn: value); + } + + public double Voltage + { + get => _model switch + { + ReferencePointNode rp => rp.Voltage, + SaveNodeNode sn => sn.RaySourceState.Voltage, + SaveNodeWithImageNode sni => sni.RaySourceState.Voltage, + _ => 0d + }; + set => UpdateRaySource(voltage: value); + } + + public double Current + { + get => _model is ReferencePointNode rp ? rp.Current : 0d; set { - if (SetProperty(ref _nodeType, value)) + if (_model is ReferencePointNode rp) { - // 类型变更时自动更新图标 | Auto-update icon when type changes - Icon = GetIconForNodeType(value); + UpdateModel(rp with { Current = value }); } } } - /// 节点图标路径 | Node icon path - public string Icon + public double Power { - get => _icon; - set => SetProperty(ref _icon, value); + get => _model switch + { + SaveNodeNode sn => sn.RaySourceState.Power, + SaveNodeWithImageNode sni => sni.RaySourceState.Power, + _ => 0d + }; + set => UpdateRaySource(power: value); + } + + public bool DetectorConnected + { + get => _model switch + { + SaveNodeNode sn => sn.DetectorState.IsConnected, + SaveNodeWithImageNode sni => sni.DetectorState.IsConnected, + _ => false + }; + set => UpdateDetector(isConnected: value); + } + + public bool DetectorAcquiring + { + get => _model switch + { + SaveNodeNode sn => sn.DetectorState.IsAcquiring, + SaveNodeWithImageNode sni => sni.DetectorState.IsAcquiring, + _ => false + }; + set => UpdateDetector(isAcquiring: value); + } + + public double FrameRate + { + get => _model switch + { + SaveNodeNode sn => sn.DetectorState.FrameRate, + SaveNodeWithImageNode sni => sni.DetectorState.FrameRate, + _ => 0d + }; + set => UpdateDetector(frameRate: value); + } + + public string Resolution + { + get => _model switch + { + SaveNodeNode sn => sn.DetectorState.Resolution, + SaveNodeWithImageNode sni => sni.DetectorState.Resolution, + _ => string.Empty + }; + set => UpdateDetector(resolution: value ?? string.Empty); + } + + public string ImageFileName + { + get => _model is SaveNodeWithImageNode sni ? sni.ImageFileName : string.Empty; + set + { + if (_model is SaveNodeWithImageNode sni) + { + UpdateModel(sni with { ImageFileName = value ?? string.Empty }); + } + } + } + + public string PipelineName + { + get => _model is InspectionModuleNode im ? im.Pipeline?.Name ?? string.Empty : string.Empty; + set + { + if (_model is InspectionModuleNode im) + { + var pipeline = im.Pipeline ?? new PipelineModel(); + pipeline.Name = value ?? string.Empty; + UpdateModel(im with { Pipeline = pipeline }); + } + } + } + + public string MarkerType + { + get => _model is InspectionMarkerNode mk ? mk.MarkerType : string.Empty; + set + { + if (_model is InspectionMarkerNode mk) + { + UpdateModel(mk with { MarkerType = value ?? string.Empty }); + } + } + } + + public double MarkerX + { + get => _model is InspectionMarkerNode mk ? mk.MarkerX : 0d; + set + { + if (_model is InspectionMarkerNode mk) + { + UpdateModel(mk with { MarkerX = value }); + } + } + } + + public double MarkerY + { + get => _model is InspectionMarkerNode mk ? mk.MarkerY : 0d; + set + { + if (_model is InspectionMarkerNode mk) + { + UpdateModel(mk with { MarkerY = value }); + } + } + } + + public string DialogTitle + { + get => _model is PauseDialogNode pd ? pd.DialogTitle : string.Empty; + set + { + if (_model is PauseDialogNode pd) + { + UpdateModel(pd with { DialogTitle = value ?? string.Empty }); + } + } + } + + public string DialogMessage + { + get => _model is PauseDialogNode pd ? pd.DialogMessage : string.Empty; + set + { + if (_model is PauseDialogNode pd) + { + UpdateModel(pd with { DialogMessage = value ?? string.Empty }); + } + } + } + + public int DelayMilliseconds + { + get => _model is WaitDelayNode wd ? wd.DelayMilliseconds : 0; + set + { + if (_model is WaitDelayNode wd) + { + UpdateModel(wd with { DelayMilliseconds = value }); + } + } + } + + public void ReplaceModel(CncNode model) + { + _model = model ?? throw new ArgumentNullException(nameof(model)); + Icon = GetIconForNodeType(model.NodeType); + RaiseAllPropertiesChanged(); } - /// - /// 根据节点类型返回对应的图标路径 - /// Returns the icon path for the given node type - /// public static string GetIconForNodeType(CncNodeType nodeType) { return nodeType switch { - CncNodeType.ReferencePoint => "/Resources/Icons/cnc_reference_point.png", - CncNodeType.SaveNodeWithImage => "/Resources/Icons/cnc_save_with_image.png", - CncNodeType.SaveNode => "/Resources/Icons/cnc_save_node.png", - CncNodeType.SavePosition => "/Resources/Icons/cnc_save_position.png", - CncNodeType.InspectionModule => "/Resources/Icons/cnc_inspection_module.png", - CncNodeType.InspectionMarker => "/Resources/Icons/cnc_inspection_marker.png", - CncNodeType.PauseDialog => "/Resources/Icons/cnc_pause_dialog.png", - CncNodeType.WaitDelay => "/Resources/Icons/cnc_wait_delay.png", - CncNodeType.CompleteProgram => "/Resources/Icons/cnc_complete_program.png", - _ => "/Resources/Icons/cnc_default.png", + CncNodeType.ReferencePoint => "/Assets/Icons/reference.png", + CncNodeType.SaveNodeWithImage => "/Assets/Icons/saveall.png", + CncNodeType.SaveNode => "/Assets/Icons/save.png", + CncNodeType.SavePosition => "/Assets/Icons/add-pos.png", + CncNodeType.InspectionModule => "/Assets/Icons/Module.png", + CncNodeType.InspectionMarker => "/Assets/Icons/mark.png", + CncNodeType.PauseDialog => "/Assets/Icons/message.png", + CncNodeType.WaitDelay => "/Assets/Icons/wait.png", + CncNodeType.CompleteProgram => "/Assets/Icons/finish.png", + _ => "/Assets/Icons/cnc.png", }; } + + private void UpdateMotion(double value, MotionAxis axis) + { + switch (_model) + { + case ReferencePointNode rp: + UpdateModel(axis switch + { + MotionAxis.XM => rp with { XM = value }, + MotionAxis.YM => rp with { YM = value }, + MotionAxis.ZT => rp with { ZT = value }, + MotionAxis.ZD => rp with { ZD = value }, + MotionAxis.TiltD => rp with { TiltD = value }, + MotionAxis.Dist => rp with { Dist = value }, + _ => rp + }); + break; + case SaveNodeNode sn: + UpdateModel(sn with { MotionState = UpdateMotionState(sn.MotionState, axis, value) }); + break; + case SaveNodeWithImageNode sni: + UpdateModel(sni with { MotionState = UpdateMotionState(sni.MotionState, axis, value) }); + break; + case SavePositionNode sp: + UpdateModel(sp with { MotionState = UpdateMotionState(sp.MotionState, axis, value) }); + break; + } + } + + private void UpdateRaySource(bool? isOn = null, double? voltage = null, double? current = null, double? power = null) + { + switch (_model) + { + case ReferencePointNode rp: + UpdateModel(rp with + { + IsRayOn = isOn ?? rp.IsRayOn, + Voltage = voltage ?? rp.Voltage, + Current = current ?? rp.Current + }); + break; + case SaveNodeNode sn: + UpdateModel(sn with + { + RaySourceState = sn.RaySourceState with + { + IsOn = isOn ?? sn.RaySourceState.IsOn, + Voltage = voltage ?? sn.RaySourceState.Voltage, + Power = power ?? sn.RaySourceState.Power + } + }); + break; + case SaveNodeWithImageNode sni: + UpdateModel(sni with + { + RaySourceState = sni.RaySourceState with + { + IsOn = isOn ?? sni.RaySourceState.IsOn, + Voltage = voltage ?? sni.RaySourceState.Voltage, + Power = power ?? sni.RaySourceState.Power + } + }); + break; + } + } + + private void UpdateDetector(bool? isConnected = null, bool? isAcquiring = null, double? frameRate = null, string resolution = null) + { + switch (_model) + { + case SaveNodeNode sn: + UpdateModel(sn with + { + DetectorState = sn.DetectorState with + { + IsConnected = isConnected ?? sn.DetectorState.IsConnected, + IsAcquiring = isAcquiring ?? sn.DetectorState.IsAcquiring, + FrameRate = frameRate ?? sn.DetectorState.FrameRate, + Resolution = resolution ?? sn.DetectorState.Resolution + } + }); + break; + case SaveNodeWithImageNode sni: + UpdateModel(sni with + { + DetectorState = sni.DetectorState with + { + IsConnected = isConnected ?? sni.DetectorState.IsConnected, + IsAcquiring = isAcquiring ?? sni.DetectorState.IsAcquiring, + FrameRate = frameRate ?? sni.DetectorState.FrameRate, + Resolution = resolution ?? sni.DetectorState.Resolution + } + }); + break; + } + } + + private static MotionState UpdateMotionState(MotionState state, MotionAxis axis, double value) + { + return axis switch + { + MotionAxis.XM => state with { XM = value }, + MotionAxis.YM => state with { YM = value }, + MotionAxis.ZT => state with { ZT = value }, + MotionAxis.ZD => state with { ZD = value }, + MotionAxis.TiltD => state with { TiltD = value }, + MotionAxis.Dist => state with { Dist = value }, + _ => state + }; + } + + private void UpdateModel(CncNode updatedModel) + { + _model = updatedModel ?? throw new ArgumentNullException(nameof(updatedModel)); + RaiseAllPropertiesChanged(); + _modelChangedCallback(this, updatedModel); + } + + private void RaiseAllPropertiesChanged() + { + RaisePropertyChanged(nameof(Model)); + RaisePropertyChanged(nameof(Id)); + RaisePropertyChanged(nameof(Index)); + RaisePropertyChanged(nameof(Name)); + RaisePropertyChanged(nameof(NodeType)); + RaisePropertyChanged(nameof(NodeTypeDisplay)); + RaisePropertyChanged(nameof(Icon)); + RaisePropertyChanged(nameof(IsReferencePoint)); + RaisePropertyChanged(nameof(IsSaveNode)); + RaisePropertyChanged(nameof(IsSaveNodeWithImage)); + RaisePropertyChanged(nameof(IsSavePosition)); + RaisePropertyChanged(nameof(IsInspectionModule)); + RaisePropertyChanged(nameof(IsInspectionMarker)); + RaisePropertyChanged(nameof(IsPauseDialog)); + RaisePropertyChanged(nameof(IsWaitDelay)); + RaisePropertyChanged(nameof(IsCompleteProgram)); + RaisePropertyChanged(nameof(IsMotionSnapshotNode)); + RaisePropertyChanged(nameof(XM)); + RaisePropertyChanged(nameof(YM)); + RaisePropertyChanged(nameof(ZT)); + RaisePropertyChanged(nameof(ZD)); + RaisePropertyChanged(nameof(TiltD)); + RaisePropertyChanged(nameof(Dist)); + RaisePropertyChanged(nameof(IsRayOn)); + RaisePropertyChanged(nameof(Voltage)); + RaisePropertyChanged(nameof(Current)); + RaisePropertyChanged(nameof(Power)); + RaisePropertyChanged(nameof(DetectorConnected)); + RaisePropertyChanged(nameof(DetectorAcquiring)); + RaisePropertyChanged(nameof(FrameRate)); + RaisePropertyChanged(nameof(Resolution)); + RaisePropertyChanged(nameof(ImageFileName)); + RaisePropertyChanged(nameof(PipelineName)); + RaisePropertyChanged(nameof(MarkerType)); + RaisePropertyChanged(nameof(MarkerX)); + RaisePropertyChanged(nameof(MarkerY)); + RaisePropertyChanged(nameof(DialogTitle)); + RaisePropertyChanged(nameof(DialogMessage)); + RaisePropertyChanged(nameof(DelayMilliseconds)); + } + + private enum MotionAxis + { + XM, + YM, + ZT, + ZD, + TiltD, + Dist + } } -} \ No newline at end of file +} diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs index f3a633e..3891463 100644 --- a/XplorePlane/ViewModels/Main/MainViewModel.cs +++ b/XplorePlane/ViewModels/Main/MainViewModel.cs @@ -227,7 +227,7 @@ namespace XplorePlane.ViewModels private void ShowCncEditor() { ImagePanelContent = _cncPageView; - ImagePanelWidth = new GridLength(430); + ImagePanelWidth = new GridLength(720); _isCncEditorMode = true; _logger.Info("CNC 编辑器已切换到主界面图像区域"); } diff --git a/XplorePlane/Views/Cnc/CncPageView.xaml b/XplorePlane/Views/Cnc/CncPageView.xaml index 10a3fd8..db3b2f1 100644 --- a/XplorePlane/Views/Cnc/CncPageView.xaml +++ b/XplorePlane/Views/Cnc/CncPageView.xaml @@ -1,27 +1,26 @@ - + + + - Microsoft YaHei UI - - + + + + + - - - + + + + + - - - - - - + + + + + - - + + + + + + + - - - + ItemsSource="{Binding TreeNodes}" + SelectedItemChanged="CncTreeView_SelectedItemChanged"> + + + - + @@ -121,7 +147,7 @@ Background="Transparent" BorderBrush="#cdcbcb" BorderThickness="1" - Command="{Binding DataContext.MoveNodeUpCommand, RelativeSource={RelativeSource AncestorType=ListBox}}" + Command="{Binding DataContext.MoveNodeUpCommand, RelativeSource={RelativeSource AncestorType=TreeView}}" CommandParameter="{Binding}" Content="▲" Cursor="Hand" @@ -134,7 +160,7 @@ Background="Transparent" BorderBrush="#cdcbcb" BorderThickness="1" - Command="{Binding DataContext.MoveNodeDownCommand, RelativeSource={RelativeSource AncestorType=ListBox}}" + Command="{Binding DataContext.MoveNodeDownCommand, RelativeSource={RelativeSource AncestorType=TreeView}}" CommandParameter="{Binding}" Content="▼" Cursor="Hand" @@ -147,7 +173,7 @@ Background="Transparent" BorderBrush="#E05050" BorderThickness="1" - Command="{Binding DataContext.DeleteNodeCommand, RelativeSource={RelativeSource AncestorType=ListBox}}" + Command="{Binding DataContext.DeleteNodeCommand, RelativeSource={RelativeSource AncestorType=TreeView}}" Content="✕" Cursor="Hand" FontSize="10" @@ -159,17 +185,225 @@ - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/XplorePlane/Views/Cnc/CncPageView.xaml.cs b/XplorePlane/Views/Cnc/CncPageView.xaml.cs index 172345e..af635de 100644 --- a/XplorePlane/Views/Cnc/CncPageView.xaml.cs +++ b/XplorePlane/Views/Cnc/CncPageView.xaml.cs @@ -1,7 +1,9 @@ -using System; +using Prism.Ioc; +using System; +using System.Globalization; using System.Windows; using System.Windows.Controls; -using Prism.Ioc; +using System.Windows.Data; using XplorePlane.ViewModels.Cnc; namespace XplorePlane.Views.Cnc @@ -31,5 +33,33 @@ namespace XplorePlane.Views.Cnc } } } + + private void CncTreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs e) + { + if (DataContext is CncEditorViewModel viewModel) + { + viewModel.SelectedNode = e.NewValue as CncNodeViewModel; + } + } + } + + public class NullToVisibilityConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + var invert = string.Equals(parameter as string, "Invert", StringComparison.OrdinalIgnoreCase); + var isVisible = value != null; + if (invert) + { + isVisible = !isVisible; + } + + return isVisible ? Visibility.Visible : Visibility.Collapsed; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } } }