From d3e75f3facf08bbec251b503a7e2d7e88dd909b7 Mon Sep 17 00:00:00 2001 From: "zhengxuan.zhang" Date: Thu, 14 May 2026 17:04:29 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9C=A8=E7=8E=B0=E6=9C=89=E7=9A=84=20?= =?UTF-8?q?=E4=BD=8D=E7=BD=AE=E8=8A=82=E7=82=B9=E5=B1=9E=E6=80=A7=E4=B8=AD?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=B8=80=E4=B8=AA=20checkbox=20=E6=8C=89?= =?UTF-8?q?=E9=92=AE=EF=BC=8C=E6=9D=A5=E7=A1=AE=E8=AE=A4=E6=98=AF=E5=90=A6?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E5=9B=BE=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/CncExecutionServiceTests.cs | 49 +++++++++++++++++++ .../ViewModels/CncNodeViewModelTests.cs | 46 +++++++++-------- XplorePlane/Models/CncModels.cs | 3 +- .../Services/Cnc/CncExecutionService.cs | 33 ++++++++++--- XplorePlane/Services/Cnc/CncProgramService.cs | 3 +- .../InspectionResultStore.cs | 4 +- .../ViewModels/Cnc/CncEditorViewModel.cs | 16 +++--- .../ViewModels/Cnc/CncNodeViewModel.cs | 13 +++++ XplorePlane/Views/Cnc/CncPageView.xaml | 9 ++++ XplorePlane/Views/Cnc/CncPageView.xaml.cs | 9 ++-- 10 files changed, 141 insertions(+), 44 deletions(-) diff --git a/XplorePlane.Tests/Services/CncExecutionServiceTests.cs b/XplorePlane.Tests/Services/CncExecutionServiceTests.cs index 2aef010..273a98f 100644 --- a/XplorePlane.Tests/Services/CncExecutionServiceTests.cs +++ b/XplorePlane.Tests/Services/CncExecutionServiceTests.cs @@ -747,5 +747,54 @@ internal sealed class SynchronousProgress : IProgress }); } + [Fact] + public async Task SavePosition_WithSaveImage_RefreshesInputImageForFollowingInspectionModule() + { + var (service, mockStore, _, mockMainViewport, _) = CreateService(); + var initialImage = CreateBitmapSource(1, 1); + var refreshedImage = CreateBitmapSource(2, 3); + + mockMainViewport.SetupGet(m => m.LatestManualImage).Returns((ImageSource)null); + mockMainViewport.SetupSequence(m => m.CurrentDisplayImage) + .Returns(initialImage) + .Returns(refreshedImage); + + List capturedAssets = null; + mockStore.Setup(s => s.AppendNodeResultAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny>())) + .Callback, PipelineExecutionSnapshot, IEnumerable>( + (_, __, ___, assets) => capturedAssets = assets?.ToList()) + .Returns(Task.CompletedTask); + + var program = new CncProgram( + Guid.NewGuid(), + "Program", + DateTime.UtcNow, + DateTime.UtcNow, + new List + { + new SavePositionNode(Guid.NewGuid(), 0, "Pos_0", MotionState.Default, SaveImage: true), + new InspectionModuleNode(Guid.NewGuid(), 1, "Inspect_0", new PipelineModel { Name = "Pipeline" }) + }.AsReadOnly()); + + await service.ExecuteAsync(program, null, CancellationToken.None); + + var inputAsset = Assert.Single(capturedAssets.Where(a => a.AssetType == InspectionAssetType.NodeInputImage)); + Assert.Equal(2, inputAsset.Width); + Assert.Equal(3, inputAsset.Height); + } + + private static BitmapSource CreateBitmapSource(int width, int height) + { + var stride = width * 4; + var pixels = new byte[stride * height]; + var bitmap = BitmapSource.Create(width, height, 96, 96, PixelFormats.Bgra32, null, pixels, stride); + bitmap.Freeze(); + return bitmap; + } + } } diff --git a/XplorePlane.Tests/ViewModels/CncNodeViewModelTests.cs b/XplorePlane.Tests/ViewModels/CncNodeViewModelTests.cs index 67eb000..18d4368 100644 --- a/XplorePlane.Tests/ViewModels/CncNodeViewModelTests.cs +++ b/XplorePlane.Tests/ViewModels/CncNodeViewModelTests.cs @@ -7,15 +7,25 @@ using FsCheck.Fluent; using FsCheck.Xunit; using XplorePlane.Models; using XplorePlane.ViewModels.Cnc; +using Xunit; namespace XplorePlane.Tests.ViewModels { public class CncNodeViewModelTests { - // ── Property 11: 节点执行状态转换正确性 ────────────────────────────── + [Fact] + public void SavePosition_SaveImage_CanBeUpdated() + { + var node = new SavePositionNode(Guid.NewGuid(), 0, "Pos_0", MotionState.Default, SaveImage: false); + var vm = new CncNodeViewModel(node, (_, __) => { }); + + vm.SaveImage = true; + + var updatedNode = Assert.IsType(vm.Model); + Assert.True(vm.SaveImage); + Assert.True(updatedNode.SaveImage); + } - // Feature: cnc-run-execution, Property 11: 节点执行状态转换正确性 - // Validates: Requirements 6.1, 6.2 [Property(MaxTest = 100)] public Property ExecutionState_TransitionsProduceCorrectBoolProperties() { @@ -30,31 +40,27 @@ namespace XplorePlane.Tests.ViewModels gen.ToArbitrary(), node => { - var vm = new CncNodeViewModel(node, (vm2, n) => { }); + var vm = new CncNodeViewModel(node, (_, __) => { }); - // Running vm.ExecutionState = NodeExecutionState.Running; - bool runningOk = vm.IsRunningNode == true - && vm.IsSucceededNode == false - && vm.IsFailedNode == false; + bool runningOk = vm.IsRunningNode + && !vm.IsSucceededNode + && !vm.IsFailedNode; - // Succeeded vm.ExecutionState = NodeExecutionState.Succeeded; - bool succeededOk = vm.IsRunningNode == false - && vm.IsSucceededNode == true - && vm.IsFailedNode == false; + bool succeededOk = !vm.IsRunningNode + && vm.IsSucceededNode + && !vm.IsFailedNode; - // Failed vm.ExecutionState = NodeExecutionState.Failed; - bool failedOk = vm.IsRunningNode == false - && vm.IsSucceededNode == false - && vm.IsFailedNode == true; + bool failedOk = !vm.IsRunningNode + && !vm.IsSucceededNode + && vm.IsFailedNode; - // Idle vm.ExecutionState = NodeExecutionState.Idle; - bool idleOk = vm.IsRunningNode == false - && vm.IsSucceededNode == false - && vm.IsFailedNode == false; + bool idleOk = !vm.IsRunningNode + && !vm.IsSucceededNode + && !vm.IsFailedNode; return runningOk && succeededOk && failedOk && idleOk; }); diff --git a/XplorePlane/Models/CncModels.cs b/XplorePlane/Models/CncModels.cs index 7418b1d..702c5a8 100644 --- a/XplorePlane/Models/CncModels.cs +++ b/XplorePlane/Models/CncModels.cs @@ -85,7 +85,8 @@ namespace XplorePlane.Models Guid Id, int Index, string Name, - MotionState MotionState) : CncNode(Id, Index, CncNodeType.SavePosition, Name); + MotionState MotionState, + bool SaveImage = false) : CncNode(Id, Index, CncNodeType.SavePosition, Name); /// 检测模块节点 | Inspection module node public record InspectionModuleNode( diff --git a/XplorePlane/Services/Cnc/CncExecutionService.cs b/XplorePlane/Services/Cnc/CncExecutionService.cs index 7bbc3a6..ee0ae59 100644 --- a/XplorePlane/Services/Cnc/CncExecutionService.cs +++ b/XplorePlane/Services/Cnc/CncExecutionService.cs @@ -82,7 +82,8 @@ namespace XplorePlane.Services.Cnc return; int inspectionNodeCount = program.Nodes.OfType().Count(); - var sourceImage = TryGetSourceImage(); + var runSourceImage = TryGetSourceImage(); + var currentSourceImage = runSourceImage; Guid runId; try @@ -95,15 +96,15 @@ namespace XplorePlane.Services.Cnc }; InspectionAssetWriteRequest sourceAsset = null; - if (sourceImage != null) + if (runSourceImage != null) { sourceAsset = new InspectionAssetWriteRequest { AssetType = InspectionAssetType.RunSourceImage, - Content = EncodeBitmapToBmp(sourceImage), + Content = EncodeBitmapToBmp(runSourceImage), FileFormat = "bmp", - Width = sourceImage.PixelWidth, - Height = sourceImage.PixelHeight + Width = runSourceImage.PixelWidth, + Height = runSourceImage.PixelHeight }; } @@ -155,13 +156,29 @@ namespace XplorePlane.Services.Cnc "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}", + "StageRotation={StageRotation} FixtureRotation={FixtureRotation} SaveImage={SaveImage}", 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); + sp.MotionState.StageRotation, sp.MotionState.FixtureRotation, + sp.SaveImage); + + if (sp.SaveImage) + { + var capturedImage = TryGetSourceImage(); + if (capturedImage != null) + { + currentSourceImage = capturedImage; + } + else + { + _logger.ForModule().Warn( + "Save-position node '{0}' requested image capture, but no current image was available.", + sp.Name); + } + } break; case SaveNodeNode sn: @@ -214,7 +231,7 @@ namespace XplorePlane.Services.Cnc case InspectionModuleNode inspectionNode: try { - var img = await ExecuteInspectionNodeAsync(runId, inspectionNode, sourceImage, linkedCts.Token); + var img = await ExecuteInspectionNodeAsync(runId, inspectionNode, currentSourceImage, linkedCts.Token); if (img != null) lastResultImage = img; } catch (Exception ex) diff --git a/XplorePlane/Services/Cnc/CncProgramService.cs b/XplorePlane/Services/Cnc/CncProgramService.cs index 9d1b472..01edd34 100644 --- a/XplorePlane/Services/Cnc/CncProgramService.cs +++ b/XplorePlane/Services/Cnc/CncProgramService.cs @@ -430,7 +430,8 @@ namespace XplorePlane.Services.Cnc { return new SavePositionNode( id, index, $"检测位置_{index}", - MotionState: _appStateService.MotionState); + MotionState: _appStateService.MotionState, + SaveImage: false); } private double TryReadCurrent() { diff --git a/XplorePlane/Services/InspectionResults/InspectionResultStore.cs b/XplorePlane/Services/InspectionResults/InspectionResultStore.cs index 2e61f60..4588d5a 100644 --- a/XplorePlane/Services/InspectionResults/InspectionResultStore.cs +++ b/XplorePlane/Services/InspectionResults/InspectionResultStore.cs @@ -930,9 +930,7 @@ WHERE run_id = @run_id"; { return Path.Combine( "Results", - startedAt.Value.ToString("yyyy"), - startedAt.Value.ToString("MM"), - startedAt.Value.ToString("dd"), + startedAt.Value.ToString("yyyy-MM-dd"), runId.ToString("D")); } diff --git a/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs b/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs index 1802177..b82bc00 100644 --- a/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs +++ b/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs @@ -420,7 +420,7 @@ namespace XplorePlane.ViewModels.Cnc return; var sb = new StringBuilder(); - 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"); + 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,SaveImage,MarkerType,MarkerX,MarkerY,DialogTitle,DialogMessage,DelayMs,Pipeline"); var inv = CultureInfo.InvariantCulture; @@ -431,13 +431,13 @@ namespace XplorePlane.ViewModels.Cnc 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.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.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.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)}", - InspectionMarkerNode mk => $"{mk.Index},{mk.NodeType},{Esc(mk.Name)},,,,,,,,,,,,,,,,,{Esc(mk.MarkerType)},{mk.MarkerX.ToString(inv)},{mk.MarkerY.ToString(inv)},,,", - PauseDialogNode pd => $"{pd.Index},{pd.NodeType},{Esc(pd.Name)},,,,,,,,,,,,,,,,,,,{Esc(pd.DialogTitle)},{Esc(pd.DialogMessage)},,", - WaitDelayNode wd => $"{wd.Index},{wd.NodeType},{Esc(wd.Name)},,,,,,,,,,,,,,,,,,,,,{wd.DelayMilliseconds},", - CompleteProgramNode cp => $"{cp.Index},{cp.NodeType},{Esc(cp.Name)},,,,,,,,,,,,,,,,,,,,,,", - _ => $"{node.Index},{node.NodeType},{Esc(node.Name)},,,,,,,,,,,,,,,,,,,,,," + 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)},,,,,,,,{sp.SaveImage},,,,,,,", + InspectionModuleNode im => $"{im.Index},{im.NodeType},{Esc(im.Name)},,,,,,,,,,,,,,,,,,,,,,,{Esc(im.Pipeline?.Name ?? string.Empty)}", + InspectionMarkerNode mk => $"{mk.Index},{mk.NodeType},{Esc(mk.Name)},,,,,,,,,,,,,,,,,,{Esc(mk.MarkerType)},{mk.MarkerX.ToString(inv)},{mk.MarkerY.ToString(inv)},,,", + PauseDialogNode pd => $"{pd.Index},{pd.NodeType},{Esc(pd.Name)},,,,,,,,,,,,,,,,,,,,{Esc(pd.DialogTitle)},{Esc(pd.DialogMessage)},,", + WaitDelayNode wd => $"{wd.Index},{wd.NodeType},{Esc(wd.Name)},,,,,,,,,,,,,,,,,,,,,,{wd.DelayMilliseconds},", + CompleteProgramNode cp => $"{cp.Index},{cp.NodeType},{Esc(cp.Name)},,,,,,,,,,,,,,,,,,,,,,,", + _ => $"{node.Index},{node.NodeType},{Esc(node.Name)},,,,,,,,,,,,,,,,,,,,,,," }; sb.AppendLine(row); diff --git a/XplorePlane/ViewModels/Cnc/CncNodeViewModel.cs b/XplorePlane/ViewModels/Cnc/CncNodeViewModel.cs index 3cbb2fe..9cc4760 100644 --- a/XplorePlane/ViewModels/Cnc/CncNodeViewModel.cs +++ b/XplorePlane/ViewModels/Cnc/CncNodeViewModel.cs @@ -349,6 +349,18 @@ namespace XplorePlane.ViewModels.Cnc } } + public bool SaveImage + { + get => _model is SavePositionNode sp && sp.SaveImage; + set + { + if (_model is SavePositionNode sp) + { + UpdateModel(sp with { SaveImage = value }); + } + } + } + public string PipelineName { get => _model is InspectionModuleNode im ? im.Pipeline?.Name ?? string.Empty : string.Empty; @@ -636,6 +648,7 @@ namespace XplorePlane.ViewModels.Cnc RaisePropertyChanged(nameof(FrameRate)); RaisePropertyChanged(nameof(Resolution)); RaisePropertyChanged(nameof(ImageFileName)); + RaisePropertyChanged(nameof(SaveImage)); RaisePropertyChanged(nameof(Pipeline)); RaisePropertyChanged(nameof(PipelineName)); RaisePropertyChanged(nameof(MarkerType)); diff --git a/XplorePlane/Views/Cnc/CncPageView.xaml b/XplorePlane/Views/Cnc/CncPageView.xaml index cb54443..ae54bc8 100644 --- a/XplorePlane/Views/Cnc/CncPageView.xaml +++ b/XplorePlane/Views/Cnc/CncPageView.xaml @@ -584,6 +584,15 @@ + + + + + +