Created
October 11, 2019 11:23
-
-
Save FNGgames/bcf16c7d8d9aca85f1b0ddf17b5eee16 to your computer and use it in GitHub Desktop.
Custom Handles Example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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