Skip to content

Instantly share code, notes, and snippets.

@bshishov
Last active October 28, 2023 18:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bshishov/f8f8529d9253b5c4b0937e6ee73404a7 to your computer and use it in GitHub Desktop.
Save bshishov/f8f8529d9253b5c4b0937e6ee73404a7 to your computer and use it in GitHub Desktop.
Unity Editor utility to add clippable pan&zoom area to EditorWindow in UGUI
// Copyright 2023 Boris Shishov (github: @bshishov)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
// associated documentation files (the “Software”), to deal in the Software without restriction,
// including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial
// portions of the Software.
//
// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
// OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
using UnityEngine;
namespace GUIExtensions
{
public static class Viewport
{
private const float KEditorWindowTabHeight = 20f; // height of editor window top-bar with tab
private static Matrix4x4 _backupMatrix;
private static bool _isDragging;
/// <summary>
/// Begins a viewport. Everything drawn between Begin() and End() would be transformed w.r.t. virtual camera position and zoom.
/// Canvas space coordinates (0, 0) are at the center of the screen if cameraCenter is (0, 0).
/// Event-handling done separately, see HandleEvents(). Should be called outside of Begin()/End() since e.mousePosition is also transformed.
/// </summary>
/// <param name="viewportRect">Window-space rectangle of the viewport</param>
/// <param name="cameraPosition">Camera position in canvas-space (virtual). Set this value to implement pan or LookAt / CenterTo behavior.</param>
/// <param name="zoom">Value of 1 is the default zoom, scale will be the same as in the editor window. If greater than 1 - zoomed-in, if less - zoomed out</param>
/// <returns>Rectangle in canvas space that is visible in viewport. Might be useful for clipping stuff that is not visible</returns>
public static Rect Begin(Rect viewportRect, Vector2 cameraPosition, float zoom)
{
GUI.EndGroup(); // End the group that Unity began so we're not bound (clipped) by the EditorWindow
zoom = Mathf.Max(zoom, 0.01f); // To prevent negative values and zero-division
var invZoom = 1f / zoom;
var clippedArea = new Rect(
viewportRect.x * invZoom,
(viewportRect.y + KEditorWindowTabHeight) * invZoom,
viewportRect.width * invZoom,
viewportRect.height * invZoom);
var scrollOffset = new Vector2(
0.5f * clippedArea.width - cameraPosition.x,
0.5f * clippedArea.height - cameraPosition.y
);
GUI.BeginClip(clippedArea, scrollOffset, Vector2.zero, false);
_backupMatrix = GUI.matrix;
var m = Matrix4x4.Scale(new Vector3(zoom, zoom, 1.0f));
GUI.matrix = m * GUI.matrix;
// Return visible rect in canvas space
return new Rect(cameraPosition - 0.5f * invZoom * viewportRect.size, clippedArea.size);
}
/// <summary>
/// Ends a viewport
/// </summary>
public static void End()
{
GUI.matrix = _backupMatrix;
GUI.EndClip();
GUI.BeginGroup(new Rect(0.0f, KEditorWindowTabHeight, Screen.width,
Screen.height - (KEditorWindowTabHeight + 3)));
}
/// <summary>
/// Processes uGUI events to implement simple Pan & Zoom behavior changing camera zoom and position which should be passed as ref and stored somewhere outside.
/// Note: use this function outside of Begin() End() since event mouse coordinated are also affected by viewport transformation matrix.
/// </summary>
/// <param name="e"></param>
/// <param name="viewportRect"></param>
/// <param name="cameraPosition">Reference to the camera position</param>
/// <param name="zoom">Reference to the camera zoom</param>
/// <param name="zoomSpeed">Amount at which ScrollWheel delta affects zoom</param>
/// <returns>if true - GUI has changed, if false - nothing happened</returns>
public static bool HandleEvents(Event e, Rect viewportRect, ref Vector2 cameraPosition, ref float zoom,
float zoomSpeed = 1.1f)
{
if (e.type == UnityEngine.EventType.MouseDown && viewportRect.Contains(e.mousePosition))
{
_isDragging = true;
}
if (e.type == UnityEngine.EventType.MouseUp)
{
_isDragging = false;
}
if (e.type == UnityEngine.EventType.MouseDrag && _isDragging)
{
cameraPosition -= e.delta / zoom;
return true;
}
if (e.type == UnityEngine.EventType.ScrollWheel && viewportRect.Contains(e.mousePosition))
{
// Apply zoom
zoom *= Mathf.Pow(zoomSpeed, -e.delta.y / 3f);
zoom = Mathf.Clamp(zoom, 0.05f, 10f);
// Difference between mouse position and canvas center
var mouseToCenterCanvasSpace = (e.mousePosition - viewportRect.center) / zoom;
// Move camera center so that the cursor
if (e.delta.y < 0)
{
// Zoom In
cameraPosition += (zoomSpeed - 1f) * mouseToCenterCanvasSpace;
}
else
{
// Zoom out
cameraPosition -= (zoomSpeed - 1f) / zoomSpeed * mouseToCenterCanvasSpace;
}
return true;
}
return false;
}
/// <summary>
/// Transforms a point in editor-window space (where viewport is positioned) to a canvas space.
/// </summary>
public static Vector2 WindowSpaceToCanvasSpace(Vector2 p, Rect viewportRect, Vector2 cameraPosition, float zoom)
{
return (p - viewportRect.center) / zoom + cameraPosition;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment