diff --git a/XplorePlane/Assets/Icons/eye.png b/XplorePlane/Assets/Icons/eye.png
new file mode 100644
index 0000000..800e01e
Binary files /dev/null and b/XplorePlane/Assets/Icons/eye.png differ
diff --git a/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs b/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs
index c6e3ae7..1802177 100644
--- a/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs
+++ b/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs
@@ -324,6 +324,9 @@ namespace XplorePlane.ViewModels.Cnc
}
}
+ /// 供外部直接 await 的保存方法
+ public Task SaveAsync() => ExecuteSaveProgramAsync();
+
private async Task ExecuteSaveProgramAsync()
{
if (_currentProgram == null)
diff --git a/XplorePlane/ViewModels/Cnc/CncInspectionModulePipelineViewModel.cs b/XplorePlane/ViewModels/Cnc/CncInspectionModulePipelineViewModel.cs
index be00d31..bf7c2d7 100644
--- a/XplorePlane/ViewModels/Cnc/CncInspectionModulePipelineViewModel.cs
+++ b/XplorePlane/ViewModels/Cnc/CncInspectionModulePipelineViewModel.cs
@@ -77,6 +77,9 @@ namespace XplorePlane.ViewModels.Cnc
_editorViewModel.PropertyChanged += OnEditorPropertyChanged;
RefreshFromSelection();
+
+ _eventAggregator?.GetEvent()
+ .Subscribe(key => { if (CanAddOperator(key)) AddOperator(key); });
}
public ObservableCollection PipelineNodes { get; }
@@ -174,6 +177,9 @@ namespace XplorePlane.ViewModels.Cnc
RaiseCommandCanExecuteChanged();
}
+ private bool CanAddOperator(string operatorKey) =>
+ HasActiveModule && !string.IsNullOrWhiteSpace(operatorKey);
+
private void AddOperator(string operatorKey)
{
if (!HasActiveModule || string.IsNullOrWhiteSpace(operatorKey))
diff --git a/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs b/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs
index f0233c0..e21d72f 100644
--- a/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs
+++ b/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs
@@ -42,6 +42,7 @@ namespace XplorePlane.ViewModels
private string _pipelineFileDisplayName = DefaultPipelineFileDisplayName;
private string _currentFilePath;
private PipelineNodeViewModel _executionEndNode;
+ private bool _isModified;
private CancellationTokenSource _executionCts;
private CancellationTokenSource _debounceCts;
@@ -172,6 +173,13 @@ namespace XplorePlane.ViewModels
private set => SetProperty(ref _pipelineFileDisplayName, value);
}
+ /// 流水线是否有未保存的修改
+ public bool IsModified
+ {
+ get => _isModified;
+ private set => SetProperty(ref _isModified, value);
+ }
+
public PipelineNodeViewModel ExecutionEndNode
{
get => _executionEndNode;
@@ -266,6 +274,7 @@ namespace XplorePlane.ViewModels
_logger.Info("节点已添加到 PipelineNodes:{Key} ({DisplayName}),当前节点数={Count}",
operatorKey, displayName, PipelineNodes.Count);
SetInfoStatus($"已添加算子:{displayName}");
+ IsModified = true;
TriggerDebouncedExecution();
}
@@ -283,6 +292,7 @@ namespace XplorePlane.ViewModels
SelectNeighborAfterRemoval(removedIndex);
SetInfoStatus($"已移除算子:{node.DisplayName}");
+ IsModified = true;
TriggerDebouncedExecution();
}
@@ -294,6 +304,7 @@ namespace XplorePlane.ViewModels
PipelineNodes.Move(index, index - 1);
RenumberNodes();
UpdateExecutionRangeState();
+ IsModified = true;
TriggerDebouncedExecution();
}
@@ -305,6 +316,7 @@ namespace XplorePlane.ViewModels
PipelineNodes.Move(index, index + 1);
RenumberNodes();
UpdateExecutionRangeState();
+ IsModified = true;
TriggerDebouncedExecution();
}
@@ -325,6 +337,7 @@ namespace XplorePlane.ViewModels
UpdateExecutionRangeState();
SelectedNode = node;
SetInfoStatus($"已调整算子顺序:{node.DisplayName}");
+ IsModified = true;
TriggerDebouncedExecution();
}
@@ -337,6 +350,7 @@ namespace XplorePlane.ViewModels
SetInfoStatus(node.IsEnabled
? $"已启用算子:{node.DisplayName}"
: $"已停用算子:{node.DisplayName}");
+ IsModified = true;
TriggerDebouncedExecution();
}
@@ -406,6 +420,7 @@ namespace XplorePlane.ViewModels
{
if (e.PropertyName == nameof(ProcessorParameterVM.Value))
{
+ IsModified = true;
if (TryReportInvalidParameters())
return;
@@ -623,9 +638,13 @@ namespace XplorePlane.ViewModels
PreviewImage = null;
_currentFilePath = null;
PipelineFileDisplayName = DefaultPipelineFileDisplayName;
+ IsModified = false;
SetInfoStatus("已新建流水线");
}
+ /// 供外部直接 await 的保存方法
+ public Task SaveAsync() => SavePipelineAsync();
+
private async Task SavePipelineAsync()
{
if (string.IsNullOrWhiteSpace(PipelineName) || PipelineName.Length > 100)
@@ -674,6 +693,7 @@ namespace XplorePlane.ViewModels
var model = BuildPipelineModel();
await _persistenceService.SaveAsync(model, filePath);
PipelineFileDisplayName = FormatPipelinePath(filePath);
+ IsModified = false;
SetInfoStatus($"流水线已保存:{Path.GetFileName(filePath)}");
}
catch (IOException ex)
@@ -750,6 +770,7 @@ namespace XplorePlane.ViewModels
UpdateExecutionRangeState();
_logger.Info("流水线已加载:{Name},节点数={Count}", model.Name, PipelineNodes.Count);
+ IsModified = false;
SetInfoStatus($"已加载流水线:{model.Name}({PipelineNodes.Count} 个节点)");
}
catch (Exception ex)
diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs
index 26e6403..9e9deb8 100644
--- a/XplorePlane/ViewModels/Main/MainViewModel.cs
+++ b/XplorePlane/ViewModels/Main/MainViewModel.cs
@@ -36,6 +36,8 @@ namespace XplorePlane.ViewModels
private readonly IXpDataPathService _xpDataPathService;
private readonly CncEditorViewModel _cncEditorViewModel;
private readonly CncPageView _cncPageView;
+ private readonly PipelineEditorViewModel _pipelineEditorViewModel;
+ private readonly PipelineEditorView _pipelineEditorView;
public string LicenseInfo
{
@@ -212,6 +214,8 @@ namespace XplorePlane.ViewModels
_xpDataPathService = xpDataPathService ?? throw new ArgumentNullException(nameof(xpDataPathService));
_cncEditorViewModel = _containerProvider.Resolve();
_cncPageView = new CncPageView { DataContext = _cncEditorViewModel };
+ _pipelineEditorViewModel = _containerProvider.Resolve();
+ _pipelineEditorView = new PipelineEditorView { DataContext = _pipelineEditorViewModel };
_mainViewportService.StateChanged += OnMainViewportStateChanged;
_cncEditorViewModel.PropertyChanged += (s, e) =>
@@ -310,7 +314,7 @@ namespace XplorePlane.ViewModels
OpenRealTimeLogViewerCommand = new DelegateCommand(ExecuteOpenRealTimeLogViewer);
UseLiveDetectorSourceCommand = new DelegateCommand(ExecuteUseLiveDetectorSource);
- ImagePanelContent = new PipelineEditorView();
+ ImagePanelContent = _pipelineEditorView;
ViewportPanelWidth = new GridLength(1, GridUnitType.Star);
ImagePanelWidth = new GridLength(320);
DataRootPath = _xpDataPathService.RootPath;
@@ -352,10 +356,31 @@ namespace XplorePlane.ViewModels
}
private void ExecuteOpenCncEditor()
+ {
+ _ = ExecuteOpenCncEditorAsync();
+ }
+
+ private async Task ExecuteOpenCncEditorAsync()
{
if (_isCncEditorMode)
{
- ImagePanelContent = new PipelineEditorView();
+ // CNC → 普通模式:检查 CNC 程序是否有未保存修改
+ if (_cncEditorViewModel.IsModified)
+ {
+ var result = MessageBox.Show(
+ "CNC 程序有未保存的修改,是否保存?",
+ "未保存的修改",
+ MessageBoxButton.YesNoCancel,
+ MessageBoxImage.Warning);
+
+ if (result == MessageBoxResult.Cancel)
+ return;
+
+ if (result == MessageBoxResult.Yes)
+ await _cncEditorViewModel.SaveAsync();
+ }
+
+ ImagePanelContent = _pipelineEditorView;
ViewportPanelWidth = new GridLength(1, GridUnitType.Star);
ImagePanelWidth = new GridLength(320);
_isCncEditorMode = false;
@@ -363,6 +388,22 @@ namespace XplorePlane.ViewModels
return;
}
+ // 普通 → CNC 模式:检查流水线是否有未保存修改
+ if (_pipelineEditorViewModel.IsModified)
+ {
+ var result = MessageBox.Show(
+ "图像处理流水线有未保存的修改,是否保存?",
+ "未保存的修改",
+ MessageBoxButton.YesNoCancel,
+ MessageBoxImage.Warning);
+
+ if (result == MessageBoxResult.Cancel)
+ return;
+
+ if (result == MessageBoxResult.Yes)
+ await _pipelineEditorViewModel.SaveAsync();
+ }
+
ShowCncEditor();
}