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
+231 -15
View File
@@ -33,6 +33,7 @@ namespace XplorePlane.ViewModels.Cnc
private CncNodeViewModel _selectedNode; private CncNodeViewModel _selectedNode;
private bool _isModified; private bool _isModified;
private string _programName; private string _programName;
private Guid? _preferredSelectedNodeId;
public CncEditorViewModel( public CncEditorViewModel(
ICncProgramService cncProgramService, ICncProgramService cncProgramService,
@@ -136,8 +137,9 @@ namespace XplorePlane.ViewModels.Cnc
try try
{ {
var node = _cncProgramService.CreateNode(nodeType); var node = _cncProgramService.CreateNode(nodeType);
int afterIndex = SelectedNode?.Index ?? _currentProgram.Nodes.Count - 1; int afterIndex = ResolveInsertAfterIndex(nodeType);
_currentProgram = _cncProgramService.InsertNode(_currentProgram, afterIndex, node); _currentProgram = _cncProgramService.InsertNode(_currentProgram, afterIndex, node);
_preferredSelectedNodeId = node.Id;
OnProgramEdited(); OnProgramEdited();
_logger.Info("Inserted node: Type={NodeType}", nodeType); _logger.Info("Inserted node: Type={NodeType}", nodeType);
@@ -154,8 +156,20 @@ namespace XplorePlane.ViewModels.Cnc
return; return;
try try
{
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); _currentProgram = _cncProgramService.RemoveNode(_currentProgram, SelectedNode.Index);
}
OnProgramEdited(); OnProgramEdited();
_logger.Info("Deleted node at index: {Index}", SelectedNode.Index); _logger.Info("Deleted node at index: {Index}", SelectedNode.Index);
} }
@@ -179,10 +193,18 @@ namespace XplorePlane.ViewModels.Cnc
try try
{ {
_currentProgram = _cncProgramService.MoveNode(_currentProgram, nodeVm.Index, nodeVm.Index - 1); if (IsSavePositionChild(nodeVm.NodeType))
{
MoveSavePositionChild(nodeVm, moveDown: false);
}
else
{
MoveRootBlock(nodeVm, moveDown: false);
}
OnProgramEdited(); OnProgramEdited();
} }
catch (ArgumentOutOfRangeException ex) catch (Exception ex) when (ex is ArgumentOutOfRangeException or InvalidOperationException)
{ {
_logger.Warn("Move node up failed: {Message}", ex.Message); _logger.Warn("Move node up failed: {Message}", ex.Message);
} }
@@ -195,10 +217,18 @@ namespace XplorePlane.ViewModels.Cnc
try try
{ {
_currentProgram = _cncProgramService.MoveNode(_currentProgram, nodeVm.Index, nodeVm.Index + 1); if (IsSavePositionChild(nodeVm.NodeType))
{
MoveSavePositionChild(nodeVm, moveDown: true);
}
else
{
MoveRootBlock(nodeVm, moveDown: true);
}
OnProgramEdited(); OnProgramEdited();
} }
catch (ArgumentOutOfRangeException ex) catch (Exception ex) when (ex is ArgumentOutOfRangeException or InvalidOperationException)
{ {
_logger.Warn("Move node down failed: {Message}", ex.Message); _logger.Warn("Move node down failed: {Message}", ex.Message);
} }
@@ -349,12 +379,12 @@ namespace XplorePlane.ViewModels.Cnc
private void RefreshNodes() private void RefreshNodes()
{ {
var selectedId = SelectedNode?.Id; var selectedId = _preferredSelectedNodeId ?? SelectedNode?.Id;
var expansionState = Nodes.ToDictionary(node => node.Id, node => node.IsExpanded); var expansionState = Nodes.ToDictionary(node => node.Id, node => node.IsExpanded);
var flatNodes = new List<CncNodeViewModel>(); var flatNodes = new List<CncNodeViewModel>();
var rootNodes = new List<CncNodeViewModel>(); var rootNodes = new List<CncNodeViewModel>();
CncNodeViewModel currentModule = null; CncNodeViewModel currentSavePosition = null;
if (_currentProgram?.Nodes != null) if (_currentProgram?.Nodes != null)
{ {
@@ -367,21 +397,21 @@ namespace XplorePlane.ViewModels.Cnc
flatNodes.Add(vm); flatNodes.Add(vm);
if (vm.IsInspectionModule) if (vm.IsSavePosition)
{ {
rootNodes.Add(vm); rootNodes.Add(vm);
currentModule = vm; currentSavePosition = vm;
continue; continue;
} }
if (currentModule != null && IsModuleChild(vm.NodeType)) if (currentSavePosition != null && IsSavePositionChild(vm.NodeType))
{ {
currentModule.Children.Add(vm); currentSavePosition.Children.Add(vm);
continue; continue;
} }
rootNodes.Add(vm); rootNodes.Add(vm);
currentModule = null; currentSavePosition = null;
} }
} }
@@ -391,13 +421,199 @@ namespace XplorePlane.ViewModels.Cnc
SelectedNode = selectedId.HasValue SelectedNode = selectedId.HasValue
? Nodes.FirstOrDefault(node => node.Id == selectedId.Value) ?? Nodes.LastOrDefault() ? Nodes.FirstOrDefault(node => node.Id == selectedId.Value) ?? Nodes.LastOrDefault()
: 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 return type is CncNodeType.InspectionMarker
or CncNodeType.PauseDialog or CncNodeType.InspectionModule;
or CncNodeType.WaitDelay;
} }
private void PublishProgramChanged() private void PublishProgramChanged()
@@ -64,7 +64,15 @@ namespace XplorePlane.ViewModels.Cnc
public bool IsPauseDialog => _model is PauseDialogNode; public bool IsPauseDialog => _model is PauseDialogNode;
public bool IsWaitDelay => _model is WaitDelayNode; public bool IsWaitDelay => _model is WaitDelayNode;
public bool IsCompleteProgram => _model is CompleteProgramNode; 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 bool IsMotionSnapshotNode => _model is ReferencePointNode or SaveNodeNode or SaveNodeWithImageNode or SavePositionNode;
public string RelationTag => _model switch
{
SavePositionNode => "位置",
InspectionModuleNode => "检测模块",
InspectionMarkerNode => "检测标记",
_ => string.Empty
};
public double XM public double XM
{ {
@@ -504,7 +512,9 @@ namespace XplorePlane.ViewModels.Cnc
RaisePropertyChanged(nameof(IsPauseDialog)); RaisePropertyChanged(nameof(IsPauseDialog));
RaisePropertyChanged(nameof(IsWaitDelay)); RaisePropertyChanged(nameof(IsWaitDelay));
RaisePropertyChanged(nameof(IsCompleteProgram)); RaisePropertyChanged(nameof(IsCompleteProgram));
RaisePropertyChanged(nameof(IsPositionChild));
RaisePropertyChanged(nameof(IsMotionSnapshotNode)); RaisePropertyChanged(nameof(IsMotionSnapshotNode));
RaisePropertyChanged(nameof(RelationTag));
RaisePropertyChanged(nameof(XM)); RaisePropertyChanged(nameof(XM));
RaisePropertyChanged(nameof(YM)); RaisePropertyChanged(nameof(YM));
RaisePropertyChanged(nameof(ZT)); RaisePropertyChanged(nameof(ZT));
+56 -16
View File
@@ -21,6 +21,10 @@
<SolidColorBrush x:Key="PanelBorder" Color="#CDCBCB" /> <SolidColorBrush x:Key="PanelBorder" Color="#CDCBCB" />
<SolidColorBrush x:Key="SeparatorBrush" Color="#E5E5E5" /> <SolidColorBrush x:Key="SeparatorBrush" Color="#E5E5E5" />
<SolidColorBrush x:Key="HeaderBg" Color="#F7F7F7" /> <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> <FontFamily x:Key="UiFont">Microsoft YaHei UI</FontFamily>
<Style x:Key="TreeItemStyle" TargetType="TreeViewItem"> <Style x:Key="TreeItemStyle" TargetType="TreeViewItem">
@@ -109,7 +113,7 @@
FontFamily="{StaticResource UiFont}" FontFamily="{StaticResource UiFont}"
FontSize="10" FontSize="10"
Foreground="#666666" Foreground="#666666"
Text="模块节点下会自动显示标记、等待、消息等子节点。" Text="保存位置节点下会自动显示检测标记和检测模块,清晰表达当前检测位置与检测内容的父子关系。"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
</StackPanel> </StackPanel>
</Border> </Border>
@@ -124,31 +128,59 @@
SelectedItemChanged="CncTreeView_SelectedItemChanged"> SelectedItemChanged="CncTreeView_SelectedItemChanged">
<TreeView.Resources> <TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type vm:CncNodeViewModel}" ItemsSource="{Binding Children}"> <HierarchicalDataTemplate DataType="{x:Type vm:CncNodeViewModel}" ItemsSource="{Binding Children}">
<Grid x:Name="NodeRoot" MinHeight="34"> <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> <Grid.ColumnDefinitions>
<ColumnDefinition Width="30" /> <ColumnDefinition Width="18" />
<ColumnDefinition Width="24" />
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<Border <Border
Grid.Column="0" x:Name="ChildStem"
Width="22" Width="2"
Height="22" 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>
<Border
Grid.Column="1"
Width="18"
Height="18"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
Background="Transparent" Background="Transparent"
CornerRadius="4"> CornerRadius="4">
<Image <Image
Width="15" Width="14"
Height="15" Height="14"
Source="{Binding Icon}" Source="{Binding Icon}"
Stretch="Uniform" /> Stretch="Uniform" />
</Border> </Border>
<StackPanel <StackPanel
Grid.Column="1" Grid.Column="2"
Margin="4,0,0,0" Margin="3,0,0,0"
VerticalAlignment="Center" VerticalAlignment="Center"
Orientation="Horizontal"> Orientation="Horizontal">
<TextBlock <TextBlock
@@ -161,12 +193,13 @@
VerticalAlignment="Center" VerticalAlignment="Center"
FontFamily="{StaticResource UiFont}" FontFamily="{StaticResource UiFont}"
FontSize="11.5" FontSize="11.5"
FontWeight="SemiBold"
Text="{Binding Name}" /> Text="{Binding Name}" />
</StackPanel> </StackPanel>
<StackPanel <StackPanel
x:Name="NodeActions" x:Name="NodeActions"
Grid.Column="2" Grid.Column="3"
Margin="0,0,2,0" Margin="0,0,2,0"
VerticalAlignment="Center" VerticalAlignment="Center"
Orientation="Horizontal" Orientation="Horizontal"
@@ -180,7 +213,7 @@
BorderThickness="1" BorderThickness="1"
Command="{Binding DataContext.MoveNodeUpCommand, RelativeSource={RelativeSource AncestorType=TreeView}}" Command="{Binding DataContext.MoveNodeUpCommand, RelativeSource={RelativeSource AncestorType=TreeView}}"
CommandParameter="{Binding}" CommandParameter="{Binding}"
Content="" Content=""
Cursor="Hand" Cursor="Hand"
FontSize="10" FontSize="10"
ToolTip="上移" /> ToolTip="上移" />
@@ -193,7 +226,7 @@
BorderThickness="1" BorderThickness="1"
Command="{Binding DataContext.MoveNodeDownCommand, RelativeSource={RelativeSource AncestorType=TreeView}}" Command="{Binding DataContext.MoveNodeDownCommand, RelativeSource={RelativeSource AncestorType=TreeView}}"
CommandParameter="{Binding}" CommandParameter="{Binding}"
Content="" Content=""
Cursor="Hand" Cursor="Hand"
FontSize="10" FontSize="10"
ToolTip="下移" /> ToolTip="下移" />
@@ -205,16 +238,23 @@
BorderBrush="#E05050" BorderBrush="#E05050"
BorderThickness="1" BorderThickness="1"
Command="{Binding DataContext.DeleteNodeCommand, RelativeSource={RelativeSource AncestorType=TreeView}}" Command="{Binding DataContext.DeleteNodeCommand, RelativeSource={RelativeSource AncestorType=TreeView}}"
Content="" Content="×"
Cursor="Hand" Cursor="Hand"
FontSize="10" FontSize="10"
ToolTip="删除" /> ToolTip="删除" />
</StackPanel> </StackPanel>
</Grid> </Grid>
</Border>
<DataTemplate.Triggers> <DataTemplate.Triggers>
<Trigger SourceName="NodeRoot" Property="IsMouseOver" Value="True"> <Trigger SourceName="NodeRoot" Property="IsMouseOver" Value="True">
<Setter TargetName="NodeActions" Property="Visibility" Value="Visible" /> <Setter TargetName="NodeActions" Property="Visibility" Value="Visible" />
<Setter TargetName="NodeCard" Property="Background" Value="#F3F7FB" />
<Setter TargetName="NodeCard" Property="BorderBrush" Value="#D7E4F1" />
</Trigger> </Trigger>
<DataTrigger Binding="{Binding IsPositionChild}" Value="True">
<Setter TargetName="ChildStem" Property="Visibility" Value="Visible" />
<Setter TargetName="ChildBranch" Property="Visibility" Value="Visible" />
</DataTrigger>
</DataTemplate.Triggers> </DataTemplate.Triggers>
</HierarchicalDataTemplate> </HierarchicalDataTemplate>
</TreeView.Resources> </TreeView.Resources>
@@ -237,7 +277,7 @@
<TextBlock Style="{StaticResource LabelStyle}" Text="名称" /> <TextBlock Style="{StaticResource LabelStyle}" Text="名称" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Name, UpdateSourceTrigger=PropertyChanged}" /> <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"> <StackPanel Margin="0,0,6,0">
<TextBlock Style="{StaticResource LabelStyle}" Text="索引" /> <TextBlock Style="{StaticResource LabelStyle}" Text="索引" />
<TextBox Style="{StaticResource EditorBox}" IsReadOnly="True" Text="{Binding SelectedNode.Index, Mode=OneWay}" /> <TextBox Style="{StaticResource EditorBox}" IsReadOnly="True" Text="{Binding SelectedNode.Index, Mode=OneWay}" />
@@ -345,7 +385,7 @@
<GroupBox <GroupBox
Style="{StaticResource CompactGroupBox}" Style="{StaticResource CompactGroupBox}"
Header="标记参数" Header="检测标记"
Visibility="{Binding SelectedNode.IsInspectionMarker, Converter={StaticResource BoolToVisibilityConverter}}"> Visibility="{Binding SelectedNode.IsInspectionMarker, Converter={StaticResource BoolToVisibilityConverter}}">
<StackPanel Margin="8,8,8,6"> <StackPanel Margin="8,8,8,6">
<TextBlock Style="{StaticResource LabelStyle}" Text="标记类型" /> <TextBlock Style="{StaticResource LabelStyle}" Text="标记类型" />
+1 -1
View File
@@ -14,7 +14,7 @@
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Border Grid.Row="0" Background="#F0F0F0" BorderBrush="#DDDDDD" BorderThickness="0,0,0,1"> <Border Grid.Row="0" Background="#F0F0F0" BorderBrush="#DDDDDD" BorderThickness="0,0,0,1">
<TextBlock Margin="4,2" HorizontalAlignment="Left" VerticalAlignment="Center" <TextBlock Margin="4,2" HorizontalAlignment="Left" VerticalAlignment="Center"
FontWeight="SemiBold" Foreground="#333333" Text="图像" /> FontWeight="SemiBold" Foreground="#333333" Text="图像处理" />
</Border> </Border>
<ContentControl Grid.Row="1" Content="{Binding ImagePanelContent}" /> <ContentControl Grid.Row="1" Content="{Binding ImagePanelContent}" />
</Grid> </Grid>
+2 -2
View File
@@ -312,11 +312,11 @@
</StackPanel> </StackPanel>
<StackPanel> <StackPanel>
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Title="标记" telerik:ScreenTip.Title="检测标记"
Size="Medium" Size="Medium"
Command="{Binding InsertInspectionMarkerCommand}" Command="{Binding InsertInspectionMarkerCommand}"
SmallImage="/Assets/Icons/mark.png" SmallImage="/Assets/Icons/mark.png"
Text="标记" /> Text="检测标记" />
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Title="模块" telerik:ScreenTip.Title="模块"
Size="Medium" Size="Medium"