Skip to content

Instantly share code, notes, and snippets.

@FNGgames
Created October 11, 2019 11:23
Show Gist options
  • Save FNGgames/bcf16c7d8d9aca85f1b0ddf17b5eee16 to your computer and use it in GitHub Desktop.
Save FNGgames/bcf16c7d8d9aca85f1b0ddf17b5eee16 to your computer and use it in GitHub Desktop.
Custom Handles Example
// free move 2d handle with a circle texture
using UnityEditor;
using UnityEngine;
namespace CableSystem.Editor
{
public class CircleHandle2D : CustomHandle
{
private float _distance;
private bool _hovered, _selected;
private int _controlId;
public bool Hovered => _hovered;
public bool Selected => _selected;
public int ControlId => _controlId;
private static readonly Color Color = Handles.color;
private static readonly Color HoveredColor = Handles.preselectionColor;
private static readonly Color SelectedColor = Handles.selectedColor;
private static readonly int Free2DMoveHandleHash = "Free2DMoveHandle".GetHashCode();
private static readonly Material Material = Resources.Load<Material>("CableSystem/CustomHandleMaterial");
private static readonly Texture2D Texture = Resources.Load<Texture2D>("CableSystem/CircleHandle");
private static readonly int MainTex = Shader.PropertyToID("_MainTex");
private static readonly int ColorPropertyHash = Shader.PropertyToID("_Color");
public Vector2 DrawHandle(Vector2 position, float size)
{
return DrawHandle(GUIUtility.GetControlID(Free2DMoveHandleHash, FocusType.Keyboard), position, size);
}
private Vector2 DrawHandle(int controlId, Vector2 position, float size)
{
_controlId = controlId;
_selected = GUIUtility.hotControl == controlId || GUIUtility.keyboardControl == controlId;
_hovered = HandleUtility.nearestControl == controlId;
switch (Evt.type)
{
case EventType.MouseDown:
if (HandleUtility.nearestControl == controlId && Evt.button == 0)
{
GUIUtility.hotControl = controlId;
GUIUtility.keyboardControl = controlId;
Evt.Use();
}
break;
case EventType.MouseUp:
if (GUIUtility.hotControl == controlId && (Evt.button == 0 || Evt.button == 2))
{
GUIUtility.hotControl = 0;
Evt.Use();
}
break;
case EventType.MouseDrag:
if (_selected) Move2DHandle(Evt, matrix, ref position);
break;
case EventType.Repaint:
DrawQuad(position, size);
break;
case EventType.Layout:
if (Evt.type == EventType.Layout) SceneView.RepaintAll();
var pointWorldPos = matrix.MultiplyPoint3x4(position);
_distance = HandleUtility.DistanceToRectangle(pointWorldPos, Camera.current.transform.rotation, size);
HandleUtility.AddControl(controlId, _distance);
break;
}
return position;
}
private void DrawQuad(Vector2 position, float size)
{
var worldPos = matrix.MultiplyPoint3x4(position);
var camTransform = Camera.current.transform;
var camRight = camTransform.right * size;
var camUp = camTransform.up * size;
var col = _selected ? SelectedColor : _hovered ? HoveredColor : Color;
Material.SetTexture(MainTex, Texture);
Material.SetColor(ColorPropertyHash, col);
Material.SetPass(0);
GL.Begin(GL.QUADS);
{
GL.TexCoord2(1, 1);
GL.Vertex(worldPos + camRight + camUp);
GL.TexCoord2(1, 0);
GL.Vertex(worldPos + camRight - camUp);
GL.TexCoord2(0, 0);
GL.Vertex(worldPos - camRight - camUp);
GL.TexCoord2(0, 1);
GL.Vertex(worldPos - camRight + camUp);
}
GL.End();
}
private static void Move2DHandle(Event e, Matrix4x4 mat, ref Vector2 position)
{
if (!CustomHandleUtility.GetMousePositionInWorld(e, mat, out var mouseWorldPos)) return;
var pointOnPlane = mat.inverse.MultiplyPoint3x4(mouseWorldPos);
if (e.delta != Vector2.zero) GUI.changed = true;
position = pointOnPlane;
}
}
}
// base class for custom handles
using UnityEngine;
namespace CableSystem.Editor
{
public class CustomHandle
{
public Quaternion rotation = Quaternion.identity;
public Vector3 scale = Vector3.one;
public Vector3 position;
public Matrix4x4 matrix = Matrix4x4.identity;
protected static Event Evt => Event.current;
public void SetTransform(Transform transform)
{
rotation = transform.rotation;
position = transform.position;
scale = transform.localScale;
matrix = Matrix4x4.TRS(position, rotation, scale);
}
public void SetTransform(Vector3 position, Quaternion rotation, Vector3 scale)
{
this.rotation = rotation;
this.position = position;
this.scale = scale;
matrix = Matrix4x4.TRS(position, rotation, scale);
}
public void SetTransform(CustomHandle handle)
{
position = handle.matrix.GetColumn(3);
rotation = Quaternion.LookRotation(
handle.matrix.GetColumn(2),
handle.matrix.GetColumn(1)
);
scale = new Vector3(
handle.matrix.GetColumn(0).magnitude,
handle.matrix.GetColumn(1).magnitude,
handle.matrix.GetColumn(2).magnitude
);
matrix = Matrix4x4.TRS(position, rotation, scale);
}
}
}
// Mimics the style of the unity Handles class allowing you to call the custom handles you make
// e.g. var handlePos = CustomHandles.Free2DMoveHandle(...);
using UnityEngine;
namespace CableSystem.Editor
{
public class CustomHandles : MonoBehaviour
{
private static readonly CircleHandle2D CircleHandle2D = new CircleHandle2D();
public static Vector2 Free2DMoveHandle(Vector2 position, float size)
{
CircleHandle2D.matrix = Matrix4x4.TRS(Vector3.zero, Quaternion.identity, Vector3.one);
return CircleHandle2D.DrawHandle(position, size);
}
public static Vector2 Free2DMoveHandle(Vector2 position, float size, out int controlId)
{
CircleHandle2D.matrix = Matrix4x4.TRS(Vector3.zero, Quaternion.identity, Vector3.one);
var result = CircleHandle2D.DrawHandle(position, size);
controlId = CircleHandle2D.ControlId;
return result;
}
public static Vector2 Free2DMoveHandle(Vector2 position, float size, out int controlId, out bool hovered, out bool selected)
{
CircleHandle2D.matrix = Matrix4x4.TRS(Vector3.zero, Quaternion.identity, Vector3.one);
var result = CircleHandle2D.DrawHandle(position, size);
controlId = CircleHandle2D.ControlId;
hovered = CircleHandle2D.Hovered;
selected = CircleHandle2D.Selected;
return result;
}
}
}
// useful functions for working with handles
using UnityEditor;
using UnityEngine;
namespace CableSystem.Editor
{
public static class CustomHandleUtility
{
public static bool GetMousePositionInWorld(Event evt, Matrix4x4 mat, out Vector3 position)
{
var r = HandleUtility.GUIPointToWorldRay(evt.mousePosition);
return GetPointOnPlane(mat, r, out position);
}
private static bool GetPointOnPlane(Matrix4x4 planeTransform, Ray ray, out Vector3 position)
{
position = Vector3.zero;
var p = new Plane(planeTransform * Vector3.forward, planeTransform.MultiplyPoint3x4(position));
p.Raycast(ray, out var dist);
if (dist < 0) return false;
position = ray.GetPoint(dist);
return true;
}
}
}
// monobehaviour for the node graph
using System;
using System.Collections.Generic;
using MongoDB.Driver.Builders;
using Sirenix.OdinInspector;
using UnityEngine;
namespace CableSystem
{
public class NodeGraph : SerializedMonoBehaviour
{
[Range(0.1f, 2)] public float handleSize = 0.5f;
[Range(1, 5)] public float arrowSize = 3f;
[SerializeField, HideInInspector] private List<ControlNode> controlNodes = new List<ControlNode>();
public ControlNode[] ControlNodes => controlNodes.ToArray();
public ControlNode First => controlNodes.Count > 0 ? controlNodes[0] : null;
public ControlNode Last => controlNodes.Count > 0 ? controlNodes[controlNodes.Count - 1] : null;
public void AddControlNode(ControlNode parent, Vector3 position)
{
var newNode = new ControlNode();
newNode.AddParent(parent);
newNode.SetPosition(position);
controlNodes.Add(newNode);
UpdateViews();
}
public void RemoveControlNode(ControlNode node, bool bridgeConnections = false)
{
if (node == null) throw new ArgumentNullException(nameof(node));
if (!controlNodes.Contains(node)) return;
node.BreakConnections(bridgeConnections);
controlNodes.Remove(node);
UpdateViews();
}
[Button("Clear Control Nodes", ButtonSizes.Large)]
public void ClearControlNodes()
{
controlNodes.Clear();
UpdateViews();
}
public void MakeConnection(ControlNode parent, ControlNode child)
{
parent.AddChild(child);
UpdateViews();
}
public void BreakConnection(ControlNode a, ControlNode b)
{
if (a.IsChildOf(b)) b.RemoveChild(a);
if (a.IsParentOf(b)) b.RemoveParent(a);
UpdateViews();
}
public void BreakConnections(ControlNode node, bool bridgeConnections = false)
{
node.BreakConnections(bridgeConnections);
UpdateViews();
}
public void SetControlNodePositions(Vector3[] positions)
{
if (positions == null) throw new ArgumentNullException(nameof(positions));
if (positions.Length != controlNodes.Count)
throw new Exception($"Position Array Length Mismatch: (required: {controlNodes.Count}, supplied: {positions.Length})");
for (var i = 0; i < positions.Length; i++) controlNodes[i].SetPosition(positions[i]);
UpdateViews();
}
private void UpdateViews()
{
var nodes = ControlNodes;
foreach (var listener in GetComponentsInChildren<INodeGraphListener>())
listener.OnGraphUpdated(nodes);
}
}
}
// editor for the node graph
using System.Collections.Generic;
using Sirenix.OdinInspector.Editor;
using UnityEditor;
using UnityEngine;
namespace CableSystem.Editor
{
[CustomEditor(typeof(NodeGraph))]
public class NodeGraphEditor : OdinEditor
{
private ControlNode _selected;
protected void OnSceneGUI()
{
if (Application.isPlaying) return;
// get the class we are editing and cache some data
var nodeGraph = (NodeGraph) target;
var evt = Event.current;
var controlNodes = nodeGraph.ControlNodes;
var handleSize = nodeGraph.handleSize;
var arrowSize = nodeGraph.arrowSize;
// catch mouse up event before it used by the handles
var mouseUp = evt.type == EventType.MouseUp;
var mouseDown = evt.type == EventType.MouseDown;
var mouseDrag = evt.type == EventType.MouseDrag;
// check for user manually connecting nodes must be done before the new selection is made
CustomHandleUtility.GetMousePositionInWorld(evt, Matrix4x4.identity, out var mousePos);
if (mouseDown)
if (GetNearestControlPoint(mousePos, controlNodes, handleSize, out var nearest))
if (evt.button == 0 && evt.shift)
if (nearest != _selected) nodeGraph.MakeConnection(_selected,nearest);
// begin the handle change check
EditorGUI.BeginChangeCheck();
// grab the positions of all the handles and record which is selected
var nodePositions = new Vector3[controlNodes.Length];
for (var i = 0; i < controlNodes.Length; i++)
{
nodePositions[i] = CustomHandles.Free2DMoveHandle(controlNodes[i].Position, handleSize, out var _,out var _, out var isSelected);
if (isSelected) _selected = controlNodes[i];
}
// user creates new node, removes node or breaks connections on a node
if (mouseDown || mouseUp || mouseDrag)
{
if (GetNearestControlPoint(mousePos, controlNodes, handleSize, out var nearest))
{
if (evt.button == 1)
{
// break connections but keep node if ctrl + right-click
if (evt.control || evt.command) nodeGraph.BreakConnections(nearest);
// remove node if simple right-click
else
{
if (_selected == nearest) _selected = nodeGraph.Last;
// bridge the connections by default or break the connections if shift is pressed
nodeGraph.RemoveControlNode(nearest, !evt.shift);
}
}
}
else if (evt.button == 0 && (evt.shift || evt.control || evt.command))
{
// shift + left-click creates a new node with the previously selected node as parent
var parent = evt.shift ? controlNodes.Length > 0 ? _selected : null : null;
nodeGraph.AddControlNode(parent, mousePos);
_selected = nodeGraph.Last;
}
}
// draw the connecting lines and arrows
foreach (var controlNode in controlNodes)
{
foreach (var child in controlNode.GetChildren())
{
var displacement = child.Position - controlNode.Position;
var rotation = Quaternion.LookRotation(displacement, Vector3.up);
var distance = Mathf.Min(arrowSize, displacement.magnitude / 2f);
var position = controlNode.Position + (displacement * 0.5f) - (distance * displacement.normalized);
Handles.DrawLine(controlNode.Position, child.Position);
Handles.ArrowHandleCap(0, position, rotation, distance, EventType.Repaint);
}
}
if (EditorGUI.EndChangeCheck())
{
nodeGraph.SetControlNodePositions(nodePositions);
}
}
// helper to get the nearest control node to the current mouse position
private static bool GetNearestControlPoint(Vector3 mousePos, IEnumerable<ControlNode> nodes, float size, out ControlNode nearest)
{
nearest = null;
var shortestDistance = float.MaxValue;
foreach (var node in nodes)
{
var d = Vector2.Distance(mousePos, node.Position);
if (d >= shortestDistance) continue;
shortestDistance = d;
nearest = node;
}
return shortestDistance < size * 2;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment