summaryrefslogtreecommitdiffstats
path: root/Scripts/Fold
diff options
context:
space:
mode:
Diffstat (limited to 'Scripts/Fold')
-rw-r--r--Scripts/Fold/Editor/AxisAlignNode.cs26
-rw-r--r--Scripts/Fold/Editor/BaseFoldNode.cs14
-rw-r--r--Scripts/Fold/Editor/FoldGraph.cs152
-rw-r--r--Scripts/Fold/Editor/FoldGraphView.cs714
-rw-r--r--Scripts/Fold/Editor/FoldWindow.cs68
-rw-r--r--Scripts/Fold/Editor/KeyframeNode.cs15
-rw-r--r--Scripts/Fold/Editor/KeyframeNodeView.cs120
-rw-r--r--Scripts/Fold/Editor/PlaneToTubeNode.cs26
-rw-r--r--Scripts/Fold/Editor/PointAlignNode.cs26
-rw-r--r--Scripts/Fold/Editor/TubeToPlaneNode.cs26
10 files changed, 901 insertions, 286 deletions
diff --git a/Scripts/Fold/Editor/AxisAlignNode.cs b/Scripts/Fold/Editor/AxisAlignNode.cs
deleted file mode 100644
index dc58aec..0000000
--- a/Scripts/Fold/Editor/AxisAlignNode.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using GraphProcessor;
-using UnityEngine;
-
-[System.Serializable, NodeMenuItem("Fold/AxisAlign")]
-public class AxisAlignNode : BaseFoldNode
-{
- [Input(name = "po"), SerializeField, Tooltip("Origin point.")]
- public Vector4 po;
-
- [Input(name = "pp"), SerializeField, Tooltip("Pivot point.")]
- public Vector4 pp;
-
- [Input(name = "r"), SerializeField, Tooltip("Radial axis. Points along this line are not moved at all.")]
- public Vector4 r;
-
- [Input(name = "t"), SerializeField, Tooltip("Effect strength.")]
- public float t;
-
- public override string name => "Axis Align";
-
- public override FoldNodeSerialized Serialize()
- {
- return new FoldNodeSerialized { opcode = 4, float0 = t, vec0 = po, vec1 = pp, vec2 = r };
- }
-}
-
diff --git a/Scripts/Fold/Editor/BaseFoldNode.cs b/Scripts/Fold/Editor/BaseFoldNode.cs
deleted file mode 100644
index e8b394e..0000000
--- a/Scripts/Fold/Editor/BaseFoldNode.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using GraphProcessor;
-
-public abstract class BaseFoldNode : BaseNode
-{
- [Input(name = "Input")]
- public BaseFoldNode input;
-
- [Output(name = "Out")]
- public BaseFoldNode output;
-
- // Generate a generic representation of the node. This is used for animation.
- public abstract FoldNodeSerialized Serialize();
-}
-
diff --git a/Scripts/Fold/Editor/FoldGraph.cs b/Scripts/Fold/Editor/FoldGraph.cs
index f1fc290..ca985f4 100644
--- a/Scripts/Fold/Editor/FoldGraph.cs
+++ b/Scripts/Fold/Editor/FoldGraph.cs
@@ -1,5 +1,151 @@
+using System;
+using System.Collections.Generic;
using UnityEngine;
-using GraphProcessor;
-[System.Serializable]
-public class FoldGraph : BaseGraph {}
+[Serializable]
+public class FoldEdge
+{
+ public string outputNodeGuid;
+ public string outputPortName;
+ public string inputNodeGuid;
+ public string inputPortName;
+}
+
+[Serializable]
+public abstract class FoldNodeData
+{
+ public string guid = Guid.NewGuid().ToString();
+ public Vector2 position;
+ public virtual string Title => GetType().Name;
+}
+
+[Serializable]
+public abstract class FoldOperationData : FoldNodeData
+{
+ public abstract FoldNodeSerialized Serialize(FoldGraphView graphView);
+}
+
+[Serializable]
+public class FloatValueNodeData : FoldNodeData
+{
+ public float value;
+ public override string Title => "Float";
+}
+
+[Serializable]
+public class VectorValueNodeData : FoldNodeData
+{
+ public Vector4 value;
+ public override string Title => "Vector";
+}
+
+[Serializable]
+public class GameObjectNodeData : FoldNodeData
+{
+ public GameObject output;
+ public override string Title => "GameObject";
+}
+
+[Serializable]
+public class AxisAlignNodeData : FoldOperationData
+{
+ public Vector4 po;
+ public Vector4 pp;
+ public Vector4 r;
+ public float t;
+
+ public override string Title => "Axis Align";
+
+ public override FoldNodeSerialized Serialize(FoldGraphView graphView)
+ {
+ return new FoldNodeSerialized
+ {
+ opcode = 4,
+ float0 = graphView.GetInputValue(guid, "t", t),
+ vec0 = graphView.GetInputValue(guid, "po", po),
+ vec1 = graphView.GetInputValue(guid, "pp", pp),
+ vec2 = graphView.GetInputValue(guid, "r", r),
+ };
+ }
+}
+
+[Serializable]
+public class PlaneToTubeNodeData : FoldOperationData
+{
+ public Vector4 p;
+ public Vector4 r;
+ public Vector4 s;
+ public float t;
+
+ public override string Title => "Plane to Tube";
+
+ public override FoldNodeSerialized Serialize(FoldGraphView graphView)
+ {
+ return new FoldNodeSerialized
+ {
+ opcode = 2,
+ float0 = graphView.GetInputValue(guid, "t", t),
+ vec0 = graphView.GetInputValue(guid, "p", p),
+ vec1 = graphView.GetInputValue(guid, "r", r),
+ vec2 = graphView.GetInputValue(guid, "s", s),
+ };
+ }
+}
+
+[Serializable]
+public class PointAlignNodeData : FoldOperationData
+{
+ public Vector4 po;
+ public Vector4 pp;
+ public Vector4 r;
+ public float t;
+
+ public override string Title => "Point Align";
+
+ public override FoldNodeSerialized Serialize(FoldGraphView graphView)
+ {
+ return new FoldNodeSerialized
+ {
+ opcode = 3,
+ float0 = graphView.GetInputValue(guid, "t", t),
+ vec0 = graphView.GetInputValue(guid, "po", po),
+ vec1 = graphView.GetInputValue(guid, "pp", pp),
+ vec2 = graphView.GetInputValue(guid, "r", r),
+ };
+ }
+}
+
+[Serializable]
+public class TubeToPlaneNodeData : FoldOperationData
+{
+ public Vector4 p;
+ public Vector4 r;
+ public Vector4 s;
+ public float t;
+
+ public override string Title => "Tube to Plane";
+
+ public override FoldNodeSerialized Serialize(FoldGraphView graphView)
+ {
+ return new FoldNodeSerialized
+ {
+ opcode = 1,
+ float0 = graphView.GetInputValue(guid, "t", t),
+ vec0 = graphView.GetInputValue(guid, "p", p),
+ vec1 = graphView.GetInputValue(guid, "r", r),
+ vec2 = graphView.GetInputValue(guid, "s", s),
+ };
+ }
+}
+
+[Serializable]
+public class KeyframeNodeData : FoldNodeData
+{
+ public override string Title => "Keyframe";
+}
+
+public class FoldGraph : ScriptableObject
+{
+ [SerializeReference] public List<FoldNodeData> nodes = new();
+ public List<FoldEdge> edges = new();
+}
diff --git a/Scripts/Fold/Editor/FoldGraphView.cs b/Scripts/Fold/Editor/FoldGraphView.cs
index b8911c1..3b0b295 100644
--- a/Scripts/Fold/Editor/FoldGraphView.cs
+++ b/Scripts/Fold/Editor/FoldGraphView.cs
@@ -1,25 +1,709 @@
-using UnityEngine;
-using UnityEditor;
-using GraphProcessor;
using System;
using System.Collections.Generic;
using System.Linq;
+using UnityEditor;
+using UnityEditor.Experimental.GraphView;
+using UnityEditor.UIElements;
+using UnityEngine;
+using UnityEngine.UIElements;
+
+public interface IFoldNode
+{
+ FoldNodeSerialized Serialize();
+ IFoldNode GetPreviousFold();
+}
+
+public interface IValueNode<T>
+{
+ T GetValue();
+}
+
+public abstract class FoldNodeView : Node
+{
+ public FoldNodeData Data { get; private set; }
+ protected FoldGraphView graphView;
+
+ readonly Dictionary<string, Port> 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));
+ }
+
+ public override void SetPosition(Rect newPos)
+ {
+ base.SetPosition(newPos);
+ if (Data != null)
+ {
+ 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;
+ 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<float>
+{
+ 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 =>
+ {
+ DataTyped.value = evt.newValue;
+ graphView.MarkDirty();
+ });
+ mainContainer.Add(field);
+ }
+
+ public float GetValue() => DataTyped.value;
+}
+
+public class VectorValueNodeView : FoldNodeView, IValueNode<Vector4>
+{
+ 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 =>
+ {
+ DataTyped.value = evt.newValue;
+ graphView.MarkDirty();
+ });
+ mainContainer.Add(field);
+ }
+
+ public Vector4 GetValue() => DataTyped.value;
+}
+
+public class GameObjectNodeView : FoldNodeView, IValueNode<GameObject>
+{
+ 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 =>
+ {
+ 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));
+
+ mainContainer.Add(CreateVectorField("Origin", () => DataTyped.po, v => DataTyped.po = v));
+ mainContainer.Add(CreateVectorField("Pivot", () => DataTyped.pp, v => DataTyped.pp = v));
+ mainContainer.Add(CreateVectorField("Radial Axis", () => DataTyped.r, v => DataTyped.r = v));
+ mainContainer.Add(CreateFloatField("Strength", () => DataTyped.t, v => DataTyped.t = v));
+ }
+
+ public override FoldNodeSerialized Serialize()
+ {
+ return DataTyped.Serialize(graphView);
+ }
+
+ Vector4Field CreateVectorField(string label, Func<Vector4> getter, Action<Vector4> setter)
+ {
+ var field = new Vector4Field(label) { value = getter() };
+ field.RegisterValueChangedCallback(evt =>
+ {
+ setter(evt.newValue);
+ graphView.MarkDirty();
+ });
+ return field;
+ }
+
+ FloatField CreateFloatField(string label, Func<float> getter, Action<float> setter)
+ {
+ var field = new FloatField(label) { value = getter() };
+ field.RegisterValueChangedCallback(evt =>
+ {
+ setter(evt.newValue);
+ graphView.MarkDirty();
+ });
+ return field;
+ }
+}
+
+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 class FoldGraphView : BaseGraphView
+ mainContainer.Add(CreateVectorField("Origin", () => DataTyped.p, v => DataTyped.p = v));
+ mainContainer.Add(CreateVectorField("Radial Axis", () => DataTyped.r, v => DataTyped.r = v));
+ mainContainer.Add(CreateVectorField("Tangent Axis", () => DataTyped.s, v => DataTyped.s = v));
+ mainContainer.Add(CreateFloatField("Strength", () => DataTyped.t, v => DataTyped.t = v));
+ }
+
+ public override FoldNodeSerialized Serialize() => DataTyped.Serialize(graphView);
+
+ Vector4Field CreateVectorField(string label, Func<Vector4> getter, Action<Vector4> setter)
+ {
+ var field = new Vector4Field(label) { value = getter() };
+ field.RegisterValueChangedCallback(evt =>
+ {
+ setter(evt.newValue);
+ graphView.MarkDirty();
+ });
+ return field;
+ }
+
+ FloatField CreateFloatField(string label, Func<float> getter, Action<float> setter)
+ {
+ var field = new FloatField(label) { value = getter() };
+ field.RegisterValueChangedCallback(evt =>
+ {
+ setter(evt.newValue);
+ graphView.MarkDirty();
+ });
+ return field;
+ }
+}
+
+public class PointAlignNodeView : FoldOperationNodeView
{
- public FoldGraphView(EditorWindow window) : base(window) {}
+ 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));
+
+ mainContainer.Add(CreateVectorField("Origin", () => DataTyped.po, v => DataTyped.po = v));
+ mainContainer.Add(CreateVectorField("Pivot", () => DataTyped.pp, v => DataTyped.pp = v));
+ mainContainer.Add(CreateVectorField("Radial Axis", () => DataTyped.r, v => DataTyped.r = v));
+ mainContainer.Add(CreateFloatField("Strength", () => DataTyped.t, v => DataTyped.t = v));
+ }
+
+ public override FoldNodeSerialized Serialize() => DataTyped.Serialize(graphView);
- public override IEnumerable<(string path, Type type)> FilterCreateNodeMenuEntries()
+ Vector4Field CreateVectorField(string label, Func<Vector4> getter, Action<Vector4> setter)
{
- return NodeProvider.GetNodeMenuEntries(graph)
- .Where(entry =>
- typeof(BaseFoldNode).IsAssignableFrom(entry.type) ||
- typeof(KeyframeNode).IsAssignableFrom(entry.type) ||
- // Whitelist a few built in nodes that we use.
- typeof(FloatNode).IsAssignableFrom(entry.type) ||
- typeof(GameObjectNode).IsAssignableFrom(entry.type) ||
- typeof(VectorNode).IsAssignableFrom(entry.type)
- );
+ var field = new Vector4Field(label) { value = getter() };
+ field.RegisterValueChangedCallback(evt =>
+ {
+ setter(evt.newValue);
+ graphView.MarkDirty();
+ });
+ return field;
+ }
+
+ FloatField CreateFloatField(string label, Func<float> getter, Action<float> setter)
+ {
+ var field = new FloatField(label) { value = getter() };
+ field.RegisterValueChangedCallback(evt =>
+ {
+ setter(evt.newValue);
+ graphView.MarkDirty();
+ });
+ return field;
}
}
+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));
+
+ mainContainer.Add(CreateVectorField("Origin", () => DataTyped.p, v => DataTyped.p = v));
+ mainContainer.Add(CreateVectorField("Radial Axis", () => DataTyped.r, v => DataTyped.r = v));
+ mainContainer.Add(CreateVectorField("Tangent Axis", () => DataTyped.s, v => DataTyped.s = v));
+ mainContainer.Add(CreateFloatField("Strength", () => DataTyped.t, v => DataTyped.t = v));
+ }
+
+ public override FoldNodeSerialized Serialize() => DataTyped.Serialize(graphView);
+
+ Vector4Field CreateVectorField(string label, Func<Vector4> getter, Action<Vector4> setter)
+ {
+ var field = new Vector4Field(label) { value = getter() };
+ field.RegisterValueChangedCallback(evt =>
+ {
+ setter(evt.newValue);
+ graphView.MarkDirty();
+ });
+ return field;
+ }
+
+ FloatField CreateFloatField(string label, Func<float> getter, Action<float> setter)
+ {
+ var field = new FloatField(label) { value = getter() };
+ field.RegisterValueChangedCallback(evt =>
+ {
+ setter(evt.newValue);
+ graphView.MarkDirty();
+ });
+ return field;
+ }
+}
+
+public class KeyframeNodeView : FoldNodeView
+{
+ Port gameObjectPort;
+ Port foldPort;
+ readonly List<string> 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<GameObject> 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<MeshRenderer>();
+ 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<IFoldNode>();
+ 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<string, FoldNodeView> nodeLookup = new();
+ bool suppressGraphChanges;
+
+ public FoldGraphView(FoldWindow owner, FoldGraph graph)
+ {
+ window = owner;
+ graphAsset = graph;
+
+ 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<KeyDownEvent>(OnKeyDown);
+
+ Reload();
+ }
+
+ GridBackground gridBackground;
+
+ 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)
+ {
+ 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();
+ }
+
+ void AddEdgeData(Edge edge)
+ {
+ if (edge.input?.node is not FoldNodeView inputNode ||
+ edge.output?.node is not FoldNodeView outputNode)
+ return;
+
+ 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;
+
+ 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<FoldNodeView>().ToList();
+ if (selectedNodes.Count == 0)
+ return;
+
+ suppressGraphChanges = true;
+
+ var cloneMap = new Dictionary<string, FoldNodeView>();
+ 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<T>(Vector2 position) where T : FoldNodeData, new()
+ {
+ var data = new T { position = position };
+ graphAsset.nodes.Add(data);
+ var view = CreateNode(data);
+ nodeLookup[data.guid] = view;
+ AddElement(view);
+ MarkDirty();
+ return data;
+ }
+
+ public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
+ {
+ base.BuildContextualMenu(evt);
+ Vector2 graphPos = contentViewContainer.WorldToLocal(evt.mousePosition);
+
+ evt.menu.AppendAction("Create/Fold/Axis Align", _ => CreateNode<AxisAlignNodeData>(graphPos));
+ evt.menu.AppendAction("Create/Fold/Plane to Tube", _ => CreateNode<PlaneToTubeNodeData>(graphPos));
+ evt.menu.AppendAction("Create/Fold/Point Align", _ => CreateNode<PointAlignNodeData>(graphPos));
+ evt.menu.AppendAction("Create/Fold/Tube to Plane", _ => CreateNode<TubeToPlaneNodeData>(graphPos));
+ evt.menu.AppendSeparator();
+ evt.menu.AppendAction("Create/Value/Float", _ => CreateNode<FloatValueNodeData>(graphPos));
+ evt.menu.AppendAction("Create/Value/Vector", _ => CreateNode<VectorValueNodeData>(graphPos));
+ evt.menu.AppendAction("Create/Value/GameObject", _ => CreateNode<GameObjectNodeData>(graphPos));
+ evt.menu.AppendSeparator();
+ evt.menu.AppendAction("Create/Output/Keyframe", _ => CreateNode<KeyframeNodeData>(graphPos));
+ }
+
+ public void MarkDirty()
+ {
+ EditorUtility.SetDirty(graphAsset);
+ }
+
+ public T GetInputValue<T>(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<T> provider)
+ return provider.GetValue();
+
+ return fallback;
+ }
+
+ public void ShowNotification(string message)
+ {
+ window.ShowNotification(message);
+ }
+}
diff --git a/Scripts/Fold/Editor/FoldWindow.cs b/Scripts/Fold/Editor/FoldWindow.cs
index 270bc2a..44e373b 100644
--- a/Scripts/Fold/Editor/FoldWindow.cs
+++ b/Scripts/Fold/Editor/FoldWindow.cs
@@ -1,32 +1,70 @@
-using UnityEngine;
using UnityEditor;
-using GraphProcessor;
+using UnityEngine;
+using UnityEngine.UIElements;
-public class FoldWindow : BaseGraphWindow
+public class FoldWindow : EditorWindow
{
const string GraphPath = "Assets/FoldGraph.asset";
+ FoldGraphView graphView;
+ FoldGraph graphAsset;
+
[MenuItem("Tools/yum_food/Fold")]
public static void Open()
{
- var graph = AssetDatabase.LoadAssetAtPath<FoldGraph>(GraphPath);
- if (graph == null)
+ var window = GetWindow<FoldWindow>();
+ window.titleContent = new GUIContent("Fold");
+ window.Show();
+ }
+
+ void OnEnable()
+ {
+ graphAsset = LoadOrCreateGraph();
+ ConstructGraphView();
+ }
+
+ void OnDisable()
+ {
+ if (graphView != null)
+ {
+ rootVisualElement.Remove(graphView);
+ graphView = null;
+ }
+
+ AssetDatabase.SaveAssets();
+ }
+
+ FoldGraph LoadOrCreateGraph()
+ {
+ var asset = AssetDatabase.LoadAssetAtPath<FoldGraph>(GraphPath);
+ if (asset == null)
{
- graph = ScriptableObject.CreateInstance<FoldGraph>();
- AssetDatabase.CreateAsset(graph, GraphPath);
+ asset = CreateInstance<FoldGraph>();
+ AssetDatabase.CreateAsset(asset, GraphPath);
AssetDatabase.SaveAssets();
}
- var w = GetWindow<FoldWindow>();
- w.InitializeGraph(graph);
- w.Show();
+ return asset;
}
- protected override void OnDestroy() => graphView?.Dispose();
+ void ConstructGraphView()
+ {
+ graphView = new FoldGraphView(this, graphAsset)
+ {
+ name = "Fold Graph"
+ };
- protected override void InitializeWindow(BaseGraph graph)
+ graphView.style.flexGrow = 1f;
+ graphView.style.width = Length.Percent(100);
+ graphView.style.height = Length.Percent(100);
+ rootVisualElement.Add(graphView);
+ }
+
+ public void ShowNotification(string message)
{
- titleContent = new GUIContent("Fold");
- if (graphView == null) rootView.Add(graphView = new FoldGraphView(this));
+ if (string.IsNullOrEmpty(message))
+ return;
+
+ ShowNotification(new GUIContent(message));
}
-} \ No newline at end of file
+}
diff --git a/Scripts/Fold/Editor/KeyframeNode.cs b/Scripts/Fold/Editor/KeyframeNode.cs
deleted file mode 100644
index 074ec62..0000000
--- a/Scripts/Fold/Editor/KeyframeNode.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using GraphProcessor;
-using UnityEngine;
-
-[System.Serializable, NodeMenuItem("Fold/Keyframe")]
-public class KeyframeNode : BaseNode
-{
- [Input(name = "Game Object")]
- public GameObject targetObject;
-
- [Input(name = "Fold Data")]
- public BaseFoldNode foldData;
-
- public override string name => "Keyframe";
-}
-
diff --git a/Scripts/Fold/Editor/KeyframeNodeView.cs b/Scripts/Fold/Editor/KeyframeNodeView.cs
deleted file mode 100644
index 65d839b..0000000
--- a/Scripts/Fold/Editor/KeyframeNodeView.cs
+++ /dev/null
@@ -1,120 +0,0 @@
-using GraphProcessor;
-using UnityEngine.UIElements;
-using UnityEngine;
-using System.Linq;
-using System.Collections.Generic;
-
-[NodeCustomEditor(typeof(KeyframeNode))]
-public class KeyframeNodeView : BaseNodeView
-{
- Button generateButton;
- const string ERROR_TYPE = "GameObject input has wrong type";
- const string ERROR_REND = "GameObject must have a MeshRenderer component";
-
- public override void Enable()
- {
- generateButton = new Button(OnGenerateClick) { text = "Generate Keyframe" };
- controlsContainer.Add(generateButton);
-
- onPortConnected += _ => Validate();
- onPortDisconnected += _ => Validate();
- schedule.Execute(Validate).Every(200);
-
- Validate();
- }
-
- void OnGenerateClick()
- {
- // Process the graph to propagate values through connections
- var processor = new ProcessGraphProcessor(nodeTarget.graph);
- processor.Run();
-
- var go = GetConnectedGameObject();
- if (go == null) return;
- var rend = go.GetComponent<MeshRenderer>();
- if (rend == null) return;
-
- var foldNode = GetConnectedFoldNode();
-
- // Accumulate nodes. We are going backwards from the output node
- // (KeyframeNode) to the root node.
- var foldNodes = new Stack<BaseFoldNode>();
- var cur = foldNode;
- while (cur != null) {
- foldNodes.Push(cur);
- cur = GetInputFoldNode(cur);
- }
-
- var mpb = new MaterialPropertyBlock();
- rend.GetPropertyBlock(mpb);
-
- // Set material properties for all 16 slots.
- const int kNumSlots = 16;
- for (int i = 0; i < kNumSlots; i++) {
- string slotPrefix = $"_Vertex_Deformation_Slot_{i}_";
- BaseFoldNode node = (foldNodes.Count > 0) ? foldNodes.Pop() : null;
- FoldNodeSerialized data = node?.Serialize();
-
- bool active = data != null;
- if (active) {
- Debug.Log($"Setting opcode {data.opcode}");
- }
- mpb.SetFloat(slotPrefix + "Enabled", active ? 1.0f : 0.0f);
- mpb.SetInt(slotPrefix + "Opcode", active ? data.opcode : 0);
-
- mpb.SetFloat(slotPrefix + "Float_0", active ? data.float0 : 0.0f);
- mpb.SetFloat(slotPrefix + "Float_1", active ? data.float1 : 0.0f);
- mpb.SetFloat(slotPrefix + "Float_2", active ? data.float2 : 0.0f);
- mpb.SetFloat(slotPrefix + "Float_3", active ? data.float3 : 0.0f);
-
- mpb.SetVector(slotPrefix + "Vector_0", active ? data.vec0 : Vector4.zero);
- if (active) {
- Debug.Log($"Setting vec0 {data.vec0}");
- }
- mpb.SetVector(slotPrefix + "Vector_1", active ? data.vec1 : Vector4.zero);
- mpb.SetVector(slotPrefix + "Vector_2", active ? data.vec2 : Vector4.zero);
- mpb.SetVector(slotPrefix + "Vector_3", active ? data.vec3 : Vector4.zero);
- }
-
- rend.SetPropertyBlock(mpb);
- }
-
- BaseFoldNode GetInputFoldNode(BaseFoldNode node)
- {
- // Traverse via edges in the graph model, as 'input' field might not be populated in Editor
- var port = node.GetPort(nameof(BaseFoldNode.input), null);
- if (port == null) return null;
-
- // Find the edge connected to this input port
- var edge = node.graph.edges.FirstOrDefault(e => e.inputPort == port);
- return edge?.outputNode as BaseFoldNode;
- }
-
- T GetConnectedNode<T>(string fieldName) where T : class
- {
- var edge = GetFirstPortViewFromFieldName(fieldName)?.GetEdges().FirstOrDefault();
- return (edge?.output.node as BaseNodeView)?.nodeTarget as T;
- }
-
- GameObject GetConnectedGameObject() => GetConnectedNode<GameObjectNode>(nameof(KeyframeNode.targetObject))?.output;
-
- BaseFoldNode GetConnectedFoldNode() => GetConnectedNode<BaseFoldNode>(nameof(KeyframeNode.foldData));
-
- void Validate()
- {
- var go = GetConnectedGameObject();
- bool hasGoConn = GetFirstPortViewFromFieldName(nameof(KeyframeNode.targetObject))?.GetEdges().Any() ?? false;
- bool hasFoldConn = GetFirstPortViewFromFieldName(nameof(KeyframeNode.foldData))?.GetEdges().Any() ?? false;
-
- RemoveMessageView(ERROR_TYPE);
- RemoveMessageView(ERROR_REND);
-
- if (hasGoConn)
- {
- if (go == null) AddMessageView(ERROR_TYPE, NodeMessageType.Error);
- else if (go.GetComponent<MeshRenderer>() == null) AddMessageView(ERROR_REND, NodeMessageType.Error);
- }
-
- generateButton.SetEnabled(hasGoConn && hasFoldConn && go?.GetComponent<MeshRenderer>() != null);
- }
-}
diff --git a/Scripts/Fold/Editor/PlaneToTubeNode.cs b/Scripts/Fold/Editor/PlaneToTubeNode.cs
deleted file mode 100644
index b9158bf..0000000
--- a/Scripts/Fold/Editor/PlaneToTubeNode.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using GraphProcessor;
-using UnityEngine;
-
-[System.Serializable, NodeMenuItem("Fold/PlaneToTube")]
-public class PlaneToTubeNode : BaseFoldNode
-{
- [Input(name = "p"), SerializeField, Tooltip("Origin point.")]
- public Vector4 p;
-
- [Input(name = "r"), SerializeField, Tooltip("Radial axis. Points along this line are not moved at all.")]
- public Vector4 r;
-
- [Input(name = "s"), SerializeField, Tooltip("Tangent axis. This line determines the axis around which the tube is unwrapped.")]
- public Vector4 s;
-
- [Input(name = "t"), SerializeField, Tooltip("Effect strength.")]
- public float t;
-
- public override string name => "Plane to Tube";
-
- public override FoldNodeSerialized Serialize()
- {
- return new FoldNodeSerialized { opcode = 2, float0 = t, vec0 = p, vec1 = r, vec2 = s };
- }
-}
-
diff --git a/Scripts/Fold/Editor/PointAlignNode.cs b/Scripts/Fold/Editor/PointAlignNode.cs
deleted file mode 100644
index ddab647..0000000
--- a/Scripts/Fold/Editor/PointAlignNode.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using GraphProcessor;
-using UnityEngine;
-
-[System.Serializable, NodeMenuItem("Fold/PointAlign")]
-public class PointAlignNode : BaseFoldNode
-{
- [Input(name = "po"), SerializeField, Tooltip("Origin point.")]
- public Vector4 po;
-
- [Input(name = "pp"), SerializeField, Tooltip("Pivot point.")]
- public Vector4 pp;
-
- [Input(name = "r"), SerializeField, Tooltip("Radial axis. Points along this line are not moved at all.")]
- public Vector4 r;
-
- [Input(name = "t"), SerializeField, Tooltip("Effect strength.")]
- public float t;
-
- public override string name => "Point Align";
-
- public override FoldNodeSerialized Serialize()
- {
- return new FoldNodeSerialized { opcode = 3, float0 = t, vec0 = po, vec1 = pp, vec2 = r };
- }
-}
-
diff --git a/Scripts/Fold/Editor/TubeToPlaneNode.cs b/Scripts/Fold/Editor/TubeToPlaneNode.cs
deleted file mode 100644
index 72f1b9d..0000000
--- a/Scripts/Fold/Editor/TubeToPlaneNode.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using GraphProcessor;
-using UnityEngine;
-
-[System.Serializable, NodeMenuItem("Fold/TubeToPlane")]
-public class TubeToPlaneNode : BaseFoldNode
-{
- [Input(name = "p"), SerializeField, Tooltip("Origin point.")]
- public Vector4 p;
-
- [Input(name = "r"), SerializeField, Tooltip("Radial axis. Points along this line are not moved at all.")]
- public Vector4 r;
-
- [Input(name = "s"), SerializeField, Tooltip("Tangent axis. This line determines the axis around which the tube is unwrapped.")]
- public Vector4 s;
-
- [Input(name = "t"), SerializeField, Tooltip("Effect strength.")]
- public float t;
-
- public override string name => "Tube to Plane";
-
- public override FoldNodeSerialized Serialize()
- {
- return new FoldNodeSerialized { opcode = 1, float0 = t, vec0 = p, vec1 = r, vec2 = s };
- }
-}
-