Separate graph layers and state machine state sub-graphs into distinct containers

- Add StateSubGraphs dictionary to AnimGraphViewModel keyed by root node property name
- BuildLayers Pass 2 now stores _StateResult sub-graphs in StateSubGraphs via AddStateSubGraph
- PrefixStateMachineLayerNames iterates StateSubGraphs instead of Layers for renaming
- BuildStateMachineOverviewLayers no longer removes from Layers (sub-graphs are separate)
- AnimGraphViewer uses StateSubGraphs.TryGetValue for direct dictionary lookup by node index

Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-03 14:04:50 +00:00
parent c37dc30133
commit 4fab6d9747
2 changed files with 49 additions and 30 deletions

View File

@@ -77,7 +77,15 @@ public class AnimGraphViewModel
public string PackageName { get; set; } = string.Empty;
public List<AnimGraphNode> Nodes { get; } = [];
public List<AnimGraphConnection> Connections { get; } = [];
/// <summary>
/// Animation blueprint graph layers, each defined by a unique AnimGraphNode_Root.
/// </summary>
public List<AnimGraphLayer> Layers { get; } = [];
/// <summary>
/// State machine state sub-graphs, keyed by the _StateResult root node's property name
/// (derived from StateRootNodeIndex) for unique identification.
/// </summary>
public Dictionary<string, AnimGraphLayer> StateSubGraphs { get; } = new();
/// <summary>
/// Extracts animation graph node information from a UAnimBlueprintGeneratedClass.
@@ -203,6 +211,7 @@ public class AnimGraphViewModel
// Pass 2: Build state machine state sub-graphs from AnimGraphNode_StateResult nodes.
// Each _StateResult node defines a state's sub-graph within a state machine.
// These are stored in StateSubGraphs keyed by the root node's property name.
var stateResultRoots = vm.Nodes
.Where(n => n.ExportType.EndsWith("_StateResult", StringComparison.OrdinalIgnoreCase))
.ToList();
@@ -211,7 +220,7 @@ public class AnimGraphViewModel
{
if (!assigned.Add(stateResultNode)) continue;
var layerNodes = CollectUpstream(stateResultNode, upstreamOf, assigned);
AddLayer(vm, layerNodes, layerIndex++);
AddStateSubGraph(vm, layerNodes, stateResultNode.Name, layerIndex++);
}
// Fallback: any remaining unassigned nodes go into connected-component layers
@@ -304,6 +313,27 @@ public class AnimGraphViewModel
vm.Layers.Add(layer);
}
/// <summary>
/// Creates an <see cref="AnimGraphLayer"/> for a state machine state sub-graph
/// and stores it in <see cref="AnimGraphViewModel.StateSubGraphs"/> keyed by the
/// root node's property name (from StateRootNodeIndex).
/// </summary>
private static void AddStateSubGraph(AnimGraphViewModel vm, List<AnimGraphNode> nodes, string rootNodePropName, int index)
{
var nodeSet = new HashSet<AnimGraphNode>(nodes);
var layer = new AnimGraphLayer { Name = GetLayerName(nodes, index) };
layer.Nodes.AddRange(nodes);
foreach (var conn in vm.Connections)
{
if (nodeSet.Contains(conn.SourceNode) && nodeSet.Contains(conn.TargetNode))
layer.Connections.Add(conn);
}
LayoutLayerNodes(layer);
vm.StateSubGraphs[rootNodePropName] = layer;
}
/// <summary>
/// Renames state machine internal layers with a parent path prefix
/// (e.g., "AnimGraph > Locomotion" for the overview, or
@@ -322,8 +352,8 @@ public class AnimGraphViewModel
}
}
// Rename layers whose nodes belong to a state machine
foreach (var layer in vm.Layers)
// Rename state sub-graphs whose nodes belong to a state machine
foreach (var (key, layer) in vm.StateSubGraphs)
{
var smName = string.Empty;
foreach (var node in layer.Nodes)
@@ -342,17 +372,11 @@ public class AnimGraphViewModel
if (!smParentLayer.TryGetValue(smName, out var parentName))
continue;
// Per-state layers are named by the _StateResult root node's property name (unique);
// use the _StateResult node's Name additional property for the display portion
if (!layer.Name.Equals(smName, StringComparison.OrdinalIgnoreCase))
{
var stateResultNode = layer.Nodes.FirstOrDefault(n =>
n.ExportType.EndsWith("_StateResult", StringComparison.OrdinalIgnoreCase));
var stateName = stateResultNode?.AdditionalProperties.GetValueOrDefault("Name") ?? layer.Name;
layer.Name = $"{parentName}{SubGraphPathSeparator}{smName}{SubGraphPathSeparator}{stateName}";
}
else
layer.Name = $"{parentName}{SubGraphPathSeparator}{smName}";
// Per-state layers: use the _StateResult node's Name additional property for display
var stateResultNode = layer.Nodes.FirstOrDefault(n =>
n.ExportType.EndsWith("_StateResult", StringComparison.OrdinalIgnoreCase));
var stateName = stateResultNode?.AdditionalProperties.GetValueOrDefault("Name") ?? layer.Name;
layer.Name = $"{parentName}{SubGraphPathSeparator}{smName}{SubGraphPathSeparator}{stateName}";
}
}
@@ -383,8 +407,7 @@ public class AnimGraphViewModel
var parentName = smParentLayer.GetValueOrDefault(sm.MachineName, "AnimGraph");
var overviewLayerName = $"{parentName}{SubGraphPathSeparator}{sm.MachineName}";
// Remove existing internal layers with this name (they'll be replaced by the overview)
vm.Layers.RemoveAll(l => l.Name.Equals(overviewLayerName, StringComparison.OrdinalIgnoreCase));
// State sub-graphs are now in StateSubGraphs (no need to remove from Layers)
var overviewLayer = new AnimGraphLayer { Name = overviewLayerName };
var stateNodes = new List<AnimGraphNode>();

View File

@@ -563,27 +563,23 @@ public partial class AnimGraphViewer
}
else if (node.IsStateMachineState)
{
// State nodes within an overview: find the per-state layer by StateRootNodeIndex
// State nodes within an overview: find the per-state sub-graph by StateRootNodeIndex
// The root node's property name is stored on the overview state node
if (node.AdditionalProperties.TryGetValue("StateRootNodeName", out var rootNodeName) &&
!string.IsNullOrEmpty(rootNodeName))
!string.IsNullOrEmpty(rootNodeName) &&
_viewModel.StateSubGraphs.TryGetValue(rootNodeName, out var stateLayer))
{
var stateLayer = _viewModel.Layers.FirstOrDefault(l =>
l.Nodes.Any(n => n.Name == rootNodeName));
if (stateLayer != null)
// If tab already exists, just select it
foreach (System.Windows.Controls.TabItem tab in LayerTabControl.Items)
{
// If tab already exists, just select it
foreach (System.Windows.Controls.TabItem tab in LayerTabControl.Items)
if (tab.Tag == stateLayer)
{
if (tab.Tag == stateLayer)
{
LayerTabControl.SelectedItem = tab;
return;
}
LayerTabControl.SelectedItem = tab;
return;
}
AddLayerTab(stateLayer);
return;
}
AddLayerTab(stateLayer);
return;
}
}
else if (node.ExportType.Contains("StateMachine", StringComparison.OrdinalIgnoreCase))