From 87cb948dfb273cf0343259099ef54ec7776726d8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 06:07:49 +0000 Subject: [PATCH] Fix SaveCachedPose to be placed in correct ancestor _Root layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of always placing SaveCachedPose in the primary (first) _Root layer, trace downstream UseCachedPose consumers through the state machine hierarchy (BelongsToStateMachine → StateMachineName) to find the correct ancestor animation blueprint layer. For example, a SaveCachedPose used by UseCachedPose in AnimGraph > BaseLayer > LocomotionStates > IdleState is now correctly placed in BaseLayer (the _Root layer containing LocomotionStates) instead of AnimGraph. - Add FindOwnerRootLayer: traces SaveCachedPose downstream consumers to find the correct _Root layer - Add GetAncestorRootLayer: walks up the layer hierarchy via BelongsToStateMachine → StateMachineName chain - Add BuildLayerLookups/LayerLookups: pre-computed lookup maps for efficient multi-node queries - Update EnforceSaveCachedPoseInRootLayers to use smart layer detection Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com> --- FModel/ViewModels/AnimGraphViewModel.cs | 178 ++++++++++++++++++------ 1 file changed, 139 insertions(+), 39 deletions(-) diff --git a/FModel/ViewModels/AnimGraphViewModel.cs b/FModel/ViewModels/AnimGraphViewModel.cs index c449f2ab..aa69ecb9 100644 --- a/FModel/ViewModels/AnimGraphViewModel.cs +++ b/FModel/ViewModels/AnimGraphViewModel.cs @@ -231,9 +231,10 @@ public class AnimGraphViewModel AddStateSubGraph(vm, layerNodes, stateResultNode.Name, layerIndex++); } - // Pass 3: Catch any still-unassigned SaveCachedPose nodes and their input - // chains, adding them to the primary AnimGraph layer before the fallback - // handles remaining nodes. + // Pass 3: Assign unassigned SaveCachedPose nodes to their correct _Root layer. + // SaveCachedPose can only exist in animation blueprint layers (defined by _Root nodes). + // Determine the correct _Root layer by tracing downstream UseCachedPose consumers + // back through the state machine hierarchy to their parent animation blueprint layer. if (primaryGraphLayer != null) { var unassignedSavePoseNodes = vm.Nodes @@ -242,14 +243,20 @@ public class AnimGraphViewModel if (unassignedSavePoseNodes.Count > 0) { + var lookups = BuildLayerLookups(vm); + var affectedLayers = new HashSet(); + foreach (var saveNode in unassignedSavePoseNodes) { if (!assigned.Add(saveNode)) continue; + var targetLayer = FindOwnerRootLayer(saveNode, vm, lookups) ?? primaryGraphLayer; var inputChain = CollectUpstream(saveNode, upstreamOf, assigned); - primaryGraphLayer.Nodes.AddRange(inputChain); + targetLayer.Nodes.AddRange(inputChain); + affectedLayers.Add(targetLayer); } - RebuildLayerConnections(vm, primaryGraphLayer); + foreach (var layer in affectedLayers) + RebuildLayerConnections(vm, layer); } } @@ -298,24 +305,23 @@ public class AnimGraphViewModel // Final enforcement: SaveCachedPose nodes can only exist in animation // blueprint layers (layers that contain a _Root node). After all passes // above, scan every non-_Root layer (state sub-graphs and fallback layers) - // and move any SaveCachedPose nodes to the primary _Root layer. + // and move any SaveCachedPose nodes to the correct _Root layer. if (primaryGraphLayer != null) { - EnforceSaveCachedPoseInAnimBlueprintLayer(vm, primaryGraphLayer); + EnforceSaveCachedPoseInRootLayers(vm, primaryGraphLayer); } } /// /// Ensures every SaveCachedPose node resides in an animation blueprint layer /// (one that contains a _Root node). Any SaveCachedPose found in a state - /// sub-graph or a fallback connected-component layer is moved to - /// and the affected layers are rebuilt. + /// sub-graph or a fallback connected-component layer is moved to the correct + /// _Root layer determined by tracing its UseCachedPose consumers. + /// Falls back to if no better target is found. /// - private static void EnforceSaveCachedPoseInAnimBlueprintLayer( - AnimGraphViewModel vm, AnimGraphLayer targetLayer) + private static void EnforceSaveCachedPoseInRootLayers( + AnimGraphViewModel vm, AnimGraphLayer fallbackLayer) { - var moved = false; - // Identify animation-blueprint layers (layers that contain a _Root node) var animBlueprintLayers = new HashSet(); foreach (var layer in vm.Layers) @@ -324,46 +330,140 @@ public class AnimGraphViewModel animBlueprintLayers.Add(layer); } - // Scan non-animation-blueprint layers in vm.Layers - var affectedLayers = new HashSet(); + // Collect SaveCachedPose nodes from non-_Root layers and state sub-graphs + var moves = new List<(AnimGraphNode node, AnimGraphLayer sourceLayer)>(); + foreach (var layer in vm.Layers) { if (animBlueprintLayers.Contains(layer)) continue; - var toMove = layer.Nodes.Where(IsSaveCachedPoseNode).ToList(); - if (toMove.Count == 0) continue; - - foreach (var node in toMove) - { - layer.Nodes.Remove(node); - targetLayer.Nodes.Add(node); - } - affectedLayers.Add(layer); - moved = true; + foreach (var node in layer.Nodes.Where(IsSaveCachedPoseNode).ToList()) + moves.Add((node, layer)); } - // Scan state sub-graphs foreach (var (_, layer) in vm.StateSubGraphs) { - var toMove = layer.Nodes.Where(IsSaveCachedPoseNode).ToList(); - if (toMove.Count == 0) continue; - - foreach (var node in toMove) - { - layer.Nodes.Remove(node); - targetLayer.Nodes.Add(node); - } - affectedLayers.Add(layer); - moved = true; + foreach (var node in layer.Nodes.Where(IsSaveCachedPoseNode).ToList()) + moves.Add((node, layer)); } - if (!moved) return; + if (moves.Count == 0) return; + + var lookups = BuildLayerLookups(vm); + var affectedLayers = new HashSet(); + foreach (var (node, sourceLayer) in moves) + { + var targetLayer = FindOwnerRootLayer(node, vm, lookups) ?? fallbackLayer; + sourceLayer.Nodes.Remove(node); + targetLayer.Nodes.Add(node); + affectedLayers.Add(sourceLayer); + affectedLayers.Add(targetLayer); + } - // Rebuild connections for the target layer and all affected source layers - RebuildLayerConnections(vm, targetLayer); foreach (var layer in affectedLayers) RebuildLayerConnections(vm, layer); } + /// + /// Pre-computed lookup maps for layer hierarchy traversal. + /// + private readonly struct LayerLookups( + Dictionary nodeToLayer, + Dictionary machineToLayer) + { + public Dictionary NodeToLayer { get; } = nodeToLayer; + public Dictionary MachineToLayer { get; } = machineToLayer; + } + + /// + /// Builds the lookup maps needed by and + /// . Call once and reuse for multiple queries. + /// + private static LayerLookups BuildLayerLookups(AnimGraphViewModel vm) + { + var nodeToLayer = new Dictionary(); + foreach (var layer in vm.Layers) + foreach (var node in layer.Nodes) + nodeToLayer.TryAdd(node, layer); + foreach (var (_, subGraph) in vm.StateSubGraphs) + foreach (var node in subGraph.Nodes) + nodeToLayer.TryAdd(node, subGraph); + + var machineToLayer = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var layer in vm.Layers) + foreach (var node in layer.Nodes) + if (node.AdditionalProperties.TryGetValue("StateMachineName", out var mn)) + machineToLayer.TryAdd(mn, layer); + foreach (var (_, subGraph) in vm.StateSubGraphs) + foreach (var node in subGraph.Nodes) + if (node.AdditionalProperties.TryGetValue("StateMachineName", out var mn)) + machineToLayer.TryAdd(mn, subGraph); + + return new LayerLookups(nodeToLayer, machineToLayer); + } + + /// + /// Finds the correct animation blueprint layer (_Root layer) for a SaveCachedPose node + /// by tracing its downstream UseCachedPose consumers through the state machine + /// hierarchy to find the parent animation blueprint layer. + /// Returns null if no suitable layer is found. + /// + private static AnimGraphLayer? FindOwnerRootLayer( + AnimGraphNode saveNode, AnimGraphViewModel vm, LayerLookups lookups) + { + // Find downstream consumers (UseCachedPose nodes referencing this SaveCachedPose) + // Trace each consumer up to find the ancestor _Root layer + foreach (var conn in vm.Connections) + { + if (conn.SourceNode != saveNode) continue; + + if (!lookups.NodeToLayer.TryGetValue(conn.TargetNode, out var consumerLayer)) + continue; + + var rootLayer = GetAncestorRootLayer(consumerLayer, lookups.MachineToLayer); + if (rootLayer != null) + return rootLayer; + } + + return null; + } + + /// + /// Walks up the layer hierarchy to find the nearest ancestor animation blueprint + /// layer (one containing a _Root node). For state sub-graphs, traces through the + /// BelongsToStateMachine → StateMachineName chain to find the parent layer. + /// + private static AnimGraphLayer? GetAncestorRootLayer( + AnimGraphLayer layer, + Dictionary machineToLayer) + { + var visited = new HashSet(); + var current = layer; + + while (current != null && visited.Add(current)) + { + // If this layer contains a _Root node, it's an animation blueprint layer + if (current.Nodes.Any(n => n.ExportType.EndsWith("_Root", StringComparison.OrdinalIgnoreCase))) + return current; + + // Find the state machine this sub-graph belongs to + string? smName = null; + foreach (var node in current.Nodes) + { + if (node.AdditionalProperties.TryGetValue("BelongsToStateMachine", out var val) && + !string.IsNullOrEmpty(val)) + { + smName = val; + break; + } + } + + if (string.IsNullOrEmpty(smName) || !machineToLayer.TryGetValue(smName, out current)) + break; + } + + return null; + } + /// /// Rebuilds a layer's connection list from vm.Connections based on which /// nodes are currently in the layer, then re-runs layout.