Skip to content

Instantly share code, notes, and snippets.

@blewert
Last active April 18, 2024 17:05
Show Gist options
  • Save blewert/b696baa4b62c15045a589ada7a92623a to your computer and use it in GitHub Desktop.
Save blewert/b696baa4b62c15045a589ada7a92623a to your computer and use it in GitHub Desktop.
Unity in-scene rect tool editor

rect tool

Unity in-scene rect tool editor

This is a small proof-of-concept project in Unity 2022 to enable the free translation and modification of various axis-aligned rectangles in-scene. The rectangles are oriented on the xz plane. This was made to help a student out with some initial code for a tools development module. The solution uses a single class, Rectangle, which has four points as members. The RectangleEditor editor script performs in-scene tooling via the Handles class to:

  1. Enable free movement of the rectangles via a handle in the centroid of the rectangle.
  2. Enable free, axis-aligned resizing of the rectangles via the four small circles shown in the image above.

This code is licensed under the Unlicense; feel free to copy, modify or whatever.

GIF not loading? See https://i.ibb.co/sQ1Cc4P/Unity-p-IMQ3e-VJ20.gif

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
[System.Serializable]
public class Rectangle : MonoBehaviour
{
public Vector3 topLeft = new Vector3(-1, 0, 1);
public Vector3 bottomLeft = new Vector3(-1, 0, -1);
public Vector3 topRight = new Vector3(1, 0, 1);
public Vector3 bottomRight = new Vector3(1, 0, -1);
/// <summary>
/// Gets a copy of all the points in the rectangle
/// </summary>
public List<Vector3> points => new List<Vector3>()
{
bottomLeft, topLeft, topRight, bottomRight
};
/// <summary>
/// Returns connected neighbours, given a point. This function is used to determine
/// which other vertices in the rectangle will be affected by axis-alignment. The tuple
/// of points returned are indices into the points list; the first is the point whose
/// x will be updated to point.x, and the second is the same but y -> point.y.
/// </summary>
/// <param name="point">The point in the rectangle</param>
/// <returns>A tuple of point indices</returns>
public (int xPoint, int yPoint) GetConnectedPoints(Vector3 point)
{
//tL -> bL (x), tR (y)
//tR -> bR (x), tL (y)
//----
//bL -> tL (x), bR (y)
//bR -> tR (x), bL (y)
if (point == bottomRight) return (2, 0);
else if (point == bottomLeft) return (1, 3);
///---
else if (point == topRight) return (3, 1);
else if (point == topLeft) return (0, 2);
//---
else return (-1, -1);
}
/// <summary>
/// Sets the points of the rectangle, given an ordered list of points
/// </summary>
/// <param name="points">The points</param>
public void SetPoints(in List<Vector3> points)
{
//Not a rect? get out of here
if (points.Count != 4)
return;
//Set them accordingly
bottomLeft = points[0];
topLeft = points[1];
topRight = points[2];
bottomRight = points[3];
}
/// <summary>
/// Finds top-most points of the rectangle
/// </summary>
public List<Vector3> topPoints => new List<Vector3> { topLeft, topRight };
/// <summary>
/// Finds bottom-most points of the rectangle
/// </summary>
public List<Vector3> bottomPoints => new List<Vector3>() { bottomLeft, bottomRight };
/// <summary>
/// Finds the midpoint of the rectangle via the arithmetic mean
/// </summary>
public Vector3 midpoint => points.Aggregate((a, b) => a + b) / points.Count;
/// <summary>
/// Finds line segment order for Handles.DrawLines(...) and/or DottedLines(...)
/// </summary>
public List<Vector3> lineSegments => new List<Vector3>()
{
bottomLeft, topLeft,
topLeft, topRight,
topRight, bottomRight,
bottomRight, bottomLeft
};
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
[CanEditMultipleObjects]
[CustomEditor(typeof(Rectangle))]
public class RectangleEditor : Editor
{
/// <summary>
/// Whether debug labels should be enabled globally for all
/// rectangle instances
/// </summary>
public static bool displayDebugLabels = true;
public override void OnInspectorGUI()
{
//Get underlying obj
Rectangle obj = (Rectangle)target;
//Set debug options
displayDebugLabels = EditorGUILayout.Toggle("Show global debug labels?", displayDebugLabels);
EditorGUILayout.Separator();
//--------
GUIStyle boldStyle = new GUIStyle(GUI.skin.label);
boldStyle.fontStyle = FontStyle.Bold;
EditorGUILayout.LabelField("Debug", boldStyle);
EditorGUILayout.BeginVertical();
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField($"Top Left = {obj.topLeft}");
EditorGUILayout.LabelField($"Top Right = {obj.topRight}");
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField($"Bottom Left = {obj.bottomLeft}");
EditorGUILayout.LabelField($"Bottom Right = {obj.bottomRight}");
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
/// <summary>
/// Draws per-vertex handles and manipulates the rectangle via calls
/// to Handles
/// </summary>
/// <param name="rect">The rectangle</param>
private void DrawVertexHandles(ref Rectangle rect)
{
//The list of points from the rectangle
var points = rect.points;
for (int i = 0; i < points.Count; i++)
{
//Run through each, set color to white
Handles.color = Color.white;
var point = points[i];
//Get screen-space point size
float size = HandleUtility.GetHandleSize(point) * 0.1f;
//Find new position and delta between this and the new point
Vector3 newPos = Handles.Slider2D(point, Vector3.up, Vector3.left, Vector3.forward, size, Handles.CircleHandleCap, 0.075f);
Vector3 diff = (newPos - point);
//Not moved? Skip
if (diff.magnitude <= 0.01f)
continue;
//Find points to alter x and z
int changeX, changeZ;
(changeX, changeZ) = rect.GetConnectedPoints(point);
//Invalid indices? skip
if (changeX < 0 || changeZ < 0)
continue;
//Find positions
Vector3 changeXPos = points[changeX];
Vector3 changeZPos = points[changeZ];
//Move this point, update xz of adjacent points
points[i] += diff;
changeXPos.x = points[i].x;
changeZPos.z = points[i].z;
//Set the points
points[changeX] = changeXPos;
points[changeZ] = changeZPos;
}
//Set the points; update underlying object
rect.SetPoints(points);
}
/// <summary>
/// Draws the midpoint/centroid handle of the rectangle, which moves all points
/// by a certain offset
/// </summary>
/// <param name="rect">The rectangle</param>
private void DrawMidpointHandle(ref Rectangle rect)
{
//Set to a transparent white
Handles.color = new Color(1, 1, 1, 0.5f);
//Find screen-space size
float size = HandleUtility.GetHandleSize(rect.midpoint) * 0.15f;
//Find new position and difference from old value
Vector3 newPos = Handles.Slider2D(rect.midpoint, Vector3.up, Vector3.left, Vector3.forward, size, Handles.RectangleHandleCap, 0.01f);
Vector3 diff = newPos - rect.midpoint;
//Find all points and add the difference to all points
var points = rect.points;
//--
for(int i = 0; i < points.Count; i++)
points[i] += diff;
//Set the points, reset colour
rect.SetPoints(points);
Handles.color = Color.white;
}
/// <summary>
/// Called when the scene needs rendering for this editor
/// </summary>
private void OnSceneGUI()
{
//Find underlying object
Rectangle obj = (Rectangle)target;
//Gone wrong? Get out of here
if (obj == null)
return;
//Draw the rectangular region with a transparent white
Handles.color = new Color(1, 1, 1, 0.3f);
Handles.DrawAAConvexPolygon(obj.points.ToArray());
//Draw outline with dotted lines
Handles.color = Color.white;
Handles.DrawDottedLines(obj.lineSegments.ToArray(), 2f);
if (displayDebugLabels)
{
//Show debug labels if needed
Handles.Label(Vector3.up * 0.1f + obj.topLeft, "tL");
Handles.Label(Vector3.up * 0.1f + obj.topRight, "tR");
Handles.Label(Vector3.up * 0.1f + obj.bottomLeft, "bL");
Handles.Label(Vector3.up * 0.1f + obj.bottomRight, "bR");
}
//Draw midpoint & vertex handles
DrawMidpointHandle(ref obj);
DrawVertexHandles(ref obj);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment