Skip to content

Add AnimBlueprint Window for preview AnimBlueprint#656

Open
LoogLong wants to merge 53 commits into4sval:devfrom
LoogPhysics:dev
Open

Add AnimBlueprint Window for preview AnimBlueprint#656
LoogLong wants to merge 53 commits into4sval:devfrom
LoogPhysics:dev

Conversation

@LoogLong
Copy link

@LoogLong LoogLong commented Mar 5, 2026

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:

  • Added a new AnimGraphViewer window (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.
  • Integrated extraction and display logic for UAnimBlueprintGeneratedClass assets in CUE4ParseViewModel.cs, allowing users to open the new graph viewer when an animation blueprint is selected.
  • Registered the required namespace for animation objects in CUE4ParseViewModel.cs to enable the new functionality.

Copilot AI and others added 30 commits February 28, 2026 09:52
…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>
Copilot AI and others added 23 commits March 4, 2026 04:45
Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants