summaryrefslogtreecommitdiff
path: root/Other/NodeEditorExamples/Assets/UNEB/Editor
diff options
context:
space:
mode:
Diffstat (limited to 'Other/NodeEditorExamples/Assets/UNEB/Editor')
-rw-r--r--Other/NodeEditorExamples/Assets/UNEB/Editor/Actions.meta9
-rw-r--r--Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/ActionBase.cs23
-rw-r--r--Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/ActionBase.cs.meta12
-rw-r--r--Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/ActionManager.cs169
-rw-r--r--Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/ActionManager.cs.meta12
-rw-r--r--Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/ActionTriggerSystem.cs456
-rw-r--r--Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/ActionTriggerSystem.cs.meta12
-rw-r--r--Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/CreateConnection.cs106
-rw-r--r--Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/CreateConnection.cs.meta12
-rw-r--r--Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/CreateNodeAction.cs55
-rw-r--r--Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/CreateNodeAction.cs.meta12
-rw-r--r--Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/DeleteNodeAction.cs128
-rw-r--r--Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/DeleteNodeAction.cs.meta12
-rw-r--r--Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/DragNode.cs42
-rw-r--r--Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/DragNode.cs.meta12
-rw-r--r--Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/MultiStageAction.cs18
-rw-r--r--Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/MultiStageAction.cs.meta12
-rw-r--r--Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/RemoveConnection.cs32
-rw-r--r--Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/RemoveConnection.cs.meta12
-rw-r--r--Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/UndoableAction.cs12
-rw-r--r--Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/UndoableAction.cs.meta12
-rw-r--r--Other/NodeEditorExamples/Assets/UNEB/Editor/NodeEditor.cs665
-rw-r--r--Other/NodeEditorExamples/Assets/UNEB/Editor/NodeEditor.cs.meta12
-rw-r--r--Other/NodeEditorExamples/Assets/UNEB/Editor/NodeEditorWindow.cs320
-rw-r--r--Other/NodeEditorExamples/Assets/UNEB/Editor/NodeEditorWindow.cs.meta12
-rw-r--r--Other/NodeEditorExamples/Assets/UNEB/Editor/SaveManager.cs473
-rw-r--r--Other/NodeEditorExamples/Assets/UNEB/Editor/SaveManager.cs.meta12
27 files changed, 2664 insertions, 0 deletions
diff --git a/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions.meta b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions.meta
new file mode 100644
index 00000000..f7f8ff4e
--- /dev/null
+++ b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions.meta
@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 5357f731dedd7cc468c40f66bcd4487f
+folderAsset: yes
+timeCreated: 1501781574
+licenseType: Free
+DefaultImporter:
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/ActionBase.cs b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/ActionBase.cs
new file mode 100644
index 00000000..426c8a50
--- /dev/null
+++ b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/ActionBase.cs
@@ -0,0 +1,23 @@
+
+namespace UNEB
+{
+ public abstract class ActionBase
+ {
+
+ public ActionManager manager;
+
+ /// <summary>
+ /// Can be used to check if the action is a valid state for furthur execution.
+ /// For example, we only want to run delete node if a node is selected for deletion.
+ /// </summary>
+ /// <returns></returns>
+ public virtual bool Init() { return true; }
+
+ public abstract void Do();
+
+ /// <summary>
+ /// Called when the action is removed from the undo/redo buffers.
+ /// </summary>
+ public virtual void OnDestroy() { }
+ }
+} \ No newline at end of file
diff --git a/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/ActionBase.cs.meta b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/ActionBase.cs.meta
new file mode 100644
index 00000000..eb9d005c
--- /dev/null
+++ b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/ActionBase.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 5dfa0b77744c2bc4eb973c5114f5de79
+timeCreated: 1501781587
+licenseType: Free
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/ActionManager.cs b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/ActionManager.cs
new file mode 100644
index 00000000..b0450b46
--- /dev/null
+++ b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/ActionManager.cs
@@ -0,0 +1,169 @@
+
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+using UNEB.Utility;
+
+namespace UNEB
+{
+ /// <summary>
+ /// Handles execution of actions, undo, and redo.
+ /// </summary>
+ public class ActionManager
+ {
+ private NodeEditorWindow _window;
+
+ private FiniteStack<UndoableAction> _undoStack;
+ private Stack<UndoableAction> _redoStack;
+
+ // Caches the current multi-stage action that is currently executing.
+ private MultiStageAction _activeMultiAction = null;
+
+ public event Action OnUndo;
+ public event Action OnRedo;
+
+ public ActionManager(NodeEditorWindow w)
+ {
+ _undoStack = new FiniteStack<UndoableAction>(100);
+ _redoStack = new Stack<UndoableAction>();
+
+ _window = w;
+
+ // Makes sure that the action cleans up after itself
+ // when it is removed from the undo buffer.
+ _undoStack.OnRemoveBottomItem += (action) =>
+ {
+ action.OnDestroy();
+ };
+ }
+
+ public void Update()
+ {
+ if (IsRunningAction) {
+ _activeMultiAction.Do();
+ }
+ }
+
+ public bool IsRunningAction
+ {
+ get { return _activeMultiAction != null; }
+ }
+
+ /// <summary>
+ /// Runs an action and stores it in the undo stack.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ public void RunUndoableAction<T>() where T : UndoableAction, new()
+ {
+ T action = new T();
+ action.manager = this;
+
+ if (action.Init()) {
+
+ clearRedoStack();
+ _undoStack.Push(action);
+ action.Do();
+ }
+ }
+
+ /// <summary>
+ /// Starts a multi stage action but does not record it in the undo stack.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ public void StartMultiStageAction<T>() where T : MultiStageAction, new()
+ {
+ // Only run 1 multi-action at a time.
+ if (_activeMultiAction != null) {
+ return;
+ }
+
+ T action = new T();
+ action.manager = this;
+
+ if (action.Init()) {
+ _activeMultiAction = action;
+ _activeMultiAction.OnActionStart();
+ }
+ }
+
+ /// <summary>
+ /// Records the multi-stage action in the undo stack.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="action"></param>
+ public void FinishMultiStageAction()
+ {
+ if (_activeMultiAction == null) {
+ return;
+ }
+
+ // We check if the action ended properly so it can be stored in undo.
+ if (_activeMultiAction.OnActionEnd()) {
+
+ clearRedoStack();
+ _undoStack.Push(_activeMultiAction);
+ }
+
+ // There is no longer an active multi-stage action.
+ _activeMultiAction = null;
+ }
+
+ public void UndoAction()
+ {
+ if (_undoStack.Count != 0) {
+
+ var action = _undoStack.Pop();
+ _redoStack.Push(action);
+
+ action.Undo();
+
+ if (OnUndo != null)
+ OnUndo();
+ }
+ }
+
+ public void RedoAction()
+ {
+ if (_redoStack.Count != 0) {
+
+ var action = _redoStack.Pop();
+ _undoStack.Push(action);
+
+ action.Redo();
+
+ if (OnRedo != null)
+ OnRedo();
+ }
+ }
+
+ public void Reset()
+ {
+ _activeMultiAction = null;
+ clearUndoStack();
+ clearRedoStack();
+ }
+
+ private void clearRedoStack()
+ {
+ foreach (var action in _redoStack) {
+ action.OnDestroy();
+ }
+
+ _redoStack.Clear();
+ }
+
+ private void clearUndoStack()
+ {
+ foreach (var action in _undoStack) {
+ action.OnDestroy();
+ }
+
+ _undoStack.Clear();
+ }
+
+ public NodeEditorWindow window
+ {
+ get { return _window; }
+ }
+ }
+} \ No newline at end of file
diff --git a/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/ActionManager.cs.meta b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/ActionManager.cs.meta
new file mode 100644
index 00000000..3cc64c36
--- /dev/null
+++ b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/ActionManager.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 62fe01f31afcc5a4ab446003a7360cb3
+timeCreated: 1501892528
+licenseType: Free
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/ActionTriggerSystem.cs b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/ActionTriggerSystem.cs
new file mode 100644
index 00000000..d5089754
--- /dev/null
+++ b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/ActionTriggerSystem.cs
@@ -0,0 +1,456 @@
+
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEditor;
+using UNEB.Utility;
+using System.Reflection;
+using System.Linq;
+
+namespace UNEB
+{
+ public class ActionTriggerSystem
+ {
+ public List<TriggerMapping> triggers;
+
+ /// <summary>
+ /// Passive triggers do not interrupt the next possible trigger.
+ /// </summary>
+ public List<TriggerMapping> passiveTriggers;
+
+ private ActionManager _manager;
+ private TriggerMapping _focus;
+
+ public ActionTriggerSystem(ActionManager m)
+ {
+ _manager = m;
+
+ triggers = new List<TriggerMapping>();
+ passiveTriggers = new List<TriggerMapping>();
+
+ setupStandardTriggers();
+ }
+
+ public void Update()
+ {
+ foreach (TriggerMapping tm in triggers) {
+ if (tm.AllTriggersSatisfied()) {
+ tm.action();
+ return;
+ }
+ }
+
+ foreach (TriggerMapping tm in passiveTriggers) {
+ if (tm.AllTriggersSatisfied()) {
+ tm.action();
+ }
+ }
+
+ // Block all key inputs from passing through the Unity Editor
+ if (Event.current.isKey)
+ Event.current.Use();
+ }
+
+ private void setupStandardTriggers()
+ {
+ setupImmediateTriggers();
+ setupContextTriggers();
+ setupMultiStageTriggers();
+ }
+
+ private void setupImmediateTriggers()
+ {
+ var panInput = Create<InputTrigger>().Mouse(EventType.MouseDrag, InputTrigger.Button.Wheel);
+ panInput.action = () =>
+ {
+ window.editor.Pan(Event.current.delta);
+ window.Repaint();
+ };
+
+ var zoomInput = Create<InputTrigger>().Key(EventType.ScrollWheel, KeyCode.None, false, false);
+ zoomInput.action = () =>
+ {
+ window.editor.Zoom(Event.current.delta.y);
+ window.Repaint();
+ };
+
+ var selectSingle = Create<InputTrigger>().Mouse(EventType.MouseDown, InputTrigger.Button.Left);
+ selectSingle.action = () =>
+ {
+ bool bResult = window.editor.OnMouseOverNode(onSingleSelected);
+
+ // If the canvas is clicked then remove focus of GUI elements.
+ if (!bResult) {
+ GUI.FocusControl(null);
+ window.Repaint();
+ }
+ };
+
+ var undoInput = Create<InputTrigger>().Key(EventType.KeyDown, KeyCode.Z, true, false);
+ undoInput.action = _manager.UndoAction;
+
+ var redoInput = Create<InputTrigger>().Key(EventType.KeyDown, KeyCode.Y, true, false);
+ redoInput.action = _manager.RedoAction;
+
+ var recordClick = Create<InputTrigger>().EventOnly(EventType.MouseDown);
+ recordClick.action = () => { window.state.lastClickedPosition = window.editor.MousePosition(); };
+
+ var homeView = Create<InputTrigger>().Key(EventType.KeyDown, KeyCode.F, false, false);
+ homeView.action = window.editor.HomeView;
+ }
+
+ private void setupContextTriggers()
+ {
+ setupNodeCreateMenu();
+
+ Pair<string, Action>[] nodeContext =
+ {
+ ContextItem("Copy Node", () => { Debug.Log("Not Implemented"); }),
+ ContextItem("Delete Node", _manager.RunUndoableAction<DeleteNodeAction>)
+ };
+
+ var nodeTrigger = Create<ContextTrigger>().Build(nodeContext).EventOnly(EventType.ContextClick);
+ nodeTrigger.triggers.Add(isMouseOverNode);
+ nodeTrigger.triggers.Add(isGraphValid);
+ }
+
+ private void setupNodeCreateMenu()
+ {
+ //Get all classes deriving from Node via reflection
+ Type derivedType = typeof(Node);
+ Assembly assembly = Assembly.GetAssembly(derivedType);
+
+ List<Type> nodeTypes = assembly
+ .GetTypes()
+ .Where(t =>
+ t != derivedType &&
+ derivedType.IsAssignableFrom(t)
+ ).ToList();
+
+ //Populate canvasContext with entries for all node types
+ var canvasContext = new Pair<string, Action>[nodeTypes.Count];
+
+ for (int i = 0; i < nodeTypes.Count; i++) {
+
+ Type nodeType = nodeTypes[i];
+ Action createNode = () =>
+ {
+ _manager.window.state.typeToCreate = nodeType;
+ _manager.RunUndoableAction<CreateNodeAction>();
+ };
+
+ string name = ObjectNames.NicifyVariableName(nodeType.Name);
+ canvasContext[i] = ContextItem(name, createNode);
+ }
+
+ var canvasTrigger = Create<ContextTrigger>().Build(canvasContext).EventOnly(EventType.ContextClick);
+ canvasTrigger.triggers.Add(isMouseOverCanvas);
+ canvasTrigger.triggers.Add(isGraphValid);
+ }
+
+ private void setupMultiStageTriggers()
+ {
+ setupNodeDrag();
+ setupNodeConnection();
+ }
+
+ private void setupNodeDrag()
+ {
+ var endDragInput = Create<InputTrigger>(false, true).Mouse(EventType.MouseUp, InputTrigger.Button.Left);
+ var runningDragInput = Create<InputTrigger>(false, true).EventOnly(EventType.MouseDrag);
+ var startDragInput = Create<InputTrigger>(false, true).Mouse(EventType.MouseDown, InputTrigger.Button.Left);
+
+ startDragInput.triggers.Add(isMouseOverNode);
+ startDragInput.action = _manager.StartMultiStageAction<DragNode>;
+
+ endDragInput.action = _manager.FinishMultiStageAction;
+
+ runningDragInput.action = _manager.Update;
+ runningDragInput.action += window.Repaint;
+
+ new MultiStageInputTrigger(startDragInput, endDragInput, runningDragInput);
+ }
+
+ private void setupNodeConnection()
+ {
+ var endConnInput = Create<InputTrigger>(false, true).Mouse(EventType.MouseUp, InputTrigger.Button.Left);
+ var runningConnInput = Create<InputTrigger>(false, true).EventOnly(EventType.MouseDrag);
+ var startConnInput = Create<InputTrigger>(false, true).Mouse(EventType.MouseDown, InputTrigger.Button.Left);
+
+ Func<bool> knobCondition = () => { return isMouseOverOutput() || isMouseOverInputStartConn(); };
+
+ startConnInput.triggers.Add(knobCondition);
+ startConnInput.triggers.Add(isOutputSelected);
+
+ startConnInput.action = _manager.StartMultiStageAction<CreateConnection>;
+
+ endConnInput.action = _manager.FinishMultiStageAction;
+ endConnInput.action += window.Repaint;
+
+ runningConnInput.action = _manager.Update;
+ runningConnInput.action += window.Repaint;
+
+ new MultiStageInputTrigger(startConnInput, endConnInput, runningConnInput);
+ }
+
+ /// <summary>
+ /// Create a trigger mapping and store it in the triggers list.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <returns></returns>
+ public T Create<T>(bool isPassive = false, bool pushToFront = false) where T : TriggerMapping, new()
+ {
+ T mapping = new T();
+
+ if (isPassive) {
+
+ if (pushToFront && passiveTriggers.Count > 0) passiveTriggers.Insert(0, mapping);
+ else passiveTriggers.Add(mapping);
+ }
+
+ else {
+
+ if (pushToFront && triggers.Count > 0) triggers.Insert(0, mapping);
+ else triggers.Add(mapping);
+ }
+
+ return mapping;
+ }
+
+ private void onSingleSelected(Node node)
+ {
+ _manager.window.state.selectedNode = node;
+ _manager.window.graph.PushToEnd(node);
+
+ Selection.activeObject = node;
+ }
+
+ private void onOutputKnobSelected(NodeOutput output)
+ {
+ _manager.window.state.selectedOutput = output;
+ }
+
+ private bool isMouseOverNode()
+ {
+ return window.editor.OnMouseOverNode(onSingleSelected);
+ }
+
+ private bool isMouseOverCanvas()
+ {
+ return !isMouseOverNode();
+ }
+
+ private bool isMouseOverOutput()
+ {
+ return window.editor.OnMouseOverOutput(onOutputKnobSelected);
+ }
+
+ private bool isMouseOverInputStartConn()
+ {
+ Action<NodeInput> startConnFromInput = (NodeInput input) =>
+ {
+ window.state.selectedOutput = input.Outputs[0];
+
+ // Detach this input if we are starting a connection action from the input.
+ if (window.state.selectedOutput != null) {
+
+ window.state.selectedInput = input;
+ _manager.RunUndoableAction<RemoveConnection>();
+ }
+ };
+
+ return window.editor.OnMouseOverInput(startConnFromInput);
+ }
+
+ private bool isOutputSelected()
+ {
+ return window.state.selectedOutput != null;
+ }
+
+ private NodeEditorWindow window
+ {
+ get { return _manager.window; }
+ }
+
+ private bool isGraphValid()
+ {
+ return window.graph != null;
+ }
+
+ public Pair<string, Action> ContextItem(string label, Action a)
+ {
+ return new Pair<string, Action>(label, a);
+ }
+
+ /// <summary>
+ /// Maps a conditional trigger with an action.
+ /// </summary>
+ public class TriggerMapping
+ {
+ public List<Func<bool>> triggers = new List<Func<bool>>();
+ public Action action;
+
+ protected TriggerMapping() { }
+
+ public TriggerMapping(Func<bool> trigger, Action action)
+ {
+ ;
+ triggers.Add(trigger);
+ this.action = action;
+ }
+
+ public bool AllTriggersSatisfied()
+ {
+ foreach (var t in triggers) {
+ if (!t()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+
+ /// <summary>
+ /// Special trigger that uses input as a conditional.
+ /// </summary>
+ public class InputTrigger : TriggerMapping
+ {
+ public enum Button
+ {
+ Left = 0,
+ Right = 1,
+ Wheel = 2
+ }
+
+ private EventType t;
+ private KeyCode k;
+ private int button;
+ private bool bIsShift, bIsCtrl;
+
+ /// <summary>
+ /// Initialize the input mapping with a key trigger.
+ /// </summary>
+ /// <param name="type"></param>
+ /// <param name="key"></param>
+ /// <param name="bShift"></param>
+ /// <param name="bCtrl"></param>
+ /// <returns></returns>
+ public InputTrigger Key(EventType type, KeyCode key, bool bShift, bool bCtrl)
+ {
+ t = type;
+ k = key;
+
+ bIsShift = bShift;
+ bIsCtrl = bCtrl;
+
+ Func<bool> trigger = () =>
+ {
+ var e = Event.current;
+
+ return
+ e.type == t &&
+ e.keyCode == k &&
+ e.shift == bIsShift &&
+ e.control == bIsCtrl;
+ };
+
+ triggers.Add(trigger);
+ return this;
+ }
+
+ /// <summary>
+ /// Initialize the input mapping with a mouse button tirgger.
+ /// </summary>
+ /// <param name="type"></param>
+ /// <param name="mButton"></param>
+ /// <returns></returns>
+ public InputTrigger Mouse(EventType type, Button mButton)
+ {
+ t = type;
+ button = (int)mButton;
+
+ Func<bool> trigger = () =>
+ {
+ var e = Event.current;
+
+ return
+ e.type == t &&
+ e.button == button;
+ };
+
+ triggers.Add(trigger);
+ return this;
+ }
+
+ /// <summary>
+ /// Initializes the input mapping with the event.
+ /// </summary>
+ /// <param name="type"></param>
+ /// <returns></returns>
+ public InputTrigger EventOnly(EventType type)
+ {
+ t = type;
+ Func<bool> trigger = () => { return Event.current.type == t; };
+
+ triggers.Add(trigger);
+ return this;
+ }
+ }
+
+ /// <summary>
+ /// Special trigger that uses context menus to execute other actions.
+ /// </summary>
+ public class ContextTrigger : InputTrigger
+ {
+ private GenericMenu menu;
+
+ public ContextTrigger()
+ {
+ menu = new GenericMenu();
+ action = menu.ShowAsContext;
+ }
+
+ public ContextTrigger Build(params Pair<string, Action>[] contents)
+ {
+ foreach (var content in contents) {
+
+ string label = content.item1;
+ Action action = content.item2;
+
+ menu.AddItem(new GUIContent(label), false, () => { action(); });
+ }
+
+ return this;
+ }
+ }
+
+ public class MultiStageInputTrigger
+ {
+ private InputTrigger _startTrigger, _endTrigger, _runningTrigger;
+
+ private bool _bStarted = false;
+
+ public MultiStageInputTrigger(InputTrigger start, InputTrigger end, InputTrigger running)
+ {
+ start.action += () => { _bStarted = true; };
+ start.triggers.Add(hasNotStarted);
+
+ end.triggers.Add(HasStarted);
+ end.action += () => { _bStarted = false; };
+
+ running.triggers.Add(HasStarted);
+ }
+
+ public bool HasStarted()
+ {
+ return _bStarted;
+ }
+
+ private bool hasNotStarted()
+ {
+ return !_bStarted;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/ActionTriggerSystem.cs.meta b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/ActionTriggerSystem.cs.meta
new file mode 100644
index 00000000..6d66a0d4
--- /dev/null
+++ b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/ActionTriggerSystem.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 2b8fd837eb8876e4fa8c9b94d8b977f0
+timeCreated: 1501897267
+licenseType: Free
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/CreateConnection.cs b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/CreateConnection.cs
new file mode 100644
index 00000000..09d3e521
--- /dev/null
+++ b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/CreateConnection.cs
@@ -0,0 +1,106 @@
+
+using System.Linq;
+using System.Collections.Generic;
+
+using UnityEngine;
+using UnityEditor;
+
+namespace UNEB
+{
+ public class CreateConnection : MultiStageAction
+ {
+ private NodeInput _input;
+ private NodeOutput _output;
+
+ // The output of the old node it was connected to.
+ private NodeOutput _oldConnectedOutput;
+
+ // Old inputs of the node.
+ private List<NodeInput> _oldConnectedInputs;
+
+ public override void Do()
+ {
+ manager.window.state.selectedOutput = _output;
+ }
+
+ public override void Undo()
+ {
+ _output.Remove(_input);
+ reconnectOldConnections();
+ }
+
+ public override void Redo()
+ {
+ disconnectOldConnections();
+ _output.Add(_input);
+ }
+
+ private void reconnectOldConnections()
+ {
+ // Re-connect old connections
+ if (_oldConnectedOutput != null) {
+ _oldConnectedOutput.Add(_input);
+ }
+
+ if (_oldConnectedInputs != null) {
+ foreach (var input in _oldConnectedInputs) {
+ _output.Add(input);
+ }
+ }
+ }
+
+ private void disconnectOldConnections()
+ {
+ // Remove old connections
+ if (_oldConnectedOutput != null) {
+ _oldConnectedOutput.Remove(_input);
+ }
+
+ if (_oldConnectedInputs != null) {
+ _output.RemoveAll();
+ }
+ }
+
+ public override void OnActionStart()
+ {
+ _output = manager.window.state.selectedOutput;
+ }
+
+ public override bool OnActionEnd()
+ {
+ manager.window.state.selectedOutput = null;
+ manager.window.editor.OnMouseOverInput((input) => { _input = input; });
+
+ // Make the connection.
+ if (_input != null && _output.CanConnectInput(_input)) {
+
+ if (!_output.bCanHaveMultipleConnections)
+ {
+ _output.RemoveAll();
+ }
+
+ if (!_input.bCanHaveMultipleConnections) {
+ cacheOldConnections();
+ disconnectOldConnections();
+ }
+
+ return _output.Add(_input);
+ }
+
+ return false;
+ }
+
+ private void cacheOldConnections()
+ {
+ // Check if the receiving node was already connected.
+ if (_input != null && _input.HasOutputConnected()) {
+ _oldConnectedOutput = _input.Outputs[0];
+ }
+
+ // Check if the origin node already had inputs
+ if (!_output.bCanHaveMultipleConnections && _output.InputCount > 0) {
+ _oldConnectedInputs = _output.Inputs.ToList();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/CreateConnection.cs.meta b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/CreateConnection.cs.meta
new file mode 100644
index 00000000..5f6468b1
--- /dev/null
+++ b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/CreateConnection.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 89cf0324fa09d7d4b82a3308a2172117
+timeCreated: 1501807840
+licenseType: Free
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/CreateNodeAction.cs b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/CreateNodeAction.cs
new file mode 100644
index 00000000..522fa0cb
--- /dev/null
+++ b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/CreateNodeAction.cs
@@ -0,0 +1,55 @@
+
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace UNEB
+{
+ public class CreateNodeAction : UndoableAction
+ {
+ private NodeGraph _graph;
+ private Node _nodeCreated;
+
+ // The node referenced can only be destroyed if the
+ // create action has been undone.
+ private bool _bCanDeleteNode = false;
+
+ public override bool Init()
+ {
+ System.Type t = manager.window.state.typeToCreate;
+ return t != null && typeof(Node).IsAssignableFrom(t);
+ }
+
+ public override void Do()
+ {
+ _graph = manager.window.graph;
+
+ var state = manager.window.state;
+
+ _nodeCreated = SaveManager.CreateNode(state.typeToCreate, _graph);
+ _nodeCreated.bodyRect.position = manager.window.state.lastClickedPosition;
+
+ // Done with this type creation.
+ state.typeToCreate = null;
+ }
+
+ public override void Undo()
+ {
+ _graph.Remove(_nodeCreated);
+ _bCanDeleteNode = true;
+ }
+
+ public override void Redo()
+ {
+ _graph.Add(_nodeCreated);
+ _bCanDeleteNode = false;
+ }
+
+ public override void OnDestroy()
+ {
+ if (_bCanDeleteNode && _nodeCreated) {
+ ScriptableObject.DestroyImmediate(_nodeCreated, true);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/CreateNodeAction.cs.meta b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/CreateNodeAction.cs.meta
new file mode 100644
index 00000000..06f04a85
--- /dev/null
+++ b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/CreateNodeAction.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: f1c2e7f15404be3468c08ac729975c13
+timeCreated: 1501781600
+licenseType: Free
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/DeleteNodeAction.cs b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/DeleteNodeAction.cs
new file mode 100644
index 00000000..2e2e6672
--- /dev/null
+++ b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/DeleteNodeAction.cs
@@ -0,0 +1,128 @@
+
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using UnityEngine;
+using UNEB.Utility;
+
+namespace UNEB
+{
+ // Each input can have many outputs
+ using InputToOutputPair = Pair<NodeInput, List<NodeOutput>>;
+
+ // Each output can have many inputs
+ using OutputToInputsPair = Pair<NodeOutput, List<NodeInput>>;
+
+ public class DeleteNodeAction : UndoableAction
+ {
+ private NodeGraph _graph;
+ private Node _nodeRemoved = null;
+
+ private List<InputToOutputPair> _oldConnectedOutputs;
+ private List<OutputToInputsPair> _oldConnectedInputs;
+
+ // The node referenced can only be destroyed if the
+ // delete action has been done or redone.
+ private bool _bCanDeleteNode = false;
+
+ public DeleteNodeAction()
+ {
+
+ _oldConnectedOutputs = new List<InputToOutputPair>();
+ _oldConnectedInputs = new List<OutputToInputsPair>();
+ }
+
+ public override bool Init()
+ {
+ return manager.window.state.selectedNode != null;
+ }
+
+ public override void Do()
+ {
+ _graph = manager.window.graph;
+ _nodeRemoved = manager.window.state.selectedNode;
+ _graph.Remove(_nodeRemoved);
+
+ // Remember all the old outputs the inputs were connected to.
+ foreach (var input in _nodeRemoved.Inputs) {
+
+ if (input.HasOutputConnected()) {
+ _oldConnectedOutputs.Add(new InputToOutputPair(input, input.Outputs.ToList()));
+ }
+ }
+
+ // Remember all the old input connections that the outputs were connected to.
+ foreach (var output in _nodeRemoved.Outputs) {
+
+ if (output.InputCount != 0) {
+ _oldConnectedInputs.Add(new OutputToInputsPair(output, output.Inputs.ToList()));
+ }
+ }
+
+ disconnectOldConnections();
+
+ _bCanDeleteNode = true;
+ }
+
+ public override void Undo()
+ {
+ _graph.Add(_nodeRemoved);
+ reconnectOldConnections();
+
+ _bCanDeleteNode = false;
+ }
+
+ public override void Redo()
+ {
+ _graph.Remove(_nodeRemoved);
+ disconnectOldConnections();
+
+ _bCanDeleteNode = true;
+ }
+
+ private void disconnectOldConnections()
+ {
+ // For all the outputs for this node, remove all the connected inputs.
+ foreach (var output in _nodeRemoved.Outputs) {
+ output.RemoveAll();
+ }
+
+ // For all the inputs for this node, remove all the connected outputs.
+ foreach (var input in _nodeRemoved.Inputs) {
+ input.RemoveAll();
+ }
+ }
+
+ private void reconnectOldConnections()
+ {
+ // For all the remembered inputs (of this node) to output pairs, reconnect.
+ foreach (InputToOutputPair inOutPair in _oldConnectedOutputs) {
+
+ NodeInput input = inOutPair.item1;
+ List<NodeOutput> outputs = inOutPair.item2;
+
+ foreach (var output in outputs) {
+ output.Add(input);
+ }
+ }
+
+ // For all the remembered outputs (of this node) to inputs, reconnect.
+ foreach (OutputToInputsPair outInsPair in _oldConnectedInputs) {
+
+ NodeOutput output = outInsPair.item1;
+ List<NodeInput> inputs = outInsPair.item2;
+
+ foreach (var input in inputs) {
+ output.Add(input);
+ }
+ }
+ }
+
+ public override void OnDestroy()
+ {
+ if (_bCanDeleteNode && _nodeRemoved) {
+ ScriptableObject.DestroyImmediate(_nodeRemoved, true);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/DeleteNodeAction.cs.meta b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/DeleteNodeAction.cs.meta
new file mode 100644
index 00000000..4a62ff87
--- /dev/null
+++ b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/DeleteNodeAction.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 5653c134c34e88b4a971e196d0f42586
+timeCreated: 1501781608
+licenseType: Free
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/DragNode.cs b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/DragNode.cs
new file mode 100644
index 00000000..9e4d89d0
--- /dev/null
+++ b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/DragNode.cs
@@ -0,0 +1,42 @@
+
+using UnityEngine;
+
+namespace UNEB
+{
+ public class DragNode : MultiStageAction
+ {
+ private Node _draggingNode;
+
+ private Vector2 _startDragPos, _endDragPos;
+
+ public const float dragSpeed = 1f;
+
+ public override void Undo()
+ {
+ _draggingNode.bodyRect.position = _startDragPos;
+ }
+
+ public override void Redo()
+ {
+ _draggingNode.bodyRect.position = _endDragPos;
+ }
+
+ public override void Do()
+ {
+ NodeEditor editor = manager.window.editor;
+ _draggingNode.bodyRect.position += Event.current.delta * editor.ZoomScale * dragSpeed;
+ }
+
+ public override void OnActionStart()
+ {
+ _draggingNode = manager.window.state.selectedNode;
+ _startDragPos = _draggingNode.bodyRect.position;
+ }
+
+ public override bool OnActionEnd()
+ {
+ _endDragPos = _draggingNode.bodyRect.position;
+ return true;
+ }
+ }
+} \ No newline at end of file
diff --git a/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/DragNode.cs.meta b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/DragNode.cs.meta
new file mode 100644
index 00000000..efbc5101
--- /dev/null
+++ b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/DragNode.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 2075b47cce2d17147a02ec2483f38698
+timeCreated: 1501792802
+licenseType: Free
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/MultiStageAction.cs b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/MultiStageAction.cs
new file mode 100644
index 00000000..b7fd41fb
--- /dev/null
+++ b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/MultiStageAction.cs
@@ -0,0 +1,18 @@
+
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace UNEB
+{
+ public abstract class MultiStageAction : UndoableAction
+ {
+
+ public abstract void OnActionStart();
+
+ /// <summary>
+ /// Returns true if the action completed succesfully.
+ /// </summary>
+ /// <returns></returns>
+ public abstract bool OnActionEnd();
+ }
+} \ No newline at end of file
diff --git a/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/MultiStageAction.cs.meta b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/MultiStageAction.cs.meta
new file mode 100644
index 00000000..bc4a1081
--- /dev/null
+++ b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/MultiStageAction.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 211fa78df12a1d440a6aa57ca343d5ab
+timeCreated: 1501892778
+licenseType: Free
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/RemoveConnection.cs b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/RemoveConnection.cs
new file mode 100644
index 00000000..341e9f63
--- /dev/null
+++ b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/RemoveConnection.cs
@@ -0,0 +1,32 @@
+
+using UnityEngine;
+
+namespace UNEB
+{
+ public class RemoveConnection : UndoableAction
+ {
+
+ private NodeOutput _output;
+ private NodeInput _input;
+
+ public override void Do()
+ {
+ _input = manager.window.state.selectedInput;
+ _output = _input.Outputs[0];
+
+ _output.Remove(_input);
+
+ manager.window.state.selectedInput = null;
+ }
+
+ public override void Undo()
+ {
+ _output.Add(_input);
+ }
+
+ public override void Redo()
+ {
+ _output.Remove(_input);
+ }
+ }
+} \ No newline at end of file
diff --git a/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/RemoveConnection.cs.meta b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/RemoveConnection.cs.meta
new file mode 100644
index 00000000..fec8868b
--- /dev/null
+++ b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/RemoveConnection.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: d1b449f51ea4820498a2b38021e549d7
+timeCreated: 1501913312
+licenseType: Free
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/UndoableAction.cs b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/UndoableAction.cs
new file mode 100644
index 00000000..53857a9f
--- /dev/null
+++ b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/UndoableAction.cs
@@ -0,0 +1,12 @@
+
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace UNEB
+{
+ public abstract class UndoableAction : ActionBase
+ {
+ public abstract void Undo();
+ public abstract void Redo();
+ }
+} \ No newline at end of file
diff --git a/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/UndoableAction.cs.meta b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/UndoableAction.cs.meta
new file mode 100644
index 00000000..b6e7512b
--- /dev/null
+++ b/Other/NodeEditorExamples/Assets/UNEB/Editor/Actions/UndoableAction.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 72c02675816f81d4c9d15134303127b4
+timeCreated: 1501892769
+licenseType: Free
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/NodeEditorExamples/Assets/UNEB/Editor/NodeEditor.cs b/Other/NodeEditorExamples/Assets/UNEB/Editor/NodeEditor.cs
new file mode 100644
index 00000000..a7b8db1d
--- /dev/null
+++ b/Other/NodeEditorExamples/Assets/UNEB/Editor/NodeEditor.cs
@@ -0,0 +1,665 @@
+
+using System;
+using System.Collections.Generic;
+
+using UnityEngine;
+using UnityEditor;
+
+using NodeEditorFramework.Utilities;
+using UNEB.Utility;
+
+namespace UNEB
+{
+ public class NodeEditor
+ {
+ /// <summary>
+ /// Callback for when a node is modified within the editor
+ /// </summary>
+ public static Action<NodeGraph, Node> onNodeGuiChange;
+
+ /// <summary>
+ /// The rect bounds defining the recticle at the grid center.
+ /// </summary>
+ 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;
+
+ /// <summary>
+ /// The associated graph to visualize and edit.
+ /// </summary>
+ 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;
+
+ /// <summary>
+ /// Enables and disables drawing the guide to the grid center.
+ /// </summary>
+ 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);
+ }
+
+ /// <summary>
+ /// Shows where the center of the grid is.
+ /// </summary>
+ 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();
+ }
+
+ /// <summary>
+ /// Draw the window mode in the background.
+ /// </summary>
+ 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);
+ }
+ }
+
+ /// <summary>
+ /// Draws a bezier between the two end points in screen space.
+ /// </summary>
+ 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);
+ }
+
+ /// <summary>
+ /// Draws a line between the two end points.
+ /// </summary>
+ /// <param name="start"></param>
+ /// <param name="end"></param>
+ public static void DrawLine(Vector2 start, Vector2 end, Color color)
+ {
+ var handleColor = Handles.color;
+ Handles.color = color;
+
+ Handles.DrawLine(start, end);
+ Handles.color = handleColor;
+ }
+
+ /// <summary>
+ /// Draws a GUI texture with a tint.
+ /// </summary>
+ /// <param name="r"></param>
+ /// <param name="t"></param>
+ /// <param name="c"></param>
+ 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);
+ }
+ }
+
+ /// <summary>
+ /// Convertes the screen position to graph space.
+ /// </summary>
+ public Vector2 ScreenToGraphSpace(Vector2 screenPos)
+ {
+ var graphRect = _window.Size;
+ var center = graphRect.size / 2f;
+ return (screenPos - center) * ZoomScale - panOffset;
+ }
+
+ /// <summary>
+ /// Returns the mouse position in graph space.
+ /// </summary>
+ /// <returns></returns>
+ public Vector2 MousePosition()
+ {
+ return ScreenToGraphSpace(Event.current.mousePosition);
+ }
+
+ /// <summary>
+ /// Tests if the rect is under the mouse.
+ /// </summary>
+ /// <param name="r"></param>
+ /// <returns></returns>
+ public bool IsUnderMouse(Rect r)
+ {
+ return r.Contains(MousePosition());
+ }
+
+ /// <summary>
+ /// Converts the graph position to screen space.
+ /// This only works for geometry inside the GUIScaleUtility.BeginScale()
+ /// </summary>
+ /// <param name="graphPos"></param>
+ /// <returns></returns>
+ public Vector2 GraphToScreenSpace(Vector2 graphPos)
+ {
+ return graphPos + _zoomAdjustment + panOffset;
+ }
+
+ /// <summary>
+ /// Converts the graph position to screen space.
+ /// This only works for geometry inside the GUIScaleUtility.BeginScale().
+ /// </summary>
+ /// <param name="graphPos"></param>
+ public void graphToScreenSpace(ref Vector2 graphPos)
+ {
+ graphPos += _zoomAdjustment + panOffset;
+ }
+
+ /// <summary>
+ /// Converts the graph position to screen space.
+ /// This works for geometry NOT inside the GUIScaleUtility.BeginScale().
+ /// </summary>
+ /// <param name="graphPos"></param>
+ public void graphToScreenSpaceZoomAdj(ref Vector2 graphPos)
+ {
+ graphPos = GraphToScreenSpace(graphPos) / ZoomScale;
+ }
+
+ /// <summary>
+ /// Executes the callback on the first node that is detected under the mouse.
+ /// </summary>
+ /// <param name="callback"></param>
+ public bool OnMouseOverNode(Action<Node> 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;
+ }
+
+ /// <summary>
+ /// Tests if the mouse is over an output.
+ /// </summary>
+ /// <param name="callback"></param>
+ /// <returns></returns>
+ public bool OnMouseOverOutput(Action<NodeOutput> 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;
+ }
+
+ /// <summary>
+ /// Tests if the mouse is over an input.
+ /// </summary>
+ /// <param name="callback"></param>
+ /// <returns></returns>
+ public bool OnMouseOverInput(Action<NodeInput> 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;
+ }
+
+ /// <summary>
+ /// Tests if the mouse is over the node or the input.
+ /// </summary>
+ /// <param name="callback"></param>
+ /// <returns></returns>
+ public bool OnMouseOverNode_OrInput(Action<Node> 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
+ }
+}
diff --git a/Other/NodeEditorExamples/Assets/UNEB/Editor/NodeEditor.cs.meta b/Other/NodeEditorExamples/Assets/UNEB/Editor/NodeEditor.cs.meta
new file mode 100644
index 00000000..15c06c0c
--- /dev/null
+++ b/Other/NodeEditorExamples/Assets/UNEB/Editor/NodeEditor.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: a13b690ab7a11cb4cb37718591f0f20a
+timeCreated: 1501781542
+licenseType: Free
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/NodeEditorExamples/Assets/UNEB/Editor/NodeEditorWindow.cs b/Other/NodeEditorExamples/Assets/UNEB/Editor/NodeEditorWindow.cs
new file mode 100644
index 00000000..e6407f19
--- /dev/null
+++ b/Other/NodeEditorExamples/Assets/UNEB/Editor/NodeEditorWindow.cs
@@ -0,0 +1,320 @@
+
+using System.IO;
+using UnityEngine;
+using UnityEditor;
+using UnityEditor.Callbacks;
+
+using NodeEditorFramework.Utilities;
+using UNEB.Utility;
+
+namespace UNEB
+{
+ public class NodeEditorWindow : EditorWindow
+ {
+ [MenuItem("Window/Node Editor")]
+ static void Init()
+ {
+ var w = EditorWindow.CreateInstance<NodeEditorWindow>();
+ w.titleContent = new GUIContent("Node Editor");
+ w.Show();
+ }
+
+ public const float kToolbarHeight = 20f;
+ public const float kToolbarButtonWidth = 50f;
+
+ [SerializeField]
+ public NodeGraph graph;
+
+ public NodeEditor editor;
+ public ActionManager actions;
+ public ActionTriggerSystem triggers;
+ public NodeEditorState state;
+ private SaveManager _saveManager;
+
+ public enum Mode { Edit, View };
+ private Mode _mode = Mode.Edit;
+
+ void OnEnable()
+ {
+ GUIScaleUtility.CheckInit();
+ TextureLib.LoadStandardTextures();
+
+ actions = new ActionManager(this);
+ editor = new NodeEditor(this);
+ triggers = new ActionTriggerSystem(actions);
+ state = new NodeEditorState();
+
+ _saveManager = new SaveManager(this);
+
+ editor.graph = graph;
+
+ // Make sure that changes from the undo system are immediately
+ // updated in the window. If not, the undo changes will be
+ // visually delayed.
+ actions.OnUndo += Repaint;
+ actions.OnRedo += Repaint;
+
+ // Always start in edit mode.
+ // The only way it can be in view mode is if the window is
+ // already opened and the user selects a some graph.
+ _mode = Mode.Edit;
+
+ editor.HomeView();
+ }
+
+ void OnDisable()
+ {
+ _saveManager.Cleanup();
+ }
+
+ void OnDestroy()
+ {
+ cleanup();
+ }
+
+ void OnGUI()
+ {
+ // Asset removed.
+ if (!graph && !_saveManager.IsInNographState()) {
+ _saveManager.InitState();
+ }
+
+ editor.Draw();
+ drawToolbar();
+
+ // This must go after draw calls or there can be
+ // GUI layout errors.
+ triggers.Update();
+ }
+
+ public void SetGraph(NodeGraph g, Mode mode = Mode.Edit)
+ {
+ graph = g;
+ editor.graph = g;
+
+ // Reset Undo and Redo buffers.
+ actions.Reset();
+
+ _mode = mode;
+ }
+
+ private void cleanup()
+ {
+ if (actions != null) {
+ actions.OnUndo -= Repaint;
+ actions.OnRedo -= Repaint;
+ }
+
+ actions.Reset();
+ _saveManager.Cleanup();
+ }
+
+ private void drawToolbar()
+ {
+ EditorGUILayout.BeginHorizontal("Toolbar");
+
+ if (DropdownButton("File", kToolbarButtonWidth)) {
+ createFileMenu();
+ }
+
+ if (DropdownButton("Edit", kToolbarButtonWidth)) {
+ createEditMenu();
+ }
+
+ if (DropdownButton("View", kToolbarButtonWidth)) {
+ createViewMenu();
+ }
+
+ if (DropdownButton("Settings", kToolbarButtonWidth + 10f)) {
+ createSettingsMenu();
+ }
+
+ if (DropdownButton("Tools", kToolbarButtonWidth)) {
+ createToolsMenu();
+ }
+
+ // Make the toolbar extend all throughout the window extension.
+ GUILayout.FlexibleSpace();
+ drawGraphName();
+
+ EditorGUILayout.EndHorizontal();
+ }
+
+ private void drawGraphName()
+ {
+ string graphName = "None";
+ if (graph != null) {
+ graphName = graph.name;
+ }
+
+ GUILayout.Label(graphName);
+ }
+
+ private void createFileMenu()
+ {
+ var menu = new GenericMenu();
+
+ menu.AddItem(new GUIContent("Create New"), false, _saveManager.RequestNew);
+ menu.AddItem(new GUIContent("Load"), false, _saveManager.RequestLoad);
+
+ menu.AddSeparator("");
+ menu.AddItem(new GUIContent("Save"), false, _saveManager.RequestSave);
+ menu.AddItem(new GUIContent("Save As"), false, _saveManager.RequestSaveAs);
+
+ menu.DropDown(new Rect(5f, kToolbarHeight, 0f, 0f));
+ }
+
+ private void createEditMenu()
+ {
+ var menu = new GenericMenu();
+
+ menu.AddItem(new GUIContent("Undo"), false, actions.UndoAction);
+ menu.AddItem(new GUIContent("Redo"), false, actions.RedoAction);
+
+ menu.DropDown(new Rect(55f, kToolbarHeight, 0f, 0f));
+ }
+
+ private void createViewMenu()
+ {
+ var menu = new GenericMenu();
+
+ menu.AddItem(new GUIContent("Home"), false, editor.HomeView);
+ menu.AddItem(new GUIContent("Zoom In"), false, () => { editor.Zoom(-1); });
+ menu.AddItem(new GUIContent("Zoom Out"), false, () => { editor.Zoom(1); });
+
+ menu.DropDown(new Rect(105f, kToolbarHeight, 0f, 0f));
+ }
+
+ private void createSettingsMenu()
+ {
+ var menu = new GenericMenu();
+
+ menu.AddItem(new GUIContent("Show Guide"), editor.bDrawGuide, editor.ToggleDrawGuide);
+
+ menu.DropDown(new Rect(155f, kToolbarHeight, 0f, 0f));
+ }
+
+ private void createToolsMenu()
+ {
+ var menu = new GenericMenu();
+
+ menu.AddItem(new GUIContent("Add Test Nodes"), false, addTestNodes);
+ menu.AddItem(new GUIContent("Clear Nodes"), false, clearNodes);
+
+ menu.DropDown(new Rect(215f, kToolbarHeight, 0f, 0f));
+ }
+
+ public bool DropdownButton(string name, float width)
+ {
+ return GUILayout.Button(name, EditorStyles.toolbarDropDown, GUILayout.Width(width));
+ }
+
+ private void addTestNodes()
+ {
+ if (graph) {
+
+ for (int x = 0; x < 10; x++) {
+ for (int y = 0; y < 10; y++) {
+
+ var node = SaveManager.CreateNode<BasicNode>(graph);
+
+ float xpos = x * Node.kDefaultSize.x * 1.5f;
+ float ypos = y * Node.kDefaultSize.y * 1.5f;
+ node.bodyRect.position = new Vector2(xpos, ypos);
+ }
+ }
+ }
+ }
+
+ private void clearNodes()
+ {
+ if (graph) {
+
+ foreach (var node in graph.nodes) {
+ ScriptableObject.DestroyImmediate(node, true);
+ }
+
+ actions.Reset();
+ graph.nodes.Clear();
+ }
+ }
+
+ /// <summary>
+ /// The size of the window.
+ /// </summary>
+ public Rect Size
+ {
+ get { return new Rect(Vector2.zero, position.size); }
+ }
+
+ /// <summary>
+ /// The rect used to filter input.
+ /// This is so the toolbar is not ignored by editor inputs.
+ /// </summary>
+ public Rect InputRect
+ {
+ get
+ {
+ var rect = Size;
+
+ rect.y += kToolbarHeight;
+ rect.height -= kToolbarHeight;
+
+ return rect;
+ }
+ }
+
+ public Mode GetMode()
+ {
+ return _mode;
+ }
+
+ /// <summary>
+ /// Opens up the node editor window from asset selection.
+ /// </summary>
+ /// <param name="instanceID"></param>
+ /// <param name="line"></param>
+ /// <returns></returns>
+ [OnOpenAsset(1)]
+ private static bool OpenGraphAsset(int instanceID, int line)
+ {
+ var graphSelected = EditorUtility.InstanceIDToObject(instanceID) as NodeGraph;
+
+ if (graphSelected != null) {
+
+ NodeEditorWindow windowToUse = null;
+
+ // Try to find an editor window without a graph...
+ var windows = Resources.FindObjectsOfTypeAll<NodeEditorWindow>();
+ foreach (var w in windows) {
+
+ // The canvas is already opened
+ if (w.graph == graphSelected) {
+ return false;
+ }
+
+ // Found a window with no active canvas.
+ if (w.graph == null) {
+ windowToUse = w;
+ break;
+ }
+ }
+
+ // No windows available...just make a new one.
+ if (!windowToUse) {
+ windowToUse = EditorWindow.CreateInstance<NodeEditorWindow>();
+ windowToUse.titleContent = new GUIContent("Node Editor");
+ windowToUse.Show();
+ }
+
+ windowToUse.SetGraph(graphSelected);
+ windowToUse._saveManager.InitState();
+ windowToUse.Repaint();
+
+ return true;
+ }
+
+ return false;
+ }
+ }
+} \ No newline at end of file
diff --git a/Other/NodeEditorExamples/Assets/UNEB/Editor/NodeEditorWindow.cs.meta b/Other/NodeEditorExamples/Assets/UNEB/Editor/NodeEditorWindow.cs.meta
new file mode 100644
index 00000000..84abdc2e
--- /dev/null
+++ b/Other/NodeEditorExamples/Assets/UNEB/Editor/NodeEditorWindow.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: e9e0c3fd288222b4ea49d6b9eb70ccaa
+timeCreated: 1501781512
+licenseType: Free
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/NodeEditorExamples/Assets/UNEB/Editor/SaveManager.cs b/Other/NodeEditorExamples/Assets/UNEB/Editor/SaveManager.cs
new file mode 100644
index 00000000..c9825ae0
--- /dev/null
+++ b/Other/NodeEditorExamples/Assets/UNEB/Editor/SaveManager.cs
@@ -0,0 +1,473 @@
+
+using System;
+using System.IO;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEditor;
+using UnityEditor.Callbacks;
+
+using Bonsai.Utility;
+
+namespace UNEB
+{
+ /// <summary>
+ /// Handles the saving and loading of tree assets.
+ /// </summary>
+ public class SaveManager
+ {
+ public enum SaveState { NoGraph, TempGraph, SavedGraph };
+
+ // The FSM used to structure the logic control of saving and loading.
+ private StateMachine<SaveState> _saveFSM;
+
+ // The events that dictate the flow of the manager's FSM.
+ private enum SaveOp { None, New, Load, Save, SaveAs };
+ private SaveOp _saveOp = SaveOp.None;
+
+ private NodeEditorWindow _window;
+
+ private const string kRootUNEB = "UNEB";
+
+ // Path that stores temporary graphs.
+ private const string kTempGraphDirectory = "TempGraphsUNEB";
+ private const string kTempFileName = "TempNodeGraphUNEB";
+
+ public SaveManager(NodeEditorWindow w)
+ {
+ _window = w;
+
+ _saveFSM = new StateMachine<SaveState>();
+
+ var noGraph = new StateMachine<SaveState>.State(SaveState.NoGraph);
+ var tempGraph = new StateMachine<SaveState>.State(SaveState.TempGraph);
+ var savedGraph = new StateMachine<SaveState>.State(SaveState.SavedGraph);
+
+ _saveFSM.AddState(noGraph);
+ _saveFSM.AddState(tempGraph);
+ _saveFSM.AddState(savedGraph);
+
+ // Actions to take when starting out on a window with no graph.
+ _saveFSM.AddTransition(noGraph, tempGraph, isNewRequested, createNewOnto_Window_WithTempOrEmpty);
+ _saveFSM.AddTransition(noGraph, savedGraph, isLoadRequested, loadOnto_EmptyWindow);
+
+ // Actions to take when the window has a temp graph.
+ _saveFSM.AddTransition(tempGraph, tempGraph, isNewRequested, createNewOnto_Window_WithTempOrEmpty);
+ _saveFSM.AddTransition(tempGraph, savedGraph, isSaveOrSaveAsRequested, saveTempAs);
+ _saveFSM.AddTransition(tempGraph, savedGraph, isLoadRequested, loadOnto_Window_WithTempgraph);
+
+ // Actions to take when the window has a valid graph (already saved).
+ _saveFSM.AddTransition(savedGraph, savedGraph, isSaveRequested, save);
+ _saveFSM.AddTransition(savedGraph, savedGraph, isSaveAsRequested, saveCloneAs);
+ _saveFSM.AddTransition(savedGraph, savedGraph, isLoadRequested, loadOnto_Window_WithSavedgraph);
+ _saveFSM.AddTransition(savedGraph, tempGraph, isNewRequested, createNewOnto_Window_WithSavedgraph);
+
+ // Consume the save operation even after the transition is made.
+ _saveFSM.OnStateChangedEvent += () => { _saveOp = SaveOp.None; };
+
+ InitState();
+
+ NodeConnection.OnConnectionCreated -= saveConnection;
+ NodeConnection.OnConnectionCreated += saveConnection;
+ }
+
+ /// <summary>
+ /// This hanldes setting up the proper state based on the window's graph.
+ /// </summary>
+ internal void InitState()
+ {
+ // If the window has a valid graph and editable.
+ if (_window.graph != null && _window.GetMode() == NodeEditorWindow.Mode.Edit) {
+
+ string path = getCurrentGraphPath();
+
+ // If the graph is temp.
+ if (path.Contains(kTempGraphDirectory)) {
+ SetState(SaveState.TempGraph);
+ }
+
+ // If the graph is saved (not a temp).
+ else {
+ SetState(SaveState.SavedGraph);
+ }
+ }
+
+ // Window is fresh, no graph yet set.
+ else {
+ SetState(SaveState.NoGraph);
+ }
+ }
+
+ /// <summary>
+ /// Get the path from open file dialog.
+ /// </summary>
+ /// <returns></returns>
+ private string getGraphFilePath()
+ {
+ string path = EditorUtility.OpenFilePanel("Open Node Graph", "Assets/", "asset");
+
+ // If the path is outside the project's asset folder.
+ if (!path.Contains(Application.dataPath)) {
+
+ // If the selection was not cancelled...
+ if (!string.IsNullOrEmpty(path)) {
+ _window.ShowNotification(new GUIContent("Please select a Graph asset within the project's Asset folder."));
+ return null;
+ }
+ }
+
+ return path;
+ }
+
+ /// <summary>
+ /// Assumes that the path is already valid.
+ /// </summary>
+ /// <param name="path"></param>
+ private void loadGraph(string path)
+ {
+ int assetIndex = path.IndexOf("/Assets/");
+ path = path.Substring(assetIndex + 1);
+
+ var graph = AssetDatabase.LoadAssetAtPath<NodeGraph>(path);
+ _window.SetGraph(graph);
+ }
+
+ /// <summary>
+ /// Gets the file path to save the canavs at.
+ /// </summary>
+ /// <returns></returns>
+ private string getSaveFilePath()
+ {
+ string path = EditorUtility.SaveFilePanelInProject("Save Node Graph", "NewNodeGraph", "asset", "Select a destination to save the graph.");
+
+ if (string.IsNullOrEmpty(path)) {
+ return "";
+ }
+
+ return path;
+ }
+
+ #region Save Operations
+
+ /// <summary>
+ /// Creates and adds a node to the graph.
+ /// </summary>
+ /// <param name="t"></param>
+ /// <param name="bt"></param>
+ /// <returns></returns>
+ public static Node CreateNode(Type t, NodeGraph g)
+ {
+ try {
+
+ var node = ScriptableObject.CreateInstance(t) as Node;
+ AssetDatabase.AddObjectToAsset(node, g);
+
+ // Optional, set reference to graph: node.graph = g
+
+ node.Init();
+ g.Add(node);
+ return node;
+ }
+
+ catch (Exception e) {
+ throw new UnityException(e.Message);
+ }
+ }
+
+ /// <summary>
+ /// Creates and adds a node to the graph.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="bt"></param>
+ /// <returns></returns>
+ public static Node CreateNode<T>(NodeGraph g) where T : Node
+ {
+ var node = ScriptableObject.CreateInstance<T>();
+ AssetDatabase.AddObjectToAsset(node, g);
+
+ // Optional, set reference to graph: node.graph = g
+
+ node.Init();
+ g.Add(node);
+ return node;
+ }
+
+ /// <summary>
+ /// Creates a graph asset and saves it.
+ /// </summary>
+ /// <param name="path">The full path including name and extension.</param>
+ /// <returns></returns>
+ public static NodeGraph CreateNodeGraph(string path)
+ {
+ // We create a graph asset in the data base in order to add node assets
+ // under the graph. This way things are organized in the editor.
+ //
+ // The drawback is that we need to create a temp asset for the tree
+ // and make sure it does not linger if the temp asset is discarded.
+ //
+ // This means that we need to have a persistent directoy to store temp
+ // assets.
+
+ var graph = ScriptableObject.CreateInstance<NodeGraph>();
+
+ AssetDatabase.CreateAsset(graph, path);
+ AssetDatabase.SaveAssets();
+ AssetDatabase.Refresh();
+
+ return graph;
+ }
+
+ /// <summary>
+ /// Creates a new temporary node graph.
+ /// </summary>
+ /// <returns></returns>
+ private NodeGraph createNew()
+ {
+ string tempPath = getTempFilePath();
+
+ if (!string.IsNullOrEmpty(tempPath)) {
+
+ _window.ShowNotification(new GUIContent("New Graph Created"));
+ return CreateNodeGraph(tempPath);
+ }
+
+ return null;
+ }
+
+ // Create a new temp graph on an empty window or with a temp graph.
+ private bool createNewOnto_Window_WithTempOrEmpty()
+ {
+ _window.SetGraph(createNew());
+ return true;
+ }
+
+ // Saves the current active graph then loads a new graph.
+ private bool createNewOnto_Window_WithSavedgraph()
+ {
+ // Save the old graph to avoid loss.
+ AssetDatabase.SaveAssets();
+
+ _window.SetGraph(createNew());
+
+ return true;
+ }
+
+ // Load a graph to a window that has no graph active.
+ private bool loadOnto_EmptyWindow()
+ {
+ loadGraph(getGraphFilePath());
+ return true;
+ }
+
+ // Load a graph to a window that has a temp graph active.
+ private bool loadOnto_Window_WithTempgraph()
+ {
+ string path = getGraphFilePath();
+
+ if (!string.IsNullOrEmpty(path)) {
+
+ // Get rid of the temporary graph.
+ AssetDatabase.DeleteAsset(getCurrentGraphPath());
+ loadGraph(path);
+ return true;
+ }
+
+ return false;
+ }
+
+ // Load a graph to a window that has a saved graph active.
+ private bool loadOnto_Window_WithSavedgraph()
+ {
+ string path = getGraphFilePath();
+
+ if (!string.IsNullOrEmpty(path)) {
+
+ // Save the old graph.
+ save();
+ loadGraph(path);
+ return true;
+ }
+
+ return false;
+ }
+
+ // Makes the temporary graph into a saved graph.
+ private bool saveTempAs()
+ {
+ string newPath = getSaveFilePath();
+ string currentPath = getCurrentGraphPath();
+
+ //If asset exists on path, delete it first.
+ if (AssetDatabase.LoadAssetAtPath<ScriptableObject>(newPath) != null) {
+ AssetDatabase.DeleteAsset(newPath);
+ }
+
+ string result = AssetDatabase.ValidateMoveAsset(currentPath, newPath);
+
+ if (result.Length == 0) {
+ AssetDatabase.MoveAsset(currentPath, newPath);
+ save();
+ return true;
+ }
+
+ else {
+ Debug.LogError(result);
+ return false;
+ }
+ }
+
+ // Copies the current active graph to a new location.
+ private bool saveCloneAs()
+ {
+ string newPath = getSaveFilePath();
+
+ if (!string.IsNullOrEmpty(newPath)) {
+
+ string currentPath = getCurrentGraphPath();
+
+ AssetDatabase.CopyAsset(currentPath, newPath);
+ AssetDatabase.SetMainObject(_window.graph, currentPath);
+
+ save();
+ return true;
+ }
+
+ return false;
+ }
+
+ // Saves the current graph (not a temp graph).
+ private bool save()
+ {
+ _window.graph.OnSave();
+ AssetDatabase.SaveAssets();
+ AssetDatabase.Refresh();
+
+ _window.ShowNotification(new GUIContent("Graph Saved"));
+ return true;
+ }
+
+ // Helper method for the NodeConnection.OnConnectionCreated callback.
+ private void saveConnection(NodeConnection conn)
+ {
+ if (!AssetDatabase.Contains(conn)) {
+ AssetDatabase.AddObjectToAsset(conn, _window.graph);
+ }
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Handles deleting temporary graph or saving valid graph.
+ /// </summary>
+ internal void Cleanup()
+ {
+ // Only save/delete things if we are in edit mode.
+ if (_window.GetMode() != NodeEditorWindow.Mode.Edit) {
+ return;
+ }
+
+ SaveState state = _saveFSM.CurrentState.Value;
+
+ if (state == SaveState.TempGraph) {
+ AssetDatabase.DeleteAsset(getCurrentGraphPath());
+ }
+
+ else if (state == SaveState.SavedGraph) {
+ save();
+ }
+ }
+
+ /*
+ * These are conditions used the save FSM to know when to transition.
+ * */
+ private bool isNewRequested() { return _saveOp == SaveOp.New; }
+ private bool isLoadRequested() { return _saveOp == SaveOp.Load; }
+ private bool isSaveRequested() { return _saveOp == SaveOp.Save; }
+ private bool isSaveAsRequested() { return _saveOp == SaveOp.SaveAs; }
+ private bool isSaveOrSaveAsRequested() { return isSaveAsRequested() || isSaveRequested(); }
+
+ /*
+ * These are the events that drive the save manager.
+ * Whenever one of this is fired, the save operation is set
+ * and the save FSM updated.
+ * */
+ internal void RequestNew() { _saveOp = SaveOp.New; _saveFSM.Update(); }
+ internal void RequestLoad() { _saveOp = SaveOp.Load; _saveFSM.Update(); }
+ internal void RequestSave() { _saveOp = SaveOp.Save; _saveFSM.Update(); }
+ internal void RequestSaveAs() { _saveOp = SaveOp.SaveAs; _saveFSM.Update(); }
+
+ private string getTempFilePath()
+ {
+ string tempRoot = getTempDirPath();
+
+ if (string.IsNullOrEmpty(tempRoot)) {
+ return "";
+ }
+
+ string filename = kTempFileName + _window.GetInstanceID().ToString().Ext("asset");
+ return tempRoot.Dir(filename);
+ }
+
+ internal void SetState(SaveState state)
+ {
+ _saveFSM.SetCurrentState(state);
+ }
+
+ internal bool IsInNographState()
+ {
+ return _saveFSM.CurrentState.Value == SaveState.NoGraph;
+ }
+
+ internal SaveState CurrentState()
+ {
+ return _saveFSM.CurrentState.Value;
+ }
+
+ private string getCurrentGraphPath()
+ {
+ return AssetDatabase.GetAssetPath(_window.graph);
+ }
+
+ private string getTempDirPath()
+ {
+ string[] dirs = Directory.GetDirectories(Application.dataPath, kTempGraphDirectory, SearchOption.AllDirectories);
+
+ // Return first occurance containing targetFolderName.
+ if (dirs.Length != 0) {
+ return getTempPathRelativeToAssets(dirs[0]);
+ }
+
+ // Could not find anything. Make the folder
+ string rootPath = getPathToRootUNEB();
+
+ if (!string.IsNullOrEmpty(rootPath)) {
+ var dirInfo = Directory.CreateDirectory(rootPath.Dir(kTempGraphDirectory));
+ return getTempPathRelativeToAssets(dirInfo.FullName);
+ }
+
+ else {
+ return "";
+ }
+ }
+
+ private static string getPathToRootUNEB()
+ {
+ // Find the UNEB project root directory within the Unity project.
+ var dirs = Directory.GetDirectories(Application.dataPath, kRootUNEB, SearchOption.AllDirectories);
+
+ if (dirs.Length != 0) {
+ return dirs[0];
+ }
+ else {
+ Debug.LogError("Could not find project root: /" + kRootUNEB + '/');
+ return "";
+ }
+ }
+
+ // Assumes that the fullTempPath is valid.
+ private static string getTempPathRelativeToAssets(string fullTempPath)
+ {
+ int index = fullTempPath.IndexOf("Assets");
+ return fullTempPath.Substring(index);
+ }
+ }
+} \ No newline at end of file
diff --git a/Other/NodeEditorExamples/Assets/UNEB/Editor/SaveManager.cs.meta b/Other/NodeEditorExamples/Assets/UNEB/Editor/SaveManager.cs.meta
new file mode 100644
index 00000000..48c5f044
--- /dev/null
+++ b/Other/NodeEditorExamples/Assets/UNEB/Editor/SaveManager.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 2c1fb70b507fdef4e947a73357d0d811
+timeCreated: 1503072834
+licenseType: Free
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant: