using UnityEngine; using UnityEditor; using System.Collections; [System.Serializable] public class ZoomArea { // Global state private static Vector2 m_MouseDownPosition = new Vector2(-1000000, -1000000); // in transformed space private static int zoomableAreaHash = "ZoomableArea".GetHashCode(); // Range lock settings [SerializeField] private bool m_HRangeLocked; [SerializeField] private bool m_VRangeLocked; public bool hRangeLocked { get { return m_HRangeLocked; } set { m_HRangeLocked = value; } } public bool vRangeLocked { get { return m_VRangeLocked; } set { m_VRangeLocked = value; } } [SerializeField] private float m_HBaseRangeMin = 0; [SerializeField] private float m_HBaseRangeMax = 1; [SerializeField] private float m_VBaseRangeMin = 0; [SerializeField] private float m_VBaseRangeMax = 1; public float hBaseRangeMin { get { return m_HBaseRangeMin; } set { m_HBaseRangeMin = value; } } public float hBaseRangeMax { get { return m_HBaseRangeMax; } set { m_HBaseRangeMax = value; } } public float vBaseRangeMin { get { return m_VBaseRangeMin; } set { m_VBaseRangeMin = value; } } public float vBaseRangeMax { get { return m_VBaseRangeMax; } set { m_VBaseRangeMax = value; } } [SerializeField] private bool m_HAllowExceedBaseRangeMin = true; [SerializeField] private bool m_HAllowExceedBaseRangeMax = true; [SerializeField] private bool m_VAllowExceedBaseRangeMin = true; [SerializeField] private bool m_VAllowExceedBaseRangeMax = true; public bool hAllowExceedBaseRangeMin { get { return m_HAllowExceedBaseRangeMin; } set { m_HAllowExceedBaseRangeMin = value; } } public bool hAllowExceedBaseRangeMax { get { return m_HAllowExceedBaseRangeMax; } set { m_HAllowExceedBaseRangeMax = value; } } public bool vAllowExceedBaseRangeMin { get { return m_VAllowExceedBaseRangeMin; } set { m_VAllowExceedBaseRangeMin = value; } } public bool vAllowExceedBaseRangeMax { get { return m_VAllowExceedBaseRangeMax; } set { m_VAllowExceedBaseRangeMax = value; } } public float hRangeMin { get { return (hAllowExceedBaseRangeMin ? Mathf.NegativeInfinity : hBaseRangeMin); } set { SetAllowExceed(ref m_HBaseRangeMin, ref m_HAllowExceedBaseRangeMin, value); } } public float hRangeMax { get { return (hAllowExceedBaseRangeMax ? Mathf.Infinity : hBaseRangeMax); } set { SetAllowExceed(ref m_HBaseRangeMax, ref m_HAllowExceedBaseRangeMax, value); } } public float vRangeMin { get { return (vAllowExceedBaseRangeMin ? Mathf.NegativeInfinity : vBaseRangeMin); } set { SetAllowExceed(ref m_VBaseRangeMin, ref m_VAllowExceedBaseRangeMin, value); } } public float vRangeMax { get { return (vAllowExceedBaseRangeMax ? Mathf.Infinity : vBaseRangeMax); } set { SetAllowExceed(ref m_VBaseRangeMax, ref m_VAllowExceedBaseRangeMax, value); } } private void SetAllowExceed(ref float rangeEnd, ref bool allowExceed, float value) { if (value == Mathf.NegativeInfinity || value == Mathf.Infinity) { rangeEnd = (value == Mathf.NegativeInfinity ? 0 : 1); allowExceed = true; } else { rangeEnd = value; allowExceed = false; } } private float m_HScaleMin = 0.001f; private float m_HScaleMax = 100000.0f; private float m_VScaleMin = 0.001f; private float m_VScaleMax = 100000.0f; // Window resize settings [SerializeField] private bool m_ScaleWithWindow = false; public bool scaleWithWindow { get { return m_ScaleWithWindow; } set { m_ScaleWithWindow = value; } } // Slider settings [SerializeField] private bool m_HSlider = true; [SerializeField] private bool m_VSlider = true; public bool hSlider { get { return m_HSlider; } set { Rect r = rect; m_HSlider = value; rect = r; } } public bool vSlider { get { return m_VSlider; } set { Rect r = rect; m_VSlider = value; rect = r; } } [SerializeField] private bool m_IgnoreScrollWheelUntilClicked = false; public bool ignoreScrollWheelUntilClicked { get { return m_IgnoreScrollWheelUntilClicked; } set { m_IgnoreScrollWheelUntilClicked = value; } } public bool m_UniformScale; public bool uniformScale { get { return m_UniformScale; } set { m_UniformScale = value; } } // View and drawing settings [SerializeField] private Rect m_DrawArea = new Rect(0, 0, 100, 100); internal void SetDrawRectHack(Rect r) { m_DrawArea = r; } [SerializeField] internal Vector2 m_Scale = new Vector2(1, -1); [SerializeField] internal Vector2 m_Translation = new Vector2(0, 0); [SerializeField] private float m_MarginLeft, m_MarginRight, m_MarginTop, m_MarginBottom; [SerializeField] private Rect m_LastShownAreaInsideMargins = new Rect(0, 0, 100, 100); public Vector2 scale { get { return m_Scale; } } public float margin { set { m_MarginLeft = m_MarginRight = m_MarginTop = m_MarginBottom = value; } } public float leftmargin { get { return m_MarginLeft; } set { m_MarginLeft = value; } } public float rightmargin { get { return m_MarginRight; } set { m_MarginRight = value; } } public float topmargin { get { return m_MarginTop; } set { m_MarginTop = value; } } public float bottommargin { get { return m_MarginBottom; } set { m_MarginBottom = value; } } [SerializeField] bool m_MinimalGUI; [System.Serializable] public class Styles { public GUIStyle background = "AnimationCurveEditorBackground"; public GUIStyle horizontalScrollbar; public GUIStyle horizontalMinMaxScrollbarThumb; public GUIStyle horizontalScrollbarLeftButton; public GUIStyle horizontalScrollbarRightButton; public GUIStyle verticalScrollbar; public GUIStyle verticalMinMaxScrollbarThumb; public GUIStyle verticalScrollbarUpButton; public GUIStyle verticalScrollbarDownButton; public float sliderWidth; public float visualSliderWidth; public Styles(bool minimalGUI) { if (minimalGUI) { visualSliderWidth = 0; sliderWidth = 15; } else { visualSliderWidth = 15; sliderWidth = 15; } } public void InitGUIStyles(bool minimalGUI) { if (minimalGUI) { horizontalMinMaxScrollbarThumb = "MiniMinMaxSliderHorizontal"; horizontalScrollbarLeftButton = GUIStyle.none; horizontalScrollbarRightButton = GUIStyle.none; horizontalScrollbar = GUIStyle.none; verticalMinMaxScrollbarThumb = "MiniMinMaxSlidervertical"; verticalScrollbarUpButton = GUIStyle.none; verticalScrollbarDownButton = GUIStyle.none; verticalScrollbar = GUIStyle.none; } else { horizontalMinMaxScrollbarThumb = "horizontalMinMaxScrollbarThumb"; horizontalScrollbarLeftButton = "horizontalScrollbarLeftbutton"; horizontalScrollbarRightButton = "horizontalScrollbarRightbutton"; horizontalScrollbar = GUI.skin.horizontalScrollbar; verticalMinMaxScrollbarThumb = "verticalMinMaxScrollbarThumb"; verticalScrollbarUpButton = "verticalScrollbarUpbutton"; verticalScrollbarDownButton = "verticalScrollbarDownbutton"; verticalScrollbar = GUI.skin.verticalScrollbar; } } } private Styles m_Styles; private Styles styles { get { if (m_Styles == null) m_Styles = new Styles(m_MinimalGUI); return m_Styles; } } public Rect rect { get { return new Rect(drawRect.x, drawRect.y, drawRect.width + (m_VSlider ? styles.visualSliderWidth : 0), drawRect.height + (m_HSlider ? styles.visualSliderWidth : 0)); } set { Rect newDrawArea = new Rect(value.x, value.y, value.width - (m_VSlider ? styles.visualSliderWidth : 0), value.height - (m_HSlider ? styles.visualSliderWidth : 0)); if (newDrawArea != m_DrawArea) { if (m_ScaleWithWindow) { m_DrawArea = newDrawArea; shownAreaInsideMargins = m_LastShownAreaInsideMargins; } else { m_Translation += new Vector2((newDrawArea.width - m_DrawArea.width) / 2, (newDrawArea.height - m_DrawArea.height) / 2); m_DrawArea = newDrawArea; } } EnforceScaleAndRange(); } } public Rect drawRect { get { return m_DrawArea; } } public void SetShownHRangeInsideMargins(float min, float max) { m_Scale.x = (drawRect.width - leftmargin - rightmargin) / (max - min); m_Translation.x = -min * m_Scale.x + leftmargin; EnforceScaleAndRange(); } public void SetShownHRange(float min, float max) { m_Scale.x = drawRect.width / (max - min); m_Translation.x = -min * m_Scale.x; EnforceScaleAndRange(); } public void SetShownVRangeInsideMargins(float min, float max) { m_Scale.y = -(drawRect.height - topmargin - bottommargin) / (max - min); m_Translation.y = drawRect.height - min * m_Scale.y - topmargin; EnforceScaleAndRange(); } public void SetShownVRange(float min, float max) { m_Scale.y = -drawRect.height / (max - min); m_Translation.y = drawRect.height - min * m_Scale.y; EnforceScaleAndRange(); } // ShownArea is in curve space public Rect shownArea { set { m_Scale.x = drawRect.width / value.width; m_Scale.y = -drawRect.height / value.height; m_Translation.x = -value.x * m_Scale.x; m_Translation.y = drawRect.height - value.y * m_Scale.y; EnforceScaleAndRange(); } get { return new Rect( -m_Translation.x / m_Scale.x, -(m_Translation.y - drawRect.height) / m_Scale.y, drawRect.width / m_Scale.x, drawRect.height / -m_Scale.y ); } } public Rect shownAreaInsideMargins { set { shownAreaInsideMarginsInternal = value; EnforceScaleAndRange(); } get { return shownAreaInsideMarginsInternal; } } private Rect shownAreaInsideMarginsInternal { set { m_Scale.x = (drawRect.width - leftmargin - rightmargin) / value.width; m_Scale.y = -(drawRect.height - topmargin - bottommargin) / value.height; m_Translation.x = -value.x * m_Scale.x + leftmargin; m_Translation.y = drawRect.height - value.y * m_Scale.y - topmargin; } get { float leftmarginRel = leftmargin / m_Scale.x; float rightmarginRel = rightmargin / m_Scale.x; float topmarginRel = topmargin / m_Scale.y; float bottommarginRel = bottommargin / m_Scale.y; Rect area = shownArea; area.x += leftmarginRel; area.y -= topmarginRel; area.width -= leftmarginRel + rightmarginRel; area.height += topmarginRel + bottommarginRel; return area; } } public virtual Bounds drawingBounds { get { return new Bounds( new Vector3((hBaseRangeMin + hBaseRangeMax) * 0.5f, (vBaseRangeMin + vBaseRangeMax) * 0.5f, 0), new Vector3(hBaseRangeMax - hBaseRangeMin, vBaseRangeMax - vBaseRangeMin, 1) ); } } // Utility transform functions public Matrix4x4 drawingToViewMatrix { get { return Matrix4x4.TRS(m_Translation, Quaternion.identity, new Vector3(m_Scale.x, m_Scale.y, 1)); } } public Vector2 DrawingToViewTransformPoint(Vector2 lhs) { return new Vector2(lhs.x * m_Scale.x + m_Translation.x, lhs.y * m_Scale.y + m_Translation.y); } public Vector3 DrawingToViewTransformPoint(Vector3 lhs) { return new Vector3(lhs.x * m_Scale.x + m_Translation.x, lhs.y * m_Scale.y + m_Translation.y, 0); } public Vector2 ViewToDrawingTransformPoint(Vector2 lhs) { return new Vector2((lhs.x - m_Translation.x) / m_Scale.x, (lhs.y - m_Translation.y) / m_Scale.y); } public Vector3 ViewToDrawingTransformPoint(Vector3 lhs) { return new Vector3((lhs.x - m_Translation.x) / m_Scale.x, (lhs.y - m_Translation.y) / m_Scale.y, 0); } public Vector2 DrawingToViewTransformVector(Vector2 lhs) { return new Vector2(lhs.x * m_Scale.x, lhs.y * m_Scale.y); } public Vector3 DrawingToViewTransformVector(Vector3 lhs) { return new Vector3(lhs.x * m_Scale.x, lhs.y * m_Scale.y, 0); } public Vector2 ViewToDrawingTransformVector(Vector2 lhs) { return new Vector2(lhs.x / m_Scale.x, lhs.y / m_Scale.y); } public Vector3 ViewToDrawingTransformVector(Vector3 lhs) { return new Vector3(lhs.x / m_Scale.x, lhs.y / m_Scale.y, 0); } public Vector2 mousePositionInDrawing { get { return ViewToDrawingTransformPoint(Event.current.mousePosition); } } public Vector2 NormalizeInViewSpace(Vector2 vec) { vec = Vector2.Scale(vec, m_Scale); vec /= vec.magnitude; return Vector2.Scale(vec, new Vector2(1 / m_Scale.x, 1 / m_Scale.y)); } // Utility mouse event functions private bool IsZoomEvent() { return ( (Event.current.button == 1 && Event.current.alt) // right+alt drag //|| (Event.current.button == 0 && Event.current.command) // left+commend drag //|| (Event.current.button == 2 && Event.current.command) // middle+command drag ); } private bool IsPanEvent() { return ( (Event.current.button == 0 && Event.current.alt) // left+alt drag || (Event.current.button == 2 && !Event.current.command) // middle drag ); } public ZoomArea() { m_MinimalGUI = false; } public ZoomArea(bool minimalGUI) { m_MinimalGUI = minimalGUI; } public void BeginViewGUI() { if (styles.horizontalScrollbar == null) styles.InitGUIStyles(m_MinimalGUI); GUILayout.BeginArea(m_DrawArea, styles.background); HandleZoomAndPanEvents(m_DrawArea); GUILayout.EndArea(); } public void HandleZoomAndPanEvents(Rect area) { area.x = 0; area.y = 0; int id = GUIUtility.GetControlID(zoomableAreaHash, FocusType.Passive, area); switch (Event.current.GetTypeForControl(id)) { case EventType.MouseDown: if (area.Contains(Event.current.mousePosition)) { // Catch keyboard control when clicked inside zoomable area // (used to restrict scrollwheel) GUIUtility.keyboardControl = id; if (IsZoomEvent() || IsPanEvent()) { GUIUtility.hotControl = id; m_MouseDownPosition = mousePositionInDrawing; Event.current.Use(); } } break; case EventType.MouseUp: //Debug.Log("mouse-up!"); if (GUIUtility.hotControl == id) { GUIUtility.hotControl = 0; // If we got the mousedown, the mouseup is ours as well // (no matter if the click was in the area or not) m_MouseDownPosition = new Vector2(-1000000, -1000000); //Event.current.Use(); } break; case EventType.MouseDrag: if (GUIUtility.hotControl != id) break; if (IsZoomEvent()) { // Zoom in around mouse down position Zoom(m_MouseDownPosition, false); Event.current.Use(); } else if (IsPanEvent()) { // Pan view Pan(); Event.current.Use(); } break; case EventType.ScrollWheel: if (!area.Contains(Event.current.mousePosition)) break; if (m_IgnoreScrollWheelUntilClicked && GUIUtility.keyboardControl != id) break; // Zoom in around cursor position Zoom(mousePositionInDrawing, true); Event.current.Use(); break; } } public void EndViewGUI() { } private void Pan() { if (!m_HRangeLocked) m_Translation.x += Event.current.delta.x; if (!m_VRangeLocked) m_Translation.y += Event.current.delta.y; EnforceScaleAndRange(); } private void Zoom(Vector2 zoomAround, bool scrollwhell) { // Get delta (from scroll wheel or mouse pad) // Add x and y delta to cover all cases // (scrool view has only y or only x when shift is pressed, // while mouse pad has both x and y at all times) float delta = Event.current.delta.x + Event.current.delta.y; if (scrollwhell) delta = -delta; // Scale multiplier. Don't allow scale of zero or below! float scale = Mathf.Max(0.01F, 1 + delta * 0.01F); if (!m_HRangeLocked && !Event.current.shift) { // Offset to make zoom centered around cursor position m_Translation.x -= zoomAround.x * (scale - 1) * m_Scale.x; // Apply zooming m_Scale.x *= scale; } if (!m_VRangeLocked && !EditorGUI.actionKey) { // Offset to make zoom centered around cursor position m_Translation.y -= zoomAround.y * (scale - 1) * m_Scale.y; // Apply zooming m_Scale.y *= scale; } EnforceScaleAndRange(); } public void EnforceScaleAndRange() { float hScaleMin = m_HScaleMin; float vScaleMin = m_VScaleMin; float hScaleMax = m_HScaleMax; float vScaleMax = m_VScaleMax; if (hRangeMax != Mathf.Infinity && hRangeMin != Mathf.NegativeInfinity) hScaleMax = Mathf.Min(m_HScaleMax, hRangeMax - hRangeMin); if (vRangeMax != Mathf.Infinity && vRangeMin != Mathf.NegativeInfinity) vScaleMax = Mathf.Min(m_VScaleMax, vRangeMax - vRangeMin); Rect oldArea = m_LastShownAreaInsideMargins; Rect newArea = shownAreaInsideMargins; if (newArea == oldArea) return; float epsilon = 0.00001f; if (newArea.width < oldArea.width - epsilon) { float xLerp = Mathf.InverseLerp(oldArea.width, newArea.width, hScaleMin); newArea = new Rect( Mathf.Lerp(oldArea.x, newArea.x, xLerp), newArea.y, Mathf.Lerp(oldArea.width, newArea.width, xLerp), newArea.height ); } if (newArea.height < oldArea.height - epsilon) { float yLerp = Mathf.InverseLerp(oldArea.height, newArea.height, vScaleMin); newArea = new Rect( newArea.x, Mathf.Lerp(oldArea.y, newArea.y, yLerp), newArea.width, Mathf.Lerp(oldArea.height, newArea.height, yLerp) ); } if (newArea.width > oldArea.width + epsilon) { float xLerp = Mathf.InverseLerp(oldArea.width, newArea.width, hScaleMax); newArea = new Rect( Mathf.Lerp(oldArea.x, newArea.x, xLerp), newArea.y, Mathf.Lerp(oldArea.width, newArea.width, xLerp), newArea.height ); } if (newArea.height > oldArea.height + epsilon) { float yLerp = Mathf.InverseLerp(oldArea.height, newArea.height, vScaleMax); newArea = new Rect( newArea.x, Mathf.Lerp(oldArea.y, newArea.y, yLerp), newArea.width, Mathf.Lerp(oldArea.height, newArea.height, yLerp) ); } // Enforce ranges if (newArea.xMin < hRangeMin) newArea.x = hRangeMin; if (newArea.xMax > hRangeMax) newArea.x = hRangeMax - newArea.width; if (newArea.yMin < vRangeMin) newArea.y = vRangeMin; if (newArea.yMax > vRangeMax) newArea.y = vRangeMax - newArea.height; shownAreaInsideMarginsInternal = newArea; m_LastShownAreaInsideMargins = newArea; } public float PixelToTime(float pixelX, Rect rect) { return ((pixelX - rect.x) * shownArea.width / rect.width + shownArea.x); } public float TimeToPixel(float time, Rect rect) { return (time - shownArea.x) / shownArea.width * rect.width + rect.x; } public float PixelDeltaToTime(Rect rect) { return shownArea.width / rect.width; } }