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

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 :smile: 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]using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;

namespace Teststuff
{

    public static class RectExt
    {
        public static Vector2 TopLeft(this Rect rect)
        {
            return new Vector2(rect.xMin, rect.yMin);
        }

        public static Rect ScaleSizeBy(this Rect rect, float scale)
        {
            return rect.ScaleSizeBy(scale, rect.center);
        }

        public static Rect ScaleSizeBy(this Rect rect, float scale, Vector2 pivotPoint)
        {
            var result = rect;
            result.x -= pivotPoint.x;
            result.y -= pivotPoint.y;
            result.xMin *= scale;
            result.xMax *= scale;
            result.yMin *= scale;
            result.yMax *= scale;
            result.x += pivotPoint.x;
            result.y += pivotPoint.y;
            return result;
        }

        public static Rect ScaleSizeBy(this Rect rect, Vector2 scale)
        {
            return rect.ScaleSizeBy(scale, rect.center);
        }

        public static Rect ScaleSizeBy(this Rect rect, Vector2 scale, Vector2 pivotPoint)
        {
            var result = rect;
            result.x -= pivotPoint.x;
            result.y -= pivotPoint.y;
            result.xMin *= scale.x;
            result.xMax *= scale.x;
            result.yMin *= scale.y;
            result.yMax *= scale.y;
            result.x += pivotPoint.x;
            result.y += pivotPoint.y;
            return result;
        }
    }


//HERE IS THE FUN STUFF

    #region PanAndZoom
    [Serializable]
    public class PanAndZoom
    {
        #region Variables

        private Vector2 Offset = Vector2.zero;
        public Vector2 Pan = new Vector2(0, 0);
        public float zoom = 1;
        private Vector2 PanOffset;
        private Matrix4x4 matrix;
        private Matrix4x4 normalMatrix = Matrix4x4.TRS(new Vector3(), Quaternion.identity, Vector3.one);
        private Vector2 mousePos;
        #endregion

        #region Begin End Area

        public void BeginArea(Rect AreaRect, float min, float max, bool resetPan)
        {
            GUI.EndGroup();
            #region Pan
            if (Offset == Vector2.zero && Event.current.rawType == EventType.MouseDown)
                Offset = Event.current.mousePosition;

            if (Event.current.rawType == EventType.MouseDrag && (Event.current.button == 2 || Event.current.alt))
            {
                Pan = Event.current.mousePosition - Offset + PanOffset;
                Event.current.Use();
            }

            if (Event.current.rawType == EventType.MouseUp)
            {
                Offset = Vector2.zero;
                PanOffset = Pan;
            }
            #endregion

            #region zoom
            if (Event.current.type == EventType.ScrollWheel)
            {
                if (mousePos != Event.current.mousePosition)
                    mousePos = Event.current.mousePosition;

                Vector2 delta = Event.current.delta;


                float zoomDelta = -delta.y / 150.0f;
                zoom += zoomDelta * 4;
                zoom = Mathf.Clamp(zoom, min, max);

                Event.current.Use();
            }

            var rect = AreaRect.ScaleSizeBy(1f / zoom, AreaRect.TopLeft());
            rect.y += 21;
            AreaRect = rect;

            Matrix4x4 trs = Matrix4x4.TRS(AreaRect.TopLeft(), Quaternion.identity, Vector3.one);
            Matrix4x4 scale = Matrix4x4.Scale(new Vector3(zoom, zoom, zoom));

            // 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)

            GUI.BeginClip(AreaRect, (Pan * (1f / zoom)), Vector2.zero, resetPan);

            GUI.matrix = trs * scale * trs.inverse * GUI.matrix;

            // this is temporarly used as a reference for the window position and scale
            GUI.Box(AreaRect,"I am  the size of the window");

            #endregion
        }


        public void EndArea()
        {
            GUI.matrix = normalMatrix;
            GUI.EndClip();
            GUI.BeginGroup(new Rect(0f, 21, Screen.width, Screen.height));



        }

        #endregion

    }

    #endregion



    //USAGE



    [Serializable]
    public class TestEditor : EditorWindow
    {

        private PanAndZoom panAndZoom;

        [MenuItem("Tools/TestEditor")]
        static void Init()
        {
            EditorWindow shootEditor = GetWindow<TestEditor>();
            shootEditor.titleContent.text = "TestEditorr";

        }

        public void OnEnable()
        {
            if (panAndZoom == null)
            {

                panAndZoom = new PanAndZoom();
            }
        }


        public void OnGUI()
        {

            panAndZoom.BeginArea(new Rect(0, 0, Screen.width, Screen.height), 0.3f, 1, false);

            GUI.Box(new Rect(300, 200, 100, 40), " i do nothing");
            GUI.Box(new Rect(600, 350, 100, 40), " i do nothing either");

            panAndZoom.EndArea();

        }

    }
}

result

No zoom

With zoom

