1
0
mirror of https://github.com/Siccity/xNode.git synced 2025-12-20 17:26:02 +08:00
xNode/Scripts/Editor/NodeEditorGUI.cs
Adsitoz 3ba0b13c77 Zoom System Fixes (#155)
- Due to the OnGUI method being called last to draw over everything else in the window, the GUI.Group being created in the new zoom system was also moving around the GUI elements created in the NodeGraphEditor OnGUI method.
2019-06-18 11:34:23 +02:00

459 lines
22 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace XNodeEditor {
/// <summary> Contains GUI methods </summary>
public partial class NodeEditorWindow {
public NodeGraphEditor graphEditor;
private List<UnityEngine.Object> selectionCache;
private List<XNode.Node> culledNodes;
/// <summary> 19 if docked, 22 if not </summary>
private int topPadding { get { return isDocked() ? 19 : 22; } }
/// <summary> 0 if docked, 3 if not </summary>
private int leftPadding { get { return isDocked() ? 2 : 0; } }
/// <summary> Executed after all other window GUI. Useful if Zoom is ruining your day. Automatically resets after being run.</summary>
public event Action onLateGUI;
private Matrix4x4 prevGuiMatrix;
private void OnGUI() {
Event e = Event.current;
Matrix4x4 m = GUI.matrix;
if (graph == null) return;
ValidateGraphEditor();
Controls();
DrawGrid(position, zoom, panOffset);
DrawConnections();
DrawDraggedConnection();
DrawNodes();
DrawSelectionBox();
DrawTooltip();
DrawGraphOnGUI();
// Run and reset onLateGUI
if (onLateGUI != null) {
onLateGUI();
onLateGUI = null;
}
GUI.matrix = m;
}
public void BeginZoomed() {
GUI.EndGroup();
Rect position = new Rect(this.position);
position.x = 0;
position.y = topPadding;
Vector2 topLeft = new Vector2(position.xMin, position.yMin - topPadding);
Rect clippedArea = ScaleSizeBy(position, zoom, topLeft);
GUI.BeginGroup(clippedArea);
prevGuiMatrix = GUI.matrix;
Matrix4x4 translation = Matrix4x4.TRS(Vector3.zero, Quaternion.identity, Vector3.one);
Matrix4x4 scale = Matrix4x4.Scale(new Vector3(1.0f / zoom, 1.0f / zoom, 1.0f));
GUI.matrix = translation * scale * translation.inverse * GUI.matrix;
}
public void EndZoomed() {
GUI.matrix = prevGuiMatrix;
GUI.EndGroup();
GUI.BeginGroup(new Rect(0.0f, topPadding - (topPadding * zoom), Screen.width, Screen.height));
}
/// <summary> Ends the GUI Group temporarily to draw any additional elements in the NodeGraphEditor. </summary>
private void DrawGraphOnGUI() {
GUI.EndGroup();
Rect rect = new Rect(new Vector2(leftPadding, topPadding), new Vector2(Screen.width, Screen.height));
GUI.BeginGroup(rect);
graphEditor.OnGUI();
GUI.EndGroup();
GUI.BeginGroup(new Rect(0.0f, topPadding - (topPadding * zoom), Screen.width, Screen.height));
}
public static Rect ScaleSizeBy(Rect rect, float scale, Vector2 pivotPoint) {
Rect result = rect;
result.x -= pivotPoint.x;
result.y -= pivotPoint.y;
result.xMin *= scale;
result.xMax *= scale;
result.yMin *= scale;
result.yMax *= scale;
result.x += pivotPoint.x;
result.y += pivotPoint.y;
return result;
}
public void DrawGrid(Rect rect, float zoom, Vector2 panOffset) {
rect.position = Vector2.zero;
Vector2 center = rect.size / 2f;
Texture2D gridTex = graphEditor.GetGridTexture();
Texture2D crossTex = graphEditor.GetSecondaryGridTexture();
// Offset from origin in tile units
float xOffset = -(center.x * zoom + panOffset.x) / gridTex.width;
float yOffset = ((center.y - rect.size.y) * zoom + panOffset.y) / gridTex.height;
Vector2 tileOffset = new Vector2(xOffset, yOffset);
// Amount of tiles
float tileAmountX = Mathf.Round(rect.size.x * zoom) / gridTex.width;
float tileAmountY = Mathf.Round(rect.size.y * zoom) / gridTex.height;
Vector2 tileAmount = new Vector2(tileAmountX, tileAmountY);
// Draw tiled background
GUI.DrawTextureWithTexCoords(rect, gridTex, new Rect(tileOffset, tileAmount));
GUI.DrawTextureWithTexCoords(rect, crossTex, new Rect(tileOffset + new Vector2(0.5f, 0.5f), tileAmount));
}
public void DrawSelectionBox() {
if (currentActivity == NodeActivity.DragGrid) {
Vector2 curPos = WindowToGridPosition(Event.current.mousePosition);
Vector2 size = curPos - dragBoxStart;
Rect r = new Rect(dragBoxStart, size);
r.position = GridToWindowPosition(r.position);
r.size /= zoom;
Handles.DrawSolidRectangleWithOutline(r, new Color(0, 0, 0, 0.1f), new Color(1, 1, 1, 0.6f));
}
}
public static bool DropdownButton(string name, float width) {
return GUILayout.Button(name, EditorStyles.toolbarDropDown, GUILayout.Width(width));
}
/// <summary> Show right-click context menu for hovered reroute </summary>
void ShowRerouteContextMenu(RerouteReference reroute) {
GenericMenu contextMenu = new GenericMenu();
contextMenu.AddItem(new GUIContent("Remove"), false, () => reroute.RemovePoint());
contextMenu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero));
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
}
/// <summary> Show right-click context menu for hovered port </summary>
void ShowPortContextMenu(XNode.NodePort hoveredPort) {
GenericMenu contextMenu = new GenericMenu();
contextMenu.AddItem(new GUIContent("Clear Connections"), false, () => hoveredPort.ClearConnections());
contextMenu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero));
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
}
/// <summary> Draw a bezier from output to input in grid coordinates </summary>
public void DrawNoodle(Color col, List<Vector2> gridPoints) {
Vector2[] windowPoints = gridPoints.Select(x => GridToWindowPosition(x)).ToArray();
Handles.color = col;
int length = gridPoints.Count;
switch (NodeEditorPreferences.GetSettings().noodleType) {
case NodeEditorPreferences.NoodleType.Curve:
Vector2 outputTangent = Vector2.right;
for (int i = 0; i < length - 1; i++) {
Vector2 inputTangent = Vector2.left;
if (i == 0) outputTangent = Vector2.right * Vector2.Distance(windowPoints[i], windowPoints[i + 1]) * 0.01f * zoom;
if (i < length - 2) {
Vector2 ab = (windowPoints[i + 1] - windowPoints[i]).normalized;
Vector2 cb = (windowPoints[i + 1] - windowPoints[i + 2]).normalized;
Vector2 ac = (windowPoints[i + 2] - windowPoints[i]).normalized;
Vector2 p = (ab + cb) * 0.5f;
float tangentLength = (Vector2.Distance(windowPoints[i], windowPoints[i + 1]) + Vector2.Distance(windowPoints[i + 1], windowPoints[i + 2])) * 0.005f * zoom;
float side = ((ac.x * (windowPoints[i + 1].y - windowPoints[i].y)) - (ac.y * (windowPoints[i + 1].x - windowPoints[i].x)));
p = new Vector2(-p.y, p.x) * Mathf.Sign(side) * tangentLength;
inputTangent = p;
} else {
inputTangent = Vector2.left * Vector2.Distance(windowPoints[i], windowPoints[i + 1]) * 0.01f * zoom;
}
Handles.DrawBezier(windowPoints[i], windowPoints[i + 1], windowPoints[i] + ((outputTangent * 50) / zoom), windowPoints[i + 1] + ((inputTangent * 50) / zoom), col, null, 4);
outputTangent = -inputTangent;
}
break;
case NodeEditorPreferences.NoodleType.Line:
for (int i = 0; i < length - 1; i++) {
Handles.DrawAAPolyLine(5, windowPoints[i], windowPoints[i + 1]);
}
break;
case NodeEditorPreferences.NoodleType.Angled:
for (int i = 0; i < length - 1; i++) {
if (i == length - 1) continue; // Skip last index
if (windowPoints[i].x <= windowPoints[i + 1].x - (50 / zoom)) {
float midpoint = (windowPoints[i].x + windowPoints[i + 1].x) * 0.5f;
Vector2 start_1 = windowPoints[i];
Vector2 end_1 = windowPoints[i + 1];
start_1.x = midpoint;
end_1.x = midpoint;
Handles.DrawAAPolyLine(5, windowPoints[i], start_1);
Handles.DrawAAPolyLine(5, start_1, end_1);
Handles.DrawAAPolyLine(5, end_1, windowPoints[i + 1]);
} else {
float midpoint = (windowPoints[i].y + windowPoints[i + 1].y) * 0.5f;
Vector2 start_1 = windowPoints[i];
Vector2 end_1 = windowPoints[i + 1];
start_1.x += 25 / zoom;
end_1.x -= 25 / zoom;
Vector2 start_2 = start_1;
Vector2 end_2 = end_1;
start_2.y = midpoint;
end_2.y = midpoint;
Handles.DrawAAPolyLine(5, windowPoints[i], start_1);
Handles.DrawAAPolyLine(5, start_1, start_2);
Handles.DrawAAPolyLine(5, start_2, end_2);
Handles.DrawAAPolyLine(5, end_2, end_1);
Handles.DrawAAPolyLine(5, end_1, windowPoints[i + 1]);
}
}
break;
}
}
/// <summary> Draws all connections </summary>
public void DrawConnections() {
Vector2 mousePos = Event.current.mousePosition;
List<RerouteReference> selection = preBoxSelectionReroute != null ? new List<RerouteReference>(preBoxSelectionReroute) : new List<RerouteReference>();
hoveredReroute = new RerouteReference();
Color col = GUI.color;
foreach (XNode.Node node in graph.nodes) {
//If a null node is found, return. This can happen if the nodes associated script is deleted. It is currently not possible in Unity to delete a null asset.
if (node == null) continue;
// Draw full connections and output > reroute
foreach (XNode.NodePort output in node.Outputs) {
//Needs cleanup. Null checks are ugly
Rect fromRect;
if (!_portConnectionPoints.TryGetValue(output, out fromRect)) continue;
Color connectionColor = graphEditor.GetPortColor(output);
for (int k = 0; k < output.ConnectionCount; k++) {
XNode.NodePort input = output.GetConnection(k);
// Error handling
if (input == null) continue; //If a script has been updated and the port doesn't exist, it is removed and null is returned. If this happens, return.
if (!input.IsConnectedTo(output)) input.Connect(output);
Rect toRect;
if (!_portConnectionPoints.TryGetValue(input, out toRect)) continue;
List<Vector2> reroutePoints = output.GetReroutePoints(k);
List<Vector2> gridPoints = new List<Vector2>();
gridPoints.Add(fromRect.center);
gridPoints.AddRange(reroutePoints);
gridPoints.Add(toRect.center);
DrawNoodle(connectionColor, gridPoints);
// Loop through reroute points again and draw the points
for (int i = 0; i < reroutePoints.Count; i++) {
RerouteReference rerouteRef = new RerouteReference(output, k, i);
// Draw reroute point at position
Rect rect = new Rect(reroutePoints[i], new Vector2(12, 12));
rect.position = new Vector2(rect.position.x - 6, rect.position.y - 6);
rect = GridToWindowRect(rect);
// Draw selected reroute points with an outline
if (selectedReroutes.Contains(rerouteRef)) {
GUI.color = NodeEditorPreferences.GetSettings().highlightColor;
GUI.DrawTexture(rect, NodeEditorResources.dotOuter);
}
GUI.color = connectionColor;
GUI.DrawTexture(rect, NodeEditorResources.dot);
if (rect.Overlaps(selectionBox)) selection.Add(rerouteRef);
if (rect.Contains(mousePos)) hoveredReroute = rerouteRef;
}
}
}
}
GUI.color = col;
if (Event.current.type != EventType.Layout && currentActivity == NodeActivity.DragGrid) selectedReroutes = selection;
}
private void DrawNodes() {
Event e = Event.current;
if (e.type == EventType.Layout) {
selectionCache = new List<UnityEngine.Object>(Selection.objects);
}
System.Reflection.MethodInfo onValidate = null;
if (Selection.activeObject != null && Selection.activeObject is XNode.Node) {
onValidate = Selection.activeObject.GetType().GetMethod("OnValidate");
if (onValidate != null) EditorGUI.BeginChangeCheck();
}
BeginZoomed();
Vector2 mousePos = Event.current.mousePosition;
if (e.type != EventType.Layout) {
hoveredNode = null;
hoveredPort = null;
}
List<UnityEngine.Object> preSelection = preBoxSelection != null ? new List<UnityEngine.Object>(preBoxSelection) : new List<UnityEngine.Object>();
// Selection box stuff
Vector2 boxStartPos = GridToWindowPositionNoClipped(dragBoxStart);
Vector2 boxSize = mousePos - boxStartPos;
if (boxSize.x < 0) { boxStartPos.x += boxSize.x; boxSize.x = Mathf.Abs(boxSize.x); }
if (boxSize.y < 0) { boxStartPos.y += boxSize.y; boxSize.y = Mathf.Abs(boxSize.y); }
Rect selectionBox = new Rect(boxStartPos, boxSize);
//Save guiColor so we can revert it
Color guiColor = GUI.color;
if (e.type == EventType.Layout) culledNodes = new List<XNode.Node>();
for (int n = 0; n < graph.nodes.Count; n++) {
// Skip null nodes. The user could be in the process of renaming scripts, so removing them at this point is not advisable.
if (graph.nodes[n] == null) continue;
if (n >= graph.nodes.Count) return;
XNode.Node node = graph.nodes[n];
// Culling
if (e.type == EventType.Layout) {
// Cull unselected nodes outside view
if (!Selection.Contains(node) && ShouldBeCulled(node)) {
culledNodes.Add(node);
continue;
}
} else if (culledNodes.Contains(node)) continue;
if (e.type == EventType.Repaint) {
_portConnectionPoints = _portConnectionPoints.Where(x => x.Key.node != node).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}
NodeEditor nodeEditor = NodeEditor.GetEditor(node, this);
NodeEditor.portPositions.Clear();
//Get node position
Vector2 nodePos = GridToWindowPositionNoClipped(node.position);
GUILayout.BeginArea(new Rect(nodePos, new Vector2(nodeEditor.GetWidth(), 4000)));
bool selected = selectionCache.Contains(graph.nodes[n]);
if (selected) {
GUIStyle style = new GUIStyle(nodeEditor.GetBodyStyle());
GUIStyle highlightStyle = new GUIStyle(NodeEditorResources.styles.nodeHighlight);
highlightStyle.padding = style.padding;
style.padding = new RectOffset();
GUI.color = nodeEditor.GetTint();
GUILayout.BeginVertical(style);
GUI.color = NodeEditorPreferences.GetSettings().highlightColor;
GUILayout.BeginVertical(new GUIStyle(highlightStyle));
} else {
GUIStyle style = new GUIStyle(nodeEditor.GetBodyStyle());
GUI.color = nodeEditor.GetTint();
GUILayout.BeginVertical(style);
}
GUI.color = guiColor;
EditorGUI.BeginChangeCheck();
//Draw node contents
nodeEditor.OnHeaderGUI();
nodeEditor.OnBodyGUI();
//If user changed a value, notify other scripts through onUpdateNode
if (EditorGUI.EndChangeCheck()) {
if (NodeEditor.onUpdateNode != null) NodeEditor.onUpdateNode(node);
EditorUtility.SetDirty(node);
nodeEditor.serializedObject.ApplyModifiedProperties();
}
GUILayout.EndVertical();
//Cache data about the node for next frame
if (e.type == EventType.Repaint) {
Vector2 size = GUILayoutUtility.GetLastRect().size;
if (nodeSizes.ContainsKey(node)) nodeSizes[node] = size;
else nodeSizes.Add(node, size);
foreach (var kvp in NodeEditor.portPositions) {
Vector2 portHandlePos = kvp.Value;
portHandlePos += node.position;
Rect rect = new Rect(portHandlePos.x - 8, portHandlePos.y - 8, 16, 16);
portConnectionPoints[kvp.Key] = rect;
}
}
if (selected) GUILayout.EndVertical();
if (e.type != EventType.Layout) {
//Check if we are hovering this node
Vector2 nodeSize = GUILayoutUtility.GetLastRect().size;
Rect windowRect = new Rect(nodePos, nodeSize);
if (windowRect.Contains(mousePos)) hoveredNode = node;
//If dragging a selection box, add nodes inside to selection
if (currentActivity == NodeActivity.DragGrid) {
if (windowRect.Overlaps(selectionBox)) preSelection.Add(node);
}
//Check if we are hovering any of this nodes ports
//Check input ports
foreach (XNode.NodePort input in node.Inputs) {
//Check if port rect is available
if (!portConnectionPoints.ContainsKey(input)) continue;
Rect r = GridToWindowRectNoClipped(portConnectionPoints[input]);
if (r.Contains(mousePos)) hoveredPort = input;
}
//Check all output ports
foreach (XNode.NodePort output in node.Outputs) {
//Check if port rect is available
if (!portConnectionPoints.ContainsKey(output)) continue;
Rect r = GridToWindowRectNoClipped(portConnectionPoints[output]);
if (r.Contains(mousePos)) hoveredPort = output;
}
}
GUILayout.EndArea();
}
if (e.type != EventType.Layout && currentActivity == NodeActivity.DragGrid) Selection.objects = preSelection.ToArray();
EndZoomed();
//If a change in is detected in the selected node, call OnValidate method.
//This is done through reflection because OnValidate is only relevant in editor,
//and thus, the code should not be included in build.
if (onValidate != null && EditorGUI.EndChangeCheck()) onValidate.Invoke(Selection.activeObject, null);
}
private bool ShouldBeCulled(XNode.Node node) {
Vector2 nodePos = GridToWindowPositionNoClipped(node.position);
if (nodePos.x / _zoom > position.width) return true; // Right
else if (nodePos.y / _zoom > position.height) return true; // Bottom
else if (nodeSizes.ContainsKey(node)) {
Vector2 size = nodeSizes[node];
if (nodePos.x + size.x < 0) return true; // Left
else if (nodePos.y + size.y < 0) return true; // Top
}
return false;
}
private void DrawTooltip() {
if (hoveredPort != null && NodeEditorPreferences.GetSettings().portTooltips) {
Type type = hoveredPort.ValueType;
GUIContent content = new GUIContent();
content.text = type.PrettyName();
if (hoveredPort.IsOutput) {
object obj = hoveredPort.node.GetValue(hoveredPort);
content.text += " = " + (obj != null ? obj.ToString() : "null");
}
Vector2 size = NodeEditorResources.styles.tooltip.CalcSize(content);
Rect rect = new Rect(Event.current.mousePosition - (size), size);
EditorGUI.LabelField(rect, content, NodeEditorResources.styles.tooltip);
Repaint();
}
}
}
}