#0050 新增CNC视图

This commit is contained in:
zhengxuan.zhang
2026-03-27 09:54:03 +08:00
parent acc9b11942
commit 08fd25cdd0
32 changed files with 3182 additions and 13 deletions
+19
View File
@@ -8,7 +8,12 @@ using XplorePlane.Views;
using XplorePlane.ViewModels; using XplorePlane.ViewModels;
using XplorePlane.Services; using XplorePlane.Services;
using XplorePlane.Services.AppState; using XplorePlane.Services.AppState;
using XplorePlane.Services.Cnc;
using XplorePlane.Services.Matrix;
using XplorePlane.Services.Measurement;
using XplorePlane.Services.Recipe; using XplorePlane.Services.Recipe;
using XplorePlane.ViewModels.Cnc;
using XplorePlane.Views.Cnc;
using Prism.Ioc; using Prism.Ioc;
using Prism.DryIoc; using Prism.DryIoc;
using Prism.Modularity; using Prism.Modularity;
@@ -247,6 +252,20 @@ namespace XplorePlane
containerRegistry.RegisterSingleton<DumpConfig>(() => XP.Common.Helpers.ConfigLoader.LoadDumpConfig()); containerRegistry.RegisterSingleton<DumpConfig>(() => XP.Common.Helpers.ConfigLoader.LoadDumpConfig());
containerRegistry.RegisterSingleton<IDumpService, DumpService>(); containerRegistry.RegisterSingleton<IDumpService, DumpService>();
// ── CNC / 矩阵编排 / 测量数据服务(单例)──
containerRegistry.RegisterSingleton<ICncProgramService, CncProgramService>();
containerRegistry.RegisterSingleton<IMatrixService, MatrixService>();
containerRegistry.RegisterSingleton<IMeasurementDataService, MeasurementDataService>();
// ── CNC / 矩阵 ViewModel(瞬态)──
containerRegistry.Register<CncEditorViewModel>();
containerRegistry.Register<MatrixEditorViewModel>();
containerRegistry.Register<MeasurementStatsViewModel>();
// ── CNC / 矩阵导航视图 ──
containerRegistry.RegisterForNavigation<CncPageView>();
containerRegistry.RegisterForNavigation<MatrixPageView>();
Log.Information("依赖注入容器配置完成"); Log.Information("依赖注入容器配置完成");
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 861 B

