using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using NodeEditorFramework.Utilities;
using UNEB.Utility;
namespace UNEB
{
public class NodeEditor
{
///
/// Callback for when a node is modified within the editor
///
public static Action onNodeGuiChange;
///
/// The rect bounds defining the recticle at the grid center.
///
public static readonly Rect kReticleRect = new Rect(0, 0, 8, 8);
public static float zoomDelta = 0.1f;
public static float minZoom = 1f;
public static float maxZoom = 4f;
public static float panSpeed = 1.2f;
///
/// The associated graph to visualize and edit.
///
public NodeGraph graph;
private NodeEditorWindow _window;
private Texture2D _gridTex;
private Texture2D _backTex;
private Texture2D _circleTex;
private Texture2D _knobTex;
private Texture2D _headerTex;
public Color backColor;
public Color knobColor;
public Color guideColor;
// To keep track of zooming.
private Vector2 _zoomAdjustment;
private Vector2 _zoom = Vector2.one;
public Vector2 panOffset = Vector2.zero;
///
/// Enables and disables drawing the guide to the grid center.
///
public bool bDrawGuide = false;
public NodeEditor(NodeEditorWindow w)
{
backColor = ColorExtensions.From255(59, 62, 74);
knobColor = ColorExtensions.From255(126, 186, 255);
guideColor = Color.gray;
guideColor.a = 0.3f;
_gridTex = TextureLib.GetTexture("Grid");
_backTex = TextureLib.GetTexture("Square");
_circleTex = TextureLib.GetTexture("Circle");
_window = w;
}
#region Drawing
public void Draw()
{
if (Event.current.type == EventType.Repaint) {
drawGrid();
updateTextures();
}
if (graph)
drawGraphContents();
drawMode();
}
private void drawGraphContents()
{
Rect graphRect = _window.Size;
var center = graphRect.size / 2f;
_zoomAdjustment = GUIScaleUtility.BeginScale(ref graphRect, center, ZoomScale, false);
drawGridOverlay();
drawConnectionPreview();
drawConnections();
drawNodes();
GUIScaleUtility.EndScale();
}
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();
}
}
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.gray);
DrawLine(up, down, Color.gray);
}
///
/// Shows where the center of the grid is.
///
private void drawGuide()
{
Vector2 gridCenter = GraphToScreenSpace(Vector2.zero);
DrawLine(gridCenter, Event.current.mousePosition, guideColor);
}
private void drawNodes()
{
// Calculate the viewing rect in graph space.
var view = _window.Size;
view.size *= ZoomScale;
view.center = -panOffset;
// Render nodes within the view space for performance.
foreach (Node node in graph.nodes) {
if (view.Overlaps(node.bodyRect)) {
drawNode(node);
drawKnobs(node);
}
}
}
private void drawKnobs(Node node)
{
foreach (var input in node.Inputs) {
drawKnob(input);
}
foreach (var output in node.Outputs) {
drawKnob(output);
}
}
private void drawKnob(NodeConnection knob)
{
// Convert the body rect from graph to screen space.
var screenRect = knob.bodyRect;
screenRect.position = GraphToScreenSpace(screenRect.position);
GUI.DrawTexture(screenRect, _knobTex);
}
private void drawConnections()
{
foreach (var node in graph.nodes) {
foreach (var output in node.Outputs) {
foreach (var input in output.Inputs) {
Vector2 start = GraphToScreenSpace(output.bodyRect.center);
Vector2 end = GraphToScreenSpace(input.bodyRect.center);
DrawBezier(start, end, knobColor);
}
}
}
}
private void drawConnectionPreview()
{
var output = _window.state.selectedOutput;
if (output != null) {
Vector2 start = GraphToScreenSpace(output.bodyRect.center);
DrawBezier(start, Event.current.mousePosition, Color.gray);
}
}
private void drawNode(Node node)
{
// Convert the node rect from graph to screen space.
Rect screenRect = node.bodyRect;
screenRect.position = GraphToScreenSpace(screenRect.position);
// The node contents are grouped together within the node body.
BeginGroup(screenRect, backgroundStyle, backColor);
// Make the body of node local to the group coordinate space.
Rect localRect = node.bodyRect;
localRect.position = Vector2.zero;
// Draw the contents inside the node body, automatically laidout.
GUILayout.BeginArea(localRect, GUIStyle.none);
node.HeaderStyle.normal.background = _headerTex;
EditorGUI.BeginChangeCheck();
node.OnNodeGUI();
if (EditorGUI.EndChangeCheck())
if (onNodeGuiChange != null) onNodeGuiChange(graph, node);
GUILayout.EndArea();
GUI.EndGroup();
}
///
/// Draw the window mode in the background.
///
public void drawMode()
{
if (!graph) {
GUI.Label(_modeStatusRect, new GUIContent("No Graph Set"), ModeStatusStyle);
}
else if (_window.GetMode() == NodeEditorWindow.Mode.Edit) {
GUI.Label(_modeStatusRect, new GUIContent("Edit"), ModeStatusStyle);
}
else {
GUI.Label(_modeStatusRect, new GUIContent("View"), ModeStatusStyle);
}
}
///
/// 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;
}
///
/// 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;
}
public static void BeginGroup(Rect r, GUIStyle style, Color color)
{
var old = GUI.color;
GUI.color = color;
GUI.BeginGroup(r, style);
GUI.color = old;
}
// TODO: Call after exiting playmode.
private void updateTextures()
{
_knobTex = TextureLib.GetTintTex("Circle", knobColor);
_headerTex = TextureLib.GetTintTex("Square", ColorExtensions.From255(79, 82, 94));
}
#endregion
#region View Operations
public void ToggleDrawGuide()
{
bDrawGuide = !bDrawGuide;
}
public void HomeView()
{
if (!graph || graph.nodes.Count == 0) {
panOffset = Vector2.zero;
return;
}
float xMin = float.MaxValue;
float xMax = float.MinValue;
float yMin = float.MaxValue;
float yMax = float.MinValue;
foreach (var node in graph.nodes) {
Rect r = node.bodyRect;
if (r.xMin < xMin) {
xMin = r.xMin;
}
if (r.xMax > xMax) {
xMax = r.xMax;
}
if (r.yMin < yMin) {
yMin = r.yMin;
}
if (r.yMax > yMax) {
yMax = r.yMax;
}
}
// Add some padding so nodes do not appear on the edge of the view.
xMin -= Node.kDefaultSize.x;
xMax += Node.kDefaultSize.x;
yMin -= Node.kDefaultSize.y;
yMax += Node.kDefaultSize.y;
var nodesArea = Rect.MinMaxRect(xMin, yMin, xMax, yMax);
// Center the pan in the bounding view.
panOffset = -nodesArea.center;
// Calculate the required zoom based on the ratio between the window view and node area rect.
var winSize = _window.Size;
float zoom = 1f;
// Use the view width to determine zoom to fit the entire node area width.
if (nodesArea.width > nodesArea.height) {
float widthRatio = nodesArea.width / winSize.width;
zoom = widthRatio;
if (widthRatio < 1f) {
zoom = 1 / widthRatio;
}
}
// Use the height to determine zoom.
else {
float heightRatio = nodesArea.height / winSize.height;
zoom = heightRatio;
if (heightRatio < 1f) {
zoom = 1 / heightRatio;
}
}
ZoomScale = zoom;
}
#endregion
#region Space Transformations and Mouse Utilities
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);
}
}
///
/// Convertes the screen position to graph space.
///
public Vector2 ScreenToGraphSpace(Vector2 screenPos)
{
var graphRect = _window.Size;
var center = graphRect.size / 2f;
return (screenPos - center) * ZoomScale - panOffset;
}
///
/// Returns the mouse position in graph space.
///
///
public Vector2 MousePosition()
{
return ScreenToGraphSpace(Event.current.mousePosition);
}
///
/// Tests if the rect is under the mouse.
///
///
///
public bool IsUnderMouse(Rect r)
{
return r.Contains(MousePosition());
}
///
/// 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;
}
///
/// Converts the graph position to screen space.
/// This only works for geometry inside the GUIScaleUtility.BeginScale().
///
///
public void graphToScreenSpace(ref Vector2 graphPos)
{
graphPos += _zoomAdjustment + panOffset;
}
///
/// Converts the graph position to screen space.
/// This works for geometry NOT inside the GUIScaleUtility.BeginScale().
///
///
public void graphToScreenSpaceZoomAdj(ref Vector2 graphPos)
{
graphPos = GraphToScreenSpace(graphPos) / ZoomScale;
}
///
/// Executes the callback on the first node that is detected under the mouse.
///
///
public bool OnMouseOverNode(Action callback)
{
if (!graph) {
return false;
}
for (int i = graph.nodes.Count - 1; i >= 0; --i) {
Node node = graph.nodes[i];
if (IsUnderMouse(node.bodyRect)) {
callback(node);
return true;
}
}
// No node under mouse.
return false;
}
///
/// Tests if the mouse is over an output.
///
///
///
public bool OnMouseOverOutput(Action callback)
{
if (!graph) {
return false;
}
foreach (var node in graph.nodes) {
foreach (var output in node.Outputs) {
if (IsUnderMouse(output.bodyRect)) {
callback(output);
return true;
}
}
}
return false;
}
///
/// Tests if the mouse is over an input.
///
///
///
public bool OnMouseOverInput(Action callback)
{
if (!graph) {
return false;
}
foreach (var node in graph.nodes) {
foreach (var input in node.Inputs) {
if (IsUnderMouse(input.bodyRect)) {
callback(input);
return true;
}
}
}
return false;
}
///
/// Tests if the mouse is over the node or the input.
///
///
///
public bool OnMouseOverNode_OrInput(Action callback)
{
if (!graph) {
return false;
}
foreach (var node in graph.nodes) {
if (IsUnderMouse(node.bodyRect)) {
callback(node);
return true;
}
// Check inputs
else {
foreach (var input in node.Inputs) {
if (IsUnderMouse(input.bodyRect)) {
callback(node);
return true;
}
}
}
}
// No node under mouse.
return false;
}
#endregion
#region Styles
private GUIStyle _backgroundStyle;
private GUIStyle backgroundStyle
{
get
{
if (_backgroundStyle == null) {
_backgroundStyle = new GUIStyle(GUI.skin.box);
_backgroundStyle.normal.background = _backTex;
}
return _backgroundStyle;
}
}
private static Rect _modeStatusRect = new Rect(20f, 20f, 250f, 150f);
private static GUIStyle _modeStatusStyle;
private static GUIStyle ModeStatusStyle
{
get
{
if (_modeStatusStyle == null) {
_modeStatusStyle = new GUIStyle();
_modeStatusStyle.fontSize = 36;
_modeStatusStyle.fontStyle = FontStyle.Bold;
_modeStatusStyle.normal.textColor = new Color(1f, 1f, 1f, 0.2f);
}
return _modeStatusStyle;
}
}
#endregion
}
}