3137328–237904–TestEditor.cs (5.2 KB)

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

            #region zoom
            if (Event.current.type == EventType.ScrollWheel)
            {
                if (mousePos != Event.current.mousePosition)
                    mousePos = Event.current.mousePosition;

                Vector2 delta = Event.current.delta;
                float zoomDelta = -delta.y / 150.0f;
                zoom += zoomDelta * 4;
                zoom = Mathf.Clamp(zoom, min, max);
                Event.current.Use();
            }


            var rect = AreaRect.ScaleSizeBy(1f / zoom, AreaRect.TopLeft());
            rect.y += 21;
            AreaRect = rect;

            GUI.BeginClip(AreaRect, (Pan), Vector2.zero, resetPan);
            GUIUtility.ScaleAroundPivot(new Vector2(zoom, zoom), mousePos);// * (1f / zoom)



            GUI.DrawTexture(AreaRect, Textures.gray);
           
            #endregion

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

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:(

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:)

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

    [Serializable]
    public class TestEditor : EditorWindow
    {
        private PanAndZoom panAndZoom;
        [MenuItem("Tools/TestEditor")]
        static void Init()
        {
            EditorWindow shootEditor = GetWindow<TestEditor>();
            shootEditor.titleContent.text = "TestEditorr";
        }
        public void OnEnable()
        {
            if (panAndZoom == null)
            {
                panAndZoom = new PanAndZoom();
            }
        }
        public void OnGUI()
        {
// begin pan and zoom

            GUI.Box(new Rect(300, 200, 100, 40), " i do nothing");
            GUI.Box(new Rect(600, 350, 100, 40), " i do nothing either");

           // end pan and zoom
        }
    }

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:

// Params: scale rectangle, zoom position, zoom value, is editor window?, adjust GUILayout with offset automatically?
zoomPanAdjust = GUIScaleUtility.BeginScale (ref canvasRect, canvasRect.size/2, zoom, true, false);
// SCALED AREA
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

1 Like

@Seneral_1 thanks for the information, this should help others also :slight_smile:

@Seneral_1 , 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 ?

Why do you ask? I can’t imagine why, so I’d answer no, although I did not test…

Didn’t count them but they are not lagging or creating any other problems for me, so… :slight_smile:

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 .

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:)

definately :slight_smile:

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.

  public class PanAndZoom
    {
        #region Variables

        private Vector2 Offset = Vector2.zero;
        public Vector2 Pan = new Vector2(0, 0);
        public float zoom = 1;
        private Vector2 PanOffset;
        private Matrix4x4 matrix;
        private Matrix4x4 normalMatrix = Matrix4x4.TRS(new Vector3(), Quaternion.identity, Vector3.one);
        private Vector2 mousePos;

        #endregion

        #region Begin End Area

        public void BeginArea(Rect AreaRect, float min, float max, bool resetPan)
        {
            GUI.EndGroup();
            #region Pan
            if (Offset == Vector2.zero && Event.current.rawType == EventType.MouseDown)
                Offset = Event.current.mousePosition;

            if (Event.current.rawType == EventType.MouseDrag && (Event.current.button == 2 || Event.current.alt))
            {
                Pan = Event.current.mousePosition - Offset + PanOffset;
                Event.current.Use();
            }

            if (Event.current.rawType == EventType.MouseUp)
            {
                Offset = Vector2.zero;
                PanOffset = Pan;
            }
            #endregion


       #region zoom
            if (Event.current.type == EventType.ScrollWheel )
            {
                if (mousePos != Event.current.mousePosition)
                    mousePos = Event.current.mousePosition;

                Vector2 delta = Event.current.delta;


                float zoomDelta = -delta.y / 150.0f;
                zoom += zoomDelta * 4;
                zoom = Mathf.Clamp(zoom, min, max);

                Event.current.Use();
            }


            Matrix4x4 matrix = GUI.matrix;
            Matrix4x4 lhs = Matrix4x4.TRS(mousePos, Quaternion.identity, new Vector3(zoom, zoom, 1f)) * Matrix4x4.TRS(-mousePos, Quaternion.identity, Vector3.one);
            GUI.matrix = lhs * matrix;


            var rect = AreaRect.ScaleSizeBy(1f / zoom, AreaRect.min);
            rect.y += 21;
            AreaRect = rect;
            GUI.BeginClip(AreaRect, (Pan * (1f / zoom)), Vector2.zero, resetPan);

           
            #endregion

 
        }


        public void EndArea()
        {
             GUI.matrix = normalMatrix;
             GUI.EndClip();
             GUI.BeginGroup(new Rect(0f, 21, Screen.width, Screen.height));



        }

        #endregion

    }

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…

// startPos and endPos are the node start and end, these have the GuiOffset applied
// how far along is 0.5 to draw it at 50% along the line

// make a copy of the current GUI matrix.
Matrix4x4 matrixBackup = GUI.matrix;

// get the position of the arrow, we want to draw the arrow's middle at this point
Vector3 pos = startPos + ((endPos - startPos) * howFarAlong);

// offset it by half the texture width and height, so the middle is over our target point.
pos.x -= ((arrowTexture.width) / 2);
pos.y -= ((arrowTexture.height) / 2);

// get the angle to draw the arrow at
float degrees = Mathf.Atan2(endPos.y - startPos.y, endPos.x - startPos.x) * Mathf.Rad2Deg;

// create a rectangle for it
Rect r = new Rect(pos.x, pos.y, arrowTexture.width, arrowTexture.height);

// rotate it around the middle of the rect
GUIUtility.RotateAroundPivot(degrees, r.center);

// and finally draw it
GUI.DrawTexture(r, arrowTexture, ScaleMode.ScaleToFit, true, 0.0f);

// put the original matrix back
GUI.matrix = matrixBackup;

Any ideas?