using System; using System.Collections; using System.Collections.Generic; using ToriaCommon.Datas.Types; using UnityEngine; using Color = UnityEngine.Color; using Material = UnityEngine.Material; namespace Wing.Gizmos { /* * NOTE: * This code comes from an old Gizmos package, it has been modified a long time ago (legacy code). * For improve the system and clean the code I have to modified it again. * Now it's possible to specify around which axis rotate. * * With all these changes, this script has become dependent on ToriaCommon. It is no longer a package script. */ public class GizmosRotate : MonoBehaviour { public delegate void RotateEventHandler(float dx, float dy, float dz); public event RotateEventHandler OnRotate; public event Action OnRotateEnd; const float interactionAngle = 20; //angle on each side of an arc where it is interactible to track it public Camera RenderCamera; public float RotateSpeed = 1; [SerializeField] float radius = .1f; //scale of the sphere public bool EnableFreeRotate = false; [Header("Colors")] [SerializeField] Color xColor = new(1, 0, 0, 0.5f); [SerializeField] Color yColor = new(0, 1, 0, 0.5f); [SerializeField] Color zColor = new(0, 0, 1, 0.5f); [SerializeField] Color selectColor = new(1, 0.92f, 0.016f, 0.5f); private Vector3 mTotal = Vector3.zero; readonly private List verticesCount = new(); private float m_fTheta = 0.1f; //arc precision (mesh generation) private GameObject m_pSphere = null; private GameObject m_pMeshGo = null; private Mesh m_pMesh; private int m_iLayer = 0; private Matrix4x4[] m_arrMats = null; private AxisXYZ trackedAxis = AxisXYZ.None; private AxisXYZ allowedAxis = AxisXYZ.All; private bool rotationChanged = false; private Vector3 lastMousePosition = Vector3.zero; void Awake() { if (RenderCamera == null) { RenderCamera = Camera.main; } m_iLayer = LayerMask.NameToLayer(Settings.GizmosLayerName); m_arrMats = new Matrix4x4[] { Matrix4x4.TRS(Vector3.zero, Quaternion.Euler(0, 0, 90), Vector3.one), Matrix4x4.identity, Matrix4x4.TRS(Vector3.zero, Quaternion.Euler(90, 0, 0), Vector3.one) }; CreateSphere(); CreateXYZCircles(); } #region Init public void Initialize(float radius, Quaternion rotation, AxisXYZ allowedAxis, bool enableFreeRotation = false) { //Destroy previous XYZ circles Destroy(m_pSphere.transform.GetChild(0).gameObject); m_pSphere.transform.rotation = rotation; EnableFreeRotate = enableFreeRotation; this.radius = radius; this.allowedAxis = allowedAxis; CreateXYZCircles(); } private void CreateSphere() { m_pSphere = GameObject.CreatePrimitive(PrimitiveType.Sphere); m_pSphere.transform.SetParent(transform); m_pSphere.transform.localPosition = Vector3.zero; RefreshScale(); m_pSphere.GetComponent().isTrigger = true; m_pSphere.layer = m_iLayer; MeshRenderer render = m_pSphere.GetComponent(); render.material = new Material(Shader.Find("TransparentShader")); render.material.color = new Color(0.5f, 0.5f, 0.5f, 0.2f); //m_pSphere.GetComponent().enabled = false; } private void CreateXYZCircles() { m_pMesh = new Mesh(); m_pMesh.MarkDynamic(); int verticesCount; //x if (IsAxisAllowed(AxisXYZ.X)) { verticesCount = CreateArc(xColor, m_pMesh, m_arrMats[0]); this.verticesCount.Add(verticesCount); } //y if (IsAxisAllowed(AxisXYZ.Y)) { verticesCount = CreateArc(yColor, m_pMesh, m_arrMats[1]); this.verticesCount.Add(verticesCount); } //z if (IsAxisAllowed(AxisXYZ.Z)) { verticesCount = CreateArc(zColor, m_pMesh, m_arrMats[2]); this.verticesCount.Add(verticesCount); } //Mesh Generation m_pMesh.subMeshCount = 1; m_pMesh.SetIndices(SequentialTriangles(m_pMesh.vertices.Length), MeshTopology.Lines, 0); m_pMeshGo = new GameObject("Rotate Axis"); m_pMeshGo.transform.SetParent(m_pSphere.transform, false); m_pMeshGo.layer = m_iLayer; m_pMeshGo.AddComponent().mesh = m_pMesh; m_pMeshGo.AddComponent().material = new Material(Shader.Find("RotateShader")); //reset circles mesh transform m_pMeshGo.transform.localPosition = Vector3.zero; m_pMeshGo.transform.localRotation = Quaternion.identity; m_pMeshGo.transform.localScale = Vector3.one / 2; } private int CreateArc(Color color, Mesh mesh, Matrix4x4 mat, float angle = 2 * Mathf.PI, bool close = false) { m_fTheta = Mathf.Max(0.0001f, m_fTheta); int count = mesh.vertexCount; List vertices = new List(mesh.vertices); List colors = new List(mesh.colors); // draw circle Vector3 beginPoint = mat * Vector3.zero; Vector3 firstPoint = mat * Vector3.zero; for (float theta = 0; theta < angle; theta += m_fTheta) { float x = Mathf.Cos(theta); float z = Mathf.Sin(theta); Vector3 endPoint = mat * new Vector3(x, 0, z); if (theta == 0 && !close) { firstPoint = endPoint; } else { vertices.Add(beginPoint); vertices.Add(endPoint); colors.Add(color); colors.Add(color); } beginPoint = endPoint; } // draw last line vertices.Add(firstPoint); vertices.Add(beginPoint); colors.Add(color); colors.Add(color); mesh.SetVertices(vertices); mesh.SetColors(colors); return vertices.Count - count; } private static int[] SequentialTriangles(int len) { int[] array = new int[len]; for (int i = 0; i < len; i++) { array[i] = i; } return array; } #endregion public Quaternion GetRotation() { return m_pSphere.transform.rotation; } private void Update() { if (Input.GetMouseButtonDown(0)) StartTracking(); if (Input.GetMouseButtonUp(0)) StopTracking(); RotateAroundTrackedAxis(); RefreshScale(); } private void StartTracking() { lastMousePosition = Input.mousePosition; Ray ray = RenderCamera.ScreenPointToRay(lastMousePosition); //Try select closest axis and start tracking it if (Physics.Raycast(ray, out RaycastHit hit, 10000, 1 << m_iLayer, QueryTriggerInteraction.Collide)) { AxisXYZ axis = FindClosetAxis(hit.point); SelectAxis(axis); rotationChanged = false; } } private void StopTracking() { SelectAxis(AxisXYZ.None); if (rotationChanged) { StartCoroutine(DelayRelease()); rotationChanged = false; } } private void RefreshScale() { m_pSphere.transform.localScale = (Vector3.Distance(RenderCamera.transform.position, m_pSphere.transform.position) / 4) * radius * Vector3.one; } private void RotateAroundTrackedAxis() { if (!IsAxisAllowed(trackedAxis)) return; Vector3 delta = TrackBall(Input.mousePosition); //Snap rotation //NOTE: the snap isn't perfect at the moment, so let it disable. //It snap axis by axis so when you touch all the axis, the final result isn't correctly snapped. //To test the snap, you must activate it beforehand with this line: SnapFactory.Instance.Get(ESnapType.Rotate).Enable = true; Vector3 total = Vector3.zero; ISnap snap = SnapFactory.Instance.Get(ESnapType.Rotate); if (snap.Snap(mTotal + delta, ref total)) { delta = total - mTotal; } mTotal += delta; mTotal.x %= 360; mTotal.y %= 360; mTotal.z %= 360; ////////////// if (delta == Vector3.zero) return; m_pSphere.transform.Rotate(delta); rotationChanged = true; OnRotate?.Invoke(delta.x, delta.y, delta.z); } private IEnumerator DelayRelease() { yield return new WaitForEndOfFrame(); // OnRotateEnd(m_eCurrentAxis, GetRotate()); OnRotateEnd?.Invoke(); } private Vector3 ComputeRotateDir(Vector3 offset, AxisXYZ type) { if (offset.x + offset.y == 0) { return Vector3.zero; } Vector3 startPos = m_pSphere.transform.position; Vector3 start = Camera.main.WorldToScreenPoint(startPos); Vector3 axis; Vector3 end; if (type == AxisXYZ.X) { end = Camera.main.WorldToScreenPoint(startPos + m_pSphere.transform.right * 2); axis = Vector3.right; } else if (type == AxisXYZ.Y) { end = Camera.main.WorldToScreenPoint(startPos + m_pSphere.transform.up * 2); axis = Vector3.up; } else { end = Camera.main.WorldToScreenPoint(startPos + m_pSphere.transform.forward * 2); axis = Vector3.forward; } Vector3 axisDir = end - start; axisDir.Normalize(); axisDir = Vector3.Cross(Vector3.forward, axisDir); float moveLen = axisDir.x * offset.x + axisDir.y * offset.y; float mx = moveLen * axis.x; float my = moveLen * axis.y; float mz = moveLen * axis.z; return new Vector3(mx, my, mz); } private Vector3 TrackBall(Vector3 screenPosition) { Vector3 offset = (screenPosition - lastMousePosition) * RotateSpeed; lastMousePosition = screenPosition; Vector3 angle = Vector3.zero; if (trackedAxis == AxisXYZ.All) { Quaternion q = Quaternion.AngleAxis(offset.y, m_pSphere.transform.InverseTransformVector(RenderCamera.transform.right)); q *= Quaternion.AngleAxis(-offset.x, m_pSphere.transform.InverseTransformVector(RenderCamera.transform.up)); angle += q.eulerAngles; } else { angle += ComputeRotateDir(offset, trackedAxis); } return angle; } private AxisXYZ FindClosetAxis(Vector3 hitPoint) { GetDeltaAngles(hitPoint, out float xAngle, out float yAngle, out float zAngle); (AxisXYZ axis, float angle) closest = (AxisXYZ.None, 0); TrySetClosest(AxisXYZ.X, xAngle); TrySetClosest(AxisXYZ.Y, yAngle); TrySetClosest(AxisXYZ.Z, zAngle); if (EnableFreeRotate && closest.axis == AxisXYZ.None && IsAxisAllowed(AxisXYZ.All)) closest.axis = AxisXYZ.All; return closest.axis; void TrySetClosest(AxisXYZ axis, float angle) { if (IsAxisAllowed(axis) && angle < interactionAngle && (closest.axis == AxisXYZ.None || angle < closest.angle)) closest = (axis, angle); } } private void GetDeltaAngles(Vector3 position, out float xAngle, out float yAngle, out float zAngle) { Vector3 dir = m_pSphere.transform.InverseTransformPoint(position).normalized; xAngle = Mathf.Acos(dir.x) * Mathf.Rad2Deg; yAngle = Mathf.Asin(dir.y) * Mathf.Rad2Deg; zAngle = Mathf.Acos(dir.z) * Mathf.Rad2Deg; xAngle = GetDelta(xAngle, true); yAngle = GetDelta(yAngle, false); zAngle = GetDelta(zAngle, true); } private float GetDelta(float angle, bool verticalAxis) { //0° is the angle for a horizontal axis //90° is the angle for a vertical axis //See trigonometry circle return Mathf.Abs(angle - (verticalAxis ? 90 : 0)); } private void SelectAxis(AxisXYZ toSelect) { if (trackedAxis == toSelect) return; trackedAxis = toSelect; List colors = new(); int index = 0; SetArcColor(AxisXYZ.X, xColor); SetArcColor(AxisXYZ.Y, yColor); SetArcColor(AxisXYZ.Z, zColor); m_pMesh.SetColors(colors); void SetArcColor(AxisXYZ axis, Color axisColor) { if (!IsAxisAllowed(axis)) return; int count = verticesCount[index++]; for (int i = 0; i < count; i++) { colors.Add(trackedAxis == axis || trackedAxis == AxisXYZ.All ? selectColor : axisColor); } } } bool IsAxisAllowed(AxisXYZ axis) => axis != AxisXYZ.None && (allowedAxis == AxisXYZ.All || allowedAxis == axis); } }