// =============================================================================== // CameraBookmarksTool - Configurable Scene View Camera Save & Load // // Creator: Scove // Last Updated: 2024-05-08 // Version: 3.0 // // Purpose: // Allows saving and loading specific Scene View camera angles and positions. // Includes an Editor Window interface to completely customize the keyboard shortcuts. // // How to Use: // 1. Place this script in an 'Editor' folder in your project. // 2. Open Settings: Menu -> Tools -> Camera Bookmarks Settings. // 3. Customize your Save/Load modifiers (e.g., Control, Shift, Alt). // 4. Customize the shortcut keys for Slot 1 to 9. // 5. Focus the Scene View and use your assigned shortcuts to Save/Load camera angles. // =============================================================================== #if UNITY_EDITOR using UnityEditor; using UnityEngine; using System.Globalization; using System; using UnityEngine.UIElements; using UnityEditor.UIElements; namespace Editor { [InitializeOnLoad] public class CameraBookmarksTool : EditorWindow { // Customizable Shortcut Keys private static EventModifiers saveModifier; private static EventModifiers loadModifier; private static KeyCode[] slotKeys = new KeyCode[9]; // Replaced static constructor with InitializeOnLoadMethod to avoid EditorPrefs exceptions [InitializeOnLoadMethod] static void Init() { LoadSettings(); SceneView.duringSceneGui += OnSceneGUI; }[MenuItem("Tools/Camera Bookmarks Settings")] public static void ShowWindow() { GetWindow("Cam Bookmarks"); } private static void LoadSettings() { // Load settings from EditorPrefs, fallback to Control/Shift if not found saveModifier = (EventModifiers)EditorPrefs.GetInt("CamBM_SaveMod", (int)EventModifiers.Control); loadModifier = (EventModifiers)EditorPrefs.GetInt("CamBM_LoadMod", (int)EventModifiers.Shift); // Load key bindings for 9 slots, fallback to Alpha1-Alpha9 for (int i = 0; i < 9; i++) { int defaultKey = (int)(KeyCode.Alpha1 + i); slotKeys[i] = (KeyCode)EditorPrefs.GetInt($"CamBM_Key_{i}", defaultKey); } } private static void SaveSettings() { EditorPrefs.SetInt("CamBM_SaveMod", (int)saveModifier); EditorPrefs.SetInt("CamBM_LoadMod", (int)loadModifier); for (int i = 0; i < 9; i++) { EditorPrefs.SetInt($"CamBM_Key_{i}", (int)slotKeys[i]); } } public void CreateGUI() { VisualElement root = rootVisualElement; root.style.paddingTop = 15; root.style.paddingBottom = 15; root.style.paddingLeft = 15; root.style.paddingRight = 15; Label title = new Label("Shortcut Configuration"); title.style.fontSize = 20; title.style.unityFontStyleAndWeight = FontStyle.Bold; title.style.color = new StyleColor(new Color(0.3f, 0.7f, 1f)); title.style.marginBottom = 15; root.Add(title); // Warning Box VisualElement warningBox = new VisualElement(); warningBox.style.backgroundColor = new StyleColor(new Color(0.8f, 0.6f, 0.1f, 0.3f)); warningBox.style.borderLeftWidth = 4; warningBox.style.borderLeftColor = new StyleColor(new Color(0.9f, 0.7f, 0.1f)); warningBox.style.paddingTop = 10; warningBox.style.paddingBottom = 10; warningBox.style.paddingLeft = 10; warningBox.style.marginBottom = 15; warningBox.style.display = DisplayStyle.None; Label warningLabel = new Label("Warning: Save and Load modifiers are the SAME! This will cause conflicts."); warningLabel.style.color = new StyleColor(new Color(0.9f, 0.8f, 0.5f)); warningLabel.style.unityFontStyleAndWeight = FontStyle.Bold; warningBox.Add(warningLabel); root.Add(warningBox); // Modifiers Card VisualElement modifiersCard = CreateCard(); EnumFlagsField saveField = new EnumFlagsField("Save Modifier", saveModifier); saveField.RegisterValueChangedCallback(evt => { saveModifier = (EventModifiers)evt.newValue; SaveSettings(); warningBox.style.display = (saveModifier == loadModifier) ? DisplayStyle.Flex : DisplayStyle.None; }); modifiersCard.Add(saveField); EnumFlagsField loadField = new EnumFlagsField("Load Modifier", loadModifier); loadField.RegisterValueChangedCallback(evt => { loadModifier = (EventModifiers)evt.newValue; SaveSettings(); warningBox.style.display = (saveModifier == loadModifier) ? DisplayStyle.Flex : DisplayStyle.None; }); loadField.style.marginTop = 10; modifiersCard.Add(loadField); warningBox.style.display = (saveModifier == loadModifier) ? DisplayStyle.Flex : DisplayStyle.None; root.Add(modifiersCard); // Slots Section Label slotsTitle = new Label("Slot Keys Assignment"); slotsTitle.style.fontSize = 16; slotsTitle.style.unityFontStyleAndWeight = FontStyle.Bold; slotsTitle.style.color = new StyleColor(new Color(0.8f, 0.8f, 0.8f)); slotsTitle.style.marginTop = 15; slotsTitle.style.marginBottom = 10; root.Add(slotsTitle); VisualElement slotsCard = CreateCard(); for (int i = 0; i < 9; i++) { int index = i; EnumField slotField = new EnumField($"Slot {i + 1}", slotKeys[i]); slotField.RegisterValueChangedCallback(evt => { slotKeys[index] = (KeyCode)evt.newValue; SaveSettings(); }); if (i > 0) slotField.style.marginTop = 8; slotsCard.Add(slotField); } root.Add(slotsCard); // Reset Button Button resetBtn = new Button(() => { saveModifier = EventModifiers.Control; loadModifier = EventModifiers.Shift; for (int i = 0; i < 9; i++) slotKeys[i] = KeyCode.Alpha1 + i; SaveSettings(); root.Clear(); CreateGUI(); // Rebuild }); resetBtn.text = "Reset to Default Settings"; resetBtn.style.height = 40; resetBtn.style.marginTop = 20; resetBtn.style.backgroundColor = new StyleColor(new Color(0.7f, 0.3f, 0.3f)); resetBtn.style.color = new StyleColor(Color.white); resetBtn.style.unityFontStyleAndWeight = FontStyle.Bold; resetBtn.style.borderTopLeftRadius = 6; resetBtn.style.borderTopRightRadius = 6; resetBtn.style.borderBottomLeftRadius = 6; resetBtn.style.borderBottomRightRadius = 6; root.Add(resetBtn); root.Add(ScovySignature.CreateSignatureBox()); } private VisualElement CreateCard() { VisualElement card = new VisualElement(); card.style.backgroundColor = new StyleColor(new Color(0.18f, 0.18f, 0.18f, 0.9f)); card.style.borderTopLeftRadius = 8; card.style.borderTopRightRadius = 8; card.style.borderBottomLeftRadius = 8; card.style.borderBottomRightRadius = 8; card.style.paddingTop = 15; card.style.paddingBottom = 15; card.style.paddingLeft = 15; card.style.paddingRight = 15; card.style.borderTopWidth = 1; card.style.borderBottomWidth = 1; card.style.borderLeftWidth = 1; card.style.borderRightWidth = 1; card.style.borderTopColor = new StyleColor(new Color(0.25f, 0.25f, 0.25f)); card.style.borderBottomColor = new StyleColor(new Color(0.25f, 0.25f, 0.25f)); card.style.borderLeftColor = new StyleColor(new Color(0.25f, 0.25f, 0.25f)); card.style.borderRightColor = new StyleColor(new Color(0.25f, 0.25f, 0.25f)); return card; } static void OnSceneGUI(SceneView view) { Event e = Event.current; if (e.type == EventType.KeyDown) { // Mask out CapsLock, NumLock, and Function keys. We only care about main modifiers. EventModifiers currentMods = e.modifiers & (EventModifiers.Shift | EventModifiers.Control | EventModifiers.Alt | EventModifiers.Command); // Check if the pressed key matches any of our custom assigned slot keys int pressedSlotIndex = -1; for (int i = 0; i < 9; i++) { if (e.keyCode == slotKeys[i] && e.keyCode != KeyCode.None) { pressedSlotIndex = i + 1; break; } } if (pressedSlotIndex != -1) { string prefsKey = $"CamBookmark_Slot_{pressedSlotIndex}"; // SAVE ACTION if (currentMods == saveModifier) { SaveBookmark(view, prefsKey, pressedSlotIndex); e.Use(); // Consume the event } // LOAD ACTION else if (currentMods == loadModifier) { LoadBookmark(view, prefsKey, pressedSlotIndex); e.Use(); // Consume the event } } } } private static void SaveBookmark(SceneView view, string key, int slotIndex) { Transform camTransform = view.camera.transform; float size = view.size; // Use InvariantCulture to ensure dot (.) is used for decimals, preventing regional bugs string data = string.Format(CultureInfo.InvariantCulture, "{0}|{1}|{2}|{3}|{4}|{5}|{6}|{7}", camTransform.position.x, camTransform.position.y, camTransform.position.z, camTransform.rotation.x, camTransform.rotation.y, camTransform.rotation.z, camTransform.rotation.w, size); EditorPrefs.SetString(key, data); // Show feedback in Scene View string message = $"Saved Camera Bookmark to Slot {slotIndex}"; view.ShowNotification(new GUIContent(message)); Debug.Log($"[CameraBookmarks] {message}"); } private static void LoadBookmark(SceneView view, string key, int slotIndex) { if (!EditorPrefs.HasKey(key)) { string emptyMsg = $"Slot {slotIndex} is empty!"; view.ShowNotification(new GUIContent(emptyMsg)); Debug.LogWarning($"[CameraBookmarks] {emptyMsg}"); return; } string[] parts = EditorPrefs.GetString(key).Split('|'); if (parts.Length == 8) { try { // Parse data back to floats using InvariantCulture Vector3 pos = new Vector3( float.Parse(parts[0], CultureInfo.InvariantCulture), float.Parse(parts[1], CultureInfo.InvariantCulture), float.Parse(parts[2], CultureInfo.InvariantCulture) ); Quaternion rot = new Quaternion( float.Parse(parts[3], CultureInfo.InvariantCulture), float.Parse(parts[4], CultureInfo.InvariantCulture), float.Parse(parts[5], CultureInfo.InvariantCulture), float.Parse(parts[6], CultureInfo.InvariantCulture) ); float size = float.Parse(parts[7], CultureInfo.InvariantCulture); // Apply the saved transform to the Scene View camera view.LookAtDirect(pos, rot, size); // Show feedback in Scene View string message = $"Loaded Camera Bookmark from Slot {slotIndex}"; view.ShowNotification(new GUIContent(message)); Debug.Log($"[CameraBookmarks] {message}"); } catch (Exception ex) { Debug.LogError($"[CameraBookmarks] Failed to parse data for Slot {slotIndex}. Error: {ex.Message}"); } } } } } #endif