Compare commits
30 Commits
8de455f0bb
...
436eaa54fa
| Author | SHA1 | Date | |
|---|---|---|---|
| 436eaa54fa | |||
| a4e257e8ce | |||
| 2124d0c0b7 | |||
| 4a4e45e479 | |||
| d079e9357b | |||
| d56caf1ab5 | |||
| bd9b24beb1 | |||
| f9be56b99f | |||
| 03a8532049 | |||
| e3a1184805 | |||
| b740f8d453 | |||
| 5852e11b9f | |||
| 996b0c5796 | |||
| 3a3ea5b5c9 | |||
| aeef1feee3 | |||
| 1ef876db2c | |||
| 1b686066c8 | |||
| 7c0f9dab73 | |||
| 3bee2898c5 | |||
| 9a8831c945 | |||
| db8a37410f | |||
| a483144d29 | |||
| 2eb3fed4d0 | |||
| 48c419d777 | |||
| d4050b4218 | |||
| f5dceeceb7 | |||
| fd4048a6c0 | |||
| fdf2419eb6 | |||
| 8a6abfb28b | |||
| fd9784ecb6 |
@@ -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": {
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
-11
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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));
|
||||||
|
|
||||||
|
// DetectorService:GetInfo 在未初始化时抛出,模拟此行为
|
||||||
|
_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 <= 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 6–8 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 9–10 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
@@ -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
|
||||||
|
|||||||
@@ -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日志配置 -->
|
||||||
|
|||||||
@@ -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)
|
||||||
|
└─ IRaySourceService(XP.Hardware.RaySource)
|
||||||
|
├─ 发布 StatusUpdatedEvent(SystemStatusData,500ms 轮询)
|
||||||
|
│ └─ AppStateService.OnRaySourceStatusUpdated() [BackgroundThread]
|
||||||
|
│ ├─ IsOn = data.IsXRayOn
|
||||||
|
│ ├─ Voltage = data.ActualVoltage (kV)
|
||||||
|
│ ├─ Power = ActualVoltage × ActualCurrent / 1000 (W)
|
||||||
|
│ └─ UpdateRaySourceState() → 触发 RaySourceStateChanged → ViewModel 刷新 UI
|
||||||
|
│
|
||||||
|
├─ 发布 RaySourceStatusChangedEvent(RaySourceStatus 枚举)
|
||||||
|
│ └─ AppStateService.OnRaySourceStatusChanged() [BackgroundThread]
|
||||||
|
│ └─ 仅在 IsOn 变化时更新,保留当前 Voltage/Power 值
|
||||||
|
│
|
||||||
|
└─ 发布 VariablesConnectedEvent(bool)
|
||||||
|
└─ 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 ← ✅ 已订阅,缓存为 LatestDetectorFrame(volatile)
|
||||||
|
├─ CorrectionCompletedEvent ← 未订阅(无需同步到 AppState)
|
||||||
|
└─ ErrorOccurredEvent ← 未订阅(无需同步到 AppState)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 数据流
|
||||||
|
|
||||||
|
```
|
||||||
|
探测器硬件
|
||||||
|
└─ IDetectorService(XP.Hardware.Detector)
|
||||||
|
├─ 发布 StatusChangedEvent(DetectorStatus 枚举)
|
||||||
|
│ └─ AppStateService.OnDetectorStatusChanged() [BackgroundThread]
|
||||||
|
│ ├─ 映射 DetectorStatus → DetectorState(IsConnected, IsAcquiring, ...)
|
||||||
|
│ ├─ UpdateDetectorState()
|
||||||
|
│ │ └─ 触发 DetectorStateChanged 事件 → ViewModel 刷新 UI
|
||||||
|
│ └─ 若从已连接变为断开:发布 DetectorDisconnectedEvent
|
||||||
|
│ ├─ CncExecutionService 订阅 → 取消当前 CNC 执行
|
||||||
|
│ └─ ViewportPanelViewModel 订阅 → 弹出断连警告对话框
|
||||||
|
│
|
||||||
|
└─ 发布 ImageCapturedEvent(ImageCapturedEventArgs)
|
||||||
|
└─ 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 数据流
|
||||||
|
|
||||||
|
```
|
||||||
|
ICameraController(XP.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
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 ────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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('、', ',', ',', '。', '.', ';', ';', ':', ':');
|
||||||
|
|||||||
@@ -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("[图像链路] OnPipelinePreviewUpdated:payload 或 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,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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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="" />
|
||||||
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="" />
|
||||||
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="" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
Command="{Binding LoadPipelineCommand}"
|
||||||
|
Style="{StaticResource ToolbarBtn}"
|
||||||
|
ToolTip="加载配方">
|
||||||
|
<TextBlock
|
||||||
|
FontFamily="Segoe MDL2 Assets"
|
||||||
|
FontSize="14"
|
||||||
|
Text="" />
|
||||||
|
</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,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"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,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>
|
||||||
Reference in New Issue
Block a user