已合并 PR 98: CNC界面的调整和仿真执行功能
1、调整CNC树形结构,将位置为一级节点;文件名使用label控件显示; 2、CNC执行功能调试,新增运动,探测器,射线源的仿真,点击执行,判断到位,取图,计算的逻辑 3、将CNC执行结果的值类型, 写入mainfest.json 
This commit is contained in:
@@ -109,3 +109,4 @@ dotnet build XplorePlane.sln -c Release
|
|||||||
- [x] 打通与硬件层的调用流程
|
- [x] 打通与硬件层的调用流程
|
||||||
- [x] 打通与图像层的调用流程
|
- [x] 打通与图像层的调用流程
|
||||||
- [ ] CNC的执行、存储逻辑的开发测试
|
- [ ] CNC的执行、存储逻辑的开发测试
|
||||||
|
- [ ] 涉及到图像校准,矩阵
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ namespace XP.Hardware.Detector.Config
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 合成帧宽度(像素)| Synthetic frame width (pixels)
|
/// 合成帧宽度(像素)| Synthetic frame width (pixels)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Width { get; set; } = 256;
|
public int Width { get; set; } = 2800;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 合成帧高度(像素)| Synthetic frame height (pixels)
|
/// 合成帧高度(像素)| Synthetic frame height (pixels)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Height { get; set; } = 256;
|
public int Height { get; set; } = 2800;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 模拟帧率(帧/秒)| Simulated frame rate (fps)
|
/// 模拟帧率(帧/秒)| Simulated frame rate (fps)
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ namespace XP.Hardware.Detector.Implementations
|
|||||||
ExposureTime = 0
|
ExposureTime = 0
|
||||||
};
|
};
|
||||||
|
|
||||||
_logger?.Info("[SimulatedDetector] 发布合成帧 #{N},分辨率 {W}x{H}", n, w, h);
|
//_logger?.Info("[SimulatedDetector] 发布合成帧 #{N},分辨率 {W}x{H}", n, w, h);
|
||||||
PublishImageCaptured(args);
|
PublishImageCaptured(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ namespace XP.Hardware.MotionControl.Implementations
|
|||||||
/// <param name="max">最大位置(mm)| Maximum position (mm)</param>
|
/// <param name="max">最大位置(mm)| Maximum position (mm)</param>
|
||||||
/// <param name="origin">原点偏移(mm)| Origin offset (mm)</param>
|
/// <param name="origin">原点偏移(mm)| Origin offset (mm)</param>
|
||||||
/// <param name="defaultSpeed">默认速度(mm/s)| Default speed (mm/s)</param>
|
/// <param name="defaultSpeed">默认速度(mm/s)| Default speed (mm/s)</param>
|
||||||
public SimulatedLinearAxis(AxisId axisId, double min, double max, double origin, double defaultSpeed = 50.0)
|
public SimulatedLinearAxis(AxisId axisId, double min, double max, double origin, double defaultSpeed = 5.0)
|
||||||
: base(axisId, min, max, origin)
|
: base(axisId, min, max, origin)
|
||||||
{
|
{
|
||||||
_defaultSpeed = defaultSpeed;
|
_defaultSpeed = defaultSpeed;
|
||||||
|
|||||||
@@ -0,0 +1,269 @@
|
|||||||
|
# CNC 执行机制说明
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
`CncExecutionService` 是 CNC 程序的核心执行引擎,负责按节点顺序执行整个检测程序。它协调运动控制、图像采集、流水线处理和结果归档等子系统,实现自动化多位置检测流程。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 类定义
|
||||||
|
|
||||||
|
| 类 | 文件 | 职责 |
|
||||||
|
|----|------|------|
|
||||||
|
| `CncExecutionService` | `Services/Cnc/CncExecutionService.cs` | CNC 程序执行引擎 |
|
||||||
|
| `ICncExecutionService` | `Services/Cnc/ICncExecutionService.cs` | 执行服务接口 |
|
||||||
|
| `CncEditorViewModel` | `ViewModels/Cnc/CncEditorViewModel.cs` | 调用执行服务的 ViewModel |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 依赖注入
|
||||||
|
|
||||||
|
`CncExecutionService` 通过构造函数注入以下服务:
|
||||||
|
|
||||||
|
| 依赖 | 接口 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| 检测结果存储 | `IInspectionResultStore` | 归档检测运行记录和节点结果 |
|
||||||
|
| 日志 | `ILoggerService` | 结构化日志记录 |
|
||||||
|
| 主视口 | `IMainViewportService` | 获取实时图像、推送结果图像 |
|
||||||
|
| 应用状态 | `IAppStateService` | 获取探测器帧数据 |
|
||||||
|
| 流水线执行 | `IPipelineExecutionService` | 执行图像处理流水线 |
|
||||||
|
| 图像处理 | `IImageProcessingService` | 获取算子定义和参数 |
|
||||||
|
| 事件聚合器 | `IEventAggregator` | 订阅探测器断连事件 |
|
||||||
|
| 图像持久化 | `IImagePersistenceService` | 保存采集图像到磁盘 |
|
||||||
|
| 运动控制 | `IMotionControlService`(可选) | 控制各轴移动到目标位置 |
|
||||||
|
| 运动系统 | `IMotionSystem`(可选) | 查询轴状态(是否到位) |
|
||||||
|
| 射线源 | `IRaySourceService`(可选) | 射线源控制 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
### 入口方法
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
Task ExecuteAsync(CncProgram program, IProgress<CncNodeExecutionProgress> progress, CancellationToken cancellationToken)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 执行阶段
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 1. 初始化阶段 │
|
||||||
|
│ - 创建 LinkedCancellationTokenSource(支持探测器断连取消) │
|
||||||
|
│ - 获取初始源图像 │
|
||||||
|
│ - 调用 _store.BeginRunAsync() 创建检测运行记录 │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ 2. 多位置执行循环(SavePositionNode) │
|
||||||
|
│ 对每个 SavePositionNode 按顺序执行: │
|
||||||
|
│ ┌─────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Step 0: 运动到目标位置 (MoveToPositionAsync) │ │
|
||||||
|
│ │ Step 1: 图像采集(探测器实时帧 或 手动图像文件) │ │
|
||||||
|
│ │ Step 2: 图像保存(如果 SaveImage=true) │ │
|
||||||
|
│ │ Step 3: 流水线执行(如果下一节点是 InspectionModule) │ │
|
||||||
|
│ │ Step 4: 报告节点执行状态 │ │
|
||||||
|
│ └─────────────────────────────────────────────────────┘ │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ 3. 批次结果汇总 │
|
||||||
|
│ - 构建 BatchCaptureResult │
|
||||||
|
│ - 调用 _imagePersistenceService.WriteSummaryAsync() │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ 4. 非位置节点执行 │
|
||||||
|
│ 按 Index 顺序处理剩余节点: │
|
||||||
|
│ - ReferencePointNode: 记录参考点参数 │
|
||||||
|
│ - SaveNodeNode / SaveNodeWithImageNode: 记录设备状态 │
|
||||||
|
│ - WaitDelayNode: 延时等待(带进度报告) │
|
||||||
|
│ - PauseDialogNode: 弹出暂停对话框 │
|
||||||
|
│ - InspectionModuleNode: 执行图像处理流水线 │
|
||||||
|
│ - CompleteProgramNode: 标记程序完成,退出循环 │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ 5. 完成阶段 │
|
||||||
|
│ - 调用 _store.CompleteRunAsync() 标记运行结束 │
|
||||||
|
│ - 清理 CancellationTokenSource │
|
||||||
|
│ - 通知 MainViewportService CNC 执行结束 │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 核心方法说明
|
||||||
|
|
||||||
|
### `ExecuteAsync` — 主执行入口
|
||||||
|
|
||||||
|
| 参数 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `program` | `CncProgram` | 待执行的 CNC 程序(含所有节点) |
|
||||||
|
| `progress` | `IProgress<CncNodeExecutionProgress>` | 进度回调(通知 UI 更新节点状态) |
|
||||||
|
| `cancellationToken` | `CancellationToken` | 外部取消令牌(用户点击停止) |
|
||||||
|
|
||||||
|
**关键行为:**
|
||||||
|
- 使用 `LinkedCancellationTokenSource` 将外部取消和探测器断连事件合并
|
||||||
|
- 每个节点执行前检查取消状态
|
||||||
|
- 失败节点不中断整体执行(容错设计),仅标记为 Failed 并继续
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `MoveToPositionAsync` — 运动控制
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private Task<MotionResult> MoveToPositionAsync(MotionState target, CancellationToken ct)
|
||||||
|
```
|
||||||
|
|
||||||
|
- 将 `MotionState` 中的微米(μm)坐标转换为毫米(mm)后发送给运动控制服务
|
||||||
|
- 依次移动:StageX → StageY → SourceZ → DetectorZ → DetectorSwing → StageRotation → FixtureRotation
|
||||||
|
- 任一轴移动失败立即返回错误
|
||||||
|
- 运动服务不可用时返回成功(优雅降级)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `WaitForAxesSettledAsync` — 等待轴到位
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private async Task<bool> WaitForAxesSettledAsync(CancellationToken ct)
|
||||||
|
```
|
||||||
|
|
||||||
|
- 每 50ms 轮询所有线性轴和旋转轴的状态
|
||||||
|
- 所有轴 `Status == Idle` 时返回 `true`
|
||||||
|
- 超时 30 秒返回 `false`(不中断执行,继续后续步骤)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `TryGetSourceImage` — 图像获取
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private BitmapSource TryGetSourceImage()
|
||||||
|
```
|
||||||
|
|
||||||
|
**图像源优先级:**
|
||||||
|
1. `MainViewportService.LatestManualImage`(用户手动加载的图像)
|
||||||
|
2. `MainViewportService.CurrentDisplayImage`(当前显示的实时图像)
|
||||||
|
3. `AppStateService.LatestDetectorFrame`(原始探测器帧,Gray16 → BitmapSource)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `ExecuteInspectionNodeAsync` — 检测模块执行
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private async Task<BitmapSource> ExecuteInspectionNodeAsync(
|
||||||
|
Guid runId, InspectionModuleNode inspectionNode, BitmapSource sourceImage, CancellationToken ct)
|
||||||
|
```
|
||||||
|
|
||||||
|
**执行步骤:**
|
||||||
|
1. 创建 `InspectionNodeResult` 记录
|
||||||
|
2. 序列化 Pipeline 定义为 JSON 快照
|
||||||
|
3. 保存输入图像为资产
|
||||||
|
4. 调用 `BuildPipelineNodeViewModels()` 构建流水线 ViewModel
|
||||||
|
5. 调用 `_pipelineExecutionService.ExecutePipelineAsync()` 执行流水线
|
||||||
|
6. 推送结果图像到主视口
|
||||||
|
7. 推送检测叠加层数据(轮廓、标注)
|
||||||
|
8. 合成背景图 + 叠加层,保存结果截图
|
||||||
|
9. 调用 `_store.AppendNodeResultAsync()` 归档节点结果
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `BuildPipelineNodeViewModels` — 构建流水线 ViewModel
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private IEnumerable<PipelineNodeViewModel> BuildPipelineNodeViewModels(PipelineModel pipeline)
|
||||||
|
```
|
||||||
|
|
||||||
|
- 将 `PipelineModel.Nodes`(数据模型)转换为 `PipelineNodeViewModel`(执行模型)
|
||||||
|
- 加载每个算子的参数定义和保存值
|
||||||
|
- 处理 JSON 参数值的类型转换(`JsonElement` → `int/double/bool/string`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `ExecuteWaitDelayWithProgressAsync` — 延时等待
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static async Task ExecuteWaitDelayWithProgressAsync(
|
||||||
|
WaitDelayNode waitNode, IProgress<CncNodeExecutionProgress> progress, CancellationToken ct)
|
||||||
|
```
|
||||||
|
|
||||||
|
- 每 50ms 报告一次进度百分比
|
||||||
|
- 支持取消(抛出 `OperationCanceledException`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 取消机制
|
||||||
|
|
||||||
|
| 取消源 | 触发方式 | 处理 |
|
||||||
|
|--------|----------|------|
|
||||||
|
| 用户点击停止 | `CancellationToken` 从 ViewModel 传入 | 各节点执行前检查 |
|
||||||
|
| 探测器断连 | `DetectorDisconnectedEvent` → `_executionCts.Cancel()` | 通过 LinkedCTS 传播 |
|
||||||
|
| 运动失败 | `MoveToPositionAsync` 返回失败 | 标记节点 Failed,继续下一个 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 进度报告
|
||||||
|
|
||||||
|
通过 `IProgress<CncNodeExecutionProgress>` 回调通知 UI:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record CncNodeExecutionProgress(
|
||||||
|
Guid NodeId,
|
||||||
|
NodeExecutionState State,
|
||||||
|
double? ProgressPercent = null,
|
||||||
|
BitmapSource ResultImage = null,
|
||||||
|
int? PositionIndex = null,
|
||||||
|
int? TotalPositions = null);
|
||||||
|
```
|
||||||
|
|
||||||
|
ViewModel 收到回调后更新 `CncNodeViewModel.ExecutionState`,触发树形节点的颜色变化。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 结果归档
|
||||||
|
|
||||||
|
| 方法 | 时机 | 存储内容 |
|
||||||
|
|------|------|----------|
|
||||||
|
| `_store.BeginRunAsync()` | 执行开始 | 运行记录 + 源图像 |
|
||||||
|
| `_store.AppendNodeResultAsync()` | 每个检测模块执行后 | 节点结果 + Pipeline 快照 + 输入/输出图像 |
|
||||||
|
| `_store.CompleteRunAsync()` | 执行结束 | 标记运行完成/失败 |
|
||||||
|
| `_imagePersistenceService.WriteSummaryAsync()` | 多位置循环结束 | 批次结果 JSON 摘要 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 容错设计
|
||||||
|
|
||||||
|
- **图像采集失败**:标记节点 Failed,跳过该位置,继续下一个
|
||||||
|
- **图像保存失败**:标记节点 Failed,但仍继续执行流水线
|
||||||
|
- **流水线执行异常**:捕获异常,标记 Failed,不中断整体执行
|
||||||
|
- **运动服务不可用**:返回成功(优雅降级,适用于仿真模式)
|
||||||
|
- **探测器断连**:通过事件取消整个执行
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 调用关系图
|
||||||
|
|
||||||
|
```
|
||||||
|
CncEditorViewModel
|
||||||
|
│
|
||||||
|
├── RunCncCommand → ExecuteRunAsync()
|
||||||
|
│ │
|
||||||
|
│ └── CncExecutionService.ExecuteAsync()
|
||||||
|
│ │
|
||||||
|
│ ├── IInspectionResultStore.BeginRunAsync()
|
||||||
|
│ │
|
||||||
|
│ ├── [循环] SavePositionNode
|
||||||
|
│ │ ├── MoveToPositionAsync() → IMotionControlService
|
||||||
|
│ │ ├── WaitForAxesSettledAsync() → IMotionSystem
|
||||||
|
│ │ ├── TryGetSourceImage() → IMainViewportService / IAppStateService
|
||||||
|
│ │ ├── IImagePersistenceService.SaveImageAsync()
|
||||||
|
│ │ └── ExecuteInspectionNodeAsync()
|
||||||
|
│ │ ├── BuildPipelineNodeViewModels()
|
||||||
|
│ │ ├── IPipelineExecutionService.ExecutePipelineAsync()
|
||||||
|
│ │ ├── IMainViewportService.SetCncResultImage()
|
||||||
|
│ │ ├── IMainViewportService.PushDetectionOverlay()
|
||||||
|
│ │ └── IInspectionResultStore.AppendNodeResultAsync()
|
||||||
|
│ │
|
||||||
|
│ ├── [循环] 非位置节点
|
||||||
|
│ │ ├── WaitDelayNode → ExecuteWaitDelayWithProgressAsync()
|
||||||
|
│ │ ├── PauseDialogNode → MessageBox
|
||||||
|
│ │ ├── InspectionModuleNode → ExecuteInspectionNodeAsync()
|
||||||
|
│ │ └── CompleteProgramNode → 退出
|
||||||
|
│ │
|
||||||
|
│ ├── IImagePersistenceService.WriteSummaryAsync()
|
||||||
|
│ └── IInspectionResultStore.CompleteRunAsync()
|
||||||
|
│
|
||||||
|
└── StopCncCommand → CancellationTokenSource.Cancel()
|
||||||
|
```
|
||||||
@@ -26,23 +26,27 @@ using XplorePlane.ViewModels;
|
|||||||
namespace XplorePlane.Services.Cnc
|
namespace XplorePlane.Services.Cnc
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// CNC 程序执行服务。
|
||||||
|
/// 按节点顺序执行整个检测程序,协调运动控制、图像采集、流水线处理和结果归档。
|
||||||
/// Executes a CNC program node-by-node, reporting progress and persisting inspection results.
|
/// Executes a CNC program node-by-node, reporting progress and persisting inspection results.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CncExecutionService : ICncExecutionService
|
public class CncExecutionService : ICncExecutionService
|
||||||
{
|
{
|
||||||
private readonly IInspectionResultStore _store;
|
// ── 依赖服务 ──────────────────────────────────────────────────────
|
||||||
private readonly ILoggerService _logger;
|
private readonly IInspectionResultStore _store; // 检测结果归档存储
|
||||||
private readonly IMainViewportService _mainViewportService;
|
private readonly ILoggerService _logger; // 日志服务
|
||||||
private readonly IAppStateService _appStateService;
|
private readonly IMainViewportService _mainViewportService; // 主视口(获取/推送图像)
|
||||||
private readonly IPipelineExecutionService _pipelineExecutionService;
|
private readonly IAppStateService _appStateService; // 应用状态(探测器帧)
|
||||||
private readonly IImageProcessingService _imageProcessingService;
|
private readonly IPipelineExecutionService _pipelineExecutionService; // 图像处理流水线执行
|
||||||
private readonly IEventAggregator _eventAggregator;
|
private readonly IImageProcessingService _imageProcessingService; // 算子注册表
|
||||||
private readonly IImagePersistenceService _imagePersistenceService;
|
private readonly IEventAggregator _eventAggregator; // 事件总线
|
||||||
private readonly IMotionControlService _motionControlService;
|
private readonly IImagePersistenceService _imagePersistenceService; // 图像文件持久化
|
||||||
private readonly IMotionSystem _motionSystem;
|
private readonly IMotionControlService _motionControlService; // 运动控制(可选)
|
||||||
private readonly IRaySourceService _raySourceService;
|
private readonly IMotionSystem _motionSystem; // 运动系统状态查询(可选)
|
||||||
|
private readonly IRaySourceService _raySourceService; // 射线源控制(可选)
|
||||||
|
|
||||||
// Task 4.2: volatile field so reads/writes are not reordered across threads
|
// 当前执行的取消令牌源(volatile 保证跨线程可见性)
|
||||||
|
// 探测器断连事件通过此字段取消正在执行的程序
|
||||||
private volatile CancellationTokenSource _executionCts;
|
private volatile CancellationTokenSource _executionCts;
|
||||||
|
|
||||||
public CncExecutionService(
|
public CncExecutionService(
|
||||||
@@ -75,7 +79,7 @@ namespace XplorePlane.Services.Cnc
|
|||||||
.Subscribe(OnDetectorDisconnected, ThreadOption.BackgroundThread);
|
.Subscribe(OnDetectorDisconnected, ThreadOption.BackgroundThread);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Task 4.3: callback – cancel the running execution when the detector disconnects
|
// 探测器断连回调:取消当前正在执行的 CNC 程序
|
||||||
private void OnDetectorDisconnected()
|
private void OnDetectorDisconnected()
|
||||||
{
|
{
|
||||||
var cts = _executionCts;
|
var cts = _executionCts;
|
||||||
@@ -88,9 +92,14 @@ namespace XplorePlane.Services.Cnc
|
|||||||
catch (ObjectDisposedException) { }
|
catch (ObjectDisposedException) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// CNC 程序主执行入口。
|
||||||
|
/// 按节点顺序执行:先处理所有 SavePositionNode(多位置循环),再处理其余节点。
|
||||||
|
/// 支持取消(用户停止 + 探测器断连)、进度报告和容错。
|
||||||
|
/// </summary>
|
||||||
public async Task ExecuteAsync(CncProgram program, IProgress<CncNodeExecutionProgress> progress, CancellationToken cancellationToken)
|
public async Task ExecuteAsync(CncProgram program, IProgress<CncNodeExecutionProgress> progress, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// Task 4.4: create a linked CTS so DetectorDisconnectedEvent can cancel independently
|
// 创建联合取消令牌:外部取消 + 探测器断连事件均可触发
|
||||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||||
_executionCts = linkedCts;
|
_executionCts = linkedCts;
|
||||||
_mainViewportService?.SetCncRunning(true);
|
_mainViewportService?.SetCncRunning(true);
|
||||||
@@ -139,311 +148,137 @@ namespace XplorePlane.Services.Cnc
|
|||||||
bool cancelled = false;
|
bool cancelled = false;
|
||||||
bool allSucceeded = true;
|
bool allSucceeded = true;
|
||||||
BitmapSource lastResultImage = null;
|
BitmapSource lastResultImage = null;
|
||||||
|
|
||||||
// Task 5.5: Record start time for batch result summary
|
|
||||||
var startTime = DateTime.UtcNow;
|
var startTime = DateTime.UtcNow;
|
||||||
var positionResults = new List<PositionResult>();
|
var positionResults = new List<PositionResult>();
|
||||||
|
|
||||||
// Task 5.1: Get all SavePositionNodes ordered by Index ascending for multi-position execution
|
// ══════════════════════════════════════════════════════════════
|
||||||
var savePositionNodes = program.Nodes
|
// 单循环:按 Index 顺序逐个执行所有节点
|
||||||
.OfType<SavePositionNode>()
|
// ══════════════════════════════════════════════════════════════
|
||||||
.OrderBy(n => n.Index)
|
var allNodes = program.Nodes.OrderBy(n => n.Index).ToList();
|
||||||
.ToList();
|
|
||||||
int totalPositions = savePositionNodes.Count;
|
|
||||||
|
|
||||||
// Task 5.1: Multi-position execution loop with progress reporting and cancellation
|
foreach (var node in allNodes)
|
||||||
for (int positionIndex = 0; positionIndex < totalPositions; positionIndex++)
|
|
||||||
{
|
{
|
||||||
// Task 5.1: Check CancellationToken at the start of each iteration
|
|
||||||
if (linkedCts.Token.IsCancellationRequested)
|
if (linkedCts.Token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
_motionControlService?.StopAll();
|
_motionControlService?.StopAll();
|
||||||
_logger.ForModule<CncExecutionService>().Info(
|
|
||||||
"Multi-position execution cancelled at position {0}/{1}",
|
|
||||||
positionIndex, totalPositions);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
var sp = savePositionNodes[positionIndex];
|
progress?.Report(new CncNodeExecutionProgress(node.Id, NodeExecutionState.Running, ProgressPercent: 0));
|
||||||
|
|
||||||
// Task 5.1: Report progress (current 0-based index, total count)
|
|
||||||
progress?.Report(new CncNodeExecutionProgress(
|
|
||||||
sp.Id,
|
|
||||||
NodeExecutionState.Running,
|
|
||||||
ProgressPercent: 0,
|
|
||||||
PositionIndex: positionIndex,
|
|
||||||
TotalPositions: totalPositions));
|
|
||||||
|
|
||||||
_logger.ForModule<CncExecutionService>().Info(
|
|
||||||
"Executing save-position node [{Index}] {Name} (position {Current}/{Total}) | " +
|
|
||||||
"StageX={StageX} StageY={StageY} SourceZ={SourceZ} DetectorZ={DetectorZ} " +
|
|
||||||
"DetectorSwing={DetectorSwing} FDD={FDD} FOD={FOD} Magnification={Magnification} " +
|
|
||||||
"StageRotation={StageRotation} FixtureRotation={FixtureRotation} SaveImage={SaveImage}",
|
|
||||||
sp.Index, sp.Name, positionIndex + 1, totalPositions,
|
|
||||||
sp.MotionState.StageX, sp.MotionState.StageY,
|
|
||||||
sp.MotionState.SourceZ, sp.MotionState.DetectorZ,
|
|
||||||
sp.MotionState.DetectorSwing, sp.MotionState.FDD,
|
|
||||||
sp.MotionState.FOD, sp.MotionState.Magnification,
|
|
||||||
sp.MotionState.StageRotation, sp.MotionState.FixtureRotation,
|
|
||||||
sp.SaveImage);
|
|
||||||
|
|
||||||
bool nodeSucceeded = true;
|
bool nodeSucceeded = true;
|
||||||
string savedImagePath = null;
|
|
||||||
string nodeErrorMessage = null;
|
|
||||||
|
|
||||||
// ── Step 0: Move to target position (motion integration) ──
|
|
||||||
var moveResult = await MoveToPositionAsync(sp.MotionState, linkedCts.Token);
|
|
||||||
if (!moveResult.Success)
|
|
||||||
{
|
|
||||||
_logger.ForModule<CncExecutionService>().Warn(
|
|
||||||
"Motion move failed for node '{0}' at index {1}: {2}",
|
|
||||||
sp.Name, positionIndex, moveResult.ErrorMessage);
|
|
||||||
progress?.Report(new CncNodeExecutionProgress(
|
|
||||||
sp.Id, NodeExecutionState.Failed,
|
|
||||||
PositionIndex: positionIndex, TotalPositions: totalPositions));
|
|
||||||
allSucceeded = false;
|
|
||||||
positionResults.Add(new PositionResult
|
|
||||||
{
|
|
||||||
NodeName = sp.Name,
|
|
||||||
NodeIndex = sp.Index,
|
|
||||||
Status = "Failed",
|
|
||||||
ErrorMessage = $"Motion failed: {moveResult.ErrorMessage}"
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for all axes to settle
|
|
||||||
var settled = await WaitForAxesSettledAsync(linkedCts.Token);
|
|
||||||
if (!settled)
|
|
||||||
{
|
|
||||||
_logger.ForModule<CncExecutionService>().Warn(
|
|
||||||
"Axes did not settle within timeout for node '{0}' at index {1}",
|
|
||||||
sp.Name, positionIndex);
|
|
||||||
// Continue anyway - axes may be close enough
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Step 1: Image Acquisition (with error tolerance - Task 5.4) ──
|
|
||||||
BitmapSource positionImage = null;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(sp.ManualImagePath))
|
switch (node)
|
||||||
{
|
{
|
||||||
// Task 5.2: ManualImagePath is set - validate and load from file
|
// ── 保存位置节点:运动 → 采集 → 保存图像 ──
|
||||||
var validationResult = ManualImageValidator.Validate(sp.ManualImagePath);
|
case SavePositionNode sp:
|
||||||
|
|
||||||
if (validationResult == ManualImageValidationResult.Valid)
|
|
||||||
{
|
{
|
||||||
positionImage = LoadImageFromFile(sp.ManualImagePath);
|
_logger.ForModule<CncExecutionService>().Info(
|
||||||
if (positionImage != null)
|
"执行位置节点 [{Index}] {Name} | StageX={StageX} StageY={StageY}",
|
||||||
|
sp.Index, sp.Name, sp.MotionState.StageX, sp.MotionState.StageY);
|
||||||
|
|
||||||
|
var moveResult = await MoveToPositionAsync(sp.MotionState, linkedCts.Token);
|
||||||
|
if (!moveResult.Success)
|
||||||
{
|
{
|
||||||
currentSourceImage = positionImage;
|
nodeSucceeded = false;
|
||||||
_logger.ForModule<CncExecutionService>().Info(
|
positionResults.Add(new PositionResult { NodeName = sp.Name, NodeIndex = sp.Index, Status = "Failed", ErrorMessage = moveResult.ErrorMessage });
|
||||||
"Loaded manual image from '{0}' for node '{1}'",
|
break;
|
||||||
sp.ManualImagePath, sp.Name);
|
}
|
||||||
|
await WaitForAxesSettledAsync(linkedCts.Token);
|
||||||
|
|
||||||
|
BitmapSource positionImage = null;
|
||||||
|
if (!string.IsNullOrEmpty(sp.ManualImagePath))
|
||||||
|
{
|
||||||
|
var vr = ManualImageValidator.Validate(sp.ManualImagePath);
|
||||||
|
if (vr == ManualImageValidationResult.Valid)
|
||||||
|
positionImage = LoadImageFromFile(sp.ManualImagePath);
|
||||||
|
if (positionImage == null) { nodeSucceeded = false; positionResults.Add(new PositionResult { NodeName = sp.Name, NodeIndex = sp.Index, Status = "Failed", ErrorMessage = "手动图像加载失败" }); break; }
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Manual image file could not be decoded - mark Failed, continue
|
positionImage = TryGetSourceImage();
|
||||||
_logger.ForModule<CncExecutionService>().Warn(
|
if (positionImage == null) { nodeSucceeded = false; positionResults.Add(new PositionResult { NodeName = sp.Name, NodeIndex = sp.Index, Status = "Failed", ErrorMessage = "探测器无有效帧" }); break; }
|
||||||
"Image acquisition failed for node '{0}' at index {1}: manual image file could not be decoded",
|
|
||||||
sp.Name, positionIndex);
|
|
||||||
progress?.Report(new CncNodeExecutionProgress(
|
|
||||||
sp.Id, NodeExecutionState.Failed,
|
|
||||||
PositionIndex: positionIndex, TotalPositions: totalPositions));
|
|
||||||
allSucceeded = false;
|
|
||||||
// Task 5.5: Track failed position result
|
|
||||||
positionResults.Add(new PositionResult
|
|
||||||
{
|
|
||||||
NodeName = sp.Name,
|
|
||||||
NodeIndex = sp.Index,
|
|
||||||
Status = "Failed",
|
|
||||||
ErrorMessage = "Manual image file could not be decoded"
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
}
|
currentSourceImage = positionImage;
|
||||||
else
|
|
||||||
{
|
if (sp.SaveImage)
|
||||||
// Validation failed - show error dialog, mark Failed, continue
|
|
||||||
var errorMessage = validationResult switch
|
|
||||||
{
|
{
|
||||||
ManualImageValidationResult.PathTooLong =>
|
try
|
||||||
$"\u56fe\u50cf\u8def\u5f84\u8fc7\u957f\uff08\u8d85\u8fc7260\u5b57\u7b26\uff09\uff1a\n{sp.ManualImagePath}",
|
{
|
||||||
ManualImageValidationResult.FileNotFound =>
|
var bytes = EncodeBitmapToBmp(positionImage);
|
||||||
$"\u56fe\u50cf\u6587\u4ef6\u4e0d\u5b58\u5728\uff1a\n{sp.ManualImagePath}",
|
var sr = await _imagePersistenceService.SaveImageAsync(bytes, sp.Name, program.Name, linkedCts.Token);
|
||||||
ManualImageValidationResult.UnsupportedFormat =>
|
positionResults.Add(new PositionResult { NodeName = sp.Name, NodeIndex = sp.Index, Status = "Success", ImagePath = sr.Success ? sr.FilePath : null });
|
||||||
$"\u4e0d\u652f\u6301\u7684\u56fe\u50cf\u683c\u5f0f\uff08\u4ec5\u652f\u6301 BMP\u3001PNG\u3001TIFF\uff09\uff1a\n{sp.ManualImagePath}",
|
}
|
||||||
_ => $"\u56fe\u50cf\u8def\u5f84\u65e0\u6548\uff1a\n{sp.ManualImagePath}"
|
catch { positionResults.Add(new PositionResult { NodeName = sp.Name, NodeIndex = sp.Index, Status = "Success" }); }
|
||||||
};
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
positionResults.Add(new PositionResult { NodeName = sp.Name, NodeIndex = sp.Index, Status = "Success" });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
_logger.ForModule<CncExecutionService>().Warn(
|
// ── 检测模块节点:使用当前源图像执行流水线 ──
|
||||||
"Image acquisition failed for node '{0}' at index {1}: manual image validation failed ({2})",
|
case InspectionModuleNode inspectionNode:
|
||||||
sp.Name, positionIndex, validationResult);
|
{
|
||||||
|
if (currentSourceImage == null) { nodeSucceeded = false; break; }
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var img = await ExecuteInspectionNodeAsync(runId, inspectionNode, currentSourceImage, linkedCts.Token);
|
||||||
|
if (img != null) lastResultImage = img;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.ForModule<CncExecutionService>().Error(ex, "检测模块 '{0}' 执行失败", inspectionNode.Name);
|
||||||
|
nodeSucceeded = false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ReferencePointNode rp:
|
||||||
|
_logger.ForModule<CncExecutionService>().Info("执行参考点 [{Index}] {Name}", rp.Index, rp.Name);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WaitDelayNode waitNode:
|
||||||
|
try { await ExecuteWaitDelayWithProgressAsync(waitNode, progress, linkedCts.Token); }
|
||||||
|
catch (OperationCanceledException) { cancelled = true; }
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PauseDialogNode pauseNode:
|
||||||
await Application.Current.Dispatcher.InvokeAsync(() =>
|
await Application.Current.Dispatcher.InvokeAsync(() =>
|
||||||
MessageBox.Show(errorMessage, "\u624b\u52a8\u56fe\u50cf\u52a0\u8f7d\u5931\u8d25", MessageBoxButton.OK, MessageBoxImage.Error));
|
MessageBox.Show(pauseNode.DialogMessage, pauseNode.DialogTitle));
|
||||||
|
if (linkedCts.Token.IsCancellationRequested) cancelled = true;
|
||||||
|
break;
|
||||||
|
|
||||||
progress?.Report(new CncNodeExecutionProgress(
|
case CompleteProgramNode:
|
||||||
sp.Id, NodeExecutionState.Failed,
|
progress?.Report(new CncNodeExecutionProgress(node.Id, NodeExecutionState.Succeeded));
|
||||||
PositionIndex: positionIndex, TotalPositions: totalPositions));
|
goto endLoop;
|
||||||
allSucceeded = false;
|
|
||||||
// Task 5.5: Track failed position result
|
default:
|
||||||
positionResults.Add(new PositionResult
|
break;
|
||||||
{
|
|
||||||
NodeName = sp.Name,
|
|
||||||
NodeIndex = sp.Index,
|
|
||||||
Status = "Failed",
|
|
||||||
ErrorMessage = $"Manual image validation failed: {validationResult}"
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// ManualImagePath is empty - use detector acquisition (default)
|
|
||||||
positionImage = TryGetSourceImage();
|
|
||||||
if (positionImage == null)
|
|
||||||
{
|
|
||||||
// Detector returned null - mark Failed, continue
|
|
||||||
_logger.ForModule<CncExecutionService>().Warn(
|
|
||||||
"Image acquisition failed for node '{0}' at index {1}: detector returned no valid image frame",
|
|
||||||
sp.Name, positionIndex);
|
|
||||||
progress?.Report(new CncNodeExecutionProgress(
|
|
||||||
sp.Id, NodeExecutionState.Failed,
|
|
||||||
PositionIndex: positionIndex, TotalPositions: totalPositions));
|
|
||||||
allSucceeded = false;
|
|
||||||
// Task 5.5: Track failed position result
|
|
||||||
positionResults.Add(new PositionResult
|
|
||||||
{
|
|
||||||
NodeName = sp.Name,
|
|
||||||
NodeIndex = sp.Index,
|
|
||||||
Status = "Failed",
|
|
||||||
ErrorMessage = "Detector returned no valid image frame"
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
currentSourceImage = positionImage;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Unexpected exception during image acquisition - mark Failed, continue
|
_logger.ForModule<CncExecutionService>().Error(ex, "节点 '{0}' 执行异常", node.Name);
|
||||||
_logger.ForModule<CncExecutionService>().Warn(
|
if (linkedCts.Token.IsCancellationRequested) cancelled = true;
|
||||||
"Image acquisition failed for node '{0}' at index {1}: {2}",
|
else nodeSucceeded = false;
|
||||||
sp.Name, positionIndex, ex.Message);
|
|
||||||
progress?.Report(new CncNodeExecutionProgress(
|
|
||||||
sp.Id, NodeExecutionState.Failed,
|
|
||||||
PositionIndex: positionIndex, TotalPositions: totalPositions));
|
|
||||||
allSucceeded = false;
|
|
||||||
// Task 5.5: Track failed position result
|
|
||||||
positionResults.Add(new PositionResult
|
|
||||||
{
|
|
||||||
NodeName = sp.Name,
|
|
||||||
NodeIndex = sp.Index,
|
|
||||||
Status = "Failed",
|
|
||||||
ErrorMessage = ex.Message
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Step 2: Image Persistence (with error tolerance - Task 5.4) ──
|
if (cancelled) { progress?.Report(new CncNodeExecutionProgress(node.Id, NodeExecutionState.Failed)); break; }
|
||||||
if (sp.SaveImage && positionImage != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var imageBytes = EncodeBitmapToBmp(positionImage);
|
|
||||||
var saveResult = await _imagePersistenceService.SaveImageAsync(
|
|
||||||
imageBytes, sp.Name, program.Name, linkedCts.Token);
|
|
||||||
|
|
||||||
if (saveResult.Success)
|
var nodeResultImage = node is InspectionModuleNode ? lastResultImage : null;
|
||||||
{
|
|
||||||
_logger.ForModule<CncExecutionService>().Info(
|
|
||||||
"Image saved for node '{0}': Path={1}, Size={2} bytes",
|
|
||||||
sp.Name, saveResult.FilePath, saveResult.FileSizeBytes);
|
|
||||||
// Task 5.5: Track saved image path
|
|
||||||
savedImagePath = saveResult.FilePath;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Save returned failure - log error, mark failed, but continue with pipeline
|
|
||||||
_logger.ForModule<CncExecutionService>().Error(
|
|
||||||
new InvalidOperationException(saveResult.ErrorMessage),
|
|
||||||
"Image save failed for node '{0}': {1}",
|
|
||||||
sp.Name, saveResult.ErrorMessage);
|
|
||||||
nodeSucceeded = false;
|
|
||||||
nodeErrorMessage = saveResult.ErrorMessage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Exception during save - log error, mark failed, but continue with pipeline
|
|
||||||
_logger.ForModule<CncExecutionService>().Error(ex,
|
|
||||||
"Image save failed for node '{0}': {1}",
|
|
||||||
sp.Name, ex.Message);
|
|
||||||
nodeSucceeded = false;
|
|
||||||
nodeErrorMessage = ex.Message;
|
|
||||||
}
|
|
||||||
// Note: image save failure does NOT prevent pipeline execution
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Step 3: Pipeline Execution (with error tolerance - Task 5.4) ──
|
|
||||||
// Check if the next node (by Index order) is an InspectionModuleNode
|
|
||||||
if (positionImage != null)
|
|
||||||
{
|
|
||||||
var allNodesOrdered = program.Nodes.OrderBy(n => n.Index).ToList();
|
|
||||||
int currentNodeOrderIndex = allNodesOrdered.FindIndex(n => n.Id == sp.Id);
|
|
||||||
if (currentNodeOrderIndex >= 0 && currentNodeOrderIndex + 1 < allNodesOrdered.Count)
|
|
||||||
{
|
|
||||||
var nextNode = allNodesOrdered[currentNodeOrderIndex + 1];
|
|
||||||
if (nextNode is InspectionModuleNode inspectionNode)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_logger.ForModule<CncExecutionService>().Info(
|
|
||||||
"Passing captured image from node '{0}' to inspection module '{1}'",
|
|
||||||
sp.Name, inspectionNode.Name);
|
|
||||||
|
|
||||||
var resultImage = await ExecuteInspectionNodeAsync(
|
|
||||||
runId, inspectionNode, positionImage, linkedCts.Token);
|
|
||||||
if (resultImage != null)
|
|
||||||
lastResultImage = resultImage;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Pipeline execution exception - log error, mark failed, continue
|
|
||||||
_logger.ForModule<CncExecutionService>().Error(ex,
|
|
||||||
"Pipeline execution failed for node '{0}' at index {1}: {2}",
|
|
||||||
sp.Name, positionIndex, ex.Message);
|
|
||||||
nodeSucceeded = false;
|
|
||||||
nodeErrorMessage = $"Pipeline execution failed: {ex.Message}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Step 4: Report final state ──
|
|
||||||
var finalState = nodeSucceeded ? NodeExecutionState.Succeeded : NodeExecutionState.Failed;
|
var finalState = nodeSucceeded ? NodeExecutionState.Succeeded : NodeExecutionState.Failed;
|
||||||
progress?.Report(new CncNodeExecutionProgress(sp.Id, finalState,
|
progress?.Report(new CncNodeExecutionProgress(node.Id, finalState, nodeResultImage));
|
||||||
PositionIndex: positionIndex, TotalPositions: totalPositions));
|
if (!nodeSucceeded) allSucceeded = false;
|
||||||
|
|
||||||
// Task 5.5: Track position result
|
|
||||||
positionResults.Add(new PositionResult
|
|
||||||
{
|
|
||||||
NodeName = sp.Name,
|
|
||||||
NodeIndex = sp.Index,
|
|
||||||
Status = nodeSucceeded ? "Success" : "Failed",
|
|
||||||
ErrorMessage = nodeErrorMessage,
|
|
||||||
ImagePath = savedImagePath
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!nodeSucceeded)
|
|
||||||
allSucceeded = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Task 5.5: Build BatchCaptureResult and write summary ──
|
endLoop:
|
||||||
var wasCancelled = cancelled;
|
|
||||||
|
// ── 写入批次结果摘要 ──
|
||||||
|
int totalPositions = program.Nodes.OfType<SavePositionNode>().Count();
|
||||||
var batchResult = new BatchCaptureResult
|
var batchResult = new BatchCaptureResult
|
||||||
{
|
{
|
||||||
ProgramName = program.Name,
|
ProgramName = program.Name,
|
||||||
@@ -453,175 +288,18 @@ namespace XplorePlane.Services.Cnc
|
|||||||
SucceededPositions = positionResults.Count(r => r.Status == "Success"),
|
SucceededPositions = positionResults.Count(r => r.Status == "Success"),
|
||||||
FailedPositions = positionResults.Count(r => r.Status == "Failed"),
|
FailedPositions = positionResults.Count(r => r.Status == "Failed"),
|
||||||
SavedImageCount = positionResults.Count(r => r.ImagePath != null),
|
SavedImageCount = positionResults.Count(r => r.ImagePath != null),
|
||||||
Status = wasCancelled ? "Cancelled" : "Completed",
|
Status = cancelled ? "Cancelled" : "Completed",
|
||||||
CompletedBeforeCancel = wasCancelled ? positionResults.Count : null,
|
CompletedBeforeCancel = cancelled ? positionResults.Count : null,
|
||||||
NotExecutedAfterCancel = wasCancelled ? totalPositions - positionResults.Count : null,
|
NotExecutedAfterCancel = cancelled ? totalPositions - positionResults.Count : null,
|
||||||
Positions = positionResults
|
Positions = positionResults
|
||||||
};
|
};
|
||||||
|
|
||||||
try
|
try { await _imagePersistenceService.WriteSummaryAsync(batchResult, program.Name, CancellationToken.None); }
|
||||||
{
|
catch (Exception ex) { _logger.ForModule<CncExecutionService>().Error(ex, "写入批次摘要失败"); }
|
||||||
var summaryWritten = await _imagePersistenceService.WriteSummaryAsync(
|
|
||||||
batchResult, program.Name, CancellationToken.None);
|
|
||||||
if (!summaryWritten)
|
|
||||||
{
|
|
||||||
_logger.ForModule<CncExecutionService>().Error(
|
|
||||||
null,
|
|
||||||
"Failed to write batch capture summary for program '{0}'",
|
|
||||||
program.Name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.ForModule<CncExecutionService>().Error(ex,
|
|
||||||
"Failed to write batch capture summary for program '{0}': {1}",
|
|
||||||
program.Name, ex.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process remaining non-SavePosition nodes in order
|
|
||||||
foreach (var node in program.Nodes.OrderBy(n => n.Index))
|
|
||||||
{
|
|
||||||
if (linkedCts.Token.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
cancelled = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip SavePositionNodes - already processed in multi-position loop above
|
|
||||||
if (node is SavePositionNode)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
progress?.Report(new CncNodeExecutionProgress(node.Id, NodeExecutionState.Running, ProgressPercent: 0));
|
|
||||||
|
|
||||||
bool nodeSucceeded = true;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
switch (node)
|
|
||||||
{
|
|
||||||
case ReferencePointNode rp:
|
|
||||||
_logger.ForModule<CncExecutionService>().Info(
|
|
||||||
"Executing reference point node [{Index}] {Name} | " +
|
|
||||||
"StageX={StageX} StageY={StageY} SourceZ={SourceZ} DetectorZ={DetectorZ} " +
|
|
||||||
"DetectorSwing={DetectorSwing} FDD={FDD} FOD={FOD} Magnification={Magnification} " +
|
|
||||||
"StageRotation={StageRotation} FixtureRotation={FixtureRotation} " +
|
|
||||||
"RayOn={RayOn} Voltage={Voltage}kV Current={Current}uA",
|
|
||||||
rp.Index, rp.Name,
|
|
||||||
rp.StageX, rp.StageY, rp.SourceZ, rp.DetectorZ,
|
|
||||||
rp.DetectorSwing, rp.FDD, rp.FOD, rp.Magnification,
|
|
||||||
rp.StageRotation, rp.FixtureRotation,
|
|
||||||
rp.IsRayOn, rp.Voltage, rp.Current);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SaveNodeNode sn:
|
|
||||||
_logger.ForModule<CncExecutionService>().Info(
|
|
||||||
"Executing save node [{Index}] {Name} | " +
|
|
||||||
"StageX={StageX} StageY={StageY} SourceZ={SourceZ} DetectorZ={DetectorZ} " +
|
|
||||||
"DetectorSwing={DetectorSwing} FDD={FDD} FOD={FOD} Magnification={Magnification} " +
|
|
||||||
"RayOn={RayOn} Voltage={Voltage}kV Power={Power}W",
|
|
||||||
sn.Index, sn.Name,
|
|
||||||
sn.MotionState.StageX, sn.MotionState.StageY,
|
|
||||||
sn.MotionState.SourceZ, sn.MotionState.DetectorZ,
|
|
||||||
sn.MotionState.DetectorSwing, sn.MotionState.FDD,
|
|
||||||
sn.MotionState.FOD, sn.MotionState.Magnification,
|
|
||||||
sn.RaySourceState.IsOn, sn.RaySourceState.Voltage, sn.RaySourceState.Power);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SaveNodeWithImageNode sni:
|
|
||||||
_logger.ForModule<CncExecutionService>().Info(
|
|
||||||
"Executing save-with-image node [{Index}] {Name} | " +
|
|
||||||
"StageX={StageX} StageY={StageY} SourceZ={SourceZ} DetectorZ={DetectorZ} " +
|
|
||||||
"DetectorSwing={DetectorSwing} FDD={FDD} FOD={FOD} Magnification={Magnification} " +
|
|
||||||
"RayOn={RayOn} Voltage={Voltage}kV Power={Power}W ImageFile={ImageFile}",
|
|
||||||
sni.Index, sni.Name,
|
|
||||||
sni.MotionState.StageX, sni.MotionState.StageY,
|
|
||||||
sni.MotionState.SourceZ, sni.MotionState.DetectorZ,
|
|
||||||
sni.MotionState.DetectorSwing, sni.MotionState.FDD,
|
|
||||||
sni.MotionState.FOD, sni.MotionState.Magnification,
|
|
||||||
sni.RaySourceState.IsOn, sni.RaySourceState.Voltage, sni.RaySourceState.Power,
|
|
||||||
sni.ImageFileName);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case WaitDelayNode waitNode:
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await ExecuteWaitDelayWithProgressAsync(waitNode, progress, linkedCts.Token);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
cancelled = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case PauseDialogNode pauseNode:
|
|
||||||
await Application.Current.Dispatcher.InvokeAsync(() =>
|
|
||||||
MessageBox.Show(pauseNode.DialogMessage, pauseNode.DialogTitle));
|
|
||||||
if (linkedCts.Token.IsCancellationRequested)
|
|
||||||
cancelled = true;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case InspectionModuleNode inspectionNode:
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var img = await ExecuteInspectionNodeAsync(runId, inspectionNode, currentSourceImage, linkedCts.Token);
|
|
||||||
if (img != null) lastResultImage = img;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.ForModule<CncExecutionService>().Error(ex,
|
|
||||||
"Failed to append node result for node '{0}' (Id={1})", inspectionNode.Name, inspectionNode.Id);
|
|
||||||
nodeSucceeded = false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case CompleteProgramNode:
|
|
||||||
// Report Succeeded before terminating the loop
|
|
||||||
progress?.Report(new CncNodeExecutionProgress(node.Id, NodeExecutionState.Succeeded));
|
|
||||||
goto endLoop;
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Unknown node types are treated as succeeded
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.ForModule<CncExecutionService>().Error(ex,
|
|
||||||
"Unexpected error executing node '{0}' (Id={1})", node.Name, node.Id);
|
|
||||||
if (linkedCts.Token.IsCancellationRequested)
|
|
||||||
cancelled = true;
|
|
||||||
else
|
|
||||||
nodeSucceeded = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cancelled)
|
|
||||||
{
|
|
||||||
progress?.Report(new CncNodeExecutionProgress(node.Id, NodeExecutionState.Failed));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Carry the latest inspection result image so the ViewModel can cache it on the node.
|
|
||||||
var nodeResultImage = node is InspectionModuleNode ? lastResultImage : null;
|
|
||||||
var finalState = nodeSucceeded ? NodeExecutionState.Succeeded : NodeExecutionState.Failed;
|
|
||||||
progress?.Report(new CncNodeExecutionProgress(node.Id, finalState, nodeResultImage));
|
|
||||||
|
|
||||||
if (!nodeSucceeded)
|
|
||||||
allSucceeded = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
endLoop:
|
|
||||||
|
|
||||||
bool? overallPass = cancelled ? null : (bool?)allSucceeded;
|
bool? overallPass = cancelled ? null : (bool?)allSucceeded;
|
||||||
|
try { await _store.CompleteRunAsync(runId, overallPass); }
|
||||||
try
|
catch (Exception ex) { _logger.ForModule<CncExecutionService>().Error(ex, "完成检测运行记录失败"); }
|
||||||
{
|
|
||||||
await _store.CompleteRunAsync(runId, overallPass);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.ForModule<CncExecutionService>().Error(ex,
|
|
||||||
"Failed to complete inspection run '{0}'", runId);
|
|
||||||
}
|
|
||||||
} // end try
|
} // end try
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -631,9 +309,10 @@ namespace XplorePlane.Services.Cnc
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Moves all axes to the target position specified by the MotionState.
|
/// 将所有轴移动到目标位置。
|
||||||
/// MotionState positions are in micrometers (μm); ILinearAxis uses millimeters (mm).
|
/// MotionState 中的坐标单位为微米(μm),运动控制接口使用毫米(mm),需除以 1000。
|
||||||
/// Returns MotionResult.Ok() if motion service is not available (graceful degradation).
|
/// 旋转轴单位为度(°),无需转换。
|
||||||
|
/// 运动服务不可用时返回成功(优雅降级,适用于仿真模式)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private Task<MotionResult> MoveToPositionAsync(MotionState target, CancellationToken ct)
|
private Task<MotionResult> MoveToPositionAsync(MotionState target, CancellationToken ct)
|
||||||
{
|
{
|
||||||
@@ -654,22 +333,26 @@ namespace XplorePlane.Services.Cnc
|
|||||||
if (!detectorZResult.Success) return Task.FromResult(detectorZResult);
|
if (!detectorZResult.Success) return Task.FromResult(detectorZResult);
|
||||||
|
|
||||||
// Rotary axes: angles are already in degrees, no conversion needed
|
// Rotary axes: angles are already in degrees, no conversion needed
|
||||||
|
// 禁用轴返回失败时仅记录警告,不中断执行
|
||||||
var detectorSwingResult = _motionControlService.MoveRotaryToTarget(RotaryAxisId.DetectorSwing, target.DetectorSwing);
|
var detectorSwingResult = _motionControlService.MoveRotaryToTarget(RotaryAxisId.DetectorSwing, target.DetectorSwing);
|
||||||
if (!detectorSwingResult.Success) return Task.FromResult(detectorSwingResult);
|
if (!detectorSwingResult.Success)
|
||||||
|
_logger.ForModule<CncExecutionService>().Warn("旋转轴 DetectorSwing 移动跳过:{0}", detectorSwingResult.ErrorMessage);
|
||||||
|
|
||||||
var stageRotationResult = _motionControlService.MoveRotaryToTarget(RotaryAxisId.StageRotation, target.StageRotation);
|
var stageRotationResult = _motionControlService.MoveRotaryToTarget(RotaryAxisId.StageRotation, target.StageRotation);
|
||||||
if (!stageRotationResult.Success) return Task.FromResult(stageRotationResult);
|
if (!stageRotationResult.Success)
|
||||||
|
_logger.ForModule<CncExecutionService>().Warn("旋转轴 StageRotation 移动跳过:{0}", stageRotationResult.ErrorMessage);
|
||||||
|
|
||||||
var fixtureRotationResult = _motionControlService.MoveRotaryToTarget(RotaryAxisId.FixtureRotation, target.FixtureRotation);
|
var fixtureRotationResult = _motionControlService.MoveRotaryToTarget(RotaryAxisId.FixtureRotation, target.FixtureRotation);
|
||||||
if (!fixtureRotationResult.Success) return Task.FromResult(fixtureRotationResult);
|
if (!fixtureRotationResult.Success)
|
||||||
|
_logger.ForModule<CncExecutionService>().Warn("旋转轴 FixtureRotation 移动跳过:{0}", fixtureRotationResult.ErrorMessage);
|
||||||
|
|
||||||
return Task.FromResult(MotionResult.Ok());
|
return Task.FromResult(MotionResult.Ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Polls all axes until they are settled (Status == Idle).
|
/// 轮询所有轴状态,等待全部到位(Status == Idle)。
|
||||||
/// Returns true when all axes are idle, false on timeout (30s).
|
/// 每 50ms 检查一次,超时 30 秒返回 false。
|
||||||
/// Returns true immediately if motion system is not available (graceful degradation).
|
/// 运动系统不可用时直接返回 true(优雅降级)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<bool> WaitForAxesSettledAsync(CancellationToken ct)
|
private async Task<bool> WaitForAxesSettledAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
@@ -708,23 +391,34 @@ namespace XplorePlane.Services.Cnc
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (allIdle)
|
if (allIdle)
|
||||||
|
{
|
||||||
|
_logger.ForModule<CncExecutionService>().Info(
|
||||||
|
"所有轴已到位,等待耗时 {0}ms", sw.ElapsedMilliseconds);
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
await Task.Delay(pollIntervalMs, ct);
|
await Task.Delay(pollIntervalMs, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.ForModule<CncExecutionService>().Warn(
|
||||||
|
"等待轴到位超时({0}ms),继续执行", timeoutMs);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前可用的源图像。
|
||||||
|
/// 优先级:手动加载图像 > 主视口显示图像 > 探测器原始帧(Gray16 转 BitmapSource)。
|
||||||
|
/// 所有源均不可用时返回 null。
|
||||||
|
/// </summary>
|
||||||
private BitmapSource TryGetSourceImage()
|
private BitmapSource TryGetSourceImage()
|
||||||
{
|
{
|
||||||
// ── 优先级 1:MainViewportService 中的手动图像或当前显示图像 ──
|
// ── 优先级 1:MainViewportService 中的手动图像或当前显示图像 ──
|
||||||
var manualImage = _mainViewportService?.LatestManualImage as BitmapSource;
|
var manualImage = _mainViewportService?.LatestManualImage as BitmapSource;
|
||||||
if (manualImage != null)
|
if (manualImage != null)
|
||||||
{
|
{
|
||||||
_logger.ForModule<CncExecutionService>().Info(
|
//_logger.ForModule<CncExecutionService>().Info(
|
||||||
"[图像链路] TryGetSourceImage:从 MainViewportService.LatestManualImage 获取图像,尺寸 {W}x{H}",
|
// "[图像链路] TryGetSourceImage:从 MainViewportService.LatestManualImage 获取图像,尺寸 {W}x{H}",
|
||||||
manualImage.PixelWidth, manualImage.PixelHeight);
|
// manualImage.PixelWidth, manualImage.PixelHeight);
|
||||||
return manualImage;
|
return manualImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -765,7 +459,7 @@ namespace XplorePlane.Services.Cnc
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loads a BitmapSource from a local file path. Returns null if loading fails.
|
/// 从本地文件加载图像。加载失败返回 null。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static BitmapSource LoadImageFromFile(string filePath)
|
private static BitmapSource LoadImageFromFile(string filePath)
|
||||||
{
|
{
|
||||||
@@ -785,6 +479,10 @@ namespace XplorePlane.Services.Cnc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行检测模块节点:运行图像处理流水线,保存输入/输出图像,归档节点结果。
|
||||||
|
/// 返回流水线输出图像(用于 UI 显示和后续节点),执行失败时返回 null。
|
||||||
|
/// </summary>
|
||||||
private async Task<BitmapSource> ExecuteInspectionNodeAsync(
|
private async Task<BitmapSource> ExecuteInspectionNodeAsync(
|
||||||
Guid runId,
|
Guid runId,
|
||||||
InspectionModuleNode inspectionNode,
|
InspectionModuleNode inspectionNode,
|
||||||
@@ -828,6 +526,7 @@ namespace XplorePlane.Services.Cnc
|
|||||||
}
|
}
|
||||||
|
|
||||||
BitmapSource resultImage = null;
|
BitmapSource resultImage = null;
|
||||||
|
IReadOnlyDictionary<string, object> lastOutputData = null;
|
||||||
if (_pipelineExecutionService != null && inspectionNode.Pipeline?.Nodes?.Count > 0 && sourceImage != null)
|
if (_pipelineExecutionService != null && inspectionNode.Pipeline?.Nodes?.Count > 0 && sourceImage != null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -836,6 +535,7 @@ namespace XplorePlane.Services.Cnc
|
|||||||
var execResult = await _pipelineExecutionService.ExecutePipelineAsync(
|
var execResult = await _pipelineExecutionService.ExecutePipelineAsync(
|
||||||
pipelineNodes, sourceImage, null, cancellationToken);
|
pipelineNodes, sourceImage, null, cancellationToken);
|
||||||
resultImage = execResult.Image;
|
resultImage = execResult.Image;
|
||||||
|
lastOutputData = execResult.LastStepOutputData;
|
||||||
|
|
||||||
if (resultImage != null)
|
if (resultImage != null)
|
||||||
{
|
{
|
||||||
@@ -909,10 +609,74 @@ namespace XplorePlane.Services.Cnc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await _store.AppendNodeResultAsync(nodeResult, pipelineSnapshot: pipelineSnapshot, assets: assets);
|
await _store.AppendNodeResultAsync(nodeResult, metrics: ExtractMetrics(runId, inspectionNode, lastOutputData), pipelineSnapshot: pipelineSnapshot, assets: assets);
|
||||||
return resultImage;
|
return resultImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从流水线最后一步的 OutputData 中提取检测指标(BGA空洞率、孔隙测量等)。
|
||||||
|
/// 写入 manifest.json 的 Metrics 部分。
|
||||||
|
/// </summary>
|
||||||
|
private static List<InspectionMetricResult> ExtractMetrics(
|
||||||
|
Guid runId,
|
||||||
|
InspectionModuleNode inspectionNode,
|
||||||
|
IReadOnlyDictionary<string, object> outputData)
|
||||||
|
{
|
||||||
|
var metrics = new List<InspectionMetricResult>();
|
||||||
|
if (outputData == null) return metrics;
|
||||||
|
|
||||||
|
var nodeId = inspectionNode.Id;
|
||||||
|
int order = 0;
|
||||||
|
|
||||||
|
// ── BGA 空洞率检测指标 ──
|
||||||
|
if (outputData.ContainsKey("BgaVoidResult"))
|
||||||
|
{
|
||||||
|
if (outputData.TryGetValue("BgaCount", out var bgaCount))
|
||||||
|
metrics.Add(new InspectionMetricResult { RunId = runId, NodeId = nodeId, MetricKey = "BgaCount", MetricName = "BGA焊球数", MetricValue = Convert.ToDouble(bgaCount), Unit = "个", IsPass = true, DisplayOrder = order++ });
|
||||||
|
|
||||||
|
if (outputData.TryGetValue("VoidRate", out var voidRate))
|
||||||
|
{
|
||||||
|
var rate = Convert.ToDouble(voidRate);
|
||||||
|
var limit = outputData.TryGetValue("VoidLimit", out var vl) ? Convert.ToDouble(vl) : 50.0;
|
||||||
|
var classification = outputData.TryGetValue("Classification", out var cls) ? cls?.ToString() : "";
|
||||||
|
metrics.Add(new InspectionMetricResult { RunId = runId, NodeId = nodeId, MetricKey = "BgaVoidRate", MetricName = "BGA空洞率", MetricValue = Math.Round(rate, 2), Unit = "%", UpperLimit = limit, IsPass = classification == "PASS", DisplayOrder = order++ });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outputData.TryGetValue("FillRate", out var fillRate))
|
||||||
|
metrics.Add(new InspectionMetricResult { RunId = runId, NodeId = nodeId, MetricKey = "BgaFillRate", MetricName = "BGA填充率", MetricValue = Math.Round(Convert.ToDouble(fillRate), 2), Unit = "%", IsPass = true, DisplayOrder = order++ });
|
||||||
|
|
||||||
|
if (outputData.TryGetValue("TotalVoidCount", out var voidCount))
|
||||||
|
metrics.Add(new InspectionMetricResult { RunId = runId, NodeId = nodeId, MetricKey = "BgaTotalVoidCount", MetricName = "气泡总数", MetricValue = Convert.ToDouble(voidCount), Unit = "个", IsPass = true, DisplayOrder = order++ });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 孔隙测量指标 ──
|
||||||
|
if (outputData.ContainsKey("VoidMeasurementResult"))
|
||||||
|
{
|
||||||
|
if (outputData.TryGetValue("VoidRate", out var voidRate))
|
||||||
|
{
|
||||||
|
var rate = Convert.ToDouble(voidRate);
|
||||||
|
var limit = outputData.TryGetValue("VoidLimit", out var vl) ? Convert.ToDouble(vl) : 50.0;
|
||||||
|
var classification = outputData.TryGetValue("Classification", out var cls) ? cls?.ToString() : "";
|
||||||
|
metrics.Add(new InspectionMetricResult { RunId = runId, NodeId = nodeId, MetricKey = "VoidRate", MetricName = "空隙率", MetricValue = Math.Round(rate, 2), Unit = "%", UpperLimit = limit, IsPass = classification == "PASS", DisplayOrder = order++ });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outputData.TryGetValue("VoidCount", out var voidCount))
|
||||||
|
metrics.Add(new InspectionMetricResult { RunId = runId, NodeId = nodeId, MetricKey = "VoidCount", MetricName = "空隙数量", MetricValue = Convert.ToDouble(voidCount), Unit = "个", IsPass = true, DisplayOrder = order++ });
|
||||||
|
|
||||||
|
if (outputData.TryGetValue("MaxVoidArea", out var maxArea))
|
||||||
|
metrics.Add(new InspectionMetricResult { RunId = runId, NodeId = nodeId, MetricKey = "MaxVoidArea", MetricName = "最大空隙面积", MetricValue = Math.Round(Convert.ToDouble(maxArea), 1), Unit = "px²", IsPass = true, DisplayOrder = order++ });
|
||||||
|
|
||||||
|
if (outputData.TryGetValue("RoiArea", out var roiArea))
|
||||||
|
metrics.Add(new InspectionMetricResult { RunId = runId, NodeId = nodeId, MetricKey = "RoiArea", MetricName = "ROI面积", MetricValue = Math.Round(Convert.ToDouble(roiArea), 1), Unit = "px²", IsPass = true, DisplayOrder = order++ });
|
||||||
|
}
|
||||||
|
|
||||||
|
return metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将 PipelineModel(数据模型)转换为 PipelineNodeViewModel 列表(执行模型)。
|
||||||
|
/// 加载每个算子的参数定义和保存值,处理 JsonElement 类型转换。
|
||||||
|
/// </summary>
|
||||||
private System.Collections.Generic.IEnumerable<PipelineNodeViewModel> BuildPipelineNodeViewModels(PipelineModel pipeline)
|
private System.Collections.Generic.IEnumerable<PipelineNodeViewModel> BuildPipelineNodeViewModels(PipelineModel pipeline)
|
||||||
{
|
{
|
||||||
var nodes = new System.Collections.Generic.List<PipelineNodeViewModel>();
|
var nodes = new System.Collections.Generic.List<PipelineNodeViewModel>();
|
||||||
@@ -947,6 +711,10 @@ namespace XplorePlane.Services.Cnc
|
|||||||
return nodes;
|
return nodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将 JsonElement 转换为目标类型(int/double/bool/string)。
|
||||||
|
/// 用于反序列化流水线参数值。
|
||||||
|
/// </summary>
|
||||||
private static object ConvertSavedValue(object savedValue, Type targetType)
|
private static object ConvertSavedValue(object savedValue, Type targetType)
|
||||||
{
|
{
|
||||||
if (savedValue is not JsonElement jsonElement)
|
if (savedValue is not JsonElement jsonElement)
|
||||||
@@ -966,6 +734,9 @@ namespace XplorePlane.Services.Cnc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将 BitmapSource 编码为 BMP 格式字节数组(用于持久化存储)。
|
||||||
|
/// </summary>
|
||||||
private static byte[] EncodeBitmapToBmp(BitmapSource bitmap)
|
private static byte[] EncodeBitmapToBmp(BitmapSource bitmap)
|
||||||
{
|
{
|
||||||
using var ms = new MemoryStream();
|
using var ms = new MemoryStream();
|
||||||
@@ -975,6 +746,10 @@ namespace XplorePlane.Services.Cnc
|
|||||||
return ms.ToArray();
|
return ms.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行延时等待节点,每 50ms 报告一次进度百分比。
|
||||||
|
/// 支持取消(抛出 OperationCanceledException)。
|
||||||
|
/// </summary>
|
||||||
private static async Task ExecuteWaitDelayWithProgressAsync(
|
private static async Task ExecuteWaitDelayWithProgressAsync(
|
||||||
WaitDelayNode waitNode,
|
WaitDelayNode waitNode,
|
||||||
IProgress<CncNodeExecutionProgress> progress,
|
IProgress<CncNodeExecutionProgress> progress,
|
||||||
|
|||||||
@@ -374,8 +374,8 @@ namespace XplorePlane.Services.Cnc
|
|||||||
result.Add(indexedNode switch
|
result.Add(indexedNode switch
|
||||||
{
|
{
|
||||||
ReferencePointNode referencePointNode => referencePointNode with { Name = $"\u53C2\u8003\u70B9_{referencePointNumber++}" },
|
ReferencePointNode referencePointNode => referencePointNode with { Name = $"\u53C2\u8003\u70B9_{referencePointNumber++}" },
|
||||||
SavePositionNode savePositionNode => savePositionNode with { Name = $"\u4FDD\u5B58\u4F4D\u7F6E_{savePositionNumber++}" },
|
SavePositionNode savePositionNode => savePositionNode with { Name = $"\u4F4D\u7F6E{++savePositionNumber}" },
|
||||||
InspectionModuleNode inspectionModuleNode => inspectionModuleNode with { Name = $"\u68C0\u6D4B\u6A21\u5757_{inspectionModuleNumber++}" },
|
InspectionModuleNode inspectionModuleNode => inspectionModuleNode with { Name = $"\u6A21\u5757{++inspectionModuleNumber}" },
|
||||||
_ => indexedNode
|
_ => indexedNode
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -389,19 +389,19 @@ namespace XplorePlane.Services.Cnc
|
|||||||
var raySource = _appStateService.RaySourceState;
|
var raySource = _appStateService.RaySourceState;
|
||||||
return new ReferencePointNode(
|
return new ReferencePointNode(
|
||||||
id, index, $"参考点_{index}",
|
id, index, $"参考点_{index}",
|
||||||
StageX: motion.StageX,
|
StageX: Math.Round(motion.StageX, 3),
|
||||||
StageY: motion.StageY,
|
StageY: Math.Round(motion.StageY, 3),
|
||||||
SourceZ: motion.SourceZ,
|
SourceZ: Math.Round(motion.SourceZ, 3),
|
||||||
DetectorZ: motion.DetectorZ,
|
DetectorZ: Math.Round(motion.DetectorZ, 3),
|
||||||
DetectorSwing: motion.DetectorSwing,
|
DetectorSwing: Math.Round(motion.DetectorSwing, 3),
|
||||||
FDD: motion.FDD,
|
FDD: Math.Round(motion.FDD, 3),
|
||||||
IsRayOn: raySource.IsOn,
|
IsRayOn: raySource.IsOn,
|
||||||
Voltage: raySource.Voltage,
|
Voltage: Math.Round(raySource.Voltage, 3),
|
||||||
Current: TryReadCurrent(),
|
Current: Math.Round(TryReadCurrent(), 3),
|
||||||
StageRotation: motion.StageRotation,
|
StageRotation: Math.Round(motion.StageRotation, 3),
|
||||||
FixtureRotation: motion.FixtureRotation,
|
FixtureRotation: Math.Round(motion.FixtureRotation, 3),
|
||||||
FOD: motion.FOD,
|
FOD: Math.Round(motion.FOD, 3),
|
||||||
Magnification: motion.Magnification);
|
Magnification: Math.Round(motion.Magnification, 3));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>创建保存节点(含图像)| Create save node with image</summary>
|
/// <summary>创建保存节点(含图像)| Create save node with image</summary>
|
||||||
@@ -409,7 +409,7 @@ namespace XplorePlane.Services.Cnc
|
|||||||
{
|
{
|
||||||
return new SaveNodeWithImageNode(
|
return new SaveNodeWithImageNode(
|
||||||
id, index, $"保存节点_图像_{index}",
|
id, index, $"保存节点_图像_{index}",
|
||||||
MotionState: _appStateService.MotionState,
|
MotionState: RoundMotionState(_appStateService.MotionState),
|
||||||
RaySourceState: _appStateService.RaySourceState,
|
RaySourceState: _appStateService.RaySourceState,
|
||||||
DetectorState: _appStateService.DetectorState,
|
DetectorState: _appStateService.DetectorState,
|
||||||
ImageFileName: "");
|
ImageFileName: "");
|
||||||
@@ -420,7 +420,7 @@ namespace XplorePlane.Services.Cnc
|
|||||||
{
|
{
|
||||||
return new SaveNodeNode(
|
return new SaveNodeNode(
|
||||||
id, index, $"保存节点_{index}",
|
id, index, $"保存节点_{index}",
|
||||||
MotionState: _appStateService.MotionState,
|
MotionState: RoundMotionState(_appStateService.MotionState),
|
||||||
RaySourceState: _appStateService.RaySourceState,
|
RaySourceState: _appStateService.RaySourceState,
|
||||||
DetectorState: _appStateService.DetectorState);
|
DetectorState: _appStateService.DetectorState);
|
||||||
}
|
}
|
||||||
@@ -430,9 +430,30 @@ namespace XplorePlane.Services.Cnc
|
|||||||
{
|
{
|
||||||
return new SavePositionNode(
|
return new SavePositionNode(
|
||||||
id, index, $"检测位置_{index}",
|
id, index, $"检测位置_{index}",
|
||||||
MotionState: _appStateService.MotionState,
|
MotionState: RoundMotionState(_appStateService.MotionState),
|
||||||
SaveImage: false);
|
SaveImage: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将 MotionState 中的位置/角度值四舍五入到 3 位小数。
|
||||||
|
/// 避免浮点精度噪声写入 CNC 程序文件。
|
||||||
|
/// </summary>
|
||||||
|
private static MotionState RoundMotionState(MotionState s, int decimals = 3)
|
||||||
|
{
|
||||||
|
return s with
|
||||||
|
{
|
||||||
|
StageX = Math.Round(s.StageX, decimals),
|
||||||
|
StageY = Math.Round(s.StageY, decimals),
|
||||||
|
SourceZ = Math.Round(s.SourceZ, decimals),
|
||||||
|
DetectorZ = Math.Round(s.DetectorZ, decimals),
|
||||||
|
DetectorSwing = Math.Round(s.DetectorSwing, decimals),
|
||||||
|
FDD = Math.Round(s.FDD, decimals),
|
||||||
|
StageRotation = Math.Round(s.StageRotation, decimals),
|
||||||
|
FixtureRotation = Math.Round(s.FixtureRotation, decimals),
|
||||||
|
FOD = Math.Round(s.FOD, decimals),
|
||||||
|
Magnification = Math.Round(s.Magnification, decimals)
|
||||||
|
};
|
||||||
|
}
|
||||||
private double TryReadCurrent()
|
private double TryReadCurrent()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -103,7 +103,8 @@ namespace XplorePlane.Services.Cnc
|
|||||||
|
|
||||||
var options = new JsonSerializerOptions
|
var options = new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
WriteIndented = true
|
WriteIndented = true,
|
||||||
|
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||||
};
|
};
|
||||||
|
|
||||||
var json = JsonSerializer.Serialize(summary, options);
|
var json = JsonSerializer.Serialize(summary, options);
|
||||||
|
|||||||
@@ -77,11 +77,11 @@ namespace XplorePlane.Services.MainViewport
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.Info(
|
//_logger.Info(
|
||||||
"[图像链路] DetectorFramePipeline 收到帧 #{N},分辨率 {W}x{H},采集时间 {T},当前队列深度 AcquireQ={AQ} ProcessQ={PQ}",
|
// "[图像链路] DetectorFramePipeline 收到帧 #{N},分辨率 {W}x{H},采集时间 {T},当前队列深度 AcquireQ={AQ} ProcessQ={PQ}",
|
||||||
args.FrameNumber, args.Width, args.Height,
|
// args.FrameNumber, args.Width, args.Height,
|
||||||
args.CaptureTime.ToString("HH:mm:ss.fff"),
|
// args.CaptureTime.ToString("HH:mm:ss.fff"),
|
||||||
AcquireQueueCount, ProcessQueueCount);
|
// AcquireQueueCount, ProcessQueueCount);
|
||||||
|
|
||||||
var rawPixels = new ushort[args.ImageData.Length];
|
var rawPixels = new ushort[args.ImageData.Length];
|
||||||
Array.Copy(args.ImageData, rawPixels, rawPixels.Length);
|
Array.Copy(args.ImageData, rawPixels, rawPixels.Length);
|
||||||
@@ -105,7 +105,7 @@ namespace XplorePlane.Services.MainViewport
|
|||||||
{
|
{
|
||||||
EnqueueBounded(_processQueue, frame, ProcessQueueCapacity, ref _processQueueCount);
|
EnqueueBounded(_processQueue, frame, ProcessQueueCapacity, ref _processQueueCount);
|
||||||
_processSignal.Release();
|
_processSignal.Release();
|
||||||
_logger.Info(
|
_logger.Debug(
|
||||||
"[图像链路] 帧 #{N} 已入处理队列(每 {Every} 帧采样一次),累计接收 {Total} 帧",
|
"[图像链路] 帧 #{N} 已入处理队列(每 {Every} 帧采样一次),累计接收 {Total} 帧",
|
||||||
args.FrameNumber, ProcessEveryNFrames, sequence);
|
args.FrameNumber, ProcessEveryNFrames, sequence);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
private readonly ICncExecutionService _cncExecutionService;
|
private readonly ICncExecutionService _cncExecutionService;
|
||||||
private readonly IXpDataPathService _dataPathService;
|
private readonly IXpDataPathService _dataPathService;
|
||||||
private readonly IPipelinePersistenceService _pipelinePersistenceService;
|
private readonly IPipelinePersistenceService _pipelinePersistenceService;
|
||||||
|
private readonly IImageProcessingService _imageProcessingService;
|
||||||
|
|
||||||
private CncProgram _currentProgram;
|
private CncProgram _currentProgram;
|
||||||
private ObservableCollection<CncNodeViewModel> _nodes;
|
private ObservableCollection<CncNodeViewModel> _nodes;
|
||||||
@@ -57,7 +58,8 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
ILoggerService logger,
|
ILoggerService logger,
|
||||||
ICncExecutionService cncExecutionService,
|
ICncExecutionService cncExecutionService,
|
||||||
IXpDataPathService dataPathService,
|
IXpDataPathService dataPathService,
|
||||||
IPipelinePersistenceService pipelinePersistenceService)
|
IPipelinePersistenceService pipelinePersistenceService,
|
||||||
|
IImageProcessingService imageProcessingService = null)
|
||||||
{
|
{
|
||||||
_cncProgramService = cncProgramService ?? throw new ArgumentNullException(nameof(cncProgramService));
|
_cncProgramService = cncProgramService ?? throw new ArgumentNullException(nameof(cncProgramService));
|
||||||
ArgumentNullException.ThrowIfNull(appStateService);
|
ArgumentNullException.ThrowIfNull(appStateService);
|
||||||
@@ -66,6 +68,7 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
_cncExecutionService = cncExecutionService ?? throw new ArgumentNullException(nameof(cncExecutionService));
|
_cncExecutionService = cncExecutionService ?? throw new ArgumentNullException(nameof(cncExecutionService));
|
||||||
_dataPathService = dataPathService ?? throw new ArgumentNullException(nameof(dataPathService));
|
_dataPathService = dataPathService ?? throw new ArgumentNullException(nameof(dataPathService));
|
||||||
_pipelinePersistenceService = pipelinePersistenceService ?? throw new ArgumentNullException(nameof(pipelinePersistenceService));
|
_pipelinePersistenceService = pipelinePersistenceService ?? throw new ArgumentNullException(nameof(pipelinePersistenceService));
|
||||||
|
_imageProcessingService = imageProcessingService; // optional — used for pipeline step display names
|
||||||
|
|
||||||
_nodes = new ObservableCollection<CncNodeViewModel>();
|
_nodes = new ObservableCollection<CncNodeViewModel>();
|
||||||
_treeNodes = new ObservableCollection<CncNodeViewModel>();
|
_treeNodes = new ObservableCollection<CncNodeViewModel>();
|
||||||
@@ -661,6 +664,12 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
IsExpanded = expansionState.TryGetValue(node.Id, out var isExpanded) ? isExpanded : true
|
IsExpanded = expansionState.TryGetValue(node.Id, out var isExpanded) ? isExpanded : true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 为检测模块节点填充流水线步骤(三级树节点)
|
||||||
|
if (node is InspectionModuleNode imNode)
|
||||||
|
{
|
||||||
|
vm.SyncPipelineSteps(imNode.Pipeline, _imageProcessingService);
|
||||||
|
}
|
||||||
|
|
||||||
flatNodes.Add(vm);
|
flatNodes.Add(vm);
|
||||||
|
|
||||||
if (vm.IsSavePosition)
|
if (vm.IsSavePosition)
|
||||||
@@ -996,15 +1005,50 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
result.Add(indexedNode switch
|
result.Add(indexedNode switch
|
||||||
{
|
{
|
||||||
ReferencePointNode referencePointNode => referencePointNode with { Name = $"\u53C2\u8003\u70B9_{referencePointNumber++}" },
|
ReferencePointNode referencePointNode => referencePointNode with { Name = $"\u53C2\u8003\u70B9_{referencePointNumber++}" },
|
||||||
SavePositionNode savePositionNode => savePositionNode with { Name = $"\u4FDD\u5B58\u4F4D\u7F6E_{savePositionNumber++}" },
|
// 保存位置:用户自定义名称时保留,否则用"位置N"(1-based)
|
||||||
InspectionModuleNode inspectionModuleNode => inspectionModuleNode with { Name = $"\u68C0\u6D4B\u6A21\u5757_{inspectionModuleNumber++}" },
|
SavePositionNode savePositionNode => IsDefaultSavePositionName(savePositionNode.Name)
|
||||||
|
? savePositionNode with { Name = $"\u4F4D\u7F6E{++savePositionNumber}" }
|
||||||
|
: (CncNode)(savePositionNode with { Index = i }),
|
||||||
|
// 检测模块:用户自定义名称时保留,否则用"模块N"(1-based)
|
||||||
|
InspectionModuleNode inspectionModuleNode => IsDefaultInspectionModuleName(inspectionModuleNode.Name)
|
||||||
|
? inspectionModuleNode with { Name = $"\u6A21\u5757{++inspectionModuleNumber}" }
|
||||||
|
: (CncNode)(inspectionModuleNode with { Index = i }),
|
||||||
_ => indexedNode
|
_ => indexedNode
|
||||||
});
|
});
|
||||||
|
// 无论是否重命名,计数器都要递增以保持后续编号连续
|
||||||
|
if (indexedNode is SavePositionNode sp && !IsDefaultSavePositionName(sp.Name))
|
||||||
|
savePositionNumber++;
|
||||||
|
if (indexedNode is InspectionModuleNode im && !IsDefaultInspectionModuleName(im.Name))
|
||||||
|
inspectionModuleNumber++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.AsReadOnly();
|
return result.AsReadOnly();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>判断是否为系统默认的保存位置名称("位置N" 格式)</summary>
|
||||||
|
private static bool IsDefaultSavePositionName(string name)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(name)) return true;
|
||||||
|
if (name.StartsWith("\u4F4D\u7F6E", StringComparison.Ordinal))
|
||||||
|
return int.TryParse(name[2..], out _);
|
||||||
|
// 兼容旧格式 "保存位置_N"
|
||||||
|
if (name.StartsWith("\u4FDD\u5B58\u4F4D\u7F6E_", StringComparison.Ordinal))
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>判断是否为系统默认的检测模块名称("模块N" 或旧格式 "检测模块_N")</summary>
|
||||||
|
private static bool IsDefaultInspectionModuleName(string name)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(name)) return true;
|
||||||
|
if (name.StartsWith("\u6A21\u5757", StringComparison.Ordinal))
|
||||||
|
return int.TryParse(name[2..], out _);
|
||||||
|
// 兼容旧格式 "检测模块_N"
|
||||||
|
if (name.StartsWith("\u68C0\u6D4B\u6A21\u5757_", StringComparison.Ordinal))
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsSavePositionChild(CncNodeType type)
|
private static bool IsSavePositionChild(CncNodeType type)
|
||||||
{
|
{
|
||||||
return type is CncNodeType.InspectionMarker
|
return type is CncNodeType.InspectionMarker
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Prism.Commands;
|
|||||||
using Prism.Mvvm;
|
using Prism.Mvvm;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
using XplorePlane.Models;
|
using XplorePlane.Models;
|
||||||
@@ -30,10 +31,18 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
_modelChangedCallback = modelChangedCallback ?? throw new ArgumentNullException(nameof(modelChangedCallback));
|
_modelChangedCallback = modelChangedCallback ?? throw new ArgumentNullException(nameof(modelChangedCallback));
|
||||||
_icon = GetIconForNodeType(model.NodeType);
|
_icon = GetIconForNodeType(model.NodeType);
|
||||||
Children = new ObservableCollection<CncNodeViewModel>();
|
Children = new ObservableCollection<CncNodeViewModel>();
|
||||||
|
PipelineSteps = new ObservableCollection<CncPipelineStepViewModel>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObservableCollection<CncNodeViewModel> Children { get; }
|
public ObservableCollection<CncNodeViewModel> Children { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检测模块的流水线步骤列表(三级节点)。
|
||||||
|
/// 仅 IsInspectionModule == true 时有内容。
|
||||||
|
/// Pipeline steps for InspectionModule nodes (3rd-level tree nodes).
|
||||||
|
/// </summary>
|
||||||
|
public ObservableCollection<CncPipelineStepViewModel> PipelineSteps { get; }
|
||||||
|
|
||||||
public CncNode Model => _model;
|
public CncNode Model => _model;
|
||||||
|
|
||||||
public Guid Id => _model.Id;
|
public Guid Id => _model.Id;
|
||||||
@@ -415,6 +424,7 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
if (_model is InspectionModuleNode im)
|
if (_model is InspectionModuleNode im)
|
||||||
{
|
{
|
||||||
UpdateModel(im with { Pipeline = value ?? new PipelineModel() });
|
UpdateModel(im with { Pipeline = value ?? new PipelineModel() });
|
||||||
|
SyncPipelineSteps(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -684,6 +694,7 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
RaisePropertyChanged(nameof(ManualImagePath));
|
RaisePropertyChanged(nameof(ManualImagePath));
|
||||||
RaisePropertyChanged(nameof(Pipeline));
|
RaisePropertyChanged(nameof(Pipeline));
|
||||||
RaisePropertyChanged(nameof(PipelineName));
|
RaisePropertyChanged(nameof(PipelineName));
|
||||||
|
RaisePropertyChanged(nameof(PipelineSteps));
|
||||||
RaisePropertyChanged(nameof(MarkerType));
|
RaisePropertyChanged(nameof(MarkerType));
|
||||||
RaisePropertyChanged(nameof(MarkerX));
|
RaisePropertyChanged(nameof(MarkerX));
|
||||||
RaisePropertyChanged(nameof(MarkerY));
|
RaisePropertyChanged(nameof(MarkerY));
|
||||||
@@ -699,6 +710,38 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
RaisePropertyChanged(nameof(ExecutionProgressText));
|
RaisePropertyChanged(nameof(ExecutionProgressText));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 树形子节点集合:
|
||||||
|
/// - 对于检测模块节点,返回 PipelineSteps(流水线步骤作为三级节点)
|
||||||
|
/// - 对于其他节点,返回 Children(CNC 子节点)
|
||||||
|
/// Tree children: PipelineSteps for InspectionModule nodes, Children otherwise.
|
||||||
|
/// </summary>
|
||||||
|
public System.Collections.IEnumerable TreeChildren =>
|
||||||
|
IsInspectionModule ? (System.Collections.IEnumerable)PipelineSteps : Children;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将 PipelineModel 的节点同步到 PipelineSteps 集合(三级树节点)。
|
||||||
|
/// Syncs PipelineModel nodes into the PipelineSteps collection (3rd-level tree nodes).
|
||||||
|
/// </summary>
|
||||||
|
public void SyncPipelineSteps(PipelineModel pipeline, Services.IImageProcessingService imageProcessingService = null)
|
||||||
|
{
|
||||||
|
PipelineSteps.Clear();
|
||||||
|
if (pipeline?.Nodes == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var node in pipeline.Nodes.OrderBy(n => n.Order))
|
||||||
|
{
|
||||||
|
// 优先使用服务获取中文显示名,回退到 OperatorKey
|
||||||
|
var displayName = imageProcessingService?.GetProcessorDisplayName(node.OperatorKey)
|
||||||
|
?? node.OperatorKey;
|
||||||
|
var icon = Services.ProcessorUiMetadata.GetOperatorIcon(node.OperatorKey);
|
||||||
|
PipelineSteps.Add(new CncPipelineStepViewModel(
|
||||||
|
displayName: displayName,
|
||||||
|
isEnabled: node.IsEnabled,
|
||||||
|
iconPath: icon));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private enum MotionAxis
|
private enum MotionAxis
|
||||||
{
|
{
|
||||||
StageX,
|
StageX,
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
using Prism.Mvvm;
|
||||||
|
|
||||||
|
namespace XplorePlane.ViewModels.Cnc
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 检测模块流水线步骤的轻量 ViewModel,用于在 CNC 树形结构中作为三级节点显示。
|
||||||
|
/// Lightweight ViewModel for a pipeline step inside an InspectionModule node,
|
||||||
|
/// displayed as a 3rd-level node in the CNC tree.
|
||||||
|
/// </summary>
|
||||||
|
public class CncPipelineStepViewModel : BindableBase
|
||||||
|
{
|
||||||
|
private string _displayName;
|
||||||
|
private bool _isEnabled;
|
||||||
|
private string _iconPath;
|
||||||
|
|
||||||
|
public CncPipelineStepViewModel(string displayName, bool isEnabled, string iconPath = null)
|
||||||
|
{
|
||||||
|
_displayName = displayName;
|
||||||
|
_isEnabled = isEnabled;
|
||||||
|
_iconPath = iconPath ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>算子显示名称(中文)| Operator display name</summary>
|
||||||
|
public string DisplayName
|
||||||
|
{
|
||||||
|
get => _displayName;
|
||||||
|
set => SetProperty(ref _displayName, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>是否启用 | Whether the step is enabled</summary>
|
||||||
|
public bool IsEnabled
|
||||||
|
{
|
||||||
|
get => _isEnabled;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _isEnabled, value))
|
||||||
|
RaisePropertyChanged(nameof(StateText));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>图标路径 | Icon path</summary>
|
||||||
|
public string IconPath
|
||||||
|
{
|
||||||
|
get => _iconPath;
|
||||||
|
set => SetProperty(ref _iconPath, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>状态文字(已启用 / 已停用)| State text</summary>
|
||||||
|
public string StateText => _isEnabled ? "已启用" : "已停用";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,7 +48,7 @@ namespace XplorePlane.ViewModels
|
|||||||
{
|
{
|
||||||
public class MainViewModel : BindableBase
|
public class MainViewModel : BindableBase
|
||||||
{
|
{
|
||||||
private const double CncEditorHostWidth = 452d;
|
private const double CncEditorHostWidth = 560d;
|
||||||
private readonly ILoggerService _logger;
|
private readonly ILoggerService _logger;
|
||||||
private readonly IContainerProvider _containerProvider;
|
private readonly IContainerProvider _containerProvider;
|
||||||
private readonly IEventAggregator _eventAggregator;
|
private readonly IEventAggregator _eventAggregator;
|
||||||
|
|||||||
@@ -20,18 +20,105 @@
|
|||||||
<local:BoolToDisplayTextConverter x:Key="BoolToDisplayTextConverter" />
|
<local:BoolToDisplayTextConverter x:Key="BoolToDisplayTextConverter" />
|
||||||
<local:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
|
<local:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
|
||||||
|
|
||||||
<SolidColorBrush x:Key="PanelBg" Color="White" />
|
<SolidColorBrush x:Key="PanelBg" Color="#F5F6FA" />
|
||||||
<SolidColorBrush x:Key="PanelBorder" Color="#CDCBCB" />
|
<SolidColorBrush x:Key="PanelBorder" Color="#CDCBCB" />
|
||||||
<SolidColorBrush x:Key="SeparatorBrush" Color="#E5E5E5" />
|
<SolidColorBrush x:Key="SeparatorBrush" Color="#E5E5E5" />
|
||||||
<SolidColorBrush x:Key="HeaderBg" Color="#F7F7F7" />
|
<SolidColorBrush x:Key="HeaderBg" Color="#F0F1F5" />
|
||||||
<SolidColorBrush x:Key="TreeChildLineBrush" Color="#C7D4DF" />
|
<SolidColorBrush x:Key="TreeChildLineBrush" Color="#C8CDD8" />
|
||||||
<FontFamily x:Key="UiFont">Microsoft YaHei UI</FontFamily>
|
<FontFamily x:Key="UiFont">Microsoft YaHei UI</FontFamily>
|
||||||
|
|
||||||
|
<!-- ══════════════════════════════════════════════
|
||||||
|
现代风格 TreeViewItem:实心三角展开箭头
|
||||||
|
══════════════════════════════════════════════ -->
|
||||||
|
|
||||||
|
<!-- 展开/折叠箭头:实心三角 ▶ / ▼ -->
|
||||||
|
<Style x:Key="ExpandCollapseToggleStyle" TargetType="ToggleButton">
|
||||||
|
<Setter Property="Focusable" Value="False" />
|
||||||
|
<Setter Property="Width" Value="16" />
|
||||||
|
<Setter Property="Height" Value="16" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ToggleButton">
|
||||||
|
<Grid Width="16" Height="16" Background="Transparent">
|
||||||
|
<!-- 折叠状态:▶ 右向三角 -->
|
||||||
|
<Path
|
||||||
|
x:Name="ArrowRight"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
|
Data="M 0,0 L 5,4 L 0,8 Z"
|
||||||
|
Fill="#666666"
|
||||||
|
Stretch="None" />
|
||||||
|
<!-- 展开状态:▼ 下向三角 -->
|
||||||
|
<Path
|
||||||
|
x:Name="ArrowDown"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
|
Data="M 0,0 L 8,0 L 4,5 Z"
|
||||||
|
Fill="#666666"
|
||||||
|
Stretch="None"
|
||||||
|
Visibility="Collapsed" />
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Setter TargetName="ArrowRight" Property="Visibility" Value="Collapsed" />
|
||||||
|
<Setter TargetName="ArrowDown" Property="Visibility" Value="Visible" />
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- TreeViewItem ControlTemplate -->
|
||||||
<Style x:Key="TreeItemStyle" TargetType="TreeViewItem">
|
<Style x:Key="TreeItemStyle" TargetType="TreeViewItem">
|
||||||
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
|
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
|
||||||
<Setter Property="Padding" Value="0" />
|
<Setter Property="Padding" Value="0" />
|
||||||
<Setter Property="Margin" Value="0" />
|
<Setter Property="Margin" Value="0,0,0,0" />
|
||||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="TreeViewItem">
|
||||||
|
<StackPanel>
|
||||||
|
<!-- 节点行:箭头 + 内容 -->
|
||||||
|
<Grid x:Name="HeaderRow">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="20" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- 展开/折叠箭头 -->
|
||||||
|
<ToggleButton
|
||||||
|
x:Name="Expander"
|
||||||
|
Grid.Column="0"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
|
IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource TemplatedParent}}"
|
||||||
|
Style="{StaticResource ExpandCollapseToggleStyle}"
|
||||||
|
Visibility="Hidden" />
|
||||||
|
|
||||||
|
<!-- 节点内容 -->
|
||||||
|
<ContentPresenter
|
||||||
|
x:Name="PART_Header"
|
||||||
|
Grid.Column="1"
|
||||||
|
ContentSource="Header"
|
||||||
|
HorizontalAlignment="Stretch" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- 子节点区域 -->
|
||||||
|
<ItemsPresenter
|
||||||
|
x:Name="ItemsHost"
|
||||||
|
Margin="8,0,0,0"
|
||||||
|
Visibility="Collapsed" />
|
||||||
|
</StackPanel>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="HasItems" Value="True">
|
||||||
|
<Setter TargetName="Expander" Property="Visibility" Value="Visible" />
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsExpanded" Value="True">
|
||||||
|
<Setter TargetName="ItemsHost" Property="Visibility" Value="Visible" />
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style x:Key="EditorTitle" TargetType="TextBlock">
|
<Style x:Key="EditorTitle" TargetType="TextBlock">
|
||||||
@@ -114,8 +201,8 @@
|
|||||||
</UserControl.Resources>
|
</UserControl.Resources>
|
||||||
|
|
||||||
<Border
|
<Border
|
||||||
Width="452"
|
Width="560"
|
||||||
MinWidth="452"
|
MinWidth="560"
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
Background="{StaticResource PanelBg}"
|
Background="{StaticResource PanelBg}"
|
||||||
BorderBrush="{StaticResource PanelBorder}"
|
BorderBrush="{StaticResource PanelBorder}"
|
||||||
@@ -123,7 +210,7 @@
|
|||||||
CornerRadius="4">
|
CornerRadius="4">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="180" />
|
<ColumnDefinition Width="260" />
|
||||||
<ColumnDefinition Width="1" />
|
<ColumnDefinition Width="1" />
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
@@ -141,6 +228,16 @@
|
|||||||
BorderBrush="{StaticResource SeparatorBrush}"
|
BorderBrush="{StaticResource SeparatorBrush}"
|
||||||
BorderThickness="0,0,0,1">
|
BorderThickness="0,0,0,1">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
|
<!-- 文件名标签:显示当前打开的程序文件名 -->
|
||||||
|
<TextBlock
|
||||||
|
Margin="2,0,0,4"
|
||||||
|
FontFamily="{StaticResource UiFont}"
|
||||||
|
FontSize="12"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Foreground="#2B8A3E"
|
||||||
|
Text="{Binding ProgramTreeRoots[0].DisplayName, StringFormat='当前加载:{0}'}"
|
||||||
|
TextTrimming="CharacterEllipsis"
|
||||||
|
ToolTip="{Binding ProgramTreeRoots[0].DisplayName}" />
|
||||||
<WrapPanel/>
|
<WrapPanel/>
|
||||||
<WrapPanel Margin="0,4,0,0" Visibility="Collapsed">
|
<WrapPanel Margin="0,4,0,0" Visibility="Collapsed">
|
||||||
<Button
|
<Button
|
||||||
@@ -251,202 +348,249 @@
|
|||||||
Padding="3,5"
|
Padding="3,5"
|
||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
BorderThickness="0"
|
BorderThickness="0"
|
||||||
ItemsSource="{Binding ProgramTreeRoots}"
|
ItemsSource="{Binding ProgramTreeRoots[0].Children}"
|
||||||
PreviewKeyDown="CncTreeView_PreviewKeyDown"
|
PreviewKeyDown="CncTreeView_PreviewKeyDown"
|
||||||
SelectedItemChanged="CncTreeView_SelectedItemChanged">
|
SelectedItemChanged="CncTreeView_SelectedItemChanged">
|
||||||
<TreeView.Resources>
|
<TreeView.Resources>
|
||||||
<HierarchicalDataTemplate
|
|
||||||
DataType="{x:Type vm:CncProgramTreeRootViewModel}"
|
|
||||||
ItemContainerStyle="{StaticResource TreeItemStyle}"
|
|
||||||
ItemsSource="{Binding Children}">
|
|
||||||
<Border
|
|
||||||
x:Name="ProgramRootCard"
|
|
||||||
Margin="0,1,0,3"
|
|
||||||
Padding="0,2"
|
|
||||||
Background="Transparent"
|
|
||||||
BorderBrush="Transparent"
|
|
||||||
BorderThickness="1"
|
|
||||||
CornerRadius="4">
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<TextBlock
|
|
||||||
Margin="1,0,4,0"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
FontFamily="{StaticResource UiFont}"
|
|
||||||
FontSize="12"
|
|
||||||
Foreground="#2B8A3E"
|
|
||||||
Text="◆" />
|
|
||||||
<TextBlock
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
FontFamily="{StaticResource UiFont}"
|
|
||||||
FontSize="12"
|
|
||||||
FontWeight="SemiBold"
|
|
||||||
x:Name="ProgramRootNameText"
|
|
||||||
Text="{Binding DisplayName}"
|
|
||||||
TextTrimming="CharacterEllipsis" />
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
<DataTemplate.Triggers>
|
|
||||||
<DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=TreeViewItem}}" Value="True">
|
|
||||||
<Setter TargetName="ProgramRootCard" Property="Background" Value="#E7F1FB" />
|
|
||||||
<Setter TargetName="ProgramRootCard" Property="BorderBrush" Value="#9FC6E8" />
|
|
||||||
<Setter TargetName="ProgramRootNameText" Property="Foreground" Value="#111111" />
|
|
||||||
</DataTrigger>
|
|
||||||
</DataTemplate.Triggers>
|
|
||||||
</HierarchicalDataTemplate>
|
|
||||||
|
|
||||||
|
<!-- ── 2级节点:CncNodeViewModel ── -->
|
||||||
<HierarchicalDataTemplate
|
<HierarchicalDataTemplate
|
||||||
DataType="{x:Type vm:CncNodeViewModel}"
|
DataType="{x:Type vm:CncNodeViewModel}"
|
||||||
ItemContainerStyle="{StaticResource TreeItemStyle}"
|
ItemsSource="{Binding TreeChildren}">
|
||||||
ItemsSource="{Binding Children}">
|
<!-- 节点行:圆角背景 + 图标 + 名称 + 状态点 + ⋮ 菜单 -->
|
||||||
<Border
|
<Border
|
||||||
x:Name="NodeCard"
|
x:Name="NodeBg"
|
||||||
Margin="0,1,0,1"
|
Margin="0,1,4,1"
|
||||||
Padding="0"
|
Padding="6,4,6,4"
|
||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
BorderBrush="Transparent"
|
CornerRadius="6"
|
||||||
BorderThickness="1"
|
SnapsToDevicePixels="True">
|
||||||
CornerRadius="4">
|
<Grid>
|
||||||
<Grid x:Name="NodeRoot" MinHeight="23">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="15" />
|
<ColumnDefinition Width="22" />
|
||||||
<ColumnDefinition Width="20" />
|
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="24" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<Grid Grid.RowSpan="2" Grid.Column="0">
|
<!-- 图标 -->
|
||||||
<Border
|
<Image
|
||||||
x:Name="ChildStem"
|
Grid.Column="0"
|
||||||
Width="1"
|
Width="16" Height="16"
|
||||||
Margin="7,0,0,0"
|
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
HorizontalAlignment="Left"
|
Source="{Binding Icon}"
|
||||||
Background="{StaticResource TreeChildLineBrush}" />
|
Stretch="Uniform" />
|
||||||
<Border
|
|
||||||
x:Name="ChildBranch"
|
|
||||||
Width="8"
|
|
||||||
Height="1"
|
|
||||||
Margin="7,0,0,0"
|
|
||||||
HorizontalAlignment="Left"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Background="{StaticResource TreeChildLineBrush}" />
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Border
|
|
||||||
Grid.RowSpan="2"
|
|
||||||
Grid.Column="1"
|
|
||||||
Width="16"
|
|
||||||
Height="16"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Background="Transparent"
|
|
||||||
CornerRadius="4">
|
|
||||||
<Image
|
|
||||||
Width="13"
|
|
||||||
Height="13"
|
|
||||||
Source="{Binding Icon}"
|
|
||||||
Stretch="Uniform" />
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
|
<!-- 名称(普通显示) -->
|
||||||
<TextBlock
|
<TextBlock
|
||||||
x:Name="NodeNameText"
|
x:Name="NodeNameText"
|
||||||
Grid.Row="0"
|
Grid.Column="1"
|
||||||
|
Margin="4,0,8,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontFamily="{StaticResource UiFont}"
|
||||||
|
FontSize="12"
|
||||||
|
Foreground="#1A1A2E"
|
||||||
|
Text="{Binding Name}"
|
||||||
|
TextTrimming="CharacterEllipsis"
|
||||||
|
MouseLeftButtonDown="NodeNameText_MouseLeftButtonDown" />
|
||||||
|
|
||||||
|
<!-- 名称(编辑模式,仅 SavePosition 节点双击后显示) -->
|
||||||
|
<TextBox
|
||||||
|
x:Name="NodeNameEditor"
|
||||||
|
Grid.Column="1"
|
||||||
|
Margin="2,0,6,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontFamily="{StaticResource UiFont}"
|
||||||
|
FontSize="12"
|
||||||
|
Text="{Binding Name, UpdateSourceTrigger=Explicit}"
|
||||||
|
Visibility="Collapsed"
|
||||||
|
BorderBrush="#7EB3F5"
|
||||||
|
BorderThickness="1"
|
||||||
|
Padding="3,1"
|
||||||
|
LostFocus="NodeNameEditor_LostFocus"
|
||||||
|
KeyDown="NodeNameEditor_KeyDown" />
|
||||||
|
|
||||||
|
<!-- 状态点 -->
|
||||||
|
<Ellipse
|
||||||
|
x:Name="StatusDot"
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
Margin="3,0,0,0"
|
Width="7" Height="7"
|
||||||
|
Margin="0,0,4,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Fill="#BBBBBB" />
|
||||||
|
|
||||||
|
<!-- 状态文字 -->
|
||||||
|
<TextBlock
|
||||||
|
x:Name="StatusText"
|
||||||
|
Grid.Column="3"
|
||||||
|
Margin="0,0,6,0"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
FontFamily="{StaticResource UiFont}"
|
FontFamily="{StaticResource UiFont}"
|
||||||
FontSize="11"
|
FontSize="11"
|
||||||
FontWeight="SemiBold"
|
Foreground="#AAAAAA"
|
||||||
Text="{Binding Name}"
|
Text="禁用" />
|
||||||
TextTrimming="CharacterEllipsis" />
|
|
||||||
|
|
||||||
<StackPanel
|
<!-- ⋮ 菜单按钮(选中时显示) -->
|
||||||
x:Name="NodeActions"
|
<Button
|
||||||
Grid.Row="0"
|
x:Name="NodeMenuBtn"
|
||||||
Grid.Column="3"
|
Grid.Column="4"
|
||||||
Margin="0,0,2,0"
|
Width="22" Height="22"
|
||||||
VerticalAlignment="Center"
|
Padding="0"
|
||||||
Orientation="Horizontal"
|
Background="Transparent"
|
||||||
Visibility="Collapsed">
|
BorderThickness="0"
|
||||||
<Button
|
Content="⋮"
|
||||||
Width="20"
|
Cursor="Hand"
|
||||||
Height="20"
|
FontSize="14"
|
||||||
Margin="1,0"
|
Foreground="#555555"
|
||||||
Background="White"
|
ToolTip="更多操作"
|
||||||
BorderBrush="#E05050"
|
Visibility="Collapsed"
|
||||||
BorderThickness="1"
|
Command="{Binding DataContext.DeleteNodeCommand, RelativeSource={RelativeSource AncestorType=TreeView}}"
|
||||||
Command="{Binding DataContext.DeleteNodeCommand, RelativeSource={RelativeSource AncestorType=TreeView}}"
|
Tag="{Binding}" />
|
||||||
Content="×"
|
|
||||||
Cursor="Hand"
|
|
||||||
FontSize="10"
|
|
||||||
ToolTip="删除" />
|
|
||||||
</StackPanel>
|
|
||||||
<Grid
|
|
||||||
x:Name="WaitDelayProgressHost"
|
|
||||||
Grid.Row="1"
|
|
||||||
Grid.Column="2"
|
|
||||||
Grid.ColumnSpan="2"
|
|
||||||
Margin="3,2,6,1"
|
|
||||||
Visibility="Collapsed">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<ProgressBar
|
|
||||||
Height="6"
|
|
||||||
Minimum="0"
|
|
||||||
Maximum="100"
|
|
||||||
Value="{Binding ExecutionProgressPercent}" />
|
|
||||||
<TextBlock
|
|
||||||
Grid.Column="1"
|
|
||||||
Margin="6,-4,0,0"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
FontFamily="{StaticResource UiFont}"
|
|
||||||
FontSize="10"
|
|
||||||
Foreground="#444444"
|
|
||||||
Text="{Binding ExecutionProgressText}" />
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
<DataTemplate.Triggers>
|
<DataTemplate.Triggers>
|
||||||
<Trigger SourceName="NodeRoot" Property="IsMouseOver" Value="True">
|
<!-- 悬停 -->
|
||||||
<Setter TargetName="NodeActions" Property="Visibility" Value="Visible" />
|
<Trigger SourceName="NodeBg" Property="IsMouseOver" Value="True">
|
||||||
<Setter TargetName="NodeCard" Property="Background" Value="#F6FAFD" />
|
<Setter TargetName="NodeBg" Property="Background" Value="#EAECF5" />
|
||||||
<Setter TargetName="NodeCard" Property="BorderBrush" Value="#DFEAF3" />
|
<Setter TargetName="NodeMenuBtn" Property="Visibility" Value="Visible" />
|
||||||
</Trigger>
|
</Trigger>
|
||||||
|
<!-- 选中 -->
|
||||||
<DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=TreeViewItem}}" Value="True">
|
<DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=TreeViewItem}}" Value="True">
|
||||||
<Setter TargetName="NodeActions" Property="Visibility" Value="Visible" />
|
<Setter TargetName="NodeBg" Property="Background" Value="#DDE6FB" />
|
||||||
<Setter TargetName="NodeCard" Property="Background" Value="#DCEEFF" />
|
<Setter TargetName="NodeNameText" Property="Foreground" Value="#1A1A2E" />
|
||||||
<Setter TargetName="NodeCard" Property="BorderBrush" Value="#71A9DB" />
|
<Setter TargetName="NodeMenuBtn" Property="Visibility" Value="Visible" />
|
||||||
<Setter TargetName="NodeNameText" Property="Foreground" Value="#1F2D3D" />
|
|
||||||
</DataTrigger>
|
</DataTrigger>
|
||||||
|
<!-- 不涉及启用/禁用的节点类型:隐藏状态点和状态文字 -->
|
||||||
|
<DataTrigger Binding="{Binding IsSavePosition}" Value="True">
|
||||||
|
<Setter TargetName="StatusDot" Property="Visibility" Value="Collapsed" />
|
||||||
|
<Setter TargetName="StatusText" Property="Visibility" Value="Collapsed" />
|
||||||
|
</DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding IsReferencePoint}" Value="True">
|
||||||
|
<Setter TargetName="StatusDot" Property="Visibility" Value="Collapsed" />
|
||||||
|
<Setter TargetName="StatusText" Property="Visibility" Value="Collapsed" />
|
||||||
|
</DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding IsPauseDialog}" Value="True">
|
||||||
|
<Setter TargetName="StatusDot" Property="Visibility" Value="Collapsed" />
|
||||||
|
<Setter TargetName="StatusText" Property="Visibility" Value="Collapsed" />
|
||||||
|
</DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding IsWaitDelay}" Value="True">
|
||||||
|
<Setter TargetName="StatusDot" Property="Visibility" Value="Collapsed" />
|
||||||
|
<Setter TargetName="StatusText" Property="Visibility" Value="Collapsed" />
|
||||||
|
</DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding IsCompleteProgram}" Value="True">
|
||||||
|
<Setter TargetName="StatusDot" Property="Visibility" Value="Collapsed" />
|
||||||
|
<Setter TargetName="StatusText" Property="Visibility" Value="Collapsed" />
|
||||||
|
</DataTrigger>
|
||||||
|
<!-- 检测模块:同样不显示启用/禁用状态 -->
|
||||||
|
<DataTrigger Binding="{Binding IsInspectionModule}" Value="True">
|
||||||
|
<Setter TargetName="StatusDot" Property="Visibility" Value="Collapsed" />
|
||||||
|
<Setter TargetName="StatusText" Property="Visibility" Value="Collapsed" />
|
||||||
|
</DataTrigger>
|
||||||
|
<!-- 执行中 -->
|
||||||
<DataTrigger Binding="{Binding ExecutionState}" Value="Running">
|
<DataTrigger Binding="{Binding ExecutionState}" Value="Running">
|
||||||
<Setter TargetName="NodeCard" Property="Background" Value="#FFD54F" />
|
<Setter TargetName="NodeBg" Property="Background" Value="#FFF8E1" />
|
||||||
<Setter TargetName="NodeCard" Property="BorderBrush" Value="#C89B00" />
|
<Setter TargetName="StatusDot" Property="Fill" Value="#FFA000" />
|
||||||
<Setter TargetName="NodeNameText" Property="Foreground" Value="#1F1F1F" />
|
<Setter TargetName="StatusDot" Property="Visibility" Value="Visible" />
|
||||||
|
<Setter TargetName="StatusText" Property="Text" Value="执行中" />
|
||||||
|
<Setter TargetName="StatusText" Property="Foreground" Value="#FFA000" />
|
||||||
|
<Setter TargetName="StatusText" Property="Visibility" Value="Visible" />
|
||||||
</DataTrigger>
|
</DataTrigger>
|
||||||
|
<!-- 执行成功 -->
|
||||||
<DataTrigger Binding="{Binding ExecutionState}" Value="Succeeded">
|
<DataTrigger Binding="{Binding ExecutionState}" Value="Succeeded">
|
||||||
<Setter TargetName="NodeCard" Property="Background" Value="#FF2E7D32" />
|
<Setter TargetName="NodeBg" Property="Background" Value="#E8F5E9" />
|
||||||
<Setter TargetName="NodeCard" Property="BorderBrush" Value="#FF1B5E20" />
|
<Setter TargetName="StatusDot" Property="Fill" Value="#43A047" />
|
||||||
<Setter TargetName="NodeNameText" Property="Foreground" Value="White" />
|
<Setter TargetName="StatusDot" Property="Visibility" Value="Visible" />
|
||||||
|
<Setter TargetName="StatusText" Property="Text" Value="完成" />
|
||||||
|
<Setter TargetName="StatusText" Property="Foreground" Value="#43A047" />
|
||||||
|
<Setter TargetName="StatusText" Property="Visibility" Value="Visible" />
|
||||||
</DataTrigger>
|
</DataTrigger>
|
||||||
|
<!-- 执行失败 -->
|
||||||
<DataTrigger Binding="{Binding ExecutionState}" Value="Failed">
|
<DataTrigger Binding="{Binding ExecutionState}" Value="Failed">
|
||||||
<Setter TargetName="NodeCard" Property="Background" Value="#FFC62828" />
|
<Setter TargetName="NodeBg" Property="Background" Value="#FFEBEE" />
|
||||||
<Setter TargetName="NodeCard" Property="BorderBrush" Value="#FF8E0000" />
|
<Setter TargetName="StatusDot" Property="Fill" Value="#E53935" />
|
||||||
<Setter TargetName="NodeNameText" Property="Foreground" Value="White" />
|
<Setter TargetName="StatusDot" Property="Visibility" Value="Visible" />
|
||||||
|
<Setter TargetName="StatusText" Property="Text" Value="失败" />
|
||||||
|
<Setter TargetName="StatusText" Property="Foreground" Value="#E53935" />
|
||||||
|
<Setter TargetName="StatusText" Property="Visibility" Value="Visible" />
|
||||||
</DataTrigger>
|
</DataTrigger>
|
||||||
<MultiDataTrigger>
|
|
||||||
<MultiDataTrigger.Conditions>
|
|
||||||
<Condition Binding="{Binding IsWaitDelay}" Value="True" />
|
|
||||||
<Condition Binding="{Binding IsRunningNode}" Value="True" />
|
|
||||||
</MultiDataTrigger.Conditions>
|
|
||||||
<Setter TargetName="WaitDelayProgressHost" Property="Visibility" Value="Visible" />
|
|
||||||
</MultiDataTrigger>
|
|
||||||
</DataTemplate.Triggers>
|
</DataTemplate.Triggers>
|
||||||
</HierarchicalDataTemplate>
|
</HierarchicalDataTemplate>
|
||||||
|
|
||||||
|
<!-- ── 3级节点:流水线步骤 CncPipelineStepViewModel ── -->
|
||||||
|
<DataTemplate DataType="{x:Type vm:CncPipelineStepViewModel}">
|
||||||
|
<Grid MinHeight="20" Background="Transparent">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<!-- 左侧竖线区 -->
|
||||||
|
<ColumnDefinition Width="3" />
|
||||||
|
<ColumnDefinition Width="6" />
|
||||||
|
<ColumnDefinition Width="20" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- 左侧竖线 -->
|
||||||
|
<Rectangle
|
||||||
|
Grid.Column="0"
|
||||||
|
Width="2"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Fill="{StaticResource TreeChildLineBrush}"
|
||||||
|
RadiusX="1" RadiusY="1" />
|
||||||
|
|
||||||
|
<!-- 图标 -->
|
||||||
|
<Image
|
||||||
|
Grid.Column="2"
|
||||||
|
Width="16" Height="16"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
|
Source="{Binding IconPath}"
|
||||||
|
Stretch="Uniform" />
|
||||||
|
|
||||||
|
<!-- 名称 -->
|
||||||
|
<TextBlock
|
||||||
|
x:Name="StepNameText"
|
||||||
|
Grid.Column="3"
|
||||||
|
Margin="4,0,8,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontFamily="{StaticResource UiFont}"
|
||||||
|
FontSize="12"
|
||||||
|
Foreground="#333344"
|
||||||
|
Text="{Binding DisplayName}"
|
||||||
|
TextTrimming="CharacterEllipsis" />
|
||||||
|
|
||||||
|
<!-- 状态点 -->
|
||||||
|
<Ellipse
|
||||||
|
x:Name="StepDot"
|
||||||
|
Grid.Column="4"
|
||||||
|
Width="7" Height="7"
|
||||||
|
Margin="0,0,4,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Fill="#43A047" />
|
||||||
|
|
||||||
|
<!-- 状态文字 -->
|
||||||
|
<TextBlock
|
||||||
|
x:Name="StepStateText"
|
||||||
|
Grid.Column="5"
|
||||||
|
Margin="0,0,8,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontFamily="{StaticResource UiFont}"
|
||||||
|
FontSize="11"
|
||||||
|
Foreground="#43A047"
|
||||||
|
Text="启用" />
|
||||||
|
</Grid>
|
||||||
|
<DataTemplate.Triggers>
|
||||||
|
<!-- 已停用 -->
|
||||||
|
<DataTrigger Binding="{Binding IsEnabled}" Value="False">
|
||||||
|
<Setter TargetName="StepNameText" Property="Foreground" Value="#AAAAAA" />
|
||||||
|
<Setter TargetName="StepNameText" Property="TextDecorations" Value="Strikethrough" />
|
||||||
|
<Setter TargetName="StepDot" Property="Fill" Value="#BBBBBB" />
|
||||||
|
<Setter TargetName="StepStateText" Property="Text" Value="停用" />
|
||||||
|
<Setter TargetName="StepStateText" Property="Foreground" Value="#AAAAAA" />
|
||||||
|
</DataTrigger>
|
||||||
|
</DataTemplate.Triggers>
|
||||||
|
</DataTemplate>
|
||||||
|
|
||||||
</TreeView.Resources>
|
</TreeView.Resources>
|
||||||
<TreeView.ItemContainerStyle>
|
<TreeView.ItemContainerStyle>
|
||||||
<Style BasedOn="{StaticResource TreeItemStyle}" TargetType="TreeViewItem">
|
<Style BasedOn="{StaticResource TreeItemStyle}" TargetType="TreeViewItem">
|
||||||
@@ -490,43 +634,43 @@
|
|||||||
<UniformGrid Margin="8,8,8,6" Columns="2">
|
<UniformGrid Margin="8,8,8,6" Columns="2">
|
||||||
<StackPanel Margin="0,0,6,0">
|
<StackPanel Margin="0,0,6,0">
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="载物台 X (μm)" />
|
<TextBlock Style="{StaticResource LabelStyle}" Text="载物台 X (μm)" />
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.StageX, UpdateSourceTrigger=LostFocus}" />
|
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.StageX, UpdateSourceTrigger=LostFocus, StringFormat=F3}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="载物台 Y (μm)" />
|
<TextBlock Style="{StaticResource LabelStyle}" Text="载物台 Y (μm)" />
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.StageY, UpdateSourceTrigger=LostFocus}" />
|
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.StageY, UpdateSourceTrigger=LostFocus, StringFormat=F3}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Margin="0,0,6,0">
|
<StackPanel Margin="0,0,6,0">
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="射线源 Z (μm)" />
|
<TextBlock Style="{StaticResource LabelStyle}" Text="射线源 Z (μm)" />
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.SourceZ, UpdateSourceTrigger=LostFocus}" />
|
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.SourceZ, UpdateSourceTrigger=LostFocus, StringFormat=F3}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="探测器 Z (μm)" />
|
<TextBlock Style="{StaticResource LabelStyle}" Text="探测器 Z (μm)" />
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.DetectorZ, UpdateSourceTrigger=LostFocus}" />
|
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.DetectorZ, UpdateSourceTrigger=LostFocus, StringFormat=F3}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Margin="0,0,6,0">
|
<StackPanel Margin="0,0,6,0">
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="探测器摆动 (°)" />
|
<TextBlock Style="{StaticResource LabelStyle}" Text="探测器摆动 (°)" />
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.DetectorSwing, UpdateSourceTrigger=LostFocus}" />
|
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.DetectorSwing, UpdateSourceTrigger=LostFocus, StringFormat=F3}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="载物台旋转 (°)" />
|
<TextBlock Style="{StaticResource LabelStyle}" Text="载物台旋转 (°)" />
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.StageRotation, UpdateSourceTrigger=LostFocus}" />
|
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.StageRotation, UpdateSourceTrigger=LostFocus, StringFormat=F3}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Margin="0,0,6,0">
|
<StackPanel Margin="0,0,6,0">
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="夹具旋转 (°)" />
|
<TextBlock Style="{StaticResource LabelStyle}" Text="夹具旋转 (°)" />
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.FixtureRotation, UpdateSourceTrigger=LostFocus}" />
|
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.FixtureRotation, UpdateSourceTrigger=LostFocus, StringFormat=F3}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="FOD (μm)" />
|
<TextBlock Style="{StaticResource LabelStyle}" Text="FOD (μm)" />
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.FOD, UpdateSourceTrigger=LostFocus}" />
|
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.FOD, UpdateSourceTrigger=LostFocus, StringFormat=F3}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Margin="0,0,6,0">
|
<StackPanel Margin="0,0,6,0">
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="FDD (μm)" />
|
<TextBlock Style="{StaticResource LabelStyle}" Text="FDD (μm)" />
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.FDD, UpdateSourceTrigger=LostFocus}" />
|
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.FDD, UpdateSourceTrigger=LostFocus, StringFormat=F3}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="放大倍率" />
|
<TextBlock Style="{StaticResource LabelStyle}" Text="放大倍率" />
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Magnification, UpdateSourceTrigger=LostFocus}" />
|
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Magnification, UpdateSourceTrigger=LostFocus, StringFormat=F3}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</UniformGrid>
|
</UniformGrid>
|
||||||
</GroupBox>
|
</GroupBox>
|
||||||
@@ -595,6 +739,7 @@
|
|||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<TextBox
|
<TextBox
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
@@ -607,7 +752,7 @@
|
|||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Height="28"
|
Height="28"
|
||||||
Width="32"
|
Width="32"
|
||||||
Margin="0,0,0,8"
|
Margin="0,0,2,8"
|
||||||
Padding="0"
|
Padding="0"
|
||||||
Content="…"
|
Content="…"
|
||||||
FontSize="13"
|
FontSize="13"
|
||||||
@@ -617,6 +762,21 @@
|
|||||||
BorderBrush="#CFCFCF"
|
BorderBrush="#CFCFCF"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
Cursor="Hand" />
|
Cursor="Hand" />
|
||||||
|
<Button
|
||||||
|
Grid.Column="2"
|
||||||
|
Height="28"
|
||||||
|
Width="28"
|
||||||
|
Margin="0,0,0,8"
|
||||||
|
Padding="0"
|
||||||
|
Content="×"
|
||||||
|
FontSize="14"
|
||||||
|
Foreground="#E05050"
|
||||||
|
ToolTip="清空手动图像路径"
|
||||||
|
Click="ClearManualImagePath_Click"
|
||||||
|
Background="#F8F8F8"
|
||||||
|
BorderBrush="#CFCFCF"
|
||||||
|
BorderThickness="1"
|
||||||
|
Cursor="Hand" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</GroupBox>
|
</GroupBox>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using Prism.Ioc;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Controls.Primitives;
|
using System.Windows.Controls.Primitives;
|
||||||
@@ -85,13 +86,98 @@ namespace XplorePlane.Views.Cnc
|
|||||||
{
|
{
|
||||||
if (DataContext is CncEditorViewModel viewModel)
|
if (DataContext is CncEditorViewModel viewModel)
|
||||||
{
|
{
|
||||||
viewModel.SelectedNode = e.NewValue as CncNodeViewModel;
|
// 三级节点(CncPipelineStepViewModel)点击时,保持当前 2 级节点选中不变
|
||||||
|
if (e.NewValue is CncNodeViewModel nodeVm)
|
||||||
|
viewModel.SelectedNode = nodeVm;
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateNodeVisualState();
|
|
||||||
UpdatePropertyEditorState();
|
UpdatePropertyEditorState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 节点名称内联编辑 ──────────────────────────────────────────
|
||||||
|
|
||||||
|
private void NodeNameText_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.ClickCount != 2) return;
|
||||||
|
if (sender is not TextBlock tb) return;
|
||||||
|
|
||||||
|
// 只允许 SavePosition 和 InspectionModule 节点重命名
|
||||||
|
var nodeVm = (tb.DataContext as CncNodeViewModel)
|
||||||
|
?? FindAncestor<TreeViewItem>(tb)?.DataContext as CncNodeViewModel;
|
||||||
|
if (nodeVm == null || (!nodeVm.IsSavePosition && !nodeVm.IsInspectionModule)) return;
|
||||||
|
|
||||||
|
// 找到同级的 TextBox 编辑器
|
||||||
|
var parent = VisualTreeHelper.GetParent(tb) as Grid;
|
||||||
|
if (parent == null) return;
|
||||||
|
|
||||||
|
var editor = FindVisualDescendants<TextBox>(parent).FirstOrDefault(x => x.Name == "NodeNameEditor");
|
||||||
|
if (editor == null) return;
|
||||||
|
|
||||||
|
// 切换到编辑模式
|
||||||
|
tb.Visibility = Visibility.Collapsed;
|
||||||
|
editor.Visibility = Visibility.Visible;
|
||||||
|
editor.Text = nodeVm.Name;
|
||||||
|
editor.SelectAll();
|
||||||
|
editor.Focus();
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NodeNameEditor_LostFocus(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
CommitNodeRename(sender as TextBox);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NodeNameEditor_KeyDown(object sender, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Key == Key.Return || e.Key == Key.Enter)
|
||||||
|
{
|
||||||
|
CommitNodeRename(sender as TextBox);
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
else if (e.Key == Key.Escape)
|
||||||
|
{
|
||||||
|
CancelNodeRename(sender as TextBox);
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CommitNodeRename(TextBox editor)
|
||||||
|
{
|
||||||
|
if (editor == null) return;
|
||||||
|
var nodeVm = FindAncestor<TreeViewItem>(editor)?.DataContext as CncNodeViewModel;
|
||||||
|
if (nodeVm == null) return;
|
||||||
|
|
||||||
|
var newName = editor.Text?.Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(newName) && newName != nodeVm.Name)
|
||||||
|
nodeVm.Name = newName;
|
||||||
|
|
||||||
|
ExitRenameMode(editor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelNodeRename(TextBox editor)
|
||||||
|
{
|
||||||
|
if (editor == null) return;
|
||||||
|
ExitRenameMode(editor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ExitRenameMode(TextBox editor)
|
||||||
|
{
|
||||||
|
var parent = VisualTreeHelper.GetParent(editor) as Grid;
|
||||||
|
if (parent == null) return;
|
||||||
|
|
||||||
|
var tb = FindVisualDescendants<TextBlock>(parent).FirstOrDefault(x => x.Name == "NodeNameText");
|
||||||
|
if (tb != null) tb.Visibility = Visibility.Visible;
|
||||||
|
editor.Visibility = Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>清空手动图像路径</summary>
|
||||||
|
private void ClearManualImagePath_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is CncEditorViewModel viewModel && viewModel.SelectedNode != null)
|
||||||
|
{
|
||||||
|
viewModel.SelectedNode.ManualImagePath = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void CncTreeView_PreviewKeyDown(object sender, KeyEventArgs e)
|
private void CncTreeView_PreviewKeyDown(object sender, KeyEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Key != Key.Delete || DataContext is not CncEditorViewModel viewModel)
|
if (e.Key != Key.Delete || DataContext is not CncEditorViewModel viewModel)
|
||||||
@@ -107,47 +193,91 @@ namespace XplorePlane.Views.Cnc
|
|||||||
private void CncTreeView_ContextMenuOpening(object sender, ContextMenuEventArgs e)
|
private void CncTreeView_ContextMenuOpening(object sender, ContextMenuEventArgs e)
|
||||||
{
|
{
|
||||||
if (DataContext is not CncEditorViewModel viewModel)
|
if (DataContext is not CncEditorViewModel viewModel)
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
var position = Mouse.GetPosition(CncTreeView);
|
var position = Mouse.GetPosition(CncTreeView);
|
||||||
var hit = VisualTreeHelper.HitTest(CncTreeView, position);
|
var hit = VisualTreeHelper.HitTest(CncTreeView, position);
|
||||||
var treeViewItem = FindAncestor<TreeViewItem>(hit?.VisualHit);
|
var treeViewItem = FindAncestor<TreeViewItem>(hit?.VisualHit);
|
||||||
if (treeViewItem?.DataContext is not CncNodeViewModel nodeVm)
|
|
||||||
|
// 右键点在节点上
|
||||||
|
if (treeViewItem?.DataContext is CncNodeViewModel nodeVm)
|
||||||
{
|
{
|
||||||
CncTreeView.ContextMenu = null;
|
viewModel.SelectedNode = nodeVm;
|
||||||
return;
|
UpdatePropertyEditorState();
|
||||||
|
|
||||||
|
CncTreeView.ContextMenu = new ContextMenu
|
||||||
|
{
|
||||||
|
Items =
|
||||||
|
{
|
||||||
|
new MenuItem
|
||||||
|
{
|
||||||
|
Header = "在上方插入位置",
|
||||||
|
Command = viewModel.PrepareInsertAboveCommand,
|
||||||
|
CommandParameter = nodeVm
|
||||||
|
},
|
||||||
|
new MenuItem
|
||||||
|
{
|
||||||
|
Header = "在下方插入位置",
|
||||||
|
Command = viewModel.PrepareInsertBelowCommand,
|
||||||
|
CommandParameter = nodeVm
|
||||||
|
},
|
||||||
|
new Separator(),
|
||||||
|
new MenuItem
|
||||||
|
{
|
||||||
|
Header = "全部展开",
|
||||||
|
Tag = "ExpandAll"
|
||||||
|
},
|
||||||
|
new MenuItem
|
||||||
|
{
|
||||||
|
Header = "全部折叠",
|
||||||
|
Tag = "CollapseAll"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 右键点在空白处:只显示展开/折叠
|
||||||
|
CncTreeView.ContextMenu = new ContextMenu
|
||||||
|
{
|
||||||
|
Items =
|
||||||
|
{
|
||||||
|
new MenuItem { Header = "全部展开", Tag = "ExpandAll" },
|
||||||
|
new MenuItem { Header = "全部折叠", Tag = "CollapseAll" }
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.SelectedNode = nodeVm;
|
// 绑定展开/折叠事件
|
||||||
UpdateNodeVisualState();
|
foreach (var item in CncTreeView.ContextMenu.Items)
|
||||||
UpdatePropertyEditorState();
|
|
||||||
|
|
||||||
CncTreeView.ContextMenu = new ContextMenu
|
|
||||||
{
|
{
|
||||||
Items =
|
if (item is MenuItem mi && mi.Tag is string tag)
|
||||||
{
|
{
|
||||||
new MenuItem
|
mi.Click += (_, _) =>
|
||||||
{
|
{
|
||||||
Header = "\u5728\u4E0A\u65B9\u63D2\u5165\u4F4D\u7F6E",
|
bool expand = tag == "ExpandAll";
|
||||||
Command = viewModel.PrepareInsertAboveCommand,
|
SetAllExpanded(CncTreeView.Items, expand);
|
||||||
CommandParameter = nodeVm
|
};
|
||||||
},
|
|
||||||
new MenuItem
|
|
||||||
{
|
|
||||||
Header = "\u5728\u4E0B\u65B9\u63D2\u5165\u4F4D\u7F6E",
|
|
||||||
Command = viewModel.PrepareInsertBelowCommand,
|
|
||||||
CommandParameter = nodeVm
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>递归设置所有 CncNodeViewModel 的展开状态</summary>
|
||||||
|
private static void SetAllExpanded(System.Collections.IEnumerable items, bool expand)
|
||||||
|
{
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
if (item is CncNodeViewModel nodeVm)
|
||||||
|
{
|
||||||
|
nodeVm.IsExpanded = expand;
|
||||||
|
SetAllExpanded(nodeVm.Children, expand);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CncTreeView_LayoutUpdated(object sender, EventArgs e)
|
private void CncTreeView_LayoutUpdated(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
HideInlineDeleteButtons();
|
HideInlineDeleteButtons();
|
||||||
UpdateNodeVisualState();
|
|
||||||
UpdatePropertyEditorState();
|
UpdatePropertyEditorState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,23 +295,7 @@ namespace XplorePlane.Views.Cnc
|
|||||||
|
|
||||||
private void UpdateNodeVisualState()
|
private void UpdateNodeVisualState()
|
||||||
{
|
{
|
||||||
foreach (var item in FindVisualDescendants<TreeViewItem>(CncTreeView))
|
// 新样式通过 DataTemplate Triggers 控制高亮,无需 code-behind 干预
|
||||||
{
|
|
||||||
if (item.DataContext is not CncNodeViewModel)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var card = FindNodeCard(item);
|
|
||||||
if (card == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
card.ClearValue(Border.BackgroundProperty);
|
|
||||||
card.ClearValue(Border.BorderBrushProperty);
|
|
||||||
ClearNodeTextForeground(card);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdatePropertyEditorState()
|
private void UpdatePropertyEditorState()
|
||||||
@@ -313,14 +427,7 @@ namespace XplorePlane.Views.Cnc
|
|||||||
|
|
||||||
private static Border FindNodeCard(DependencyObject root)
|
private static Border FindNodeCard(DependencyObject root)
|
||||||
{
|
{
|
||||||
foreach (var border in FindVisualDescendants<Border>(root))
|
// 新样式不再使用 NodeCard,保留方法避免编译错误
|
||||||
{
|
|
||||||
if (border.DataContext is CncNodeViewModel && border.CornerRadius.TopLeft == 4 && border.BorderThickness.Left == 1)
|
|
||||||
{
|
|
||||||
return border;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,9 +61,9 @@
|
|||||||
<Grid>
|
<Grid>
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="4*" MinHeight="180" />
|
<RowDefinition Height="6*" MinHeight="200" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="2*" MinHeight="80" />
|
<RowDefinition Height="5*" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<Border
|
<Border
|
||||||
|
|||||||
@@ -597,7 +597,7 @@
|
|||||||
</telerik:RadRibbonGroup>
|
</telerik:RadRibbonGroup>
|
||||||
</telerik:RadRibbonTab>
|
</telerik:RadRibbonTab>
|
||||||
<telerik:RadRibbonTab Header="帮助">
|
<telerik:RadRibbonTab Header="帮助">
|
||||||
<telerik:RadRibbonGroup Header="关于">
|
<telerik:RadRibbonGroup Header="帮助">
|
||||||
<telerik:RadRibbonGroup.Variants>
|
<telerik:RadRibbonGroup.Variants>
|
||||||
<telerik:GroupVariant Priority="0" Variant="Large" />
|
<telerik:GroupVariant Priority="0" Variant="Large" />
|
||||||
</telerik:RadRibbonGroup.Variants>
|
</telerik:RadRibbonGroup.Variants>
|
||||||
|
|||||||
Reference in New Issue
Block a user