diff --git a/Packages/com.rankupgames.unity-cursor-toolkit/Editor/ApplicationScreenshotCapture.cs b/Packages/com.rankupgames.unity-cursor-toolkit/Editor/ApplicationScreenshotCapture.cs new file mode 100644 index 0000000..a77cb9f --- /dev/null +++ b/Packages/com.rankupgames.unity-cursor-toolkit/Editor/ApplicationScreenshotCapture.cs @@ -0,0 +1,79 @@ +/* + * Author: Miguel A. Lopez + * Company: Rank Up Games LLC + * Project: Unity Cursor Toolkit + * Description: Captures the current application camera view for editor diagnostics. + */ + +#if UNITY_EDITOR +using System; +using System.IO; + +using UnityEngine; + +namespace UnityCursorToolkit +{ + /// + /// Captures the current application view from the main camera to one stable temp file. + /// + internal static class ApplicationScreenshotCapture + { + private const string ScreenshotFileName = "unity-cursor-toolkit-application.png"; + + internal static string ScreenshotPath => Path.Combine(Application.temporaryCachePath, ScreenshotFileName); + + internal static bool TryCapture(out string path, out string error) + { + path = ScreenshotPath; + error = null; + + Camera camera = Camera.main; + if (camera == null) + { + error = "No main camera found"; + return false; + } + + int width = Math.Max(1, Screen.width); + int height = Math.Max(1, Screen.height); + RenderTexture previousTarget = camera.targetTexture; + RenderTexture previousActive = RenderTexture.active; + RenderTexture renderTexture = null; + Texture2D texture = null; + + try + { + renderTexture = RenderTexture.GetTemporary(width, height, 24); + camera.targetTexture = renderTexture; + camera.Render(); + + RenderTexture.active = renderTexture; + texture = new Texture2D(width, height, TextureFormat.RGB24, false); + texture.ReadPixels(new Rect(0, 0, width, height), 0, 0); + texture.Apply(); + File.WriteAllBytes(path, texture.EncodeToPNG()); + return true; + } + catch (Exception exception) + { + error = exception.Message; + return false; + } + finally + { + camera.targetTexture = previousTarget; + RenderTexture.active = previousActive; + if (texture != null) + { + UnityEngine.Object.DestroyImmediate(texture); + } + if (renderTexture != null) + { + RenderTexture.ReleaseTemporary(renderTexture); + } + } + } + } +} + +#endif // UNITY_EDITOR diff --git a/Packages/com.rankupgames.unity-cursor-toolkit/Editor/ApplicationScreenshotCapture.cs.meta b/Packages/com.rankupgames.unity-cursor-toolkit/Editor/ApplicationScreenshotCapture.cs.meta new file mode 100644 index 0000000..aaa8598 --- /dev/null +++ b/Packages/com.rankupgames.unity-cursor-toolkit/Editor/ApplicationScreenshotCapture.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5f0a4b7cdb8445b08c94c335f47c0b1d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.rankupgames.unity-cursor-toolkit/Editor/ConsoleLogCopyTool.cs b/Packages/com.rankupgames.unity-cursor-toolkit/Editor/ConsoleLogCopyTool.cs index 7f8f03c..e8c8a06 100644 --- a/Packages/com.rankupgames.unity-cursor-toolkit/Editor/ConsoleLogCopyTool.cs +++ b/Packages/com.rankupgames.unity-cursor-toolkit/Editor/ConsoleLogCopyTool.cs @@ -36,7 +36,7 @@ static MainToolbarButton CreateMainToolbarCopyLogsButton() { Texture2D _icon = EditorGUIUtility.IconContent("Clipboard").image as Texture2D; return new MainToolbarButton( - new MainToolbarContent(_icon, "Copy profiler session and console transcript context"), + new MainToolbarContent(_icon, "Copy profiler session, console transcript, and application screenshot path"), () => CopyConsoleLogs()); } #endif @@ -49,7 +49,19 @@ static MainToolbarButton CreateMainToolbarCopyLogsButton() [MenuItem("Tools/Unity Cursor Toolkit/Copy Console Logs %#l")] internal static void CopyConsoleLogs() { - GUIUtility.systemCopyBuffer = ProfilerSessionRecorder.BuildClipboardSnapshot(ProfilerSnapshotSettings.Current.IncludeRawFrameArrays); + string clipboardContent = ProfilerSessionRecorder.BuildClipboardSnapshot(ProfilerSnapshotSettings.Current.IncludeRawFrameArrays); + string screenshotPath; + string screenshotError; + if (ApplicationScreenshotCapture.TryCapture(out screenshotPath, out screenshotError)) + { + clipboardContent += "\n\nApplication screenshot: " + screenshotPath; + } + else + { + Debug.LogWarning("(ConsoleLogCopyTool - CopyConsoleLogs) Application screenshot was not captured: " + screenshotError); + } + + GUIUtility.systemCopyBuffer = clipboardContent; Debug.Log("(ConsoleLogCopyTool - CopyConsoleLogs) Copied current profiler session path, console transcript path, and error summary to clipboard"); } } diff --git a/unity-cursor-toolkit/test/run-tests.js b/unity-cursor-toolkit/test/run-tests.js index 404dc3d..2c314f2 100644 --- a/unity-cursor-toolkit/test/run-tests.js +++ b/unity-cursor-toolkit/test/run-tests.js @@ -1226,6 +1226,8 @@ function testUnityProfilerSafetySource() { const hotReloadSource = fs.readFileSync(path.join(editorRoot, 'HotReloadHandler.cs'), 'utf8'); const validationSource = fs.readFileSync(path.join(editorRoot, 'MCP', 'EditorValidationTool.cs'), 'utf8'); const editorControlSource = fs.readFileSync(path.join(editorRoot, 'MCP', 'EditorControlTools.cs'), 'utf8'); + const copySource = fs.readFileSync(path.join(editorRoot, 'ConsoleLogCopyTool.cs'), 'utf8'); + const screenshotSource = fs.readFileSync(path.join(editorRoot, 'ApplicationScreenshotCapture.cs'), 'utf8'); test('background profiler is Play-Mode-only and not reconfigured from Tick', () => { const tickStart = profilerSource.indexOf('private static void Tick()'); @@ -1243,6 +1245,25 @@ function testUnityProfilerSafetySource() { assert.ok(profilerSource.includes('private const long MaxTempSessionBytes = 64L * 1024L * 1024L;')); }); + test('console copy captures a stable application screenshot and preserves fallback copy', () => { + assert.ok(fs.existsSync(path.join(editorRoot, 'ApplicationScreenshotCapture.cs.meta'))); + assert.ok(screenshotSource.includes('Application.temporaryCachePath')); + assert.ok(screenshotSource.includes('private const string ScreenshotFileName = "unity-cursor-toolkit-application.png"')); + assert.ok(screenshotSource.includes('Path.Combine(Application.temporaryCachePath, ScreenshotFileName)')); + assert.ok(screenshotSource.includes('Camera.main')); + assert.ok(screenshotSource.includes('File.WriteAllBytes(path, texture.EncodeToPNG())')); + assert.ok(screenshotSource.includes('RenderTexture.active = previousActive')); + assert.ok(screenshotSource.includes('camera.targetTexture = previousTarget')); + assert.ok(screenshotSource.includes('UnityEngine.Object.DestroyImmediate(texture)')); + assert.ok(screenshotSource.includes('RenderTexture.ReleaseTemporary(renderTexture)')); + assert.ok(screenshotSource.includes('finally')); + assert.ok(!screenshotSource.includes('DateTime.UtcNow.Ticks')); + assert.ok(copySource.includes('ApplicationScreenshotCapture.TryCapture')); + assert.ok(copySource.includes('Application screenshot: ')); + assert.ok(copySource.includes('GUIUtility.systemCopyBuffer = clipboardContent')); + assert.ok(copySource.includes('Debug.LogWarning')); + }); + test('refresh handling avoids duplicate compilation and bounds queued message bytes', () => { assert.ok(hotReloadSource.includes('private const int MAX_QUEUED_MESSAGE_CHARACTERS = 4 * 1024 * 1024;')); assert.ok(hotReloadSource.includes('EnqueueMessage(line);'));