Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| faf58b2768 | |||
| 3d6901f877 | |||
| 188bac53f1 | |||
| 12882bd1c6 | |||
| 8189e76492 | |||
| 80e71054f6 | |||
| 95b9a6a2ae | |||
| 9218384e3f | |||
| e166eca3d7 |
@@ -1,110 +1,110 @@
|
|||||||
## XplorePlane 平面CT软件
|
## XplorePlane 平面CT软件
|
||||||
|
|
||||||
### 系统目标
|
### 系统目标
|
||||||
|
|
||||||
XplorePlane 系统用于控制平面 CT 设备的各个子系统(射线源、探测器、运动控制、相机)并完成采集图像的处理与分析,为研发与调试提供统一的软件平台。
|
XplorePlane 系统用于控制平面 CT 设备的各个子系统(射线源、探测器、运动控制、相机)并完成采集图像的处理与分析,为研发与调试提供统一的软件平台。
|
||||||
|
|
||||||
### 总体架构
|
### 总体架构
|
||||||
|
|
||||||
- 客户端框架: WPF + Prism MVVM(目标框架 net8.0-windows)
|
- 客户端框架: WPF + Prism MVVM(目标框架 net8.0-windows)
|
||||||
- 图像处理内核: ImageProcessing.Core(算子基类)+ ImageProcessing.Processors(具体算子),基于 EmguCV
|
- 图像处理内核: ImageProcessing.Core(算子基类)+ ImageProcessing.Processors(具体算子),基于 EmguCV
|
||||||
- 相机控制: XP.Camera(Basler pylon SDK 封装,支持软件触发、参数读写)
|
- 相机控制: XP.Camera(Basler pylon SDK 封装,支持软件触发、参数读写)
|
||||||
- 硬件抽象: XP.Common + XP.Hardware.RaySource(射线源控制)
|
- 硬件抽象: XP.Common + XP.Hardware.RaySource(射线源控制)
|
||||||
- 日志: Serilog
|
- 日志: Serilog
|
||||||
- UI 组件: Telerik RadRibbonView、Fluent.Ribbon
|
- UI 组件: Telerik RadRibbonView、Fluent.Ribbon
|
||||||
|
|
||||||
### 解决方案结构
|
### 解决方案结构
|
||||||
|
|
||||||
```
|
```
|
||||||
XplorePlane.sln
|
XplorePlane.sln
|
||||||
├── XplorePlane/ # 主应用程序(WPF)
|
├── XplorePlane/ # 主应用程序(WPF)
|
||||||
├── XP.Camera/ # 相机控制库(Basler)
|
├── XP.Camera/ # 相机控制库(Basler)
|
||||||
├── ImageProcessing/ # 独立图像处理应用
|
├── ImageProcessing/ # 独立图像处理应用
|
||||||
├── ImageProcessing.Core/ # 图像处理算子基类
|
├── ImageProcessing.Core/ # 图像处理算子基类
|
||||||
├── ImageProcessing.Processors/ # 具体算子实现
|
├── ImageProcessing.Processors/ # 具体算子实现
|
||||||
├── ImageProcessing.Controls/ # 图像显示控件(ImageCanvasControl)
|
├── ImageProcessing.Controls/ # 图像显示控件(ImageCanvasControl)
|
||||||
├── ImageROIControl/ # ROI 绘制控件
|
├── ImageROIControl/ # ROI 绘制控件
|
||||||
├── XplorePlane.Tests/ # 单元测试
|
├── XplorePlane.Tests/ # 单元测试
|
||||||
└── ExternalLibraries/ # 外部 DLL 和 ONNX 模型
|
└── ExternalLibraries/ # 外部 DLL 和 ONNX 模型
|
||||||
```
|
```
|
||||||
|
|
||||||
### XplorePlane 主项目结构
|
### XplorePlane 主项目结构
|
||||||
|
|
||||||
```
|
```
|
||||||
XplorePlane/
|
XplorePlane/
|
||||||
├── App.xaml / App.xaml.cs # 应用入口 + DI 容器配置(AppBootstrapper)
|
├── App.xaml / App.xaml.cs # 应用入口 + DI 容器配置(AppBootstrapper)
|
||||||
├── Views/
|
├── Views/
|
||||||
│ ├── Main/
|
│ ├── Main/
|
||||||
│ │ ├── MainWindow.xaml # 主窗口(Telerik Ribbon + 三栏布局)
|
│ │ ├── MainWindow.xaml # 主窗口(Telerik Ribbon + 三栏布局)
|
||||||
│ │ ├── NavigationPropertyPanelView.xaml # 相机实时预览面板
|
│ │ ├── NavigationPropertyPanelView.xaml # 相机实时预览面板
|
||||||
│ │ └── MotionControlPanelView.xaml # 运动控制面板
|
│ │ └── MotionControlPanelView.xaml # 运动控制面板
|
||||||
│ ├── Cnc/ # CNC 编辑器 / 矩阵编排视图
|
│ ├── Cnc/ # CNC 编辑器 / 矩阵编排视图
|
||||||
│ ├── ImageProcessing/ # 图像处理面板视图
|
│ ├── ImageProcessing/ # 图像处理面板视图
|
||||||
│ └── CameraSettingsWindow.xaml # 相机参数设置对话框
|
│ └── CameraSettingsWindow.xaml # 相机参数设置对话框
|
||||||
├── ViewModels/
|
├── ViewModels/
|
||||||
│ ├── Main/
|
│ ├── Main/
|
||||||
│ │ ├── MainViewModel.cs # 主窗口 ViewModel
|
│ │ ├── MainViewModel.cs # 主窗口 ViewModel
|
||||||
│ │ └── NavigationPropertyPanelViewModel.cs # 相机预览 ViewModel
|
│ │ └── NavigationPropertyPanelViewModel.cs # 相机预览 ViewModel
|
||||||
│ ├── Cnc/ # CNC / 矩阵 ViewModel
|
│ ├── Cnc/ # CNC / 矩阵 ViewModel
|
||||||
│ └── ImageProcessing/ # 图像处理 / 流水线 ViewModel
|
│ └── ImageProcessing/ # 图像处理 / 流水线 ViewModel
|
||||||
├── Services/
|
├── Services/
|
||||||
│ ├── AppState/ # 全局状态管理(线程安全)
|
│ ├── AppState/ # 全局状态管理(线程安全)
|
||||||
│ ├── Camera/ # 相机服务
|
│ ├── Camera/ # 相机服务
|
||||||
│ ├── Cnc/ # CNC 程序服务
|
│ ├── Cnc/ # CNC 程序服务
|
||||||
│ ├── Matrix/ # 矩阵编排服务
|
│ ├── Matrix/ # 矩阵编排服务
|
||||||
│ ├── Measurement/ # 测量数据服务
|
│ ├── Measurement/ # 测量数据服务
|
||||||
│ ├── Pipeline/ # 流水线执行 / 持久化
|
│ ├── Pipeline/ # 流水线执行 / 持久化
|
||||||
│ └── Recipe/ # 检测配方服务
|
│ └── Recipe/ # 检测配方服务
|
||||||
├── Models/ # 数据模型(State、CNC、Matrix、Pipeline 等)
|
├── Models/ # 数据模型(State、CNC、Matrix、Pipeline 等)
|
||||||
├── Events/ # Prism 事件
|
├── Events/ # Prism 事件
|
||||||
├── Libs/
|
├── Libs/
|
||||||
│ ├── Hardware/ # 硬件库 DLL(XP.Common、XP.Hardware.RaySource)
|
│ ├── Hardware/ # 硬件库 DLL(XP.Common、XP.Hardware.RaySource)
|
||||||
│ └── Native/ # 原生依赖库
|
│ └── Native/ # 原生依赖库
|
||||||
└── Assets/Icons/ # 工具栏图标
|
└── Assets/Icons/ # 工具栏图标
|
||||||
```
|
```
|
||||||
|
|
||||||
### 相机集成
|
### 相机集成
|
||||||
|
|
||||||
相机实时影像集成在主窗口左下角的 NavigationPropertyPanelView 中:
|
相机实时影像集成在主窗口左下角的 NavigationPropertyPanelView 中:
|
||||||
|
|
||||||
- 连接/断开相机(Basler,通过 ICameraController)
|
- 连接/断开相机(Basler,通过 ICameraController)
|
||||||
- 开始/停止采集(软件触发模式)
|
- 开始/停止采集(软件触发模式)
|
||||||
- 实时预览(Live View,勾选"实时"复选框)
|
- 实时预览(Live View,勾选"实时"复选框)
|
||||||
- 鼠标悬停显示像素坐标
|
- 鼠标悬停显示像素坐标
|
||||||
- 相机参数设置对话框(曝光时间、增益、分辨率、像素格式)
|
- 相机参数设置对话框(曝光时间、增益、分辨率、像素格式)
|
||||||
- 主界面 Ribbon 硬件栏提供"相机设置"快捷按钮
|
- 主界面 Ribbon 硬件栏提供"相机设置"快捷按钮
|
||||||
|
|
||||||
相机控制逻辑移植自 ImageProcessing 项目,使用 XP.Camera.PixelConverter 进行像素数据转换,通过 Application.Dispatcher.Invoke 保证 UI 线程安全。
|
相机控制逻辑移植自 ImageProcessing 项目,使用 XP.Camera.PixelConverter 进行像素数据转换,通过 Application.Dispatcher.Invoke 保证 UI 线程安全。
|
||||||
|
|
||||||
### 依赖注入(DI)
|
### 依赖注入(DI)
|
||||||
|
|
||||||
使用 Prism + DryIoc,在 AppBootstrapper.RegisterTypes() 中统一注册:
|
使用 Prism + DryIoc,在 AppBootstrapper.RegisterTypes() 中统一注册:
|
||||||
|
|
||||||
- ICameraFactory / ICameraController / ICameraService(单例)
|
- ICameraFactory / ICameraController / ICameraService(单例)
|
||||||
- IRaySourceService / IRaySourceFactory(单例)
|
- IRaySourceService / IRaySourceFactory(单例)
|
||||||
- IAppStateService(单例,线程安全状态管理)
|
- IAppStateService(单例,线程安全状态管理)
|
||||||
- NavigationPropertyPanelViewModel(单例,相机预览共享实例)
|
- NavigationPropertyPanelViewModel(单例,相机预览共享实例)
|
||||||
- 各 Service 和 ViewModel(按需注册)
|
- 各 Service 和 ViewModel(按需注册)
|
||||||
|
|
||||||
### 构建
|
### 构建
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Debug
|
# Debug
|
||||||
dotnet build XplorePlane.sln -c Debug
|
dotnet build XplorePlane.sln -c Debug
|
||||||
|
|
||||||
# Release
|
# Release
|
||||||
dotnet build XplorePlane.sln -c Release
|
dotnet build XplorePlane.sln -c Release
|
||||||
```
|
```
|
||||||
|
|
||||||
### TO-DO List
|
### TO-DO List
|
||||||
|
|
||||||
- [x] 软件基于 WPF + Prism 基础的框架
|
- [x] 软件基于 WPF + Prism 基础的框架
|
||||||
- [x] 日志库的引用(通过 XP.Common.dll)
|
- [x] 日志库的引用(通过 XP.Common.dll)
|
||||||
- [x] 按推荐的 DLL 目录结构进行修改
|
- [x] 按推荐的 DLL 目录结构进行修改
|
||||||
- [x] 通过库依赖的方式调用日志功能
|
- [x] 通过库依赖的方式调用日志功能
|
||||||
- [x] 界面的布局
|
- [x] 界面的布局
|
||||||
- [x] 相机实时影像集成(连接、采集、Live View、像素坐标显示)
|
- [x] 相机实时影像集成(连接、采集、Live View、像素坐标显示)
|
||||||
- [x] 相机参数设置对话框(曝光、增益、分辨率、像素格式)
|
- [x] 相机参数设置对话框(曝光、增益、分辨率、像素格式)
|
||||||
- [x] 主界面硬件栏相机设置按钮
|
- [x] 主界面硬件栏相机设置按钮
|
||||||
- [ ] 打通与硬件层的调用流程
|
- [ ] 打通与硬件层的调用流程
|
||||||
- [ ] 打通与图像层的调用流程
|
- [ ] 打通与图像层的调用流程
|
||||||
|
|||||||
@@ -195,7 +195,6 @@ public class BaslerCameraController : ICameraController
|
|||||||
}
|
}
|
||||||
|
|
||||||
_camera.ExecuteSoftwareTrigger();
|
_camera.ExecuteSoftwareTrigger();
|
||||||
_logger.Debug("Software trigger executed.");
|
|
||||||
}
|
}
|
||||||
catch (TimeoutException)
|
catch (TimeoutException)
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using XP.Camera.Calibration.Resources;
|
||||||
|
|
||||||
|
namespace XP.Camera.Calibration;
|
||||||
|
|
||||||
|
public class CalibrationLocalizedStrings
|
||||||
|
{
|
||||||
|
public CalibrationResources Resources { get; } = new CalibrationResources();
|
||||||
|
}
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
// ============================================================================
|
||||||
|
// 文件名: CalibrationProcessor.cs
|
||||||
|
// 描述: 标定处理器,实现图像坐标系到世界坐标系的转换
|
||||||
|
// 功能:
|
||||||
|
// - 基于多点标定计算透视变换矩阵(支持4点及以上)
|
||||||
|
// - 像素坐标到世界坐标的转换
|
||||||
|
// - 标定数据的保存和加载(JSON格式)
|
||||||
|
// - 从CSV文件导入标定点数据
|
||||||
|
// 算法: 使用DLT(Direct Linear Transformation)方法求解单应性矩阵
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
using Emgu.CV;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace XP.Camera.Calibration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 九点标定处理器
|
||||||
|
/// </summary>
|
||||||
|
public class CalibrationProcessor
|
||||||
|
{
|
||||||
|
public class CalibrationPoint
|
||||||
|
{
|
||||||
|
public double PixelX { get; set; }
|
||||||
|
public double PixelY { get; set; }
|
||||||
|
public double WorldX { get; set; }
|
||||||
|
public double WorldY { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CalibrationData
|
||||||
|
{
|
||||||
|
public List<CalibrationPoint> Points { get; set; } = new List<CalibrationPoint>();
|
||||||
|
public double[] TransformMatrix { get; set; } = new double[9];
|
||||||
|
public DateTime CalibrationTime { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private Matrix<double>? _transformMatrix;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行九点标定
|
||||||
|
/// </summary>
|
||||||
|
public bool Calibrate(List<CalibrationPoint> points)
|
||||||
|
{
|
||||||
|
if (points.Count < 4)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
int n = points.Count;
|
||||||
|
var A = new Matrix<double>(2 * n, 8);
|
||||||
|
var b = new Matrix<double>(2 * n, 1);
|
||||||
|
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
{
|
||||||
|
double u = points[i].PixelX;
|
||||||
|
double v = points[i].PixelY;
|
||||||
|
double x = points[i].WorldX;
|
||||||
|
double y = points[i].WorldY;
|
||||||
|
|
||||||
|
A[2 * i, 0] = u;
|
||||||
|
A[2 * i, 1] = v;
|
||||||
|
A[2 * i, 2] = 1;
|
||||||
|
A[2 * i, 3] = 0;
|
||||||
|
A[2 * i, 4] = 0;
|
||||||
|
A[2 * i, 5] = 0;
|
||||||
|
A[2 * i, 6] = -x * u;
|
||||||
|
A[2 * i, 7] = -x * v;
|
||||||
|
b[2 * i, 0] = x;
|
||||||
|
|
||||||
|
A[2 * i + 1, 0] = 0;
|
||||||
|
A[2 * i + 1, 1] = 0;
|
||||||
|
A[2 * i + 1, 2] = 0;
|
||||||
|
A[2 * i + 1, 3] = u;
|
||||||
|
A[2 * i + 1, 4] = v;
|
||||||
|
A[2 * i + 1, 5] = 1;
|
||||||
|
A[2 * i + 1, 6] = -y * u;
|
||||||
|
A[2 * i + 1, 7] = -y * v;
|
||||||
|
b[2 * i + 1, 0] = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
var h = new Matrix<double>(8, 1);
|
||||||
|
CvInvoke.Solve(A, b, h, Emgu.CV.CvEnum.DecompMethod.Svd);
|
||||||
|
|
||||||
|
_transformMatrix = new Matrix<double>(3, 3);
|
||||||
|
_transformMatrix[0, 0] = h[0, 0];
|
||||||
|
_transformMatrix[0, 1] = h[1, 0];
|
||||||
|
_transformMatrix[0, 2] = h[2, 0];
|
||||||
|
_transformMatrix[1, 0] = h[3, 0];
|
||||||
|
_transformMatrix[1, 1] = h[4, 0];
|
||||||
|
_transformMatrix[1, 2] = h[5, 0];
|
||||||
|
_transformMatrix[2, 0] = h[6, 0];
|
||||||
|
_transformMatrix[2, 1] = h[7, 0];
|
||||||
|
_transformMatrix[2, 2] = 1.0;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 像素坐标转世界坐标
|
||||||
|
/// </summary>
|
||||||
|
public System.Drawing.PointF PixelToWorld(System.Drawing.PointF pixel)
|
||||||
|
{
|
||||||
|
if (_transformMatrix == null)
|
||||||
|
return pixel;
|
||||||
|
|
||||||
|
double u = pixel.X;
|
||||||
|
double v = pixel.Y;
|
||||||
|
|
||||||
|
double w = _transformMatrix[2, 0] * u + _transformMatrix[2, 1] * v + _transformMatrix[2, 2];
|
||||||
|
double x = (_transformMatrix[0, 0] * u + _transformMatrix[0, 1] * v + _transformMatrix[0, 2]) / w;
|
||||||
|
double y = (_transformMatrix[1, 0] * u + _transformMatrix[1, 1] * v + _transformMatrix[1, 2]) / w;
|
||||||
|
|
||||||
|
return new System.Drawing.PointF((float)x, (float)y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存标定文件
|
||||||
|
/// </summary>
|
||||||
|
public void SaveCalibration(string filePath, List<CalibrationPoint> points)
|
||||||
|
{
|
||||||
|
var data = new CalibrationData
|
||||||
|
{
|
||||||
|
Points = points,
|
||||||
|
TransformMatrix = new double[9],
|
||||||
|
CalibrationTime = DateTime.Now
|
||||||
|
};
|
||||||
|
|
||||||
|
if (_transformMatrix != null)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
for (int j = 0; j < 3; j++)
|
||||||
|
data.TransformMatrix[i * 3 + j] = _transformMatrix[i, j];
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true });
|
||||||
|
File.WriteAllText(filePath, json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 加载标定文件
|
||||||
|
/// </summary>
|
||||||
|
public bool LoadCalibration(string filePath)
|
||||||
|
{
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(filePath);
|
||||||
|
var data = JsonSerializer.Deserialize<CalibrationData>(json);
|
||||||
|
|
||||||
|
if (data == null || data.TransformMatrix == null || data.TransformMatrix.Length != 9)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
_transformMatrix = new Matrix<double>(3, 3);
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
for (int j = 0; j < 3; j++)
|
||||||
|
_transformMatrix[i, j] = data.TransformMatrix[i * 3 + j];
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从CSV文件加载标定点
|
||||||
|
/// </summary>
|
||||||
|
public List<CalibrationPoint> LoadPointsFromCsv(string filePath)
|
||||||
|
{
|
||||||
|
var points = new List<CalibrationPoint>();
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
return points;
|
||||||
|
|
||||||
|
var lines = File.ReadAllLines(filePath);
|
||||||
|
for (int i = 0; i < lines.Length; i++)
|
||||||
|
{
|
||||||
|
if (i == 0 && (lines[i].Contains("PixelX") || lines[i].Contains("像素"))) continue;
|
||||||
|
|
||||||
|
var parts = lines[i].Split(',');
|
||||||
|
if (parts.Length >= 4)
|
||||||
|
{
|
||||||
|
if (double.TryParse(parts[0].Trim(), out double px) &&
|
||||||
|
double.TryParse(parts[1].Trim(), out double py) &&
|
||||||
|
double.TryParse(parts[2].Trim(), out double wx) &&
|
||||||
|
double.TryParse(parts[3].Trim(), out double wy))
|
||||||
|
{
|
||||||
|
points.Add(new CalibrationPoint
|
||||||
|
{
|
||||||
|
PixelX = px,
|
||||||
|
PixelY = py,
|
||||||
|
WorldX = wx,
|
||||||
|
WorldY = wy
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
// ============================================================================
|
||||||
|
// 文件名: ChessboardCalibrator.cs
|
||||||
|
// 描述: 棋盘格标定器,实现基于棋盘格的相机内参标定
|
||||||
|
// 功能:
|
||||||
|
// - 从多张棋盘格图像中检测角点
|
||||||
|
// - 计算相机内参矩阵和畸变系数
|
||||||
|
// - 图像去畸变处理
|
||||||
|
// - 计算重投影误差评估标定质量
|
||||||
|
// - 标定结果的保存和加载(JSON格式)
|
||||||
|
// 算法: 使用 Zhang's 标定方法进行相机标定
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
using Emgu.CV;
|
||||||
|
using Emgu.CV.CvEnum;
|
||||||
|
using Emgu.CV.Structure;
|
||||||
|
using Emgu.CV.Util;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace XP.Camera.Calibration;
|
||||||
|
|
||||||
|
public class ChessboardCalibrator
|
||||||
|
{
|
||||||
|
public class CalibrationResult
|
||||||
|
{
|
||||||
|
public double[][] CameraMatrix { get; set; } = new double[3][];
|
||||||
|
public double[] DistortionCoeffs { get; set; } = Array.Empty<double>();
|
||||||
|
public double ReprojectionError { get; set; }
|
||||||
|
public DateTime CalibrationTime { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mat? _cameraMatrix;
|
||||||
|
private Mat? _distCoeffs;
|
||||||
|
private double _reprojectionError;
|
||||||
|
private List<double> _perImageErrors = new List<double>();
|
||||||
|
|
||||||
|
public double ReprojectionError => _reprojectionError;
|
||||||
|
public List<double> PerImageErrors => _perImageErrors;
|
||||||
|
|
||||||
|
// 进度报告委托
|
||||||
|
public delegate void ProgressReportHandler(int current, int total, string message);
|
||||||
|
|
||||||
|
public event ProgressReportHandler? ProgressChanged;
|
||||||
|
|
||||||
|
public bool CalibrateFromImages(List<string> imagePaths, int boardWidth, int boardHeight, float squareSize, out string errorMsg)
|
||||||
|
{
|
||||||
|
errorMsg = "";
|
||||||
|
var objectPoints = new VectorOfVectorOfPoint3D32F();
|
||||||
|
var imagePoints = new VectorOfVectorOfPointF();
|
||||||
|
var imageSize = new System.Drawing.Size();
|
||||||
|
|
||||||
|
var objp = new MCvPoint3D32f[boardWidth * boardHeight];
|
||||||
|
for (int i = 0; i < boardHeight; i++)
|
||||||
|
for (int j = 0; j < boardWidth; j++)
|
||||||
|
objp[i * boardWidth + j] = new MCvPoint3D32f(j * squareSize, i * squareSize, 0);
|
||||||
|
|
||||||
|
int validImages = 0;
|
||||||
|
int totalImages = imagePaths.Count;
|
||||||
|
|
||||||
|
// 第一阶段:检测角点
|
||||||
|
for (int idx = 0; idx < totalImages; idx++)
|
||||||
|
{
|
||||||
|
var path = imagePaths[idx];
|
||||||
|
ProgressChanged?.Invoke(idx + 1, totalImages * 2, $"检测角点 ({idx + 1}/{totalImages})");
|
||||||
|
|
||||||
|
var img = CvInvoke.Imread(path, ImreadModes.Grayscale);
|
||||||
|
if (img.IsEmpty) continue;
|
||||||
|
|
||||||
|
imageSize = img.Size;
|
||||||
|
var corners = new VectorOfPointF();
|
||||||
|
|
||||||
|
if (CvInvoke.FindChessboardCorners(img, new System.Drawing.Size(boardWidth, boardHeight), corners))
|
||||||
|
{
|
||||||
|
// 亚像素级角点精化
|
||||||
|
CvInvoke.CornerSubPix(img, corners, new System.Drawing.Size(5, 5), new System.Drawing.Size(-1, -1),
|
||||||
|
new MCvTermCriteria(30, 0.001));
|
||||||
|
|
||||||
|
objectPoints.Push(new VectorOfPoint3D32F(objp));
|
||||||
|
imagePoints.Push(corners);
|
||||||
|
validImages++;
|
||||||
|
}
|
||||||
|
img.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validImages < 3)
|
||||||
|
{
|
||||||
|
errorMsg = $"有效图像不足,需要至少3张,当前{validImages}张";
|
||||||
|
ProgressChanged?.Invoke(0, 100, "标定失败");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第二阶段:执行标定
|
||||||
|
ProgressChanged?.Invoke(totalImages, totalImages * 2, "执行相机标定...");
|
||||||
|
|
||||||
|
_cameraMatrix = new Mat();
|
||||||
|
_distCoeffs = new Mat();
|
||||||
|
var rvecs = new VectorOfMat();
|
||||||
|
var tvecs = new VectorOfMat();
|
||||||
|
|
||||||
|
_reprojectionError = CvInvoke.CalibrateCamera(objectPoints, imagePoints, imageSize, _cameraMatrix, _distCoeffs,
|
||||||
|
rvecs, tvecs, CalibType.Default, new MCvTermCriteria(30, 1e-6));
|
||||||
|
|
||||||
|
// 第三阶段:计算每张图像的重投影误差
|
||||||
|
_perImageErrors.Clear();
|
||||||
|
for (int i = 0; i < objectPoints.Size; i++)
|
||||||
|
{
|
||||||
|
ProgressChanged?.Invoke(totalImages + i + 1, totalImages * 2, $"计算重投影误差 ({i + 1}/{objectPoints.Size})");
|
||||||
|
|
||||||
|
var projectedPoints = new VectorOfPointF();
|
||||||
|
CvInvoke.ProjectPoints(objectPoints[i], rvecs[i], tvecs[i], _cameraMatrix, _distCoeffs, projectedPoints);
|
||||||
|
|
||||||
|
double error = 0;
|
||||||
|
for (int j = 0; j < projectedPoints.Size; j++)
|
||||||
|
{
|
||||||
|
var dx = imagePoints[i][j].X - projectedPoints[j].X;
|
||||||
|
var dy = imagePoints[i][j].Y - projectedPoints[j].Y;
|
||||||
|
error += Math.Sqrt(dx * dx + dy * dy);
|
||||||
|
}
|
||||||
|
_perImageErrors.Add(error / projectedPoints.Size);
|
||||||
|
}
|
||||||
|
|
||||||
|
ProgressChanged?.Invoke(totalImages * 2, totalImages * 2, "标定完成");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mat UndistortImage(Mat inputImage)
|
||||||
|
{
|
||||||
|
if (_cameraMatrix == null || _distCoeffs == null)
|
||||||
|
return inputImage;
|
||||||
|
|
||||||
|
var output = new Mat();
|
||||||
|
CvInvoke.Undistort(inputImage, output, _cameraMatrix, _distCoeffs);
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mat? DrawChessboardCorners(string imagePath, int boardWidth, int boardHeight)
|
||||||
|
{
|
||||||
|
var img = CvInvoke.Imread(imagePath);
|
||||||
|
if (img.IsEmpty) return null;
|
||||||
|
|
||||||
|
var gray = new Mat();
|
||||||
|
CvInvoke.CvtColor(img, gray, ColorConversion.Bgr2Gray);
|
||||||
|
var corners = new VectorOfPointF();
|
||||||
|
|
||||||
|
if (CvInvoke.FindChessboardCorners(gray, new System.Drawing.Size(boardWidth, boardHeight), corners))
|
||||||
|
{
|
||||||
|
CvInvoke.CornerSubPix(gray, corners, new System.Drawing.Size(11, 11), new System.Drawing.Size(-1, -1),
|
||||||
|
new MCvTermCriteria(30, 0.001));
|
||||||
|
CvInvoke.DrawChessboardCorners(img, new System.Drawing.Size(boardWidth, boardHeight), corners, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
gray.Dispose();
|
||||||
|
return img;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveCalibration(string filePath)
|
||||||
|
{
|
||||||
|
if (_cameraMatrix == null || _distCoeffs == null)
|
||||||
|
throw new InvalidOperationException("请先执行标定");
|
||||||
|
|
||||||
|
var result = new CalibrationResult
|
||||||
|
{
|
||||||
|
CameraMatrix = new double[3][],
|
||||||
|
DistortionCoeffs = new double[_distCoeffs.Rows],
|
||||||
|
ReprojectionError = _reprojectionError,
|
||||||
|
CalibrationTime = DateTime.Now
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
result.CameraMatrix[i] = new double[3];
|
||||||
|
for (int j = 0; j < 3; j++)
|
||||||
|
{
|
||||||
|
double[] data = new double[1];
|
||||||
|
Marshal.Copy(_cameraMatrix.DataPointer + (i * 3 + j) * 8, data, 0, 1);
|
||||||
|
result.CameraMatrix[i][j] = data[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < _distCoeffs.Rows; i++)
|
||||||
|
{
|
||||||
|
double[] data = new double[1];
|
||||||
|
Marshal.Copy(_distCoeffs.DataPointer + i * 8, data, 0, 1);
|
||||||
|
result.DistortionCoeffs[i] = data[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true });
|
||||||
|
File.WriteAllText(filePath, json);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool LoadCalibration(string filePath)
|
||||||
|
{
|
||||||
|
if (!File.Exists(filePath)) return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(filePath);
|
||||||
|
var result = JsonSerializer.Deserialize<CalibrationResult>(json);
|
||||||
|
|
||||||
|
if (result == null) return false;
|
||||||
|
|
||||||
|
_reprojectionError = result.ReprojectionError;
|
||||||
|
|
||||||
|
_cameraMatrix = new Mat(3, 3, DepthType.Cv64F, 1);
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
for (int j = 0; j < 3; j++)
|
||||||
|
Marshal.Copy(new[] { result.CameraMatrix[i][j] }, 0, _cameraMatrix.DataPointer + (i * 3 + j) * 8, 1);
|
||||||
|
|
||||||
|
_distCoeffs = new Mat(result.DistortionCoeffs.Length, 1, DepthType.Cv64F, 1);
|
||||||
|
for (int i = 0; i < result.DistortionCoeffs.Length; i++)
|
||||||
|
Marshal.Copy(new[] { result.DistortionCoeffs[i] }, 0, _distCoeffs.DataPointer + i * 8, 1);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
<UserControl x:Class="XP.Camera.Calibration.Controls.CalibrationControl"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"
|
||||||
|
xmlns:cal="clr-namespace:XP.Camera.Calibration"
|
||||||
|
xmlns:controls="clr-namespace:XP.Camera.Calibration.Controls"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="850"
|
||||||
|
d:DesignWidth="1400">
|
||||||
|
<UserControl.Resources>
|
||||||
|
<cal:CalibrationLocalizedStrings x:Key="LocalizedStrings" />
|
||||||
|
<SolidColorBrush x:Key="PrimaryColor" Color="#F5F5F5" />
|
||||||
|
<SolidColorBrush x:Key="AccentColor" Color="#0078D4" />
|
||||||
|
<SolidColorBrush x:Key="BackgroundColor" Color="#FAFAFA" />
|
||||||
|
<SolidColorBrush x:Key="SidebarColor" Color="#FFFFFF" />
|
||||||
|
<SolidColorBrush x:Key="BorderColor" Color="#E1E1E1" />
|
||||||
|
<SolidColorBrush x:Key="TextColor" Color="#333333" />
|
||||||
|
<SolidColorBrush x:Key="TextSecondaryColor" Color="#666666" />
|
||||||
|
|
||||||
|
<Style x:Key="ToolbarButtonStyle" TargetType="Button">
|
||||||
|
<Setter Property="Width" Value="90" />
|
||||||
|
<Setter Property="Height" Value="70" />
|
||||||
|
<Setter Property="Margin" Value="0,0,8,0" />
|
||||||
|
<Setter Property="Background" Value="White" />
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextColor}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}" />
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
|
<Setter Property="Cursor" Value="Hand" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
CornerRadius="3">
|
||||||
|
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||||
|
<ContentPresenter Content="{TemplateBinding Tag}"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Margin="0,4,0,4" />
|
||||||
|
<TextBlock Text="{TemplateBinding Content}"
|
||||||
|
FontSize="12"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Foreground="{TemplateBinding Foreground}"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
TextAlignment="Center" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter Property="Background" Value="#E5F3FF" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource AccentColor}" />
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter Property="Background" Value="#CCE8FF" />
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.5" />
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
</UserControl.Resources>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Border Grid.Row="0" Background="{StaticResource PrimaryColor}" BorderBrush="{StaticResource BorderColor}"
|
||||||
|
BorderThickness="0,0,0,1" Padding="15,10">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.CalibrationLoadImage}"
|
||||||
|
Command="{Binding LoadImageCommand}" FontFamily="Segoe UI"
|
||||||
|
Style="{StaticResource ToolbarButtonStyle}">
|
||||||
|
<Button.Tag>
|
||||||
|
<iconPacks:PackIconMaterial Kind="ImageOutline" Width="24" Height="24" />
|
||||||
|
</Button.Tag>
|
||||||
|
</Button>
|
||||||
|
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.CalibrationLoadCsv}"
|
||||||
|
Command="{Binding LoadCsvCommand}" FontFamily="Segoe UI"
|
||||||
|
Style="{StaticResource ToolbarButtonStyle}">
|
||||||
|
<Button.Tag>
|
||||||
|
<iconPacks:PackIconMaterial Kind="FileDelimited" Width="24" Height="24" />
|
||||||
|
</Button.Tag>
|
||||||
|
</Button>
|
||||||
|
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.CalibrationExecute}"
|
||||||
|
Command="{Binding CalibrateCommand}" FontFamily="Segoe UI"
|
||||||
|
Style="{StaticResource ToolbarButtonStyle}">
|
||||||
|
<Button.Tag>
|
||||||
|
<iconPacks:PackIconMaterial Kind="Crosshairs" Width="24" Height="24" />
|
||||||
|
</Button.Tag>
|
||||||
|
</Button>
|
||||||
|
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.CalibrationSave}"
|
||||||
|
Command="{Binding SaveCalibrationCommand}" FontFamily="Segoe UI"
|
||||||
|
Style="{StaticResource ToolbarButtonStyle}">
|
||||||
|
<Button.Tag>
|
||||||
|
<iconPacks:PackIconMaterial Kind="ContentSave" Width="24" Height="24" />
|
||||||
|
</Button.Tag>
|
||||||
|
</Button>
|
||||||
|
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.CalibrationLoad}"
|
||||||
|
Command="{Binding LoadCalibrationCommand}" FontFamily="Segoe UI"
|
||||||
|
Style="{StaticResource ToolbarButtonStyle}">
|
||||||
|
<Button.Tag>
|
||||||
|
<iconPacks:PackIconMaterial Kind="FolderOpen" Width="24" Height="24" />
|
||||||
|
</Button.Tag>
|
||||||
|
</Button>
|
||||||
|
<CheckBox Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.CalibrationShowWorld}"
|
||||||
|
VerticalAlignment="Center" FontFamily="Segoe UI"
|
||||||
|
IsChecked="{Binding ShowWorldCoordinates}"
|
||||||
|
Margin="10,0,0,0" FontSize="13" Foreground="{StaticResource TextColor}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Grid Grid.Row="1">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="400" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<Border Grid.Column="0" BorderBrush="{StaticResource BorderColor}" BorderThickness="0,0,1,0"
|
||||||
|
Background="{StaticResource SidebarColor}">
|
||||||
|
<Grid Margin="12">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0" Text="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.CalibrationPointList}"
|
||||||
|
FontSize="15" FontWeight="SemiBold" FontFamily="Segoe UI"
|
||||||
|
Margin="0,0,0,12" Foreground="{StaticResource TextColor}" />
|
||||||
|
|
||||||
|
<DataGrid Grid.Row="1" AutoGenerateColumns="False" CanUserAddRows="True"
|
||||||
|
ItemsSource="{Binding CalibrationPoints}"
|
||||||
|
HeadersVisibility="Column" GridLinesVisibility="All"
|
||||||
|
FontFamily="Segoe UI"
|
||||||
|
BorderBrush="{StaticResource BorderColor}" BorderThickness="1">
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<DataGridTextColumn Header="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.CalibrationPixelX}"
|
||||||
|
Binding="{Binding PixelX}" FontFamily="Segoe UI" Width="*" />
|
||||||
|
<DataGridTextColumn Header="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.CalibrationPixelY}"
|
||||||
|
Binding="{Binding PixelY}" FontFamily="Segoe UI" Width="*" />
|
||||||
|
<DataGridTextColumn Header="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.CalibrationWorldX}"
|
||||||
|
Binding="{Binding WorldX}" FontFamily="Segoe UI" Width="*" />
|
||||||
|
<DataGridTextColumn Header="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.CalibrationWorldY}"
|
||||||
|
Binding="{Binding WorldY}" FontFamily="Segoe UI" Width="*" />
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Grid Grid.Column="1" Background="{StaticResource BackgroundColor}">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<controls:ImageCanvasControl Grid.Row="0" x:Name="imageCanvas" Margin="12,12,12,8" />
|
||||||
|
|
||||||
|
<Border Grid.Row="1" Background="White" BorderBrush="{StaticResource BorderColor}" BorderThickness="1"
|
||||||
|
Margin="12,0,12,12" Padding="12" MinHeight="80">
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
||||||
|
<TextBlock TextWrapping="Wrap" FontSize="12"
|
||||||
|
Text="{Binding StatusText}" FontFamily="Segoe UI"
|
||||||
|
Foreground="{StaticResource TextColor}" />
|
||||||
|
</ScrollViewer>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
using System.Drawing;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using XP.Camera.Calibration.ViewModels;
|
||||||
|
using WpfBrushes = System.Windows.Media.Brushes;
|
||||||
|
using WpfColor = System.Windows.Media.Color;
|
||||||
|
|
||||||
|
namespace XP.Camera.Calibration.Controls;
|
||||||
|
|
||||||
|
public partial class CalibrationControl : UserControl
|
||||||
|
{
|
||||||
|
private CalibrationViewModel? _viewModel;
|
||||||
|
|
||||||
|
public CalibrationControl()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
Loaded += CalibrationControl_Loaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CalibrationControl_Loaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is CalibrationViewModel viewModel)
|
||||||
|
{
|
||||||
|
_viewModel = viewModel;
|
||||||
|
|
||||||
|
_viewModel.ImageLoadedRequested += (s, e) =>
|
||||||
|
{
|
||||||
|
imageCanvas.ReferenceImage = _viewModel.ImageSource;
|
||||||
|
imageCanvas.RoiCanvas.Children.Clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
imageCanvas.CanvasRightMouseUp += ImageCanvas_RightMouseUp;
|
||||||
|
imageCanvas.CanvasMouseWheel += ImageCanvas_MouseWheel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ImageCanvas_MouseWheel(object? sender, MouseWheelEventArgs e)
|
||||||
|
{
|
||||||
|
if (_viewModel?.CurrentImage == null) return;
|
||||||
|
|
||||||
|
double zoom = e.Delta > 0 ? 1.1 : 0.9;
|
||||||
|
imageCanvas.ZoomScale *= zoom;
|
||||||
|
imageCanvas.ZoomScale = Math.Max(0.1, Math.Min(imageCanvas.ZoomScale, 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ImageCanvas_RightMouseUp(object? sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
if (_viewModel?.CurrentImage == null) return;
|
||||||
|
|
||||||
|
var pos = e.GetPosition(imageCanvas.RoiCanvas);
|
||||||
|
float imageX = (float)pos.X;
|
||||||
|
float imageY = (float)pos.Y;
|
||||||
|
|
||||||
|
if (imageX >= 0 && imageX < _viewModel.CurrentImage.Width &&
|
||||||
|
imageY >= 0 && imageY < _viewModel.CurrentImage.Height)
|
||||||
|
{
|
||||||
|
var pixelPoint = new PointF(imageX, imageY);
|
||||||
|
var worldPoint = _viewModel.ConvertPixelToWorld(pixelPoint);
|
||||||
|
|
||||||
|
_viewModel.StatusText = $"像素坐标: ({imageX:F2}, {imageY:F2})\n世界坐标: ({worldPoint.X:F2}, {worldPoint.Y:F2})";
|
||||||
|
|
||||||
|
DrawMarkerOnCanvas(imageX, imageY, worldPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawMarkerOnCanvas(float imageX, float imageY, PointF worldPoint)
|
||||||
|
{
|
||||||
|
imageCanvas.RoiCanvas.Children.Clear();
|
||||||
|
|
||||||
|
var ellipse = new System.Windows.Shapes.Ellipse
|
||||||
|
{
|
||||||
|
Width = 10, Height = 10,
|
||||||
|
Stroke = WpfBrushes.Red, StrokeThickness = 2,
|
||||||
|
Fill = WpfBrushes.Transparent
|
||||||
|
};
|
||||||
|
Canvas.SetLeft(ellipse, imageX - 5);
|
||||||
|
Canvas.SetTop(ellipse, imageY - 5);
|
||||||
|
imageCanvas.RoiCanvas.Children.Add(ellipse);
|
||||||
|
|
||||||
|
var pixelText = new TextBlock
|
||||||
|
{
|
||||||
|
Text = $"P:({imageX:F0},{imageY:F0})",
|
||||||
|
Foreground = WpfBrushes.Red, FontSize = 12,
|
||||||
|
Background = new System.Windows.Media.SolidColorBrush(WpfColor.FromArgb(180, 255, 255, 255))
|
||||||
|
};
|
||||||
|
Canvas.SetLeft(pixelText, imageX + 10);
|
||||||
|
Canvas.SetTop(pixelText, imageY - 20);
|
||||||
|
imageCanvas.RoiCanvas.Children.Add(pixelText);
|
||||||
|
|
||||||
|
if (_viewModel?.ShowWorldCoordinates == true)
|
||||||
|
{
|
||||||
|
var worldText = new TextBlock
|
||||||
|
{
|
||||||
|
Text = $"W:({worldPoint.X:F2},{worldPoint.Y:F2})",
|
||||||
|
Foreground = WpfBrushes.Blue, FontSize = 12,
|
||||||
|
Background = new System.Windows.Media.SolidColorBrush(WpfColor.FromArgb(180, 255, 255, 255))
|
||||||
|
};
|
||||||
|
Canvas.SetLeft(worldText, imageX + 10);
|
||||||
|
Canvas.SetTop(worldText, imageY + 5);
|
||||||
|
imageCanvas.RoiCanvas.Children.Add(worldText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
<UserControl x:Class="XP.Camera.Calibration.Controls.ChessboardCalibrationControl"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"
|
||||||
|
xmlns:cal="clr-namespace:XP.Camera.Calibration"
|
||||||
|
xmlns:controls="clr-namespace:XP.Camera.Calibration.Controls"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="900"
|
||||||
|
d:DesignWidth="1600">
|
||||||
|
<UserControl.Resources>
|
||||||
|
<cal:CalibrationLocalizedStrings x:Key="LocalizedStrings" />
|
||||||
|
<SolidColorBrush x:Key="PrimaryColor" Color="#F5F5F5" />
|
||||||
|
<SolidColorBrush x:Key="AccentColor" Color="#0078D4" />
|
||||||
|
<SolidColorBrush x:Key="BackgroundColor" Color="#FAFAFA" />
|
||||||
|
<SolidColorBrush x:Key="SidebarColor" Color="#FFFFFF" />
|
||||||
|
<SolidColorBrush x:Key="BorderColor" Color="#E1E1E1" />
|
||||||
|
<SolidColorBrush x:Key="TextColor" Color="#333333" />
|
||||||
|
<SolidColorBrush x:Key="TextSecondaryColor" Color="#666666" />
|
||||||
|
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
|
||||||
|
|
||||||
|
<Style x:Key="ToolbarButtonStyle" TargetType="Button">
|
||||||
|
<Setter Property="Width" Value="90" />
|
||||||
|
<Setter Property="Height" Value="70" />
|
||||||
|
<Setter Property="Margin" Value="0,0,8,0" />
|
||||||
|
<Setter Property="Background" Value="White" />
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextColor}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}" />
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
|
<Setter Property="Cursor" Value="Hand" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
CornerRadius="3">
|
||||||
|
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||||
|
<ContentPresenter Content="{TemplateBinding Tag}"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Margin="0,4,0,4" />
|
||||||
|
<TextBlock Text="{TemplateBinding Content}"
|
||||||
|
FontSize="12" FontFamily="Segoe UI"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Foreground="{TemplateBinding Foreground}"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
TextAlignment="Center" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter Property="Background" Value="#E5F3FF" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource AccentColor}" />
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter Property="Background" Value="#CCE8FF" />
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.5" />
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
</UserControl.Resources>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Border Grid.Row="0" Background="{StaticResource PrimaryColor}" BorderBrush="{StaticResource BorderColor}"
|
||||||
|
BorderThickness="0,0,0,1" Padding="15,10">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.ChessboardAddImages}" Command="{Binding AddImagesCommand}"
|
||||||
|
Style="{StaticResource ToolbarButtonStyle}">
|
||||||
|
<Button.Tag><iconPacks:PackIconMaterial Kind="ImageMultiple" Width="24" Height="24" /></Button.Tag>
|
||||||
|
</Button>
|
||||||
|
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.ChessboardClearImages}" Command="{Binding ClearImagesCommand}"
|
||||||
|
Style="{StaticResource ToolbarButtonStyle}">
|
||||||
|
<Button.Tag><iconPacks:PackIconMaterial Kind="DeleteSweep" Width="24" Height="24" /></Button.Tag>
|
||||||
|
</Button>
|
||||||
|
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.ChessboardCalibrate}" Command="{Binding CalibrateCommand}"
|
||||||
|
Style="{StaticResource ToolbarButtonStyle}">
|
||||||
|
<Button.Tag><iconPacks:PackIconMaterial Kind="GridLarge" Width="24" Height="24" /></Button.Tag>
|
||||||
|
</Button>
|
||||||
|
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.ChessboardSave}" Command="{Binding SaveCalibrationCommand}"
|
||||||
|
Style="{StaticResource ToolbarButtonStyle}">
|
||||||
|
<Button.Tag><iconPacks:PackIconMaterial Kind="ContentSave" Width="24" Height="24" /></Button.Tag>
|
||||||
|
</Button>
|
||||||
|
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.ChessboardLoad}" Command="{Binding LoadCalibrationCommand}"
|
||||||
|
Style="{StaticResource ToolbarButtonStyle}">
|
||||||
|
<Button.Tag><iconPacks:PackIconMaterial Kind="FolderOpen" Width="24" Height="24" /></Button.Tag>
|
||||||
|
</Button>
|
||||||
|
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.ChessboardUndistort}" Command="{Binding UndistortImageCommand}"
|
||||||
|
Style="{StaticResource ToolbarButtonStyle}">
|
||||||
|
<Button.Tag><iconPacks:PackIconMaterial Kind="ImageEdit" Width="24" Height="24" /></Button.Tag>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Grid Grid.Row="1">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="400" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<Border Grid.Column="0" BorderBrush="{StaticResource BorderColor}" BorderThickness="0,0,1,0"
|
||||||
|
Background="{StaticResource SidebarColor}">
|
||||||
|
<Grid Margin="12">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="0" Margin="0,0,0,16">
|
||||||
|
<TextBlock Text="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.ChessboardParameters}" FontFamily="Segoe UI" FontSize="15" FontWeight="SemiBold"
|
||||||
|
Margin="0,0,0,12" Foreground="{StaticResource TextColor}" />
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.ChessboardWidth}" FontFamily="Segoe UI" VerticalAlignment="Center" Margin="0,0,8,8" />
|
||||||
|
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding ChessboardWidth}" FontFamily="Segoe UI" Height="28" Margin="0,0,0,8" />
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0" Text="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.ChessboardHeight}" FontFamily="Segoe UI" VerticalAlignment="Center" Margin="0,0,8,8" />
|
||||||
|
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding ChessboardHeight}" FontFamily="Segoe UI" Height="28" Margin="0,0,0,8" />
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0" Text="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.ChessboardSquareSize}" FontFamily="Segoe UI" VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||||
|
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding SquareSize}" FontFamily="Segoe UI" Height="28" />
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Text="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.ChessboardImageList}" FontFamily="Segoe UI" FontSize="15" FontWeight="SemiBold"
|
||||||
|
Margin="0,0,0,12" Foreground="{StaticResource TextColor}" />
|
||||||
|
<ListBox Grid.Row="2" ItemsSource="{Binding ImageFileNames}" SelectedIndex="{Binding SelectedImageIndex}"
|
||||||
|
FontFamily="Segoe UI" BorderBrush="{StaticResource BorderColor}" BorderThickness="1" />
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Grid Grid.Column="1" Background="{StaticResource BackgroundColor}">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="350" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<Grid Grid.Column="0">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<controls:ImageCanvasControl Grid.Row="0" x:Name="imageCanvas" Margin="12,12,8,8" />
|
||||||
|
<Border Grid.Row="1" Background="White" BorderBrush="{StaticResource BorderColor}" BorderThickness="1"
|
||||||
|
Margin="12,0,8,12" Padding="12" Height="70">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<TextBlock Grid.Row="0" Text="{Binding ProgressText}" FontSize="12" FontFamily="Segoe UI"
|
||||||
|
Margin="0,0,0,6" Foreground="{StaticResource TextColor}"
|
||||||
|
Visibility="{Binding IsCalibrating, Converter={StaticResource BooleanToVisibilityConverter}}" />
|
||||||
|
<ProgressBar Grid.Row="1" Height="24" Value="{Binding ProgressValue}" Maximum="100"
|
||||||
|
Visibility="{Binding IsCalibrating, Converter={StaticResource BooleanToVisibilityConverter}}" />
|
||||||
|
<TextBlock Grid.Row="0" Grid.RowSpan="2" Text="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.ChessboardStatusReady}" FontFamily="Segoe UI" FontSize="12"
|
||||||
|
VerticalAlignment="Center" Foreground="{StaticResource TextSecondaryColor}">
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Visibility" Value="Collapsed" />
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsCalibrating}" Value="False">
|
||||||
|
<Setter Property="Visibility" Value="Visible" />
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Border Grid.Column="1" Background="White" BorderBrush="{StaticResource BorderColor}"
|
||||||
|
BorderThickness="1,0,0,0" Padding="12" Margin="0,12,12,12">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<TextBlock Grid.Row="0" Text="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.ChessboardStatusInfo}" FontFamily="Segoe UI" FontSize="15" FontWeight="SemiBold"
|
||||||
|
Margin="0,0,0,12" Foreground="{StaticResource TextColor}" />
|
||||||
|
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
||||||
|
<TextBlock TextWrapping="Wrap" FontSize="12" FontFamily="Segoe UI"
|
||||||
|
Text="{Binding StatusText}" Foreground="{StaticResource TextColor}" />
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using XP.Camera.Calibration.ViewModels;
|
||||||
|
|
||||||
|
namespace XP.Camera.Calibration.Controls;
|
||||||
|
|
||||||
|
public partial class ChessboardCalibrationControl : UserControl
|
||||||
|
{
|
||||||
|
private ChessboardCalibrationViewModel? _viewModel;
|
||||||
|
|
||||||
|
public ChessboardCalibrationControl()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
Loaded += ChessboardCalibrationControl_Loaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ChessboardCalibrationControl_Loaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is ChessboardCalibrationViewModel viewModel)
|
||||||
|
{
|
||||||
|
_viewModel = viewModel;
|
||||||
|
|
||||||
|
_viewModel.ImageLoadedRequested += (s, e) =>
|
||||||
|
{
|
||||||
|
imageCanvas.ReferenceImage = _viewModel.ImageSource;
|
||||||
|
};
|
||||||
|
|
||||||
|
_viewModel.ImageClearedRequested += (s, e) =>
|
||||||
|
{
|
||||||
|
imageCanvas.ReferenceImage = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
imageCanvas.CanvasMouseWheel += ImageCanvas_MouseWheel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ImageCanvas_MouseWheel(object? sender, MouseWheelEventArgs e)
|
||||||
|
{
|
||||||
|
if (_viewModel?.ImageSource == null) return;
|
||||||
|
|
||||||
|
double zoom = e.Delta > 0 ? 1.1 : 0.9;
|
||||||
|
imageCanvas.ZoomScale *= zoom;
|
||||||
|
imageCanvas.ZoomScale = Math.Max(0.1, Math.Min(imageCanvas.ZoomScale, 10));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<UserControl x:Class="XP.Camera.Calibration.Controls.ImageCanvasControl"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="600" d:DesignWidth="800" x:Name="imageCanvasControl">
|
||||||
|
<Border ClipToBounds="True" RenderOptions.BitmapScalingMode="NearestNeighbor">
|
||||||
|
<Viewbox>
|
||||||
|
<AdornerDecorator x:Name="adorner" MouseWheel="Adorner_MouseWheel">
|
||||||
|
<AdornerDecorator.RenderTransform>
|
||||||
|
<TransformGroup>
|
||||||
|
<TranslateTransform X="{Binding PanningOffsetX, ElementName=imageCanvasControl}"
|
||||||
|
Y="{Binding PanningOffsetY, ElementName=imageCanvasControl}" />
|
||||||
|
<ScaleTransform ScaleX="{Binding ZoomScale, ElementName=imageCanvasControl}"
|
||||||
|
ScaleY="{Binding ZoomScale, ElementName=imageCanvasControl}"
|
||||||
|
CenterX="{Binding ZoomCenter.X, ElementName=imageCanvasControl}"
|
||||||
|
CenterY="{Binding ZoomCenter.Y, ElementName=imageCanvasControl}" />
|
||||||
|
</TransformGroup>
|
||||||
|
</AdornerDecorator.RenderTransform>
|
||||||
|
<Grid PreviewMouseMove="Canvas_MouseMove"
|
||||||
|
PreviewMouseLeftButtonUp="Canvas_MouseLeftButtonUp"
|
||||||
|
PreviewMouseRightButtonUp="Canvas_MouseRightButtonUp"
|
||||||
|
MouseEnter="Canvas_MouseEnter"
|
||||||
|
PreviewMouseLeftButtonDown="Canvas_MouseLeftButtonDown"
|
||||||
|
PreviewMouseRightButtonDown="Canvas_MouseRightButtonDown">
|
||||||
|
<ContentPresenter Content="{Binding RoiCanvas, ElementName=imageCanvasControl}"
|
||||||
|
SizeChanged="ContentPresenter_SizeChanged" />
|
||||||
|
</Grid>
|
||||||
|
</AdornerDecorator>
|
||||||
|
</Viewbox>
|
||||||
|
</Border>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
|
||||||
|
namespace XP.Camera.Calibration.Controls;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 图像画布控件 - 提供图像显示、缩放、平移功能
|
||||||
|
/// </summary>
|
||||||
|
public partial class ImageCanvasControl : UserControl
|
||||||
|
{
|
||||||
|
private Point mouseDownPoint = new Point();
|
||||||
|
|
||||||
|
#region Dependency Properties
|
||||||
|
|
||||||
|
public static readonly DependencyProperty ZoomScaleProperty =
|
||||||
|
DependencyProperty.Register("ZoomScale", typeof(double), typeof(ImageCanvasControl), new PropertyMetadata(1.0));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty ZoomCenterProperty =
|
||||||
|
DependencyProperty.Register("ZoomCenter", typeof(Point), typeof(ImageCanvasControl), new PropertyMetadata(new Point()));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty PanningOffsetXProperty =
|
||||||
|
DependencyProperty.Register("PanningOffsetX", typeof(double), typeof(ImageCanvasControl), new PropertyMetadata(0.0));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty PanningOffsetYProperty =
|
||||||
|
DependencyProperty.Register("PanningOffsetY", typeof(double), typeof(ImageCanvasControl), new PropertyMetadata(0.0));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty ReferenceImageProperty =
|
||||||
|
DependencyProperty.Register("ReferenceImage", typeof(BitmapSource), typeof(ImageCanvasControl),
|
||||||
|
new UIPropertyMetadata(null, ReferenceImageChanged));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty ImageScaleFactorProperty =
|
||||||
|
DependencyProperty.Register("ImageScaleFactor", typeof(double), typeof(ImageCanvasControl), new PropertyMetadata(1.0));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty MaxImageWidthProperty =
|
||||||
|
DependencyProperty.Register("MaxImageWidth", typeof(int), typeof(ImageCanvasControl), new PropertyMetadata(0));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty MaxImageHeightProperty =
|
||||||
|
DependencyProperty.Register("MaxImageHeight", typeof(int), typeof(ImageCanvasControl), new PropertyMetadata(0));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty EnablePanningProperty =
|
||||||
|
DependencyProperty.Register("EnablePanning", typeof(bool), typeof(ImageCanvasControl), new PropertyMetadata(true));
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Properties
|
||||||
|
|
||||||
|
public double ZoomScale
|
||||||
|
{
|
||||||
|
get => (double)GetValue(ZoomScaleProperty);
|
||||||
|
set => SetValue(ZoomScaleProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Point ZoomCenter
|
||||||
|
{
|
||||||
|
get => (Point)GetValue(ZoomCenterProperty);
|
||||||
|
set => SetValue(ZoomCenterProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double PanningOffsetX
|
||||||
|
{
|
||||||
|
get => (double)GetValue(PanningOffsetXProperty);
|
||||||
|
set => SetValue(PanningOffsetXProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double PanningOffsetY
|
||||||
|
{
|
||||||
|
get => (double)GetValue(PanningOffsetYProperty);
|
||||||
|
set => SetValue(PanningOffsetYProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BitmapSource? ReferenceImage
|
||||||
|
{
|
||||||
|
get => (BitmapSource?)GetValue(ReferenceImageProperty);
|
||||||
|
set => SetValue(ReferenceImageProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double ImageScaleFactor
|
||||||
|
{
|
||||||
|
get => (double)GetValue(ImageScaleFactorProperty);
|
||||||
|
set => SetValue(ImageScaleFactorProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int MaxImageWidth
|
||||||
|
{
|
||||||
|
get => (int)GetValue(MaxImageWidthProperty);
|
||||||
|
set => SetValue(MaxImageWidthProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int MaxImageHeight
|
||||||
|
{
|
||||||
|
get => (int)GetValue(MaxImageHeightProperty);
|
||||||
|
set => SetValue(MaxImageHeightProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool EnablePanning
|
||||||
|
{
|
||||||
|
get => (bool)GetValue(EnablePanningProperty);
|
||||||
|
set => SetValue(EnablePanningProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Canvas roiCanvas = new Canvas();
|
||||||
|
public Canvas RoiCanvas
|
||||||
|
{
|
||||||
|
get => roiCanvas;
|
||||||
|
set => roiCanvas = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Events
|
||||||
|
|
||||||
|
public event EventHandler<MouseButtonEventArgs>? CanvasRightMouseUp;
|
||||||
|
public event EventHandler<MouseButtonEventArgs>? CanvasRightMouseDown;
|
||||||
|
public event EventHandler<MouseButtonEventArgs>? CanvasLeftMouseDown;
|
||||||
|
public event EventHandler<MouseEventArgs>? CanvasMouseMove;
|
||||||
|
public event EventHandler<MouseWheelEventArgs>? CanvasMouseWheel;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public ImageCanvasControl()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Private Methods
|
||||||
|
|
||||||
|
private static void ReferenceImageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
(d as ImageCanvasControl)?.OnReferenceImageChanged(e.NewValue as BitmapSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnReferenceImageChanged(BitmapSource? bitmapSource)
|
||||||
|
{
|
||||||
|
if (bitmapSource != null)
|
||||||
|
{
|
||||||
|
ImageBrush brush = new ImageBrush { ImageSource = bitmapSource, Stretch = Stretch.Uniform };
|
||||||
|
RoiCanvas.Background = brush;
|
||||||
|
RoiCanvas.Height = bitmapSource.Height;
|
||||||
|
RoiCanvas.Width = bitmapSource.Width;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RoiCanvas.Height = MaxImageHeight > 0 ? MaxImageHeight : 600;
|
||||||
|
RoiCanvas.Width = MaxImageWidth > 0 ? MaxImageWidth : 800;
|
||||||
|
RoiCanvas.Background = Brushes.LightGray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private double CalculateScaleFactor()
|
||||||
|
{
|
||||||
|
if (ActualWidth <= 0) return 1;
|
||||||
|
double scaleFactor = Math.Max(RoiCanvas.Width / ActualWidth, RoiCanvas.Height / ActualHeight);
|
||||||
|
if (scaleFactor < 0)
|
||||||
|
scaleFactor = Math.Min(RoiCanvas.Width / ActualWidth, RoiCanvas.Height / ActualHeight);
|
||||||
|
return scaleFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Event Handlers
|
||||||
|
|
||||||
|
private void Canvas_MouseEnter(object sender, MouseEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.LeftButton == MouseButtonState.Pressed)
|
||||||
|
mouseDownPoint = e.GetPosition(RoiCanvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Canvas_MouseMove(object sender, MouseEventArgs e)
|
||||||
|
{
|
||||||
|
CanvasMouseMove?.Invoke(sender, e);
|
||||||
|
if (EnablePanning && e.LeftButton == MouseButtonState.Pressed)
|
||||||
|
{
|
||||||
|
Point mousePoint = e.GetPosition(RoiCanvas);
|
||||||
|
double mouseMoveLength = Point.Subtract(mousePoint, mouseDownPoint).Length;
|
||||||
|
if (mouseMoveLength > (10 * CalculateScaleFactor()) / ZoomScale)
|
||||||
|
{
|
||||||
|
PanningOffsetX += mousePoint.X - mouseDownPoint.X;
|
||||||
|
PanningOffsetY += mousePoint.Y - mouseDownPoint.Y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Canvas_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { }
|
||||||
|
|
||||||
|
private void Canvas_MouseRightButtonUp(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
CanvasRightMouseUp?.Invoke(sender, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Canvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
mouseDownPoint = e.GetPosition(RoiCanvas);
|
||||||
|
CanvasLeftMouseDown?.Invoke(sender, e);
|
||||||
|
if (EnablePanning && e.ClickCount == 2)
|
||||||
|
{
|
||||||
|
ResetView();
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Canvas_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
CanvasRightMouseDown?.Invoke(sender, e);
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Adorner_MouseWheel(object sender, MouseWheelEventArgs e)
|
||||||
|
{
|
||||||
|
CanvasMouseWheel?.Invoke(sender, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ContentPresenter_SizeChanged(object sender, SizeChangedEventArgs e)
|
||||||
|
{
|
||||||
|
ImageScaleFactor = CalculateScaleFactor();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetView()
|
||||||
|
{
|
||||||
|
ZoomScale = 1.0;
|
||||||
|
PanningOffsetX = 0.0;
|
||||||
|
PanningOffsetY = 0.0;
|
||||||
|
ZoomCenter = new Point(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using Microsoft.Win32;
|
||||||
|
using System.Windows;
|
||||||
|
using XP.Common.GeneralForm.Views;
|
||||||
|
|
||||||
|
namespace XP.Camera.Calibration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 默认对话框服务实现,使用 HexMessageBox 自定义消息框。
|
||||||
|
/// 外部项目可直接使用,无需额外依赖。
|
||||||
|
/// </summary>
|
||||||
|
public class DefaultCalibrationDialogService : ICalibrationDialogService
|
||||||
|
{
|
||||||
|
public void ShowMessage(string message, string title)
|
||||||
|
{
|
||||||
|
HexMessageBox.Show(message, MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowError(string message, string title)
|
||||||
|
{
|
||||||
|
HexMessageBox.Show(message, MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowInfo(string message, string title)
|
||||||
|
{
|
||||||
|
HexMessageBox.Show(message, MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShowConfirm(string message, string title)
|
||||||
|
{
|
||||||
|
return HexMessageBox.Show(message, MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? ShowOpenFileDialog(string filter)
|
||||||
|
{
|
||||||
|
var dialog = new OpenFileDialog { Filter = filter };
|
||||||
|
return dialog.ShowDialog() == true ? dialog.FileName : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string[]? ShowOpenMultipleFilesDialog(string filter)
|
||||||
|
{
|
||||||
|
var dialog = new OpenFileDialog { Filter = filter, Multiselect = true };
|
||||||
|
return dialog.ShowDialog() == true ? dialog.FileNames : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? ShowSaveFileDialog(string filter, string? defaultFileName = null)
|
||||||
|
{
|
||||||
|
var dialog = new SaveFileDialog { Filter = filter, FileName = defaultFileName ?? string.Empty };
|
||||||
|
return dialog.ShowDialog() == true ? dialog.FileName : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace XP.Camera.Calibration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 对话框服务接口,用于标定模块的文件选择和消息提示。
|
||||||
|
/// </summary>
|
||||||
|
public interface ICalibrationDialogService
|
||||||
|
{
|
||||||
|
void ShowMessage(string message, string title);
|
||||||
|
void ShowError(string message, string title);
|
||||||
|
void ShowInfo(string message, string title);
|
||||||
|
bool ShowConfirm(string message, string title);
|
||||||
|
string? ShowOpenFileDialog(string filter);
|
||||||
|
string[]? ShowOpenMultipleFilesDialog(string filter);
|
||||||
|
string? ShowSaveFileDialog(string filter, string? defaultFileName = null);
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// <auto-generated>
|
||||||
|
// 此代码由工具生成。
|
||||||
|
// 如果重新生成代码,将丢失对此文件所做的更改。
|
||||||
|
// </auto-generated>
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace XP.Camera.Calibration.Resources {
|
||||||
|
using System;
|
||||||
|
|
||||||
|
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||||
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
|
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||||
|
public class CalibrationResources {
|
||||||
|
|
||||||
|
private static global::System.Resources.ResourceManager resourceMan;
|
||||||
|
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||||
|
|
||||||
|
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||||
|
internal CalibrationResources() { }
|
||||||
|
|
||||||
|
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||||
|
public static global::System.Resources.ResourceManager ResourceManager {
|
||||||
|
get {
|
||||||
|
if (object.ReferenceEquals(resourceMan, null)) {
|
||||||
|
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("XP.Camera.Calibration.Resources.CalibrationResources", typeof(CalibrationResources).Assembly);
|
||||||
|
resourceMan = temp;
|
||||||
|
}
|
||||||
|
return resourceMan;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||||
|
public static global::System.Globalization.CultureInfo Culture {
|
||||||
|
get { return resourceCulture; }
|
||||||
|
set { resourceCulture = value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 九点标定
|
||||||
|
public static string CalibrationToolTitle => ResourceManager.GetString("CalibrationToolTitle", resourceCulture) ?? "";
|
||||||
|
public static string CalibrationLoadImage => ResourceManager.GetString("CalibrationLoadImage", resourceCulture) ?? "";
|
||||||
|
public static string CalibrationLoadCsv => ResourceManager.GetString("CalibrationLoadCsv", resourceCulture) ?? "";
|
||||||
|
public static string CalibrationExecute => ResourceManager.GetString("CalibrationExecute", resourceCulture) ?? "";
|
||||||
|
public static string CalibrationSave => ResourceManager.GetString("CalibrationSave", resourceCulture) ?? "";
|
||||||
|
public static string CalibrationLoad => ResourceManager.GetString("CalibrationLoad", resourceCulture) ?? "";
|
||||||
|
public static string CalibrationShowWorld => ResourceManager.GetString("CalibrationShowWorld", resourceCulture) ?? "";
|
||||||
|
public static string CalibrationPointList => ResourceManager.GetString("CalibrationPointList", resourceCulture) ?? "";
|
||||||
|
public static string CalibrationPixelX => ResourceManager.GetString("CalibrationPixelX", resourceCulture) ?? "";
|
||||||
|
public static string CalibrationPixelY => ResourceManager.GetString("CalibrationPixelY", resourceCulture) ?? "";
|
||||||
|
public static string CalibrationWorldX => ResourceManager.GetString("CalibrationWorldX", resourceCulture) ?? "";
|
||||||
|
public static string CalibrationWorldY => ResourceManager.GetString("CalibrationWorldY", resourceCulture) ?? "";
|
||||||
|
public static string CalibrationStatusReady => ResourceManager.GetString("CalibrationStatusReady", resourceCulture) ?? "";
|
||||||
|
public static string CalibrationStatusImageLoaded => ResourceManager.GetString("CalibrationStatusImageLoaded", resourceCulture) ?? "";
|
||||||
|
public static string CalibrationStatusCsvLoaded => ResourceManager.GetString("CalibrationStatusCsvLoaded", resourceCulture) ?? "";
|
||||||
|
public static string CalibrationStatusSuccess => ResourceManager.GetString("CalibrationStatusSuccess", resourceCulture) ?? "";
|
||||||
|
public static string CalibrationStatusFailed => ResourceManager.GetString("CalibrationStatusFailed", resourceCulture) ?? "";
|
||||||
|
public static string CalibrationStatusSaved => ResourceManager.GetString("CalibrationStatusSaved", resourceCulture) ?? "";
|
||||||
|
public static string CalibrationStatusLoaded => ResourceManager.GetString("CalibrationStatusLoaded", resourceCulture) ?? "";
|
||||||
|
public static string CalibrationCoordinates => ResourceManager.GetString("CalibrationCoordinates", resourceCulture) ?? "";
|
||||||
|
public static string CalibrationErrorMinPoints => ResourceManager.GetString("CalibrationErrorMinPoints", resourceCulture) ?? "";
|
||||||
|
public static string CalibrationSuccessTitle => ResourceManager.GetString("CalibrationSuccessTitle", resourceCulture) ?? "";
|
||||||
|
public static string CalibrationSuccessMessage => ResourceManager.GetString("CalibrationSuccessMessage", resourceCulture) ?? "";
|
||||||
|
public static string CalibrationSaveSuccess => ResourceManager.GetString("CalibrationSaveSuccess", resourceCulture) ?? "";
|
||||||
|
public static string CalibrationLoadSuccess => ResourceManager.GetString("CalibrationLoadSuccess", resourceCulture) ?? "";
|
||||||
|
public static string CalibrationLoadFailed => ResourceManager.GetString("CalibrationLoadFailed", resourceCulture) ?? "";
|
||||||
|
|
||||||
|
// 棋盘格标定
|
||||||
|
public static string ChessboardToolTitle => ResourceManager.GetString("ChessboardToolTitle", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardAddImages => ResourceManager.GetString("ChessboardAddImages", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardClearImages => ResourceManager.GetString("ChessboardClearImages", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardCalibrate => ResourceManager.GetString("ChessboardCalibrate", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardSave => ResourceManager.GetString("ChessboardSave", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardLoad => ResourceManager.GetString("ChessboardLoad", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardUndistort => ResourceManager.GetString("ChessboardUndistort", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardParameters => ResourceManager.GetString("ChessboardParameters", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardWidth => ResourceManager.GetString("ChessboardWidth", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardHeight => ResourceManager.GetString("ChessboardHeight", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardSquareSize => ResourceManager.GetString("ChessboardSquareSize", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardImageList => ResourceManager.GetString("ChessboardImageList", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardStatusInfo => ResourceManager.GetString("ChessboardStatusInfo", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardStatusReady => ResourceManager.GetString("ChessboardStatusReady", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardStatusAdded => ResourceManager.GetString("ChessboardStatusAdded", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardStatusCleared => ResourceManager.GetString("ChessboardStatusCleared", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardStatusCalibrating => ResourceManager.GetString("ChessboardStatusCalibrating", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardStatusSuccess => ResourceManager.GetString("ChessboardStatusSuccess", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardStatusFailed => ResourceManager.GetString("ChessboardStatusFailed", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardStatusSaved => ResourceManager.GetString("ChessboardStatusSaved", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardStatusLoaded => ResourceManager.GetString("ChessboardStatusLoaded", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardStatusUndistorted => ResourceManager.GetString("ChessboardStatusUndistorted", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardStatusImageError => ResourceManager.GetString("ChessboardStatusImageError", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardProgressPreparing => ResourceManager.GetString("ChessboardProgressPreparing", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardProgressDetecting => ResourceManager.GetString("ChessboardProgressDetecting", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardProgressCalibrating => ResourceManager.GetString("ChessboardProgressCalibrating", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardProgressCalculating => ResourceManager.GetString("ChessboardProgressCalculating", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardProgressComplete => ResourceManager.GetString("ChessboardProgressComplete", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardProgressFailed => ResourceManager.GetString("ChessboardProgressFailed", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardErrorMinImages => ResourceManager.GetString("ChessboardErrorMinImages", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardErrorInsufficientValid => ResourceManager.GetString("ChessboardErrorInsufficientValid", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardSaveSuccess => ResourceManager.GetString("ChessboardSaveSuccess", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardLoadSuccess => ResourceManager.GetString("ChessboardLoadSuccess", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardCalibrationComplete => ResourceManager.GetString("ChessboardCalibrationComplete", resourceCulture) ?? "";
|
||||||
|
public static string ChessboardImageError => ResourceManager.GetString("ChessboardImageError", resourceCulture) ?? "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<!-- Nine-Point Calibration -->
|
||||||
|
<data name="CalibrationToolTitle" xml:space="preserve">
|
||||||
|
<value>Nine-Point Calibration Tool</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationLoadImage" xml:space="preserve">
|
||||||
|
<value>Load Image</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationLoadCsv" xml:space="preserve">
|
||||||
|
<value>Load from CSV</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationExecute" xml:space="preserve">
|
||||||
|
<value>Calibrate</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationSave" xml:space="preserve">
|
||||||
|
<value>Save Calibration</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationLoad" xml:space="preserve">
|
||||||
|
<value>Load Calibration</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationShowWorld" xml:space="preserve">
|
||||||
|
<value>Show World Coordinates</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationPointList" xml:space="preserve">
|
||||||
|
<value>Calibration Points</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationPixelX" xml:space="preserve">
|
||||||
|
<value>Pixel X</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationPixelY" xml:space="preserve">
|
||||||
|
<value>Pixel Y</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationWorldX" xml:space="preserve">
|
||||||
|
<value>World X</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationWorldY" xml:space="preserve">
|
||||||
|
<value>World Y</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationStatusReady" xml:space="preserve">
|
||||||
|
<value>Ready</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationStatusImageLoaded" xml:space="preserve">
|
||||||
|
<value>Status: Image loaded
|
||||||
|
{0}
|
||||||
|
Right-click on image to view coordinate conversion</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationStatusCsvLoaded" xml:space="preserve">
|
||||||
|
<value>Status: Loaded {0} calibration points from CSV
|
||||||
|
{1}</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationStatusSuccess" xml:space="preserve">
|
||||||
|
<value>Status: Calibration successful! Using {0} points</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationStatusFailed" xml:space="preserve">
|
||||||
|
<value>Status: Calibration failed</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationStatusSaved" xml:space="preserve">
|
||||||
|
<value>Status: Calibration saved to
|
||||||
|
{0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationStatusLoaded" xml:space="preserve">
|
||||||
|
<value>Status: Calibration loaded from
|
||||||
|
{0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationCoordinates" xml:space="preserve">
|
||||||
|
<value>Pixel coordinates: ({0:F2}, {1:F2})
|
||||||
|
World coordinates: ({2:F2}, {3:F2})</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationErrorMinPoints" xml:space="preserve">
|
||||||
|
<value>At least 4 calibration points required!</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationSuccessTitle" xml:space="preserve">
|
||||||
|
<value>Success</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationSuccessMessage" xml:space="preserve">
|
||||||
|
<value>Calibration completed!</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationSaveSuccess" xml:space="preserve">
|
||||||
|
<value>Save successful!</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationLoadSuccess" xml:space="preserve">
|
||||||
|
<value>Load successful!</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationLoadFailed" xml:space="preserve">
|
||||||
|
<value>Load failed!</value>
|
||||||
|
</data>
|
||||||
|
<!-- Chessboard Calibration -->
|
||||||
|
<data name="ChessboardToolTitle" xml:space="preserve">
|
||||||
|
<value>Chessboard Calibration Tool</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardAddImages" xml:space="preserve">
|
||||||
|
<value>Add Images</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardClearImages" xml:space="preserve">
|
||||||
|
<value>Clear List</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardCalibrate" xml:space="preserve">
|
||||||
|
<value>Calibrate</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardSave" xml:space="preserve">
|
||||||
|
<value>Save Calibration</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardLoad" xml:space="preserve">
|
||||||
|
<value>Load Calibration</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardUndistort" xml:space="preserve">
|
||||||
|
<value>Undistort Image</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardParameters" xml:space="preserve">
|
||||||
|
<value>Chessboard Parameters</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardWidth" xml:space="preserve">
|
||||||
|
<value>Inner Corners Width:</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardHeight" xml:space="preserve">
|
||||||
|
<value>Inner Corners Height:</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardSquareSize" xml:space="preserve">
|
||||||
|
<value>Square Size (mm):</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardImageList" xml:space="preserve">
|
||||||
|
<value>Calibration Images</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardStatusInfo" xml:space="preserve">
|
||||||
|
<value>Status Information</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardStatusReady" xml:space="preserve">
|
||||||
|
<value>Ready</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardStatusAdded" xml:space="preserve">
|
||||||
|
<value>Added {0} images</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardStatusCleared" xml:space="preserve">
|
||||||
|
<value>Image list cleared</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardStatusCalibrating" xml:space="preserve">
|
||||||
|
<value>Calibrating, please wait...</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardStatusSuccess" xml:space="preserve">
|
||||||
|
<value>Calibration successful!
|
||||||
|
Overall reprojection error: {0:F4} pixels
|
||||||
|
|
||||||
|
{1}</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardStatusFailed" xml:space="preserve">
|
||||||
|
<value>Calibration failed: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardStatusSaved" xml:space="preserve">
|
||||||
|
<value>Calibration saved:
|
||||||
|
{0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardStatusLoaded" xml:space="preserve">
|
||||||
|
<value>Calibration loaded:
|
||||||
|
{0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardStatusUndistorted" xml:space="preserve">
|
||||||
|
<value>Image undistorted:
|
||||||
|
{0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardStatusImageError" xml:space="preserve">
|
||||||
|
<value>Image {0}
|
||||||
|
Reprojection error: {1:F4} pixels</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardProgressPreparing" xml:space="preserve">
|
||||||
|
<value>Preparing calibration...</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardProgressDetecting" xml:space="preserve">
|
||||||
|
<value>Detecting corners ({0}/{1})</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardProgressCalibrating" xml:space="preserve">
|
||||||
|
<value>Performing camera calibration...</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardProgressCalculating" xml:space="preserve">
|
||||||
|
<value>Calculating reprojection errors ({0}/{1})</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardProgressComplete" xml:space="preserve">
|
||||||
|
<value>Calibration complete</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardProgressFailed" xml:space="preserve">
|
||||||
|
<value>Calibration failed</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardErrorMinImages" xml:space="preserve">
|
||||||
|
<value>At least 3 images required!</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardErrorInsufficientValid" xml:space="preserve">
|
||||||
|
<value>Insufficient valid images, need at least 3, current {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardSaveSuccess" xml:space="preserve">
|
||||||
|
<value>Save successful!</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardLoadSuccess" xml:space="preserve">
|
||||||
|
<value>Load successful!</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardCalibrationComplete" xml:space="preserve">
|
||||||
|
<value>Calibration completed!</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardImageError" xml:space="preserve">
|
||||||
|
<value>Image{0}: {1:F4} pixels</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<!-- 九点标定 -->
|
||||||
|
<data name="CalibrationToolTitle" xml:space="preserve">
|
||||||
|
<value>九点标定工具</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationLoadImage" xml:space="preserve">
|
||||||
|
<value>加载图像</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationLoadCsv" xml:space="preserve">
|
||||||
|
<value>从CSV加载</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationExecute" xml:space="preserve">
|
||||||
|
<value>执行标定</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationSave" xml:space="preserve">
|
||||||
|
<value>保存标定</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationLoad" xml:space="preserve">
|
||||||
|
<value>加载标定</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationShowWorld" xml:space="preserve">
|
||||||
|
<value>显示世界坐标</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationPointList" xml:space="preserve">
|
||||||
|
<value>标定点列表</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationPixelX" xml:space="preserve">
|
||||||
|
<value>像素X</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationPixelY" xml:space="preserve">
|
||||||
|
<value>像素Y</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationWorldX" xml:space="preserve">
|
||||||
|
<value>世界X</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationWorldY" xml:space="preserve">
|
||||||
|
<value>世界Y</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationStatusReady" xml:space="preserve">
|
||||||
|
<value>就绪</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationStatusImageLoaded" xml:space="preserve">
|
||||||
|
<value>状态:图像已加载
|
||||||
|
{0}
|
||||||
|
右键点击图像查看坐标转换</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationStatusCsvLoaded" xml:space="preserve">
|
||||||
|
<value>状态:已从CSV加载 {0} 个标定点
|
||||||
|
{1}</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationStatusSuccess" xml:space="preserve">
|
||||||
|
<value>状态:标定成功!使用 {0} 个点</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationStatusFailed" xml:space="preserve">
|
||||||
|
<value>状态:标定失败</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationStatusSaved" xml:space="preserve">
|
||||||
|
<value>状态:标定文件已保存到
|
||||||
|
{0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationStatusLoaded" xml:space="preserve">
|
||||||
|
<value>状态:标定文件已加载
|
||||||
|
{0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationCoordinates" xml:space="preserve">
|
||||||
|
<value>像素坐标: ({0:F2}, {1:F2})
|
||||||
|
世界坐标: ({2:F2}, {3:F2})</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationErrorMinPoints" xml:space="preserve">
|
||||||
|
<value>至少需要4个标定点!</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationSuccessTitle" xml:space="preserve">
|
||||||
|
<value>成功</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationSuccessMessage" xml:space="preserve">
|
||||||
|
<value>标定完成!</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationSaveSuccess" xml:space="preserve">
|
||||||
|
<value>保存成功!</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationLoadSuccess" xml:space="preserve">
|
||||||
|
<value>加载成功!</value>
|
||||||
|
</data>
|
||||||
|
<data name="CalibrationLoadFailed" xml:space="preserve">
|
||||||
|
<value>加载失败!</value>
|
||||||
|
</data>
|
||||||
|
<!-- 棋盘格标定 -->
|
||||||
|
<data name="ChessboardToolTitle" xml:space="preserve">
|
||||||
|
<value>棋盘格畸变校正工具</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardAddImages" xml:space="preserve">
|
||||||
|
<value>添加图像</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardClearImages" xml:space="preserve">
|
||||||
|
<value>清空列表</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardCalibrate" xml:space="preserve">
|
||||||
|
<value>执行标定</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardSave" xml:space="preserve">
|
||||||
|
<value>保存标定</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardLoad" xml:space="preserve">
|
||||||
|
<value>加载标定</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardUndistort" xml:space="preserve">
|
||||||
|
<value>校正图像</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardParameters" xml:space="preserve">
|
||||||
|
<value>棋盘格参数</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardWidth" xml:space="preserve">
|
||||||
|
<value>内角点宽度:</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardHeight" xml:space="preserve">
|
||||||
|
<value>内角点高度:</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardSquareSize" xml:space="preserve">
|
||||||
|
<value>方格尺寸(mm):</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardImageList" xml:space="preserve">
|
||||||
|
<value>标定图像列表</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardStatusInfo" xml:space="preserve">
|
||||||
|
<value>状态信息</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardStatusReady" xml:space="preserve">
|
||||||
|
<value>就绪</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardStatusAdded" xml:space="preserve">
|
||||||
|
<value>已添加 {0} 张图像</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardStatusCleared" xml:space="preserve">
|
||||||
|
<value>已清空图像列表</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardStatusCalibrating" xml:space="preserve">
|
||||||
|
<value>正在标定,请稍候...</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardStatusSuccess" xml:space="preserve">
|
||||||
|
<value>标定成功!
|
||||||
|
总体重投影误差: {0:F4} 像素
|
||||||
|
|
||||||
|
{1}</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardStatusFailed" xml:space="preserve">
|
||||||
|
<value>标定失败: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardStatusSaved" xml:space="preserve">
|
||||||
|
<value>标定已保存:
|
||||||
|
{0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardStatusLoaded" xml:space="preserve">
|
||||||
|
<value>标定已加载:
|
||||||
|
{0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardStatusUndistorted" xml:space="preserve">
|
||||||
|
<value>已校正图像:
|
||||||
|
{0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardStatusImageError" xml:space="preserve">
|
||||||
|
<value>图像 {0}
|
||||||
|
重投影误差: {1:F4} 像素</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardProgressPreparing" xml:space="preserve">
|
||||||
|
<value>准备标定...</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardProgressDetecting" xml:space="preserve">
|
||||||
|
<value>检测角点 ({0}/{1})</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardProgressCalibrating" xml:space="preserve">
|
||||||
|
<value>执行相机标定...</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardProgressCalculating" xml:space="preserve">
|
||||||
|
<value>计算重投影误差 ({0}/{1})</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardProgressComplete" xml:space="preserve">
|
||||||
|
<value>标定完成</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardProgressFailed" xml:space="preserve">
|
||||||
|
<value>标定失败</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardErrorMinImages" xml:space="preserve">
|
||||||
|
<value>至少需要3张图像!</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardErrorInsufficientValid" xml:space="preserve">
|
||||||
|
<value>有效图像不足,需要至少3张,当前{0}张</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardSaveSuccess" xml:space="preserve">
|
||||||
|
<value>保存成功!</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardLoadSuccess" xml:space="preserve">
|
||||||
|
<value>加载成功!</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardCalibrationComplete" xml:space="preserve">
|
||||||
|
<value>标定完成!</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChessboardImageError" xml:space="preserve">
|
||||||
|
<value>图像{0}: {1:F4} 像素</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
using Emgu.CV;
|
||||||
|
using Emgu.CV.Structure;
|
||||||
|
using Serilog;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
using Res = XP.Camera.Calibration.Resources.CalibrationResources;
|
||||||
|
|
||||||
|
namespace XP.Camera.Calibration.ViewModels;
|
||||||
|
|
||||||
|
public class CalibrationViewModel : BindableBase
|
||||||
|
{
|
||||||
|
private readonly ICalibrationDialogService _dialogService;
|
||||||
|
private readonly CalibrationProcessor _calibrator = new();
|
||||||
|
private Image<Bgr, byte>? _currentImage;
|
||||||
|
private static readonly ILogger _logger = Log.ForContext<CalibrationViewModel>();
|
||||||
|
private BitmapSource? _imageSource;
|
||||||
|
private string _statusText = Res.CalibrationStatusReady;
|
||||||
|
private bool _showWorldCoordinates;
|
||||||
|
|
||||||
|
public CalibrationViewModel(ICalibrationDialogService dialogService)
|
||||||
|
{
|
||||||
|
_dialogService = dialogService;
|
||||||
|
CalibrationPoints = new ObservableCollection<CalibrationProcessor.CalibrationPoint>();
|
||||||
|
|
||||||
|
LoadImageCommand = new DelegateCommand(LoadImage);
|
||||||
|
LoadCsvCommand = new DelegateCommand(LoadCsv);
|
||||||
|
CalibrateCommand = new DelegateCommand(Calibrate, CanCalibrate)
|
||||||
|
.ObservesProperty(() => CalibrationPoints.Count);
|
||||||
|
SaveCalibrationCommand = new DelegateCommand(SaveCalibration);
|
||||||
|
LoadCalibrationCommand = new DelegateCommand(LoadCalibration);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableCollection<CalibrationProcessor.CalibrationPoint> CalibrationPoints { get; }
|
||||||
|
|
||||||
|
public BitmapSource? ImageSource
|
||||||
|
{
|
||||||
|
get => _imageSource;
|
||||||
|
set => SetProperty(ref _imageSource, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string StatusText
|
||||||
|
{
|
||||||
|
get => _statusText;
|
||||||
|
set => SetProperty(ref _statusText, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShowWorldCoordinates
|
||||||
|
{
|
||||||
|
get => _showWorldCoordinates;
|
||||||
|
set => SetProperty(ref _showWorldCoordinates, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DelegateCommand LoadImageCommand { get; }
|
||||||
|
public DelegateCommand LoadCsvCommand { get; }
|
||||||
|
public DelegateCommand CalibrateCommand { get; }
|
||||||
|
public DelegateCommand SaveCalibrationCommand { get; }
|
||||||
|
public DelegateCommand LoadCalibrationCommand { get; }
|
||||||
|
|
||||||
|
private void LoadImage()
|
||||||
|
{
|
||||||
|
_logger.Information("Loading image file");
|
||||||
|
var fileName = _dialogService.ShowOpenFileDialog("图像文件|*.jpg;*.png;*.bmp;*.tif");
|
||||||
|
if (fileName == null) return;
|
||||||
|
|
||||||
|
_currentImage = new Image<Bgr, byte>(fileName);
|
||||||
|
ImageSource = MatToBitmapSource(_currentImage.Mat);
|
||||||
|
StatusText = string.Format(Res.CalibrationStatusImageLoaded, fileName);
|
||||||
|
RaiseEvent(ImageLoadedRequested);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadCsv()
|
||||||
|
{
|
||||||
|
var fileName = _dialogService.ShowOpenFileDialog("CSV文件|*.csv|所有文件|*.*");
|
||||||
|
if (fileName == null) return;
|
||||||
|
|
||||||
|
var points = _calibrator.LoadPointsFromCsv(fileName);
|
||||||
|
CalibrationPoints.Clear();
|
||||||
|
foreach (var pt in points)
|
||||||
|
CalibrationPoints.Add(pt);
|
||||||
|
|
||||||
|
StatusText = string.Format(Res.CalibrationStatusCsvLoaded, CalibrationPoints.Count, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanCalibrate() => CalibrationPoints.Count >= 4;
|
||||||
|
|
||||||
|
private void Calibrate()
|
||||||
|
{
|
||||||
|
if (CalibrationPoints.Count < 4)
|
||||||
|
{
|
||||||
|
_dialogService.ShowError(Res.CalibrationErrorMinPoints, Res.CalibrationSuccessTitle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_calibrator.Calibrate(new List<CalibrationProcessor.CalibrationPoint>(CalibrationPoints)))
|
||||||
|
{
|
||||||
|
StatusText = string.Format(Res.CalibrationStatusSuccess, CalibrationPoints.Count);
|
||||||
|
_dialogService.ShowInfo(Res.CalibrationSuccessMessage, Res.CalibrationSuccessTitle);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StatusText = Res.CalibrationStatusFailed;
|
||||||
|
_dialogService.ShowError(Res.CalibrationStatusFailed, Res.CalibrationSuccessTitle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveCalibration()
|
||||||
|
{
|
||||||
|
var fileName = _dialogService.ShowSaveFileDialog("标定文件|*.json", "calibration.json");
|
||||||
|
if (fileName == null) return;
|
||||||
|
|
||||||
|
_calibrator.SaveCalibration(fileName, new List<CalibrationProcessor.CalibrationPoint>(CalibrationPoints));
|
||||||
|
StatusText = string.Format(Res.CalibrationStatusSaved, fileName);
|
||||||
|
_dialogService.ShowInfo(Res.CalibrationSaveSuccess, Res.CalibrationSuccessTitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadCalibration()
|
||||||
|
{
|
||||||
|
var fileName = _dialogService.ShowOpenFileDialog("标定文件|*.json");
|
||||||
|
if (fileName == null) return;
|
||||||
|
|
||||||
|
if (_calibrator.LoadCalibration(fileName))
|
||||||
|
{
|
||||||
|
StatusText = string.Format(Res.CalibrationStatusLoaded, fileName);
|
||||||
|
_dialogService.ShowInfo(Res.CalibrationLoadSuccess, Res.CalibrationSuccessTitle);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_dialogService.ShowError(Res.CalibrationLoadFailed, Res.CalibrationSuccessTitle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PointF ConvertPixelToWorld(PointF pixel) => _calibrator.PixelToWorld(pixel);
|
||||||
|
|
||||||
|
public Image<Bgr, byte>? CurrentImage => _currentImage;
|
||||||
|
|
||||||
|
public event EventHandler? ImageLoadedRequested;
|
||||||
|
|
||||||
|
private void RaiseEvent(EventHandler? handler) => handler?.Invoke(this, EventArgs.Empty);
|
||||||
|
|
||||||
|
private static BitmapSource MatToBitmapSource(Mat mat)
|
||||||
|
{
|
||||||
|
using var bitmap = mat.ToBitmap();
|
||||||
|
var hBitmap = bitmap.GetHbitmap();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(
|
||||||
|
hBitmap, IntPtr.Zero, System.Windows.Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
DeleteObject(hBitmap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[System.Runtime.InteropServices.DllImport("gdi32.dll")]
|
||||||
|
private static extern bool DeleteObject(IntPtr hObject);
|
||||||
|
}
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
using Emgu.CV;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
using Res = XP.Camera.Calibration.Resources.CalibrationResources;
|
||||||
|
|
||||||
|
namespace XP.Camera.Calibration.ViewModels;
|
||||||
|
|
||||||
|
public class ChessboardCalibrationViewModel : BindableBase
|
||||||
|
{
|
||||||
|
private readonly ICalibrationDialogService _dialogService;
|
||||||
|
private readonly ChessboardCalibrator _calibrator = new();
|
||||||
|
private readonly ObservableCollection<string> _imagePaths = new();
|
||||||
|
|
||||||
|
private BitmapSource? _imageSource;
|
||||||
|
private string _statusText = Res.ChessboardStatusReady;
|
||||||
|
private int _chessboardWidth = 11;
|
||||||
|
private int _chessboardHeight = 8;
|
||||||
|
private float _squareSize = 15;
|
||||||
|
private int _selectedImageIndex = -1;
|
||||||
|
private bool _isCalibrating = false;
|
||||||
|
private double _progressValue = 0;
|
||||||
|
private string _progressText = "";
|
||||||
|
|
||||||
|
public ChessboardCalibrationViewModel(ICalibrationDialogService dialogService)
|
||||||
|
{
|
||||||
|
_dialogService = dialogService;
|
||||||
|
ImageFileNames = new ObservableCollection<string>();
|
||||||
|
|
||||||
|
AddImagesCommand = new DelegateCommand(AddImages);
|
||||||
|
ClearImagesCommand = new DelegateCommand(ClearImages, CanClearImages)
|
||||||
|
.ObservesProperty(() => ImageFileNames.Count);
|
||||||
|
CalibrateCommand = new DelegateCommand(async () => await CalibrateAsync(), CanCalibrate)
|
||||||
|
.ObservesProperty(() => ImageFileNames.Count)
|
||||||
|
.ObservesProperty(() => IsCalibrating);
|
||||||
|
SaveCalibrationCommand = new DelegateCommand(SaveCalibration);
|
||||||
|
LoadCalibrationCommand = new DelegateCommand(LoadCalibration);
|
||||||
|
UndistortImageCommand = new DelegateCommand(UndistortImage);
|
||||||
|
|
||||||
|
_calibrator.ProgressChanged += OnCalibrationProgressChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableCollection<string> ImageFileNames { get; }
|
||||||
|
public BitmapSource? ImageSource { get => _imageSource; set => SetProperty(ref _imageSource, value); }
|
||||||
|
public string StatusText { get => _statusText; set => SetProperty(ref _statusText, value); }
|
||||||
|
public int ChessboardWidth { get => _chessboardWidth; set => SetProperty(ref _chessboardWidth, value); }
|
||||||
|
public int ChessboardHeight { get => _chessboardHeight; set => SetProperty(ref _chessboardHeight, value); }
|
||||||
|
public float SquareSize { get => _squareSize; set => SetProperty(ref _squareSize, value); }
|
||||||
|
|
||||||
|
public int SelectedImageIndex
|
||||||
|
{
|
||||||
|
get => _selectedImageIndex;
|
||||||
|
set { if (SetProperty(ref _selectedImageIndex, value) && value >= 0) LoadSelectedImage(value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsCalibrating { get => _isCalibrating; set => SetProperty(ref _isCalibrating, value); }
|
||||||
|
public double ProgressValue { get => _progressValue; set => SetProperty(ref _progressValue, value); }
|
||||||
|
public string ProgressText { get => _progressText; set => SetProperty(ref _progressText, value); }
|
||||||
|
|
||||||
|
public DelegateCommand AddImagesCommand { get; }
|
||||||
|
public DelegateCommand ClearImagesCommand { get; }
|
||||||
|
public DelegateCommand CalibrateCommand { get; }
|
||||||
|
public DelegateCommand SaveCalibrationCommand { get; }
|
||||||
|
public DelegateCommand LoadCalibrationCommand { get; }
|
||||||
|
public DelegateCommand UndistortImageCommand { get; }
|
||||||
|
|
||||||
|
private void AddImages()
|
||||||
|
{
|
||||||
|
var fileNames = _dialogService.ShowOpenMultipleFilesDialog("图像文件|*.jpg;*.png;*.bmp;*.tif");
|
||||||
|
if (fileNames == null) return;
|
||||||
|
foreach (var file in fileNames)
|
||||||
|
{
|
||||||
|
_imagePaths.Add(file);
|
||||||
|
ImageFileNames.Add(Path.GetFileName(file));
|
||||||
|
}
|
||||||
|
StatusText = string.Format(Res.ChessboardStatusAdded, _imagePaths.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanClearImages() => ImageFileNames.Count > 0;
|
||||||
|
|
||||||
|
private void ClearImages()
|
||||||
|
{
|
||||||
|
_imagePaths.Clear();
|
||||||
|
ImageFileNames.Clear();
|
||||||
|
ImageSource = null;
|
||||||
|
StatusText = Res.ChessboardStatusCleared;
|
||||||
|
RaiseEvent(ImageClearedRequested);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanCalibrate() => ImageFileNames.Count >= 3 && !IsCalibrating;
|
||||||
|
|
||||||
|
private void OnCalibrationProgressChanged(int current, int total, string message)
|
||||||
|
{
|
||||||
|
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
ProgressValue = (double)current / total * 100;
|
||||||
|
if (message.Contains("检测角点"))
|
||||||
|
{
|
||||||
|
var match = System.Text.RegularExpressions.Regex.Match(message, @"\((\d+)/(\d+)\)");
|
||||||
|
ProgressText = match.Success
|
||||||
|
? string.Format(Res.ChessboardProgressDetecting, match.Groups[1].Value, match.Groups[2].Value)
|
||||||
|
: message;
|
||||||
|
}
|
||||||
|
else if (message.Contains("执行相机标定"))
|
||||||
|
ProgressText = Res.ChessboardProgressCalibrating;
|
||||||
|
else if (message.Contains("计算重投影误差"))
|
||||||
|
{
|
||||||
|
var match = System.Text.RegularExpressions.Regex.Match(message, @"\((\d+)/(\d+)\)");
|
||||||
|
ProgressText = match.Success
|
||||||
|
? string.Format(Res.ChessboardProgressCalculating, match.Groups[1].Value, match.Groups[2].Value)
|
||||||
|
: message;
|
||||||
|
}
|
||||||
|
else if (message.Contains("标定完成"))
|
||||||
|
ProgressText = Res.ChessboardProgressComplete;
|
||||||
|
else if (message.Contains("标定失败"))
|
||||||
|
ProgressText = Res.ChessboardProgressFailed;
|
||||||
|
else
|
||||||
|
ProgressText = message;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async System.Threading.Tasks.Task CalibrateAsync()
|
||||||
|
{
|
||||||
|
if (_imagePaths.Count < 3)
|
||||||
|
{
|
||||||
|
_dialogService.ShowError(Res.ChessboardErrorMinImages, Res.ChessboardCalibrationComplete);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IsCalibrating = true;
|
||||||
|
ProgressValue = 0;
|
||||||
|
ProgressText = Res.ChessboardProgressPreparing;
|
||||||
|
StatusText = Res.ChessboardStatusCalibrating;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await System.Threading.Tasks.Task.Run(() =>
|
||||||
|
{
|
||||||
|
if (_calibrator.CalibrateFromImages(new List<string>(_imagePaths), ChessboardWidth, ChessboardHeight, SquareSize, out string error))
|
||||||
|
{
|
||||||
|
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
var imageErrors = new System.Text.StringBuilder();
|
||||||
|
for (int i = 0; i < _calibrator.PerImageErrors.Count; i++)
|
||||||
|
imageErrors.AppendLine(string.Format(Res.ChessboardImageError, i + 1, _calibrator.PerImageErrors[i]));
|
||||||
|
StatusText = string.Format(Res.ChessboardStatusSuccess, _calibrator.ReprojectionError, imageErrors.ToString());
|
||||||
|
_dialogService.ShowInfo(Res.ChessboardCalibrationComplete, Res.ChessboardSaveSuccess);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
StatusText = string.Format(Res.ChessboardStatusFailed, error);
|
||||||
|
_dialogService.ShowError(error, Res.ChessboardCalibrationComplete);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsCalibrating = false;
|
||||||
|
ProgressValue = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveCalibration()
|
||||||
|
{
|
||||||
|
var fileName = _dialogService.ShowSaveFileDialog("标定文件|*.json", "camera_calibration.json");
|
||||||
|
if (fileName == null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_calibrator.SaveCalibration(fileName);
|
||||||
|
StatusText = string.Format(Res.ChessboardStatusSaved, fileName);
|
||||||
|
_dialogService.ShowInfo(Res.ChessboardSaveSuccess, Res.ChessboardCalibrationComplete);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_dialogService.ShowError($"保存失败: {ex.Message}", Res.ChessboardCalibrationComplete);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadCalibration()
|
||||||
|
{
|
||||||
|
var fileName = _dialogService.ShowOpenFileDialog("标定文件|*.json");
|
||||||
|
if (fileName == null) return;
|
||||||
|
if (_calibrator.LoadCalibration(fileName))
|
||||||
|
{
|
||||||
|
StatusText = string.Format(Res.ChessboardStatusLoaded, fileName);
|
||||||
|
_dialogService.ShowInfo(Res.ChessboardLoadSuccess, Res.ChessboardCalibrationComplete);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_dialogService.ShowError(Res.CalibrationLoadFailed, Res.ChessboardCalibrationComplete);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UndistortImage()
|
||||||
|
{
|
||||||
|
var fileName = _dialogService.ShowOpenFileDialog("图像文件|*.jpg;*.png;*.bmp;*.tif");
|
||||||
|
if (fileName == null) return;
|
||||||
|
using var image = CvInvoke.Imread(fileName);
|
||||||
|
var undistorted = _calibrator.UndistortImage(image);
|
||||||
|
ImageSource = MatToBitmapSource(undistorted);
|
||||||
|
StatusText = string.Format(Res.ChessboardStatusUndistorted, Path.GetFileName(fileName));
|
||||||
|
RaiseEvent(ImageLoadedRequested);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadSelectedImage(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= _imagePaths.Count) return;
|
||||||
|
var img = _calibrator.DrawChessboardCorners(_imagePaths[index], ChessboardWidth, ChessboardHeight);
|
||||||
|
if (img != null)
|
||||||
|
{
|
||||||
|
ImageSource = MatToBitmapSource(img);
|
||||||
|
RaiseEvent(ImageLoadedRequested);
|
||||||
|
}
|
||||||
|
if (_calibrator.PerImageErrors.Count > index)
|
||||||
|
StatusText = string.Format(Res.ChessboardStatusImageError, index + 1, _calibrator.PerImageErrors[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler? ImageLoadedRequested;
|
||||||
|
public event EventHandler? ImageClearedRequested;
|
||||||
|
|
||||||
|
private void RaiseEvent(EventHandler? handler) => handler?.Invoke(this, EventArgs.Empty);
|
||||||
|
|
||||||
|
private static BitmapSource MatToBitmapSource(Mat mat)
|
||||||
|
{
|
||||||
|
using var bitmap = mat.ToBitmap();
|
||||||
|
var hBitmap = bitmap.GetHbitmap();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(
|
||||||
|
hBitmap, IntPtr.Zero, System.Windows.Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
DeleteObject(hBitmap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[System.Runtime.InteropServices.DllImport("gdi32.dll")]
|
||||||
|
private static extern bool DeleteObject(IntPtr hObject);
|
||||||
|
}
|
||||||
+97
-124
@@ -1,6 +1,6 @@
|
|||||||
# XP.Camera 使用说明
|
# XP.Camera 使用说明
|
||||||
|
|
||||||
基于 .NET 8 WPF 的工业相机控制类库,采用工厂模式 + 统一接口设计,支持多品牌相机扩展。当前已实现 Basler pylon SDK 驱动。
|
基于 .NET 8 WPF 的工业相机控制与标定类库,采用工厂模式 + 统一接口设计,支持多品牌相机扩展。当前已实现 Basler pylon SDK 驱动。
|
||||||
|
|
||||||
## 环境要求
|
## 环境要求
|
||||||
|
|
||||||
@@ -12,16 +12,36 @@
|
|||||||
|
|
||||||
```
|
```
|
||||||
XP.Camera/
|
XP.Camera/
|
||||||
├── ICameraController.cs # 控制器接口 + 工厂接口
|
├── Core/ # 相机核心抽象
|
||||||
├── CameraFactory.cs # 统一工厂(根据品牌创建控制器)
|
│ ├── ICameraController.cs # 控制器接口 + 工厂接口
|
||||||
├── BaslerCameraController.cs # Basler 实现
|
│ ├── CameraFactory.cs # 统一工厂(根据品牌创建控制器)
|
||||||
├── CameraModels.cs # CameraInfo、ImageGrabbedEventArgs、GrabErrorEventArgs
|
│ ├── CameraModels.cs # CameraInfo、ImageGrabbedEventArgs、GrabErrorEventArgs
|
||||||
├── CameraExceptions.cs # CameraException、ConnectionLostException、DeviceNotFoundException
|
│ └── CameraExceptions.cs # CameraException、ConnectionLostException、DeviceNotFoundException
|
||||||
├── PixelConverter.cs # 像素数据 → WPF BitmapSource 转换工具
|
├── Basler/ # Basler 品牌实现
|
||||||
|
│ └── BaslerCameraController.cs # Basler pylon SDK 实现
|
||||||
|
├── Converters/ # 数据转换工具
|
||||||
|
│ └── PixelConverter.cs # 像素数据 → WPF BitmapSource 转换
|
||||||
|
├── Calibration/ # 相机标定模块
|
||||||
|
│ ├── CalibrationProcessor.cs # 九点标定(DLT 单应性矩阵,像素→世界坐标)
|
||||||
|
│ ├── ChessboardCalibrator.cs # 棋盘格标定(Zhang's 方法,内参 + 畸变校正)
|
||||||
|
│ ├── IDialogService.cs # ICalibrationDialogService 接口
|
||||||
|
│ ├── DefaultCalibrationDialogService.cs # 默认实现(标准 WPF MessageBox)
|
||||||
|
│ ├── CalibrationLocalizedStrings.cs # XAML 本地化绑定辅助
|
||||||
|
│ ├── Controls/ # 标定 UI 控件(UserControl)
|
||||||
|
│ │ ├── CalibrationControl.xaml/.cs # 九点标定界面
|
||||||
|
│ │ ├── ChessboardCalibrationControl.xaml/.cs # 棋盘格标定界面
|
||||||
|
│ │ └── ImageCanvasControl.xaml/.cs # 图像画布(缩放/平移)
|
||||||
|
│ ├── ViewModels/ # 标定视图模型
|
||||||
|
│ │ ├── CalibrationViewModel.cs
|
||||||
|
│ │ └── ChessboardCalibrationViewModel.cs
|
||||||
|
│ └── Resources/ # 本地化资源
|
||||||
|
│ ├── CalibrationResources.resx # 中文(默认)
|
||||||
|
│ ├── CalibrationResources.en-US.resx # 英文
|
||||||
|
│ └── CalibrationResources.Designer.cs
|
||||||
└── XP.Camera.csproj
|
└── XP.Camera.csproj
|
||||||
```
|
```
|
||||||
|
|
||||||
所有类型统一在 `XP.Camera` 命名空间下。
|
所有相机核心类型在 `XP.Camera` 命名空间下,标定模块在 `XP.Camera.Calibration` 命名空间下。
|
||||||
|
|
||||||
## 项目引用
|
## 项目引用
|
||||||
|
|
||||||
@@ -48,27 +68,12 @@ Console.WriteLine($"已连接: {info.ModelName} (SN: {info.SerialNumber})");
|
|||||||
在 Prism / DI 容器中注册:
|
在 Prism / DI 容器中注册:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// App.xaml.cs
|
|
||||||
var config = AppConfig.Load();
|
var config = AppConfig.Load();
|
||||||
containerRegistry.RegisterSingleton<ICameraFactory, CameraFactory>();
|
containerRegistry.RegisterSingleton<ICameraFactory, CameraFactory>();
|
||||||
containerRegistry.RegisterSingleton<ICameraController>(() =>
|
containerRegistry.RegisterSingleton<ICameraController>(() =>
|
||||||
new CameraFactory().CreateController(config.CameraType));
|
new CameraFactory().CreateController(config.CameraType));
|
||||||
```
|
```
|
||||||
|
|
||||||
ViewModel 中注入使用:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public class MyViewModel
|
|
||||||
{
|
|
||||||
private readonly ICameraController _camera;
|
|
||||||
|
|
||||||
public MyViewModel(ICameraController camera)
|
|
||||||
{
|
|
||||||
_camera = camera;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
相机品牌通过配置文件 `config.json` 指定:
|
相机品牌通过配置文件 `config.json` 指定:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -82,60 +87,85 @@ public class MyViewModel
|
|||||||
```csharp
|
```csharp
|
||||||
_camera.ImageGrabbed += (s, e) =>
|
_camera.ImageGrabbed += (s, e) =>
|
||||||
{
|
{
|
||||||
// PixelConverter 返回已 Freeze 的 BitmapSource,可跨线程传递
|
|
||||||
var bitmap = PixelConverter.ToBitmapSource(
|
var bitmap = PixelConverter.ToBitmapSource(
|
||||||
e.PixelData, e.Width, e.Height, e.PixelFormat);
|
e.PixelData, e.Width, e.Height, e.PixelFormat);
|
||||||
|
Application.Current.Dispatcher.Invoke(() => CameraImageSource = bitmap);
|
||||||
Application.Current.Dispatcher.Invoke(() =>
|
|
||||||
{
|
|
||||||
CameraImageSource = bitmap;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
XAML 绑定:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<Image Source="{Binding CameraImageSource}" Stretch="Uniform" />
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 软件触发采集流程
|
### 4. 软件触发采集流程
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
camera.Open();
|
camera.Open();
|
||||||
camera.SetExposureTime(10000); // 10ms
|
camera.SetExposureTime(10000); // 10ms
|
||||||
camera.StartGrabbing();
|
camera.StartGrabbing();
|
||||||
|
|
||||||
// 每次需要采集时调用(结果通过 ImageGrabbed 事件返回)
|
|
||||||
camera.ExecuteSoftwareTrigger();
|
camera.ExecuteSoftwareTrigger();
|
||||||
|
|
||||||
camera.StopGrabbing();
|
camera.StopGrabbing();
|
||||||
camera.Close();
|
camera.Close();
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5. 实时连续采集(链式触发)
|
### 5. 使用标定模块
|
||||||
|
|
||||||
收到上一帧后立即触发下一帧,自动适配任何帧率:
|
标定模块完全自包含,可独立使用,无需外部依赖。
|
||||||
|
|
||||||
|
#### 棋盘格标定(相机内参 + 畸变校正)
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
private volatile bool _liveViewRunning;
|
using XP.Camera.Calibration;
|
||||||
|
using XP.Camera.Calibration.ViewModels;
|
||||||
|
using XP.Camera.Calibration.Controls;
|
||||||
|
|
||||||
_camera.ImageGrabbed += (s, e) =>
|
// 使用默认对话框服务(标准 WPF MessageBox)
|
||||||
|
var dialogService = new DefaultCalibrationDialogService();
|
||||||
|
var viewModel = new ChessboardCalibrationViewModel(dialogService);
|
||||||
|
|
||||||
|
var window = new Window
|
||||||
{
|
{
|
||||||
var bitmap = PixelConverter.ToBitmapSource(e.PixelData, e.Width, e.Height, e.PixelFormat);
|
Title = "棋盘格标定",
|
||||||
Application.Current.Dispatcher.Invoke(() => CameraImageSource = bitmap);
|
Width = 1600, Height = 900,
|
||||||
|
Content = new ChessboardCalibrationControl { DataContext = viewModel }
|
||||||
if (_liveViewRunning)
|
|
||||||
_camera.ExecuteSoftwareTrigger(); // 链式触发下一帧
|
|
||||||
};
|
};
|
||||||
|
window.ShowDialog();
|
||||||
|
```
|
||||||
|
|
||||||
// 启动实时
|
#### 九点标定(像素→世界坐标)
|
||||||
_camera.StartGrabbing();
|
|
||||||
_liveViewRunning = true;
|
|
||||||
_camera.ExecuteSoftwareTrigger(); // 触发第一帧
|
|
||||||
|
|
||||||
// 停止实时
|
```csharp
|
||||||
_liveViewRunning = false;
|
var dialogService = new DefaultCalibrationDialogService();
|
||||||
|
var viewModel = new CalibrationViewModel(dialogService);
|
||||||
|
|
||||||
|
var window = new Window
|
||||||
|
{
|
||||||
|
Title = "九点标定",
|
||||||
|
Width = 1400, Height = 850,
|
||||||
|
Content = new CalibrationControl { DataContext = viewModel }
|
||||||
|
};
|
||||||
|
window.ShowDialog();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 纯算法调用(不使用 UI)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 棋盘格标定
|
||||||
|
var calibrator = new ChessboardCalibrator();
|
||||||
|
calibrator.CalibrateFromImages(imagePaths, boardWidth: 11, boardHeight: 8, squareSize: 15f, out string error);
|
||||||
|
calibrator.SaveCalibration("camera_calibration.json");
|
||||||
|
|
||||||
|
// 九点标定
|
||||||
|
var processor = new CalibrationProcessor();
|
||||||
|
processor.Calibrate(points);
|
||||||
|
var worldPoint = processor.PixelToWorld(new PointF(100, 200));
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 自定义对话框服务
|
||||||
|
|
||||||
|
如需自定义弹框样式,实现 `ICalibrationDialogService` 接口即可:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class MyDialogService : ICalibrationDialogService
|
||||||
|
{
|
||||||
|
// 实现所有接口方法,使用自定义 UI 组件...
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 核心接口
|
## 核心接口
|
||||||
@@ -149,94 +179,37 @@ _liveViewRunning = false;
|
|||||||
| `StartGrabbing()` | 以软件触发模式启动采集 |
|
| `StartGrabbing()` | 以软件触发模式启动采集 |
|
||||||
| `ExecuteSoftwareTrigger()` | 触发一帧采集 |
|
| `ExecuteSoftwareTrigger()` | 触发一帧采集 |
|
||||||
| `StopGrabbing()` | 停止采集 |
|
| `StopGrabbing()` | 停止采集 |
|
||||||
|
| `Get/SetExposureTime` | 曝光时间(微秒) |
|
||||||
|
| `Get/SetGain` | 增益值 |
|
||||||
|
| `Get/SetWidth/Height` | 图像尺寸 |
|
||||||
|
| `Get/SetPixelFormat` | 像素格式(Mono8 / BGR8 / BGRA8) |
|
||||||
|
|
||||||
### 参数读写
|
### 事件
|
||||||
|
|
||||||
| 方法 | 说明 |
|
| 事件 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `Get/SetExposureTime(double)` | 曝光时间(微秒) |
|
| `ImageGrabbed` | 成功采集一帧图像 |
|
||||||
| `Get/SetGain(double)` | 增益值 |
|
| `GrabError` | 图像采集失败 |
|
||||||
| `Get/SetWidth(int)` | 图像宽度(自动校正到有效值) |
|
| `ConnectionLost` | 相机连接意外断开 |
|
||||||
| `Get/SetHeight(int)` | 图像高度(自动校正到有效值) |
|
|
||||||
| `Get/SetPixelFormat(string)` | 像素格式(Mono8 / BGR8 / BGRA8) |
|
|
||||||
|
|
||||||
### ICameraFactory
|
> 所有事件均在非 UI 线程触发,更新 WPF 界面时需通过 `Dispatcher.Invoke` 调度。
|
||||||
|
|
||||||
| 方法 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| `CreateController(string cameraType)` | 根据品牌名创建控制器 |
|
|
||||||
|
|
||||||
当前支持的 `cameraType` 值:`"Basler"`
|
|
||||||
|
|
||||||
## 事件
|
|
||||||
|
|
||||||
| 事件 | 说明 | 触发线程 |
|
|
||||||
|------|------|----------|
|
|
||||||
| `ImageGrabbed` | 成功采集一帧图像 | StreamGrabber 回调线程 |
|
|
||||||
| `GrabError` | 图像采集失败 | StreamGrabber 回调线程 |
|
|
||||||
| `ConnectionLost` | 相机连接意外断开 | pylon SDK 事件线程 |
|
|
||||||
|
|
||||||
> 所有事件均在非 UI 线程触发。更新 WPF 界面时需通过 `Dispatcher.Invoke` 调度。
|
|
||||||
> `PixelConverter.ToBitmapSource()` 返回的 BitmapSource 已调用 `Freeze()`,可直接跨线程传递。
|
|
||||||
|
|
||||||
## 异常处理
|
## 异常处理
|
||||||
|
|
||||||
```csharp
|
|
||||||
try
|
|
||||||
{
|
|
||||||
camera.Open();
|
|
||||||
}
|
|
||||||
catch (DeviceNotFoundException)
|
|
||||||
{
|
|
||||||
// 无可用相机设备
|
|
||||||
}
|
|
||||||
catch (CameraException ex)
|
|
||||||
{
|
|
||||||
// 其他相机错误,ex.InnerException 包含原始 SDK 异常
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| 异常类型 | 场景 |
|
| 异常类型 | 场景 |
|
||||||
|---------|------|
|
|---------|------|
|
||||||
| `DeviceNotFoundException` | 无可用相机 |
|
| `DeviceNotFoundException` | 无可用相机 |
|
||||||
| `ConnectionLostException` | 相机物理断开 |
|
| `ConnectionLostException` | 相机物理断开 |
|
||||||
| `CameraException` | SDK 操作失败(基类) |
|
| `CameraException` | SDK 操作失败(基类) |
|
||||||
| `InvalidOperationException` | 未连接时访问参数,未采集时触发 |
|
|
||||||
| `TimeoutException` | 软件触发等待超时 |
|
|
||||||
|
|
||||||
## 扩展其他品牌相机
|
## 扩展其他品牌相机
|
||||||
|
|
||||||
1. 实现 `ICameraController` 接口:
|
1. 在 `Basler/` 同级创建新文件夹,实现 `ICameraController` 接口
|
||||||
|
2. 在 `CameraFactory.cs` 中注册新品牌
|
||||||
```csharp
|
3. 配置文件切换品牌即可,业务代码无需修改
|
||||||
public class HikvisionCameraController : ICameraController
|
|
||||||
{
|
|
||||||
// 实现所有接口方法...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 在 `CameraFactory.cs` 中注册:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public ICameraController CreateController(string cameraType)
|
|
||||||
{
|
|
||||||
return cameraType switch
|
|
||||||
{
|
|
||||||
"Basler" => new BaslerCameraController(),
|
|
||||||
"Hikvision" => new HikvisionCameraController(),
|
|
||||||
_ => throw new NotSupportedException($"不支持的相机品牌: {cameraType}")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. 配置文件切换品牌即可,业务代码无需修改。
|
|
||||||
|
|
||||||
## 线程安全
|
## 线程安全
|
||||||
|
|
||||||
- 所有公共方法(Open / Close / StartGrabbing / StopGrabbing / ExecuteSoftwareTrigger / 参数读写)均线程安全
|
- 所有公共方法均线程安全
|
||||||
- 事件回调不持有内部锁,不会导致死锁
|
- 事件回调不持有内部锁,不会导致死锁
|
||||||
- `Open()` / `Close()` 幂等,重复调用安全
|
- `Open()` / `Close()` 幂等,重复调用安全
|
||||||
|
|
||||||
## 日志
|
|
||||||
|
|
||||||
使用 Serilog 静态 API(`Log.ForContext<T>()`),与宿主应用共享同一个日志管道。宿主应用只需在启动时配置 `Log.Logger` 即可。
|
|
||||||
|
|||||||
@@ -13,7 +13,24 @@
|
|||||||
<Reference Include="Basler.Pylon">
|
<Reference Include="Basler.Pylon">
|
||||||
<HintPath>..\ExternalLibraries\Basler.Pylon.dll</HintPath>
|
<HintPath>..\ExternalLibraries\Basler.Pylon.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
|
<PackageReference Include="Emgu.CV" Version="4.10.0.5680" />
|
||||||
|
<PackageReference Include="Emgu.CV.runtime.windows" Version="4.10.0.5680" />
|
||||||
|
<PackageReference Include="Emgu.CV.Bitmap" Version="4.10.0.5680" />
|
||||||
|
<PackageReference Include="MahApps.Metro.IconPacks" Version="6.2.1" />
|
||||||
|
<PackageReference Include="Prism.DryIoc" Version="9.0.537" />
|
||||||
|
<PackageReference Include="Prism.Wpf" Version="9.0.537" />
|
||||||
<PackageReference Include="Serilog" Version="4.3.1" />
|
<PackageReference Include="Serilog" Version="4.3.1" />
|
||||||
|
<ProjectReference Include="..\XP.Common\XP.Common.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Update="Calibration\Resources\CalibrationResources.resx">
|
||||||
|
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||||
|
<LastGenOutput>CalibrationResources.Designer.cs</LastGenOutput>
|
||||||
|
</EmbeddedResource>
|
||||||
|
<EmbeddedResource Update="Calibration\Resources\CalibrationResources.en-US.resx">
|
||||||
|
<DependentUpon>CalibrationResources.resx</DependentUpon>
|
||||||
|
</EmbeddedResource>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
<Window x:Class="XP.Common.GeneralForm.Views.HexMessageBox"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Height="240" Width="400"
|
||||||
|
MinHeight="240" MinWidth="400"
|
||||||
|
MaxHeight="240" MaxWidth="400"
|
||||||
|
WindowStartupLocation="Manual"
|
||||||
|
Topmost="True"
|
||||||
|
WindowStyle="None"
|
||||||
|
x:Name="csdMessageBox"
|
||||||
|
Background="White">
|
||||||
|
|
||||||
|
<Window.Resources>
|
||||||
|
<Style x:Key="MenuButton" TargetType="Button">
|
||||||
|
<Setter Property="MinWidth" Value="80" />
|
||||||
|
<Setter Property="Height" Value="30" />
|
||||||
|
<Setter Property="Background" Value="#0078D7" />
|
||||||
|
<Setter Property="Foreground" Value="White" />
|
||||||
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
|
<Setter Property="FontSize" Value="14" />
|
||||||
|
<Setter Property="Cursor" Value="Hand" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border Background="{TemplateBinding Background}" CornerRadius="2" Padding="10,5">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter Property="Background" Value="#005A9E" />
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter Property="Background" Value="#004275" />
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
</Window.Resources>
|
||||||
|
|
||||||
|
<Border BorderBrush="#CCCCCC" BorderThickness="1">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Grid Grid.Row="0" Margin="10">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<Image Grid.Column="0" Source="{Binding Icon, ElementName=csdMessageBox}"
|
||||||
|
Width="34" Height="33" Margin="0,0,10,0" VerticalAlignment="Top" />
|
||||||
|
|
||||||
|
<ScrollViewer Grid.Column="1" VerticalScrollBarVisibility="Auto" MaxHeight="140">
|
||||||
|
<TextBlock Text="{Binding MessageText, ElementName=csdMessageBox}"
|
||||||
|
TextWrapping="Wrap" FontSize="14" VerticalAlignment="Center" />
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Rectangle Grid.Row="1" Fill="#CCCCCC" Height="1" />
|
||||||
|
|
||||||
|
<Grid Grid.Row="2" Margin="10">
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
|
<StackPanel.Style>
|
||||||
|
<Style TargetType="StackPanel">
|
||||||
|
<Setter Property="Visibility" Value="Collapsed" />
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding MessageBoxButton, ElementName=csdMessageBox}" Value="{x:Static MessageBoxButton.YesNo}">
|
||||||
|
<Setter Property="Visibility" Value="Visible" />
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</StackPanel.Style>
|
||||||
|
<Button Content="Yes" Style="{StaticResource MenuButton}" Margin="0,0,10,0" Click="Yes_Click" />
|
||||||
|
<Button Content="No" Style="{StaticResource MenuButton}" Click="No_Click" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
|
<StackPanel.Style>
|
||||||
|
<Style TargetType="StackPanel">
|
||||||
|
<Setter Property="Visibility" Value="Collapsed" />
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding MessageBoxButton, ElementName=csdMessageBox}" Value="{x:Static MessageBoxButton.OKCancel}">
|
||||||
|
<Setter Property="Visibility" Value="Visible" />
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</StackPanel.Style>
|
||||||
|
<Button Content="OK" Style="{StaticResource MenuButton}" Margin="0,0,10,0" Click="OK_Click" />
|
||||||
|
<Button Content="Cancel" Style="{StaticResource MenuButton}" Click="Cancel_Click" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel HorizontalAlignment="Right">
|
||||||
|
<StackPanel.Style>
|
||||||
|
<Style TargetType="StackPanel">
|
||||||
|
<Setter Property="Visibility" Value="Collapsed" />
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding MessageBoxButton, ElementName=csdMessageBox}" Value="{x:Static MessageBoxButton.OK}">
|
||||||
|
<Setter Property="Visibility" Value="Visible" />
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</StackPanel.Style>
|
||||||
|
<Button Content="OK" Style="{StaticResource MenuButton}" Click="OK_Click" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
using System;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Interop;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
|
||||||
|
namespace XP.Common.GeneralForm.Views;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 自定义消息框,可替代标准 MessageBox
|
||||||
|
/// </summary>
|
||||||
|
public partial class HexMessageBox : Window
|
||||||
|
{
|
||||||
|
public MessageBoxResult Result { get; set; }
|
||||||
|
|
||||||
|
public string MessageText
|
||||||
|
{
|
||||||
|
get => (string)GetValue(MessageTextProperty);
|
||||||
|
set => SetValue(MessageTextProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly DependencyProperty MessageTextProperty =
|
||||||
|
DependencyProperty.Register("MessageText", typeof(string), typeof(HexMessageBox), new PropertyMetadata(""));
|
||||||
|
|
||||||
|
public new ImageSource Icon
|
||||||
|
{
|
||||||
|
get => (ImageSource)GetValue(IconProperty);
|
||||||
|
set => SetValue(IconProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public new static readonly DependencyProperty IconProperty =
|
||||||
|
DependencyProperty.Register("Icon", typeof(ImageSource), typeof(HexMessageBox), new PropertyMetadata(null));
|
||||||
|
|
||||||
|
public MessageBoxButton MessageBoxButton
|
||||||
|
{
|
||||||
|
get => (MessageBoxButton)GetValue(MessageBoxButtonProperty);
|
||||||
|
set => SetValue(MessageBoxButtonProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly DependencyProperty MessageBoxButtonProperty =
|
||||||
|
DependencyProperty.Register("MessageBoxButton", typeof(MessageBoxButton), typeof(HexMessageBox), new PropertyMetadata(MessageBoxButton.OK));
|
||||||
|
|
||||||
|
public HexMessageBox(string messageText, MessageBoxButton messageBoxButton, MessageBoxImage image)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
Left = (SystemParameters.PrimaryScreenWidth - Width) / 2;
|
||||||
|
Top = (SystemParameters.PrimaryScreenHeight - Height) / 2;
|
||||||
|
|
||||||
|
MessageText = messageText;
|
||||||
|
IntPtr iconHandle = SystemIcons.Information.Handle;
|
||||||
|
|
||||||
|
switch (image)
|
||||||
|
{
|
||||||
|
case MessageBoxImage.Error:
|
||||||
|
iconHandle = SystemIcons.Error.Handle;
|
||||||
|
break;
|
||||||
|
case MessageBoxImage.Question:
|
||||||
|
iconHandle = SystemIcons.Question.Handle;
|
||||||
|
break;
|
||||||
|
case MessageBoxImage.Warning:
|
||||||
|
iconHandle = SystemIcons.Warning.Handle;
|
||||||
|
break;
|
||||||
|
case MessageBoxImage.Information:
|
||||||
|
iconHandle = SystemIcons.Information.Handle;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Icon = Imaging.CreateBitmapSourceFromHIcon(iconHandle, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
|
||||||
|
Icon.Freeze();
|
||||||
|
|
||||||
|
MessageBoxButton = messageBoxButton;
|
||||||
|
Result = MessageBoxResult.None;
|
||||||
|
Topmost = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 显示模态对话框并返回结果
|
||||||
|
/// </summary>
|
||||||
|
public static MessageBoxResult Show(string messageText, MessageBoxButton messageBoxButton, MessageBoxImage image)
|
||||||
|
{
|
||||||
|
return Application.Current.Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
var window = new HexMessageBox(messageText, messageBoxButton, image);
|
||||||
|
window.ShowDialog();
|
||||||
|
return window.Result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 显示非模态消息
|
||||||
|
/// </summary>
|
||||||
|
public static HexMessageBox ShowMessage(string messageText, MessageBoxImage image)
|
||||||
|
{
|
||||||
|
HexMessageBox? messageBox = null;
|
||||||
|
Application.Current.Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
messageBox = new HexMessageBox(messageText, MessageBoxButton.OK, image);
|
||||||
|
messageBox.Show();
|
||||||
|
});
|
||||||
|
return messageBox!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Yes_Click(object sender, RoutedEventArgs e) { Result = MessageBoxResult.Yes; Close(); }
|
||||||
|
private void No_Click(object sender, RoutedEventArgs e) { Result = MessageBoxResult.No; Close(); }
|
||||||
|
private void OK_Click(object sender, RoutedEventArgs e) { Result = MessageBoxResult.OK; Close(); }
|
||||||
|
private void Cancel_Click(object sender, RoutedEventArgs e) { Result = MessageBoxResult.Cancel; Close(); }
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Configuration;
|
using System.Configuration;
|
||||||
using XP.Common.Configs;
|
using XP.Common.Configs;
|
||||||
using XP.Common.Dump.Configs;
|
using XP.Common.Dump.Configs;
|
||||||
|
|
||||||
|
|||||||
@@ -1878,7 +1878,10 @@
|
|||||||
"Telerik.UI.for.Wpf.NetCore.Xaml": "2024.1.408"
|
"Telerik.UI.for.Wpf.NetCore.Xaml": "2024.1.408"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"XP.Common.dll": {}
|
"XP.Common.dll": {
|
||||||
|
"assemblyVersion": "1.4.16.1",
|
||||||
|
"fileVersion": "1.4.16.1"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"en-US/XP.Common.resources.dll": {
|
"en-US/XP.Common.resources.dll": {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Prism.Mvvm;
|
using Prism.Mvvm;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Prism.Ioc;
|
using Prism.Ioc;
|
||||||
using Prism.Modularity;
|
using Prism.Modularity;
|
||||||
using System.Resources;
|
using System.Resources;
|
||||||
using XP.Common.Localization;
|
using XP.Common.Localization;
|
||||||
|
|||||||
@@ -1,340 +0,0 @@
|
|||||||
using Moq;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using XP.Common.Configs;
|
|
||||||
using XP.Common.Database.Implementations;
|
|
||||||
using XP.Common.Database.Interfaces;
|
|
||||||
using XP.Common.Logging.Interfaces;
|
|
||||||
using XplorePlane.Models;
|
|
||||||
using XplorePlane.Services.InspectionResults;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace XplorePlane.Tests.Services
|
|
||||||
{
|
|
||||||
public class InspectionResultStoreTests : IDisposable
|
|
||||||
{
|
|
||||||
private readonly string _tempRoot;
|
|
||||||
private readonly Mock<ILoggerService> _mockLogger;
|
|
||||||
private readonly IDbContext _dbContext;
|
|
||||||
private readonly InspectionResultStore _store;
|
|
||||||
|
|
||||||
public InspectionResultStoreTests()
|
|
||||||
{
|
|
||||||
_tempRoot = Path.Combine(Path.GetTempPath(), "XplorePlane.Tests", Guid.NewGuid().ToString("N"));
|
|
||||||
Directory.CreateDirectory(_tempRoot);
|
|
||||||
|
|
||||||
_mockLogger = new Mock<ILoggerService>();
|
|
||||||
_mockLogger.Setup(l => l.ForModule(It.IsAny<string>())).Returns(_mockLogger.Object);
|
|
||||||
_mockLogger.Setup(l => l.ForModule<InspectionResultStore>()).Returns(_mockLogger.Object);
|
|
||||||
|
|
||||||
var sqliteConfig = new SqliteConfig
|
|
||||||
{
|
|
||||||
DbFilePath = Path.Combine(_tempRoot, "inspection-results.db"),
|
|
||||||
CreateIfNotExists = true,
|
|
||||||
EnableWalMode = false,
|
|
||||||
EnableSqlLogging = false
|
|
||||||
};
|
|
||||||
|
|
||||||
_dbContext = new SqliteContext(sqliteConfig, _mockLogger.Object);
|
|
||||||
_store = new InspectionResultStore(_dbContext, _mockLogger.Object, Path.Combine(_tempRoot, "assets"));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task FullRun_WithTwoNodes_CanRoundTripDetailAndQuery()
|
|
||||||
{
|
|
||||||
var startedAt = new DateTime(2026, 4, 21, 10, 0, 0, DateTimeKind.Utc);
|
|
||||||
var run = new InspectionRunRecord
|
|
||||||
{
|
|
||||||
ProgramName = "NewCncProgram",
|
|
||||||
WorkpieceId = "QFN_1",
|
|
||||||
SerialNumber = "SN-001",
|
|
||||||
StartedAt = startedAt
|
|
||||||
};
|
|
||||||
|
|
||||||
var runSource = CreateTempFile("run-source.bmp", "run-source");
|
|
||||||
await _store.BeginRunAsync(run, new InspectionAssetWriteRequest
|
|
||||||
{
|
|
||||||
AssetType = InspectionAssetType.RunSourceImage,
|
|
||||||
SourceFilePath = runSource,
|
|
||||||
FileFormat = "bmp"
|
|
||||||
});
|
|
||||||
|
|
||||||
var pipelineA = BuildPipeline("Recipe-A", ("GaussianBlur", 0), ("Threshold", 1));
|
|
||||||
var node1Id = Guid.NewGuid();
|
|
||||||
await _store.AppendNodeResultAsync(
|
|
||||||
new InspectionNodeResult
|
|
||||||
{
|
|
||||||
RunId = run.RunId,
|
|
||||||
NodeId = node1Id,
|
|
||||||
NodeIndex = 1,
|
|
||||||
NodeName = "检测节点1",
|
|
||||||
PipelineId = pipelineA.Id,
|
|
||||||
PipelineName = pipelineA.Name,
|
|
||||||
NodePass = true,
|
|
||||||
DurationMs = 135
|
|
||||||
},
|
|
||||||
new[]
|
|
||||||
{
|
|
||||||
new InspectionMetricResult
|
|
||||||
{
|
|
||||||
MetricKey = "bridge.rate",
|
|
||||||
MetricName = "Bridge Rate",
|
|
||||||
MetricValue = 0.12,
|
|
||||||
Unit = "%",
|
|
||||||
UpperLimit = 0.2,
|
|
||||||
IsPass = true,
|
|
||||||
DisplayOrder = 1
|
|
||||||
},
|
|
||||||
new InspectionMetricResult
|
|
||||||
{
|
|
||||||
MetricKey = "void.area",
|
|
||||||
MetricName = "Void Area",
|
|
||||||
MetricValue = 5.6,
|
|
||||||
Unit = "px",
|
|
||||||
UpperLimit = 8,
|
|
||||||
IsPass = true,
|
|
||||||
DisplayOrder = 2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new PipelineExecutionSnapshot
|
|
||||||
{
|
|
||||||
PipelineName = pipelineA.Name,
|
|
||||||
PipelineDefinitionJson = JsonSerializer.Serialize(pipelineA)
|
|
||||||
},
|
|
||||||
new[]
|
|
||||||
{
|
|
||||||
new InspectionAssetWriteRequest
|
|
||||||
{
|
|
||||||
AssetType = InspectionAssetType.NodeInputImage,
|
|
||||||
SourceFilePath = CreateTempFile("node1-input.bmp", "node1-input"),
|
|
||||||
FileFormat = "bmp"
|
|
||||||
},
|
|
||||||
new InspectionAssetWriteRequest
|
|
||||||
{
|
|
||||||
AssetType = InspectionAssetType.NodeResultImage,
|
|
||||||
SourceFilePath = CreateTempFile("node1-result.bmp", "node1-result"),
|
|
||||||
FileFormat = "bmp"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var pipelineB = BuildPipeline("Recipe-B", ("MeanFilter", 0), ("ContourDetection", 1));
|
|
||||||
var node2Id = Guid.NewGuid();
|
|
||||||
await _store.AppendNodeResultAsync(
|
|
||||||
new InspectionNodeResult
|
|
||||||
{
|
|
||||||
RunId = run.RunId,
|
|
||||||
NodeId = node2Id,
|
|
||||||
NodeIndex = 2,
|
|
||||||
NodeName = "检测节点2",
|
|
||||||
PipelineId = pipelineB.Id,
|
|
||||||
PipelineName = pipelineB.Name,
|
|
||||||
NodePass = false,
|
|
||||||
Status = InspectionNodeStatus.Failed,
|
|
||||||
DurationMs = 240
|
|
||||||
},
|
|
||||||
new[]
|
|
||||||
{
|
|
||||||
new InspectionMetricResult
|
|
||||||
{
|
|
||||||
MetricKey = "solder.height",
|
|
||||||
MetricName = "Solder Height",
|
|
||||||
MetricValue = 1.7,
|
|
||||||
Unit = "mm",
|
|
||||||
LowerLimit = 1.8,
|
|
||||||
IsPass = false,
|
|
||||||
DisplayOrder = 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new PipelineExecutionSnapshot
|
|
||||||
{
|
|
||||||
PipelineName = pipelineB.Name,
|
|
||||||
PipelineDefinitionJson = JsonSerializer.Serialize(pipelineB)
|
|
||||||
},
|
|
||||||
new[]
|
|
||||||
{
|
|
||||||
new InspectionAssetWriteRequest
|
|
||||||
{
|
|
||||||
AssetType = InspectionAssetType.NodeResultImage,
|
|
||||||
SourceFilePath = CreateTempFile("node2-result.bmp", "node2-result"),
|
|
||||||
FileFormat = "bmp"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await _store.CompleteRunAsync(run.RunId);
|
|
||||||
|
|
||||||
var queried = await _store.QueryRunsAsync(new InspectionRunQuery
|
|
||||||
{
|
|
||||||
ProgramName = "NewCncProgram",
|
|
||||||
WorkpieceId = "QFN_1",
|
|
||||||
PipelineName = "Recipe-A"
|
|
||||||
});
|
|
||||||
|
|
||||||
var detail = await _store.GetRunDetailAsync(run.RunId);
|
|
||||||
|
|
||||||
Assert.Single(queried);
|
|
||||||
Assert.Equal(run.RunId, queried[0].RunId);
|
|
||||||
Assert.False(detail.Run.OverallPass);
|
|
||||||
Assert.Equal(2, detail.Run.NodeCount);
|
|
||||||
Assert.Equal(2, detail.Nodes.Count);
|
|
||||||
Assert.Equal(3, detail.Metrics.Count);
|
|
||||||
Assert.Equal(4, detail.Assets.Count);
|
|
||||||
Assert.Equal(2, detail.PipelineSnapshots.Count);
|
|
||||||
Assert.Contains(detail.Nodes, n => n.NodeId == node1Id && n.NodePass);
|
|
||||||
Assert.Contains(detail.Nodes, n => n.NodeId == node2Id && !n.NodePass);
|
|
||||||
Assert.All(detail.PipelineSnapshots, snapshot => Assert.False(string.IsNullOrWhiteSpace(snapshot.PipelineHash)));
|
|
||||||
|
|
||||||
var manifestPath = Path.Combine(_tempRoot, "assets", detail.Run.ResultRootPath.Replace('/', Path.DirectorySeparatorChar), "manifest.json");
|
|
||||||
Assert.True(File.Exists(manifestPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task AppendNodeResult_MissingAsset_DoesNotCrashAndMarksAssetMissing()
|
|
||||||
{
|
|
||||||
var run = new InspectionRunRecord
|
|
||||||
{
|
|
||||||
ProgramName = "Program-A",
|
|
||||||
WorkpieceId = "Part-01",
|
|
||||||
SerialNumber = "SN-404"
|
|
||||||
};
|
|
||||||
|
|
||||||
await _store.BeginRunAsync(run);
|
|
||||||
|
|
||||||
var nodeId = Guid.NewGuid();
|
|
||||||
await _store.AppendNodeResultAsync(
|
|
||||||
new InspectionNodeResult
|
|
||||||
{
|
|
||||||
RunId = run.RunId,
|
|
||||||
NodeId = nodeId,
|
|
||||||
NodeIndex = 1,
|
|
||||||
NodeName = "缺图节点",
|
|
||||||
PipelineId = Guid.NewGuid(),
|
|
||||||
PipelineName = "Recipe-Missing",
|
|
||||||
NodePass = true
|
|
||||||
},
|
|
||||||
new[]
|
|
||||||
{
|
|
||||||
new InspectionMetricResult
|
|
||||||
{
|
|
||||||
MetricKey = "metric.only",
|
|
||||||
MetricName = "Metric Only",
|
|
||||||
MetricValue = 1,
|
|
||||||
Unit = "pcs",
|
|
||||||
IsPass = true,
|
|
||||||
DisplayOrder = 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new PipelineExecutionSnapshot
|
|
||||||
{
|
|
||||||
PipelineName = "Recipe-Missing",
|
|
||||||
PipelineDefinitionJson = "{\"nodes\":[\"gaussian\"]}"
|
|
||||||
},
|
|
||||||
new[]
|
|
||||||
{
|
|
||||||
new InspectionAssetWriteRequest
|
|
||||||
{
|
|
||||||
AssetType = InspectionAssetType.NodeResultImage,
|
|
||||||
SourceFilePath = Path.Combine(_tempRoot, "missing-file.bmp"),
|
|
||||||
FileFormat = "bmp"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var detail = await _store.GetRunDetailAsync(run.RunId);
|
|
||||||
var node = Assert.Single(detail.Nodes);
|
|
||||||
|
|
||||||
Assert.Equal(InspectionNodeStatus.AssetMissing, node.Status);
|
|
||||||
Assert.Single(detail.Metrics);
|
|
||||||
Assert.Empty(detail.Assets);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task PipelineSnapshot_IsStoredAsExecutionSnapshot_NotDependentOnLaterChanges()
|
|
||||||
{
|
|
||||||
var run = new InspectionRunRecord
|
|
||||||
{
|
|
||||||
ProgramName = "Program-Snapshot",
|
|
||||||
WorkpieceId = "Part-02",
|
|
||||||
SerialNumber = "SN-SNAP"
|
|
||||||
};
|
|
||||||
|
|
||||||
await _store.BeginRunAsync(run);
|
|
||||||
|
|
||||||
var pipeline = BuildPipeline("Recipe-Snapshot", ("GaussianBlur", 0), ("ContourDetection", 1));
|
|
||||||
var snapshotJson = JsonSerializer.Serialize(pipeline);
|
|
||||||
var originalHash = ComputeExpectedHash(snapshotJson);
|
|
||||||
|
|
||||||
await _store.AppendNodeResultAsync(
|
|
||||||
new InspectionNodeResult
|
|
||||||
{
|
|
||||||
RunId = run.RunId,
|
|
||||||
NodeId = Guid.NewGuid(),
|
|
||||||
NodeIndex = 1,
|
|
||||||
NodeName = "快照节点",
|
|
||||||
PipelineId = pipeline.Id,
|
|
||||||
PipelineName = pipeline.Name,
|
|
||||||
NodePass = true
|
|
||||||
},
|
|
||||||
pipelineSnapshot: new PipelineExecutionSnapshot
|
|
||||||
{
|
|
||||||
PipelineName = pipeline.Name,
|
|
||||||
PipelineDefinitionJson = snapshotJson
|
|
||||||
});
|
|
||||||
|
|
||||||
pipeline.Name = "Recipe-Snapshot-Changed";
|
|
||||||
pipeline.Nodes[0].OperatorKey = "MeanFilter";
|
|
||||||
|
|
||||||
var detail = await _store.GetRunDetailAsync(run.RunId);
|
|
||||||
var snapshot = Assert.Single(detail.PipelineSnapshots);
|
|
||||||
|
|
||||||
Assert.Equal("Recipe-Snapshot", snapshot.PipelineName);
|
|
||||||
Assert.Equal(snapshotJson, snapshot.PipelineDefinitionJson);
|
|
||||||
Assert.Equal(originalHash, snapshot.PipelineHash);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_dbContext.Dispose();
|
|
||||||
if (Directory.Exists(_tempRoot))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.Delete(_tempRoot, true);
|
|
||||||
}
|
|
||||||
catch (IOException)
|
|
||||||
{
|
|
||||||
// SQLite file handles may release slightly after test teardown.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string CreateTempFile(string fileName, string content)
|
|
||||||
{
|
|
||||||
var path = Path.Combine(_tempRoot, fileName);
|
|
||||||
File.WriteAllText(path, content);
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static PipelineModel BuildPipeline(string name, params (string OperatorKey, int Order)[] nodes)
|
|
||||||
{
|
|
||||||
return new PipelineModel
|
|
||||||
{
|
|
||||||
Name = name,
|
|
||||||
Nodes = nodes.Select(node => new PipelineNodeModel
|
|
||||||
{
|
|
||||||
OperatorKey = node.OperatorKey,
|
|
||||||
Order = node.Order
|
|
||||||
}).ToList()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ComputeExpectedHash(string value)
|
|
||||||
{
|
|
||||||
using var sha = System.Security.Cryptography.SHA256.Create();
|
|
||||||
var bytes = System.Text.Encoding.UTF8.GetBytes(value);
|
|
||||||
return Convert.ToHexString(sha.ComputeHash(bytes));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -34,4 +34,11 @@
|
|||||||
<ProjectReference Include="..\XplorePlane\XplorePlane.csproj" />
|
<ProjectReference Include="..\XplorePlane\XplorePlane.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="ImageProcessing.Core">
|
||||||
|
<HintPath>..\XplorePlane\Libs\ImageProcessing\ImageProcessing.Core.dll</HintPath>
|
||||||
|
<Private>True</Private>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
+74
-11
@@ -13,6 +13,7 @@ using XP.Camera;
|
|||||||
using XP.Common.Configs;
|
using XP.Common.Configs;
|
||||||
using XP.Common.Database.Implementations;
|
using XP.Common.Database.Implementations;
|
||||||
using XP.Common.Database.Interfaces;
|
using XP.Common.Database.Interfaces;
|
||||||
|
using XP.Common.GeneralForm.Views;
|
||||||
using XP.Common.Dump.Configs;
|
using XP.Common.Dump.Configs;
|
||||||
using XP.Common.Dump.Implementations;
|
using XP.Common.Dump.Implementations;
|
||||||
using XP.Common.Dump.Interfaces;
|
using XP.Common.Dump.Interfaces;
|
||||||
@@ -36,7 +37,6 @@ using XplorePlane.Services.Camera;
|
|||||||
using XplorePlane.Services.Cnc;
|
using XplorePlane.Services.Cnc;
|
||||||
using XplorePlane.Services.Matrix;
|
using XplorePlane.Services.Matrix;
|
||||||
using XplorePlane.Services.Measurement;
|
using XplorePlane.Services.Measurement;
|
||||||
using XplorePlane.Services.InspectionResults;
|
|
||||||
using XplorePlane.Services.Recipe;
|
using XplorePlane.Services.Recipe;
|
||||||
using XplorePlane.ViewModels;
|
using XplorePlane.ViewModels;
|
||||||
using XplorePlane.ViewModels.Cnc;
|
using XplorePlane.ViewModels.Cnc;
|
||||||
@@ -150,7 +150,23 @@ namespace XplorePlane
|
|||||||
Log.Error(ex, "射线源资源释放失败");
|
Log.Error(ex, "射线源资源释放失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 释放相机服务资源
|
// 先停止导航相机实时采集,再释放资源,避免回调死锁
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var bootstrapper = AppBootstrapper.Instance;
|
||||||
|
if (bootstrapper != null)
|
||||||
|
{
|
||||||
|
var cameraVm = bootstrapper.Container.Resolve<NavigationPropertyPanelViewModel>();
|
||||||
|
cameraVm?.Dispose();
|
||||||
|
Log.Information("导航相机 ViewModel 已释放");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "导航相机 ViewModel 释放失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 释放导航相机服务资源
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var bootstrapper = AppBootstrapper.Instance;
|
var bootstrapper = AppBootstrapper.Instance;
|
||||||
@@ -158,12 +174,12 @@ namespace XplorePlane
|
|||||||
{
|
{
|
||||||
var cameraService = bootstrapper.Container.Resolve<ICameraService>();
|
var cameraService = bootstrapper.Container.Resolve<ICameraService>();
|
||||||
cameraService?.Dispose();
|
cameraService?.Dispose();
|
||||||
Log.Information("相机服务资源已释放");
|
Log.Information("导航相机服务资源已释放");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Error(ex, "相机服务资源释放失败");
|
Log.Error(ex, "导航相机服务资源释放失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 释放SQLite数据库资源 | Release SQLite database resources
|
// 释放SQLite数据库资源 | Release SQLite database resources
|
||||||
@@ -232,19 +248,67 @@ namespace XplorePlane
|
|||||||
|
|
||||||
private bool _modulesInitialized = false;
|
private bool _modulesInitialized = false;
|
||||||
|
|
||||||
|
private string? _cameraError;
|
||||||
|
|
||||||
protected override Window CreateShell()
|
protected override Window CreateShell()
|
||||||
{
|
{
|
||||||
// 提前初始化模块,确保硬件服务在 MainWindow XAML 解析前已注册
|
// 提前初始化模块,确保硬件服务在 MainWindow XAML 解析前已注册
|
||||||
// 默认 Prism 顺序是 CreateShell → InitializeModules,
|
|
||||||
// 但 MainWindow 中嵌入的硬件控件会在 XAML 解析时触发 ViewModelLocator,
|
|
||||||
// 此时模块尚未加载,导致依赖解析失败
|
|
||||||
if (!_modulesInitialized)
|
if (!_modulesInitialized)
|
||||||
{
|
{
|
||||||
base.InitializeModules();
|
base.InitializeModules();
|
||||||
_modulesInitialized = true;
|
_modulesInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Container.Resolve<MainWindow>();
|
var shell = Container.Resolve<MainWindow>();
|
||||||
|
|
||||||
|
// 主窗口加载完成后再连接相机,确保所有模块和原生 DLL 已完成初始化
|
||||||
|
shell.Loaded += (s, e) =>
|
||||||
|
{
|
||||||
|
TryConnectCamera();
|
||||||
|
|
||||||
|
// 通知 ViewModel 相机状态已确定,启动实时预览或显示错误
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cameraVm = Container.Resolve<NavigationPropertyPanelViewModel>();
|
||||||
|
cameraVm.OnCameraReady();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "通知相机 ViewModel 失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_cameraError != null)
|
||||||
|
{
|
||||||
|
HexMessageBox.Show(_cameraError, MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return shell;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 在主线程上检索并连接导航相机。
|
||||||
|
/// pylon SDK 要求在主线程(STA)上操作,不能放到后台线程。
|
||||||
|
/// </summary>
|
||||||
|
private void TryConnectCamera()
|
||||||
|
{
|
||||||
|
var camera = Container.Resolve<ICameraController>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var info = camera.Open();
|
||||||
|
Log.Information("导航相机已连接: {ModelName} (SN: {SerialNumber})", info.ModelName, info.SerialNumber);
|
||||||
|
}
|
||||||
|
catch (DeviceNotFoundException)
|
||||||
|
{
|
||||||
|
Log.Warning("未检测到导航相机");
|
||||||
|
_cameraError = "未检测到导航相机,请检查连接后重启软件。";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Warning(ex, "导航相机自动连接失败: {Message}", ex.Message);
|
||||||
|
_cameraError = $"导航相机连接失败: {ex.Message}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -318,7 +382,6 @@ namespace XplorePlane
|
|||||||
containerRegistry.RegisterSingleton<ICncProgramService, CncProgramService>();
|
containerRegistry.RegisterSingleton<ICncProgramService, CncProgramService>();
|
||||||
containerRegistry.RegisterSingleton<IMatrixService, MatrixService>();
|
containerRegistry.RegisterSingleton<IMatrixService, MatrixService>();
|
||||||
containerRegistry.RegisterSingleton<IMeasurementDataService, MeasurementDataService>();
|
containerRegistry.RegisterSingleton<IMeasurementDataService, MeasurementDataService>();
|
||||||
containerRegistry.RegisterSingleton<IInspectionResultStore, InspectionResultStore>();
|
|
||||||
|
|
||||||
// ── CNC / 矩阵 ViewModel(瞬态)──
|
// ── CNC / 矩阵 ViewModel(瞬态)──
|
||||||
containerRegistry.Register<CncEditorViewModel>();
|
containerRegistry.Register<CncEditorViewModel>();
|
||||||
@@ -329,7 +392,7 @@ namespace XplorePlane
|
|||||||
containerRegistry.RegisterForNavigation<CncPageView>();
|
containerRegistry.RegisterForNavigation<CncPageView>();
|
||||||
containerRegistry.RegisterForNavigation<MatrixPageView>();
|
containerRegistry.RegisterForNavigation<MatrixPageView>();
|
||||||
|
|
||||||
// ── 相机服务(单例)──
|
// ── 导航相机服务(单例)──
|
||||||
containerRegistry.RegisterSingleton<ICameraFactory, CameraFactory>();
|
containerRegistry.RegisterSingleton<ICameraFactory, CameraFactory>();
|
||||||
containerRegistry.RegisterSingleton<ICameraController>(() =>
|
containerRegistry.RegisterSingleton<ICameraController>(() =>
|
||||||
new CameraFactory().CreateController("Basler"));
|
new CameraFactory().CreateController("Basler"));
|
||||||
@@ -356,4 +419,4 @@ namespace XplorePlane
|
|||||||
base.ConfigureModuleCatalog(moduleCatalog);
|
base.ConfigureModuleCatalog(moduleCatalog);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 174 KiB |
@@ -46,8 +46,7 @@ namespace XplorePlane.Models
|
|||||||
/// <summary>参考点节点 | Reference point node</summary>
|
/// <summary>参考点节点 | Reference point node</summary>
|
||||||
public record ReferencePointNode(
|
public record ReferencePointNode(
|
||||||
Guid Id, int Index, string Name,
|
Guid Id, int Index, string Name,
|
||||||
double XM, double YM, double ZT, double ZD, double TiltD, double Dist,
|
double XM, double YM, double ZT, double ZD, double TiltD, double Dist
|
||||||
bool IsRayOn, double Voltage, double Current
|
|
||||||
) : CncNode(Id, Index, CncNodeType.ReferencePoint, Name);
|
) : CncNode(Id, Index, CncNodeType.ReferencePoint, Name);
|
||||||
|
|
||||||
/// <summary>保存节点(含图像)| Save node with image</summary>
|
/// <summary>保存节点(含图像)| Save node with image</summary>
|
||||||
@@ -114,4 +113,4 @@ namespace XplorePlane.Models
|
|||||||
DateTime UpdatedAt,
|
DateTime UpdatedAt,
|
||||||
IReadOnlyList<CncNode> Nodes
|
IReadOnlyList<CncNode> Nodes
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace XplorePlane.Models
|
|
||||||
{
|
|
||||||
public enum InspectionAssetType
|
|
||||||
{
|
|
||||||
RunSourceImage,
|
|
||||||
NodeInputImage,
|
|
||||||
NodeResultImage
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum InspectionNodeStatus
|
|
||||||
{
|
|
||||||
Succeeded,
|
|
||||||
Failed,
|
|
||||||
PartialSuccess,
|
|
||||||
AssetMissing
|
|
||||||
}
|
|
||||||
|
|
||||||
public class InspectionRunRecord
|
|
||||||
{
|
|
||||||
public Guid RunId { get; set; } = Guid.NewGuid();
|
|
||||||
public string ProgramName { get; set; } = string.Empty;
|
|
||||||
public string WorkpieceId { get; set; } = string.Empty;
|
|
||||||
public string SerialNumber { get; set; } = string.Empty;
|
|
||||||
public DateTime StartedAt { get; set; } = DateTime.UtcNow;
|
|
||||||
public DateTime? CompletedAt { get; set; }
|
|
||||||
public bool OverallPass { get; set; }
|
|
||||||
public string SourceImagePath { get; set; } = string.Empty;
|
|
||||||
public string ResultRootPath { get; set; } = string.Empty;
|
|
||||||
public int NodeCount { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class InspectionNodeResult
|
|
||||||
{
|
|
||||||
public Guid RunId { get; set; }
|
|
||||||
public Guid NodeId { get; set; } = Guid.NewGuid();
|
|
||||||
public int NodeIndex { get; set; }
|
|
||||||
public string NodeName { get; set; } = string.Empty;
|
|
||||||
public Guid PipelineId { get; set; }
|
|
||||||
public string PipelineName { get; set; } = string.Empty;
|
|
||||||
public string PipelineVersionHash { get; set; } = string.Empty;
|
|
||||||
public bool NodePass { get; set; }
|
|
||||||
public string SourceImagePath { get; set; } = string.Empty;
|
|
||||||
public string ResultImagePath { get; set; } = string.Empty;
|
|
||||||
public InspectionNodeStatus Status { get; set; } = InspectionNodeStatus.Succeeded;
|
|
||||||
public long DurationMs { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class InspectionMetricResult
|
|
||||||
{
|
|
||||||
public Guid RunId { get; set; }
|
|
||||||
public Guid NodeId { get; set; }
|
|
||||||
public string MetricKey { get; set; } = string.Empty;
|
|
||||||
public string MetricName { get; set; } = string.Empty;
|
|
||||||
public double MetricValue { get; set; }
|
|
||||||
public string Unit { get; set; } = string.Empty;
|
|
||||||
public double? LowerLimit { get; set; }
|
|
||||||
public double? UpperLimit { get; set; }
|
|
||||||
public bool IsPass { get; set; }
|
|
||||||
public int DisplayOrder { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class InspectionAssetRecord
|
|
||||||
{
|
|
||||||
public Guid RunId { get; set; }
|
|
||||||
public Guid? NodeId { get; set; }
|
|
||||||
public InspectionAssetType AssetType { get; set; }
|
|
||||||
public string RelativePath { get; set; } = string.Empty;
|
|
||||||
public string FileFormat { get; set; } = string.Empty;
|
|
||||||
public int Width { get; set; }
|
|
||||||
public int Height { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class PipelineExecutionSnapshot
|
|
||||||
{
|
|
||||||
public Guid RunId { get; set; }
|
|
||||||
public Guid NodeId { get; set; }
|
|
||||||
public string PipelineName { get; set; } = string.Empty;
|
|
||||||
public string PipelineDefinitionJson { get; set; } = string.Empty;
|
|
||||||
public string PipelineHash { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class InspectionAssetWriteRequest
|
|
||||||
{
|
|
||||||
public InspectionAssetType AssetType { get; set; }
|
|
||||||
public string FileName { get; set; } = string.Empty;
|
|
||||||
public string SourceFilePath { get; set; } = string.Empty;
|
|
||||||
public byte[] Content { get; set; }
|
|
||||||
public string FileFormat { get; set; } = string.Empty;
|
|
||||||
public int Width { get; set; }
|
|
||||||
public int Height { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class InspectionRunQuery
|
|
||||||
{
|
|
||||||
public string ProgramName { get; set; } = string.Empty;
|
|
||||||
public string WorkpieceId { get; set; } = string.Empty;
|
|
||||||
public string SerialNumber { get; set; } = string.Empty;
|
|
||||||
public string PipelineName { get; set; } = string.Empty;
|
|
||||||
public DateTime? From { get; set; }
|
|
||||||
public DateTime? To { get; set; }
|
|
||||||
public int? Skip { get; set; }
|
|
||||||
public int? Take { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class InspectionRunDetail
|
|
||||||
{
|
|
||||||
public InspectionRunRecord Run { get; set; } = new();
|
|
||||||
public IReadOnlyList<InspectionNodeResult> Nodes { get; set; } = Array.Empty<InspectionNodeResult>();
|
|
||||||
public IReadOnlyList<InspectionMetricResult> Metrics { get; set; } = Array.Empty<InspectionMetricResult>();
|
|
||||||
public IReadOnlyList<InspectionAssetRecord> Assets { get; set; } = Array.Empty<InspectionAssetRecord>();
|
|
||||||
public IReadOnlyList<PipelineExecutionSnapshot> PipelineSnapshots { get; set; } = Array.Empty<PipelineExecutionSnapshot>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@ using System.Text.Json;
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using XP.Common.Logging.Interfaces;
|
using XP.Common.Logging.Interfaces;
|
||||||
using XP.Hardware.RaySource.Services;
|
|
||||||
using XplorePlane.Models;
|
using XplorePlane.Models;
|
||||||
using XplorePlane.Services.AppState;
|
using XplorePlane.Services.AppState;
|
||||||
|
|
||||||
@@ -21,7 +20,6 @@ namespace XplorePlane.Services.Cnc
|
|||||||
public class CncProgramService : ICncProgramService
|
public class CncProgramService : ICncProgramService
|
||||||
{
|
{
|
||||||
private readonly IAppStateService _appStateService;
|
private readonly IAppStateService _appStateService;
|
||||||
private readonly IRaySourceService _raySourceService;
|
|
||||||
private readonly ILoggerService _logger;
|
private readonly ILoggerService _logger;
|
||||||
|
|
||||||
// ── 序列化配置 | Serialization options ──
|
// ── 序列化配置 | Serialization options ──
|
||||||
@@ -34,15 +32,12 @@ namespace XplorePlane.Services.Cnc
|
|||||||
|
|
||||||
public CncProgramService(
|
public CncProgramService(
|
||||||
IAppStateService appStateService,
|
IAppStateService appStateService,
|
||||||
IRaySourceService raySourceService,
|
|
||||||
ILoggerService logger)
|
ILoggerService logger)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(appStateService);
|
ArgumentNullException.ThrowIfNull(appStateService);
|
||||||
ArgumentNullException.ThrowIfNull(raySourceService);
|
|
||||||
ArgumentNullException.ThrowIfNull(logger);
|
ArgumentNullException.ThrowIfNull(logger);
|
||||||
|
|
||||||
_appStateService = appStateService;
|
_appStateService = appStateService;
|
||||||
_raySourceService = raySourceService;
|
|
||||||
_logger = logger.ForModule<CncProgramService>();
|
_logger = logger.ForModule<CncProgramService>();
|
||||||
|
|
||||||
_logger.Info("CncProgramService 已初始化 | CncProgramService initialized");
|
_logger.Info("CncProgramService 已初始化 | CncProgramService initialized");
|
||||||
@@ -205,32 +200,6 @@ namespace XplorePlane.Services.Cnc
|
|||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public CncProgram UpdateNode(CncProgram program, int index, CncNode node)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(program);
|
|
||||||
ArgumentNullException.ThrowIfNull(node);
|
|
||||||
|
|
||||||
if (index < 0 || index >= program.Nodes.Count)
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(index),
|
|
||||||
$"Index out of range: {index}, Count={program.Nodes.Count}");
|
|
||||||
|
|
||||||
var nodes = new List<CncNode>(program.Nodes)
|
|
||||||
{
|
|
||||||
[index] = node with { Index = index }
|
|
||||||
};
|
|
||||||
|
|
||||||
var updated = program with
|
|
||||||
{
|
|
||||||
Nodes = nodes.AsReadOnly(),
|
|
||||||
UpdatedAt = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
_logger.Info("Updated node: Index={Index}, Type={NodeType}, Program={ProgramName}",
|
|
||||||
index, node.NodeType, program.Name);
|
|
||||||
return updated;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task SaveAsync(CncProgram program, string filePath)
|
public async Task SaveAsync(CncProgram program, string filePath)
|
||||||
{
|
{
|
||||||
@@ -375,7 +344,6 @@ namespace XplorePlane.Services.Cnc
|
|||||||
private ReferencePointNode CreateReferencePointNode(Guid id, int index)
|
private ReferencePointNode CreateReferencePointNode(Guid id, int index)
|
||||||
{
|
{
|
||||||
var motion = _appStateService.MotionState;
|
var motion = _appStateService.MotionState;
|
||||||
var raySource = _appStateService.RaySourceState;
|
|
||||||
return new ReferencePointNode(
|
return new ReferencePointNode(
|
||||||
id, index, $"参考点_{index}",
|
id, index, $"参考点_{index}",
|
||||||
XM: motion.XM,
|
XM: motion.XM,
|
||||||
@@ -383,10 +351,7 @@ namespace XplorePlane.Services.Cnc
|
|||||||
ZT: motion.ZT,
|
ZT: motion.ZT,
|
||||||
ZD: motion.ZD,
|
ZD: motion.ZD,
|
||||||
TiltD: motion.TiltD,
|
TiltD: motion.TiltD,
|
||||||
Dist: motion.Dist,
|
Dist: motion.Dist);
|
||||||
IsRayOn: raySource.IsOn,
|
|
||||||
Voltage: raySource.Voltage,
|
|
||||||
Current: TryReadCurrent());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>创建保存节点(含图像)| Create save node with image</summary>
|
/// <summary>创建保存节点(含图像)| Create save node with image</summary>
|
||||||
@@ -417,24 +382,5 @@ namespace XplorePlane.Services.Cnc
|
|||||||
id, index, $"保存位置_{index}",
|
id, index, $"保存位置_{index}",
|
||||||
MotionState: _appStateService.MotionState);
|
MotionState: _appStateService.MotionState);
|
||||||
}
|
}
|
||||||
private double TryReadCurrent()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = _raySourceService.ReadCurrent();
|
|
||||||
if (result?.Success == true)
|
|
||||||
{
|
|
||||||
return result.GetFloat();
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.Warn("Failed to read ray source current, ReferencePoint node will use 0");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.Warn("Failed to read ray source current: {Message}", ex.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0d;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,28 +4,36 @@ using XplorePlane.Models;
|
|||||||
namespace XplorePlane.Services.Cnc
|
namespace XplorePlane.Services.Cnc
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// CNC program management service interface.
|
/// CNC 程序管理服务接口,负责程序的创建、节点编辑、序列化/反序列化和文件读写
|
||||||
|
/// CNC program management service interface for creation, node editing, serialization and file I/O
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ICncProgramService
|
public interface ICncProgramService
|
||||||
{
|
{
|
||||||
|
/// <summary>创建空的 CNC 程序 | Create an empty CNC program</summary>
|
||||||
CncProgram CreateProgram(string name);
|
CncProgram CreateProgram(string name);
|
||||||
|
|
||||||
|
/// <summary>根据节点类型创建节点(从 IAppStateService 捕获设备状态)| Create a node by type (captures device state from IAppStateService)</summary>
|
||||||
CncNode CreateNode(CncNodeType type);
|
CncNode CreateNode(CncNodeType type);
|
||||||
|
|
||||||
|
/// <summary>在指定索引之后插入节点并重新编号 | Insert a node after the given index and renumber</summary>
|
||||||
CncProgram InsertNode(CncProgram program, int afterIndex, CncNode node);
|
CncProgram InsertNode(CncProgram program, int afterIndex, CncNode node);
|
||||||
|
|
||||||
|
/// <summary>移除指定索引的节点并重新编号 | Remove the node at the given index and renumber</summary>
|
||||||
CncProgram RemoveNode(CncProgram program, int index);
|
CncProgram RemoveNode(CncProgram program, int index);
|
||||||
|
|
||||||
|
/// <summary>将节点从旧索引移动到新索引并重新编号 | Move a node from old index to new index and renumber</summary>
|
||||||
CncProgram MoveNode(CncProgram program, int oldIndex, int newIndex);
|
CncProgram MoveNode(CncProgram program, int oldIndex, int newIndex);
|
||||||
|
|
||||||
CncProgram UpdateNode(CncProgram program, int index, CncNode node);
|
/// <summary>将 CNC 程序保存到 .xp 文件 | Save CNC program to .xp file</summary>
|
||||||
|
|
||||||
Task SaveAsync(CncProgram program, string filePath);
|
Task SaveAsync(CncProgram program, string filePath);
|
||||||
|
|
||||||
|
/// <summary>从 .xp 文件加载 CNC 程序 | Load CNC program from .xp file</summary>
|
||||||
Task<CncProgram> LoadAsync(string filePath);
|
Task<CncProgram> LoadAsync(string filePath);
|
||||||
|
|
||||||
|
/// <summary>将 CNC 程序序列化为 JSON 字符串 | Serialize CNC program to JSON string</summary>
|
||||||
string Serialize(CncProgram program);
|
string Serialize(CncProgram program);
|
||||||
|
|
||||||
|
/// <summary>从 JSON 字符串反序列化 CNC 程序 | Deserialize CNC program from JSON string</summary>
|
||||||
CncProgram Deserialize(string json);
|
CncProgram Deserialize(string json);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,11 +28,11 @@ namespace XplorePlane.Services
|
|||||||
var formatted = new FormatConvertedBitmap(bitmapSource, PixelFormats.Gray8, null, 0);
|
var formatted = new FormatConvertedBitmap(bitmapSource, PixelFormats.Gray8, null, 0);
|
||||||
int width = formatted.PixelWidth;
|
int width = formatted.PixelWidth;
|
||||||
int height = formatted.PixelHeight;
|
int height = formatted.PixelHeight;
|
||||||
int stride = width;
|
|
||||||
byte[] pixels = new byte[height * stride];
|
|
||||||
formatted.CopyPixels(pixels, stride, 0);
|
|
||||||
|
|
||||||
var image = new Image<Gray, byte>(width, height);
|
var image = new Image<Gray, byte>(width, height);
|
||||||
|
int stride = image.Bytes.Length / height;
|
||||||
|
var pixels = new byte[height * stride];
|
||||||
|
formatted.CopyPixels(pixels, stride, 0);
|
||||||
image.Bytes = pixels;
|
image.Bytes = pixels;
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,19 @@ namespace XplorePlane.Services
|
|||||||
public static Image<Gray, byte> ToEmguCVFromPixels(byte[] pixels, int width, int height)
|
public static Image<Gray, byte> ToEmguCVFromPixels(byte[] pixels, int width, int height)
|
||||||
{
|
{
|
||||||
var image = new Image<Gray, byte>(width, height);
|
var image = new Image<Gray, byte>(width, height);
|
||||||
image.Bytes = pixels;
|
int required = image.Bytes.Length;
|
||||||
|
if (pixels.Length == required)
|
||||||
|
{
|
||||||
|
image.Bytes = pixels;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int stride = required / height;
|
||||||
|
var padded = new byte[required];
|
||||||
|
for (int row = 0; row < height; row++)
|
||||||
|
Buffer.BlockCopy(pixels, row * width, padded, row * stride, width);
|
||||||
|
image.Bytes = padded;
|
||||||
|
}
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,8 +62,8 @@ namespace XplorePlane.Services
|
|||||||
|
|
||||||
int width = emguImage.Width;
|
int width = emguImage.Width;
|
||||||
int height = emguImage.Height;
|
int height = emguImage.Height;
|
||||||
int stride = width;
|
|
||||||
byte[] pixels = emguImage.Bytes;
|
byte[] pixels = emguImage.Bytes;
|
||||||
|
int stride = pixels.Length / height;
|
||||||
|
|
||||||
return BitmapSource.Create(width, height, 96, 96, PixelFormats.Gray8, null, pixels, stride);
|
return BitmapSource.Create(width, height, 96, 96, PixelFormats.Gray8, null, pixels, stride);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using XplorePlane.Models;
|
|
||||||
|
|
||||||
namespace XplorePlane.Services.InspectionResults
|
|
||||||
{
|
|
||||||
public interface IInspectionResultStore
|
|
||||||
{
|
|
||||||
Task BeginRunAsync(InspectionRunRecord runRecord, InspectionAssetWriteRequest runSourceAsset = null);
|
|
||||||
|
|
||||||
Task AppendNodeResultAsync(
|
|
||||||
InspectionNodeResult nodeResult,
|
|
||||||
IEnumerable<InspectionMetricResult> metrics = null,
|
|
||||||
PipelineExecutionSnapshot pipelineSnapshot = null,
|
|
||||||
IEnumerable<InspectionAssetWriteRequest> assets = null);
|
|
||||||
|
|
||||||
Task CompleteRunAsync(Guid runId, bool? overallPass = null, DateTime? completedAt = null);
|
|
||||||
|
|
||||||
Task<IReadOnlyList<InspectionRunRecord>> QueryRunsAsync(InspectionRunQuery query = null);
|
|
||||||
|
|
||||||
Task<InspectionRunDetail> GetRunDetailAsync(Guid runId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,6 @@ using Prism.Commands;
|
|||||||
using Prism.Events;
|
using Prism.Events;
|
||||||
using Prism.Mvvm;
|
using Prism.Mvvm;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@@ -19,21 +18,27 @@ using XplorePlane.Services.Cnc;
|
|||||||
namespace XplorePlane.ViewModels.Cnc
|
namespace XplorePlane.ViewModels.Cnc
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// CNC editor ViewModel that manages the node tree, editing operations and file operations.
|
/// CNC 编辑器 ViewModel,管理 CNC 程序的节点列表、编辑操作和文件操作
|
||||||
|
/// CNC editor ViewModel that manages the node list, editing operations and file operations
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CncEditorViewModel : BindableBase
|
public class CncEditorViewModel : BindableBase
|
||||||
{
|
{
|
||||||
private readonly ICncProgramService _cncProgramService;
|
private readonly ICncProgramService _cncProgramService;
|
||||||
|
private readonly IAppStateService _appStateService;
|
||||||
private readonly IEventAggregator _eventAggregator;
|
private readonly IEventAggregator _eventAggregator;
|
||||||
private readonly ILoggerService _logger;
|
private readonly ILoggerService _logger;
|
||||||
|
|
||||||
|
// 当前 CNC 程序 | Current CNC program
|
||||||
private CncProgram _currentProgram;
|
private CncProgram _currentProgram;
|
||||||
|
|
||||||
private ObservableCollection<CncNodeViewModel> _nodes;
|
private ObservableCollection<CncNodeViewModel> _nodes;
|
||||||
private ObservableCollection<CncNodeViewModel> _treeNodes;
|
|
||||||
private CncNodeViewModel _selectedNode;
|
private CncNodeViewModel _selectedNode;
|
||||||
private bool _isModified;
|
private bool _isModified;
|
||||||
private string _programName;
|
private string _programName;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数 | Constructor
|
||||||
|
/// </summary>
|
||||||
public CncEditorViewModel(
|
public CncEditorViewModel(
|
||||||
ICncProgramService cncProgramService,
|
ICncProgramService cncProgramService,
|
||||||
IAppStateService appStateService,
|
IAppStateService appStateService,
|
||||||
@@ -41,13 +46,13 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
ILoggerService logger)
|
ILoggerService logger)
|
||||||
{
|
{
|
||||||
_cncProgramService = cncProgramService ?? throw new ArgumentNullException(nameof(cncProgramService));
|
_cncProgramService = cncProgramService ?? throw new ArgumentNullException(nameof(cncProgramService));
|
||||||
ArgumentNullException.ThrowIfNull(appStateService);
|
_appStateService = appStateService ?? throw new ArgumentNullException(nameof(appStateService));
|
||||||
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
|
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
|
||||||
_logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule<CncEditorViewModel>();
|
_logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule<CncEditorViewModel>();
|
||||||
|
|
||||||
_nodes = new ObservableCollection<CncNodeViewModel>();
|
_nodes = new ObservableCollection<CncNodeViewModel>();
|
||||||
_treeNodes = new ObservableCollection<CncNodeViewModel>();
|
|
||||||
|
|
||||||
|
// ── 节点插入命令 | Node insertion commands ──
|
||||||
InsertReferencePointCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.ReferencePoint));
|
InsertReferencePointCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.ReferencePoint));
|
||||||
InsertSaveNodeWithImageCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNodeWithImage));
|
InsertSaveNodeWithImageCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNodeWithImage));
|
||||||
InsertSaveNodeCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNode));
|
InsertSaveNodeCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNode));
|
||||||
@@ -58,79 +63,117 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
InsertWaitDelayCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.WaitDelay));
|
InsertWaitDelayCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.WaitDelay));
|
||||||
InsertCompleteProgramCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.CompleteProgram));
|
InsertCompleteProgramCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.CompleteProgram));
|
||||||
|
|
||||||
|
// ── 节点编辑命令 | Node editing commands ──
|
||||||
DeleteNodeCommand = new DelegateCommand(ExecuteDeleteNode, CanExecuteDeleteNode)
|
DeleteNodeCommand = new DelegateCommand(ExecuteDeleteNode, CanExecuteDeleteNode)
|
||||||
.ObservesProperty(() => SelectedNode);
|
.ObservesProperty(() => SelectedNode);
|
||||||
MoveNodeUpCommand = new DelegateCommand<CncNodeViewModel>(ExecuteMoveNodeUp);
|
MoveNodeUpCommand = new DelegateCommand<CncNodeViewModel>(ExecuteMoveNodeUp);
|
||||||
MoveNodeDownCommand = new DelegateCommand<CncNodeViewModel>(ExecuteMoveNodeDown);
|
MoveNodeDownCommand = new DelegateCommand<CncNodeViewModel>(ExecuteMoveNodeDown);
|
||||||
|
|
||||||
|
// ── 文件操作命令 | File operation commands ──
|
||||||
SaveProgramCommand = new DelegateCommand(async () => await ExecuteSaveProgramAsync());
|
SaveProgramCommand = new DelegateCommand(async () => await ExecuteSaveProgramAsync());
|
||||||
LoadProgramCommand = new DelegateCommand(async () => await ExecuteLoadProgramAsync());
|
LoadProgramCommand = new DelegateCommand(async () => await ExecuteLoadProgramAsync());
|
||||||
NewProgramCommand = new DelegateCommand(ExecuteNewProgram);
|
NewProgramCommand = new DelegateCommand(ExecuteNewProgram);
|
||||||
ExportCsvCommand = new DelegateCommand(ExecuteExportCsv);
|
ExportCsvCommand = new DelegateCommand(ExecuteExportCsv);
|
||||||
|
|
||||||
_logger.Info("CncEditorViewModel initialized");
|
_logger.Info("CncEditorViewModel 已初始化 | CncEditorViewModel initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 属性 | Properties ──────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>节点列表 | Node list</summary>
|
||||||
public ObservableCollection<CncNodeViewModel> Nodes
|
public ObservableCollection<CncNodeViewModel> Nodes
|
||||||
{
|
{
|
||||||
get => _nodes;
|
get => _nodes;
|
||||||
private set => SetProperty(ref _nodes, value);
|
set => SetProperty(ref _nodes, value);
|
||||||
}
|
|
||||||
|
|
||||||
public ObservableCollection<CncNodeViewModel> TreeNodes
|
|
||||||
{
|
|
||||||
get => _treeNodes;
|
|
||||||
private set => SetProperty(ref _treeNodes, value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>当前选中的节点 | Currently selected node</summary>
|
||||||
public CncNodeViewModel SelectedNode
|
public CncNodeViewModel SelectedNode
|
||||||
{
|
{
|
||||||
get => _selectedNode;
|
get => _selectedNode;
|
||||||
set
|
set => SetProperty(ref _selectedNode, value);
|
||||||
{
|
|
||||||
if (SetProperty(ref _selectedNode, value))
|
|
||||||
{
|
|
||||||
RaisePropertyChanged(nameof(HasSelection));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool HasSelection => SelectedNode != null;
|
/// <summary>程序是否已修改 | Whether the program has been modified</summary>
|
||||||
|
|
||||||
public bool IsModified
|
public bool IsModified
|
||||||
{
|
{
|
||||||
get => _isModified;
|
get => _isModified;
|
||||||
set => SetProperty(ref _isModified, value);
|
set => SetProperty(ref _isModified, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>当前程序名称 | Current program name</summary>
|
||||||
public string ProgramName
|
public string ProgramName
|
||||||
{
|
{
|
||||||
get => _programName;
|
get => _programName;
|
||||||
set => SetProperty(ref _programName, value);
|
set => SetProperty(ref _programName, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 节点插入命令 | Node insertion commands ──────────────────────
|
||||||
|
|
||||||
|
/// <summary>插入参考点命令 | Insert reference point command</summary>
|
||||||
public DelegateCommand InsertReferencePointCommand { get; }
|
public DelegateCommand InsertReferencePointCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>插入保存节点(含图像)命令 | Insert save node with image command</summary>
|
||||||
public DelegateCommand InsertSaveNodeWithImageCommand { get; }
|
public DelegateCommand InsertSaveNodeWithImageCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>插入保存节点命令 | Insert save node command</summary>
|
||||||
public DelegateCommand InsertSaveNodeCommand { get; }
|
public DelegateCommand InsertSaveNodeCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>插入保存位置命令 | Insert save position command</summary>
|
||||||
public DelegateCommand InsertSavePositionCommand { get; }
|
public DelegateCommand InsertSavePositionCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>插入检测模块命令 | Insert inspection module command</summary>
|
||||||
public DelegateCommand InsertInspectionModuleCommand { get; }
|
public DelegateCommand InsertInspectionModuleCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>插入检测标记命令 | Insert inspection marker command</summary>
|
||||||
public DelegateCommand InsertInspectionMarkerCommand { get; }
|
public DelegateCommand InsertInspectionMarkerCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>插入停顿对话框命令 | Insert pause dialog command</summary>
|
||||||
public DelegateCommand InsertPauseDialogCommand { get; }
|
public DelegateCommand InsertPauseDialogCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>插入等待延时命令 | Insert wait delay command</summary>
|
||||||
public DelegateCommand InsertWaitDelayCommand { get; }
|
public DelegateCommand InsertWaitDelayCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>插入完成程序命令 | Insert complete program command</summary>
|
||||||
public DelegateCommand InsertCompleteProgramCommand { get; }
|
public DelegateCommand InsertCompleteProgramCommand { get; }
|
||||||
|
|
||||||
|
// ── 节点编辑命令 | Node editing commands ────────────────────────
|
||||||
|
|
||||||
|
/// <summary>删除选中节点命令 | Delete selected node command</summary>
|
||||||
public DelegateCommand DeleteNodeCommand { get; }
|
public DelegateCommand DeleteNodeCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>上移节点命令 | Move node up command</summary>
|
||||||
public DelegateCommand<CncNodeViewModel> MoveNodeUpCommand { get; }
|
public DelegateCommand<CncNodeViewModel> MoveNodeUpCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>下移节点命令 | Move node down command</summary>
|
||||||
public DelegateCommand<CncNodeViewModel> MoveNodeDownCommand { get; }
|
public DelegateCommand<CncNodeViewModel> MoveNodeDownCommand { get; }
|
||||||
|
|
||||||
|
// ── 文件操作命令 | File operation commands ──────────────────────
|
||||||
|
|
||||||
|
/// <summary>保存程序命令 | Save program command</summary>
|
||||||
public DelegateCommand SaveProgramCommand { get; }
|
public DelegateCommand SaveProgramCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>加载程序命令 | Load program command</summary>
|
||||||
public DelegateCommand LoadProgramCommand { get; }
|
public DelegateCommand LoadProgramCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>新建程序命令 | New program command</summary>
|
||||||
public DelegateCommand NewProgramCommand { get; }
|
public DelegateCommand NewProgramCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>导出 CSV 命令 | Export CSV command</summary>
|
||||||
public DelegateCommand ExportCsvCommand { get; }
|
public DelegateCommand ExportCsvCommand { get; }
|
||||||
|
|
||||||
|
// ── 命令执行方法 | Command execution methods ────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 插入指定类型的节点到选中节点之后
|
||||||
|
/// Insert a node of the specified type after the selected node
|
||||||
|
/// </summary>
|
||||||
private void ExecuteInsertNode(CncNodeType nodeType)
|
private void ExecuteInsertNode(CncNodeType nodeType)
|
||||||
{
|
{
|
||||||
if (_currentProgram == null)
|
if (_currentProgram == null)
|
||||||
{
|
{
|
||||||
ExecuteNewProgram();
|
_logger.Warn("无法插入节点:当前无程序 | Cannot insert node: no current program");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -140,14 +183,18 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
_currentProgram = _cncProgramService.InsertNode(_currentProgram, afterIndex, node);
|
_currentProgram = _cncProgramService.InsertNode(_currentProgram, afterIndex, node);
|
||||||
|
|
||||||
OnProgramEdited();
|
OnProgramEdited();
|
||||||
_logger.Info("Inserted node: Type={NodeType}", nodeType);
|
_logger.Info("已插入节点 | Inserted node: Type={NodeType}", nodeType);
|
||||||
}
|
}
|
||||||
catch (InvalidOperationException ex)
|
catch (InvalidOperationException ex)
|
||||||
{
|
{
|
||||||
_logger.Warn("Node insertion blocked: {Message}", ex.Message);
|
// 重复插入 CompleteProgram 等业务规则异常 | Business rule exceptions like duplicate CompleteProgram
|
||||||
|
_logger.Warn("插入节点被阻止 | Node insertion blocked: {Message}", ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除选中节点 | Delete the selected node
|
||||||
|
/// </summary>
|
||||||
private void ExecuteDeleteNode()
|
private void ExecuteDeleteNode()
|
||||||
{
|
{
|
||||||
if (_currentProgram == null || SelectedNode == null)
|
if (_currentProgram == null || SelectedNode == null)
|
||||||
@@ -157,14 +204,18 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
{
|
{
|
||||||
_currentProgram = _cncProgramService.RemoveNode(_currentProgram, SelectedNode.Index);
|
_currentProgram = _cncProgramService.RemoveNode(_currentProgram, SelectedNode.Index);
|
||||||
OnProgramEdited();
|
OnProgramEdited();
|
||||||
_logger.Info("Deleted node at index: {Index}", SelectedNode.Index);
|
_logger.Info("已删除节点 | Deleted node at index: {Index}", SelectedNode.Index);
|
||||||
}
|
}
|
||||||
catch (ArgumentOutOfRangeException ex)
|
catch (ArgumentOutOfRangeException ex)
|
||||||
{
|
{
|
||||||
_logger.Warn("Delete node failed: {Message}", ex.Message);
|
_logger.Warn("删除节点失败 | Delete node failed: {Message}", ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断是否可以删除节点(至少保留 1 个节点)
|
||||||
|
/// Determines whether delete is allowed (at least 1 node must remain)
|
||||||
|
/// </summary>
|
||||||
private bool CanExecuteDeleteNode()
|
private bool CanExecuteDeleteNode()
|
||||||
{
|
{
|
||||||
return SelectedNode != null
|
return SelectedNode != null
|
||||||
@@ -172,6 +223,9 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
&& _currentProgram.Nodes.Count > 1;
|
&& _currentProgram.Nodes.Count > 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 上移节点 | Move node up
|
||||||
|
/// </summary>
|
||||||
private void ExecuteMoveNodeUp(CncNodeViewModel nodeVm)
|
private void ExecuteMoveNodeUp(CncNodeViewModel nodeVm)
|
||||||
{
|
{
|
||||||
if (_currentProgram == null || nodeVm == null || nodeVm.Index <= 0)
|
if (_currentProgram == null || nodeVm == null || nodeVm.Index <= 0)
|
||||||
@@ -181,13 +235,17 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
{
|
{
|
||||||
_currentProgram = _cncProgramService.MoveNode(_currentProgram, nodeVm.Index, nodeVm.Index - 1);
|
_currentProgram = _cncProgramService.MoveNode(_currentProgram, nodeVm.Index, nodeVm.Index - 1);
|
||||||
OnProgramEdited();
|
OnProgramEdited();
|
||||||
|
_logger.Info("已上移节点 | Moved node up: {OldIndex} -> {NewIndex}", nodeVm.Index, nodeVm.Index - 1);
|
||||||
}
|
}
|
||||||
catch (ArgumentOutOfRangeException ex)
|
catch (ArgumentOutOfRangeException ex)
|
||||||
{
|
{
|
||||||
_logger.Warn("Move node up failed: {Message}", ex.Message);
|
_logger.Warn("上移节点失败 | Move node up failed: {Message}", ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下移节点 | Move node down
|
||||||
|
/// </summary>
|
||||||
private void ExecuteMoveNodeDown(CncNodeViewModel nodeVm)
|
private void ExecuteMoveNodeDown(CncNodeViewModel nodeVm)
|
||||||
{
|
{
|
||||||
if (_currentProgram == null || nodeVm == null || nodeVm.Index >= _currentProgram.Nodes.Count - 1)
|
if (_currentProgram == null || nodeVm == null || nodeVm.Index >= _currentProgram.Nodes.Count - 1)
|
||||||
@@ -197,18 +255,22 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
{
|
{
|
||||||
_currentProgram = _cncProgramService.MoveNode(_currentProgram, nodeVm.Index, nodeVm.Index + 1);
|
_currentProgram = _cncProgramService.MoveNode(_currentProgram, nodeVm.Index, nodeVm.Index + 1);
|
||||||
OnProgramEdited();
|
OnProgramEdited();
|
||||||
|
_logger.Info("已下移节点 | Moved node down: {OldIndex} -> {NewIndex}", nodeVm.Index, nodeVm.Index + 1);
|
||||||
}
|
}
|
||||||
catch (ArgumentOutOfRangeException ex)
|
catch (ArgumentOutOfRangeException ex)
|
||||||
{
|
{
|
||||||
_logger.Warn("Move node down failed: {Message}", ex.Message);
|
_logger.Warn("下移节点失败 | Move node down failed: {Message}", ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存当前程序到文件 | Save current program to file
|
||||||
|
/// </summary>
|
||||||
private async Task ExecuteSaveProgramAsync()
|
private async Task ExecuteSaveProgramAsync()
|
||||||
{
|
{
|
||||||
if (_currentProgram == null)
|
if (_currentProgram == null)
|
||||||
{
|
{
|
||||||
_logger.Warn("Cannot save: no current program");
|
_logger.Warn("无法保存:当前无程序 | Cannot save: no current program");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,13 +289,17 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
|
|
||||||
await _cncProgramService.SaveAsync(_currentProgram, dlg.FileName);
|
await _cncProgramService.SaveAsync(_currentProgram, dlg.FileName);
|
||||||
IsModified = false;
|
IsModified = false;
|
||||||
|
_logger.Info("程序已保存 | Program saved: {FilePath}", dlg.FileName);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.Error(ex, "Failed to save program");
|
_logger.Error(ex, "保存程序失败 | Failed to save program");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从文件加载程序 | Load program from file
|
||||||
|
/// </summary>
|
||||||
private async Task ExecuteLoadProgramAsync()
|
private async Task ExecuteLoadProgramAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -252,27 +318,35 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
ProgramName = _currentProgram.Name;
|
ProgramName = _currentProgram.Name;
|
||||||
IsModified = false;
|
IsModified = false;
|
||||||
RefreshNodes();
|
RefreshNodes();
|
||||||
|
_logger.Info("程序已加载 | Program loaded: {ProgramName}", _currentProgram.Name);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.Error(ex, "Failed to load program");
|
_logger.Error(ex, "加载程序失败 | Failed to load program");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建新程序 | Create a new program
|
||||||
|
/// </summary>
|
||||||
private void ExecuteNewProgram()
|
private void ExecuteNewProgram()
|
||||||
{
|
{
|
||||||
var name = string.IsNullOrWhiteSpace(ProgramName) ? "NewCncProgram" : ProgramName;
|
var name = string.IsNullOrWhiteSpace(ProgramName) ? "新程序" : ProgramName;
|
||||||
_currentProgram = _cncProgramService.CreateProgram(name);
|
_currentProgram = _cncProgramService.CreateProgram(name);
|
||||||
ProgramName = _currentProgram.Name;
|
ProgramName = _currentProgram.Name;
|
||||||
IsModified = false;
|
IsModified = false;
|
||||||
RefreshNodes();
|
RefreshNodes();
|
||||||
|
_logger.Info("已创建新程序 | Created new program: {ProgramName}", name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 导出当前程序为 CSV 文件 | Export current program to CSV file
|
||||||
|
/// </summary>
|
||||||
private void ExecuteExportCsv()
|
private void ExecuteExportCsv()
|
||||||
{
|
{
|
||||||
if (_currentProgram == null || _currentProgram.Nodes.Count == 0)
|
if (_currentProgram == null || _currentProgram.Nodes.Count == 0)
|
||||||
{
|
{
|
||||||
_logger.Warn("Cannot export CSV: no program or empty nodes");
|
_logger.Warn("无法导出 CSV:当前无程序或节点为空 | Cannot export CSV: no program or empty nodes");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,7 +364,8 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.AppendLine("Index,NodeType,Name,XM,YM,ZT,ZD,TiltD,Dist,Voltage_kV,Current_uA,Power_W,RayOn,DetectorConnected,FrameRate,Resolution,ImageFile,MarkerType,MarkerX,MarkerY,DialogTitle,DialogMessage,DelayMs,Pipeline");
|
// CSV 表头 | CSV header
|
||||||
|
sb.AppendLine("Index,NodeType,Name,XM,YM,ZT,ZD,TiltD,Dist,Voltage_kV,Power_W,RayOn,DetectorConnected,FrameRate,Resolution,ImageFile,MarkerType,MarkerX,MarkerY,DialogTitle,DialogMessage,DelayMs,Pipeline");
|
||||||
|
|
||||||
var inv = CultureInfo.InvariantCulture;
|
var inv = CultureInfo.InvariantCulture;
|
||||||
|
|
||||||
@@ -298,15 +373,24 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
{
|
{
|
||||||
var row = node switch
|
var row = node switch
|
||||||
{
|
{
|
||||||
ReferencePointNode rp => $"{rp.Index},{rp.NodeType},{Esc(rp.Name)},{rp.XM.ToString(inv)},{rp.YM.ToString(inv)},{rp.ZT.ToString(inv)},{rp.ZD.ToString(inv)},{rp.TiltD.ToString(inv)},{rp.Dist.ToString(inv)},{rp.Voltage.ToString(inv)},{rp.Current.ToString(inv)},,{rp.IsRayOn},,,,,,,,,,",
|
ReferencePointNode rp => $"{rp.Index},{rp.NodeType},{Esc(rp.Name)},{rp.XM.ToString(inv)},{rp.YM.ToString(inv)},{rp.ZT.ToString(inv)},{rp.ZD.ToString(inv)},{rp.TiltD.ToString(inv)},{rp.Dist.ToString(inv)},,,,,,,,,,,,,,",
|
||||||
SaveNodeWithImageNode sni => $"{sni.Index},{sni.NodeType},{Esc(sni.Name)},{sni.MotionState.XM.ToString(inv)},{sni.MotionState.YM.ToString(inv)},{sni.MotionState.ZT.ToString(inv)},{sni.MotionState.ZD.ToString(inv)},{sni.MotionState.TiltD.ToString(inv)},{sni.MotionState.Dist.ToString(inv)},{sni.RaySourceState.Voltage.ToString(inv)},,{sni.RaySourceState.Power.ToString(inv)},{sni.RaySourceState.IsOn},{sni.DetectorState.IsConnected},{sni.DetectorState.FrameRate.ToString(inv)},{Esc(sni.DetectorState.Resolution)},{Esc(sni.ImageFileName)},,,,,,",
|
|
||||||
SaveNodeNode sn => $"{sn.Index},{sn.NodeType},{Esc(sn.Name)},{sn.MotionState.XM.ToString(inv)},{sn.MotionState.YM.ToString(inv)},{sn.MotionState.ZT.ToString(inv)},{sn.MotionState.ZD.ToString(inv)},{sn.MotionState.TiltD.ToString(inv)},{sn.MotionState.Dist.ToString(inv)},{sn.RaySourceState.Voltage.ToString(inv)},,{sn.RaySourceState.Power.ToString(inv)},{sn.RaySourceState.IsOn},{sn.DetectorState.IsConnected},{sn.DetectorState.FrameRate.ToString(inv)},{Esc(sn.DetectorState.Resolution)},,,,,,,",
|
SaveNodeWithImageNode sni => $"{sni.Index},{sni.NodeType},{Esc(sni.Name)},{sni.MotionState.XM.ToString(inv)},{sni.MotionState.YM.ToString(inv)},{sni.MotionState.ZT.ToString(inv)},{sni.MotionState.ZD.ToString(inv)},{sni.MotionState.TiltD.ToString(inv)},{sni.MotionState.Dist.ToString(inv)},{sni.RaySourceState.Voltage.ToString(inv)},{sni.RaySourceState.Power.ToString(inv)},{sni.RaySourceState.IsOn},{sni.DetectorState.IsConnected},{sni.DetectorState.FrameRate.ToString(inv)},{Esc(sni.DetectorState.Resolution)},{Esc(sni.ImageFileName)},,,,,,",
|
||||||
|
|
||||||
|
SaveNodeNode sn => $"{sn.Index},{sn.NodeType},{Esc(sn.Name)},{sn.MotionState.XM.ToString(inv)},{sn.MotionState.YM.ToString(inv)},{sn.MotionState.ZT.ToString(inv)},{sn.MotionState.ZD.ToString(inv)},{sn.MotionState.TiltD.ToString(inv)},{sn.MotionState.Dist.ToString(inv)},{sn.RaySourceState.Voltage.ToString(inv)},{sn.RaySourceState.Power.ToString(inv)},{sn.RaySourceState.IsOn},{sn.DetectorState.IsConnected},{sn.DetectorState.FrameRate.ToString(inv)},{Esc(sn.DetectorState.Resolution)},,,,,,,",
|
||||||
|
|
||||||
SavePositionNode sp => $"{sp.Index},{sp.NodeType},{Esc(sp.Name)},{sp.MotionState.XM.ToString(inv)},{sp.MotionState.YM.ToString(inv)},{sp.MotionState.ZT.ToString(inv)},{sp.MotionState.ZD.ToString(inv)},{sp.MotionState.TiltD.ToString(inv)},{sp.MotionState.Dist.ToString(inv)},,,,,,,,,,,,,,",
|
SavePositionNode sp => $"{sp.Index},{sp.NodeType},{Esc(sp.Name)},{sp.MotionState.XM.ToString(inv)},{sp.MotionState.YM.ToString(inv)},{sp.MotionState.ZT.ToString(inv)},{sp.MotionState.ZD.ToString(inv)},{sp.MotionState.TiltD.ToString(inv)},{sp.MotionState.Dist.ToString(inv)},,,,,,,,,,,,,,",
|
||||||
|
|
||||||
InspectionModuleNode im => $"{im.Index},{im.NodeType},{Esc(im.Name)},,,,,,,,,,,,,,,,,,,{Esc(im.Pipeline?.Name ?? string.Empty)}",
|
InspectionModuleNode im => $"{im.Index},{im.NodeType},{Esc(im.Name)},,,,,,,,,,,,,,,,,,,{Esc(im.Pipeline?.Name ?? string.Empty)}",
|
||||||
|
|
||||||
InspectionMarkerNode mk => $"{mk.Index},{mk.NodeType},{Esc(mk.Name)},,,,,,,,,,,,,,{Esc(mk.MarkerType)},{mk.MarkerX.ToString(inv)},{mk.MarkerY.ToString(inv)},,,",
|
InspectionMarkerNode mk => $"{mk.Index},{mk.NodeType},{Esc(mk.Name)},,,,,,,,,,,,,,{Esc(mk.MarkerType)},{mk.MarkerX.ToString(inv)},{mk.MarkerY.ToString(inv)},,,",
|
||||||
|
|
||||||
PauseDialogNode pd => $"{pd.Index},{pd.NodeType},{Esc(pd.Name)},,,,,,,,,,,,,,,,,{Esc(pd.DialogTitle)},{Esc(pd.DialogMessage)},,",
|
PauseDialogNode pd => $"{pd.Index},{pd.NodeType},{Esc(pd.Name)},,,,,,,,,,,,,,,,,{Esc(pd.DialogTitle)},{Esc(pd.DialogMessage)},,",
|
||||||
|
|
||||||
WaitDelayNode wd => $"{wd.Index},{wd.NodeType},{Esc(wd.Name)},,,,,,,,,,,,,,,,,,,{wd.DelayMilliseconds},",
|
WaitDelayNode wd => $"{wd.Index},{wd.NodeType},{Esc(wd.Name)},,,,,,,,,,,,,,,,,,,{wd.DelayMilliseconds},",
|
||||||
|
|
||||||
CompleteProgramNode cp => $"{cp.Index},{cp.NodeType},{Esc(cp.Name)},,,,,,,,,,,,,,,,,,,,",
|
CompleteProgramNode cp => $"{cp.Index},{cp.NodeType},{Esc(cp.Name)},,,,,,,,,,,,,,,,,,,,",
|
||||||
|
|
||||||
_ => $"{node.Index},{node.NodeType},{Esc(node.Name)},,,,,,,,,,,,,,,,,,,,"
|
_ => $"{node.Index},{node.NodeType},{Esc(node.Name)},,,,,,,,,,,,,,,,,,,,"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -314,13 +398,18 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
}
|
}
|
||||||
|
|
||||||
File.WriteAllText(dlg.FileName, sb.ToString(), Encoding.UTF8);
|
File.WriteAllText(dlg.FileName, sb.ToString(), Encoding.UTF8);
|
||||||
|
_logger.Info("CSV 已导出 | CSV exported: {FilePath}, 节点数={Count}", dlg.FileName, _currentProgram.Nodes.Count);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.Error(ex, "Failed to export CSV");
|
_logger.Error(ex, "导出 CSV 失败 | Failed to export CSV");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// CSV 字段转义:含逗号、引号或换行时用双引号包裹
|
||||||
|
/// Escape CSV field: wrap with double quotes if it contains comma, quote, or newline
|
||||||
|
/// </summary>
|
||||||
private static string Esc(string value)
|
private static string Esc(string value)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(value)) return string.Empty;
|
if (string.IsNullOrEmpty(value)) return string.Empty;
|
||||||
@@ -329,6 +418,12 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 辅助方法 | Helper methods ───────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 编辑操作后的统一处理:刷新节点、标记已修改、发布变更事件
|
||||||
|
/// Unified post-edit handling: refresh nodes, mark modified, publish change event
|
||||||
|
/// </summary>
|
||||||
private void OnProgramEdited()
|
private void OnProgramEdited()
|
||||||
{
|
{
|
||||||
IsModified = true;
|
IsModified = true;
|
||||||
@@ -336,70 +431,34 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
PublishProgramChanged();
|
PublishProgramChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleNodeModelChanged(CncNodeViewModel nodeVm, CncNode updatedNode)
|
/// <summary>
|
||||||
{
|
/// 从 _currentProgram.Nodes 重建 Nodes 集合
|
||||||
if (_currentProgram == null)
|
/// Rebuild the Nodes collection from _currentProgram.Nodes
|
||||||
return;
|
/// </summary>
|
||||||
|
|
||||||
_currentProgram = _cncProgramService.UpdateNode(_currentProgram, nodeVm.Index, updatedNode);
|
|
||||||
IsModified = true;
|
|
||||||
ProgramName = _currentProgram.Name;
|
|
||||||
PublishProgramChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RefreshNodes()
|
private void RefreshNodes()
|
||||||
{
|
{
|
||||||
var selectedId = SelectedNode?.Id;
|
Nodes.Clear();
|
||||||
var expansionState = Nodes.ToDictionary(node => node.Id, node => node.IsExpanded);
|
|
||||||
|
|
||||||
var flatNodes = new List<CncNodeViewModel>();
|
if (_currentProgram?.Nodes == null)
|
||||||
var rootNodes = new List<CncNodeViewModel>();
|
return;
|
||||||
CncNodeViewModel currentModule = null;
|
|
||||||
|
|
||||||
if (_currentProgram?.Nodes != null)
|
foreach (var node in _currentProgram.Nodes)
|
||||||
{
|
{
|
||||||
foreach (var node in _currentProgram.Nodes)
|
Nodes.Add(new CncNodeViewModel(node));
|
||||||
{
|
|
||||||
var vm = new CncNodeViewModel(node, HandleNodeModelChanged)
|
|
||||||
{
|
|
||||||
IsExpanded = expansionState.TryGetValue(node.Id, out var isExpanded) ? isExpanded : true
|
|
||||||
};
|
|
||||||
|
|
||||||
flatNodes.Add(vm);
|
|
||||||
|
|
||||||
if (vm.IsInspectionModule)
|
|
||||||
{
|
|
||||||
rootNodes.Add(vm);
|
|
||||||
currentModule = vm;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentModule != null && IsModuleChild(vm.NodeType))
|
|
||||||
{
|
|
||||||
currentModule.Children.Add(vm);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
rootNodes.Add(vm);
|
|
||||||
currentModule = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Nodes = new ObservableCollection<CncNodeViewModel>(flatNodes);
|
// 尝试保持选中状态 | Try to preserve selection
|
||||||
TreeNodes = new ObservableCollection<CncNodeViewModel>(rootNodes);
|
if (SelectedNode != null)
|
||||||
|
{
|
||||||
SelectedNode = selectedId.HasValue
|
var match = Nodes.FirstOrDefault(n => n.Index == SelectedNode.Index);
|
||||||
? Nodes.FirstOrDefault(node => node.Id == selectedId.Value) ?? Nodes.LastOrDefault()
|
SelectedNode = match ?? Nodes.LastOrDefault();
|
||||||
: Nodes.LastOrDefault();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsModuleChild(CncNodeType type)
|
|
||||||
{
|
|
||||||
return type is CncNodeType.InspectionMarker
|
|
||||||
or CncNodeType.PauseDialog
|
|
||||||
or CncNodeType.WaitDelay;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过 IEventAggregator 发布 CNC 程序变更事件
|
||||||
|
/// Publish CNC program changed event via IEventAggregator
|
||||||
|
/// </summary>
|
||||||
private void PublishProgramChanged()
|
private void PublishProgramChanged()
|
||||||
{
|
{
|
||||||
_eventAggregator
|
_eventAggregator
|
||||||
@@ -407,4 +466,4 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
.Publish(new CncProgramChangedPayload(ProgramName ?? string.Empty, IsModified));
|
.Publish(new CncProgramChangedPayload(ProgramName ?? string.Empty, IsModified));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,543 +1,89 @@
|
|||||||
using Prism.Mvvm;
|
using Prism.Mvvm;
|
||||||
using System;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using XplorePlane.Models;
|
using XplorePlane.Models;
|
||||||
|
|
||||||
namespace XplorePlane.ViewModels.Cnc
|
namespace XplorePlane.ViewModels.Cnc
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// CNC node ViewModel with editable properties and tree children.
|
/// CNC 节点 ViewModel,将 CncNode 模型封装为可绑定的 WPF ViewModel
|
||||||
|
/// CNC node ViewModel that wraps a CncNode model into a bindable WPF ViewModel
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CncNodeViewModel : BindableBase
|
public class CncNodeViewModel : BindableBase
|
||||||
{
|
{
|
||||||
private readonly Action<CncNodeViewModel, CncNode> _modelChangedCallback;
|
private int _index;
|
||||||
private CncNode _model;
|
private string _name;
|
||||||
|
private CncNodeType _nodeType;
|
||||||
private string _icon;
|
private string _icon;
|
||||||
private bool _isExpanded = true;
|
|
||||||
|
|
||||||
public CncNodeViewModel(CncNode model, Action<CncNodeViewModel, CncNode> modelChangedCallback)
|
/// <summary>
|
||||||
|
/// 构造函数,从 CncNode 模型初始化 ViewModel
|
||||||
|
/// Constructor that initializes the ViewModel from a CncNode model
|
||||||
|
/// </summary>
|
||||||
|
public CncNodeViewModel(CncNode model)
|
||||||
{
|
{
|
||||||
_model = model ?? throw new ArgumentNullException(nameof(model));
|
Model = model;
|
||||||
_modelChangedCallback = modelChangedCallback ?? throw new ArgumentNullException(nameof(modelChangedCallback));
|
_index = model.Index;
|
||||||
|
_name = model.Name;
|
||||||
|
_nodeType = model.NodeType;
|
||||||
_icon = GetIconForNodeType(model.NodeType);
|
_icon = GetIconForNodeType(model.NodeType);
|
||||||
Children = new ObservableCollection<CncNodeViewModel>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObservableCollection<CncNodeViewModel> Children { get; }
|
/// <summary>底层 CNC 节点模型(只读)| Underlying CNC node model (read-only)</summary>
|
||||||
|
public CncNode Model { get; }
|
||||||
|
|
||||||
public CncNode Model => _model;
|
/// <summary>节点在程序中的索引 | Node index in the program</summary>
|
||||||
|
public int Index
|
||||||
public Guid Id => _model.Id;
|
{
|
||||||
|
get => _index;
|
||||||
public int Index => _model.Index;
|
set => SetProperty(ref _index, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>节点显示名称 | Node display name</summary>
|
||||||
public string Name
|
public string Name
|
||||||
{
|
{
|
||||||
get => _model.Name;
|
get => _name;
|
||||||
set => UpdateModel(_model with { Name = value ?? string.Empty });
|
set => SetProperty(ref _name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CncNodeType NodeType => _model.NodeType;
|
/// <summary>节点类型 | Node type</summary>
|
||||||
|
public CncNodeType NodeType
|
||||||
public string NodeTypeDisplay => NodeType.ToString();
|
{
|
||||||
|
get => _nodeType;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _nodeType, value))
|
||||||
|
{
|
||||||
|
// 类型变更时自动更新图标 | Auto-update icon when type changes
|
||||||
|
Icon = GetIconForNodeType(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>节点图标路径 | Node icon path</summary>
|
||||||
public string Icon
|
public string Icon
|
||||||
{
|
{
|
||||||
get => _icon;
|
get => _icon;
|
||||||
private set => SetProperty(ref _icon, value);
|
set => SetProperty(ref _icon, value);
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsExpanded
|
|
||||||
{
|
|
||||||
get => _isExpanded;
|
|
||||||
set => SetProperty(ref _isExpanded, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool HasChildren => Children.Count > 0;
|
|
||||||
|
|
||||||
public bool IsReferencePoint => _model is ReferencePointNode;
|
|
||||||
public bool IsSaveNode => _model is SaveNodeNode;
|
|
||||||
public bool IsSaveNodeWithImage => _model is SaveNodeWithImageNode;
|
|
||||||
public bool IsSavePosition => _model is SavePositionNode;
|
|
||||||
public bool IsInspectionModule => _model is InspectionModuleNode;
|
|
||||||
public bool IsInspectionMarker => _model is InspectionMarkerNode;
|
|
||||||
public bool IsPauseDialog => _model is PauseDialogNode;
|
|
||||||
public bool IsWaitDelay => _model is WaitDelayNode;
|
|
||||||
public bool IsCompleteProgram => _model is CompleteProgramNode;
|
|
||||||
public bool IsMotionSnapshotNode => _model is ReferencePointNode or SaveNodeNode or SaveNodeWithImageNode or SavePositionNode;
|
|
||||||
|
|
||||||
public double XM
|
|
||||||
{
|
|
||||||
get => _model switch
|
|
||||||
{
|
|
||||||
ReferencePointNode rp => rp.XM,
|
|
||||||
SaveNodeNode sn => sn.MotionState.XM,
|
|
||||||
SaveNodeWithImageNode sni => sni.MotionState.XM,
|
|
||||||
SavePositionNode sp => sp.MotionState.XM,
|
|
||||||
_ => 0d
|
|
||||||
};
|
|
||||||
set => UpdateMotion(value, MotionAxis.XM);
|
|
||||||
}
|
|
||||||
|
|
||||||
public double YM
|
|
||||||
{
|
|
||||||
get => _model switch
|
|
||||||
{
|
|
||||||
ReferencePointNode rp => rp.YM,
|
|
||||||
SaveNodeNode sn => sn.MotionState.YM,
|
|
||||||
SaveNodeWithImageNode sni => sni.MotionState.YM,
|
|
||||||
SavePositionNode sp => sp.MotionState.YM,
|
|
||||||
_ => 0d
|
|
||||||
};
|
|
||||||
set => UpdateMotion(value, MotionAxis.YM);
|
|
||||||
}
|
|
||||||
|
|
||||||
public double ZT
|
|
||||||
{
|
|
||||||
get => _model switch
|
|
||||||
{
|
|
||||||
ReferencePointNode rp => rp.ZT,
|
|
||||||
SaveNodeNode sn => sn.MotionState.ZT,
|
|
||||||
SaveNodeWithImageNode sni => sni.MotionState.ZT,
|
|
||||||
SavePositionNode sp => sp.MotionState.ZT,
|
|
||||||
_ => 0d
|
|
||||||
};
|
|
||||||
set => UpdateMotion(value, MotionAxis.ZT);
|
|
||||||
}
|
|
||||||
|
|
||||||
public double ZD
|
|
||||||
{
|
|
||||||
get => _model switch
|
|
||||||
{
|
|
||||||
ReferencePointNode rp => rp.ZD,
|
|
||||||
SaveNodeNode sn => sn.MotionState.ZD,
|
|
||||||
SaveNodeWithImageNode sni => sni.MotionState.ZD,
|
|
||||||
SavePositionNode sp => sp.MotionState.ZD,
|
|
||||||
_ => 0d
|
|
||||||
};
|
|
||||||
set => UpdateMotion(value, MotionAxis.ZD);
|
|
||||||
}
|
|
||||||
|
|
||||||
public double TiltD
|
|
||||||
{
|
|
||||||
get => _model switch
|
|
||||||
{
|
|
||||||
ReferencePointNode rp => rp.TiltD,
|
|
||||||
SaveNodeNode sn => sn.MotionState.TiltD,
|
|
||||||
SaveNodeWithImageNode sni => sni.MotionState.TiltD,
|
|
||||||
SavePositionNode sp => sp.MotionState.TiltD,
|
|
||||||
_ => 0d
|
|
||||||
};
|
|
||||||
set => UpdateMotion(value, MotionAxis.TiltD);
|
|
||||||
}
|
|
||||||
|
|
||||||
public double Dist
|
|
||||||
{
|
|
||||||
get => _model switch
|
|
||||||
{
|
|
||||||
ReferencePointNode rp => rp.Dist,
|
|
||||||
SaveNodeNode sn => sn.MotionState.Dist,
|
|
||||||
SaveNodeWithImageNode sni => sni.MotionState.Dist,
|
|
||||||
SavePositionNode sp => sp.MotionState.Dist,
|
|
||||||
_ => 0d
|
|
||||||
};
|
|
||||||
set => UpdateMotion(value, MotionAxis.Dist);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsRayOn
|
|
||||||
{
|
|
||||||
get => _model switch
|
|
||||||
{
|
|
||||||
ReferencePointNode rp => rp.IsRayOn,
|
|
||||||
SaveNodeNode sn => sn.RaySourceState.IsOn,
|
|
||||||
SaveNodeWithImageNode sni => sni.RaySourceState.IsOn,
|
|
||||||
_ => false
|
|
||||||
};
|
|
||||||
set => UpdateRaySource(isOn: value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public double Voltage
|
|
||||||
{
|
|
||||||
get => _model switch
|
|
||||||
{
|
|
||||||
ReferencePointNode rp => rp.Voltage,
|
|
||||||
SaveNodeNode sn => sn.RaySourceState.Voltage,
|
|
||||||
SaveNodeWithImageNode sni => sni.RaySourceState.Voltage,
|
|
||||||
_ => 0d
|
|
||||||
};
|
|
||||||
set => UpdateRaySource(voltage: value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public double Current
|
|
||||||
{
|
|
||||||
get => _model is ReferencePointNode rp ? rp.Current : 0d;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_model is ReferencePointNode rp)
|
|
||||||
{
|
|
||||||
UpdateModel(rp with { Current = value });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public double Power
|
|
||||||
{
|
|
||||||
get => _model switch
|
|
||||||
{
|
|
||||||
SaveNodeNode sn => sn.RaySourceState.Power,
|
|
||||||
SaveNodeWithImageNode sni => sni.RaySourceState.Power,
|
|
||||||
_ => 0d
|
|
||||||
};
|
|
||||||
set => UpdateRaySource(power: value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool DetectorConnected
|
|
||||||
{
|
|
||||||
get => _model switch
|
|
||||||
{
|
|
||||||
SaveNodeNode sn => sn.DetectorState.IsConnected,
|
|
||||||
SaveNodeWithImageNode sni => sni.DetectorState.IsConnected,
|
|
||||||
_ => false
|
|
||||||
};
|
|
||||||
set => UpdateDetector(isConnected: value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool DetectorAcquiring
|
|
||||||
{
|
|
||||||
get => _model switch
|
|
||||||
{
|
|
||||||
SaveNodeNode sn => sn.DetectorState.IsAcquiring,
|
|
||||||
SaveNodeWithImageNode sni => sni.DetectorState.IsAcquiring,
|
|
||||||
_ => false
|
|
||||||
};
|
|
||||||
set => UpdateDetector(isAcquiring: value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public double FrameRate
|
|
||||||
{
|
|
||||||
get => _model switch
|
|
||||||
{
|
|
||||||
SaveNodeNode sn => sn.DetectorState.FrameRate,
|
|
||||||
SaveNodeWithImageNode sni => sni.DetectorState.FrameRate,
|
|
||||||
_ => 0d
|
|
||||||
};
|
|
||||||
set => UpdateDetector(frameRate: value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Resolution
|
|
||||||
{
|
|
||||||
get => _model switch
|
|
||||||
{
|
|
||||||
SaveNodeNode sn => sn.DetectorState.Resolution,
|
|
||||||
SaveNodeWithImageNode sni => sni.DetectorState.Resolution,
|
|
||||||
_ => string.Empty
|
|
||||||
};
|
|
||||||
set => UpdateDetector(resolution: value ?? string.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ImageFileName
|
|
||||||
{
|
|
||||||
get => _model is SaveNodeWithImageNode sni ? sni.ImageFileName : string.Empty;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_model is SaveNodeWithImageNode sni)
|
|
||||||
{
|
|
||||||
UpdateModel(sni with { ImageFileName = value ?? string.Empty });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string PipelineName
|
|
||||||
{
|
|
||||||
get => _model is InspectionModuleNode im ? im.Pipeline?.Name ?? string.Empty : string.Empty;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_model is InspectionModuleNode im)
|
|
||||||
{
|
|
||||||
var pipeline = im.Pipeline ?? new PipelineModel();
|
|
||||||
pipeline.Name = value ?? string.Empty;
|
|
||||||
UpdateModel(im with { Pipeline = pipeline });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public PipelineModel Pipeline
|
|
||||||
{
|
|
||||||
get => _model is InspectionModuleNode im ? im.Pipeline : null;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_model is InspectionModuleNode im)
|
|
||||||
{
|
|
||||||
UpdateModel(im with { Pipeline = value ?? new PipelineModel() });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string MarkerType
|
|
||||||
{
|
|
||||||
get => _model is InspectionMarkerNode mk ? mk.MarkerType : string.Empty;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_model is InspectionMarkerNode mk)
|
|
||||||
{
|
|
||||||
UpdateModel(mk with { MarkerType = value ?? string.Empty });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public double MarkerX
|
|
||||||
{
|
|
||||||
get => _model is InspectionMarkerNode mk ? mk.MarkerX : 0d;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_model is InspectionMarkerNode mk)
|
|
||||||
{
|
|
||||||
UpdateModel(mk with { MarkerX = value });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public double MarkerY
|
|
||||||
{
|
|
||||||
get => _model is InspectionMarkerNode mk ? mk.MarkerY : 0d;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_model is InspectionMarkerNode mk)
|
|
||||||
{
|
|
||||||
UpdateModel(mk with { MarkerY = value });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string DialogTitle
|
|
||||||
{
|
|
||||||
get => _model is PauseDialogNode pd ? pd.DialogTitle : string.Empty;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_model is PauseDialogNode pd)
|
|
||||||
{
|
|
||||||
UpdateModel(pd with { DialogTitle = value ?? string.Empty });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string DialogMessage
|
|
||||||
{
|
|
||||||
get => _model is PauseDialogNode pd ? pd.DialogMessage : string.Empty;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_model is PauseDialogNode pd)
|
|
||||||
{
|
|
||||||
UpdateModel(pd with { DialogMessage = value ?? string.Empty });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int DelayMilliseconds
|
|
||||||
{
|
|
||||||
get => _model is WaitDelayNode wd ? wd.DelayMilliseconds : 0;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_model is WaitDelayNode wd)
|
|
||||||
{
|
|
||||||
UpdateModel(wd with { DelayMilliseconds = value });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ReplaceModel(CncNode model)
|
|
||||||
{
|
|
||||||
_model = model ?? throw new ArgumentNullException(nameof(model));
|
|
||||||
Icon = GetIconForNodeType(model.NodeType);
|
|
||||||
RaiseAllPropertiesChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据节点类型返回对应的图标路径
|
||||||
|
/// Returns the icon path for the given node type
|
||||||
|
/// </summary>
|
||||||
public static string GetIconForNodeType(CncNodeType nodeType)
|
public static string GetIconForNodeType(CncNodeType nodeType)
|
||||||
{
|
{
|
||||||
return nodeType switch
|
return nodeType switch
|
||||||
{
|
{
|
||||||
CncNodeType.ReferencePoint => "/Assets/Icons/reference.png",
|
CncNodeType.ReferencePoint => "/Resources/Icons/cnc_reference_point.png",
|
||||||
CncNodeType.SaveNodeWithImage => "/Assets/Icons/saveall.png",
|
CncNodeType.SaveNodeWithImage => "/Resources/Icons/cnc_save_with_image.png",
|
||||||
CncNodeType.SaveNode => "/Assets/Icons/save.png",
|
CncNodeType.SaveNode => "/Resources/Icons/cnc_save_node.png",
|
||||||
CncNodeType.SavePosition => "/Assets/Icons/add-pos.png",
|
CncNodeType.SavePosition => "/Resources/Icons/cnc_save_position.png",
|
||||||
CncNodeType.InspectionModule => "/Assets/Icons/Module.png",
|
CncNodeType.InspectionModule => "/Resources/Icons/cnc_inspection_module.png",
|
||||||
CncNodeType.InspectionMarker => "/Assets/Icons/mark.png",
|
CncNodeType.InspectionMarker => "/Resources/Icons/cnc_inspection_marker.png",
|
||||||
CncNodeType.PauseDialog => "/Assets/Icons/message.png",
|
CncNodeType.PauseDialog => "/Resources/Icons/cnc_pause_dialog.png",
|
||||||
CncNodeType.WaitDelay => "/Assets/Icons/wait.png",
|
CncNodeType.WaitDelay => "/Resources/Icons/cnc_wait_delay.png",
|
||||||
CncNodeType.CompleteProgram => "/Assets/Icons/finish.png",
|
CncNodeType.CompleteProgram => "/Resources/Icons/cnc_complete_program.png",
|
||||||
_ => "/Assets/Icons/cnc.png",
|
_ => "/Resources/Icons/cnc_default.png",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateMotion(double value, MotionAxis axis)
|
|
||||||
{
|
|
||||||
switch (_model)
|
|
||||||
{
|
|
||||||
case ReferencePointNode rp:
|
|
||||||
UpdateModel(axis switch
|
|
||||||
{
|
|
||||||
MotionAxis.XM => rp with { XM = value },
|
|
||||||
MotionAxis.YM => rp with { YM = value },
|
|
||||||
MotionAxis.ZT => rp with { ZT = value },
|
|
||||||
MotionAxis.ZD => rp with { ZD = value },
|
|
||||||
MotionAxis.TiltD => rp with { TiltD = value },
|
|
||||||
MotionAxis.Dist => rp with { Dist = value },
|
|
||||||
_ => rp
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case SaveNodeNode sn:
|
|
||||||
UpdateModel(sn with { MotionState = UpdateMotionState(sn.MotionState, axis, value) });
|
|
||||||
break;
|
|
||||||
case SaveNodeWithImageNode sni:
|
|
||||||
UpdateModel(sni with { MotionState = UpdateMotionState(sni.MotionState, axis, value) });
|
|
||||||
break;
|
|
||||||
case SavePositionNode sp:
|
|
||||||
UpdateModel(sp with { MotionState = UpdateMotionState(sp.MotionState, axis, value) });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateRaySource(bool? isOn = null, double? voltage = null, double? current = null, double? power = null)
|
|
||||||
{
|
|
||||||
switch (_model)
|
|
||||||
{
|
|
||||||
case ReferencePointNode rp:
|
|
||||||
UpdateModel(rp with
|
|
||||||
{
|
|
||||||
IsRayOn = isOn ?? rp.IsRayOn,
|
|
||||||
Voltage = voltage ?? rp.Voltage,
|
|
||||||
Current = current ?? rp.Current
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case SaveNodeNode sn:
|
|
||||||
UpdateModel(sn with
|
|
||||||
{
|
|
||||||
RaySourceState = sn.RaySourceState with
|
|
||||||
{
|
|
||||||
IsOn = isOn ?? sn.RaySourceState.IsOn,
|
|
||||||
Voltage = voltage ?? sn.RaySourceState.Voltage,
|
|
||||||
Power = power ?? sn.RaySourceState.Power
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case SaveNodeWithImageNode sni:
|
|
||||||
UpdateModel(sni with
|
|
||||||
{
|
|
||||||
RaySourceState = sni.RaySourceState with
|
|
||||||
{
|
|
||||||
IsOn = isOn ?? sni.RaySourceState.IsOn,
|
|
||||||
Voltage = voltage ?? sni.RaySourceState.Voltage,
|
|
||||||
Power = power ?? sni.RaySourceState.Power
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateDetector(bool? isConnected = null, bool? isAcquiring = null, double? frameRate = null, string resolution = null)
|
|
||||||
{
|
|
||||||
switch (_model)
|
|
||||||
{
|
|
||||||
case SaveNodeNode sn:
|
|
||||||
UpdateModel(sn with
|
|
||||||
{
|
|
||||||
DetectorState = sn.DetectorState with
|
|
||||||
{
|
|
||||||
IsConnected = isConnected ?? sn.DetectorState.IsConnected,
|
|
||||||
IsAcquiring = isAcquiring ?? sn.DetectorState.IsAcquiring,
|
|
||||||
FrameRate = frameRate ?? sn.DetectorState.FrameRate,
|
|
||||||
Resolution = resolution ?? sn.DetectorState.Resolution
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case SaveNodeWithImageNode sni:
|
|
||||||
UpdateModel(sni with
|
|
||||||
{
|
|
||||||
DetectorState = sni.DetectorState with
|
|
||||||
{
|
|
||||||
IsConnected = isConnected ?? sni.DetectorState.IsConnected,
|
|
||||||
IsAcquiring = isAcquiring ?? sni.DetectorState.IsAcquiring,
|
|
||||||
FrameRate = frameRate ?? sni.DetectorState.FrameRate,
|
|
||||||
Resolution = resolution ?? sni.DetectorState.Resolution
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MotionState UpdateMotionState(MotionState state, MotionAxis axis, double value)
|
|
||||||
{
|
|
||||||
return axis switch
|
|
||||||
{
|
|
||||||
MotionAxis.XM => state with { XM = value },
|
|
||||||
MotionAxis.YM => state with { YM = value },
|
|
||||||
MotionAxis.ZT => state with { ZT = value },
|
|
||||||
MotionAxis.ZD => state with { ZD = value },
|
|
||||||
MotionAxis.TiltD => state with { TiltD = value },
|
|
||||||
MotionAxis.Dist => state with { Dist = value },
|
|
||||||
_ => state
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateModel(CncNode updatedModel)
|
|
||||||
{
|
|
||||||
_model = updatedModel ?? throw new ArgumentNullException(nameof(updatedModel));
|
|
||||||
RaiseAllPropertiesChanged();
|
|
||||||
_modelChangedCallback(this, updatedModel);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RaiseAllPropertiesChanged()
|
|
||||||
{
|
|
||||||
RaisePropertyChanged(nameof(Model));
|
|
||||||
RaisePropertyChanged(nameof(Id));
|
|
||||||
RaisePropertyChanged(nameof(Index));
|
|
||||||
RaisePropertyChanged(nameof(Name));
|
|
||||||
RaisePropertyChanged(nameof(NodeType));
|
|
||||||
RaisePropertyChanged(nameof(NodeTypeDisplay));
|
|
||||||
RaisePropertyChanged(nameof(Icon));
|
|
||||||
RaisePropertyChanged(nameof(IsReferencePoint));
|
|
||||||
RaisePropertyChanged(nameof(IsSaveNode));
|
|
||||||
RaisePropertyChanged(nameof(IsSaveNodeWithImage));
|
|
||||||
RaisePropertyChanged(nameof(IsSavePosition));
|
|
||||||
RaisePropertyChanged(nameof(IsInspectionModule));
|
|
||||||
RaisePropertyChanged(nameof(IsInspectionMarker));
|
|
||||||
RaisePropertyChanged(nameof(IsPauseDialog));
|
|
||||||
RaisePropertyChanged(nameof(IsWaitDelay));
|
|
||||||
RaisePropertyChanged(nameof(IsCompleteProgram));
|
|
||||||
RaisePropertyChanged(nameof(IsMotionSnapshotNode));
|
|
||||||
RaisePropertyChanged(nameof(XM));
|
|
||||||
RaisePropertyChanged(nameof(YM));
|
|
||||||
RaisePropertyChanged(nameof(ZT));
|
|
||||||
RaisePropertyChanged(nameof(ZD));
|
|
||||||
RaisePropertyChanged(nameof(TiltD));
|
|
||||||
RaisePropertyChanged(nameof(Dist));
|
|
||||||
RaisePropertyChanged(nameof(IsRayOn));
|
|
||||||
RaisePropertyChanged(nameof(Voltage));
|
|
||||||
RaisePropertyChanged(nameof(Current));
|
|
||||||
RaisePropertyChanged(nameof(Power));
|
|
||||||
RaisePropertyChanged(nameof(DetectorConnected));
|
|
||||||
RaisePropertyChanged(nameof(DetectorAcquiring));
|
|
||||||
RaisePropertyChanged(nameof(FrameRate));
|
|
||||||
RaisePropertyChanged(nameof(Resolution));
|
|
||||||
RaisePropertyChanged(nameof(ImageFileName));
|
|
||||||
RaisePropertyChanged(nameof(Pipeline));
|
|
||||||
RaisePropertyChanged(nameof(PipelineName));
|
|
||||||
RaisePropertyChanged(nameof(MarkerType));
|
|
||||||
RaisePropertyChanged(nameof(MarkerX));
|
|
||||||
RaisePropertyChanged(nameof(MarkerY));
|
|
||||||
RaisePropertyChanged(nameof(DialogTitle));
|
|
||||||
RaisePropertyChanged(nameof(DialogMessage));
|
|
||||||
RaisePropertyChanged(nameof(DelayMilliseconds));
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum MotionAxis
|
|
||||||
{
|
|
||||||
XM,
|
|
||||||
YM,
|
|
||||||
ZT,
|
|
||||||
ZD,
|
|
||||||
TiltD,
|
|
||||||
Dist
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,6 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Input;
|
|
||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
using XP.Common.Logging.Interfaces;
|
using XP.Common.Logging.Interfaces;
|
||||||
using XplorePlane.Events;
|
using XplorePlane.Events;
|
||||||
@@ -17,7 +16,7 @@ using XplorePlane.Services;
|
|||||||
|
|
||||||
namespace XplorePlane.ViewModels
|
namespace XplorePlane.ViewModels
|
||||||
{
|
{
|
||||||
public class PipelineEditorViewModel : BindableBase, IPipelineEditorHostViewModel
|
public class PipelineEditorViewModel : BindableBase
|
||||||
{
|
{
|
||||||
private const int MaxPipelineLength = 20;
|
private const int MaxPipelineLength = 20;
|
||||||
private const int DebounceDelayMs = 300;
|
private const int DebounceDelayMs = 300;
|
||||||
@@ -166,15 +165,6 @@ namespace XplorePlane.ViewModels
|
|||||||
public DelegateCommand<PipelineNodeViewModel> MoveNodeUpCommand { get; }
|
public DelegateCommand<PipelineNodeViewModel> MoveNodeUpCommand { get; }
|
||||||
public DelegateCommand<PipelineNodeViewModel> MoveNodeDownCommand { get; }
|
public DelegateCommand<PipelineNodeViewModel> MoveNodeDownCommand { get; }
|
||||||
|
|
||||||
ICommand IPipelineEditorHostViewModel.AddOperatorCommand => AddOperatorCommand;
|
|
||||||
ICommand IPipelineEditorHostViewModel.RemoveOperatorCommand => RemoveOperatorCommand;
|
|
||||||
ICommand IPipelineEditorHostViewModel.MoveNodeUpCommand => MoveNodeUpCommand;
|
|
||||||
ICommand IPipelineEditorHostViewModel.MoveNodeDownCommand => MoveNodeDownCommand;
|
|
||||||
ICommand IPipelineEditorHostViewModel.NewPipelineCommand => NewPipelineCommand;
|
|
||||||
ICommand IPipelineEditorHostViewModel.SavePipelineCommand => SavePipelineCommand;
|
|
||||||
ICommand IPipelineEditorHostViewModel.SaveAsPipelineCommand => SaveAsPipelineCommand;
|
|
||||||
ICommand IPipelineEditorHostViewModel.LoadPipelineCommand => LoadPipelineCommand;
|
|
||||||
|
|
||||||
// ── Command Implementations ───────────────────────────────────
|
// ── Command Implementations ───────────────────────────────────
|
||||||
|
|
||||||
private bool CanAddOperator(string operatorKey) =>
|
private bool CanAddOperator(string operatorKey) =>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Prism.Commands;
|
using Prism.Commands;
|
||||||
using Prism.Events;
|
using Prism.Events;
|
||||||
using Prism.Ioc;
|
using Prism.Ioc;
|
||||||
using Prism.Mvvm;
|
using Prism.Mvvm;
|
||||||
@@ -10,9 +10,6 @@ using System.IO;
|
|||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
using XplorePlane.Events;
|
using XplorePlane.Events;
|
||||||
using XplorePlane.ViewModels.Cnc;
|
|
||||||
using XplorePlane.Views;
|
|
||||||
using XplorePlane.Views.Cnc;
|
|
||||||
using XP.Common.Logging.Interfaces;
|
using XP.Common.Logging.Interfaces;
|
||||||
using XP.Common.PdfViewer.Interfaces;
|
using XP.Common.PdfViewer.Interfaces;
|
||||||
using XP.Hardware.MotionControl.Abstractions;
|
using XP.Hardware.MotionControl.Abstractions;
|
||||||
@@ -21,12 +18,9 @@ namespace XplorePlane.ViewModels
|
|||||||
{
|
{
|
||||||
public class MainViewModel : BindableBase
|
public class MainViewModel : BindableBase
|
||||||
{
|
{
|
||||||
private const double CncEditorHostWidth = 710d;
|
|
||||||
private readonly ILoggerService _logger;
|
private readonly ILoggerService _logger;
|
||||||
private readonly IContainerProvider _containerProvider;
|
private readonly IContainerProvider _containerProvider;
|
||||||
private readonly IEventAggregator _eventAggregator;
|
private readonly IEventAggregator _eventAggregator;
|
||||||
private readonly CncEditorViewModel _cncEditorViewModel;
|
|
||||||
private readonly CncPageView _cncPageView;
|
|
||||||
private string _licenseInfo = "当前时间";
|
private string _licenseInfo = "当前时间";
|
||||||
|
|
||||||
public string LicenseInfo
|
public string LicenseInfo
|
||||||
@@ -55,17 +49,6 @@ namespace XplorePlane.ViewModels
|
|||||||
public DelegateCommand OpenLibraryVersionsCommand { get; }
|
public DelegateCommand OpenLibraryVersionsCommand { get; }
|
||||||
public DelegateCommand OpenUserManualCommand { get; }
|
public DelegateCommand OpenUserManualCommand { get; }
|
||||||
public DelegateCommand OpenCameraSettingsCommand { get; }
|
public DelegateCommand OpenCameraSettingsCommand { get; }
|
||||||
public DelegateCommand NewCncProgramCommand { get; }
|
|
||||||
public DelegateCommand SaveCncProgramCommand { get; }
|
|
||||||
public DelegateCommand LoadCncProgramCommand { get; }
|
|
||||||
public DelegateCommand InsertReferencePointCommand { get; }
|
|
||||||
public DelegateCommand InsertSavePositionCommand { get; }
|
|
||||||
public DelegateCommand InsertCompleteProgramCommand { get; }
|
|
||||||
public DelegateCommand InsertInspectionMarkerCommand { get; }
|
|
||||||
public DelegateCommand InsertInspectionModuleCommand { get; }
|
|
||||||
public DelegateCommand InsertSaveNodeCommand { get; }
|
|
||||||
public DelegateCommand InsertPauseDialogCommand { get; }
|
|
||||||
public DelegateCommand InsertWaitDelayCommand { get; }
|
|
||||||
|
|
||||||
// 硬件命令
|
// 硬件命令
|
||||||
public DelegateCommand AxisResetCommand { get; }
|
public DelegateCommand AxisResetCommand { get; }
|
||||||
@@ -79,27 +62,6 @@ namespace XplorePlane.ViewModels
|
|||||||
public DelegateCommand OpenLanguageSwitcherCommand { get; }
|
public DelegateCommand OpenLanguageSwitcherCommand { get; }
|
||||||
public DelegateCommand OpenRealTimeLogViewerCommand { get; }
|
public DelegateCommand OpenRealTimeLogViewerCommand { get; }
|
||||||
|
|
||||||
/// <summary>右侧图像区域内容 | Right-side image panel content</summary>
|
|
||||||
public object ImagePanelContent
|
|
||||||
{
|
|
||||||
get => _imagePanelContent;
|
|
||||||
set => SetProperty(ref _imagePanelContent, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>右侧图像区域宽度 | Right-side image panel width</summary>
|
|
||||||
public GridLength ImagePanelWidth
|
|
||||||
{
|
|
||||||
get => _imagePanelWidth;
|
|
||||||
set => SetProperty(ref _imagePanelWidth, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>主视图区宽度 | Main viewport width</summary>
|
|
||||||
public GridLength ViewportPanelWidth
|
|
||||||
{
|
|
||||||
get => _viewportPanelWidth;
|
|
||||||
set => SetProperty(ref _viewportPanelWidth, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 窗口引用(单例窗口防止重复打开)
|
// 窗口引用(单例窗口防止重复打开)
|
||||||
private Window _motionDebugWindow;
|
private Window _motionDebugWindow;
|
||||||
private Window _detectorConfigWindow;
|
private Window _detectorConfigWindow;
|
||||||
@@ -107,18 +69,12 @@ namespace XplorePlane.ViewModels
|
|||||||
private Window _realTimeLogViewerWindow;
|
private Window _realTimeLogViewerWindow;
|
||||||
private Window _toolboxWindow;
|
private Window _toolboxWindow;
|
||||||
private Window _raySourceConfigWindow;
|
private Window _raySourceConfigWindow;
|
||||||
private object _imagePanelContent;
|
|
||||||
private GridLength _viewportPanelWidth = new GridLength(1, GridUnitType.Star);
|
|
||||||
private GridLength _imagePanelWidth = new GridLength(320);
|
|
||||||
private bool _isCncEditorMode;
|
|
||||||
|
|
||||||
public MainViewModel(ILoggerService logger, IContainerProvider containerProvider, IEventAggregator eventAggregator)
|
public MainViewModel(ILoggerService logger, IContainerProvider containerProvider, IEventAggregator eventAggregator)
|
||||||
{
|
{
|
||||||
_logger = logger?.ForModule<MainViewModel>() ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger?.ForModule<MainViewModel>() ?? throw new ArgumentNullException(nameof(logger));
|
||||||
_containerProvider = containerProvider ?? throw new ArgumentNullException(nameof(containerProvider));
|
_containerProvider = containerProvider ?? throw new ArgumentNullException(nameof(containerProvider));
|
||||||
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
|
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
|
||||||
_cncEditorViewModel = _containerProvider.Resolve<CncEditorViewModel>();
|
|
||||||
_cncPageView = new CncPageView { DataContext = _cncEditorViewModel };
|
|
||||||
|
|
||||||
NavigationTree = new ObservableCollection<object>();
|
NavigationTree = new ObservableCollection<object>();
|
||||||
|
|
||||||
@@ -134,23 +90,12 @@ namespace XplorePlane.ViewModels
|
|||||||
OpenImageProcessingCommand = new DelegateCommand(() => ShowWindow(new Views.ImageProcessingWindow(), "图像处理"));
|
OpenImageProcessingCommand = new DelegateCommand(() => ShowWindow(new Views.ImageProcessingWindow(), "图像处理"));
|
||||||
LoadImageCommand = new DelegateCommand(ExecuteLoadImage);
|
LoadImageCommand = new DelegateCommand(ExecuteLoadImage);
|
||||||
OpenPipelineEditorCommand = new DelegateCommand(() => ShowWindow(new Views.PipelineEditorWindow(), "流水线编辑器"));
|
OpenPipelineEditorCommand = new DelegateCommand(() => ShowWindow(new Views.PipelineEditorWindow(), "流水线编辑器"));
|
||||||
OpenCncEditorCommand = new DelegateCommand(ExecuteOpenCncEditor);
|
OpenCncEditorCommand = new DelegateCommand(() => ShowWindow(new Views.Cnc.CncEditorWindow(), "CNC 编辑器"));
|
||||||
OpenMatrixEditorCommand = new DelegateCommand(() => ShowWindow(new Views.Cnc.MatrixEditorWindow(), "矩阵编排"));
|
OpenMatrixEditorCommand = new DelegateCommand(() => ShowWindow(new Views.Cnc.MatrixEditorWindow(), "矩阵编排"));
|
||||||
OpenToolboxCommand = new DelegateCommand(ExecuteOpenToolbox);
|
OpenToolboxCommand = new DelegateCommand(ExecuteOpenToolbox);
|
||||||
OpenLibraryVersionsCommand = new DelegateCommand(() => ShowWindow(new Views.LibraryVersionsWindow(), "关于"));
|
OpenLibraryVersionsCommand = new DelegateCommand(() => ShowWindow(new Views.LibraryVersionsWindow(), "关于"));
|
||||||
OpenUserManualCommand = new DelegateCommand(ExecuteOpenUserManual);
|
OpenUserManualCommand = new DelegateCommand(ExecuteOpenUserManual);
|
||||||
OpenCameraSettingsCommand = new DelegateCommand(ExecuteOpenCameraSettings);
|
OpenCameraSettingsCommand = new DelegateCommand(ExecuteOpenCameraSettings);
|
||||||
NewCncProgramCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.NewProgramCommand.Execute()));
|
|
||||||
SaveCncProgramCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.SaveProgramCommand.Execute()));
|
|
||||||
LoadCncProgramCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.LoadProgramCommand.Execute()));
|
|
||||||
InsertReferencePointCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertReferencePointCommand.Execute()));
|
|
||||||
InsertSavePositionCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertSavePositionCommand.Execute()));
|
|
||||||
InsertCompleteProgramCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertCompleteProgramCommand.Execute()));
|
|
||||||
InsertInspectionMarkerCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertInspectionMarkerCommand.Execute()));
|
|
||||||
InsertInspectionModuleCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertInspectionModuleCommand.Execute()));
|
|
||||||
InsertSaveNodeCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertSaveNodeCommand.Execute()));
|
|
||||||
InsertPauseDialogCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertPauseDialogCommand.Execute()));
|
|
||||||
InsertWaitDelayCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertWaitDelayCommand.Execute()));
|
|
||||||
|
|
||||||
// 硬件命令
|
// 硬件命令
|
||||||
AxisResetCommand = new DelegateCommand(ExecuteAxisReset);
|
AxisResetCommand = new DelegateCommand(ExecuteAxisReset);
|
||||||
@@ -164,10 +109,6 @@ namespace XplorePlane.ViewModels
|
|||||||
OpenLanguageSwitcherCommand = new DelegateCommand(ExecuteOpenLanguageSwitcher);
|
OpenLanguageSwitcherCommand = new DelegateCommand(ExecuteOpenLanguageSwitcher);
|
||||||
OpenRealTimeLogViewerCommand = new DelegateCommand(ExecuteOpenRealTimeLogViewer);
|
OpenRealTimeLogViewerCommand = new DelegateCommand(ExecuteOpenRealTimeLogViewer);
|
||||||
|
|
||||||
ImagePanelContent = new PipelineEditorView();
|
|
||||||
ViewportPanelWidth = new GridLength(1, GridUnitType.Star);
|
|
||||||
ImagePanelWidth = new GridLength(320);
|
|
||||||
|
|
||||||
_logger.Info("MainViewModel 已初始化");
|
_logger.Info("MainViewModel 已初始化");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,38 +153,6 @@ namespace XplorePlane.ViewModels
|
|||||||
ShowOrActivate(_toolboxWindow, w => _toolboxWindow = w, () => new Views.OperatorToolboxWindow(), "算子工具箱");
|
ShowOrActivate(_toolboxWindow, w => _toolboxWindow = w, () => new Views.OperatorToolboxWindow(), "算子工具箱");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExecuteOpenCncEditor()
|
|
||||||
{
|
|
||||||
if (_isCncEditorMode)
|
|
||||||
{
|
|
||||||
ImagePanelContent = new PipelineEditorView();
|
|
||||||
ViewportPanelWidth = new GridLength(1, GridUnitType.Star);
|
|
||||||
ImagePanelWidth = new GridLength(320);
|
|
||||||
_isCncEditorMode = false;
|
|
||||||
_logger.Info("已退出 CNC 编辑模式");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ShowCncEditor();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ExecuteCncEditorAction(Action<CncEditorViewModel> action)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(action);
|
|
||||||
|
|
||||||
ShowCncEditor();
|
|
||||||
action(_cncEditorViewModel);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ShowCncEditor()
|
|
||||||
{
|
|
||||||
ImagePanelContent = _cncPageView;
|
|
||||||
ViewportPanelWidth = new GridLength(1, GridUnitType.Star);
|
|
||||||
ImagePanelWidth = new GridLength(CncEditorHostWidth);
|
|
||||||
_isCncEditorMode = true;
|
|
||||||
_logger.Info("CNC 编辑器已切换到主界面图像区域");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ExecuteOpenUserManual()
|
private void ExecuteOpenUserManual()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -176,6 +176,30 @@ namespace XplorePlane.ViewModels
|
|||||||
ApplyPixelFormatCommand = new DelegateCommand(() => ApplyCameraParam(() => _camera.SetPixelFormat(SelectedPixelFormat)), () => IsCameraConnected);
|
ApplyPixelFormatCommand = new DelegateCommand(() => ApplyCameraParam(() => _camera.SetPixelFormat(SelectedPixelFormat)), () => IsCameraConnected);
|
||||||
RefreshCameraParamsCommand = new DelegateCommand(RefreshCameraParams, () => IsCameraConnected);
|
RefreshCameraParamsCommand = new DelegateCommand(RefreshCameraParams, () => IsCameraConnected);
|
||||||
OpenCameraSettingsCommand = new DelegateCommand(OpenCameraSettings, () => IsCameraConnected);
|
OpenCameraSettingsCommand = new DelegateCommand(OpenCameraSettings, () => IsCameraConnected);
|
||||||
|
|
||||||
|
CameraStatusText = "正在检索相机...";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 相机连接完成后由外部调用,启动实时预览。
|
||||||
|
/// </summary>
|
||||||
|
public void OnCameraReady()
|
||||||
|
{
|
||||||
|
if (!_camera.IsConnected)
|
||||||
|
{
|
||||||
|
CameraStatusText = "未检测到相机";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_camera.ImageGrabbed += OnCameraImageGrabbed;
|
||||||
|
_camera.GrabError += OnCameraGrabError;
|
||||||
|
_camera.ConnectionLost += OnCameraConnectionLost;
|
||||||
|
|
||||||
|
IsCameraConnected = true;
|
||||||
|
CameraStatusText = "已连接";
|
||||||
|
RefreshCameraParams();
|
||||||
|
StartGrab();
|
||||||
|
IsLiveViewEnabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Camera Methods
|
#region Camera Methods
|
||||||
@@ -326,15 +350,18 @@ namespace XplorePlane.ViewModels
|
|||||||
|
|
||||||
private void OnCameraImageGrabbed(object? sender, ImageGrabbedEventArgs e)
|
private void OnCameraImageGrabbed(object? sender, ImageGrabbedEventArgs e)
|
||||||
{
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var bitmap = PixelConverter.ToBitmapSource(e.PixelData, e.Width, e.Height, e.PixelFormat);
|
var bitmap = PixelConverter.ToBitmapSource(e.PixelData, e.Width, e.Height, e.PixelFormat);
|
||||||
var app = Application.Current;
|
var app = Application.Current;
|
||||||
if (app == null) return;
|
if (app == null) return;
|
||||||
|
|
||||||
app.Dispatcher.Invoke(() =>
|
app.Dispatcher.BeginInvoke(() =>
|
||||||
{
|
{
|
||||||
CameraImageSource = bitmap;
|
if (!_disposed)
|
||||||
|
CameraImageSource = bitmap;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (_liveViewRunning)
|
if (_liveViewRunning)
|
||||||
@@ -344,7 +371,8 @@ namespace XplorePlane.ViewModels
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.Error(ex, "Failed to process camera image");
|
if (!_disposed)
|
||||||
|
_logger.Error(ex, "Failed to process camera image");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,9 +382,10 @@ namespace XplorePlane.ViewModels
|
|||||||
var app = Application.Current;
|
var app = Application.Current;
|
||||||
if (app == null) return;
|
if (app == null) return;
|
||||||
|
|
||||||
app.Dispatcher.Invoke(() =>
|
app.Dispatcher.BeginInvoke(() =>
|
||||||
{
|
{
|
||||||
CameraStatusText = $"采集错误: {e.ErrorDescription}";
|
if (!_disposed)
|
||||||
|
CameraStatusText = $"采集错误: {e.ErrorDescription}";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,8 +395,9 @@ namespace XplorePlane.ViewModels
|
|||||||
var app = Application.Current;
|
var app = Application.Current;
|
||||||
if (app == null) return;
|
if (app == null) return;
|
||||||
|
|
||||||
app.Dispatcher.Invoke(() =>
|
app.Dispatcher.BeginInvoke(() =>
|
||||||
{
|
{
|
||||||
|
if (_disposed) return;
|
||||||
IsCameraConnected = false;
|
IsCameraConnected = false;
|
||||||
IsCameraGrabbing = false;
|
IsCameraGrabbing = false;
|
||||||
CameraStatusText = "连接已断开";
|
CameraStatusText = "连接已断开";
|
||||||
@@ -382,12 +412,19 @@ namespace XplorePlane.ViewModels
|
|||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (_disposed) return;
|
if (_disposed) return;
|
||||||
|
_disposed = true;
|
||||||
_liveViewRunning = false;
|
_liveViewRunning = false;
|
||||||
|
|
||||||
try { _camera.Dispose(); }
|
// 先取消事件订阅,防止回调继续触发
|
||||||
catch (Exception ex) { _logger.Error(ex, "Error disposing camera"); }
|
_camera.ImageGrabbed -= OnCameraImageGrabbed;
|
||||||
|
_camera.GrabError -= OnCameraGrabError;
|
||||||
|
_camera.ConnectionLost -= OnCameraConnectionLost;
|
||||||
|
|
||||||
_disposed = true;
|
// 停止采集后再关闭连接
|
||||||
|
try { if (_camera.IsGrabbing) _camera.StopGrabbing(); } catch { }
|
||||||
|
try { _camera.Close(); } catch { }
|
||||||
|
|
||||||
|
_logger.Information("NavigationPropertyPanelViewModel disposed");
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion IDisposable
|
#endregion IDisposable
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
<Window
|
<Window
|
||||||
x:Class="XplorePlane.Views.Cnc.CncEditorWindow"
|
x:Class="XplorePlane.Views.Cnc.CncEditorWindow"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:cnc="clr-namespace:XplorePlane.Views.Cnc"
|
xmlns:cnc="clr-namespace:XplorePlane.Views.Cnc"
|
||||||
Title="CNC 编辑器"
|
Title="CNC 编辑器"
|
||||||
Width="1040"
|
Width="350"
|
||||||
Height="780"
|
Height="750"
|
||||||
MinWidth="960"
|
|
||||||
MinHeight="720"
|
|
||||||
ResizeMode="CanResize"
|
|
||||||
ShowInTaskbar="False"
|
ShowInTaskbar="False"
|
||||||
WindowStartupLocation="CenterOwner">
|
WindowStartupLocation="CenterOwner">
|
||||||
<cnc:CncPageView />
|
<cnc:CncPageView />
|
||||||
</Window>
|
</Window>
|
||||||
@@ -1,457 +1,297 @@
|
|||||||
|
<!-- CNC 编辑器主页面视图 | CNC editor main page view -->
|
||||||
<UserControl
|
<UserControl
|
||||||
x:Class="XplorePlane.Views.Cnc.CncPageView"
|
x:Class="XplorePlane.Views.Cnc.CncPageView"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:local="clr-namespace:XplorePlane.Views.Cnc"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:prism="http://prismlibrary.com/"
|
xmlns:prism="http://prismlibrary.com/"
|
||||||
xmlns:views="clr-namespace:XplorePlane.Views"
|
d:DesignHeight="700"
|
||||||
xmlns:vm="clr-namespace:XplorePlane.ViewModels.Cnc"
|
d:DesignWidth="350"
|
||||||
d:DesignHeight="760"
|
|
||||||
d:DesignWidth="702"
|
|
||||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
prism:ViewModelLocator.AutoWireViewModel="True"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
|
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
<BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
<!-- 面板背景和边框颜色 | Panel background and border colors -->
|
||||||
<local:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
|
|
||||||
|
|
||||||
<SolidColorBrush x:Key="PanelBg" Color="White" />
|
<SolidColorBrush x:Key="PanelBg" Color="White" />
|
||||||
<SolidColorBrush x:Key="PanelBorder" Color="#CDCBCB" />
|
<SolidColorBrush x:Key="PanelBorder" Color="#cdcbcb" />
|
||||||
<SolidColorBrush x:Key="SeparatorBrush" Color="#E5E5E5" />
|
<SolidColorBrush x:Key="SeparatorBrush" Color="#E0E0E0" />
|
||||||
<SolidColorBrush x:Key="HeaderBg" Color="#F7F7F7" />
|
<SolidColorBrush x:Key="AccentBlue" Color="#E3F0FF" />
|
||||||
<FontFamily x:Key="UiFont">Microsoft YaHei UI</FontFamily>
|
<FontFamily x:Key="CsdFont">Microsoft YaHei UI</FontFamily>
|
||||||
|
|
||||||
<Style x:Key="TreeItemStyle" TargetType="TreeViewItem">
|
<!-- 节点列表项样式 | Node list item style -->
|
||||||
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
|
<Style x:Key="CncNodeItemStyle" TargetType="ListBoxItem">
|
||||||
<Setter Property="Padding" Value="0" />
|
<Setter Property="Padding" Value="0" />
|
||||||
<Setter Property="Margin" Value="0" />
|
<Setter Property="Margin" Value="0" />
|
||||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style x:Key="EditorTitle" TargetType="TextBlock">
|
<!-- 工具栏按钮样式 | Toolbar button style -->
|
||||||
<Setter Property="FontFamily" Value="{StaticResource UiFont}" />
|
<Style x:Key="ToolbarBtn" TargetType="Button">
|
||||||
<Setter Property="FontSize" Value="13" />
|
|
||||||
<Setter Property="FontWeight" Value="SemiBold" />
|
|
||||||
<Setter Property="Margin" Value="0,0,0,8" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style x:Key="LabelStyle" TargetType="TextBlock">
|
|
||||||
<Setter Property="FontFamily" Value="{StaticResource UiFont}" />
|
|
||||||
<Setter Property="FontSize" Value="11" />
|
|
||||||
<Setter Property="Foreground" Value="#666666" />
|
|
||||||
<Setter Property="Margin" Value="0,0,0,3" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style x:Key="EditorBox" TargetType="TextBox">
|
|
||||||
<Setter Property="Height" Value="28" />
|
<Setter Property="Height" Value="28" />
|
||||||
<Setter Property="Padding" Value="8,3" />
|
<Setter Property="Margin" Value="2,0" />
|
||||||
<Setter Property="Margin" Value="0,0,0,8" />
|
<Setter Property="Padding" Value="6,0" />
|
||||||
<Setter Property="BorderBrush" Value="#CFCFCF" />
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="BorderBrush" Value="#cdcbcb" />
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
<Setter Property="FontFamily" Value="{StaticResource UiFont}" />
|
<Setter Property="FontFamily" Value="Microsoft YaHei UI" />
|
||||||
<Setter Property="FontSize" Value="11" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style x:Key="EditorCheck" TargetType="CheckBox">
|
|
||||||
<Setter Property="Margin" Value="0,2,0,8" />
|
|
||||||
<Setter Property="FontFamily" Value="{StaticResource UiFont}" />
|
|
||||||
<Setter Property="FontSize" Value="11" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style x:Key="CompactGroupBox" TargetType="GroupBox">
|
|
||||||
<Setter Property="Margin" Value="0,0,0,10" />
|
|
||||||
<Setter Property="Padding" Value="0" />
|
|
||||||
<Setter Property="FontFamily" Value="{StaticResource UiFont}" />
|
|
||||||
<Setter Property="FontSize" Value="11" />
|
<Setter Property="FontSize" Value="11" />
|
||||||
|
<Setter Property="Cursor" Value="Hand" />
|
||||||
</Style>
|
</Style>
|
||||||
</UserControl.Resources>
|
</UserControl.Resources>
|
||||||
|
|
||||||
<Border
|
<Border
|
||||||
Width="702"
|
|
||||||
MinWidth="702"
|
|
||||||
HorizontalAlignment="Left"
|
|
||||||
Background="{StaticResource PanelBg}"
|
Background="{StaticResource PanelBg}"
|
||||||
BorderBrush="{StaticResource PanelBorder}"
|
BorderBrush="{StaticResource PanelBorder}"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
CornerRadius="4">
|
CornerRadius="4">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<ColumnDefinition Width="200" />
|
<!-- Row 0: 工具栏 | Toolbar -->
|
||||||
<ColumnDefinition Width="1" />
|
<RowDefinition Height="Auto" />
|
||||||
<ColumnDefinition Width="250" />
|
<!-- Row 1: 主内容区(左侧节点列表 + 右侧参数面板)| Main content (left: node list, right: parameter panel) -->
|
||||||
<ColumnDefinition Width="1" />
|
<RowDefinition Height="*" />
|
||||||
<ColumnDefinition Width="250" />
|
</Grid.RowDefinitions>
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<Grid Grid.Column="0">
|
<!-- ═══ 工具栏:节点插入命令 + 文件操作命令 | Toolbar: node insert commands + file operation commands ═══ -->
|
||||||
<Grid.RowDefinitions>
|
<Border
|
||||||
<RowDefinition Height="Auto" />
|
Grid.Row="0"
|
||||||
<RowDefinition Height="*" />
|
Padding="6,4"
|
||||||
</Grid.RowDefinitions>
|
Background="#F5F5F5"
|
||||||
|
BorderBrush="{StaticResource PanelBorder}"
|
||||||
|
BorderThickness="0,0,0,1">
|
||||||
|
<WrapPanel Orientation="Horizontal">
|
||||||
|
<!-- 文件操作按钮 | File operation buttons -->
|
||||||
|
<Button
|
||||||
|
Command="{Binding NewProgramCommand}"
|
||||||
|
Content="新建"
|
||||||
|
Style="{StaticResource ToolbarBtn}"
|
||||||
|
ToolTip="新建程序 | New Program" />
|
||||||
|
<Button
|
||||||
|
Command="{Binding SaveProgramCommand}"
|
||||||
|
Content="保存"
|
||||||
|
Style="{StaticResource ToolbarBtn}"
|
||||||
|
ToolTip="保存程序 | Save Program" />
|
||||||
|
<Button
|
||||||
|
Command="{Binding LoadProgramCommand}"
|
||||||
|
Content="加载"
|
||||||
|
Style="{StaticResource ToolbarBtn}"
|
||||||
|
ToolTip="加载程序 | Load Program" />
|
||||||
|
<Button
|
||||||
|
Command="{Binding ExportCsvCommand}"
|
||||||
|
Content="导出CSV"
|
||||||
|
Style="{StaticResource ToolbarBtn}"
|
||||||
|
ToolTip="导出 CSV | Export CSV" />
|
||||||
|
|
||||||
<Border
|
<!-- 分隔线 | Separator -->
|
||||||
Grid.Row="0"
|
<Rectangle
|
||||||
Padding="10,8"
|
Width="1"
|
||||||
Background="{StaticResource HeaderBg}"
|
Height="20"
|
||||||
BorderBrush="{StaticResource SeparatorBrush}"
|
Margin="4,0"
|
||||||
BorderThickness="0,0,0,1">
|
Fill="{StaticResource SeparatorBrush}" />
|
||||||
<StackPanel>
|
|
||||||
<TextBlock
|
|
||||||
FontFamily="{StaticResource UiFont}"
|
|
||||||
FontSize="13"
|
|
||||||
FontWeight="SemiBold"
|
|
||||||
Text="{Binding ProgramName, TargetNullValue=CNC编辑}"
|
|
||||||
TextWrapping="Wrap" />
|
|
||||||
<TextBlock
|
|
||||||
Margin="0,3,0,0"
|
|
||||||
FontFamily="{StaticResource UiFont}"
|
|
||||||
FontSize="10"
|
|
||||||
Foreground="#666666"
|
|
||||||
Text="模块节点下会自动显示标记、等待、消息等子节点。"
|
|
||||||
TextWrapping="Wrap" />
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<TreeView
|
<!-- 节点插入按钮(9 种节点类型)| Node insert buttons (9 node types) -->
|
||||||
x:Name="CncTreeView"
|
<Button
|
||||||
Grid.Row="1"
|
Command="{Binding InsertReferencePointCommand}"
|
||||||
Padding="4,6"
|
Content="参考点"
|
||||||
|
Style="{StaticResource ToolbarBtn}"
|
||||||
|
ToolTip="插入参考点 | Insert Reference Point" />
|
||||||
|
<Button
|
||||||
|
Command="{Binding InsertSaveNodeWithImageCommand}"
|
||||||
|
Content="保存+图"
|
||||||
|
Style="{StaticResource ToolbarBtn}"
|
||||||
|
ToolTip="保存节点并保存图片 | Save Node With Image" />
|
||||||
|
<Button
|
||||||
|
Command="{Binding InsertSaveNodeCommand}"
|
||||||
|
Content="保存节点"
|
||||||
|
Style="{StaticResource ToolbarBtn}"
|
||||||
|
ToolTip="仅保存节点 | Save Node" />
|
||||||
|
<Button
|
||||||
|
Command="{Binding InsertSavePositionCommand}"
|
||||||
|
Content="保存位置"
|
||||||
|
Style="{StaticResource ToolbarBtn}"
|
||||||
|
ToolTip="保存位置 | Save Position" />
|
||||||
|
<Button
|
||||||
|
Command="{Binding InsertInspectionModuleCommand}"
|
||||||
|
Content="检测模块"
|
||||||
|
Style="{StaticResource ToolbarBtn}"
|
||||||
|
ToolTip="插入检测模块 | Insert Inspection Module" />
|
||||||
|
<Button
|
||||||
|
Command="{Binding InsertInspectionMarkerCommand}"
|
||||||
|
Content="检测标记"
|
||||||
|
Style="{StaticResource ToolbarBtn}"
|
||||||
|
ToolTip="插入检测标记 | Insert Inspection Marker" />
|
||||||
|
<Button
|
||||||
|
Command="{Binding InsertPauseDialogCommand}"
|
||||||
|
Content="停顿"
|
||||||
|
Style="{StaticResource ToolbarBtn}"
|
||||||
|
ToolTip="插入停顿对话框 | Insert Pause Dialog" />
|
||||||
|
<Button
|
||||||
|
Command="{Binding InsertWaitDelayCommand}"
|
||||||
|
Content="延时"
|
||||||
|
Style="{StaticResource ToolbarBtn}"
|
||||||
|
ToolTip="设置等待延时 | Insert Wait Delay" />
|
||||||
|
<Button
|
||||||
|
Command="{Binding InsertCompleteProgramCommand}"
|
||||||
|
Content="完成"
|
||||||
|
Style="{StaticResource ToolbarBtn}"
|
||||||
|
ToolTip="完成程序 | Complete Program" />
|
||||||
|
</WrapPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- ═══ 主内容区:左侧节点列表 + 右侧参数面板 | Main content: left node list + right parameter panel ═══ -->
|
||||||
|
<Grid Grid.Row="1">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<!-- 左侧:节点列表 | Left: node list -->
|
||||||
|
<ColumnDefinition Width="3*" MinWidth="150" />
|
||||||
|
<!-- 分隔线 | Splitter -->
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<!-- 右侧:参数面板 | Right: parameter panel -->
|
||||||
|
<ColumnDefinition Width="2*" MinWidth="150" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- ── 左侧:CNC 节点列表 | Left: CNC node list ── -->
|
||||||
|
<ListBox
|
||||||
|
x:Name="CncNodeListBox"
|
||||||
|
Grid.Column="0"
|
||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
BorderThickness="0"
|
BorderThickness="0"
|
||||||
ItemsSource="{Binding TreeNodes}"
|
ItemContainerStyle="{StaticResource CncNodeItemStyle}"
|
||||||
SelectedItemChanged="CncTreeView_SelectedItemChanged">
|
ItemsSource="{Binding Nodes}"
|
||||||
<TreeView.Resources>
|
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||||
<HierarchicalDataTemplate DataType="{x:Type vm:CncNodeViewModel}" ItemsSource="{Binding Children}">
|
SelectedItem="{Binding SelectedNode, Mode=TwoWay}">
|
||||||
<Grid x:Name="NodeRoot" MinHeight="34">
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Grid x:Name="NodeRoot" MinHeight="40">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="30" />
|
<!-- 图标列 | Icon column -->
|
||||||
|
<ColumnDefinition Width="40" />
|
||||||
|
<!-- 名称列 | Name column -->
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
|
<!-- 操作按钮列 | Action buttons column -->
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- 节点图标 | Node icon -->
|
||||||
<Border
|
<Border
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Width="22"
|
Width="28"
|
||||||
Height="22"
|
Height="28"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Background="Transparent"
|
Background="#E8F0FE"
|
||||||
|
BorderBrush="#5B9BD5"
|
||||||
|
BorderThickness="1.5"
|
||||||
CornerRadius="4">
|
CornerRadius="4">
|
||||||
<Image
|
<Image
|
||||||
Width="15"
|
Width="20"
|
||||||
Height="15"
|
Height="20"
|
||||||
Source="{Binding Icon}"
|
Source="{Binding Icon}"
|
||||||
Stretch="Uniform" />
|
Stretch="Uniform" />
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<!-- 节点序号和名称 | Node index and name -->
|
||||||
<StackPanel
|
<StackPanel
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="4,0,0,0"
|
Margin="6,0,0,0"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Orientation="Horizontal">
|
Orientation="Horizontal">
|
||||||
<TextBlock
|
<TextBlock
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
FontFamily="{StaticResource UiFont}"
|
FontFamily="Microsoft YaHei UI"
|
||||||
FontSize="10.5"
|
FontSize="11"
|
||||||
Foreground="#888888"
|
Foreground="#888"
|
||||||
Text="{Binding Index, StringFormat='[{0}] '}" />
|
Text="{Binding Index, StringFormat='[{0}] '}" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
FontFamily="{StaticResource UiFont}"
|
FontFamily="Microsoft YaHei UI"
|
||||||
FontSize="11.5"
|
FontSize="12"
|
||||||
Text="{Binding Name}" />
|
Text="{Binding Name}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 悬停操作按钮:上移 / 下移 / 删除 | Hover actions: MoveUp / MoveDown / Delete -->
|
||||||
<StackPanel
|
<StackPanel
|
||||||
x:Name="NodeActions"
|
x:Name="NodeActions"
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
Margin="0,0,2,0"
|
Margin="0,0,4,0"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Orientation="Horizontal"
|
Orientation="Horizontal"
|
||||||
Visibility="Collapsed">
|
Visibility="Collapsed">
|
||||||
<Button
|
<Button
|
||||||
Width="20"
|
Width="22"
|
||||||
Height="20"
|
Height="22"
|
||||||
Margin="1,0"
|
Margin="1,0"
|
||||||
Background="White"
|
Background="Transparent"
|
||||||
BorderBrush="#CDCBCB"
|
BorderBrush="#cdcbcb"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
Command="{Binding DataContext.MoveNodeUpCommand, RelativeSource={RelativeSource AncestorType=TreeView}}"
|
Command="{Binding DataContext.MoveNodeUpCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
|
||||||
CommandParameter="{Binding}"
|
CommandParameter="{Binding}"
|
||||||
Content="上"
|
Content="▲"
|
||||||
Cursor="Hand"
|
Cursor="Hand"
|
||||||
FontSize="10"
|
FontSize="10"
|
||||||
ToolTip="上移" />
|
ToolTip="上移 | Move Up" />
|
||||||
<Button
|
<Button
|
||||||
Width="20"
|
Width="22"
|
||||||
Height="20"
|
Height="22"
|
||||||
Margin="1,0"
|
Margin="1,0"
|
||||||
Background="White"
|
Background="Transparent"
|
||||||
BorderBrush="#CDCBCB"
|
BorderBrush="#cdcbcb"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
Command="{Binding DataContext.MoveNodeDownCommand, RelativeSource={RelativeSource AncestorType=TreeView}}"
|
Command="{Binding DataContext.MoveNodeDownCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
|
||||||
CommandParameter="{Binding}"
|
CommandParameter="{Binding}"
|
||||||
Content="下"
|
Content="▼"
|
||||||
Cursor="Hand"
|
Cursor="Hand"
|
||||||
FontSize="10"
|
FontSize="10"
|
||||||
ToolTip="下移" />
|
ToolTip="下移 | Move Down" />
|
||||||
<Button
|
<Button
|
||||||
Width="20"
|
Width="22"
|
||||||
Height="20"
|
Height="22"
|
||||||
Margin="1,0"
|
Margin="1,0"
|
||||||
Background="White"
|
Background="Transparent"
|
||||||
BorderBrush="#E05050"
|
BorderBrush="#E05050"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
Command="{Binding DataContext.DeleteNodeCommand, RelativeSource={RelativeSource AncestorType=TreeView}}"
|
Command="{Binding DataContext.DeleteNodeCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
|
||||||
Content="删"
|
Content="✕"
|
||||||
Cursor="Hand"
|
Cursor="Hand"
|
||||||
FontSize="10"
|
FontSize="10"
|
||||||
ToolTip="删除" />
|
ToolTip="删除 | Delete" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
<DataTemplate.Triggers>
|
<DataTemplate.Triggers>
|
||||||
|
<!-- 鼠标悬停时显示操作按钮 | Show action buttons on mouse hover -->
|
||||||
<Trigger SourceName="NodeRoot" Property="IsMouseOver" Value="True">
|
<Trigger SourceName="NodeRoot" Property="IsMouseOver" Value="True">
|
||||||
<Setter TargetName="NodeActions" Property="Visibility" Value="Visible" />
|
<Setter TargetName="NodeActions" Property="Visibility" Value="Visible" />
|
||||||
</Trigger>
|
</Trigger>
|
||||||
</DataTemplate.Triggers>
|
</DataTemplate.Triggers>
|
||||||
</HierarchicalDataTemplate>
|
</DataTemplate>
|
||||||
</TreeView.Resources>
|
</ListBox.ItemTemplate>
|
||||||
<TreeView.ItemContainerStyle>
|
</ListBox>
|
||||||
<Style BasedOn="{StaticResource TreeItemStyle}" TargetType="TreeViewItem" />
|
|
||||||
</TreeView.ItemContainerStyle>
|
|
||||||
</TreeView>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Rectangle
|
<!-- 垂直分隔线 | Vertical separator -->
|
||||||
Grid.Column="1"
|
<Rectangle
|
||||||
Width="1"
|
Grid.Column="1"
|
||||||
Fill="{StaticResource SeparatorBrush}" />
|
Width="1"
|
||||||
|
Fill="{StaticResource SeparatorBrush}" />
|
||||||
|
|
||||||
<ScrollViewer Grid.Column="2" VerticalScrollBarVisibility="Auto">
|
<!-- ── 右侧:参数面板(根据节点类型动态渲染)| Right: parameter panel (dynamic rendering by node type) ── -->
|
||||||
<Grid Margin="10">
|
<ScrollViewer
|
||||||
<StackPanel Visibility="{Binding SelectedNode, Converter={StaticResource NullToVisibilityConverter}}">
|
Grid.Column="2"
|
||||||
<TextBlock Style="{StaticResource EditorTitle}" Text="节点属性" />
|
HorizontalScrollBarVisibility="Disabled"
|
||||||
|
VerticalScrollBarVisibility="Auto">
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="名称" />
|
<StackPanel Margin="8,6">
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Name, UpdateSourceTrigger=PropertyChanged}" />
|
|
||||||
|
|
||||||
<UniformGrid Columns="2" Margin="0,0,0,8">
|
|
||||||
<StackPanel Margin="0,0,6,0">
|
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="索引" />
|
|
||||||
<TextBox Style="{StaticResource EditorBox}" IsReadOnly="True" Text="{Binding SelectedNode.Index, Mode=OneWay}" />
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="类型" />
|
|
||||||
<TextBox Style="{StaticResource EditorBox}" IsReadOnly="True" Text="{Binding SelectedNode.NodeTypeDisplay, Mode=OneWay}" />
|
|
||||||
</StackPanel>
|
|
||||||
</UniformGrid>
|
|
||||||
|
|
||||||
<GroupBox
|
|
||||||
Style="{StaticResource CompactGroupBox}"
|
|
||||||
Header="运动参数"
|
|
||||||
Visibility="{Binding SelectedNode.IsMotionSnapshotNode, Converter={StaticResource BoolToVisibilityConverter}}">
|
|
||||||
<UniformGrid Margin="8,8,8,6" Columns="2">
|
|
||||||
<StackPanel Margin="0,0,6,0">
|
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="XM" />
|
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.XM, UpdateSourceTrigger=LostFocus}" />
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="YM" />
|
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.YM, UpdateSourceTrigger=LostFocus}" />
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Margin="0,0,6,0">
|
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="ZT" />
|
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.ZT, UpdateSourceTrigger=LostFocus}" />
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="ZD" />
|
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.ZD, UpdateSourceTrigger=LostFocus}" />
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Margin="0,0,6,0">
|
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="TiltD" />
|
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.TiltD, UpdateSourceTrigger=LostFocus}" />
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="Dist" />
|
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Dist, UpdateSourceTrigger=LostFocus}" />
|
|
||||||
</StackPanel>
|
|
||||||
</UniformGrid>
|
|
||||||
</GroupBox>
|
|
||||||
|
|
||||||
<GroupBox
|
|
||||||
Style="{StaticResource CompactGroupBox}"
|
|
||||||
Header="射线源"
|
|
||||||
Visibility="{Binding SelectedNode.IsReferencePoint, Converter={StaticResource BoolToVisibilityConverter}}">
|
|
||||||
<StackPanel Margin="8,8,8,6">
|
|
||||||
<CheckBox Style="{StaticResource EditorCheck}" Content="射线源开启" IsChecked="{Binding SelectedNode.IsRayOn}" />
|
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="电压 (kV)" />
|
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Voltage, UpdateSourceTrigger=LostFocus}" />
|
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="电流 (uA)" />
|
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Current, UpdateSourceTrigger=LostFocus}" />
|
|
||||||
</StackPanel>
|
|
||||||
</GroupBox>
|
|
||||||
|
|
||||||
<GroupBox
|
|
||||||
Style="{StaticResource CompactGroupBox}"
|
|
||||||
Header="保存参数"
|
|
||||||
Visibility="{Binding SelectedNode.IsSaveNode, Converter={StaticResource BoolToVisibilityConverter}}">
|
|
||||||
<StackPanel Margin="8,8,8,6">
|
|
||||||
<CheckBox Style="{StaticResource EditorCheck}" Content="射线源开启" IsChecked="{Binding SelectedNode.IsRayOn}" />
|
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="电压 (kV)" />
|
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Voltage, UpdateSourceTrigger=LostFocus}" />
|
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="功率 (W)" />
|
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Power, UpdateSourceTrigger=LostFocus}" />
|
|
||||||
<CheckBox Style="{StaticResource EditorCheck}" Content="探测器已连接" IsChecked="{Binding SelectedNode.DetectorConnected}" />
|
|
||||||
<CheckBox Style="{StaticResource EditorCheck}" Content="探测器正在采集" IsChecked="{Binding SelectedNode.DetectorAcquiring}" />
|
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="帧率" />
|
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.FrameRate, UpdateSourceTrigger=LostFocus}" />
|
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="分辨率" />
|
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Resolution, UpdateSourceTrigger=PropertyChanged}" />
|
|
||||||
</StackPanel>
|
|
||||||
</GroupBox>
|
|
||||||
|
|
||||||
<GroupBox
|
|
||||||
Style="{StaticResource CompactGroupBox}"
|
|
||||||
Header="保存参数"
|
|
||||||
Visibility="{Binding SelectedNode.IsSaveNodeWithImage, Converter={StaticResource BoolToVisibilityConverter}}">
|
|
||||||
<StackPanel Margin="8,8,8,6">
|
|
||||||
<CheckBox Style="{StaticResource EditorCheck}" Content="射线源开启" IsChecked="{Binding SelectedNode.IsRayOn}" />
|
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="电压 (kV)" />
|
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Voltage, UpdateSourceTrigger=LostFocus}" />
|
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="功率 (W)" />
|
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Power, UpdateSourceTrigger=LostFocus}" />
|
|
||||||
<CheckBox Style="{StaticResource EditorCheck}" Content="探测器已连接" IsChecked="{Binding SelectedNode.DetectorConnected}" />
|
|
||||||
<CheckBox Style="{StaticResource EditorCheck}" Content="探测器正在采集" IsChecked="{Binding SelectedNode.DetectorAcquiring}" />
|
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="帧率" />
|
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.FrameRate, UpdateSourceTrigger=LostFocus}" />
|
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="分辨率" />
|
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Resolution, UpdateSourceTrigger=PropertyChanged}" />
|
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="图像文件" />
|
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.ImageFileName, UpdateSourceTrigger=PropertyChanged}" />
|
|
||||||
</StackPanel>
|
|
||||||
</GroupBox>
|
|
||||||
|
|
||||||
<GroupBox
|
|
||||||
Style="{StaticResource CompactGroupBox}"
|
|
||||||
Header="检测模块"
|
|
||||||
Visibility="{Binding SelectedNode.IsInspectionModule, Converter={StaticResource BoolToVisibilityConverter}}">
|
|
||||||
<StackPanel Margin="8,8,8,6">
|
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="Pipeline 名称" />
|
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.PipelineName, UpdateSourceTrigger=PropertyChanged}" />
|
|
||||||
</StackPanel>
|
|
||||||
</GroupBox>
|
|
||||||
|
|
||||||
<GroupBox
|
|
||||||
Style="{StaticResource CompactGroupBox}"
|
|
||||||
Header="标记参数"
|
|
||||||
Visibility="{Binding SelectedNode.IsInspectionMarker, Converter={StaticResource BoolToVisibilityConverter}}">
|
|
||||||
<StackPanel Margin="8,8,8,6">
|
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="标记类型" />
|
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.MarkerType, UpdateSourceTrigger=PropertyChanged}" />
|
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="MarkerX" />
|
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.MarkerX, UpdateSourceTrigger=LostFocus}" />
|
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="MarkerY" />
|
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.MarkerY, UpdateSourceTrigger=LostFocus}" />
|
|
||||||
</StackPanel>
|
|
||||||
</GroupBox>
|
|
||||||
|
|
||||||
<GroupBox
|
|
||||||
Style="{StaticResource CompactGroupBox}"
|
|
||||||
Header="消息弹窗"
|
|
||||||
Visibility="{Binding SelectedNode.IsPauseDialog, Converter={StaticResource BoolToVisibilityConverter}}">
|
|
||||||
<StackPanel Margin="8,8,8,6">
|
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="标题" />
|
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.DialogTitle, UpdateSourceTrigger=PropertyChanged}" />
|
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="消息内容" />
|
|
||||||
<TextBox
|
|
||||||
MinHeight="68"
|
|
||||||
Margin="0,0,0,8"
|
|
||||||
Padding="8,6"
|
|
||||||
AcceptsReturn="True"
|
|
||||||
BorderBrush="#CFCFCF"
|
|
||||||
BorderThickness="1"
|
|
||||||
FontFamily="{StaticResource UiFont}"
|
|
||||||
FontSize="11"
|
|
||||||
Text="{Binding SelectedNode.DialogMessage, UpdateSourceTrigger=PropertyChanged}"
|
|
||||||
TextWrapping="Wrap" />
|
|
||||||
</StackPanel>
|
|
||||||
</GroupBox>
|
|
||||||
|
|
||||||
<GroupBox
|
|
||||||
Style="{StaticResource CompactGroupBox}"
|
|
||||||
Header="等待参数"
|
|
||||||
Visibility="{Binding SelectedNode.IsWaitDelay, Converter={StaticResource BoolToVisibilityConverter}}">
|
|
||||||
<StackPanel Margin="10,8,10,6">
|
|
||||||
<TextBlock Style="{StaticResource LabelStyle}" Text="延时 (ms)" />
|
|
||||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.DelayMilliseconds, UpdateSourceTrigger=LostFocus}" />
|
|
||||||
</StackPanel>
|
|
||||||
</GroupBox>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<Border
|
|
||||||
Padding="12"
|
|
||||||
Background="#FAFAFA"
|
|
||||||
BorderBrush="#E6E6E6"
|
|
||||||
BorderThickness="1"
|
|
||||||
CornerRadius="6"
|
|
||||||
Visibility="{Binding SelectedNode, Converter={StaticResource NullToVisibilityConverter}, ConverterParameter=Invert}">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock
|
|
||||||
FontFamily="{StaticResource UiFont}"
|
|
||||||
FontSize="13"
|
|
||||||
FontWeight="SemiBold"
|
|
||||||
Text="未选择节点"
|
|
||||||
TextWrapping="Wrap" />
|
|
||||||
<TextBlock
|
|
||||||
Margin="0,6,0,0"
|
|
||||||
FontFamily="{StaticResource UiFont}"
|
|
||||||
FontSize="10"
|
|
||||||
Foreground="#666666"
|
|
||||||
Text="从左侧树中选择一个节点后,这里会显示可编辑的参数。"
|
|
||||||
TextWrapping="Wrap" />
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
</ScrollViewer>
|
|
||||||
|
|
||||||
<Rectangle
|
|
||||||
Grid.Column="3"
|
|
||||||
Width="1"
|
|
||||||
Fill="{StaticResource SeparatorBrush}" />
|
|
||||||
|
|
||||||
<Grid Grid.Column="4">
|
|
||||||
<views:PipelineEditorView
|
|
||||||
x:Name="InspectionModulePipelineEditor"
|
|
||||||
Visibility="{Binding EditorVisibility}" />
|
|
||||||
|
|
||||||
<Border
|
|
||||||
x:Name="InspectionModulePipelineEmptyState"
|
|
||||||
Margin="12"
|
|
||||||
Padding="16"
|
|
||||||
Background="#FAFAFA"
|
|
||||||
BorderBrush="#E6E6E6"
|
|
||||||
BorderThickness="1"
|
|
||||||
CornerRadius="6"
|
|
||||||
Visibility="{Binding EmptyStateVisibility}">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
FontFamily="{StaticResource UiFont}"
|
Margin="0,0,0,4"
|
||||||
FontSize="13"
|
FontFamily="{StaticResource CsdFont}"
|
||||||
FontWeight="SemiBold"
|
FontSize="11"
|
||||||
Text="未选择检测模块"
|
FontWeight="Bold"
|
||||||
TextWrapping="Wrap" />
|
Foreground="#555"
|
||||||
<TextBlock
|
Text="参数配置" />
|
||||||
Margin="0,6,0,0"
|
|
||||||
FontFamily="{StaticResource UiFont}"
|
<!-- 动态参数内容区域(占位:根据 SelectedNode 类型渲染)| Dynamic parameter content area (placeholder for node-type-based rendering) -->
|
||||||
FontSize="10"
|
<ContentControl Content="{Binding SelectedNode}" />
|
||||||
Foreground="#666666"
|
|
||||||
Text="请选择一个检测模块节点后,在这里拖拽算子并配置参数。"
|
|
||||||
TextWrapping="Wrap" />
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</ScrollViewer>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@@ -1,94 +1,16 @@
|
|||||||
using Prism.Ioc;
|
|
||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Data;
|
|
||||||
using XP.Common.Logging.Interfaces;
|
|
||||||
using XplorePlane.Services;
|
|
||||||
using XplorePlane.ViewModels.Cnc;
|
|
||||||
|
|
||||||
namespace XplorePlane.Views.Cnc
|
namespace XplorePlane.Views.Cnc
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// CNC editor main page view.
|
/// CNC 编辑器主页面视图(MVVM 模式,逻辑在 ViewModel 中)
|
||||||
|
/// CNC editor main page view (MVVM pattern, logic in ViewModel)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class CncPageView : UserControl
|
public partial class CncPageView : UserControl
|
||||||
{
|
{
|
||||||
private CncInspectionModulePipelineViewModel _inspectionModulePipelineViewModel;
|
|
||||||
|
|
||||||
public CncPageView()
|
public CncPageView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
Loaded += OnLoaded;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
CncEditorViewModel editorViewModel = DataContext as CncEditorViewModel;
|
|
||||||
if (editorViewModel == null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
editorViewModel = ContainerLocator.Current?.Resolve<CncEditorViewModel>();
|
|
||||||
DataContext = editorViewModel;
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
// keep existing DataContext if resolution fails
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (editorViewModel == null || _inspectionModulePipelineViewModel != null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var imageProcessingService = ContainerLocator.Current.Resolve<IImageProcessingService>();
|
|
||||||
var persistenceService = ContainerLocator.Current.Resolve<IPipelinePersistenceService>();
|
|
||||||
var logger = ContainerLocator.Current.Resolve<ILoggerService>();
|
|
||||||
|
|
||||||
_inspectionModulePipelineViewModel = new CncInspectionModulePipelineViewModel(
|
|
||||||
editorViewModel,
|
|
||||||
imageProcessingService,
|
|
||||||
persistenceService,
|
|
||||||
logger);
|
|
||||||
|
|
||||||
InspectionModulePipelineEditor.DataContext = _inspectionModulePipelineViewModel;
|
|
||||||
InspectionModulePipelineEmptyState.DataContext = _inspectionModulePipelineViewModel;
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
// keep page usable even if pipeline editor host setup fails
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CncTreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
|
|
||||||
{
|
|
||||||
if (DataContext is CncEditorViewModel viewModel)
|
|
||||||
{
|
|
||||||
viewModel.SelectedNode = e.NewValue as CncNodeViewModel;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
public class NullToVisibilityConverter : IValueConverter
|
|
||||||
{
|
|
||||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
|
||||||
{
|
|
||||||
var invert = string.Equals(parameter as string, "Invert", StringComparison.OrdinalIgnoreCase);
|
|
||||||
var isVisible = value != null;
|
|
||||||
if (invert)
|
|
||||||
{
|
|
||||||
isVisible = !isVisible;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isVisible ? Visibility.Visible : Visibility.Collapsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
|
||||||
{
|
|
||||||
throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,23 +11,25 @@
|
|||||||
|
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
<SolidColorBrush x:Key="PanelBg" Color="White" />
|
<SolidColorBrush x:Key="PanelBg" Color="White" />
|
||||||
<SolidColorBrush x:Key="PanelBorder" Color="#CDCBCB" />
|
<SolidColorBrush x:Key="PanelBorder" Color="#cdcbcb" />
|
||||||
<SolidColorBrush x:Key="SeparatorBrush" Color="#E0E0E0" />
|
<SolidColorBrush x:Key="SeparatorBrush" Color="#E0E0E0" />
|
||||||
<SolidColorBrush x:Key="AccentBlue" Color="#E3F0FF" />
|
<SolidColorBrush x:Key="AccentBlue" Color="#E3F0FF" />
|
||||||
<FontFamily x:Key="CsdFont">Microsoft YaHei UI</FontFamily>
|
<FontFamily x:Key="CsdFont">Microsoft YaHei UI</FontFamily>
|
||||||
|
|
||||||
|
<!-- 节点项样式 -->
|
||||||
<Style x:Key="PipelineNodeItemStyle" TargetType="ListBoxItem">
|
<Style x:Key="PipelineNodeItemStyle" TargetType="ListBoxItem">
|
||||||
<Setter Property="Padding" Value="0" />
|
<Setter Property="Padding" Value="0" />
|
||||||
<Setter Property="Margin" Value="0" />
|
<Setter Property="Margin" Value="0" />
|
||||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<!-- 工具栏按钮样式 -->
|
||||||
<Style x:Key="ToolbarBtn" TargetType="Button">
|
<Style x:Key="ToolbarBtn" TargetType="Button">
|
||||||
<Setter Property="Width" Value="52" />
|
<Setter Property="Width" Value="28" />
|
||||||
<Setter Property="Height" Value="28" />
|
<Setter Property="Height" Value="28" />
|
||||||
<Setter Property="Margin" Value="2,0" />
|
<Setter Property="Margin" Value="2,0" />
|
||||||
<Setter Property="Background" Value="Transparent" />
|
<Setter Property="Background" Value="Transparent" />
|
||||||
<Setter Property="BorderBrush" Value="#CDCBCB" />
|
<Setter Property="BorderBrush" Value="#cdcbcb" />
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
<Setter Property="FontFamily" Value="Microsoft YaHei UI" />
|
<Setter Property="FontFamily" Value="Microsoft YaHei UI" />
|
||||||
<Setter Property="FontSize" Value="11" />
|
<Setter Property="FontSize" Value="11" />
|
||||||
@@ -42,13 +44,21 @@
|
|||||||
CornerRadius="4">
|
CornerRadius="4">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
|
<!-- Row 0: 工具栏 -->
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
|
|
||||||
|
<!-- Row 2: 流水线节点列表 -->
|
||||||
<RowDefinition Height="2*" MinHeight="180" />
|
<RowDefinition Height="2*" MinHeight="180" />
|
||||||
|
<!-- Row 3: 分隔线 -->
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
|
<!-- Row 4: 参数面板 -->
|
||||||
<RowDefinition Height="2*" MinHeight="80" />
|
<RowDefinition Height="2*" MinHeight="80" />
|
||||||
|
<!-- Row 5: 状态栏 -->
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- 标题栏:流水线名称 + 设备选择 -->
|
||||||
|
<!-- 工具栏 -->
|
||||||
<Border
|
<Border
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Padding="6,4"
|
Padding="6,4"
|
||||||
@@ -66,23 +76,50 @@
|
|||||||
Command="{Binding SavePipelineCommand}"
|
Command="{Binding SavePipelineCommand}"
|
||||||
Content="保存"
|
Content="保存"
|
||||||
Style="{StaticResource ToolbarBtn}"
|
Style="{StaticResource ToolbarBtn}"
|
||||||
ToolTip="保存当前检测模块流水线" />
|
ToolTip="保存流水线" />
|
||||||
<Button
|
<Button
|
||||||
Width="60"
|
Width="43"
|
||||||
Command="{Binding SaveAsPipelineCommand}"
|
Command="{Binding SaveAsPipelineCommand}"
|
||||||
Content="另存为"
|
Content="另存为"
|
||||||
Style="{StaticResource ToolbarBtn}"
|
Style="{StaticResource ToolbarBtn}"
|
||||||
ToolTip="导出当前检测模块流水线" />
|
ToolTip="另存为" />
|
||||||
<Button
|
<Button
|
||||||
Width="52"
|
Width="43"
|
||||||
Command="{Binding LoadPipelineCommand}"
|
Command="{Binding LoadPipelineCommand}"
|
||||||
Content="加载"
|
Content="加载"
|
||||||
Style="{StaticResource ToolbarBtn}"
|
Style="{StaticResource ToolbarBtn}"
|
||||||
ToolTip="将流水线模板加载到当前检测模块" />
|
ToolTip="加载流水线" />
|
||||||
|
<!--
|
||||||
|
<Button
|
||||||
|
Width="64"
|
||||||
|
Command="{Binding LoadImageCommand}"
|
||||||
|
Content="加载图像"
|
||||||
|
Style="{StaticResource ToolbarBtn}"
|
||||||
|
ToolTip="加载输入图像" />
|
||||||
|
-->
|
||||||
|
<Button
|
||||||
|
Command="{Binding ExecutePipelineCommand}"
|
||||||
|
Content="▶"
|
||||||
|
Style="{StaticResource ToolbarBtn}"
|
||||||
|
ToolTip="执行流水线" />
|
||||||
|
<Button
|
||||||
|
Command="{Binding CancelExecutionCommand}"
|
||||||
|
Content="■"
|
||||||
|
Style="{StaticResource ToolbarBtn}"
|
||||||
|
ToolTip="取消执行" />
|
||||||
|
<Button
|
||||||
|
Command="{Binding DeletePipelineCommand}"
|
||||||
|
Content="🗑"
|
||||||
|
Style="{StaticResource ToolbarBtn}"
|
||||||
|
ToolTip="工具箱" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 流水线节点列表(拖拽目标) -->
|
||||||
<ListBox
|
<ListBox
|
||||||
x:Name="PipelineListBox"
|
x:Name="PipelineListBox"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
@@ -101,6 +138,7 @@
|
|||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- 连接线列:上半段 + 下半段 -->
|
||||||
<Line
|
<Line
|
||||||
x:Name="TopLine"
|
x:Name="TopLine"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
@@ -121,6 +159,7 @@
|
|||||||
Y1="0"
|
Y1="0"
|
||||||
Y2="14" />
|
Y2="14" />
|
||||||
|
|
||||||
|
<!-- 算子图标 -->
|
||||||
<Border
|
<Border
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Width="28"
|
Width="28"
|
||||||
@@ -138,6 +177,7 @@
|
|||||||
Text="{Binding IconPath}" />
|
Text="{Binding IconPath}" />
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<!-- 算子名称 -->
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="6,0,0,0"
|
Margin="6,0,0,0"
|
||||||
@@ -146,6 +186,7 @@
|
|||||||
FontSize="12"
|
FontSize="12"
|
||||||
Text="{Binding DisplayName}" />
|
Text="{Binding DisplayName}" />
|
||||||
|
|
||||||
|
<!-- 操作按钮:上移 / 下移 / 删除(悬停显示) -->
|
||||||
<StackPanel
|
<StackPanel
|
||||||
x:Name="NodeActions"
|
x:Name="NodeActions"
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
@@ -158,11 +199,11 @@
|
|||||||
Height="22"
|
Height="22"
|
||||||
Margin="1,0"
|
Margin="1,0"
|
||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
BorderBrush="#CDCBCB"
|
BorderBrush="#cdcbcb"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
Command="{Binding DataContext.MoveNodeUpCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
|
Command="{Binding DataContext.MoveNodeUpCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
|
||||||
CommandParameter="{Binding}"
|
CommandParameter="{Binding}"
|
||||||
Content="上"
|
Content="▲"
|
||||||
Cursor="Hand"
|
Cursor="Hand"
|
||||||
FontSize="10"
|
FontSize="10"
|
||||||
ToolTip="上移" />
|
ToolTip="上移" />
|
||||||
@@ -171,11 +212,11 @@
|
|||||||
Height="22"
|
Height="22"
|
||||||
Margin="1,0"
|
Margin="1,0"
|
||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
BorderBrush="#CDCBCB"
|
BorderBrush="#cdcbcb"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
Command="{Binding DataContext.MoveNodeDownCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
|
Command="{Binding DataContext.MoveNodeDownCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
|
||||||
CommandParameter="{Binding}"
|
CommandParameter="{Binding}"
|
||||||
Content="下"
|
Content="▼"
|
||||||
Cursor="Hand"
|
Cursor="Hand"
|
||||||
FontSize="10"
|
FontSize="10"
|
||||||
ToolTip="下移" />
|
ToolTip="下移" />
|
||||||
@@ -188,7 +229,7 @@
|
|||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
Command="{Binding DataContext.RemoveOperatorCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
|
Command="{Binding DataContext.RemoveOperatorCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
|
||||||
CommandParameter="{Binding}"
|
CommandParameter="{Binding}"
|
||||||
Content="删"
|
Content="✕"
|
||||||
Cursor="Hand"
|
Cursor="Hand"
|
||||||
FontSize="10"
|
FontSize="10"
|
||||||
ToolTip="删除" />
|
ToolTip="删除" />
|
||||||
@@ -205,12 +246,13 @@
|
|||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ListBox.ItemTemplate>
|
</ListBox.ItemTemplate>
|
||||||
</ListBox>
|
</ListBox>
|
||||||
|
<!-- 分隔线 -->
|
||||||
<Rectangle
|
<Rectangle
|
||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
Height="1"
|
Height="1"
|
||||||
Fill="{StaticResource SeparatorBrush}" />
|
Fill="{StaticResource SeparatorBrush}" />
|
||||||
|
|
||||||
|
<!-- 参数面板 -->
|
||||||
<ScrollViewer
|
<ScrollViewer
|
||||||
Grid.Row="3"
|
Grid.Row="3"
|
||||||
HorizontalScrollBarVisibility="Disabled"
|
HorizontalScrollBarVisibility="Disabled"
|
||||||
@@ -241,14 +283,14 @@
|
|||||||
<TextBox
|
<TextBox
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Padding="4,2"
|
Padding="4,2"
|
||||||
BorderBrush="#CDCBCB"
|
BorderBrush="#cdcbcb"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
FontFamily="Microsoft YaHei UI"
|
FontFamily="Microsoft YaHei UI"
|
||||||
FontSize="11"
|
FontSize="11"
|
||||||
Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}">
|
Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}">
|
||||||
<TextBox.Style>
|
<TextBox.Style>
|
||||||
<Style TargetType="TextBox">
|
<Style TargetType="TextBox">
|
||||||
<Setter Property="BorderBrush" Value="#CDCBCB" />
|
<Setter Property="BorderBrush" Value="#cdcbcb" />
|
||||||
<Setter Property="Background" Value="White" />
|
<Setter Property="Background" Value="White" />
|
||||||
<Style.Triggers>
|
<Style.Triggers>
|
||||||
<DataTrigger Binding="{Binding IsValueValid}" Value="False">
|
<DataTrigger Binding="{Binding IsValueValid}" Value="False">
|
||||||
@@ -266,6 +308,7 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<!-- 状态栏 -->
|
||||||
<Border
|
<Border
|
||||||
Grid.Row="4"
|
Grid.Row="4"
|
||||||
Padding="6,4"
|
Padding="6,4"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using Prism.Ioc;
|
|
||||||
using System;
|
using System;
|
||||||
|
using Prism.Ioc;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using XP.Common.Logging.Interfaces;
|
using XP.Common.Logging.Interfaces;
|
||||||
@@ -23,7 +23,7 @@ namespace XplorePlane.Views
|
|||||||
|
|
||||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (DataContext is not IPipelineEditorHostViewModel)
|
if (DataContext is not PipelineEditorViewModel)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -46,35 +46,36 @@ namespace XplorePlane.Views
|
|||||||
|
|
||||||
private void OnDragOver(object sender, DragEventArgs e)
|
private void OnDragOver(object sender, DragEventArgs e)
|
||||||
{
|
{
|
||||||
e.Effects = e.Data.GetDataPresent(OperatorToolboxView.DragFormat)
|
if (e.Data.GetDataPresent(OperatorToolboxView.DragFormat))
|
||||||
? DragDropEffects.Copy
|
e.Effects = DragDropEffects.Copy;
|
||||||
: DragDropEffects.None;
|
else
|
||||||
|
e.Effects = DragDropEffects.None;
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnOperatorDropped(object sender, DragEventArgs e)
|
private void OnOperatorDropped(object sender, DragEventArgs e)
|
||||||
{
|
{
|
||||||
if (DataContext is not IPipelineEditorHostViewModel vm)
|
if (DataContext is not PipelineEditorViewModel vm)
|
||||||
{
|
{
|
||||||
_logger?.Warn("Drop 事件触发,但 DataContext 不是流水线宿主 ViewModel");
|
_logger?.Warn("Drop 事件触发但 DataContext 不是 PipelineEditorViewModel");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!e.Data.GetDataPresent(OperatorToolboxView.DragFormat))
|
if (!e.Data.GetDataPresent(OperatorToolboxView.DragFormat))
|
||||||
{
|
{
|
||||||
_logger?.Warn("Drop 事件触发,但数据中没有 {Format}", OperatorToolboxView.DragFormat);
|
_logger?.Warn("Drop 事件触发但数据中没有 {Format}", OperatorToolboxView.DragFormat);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var operatorKey = e.Data.GetData(OperatorToolboxView.DragFormat) as string;
|
var operatorKey = e.Data.GetData(OperatorToolboxView.DragFormat) as string;
|
||||||
if (string.IsNullOrWhiteSpace(operatorKey))
|
if (string.IsNullOrEmpty(operatorKey))
|
||||||
{
|
{
|
||||||
_logger?.Warn("Drop 事件触发,但 OperatorKey 为空");
|
_logger?.Warn("Drop 事件触发但 OperatorKey 为空");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger?.Info("算子已放入流水线:{OperatorKey},当前节点数(执行前)={Count}",
|
_logger?.Info("算子已放入流水线:{OperatorKey},VM HashCode={Hash},当前节点数(执行前)={Count}",
|
||||||
operatorKey, vm.PipelineNodes.Count);
|
operatorKey, vm.GetHashCode(), vm.PipelineNodes.Count);
|
||||||
vm.AddOperatorCommand.Execute(operatorKey);
|
vm.AddOperatorCommand.Execute(operatorKey);
|
||||||
_logger?.Info("AddOperator 执行后节点数={Count},PipelineListBox.Items.Count={ItemsCount}",
|
_logger?.Info("AddOperator 执行后节点数={Count},PipelineListBox.Items.Count={ItemsCount}",
|
||||||
vm.PipelineNodes.Count, PipelineListBox.Items.Count);
|
vm.PipelineNodes.Count, PipelineListBox.Items.Count);
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<UserControl
|
<UserControl
|
||||||
x:Class="XplorePlane.Views.ImagePanelView"
|
x:Class="XplorePlane.Views.ImagePanelView"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:views="clr-namespace:XplorePlane.Views"
|
||||||
d:DesignHeight="400"
|
d:DesignHeight="400"
|
||||||
d:DesignWidth="250"
|
d:DesignWidth="250"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
@@ -13,9 +14,9 @@
|
|||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<Border Grid.Row="0" Background="#F0F0F0" BorderBrush="#DDDDDD" BorderThickness="0,0,0,1">
|
<Border Grid.Row="0" Background="#F0F0F0" BorderBrush="#DDDDDD" BorderThickness="0,0,0,1">
|
||||||
<TextBlock Margin="4,2" HorizontalAlignment="Left" VerticalAlignment="Center"
|
<TextBlock Margin="8,4" HorizontalAlignment="Left" VerticalAlignment="Center"
|
||||||
FontWeight="SemiBold" Foreground="#333333" Text="图像" />
|
FontWeight="SemiBold" Foreground="#333333" Text="图像" />
|
||||||
</Border>
|
</Border>
|
||||||
<ContentControl Grid.Row="1" Content="{Binding ImagePanelContent}" />
|
<views:PipelineEditorView Grid.Row="1" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
Height="1040"
|
Height="1040"
|
||||||
d:DesignWidth="1580"
|
d:DesignWidth="1580"
|
||||||
Background="#F5F5F5"
|
Background="#F5F5F5"
|
||||||
Icon="pack://application:,,,/XplorePlane;component/XplorerPlane.ico"
|
Icon="pack://application:,,,/XplorerPlane.ico"
|
||||||
WindowStartupLocation="CenterScreen"
|
WindowStartupLocation="CenterScreen"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
<Window.Resources>
|
<Window.Resources>
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
<StackPanel>
|
<StackPanel>
|
||||||
<telerik:RadRibbonButton
|
<telerik:RadRibbonButton
|
||||||
telerik:ScreenTip.Title="新建CNC"
|
telerik:ScreenTip.Title="新建CNC"
|
||||||
Command="{Binding NewCncProgramCommand}"
|
Command="{Binding Path=SetStyle.Command}"
|
||||||
Size="Medium"
|
Size="Medium"
|
||||||
SmallImage="/Assets/Icons/new-doc.png"
|
SmallImage="/Assets/Icons/new-doc.png"
|
||||||
Text="新建CNC" />
|
Text="新建CNC" />
|
||||||
@@ -82,12 +82,11 @@
|
|||||||
telerik:ScreenTip.Description="保存当前X射线实时图像"
|
telerik:ScreenTip.Description="保存当前X射线实时图像"
|
||||||
telerik:ScreenTip.Title="保存图像"
|
telerik:ScreenTip.Title="保存图像"
|
||||||
Size="Medium"
|
Size="Medium"
|
||||||
Command="{Binding SaveCncProgramCommand}"
|
|
||||||
SmallImage="/Assets/Icons/save.png"
|
SmallImage="/Assets/Icons/save.png"
|
||||||
Text="保存" />
|
Text="保存" />
|
||||||
<telerik:RadRibbonButton
|
<telerik:RadRibbonButton
|
||||||
telerik:ScreenTip.Title="另存为"
|
telerik:ScreenTip.Title="另存为"
|
||||||
Command="{Binding SaveCncProgramCommand}"
|
Command="{Binding OpenFileCommand}"
|
||||||
Size="Medium"
|
Size="Medium"
|
||||||
SmallImage="/Assets/Icons/saveas.png"
|
SmallImage="/Assets/Icons/saveas.png"
|
||||||
Text="另存为" />
|
Text="另存为" />
|
||||||
@@ -95,7 +94,7 @@
|
|||||||
<StackPanel>
|
<StackPanel>
|
||||||
<telerik:RadRibbonButton
|
<telerik:RadRibbonButton
|
||||||
telerik:ScreenTip.Title="加载CNC"
|
telerik:ScreenTip.Title="加载CNC"
|
||||||
Command="{Binding LoadCncProgramCommand}"
|
Command="{Binding OpenFileCommand}"
|
||||||
Size="Large"
|
Size="Large"
|
||||||
SmallImage="/Assets/Icons/open.png"
|
SmallImage="/Assets/Icons/open.png"
|
||||||
Text="加载CNC" />
|
Text="加载CNC" />
|
||||||
@@ -285,28 +284,32 @@
|
|||||||
Command="{Binding OpenCncEditorCommand}"
|
Command="{Binding OpenCncEditorCommand}"
|
||||||
Size="Large"
|
Size="Large"
|
||||||
SmallImage="/Assets/Icons/cnc.png"
|
SmallImage="/Assets/Icons/cnc.png"
|
||||||
Text="CNC 编辑" />
|
Text="CNC 编辑器" />
|
||||||
|
|
||||||
|
<!-- 矩阵编排入口按钮 -->
|
||||||
|
<telerik:RadRibbonButton
|
||||||
|
telerik:ScreenTip.Description="打开矩阵编排窗口,配置多工件阵列检测方案"
|
||||||
|
telerik:ScreenTip.Title="矩阵编排"
|
||||||
|
Command="{Binding OpenMatrixEditorCommand}"
|
||||||
|
Size="Large"
|
||||||
|
SmallImage="/Assets/Icons/matrix.png"
|
||||||
|
Text="矩阵编排" />
|
||||||
|
|
||||||
<!-- CNC 节点快捷工具 -->
|
<!-- CNC 节点快捷工具 -->
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<telerik:RadRibbonButton
|
<telerik:RadRibbonButton
|
||||||
telerik:ScreenTip.Title="参考点"
|
telerik:ScreenTip.Title="参考点"
|
||||||
Size="Medium"
|
Size="Medium"
|
||||||
Command="{Binding InsertReferencePointCommand}"
|
|
||||||
SmallImage="/Assets/Icons/reference.png"
|
SmallImage="/Assets/Icons/reference.png"
|
||||||
Text="参考点" />
|
Text="参考点" />
|
||||||
<telerik:RadRibbonButton
|
<telerik:RadRibbonButton
|
||||||
telerik:ScreenTip.Title="添加位置"
|
telerik:ScreenTip.Title="添加位置"
|
||||||
Size="Medium"
|
Size="Medium"
|
||||||
Command="{Binding InsertSavePositionCommand}"
|
|
||||||
SmallImage="/Assets/Icons/add-pos.png"
|
SmallImage="/Assets/Icons/add-pos.png"
|
||||||
Text="添加位置" />
|
Text="添加位置" />
|
||||||
<telerik:RadRibbonButton
|
<telerik:RadRibbonButton
|
||||||
telerik:ScreenTip.Title="完成"
|
telerik:ScreenTip.Title="完成"
|
||||||
Size="Medium"
|
Size="Medium"
|
||||||
Command="{Binding InsertCompleteProgramCommand}"
|
|
||||||
SmallImage="/Assets/Icons/finish.png"
|
SmallImage="/Assets/Icons/finish.png"
|
||||||
Text="完成" />
|
Text="完成" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -314,19 +317,16 @@
|
|||||||
<telerik:RadRibbonButton
|
<telerik:RadRibbonButton
|
||||||
telerik:ScreenTip.Title="标记"
|
telerik:ScreenTip.Title="标记"
|
||||||
Size="Medium"
|
Size="Medium"
|
||||||
Command="{Binding InsertInspectionMarkerCommand}"
|
|
||||||
SmallImage="/Assets/Icons/mark.png"
|
SmallImage="/Assets/Icons/mark.png"
|
||||||
Text="标记" />
|
Text="标记" />
|
||||||
<telerik:RadRibbonButton
|
<telerik:RadRibbonButton
|
||||||
telerik:ScreenTip.Title="模块"
|
telerik:ScreenTip.Title="模块"
|
||||||
Size="Medium"
|
Size="Medium"
|
||||||
Command="{Binding InsertInspectionModuleCommand}"
|
|
||||||
SmallImage="/Assets/Icons/Module.png"
|
SmallImage="/Assets/Icons/Module.png"
|
||||||
Text="检测模块" />
|
Text="检测模块" />
|
||||||
<telerik:RadRibbonButton
|
<telerik:RadRibbonButton
|
||||||
telerik:ScreenTip.Title="全部保存"
|
telerik:ScreenTip.Title="全部保存"
|
||||||
Size="Medium"
|
Size="Medium"
|
||||||
Command="{Binding SaveCncProgramCommand}"
|
|
||||||
SmallImage="/Assets/Icons/saveall.png"
|
SmallImage="/Assets/Icons/saveall.png"
|
||||||
Text="全部保存" />
|
Text="全部保存" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -334,26 +334,14 @@
|
|||||||
<telerik:RadRibbonButton
|
<telerik:RadRibbonButton
|
||||||
telerik:ScreenTip.Title="消息"
|
telerik:ScreenTip.Title="消息"
|
||||||
Size="Medium"
|
Size="Medium"
|
||||||
Command="{Binding InsertPauseDialogCommand}"
|
|
||||||
SmallImage="/Assets/Icons/message.png"
|
SmallImage="/Assets/Icons/message.png"
|
||||||
Text="消息弹窗" />
|
Text="消息弹窗" />
|
||||||
<telerik:RadRibbonButton
|
<telerik:RadRibbonButton
|
||||||
telerik:ScreenTip.Title="等待"
|
telerik:ScreenTip.Title="等待"
|
||||||
Size="Medium"
|
Size="Medium"
|
||||||
Command="{Binding InsertWaitDelayCommand}"
|
|
||||||
SmallImage="/Assets/Icons/wait.png"
|
SmallImage="/Assets/Icons/wait.png"
|
||||||
Text="插入等待" />
|
Text="插入等待" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- 矩阵编排入口按钮 -->
|
|
||||||
<telerik:RadRibbonButton
|
|
||||||
telerik:ScreenTip.Description="打开矩阵编排窗口,配置多工件阵列检测方案"
|
|
||||||
telerik:ScreenTip.Title="矩阵编排"
|
|
||||||
Command="{Binding OpenMatrixEditorCommand}"
|
|
||||||
Size="Large"
|
|
||||||
SmallImage="/Assets/Icons/matrix.png"
|
|
||||||
Text="矩阵编排" />
|
|
||||||
|
|
||||||
</telerik:RadRibbonGroup>
|
</telerik:RadRibbonGroup>
|
||||||
|
|
||||||
<telerik:RadRibbonGroup Header="高级模块" IsEnabled="{Binding Path=CellsGroup.IsEnabled}">
|
<telerik:RadRibbonGroup Header="高级模块" IsEnabled="{Binding Path=CellsGroup.IsEnabled}">
|
||||||
@@ -400,7 +388,7 @@
|
|||||||
Size="Large"
|
Size="Large"
|
||||||
SmallImage="/Assets/Icons/spiral.png" />
|
SmallImage="/Assets/Icons/spiral.png" />
|
||||||
</telerik:RadRibbonGroup>
|
</telerik:RadRibbonGroup>
|
||||||
<!--
|
<!--
|
||||||
<telerik:RadRibbonGroup Header="图像处理">
|
<telerik:RadRibbonGroup Header="图像处理">
|
||||||
<telerik:RadRibbonGroup.Variants>
|
<telerik:RadRibbonGroup.Variants>
|
||||||
<telerik:GroupVariant Priority="0" Variant="Large" />
|
<telerik:GroupVariant Priority="0" Variant="Large" />
|
||||||
@@ -470,9 +458,9 @@
|
|||||||
Margin="0">
|
Margin="0">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition x:Name="NavColumn" Width="0" />
|
<ColumnDefinition x:Name="NavColumn" Width="0" />
|
||||||
<ColumnDefinition Width="{Binding ViewportPanelWidth}" />
|
<ColumnDefinition Width="*" />
|
||||||
<ColumnDefinition Width="{Binding ImagePanelWidth}" />
|
<ColumnDefinition Width="350" />
|
||||||
<ColumnDefinition Width="300" />
|
<ColumnDefinition Width="350" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<!-- 左侧: 计划导航 (默认隐藏,点击CNC AccountingNumberFormatButton显示) -->
|
<!-- 左侧: 计划导航 (默认隐藏,点击CNC AccountingNumberFormatButton显示) -->
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<!-- 标题栏 -->
|
<!-- 标题栏 -->
|
||||||
@@ -56,50 +55,5 @@
|
|||||||
<Run Foreground="#0078D4" Text="{Binding CameraPixelCoord, Mode=OneWay}" />
|
<Run Foreground="#0078D4" Text="{Binding CameraPixelCoord, Mode=OneWay}" />
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- 控制按钮栏 -->
|
|
||||||
<Border
|
|
||||||
Grid.Row="3"
|
|
||||||
Background="#F0F0F0"
|
|
||||||
BorderBrush="#DDDDDD"
|
|
||||||
BorderThickness="0,1,0,0"
|
|
||||||
Padding="4">
|
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
|
||||||
<Button
|
|
||||||
Command="{Binding ConnectCameraCommand}"
|
|
||||||
Width="60" Height="26"
|
|
||||||
Margin="2"
|
|
||||||
Background="#4CAF50"
|
|
||||||
Foreground="#000000"
|
|
||||||
Content="连接" />
|
|
||||||
<Button
|
|
||||||
Command="{Binding DisconnectCameraCommand}"
|
|
||||||
Width="60" Height="26"
|
|
||||||
Margin="2"
|
|
||||||
Background="#F44336"
|
|
||||||
Foreground="#000000"
|
|
||||||
Content="断开" />
|
|
||||||
<Button
|
|
||||||
Command="{Binding StartGrabCommand}"
|
|
||||||
Width="60" Height="26"
|
|
||||||
Margin="2"
|
|
||||||
Background="#2196F3"
|
|
||||||
Foreground="#000000"
|
|
||||||
Content="采集" />
|
|
||||||
<Button
|
|
||||||
Command="{Binding StopGrabCommand}"
|
|
||||||
Width="60" Height="26"
|
|
||||||
Margin="2"
|
|
||||||
Background="#FF9800"
|
|
||||||
Foreground="#000000"
|
|
||||||
Content="停止" />
|
|
||||||
<CheckBox
|
|
||||||
IsChecked="{Binding IsLiveViewEnabled}"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Margin="6,0,0,0"
|
|
||||||
Foreground="#333333"
|
|
||||||
Content="实时" />
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@@ -28,8 +28,16 @@ namespace XplorePlane.Views
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 双击相机图像时,计算并显示点击位置的像素坐标。
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// TODO: 后续需要将点击的像素坐标通过 CalibrationProcessor 转换为世界坐标,
|
||||||
|
/// 再传给运动机构执行定位。
|
||||||
|
/// </remarks>
|
||||||
private void ImgCamera_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
private void ImgCamera_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||||
{
|
{
|
||||||
|
if (e.ClickCount != 2) return;
|
||||||
if (_viewModel?.CameraImageSource == null) return;
|
if (_viewModel?.CameraImageSource == null) return;
|
||||||
|
|
||||||
var image = (Image)sender;
|
var image = (Image)sender;
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
<!-- 标题栏 -->
|
<!-- 标题栏 -->
|
||||||
<Border Grid.Row="0" Background="#F0F0F0" BorderBrush="#DDDDDD" BorderThickness="0,0,0,1">
|
<Border Grid.Row="0" Background="#F0F0F0" BorderBrush="#DDDDDD" BorderThickness="0,0,0,1">
|
||||||
<TextBlock Margin="4,2" HorizontalAlignment="Left" VerticalAlignment="Center"
|
<TextBlock Margin="8,4" HorizontalAlignment="Left" VerticalAlignment="Center"
|
||||||
FontWeight="SemiBold" Foreground="#333333" Text="实时图像" />
|
FontWeight="SemiBold" Foreground="#333333" Text="实时图像" />
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
<!-- 图像信息栏 -->
|
<!-- 图像信息栏 -->
|
||||||
<Border Grid.Row="2" Background="#F0F0F0" BorderBrush="#DDDDDD" BorderThickness="0,1,0,0">
|
<Border Grid.Row="2" Background="#F0F0F0" BorderBrush="#DDDDDD" BorderThickness="0,1,0,0">
|
||||||
<TextBlock Margin="4,2" FontSize="11" Foreground="#666666"
|
<TextBlock Margin="8,2" FontSize="11" Foreground="#666666"
|
||||||
Text="{Binding ImageInfo}" />
|
Text="{Binding ImageInfo}" />
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -146,9 +146,6 @@
|
|||||||
<Link>Libs\Hardware\zh-TW\%(Filename)%(Extension)</Link>
|
<Link>Libs\Hardware\zh-TW\%(Filename)%(Extension)</Link>
|
||||||
</None>
|
</None>
|
||||||
<Compile Remove="Views\Main\MainWindowB.xaml.cs" />
|
<Compile Remove="Views\Main\MainWindowB.xaml.cs" />
|
||||||
<Content Include="XplorerPlane.ico">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</Content>
|
|
||||||
|
|
||||||
<Resource Include="XplorerPlane.ico" />
|
<Resource Include="XplorerPlane.ico" />
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 20 KiB |
Reference in New Issue
Block a user