ComponentStatsController.cs 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. using AssetBank.Settings;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Diagnostics;
  5. using System.IO;
  6. using System.Linq;
  7. using UnityEditor;
  8. using UnityEngine;
  9. using Newtonsoft.Json.Linq;
  10. namespace AssetBank.Editor.Tools
  11. {
  12. /// <summary>
  13. /// Handles the core logic for scanning the project and managing the component stats.
  14. /// </summary>
  15. public class ComponentStatsController
  16. {
  17. private static readonly string ProjectRoot = Path.GetDirectoryName(Application.dataPath);
  18. public enum ScanSource
  19. {
  20. Scenes,
  21. Prefabs,
  22. Both
  23. }
  24. /// <summary>
  25. /// Scans the project for components and gathers statistics about them.
  26. /// </summary>
  27. /// <param name="scanSource">The type of assets to scan.</param>
  28. /// <returns>A dictionary mapping component names to their statistics.</returns>
  29. public Dictionary<string, ComponentStats> ScanProject(ScanSource scanSource)
  30. {
  31. var settings = ProjectExporterSettings.GetOrCreateSettings();
  32. var ignoredFolders = settings.FoldersToIgnore;
  33. var assetsToScan = new List<string>();
  34. if (scanSource == ScanSource.Scenes || scanSource == ScanSource.Both)
  35. {
  36. assetsToScan.AddRange(AssetDatabase.FindAssets("t:Scene", new[] { "Assets" }));
  37. }
  38. if (scanSource == ScanSource.Prefabs || scanSource == ScanSource.Both)
  39. {
  40. assetsToScan.AddRange(AssetDatabase.FindAssets("t:Prefab", new[] { "Assets" }));
  41. }
  42. var assetPaths = assetsToScan.Distinct().Select(AssetDatabase.GUIDToAssetPath).ToList();
  43. var filteredPaths = assetPaths.Where(path => !ignoredFolders.Any(folder => path.StartsWith(folder, StringComparison.OrdinalIgnoreCase))).ToList();
  44. var tempDir = Path.Combine("Library", "ComponentScanTemp");
  45. if (Directory.Exists(tempDir))
  46. {
  47. Directory.Delete(tempDir, true);
  48. }
  49. Directory.CreateDirectory(tempDir);
  50. var componentStats = new Dictionary<string, ComponentStats>();
  51. try
  52. {
  53. EditorUtility.DisplayProgressBar("Scanning Components", "Preparing to scan...", 0f);
  54. for (int i = 0; i < filteredPaths.Count; i++)
  55. {
  56. var assetPath = filteredPaths[i];
  57. var progress = (float)i / filteredPaths.Count;
  58. EditorUtility.DisplayProgressBar("Scanning Components", $"Processing: {Path.GetFileName(assetPath)}", progress);
  59. var jsonPath = Path.Combine(tempDir, $"{Path.GetFileNameWithoutExtension(assetPath)}_{i}.json");
  60. if (ExecutePythonConversion(assetPath, jsonPath))
  61. {
  62. ParseComponentsFromJson(jsonPath, componentStats);
  63. }
  64. }
  65. }
  66. finally
  67. {
  68. if (Directory.Exists(tempDir))
  69. {
  70. Directory.Delete(tempDir, true);
  71. }
  72. EditorUtility.ClearProgressBar();
  73. }
  74. return componentStats;
  75. }
  76. private string FindPythonExecutable()
  77. {
  78. var venvPath = Path.Combine(ProjectRoot, "venv", "bin", "python3");
  79. if (File.Exists(venvPath))
  80. {
  81. return venvPath;
  82. }
  83. venvPath = Path.Combine(ProjectRoot, "venv", "bin", "python");
  84. if (File.Exists(venvPath))
  85. {
  86. return venvPath;
  87. }
  88. return "python3";
  89. }
  90. private bool ExecutePythonConversion(string assetPath, string jsonOutputPath)
  91. {
  92. var pythonExecutable = FindPythonExecutable();
  93. var scriptGuid = AssetDatabase.FindAssets("convert_scene").FirstOrDefault();
  94. if (string.IsNullOrEmpty(scriptGuid))
  95. {
  96. UnityEngine.Debug.LogError("Conversion script 'convert_scene.py' not found.");
  97. return false;
  98. }
  99. var scriptPath = Path.Combine(ProjectRoot, AssetDatabase.GUIDToAssetPath(scriptGuid));
  100. var absoluteAssetPath = Path.Combine(ProjectRoot, assetPath);
  101. var process = new Process
  102. {
  103. StartInfo = new ProcessStartInfo
  104. {
  105. FileName = pythonExecutable,
  106. Arguments = $"\"{scriptPath}\" \"{absoluteAssetPath}\" \"{jsonOutputPath}\"",
  107. RedirectStandardOutput = true,
  108. RedirectStandardError = true,
  109. UseShellExecute = false,
  110. CreateNoWindow = true
  111. }
  112. };
  113. process.Start();
  114. string error = process.StandardError.ReadToEnd();
  115. process.WaitForExit();
  116. if (process.ExitCode == 0) return true;
  117. UnityEngine.Debug.LogError($"Failed to convert {assetPath}. Error: {error}");
  118. return false;
  119. }
  120. /// <summary>
  121. /// Parses a JSON file and updates the statistics for each component found.
  122. /// </summary>
  123. private void ParseComponentsFromJson(string jsonPath, Dictionary<string, ComponentStats> stats)
  124. {
  125. try
  126. {
  127. var jsonContent = File.ReadAllText(jsonPath);
  128. var jsonArray = JArray.Parse(jsonContent);
  129. foreach (var item in jsonArray)
  130. {
  131. if (item["data"] is not JObject dataObject) continue;
  132. var componentProperty = dataObject.Properties().FirstOrDefault();
  133. if (componentProperty == null) continue;
  134. var componentName = componentProperty.Name;
  135. if (!stats.ContainsKey(componentName))
  136. {
  137. stats[componentName] = new ComponentStats { Name = componentName };
  138. }
  139. var currentStats = stats[componentName];
  140. currentStats.TotalOccurrences++;
  141. // Pass the *value* of the component property to the counter
  142. var propertyCount = CountProperties(componentProperty.Value);
  143. currentStats.TotalProperties += propertyCount;
  144. if (propertyCount > currentStats.MaxProperties)
  145. {
  146. currentStats.MaxProperties = propertyCount;
  147. }
  148. }
  149. }
  150. catch (Exception e)
  151. {
  152. UnityEngine.Debug.LogError($"Failed to parse JSON file {jsonPath}. Error: {e.Message}");
  153. }
  154. }
  155. /// <summary>
  156. /// Recursively counts the number of properties in a JToken.
  157. /// </summary>
  158. private int CountProperties(JToken token)
  159. {
  160. int count = 0;
  161. if (token is JObject obj)
  162. {
  163. foreach (var prop in obj.Properties())
  164. {
  165. count++; // Count the key itself
  166. count += CountProperties(prop.Value); // Recursively count properties in the value
  167. }
  168. }
  169. else if (token is JArray arr)
  170. {
  171. foreach (var item in arr)
  172. {
  173. count += CountProperties(item);
  174. }
  175. }
  176. return count;
  177. }
  178. public void UpdateAndSaveScanResults(Dictionary<string, ComponentStats> stats)
  179. {
  180. var settings = ComponentStatsSettings.GetOrCreateSettings();
  181. settings.ComponentStats.Clear();
  182. settings.ComponentStats.AddRange(stats.Values.OrderBy(s => s.Name));
  183. settings.LastScanTime = DateTime.Now.ToString("g"); // "g" for general short date/time
  184. EditorUtility.SetDirty(settings);
  185. AssetDatabase.SaveAssets();
  186. }
  187. public void SaveStats(Dictionary<string, ComponentStats> stats)
  188. {
  189. var settings = ComponentStatsSettings.GetOrCreateSettings();
  190. settings.ComponentStats.Clear();
  191. settings.ComponentStats.AddRange(stats.Values.OrderBy(s => s.Name));
  192. EditorUtility.SetDirty(settings);
  193. AssetDatabase.SaveAssets();
  194. EditorUtility.DisplayDialog("Success", $"Saved {settings.ComponentStats.Count} component stats.", "OK");
  195. }
  196. }
  197. }