diff options
Diffstat (limited to 'Scripts/Fold')
| -rw-r--r-- | Scripts/Fold/Editor/AxisAlignNode.cs | 26 | ||||
| -rw-r--r-- | Scripts/Fold/Editor/BaseFoldNode.cs | 14 | ||||
| -rw-r--r-- | Scripts/Fold/Editor/FoldGraph.cs | 152 | ||||
| -rw-r--r-- | Scripts/Fold/Editor/FoldGraphView.cs | 714 | ||||
| -rw-r--r-- | Scripts/Fold/Editor/FoldWindow.cs | 68 | ||||
| -rw-r--r-- | Scripts/Fold/Editor/KeyframeNode.cs | 15 | ||||
| -rw-r--r-- | Scripts/Fold/Editor/KeyframeNodeView.cs | 120 | ||||
| -rw-r--r-- | Scripts/Fold/Editor/PlaneToTubeNode.cs | 26 | ||||
| -rw-r--r-- | Scripts/Fold/Editor/PointAlignNode.cs | 26 | ||||
| -rw-r--r-- | Scripts/Fold/Editor/TubeToPlaneNode.cs | 26 |
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 }; - } -} - |
