using System; using System.Collections.Generic; using System.IO; using System.Reflection; using System.Runtime.ExceptionServices; using System.Threading; using MCPForUnity.Runtime.Helpers; using UnityEditor; using UnityEngine; namespace MCPForUnity.Editor.Helpers { /// /// Captures the pixels currently displayed in an editor window viewport. /// Uses the editor view's own pixel grab path instead of re-rendering through a Camera. /// internal static class EditorWindowScreenshotUtility { // Keep capture synchronous so callers can immediately return the screenshot payload. // The short sleep gives Unity a chance to flush repaint work before GrabPixels reads the viewport. private const int RepaintSettlingDelayMs = 75; private static readonly HashSet WindowsReservedNames = new HashSet(StringComparer.OrdinalIgnoreCase) { "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", }; /// /// Captures the active Scene View viewport to a PNG asset. /// /// Scene View window to capture. /// Optional file name, defaulting to a timestamped PNG. /// /// Preserved in the result for API consistency, but Scene View capture always uses the current viewport resolution. /// /// If true, appends a suffix instead of overwriting an existing file. /// If true, includes a base64 PNG in the returned result. /// Maximum edge length for the inline image payload. /// Captured viewport width in pixels. /// Captured viewport height in pixels. public static ScreenshotCaptureResult CaptureSceneViewViewportToProject( SceneView sceneView, string fileName, int superSize, bool ensureUniqueFileName, bool includeImage, int maxResolution, out int viewportWidth, out int viewportHeight, string folderOverride = null) { if (sceneView == null) throw new ArgumentNullException(nameof(sceneView)); int effectiveSuperSize = NormalizeSceneViewSuperSize(superSize); FocusAndRepaint(sceneView); Rect viewportRectPixels = GetSceneViewViewportPixelRect(sceneView); viewportWidth = Mathf.RoundToInt(viewportRectPixels.width); viewportHeight = Mathf.RoundToInt(viewportRectPixels.height); if (viewportWidth <= 0 || viewportHeight <= 0) throw new InvalidOperationException("Captured Scene view viewport is empty."); Texture2D captured = null; Texture2D downscaled = null; try { captured = CaptureViewRect(sceneView, viewportRectPixels); var result = PrepareCaptureResult(fileName, effectiveSuperSize, ensureUniqueFileName, folderOverride); byte[] png = captured.EncodeToPNG(); File.WriteAllBytes(result.FullPath, png); if (includeImage) { int targetMax = maxResolution > 0 ? maxResolution : 640; string imageBase64; int imageWidth; int imageHeight; if (captured.width > targetMax || captured.height > targetMax) { downscaled = ScreenshotUtility.DownscaleTexture(captured, targetMax); imageBase64 = Convert.ToBase64String(downscaled.EncodeToPNG()); imageWidth = downscaled.width; imageHeight = downscaled.height; } else { imageBase64 = Convert.ToBase64String(png); imageWidth = captured.width; imageHeight = captured.height; } return new ScreenshotCaptureResult( result.FullPath, result.ProjectRelativePath, result.SuperSize, false, imageBase64, imageWidth, imageHeight); } return result; } finally { DestroyTexture(captured); DestroyTexture(downscaled); } } private static void FocusAndRepaint(SceneView sceneView) { try { sceneView.Focus(); } catch (Exception ex) { McpLog.Debug($"[EditorWindowScreenshotUtility] SceneView focus failed: {ex.Message}"); } try { sceneView.Repaint(); InvokeMethodIfExists(sceneView, "RepaintImmediately"); SceneView.RepaintAll(); UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); EditorApplication.QueuePlayerLoopUpdate(); Thread.Sleep(RepaintSettlingDelayMs); } catch (Exception ex) { McpLog.Debug($"[EditorWindowScreenshotUtility] SceneView repaint failed: {ex.Message}"); } } private static Rect GetSceneViewViewportPixelRect(SceneView sceneView) { float pixelsPerPoint = EditorGUIUtility.pixelsPerPoint; Rect viewportLocalPoints = GetViewportLocalRectPoints(sceneView, pixelsPerPoint); if (viewportLocalPoints.width <= 0f || viewportLocalPoints.height <= 0f) throw new InvalidOperationException("Failed to resolve Scene view viewport rect."); return new Rect( Mathf.Round(viewportLocalPoints.x * pixelsPerPoint), Mathf.Round(viewportLocalPoints.y * pixelsPerPoint), Mathf.Round(viewportLocalPoints.width * pixelsPerPoint), Mathf.Round(viewportLocalPoints.height * pixelsPerPoint)); } private static Rect GetViewportLocalRectPoints(SceneView sceneView, float pixelsPerPoint) { Rect? cameraViewport = GetRectProperty(sceneView, "cameraViewport"); if (cameraViewport.HasValue && cameraViewport.Value.width > 0f && cameraViewport.Value.height > 0f) { return cameraViewport.Value; } Camera camera = sceneView.camera; if (camera == null) throw new InvalidOperationException("Active Scene View has no camera to derive viewport size from."); float viewportWidth = camera.pixelWidth / Mathf.Max(0.0001f, pixelsPerPoint); float viewportHeight = camera.pixelHeight / Mathf.Max(0.0001f, pixelsPerPoint); Rect windowRect = sceneView.position; return new Rect( 0f, Mathf.Max(0f, windowRect.height - viewportHeight), Mathf.Min(windowRect.width, viewportWidth), Mathf.Min(windowRect.height, viewportHeight)); } private static Texture2D CaptureViewRect(SceneView sceneView, Rect viewportRectPixels) { object hostView = GetHostView(sceneView); if (hostView == null) throw new InvalidOperationException("Failed to resolve Scene view host view."); // GrabPixels is an internal extern on GUIView (parent of HostView), present since at least Unity 2021.1. // See: UnityCsReference/Editor/Mono/GUIView.bindings.cs — `internal extern void GrabPixels(RenderTexture, Rect)` // If Unity removes this, the MissingMethodException below keeps the failure explicit. MethodInfo grabPixels = hostView.GetType().GetMethod( "GrabPixels", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new[] { typeof(RenderTexture), typeof(Rect) }, null); if (grabPixels == null) throw new MissingMethodException($"{hostView.GetType().FullName}.GrabPixels(RenderTexture, Rect)"); int width = Mathf.RoundToInt(viewportRectPixels.width); int height = Mathf.RoundToInt(viewportRectPixels.height); RenderTexture rt = null; RenderTexture previousActive = RenderTexture.active; try { rt = new RenderTexture(width, height, 0, RenderTextureFormat.ARGB32) { antiAliasing = 1, filterMode = FilterMode.Bilinear, hideFlags = HideFlags.HideAndDontSave, }; rt.Create(); grabPixels.Invoke(hostView, new object[] { rt, viewportRectPixels }); RenderTexture.active = rt; var texture = new Texture2D(width, height, TextureFormat.RGBA32, false); texture.ReadPixels(new Rect(0, 0, width, height), 0, 0); texture.Apply(); FlipTextureVertically(texture); return texture; } catch (TargetInvocationException ex) { ExceptionDispatchInfo.Capture(ex.InnerException ?? ex).Throw(); throw; } finally { RenderTexture.active = previousActive; if (rt != null) { rt.Release(); UnityEngine.Object.DestroyImmediate(rt); } } } private static object GetHostView(EditorWindow window) { if (window == null) return null; Type windowType = typeof(EditorWindow); FieldInfo parentField = windowType.GetField("m_Parent", BindingFlags.Instance | BindingFlags.NonPublic); if (parentField != null) { object parent = parentField.GetValue(window); if (parent != null) return parent; } PropertyInfo hostViewProperty = windowType.GetProperty("hostView", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); return hostViewProperty?.GetValue(window, null); } private static Rect? GetRectProperty(object instance, string propertyName) { if (instance == null) return null; Type type = instance.GetType(); PropertyInfo property = type.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (property == null || property.PropertyType != typeof(Rect)) return null; try { return (Rect)property.GetValue(instance, null); } catch (Exception ex) { McpLog.Debug($"[EditorWindowScreenshotUtility] Failed to read rect property '{propertyName}': {ex.Message}"); return null; } } private static void InvokeMethodIfExists(object instance, string methodName) { if (instance == null) return; MethodInfo method = instance.GetType().GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (method == null || method.GetParameters().Length != 0) return; try { method.Invoke(instance, null); } catch (Exception ex) { McpLog.Debug($"[EditorWindowScreenshotUtility] Best-effort invoke of '{methodName}' failed: {ex.Message}"); } } private static void FlipTextureVertically(Texture2D texture) { if (texture == null) return; int width = texture.width; int height = texture.height; Color32[] pixels = texture.GetPixels32(); var temp = new Color32[width]; for (int y = 0; y < height / 2; y++) { int topRow = y * width; int bottomRow = (height - 1 - y) * width; Array.Copy(pixels, topRow, temp, 0, width); Array.Copy(pixels, bottomRow, pixels, topRow, width); Array.Copy(temp, 0, pixels, bottomRow, width); } texture.SetPixels32(pixels); texture.Apply(); } private static ScreenshotCaptureResult PrepareCaptureResult(string fileName, int superSize, bool ensureUniqueFileName, string folderOverride) { int size = Mathf.Max(1, superSize); string resolvedName = BuildFileName(fileName); string folder = ScreenshotUtility.ResolveFolderAbsolute(folderOverride); Directory.CreateDirectory(folder); string fullPath = Path.Combine(folder, resolvedName); if (ensureUniqueFileName) { fullPath = EnsureUnique(fullPath); } string normalizedFullPath = fullPath.Replace('\\', '/'); string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")).Replace('\\', '/'); string normalizedRoot = projectRoot.EndsWith("/") ? projectRoot : projectRoot + "/"; string projectRelativePath = normalizedFullPath.StartsWith(normalizedRoot, StringComparison.OrdinalIgnoreCase) ? normalizedFullPath.Substring(normalizedRoot.Length) : normalizedFullPath; return new ScreenshotCaptureResult(normalizedFullPath, projectRelativePath, size, false); } private static string BuildFileName(string fileName) { string baseName = string.IsNullOrWhiteSpace(fileName) ? $"screenshot-{DateTime.Now:yyyyMMdd-HHmmss}.png" : SanitizeFileName(fileName); if (!baseName.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) baseName += ".png"; return baseName; } private static int NormalizeSceneViewSuperSize(int superSize) { if (superSize > 1) { McpLog.Warn("[EditorWindowScreenshotUtility] Scene View capture ignores superSize and uses the displayed viewport resolution."); return 1; } return Mathf.Max(1, superSize); } private static string SanitizeFileName(string fileName) { string trimmed = (fileName ?? string.Empty).Trim(); if (string.IsNullOrEmpty(trimmed)) return $"screenshot-{DateTime.Now:yyyyMMdd-HHmmss}.png"; string candidate = trimmed; string normalizedSeparators = candidate.Replace('\\', '/'); if (Path.IsPathRooted(candidate) || normalizedSeparators.Contains("/") || normalizedSeparators.Contains("..")) { string[] pathParts = normalizedSeparators.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); candidate = pathParts.Length > 0 ? pathParts[pathParts.Length - 1] : string.Empty; } if (string.IsNullOrWhiteSpace(candidate) || candidate == "." || candidate == "..") candidate = $"screenshot-{DateTime.Now:yyyyMMdd-HHmmss}.png"; char[] invalidChars = Path.GetInvalidFileNameChars(); foreach (char invalidChar in invalidChars) { candidate = candidate.Replace(invalidChar, '_'); } string extension = Path.GetExtension(candidate); string stem = Path.GetFileNameWithoutExtension(candidate); extension = extension.TrimEnd(' ', '.'); stem = stem.TrimEnd(' ', '.'); if (WindowsReservedNames.Contains(stem)) { candidate = $"_{stem}{extension}"; } return candidate; } private static string EnsureUnique(string fullPath) { if (!File.Exists(fullPath)) return fullPath; string directory = Path.GetDirectoryName(fullPath) ?? string.Empty; string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fullPath); string extension = Path.GetExtension(fullPath); for (int i = 1; i < 10000; i++) { string candidate = Path.Combine(directory, $"{fileNameWithoutExtension}-{i}{extension}"); if (!File.Exists(candidate)) return candidate; } throw new IOException($"Could not generate a unique screenshot filename for '{fullPath}'."); } private static void DestroyTexture(Texture2D texture) { if (texture == null) return; UnityEngine.Object.DestroyImmediate(texture); } } }