#nullable enable using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using UnityEditor; using UnityEngine; namespace io.github.hatayama.uLoopMCP { /// /// Utility class for capturing any EditorWindow to a Texture2D. /// Uses InternalEditorUtilityBridge to access Unity internal GrabPixels method. /// public static class EditorWindowCaptureUtility { /// /// Find all EditorWindows matching the given name (title bar text). /// /// Window name displayed in the title bar (e.g., "Console", "Inspector") /// Matching mode: exact, prefix, or contains (all case-insensitive) /// Array of matching EditorWindows (empty if none found) public static EditorWindow[] FindWindowsByName(string windowName, WindowMatchMode matchMode = WindowMatchMode.exact) { if (string.IsNullOrEmpty(windowName)) { return Array.Empty(); } List matchingWindows = new List(); EditorWindow[] allWindows = Resources.FindObjectsOfTypeAll(); foreach (EditorWindow window in allWindows) { if (window.titleContent == null) { continue; } string title = window.titleContent.text; bool isMatch = matchMode switch { WindowMatchMode.exact => title.Equals(windowName, StringComparison.OrdinalIgnoreCase), WindowMatchMode.prefix => title.StartsWith(windowName, StringComparison.OrdinalIgnoreCase), WindowMatchMode.contains => title.Contains(windowName, StringComparison.OrdinalIgnoreCase), _ => title.Equals(windowName, StringComparison.OrdinalIgnoreCase) }; if (isMatch) { matchingWindows.Add(window); } } return matchingWindows.ToArray(); } /// /// Capture an EditorWindow to a Texture2D asynchronously. /// Waits for 2 frames after showing the window to ensure it is fully rendered. /// /// The EditorWindow to capture /// Resolution scale (0.1 to 1.0) /// Cancellation token /// Captured Texture2D, or null if capture failed public static async Task CaptureWindowAsync(EditorWindow window, float resolutionScale, CancellationToken ct) { if (window == null) { return null; } window.ShowTab(); await EditorDelay.DelayFrame(2, ct); return CaptureWindowInternal(window, resolutionScale); } /// /// Internal capture logic shared by sync and async methods. /// private static Texture2D? CaptureWindowInternal(EditorWindow window, float resolutionScale) { float scale = EditorGUIUtility.pixelsPerPoint; int width = Mathf.RoundToInt(window.position.width * scale); int height = Mathf.RoundToInt(window.position.height * scale); if (width <= 0 || height <= 0) { return null; } // For Linear color space, disable the sRGB flag to prevent double gamma conversion RenderTextureDescriptor descriptor = new RenderTextureDescriptor(width, height, RenderTextureFormat.ARGB32, 24); if (QualitySettings.activeColorSpace == ColorSpace.Linear) { descriptor.sRGB = false; } RenderTexture rt = RenderTexture.GetTemporary(descriptor); InternalEditorUtilityBridge.CaptureEditorWindow(window, rt); RenderTexture previousActive = RenderTexture.active; RenderTexture.active = rt; Texture2D texture = new Texture2D(width, height, TextureFormat.RGB24, false); texture.ReadPixels(new Rect(0, 0, width, height), 0, 0); texture.Apply(); RenderTexture.active = previousActive; RenderTexture.ReleaseTemporary(rt); if (!Mathf.Approximately(resolutionScale, 1.0f)) { texture = ApplyResolutionScaling(texture, resolutionScale); } return texture; } /// /// Get a list of all open EditorWindow names. /// /// Array of window names public static string[] GetOpenWindowNames() { EditorWindow[] allWindows = Resources.FindObjectsOfTypeAll(); List names = new List(); foreach (EditorWindow window in allWindows) { if (window.titleContent != null && !string.IsNullOrEmpty(window.titleContent.text)) { names.Add(window.titleContent.text); } } return names.ToArray(); } // Captures game rendering by reading GameView's composited RenderTexture (PlayMode only). // Contains all cameras + Screen Space Overlay Canvas, without tab bar or borders. // Coordinate mapping to simulate-mouse: // sim_x = image_x / resolutionScale, sim_y = image_y / resolutionScale + YOffset // where YOffset = Screen.height - RenderTexture.height (returned in the tuple). public static async Task<(Texture2D? texture, int yOffset)> CaptureGameRenderingAsync(float resolutionScale, CancellationToken ct) { Debug.Assert(UnityEditor.EditorApplication.isPlaying, "CaptureGameRenderingAsync requires PlayMode"); // Wait for the game camera to complete at least one full render cycle after any state change await EditorDelay.DelayFrame(2, ct); RenderTexture rt = GameViewBridge.GetRenderTexture(); if (rt == null) { Debug.LogWarning("[EditorWindowCaptureUtility] GameView RenderTexture is not available"); return (null, 0); } int yOffset = (int)Handles.GetMainGameViewSize().y - rt.height; // RenderTexture uses bottom-left origin; flip vertically for standard top-left image format RenderTextureDescriptor flipDescriptor = new RenderTextureDescriptor(rt.width, rt.height, rt.format, 0); if (QualitySettings.activeColorSpace == ColorSpace.Linear) { flipDescriptor.sRGB = false; } RenderTexture flipped = RenderTexture.GetTemporary(flipDescriptor); Graphics.Blit(rt, flipped, new Vector2(1f, -1f), new Vector2(0f, 1f)); RenderTexture previousActive = RenderTexture.active; RenderTexture.active = flipped; Texture2D texture = new Texture2D(rt.width, rt.height, TextureFormat.RGB24, false); texture.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0); texture.Apply(); RenderTexture.active = previousActive; RenderTexture.ReleaseTemporary(flipped); if (!Mathf.Approximately(resolutionScale, 1.0f)) { texture = ApplyResolutionScaling(texture, resolutionScale); } return (texture, yOffset); } private static Texture2D ApplyResolutionScaling(Texture2D originalTexture, float scale) { int newWidth = Mathf.RoundToInt(originalTexture.width * scale); int newHeight = Mathf.RoundToInt(originalTexture.height * scale); Texture2D scaledTexture = new Texture2D(newWidth, newHeight, originalTexture.format, false); RenderTexture rt = RenderTexture.GetTemporary(newWidth, newHeight); Graphics.Blit(originalTexture, rt); RenderTexture.active = rt; scaledTexture.ReadPixels(new Rect(0, 0, newWidth, newHeight), 0, 0); scaledTexture.Apply(); RenderTexture.active = null; RenderTexture.ReleaseTemporary(rt); UnityEngine.Object.DestroyImmediate(originalTexture); return scaledTexture; } } }