diff options
Diffstat (limited to 'Other/NodeEditorExamples/Assets/UNEB/Editor')
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: |