Files
XplorePlane/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs
T
2026-04-22 01:44:52 +08:00

627 lines
24 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.Tasks;
using XP.Common.Logging.Interfaces;
using XplorePlane.Events;
using XplorePlane.Models;
using XplorePlane.Services.AppState;
using XplorePlane.Services.Cnc;
namespace XplorePlane.ViewModels.Cnc
{
/// <summary>
/// CNC 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 CncProgram _currentProgram;
private ObservableCollection<CncNodeViewModel> _nodes;
private ObservableCollection<CncNodeViewModel> _treeNodes;
private CncNodeViewModel _selectedNode;
private bool _isModified;
private string _programName;
private Guid? _preferredSelectedNodeId;
public CncEditorViewModel(
ICncProgramService cncProgramService,
IAppStateService appStateService,
IEventAggregator eventAggregator,
ILoggerService logger)
{
_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>();
_nodes = new ObservableCollection<CncNodeViewModel>();
_treeNodes = new ObservableCollection<CncNodeViewModel>();
InsertReferencePointCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.ReferencePoint));
InsertSaveNodeWithImageCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNodeWithImage));
InsertSaveNodeCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNode));
InsertSavePositionCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SavePosition));
InsertInspectionModuleCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.InspectionModule));
InsertInspectionMarkerCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.InspectionMarker));
InsertPauseDialogCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.PauseDialog));
InsertWaitDelayCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.WaitDelay));
InsertCompleteProgramCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.CompleteProgram));
DeleteNodeCommand = new DelegateCommand(ExecuteDeleteNode, CanExecuteDeleteNode)
.ObservesProperty(() => SelectedNode);
MoveNodeUpCommand = new DelegateCommand<CncNodeViewModel>(ExecuteMoveNodeUp);
MoveNodeDownCommand = new DelegateCommand<CncNodeViewModel>(ExecuteMoveNodeDown);
SaveProgramCommand = new DelegateCommand(async () => await ExecuteSaveProgramAsync());
LoadProgramCommand = new DelegateCommand(async () => await ExecuteLoadProgramAsync());
NewProgramCommand = new DelegateCommand(ExecuteNewProgram);
ExportCsvCommand = new DelegateCommand(ExecuteExportCsv);
_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 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 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 SaveProgramCommand { get; }
public DelegateCommand LoadProgramCommand { get; }
public DelegateCommand NewProgramCommand { get; }
public DelegateCommand ExportCsvCommand { get; }
private void ExecuteInsertNode(CncNodeType nodeType)
{
if (_currentProgram == null)
{
ExecuteNewProgram();
}
try
{
var node = _cncProgramService.CreateNode(nodeType);
int afterIndex = ResolveInsertAfterIndex(nodeType);
_currentProgram = _cncProgramService.InsertNode(_currentProgram, afterIndex, node);
_preferredSelectedNodeId = node.Id;
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
{
if (SelectedNode.IsSavePosition)
{
var nodes = _currentProgram.Nodes.ToList();
int startIndex = SelectedNode.Index;
int endIndex = GetSavePositionBlockEndIndex(startIndex);
nodes.RemoveRange(startIndex, endIndex - startIndex + 1);
_currentProgram = ReplaceProgramNodes(nodes);
}
else
{
_currentProgram = _cncProgramService.RemoveNode(_currentProgram, SelectedNode.Index);
}
OnProgramEdited();
_logger.Info("Deleted node at index: {Index}", SelectedNode.Index);
}
catch (ArgumentOutOfRangeException ex)
{
_logger.Warn("Delete node failed: {Message}", ex.Message);
}
}
private bool CanExecuteDeleteNode()
{
return SelectedNode != null
&& _currentProgram != null
&& _currentProgram.Nodes.Count > 1;
}
private void ExecuteMoveNodeUp(CncNodeViewModel nodeVm)
{
if (_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 (_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);
}
}
private async Task ExecuteSaveProgramAsync()
{
if (_currentProgram == null)
{
_logger.Warn("Cannot save: no current program");
return;
}
try
{
var dlg = new SaveFileDialog
{
Title = "保存 CNC 程序",
Filter = "CNC 程序文件 (*.xp)|*.xp|所有文件 (*.*)|*.*",
DefaultExt = ".xp",
FileName = _currentProgram.Name
};
if (dlg.ShowDialog() != true)
return;
await _cncProgramService.SaveAsync(_currentProgram, dlg.FileName);
IsModified = false;
}
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"
};
if (dlg.ShowDialog() != true)
return;
_currentProgram = await _cncProgramService.LoadAsync(dlg.FileName);
ProgramName = _currentProgram.Name;
IsModified = false;
RefreshNodes();
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to load program");
}
}
private void ExecuteNewProgram()
{
var name = string.IsNullOrWhiteSpace(ProgramName) ? "NewCncProgram" : ProgramName;
_currentProgram = _cncProgramService.CreateProgram(name);
ProgramName = _currentProgram.Name;
IsModified = false;
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,XM,YM,ZT,ZD,TiltD,Dist,Voltage_kV,Current_uA,Power_W,RayOn,DetectorConnected,FrameRate,Resolution,ImageFile,MarkerType,MarkerX,MarkerY,DialogTitle,DialogMessage,DelayMs,Pipeline");
var inv = CultureInfo.InvariantCulture;
foreach (var node in _currentProgram.Nodes)
{
var row = node switch
{
ReferencePointNode rp => $"{rp.Index},{rp.NodeType},{Esc(rp.Name)},{rp.XM.ToString(inv)},{rp.YM.ToString(inv)},{rp.ZT.ToString(inv)},{rp.ZD.ToString(inv)},{rp.TiltD.ToString(inv)},{rp.Dist.ToString(inv)},{rp.Voltage.ToString(inv)},{rp.Current.ToString(inv)},,{rp.IsRayOn},,,,,,,,,,",
SaveNodeWithImageNode sni => $"{sni.Index},{sni.NodeType},{Esc(sni.Name)},{sni.MotionState.XM.ToString(inv)},{sni.MotionState.YM.ToString(inv)},{sni.MotionState.ZT.ToString(inv)},{sni.MotionState.ZD.ToString(inv)},{sni.MotionState.TiltD.ToString(inv)},{sni.MotionState.Dist.ToString(inv)},{sni.RaySourceState.Voltage.ToString(inv)},,{sni.RaySourceState.Power.ToString(inv)},{sni.RaySourceState.IsOn},{sni.DetectorState.IsConnected},{sni.DetectorState.FrameRate.ToString(inv)},{Esc(sni.DetectorState.Resolution)},{Esc(sni.ImageFileName)},,,,,,",
SaveNodeNode sn => $"{sn.Index},{sn.NodeType},{Esc(sn.Name)},{sn.MotionState.XM.ToString(inv)},{sn.MotionState.YM.ToString(inv)},{sn.MotionState.ZT.ToString(inv)},{sn.MotionState.ZD.ToString(inv)},{sn.MotionState.TiltD.ToString(inv)},{sn.MotionState.Dist.ToString(inv)},{sn.RaySourceState.Voltage.ToString(inv)},,{sn.RaySourceState.Power.ToString(inv)},{sn.RaySourceState.IsOn},{sn.DetectorState.IsConnected},{sn.DetectorState.FrameRate.ToString(inv)},{Esc(sn.DetectorState.Resolution)},,,,,,,",
SavePositionNode sp => $"{sp.Index},{sp.NodeType},{Esc(sp.Name)},{sp.MotionState.XM.ToString(inv)},{sp.MotionState.YM.ToString(inv)},{sp.MotionState.ZT.ToString(inv)},{sp.MotionState.ZD.ToString(inv)},{sp.MotionState.TiltD.ToString(inv)},{sp.MotionState.Dist.ToString(inv)},,,,,,,,,,,,,,",
InspectionModuleNode im => $"{im.Index},{im.NodeType},{Esc(im.Name)},,,,,,,,,,,,,,,,,,,{Esc(im.Pipeline?.Name ?? string.Empty)}",
InspectionMarkerNode mk => $"{mk.Index},{mk.NodeType},{Esc(mk.Name)},,,,,,,,,,,,,,{Esc(mk.MarkerType)},{mk.MarkerX.ToString(inv)},{mk.MarkerY.ToString(inv)},,,",
PauseDialogNode pd => $"{pd.Index},{pd.NodeType},{Esc(pd.Name)},,,,,,,,,,,,,,,,,{Esc(pd.DialogTitle)},{Esc(pd.DialogMessage)},,",
WaitDelayNode wd => $"{wd.Index},{wd.NodeType},{Esc(wd.Name)},,,,,,,,,,,,,,,,,,,{wd.DelayMilliseconds},",
CompleteProgramNode cp => $"{cp.Index},{cp.NodeType},{Esc(cp.Name)},,,,,,,,,,,,,,,,,,,,",
_ => $"{node.Index},{node.NodeType},{Esc(node.Name)},,,,,,,,,,,,,,,,,,,,"
};
sb.AppendLine(row);
}
File.WriteAllText(dlg.FileName, sb.ToString(), Encoding.UTF8);
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to export CSV");
}
}
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 void OnProgramEdited()
{
IsModified = true;
RefreshNodes();
PublishProgramChanged();
}
private void HandleNodeModelChanged(CncNodeViewModel nodeVm, CncNode updatedNode)
{
if (_currentProgram == null)
return;
_currentProgram = _cncProgramService.UpdateNode(_currentProgram, nodeVm.Index, updatedNode);
IsModified = true;
ProgramName = _currentProgram.Name;
PublishProgramChanged();
}
private void RefreshNodes()
{
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
};
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);
SelectedNode = selectedId.HasValue
? Nodes.FirstOrDefault(node => node.Id == selectedId.Value) ?? Nodes.LastOrDefault()
: Nodes.LastOrDefault();
_preferredSelectedNodeId = null;
}
private int ResolveInsertAfterIndex(CncNodeType nodeType)
{
if (_currentProgram == null || _currentProgram.Nodes.Count == 0)
{
return -1;
}
if (!IsSavePositionChild(nodeType))
{
return SelectedNode?.Index ?? _currentProgram.Nodes.Count - 1;
}
int? savePositionIndex = FindOwningSavePositionIndex(SelectedNode?.Index);
if (!savePositionIndex.HasValue)
{
throw new InvalidOperationException("请先选择一个“保存位置”节点,再插入标记点或检测模块。");
}
return GetSavePositionBlockEndIndex(savePositionIndex.Value);
}
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 = nodes
.Select((node, index) => node with { Index = index })
.ToList()
.AsReadOnly();
return _currentProgram with
{
Nodes = renumberedNodes,
UpdatedAt = DateTime.UtcNow
};
}
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));
}
}
}