using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEditor; using UnityEditor.Experimental.GraphView; using UnityEditor.UIElements; using UnityEngine; using UnityEngine.UIElements; using SearchWindow = UnityEditor.Experimental.GraphView.SearchWindow; public interface IFoldNode { FoldNodeSerialized Serialize(); IFoldNode GetPreviousFold(); } public interface IValueNode { T GetValue(); } public abstract class FoldNodeView : Node { public FoldNodeData Data { get; private set; } protected FoldGraphView graphView; readonly Dictionary ports = new(); public virtual void Initialize(FoldGraphView view, FoldNodeData data) { graphView = view; Data = data; title = data.Title; viewDataKey = data.guid; SetPosition(new Rect(data.position, Vector2.zero)); // Allow shift+click to add to the current selection without clearing it. RegisterCallback(evt => { if (!evt.shiftKey) return; if (!graphView.selection.Contains(this)) graphView.AddToSelection(this); evt.StopImmediatePropagation(); evt.PreventDefault(); }, TrickleDown.TrickleDown); } public override void SetPosition(Rect newPos) { base.SetPosition(newPos); if (Data != null) { graphView.RecordUndo("Move Node"); Data.position = newPos.position; graphView.MarkDirty(); } } protected Port AddPort(Direction direction, Port.Capacity capacity, string name, Type type) { var port = InstantiatePort(Orientation.Horizontal, direction, capacity, type); port.portName = name; ports[name] = port; graphView.BindEdgeConnector(port); if (direction == Direction.Input) inputContainer.Add(port); else outputContainer.Add(port); return port; } public Port GetPort(string name) => ports.TryGetValue(name, out var port) ? port : null; } public abstract class FoldOperationNodeView : FoldNodeView, IFoldNode { protected Port foldInputPort; protected Port foldOutputPort; public override void Initialize(FoldGraphView view, FoldNodeData data) { base.Initialize(view, data); foldInputPort = AddPort(Direction.Input, Port.Capacity.Single, "Input", typeof(IFoldNode)); foldOutputPort = AddPort(Direction.Output, Port.Capacity.Single, "Out", typeof(IFoldNode)); } public IFoldNode GetPreviousFold() { var edge = foldInputPort.connections.FirstOrDefault(); return edge?.output?.node as IFoldNode; } public abstract FoldNodeSerialized Serialize(); } public class FloatValueNodeView : FoldNodeView, IValueNode { FloatValueNodeData DataTyped => (FloatValueNodeData)Data; public override void Initialize(FoldGraphView view, FoldNodeData data) { base.Initialize(view, data); AddPort(Direction.Output, Port.Capacity.Multi, "Out", typeof(float)); var field = new FloatField("Value") { value = DataTyped.value }; field.RegisterValueChangedCallback(evt => { graphView.RecordUndo("Edit Float"); DataTyped.value = evt.newValue; graphView.MarkDirty(); }); mainContainer.Add(field); } public float GetValue() => DataTyped.value; } public class VectorValueNodeView : FoldNodeView, IValueNode { VectorValueNodeData DataTyped => (VectorValueNodeData)Data; public override void Initialize(FoldGraphView view, FoldNodeData data) { base.Initialize(view, data); AddPort(Direction.Output, Port.Capacity.Multi, "Out", typeof(Vector4)); var field = new Vector4Field("Value") { value = DataTyped.value }; field.RegisterValueChangedCallback(evt => { graphView.RecordUndo("Edit Vector"); DataTyped.value = evt.newValue; graphView.MarkDirty(); }); mainContainer.Add(field); } public Vector4 GetValue() => DataTyped.value; } public class GameObjectNodeView : FoldNodeView, IValueNode { GameObjectNodeData DataTyped => (GameObjectNodeData)Data; public override void Initialize(FoldGraphView view, FoldNodeData data) { base.Initialize(view, data); AddPort(Direction.Output, Port.Capacity.Multi, "Out", typeof(GameObject)); var field = new ObjectField("Object") { allowSceneObjects = true, objectType = typeof(GameObject), value = DataTyped.output }; field.RegisterValueChangedCallback(evt => { graphView.RecordUndo("Edit GameObject"); DataTyped.output = evt.newValue as GameObject; graphView.MarkDirty(); }); mainContainer.Add(field); } public GameObject GetValue() => DataTyped.output; } public class AxisAlignNodeView : FoldOperationNodeView { AxisAlignNodeData DataTyped => (AxisAlignNodeData)Data; Port poPort, ppPort, rPort, tPort; public override void Initialize(FoldGraphView view, FoldNodeData data) { base.Initialize(view, data); poPort = AddPort(Direction.Input, Port.Capacity.Single, "po", typeof(Vector4)); ppPort = AddPort(Direction.Input, Port.Capacity.Single, "pp", typeof(Vector4)); rPort = AddPort(Direction.Input, Port.Capacity.Single, "r", typeof(Vector4)); tPort = AddPort(Direction.Input, Port.Capacity.Single, "t", typeof(float)); } public override FoldNodeSerialized Serialize() { return DataTyped.Serialize(graphView); } } public class PlaneToTubeNodeView : FoldOperationNodeView { PlaneToTubeNodeData DataTyped => (PlaneToTubeNodeData)Data; public override void Initialize(FoldGraphView view, FoldNodeData data) { base.Initialize(view, data); AddPort(Direction.Input, Port.Capacity.Single, "p", typeof(Vector4)); AddPort(Direction.Input, Port.Capacity.Single, "r", typeof(Vector4)); AddPort(Direction.Input, Port.Capacity.Single, "s", typeof(Vector4)); AddPort(Direction.Input, Port.Capacity.Single, "t", typeof(float)); } public override FoldNodeSerialized Serialize() => DataTyped.Serialize(graphView); } public class PointAlignNodeView : FoldOperationNodeView { PointAlignNodeData DataTyped => (PointAlignNodeData)Data; public override void Initialize(FoldGraphView view, FoldNodeData data) { base.Initialize(view, data); AddPort(Direction.Input, Port.Capacity.Single, "po", typeof(Vector4)); AddPort(Direction.Input, Port.Capacity.Single, "pp", typeof(Vector4)); AddPort(Direction.Input, Port.Capacity.Single, "r", typeof(Vector4)); AddPort(Direction.Input, Port.Capacity.Single, "t", typeof(float)); } public override FoldNodeSerialized Serialize() => DataTyped.Serialize(graphView); } public class TubeToPlaneNodeView : FoldOperationNodeView { TubeToPlaneNodeData DataTyped => (TubeToPlaneNodeData)Data; public override void Initialize(FoldGraphView view, FoldNodeData data) { base.Initialize(view, data); AddPort(Direction.Input, Port.Capacity.Single, "p", typeof(Vector4)); AddPort(Direction.Input, Port.Capacity.Single, "r", typeof(Vector4)); AddPort(Direction.Input, Port.Capacity.Single, "s", typeof(Vector4)); AddPort(Direction.Input, Port.Capacity.Single, "t", typeof(float)); } public override FoldNodeSerialized Serialize() => DataTyped.Serialize(graphView); } public class KeyframeNodeView : FoldNodeView { Port gameObjectPort; Port foldPort; readonly List validationMessages = new(); public override void Initialize(FoldGraphView view, FoldNodeData data) { base.Initialize(view, data); gameObjectPort = AddPort(Direction.Input, Port.Capacity.Single, "GameObject", typeof(GameObject)); foldPort = AddPort(Direction.Input, Port.Capacity.Single, "Fold Data", typeof(IFoldNode)); var button = new Button(OnGenerateClick) { text = "Generate Keyframe" }; mainContainer.Add(button); } GameObject GetTargetObject() { var edge = gameObjectPort.connections.FirstOrDefault(); if (edge?.output?.node is IValueNode goProvider) return goProvider.GetValue(); return null; } IFoldNode GetFoldInput() { var edge = foldPort.connections.FirstOrDefault(); return edge?.output?.node as IFoldNode; } void OnGenerateClick() { validationMessages.Clear(); var go = GetTargetObject(); if (go == null) { validationMessages.Add("GameObject input missing or empty."); ShowValidation(); return; } var renderer = go.GetComponent(); if (renderer == null) { validationMessages.Add("GameObject must have a MeshRenderer component."); ShowValidation(); return; } var foldNode = GetFoldInput(); if (foldNode == null) { validationMessages.Add("Fold input not connected."); ShowValidation(); return; } var stack = new Stack(); var current = foldNode; while (current != null) { stack.Push(current); current = current.GetPreviousFold(); } Undo.RecordObject(renderer, "Generate Keyframe"); var mat = renderer.material; const int kNumSlots = 16; for (int i = 0; i < kNumSlots; i++) { string slotPrefix = $"_Vertex_Deformation_Slot_{i}_"; IFoldNode node = stack.Count > 0 ? stack.Pop() : null; FoldNodeSerialized data = node?.Serialize(); bool active = data != null; mat.SetFloat(slotPrefix + "Enabled", active ? 1.0f : 0.0f); mat.SetInt(slotPrefix + "Opcode", active ? data.opcode : 0); mat.SetFloat(slotPrefix + "Float_0", active ? data.float0 : 0.0f); mat.SetFloat(slotPrefix + "Float_1", active ? data.float1 : 0.0f); mat.SetFloat(slotPrefix + "Float_2", active ? data.float2 : 0.0f); mat.SetFloat(slotPrefix + "Float_3", active ? data.float3 : 0.0f); mat.SetVector(slotPrefix + "Vector_0", active ? data.vec0 : Vector4.zero); mat.SetVector(slotPrefix + "Vector_1", active ? data.vec1 : Vector4.zero); mat.SetVector(slotPrefix + "Vector_2", active ? data.vec2 : Vector4.zero); mat.SetVector(slotPrefix + "Vector_3", active ? data.vec3 : Vector4.zero); } EditorUtility.SetDirty(renderer); } void ShowValidation() { graphView.ShowNotification(string.Join("\n", validationMessages)); } } public class FoldGraphView : GraphView { readonly FoldWindow window; public readonly FoldGraph graphAsset; readonly Dictionary nodeLookup = new(); bool suppressGraphChanges; readonly FoldEdgeConnectorListener edgeConnectorListener; public FoldGraphView(FoldWindow owner, FoldGraph graph) { window = owner; graphAsset = graph; edgeConnectorListener = new FoldEdgeConnectorListener(this); style.flexGrow = 1; this.AddManipulator(new ContentZoomer()); this.AddManipulator(new ContentDragger()); this.AddManipulator(new SelectionDragger()); this.AddManipulator(new RectangleSelector()); gridBackground = new GridBackground(); Insert(0, gridBackground); gridBackground.StretchToParentSize(); graphViewChanged = OnGraphViewChanged; RegisterCallback(OnKeyDown); Undo.undoRedoPerformed += OnUndoRedo; Reload(); } GridBackground gridBackground; public override List GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter) { return ports.ToList().Where(p => p.node != startPort.node && p.direction != startPort.direction && IsTypeCompatible(startPort.portType, p.portType)).ToList(); } bool IsTypeCompatible(Type a, Type b) { if (a == null || b == null) return true; return a.IsAssignableFrom(b) || b.IsAssignableFrom(a); } internal void BindEdgeConnector(Port port) { var field = typeof(Port).GetField("m_EdgeConnector", BindingFlags.Instance | BindingFlags.NonPublic); if (field != null) { if (field.GetValue(port) is IManipulator oldManipulator) port.RemoveManipulator(oldManipulator); } var connector = new EdgeConnector(edgeConnectorListener); field?.SetValue(port, connector); port.AddManipulator(connector); } GraphViewChange OnGraphViewChanged(GraphViewChange change) { if (suppressGraphChanges) return change; if (change.elementsToRemove != null) { foreach (var element in change.elementsToRemove) { if (element is Edge e) { RemoveEdgeData(e); } else if (element is FoldNodeView node) { RemoveNode(node); } } } if (change.edgesToCreate != null) { foreach (var edge in change.edgesToCreate) { AddEdgeData(edge); } } return change; } void RemoveNode(FoldNodeView node) { RecordUndo("Delete Node"); graphAsset.nodes.Remove(node.Data); graphAsset.edges.RemoveAll(e => e.inputNodeGuid == node.Data.guid || e.outputNodeGuid == node.Data.guid); nodeLookup.Remove(node.Data.guid); MarkDirty(); } internal void AddEdgeData(Edge edge) { if (edge.input?.node is not FoldNodeView inputNode || edge.output?.node is not FoldNodeView outputNode) return; RecordUndo("Add Edge"); graphAsset.edges.Add(new FoldEdge { inputNodeGuid = inputNode.Data.guid, inputPortName = edge.input.portName, outputNodeGuid = outputNode.Data.guid, outputPortName = edge.output.portName }); MarkDirty(); } void RemoveEdgeData(Edge edge) { if (edge.input?.node is not FoldNodeView inputNode || edge.output?.node is not FoldNodeView outputNode) return; RecordUndo("Remove Edge"); graphAsset.edges.RemoveAll(e => e.inputNodeGuid == inputNode.Data.guid && e.outputNodeGuid == outputNode.Data.guid && e.inputPortName == edge.input.portName && e.outputPortName == edge.output.portName); MarkDirty(); } public void Reload() { suppressGraphChanges = true; nodeLookup.Clear(); var removable = graphElements.Where(e => e is Node || e is Edge).ToList(); DeleteElements(removable); foreach (var nodeData in graphAsset.nodes) { var node = CreateNode(nodeData); nodeLookup[nodeData.guid] = node; AddElement(node); } foreach (var edgeData in graphAsset.edges) { if (!nodeLookup.TryGetValue(edgeData.outputNodeGuid, out var outputNode) || !nodeLookup.TryGetValue(edgeData.inputNodeGuid, out var inputNode)) continue; var outputPort = outputNode.GetPort(edgeData.outputPortName); var inputPort = inputNode.GetPort(edgeData.inputPortName); if (outputPort == null || inputPort == null) continue; var edge = new Edge { output = outputPort, input = inputPort }; edge.input.Connect(edge); edge.output.Connect(edge); AddElement(edge); } suppressGraphChanges = false; } void OnKeyDown(KeyDownEvent evt) { bool isDuplicate = (evt.keyCode == KeyCode.D) && (evt.commandKey || evt.ctrlKey); if (!isDuplicate) return; evt.StopImmediatePropagation(); DuplicateSelectedNodes(); } void DuplicateSelectedNodes() { var selectedNodes = selection.OfType().ToList(); if (selectedNodes.Count == 0) return; suppressGraphChanges = true; RecordUndo("Duplicate Nodes"); var cloneMap = new Dictionary(); foreach (var original in selectedNodes) { var cloneData = CloneNodeData(original.Data); cloneData.position += new Vector2(30f, 30f); graphAsset.nodes.Add(cloneData); var cloneView = CreateNode(cloneData); cloneMap[original.Data.guid] = cloneView; nodeLookup[cloneData.guid] = cloneView; AddElement(cloneView); } var edgesToClone = graphAsset.edges .Where(e => cloneMap.ContainsKey(e.outputNodeGuid) && cloneMap.ContainsKey(e.inputNodeGuid)) .ToList(); foreach (var edgeData in edgesToClone) { var outputNode = cloneMap[edgeData.outputNodeGuid]; var inputNode = cloneMap[edgeData.inputNodeGuid]; var outputPort = outputNode.GetPort(edgeData.outputPortName); var inputPort = inputNode.GetPort(edgeData.inputPortName); if (outputPort == null || inputPort == null) continue; var edge = new Edge { output = outputPort, input = inputPort }; edge.input.Connect(edge); edge.output.Connect(edge); AddElement(edge); AddEdgeData(edge); } suppressGraphChanges = false; MarkDirty(); } FoldNodeData CloneNodeData(FoldNodeData data) { var json = JsonUtility.ToJson(data); var clone = (FoldNodeData)Activator.CreateInstance(data.GetType()); JsonUtility.FromJsonOverwrite(json, clone); clone.guid = Guid.NewGuid().ToString(); return clone; } FoldNodeView CreateNode(FoldNodeData data) { FoldNodeView view = data switch { FloatValueNodeData => new FloatValueNodeView(), VectorValueNodeData => new VectorValueNodeView(), GameObjectNodeData => new GameObjectNodeView(), AxisAlignNodeData => new AxisAlignNodeView(), PlaneToTubeNodeData => new PlaneToTubeNodeView(), PointAlignNodeData => new PointAlignNodeView(), TubeToPlaneNodeData => new TubeToPlaneNodeView(), KeyframeNodeData => new KeyframeNodeView(), _ => throw new ArgumentOutOfRangeException(nameof(data), $"Unknown node data type {data.GetType().Name}") }; view.Initialize(this, data); return view; } public T CreateNode(Vector2 position) where T : FoldNodeData, new() { RecordUndo("Create Node"); var data = new T { position = position }; graphAsset.nodes.Add(data); var view = CreateNode(data); nodeLookup[data.guid] = view; AddElement(view); MarkDirty(); return data; } internal FoldNodeView CreateNode(Type dataType, Vector2 position) { RecordUndo("Create Node"); var data = (FoldNodeData)Activator.CreateInstance(dataType); data.position = position; graphAsset.nodes.Add(data); var view = CreateNode(data); nodeLookup[data.guid] = view; AddElement(view); return view; } public override void BuildContextualMenu(ContextualMenuPopulateEvent evt) { ShowSearchWindow(null, evt.mousePosition); evt.StopPropagation(); } public void MarkDirty() { EditorUtility.SetDirty(graphAsset); } public void ShowNodeCreationMenuFromPort(Port startPort, Vector2 worldPosition) => ShowSearchWindow(startPort, worldPosition); public void CreateNodeAndConnect(Port startPort, Type dataType, Vector2 graphPosition) { suppressGraphChanges = true; var view = CreateNode(dataType, graphPosition); var compatible = GetCompatiblePorts(startPort, null).FirstOrDefault(p => p.node == view); if (compatible != null) { var output = startPort.direction == Direction.Output ? startPort : compatible; var input = startPort.direction == Direction.Output ? compatible : startPort; var edge = new Edge { output = output, input = input }; edge.input.Connect(edge); edge.output.Connect(edge); AddElement(edge); AddEdgeData(edge); } suppressGraphChanges = false; MarkDirty(); } IEnumerable<(string label, Type dataType)> GetCompatibleNodeOptions(Port startPort) { Type type = startPort.portType; bool fromOutput = startPort.direction == Direction.Output; if (type == typeof(float)) { if (fromOutput) { yield return ("Fold/Axis Align", typeof(AxisAlignNodeData)); yield return ("Fold/Plane to Tube", typeof(PlaneToTubeNodeData)); yield return ("Fold/Point Align", typeof(PointAlignNodeData)); yield return ("Fold/Tube to Plane", typeof(TubeToPlaneNodeData)); } else { yield return ("Value/Float", typeof(FloatValueNodeData)); } } else if (type == typeof(Vector4)) { if (fromOutput) { yield return ("Fold/Axis Align", typeof(AxisAlignNodeData)); yield return ("Fold/Plane to Tube", typeof(PlaneToTubeNodeData)); yield return ("Fold/Point Align", typeof(PointAlignNodeData)); yield return ("Fold/Tube to Plane", typeof(TubeToPlaneNodeData)); } else { yield return ("Value/Vector", typeof(VectorValueNodeData)); } } else if (type == typeof(GameObject)) { if (fromOutput) yield return ("Output/Keyframe", typeof(KeyframeNodeData)); else yield return ("Value/GameObject", typeof(GameObjectNodeData)); } else if (typeof(IFoldNode).IsAssignableFrom(type)) { if (fromOutput) { yield return ("Fold/Axis Align", typeof(AxisAlignNodeData)); yield return ("Fold/Plane to Tube", typeof(PlaneToTubeNodeData)); yield return ("Fold/Point Align", typeof(PointAlignNodeData)); yield return ("Fold/Tube to Plane", typeof(TubeToPlaneNodeData)); yield return ("Output/Keyframe", typeof(KeyframeNodeData)); } else { yield return ("Fold/Axis Align", typeof(AxisAlignNodeData)); yield return ("Fold/Plane to Tube", typeof(PlaneToTubeNodeData)); yield return ("Fold/Point Align", typeof(PointAlignNodeData)); yield return ("Fold/Tube to Plane", typeof(TubeToPlaneNodeData)); } } } IEnumerable<(string label, Type dataType)> GetAllNodeOptions() { yield return ("Fold/Axis Align", typeof(AxisAlignNodeData)); yield return ("Fold/Plane to Tube", typeof(PlaneToTubeNodeData)); yield return ("Fold/Point Align", typeof(PointAlignNodeData)); yield return ("Fold/Tube to Plane", typeof(TubeToPlaneNodeData)); yield return ("Value/Float", typeof(FloatValueNodeData)); yield return ("Value/Vector", typeof(VectorValueNodeData)); yield return ("Value/GameObject", typeof(GameObjectNodeData)); yield return ("Output/Keyframe", typeof(KeyframeNodeData)); } internal IEnumerable<(string label, Type dataType)> GetNodeOptions(Port startPort) { return startPort == null ? GetAllNodeOptions() : GetCompatibleNodeOptions(startPort); } public T GetInputValue(string nodeGuid, string portName, T fallback) { var node = nodeLookup.TryGetValue(nodeGuid, out var found) ? found : null; if (node == null) return fallback; var port = node.GetPort(portName); var edge = port?.connections.FirstOrDefault(); if (edge?.output?.node is IValueNode provider) return provider.GetValue(); return fallback; } public void ShowNotification(string message) { window.ShowNotification(message); } void OnUndoRedo() { Reload(); } public void Dispose() { Undo.undoRedoPerformed -= OnUndoRedo; } internal void RecordUndo(string label) { if (suppressGraphChanges) return; Undo.RegisterCompleteObjectUndo(graphAsset, label); } void ShowSearchWindow(Port startPort, Vector2 worldPosition) { var provider = ScriptableObject.CreateInstance(); provider.Initialize(this, startPort, worldPosition); if (!provider.HasEntries()) return; SearchWindow.Open(new SearchWindowContext(GUIUtility.GUIToScreenPoint(worldPosition)), provider); } } class FoldEdgeConnectorListener : IEdgeConnectorListener { readonly FoldGraphView graphView; public FoldEdgeConnectorListener(FoldGraphView graphView) { this.graphView = graphView; } public void OnDropOutsidePort(Edge edge, Vector2 position) { var startPort = edge.output ?? edge.input; if (startPort == null) return; graphView.ShowNodeCreationMenuFromPort(startPort, position); } public void OnDrop(GraphView gv, Edge edge) { if (edge.input == null || edge.output == null) return; edge.input.Connect(edge); edge.output.Connect(edge); gv.AddElement(edge); if (graphView != null) graphView.AddEdgeData(edge); } } class FoldSearchProvider : ScriptableObject, ISearchWindowProvider { FoldGraphView graphView; Port startPort; Vector2 worldPosition; List<(string label, Type dataType)> cachedOptions; public void Initialize(FoldGraphView graphView, Port startPort, Vector2 worldPosition) { this.graphView = graphView; this.startPort = startPort; this.worldPosition = worldPosition; cachedOptions = graphView.GetNodeOptions(startPort).ToList(); } public bool HasEntries() => cachedOptions != null && cachedOptions.Count > 0; public List CreateSearchTree(SearchWindowContext context) { var entries = new List { new SearchTreeGroupEntry(new GUIContent("Create Node"), 0) }; foreach (var option in cachedOptions) { entries.Add(new SearchTreeEntry(new GUIContent(option.label)) { level = 1, userData = option.dataType }); } return entries; } public bool OnSelectEntry(SearchTreeEntry entry, SearchWindowContext context) { var dataType = entry.userData as Type; if (dataType == null) return false; Vector2 graphPosition = graphView.contentViewContainer.WorldToLocal(worldPosition); if (startPort != null) graphView.CreateNodeAndConnect(startPort, dataType, graphPosition); else graphView.CreateNode(dataType, graphPosition); return true; } }