@@ -0,0 +1,15 @@
using Prism.Events;
namespace XplorePlane.Events
{
/// <summary>
/// CNC 程序状态变更事件 | CNC program changed event
/// 当 CNC 程序被修改时通过 IEventAggregator 发布 | Published via IEventAggregator when CNC program is modified
/// </summary>
public class CncProgramChangedEvent : PubSubEvent<CncProgramChangedPayload>
{
}
/// <summary>CNC 程序变更载荷 | CNC program changed payload</summary>
public record CncProgramChangedPayload(string ProgramName, bool IsModified);
}
@@ -0,0 +1,22 @@
using Prism.Events;
using XplorePlane.Models;
namespace XplorePlane.Events
{
/// <summary>
/// 矩阵执行进度事件 | Matrix execution progress event
/// 矩阵执行过程中通过 IEventAggregator 发布进度更新 | Published via IEventAggregator during matrix execution
/// </summary>
public class MatrixExecutionProgressEvent : PubSubEvent<MatrixExecutionProgressPayload>
{
}
/// <summary>矩阵执行进度载荷 | Matrix execution progress payload</summary>
public record MatrixExecutionProgressPayload(
int CurrentRow,
int CurrentColumn,
int TotalCells,
int CompletedCells,
MatrixCellStatus Status
);
}
+116
View File
@@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace XplorePlane.Models
{
// ── CNC 节点类型枚举 | CNC Node Type Enumeration ──────────────────
/// <summary>CNC 节点类型 | CNC node type</summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum CncNodeType
{
ReferencePoint,
SaveNodeWithImage,
SaveNode,
SavePosition,
InspectionModule,
InspectionMarker,
PauseDialog,
WaitDelay,
CompleteProgram
}
// ── CNC 节点基类与派生类型 | CNC Node Base & Derived Types ────────
/// <summary>
/// CNC 节点抽象基类(不可变)| CNC node abstract base (immutable)
/// 使用 System.Text.Json 多态序列化 | Uses System.Text.Json polymorphic serialization
/// </summary>
[JsonDerivedType(typeof(ReferencePointNode), "ReferencePoint")]
[JsonDerivedType(typeof(SaveNodeWithImageNode), "SaveNodeWithImage")]
[JsonDerivedType(typeof(SaveNodeNode), "SaveNode")]
[JsonDerivedType(typeof(SavePositionNode), "SavePosition")]
[JsonDerivedType(typeof(InspectionModuleNode), "InspectionModule")]
[JsonDerivedType(typeof(InspectionMarkerNode), "InspectionMarker")]
[JsonDerivedType(typeof(PauseDialogNode), "PauseDialog")]
[JsonDerivedType(typeof(WaitDelayNode), "WaitDelay")]
[JsonDerivedType(typeof(CompleteProgramNode), "CompleteProgram")]
public abstract record CncNode(
Guid Id,
int Index,
CncNodeType NodeType,
string Name
);
/// <summary>参考点节点 | Reference point node</summary>
public record ReferencePointNode(
Guid Id, int Index, string Name,
double XM, double YM, double ZT, double ZD, double TiltD, double Dist
) : CncNode(Id, Index, CncNodeType.ReferencePoint, Name);
/// <summary>保存节点(含图像)| Save node with image</summary>
public record SaveNodeWithImageNode(
Guid Id, int Index, string Name,
MotionState MotionState,
RaySourceState RaySourceState,
DetectorState DetectorState,
string ImageFileName
) : CncNode(Id, Index, CncNodeType.SaveNodeWithImage, Name);
/// <summary>保存节点(不含图像)| Save node without image</summary>
public record SaveNodeNode(
Guid Id, int Index, string Name,
MotionState MotionState,
RaySourceState RaySourceState,
DetectorState DetectorState
) : CncNode(Id, Index, CncNodeType.SaveNode, Name);
/// <summary>保存位置节点 | Save position node</summary>
public record SavePositionNode(
Guid Id, int Index, string Name,
MotionState MotionState
) : CncNode(Id, Index, CncNodeType.SavePosition, Name);
/// <summary>检测模块节点 | Inspection module node</summary>
public record InspectionModuleNode(
Guid Id, int Index, string Name,
PipelineModel Pipeline
) : CncNode(Id, Index, CncNodeType.InspectionModule, Name);
/// <summary>检测标记节点 | Inspection marker node</summary>
public record InspectionMarkerNode(
Guid Id, int Index, string Name,
string MarkerType,
double MarkerX, double MarkerY
) : CncNode(Id, Index, CncNodeType.InspectionMarker, Name);
/// <summary>停顿对话框节点 | Pause dialog node</summary>
public record PauseDialogNode(
Guid Id, int Index, string Name,
string DialogTitle,
string DialogMessage
) : CncNode(Id, Index, CncNodeType.PauseDialog, Name);
/// <summary>等待延时节点 | Wait delay node</summary>
public record WaitDelayNode(
Guid Id, int Index, string Name,
int DelayMilliseconds
) : CncNode(Id, Index, CncNodeType.WaitDelay, Name);
/// <summary>完成程序节点 | Complete program node</summary>
public record CompleteProgramNode(
Guid Id, int Index, string Name
) : CncNode(Id, Index, CncNodeType.CompleteProgram, Name);
// ── CNC 程序 | CNC Program ────────────────────────────────────────
/// <summary>CNC 程序(不可变)| CNC program (immutable)</summary>
public record CncProgram(
Guid Id,
string Name,
DateTime CreatedAt,
DateTime UpdatedAt,
IReadOnlyList<CncNode> Nodes
);
}
+45
View File
@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace XplorePlane.Models
{
// ── 矩阵单元格状态枚举 | Matrix Cell Status Enumeration ──────────
/// <summary>矩阵单元格执行状态 | Matrix cell execution status</summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum MatrixCellStatus
{
NotExecuted,
Executing,
Completed,
Error,
Disabled
}
// ── 矩阵数据模型 | Matrix Data Models ────────────────────────────
/// <summary>矩阵单元格(不可变)| Matrix cell (immutable)</summary>
public record MatrixCell(
int Row,
int Column,
double OffsetX,
double OffsetY,
bool IsEnabled,
MatrixCellStatus Status,
string ErrorMessage
);
/// <summary>矩阵布局(不可变)| Matrix layout (immutable)</summary>
public record MatrixLayout(
Guid Id,
int Rows,
int Columns,
double RowSpacing,
double ColumnSpacing,
double StartOffsetX,
double StartOffsetY,
string CncProgramPath,
IReadOnlyList<MatrixCell> Cells
);
}
+25
View File
@@ -0,0 +1,25 @@
using System;
namespace XplorePlane.Models
{
// ── 测量数据模型 | Measurement Data Models ────────────────────────
/// <summary>测量记录 | Measurement record</summary>
public class MeasurementRecord
{
public long Id { get; set; }
public string RecipeName { get; set; } = string.Empty;
public int StepIndex { get; set; }
public DateTime Timestamp { get; set; }
public double ResultValue { get; set; }
public bool IsPass { get; set; }
}
/// <summary>测量统计(不可变)| Measurement statistics (immutable)</summary>
public record MeasurementStatistics(
int TotalCount,
int PassCount,
int FailCount,
double PassRate
);
}
@@ -0,0 +1,386 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using XP.Common.Logging.Interfaces;
using XplorePlane.Models;
using XplorePlane.Services.AppState;
namespace XplorePlane.Services.Cnc
{
/// <summary>
/// CNC 程序管理服务实现。
/// 负责程序创建、节点编辑(插入/删除/移动 + 自动重编号)、JSON 序列化/反序列化和 .xp 文件读写。
/// CNC program management service implementation.
/// Handles program creation, node editing (insert/remove/move + auto-renumber), JSON serialization and .xp file I/O.
/// </summary>
public class CncProgramService : ICncProgramService
{
private readonly IAppStateService _appStateService;
private readonly ILoggerService _logger;
// ── 序列化配置 | Serialization options ──
private static readonly JsonSerializerOptions CncJsonOptions = new()
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter() }
};
public CncProgramService(
IAppStateService appStateService,
ILoggerService logger)
{
ArgumentNullException.ThrowIfNull(appStateService);
ArgumentNullException.ThrowIfNull(logger);
_appStateService = appStateService;
_logger = logger.ForModule<CncProgramService>();
_logger.Info("CncProgramService 已初始化 | CncProgramService initialized");
}
/// <inheritdoc />
public CncProgram CreateProgram(string name)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("程序名称不能为空 | Program name cannot be empty", nameof(name));
var now = DateTime.UtcNow;
var program = new CncProgram(
Id: Guid.NewGuid(),
Name: name,
CreatedAt: now,
UpdatedAt: now,
Nodes: Array.Empty<CncNode>());
_logger.Info("已创建 CNC 程序 | Created CNC program: {ProgramName}, Id={ProgramId}", name, program.Id);
return program;
}
/// <inheritdoc />
public CncNode CreateNode(CncNodeType type)
{
var id = Guid.NewGuid();
const int defaultIndex = 0;
CncNode node = type switch
{
// 参考点:捕获 6 轴位置 | Reference point: capture 6-axis position
CncNodeType.ReferencePoint => CreateReferencePointNode(id, defaultIndex),
// 保存节点(含图像):捕获全部设备状态 | Save node with image: capture all device states
CncNodeType.SaveNodeWithImage => CreateSaveNodeWithImageNode(id, defaultIndex),
// 保存节点(不含图像):捕获全部设备状态 | Save node: capture all device states
CncNodeType.SaveNode => CreateSaveNodeNode(id, defaultIndex),
// 保存位置:仅捕获运动状态 | Save position: capture motion state only
CncNodeType.SavePosition => CreateSavePositionNode(id, defaultIndex),
// 检测模块:空 Pipeline | Inspection module: empty pipeline
CncNodeType.InspectionModule => new InspectionModuleNode(
id, defaultIndex, $"检测模块_{defaultIndex}",
Pipeline: new PipelineModel()),
// 检测标记:默认标记值 | Inspection marker: default marker values
CncNodeType.InspectionMarker => new InspectionMarkerNode(
id, defaultIndex, $"检测标记_{defaultIndex}",
MarkerType: "Default",
MarkerX: 0.0, MarkerY: 0.0),
// 停顿对话框:默认标题/消息 | Pause dialog: default title/message
CncNodeType.PauseDialog => new PauseDialogNode(
id, defaultIndex, $"停顿_{defaultIndex}",
DialogTitle: "暂停",
DialogMessage: "请确认后继续"),
// 等待延时:默认 1000ms | Wait delay: default 1000ms
CncNodeType.WaitDelay => new WaitDelayNode(
id, defaultIndex, $"延时_{defaultIndex}",
DelayMilliseconds: 1000),
// 完成程序:默认名称 | Complete program: default name
CncNodeType.CompleteProgram => new CompleteProgramNode(
id, defaultIndex, "完成程序"),
_ => throw new ArgumentOutOfRangeException(nameof(type), type, $"不支持的节点类型 | Unsupported node type: {type}")
};
_logger.Info("已创建节点 | Created node: Type={NodeType}, Id={NodeId}", type, id);
return node;
}
/// <inheritdoc />
public CncProgram InsertNode(CncProgram program, int afterIndex, CncNode node)
{
ArgumentNullException.ThrowIfNull(program);
ArgumentNullException.ThrowIfNull(node);
// 防止重复插入 CompleteProgram | Prevent duplicate CompleteProgram insertion
if (node.NodeType == CncNodeType.CompleteProgram &&
program.Nodes.Any(n => n.NodeType == CncNodeType.CompleteProgram))
{
throw new InvalidOperationException(
"CNC 程序中已存在完成程序节点,不允许重复插入 | A CompleteProgram node already exists, duplicate insertion is not allowed");
}
var nodes = new List<CncNode>(program.Nodes);
// afterIndex = -1 表示插入到开头 | afterIndex = -1 means insert at the beginning
int insertAt = afterIndex + 1;
if (insertAt < 0) insertAt = 0;
if (insertAt > nodes.Count) insertAt = nodes.Count;
nodes.Insert(insertAt, node);
var updated = program with
{
Nodes = RenumberNodes(nodes),
UpdatedAt = DateTime.UtcNow
};
_logger.Info("已插入节点 | Inserted node: Type={NodeType}, AfterIndex={AfterIndex}, Program={ProgramName}",
node.NodeType, afterIndex, program.Name);
return updated;
}
/// <inheritdoc />
public CncProgram RemoveNode(CncProgram program, int index)
{
ArgumentNullException.ThrowIfNull(program);
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<CncNode>(program.Nodes);
nodes.RemoveAt(index);
var updated = program with
{
Nodes = RenumberNodes(nodes),
UpdatedAt = DateTime.UtcNow
};
_logger.Info("已移除节点 | Removed node: Index={Index}, Program={ProgramName}",
index, program.Name);
return updated;
}
/// <inheritdoc />
public CncProgram MoveNode(CncProgram program, int oldIndex, int newIndex)
{
ArgumentNullException.ThrowIfNull(program);
if (oldIndex < 0 || oldIndex >= program.Nodes.Count)
throw new ArgumentOutOfRangeException(nameof(oldIndex),
$"旧索引超出范围 | Old index out of range: {oldIndex}, Count={program.Nodes.Count}");
if (newIndex < 0 || newIndex >= program.Nodes.Count)
throw new ArgumentOutOfRangeException(nameof(newIndex),
$"新索引超出范围 | New index out of range: {newIndex}, Count={program.Nodes.Count}");
var nodes = new List<CncNode>(program.Nodes);
var node = nodes[oldIndex];
nodes.RemoveAt(oldIndex);
nodes.Insert(newIndex, node);
var updated = program with
{
Nodes = RenumberNodes(nodes),
UpdatedAt = DateTime.UtcNow
};
_logger.Info("已移动节点 | Moved node: OldIndex={OldIndex} -> NewIndex={NewIndex}, Program={ProgramName}",
oldIndex, newIndex, program.Name);
return updated;
}
/// <inheritdoc />
public async Task SaveAsync(CncProgram program, string filePath)
{
ArgumentNullException.ThrowIfNull(program);
ArgumentNullException.ThrowIfNull(filePath);
// 路径安全性验证:检查路径遍历 | Path safety: check for path traversal
if (filePath.Contains("..", StringComparison.Ordinal))
throw new UnauthorizedAccessException(
$"文件路径包含不安全的路径遍历字符 | File path contains unsafe path traversal characters: {filePath}");
var json = Serialize(program);
await File.WriteAllTextAsync(filePath, json).ConfigureAwait(false);
_logger.Info("已保存 CNC 程序到文件 | Saved CNC program to file: {FilePath}, Program={ProgramName}",
filePath, program.Name);
}
/// <inheritdoc />
public async Task<CncProgram> LoadAsync(string filePath)
{
ArgumentNullException.ThrowIfNull(filePath);
// 路径安全性验证 | Path safety check
if (filePath.Contains("..", StringComparison.Ordinal))
throw new UnauthorizedAccessException(
$"文件路径包含不安全的路径遍历字符 | File path contains unsafe path traversal characters: {filePath}");
if (!File.Exists(filePath))
throw new FileNotFoundException(
$"CNC 程序文件不存在 | CNC program file not found: {filePath}", filePath);
string json;
try
{
json = await File.ReadAllTextAsync(filePath).ConfigureAwait(false);
}
catch (Exception ex) when (ex is not FileNotFoundException)
{
throw new InvalidDataException(
$"无法读取 CNC 程序文件 | Cannot read CNC program file: {filePath}", ex);
}
var program = Deserialize(json);
_logger.Info("已加载 CNC 程序 | Loaded CNC program: {FilePath}, Program={ProgramName}, Nodes={NodeCount}",
filePath, program.Name, program.Nodes.Count);
return program;
}
/// <inheritdoc />
public string Serialize(CncProgram program)
{
ArgumentNullException.ThrowIfNull(program);
return JsonSerializer.Serialize(program, CncJsonOptions);
}
/// <inheritdoc />
public CncProgram Deserialize(string json)
{
if (string.IsNullOrWhiteSpace(json))
throw new InvalidDataException("JSON 字符串为空 | JSON string is empty");
CncProgram program;
try
{
program = JsonSerializer.Deserialize<CncProgram>(json, CncJsonOptions);
}
catch (JsonException ex)
{
throw new InvalidDataException(
$"CNC 程序 JSON 格式无效 | Invalid CNC program JSON format: {ex.Message}", ex);
}
if (program is null)
throw new InvalidDataException("反序列化结果为 null | Deserialization result is null");
if (string.IsNullOrWhiteSpace(program.Name))
throw new InvalidDataException("CNC 程序数据不完整,缺少名称 | CNC program data incomplete, missing Name");
if (program.Nodes is null)
throw new InvalidDataException("CNC 程序数据不完整,缺少节点列表 | CNC program data incomplete, missing Nodes");
// 对节点应用值域校验(如 WaitDelay 截断)| Apply value range validation to nodes
var validatedNodes = ValidateNodes(program.Nodes);
return program with { Nodes = validatedNodes };
}
// ── 常量 | Constants ──
/// <summary>等待延时最小值(毫秒)| Wait delay minimum (ms)</summary>
private const int WaitDelayMin = 0;
/// <summary>等待延时最大值(毫秒)| Wait delay maximum (ms)</summary>
private const int WaitDelayMax = 300000;
// ── 内部辅助方法 | Internal helper methods ──
/// <summary>
/// 将等待延时值截断到 [0, 300000] 范围 | Clamp wait delay value to [0, 300000] range
/// </summary>
internal static int ClampWaitDelay(int value) =>
Math.Clamp(value, WaitDelayMin, WaitDelayMax);
/// <summary>
/// 对反序列化后的节点列表应用值域校验(如 WaitDelay 截断)
/// Apply value range validation to deserialized nodes (e.g. WaitDelay clamping)
/// </summary>
private static IReadOnlyList<CncNode> ValidateNodes(IReadOnlyList<CncNode> nodes)
{
var result = new List<CncNode>(nodes.Count);
foreach (var node in nodes)
{
if (node is WaitDelayNode wdn)
{
var clamped = ClampWaitDelay(wdn.DelayMilliseconds);
result.Add(clamped != wdn.DelayMilliseconds ? wdn with { DelayMilliseconds = clamped } : wdn);
}
else
{
result.Add(node);
}
}
return result.AsReadOnly();
}
/// <summary>
/// 重新编号节点索引为连续序列 0, 1, 2, ..., N-1record 不可变,使用 with 语法)
/// Renumber node indices to consecutive sequence 0, 1, 2, ..., N-1 (records are immutable, use with syntax)
/// </summary>
private static IReadOnlyList<CncNode> RenumberNodes(List<CncNode> nodes)
{
var result = new List<CncNode>(nodes.Count);
for (int i = 0; i < nodes.Count; i++)
{
result.Add(nodes[i] with { Index = i });
}
return result.AsReadOnly();
}
/// <summary>创建参考点节点 | Create reference point node</summary>
private ReferencePointNode CreateReferencePointNode(Guid id, int index)
{
var motion = _appStateService.MotionState;
return new ReferencePointNode(
id, index, $"参考点_{index}",
XM: motion.XM,
YM: motion.YM,
ZT: motion.ZT,
ZD: motion.ZD,
TiltD: motion.TiltD,
Dist: motion.Dist);
}
/// <summary>创建保存节点(含图像)| Create save node with image</summary>
private SaveNodeWithImageNode CreateSaveNodeWithImageNode(Guid id, int index)
{
return new SaveNodeWithImageNode(
id, index, $"保存节点(图像)_{index}",
MotionState: _appStateService.MotionState,
RaySourceState: _appStateService.RaySourceState,
DetectorState: _appStateService.DetectorState,
ImageFileName: "");
}
/// <summary>创建保存节点(不含图像)| Create save node without image</summary>
private SaveNodeNode CreateSaveNodeNode(Guid id, int index)
{
return new SaveNodeNode(
id, index, $"保存节点_{index}",
MotionState: _appStateService.MotionState,
RaySourceState: _appStateService.RaySourceState,
DetectorState: _appStateService.DetectorState);
}
/// <summary>创建保存位置节点 | Create save position node</summary>
private SavePositionNode CreateSavePositionNode(Guid id, int index)
{
return new SavePositionNode(
id, index, $"保存位置_{index}",
MotionState: _appStateService.MotionState);
}
}
}
@@ -0,0 +1,39 @@
using System.Threading.Tasks;
using XplorePlane.Models;
namespace XplorePlane.Services.Cnc
{
/// <summary>
/// CNC 程序管理服务接口,负责程序的创建、节点编辑、序列化/反序列化和文件读写
/// CNC program management service interface for creation, node editing, serialization and file I/O
/// </summary>
public interface ICncProgramService
{
/// <summary>创建空的 CNC 程序 | Create an empty CNC program</summary>
CncProgram CreateProgram(string name);
/// <summary>根据节点类型创建节点(从 IAppStateService 捕获设备状态)| Create a node by type (captures device state from IAppStateService)</summary>
CncNode CreateNode(CncNodeType type);
/// <summary>在指定索引之后插入节点并重新编号 | Insert a node after the given index and renumber</summary>
CncProgram InsertNode(CncProgram program, int afterIndex, CncNode node);
/// <summary>移除指定索引的节点并重新编号 | Remove the node at the given index and renumber</summary>
CncProgram RemoveNode(CncProgram program, int index);
/// <summary>将节点从旧索引移动到新索引并重新编号 | Move a node from old index to new index and renumber</summary>
CncProgram MoveNode(CncProgram program, int oldIndex, int newIndex);
/// <summary>将 CNC 程序保存到 .xp 文件 | Save CNC program to .xp file</summary>
Task SaveAsync(CncProgram program, string filePath);
/// <summary>从 .xp 文件加载 CNC 程序 | Load CNC program from .xp file</summary>
Task<CncProgram> LoadAsync(string filePath);
/// <summary>将 CNC 程序序列化为 JSON 字符串 | Serialize CNC program to JSON string</summary>
string Serialize(CncProgram program);
/// <summary>从 JSON 字符串反序列化 CNC 程序 | Deserialize CNC program from JSON string</summary>
CncProgram Deserialize(string json);
}
}
@@ -0,0 +1,39 @@
using System.Threading.Tasks;
using XplorePlane.Models;
namespace XplorePlane.Services.Matrix
{
/// <summary>
/// 矩阵编排管理服务接口,负责矩阵布局的创建、编辑、序列化/反序列化和文件读写
/// Matrix layout management service interface for creation, editing, serialization and file I/O
/// </summary>
public interface IMatrixService
{
/// <summary>创建矩阵布局 | Create a matrix layout</summary>
MatrixLayout CreateLayout(int rows, int columns, double rowSpacing, double columnSpacing);
/// <summary>更新矩阵布局参数并重新计算单元格偏移 | Update layout parameters and recalculate cell offsets</summary>
MatrixLayout UpdateLayout(MatrixLayout layout, int rows, int columns, double rowSpacing, double columnSpacing);
/// <summary>获取指定行列的单元格 | Get the cell at the specified row and column</summary>
MatrixCell GetCell(MatrixLayout layout, int row, int column);
/// <summary>切换单元格启用/禁用状态 | Toggle cell enabled/disabled state</summary>
MatrixLayout ToggleCellEnabled(MatrixLayout layout, int row, int column);
/// <summary>关联 CNC 程序到矩阵布局 | Associate a CNC program with the matrix layout</summary>
MatrixLayout AssociateCncProgram(MatrixLayout layout, CncProgram program);
/// <summary>将矩阵布局保存到 JSON 文件 | Save matrix layout to JSON file</summary>
Task SaveAsync(MatrixLayout layout, string filePath);
/// <summary>从 JSON 文件加载矩阵布局 | Load matrix layout from JSON file</summary>
Task<MatrixLayout> LoadAsync(string filePath);
/// <summary>将矩阵布局序列化为 JSON 字符串 | Serialize matrix layout to JSON string</summary>
string Serialize(MatrixLayout layout);
/// <summary>从 JSON 字符串反序列化矩阵布局 | Deserialize matrix layout from JSON string</summary>
MatrixLayout Deserialize(string json);
}
}
@@ -0,0 +1,280 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using XP.Common.Logging.Interfaces;
using XplorePlane.Models;
namespace XplorePlane.Services.Matrix
{
/// <summary>
/// 矩阵编排管理服务实现。
/// 负责矩阵布局的创建、编辑、单元格偏移计算、JSON 序列化/反序列化和文件读写。
/// Matrix layout management service implementation.
/// Handles layout creation, editing, cell offset calculation, JSON serialization and file I/O.
/// </summary>
public class MatrixService : IMatrixService
{
private readonly ILoggerService _logger;
// ── 序列化配置 | Serialization options ──
private static readonly JsonSerializerOptions MatrixJsonOptions = new()
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter() }
};
public MatrixService(ILoggerService logger)
{
ArgumentNullException.ThrowIfNull(logger);
_logger = logger.ForModule<MatrixService>();
_logger.Info("MatrixService 已初始化 | MatrixService initialized");
}
/// <inheritdoc />
public MatrixLayout CreateLayout(int rows, int columns, double rowSpacing, double columnSpacing)
{
ValidateDimensions(rows, columns);
var layout = new MatrixLayout(
Id: Guid.NewGuid(),
Rows: rows,
Columns: columns,
RowSpacing: rowSpacing,
ColumnSpacing: columnSpacing,
StartOffsetX: 0.0,
StartOffsetY: 0.0,
CncProgramPath: string.Empty,
Cells: BuildCells(rows, columns, rowSpacing, columnSpacing, 0.0, 0.0));
_logger.Info("已创建矩阵布局 | Created matrix layout: Id={LayoutId}, Rows={Rows}, Columns={Columns}",
layout.Id, rows, columns);
return layout;
}
/// <inheritdoc />
public MatrixLayout UpdateLayout(MatrixLayout layout, int rows, int columns, double rowSpacing, double columnSpacing)
{
ArgumentNullException.ThrowIfNull(layout);
ValidateDimensions(rows, columns);
var updated = layout with
{
Rows = rows,
Columns = columns,
RowSpacing = rowSpacing,
ColumnSpacing = columnSpacing,
Cells = BuildCells(rows, columns, rowSpacing, columnSpacing, layout.StartOffsetX, layout.StartOffsetY)
};
_logger.Info("已更新矩阵布局 | Updated matrix layout: Id={LayoutId}, Rows={Rows}, Columns={Columns}",
layout.Id, rows, columns);
return updated;
}
/// <inheritdoc />
public MatrixCell GetCell(MatrixLayout layout, int row, int column)
{
ArgumentNullException.ThrowIfNull(layout);
ValidateCellCoordinates(layout, row, column);
int index = row * layout.Columns + column;
return layout.Cells[index];
}
/// <inheritdoc />
public MatrixLayout ToggleCellEnabled(MatrixLayout layout, int row, int column)
{
ArgumentNullException.ThrowIfNull(layout);
ValidateCellCoordinates(layout, row, column);
int targetIndex = row * layout.Columns + column;
var cells = new List<MatrixCell>(layout.Cells.Count);
for (int i = 0; i < layout.Cells.Count; i++)
{
if (i == targetIndex)
{
var cell = layout.Cells[i];
cells.Add(cell with { IsEnabled = !cell.IsEnabled });
}
else
{
cells.Add(layout.Cells[i]);
}
}
var updated = layout with { Cells = cells.AsReadOnly() };
_logger.Info("已切换单元格启用状态 | Toggled cell enabled state: Row={Row}, Column={Column}, IsEnabled={IsEnabled}",
row, column, updated.Cells[targetIndex].IsEnabled);
return updated;
}
/// <inheritdoc />
public MatrixLayout AssociateCncProgram(MatrixLayout layout, CncProgram program)
{
ArgumentNullException.ThrowIfNull(layout);
ArgumentNullException.ThrowIfNull(program);
var updated = layout with { CncProgramPath = program.Name };
_logger.Info("已关联 CNC 程序到矩阵布局 | Associated CNC program with matrix layout: LayoutId={LayoutId}, Program={ProgramName}",
layout.Id, program.Name);
return updated;
}
/// <inheritdoc />
public async Task SaveAsync(MatrixLayout layout, string filePath)
{
ArgumentNullException.ThrowIfNull(layout);
ArgumentNullException.ThrowIfNull(filePath);
// 路径安全性验证:检查路径遍历 | Path safety: check for path traversal
if (filePath.Contains("..", StringComparison.Ordinal))
throw new UnauthorizedAccessException(
$"文件路径包含不安全的路径遍历字符 | File path contains unsafe path traversal characters: {filePath}");
var json = Serialize(layout);
await File.WriteAllTextAsync(filePath, json).ConfigureAwait(false);
_logger.Info("已保存矩阵布局到文件 | Saved matrix layout to file: {FilePath}, LayoutId={LayoutId}",
filePath, layout.Id);
}
/// <inheritdoc />
public async Task<MatrixLayout> LoadAsync(string filePath)
{
ArgumentNullException.ThrowIfNull(filePath);
// 路径安全性验证 | Path safety check
if (filePath.Contains("..", StringComparison.Ordinal))
throw new UnauthorizedAccessException(
$"文件路径包含不安全的路径遍历字符 | File path contains unsafe path traversal characters: {filePath}");
if (!File.Exists(filePath))
throw new FileNotFoundException(
$"矩阵布局文件不存在 | Matrix layout file not found: {filePath}", filePath);
string json;
try
{
json = await File.ReadAllTextAsync(filePath).ConfigureAwait(false);
}
catch (Exception ex) when (ex is not FileNotFoundException)
{
throw new InvalidDataException(
$"无法读取矩阵布局文件 | Cannot read matrix layout file: {filePath}", ex);
}
var layout = Deserialize(json);
_logger.Info("已加载矩阵布局 | Loaded matrix layout: {FilePath}, LayoutId={LayoutId}, Rows={Rows}, Columns={Columns}",
filePath, layout.Id, layout.Rows, layout.Columns);
return layout;
}
/// <inheritdoc />
public string Serialize(MatrixLayout layout)
{
ArgumentNullException.ThrowIfNull(layout);
return JsonSerializer.Serialize(layout, MatrixJsonOptions);
}
/// <inheritdoc />
public MatrixLayout Deserialize(string json)
{
if (string.IsNullOrWhiteSpace(json))
throw new InvalidDataException("JSON 字符串为空 | JSON string is empty");
MatrixLayout layout;
try
{
layout = JsonSerializer.Deserialize<MatrixLayout>(json, MatrixJsonOptions);
}
catch (JsonException ex)
{
throw new InvalidDataException(
$"矩阵布局 JSON 格式无效 | Invalid matrix layout JSON format: {ex.Message}", ex);
}
if (layout is null)
throw new InvalidDataException("反序列化结果为 null | Deserialization result is null");
if (layout.Rows <= 0)
throw new InvalidDataException("矩阵布局数据不完整,行数无效 | Matrix layout data incomplete, invalid Rows");
if (layout.Columns <= 0)
throw new InvalidDataException("矩阵布局数据不完整,列数无效 | Matrix layout data incomplete, invalid Columns");
if (layout.Cells is null)
throw new InvalidDataException("矩阵布局数据不完整,缺少单元格列表 | Matrix layout data incomplete, missing Cells");
return layout;
}
// ── 内部辅助方法 | Internal helper methods ──
/// <summary>
/// 构建矩阵单元格列表,自动计算每个单元格的偏移坐标
/// Build matrix cell list with auto-calculated offset coordinates
/// OffsetX = StartOffsetX + col * ColumnSpacing
/// OffsetY = StartOffsetY + row * RowSpacing
/// </summary>
internal static IReadOnlyList<MatrixCell> BuildCells(
int rows, int columns,
double rowSpacing, double columnSpacing,
double startOffsetX, double startOffsetY)
{
var cells = new List<MatrixCell>(rows * columns);
for (int row = 0; row < rows; row++)
{
for (int col = 0; col < columns; col++)
{
cells.Add(new MatrixCell(
Row: row,
Column: col,
OffsetX: startOffsetX + col * columnSpacing,
OffsetY: startOffsetY + row * rowSpacing,
IsEnabled: true,
Status: MatrixCellStatus.NotExecuted,
ErrorMessage: string.Empty));
}
}
return cells.AsReadOnly();
}
/// <summary>
/// 验证行列数参数 | Validate row and column dimensions
/// </summary>
private static void ValidateDimensions(int rows, int columns)
{
if (rows <= 0)
throw new ArgumentOutOfRangeException(nameof(rows),
$"行数必须大于 0 | Rows must be greater than 0: {rows}");
if (columns <= 0)
throw new ArgumentOutOfRangeException(nameof(columns),
$"列数必须大于 0 | Columns must be greater than 0: {columns}");
}
/// <summary>
/// 验证单元格坐标是否在布局范围内 | Validate cell coordinates are within layout bounds
/// </summary>
private static void ValidateCellCoordinates(MatrixLayout layout, int row, int column)
{
if (row < 0 || row >= layout.Rows)
throw new ArgumentOutOfRangeException(nameof(row),
$"行索引超出范围 | Row index out of range: {row}, Rows={layout.Rows}");
if (column < 0 || column >= layout.Columns)
throw new ArgumentOutOfRangeException(nameof(column),
$"列索引超出范围 | Column index out of range: {column}, Columns={layout.Columns}");
}
}
}
@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using XplorePlane.Models;
namespace XplorePlane.Services.Measurement
{
/// <summary>
/// 测量数据存储服务接口,负责测量记录的持久化、查询和统计
/// Measurement data storage service interface for persistence, querying and statistics of measurement records
/// </summary>
public interface IMeasurementDataService
{
/// <summary>初始化数据库表和索引 | Initialize database tables and indexes</summary>
Task InitializeAsync();
/// <summary>保存单条测量记录 | Save a single measurement record</summary>
Task SaveMeasurementAsync(MeasurementRecord record);
/// <summary>按配方名称和时间范围查询测量记录 | Query measurement records by recipe name and time range</summary>
Task<IReadOnlyList<MeasurementRecord>> QueryAsync(string recipeName, DateTime? from, DateTime? to);
/// <summary>按配方名称和时间范围获取统计数据 | Get statistics by recipe name and time range</summary>
Task<MeasurementStatistics> GetStatisticsAsync(string recipeName, DateTime? from, DateTime? to);
}
}
@@ -0,0 +1,252 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using XP.Common.Database.Interfaces;
using XP.Common.Logging.Interfaces;
using XplorePlane.Models;
namespace XplorePlane.Services.Measurement
{
/// <summary>
/// 测量数据存储服务实现,使用 SQLite(通过 IDbContext)持久化测量记录并提供统计查询。
/// Measurement data storage service implementation using SQLite (via IDbContext) to persist records and provide statistics.
/// </summary>
public class MeasurementDataService : IMeasurementDataService
{
private readonly IDbContext _db;
private readonly ILoggerService _logger;
// ── SQL 常量 | SQL constants ──
private const string CreateTableSql = @"
CREATE TABLE IF NOT EXISTS measurement_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
recipe_name TEXT NOT NULL,
step_index INTEGER NOT NULL,
timestamp TEXT NOT NULL,
result_value REAL NOT NULL,
is_pass INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_measurement_recipe ON measurement_data(recipe_name);
CREATE INDEX IF NOT EXISTS idx_measurement_timestamp ON measurement_data(timestamp);";
private const string InsertSql = @"
INSERT INTO measurement_data (recipe_name, step_index, timestamp, result_value, is_pass)
VALUES (@recipe_name, @step_index, @timestamp, @result_value, @is_pass)";
// ── 构造函数 | Constructor ──
public MeasurementDataService(IDbContext db, ILoggerService logger)
{
ArgumentNullException.ThrowIfNull(db);
ArgumentNullException.ThrowIfNull(logger);
_db = db;
_logger = logger.ForModule<MeasurementDataService>();
_logger.Info("MeasurementDataService 已初始化 | MeasurementDataService initialized");
}
// ── 公共方法 | Public methods ──
/// <inheritdoc />
public async Task InitializeAsync()
{
_logger.Info("正在初始化测量数据表 | Initializing measurement data table");
var result = await _db.ExecuteNonQueryAsync(CreateTableSql).ConfigureAwait(false);
if (!result.IsSuccess)
{
_logger.Error(result.Exception!, "初始化测量数据表失败 | Failed to initialize measurement data table: {Message}", result.Message);
throw new InvalidOperationException(
$"初始化测量数据表失败 | Failed to initialize measurement data table: {result.Message}",
result.Exception);
}
_logger.Info("测量数据表初始化完成 | Measurement data table initialized");
}
/// <inheritdoc />
public async Task SaveMeasurementAsync(MeasurementRecord record)
{
ArgumentNullException.ThrowIfNull(record);
var parameters = new Dictionary<string, object>
{
["@recipe_name"] = record.RecipeName,
["@step_index"] = record.StepIndex,
["@timestamp"] = record.Timestamp.ToString("o"), // ISO 8601 往返格式 | ISO 8601 round-trip format
["@result_value"] = record.ResultValue,
["@is_pass"] = record.IsPass ? 1 : 0
};
var result = await _db.ExecuteNonQueryAsync(InsertSql, parameters).ConfigureAwait(false);
if (!result.IsSuccess)
{
_logger.Error(result.Exception!, "保存测量记录失败 | Failed to save measurement record: {Message}", result.Message);
throw new InvalidOperationException(
$"保存测量记录失败 | Failed to save measurement record: {result.Message}",
result.Exception);
}
_logger.Debug("已保存测量记录 | Saved measurement record: Recipe={RecipeName}, Step={StepIndex}, IsPass={IsPass}",
record.RecipeName, record.StepIndex, record.IsPass);
}
/// <inheritdoc />
public async Task<IReadOnlyList<MeasurementRecord>> QueryAsync(string recipeName, DateTime? from, DateTime? to)
{
var (sql, parameters) = BuildFilteredQuery(
"SELECT id, recipe_name, step_index, timestamp, result_value, is_pass FROM measurement_data",
recipeName, from, to,
orderBy: "ORDER BY timestamp ASC");
var (result, data) = await _db.QueryListAsync<MeasurementDataRow>(sql, parameters).ConfigureAwait(false);
if (!result.IsSuccess)
{
_logger.Error(result.Exception!, "查询测量记录失败 | Failed to query measurement records: {Message}", result.Message);
throw new InvalidOperationException(
$"查询测量记录失败 | Failed to query measurement records: {result.Message}",
result.Exception);
}
// 将数据库行映射为领域模型 | Map database rows to domain model
var records = new List<MeasurementRecord>(data.Count);
foreach (var row in data)
{
records.Add(MapRow(row));
}
_logger.Debug("查询到 {Count} 条测量记录 | Queried {Count} measurement records", records.Count);
return records.AsReadOnly();
}
/// <inheritdoc />
public async Task<MeasurementStatistics> GetStatisticsAsync(string recipeName, DateTime? from, DateTime? to)
{
var (sql, parameters) = BuildFilteredQuery(
"SELECT COUNT(*) AS total_count, SUM(CASE WHEN is_pass = 1 THEN 1 ELSE 0 END) AS pass_count FROM measurement_data",
recipeName, from, to);
var (result, data) = await _db.QueryListAsync<StatisticsRow>(sql, parameters).ConfigureAwait(false);
if (!result.IsSuccess)
{
_logger.Error(result.Exception!, "获取测量统计失败 | Failed to get measurement statistics: {Message}", result.Message);
throw new InvalidOperationException(
$"获取测量统计失败 | Failed to get measurement statistics: {result.Message}",
result.Exception);
}
// 无数据时返回全零统计 | Return zero statistics when no data
if (data.Count == 0)
return new MeasurementStatistics(0, 0, 0, 0.0);
var row = data[0];
int total = row.total_count;
int pass = row.pass_count;
int fail = total - pass;
double passRate = total > 0 ? (double)pass / total : 0.0;
_logger.Debug("统计结果 | Statistics: Total={Total}, Pass={Pass}, Fail={Fail}, PassRate={PassRate:P2}",
total, pass, fail, passRate);
return new MeasurementStatistics(total, pass, fail, passRate);
}
// ── 内部辅助方法 | Internal helper methods ──
/// <summary>
/// 根据筛选条件动态构建 SQL 和参数字典
/// Dynamically build SQL and parameter dictionary based on filter conditions
/// </summary>
private static (string Sql, Dictionary<string, object>? Parameters) BuildFilteredQuery(
string selectClause,
string? recipeName,
DateTime? from,
DateTime? to,
string? orderBy = null)
{
var conditions = new List<string>();
Dictionary<string, object>? parameters = null;
// 按配方名称筛选 | Filter by recipe name
if (!string.IsNullOrWhiteSpace(recipeName))
{
conditions.Add("recipe_name = @recipe_name");
parameters ??= new Dictionary<string, object>();
parameters["@recipe_name"] = recipeName;
}
// 按起始时间筛选 | Filter by start time
if (from.HasValue)
{
conditions.Add("timestamp >= @from");
parameters ??= new Dictionary<string, object>();
parameters["@from"] = from.Value.ToString("o");
}
// 按结束时间筛选 | Filter by end time
if (to.HasValue)
{
conditions.Add("timestamp <= @to");
parameters ??= new Dictionary<string, object>();
parameters["@to"] = to.Value.ToString("o");
}
// 组装完整 SQL | Assemble complete SQL
var sql = selectClause;
if (conditions.Count > 0)
{
sql += " WHERE " + string.Join(" AND ", conditions);
}
if (!string.IsNullOrEmpty(orderBy))
{
sql += " " + orderBy;
}
return (sql, parameters);
}
/// <summary>
/// 将数据库行 DTO 映射为领域模型
/// Map database row DTO to domain model
/// </summary>
private static MeasurementRecord MapRow(MeasurementDataRow row)
{
return new MeasurementRecord
{
Id = row.id,
RecipeName = row.recipe_name,
StepIndex = row.step_index,
Timestamp = DateTime.Parse(row.timestamp, null, System.Globalization.DateTimeStyles.RoundtripKind),
ResultValue = row.result_value,
IsPass = row.is_pass != 0
};
}
// ── 内部 DTO(用于 QueryListAsync 自动映射)| Internal DTOs for QueryListAsync auto-mapping ──
/// <summary>数据库行 DTO,属性名与 SQL 列名一致以支持自动映射</summary>
internal class MeasurementDataRow
{
public long id { get; set; }
public string recipe_name { get; set; } = string.Empty;
public int step_index { get; set; }
public string timestamp { get; set; } = string.Empty;
public double result_value { get; set; }
public int is_pass { get; set; }
}
/// <summary>统计查询 DTO</summary>
internal class StatisticsRow
{
public int total_count { get; set; }
public int pass_count { get; set; }
}
}
}
@@ -0,0 +1,378 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using Prism.Commands;
using Prism.Events;
using Prism.Mvvm;
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 编辑器 ViewModel,管理 CNC 程序的节点列表、编辑操作和文件操作
/// CNC editor ViewModel that manages the node list, editing operations and file operations
/// </summary>
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<CncNodeViewModel> _nodes;
private CncNodeViewModel _selectedNode;
private bool _isModified;
private string _programName;
/// <summary>
/// 构造函数 | Constructor
/// </summary>
public CncEditorViewModel(
ICncProgramService cncProgramService,
IAppStateService appStateService,
IEventAggregator eventAggregator,
ILoggerService logger)
{
_cncProgramService = cncProgramService ?? throw new ArgumentNullException(nameof(cncProgramService));
_appStateService = appStateService ?? throw new ArgumentNullException(nameof(appStateService));
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule<CncEditorViewModel>();
_nodes = new ObservableCollection<CncNodeViewModel>();
// ── 节点插入命令 | Node insertion commands ──
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));
// ── 节点编辑命令 | Node editing commands ──
DeleteNodeCommand = new DelegateCommand(ExecuteDeleteNode, CanExecuteDeleteNode)
.ObservesProperty(() => SelectedNode);
MoveNodeUpCommand = new DelegateCommand<CncNodeViewModel>(ExecuteMoveNodeUp);
MoveNodeDownCommand = new DelegateCommand<CncNodeViewModel>(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");
}
// ── 属性 | Properties ──────────────────────────────────────────
/// <summary>节点列表 | Node list</summary>
public ObservableCollection<CncNodeViewModel> Nodes
{
get => _nodes;
set => SetProperty(ref _nodes, value);
}
/// <summary>当前选中的节点 | Currently selected node</summary>
public CncNodeViewModel SelectedNode
{
get => _selectedNode;
set => SetProperty(ref _selectedNode, value);
}
/// <summary>程序是否已修改 | Whether the program has been modified</summary>
public bool IsModified
{
get => _isModified;
set => SetProperty(ref _isModified, value);
}
/// <summary>当前程序名称 | Current program name</summary>
public string ProgramName
{
get => _programName;
set => SetProperty(ref _programName, value);
}
// ── 节点插入命令 | Node insertion commands ──────────────────────
/// <summary>插入参考点命令 | Insert reference point command</summary>
public DelegateCommand InsertReferencePointCommand { get; }
/// <summary>插入保存节点(含图像)命令 | Insert save node with image command</summary>
public DelegateCommand InsertSaveNodeWithImageCommand { get; }
/// <summary>插入保存节点命令 | Insert save node command</summary>
public DelegateCommand InsertSaveNodeCommand { get; }
/// <summary>插入保存位置命令 | Insert save position command</summary>
public DelegateCommand InsertSavePositionCommand { get; }
/// <summary>插入检测模块命令 | Insert inspection module command</summary>
public DelegateCommand InsertInspectionModuleCommand { get; }
/// <summary>插入检测标记命令 | Insert inspection marker command</summary>
public DelegateCommand InsertInspectionMarkerCommand { get; }
/// <summary>插入停顿对话框命令 | Insert pause dialog command</summary>
public DelegateCommand InsertPauseDialogCommand { get; }
/// <summary>插入等待延时命令 | Insert wait delay command</summary>
public DelegateCommand InsertWaitDelayCommand { get; }
/// <summary>插入完成程序命令 | Insert complete program command</summary>
public DelegateCommand InsertCompleteProgramCommand { get; }
// ── 节点编辑命令 | Node editing commands ────────────────────────
/// <summary>删除选中节点命令 | Delete selected node command</summary>
public DelegateCommand DeleteNodeCommand { get; }
/// <summary>上移节点命令 | Move node up command</summary>
public DelegateCommand<CncNodeViewModel> MoveNodeUpCommand { get; }
/// <summary>下移节点命令 | Move node down command</summary>
public DelegateCommand<CncNodeViewModel> MoveNodeDownCommand { get; }
// ── 文件操作命令 | File operation commands ──────────────────────
/// <summary>保存程序命令 | Save program command</summary>
public DelegateCommand SaveProgramCommand { get; }
/// <summary>加载程序命令 | Load program command</summary>
public DelegateCommand LoadProgramCommand { get; }
/// <summary>新建程序命令 | New program command</summary>
public DelegateCommand NewProgramCommand { get; }
/// <summary>导出 CSV 命令 | Export CSV command</summary>
public DelegateCommand ExportCsvCommand { get; }
// ── 命令执行方法 | Command execution methods ────────────────────
/// <summary>
/// 插入指定类型的节点到选中节点之后
/// Insert a node of the specified type after the selected node
/// </summary>
private void ExecuteInsertNode(CncNodeType nodeType)
{
if (_currentProgram == null)
{
_logger.Warn("无法插入节点:当前无程序 | Cannot insert node: no current program");
return;
}
try
{
var node = _cncProgramService.CreateNode(nodeType);
int afterIndex = SelectedNode?.Index ?? _currentProgram.Nodes.Count - 1;
_currentProgram = _cncProgramService.InsertNode(_currentProgram, afterIndex, node);
OnProgramEdited();
_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);
}
}
/// <summary>
/// 删除选中节点 | Delete the selected node
/// </summary>
private void ExecuteDeleteNode()
{
if (_currentProgram == null || SelectedNode == null)
return;
try
{
_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);
}
}
/// <summary>
/// 判断是否可以删除节点(至少保留 1 个节点)
/// Determines whether delete is allowed (at least 1 node must remain)
/// </summary>
private bool CanExecuteDeleteNode()
{
return SelectedNode != null
&& _currentProgram != null
&& _currentProgram.Nodes.Count > 1;
}
/// <summary>
/// 上移节点 | Move node up
/// </summary>
private void ExecuteMoveNodeUp(CncNodeViewModel nodeVm)
{
if (_currentProgram == null || nodeVm == null || nodeVm.Index <= 0)
return;
try
{
_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);
}
}
/// <summary>
/// 下移节点 | Move node down
/// </summary>
private void ExecuteMoveNodeDown(CncNodeViewModel nodeVm)
{
if (_currentProgram == null || nodeVm == null || nodeVm.Index >= _currentProgram.Nodes.Count - 1)
return;
try
{
_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);
}
}
/// <summary>
/// 保存当前程序到文件 | Save current program to file
/// </summary>
private async Task ExecuteSaveProgramAsync()
{
if (_currentProgram == null)
{
_logger.Warn("无法保存:当前无程序 | Cannot save: no current program");
return;
}
try
{
// 使用程序名称作为文件名 | Use program name as file name
var filePath = $"{_currentProgram.Name}.xp";
await _cncProgramService.SaveAsync(_currentProgram, filePath);
IsModified = false;
_logger.Info("程序已保存 | Program saved: {FilePath}", filePath);
}
catch (Exception ex)
{
_logger.Error(ex, "保存程序失败 | Failed to save program");
}
}
/// <summary>
/// 从文件加载程序 | Load program from file
/// </summary>
private async Task ExecuteLoadProgramAsync()
{
try
{
// 占位:实际应通过文件对话框选择路径 | Placeholder: should use file dialog to select path
var filePath = $"{ProgramName}.xp";
_currentProgram = await _cncProgramService.LoadAsync(filePath);
ProgramName = _currentProgram.Name;
IsModified = false;
RefreshNodes();
_logger.Info("程序已加载 | Program loaded: {ProgramName}", _currentProgram.Name);
}
catch (Exception ex)
{
_logger.Error(ex, "加载程序失败 | Failed to load program");
}
}
/// <summary>
/// 创建新程序 | Create a new program
/// </summary>
private void ExecuteNewProgram()
{
var name = string.IsNullOrWhiteSpace(ProgramName) ? "新程序" : ProgramName;
_currentProgram = _cncProgramService.CreateProgram(name);
ProgramName = _currentProgram.Name;
IsModified = false;
RefreshNodes();
_logger.Info("已创建新程序 | Created new program: {ProgramName}", name);
}
/// <summary>
/// 导出 CSV(占位实现)| Export CSV (placeholder)
/// </summary>
private void ExecuteExportCsv()
{
// TODO: 实现 CSV 导出功能 | Implement CSV export functionality
_logger.Info("CSV 导出功能尚未实现 | CSV export not yet implemented");
}
// ── 辅助方法 | Helper methods ───────────────────────────────────
/// <summary>
/// 编辑操作后的统一处理:刷新节点、标记已修改、发布变更事件
/// Unified post-edit handling: refresh nodes, mark modified, publish change event
/// </summary>
private void OnProgramEdited()
{
IsModified = true;
RefreshNodes();
PublishProgramChanged();
}
/// <summary>
/// 从 _currentProgram.Nodes 重建 Nodes 集合
/// Rebuild the Nodes collection from _currentProgram.Nodes
/// </summary>
private void RefreshNodes()
{
Nodes.Clear();
if (_currentProgram?.Nodes == 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();
}
}
/// <summary>
/// 通过 IEventAggregator 发布 CNC 程序变更事件
/// Publish CNC program changed event via IEventAggregator
/// </summary>
private void PublishProgramChanged()
{
_eventAggregator
.GetEvent<CncProgramChangedEvent>()
.Publish(new CncProgramChangedPayload(ProgramName ?? string.Empty, IsModified));
}
}
}
@@ -0,0 +1,89 @@
using Prism.Mvvm;
using XplorePlane.Models;
namespace XplorePlane.ViewModels.Cnc
{
/// <summary>
/// CNC 节点 ViewModel,将 CncNode 模型封装为可绑定的 WPF ViewModel
/// CNC node ViewModel that wraps a CncNode model into a bindable WPF ViewModel
/// </summary>
public class CncNodeViewModel : BindableBase
{
private int _index;
private string _name;
private CncNodeType _nodeType;
private string _icon;
/// <summary>
/// 构造函数,从 CncNode 模型初始化 ViewModel
/// Constructor that initializes the ViewModel from a CncNode model
/// </summary>
public CncNodeViewModel(CncNode model)
{
Model = model;
_index = model.Index;
_name = model.Name;
_nodeType = model.NodeType;
_icon = GetIconForNodeType(model.NodeType);
}
/// <summary>底层 CNC 节点模型(只读)| Underlying CNC node model (read-only)</summary>
public CncNode Model { get; }
/// <summary>节点在程序中的索引 | Node index in the program</summary>
public int Index
{
get => _index;
set => SetProperty(ref _index, value);
}
/// <summary>节点显示名称 | Node display name</summary>
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
/// <summary>节点类型 | Node type</summary>
public CncNodeType NodeType
{
get => _nodeType;
set
{
if (SetProperty(ref _nodeType, value))
{
// 类型变更时自动更新图标 | Auto-update icon when type changes
Icon = GetIconForNodeType(value);
}
}
}
/// <summary>节点图标路径 | Node icon path</summary>
public string Icon
{
get => _icon;
set => SetProperty(ref _icon, value);
}
/// <summary>
/// 根据节点类型返回对应的图标路径
/// Returns the icon path for the given node type
/// </summary>
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",
};
}
}
}
@@ -0,0 +1,96 @@
using Prism.Mvvm;
using XplorePlane.Models;
namespace XplorePlane.ViewModels.Cnc
{
/// <summary>
/// 矩阵单元格 ViewModel,将 MatrixCell 模型封装为可绑定的 WPF ViewModel
/// Matrix cell ViewModel that wraps a MatrixCell model into a bindable WPF ViewModel
/// </summary>
public class MatrixCellViewModel : BindableBase
{
private int _row;
private int _column;
private double _offsetX;
private double _offsetY;
private bool _isEnabled;
private MatrixCellStatus _status;
private string _errorMessage;
private bool _isSelected;
/// <summary>
/// 构造函数,从 MatrixCell 模型初始化 ViewModel
/// Constructor that initializes the ViewModel from a MatrixCell model
/// </summary>
public MatrixCellViewModel(MatrixCell model)
{
Model = model;
_row = model.Row;
_column = model.Column;
_offsetX = model.OffsetX;
_offsetY = model.OffsetY;
_isEnabled = model.IsEnabled;
_status = model.Status;
_errorMessage = model.ErrorMessage;
}
/// <summary>底层矩阵单元格模型(只读)| Underlying matrix cell model (read-only)</summary>
public MatrixCell Model { get; }
/// <summary>单元格行索引 | Cell row index</summary>
public int Row
{
get => _row;
set => SetProperty(ref _row, value);
}
/// <summary>单元格列索引 | Cell column index</summary>
public int Column
{
get => _column;
set => SetProperty(ref _column, value);
}
/// <summary>计算后的 X 偏移量 | Calculated X offset</summary>
public double OffsetX
{
get => _offsetX;
set => SetProperty(ref _offsetX, value);
}
/// <summary>计算后的 Y 偏移量 | Calculated Y offset</summary>
public double OffsetY
{
get => _offsetY;
set => SetProperty(ref _offsetY, value);
}
/// <summary>单元格是否启用检测 | Whether the cell is enabled for inspection</summary>
public bool IsEnabled
{
get => _isEnabled;
set => SetProperty(ref _isEnabled, value);
}
/// <summary>单元格执行状态 | Cell execution status</summary>
public MatrixCellStatus Status
{
get => _status;
set => SetProperty(ref _status, value);
}
/// <summary>错误信息 | Error message if any</summary>
public string ErrorMessage
{
get => _errorMessage;
set => SetProperty(ref _errorMessage, value);
}
/// <summary>单元格是否被选中 | Whether the cell is currently selected in the UI</summary>
public bool IsSelected
{
get => _isSelected;
set => SetProperty(ref _isSelected, value);
}
}
}
@@ -0,0 +1,308 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using Prism.Commands;
using Prism.Events;
using Prism.Mvvm;
using XP.Common.Logging.Interfaces;
using XplorePlane.Models;
using XplorePlane.Services.Cnc;
using XplorePlane.Services.Matrix;
namespace XplorePlane.ViewModels.Cnc
{
/// <summary>
/// 矩阵编辑器 ViewModel,管理矩阵布局的配置、单元格网格和文件操作
/// Matrix editor ViewModel that manages layout configuration, cell grid and file operations
/// </summary>
public class MatrixEditorViewModel : BindableBase
{
private readonly IMatrixService _matrixService;
private readonly ICncProgramService _cncProgramService;
private readonly IEventAggregator _eventAggregator;
private readonly ILoggerService _logger;
// 当前矩阵布局 | Current matrix layout
private MatrixLayout _currentLayout;
private int _rows = 1;
private int _columns = 1;
private double _rowSpacing;
private double _columnSpacing;
private ObservableCollection<MatrixCellViewModel> _cells;
private MatrixCellViewModel _selectedCell;
private string _associatedProgramPath;
/// <summary>
/// 构造函数 | Constructor
/// </summary>
public MatrixEditorViewModel(
IMatrixService matrixService,
ICncProgramService cncProgramService,
IEventAggregator eventAggregator,
ILoggerService logger)
{
_matrixService = matrixService ?? throw new ArgumentNullException(nameof(matrixService));
_cncProgramService = cncProgramService ?? throw new ArgumentNullException(nameof(cncProgramService));
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule<MatrixEditorViewModel>();
_cells = new ObservableCollection<MatrixCellViewModel>();
// ── 命令初始化 | Command initialization ──
UpdateLayoutCommand = new DelegateCommand(ExecuteUpdateLayout);
ToggleCellCommand = new DelegateCommand<MatrixCellViewModel>(ExecuteToggleCell);
SelectCellCommand = new DelegateCommand<MatrixCellViewModel>(ExecuteSelectCell);
AssociateProgramCommand = new DelegateCommand(async () => await ExecuteAssociateProgramAsync());
SaveLayoutCommand = new DelegateCommand(async () => await ExecuteSaveLayoutAsync());
LoadLayoutCommand = new DelegateCommand(async () => await ExecuteLoadLayoutAsync());
_logger.Info("MatrixEditorViewModel 已初始化 | MatrixEditorViewModel initialized");
}
// ── 属性 | Properties ──────────────────────────────────────────
/// <summary>矩阵行数 | Number of rows</summary>
public int Rows
{
get => _rows;
set => SetProperty(ref _rows, value);
}
/// <summary>矩阵列数 | Number of columns</summary>
public int Columns
{
get => _columns;
set => SetProperty(ref _columns, value);
}
/// <summary>行间距(mm| Row spacing in mm</summary>
public double RowSpacing
{
get => _rowSpacing;
set => SetProperty(ref _rowSpacing, value);
}
/// <summary>列间距(mm| Column spacing in mm</summary>
public double ColumnSpacing
{
get => _columnSpacing;
set => SetProperty(ref _columnSpacing, value);
}
/// <summary>矩阵单元格集合 | Matrix cell collection</summary>
public ObservableCollection<MatrixCellViewModel> Cells
{
get => _cells;
set => SetProperty(ref _cells, value);
}
/// <summary>当前选中的单元格 | Currently selected cell</summary>
public MatrixCellViewModel SelectedCell
{
get => _selectedCell;
set => SetProperty(ref _selectedCell, value);
}
/// <summary>关联的 CNC 程序路径 | Associated CNC program path</summary>
public string AssociatedProgramPath
{
get => _associatedProgramPath;
set => SetProperty(ref _associatedProgramPath, value);
}
// ── 命令 | Commands ────────────────────────────────────────────
/// <summary>更新矩阵布局命令 | Update matrix layout command</summary>
public DelegateCommand UpdateLayoutCommand { get; }
/// <summary>切换单元格启用状态命令 | Toggle cell enabled state command</summary>
public DelegateCommand<MatrixCellViewModel> ToggleCellCommand { get; }
/// <summary>选中单元格命令 | Select cell command</summary>
public DelegateCommand<MatrixCellViewModel> SelectCellCommand { get; }
/// <summary>关联 CNC 程序命令 | Associate CNC program command</summary>
public DelegateCommand AssociateProgramCommand { get; }
/// <summary>保存矩阵布局命令 | Save matrix layout command</summary>
public DelegateCommand SaveLayoutCommand { get; }
/// <summary>加载矩阵布局命令 | Load matrix layout command</summary>
public DelegateCommand LoadLayoutCommand { get; }
// ── 命令执行方法 | Command execution methods ────────────────────
/// <summary>
/// 更新矩阵布局:如果当前无布局则创建,否则更新现有布局
/// Update matrix layout: create if none exists, otherwise update existing
/// </summary>
private void ExecuteUpdateLayout()
{
try
{
if (_currentLayout == null)
{
_currentLayout = _matrixService.CreateLayout(Rows, Columns, RowSpacing, ColumnSpacing);
}
else
{
_currentLayout = _matrixService.UpdateLayout(_currentLayout, Rows, Columns, RowSpacing, ColumnSpacing);
}
RefreshCells();
_logger.Info("已更新矩阵布局 | Updated matrix layout: Rows={Rows}, Columns={Columns}", Rows, Columns);
}
catch (Exception ex)
{
_logger.Error(ex, "更新矩阵布局失败 | Failed to update matrix layout");
}
}
/// <summary>
/// 选中指定单元格,更新高亮状态
/// Select the specified cell and update highlight state
/// </summary>
private void ExecuteSelectCell(MatrixCellViewModel cellVm)
{
if (cellVm == null)
return;
// 取消之前选中的单元格 | Deselect previously selected cell
if (SelectedCell != null)
SelectedCell.IsSelected = false;
cellVm.IsSelected = true;
SelectedCell = cellVm;
}
/// <summary>
/// 切换指定单元格的启用/禁用状态
/// Toggle the enabled/disabled state of the specified cell
/// </summary>
private void ExecuteToggleCell(MatrixCellViewModel cellVm)
{
if (_currentLayout == null || cellVm == null)
return;
try
{
_currentLayout = _matrixService.ToggleCellEnabled(_currentLayout, cellVm.Row, cellVm.Column);
RefreshCells();
_logger.Info("已切换单元格状态 | Toggled cell state: Row={Row}, Column={Column}", cellVm.Row, cellVm.Column);
}
catch (Exception ex)
{
_logger.Error(ex, "切换单元格状态失败 | Failed to toggle cell state");
}
}
/// <summary>
/// 关联 CNC 程序到当前矩阵布局(占位:从 AssociatedProgramPath 加载)
/// Associate a CNC program with the current layout (placeholder: loads from AssociatedProgramPath)
/// </summary>
private async Task ExecuteAssociateProgramAsync()
{
if (_currentLayout == null)
{
_logger.Warn("无法关联程序:当前无矩阵布局 | Cannot associate program: no current layout");
return;
}
if (string.IsNullOrWhiteSpace(AssociatedProgramPath))
{
_logger.Warn("无法关联程序:程序路径为空 | Cannot associate program: program path is empty");
return;
}
try
{
var program = await _cncProgramService.LoadAsync(AssociatedProgramPath);
_currentLayout = _matrixService.AssociateCncProgram(_currentLayout, program);
_logger.Info("已关联 CNC 程序 | Associated CNC program: {ProgramPath}", AssociatedProgramPath);
}
catch (Exception ex)
{
_logger.Error(ex, "关联 CNC 程序失败 | Failed to associate CNC program");
}
}
/// <summary>
/// 保存当前矩阵布局到文件 | Save current matrix layout to file
/// </summary>
private async Task ExecuteSaveLayoutAsync()
{
if (_currentLayout == null)
{
_logger.Warn("无法保存:当前无矩阵布局 | Cannot save: no current layout");
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);
}
catch (Exception ex)
{
_logger.Error(ex, "保存矩阵布局失败 | Failed to save matrix layout");
}
}
/// <summary>
/// 从文件加载矩阵布局 | Load matrix layout from file
/// </summary>
private async Task ExecuteLoadLayoutAsync()
{
try
{
// 占位:实际应通过文件对话框选择路径 | Placeholder: should use file dialog to select path
var filePath = $"matrix_layout.json";
_currentLayout = await _matrixService.LoadAsync(filePath);
// 同步属性到 ViewModel | Sync properties to ViewModel
Rows = _currentLayout.Rows;
Columns = _currentLayout.Columns;
RowSpacing = _currentLayout.RowSpacing;
ColumnSpacing = _currentLayout.ColumnSpacing;
AssociatedProgramPath = _currentLayout.CncProgramPath;
RefreshCells();
_logger.Info("矩阵布局已加载 | Matrix layout loaded: Rows={Rows}, Columns={Columns}", Rows, Columns);
}
catch (Exception ex)
{
_logger.Error(ex, "加载矩阵布局失败 | Failed to load matrix layout");
}
}
// ── 辅助方法 | Helper methods ───────────────────────────────────
/// <summary>
/// 从 _currentLayout.Cells 重建 Cells 集合
/// Rebuild the Cells collection from _currentLayout.Cells
/// </summary>
private void RefreshCells()
{
Cells.Clear();
if (_currentLayout?.Cells == null)
return;
foreach (var cell in _currentLayout.Cells)
{
Cells.Add(new MatrixCellViewModel(cell));
}
// 尝试保持选中状态 | Try to preserve selection
if (SelectedCell != null)
{
var match = Cells.FirstOrDefault(c => c.Row == SelectedCell.Row && c.Column == SelectedCell.Column);
SelectedCell = match ?? Cells.FirstOrDefault();
}
}
}
}
@@ -0,0 +1,130 @@
using System;
using System.Threading.Tasks;
using Prism.Commands;
using Prism.Mvvm;
using XP.Common.Logging.Interfaces;
using XplorePlane.Services.Measurement;
namespace XplorePlane.ViewModels.Cnc
{
/// <summary>
/// 测量统计 ViewModel,显示测量统计数据并支持筛选
/// Measurement statistics ViewModel that displays measurement statistics and supports filtering
/// </summary>
public class MeasurementStatsViewModel : BindableBase
{
private readonly IMeasurementDataService _measurementDataService;
private readonly ILoggerService _logger;
// ── 统计属性字段 | Statistics property fields ──
private int _totalCount;
private int _passCount;
private int _failCount;
private double _passRate;
// ── 筛选属性字段 | Filter property fields ──
private string _recipeName;
private DateTime? _fromDate;
private DateTime? _toDate;
/// <summary>
/// 构造函数 | Constructor
/// </summary>
public MeasurementStatsViewModel(
IMeasurementDataService measurementDataService,
ILoggerService logger)
{
_measurementDataService = measurementDataService ?? throw new ArgumentNullException(nameof(measurementDataService));
_logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule<MeasurementStatsViewModel>();
// ── 命令初始化 | Command initialization ──
RefreshCommand = new DelegateCommand(async () => await ExecuteRefreshAsync());
_logger.Info("MeasurementStatsViewModel 已初始化 | MeasurementStatsViewModel initialized");
}
// ── 统计属性 | Statistics properties ────────────────────────────
/// <summary>总测量数 | Total measurement count</summary>
public int TotalCount
{
get => _totalCount;
set => SetProperty(ref _totalCount, value);
}
/// <summary>合格数 | Pass count</summary>
public int PassCount
{
get => _passCount;
set => SetProperty(ref _passCount, value);
}
/// <summary>不合格数 | Fail count</summary>
public int FailCount
{
get => _failCount;
set => SetProperty(ref _failCount, value);
}
/// <summary>合格率(0.0 ~ 1.0| Pass rate (0.0 to 1.0)</summary>
public double PassRate
{
get => _passRate;
set => SetProperty(ref _passRate, value);
}
// ── 筛选属性 | Filter properties ────────────────────────────────
/// <summary>按配方名称筛选 | Filter by recipe name</summary>
public string RecipeName
{
get => _recipeName;
set => SetProperty(ref _recipeName, value);
}
/// <summary>筛选起始日期 | Filter start date</summary>
public DateTime? FromDate
{
get => _fromDate;
set => SetProperty(ref _fromDate, value);
}
/// <summary>筛选结束日期 | Filter end date</summary>
public DateTime? ToDate
{
get => _toDate;
set => SetProperty(ref _toDate, value);
}
// ── 命令 | Commands ─────────────────────────────────────────────
/// <summary>刷新统计数据命令 | Refresh statistics command</summary>
public DelegateCommand RefreshCommand { get; }
// ── 命令执行方法 | Command execution methods ────────────────────
/// <summary>
/// 从服务获取统计数据并更新属性
/// Fetch statistics from service and update properties
/// </summary>
private async Task ExecuteRefreshAsync()
{
try
{
var stats = await _measurementDataService.GetStatisticsAsync(RecipeName, FromDate, ToDate);
TotalCount = stats.TotalCount;
PassCount = stats.PassCount;
FailCount = stats.FailCount;
PassRate = stats.PassRate;
_logger.Info("统计数据已刷新 | Statistics refreshed: Total={TotalCount}, Pass={PassCount}, Fail={FailCount}, Rate={PassRate:P1}",
stats.TotalCount, stats.PassCount, stats.FailCount, stats.PassRate);
}
catch (Exception ex)
{
_logger.Error(ex, "刷新统计数据失败 | Failed to refresh statistics");
}
}
}
}
@@ -26,6 +26,8 @@ namespace XplorePlane.ViewModels
public DelegateCommand EditPropertiesCommand { get; set; } public DelegateCommand EditPropertiesCommand { get; set; }
public DelegateCommand OpenImageProcessingCommand { get; set; } public DelegateCommand OpenImageProcessingCommand { get; set; }
public DelegateCommand OpenPipelineEditorCommand { get; set; } public DelegateCommand OpenPipelineEditorCommand { get; set; }
public DelegateCommand OpenCncEditorCommand { get; set; }
public DelegateCommand OpenMatrixEditorCommand { get; set; }
public MainViewModel(ILoggerService logger) public MainViewModel(ILoggerService logger)
{ {
@@ -53,6 +55,20 @@ namespace XplorePlane.ViewModels
_logger.Info("流水线编辑器窗口已打开"); _logger.Info("流水线编辑器窗口已打开");
}); });
OpenCncEditorCommand = new DelegateCommand(() =>
{
var window = new Views.Cnc.CncEditorWindow();
window.Show();
_logger.Info("CNC 编辑器窗口已打开");
});
OpenMatrixEditorCommand = new DelegateCommand(() =>
{
var window = new Views.Cnc.MatrixEditorWindow();
window.Show();
_logger.Info("矩阵编排窗口已打开");
});
_logger.Info("MainViewModel 已初始化"); _logger.Info("MainViewModel 已初始化");
} }
@@ -0,0 +1,14 @@
<Window
x:Class="XplorePlane.Views.Cnc.CncEditorWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cnc="clr-namespace:XplorePlane.Views.Cnc"
Title="CNC 编辑器"
Width="1200"
Height="750"
MinWidth="900"
MinHeight="550"
ShowInTaskbar="False"
WindowStartupLocation="CenterOwner">
<cnc:CncPageView />
</Window>
@@ -0,0 +1,12 @@
using System.Windows;
namespace XplorePlane.Views.Cnc
{
public partial class CncEditorWindow : Window
{
public CncEditorWindow()
{
InitializeComponent();
}
}
}
+297
View File
@@ -0,0 +1,297 @@
<!-- CNC 编辑器主页面视图 | CNC editor main page view -->
<UserControl
x:Class="XplorePlane.Views.Cnc.CncPageView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://prismlibrary.com/"
d:DesignHeight="700"
d:DesignWidth="900"
prism:ViewModelLocator.AutoWireViewModel="True"
mc:Ignorable="d">
<UserControl.Resources>
<!-- 面板背景和边框颜色 | Panel background and border colors -->
<SolidColorBrush x:Key="PanelBg" Color="White" />
<SolidColorBrush x:Key="PanelBorder" Color="#cdcbcb" />
<SolidColorBrush x:Key="SeparatorBrush" Color="#E0E0E0" />
<SolidColorBrush x:Key="AccentBlue" Color="#E3F0FF" />
<FontFamily x:Key="CsdFont">Microsoft YaHei UI</FontFamily>
<!-- 节点列表项样式 | Node list item style -->
<Style x:Key="CncNodeItemStyle" TargetType="ListBoxItem">
<Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="0" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
<!-- 工具栏按钮样式 | Toolbar button style -->
<Style x:Key="ToolbarBtn" TargetType="Button">
<Setter Property="Height" Value="28" />
<Setter Property="Margin" Value="2,0" />
<Setter Property="Padding" Value="6,0" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="#cdcbcb" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="FontFamily" Value="Microsoft YaHei UI" />
<Setter Property="FontSize" Value="11" />
<Setter Property="Cursor" Value="Hand" />
</Style>
</UserControl.Resources>
<Border
Background="{StaticResource PanelBg}"
BorderBrush="{StaticResource PanelBorder}"
BorderThickness="1"
CornerRadius="4">
<Grid>
<Grid.RowDefinitions>
<!-- Row 0: 工具栏 | Toolbar -->
<RowDefinition Height="Auto" />
<!-- Row 1: 主内容区(左侧节点列表 + 右侧参数面板)| Main content (left: node list, right: parameter panel) -->
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- ═══ 工具栏:节点插入命令 + 文件操作命令 | Toolbar: node insert commands + file operation commands ═══ -->
<Border
Grid.Row="0"
Padding="6,4"
Background="#F5F5F5"
BorderBrush="{StaticResource PanelBorder}"
BorderThickness="0,0,0,1">
<WrapPanel Orientation="Horizontal">
<!-- 文件操作按钮 | File operation buttons -->
<Button
Command="{Binding NewProgramCommand}"
Content="新建"
Style="{StaticResource ToolbarBtn}"
ToolTip="新建程序 | New Program" />
<Button
Command="{Binding SaveProgramCommand}"
Content="保存"
Style="{StaticResource ToolbarBtn}"
ToolTip="保存程序 | Save Program" />
<Button
Command="{Binding LoadProgramCommand}"
Content="加载"
Style="{StaticResource ToolbarBtn}"
ToolTip="加载程序 | Load Program" />
<Button
Command="{Binding ExportCsvCommand}"
Content="导出CSV"
Style="{StaticResource ToolbarBtn}"
ToolTip="导出 CSV | Export CSV" />
<!-- 分隔线 | Separator -->
<Rectangle
Width="1"
Height="20"
Margin="4,0"
Fill="{StaticResource SeparatorBrush}" />
<!-- 节点插入按钮(9 种节点类型)| Node insert buttons (9 node types) -->
<Button
Command="{Binding InsertReferencePointCommand}"
Content="参考点"
Style="{StaticResource ToolbarBtn}"
ToolTip="插入参考点 | Insert Reference Point" />
<Button
Command="{Binding InsertSaveNodeWithImageCommand}"
Content="保存+图"
Style="{StaticResource ToolbarBtn}"
ToolTip="保存节点并保存图片 | Save Node With Image" />
<Button
Command="{Binding InsertSaveNodeCommand}"
Content="保存节点"
Style="{StaticResource ToolbarBtn}"
ToolTip="仅保存节点 | Save Node" />
<Button
Command="{Binding InsertSavePositionCommand}"
Content="保存位置"
Style="{StaticResource ToolbarBtn}"
ToolTip="保存位置 | Save Position" />
<Button
Command="{Binding InsertInspectionModuleCommand}"
Content="检测模块"
Style="{StaticResource ToolbarBtn}"
ToolTip="插入检测模块 | Insert Inspection Module" />
<Button
Command="{Binding InsertInspectionMarkerCommand}"
Content="检测标记"
Style="{StaticResource ToolbarBtn}"
ToolTip="插入检测标记 | Insert Inspection Marker" />
<Button
Command="{Binding InsertPauseDialogCommand}"
Content="停顿"
Style="{StaticResource ToolbarBtn}"
ToolTip="插入停顿对话框 | Insert Pause Dialog" />
<Button
Command="{Binding InsertWaitDelayCommand}"
Content="延时"
Style="{StaticResource ToolbarBtn}"
ToolTip="设置等待延时 | Insert Wait Delay" />
<Button
Command="{Binding InsertCompleteProgramCommand}"
Content="完成"
Style="{StaticResource ToolbarBtn}"
ToolTip="完成程序 | Complete Program" />
</WrapPanel>
</Border>
<!-- ═══ 主内容区:左侧节点列表 + 右侧参数面板 | Main content: left node list + right parameter panel ═══ -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<!-- 左侧:节点列表 | Left: node list -->
<ColumnDefinition Width="3*" MinWidth="200" />
<!-- 分隔线 | Splitter -->
<ColumnDefinition Width="Auto" />
<!-- 右侧:参数面板 | Right: parameter panel -->
<ColumnDefinition Width="2*" MinWidth="200" />
</Grid.ColumnDefinitions>
<!-- ── 左侧:CNC 节点列表 | Left: CNC node list ── -->
<ListBox
x:Name="CncNodeListBox"
Grid.Column="0"
Background="Transparent"
BorderThickness="0"
ItemContainerStyle="{StaticResource CncNodeItemStyle}"
ItemsSource="{Binding Nodes}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
SelectedItem="{Binding SelectedNode, Mode=TwoWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid x:Name="NodeRoot" MinHeight="40">
<Grid.ColumnDefinitions>
<!-- 图标列 | Icon column -->
<ColumnDefinition Width="40" />
<!-- 名称列 | Name column -->
<ColumnDefinition Width="*" />
<!-- 操作按钮列 | Action buttons column -->
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- 节点图标 | Node icon -->
<Border
Grid.Column="0"
Width="28"
Height="28"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="#E8F0FE"
BorderBrush="#5B9BD5"
BorderThickness="1.5"
CornerRadius="4">
<Image
Width="20"
Height="20"
Source="{Binding Icon}"
Stretch="Uniform" />
</Border>
<!-- 节点序号和名称 | Node index and name -->
<StackPanel
Grid.Column="1"
Margin="6,0,0,0"
VerticalAlignment="Center"
Orientation="Horizontal">
<TextBlock
VerticalAlignment="Center"
FontFamily="Microsoft YaHei UI"
FontSize="11"
Foreground="#888"
Text="{Binding Index, StringFormat='[{0}] '}" />
<TextBlock
VerticalAlignment="Center"
FontFamily="Microsoft YaHei UI"
FontSize="12"
Text="{Binding Name}" />
</StackPanel>
<!-- 悬停操作按钮:上移 / 下移 / 删除 | Hover actions: MoveUp / MoveDown / Delete -->
<StackPanel
x:Name="NodeActions"
Grid.Column="2"
Margin="0,0,4,0"
VerticalAlignment="Center"
Orientation="Horizontal"
Visibility="Collapsed">
<Button
Width="22"
Height="22"
Margin="1,0"
Background="Transparent"
BorderBrush="#cdcbcb"
BorderThickness="1"
Command="{Binding DataContext.MoveNodeUpCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
CommandParameter="{Binding}"
Content="▲"
Cursor="Hand"
FontSize="10"
ToolTip="上移 | Move Up" />
<Button
Width="22"
Height="22"
Margin="1,0"
Background="Transparent"
BorderBrush="#cdcbcb"
BorderThickness="1"
Command="{Binding DataContext.MoveNodeDownCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
CommandParameter="{Binding}"
Content="▼"
Cursor="Hand"
FontSize="10"
ToolTip="下移 | Move Down" />
<Button
Width="22"
Height="22"
Margin="1,0"
Background="Transparent"
BorderBrush="#E05050"
BorderThickness="1"
Command="{Binding DataContext.DeleteNodeCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
Content="✕"
Cursor="Hand"
FontSize="10"
ToolTip="删除 | Delete" />
</StackPanel>
</Grid>
<DataTemplate.Triggers>
<!-- 鼠标悬停时显示操作按钮 | Show action buttons on mouse hover -->
<Trigger SourceName="NodeRoot" Property="IsMouseOver" Value="True">
<Setter TargetName="NodeActions" Property="Visibility" Value="Visible" />
</Trigger>
</DataTemplate.Triggers>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<!-- 垂直分隔线 | Vertical separator -->
<Rectangle
Grid.Column="1"
Width="1"
Fill="{StaticResource SeparatorBrush}" />
<!-- ── 右侧:参数面板(根据节点类型动态渲染)| Right: parameter panel (dynamic rendering by node type) ── -->
<ScrollViewer
Grid.Column="2"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<StackPanel Margin="8,6">
<TextBlock
Margin="0,0,0,4"
FontFamily="{StaticResource CsdFont}"
FontSize="11"
FontWeight="Bold"
Foreground="#555"
Text="参数配置 | Parameters" />
<!-- 动态参数内容区域(占位:根据 SelectedNode 类型渲染)| Dynamic parameter content area (placeholder for node-type-based rendering) -->
<ContentControl Content="{Binding SelectedNode}" />
</StackPanel>
</ScrollViewer>
</Grid>
</Grid>
</Border>
</UserControl>
+16
View File
@@ -0,0 +1,16 @@
using System.Windows.Controls;
namespace XplorePlane.Views.Cnc
{
/// <summary>
/// CNC 编辑器主页面视图(MVVM 模式,逻辑在 ViewModel 中)
/// CNC editor main page view (MVVM pattern, logic in ViewModel)
/// </summary>
public partial class CncPageView : UserControl
{
public CncPageView()
{
InitializeComponent();
}
}
}
@@ -0,0 +1,14 @@
<Window
x:Class="XplorePlane.Views.Cnc.MatrixEditorWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cnc="clr-namespace:XplorePlane.Views.Cnc"
Title="矩阵编排"
Width="1200"
Height="750"
MinWidth="900"
MinHeight="550"
ShowInTaskbar="False"
WindowStartupLocation="CenterOwner">
<cnc:MatrixPageView />
</Window>
@@ -0,0 +1,12 @@
using System.Windows;
namespace XplorePlane.Views.Cnc
{
public partial class MatrixEditorWindow : Window
{
public MatrixEditorWindow()
{
InitializeComponent();
}
}
}
+293
View File
@@ -0,0 +1,293 @@
<!-- 矩阵编排页面视图 | Matrix arrangement page view -->
<UserControl
x:Class="XplorePlane.Views.Cnc.MatrixPageView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://prismlibrary.com/"
d:DesignHeight="700"
d:DesignWidth="900"
prism:ViewModelLocator.AutoWireViewModel="True"
mc:Ignorable="d">
<UserControl.Resources>
<!-- 面板背景和边框颜色(与 CncPageView 一致)| Panel background and border colors (consistent with CncPageView) -->
<SolidColorBrush x:Key="PanelBg" Color="White" />
<SolidColorBrush x:Key="PanelBorder" Color="#cdcbcb" />
<SolidColorBrush x:Key="SeparatorBrush" Color="#E0E0E0" />
<SolidColorBrush x:Key="AccentBlue" Color="#E3F0FF" />
<FontFamily x:Key="CsdFont">Microsoft YaHei UI</FontFamily>
<!-- 配置面板标签样式 | Configuration panel label style -->
<Style x:Key="ConfigLabel" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Microsoft YaHei UI" />
<Setter Property="FontSize" Value="12" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Margin" Value="0,0,6,0" />
</Style>
<!-- 配置面板输入框样式 | Configuration panel input style -->
<Style x:Key="ConfigInput" TargetType="TextBox">
<Setter Property="FontFamily" Value="Microsoft YaHei UI" />
<Setter Property="FontSize" Value="12" />
<Setter Property="Height" Value="26" />
<Setter Property="Padding" Value="4,2" />
<Setter Property="BorderBrush" Value="#cdcbcb" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
<!-- 工具栏按钮样式(与 CncPageView 一致)| Toolbar button style (consistent with CncPageView) -->
<Style x:Key="ToolbarBtn" TargetType="Button">
<Setter Property="Height" Value="28" />
<Setter Property="Margin" Value="2,4" />
<Setter Property="Padding" Value="10,0" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="#cdcbcb" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="FontFamily" Value="Microsoft YaHei UI" />
<Setter Property="FontSize" Value="11" />
<Setter Property="Cursor" Value="Hand" />
</Style>
</UserControl.Resources>
<Border
Background="{StaticResource PanelBg}"
BorderBrush="{StaticResource PanelBorder}"
BorderThickness="1"
CornerRadius="4">
<Grid>
<Grid.ColumnDefinitions>
<!-- 左侧:配置面板 | Left: configuration panel -->
<ColumnDefinition Width="220" />
<!-- 分隔线 | Splitter -->
<ColumnDefinition Width="Auto" />
<!-- 右侧:矩阵网格 | Right: matrix grid -->
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- ═══ 左侧:矩阵配置面板 | Left: matrix configuration panel ═══ -->
<ScrollViewer
Grid.Column="0"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<StackPanel Margin="10,8">
<!-- 面板标题 | Panel title -->
<TextBlock
Margin="0,0,0,10"
FontFamily="{StaticResource CsdFont}"
FontSize="13"
FontWeight="Bold"
Foreground="#333"
Text="矩阵配置 | Matrix Config" />
<!-- 行数 | Rows -->
<TextBlock Style="{StaticResource ConfigLabel}" Text="行数 | Rows" />
<TextBox
Margin="0,2,0,8"
Style="{StaticResource ConfigInput}"
Text="{Binding Rows, UpdateSourceTrigger=PropertyChanged}"
ToolTip="矩阵行数 | Number of rows" />
<!-- 列数 | Columns -->
<TextBlock Style="{StaticResource ConfigLabel}" Text="列数 | Columns" />
<TextBox
Margin="0,2,0,8"
Style="{StaticResource ConfigInput}"
Text="{Binding Columns, UpdateSourceTrigger=PropertyChanged}"
ToolTip="矩阵列数 | Number of columns" />
<!-- 行间距 | Row Spacing -->
<TextBlock Style="{StaticResource ConfigLabel}" Text="行间距 (mm) | Row Spacing" />
<TextBox
Margin="0,2,0,8"
Style="{StaticResource ConfigInput}"
Text="{Binding RowSpacing, UpdateSourceTrigger=PropertyChanged}"
ToolTip="行间距(毫米)| Row spacing in mm" />
<!-- 列间距 | Column Spacing -->
<TextBlock Style="{StaticResource ConfigLabel}" Text="列间距 (mm) | Col Spacing" />
<TextBox
Margin="0,2,0,8"
Style="{StaticResource ConfigInput}"
Text="{Binding ColumnSpacing, UpdateSourceTrigger=PropertyChanged}"
ToolTip="列间距(毫米)| Column spacing in mm" />
<!-- 分隔线 | Separator -->
<Rectangle
Height="1"
Margin="0,4,0,8"
Fill="{StaticResource SeparatorBrush}" />
<!-- 操作按钮 | Action buttons -->
<Button
Command="{Binding UpdateLayoutCommand}"
Content="更新布局 | Update"
Style="{StaticResource ToolbarBtn}"
ToolTip="根据当前参数更新矩阵网格 | Update matrix grid with current parameters" />
<Button
Command="{Binding AssociateProgramCommand}"
Content="关联程序 | Associate"
Style="{StaticResource ToolbarBtn}"
ToolTip="关联 CNC 程序到矩阵 | Associate CNC program to matrix" />
<Button
Command="{Binding SaveLayoutCommand}"
Content="保存方案 | Save"
Style="{StaticResource ToolbarBtn}"
ToolTip="保存矩阵方案 | Save matrix layout" />
<Button
Command="{Binding LoadLayoutCommand}"
Content="加载方案 | Load"
Style="{StaticResource ToolbarBtn}"
ToolTip="加载矩阵方案 | Load matrix layout" />
<!-- 分隔线 | Separator -->
<Rectangle
Height="1"
Margin="0,8,0,8"
Fill="{StaticResource SeparatorBrush}" />
<!-- 选中单元格详情 | Selected cell details -->
<TextBlock
Margin="0,0,0,6"
FontFamily="{StaticResource CsdFont}"
FontSize="11"
FontWeight="Bold"
Foreground="#555"
Text="选中单元格 | Selected Cell" />
<StackPanel DataContext="{Binding SelectedCell}">
<TextBlock FontFamily="{StaticResource CsdFont}" FontSize="11">
<Run Foreground="#888" Text="位置 | Position: " />
<Run Text="{Binding Row, Mode=OneWay, StringFormat='R{0}'}" />
<Run Text=", " />
<Run Text="{Binding Column, Mode=OneWay, StringFormat='C{0}'}" />
</TextBlock>
<TextBlock FontFamily="{StaticResource CsdFont}" FontSize="11">
<Run Foreground="#888" Text="偏移 X | Offset X: " />
<Run Text="{Binding OffsetX, Mode=OneWay, StringFormat='{}{0:F2}'}" />
</TextBlock>
<TextBlock FontFamily="{StaticResource CsdFont}" FontSize="11">
<Run Foreground="#888" Text="偏移 Y | Offset Y: " />
<Run Text="{Binding OffsetY, Mode=OneWay, StringFormat='{}{0:F2}'}" />
</TextBlock>
<TextBlock FontFamily="{StaticResource CsdFont}" FontSize="11">
<Run Foreground="#888" Text="状态 | Enabled: " />
<Run Text="{Binding IsEnabled, Mode=OneWay}" />
</TextBlock>
</StackPanel>
</StackPanel>
</ScrollViewer>
<!-- 垂直分隔线 | Vertical separator -->
<Rectangle
Grid.Column="1"
Width="1"
Fill="{StaticResource SeparatorBrush}" />
<!-- ═══ 右侧:矩阵网格视图 | Right: matrix grid view ═══ -->
<ScrollViewer
Grid.Column="2"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<ItemsControl
Margin="8"
ItemsSource="{Binding Cells}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<!-- 使用 UniformGrid 按行列排列单元格 | Use UniformGrid to arrange cells by rows and columns -->
<UniformGrid Columns="{Binding Columns}" Rows="{Binding Rows}" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<!-- 单元格视觉模板 | Cell visual template -->
<Border
x:Name="CellBorder"
MinWidth="80"
MinHeight="64"
Margin="2"
Padding="4"
Background="#F0F8FF"
BorderBrush="#B0C4DE"
BorderThickness="1"
CornerRadius="3"
Cursor="Hand">
<Border.InputBindings>
<!-- 左键点击选中单元格 | Left click to select cell -->
<MouseBinding
Command="{Binding DataContext.SelectCellCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
CommandParameter="{Binding}"
MouseAction="LeftClick" />
</Border.InputBindings>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel
Grid.Row="0"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<!-- 行列索引 | Row/Column index -->
<TextBlock
HorizontalAlignment="Center"
FontFamily="Microsoft YaHei UI"
FontSize="11"
FontWeight="SemiBold"
Foreground="#333">
<Run Text="{Binding Row, Mode=OneWay, StringFormat='R{0}'}" />
<Run Text=" " />
<Run Text="{Binding Column, Mode=OneWay, StringFormat='C{0}'}" />
</TextBlock>
<!-- 偏移坐标 | Offset coordinates -->
<TextBlock
HorizontalAlignment="Center"
FontFamily="Microsoft YaHei UI"
FontSize="9"
Foreground="#666">
<Run Text="{Binding OffsetX, Mode=OneWay, StringFormat='X:{0:F1}'}" />
<Run Text=" " />
<Run Text="{Binding OffsetY, Mode=OneWay, StringFormat='Y:{0:F1}'}" />
</TextBlock>
</StackPanel>
<!-- 启用/禁用切换按钮 | Enable/Disable toggle button -->
<Button
x:Name="ToggleBtn"
Grid.Row="1"
HorizontalAlignment="Center"
Margin="0,2,0,0"
Padding="4,1"
Background="Transparent"
BorderBrush="#B0C4DE"
BorderThickness="1"
Command="{Binding DataContext.ToggleCellCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
CommandParameter="{Binding}"
Content="✓"
Cursor="Hand"
FontFamily="Microsoft YaHei UI"
FontSize="9"
ToolTip="切换启用/禁用 | Toggle enabled/disabled" />
</Grid>
</Border>
<DataTemplate.Triggers>
<!-- 禁用状态:灰色背景 | Disabled state: gray background -->
<DataTrigger Binding="{Binding IsEnabled}" Value="False">
<Setter TargetName="CellBorder" Property="Background" Value="#E8E8E8" />
<Setter TargetName="CellBorder" Property="BorderBrush" Value="#CCCCCC" />
<Setter TargetName="CellBorder" Property="Opacity" Value="0.6" />
</DataTrigger>
<!-- 选中状态:蓝色高亮 | Selected state: blue highlight -->
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<Setter TargetName="CellBorder" Property="Background" Value="#E3F0FF" />
<Setter TargetName="CellBorder" Property="BorderBrush" Value="#5B9BD5" />
<Setter TargetName="CellBorder" Property="BorderThickness" Value="2" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</Border>
</UserControl>
@@ -0,0 +1,16 @@
using System.Windows.Controls;
namespace XplorePlane.Views.Cnc
{
/// <summary>
/// 矩阵编排页面视图(MVVM 模式,逻辑在 ViewModel 中)
/// Matrix arrangement page view (MVVM pattern, logic in ViewModel)
/// </summary>
public partial class MatrixPageView : UserControl
{
public MatrixPageView()
{
InitializeComponent();
}
}
}
@@ -0,0 +1,189 @@
<!-- 测量统计视图 | Measurement statistics view -->
<UserControl
x:Class="XplorePlane.Views.Cnc.MeasurementStatsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://prismlibrary.com/"
d:DesignHeight="500"
d:DesignWidth="600"
prism:ViewModelLocator.AutoWireViewModel="True"
mc:Ignorable="d">
<UserControl.Resources>
<!-- 面板背景和边框颜色(与 CncPageView 一致)| Panel background and border colors (consistent with CncPageView) -->
<SolidColorBrush x:Key="PanelBg" Color="White" />
<SolidColorBrush x:Key="PanelBorder" Color="#cdcbcb" />
<SolidColorBrush x:Key="SeparatorBrush" Color="#E0E0E0" />
<SolidColorBrush x:Key="AccentBlue" Color="#E3F0FF" />
<FontFamily x:Key="CsdFont">Microsoft YaHei UI</FontFamily>
<!-- 标签样式 | Label style -->
<Style x:Key="StatsLabel" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Microsoft YaHei UI" />
<Setter Property="FontSize" Value="12" />
<Setter Property="Foreground" Value="#888" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Margin" Value="0,0,6,0" />
</Style>
<!-- 统计数值样式 | Statistics value style -->
<Style x:Key="StatsValue" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Microsoft YaHei UI" />
<Setter Property="FontSize" Value="20" />
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="Foreground" Value="#333" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<!-- 筛选输入框样式 | Filter input style -->
<Style x:Key="FilterInput" TargetType="TextBox">
<Setter Property="FontFamily" Value="Microsoft YaHei UI" />
<Setter Property="FontSize" Value="12" />
<Setter Property="Height" Value="26" />
<Setter Property="Padding" Value="4,2" />
<Setter Property="BorderBrush" Value="#cdcbcb" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
<!-- 工具栏按钮样式(与 CncPageView 一致)| Toolbar button style (consistent with CncPageView) -->
<Style x:Key="ToolbarBtn" TargetType="Button">
<Setter Property="Height" Value="28" />
<Setter Property="Margin" Value="2,4" />
<Setter Property="Padding" Value="10,0" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="#cdcbcb" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="FontFamily" Value="Microsoft YaHei UI" />
<Setter Property="FontSize" Value="11" />
<Setter Property="Cursor" Value="Hand" />
</Style>
</UserControl.Resources>
<Border
Background="{StaticResource PanelBg}"
BorderBrush="{StaticResource PanelBorder}"
BorderThickness="1"
CornerRadius="4">
<StackPanel Margin="16,12">
<!-- ═══ 标题 | Title ═══ -->
<TextBlock
Margin="0,0,0,12"
FontFamily="{StaticResource CsdFont}"
FontSize="14"
FontWeight="Bold"
Foreground="#333"
Text="测量统计 | Measurement Statistics" />
<!-- ═══ 统计数据展示区域 | Statistics display area ═══ -->
<Border
Margin="0,0,0,12"
Padding="12,10"
Background="{StaticResource AccentBlue}"
BorderBrush="{StaticResource PanelBorder}"
BorderThickness="1"
CornerRadius="4">
<UniformGrid Columns="4" Rows="1">
<!-- 总检测数 | Total count -->
<StackPanel HorizontalAlignment="Center">
<TextBlock Style="{StaticResource StatsLabel}" Text="总检测数 | Total" />
<TextBlock Style="{StaticResource StatsValue}" Text="{Binding TotalCount}" />
</StackPanel>
<!-- 合格数 | Pass count -->
<StackPanel HorizontalAlignment="Center">
<TextBlock Style="{StaticResource StatsLabel}" Text="合格数 | Pass" />
<TextBlock Foreground="#2E7D32" Style="{StaticResource StatsValue}" Text="{Binding PassCount}" />
</StackPanel>
<!-- 不合格数 | Fail count -->
<StackPanel HorizontalAlignment="Center">
<TextBlock Style="{StaticResource StatsLabel}" Text="不合格数 | Fail" />
<TextBlock Foreground="#C62828" Style="{StaticResource StatsValue}" Text="{Binding FailCount}" />
</StackPanel>
<!-- 合格率 | Pass rate -->
<StackPanel HorizontalAlignment="Center">
<TextBlock Style="{StaticResource StatsLabel}" Text="合格率 | Pass Rate" />
<TextBlock Style="{StaticResource StatsValue}" Text="{Binding PassRate, StringFormat='{}{0:P1}'}" />
</StackPanel>
</UniformGrid>
</Border>
<!-- 分隔线 | Separator -->
<Rectangle
Height="1"
Margin="0,0,0,12"
Fill="{StaticResource SeparatorBrush}" />
<!-- ═══ 筛选控件区域 | Filter controls area ═══ -->
<TextBlock
Margin="0,0,0,8"
FontFamily="{StaticResource CsdFont}"
FontSize="12"
FontWeight="Bold"
Foreground="#555"
Text="筛选条件 | Filters" />
<Grid Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- 配方名称 | Recipe name -->
<TextBlock
Grid.Column="0"
Style="{StaticResource StatsLabel}"
Text="配方名称 | Recipe:" />
<TextBox
Grid.Column="1"
Style="{StaticResource FilterInput}"
Text="{Binding RecipeName, UpdateSourceTrigger=PropertyChanged}"
ToolTip="按配方名称筛选 | Filter by recipe name" />
</Grid>
<Grid Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- 起始日期 | From date -->
<TextBlock
Grid.Column="0"
Style="{StaticResource StatsLabel}"
Text="起始日期 | From:" />
<DatePicker
Grid.Column="1"
Margin="0,0,12,0"
FontFamily="Microsoft YaHei UI"
FontSize="12"
SelectedDate="{Binding FromDate, Mode=TwoWay}"
ToolTip="筛选起始日期 | Filter start date" />
<!-- 结束日期 | To date -->
<TextBlock
Grid.Column="2"
Style="{StaticResource StatsLabel}"
Text="结束日期 | To:" />
<DatePicker
Grid.Column="3"
FontFamily="Microsoft YaHei UI"
FontSize="12"
SelectedDate="{Binding ToDate, Mode=TwoWay}"
ToolTip="筛选结束日期 | Filter end date" />
</Grid>
<!-- 刷新按钮 | Refresh button -->
<Button
HorizontalAlignment="Left"
Command="{Binding RefreshCommand}"
Content="刷新 | Refresh"
Style="{StaticResource ToolbarBtn}"
ToolTip="刷新统计数据 | Refresh statistics" />
</StackPanel>
</Border>
</UserControl>
@@ -0,0 +1,16 @@
using System.Windows.Controls;
namespace XplorePlane.Views.Cnc
{
/// <summary>
/// 测量统计视图(MVVM 模式,逻辑在 ViewModel 中)
/// Measurement statistics view (MVVM pattern, logic in ViewModel)
/// </summary>
public partial class MeasurementStatsView : UserControl
{
public MeasurementStatsView()
{
InitializeComponent();
}
}
}
+20 -12
View File
@@ -268,64 +268,72 @@
<telerik:RadRibbonGroup.Variants> <telerik:RadRibbonGroup.Variants>
<telerik:GroupVariant Priority="0" Variant="Large" /> <telerik:GroupVariant Priority="0" Variant="Large" />
</telerik:RadRibbonGroup.Variants> </telerik:RadRibbonGroup.Variants>
<!-- CNC 编辑器入口按钮 -->
<telerik:RadRibbonButton
telerik:ScreenTip.Description="打开 CNC 编辑器窗口,创建和编辑检测配方程序"
telerik:ScreenTip.Title="CNC 编辑器"
Command="{Binding OpenCncEditorCommand}"
Size="Large"
SmallImage="/Assets/Icons/cnc.png"
Text="CNC 编辑器" />
<!-- 矩阵编排入口按钮 -->
<telerik:RadRibbonButton
telerik:ScreenTip.Description="打开矩阵编排窗口,配置多工件阵列检测方案"
telerik:ScreenTip.Title="矩阵编排"
Command="{Binding OpenMatrixEditorCommand}"
Size="Large"
SmallImage="/Assets/Icons/matrix.png"
Text="矩阵编排" />
<!-- CNC 节点快捷工具 -->
<StackPanel> <StackPanel>
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Title="参考点" telerik:ScreenTip.Title="参考点"
Command="{Binding Path=SetStyle.Command}"
Size="Medium" Size="Medium"
SmallImage="/Assets/Icons/reference.png" SmallImage="/Assets/Icons/reference.png"
Text="参考点" /> Text="参考点" />
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Title="添加位置" telerik:ScreenTip.Title="添加位置"
Command="{Binding Path=SetStyle.Command}"
Size="Medium" Size="Medium"
SmallImage="/Assets/Icons/add-pos.png" SmallImage="/Assets/Icons/add-pos.png"
Text="添加位置" /> Text="添加位置" />
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Title="完成" telerik:ScreenTip.Title="完成"
Command="{Binding Path=SetStyle.Command}"
Size="Medium" Size="Medium"
SmallImage="/Assets/Icons/finish.png" SmallImage="/Assets/Icons/finish.png"
Text="完成" /> Text="完成" />
</StackPanel> </StackPanel>
<StackPanel> <StackPanel>
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Title="标记" telerik:ScreenTip.Title="标记"
Command="{Binding Path=SetStyle.Command}"
Size="Medium" Size="Medium"
SmallImage="/Assets/Icons/mark.png" SmallImage="/Assets/Icons/mark.png"
Text="标记" /> Text="标记" />
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Title="模块" telerik:ScreenTip.Title="模块"
Command="{Binding Path=SetStyle.Command}"
Size="Medium" Size="Medium"
SmallImage="/Assets/Icons/Module.png" SmallImage="/Assets/Icons/Module.png"
Text="模块" /> Text="模块" />
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Title="全部保存" telerik:ScreenTip.Title="全部保存"
Command="{Binding Path=SetStyle.Command}"
Size="Medium" Size="Medium"
SmallImage="/Assets/Icons/saveall.png" SmallImage="/Assets/Icons/saveall.png"
Text="全部保存" /> Text="全部保存" />
</StackPanel> </StackPanel>
<StackPanel> <StackPanel>
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Title="消息" telerik:ScreenTip.Title="消息"
Command="{Binding Path=SetStyle.Command}"
Size="Medium" Size="Medium"
SmallImage="/Assets/Icons/message.png" SmallImage="/Assets/Icons/message.png"
Text="消息" /> Text="消息" />
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Title="等待" telerik:ScreenTip.Title="等待"
Command="{Binding Path=SetStyle.Command}"
Size="Medium" Size="Medium"
SmallImage="/Assets/Icons/wait.png" SmallImage="/Assets/Icons/wait.png"
Text="等待" /> Text="等待" />
</StackPanel> </StackPanel>
<StackPanel />
</telerik:RadRibbonGroup> </telerik:RadRibbonGroup>
<telerik:RadRibbonGroup Header="高级模块" IsEnabled="{Binding Path=CellsGroup.IsEnabled}"> <telerik:RadRibbonGroup Header="高级模块" IsEnabled="{Binding Path=CellsGroup.IsEnabled}">
+2 -1
View File
@@ -47,7 +47,8 @@
2、将telerik 升级到 2024.1.408.310;调整界面和主题;引入 硬件层依赖 √ 2、将telerik 升级到 2024.1.408.310;调整界面和主题;引入 硬件层依赖 √
3、图像算子流程文件,保存文件后缀 .imw, image process workflow 缩写 √ 3、图像算子流程文件,保存文件后缀 .imw, image process workflow 缩写 √
4、CNC保存文件后缀为.xp, 表示 XplorePlane CNC file 的缩写 4、CNC保存文件后缀为.xp, 表示 XplorePlane CNC file 的缩写
5、硬件层射线源控件的初步集成(采用库层面的自定义控件方式) 5、硬件层射线源控件的初步集成(采用库层面的自定义控件方式)
PrismBootstrapper 的执行顺序是:RegisterTypes() → ConfigureModuleCatalog() → InitializeModules() → CreateShell()