diff --git a/Source/Editor/Windows/AssetReferencesGraphWindow.cs b/Source/Editor/Windows/AssetReferencesGraphWindow.cs new file mode 100644 index 000000000..7bc5559bd --- /dev/null +++ b/Source/Editor/Windows/AssetReferencesGraphWindow.cs @@ -0,0 +1,420 @@ +// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FlaxEditor.Content; +using FlaxEditor.GUI; +using FlaxEditor.Scripting; +using FlaxEditor.Surface; +using FlaxEditor.Surface.Elements; +using FlaxEngine; +using FlaxEngine.GUI; + +namespace FlaxEditor.Windows +{ + /// + /// Editor tool window for references debugging in a virtual dependencies graph. + /// + /// + internal sealed class AssetReferencesGraphWindow : EditorWindow, IVisjectSurfaceOwner + { + private sealed class AssetNode : SurfaceNode + { + public readonly Guid AssetId; + public float LayoutHeight; + public int FirstChild, LastChild; + private int _inputs, _outputs; + + public AssetNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch, Guid assetId) + : base(id, context, nodeArch, groupArch) + { + AssetId = assetId; + + // Init node UI + var picker = new AssetPicker + { + Location = new Vector2(40, 2 * Constants.LayoutOffsetY), + Width = 100.0f, + CanEdit = false, + Parent = this, + }; + // TODO: display some asset info like disk size, memory usage, etc. + var asset = FlaxEngine.Content.LoadAsync(AssetId); + if (asset != null) + { + var path = asset.Path; + picker.SelectedAsset = asset; + Title = System.IO.Path.GetFileNameWithoutExtension(path); + TooltipText = asset.TypeName + '\n' + path; + } + else + { + picker.SelectedID = AssetId; + var assetItem = picker.SelectedItem; + if (assetItem != null) + { + Title = assetItem.ShortName; + TooltipText = assetItem.TypeName + '\n' + assetItem.Path; + } + else + { + Title = AssetId.ToString(); + } + } + ResizeAuto(); + LayoutHeight = Height; + } + + public void ConnectTo(AssetNode target, bool reverse) + { + var outputNode = reverse ? target : this; + var inputNode = reverse ? this : target; + + var output = new OutputBox(outputNode, NodeElementArchetype.Factory.Output(outputNode._outputs, string.Empty, ScriptType.Void, outputNode._outputs, true)); + outputNode.AddElement(output); + outputNode._outputs++; + + var input = new InputBox(inputNode, NodeElementArchetype.Factory.Input(inputNode._inputs, string.Empty, true, ScriptType.Void, inputNode._outputs)); + inputNode.AddElement(input); + inputNode._inputs++; + + output.Connect(input); + } + + public override string ToString() + { + return Title; + } + } + + private static readonly NodeArchetype[] GraphNodes = + { + new NodeArchetype + { + TypeID = 1, + Title = "Asset", + Description = string.Empty, + Flags = NodeFlags.AllGraphs | NodeFlags.NoRemove | NodeFlags.NoSpawnViaGUI | NodeFlags.NoCloseButton, + Size = new Vector2(150, 200), + }, + }; + + private static readonly List GraphGroups = new List + { + new GroupArchetype + { + GroupID = 1, + Name = "Assets", + Color = new Color(118, 82, 186), + Archetypes = GraphNodes + }, + }; + + private sealed class Surface : VisjectSurface + { + public Surface(IVisjectSurfaceOwner owner) + : base(owner) + { + CanEdit = false; + } + + public void Init(List nodes) + { + LockChildrenRecursive(); + Nodes.AddRange(nodes); + foreach (var node in nodes) + { + Context.OnControlLoaded(node); + node.OnSurfaceLoaded(); + Context.OnControlSpawned(node); + } + ShowWholeGraph(); + UnlockChildrenRecursive(); + PerformLayout(); + } + } + + private Guid _assetId; + private Surface _surface; + private Label _loadingLabel; + private CancellationTokenSource _token; + private Task _task; + private const float MarginX = 200; + private const float MarginY = 50; + + // Async task data + private float _progress; + private Dictionary _refs; + private List _nodes; + private HashSet _nodesAssets; + + /// + /// Initializes a new instance of the class. + /// + /// The editor. + /// The asset. + public AssetReferencesGraphWindow(Editor editor, AssetItem assetItem) + : base(editor, false, ScrollBars.None) + { + Title = assetItem.ShortName + " References"; + + _assetId = assetItem.ID; + _surface = new Surface(this) + { + AnchorPreset = AnchorPresets.StretchAll, + Offsets = Margin.Zero, + Parent = this, + }; + _loadingLabel = new Label + { + Text = "Loading...", + AnchorPreset = AnchorPresets.StretchAll, + Offsets = Margin.Zero, + Parent = this, + }; + + // Start async initialization + _token = new CancellationTokenSource(); + _task = Task.Run(Load, _token.Token); + } + + private AssetNode SpawnNode(Guid assetId) + { + _nodesAssets.Add(assetId); + var node = new AssetNode((uint)_nodes.Count + 1, _surface.Context, GraphNodes[0], GraphGroups[0], assetId); + _nodes.Add(node); + return node; + } + + private void SearchRefs(Guid assetId) + { + // Skip assets that never contain references to prevent loading them + if (FlaxEngine.Content.GetAssetInfo(assetId, out var assetInfo) && + (assetInfo.TypeName == "FlaxEngine.Texture" || + assetInfo.TypeName == "FlaxEngine.CubeTexture" || + assetInfo.TypeName == "FlaxEngine.Shader")) + return; + + // Skip if already in cache + if (_refs.ContainsKey(assetId)) + return; + + // Load asset (with cancel support) + //Debug.Log("Searching refs for " + assetInfo.Path); + var obj = FlaxEngine.Object.TryFind(ref assetId); + if (obj is Scene scene) + { + // Special case for scene assets that are also loaded + _refs[assetId] = scene.GetAssetReferences(); + return; + } + var asset = obj as Asset; + if (!asset) + asset = FlaxEngine.Content.LoadAsync(assetId); + if (asset == null || asset.IsVirtual) + return; + while (asset && !asset.IsLoaded) + { + if (_token.IsCancellationRequested) + return; + Thread.Sleep(10); + } + if (!asset || !asset.IsLoaded) + return; + + // Get direct references + _refs[assetId] = asset.GetReferences(); + } + + private void BuildGraph(AssetNode node, int level, bool reverse) + { + if (level == 0) + return; + level--; + Guid[] assetRefs; + if (reverse) + { + // Search for assets that reference this asset + var list = new List(); + foreach (var e in _refs) + { + if (e.Value.Contains(node.AssetId)) + list.Add(e.Key); + } + if (list.Count == 0) + return; + assetRefs = list.ToArray(); + } + else if (!_refs.TryGetValue(node.AssetId, out assetRefs)) + return; + + // Create child nodes + node.FirstChild = _nodes.Count; + for (int i = 0; i < assetRefs.Length; i++) + { + if (_token.IsCancellationRequested) + return; + var assetRef = assetRefs[i]; + + // Check if asset exists + var obj = FlaxEngine.Object.TryFind(ref assetRef); + if (!(obj is Asset) && !(obj is Scene)) + { + var asset = FlaxEngine.Content.LoadAsync(assetRef); + if (asset == null || asset.IsVirtual) + continue; + } + + // Skip nodes that were already added to the graph + if (_nodesAssets.Contains(assetRef)) + continue; + + // Build graph further + var assetRefNode = SpawnNode(assetRef); + node.ConnectTo(assetRefNode, reverse); + } + node.LastChild = _nodes.Count; + + // Build child nodes (recursive) + var childrenCount = node.LastChild - node.FirstChild; + if (childrenCount != 0) + node.ResizeAuto(); + node.LayoutHeight = 0; + for (int i = 0; i < childrenCount; i++) + { + if (_token.IsCancellationRequested) + return; + var assetRefNode = (AssetNode)_nodes[node.FirstChild + i]; + BuildGraph(assetRefNode, level, reverse); + node.LayoutHeight += assetRefNode.LayoutHeight + MarginY; + } + node.LayoutHeight = Mathf.Max(node.Height, node.LayoutHeight - MarginY); + } + + private void ArrangeGraph(AssetNode node, bool reverse) + { + var childrenCount = node.LastChild - node.FirstChild; + if (childrenCount == 0) + return; + + // Place children relative to the node origin but account for the whole sub-tree layout + var origin = new Vector2(reverse ? node.Left : node.Right, node.Center.Y - node.LayoutHeight * 0.5f); + var layoutProgress = 0.0f; + var maxWidth = MarginX; + if (reverse) + { + for (int i = 0; i < childrenCount; i++) + { + var assetRefNode = (AssetNode)_nodes[node.FirstChild + i]; + maxWidth = Mathf.Max(maxWidth, assetRefNode.Width + MarginX); + } + } + for (int i = 0; i < childrenCount; i++) + { + var assetRefNode = (AssetNode)_nodes[node.FirstChild + i]; + if (reverse) + assetRefNode.Location = origin + new Vector2(-maxWidth, layoutProgress + assetRefNode.LayoutHeight * 0.5f - assetRefNode.Height * 0.5f); + else + assetRefNode.Location = origin + new Vector2(MarginX, layoutProgress + assetRefNode.LayoutHeight * 0.5f - assetRefNode.Height * 0.5f); + ArrangeGraph(assetRefNode, reverse); + layoutProgress += assetRefNode.LayoutHeight + MarginY; + } + } + + private void LoadInner() + { + // Build asset references graph + // TODO: add caching of asset refs in editor Cache (eg. asset refs + file write date) + _refs = new Dictionary(); + _progress = 0.0f; + var assets = FlaxEngine.Content.GetAllAssets(); + _progress = 5.0f; + for (var i = 0; i < assets.Length; i++) + { + var id = assets[i]; + if (_token.IsCancellationRequested) + return; + SearchRefs(id); + _progress = ((float)i / assets.Length) * 90.0f + 5.0f; + } + + // Build surface graph + _progress = 95.0f; + _nodes = new List(); + _nodesAssets = new HashSet(); + var searchLevel = 4; // TODO: make it as an option (somewhere in window UI) + // TODO: add option to filter assets by type (eg. show only textures as leaf nodes) + var assetNode = SpawnNode(_assetId); + // TODO: add some outline or tint color to the main node + BuildGraph(assetNode, searchLevel, false); + ArrangeGraph(assetNode, false); + BuildGraph(assetNode, searchLevel, true); + ArrangeGraph(assetNode, true); + if (_token.IsCancellationRequested) + return; + _progress = 100.0f; + + // Update UI + FlaxEngine.Scripting.InvokeOnUpdate(() => _surface.Init(_nodes)); + } + + private void Load() + { + LoadInner(); + FlaxEngine.Scripting.InvokeOnUpdate(() => _loadingLabel.Visible = false); + } + + /// + public override void OnUpdate() + { + base.OnUpdate(); + + if (_loadingLabel.Visible) + { + _loadingLabel.Text = string.Format("Loading {0}%...", (int)_progress); + } + } + + /// + public override void OnDestroy() + { + // Wait for async end + _token.Cancel(); + _task.Wait(); + + base.OnDestroy(); + } + + /// + public string SurfaceName => "References"; + + /// + public byte[] SurfaceData { get; set; } + + /// + public void OnContextCreated(VisjectSurfaceContext context) + { + } + + /// + public Undo Undo => null; + + /// + public void OnSurfaceEditedChanged() + { + } + + /// + public void OnSurfaceGraphEdited() + { + } + + /// + public void OnSurfaceClose() + { + } + } +} diff --git a/Source/Editor/Windows/ContentWindow.ContextMenu.cs b/Source/Editor/Windows/ContentWindow.ContextMenu.cs index 7da8f97f1..def85a432 100644 --- a/Source/Editor/Windows/ContentWindow.ContextMenu.cs +++ b/Source/Editor/Windows/ContentWindow.ContextMenu.cs @@ -100,6 +100,7 @@ namespace FlaxEditor.Windows { cm.AddButton("Copy asset ID", () => Clipboard.Text = JsonSerializer.GetStringID(assetItem.ID)); cm.AddButton("Select actors using this asset", () => Editor.SceneEditing.SelectActorsUsingAsset(assetItem.ID)); + cm.AddButton("Show asset references graph", () => Editor.Windows.Open(new AssetReferencesGraphWindow(Editor, assetItem))); } if (Editor.CanExport(item.Path)) diff --git a/Source/Engine/Level/Scene/Scene.cpp b/Source/Engine/Level/Scene/Scene.cpp index b9b5444ab..b5035f4f8 100644 --- a/Source/Engine/Level/Scene/Scene.cpp +++ b/Source/Engine/Level/Scene/Scene.cpp @@ -122,6 +122,21 @@ String Scene::GetDataFolderPath() const return Globals::ProjectContentFolder / TEXT("SceneData") / GetFilename(); } +Array Scene::GetAssetReferences() const +{ + Array result; + const auto asset = Content::Load(GetID()); + if (asset) + { + asset->GetReferences(result); + } + else + { + // TODO: serialize scene to json and collect refs + } + return result; +} + #endif MeshCollider* Scene::TryGetCsgCollider() diff --git a/Source/Engine/Level/Scene/Scene.h b/Source/Engine/Level/Scene/Scene.h index 474e0907f..534c7e00a 100644 --- a/Source/Engine/Level/Scene/Scene.h +++ b/Source/Engine/Level/Scene/Scene.h @@ -110,6 +110,13 @@ public: /// API_PROPERTY() String GetDataFolderPath() const; + /// + /// Gets the asset references (scene asset). Supported only in Editor. + /// + /// + /// The collection of the asset ids referenced by this asset. + API_FUNCTION() Array GetAssetReferences() const; + #endif private: