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: