6 Commits

Author SHA1 Message Date
zhengxuan.zhang a483144d29 调整流水线算子参数区 滑块控件的大小 2026-05-06 13:25:18 +08:00
zhengxuan.zhang 2eb3fed4d0 1、当前根节点选择中是 高亮背景和文字不协调,文字改为黑色
2、延时节点执行时,进度条显示功能失效,修复
   3、自动执行,当加载CNC后,点击rabbion中运行,此时没有看到执行中,已执行,等高亮规则
2026-05-06 11:46:49 +08:00
zhengxuan.zhang 48c419d777 调整CNC编辑页面按钮样式 2026-05-06 11:22:21 +08:00
zhengxuan.zhang d4050b4218 调整CNC按钮逻辑改为内置 2026-05-06 11:12:05 +08:00
zhengxuan.zhang f5dceeceb7 对 流程图 新增 4个状态 已启用 已停用 执行到此 未参与本次执行 和右键菜单,执行到此,执行全部 2026-05-06 10:39:19 +08:00
zhengxuan.zhang fd4048a6c0 修复算子工具箱搜索功能 2026-05-06 10:01:27 +08:00
22 changed files with 740 additions and 115 deletions
@@ -16,8 +16,8 @@
Background="Transparent"
BorderBrush="#FFD5DFE5"
BorderThickness="1"
Padding="10"
Margin="0,0,0,10">
Padding="8"
Margin="0,0,0,8">
<StackPanel>
<TextBlock x:Name="txtProcessorName"
FontSize="14"
@@ -32,7 +32,7 @@
<!-- 参数列表 -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<StackPanel x:Name="pnlParameters" Margin="5" />
<StackPanel x:Name="pnlParameters" Margin="2" />
</ScrollViewer>
</Grid>
</UserControl>
</UserControl>
@@ -143,12 +143,15 @@ public partial class ProcessorParameterControl : UserControl
var textBox = new TextBox
{
Text = param.Value.ToString(),
Width = 100,
HorizontalAlignment = HorizontalAlignment.Left
Width = 56,
MinWidth = 56,
HorizontalContentAlignment = HorizontalAlignment.Center,
VerticalContentAlignment = VerticalAlignment.Center
};
if (param.MinValue != null && param.MaxValue != null)
{
var rangeGrid = CreateRangeEditorContainer();
var slider = new Slider
{
Minimum = Convert.ToDouble(param.MinValue),
@@ -156,7 +159,8 @@ public partial class ProcessorParameterControl : UserControl
Value = Convert.ToDouble(param.Value),
TickFrequency = 1,
IsSnapToTickEnabled = true,
Margin = new Thickness(0, 0, 0, 5)
MinWidth = 120,
VerticalAlignment = VerticalAlignment.Center
};
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
{
@@ -193,9 +201,9 @@ public partial class ProcessorParameterControl : UserControl
OnParameterChanged();
}
};
}
panel.Children.Add(textBox);
panel.Children.Add(textBox);
}
return panel;
}
@@ -211,19 +219,23 @@ public partial class ProcessorParameterControl : UserControl
var textBox = new TextBox
{
Text = Convert.ToDouble(param.Value).ToString("F2"),
Width = 100,
HorizontalAlignment = HorizontalAlignment.Left
Width = 56,
MinWidth = 56,
HorizontalContentAlignment = HorizontalAlignment.Center,
VerticalContentAlignment = VerticalAlignment.Center
};
if (param.MinValue != null && param.MaxValue != null)
{
var rangeGrid = CreateRangeEditorContainer();
var slider = new Slider
{
Minimum = Convert.ToDouble(param.MinValue),
Maximum = Convert.ToDouble(param.MaxValue),
Value = Convert.ToDouble(param.Value),
TickFrequency = 0.1,
Margin = new Thickness(0, 0, 0, 5)
MinWidth = 120,
VerticalAlignment = VerticalAlignment.Center
};
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
{
@@ -260,9 +276,9 @@ public partial class ProcessorParameterControl : UserControl
OnParameterChanged();
}
};
}
panel.Children.Add(textBox);
panel.Children.Add(textBox);
}
return panel;
}
@@ -302,8 +318,8 @@ public partial class ProcessorParameterControl : UserControl
var comboBox = new ComboBox
{
Margin = new Thickness(0, 5, 0, 0),
Width = 200,
HorizontalAlignment = HorizontalAlignment.Left
MinWidth = 160,
HorizontalAlignment = HorizontalAlignment.Stretch
};
if (param.Options != null)
@@ -344,8 +360,8 @@ public partial class ProcessorParameterControl : UserControl
{
Text = param.Value?.ToString() ?? "",
Margin = new Thickness(0, 5, 0, 0),
Width = 200,
HorizontalAlignment = HorizontalAlignment.Left
MinWidth = 160,
HorizontalAlignment = HorizontalAlignment.Stretch
};
textBox.TextChanged += (s, e) =>
@@ -374,4 +390,16 @@ public partial class ProcessorParameterControl : UserControl
pnlParameters.Children.Clear();
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;
}
}
@@ -95,7 +95,7 @@ namespace XplorePlane.Services.Cnc
break;
}
progress?.Report(new CncNodeExecutionProgress(node.Id, NodeExecutionState.Running));
progress?.Report(new CncNodeExecutionProgress(node.Id, NodeExecutionState.Running, ProgressPercent: 0));
bool nodeSucceeded = true;
@@ -106,7 +106,7 @@ namespace XplorePlane.Services.Cnc
case WaitDelayNode waitNode:
try
{
await ExecuteWaitDelayWithProgressAsync(waitNode, cancellationToken);
await ExecuteWaitDelayWithProgressAsync(waitNode, progress, cancellationToken);
}
catch (OperationCanceledException)
{
@@ -324,11 +324,17 @@ namespace XplorePlane.Services.Cnc
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;
if (totalMs <= 0)
{
progress?.Report(new CncNodeExecutionProgress(waitNode.Id, NodeExecutionState.Running, ProgressPercent: 100));
return;
}
const int tickMs = 50;
@@ -341,6 +347,10 @@ namespace XplorePlane.Services.Cnc
int delay = Math.Min(tickMs, remaining);
await Task.Delay(delay, cancellationToken);
elapsed += delay;
progress?.Report(new CncNodeExecutionProgress(
waitNode.Id,
NodeExecutionState.Running,
ProgressPercent: elapsed * 100d / totalMs));
}
}
}
@@ -17,6 +17,11 @@ namespace XplorePlane.Services.Cnc
/// <summary>
/// Progress report for a single CNC node execution.
/// ResultImage is non-null when an InspectionModuleNode produces output.
/// ProgressPercent is used by long-running nodes such as WaitDelayNode.
/// </summary>
public record CncNodeExecutionProgress(Guid NodeId, NodeExecutionState State, BitmapSource ResultImage = null);
public record CncNodeExecutionProgress(
Guid NodeId,
NodeExecutionState State,
BitmapSource ResultImage = null,
double? ProgressPercent = null);
}
@@ -455,6 +455,7 @@ namespace XplorePlane.ViewModels.Cnc
private async Task ExecuteRunAsync()
{
_cts = new CancellationTokenSource();
ResetAllNodeStates();
IsRunning = true;
HasExecutionError = false;
ExecutionError = null;
@@ -482,7 +483,6 @@ namespace XplorePlane.ViewModels.Cnc
finally
{
IsRunning = false;
ResetAllNodeStates();
_cts?.Dispose();
_cts = null;
}
@@ -499,6 +499,7 @@ namespace XplorePlane.ViewModels.Cnc
if (nodeVm != null)
{
nodeVm.ExecutionState = progress.State;
nodeVm.ExecutionProgressPercent = progress.ProgressPercent ?? (progress.State == NodeExecutionState.Succeeded ? 100d : 0d);
if (progress.State == NodeExecutionState.Running)
StatusMessage = $"正在执行节点:{nodeVm.Name}{nodeVm.Index + 1}/{_currentProgram?.Nodes?.Count ?? 0}";
else if (progress.State == NodeExecutionState.Succeeded)
@@ -519,7 +520,10 @@ namespace XplorePlane.ViewModels.Cnc
private void ResetAllNodeStates()
{
foreach (var node in Nodes)
{
node.ExecutionState = NodeExecutionState.Idle;
node.ExecutionProgressPercent = 0;
}
}
private void RaiseEditCommandsCanExecuteChanged()
@@ -37,6 +37,7 @@ namespace XplorePlane.ViewModels.Cnc
private string _statusMessage = "请选择检测模块以编辑其流水线。";
private string _pipelineFileDisplayName = "未命名模块.xpm";
private string _currentFilePath;
private PipelineNodeViewModel _executionEndNode;
private bool _isSynchronizing;
private CancellationTokenSource _debounceCts;
@@ -65,6 +66,8 @@ namespace XplorePlane.ViewModels.Cnc
RemoveOperatorCommand = new DelegateCommand<PipelineNodeViewModel>(RemoveOperator);
ReorderOperatorCommand = new DelegateCommand<PipelineReorderArgs>(ReorderOperator);
ToggleOperatorEnabledCommand = new DelegateCommand<PipelineNodeViewModel>(ToggleOperatorEnabled);
ExecuteToNodeCommand = new DelegateCommand<PipelineNodeViewModel>(ExecuteToNode);
ClearExecutionRangeCommand = new DelegateCommand(ClearExecutionRange);
MoveNodeUpCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeUp);
MoveNodeDownCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeDown);
NewPipelineCommand = new DelegateCommand(NewPipeline);
@@ -98,6 +101,16 @@ namespace XplorePlane.ViewModels.Cnc
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 Visibility EditorVisibility => HasActiveModule ? Visibility.Visible : Visibility.Collapsed;
@@ -112,6 +125,10 @@ namespace XplorePlane.ViewModels.Cnc
public ICommand ToggleOperatorEnabledCommand { get; }
public ICommand ExecuteToNodeCommand { get; }
public ICommand ClearExecutionRangeCommand { get; }
public ICommand MoveNodeUpCommand { get; }
public ICommand MoveNodeDownCommand { get; }
@@ -179,6 +196,7 @@ namespace XplorePlane.ViewModels.Cnc
LoadNodeParameters(node, null);
PipelineNodes.Add(node);
SelectedNode = node;
UpdateExecutionRangeState();
PersistActiveModule($"已添加算子:{displayName}");
}
@@ -190,6 +208,10 @@ namespace XplorePlane.ViewModels.Cnc
var removedIndex = PipelineNodes.IndexOf(node);
PipelineNodes.Remove(node);
RenumberNodes();
if (ReferenceEquals(ExecutionEndNode, node))
ExecutionEndNode = null;
else
UpdateExecutionRangeState();
SelectNeighborAfterRemoval(removedIndex);
PersistActiveModule($"已移除算子:{node.DisplayName}");
@@ -206,6 +228,7 @@ namespace XplorePlane.ViewModels.Cnc
PipelineNodes.Move(index, index - 1);
RenumberNodes();
UpdateExecutionRangeState();
PersistActiveModule($"已上移算子:{node.DisplayName}");
}
@@ -225,6 +248,7 @@ namespace XplorePlane.ViewModels.Cnc
var node = PipelineNodes[oldIndex];
PipelineNodes.Move(oldIndex, newIndex);
RenumberNodes();
UpdateExecutionRangeState();
SelectedNode = node;
PersistActiveModule($"已调整算子顺序:{node.DisplayName}");
}
@@ -240,6 +264,7 @@ namespace XplorePlane.ViewModels.Cnc
PipelineNodes.Move(index, index + 1);
RenumberNodes();
UpdateExecutionRangeState();
PersistActiveModule($"已下移算子:{node.DisplayName}");
}
@@ -255,6 +280,25 @@ namespace XplorePlane.ViewModels.Cnc
: $"已停用算子:{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()
{
if (!HasActiveModule)
@@ -262,6 +306,7 @@ namespace XplorePlane.ViewModels.Cnc
PipelineNodes.Clear();
SelectedNode = null;
ExecutionEndNode = null;
_currentFilePath = null;
PipelineFileDisplayName = GetActivePipelineFileDisplayName();
PersistActiveModule("已为当前检测模块新建空流水线。");
@@ -326,6 +371,7 @@ namespace XplorePlane.ViewModels.Cnc
{
PipelineNodes.Clear();
SelectedNode = null;
ExecutionEndNode = null;
var orderedNodes = (pipeline?.Nodes ?? new List<PipelineNodeModel>())
.OrderBy(node => node.Order)
@@ -346,6 +392,7 @@ namespace XplorePlane.ViewModels.Cnc
}
SelectedNode = PipelineNodes.FirstOrDefault();
UpdateExecutionRangeState();
if (string.IsNullOrEmpty(_currentFilePath))
PipelineFileDisplayName = GetActivePipelineFileDisplayName();
StatusMessage = HasActiveModule
@@ -423,7 +470,7 @@ namespace XplorePlane.ViewModels.Cnc
try
{
_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:执行完成,推送结果图像");
_mainViewportService.SetManualImage(result, string.Empty);
_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()
{
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)
{
if (PipelineNodes.Count == 0)
@@ -16,6 +16,7 @@ namespace XplorePlane.ViewModels.Cnc
private string _icon;
private bool _isExpanded = true;
private NodeExecutionState _executionState = NodeExecutionState.Idle;
private double _executionProgressPercent;
/// <summary>执行后缓存的流水线输出图像(仅 InspectionModuleNode</summary>
public BitmapSource ResultImage { get; set; }
@@ -72,6 +73,7 @@ namespace XplorePlane.ViewModels.Cnc
RaisePropertyChanged(nameof(IsRunningNode));
RaisePropertyChanged(nameof(IsSucceededNode));
RaisePropertyChanged(nameof(IsFailedNode));
RaisePropertyChanged(nameof(IsDelayProgressVisible));
}
}
}
@@ -79,6 +81,21 @@ namespace XplorePlane.ViewModels.Cnc
public bool IsRunningNode => ExecutionState == NodeExecutionState.Running;
public bool IsSucceededNode => ExecutionState == NodeExecutionState.Succeeded;
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 IsSaveNode => _model is SaveNodeNode;
@@ -567,6 +584,9 @@ namespace XplorePlane.ViewModels.Cnc
RaisePropertyChanged(nameof(IsRunningNode));
RaisePropertyChanged(nameof(IsSucceededNode));
RaisePropertyChanged(nameof(IsFailedNode));
RaisePropertyChanged(nameof(IsDelayProgressVisible));
RaisePropertyChanged(nameof(ExecutionProgressPercent));
RaisePropertyChanged(nameof(ExecutionProgressText));
}
private enum MotionAxis
@@ -23,6 +23,10 @@ namespace XplorePlane.ViewModels
ICommand ToggleOperatorEnabledCommand { get; }
ICommand ExecuteToNodeCommand { get; }
ICommand ClearExecutionRangeCommand { get; }
ICommand MoveNodeUpCommand { get; }
ICommand MoveNodeDownCommand { get; }
@@ -36,6 +36,7 @@ namespace XplorePlane.ViewModels
{
private readonly IImageProcessingService _imageProcessingService;
private string _searchText = string.Empty;
private OperatorGroupViewModel _selectedGroup;
// UI 元数据(分类 + 图标)由 ProcessorUiMetadata 统一提供,保持工具箱与流水线图标一致
@@ -52,6 +53,12 @@ namespace XplorePlane.ViewModels
public ObservableCollection<OperatorDescriptor> FilteredOperators { get; }
public ObservableCollection<OperatorGroupViewModel> FilteredGroups { get; }
public OperatorGroupViewModel SelectedGroup
{
get => _selectedGroup;
set => SetProperty(ref _selectedGroup, value);
}
public string SearchText
{
get => _searchText;
@@ -78,6 +85,7 @@ namespace XplorePlane.ViewModels
{
FilteredOperators.Clear();
FilteredGroups.Clear();
SelectedGroup = null;
var filtered = string.IsNullOrWhiteSpace(SearchText)
? AvailableOperators
@@ -101,6 +109,8 @@ namespace XplorePlane.ViewModels
Operators = new ObservableCollection<OperatorDescriptor>(group)
});
}
SelectedGroup = FilteredGroups.FirstOrDefault();
}
private static int GetCategoryOrder(string category) => category switch
@@ -32,13 +32,14 @@ namespace XplorePlane.ViewModels
private PipelineNodeViewModel _selectedNode;
private BitmapSource _sourceImage;
private BitmapSource _previewImage;
private string _pipelineName = "新建流水线";
private string _pipelineName = "新建模块";
private string _selectedDevice = string.Empty;
private bool _isExecuting;
private bool _isStatusError;
private string _statusMessage = string.Empty;
private string _pipelineFileDisplayName = DefaultPipelineFileDisplayName;
private string _currentFilePath;
private PipelineNodeViewModel _executionEndNode;
private CancellationTokenSource _executionCts;
private CancellationTokenSource _debounceCts;
@@ -64,6 +65,8 @@ namespace XplorePlane.ViewModels
ReorderOperatorCommand = new DelegateCommand<PipelineReorderArgs>(ReorderOperator);
ToggleOperatorEnabledCommand = new DelegateCommand<PipelineNodeViewModel>(ToggleOperatorEnabled);
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);
NewPipelineCommand = new DelegateCommand(NewPipeline);
SavePipelineCommand = new DelegateCommand(async () => await SavePipelineAsync());
@@ -162,6 +165,20 @@ namespace XplorePlane.ViewModels
private set => SetProperty(ref _pipelineFileDisplayName, value);
}
public PipelineNodeViewModel ExecutionEndNode
{
get => _executionEndNode;
private set
{
if (SetProperty(ref _executionEndNode, value))
{
UpdateExecutionRangeState();
ExecuteToNodeCommand.RaiseCanExecuteChanged();
ClearExecutionRangeCommand.RaiseCanExecuteChanged();
}
}
}
// ── Commands ──────────────────────────────────────────────────
public DelegateCommand<string> AddOperatorCommand { get; }
@@ -169,6 +186,8 @@ namespace XplorePlane.ViewModels
public DelegateCommand<PipelineReorderArgs> ReorderOperatorCommand { get; }
public DelegateCommand<PipelineNodeViewModel> ToggleOperatorEnabledCommand { get; }
public DelegateCommand ExecutePipelineCommand { get; }
public DelegateCommand<PipelineNodeViewModel> ExecuteToNodeCommand { get; }
public DelegateCommand ClearExecutionRangeCommand { get; }
public DelegateCommand CancelExecutionCommand { get; }
public DelegateCommand NewPipelineCommand { get; }
public DelegateCommand SavePipelineCommand { get; }
@@ -184,6 +203,8 @@ namespace XplorePlane.ViewModels
ICommand IPipelineEditorHostViewModel.RemoveOperatorCommand => RemoveOperatorCommand;
ICommand IPipelineEditorHostViewModel.ReorderOperatorCommand => ReorderOperatorCommand;
ICommand IPipelineEditorHostViewModel.ToggleOperatorEnabledCommand => ToggleOperatorEnabledCommand;
ICommand IPipelineEditorHostViewModel.ExecuteToNodeCommand => ExecuteToNodeCommand;
ICommand IPipelineEditorHostViewModel.ClearExecutionRangeCommand => ClearExecutionRangeCommand;
ICommand IPipelineEditorHostViewModel.MoveNodeUpCommand => MoveNodeUpCommand;
ICommand IPipelineEditorHostViewModel.MoveNodeDownCommand => MoveNodeDownCommand;
ICommand IPipelineEditorHostViewModel.NewPipelineCommand => NewPipelineCommand;
@@ -234,6 +255,7 @@ namespace XplorePlane.ViewModels
LoadNodeParameters(node);
PipelineNodes.Add(node);
SelectedNode = node;
UpdateExecutionRangeState();
_logger.Info("节点已添加到 PipelineNodes{Key} ({DisplayName}),当前节点数={Count}",
operatorKey, displayName, PipelineNodes.Count);
SetInfoStatus($"已添加算子:{displayName}");
@@ -247,6 +269,10 @@ namespace XplorePlane.ViewModels
var removedIndex = PipelineNodes.IndexOf(node);
PipelineNodes.Remove(node);
RenumberNodes();
if (ReferenceEquals(ExecutionEndNode, node))
ExecutionEndNode = null;
else
UpdateExecutionRangeState();
SelectNeighborAfterRemoval(removedIndex);
SetInfoStatus($"已移除算子:{node.DisplayName}");
@@ -260,6 +286,7 @@ namespace XplorePlane.ViewModels
if (index <= 0) return;
PipelineNodes.Move(index, index - 1);
RenumberNodes();
UpdateExecutionRangeState();
TriggerDebouncedExecution();
}
@@ -270,6 +297,7 @@ namespace XplorePlane.ViewModels
if (index < 0 || index >= PipelineNodes.Count - 1) return;
PipelineNodes.Move(index, index + 1);
RenumberNodes();
UpdateExecutionRangeState();
TriggerDebouncedExecution();
}
@@ -287,6 +315,7 @@ namespace XplorePlane.ViewModels
PipelineNodes.RemoveAt(oldIndex);
PipelineNodes.Insert(newIndex, node);
RenumberNodes();
UpdateExecutionRangeState();
SelectedNode = node;
SetInfoStatus($"已调整算子顺序:{node.DisplayName}");
TriggerDebouncedExecution();
@@ -304,6 +333,34 @@ namespace XplorePlane.ViewModels
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()
{
for (int i = 0; i < PipelineNodes.Count; i++)
@@ -367,10 +424,15 @@ namespace XplorePlane.ViewModels
_executionCts?.Cancel();
_executionCts = new CancellationTokenSource();
var token = _executionCts.Token;
var executionNodes = GetNodesInExecutionScope()
.Where(n => n.IsEnabled)
.OrderBy(n => n.Order)
.ToList();
IsExecuting = true;
SetInfoStatus("正在执行流水线...");
_logger.Info("[图像链路] ExecutePipelineAsync:开始执行,节点数={Count}", PipelineNodes.Count);
SetInfoStatus(BuildExecutionStartMessage(executionNodes.Count));
_logger.Info("[图像链路] ExecutePipelineAsync:开始执行,范围节点数={Count},截止节点={Node}",
executionNodes.Count, ExecutionEndNode?.DisplayName ?? "<all>");
try
{
@@ -378,10 +440,10 @@ namespace XplorePlane.ViewModels
SetInfoStatus($"执行中:{p.CurrentOperator} ({p.CurrentStep}/{p.TotalSteps})"));
var result = await _executionService.ExecutePipelineAsync(
PipelineNodes, SourceImage, progress, token);
executionNodes, SourceImage, progress, token);
PreviewImage = result;
SetInfoStatus("流水线执行完成");
SetInfoStatus(BuildExecutionCompletedMessage(executionNodes.Count));
_logger.Info("[图像链路] ExecutePipelineAsync:执行完成,准备发布 PipelinePreviewUpdatedEvent");
PublishPipelinePreviewUpdated(result, StatusMessage);
}
@@ -408,7 +470,7 @@ namespace XplorePlane.ViewModels
private bool TryReportInvalidParameters()
{
var firstInvalidNode = PipelineNodes
var firstInvalidNode = GetNodesInExecutionScope()
.Where(n => n.IsEnabled)
.OrderBy(n => n.Order)
.FirstOrDefault(n => n.Parameters.Any(p => !p.IsValueValid));
@@ -549,6 +611,7 @@ namespace XplorePlane.ViewModels
{
PipelineNodes.Clear();
SelectedNode = null;
ExecutionEndNode = null;
PipelineName = "新建流水线";
PreviewImage = null;
_currentFilePath = null;
@@ -583,7 +646,7 @@ namespace XplorePlane.ViewModels
var dialog = new SaveFileDialog
{
Filter = "XP 模块流水线 (*.xpm)|*.xpm",
Filter = "XP 模块 (*.xpm)|*.xpm",
DefaultExt = ".xpm",
AddExtension = true,
FileName = PipelineName,
@@ -635,7 +698,7 @@ namespace XplorePlane.ViewModels
{
var dialog = new OpenFileDialog
{
Filter = "XP 模块流水线 (*.xpm)|*.xpm",
Filter = "XP 模块 (*.xpm)|*.xpm",
DefaultExt = ".xpm",
InitialDirectory = GetPipelineDirectory()
};
@@ -648,6 +711,7 @@ namespace XplorePlane.ViewModels
PipelineNodes.Clear();
SelectedNode = null;
ExecutionEndNode = null;
PipelineName = model.Name;
SelectedDevice = model.DeviceId;
@@ -676,6 +740,8 @@ namespace XplorePlane.ViewModels
PipelineNodes.Add(node);
}
UpdateExecutionRangeState();
_logger.Info("流水线已加载:{Name},节点数={Count}", model.Name, PipelineNodes.Count);
SetInfoStatus($"已加载流水线:{model.Name}{PipelineNodes.Count} 个节点)");
}
@@ -704,6 +770,44 @@ namespace XplorePlane.ViewModels
};
}
private System.Collections.Generic.IEnumerable<PipelineNodeViewModel> GetNodesInExecutionScope()
{
var orderedNodes = PipelineNodes.OrderBy(n => n.Order);
if (ExecutionEndNode == null)
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 static string GetPipelineDirectory()
{
var dir = Path.Combine(
@@ -11,6 +11,8 @@ namespace XplorePlane.ViewModels
private int _order;
private bool _isSelected;
private bool _isEnabled = true;
private bool _isExecutionEndNode;
private bool _isSkippedByExecutionRange;
public PipelineNodeViewModel(string operatorKey, string displayName, string iconPath = null)
{
@@ -51,9 +53,49 @@ namespace XplorePlane.ViewModels
public bool IsEnabled
{
get => _isEnabled;
set => SetProperty(ref _isEnabled, value);
set
{
if (SetProperty(ref _isEnabled, value))
RaisePropertyChanged(nameof(NodeStateText));
}
}
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 bool HasOptions => Options is { Length: > 0 };
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
{
@@ -60,10 +68,46 @@ namespace XplorePlane.ViewModels
RaisePropertyChanged(nameof(Value));
RaisePropertyChanged(nameof(BoolValue));
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
{
get => ParameterType == "bool" && TryConvertToBool(_value, out var boolValue) && boolValue;
@@ -154,6 +198,58 @@ namespace XplorePlane.ViewModels
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)
{
return value.Trim().TrimEnd('、', '', ',', '。', '.', ';', '', ':', '');
+2 -2
View File
@@ -23,7 +23,7 @@ namespace XplorePlane.ViewModels
{
public class MainViewModel : BindableBase
{
private const double CncEditorHostWidth = 402d;
private const double CncEditorHostWidth = 452d;
private readonly ILoggerService _logger;
private readonly IContainerProvider _containerProvider;
@@ -205,7 +205,7 @@ namespace XplorePlane.ViewModels
LoadImageCommand = new DelegateCommand(ExecuteLoadImage);
OpenPipelineEditorCommand = new DelegateCommand(() => ShowWindow(new Views.PipelineEditorWindow(), "流水线编辑器"));
OpenCncEditorCommand = new DelegateCommand(ExecuteOpenCncEditor);
OpenMatrixEditorCommand = new DelegateCommand(() => ShowWindow(new Views.Cnc.MatrixEditorWindow(), "矩阵编排"));
OpenToolboxCommand = new DelegateCommand(ExecuteOpenToolbox);
+179 -23
View File
@@ -10,7 +10,7 @@
xmlns:views="clr-namespace:XplorePlane.Views"
xmlns:vm="clr-namespace:XplorePlane.ViewModels.Cnc"
d:DesignHeight="760"
d:DesignWidth="402"
d:DesignWidth="452"
prism:ViewModelLocator.AutoWireViewModel="True"
mc:Ignorable="d">
@@ -96,11 +96,26 @@
<Setter Property="FontFamily" Value="{StaticResource UiFont}" />
<Setter Property="FontSize" Value="11" />
</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>
<Border
Width="402"
MinWidth="402"
Width="452"
MinWidth="452"
HorizontalAlignment="Left"
Background="{StaticResource PanelBg}"
BorderBrush="{StaticResource PanelBorder}"
@@ -108,7 +123,7 @@
CornerRadius="4">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" />
<ColumnDefinition Width="180" />
<ColumnDefinition Width="1" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
@@ -125,24 +140,109 @@
Background="{StaticResource HeaderBg}"
BorderBrush="{StaticResource SeparatorBrush}"
BorderThickness="0,0,0,1">
<WrapPanel>
<StackPanel>
<WrapPanel/>
<WrapPanel Margin="0,4,0,0" Visibility="Collapsed">
<Button
Command="{Binding NewProgramCommand}"
Content="新建"
Style="{StaticResource TreeToolbarButton}" />
Command="{Binding InsertReferencePointCommand}"
Content="参考点"
Style="{StaticResource TreeToolbarButtonCompact}" />
<Button
Command="{Binding SaveProgramCommand}"
Content="保存"
Style="{StaticResource TreeToolbarButton}" />
Command="{Binding InsertSavePositionCommand}"
Content="添加位置"
Style="{StaticResource TreeToolbarButtonCompact}" />
<Button
Command="{Binding LoadProgramCommand}"
Content="加载"
Style="{StaticResource TreeToolbarButton}" />
Command="{Binding InsertInspectionModuleCommand}"
Content="检测模块"
Style="{StaticResource TreeToolbarButtonCompact}" />
<Button
Command="{Binding ExportCsvCommand}"
Content="导出"
Style="{StaticResource TreeToolbarButton}" />
Command="{Binding InsertInspectionMarkerCommand}"
Content="检测标记"
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 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>
<TreeView
@@ -180,6 +280,7 @@
FontFamily="{StaticResource UiFont}"
FontSize="12"
FontWeight="SemiBold"
x:Name="ProgramRootNameText"
Text="{Binding DisplayName}"
TextTrimming="CharacterEllipsis" />
</StackPanel>
@@ -188,6 +289,7 @@
<DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=TreeViewItem}}" Value="True">
<Setter TargetName="ProgramRootCard" Property="Background" Value="#E7F1FB" />
<Setter TargetName="ProgramRootCard" Property="BorderBrush" Value="#9FC6E8" />
<Setter TargetName="ProgramRootNameText" Property="Foreground" Value="#111111" />
</DataTrigger>
</DataTemplate.Triggers>
</HierarchicalDataTemplate>
@@ -205,6 +307,10 @@
BorderThickness="1"
CornerRadius="4">
<Grid x:Name="NodeRoot" MinHeight="23">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="15" />
<ColumnDefinition Width="20" />
@@ -212,7 +318,7 @@
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<Grid Grid.RowSpan="2" Grid.Column="0">
<Border
x:Name="ChildStem"
Width="1"
@@ -230,6 +336,7 @@
</Grid>
<Border
Grid.RowSpan="2"
Grid.Column="1"
Width="16"
Height="16"
@@ -246,6 +353,7 @@
<TextBlock
x:Name="NodeNameText"
Grid.Row="0"
Grid.Column="2"
Margin="3,0,0,0"
VerticalAlignment="Center"
@@ -257,6 +365,7 @@
<StackPanel
x:Name="NodeActions"
Grid.Row="0"
Grid.Column="3"
Margin="0,0,2,0"
VerticalAlignment="Center"
@@ -275,6 +384,31 @@
FontSize="10"
ToolTip="删除" />
</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>
</Border>
<DataTemplate.Triggers>
@@ -287,22 +421,30 @@
<Setter TargetName="NodeActions" Property="Visibility" Value="Visible" />
<Setter TargetName="NodeCard" Property="Background" Value="#DCEEFF" />
<Setter TargetName="NodeCard" Property="BorderBrush" Value="#71A9DB" />
<Setter TargetName="NodeNameText" Property="Foreground" Value="#1F2D3D" />
</DataTrigger>
<DataTrigger Binding="{Binding ExecutionState}" Value="Running">
<Setter TargetName="NodeCard" Property="Background" Value="#FF1E6FD9" />
<Setter TargetName="NodeCard" Property="BorderBrush" Value="#FF1E6FD9" />
<Setter TargetName="NodeNameText" Property="Foreground" Value="White" />
<Setter TargetName="NodeCard" Property="Background" Value="#FFD54F" />
<Setter TargetName="NodeCard" Property="BorderBrush" Value="#C89B00" />
<Setter TargetName="NodeNameText" Property="Foreground" Value="#1F1F1F" />
</DataTrigger>
<DataTrigger Binding="{Binding ExecutionState}" Value="Succeeded">
<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" />
</DataTrigger>
<DataTrigger Binding="{Binding ExecutionState}" Value="Failed">
<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" />
</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>
</HierarchicalDataTemplate>
</TreeView.Resources>
@@ -469,6 +611,20 @@
<StackPanel Margin="10,8,10,6">
<TextBlock Style="{StaticResource LabelStyle}" Text="延时 (ms)" />
<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>
</GroupBox>
</StackPanel>
+5 -31
View File
@@ -19,14 +19,6 @@ namespace XplorePlane.Views.Cnc
/// </summary>
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 readonly Dictionary<TextBox, Label> _textDisplayLabels = new();
private readonly Dictionary<CheckBox, Label> _checkDisplayLabels = new();
@@ -186,24 +178,9 @@ namespace XplorePlane.Views.Cnc
continue;
}
if (item.IsSelected)
{
card.Background = SelectedNodeBackground;
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);
}
card.ClearValue(Border.BackgroundProperty);
card.ClearValue(Border.BorderBrushProperty);
ClearNodeTextForeground(card);
}
}
@@ -318,14 +295,11 @@ namespace XplorePlane.Views.Cnc
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))
{
if (textBlock.Visibility == Visibility.Visible)
{
textBlock.Foreground = foreground;
}
textBlock.ClearValue(TextBlock.ForegroundProperty);
}
}
@@ -63,7 +63,7 @@
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="Foreground" Value="#1c1c1b" />
<Setter Property="BorderBrush" Value="#cdcbcb" />
<Setter Property="Padding" Value="4,6,4,4" />
<Setter Property="Padding" Value="3,4,3,3" />
</Style>
</UserControl.Resources>
@@ -145,7 +145,7 @@
<!-- 右侧:算子选择 + 参数配置 -->
<Border Grid.Column="3" Style="{StaticResource PanelBorderStyle}">
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<StackPanel Margin="10,8,10,10">
<StackPanel Margin="8,6,8,8">
<GroupBox Margin="0,0,0,8" Header="选择算子">
<ComboBox
@@ -204,4 +204,4 @@
</Grid>
</Border>
</Grid>
</UserControl>
</UserControl>
@@ -100,6 +100,7 @@
<TabControl x:Name="ToolboxListBox"
Margin="8"
ItemContainerStyle="{StaticResource OperatorToolboxTabItemStyle}"
SelectedItem="{Binding SelectedGroup, Mode=TwoWay}"
ItemsSource="{Binding FilteredGroups}">
<TabControl.ItemTemplate>
<DataTemplate>
@@ -3,7 +3,8 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:XplorePlane.Views"
Title="算子工具箱"
Width="500" Height="560"
Width="460" Height="540"
MinWidth="420" MinHeight="500"
WindowStartupLocation="CenterOwner"
ShowInTaskbar="False"
WindowStyle="None"
@@ -80,25 +80,25 @@
Orientation="Horizontal">
<Button
Command="{Binding NewPipelineCommand}"
Content="新建"
Content="新建配方"
Style="{StaticResource ToolbarBtn}"
ToolTip="新建流水线" />
ToolTip="新建配方" />
<Button
Command="{Binding SavePipelineCommand}"
Content="保存"
Content="保存配方"
Style="{StaticResource ToolbarBtn}"
ToolTip="保存当前流水线" />
ToolTip="保存当前配方" />
<Button
Width="64"
Command="{Binding SaveAsPipelineCommand}"
Content="另存为"
Style="{StaticResource ToolbarBtn}"
ToolTip="另存当前流水线" />
ToolTip="另存当前配方" />
<Button
Command="{Binding LoadPipelineCommand}"
Content="加载"
Content="加载配方"
Style="{StaticResource ToolbarBtn}"
ToolTip="加载流水线" />
ToolTip="加载配方" />
</StackPanel>
<TextBlock
@@ -132,10 +132,20 @@
x:Name="NodeContainer"
Margin="2"
Padding="2"
Tag="{Binding DataContext, ElementName=RootControl}"
Background="Transparent"
BorderBrush="Transparent"
BorderThickness="1"
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.ColumnDefinitions>
<ColumnDefinition Width="44" />
@@ -193,7 +203,7 @@
FontFamily="{StaticResource UiFont}"
FontSize="10"
Foreground="#6E6E6E"
Text="已启用" />
Text="{Binding NodeStateText}" />
</StackPanel>
</Grid>
</Border>
@@ -209,13 +219,37 @@
<Setter TargetName="IconBorder" Property="BorderBrush" Value="{StaticResource DisabledNodeLine}" />
<Setter TargetName="IconBorder" Property="Background" Value="#ECECEC" />
<Setter TargetName="NodeTitle" Property="Foreground" Value="{StaticResource DisabledNodeText}" />
<Setter TargetName="NodeState" Property="Text" Value="已停用" />
<Setter TargetName="NodeState" Property="Foreground" Value="#9A6767" />
</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">
<Setter TargetName="NodeContainer" Property="Background" Value="#D9ECFF" />
<Setter TargetName="NodeContainer" Property="BorderBrush" Value="#5B9BD5" />
</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.Conditions>
<Condition Binding="{Binding IsEnabled}" Value="False" />
@@ -225,6 +259,15 @@
<Setter TargetName="NodeContainer" Property="BorderBrush" Value="#7E9AB6" />
<Setter TargetName="NodeContainer" Property="Opacity" Value="1" />
</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>
</ListBox.ItemTemplate>
@@ -274,9 +317,53 @@
Text="{Binding DisplayName}"
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
Grid.Column="1"
Padding="4,2"
Padding="2,2"
BorderBrush="#CDCBCB"
BorderThickness="1"
FontFamily="{StaticResource UiFont}"
@@ -59,6 +59,8 @@ namespace XplorePlane.Views
PipelineListBox.PreviewMouseMove += OnPreviewMouseMove;
PipelineListBox.PreviewMouseLeftButtonUp -= OnPreviewMouseLeftButtonUp;
PipelineListBox.PreviewMouseLeftButtonUp += OnPreviewMouseLeftButtonUp;
PipelineListBox.PreviewMouseRightButtonDown -= OnPreviewMouseRightButtonDown;
PipelineListBox.PreviewMouseRightButtonDown += OnPreviewMouseRightButtonDown;
PipelineListBox.MouseDoubleClick -= OnMouseDoubleClick;
PipelineListBox.MouseDoubleClick += OnMouseDoubleClick;
PipelineListBox.PreviewKeyDown -= OnPreviewKeyDown;
@@ -133,6 +135,16 @@ namespace XplorePlane.Views
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)
{
var vm = DataContext as IPipelineEditorHostViewModel;
+3 -7
View File
@@ -279,7 +279,7 @@
Size="Large"
SmallImage="/Assets/Icons/cnc.png"
Text="CNC 编辑" />
<!--
<StackPanel>
<telerik:RadRibbonButton
telerik:ScreenTip.Title="参考点"
@@ -313,12 +313,7 @@
Command="{Binding InsertInspectionModuleCommand}"
SmallImage="/Assets/Icons/Module.png"
Text="检测模块" />
<telerik:RadRibbonButton
telerik:ScreenTip.Title="全部保存"
Size="Medium"
Command="{Binding SaveCncProgramCommand}"
SmallImage="/Assets/Icons/saveall.png"
Text="全部保存" />
</StackPanel>
<StackPanel>
<telerik:RadRibbonButton
@@ -334,6 +329,7 @@
SmallImage="/Assets/Icons/wait.png"
Text="插入等待" />
</StackPanel>
-->
<telerik:RadRibbonButton
telerik:ScreenTip.Description="打开矩阵编排窗口,配置多工件阵列检测方案"
+6
View File
@@ -17,6 +17,7 @@
<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\NavigationPanelView.xaml" />
</ItemGroup>
@@ -149,8 +150,13 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>Libs\Hardware\zh-TW\%(Filename)%(Extension)</Link>
</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\NavigationPanelView.xaml.cs" />
<Content Include="XplorerPlane.ico">