Add AnimBlueprint Window for preview AnimBlueprint#656
Add AnimBlueprint Window for preview AnimBlueprint#656
Conversation
…and visualization Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
…mprove null handling Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
…and CDO instead of exports In cooked UE assets, UEdGraphNode objects are stripped as editor-only data (PKG_FilterEditorOnly). The actual animation node information is stored in: - UAnimBlueprintGeneratedClass.ChildProperties (FStructProperty entries describing node types) - ClassDefaultObject properties (FStructFallback values with node data) - FPoseLink/FComponentSpacePoseLink structs with LinkID for connections Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
…ion check, fix comment Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
- AnimGraphViewModel: Add AnimGraphLayer class and BuildLayers() to group nodes into connected subgraphs displayed as separate tabs - AnimGraphViewModel: Add topological layout (LayoutLayerNodes) so nodes flow left-to-right by dependency depth instead of grid - AnimGraphViewer.xaml: Replace single canvas with TabControl for layers and a right-side properties panel with GridSplitter - AnimGraphViewer.xaml.cs: Per-layer canvas state, node selection with property panel population, per-tab zoom/pan Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
…onstants Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
- Fix OnMouseWheel: get mouse position relative to parent Border (stable coords) instead of Canvas (transformed coords that shift during zoom); compute canvas-local point manually and preserve it after scale change - Fix OnCanvasMouseDown/Move: use parent Border coords for consistent panning behavior - Improve connection lines: thicker stroke (2px), higher opacity (0.8), minimum bezier tangent length (50px) for smoother short-distance curves Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
In WPF, MouseLeftButtonDown is a Direct routed event that fires independently on each UIElement with a fresh Handled=false. Setting e.Handled=true in the node's click handler does NOT prevent OnCanvasMouseDown on the parent canvasBorder from firing. This caused clicking a node to both select it AND start panning, making the graph jump when the mouse moved. Fix: check e.OriginalSource != sender in OnCanvasMouseDown so panning only starts when clicking directly on the canvas background. Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
…ld panning The previous fix (e.OriginalSource != sender) blocked panning entirely when clicking on nodes, making them feel like buttons. Connection Path elements also received mouse events, potentially interfering. Fix: - Use a drag-threshold mechanism: clicking selects a node without jumping, dragging from anywhere (including nodes) starts panning after a 5px threshold is exceeded. - Register MouseLeftButtonDown with AddHandler(handledEventsToo: true) so the panning handler fires even when nodes set e.Handled = true. - Set IsHitTestVisible = false on connection Path elements so they don't interfere with mouse events. - Check e.LeftButton state in OnCanvasMouseMove to prevent stale pan state when the button is released unexpectedly. Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
- Dark translucent node body with gradient title bar color-coded by type - Proper pin circles (Ellipses) on node edges replacing text bullets - Connection wires colored by source pin type with thicker strokes - Subtle drop shadow behind each node - Orange selection highlight matching UE's selection color - Darker canvas background matching UE blueprint editor - Extract named constants for pin label offset and gradient factor Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
Only the AnimGraph layer (containing Root/Result nodes) is shown when first opening the animation blueprint, instead of all layer tabs. Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
…raph" The output pose layer is now correctly identified by finding the node whose Name property is "AnimGraph" (stored on AnimGraphNode_Root), instead of incorrectly matching any node with "Root" in ExportType. Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
…nstead of node.Name The root node's meaningful identifier "AnimGraph" is stored in the struct's "Name" property (AdditionalProperties["Name"]), not the node's generated Name field (e.g. "AnimGraphNode_Root_0"). Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
FPropertyTagType.ToString() returns "Value (TypeName)" format, including extra type information. Using GenericValue?.ToString() returns just the raw value, which fixes property value extraction and root node detection. Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
TryResolvePoseLink only handled direct struct properties (single FPoseLink). Many animation nodes use arrays of pose links (e.g., BlendPose TArray<FPoseLink> in blend list nodes). Added handling for UScriptArray to iterate array elements and resolve each as a pose link. Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
Generalized GetLayerName to use any Root node's "Name" property as the layer name (not just "AnimGraph"), enabling proper naming for LinkedAnimLayer sub-graphs. Added double-click handling on nodes: double-clicking a LinkedAnimLayer node finds the matching layer and opens/selects a tab for it. Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
Read BakedStateMachines from the animation blueprint class to extract machine names. Associate FAnimNode_StateMachine nodes with their machine name via StateMachineIndexInClass, and mark internal state root nodes with BelongsToStateMachine for correct layer naming. Extend TryOpenSubGraph to handle both LinkedAnimLayer and StateMachine node types for double-click tab navigation. Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
StateMachine internal layer tabs are now named with a parent path (e.g., "AnimGraph > Locomotion") to avoid collisions when a state machine shares a name with a LinkedAnimLayer layer. PrefixStateMachineLayerNames runs after BuildLayers to find each SM node's parent layer and prepend its name. TryOpenSubGraph constructs the same path for lookup. The path separator is a shared constant. Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
…tion connections Parse States and Transitions from BakedStateMachines to build state-level overview layers for each state machine. Each overview shows: - Entry node (filled circle) connecting to the initial state - State nodes (rounded rectangles) with their names - Directional transition arrows between states The overview layer replaces the old internal per-state layer and is opened via double-click on StateMachine nodes in the parent graph. Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
- GetLayerName: also match _StateResult nodes for per-state layer naming - PrefixStateMachineLayerNames: per-state layers get 3-level path prefix (e.g., "AnimGraph > Locomotion > Idle") - TryOpenSubGraph: state nodes navigate to per-state sub-graph layer Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
… matching - StateMachineMetadata: store root node property names per state via StateRootPropNames - AssociateStateMachineNames: capture root prop name from StateRootNodeIndex - BuildStateMachineOverviewLayers: store StateRootNodeName on overview state nodes - TryOpenSubGraph: find per-state layer by root node reference, not name path Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
GetLayerName now distinguishes animation blueprint layers (_Root nodes, found by Name property) from state machine state sub-graphs (_StateResult nodes, found by unique property name from StateRootNodeIndex). This avoids duplicate name collisions when multiple states share the same display name. PrefixStateMachineLayerNames resolves the _StateResult node's Name additional property for the display portion of path-prefixed names. Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
Instead of using connected components (undirected BFS), BuildLayers now groups nodes by their defining root nodes. Each AnimGraphNode_Root defines an animation blueprint layer and each AnimGraphNode_StateResult defines a state machine state sub-graph. For each root, we trace upstream through directed connections to collect all nodes that feed into it. Remaining unassigned nodes fall back to connected-component grouping. Extracted AddLayer helper to reduce duplication. Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
Split BuildLayers into two distinct passes: - Pass 1: AnimGraphNode_Root nodes → graph layers - Pass 2: AnimGraphNode_StateResult nodes → state sub-graphs Previously these were incorrectly mixed in a single loop. Each type defines a fundamentally different concept in UE and must be processed independently. Extracted CollectUpstream helper to share BFS logic. Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
…to avoid scope conflict Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
…t 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>
… unused key Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
…hildProperties order Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
…t circles for same direction Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
…irection circles spread perpendicular Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
…ne line with circles offset along it Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
…ular vector The perpendicular vector was being computed per-connection from its own direction. For B→A connections the perp flipped vs A→B, and when multiplied by the negative perpSide the offsets cancelled out — both lines landed on the same position. Now a stable perpendicular is computed once from the canonical pair direction (nodeA→nodeB centers) and passed to DrawConnectionLine, so perpSide=+1/-1 correctly separates them to opposite sides. Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
PrefixStateMachineLayerNames and BuildStateMachineOverviewLayers only scanned vm.Layers for StateMachine nodes, missing nested SM nodes in vm.StateSubGraphs. This caused overview layers for nested SMs to be named incorrectly (e.g. "AnimGraph > NestedSM" instead of "AnimGraph > OuterSM > StateName > NestedSM"), so double-click from a state sub-graph could not find the matching overview layer. Fix: iteratively discover nested SM nodes in state sub-graphs and also scan StateSubGraphs in BuildStateMachineOverviewLayers. Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
- Tab headers for dynamically-opened sub-graphs now include a close button (×). The initial AnimGraph tab is not closable. - TabControl uses a custom template with horizontal ScrollViewer to keep all tabs in a single line instead of wrapping. - Long tab names are truncated with ellipsis (MaxWidth=200). Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
…e nodes Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
…ph layer Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
- Revert Pass 1 exclusion so SaveCachedPose is naturally collected into the correct _Root layer when reachable via BFS - Add EnforceSaveCachedPoseInAnimBlueprintLayer as a final post-processing step that scans all non-_Root layers (state sub-graphs and fallback layers) and moves any stray SaveCachedPose nodes to the primary _Root layer - Extract RebuildLayerConnections helper for DRY connection rebuilding Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
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>
…am collection in Pass 3 Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
…cases and outermost AnimGraph layer for fallback Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
…cies When sequential Save/Use dependencies exist (e.g., UseCachedPose(A)-> SaveCachedPose(B)->UseCachedPose(B)->SaveCachedPose(C)), the stale BuildLayerLookups built once before the loop caused FindOwnerRootLayer to miss consumers assigned during earlier iterations. The fix iteratively processes SaveCachedPose nodes: each pass rebuilds lookups and resolves nodes whose consumers are already placed, deferring nodes with unresolved consumers. A maxPasses guard prevents infinite loops. Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
When double-clicking a UseCachedPose node to jump to the SaveCachedPose node's tab, the view now centers on the target node. Added CenterOnNode helper that adjusts TranslateTransform to place the node at the viewport center while preserving the current zoom level. Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
…t-parsing Add AnimBlueprint Window
There was a problem hiding this comment.
Pull request overview
Adds an Unreal Engine Animation Blueprint graph preview feature to FModel by extracting AnimBlueprint node/connection data and presenting it in a dedicated WPF viewer window.
Changes:
- Added a new
AnimGraphViewerWPF window (XAML + code-behind) to render AnimBlueprint layers/state machines with zoom/pan, selection, and a properties panel. - Added
AnimGraphViewModelextraction + layering logic to build nodes/connections fromUAnimBlueprintGeneratedClasscooked data. - Integrated AnimBlueprint preview launching into
CUE4ParseViewModelwhen an AnimBlueprint generated class is selected.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
| FModel/Views/AnimGraphViewer.xaml.cs | Implements graph rendering, interaction (tabs, zoom/pan, selection), and properties UI logic. |
| FModel/Views/AnimGraphViewer.xaml | Defines the viewer window layout (toolbar, tabbed graph area, properties panel, status bar). |
| FModel/ViewModels/CUE4ParseViewModel.cs | Hooks AnimBlueprint selection to extract graph data and open the viewer window. |
| FModel/ViewModels/AnimGraphViewModel.cs | Extracts nodes/connections from cooked AnimBlueprint class data and builds layered graph/state-machine representations. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| canvasBorder.AddHandler(UIElement.MouseLeftButtonDownEvent, | ||
| new MouseButtonEventHandler(OnCanvasMouseDown), true); |
There was a problem hiding this comment.
canvasBorder.AddHandler(..., handledEventsToo: true) causes the pan gesture to begin even when a node/transition handled the click (your node/transition handlers set e.Handled = true, but the Border still receives the event). This makes selection interactions accidentally trigger panning. Consider removing handledEventsToo: true, or gate panning to background clicks only (e.g., ignore when e.OriginalSource is a node/transition element).
| canvasBorder.AddHandler(UIElement.MouseLeftButtonDownEvent, | |
| new MouseButtonEventHandler(OnCanvasMouseDown), true); | |
| canvasBorder.MouseLeftButtonDown += OnCanvasMouseDown; |
| // Deselect previous node | ||
| if (_selectedBorder != null) | ||
| { | ||
| _selectedBorder.BorderBrush = new SolidColorBrush(Color.FromRgb(20, 20, 20)); | ||
| _selectedBorder.BorderThickness = new Thickness(1.5); | ||
| } |
There was a problem hiding this comment.
SelectNode always restores the previously selected Border to a hard-coded BorderBrush/BorderThickness (dark + 1.5). For elements whose default border is different (notably the Entry node hitArea, which starts with thickness 0), this leaves a visible outline after deselection. Suggest storing and restoring the original border brush/thickness per selected visual (or special-case visuals like Entry nodes).
| private void CloseTab(System.Windows.Controls.TabItem tabItem) | ||
| { | ||
| if (tabItem.Tag is AnimGraphLayer layer) | ||
| _layerStates.Remove(layer); | ||
|
|
||
| var index = LayerTabControl.Items.IndexOf(tabItem); | ||
| LayerTabControl.Items.Remove(tabItem); | ||
|
|
||
| // Select the previous tab or the first one | ||
| if (LayerTabControl.Items.Count > 0) | ||
| { | ||
| LayerTabControl.SelectedIndex = Math.Max(0, index - 1); | ||
| } | ||
| } |
There was a problem hiding this comment.
Closing a tab removes the layer state from _layerStates, but _currentLayerState is not updated/cleared if the closed tab was the selected one (or if the last tab is closed). Toolbar actions like Fit/Reset can then operate on a stale state. Consider updating _currentLayerState after removal (or clearing it when no tabs remain).
| foreach (var pin in inputPins) | ||
| { | ||
| var defaultVal = string.IsNullOrEmpty(pin.DefaultValue) ? "" : $" = {pin.DefaultValue}"; | ||
| AddPropertyRow(pin.PinName, $"{pin.PinType}{defaultVal}"); | ||
| } |
There was a problem hiding this comment.
In the properties panel, pin rows use AddPropertyRow(pin.PinName, ...). If PinName is empty (which you already handle elsewhere by showing "(unnamed)"), the key column becomes blank here. Consider using the same fallback display name in the properties panel for consistency/readability.
| var shadow = new Border | ||
| { | ||
| Width = NodeWidth, | ||
| Height = nodeHeight, | ||
| CornerRadius = new CornerRadius(NodeCornerRadius), | ||
| Background = Brushes.Black, | ||
| Opacity = 0.4, | ||
| Effect = new BlurEffect { Radius = 8 } | ||
| }; |
There was a problem hiding this comment.
A new BlurEffect instance is created for every node shadow (new BlurEffect { Radius = ... }). With large graphs this can become a rendering/perf bottleneck in WPF. Consider reusing a shared frozen BlurEffect (or simplifying shadows) to reduce allocations and GPU/CPU overhead.
| Application.Current.Dispatcher.Invoke(() => | ||
| { | ||
| Helper.OpenWindow<AnimGraphViewer>("Animation Blueprint Graph Viewer", () => |
There was a problem hiding this comment.
Helper.OpenWindow de-dupes windows by Title. Using the constant title "Animation Blueprint Graph Viewer" means selecting a different AnimBlueprint while the viewer is already open will only focus the existing window and won't display the newly selected graph (since the action won’t run). Consider including the asset/package name in the window title to allow multiple viewers, or reuse the existing window but add an update pathway (e.g., set DataContext / provide a method to load a new AnimGraphViewModel).
| Application.Current.Dispatcher.Invoke(() => | |
| { | |
| Helper.OpenWindow<AnimGraphViewer>("Animation Blueprint Graph Viewer", () => | |
| var windowTitle = $"Animation Blueprint Graph Viewer - {pkg.Name}"; | |
| Application.Current.Dispatcher.Invoke(() => | |
| { | |
| Helper.OpenWindow<AnimGraphViewer>(windowTitle, () => |
| private static bool IsPoseProperty(string name) | ||
| { | ||
| return name.Contains("Pose", StringComparison.OrdinalIgnoreCase) && | ||
| !name.Contains("PoseSnapshot", StringComparison.OrdinalIgnoreCase); | ||
| } |
There was a problem hiding this comment.
IsPoseProperty uses name.Contains("Pose"), which will also match unrelated property names like "Expose…" (since it contains "pose"), causing non-pose properties to be treated as pose inputs and creating bogus pins. Consider tightening this heuristic (e.g., EndsWith("Pose") / known pose-link property names) or, better, add pins only when the underlying property tag is actually a FPoseLink/FComponentSpacePoseLink (same criteria you already use when resolving connections).
…nkIndex properties
本次提交对动画蓝图(AnimGraph)查看器和相关底层数据结构进行了大幅重构和功能增强,主要内容包括: - AnimGraphViewModel 支持全图合成视图、函数层识别、节点元数据追踪、Conduit 节点标记、状态机元数据完善等,图层布局算法更智能,兼容性和可读性显著提升。 - UI 增加图层侧边栏、合成全图优先显示、Conduit 节点高亮、属性面板调试信息等,交互体验更流畅。 - UAnimBlueprintGeneratedClass 及相关类型重构,支持节点、函数、PoseLink、属性等多维度高效访问,底层数据结构更健壮,接口更丰富。
AnimBlueprintWindow.mp4
This pull request introduces a new feature for visualizing Unreal Engine Animation Blueprint graphs within the application. The main changes add support for extracting, displaying, and interacting with animation blueprint graphs in a dedicated viewer window.
Animation Blueprint Graph Visualization:
AnimGraphViewerwindow (AnimGraphViewer.xaml) with a modern UI for visualizing Animation Blueprint graphs, including toolbar controls, a graph canvas with layer tabs, and a node properties panel.UAnimBlueprintGeneratedClassassets inCUE4ParseViewModel.cs, allowing users to open the new graph viewer when an animation blueprint is selected.CUE4ParseViewModel.csto enable the new functionality.