30 Commits

Author SHA1 Message Date
ZHANG Zhengxuan 436eaa54fa 已合并 PR 60: 修复算子工具箱及文件夹组织
1、修复编译问题,CNC运行完,实时图像为最后一个节点的输出
2、移除XP单元测试
3、算子工具箱改为Tab窗体
4、流程图 新增执行到此,和执行全部
5、调整Rabbion按钮为CNC页面内置
6、调整算子参数区为滑块控件
7、新增设置窗体,统一数据存储目录
8、增加下拉图像配方列表
2026-05-07 09:38:47 +08:00
zhengxuan.zhang a4e257e8ce 对主界面rabbion按钮的 开关门 绑定到硬件库层真实的操作 2026-05-07 00:12:21 +08:00
zhengxuan.zhang 2124d0c0b7 将XplorePlane项目中所有中文弹窗改为英文弹窗 2026-05-07 00:03:09 +08:00
zhengxuan.zhang 4a4e45e479 打通射线源与相机的断链问题 2026-05-06 23:55:28 +08:00
zhengxuan.zhang d079e9357b 更新硬件集成文档 2026-05-06 23:26:55 +08:00
zhengxuan.zhang d56caf1ab5 双队列的打通 实时按钮的切换 补充测试用例 2026-05-06 23:25:37 +08:00
zhengxuan.zhang bd9b24beb1 探测器双队列的打通与实时按钮的切换 2026-05-06 23:18:28 +08:00
zhengxuan.zhang f9be56b99f Merge remote-tracking branch 'origin/turbo-002-cnc' into turbo-002-cnc 2026-05-06 20:49:44 +08:00
zhengxuan.zhang 03a8532049 修改说明 2026-05-06 20:46:33 +08:00
zhengxuan.zhang e3a1184805 主视口没有可用图像时,回退到 IAppStateService.LatestDetectorFrame 2026-05-06 20:31:07 +08:00
zhengxuan.zhang b740f8d453 修复探测器的订阅与获取 2026-05-06 18:20:52 +08:00
zhengxuan.zhang 5852e11b9f CNC 运行日志、导出字段顺序、以及参考点/保存点同步适配 2026-05-06 17:43:29 +08:00
zhengxuan.zhang 996b0c5796 修复测试用例AfterExecution_IsRunningFalse_AllNodesIdle 2026-05-06 17:27:18 +08:00
zhengxuan.zhang 3a3ea5b5c9 修复乱码 2026-05-06 17:23:51 +08:00
zhengxuan.zhang aeef1feee3 以硬件库层面运动硬件轴定义为准,同步修改appstate, 包括CNC 节点属性面板和 XP导出 2026-05-06 17:11:35 +08:00
zhengxuan.zhang 1ef876db2c 调整硬件appstate 保持与硬件库层面定义一致 2026-05-06 16:48:09 +08:00
zhengxuan.zhang 1b686066c8 Merge branch 'Develop/XP' into turbo-002-cnc
# Conflicts:
#	XplorePlane/Views/Main/MainWindow.xaml
2026-05-06 15:37:58 +08:00
zhengxuan.zhang 7c0f9dab73 下拉检测模块列表 2026-05-06 15:28:29 +08:00
zhengxuan.zhang 3bee2898c5 Plan 用于 CNC 默认保存和加载,Tools 用于流程图配方 xpm,Data 用于执行结果和中间图像,Report 为报告预留目录 2026-05-06 14:56:07 +08:00
zhengxuan.zhang 9a8831c945 新增设置窗体 2026-05-06 14:13:19 +08:00
zhengxuan.zhang db8a37410f 将流程图按钮改为图标 2026-05-06 13:32:21 +08:00
zhengxuan.zhang a483144d29 调整流水线算子参数区 滑块控件的大小 2026-05-06 13:25:18 +08:00
zhengxuan.zhang 2eb3fed4d0 1、当前根节点选择中是 高亮背景和文字不协调,文字改为黑色
2、延时节点执行时,进度条显示功能失效,修复
   3、自动执行,当加载CNC后,点击rabbion中运行,此时没有看到执行中,已执行,等高亮规则
