using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.IO;
using System.Reflection;
using System;
public class NodeEditor : EditorWindow
{
static NodeEditor _window;
public static float zoomDelta = 0.01f;
public static float minZoom = 1f;
public static float maxZoom = 8f;
public static float panSpeed = 1.2f;
// To keep track of zooming.
private Vector2 _zoomAdjustment;
private Vector2 _zoom = Vector2.one;
public Vector2 panOffset = Vector2.zero;
public void Pan(Vector2 delta)
{
panOffset += delta * ZoomScale * panSpeed;
}
public void Zoom(float zoomDirection)
{
float scale = (zoomDirection < 0f) ? (1f - zoomDelta) : (1f + zoomDelta);
_zoom *= scale;
float cap = Mathf.Clamp(_zoom.x, minZoom, maxZoom);
_zoom.Set(cap, cap);
}
public float ZoomScale
{
get { return _zoom.x; }
set
{
float z = Mathf.Clamp(value, minZoom, maxZoom);
_zoom.Set(z, z);
}
}
///
/// The size of the window.
///
public Rect Size
{
get { return new Rect(Vector2.zero, position.size); }
}
[MenuItem("Scene/NodeEditor #")]
static void Init()
{
if (_window == null)
{
_window = EditorWindow.GetWindow(typeof(NodeEditor)) as NodeEditor;
GUIScaleUtility.CheckInit();
}
}
private static string getFullPath(string root, string targetFolderName)
{
string[] dirs = Directory.GetDirectories(root, targetFolderName, SearchOption.AllDirectories);
// Return first occurance containing targetFolderName.
if (dirs.Length != 0)
{
return dirs[0];
}
// Could not find anything.
return "";
}
public static string GetTextureFolderPath()
{
string fullpath = Application.dataPath;// getFullPath(Application.dataPath, "");
if (!string.IsNullOrEmpty(fullpath))
{
// Return the texture folder path relative to Unity's Asset folder.
int index = fullpath.IndexOf("Assets");
string localPath = fullpath.Substring(index);
return localPath;
}
Debug.LogError("Could not find folder: " + "");
return "";
}
Texture2D gridTex;
Texture2D _gridTex
{
get
{
if(gridTex==null)
{
string path = GetTextureFolderPath().Dir("Grid.png");
gridTex = AssetDatabase.LoadAssetAtPath
(path);
// Debug.Log(path);
if(gridTex==null)
{
Debug.Log("gridTex is null");
}
}
return gridTex;
}
}
Texture2D circleTex;
Texture2D _circleTex
{
get
{
if (circleTex == null)
{
string path = GetTextureFolderPath().Dir("Circle.png");
circleTex = AssetDatabase.LoadAssetAtPath(path);
// Debug.Log(path);
if (circleTex == null)
{
Debug.Log("circleTex is null");
}
}
return circleTex;
}
}
private void OnGUI()
{
if (_window)
{
if (Event.current.type == EventType.Repaint)
{
if (bDrawGuide)
{ DrawGrid(); }
}
Rect graphRect = _window.Size;
var center = graphRect.size / 2f;
_zoomAdjustment = GUIScaleUtility.BeginScale(ref graphRect, center, ZoomScale, false);
drawGridOverlay();
GUIScaleUtility.EndScale();
if (Event.current.type == EventType.ScrollWheel)
{
Zoom(Event.current.delta.y);
Repaint();
}
if (Event.current.type == EventType.MouseDrag)
{
Pan(Event.current.delta);
Repaint();
}
EditorGUILayout.BeginHorizontal("Toolbar");
if (DropdownButton("Settings", kToolbarButtonWidth + 10f))
{
createSettingsMenu();
}
if (DropButton("ReCenter", kToolbarButtonWidth + 10f))
{
ReCenterGrid();
}
EditorGUILayout.EndHorizontal();
}
}
public void ToggleDrawGuide()
{
bDrawGuide = !bDrawGuide;
}
public void ReCenterGrid()
{
panOffset = Vector2.zero;
}
public const float kToolbarHeight = 20f;
public const float kToolbarButtonWidth = 50f;
private void createSettingsMenu()
{
var menu = new GenericMenu();
menu.AddItem(new GUIContent("Show Guide"), bDrawGuide, ToggleDrawGuide);
menu.DropDown(new Rect(0, kToolbarHeight, 0f, 0f));
}
public bool DropButton(string name, float width)
{
return GUILayout.Button(name, EditorStyles.miniButton, GUILayout.Width(width));
}
public bool DropdownButton(string name, float width)
{
return GUILayout.Button(name, EditorStyles.toolbarDropDown, GUILayout.Width(width));
}
///
/// The rect bounds defining the recticle at the grid center.
///
public static readonly Rect kReticleRect = new Rect(0, 0, 8, 8);
///
/// Enables and disables drawing the guide to the grid center.
///
public bool bDrawGuide = false;
public Color guideColor =Color.gray;
private void DrawGrid()
{
var size = _window.Size.size;
var center = size / 2f;
float zoom = ZoomScale;
// Offset from origin in tile units
float xOffset = -(center.x * zoom + panOffset.x) / _gridTex.width;
float yOffset = ((center.y - size.y) * zoom + panOffset.y) / _gridTex.height;
Vector2 tileOffset = new Vector2(xOffset, yOffset);
// Amount of tiles
float tileAmountX = Mathf.Round(size.x * zoom) / _gridTex.width;
float tileAmountY = Mathf.Round(size.y * zoom) / _gridTex.height;
Vector2 tileAmount = new Vector2(tileAmountX, tileAmountY);
// Draw tiled background
GUI.DrawTextureWithTexCoords(_window.Size, _gridTex, new Rect(tileOffset, tileAmount));
}
// Handles drawing things over the grid such as axes.
private void drawGridOverlay()
{
drawAxes();
// drawGridCenter();
if (bDrawGuide)
{
//drawGuide();
//_window.Repaint();
}
}
///
/// Converts the graph position to screen space.
/// This only works for geometry inside the GUIScaleUtility.BeginScale()
///
///
///
public Vector2 GraphToScreenSpace(Vector2 graphPos)
{
return graphPos + _zoomAdjustment + panOffset;
}
///
/// Draws a GUI texture with a tint.
///
///
///
///
public static void DrawTintTexture(Rect r, Texture t, Color c)
{
var guiColor = GUI.color;
GUI.color = c;
GUI.DrawTexture(r, t);
GUI.color = guiColor;
}
private void drawGridCenter()
{
var rect = kReticleRect;
rect.size *= ZoomScale;
rect.center = Vector2.zero;
rect.position = GraphToScreenSpace(rect.position);
DrawTintTexture(rect, _circleTex, Color.gray);
}
private void drawAxes()
{
// Draw axes. Make sure to scale based on zoom.
Vector2 up = Vector2.up * _window.Size.height * ZoomScale;
Vector2 right = Vector2.right * _window.Size.width * ZoomScale;
Vector2 down = -up;
Vector2 left = -right;
// Make sure the axes follow the pan.
up.y -= panOffset.y;
down.y -= panOffset.y;
right.x -= panOffset.x;
left.x -= panOffset.x;
up = GraphToScreenSpace(up);
down = GraphToScreenSpace(down);
right = GraphToScreenSpace(right);
left = GraphToScreenSpace(left);
DrawLine(right, left, Color.white);
DrawLine(up, down, Color.white);
}
///
/// Shows where the center of the grid is.
///
private void drawGuide()
{
Vector2 gridCenter = GraphToScreenSpace(Vector2.zero);
DrawLine(gridCenter, Event.current.mousePosition, guideColor);
}
///
/// Draws a bezier between the two end points in screen space.
///
public static void DrawBezier(Vector2 start, Vector2 end, Color color)
{
Vector2 endToStart = (end - start);
float dirFactor = Mathf.Clamp(endToStart.magnitude, 20f, 80f);
endToStart.Normalize();
Vector2 project = Vector3.Project(endToStart, Vector3.right);
Vector2 startTan = start + project * dirFactor;
Vector2 endTan = end - project * dirFactor;
UnityEditor.Handles.DrawBezier(start, end, startTan, endTan, color, null, 3f);
}
///
/// Draws a line between the two end points.
///
///
///
public static void DrawLine(Vector2 start, Vector2 end, Color color)
{
var handleColor = Handles.color;
Handles.color = color;
Handles.DrawLine(start, end);
Handles.color = handleColor;
}
}
public static class StringExtensions
{
///
/// Merges the parent and child paths with the '/' character.
///
///
///
///
public static string Dir(this string parentDir, string childDir)
{
return parentDir + '/' + childDir;
}
///
/// Appends the extension to the file name with '.'
///
///
///
///
public static string Ext(this string file, string extension)
{
return file + '.' + extension;
}
}
public static class GUIScaleUtility
{
// General
private static bool compabilityMode;
private static bool initiated;
// Delegates to the reflected methods
private static Func GetTopRectDelegate;
private static Func topmostRectDelegate;
// Delegate accessors
public static Rect getTopRect { get { return (Rect)GetTopRectDelegate.Invoke(); } }
// Rect stack for manipulating groups
public static List currentRectStack { get; private set; }
private static List> rectStackGroups;
// Matrices stack
private static List GUIMatrices;
private static List adjustedGUILayout;
#region Init
public static void CheckInit()
{
if (!initiated)
Init();
}
public static void Init()
{
// Fetch rect acessors using Reflection
Assembly UnityEngine = Assembly.GetAssembly(typeof(UnityEngine.GUI));
Type GUIClipType = UnityEngine.GetType("UnityEngine.GUIClip", true);
PropertyInfo topmostRect = GUIClipType.GetProperty("topmostRect", BindingFlags.Static | BindingFlags.Public);
MethodInfo GetTopRect = GUIClipType.GetMethod("GetTopRect", BindingFlags.Static | BindingFlags.NonPublic);
MethodInfo ClipRect = GUIClipType.GetMethod("Clip", BindingFlags.Static | BindingFlags.Public, Type.DefaultBinder, new Type[] { typeof(Rect) }, new ParameterModifier[] { });
if (GUIClipType == null || topmostRect == null || GetTopRect == null || ClipRect == null)
{
Debug.LogWarning("GUIScaleUtility cannot run on this system! Compability mode enabled. For you that means you're not able to use the Node Editor inside more than one group:( Please PM me (Seneral @UnityForums) so I can figure out what causes this! Thanks!");
Debug.LogWarning((GUIClipType == null ? "GUIClipType is Null, " : "") + (topmostRect == null ? "topmostRect is Null, " : "") + (GetTopRect == null ? "GetTopRect is Null, " : "") + (ClipRect == null ? "ClipRect is Null, " : ""));
compabilityMode = true;
initiated = true;
return;
}
// Create simple acessor delegates
GetTopRectDelegate = (Func)Delegate.CreateDelegate(typeof(Func), GetTopRect);
topmostRectDelegate = (Func)Delegate.CreateDelegate(typeof(Func), topmostRect.GetGetMethod());
if (GetTopRectDelegate == null || topmostRectDelegate == null)
{
Debug.LogWarning("GUIScaleUtility cannot run on this system! Compability mode enabled. For you that means you're not able to use the Node Editor inside more than one group:( Please PM me (Seneral @UnityForums) so I can figure out what causes this! Thanks!");
Debug.LogWarning((GUIClipType == null ? "GUIClipType is Null, " : "") + (topmostRect == null ? "topmostRect is Null, " : "") + (GetTopRect == null ? "GetTopRect is Null, " : "") + (ClipRect == null ? "ClipRect is Null, " : ""));
compabilityMode = true;
initiated = true;
return;
}
// As we can call Begin/Ends inside another, we need to save their states hierarchial in Lists (not Stack, as we need to iterate over them!):
currentRectStack = new List();
rectStackGroups = new List>();
GUIMatrices = new List();
adjustedGUILayout = new List();
initiated = true;
}
#endregion
#region Scale Area
///
/// Begins a scaled local area.
/// Returns vector to offset GUI controls with to account for zooming to the pivot.
/// Using adjustGUILayout does that automatically for GUILayout rects. Theoretically can be nested!
///
public static Vector2 BeginScale(ref Rect rect, Vector2 zoomPivot, float zoom, bool adjustGUILayout)
{
Rect screenRect;
if (compabilityMode)
{
// In compability mode, we will assume only one top group and do everything manually, not using reflected calls (-> practically blind)
GUI.EndGroup();
screenRect = rect;
}
else
{
// If it's supported, we take the completely generic way using reflected calls
GUIScaleUtility.BeginNoClip();
screenRect = GUIScaleUtility.GUIToScaledSpace(rect);
}
rect = Scale(screenRect, screenRect.position + zoomPivot, new Vector2(zoom, zoom));
// Now continue drawing using the new clipping group
GUI.BeginGroup(rect);
rect.position = Vector2.zero; // Adjust because we entered the new group
// Because I currently found no way to actually scale to a custom pivot rather than (0, 0),
// we'll make use of a cheat and just offset it accordingly to let it appear as if it would scroll to the center
// Note, due to that, controls not adjusted are still scaled to (0, 0)
Vector2 zoomPosAdjust = rect.center - screenRect.size / 2 + zoomPivot;
// For GUILayout, we can make this adjustment here if desired
adjustedGUILayout.Add(adjustGUILayout);
if (adjustGUILayout)
{
GUILayout.BeginHorizontal();
GUILayout.Space(rect.center.x - screenRect.size.x + zoomPivot.x);
GUILayout.BeginVertical();
GUILayout.Space(rect.center.y - screenRect.size.y + zoomPivot.y);
}
// Take a matrix backup to restore back later on
GUIMatrices.Add(GUI.matrix);
// Scale GUI.matrix. After that we have the correct clipping group again.
GUIUtility.ScaleAroundPivot(new Vector2(1 / zoom, 1 / zoom), zoomPosAdjust);
return zoomPosAdjust;
}
///
/// Ends a scale region previously opened with BeginScale
///
public static void EndScale()
{
// Set last matrix and clipping group
if (GUIMatrices.Count == 0 || adjustedGUILayout.Count == 0)
throw new UnityException("GUIScaleUtility: You are ending more scale regions than you are beginning!");
GUI.matrix = GUIMatrices[GUIMatrices.Count - 1];
GUIMatrices.RemoveAt(GUIMatrices.Count - 1);
// End GUILayout zoomPosAdjustment
if (adjustedGUILayout[adjustedGUILayout.Count - 1])
{
GUILayout.EndVertical();
GUILayout.EndHorizontal();
}
adjustedGUILayout.RemoveAt(adjustedGUILayout.Count - 1);
// End the scaled group
GUI.EndGroup();
if (compabilityMode)
{
// In compability mode, we don't know the previous group rect, but as we cannot use top groups there either way, we restore the screen group
GUI.BeginClip(new Rect(0, 23, Screen.width, Screen.height - 23));
}
else
{
// Else, restore the clips (groups)
GUIScaleUtility.RestoreClips();
}
}
#endregion
#region Clips Hierarchy
///
/// Begins a field without groups. They should be restored using RestoreClips. Can be nested!
///
public static void BeginNoClip()
{
// Record and close all clips one by one, from bottom to top, until we hit the 'origin'
List rectStackGroup = new List();
Rect topMostClip = getTopRect;
while (topMostClip != new Rect(-10000, -10000, 40000, 40000))
{
rectStackGroup.Add(topMostClip);
GUI.EndClip();
topMostClip = getTopRect;
}
// Store the clips appropriately
rectStackGroup.Reverse();
rectStackGroups.Add(rectStackGroup);
currentRectStack.AddRange(rectStackGroup);
}
///
/// Restores the clips removed in BeginNoClip or MoveClipsUp
///
public static void RestoreClips()
{
if (rectStackGroups.Count == 0)
{
Debug.LogError("GUIClipHierarchy: BeginNoClip/MoveClipsUp - RestoreClips count not balanced!");
return;
}
// Read and restore clips one by one, from top to bottom
List rectStackGroup = rectStackGroups[rectStackGroups.Count - 1];
for (int clipCnt = 0; clipCnt < rectStackGroup.Count; clipCnt++)
{
GUI.BeginClip(rectStackGroup[clipCnt]);
currentRectStack.RemoveAt(currentRectStack.Count - 1);
}
rectStackGroups.RemoveAt(rectStackGroups.Count - 1);
}
#endregion
#region Space Transformations
///
/// Scales the rect around the pivot with scale
///
public static Rect Scale(Rect rect, Vector2 pivot, Vector2 scale)
{
rect.position = Vector2.Scale(rect.position - pivot, scale) + pivot;
rect.size = Vector2.Scale(rect.size, scale);
return rect;
}
public static Vector2 GUIToScaledSpace(Vector2 guiPosition)
{
if (rectStackGroups == null || rectStackGroups.Count == 0)
return guiPosition;
// Iterate through the clips and add positions ontop
List rectStackGroup = rectStackGroups[rectStackGroups.Count - 1];
for (int clipCnt = 0; clipCnt < rectStackGroup.Count; clipCnt++)
guiPosition += rectStackGroup[clipCnt].position;
return guiPosition;
}
///
/// Transforms the rect to the new space aquired with BeginNoClip or MoveClipsUp.
/// DOES NOT scale the rect, only offsets it!
/// It's way faster to call GUIToScreenSpace before modifying the space though!
///
public static Rect GUIToScaledSpace(Rect guiRect)
{
if (rectStackGroups == null || rectStackGroups.Count == 0)
return guiRect;
guiRect.position = GUIToScaledSpace(guiRect.position);
return guiRect;
}
#endregion
}