2 Commits

Author SHA1 Message Date
zhengxuan.zhang 6a96befac4 CNC树形结构的优化 2026-04-22 01:59:33 +08:00
zhengxuan.zhang 3211dbc473 突出检测模块和检测标记的父子节点关系 2026-04-22 01:44:52 +08:00
5 changed files with 371 additions and 105 deletions
+232 -16
View File
@@ -33,6 +33,7 @@ namespace XplorePlane.ViewModels.Cnc
private CncNodeViewModel _selectedNode;
private bool _isModified;
private string _programName;
private Guid? _preferredSelectedNodeId;
public CncEditorViewModel(
ICncProgramService cncProgramService,
@@ -136,8 +137,9 @@ namespace XplorePlane.ViewModels.Cnc
try
{
var node = _cncProgramService.CreateNode(nodeType);
int afterIndex = SelectedNode?.Index ?? _currentProgram.Nodes.Count - 1;
int afterIndex = ResolveInsertAfterIndex(nodeType);
_currentProgram = _cncProgramService.InsertNode(_currentProgram, afterIndex, node);
_preferredSelectedNodeId = node.Id;
OnProgramEdited();
_logger.Info("Inserted node: Type={NodeType}", nodeType);
@@ -155,7 +157,19 @@ namespace XplorePlane.ViewModels.Cnc
try
{
_currentProgram = _cncProgramService.RemoveNode(_currentProgram, SelectedNode.Index);
if (SelectedNode.IsSavePosition)
{
var nodes = _currentProgram.Nodes.ToList();
int startIndex = SelectedNode.Index;
int endIndex = GetSavePositionBlockEndIndex(startIndex);
nodes.RemoveRange(startIndex, endIndex - startIndex + 1);
_currentProgram = ReplaceProgramNodes(nodes);
}
else
{
_currentProgram = _cncProgramService.RemoveNode(_currentProgram, SelectedNode.Index);
}
OnProgramEdited();
_logger.Info("Deleted node at index: {Index}", SelectedNode.Index);
}
@@ -179,10 +193,18 @@ namespace XplorePlane.ViewModels.Cnc
try
{
_currentProgram = _cncProgramService.MoveNode(_currentProgram, nodeVm.Index, nodeVm.Index - 1);
if (IsSavePositionChild(nodeVm.NodeType))
{
MoveSavePositionChild(nodeVm, moveDown: false);
}
else
{
MoveRootBlock(nodeVm, moveDown: false);
}
OnProgramEdited();
}
catch (ArgumentOutOfRangeException ex)
catch (Exception ex) when (ex is ArgumentOutOfRangeException or InvalidOperationException)
{
_logger.Warn("Move node up failed: {Message}", ex.Message);
}
@@ -195,10 +217,18 @@ namespace XplorePlane.ViewModels.Cnc
try
{
_currentProgram = _cncProgramService.MoveNode(_currentProgram, nodeVm.Index, nodeVm.Index + 1);
if (IsSavePositionChild(nodeVm.NodeType))
{
MoveSavePositionChild(nodeVm, moveDown: true);
}
else
{
MoveRootBlock(nodeVm, moveDown: true);
}
OnProgramEdited();
}
catch (ArgumentOutOfRangeException ex)
catch (Exception ex) when (ex is ArgumentOutOfRangeException or InvalidOperationException)
{
_logger.Warn("Move node down failed: {Message}", ex.Message);
}
@@ -349,12 +379,12 @@ namespace XplorePlane.ViewModels.Cnc
private void RefreshNodes()
{
var selectedId = SelectedNode?.Id;
var selectedId = _preferredSelectedNodeId ?? SelectedNode?.Id;
var expansionState = Nodes.ToDictionary(node => node.Id, node => node.IsExpanded);
var flatNodes = new List<CncNodeViewModel>();
var rootNodes = new List<CncNodeViewModel>();
CncNodeViewModel currentModule = null;
CncNodeViewModel currentSavePosition = null;
if (_currentProgram?.Nodes != null)
{
@@ -367,21 +397,21 @@ namespace XplorePlane.ViewModels.Cnc
flatNodes.Add(vm);
if (vm.IsInspectionModule)
if (vm.IsSavePosition)
{
rootNodes.Add(vm);
currentModule = vm;
currentSavePosition = vm;
continue;
}
if (currentModule != null && IsModuleChild(vm.NodeType))
if (currentSavePosition != null && IsSavePositionChild(vm.NodeType))
{
currentModule.Children.Add(vm);
currentSavePosition.Children.Add(vm);
continue;
}
rootNodes.Add(vm);
currentModule = null;
currentSavePosition = null;
}
}
@@ -391,13 +421,199 @@ namespace XplorePlane.ViewModels.Cnc
SelectedNode = selectedId.HasValue
? Nodes.FirstOrDefault(node => node.Id == selectedId.Value) ?? Nodes.LastOrDefault()
: Nodes.LastOrDefault();
_preferredSelectedNodeId = null;
}
private static bool IsModuleChild(CncNodeType type)
private int ResolveInsertAfterIndex(CncNodeType nodeType)
{
if (_currentProgram == null || _currentProgram.Nodes.Count == 0)
{
return -1;
}
if (!IsSavePositionChild(nodeType))
{
return SelectedNode?.Index ?? _currentProgram.Nodes.Count - 1;
}
int? savePositionIndex = FindOwningSavePositionIndex(SelectedNode?.Index);
if (!savePositionIndex.HasValue)
{
throw new InvalidOperationException("请先选择一个“保存位置”节点,再插入标记点或检测模块。");
}
return GetSavePositionBlockEndIndex(savePositionIndex.Value);
}
private void MoveSavePositionChild(CncNodeViewModel nodeVm, bool moveDown)
{
int? parentIndex = FindOwningSavePositionIndex(nodeVm.Index);
if (!parentIndex.HasValue)
{
throw new InvalidOperationException("当前子节点未归属于任何保存位置,无法移动。");
}
int childStartIndex = parentIndex.Value + 1;
int childEndIndex = GetSavePositionBlockEndIndex(parentIndex.Value);
int targetIndex = moveDown ? nodeVm.Index + 1 : nodeVm.Index - 1;
if (targetIndex < childStartIndex || targetIndex > childEndIndex)
{
return;
}
_currentProgram = _cncProgramService.MoveNode(_currentProgram, nodeVm.Index, targetIndex);
_preferredSelectedNodeId = nodeVm.Id;
}
private void MoveRootBlock(CncNodeViewModel nodeVm, bool moveDown)
{
var blocks = BuildRootBlocks(_currentProgram.Nodes);
int blockIndex = blocks.FindIndex(block => block.Start == nodeVm.Index);
if (blockIndex < 0)
{
return;
}
if (!moveDown && blockIndex == 0)
{
return;
}
if (moveDown && blockIndex >= blocks.Count - 1)
{
return;
}
var currentBlock = blocks[blockIndex];
var targetBlock = blocks[moveDown ? blockIndex + 1 : blockIndex - 1];
var nodes = _currentProgram.Nodes.ToList();
var movingNodes = nodes.GetRange(currentBlock.Start, currentBlock.End - currentBlock.Start + 1);
nodes.RemoveRange(currentBlock.Start, movingNodes.Count);
int insertAt = moveDown
? targetBlock.End - movingNodes.Count + 1
: targetBlock.Start;
nodes.InsertRange(insertAt, movingNodes);
_currentProgram = ReplaceProgramNodes(nodes);
_preferredSelectedNodeId = nodeVm.Id;
}
private int? FindOwningSavePositionIndex(int? startIndex)
{
if (!startIndex.HasValue || _currentProgram == null)
{
return null;
}
int index = startIndex.Value;
if (index < 0 || index >= _currentProgram.Nodes.Count)
{
return null;
}
var selectedType = _currentProgram.Nodes[index].NodeType;
if (_currentProgram.Nodes[index].NodeType == CncNodeType.SavePosition)
{
return index;
}
if (!IsSavePositionChild(selectedType))
{
return null;
}
for (int i = index - 1; i >= 0; i--)
{
var type = _currentProgram.Nodes[i].NodeType;
if (type == CncNodeType.SavePosition)
{
return i;
}
if (!IsSavePositionChild(type))
{
break;
}
}
return null;
}
private int GetSavePositionBlockEndIndex(int savePositionIndex)
{
if (_currentProgram == null)
{
return savePositionIndex;
}
int endIndex = savePositionIndex;
for (int i = savePositionIndex + 1; i < _currentProgram.Nodes.Count; i++)
{
if (!IsSavePositionChild(_currentProgram.Nodes[i].NodeType))
{
break;
}
endIndex = i;
}
return endIndex;
}
private static List<(int Start, int End)> BuildRootBlocks(IReadOnlyList<CncNode> nodes)
{
var blocks = new List<(int Start, int End)>();
for (int i = 0; i < nodes.Count; i++)
{
if (IsSavePositionChild(nodes[i].NodeType))
{
continue;
}
int end = i;
if (nodes[i].NodeType == CncNodeType.SavePosition)
{
for (int j = i + 1; j < nodes.Count; j++)
{
if (!IsSavePositionChild(nodes[j].NodeType))
{
break;
}
end = j;
}
}
blocks.Add((i, end));
}
return blocks;
}
private CncProgram ReplaceProgramNodes(List<CncNode> nodes)
{
var renumberedNodes = nodes
.Select((node, index) => node with { Index = index })
.ToList()
.AsReadOnly();
return _currentProgram with
{
Nodes = renumberedNodes,
UpdatedAt = DateTime.UtcNow
};
}
private static bool IsSavePositionChild(CncNodeType type)
{
return type is CncNodeType.InspectionMarker
or CncNodeType.PauseDialog
or CncNodeType.WaitDelay;
or CncNodeType.InspectionModule;
}
private void PublishProgramChanged()
@@ -64,7 +64,15 @@ namespace XplorePlane.ViewModels.Cnc
public bool IsPauseDialog => _model is PauseDialogNode;
public bool IsWaitDelay => _model is WaitDelayNode;
public bool IsCompleteProgram => _model is CompleteProgramNode;
public bool IsPositionChild => _model is InspectionModuleNode or InspectionMarkerNode;
public bool IsMotionSnapshotNode => _model is ReferencePointNode or SaveNodeNode or SaveNodeWithImageNode or SavePositionNode;
public string RelationTag => _model switch
{
SavePositionNode => "位置",
InspectionModuleNode => "检测模块",
InspectionMarkerNode => "检测标记",
_ => string.Empty
};
public double XM
{
@@ -504,7 +512,9 @@ namespace XplorePlane.ViewModels.Cnc
RaisePropertyChanged(nameof(IsPauseDialog));
RaisePropertyChanged(nameof(IsWaitDelay));
RaisePropertyChanged(nameof(IsCompleteProgram));
RaisePropertyChanged(nameof(IsPositionChild));
RaisePropertyChanged(nameof(IsMotionSnapshotNode));
RaisePropertyChanged(nameof(RelationTag));
RaisePropertyChanged(nameof(XM));
RaisePropertyChanged(nameof(YM));
RaisePropertyChanged(nameof(ZT));
+126 -86
View File
@@ -21,6 +21,10 @@
<SolidColorBrush x:Key="PanelBorder" Color="#CDCBCB" />
<SolidColorBrush x:Key="SeparatorBrush" Color="#E5E5E5" />
<SolidColorBrush x:Key="HeaderBg" Color="#F7F7F7" />
<SolidColorBrush x:Key="TreeParentBg" Color="#F6F8FB" />
<SolidColorBrush x:Key="TreeChildBg" Color="#FBFCFE" />
<SolidColorBrush x:Key="TreeAccentBrush" Color="#2E6FA3" />
<SolidColorBrush x:Key="TreeChildLineBrush" Color="#B9CDE0" />
<FontFamily x:Key="UiFont">Microsoft YaHei UI</FontFamily>
<Style x:Key="TreeItemStyle" TargetType="TreeViewItem">
@@ -109,7 +113,7 @@
FontFamily="{StaticResource UiFont}"
FontSize="10"
Foreground="#666666"
Text="模块节点下会自动显示标记、等待、消息等子节点。"
Text="保存位置节点下会自动显示检测标记和检测模块,清晰表达当前检测位置与检测内容的父子关系。"
TextWrapping="Wrap" />
</StackPanel>
</Border>
@@ -124,97 +128,133 @@
SelectedItemChanged="CncTreeView_SelectedItemChanged">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type vm:CncNodeViewModel}" ItemsSource="{Binding Children}">
<Grid x:Name="NodeRoot" MinHeight="34">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="30" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Border
x:Name="NodeCard"
Margin="0,1,0,1"
Padding="0"
Background="Transparent"
BorderBrush="Transparent"
BorderThickness="1"
CornerRadius="4">
<Grid x:Name="NodeRoot" MinHeight="28">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="18" />
<ColumnDefinition Width="24" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Border
Grid.Column="0"
Width="22"
Height="22"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="Transparent"
CornerRadius="4">
<Image
Width="15"
Height="15"
Source="{Binding Icon}"
Stretch="Uniform" />
</Border>
<Grid Grid.Column="0">
<Border
x:Name="ChildStem"
Width="2"
Margin="8,0,0,0"
HorizontalAlignment="Left"
Background="{StaticResource TreeChildLineBrush}"
Visibility="Collapsed" />
<Border
x:Name="ChildBranch"
Width="10"
Height="2"
Margin="8,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Background="{StaticResource TreeChildLineBrush}"
Visibility="Collapsed" />
</Grid>
<StackPanel
Grid.Column="1"
Margin="4,0,0,0"
VerticalAlignment="Center"
Orientation="Horizontal">
<TextBlock
<Border
Grid.Column="1"
Width="18"
Height="18"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontFamily="{StaticResource UiFont}"
FontSize="10.5"
Foreground="#888888"
Text="{Binding Index, StringFormat='[{0}] '}" />
<TextBlock
VerticalAlignment="Center"
FontFamily="{StaticResource UiFont}"
FontSize="11.5"
Text="{Binding Name}" />
</StackPanel>
Background="Transparent"
CornerRadius="4">
<Image
Width="14"
Height="14"
Source="{Binding Icon}"
Stretch="Uniform" />
</Border>
<StackPanel
x:Name="NodeActions"
Grid.Column="2"
Margin="0,0,2,0"
VerticalAlignment="Center"
Orientation="Horizontal"
Visibility="Collapsed">
<Button
Width="20"
Height="20"
Margin="1,0"
Background="White"
BorderBrush="#CDCBCB"
BorderThickness="1"
Command="{Binding DataContext.MoveNodeUpCommand, RelativeSource={RelativeSource AncestorType=TreeView}}"
CommandParameter="{Binding}"
Content="上"
Cursor="Hand"
FontSize="10"
ToolTip="上移" />
<Button
Width="20"
Height="20"
Margin="1,0"
Background="White"
BorderBrush="#CDCBCB"
BorderThickness="1"
Command="{Binding DataContext.MoveNodeDownCommand, RelativeSource={RelativeSource AncestorType=TreeView}}"
CommandParameter="{Binding}"
Content="下"
Cursor="Hand"
FontSize="10"
ToolTip="下移" />
<Button
Width="20"
Height="20"
Margin="1,0"
Background="White"
BorderBrush="#E05050"
BorderThickness="1"
Command="{Binding DataContext.DeleteNodeCommand, RelativeSource={RelativeSource AncestorType=TreeView}}"
Content=""
Cursor="Hand"
FontSize="10"
ToolTip="删除" />
</StackPanel>
</Grid>
<StackPanel
Grid.Column="2"
Margin="3,0,0,0"
VerticalAlignment="Center"
Orientation="Horizontal">
<TextBlock
VerticalAlignment="Center"
FontFamily="{StaticResource UiFont}"
FontSize="10.5"
Foreground="#888888"
Text="{Binding Index, StringFormat='[{0}] '}" />
<TextBlock
VerticalAlignment="Center"
FontFamily="{StaticResource UiFont}"
FontSize="11.5"
FontWeight="SemiBold"
Text="{Binding Name}" />
</StackPanel>
<StackPanel
x:Name="NodeActions"
Grid.Column="3"
Margin="0,0,2,0"
VerticalAlignment="Center"
Orientation="Horizontal"
Visibility="Collapsed">
<Button
Width="20"
Height="20"
Margin="1,0"
Background="White"
BorderBrush="#CDCBCB"
BorderThickness="1"
Command="{Binding DataContext.MoveNodeUpCommand, RelativeSource={RelativeSource AncestorType=TreeView}}"
CommandParameter="{Binding}"
Content=""
Cursor="Hand"
FontSize="10"
ToolTip="上移" />
<Button
Width="20"
Height="20"
Margin="1,0"
Background="White"
BorderBrush="#CDCBCB"
BorderThickness="1"
Command="{Binding DataContext.MoveNodeDownCommand, RelativeSource={RelativeSource AncestorType=TreeView}}"
CommandParameter="{Binding}"
Content="↓"
Cursor="Hand"
FontSize="10"
ToolTip="下移" />
<Button
Width="20"
Height="20"
Margin="1,0"
Background="White"
BorderBrush="#E05050"
BorderThickness="1"
Command="{Binding DataContext.DeleteNodeCommand, RelativeSource={RelativeSource AncestorType=TreeView}}"
Content="×"
Cursor="Hand"
FontSize="10"
ToolTip="删除" />
</StackPanel>
</Grid>
</Border>
<DataTemplate.Triggers>
<Trigger SourceName="NodeRoot" Property="IsMouseOver" Value="True">
<Setter TargetName="NodeActions" Property="Visibility" Value="Visible" />
<Setter TargetName="NodeCard" Property="Background" Value="#F3F7FB" />
<Setter TargetName="NodeCard" Property="BorderBrush" Value="#D7E4F1" />
</Trigger>
<DataTrigger Binding="{Binding IsPositionChild}" Value="True">
<Setter TargetName="ChildStem" Property="Visibility" Value="Visible" />
<Setter TargetName="ChildBranch" Property="Visibility" Value="Visible" />
</DataTrigger>
</DataTemplate.Triggers>
</HierarchicalDataTemplate>
</TreeView.Resources>
@@ -237,7 +277,7 @@
<TextBlock Style="{StaticResource LabelStyle}" Text="名称" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Name, UpdateSourceTrigger=PropertyChanged}" />
<UniformGrid Columns="2" Margin="0,0,0,8">
<UniformGrid Margin="0,0,0,8" Columns="2">
<StackPanel Margin="0,0,6,0">
<TextBlock Style="{StaticResource LabelStyle}" Text="索引" />
<TextBox Style="{StaticResource EditorBox}" IsReadOnly="True" Text="{Binding SelectedNode.Index, Mode=OneWay}" />
@@ -345,7 +385,7 @@
<GroupBox
Style="{StaticResource CompactGroupBox}"
Header="标记参数"
Header="检测标记"
Visibility="{Binding SelectedNode.IsInspectionMarker, Converter={StaticResource BoolToVisibilityConverter}}">
<StackPanel Margin="8,8,8,6">
<TextBlock Style="{StaticResource LabelStyle}" Text="标记类型" />
+1 -1
View File
@@ -14,7 +14,7 @@
</Grid.RowDefinitions>
<Border Grid.Row="0" Background="#F0F0F0" BorderBrush="#DDDDDD" BorderThickness="0,0,0,1">
<TextBlock Margin="4,2" HorizontalAlignment="Left" VerticalAlignment="Center"
FontWeight="SemiBold" Foreground="#333333" Text="图像" />
FontWeight="SemiBold" Foreground="#333333" Text="图像处理" />
</Border>
<ContentControl Grid.Row="1" Content="{Binding ImagePanelContent}" />
</Grid>
+2 -2
View File
@@ -312,11 +312,11 @@
</StackPanel>
<StackPanel>
<telerik:RadRibbonButton
telerik:ScreenTip.Title="标记"
telerik:ScreenTip.Title="检测标记"
Size="Medium"
Command="{Binding InsertInspectionMarkerCommand}"
SmallImage="/Assets/Icons/mark.png"
Text="标记" />
Text="检测标记" />
<telerik:RadRibbonButton
telerik:ScreenTip.Title="模块"
Size="Medium"