diff --git a/Editor/Camera.meta b/Editor/Camera.meta new file mode 100644 index 0000000..37daff3 --- /dev/null +++ b/Editor/Camera.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 79a1c58178f14b939130bfdec05b2caf +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Camera/ResolutionScreenshotMenu.cs b/Editor/Camera/ResolutionScreenshotMenu.cs new file mode 100644 index 0000000..0104194 --- /dev/null +++ b/Editor/Camera/ResolutionScreenshotMenu.cs @@ -0,0 +1,350 @@ +using System; +using System.IO; +using UnityEditor; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace Streamingle.Editor.Utilities +{ + public static class ResolutionScreenshotMenu + { + private const string MenuRoot = "Tools/Streamingle/Capture/Screenshots/Resolution/"; + private const int MenuPriority = 500; + private const string SaveFolderPrefsKey = "ScreenshotTool_SaveFolder"; + private const string PrefixPrefsKey = "ScreenshotTool_Prefix"; + + [MenuItem(MenuRoot + "Custom...", false, MenuPriority)] + private static void ShowCustomWindow() + { + ResolutionScreenshotWindow.ShowWindow(); + } + + [MenuItem(MenuRoot + "Capture 1280x720 (HD)", false, MenuPriority + 10)] + private static void Capture1280x720() + { + Capture(1280, 720); + } + + [MenuItem(MenuRoot + "Capture 1920x1080 (FHD)", false, MenuPriority + 11)] + private static void Capture1920x1080() + { + Capture(1920, 1080); + } + + [MenuItem(MenuRoot + "Capture 2560x1440 (QHD)", false, MenuPriority + 12)] + private static void Capture2560x1440() + { + Capture(2560, 1440); + } + + [MenuItem(MenuRoot + "Capture 3840x2160 (4K)", false, MenuPriority + 13)] + private static void Capture3840x2160() + { + Capture(3840, 2160); + } + + [MenuItem(MenuRoot + "Capture 1080x1920 (Portrait)", false, MenuPriority + 20)] + private static void Capture1080x1920() + { + Capture(1080, 1920); + } + + [MenuItem(MenuRoot + "Capture 1080x1080 (Square)", false, MenuPriority + 21)] + private static void Capture1080x1080() + { + Capture(1080, 1080); + } + + [MenuItem(MenuRoot + "Open Screenshot Folder", false, MenuPriority + 100)] + private static void OpenScreenshotFolder() + { + string folder = GetScreenshotFolder(); + Directory.CreateDirectory(folder); + EditorUtility.RevealInFinder(folder); + } + + internal static void Capture(int width, int height, Camera camera = null, bool transparentBackground = false, bool revealAfterCapture = false) + { + if (width <= 0 || height <= 0) + { + EditorUtility.DisplayDialog("Screenshot", "Width and height must be greater than zero.", "OK"); + return; + } + + camera ??= FindDefaultCaptureCamera(); + if (camera == null) + { + EditorUtility.DisplayDialog( + "Screenshot", + "No capture camera found. Select a Camera in the hierarchy or add a MainCamera-tagged camera.", + "OK"); + return; + } + + string folder = GetScreenshotFolder(); + Directory.CreateDirectory(folder); + + string filePath = BuildFilePath(folder, camera, width, height); + try + { + RenderCameraToPng(camera, width, height, filePath, transparentBackground); + Debug.Log($"[ResolutionScreenshot] Saved {width}x{height} screenshot: {filePath}"); + + if (IsPathUnderAssets(filePath)) + { + AssetDatabase.Refresh(); + } + + if (revealAfterCapture) + { + EditorUtility.RevealInFinder(filePath); + } + } + catch (Exception ex) + { + Debug.LogError($"[ResolutionScreenshot] Capture failed: {ex.Message}"); + EditorUtility.DisplayDialog("Screenshot Failed", ex.Message, "OK"); + } + } + + internal static Camera FindDefaultCaptureCamera() + { + var selectedObject = Selection.activeGameObject; + if (selectedObject != null) + { + var selectedCamera = selectedObject.GetComponent(); + if (IsUsableSceneCamera(selectedCamera)) + { + return selectedCamera; + } + + selectedCamera = selectedObject.GetComponentInChildren(true); + if (IsUsableSceneCamera(selectedCamera)) + { + return selectedCamera; + } + } + + var mainCamera = Camera.main; + if (IsUsableSceneCamera(mainCamera)) + { + return mainCamera; + } + + Camera bestCamera = null; + var cameras = UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None); + foreach (var sceneCamera in cameras) + { + if (!IsUsableSceneCamera(sceneCamera)) + { + continue; + } + + if (bestCamera == null || sceneCamera.depth > bestCamera.depth) + { + bestCamera = sceneCamera; + } + } + + if (bestCamera != null) + { + return bestCamera; + } + + return SceneView.lastActiveSceneView != null ? SceneView.lastActiveSceneView.camera : null; + } + + internal static string GetScreenshotFolder() + { + string defaultFolder = Path.GetFullPath(Path.Combine(Application.dataPath, "..", "Screenshots")); + string savedFolder = EditorPrefs.GetString(SaveFolderPrefsKey, defaultFolder); + return string.IsNullOrWhiteSpace(savedFolder) ? defaultFolder : Path.GetFullPath(savedFolder); + } + + private static void RenderCameraToPng(Camera camera, int width, int height, string filePath, bool transparentBackground) + { + RenderTexture previousTargetTexture = camera.targetTexture; + RenderTexture previousActiveTexture = RenderTexture.active; + CameraClearFlags previousClearFlags = camera.clearFlags; + Color previousBackgroundColor = camera.backgroundColor; + + RenderTexture renderTexture = null; + Texture2D texture = null; + + try + { + int antiAliasing = Mathf.Max(1, QualitySettings.antiAliasing); + renderTexture = RenderTexture.GetTemporary( + width, + height, + 24, + RenderTextureFormat.ARGB32, + RenderTextureReadWrite.sRGB, + antiAliasing); + + camera.targetTexture = renderTexture; + + if (transparentBackground) + { + camera.clearFlags = CameraClearFlags.SolidColor; + camera.backgroundColor = new Color(0f, 0f, 0f, 0f); + } + + camera.Render(); + + TextureFormat textureFormat = transparentBackground ? TextureFormat.ARGB32 : TextureFormat.RGB24; + texture = new Texture2D(width, height, textureFormat, false); + + RenderTexture.active = renderTexture; + texture.ReadPixels(new Rect(0, 0, width, height), 0, 0); + texture.Apply(); + + File.WriteAllBytes(filePath, texture.EncodeToPNG()); + } + finally + { + camera.targetTexture = previousTargetTexture; + camera.clearFlags = previousClearFlags; + camera.backgroundColor = previousBackgroundColor; + RenderTexture.active = previousActiveTexture; + + if (texture != null) + { + UnityEngine.Object.DestroyImmediate(texture); + } + + if (renderTexture != null) + { + RenderTexture.ReleaseTemporary(renderTexture); + } + } + } + + private static bool IsUsableSceneCamera(Camera camera) + { + return camera != null + && camera.isActiveAndEnabled + && camera.gameObject.scene.IsValid() + && !EditorUtility.IsPersistent(camera); + } + + private static string BuildFilePath(string folder, Camera camera, int width, int height) + { + string prefix = SanitizeFilePart(EditorPrefs.GetString(PrefixPrefsKey, "Screenshot")); + string sceneName = SanitizeFilePart(GetSceneName(camera)); + string cameraName = SanitizeFilePart(camera.name); + string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmssfff"); + string fileName = $"{prefix}_{sceneName}_{cameraName}_{width}x{height}_{timestamp}.png"; + return Path.Combine(folder, fileName); + } + + private static string GetSceneName(Camera camera) + { + Scene scene = camera.gameObject.scene; + if (scene.IsValid() && !string.IsNullOrWhiteSpace(scene.name)) + { + return scene.name; + } + + Scene activeScene = SceneManager.GetActiveScene(); + return !string.IsNullOrWhiteSpace(activeScene.name) ? activeScene.name : "UntitledScene"; + } + + private static string SanitizeFilePart(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return "Screenshot"; + } + + foreach (char invalidChar in Path.GetInvalidFileNameChars()) + { + value = value.Replace(invalidChar, '_'); + } + + return value.Trim(); + } + + private static bool IsPathUnderAssets(string filePath) + { + string fullPath = Path.GetFullPath(filePath).Replace('\\', '/'); + string assetsPath = Path.GetFullPath(Application.dataPath).Replace('\\', '/'); + return fullPath.StartsWith(assetsPath + "/", StringComparison.OrdinalIgnoreCase); + } + } + + public sealed class ResolutionScreenshotWindow : EditorWindow + { + private Camera captureCamera; + private int width = 1920; + private int height = 1080; + private bool transparentBackground; + private bool revealAfterCapture = true; + + public static void ShowWindow() + { + var window = GetWindow("Resolution Screenshot"); + window.minSize = new Vector2(360f, 230f); + window.Show(); + } + + private void OnEnable() + { + captureCamera = ResolutionScreenshotMenu.FindDefaultCaptureCamera(); + } + + private void OnGUI() + { + EditorGUILayout.Space(8f); + EditorGUILayout.LabelField("Resolution Screenshot", EditorStyles.boldLabel); + EditorGUILayout.Space(4f); + + captureCamera = (Camera)EditorGUILayout.ObjectField("Camera", captureCamera, typeof(Camera), true); + if (captureCamera == null) + { + EditorGUILayout.HelpBox("Uses selected Camera, Main Camera, then the first active scene Camera.", MessageType.Info); + } + + EditorGUILayout.Space(6f); + EditorGUILayout.BeginHorizontal(); + width = Mathf.Max(1, EditorGUILayout.IntField("Width", width)); + height = Mathf.Max(1, EditorGUILayout.IntField("Height", height)); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(4f); + EditorGUILayout.BeginHorizontal(); + AddPresetButton("HD", 1280, 720); + AddPresetButton("FHD", 1920, 1080); + AddPresetButton("QHD", 2560, 1440); + AddPresetButton("4K", 3840, 2160); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.BeginHorizontal(); + AddPresetButton("Portrait", 1080, 1920); + AddPresetButton("Square", 1080, 1080); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(6f); + transparentBackground = EditorGUILayout.Toggle("Transparent Background", transparentBackground); + revealAfterCapture = EditorGUILayout.Toggle("Reveal After Capture", revealAfterCapture); + + EditorGUILayout.Space(8f); + EditorGUILayout.LabelField("Save Folder", ResolutionScreenshotMenu.GetScreenshotFolder(), EditorStyles.miniLabel); + + EditorGUILayout.Space(8f); + if (GUILayout.Button("Capture", GUILayout.Height(32f))) + { + ResolutionScreenshotMenu.Capture(width, height, captureCamera, transparentBackground, revealAfterCapture); + } + } + + private void AddPresetButton(string label, int presetWidth, int presetHeight) + { + if (GUILayout.Button(label)) + { + width = presetWidth; + height = presetHeight; + } + } + } +} diff --git a/Editor/Camera/ResolutionScreenshotMenu.cs.meta b/Editor/Camera/ResolutionScreenshotMenu.cs.meta new file mode 100644 index 0000000..61021a8 --- /dev/null +++ b/Editor/Camera/ResolutionScreenshotMenu.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b0a2f6d13cc447e9a60ef0d4cf3a6dd2 diff --git a/Editor/Camera/SceneViewCaptureOverlay.cs b/Editor/Camera/SceneViewCaptureOverlay.cs new file mode 100644 index 0000000..bbec46f --- /dev/null +++ b/Editor/Camera/SceneViewCaptureOverlay.cs @@ -0,0 +1,125 @@ +using System.IO; +using Streamingle.Editor.Utilities; +using UnityEditor; +using UnityEditor.Overlays; +using UnityEngine; +using UnityEngine.UIElements; + +/// +/// Native Scene View overlay for the screenshot workflow. Unity owns the title +/// bar, drag behavior, collapse state, and overlay menu; this class only +/// supplies the capture actions inside the panel. +/// +[Overlay( + typeof(SceneView), + "Screenshot", + defaultDisplay = true, + defaultDockZone = DockZone.Floating)] +public sealed class SceneViewCaptureOverlay : Overlay +{ + public override VisualElement CreatePanelContent() + { + var root = new VisualElement(); + root.style.minWidth = 150f; + root.style.paddingLeft = 2f; + root.style.paddingRight = 2f; + root.style.paddingTop = 2f; + root.style.paddingBottom = 2f; + + var shotButton = new Button(() => CaptureCamera(1920, 1080)) + { + text = "Shot" + }; + shotButton.style.height = 24f; + shotButton.style.marginBottom = 4f; + root.Add(shotButton); + + root.Add(CreateLabel("Camera")); + root.Add(CreateButtonRow( + ("FHD", () => CaptureCamera(1920, 1080)), + ("QHD", () => CaptureCamera(2560, 1440)), + ("4K", () => CaptureCamera(3840, 2160)))); + + root.Add(CreateLabel("Utilities")); + root.Add(CreateButtonRow( + ("Scene", CaptureSceneView), + ("Frame", FrameSelection), + ("Folder", OpenScreenshotFolder))); + + return root; + } + + private static Label CreateLabel(string text) + { + var label = new Label(text); + label.style.fontSize = 10; + label.style.marginTop = 2f; + label.style.marginBottom = 1f; + return label; + } + + private static VisualElement CreateButtonRow(params (string text, System.Action action)[] buttons) + { + var row = new VisualElement(); + row.style.flexDirection = FlexDirection.Row; + row.style.marginBottom = 3f; + + foreach ((string text, System.Action action) in buttons) + { + var button = new Button(action) { text = text }; + button.style.flexGrow = 1f; + button.style.marginRight = 2f; + row.Add(button); + } + + return row; + } + + private static void CaptureCamera(int width, int height) + { + // Keep the established workflow: selected Camera, then Main Camera, + // then the best available scene camera. + ResolutionScreenshotMenu.Capture( + width, + height, + camera: null, + transparentBackground: false, + revealAfterCapture: false); + } + + private void CaptureSceneView() + { + SceneView sceneView = containerWindow as SceneView ?? SceneView.lastActiveSceneView; + if (sceneView == null || sceneView.camera == null) + { + Debug.LogWarning("[Screenshot] Active Scene View camera was not available."); + return; + } + + ResolutionScreenshotMenu.Capture( + 1920, + 1080, + sceneView.camera, + transparentBackground: false, + revealAfterCapture: false); + } + + private void FrameSelection() + { + SceneView sceneView = containerWindow as SceneView ?? SceneView.lastActiveSceneView; + if (sceneView == null) + { + return; + } + + sceneView.FrameSelected(); + sceneView.Repaint(); + } + + private static void OpenScreenshotFolder() + { + string folder = ResolutionScreenshotMenu.GetScreenshotFolder(); + Directory.CreateDirectory(folder); + EditorUtility.RevealInFinder(folder); + } +} diff --git a/Editor/Camera/SceneViewCaptureOverlay.cs.meta b/Editor/Camera/SceneViewCaptureOverlay.cs.meta new file mode 100644 index 0000000..b8358a2 --- /dev/null +++ b/Editor/Camera/SceneViewCaptureOverlay.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: af06ad547a1b4164b140005e12f2cdb9 diff --git a/package.json b/package.json index 63afec1..203aa09 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "com.streamingle.utilities", "displayName": "Streamingle Utilities", - "version": "0.1.2", + "version": "0.1.3", "unity": "6000.0", "description": "Reusable Streamingle runtime components and Unity editor utilities.", "keywords": [