Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| faf58b2768 | |||
| 3d6901f877 | |||
| 188bac53f1 | |||
| 12882bd1c6 | |||
| 8189e76492 | |||
| 80e71054f6 | |||
| 95b9a6a2ae | |||
| 9218384e3f | |||
| e166eca3d7 |
@@ -1,110 +1,110 @@
|
||||
## XplorePlane 平面CT软件
|
||||
|
||||
### 系统目标
|
||||
|
||||
XplorePlane 系统用于控制平面 CT 设备的各个子系统(射线源、探测器、运动控制、相机)并完成采集图像的处理与分析,为研发与调试提供统一的软件平台。
|
||||
|
||||
### 总体架构
|
||||
|
||||
- 客户端框架: WPF + Prism MVVM(目标框架 net8.0-windows)
|
||||
- 图像处理内核: ImageProcessing.Core(算子基类)+ ImageProcessing.Processors(具体算子),基于 EmguCV
|
||||
- 相机控制: XP.Camera(Basler pylon SDK 封装,支持软件触发、参数读写)
|
||||
- 硬件抽象: XP.Common + XP.Hardware.RaySource(射线源控制)
|
||||
- 日志: Serilog
|
||||
- UI 组件: Telerik RadRibbonView、Fluent.Ribbon
|
||||
|
||||
### 解决方案结构
|
||||
|
||||
```
|
||||
XplorePlane.sln
|
||||
├── XplorePlane/ # 主应用程序(WPF)
|
||||
├── XP.Camera/ # 相机控制库(Basler)
|
||||
├── ImageProcessing/ # 独立图像处理应用
|
||||
├── ImageProcessing.Core/ # 图像处理算子基类
|
||||
├── ImageProcessing.Processors/ # 具体算子实现
|
||||
├── ImageProcessing.Controls/ # 图像显示控件(ImageCanvasControl)
|
||||
├── ImageROIControl/ # ROI 绘制控件
|
||||
├── XplorePlane.Tests/ # 单元测试
|
||||
└── ExternalLibraries/ # 外部 DLL 和 ONNX 模型
|
||||
```
|
||||
|
||||
### XplorePlane 主项目结构
|
||||
|
||||
```
|
||||
XplorePlane/
|
||||
├── App.xaml / App.xaml.cs # 应用入口 + DI 容器配置(AppBootstrapper)
|
||||
├── Views/
|
||||
│ ├── Main/
|
||||
│ │ ├── MainWindow.xaml # 主窗口(Telerik Ribbon + 三栏布局)
|
||||
│ │ ├── NavigationPropertyPanelView.xaml # 相机实时预览面板
|
||||
│ │ └── MotionControlPanelView.xaml # 运动控制面板
|
||||
│ ├── Cnc/ # CNC 编辑器 / 矩阵编排视图
|
||||
│ ├── ImageProcessing/ # 图像处理面板视图
|
||||
│ └── CameraSettingsWindow.xaml # 相机参数设置对话框
|
||||
├── ViewModels/
|
||||
│ ├── Main/
|
||||
│ │ ├── MainViewModel.cs # 主窗口 ViewModel
|
||||
│ │ └── NavigationPropertyPanelViewModel.cs # 相机预览 ViewModel
|
||||
│ ├── Cnc/ # CNC / 矩阵 ViewModel
|
||||
│ └── ImageProcessing/ # 图像处理 / 流水线 ViewModel
|
||||
├── Services/
|
||||
│ ├── AppState/ # 全局状态管理(线程安全)
|
||||
│ ├── Camera/ # 相机服务
|
||||
│ ├── Cnc/ # CNC 程序服务
|
||||
│ ├── Matrix/ # 矩阵编排服务
|
||||
│ ├── Measurement/ # 测量数据服务
|
||||
│ ├── Pipeline/ # 流水线执行 / 持久化
|
||||
│ └── Recipe/ # 检测配方服务
|
||||
├── Models/ # 数据模型(State、CNC、Matrix、Pipeline 等)
|
||||
├── Events/ # Prism 事件
|
||||
├── Libs/
|
||||
│ ├── Hardware/ # 硬件库 DLL(XP.Common、XP.Hardware.RaySource)
|
||||
│ └── Native/ # 原生依赖库
|
||||
└── Assets/Icons/ # 工具栏图标
|
||||
```
|
||||
|
||||
### 相机集成
|
||||
|
||||
相机实时影像集成在主窗口左下角的 NavigationPropertyPanelView 中:
|
||||
|
||||
- 连接/断开相机(Basler,通过 ICameraController)
|
||||
- 开始/停止采集(软件触发模式)
|
||||
- 实时预览(Live View,勾选"实时"复选框)
|
||||
- 鼠标悬停显示像素坐标
|
||||
- 相机参数设置对话框(曝光时间、增益、分辨率、像素格式)
|
||||
- 主界面 Ribbon 硬件栏提供"相机设置"快捷按钮
|
||||
|
||||
相机控制逻辑移植自 ImageProcessing 项目,使用 XP.Camera.PixelConverter 进行像素数据转换,通过 Application.Dispatcher.Invoke 保证 UI 线程安全。
|
||||
|
||||
### 依赖注入(DI)
|
||||
|
||||
使用 Prism + DryIoc,在 AppBootstrapper.RegisterTypes() 中统一注册:
|
||||
|
||||
- ICameraFactory / ICameraController / ICameraService(单例)
|
||||
- IRaySourceService / IRaySourceFactory(单例)
|
||||
- IAppStateService(单例,线程安全状态管理)
|
||||
- NavigationPropertyPanelViewModel(单例,相机预览共享实例)
|
||||
- 各 Service 和 ViewModel(按需注册)
|
||||
|
||||
### 构建
|
||||
|
||||
```bash
|
||||
# Debug
|
||||
dotnet build XplorePlane.sln -c Debug
|
||||
|
||||
# Release
|
||||
dotnet build XplorePlane.sln -c Release
|
||||
```
|
||||
|
||||
### TO-DO List
|
||||
|
||||
- [x] 软件基于 WPF + Prism 基础的框架
|
||||
- [x] 日志库的引用(通过 XP.Common.dll)
|
||||
- [x] 按推荐的 DLL 目录结构进行修改
|
||||
- [x] 通过库依赖的方式调用日志功能
|
||||
- [x] 界面的布局
|
||||
- [x] 相机实时影像集成(连接、采集、Live View、像素坐标显示)
|
||||
- [x] 相机参数设置对话框(曝光、增益、分辨率、像素格式)
|
||||
- [x] 主界面硬件栏相机设置按钮
|
||||
- [ ] 打通与硬件层的调用流程
|
||||
- [ ] 打通与图像层的调用流程
|
||||
## XplorePlane 平面CT软件
|
||||
|
||||
### 系统目标
|
||||
|
||||
XplorePlane 系统用于控制平面 CT 设备的各个子系统(射线源、探测器、运动控制、相机)并完成采集图像的处理与分析,为研发与调试提供统一的软件平台。
|
||||
|
||||
### 总体架构
|
||||
|
||||
- 客户端框架: WPF + Prism MVVM(目标框架 net8.0-windows)
|
||||
- 图像处理内核: ImageProcessing.Core(算子基类)+ ImageProcessing.Processors(具体算子),基于 EmguCV
|
||||
- 相机控制: XP.Camera(Basler pylon SDK 封装,支持软件触发、参数读写)
|
||||
- 硬件抽象: XP.Common + XP.Hardware.RaySource(射线源控制)
|
||||
- 日志: Serilog
|
||||
- UI 组件: Telerik RadRibbonView、Fluent.Ribbon
|
||||
|
||||
### 解决方案结构
|
||||
|
||||
```
|
||||
XplorePlane.sln
|
||||
├── XplorePlane/ # 主应用程序(WPF)
|
||||
├── XP.Camera/ # 相机控制库(Basler)
|
||||
├── ImageProcessing/ # 独立图像处理应用
|
||||
├── ImageProcessing.Core/ # 图像处理算子基类
|
||||
├── ImageProcessing.Processors/ # 具体算子实现
|
||||
├── ImageProcessing.Controls/ # 图像显示控件(ImageCanvasControl)
|
||||
├── ImageROIControl/ # ROI 绘制控件
|
||||
├── XplorePlane.Tests/ # 单元测试
|
||||
└── ExternalLibraries/ # 外部 DLL 和 ONNX 模型
|
||||
```
|
||||
|
||||
### XplorePlane 主项目结构
|
||||
|
||||
```
|
||||
XplorePlane/
|
||||
├── App.xaml / App.xaml.cs # 应用入口 + DI 容器配置(AppBootstrapper)
|
||||
├── Views/
|
||||
│ ├── Main/
|
||||
│ │ ├── MainWindow.xaml # 主窗口(Telerik Ribbon + 三栏布局)
|
||||
│ │ ├── NavigationPropertyPanelView.xaml # 相机实时预览面板
|
||||
│ │ └── MotionControlPanelView.xaml # 运动控制面板
|
||||
│ ├── Cnc/ # CNC 编辑器 / 矩阵编排视图
|
||||
│ ├── ImageProcessing/ # 图像处理面板视图
|
||||
│ └── CameraSettingsWindow.xaml # 相机参数设置对话框
|
||||
├── ViewModels/
|
||||
│ ├── Main/
|
||||
│ │ ├── MainViewModel.cs # 主窗口 ViewModel
|
||||
│ │ └── NavigationPropertyPanelViewModel.cs # 相机预览 ViewModel
|
||||
│ ├── Cnc/ # CNC / 矩阵 ViewModel
|
||||
│ └── ImageProcessing/ # 图像处理 / 流水线 ViewModel
|
||||
├── Services/
|
||||
│ ├── AppState/ # 全局状态管理(线程安全)
|
||||
│ ├── Camera/ # 相机服务
|
||||
│ ├── Cnc/ # CNC 程序服务
|
||||
│ ├── Matrix/ # 矩阵编排服务
|
||||
│ ├── Measurement/ # 测量数据服务
|
||||
│ ├── Pipeline/ # 流水线执行 / 持久化
|
||||
│ └── Recipe/ # 检测配方服务
|
||||
├── Models/ # 数据模型(State、CNC、Matrix、Pipeline 等)
|
||||
├── Events/ # Prism 事件
|
||||
├── Libs/
|
||||
│ ├── Hardware/ # 硬件库 DLL(XP.Common、XP.Hardware.RaySource)
|
||||
│ └── Native/ # 原生依赖库
|
||||
└── Assets/Icons/ # 工具栏图标
|
||||
```
|
||||
|
||||
### 相机集成
|
||||
|
||||
相机实时影像集成在主窗口左下角的 NavigationPropertyPanelView 中:
|
||||
|
||||
- 连接/断开相机(Basler,通过 ICameraController)
|
||||
- 开始/停止采集(软件触发模式)
|
||||
- 实时预览(Live View,勾选"实时"复选框)
|
||||
- 鼠标悬停显示像素坐标
|
||||
- 相机参数设置对话框(曝光时间、增益、分辨率、像素格式)
|
||||
- 主界面 Ribbon 硬件栏提供"相机设置"快捷按钮
|
||||
|
||||
相机控制逻辑移植自 ImageProcessing 项目,使用 XP.Camera.PixelConverter 进行像素数据转换,通过 Application.Dispatcher.Invoke 保证 UI 线程安全。
|
||||
|
||||
### 依赖注入(DI)
|
||||
|
||||
使用 Prism + DryIoc,在 AppBootstrapper.RegisterTypes() 中统一注册:
|
||||
|
||||
- ICameraFactory / ICameraController / ICameraService(单例)
|
||||
- IRaySourceService / IRaySourceFactory(单例)
|
||||
- IAppStateService(单例,线程安全状态管理)
|
||||
- NavigationPropertyPanelViewModel(单例,相机预览共享实例)
|
||||
- 各 Service 和 ViewModel(按需注册)
|
||||
|
||||
### 构建
|
||||
|
||||
```bash
|
||||
# Debug
|
||||
dotnet build XplorePlane.sln -c Debug
|
||||
|
||||
# Release
|
||||
dotnet build XplorePlane.sln -c Release
|
||||
```
|
||||
|
||||
### TO-DO List
|
||||
|
||||
- [x] 软件基于 WPF + Prism 基础的框架
|
||||
- [x] 日志库的引用(通过 XP.Common.dll)
|
||||
- [x] 按推荐的 DLL 目录结构进行修改
|
||||
- [x] 通过库依赖的方式调用日志功能
|
||||
- [x] 界面的布局
|
||||
- [x] 相机实时影像集成(连接、采集、Live View、像素坐标显示)
|
||||
- [x] 相机参数设置对话框(曝光、增益、分辨率、像素格式)
|
||||
- [x] 主界面硬件栏相机设置按钮
|
||||
- [ ] 打通与硬件层的调用流程
|
||||
- [ ] 打通与图像层的调用流程
|
||||
|
||||
@@ -195,7 +195,6 @@ public class BaslerCameraController : ICameraController
|
||||
}
|
||||
|
||||
_camera.ExecuteSoftwareTrigger();
|
||||
_logger.Debug("Software trigger executed.");
|
||||
}
|
||||
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 使用说明
|
||||
|
||||
基于 .NET 8 WPF 的工业相机控制类库,采用工厂模式 + 统一接口设计,支持多品牌相机扩展。当前已实现 Basler pylon SDK 驱动。
|
||||
基于 .NET 8 WPF 的工业相机控制与标定类库,采用工厂模式 + 统一接口设计,支持多品牌相机扩展。当前已实现 Basler pylon SDK 驱动。
|
||||
|
||||
## 环境要求
|
||||
|
||||
@@ -12,16 +12,36 @@
|
||||
|
||||
```
|
||||
XP.Camera/
|
||||
├── ICameraController.cs # 控制器接口 + 工厂接口
|
||||
├── CameraFactory.cs # 统一工厂(根据品牌创建控制器)
|
||||
├── BaslerCameraController.cs # Basler 实现
|
||||
├── CameraModels.cs # CameraInfo、ImageGrabbedEventArgs、GrabErrorEventArgs
|
||||
├── CameraExceptions.cs # CameraException、ConnectionLostException、DeviceNotFoundException
|
||||
├── PixelConverter.cs # 像素数据 → WPF BitmapSource 转换工具
|
||||
├── Core/ # 相机核心抽象
|
||||
│ ├── ICameraController.cs # 控制器接口 + 工厂接口
|
||||
│ ├── CameraFactory.cs # 统一工厂(根据品牌创建控制器)
|
||||
│ ├── CameraModels.cs # CameraInfo、ImageGrabbedEventArgs、GrabErrorEventArgs
|
||||
│ └── CameraExceptions.cs # CameraException、ConnectionLostException、DeviceNotFoundException
|
||||
├── 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` 命名空间下。
|
||||
所有相机核心类型在 `XP.Camera` 命名空间下,标定模块在 `XP.Camera.Calibration` 命名空间下。
|
||||
|
||||
## 项目引用
|
||||
|
||||
@@ -48,27 +68,12 @@ Console.WriteLine($"已连接: {info.ModelName} (SN: {info.SerialNumber})");
|
||||
在 Prism / DI 容器中注册:
|
||||
|
||||
```csharp
|
||||
// App.xaml.cs
|
||||
var config = AppConfig.Load();
|
||||
containerRegistry.RegisterSingleton<ICameraFactory, CameraFactory>();
|
||||
containerRegistry.RegisterSingleton<ICameraController>(() =>
|
||||
new CameraFactory().CreateController(config.CameraType));
|
||||
```
|
||||
|
||||
ViewModel 中注入使用:
|
||||
|
||||
```csharp
|
||||
public class MyViewModel
|
||||
{
|
||||
private readonly ICameraController _camera;
|
||||
|
||||
public MyViewModel(ICameraController camera)
|
||||
{
|
||||
_camera = camera;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
相机品牌通过配置文件 `config.json` 指定:
|
||||
|
||||
```json
|
||||
@@ -82,60 +87,85 @@ public class MyViewModel
|
||||
```csharp
|
||||
_camera.ImageGrabbed += (s, e) =>
|
||||
{
|
||||
// PixelConverter 返回已 Freeze 的 BitmapSource,可跨线程传递
|
||||
var bitmap = PixelConverter.ToBitmapSource(
|
||||
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. 软件触发采集流程
|
||||
|
||||
```csharp
|
||||
camera.Open();
|
||||
camera.SetExposureTime(10000); // 10ms
|
||||
camera.StartGrabbing();
|
||||
|
||||
// 每次需要采集时调用(结果通过 ImageGrabbed 事件返回)
|
||||
camera.ExecuteSoftwareTrigger();
|
||||
|
||||
camera.StopGrabbing();
|
||||
camera.Close();
|
||||
```
|
||||
|
||||
### 5. 实时连续采集(链式触发)
|
||||
### 5. 使用标定模块
|
||||
|
||||
收到上一帧后立即触发下一帧,自动适配任何帧率:
|
||||
标定模块完全自包含,可独立使用,无需外部依赖。
|
||||
|
||||
#### 棋盘格标定(相机内参 + 畸变校正)
|
||||
|
||||
```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);
|
||||
Application.Current.Dispatcher.Invoke(() => CameraImageSource = bitmap);
|
||||
|
||||
if (_liveViewRunning)
|
||||
_camera.ExecuteSoftwareTrigger(); // 链式触发下一帧
|
||||
Title = "棋盘格标定",
|
||||
Width = 1600, Height = 900,
|
||||
Content = new ChessboardCalibrationControl { DataContext = viewModel }
|
||||
};
|
||||
window.ShowDialog();
|
||||
```
|
||||
|
||||
// 启动实时
|
||||
_camera.StartGrabbing();
|
||||
_liveViewRunning = true;
|
||||
_camera.ExecuteSoftwareTrigger(); // 触发第一帧
|
||||
#### 九点标定(像素→世界坐标)
|
||||
|
||||
// 停止实时
|
||||
_liveViewRunning = false;
|
||||
```csharp
|
||||
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()` | 以软件触发模式启动采集 |
|
||||
| `ExecuteSoftwareTrigger()` | 触发一帧采集 |
|
||||
| `StopGrabbing()` | 停止采集 |
|
||||
| `Get/SetExposureTime` | 曝光时间(微秒) |
|
||||
| `Get/SetGain` | 增益值 |
|
||||
| `Get/SetWidth/Height` | 图像尺寸 |
|
||||
| `Get/SetPixelFormat` | 像素格式(Mono8 / BGR8 / BGRA8) |
|
||||
|
||||
### 参数读写
|
||||
### 事件
|
||||
|
||||
| 方法 | 说明 |
|
||||
| 事件 | 说明 |
|
||||
|------|------|
|
||||
| `Get/SetExposureTime(double)` | 曝光时间(微秒) |
|
||||
| `Get/SetGain(double)` | 增益值 |
|
||||
| `Get/SetWidth(int)` | 图像宽度(自动校正到有效值) |
|
||||
| `Get/SetHeight(int)` | 图像高度(自动校正到有效值) |
|
||||
| `Get/SetPixelFormat(string)` | 像素格式(Mono8 / BGR8 / BGRA8) |
|
||||
| `ImageGrabbed` | 成功采集一帧图像 |
|
||||
| `GrabError` | 图像采集失败 |
|
||||
| `ConnectionLost` | 相机连接意外断开 |
|
||||
|
||||
### ICameraFactory
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `CreateController(string cameraType)` | 根据品牌名创建控制器 |
|
||||
|
||||
当前支持的 `cameraType` 值:`"Basler"`
|
||||
|
||||
## 事件
|
||||
|
||||
| 事件 | 说明 | 触发线程 |
|
||||
|------|------|----------|
|
||||
| `ImageGrabbed` | 成功采集一帧图像 | StreamGrabber 回调线程 |
|
||||
| `GrabError` | 图像采集失败 | StreamGrabber 回调线程 |
|
||||
| `ConnectionLost` | 相机连接意外断开 | pylon SDK 事件线程 |
|
||||
|
||||
> 所有事件均在非 UI 线程触发。更新 WPF 界面时需通过 `Dispatcher.Invoke` 调度。
|
||||
> `PixelConverter.ToBitmapSource()` 返回的 BitmapSource 已调用 `Freeze()`,可直接跨线程传递。
|
||||
> 所有事件均在非 UI 线程触发,更新 WPF 界面时需通过 `Dispatcher.Invoke` 调度。
|
||||
|
||||
## 异常处理
|
||||
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
camera.Open();
|
||||
}
|
||||
catch (DeviceNotFoundException)
|
||||
{
|
||||
// 无可用相机设备
|
||||
}
|
||||
catch (CameraException ex)
|
||||
{
|
||||
// 其他相机错误,ex.InnerException 包含原始 SDK 异常
|
||||
}
|
||||
```
|
||||
|
||||
| 异常类型 | 场景 |
|
||||
|---------|------|
|
||||
| `DeviceNotFoundException` | 无可用相机 |
|
||||
| `ConnectionLostException` | 相机物理断开 |
|
||||
| `CameraException` | SDK 操作失败(基类) |
|
||||
| `InvalidOperationException` | 未连接时访问参数,未采集时触发 |
|
||||
| `TimeoutException` | 软件触发等待超时 |
|
||||
|
||||
## 扩展其他品牌相机
|
||||
|
||||
1. 实现 `ICameraController` 接口:
|
||||
|
||||
```csharp
|
||||
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. 配置文件切换品牌即可,业务代码无需修改。
|
||||
1. 在 `Basler/` 同级创建新文件夹,实现 `ICameraController` 接口
|
||||
2. 在 `CameraFactory.cs` 中注册新品牌
|
||||
3. 配置文件切换品牌即可,业务代码无需修改
|
||||
|
||||
## 线程安全
|
||||
|
||||
- 所有公共方法(Open / Close / StartGrabbing / StopGrabbing / ExecuteSoftwareTrigger / 参数读写)均线程安全
|
||||
- 所有公共方法均线程安全
|
||||
- 事件回调不持有内部锁,不会导致死锁
|
||||
- `Open()` / `Close()` 幂等,重复调用安全
|
||||
|
||||
## 日志
|
||||
|
||||
使用 Serilog 静态 API(`Log.ForContext<T>()`),与宿主应用共享同一个日志管道。宿主应用只需在启动时配置 `Log.Logger` 即可。
|
||||
|
||||
@@ -13,7 +13,24 @@
|
||||
<Reference Include="Basler.Pylon">
|
||||
<HintPath>..\ExternalLibraries\Basler.Pylon.dll</HintPath>
|
||||
</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" />
|
||||
<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>
|
||||
|
||||
</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.Dump.Configs;
|
||||
|
||||
|
||||
@@ -1878,7 +1878,10 @@
|
||||
"Telerik.UI.for.Wpf.NetCore.Xaml": "2024.1.408"
|
||||
},
|
||||
"runtime": {
|
||||
"XP.Common.dll": {}
|
||||
"XP.Common.dll": {
|
||||
"assemblyVersion": "1.4.16.1",
|
||||
"fileVersion": "1.4.16.1"
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"en-US/XP.Common.resources.dll": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Prism.Mvvm;
|
||||
using Prism.Mvvm;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Prism.Ioc;
|
||||
using Prism.Ioc;
|
||||
using Prism.Modularity;
|
||||
using System.Resources;
|
||||
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" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="ImageProcessing.Core">
|
||||
<HintPath>..\XplorePlane\Libs\ImageProcessing\ImageProcessing.Core.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
+74
-11
@@ -13,6 +13,7 @@ using XP.Camera;
|
||||
using XP.Common.Configs;
|
||||
using XP.Common.Database.Implementations;
|
||||
using XP.Common.Database.Interfaces;
|
||||
using XP.Common.GeneralForm.Views;
|
||||
using XP.Common.Dump.Configs;
|
||||
using XP.Common.Dump.Implementations;
|
||||
using XP.Common.Dump.Interfaces;
|
||||
@@ -36,7 +37,6 @@ using XplorePlane.Services.Camera;
|
||||
using XplorePlane.Services.Cnc;
|
||||
using XplorePlane.Services.Matrix;
|
||||
using XplorePlane.Services.Measurement;
|
||||
using XplorePlane.Services.InspectionResults;
|
||||
using XplorePlane.Services.Recipe;
|
||||
using XplorePlane.ViewModels;
|
||||
using XplorePlane.ViewModels.Cnc;
|
||||
@@ -150,7 +150,23 @@ namespace XplorePlane
|
||||
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
|
||||
{
|
||||
var bootstrapper = AppBootstrapper.Instance;
|
||||
@@ -158,12 +174,12 @@ namespace XplorePlane
|
||||
{
|
||||
var cameraService = bootstrapper.Container.Resolve<ICameraService>();
|
||||
cameraService?.Dispose();
|
||||
Log.Information("相机服务资源已释放");
|
||||
Log.Information("导航相机服务资源已释放");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "相机服务资源释放失败");
|
||||
Log.Error(ex, "导航相机服务资源释放失败");
|
||||
}
|
||||
|
||||
// 释放SQLite数据库资源 | Release SQLite database resources
|
||||
@@ -232,19 +248,67 @@ namespace XplorePlane
|
||||
|
||||
private bool _modulesInitialized = false;
|
||||
|
||||
private string? _cameraError;
|
||||
|
||||
protected override Window CreateShell()
|
||||
{
|
||||
// 提前初始化模块,确保硬件服务在 MainWindow XAML 解析前已注册
|
||||
// 默认 Prism 顺序是 CreateShell → InitializeModules,
|
||||
// 但 MainWindow 中嵌入的硬件控件会在 XAML 解析时触发 ViewModelLocator,
|
||||
// 此时模块尚未加载,导致依赖解析失败
|
||||
if (!_modulesInitialized)
|
||||
{
|
||||
base.InitializeModules();
|
||||
_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>
|
||||
@@ -318,7 +382,6 @@ namespace XplorePlane
|
||||
containerRegistry.RegisterSingleton<ICncProgramService, CncProgramService>();
|
||||
containerRegistry.RegisterSingleton<IMatrixService, MatrixService>();
|
||||
containerRegistry.RegisterSingleton<IMeasurementDataService, MeasurementDataService>();
|
||||
containerRegistry.RegisterSingleton<IInspectionResultStore, InspectionResultStore>();
|
||||
|
||||
// ── CNC / 矩阵 ViewModel(瞬态)──
|
||||
containerRegistry.Register<CncEditorViewModel>();
|
||||
@@ -329,7 +392,7 @@ namespace XplorePlane
|
||||
containerRegistry.RegisterForNavigation<CncPageView>();
|
||||
containerRegistry.RegisterForNavigation<MatrixPageView>();
|
||||
|
||||
// ── 相机服务(单例)──
|
||||
// ── 导航相机服务(单例)──
|
||||
containerRegistry.RegisterSingleton<ICameraFactory, CameraFactory>();
|
||||
containerRegistry.RegisterSingleton<ICameraController>(() =>
|
||||
new CameraFactory().CreateController("Basler"));
|
||||
@@ -356,4 +419,4 @@ namespace XplorePlane
|
||||
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>
|
||||
public record ReferencePointNode(
|
||||
Guid Id, int Index, string Name,
|
||||
double XM, double YM, double ZT, double ZD, double TiltD, double Dist,
|
||||
bool IsRayOn, double Voltage, double Current
|
||||
double XM, double YM, double ZT, double ZD, double TiltD, double Dist
|
||||
) : CncNode(Id, Index, CncNodeType.ReferencePoint, Name);
|
||||
|
||||
/// <summary>保存节点(含图像)| Save node with image</summary>
|
||||
@@ -114,4 +113,4 @@ namespace XplorePlane.Models
|
||||
DateTime UpdatedAt,
|
||||
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.Threading.Tasks;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XP.Hardware.RaySource.Services;
|
||||
using XplorePlane.Models;
|
||||
using XplorePlane.Services.AppState;
|
||||
|
||||
@@ -21,7 +20,6 @@ namespace XplorePlane.Services.Cnc
|
||||
public class CncProgramService : ICncProgramService
|
||||
{
|
||||
private readonly IAppStateService _appStateService;
|
||||
private readonly IRaySourceService _raySourceService;
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
// ── 序列化配置 | Serialization options ──
|
||||
@@ -34,15 +32,12 @@ namespace XplorePlane.Services.Cnc
|
||||
|
||||
public CncProgramService(
|
||||
IAppStateService appStateService,
|
||||
IRaySourceService raySourceService,
|
||||
ILoggerService logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(appStateService);
|
||||
ArgumentNullException.ThrowIfNull(raySourceService);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
_appStateService = appStateService;
|
||||
_raySourceService = raySourceService;
|
||||
_logger = logger.ForModule<CncProgramService>();
|
||||
|
||||
_logger.Info("CncProgramService 已初始化 | CncProgramService initialized");
|
||||
@@ -205,32 +200,6 @@ namespace XplorePlane.Services.Cnc
|
||||
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 />
|
||||
public async Task SaveAsync(CncProgram program, string filePath)
|
||||
{
|
||||
@@ -375,7 +344,6 @@ namespace XplorePlane.Services.Cnc
|
||||
private ReferencePointNode CreateReferencePointNode(Guid id, int index)
|
||||
{
|
||||
var motion = _appStateService.MotionState;
|
||||
var raySource = _appStateService.RaySourceState;
|
||||
return new ReferencePointNode(
|
||||
id, index, $"参考点_{index}",
|
||||
XM: motion.XM,
|
||||
@@ -383,10 +351,7 @@ namespace XplorePlane.Services.Cnc
|
||||
ZT: motion.ZT,
|
||||
ZD: motion.ZD,
|
||||
TiltD: motion.TiltD,
|
||||
Dist: motion.Dist,
|
||||
IsRayOn: raySource.IsOn,
|
||||
Voltage: raySource.Voltage,
|
||||
Current: TryReadCurrent());
|
||||
Dist: motion.Dist);
|
||||
}
|
||||
|
||||
/// <summary>创建保存节点(含图像)| Create save node with image</summary>
|
||||
@@ -417,24 +382,5 @@ namespace XplorePlane.Services.Cnc
|
||||
id, index, $"保存位置_{index}",
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// CNC program management service interface.
|
||||
/// CNC 程序管理服务接口,负责程序的创建、节点编辑、序列化/反序列化和文件读写
|
||||
/// CNC program management service interface for creation, node editing, serialization and file I/O
|
||||
/// </summary>
|
||||
public interface ICncProgramService
|
||||
{
|
||||
/// <summary>创建空的 CNC 程序 | Create an empty CNC program</summary>
|
||||
CncProgram CreateProgram(string name);
|
||||
|
||||
/// <summary>根据节点类型创建节点(从 IAppStateService 捕获设备状态)| Create a node by type (captures device state from IAppStateService)</summary>
|
||||
CncNode CreateNode(CncNodeType type);
|
||||
|
||||
/// <summary>在指定索引之后插入节点并重新编号 | Insert a node after the given index and renumber</summary>
|
||||
CncProgram InsertNode(CncProgram program, int afterIndex, CncNode node);
|
||||
|
||||
/// <summary>移除指定索引的节点并重新编号 | Remove the node at the given index and renumber</summary>
|
||||
CncProgram RemoveNode(CncProgram program, int index);
|
||||
|
||||
/// <summary>将节点从旧索引移动到新索引并重新编号 | Move a node from old index to new index and renumber</summary>
|
||||
CncProgram MoveNode(CncProgram program, int oldIndex, int newIndex);
|
||||
|
||||
CncProgram UpdateNode(CncProgram program, int index, CncNode node);
|
||||
|
||||
/// <summary>将 CNC 程序保存到 .xp 文件 | Save CNC program to .xp file</summary>
|
||||
Task SaveAsync(CncProgram program, string filePath);
|
||||
|
||||
/// <summary>从 .xp 文件加载 CNC 程序 | Load CNC program from .xp file</summary>
|
||||
Task<CncProgram> LoadAsync(string filePath);
|
||||
|
||||
/// <summary>将 CNC 程序序列化为 JSON 字符串 | Serialize CNC program to JSON string</summary>
|
||||
string Serialize(CncProgram program);
|
||||
|
||||
/// <summary>从 JSON 字符串反序列化 CNC 程序 | Deserialize CNC program from JSON string</summary>
|
||||
CncProgram Deserialize(string json);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,11 +28,11 @@ namespace XplorePlane.Services
|
||||
var formatted = new FormatConvertedBitmap(bitmapSource, PixelFormats.Gray8, null, 0);
|
||||
int width = formatted.PixelWidth;
|
||||
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);
|
||||
int stride = image.Bytes.Length / height;
|
||||
var pixels = new byte[height * stride];
|
||||
formatted.CopyPixels(pixels, stride, 0);
|
||||
image.Bytes = pixels;
|
||||
return image;
|
||||
}
|
||||
@@ -40,7 +40,19 @@ namespace XplorePlane.Services
|
||||
public static Image<Gray, byte> ToEmguCVFromPixels(byte[] pixels, int width, int 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;
|
||||
}
|
||||
|
||||
@@ -50,8 +62,8 @@ namespace XplorePlane.Services
|
||||
|
||||
int width = emguImage.Width;
|
||||
int height = emguImage.Height;
|
||||
int stride = width;
|
||||
byte[] pixels = emguImage.Bytes;
|
||||
int stride = pixels.Length / height;
|
||||
|
||||
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.Mvvm;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
@@ -19,21 +18,27 @@ using XplorePlane.Services.Cnc;
|
||||
namespace XplorePlane.ViewModels.Cnc
|
||||
{
|
||||
/// <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>
|
||||
public class CncEditorViewModel : BindableBase
|
||||
{
|
||||
private readonly ICncProgramService _cncProgramService;
|
||||
private readonly IAppStateService _appStateService;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
// 当前 CNC 程序 | Current CNC program
|
||||
private CncProgram _currentProgram;
|
||||
|
||||
private ObservableCollection<CncNodeViewModel> _nodes;
|
||||
private ObservableCollection<CncNodeViewModel> _treeNodes;
|
||||
private CncNodeViewModel _selectedNode;
|
||||
private bool _isModified;
|
||||
private string _programName;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
public CncEditorViewModel(
|
||||
ICncProgramService cncProgramService,
|
||||
IAppStateService appStateService,
|
||||
@@ -41,13 +46,13 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
ILoggerService logger)
|
||||
{
|
||||
_cncProgramService = cncProgramService ?? throw new ArgumentNullException(nameof(cncProgramService));
|
||||
ArgumentNullException.ThrowIfNull(appStateService);
|
||||
_appStateService = appStateService ?? throw new ArgumentNullException(nameof(appStateService));
|
||||
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
|
||||
_logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule<CncEditorViewModel>();
|
||||
|
||||
_nodes = new ObservableCollection<CncNodeViewModel>();
|
||||
_treeNodes = new ObservableCollection<CncNodeViewModel>();
|
||||
|
||||
// ── 节点插入命令 | Node insertion commands ──
|
||||
InsertReferencePointCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.ReferencePoint));
|
||||
InsertSaveNodeWithImageCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNodeWithImage));
|
||||
InsertSaveNodeCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNode));
|
||||
@@ -58,79 +63,117 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
InsertWaitDelayCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.WaitDelay));
|
||||
InsertCompleteProgramCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.CompleteProgram));
|
||||
|
||||
// ── 节点编辑命令 | Node editing commands ──
|
||||
DeleteNodeCommand = new DelegateCommand(ExecuteDeleteNode, CanExecuteDeleteNode)
|
||||
.ObservesProperty(() => SelectedNode);
|
||||
MoveNodeUpCommand = new DelegateCommand<CncNodeViewModel>(ExecuteMoveNodeUp);
|
||||
MoveNodeDownCommand = new DelegateCommand<CncNodeViewModel>(ExecuteMoveNodeDown);
|
||||
|
||||
// ── 文件操作命令 | File operation commands ──
|
||||
SaveProgramCommand = new DelegateCommand(async () => await ExecuteSaveProgramAsync());
|
||||
LoadProgramCommand = new DelegateCommand(async () => await ExecuteLoadProgramAsync());
|
||||
NewProgramCommand = new DelegateCommand(ExecuteNewProgram);
|
||||
ExportCsvCommand = new DelegateCommand(ExecuteExportCsv);
|
||||
|
||||
_logger.Info("CncEditorViewModel initialized");
|
||||
_logger.Info("CncEditorViewModel 已初始化 | CncEditorViewModel initialized");
|
||||
}
|
||||
|
||||
// ── 属性 | Properties ──────────────────────────────────────────
|
||||
|
||||
/// <summary>节点列表 | Node list</summary>
|
||||
public ObservableCollection<CncNodeViewModel> Nodes
|
||||
{
|
||||
get => _nodes;
|
||||
private set => SetProperty(ref _nodes, value);
|
||||
}
|
||||
|
||||
public ObservableCollection<CncNodeViewModel> TreeNodes
|
||||
{
|
||||
get => _treeNodes;
|
||||
private set => SetProperty(ref _treeNodes, value);
|
||||
set => SetProperty(ref _nodes, value);
|
||||
}
|
||||
|
||||
/// <summary>当前选中的节点 | Currently selected node</summary>
|
||||
public CncNodeViewModel SelectedNode
|
||||
{
|
||||
get => _selectedNode;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _selectedNode, value))
|
||||
{
|
||||
RaisePropertyChanged(nameof(HasSelection));
|
||||
}
|
||||
}
|
||||
set => SetProperty(ref _selectedNode, value);
|
||||
}
|
||||
|
||||
public bool HasSelection => SelectedNode != null;
|
||||
|
||||
/// <summary>程序是否已修改 | Whether the program has been modified</summary>
|
||||
public bool IsModified
|
||||
{
|
||||
get => _isModified;
|
||||
set => SetProperty(ref _isModified, value);
|
||||
}
|
||||
|
||||
/// <summary>当前程序名称 | Current program name</summary>
|
||||
public string ProgramName
|
||||
{
|
||||
get => _programName;
|
||||
set => SetProperty(ref _programName, value);
|
||||
}
|
||||
|
||||
// ── 节点插入命令 | Node insertion commands ──────────────────────
|
||||
|
||||
/// <summary>插入参考点命令 | Insert reference point command</summary>
|
||||
public DelegateCommand InsertReferencePointCommand { get; }
|
||||
|
||||
/// <summary>插入保存节点(含图像)命令 | Insert save node with image command</summary>
|
||||
public DelegateCommand InsertSaveNodeWithImageCommand { get; }
|
||||
|
||||
/// <summary>插入保存节点命令 | Insert save node command</summary>
|
||||
public DelegateCommand InsertSaveNodeCommand { get; }
|
||||
|
||||
/// <summary>插入保存位置命令 | Insert save position command</summary>
|
||||
public DelegateCommand InsertSavePositionCommand { get; }
|
||||
|
||||
/// <summary>插入检测模块命令 | Insert inspection module command</summary>
|
||||
public DelegateCommand InsertInspectionModuleCommand { get; }
|
||||
|
||||
/// <summary>插入检测标记命令 | Insert inspection marker command</summary>
|
||||
public DelegateCommand InsertInspectionMarkerCommand { get; }
|
||||
|
||||
/// <summary>插入停顿对话框命令 | Insert pause dialog command</summary>
|
||||
public DelegateCommand InsertPauseDialogCommand { get; }
|
||||
|
||||
/// <summary>插入等待延时命令 | Insert wait delay command</summary>
|
||||
public DelegateCommand InsertWaitDelayCommand { get; }
|
||||
|
||||
/// <summary>插入完成程序命令 | Insert complete program command</summary>
|
||||
public DelegateCommand InsertCompleteProgramCommand { get; }
|
||||
|
||||
// ── 节点编辑命令 | Node editing commands ────────────────────────
|
||||
|
||||
/// <summary>删除选中节点命令 | Delete selected node command</summary>
|
||||
public DelegateCommand DeleteNodeCommand { get; }
|
||||
|
||||
/// <summary>上移节点命令 | Move node up command</summary>
|
||||
public DelegateCommand<CncNodeViewModel> MoveNodeUpCommand { get; }
|
||||
|
||||
/// <summary>下移节点命令 | Move node down command</summary>
|
||||
public DelegateCommand<CncNodeViewModel> MoveNodeDownCommand { get; }
|
||||
|
||||
// ── 文件操作命令 | File operation commands ──────────────────────
|
||||
|
||||
/// <summary>保存程序命令 | Save program command</summary>
|
||||
public DelegateCommand SaveProgramCommand { get; }
|
||||
|
||||
/// <summary>加载程序命令 | Load program command</summary>
|
||||
public DelegateCommand LoadProgramCommand { get; }
|
||||
|
||||
/// <summary>新建程序命令 | New program command</summary>
|
||||
public DelegateCommand NewProgramCommand { get; }
|
||||
|
||||
/// <summary>导出 CSV 命令 | Export CSV command</summary>
|
||||
public DelegateCommand ExportCsvCommand { get; }
|
||||
|
||||
// ── 命令执行方法 | Command execution methods ────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 插入指定类型的节点到选中节点之后
|
||||
/// Insert a node of the specified type after the selected node
|
||||
/// </summary>
|
||||
private void ExecuteInsertNode(CncNodeType nodeType)
|
||||
{
|
||||
if (_currentProgram == null)
|
||||
{
|
||||
ExecuteNewProgram();
|
||||
_logger.Warn("无法插入节点:当前无程序 | Cannot insert node: no current program");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
@@ -140,14 +183,18 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
_currentProgram = _cncProgramService.InsertNode(_currentProgram, afterIndex, node);
|
||||
|
||||
OnProgramEdited();
|
||||
_logger.Info("Inserted node: Type={NodeType}", nodeType);
|
||||
_logger.Info("已插入节点 | Inserted node: Type={NodeType}", nodeType);
|
||||
}
|
||||
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()
|
||||
{
|
||||
if (_currentProgram == null || SelectedNode == null)
|
||||
@@ -157,14 +204,18 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
{
|
||||
_currentProgram = _cncProgramService.RemoveNode(_currentProgram, SelectedNode.Index);
|
||||
OnProgramEdited();
|
||||
_logger.Info("Deleted node at index: {Index}", SelectedNode.Index);
|
||||
_logger.Info("已删除节点 | Deleted node at index: {Index}", SelectedNode.Index);
|
||||
}
|
||||
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()
|
||||
{
|
||||
return SelectedNode != null
|
||||
@@ -172,6 +223,9 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
&& _currentProgram.Nodes.Count > 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上移节点 | Move node up
|
||||
/// </summary>
|
||||
private void ExecuteMoveNodeUp(CncNodeViewModel nodeVm)
|
||||
{
|
||||
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);
|
||||
OnProgramEdited();
|
||||
_logger.Info("已上移节点 | Moved node up: {OldIndex} -> {NewIndex}", nodeVm.Index, nodeVm.Index - 1);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException ex)
|
||||
{
|
||||
_logger.Warn("Move node up failed: {Message}", ex.Message);
|
||||
_logger.Warn("上移节点失败 | Move node up failed: {Message}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下移节点 | Move node down
|
||||
/// </summary>
|
||||
private void ExecuteMoveNodeDown(CncNodeViewModel nodeVm)
|
||||
{
|
||||
if (_currentProgram == null || nodeVm == null || nodeVm.Index >= _currentProgram.Nodes.Count - 1)
|
||||
@@ -197,18 +255,22 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
{
|
||||
_currentProgram = _cncProgramService.MoveNode(_currentProgram, nodeVm.Index, nodeVm.Index + 1);
|
||||
OnProgramEdited();
|
||||
_logger.Info("已下移节点 | Moved node down: {OldIndex} -> {NewIndex}", nodeVm.Index, nodeVm.Index + 1);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException ex)
|
||||
{
|
||||
_logger.Warn("Move node down failed: {Message}", ex.Message);
|
||||
_logger.Warn("下移节点失败 | Move node down failed: {Message}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存当前程序到文件 | Save current program to file
|
||||
/// </summary>
|
||||
private async Task ExecuteSaveProgramAsync()
|
||||
{
|
||||
if (_currentProgram == null)
|
||||
{
|
||||
_logger.Warn("Cannot save: no current program");
|
||||
_logger.Warn("无法保存:当前无程序 | Cannot save: no current program");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -227,13 +289,17 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
|
||||
await _cncProgramService.SaveAsync(_currentProgram, dlg.FileName);
|
||||
IsModified = false;
|
||||
_logger.Info("程序已保存 | Program saved: {FilePath}", dlg.FileName);
|
||||
}
|
||||
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()
|
||||
{
|
||||
try
|
||||
@@ -252,27 +318,35 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
ProgramName = _currentProgram.Name;
|
||||
IsModified = false;
|
||||
RefreshNodes();
|
||||
_logger.Info("程序已加载 | Program loaded: {ProgramName}", _currentProgram.Name);
|
||||
}
|
||||
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()
|
||||
{
|
||||
var name = string.IsNullOrWhiteSpace(ProgramName) ? "NewCncProgram" : ProgramName;
|
||||
var name = string.IsNullOrWhiteSpace(ProgramName) ? "新程序" : ProgramName;
|
||||
_currentProgram = _cncProgramService.CreateProgram(name);
|
||||
ProgramName = _currentProgram.Name;
|
||||
IsModified = false;
|
||||
RefreshNodes();
|
||||
_logger.Info("已创建新程序 | Created new program: {ProgramName}", name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出当前程序为 CSV 文件 | Export current program to CSV file
|
||||
/// </summary>
|
||||
private void ExecuteExportCsv()
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -290,7 +364,8 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
return;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("Index,NodeType,Name,XM,YM,ZT,ZD,TiltD,Dist,Voltage_kV,Current_uA,Power_W,RayOn,DetectorConnected,FrameRate,Resolution,ImageFile,MarkerType,MarkerX,MarkerY,DialogTitle,DialogMessage,DelayMs,Pipeline");
|
||||
// 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;
|
||||
|
||||
@@ -298,15 +373,24 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
{
|
||||
var row = node switch
|
||||
{
|
||||
ReferencePointNode rp => $"{rp.Index},{rp.NodeType},{Esc(rp.Name)},{rp.XM.ToString(inv)},{rp.YM.ToString(inv)},{rp.ZT.ToString(inv)},{rp.ZD.ToString(inv)},{rp.TiltD.ToString(inv)},{rp.Dist.ToString(inv)},{rp.Voltage.ToString(inv)},{rp.Current.ToString(inv)},,{rp.IsRayOn},,,,,,,,,,",
|
||||
SaveNodeWithImageNode sni => $"{sni.Index},{sni.NodeType},{Esc(sni.Name)},{sni.MotionState.XM.ToString(inv)},{sni.MotionState.YM.ToString(inv)},{sni.MotionState.ZT.ToString(inv)},{sni.MotionState.ZD.ToString(inv)},{sni.MotionState.TiltD.ToString(inv)},{sni.MotionState.Dist.ToString(inv)},{sni.RaySourceState.Voltage.ToString(inv)},,{sni.RaySourceState.Power.ToString(inv)},{sni.RaySourceState.IsOn},{sni.DetectorState.IsConnected},{sni.DetectorState.FrameRate.ToString(inv)},{Esc(sni.DetectorState.Resolution)},{Esc(sni.ImageFileName)},,,,,,",
|
||||
SaveNodeNode sn => $"{sn.Index},{sn.NodeType},{Esc(sn.Name)},{sn.MotionState.XM.ToString(inv)},{sn.MotionState.YM.ToString(inv)},{sn.MotionState.ZT.ToString(inv)},{sn.MotionState.ZD.ToString(inv)},{sn.MotionState.TiltD.ToString(inv)},{sn.MotionState.Dist.ToString(inv)},{sn.RaySourceState.Voltage.ToString(inv)},,{sn.RaySourceState.Power.ToString(inv)},{sn.RaySourceState.IsOn},{sn.DetectorState.IsConnected},{sn.DetectorState.FrameRate.ToString(inv)},{Esc(sn.DetectorState.Resolution)},,,,,,,",
|
||||
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)},,,,,,,",
|
||||
|
||||
SavePositionNode sp => $"{sp.Index},{sp.NodeType},{Esc(sp.Name)},{sp.MotionState.XM.ToString(inv)},{sp.MotionState.YM.ToString(inv)},{sp.MotionState.ZT.ToString(inv)},{sp.MotionState.ZD.ToString(inv)},{sp.MotionState.TiltD.ToString(inv)},{sp.MotionState.Dist.ToString(inv)},,,,,,,,,,,,,,",
|
||||
|
||||
InspectionModuleNode im => $"{im.Index},{im.NodeType},{Esc(im.Name)},,,,,,,,,,,,,,,,,,,{Esc(im.Pipeline?.Name ?? string.Empty)}",
|
||||
|
||||
InspectionMarkerNode mk => $"{mk.Index},{mk.NodeType},{Esc(mk.Name)},,,,,,,,,,,,,,{Esc(mk.MarkerType)},{mk.MarkerX.ToString(inv)},{mk.MarkerY.ToString(inv)},,,",
|
||||
|
||||
PauseDialogNode pd => $"{pd.Index},{pd.NodeType},{Esc(pd.Name)},,,,,,,,,,,,,,,,,{Esc(pd.DialogTitle)},{Esc(pd.DialogMessage)},,",
|
||||
|
||||
WaitDelayNode wd => $"{wd.Index},{wd.NodeType},{Esc(wd.Name)},,,,,,,,,,,,,,,,,,,{wd.DelayMilliseconds},",
|
||||
|
||||
CompleteProgramNode cp => $"{cp.Index},{cp.NodeType},{Esc(cp.Name)},,,,,,,,,,,,,,,,,,,,",
|
||||
|
||||
_ => $"{node.Index},{node.NodeType},{Esc(node.Name)},,,,,,,,,,,,,,,,,,,,"
|
||||
};
|
||||
|
||||
@@ -314,13 +398,18 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
}
|
||||
|
||||
File.WriteAllText(dlg.FileName, sb.ToString(), Encoding.UTF8);
|
||||
_logger.Info("CSV 已导出 | CSV exported: {FilePath}, 节点数={Count}", dlg.FileName, _currentProgram.Nodes.Count);
|
||||
}
|
||||
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)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return string.Empty;
|
||||
@@ -329,6 +418,12 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
return value;
|
||||
}
|
||||
|
||||
// ── 辅助方法 | Helper methods ───────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 编辑操作后的统一处理:刷新节点、标记已修改、发布变更事件
|
||||
/// Unified post-edit handling: refresh nodes, mark modified, publish change event
|
||||
/// </summary>
|
||||
private void OnProgramEdited()
|
||||
{
|
||||
IsModified = true;
|
||||
@@ -336,70 +431,34 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
PublishProgramChanged();
|
||||
}
|
||||
|
||||
private void HandleNodeModelChanged(CncNodeViewModel nodeVm, CncNode updatedNode)
|
||||
{
|
||||
if (_currentProgram == null)
|
||||
return;
|
||||
|
||||
_currentProgram = _cncProgramService.UpdateNode(_currentProgram, nodeVm.Index, updatedNode);
|
||||
IsModified = true;
|
||||
ProgramName = _currentProgram.Name;
|
||||
PublishProgramChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 _currentProgram.Nodes 重建 Nodes 集合
|
||||
/// Rebuild the Nodes collection from _currentProgram.Nodes
|
||||
/// </summary>
|
||||
private void RefreshNodes()
|
||||
{
|
||||
var selectedId = SelectedNode?.Id;
|
||||
var expansionState = Nodes.ToDictionary(node => node.Id, node => node.IsExpanded);
|
||||
Nodes.Clear();
|
||||
|
||||
var flatNodes = new List<CncNodeViewModel>();
|
||||
var rootNodes = new List<CncNodeViewModel>();
|
||||
CncNodeViewModel currentModule = null;
|
||||
if (_currentProgram?.Nodes == null)
|
||||
return;
|
||||
|
||||
if (_currentProgram?.Nodes != null)
|
||||
foreach (var node in _currentProgram.Nodes)
|
||||
{
|
||||
foreach (var node in _currentProgram.Nodes)
|
||||
{
|
||||
var vm = new CncNodeViewModel(node, HandleNodeModelChanged)
|
||||
{
|
||||
IsExpanded = expansionState.TryGetValue(node.Id, out var isExpanded) ? isExpanded : true
|
||||
};
|
||||
|
||||
flatNodes.Add(vm);
|
||||
|
||||
if (vm.IsInspectionModule)
|
||||
{
|
||||
rootNodes.Add(vm);
|
||||
currentModule = vm;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentModule != null && IsModuleChild(vm.NodeType))
|
||||
{
|
||||
currentModule.Children.Add(vm);
|
||||
continue;
|
||||
}
|
||||
|
||||
rootNodes.Add(vm);
|
||||
currentModule = null;
|
||||
}
|
||||
Nodes.Add(new CncNodeViewModel(node));
|
||||
}
|
||||
|
||||
Nodes = new ObservableCollection<CncNodeViewModel>(flatNodes);
|
||||
TreeNodes = new ObservableCollection<CncNodeViewModel>(rootNodes);
|
||||
|
||||
SelectedNode = selectedId.HasValue
|
||||
? Nodes.FirstOrDefault(node => node.Id == selectedId.Value) ?? Nodes.LastOrDefault()
|
||||
: Nodes.LastOrDefault();
|
||||
}
|
||||
|
||||
private static bool IsModuleChild(CncNodeType type)
|
||||
{
|
||||
return type is CncNodeType.InspectionMarker
|
||||
or CncNodeType.PauseDialog
|
||||
or CncNodeType.WaitDelay;
|
||||
// 尝试保持选中状态 | Try to preserve selection
|
||||
if (SelectedNode != null)
|
||||
{
|
||||
var match = Nodes.FirstOrDefault(n => n.Index == SelectedNode.Index);
|
||||
SelectedNode = match ?? Nodes.LastOrDefault();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 IEventAggregator 发布 CNC 程序变更事件
|
||||
/// Publish CNC program changed event via IEventAggregator
|
||||
/// </summary>
|
||||
private void PublishProgramChanged()
|
||||
{
|
||||
_eventAggregator
|
||||
@@ -407,4 +466,4 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
.Publish(new CncProgramChangedPayload(ProgramName ?? string.Empty, IsModified));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,543 +1,89 @@
|
||||
using Prism.Mvvm;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using XplorePlane.Models;
|
||||
|
||||
namespace XplorePlane.ViewModels.Cnc
|
||||
{
|
||||
/// <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>
|
||||
public class CncNodeViewModel : BindableBase
|
||||
{
|
||||
private readonly Action<CncNodeViewModel, CncNode> _modelChangedCallback;
|
||||
private CncNode _model;
|
||||
private int _index;
|
||||
private string _name;
|
||||
private CncNodeType _nodeType;
|
||||
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));
|
||||
_modelChangedCallback = modelChangedCallback ?? throw new ArgumentNullException(nameof(modelChangedCallback));
|
||||
Model = model;
|
||||
_index = model.Index;
|
||||
_name = model.Name;
|
||||
_nodeType = 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;
|
||||
|
||||
public Guid Id => _model.Id;
|
||||
|
||||
public int Index => _model.Index;
|
||||
/// <summary>节点在程序中的索引 | Node index in the program</summary>
|
||||
public int Index
|
||||
{
|
||||
get => _index;
|
||||
set => SetProperty(ref _index, value);
|
||||
}
|
||||
|
||||
/// <summary>节点显示名称 | Node display name</summary>
|
||||
public string Name
|
||||
{
|
||||
get => _model.Name;
|
||||
set => UpdateModel(_model with { Name = value ?? string.Empty });
|
||||
get => _name;
|
||||
set => SetProperty(ref _name, value);
|
||||
}
|
||||
|
||||
public CncNodeType NodeType => _model.NodeType;
|
||||
|
||||
public string NodeTypeDisplay => NodeType.ToString();
|
||||
/// <summary>节点类型 | Node type</summary>
|
||||
public CncNodeType NodeType
|
||||
{
|
||||
get => _nodeType;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _nodeType, value))
|
||||
{
|
||||
// 类型变更时自动更新图标 | Auto-update icon when type changes
|
||||
Icon = GetIconForNodeType(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>节点图标路径 | Node icon path</summary>
|
||||
public string Icon
|
||||
{
|
||||
get => _icon;
|
||||
private 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();
|
||||
set => SetProperty(ref _icon, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据节点类型返回对应的图标路径
|
||||
/// Returns the icon path for the given node type
|
||||
/// </summary>
|
||||
public static string GetIconForNodeType(CncNodeType nodeType)
|
||||
{
|
||||
return nodeType switch
|
||||
{
|
||||
CncNodeType.ReferencePoint => "/Assets/Icons/reference.png",
|
||||
CncNodeType.SaveNodeWithImage => "/Assets/Icons/saveall.png",
|
||||
CncNodeType.SaveNode => "/Assets/Icons/save.png",
|
||||
CncNodeType.SavePosition => "/Assets/Icons/add-pos.png",
|
||||
CncNodeType.InspectionModule => "/Assets/Icons/Module.png",
|
||||
CncNodeType.InspectionMarker => "/Assets/Icons/mark.png",
|
||||
CncNodeType.PauseDialog => "/Assets/Icons/message.png",
|
||||
CncNodeType.WaitDelay => "/Assets/Icons/wait.png",
|
||||
CncNodeType.CompleteProgram => "/Assets/Icons/finish.png",
|
||||
_ => "/Assets/Icons/cnc.png",
|
||||
CncNodeType.ReferencePoint => "/Resources/Icons/cnc_reference_point.png",
|
||||
CncNodeType.SaveNodeWithImage => "/Resources/Icons/cnc_save_with_image.png",
|
||||
CncNodeType.SaveNode => "/Resources/Icons/cnc_save_node.png",
|
||||
CncNodeType.SavePosition => "/Resources/Icons/cnc_save_position.png",
|
||||
CncNodeType.InspectionModule => "/Resources/Icons/cnc_inspection_module.png",
|
||||
CncNodeType.InspectionMarker => "/Resources/Icons/cnc_inspection_marker.png",
|
||||
CncNodeType.PauseDialog => "/Resources/Icons/cnc_pause_dialog.png",
|
||||
CncNodeType.WaitDelay => "/Resources/Icons/cnc_wait_delay.png",
|
||||
CncNodeType.CompleteProgram => "/Resources/Icons/cnc_complete_program.png",
|
||||
_ => "/Resources/Icons/cnc_default.png",
|
||||
};
|
||||
}
|
||||
|
||||
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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media.Imaging;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XplorePlane.Events;
|
||||
@@ -17,7 +16,7 @@ using XplorePlane.Services;
|
||||
|
||||
namespace XplorePlane.ViewModels
|
||||
{
|
||||
public class PipelineEditorViewModel : BindableBase, IPipelineEditorHostViewModel
|
||||
public class PipelineEditorViewModel : BindableBase
|
||||
{
|
||||
private const int MaxPipelineLength = 20;
|
||||
private const int DebounceDelayMs = 300;
|
||||
@@ -166,15 +165,6 @@ namespace XplorePlane.ViewModels
|
||||
public DelegateCommand<PipelineNodeViewModel> MoveNodeUpCommand { 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 ───────────────────────────────────
|
||||
|
||||
private bool CanAddOperator(string operatorKey) =>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Prism.Commands;
|
||||
using Prism.Commands;
|
||||
using Prism.Events;
|
||||
using Prism.Ioc;
|
||||
using Prism.Mvvm;
|
||||
@@ -10,9 +10,6 @@ using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Media.Imaging;
|
||||
using XplorePlane.Events;
|
||||
using XplorePlane.ViewModels.Cnc;
|
||||
using XplorePlane.Views;
|
||||
using XplorePlane.Views.Cnc;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XP.Common.PdfViewer.Interfaces;
|
||||
using XP.Hardware.MotionControl.Abstractions;
|
||||
@@ -21,12 +18,9 @@ namespace XplorePlane.ViewModels
|
||||
{
|
||||
public class MainViewModel : BindableBase
|
||||
{
|
||||
private const double CncEditorHostWidth = 710d;
|
||||
private readonly ILoggerService _logger;
|
||||
private readonly IContainerProvider _containerProvider;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly CncEditorViewModel _cncEditorViewModel;
|
||||
private readonly CncPageView _cncPageView;
|
||||
private string _licenseInfo = "当前时间";
|
||||
|
||||
public string LicenseInfo
|
||||
@@ -55,17 +49,6 @@ namespace XplorePlane.ViewModels
|
||||
public DelegateCommand OpenLibraryVersionsCommand { get; }
|
||||
public DelegateCommand OpenUserManualCommand { 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; }
|
||||
@@ -79,27 +62,6 @@ namespace XplorePlane.ViewModels
|
||||
public DelegateCommand OpenLanguageSwitcherCommand { 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 _detectorConfigWindow;
|
||||
@@ -107,18 +69,12 @@ namespace XplorePlane.ViewModels
|
||||
private Window _realTimeLogViewerWindow;
|
||||
private Window _toolboxWindow;
|
||||
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)
|
||||
{
|
||||
_logger = logger?.ForModule<MainViewModel>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
_containerProvider = containerProvider ?? throw new ArgumentNullException(nameof(containerProvider));
|
||||
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
|
||||
_cncEditorViewModel = _containerProvider.Resolve<CncEditorViewModel>();
|
||||
_cncPageView = new CncPageView { DataContext = _cncEditorViewModel };
|
||||
|
||||
NavigationTree = new ObservableCollection<object>();
|
||||
|
||||
@@ -134,23 +90,12 @@ namespace XplorePlane.ViewModels
|
||||
OpenImageProcessingCommand = new DelegateCommand(() => ShowWindow(new Views.ImageProcessingWindow(), "图像处理"));
|
||||
LoadImageCommand = new DelegateCommand(ExecuteLoadImage);
|
||||
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(), "矩阵编排"));
|
||||
OpenToolboxCommand = new DelegateCommand(ExecuteOpenToolbox);
|
||||
OpenLibraryVersionsCommand = new DelegateCommand(() => ShowWindow(new Views.LibraryVersionsWindow(), "关于"));
|
||||
OpenUserManualCommand = new DelegateCommand(ExecuteOpenUserManual);
|
||||
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);
|
||||
@@ -164,10 +109,6 @@ namespace XplorePlane.ViewModels
|
||||
OpenLanguageSwitcherCommand = new DelegateCommand(ExecuteOpenLanguageSwitcher);
|
||||
OpenRealTimeLogViewerCommand = new DelegateCommand(ExecuteOpenRealTimeLogViewer);
|
||||
|
||||
ImagePanelContent = new PipelineEditorView();
|
||||
ViewportPanelWidth = new GridLength(1, GridUnitType.Star);
|
||||
ImagePanelWidth = new GridLength(320);
|
||||
|
||||
_logger.Info("MainViewModel 已初始化");
|
||||
}
|
||||
|
||||
@@ -212,38 +153,6 @@ namespace XplorePlane.ViewModels
|
||||
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()
|
||||
{
|
||||
try
|
||||
|
||||
@@ -176,6 +176,30 @@ namespace XplorePlane.ViewModels
|
||||
ApplyPixelFormatCommand = new DelegateCommand(() => ApplyCameraParam(() => _camera.SetPixelFormat(SelectedPixelFormat)), () => IsCameraConnected);
|
||||
RefreshCameraParamsCommand = new DelegateCommand(RefreshCameraParams, () => 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
|
||||
@@ -326,15 +350,18 @@ namespace XplorePlane.ViewModels
|
||||
|
||||
private void OnCameraImageGrabbed(object? sender, ImageGrabbedEventArgs e)
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
try
|
||||
{
|
||||
var bitmap = PixelConverter.ToBitmapSource(e.PixelData, e.Width, e.Height, e.PixelFormat);
|
||||
var app = Application.Current;
|
||||
if (app == null) return;
|
||||
|
||||
app.Dispatcher.Invoke(() =>
|
||||
app.Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
CameraImageSource = bitmap;
|
||||
if (!_disposed)
|
||||
CameraImageSource = bitmap;
|
||||
});
|
||||
|
||||
if (_liveViewRunning)
|
||||
@@ -344,7 +371,8 @@ namespace XplorePlane.ViewModels
|
||||
}
|
||||
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;
|
||||
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;
|
||||
if (app == null) return;
|
||||
|
||||
app.Dispatcher.Invoke(() =>
|
||||
app.Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
if (_disposed) return;
|
||||
IsCameraConnected = false;
|
||||
IsCameraGrabbing = false;
|
||||
CameraStatusText = "连接已断开";
|
||||
@@ -382,12 +412,19 @@ namespace XplorePlane.ViewModels
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
_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
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
<Window
|
||||
<Window
|
||||
x:Class="XplorePlane.Views.Cnc.CncEditorWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:cnc="clr-namespace:XplorePlane.Views.Cnc"
|
||||
Title="CNC 编辑器"
|
||||
Width="1040"
|
||||
Height="780"
|
||||
MinWidth="960"
|
||||
MinHeight="720"
|
||||
ResizeMode="CanResize"
|
||||
Width="350"
|
||||
Height="750"
|
||||
ShowInTaskbar="False"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
<cnc:CncPageView />
|
||||
</Window>
|
||||
</Window>
|
||||
@@ -1,457 +1,297 @@
|
||||
<!-- CNC 编辑器主页面视图 | CNC editor main page view -->
|
||||
<UserControl
|
||||
x:Class="XplorePlane.Views.Cnc.CncPageView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:XplorePlane.Views.Cnc"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
xmlns:views="clr-namespace:XplorePlane.Views"
|
||||
xmlns:vm="clr-namespace:XplorePlane.ViewModels.Cnc"
|
||||
d:DesignHeight="760"
|
||||
d:DesignWidth="702"
|
||||
d:DesignHeight="700"
|
||||
d:DesignWidth="350"
|
||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<UserControl.Resources>
|
||||
<BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||
<local:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
|
||||
|
||||
<!-- 面板背景和边框颜色 | Panel background and border colors -->
|
||||
<SolidColorBrush x:Key="PanelBg" Color="White" />
|
||||
<SolidColorBrush x:Key="PanelBorder" Color="#CDCBCB" />
|
||||
<SolidColorBrush x:Key="SeparatorBrush" Color="#E5E5E5" />
|
||||
<SolidColorBrush x:Key="HeaderBg" Color="#F7F7F7" />
|
||||
<FontFamily x:Key="UiFont">Microsoft YaHei UI</FontFamily>
|
||||
<SolidColorBrush x:Key="PanelBorder" Color="#cdcbcb" />
|
||||
<SolidColorBrush x:Key="SeparatorBrush" Color="#E0E0E0" />
|
||||
<SolidColorBrush x:Key="AccentBlue" Color="#E3F0FF" />
|
||||
<FontFamily x:Key="CsdFont">Microsoft YaHei UI</FontFamily>
|
||||
|
||||
<Style x:Key="TreeItemStyle" TargetType="TreeViewItem">
|
||||
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
|
||||
<!-- 节点列表项样式 | Node list item style -->
|
||||
<Style x:Key="CncNodeItemStyle" TargetType="ListBoxItem">
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="EditorTitle" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="{StaticResource UiFont}" />
|
||||
<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">
|
||||
<!-- 工具栏按钮样式 | Toolbar button style -->
|
||||
<Style x:Key="ToolbarBtn" TargetType="Button">
|
||||
<Setter Property="Height" Value="28" />
|
||||
<Setter Property="Padding" Value="8,3" />
|
||||
<Setter Property="Margin" Value="0,0,0,8" />
|
||||
<Setter Property="BorderBrush" Value="#CFCFCF" />
|
||||
<Setter Property="Margin" Value="2,0" />
|
||||
<Setter Property="Padding" Value="6,0" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderBrush" Value="#cdcbcb" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="FontFamily" Value="{StaticResource UiFont}" />
|
||||
<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="FontFamily" Value="Microsoft YaHei UI" />
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Border
|
||||
Width="702"
|
||||
MinWidth="702"
|
||||
HorizontalAlignment="Left"
|
||||
Background="{StaticResource PanelBg}"
|
||||
BorderBrush="{StaticResource PanelBorder}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="4">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="200" />
|
||||
<ColumnDefinition Width="1" />
|
||||
<ColumnDefinition Width="250" />
|
||||
<ColumnDefinition Width="1" />
|
||||
<ColumnDefinition Width="250" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<!-- Row 0: 工具栏 | Toolbar -->
|
||||
<RowDefinition Height="Auto" />
|
||||
<!-- Row 1: 主内容区(左侧节点列表 + 右侧参数面板)| Main content (left: node list, right: parameter panel) -->
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid Grid.Column="0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<!-- ═══ 工具栏:节点插入命令 + 文件操作命令 | Toolbar: node insert commands + file operation commands ═══ -->
|
||||
<Border
|
||||
Grid.Row="0"
|
||||
Padding="6,4"
|
||||
Background="#F5F5F5"
|
||||
BorderBrush="{StaticResource PanelBorder}"
|
||||
BorderThickness="0,0,0,1">
|
||||
<WrapPanel Orientation="Horizontal">
|
||||
<!-- 文件操作按钮 | File operation buttons -->
|
||||
<Button
|
||||
Command="{Binding NewProgramCommand}"
|
||||
Content="新建"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="新建程序 | New Program" />
|
||||
<Button
|
||||
Command="{Binding SaveProgramCommand}"
|
||||
Content="保存"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="保存程序 | Save Program" />
|
||||
<Button
|
||||
Command="{Binding LoadProgramCommand}"
|
||||
Content="加载"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="加载程序 | Load Program" />
|
||||
<Button
|
||||
Command="{Binding ExportCsvCommand}"
|
||||
Content="导出CSV"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="导出 CSV | Export CSV" />
|
||||
|
||||
<Border
|
||||
Grid.Row="0"
|
||||
Padding="10,8"
|
||||
Background="{StaticResource HeaderBg}"
|
||||
BorderBrush="{StaticResource SeparatorBrush}"
|
||||
BorderThickness="0,0,0,1">
|
||||
<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>
|
||||
<!-- 分隔线 | Separator -->
|
||||
<Rectangle
|
||||
Width="1"
|
||||
Height="20"
|
||||
Margin="4,0"
|
||||
Fill="{StaticResource SeparatorBrush}" />
|
||||
|
||||
<TreeView
|
||||
x:Name="CncTreeView"
|
||||
Grid.Row="1"
|
||||
Padding="4,6"
|
||||
<!-- 节点插入按钮(9 种节点类型)| Node insert buttons (9 node types) -->
|
||||
<Button
|
||||
Command="{Binding InsertReferencePointCommand}"
|
||||
Content="参考点"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="插入参考点 | Insert Reference Point" />
|
||||
<Button
|
||||
Command="{Binding InsertSaveNodeWithImageCommand}"
|
||||
Content="保存+图"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="保存节点并保存图片 | Save Node With Image" />
|
||||
<Button
|
||||
Command="{Binding InsertSaveNodeCommand}"
|
||||
Content="保存节点"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="仅保存节点 | Save Node" />
|
||||
<Button
|
||||
Command="{Binding InsertSavePositionCommand}"
|
||||
Content="保存位置"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="保存位置 | Save Position" />
|
||||
<Button
|
||||
Command="{Binding InsertInspectionModuleCommand}"
|
||||
Content="检测模块"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="插入检测模块 | Insert Inspection Module" />
|
||||
<Button
|
||||
Command="{Binding InsertInspectionMarkerCommand}"
|
||||
Content="检测标记"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="插入检测标记 | Insert Inspection Marker" />
|
||||
<Button
|
||||
Command="{Binding InsertPauseDialogCommand}"
|
||||
Content="停顿"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="插入停顿对话框 | Insert Pause Dialog" />
|
||||
<Button
|
||||
Command="{Binding InsertWaitDelayCommand}"
|
||||
Content="延时"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="设置等待延时 | Insert Wait Delay" />
|
||||
<Button
|
||||
Command="{Binding InsertCompleteProgramCommand}"
|
||||
Content="完成"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="完成程序 | Complete Program" />
|
||||
</WrapPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ═══ 主内容区:左侧节点列表 + 右侧参数面板 | Main content: left node list + right parameter panel ═══ -->
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<!-- 左侧:节点列表 | Left: node list -->
|
||||
<ColumnDefinition Width="3*" MinWidth="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"
|
||||
BorderThickness="0"
|
||||
ItemsSource="{Binding TreeNodes}"
|
||||
SelectedItemChanged="CncTreeView_SelectedItemChanged">
|
||||
<TreeView.Resources>
|
||||
<HierarchicalDataTemplate DataType="{x:Type vm:CncNodeViewModel}" ItemsSource="{Binding Children}">
|
||||
<Grid x:Name="NodeRoot" MinHeight="34">
|
||||
ItemContainerStyle="{StaticResource CncNodeItemStyle}"
|
||||
ItemsSource="{Binding Nodes}"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
SelectedItem="{Binding SelectedNode, Mode=TwoWay}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid x:Name="NodeRoot" MinHeight="40">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="30" />
|
||||
<!-- 图标列 | Icon column -->
|
||||
<ColumnDefinition Width="40" />
|
||||
<!-- 名称列 | Name column -->
|
||||
<ColumnDefinition Width="*" />
|
||||
<!-- 操作按钮列 | Action buttons column -->
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 节点图标 | Node icon -->
|
||||
<Border
|
||||
Grid.Column="0"
|
||||
Width="22"
|
||||
Height="22"
|
||||
Width="28"
|
||||
Height="28"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Background="Transparent"
|
||||
Background="#E8F0FE"
|
||||
BorderBrush="#5B9BD5"
|
||||
BorderThickness="1.5"
|
||||
CornerRadius="4">
|
||||
<Image
|
||||
Width="15"
|
||||
Height="15"
|
||||
Width="20"
|
||||
Height="20"
|
||||
Source="{Binding Icon}"
|
||||
Stretch="Uniform" />
|
||||
</Border>
|
||||
|
||||
<!-- 节点序号和名称 | Node index and name -->
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
Margin="4,0,0,0"
|
||||
Margin="6,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="{StaticResource UiFont}"
|
||||
FontSize="10.5"
|
||||
Foreground="#888888"
|
||||
FontFamily="Microsoft YaHei UI"
|
||||
FontSize="11"
|
||||
Foreground="#888"
|
||||
Text="{Binding Index, StringFormat='[{0}] '}" />
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="{StaticResource UiFont}"
|
||||
FontSize="11.5"
|
||||
FontFamily="Microsoft YaHei UI"
|
||||
FontSize="12"
|
||||
Text="{Binding Name}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- 悬停操作按钮:上移 / 下移 / 删除 | Hover actions: MoveUp / MoveDown / Delete -->
|
||||
<StackPanel
|
||||
x:Name="NodeActions"
|
||||
Grid.Column="2"
|
||||
Margin="0,0,2,0"
|
||||
Margin="0,0,4,0"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal"
|
||||
Visibility="Collapsed">
|
||||
<Button
|
||||
Width="20"
|
||||
Height="20"
|
||||
Width="22"
|
||||
Height="22"
|
||||
Margin="1,0"
|
||||
Background="White"
|
||||
BorderBrush="#CDCBCB"
|
||||
Background="Transparent"
|
||||
BorderBrush="#cdcbcb"
|
||||
BorderThickness="1"
|
||||
Command="{Binding DataContext.MoveNodeUpCommand, RelativeSource={RelativeSource AncestorType=TreeView}}"
|
||||
Command="{Binding DataContext.MoveNodeUpCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content="上"
|
||||
Content="▲"
|
||||
Cursor="Hand"
|
||||
FontSize="10"
|
||||
ToolTip="上移" />
|
||||
ToolTip="上移 | Move Up" />
|
||||
<Button
|
||||
Width="20"
|
||||
Height="20"
|
||||
Width="22"
|
||||
Height="22"
|
||||
Margin="1,0"
|
||||
Background="White"
|
||||
BorderBrush="#CDCBCB"
|
||||
Background="Transparent"
|
||||
BorderBrush="#cdcbcb"
|
||||
BorderThickness="1"
|
||||
Command="{Binding DataContext.MoveNodeDownCommand, RelativeSource={RelativeSource AncestorType=TreeView}}"
|
||||
Command="{Binding DataContext.MoveNodeDownCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content="下"
|
||||
Content="▼"
|
||||
Cursor="Hand"
|
||||
FontSize="10"
|
||||
ToolTip="下移" />
|
||||
ToolTip="下移 | Move Down" />
|
||||
<Button
|
||||
Width="20"
|
||||
Height="20"
|
||||
Width="22"
|
||||
Height="22"
|
||||
Margin="1,0"
|
||||
Background="White"
|
||||
Background="Transparent"
|
||||
BorderBrush="#E05050"
|
||||
BorderThickness="1"
|
||||
Command="{Binding DataContext.DeleteNodeCommand, RelativeSource={RelativeSource AncestorType=TreeView}}"
|
||||
Content="删"
|
||||
Command="{Binding DataContext.DeleteNodeCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
|
||||
Content="✕"
|
||||
Cursor="Hand"
|
||||
FontSize="10"
|
||||
ToolTip="删除" />
|
||||
ToolTip="删除 | Delete" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<DataTemplate.Triggers>
|
||||
<!-- 鼠标悬停时显示操作按钮 | Show action buttons on mouse hover -->
|
||||
<Trigger SourceName="NodeRoot" Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="NodeActions" Property="Visibility" Value="Visible" />
|
||||
</Trigger>
|
||||
</DataTemplate.Triggers>
|
||||
</HierarchicalDataTemplate>
|
||||
</TreeView.Resources>
|
||||
<TreeView.ItemContainerStyle>
|
||||
<Style BasedOn="{StaticResource TreeItemStyle}" TargetType="TreeViewItem" />
|
||||
</TreeView.ItemContainerStyle>
|
||||
</TreeView>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<Rectangle
|
||||
Grid.Column="1"
|
||||
Width="1"
|
||||
Fill="{StaticResource SeparatorBrush}" />
|
||||
<!-- 垂直分隔线 | Vertical separator -->
|
||||
<Rectangle
|
||||
Grid.Column="1"
|
||||
Width="1"
|
||||
Fill="{StaticResource SeparatorBrush}" />
|
||||
|
||||
<ScrollViewer Grid.Column="2" VerticalScrollBarVisibility="Auto">
|
||||
<Grid Margin="10">
|
||||
<StackPanel Visibility="{Binding SelectedNode, Converter={StaticResource NullToVisibilityConverter}}">
|
||||
<TextBlock Style="{StaticResource EditorTitle}" Text="节点属性" />
|
||||
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="名称" />
|
||||
<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>
|
||||
<!-- ── 右侧:参数面板(根据节点类型动态渲染)| Right: parameter panel (dynamic rendering by node type) ── -->
|
||||
<ScrollViewer
|
||||
Grid.Column="2"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Margin="8,6">
|
||||
<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" />
|
||||
Margin="0,0,0,4"
|
||||
FontFamily="{StaticResource CsdFont}"
|
||||
FontSize="11"
|
||||
FontWeight="Bold"
|
||||
Foreground="#555"
|
||||
Text="参数配置" />
|
||||
|
||||
<!-- 动态参数内容区域(占位:根据 SelectedNode 类型渲染)| Dynamic parameter content area (placeholder for node-type-based rendering) -->
|
||||
<ContentControl Content="{Binding SelectedNode}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</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.Data;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XplorePlane.Services;
|
||||
using XplorePlane.ViewModels.Cnc;
|
||||
|
||||
namespace XplorePlane.Views.Cnc
|
||||
{
|
||||
/// <summary>
|
||||
/// CNC editor main page view.
|
||||
/// CNC 编辑器主页面视图(MVVM 模式,逻辑在 ViewModel 中)
|
||||
/// CNC editor main page view (MVVM pattern, logic in ViewModel)
|
||||
/// </summary>
|
||||
public partial class CncPageView : UserControl
|
||||
{
|
||||
private CncInspectionModulePipelineViewModel _inspectionModulePipelineViewModel;
|
||||
|
||||
public CncPageView()
|
||||
{
|
||||
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>
|
||||
<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="AccentBlue" Color="#E3F0FF" />
|
||||
<FontFamily x:Key="CsdFont">Microsoft YaHei UI</FontFamily>
|
||||
|
||||
<!-- 节点项样式 -->
|
||||
<Style x:Key="PipelineNodeItemStyle" TargetType="ListBoxItem">
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
</Style>
|
||||
|
||||
<!-- 工具栏按钮样式 -->
|
||||
<Style x:Key="ToolbarBtn" TargetType="Button">
|
||||
<Setter Property="Width" Value="52" />
|
||||
<Setter Property="Width" Value="28" />
|
||||
<Setter Property="Height" Value="28" />
|
||||
<Setter Property="Margin" Value="2,0" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderBrush" Value="#CDCBCB" />
|
||||
<Setter Property="BorderBrush" Value="#cdcbcb" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="FontFamily" Value="Microsoft YaHei UI" />
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
@@ -42,13 +44,21 @@
|
||||
CornerRadius="4">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<!-- Row 0: 工具栏 -->
|
||||
<RowDefinition Height="Auto" />
|
||||
|
||||
<!-- Row 2: 流水线节点列表 -->
|
||||
<RowDefinition Height="2*" MinHeight="180" />
|
||||
<!-- Row 3: 分隔线 -->
|
||||
<RowDefinition Height="Auto" />
|
||||
<!-- Row 4: 参数面板 -->
|
||||
<RowDefinition Height="2*" MinHeight="80" />
|
||||
<!-- Row 5: 状态栏 -->
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 标题栏:流水线名称 + 设备选择 -->
|
||||
<!-- 工具栏 -->
|
||||
<Border
|
||||
Grid.Row="0"
|
||||
Padding="6,4"
|
||||
@@ -66,23 +76,50 @@
|
||||
Command="{Binding SavePipelineCommand}"
|
||||
Content="保存"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="保存当前检测模块流水线" />
|
||||
ToolTip="保存流水线" />
|
||||
<Button
|
||||
Width="60"
|
||||
Width="43"
|
||||
Command="{Binding SaveAsPipelineCommand}"
|
||||
Content="另存为"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="导出当前检测模块流水线" />
|
||||
ToolTip="另存为" />
|
||||
<Button
|
||||
Width="52"
|
||||
Width="43"
|
||||
Command="{Binding LoadPipelineCommand}"
|
||||
Content="加载"
|
||||
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>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- 流水线节点列表(拖拽目标) -->
|
||||
<ListBox
|
||||
x:Name="PipelineListBox"
|
||||
Grid.Row="1"
|
||||
@@ -101,6 +138,7 @@
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 连接线列:上半段 + 下半段 -->
|
||||
<Line
|
||||
x:Name="TopLine"
|
||||
HorizontalAlignment="Center"
|
||||
@@ -121,6 +159,7 @@
|
||||
Y1="0"
|
||||
Y2="14" />
|
||||
|
||||
<!-- 算子图标 -->
|
||||
<Border
|
||||
Grid.Column="0"
|
||||
Width="28"
|
||||
@@ -138,6 +177,7 @@
|
||||
Text="{Binding IconPath}" />
|
||||
</Border>
|
||||
|
||||
<!-- 算子名称 -->
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Margin="6,0,0,0"
|
||||
@@ -146,6 +186,7 @@
|
||||
FontSize="12"
|
||||
Text="{Binding DisplayName}" />
|
||||
|
||||
<!-- 操作按钮:上移 / 下移 / 删除(悬停显示) -->
|
||||
<StackPanel
|
||||
x:Name="NodeActions"
|
||||
Grid.Column="2"
|
||||
@@ -158,11 +199,11 @@
|
||||
Height="22"
|
||||
Margin="1,0"
|
||||
Background="Transparent"
|
||||
BorderBrush="#CDCBCB"
|
||||
BorderBrush="#cdcbcb"
|
||||
BorderThickness="1"
|
||||
Command="{Binding DataContext.MoveNodeUpCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content="上"
|
||||
Content="▲"
|
||||
Cursor="Hand"
|
||||
FontSize="10"
|
||||
ToolTip="上移" />
|
||||
@@ -171,11 +212,11 @@
|
||||
Height="22"
|
||||
Margin="1,0"
|
||||
Background="Transparent"
|
||||
BorderBrush="#CDCBCB"
|
||||
BorderBrush="#cdcbcb"
|
||||
BorderThickness="1"
|
||||
Command="{Binding DataContext.MoveNodeDownCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content="下"
|
||||
Content="▼"
|
||||
Cursor="Hand"
|
||||
FontSize="10"
|
||||
ToolTip="下移" />
|
||||
@@ -188,7 +229,7 @@
|
||||
BorderThickness="1"
|
||||
Command="{Binding DataContext.RemoveOperatorCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content="删"
|
||||
Content="✕"
|
||||
Cursor="Hand"
|
||||
FontSize="10"
|
||||
ToolTip="删除" />
|
||||
@@ -205,12 +246,13 @@
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<Rectangle
|
||||
Grid.Row="2"
|
||||
Height="1"
|
||||
Fill="{StaticResource SeparatorBrush}" />
|
||||
|
||||
<!-- 参数面板 -->
|
||||
<ScrollViewer
|
||||
Grid.Row="3"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
@@ -241,14 +283,14 @@
|
||||
<TextBox
|
||||
Grid.Column="1"
|
||||
Padding="4,2"
|
||||
BorderBrush="#CDCBCB"
|
||||
BorderBrush="#cdcbcb"
|
||||
BorderThickness="1"
|
||||
FontFamily="Microsoft YaHei UI"
|
||||
FontSize="11"
|
||||
Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}">
|
||||
<TextBox.Style>
|
||||
<Style TargetType="TextBox">
|
||||
<Setter Property="BorderBrush" Value="#CDCBCB" />
|
||||
<Setter Property="BorderBrush" Value="#cdcbcb" />
|
||||
<Setter Property="Background" Value="White" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsValueValid}" Value="False">
|
||||
@@ -266,6 +308,7 @@
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- 状态栏 -->
|
||||
<Border
|
||||
Grid.Row="4"
|
||||
Padding="6,4"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Prism.Ioc;
|
||||
using System;
|
||||
using Prism.Ioc;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
@@ -23,7 +23,7 @@ namespace XplorePlane.Views
|
||||
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is not IPipelineEditorHostViewModel)
|
||||
if (DataContext is not PipelineEditorViewModel)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -46,35 +46,36 @@ namespace XplorePlane.Views
|
||||
|
||||
private void OnDragOver(object sender, DragEventArgs e)
|
||||
{
|
||||
e.Effects = e.Data.GetDataPresent(OperatorToolboxView.DragFormat)
|
||||
? DragDropEffects.Copy
|
||||
: DragDropEffects.None;
|
||||
if (e.Data.GetDataPresent(OperatorToolboxView.DragFormat))
|
||||
e.Effects = DragDropEffects.Copy;
|
||||
else
|
||||
e.Effects = DragDropEffects.None;
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (!e.Data.GetDataPresent(OperatorToolboxView.DragFormat))
|
||||
{
|
||||
_logger?.Warn("Drop 事件触发,但数据中没有 {Format}", OperatorToolboxView.DragFormat);
|
||||
_logger?.Warn("Drop 事件触发但数据中没有 {Format}", OperatorToolboxView.DragFormat);
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
_logger?.Info("算子已放入流水线:{OperatorKey},当前节点数(执行前)={Count}",
|
||||
operatorKey, vm.PipelineNodes.Count);
|
||||
_logger?.Info("算子已放入流水线:{OperatorKey},VM HashCode={Hash},当前节点数(执行前)={Count}",
|
||||
operatorKey, vm.GetHashCode(), vm.PipelineNodes.Count);
|
||||
vm.AddOperatorCommand.Execute(operatorKey);
|
||||
_logger?.Info("AddOperator 执行后节点数={Count},PipelineListBox.Items.Count={ItemsCount}",
|
||||
vm.PipelineNodes.Count, PipelineListBox.Items.Count);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<UserControl
|
||||
<UserControl
|
||||
x:Class="XplorePlane.Views.ImagePanelView"
|
||||
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:views="clr-namespace:XplorePlane.Views"
|
||||
d:DesignHeight="400"
|
||||
d:DesignWidth="250"
|
||||
mc:Ignorable="d">
|
||||
@@ -13,9 +14,9 @@
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<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="图像" />
|
||||
</Border>
|
||||
<ContentControl Grid.Row="1" Content="{Binding ImagePanelContent}" />
|
||||
<views:PipelineEditorView Grid.Row="1" />
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
Height="1040"
|
||||
d:DesignWidth="1580"
|
||||
Background="#F5F5F5"
|
||||
Icon="pack://application:,,,/XplorePlane;component/XplorerPlane.ico"
|
||||
Icon="pack://application:,,,/XplorerPlane.ico"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
mc:Ignorable="d">
|
||||
<Window.Resources>
|
||||
@@ -74,7 +74,7 @@
|
||||
<StackPanel>
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Title="新建CNC"
|
||||
Command="{Binding NewCncProgramCommand}"
|
||||
Command="{Binding Path=SetStyle.Command}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/new-doc.png"
|
||||
Text="新建CNC" />
|
||||
@@ -82,12 +82,11 @@
|
||||
telerik:ScreenTip.Description="保存当前X射线实时图像"
|
||||
telerik:ScreenTip.Title="保存图像"
|
||||
Size="Medium"
|
||||
Command="{Binding SaveCncProgramCommand}"
|
||||
SmallImage="/Assets/Icons/save.png"
|
||||
Text="保存" />
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Title="另存为"
|
||||
Command="{Binding SaveCncProgramCommand}"
|
||||
Command="{Binding OpenFileCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/saveas.png"
|
||||
Text="另存为" />
|
||||
@@ -95,7 +94,7 @@
|
||||
<StackPanel>
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Title="加载CNC"
|
||||
Command="{Binding LoadCncProgramCommand}"
|
||||
Command="{Binding OpenFileCommand}"
|
||||
Size="Large"
|
||||
SmallImage="/Assets/Icons/open.png"
|
||||
Text="加载CNC" />
|
||||
@@ -285,28 +284,32 @@
|
||||
Command="{Binding OpenCncEditorCommand}"
|
||||
Size="Large"
|
||||
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 节点快捷工具 -->
|
||||
<StackPanel>
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Title="参考点"
|
||||
Size="Medium"
|
||||
Command="{Binding InsertReferencePointCommand}"
|
||||
SmallImage="/Assets/Icons/reference.png"
|
||||
Text="参考点" />
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Title="添加位置"
|
||||
Size="Medium"
|
||||
Command="{Binding InsertSavePositionCommand}"
|
||||
SmallImage="/Assets/Icons/add-pos.png"
|
||||
Text="添加位置" />
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Title="完成"
|
||||
Size="Medium"
|
||||
Command="{Binding InsertCompleteProgramCommand}"
|
||||
SmallImage="/Assets/Icons/finish.png"
|
||||
Text="完成" />
|
||||
</StackPanel>
|
||||
@@ -314,19 +317,16 @@
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Title="标记"
|
||||
Size="Medium"
|
||||
Command="{Binding InsertInspectionMarkerCommand}"
|
||||
SmallImage="/Assets/Icons/mark.png"
|
||||
Text="标记" />
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Title="模块"
|
||||
Size="Medium"
|
||||
Command="{Binding InsertInspectionModuleCommand}"
|
||||
SmallImage="/Assets/Icons/Module.png"
|
||||
Text="检测模块" />
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Title="全部保存"
|
||||
Size="Medium"
|
||||
Command="{Binding SaveCncProgramCommand}"
|
||||
SmallImage="/Assets/Icons/saveall.png"
|
||||
Text="全部保存" />
|
||||
</StackPanel>
|
||||
@@ -334,26 +334,14 @@
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Title="消息"
|
||||
Size="Medium"
|
||||
Command="{Binding InsertPauseDialogCommand}"
|
||||
SmallImage="/Assets/Icons/message.png"
|
||||
Text="消息弹窗" />
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Title="等待"
|
||||
Size="Medium"
|
||||
Command="{Binding InsertWaitDelayCommand}"
|
||||
SmallImage="/Assets/Icons/wait.png"
|
||||
Text="插入等待" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- 矩阵编排入口按钮 -->
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="打开矩阵编排窗口,配置多工件阵列检测方案"
|
||||
telerik:ScreenTip.Title="矩阵编排"
|
||||
Command="{Binding OpenMatrixEditorCommand}"
|
||||
Size="Large"
|
||||
SmallImage="/Assets/Icons/matrix.png"
|
||||
Text="矩阵编排" />
|
||||
|
||||
</telerik:RadRibbonGroup>
|
||||
|
||||
<telerik:RadRibbonGroup Header="高级模块" IsEnabled="{Binding Path=CellsGroup.IsEnabled}">
|
||||
@@ -400,7 +388,7 @@
|
||||
Size="Large"
|
||||
SmallImage="/Assets/Icons/spiral.png" />
|
||||
</telerik:RadRibbonGroup>
|
||||
<!--
|
||||
<!--
|
||||
<telerik:RadRibbonGroup Header="图像处理">
|
||||
<telerik:RadRibbonGroup.Variants>
|
||||
<telerik:GroupVariant Priority="0" Variant="Large" />
|
||||
@@ -470,9 +458,9 @@
|
||||
Margin="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition x:Name="NavColumn" Width="0" />
|
||||
<ColumnDefinition Width="{Binding ViewportPanelWidth}" />
|
||||
<ColumnDefinition Width="{Binding ImagePanelWidth}" />
|
||||
<ColumnDefinition Width="300" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="350" />
|
||||
<ColumnDefinition Width="350" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 左侧: 计划导航 (默认隐藏,点击CNC AccountingNumberFormatButton显示) -->
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 标题栏 -->
|
||||
@@ -56,50 +55,5 @@
|
||||
<Run Foreground="#0078D4" Text="{Binding CameraPixelCoord, Mode=OneWay}" />
|
||||
</TextBlock>
|
||||
</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>
|
||||
</UserControl>
|
||||
@@ -28,8 +28,16 @@ namespace XplorePlane.Views
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 双击相机图像时,计算并显示点击位置的像素坐标。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// TODO: 后续需要将点击的像素坐标通过 CalibrationProcessor 转换为世界坐标,
|
||||
/// 再传给运动机构执行定位。
|
||||
/// </remarks>
|
||||
private void ImgCamera_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.ClickCount != 2) return;
|
||||
if (_viewModel?.CameraImageSource == null) return;
|
||||
|
||||
var image = (Image)sender;
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
<!-- 标题栏 -->
|
||||
<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="实时图像" />
|
||||
</Border>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
<!-- 图像信息栏 -->
|
||||
<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}" />
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
@@ -146,9 +146,6 @@
|
||||
<Link>Libs\Hardware\zh-TW\%(Filename)%(Extension)</Link>
|
||||
</None>
|
||||
<Compile Remove="Views\Main\MainWindowB.xaml.cs" />
|
||||
<Content Include="XplorerPlane.ico">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
|
||||
<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