Merge branch 'turbo-002-cnc' of ssh://git.wio.me:2222/xhouse/XplorePlane into turbo-002-cnc

This commit is contained in:
zhengxuan.zhang
2026-05-06 20:43:46 +08:00
23 changed files with 1094 additions and 287 deletions
+22 -18
View File
@@ -20,21 +20,25 @@ namespace XplorePlane.Tests.Models
public void MotionState_Default_AllZeros() public void MotionState_Default_AllZeros()
{ {
var state = MotionState.Default; var state = MotionState.Default;
_output.WriteLine($"MotionState.Default: XM={state.XM}, YM={state.YM}, ZT={state.ZT}, ZD={state.ZD}, TiltD={state.TiltD}, Dist={state.Dist}"); _output.WriteLine($"MotionState.Default: StageX={state.StageX}, StageY={state.StageY}, SourceZ={state.SourceZ}, DetectorZ={state.DetectorZ}, DetectorSwing={state.DetectorSwing}, FDD={state.FDD}");
_output.WriteLine($" Speeds: XM={state.XMSpeed}, YM={state.YMSpeed}, ZT={state.ZTSpeed}, ZD={state.ZDSpeed}, TiltD={state.TiltDSpeed}, Dist={state.DistSpeed}"); _output.WriteLine($" Speeds: StageX={state.StageXSpeed}, StageY={state.StageYSpeed}, SourceZ={state.SourceZSpeed}, DetectorZ={state.DetectorZSpeed}, DetectorSwing={state.DetectorSwingSpeed}, FDD={state.FDDSpeed}");
Assert.Equal(0, state.XM); Assert.Equal(0, state.StageX);
Assert.Equal(0, state.YM); Assert.Equal(0, state.StageY);
Assert.Equal(0, state.ZT); Assert.Equal(0, state.SourceZ);
Assert.Equal(0, state.ZD); Assert.Equal(0, state.DetectorZ);
Assert.Equal(0, state.TiltD); Assert.Equal(0, state.DetectorSwing);
Assert.Equal(0, state.Dist); Assert.Equal(0, state.FDD);
Assert.Equal(0, state.XMSpeed); Assert.Equal(0, state.StageXSpeed);
Assert.Equal(0, state.YMSpeed); Assert.Equal(0, state.StageYSpeed);
Assert.Equal(0, state.ZTSpeed); Assert.Equal(0, state.SourceZSpeed);
Assert.Equal(0, state.ZDSpeed); Assert.Equal(0, state.DetectorZSpeed);
Assert.Equal(0, state.TiltDSpeed); Assert.Equal(0, state.DetectorSwingSpeed);
Assert.Equal(0, state.DistSpeed); Assert.Equal(0, state.FDDSpeed);
Assert.Equal(0, state.StageRotation);
Assert.Equal(0, state.FixtureRotation);
Assert.Equal(0, state.FOD);
Assert.Equal(0, state.Magnification);
} }
[Fact] [Fact]
@@ -116,15 +120,15 @@ namespace XplorePlane.Tests.Models
public void MotionState_WithExpression_ProducesNewInstance() public void MotionState_WithExpression_ProducesNewInstance()
{ {
var original = MotionState.Default; var original = MotionState.Default;
var modified = original with { XM = 100 }; var modified = original with { StageX = 100 };
_output.WriteLine($"Original.XM={original.XM}, Modified.XM={modified.XM}, SameRef={ReferenceEquals(original, modified)}"); _output.WriteLine($"Original.StageX={original.StageX}, Modified.StageX={modified.StageX}, SameRef={ReferenceEquals(original, modified)}");
// New instance is different from original // New instance is different from original
Assert.NotSame(original, modified); Assert.NotSame(original, modified);
Assert.Equal(100, modified.XM); Assert.Equal(100, modified.StageX);
// Original is unchanged // Original is unchanged
Assert.Equal(0, original.XM); Assert.Equal(0, original.StageX);
} }
// ── CalibrationMatrix Transform Tests ───────────────────────── // ── CalibrationMatrix Transform Tests ─────────────────────────
@@ -1,7 +1,15 @@
using Moq; using Moq;
using Prism.Events;
using System; using System;
using System.Windows; using System.Windows;
using XP.Common.Logging.Interfaces; using XP.Common.Logging.Interfaces;
using XP.Hardware.Detector.Abstractions.Enums;
using XP.Hardware.Detector.Abstractions.Events;
using XP.Hardware.Detector.Services;
using XP.Hardware.MotionControl.Abstractions;
using XP.Hardware.MotionControl.Abstractions.Enums;
using XP.Hardware.MotionControl.Abstractions.Events;
using XP.Hardware.MotionControl.Services;
using XP.Hardware.RaySource.Services; using XP.Hardware.RaySource.Services;
using XplorePlane.Models; using XplorePlane.Models;
using XplorePlane.Services.AppState; using XplorePlane.Services.AppState;
@@ -11,30 +19,82 @@ using Xunit.Abstractions;
namespace XplorePlane.Tests.Services namespace XplorePlane.Tests.Services
{ {
/// <summary> /// <summary>
/// AppStateService 单元测试。 /// AppStateService unit tests.
/// 验证默认状态值、Dispose 后行为、null 参数校验、CalibrationMatrix 缺失时的错误处理。 /// Verifies default values, null guards, dispose behavior, and hardware-driven motion-state sync.
/// </summary> /// </summary>
public class AppStateServiceTests : IDisposable public class AppStateServiceTests : IDisposable
{ {
private readonly AppStateService _service; private readonly AppStateService _service;
private readonly Mock<IRaySourceService> _mockRaySource; private readonly Mock<IRaySourceService> _mockRaySource;
private readonly Mock<IMotionSystem> _mockMotionSystem;
private readonly Mock<IMotionControlService> _mockMotionControlService;
private readonly Mock<IDetectorService> _mockDetectorService;
private readonly Mock<ILinearAxis> _mockStageX;
private readonly Mock<ILinearAxis> _mockStageY;
private readonly Mock<ILinearAxis> _mockSourceZ;
private readonly Mock<ILinearAxis> _mockDetectorZ;
private readonly Mock<IRotaryAxis> _mockDetectorSwing;
private readonly Mock<IRotaryAxis> _mockStageRotation;
private readonly Mock<IRotaryAxis> _mockFixtureRotation;
private readonly Mock<ILoggerService> _mockLogger; private readonly Mock<ILoggerService> _mockLogger;
private readonly EventAggregator _eventAggregator;
private readonly ITestOutputHelper _output; private readonly ITestOutputHelper _output;
public AppStateServiceTests(ITestOutputHelper output) public AppStateServiceTests(ITestOutputHelper output)
{ {
_output = output; _output = output;
// Ensure WPF Application exists for Dispatcher
if (Application.Current == null) if (Application.Current == null)
{ {
new Application(); new Application();
} }
_mockRaySource = new Mock<IRaySourceService>(); _mockRaySource = new Mock<IRaySourceService>();
_mockMotionSystem = new Mock<IMotionSystem>();
_mockMotionControlService = new Mock<IMotionControlService>();
_mockDetectorService = new Mock<IDetectorService>();
_mockStageX = CreateLinearAxis(AxisId.StageX, 0);
_mockStageY = CreateLinearAxis(AxisId.StageY, 0);
_mockSourceZ = CreateLinearAxis(AxisId.SourceZ, 0);
_mockDetectorZ = CreateLinearAxis(AxisId.DetectorZ, 0);
_mockDetectorSwing = CreateRotaryAxis(RotaryAxisId.DetectorSwing, 0);
_mockStageRotation = CreateRotaryAxis(RotaryAxisId.StageRotation, 0);
_mockFixtureRotation = CreateRotaryAxis(RotaryAxisId.FixtureRotation, 0);
_mockLogger = new Mock<ILoggerService>(); _mockLogger = new Mock<ILoggerService>();
_eventAggregator = new EventAggregator();
_mockMotionSystem.Setup(x => x.GetLinearAxis(AxisId.StageX)).Returns(_mockStageX.Object);
_mockMotionSystem.Setup(x => x.GetLinearAxis(AxisId.StageY)).Returns(_mockStageY.Object);
_mockMotionSystem.Setup(x => x.GetLinearAxis(AxisId.SourceZ)).Returns(_mockSourceZ.Object);
_mockMotionSystem.Setup(x => x.GetLinearAxis(AxisId.DetectorZ)).Returns(_mockDetectorZ.Object);
_mockMotionSystem.Setup(x => x.GetRotaryAxis(RotaryAxisId.DetectorSwing)).Returns(_mockDetectorSwing.Object);
_mockMotionSystem.Setup(x => x.GetRotaryAxis(RotaryAxisId.StageRotation)).Returns(_mockStageRotation.Object);
_mockMotionSystem.Setup(x => x.GetRotaryAxis(RotaryAxisId.FixtureRotation)).Returns(_mockFixtureRotation.Object);
_mockMotionControlService
.Setup(x => x.GetCurrentGeometry())
.Returns((0d, 0d, 1d));
// DetectorServiceGetInfo 在未初始化时抛出,模拟此行为
_mockDetectorService
.Setup(x => x.GetInfo())
.Throws(new InvalidOperationException("探测器未初始化"));
_mockDetectorService
.SetupGet(x => x.Status)
.Returns(DetectorStatus.Uninitialized);
_mockDetectorService
.SetupGet(x => x.IsConnected)
.Returns(false);
_mockLogger.Setup(l => l.ForModule<AppStateService>()).Returns(_mockLogger.Object); _mockLogger.Setup(l => l.ForModule<AppStateService>()).Returns(_mockLogger.Object);
_service = new AppStateService(_mockRaySource.Object, _mockLogger.Object);
_service = new AppStateService(
_mockRaySource.Object,
_mockMotionSystem.Object,
_mockMotionControlService.Object,
_mockDetectorService.Object,
_eventAggregator,
_mockLogger.Object);
} }
public void Dispose() public void Dispose()
@@ -42,13 +102,15 @@ namespace XplorePlane.Tests.Services
_service.Dispose(); _service.Dispose();
} }
// ── 默认状态值验证 ──
[Fact] [Fact]
public void DefaultState_MotionState_IsDefault() public void DefaultState_MotionState_IsHardwareSnapshot()
{ {
_output.WriteLine($"MotionState == MotionState.Default: {ReferenceEquals(MotionState.Default, _service.MotionState)}"); Assert.Equal(0, _service.MotionState.StageX);
Assert.Same(MotionState.Default, _service.MotionState); Assert.Equal(0, _service.MotionState.StageY);
Assert.Equal(0, _service.MotionState.SourceZ);
Assert.Equal(0, _service.MotionState.DetectorZ);
Assert.Equal(0, _service.MotionState.DetectorSwing);
Assert.Equal(0, _service.MotionState.FDD);
} }
[Fact] [Fact]
@@ -72,8 +134,6 @@ namespace XplorePlane.Tests.Services
Assert.Null(_service.CalibrationMatrix); Assert.Null(_service.CalibrationMatrix);
} }
// ── null 参数抛出 ArgumentNullException ──
[Fact] [Fact]
public void UpdateMotionState_NullArgument_ThrowsArgumentNullException() public void UpdateMotionState_NullArgument_ThrowsArgumentNullException()
{ {
@@ -102,36 +162,117 @@ namespace XplorePlane.Tests.Services
_output.WriteLine($"UpdateSystemState(null) threw: {ex.GetType().Name}, Param={ex.ParamName}"); _output.WriteLine($"UpdateSystemState(null) threw: {ex.GetType().Name}, Param={ex.ParamName}");
} }
// ── Dispose 后 Update 被忽略 ──
[Fact] [Fact]
public void Dispose_ThenUpdate_IsIgnored() public void Dispose_ThenUpdate_IsIgnored()
{ {
var originalState = _service.MotionState; var originalState = _service.MotionState;
_service.Dispose(); _service.Dispose();
_output.WriteLine("Service disposed, attempting UpdateMotionState...");
// Should not throw, and state should remain unchanged
var newState = new MotionState(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); var newState = new MotionState(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);
_service.UpdateMotionState(newState); _service.UpdateMotionState(newState);
_output.WriteLine($"State unchanged after dispose: {ReferenceEquals(originalState, _service.MotionState)}");
Assert.Same(originalState, _service.MotionState); Assert.Same(originalState, _service.MotionState);
} }
// ── CalibrationMatrix 为 null 时 RequestLinkedView 设置错误状态 ──
[Fact] [Fact]
public void RequestLinkedView_NoCalibrationMatrix_SetsErrorState() public void RequestLinkedView_NoCalibrationMatrix_SetsErrorState()
{ {
// CalibrationMatrix is null by default
Assert.Null(_service.CalibrationMatrix); Assert.Null(_service.CalibrationMatrix);
_service.RequestLinkedView(100.0, 200.0); _service.RequestLinkedView(100.0, 200.0);
_output.WriteLine($"RequestLinkedView(100, 200) without CalibrationMatrix: HasError={_service.SystemState.HasError}, ErrorMessage='{_service.SystemState.ErrorMessage}'");
Assert.True(_service.SystemState.HasError); Assert.True(_service.SystemState.HasError);
Assert.NotEmpty(_service.SystemState.ErrorMessage); Assert.NotEmpty(_service.SystemState.ErrorMessage);
} }
[Fact]
public void GeometryUpdatedEvent_RefreshesMotionStateFromHardware()
{
_mockStageX.SetupGet(x => x.ActualPosition).Returns(12.5);
_mockStageY.SetupGet(x => x.ActualPosition).Returns(34.5);
_mockSourceZ.SetupGet(x => x.ActualPosition).Returns(56.5);
_mockDetectorZ.SetupGet(x => x.ActualPosition).Returns(78.5);
_mockDetectorSwing.SetupGet(x => x.ActualAngle).Returns(9.5);
_eventAggregator.GetEvent<GeometryUpdatedEvent>()
.Publish(new GeometryData(100, 222.2, 2.22));
Assert.Equal(12.5, _service.MotionState.StageX);
Assert.Equal(34.5, _service.MotionState.StageY);
Assert.Equal(56.5, _service.MotionState.SourceZ);
Assert.Equal(78.5, _service.MotionState.DetectorZ);
Assert.Equal(9.5, _service.MotionState.DetectorSwing);
Assert.Equal(222.2, _service.MotionState.FDD);
}
[Fact]
public void StatusChangedEvent_Acquiring_SyncsDetectorState()
{
// 模拟探测器进入采集状态
_mockDetectorService.SetupGet(x => x.Status).Returns(DetectorStatus.Acquiring);
_mockDetectorService.Setup(x => x.GetInfo()).Throws(new InvalidOperationException());
_eventAggregator.GetEvent<StatusChangedEvent>()
.Publish(DetectorStatus.Acquiring);
// 等待后台线程处理(BackgroundThread 订阅)
System.Threading.Thread.Sleep(100);
Assert.True(_service.DetectorState.IsConnected);
Assert.True(_service.DetectorState.IsAcquiring);
}
[Fact]
public void StatusChangedEvent_Uninitialized_SyncsDetectorStateDisconnected()
{
_eventAggregator.GetEvent<StatusChangedEvent>()
.Publish(DetectorStatus.Uninitialized);
System.Threading.Thread.Sleep(100);
Assert.False(_service.DetectorState.IsConnected);
Assert.False(_service.DetectorState.IsAcquiring);
}
[Fact]
public void ImageCapturedEvent_UpdatesLatestDetectorFrame()
{
Assert.Null(_service.LatestDetectorFrame);
var args = new XP.Hardware.Detector.Abstractions.ImageCapturedEventArgs
{
ImageData = new ushort[4],
Width = 2,
Height = 2,
FrameNumber = 1,
CaptureTime = DateTime.UtcNow
};
_eventAggregator.GetEvent<ImageCapturedEvent>().Publish(args);
// 等待后台线程处理
System.Threading.Thread.Sleep(100);
Assert.Same(args, _service.LatestDetectorFrame);
}
private static Mock<ILinearAxis> CreateLinearAxis(AxisId axisId, double position)
{
var axis = new Mock<ILinearAxis>();
axis.SetupGet(x => x.Id).Returns(axisId);
axis.SetupGet(x => x.ActualPosition).Returns(position);
axis.SetupGet(x => x.Status).Returns(AxisStatus.Idle);
return axis;
}
private static Mock<IRotaryAxis> CreateRotaryAxis(RotaryAxisId axisId, double angle)
{
var axis = new Mock<IRotaryAxis>();
axis.SetupGet(x => x.Id).Returns(axisId);
axis.SetupGet(x => x.ActualAngle).Returns(angle);
axis.SetupGet(x => x.Status).Returns(AxisStatus.Idle);
axis.SetupGet(x => x.Enabled).Returns(true);
return axis;
}
} }
} }
@@ -96,7 +96,7 @@ namespace XplorePlane.Tests.Services
var pipeline = new PipelineModel { Name = "TestPipeline" }; var pipeline = new PipelineModel { Name = "TestPipeline" };
var step = _service.RecordCurrentStep(recipe, pipeline); var step = _service.RecordCurrentStep(recipe, pipeline);
_output.WriteLine($"RecordCurrentStep: StepIndex={step.StepIndex}, MotionState.XM={step.MotionState.XM}, RaySource.Voltage={step.RaySourceState.Voltage}, Detector.Resolution={step.DetectorState.Resolution}, Pipeline={step.Pipeline.Name}"); _output.WriteLine($"RecordCurrentStep: StepIndex={step.StepIndex}, MotionState.StageX={step.MotionState.StageX}, RaySource.Voltage={step.RaySourceState.Voltage}, Detector.Resolution={step.DetectorState.Resolution}, Pipeline={step.Pipeline.Name}");
Assert.Equal(0, step.StepIndex); Assert.Equal(0, step.StepIndex);
Assert.Same(motionState, step.MotionState); Assert.Same(motionState, step.MotionState);
@@ -25,7 +25,7 @@ namespace XplorePlane.Tests.ViewModels
{ {
public class CncEditorViewModelTests public class CncEditorViewModelTests
{ {
// 鈹€鈹€ Helpers 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€ // ── Helpers ──────────────────────────────────────────────────────────────────
private static CncEditorViewModel CreateVm( private static CncEditorViewModel CreateVm(
Mock<ICncExecutionService> mockExecSvc = null, Mock<ICncExecutionService> mockExecSvc = null,
@@ -50,7 +50,7 @@ namespace XplorePlane.Tests.ViewModels
DateTime.UtcNow, DateTime.UtcNow, DateTime.UtcNow, DateTime.UtcNow,
new List<CncNode> new List<CncNode>
{ {
new ReferencePointNode(Guid.NewGuid(), 0, "鍙傝€冪偣_0", 0, 0, 0, 0, 0, 0, false, 0, 0) new ReferencePointNode(Guid.NewGuid(), 0, "参考点_0", 0, 0, 0, 0, 0, 0, false, 0, 0)
}.AsReadOnly())); }.AsReadOnly()));
mockCncProgramSvc mockCncProgramSvc
@@ -121,9 +121,9 @@ namespace XplorePlane.Tests.ViewModels
return new CncProgram(Guid.NewGuid(), "TestProgram", DateTime.UtcNow, DateTime.UtcNow, nodes); return new CncProgram(Guid.NewGuid(), "TestProgram", DateTime.UtcNow, DateTime.UtcNow, nodes);
} }
// 鈹€鈹€ Property 1: 杩愯/鍋滄鎸夐挳鐘舵€佷簰鏂?鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€ // ── Property 1: 运行/停止按钮状态互斥 ──────────────────────────────────────────
// Feature: cnc-run-execution, Property 1: 杩愯/鍋滄鎸夐挳鐘舵€佷簰鏂? // Feature: cnc-run-execution, Property 1: 运行/停止按钮状态互斥
// Validates: Requirements 1.1, 1.3, 1.4 // Validates: Requirements 1.1, 1.3, 1.4
[Property(MaxTest = 100)] [Property(MaxTest = 100)]
public Property RunStop_Commands_AreMutuallyExclusive() public Property RunStop_Commands_AreMutuallyExclusive()
@@ -168,9 +168,9 @@ namespace XplorePlane.Tests.ViewModels
}); });
} }
// 鈹€鈹€ Property 2: 鎵ц瀹屾垚鍚庣姸鎬侀噸缃?鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€ // ── Property 2: 执行完成后状态重置 ──────────────────────────────────────────────
// Feature: cnc-run-execution, Property 2: 鎵ц瀹屾垚鍚庣姸鎬侀噸缃? // Feature: cnc-run-execution, Property 2: 执行完成后状态重置
// Validates: Requirements 1.7, 6.5 // Validates: Requirements 1.7, 6.5
[Property(MaxTest = 100)] [Property(MaxTest = 100)]
public Property AfterExecution_IsRunningFalse_AllNodesIdle() public Property AfterExecution_IsRunningFalse_AllNodesIdle()
@@ -219,9 +219,9 @@ namespace XplorePlane.Tests.ViewModels
}); });
} }
// 鈹€鈹€ Property 12: 鎵ц涓紪杈戝懡浠ゅ叏閮ㄧ鐢?鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€ // ── Property 12: 执行中编辑命令全部禁用 ──────────────────────────────────────────
// Feature: cnc-run-execution, Property 12: 鎵ц涓紪杈戝懡浠ゅ叏閮ㄧ鐢? // Feature: cnc-run-execution, Property 12: 执行中编辑命令全部禁用
// Validates: Requirements 6.7 // Validates: Requirements 6.7
[Property(MaxTest = 100)] [Property(MaxTest = 100)]
public Property WhileRunning_AllEditCommands_AreDisabled() public Property WhileRunning_AllEditCommands_AreDisabled()
@@ -169,3 +169,258 @@ raySourceService?.Dispose();
``` ```
确保硬件连接在应用退出时正确断开。 确保硬件连接在应用退出时正确断开。
### 7.
---
# 硬件层 → AppState → UI 状态同步机制
## 整体结论
| 硬件子系统 | 同步方式 | AppState 订阅 | 状态是否自动同步 |
|-----------|---------|--------------|----------------|
| 运动控制(Motion) | 事件驱动 + 轮询 | ✅ 已订阅 `GeometryUpdatedEvent` / `AxisStatusChangedEvent` | ✅ 自动同步 |
| 射线源(RaySource) | 事件定义存在 | ❌ 未订阅任何事件 | ⚠️ 需手动推送 |
| 探测器(Detector) | 事件定义存在 | ❌ 未订阅任何事件 | ⚠️ 需手动推送 |
| 相机(Camera | ViewModel 直连 | ❌ 不经过 AppState | ⚠️ `CameraState` 为空壳 |
---
## 一、运动控制 — 完整链路
### 1.1 数据流
```
PLC 硬件(B&R
└─ IPlcService.IsConnected(轮询前置检查)
└─ MotionControlService.OnPollingTick() [System.Threading.Timer,周期 = PollingInterval ms]
├─ _motionSystem.UpdateAllStatus() ← 从 PLC 读取所有轴实际位置
├─ GetCurrentGeometry() ← 正算 FOD / FDD / Magnification
├─ 发布 GeometryUpdatedEvent ─┐
└─ 发布 AxisStatusChangedEvent ─┤
AppStateService(构造时订阅两个事件)
├─ OnGeometryUpdated()
└─ OnAxisStatusChanged()
└─ TryRefreshMotionStateFromHardware()
└─ BuildMotionStateSnapshot()
├─ 读取所有轴 ActualPosition / ActualAngle
└─ SetMotionState()
└─ 触发 MotionStateChanged 事件
└─ ViewModel 绑定自动刷新 UI
```
### 1.2 关键实现细节
**轮询启动**`MotionControlService.StartPolling()` 需在应用启动时显式调用,否则轮询不会运行。
**PLC 未连接时的保护**
```csharp
// MotionControlService.OnPollingTick()
if (!_plcService.IsConnected) return; // 直接跳过,不报错
```
**连续错误降频**
```csharp
if (_pollErrorCount > 3)
{
if (++_pollErrorCount % 50 != 0) return; // 每 50 次才尝试一次,防止日志刷屏
}
```
**AppStateService 初始化时的首次刷新**
```csharp
// AppStateService 构造函数末尾
private void SubscribeToExistingServices()
{
if (TryRefreshMotionStateFromHardware("initialization"))
_logger.Info("AppStateService subscribed to motion hardware state");
else
_logger.Warn("AppStateService could not initialize motion state from hardware");
}
```
PLC 未连接时 warn 但不崩溃,等待后续轮询事件触发再同步。
**`UpdateMotionState()` 的特殊行为**
```csharp
public void UpdateMotionState(MotionState newState)
{
// 优先从硬件层拉取最新快照,忽略外部传入值
if (TryRefreshMotionStateFromHardware("UpdateMotionState"))
return;
// 硬件不可用时才使用传入值(降级路径)
SetMotionState(newState);
}
```
硬件连接后,外部调用 `UpdateMotionState()` 实际上会被硬件快照覆盖,硬件层始终是 `MotionState` 的唯一真实来源。
### 1.3 事件清单
| 事件 | 发布方 | 订阅方 | 触发时机 |
|------|--------|--------|---------|
| `GeometryUpdatedEvent` | `MotionControlService` | `AppStateService``MotionControlViewModel``AxisControlViewModel` | 每次轮询 tick |
| `AxisStatusChangedEvent` | `MotionControlService` | `AppStateService``MotionControlViewModel``AxisControlViewModel` | 轴状态发生变化时 |
| `MotionErrorEvent` | `MotionControlService` | — | 轴状态变为 Error / Alarm |
| `DoorStatusChangedEvent` | `MotionControlService` | `MotionControlViewModel` | 安全门状态变化 |
| `DoorInterlockChangedEvent` | `MotionControlService` | — | 联锁状态变化 |
| `GeometryApplyRequestEvent` | DebugWindow | `MotionControlViewModel` | 调试窗口发起几何反算请求 |
---
## 二、射线源 — 断链(待补全)
### 2.1 现状
`AppStateService` 持有 `IRaySourceService` 引用,但**没有订阅任何射线源事件**。`RaySourceState` 只能通过外部显式调用 `UpdateRaySourceState()` 手动推送,硬件状态变化不会自动反映到 AppState。
```
XP.Hardware.RaySource 发布的事件(均无人在 AppState 层订阅):
├─ StatusUpdatedEvent ← 实时电压 / 电流 / 功率数据
├─ RaySourceStatusChangedEvent ← 三态状态(On / Off / Fault
├─ VariablesConnectedEvent ← PLC 变量连接状态
└─ OperationResultEvent ← 操作执行结果
```
### 2.2 当前 `RaySourceState` 的写入路径
目前只有两处会更新 `RaySourceState`
1. `CncProgramService.CreateReferencePointNode()` — 创建参考点时读取一次快照
2. 任何直接调用 `appStateService.UpdateRaySourceState()` 的地方(目前代码中无此调用)
### 2.3 修复方案
`AppStateService` 构造时订阅 `StatusUpdatedEvent`,将实时数据映射到 `RaySourceState`
```csharp
// AppStateService 构造函数中补充
_raySourceStatusToken = _eventAggregator
.GetEvent<StatusUpdatedEvent>()
.Subscribe(OnRaySourceStatusUpdated);
// 处理方法
private void OnRaySourceStatusUpdated(SystemStatusData data)
{
if (_disposed) return;
UpdateRaySourceState(new RaySourceState(
IsOn: data.IsOn,
Voltage: data.Voltage,
Power: data.Power));
}
```
同时在 `Dispose()` 中取消订阅,并在 `AppStateService` 的字段和 `Dispose` 方法中补充对应的 `SubscriptionToken`
---
## 三、探测器 — 断链(待补全)
### 3.1 现状
与射线源情况相同,`AppStateService` 没有订阅任何探测器事件,`DetectorState` 是静态默认值。
```
XP.Hardware.Detector 发布的事件(均无人在 AppState 层订阅):
├─ StatusChangedEvent ← 探测器连接 / 采集状态变化
├─ ImageCapturedEvent ← 图像采集完成
├─ CorrectionCompletedEvent ← 校正完成
└─ ErrorOccurredEvent ← 错误发生
```
### 3.2 修复方案
`AppStateService` 构造时订阅 `StatusChangedEvent`
```csharp
_detectorStatusToken = _eventAggregator
.GetEvent<XP.Hardware.Detector.Abstractions.Events.StatusChangedEvent>()
.Subscribe(OnDetectorStatusChanged);
private void OnDetectorStatusChanged(DetectorStatus status)
{
if (_disposed) return;
UpdateDetectorState(new DetectorState(
IsConnected: status.IsConnected,
IsAcquiring: status.IsAcquiring,
FrameRate: status.FrameRate,
Resolution: status.Resolution));
}
```
---
## 四、相机 — 独立直连,不经过 AppState
### 4.1 现状
相机完全绕过 `AppStateService`,由 `NavigationPropertyPanelViewModel` 直接持有 `ICamera` 引用并订阅硬件事件:
```
NavigationPropertyPanelViewModel
└─ 直接持有 ICamera 引用
├─ _camera.ImageGrabbed += OnCameraImageGrabbed
├─ _camera.GrabError += OnCameraGrabError
└─ _camera.ConnectionLost += OnCameraConnectionLost
└─ IsCameraConnected 属性直接驱动 UI 命令可用性
```
`AppStateService.CameraState` 字段目前是空壳,没有任何地方向它写入真实数据。
### 4.2 架构说明
相机的独立直连是有意为之的设计——相机图像流数据量大、帧率高,不适合经过 AppState 的不可变 record 替换机制。当前图像通过 `ManualImageLoadedEvent` 在模块间传递,与状态管理解耦。
如需将相机连接状态纳入 AppState 统一管理(例如供其他模块查询),可在 `OnCameraConnectionLost` / `ConnectCamera` 中补充调用 `_appStateService.UpdateCameraState()`,仅同步连接状态,不同步图像帧。
---
## 五、状态同步全景图
```
┌─────────────────────────────────────────────────────────────────────┐
│ 硬件层 │
│ │
│ PLC ──→ MotionControlService ──→ GeometryUpdatedEvent ─────┐ │
│ └──→ AxisStatusChangedEvent ────┤ │
│ │ │
│ RaySource ──→ IRaySourceService ──→ StatusUpdatedEvent ─ ─ ┤ │
│ └──→ RaySourceStatusChangedEvent ─┤│
│ │ │
│ Detector ──→ IDetectorService ──→ StatusChangedEvent ─ ─ ┤ │
│ │ │
│ Camera ──→ ICamera ──→ ImageGrabbed / ConnectionLost ─ ─ ┘ │
└─────────────────────────────────────────────────────────────────────┘
│ 已接通 ✅ │ 断链 ⚠️
↓ ↓
┌─────────────────────────────────────────────────────────────────────┐
│ AppStateService │
│ │
│ MotionState ← 自动同步(轮询 + 事件驱动) ✅ │
│ RaySourceState ← 需手动推送 / 待补订阅 ⚠️ │
│ DetectorState ← 需手动推送 / 待补订阅 ⚠️ │
│ CameraState ← 空壳,相机走独立直连路径 ⚠️ │
└─────────────────────────────────────────────────────────────────────┘
↓ PropertyChanged / StateChangedEvent
┌─────────────────────────────────────────────────────────────────────┐
│ ViewModel 层 │
│ │
│ MotionControlViewModel ← 同时订阅 GeometryUpdatedEvent(双路) │
│ CncNodeViewModel ← 通过 IAppStateService 读取快照 │
│ CncEditorViewModel ← 通过 IAppStateService 读取快照 │
│ NavigationPropertyPanelViewModel ← 直连 ICamera(独立路径) │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 六、待办事项
| 优先级 | 项目 | 涉及文件 |
|--------|------|---------|
| 高 | 补充射线源事件订阅,打通 `RaySourceState` 自动同步 | `AppStateService.cs` |
| 高 | 补充探测器事件订阅,打通 `DetectorState` 自动同步 | `AppStateService.cs` |
| 中 | 确认 `StartPolling()` 在应用启动流程中被调用 | `App.xaml.cs` / `AppBootstrapper` |
| 低 | 评估是否将相机连接状态纳入 `CameraState` 统一管理 | `NavigationPropertyPanelViewModel.cs``AppStateService.cs` |
+49 -29
View File
@@ -40,69 +40,89 @@ namespace XplorePlane.Models
Guid Id, Guid Id,
int Index, int Index,
CncNodeType NodeType, CncNodeType NodeType,
string Name string Name);
);
/// <summary>参考点节点 | Reference point node</summary> /// <summary>参考点节点 | Reference point node</summary>
public record ReferencePointNode( public record ReferencePointNode(
Guid Id, int Index, string Name, Guid Id,
double XM, double YM, double ZT, double ZD, double TiltD, double Dist, int Index,
bool IsRayOn, double Voltage, double Current string Name,
) : CncNode(Id, Index, CncNodeType.ReferencePoint, Name); double StageX,
double StageY,
double SourceZ,
double DetectorZ,
double DetectorSwing,
double FDD,
bool IsRayOn,
double Voltage,
double Current,
double StageRotation = 0,
double FixtureRotation = 0,
double FOD = 0,
double Magnification = 0) : CncNode(Id, Index, CncNodeType.ReferencePoint, Name);
/// <summary>保存节点(含图像)| Save node with image</summary> /// <summary>保存节点(含图像)| Save node with image</summary>
public record SaveNodeWithImageNode( public record SaveNodeWithImageNode(
Guid Id, int Index, string Name, Guid Id,
int Index,
string Name,
MotionState MotionState, MotionState MotionState,
RaySourceState RaySourceState, RaySourceState RaySourceState,
DetectorState DetectorState, DetectorState DetectorState,
string ImageFileName string ImageFileName) : CncNode(Id, Index, CncNodeType.SaveNodeWithImage, Name);
) : CncNode(Id, Index, CncNodeType.SaveNodeWithImage, Name);
/// <summary>保存节点(不含图像)| Save node without image</summary> /// <summary>保存节点(不含图像)| Save node without image</summary>
public record SaveNodeNode( public record SaveNodeNode(
Guid Id, int Index, string Name, Guid Id,
int Index,
string Name,
MotionState MotionState, MotionState MotionState,
RaySourceState RaySourceState, RaySourceState RaySourceState,
DetectorState DetectorState DetectorState DetectorState) : CncNode(Id, Index, CncNodeType.SaveNode, Name);
) : CncNode(Id, Index, CncNodeType.SaveNode, Name);
/// <summary>保存位置节点 | Save position node</summary> /// <summary>保存位置节点 | Save position node</summary>
public record SavePositionNode( public record SavePositionNode(
Guid Id, int Index, string Name, Guid Id,
MotionState MotionState int Index,
) : CncNode(Id, Index, CncNodeType.SavePosition, Name); string Name,
MotionState MotionState) : CncNode(Id, Index, CncNodeType.SavePosition, Name);
/// <summary>检测模块节点 | Inspection module node</summary> /// <summary>检测模块节点 | Inspection module node</summary>
public record InspectionModuleNode( public record InspectionModuleNode(
Guid Id, int Index, string Name, Guid Id,
PipelineModel Pipeline int Index,
) : CncNode(Id, Index, CncNodeType.InspectionModule, Name); string Name,
PipelineModel Pipeline) : CncNode(Id, Index, CncNodeType.InspectionModule, Name);
/// <summary>检测标记节点 | Inspection marker node</summary> /// <summary>检测标记节点 | Inspection marker node</summary>
public record InspectionMarkerNode( public record InspectionMarkerNode(
Guid Id, int Index, string Name, Guid Id,
int Index,
string Name,
string MarkerType, string MarkerType,
double MarkerX, double MarkerY double MarkerX,
) : CncNode(Id, Index, CncNodeType.InspectionMarker, Name); double MarkerY) : CncNode(Id, Index, CncNodeType.InspectionMarker, Name);
/// <summary>停顿对话框节点 | Pause dialog node</summary> /// <summary>停顿对话框节点 | Pause dialog node</summary>
public record PauseDialogNode( public record PauseDialogNode(
Guid Id, int Index, string Name, Guid Id,
int Index,
string Name,
string DialogTitle, string DialogTitle,
string DialogMessage string DialogMessage) : CncNode(Id, Index, CncNodeType.PauseDialog, Name);
) : CncNode(Id, Index, CncNodeType.PauseDialog, Name);
/// <summary>等待延时节点 | Wait delay node</summary> /// <summary>等待延时节点 | Wait delay node</summary>
public record WaitDelayNode( public record WaitDelayNode(
Guid Id, int Index, string Name, Guid Id,
int DelayMilliseconds int Index,
) : CncNode(Id, Index, CncNodeType.WaitDelay, Name); string Name,
int DelayMilliseconds) : CncNode(Id, Index, CncNodeType.WaitDelay, Name);
/// <summary>完成程序节点 | Complete program node</summary> /// <summary>完成程序节点 | Complete program node</summary>
public record CompleteProgramNode( public record CompleteProgramNode(
Guid Id, int Index, string Name Guid Id,
) : CncNode(Id, Index, CncNodeType.CompleteProgram, Name); int Index,
string Name) : CncNode(Id, Index, CncNodeType.CompleteProgram, Name);
// ── CNC 程序 | CNC Program ──────────────────────────────────────── // ── CNC 程序 | CNC Program ────────────────────────────────────────
+50 -50
View File
@@ -2,8 +2,6 @@ using System;
namespace XplorePlane.Models namespace XplorePlane.Models
{ {
// ── Enumerations ──────────────────────────────────────────────────
/// <summary>系统操作模式</summary> /// <summary>系统操作模式</summary>
public enum OperationMode public enum OperationMode
{ {
@@ -23,82 +21,85 @@ namespace XplorePlane.Models
Error // 出错 Error // 出错
} }
// ── State Records ───────────────────────────────────────────────── // State Records
/// <summary>运动控制状态(不可变)</summary> /// <summary>
/// 运动控制状态(不可变)。
/// 统一的运动与几何快照,与运动硬件模型对齐。
/// </summary>
public record MotionState( public record MotionState(
double XM, // X 轴位置 (μm) double StageX, // X 轴位置μm
double YM, // Y 轴位置 (μm) double StageY, // Y 轴位置μm
double ZT, // Z 上轴位置 (μm) double SourceZ, // Z 上轴位置μm
double ZD, // Z 下轴位置 (μm) double DetectorZ, // Z 下轴位置μm
double TiltD, // 倾斜角度 (m°) double DetectorSwing, // 探测器摆角(°)
double Dist, // 距离 (μm) double FDD, // 焦点-探测器距离(μm
double XMSpeed, // X 轴速度 (μm/s) double StageXSpeed, // X 轴速度μm/s
double YMSpeed, // Y 轴速度 (μm/s) double StageYSpeed, // Y 轴速度μm/s
double ZTSpeed, // Z 上轴速度 (μm/s) double SourceZSpeed, // Z 上轴速度μm/s
double ZDSpeed, // Z 下轴速度 (μm/s) double DetectorZSpeed, // Z 下轴速度μm/s
double TiltDSpeed, // 倾斜速度 (m°/s) double DetectorSwingSpeed, // 探测器摆角速度(°/s
double DistSpeed // 距离速度 (μm/s) double FDDSpeed, // 焦点-探测器距离速度μm/s
) double StageRotation = 0, // 载台旋转角度(°)
double FixtureRotation = 0, // 夹具旋转角度(°)
double FOD = 0, // 焦点-物体距离(μm
double Magnification = 0, // 放大倍率
double StageRotationSpeed = 0, // 载台旋转速度(°/s
double FixtureRotationSpeed = 0) // 夹具旋转速度(°/s
{ {
public static readonly MotionState Default = new(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); public static readonly MotionState Default = new(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
} }
/// <summary>射线源状态(不可变)</summary> /// <summary>射线源状态(不可变)</summary>
public record RaySourceState( public record RaySourceState(
bool IsOn, // 开关状态 bool IsOn, // 是否开启
double Voltage, // 电压 (kV) double Voltage, // 电压kV
double Power // 功率 (W) double Power) // 功率W
)
{ {
public static readonly RaySourceState Default = new(false, 0, 0); public static readonly RaySourceState Default = new(false, 0, 0);
} }
/// <summary>探测器状态(不可变)</summary> /// <summary>探测器状态(不可变)</summary>
public record DetectorState( public record DetectorState(
bool IsConnected, // 连接状态 bool IsConnected, // 是否已连接
bool IsAcquiring, // 是否正在采集 bool IsAcquiring, // 是否正在采集
double FrameRate, // 当前帧率 (fps) double FrameRate, // 帧率(fps
string Resolution // 分辨率描述,如 "2048x2048" string Resolution) // 分辨率描述
)
{ {
public static readonly DetectorState Default = new(false, false, 0, string.Empty); public static readonly DetectorState Default = new(false, false, 0, string.Empty);
} }
/// <summary>系统状态(不可变)</summary> /// <summary>系统整体状态(不可变)</summary>
public record SystemState( public record SystemState(
OperationMode OperationMode, // 当前操作模式 OperationMode OperationMode, // 当前操作模式
bool HasError, // 是否存在系统错误 bool HasError, // 是否存在错误
string ErrorMessage // 错误描述 string ErrorMessage) // 错误信息
)
{ {
public static readonly SystemState Default = new(OperationMode.Idle, false, string.Empty); public static readonly SystemState Default = new(OperationMode.Idle, false, string.Empty);
} }
/// <summary>摄像头视频流状态(不可变)</summary> /// <summary>相机状态(不可变)</summary>
public record CameraState( public record CameraState(
bool IsConnected, // 连接状态 bool IsConnected, // 是否已连接
bool IsStreaming, // 是否正在推流 bool IsStreaming, // 是否正在推流
object CurrentFrame, // 当前帧数据引用(BitmapSource 或 byte[]Frozen object CurrentFrame, // 当前帧数据
int Width, // 分辨率宽 int Width, // 图像宽度(px
int Height, // 分辨率高 int Height, // 图像高度(px
double FrameRate // 帧率 (fps) double FrameRate) // 帧率fps
)
{ {
public static readonly CameraState Default = new(false, false, null, 0, 0, 0); public static readonly CameraState Default = new(false, false, null, 0, 0, 0);
} }
/// <summary>物理坐标</summary> /// <summary>物理坐标位置</summary>
public record PhysicalPosition(double X, double Y, double Z); public record PhysicalPosition(double X, double Y, double Z);
/// <summary>图像标定矩阵,像素坐标 → 物理坐标映射</summary> /// <summary>标定矩阵3×3 仿射变换)</summary>
public record CalibrationMatrix( public record CalibrationMatrix(
double M11, double M12, double M13, // 3x3 仿射变换矩阵 double M11, double M12, double M13,
double M21, double M22, double M23, double M21, double M22, double M23,
double M31, double M32, double M33 double M31, double M32, double M33)
)
{ {
/// <summary>将像素坐标换为物理坐标</summary> /// <summary>将像素坐标换为物理坐标</summary>
public (double X, double Y, double Z) Transform(double pixelX, double pixelY) public (double X, double Y, double Z) Transform(double pixelX, double pixelY)
{ {
double x = M11 * pixelX + M12 * pixelY + M13; double x = M11 * pixelX + M12 * pixelY + M13;
@@ -108,12 +109,11 @@ namespace XplorePlane.Models
} }
} }
/// <summary>画面联动状态(不可变)</summary> /// <summary>联动视图状态(不可变)</summary>
public record LinkedViewState( public record LinkedViewState(
PhysicalPosition TargetPosition, // 目标物理坐标 PhysicalPosition TargetPosition, // 目标物理位置
bool IsExecuting, // 联动是否正在执行 bool IsExecuting, // 是否正在执行移动
DateTime LastRequestTime // 最近一次联动请求时间 DateTime LastRequestTime) // 最近一次请求时间
)
{ {
public static readonly LinkedViewState Default = new(new PhysicalPosition(0, 0, 0), false, DateTime.MinValue); public static readonly LinkedViewState Default = new(new PhysicalPosition(0, 0, 0), false, DateTime.MinValue);
} }
+283 -50
View File
@@ -1,29 +1,48 @@
using Prism.Events;
using Prism.Mvvm; using Prism.Mvvm;
using System; using System;
using System.Threading; using System.Threading;
using System.Windows; using System.Windows;
using System.Windows.Threading; using System.Windows.Threading;
using XP.Common.Logging.Interfaces; using XP.Common.Logging.Interfaces;
using XP.Hardware.Detector.Abstractions;
using XP.Hardware.Detector.Abstractions.Enums;
using XP.Hardware.Detector.Abstractions.Events;
using XP.Hardware.Detector.Services;
using XP.Hardware.MotionControl.Abstractions;
using XP.Hardware.MotionControl.Abstractions.Enums;
using XP.Hardware.MotionControl.Abstractions.Events;
using XP.Hardware.MotionControl.Services;
using XP.Hardware.RaySource.Services; using XP.Hardware.RaySource.Services;
using XplorePlane.Models; using XplorePlane.Models;
namespace XplorePlane.Services.AppState namespace XplorePlane.Services.AppState
{ {
/// <summary> /// <summary>
/// 全局应用状态管理服务实现。 /// Global application state service.
/// 继承 BindableBase 以支持 WPF 数据绑定,使用 Interlocked.Exchange 保证线程安全写入, /// Motion state is synchronized from the motion hardware service layer and
/// 通过 Dispatcher.BeginInvoke 将事件调度到 UI 线程。 /// mapped into the legacy business model for compatibility.
/// Detector state and latest frame are synchronized via Prism EventAggregator subscriptions.
/// </summary> /// </summary>
public class AppStateService : BindableBase, IAppStateService public class AppStateService : BindableBase, IAppStateService
{ {
private readonly Dispatcher _dispatcher; private readonly Dispatcher _dispatcher;
private readonly IRaySourceService _raySourceService; private readonly IRaySourceService _raySourceService;
private readonly IMotionSystem _motionSystem;
private readonly IMotionControlService _motionControlService;
private readonly IDetectorService _detectorService;
private readonly IEventAggregator _eventAggregator;
private readonly ILoggerService _logger; private readonly ILoggerService _logger;
private readonly SubscriptionToken _axisStatusChangedToken;
private readonly SubscriptionToken _geometryUpdatedToken;
private readonly SubscriptionToken _detectorStatusChangedToken;
private readonly SubscriptionToken _detectorImageCapturedToken;
private bool _disposed; private bool _disposed;
private GeometryData _latestGeometry;
// ── 状态字段(通过 Interlocked.Exchange 原子替换)── // ── 状态字段(通过 Interlocked.Exchange 原子替换)──
private MotionState _motionState = MotionState.Default; private MotionState _motionState = MotionState.Default;
private RaySourceState _raySourceState = RaySourceState.Default; private RaySourceState _raySourceState = RaySourceState.Default;
private DetectorState _detectorState = DetectorState.Default; private DetectorState _detectorState = DetectorState.Default;
private SystemState _systemState = SystemState.Default; private SystemState _systemState = SystemState.Default;
@@ -32,26 +51,21 @@ namespace XplorePlane.Services.AppState
private LinkedViewState _linkedViewState = LinkedViewState.Default; private LinkedViewState _linkedViewState = LinkedViewState.Default;
private RecipeExecutionState _recipeExecutionState = RecipeExecutionState.Default; private RecipeExecutionState _recipeExecutionState = RecipeExecutionState.Default;
// ── 探测器最新帧(volatile,供任意线程读取)──
private volatile ImageCapturedEventArgs _latestDetectorFrame;
// ── 类型化状态变更事件 ── // ── 类型化状态变更事件 ──
public event EventHandler<StateChangedEventArgs<MotionState>> MotionStateChanged; public event EventHandler<StateChangedEventArgs<MotionState>> MotionStateChanged;
public event EventHandler<StateChangedEventArgs<RaySourceState>> RaySourceStateChanged; public event EventHandler<StateChangedEventArgs<RaySourceState>> RaySourceStateChanged;
public event EventHandler<StateChangedEventArgs<DetectorState>> DetectorStateChanged; public event EventHandler<StateChangedEventArgs<DetectorState>> DetectorStateChanged;
public event EventHandler<StateChangedEventArgs<SystemState>> SystemStateChanged; public event EventHandler<StateChangedEventArgs<SystemState>> SystemStateChanged;
public event EventHandler<StateChangedEventArgs<CameraState>> CameraStateChanged; public event EventHandler<StateChangedEventArgs<CameraState>> CameraStateChanged;
public event EventHandler<StateChangedEventArgs<LinkedViewState>> LinkedViewStateChanged; public event EventHandler<StateChangedEventArgs<LinkedViewState>> LinkedViewStateChanged;
public event EventHandler<StateChangedEventArgs<RecipeExecutionState>> RecipeExecutionStateChanged; public event EventHandler<StateChangedEventArgs<RecipeExecutionState>> RecipeExecutionStateChanged;
public event EventHandler<LinkedViewRequestEventArgs> LinkedViewRequested; public event EventHandler<LinkedViewRequestEventArgs> LinkedViewRequested;
// ── 状态属性(只读)── // ── 状态属性(只读)──
public MotionState MotionState => _motionState; public MotionState MotionState => _motionState;
public RaySourceState RaySourceState => _raySourceState; public RaySourceState RaySourceState => _raySourceState;
public DetectorState DetectorState => _detectorState; public DetectorState DetectorState => _detectorState;
public SystemState SystemState => _systemState; public SystemState SystemState => _systemState;
@@ -60,19 +74,56 @@ namespace XplorePlane.Services.AppState
public LinkedViewState LinkedViewState => _linkedViewState; public LinkedViewState LinkedViewState => _linkedViewState;
public RecipeExecutionState RecipeExecutionState => _recipeExecutionState; public RecipeExecutionState RecipeExecutionState => _recipeExecutionState;
/// <summary>
/// 探测器最新采集帧(线程安全,可从任意线程读取)。
/// 由 ImageCapturedEvent 驱动更新,无采集时为 null。
/// </summary>
public ImageCapturedEventArgs LatestDetectorFrame => _latestDetectorFrame;
public AppStateService( public AppStateService(
IRaySourceService raySourceService, IRaySourceService raySourceService,
IMotionSystem motionSystem,
IMotionControlService motionControlService,
IDetectorService detectorService,
IEventAggregator eventAggregator,
ILoggerService logger) ILoggerService logger)
{ {
ArgumentNullException.ThrowIfNull(raySourceService); ArgumentNullException.ThrowIfNull(raySourceService);
ArgumentNullException.ThrowIfNull(motionSystem);
ArgumentNullException.ThrowIfNull(motionControlService);
ArgumentNullException.ThrowIfNull(detectorService);
ArgumentNullException.ThrowIfNull(eventAggregator);
ArgumentNullException.ThrowIfNull(logger); ArgumentNullException.ThrowIfNull(logger);
_raySourceService = raySourceService; _raySourceService = raySourceService;
_motionSystem = motionSystem;
_motionControlService = motionControlService;
_detectorService = detectorService;
_eventAggregator = eventAggregator;
_logger = logger.ForModule<AppStateService>(); _logger = logger.ForModule<AppStateService>();
_dispatcher = Application.Current.Dispatcher; _dispatcher = Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher;
// ── 运动控制事件订阅 ──
_geometryUpdatedToken = _eventAggregator
.GetEvent<GeometryUpdatedEvent>()
.Subscribe(OnGeometryUpdated);
_axisStatusChangedToken = _eventAggregator
.GetEvent<AxisStatusChangedEvent>()
.Subscribe(OnAxisStatusChanged);
// ── 探测器状态事件订阅(后台线程,避免阻塞采集)──
_detectorStatusChangedToken = _eventAggregator
.GetEvent<StatusChangedEvent>()
.Subscribe(OnDetectorStatusChanged, ThreadOption.BackgroundThread);
// ── 探测器图像事件订阅(后台线程,仅缓存最新帧)──
_detectorImageCapturedToken = _eventAggregator
.GetEvent<ImageCapturedEvent>()
.Subscribe(OnDetectorImageCaptured, ThreadOption.BackgroundThread);
SubscribeToExistingServices(); SubscribeToExistingServices();
_logger.Info("AppStateService 已初始化"); _logger.Info("AppStateService initialized");
} }
// ── 状态更新方法 ── // ── 状态更新方法 ──
@@ -80,17 +131,30 @@ namespace XplorePlane.Services.AppState
public void UpdateMotionState(MotionState newState) public void UpdateMotionState(MotionState newState)
{ {
ArgumentNullException.ThrowIfNull(newState); ArgumentNullException.ThrowIfNull(newState);
if (_disposed) { _logger.Warn("AppStateService 已释放,忽略 UpdateMotionState 调用"); return; } if (_disposed)
{
_logger.Warn("AppStateService is disposed, ignoring UpdateMotionState");
return;
}
var old = Interlocked.Exchange(ref _motionState, newState); // Keep the legacy API surface, but let the hardware service layer
if (ReferenceEquals(old, newState)) return; // remain the source of truth whenever a fresh hardware snapshot is available.
RaiseOnDispatcher(old, newState, MotionStateChanged, nameof(MotionState)); if (TryRefreshMotionStateFromHardware("UpdateMotionState"))
{
return;
}
SetMotionState(newState);
} }
public void UpdateRaySourceState(RaySourceState newState) public void UpdateRaySourceState(RaySourceState newState)
{ {
ArgumentNullException.ThrowIfNull(newState); ArgumentNullException.ThrowIfNull(newState);
if (_disposed) { _logger.Warn("AppStateService 已释放,忽略 UpdateRaySourceState 调用"); return; } if (_disposed)
{
_logger.Warn("AppStateService is disposed, ignoring UpdateRaySourceState");
return;
}
var old = Interlocked.Exchange(ref _raySourceState, newState); var old = Interlocked.Exchange(ref _raySourceState, newState);
if (ReferenceEquals(old, newState)) return; if (ReferenceEquals(old, newState)) return;
@@ -100,7 +164,11 @@ namespace XplorePlane.Services.AppState
public void UpdateDetectorState(DetectorState newState) public void UpdateDetectorState(DetectorState newState)
{ {
ArgumentNullException.ThrowIfNull(newState); ArgumentNullException.ThrowIfNull(newState);
if (_disposed) { _logger.Warn("AppStateService 已释放,忽略 UpdateDetectorState 调用"); return; } if (_disposed)
{
_logger.Warn("AppStateService is disposed, ignoring UpdateDetectorState");
return;
}
var old = Interlocked.Exchange(ref _detectorState, newState); var old = Interlocked.Exchange(ref _detectorState, newState);
if (ReferenceEquals(old, newState)) return; if (ReferenceEquals(old, newState)) return;
@@ -110,7 +178,11 @@ namespace XplorePlane.Services.AppState
public void UpdateSystemState(SystemState newState) public void UpdateSystemState(SystemState newState)
{ {
ArgumentNullException.ThrowIfNull(newState); ArgumentNullException.ThrowIfNull(newState);
if (_disposed) { _logger.Warn("AppStateService 已释放,忽略 UpdateSystemState 调用"); return; } if (_disposed)
{
_logger.Warn("AppStateService is disposed, ignoring UpdateSystemState");
return;
}
var old = Interlocked.Exchange(ref _systemState, newState); var old = Interlocked.Exchange(ref _systemState, newState);
if (ReferenceEquals(old, newState)) return; if (ReferenceEquals(old, newState)) return;
@@ -120,7 +192,11 @@ namespace XplorePlane.Services.AppState
public void UpdateCameraState(CameraState newState) public void UpdateCameraState(CameraState newState)
{ {
ArgumentNullException.ThrowIfNull(newState); ArgumentNullException.ThrowIfNull(newState);
if (_disposed) { _logger.Warn("AppStateService 已释放,忽略 UpdateCameraState 调用"); return; } if (_disposed)
{
_logger.Warn("AppStateService is disposed, ignoring UpdateCameraState");
return;
}
var old = Interlocked.Exchange(ref _cameraState, newState); var old = Interlocked.Exchange(ref _cameraState, newState);
if (ReferenceEquals(old, newState)) return; if (ReferenceEquals(old, newState)) return;
@@ -130,21 +206,26 @@ namespace XplorePlane.Services.AppState
public void UpdateCalibrationMatrix(CalibrationMatrix newMatrix) public void UpdateCalibrationMatrix(CalibrationMatrix newMatrix)
{ {
ArgumentNullException.ThrowIfNull(newMatrix); ArgumentNullException.ThrowIfNull(newMatrix);
if (_disposed) { _logger.Warn("AppStateService 已释放,忽略 UpdateCalibrationMatrix 调用"); return; } if (_disposed)
{
_logger.Warn("AppStateService is disposed, ignoring UpdateCalibrationMatrix");
return;
}
var old = Interlocked.Exchange(ref _calibrationMatrix, newMatrix); var old = Interlocked.Exchange(ref _calibrationMatrix, newMatrix);
if (ReferenceEquals(old, newMatrix)) return; if (ReferenceEquals(old, newMatrix)) return;
_dispatcher.BeginInvoke(() => _dispatcher.BeginInvoke(() => RaisePropertyChanged(nameof(CalibrationMatrix)));
{
RaisePropertyChanged(nameof(CalibrationMatrix));
});
} }
public void UpdateLinkedViewState(LinkedViewState newState) public void UpdateLinkedViewState(LinkedViewState newState)
{ {
ArgumentNullException.ThrowIfNull(newState); ArgumentNullException.ThrowIfNull(newState);
if (_disposed) { _logger.Warn("AppStateService 已释放,忽略 UpdateLinkedViewState 调用"); return; } if (_disposed)
{
_logger.Warn("AppStateService is disposed, ignoring UpdateLinkedViewState");
return;
}
var old = Interlocked.Exchange(ref _linkedViewState, newState); var old = Interlocked.Exchange(ref _linkedViewState, newState);
if (ReferenceEquals(old, newState)) return; if (ReferenceEquals(old, newState)) return;
@@ -154,7 +235,11 @@ namespace XplorePlane.Services.AppState
public void UpdateRecipeExecutionState(RecipeExecutionState newState) public void UpdateRecipeExecutionState(RecipeExecutionState newState)
{ {
ArgumentNullException.ThrowIfNull(newState); ArgumentNullException.ThrowIfNull(newState);
if (_disposed) { _logger.Warn("AppStateService 已释放,忽略 UpdateRecipeExecutionState 调用"); return; } if (_disposed)
{
_logger.Warn("AppStateService is disposed, ignoring UpdateRecipeExecutionState");
return;
}
var old = Interlocked.Exchange(ref _recipeExecutionState, newState); var old = Interlocked.Exchange(ref _recipeExecutionState, newState);
if (ReferenceEquals(old, newState)) return; if (ReferenceEquals(old, newState)) return;
@@ -168,11 +253,11 @@ namespace XplorePlane.Services.AppState
var matrix = _calibrationMatrix; var matrix = _calibrationMatrix;
if (matrix is null) if (matrix is null)
{ {
_logger.Warn("CalibrationMatrix 未设置,无法执行画面联动 (pixelX={PixelX}, pixelY={PixelY})", pixelX, pixelY); _logger.Warn("CalibrationMatrix is not configured, cannot execute linked view request (pixelX={PixelX}, pixelY={PixelY})", pixelX, pixelY);
UpdateSystemState(SystemState with UpdateSystemState(SystemState with
{ {
HasError = true, HasError = true,
ErrorMessage = "CalibrationMatrix 未设置,无法执行画面联动" ErrorMessage = "CalibrationMatrix is not configured, cannot execute linked view request"
}); });
return; return;
} }
@@ -191,10 +276,48 @@ namespace XplorePlane.Services.AppState
}); });
} }
// ── 内部辅助方法 ── public void Dispose()
{
if (_disposed) return;
_disposed = true;
private void RaiseOnDispatcher<T>(T oldVal, T newVal, if (_axisStatusChangedToken is not null)
EventHandler<StateChangedEventArgs<T>> handler, string propertyName) {
_eventAggregator.GetEvent<AxisStatusChangedEvent>().Unsubscribe(_axisStatusChangedToken);
}
if (_geometryUpdatedToken is not null)
{
_eventAggregator.GetEvent<GeometryUpdatedEvent>().Unsubscribe(_geometryUpdatedToken);
}
if (_detectorStatusChangedToken is not null)
{
_eventAggregator.GetEvent<StatusChangedEvent>().Unsubscribe(_detectorStatusChangedToken);
}
if (_detectorImageCapturedToken is not null)
{
_eventAggregator.GetEvent<ImageCapturedEvent>().Unsubscribe(_detectorImageCapturedToken);
}
MotionStateChanged = null;
RaySourceStateChanged = null;
DetectorStateChanged = null;
SystemStateChanged = null;
CameraStateChanged = null;
LinkedViewStateChanged = null;
RecipeExecutionStateChanged = null;
LinkedViewRequested = null;
_logger.Info("AppStateService disposed");
}
private void RaiseOnDispatcher<T>(
T oldVal,
T newVal,
EventHandler<StateChangedEventArgs<T>> handler,
string propertyName)
{ {
_dispatcher.BeginInvoke(() => _dispatcher.BeginInvoke(() =>
{ {
@@ -205,34 +328,144 @@ namespace XplorePlane.Services.AppState
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Error(ex, "状态变更事件处理器抛出异常 (property={PropertyName})", propertyName); _logger.Error(ex, "State changed handler failed (property={PropertyName})", propertyName);
} }
}); });
} }
private void SubscribeToExistingServices() private void SubscribeToExistingServices()
{ {
_logger.Info("AppStateService 已准备好接收外部服务状态更新"); if (TryRefreshMotionStateFromHardware("initialization"))
{
_logger.Info("AppStateService subscribed to motion hardware state");
return;
}
_logger.Warn("AppStateService could not initialize motion state from hardware");
} }
// ── Dispose ── private void OnAxisStatusChanged(AxisStatusChangedData _)
public void Dispose()
{ {
if (_disposed) return; if (_disposed) return;
_disposed = true; TryRefreshMotionStateFromHardware("axis-status-changed");
}
// 清除所有事件订阅 private void OnGeometryUpdated(GeometryData geometry)
MotionStateChanged = null; {
RaySourceStateChanged = null; if (_disposed) return;
DetectorStateChanged = null;
SystemStateChanged = null;
CameraStateChanged = null;
LinkedViewStateChanged = null;
RecipeExecutionStateChanged = null;
LinkedViewRequested = null;
_logger.Info("AppStateService 已释放"); _latestGeometry = geometry;
TryRefreshMotionStateFromHardware("geometry-updated");
}
/// <summary>
/// 探测器状态变更回调。
/// 将硬件层 DetectorStatus 映射为应用层 DetectorState 并同步到 AppState。
/// 运行在后台线程(ThreadOption.BackgroundThread),不阻塞采集。
/// </summary>
private void OnDetectorStatusChanged(DetectorStatus status)
{
if (_disposed) return;
// 从 IDetectorService 读取分辨率等补充信息
string resolution = string.Empty;
double frameRate = 0;
try
{
var info = _detectorService.GetInfo();
if (info != null)
resolution = $"{info.MaxWidth}x{info.MaxHeight}";
}
catch
{
// 探测器未初始化时 GetInfo 会抛出,忽略即可
}
bool isConnected = status != DetectorStatus.Uninitialized && status != DetectorStatus.Error;
bool isAcquiring = status == DetectorStatus.Acquiring;
var newState = new DetectorState(
IsConnected: isConnected,
IsAcquiring: isAcquiring,
FrameRate: frameRate,
Resolution: resolution);
UpdateDetectorState(newState);
_logger.Info(
"探测器状态已同步:{Status} → IsConnected={IsConnected} IsAcquiring={IsAcquiring} | " +
"Detector state synced: {Status} → IsConnected={IsConnected} IsAcquiring={IsAcquiring}",
status, isConnected, isAcquiring);
}
/// <summary>
/// 探测器图像采集回调。
/// 仅缓存最新帧引用(volatile 写),不做任何图像处理,保持采集链路零阻塞。
/// 上层通过 LatestDetectorFrame 属性按需读取。
/// </summary>
private void OnDetectorImageCaptured(ImageCapturedEventArgs args)
{
if (_disposed || args?.ImageData == null) return;
_latestDetectorFrame = args;
}
private bool TryRefreshMotionStateFromHardware(string reason)
{
try
{
if (_latestGeometry is null)
{
var geometry = _motionControlService.GetCurrentGeometry();
_latestGeometry = new GeometryData(geometry.FOD, geometry.FDD, geometry.Magnification);
}
SetMotionState(BuildMotionStateSnapshot(_latestGeometry));
return true;
}
catch (Exception ex)
{
_logger.Warn("Failed to refresh motion state from hardware during {Reason}: {Message}", reason, ex.Message);
_logger.Error(ex, "Motion state refresh exception during {Reason}", reason);
return false;
}
}
private MotionState BuildMotionStateSnapshot(GeometryData geometry)
{
var stageX = _motionSystem.GetLinearAxis(AxisId.StageX);
var stageY = _motionSystem.GetLinearAxis(AxisId.StageY);
var sourceZ = _motionSystem.GetLinearAxis(AxisId.SourceZ);
var detectorZ = _motionSystem.GetLinearAxis(AxisId.DetectorZ);
var detectorSwing = _motionSystem.GetRotaryAxis(RotaryAxisId.DetectorSwing);
var stageRotation = _motionSystem.GetRotaryAxis(RotaryAxisId.StageRotation);
var fixtureRotation = _motionSystem.GetRotaryAxis(RotaryAxisId.FixtureRotation);
return new MotionState(
StageX: stageX.ActualPosition,
StageY: stageY.ActualPosition,
SourceZ: sourceZ.ActualPosition,
DetectorZ: detectorZ.ActualPosition,
DetectorSwing: detectorSwing.ActualAngle,
FDD: geometry?.FDD ?? 0,
StageXSpeed: 0,
StageYSpeed: 0,
SourceZSpeed: 0,
DetectorZSpeed: 0,
DetectorSwingSpeed: 0,
FDDSpeed: 0,
StageRotation: stageRotation.ActualAngle,
FixtureRotation: fixtureRotation.ActualAngle,
FOD: geometry?.FOD ?? 0,
Magnification: geometry?.Magnification ?? 0,
StageRotationSpeed: 0,
FixtureRotationSpeed: 0);
}
private void SetMotionState(MotionState newState)
{
var old = Interlocked.Exchange(ref _motionState, newState);
if (ReferenceEquals(old, newState)) return;
RaiseOnDispatcher(old, newState, MotionStateChanged, nameof(MotionState));
} }
} }
} }
@@ -1,5 +1,6 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using XP.Hardware.Detector.Abstractions;
using XplorePlane.Models; using XplorePlane.Models;
namespace XplorePlane.Services.AppState namespace XplorePlane.Services.AppState
@@ -21,6 +22,13 @@ namespace XplorePlane.Services.AppState
LinkedViewState LinkedViewState { get; } LinkedViewState LinkedViewState { get; }
RecipeExecutionState RecipeExecutionState { get; } RecipeExecutionState RecipeExecutionState { get; }
/// <summary>
/// 探测器最新采集帧(线程安全,可从任意线程读取)。
/// 由 ImageCapturedEvent 驱动更新,探测器未采集时为 null。
/// CNC 执行、图像处理等上层服务通过此属性按需取图,无需自行订阅事件。
/// </summary>
ImageCapturedEventArgs LatestDetectorFrame { get; }
// ── 状态更新方法(线程安全,可从任意线程调用)── // ── 状态更新方法(线程安全,可从任意线程调用)──
void UpdateMotionState(MotionState newState); void UpdateMotionState(MotionState newState);
@@ -103,6 +103,63 @@ namespace XplorePlane.Services.Cnc
{ {
switch (node) switch (node)
{ {
case ReferencePointNode rp:
_logger.ForModule<CncExecutionService>().Info(
"执行参考点节点 [{Index}] {Name} | " +
"StageX={StageX} StageY={StageY} SourceZ={SourceZ} DetectorZ={DetectorZ} " +
"DetectorSwing={DetectorSwing} FDD={FDD} FOD={FOD} Magnification={Magnification} " +
"StageRotation={StageRotation} FixtureRotation={FixtureRotation} " +
"RayOn={RayOn} Voltage={Voltage}kV Current={Current}uA",
rp.Index, rp.Name,
rp.StageX, rp.StageY, rp.SourceZ, rp.DetectorZ,
rp.DetectorSwing, rp.FDD, rp.FOD, rp.Magnification,
rp.StageRotation, rp.FixtureRotation,
rp.IsRayOn, rp.Voltage, rp.Current);
break;
case SavePositionNode sp:
_logger.ForModule<CncExecutionService>().Info(
"执行保存位置节点 [{Index}] {Name} | " +
"StageX={StageX} StageY={StageY} SourceZ={SourceZ} DetectorZ={DetectorZ} " +
"DetectorSwing={DetectorSwing} FDD={FDD} FOD={FOD} Magnification={Magnification} " +
"StageRotation={StageRotation} FixtureRotation={FixtureRotation}",
sp.Index, sp.Name,
sp.MotionState.StageX, sp.MotionState.StageY,
sp.MotionState.SourceZ, sp.MotionState.DetectorZ,
sp.MotionState.DetectorSwing, sp.MotionState.FDD,
sp.MotionState.FOD, sp.MotionState.Magnification,
sp.MotionState.StageRotation, sp.MotionState.FixtureRotation);
break;
case SaveNodeNode sn:
_logger.ForModule<CncExecutionService>().Info(
"执行保存节点 [{Index}] {Name} | " +
"StageX={StageX} StageY={StageY} SourceZ={SourceZ} DetectorZ={DetectorZ} " +
"DetectorSwing={DetectorSwing} FDD={FDD} FOD={FOD} Magnification={Magnification} " +
"RayOn={RayOn} Voltage={Voltage}kV Power={Power}W",
sn.Index, sn.Name,
sn.MotionState.StageX, sn.MotionState.StageY,
sn.MotionState.SourceZ, sn.MotionState.DetectorZ,
sn.MotionState.DetectorSwing, sn.MotionState.FDD,
sn.MotionState.FOD, sn.MotionState.Magnification,
sn.RaySourceState.IsOn, sn.RaySourceState.Voltage, sn.RaySourceState.Power);
break;
case SaveNodeWithImageNode sni:
_logger.ForModule<CncExecutionService>().Info(
"执行保存节点(图像) [{Index}] {Name} | " +
"StageX={StageX} StageY={StageY} SourceZ={SourceZ} DetectorZ={DetectorZ} " +
"DetectorSwing={DetectorSwing} FDD={FDD} FOD={FOD} Magnification={Magnification} " +
"RayOn={RayOn} Voltage={Voltage}kV Power={Power}W ImageFile={ImageFile}",
sni.Index, sni.Name,
sni.MotionState.StageX, sni.MotionState.StageY,
sni.MotionState.SourceZ, sni.MotionState.DetectorZ,
sni.MotionState.DetectorSwing, sni.MotionState.FDD,
sni.MotionState.FOD, sni.MotionState.Magnification,
sni.RaySourceState.IsOn, sni.RaySourceState.Voltage, sni.RaySourceState.Power,
sni.ImageFileName);
break;
case WaitDelayNode waitNode: case WaitDelayNode waitNode:
try try
{ {
+12 -8
View File
@@ -389,22 +389,26 @@ namespace XplorePlane.Services.Cnc
var raySource = _appStateService.RaySourceState; var raySource = _appStateService.RaySourceState;
return new ReferencePointNode( return new ReferencePointNode(
id, index, $"参考点_{index}", id, index, $"参考点_{index}",
XM: motion.XM, StageX: motion.StageX,
YM: motion.YM, StageY: motion.StageY,
ZT: motion.ZT, SourceZ: motion.SourceZ,
ZD: motion.ZD, DetectorZ: motion.DetectorZ,
TiltD: motion.TiltD, DetectorSwing: motion.DetectorSwing,
Dist: motion.Dist, FDD: motion.FDD,
IsRayOn: raySource.IsOn, IsRayOn: raySource.IsOn,
Voltage: raySource.Voltage, Voltage: raySource.Voltage,
Current: TryReadCurrent()); Current: TryReadCurrent(),
StageRotation: motion.StageRotation,
FixtureRotation: motion.FixtureRotation,
FOD: motion.FOD,
Magnification: motion.Magnification);
} }
/// <summary>创建保存节点(含图像)| Create save node with image</summary> /// <summary>创建保存节点(含图像)| Create save node with image</summary>
private SaveNodeWithImageNode CreateSaveNodeWithImageNode(Guid id, int index) private SaveNodeWithImageNode CreateSaveNodeWithImageNode(Guid id, int index)
{ {
return new SaveNodeWithImageNode( return new SaveNodeWithImageNode(
id, index, $"保存节点(图像)_{index}", id, index, $"保存节点_图像_{index}",
MotionState: _appStateService.MotionState, MotionState: _appStateService.MotionState,
RaySourceState: _appStateService.RaySourceState, RaySourceState: _appStateService.RaySourceState,
DetectorState: _appStateService.DetectorState, DetectorState: _appStateService.DetectorState,
@@ -417,7 +417,7 @@ namespace XplorePlane.ViewModels.Cnc
return; return;
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.AppendLine("Index,NodeType,Name,XM,YM,ZT,ZD,TiltD,Dist,Voltage_kV,Current_uA,Power_W,RayOn,DetectorConnected,FrameRate,Resolution,ImageFile,MarkerType,MarkerX,MarkerY,DialogTitle,DialogMessage,DelayMs,Pipeline"); sb.AppendLine("Index,NodeType,Name,SourceZ,DetectorZ,StageX,StageY,DetectorSwing,StageRotation,FixtureRotation,FOD,FDD,Magnification,Voltage_kV,Current_uA,Power_W,RayOn,DetectorConnected,FrameRate,Resolution,ImageFile,MarkerType,MarkerX,MarkerY,DialogTitle,DialogMessage,DelayMs,Pipeline");
var inv = CultureInfo.InvariantCulture; var inv = CultureInfo.InvariantCulture;
@@ -425,16 +425,16 @@ namespace XplorePlane.ViewModels.Cnc
{ {
var row = node switch var row = node switch
{ {
ReferencePointNode rp => $"{rp.Index},{rp.NodeType},{Esc(rp.Name)},{rp.XM.ToString(inv)},{rp.YM.ToString(inv)},{rp.ZT.ToString(inv)},{rp.ZD.ToString(inv)},{rp.TiltD.ToString(inv)},{rp.Dist.ToString(inv)},{rp.Voltage.ToString(inv)},{rp.Current.ToString(inv)},,{rp.IsRayOn},,,,,,,,,,", ReferencePointNode rp => $"{rp.Index},{rp.NodeType},{Esc(rp.Name)},{rp.SourceZ.ToString(inv)},{rp.DetectorZ.ToString(inv)},{rp.StageX.ToString(inv)},{rp.StageY.ToString(inv)},{rp.DetectorSwing.ToString(inv)},{rp.StageRotation.ToString(inv)},{rp.FixtureRotation.ToString(inv)},{rp.FOD.ToString(inv)},{rp.FDD.ToString(inv)},{rp.Magnification.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)},,,,,,", SaveNodeWithImageNode sni => $"{sni.Index},{sni.NodeType},{Esc(sni.Name)},{sni.MotionState.SourceZ.ToString(inv)},{sni.MotionState.DetectorZ.ToString(inv)},{sni.MotionState.StageX.ToString(inv)},{sni.MotionState.StageY.ToString(inv)},{sni.MotionState.DetectorSwing.ToString(inv)},{sni.MotionState.StageRotation.ToString(inv)},{sni.MotionState.FixtureRotation.ToString(inv)},{sni.MotionState.FOD.ToString(inv)},{sni.MotionState.FDD.ToString(inv)},{sni.MotionState.Magnification.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)},,,,,,,", SaveNodeNode sn => $"{sn.Index},{sn.NodeType},{Esc(sn.Name)},{sn.MotionState.SourceZ.ToString(inv)},{sn.MotionState.DetectorZ.ToString(inv)},{sn.MotionState.StageX.ToString(inv)},{sn.MotionState.StageY.ToString(inv)},{sn.MotionState.DetectorSwing.ToString(inv)},{sn.MotionState.StageRotation.ToString(inv)},{sn.MotionState.FixtureRotation.ToString(inv)},{sn.MotionState.FOD.ToString(inv)},{sn.MotionState.FDD.ToString(inv)},{sn.MotionState.Magnification.ToString(inv)},{sn.RaySourceState.Voltage.ToString(inv)},,{sn.RaySourceState.Power.ToString(inv)},{sn.RaySourceState.IsOn},{sn.DetectorState.IsConnected},{sn.DetectorState.FrameRate.ToString(inv)},{Esc(sn.DetectorState.Resolution)},,,,,,,",
SavePositionNode sp => $"{sp.Index},{sp.NodeType},{Esc(sp.Name)},{sp.MotionState.XM.ToString(inv)},{sp.MotionState.YM.ToString(inv)},{sp.MotionState.ZT.ToString(inv)},{sp.MotionState.ZD.ToString(inv)},{sp.MotionState.TiltD.ToString(inv)},{sp.MotionState.Dist.ToString(inv)},,,,,,,,,,,,,,", SavePositionNode sp => $"{sp.Index},{sp.NodeType},{Esc(sp.Name)},{sp.MotionState.SourceZ.ToString(inv)},{sp.MotionState.DetectorZ.ToString(inv)},{sp.MotionState.StageX.ToString(inv)},{sp.MotionState.StageY.ToString(inv)},{sp.MotionState.DetectorSwing.ToString(inv)},{sp.MotionState.StageRotation.ToString(inv)},{sp.MotionState.FixtureRotation.ToString(inv)},{sp.MotionState.FOD.ToString(inv)},{sp.MotionState.FDD.ToString(inv)},{sp.MotionState.Magnification.ToString(inv)},,,,,,,,,,,,,,",
InspectionModuleNode im => $"{im.Index},{im.NodeType},{Esc(im.Name)},,,,,,,,,,,,,,,,,,,{Esc(im.Pipeline?.Name ?? string.Empty)}", InspectionModuleNode im => $"{im.Index},{im.NodeType},{Esc(im.Name)},,,,,,,,,,,,,,,,,,,,,,{Esc(im.Pipeline?.Name ?? string.Empty)}",
InspectionMarkerNode mk => $"{mk.Index},{mk.NodeType},{Esc(mk.Name)},,,,,,,,,,,,,,{Esc(mk.MarkerType)},{mk.MarkerX.ToString(inv)},{mk.MarkerY.ToString(inv)},,,", InspectionMarkerNode mk => $"{mk.Index},{mk.NodeType},{Esc(mk.Name)},,,,,,,,,,,,,,,,,{Esc(mk.MarkerType)},{mk.MarkerX.ToString(inv)},{mk.MarkerY.ToString(inv)},,,",
PauseDialogNode pd => $"{pd.Index},{pd.NodeType},{Esc(pd.Name)},,,,,,,,,,,,,,,,,{Esc(pd.DialogTitle)},{Esc(pd.DialogMessage)},,", PauseDialogNode pd => $"{pd.Index},{pd.NodeType},{Esc(pd.Name)},,,,,,,,,,,,,,,,,,,{Esc(pd.DialogTitle)},{Esc(pd.DialogMessage)},,",
WaitDelayNode wd => $"{wd.Index},{wd.NodeType},{Esc(wd.Name)},,,,,,,,,,,,,,,,,,,{wd.DelayMilliseconds},", WaitDelayNode wd => $"{wd.Index},{wd.NodeType},{Esc(wd.Name)},,,,,,,,,,,,,,,,,,,,,{wd.DelayMilliseconds},",
CompleteProgramNode cp => $"{cp.Index},{cp.NodeType},{Esc(cp.Name)},,,,,,,,,,,,,,,,,,,,", CompleteProgramNode cp => $"{cp.Index},{cp.NodeType},{Esc(cp.Name)},,,,,,,,,,,,,,,,,,,,,,",
_ => $"{node.Index},{node.NodeType},{Esc(node.Name)},,,,,,,,,,,,,,,,,,,," _ => $"{node.Index},{node.NodeType},{Esc(node.Name)},,,,,,,,,,,,,,,,,,,,,,"
}; };
sb.AppendLine(row); sb.AppendLine(row);
@@ -551,6 +551,7 @@ namespace XplorePlane.ViewModels.Cnc
finally finally
{ {
IsRunning = false; IsRunning = false;
ResetAllNodeStates();
_cts?.Dispose(); _cts?.Dispose();
_cts = null; _cts = null;
} }
@@ -1,4 +1,4 @@
using Microsoft.Win32; using Microsoft.Win32;
using Prism.Commands; using Prism.Commands;
using Prism.Mvvm; using Prism.Mvvm;
using System; using System;
+128 -60
View File
@@ -116,82 +116,134 @@ namespace XplorePlane.ViewModels.Cnc
_ => string.Empty _ => string.Empty
}; };
public double XM public double StageX
{ {
get => _model switch get => _model switch
{ {
ReferencePointNode rp => rp.XM, ReferencePointNode rp => rp.StageX,
SaveNodeNode sn => sn.MotionState.XM, SaveNodeNode sn => sn.MotionState.StageX,
SaveNodeWithImageNode sni => sni.MotionState.XM, SaveNodeWithImageNode sni => sni.MotionState.StageX,
SavePositionNode sp => sp.MotionState.XM, SavePositionNode sp => sp.MotionState.StageX,
_ => 0d _ => 0d
}; };
set => UpdateMotion(value, MotionAxis.XM); set => UpdateMotion(value, MotionAxis.StageX);
} }
public double YM public double StageY
{ {
get => _model switch get => _model switch
{ {
ReferencePointNode rp => rp.YM, ReferencePointNode rp => rp.StageY,
SaveNodeNode sn => sn.MotionState.YM, SaveNodeNode sn => sn.MotionState.StageY,
SaveNodeWithImageNode sni => sni.MotionState.YM, SaveNodeWithImageNode sni => sni.MotionState.StageY,
SavePositionNode sp => sp.MotionState.YM, SavePositionNode sp => sp.MotionState.StageY,
_ => 0d _ => 0d
}; };
set => UpdateMotion(value, MotionAxis.YM); set => UpdateMotion(value, MotionAxis.StageY);
} }
public double ZT public double SourceZ
{ {
get => _model switch get => _model switch
{ {
ReferencePointNode rp => rp.ZT, ReferencePointNode rp => rp.SourceZ,
SaveNodeNode sn => sn.MotionState.ZT, SaveNodeNode sn => sn.MotionState.SourceZ,
SaveNodeWithImageNode sni => sni.MotionState.ZT, SaveNodeWithImageNode sni => sni.MotionState.SourceZ,
SavePositionNode sp => sp.MotionState.ZT, SavePositionNode sp => sp.MotionState.SourceZ,
_ => 0d _ => 0d
}; };
set => UpdateMotion(value, MotionAxis.ZT); set => UpdateMotion(value, MotionAxis.SourceZ);
} }
public double ZD public double DetectorZ
{ {
get => _model switch get => _model switch
{ {
ReferencePointNode rp => rp.ZD, ReferencePointNode rp => rp.DetectorZ,
SaveNodeNode sn => sn.MotionState.ZD, SaveNodeNode sn => sn.MotionState.DetectorZ,
SaveNodeWithImageNode sni => sni.MotionState.ZD, SaveNodeWithImageNode sni => sni.MotionState.DetectorZ,
SavePositionNode sp => sp.MotionState.ZD, SavePositionNode sp => sp.MotionState.DetectorZ,
_ => 0d _ => 0d
}; };
set => UpdateMotion(value, MotionAxis.ZD); set => UpdateMotion(value, MotionAxis.DetectorZ);
} }
public double TiltD public double DetectorSwing
{ {
get => _model switch get => _model switch
{ {
ReferencePointNode rp => rp.TiltD, ReferencePointNode rp => rp.DetectorSwing,
SaveNodeNode sn => sn.MotionState.TiltD, SaveNodeNode sn => sn.MotionState.DetectorSwing,
SaveNodeWithImageNode sni => sni.MotionState.TiltD, SaveNodeWithImageNode sni => sni.MotionState.DetectorSwing,
SavePositionNode sp => sp.MotionState.TiltD, SavePositionNode sp => sp.MotionState.DetectorSwing,
_ => 0d _ => 0d
}; };
set => UpdateMotion(value, MotionAxis.TiltD); set => UpdateMotion(value, MotionAxis.DetectorSwing);
} }
public double Dist public double StageRotation
{ {
get => _model switch get => _model switch
{ {
ReferencePointNode rp => rp.Dist, ReferencePointNode rp => rp.StageRotation,
SaveNodeNode sn => sn.MotionState.Dist, SaveNodeNode sn => sn.MotionState.StageRotation,
SaveNodeWithImageNode sni => sni.MotionState.Dist, SaveNodeWithImageNode sni => sni.MotionState.StageRotation,
SavePositionNode sp => sp.MotionState.Dist, SavePositionNode sp => sp.MotionState.StageRotation,
_ => 0d _ => 0d
}; };
set => UpdateMotion(value, MotionAxis.Dist); set => UpdateMotion(value, MotionAxis.StageRotation);
}
public double FixtureRotation
{
get => _model switch
{
ReferencePointNode rp => rp.FixtureRotation,
SaveNodeNode sn => sn.MotionState.FixtureRotation,
SaveNodeWithImageNode sni => sni.MotionState.FixtureRotation,
SavePositionNode sp => sp.MotionState.FixtureRotation,
_ => 0d
};
set => UpdateMotion(value, MotionAxis.FixtureRotation);
}
public double FOD
{
get => _model switch
{
ReferencePointNode rp => rp.FOD,
SaveNodeNode sn => sn.MotionState.FOD,
SaveNodeWithImageNode sni => sni.MotionState.FOD,
SavePositionNode sp => sp.MotionState.FOD,
_ => 0d
};
set => UpdateMotion(value, MotionAxis.FOD);
}
public double FDD
{
get => _model switch
{
ReferencePointNode rp => rp.FDD,
SaveNodeNode sn => sn.MotionState.FDD,
SaveNodeWithImageNode sni => sni.MotionState.FDD,
SavePositionNode sp => sp.MotionState.FDD,
_ => 0d
};
set => UpdateMotion(value, MotionAxis.FDD);
}
public double Magnification
{
get => _model switch
{
ReferencePointNode rp => rp.Magnification,
SaveNodeNode sn => sn.MotionState.Magnification,
SaveNodeWithImageNode sni => sni.MotionState.Magnification,
SavePositionNode sp => sp.MotionState.Magnification,
_ => 0d
};
set => UpdateMotion(value, MotionAxis.Magnification);
} }
public bool IsRayOn public bool IsRayOn
@@ -426,12 +478,16 @@ namespace XplorePlane.ViewModels.Cnc
case ReferencePointNode rp: case ReferencePointNode rp:
UpdateModel(axis switch UpdateModel(axis switch
{ {
MotionAxis.XM => rp with { XM = value }, MotionAxis.StageX => rp with { StageX = value },
MotionAxis.YM => rp with { YM = value }, MotionAxis.StageY => rp with { StageY = value },
MotionAxis.ZT => rp with { ZT = value }, MotionAxis.SourceZ => rp with { SourceZ = value },
MotionAxis.ZD => rp with { ZD = value }, MotionAxis.DetectorZ => rp with { DetectorZ = value },
MotionAxis.TiltD => rp with { TiltD = value }, MotionAxis.DetectorSwing => rp with { DetectorSwing = value },
MotionAxis.Dist => rp with { Dist = value }, MotionAxis.StageRotation => rp with { StageRotation = value },
MotionAxis.FixtureRotation => rp with { FixtureRotation = value },
MotionAxis.FOD => rp with { FOD = value },
MotionAxis.FDD => rp with { FDD = value },
MotionAxis.Magnification => rp with { Magnification = value },
_ => rp _ => rp
}); });
break; break;
@@ -519,12 +575,16 @@ namespace XplorePlane.ViewModels.Cnc
{ {
return axis switch return axis switch
{ {
MotionAxis.XM => state with { XM = value }, MotionAxis.StageX => state with { StageX = value },
MotionAxis.YM => state with { YM = value }, MotionAxis.StageY => state with { StageY = value },
MotionAxis.ZT => state with { ZT = value }, MotionAxis.SourceZ => state with { SourceZ = value },
MotionAxis.ZD => state with { ZD = value }, MotionAxis.DetectorZ => state with { DetectorZ = value },
MotionAxis.TiltD => state with { TiltD = value }, MotionAxis.DetectorSwing => state with { DetectorSwing = value },
MotionAxis.Dist => state with { Dist = value }, MotionAxis.StageRotation => state with { StageRotation = value },
MotionAxis.FixtureRotation => state with { FixtureRotation = value },
MotionAxis.FOD => state with { FOD = value },
MotionAxis.FDD => state with { FDD = value },
MotionAxis.Magnification => state with { Magnification = value },
_ => state _ => state
}; };
} }
@@ -557,12 +617,16 @@ namespace XplorePlane.ViewModels.Cnc
RaisePropertyChanged(nameof(IsPositionChild)); RaisePropertyChanged(nameof(IsPositionChild));
RaisePropertyChanged(nameof(IsMotionSnapshotNode)); RaisePropertyChanged(nameof(IsMotionSnapshotNode));
RaisePropertyChanged(nameof(RelationTag)); RaisePropertyChanged(nameof(RelationTag));
RaisePropertyChanged(nameof(XM)); RaisePropertyChanged(nameof(StageX));
RaisePropertyChanged(nameof(YM)); RaisePropertyChanged(nameof(StageY));
RaisePropertyChanged(nameof(ZT)); RaisePropertyChanged(nameof(SourceZ));
RaisePropertyChanged(nameof(ZD)); RaisePropertyChanged(nameof(DetectorZ));
RaisePropertyChanged(nameof(TiltD)); RaisePropertyChanged(nameof(DetectorSwing));
RaisePropertyChanged(nameof(Dist)); RaisePropertyChanged(nameof(StageRotation));
RaisePropertyChanged(nameof(FixtureRotation));
RaisePropertyChanged(nameof(FOD));
RaisePropertyChanged(nameof(FDD));
RaisePropertyChanged(nameof(Magnification));
RaisePropertyChanged(nameof(IsRayOn)); RaisePropertyChanged(nameof(IsRayOn));
RaisePropertyChanged(nameof(Voltage)); RaisePropertyChanged(nameof(Voltage));
RaisePropertyChanged(nameof(Current)); RaisePropertyChanged(nameof(Current));
@@ -591,12 +655,16 @@ namespace XplorePlane.ViewModels.Cnc
private enum MotionAxis private enum MotionAxis
{ {
XM, StageX,
YM, StageY,
ZT, SourceZ,
ZD, DetectorZ,
TiltD, DetectorSwing,
Dist StageRotation,
FixtureRotation,
FOD,
FDD,
Magnification
} }
} }
} }
@@ -1,4 +1,4 @@
using Microsoft.Win32; using Microsoft.Win32;
using Prism.Events; using Prism.Events;
using Prism.Commands; using Prism.Commands;
using Prism.Mvvm; using Prism.Mvvm;
+1 -1
View File
@@ -1,4 +1,4 @@
using Microsoft.Win32; using Microsoft.Win32;
using Prism.Commands; using Prism.Commands;
using Prism.Events; using Prism.Events;
using Prism.Ioc; using Prism.Ioc;
+1 -1
View File
@@ -1,4 +1,4 @@
<Window <Window
x:Class="XplorePlane.Views.Cnc.CncEditorWindow" x:Class="XplorePlane.Views.Cnc.CncEditorWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+28 -12
View File
@@ -489,28 +489,44 @@
Visibility="{Binding SelectedNode.IsMotionSnapshotNode, Converter={StaticResource BoolToVisibilityConverter}}"> Visibility="{Binding SelectedNode.IsMotionSnapshotNode, Converter={StaticResource BoolToVisibilityConverter}}">
<UniformGrid Margin="8,8,8,6" Columns="2"> <UniformGrid Margin="8,8,8,6" Columns="2">
<StackPanel Margin="0,0,6,0"> <StackPanel Margin="0,0,6,0">
<TextBlock Style="{StaticResource LabelStyle}" Text="XM" /> <TextBlock Style="{StaticResource LabelStyle}" Text="载物台 X (μm)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.XM, UpdateSourceTrigger=LostFocus}" /> <TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.StageX, UpdateSourceTrigger=LostFocus}" />
</StackPanel> </StackPanel>
<StackPanel> <StackPanel>
<TextBlock Style="{StaticResource LabelStyle}" Text="YM" /> <TextBlock Style="{StaticResource LabelStyle}" Text="载物台 Y (μm)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.YM, UpdateSourceTrigger=LostFocus}" /> <TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.StageY, UpdateSourceTrigger=LostFocus}" />
</StackPanel> </StackPanel>
<StackPanel Margin="0,0,6,0"> <StackPanel Margin="0,0,6,0">
<TextBlock Style="{StaticResource LabelStyle}" Text="ZT" /> <TextBlock Style="{StaticResource LabelStyle}" Text="射线源 Z (μm)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.ZT, UpdateSourceTrigger=LostFocus}" /> <TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.SourceZ, UpdateSourceTrigger=LostFocus}" />
</StackPanel> </StackPanel>
<StackPanel> <StackPanel>
<TextBlock Style="{StaticResource LabelStyle}" Text="ZD" /> <TextBlock Style="{StaticResource LabelStyle}" Text="探测器 Z (μm)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.ZD, UpdateSourceTrigger=LostFocus}" /> <TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.DetectorZ, UpdateSourceTrigger=LostFocus}" />
</StackPanel> </StackPanel>
<StackPanel Margin="0,0,6,0"> <StackPanel Margin="0,0,6,0">
<TextBlock Style="{StaticResource LabelStyle}" Text="TiltD" /> <TextBlock Style="{StaticResource LabelStyle}" Text="探测器摆动 (°)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.TiltD, UpdateSourceTrigger=LostFocus}" /> <TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.DetectorSwing, UpdateSourceTrigger=LostFocus}" />
</StackPanel> </StackPanel>
<StackPanel> <StackPanel>
<TextBlock Style="{StaticResource LabelStyle}" Text="Dist" /> <TextBlock Style="{StaticResource LabelStyle}" Text="载物台旋转 (°)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Dist, UpdateSourceTrigger=LostFocus}" /> <TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.StageRotation, UpdateSourceTrigger=LostFocus}" />
</StackPanel>
<StackPanel Margin="0,0,6,0">
<TextBlock Style="{StaticResource LabelStyle}" Text="夹具旋转 (°)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.FixtureRotation, UpdateSourceTrigger=LostFocus}" />
</StackPanel>
<StackPanel>
<TextBlock Style="{StaticResource LabelStyle}" Text="FOD (μm)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.FOD, UpdateSourceTrigger=LostFocus}" />
</StackPanel>
<StackPanel Margin="0,0,6,0">
<TextBlock Style="{StaticResource LabelStyle}" Text="FDD (μm)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.FDD, UpdateSourceTrigger=LostFocus}" />
</StackPanel>
<StackPanel>
<TextBlock Style="{StaticResource LabelStyle}" Text="放大倍率" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Magnification, UpdateSourceTrigger=LostFocus}" />
</StackPanel> </StackPanel>
</UniformGrid> </UniformGrid>
</GroupBox> </GroupBox>
@@ -53,7 +53,7 @@
Margin="0,0,4,3" Margin="0,0,4,3"
VerticalAlignment="Center" VerticalAlignment="Center"
Foreground="#333333" Foreground="#333333"
Text="XM" /> Text="StageX" />
<TextBox <TextBox
Grid.Row="0" Grid.Row="0"
Grid.Column="1" Grid.Column="1"
@@ -94,7 +94,7 @@
Margin="0,0,4,3" Margin="0,0,4,3"
VerticalAlignment="Center" VerticalAlignment="Center"
Foreground="#333333" Foreground="#333333"
Text="YM" /> Text="StageY" />
<TextBox <TextBox
Grid.Row="1" Grid.Row="1"
Grid.Column="1" Grid.Column="1"
@@ -127,7 +127,7 @@
Margin="0,0,4,3" Margin="0,0,4,3"
VerticalAlignment="Center" VerticalAlignment="Center"
Foreground="#333333" Foreground="#333333"
Text="ZT" /> Text="SourceZ" />
<TextBox <TextBox
Grid.Row="2" Grid.Row="2"
Grid.Column="1" Grid.Column="1"
@@ -151,7 +151,7 @@
Margin="0,0,4,3" Margin="0,0,4,3"
VerticalAlignment="Center" VerticalAlignment="Center"
Foreground="#333333" Foreground="#333333"
Text="ZD" /> Text="DetectorZ" />
<TextBox <TextBox
Grid.Row="3" Grid.Row="3"
Grid.Column="1" Grid.Column="1"
@@ -175,7 +175,7 @@
Margin="0,0,4,3" Margin="0,0,4,3"
VerticalAlignment="Center" VerticalAlignment="Center"
Foreground="#333333" Foreground="#333333"
Text="TiltD" /> Text="DetectorSwing" />
<TextBox <TextBox
Grid.Row="4" Grid.Row="4"
Grid.Column="1" Grid.Column="1"
@@ -199,7 +199,7 @@
Margin="0,0,4,0" Margin="0,0,4,0"
VerticalAlignment="Center" VerticalAlignment="Center"
Foreground="#333333" Foreground="#333333"
Text="Dist" /> Text="FDD" />
<TextBox <TextBox
Grid.Row="5" Grid.Row="5"
Grid.Column="1" Grid.Column="1"
@@ -1,4 +1,4 @@
<UserControl <UserControl
x:Class="XplorePlane.Views.PipelineEditorView" x:Class="XplorePlane.Views.PipelineEditorView"
x:Name="RootControl" x:Name="RootControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+1 -1
View File
@@ -1,4 +1,4 @@
<UserControl <UserControl
x:Class="XplorePlane.Views.ImagePanelView" x:Class="XplorePlane.Views.ImagePanelView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+1 -1
View File
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework> <TargetFramework>net8.0-windows</TargetFramework>
+1 -1
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup /> <PropertyGroup />
</Project> </Project>