Search Unity

A simple Plug n Play Pan & Zoom class for EditorWindows (196 line long code)

Discussion in 'Immediate Mode GUI (IMGUI)' started by DaiMangouDev, Jul 8, 2017.

  1. DaiMangouDev

    DaiMangouDev

    Joined:
    Mar 26, 2014
    Posts:
    33
    So after looking at various examples of handling Pan and Zoom inside of editor windows. I have noted three techniques that can be used to achiever pan and zoom ...but not without annoying side-effects or cluttered code in the back end.
    Needless to say My code that i spent 2 days working on has its own issues which i would like your help with .
    If we can get this working right then it could be helpful to many (i think) .

    by now , if you have tried making pan and zoom systems than you have most likely come across

    the RectExtension class and its Rect overloads :D we all just use them after briefly reading through the code .
    I called it RectExt
    it contains extension methods for setting the values of a Rect

    download the code and run it in unity . Tools > TestEditor .

    if you can help make the zoom happen at the mouse position instead of the uppler left cornner , It would help not just me but everyone who needed a code like this.

    Code (CSharp):
    1.        
    2.  
    3.  
    4.  
    5. [code=CSharp]using System;
    6. using System.Collections.Generic;
    7. using System.IO;
    8. using System.Linq;
    9. using UnityEditor;
    10. using UnityEngine;
    11.  
    12. namespace Teststuff
    13. {
    14.  
    15.     public static class RectExt
    16.     {
    17.         public static Vector2 TopLeft(this Rect rect)
    18.         {
    19.             return new Vector2(rect.xMin, rect.yMin);
    20.         }
    21.  
    22.         public static Rect ScaleSizeBy(this Rect rect, float scale)
    23.         {
    24.             return rect.ScaleSizeBy(scale, rect.center);
    25.         }
    26.  
    27.         public static Rect ScaleSizeBy(this Rect rect, float scale, Vector2 pivotPoint)
    28.         {
    29.             var result = rect;
    30.             result.x -= pivotPoint.x;
    31.             result.y -= pivotPoint.y;
    32.             result.xMin *= scale;
    33.             result.xMax *= scale;
    34.             result.yMin *= scale;
    35.             result.yMax *= scale;
    36.             result.x += pivotPoint.x;
    37.             result.y += pivotPoint.y;
    38.             return result;
    39.         }
    40.  
    41.         public static Rect ScaleSizeBy(this Rect rect, Vector2 scale)
    42.         {
    43.             return rect.ScaleSizeBy(scale, rect.center);
    44.         }
    45.  
    46.         public static Rect ScaleSizeBy(this Rect rect, Vector2 scale, Vector2 pivotPoint)
    47.         {
    48.             var result = rect;
    49.             result.x -= pivotPoint.x;
    50.             result.y -= pivotPoint.y;
    51.             result.xMin *= scale.x;
    52.             result.xMax *= scale.x;
    53.             result.yMin *= scale.y;
    54.             result.yMax *= scale.y;
    55.             result.x += pivotPoint.x;
    56.             result.y += pivotPoint.y;
    57.             return result;
    58.         }
    59.     }
    60.  
    61.  
    62. //HERE IS THE FUN STUFF
    63.  
    64.     #region PanAndZoom
    65.     [Serializable]
    66.     public class PanAndZoom
    67.     {
    68.         #region Variables
    69.  
    70.         private Vector2 Offset = Vector2.zero;
    71.         public Vector2 Pan = new Vector2(0, 0);
    72.         public float zoom = 1;
    73.         private Vector2 PanOffset;
    74.         private Matrix4x4 matrix;
    75.         private Matrix4x4 normalMatrix = Matrix4x4.TRS(new Vector3(), Quaternion.identity, Vector3.one);
    76.         private Vector2 mousePos;
    77.         #endregion
    78.  
    79.         #region Begin End Area
    80.  
    81.         public void BeginArea(Rect AreaRect, float min, float max, bool resetPan)
    82.         {
    83.             GUI.EndGroup();
    84.             #region Pan
    85.             if (Offset == Vector2.zero && Event.current.rawType == EventType.MouseDown)
    86.                 Offset = Event.current.mousePosition;
    87.  
    88.             if (Event.current.rawType == EventType.MouseDrag && (Event.current.button == 2 || Event.current.alt))
    89.             {
    90.                 Pan = Event.current.mousePosition - Offset + PanOffset;
    91.                 Event.current.Use();
    92.             }
    93.  
    94.             if (Event.current.rawType == EventType.MouseUp)
    95.             {
    96.                 Offset = Vector2.zero;
    97.                 PanOffset = Pan;
    98.             }
    99.             #endregion
    100.  
    101.             #region zoom
    102.             if (Event.current.type == EventType.ScrollWheel)
    103.             {
    104.                 if (mousePos != Event.current.mousePosition)
    105.                     mousePos = Event.current.mousePosition;
    106.  
    107.                 Vector2 delta = Event.current.delta;
    108.  
    109.  
    110.                 float zoomDelta = -delta.y / 150.0f;
    111.                 zoom += zoomDelta * 4;
    112.                 zoom = Mathf.Clamp(zoom, min, max);
    113.  
    114.                 Event.current.Use();
    115.             }
    116.  
    117.             var rect = AreaRect.ScaleSizeBy(1f / zoom, AreaRect.TopLeft());
    118.             rect.y += 21;
    119.             AreaRect = rect;
    120.  
    121.             Matrix4x4 trs = Matrix4x4.TRS(AreaRect.TopLeft(), Quaternion.identity, Vector3.one);
    122.             Matrix4x4 scale = Matrix4x4.Scale(new Vector3(zoom, zoom, zoom));
    123.  
    124.             // once we begin zooming out , the pan speed of the content zoomed and the unzoomed canvas (if your canvas size wont change ) will pan slower, so we incleae its pan speed by * (1f / zoom)
    125.  
    126.             GUI.BeginClip(AreaRect, (Pan * (1f / zoom)), Vector2.zero, resetPan);
    127.  
    128.             GUI.matrix = trs * scale * trs.inverse * GUI.matrix;
    129.  
    130.             // this is temporarly used as a reference for the window position and scale
    131.             GUI.Box(AreaRect,"I am  the size of the window");
    132.  
    133.             #endregion
    134.         }
    135.  
    136.  
    137.         public void EndArea()
    138.         {
    139.             GUI.matrix = normalMatrix;
    140.             GUI.EndClip();
    141.             GUI.BeginGroup(new Rect(0f, 21, Screen.width, Screen.height));
    142.  
    143.  
    144.  
    145.         }
    146.  
    147.         #endregion
    148.  
    149.     }
    150.  
    151.     #endregion
    152.  
    153.  
    154.  
    155.     //USAGE
    156.  
    157.  
    158.  
    159.     [Serializable]
    160.     public class TestEditor : EditorWindow
    161.     {
    162.  
    163.         private PanAndZoom panAndZoom;
    164.  
    165.         [MenuItem("Tools/TestEditor")]
    166.         static void Init()
    167.         {
    168.             EditorWindow shootEditor = GetWindow<TestEditor>();
    169.             shootEditor.titleContent.text = "TestEditorr";
    170.  
    171.         }
    172.  
    173.         public void OnEnable()
    174.         {
    175.             if (panAndZoom == null)
    176.             {
    177.  
    178.                 panAndZoom = new PanAndZoom();
    179.             }
    180.         }
    181.  
    182.  
    183.         public void OnGUI()
    184.         {
    185.  
    186.             panAndZoom.BeginArea(new Rect(0, 0, Screen.width, Screen.height), 0.3f, 1, false);
    187.  
    188.             GUI.Box(new Rect(300, 200, 100, 40), " i do nothing");
    189.             GUI.Box(new Rect(600, 350, 100, 40), " i do nothing either");
    190.  
    191.             panAndZoom.EndArea();
    192.  
    193.         }
    194.  
    195.     }
    196. }
    result

    No zoom


    With zoom
     

    Attached Files:

    Last edited: Jul 8, 2017
  2. DaiMangouDev

    DaiMangouDev

    Joined:
    Mar 26, 2014
    Posts:
    33
    OH also you can use this zoom system which allows you to zoom at mouse position but be warned . you will have nightmare without end .

    just replace the zoom region the ihte first code with this zoom region

    Code (CSharp):
    1.             #region zoom
    2.             if (Event.current.type == EventType.ScrollWheel)
    3.             {
    4.                 if (mousePos != Event.current.mousePosition)
    5.                     mousePos = Event.current.mousePosition;
    6.  
    7.                 Vector2 delta = Event.current.delta;
    8.                 float zoomDelta = -delta.y / 150.0f;
    9.                 zoom += zoomDelta * 4;
    10.                 zoom = Mathf.Clamp(zoom, min, max);
    11.                 Event.current.Use();
    12.             }
    13.  
    14.  
    15.             var rect = AreaRect.ScaleSizeBy(1f / zoom, AreaRect.TopLeft());
    16.             rect.y += 21;
    17.             AreaRect = rect;
    18.  
    19.             GUI.BeginClip(AreaRect, (Pan), Vector2.zero, resetPan);
    20.             GUIUtility.ScaleAroundPivot(new Vector2(zoom, zoom), mousePos);// * (1f / zoom)
    21.  
    22.  
    23.  
    24.             GUI.DrawTexture(AreaRect, Textures.gray);
    25.            
    26.             #endregion
     
  3. Seneral

    Seneral

    Joined:
    Jun 2, 2014
    Posts:
    1,206
    Hi,
    Maybe you have not come across this problem yet, but if you zoom out, the clipping rect stays the same so everything outside of the normal unzoomed rect is invisible, because the GUI system unfortunately does not account for the clipping rect.
    After facing that problem (which you'll likely do at some point) you've to basically start over from scratch.
    I've implemented GUIScaleUtility for that purpose, which hacks into the grouping system and basically destroys all groups when beginning the scaling rect, renders the scaled GUI in an extended rect that is not clipped, and then restores all groups afterwards. That also allows for easy zom targets and panning.
    This fixes the scaling rect but adds another small problem, to account for the extended rects you need to offset all embedded scaled GUI elements by a calculated amount. But for the framework that was no big deal as the nodes and stuff need to be offsetted by pan either way.
    It's used in the Node Editor Framework, but you can download it from my gist, too:
    https://gist.github.com/Seneral/2c8e7dfe712b9f53c60f80722fbce5bd

    Seneral
     
  4. DaiMangouDev

    DaiMangouDev

    Joined:
    Mar 26, 2014
    Posts:
    33
    This is awesome! , i'm glad that I didn't go ahead and build out a messy framework for my pan and zoom and then have to start over .

    This is great , i''m going to do implementation ASAP.

    Looks like you had a whole lot of really difficult work to do .
    life of a programmer:(
     
  5. Seneral

    Seneral

    Joined:
    Jun 2, 2014
    Posts:
    1,206
    Hehe yes it was, that alone took me like 1 month nonstop work (3-6hrs a day usually)...
    Hope you'll be able to use it quickly, too:)
     
  6. DaiMangouDev

    DaiMangouDev

    Joined:
    Mar 26, 2014
    Posts:
    33
    yeahhh about that hahaha I am lost . I was reading the code and keep losing track of how the various functions connect.

    Do you have a snippet us use in editor window

    of just edit this
    Code (CSharp):
    1.     [Serializable]
    2.     public class TestEditor : EditorWindow
    3.     {
    4.         private PanAndZoom panAndZoom;
    5.         [MenuItem("Tools/TestEditor")]
    6.         static void Init()
    7.         {
    8.             EditorWindow shootEditor = GetWindow<TestEditor>();
    9.             shootEditor.titleContent.text = "TestEditorr";
    10.         }
    11.         public void OnEnable()
    12.         {
    13.             if (panAndZoom == null)
    14.             {
    15.                 panAndZoom = new PanAndZoom();
    16.             }
    17.         }
    18.         public void OnGUI()
    19.         {
    20. // begin pan and zoom
    21.  
    22.             GUI.Box(new Rect(300, 200, 100, 40), " i do nothing");
    23.             GUI.Box(new Rect(600, 350, 100, 40), " i do nothing either");
    24.  
    25.            // end pan and zoom
    26.         }
    27.     }
     
  7. Seneral

    Seneral

    Joined:
    Jun 2, 2014
    Posts:
    1,206
    Sure, as I said I used it in my Node Editor Framework. Take a look at this draw function to get an idea.
    Basically, you use the function to start a scaling area:
    Code (csharp):
    1. // Params: scale rectangle, zoom position, zoom value, is editor window?, adjust GUILayout with offset automatically?
    2. zoomPanAdjust = GUIScaleUtility.BeginScale (ref canvasRect, canvasRect.size/2, zoom, true, false);
    3. // SCALED AREA
    4. GUIScaleUtility.EndScale ();
    The returned value zoomPanAdjust has to be applied to all embedded GUI controls, the last parameter allows to automatically adjust embedded GUILayout controls to be adjusted... The editor window bool is only used for the header bar, for some reason it is not found in any grouping, so it has to be added manually.
    The adjusted ref rect is the new rectangle in scaled space that your GUI controls can occupy: (0,0,maxScaleX,maxScaleY).
    Apart from that, the rest should be obvious. Of course there are a lot of other functionalities aswell but for a start that is the basics.
    You need special space transformation functions for Input and GUI though, may need to look into the framework for that, most noticeably GUIToScreenSpace...
    Hope that clears it up:)
    Seneral
     
    DaiMangouDev likes this.
  8. DaiMangouDev

    DaiMangouDev

    Joined:
    Mar 26, 2014
    Posts:
    33
    @Seneral thanks for the information, this should help others also :)
     
  9. DaiMangouDev

    DaiMangouDev

    Joined:
    Mar 26, 2014
    Posts:
    33
    @Seneral , two really important questions.

    when you instance up to 300 nodes in your node editor , do you see any significant drop in speed at which individual nodes can be dragged ?
     
  10. Seneral

    Seneral

    Joined:
    Jun 2, 2014
    Posts:
    1,206
    Why do you ask? I can't imagine why, so I'd answer no, although I did not test...
     
  11. Seneral

    Seneral

    Joined:
    Jun 2, 2014
    Posts:
    1,206
    Didn't count them but they are not lagging or creating any other problems for me, so... :)
    Screenshot6.jpg
     
  12. DaiMangouDev

    DaiMangouDev

    Joined:
    Mar 26, 2014
    Posts:
    33

    I ask because during Reapint(); when all the nodes are redrwan there is a spike in garbage and and you will see the amount of MS it takes a frame to run thorough each function that makes up your nodes .

    so I constantly Reapint(); each frame so that when i look through my profiler in DeepProfile mode , i can see what parts of my code are taking long to execute and are generating garbage.

    my nodes will get dragged about at half speed if I had about 30 of them on canvas , so i wanted to know i hat is the same for you .
     
  13. Seneral

    Seneral

    Joined:
    Jun 2, 2014
    Posts:
    1,206
    Hm, will take a look at that when I have time...
    Can we continue on the appropriate thread for the framework? Just to keep everything organized:)
     
  14. DaiMangouDev

    DaiMangouDev

    Joined:
    Mar 26, 2014
    Posts:
    33
    definately :)
     
  15. DaiMangouDev

    DaiMangouDev

    Joined:
    Mar 26, 2014
    Posts:
    33
    I couldnt get that to work and I coudnt afford to stress over the code so I looked at my code again and made some changes.
    Than pan is fine and the zoom works better. But it needs Some fixings up

    anyone have any Ideas on how to fix the zoom.
    Code (CSharp):
    1.   public class PanAndZoom
    2.     {
    3.         #region Variables
    4.  
    5.         private Vector2 Offset = Vector2.zero;
    6.         public Vector2 Pan = new Vector2(0, 0);
    7.         public float zoom = 1;
    8.         private Vector2 PanOffset;
    9.         private Matrix4x4 matrix;
    10.         private Matrix4x4 normalMatrix = Matrix4x4.TRS(new Vector3(), Quaternion.identity, Vector3.one);
    11.         private Vector2 mousePos;
    12.  
    13.         #endregion
    14.  
    15.         #region Begin End Area
    16.  
    17.         public void BeginArea(Rect AreaRect, float min, float max, bool resetPan)
    18.         {
    19.             GUI.EndGroup();
    20.             #region Pan
    21.             if (Offset == Vector2.zero && Event.current.rawType == EventType.MouseDown)
    22.                 Offset = Event.current.mousePosition;
    23.  
    24.             if (Event.current.rawType == EventType.MouseDrag && (Event.current.button == 2 || Event.current.alt))
    25.             {
    26.                 Pan = Event.current.mousePosition - Offset + PanOffset;
    27.                 Event.current.Use();
    28.             }
    29.  
    30.             if (Event.current.rawType == EventType.MouseUp)
    31.             {
    32.                 Offset = Vector2.zero;
    33.                 PanOffset = Pan;
    34.             }
    35.             #endregion
    36.  
    37.  
    38.        #region zoom
    39.             if (Event.current.type == EventType.ScrollWheel )
    40.             {
    41.                 if (mousePos != Event.current.mousePosition)
    42.                     mousePos = Event.current.mousePosition;
    43.  
    44.                 Vector2 delta = Event.current.delta;
    45.  
    46.  
    47.                 float zoomDelta = -delta.y / 150.0f;
    48.                 zoom += zoomDelta * 4;
    49.                 zoom = Mathf.Clamp(zoom, min, max);
    50.  
    51.                 Event.current.Use();
    52.             }
    53.  
    54.  
    55.             Matrix4x4 matrix = GUI.matrix;
    56.             Matrix4x4 lhs = Matrix4x4.TRS(mousePos, Quaternion.identity, new Vector3(zoom, zoom, 1f)) * Matrix4x4.TRS(-mousePos, Quaternion.identity, Vector3.one);
    57.             GUI.matrix = lhs * matrix;
    58.  
    59.  
    60.             var rect = AreaRect.ScaleSizeBy(1f / zoom, AreaRect.min);
    61.             rect.y += 21;
    62.             AreaRect = rect;
    63.             GUI.BeginClip(AreaRect, (Pan * (1f / zoom)), Vector2.zero, resetPan);
    64.  
    65.            
    66.             #endregion
    67.  
    68.  
    69.         }
    70.  
    71.  
    72.         public void EndArea()
    73.         {
    74.              GUI.matrix = normalMatrix;
    75.              GUI.EndClip();
    76.              GUI.BeginGroup(new Rect(0f, 21, Screen.width, Screen.height));
    77.  
    78.  
    79.  
    80.         }
    81.  
    82.         #endregion
    83.  
    84.     }
    85.  
     
  16. waheyluggage

    waheyluggage

    Joined:
    Aug 22, 2015
    Posts:
    15
    Hello

    I'm using this in a little tool but I'm trying to draw an arrow (which is a GUITexture) that sits on the line and points between the nodes. The code below works perfectly when I'm not zoomed in (so scale is 1.0f) and as I pan around. If I zoom in though and the arrows all get offset. The size of the arrow is correct though. The startpos and endpos are also used to draw the line and they appear correctly. If I remove the call to RotateAroundPivot the arrows zoom in and stay in the right place (they just don't rotate to match the line angle).

    During the BeginScaled section I'm doing...

    Code (CSharp):
    1.  
    2. // startPos and endPos are the node start and end, these have the GuiOffset applied
    3. // how far along is 0.5 to draw it at 50% along the line
    4.  
    5. // make a copy of the current GUI matrix.
    6. Matrix4x4 matrixBackup = GUI.matrix;
    7.  
    8. // get the position of the arrow, we want to draw the arrow's middle at this point
    9. Vector3 pos = startPos + ((endPos - startPos) * howFarAlong);
    10.  
    11. // offset it by half the texture width and height, so the middle is over our target point.
    12. pos.x -= ((arrowTexture.width) / 2);
    13. pos.y -= ((arrowTexture.height) / 2);
    14.  
    15. // get the angle to draw the arrow at
    16. float degrees = Mathf.Atan2(endPos.y - startPos.y, endPos.x - startPos.x) * Mathf.Rad2Deg;
    17.  
    18. // create a rectangle for it
    19. Rect r = new Rect(pos.x, pos.y, arrowTexture.width, arrowTexture.height);
    20.  
    21. // rotate it around the middle of the rect
    22. GUIUtility.RotateAroundPivot(degrees, r.center);
    23.  
    24. // and finally draw it
    25. GUI.DrawTexture(r, arrowTexture, ScaleMode.ScaleToFit, true, 0.0f);
    26.  
    27. // put the original matrix back
    28. GUI.matrix = matrixBackup;
    29.  
    Any ideas?