2026-05-06 11:46:49 +08:00
zhengxuan.zhang 48c419d777 调整CNC编辑页面按钮样式 2026-05-06 11:22:21 +08:00
zhengxuan.zhang d4050b4218 调整CNC按钮逻辑改为内置 2026-05-06 11:12:05 +08:00
zhengxuan.zhang f5dceeceb7 对 流程图 新增 4个状态 已启用 已停用 执行到此 未参与本次执行 和右键菜单,执行到此,执行全部 2026-05-06 10:39:19 +08:00
zhengxuan.zhang fd4048a6c0 修复算子工具箱搜索功能 2026-05-06 10:01:27 +08:00
zhengxuan.zhang fdf2419eb6 移除XP单元测试 2026-04-30 14:12:22 +08:00
zhengxuan.zhang 8a6abfb28b 简化算子工具箱调用入口 2026-04-30 14:08:04 +08:00
zhengxuan.zhang fd9784ecb6 #调整页面布局,新增操作 Tab; 图像算子改为Tab页选择; 2026-04-30 13:56:35 +08:00
69 changed files with 4273 additions and 12212 deletions
+1
View File
@@ -63,3 +63,4 @@ ExternalLibraries/Models/
XplorePlane/Tests/ XplorePlane/Tests/
ExternalLibraries/Telerik/ ExternalLibraries/Telerik/
build_out.txt build_out.txt
XplorePlane/data/
@@ -1705,10 +1705,7 @@
"Telerik.UI.for.Wpf.NetCore.Xaml": "2024.1.408" "Telerik.UI.for.Wpf.NetCore.Xaml": "2024.1.408"
}, },
"runtime": { "runtime": {
"XP.Common.dll": { "XP.Common.dll": {}
"assemblyVersion": "1.4.16.1",
"fileVersion": "1.4.16.1"
}
}, },
"resources": { "resources": {
"en-US/XP.Common.resources.dll": { "en-US/XP.Common.resources.dll": {
File diff suppressed because it is too large Load Diff
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-13.0.0.0" newVersion="13.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
@@ -16,8 +16,8 @@
Background="Transparent" Background="Transparent"
BorderBrush="#FFD5DFE5" BorderBrush="#FFD5DFE5"
BorderThickness="1" BorderThickness="1"
Padding="10" Padding="8"
Margin="0,0,0,10"> Margin="0,0,0,8">
<StackPanel> <StackPanel>
<TextBlock x:Name="txtProcessorName" <TextBlock x:Name="txtProcessorName"
FontSize="14" FontSize="14"
@@ -32,7 +32,7 @@
<!-- 参数列表 --> <!-- 参数列表 -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto"> <ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<StackPanel x:Name="pnlParameters" Margin="5" /> <StackPanel x:Name="pnlParameters" Margin="2" />
</ScrollViewer> </ScrollViewer>
</Grid> </Grid>
</UserControl> </UserControl>
@@ -143,12 +143,15 @@ public partial class ProcessorParameterControl : UserControl
var textBox = new TextBox var textBox = new TextBox
{ {
Text = param.Value.ToString(), Text = param.Value.ToString(),
Width = 100, Width = 56,
HorizontalAlignment = HorizontalAlignment.Left MinWidth = 56,
HorizontalContentAlignment = HorizontalAlignment.Center,
VerticalContentAlignment = VerticalAlignment.Center
}; };
if (param.MinValue != null && param.MaxValue != null) if (param.MinValue != null && param.MaxValue != null)
{ {
var rangeGrid = CreateRangeEditorContainer();
var slider = new Slider var slider = new Slider
{ {
Minimum = Convert.ToDouble(param.MinValue), Minimum = Convert.ToDouble(param.MinValue),
@@ -156,7 +159,8 @@ public partial class ProcessorParameterControl : UserControl
Value = Convert.ToDouble(param.Value), Value = Convert.ToDouble(param.Value),
TickFrequency = 1, TickFrequency = 1,
IsSnapToTickEnabled = true, IsSnapToTickEnabled = true,
Margin = new Thickness(0, 0, 0, 5) MinWidth = 120,
VerticalAlignment = VerticalAlignment.Center
}; };
slider.ValueChanged += (s, e) => slider.ValueChanged += (s, e) =>
@@ -181,7 +185,11 @@ public partial class ProcessorParameterControl : UserControl
} }
}; };
panel.Children.Add(slider); Grid.SetColumn(slider, 0);
Grid.SetColumn(textBox, 2);
rangeGrid.Children.Add(slider);
rangeGrid.Children.Add(textBox);
panel.Children.Add(rangeGrid);
} }
else else
{ {
@@ -193,9 +201,9 @@ public partial class ProcessorParameterControl : UserControl
OnParameterChanged(); OnParameterChanged();
} }
}; };
}
panel.Children.Add(textBox); panel.Children.Add(textBox);
}
return panel; return panel;
} }
@@ -211,19 +219,23 @@ public partial class ProcessorParameterControl : UserControl
var textBox = new TextBox var textBox = new TextBox
{ {
Text = Convert.ToDouble(param.Value).ToString("F2"), Text = Convert.ToDouble(param.Value).ToString("F2"),
Width = 100, Width = 56,
HorizontalAlignment = HorizontalAlignment.Left MinWidth = 56,
HorizontalContentAlignment = HorizontalAlignment.Center,
VerticalContentAlignment = VerticalAlignment.Center
}; };
if (param.MinValue != null && param.MaxValue != null) if (param.MinValue != null && param.MaxValue != null)
{ {
var rangeGrid = CreateRangeEditorContainer();
var slider = new Slider var slider = new Slider
{ {
Minimum = Convert.ToDouble(param.MinValue), Minimum = Convert.ToDouble(param.MinValue),
Maximum = Convert.ToDouble(param.MaxValue), Maximum = Convert.ToDouble(param.MaxValue),
Value = Convert.ToDouble(param.Value), Value = Convert.ToDouble(param.Value),
TickFrequency = 0.1, TickFrequency = 0.1,
Margin = new Thickness(0, 0, 0, 5) MinWidth = 120,
VerticalAlignment = VerticalAlignment.Center
}; };
slider.ValueChanged += (s, e) => slider.ValueChanged += (s, e) =>
@@ -248,7 +260,11 @@ public partial class ProcessorParameterControl : UserControl
} }
}; };
panel.Children.Add(slider); Grid.SetColumn(slider, 0);
Grid.SetColumn(textBox, 2);
rangeGrid.Children.Add(slider);
rangeGrid.Children.Add(textBox);
panel.Children.Add(rangeGrid);
} }
else else
{ {
@@ -260,9 +276,9 @@ public partial class ProcessorParameterControl : UserControl
OnParameterChanged(); OnParameterChanged();
} }
}; };
}
panel.Children.Add(textBox); panel.Children.Add(textBox);
}
return panel; return panel;
} }
@@ -302,8 +318,8 @@ public partial class ProcessorParameterControl : UserControl
var comboBox = new ComboBox var comboBox = new ComboBox
{ {
Margin = new Thickness(0, 5, 0, 0), Margin = new Thickness(0, 5, 0, 0),
Width = 200, MinWidth = 160,
HorizontalAlignment = HorizontalAlignment.Left HorizontalAlignment = HorizontalAlignment.Stretch
}; };
if (param.Options != null) if (param.Options != null)
@@ -344,8 +360,8 @@ public partial class ProcessorParameterControl : UserControl
{ {
Text = param.Value?.ToString() ?? "", Text = param.Value?.ToString() ?? "",
Margin = new Thickness(0, 5, 0, 0), Margin = new Thickness(0, 5, 0, 0),
Width = 200, MinWidth = 160,
HorizontalAlignment = HorizontalAlignment.Left HorizontalAlignment = HorizontalAlignment.Stretch
}; };
textBox.TextChanged += (s, e) => textBox.TextChanged += (s, e) =>
@@ -374,4 +390,16 @@ public partial class ProcessorParameterControl : UserControl
pnlParameters.Children.Clear(); pnlParameters.Children.Clear();
UpdateNoProcessorText(); UpdateNoProcessorText();
} }
private static Grid CreateRangeEditorContainer()
{
var grid = new Grid
{
Margin = new Thickness(0, 0, 0, 4)
};
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(6) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
return grid;
}
} }
+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 ─────────────────────────
@@ -6,6 +6,7 @@ using System.Windows.Media.Imaging;
using XP.Common.Logging.Interfaces; using XP.Common.Logging.Interfaces;
using XplorePlane.Models; using XplorePlane.Models;
using XplorePlane.Services; using XplorePlane.Services;
using XplorePlane.Services.Storage;
using XplorePlane.Tests.Helpers; using XplorePlane.Tests.Helpers;
using XplorePlane.ViewModels; using XplorePlane.ViewModels;
using Xunit; using Xunit;
@@ -22,6 +23,7 @@ namespace XplorePlane.Tests.Pipeline
private readonly Mock<IPipelineExecutionService> _mockExecSvc; private readonly Mock<IPipelineExecutionService> _mockExecSvc;
private readonly Mock<IPipelinePersistenceService> _mockPersistSvc; private readonly Mock<IPipelinePersistenceService> _mockPersistSvc;
private readonly Mock<ILoggerService> _mockLogger; private readonly Mock<ILoggerService> _mockLogger;
private readonly Mock<IXpDataPathService> _mockDataPathService;
public PipelineEditorViewModelTests() public PipelineEditorViewModelTests()
{ {
@@ -29,11 +31,19 @@ namespace XplorePlane.Tests.Pipeline
_mockExecSvc = new Mock<IPipelineExecutionService>(); _mockExecSvc = new Mock<IPipelineExecutionService>();
_mockPersistSvc = new Mock<IPipelinePersistenceService>(); _mockPersistSvc = new Mock<IPipelinePersistenceService>();
_mockLogger = new Mock<ILoggerService>(); _mockLogger = new Mock<ILoggerService>();
_mockDataPathService = new Mock<IXpDataPathService>();
_mockLogger.Setup(l => l.ForModule<PipelineEditorViewModel>()).Returns(_mockLogger.Object); _mockLogger.Setup(l => l.ForModule<PipelineEditorViewModel>()).Returns(_mockLogger.Object);
_mockDataPathService.SetupGet(s => s.ToolsPath).Returns(Path.GetTempPath());
} }
private PipelineEditorViewModel CreateVm() => private PipelineEditorViewModel CreateVm() =>
new PipelineEditorViewModel(_mockImageSvc.Object, _mockExecSvc.Object, _mockPersistSvc.Object, new EventAggregator(), _mockLogger.Object); new PipelineEditorViewModel(
_mockImageSvc.Object,
_mockExecSvc.Object,
_mockPersistSvc.Object,
new EventAggregator(),
_mockLogger.Object,
_mockDataPathService.Object);
// ── 6.1 AddOperatorCommand ──────────────────────────────────── // ── 6.1 AddOperatorCommand ────────────────────────────────────
@@ -1,8 +1,10 @@
using System; using System;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Moq;
using XplorePlane.Models; using XplorePlane.Models;
using XplorePlane.Services; using XplorePlane.Services;
using XplorePlane.Services.Storage;
using XplorePlane.Tests.Helpers; using XplorePlane.Tests.Helpers;
using Xunit; using Xunit;
@@ -199,5 +201,21 @@ namespace XplorePlane.Tests.Pipeline
Assert.Equal(2, result.Count); Assert.Equal(2, result.Count);
} }
[Fact]
public async Task LoadAllAsync_UsesToolsPath_WhenConstructedWithDataPathService()
{
var mockImageSvc = TestHelpers.CreateMockImageService(new[] { "Blur" });
var mockDataPathSvc = new Mock<IXpDataPathService>();
mockDataPathSvc.SetupGet(s => s.ToolsPath).Returns(_tempDir);
var service = new PipelinePersistenceService(mockImageSvc.Object, mockDataPathSvc.Object);
await service.SaveAsync(BuildModel("P3", "Blur"), Path.Combine(_tempDir, "p3.xpm"));
var result = await service.LoadAllAsync(null);
Assert.Single(result);
Assert.Equal("P3", result[0].Name);
}
} }
} }
@@ -12,6 +12,7 @@ using System.Threading.Tasks;
using XP.Common.Logging.Interfaces; using XP.Common.Logging.Interfaces;
using XplorePlane.Models; using XplorePlane.Models;
using XplorePlane.Services; using XplorePlane.Services;
using XplorePlane.Services.Storage;
using XplorePlane.Tests.Helpers; using XplorePlane.Tests.Helpers;
using XplorePlane.ViewModels; using XplorePlane.ViewModels;
@@ -31,8 +32,16 @@ namespace XplorePlane.Tests.Pipeline
var mockExecSvc = new Mock<IPipelineExecutionService>(); var mockExecSvc = new Mock<IPipelineExecutionService>();
var mockPersistSvc = new Mock<IPipelinePersistenceService>(); var mockPersistSvc = new Mock<IPipelinePersistenceService>();
var mockLogger = new Mock<ILoggerService>(); var mockLogger = new Mock<ILoggerService>();
var mockDataPathService = new Mock<IXpDataPathService>();
mockLogger.Setup(l => l.ForModule<PipelineEditorViewModel>()).Returns(mockLogger.Object); mockLogger.Setup(l => l.ForModule<PipelineEditorViewModel>()).Returns(mockLogger.Object);
return new PipelineEditorViewModel(mockImageSvc.Object, mockExecSvc.Object, mockPersistSvc.Object, new EventAggregator(), mockLogger.Object); mockDataPathService.SetupGet(s => s.ToolsPath).Returns(Path.GetTempPath());
return new PipelineEditorViewModel(
mockImageSvc.Object,
mockExecSvc.Object,
mockPersistSvc.Object,
new EventAggregator(),
mockLogger.Object,
mockDataPathService.Object);
} }
/// <summary> /// <summary>
@@ -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;
}
} }
} }
@@ -3,14 +3,22 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using FsCheck; using FsCheck;
using FsCheck.Fluent; using FsCheck.Fluent;
using FsCheck.Xunit; using FsCheck.Xunit;
using Moq; using Moq;
using Prism.Events;
using XP.Common.Logging.Interfaces; using XP.Common.Logging.Interfaces;
using XP.Hardware.Detector.Abstractions;
using XplorePlane.Events;
using XplorePlane.Models; using XplorePlane.Models;
using XplorePlane.Services.Cnc; using XplorePlane.Services.Cnc;
using XplorePlane.Services;
using XplorePlane.Services.AppState;
using XplorePlane.Services.InspectionResults; using XplorePlane.Services.InspectionResults;
using XplorePlane.Services.MainViewport;
using Xunit; using Xunit;
namespace XplorePlane.Tests.Services namespace XplorePlane.Tests.Services
@@ -170,13 +178,23 @@ internal sealed class SynchronousProgress<T> : IProgress<T>
public class CncExecutionServiceTests public class CncExecutionServiceTests
{ {
private static (CncExecutionService Service, Mock<IInspectionResultStore> Store, Mock<ILoggerService> Logger) private static (CncExecutionService Service, Mock<IInspectionResultStore> Store, Mock<ILoggerService> Logger, Mock<IMainViewportService> MainViewport, Mock<IAppStateService> AppState)
CreateService() CreateService()
{ {
var mockStore = new Mock<IInspectionResultStore>(); var mockStore = new Mock<IInspectionResultStore>();
var mockLogger = new Mock<ILoggerService>(); var mockLogger = new Mock<ILoggerService>();
var mockMainViewportService = new Mock<IMainViewportService>();
var mockAppStateService = new Mock<IAppStateService>();
var mockPipelineExecutionService = new Mock<IPipelineExecutionService>();
var mockImageProcessingService = new Mock<IImageProcessingService>();
var mockEventAggregator = new Mock<IEventAggregator>();
mockLogger.Setup(l => l.ForModule<CncExecutionService>()).Returns(mockLogger.Object); mockLogger.Setup(l => l.ForModule<CncExecutionService>()).Returns(mockLogger.Object);
// Set up GetEvent<DetectorDisconnectedEvent>() so the constructor subscription doesn't throw
mockEventAggregator
.Setup(ea => ea.GetEvent<DetectorDisconnectedEvent>())
.Returns(new DetectorDisconnectedEvent());
mockStore.Setup(s => s.BeginRunAsync( mockStore.Setup(s => s.BeginRunAsync(
It.IsAny<InspectionRunRecord>(), It.IsAny<InspectionRunRecord>(),
It.IsAny<InspectionAssetWriteRequest>())) It.IsAny<InspectionAssetWriteRequest>()))
@@ -195,8 +213,15 @@ internal sealed class SynchronousProgress<T> : IProgress<T>
It.IsAny<DateTime?>())) It.IsAny<DateTime?>()))
.Returns(Task.CompletedTask); .Returns(Task.CompletedTask);
var service = new CncExecutionService(mockStore.Object, mockLogger.Object); var service = new CncExecutionService(
return (service, mockStore, mockLogger); mockStore.Object,
mockLogger.Object,
mockMainViewportService.Object,
mockAppStateService.Object,
mockPipelineExecutionService.Object,
mockImageProcessingService.Object,
mockEventAggregator.Object);
return (service, mockStore, mockLogger, mockMainViewportService, mockAppStateService);
} }
// ── Property 3: 预取消立即返回 ──────────────────────────────────────── // ── Property 3: 预取消立即返回 ────────────────────────────────────────
@@ -210,7 +235,7 @@ internal sealed class SynchronousProgress<T> : IProgress<T>
CncProgramGenerators.CncProgramArb(1, 10), CncProgramGenerators.CncProgramArb(1, 10),
program => program =>
{ {
var (service, mockStore, _) = CreateService(); var (service, mockStore, _, _, _) = CreateService();
using var cts = new CancellationTokenSource(); using var cts = new CancellationTokenSource();
cts.Cancel(); cts.Cancel();
@@ -237,7 +262,7 @@ internal sealed class SynchronousProgress<T> : IProgress<T>
CncProgramGenerators.CncProgramArb(2, 8), CncProgramGenerators.CncProgramArb(2, 8),
program => program =>
{ {
var (service, _, _) = CreateService(); var (service, _, _, _, _) = CreateService();
var runningReports = new List<Guid>(); var runningReports = new List<Guid>();
// Use SynchronousProgress to avoid async callback timing issues // Use SynchronousProgress to avoid async callback timing issues
@@ -294,7 +319,7 @@ internal sealed class SynchronousProgress<T> : IProgress<T>
gen.ToArbitrary(), gen.ToArbitrary(),
program => program =>
{ {
var (service, _, _) = CreateService(); var (service, _, _, _, _) = CreateService();
var runningIds = new List<Guid>(); var runningIds = new List<Guid>();
var progress = new SynchronousProgress<CncNodeExecutionProgress>(p => var progress = new SynchronousProgress<CncNodeExecutionProgress>(p =>
@@ -345,7 +370,7 @@ internal sealed class SynchronousProgress<T> : IProgress<T>
CncProgramGenerators.CncProgramArb(1, 8), CncProgramGenerators.CncProgramArb(1, 8),
program => program =>
{ {
var (service, mockStore, _) = CreateService(); var (service, mockStore, _, _, _) = CreateService();
InspectionRunRecord capturedRecord = null; InspectionRunRecord capturedRecord = null;
mockStore.Setup(s => s.BeginRunAsync( mockStore.Setup(s => s.BeginRunAsync(
@@ -376,7 +401,7 @@ internal sealed class SynchronousProgress<T> : IProgress<T>
CncProgramGenerators.CncProgramArb(1, 10), CncProgramGenerators.CncProgramArb(1, 10),
program => program =>
{ {
var (service, mockStore, _) = CreateService(); var (service, mockStore, _, _, _) = CreateService();
service.ExecuteAsync(program, null, CancellationToken.None) service.ExecuteAsync(program, null, CancellationToken.None)
.GetAwaiter().GetResult(); .GetAwaiter().GetResult();
@@ -411,7 +436,7 @@ internal sealed class SynchronousProgress<T> : IProgress<T>
CncProgramGenerators.CncProgramArb(1, 10), CncProgramGenerators.CncProgramArb(1, 10),
program => program =>
{ {
var (service, mockStore, _) = CreateService(); var (service, mockStore, _, _, _) = CreateService();
InspectionRunRecord capturedRecord = null; InspectionRunRecord capturedRecord = null;
mockStore.Setup(s => s.BeginRunAsync( mockStore.Setup(s => s.BeginRunAsync(
@@ -439,7 +464,7 @@ internal sealed class SynchronousProgress<T> : IProgress<T>
CncProgramGenerators.CncProgramArb(1, 8), CncProgramGenerators.CncProgramArb(1, 8),
program => program =>
{ {
var (service, mockStore, _) = CreateService(); var (service, mockStore, _, _, _) = CreateService();
bool? capturedOverallPass = default; bool? capturedOverallPass = default;
bool callbackInvoked = false; bool callbackInvoked = false;
@@ -472,7 +497,7 @@ internal sealed class SynchronousProgress<T> : IProgress<T>
// Validates: Requirements 4.4, 4.5 // Validates: Requirements 4.4, 4.5
public void CompleteRunAsync_CalledWithNullOverallPass_WhenCancelled() public void CompleteRunAsync_CalledWithNullOverallPass_WhenCancelled()
{ {
var (service, mockStore, _) = CreateService(); var (service, mockStore, _, _, _) = CreateService();
// Use a WaitDelayNode with long delay so cancellation happens during execution // Use a WaitDelayNode with long delay so cancellation happens during execution
var waitNode = new WaitDelayNode(Guid.NewGuid(), 0, "LongWait", 5000); var waitNode = new WaitDelayNode(Guid.NewGuid(), 0, "LongWait", 5000);
@@ -520,7 +545,7 @@ internal sealed class SynchronousProgress<T> : IProgress<T>
CncProgramGenerators.CncProgramWithInspectionNodesArb(2), CncProgramGenerators.CncProgramWithInspectionNodesArb(2),
program => program =>
{ {
var (service, mockStore, _) = CreateService(); var (service, mockStore, _, _, _) = CreateService();
// Make AppendNodeResultAsync always throw // Make AppendNodeResultAsync always throw
mockStore.Setup(s => s.AppendNodeResultAsync( mockStore.Setup(s => s.AppendNodeResultAsync(
@@ -577,7 +602,7 @@ internal sealed class SynchronousProgress<T> : IProgress<T>
gen.ToArbitrary(), gen.ToArbitrary(),
waitNode => waitNode =>
{ {
var (service, mockStore, _) = CreateService(); var (service, mockStore, _, _, _) = CreateService();
bool? capturedOverallPass = default; bool? capturedOverallPass = default;
mockStore.Setup(s => s.CompleteRunAsync( mockStore.Setup(s => s.CompleteRunAsync(
@@ -620,7 +645,7 @@ internal sealed class SynchronousProgress<T> : IProgress<T>
CncProgramGenerators.CncProgramArb(1, 8), CncProgramGenerators.CncProgramArb(1, 8),
program => program =>
{ {
var (service, _, _) = CreateService(); var (service, _, _, _, _) = CreateService();
// Build a map of NodeId → CncNodeViewModel // Build a map of NodeId → CncNodeViewModel
var nodeVms = program.Nodes var nodeVms = program.Nodes
@@ -690,7 +715,7 @@ internal sealed class SynchronousProgress<T> : IProgress<T>
tuple => tuple =>
{ {
var (program, node, expectedPipelineName) = tuple; var (program, node, expectedPipelineName) = tuple;
var (service, mockStore, _) = CreateService(); var (service, mockStore, _, _, _) = CreateService();
PipelineExecutionSnapshot capturedSnapshot = null; PipelineExecutionSnapshot capturedSnapshot = null;
mockStore.Setup(s => s.AppendNodeResultAsync( mockStore.Setup(s => s.AppendNodeResultAsync(
@@ -0,0 +1,101 @@
using FsCheck;
using FsCheck.Fluent;
using FsCheck.Xunit;
using Moq;
using Prism.Events;
using XP.Common.Logging.Interfaces;
using XplorePlane.Services.MainViewport;
namespace XplorePlane.Tests.Services
{
/// <summary>
/// FsCheck property-based tests for DetectorFramePipelineService configuration correction.
/// Property 11 validates that values &lt;= 0 are corrected to 1.
/// </summary>
public class ConfigCorrectionPropertyTests
{
// ── Helpers ──────────────────────────────────────────────────────────
private static DetectorFramePipelineService CreateServiceWithConfig(
int acquireCapacity,
int processCapacity,
int processEveryN)
{
var mockLogger = new Mock<ILoggerService>();
mockLogger.Setup(l => l.ForModule<DetectorFramePipelineService>()).Returns(mockLogger.Object);
var mockMainViewport = new Mock<IMainViewportService>();
var eventAggregator = new EventAggregator();
return new DetectorFramePipelineService(
eventAggregator,
mockMainViewport.Object,
mockLogger.Object,
acquireCapacity,
processCapacity,
processEveryN);
}
// ── Property 11 ──────────────────────────────────────────────────────
// Feature: live-image-display, Property 11: 配置值下界修正
// Validates: Requirements 6.3, 6.4
//
// For any integer config value <= 0 (ProcessEveryNFrames, AcquireQueueCapacity,
// ProcessQueueCapacity), DetectorFramePipelineService corrects it to 1.
[Property(MaxTest = 100)]
public Property InvalidConfigValues_AreCorrectedToOne()
{
// Generate values <= 0 (including 0 and negative integers)
var nonPositiveGen = Gen.Choose(int.MinValue / 2, 0);
var gen =
from acquireCapacity in nonPositiveGen
from processCapacity in nonPositiveGen
from processEveryN in nonPositiveGen
select (acquireCapacity, processCapacity, processEveryN);
return Prop.ForAll(
gen.ToArbitrary(),
tuple =>
{
var (acquireCapacity, processCapacity, processEveryN) = tuple;
using var service = CreateServiceWithConfig(acquireCapacity, processCapacity, processEveryN);
// All three values must be corrected to 1
bool acquireCorrected = service.AcquireQueueCapacity == 1;
bool processCorrected = service.ProcessQueueCapacity == 1;
bool everyNCorrected = service.ProcessEveryNFrames == 1;
return acquireCorrected && processCorrected && everyNCorrected;
});
}
// Additional property: valid positive values are preserved as-is
[Property(MaxTest = 100)]
public Property ValidConfigValues_ArePreservedAsIs()
{
var positiveGen = Gen.Choose(1, 1000);
var gen =
from acquireCapacity in positiveGen
from processCapacity in positiveGen
from processEveryN in positiveGen
select (acquireCapacity, processCapacity, processEveryN);
return Prop.ForAll(
gen.ToArbitrary(),
tuple =>
{
var (acquireCapacity, processCapacity, processEveryN) = tuple;
using var service = CreateServiceWithConfig(acquireCapacity, processCapacity, processEveryN);
return service.AcquireQueueCapacity == acquireCapacity
&& service.ProcessQueueCapacity == processCapacity
&& service.ProcessEveryNFrames == processEveryN;
});
}
}
}
@@ -0,0 +1,194 @@
using System;
using System.Threading;
using FsCheck;
using FsCheck.Fluent;
using FsCheck.Xunit;
using Moq;
using Prism.Events;
using XP.Common.Logging.Interfaces;
using XP.Hardware.Detector.Abstractions;
using XP.Hardware.Detector.Abstractions.Events;
using XplorePlane.Services.MainViewport;
namespace XplorePlane.Tests.Services
{
/// <summary>
/// FsCheck property-based tests for DetectorFramePipelineService.
/// Properties 68 validate queue bounds and sampling correctness.
/// </summary>
public class DetectorFramePipelineServicePropertyTests
{
// ── Helpers ──────────────────────────────────────────────────────────
/// <summary>
/// Creates a DetectorFramePipelineService with the given capacity/sampling config.
/// Uses the internal test constructor to bypass App.config reads.
/// Uses a real EventAggregator so we can publish ImageCapturedEvent to drive the service.
/// </summary>
private static (DetectorFramePipelineService Service, EventAggregator EventAggregator)
CreateService(int acquireCapacity = 16, int processCapacity = 8, int processEveryN = 1)
{
var mockLogger = new Mock<ILoggerService>();
mockLogger.Setup(l => l.ForModule<DetectorFramePipelineService>()).Returns(mockLogger.Object);
var mockMainViewport = new Mock<IMainViewportService>();
var eventAggregator = new EventAggregator();
var service = new DetectorFramePipelineService(
eventAggregator,
mockMainViewport.Object,
mockLogger.Object,
acquireCapacity,
processCapacity,
processEveryN);
return (service, eventAggregator);
}
/// <summary>
/// Publishes M frames via the EventAggregator and waits for background processing.
/// </summary>
private static void PublishFrames(EventAggregator eventAggregator, int frameCount, int width = 4, int height = 4)
{
for (int i = 0; i < frameCount; i++)
{
var args = new ImageCapturedEventArgs
{
ImageData = new ushort[width * height],
Width = (uint)width,
Height = (uint)height,
FrameNumber = i + 1,
CaptureTime = DateTime.UtcNow
};
eventAggregator.GetEvent<ImageCapturedEvent>().Publish(args);
}
// Wait for background thread processing to complete.
// The subscription runs on BackgroundThread, so we need a brief wait.
Thread.Sleep(300);
}
// ── Property 6 ───────────────────────────────────────────────────────
// Feature: live-image-display, Property 6: 采集队列有界不变量
// Validates: Requirements 2.2
[Property(MaxTest = 100)]
public Property AcquireQueueCount_NeverExceedsCapacity()
{
var gen =
from capacity in Gen.Choose(1, 10)
from frameCount in Gen.Choose(1, 100)
select (capacity, frameCount);
return Prop.ForAll(
gen.ToArbitrary(),
tuple =>
{
var (capacity, frameCount) = tuple;
var (service, eventAggregator) = CreateService(acquireCapacity: capacity);
try
{
PublishFrames(eventAggregator, frameCount);
return service.AcquireQueueCount <= service.AcquireQueueCapacity;
}
finally
{
service.Dispose();
}
});
}
// ── Property 7 ───────────────────────────────────────────────────────
// Feature: live-image-display, Property 7: 处理队列有界不变量
// Validates: Requirements 2.4
[Property(MaxTest = 100)]
public Property ProcessQueueCount_NeverExceedsCapacity()
{
var gen =
from capacity in Gen.Choose(1, 10)
from frameCount in Gen.Choose(1, 100)
select (capacity, frameCount);
return Prop.ForAll(
gen.ToArbitrary(),
tuple =>
{
var (capacity, frameCount) = tuple;
var (service, eventAggregator) = CreateService(processCapacity: capacity);
try
{
PublishFrames(eventAggregator, frameCount);
return service.ProcessQueueCount <= service.ProcessQueueCapacity;
}
finally
{
service.Dispose();
}
});
}
// ── Property 8 ───────────────────────────────────────────────────────
// Feature: live-image-display, Property 8: 隔帧抽样正确性
// Validates: Requirements 2.3
//
// For any N (ProcessEveryNFrames) and M frames, the number of frames entering
// the process queue equals ceil(M / N).
//
// We use a large process queue capacity to avoid overflow dropping frames,
// and count frames via ProcessFrameDequeued events.
[Property(MaxTest = 100)]
public Property ProcessQueueEntries_EqualsCeilMDivN()
{
var gen =
from n in Gen.Choose(1, 10)
from m in Gen.Choose(1, 50)
select (n, m);
return Prop.ForAll(
gen.ToArbitrary(),
tuple =>
{
var (n, m) = tuple;
// Use a large process queue capacity so no frames are dropped due to overflow
var (service, eventAggregator) = CreateService(
acquireCapacity: 200,
processCapacity: 200,
processEveryN: n);
int dequeuedCount = 0;
using var allDequeued = new SemaphoreSlim(0);
int expected = (int)Math.Ceiling((double)m / n);
service.ProcessFrameDequeued += (_, __) =>
{
int count = Interlocked.Increment(ref dequeuedCount);
if (count >= expected)
allDequeued.Release();
};
try
{
PublishFrames(eventAggregator, m);
// Wait up to 5 seconds for all expected frames to be dequeued
bool completed = allDequeued.Wait(TimeSpan.FromSeconds(5));
// If we didn't get the expected count, give a bit more time
if (!completed)
Thread.Sleep(500);
return dequeuedCount == expected;
}
finally
{
service.Dispose();
}
});
}
}
}
@@ -11,6 +11,7 @@ using XP.Common.Database.Interfaces;
using XP.Common.Logging.Interfaces; using XP.Common.Logging.Interfaces;
using XplorePlane.Models; using XplorePlane.Models;
using XplorePlane.Services.InspectionResults; using XplorePlane.Services.InspectionResults;
using XplorePlane.Services.Storage;
using Xunit; using Xunit;
namespace XplorePlane.Tests.Services namespace XplorePlane.Tests.Services
@@ -294,6 +295,33 @@ namespace XplorePlane.Tests.Services
Assert.Equal(originalHash, snapshot.PipelineHash); Assert.Equal(originalHash, snapshot.PipelineHash);
} }
[Fact]
public async Task Constructor_WithDataPathService_WritesIntoDataInspectionResultsDirectory()
{
var mockDataPathService = new Mock<IXpDataPathService>();
var dataRoot = Path.Combine(_tempRoot, "xpdata", "Data");
mockDataPathService.SetupGet(s => s.DataPath).Returns(dataRoot);
var store = new InspectionResultStore(_dbContext, _mockLogger.Object, mockDataPathService.Object);
var run = new InspectionRunRecord
{
ProgramName = "Program-By-Service",
WorkpieceId = "Part-03",
SerialNumber = "SN-DATA"
};
await store.BeginRunAsync(run);
await store.CompleteRunAsync(run.RunId);
var manifestPath = Path.Combine(
dataRoot,
"InspectionResults",
run.ResultRootPath.Replace('/', Path.DirectorySeparatorChar),
"manifest.json");
Assert.True(File.Exists(manifestPath));
}
public void Dispose() public void Dispose()
{ {
_dbContext.Dispose(); _dbContext.Dispose();
@@ -0,0 +1,208 @@
using System;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using FsCheck;
using FsCheck.Fluent;
using FsCheck.Xunit;
using Moq;
using XP.Common.Logging.Interfaces;
using XplorePlane.Services.MainViewport;
namespace XplorePlane.Tests.Services
{
/// <summary>
/// FsCheck property-based tests for MainViewportService.
/// Uses the real MainViewportService (no mocking of the service itself).
/// </summary>
public class MainViewportServicePropertyTests
{
// ── Helpers ──────────────────────────────────────────────────────────
private static MainViewportService CreateService()
{
var mockLogger = new Mock<ILoggerService>();
mockLogger.Setup(l => l.ForModule<MainViewportService>()).Returns(mockLogger.Object);
return new MainViewportService(mockLogger.Object);
}
/// <summary>
/// Creates a frozen BitmapSource suitable for cross-thread use in tests.
/// </summary>
private static BitmapSource CreateFrozenBitmap(int width = 4, int height = 4)
{
var pixels = new byte[width * height * 4];
var bitmap = BitmapSource.Create(
width, height, 96, 96,
PixelFormats.Bgra32, null,
pixels, width * 4);
bitmap.Freeze();
return bitmap;
}
/// <summary>
/// Generator for DetectorFrame with a frozen BitmapSource.
/// </summary>
private static Gen<DetectorFrame> DetectorFrameGen =>
from frameId in Gen.Choose(1, 100000).Select(i => (long)i)
from width in Gen.Choose(1, 64)
from height in Gen.Choose(1, 64)
select new DetectorFrame(
frameId: frameId,
captureTime: DateTime.UtcNow,
width: width,
height: height,
rawPixels: new ushort[width * height],
previewImage: CreateFrozenBitmap(width, height));
// ── Property 1 ───────────────────────────────────────────────────────
// Feature: live-image-display, Property 1: 实时模式下帧更新触发显示刷新
// Validates: Requirements 3.2
[Property(MaxTest = 100)]
public Property LiveDetector_RealtimeEnabled_UpdateDetectorFrame_UpdatesDisplayAndRaisesEvent()
{
return Prop.ForAll(
DetectorFrameGen.ToArbitrary(),
frame =>
{
var service = CreateService();
// Ensure we are in LiveDetector mode with realtime enabled (default state)
service.SetSourceMode(MainViewportSourceMode.LiveDetector);
service.SetRealtimeDisplayEnabled(true);
bool stateChangedRaised = false;
service.StateChanged += (_, __) => stateChangedRaised = true;
service.UpdateDetectorFrame(frame);
bool imageMatches = ReferenceEquals(service.CurrentDisplayImage, frame.PreviewImage);
bool eventRaised = stateChangedRaised;
return imageMatches && eventRaised;
});
}
// ── Property 2 ───────────────────────────────────────────────────────
// Feature: live-image-display, Property 2: 实时关闭时帧更新不触发显示刷新
// Validates: Requirements 3.3
[Property(MaxTest = 100)]
public Property LiveDetector_RealtimeDisabled_UpdateDetectorFrame_NoEventAndImageUnchanged()
{
return Prop.ForAll(
DetectorFrameGen.ToArbitrary(),
frame =>
{
var service = CreateService();
// Set up: LiveDetector mode, realtime disabled
service.SetSourceMode(MainViewportSourceMode.LiveDetector);
service.SetRealtimeDisplayEnabled(false);
// Capture the display image before the update
var imageBefore = service.CurrentDisplayImage;
bool stateChangedRaised = false;
service.StateChanged += (_, __) => stateChangedRaised = true;
service.UpdateDetectorFrame(frame);
bool noEvent = !stateChangedRaised;
bool imageUnchanged = ReferenceEquals(service.CurrentDisplayImage, imageBefore);
return noEvent && imageUnchanged;
});
}
// ── Property 3 ───────────────────────────────────────────────────────
// Feature: live-image-display, Property 3: ManualImage 模式下探测器帧不覆盖显示图像
// Validates: Requirements 3.4
[Property(MaxTest = 100)]
public Property ManualImage_MultipleUpdateDetectorFrame_DisplayAlwaysManualImage()
{
var gen =
from frameCount in Gen.Choose(1, 10)
from frames in Gen.ListOf<DetectorFrame>(DetectorFrameGen, frameCount)
select frames;
return Prop.ForAll(
gen.ToArbitrary(),
frames =>
{
var service = CreateService();
// Set a manual image first
var manualImage = CreateFrozenBitmap(8, 8);
service.SetManualImage(manualImage, "test.png");
// Verify we are in ManualImage mode
if (service.CurrentSourceMode != MainViewportSourceMode.ManualImage)
return false;
// Call UpdateDetectorFrame multiple times
foreach (var frame in (System.Collections.Generic.IEnumerable<DetectorFrame>)frames)
{
service.UpdateDetectorFrame(frame);
}
// CurrentDisplayImage must still be the manual image
return ReferenceEquals(service.CurrentDisplayImage, manualImage);
});
}
// ── Property 4 ───────────────────────────────────────────────────────
// Feature: live-image-display, Property 4: CNC 运行时 SetManualImage 被忽略
// Validates: Requirements 3.10
[Property(MaxTest = 100)]
public Property CncRunning_SetManualImage_SourceModeRemainsLiveDetector()
{
return Prop.ForAll(
DetectorFrameGen.ToArbitrary(),
frame =>
{
var service = CreateService();
// Start CNC running
service.SetCncRunning(true);
// Attempt to set a manual image
var manualImage = CreateFrozenBitmap(4, 4);
service.SetManualImage(manualImage, "manual.png");
// CurrentSourceMode must remain LiveDetector
return service.CurrentSourceMode == MainViewportSourceMode.LiveDetector;
});
}
// ── Property 5 ───────────────────────────────────────────────────────
// Feature: live-image-display, Property 5: CNC 运行时无法关闭实时刷新
// Validates: Requirements 3.11
[Property(MaxTest = 100)]
public Property CncRunning_SetRealtimeDisplayEnabledFalse_IsRealtimeDisplayEnabledRemainsTrue()
{
// No generator needed — fixed input
return Prop.ForAll(
Gen.Constant(true).ToArbitrary(),
_ =>
{
var service = CreateService();
// Ensure realtime is enabled first
service.SetRealtimeDisplayEnabled(true);
// Start CNC running
service.SetCncRunning(true);
// Attempt to disable realtime display
service.SetRealtimeDisplayEnabled(false);
// IsRealtimeDisplayEnabled must remain true
return service.IsRealtimeDisplayEnabled;
});
}
}
}
@@ -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);
@@ -1,4 +1,4 @@
// Feature: cnc-run-execution // Feature: cnc-run-execution
// Properties 1, 2, 12: CncEditorViewModel execution control // Properties 1, 2, 12: CncEditorViewModel execution control
using System; using System;
@@ -16,6 +16,8 @@ using XP.Common.Logging.Interfaces;
using XplorePlane.Models; using XplorePlane.Models;
using XplorePlane.Services.AppState; using XplorePlane.Services.AppState;
using XplorePlane.Services.Cnc; using XplorePlane.Services.Cnc;
using XplorePlane.Services.Storage;
using XplorePlane.Services;
using XplorePlane.ViewModels.Cnc; using XplorePlane.ViewModels.Cnc;
using Xunit; using Xunit;
@@ -23,16 +25,20 @@ 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,
CncProgram initialProgram = null) CncProgram initialProgram = null,
Mock<IPipelinePersistenceService> mockPipelinePersistenceService = null)
{ {
var mockCncProgramSvc = new Mock<ICncProgramService>(); var mockCncProgramSvc = new Mock<ICncProgramService>();
var mockAppState = new Mock<IAppStateService>(); var mockAppState = new Mock<IAppStateService>();
var mockLogger = new Mock<ILoggerService>(); var mockLogger = new Mock<ILoggerService>();
var mockDataPathService = new Mock<IXpDataPathService>();
mockPipelinePersistenceService ??= new Mock<IPipelinePersistenceService>();
mockLogger.Setup(l => l.ForModule<CncEditorViewModel>()).Returns(mockLogger.Object); mockLogger.Setup(l => l.ForModule<CncEditorViewModel>()).Returns(mockLogger.Object);
mockDataPathService.SetupGet(s => s.PlanPath).Returns(System.IO.Path.GetTempPath());
mockExecSvc ??= new Mock<ICncExecutionService>(); mockExecSvc ??= new Mock<ICncExecutionService>();
@@ -47,12 +53,49 @@ namespace XplorePlane.Tests.ViewModels
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
.Setup(s => s.CreateNode(It.IsAny<CncNodeType>()))
.Returns((CncNodeType nodeType) => nodeType switch
{
CncNodeType.InspectionModule => new InspectionModuleNode(Guid.NewGuid(), 0, "检测模块_0", new PipelineModel()),
CncNodeType.ReferencePoint => new ReferencePointNode(Guid.NewGuid(), 0, "参考点_0", 0, 0, 0, 0, 0, 0, false, 0, 0),
_ => throw new InvalidOperationException($"Unsupported node type in test: {nodeType}")
});
mockCncProgramSvc
.Setup(s => s.InsertNode(It.IsAny<CncProgram>(), It.IsAny<int>(), It.IsAny<CncNode>()))
.Returns((CncProgram program, int afterIndex, CncNode node) =>
{
var nodes = program.Nodes.ToList();
var insertIndex = Math.Clamp(afterIndex + 1, 0, nodes.Count);
nodes.Insert(insertIndex, node with { Index = insertIndex });
for (var i = 0; i < nodes.Count; i++)
{
nodes[i] = nodes[i] with { Index = i };
}
return program with { Nodes = nodes.AsReadOnly(), UpdatedAt = DateTime.UtcNow };
});
mockCncProgramSvc
.Setup(s => s.UpdateNode(It.IsAny<CncProgram>(), It.IsAny<int>(), It.IsAny<CncNode>()))
.Returns((CncProgram program, int index, CncNode updatedNode) =>
{
var nodes = program.Nodes.ToList();
nodes[index] = updatedNode with { Index = index };
return program with { Nodes = nodes.AsReadOnly(), UpdatedAt = DateTime.UtcNow };
});
var vm = new CncEditorViewModel( var vm = new CncEditorViewModel(
mockCncProgramSvc.Object, mockCncProgramSvc.Object,
mockAppState.Object, mockAppState.Object,
new EventAggregator(), new EventAggregator(),
mockLogger.Object, mockLogger.Object,
mockExecSvc.Object); mockExecSvc.Object,
mockDataPathService.Object,
mockPipelinePersistenceService.Object);
if (initialProgram != null) if (initialProgram != null)
{ {
@@ -78,7 +121,7 @@ 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
@@ -125,7 +168,7 @@ 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
@@ -176,7 +219,7 @@ 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
@@ -228,5 +271,50 @@ namespace XplorePlane.Tests.ViewModels
return allDisabled; return allDisabled;
}); });
} }
[Fact]
public async Task InsertInspectionModuleFromPipelineFileAsync_LoadsPipelineAndInsertsNode()
{
var pipelineFile = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"{Guid.NewGuid():N}.xpm");
await System.IO.File.WriteAllTextAsync(pipelineFile, "{}");
try
{
var expectedPipeline = new PipelineModel
{
Name = "BuiltIn/ModuleA"
};
var mockPipelinePersistenceService = new Mock<IPipelinePersistenceService>();
mockPipelinePersistenceService
.Setup(s => s.LoadAsync(pipelineFile))
.ReturnsAsync(expectedPipeline);
var initialProgram = new CncProgram(
Guid.NewGuid(),
"TestProgram",
DateTime.UtcNow,
DateTime.UtcNow,
new List<CncNode>
{
new SavePositionNode(Guid.NewGuid(), 0, "保存位置_0", MotionState.Default)
}.AsReadOnly());
var vm = CreateVm(
initialProgram: initialProgram,
mockPipelinePersistenceService: mockPipelinePersistenceService);
await vm.InsertInspectionModuleFromPipelineFileAsync(pipelineFile);
var insertedNode = Assert.IsType<InspectionModuleNode>(vm.Nodes.Last().Model);
Assert.Same(expectedPipeline, insertedNode.Pipeline);
Assert.Equal("BuiltIn/ModuleA", insertedNode.Pipeline.Name);
}
finally
{
if (System.IO.File.Exists(pipelineFile))
System.IO.File.Delete(pipelineFile);
}
}
} }
} }
@@ -0,0 +1,216 @@
using System;
using System.Threading;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using FsCheck;
using FsCheck.Fluent;
using FsCheck.Xunit;
using Moq;
using Prism.Events;
using XP.Common.Logging.Interfaces;
using XplorePlane.Events;
using XplorePlane.Models;
using XplorePlane.Services.AppState;
using XplorePlane.Services.MainViewport;
using XplorePlane.ViewModels;
namespace XplorePlane.Tests.ViewModels
{
/// <summary>
/// FsCheck property-based tests for ViewportPanelViewModel.
/// Properties 910 validate IsAnimatedSwitchEnabled logic and ImageInfo protection.
/// </summary>
public class ViewportPanelViewModelPropertyTests
{
// ── Helpers ──────────────────────────────────────────────────────────
/// <summary>
/// Runs an action on a dedicated STA thread with a SynchronizationContext,
/// which satisfies Prism's UIThread requirement for EventAggregator.
/// </summary>
private static T RunOnStaThread<T>(Func<T> action)
{
T result = default;
Exception exception = null;
var thread = new Thread(() =>
{
SynchronizationContext.SetSynchronizationContext(new DispatcherSynchronizationContext());
try
{
result = action();
}
catch (Exception ex)
{
exception = ex;
}
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
if (exception != null)
throw new Exception("STA thread threw an exception", exception);
return result;
}
// ── Property 9 ───────────────────────────────────────────────────────
// Feature: live-image-display, Property 9: AnimatedSwitch 可用状态与连接/CNC 状态一致
// Validates: Requirements 3.13, 7.5, 7.6
//
// For any combination of IsDetectorConnected and IsCncRunning,
// IsAnimatedSwitchEnabled always equals IsDetectorConnected && !IsCncRunning.
//
// The implementation in ViewportPanelViewModel computes:
// IsAnimatedSwitchEnabled = _isDetectorConnected && !_isCncRunning
// We verify this formula holds for all boolean combinations.
[Property(MaxTest = 100)]
public Property IsAnimatedSwitchEnabled_AlwaysEqualsDetectorConnectedAndNotCncRunning()
{
var gen =
from isDetectorConnected in ArbMap.Default.GeneratorFor<bool>()
from isCncRunning in ArbMap.Default.GeneratorFor<bool>()
select (isDetectorConnected, isCncRunning);
return Prop.ForAll(
gen.ToArbitrary(),
tuple =>
{
var (isDetectorConnected, isCncRunning) = tuple;
// Run the entire test on a single STA thread with SynchronizationContext
return RunOnStaThread(() =>
{
var mockViewport = new Mock<IMainViewportService>();
mockViewport.Setup(s => s.CurrentDisplayImage).Returns((ImageSource)null);
mockViewport.Setup(s => s.CurrentDisplayInfo).Returns("等待探测器图像...");
mockViewport.Setup(s => s.IsRealtimeDisplayEnabled).Returns(true);
mockViewport.Setup(s => s.IsCncRunning).Returns(isCncRunning);
var mockAppState = new Mock<IAppStateService>();
mockAppState.SetupAdd(s => s.DetectorStateChanged += It.IsAny<EventHandler<StateChangedEventArgs<DetectorState>>>());
var mockLogger = new Mock<ILoggerService>();
mockLogger.Setup(l => l.ForModule<ViewportPanelViewModel>()).Returns(mockLogger.Object);
var eventAggregator = new EventAggregator();
var vm = new ViewportPanelViewModel(
mockViewport.Object,
eventAggregator,
mockAppState.Object,
mockLogger.Object);
// Simulate detector connection state change via DetectorStateChanged event.
var detectorState = new DetectorState(isDetectorConnected, false, 0, "");
var oldState = new DetectorState(!isDetectorConnected, false, 0, "");
var stateChangedArgs = new StateChangedEventArgs<DetectorState>(oldState, detectorState);
mockAppState.Raise(s => s.DetectorStateChanged += null, stateChangedArgs);
// Pump the dispatcher to process BeginInvoke callbacks
Dispatcher.CurrentDispatcher.Invoke(() => { }, DispatcherPriority.Background);
// Simulate CNC running state via StateChanged on the viewport service
mockViewport.Setup(s => s.IsCncRunning).Returns(isCncRunning);
mockViewport.Raise(s => s.StateChanged += null, EventArgs.Empty);
// Pump the dispatcher again
Dispatcher.CurrentDispatcher.Invoke(() => { }, DispatcherPriority.Background);
// The core invariant: IsAnimatedSwitchEnabled == IsDetectorConnected && !IsCncRunning
return vm.IsAnimatedSwitchEnabled == (vm.IsDetectorConnected && !isCncRunning);
});
});
}
// ── Property 10 ──────────────────────────────────────────────────────
// Feature: live-image-display, Property 10: 测量模式下 ImageInfo 不被覆盖
// Validates: Requirements 5.3
//
// For any active measurement mode (CurrentMeasurementMode != None) and any number
// of StateChanged triggers, ImageInfo always retains the measurement hint text
// and is not overwritten by service state.
//
// Implementation note: ViewportPanelViewModel.OnMainViewportStateChanged calls
// UpdateFromState(updateInfo: CurrentMeasurementMode == MeasurementToolMode.None)
// When CurrentMeasurementMode != None, updateInfo is false, so ImageInfo is NOT updated.
// We verify this by directly setting CurrentMeasurementMode and triggering StateChanged.
[Property(MaxTest = 100)]
public Property ActiveMeasurementMode_ImageInfo_NotOverwrittenByStateChanged()
{
// Generate non-None measurement modes
var modeGen = Gen.Elements(
MeasurementToolMode.PointDistance,
MeasurementToolMode.PointLineDistance,
MeasurementToolMode.Angle,
MeasurementToolMode.ThroughHoleFillRate);
var gen =
from mode in modeGen
from triggerCount in Gen.Choose(1, 20)
select (mode, triggerCount);
return Prop.ForAll(
gen.ToArbitrary(),
tuple =>
{
var (mode, triggerCount) = tuple;
// Run the entire test on a single STA thread with SynchronizationContext
return RunOnStaThread(() =>
{
var mockViewport = new Mock<IMainViewportService>();
mockViewport.Setup(s => s.CurrentDisplayImage).Returns((ImageSource)null);
mockViewport.Setup(s => s.CurrentDisplayInfo).Returns("等待探测器图像...");
mockViewport.Setup(s => s.IsRealtimeDisplayEnabled).Returns(true);
mockViewport.Setup(s => s.IsCncRunning).Returns(false);
var mockAppState = new Mock<IAppStateService>();
mockAppState.SetupAdd(s => s.DetectorStateChanged += It.IsAny<EventHandler<StateChangedEventArgs<DetectorState>>>());
var mockLogger = new Mock<ILoggerService>();
mockLogger.Setup(l => l.ForModule<ViewportPanelViewModel>()).Returns(mockLogger.Object);
var eventAggregator = new EventAggregator();
var vm = new ViewportPanelViewModel(
mockViewport.Object,
eventAggregator,
mockAppState.Object,
mockLogger.Object);
// Directly set the measurement mode on the ViewModel.
// This bypasses the UIThread event dispatch issue and directly
// sets the backing field that controls ImageInfo protection.
vm.CurrentMeasurementMode = mode;
// Set ImageInfo to the measurement hint text (as OnMeasurementToolActivated would)
vm.ImageInfo = vm.MeasurementModeText;
string measurementInfo = vm.ImageInfo;
// Verify measurement mode is active
if (vm.CurrentMeasurementMode == MeasurementToolMode.None)
return false;
// Trigger StateChanged multiple times.
// The handler calls BeginInvoke — we pump the dispatcher to process callbacks.
for (int i = 0; i < triggerCount; i++)
{
mockViewport.Raise(s => s.StateChanged += null, EventArgs.Empty);
// Pump the dispatcher to process BeginInvoke callbacks
Dispatcher.CurrentDispatcher.Invoke(() => { }, DispatcherPriority.Background);
}
// ImageInfo must still be the measurement hint text, not the service info.
// This holds because UpdateFromState is called with updateInfo: false
// when CurrentMeasurementMode != None.
return vm.ImageInfo == measurementInfo;
});
});
}
}
}
+15 -29
View File
@@ -3,19 +3,17 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 17
VisualStudioVersion = 17.14.36811.4 VisualStudioVersion = 17.14.36811.4
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XplorePlane", "XplorePlane\XplorePlane.csproj", "{07978DB9-4B88-4F42-9054-73992742BD6A}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XplorePlane", "XplorePlane\XplorePlane.csproj", "{07978DB9-4B88-4F42-9054-73992742BD6A}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XplorePlane.Tests", "XplorePlane.Tests\XplorePlane.Tests.csproj", "{6234B622-8DF2-4A8D-AF93-B17774019555}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XP.Camera", "XP.Camera\XP.Camera.csproj", "{82762CDE-48CC-4E28-ABEC-1FC752BACEF4}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XP.Camera", "XP.Camera\XP.Camera.csproj", "{82762CDE-48CC-4E28-ABEC-1FC752BACEF4}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XP.ImageProcessing.Core", "XP.ImageProcessing.Core\XP.ImageProcessing.Core.csproj", "{01EDC1D8-F6BC-2677-AE59-89BA3FC2C74F}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XP.ImageProcessing.Core", "XP.ImageProcessing.Core\XP.ImageProcessing.Core.csproj", "{01EDC1D8-F6BC-2677-AE59-89BA3FC2C74F}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XP.ImageProcessing.Processors", "XP.ImageProcessing.Processors\XP.ImageProcessing.Processors.csproj", "{2687E12E-3053-E1C6-5268-E4FF547EC212}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XP.ImageProcessing.Processors", "XP.ImageProcessing.Processors\XP.ImageProcessing.Processors.csproj", "{2687E12E-3053-E1C6-5268-E4FF547EC212}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XP.ImageProcessing.CfgControl", "XP.ImageProcessing.CfgControl\XP.ImageProcessing.CfgControl.csproj", "{9460CF45-8A25-9770-03AF-4602A2FFF016}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XP.ImageProcessing.CfgControl", "XP.ImageProcessing.CfgControl\XP.ImageProcessing.CfgControl.csproj", "{9460CF45-8A25-9770-03AF-4602A2FFF016}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XP.ImageProcessing.RoiControl", "XP.ImageProcessing.RoiControl\XP.ImageProcessing.RoiControl.csproj", "{57061533-EC58-1B1C-3862-9164BC73C806}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XP.ImageProcessing.RoiControl", "XP.ImageProcessing.RoiControl\XP.ImageProcessing.RoiControl.csproj", "{57061533-EC58-1B1C-3862-9164BC73C806}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ExternalLibraries", "ExternalLibraries", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ExternalLibraries", "ExternalLibraries", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
@@ -40,31 +38,31 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ExternalLibraries", "Extern
ExternalLibraries\version_string.inc = ExternalLibraries\version_string.inc ExternalLibraries\version_string.inc = ExternalLibraries\version_string.inc
EndProjectSection EndProjectSection
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XP.Common", "XP.Common\XP.Common.csproj", "{866A7353-C822-114E-48DE-26E8A3E17F9E}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XP.Common", "XP.Common\XP.Common.csproj", "{866A7353-C822-114E-48DE-26E8A3E17F9E}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XP.Hardware.Detector", "XP.Hardware.Detector\XP.Hardware.Detector.csproj", "{760D5EAC-795A-6666-1BE0-E30B1B2822C3}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XP.Hardware.Detector", "XP.Hardware.Detector\XP.Hardware.Detector.csproj", "{760D5EAC-795A-6666-1BE0-E30B1B2822C3}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XP.Hardware.MotionControl", "XP.Hardware.MotionControl\XP.Hardware.MotionControl.csproj", "{9391C622-4552-8081-A1E8-B3447E0E7A4F}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XP.Hardware.MotionControl", "XP.Hardware.MotionControl\XP.Hardware.MotionControl.csproj", "{9391C622-4552-8081-A1E8-B3447E0E7A4F}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XP.Hardware.PLC", "XP.Hardware.PLC\XP.Hardware.PLC.csproj", "{9E9FB4E7-22F9-D475-D9DA-0D647BF6DFD9}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XP.Hardware.PLC", "XP.Hardware.PLC\XP.Hardware.PLC.csproj", "{9E9FB4E7-22F9-D475-D9DA-0D647BF6DFD9}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XP.Hardware.PLC.Sentry", "XP.Hardware.PLC.Sentry\XP.Hardware.PLC.Sentry.csproj", "{C0635DDF-1BCC-2F86-3BA1-12E0469F114B}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XP.Hardware.PLC.Sentry", "XP.Hardware.PLC.Sentry\XP.Hardware.PLC.Sentry.csproj", "{C0635DDF-1BCC-2F86-3BA1-12E0469F114B}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XP.Hardware.RaySource", "XP.Hardware.RaySource\XP.Hardware.RaySource.csproj", "{67D180E8-AB8F-FF62-ED46-270803B8F713}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XP.Hardware.RaySource", "XP.Hardware.RaySource\XP.Hardware.RaySource.csproj", "{67D180E8-AB8F-FF62-ED46-270803B8F713}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XP.Hardware.RaySource.Comet.Host", "XP.Hardware.RaySource.Comet.Host\XP.Hardware.RaySource.Comet.Host.csproj", "{B8F5E3A1-7C2D-4E9F-A1B3-6D8E4F2C9A01}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XP.Hardware.RaySource.Comet.Host", "XP.Hardware.RaySource.Comet.Host\XP.Hardware.RaySource.Comet.Host.csproj", "{B8F5E3A1-7C2D-4E9F-A1B3-6D8E4F2C9A01}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XP.Hardware.RaySource.Comet.Messages", "XP.Hardware.RaySource.Comet.Messages\XP.Hardware.RaySource.Comet.Messages.csproj", "{6170AF9F-A792-6BDC-4E25-072EA87FAA15}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XP.Hardware.RaySource.Comet.Messages", "XP.Hardware.RaySource.Comet.Messages\XP.Hardware.RaySource.Comet.Messages.csproj", "{6170AF9F-A792-6BDC-4E25-072EA87FAA15}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "XP.Hardware", "XP.Hardware", "{29E2D405-341A-4445-B788-3E77A677C2BA}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "XP.Hardware", "XP.Hardware", "{29E2D405-341A-4445-B788-3E77A677C2BA}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "XP.ImageProcessing", "XP.ImageProcessing", "{C24535A4-6717-4149-AB81-1EF09A15F90F}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "XP.ImageProcessing", "XP.ImageProcessing", "{C24535A4-6717-4149-AB81-1EF09A15F90F}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XP.Scan", "XP.Scan\XP.Scan.csproj", "{F40C71DC-7639-CD57-6183-2EAA78980EC5}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XP.Scan", "XP.Scan\XP.Scan.csproj", "{F40C71DC-7639-CD57-6183-2EAA78980EC5}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "XP.ScanMode", "XP.ScanMode", "{E208A5EA-7E3B-46B4-B045-A703F6274218}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "XP.ScanMode", "XP.ScanMode", "{E208A5EA-7E3B-46B4-B045-A703F6274218}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XP.Calibration", "XP.Calibration\XP.Calibration.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XP.Calibration", "XP.Calibration\XP.Calibration.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "XP.Calibration", "XP.Calibration", "{D4E5F6A7-B8C9-0123-4567-89ABCDEF0123}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "XP.Calibration", "XP.Calibration", "{D4E5F6A7-B8C9-0123-4567-89ABCDEF0123}"
EndProject EndProject
@@ -90,18 +88,6 @@ Global
{07978DB9-4B88-4F42-9054-73992742BD6A}.Release|x64.Build.0 = Release|Any CPU {07978DB9-4B88-4F42-9054-73992742BD6A}.Release|x64.Build.0 = Release|Any CPU
{07978DB9-4B88-4F42-9054-73992742BD6A}.Release|x86.ActiveCfg = Release|Any CPU {07978DB9-4B88-4F42-9054-73992742BD6A}.Release|x86.ActiveCfg = Release|Any CPU
{07978DB9-4B88-4F42-9054-73992742BD6A}.Release|x86.Build.0 = Release|Any CPU {07978DB9-4B88-4F42-9054-73992742BD6A}.Release|x86.Build.0 = Release|Any CPU
{6234B622-8DF2-4A8D-AF93-B17774019555}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6234B622-8DF2-4A8D-AF93-B17774019555}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6234B622-8DF2-4A8D-AF93-B17774019555}.Debug|x64.ActiveCfg = Debug|Any CPU
{6234B622-8DF2-4A8D-AF93-B17774019555}.Debug|x64.Build.0 = Debug|Any CPU
{6234B622-8DF2-4A8D-AF93-B17774019555}.Debug|x86.ActiveCfg = Debug|Any CPU
{6234B622-8DF2-4A8D-AF93-B17774019555}.Debug|x86.Build.0 = Debug|Any CPU
{6234B622-8DF2-4A8D-AF93-B17774019555}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6234B622-8DF2-4A8D-AF93-B17774019555}.Release|Any CPU.Build.0 = Release|Any CPU
{6234B622-8DF2-4A8D-AF93-B17774019555}.Release|x64.ActiveCfg = Release|Any CPU
{6234B622-8DF2-4A8D-AF93-B17774019555}.Release|x64.Build.0 = Release|Any CPU
{6234B622-8DF2-4A8D-AF93-B17774019555}.Release|x86.ActiveCfg = Release|Any CPU
{6234B622-8DF2-4A8D-AF93-B17774019555}.Release|x86.Build.0 = Release|Any CPU
{82762CDE-48CC-4E28-ABEC-1FC752BACEF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {82762CDE-48CC-4E28-ABEC-1FC752BACEF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{82762CDE-48CC-4E28-ABEC-1FC752BACEF4}.Debug|Any CPU.Build.0 = Debug|Any CPU {82762CDE-48CC-4E28-ABEC-1FC752BACEF4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{82762CDE-48CC-4E28-ABEC-1FC752BACEF4}.Debug|x64.ActiveCfg = Debug|Any CPU {82762CDE-48CC-4E28-ABEC-1FC752BACEF4}.Debug|x64.ActiveCfg = Debug|Any CPU
+1
View File
@@ -7,6 +7,7 @@
<appSettings> <appSettings>
<!-- 语言配置 可选值: ZhCN, ZhTW, EnUS| Language Configuration --> <!-- 语言配置 可选值: ZhCN, ZhTW, EnUS| Language Configuration -->
<add key="Language" value="ZhCN" /> <add key="Language" value="ZhCN" />
<add key="XpData:RootPath" value="D:\XPData" />
<add key="UserManual" value="D:\HMQProject\XplorePlane_CT\Code\XplorePlane\XP.App\bin\Debug\net8.0-windows7.0\UserManual.pdf" /> <add key="UserManual" value="D:\HMQProject\XplorePlane_CT\Code\XplorePlane\XP.App\bin\Debug\net8.0-windows7.0\UserManual.pdf" />
<!-- Serilog日志配置 --> <!-- Serilog日志配置 -->
+7 -5
View File
@@ -40,6 +40,7 @@ using XplorePlane.Services.MainViewport;
using XplorePlane.Services.Matrix; using XplorePlane.Services.Matrix;
using XplorePlane.Services.Measurement; using XplorePlane.Services.Measurement;
using XplorePlane.Services.Recipe; using XplorePlane.Services.Recipe;
using XplorePlane.Services.Storage;
using XplorePlane.ViewModels; using XplorePlane.ViewModels;
using XplorePlane.ViewModels.Cnc; using XplorePlane.ViewModels.Cnc;
using XplorePlane.Views; using XplorePlane.Views;
@@ -229,8 +230,8 @@ namespace XplorePlane
Log.Fatal(exception, "应用程序发生未处理的异常"); Log.Fatal(exception, "应用程序发生未处理的异常");
MessageBox.Show( MessageBox.Show(
$"应用程序发生严重错误:\n\n{exception?.Message}\n\n请查看日志文件获取详细信息。", $"A fatal error has occurred:\n\n{exception?.Message}\n\nPlease check the log file for details.",
"严重错误", "Fatal Error",
MessageBoxButton.OK, MessageBoxButton.OK,
MessageBoxImage.Error); MessageBoxImage.Error);
} }
@@ -243,8 +244,8 @@ namespace XplorePlane
Log.Error(e.Exception, "UI 线程发生未处理的异常"); Log.Error(e.Exception, "UI 线程发生未处理的异常");
MessageBox.Show( MessageBox.Show(
$"应用程序发生错误:\n\n{e.Exception.Message}\n\n请查看日志文件获取详细信息。", $"An error has occurred:\n\n{e.Exception.Message}\n\nPlease check the log file for details.",
"错误", "Error",
MessageBoxButton.OK, MessageBoxButton.OK,
MessageBoxImage.Error); MessageBoxImage.Error);
@@ -369,7 +370,7 @@ namespace XplorePlane
// 注册图像处理服务与视图 // 注册图像处理服务与视图
containerRegistry.RegisterSingleton<IImageProcessingService, ImageProcessingService>(); containerRegistry.RegisterSingleton<IImageProcessingService, ImageProcessingService>();
containerRegistry.Register<ImageProcessingViewModel>(); containerRegistry.Register<ImageProcessingViewModel>();
containerRegistry.RegisterForNavigation<ImageProcessingPanelView>();
// 注册流水线服务(单例,共享 IImageProcessingService // 注册流水线服务(单例,共享 IImageProcessingService
containerRegistry.RegisterSingleton<IPipelineExecutionService, PipelineExecutionService>(); containerRegistry.RegisterSingleton<IPipelineExecutionService, PipelineExecutionService>();
@@ -377,6 +378,7 @@ namespace XplorePlane
// 注册全局状态服务(单例) // 注册全局状态服务(单例)
containerRegistry.RegisterSingleton<IAppStateService, AppStateService>(); containerRegistry.RegisterSingleton<IAppStateService, AppStateService>();
containerRegistry.RegisterSingleton<IXpDataPathService, XpDataPathService>();
// 注册检测配方服务(单例) // 注册检测配方服务(单例)
containerRegistry.RegisterSingleton<IRecipeService, RecipeService>(); containerRegistry.RegisterSingleton<IRecipeService, RecipeService>();
@@ -1,4 +1,4 @@
# RaySourceOperateView 集成技术路线 # 硬件层及 UI 集成技术路线
## 整体架构 ## 整体架构
@@ -169,3 +169,357 @@ raySourceService?.Dispose();
``` ```
确保硬件连接在应用退出时正确断开。 确保硬件连接在应用退出时正确断开。
### 7.
---
# 硬件层 → AppState → UI 状态同步机制
## 整体结论
| 硬件子系统 | 同步方式 | AppState 订阅 | 状态是否自动同步 |
|-----------|---------|--------------|----------------|
| 运动控制(Motion) | 事件驱动 + 轮询 | ✅ 已订阅 `GeometryUpdatedEvent` / `AxisStatusChangedEvent` | ✅ 自动同步 |
| 射线源(RaySource) | 事件驱动 | ✅ 已订阅 `StatusUpdatedEvent` / `RaySourceStatusChangedEvent` / `VariablesConnectedEvent` | ✅ 自动同步 |
| 探测器(Detector) | 事件驱动 | ✅ 已订阅 `StatusChangedEvent` / `ImageCapturedEvent` | ✅ 自动同步 |
| 相机(Camera | ViewModel 直连 + AppState 同步 | ✅ 连接/采集状态通过 `SyncCameraStateToAppState()` 推送 | ✅ 连接状态自动同步(帧数据不经过 AppState) |
---
## 一、运动控制 — 完整链路
### 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` 已在构造时订阅 `StatusUpdatedEvent``RaySourceStatusChangedEvent``VariablesConnectedEvent``RaySourceState` 自动同步。
```
XP.Hardware.RaySource 发布的事件(AppState 层订阅情况):
├─ StatusUpdatedEvent ← ✅ 已订阅(主路径),映射为 RaySourceState
├─ RaySourceStatusChangedEvent ← ✅ 已订阅(补充路径),快速同步 IsOn 状态
├─ VariablesConnectedEvent ← ✅ 已订阅,断开时重置 RaySourceState 为默认值
└─ OperationResultEvent ← 未订阅(操作结果由 ViewModel 层直接处理)
```
### 2.2 数据流
```
射线源硬件(B&R PVI
└─ IRaySourceServiceXP.Hardware.RaySource
├─ 发布 StatusUpdatedEventSystemStatusData500ms 轮询)
│ └─ AppStateService.OnRaySourceStatusUpdated() [BackgroundThread]
│ ├─ IsOn = data.IsXRayOn
│ ├─ Voltage = data.ActualVoltage (kV)
│ ├─ Power = ActualVoltage × ActualCurrent / 1000 (W)
│ └─ UpdateRaySourceState() → 触发 RaySourceStateChanged → ViewModel 刷新 UI
├─ 发布 RaySourceStatusChangedEventRaySourceStatus 枚举)
│ └─ AppStateService.OnRaySourceStatusChanged() [BackgroundThread]
│ └─ 仅在 IsOn 变化时更新,保留当前 Voltage/Power 值
└─ 发布 VariablesConnectedEventbool
└─ AppStateService.OnRaySourceVariablesConnected() [BackgroundThread]
└─ 断开时:UpdateRaySourceState(RaySourceState.Default)
```
### 2.3 关键实现细节
**双路径设计**`StatusUpdatedEvent` 是主路径,携带完整的电压、电流、开关状态;`RaySourceStatusChangedEvent` 是补充路径,仅在 `StatusUpdatedEvent` 尚未到达时快速同步 `IsOn` 字段,避免覆盖精确数据:
```csharp
// OnRaySourceStatusChanged — 仅当 IsOn 状态发生变化时才更新
if (current.IsOn == isOn) return;
UpdateRaySourceState(new RaySourceState(IsOn: isOn, Voltage: current.Voltage, Power: current.Power));
```
**功率计算**`Power(W) = ActualVoltage(kV) × ActualCurrent(μA) / 1000`
**断连重置**PVI 变量断开时将 `RaySourceState` 重置为 `Default(false, 0, 0)`,防止 UI 显示过期数据。
**Dispose 时取消订阅**:三个 token 均在 `Dispose()` 中取消。
---
## 三、探测器 — ✅ 已打通
### 3.1 当前状态
`AppStateService` 已在构造时订阅 `StatusChangedEvent``ImageCapturedEvent`,探测器状态和最新帧均自动同步。
```
XP.Hardware.Detector 发布的事件(AppState 层订阅情况):
├─ StatusChangedEvent ← ✅ 已订阅,映射为 DetectorState
├─ ImageCapturedEvent ← ✅ 已订阅,缓存为 LatestDetectorFramevolatile
├─ CorrectionCompletedEvent ← 未订阅(无需同步到 AppState)
└─ ErrorOccurredEvent ← 未订阅(无需同步到 AppState)
```
### 3.2 数据流
```
探测器硬件
└─ IDetectorServiceXP.Hardware.Detector
├─ 发布 StatusChangedEventDetectorStatus 枚举)
│ └─ AppStateService.OnDetectorStatusChanged() [BackgroundThread]
│ ├─ 映射 DetectorStatus → DetectorState(IsConnected, IsAcquiring, ...)
│ ├─ UpdateDetectorState()
│ │ └─ 触发 DetectorStateChanged 事件 → ViewModel 刷新 UI
│ └─ 若从已连接变为断开:发布 DetectorDisconnectedEvent
│ ├─ CncExecutionService 订阅 → 取消当前 CNC 执行
│ └─ ViewportPanelViewModel 订阅 → 弹出断连警告对话框
└─ 发布 ImageCapturedEventImageCapturedEventArgs
└─ AppStateService.OnDetectorImageCaptured() [BackgroundThread]
└─ volatile 写 _latestDetectorFrame
└─ 上层通过 LatestDetectorFrame 属性按需读取(CNC 执行、图像处理等)
```
### 3.3 关键实现细节
**订阅均在后台线程**,避免阻塞采集链路:
```csharp
_detectorStatusChangedToken = _eventAggregator
.GetEvent<StatusChangedEvent>()
.Subscribe(OnDetectorStatusChanged, ThreadOption.BackgroundThread);
_detectorImageCapturedToken = _eventAggregator
.GetEvent<ImageCapturedEvent>()
.Subscribe(OnDetectorImageCaptured, ThreadOption.BackgroundThread);
```
**断连检测**`OnDetectorStatusChanged` 在更新状态前记录 `wasConnected`,状态更新后若检测到从已连接变为断开,则发布 `DetectorDisconnectedEvent`
```csharp
bool wasConnected = _detectorState?.IsConnected ?? false;
// ... 更新状态 ...
if (wasConnected && !isConnected)
{
_eventAggregator.GetEvent<DetectorDisconnectedEvent>().Publish();
_logger.Warn("探测器已断连,发布 DetectorDisconnectedEvent");
}
```
**最新帧缓存**`LatestDetectorFrame``volatile` 字段,任意线程可安全读取,无需加锁:
```csharp
private volatile ImageCapturedEventArgs _latestDetectorFrame;
public ImageCapturedEventArgs LatestDetectorFrame => _latestDetectorFrame;
```
**Dispose 时取消订阅**
```csharp
_eventAggregator.GetEvent<StatusChangedEvent>().Unsubscribe(_detectorStatusChangedToken);
_eventAggregator.GetEvent<ImageCapturedEvent>().Unsubscribe(_detectorImageCapturedToken);
```
### 3.4 DetectorDisconnectedEvent 下游链路
`DetectorDisconnectedEvent` 是无载荷 Prism 事件,定义于 `XplorePlane/Events/DetectorDisconnectedEvent.cs`
| 订阅方 | 线程选项 | 行为 |
|--------|---------|------|
| `CncExecutionService` | `BackgroundThread` | 取消 `_executionCts`,中止当前 CNC 执行 |
| `ViewportPanelViewModel` | `UIThread` | 若 CNC 正在运行,弹出 `MessageBox` 警告 |
---
## 四、相机 — ✅ 已打通(连接状态同步)
### 4.1 当前状态
相机图像流数据量大、帧率高,不适合经过 AppState 的不可变 record 替换机制,因此相机的图像帧仍由 `NavigationPropertyPanelViewModel` 直接持有并渲染。但**连接状态和采集状态**现已通过 `UpdateCameraState()` 同步到 `AppStateService`,供其他模块查询。
```
ICameraController 事件(NavigationPropertyPanelViewModel 直接订阅):
├─ ImageGrabbed ← 直接渲染到 CameraImageSource(不经过 AppState,避免高频刷新)
├─ GrabError ← 更新 CameraStatusText
└─ ConnectionLost ← 更新 IsCameraConnected + 同步 CameraState ✅
```
### 4.2 数据流
```
ICameraControllerXP.Camera
└─ NavigationPropertyPanelViewModel(直接持有 _camera
├─ ConnectCamera() / OnCameraReady()
│ └─ IsCameraConnected = true → SyncCameraStateToAppState()
│ └─ AppStateService.UpdateCameraState(IsConnected=true, IsStreaming=false, ...)
├─ DisconnectCamera()
│ └─ IsCameraConnected = false → SyncCameraStateToAppState()
│ └─ AppStateService.UpdateCameraState(IsConnected=false, IsStreaming=false, ...)
├─ StartGrab() / StopGrab()
│ └─ IsCameraGrabbing = true/false → SyncCameraStateToAppState()
│ └─ AppStateService.UpdateCameraState(IsStreaming=true/false, ...)
└─ OnCameraConnectionLost()
└─ IsCameraConnected = false → SyncCameraStateToAppState()
└─ AppStateService.UpdateCameraState(IsConnected=false, IsStreaming=false, ...)
```
### 4.3 关键实现细节
`SyncCameraStateToAppState()` 是统一的同步入口,在所有状态变更节点调用:
```csharp
private void SyncCameraStateToAppState()
{
_appStateService.UpdateCameraState(new CameraState(
IsConnected: IsCameraConnected,
IsStreaming: IsCameraGrabbing,
CurrentFrame: null, // 帧数据不经过 AppState,避免高频触发 UI 刷新
Width: IsCameraConnected ? ImageWidth : 0,
Height: IsCameraConnected ? ImageHeight : 0,
FrameRate: 0));
}
```
**设计决策**`CurrentFrame` 始终为 null,帧数据仍由 ViewModel 直接持有。`CameraState` 只承载连接/采集状态和图像尺寸,供其他模块(如 CNC 执行、状态栏)查询相机是否可用。
**DI 注册**`NavigationPropertyPanelViewModel` 已通过 `RegisterSingleton<NavigationPropertyPanelViewModel>()` 注册,DryIoc 自动解析新增的 `IAppStateService` 构造函数参数,无需修改注册代码。
---
## 五、状态同步全景图
```
┌─────────────────────────────────────────────────────────────────────┐
│ 硬件层 │
│ │
│ PLC ──→ MotionControlService ──→ GeometryUpdatedEvent ─────┐ │
│ └──→ AxisStatusChangedEvent ────┤ │
│ │ │
│ RaySource ──→ IRaySourceService ──→ StatusUpdatedEvent ────┤ │
│ └──→ RaySourceStatusChangedEvent ─┤│
│ └──→ VariablesConnectedEvent ────┤│
│ │ │
│ Detector ──→ IDetectorService ──→ StatusChangedEvent ────┤ │
│ └──→ ImageCapturedEvent ────┤ │
│ │ │
│ Camera ──→ ICameraController ──→ ConnectionLost / Grab ────┘ │
│ (via SyncCameraStateToAppState) │
└─────────────────────────────────────────────────────────────────────┘
│ 全部已接通 ✅
┌─────────────────────────────────────────────────────────────────────┐
│ AppStateService │
│ │
│ MotionState ← 自动同步(轮询 + 事件驱动) ✅ │
│ RaySourceState ← 自动同步(StatusUpdatedEvent 等) ✅ │
│ DetectorState ← 自动同步(StatusChangedEvent ✅ │
│ LatestDetectorFrame ← 自动缓存(ImageCapturedEvent ✅ │
│ CameraState ← 连接/采集状态同步(帧数据不经过) ✅ │
└─────────────────────────────────────────────────────────────────────┘
↓ PropertyChanged / StateChangedEvent
┌─────────────────────────────────────────────────────────────────────┐
│ ViewModel 层 │
│ │
│ MotionControlViewModel ← 同时订阅 GeometryUpdatedEvent(双路) │
│ CncNodeViewModel ← 通过 IAppStateService 读取快照 │
│ CncEditorViewModel ← 通过 IAppStateService 读取快照 │
│ ViewportPanelViewModel ← 订阅 DetectorStateChanged │
│ 订阅 DetectorDisconnectedEvent │
│ CncExecutionService ← 订阅 DetectorDisconnectedEvent │
│ NavigationPropertyPanelViewModel ← 直连 ICameraController │
│ + 推送 CameraState 到 AppState │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 六、待办事项
| 优先级 | 项目 | 涉及文件 | 状态 |
|--------|------|---------|------|
| 已完成 | 射线源事件订阅,打通 `RaySourceState` 自动同步 | `AppStateService.cs` | ✅ 已完成 |
| 已完成 | 探测器事件订阅,打通 `DetectorState` 自动同步 + `LatestDetectorFrame` 缓存 | `AppStateService.cs` | ✅ 已完成 |
| 已完成 | 探测器断连事件(`DetectorDisconnectedEvent`)下游链路 | `CncExecutionService.cs``ViewportPanelViewModel.cs` | ✅ 已完成 |
| 已完成 | 手动模式下实时按钮切换回 `LiveDetector` 模式 | `MainViewportService.cs` | ✅ 已完成 |
| 中 | 确认 `StartPolling()` 在应用启动流程中被调用 | `App.xaml.cs` / `AppBootstrapper` | ✅ 已确认 |
| 低 | 评估是否将相机连接状态纳入 `CameraState` 统一管理 | `NavigationPropertyPanelViewModel.cs``AppStateService.cs` | ✅ 已完成 |
@@ -0,0 +1,12 @@
using Prism.Events;
namespace XplorePlane.Events
{
/// <summary>
/// 探测器断连事件。
/// 当探测器连接状态从已连接变为断开时,由 AppStateService 发布。
/// </summary>
public sealed class DetectorDisconnectedEvent : PubSubEvent
{
}
}
+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);
} }
+387 -50
View File
@@ -1,29 +1,54 @@
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.Abstractions.Enums;
using XP.Hardware.RaySource.Abstractions.Events;
using XP.Hardware.RaySource.Services; using XP.Hardware.RaySource.Services;
using XplorePlane.Events;
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 readonly SubscriptionToken _raySourceStatusUpdatedToken;
private readonly SubscriptionToken _raySourceStatusChangedToken;
private readonly SubscriptionToken _raySourceVariablesConnectedToken;
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 +57,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 +80,69 @@ 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);
// ── 射线源状态事件订阅(后台线程)──
_raySourceStatusUpdatedToken = _eventAggregator
.GetEvent<StatusUpdatedEvent>()
.Subscribe(OnRaySourceStatusUpdated, ThreadOption.BackgroundThread);
_raySourceStatusChangedToken = _eventAggregator
.GetEvent<RaySourceStatusChangedEvent>()
.Subscribe(OnRaySourceStatusChanged, ThreadOption.BackgroundThread);
_raySourceVariablesConnectedToken = _eventAggregator
.GetEvent<VariablesConnectedEvent>()
.Subscribe(OnRaySourceVariablesConnected, ThreadOption.BackgroundThread);
SubscribeToExistingServices(); SubscribeToExistingServices();
_logger.Info("AppStateService 已初始化"); _logger.Info("AppStateService initialized");
} }
// ── 状态更新方法 ── // ── 状态更新方法 ──
@@ -80,17 +150,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 +183,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 +197,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 +211,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 +225,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 +254,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 +272,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 +295,63 @@ 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);
}
if (_raySourceStatusUpdatedToken is not null)
{
_eventAggregator.GetEvent<StatusUpdatedEvent>().Unsubscribe(_raySourceStatusUpdatedToken);
}
if (_raySourceStatusChangedToken is not null)
{
_eventAggregator.GetEvent<RaySourceStatusChangedEvent>().Unsubscribe(_raySourceStatusChangedToken);
}
if (_raySourceVariablesConnectedToken is not null)
{
_eventAggregator.GetEvent<VariablesConnectedEvent>().Unsubscribe(_raySourceVariablesConnectedToken);
}
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 +362,214 @@ 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;
// 在更新状态前记录当前连接状态,用于检测断连转换
bool wasConnected = _detectorState?.IsConnected ?? false;
// 从 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);
// 检测从已连接变为断开,发布断连事件
if (wasConnected && !isConnected)
{
_eventAggregator.GetEvent<DetectorDisconnectedEvent>().Publish();
_logger.Warn("探测器已断连,发布 DetectorDisconnectedEvent | Detector disconnected, publishing DetectorDisconnectedEvent");
}
}
/// <summary>
/// 探测器图像采集回调。
/// 仅缓存最新帧引用(volatile 写),不做任何图像处理,保持采集链路零阻塞。
/// 上层通过 LatestDetectorFrame 属性按需读取。
/// </summary>
private void OnDetectorImageCaptured(ImageCapturedEventArgs args)
{
if (_disposed || args?.ImageData == null) return;
_latestDetectorFrame = args;
}
/// <summary>
/// 射线源全量状态更新回调(主路径)。
/// StatusUpdatedEvent 携带实际电压、电流和开关状态,是 RaySourceState 的主要数据来源。
/// Power(W) = ActualVoltage(kV) × ActualCurrent(μA) / 1000
/// </summary>
private void OnRaySourceStatusUpdated(SystemStatusData data)
{
if (_disposed || data == null) return;
double power = data.ActualVoltage * data.ActualCurrent / 1000.0;
UpdateRaySourceState(new RaySourceState(
IsOn: data.IsXRayOn,
Voltage: data.ActualVoltage,
Power: power));
}
/// <summary>
/// 射线源三态状态变更回调(补充路径)。
/// RaySourceStatusChangedEvent 仅携带枚举状态,用于在 StatusUpdatedEvent 尚未到达时
/// 快速同步 IsOn 字段,电压和功率保持当前值不变。
/// </summary>
private void OnRaySourceStatusChanged(RaySourceStatus status)
{
if (_disposed) return;
bool isOn = status == RaySourceStatus.Opened;
var current = _raySourceState;
// 仅当 IsOn 状态发生变化时才更新,避免覆盖 StatusUpdatedEvent 写入的精确数据
if (current.IsOn == isOn) return;
UpdateRaySourceState(new RaySourceState(
IsOn: isOn,
Voltage: current.Voltage,
Power: current.Power));
_logger.Info(
"射线源状态变更:{Status} → IsOn={IsOn} | RaySource status changed: {Status} → IsOn={IsOn}",
status, isOn);
}
/// <summary>
/// 射线源 PVI 变量连接状态变更回调。
/// 断开时将 RaySourceState 重置为默认值(IsOn=false, Voltage=0, Power=0)。
/// </summary>
private void OnRaySourceVariablesConnected(bool isConnected)
{
if (_disposed) return;
if (!isConnected)
{
UpdateRaySourceState(RaySourceState.Default);
_logger.Warn("射线源 PVI 变量已断开,RaySourceState 已重置 | RaySource PVI variables disconnected, RaySourceState reset");
}
else
{
_logger.Info("射线源 PVI 变量已连接 | RaySource PVI variables connected");
}
}
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);
+160 -66
View File
@@ -6,9 +6,12 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using XP.Common.GeneralForm.Views; using Prism.Events;
using XP.Common.Converters;
using XP.Common.Logging.Interfaces; using XP.Common.Logging.Interfaces;
using XplorePlane.Events;
using XplorePlane.Models; using XplorePlane.Models;
using XplorePlane.Services.AppState;
using XplorePlane.Services.InspectionResults; using XplorePlane.Services.InspectionResults;
using XplorePlane.Services.MainViewport; using XplorePlane.Services.MainViewport;
using XplorePlane.ViewModels; using XplorePlane.ViewModels;
@@ -23,34 +26,63 @@ namespace XplorePlane.Services.Cnc
private readonly IInspectionResultStore _store; private readonly IInspectionResultStore _store;
private readonly ILoggerService _logger; private readonly ILoggerService _logger;
private readonly IMainViewportService _mainViewportService; private readonly IMainViewportService _mainViewportService;
private readonly IAppStateService _appStateService;
private readonly IPipelineExecutionService _pipelineExecutionService; private readonly IPipelineExecutionService _pipelineExecutionService;
private readonly IImageProcessingService _imageProcessingService; private readonly IImageProcessingService _imageProcessingService;
private readonly IEventAggregator _eventAggregator;
// Task 4.2: volatile field so reads/writes are not reordered across threads
private volatile CancellationTokenSource _executionCts;
public CncExecutionService( public CncExecutionService(
IInspectionResultStore store, IInspectionResultStore store,
ILoggerService logger, ILoggerService logger,
IMainViewportService mainViewportService, IMainViewportService mainViewportService,
IAppStateService appStateService,
IPipelineExecutionService pipelineExecutionService, IPipelineExecutionService pipelineExecutionService,
IImageProcessingService imageProcessingService) IImageProcessingService imageProcessingService,
IEventAggregator eventAggregator)
{ {
_store = store ?? throw new ArgumentNullException(nameof(store)); _store = store ?? throw new ArgumentNullException(nameof(store));
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
_mainViewportService = mainViewportService; _mainViewportService = mainViewportService;
_appStateService = appStateService ?? throw new ArgumentNullException(nameof(appStateService));
_pipelineExecutionService = pipelineExecutionService; _pipelineExecutionService = pipelineExecutionService;
_imageProcessingService = imageProcessingService; _imageProcessingService = imageProcessingService;
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
// Task 4.3: subscribe to DetectorDisconnectedEvent on a background thread
_eventAggregator.GetEvent<DetectorDisconnectedEvent>()
.Subscribe(OnDetectorDisconnected, ThreadOption.BackgroundThread);
}
// Task 4.3: callback cancel the running execution when the detector disconnects
private void OnDetectorDisconnected()
{
var cts = _executionCts;
if (cts == null) return;
try
{
cts.Cancel();
_logger.ForModule<CncExecutionService>().Warn("探测器断连,已取消当前 CNC 执行");
}
catch (ObjectDisposedException) { }
} }
public async Task ExecuteAsync(CncProgram program, IProgress<CncNodeExecutionProgress> progress, CancellationToken cancellationToken) public async Task ExecuteAsync(CncProgram program, IProgress<CncNodeExecutionProgress> progress, CancellationToken cancellationToken)
{ {
// Pre-cancellation check — do NOT call BeginRunAsync if already cancelled // Task 4.4: create a linked CTS so DetectorDisconnectedEvent can cancel independently
if (cancellationToken.IsCancellationRequested) using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_executionCts = linkedCts;
_mainViewportService?.SetCncRunning(true);
try
{
// Pre-cancellation check - do NOT call BeginRunAsync if already cancelled
if (linkedCts.Token.IsCancellationRequested)
return; return;
int inspectionNodeCount = program.Nodes.OfType<InspectionModuleNode>().Count(); int inspectionNodeCount = program.Nodes.OfType<InspectionModuleNode>().Count();
var sourceImage = TryGetSourceImage();
// 获取当前源图像(用于 run/source.bmp
var sourceImage = _mainViewportService?.LatestManualImage as BitmapSource
?? _mainViewportService?.CurrentDisplayImage as BitmapSource;
Guid runId; Guid runId;
try try
@@ -90,13 +122,13 @@ namespace XplorePlane.Services.Cnc
foreach (var node in program.Nodes.OrderBy(n => n.Index)) foreach (var node in program.Nodes.OrderBy(n => n.Index))
{ {
if (cancellationToken.IsCancellationRequested) if (linkedCts.Token.IsCancellationRequested)
{ {
cancelled = true; cancelled = true;
break; break;
} }
progress?.Report(new CncNodeExecutionProgress(node.Id, NodeExecutionState.Running)); progress?.Report(new CncNodeExecutionProgress(node.Id, NodeExecutionState.Running, ProgressPercent: 0));
bool nodeSucceeded = true; bool nodeSucceeded = true;
@@ -104,10 +136,67 @@ namespace XplorePlane.Services.Cnc
{ {
switch (node) switch (node)
{ {
case ReferencePointNode rp:
_logger.ForModule<CncExecutionService>().Info(
"Executing reference point node [{Index}] {Name} | " +
"StageX={StageX} StageY={StageY} SourceZ={SourceZ} DetectorZ={DetectorZ} " +
"DetectorSwing={DetectorSwing} FDD={FDD} FOD={FOD} Magnification={Magnification} " +
"StageRotation={StageRotation} FixtureRotation={FixtureRotation} " +
"RayOn={RayOn} Voltage={Voltage}kV Current={Current}uA",
rp.Index, rp.Name,
rp.StageX, rp.StageY, rp.SourceZ, rp.DetectorZ,
rp.DetectorSwing, rp.FDD, rp.FOD, rp.Magnification,
rp.StageRotation, rp.FixtureRotation,
rp.IsRayOn, rp.Voltage, rp.Current);
break;
case SavePositionNode sp:
_logger.ForModule<CncExecutionService>().Info(
"Executing save-position node [{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(
"Executing save node [{Index}] {Name} | " +
"StageX={StageX} StageY={StageY} SourceZ={SourceZ} DetectorZ={DetectorZ} " +
"DetectorSwing={DetectorSwing} FDD={FDD} FOD={FOD} Magnification={Magnification} " +
"RayOn={RayOn} Voltage={Voltage}kV Power={Power}W",
sn.Index, sn.Name,
sn.MotionState.StageX, sn.MotionState.StageY,
sn.MotionState.SourceZ, sn.MotionState.DetectorZ,
sn.MotionState.DetectorSwing, sn.MotionState.FDD,
sn.MotionState.FOD, sn.MotionState.Magnification,
sn.RaySourceState.IsOn, sn.RaySourceState.Voltage, sn.RaySourceState.Power);
break;
case SaveNodeWithImageNode sni:
_logger.ForModule<CncExecutionService>().Info(
"Executing save-with-image node [{Index}] {Name} | " +
"StageX={StageX} StageY={StageY} SourceZ={SourceZ} DetectorZ={DetectorZ} " +
"DetectorSwing={DetectorSwing} FDD={FDD} FOD={FOD} Magnification={Magnification} " +
"RayOn={RayOn} Voltage={Voltage}kV Power={Power}W ImageFile={ImageFile}",
sni.Index, sni.Name,
sni.MotionState.StageX, sni.MotionState.StageY,
sni.MotionState.SourceZ, sni.MotionState.DetectorZ,
sni.MotionState.DetectorSwing, sni.MotionState.FDD,
sni.MotionState.FOD, sni.MotionState.Magnification,
sni.RaySourceState.IsOn, sni.RaySourceState.Voltage, sni.RaySourceState.Power,
sni.ImageFileName);
break;
case WaitDelayNode waitNode: case WaitDelayNode waitNode:
try try
{ {
await ExecuteWaitDelayWithProgressAsync(waitNode, cancellationToken); await ExecuteWaitDelayWithProgressAsync(waitNode, progress, linkedCts.Token);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@@ -118,14 +207,14 @@ namespace XplorePlane.Services.Cnc
case PauseDialogNode pauseNode: case PauseDialogNode pauseNode:
await Application.Current.Dispatcher.InvokeAsync(() => await Application.Current.Dispatcher.InvokeAsync(() =>
MessageBox.Show(pauseNode.DialogMessage, pauseNode.DialogTitle)); MessageBox.Show(pauseNode.DialogMessage, pauseNode.DialogTitle));
if (cancellationToken.IsCancellationRequested) if (linkedCts.Token.IsCancellationRequested)
cancelled = true; cancelled = true;
break; break;
case InspectionModuleNode inspectionNode: case InspectionModuleNode inspectionNode:
try try
{ {
var img = await ExecuteInspectionNodeAsync(runId, inspectionNode, sourceImage, cancellationToken); var img = await ExecuteInspectionNodeAsync(runId, inspectionNode, sourceImage, linkedCts.Token);
if (img != null) lastResultImage = img; if (img != null) lastResultImage = img;
} }
catch (Exception ex) catch (Exception ex)
@@ -150,7 +239,10 @@ namespace XplorePlane.Services.Cnc
{ {
_logger.ForModule<CncExecutionService>().Error(ex, _logger.ForModule<CncExecutionService>().Error(ex,
"Unexpected error executing node '{0}' (Id={1})", node.Name, node.Id); "Unexpected error executing node '{0}' (Id={1})", node.Name, node.Id);
nodeSucceeded = false; if (linkedCts.Token.IsCancellationRequested)
cancelled = true;
else
nodeSucceeded = false;
} }
if (cancelled) if (cancelled)
@@ -159,8 +251,8 @@ namespace XplorePlane.Services.Cnc
break; break;
} }
// InspectionModuleNode 完成时携带结果图像,供 ViewModel 缓存到节点上 // Carry the latest inspection result image so the ViewModel can cache it on the node.
var nodeResultImage = (node is InspectionModuleNode) ? lastResultImage : null; var nodeResultImage = node is InspectionModuleNode ? lastResultImage : null;
var finalState = nodeSucceeded ? NodeExecutionState.Succeeded : NodeExecutionState.Failed; var finalState = nodeSucceeded ? NodeExecutionState.Succeeded : NodeExecutionState.Failed;
progress?.Report(new CncNodeExecutionProgress(node.Id, finalState, nodeResultImage)); progress?.Report(new CncNodeExecutionProgress(node.Id, finalState, nodeResultImage));
@@ -168,7 +260,7 @@ namespace XplorePlane.Services.Cnc
allSucceeded = false; allSucceeded = false;
} }
endLoop: endLoop:
bool? overallPass = cancelled ? null : (bool?)allSucceeded; bool? overallPass = cancelled ? null : (bool?)allSucceeded;
@@ -181,6 +273,31 @@ namespace XplorePlane.Services.Cnc
_logger.ForModule<CncExecutionService>().Error(ex, _logger.ForModule<CncExecutionService>().Error(ex,
"Failed to complete inspection run '{0}'", runId); "Failed to complete inspection run '{0}'", runId);
} }
} // end try
finally
{
_executionCts = null;
_mainViewportService?.SetCncRunning(false);
}
}
private BitmapSource TryGetSourceImage()
{
var viewportImage = _mainViewportService?.LatestManualImage as BitmapSource
?? _mainViewportService?.CurrentDisplayImage as BitmapSource;
if (viewportImage != null)
return viewportImage;
var detectorFrame = _appStateService?.LatestDetectorFrame;
if (detectorFrame?.ImageData == null || detectorFrame.Width <= 0 || detectorFrame.Height <= 0)
return null;
var bitmap = XP.Common.Converters.ImageConverter.ConvertGray16ToBitmapSource(
detectorFrame.ImageData,
(int)detectorFrame.Width,
(int)detectorFrame.Height);
bitmap.Freeze();
return bitmap;
} }
private async Task<BitmapSource> ExecuteInspectionNodeAsync( private async Task<BitmapSource> ExecuteInspectionNodeAsync(
@@ -211,10 +328,8 @@ namespace XplorePlane.Services.Cnc
}; };
} }
// 构建资产列表
var assets = new System.Collections.Generic.List<InspectionAssetWriteRequest>(); var assets = new System.Collections.Generic.List<InspectionAssetWriteRequest>();
// input.bmp — 当前源图像
if (sourceImage != null) if (sourceImage != null)
{ {
assets.Add(new InspectionAssetWriteRequest assets.Add(new InspectionAssetWriteRequest
@@ -227,7 +342,6 @@ namespace XplorePlane.Services.Cnc
}); });
} }
// result_overlay.bmp — 执行流水线后的结果图像
BitmapSource resultImage = null; BitmapSource resultImage = null;
if (_pipelineExecutionService != null && inspectionNode.Pipeline?.Nodes?.Count > 0 && sourceImage != null) if (_pipelineExecutionService != null && inspectionNode.Pipeline?.Nodes?.Count > 0 && sourceImage != null)
{ {
@@ -248,9 +362,7 @@ namespace XplorePlane.Services.Cnc
Height = resultImage.PixelHeight Height = resultImage.PixelHeight
}); });
nodeResult.Status = InspectionNodeStatus.Succeeded; nodeResult.Status = InspectionNodeStatus.Succeeded;
_mainViewportService?.SetManualImage(resultImage, $"CNC Node: {inspectionNode.Name}");
// 执行完立即更新主视口
_mainViewportService?.SetManualImage(resultImage, $"CNC节点:{inspectionNode.Name}");
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -265,21 +377,20 @@ namespace XplorePlane.Services.Cnc
return resultImage; return resultImage;
} }
private System.Collections.Generic.IEnumerable<ViewModels.PipelineNodeViewModel> BuildPipelineNodeViewModels(PipelineModel pipeline) private System.Collections.Generic.IEnumerable<PipelineNodeViewModel> BuildPipelineNodeViewModels(PipelineModel pipeline)
{ {
var nodes = new System.Collections.Generic.List<ViewModels.PipelineNodeViewModel>(); var nodes = new System.Collections.Generic.List<PipelineNodeViewModel>();
if (pipeline?.Nodes == null) return nodes; if (pipeline?.Nodes == null) return nodes;
foreach (var nodeModel in pipeline.Nodes.OrderBy(n => n.Order)) foreach (var nodeModel in pipeline.Nodes.OrderBy(n => n.Order))
{ {
var displayName = _imageProcessingService?.GetProcessorDisplayName(nodeModel.OperatorKey) ?? nodeModel.OperatorKey; var displayName = _imageProcessingService?.GetProcessorDisplayName(nodeModel.OperatorKey) ?? nodeModel.OperatorKey;
var vm = new ViewModels.PipelineNodeViewModel(nodeModel.OperatorKey, displayName, string.Empty) var vm = new PipelineNodeViewModel(nodeModel.OperatorKey, displayName, string.Empty)
{ {
Order = nodeModel.Order, Order = nodeModel.Order,
IsEnabled = nodeModel.IsEnabled IsEnabled = nodeModel.IsEnabled
}; };
// 加载参数定义并恢复保存的值
if (_imageProcessingService != null) if (_imageProcessingService != null)
{ {
var paramDefs = _imageProcessingService.GetProcessorParameters(nodeModel.OperatorKey); var paramDefs = _imageProcessingService.GetProcessorParameters(nodeModel.OperatorKey);
@@ -287,7 +398,7 @@ namespace XplorePlane.Services.Cnc
{ {
foreach (var def in paramDefs) foreach (var def in paramDefs)
{ {
var paramVm = new ViewModels.ProcessorParameterVM(def); var paramVm = new ProcessorParameterVM(def);
if (nodeModel.Parameters != null && nodeModel.Parameters.TryGetValue(def.Name, out var saved)) if (nodeModel.Parameters != null && nodeModel.Parameters.TryGetValue(def.Name, out var saved))
paramVm.Value = ConvertSavedValue(saved, def.ValueType); paramVm.Value = ConvertSavedValue(saved, def.ValueType);
vm.Parameters.Add(paramVm); vm.Parameters.Add(paramVm);
@@ -300,19 +411,16 @@ namespace XplorePlane.Services.Cnc
return nodes; return nodes;
} }
/// <summary>
/// 将 JSON 反序列化后的 JsonElement 转换为参数所需的实际类型。
/// </summary>
private static object ConvertSavedValue(object savedValue, Type targetType) private static object ConvertSavedValue(object savedValue, Type targetType)
{ {
if (savedValue is not System.Text.Json.JsonElement jsonElement) if (savedValue is not JsonElement jsonElement)
return savedValue; return savedValue;
try try
{ {
if (targetType == typeof(int)) return jsonElement.GetInt32(); if (targetType == typeof(int)) return jsonElement.GetInt32();
if (targetType == typeof(double)) return jsonElement.GetDouble(); if (targetType == typeof(double)) return jsonElement.GetDouble();
if (targetType == typeof(bool)) return jsonElement.GetBoolean(); if (targetType == typeof(bool)) return jsonElement.GetBoolean();
if (targetType == typeof(string)) return jsonElement.GetString() ?? string.Empty; if (targetType == typeof(string)) return jsonElement.GetString() ?? string.Empty;
return jsonElement.ToString(); return jsonElement.ToString();
} }
@@ -331,47 +439,33 @@ namespace XplorePlane.Services.Cnc
return ms.ToArray(); return ms.ToArray();
} }
private static async Task ExecuteWaitDelayWithProgressAsync(WaitDelayNode waitNode, CancellationToken cancellationToken) private static async Task ExecuteWaitDelayWithProgressAsync(
WaitDelayNode waitNode,
IProgress<CncNodeExecutionProgress> progress,
CancellationToken cancellationToken)
{ {
int totalMs = waitNode.DelayMilliseconds; int totalMs = waitNode.DelayMilliseconds;
if (totalMs <= 0) if (totalMs <= 0)
{
progress?.Report(new CncNodeExecutionProgress(waitNode.Id, NodeExecutionState.Running, ProgressPercent: 100));
return; return;
}
const int tickMs = 50; const int tickMs = 50;
ProgressWindow progressWindow = null;
await Application.Current.Dispatcher.InvokeAsync(() => int elapsed = 0;
while (elapsed < totalMs)
{ {
progressWindow = new ProgressWindow( cancellationToken.ThrowIfCancellationRequested();
title: "延时等待",
message: $"节点:{waitNode.Name} 等待 {totalMs / 1000.0:F1} 秒",
isCancelable: false);
progressWindow.Owner = Application.Current.MainWindow;
progressWindow.Show();
});
try int remaining = totalMs - elapsed;
{ int delay = Math.Min(tickMs, remaining);
int elapsed = 0; await Task.Delay(delay, cancellationToken);
while (elapsed < totalMs) elapsed += delay;
{ progress?.Report(new CncNodeExecutionProgress(
cancellationToken.ThrowIfCancellationRequested(); waitNode.Id,
NodeExecutionState.Running,
int remaining = totalMs - elapsed; ProgressPercent: elapsed * 100d / totalMs));
int delay = Math.Min(tickMs, remaining);
await Task.Delay(delay, cancellationToken);
elapsed += delay;
double pct = Math.Min(100.0 * elapsed / totalMs, 100.0);
double remainSec = Math.Max(0, (totalMs - elapsed) / 1000.0);
string msg = $"节点:{waitNode.Name} 剩余 {remainSec:F1} 秒";
progressWindow?.UpdateProgress(msg, pct);
}
}
finally
{
progressWindow?.Close();
} }
} }
} }
+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,
@@ -17,6 +17,11 @@ namespace XplorePlane.Services.Cnc
/// <summary> /// <summary>
/// Progress report for a single CNC node execution. /// Progress report for a single CNC node execution.
/// ResultImage is non-null when an InspectionModuleNode produces output. /// ResultImage is non-null when an InspectionModuleNode produces output.
/// ProgressPercent is used by long-running nodes such as WaitDelayNode.
/// </summary> /// </summary>
public record CncNodeExecutionProgress(Guid NodeId, NodeExecutionState State, BitmapSource ResultImage = null); public record CncNodeExecutionProgress(
Guid NodeId,
NodeExecutionState State,
BitmapSource ResultImage = null,
double? ProgressPercent = null);
} }
@@ -10,6 +10,7 @@ using System.Threading.Tasks;
using XP.Common.Database.Interfaces; using XP.Common.Database.Interfaces;
using XP.Common.Logging.Interfaces; using XP.Common.Logging.Interfaces;
using XplorePlane.Models; using XplorePlane.Models;
using XplorePlane.Services.Storage;
namespace XplorePlane.Services.InspectionResults namespace XplorePlane.Services.InspectionResults
{ {
@@ -160,6 +161,14 @@ WHERE run_id = @run_id";
"InspectionResults"); "InspectionResults");
} }
public InspectionResultStore(IDbContext db, ILoggerService logger, IXpDataPathService dataPathService)
: this(
db,
logger,
Path.Combine(dataPathService?.DataPath ?? throw new ArgumentNullException(nameof(dataPathService)), "InspectionResults"))
{
}
public async Task BeginRunAsync(InspectionRunRecord runRecord, InspectionAssetWriteRequest runSourceAsset = null) public async Task BeginRunAsync(InspectionRunRecord runRecord, InspectionAssetWriteRequest runSourceAsset = null)
{ {
ArgumentNullException.ThrowIfNull(runRecord); ArgumentNullException.ThrowIfNull(runRecord);
@@ -29,14 +29,32 @@ namespace XplorePlane.Services.MainViewport
IEventAggregator eventAggregator, IEventAggregator eventAggregator,
IMainViewportService mainViewportService, IMainViewportService mainViewportService,
ILoggerService logger) ILoggerService logger)
: this(eventAggregator, mainViewportService, logger,
ReadInt("DetectorPipeline:AcquireQueueCapacity", 16, 1),
ReadInt("DetectorPipeline:ProcessQueueCapacity", 8, 1),
ReadInt("DetectorPipeline:ProcessEveryNFrames", 1, 1))
{
}
/// <summary>
/// Internal constructor for testing: accepts capacity and sampling values directly,
/// bypassing App.config reads.
/// </summary>
internal DetectorFramePipelineService(
IEventAggregator eventAggregator,
IMainViewportService mainViewportService,
ILoggerService logger,
int acquireQueueCapacity,
int processQueueCapacity,
int processEveryNFrames)
{ {
ArgumentNullException.ThrowIfNull(eventAggregator); ArgumentNullException.ThrowIfNull(eventAggregator);
_mainViewportService = mainViewportService ?? throw new ArgumentNullException(nameof(mainViewportService)); _mainViewportService = mainViewportService ?? throw new ArgumentNullException(nameof(mainViewportService));
_logger = logger?.ForModule<DetectorFramePipelineService>() ?? throw new ArgumentNullException(nameof(logger)); _logger = logger?.ForModule<DetectorFramePipelineService>() ?? throw new ArgumentNullException(nameof(logger));
AcquireQueueCapacity = ReadInt("DetectorPipeline:AcquireQueueCapacity", 16, 1); AcquireQueueCapacity = Math.Max(1, acquireQueueCapacity);
ProcessQueueCapacity = ReadInt("DetectorPipeline:ProcessQueueCapacity", 8, 1); ProcessQueueCapacity = Math.Max(1, processQueueCapacity);
ProcessEveryNFrames = ReadInt("DetectorPipeline:ProcessEveryNFrames", 1, 1); ProcessEveryNFrames = Math.Max(1, processEveryNFrames);
eventAggregator.GetEvent<ImageCapturedEvent>() eventAggregator.GetEvent<ImageCapturedEvent>()
.Subscribe(OnImageCaptured, ThreadOption.BackgroundThread); .Subscribe(OnImageCaptured, ThreadOption.BackgroundThread);
@@ -7,6 +7,7 @@ namespace XplorePlane.Services.MainViewport
{ {
MainViewportSourceMode CurrentSourceMode { get; } MainViewportSourceMode CurrentSourceMode { get; }
bool IsRealtimeDisplayEnabled { get; } bool IsRealtimeDisplayEnabled { get; }
bool IsCncRunning { get; }
ImageSource CurrentDisplayImage { get; } ImageSource CurrentDisplayImage { get; }
string CurrentDisplayInfo { get; } string CurrentDisplayInfo { get; }
ImageSource LatestDetectorImage { get; } ImageSource LatestDetectorImage { get; }
@@ -21,5 +22,11 @@ namespace XplorePlane.Services.MainViewport
void UpdateDetectorFrame(DetectorFrame frame); void UpdateDetectorFrame(DetectorFrame frame);
void SetManualImage(ImageSource image, string filePath); void SetManualImage(ImageSource image, string filePath);
/// <summary>
/// 通知 MainViewportService 当前 CNC 运行状态。
/// CNC 开始运行时传入 true,结束时传入 false。
/// </summary>
void SetCncRunning(bool isRunning);
} }
} }
@@ -13,6 +13,7 @@ namespace XplorePlane.Services.MainViewport
private MainViewportSourceMode _currentSourceMode = MainViewportSourceMode.LiveDetector; private MainViewportSourceMode _currentSourceMode = MainViewportSourceMode.LiveDetector;
private bool _isRealtimeDisplayEnabled; private bool _isRealtimeDisplayEnabled;
private bool _isCncRunning;
private ImageSource _currentDisplayImage; private ImageSource _currentDisplayImage;
private string _currentDisplayInfo = "等待探测器图像..."; private string _currentDisplayInfo = "等待探测器图像...";
private ImageSource _latestDetectorImage; private ImageSource _latestDetectorImage;
@@ -48,6 +49,17 @@ namespace XplorePlane.Services.MainViewport
} }
} }
public bool IsCncRunning
{
get
{
lock (_syncRoot)
{
return _isCncRunning;
}
}
}
public ImageSource CurrentDisplayImage public ImageSource CurrentDisplayImage
{ {
get get
@@ -99,11 +111,21 @@ namespace XplorePlane.Services.MainViewport
bool changed; bool changed;
lock (_syncRoot) lock (_syncRoot)
{ {
changed = _isRealtimeDisplayEnabled != isEnabled; if (!isEnabled && _isCncRunning)
{
_logger.Warn("CNC 正在运行,忽略 SetRealtimeDisplayEnabled(false) 调用");
return;
}
changed = _isRealtimeDisplayEnabled != isEnabled
|| (isEnabled && _currentSourceMode == MainViewportSourceMode.ManualImage);
_isRealtimeDisplayEnabled = isEnabled; _isRealtimeDisplayEnabled = isEnabled;
if (_currentSourceMode == MainViewportSourceMode.LiveDetector && isEnabled) if (isEnabled)
{ {
// 开启实时:无论当前是 ManualImage 还是 LiveDetector,都切回实时帧显示
_currentSourceMode = MainViewportSourceMode.LiveDetector;
ApplyLiveDetectorDisplay_NoLock(); ApplyLiveDetectorDisplay_NoLock();
} }
} }
@@ -173,6 +195,12 @@ namespace XplorePlane.Services.MainViewport
lock (_syncRoot) lock (_syncRoot)
{ {
if (_isCncRunning)
{
_logger.Warn("CNC 正在运行,忽略 SetManualImage 调用");
return;
}
_latestManualImage = image; _latestManualImage = image;
_latestManualInfo = $"手动加载图像 {fileName}"; _latestManualInfo = $"手动加载图像 {fileName}";
_currentSourceMode = MainViewportSourceMode.ManualImage; _currentSourceMode = MainViewportSourceMode.ManualImage;
@@ -184,6 +212,23 @@ namespace XplorePlane.Services.MainViewport
RaiseStateChanged(); RaiseStateChanged();
} }
public void SetCncRunning(bool isRunning)
{
bool modeChanged = false;
lock (_syncRoot)
{
_isCncRunning = isRunning;
if (isRunning && _currentSourceMode == MainViewportSourceMode.ManualImage)
{
_currentSourceMode = MainViewportSourceMode.LiveDetector;
ApplyLiveDetectorDisplay_NoLock();
modeChanged = true;
}
}
_logger.Info("CNC 运行状态已更新:{IsRunning}", isRunning);
if (modeChanged) RaiseStateChanged();
}
private void ApplyLiveDetectorDisplay_NoLock() private void ApplyLiveDetectorDisplay_NoLock()
{ {
_currentDisplayImage = _latestDetectorImage; _currentDisplayImage = _latestDetectorImage;
@@ -6,6 +6,7 @@ using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading.Tasks; using System.Threading.Tasks;
using XplorePlane.Models; using XplorePlane.Models;
using XplorePlane.Services.Storage;
namespace XplorePlane.Services namespace XplorePlane.Services
{ {
@@ -29,6 +30,11 @@ namespace XplorePlane.Services
"XplorePlane", "Pipelines"); "XplorePlane", "Pipelines");
} }
public PipelinePersistenceService(IImageProcessingService imageProcessingService, IXpDataPathService dataPathService)
: this(imageProcessingService, dataPathService?.ToolsPath)
{
}
public async Task SaveAsync(PipelineModel pipeline, string filePath) public async Task SaveAsync(PipelineModel pipeline, string filePath)
{ {
if (pipeline == null) throw new ArgumentNullException(nameof(pipeline)); if (pipeline == null) throw new ArgumentNullException(nameof(pipeline));
@@ -0,0 +1,23 @@
using System;
namespace XplorePlane.Services.Storage
{
public interface IXpDataPathService
{
string DefaultRootPath { get; }
string RootPath { get; }
string PlanPath { get; }
string ToolsPath { get; }
string DataPath { get; }
string ReportPath { get; }
string LegacyInspectionDataPath { get; }
void SaveRootPath(string rootPath);
}
}
@@ -0,0 +1,145 @@
using System;
using System.Configuration;
using System.IO;
using XP.Common.Logging.Interfaces;
namespace XplorePlane.Services.Storage
{
public class XpDataPathService : IXpDataPathService
{
internal const string RootPathSettingKey = "XpData:RootPath";
private const string DefaultRoot = @"D:\XPData";
private readonly ILoggerService _logger;
private readonly object _syncRoot = new();
private string _rootPath;
public XpDataPathService(ILoggerService logger)
{
_logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule<XpDataPathService>();
_rootPath = LoadConfiguredRootPath();
EnsureManagedDirectories(_rootPath);
}
public string DefaultRootPath => DefaultRoot;
public string RootPath
{
get
{
lock (_syncRoot)
{
return _rootPath;
}
}
}
public string PlanPath => EnsureSubdirectory("Plan");
public string ToolsPath => EnsureSubdirectory("Tools");
public string DataPath => EnsureSubdirectory("Data");
public string ReportPath => EnsureSubdirectory("Report");
public string LegacyInspectionDataPath => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"XplorePlane",
"InspectionResults");
public void SaveRootPath(string rootPath)
{
var normalizedRootPath = NormalizeRootPath(rootPath);
EnsureManagedDirectories(normalizedRootPath);
try
{
var config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
var appSettings = config.AppSettings.Settings;
if (appSettings[RootPathSettingKey] == null)
{
appSettings.Add(RootPathSettingKey, normalizedRootPath);
}
else
{
appSettings[RootPathSettingKey].Value = normalizedRootPath;
}
config.Save(ConfigurationSaveMode.Modified);
ConfigurationManager.RefreshSection("appSettings");
}
catch (ConfigurationErrorsException ex)
{
_logger.Error(ex, "保存 XP 数据根目录失败:配置文件写入异常");
throw new InvalidOperationException($"保存数据根目录失败:{ex.Message}", ex);
}
catch (UnauthorizedAccessException ex)
{
_logger.Error(ex, "保存 XP 数据根目录失败:无权写入配置文件");
throw new InvalidOperationException("保存数据根目录失败:没有配置文件写入权限。", ex);
}
lock (_syncRoot)
{
_rootPath = normalizedRootPath;
}
}
private string EnsureSubdirectory(string directoryName)
{
string rootPath;
lock (_syncRoot)
{
rootPath = _rootPath;
}
EnsureManagedDirectories(rootPath);
return Path.Combine(rootPath, directoryName);
}
private string LoadConfiguredRootPath()
{
try
{
return NormalizeRootPath(ConfigurationManager.AppSettings[RootPathSettingKey]);
}
catch (Exception ex)
{
_logger.Warn("读取 XP 数据根目录失败,回退默认目录:{Message}", ex.Message);
return NormalizeRootPath(null);
}
}
private static string NormalizeRootPath(string rootPath)
{
var candidate = string.IsNullOrWhiteSpace(rootPath) ? DefaultRoot : rootPath.Trim();
try
{
var normalized = Path.GetFullPath(candidate)
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
if (!Path.IsPathRooted(normalized))
{
return DefaultRoot;
}
return normalized;
}
catch
{
return DefaultRoot;
}
}
private static void EnsureManagedDirectories(string rootPath)
{
Directory.CreateDirectory(rootPath);
Directory.CreateDirectory(Path.Combine(rootPath, "Plan"));
Directory.CreateDirectory(Path.Combine(rootPath, "Tools"));
Directory.CreateDirectory(Path.Combine(rootPath, "Data"));
Directory.CreateDirectory(Path.Combine(rootPath, "Report"));
}
}
}
@@ -16,6 +16,8 @@ using XplorePlane.Events;
using XplorePlane.Models; using XplorePlane.Models;
using XplorePlane.Services.AppState; using XplorePlane.Services.AppState;
using XplorePlane.Services.Cnc; using XplorePlane.Services.Cnc;
using XplorePlane.Services;
using XplorePlane.Services.Storage;
namespace XplorePlane.ViewModels.Cnc namespace XplorePlane.ViewModels.Cnc
{ {
@@ -28,6 +30,8 @@ namespace XplorePlane.ViewModels.Cnc
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly ILoggerService _logger; private readonly ILoggerService _logger;
private readonly ICncExecutionService _cncExecutionService; private readonly ICncExecutionService _cncExecutionService;
private readonly IXpDataPathService _dataPathService;
private readonly IPipelinePersistenceService _pipelinePersistenceService;
private CncProgram _currentProgram; private CncProgram _currentProgram;
private ObservableCollection<CncNodeViewModel> _nodes; private ObservableCollection<CncNodeViewModel> _nodes;
@@ -51,13 +55,17 @@ namespace XplorePlane.ViewModels.Cnc
IAppStateService appStateService, IAppStateService appStateService,
IEventAggregator eventAggregator, IEventAggregator eventAggregator,
ILoggerService logger, ILoggerService logger,
ICncExecutionService cncExecutionService) ICncExecutionService cncExecutionService,
IXpDataPathService dataPathService,
IPipelinePersistenceService pipelinePersistenceService)
{ {
_cncProgramService = cncProgramService ?? throw new ArgumentNullException(nameof(cncProgramService)); _cncProgramService = cncProgramService ?? throw new ArgumentNullException(nameof(cncProgramService));
ArgumentNullException.ThrowIfNull(appStateService); ArgumentNullException.ThrowIfNull(appStateService);
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule<CncEditorViewModel>(); _logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule<CncEditorViewModel>();
_cncExecutionService = cncExecutionService ?? throw new ArgumentNullException(nameof(cncExecutionService)); _cncExecutionService = cncExecutionService ?? throw new ArgumentNullException(nameof(cncExecutionService));
_dataPathService = dataPathService ?? throw new ArgumentNullException(nameof(dataPathService));
_pipelinePersistenceService = pipelinePersistenceService ?? throw new ArgumentNullException(nameof(pipelinePersistenceService));
_nodes = new ObservableCollection<CncNodeViewModel>(); _nodes = new ObservableCollection<CncNodeViewModel>();
_treeNodes = new ObservableCollection<CncNodeViewModel>(); _treeNodes = new ObservableCollection<CncNodeViewModel>();
@@ -331,7 +339,8 @@ namespace XplorePlane.ViewModels.Cnc
Title = "保存 CNC 程序", Title = "保存 CNC 程序",
Filter = "CNC 程序文件 (*.xp)|*.xp|所有文件 (*.*)|*.*", Filter = "CNC 程序文件 (*.xp)|*.xp|所有文件 (*.*)|*.*",
DefaultExt = ".xp", DefaultExt = ".xp",
FileName = _currentProgram.Name FileName = _currentProgram.Name,
InitialDirectory = GetPlanDirectory()
}; };
if (dlg.ShowDialog() != true) if (dlg.ShowDialog() != true)
@@ -355,7 +364,8 @@ namespace XplorePlane.ViewModels.Cnc
{ {
Title = "加载 CNC 程序", Title = "加载 CNC 程序",
Filter = "CNC 程序文件 (*.xp)|*.xp|所有文件 (*.*)|*.*", Filter = "CNC 程序文件 (*.xp)|*.xp|所有文件 (*.*)|*.*",
DefaultExt = ".xp" DefaultExt = ".xp",
InitialDirectory = GetPlanDirectory()
}; };
if (dlg.ShowDialog() != true) if (dlg.ShowDialog() != true)
@@ -407,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;
@@ -415,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);
@@ -438,6 +448,64 @@ namespace XplorePlane.ViewModels.Cnc
} }
} }
public async Task InsertInspectionModuleFromPipelineFileAsync(string filePath)
{
if (IsRunning)
return;
if (string.IsNullOrWhiteSpace(filePath))
throw new ArgumentException("检测模块文件路径不能为空。", nameof(filePath));
if (!File.Exists(filePath))
throw new FileNotFoundException("检测模块文件不存在。", filePath);
if (_currentProgram == null)
{
ExecuteNewProgram();
}
var pipeline = await _pipelinePersistenceService.LoadAsync(filePath);
try
{
var node = _cncProgramService.CreateNode(CncNodeType.InspectionModule);
if (node is not InspectionModuleNode inspectionModuleNode)
throw new InvalidOperationException("无法创建检测模块节点。");
var pipelineName = string.IsNullOrWhiteSpace(pipeline.Name)
? Path.GetFileNameWithoutExtension(filePath)
: pipeline.Name;
pipeline.Name = pipelineName;
var configuredNode = inspectionModuleNode with
{
Pipeline = pipeline,
Name = pipelineName
};
int afterIndex = ResolveInsertAfterIndex(CncNodeType.InspectionModule);
_currentProgram = _cncProgramService.InsertNode(_currentProgram, afterIndex, configuredNode);
_preferredSelectedNodeId = configuredNode.Id;
ClearPendingInsertAnchor();
OnProgramEdited();
StatusMessage = $"已插入检测模块:{pipelineName}";
_logger.Info("Inserted built-in inspection module from file: {FilePath}", filePath);
}
catch (InvalidOperationException ex)
{
_logger.Warn("Built-in inspection module insertion blocked: {Message}", ex.Message);
throw;
}
}
private string GetPlanDirectory()
{
var directory = _dataPathService.PlanPath;
Directory.CreateDirectory(directory);
return directory;
}
private static string Esc(string value) private static string Esc(string value)
{ {
if (string.IsNullOrEmpty(value)) return string.Empty; if (string.IsNullOrEmpty(value)) return string.Empty;
@@ -455,6 +523,7 @@ namespace XplorePlane.ViewModels.Cnc
private async Task ExecuteRunAsync() private async Task ExecuteRunAsync()
{ {
_cts = new CancellationTokenSource(); _cts = new CancellationTokenSource();
ResetAllNodeStates();
IsRunning = true; IsRunning = true;
HasExecutionError = false; HasExecutionError = false;
ExecutionError = null; ExecutionError = null;
@@ -499,6 +568,7 @@ namespace XplorePlane.ViewModels.Cnc
if (nodeVm != null) if (nodeVm != null)
{ {
nodeVm.ExecutionState = progress.State; nodeVm.ExecutionState = progress.State;
nodeVm.ExecutionProgressPercent = progress.ProgressPercent ?? (progress.State == NodeExecutionState.Succeeded ? 100d : 0d);
if (progress.State == NodeExecutionState.Running) if (progress.State == NodeExecutionState.Running)
StatusMessage = $"正在执行节点:{nodeVm.Name}{nodeVm.Index + 1}/{_currentProgram?.Nodes?.Count ?? 0}"; StatusMessage = $"正在执行节点:{nodeVm.Name}{nodeVm.Index + 1}/{_currentProgram?.Nodes?.Count ?? 0}";
else if (progress.State == NodeExecutionState.Succeeded) else if (progress.State == NodeExecutionState.Succeeded)
@@ -519,7 +589,10 @@ namespace XplorePlane.ViewModels.Cnc
private void ResetAllNodeStates() private void ResetAllNodeStates()
{ {
foreach (var node in Nodes) foreach (var node in Nodes)
{
node.ExecutionState = NodeExecutionState.Idle; node.ExecutionState = NodeExecutionState.Idle;
node.ExecutionProgressPercent = 0;
}
} }
private void RaiseEditCommandsCanExecuteChanged() private void RaiseEditCommandsCanExecuteChanged()
@@ -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;
@@ -37,6 +37,7 @@ namespace XplorePlane.ViewModels.Cnc
private string _statusMessage = "请选择检测模块以编辑其流水线。"; private string _statusMessage = "请选择检测模块以编辑其流水线。";
private string _pipelineFileDisplayName = "未命名模块.xpm"; private string _pipelineFileDisplayName = "未命名模块.xpm";
private string _currentFilePath; private string _currentFilePath;
private PipelineNodeViewModel _executionEndNode;
private bool _isSynchronizing; private bool _isSynchronizing;
private CancellationTokenSource _debounceCts; private CancellationTokenSource _debounceCts;
@@ -65,6 +66,8 @@ namespace XplorePlane.ViewModels.Cnc
RemoveOperatorCommand = new DelegateCommand<PipelineNodeViewModel>(RemoveOperator); RemoveOperatorCommand = new DelegateCommand<PipelineNodeViewModel>(RemoveOperator);
ReorderOperatorCommand = new DelegateCommand<PipelineReorderArgs>(ReorderOperator); ReorderOperatorCommand = new DelegateCommand<PipelineReorderArgs>(ReorderOperator);
ToggleOperatorEnabledCommand = new DelegateCommand<PipelineNodeViewModel>(ToggleOperatorEnabled); ToggleOperatorEnabledCommand = new DelegateCommand<PipelineNodeViewModel>(ToggleOperatorEnabled);
ExecuteToNodeCommand = new DelegateCommand<PipelineNodeViewModel>(ExecuteToNode);
ClearExecutionRangeCommand = new DelegateCommand(ClearExecutionRange);
MoveNodeUpCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeUp); MoveNodeUpCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeUp);
MoveNodeDownCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeDown); MoveNodeDownCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeDown);
NewPipelineCommand = new DelegateCommand(NewPipeline); NewPipelineCommand = new DelegateCommand(NewPipeline);
@@ -98,6 +101,16 @@ namespace XplorePlane.ViewModels.Cnc
private set => SetProperty(ref _pipelineFileDisplayName, value); private set => SetProperty(ref _pipelineFileDisplayName, value);
} }
public PipelineNodeViewModel ExecutionEndNode
{
get => _executionEndNode;
private set
{
if (SetProperty(ref _executionEndNode, value))
UpdateExecutionRangeState();
}
}
public bool HasActiveModule => _activeModuleNode?.IsInspectionModule == true; public bool HasActiveModule => _activeModuleNode?.IsInspectionModule == true;
public Visibility EditorVisibility => HasActiveModule ? Visibility.Visible : Visibility.Collapsed; public Visibility EditorVisibility => HasActiveModule ? Visibility.Visible : Visibility.Collapsed;
@@ -112,6 +125,10 @@ namespace XplorePlane.ViewModels.Cnc
public ICommand ToggleOperatorEnabledCommand { get; } public ICommand ToggleOperatorEnabledCommand { get; }
public ICommand ExecuteToNodeCommand { get; }
public ICommand ClearExecutionRangeCommand { get; }
public ICommand MoveNodeUpCommand { get; } public ICommand MoveNodeUpCommand { get; }
public ICommand MoveNodeDownCommand { get; } public ICommand MoveNodeDownCommand { get; }
@@ -179,6 +196,7 @@ namespace XplorePlane.ViewModels.Cnc
LoadNodeParameters(node, null); LoadNodeParameters(node, null);
PipelineNodes.Add(node); PipelineNodes.Add(node);
SelectedNode = node; SelectedNode = node;
UpdateExecutionRangeState();
PersistActiveModule($"已添加算子:{displayName}"); PersistActiveModule($"已添加算子:{displayName}");
} }
@@ -190,6 +208,10 @@ namespace XplorePlane.ViewModels.Cnc
var removedIndex = PipelineNodes.IndexOf(node); var removedIndex = PipelineNodes.IndexOf(node);
PipelineNodes.Remove(node); PipelineNodes.Remove(node);
RenumberNodes(); RenumberNodes();
if (ReferenceEquals(ExecutionEndNode, node))
ExecutionEndNode = null;
else
UpdateExecutionRangeState();
SelectNeighborAfterRemoval(removedIndex); SelectNeighborAfterRemoval(removedIndex);
PersistActiveModule($"已移除算子:{node.DisplayName}"); PersistActiveModule($"已移除算子:{node.DisplayName}");
@@ -206,6 +228,7 @@ namespace XplorePlane.ViewModels.Cnc
PipelineNodes.Move(index, index - 1); PipelineNodes.Move(index, index - 1);
RenumberNodes(); RenumberNodes();
UpdateExecutionRangeState();
PersistActiveModule($"已上移算子:{node.DisplayName}"); PersistActiveModule($"已上移算子:{node.DisplayName}");
} }
@@ -225,6 +248,7 @@ namespace XplorePlane.ViewModels.Cnc
var node = PipelineNodes[oldIndex]; var node = PipelineNodes[oldIndex];
PipelineNodes.Move(oldIndex, newIndex); PipelineNodes.Move(oldIndex, newIndex);
RenumberNodes(); RenumberNodes();
UpdateExecutionRangeState();
SelectedNode = node; SelectedNode = node;
PersistActiveModule($"已调整算子顺序:{node.DisplayName}"); PersistActiveModule($"已调整算子顺序:{node.DisplayName}");
} }
@@ -240,6 +264,7 @@ namespace XplorePlane.ViewModels.Cnc
PipelineNodes.Move(index, index + 1); PipelineNodes.Move(index, index + 1);
RenumberNodes(); RenumberNodes();
UpdateExecutionRangeState();
PersistActiveModule($"已下移算子:{node.DisplayName}"); PersistActiveModule($"已下移算子:{node.DisplayName}");
} }
@@ -255,6 +280,25 @@ namespace XplorePlane.ViewModels.Cnc
: $"已停用算子:{node.DisplayName}"); : $"已停用算子:{node.DisplayName}");
} }
private void ExecuteToNode(PipelineNodeViewModel node)
{
if (!HasActiveModule || node == null || !PipelineNodes.Contains(node))
return;
SelectedNode = node;
ExecutionEndNode = node;
PersistActiveModule($"已设置执行截止点:{node.DisplayName}");
}
private void ClearExecutionRange()
{
if (!HasActiveModule || ExecutionEndNode == null)
return;
ExecutionEndNode = null;
PersistActiveModule("已切换为执行全部节点");
}
private void NewPipeline() private void NewPipeline()
{ {
if (!HasActiveModule) if (!HasActiveModule)
@@ -262,6 +306,7 @@ namespace XplorePlane.ViewModels.Cnc
PipelineNodes.Clear(); PipelineNodes.Clear();
SelectedNode = null; SelectedNode = null;
ExecutionEndNode = null;
_currentFilePath = null; _currentFilePath = null;
PipelineFileDisplayName = GetActivePipelineFileDisplayName(); PipelineFileDisplayName = GetActivePipelineFileDisplayName();
PersistActiveModule("已为当前检测模块新建空流水线。"); PersistActiveModule("已为当前检测模块新建空流水线。");
@@ -326,6 +371,7 @@ namespace XplorePlane.ViewModels.Cnc
{ {
PipelineNodes.Clear(); PipelineNodes.Clear();
SelectedNode = null; SelectedNode = null;
ExecutionEndNode = null;
var orderedNodes = (pipeline?.Nodes ?? new List<PipelineNodeModel>()) var orderedNodes = (pipeline?.Nodes ?? new List<PipelineNodeModel>())
.OrderBy(node => node.Order) .OrderBy(node => node.Order)
@@ -346,6 +392,7 @@ namespace XplorePlane.ViewModels.Cnc
} }
SelectedNode = PipelineNodes.FirstOrDefault(); SelectedNode = PipelineNodes.FirstOrDefault();
UpdateExecutionRangeState();
if (string.IsNullOrEmpty(_currentFilePath)) if (string.IsNullOrEmpty(_currentFilePath))
PipelineFileDisplayName = GetActivePipelineFileDisplayName(); PipelineFileDisplayName = GetActivePipelineFileDisplayName();
StatusMessage = HasActiveModule StatusMessage = HasActiveModule
@@ -423,7 +470,7 @@ namespace XplorePlane.ViewModels.Cnc
try try
{ {
_logger.Info("[图像链路][CNC] ExecutePreviewAsync:开始执行,节点数={Count}", PipelineNodes.Count); _logger.Info("[图像链路][CNC] ExecutePreviewAsync:开始执行,节点数={Count}", PipelineNodes.Count);
var result = await _executionService.ExecutePipelineAsync(PipelineNodes, sourceImage, null, token); var result = await _executionService.ExecutePipelineAsync(GetNodesInExecutionScope(), sourceImage, null, token);
_logger.Info("[图像链路][CNC] ExecutePreviewAsync:执行完成,推送结果图像"); _logger.Info("[图像链路][CNC] ExecutePreviewAsync:执行完成,推送结果图像");
_mainViewportService.SetManualImage(result, string.Empty); _mainViewportService.SetManualImage(result, string.Empty);
_eventAggregator?.GetEvent<PipelinePreviewUpdatedEvent>() _eventAggregator?.GetEvent<PipelinePreviewUpdatedEvent>()
@@ -459,6 +506,15 @@ namespace XplorePlane.ViewModels.Cnc
}; };
} }
private IEnumerable<PipelineNodeViewModel> GetNodesInExecutionScope()
{
var orderedNodes = PipelineNodes.OrderBy(node => node.Order);
if (ExecutionEndNode == null)
return orderedNodes;
return orderedNodes.Where(node => node.Order <= ExecutionEndNode.Order);
}
private string GetActivePipelineName() private string GetActivePipelineName()
{ {
if (!HasActiveModule) if (!HasActiveModule)
@@ -492,6 +548,19 @@ namespace XplorePlane.ViewModels.Cnc
} }
} }
private void UpdateExecutionRangeState()
{
if (_executionEndNode != null && !PipelineNodes.Contains(_executionEndNode))
_executionEndNode = null;
var endOrder = _executionEndNode?.Order;
foreach (var node in PipelineNodes)
{
node.IsExecutionEndNode = endOrder.HasValue && node.Order == endOrder.Value;
node.IsSkippedByExecutionRange = endOrder.HasValue && node.Order > endOrder.Value;
}
}
private void SelectNeighborAfterRemoval(int removedIndex) private void SelectNeighborAfterRemoval(int removedIndex)
{ {
if (PipelineNodes.Count == 0) if (PipelineNodes.Count == 0)
+148 -60
View File
@@ -16,6 +16,7 @@ namespace XplorePlane.ViewModels.Cnc
private string _icon; private string _icon;
private bool _isExpanded = true; private bool _isExpanded = true;
private NodeExecutionState _executionState = NodeExecutionState.Idle; private NodeExecutionState _executionState = NodeExecutionState.Idle;
private double _executionProgressPercent;
/// <summary>执行后缓存的流水线输出图像(仅 InspectionModuleNode</summary> /// <summary>执行后缓存的流水线输出图像(仅 InspectionModuleNode</summary>
public BitmapSource ResultImage { get; set; } public BitmapSource ResultImage { get; set; }
@@ -72,6 +73,7 @@ namespace XplorePlane.ViewModels.Cnc
RaisePropertyChanged(nameof(IsRunningNode)); RaisePropertyChanged(nameof(IsRunningNode));
RaisePropertyChanged(nameof(IsSucceededNode)); RaisePropertyChanged(nameof(IsSucceededNode));
RaisePropertyChanged(nameof(IsFailedNode)); RaisePropertyChanged(nameof(IsFailedNode));
RaisePropertyChanged(nameof(IsDelayProgressVisible));
} }
} }
} }
@@ -79,6 +81,21 @@ namespace XplorePlane.ViewModels.Cnc
public bool IsRunningNode => ExecutionState == NodeExecutionState.Running; public bool IsRunningNode => ExecutionState == NodeExecutionState.Running;
public bool IsSucceededNode => ExecutionState == NodeExecutionState.Succeeded; public bool IsSucceededNode => ExecutionState == NodeExecutionState.Succeeded;
public bool IsFailedNode => ExecutionState == NodeExecutionState.Failed; public bool IsFailedNode => ExecutionState == NodeExecutionState.Failed;
public bool IsDelayProgressVisible => IsWaitDelay && IsRunningNode;
public double ExecutionProgressPercent
{
get => _executionProgressPercent;
set
{
if (SetProperty(ref _executionProgressPercent, Math.Clamp(value, 0d, 100d)))
{
RaisePropertyChanged(nameof(ExecutionProgressText));
}
}
}
public string ExecutionProgressText => $"{ExecutionProgressPercent:0}%";
public bool IsReferencePoint => _model is ReferencePointNode; public bool IsReferencePoint => _model is ReferencePointNode;
public bool IsSaveNode => _model is SaveNodeNode; public bool IsSaveNode => _model is SaveNodeNode;
@@ -99,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
@@ -409,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;
@@ -502,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
}; };
} }
@@ -540,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));
@@ -567,16 +648,23 @@ namespace XplorePlane.ViewModels.Cnc
RaisePropertyChanged(nameof(IsRunningNode)); RaisePropertyChanged(nameof(IsRunningNode));
RaisePropertyChanged(nameof(IsSucceededNode)); RaisePropertyChanged(nameof(IsSucceededNode));
RaisePropertyChanged(nameof(IsFailedNode)); RaisePropertyChanged(nameof(IsFailedNode));
RaisePropertyChanged(nameof(IsDelayProgressVisible));
RaisePropertyChanged(nameof(ExecutionProgressPercent));
RaisePropertyChanged(nameof(ExecutionProgressText));
} }
private enum MotionAxis private enum MotionAxis
{ {
XM, StageX,
YM, StageY,
ZT, SourceZ,
ZD, DetectorZ,
TiltD, DetectorSwing,
Dist StageRotation,
FixtureRotation,
FOD,
FDD,
Magnification
} }
} }
} }
@@ -23,6 +23,10 @@ namespace XplorePlane.ViewModels
ICommand ToggleOperatorEnabledCommand { get; } ICommand ToggleOperatorEnabledCommand { get; }
ICommand ExecuteToNodeCommand { get; }
ICommand ClearExecutionRangeCommand { get; }
ICommand MoveNodeUpCommand { get; } ICommand MoveNodeUpCommand { get; }
ICommand MoveNodeDownCommand { get; } ICommand MoveNodeDownCommand { get; }
@@ -36,6 +36,7 @@ namespace XplorePlane.ViewModels
{ {
private readonly IImageProcessingService _imageProcessingService; private readonly IImageProcessingService _imageProcessingService;
private string _searchText = string.Empty; private string _searchText = string.Empty;
private OperatorGroupViewModel _selectedGroup;
// UI 元数据(分类 + 图标)由 ProcessorUiMetadata 统一提供,保持工具箱与流水线图标一致 // UI 元数据(分类 + 图标)由 ProcessorUiMetadata 统一提供,保持工具箱与流水线图标一致
@@ -52,6 +53,12 @@ namespace XplorePlane.ViewModels
public ObservableCollection<OperatorDescriptor> FilteredOperators { get; } public ObservableCollection<OperatorDescriptor> FilteredOperators { get; }
public ObservableCollection<OperatorGroupViewModel> FilteredGroups { get; } public ObservableCollection<OperatorGroupViewModel> FilteredGroups { get; }
public OperatorGroupViewModel SelectedGroup
{
get => _selectedGroup;
set => SetProperty(ref _selectedGroup, value);
}
public string SearchText public string SearchText
{ {
get => _searchText; get => _searchText;
@@ -78,6 +85,7 @@ namespace XplorePlane.ViewModels
{ {
FilteredOperators.Clear(); FilteredOperators.Clear();
FilteredGroups.Clear(); FilteredGroups.Clear();
SelectedGroup = null;
var filtered = string.IsNullOrWhiteSpace(SearchText) var filtered = string.IsNullOrWhiteSpace(SearchText)
? AvailableOperators ? AvailableOperators
@@ -101,6 +109,8 @@ namespace XplorePlane.ViewModels
Operators = new ObservableCollection<OperatorDescriptor>(group) Operators = new ObservableCollection<OperatorDescriptor>(group)
}); });
} }
SelectedGroup = FilteredGroups.FirstOrDefault();
} }
private static int GetCategoryOrder(string category) => category switch private static int GetCategoryOrder(string category) => category switch
@@ -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;
@@ -14,6 +14,7 @@ using XP.Common.Logging.Interfaces;
using XplorePlane.Events; using XplorePlane.Events;
using XplorePlane.Models; using XplorePlane.Models;
using XplorePlane.Services; using XplorePlane.Services;
using XplorePlane.Services.Storage;
namespace XplorePlane.ViewModels namespace XplorePlane.ViewModels
{ {
@@ -28,17 +29,19 @@ namespace XplorePlane.ViewModels
private readonly IPipelinePersistenceService _persistenceService; private readonly IPipelinePersistenceService _persistenceService;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly ILoggerService _logger; private readonly ILoggerService _logger;
private readonly IXpDataPathService _dataPathService;
private PipelineNodeViewModel _selectedNode; private PipelineNodeViewModel _selectedNode;
private BitmapSource _sourceImage; private BitmapSource _sourceImage;
private BitmapSource _previewImage; private BitmapSource _previewImage;
private string _pipelineName = "新建流水线"; private string _pipelineName = "新建模块";
private string _selectedDevice = string.Empty; private string _selectedDevice = string.Empty;
private bool _isExecuting; private bool _isExecuting;
private bool _isStatusError; private bool _isStatusError;
private string _statusMessage = string.Empty; private string _statusMessage = string.Empty;
private string _pipelineFileDisplayName = DefaultPipelineFileDisplayName; private string _pipelineFileDisplayName = DefaultPipelineFileDisplayName;
private string _currentFilePath; private string _currentFilePath;
private PipelineNodeViewModel _executionEndNode;
private CancellationTokenSource _executionCts; private CancellationTokenSource _executionCts;
private CancellationTokenSource _debounceCts; private CancellationTokenSource _debounceCts;
@@ -48,13 +51,15 @@ namespace XplorePlane.ViewModels
IPipelineExecutionService executionService, IPipelineExecutionService executionService,
IPipelinePersistenceService persistenceService, IPipelinePersistenceService persistenceService,
IEventAggregator eventAggregator, IEventAggregator eventAggregator,
ILoggerService logger) ILoggerService logger,
IXpDataPathService dataPathService)
{ {
_imageProcessingService = imageProcessingService ?? throw new ArgumentNullException(nameof(imageProcessingService)); _imageProcessingService = imageProcessingService ?? throw new ArgumentNullException(nameof(imageProcessingService));
_executionService = executionService ?? throw new ArgumentNullException(nameof(executionService)); _executionService = executionService ?? throw new ArgumentNullException(nameof(executionService));
_persistenceService = persistenceService ?? throw new ArgumentNullException(nameof(persistenceService)); _persistenceService = persistenceService ?? throw new ArgumentNullException(nameof(persistenceService));
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_logger = logger?.ForModule<PipelineEditorViewModel>() ?? throw new ArgumentNullException(nameof(logger)); _logger = logger?.ForModule<PipelineEditorViewModel>() ?? throw new ArgumentNullException(nameof(logger));
_dataPathService = dataPathService ?? throw new ArgumentNullException(nameof(dataPathService));
PipelineNodes = new ObservableCollection<PipelineNodeViewModel>(); PipelineNodes = new ObservableCollection<PipelineNodeViewModel>();
AvailableDevices = new ObservableCollection<string>(); AvailableDevices = new ObservableCollection<string>();
@@ -64,6 +69,8 @@ namespace XplorePlane.ViewModels
ReorderOperatorCommand = new DelegateCommand<PipelineReorderArgs>(ReorderOperator); ReorderOperatorCommand = new DelegateCommand<PipelineReorderArgs>(ReorderOperator);
ToggleOperatorEnabledCommand = new DelegateCommand<PipelineNodeViewModel>(ToggleOperatorEnabled); ToggleOperatorEnabledCommand = new DelegateCommand<PipelineNodeViewModel>(ToggleOperatorEnabled);
ExecutePipelineCommand = new DelegateCommand(async () => await ExecutePipelineAsync(), () => !IsExecuting && SourceImage != null); ExecutePipelineCommand = new DelegateCommand(async () => await ExecutePipelineAsync(), () => !IsExecuting && SourceImage != null);
ExecuteToNodeCommand = new DelegateCommand<PipelineNodeViewModel>(async node => await ExecuteToNodeAsync(node), CanExecuteToNode);
ClearExecutionRangeCommand = new DelegateCommand(async () => await ClearExecutionRangeAsync(), CanClearExecutionRange);
CancelExecutionCommand = new DelegateCommand(CancelExecution, () => IsExecuting); CancelExecutionCommand = new DelegateCommand(CancelExecution, () => IsExecuting);
NewPipelineCommand = new DelegateCommand(NewPipeline); NewPipelineCommand = new DelegateCommand(NewPipeline);
SavePipelineCommand = new DelegateCommand(async () => await SavePipelineAsync()); SavePipelineCommand = new DelegateCommand(async () => await SavePipelineAsync());
@@ -71,7 +78,6 @@ namespace XplorePlane.ViewModels
DeletePipelineCommand = new DelegateCommand(async () => await DeletePipelineAsync()); DeletePipelineCommand = new DelegateCommand(async () => await DeletePipelineAsync());
LoadPipelineCommand = new DelegateCommand(async () => await LoadPipelineAsync()); LoadPipelineCommand = new DelegateCommand(async () => await LoadPipelineAsync());
LoadImageCommand = new DelegateCommand(LoadImage); LoadImageCommand = new DelegateCommand(LoadImage);
OpenToolboxCommand = new DelegateCommand(OpenToolbox);
MoveNodeUpCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeUp); MoveNodeUpCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeUp);
MoveNodeDownCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeDown); MoveNodeDownCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeDown);
@@ -163,6 +169,20 @@ namespace XplorePlane.ViewModels
private set => SetProperty(ref _pipelineFileDisplayName, value); private set => SetProperty(ref _pipelineFileDisplayName, value);
} }
public PipelineNodeViewModel ExecutionEndNode
{
get => _executionEndNode;
private set
{
if (SetProperty(ref _executionEndNode, value))
{
UpdateExecutionRangeState();
ExecuteToNodeCommand.RaiseCanExecuteChanged();
ClearExecutionRangeCommand.RaiseCanExecuteChanged();
}
}
}
// ── Commands ────────────────────────────────────────────────── // ── Commands ──────────────────────────────────────────────────
public DelegateCommand<string> AddOperatorCommand { get; } public DelegateCommand<string> AddOperatorCommand { get; }
@@ -170,6 +190,8 @@ namespace XplorePlane.ViewModels
public DelegateCommand<PipelineReorderArgs> ReorderOperatorCommand { get; } public DelegateCommand<PipelineReorderArgs> ReorderOperatorCommand { get; }
public DelegateCommand<PipelineNodeViewModel> ToggleOperatorEnabledCommand { get; } public DelegateCommand<PipelineNodeViewModel> ToggleOperatorEnabledCommand { get; }
public DelegateCommand ExecutePipelineCommand { get; } public DelegateCommand ExecutePipelineCommand { get; }
public DelegateCommand<PipelineNodeViewModel> ExecuteToNodeCommand { get; }
public DelegateCommand ClearExecutionRangeCommand { get; }
public DelegateCommand CancelExecutionCommand { get; } public DelegateCommand CancelExecutionCommand { get; }
public DelegateCommand NewPipelineCommand { get; } public DelegateCommand NewPipelineCommand { get; }
public DelegateCommand SavePipelineCommand { get; } public DelegateCommand SavePipelineCommand { get; }
@@ -178,8 +200,6 @@ namespace XplorePlane.ViewModels
public DelegateCommand LoadPipelineCommand { get; } public DelegateCommand LoadPipelineCommand { get; }
public DelegateCommand LoadImageCommand { get; } public DelegateCommand LoadImageCommand { get; }
public DelegateCommand OpenToolboxCommand { get; }
public DelegateCommand<PipelineNodeViewModel> MoveNodeUpCommand { get; } public DelegateCommand<PipelineNodeViewModel> MoveNodeUpCommand { get; }
public DelegateCommand<PipelineNodeViewModel> MoveNodeDownCommand { get; } public DelegateCommand<PipelineNodeViewModel> MoveNodeDownCommand { get; }
@@ -187,6 +207,8 @@ namespace XplorePlane.ViewModels
ICommand IPipelineEditorHostViewModel.RemoveOperatorCommand => RemoveOperatorCommand; ICommand IPipelineEditorHostViewModel.RemoveOperatorCommand => RemoveOperatorCommand;
ICommand IPipelineEditorHostViewModel.ReorderOperatorCommand => ReorderOperatorCommand; ICommand IPipelineEditorHostViewModel.ReorderOperatorCommand => ReorderOperatorCommand;
ICommand IPipelineEditorHostViewModel.ToggleOperatorEnabledCommand => ToggleOperatorEnabledCommand; ICommand IPipelineEditorHostViewModel.ToggleOperatorEnabledCommand => ToggleOperatorEnabledCommand;
ICommand IPipelineEditorHostViewModel.ExecuteToNodeCommand => ExecuteToNodeCommand;
ICommand IPipelineEditorHostViewModel.ClearExecutionRangeCommand => ClearExecutionRangeCommand;
ICommand IPipelineEditorHostViewModel.MoveNodeUpCommand => MoveNodeUpCommand; ICommand IPipelineEditorHostViewModel.MoveNodeUpCommand => MoveNodeUpCommand;
ICommand IPipelineEditorHostViewModel.MoveNodeDownCommand => MoveNodeDownCommand; ICommand IPipelineEditorHostViewModel.MoveNodeDownCommand => MoveNodeDownCommand;
ICommand IPipelineEditorHostViewModel.NewPipelineCommand => NewPipelineCommand; ICommand IPipelineEditorHostViewModel.NewPipelineCommand => NewPipelineCommand;
@@ -237,6 +259,7 @@ namespace XplorePlane.ViewModels
LoadNodeParameters(node); LoadNodeParameters(node);
PipelineNodes.Add(node); PipelineNodes.Add(node);
SelectedNode = node; SelectedNode = node;
UpdateExecutionRangeState();
_logger.Info("节点已添加到 PipelineNodes{Key} ({DisplayName}),当前节点数={Count}", _logger.Info("节点已添加到 PipelineNodes{Key} ({DisplayName}),当前节点数={Count}",
operatorKey, displayName, PipelineNodes.Count); operatorKey, displayName, PipelineNodes.Count);
SetInfoStatus($"已添加算子:{displayName}"); SetInfoStatus($"已添加算子:{displayName}");
@@ -250,6 +273,10 @@ namespace XplorePlane.ViewModels
var removedIndex = PipelineNodes.IndexOf(node); var removedIndex = PipelineNodes.IndexOf(node);
PipelineNodes.Remove(node); PipelineNodes.Remove(node);
RenumberNodes(); RenumberNodes();
if (ReferenceEquals(ExecutionEndNode, node))
ExecutionEndNode = null;
else
UpdateExecutionRangeState();
SelectNeighborAfterRemoval(removedIndex); SelectNeighborAfterRemoval(removedIndex);
SetInfoStatus($"已移除算子:{node.DisplayName}"); SetInfoStatus($"已移除算子:{node.DisplayName}");
@@ -263,6 +290,7 @@ namespace XplorePlane.ViewModels
if (index <= 0) return; if (index <= 0) return;
PipelineNodes.Move(index, index - 1); PipelineNodes.Move(index, index - 1);
RenumberNodes(); RenumberNodes();
UpdateExecutionRangeState();
TriggerDebouncedExecution(); TriggerDebouncedExecution();
} }
@@ -273,6 +301,7 @@ namespace XplorePlane.ViewModels
if (index < 0 || index >= PipelineNodes.Count - 1) return; if (index < 0 || index >= PipelineNodes.Count - 1) return;
PipelineNodes.Move(index, index + 1); PipelineNodes.Move(index, index + 1);
RenumberNodes(); RenumberNodes();
UpdateExecutionRangeState();
TriggerDebouncedExecution(); TriggerDebouncedExecution();
} }
@@ -290,6 +319,7 @@ namespace XplorePlane.ViewModels
PipelineNodes.RemoveAt(oldIndex); PipelineNodes.RemoveAt(oldIndex);
PipelineNodes.Insert(newIndex, node); PipelineNodes.Insert(newIndex, node);
RenumberNodes(); RenumberNodes();
UpdateExecutionRangeState();
SelectedNode = node; SelectedNode = node;
SetInfoStatus($"已调整算子顺序:{node.DisplayName}"); SetInfoStatus($"已调整算子顺序:{node.DisplayName}");
TriggerDebouncedExecution(); TriggerDebouncedExecution();
@@ -307,6 +337,34 @@ namespace XplorePlane.ViewModels
TriggerDebouncedExecution(); TriggerDebouncedExecution();
} }
private bool CanExecuteToNode(PipelineNodeViewModel node) =>
node != null && PipelineNodes.Contains(node) && !IsExecuting && SourceImage != null;
private async Task ExecuteToNodeAsync(PipelineNodeViewModel node)
{
if (!CanExecuteToNode(node))
return;
SelectedNode = node;
ExecutionEndNode = node;
await ExecutePipelineAsync();
}
private bool CanClearExecutionRange() =>
ExecutionEndNode != null && !IsExecuting;
private async Task ClearExecutionRangeAsync()
{
if (ExecutionEndNode == null)
return;
ExecutionEndNode = null;
SetInfoStatus("已切换为执行全部节点");
if (SourceImage != null)
await ExecutePipelineAsync();
}
private void RenumberNodes() private void RenumberNodes()
{ {
for (int i = 0; i < PipelineNodes.Count; i++) for (int i = 0; i < PipelineNodes.Count; i++)
@@ -370,10 +428,15 @@ namespace XplorePlane.ViewModels
_executionCts?.Cancel(); _executionCts?.Cancel();
_executionCts = new CancellationTokenSource(); _executionCts = new CancellationTokenSource();
var token = _executionCts.Token; var token = _executionCts.Token;
var executionNodes = GetNodesInExecutionScope()
.Where(n => n.IsEnabled)
.OrderBy(n => n.Order)
.ToList();
IsExecuting = true; IsExecuting = true;
SetInfoStatus("正在执行流水线..."); SetInfoStatus(BuildExecutionStartMessage(executionNodes.Count));
_logger.Info("[图像链路] ExecutePipelineAsync:开始执行,节点数={Count}", PipelineNodes.Count); _logger.Info("[图像链路] ExecutePipelineAsync:开始执行,范围节点数={Count},截止节点={Node}",
executionNodes.Count, ExecutionEndNode?.DisplayName ?? "<all>");
try try
{ {
@@ -381,10 +444,10 @@ namespace XplorePlane.ViewModels
SetInfoStatus($"执行中:{p.CurrentOperator} ({p.CurrentStep}/{p.TotalSteps})")); SetInfoStatus($"执行中:{p.CurrentOperator} ({p.CurrentStep}/{p.TotalSteps})"));
var result = await _executionService.ExecutePipelineAsync( var result = await _executionService.ExecutePipelineAsync(
PipelineNodes, SourceImage, progress, token); executionNodes, SourceImage, progress, token);
PreviewImage = result; PreviewImage = result;
SetInfoStatus("流水线执行完成"); SetInfoStatus(BuildExecutionCompletedMessage(executionNodes.Count));
_logger.Info("[图像链路] ExecutePipelineAsync:执行完成,准备发布 PipelinePreviewUpdatedEvent"); _logger.Info("[图像链路] ExecutePipelineAsync:执行完成,准备发布 PipelinePreviewUpdatedEvent");
PublishPipelinePreviewUpdated(result, StatusMessage); PublishPipelinePreviewUpdated(result, StatusMessage);
} }
@@ -411,7 +474,7 @@ namespace XplorePlane.ViewModels
private bool TryReportInvalidParameters() private bool TryReportInvalidParameters()
{ {
var firstInvalidNode = PipelineNodes var firstInvalidNode = GetNodesInExecutionScope()
.Where(n => n.IsEnabled) .Where(n => n.IsEnabled)
.OrderBy(n => n.Order) .OrderBy(n => n.Order)
.FirstOrDefault(n => n.Parameters.Any(p => !p.IsValueValid)); .FirstOrDefault(n => n.Parameters.Any(p => !p.IsValueValid));
@@ -552,6 +615,7 @@ namespace XplorePlane.ViewModels
{ {
PipelineNodes.Clear(); PipelineNodes.Clear();
SelectedNode = null; SelectedNode = null;
ExecutionEndNode = null;
PipelineName = "新建流水线"; PipelineName = "新建流水线";
PreviewImage = null; PreviewImage = null;
_currentFilePath = null; _currentFilePath = null;
@@ -586,7 +650,7 @@ namespace XplorePlane.ViewModels
var dialog = new SaveFileDialog var dialog = new SaveFileDialog
{ {
Filter = "XP 模块流水线 (*.xpm)|*.xpm", Filter = "XP 模块 (*.xpm)|*.xpm",
DefaultExt = ".xpm", DefaultExt = ".xpm",
AddExtension = true, AddExtension = true,
FileName = PipelineName, FileName = PipelineName,
@@ -638,7 +702,7 @@ namespace XplorePlane.ViewModels
{ {
var dialog = new OpenFileDialog var dialog = new OpenFileDialog
{ {
Filter = "XP 模块流水线 (*.xpm)|*.xpm", Filter = "XP 模块 (*.xpm)|*.xpm",
DefaultExt = ".xpm", DefaultExt = ".xpm",
InitialDirectory = GetPipelineDirectory() InitialDirectory = GetPipelineDirectory()
}; };
@@ -651,6 +715,7 @@ namespace XplorePlane.ViewModels
PipelineNodes.Clear(); PipelineNodes.Clear();
SelectedNode = null; SelectedNode = null;
ExecutionEndNode = null;
PipelineName = model.Name; PipelineName = model.Name;
SelectedDevice = model.DeviceId; SelectedDevice = model.DeviceId;
@@ -679,6 +744,8 @@ namespace XplorePlane.ViewModels
PipelineNodes.Add(node); PipelineNodes.Add(node);
} }
UpdateExecutionRangeState();
_logger.Info("流水线已加载:{Name},节点数={Count}", model.Name, PipelineNodes.Count); _logger.Info("流水线已加载:{Name},节点数={Count}", model.Name, PipelineNodes.Count);
SetInfoStatus($"已加载流水线:{model.Name}{PipelineNodes.Count} 个节点)"); SetInfoStatus($"已加载流水线:{model.Name}{PipelineNodes.Count} 个节点)");
} }
@@ -689,25 +756,6 @@ namespace XplorePlane.ViewModels
} }
} }
private void OpenToolbox() //打开图像工具箱
{
_logger.Info("OpenToolbox 被调用");
try
{
var window = new Views.OperatorToolboxWindow
{
Owner = System.Windows.Application.Current.MainWindow
};
_logger.Info("OperatorToolboxWindow 已创建,准备 Show()");
window.Show();
_logger.Info("OperatorToolboxWindow.Show() 完成");
}
catch (Exception ex)
{
_logger.Error(ex, "OpenToolbox 打开窗口失败");
}
}
private PipelineModel BuildPipelineModel() private PipelineModel BuildPipelineModel()
{ {
return new PipelineModel return new PipelineModel
@@ -726,11 +774,47 @@ namespace XplorePlane.ViewModels
}; };
} }
private static string GetPipelineDirectory() private System.Collections.Generic.IEnumerable<PipelineNodeViewModel> GetNodesInExecutionScope()
{ {
var dir = Path.Combine( var orderedNodes = PipelineNodes.OrderBy(n => n.Order);
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), if (ExecutionEndNode == null)
"XplorePlane", "Pipelines"); return orderedNodes;
return orderedNodes.Where(n => n.Order <= ExecutionEndNode.Order);
}
private void UpdateExecutionRangeState()
{
if (_executionEndNode != null && !PipelineNodes.Contains(_executionEndNode))
_executionEndNode = null;
var endOrder = _executionEndNode?.Order;
foreach (var node in PipelineNodes)
{
node.IsExecutionEndNode = endOrder.HasValue && node.Order == endOrder.Value;
node.IsSkippedByExecutionRange = endOrder.HasValue && node.Order > endOrder.Value;
}
}
private string BuildExecutionStartMessage(int executionCount)
{
if (ExecutionEndNode == null)
return "正在执行流水线...";
return $"正在执行到“{ExecutionEndNode.DisplayName}” ({executionCount} 个有效节点)...";
}
private string BuildExecutionCompletedMessage(int executionCount)
{
if (ExecutionEndNode == null)
return "流水线执行完成";
return $"已执行到“{ExecutionEndNode.DisplayName}” ({executionCount} 个有效节点)";
}
private string GetPipelineDirectory()
{
var dir = _dataPathService.ToolsPath;
Directory.CreateDirectory(dir); Directory.CreateDirectory(dir);
return dir; return dir;
} }
@@ -11,6 +11,8 @@ namespace XplorePlane.ViewModels
private int _order; private int _order;
private bool _isSelected; private bool _isSelected;
private bool _isEnabled = true; private bool _isEnabled = true;
private bool _isExecutionEndNode;
private bool _isSkippedByExecutionRange;
public PipelineNodeViewModel(string operatorKey, string displayName, string iconPath = null) public PipelineNodeViewModel(string operatorKey, string displayName, string iconPath = null)
{ {
@@ -51,9 +53,49 @@ namespace XplorePlane.ViewModels
public bool IsEnabled public bool IsEnabled
{ {
get => _isEnabled; get => _isEnabled;
set => SetProperty(ref _isEnabled, value); set
{
if (SetProperty(ref _isEnabled, value))
RaisePropertyChanged(nameof(NodeStateText));
}
} }
public ObservableCollection<ProcessorParameterVM> Parameters { get; } public ObservableCollection<ProcessorParameterVM> Parameters { get; }
public bool IsExecutionEndNode
{
get => _isExecutionEndNode;
set
{
if (SetProperty(ref _isExecutionEndNode, value))
RaisePropertyChanged(nameof(NodeStateText));
}
}
public bool IsSkippedByExecutionRange
{
get => _isSkippedByExecutionRange;
set
{
if (SetProperty(ref _isSkippedByExecutionRange, value))
RaisePropertyChanged(nameof(NodeStateText));
}
}
public string NodeStateText
{
get
{
if (IsExecutionEndNode && !IsEnabled)
return "执行到此(停用)";
if (IsExecutionEndNode)
return "执行到此";
if (!IsEnabled)
return "已停用";
if (IsSkippedByExecutionRange)
return "未参与本次执行";
return "已启用";
}
}
} }
} }
@@ -39,7 +39,15 @@ namespace XplorePlane.ViewModels
public string ParameterType { get; } public string ParameterType { get; }
public bool HasOptions => Options is { Length: > 0 }; public bool HasOptions => Options is { Length: > 0 };
public bool IsBool => ParameterType == "bool"; public bool IsBool => ParameterType == "bool";
public bool IsTextInput => !IsBool && !HasOptions; public bool IsNumeric => ParameterType is "int" or "double";
public bool HasRange => IsNumeric && MinValue != null && MaxValue != null;
public bool IsSliderInput => HasRange;
public bool IsTextInput => !IsBool && !HasOptions && !IsSliderInput;
public double SliderMinimum => TryConvertToDouble(MinValue, out var minValue) ? minValue : 0d;
public double SliderMaximum => TryConvertToDouble(MaxValue, out var maxValue) ? maxValue : 100d;
public double SliderTickFrequency => ResolveTickFrequency();
public bool IsIntegerSlider => ParameterType == "int";
public bool IsValueValid public bool IsValueValid
{ {
@@ -60,10 +68,46 @@ namespace XplorePlane.ViewModels
RaisePropertyChanged(nameof(Value)); RaisePropertyChanged(nameof(Value));
RaisePropertyChanged(nameof(BoolValue)); RaisePropertyChanged(nameof(BoolValue));
RaisePropertyChanged(nameof(SelectedOption)); RaisePropertyChanged(nameof(SelectedOption));
RaisePropertyChanged(nameof(SliderValue));
RaisePropertyChanged(nameof(DisplayValueText));
} }
} }
} }
public double SliderValue
{
get => TryConvertToDouble(_value, out var sliderValue) ? sliderValue : SliderMinimum;
set
{
if (!IsSliderInput)
{
return;
}
Value = ParameterType == "int"
? (object)Convert.ToInt32(Math.Round(value, MidpointRounding.AwayFromZero), CultureInfo.InvariantCulture)
: Math.Round(value, ResolveDecimalPlaces(), MidpointRounding.AwayFromZero);
}
}
public string DisplayValueText
{
get
{
if (ParameterType == "int" && TryConvertToInt(_value, out var intValue))
{
return intValue.ToString(CultureInfo.InvariantCulture);
}
if (ParameterType == "double" && TryConvertToDouble(_value, out var doubleValue))
{
return doubleValue.ToString($"F{ResolveDecimalPlaces()}", CultureInfo.InvariantCulture);
}
return Convert.ToString(_value, CultureInfo.InvariantCulture) ?? string.Empty;
}
}
public bool BoolValue public bool BoolValue
{ {
get => ParameterType == "bool" && TryConvertToBool(_value, out var boolValue) && boolValue; get => ParameterType == "bool" && TryConvertToBool(_value, out var boolValue) && boolValue;
@@ -154,6 +198,58 @@ namespace XplorePlane.ViewModels
return true; return true;
} }
private double ResolveTickFrequency()
{
if (!HasRange)
{
return 1d;
}
if (ParameterType == "int")
{
return 1d;
}
double range = SliderMaximum - SliderMinimum;
if (range <= 1d)
{
return 0.01d;
}
if (range <= 10d)
{
return 0.1d;
}
return 1d;
}
private int ResolveDecimalPlaces()
{
if (ParameterType == "int")
{
return 0;
}
double tick = SliderTickFrequency;
if (tick >= 1d)
{
return 0;
}
if (tick >= 0.1d)
{
return 1;
}
if (tick >= 0.01d)
{
return 2;
}
return 3;
}
private static string NormalizeNumericText(string value) private static string NormalizeNumericText(string value)
{ {
return value.Trim().TrimEnd('、', '', ',', '。', '.', ';', '', ':', ''); return value.Trim().TrimEnd('、', '', ',', '。', '.', ';', '', ':', '');
+292 -62
View File
@@ -7,10 +7,13 @@ using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Configuration; using System.Configuration;
using System.IO; using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using XplorePlane.Events; using XplorePlane.Events;
using XplorePlane.Services.MainViewport; using XplorePlane.Services.MainViewport;
using XplorePlane.Services.Storage;
using XplorePlane.ViewModels.Cnc; using XplorePlane.ViewModels.Cnc;
using XplorePlane.Views; using XplorePlane.Views;
using XplorePlane.Views.Cnc; using XplorePlane.Views.Cnc;
@@ -18,17 +21,19 @@ using XP.Common.Logging.Interfaces;
using XP.Common.GeneralForm.Views; using XP.Common.GeneralForm.Views;
using XP.Common.PdfViewer.Interfaces; using XP.Common.PdfViewer.Interfaces;
using XP.Hardware.MotionControl.Abstractions; using XP.Hardware.MotionControl.Abstractions;
using XP.Hardware.MotionControl.Services;
namespace XplorePlane.ViewModels namespace XplorePlane.ViewModels
{ {
public class MainViewModel : BindableBase public class MainViewModel : BindableBase
{ {
private const double CncEditorHostWidth = 502d; private const double CncEditorHostWidth = 452d;
private readonly ILoggerService _logger; private readonly ILoggerService _logger;
private readonly IContainerProvider _containerProvider; private readonly IContainerProvider _containerProvider;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly IMainViewportService _mainViewportService; private readonly IMainViewportService _mainViewportService;
private readonly IXpDataPathService _xpDataPathService;
private readonly CncEditorViewModel _cncEditorViewModel; private readonly CncEditorViewModel _cncEditorViewModel;
private readonly CncPageView _cncPageView; private readonly CncPageView _cncPageView;
@@ -65,6 +70,10 @@ namespace XplorePlane.ViewModels
public DelegateCommand OpenLibraryVersionsCommand { get; } public DelegateCommand OpenLibraryVersionsCommand { get; }
public DelegateCommand OpenUserManualCommand { get; } public DelegateCommand OpenUserManualCommand { get; }
public DelegateCommand OpenCameraSettingsCommand { get; } public DelegateCommand OpenCameraSettingsCommand { get; }
public DelegateCommand OpenSettingsCommand { get; }
public DelegateCommand BrowseDataRootPathCommand { get; }
public DelegateCommand ResetDataRootPathCommand { get; }
public DelegateCommand SaveDataRootPathCommand { get; }
public DelegateCommand NewCncProgramCommand { get; } public DelegateCommand NewCncProgramCommand { get; }
public DelegateCommand SaveCncProgramCommand { get; } public DelegateCommand SaveCncProgramCommand { get; }
public DelegateCommand LoadCncProgramCommand { get; } public DelegateCommand LoadCncProgramCommand { get; }
@@ -73,6 +82,7 @@ namespace XplorePlane.ViewModels
public DelegateCommand InsertCompleteProgramCommand { get; } public DelegateCommand InsertCompleteProgramCommand { get; }
public DelegateCommand InsertInspectionMarkerCommand { get; } public DelegateCommand InsertInspectionMarkerCommand { get; }
public DelegateCommand InsertInspectionModuleCommand { get; } public DelegateCommand InsertInspectionModuleCommand { get; }
public DelegateCommand InsertBuiltInInspectionModuleCommand { get; }
public DelegateCommand InsertSaveNodeCommand { get; } public DelegateCommand InsertSaveNodeCommand { get; }
public DelegateCommand InsertPauseDialogCommand { get; } public DelegateCommand InsertPauseDialogCommand { get; }
public DelegateCommand InsertWaitDelayCommand { get; } public DelegateCommand InsertWaitDelayCommand { get; }
@@ -81,6 +91,8 @@ namespace XplorePlane.ViewModels
// 硬件命令 // 硬件命令
public DelegateCommand AxisResetCommand { get; } public DelegateCommand AxisResetCommand { get; }
public DelegateCommand OpenDoorCommand { get; }
public DelegateCommand CloseDoorCommand { get; }
public DelegateCommand OpenDetectorConfigCommand { get; } public DelegateCommand OpenDetectorConfigCommand { get; }
public DelegateCommand OpenMotionDebugCommand { get; } public DelegateCommand OpenMotionDebugCommand { get; }
public DelegateCommand OpenPlcAddrConfigCommand { get; } public DelegateCommand OpenPlcAddrConfigCommand { get; }
@@ -120,6 +132,34 @@ namespace XplorePlane.ViewModels
public bool IsUsingLiveDetectorSource => _mainViewportService.CurrentSourceMode == MainViewportSourceMode.LiveDetector; public bool IsUsingLiveDetectorSource => _mainViewportService.CurrentSourceMode == MainViewportSourceMode.LiveDetector;
public string DataRootPath
{
get => _dataRootPath;
set => SetProperty(ref _dataRootPath, value);
}
public string PlanRootPath => _xpDataPathService.PlanPath;
public string ToolsRootPath => _xpDataPathService.ToolsPath;
public string ResultsRootPath => _xpDataPathService.DataPath;
public string ReportRootPath => _xpDataPathService.ReportPath;
public ObservableCollection<BuiltInInspectionModuleItem> BuiltInInspectionModules { get; } = new();
public BuiltInInspectionModuleItem SelectedBuiltInInspectionModule
{
get => _selectedBuiltInInspectionModule;
set
{
if (SetProperty(ref _selectedBuiltInInspectionModule, value))
{
InsertBuiltInInspectionModuleCommand?.RaiseCanExecuteChanged();
}
}
}
/// <summary>右侧图像区域内容 | Right-side image panel content</summary> /// <summary>右侧图像区域内容 | Right-side image panel content</summary>
public object ImagePanelContent public object ImagePanelContent
{ {
@@ -142,11 +182,11 @@ namespace XplorePlane.ViewModels
} }
// 窗口引用(单例窗口防止重复打开) // 窗口引用(单例窗口防止重复打开)
private Window _motionDebugWindow; private Window _motionDebugWindow;
private Window _detectorConfigWindow; private Window _detectorConfigWindow;
private Window _plcAddrConfigWindow; private Window _plcAddrConfigWindow;
private Window _realTimeLogViewerWindow; private Window _realTimeLogViewerWindow;
private Window _settingsWindow;
private Window _toolboxWindow; private Window _toolboxWindow;
private Window _raySourceConfigWindow; private Window _raySourceConfigWindow;
private object _imagePanelContent; private object _imagePanelContent;
@@ -155,16 +195,21 @@ namespace XplorePlane.ViewModels
private bool _isCncEditorMode; private bool _isCncEditorMode;
private string _licenseInfo = "当前时间"; private string _licenseInfo = "当前时间";
private string _dataRootPath = string.Empty;
private BuiltInInspectionModuleItem _selectedBuiltInInspectionModule;
public MainViewModel( public MainViewModel(
ILoggerService logger, ILoggerService logger,
IContainerProvider containerProvider, IContainerProvider containerProvider,
IEventAggregator eventAggregator, IEventAggregator eventAggregator,
IMainViewportService mainViewportService) IMainViewportService mainViewportService,
IXpDataPathService xpDataPathService)
{ {
_logger = logger?.ForModule<MainViewModel>() ?? throw new ArgumentNullException(nameof(logger)); _logger = logger?.ForModule<MainViewModel>() ?? throw new ArgumentNullException(nameof(logger));
_containerProvider = containerProvider ?? throw new ArgumentNullException(nameof(containerProvider)); _containerProvider = containerProvider ?? throw new ArgumentNullException(nameof(containerProvider));
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_mainViewportService = mainViewportService ?? throw new ArgumentNullException(nameof(mainViewportService)); _mainViewportService = mainViewportService ?? throw new ArgumentNullException(nameof(mainViewportService));
_xpDataPathService = xpDataPathService ?? throw new ArgumentNullException(nameof(xpDataPathService));
_cncEditorViewModel = _containerProvider.Resolve<CncEditorViewModel>(); _cncEditorViewModel = _containerProvider.Resolve<CncEditorViewModel>();
_cncPageView = new CncPageView { DataContext = _cncEditorViewModel }; _cncPageView = new CncPageView { DataContext = _cncEditorViewModel };
@@ -185,8 +230,8 @@ namespace XplorePlane.ViewModels
var node = _cncEditorViewModel.SelectedNode; var node = _cncEditorViewModel.SelectedNode;
if (node?.ResultImage != null) if (node?.ResultImage != null)
{ {
_logger.Info("[图像链路] 切换到节点 [{Name}],显示缓存结果图像", node.Name); _logger.Info("[Image] Switched to node [{Name}], showing cached result image.", node.Name);
_mainViewportService.SetManualImage(node.ResultImage, $"CNC节点:{node.Name}"); _mainViewportService.SetManualImage(node.ResultImage, $"CNC node: {node.Name}");
} }
} }
}; };
@@ -205,15 +250,19 @@ namespace XplorePlane.ViewModels
ClearCommand = new DelegateCommand(OnClear); ClearCommand = new DelegateCommand(OnClear);
EditPropertiesCommand = new DelegateCommand(OnEditProperties); EditPropertiesCommand = new DelegateCommand(OnEditProperties);
OpenImageProcessingCommand = new DelegateCommand(() => ShowWindow(new Views.ImageProcessingWindow(), "图像处理"));
LoadImageCommand = new DelegateCommand(ExecuteLoadImage); LoadImageCommand = new DelegateCommand(ExecuteLoadImage);
OpenPipelineEditorCommand = new DelegateCommand(() => ShowWindow(new Views.PipelineEditorWindow(), "流水线编辑器"));
OpenCncEditorCommand = new DelegateCommand(ExecuteOpenCncEditor); OpenCncEditorCommand = new DelegateCommand(ExecuteOpenCncEditor);
OpenMatrixEditorCommand = new DelegateCommand(() => ShowWindow(new Views.Cnc.MatrixEditorWindow(), "矩阵编")); OpenMatrixEditorCommand = new DelegateCommand(() => ShowWindow(new Views.Cnc.MatrixEditorWindow(), "矩阵编"));
OpenToolboxCommand = new DelegateCommand(ExecuteOpenToolbox); OpenToolboxCommand = new DelegateCommand(ExecuteOpenToolbox);
OpenLibraryVersionsCommand = new DelegateCommand(() => ShowWindow(new Views.LibraryVersionsWindow(), "关于")); OpenLibraryVersionsCommand = new DelegateCommand(() => ShowWindow(new Views.LibraryVersionsWindow(), "关于"));
OpenUserManualCommand = new DelegateCommand(ExecuteOpenUserManual); OpenUserManualCommand = new DelegateCommand(ExecuteOpenUserManual);
OpenCameraSettingsCommand = new DelegateCommand(ExecuteOpenCameraSettings); OpenCameraSettingsCommand = new DelegateCommand(ExecuteOpenCameraSettings);
OpenSettingsCommand = new DelegateCommand(ExecuteOpenSettings);
BrowseDataRootPathCommand = new DelegateCommand(ExecuteBrowseDataRootPath);
ResetDataRootPathCommand = new DelegateCommand(ExecuteResetDataRootPath);
SaveDataRootPathCommand = new DelegateCommand(ExecuteSaveDataRootPath);
NewCncProgramCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.NewProgramCommand.Execute())); NewCncProgramCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.NewProgramCommand.Execute()));
SaveCncProgramCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.SaveProgramCommand.Execute())); SaveCncProgramCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.SaveProgramCommand.Execute()));
LoadCncProgramCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.LoadProgramCommand.Execute())); LoadCncProgramCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.LoadProgramCommand.Execute()));
@@ -222,6 +271,9 @@ namespace XplorePlane.ViewModels
InsertCompleteProgramCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertCompleteProgramCommand.Execute())); InsertCompleteProgramCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertCompleteProgramCommand.Execute()));
InsertInspectionMarkerCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertInspectionMarkerCommand.Execute())); InsertInspectionMarkerCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertInspectionMarkerCommand.Execute()));
InsertInspectionModuleCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertInspectionModuleCommand.Execute())); InsertInspectionModuleCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertInspectionModuleCommand.Execute()));
InsertBuiltInInspectionModuleCommand = new DelegateCommand(
async () => await ExecuteInsertBuiltInInspectionModuleAsync(),
CanExecuteInsertBuiltInInspectionModule);
InsertSaveNodeCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertSaveNodeCommand.Execute())); InsertSaveNodeCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertSaveNodeCommand.Execute()));
InsertPauseDialogCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertPauseDialogCommand.Execute())); InsertPauseDialogCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertPauseDialogCommand.Execute()));
InsertWaitDelayCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertWaitDelayCommand.Execute())); InsertWaitDelayCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertWaitDelayCommand.Execute()));
@@ -242,11 +294,12 @@ namespace XplorePlane.ViewModels
BubbleMeasureCommand = new DelegateCommand(ExecuteBubbleMeasure); BubbleMeasureCommand = new DelegateCommand(ExecuteBubbleMeasure);
// 辅助线命令 // 辅助线命令
ToggleCrosshairCommand = new DelegateCommand(() => ToggleCrosshairCommand = new DelegateCommand(() =>
_eventAggregator.GetEvent<ToggleCrosshairEvent>().Publish()); _eventAggregator.GetEvent<ToggleCrosshairEvent>().Publish());
AxisResetCommand = new DelegateCommand(ExecuteAxisReset); AxisResetCommand = new DelegateCommand(ExecuteAxisReset);
OpenDoorCommand = new DelegateCommand(ExecuteOpenDoor);
CloseDoorCommand = new DelegateCommand(ExecuteCloseDoor);
OpenDetectorConfigCommand = new DelegateCommand(ExecuteOpenDetectorConfig); OpenDetectorConfigCommand = new DelegateCommand(ExecuteOpenDetectorConfig);
OpenMotionDebugCommand = new DelegateCommand(ExecuteOpenMotionDebug); OpenMotionDebugCommand = new DelegateCommand(ExecuteOpenMotionDebug);
OpenPlcAddrConfigCommand = new DelegateCommand(ExecuteOpenPlcAddrConfig); OpenPlcAddrConfigCommand = new DelegateCommand(ExecuteOpenPlcAddrConfig);
@@ -260,6 +313,8 @@ namespace XplorePlane.ViewModels
ImagePanelContent = new PipelineEditorView(); ImagePanelContent = new PipelineEditorView();
ViewportPanelWidth = new GridLength(1, GridUnitType.Star); ViewportPanelWidth = new GridLength(1, GridUnitType.Star);
ImagePanelWidth = new GridLength(320); ImagePanelWidth = new GridLength(320);
DataRootPath = _xpDataPathService.RootPath;
LoadBuiltInInspectionModules();
_logger.Info("MainViewModel 已初始化"); _logger.Info("MainViewModel 已初始化");
} }
@@ -293,7 +348,7 @@ namespace XplorePlane.ViewModels
private void ExecuteOpenToolbox() private void ExecuteOpenToolbox()
{ {
ShowOrActivate(_toolboxWindow, w => _toolboxWindow = w, () => new Views.OperatorToolboxWindow(), "算子工具箱"); ShowOrActivate(_toolboxWindow, w => _toolboxWindow = w, () => new Views.OperatorToolboxWindow(), "Operator Toolbox");
} }
private void ExecuteOpenCncEditor() private void ExecuteOpenCncEditor()
@@ -334,17 +389,17 @@ namespace XplorePlane.ViewModels
var manualPath = ConfigurationManager.AppSettings["UserManual"]; var manualPath = ConfigurationManager.AppSettings["UserManual"];
if (string.IsNullOrEmpty(manualPath)) if (string.IsNullOrEmpty(manualPath))
{ {
_logger.Warn("未配置用户手册路径"); _logger.Warn("User manual path is not configured.");
MessageBox.Show("未配置用户手册路径,请检查 App.config 中的 UserManual 配置项。", MessageBox.Show("User manual path is not configured. Please check the UserManual setting in App.config.",
"提示", MessageBoxButton.OK, MessageBoxImage.Warning); "Info", MessageBoxButton.OK, MessageBoxImage.Warning);
return; return;
} }
if (!File.Exists(manualPath)) if (!File.Exists(manualPath))
{ {
_logger.Warn("用户手册文件不存在:{Path}", manualPath); _logger.Warn("User manual file not found: {Path}", manualPath);
MessageBox.Show($"用户手册文件不存在:\n{manualPath}", MessageBox.Show($"User manual file not found:\n{manualPath}",
"提示", MessageBoxButton.OK, MessageBoxImage.Warning); "Info", MessageBoxButton.OK, MessageBoxImage.Warning);
return; return;
} }
@@ -355,9 +410,9 @@ namespace XplorePlane.ViewModels
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Error(ex, "打开用户手册失败"); _logger.Error(ex, "Failed to open user manual.");
MessageBox.Show($"打开用户手册失败:{ex.Message}", MessageBox.Show($"Failed to open user manual: {ex.Message}",
"错误", MessageBoxButton.OK, MessageBoxImage.Error); "Error", MessageBoxButton.OK, MessageBoxImage.Error);
} }
} }
@@ -368,7 +423,7 @@ namespace XplorePlane.ViewModels
var vm = _containerProvider.Resolve<NavigationPropertyPanelViewModel>(); var vm = _containerProvider.Resolve<NavigationPropertyPanelViewModel>();
if (!vm.IsCameraConnected) if (!vm.IsCameraConnected)
{ {
MessageBox.Show("请先连接相机", "提示", MessageBoxButton.OK, MessageBoxImage.Information); MessageBox.Show("Please connect the camera first", "Info", MessageBoxButton.OK, MessageBoxImage.Information);
return; return;
} }
@@ -377,13 +432,146 @@ namespace XplorePlane.ViewModels
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Error(ex, "打开相机设置失败"); _logger.Error(ex, "Failed to open camera settings.");
} }
} }
private void ExecuteOpenSettings()
{
try
{
ShowOrActivate(_settingsWindow, w => _settingsWindow = w,
() => new Views.SettingsWindow(this), "Settings");
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to open settings window");
MessageBox.Show($"Failed to open settings window: {ex.Message}",
"Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void ExecuteBrowseDataRootPath()
{
try
{
var dialog = new OpenFolderDialog
{
Title = "选择 XP 数据根目录",
InitialDirectory = Directory.Exists(DataRootPath) ? DataRootPath : _xpDataPathService.RootPath
};
if (dialog.ShowDialog() == true)
{
DataRootPath = dialog.FolderName;
}
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to browse XP data root.");
MessageBox.Show($"Failed to browse data root: {ex.Message}",
"Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void ExecuteResetDataRootPath()
{
DataRootPath = _xpDataPathService.DefaultRootPath;
}
private void ExecuteSaveDataRootPath()
{
try
{
_xpDataPathService.SaveRootPath(DataRootPath);
DataRootPath = _xpDataPathService.RootPath;
RaisePropertyChanged(nameof(PlanRootPath));
RaisePropertyChanged(nameof(ToolsRootPath));
RaisePropertyChanged(nameof(ResultsRootPath));
RaisePropertyChanged(nameof(ReportRootPath));
LoadBuiltInInspectionModules();
MessageBox.Show("XP data root saved. New save/load dialogs will use the new path immediately.",
"Info", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to save XP data root.");
MessageBox.Show($"Failed to save data root: {ex.Message}",
"Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private bool CanExecuteInsertBuiltInInspectionModule()
{
return SelectedBuiltInInspectionModule != null;
}
private async Task ExecuteInsertBuiltInInspectionModuleAsync()
{
var module = SelectedBuiltInInspectionModule;
if (module == null)
return;
try
{
ShowCncEditor();
await _cncEditorViewModel.InsertInspectionModuleFromPipelineFileAsync(module.FilePath);
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to insert built-in inspection module: {FilePath}", module.FilePath);
MessageBox.Show($"Failed to insert built-in inspection module: {ex.Message}",
"Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void LoadBuiltInInspectionModules()
{
BuiltInInspectionModules.Clear();
try
{
var toolsPath = _xpDataPathService.ToolsPath;
if (!Directory.Exists(toolsPath))
{
SelectedBuiltInInspectionModule = null;
return;
}
var files = Directory
.EnumerateFiles(toolsPath, "*.xpm", SearchOption.AllDirectories)
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
.Select(path => new BuiltInInspectionModuleItem(
GetBuiltInModuleDisplayName(toolsPath, path),
path))
.ToList();
foreach (var file in files)
{
BuiltInInspectionModules.Add(file);
}
SelectedBuiltInInspectionModule = BuiltInInspectionModules.FirstOrDefault();
_logger.Info("Loaded {Count} built-in inspection modules from {ToolsPath}", BuiltInInspectionModules.Count, toolsPath);
}
catch (Exception ex)
{
SelectedBuiltInInspectionModule = null;
_logger.Error(ex, "Failed to load built-in inspection modules.");
}
}
private static string GetBuiltInModuleDisplayName(string toolsPath, string filePath)
{
var relativePath = Path.GetRelativePath(toolsPath, filePath);
var withoutExtension = Path.ChangeExtension(relativePath, null) ?? relativePath;
return withoutExtension.Replace(Path.DirectorySeparatorChar, '/');
}
private void ExecuteAxisReset() private void ExecuteAxisReset()
{ {
var result = MessageBox.Show("确认执行轴复位操作?", "轴复位", var result = MessageBox.Show("Confirm axis reset?", "Axis Reset",
MessageBoxButton.OKCancel, MessageBoxImage.Question); MessageBoxButton.OKCancel, MessageBoxImage.Question);
if (result != MessageBoxResult.OK) if (result != MessageBoxResult.OK)
return; return;
@@ -394,14 +582,54 @@ namespace XplorePlane.ViewModels
var resetResult = motionSystem.AxisReset.Reset(); var resetResult = motionSystem.AxisReset.Reset();
if (!resetResult.Success) if (!resetResult.Success)
{ {
MessageBox.Show($"轴复位失败:{resetResult.ErrorMessage}", "错误", MessageBox.Show($"Axis reset failed: {resetResult.ErrorMessage}", "Error",
MessageBoxButton.OK, MessageBoxImage.Warning); MessageBoxButton.OK, MessageBoxImage.Warning);
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Error(ex, "轴复位异常"); _logger.Error(ex, "Axis reset failed.");
MessageBox.Show($"轴复位异常:{ex.Message}", "错误", MessageBox.Show($"Axis reset error: {ex.Message}", "Error",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void ExecuteOpenDoor()
{
try
{
var motionService = _containerProvider.Resolve<IMotionControlService>();
var result = motionService.OpenDoor();
if (!result.Success)
{
MessageBox.Show($"Open door failed: {result.ErrorMessage}", "Error",
MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
catch (Exception ex)
{
_logger.Error(ex, "Open door failed.");
MessageBox.Show($"Open door error: {ex.Message}", "Error",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void ExecuteCloseDoor()
{
try
{
var motionService = _containerProvider.Resolve<IMotionControlService>();
var result = motionService.CloseDoor();
if (!result.Success)
{
MessageBox.Show($"Close door failed: {result.ErrorMessage}", "Error",
MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
catch (Exception ex)
{
_logger.Error(ex, "Close door failed.");
MessageBox.Show($"Close door error: {ex.Message}", "Error",
MessageBoxButton.OK, MessageBoxImage.Error); MessageBoxButton.OK, MessageBoxImage.Error);
} }
} }
@@ -411,13 +639,13 @@ namespace XplorePlane.ViewModels
try try
{ {
ShowOrActivate(_detectorConfigWindow, w => _detectorConfigWindow = w, ShowOrActivate(_detectorConfigWindow, w => _detectorConfigWindow = w,
() => new XP.Hardware.Detector.Views.DetectorConfigWindow(), "探测器配置"); () => new XP.Hardware.Detector.Views.DetectorConfigWindow(), "Detector Config");
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Error(ex, "打开探测器配置窗口失败"); _logger.Error(ex, "Failed to open detector config window.");
MessageBox.Show($"打开探测器配置窗口失败:\n{ex.InnerException?.Message ?? ex.Message}", MessageBox.Show($"Failed to open detector config window:\n{ex.InnerException?.Message ?? ex.Message}",
"错误", MessageBoxButton.OK, MessageBoxImage.Error); "Error", MessageBoxButton.OK, MessageBoxImage.Error);
} }
} }
@@ -436,7 +664,7 @@ namespace XplorePlane.ViewModels
private void ExecuteOpenRaySourceConfig() private void ExecuteOpenRaySourceConfig()
{ {
ShowOrActivate(_raySourceConfigWindow, w => _raySourceConfigWindow = w, ShowOrActivate(_raySourceConfigWindow, w => _raySourceConfigWindow = w,
() => new XP.Hardware.RaySource.Views.RaySourceConfigWindow(), "射线源配置"); () => new XP.Hardware.RaySource.Views.RaySourceConfigWindow(), "Ray Source Config");
} }
private void ExecuteLoadImage() private void ExecuteLoadImage()
@@ -459,20 +687,20 @@ namespace XplorePlane.ViewModels
bitmap.EndInit(); bitmap.EndInit();
bitmap.Freeze(); bitmap.Freeze();
_logger.Info("[图像链路] ExecuteLoadImage:加载图像 {Path},准备推送到 MainViewportService ManualImageLoadedEvent", dialog.FileName); _logger.Info("[Image] ExecuteLoadImage loaded image {Path} and will push it to MainViewportService and ManualImageLoadedEvent.", dialog.FileName);
_mainViewportService.SetManualImage(bitmap, dialog.FileName); _mainViewportService.SetManualImage(bitmap, dialog.FileName);
// 同时发布事件,让 PipelineEditorViewModel 收到图像并触发流水线执行 // Publish the image to the pipeline editor at the same time.
_eventAggregator.GetEvent<ManualImageLoadedEvent>() _eventAggregator.GetEvent<ManualImageLoadedEvent>()
.Publish(new ManualImageLoadedPayload(bitmap, dialog.FileName)); .Publish(new ManualImageLoadedPayload(bitmap, dialog.FileName));
_logger.Info("[图像链路] ManualImageLoadedEvent 已发布"); _logger.Info("[Image] ManualImageLoadedEvent published.");
RaisePropertyChanged(nameof(IsUsingLiveDetectorSource)); RaisePropertyChanged(nameof(IsUsingLiveDetectorSource));
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Error(ex, "加载图像失败:{Path}", dialog.FileName); _logger.Error(ex, "Failed to load image: {Path}", dialog.FileName);
MessageBox.Show($"加载图像失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); MessageBox.Show($"Failed to load image: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
} }
} }
@@ -484,7 +712,7 @@ namespace XplorePlane.ViewModels
private void ExecuteWarmUp() private void ExecuteWarmUp()
{ {
var messageBoxResult = MessageBox.Show("确认执行射线源暖机操作?", "暖机", var messageBoxResult = MessageBox.Show("Confirm X-ray source warm-up?", "Warm-up",
MessageBoxButton.OKCancel, MessageBoxImage.Question); MessageBoxButton.OKCancel, MessageBoxImage.Question);
if (messageBoxResult != MessageBoxResult.OK) if (messageBoxResult != MessageBoxResult.OK)
return; return;
@@ -495,18 +723,18 @@ namespace XplorePlane.ViewModels
var result = raySourceService.WarmUp(); var result = raySourceService.WarmUp();
if (!result.Success) if (!result.Success)
{ {
MessageBox.Show($"暖机失败:{result.ErrorMessage}", "错误", MessageBox.Show($"Warm-up failed: {result.ErrorMessage}", "Error",
MessageBoxButton.OK, MessageBoxImage.Warning); MessageBoxButton.OK, MessageBoxImage.Warning);
} }
else else
{ {
_logger.Info("暖机命令已发送"); _logger.Info("Warm-up command sent.");
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Error(ex, "暖机异常"); _logger.Error(ex, "Warm-up failed.");
MessageBox.Show($"暖机异常:{ex.Message}", "错误", MessageBox.Show($"Warm-up error: {ex.Message}", "Error",
MessageBoxButton.OK, MessageBoxImage.Error); MessageBoxButton.OK, MessageBoxImage.Error);
} }
} }
@@ -521,7 +749,7 @@ namespace XplorePlane.ViewModels
if (viewportVm?.ImageSource != null) return true; if (viewportVm?.ImageSource != null) return true;
} }
catch { } catch { }
HexMessageBox.Show("请先加载图像", MessageBoxButton.OK, MessageBoxImage.Information); HexMessageBox.Show("Please load an image first", MessageBoxButton.OK, MessageBoxImage.Information);
return false; return false;
} }
@@ -529,28 +757,28 @@ namespace XplorePlane.ViewModels
private void ExecutePointDistanceMeasure() private void ExecutePointDistanceMeasure()
{ {
if (!CheckImageLoaded()) return; if (!CheckImageLoaded()) return;
_logger.Info("点点距测量功能已触发"); _logger.Info("Point distance measurement triggered.");
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.PointDistance); _eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.PointDistance);
} }
private void ExecutePointLineDistanceMeasure() private void ExecutePointLineDistanceMeasure()
{ {
if (!CheckImageLoaded()) return; if (!CheckImageLoaded()) return;
_logger.Info("点线距测量功能已触发"); _logger.Info("Point-line distance measurement triggered.");
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.PointLineDistance); _eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.PointLineDistance);
} }
private void ExecuteAngleMeasure() private void ExecuteAngleMeasure()
{ {
if (!CheckImageLoaded()) return; if (!CheckImageLoaded()) return;
_logger.Info("角度测量功能已触发"); _logger.Info("Angle measurement triggered.");
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.Angle); _eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.Angle);
} }
private void ExecuteThroughHoleFillRateMeasure() private void ExecuteThroughHoleFillRateMeasure()
{ {
if (!CheckImageLoaded()) return; if (!CheckImageLoaded()) return;
_logger.Info("通孔填锡率测量功能已触发"); _logger.Info("Through-hole fill-rate measurement triggered.");
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.ThroughHoleFillRate); _eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.ThroughHoleFillRate);
} }
@@ -559,7 +787,7 @@ namespace XplorePlane.ViewModels
private void ExecuteBgaVoidMeasure() private void ExecuteBgaVoidMeasure()
{ {
if (!CheckImageLoaded()) return; if (!CheckImageLoaded()) return;
_logger.Info("BGA空隙测量功能已触发"); _logger.Info("BGA void measurement triggered.");
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.BgaVoid); _eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.BgaVoid);
if (_bgaMeasurePanel != null && _bgaMeasurePanel.IsVisible) if (_bgaMeasurePanel != null && _bgaMeasurePanel.IsVisible)
@@ -624,12 +852,12 @@ namespace XplorePlane.ViewModels
private void ExecuteBubbleMeasure() private void ExecuteBubbleMeasure()
{ {
if (!CheckImageLoaded()) return; if (!CheckImageLoaded()) return;
_logger.Info("气泡测量功能已触发"); _logger.Info("Bubble measurement triggered.");
// 进入气泡测量模式 // Enter bubble measurement mode.
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.BubbleMeasure); _eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.BubbleMeasure);
// 弹出工具面板 // Open the tool panel.
if (_bubbleMeasurePanel != null && _bubbleMeasurePanel.IsVisible) if (_bubbleMeasurePanel != null && _bubbleMeasurePanel.IsVisible)
{ {
_bubbleMeasurePanel.Activate(); _bubbleMeasurePanel.Activate();
@@ -642,7 +870,7 @@ namespace XplorePlane.ViewModels
}; };
_bubbleMeasurePanel.Closed += (s, e) => _bubbleMeasurePanel.Closed += (s, e) =>
{ {
// 关闭面板时退出气泡测量模式 // Exit bubble measurement mode when the panel closes.
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.None); _eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.None);
}; };
_bubbleMeasurePanel.Show(); _bubbleMeasurePanel.Show();
@@ -663,7 +891,7 @@ namespace XplorePlane.ViewModels
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Error(ex, "打开语言设置失败"); _logger.Error(ex, "Failed to open language settings.");
} }
} }
@@ -675,38 +903,38 @@ namespace XplorePlane.ViewModels
private void OnNavigateHome() private void OnNavigateHome()
{ {
_logger.Info("导航到主页"); _logger.Info("Navigated to home.");
LicenseInfo = "页"; LicenseInfo = "页";
} }
private void OnNavigateInspect() private void OnNavigateInspect()
{ {
_logger.Info("导航到检测页面"); _logger.Info("Navigated to inspection page.");
LicenseInfo = "检测页面"; LicenseInfo = "Inspection";
} }
private void OnOpenFile() private void OnOpenFile()
{ {
_logger.Info("打开文件"); _logger.Info("Open file.");
LicenseInfo = "打开文件"; LicenseInfo = "打开文件";
} }
private void OnExport() private void OnExport()
{ {
_logger.Info("导出数据"); _logger.Info("Export data.");
LicenseInfo = "导出数据"; LicenseInfo = "导出数据";
} }
private void OnClear() private void OnClear()
{ {
_logger.Info("清除数据"); _logger.Info("Clear data.");
LicenseInfo = "清除数据"; LicenseInfo = "清除数据";
} }
private void OnEditProperties() private void OnEditProperties()
{ {
_logger.Info("编辑属性"); _logger.Info("Edit properties.");
LicenseInfo = "编辑属性"; LicenseInfo = "Edit properties";
} }
private void OnMainViewportStateChanged(object sender, EventArgs e) private void OnMainViewportStateChanged(object sender, EventArgs e)
@@ -722,12 +950,14 @@ namespace XplorePlane.ViewModels
{ {
if (payload?.Image == null) if (payload?.Image == null)
{ {
_logger.Warn("[图像链路] OnPipelinePreviewUpdatedpayload 或 Image null,跳过"); _logger.Warn("[Image] OnPipelinePreviewUpdated skipped because payload or image is null.");
return; return;
} }
_logger.Info("[图像链路] OnPipelinePreviewUpdated:收到流水线结果图像,推送到 MainViewportService"); _logger.Info("[Image] OnPipelinePreviewUpdated received a pipeline preview image and pushed it to MainViewportService.");
_mainViewportService.SetManualImage(payload.Image, string.Empty); _mainViewportService.SetManualImage(payload.Image, string.Empty);
} }
public sealed record BuiltInInspectionModuleItem(string DisplayName, string FilePath);
#endregion #endregion
} }
} }
@@ -6,6 +6,8 @@ using System.Collections.ObjectModel;
using System.Windows; using System.Windows;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using XP.Camera; using XP.Camera;
using XplorePlane.Models;
using XplorePlane.Services.AppState;
namespace XplorePlane.ViewModels namespace XplorePlane.ViewModels
{ {
@@ -16,6 +18,7 @@ namespace XplorePlane.ViewModels
{ {
private static readonly ILogger _logger = Log.ForContext<NavigationPropertyPanelViewModel>(); private static readonly ILogger _logger = Log.ForContext<NavigationPropertyPanelViewModel>();
private readonly ICameraController _camera; private readonly ICameraController _camera;
private readonly IAppStateService _appStateService;
private volatile bool _liveViewRunning; private volatile bool _liveViewRunning;
private bool _disposed; private bool _disposed;
@@ -161,9 +164,10 @@ namespace XplorePlane.ViewModels
#endregion Commands #endregion Commands
public NavigationPropertyPanelViewModel(ICameraController camera) public NavigationPropertyPanelViewModel(ICameraController camera, IAppStateService appStateService)
{ {
_camera = camera ?? throw new ArgumentNullException(nameof(camera)); _camera = camera ?? throw new ArgumentNullException(nameof(camera));
_appStateService = appStateService ?? throw new ArgumentNullException(nameof(appStateService));
ConnectCameraCommand = new DelegateCommand(ConnectCamera, () => !IsCameraConnected); ConnectCameraCommand = new DelegateCommand(ConnectCamera, () => !IsCameraConnected);
DisconnectCameraCommand = new DelegateCommand(DisconnectCamera, () => IsCameraConnected); DisconnectCameraCommand = new DelegateCommand(DisconnectCamera, () => IsCameraConnected);
@@ -198,6 +202,7 @@ namespace XplorePlane.ViewModels
IsCameraConnected = true; IsCameraConnected = true;
CameraStatusText = "已连接"; CameraStatusText = "已连接";
RefreshCameraParams(); RefreshCameraParams();
SyncCameraStateToAppState();
StartGrab(); StartGrab();
IsLiveViewEnabled = true; IsLiveViewEnabled = true;
} }
@@ -217,12 +222,14 @@ namespace XplorePlane.ViewModels
CameraStatusText = $"已连接: {info.ModelName} (SN: {info.SerialNumber})"; CameraStatusText = $"已连接: {info.ModelName} (SN: {info.SerialNumber})";
_logger.Information("Camera connected: {ModelName}", info.ModelName); _logger.Information("Camera connected: {ModelName}", info.ModelName);
RefreshCameraParams(); RefreshCameraParams();
SyncCameraStateToAppState();
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Error(ex, "Failed to connect camera"); _logger.Error(ex, "Failed to connect camera");
CameraStatusText = $"连接失败: {ex.Message}"; CameraStatusText = $"连接失败: {ex.Message}";
IsCameraConnected = false; IsCameraConnected = false;
SyncCameraStateToAppState();
} }
} }
@@ -246,6 +253,7 @@ namespace XplorePlane.ViewModels
IsCameraGrabbing = false; IsCameraGrabbing = false;
CameraStatusText = "未连接"; CameraStatusText = "未连接";
CameraImageSource = null; CameraImageSource = null;
SyncCameraStateToAppState();
_logger.Information("Camera disconnected"); _logger.Information("Camera disconnected");
} }
} }
@@ -257,6 +265,7 @@ namespace XplorePlane.ViewModels
_camera.StartGrabbing(); _camera.StartGrabbing();
IsCameraGrabbing = true; IsCameraGrabbing = true;
CameraStatusText = "采集中..."; CameraStatusText = "采集中...";
SyncCameraStateToAppState();
// 如果已勾选实时,自动启动 Live View // 如果已勾选实时,自动启动 Live View
if (IsLiveViewEnabled) if (IsLiveViewEnabled)
@@ -279,6 +288,7 @@ namespace XplorePlane.ViewModels
_camera.StopGrabbing(); _camera.StopGrabbing();
IsCameraGrabbing = false; IsCameraGrabbing = false;
CameraStatusText = "已停止采集"; CameraStatusText = "已停止采集";
SyncCameraStateToAppState();
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -402,11 +412,33 @@ namespace XplorePlane.ViewModels
IsCameraGrabbing = false; IsCameraGrabbing = false;
CameraStatusText = "连接已断开"; CameraStatusText = "连接已断开";
CameraImageSource = null; CameraImageSource = null;
SyncCameraStateToAppState();
}); });
} }
#endregion Camera Event Handlers #endregion Camera Event Handlers
#region AppState Sync
/// <summary>
/// 将当前相机连接/采集状态同步到 AppStateService.CameraState。
/// 仅同步连接状态、采集状态和图像尺寸,不同步帧数据(避免高频触发 UI 刷新)。
/// </summary>
private void SyncCameraStateToAppState()
{
if (_appStateService == null) return;
_appStateService.UpdateCameraState(new CameraState(
IsConnected: IsCameraConnected,
IsStreaming: IsCameraGrabbing,
CurrentFrame: null,
Width: IsCameraConnected ? ImageWidth : 0,
Height: IsCameraConnected ? ImageHeight : 0,
FrameRate: 0));
}
#endregion AppState Sync
#region IDisposable #region IDisposable
public void Dispose() public void Dispose()
@@ -6,6 +6,8 @@ using System.Windows;
using System.Windows.Media; using System.Windows.Media;
using XP.Common.Logging.Interfaces; using XP.Common.Logging.Interfaces;
using XplorePlane.Events; using XplorePlane.Events;
using XplorePlane.Models;
using XplorePlane.Services.AppState;
using XplorePlane.Services.MainViewport; using XplorePlane.Services.MainViewport;
namespace XplorePlane.ViewModels namespace XplorePlane.ViewModels
@@ -18,6 +20,7 @@ namespace XplorePlane.ViewModels
private readonly ILoggerService _logger; private readonly ILoggerService _logger;
private readonly IMainViewportService _mainViewportService; private readonly IMainViewportService _mainViewportService;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly IAppStateService _appStateService;
private ImageSource _imageSource; private ImageSource _imageSource;
private string _imageInfo = "等待探测器图像..."; private string _imageInfo = "等待探测器图像...";
@@ -26,21 +29,42 @@ namespace XplorePlane.ViewModels
private Point? _measurePoint2; private Point? _measurePoint2;
private string _measurementResult; private string _measurementResult;
// Task 5.2: IsRealtimeEnabled backing field
private bool _isRealtimeEnabled;
// Task 5.3: IsDetectorConnected backing field
private bool _isDetectorConnected = true;
// Task 5.4: IsCncRunning backing field
private bool _isCncRunning;
public ViewportPanelViewModel( public ViewportPanelViewModel(
IMainViewportService mainViewportService, IMainViewportService mainViewportService,
IEventAggregator eventAggregator, IEventAggregator eventAggregator,
IAppStateService appStateService,
ILoggerService logger) ILoggerService logger)
{ {
_mainViewportService = mainViewportService ?? throw new ArgumentNullException(nameof(mainViewportService)); _mainViewportService = mainViewportService ?? throw new ArgumentNullException(nameof(mainViewportService));
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_appStateService = appStateService ?? throw new ArgumentNullException(nameof(appStateService));
_logger = logger?.ForModule<ViewportPanelViewModel>() ?? throw new ArgumentNullException(nameof(logger)); _logger = logger?.ForModule<ViewportPanelViewModel>() ?? throw new ArgumentNullException(nameof(logger));
CancelMeasurementCommand = new DelegateCommand(ExecuteCancelMeasurement); CancelMeasurementCommand = new DelegateCommand(ExecuteCancelMeasurement);
// Task 5.5: ToggleRealtimeCommand
ToggleRealtimeCommand = new DelegateCommand(() => IsRealtimeEnabled = !IsRealtimeEnabled);
_mainViewportService.StateChanged += OnMainViewportStateChanged; _mainViewportService.StateChanged += OnMainViewportStateChanged;
_eventAggregator.GetEvent<MeasurementToolEvent>() _eventAggregator.GetEvent<MeasurementToolEvent>()
.Subscribe(OnMeasurementToolActivated, ThreadOption.UIThread); .Subscribe(OnMeasurementToolActivated, ThreadOption.UIThread);
// Task 5.6: Subscribe to DetectorStateChanged
_appStateService.DetectorStateChanged += OnDetectorStateChanged;
// Task 5.7: Subscribe to DetectorDisconnectedEvent on UI thread
_eventAggregator.GetEvent<DetectorDisconnectedEvent>()
.Subscribe(OnDetectorDisconnectedForUI, ThreadOption.UIThread);
UpdateFromState(updateInfo: true); UpdateFromState(updateInfo: true);
} }
@@ -56,6 +80,31 @@ namespace XplorePlane.ViewModels
set => SetProperty(ref _imageInfo, value); set => SetProperty(ref _imageInfo, value);
} }
// Task 5.2: IsRealtimeEnabled property (two-way binding)
public bool IsRealtimeEnabled
{
get => _isRealtimeEnabled;
set
{
if (SetProperty(ref _isRealtimeEnabled, value))
_mainViewportService.SetRealtimeDisplayEnabled(value);
}
}
// Task 5.3: IsDetectorConnected property (read-only, private setter)
public bool IsDetectorConnected
{
get => _isDetectorConnected;
private set
{
if (SetProperty(ref _isDetectorConnected, value))
RaisePropertyChanged(nameof(IsAnimatedSwitchEnabled));
}
}
// Task 5.4: IsAnimatedSwitchEnabled computed property
public bool IsAnimatedSwitchEnabled => _isDetectorConnected && !_isCncRunning;
public MeasurementToolMode CurrentMeasurementMode public MeasurementToolMode CurrentMeasurementMode
{ {
get => _currentMeasurementMode; get => _currentMeasurementMode;
@@ -100,6 +149,9 @@ namespace XplorePlane.ViewModels
public DelegateCommand CancelMeasurementCommand { get; } public DelegateCommand CancelMeasurementCommand { get; }
// Task 5.5: ToggleRealtimeCommand
public DelegateCommand ToggleRealtimeCommand { get; }
public void ResetMeasurementState() public void ResetMeasurementState()
{ {
MeasurePoint1 = null; MeasurePoint1 = null;
@@ -151,10 +203,40 @@ namespace XplorePlane.ViewModels
{ {
ImageSource = _mainViewportService.CurrentDisplayImage; ImageSource = _mainViewportService.CurrentDisplayImage;
// Task 5.8: Sync IsRealtimeEnabled from service
_isRealtimeEnabled = _mainViewportService.IsRealtimeDisplayEnabled;
RaisePropertyChanged(nameof(IsRealtimeEnabled));
// Task 5.8: Sync _isCncRunning from service and raise IsAnimatedSwitchEnabled
_isCncRunning = _mainViewportService.IsCncRunning;
RaisePropertyChanged(nameof(IsAnimatedSwitchEnabled));
if (updateInfo) if (updateInfo)
{ {
ImageInfo = _mainViewportService.CurrentDisplayInfo; ImageInfo = _mainViewportService.CurrentDisplayInfo;
} }
} }
// Task 5.6: Handle DetectorStateChanged on background thread, dispatch to UI
private void OnDetectorStateChanged(object sender, StateChangedEventArgs<DetectorState> e)
{
Application.Current?.Dispatcher?.BeginInvoke(new Action(() =>
{
IsDetectorConnected = e.NewValue.IsConnected;
}));
}
// Task 5.7: Handle DetectorDisconnectedEvent on UI thread
private void OnDetectorDisconnectedForUI()
{
if (_isCncRunning)
{
MessageBox.Show(
"Detector disconnected, CNC has been automatically stopped. Please check the detector connection before continuing.",
"Detector Disconnected",
MessageBoxButton.OK,
MessageBoxImage.Warning);
}
}
} }
} }
+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"
+207 -35
View File
@@ -10,7 +10,7 @@
xmlns:views="clr-namespace:XplorePlane.Views" xmlns:views="clr-namespace:XplorePlane.Views"
xmlns:vm="clr-namespace:XplorePlane.ViewModels.Cnc" xmlns:vm="clr-namespace:XplorePlane.ViewModels.Cnc"
d:DesignHeight="760" d:DesignHeight="760"
d:DesignWidth="502" d:DesignWidth="452"
prism:ViewModelLocator.AutoWireViewModel="True" prism:ViewModelLocator.AutoWireViewModel="True"
mc:Ignorable="d"> mc:Ignorable="d">
@@ -96,11 +96,26 @@
<Setter Property="FontFamily" Value="{StaticResource UiFont}" /> <Setter Property="FontFamily" Value="{StaticResource UiFont}" />
<Setter Property="FontSize" Value="11" /> <Setter Property="FontSize" Value="11" />
</Style> </Style>
<Style x:Key="TreeToolbarButtonCompact" TargetType="Button" BasedOn="{StaticResource TreeToolbarButton}">
<Setter Property="Width" Value="28" />
<Setter Property="Height" Value="28" />
<Setter Property="MinWidth" Value="28" />
<Setter Property="Margin" Value="0,0,4,4" />
<Setter Property="Padding" Value="0" />
<Setter Property="FontSize" Value="10.5" />
</Style>
<Style x:Key="TreeToolbarIcon" TargetType="Image">
<Setter Property="Width" Value="14" />
<Setter Property="Height" Value="14" />
<Setter Property="Margin" Value="0" />
<Setter Property="Stretch" Value="Uniform" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
</UserControl.Resources> </UserControl.Resources>
<Border <Border
Width="502" Width="452"
MinWidth="502" MinWidth="452"
HorizontalAlignment="Left" HorizontalAlignment="Left"
Background="{StaticResource PanelBg}" Background="{StaticResource PanelBg}"
BorderBrush="{StaticResource PanelBorder}" BorderBrush="{StaticResource PanelBorder}"
@@ -108,7 +123,7 @@
CornerRadius="4"> CornerRadius="4">
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="200" /> <ColumnDefinition Width="180" />
<ColumnDefinition Width="1" /> <ColumnDefinition Width="1" />
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
@@ -125,24 +140,109 @@
Background="{StaticResource HeaderBg}" Background="{StaticResource HeaderBg}"
BorderBrush="{StaticResource SeparatorBrush}" BorderBrush="{StaticResource SeparatorBrush}"
BorderThickness="0,0,0,1"> BorderThickness="0,0,0,1">
<WrapPanel> <StackPanel>
<WrapPanel/>
<WrapPanel Margin="0,4,0,0" Visibility="Collapsed">
<Button <Button
Command="{Binding NewProgramCommand}" Command="{Binding InsertReferencePointCommand}"
Content="新建" Content="参考点"
Style="{StaticResource TreeToolbarButton}" /> Style="{StaticResource TreeToolbarButtonCompact}" />
<Button <Button
Command="{Binding SaveProgramCommand}" Command="{Binding InsertSavePositionCommand}"
Content="保存" Content="添加位置"
Style="{StaticResource TreeToolbarButton}" /> Style="{StaticResource TreeToolbarButtonCompact}" />
<Button <Button
Command="{Binding LoadProgramCommand}" Command="{Binding InsertInspectionModuleCommand}"
Content="加载" Content="检测模块"
Style="{StaticResource TreeToolbarButton}" /> Style="{StaticResource TreeToolbarButtonCompact}" />
<Button <Button
Command="{Binding ExportCsvCommand}" Command="{Binding InsertInspectionMarkerCommand}"
Content="导出" Content="检测标记"
Style="{StaticResource TreeToolbarButton}" /> Style="{StaticResource TreeToolbarButtonCompact}" />
<Button
Command="{Binding InsertPauseDialogCommand}"
Content="消息弹窗"
Style="{StaticResource TreeToolbarButtonCompact}" />
<Button
Command="{Binding InsertWaitDelayCommand}"
Content="插入等待"
Style="{StaticResource TreeToolbarButtonCompact}" />
<Button
Command="{Binding InsertCompleteProgramCommand}"
Content="完成"
Style="{StaticResource TreeToolbarButtonCompact}" />
</WrapPanel> </WrapPanel>
<WrapPanel Margin="0,4,0,0">
<WrapPanel.Resources>
<Style TargetType="TextBlock">
<Setter Property="Visibility" Value="Collapsed" />
</Style>
</WrapPanel.Resources>
<Button
Command="{Binding InsertReferencePointCommand}"
Style="{StaticResource TreeToolbarButtonCompact}"
ToolTip="参考点">
<StackPanel Orientation="Horizontal">
<Image Source="/Assets/Icons/reference.png" Style="{StaticResource TreeToolbarIcon}" />
<TextBlock Text="参考点" />
</StackPanel>
</Button>
<Button
Command="{Binding InsertSavePositionCommand}"
Style="{StaticResource TreeToolbarButtonCompact}"
ToolTip="添加位置">
<StackPanel Orientation="Horizontal">
<Image Source="/Assets/Icons/add-pos.png" Style="{StaticResource TreeToolbarIcon}" />
<TextBlock Text="添加位置" />
</StackPanel>
</Button>
<Button
Command="{Binding InsertInspectionModuleCommand}"
Style="{StaticResource TreeToolbarButtonCompact}"
ToolTip="检测模块">
<StackPanel Orientation="Horizontal">
<Image Source="/Assets/Icons/Module.png" Style="{StaticResource TreeToolbarIcon}" />
<TextBlock Text="检测模块" />
</StackPanel>
</Button>
<Button
Command="{Binding InsertInspectionMarkerCommand}"
Style="{StaticResource TreeToolbarButtonCompact}"
ToolTip="检测标记">
<StackPanel Orientation="Horizontal">
<Image Source="/Assets/Icons/mark.png" Style="{StaticResource TreeToolbarIcon}" />
<TextBlock Text="检测标记" />
</StackPanel>
</Button>
<Button
Command="{Binding InsertPauseDialogCommand}"
Style="{StaticResource TreeToolbarButtonCompact}"
ToolTip="消息弹窗">
<StackPanel Orientation="Horizontal">
<Image Source="/Assets/Icons/message.png" Style="{StaticResource TreeToolbarIcon}" />
<TextBlock Text="消息弹窗" />
</StackPanel>
</Button>
<Button
Command="{Binding InsertWaitDelayCommand}"
Style="{StaticResource TreeToolbarButtonCompact}"
ToolTip="插入等待">
<StackPanel Orientation="Horizontal">
<Image Source="/Assets/Icons/wait.png" Style="{StaticResource TreeToolbarIcon}" />
<TextBlock Text="插入等待" />
</StackPanel>
</Button>
<Button
Command="{Binding InsertCompleteProgramCommand}"
Style="{StaticResource TreeToolbarButtonCompact}"
ToolTip="完成">
<StackPanel Orientation="Horizontal">
<Image Source="/Assets/Icons/finish.png" Style="{StaticResource TreeToolbarIcon}" />
<TextBlock Text="完成" />
</StackPanel>
</Button>
</WrapPanel>
</StackPanel>
</Border> </Border>
<TreeView <TreeView
@@ -180,6 +280,7 @@
FontFamily="{StaticResource UiFont}" FontFamily="{StaticResource UiFont}"
FontSize="12" FontSize="12"
FontWeight="SemiBold" FontWeight="SemiBold"
x:Name="ProgramRootNameText"
Text="{Binding DisplayName}" Text="{Binding DisplayName}"
TextTrimming="CharacterEllipsis" /> TextTrimming="CharacterEllipsis" />
</StackPanel> </StackPanel>
@@ -188,6 +289,7 @@
<DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=TreeViewItem}}" Value="True"> <DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=TreeViewItem}}" Value="True">
<Setter TargetName="ProgramRootCard" Property="Background" Value="#E7F1FB" /> <Setter TargetName="ProgramRootCard" Property="Background" Value="#E7F1FB" />
<Setter TargetName="ProgramRootCard" Property="BorderBrush" Value="#9FC6E8" /> <Setter TargetName="ProgramRootCard" Property="BorderBrush" Value="#9FC6E8" />
<Setter TargetName="ProgramRootNameText" Property="Foreground" Value="#111111" />
</DataTrigger> </DataTrigger>
</DataTemplate.Triggers> </DataTemplate.Triggers>
</HierarchicalDataTemplate> </HierarchicalDataTemplate>
@@ -205,6 +307,10 @@
BorderThickness="1" BorderThickness="1"
CornerRadius="4"> CornerRadius="4">
<Grid x:Name="NodeRoot" MinHeight="23"> <Grid x:Name="NodeRoot" MinHeight="23">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="15" /> <ColumnDefinition Width="15" />
<ColumnDefinition Width="20" /> <ColumnDefinition Width="20" />
@@ -212,7 +318,7 @@
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Grid Grid.Column="0"> <Grid Grid.RowSpan="2" Grid.Column="0">
<Border <Border
x:Name="ChildStem" x:Name="ChildStem"
Width="1" Width="1"
@@ -230,6 +336,7 @@
</Grid> </Grid>
<Border <Border
Grid.RowSpan="2"
Grid.Column="1" Grid.Column="1"
Width="16" Width="16"
Height="16" Height="16"
@@ -246,6 +353,7 @@
<TextBlock <TextBlock
x:Name="NodeNameText" x:Name="NodeNameText"
Grid.Row="0"
Grid.Column="2" Grid.Column="2"
Margin="3,0,0,0" Margin="3,0,0,0"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -257,6 +365,7 @@
<StackPanel <StackPanel
x:Name="NodeActions" x:Name="NodeActions"
Grid.Row="0"
Grid.Column="3" Grid.Column="3"
Margin="0,0,2,0" Margin="0,0,2,0"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -275,6 +384,31 @@
FontSize="10" FontSize="10"
ToolTip="删除" /> ToolTip="删除" />
</StackPanel> </StackPanel>
<Grid
x:Name="WaitDelayProgressHost"
Grid.Row="1"
Grid.Column="2"
Grid.ColumnSpan="2"
Margin="3,2,6,1"
Visibility="Collapsed">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ProgressBar
Height="6"
Minimum="0"
Maximum="100"
Value="{Binding ExecutionProgressPercent}" />
<TextBlock
Grid.Column="1"
Margin="6,-4,0,0"
VerticalAlignment="Center"
FontFamily="{StaticResource UiFont}"
FontSize="10"
Foreground="#444444"
Text="{Binding ExecutionProgressText}" />
</Grid>
</Grid> </Grid>
</Border> </Border>
<DataTemplate.Triggers> <DataTemplate.Triggers>
@@ -287,22 +421,30 @@
<Setter TargetName="NodeActions" Property="Visibility" Value="Visible" /> <Setter TargetName="NodeActions" Property="Visibility" Value="Visible" />
<Setter TargetName="NodeCard" Property="Background" Value="#DCEEFF" /> <Setter TargetName="NodeCard" Property="Background" Value="#DCEEFF" />
<Setter TargetName="NodeCard" Property="BorderBrush" Value="#71A9DB" /> <Setter TargetName="NodeCard" Property="BorderBrush" Value="#71A9DB" />
<Setter TargetName="NodeNameText" Property="Foreground" Value="#1F2D3D" />
</DataTrigger> </DataTrigger>
<DataTrigger Binding="{Binding ExecutionState}" Value="Running"> <DataTrigger Binding="{Binding ExecutionState}" Value="Running">
<Setter TargetName="NodeCard" Property="Background" Value="#FF1E6FD9" /> <Setter TargetName="NodeCard" Property="Background" Value="#FFD54F" />
<Setter TargetName="NodeCard" Property="BorderBrush" Value="#FF1E6FD9" /> <Setter TargetName="NodeCard" Property="BorderBrush" Value="#C89B00" />
<Setter TargetName="NodeNameText" Property="Foreground" Value="White" /> <Setter TargetName="NodeNameText" Property="Foreground" Value="#1F1F1F" />
</DataTrigger> </DataTrigger>
<DataTrigger Binding="{Binding ExecutionState}" Value="Succeeded"> <DataTrigger Binding="{Binding ExecutionState}" Value="Succeeded">
<Setter TargetName="NodeCard" Property="Background" Value="#FF2E7D32" /> <Setter TargetName="NodeCard" Property="Background" Value="#FF2E7D32" />
<Setter TargetName="NodeCard" Property="BorderBrush" Value="#FF2E7D32" /> <Setter TargetName="NodeCard" Property="BorderBrush" Value="#FF1B5E20" />
<Setter TargetName="NodeNameText" Property="Foreground" Value="White" /> <Setter TargetName="NodeNameText" Property="Foreground" Value="White" />
</DataTrigger> </DataTrigger>
<DataTrigger Binding="{Binding ExecutionState}" Value="Failed"> <DataTrigger Binding="{Binding ExecutionState}" Value="Failed">
<Setter TargetName="NodeCard" Property="Background" Value="#FFC62828" /> <Setter TargetName="NodeCard" Property="Background" Value="#FFC62828" />
<Setter TargetName="NodeCard" Property="BorderBrush" Value="#FFC62828" /> <Setter TargetName="NodeCard" Property="BorderBrush" Value="#FF8E0000" />
<Setter TargetName="NodeNameText" Property="Foreground" Value="White" /> <Setter TargetName="NodeNameText" Property="Foreground" Value="White" />
</DataTrigger> </DataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsWaitDelay}" Value="True" />
<Condition Binding="{Binding IsRunningNode}" Value="True" />
</MultiDataTrigger.Conditions>
<Setter TargetName="WaitDelayProgressHost" Property="Visibility" Value="Visible" />
</MultiDataTrigger>
</DataTemplate.Triggers> </DataTemplate.Triggers>
</HierarchicalDataTemplate> </HierarchicalDataTemplate>
</TreeView.Resources> </TreeView.Resources>
@@ -347,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>
@@ -469,6 +627,20 @@
<StackPanel Margin="10,8,10,6"> <StackPanel Margin="10,8,10,6">
<TextBlock Style="{StaticResource LabelStyle}" Text="延时 (ms)" /> <TextBlock Style="{StaticResource LabelStyle}" Text="延时 (ms)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.DelayMilliseconds, UpdateSourceTrigger=LostFocus}" /> <TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.DelayMilliseconds, UpdateSourceTrigger=LostFocus}" />
<ProgressBar
Height="8"
Margin="0,2,0,0"
Minimum="0"
Maximum="100"
Value="{Binding SelectedNode.ExecutionProgressPercent}"
Visibility="{Binding SelectedNode.IsDelayProgressVisible, Converter={StaticResource BoolToVisibilityConverter}}" />
<TextBlock
Margin="0,4,0,0"
FontFamily="{StaticResource UiFont}"
FontSize="10"
Foreground="#666666"
Text="{Binding SelectedNode.ExecutionProgressText}"
Visibility="{Binding SelectedNode.IsDelayProgressVisible, Converter={StaticResource BoolToVisibilityConverter}}" />
</StackPanel> </StackPanel>
</GroupBox> </GroupBox>
</StackPanel> </StackPanel>
+5 -31
View File
@@ -19,14 +19,6 @@ namespace XplorePlane.Views.Cnc
/// </summary> /// </summary>
public partial class CncPageView : UserControl public partial class CncPageView : UserControl
{ {
private static readonly Brush SelectedNodeBackground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#E7F0F7"));
private static readonly Brush SelectedNodeBorder = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9CB9D1"));
private static readonly Brush HoverNodeBackground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#F6FAFC"));
private static readonly Brush HoverNodeBorder = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#D7E4EE"));
private static readonly Brush SelectedNodeForeground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#1F4E79"));
private static readonly Brush DefaultNodeForeground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#202020"));
private static readonly Brush TransparentBrush = Brushes.Transparent;
private CncInspectionModulePipelineViewModel _inspectionModulePipelineViewModel; private CncInspectionModulePipelineViewModel _inspectionModulePipelineViewModel;
private readonly Dictionary<TextBox, Label> _textDisplayLabels = new(); private readonly Dictionary<TextBox, Label> _textDisplayLabels = new();
private readonly Dictionary<CheckBox, Label> _checkDisplayLabels = new(); private readonly Dictionary<CheckBox, Label> _checkDisplayLabels = new();
@@ -186,24 +178,9 @@ namespace XplorePlane.Views.Cnc
continue; continue;
} }
if (item.IsSelected) card.ClearValue(Border.BackgroundProperty);
{ card.ClearValue(Border.BorderBrushProperty);
card.Background = SelectedNodeBackground; ClearNodeTextForeground(card);
card.BorderBrush = SelectedNodeBorder;
ApplyNodeTextForeground(card, SelectedNodeForeground);
}
else if (card.IsMouseOver)
{
card.Background = HoverNodeBackground;
card.BorderBrush = HoverNodeBorder;
ApplyNodeTextForeground(card, DefaultNodeForeground);
}
else
{
card.Background = TransparentBrush;
card.BorderBrush = TransparentBrush;
ApplyNodeTextForeground(card, DefaultNodeForeground);
}
} }
} }
@@ -318,14 +295,11 @@ namespace XplorePlane.Views.Cnc
panel.Children.Insert(index + 1, companionControl); panel.Children.Insert(index + 1, companionControl);
} }
private static void ApplyNodeTextForeground(Border card, Brush foreground) private static void ClearNodeTextForeground(Border card)
{ {
foreach (var textBlock in FindVisualDescendants<TextBlock>(card)) foreach (var textBlock in FindVisualDescendants<TextBlock>(card))
{ {
if (textBlock.Visibility == Visibility.Visible) textBlock.ClearValue(TextBlock.ForegroundProperty);
{
textBlock.Foreground = foreground;
}
} }
} }
@@ -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"
@@ -63,7 +63,7 @@
<Setter Property="FontWeight" Value="Bold" /> <Setter Property="FontWeight" Value="Bold" />
<Setter Property="Foreground" Value="#1c1c1b" /> <Setter Property="Foreground" Value="#1c1c1b" />
<Setter Property="BorderBrush" Value="#cdcbcb" /> <Setter Property="BorderBrush" Value="#cdcbcb" />
<Setter Property="Padding" Value="4,6,4,4" /> <Setter Property="Padding" Value="3,4,3,3" />
</Style> </Style>
</UserControl.Resources> </UserControl.Resources>
@@ -145,7 +145,7 @@
<!-- 右侧:算子选择 + 参数配置 --> <!-- 右侧:算子选择 + 参数配置 -->
<Border Grid.Column="3" Style="{StaticResource PanelBorderStyle}"> <Border Grid.Column="3" Style="{StaticResource PanelBorderStyle}">
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto"> <ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<StackPanel Margin="10,8,10,10"> <StackPanel Margin="8,6,8,8">
<GroupBox Margin="0,0,0,8" Header="选择算子"> <GroupBox Margin="0,0,0,8" Header="选择算子">
<ComboBox <ComboBox
@@ -4,22 +4,30 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:prism="http://prismlibrary.com/" xmlns:prism="http://prismlibrary.com/"
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
prism:ViewModelLocator.AutoWireViewModel="True" prism:ViewModelLocator.AutoWireViewModel="True"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="600" d:DesignWidth="280"> d:DesignHeight="600"
d:DesignWidth="500">
<UserControl.Resources> <UserControl.Resources>
<SolidColorBrush x:Key="PanelBg" Color="White" /> <SolidColorBrush x:Key="PanelBg" Color="White" />
<SolidColorBrush x:Key="PanelBorder" Color="#cdcbcb" /> <SolidColorBrush x:Key="PanelBorder" Color="#cdcbcb" />
<SolidColorBrush x:Key="CategoryBg" Color="#F5F7FA" />
<SolidColorBrush x:Key="HoverBg" Color="#E8F0FE" /> <SolidColorBrush x:Key="HoverBg" Color="#E8F0FE" />
<FontFamily x:Key="CsdFont">Microsoft YaHei UI</FontFamily> <FontFamily x:Key="CsdFont">Microsoft YaHei UI</FontFamily>
<Style x:Key="OperatorToolboxTabItemStyle" TargetType="TabItem">
<Setter Property="Padding" Value="6,3" />
<Setter Property="Margin" Value="0,0,2,0" />
<Setter Property="FontFamily" Value="{StaticResource CsdFont}" />
<Setter Property="FontSize" Value="10.5" />
<Setter Property="MinWidth" Value="0" />
</Style>
</UserControl.Resources> </UserControl.Resources>
<Border Background="{StaticResource PanelBg}" <Border Background="{StaticResource PanelBg}"
BorderBrush="{StaticResource PanelBorder}" BorderBrush="{StaticResource PanelBorder}"
BorderThickness="1" CornerRadius="4"> BorderThickness="1"
CornerRadius="4">
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
@@ -27,14 +35,24 @@
<RowDefinition Height="*" /> <RowDefinition Height="*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- 标题(支持无边框窗口拖拽) --> <Border x:Name="TitleBar"
<Border x:Name="TitleBar" Grid.Row="0" Background="#0060A0" Padding="10,8"> Grid.Row="0"
Background="#0060A0"
Padding="10,8">
<Grid> <Grid>
<TextBlock Text="🧰 算子工具箱" FontFamily="{StaticResource CsdFont}" <TextBlock Text="算子工具箱"
FontWeight="Bold" FontSize="13" Foreground="White" FontFamily="{StaticResource CsdFont}"
FontWeight="Bold"
FontSize="13"
Foreground="White"
VerticalAlignment="Center" /> VerticalAlignment="Center" />
<Button x:Name="CloseBtn" HorizontalAlignment="Right" VerticalAlignment="Center" <Button x:Name="CloseBtn"
Content="✕" FontSize="12" Foreground="White" Cursor="Hand" HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="×"
FontSize="12"
Foreground="White"
Cursor="Hand"
Visibility="Collapsed" Visibility="Collapsed"
ToolTip="关闭"> ToolTip="关闭">
<Button.Style> <Button.Style>
@@ -46,7 +64,8 @@
<Setter.Value> <Setter.Value>
<ControlTemplate TargetType="Button"> <ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" <Border Background="{TemplateBinding Background}"
CornerRadius="3" Padding="{TemplateBinding Padding}"> CornerRadius="3"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" <ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center" /> VerticalAlignment="Center" />
</Border> </Border>
@@ -64,76 +83,64 @@
</Grid> </Grid>
</Border> </Border>
<!-- 搜索框 --> <Border Grid.Row="1"
<Border Grid.Row="1" Padding="8,6" BorderBrush="{StaticResource PanelBorder}" Padding="8,6"
BorderBrush="{StaticResource PanelBorder}"
BorderThickness="0,0,0,1"> BorderThickness="0,0,0,1">
<TextBox Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}" <TextBox Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}"
FontFamily="{StaticResource CsdFont}" FontSize="11" FontFamily="{StaticResource CsdFont}"
Padding="6,4" BorderBrush="#cdcbcb" BorderThickness="1" FontSize="11"
ToolTip="输入关键字搜索算子"> Padding="6,4"
<TextBox.Style> BorderBrush="#cdcbcb"
<Style TargetType="TextBox"> BorderThickness="1"
<Style.Triggers> ToolTip="输入关键字搜索算子" />
<Trigger Property="Text" Value="">
<Setter Property="Background">
<Setter.Value>
<VisualBrush AlignmentX="Left" AlignmentY="Center" Stretch="None">
<VisualBrush.Visual>
<TextBlock Text="🔍 搜索算子..." Foreground="#aaa"
FontFamily="Microsoft YaHei UI" FontSize="11"
Margin="4,0,0,0" />
</VisualBrush.Visual>
</VisualBrush>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
</Border> </Border>
<!-- 分组算子列表 --> <Grid Grid.Row="2">
<ScrollViewer x:Name="ToolboxListBox" Grid.Row="2" VerticalScrollBarVisibility="Auto"> <TabControl x:Name="ToolboxListBox"
<ItemsControl ItemsSource="{Binding FilteredGroups}"> Margin="8"
<ItemsControl.ItemTemplate> ItemContainerStyle="{StaticResource OperatorToolboxTabItemStyle}"
SelectedItem="{Binding SelectedGroup, Mode=TwoWay}"
ItemsSource="{Binding FilteredGroups}">
<TabControl.ItemTemplate>
<DataTemplate> <DataTemplate>
<StackPanel Margin="0,0,0,2"> <StackPanel Orientation="Horizontal">
<!-- 分类标题 --> <TextBlock Text="{Binding CategoryIcon}"
<Border Background="{StaticResource CategoryBg}" FontSize="11"
BorderBrush="{StaticResource PanelBorder}" VerticalAlignment="Center" />
BorderThickness="0,0,0,1" <TextBlock Text="{Binding CategoryName}"
Padding="10,6"> Margin="3,0,0,0"
<StackPanel Orientation="Horizontal"> FontSize="10.5"
<TextBlock Text="{Binding CategoryIcon}" FontSize="13" VerticalAlignment="Center" />
VerticalAlignment="Center" Margin="0,0,6,0" /> </StackPanel>
<TextBlock Text="{Binding CategoryName}" </DataTemplate>
FontFamily="Microsoft YaHei UI" </TabControl.ItemTemplate>
FontWeight="SemiBold" FontSize="12" <TabControl.ContentTemplate>
Foreground="#333" VerticalAlignment="Center" /> <DataTemplate>
</StackPanel> <ScrollViewer VerticalScrollBarVisibility="Auto">
</Border>
<!-- 分类下的算子 -->
<ItemsControl ItemsSource="{Binding Operators}"> <ItemsControl ItemsSource="{Binding Operators}">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate>
<Border Padding="12,5,8,5" Cursor="Hand" <Border Padding="12,6,8,6"
Cursor="Hand"
Background="Transparent" Background="Transparent"
BorderBrush="Transparent" BorderBrush="#ECECEC"
BorderThickness="0,0,0,1"> BorderThickness="0,0,0,1">
<Border.Style> <Border.Style>
<Style TargetType="Border"> <Style TargetType="Border">
<Style.Triggers> <Style.Triggers>
<Trigger Property="IsMouseOver" Value="True"> <Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#E8F0FE" /> <Setter Property="Background" Value="{StaticResource HoverBg}" />
</Trigger> </Trigger>
</Style.Triggers> </Style.Triggers>
</Style> </Style>
</Border.Style> </Border.Style>
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<Border Width="26" Height="26" <Border Width="28"
Height="28"
Background="#EEF2FF" Background="#EEF2FF"
CornerRadius="4" Margin="0,0,8,0"> CornerRadius="4"
Margin="0,0,8,0">
<TextBlock Text="{Binding IconPath}" <TextBlock Text="{Binding IconPath}"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -154,11 +161,35 @@
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
</ItemsControl> </ItemsControl>
</StackPanel> </ScrollViewer>
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </TabControl.ContentTemplate>
</ItemsControl> </TabControl>
</ScrollViewer>
<Border Margin="8"
Padding="18"
Background="#FAFAFA"
BorderBrush="#E6E6E6"
BorderThickness="1"
CornerRadius="6">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=ToolboxListBox, Path=HasItems}" Value="False">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock Text="没有匹配的算子"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontFamily="{StaticResource CsdFont}"
FontSize="12"
Foreground="#666666" />
</Border>
</Grid>
</Grid> </Grid>
</Border> </Border>
</UserControl> </UserControl>
@@ -3,7 +3,8 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:XplorePlane.Views" xmlns:views="clr-namespace:XplorePlane.Views"
Title="算子工具箱" Title="算子工具箱"
Width="260" Height="500" Width="460" Height="540"
MinWidth="420" MinHeight="500"
WindowStartupLocation="CenterOwner" WindowStartupLocation="CenterOwner"
ShowInTaskbar="False" ShowInTaskbar="False"
WindowStyle="None" WindowStyle="None"
@@ -1,11 +1,12 @@
<UserControl <UserControl
x:Class="XplorePlane.Views.PipelineEditorView" x:Class="XplorePlane.Views.PipelineEditorView"
x:Name="RootControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="700" d:DesignHeight="700"
d:DesignWidth="350" d:DesignWidth="300"
mc:Ignorable="d"> mc:Ignorable="d">
<UserControl.Resources> <UserControl.Resources>
@@ -16,7 +17,7 @@
<SolidColorBrush x:Key="DisabledNodeBg" Color="#F3F3F3" /> <SolidColorBrush x:Key="DisabledNodeBg" Color="#F3F3F3" />
<SolidColorBrush x:Key="DisabledNodeLine" Color="#B9B9B9" /> <SolidColorBrush x:Key="DisabledNodeLine" Color="#B9B9B9" />
<SolidColorBrush x:Key="DisabledNodeText" Color="#8A8A8A" /> <SolidColorBrush x:Key="DisabledNodeText" Color="#8A8A8A" />
<FontFamily x:Key="CsdFont">Microsoft YaHei UI</FontFamily> <FontFamily x:Key="UiFont">Microsoft YaHei UI</FontFamily>
<Style x:Key="PipelineNodeItemStyle" TargetType="ListBoxItem"> <Style x:Key="PipelineNodeItemStyle" TargetType="ListBoxItem">
<Setter Property="Padding" Value="0" /> <Setter Property="Padding" Value="0" />
@@ -36,15 +37,19 @@
</Style> </Style>
<Style x:Key="ToolbarBtn" TargetType="Button"> <Style x:Key="ToolbarBtn" TargetType="Button">
<Setter Property="Width" Value="52" />
<Setter Property="Height" Value="28" /> <Setter Property="Height" Value="28" />
<Setter Property="Width" Value="32" />
<Setter Property="MinWidth" Value="32" />
<Setter Property="Margin" Value="2,0" /> <Setter Property="Margin" Value="2,0" />
<Setter Property="Padding" Value="0" />
<Setter Property="Background" Value="Transparent" /> <Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="#CDCBCB" /> <Setter Property="BorderBrush" Value="#CDCBCB" />
<Setter Property="BorderThickness" Value="1" /> <Setter Property="BorderThickness" Value="1" />
<Setter Property="FontFamily" Value="Microsoft YaHei UI" /> <Setter Property="FontFamily" Value="{StaticResource UiFont}" />
<Setter Property="FontSize" Value="11" /> <Setter Property="FontSize" Value="11" />
<Setter Property="Cursor" Value="Hand" /> <Setter Property="Cursor" Value="Hand" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style> </Style>
</UserControl.Resources> </UserControl.Resources>
@@ -59,7 +64,6 @@
<RowDefinition Height="4*" MinHeight="180" /> <RowDefinition Height="4*" MinHeight="180" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="2*" MinHeight="80" /> <RowDefinition Height="2*" MinHeight="80" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Border <Border
@@ -73,37 +77,53 @@
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<StackPanel <StackPanel
Grid.Row="0" Grid.Row="0"
Orientation="Horizontal"> Orientation="Horizontal">
<Button <Button
Command="{Binding NewPipelineCommand}" Command="{Binding NewPipelineCommand}"
Content="新建" Style="{StaticResource ToolbarBtn}"
Style="{StaticResource ToolbarBtn}" ToolTip="新建配方">
ToolTip="新建流水线" /> <TextBlock
<Button FontFamily="Segoe MDL2 Assets"
Command="{Binding SavePipelineCommand}" FontSize="14"
Content="保存" Text="&#xE710;" />
Style="{StaticResource ToolbarBtn}" </Button>
ToolTip="保存当前流水线" /> <Button
<Button Command="{Binding SavePipelineCommand}"
Width="60" Style="{StaticResource ToolbarBtn}"
Command="{Binding SaveAsPipelineCommand}" ToolTip="保存当前配方">
Content="另存为" <TextBlock
Style="{StaticResource ToolbarBtn}" FontFamily="Segoe MDL2 Assets"
ToolTip="另存当前流水线" /> FontSize="14"
<Button Text="&#xE74E;" />
Width="52" </Button>
Command="{Binding LoadPipelineCommand}" <Button
Content="加载" Command="{Binding SaveAsPipelineCommand}"
Style="{StaticResource ToolbarBtn}" Style="{StaticResource ToolbarBtn}"
ToolTip="加载流水线" /> ToolTip="另存当前配方">
<TextBlock
FontFamily="Segoe MDL2 Assets"
FontSize="14"
Text="&#xE792;" />
</Button>
<Button
Command="{Binding LoadPipelineCommand}"
Style="{StaticResource ToolbarBtn}"
ToolTip="加载配方">
<TextBlock
FontFamily="Segoe MDL2 Assets"
FontSize="14"
Text="&#xE8E5;" />
</Button>
</StackPanel> </StackPanel>
<TextBlock <TextBlock
Grid.Row="1" Grid.Row="1"
Margin="2,4,2,0" Margin="2,4,2,0"
VerticalAlignment="Center" VerticalAlignment="Center"
FontFamily="{StaticResource CsdFont}" FontFamily="{StaticResource UiFont}"
FontSize="11" FontSize="11"
FontWeight="SemiBold" FontWeight="SemiBold"
Foreground="#333333" Foreground="#333333"
@@ -130,10 +150,20 @@
x:Name="NodeContainer" x:Name="NodeContainer"
Margin="2" Margin="2"
Padding="2" Padding="2"
Tag="{Binding DataContext, ElementName=RootControl}"
Background="Transparent" Background="Transparent"
BorderBrush="Transparent" BorderBrush="Transparent"
BorderThickness="1" BorderThickness="1"
CornerRadius="3"> CornerRadius="3">
<Border.ContextMenu>
<ContextMenu>
<MenuItem Header="执行到此处"
Command="{Binding PlacementTarget.Tag.ExecuteToNodeCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
CommandParameter="{Binding}" />
<MenuItem Header="执行全部"
Command="{Binding PlacementTarget.Tag.ClearExecutionRangeCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
</ContextMenu>
</Border.ContextMenu>
<Grid x:Name="NodeRoot" MinHeight="48"> <Grid x:Name="NodeRoot" MinHeight="48">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="44" /> <ColumnDefinition Width="44" />
@@ -182,16 +212,16 @@
<StackPanel Grid.Column="1" Margin="6,0,0,0" VerticalAlignment="Center"> <StackPanel Grid.Column="1" Margin="6,0,0,0" VerticalAlignment="Center">
<TextBlock <TextBlock
x:Name="NodeTitle" x:Name="NodeTitle"
FontFamily="Microsoft YaHei UI" FontFamily="{StaticResource UiFont}"
FontSize="12" FontSize="12"
Text="{Binding DisplayName}" /> Text="{Binding DisplayName}" />
<TextBlock <TextBlock
x:Name="NodeState" x:Name="NodeState"
Margin="0,2,0,0" Margin="0,2,0,0"
FontFamily="Microsoft YaHei UI" FontFamily="{StaticResource UiFont}"
FontSize="10" FontSize="10"
Foreground="#6E6E6E" Foreground="#6E6E6E"
Text="已启用" /> Text="{Binding NodeStateText}" />
</StackPanel> </StackPanel>
</Grid> </Grid>
</Border> </Border>
@@ -207,13 +237,37 @@
<Setter TargetName="IconBorder" Property="BorderBrush" Value="{StaticResource DisabledNodeLine}" /> <Setter TargetName="IconBorder" Property="BorderBrush" Value="{StaticResource DisabledNodeLine}" />
<Setter TargetName="IconBorder" Property="Background" Value="#ECECEC" /> <Setter TargetName="IconBorder" Property="Background" Value="#ECECEC" />
<Setter TargetName="NodeTitle" Property="Foreground" Value="{StaticResource DisabledNodeText}" /> <Setter TargetName="NodeTitle" Property="Foreground" Value="{StaticResource DisabledNodeText}" />
<Setter TargetName="NodeState" Property="Text" Value="已停用" />
<Setter TargetName="NodeState" Property="Foreground" Value="#9A6767" /> <Setter TargetName="NodeState" Property="Foreground" Value="#9A6767" />
</DataTrigger> </DataTrigger>
<DataTrigger Binding="{Binding IsExecutionEndNode}" Value="True">
<Setter TargetName="NodeContainer" Property="Background" Value="#E8F6EA" />
<Setter TargetName="NodeContainer" Property="BorderBrush" Value="#4F9D69" />
<Setter TargetName="IconBorder" Property="Background" Value="#E8F6EA" />
<Setter TargetName="IconBorder" Property="BorderBrush" Value="#4F9D69" />
<Setter TargetName="NodeState" Property="Foreground" Value="#2E7D32" />
</DataTrigger>
<DataTrigger Binding="{Binding IsSkippedByExecutionRange}" Value="True">
<Setter TargetName="NodeContainer" Property="Background" Value="#FAFAFA" />
<Setter TargetName="NodeContainer" Property="Opacity" Value="0.72" />
<Setter TargetName="TopLine" Property="Stroke" Value="#D0D0D0" />
<Setter TargetName="BottomLine" Property="Stroke" Value="#D0D0D0" />
<Setter TargetName="IconBorder" Property="BorderBrush" Value="#C8C8C8" />
<Setter TargetName="IconBorder" Property="Background" Value="#F4F4F4" />
<Setter TargetName="NodeTitle" Property="Foreground" Value="#909090" />
<Setter TargetName="NodeState" Property="Foreground" Value="#A0A0A0" />
</DataTrigger>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=ListBoxItem}, Path=IsSelected}" Value="True"> <DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=ListBoxItem}, Path=IsSelected}" Value="True">
<Setter TargetName="NodeContainer" Property="Background" Value="#D9ECFF" /> <Setter TargetName="NodeContainer" Property="Background" Value="#D9ECFF" />
<Setter TargetName="NodeContainer" Property="BorderBrush" Value="#5B9BD5" /> <Setter TargetName="NodeContainer" Property="BorderBrush" Value="#5B9BD5" />
</DataTrigger> </DataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsExecutionEndNode}" Value="True" />
<Condition Binding="{Binding RelativeSource={RelativeSource AncestorType=ListBoxItem}, Path=IsSelected}" Value="True" />
</MultiDataTrigger.Conditions>
<Setter TargetName="NodeContainer" Property="Background" Value="#DFF0E3" />
<Setter TargetName="NodeContainer" Property="BorderBrush" Value="#3F8A58" />
</MultiDataTrigger>
<MultiDataTrigger> <MultiDataTrigger>
<MultiDataTrigger.Conditions> <MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsEnabled}" Value="False" /> <Condition Binding="{Binding IsEnabled}" Value="False" />
@@ -223,6 +277,15 @@
<Setter TargetName="NodeContainer" Property="BorderBrush" Value="#7E9AB6" /> <Setter TargetName="NodeContainer" Property="BorderBrush" Value="#7E9AB6" />
<Setter TargetName="NodeContainer" Property="Opacity" Value="1" /> <Setter TargetName="NodeContainer" Property="Opacity" Value="1" />
</MultiDataTrigger> </MultiDataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsSkippedByExecutionRange}" Value="True" />
<Condition Binding="{Binding RelativeSource={RelativeSource AncestorType=ListBoxItem}, Path=IsSelected}" Value="True" />
</MultiDataTrigger.Conditions>
<Setter TargetName="NodeContainer" Property="Background" Value="#EEF1F4" />
<Setter TargetName="NodeContainer" Property="BorderBrush" Value="#AAB4BF" />
<Setter TargetName="NodeContainer" Property="Opacity" Value="0.9" />
</MultiDataTrigger>
</DataTemplate.Triggers> </DataTemplate.Triggers>
</DataTemplate> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
@@ -240,7 +303,7 @@
<StackPanel Margin="8,6"> <StackPanel Margin="8,6">
<TextBlock <TextBlock
Margin="0,0,0,4" Margin="0,0,0,4"
FontFamily="{StaticResource CsdFont}" FontFamily="{StaticResource UiFont}"
FontSize="11" FontSize="11"
FontWeight="Bold" FontWeight="Bold"
Foreground="#555" Foreground="#555"
@@ -263,25 +326,69 @@
<ColumnDefinition Width="100" /> <ColumnDefinition Width="100" />
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock <TextBlock
Grid.Column="0" Grid.Column="0"
VerticalAlignment="Center" VerticalAlignment="Center"
FontFamily="Microsoft YaHei UI" FontFamily="{StaticResource UiFont}"
FontSize="11" FontSize="11"
Text="{Binding DisplayName}" Text="{Binding DisplayName}"
TextTrimming="CharacterEllipsis" /> TextTrimming="CharacterEllipsis" />
<Grid Grid.Column="1">
<Grid.Style>
<Style TargetType="Grid">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsSliderInput}" Value="True">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="58" />
</Grid.ColumnDefinitions>
<Slider
Grid.Column="0"
Margin="0,0,8,0"
VerticalAlignment="Center"
IsSnapToTickEnabled="{Binding IsIntegerSlider}"
LargeChange="{Binding SliderTickFrequency}"
Maximum="{Binding SliderMaximum}"
Minimum="{Binding SliderMinimum}"
SmallChange="{Binding SliderTickFrequency}"
TickFrequency="{Binding SliderTickFrequency}"
Value="{Binding SliderValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<Border
Grid.Column="1"
Padding="2,2"
Background="#F8F8F8"
BorderBrush="#CDCBCB"
BorderThickness="1"
CornerRadius="2">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontFamily="{StaticResource UiFont}"
FontSize="11"
Text="{Binding DisplayValueText}" />
</Border>
</Grid>
<TextBox <TextBox
x:Name="TextValueEditor"
Grid.Column="1" Grid.Column="1"
Padding="4,2" Padding="2,2"
BorderBrush="#CDCBCB" BorderBrush="#CDCBCB"
BorderThickness="1" BorderThickness="1"
FontFamily="Microsoft YaHei UI" FontFamily="{StaticResource UiFont}"
FontSize="11" FontSize="11"
Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}"> Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}">
<TextBox.Style> <TextBox.Style>
<Style TargetType="TextBox"> <Style TargetType="TextBox">
<Setter Property="BorderBrush" Value="#CDCBCB" />
<Setter Property="Background" Value="White" /> <Setter Property="Background" Value="White" />
<Setter Property="Visibility" Value="Visible" /> <Setter Property="Visibility" Value="Visible" />
<Style.Triggers> <Style.Triggers>
@@ -295,13 +402,14 @@
</Style> </Style>
</TextBox.Style> </TextBox.Style>
</TextBox> </TextBox>
<ComboBox <ComboBox
Grid.Column="1" Grid.Column="1"
MinHeight="24" MinHeight="24"
Padding="4,1" Padding="4,1"
BorderBrush="#CDCBCB" BorderBrush="#CDCBCB"
BorderThickness="1" BorderThickness="1"
FontFamily="Microsoft YaHei UI" FontFamily="{StaticResource UiFont}"
FontSize="11" FontSize="11"
ItemsSource="{Binding Options}" ItemsSource="{Binding Options}"
SelectedItem="{Binding SelectedOption, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"> SelectedItem="{Binding SelectedOption, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
@@ -316,10 +424,11 @@
</Style> </Style>
</ComboBox.Style> </ComboBox.Style>
</ComboBox> </ComboBox>
<CheckBox <CheckBox
Grid.Column="1" Grid.Column="1"
VerticalAlignment="Center" VerticalAlignment="Center"
FontFamily="Microsoft YaHei UI" FontFamily="{StaticResource UiFont}"
FontSize="11" FontSize="11"
IsChecked="{Binding BoolValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"> IsChecked="{Binding BoolValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<CheckBox.Style> <CheckBox.Style>
@@ -339,42 +448,7 @@
</ItemsControl> </ItemsControl>
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
<Border
Grid.Row="4"
Height="24"
Padding="6,0"
BorderThickness="0,1,0,0">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="#F5F5F5" />
<Setter Property="BorderBrush" Value="{StaticResource PanelBorder}" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsStatusError}" Value="True">
<Setter Property="Background" Value="#FFF1F1" />
<Setter Property="BorderBrush" Value="#D9534F" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock
VerticalAlignment="Center"
FontFamily="{StaticResource CsdFont}"
FontSize="11"
Text="{Binding StatusMessage, StringFormat='Status: {0}'}"
TextTrimming="CharacterEllipsis">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="#555" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsStatusError}" Value="True">
<Setter Property="Foreground" Value="#A12A2A" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Border>
</Grid> </Grid>
</Border> </Border>
</UserControl> </UserControl>
@@ -59,6 +59,8 @@ namespace XplorePlane.Views
PipelineListBox.PreviewMouseMove += OnPreviewMouseMove; PipelineListBox.PreviewMouseMove += OnPreviewMouseMove;
PipelineListBox.PreviewMouseLeftButtonUp -= OnPreviewMouseLeftButtonUp; PipelineListBox.PreviewMouseLeftButtonUp -= OnPreviewMouseLeftButtonUp;
PipelineListBox.PreviewMouseLeftButtonUp += OnPreviewMouseLeftButtonUp; PipelineListBox.PreviewMouseLeftButtonUp += OnPreviewMouseLeftButtonUp;
PipelineListBox.PreviewMouseRightButtonDown -= OnPreviewMouseRightButtonDown;
PipelineListBox.PreviewMouseRightButtonDown += OnPreviewMouseRightButtonDown;
PipelineListBox.MouseDoubleClick -= OnMouseDoubleClick; PipelineListBox.MouseDoubleClick -= OnMouseDoubleClick;
PipelineListBox.MouseDoubleClick += OnMouseDoubleClick; PipelineListBox.MouseDoubleClick += OnMouseDoubleClick;
PipelineListBox.PreviewKeyDown -= OnPreviewKeyDown; PipelineListBox.PreviewKeyDown -= OnPreviewKeyDown;
@@ -133,6 +135,16 @@ namespace XplorePlane.Views
ResetDragState(); ResetDragState();
} }
private void OnPreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
var clickedNode = FindNodeFromOriginalSource(e.OriginalSource);
if (clickedNode == null || IsInteractiveChild(e.OriginalSource))
return;
PipelineListBox.SelectedItem = clickedNode;
PipelineListBox.Focus();
}
private void OnMouseDoubleClick(object sender, MouseButtonEventArgs e) private void OnMouseDoubleClick(object sender, MouseButtonEventArgs e)
{ {
var vm = DataContext as IPipelineEditorHostViewModel; var vm = DataContext as IPipelineEditorHostViewModel;
@@ -10,21 +10,11 @@
ShowInTaskbar="False"> ShowInTaskbar="False">
<Grid Background="#F3F3F3"> <Grid Background="#F3F3F3">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="240" MinWidth="200" />
<ColumnDefinition Width="*" MinWidth="400" /> <ColumnDefinition Width="*" MinWidth="400" />
<ColumnDefinition Width="250" MinWidth="250" /> <ColumnDefinition Width="320" MinWidth="300" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Border Grid.Column="0" <Border Grid.Column="0"
Margin="8"
Background="White"
BorderBrush="#D0D0D0"
BorderThickness="1"
CornerRadius="4">
<views:OperatorToolboxView />
</Border>
<Border Grid.Column="1"
Margin="8,8,4,8" Margin="8,8,4,8"
Background="White" Background="White"
BorderBrush="#D0D0D0" BorderBrush="#D0D0D0"
@@ -34,7 +24,7 @@
Background="White" /> Background="White" />
</Border> </Border>
<Border Grid.Column="2" <Border Grid.Column="1"
Margin="4,8,8,8" Margin="4,8,8,8"
Background="White" Background="White"
BorderBrush="#D0D0D0" BorderBrush="#D0D0D0"
+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"
+110 -83
View File
@@ -138,12 +138,14 @@
</StackPanel> </StackPanel>
<StackPanel> <StackPanel>
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Title="开门" Command="{Binding OpenDoorCommand}"
telerik:ScreenTip.Title="Open Door"
Size="Medium" Size="Medium"
SmallImage="/Assets/Icons/opendoor.png" SmallImage="/Assets/Icons/opendoor.png"
Text="开门" /> Text="开门" />
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Title="关门" Command="{Binding CloseDoorCommand}"
telerik:ScreenTip.Title="Close Door"
Size="Medium" Size="Medium"
SmallImage="/Assets/Icons/closedoor.png" SmallImage="/Assets/Icons/closedoor.png"
Text="关门" /> Text="关门" />
@@ -277,7 +279,34 @@
Size="Large" Size="Large"
SmallImage="/Assets/Icons/cnc.png" SmallImage="/Assets/Icons/cnc.png"
Text="CNC 编辑" /> Text="CNC 编辑" />
<telerik:RadRibbonButton
telerik:ScreenTip.Description="打开矩阵编排窗口,配置多工件阵列检测方案"
telerik:ScreenTip.Title="矩阵编排"
Command="{Binding OpenMatrixEditorCommand}"
Size="Large"
SmallImage="/Assets/Icons/matrix.png"
Text="矩阵编排" />
<StackPanel Width="170">
<TextBlock
Margin="0,0,0,4"
HorizontalAlignment="Center"
Text="内置检测模块" />
<telerik:RadRibbonComboBox
Width="160"
ItemsSource="{Binding BuiltInInspectionModules}"
DisplayMemberPath="DisplayName"
SelectedItem="{Binding SelectedBuiltInInspectionModule}"
IsEditable="False" />
<telerik:RadRibbonButton
Margin="0,4,0,0"
telerik:ScreenTip.Description="从 Tools 目录扫描到的 .xpm 中选择一个配方,并插入到当前 CNC 程序中"
telerik:ScreenTip.Title="插入内置检测模块"
Command="{Binding InsertBuiltInInspectionModuleCommand}"
Size="Medium"
SmallImage="/Assets/Icons/Module.png"
Text="插入模块" />
</StackPanel>
<!--
<StackPanel> <StackPanel>
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Title="参考点" telerik:ScreenTip.Title="参考点"
@@ -311,12 +340,7 @@
Command="{Binding InsertInspectionModuleCommand}" Command="{Binding InsertInspectionModuleCommand}"
SmallImage="/Assets/Icons/Module.png" SmallImage="/Assets/Icons/Module.png"
Text="检测模块" /> Text="检测模块" />
<telerik:RadRibbonButton
telerik:ScreenTip.Title="全部保存"
Size="Medium"
Command="{Binding SaveCncProgramCommand}"
SmallImage="/Assets/Icons/saveall.png"
Text="全部保存" />
</StackPanel> </StackPanel>
<StackPanel> <StackPanel>
<telerik:RadRibbonButton <telerik:RadRibbonButton
@@ -332,14 +356,9 @@
SmallImage="/Assets/Icons/wait.png" SmallImage="/Assets/Icons/wait.png"
Text="插入等待" /> Text="插入等待" />
</StackPanel> </StackPanel>
-->
<telerik:RadRibbonButton
telerik:ScreenTip.Description="打开矩阵编排窗口,配置多工件阵列检测方案"
telerik:ScreenTip.Title="矩阵编排"
Command="{Binding OpenMatrixEditorCommand}"
Size="Large"
SmallImage="/Assets/Icons/matrix.png"
Text="矩阵编排" />
</telerik:RadRibbonGroup> </telerik:RadRibbonGroup>
<telerik:RadRibbonGroup Header="高级模块" IsEnabled="{Binding Path=CellsGroup.IsEnabled}"> <telerik:RadRibbonGroup Header="高级模块" IsEnabled="{Binding Path=CellsGroup.IsEnabled}">
@@ -385,7 +404,24 @@
SmallImage="/Assets/Icons/spiral.png" /> SmallImage="/Assets/Icons/spiral.png" />
</telerik:RadRibbonGroup> </telerik:RadRibbonGroup>
</telerik:RadRibbonTab> </telerik:RadRibbonTab>
<telerik:RadRibbonTab Header="操作">
</telerik:RadRibbonTab>
<telerik:RadRibbonTab Header="设置"> <telerik:RadRibbonTab Header="设置">
<telerik:RadRibbonGroup Header="全局设置">
<telerik:RadRibbonGroup.Variants>
<telerik:GroupVariant Priority="0" Variant="Large" />
</telerik:RadRibbonGroup.Variants>
<telerik:RadRibbonButton
Size="Large"
SmallImage="/Assets/Icons/setting.png"
Command="{Binding OpenSettingsCommand}"
Text="全局设置" />
</telerik:RadRibbonGroup>
<telerik:RadRibbonGroup <telerik:RadRibbonGroup
telerik:ScreenTip.Description="Show the Alignment tab of the Format Cells dialog box." telerik:ScreenTip.Description="Show the Alignment tab of the Format Cells dialog box."
telerik:ScreenTip.Title="Format Cells: Alignment" telerik:ScreenTip.Title="Format Cells: Alignment"
@@ -402,22 +438,7 @@
<spreadsheetControls:RadVerticalAlignmentToBooleanConverter x:Key="verticalAlignmentToBooleanConverter" /> <spreadsheetControls:RadVerticalAlignmentToBooleanConverter x:Key="verticalAlignmentToBooleanConverter" />
</telerik:RadRibbonGroup.Resources> </telerik:RadRibbonGroup.Resources>
<StackPanel> <StackPanel/>
<telerik:RadRibbonToggleButton
telerik:ScreenTip.Description="暖机"
telerik:ScreenTip.Title="暖机"
Command="{Binding WarmUpCommand}"
Size="Medium"
SmallImage="/Assets/Icons/heat-engine.png"
Text="暖机" />
<telerik:RadRibbonToggleButton
telerik:ScreenTip.Description="轴复位"
telerik:ScreenTip.Title="轴复位"
Command="{Binding AxisResetCommand}"
Size="Medium"
SmallImage="/Assets/Icons/home.png"
Text="轴复位" />
</StackPanel>
<StackPanel> <StackPanel>
<telerik:RadRibbonToggleButton <telerik:RadRibbonToggleButton
@@ -428,56 +449,67 @@
SmallImage="/Assets/Icons/xray.png" SmallImage="/Assets/Icons/xray.png"
Text="射线源" /> Text="射线源" />
<telerik:RadRibbonToggleButton <telerik:RadRibbonToggleButton
telerik:ScreenTip.Description="探测器控制" telerik:ScreenTip.Description="探测器控制"
telerik:ScreenTip.Title="探测器" telerik:ScreenTip.Title="探测器"
Command="{Binding OpenDetectorConfigCommand}" Command="{Binding OpenDetectorConfigCommand}"
Size="Medium" Size="Medium"
SmallImage="/Assets/Icons/detector2.png" SmallImage="/Assets/Icons/detector2.png"
Text="探测器" /> Text="探测器" />
<telerik:RadRibbonToggleButton <telerik:RadRibbonToggleButton
telerik:ScreenTip.Description="运动控制" telerik:ScreenTip.Description="运动控制"
telerik:ScreenTip.Title="运动控制" telerik:ScreenTip.Title="运动控制"
Command="{Binding OpenMotionDebugCommand}" Command="{Binding OpenMotionDebugCommand}"
Size="Medium" Size="Medium"
SmallImage="/Assets/Icons/xyz.png" SmallImage="/Assets/Icons/xyz.png"
Text="运动控制" /> Text="运动控制" />
</StackPanel> </StackPanel>
<StackPanel> <StackPanel>
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Description="打开相机参数设置窗口" telerik:ScreenTip.Description="打开相机参数设置窗口"
telerik:ScreenTip.Title="相机设置" telerik:ScreenTip.Title="相机设置"
Command="{Binding OpenCameraSettingsCommand}" Command="{Binding OpenCameraSettingsCommand}"
Size="Medium" Size="Medium"
SmallImage="/Assets/Icons/detector2.png" SmallImage="/Assets/Icons/detector2.png"
Text="相机设置" /> Text="相机设置" />
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Description="打开 PLC 地址配置窗口" telerik:ScreenTip.Description="打开 PLC 地址配置窗口"
telerik:ScreenTip.Title="PLC 地址配置" telerik:ScreenTip.Title="PLC 地址配置"
Command="{Binding OpenPlcAddrConfigCommand}" Command="{Binding OpenPlcAddrConfigCommand}"
Size="Medium" Size="Medium"
SmallImage="/Assets/Icons/tools.png" SmallImage="/Assets/Icons/tools.png"
Text="PLC 地址" /> Text="PLC 地址" />
</StackPanel> </StackPanel>
</telerik:RadRibbonGroup> </telerik:RadRibbonGroup>
<telerik:RadRibbonGroup Header="多语言"> <telerik:RadRibbonGroup Header="多语言">
<telerik:RadRibbonGroup.Variants> <telerik:RadRibbonGroup.Variants>
<telerik:GroupVariant Priority="0" Variant="Large" /> <telerik:GroupVariant Priority="0" Variant="Large" />
</telerik:RadRibbonGroup.Variants> </telerik:RadRibbonGroup.Variants>
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Description="切换应用程序显示语言" telerik:ScreenTip.Description="切换应用程序显示语言"
telerik:ScreenTip.Title="多语言设置" telerik:ScreenTip.Title="多语言设置"
Size="Large" Size="Large"
SmallImage="/Assets/Icons/tools.png" SmallImage="/Assets/Icons/tools.png"
Command="{Binding OpenLanguageSwitcherCommand}" Command="{Binding OpenLanguageSwitcherCommand}"
Text="语言设置" /> Text="语言设置" />
</telerik:RadRibbonGroup>
<telerik:RadRibbonGroup Header="日志">
<telerik:RadRibbonGroup.Variants>
<telerik:GroupVariant Priority="0" Variant="Large" />
</telerik:RadRibbonGroup.Variants>
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Description="打开实时日志查看器" telerik:ScreenTip.Description="打开实时日志查看器"
telerik:ScreenTip.Title="查看日志" telerik:ScreenTip.Title="查看日志"
Size="Large" Size="Large"
SmallImage="/Assets/Icons/message.png" SmallImage="/Assets/Icons/message.png"
Command="{Binding OpenRealTimeLogViewerCommand}" Command="{Binding OpenRealTimeLogViewerCommand}"
Text="查看日志" /> Text="查看日志" />
</telerik:RadRibbonGroup> </telerik:RadRibbonGroup>
</telerik:RadRibbonTab> </telerik:RadRibbonTab>
<telerik:RadRibbonTab Header="关于"> <telerik:RadRibbonTab Header="关于">
@@ -487,24 +519,19 @@
</telerik:RadRibbonGroup.Variants> </telerik:RadRibbonGroup.Variants>
<telerik:RadRibbonButton <telerik:RadRibbonButton
Size="Large" Size="Large"
SmallImage="/Assets/Icons/message.png" SmallImage="/Assets/Icons/message.png"
Command="{Binding OpenUserManualCommand}" Command="{Binding OpenUserManualCommand}"
Text="帮助文档" /> Text="帮助文档" />
<telerik:RadRibbonButton <telerik:RadRibbonButton
Size="Large" Size="Large"
SmallImage="/Assets/Icons/tools.png" SmallImage="/Assets/Icons/tools.png"
Command="{Binding OpenLibraryVersionsCommand}" Command="{Binding OpenLibraryVersionsCommand}"
Text="关于" /> Text="关于" />
</telerik:RadRibbonGroup> </telerik:RadRibbonGroup>
</telerik:RadRibbonTab>
<telerik:RadRibbonView.ContextualGroups>
<telerik:RadRibbonContextualGroup </telerik:RadRibbonTab>
x:Name="PictureTools"
Header="Picture Tools"
IsActive="{Binding Path=PictureToolsTab.IsEnabled, Mode=OneWay}" />
</telerik:RadRibbonView.ContextualGroups>
</telerik:RadRibbonView> </telerik:RadRibbonView>
<Grid <Grid
@@ -1,4 +1,8 @@
using System.Windows; using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Linq;
using Telerik.Windows.Controls;
using XplorePlane.ViewModels; using XplorePlane.ViewModels;
namespace XplorePlane.Views namespace XplorePlane.Views
@@ -12,6 +16,8 @@ namespace XplorePlane.Views
{ {
InitializeComponent(); InitializeComponent();
DataContext = viewModel; DataContext = viewModel;
} }
} }
} }
@@ -26,7 +26,7 @@
VerticalAlignment="Center" VerticalAlignment="Center"
FontWeight="SemiBold" FontWeight="SemiBold"
Foreground="#333333" Foreground="#333333"
Text="相机预览" /> Text="导航" />
</Border> </Border>
<!-- 相机图像显示区域 --> <!-- 相机图像显示区域 -->
@@ -148,7 +148,7 @@ namespace XplorePlane.Views
{ {
if (DataContext is not ViewportPanelViewModel vm || vm.ImageSource is not BitmapSource bitmap) if (DataContext is not ViewportPanelViewModel vm || vm.ImageSource is not BitmapSource bitmap)
{ {
MessageBox.Show("当前没有可保存的图像", "提示", MessageBoxButton.OK, MessageBoxImage.Information); MessageBox.Show("No image available to save", "Info", MessageBoxButton.OK, MessageBoxImage.Information);
return; return;
} }
SaveBitmapToFile(bitmap, "保存原始图像"); SaveBitmapToFile(bitmap, "保存原始图像");
@@ -159,7 +159,7 @@ namespace XplorePlane.Views
var target = FindChildByName<Canvas>(RoiCanvas, "mainCanvas"); var target = FindChildByName<Canvas>(RoiCanvas, "mainCanvas");
if (target == null) if (target == null)
{ {
MessageBox.Show("当前没有可保存的图像", "提示", MessageBoxButton.OK, MessageBoxImage.Information); MessageBox.Show("No image available to save", "Info", MessageBoxButton.OK, MessageBoxImage.Information);
return; return;
} }
var width = (int)target.ActualWidth; var width = (int)target.ActualWidth;
@@ -0,0 +1,169 @@
<Window x:Class="XplorePlane.Views.SettingsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="系统设置"
Width="860"
Height="620"
MinWidth="760"
MinHeight="540"
WindowStartupLocation="CenterOwner"
ShowInTaskbar="False"
Background="#F5F5F5">
<Window.Resources>
<Style x:Key="SectionTitleStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="16" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="#1F1F1F" />
<Setter Property="Margin" Value="0,0,0,12" />
</Style>
<Style x:Key="CardStyle" TargetType="Border">
<Setter Property="Background" Value="White" />
<Setter Property="BorderBrush" Value="#D9D9D9" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="6" />
<Setter Property="Padding" Value="16" />
<Setter Property="Margin" Value="0,0,0,12" />
</Style>
<Style x:Key="ActionButtonStyle" TargetType="Button">
<Setter Property="Height" Value="32" />
<Setter Property="MinWidth" Value="96" />
<Setter Property="Margin" Value="0,0,10,10" />
<Setter Property="Padding" Value="12,0" />
</Style>
</Window.Resources>
<DockPanel Margin="12">
<TabControl Background="White">
<TabItem Header="通用">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="16">
<TextBlock Style="{StaticResource SectionTitleStyle}" Text="通用设置" />
<Border Style="{StaticResource CardStyle}">
<StackPanel>
<TextBlock FontSize="13"
FontWeight="SemiBold"
Text="XP 数据目录" />
<TextBlock Margin="0,8,0,8"
Foreground="#666666"
Text="Plan 用于 CNC 默认保存和加载,Tools 用于流程图配方 xpm,Data 用于执行结果和中间图像,Report 为报告预留目录。" />
<Grid Margin="0,4,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
VerticalAlignment="Center"
Text="数据根目录" />
<TextBox Grid.Column="1"
Height="30"
Margin="0,0,10,0"
Padding="8,0"
Text="{Binding DataRootPath, UpdateSourceTrigger=PropertyChanged}" />
<Button Grid.Column="2"
Command="{Binding BrowseDataRootPathCommand}"
Content="浏览"
Style="{StaticResource ActionButtonStyle}" />
<Button Grid.Column="3"
Command="{Binding ResetDataRootPathCommand}"
Content="恢复默认"
Style="{StaticResource ActionButtonStyle}" />
<Button Grid.Column="4"
Command="{Binding SaveDataRootPathCommand}"
Content="保存"
Style="{StaticResource ActionButtonStyle}" />
</Grid>
</StackPanel>
</Border>
<Border Style="{StaticResource CardStyle}">
<StackPanel>
<TextBlock FontSize="13"
FontWeight="SemiBold"
Text="界面与使用习惯" />
<CheckBox Margin="0,12,0,0"
Content="启动时默认显示实时图像" />
<CheckBox Margin="0,8,0,0"
Content="允许自动恢复上次工作状态" />
<CheckBox Margin="0,8,0,0"
Content="启用状态栏详细提示" />
</StackPanel>
</Border>
<Border Style="{StaticResource CardStyle}">
<StackPanel>
<TextBlock FontSize="13"
FontWeight="SemiBold"
Text="调试" />
<WrapPanel>
<Button Command="{Binding OpenLibraryVersionsCommand}"
Content="版本信息"
Style="{StaticResource ActionButtonStyle}" />
</WrapPanel>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</TabItem>
<TabItem Header="报告">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="16">
<TextBlock Style="{StaticResource SectionTitleStyle}" Text="报告设置" />
<Border Style="{StaticResource CardStyle}">
<StackPanel>
<TextBlock FontSize="13"
FontWeight="SemiBold"
Text="报告模板" />
<TextBlock Margin="0,10,0,8"
Foreground="#666666"
Text="这里预留报告输出相关设置,可继续扩展公司信息、模板路径、签核信息和导出规则。" />
<Grid Margin="0,4,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
VerticalAlignment="Center"
Text="报告标题" />
<TextBox Grid.Column="1"
Height="30"
Padding="8,0"
Text="检测报告" />
</Grid>
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
VerticalAlignment="Center"
Text="模板路径" />
<TextBox Grid.Column="1"
Height="30"
Padding="8,0"
Text="D:\XPData\Report\Templates\Default" />
</Grid>
<CheckBox Margin="0,12,0,0"
Content="导出时自动附带原始图像" />
<CheckBox Margin="0,8,0,0"
Content="导出时自动附带处理结果图像" />
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</TabItem>
</TabControl>
</DockPanel>
</Window>
@@ -0,0 +1,13 @@
using System.Windows;
namespace XplorePlane.Views
{
public partial class SettingsWindow : Window
{
public SettingsWindow(object viewModel)
{
InitializeComponent();
DataContext = viewModel;
}
}
}
+13 -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>
@@ -15,7 +15,11 @@
<ItemGroup> <ItemGroup>
<Page Remove="MainWindow.xaml" /> <Page Remove="MainWindow.xaml" />
<Page Remove="Views\ImageProcessing\ImageProcessingPanelView.xaml" />
<Page Remove="Views\ImageProcessing\ImageProcessingWindow.xaml" />
<Page Remove="Views\ImageProcessing\PipelineEditorWindow.xaml" />
<Page Remove="Views\Main\MainWindowB.xaml" /> <Page Remove="Views\Main\MainWindowB.xaml" />
<Page Remove="Views\Main\NavigationPanelView.xaml" />
</ItemGroup> </ItemGroup>
<!-- NuGet 包引用 --> <!-- NuGet 包引用 -->
@@ -146,7 +150,15 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>Libs\Hardware\zh-TW\%(Filename)%(Extension)</Link> <Link>Libs\Hardware\zh-TW\%(Filename)%(Extension)</Link>
</None> </None>
<Compile Remove="Views\Hardware\**" />
<EmbeddedResource Remove="Views\Hardware\**" />
<None Remove="Views\Hardware\**" />
<Page Remove="Views\Hardware\**" />
<Compile Remove="Views\ImageProcessing\ImageProcessingPanelView.xaml.cs" />
<Compile Remove="Views\ImageProcessing\ImageProcessingWindow.xaml.cs" />
<Compile Remove="Views\ImageProcessing\PipelineEditorWindow.xaml.cs" />
<Compile Remove="Views\Main\MainWindowB.xaml.cs" /> <Compile Remove="Views\Main\MainWindowB.xaml.cs" />
<Compile Remove="Views\Main\NavigationPanelView.xaml.cs" />
<Content Include="XplorerPlane.ico"> <Content Include="XplorerPlane.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
+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>