diff --git a/XplorePlane/App.xaml.cs b/XplorePlane/App.xaml.cs index f2edd10..e7704a0 100644 --- a/XplorePlane/App.xaml.cs +++ b/XplorePlane/App.xaml.cs @@ -632,6 +632,8 @@ namespace XplorePlane containerRegistry.RegisterSingleton(); containerRegistry.RegisterSingleton(); containerRegistry.RegisterSingleton(); + containerRegistry.RegisterSingleton(); + containerRegistry.RegisterSingleton(); // ── 主界面实时图像 / 探测器双队列服务(单例)── containerRegistry.RegisterSingleton(); diff --git a/XplorePlane/Converters/InverseBoolConverter.cs b/XplorePlane/Converters/InverseBoolConverter.cs new file mode 100644 index 0000000..129a0ec --- /dev/null +++ b/XplorePlane/Converters/InverseBoolConverter.cs @@ -0,0 +1,24 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace XplorePlane.Converters +{ + /// + /// 布尔值取反转换器,用于将 true 转为 false、false 转为 true + /// Inverse boolean converter: converts true to false and false to true + /// + [ValueConversion(typeof(bool), typeof(bool))] + public class InverseBoolConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value is bool b ? !b : true; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return value is bool b ? !b : false; + } + } +} diff --git a/XplorePlane/Models/MatrixModels.cs b/XplorePlane/Models/MatrixModels.cs index 79d87a4..ddea94e 100644 --- a/XplorePlane/Models/MatrixModels.cs +++ b/XplorePlane/Models/MatrixModels.cs @@ -42,4 +42,39 @@ namespace XplorePlane.Models string CncProgramPath, IReadOnlyList Cells ); + + // ── 矩阵执行摘要模型(用于 matrix_summary.json 序列化)──────────── + + /// 矩阵执行摘要文件根对象 | Matrix execution summary file root object + public class MatrixSummaryFile + { + public MatrixSummaryConfig Config { get; set; } + public string ProgramName { get; set; } + public string StartTime { get; set; } // ISO 8601 + public double DurationSeconds { get; set; } + public int TotalCells { get; set; } + public int EnabledCells { get; set; } + public int CompletedCells { get; set; } + public int FailedCells { get; set; } + public List Cells { get; set; } + } + + /// 矩阵配置信息 | Matrix configuration + public class MatrixSummaryConfig + { + public int Rows { get; set; } + public int Columns { get; set; } + public double RowSpacing { get; set; } + public double ColumnSpacing { get; set; } + } + + /// 矩阵单元格摘要条目 | Matrix cell summary entry + public class MatrixCellSummaryEntry + { + public string RunId { get; set; } + public int Row { get; set; } + public int Column { get; set; } + public string Status { get; set; } + public bool? Pass { get; set; } + } } \ No newline at end of file diff --git a/XplorePlane/Services/Matrix/IMatrixOrchestrationService.cs b/XplorePlane/Services/Matrix/IMatrixOrchestrationService.cs new file mode 100644 index 0000000..042745c --- /dev/null +++ b/XplorePlane/Services/Matrix/IMatrixOrchestrationService.cs @@ -0,0 +1,25 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using XplorePlane.Models; + +namespace XplorePlane.Services.Matrix +{ + /// + /// 矩阵编排执行服务接口。 + /// 按行优先顺序展开矩阵并执行每个启用单元格。 + /// 对每个单元格:深度克隆模板程序 → 叠加偏移量 → 调用 ICncExecutionService。 + /// + public interface IMatrixOrchestrationService + { + /// + /// 按行优先顺序展开矩阵并执行每个启用单元格。 + /// 对每个单元格:深度克隆模板程序 → 叠加偏移量 → 调用 ICncExecutionService。 + /// + Task ExecuteAsync( + MatrixLayout layout, + CncProgram templateProgram, + IProgress progress, + CancellationToken cancellationToken); + } +} diff --git a/XplorePlane/Services/Matrix/MatrixCellExecutionProgress.cs b/XplorePlane/Services/Matrix/MatrixCellExecutionProgress.cs new file mode 100644 index 0000000..013bd7a --- /dev/null +++ b/XplorePlane/Services/Matrix/MatrixCellExecutionProgress.cs @@ -0,0 +1,16 @@ +using XplorePlane.Models; + +namespace XplorePlane.Services.Matrix +{ + /// + /// 矩阵单元格执行进度报告。 + /// + public record MatrixCellExecutionProgress( + int Row, + int Column, + MatrixCellStatus Status, + int CurrentIndex, // 当前启用单元格序号(从 1 开始) + int TotalEnabled, // 总启用单元格数 + string ErrorMessage = null + ); +} diff --git a/XplorePlane/Services/Matrix/MatrixOrchestrationService.cs b/XplorePlane/Services/Matrix/MatrixOrchestrationService.cs new file mode 100644 index 0000000..95b89cc --- /dev/null +++ b/XplorePlane/Services/Matrix/MatrixOrchestrationService.cs @@ -0,0 +1,261 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using XP.Common.Logging.Interfaces; +using XplorePlane.Models; +using XplorePlane.Services.Cnc; +using XplorePlane.Services.InspectionResults; +using XplorePlane.Services.Storage; + +namespace XplorePlane.Services.Matrix +{ + /// + /// 矩阵编排执行服务实现。 + /// 按行优先顺序展开矩阵并执行每个启用单元格:深度克隆模板程序 → 叠加偏移量 → 调用 ICncExecutionService。 + /// Matrix orchestration execution service implementation. + /// Expands matrix in row-major order and executes each enabled cell: deep clone template → apply offset → call ICncExecutionService. + /// + public class MatrixOrchestrationService : IMatrixOrchestrationService + { + private readonly ICncExecutionService _cncExecutionService; + private readonly IInspectionResultStore _inspectionResultStore; + private readonly MatrixSummaryWriter _matrixSummaryWriter; + private readonly IXpDataPathService _dataPathService; + private readonly ILoggerService _logger; + + public MatrixOrchestrationService( + ICncExecutionService cncExecutionService, + IInspectionResultStore inspectionResultStore, + MatrixSummaryWriter matrixSummaryWriter, + IXpDataPathService dataPathService, + ILoggerService logger) + { + ArgumentNullException.ThrowIfNull(cncExecutionService); + ArgumentNullException.ThrowIfNull(inspectionResultStore); + ArgumentNullException.ThrowIfNull(matrixSummaryWriter); + ArgumentNullException.ThrowIfNull(dataPathService); + ArgumentNullException.ThrowIfNull(logger); + + _cncExecutionService = cncExecutionService; + _inspectionResultStore = inspectionResultStore; + _matrixSummaryWriter = matrixSummaryWriter; + _dataPathService = dataPathService; + _logger = logger.ForModule(); + } + + /// + public async Task ExecuteAsync( + MatrixLayout layout, + CncProgram templateProgram, + IProgress progress, + CancellationToken cancellationToken) + { + // 筛选启用的单元格,按行优先(Row 升序,Column 升序)排列 + var enabledCells = layout.Cells + .Where(c => c.IsEnabled) + .OrderBy(c => c.Row) + .ThenBy(c => c.Column) + .ToList(); + + if (enabledCells.Count == 0) + { + throw new InvalidOperationException("没有已启用的位置,请至少启用一个单元格"); + } + + var totalEnabled = enabledCells.Count; + var startTime = DateTime.UtcNow; + var stopwatch = Stopwatch.StartNew(); + int completedCount = 0; + int failedCount = 0; + var cellEntries = new List(); + + _logger.Info( + "矩阵执行开始 | Matrix execution started: Program={ProgramName}, EnabledCells={EnabledCount}, TotalCells={TotalCells}", + templateProgram.Name, totalEnabled, layout.Rows * layout.Columns); + + for (int i = 0; i < enabledCells.Count; i++) + { + var cell = enabledCells[i]; + var currentIndex = i + 1; + + // 检查取消请求 + cancellationToken.ThrowIfCancellationRequested(); + + // 报告 Executing 状态 + progress?.Report(new MatrixCellExecutionProgress( + cell.Row, + cell.Column, + MatrixCellStatus.Executing, + currentIndex, + totalEnabled)); + + // 生成单元格专用程序 + var cellProgram = ApplyOffset(templateProgram, cell); + + try + { + // 调用 CNC 执行服务(内部已处理 BeginRunAsync 归档) + await _cncExecutionService.ExecuteAsync( + cellProgram, + new Progress(), + cancellationToken).ConfigureAwait(false); + + // 成功完成 + completedCount++; + progress?.Report(new MatrixCellExecutionProgress( + cell.Row, + cell.Column, + MatrixCellStatus.Completed, + currentIndex, + totalEnabled)); + + cellEntries.Add(new MatrixCellSummaryEntry + { + RunId = cellProgram.Id.ToString(), + Row = cell.Row, + Column = cell.Column, + Status = nameof(MatrixCellStatus.Completed), + Pass = true + }); + + _logger.Info( + "单元格执行完成 | Cell completed: R{Row}C{Col} ({Current}/{Total})", + cell.Row, cell.Column, currentIndex, totalEnabled); + } + catch (OperationCanceledException) + { + // 探测器断连或用户取消 — 标记当前单元格为 Error,传播异常退出循环 + failedCount++; + progress?.Report(new MatrixCellExecutionProgress( + cell.Row, + cell.Column, + MatrixCellStatus.Error, + currentIndex, + totalEnabled, + "操作已取消(探测器断连或用户停止)")); + + cellEntries.Add(new MatrixCellSummaryEntry + { + RunId = cellProgram.Id.ToString(), + Row = cell.Row, + Column = cell.Column, + Status = nameof(MatrixCellStatus.Error), + Pass = false + }); + + _logger.Info( + "矩阵执行被取消 | Matrix execution cancelled at R{Row}C{Col} ({Current}/{Total})", + cell.Row, cell.Column, currentIndex, totalEnabled); + + // 写入摘要(即使被取消也尝试写入已有结果) + stopwatch.Stop(); + await WriteSummaryAsync(layout, templateProgram.Name, startTime, cellEntries, cancellationToken: default).ConfigureAwait(false); + LogSummary(templateProgram.Name, totalEnabled, completedCount, failedCount, stopwatch.Elapsed); + + throw; // 传播 OperationCanceledException + } + catch (Exception ex) + { + // 普通异常 — 标记 Error,继续下一个单元格 + failedCount++; + var errorMessage = ex.Message; + + progress?.Report(new MatrixCellExecutionProgress( + cell.Row, + cell.Column, + MatrixCellStatus.Error, + currentIndex, + totalEnabled, + errorMessage)); + + cellEntries.Add(new MatrixCellSummaryEntry + { + RunId = cellProgram.Id.ToString(), + Row = cell.Row, + Column = cell.Column, + Status = nameof(MatrixCellStatus.Error), + Pass = false + }); + + _logger.Error(ex, + "单元格执行失败 | Cell execution failed: R{Row}C{Col} ({Current}/{Total})", + cell.Row, cell.Column, currentIndex, totalEnabled); + } + } + + // 全部完成后写入摘要 + stopwatch.Stop(); + await WriteSummaryAsync(layout, templateProgram.Name, startTime, cellEntries, cancellationToken: default).ConfigureAwait(false); + LogSummary(templateProgram.Name, totalEnabled, completedCount, failedCount, stopwatch.Elapsed); + } + + /// + /// 写入矩阵执行摘要文件。 + /// + private async Task WriteSummaryAsync( + MatrixLayout layout, + string templateProgramName, + DateTime startTime, + IReadOnlyList cellEntries, + CancellationToken cancellationToken) + { + var outputDirectory = System.IO.Path.Combine(_dataPathService.DataPath, "MatrixResults"); + await _matrixSummaryWriter.WriteAsync( + layout, + templateProgramName, + startTime, + cellEntries, + outputDirectory, + cancellationToken).ConfigureAwait(false); + } + + /// + /// 记录汇总日志。 + /// + private void LogSummary(string programName, int totalCells, int completed, int failed, TimeSpan duration) + { + _logger.Info( + "矩阵执行汇总 | Matrix execution summary: Program={ProgramName}, TotalCells={TotalCells}, Completed={Completed}, Failed={Failed}, Duration={Duration:F1}s", + programName, totalCells, completed, failed, duration.TotalSeconds); + } + + /// + /// 将模板程序按单元格偏移量生成该单元格专用的 CNC 程序副本。 + /// 使用 record with 表达式深度克隆,对每个 SavePositionNode 叠加偏移量(μm), + /// 生成新 Id 和新程序名,原模板对象不被修改。 + /// + /// Generates a cell-specific CNC program copy by applying cell offset to the template. + /// Uses record with expression for deep clone, adds offset (μm) to each SavePositionNode, + /// generates new Id and program name. Original template is NOT modified. + /// + internal static CncProgram ApplyOffset(CncProgram template, MatrixCell cell) + { + var offsetXum = cell.OffsetX * 1000.0; + var offsetYum = cell.OffsetY * 1000.0; + + var newNodes = template.Nodes + .Select(node => node is SavePositionNode sp + ? sp with + { + MotionState = sp.MotionState with + { + StageX = sp.MotionState.StageX + offsetXum, + StageY = sp.MotionState.StageY + offsetYum + } + } + : node) + .ToList() + .AsReadOnly(); + + return template with + { + Id = Guid.NewGuid(), + Name = $"{template.Name}_R{cell.Row}C{cell.Column}", + Nodes = newNodes + }; + } + } +} diff --git a/XplorePlane/Services/Matrix/MatrixService.cs b/XplorePlane/Services/Matrix/MatrixService.cs index 310c444..100fe82 100644 --- a/XplorePlane/Services/Matrix/MatrixService.cs +++ b/XplorePlane/Services/Matrix/MatrixService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; @@ -24,6 +25,7 @@ namespace XplorePlane.Services.Matrix { WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, Converters = { new JsonStringEnumConverter() } }; @@ -40,6 +42,8 @@ namespace XplorePlane.Services.Matrix public MatrixLayout CreateLayout(int rows, int columns, double rowSpacing, double columnSpacing) { ValidateDimensions(rows, columns); + ValidateSpacing(rowSpacing); + ValidateSpacing(columnSpacing); var layout = new MatrixLayout( Id: Guid.NewGuid(), @@ -62,6 +66,8 @@ namespace XplorePlane.Services.Matrix { ArgumentNullException.ThrowIfNull(layout); ValidateDimensions(rows, columns); + ValidateSpacing(rowSpacing); + ValidateSpacing(columnSpacing); var updated = layout with { @@ -249,8 +255,24 @@ namespace XplorePlane.Services.Matrix return cells.AsReadOnly(); } + /// + /// 验证间距参数 | Validate spacing parameter + /// Spacing must be in [0.0, 1000.0]. + /// + private static void ValidateSpacing(double spacing) + { + if (spacing < 0) + throw new ArgumentOutOfRangeException(nameof(spacing), + $"间距不能为负数 | Spacing must not be negative: {spacing}"); + + if (spacing > 1000.0) + throw new ArgumentOutOfRangeException(nameof(spacing), + $"间距不能超过 1000mm | Spacing must not exceed 1000mm: {spacing}"); + } + /// /// 验证行列数参数 | Validate row and column dimensions + /// Rows and Columns must be in [1, 50]. /// private static void ValidateDimensions(int rows, int columns) { @@ -258,9 +280,17 @@ namespace XplorePlane.Services.Matrix throw new ArgumentOutOfRangeException(nameof(rows), $"行数必须大于 0 | Rows must be greater than 0: {rows}"); + if (rows > 50) + throw new ArgumentOutOfRangeException(nameof(rows), + $"行数不能超过 50 | Rows must not exceed 50: {rows}"); + if (columns <= 0) throw new ArgumentOutOfRangeException(nameof(columns), $"列数必须大于 0 | Columns must be greater than 0: {columns}"); + + if (columns > 50) + throw new ArgumentOutOfRangeException(nameof(columns), + $"列数不能超过 50 | Columns must not exceed 50: {columns}"); } /// diff --git a/XplorePlane/Services/Matrix/MatrixSummaryWriter.cs b/XplorePlane/Services/Matrix/MatrixSummaryWriter.cs new file mode 100644 index 0000000..69b9861 --- /dev/null +++ b/XplorePlane/Services/Matrix/MatrixSummaryWriter.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using XP.Common.Logging.Interfaces; +using XplorePlane.Models; + +namespace XplorePlane.Services.Matrix +{ + /// + /// 矩阵执行摘要写入器,负责将矩阵执行结果序列化为 matrix_summary.json 文件。 + /// Matrix execution summary writer, responsible for serializing matrix execution results to matrix_summary.json. + /// + public class MatrixSummaryWriter + { + private readonly ILoggerService _logger; + + private static readonly JsonSerializerOptions SummaryJsonOptions = new() + { + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Converters = { new JsonStringEnumConverter() } + }; + + public MatrixSummaryWriter(ILoggerService logger) + { + ArgumentNullException.ThrowIfNull(logger); + _logger = logger.ForModule(); + } + + /// + /// 将矩阵执行摘要写入 JSON 文件。写入失败时记录错误日志,不重新抛出异常。 + /// Write matrix execution summary to JSON file. Logs error on failure without rethrowing. + /// + /// 矩阵布局 | Matrix layout + /// 模板程序名称 | Template program name + /// 执行开始时间 | Execution start time + /// 各单元格摘要条目 | Cell summary entries + /// 输出目录 | Output directory + /// 取消令牌 | Cancellation token + public async Task WriteAsync( + MatrixLayout layout, + string templateProgramName, + DateTime startTime, + IReadOnlyList cellEntries, + string outputDirectory, + CancellationToken ct = default) + { + try + { + var duration = DateTime.UtcNow - startTime; + + var summaryFile = new MatrixSummaryFile + { + Config = new MatrixSummaryConfig + { + Rows = layout.Rows, + Columns = layout.Columns, + RowSpacing = layout.RowSpacing, + ColumnSpacing = layout.ColumnSpacing + }, + ProgramName = templateProgramName, + StartTime = startTime.ToString("o"), + DurationSeconds = Math.Round(duration.TotalSeconds, 2), + TotalCells = layout.Rows * layout.Columns, + EnabledCells = layout.Cells.Count(c => c.IsEnabled), + CompletedCells = cellEntries.Count(e => e.Status == nameof(MatrixCellStatus.Completed)), + FailedCells = cellEntries.Count(e => e.Status == nameof(MatrixCellStatus.Error)), + Cells = cellEntries.ToList() + }; + + var json = JsonSerializer.Serialize(summaryFile, SummaryJsonOptions); + var filePath = Path.Combine(outputDirectory, "matrix_summary.json"); + + Directory.CreateDirectory(outputDirectory); + await File.WriteAllTextAsync(filePath, json, ct).ConfigureAwait(false); + + _logger.Info("已写入矩阵执行摘要 | Matrix summary written: {FilePath}", filePath); + } + catch (Exception ex) + { + _logger.Error(ex, "写入矩阵执行摘要失败 | Failed to write matrix summary to {OutputDirectory}", outputDirectory); + // 不重新抛出,确保不影响已完成的单元格检测结果归档 + } + } + } +} diff --git a/XplorePlane/ViewModels/Cnc/MatrixEditorViewModel.cs b/XplorePlane/ViewModels/Cnc/MatrixEditorViewModel.cs index 46efd6b..6bfe8bb 100644 --- a/XplorePlane/ViewModels/Cnc/MatrixEditorViewModel.cs +++ b/XplorePlane/ViewModels/Cnc/MatrixEditorViewModel.cs @@ -1,10 +1,14 @@ +using Microsoft.Win32; using Prism.Commands; using Prism.Events; using Prism.Mvvm; using System; using System.Collections.ObjectModel; +using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; +using System.Windows; using XP.Common.Logging.Interfaces; using XplorePlane.Models; using XplorePlane.Services.Cnc; @@ -20,12 +24,16 @@ namespace XplorePlane.ViewModels.Cnc { private readonly IMatrixService _matrixService; private readonly ICncProgramService _cncProgramService; + private readonly IMatrixOrchestrationService _orchestrationService; private readonly IEventAggregator _eventAggregator; private readonly ILoggerService _logger; // 当前矩阵布局 | Current matrix layout private MatrixLayout _currentLayout; + // 执行取消令牌源 | Execution cancellation token source + private CancellationTokenSource _executionCts; + private int _rows = 1; private int _columns = 1; private double _rowSpacing; @@ -34,29 +42,51 @@ namespace XplorePlane.ViewModels.Cnc private MatrixCellViewModel _selectedCell; private string _associatedProgramPath; + // ── 验证错误属性 | Validation error properties ── + private string _rowsError; + private string _columnsError; + private string _rowSpacingError; + private string _colSpacingError; + private bool _canUpdateLayout = true; + + // ── 显示与状态属性 | Display and state properties ── + private string _associatedProgramName; + private int _enabledCount; + private int _totalCount; + private bool _isExecuting; + private string _statusText; + /// /// 构造函数 | Constructor /// public MatrixEditorViewModel( IMatrixService matrixService, ICncProgramService cncProgramService, + IMatrixOrchestrationService orchestrationService, IEventAggregator eventAggregator, ILoggerService logger) { _matrixService = matrixService ?? throw new ArgumentNullException(nameof(matrixService)); _cncProgramService = cncProgramService ?? throw new ArgumentNullException(nameof(cncProgramService)); + _orchestrationService = orchestrationService ?? throw new ArgumentNullException(nameof(orchestrationService)); _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); _logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule(); _cells = new ObservableCollection(); // ── 命令初始化 | Command initialization ── - UpdateLayoutCommand = new DelegateCommand(ExecuteUpdateLayout); + UpdateLayoutCommand = new DelegateCommand(ExecuteUpdateLayout, () => CanUpdateLayout) + .ObservesProperty(() => CanUpdateLayout); ToggleCellCommand = new DelegateCommand(ExecuteToggleCell); SelectCellCommand = new DelegateCommand(ExecuteSelectCell); AssociateProgramCommand = new DelegateCommand(async () => await ExecuteAssociateProgramAsync()); SaveLayoutCommand = new DelegateCommand(async () => await ExecuteSaveLayoutAsync()); LoadLayoutCommand = new DelegateCommand(async () => await ExecuteLoadLayoutAsync()); + RunMatrixCommand = new DelegateCommand(async () => await ExecuteRunMatrixAsync(), CanExecuteRunMatrix) + .ObservesProperty(() => IsExecuting) + .ObservesProperty(() => AssociatedProgramPath); + StopCommand = new DelegateCommand(ExecuteStop, () => IsExecuting) + .ObservesProperty(() => IsExecuting); _logger.Info("MatrixEditorViewModel 已初始化 | MatrixEditorViewModel initialized"); } @@ -109,7 +139,89 @@ namespace XplorePlane.ViewModels.Cnc public string AssociatedProgramPath { get => _associatedProgramPath; - set => SetProperty(ref _associatedProgramPath, value); + set + { + if (SetProperty(ref _associatedProgramPath, value)) + { + AssociatedProgramName = string.IsNullOrWhiteSpace(value) + ? null + : Path.GetFileName(value); + } + } + } + + // ── 验证错误属性 | Validation error properties ────────────────── + + /// 行数输入错误提示 | Rows input error message + public string RowsError + { + get => _rowsError; + set => SetProperty(ref _rowsError, value); + } + + /// 列数输入错误提示 | Columns input error message + public string ColumnsError + { + get => _columnsError; + set => SetProperty(ref _columnsError, value); + } + + /// 行间距输入错误提示 | Row spacing input error message + public string RowSpacingError + { + get => _rowSpacingError; + set => SetProperty(ref _rowSpacingError, value); + } + + /// 列间距输入错误提示 | Column spacing input error message + public string ColSpacingError + { + get => _colSpacingError; + set => SetProperty(ref _colSpacingError, value); + } + + /// 是否可以更新布局(输入合法时为 true)| Whether layout can be updated (true when inputs are valid) + public bool CanUpdateLayout + { + get => _canUpdateLayout; + set => SetProperty(ref _canUpdateLayout, value); + } + + // ── 显示与状态属性 | Display and state properties ──────────────── + + /// 关联程序的文件名(不含路径)| Associated program filename (without path) + public string AssociatedProgramName + { + get => _associatedProgramName; + set => SetProperty(ref _associatedProgramName, value); + } + + /// 已启用的单元格数量 | Number of enabled cells + public int EnabledCount + { + get => _enabledCount; + set => SetProperty(ref _enabledCount, value); + } + + /// 单元格总数 | Total number of cells + public int TotalCount + { + get => _totalCount; + set => SetProperty(ref _totalCount, value); + } + + /// 是否正在执行矩阵 | Whether matrix execution is in progress + public bool IsExecuting + { + get => _isExecuting; + set => SetProperty(ref _isExecuting, value); + } + + /// 底部状态栏文本 | Bottom status bar text + public string StatusText + { + get => _statusText; + set => SetProperty(ref _statusText, value); } // ── 命令 | Commands ──────────────────────────────────────────── @@ -132,6 +244,12 @@ namespace XplorePlane.ViewModels.Cnc /// 加载矩阵布局命令 | Load matrix layout command public DelegateCommand LoadLayoutCommand { get; } + /// 运行矩阵执行命令 | Run matrix execution command + public DelegateCommand RunMatrixCommand { get; } + + /// 停止执行命令 | Stop execution command + public DelegateCommand StopCommand { get; } + // ── 命令执行方法 | Command execution methods ──────────────────── /// @@ -140,6 +258,13 @@ namespace XplorePlane.ViewModels.Cnc /// private void ExecuteUpdateLayout() { + // 清除之前的错误 | Clear previous errors + RowsError = null; + ColumnsError = null; + RowSpacingError = null; + ColSpacingError = null; + CanUpdateLayout = true; + try { if (_currentLayout == null) @@ -154,12 +279,56 @@ namespace XplorePlane.ViewModels.Cnc RefreshCells(); _logger.Info("已更新矩阵布局 | Updated matrix layout: Rows={Rows}, Columns={Columns}", Rows, Columns); } + catch (ArgumentOutOfRangeException ex) + { + HandleValidationError(ex); + _logger.Error(ex, "更新矩阵布局失败 | Failed to update matrix layout"); + } catch (Exception ex) { _logger.Error(ex, "更新矩阵布局失败 | Failed to update matrix layout"); } } + /// + /// 处理参数验证异常,设置对应的错误属性 + /// Handle parameter validation exception and set corresponding error properties + /// + private void HandleValidationError(ArgumentOutOfRangeException ex) + { + CanUpdateLayout = false; + var paramName = ex.ParamName?.ToLowerInvariant() ?? string.Empty; + + if (paramName.Contains("row") && paramName.Contains("spacing")) + { + RowSpacingError = "间距不能为负数"; + } + else if (paramName.Contains("col") && paramName.Contains("spacing")) + { + ColSpacingError = "间距不能为负数"; + } + else if (paramName.Contains("spacing")) + { + // Generic spacing error — set both + RowSpacingError = "间距不能为负数"; + ColSpacingError = "间距不能为负数"; + } + else if (paramName.Contains("row")) + { + RowsError = "行数/列数必须在 1 到 50 之间"; + } + else if (paramName.Contains("col")) + { + ColumnsError = "行数/列数必须在 1 到 50 之间"; + } + else + { + // Fallback: try to determine from message or set both + RowsError = "行数/列数必须在 1 到 50 之间"; + ColumnsError = "行数/列数必须在 1 到 50 之间"; + } + } + /// /// 选中指定单元格,更新高亮状态 /// Select the specified cell and update highlight state @@ -199,8 +368,8 @@ namespace XplorePlane.ViewModels.Cnc } /// - /// 关联 CNC 程序到当前矩阵布局(占位:从 AssociatedProgramPath 加载) - /// Associate a CNC program with the current layout (placeholder: loads from AssociatedProgramPath) + /// 关联 CNC 程序:使用 OpenFileDialog 选择 .xp 文件,验证文件有效性 + /// Associate CNC program: use OpenFileDialog to select .xp file, validate file /// private async Task ExecuteAssociateProgramAsync() { @@ -210,26 +379,50 @@ namespace XplorePlane.ViewModels.Cnc return; } - if (string.IsNullOrWhiteSpace(AssociatedProgramPath)) + var dialog = new OpenFileDialog { - _logger.Warn("无法关联程序:程序路径为空 | Cannot associate program: program path is empty"); + Title = "选择 CNC 模板程序", + Filter = "CNC 程序文件 (*.xp)|*.xp", + CheckFileExists = true + }; + + if (dialog.ShowDialog() != true) return; - } + + var filePath = dialog.FileName; + var fileName = Path.GetFileName(filePath); try { - var program = await _cncProgramService.LoadAsync(AssociatedProgramPath); + if (!File.Exists(filePath)) + { + MessageBox.Show($"无法加载程序文件:{fileName}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + return; + } + + var program = await _cncProgramService.LoadAsync(filePath); + + // 验证程序包含至少一个 SavePositionNode + if (!program.Nodes.OfType().Any()) + { + MessageBox.Show("所选程序不包含位置节点,无法用作矩阵模板", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + return; + } + _currentLayout = _matrixService.AssociateCncProgram(_currentLayout, program); - _logger.Info("已关联 CNC 程序 | Associated CNC program: {ProgramPath}", AssociatedProgramPath); + AssociatedProgramPath = filePath; + _logger.Info("已关联 CNC 程序 | Associated CNC program: {ProgramPath}", filePath); } catch (Exception ex) { - _logger.Error(ex, "关联 CNC 程序失败 | Failed to associate CNC program"); + MessageBox.Show($"无法加载程序文件:{fileName}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + _logger.Error(ex, "关联 CNC 程序失败 | Failed to associate CNC program: {FileName}", fileName); } } /// - /// 保存当前矩阵布局到文件 | Save current matrix layout to file + /// 保存当前矩阵布局:使用 SaveFileDialog 选择保存路径 + /// Save current matrix layout: use SaveFileDialog to select save path /// private async Task ExecuteSaveLayoutAsync() { @@ -239,12 +432,21 @@ namespace XplorePlane.ViewModels.Cnc return; } + var dialog = new SaveFileDialog + { + Title = "保存矩阵方案", + Filter = "JSON 文件 (*.json)|*.json", + DefaultExt = ".json", + FileName = $"matrix_{_currentLayout.Id}" + }; + + if (dialog.ShowDialog() != true) + return; + try { - // 占位:实际应通过文件对话框选择路径 | Placeholder: should use file dialog to select path - var filePath = $"matrix_{_currentLayout.Id}.json"; - await _matrixService.SaveAsync(_currentLayout, filePath); - _logger.Info("矩阵布局已保存 | Matrix layout saved: {FilePath}", filePath); + await _matrixService.SaveAsync(_currentLayout, dialog.FileName); + _logger.Info("矩阵布局已保存 | Matrix layout saved: {FilePath}", dialog.FileName); } catch (Exception ex) { @@ -253,15 +455,24 @@ namespace XplorePlane.ViewModels.Cnc } /// - /// 从文件加载矩阵布局 | Load matrix layout from file + /// 从文件加载矩阵布局:使用 OpenFileDialog 选择文件 + /// Load matrix layout from file: use OpenFileDialog to select file /// private async Task ExecuteLoadLayoutAsync() { + var dialog = new OpenFileDialog + { + Title = "加载矩阵方案", + Filter = "JSON 文件 (*.json)|*.json", + CheckFileExists = true + }; + + if (dialog.ShowDialog() != true) + return; + try { - // 占位:实际应通过文件对话框选择路径 | Placeholder: should use file dialog to select path - var filePath = $"matrix_layout.json"; - _currentLayout = await _matrixService.LoadAsync(filePath); + _currentLayout = await _matrixService.LoadAsync(dialog.FileName); // 同步属性到 ViewModel | Sync properties to ViewModel Rows = _currentLayout.Rows; @@ -271,14 +482,132 @@ namespace XplorePlane.ViewModels.Cnc AssociatedProgramPath = _currentLayout.CncProgramPath; RefreshCells(); + + // 检查关联的 CNC 程序文件是否存在 | Check if associated CNC program file exists + if (!string.IsNullOrWhiteSpace(_currentLayout.CncProgramPath) + && !File.Exists(_currentLayout.CncProgramPath)) + { + MessageBox.Show("关联的 CNC 程序文件已移动或删除,请重新关联", "警告", MessageBoxButton.OK, MessageBoxImage.Warning); + } + _logger.Info("矩阵布局已加载 | Matrix layout loaded: Rows={Rows}, Columns={Columns}", Rows, Columns); } + catch (InvalidDataException ex) + { + MessageBox.Show($"方案文件格式无效:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + _logger.Error(ex, "加载矩阵布局失败:格式无效 | Failed to load matrix layout: invalid format"); + } catch (Exception ex) { _logger.Error(ex, "加载矩阵布局失败 | Failed to load matrix layout"); } } + /// + /// 判断 RunMatrixCommand 是否可执行 + /// Determine if RunMatrixCommand can execute + /// + private bool CanExecuteRunMatrix() + { + return !IsExecuting && !string.IsNullOrWhiteSpace(AssociatedProgramPath); + } + + /// + /// 执行矩阵运行:加载模板程序 → 重置状态 → 执行 → 更新 UI + /// Execute matrix run: load template → reset states → execute → update UI + /// + private async Task ExecuteRunMatrixAsync() + { + if (_currentLayout == null || string.IsNullOrWhiteSpace(AssociatedProgramPath)) + return; + + CncProgram templateProgram; + try + { + templateProgram = await _cncProgramService.LoadAsync(AssociatedProgramPath); + } + catch (Exception ex) + { + MessageBox.Show($"无法加载程序文件:{AssociatedProgramName}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + _logger.Error(ex, "运行矩阵失败:无法加载模板程序 | Run matrix failed: cannot load template program"); + return; + } + + IsExecuting = true; + + // 重置所有单元格状态为 NotExecuted | Reset all cell statuses to NotExecuted + foreach (var cell in Cells) + { + cell.Status = MatrixCellStatus.NotExecuted; + cell.ErrorMessage = null; + } + + StatusText = null; + _executionCts = new CancellationTokenSource(); + + var progress = new Progress(p => + { + Application.Current.Dispatcher.Invoke(() => + { + // 更新对应单元格的状态 | Update the corresponding cell's status + var cellVm = Cells.FirstOrDefault(c => c.Row == p.Row && c.Column == p.Column); + if (cellVm != null) + { + cellVm.Status = p.Status; + if (!string.IsNullOrEmpty(p.ErrorMessage)) + cellVm.ErrorMessage = p.ErrorMessage; + } + + // 更新状态文本 | Update status text + if (p.Status == MatrixCellStatus.Executing) + { + StatusText = $"正在执行:{AssociatedProgramName} | 当前位置:R{p.Row}C{p.Column}({p.CurrentIndex}/{p.TotalEnabled})"; + } + }); + }); + + try + { + await _orchestrationService.ExecuteAsync(_currentLayout, templateProgram, progress, _executionCts.Token); + + // 执行完成 | Execution completed + var completedCount = Cells.Count(c => c.Status == MatrixCellStatus.Completed); + var failedCount = Cells.Count(c => c.Status == MatrixCellStatus.Error); + var totalEnabled = Cells.Count(c => c.IsEnabled); + StatusText = $"执行完成:共 {totalEnabled} 个位置,{completedCount} 成功,{failedCount} 失败"; + } + catch (OperationCanceledException) + { + // 用户停止或外部取消 | User stopped or external cancellation + var completedCount = Cells.Count(c => c.Status == MatrixCellStatus.Completed || c.Status == MatrixCellStatus.Error); + var notExecutedCount = Cells.Count(c => c.IsEnabled && c.Status == MatrixCellStatus.NotExecuted); + StatusText = $"执行已停止:已完成 {completedCount} 个位置,{notExecutedCount} 个位置未执行"; + } + catch (InvalidOperationException ex) + { + MessageBox.Show(ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error); + _logger.Error(ex, "运行矩阵失败 | Run matrix failed"); + } + catch (Exception ex) + { + _logger.Error(ex, "矩阵执行异常 | Matrix execution exception"); + } + finally + { + IsExecuting = false; + _executionCts?.Dispose(); + _executionCts = null; + } + } + + /// + /// 停止矩阵执行 | Stop matrix execution + /// + private void ExecuteStop() + { + _executionCts?.Cancel(); + } + // ── 辅助方法 | Helper methods ─────────────────────────────────── /// @@ -290,13 +619,21 @@ namespace XplorePlane.ViewModels.Cnc Cells.Clear(); if (_currentLayout?.Cells == null) + { + EnabledCount = 0; + TotalCount = 0; return; + } foreach (var cell in _currentLayout.Cells) { Cells.Add(new MatrixCellViewModel(cell)); } + // 更新统计 | Update statistics + TotalCount = Cells.Count; + EnabledCount = Cells.Count(c => c.IsEnabled); + // 尝试保持选中状态 | Try to preserve selection if (SelectedCell != null) { @@ -305,4 +642,4 @@ namespace XplorePlane.ViewModels.Cnc } } } -} \ No newline at end of file +} diff --git a/XplorePlane/Views/Cnc/MatrixPageView.xaml b/XplorePlane/Views/Cnc/MatrixPageView.xaml index 0d7ef71..2512bde 100644 --- a/XplorePlane/Views/Cnc/MatrixPageView.xaml +++ b/XplorePlane/Views/Cnc/MatrixPageView.xaml @@ -6,6 +6,8 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:prism="http://prismlibrary.com/" + xmlns:models="clr-namespace:XplorePlane.Models" + xmlns:converters="clr-namespace:XplorePlane.Converters" d:DesignHeight="700" d:DesignWidth="900" prism:ViewModelLocator.AutoWireViewModel="True" @@ -19,6 +21,10 @@ Microsoft YaHei UI + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -