From bd366ea12fdad7905c6fe893994f2d7397e52850 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 09:52:13 +0000 Subject: [PATCH 01/52] Initial plan From ba9bb323a385ce648ad32f9b21c12d9b34efcbff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 10:05:39 +0000 Subject: [PATCH 02/52] Add animation blueprint graph viewer with node/connection extraction and visualization Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com> --- FModel/ViewModels/AnimGraphViewModel.cs | 136 ++++++++ FModel/ViewModels/CUE4ParseViewModel.cs | 16 + FModel/Views/AnimGraphViewer.xaml | 60 ++++ FModel/Views/AnimGraphViewer.xaml.cs | 431 ++++++++++++++++++++++++ 4 files changed, 643 insertions(+) create mode 100644 FModel/ViewModels/AnimGraphViewModel.cs create mode 100644 FModel/Views/AnimGraphViewer.xaml create mode 100644 FModel/Views/AnimGraphViewer.xaml.cs diff --git a/FModel/ViewModels/AnimGraphViewModel.cs b/FModel/ViewModels/AnimGraphViewModel.cs new file mode 100644 index 00000000..9f4cf23a --- /dev/null +++ b/FModel/ViewModels/AnimGraphViewModel.cs @@ -0,0 +1,136 @@ +using System.Collections.Generic; +using System.Linq; +using CUE4Parse.UE4.Assets; +using CUE4Parse.UE4.Assets.Exports; +using CUE4Parse.UE4.Assets.Exports.EdGraph; +using CUE4Parse.UE4.Objects.Engine.EdGraph; + +namespace FModel.ViewModels; + +public class AnimGraphNode +{ + public string Name { get; set; } = string.Empty; + public string ExportType { get; set; } = string.Empty; + public string NodeComment { get; set; } = string.Empty; + public int NodePosX { get; set; } + public int NodePosY { get; set; } + public List Pins { get; set; } = []; + + public override string ToString() => $"{ExportType} ({Name})"; +} + +public class AnimGraphPin +{ + public string PinName { get; set; } = string.Empty; + public EEdGraphPinDirection Direction { get; set; } + public string PinType { get; set; } = string.Empty; + public string DefaultValue { get; set; } = string.Empty; + public AnimGraphNode OwnerNode { get; set; } = null!; +} + +public class AnimGraphConnection +{ + public AnimGraphNode SourceNode { get; set; } = null!; + public string SourcePinName { get; set; } = string.Empty; + public AnimGraphNode TargetNode { get; set; } = null!; + public string TargetPinName { get; set; } = string.Empty; +} + +public class AnimGraphViewModel +{ + public string PackageName { get; set; } = string.Empty; + public List Nodes { get; } = []; + public List Connections { get; } = []; + + public static AnimGraphViewModel ExtractFromPackage(IPackage package) + { + var vm = new AnimGraphViewModel { PackageName = package.Name }; + + var allExports = package.GetExports().ToList(); + + // Map from UObject to AnimGraphNode for connection resolution + var nodeMap = new Dictionary(); + + // First pass: extract all graph nodes + foreach (var export in allExports) + { + if (export is not UEdGraphNode graphNode) continue; + + var node = new AnimGraphNode + { + Name = graphNode.Name, + ExportType = graphNode.ExportType, + NodePosX = graphNode.GetOrDefault("NodePosX", 0), + NodePosY = graphNode.GetOrDefault("NodePosY", 0), + NodeComment = graphNode.GetOrDefault("NodeComment", string.Empty) + }; + + // Extract pins + foreach (var pinRef in graphNode.Pins) + { + if (pinRef is not UEdGraphPin pin) continue; + + var graphPin = new AnimGraphPin + { + PinName = pin.PinName.Text ?? string.Empty, + Direction = pin.Direction, + PinType = pin.PinType?.PinCategory.Text ?? string.Empty, + DefaultValue = pin.DefaultValue ?? string.Empty, + OwnerNode = node + }; + node.Pins.Add(graphPin); + } + + nodeMap[graphNode] = node; + vm.Nodes.Add(node); + } + + // Second pass: resolve connections via LinkedTo references + foreach (var export in allExports) + { + if (export is not UEdGraphNode graphNode) continue; + if (!nodeMap.TryGetValue(graphNode, out var sourceNode)) continue; + + foreach (var pinRef in graphNode.Pins) + { + if (pinRef is not UEdGraphPin pin) continue; + + foreach (var linkedRef in pin.LinkedTo) + { + if (linkedRef == null) continue; + + // Resolve the owning node of the linked pin + var linkedNodeObj = linkedRef.OwningNode.ResolvedObject?.Object?.Value; + if (linkedNodeObj == null || !nodeMap.TryGetValue(linkedNodeObj, out var targetNode)) continue; + + // Only add connection from output to input to avoid duplicates + if (pin.Direction != EEdGraphPinDirection.EGPD_Output) continue; + + // Try to find the target pin name + var targetPinName = string.Empty; + if (linkedNodeObj is UEdGraphNode linkedGraphNode) + { + foreach (var tp in linkedGraphNode.Pins) + { + if (tp is UEdGraphPin targetPin && targetPin.PinId == linkedRef.PinId) + { + targetPinName = targetPin.PinName.Text ?? string.Empty; + break; + } + } + } + + vm.Connections.Add(new AnimGraphConnection + { + SourceNode = sourceNode, + SourcePinName = pin.PinName.Text ?? string.Empty, + TargetNode = targetNode, + TargetPinName = targetPinName + }); + } + } + } + + return vm; + } +} diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index defb2bf0..48b40a93 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -49,6 +49,7 @@ using CUE4Parse.UE4.IO; using CUE4Parse.UE4.Localization; using CUE4Parse.UE4.Objects.Core.Serialization; using CUE4Parse.UE4.Objects.Engine; +using CUE4Parse.UE4.Objects.Engine.Animation; using CUE4Parse.UE4.Objects.UObject; using CUE4Parse.UE4.Objects.UObject.Editor; using CUE4Parse.UE4.Oodle.Objects; @@ -1265,6 +1266,21 @@ public class CUE4ParseViewModel : ViewModel return false; } + case UAnimBlueprintGeneratedClass when isNone: + { + var graphVm = AnimGraphViewModel.ExtractFromPackage(pkg); + if (graphVm.Nodes.Count > 0) + { + Application.Current.Dispatcher.Invoke(() => + { + Helper.OpenWindow("Animation Blueprint Graph Viewer", () => + { + new AnimGraphViewer(graphVm).Show(); + }); + }); + } + return true; + } case UWorld when isNone && UserSettings.Default.PreviewWorlds: case UBlueprintGeneratedClass when isNone && UserSettings.Default.PreviewWorlds && TabControl.SelectedTab.ParentExportType switch { diff --git a/FModel/Views/AnimGraphViewer.xaml b/FModel/Views/AnimGraphViewer.xaml new file mode 100644 index 00000000..55aec8d7 --- /dev/null +++ b/FModel/Views/AnimGraphViewer.xaml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + +