// Copyright (c) 2012-2023 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 Float2(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 as AssetItem; 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 Float2(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, SurfaceNodeActions.Load); node.OnSurfaceLoaded(SurfaceNodeActions.Load); Context.OnControlSpawned(node, SurfaceNodeActions.Load); } 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 && !asset.LastLoadFailed) { 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 Float2(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 Float2(-maxWidth, layoutProgress + assetRefNode.LayoutHeight * 0.5f - assetRefNode.Height * 0.5f); else assetRefNode.Location = origin + new Float2(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); _loadingLabel.Visible = false; }); } private void Load() { try { LoadInner(); } catch (Exception ex) { Editor.LogWarning(ex); Close(); } } /// 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 Asset SurfaceAsset => null; /// public string SurfaceName => "References"; /// public byte[] SurfaceData { get; set; } /// public VisjectSurfaceContext ParentContext => null; /// public void OnContextCreated(VisjectSurfaceContext context) { } /// public Undo Undo => null; /// public void OnSurfaceEditedChanged() { } /// public void OnSurfaceGraphEdited() { } /// public void OnSurfaceClose() { } } }