using UnityEngine; namespace GraphicsCat { public class SlideRotationController : MonoBehaviour, IMGUIDockable { private const float INERTIA_THRESHOLD = 0.1f; // Threshold to stop inertia rotation [Header("Slide Rotation")] [InspectorDisplayName("Enable")] public bool slideRotationEnabled = false; [InspectorDisplayName("Sensitivity")] public Vector3 slideRotationSensitivity = new(0, 20, 0); [InspectorDisplayName("Inertia Damping")] [Range(0f, 1f)] public float inertiaDamping = 0.8f; // 0 = no inertia, 1 = maximum inertia [Header("Auto Rotation")] [InspectorDisplayName("Enable")] public bool autoRotationEnabled = true; [InspectorDisplayName("Speed")] public Vector3 autoRotationSpeed = new(0, 5, 0); [Header("Other")] public bool displayInGameGUI = false; // Input state private bool m_IsDragging = false; private Vector3 m_LastPointerPosition = Vector3.zero; private Vector3 m_PointerDelta = Vector3.zero; private Transform m_Target = null; // Touch input private Touch m_MainTouch; private bool m_IsTouchActive = false; // Auto rotation private bool m_WasAutoRotationEnabled; private bool m_IsAutoRotationPaused = false; // Inertia private Vector3 m_CurrentInertia = Vector3.zero; private Vector3 m_LastAngularVelocity = Vector3.zero; private bool m_HasAngularMovement = false; // Track if there was actual angular movement private void Start() { m_Target = transform; IMGUIDock.topRight.DockGUI(this); } private void Update() { if (!slideRotationEnabled) return; m_PointerDelta = Vector3.zero; HandleMouseInput(); if (Application.isMobilePlatform) HandleTouchInput(); } private void FixedUpdate() { Vector3 slideRotationDelta = Vector3.zero; if (slideRotationEnabled) { // Handle manual rotation if (m_PointerDelta != Vector3.zero) { slideRotationDelta = ComputeRotationDelta(m_PointerDelta.x, m_PointerDelta.y); } // Apply inertia when not actively rotating else if (m_CurrentInertia.magnitude > INERTIA_THRESHOLD) { slideRotationDelta = m_CurrentInertia; m_CurrentInertia *= inertiaDamping; if (m_CurrentInertia.magnitude < INERTIA_THRESHOLD) m_CurrentInertia = Vector3.zero; } // Apply manual rotation or inertia if (slideRotationDelta != Vector3.zero) { Quaternion deltaRotation = Quaternion.Euler(slideRotationDelta * Time.fixedDeltaTime); m_Target.localRotation = deltaRotation * m_Target.localRotation; m_LastAngularVelocity = slideRotationDelta; m_HasAngularMovement = true; // There was actual rotation this frame } else { // No rotation this frame m_HasAngularMovement = false; } } // Apply auto rotation if (slideRotationDelta == Vector3.zero && autoRotationEnabled && !m_IsAutoRotationPaused) { ApplyAutoRotation(); } } /// /// Apply auto rotation according to configured speed. /// private void ApplyAutoRotation() { Quaternion rotation = Quaternion.Euler(autoRotationSpeed * Time.fixedDeltaTime); m_Target.localRotation = rotation * m_Target.localRotation; } /// /// Compute rotation delta based on pointer movement. /// private Vector3 ComputeRotationDelta(float x, float y) { return Vector3.Scale(new Vector3(y, x, 0), -slideRotationSensitivity); } /// /// Handle mouse input for slide rotation. /// private void HandleMouseInput() { if (Input.GetMouseButtonDown(0)) { if (IsPointerOverTarget(Input.mousePosition)) { m_IsDragging = true; m_LastPointerPosition = Input.mousePosition; m_CurrentInertia = Vector3.zero; // Reset inertia when starting drag m_HasAngularMovement = false; PauseAutoRotation(); } } else if (Input.GetMouseButtonUp(0)) { if (m_IsDragging) { m_IsDragging = false; // Only apply inertia if there was actual angular movement if (m_HasAngularMovement) ApplyInertia(); else m_CurrentInertia = Vector3.zero; // No movement, no inertia ResumeAutoRotation(); } } else if (Input.GetMouseButton(0) && m_IsDragging) { Vector3 currentPosition = Input.mousePosition; m_PointerDelta = currentPosition - m_LastPointerPosition; m_LastPointerPosition = currentPosition; } else { m_IsDragging = false; } } /// /// Handle touch input for slide rotation. /// private void HandleTouchInput() { int touchCount = Input.touchCount; if (touchCount <= 0) { m_IsTouchActive = false; return; } for (int i = 0; i < touchCount; i++) { Touch touch = Input.GetTouch(i); switch (touch.phase) { case TouchPhase.Began: if (IsPointerOverTarget(touch.position)) { m_MainTouch = touch; m_IsTouchActive = true; m_LastPointerPosition = touch.position; m_CurrentInertia = Vector3.zero; // Reset inertia when starting touch m_HasAngularMovement = false; PauseAutoRotation(); } break; case TouchPhase.Moved: if (m_IsTouchActive && touch.fingerId == m_MainTouch.fingerId) { Vector3 currentTouchPosition = touch.position; m_PointerDelta = currentTouchPosition - m_LastPointerPosition; m_LastPointerPosition = currentTouchPosition; } break; case TouchPhase.Ended: case TouchPhase.Canceled: if (m_IsTouchActive && touch.fingerId == m_MainTouch.fingerId) { m_IsTouchActive = false; m_PointerDelta = Vector3.zero; // Only apply inertia if there was actual angular movement if (m_HasAngularMovement) ApplyInertia(); else m_CurrentInertia = Vector3.zero; // No movement, no inertia ResumeAutoRotation(); } break; } } } /// /// Apply inertia based on the last angular velocity. /// Inertia is directly proportional to the angular velocity at the moment of release. /// private void ApplyInertia() { // Only apply inertia if there was significant angular velocity // This ensures inertia magnitude correlates with rotation speed at release if (inertiaDamping > 0 && m_LastAngularVelocity.magnitude > INERTIA_THRESHOLD) m_CurrentInertia = m_LastAngularVelocity * inertiaDamping; else m_CurrentInertia = Vector3.zero; } /// /// Temporarily pause auto rotation during user interaction. /// private void PauseAutoRotation() { if (autoRotationEnabled) { m_WasAutoRotationEnabled = autoRotationEnabled; autoRotationEnabled = false; m_IsAutoRotationPaused = true; } } /// /// Resume auto rotation after user interaction ends. /// private void ResumeAutoRotation() { if (m_IsAutoRotationPaused) { autoRotationEnabled = m_WasAutoRotationEnabled; m_IsAutoRotationPaused = false; } } /// /// Check if the given screen point is over this object using renderer bounds. /// private bool IsPointerOverTarget(Vector3 screenPoint) { Camera camera = Camera.main; if (camera == null) return false; Renderer[] renderers = m_Target.GetComponentsInChildren(); if (renderers.Length == 0) return false; Ray ray = camera.ScreenPointToRay(screenPoint); foreach (Renderer renderer in renderers) { if (renderer.enabled && renderer.bounds.IntersectRay(ray)) return true; } return false; } public void OnDockGUI() { if (!displayInGameGUI) return; GUILayout.BeginHorizontal(); if (GUILayout.Button("Slide Rotation " + (slideRotationEnabled ? "O" : "X"))) slideRotationEnabled = !slideRotationEnabled; GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); if (GUILayout.Button("Auto Rotation " + (autoRotationEnabled ? "O" : "X"))) autoRotationEnabled = !autoRotationEnabled; GUILayout.EndHorizontal(); } } }