summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoryum <yum.food.vr@gmail.com>2026-01-12 19:31:54 -0800
committeryum <yum.food.vr@gmail.com>2026-01-12 19:31:54 -0800
commit6d86c9663bab3ec1ef95ba455dfa7281415b7f44 (patch)
treecda255c2dfd0ef2c96334fb7ecf2182d4950086e
parentcbc299f489d6bdc38cce30d74040c54197efe125 (diff)
Fold: move from visual programming to fluent builder
-rw-r--r--Scripts/Fold/Editor/FoldGraph.cs146
-rw-r--r--Scripts/Fold/Editor/FoldGraphView.cs623
-rw-r--r--Scripts/Fold/Editor/FoldNodeSerialized.cs19
-rw-r--r--Scripts/Fold/Editor/FoldWindow.cs85
-rw-r--r--Scripts/Fold/Editor/README.md131
-rw-r--r--Scripts/Fold/Editor/VertexDeformationBuilder.cs339
-rw-r--r--Scripts/Fold/Editor/VertexDeformationExamples.cs114
7 files changed, 584 insertions, 873 deletions
diff --git a/Scripts/Fold/Editor/FoldGraph.cs b/Scripts/Fold/Editor/FoldGraph.cs
deleted file mode 100644
index c0719d9..0000000
--- a/Scripts/Fold/Editor/FoldGraph.cs
+++ /dev/null
@@ -1,146 +0,0 @@
-using System;
-using System.Collections.Generic;
-using UnityEngine;
-
-[Serializable]
-public class FoldEdge
-{
- public string outputNodeGuid, outputPortName, inputNodeGuid, inputPortName;
-}
-
-[Serializable]
-public readonly struct FoldOperationDefinition
-{
- public readonly string title;
- public readonly int opcode;
- public readonly (string name, Type type)[] inputs;
-
- public FoldOperationDefinition(string title, int opcode, params (string name, Type type)[] inputs)
- {
- this.title = title;
- this.opcode = opcode;
- this.inputs = inputs;
- }
-
- public FoldNodeSerialized Serialize(FoldGraphView graphView, string guid)
- {
- var serialized = new FoldNodeSerialized { opcode = opcode };
- int floatIndex = 0, vectorIndex = 0;
-
- foreach (var (name, type) in inputs)
- {
- if (type == typeof(float))
- SetFloat(ref serialized, floatIndex++, graphView.GetInputValue(guid, name, 0f));
- else if (type == typeof(Vector4))
- SetVector(ref serialized, vectorIndex++, graphView.GetInputValue(guid, name, Vector4.zero));
- }
-
- return serialized;
- }
-
- static void SetFloat(ref FoldNodeSerialized node, int index, float value)
- {
- switch (index)
- {
- case 0: node.float0 = value; break;
- case 1: node.float1 = value; break;
- case 2: node.float2 = value; break;
- case 3: node.float3 = value; break;
- }
- }
-
- static void SetVector(ref FoldNodeSerialized node, int index, Vector4 value)
- {
- switch (index)
- {
- case 0: node.vec0 = value; break;
- case 1: node.vec1 = value; break;
- case 2: node.vec2 = value; break;
- case 3: node.vec3 = value; break;
- }
- }
-}
-
-[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
-{
- protected abstract FoldOperationDefinition Definition { get; }
- public override string Title => Definition.title;
- public virtual IEnumerable<(string name, Type type)> GetInputPorts() => Definition.inputs;
- public virtual FoldNodeSerialized Serialize(FoldGraphView graphView) => Definition.Serialize(graphView, guid);
-}
-
-[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 KeyframeNodeData : FoldNodeData { public override string Title => "Keyframe"; }
-
-[Serializable]
-public class AxisAlignNodeData : FoldOperationData
-{
- static readonly FoldOperationDefinition DefinitionInfo = new(
- "Axis Align",
- 4,
- ("po", typeof(Vector4)),
- ("pp", typeof(Vector4)),
- ("r", typeof(Vector4)),
- ("t", typeof(float)));
-
- protected override FoldOperationDefinition Definition => DefinitionInfo;
-}
-
-[Serializable]
-public class PlaneToTubeNodeData : FoldOperationData
-{
- static readonly FoldOperationDefinition DefinitionInfo = new(
- "Plane to Tube",
- 2,
- ("p", typeof(Vector4)),
- ("r", typeof(Vector4)),
- ("s", typeof(Vector4)),
- ("t", typeof(float)));
-
- protected override FoldOperationDefinition Definition => DefinitionInfo;
-}
-
-[Serializable]
-public class PointAlignNodeData : FoldOperationData
-{
- static readonly FoldOperationDefinition DefinitionInfo = new(
- "Point Align",
- 3,
- ("po", typeof(Vector4)),
- ("pp", typeof(Vector4)),
- ("r", typeof(Vector4)),
- ("t", typeof(float)));
-
- protected override FoldOperationDefinition Definition => DefinitionInfo;
-}
-
-[Serializable]
-public class TubeToPlaneNodeData : FoldOperationData
-{
- static readonly FoldOperationDefinition DefinitionInfo = new(
- "Tube to Plane",
- 1,
- ("p", typeof(Vector4)),
- ("r", typeof(Vector4)),
- ("s", typeof(Vector4)),
- ("t", typeof(float)));
-
- protected override FoldOperationDefinition Definition => DefinitionInfo;
-}
-
-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
deleted file mode 100644
index e01bf4f..0000000
--- a/Scripts/Fold/Editor/FoldGraphView.cs
+++ /dev/null
@@ -1,623 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-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 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));
-
- RegisterCallback<MouseDownEvent>(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);
- (direction == Direction.Input ? inputContainer : outputContainer).Add(port);
- return port;
- }
-
- public Port GetPort(string name) => ports.TryGetValue(name, out var p) ? p : null;
-}
-
-public class ValueNodeView : FoldNodeView
-{
- object GetValue() => Data switch
- {
- FloatValueNodeData f => f.value,
- VectorValueNodeData v => v.value,
- GameObjectNodeData g => g.output,
- _ => null
- };
-
- public T GetTypedValue<T>() => GetValue() is T v ? v : default;
-
- public override void Initialize(FoldGraphView view, FoldNodeData data)
- {
- base.Initialize(view, data);
- var portType = data switch
- {
- FloatValueNodeData => typeof(float),
- VectorValueNodeData => typeof(Vector4),
- GameObjectNodeData => typeof(GameObject),
- _ => typeof(object)
- };
- AddPort(Direction.Output, Port.Capacity.Multi, "Out", portType);
-
- VisualElement field = data switch
- {
- FloatValueNodeData f => CreateFloatField(f),
- VectorValueNodeData v => CreateVectorField(v),
- GameObjectNodeData g => CreateObjectField(g),
- _ => null
- };
- if (field != null) extensionContainer.Add(field);
- RefreshExpandedState();
- }
-
- FloatField CreateFloatField(FloatValueNodeData d)
- {
- var f = new FloatField("Value") { value = d.value };
- f.RegisterValueChangedCallback(e => { graphView.RecordUndo("Edit Float"); d.value = e.newValue; graphView.MarkDirty(); });
- return f;
- }
-
- Vector4Field CreateVectorField(VectorValueNodeData d)
- {
- var f = new Vector4Field("Value") { value = d.value };
- f.RegisterValueChangedCallback(e => { graphView.RecordUndo("Edit Vector"); d.value = e.newValue; graphView.MarkDirty(); });
- return f;
- }
-
- ObjectField CreateObjectField(GameObjectNodeData d)
- {
- var f = new ObjectField("Object") { allowSceneObjects = true, objectType = typeof(GameObject), value = d.output };
- f.RegisterValueChangedCallback(e => { graphView.RecordUndo("Edit GameObject"); d.output = e.newValue as GameObject; graphView.MarkDirty(); });
- return f;
- }
-}
-
-public class OperationNodeView : FoldNodeView, IFoldNode
-{
- Port foldInputPort;
-
- public override void Initialize(FoldGraphView view, FoldNodeData data)
- {
- base.Initialize(view, data);
- foldInputPort = AddPort(Direction.Input, Port.Capacity.Single, "Input", typeof(IFoldNode));
- AddPort(Direction.Output, Port.Capacity.Single, "Out", typeof(IFoldNode));
-
- foreach (var (name, type) in ((FoldOperationData)data).GetInputPorts())
- AddPort(Direction.Input, Port.Capacity.Single, name, type);
- }
-
- public IFoldNode GetPreviousFold() => foldInputPort.connections.FirstOrDefault()?.output?.node as IFoldNode;
- public FoldNodeSerialized Serialize() => ((FoldOperationData)Data).Serialize(graphView);
-}
-
-public class KeyframeNodeView : FoldNodeView
-{
- Port gameObjectPort, foldPort;
-
- 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));
- extensionContainer.Add(new Button(OnGenerateClick) { text = "Generate Keyframe" });
- RefreshExpandedState();
- }
-
- void OnGenerateClick()
- {
- var goEdge = gameObjectPort.connections.FirstOrDefault();
- var go = (goEdge?.output?.node as ValueNodeView)?.GetTypedValue<GameObject>();
- if (go == null) { graphView.ShowNotification("GameObject input missing or empty."); return; }
-
- var renderer = go.GetComponent<MeshRenderer>();
- if (renderer == null) { graphView.ShowNotification("GameObject must have a MeshRenderer component."); return; }
-
- var foldNode = foldPort.connections.FirstOrDefault()?.output?.node as IFoldNode;
- if (foldNode == null) { graphView.ShowNotification("Fold input not connected."); return; }
-
- var stack = new Stack<IFoldNode>();
- for (var cur = foldNode; cur != null; cur = cur.GetPreviousFold())
- stack.Push(cur);
-
- Undo.RecordObject(renderer, "Generate Keyframe");
- var mat = renderer.material;
-
- for (int i = 0; i < 16; i++)
- {
- var prefix = $"_Vertex_Deformation_Slot_{i}_";
- var node = stack.Count > 0 ? stack.Pop() : null;
- var s = node?.Serialize();
- bool on = s != null;
-
- mat.SetFloat(prefix + "Enabled", on ? 1f : 0f);
- mat.SetInt(prefix + "Opcode", on ? s.opcode : 0);
- mat.SetFloat(prefix + "Float_0", on ? s.float0 : 0f);
- mat.SetFloat(prefix + "Float_1", on ? s.float1 : 0f);
- mat.SetFloat(prefix + "Float_2", on ? s.float2 : 0f);
- mat.SetFloat(prefix + "Float_3", on ? s.float3 : 0f);
- mat.SetVector(prefix + "Vector_0", on ? s.vec0 : Vector4.zero);
- mat.SetVector(prefix + "Vector_1", on ? s.vec1 : Vector4.zero);
- mat.SetVector(prefix + "Vector_2", on ? s.vec2 : Vector4.zero);
- mat.SetVector(prefix + "Vector_3", on ? s.vec3 : Vector4.zero);
- }
- EditorUtility.SetDirty(renderer);
- }
-}
-
-public class FoldGraphView : GraphView
-{
- static readonly (string label, Type dataType)[] AllNodeOptions =
- {
- ("Fold/Axis Align", typeof(AxisAlignNodeData)),
- ("Fold/Plane to Tube", typeof(PlaneToTubeNodeData)),
- ("Fold/Point Align", typeof(PointAlignNodeData)),
- ("Fold/Tube to Plane", typeof(TubeToPlaneNodeData)),
- ("Value/Float", typeof(FloatValueNodeData)),
- ("Value/Vector", typeof(VectorValueNodeData)),
- ("Value/GameObject", typeof(GameObjectNodeData)),
- ("Output/Keyframe", typeof(KeyframeNodeData)),
- };
-
- readonly FoldWindow window;
- public readonly FoldGraph graphAsset;
- readonly Dictionary<string, FoldNodeView> nodeLookup = new();
- readonly FoldEdgeConnectorListener edgeConnectorListener;
- bool suppressGraphChanges;
- readonly HashSet<string> pendingAutoComplete = new();
-
- void WithGraphChangesSuppressed(Action action)
- {
- var previous = suppressGraphChanges;
- suppressGraphChanges = true;
- try { action(); }
- finally { suppressGraphChanges = previous; }
- }
-
- 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());
-
- var grid = new GridBackground();
- Insert(0, grid);
- grid.StretchToParentSize();
-
- graphViewChanged = OnGraphViewChanged;
- nodeCreationRequest = ctx => ShowSearchWindow(null, ctx.screenMousePosition);
- RegisterCallback<KeyDownEvent>(OnKeyDown);
- RegisterCallback<ContextualMenuPopulateEvent>(evt => { evt.menu.ClearItems(); evt.StopImmediatePropagation(); evt.PreventDefault(); ShowSearchWindow(null, evt.mousePosition); });
- Undo.undoRedoPerformed += OnUndoRedo;
-
- Reload();
- }
-
- public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter) =>
- ports.Where(p => p.node != startPort.node && p.direction != startPort.direction &&
- (startPort.portType == null || p.portType == null || startPort.portType.IsAssignableFrom(p.portType) || p.portType.IsAssignableFrom(startPort.portType))).ToList();
-
- internal void BindEdgeConnector(Port port) => port.AddManipulator(new EdgeConnector<Edge>(edgeConnectorListener));
-
- GraphViewChange OnGraphViewChanged(GraphViewChange change)
- {
- if (suppressGraphChanges) return change;
- change.elementsToRemove?.ForEach(e => { if (e is Edge edge) RemoveEdgeData(edge); else if (e is FoldNodeView node) RemoveNode(node); });
- change.edgesToCreate?.ForEach(AddEdgeData);
- 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 inp || edge.output?.node is not FoldNodeView outp) return;
- RecordUndo("Add Edge");
- graphAsset.edges.Add(new FoldEdge { inputNodeGuid = inp.Data.guid, inputPortName = edge.input.portName, outputNodeGuid = outp.Data.guid, outputPortName = edge.output.portName });
- MarkDirty();
- }
-
- void RemoveEdgeData(Edge edge)
- {
- if (edge.input?.node is not FoldNodeView inp || edge.output?.node is not FoldNodeView outp) return;
- RecordUndo("Remove Edge");
- graphAsset.edges.RemoveAll(e => e.inputNodeGuid == inp.Data.guid && e.outputNodeGuid == outp.Data.guid && e.inputPortName == edge.input.portName && e.outputPortName == edge.output.portName);
- MarkDirty();
- }
-
- public void Reload() => WithGraphChangesSuppressed(() =>
- {
- nodeLookup.Clear();
- DeleteElements(graphElements.Where(e => e is Node or Edge).ToList());
-
- foreach (var d in graphAsset.nodes) { var n = CreateNode(d); nodeLookup[d.guid] = n; AddElement(n); }
- foreach (var e in graphAsset.edges)
- {
- if (nodeLookup.TryGetValue(e.outputNodeGuid, out var o) && nodeLookup.TryGetValue(e.inputNodeGuid, out var i))
- if (o.GetPort(e.outputPortName) is { } op && i.GetPort(e.inputPortName) is { } ip)
- AddElement(op.ConnectTo(ip));
- }
- });
-
- void OnKeyDown(KeyDownEvent evt)
- {
- if (evt.keyCode == KeyCode.D && (evt.commandKey || evt.ctrlKey)) { evt.StopImmediatePropagation(); DuplicateSelectedNodes(); }
- }
-
- void DuplicateSelectedNodes()
- {
- var sel = selection.OfType<FoldNodeView>().ToList();
- if (sel.Count == 0) return;
-
- RecordUndo("Duplicate Nodes");
-
- var map = new Dictionary<string, FoldNodeView>();
- WithGraphChangesSuppressed(() =>
- {
- foreach (var orig in sel)
- {
- var clone = (FoldNodeData)Activator.CreateInstance(orig.Data.GetType());
- JsonUtility.FromJsonOverwrite(JsonUtility.ToJson(orig.Data), clone);
- clone.guid = Guid.NewGuid().ToString();
- clone.position += new Vector2(30f, 30f);
- graphAsset.nodes.Add(clone);
- var v = CreateNode(clone);
- map[orig.Data.guid] = v;
- nodeLookup[clone.guid] = v;
- AddElement(v);
- }
-
- foreach (var e in graphAsset.edges.Where(e => map.ContainsKey(e.outputNodeGuid) && map.ContainsKey(e.inputNodeGuid)))
- if (map[e.outputNodeGuid].GetPort(e.outputPortName) is { } op && map[e.inputNodeGuid].GetPort(e.inputPortName) is { } ip)
- { var edge = op.ConnectTo(ip); AddElement(edge); AddEdgeData(edge); }
- });
-
- ClearSelection();
- foreach (var n in map.Values) AddToSelection(n);
- MarkDirty();
- }
-
- FoldNodeView CreateNode(FoldNodeData data)
- {
- FoldNodeView view = data switch
- {
- FloatValueNodeData or VectorValueNodeData or GameObjectNodeData => new ValueNodeView(),
- FoldOperationData => new OperationNodeView(),
- KeyframeNodeData => new KeyframeNodeView(),
- _ => throw new ArgumentOutOfRangeException(nameof(data))
- };
- view.Initialize(this, data);
- return view;
- }
-
- internal FoldNodeView CreateNode(Type dataType, Vector2 position, bool autoComplete = false)
- {
- if (!suppressGraphChanges) 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);
- view.RefreshExpandedState();
- view.RefreshPorts();
- if (autoComplete) RegisterGeometryOnce(view, () => AutoCreateSingleOptionNodes(view));
- MarkDirty();
- return view;
- }
-
- public void MarkDirty() => EditorUtility.SetDirty(graphAsset);
- public void ShowNotification(string msg) => window.ShowNotification(msg);
- public void ShowNodeCreationMenuFromPort(Port port, Vector2 pos) => ShowSearchWindow(port, pos);
- public void Dispose() => Undo.undoRedoPerformed -= OnUndoRedo;
- void OnUndoRedo() => Reload();
- internal void RecordUndo(string label) { if (!suppressGraphChanges) Undo.RegisterCompleteObjectUndo(graphAsset, label); }
-
- public FoldNodeView CreateNodeAndConnect(Port startPort, Type dataType, Vector2 pos)
- {
- RecordUndo("Create Node");
- FoldNodeView view = null;
- WithGraphChangesSuppressed(() =>
- {
- view = CreateNode(dataType, pos);
-
- // If dragging from input port, offset so the new node's right-center is at cursor once layout is computed
- if (startPort.direction == Direction.Input)
- RegisterGeometryOnce(view, () =>
- {
- var size = view.layout.size;
- view.SetPosition(new Rect(pos - new Vector2(size.x, size.y / 2), size));
- });
-
- var compat = GetCompatiblePorts(startPort, null).FirstOrDefault(p => p.node == view);
- if (compat != null)
- {
- var (o, i) = startPort.direction == Direction.Output ? (startPort, compat) : (compat, startPort);
- var edge = o.ConnectTo(i);
- AddElement(edge);
- AddEdgeData(edge);
- }
- });
- RegisterGeometryOnce(view, () => AutoCreateSingleOptionNodes(view));
- MarkDirty();
- return view;
- }
-
- internal IEnumerable<(string label, Type dataType)> GetNodeOptions(Port port)
- {
- if (port == null) return AllNodeOptions;
- var t = port.portType;
- bool @out = port.direction == Direction.Output;
- return AllNodeOptions.Where(o =>
- (t == typeof(float) || t == typeof(Vector4)) ? (@out ? typeof(FoldOperationData).IsAssignableFrom(o.dataType) : (t == typeof(float) ? o.dataType == typeof(FloatValueNodeData) : o.dataType == typeof(VectorValueNodeData))) :
- t == typeof(GameObject) ? (@out ? o.dataType == typeof(KeyframeNodeData) : o.dataType == typeof(GameObjectNodeData)) :
- typeof(IFoldNode).IsAssignableFrom(t) && (typeof(FoldOperationData).IsAssignableFrom(o.dataType) || (@out && o.dataType == typeof(KeyframeNodeData))));
- }
-
- public T GetInputValue<T>(string guid, string port, T fallback) =>
- nodeLookup.TryGetValue(guid, out var n) && n.GetPort(port)?.connections.FirstOrDefault()?.output?.node is ValueNodeView v ? v.GetTypedValue<T>() : fallback;
-
- void ShowSearchWindow(Port port, Vector2 pos)
- {
- var p = ScriptableObject.CreateInstance<FoldSearchProvider>();
- p.Initialize(this, port, pos);
- if (p.HasEntries()) SearchWindow.Open(new SearchWindowContext(GUIUtility.GUIToScreenPoint(pos)), p);
- }
-
- void AutoCreateSingleOptionNodes(FoldNodeView node)
- {
- if (node == null) return;
- pendingAutoComplete.Remove(node.Data.guid);
- var debug = new List<string>();
- var targetRect = EstimateNodeRect(node);
- if (targetRect.size == Vector2.zero)
- {
- if (pendingAutoComplete.Add(node.Data.guid))
- RegisterGeometryOnce(node, () => AutoCreateSingleOptionNodes(node));
- Debug.Log("Fold AutoComplete: defer (no size); layout=" + node.layout.size + " resolved=(" + node.resolvedStyle.width + "," + node.resolvedStyle.height + ")");
- return;
- }
-
- var ports = node.inputContainer.Children().OfType<Port>()
- .Where(p => !p.connections.Any())
- .Select((p, i) => (port: p, index: i))
- .ToList();
-
- var candidates = ports
- .Select(p => (p.port, p.index, options: GetNodeOptions(p.port).ToList()))
- .Where(p => p.options.Count == 1)
- .ToList();
-
- if (candidates.Count == 0) return;
-
- var created = new List<FoldNodeView>();
- var horizontalSpacing = targetRect.width + GetHorizontalMargins(node);
- var verticalSpacing = targetRect.height;
- var occupied = new List<Rect> { targetRect };
-
- RecordUndo("Auto-complete Ports");
- WithGraphChangesSuppressed(() =>
- {
- foreach (var (port, index, options) in candidates)
- {
- var dataType = options[0].dataType;
- var targetPos = targetRect.position;
- var desired = new Vector2(
- targetPos.x - horizontalSpacing,
- targetPos.y + (index - (candidates.Count - 1) * 0.5f) * verticalSpacing);
-
- var rect = FindFreePosition(new Rect(desired, targetRect.size), occupied, debug);
- if (rect == Rect.zero)
- {
- debug.Add($"fail place idx={index} type={dataType.Name} desired={desired}");
- continue;
- }
-
- var newNode = CreateNode(dataType, rect.position);
- created.Add(newNode);
-
- var compat = GetCompatiblePorts(port, null).FirstOrDefault(p => p.node == newNode);
- if (compat != null)
- {
- var (o, i) = port.direction == Direction.Output ? (port, compat) : (compat, port);
- var edge = o.ConnectTo(i);
- AddElement(edge);
- AddEdgeData(edge);
- }
- }
- });
-
- foreach (var child in created)
- AutoCreateSingleOptionNodes(child);
-
- Debug.Log($"Fold AutoComplete: node='{node.title}' size={targetRect.size} spacing=({horizontalSpacing},{verticalSpacing}) candidates={candidates.Count} created={created.Count} notes=[{string.Join("; ", debug)}]");
- MarkDirty();
- }
-
- Rect FindFreePosition(Rect desired, List<Rect> occupied, List<string> debug)
- {
- if (desired.size == Vector2.zero) return Rect.zero;
-
- Rect rect = desired;
- int safety = 0;
- while ((occupied?.Any(o => RectsOverlap(o, rect)) == true || OverlapsExisting(rect)) && safety++ < 50)
- rect.position += new Vector2(0f, rect.height);
- if (safety >= 50)
- {
- debug?.Add($"exceeded iterations({safety}) starting={desired.position}");
- return Rect.zero;
- }
- occupied?.Add(rect);
- return rect;
- }
-
- bool OverlapsExisting(Rect rect)
- {
- return nodeLookup.Values.Any(n =>
- {
- var existing = EstimateNodeRect(n);
- return existing.size != Vector2.zero && RectsOverlap(existing, rect);
- });
- }
-
- static Rect EstimateNodeRect(FoldNodeView node)
- {
- var size = node.layout.size;
- if (float.IsNaN(size.x) || float.IsNaN(size.y))
- size = Vector2.zero;
- if (size == Vector2.zero)
- {
- var rs = node.resolvedStyle;
- size = new Vector2(float.IsNaN(rs.width) ? 0f : rs.width, float.IsNaN(rs.height) ? 0f : rs.height);
- }
- if (float.IsNaN(size.x) || float.IsNaN(size.y))
- size = Vector2.zero;
- return size == Vector2.zero ? Rect.zero : new Rect(node.GetPosition().position, size);
- }
-
- static float GetHorizontalMargins(FoldNodeView node)
- {
- var rs = node.resolvedStyle;
- float left = float.IsNaN(rs.marginLeft) ? 0f : rs.marginLeft;
- float right = float.IsNaN(rs.marginRight) ? 0f : rs.marginRight;
- return left + right;
- }
-
- static bool RectsOverlap(Rect a, Rect b) =>
- a.xMin < b.xMax && a.xMax > b.xMin && a.yMin < b.yMax && a.yMax > b.yMin;
-
- static void RegisterGeometryOnce(VisualElement element, Action action)
- {
- bool TryInvoke()
- {
- var size = element.layout.size;
- if (float.IsNaN(size.x) || float.IsNaN(size.y) || size == Vector2.zero)
- {
- var rs = element.resolvedStyle;
- size = new Vector2(float.IsNaN(rs.width) ? 0f : rs.width, float.IsNaN(rs.height) ? 0f : rs.height);
- }
-
- if (size == Vector2.zero) return false;
- action?.Invoke();
- return true;
- };
-
- if (TryInvoke()) return;
-
- EventCallback<GeometryChangedEvent> handler = null;
- handler = evt =>
- {
- if (TryInvoke())
- element.UnregisterCallback<GeometryChangedEvent>(handler);
- };
- element.RegisterCallback<GeometryChangedEvent>(handler);
- }
-}
-
-class FoldEdgeConnectorListener : IEdgeConnectorListener
-{
- readonly FoldGraphView graphView;
- public FoldEdgeConnectorListener(FoldGraphView gv) => graphView = gv;
-
- public void OnDropOutsidePort(Edge edge, Vector2 pos) => graphView.ShowNodeCreationMenuFromPort(edge.output ?? edge.input, pos);
-
- 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);
- graphView?.AddEdgeData(edge);
- }
-}
-
-class FoldSearchProvider : ScriptableObject, ISearchWindowProvider
-{
- FoldGraphView graphView;
- Port startPort;
- Vector2 worldPosition;
- List<(string label, Type dataType)> options;
-
- public void Initialize(FoldGraphView gv, Port port, Vector2 pos) { graphView = gv; startPort = port; worldPosition = pos; options = gv.GetNodeOptions(port).ToList(); }
- public bool HasEntries() => options?.Count > 0;
-
- public List<SearchTreeEntry> CreateSearchTree(SearchWindowContext ctx)
- {
- var entries = new List<SearchTreeEntry> { new SearchTreeGroupEntry(new GUIContent("Create Node"), 0) };
- foreach (var grp in options.GroupBy(o => o.label.Split('/')[0]).OrderBy(g => g.Key))
- {
- entries.Add(new SearchTreeGroupEntry(new GUIContent(grp.Key), 1));
- foreach (var opt in grp)
- entries.Add(new SearchTreeEntry(new GUIContent(opt.label.Split('/')[1])) { level = 2, userData = opt.dataType });
- }
- return entries;
- }
-
- public bool OnSelectEntry(SearchTreeEntry entry, SearchWindowContext ctx)
- {
- if (entry.userData is not Type dataType) return false;
- var pos = graphView.contentViewContainer.WorldToLocal(worldPosition);
- if (startPort != null) graphView.CreateNodeAndConnect(startPort, dataType, pos);
- else graphView.CreateNode(dataType, pos, true);
- return true;
- }
-}
diff --git a/Scripts/Fold/Editor/FoldNodeSerialized.cs b/Scripts/Fold/Editor/FoldNodeSerialized.cs
deleted file mode 100644
index 62431ae..0000000
--- a/Scripts/Fold/Editor/FoldNodeSerialized.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using UnityEngine;
-
-public class FoldNodeSerialized
-{
- public int opcode;
-
- // Correspond to _Vertex_Deformation_Float_0, etc.
- public float float0;
- public float float1;
- public float float2;
- public float float3;
-
- // _Vertex_Deformation_Vector_0, etc.
- public Vector4 vec0;
- public Vector4 vec1;
- public Vector4 vec2;
- public Vector4 vec3;
-};
-
diff --git a/Scripts/Fold/Editor/FoldWindow.cs b/Scripts/Fold/Editor/FoldWindow.cs
deleted file mode 100644
index f0f9b42..0000000
--- a/Scripts/Fold/Editor/FoldWindow.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-using UnityEditor;
-using UnityEngine;
-using UnityEngine.UIElements;
-
-public class FoldWindow : EditorWindow
-{
- const string GraphPath = "Assets/FoldGraph.asset";
-
- FoldGraphView graphView;
- FoldGraph graphAsset;
-
- [MenuItem("Tools/yum_food/Fold")]
- public static void Open()
- {
- 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.Dispose();
- graphView = null;
- }
-
- AssetDatabase.SaveAssets();
- }
-
- void OnFocus()
- {
- // If we lost the graph view for any reason, rebuild it. Otherwise, reload to match the asset on disk.
- if (graphView == null)
- {
- graphAsset = LoadOrCreateGraph();
- ConstructGraphView();
- }
- else
- {
- graphView.Reload();
- }
- }
-
- FoldGraph LoadOrCreateGraph()
- {
- var asset = AssetDatabase.LoadAssetAtPath<FoldGraph>(GraphPath);
- if (asset == null)
- {
- asset = CreateInstance<FoldGraph>();
- AssetDatabase.CreateAsset(asset, GraphPath);
- AssetDatabase.SaveAssets();
- }
-
- return asset;
- }
-
- void ConstructGraphView()
- {
- graphView = new FoldGraphView(this, graphAsset)
- {
- name = "Fold 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)
- {
- if (string.IsNullOrEmpty(message))
- return;
-
- ShowNotification(new GUIContent(message));
- }
-}
diff --git a/Scripts/Fold/Editor/README.md b/Scripts/Fold/Editor/README.md
new file mode 100644
index 0000000..8305abf
--- /dev/null
+++ b/Scripts/Fold/Editor/README.md
@@ -0,0 +1,131 @@
+# Vertex Deformation Fluent Builder
+
+A simple, code-first API for building vertex deformation pipelines without visual node editors.
+
+## Quick Start
+
+```csharp
+using UnityEngine;
+
+// Apply to a material
+VertexDeformationBuilder.Create()
+ .For(material)
+ .TubeToPlane(Vector3.zero, Vector3.right, Vector3.forward, 1f)
+ .NormConversion(2f, 1f, 1f)
+ .Apply();
+```
+
+## Available Operations
+
+### TubeToPlane(p, r, s, t)
+Unfolds a tube into a plane.
+- `p`: Origin point (Vector3)
+- `r`: R axis direction (Vector3)
+- `s`: S axis direction (Vector3)
+- `t`: Interpolation factor (float, 0-1)
+
+### PlaneToTube(p, r, s, t)
+Folds a plane into a tube.
+- Same parameters as TubeToPlane
+
+### PointAlign(po, pp, r, t)
+Aligns geometry to a point.
+- `po`: Original point (Vector3)
+- `pp`: Target point (Vector3)
+- `r`: Rotation axis (Vector3)
+- `t`: Interpolation factor (float, 0-1)
+
+### AxisAlign(po, pp, r, t)
+Aligns geometry along an axis.
+- Same parameters as PointAlign
+
+### NormConversion(inputK, outputK, t)
+Converts between different norms (L1, L2, L∞).
+- `inputK`: Input norm (float, e.g., 1 for L1, 2 for L2, ∞ for L∞)
+- `outputK`: Output norm (float)
+- `t`: Interpolation factor (float, 0-1)
+
+Common conversions:
+- Sphere to box: `NormConversion(2f, float.PositiveInfinity, 1f)`
+- Box to sphere: `NormConversion(float.PositiveInfinity, 2f, 1f)`
+- Diamond to sphere: `NormConversion(1f, 2f, 1f)`
+
+### Seal(A, k, st, t)
+Sealing operation for closing geometry.
+- `A`: Amplitude (float)
+- `k`: Smoothness factor (float)
+- `st`: Scale factor (float)
+- `t`: Interpolation factor (float, 0-1)
+
+### SineWaves()
+Applies sine wave deformation.
+
+### FBM()
+Applies fractal Brownian motion deformation.
+
+### Custom(opcode, f0-f3, v0-v3)
+For advanced use cases or custom opcodes.
+
+## Presets
+
+Use built-in presets from anywhere:
+
+```csharp
+VertexDeformationPresets.TubeToPlaneFull(material);
+VertexDeformationPresets.NormConvL2ToL1(material);
+VertexDeformationPresets.NormConvL2ToLinf(material);
+```
+
+## GUI Window
+
+Open `Window > Vertex Deformation Presets` for a GUI with buttons for common operations.
+
+## Adding Custom Presets
+
+1. Edit `VertexDeformationPresets` class:
+
+```csharp
+public static void MyCustomEffect(Material mat) =>
+ VertexDeformationBuilder.Create()
+ .For(mat)
+ .TubeToPlane(Vector3.zero, Vector3.right, Vector3.forward, 1f)
+ .NormConversion(2f, 1f, 0.5f)
+ .Seal(0.1f, 2f, 0.8f, 1f)
+ .Apply();
+```
+
+2. Add button in `VertexDeformationEditorWindow`:
+
+```csharp
+if (GUILayout.Button("My Custom Effect"))
+ VertexDeformationPresets.MyCustomEffect(targetMaterial);
+```
+
+## Pipeline Chaining
+
+Operations are applied in the order they're chained:
+
+```csharp
+VertexDeformationBuilder.Create()
+ .For(material)
+ .TubeToPlane(...) // Step 1
+ .NormConversion(...) // Step 2
+ .PointAlign(...) // Step 3
+ .Seal(...) // Step 4
+ .Apply(); // Execute
+```
+
+The shader supports up to 16 operations per material.
+
+## Clearing Deformations
+
+```csharp
+// Via GUI: Use "Clear All Deformations" button
+
+// Via code:
+targetMaterial.SetFloat("_Vertex_Deformation_Enabled", 0f);
+```
+
+## Examples
+
+See `VertexDeformationExamples.cs` for menu items under `Tools > Vertex Deformation > Example: ...`
diff --git a/Scripts/Fold/Editor/VertexDeformationBuilder.cs b/Scripts/Fold/Editor/VertexDeformationBuilder.cs
new file mode 100644
index 0000000..c04a66a
--- /dev/null
+++ b/Scripts/Fold/Editor/VertexDeformationBuilder.cs
@@ -0,0 +1,339 @@
+using System;
+using System.Collections.Generic;
+using UnityEditor;
+using UnityEngine;
+
+public class VertexDeformationSlot
+{
+ public int opcode;
+ public float float0, float1, float2, float3;
+ public Vector4 vec0, vec1, vec2, vec3;
+
+ public void ApplyToMaterial(Material mat, int slotIndex)
+ {
+ var prefix = $"_Vertex_Deformation_Slot_{slotIndex}_";
+ mat.SetFloat(prefix + "Enabled", 1f);
+ mat.SetInt(prefix + "Opcode", opcode);
+ mat.SetFloat(prefix + "Float_0", float0);
+ mat.SetFloat(prefix + "Float_1", float1);
+ mat.SetFloat(prefix + "Float_2", float2);
+ mat.SetFloat(prefix + "Float_3", float3);
+ mat.SetVector(prefix + "Vector_0", vec0);
+ mat.SetVector(prefix + "Vector_1", vec1);
+ mat.SetVector(prefix + "Vector_2", vec2);
+ mat.SetVector(prefix + "Vector_3", vec3);
+ }
+}
+
+public class VertexDeformationBuilder
+{
+ readonly List<VertexDeformationSlot> slots = new();
+ Material targetMaterial;
+
+ public static class Opcodes
+ {
+ public const int None = 0;
+ public const int TubeToPlane = 1;
+ public const int PlaneToTube = 2;
+ public const int PointAlign = 3;
+ public const int AxisAlign = 4;
+ public const int NormConversion = 5;
+ public const int Seal = 6;
+ public const int SineWaves = 7;
+ public const int FBM = 8;
+ }
+
+ VertexDeformationBuilder() { }
+
+ public static VertexDeformationBuilder Create() => new();
+
+ public VertexDeformationBuilder For(Material material)
+ {
+ targetMaterial = material;
+ return this;
+ }
+
+ public VertexDeformationBuilder TubeToPlane(Vector3 p, Vector3 r, Vector3 s, float t)
+ {
+ slots.Add(new VertexDeformationSlot
+ {
+ opcode = Opcodes.TubeToPlane,
+ vec0 = p,
+ vec1 = r,
+ vec2 = s,
+ float0 = t
+ });
+ return this;
+ }
+
+ public VertexDeformationBuilder PlaneToTube(Vector3 p, Vector3 r, Vector3 s, float t)
+ {
+ slots.Add(new VertexDeformationSlot
+ {
+ opcode = Opcodes.PlaneToTube,
+ vec0 = p,
+ vec1 = r,
+ vec2 = s,
+ float0 = t
+ });
+ return this;
+ }
+
+ public VertexDeformationBuilder PointAlign(Vector3 po, Vector3 pp, Vector3 r, float t)
+ {
+ slots.Add(new VertexDeformationSlot
+ {
+ opcode = Opcodes.PointAlign,
+ vec0 = po,
+ vec1 = pp,
+ vec2 = r,
+ float0 = t
+ });
+ return this;
+ }
+
+ public VertexDeformationBuilder AxisAlign(Vector3 po, Vector3 pp, Vector3 r, float t)
+ {
+ slots.Add(new VertexDeformationSlot
+ {
+ opcode = Opcodes.AxisAlign,
+ vec0 = po,
+ vec1 = pp,
+ vec2 = r,
+ float0 = t
+ });
+ return this;
+ }
+
+ public VertexDeformationBuilder NormConversion(float inputK, float outputK, float t)
+ {
+ slots.Add(new VertexDeformationSlot
+ {
+ opcode = Opcodes.NormConversion,
+ float0 = inputK,
+ float1 = outputK,
+ float2 = t
+ });
+ return this;
+ }
+
+ public VertexDeformationBuilder Seal(float A, float k, float st, float t)
+ {
+ slots.Add(new VertexDeformationSlot
+ {
+ opcode = Opcodes.Seal,
+ float0 = A,
+ float1 = k,
+ float2 = st,
+ float3 = t
+ });
+ return this;
+ }
+
+ public VertexDeformationBuilder SineWaves()
+ {
+ slots.Add(new VertexDeformationSlot { opcode = Opcodes.SineWaves });
+ return this;
+ }
+
+ public VertexDeformationBuilder FBM()
+ {
+ slots.Add(new VertexDeformationSlot { opcode = Opcodes.FBM });
+ return this;
+ }
+
+ public VertexDeformationBuilder Custom(int opcode,
+ float f0 = 0, float f1 = 0, float f2 = 0, float f3 = 0,
+ Vector4? v0 = null, Vector4? v1 = null, Vector4? v2 = null, Vector4? v3 = null)
+ {
+ slots.Add(new VertexDeformationSlot
+ {
+ opcode = opcode,
+ float0 = f0, float1 = f1, float2 = f2, float3 = f3,
+ vec0 = v0 ?? Vector4.zero,
+ vec1 = v1 ?? Vector4.zero,
+ vec2 = v2 ?? Vector4.zero,
+ vec3 = v3 ?? Vector4.zero
+ });
+ return this;
+ }
+
+ public void Apply()
+ {
+ if (targetMaterial == null)
+ {
+ Debug.LogError("No target material set. Use .For(material) before .Apply()");
+ return;
+ }
+
+ if (slots.Count > 16)
+ {
+ Debug.LogWarning($"Too many operations ({slots.Count}). Only the first 16 will be applied.");
+ }
+
+ Undo.RecordObject(targetMaterial, "Apply Vertex Deformation");
+
+ targetMaterial.SetFloat("_Vertex_Deformation_Enabled", 1f);
+
+ for (int i = 0; i < 16; i++)
+ {
+ if (i < slots.Count)
+ {
+ slots[i].ApplyToMaterial(targetMaterial, i);
+ }
+ else
+ {
+ var prefix = $"_Vertex_Deformation_Slot_{i}_";
+ targetMaterial.SetFloat(prefix + "Enabled", 0f);
+ }
+ }
+
+ EditorUtility.SetDirty(targetMaterial);
+ }
+
+ public void Clear()
+ {
+ slots.Clear();
+ }
+
+ public int Count => slots.Count;
+}
+
+public static class VertexDeformationPresets
+{
+ public static void TubeToPlaneFull(Material mat) =>
+ VertexDeformationBuilder.Create()
+ .For(mat)
+ .TubeToPlane(Vector3.zero, Vector3.right, Vector3.forward, 1f)
+ .Apply();
+
+ public static void PlaneToTubeFull(Material mat) =>
+ VertexDeformationBuilder.Create()
+ .For(mat)
+ .PlaneToTube(Vector3.zero, Vector3.right, Vector3.forward, 1f)
+ .Apply();
+
+ public static void TubeToPlaneThenNormConv(Material mat) =>
+ VertexDeformationBuilder.Create()
+ .For(mat)
+ .TubeToPlane(Vector3.zero, Vector3.right, Vector3.forward, 1f)
+ .NormConversion(2f, 1f, 1f)
+ .Apply();
+
+ public static void NormConvL1ToL2(Material mat) =>
+ VertexDeformationBuilder.Create()
+ .For(mat)
+ .NormConversion(1f, 2f, 1f)
+ .Apply();
+
+ public static void NormConvL2ToL1(Material mat) =>
+ VertexDeformationBuilder.Create()
+ .For(mat)
+ .NormConversion(2f, 1f, 1f)
+ .Apply();
+
+ public static void NormConvL2ToLinf(Material mat) =>
+ VertexDeformationBuilder.Create()
+ .For(mat)
+ .NormConversion(2f, float.PositiveInfinity, 1f)
+ .Apply();
+}
+
+public class VertexDeformationEditorWindow : EditorWindow
+{
+ Material targetMaterial;
+ Vector2 scrollPos;
+
+ [MenuItem("Window/Vertex Deformation Presets")]
+ static void ShowWindow()
+ {
+ GetWindow<VertexDeformationEditorWindow>("Vertex Deformation");
+ }
+
+ void OnGUI()
+ {
+ GUILayout.Label("Vertex Deformation Builder", EditorStyles.boldLabel);
+
+ targetMaterial = EditorGUILayout.ObjectField("Target Material", targetMaterial, typeof(Material), false) as Material;
+
+ if (targetMaterial == null)
+ {
+ EditorGUILayout.HelpBox("Select a material to apply vertex deformations", MessageType.Info);
+ return;
+ }
+
+ scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
+
+ DrawSection("Basic Operations", () =>
+ {
+ if (GUILayout.Button("Tube to Plane"))
+ VertexDeformationPresets.TubeToPlaneFull(targetMaterial);
+
+ if (GUILayout.Button("Plane to Tube"))
+ VertexDeformationPresets.PlaneToTubeFull(targetMaterial);
+ });
+
+ DrawSection("Norm Conversions", () =>
+ {
+ if (GUILayout.Button("L1 to L2"))
+ VertexDeformationPresets.NormConvL1ToL2(targetMaterial);
+
+ if (GUILayout.Button("L2 to L1"))
+ VertexDeformationPresets.NormConvL2ToL1(targetMaterial);
+
+ if (GUILayout.Button("L2 to L∞"))
+ VertexDeformationPresets.NormConvL2ToLinf(targetMaterial);
+ });
+
+ DrawSection("Complex Pipelines", () =>
+ {
+ if (GUILayout.Button("Tube to Plane + Norm Conv (L2→L1)"))
+ VertexDeformationPresets.TubeToPlaneThenNormConv(targetMaterial);
+ });
+
+ DrawSection("Utilities", () =>
+ {
+ if (GUILayout.Button("Clear All Deformations"))
+ {
+ Undo.RecordObject(targetMaterial, "Clear Vertex Deformations");
+ targetMaterial.SetFloat("_Vertex_Deformation_Enabled", 0f);
+ for (int i = 0; i < 16; i++)
+ {
+ var prefix = $"_Vertex_Deformation_Slot_{i}_";
+ targetMaterial.SetFloat(prefix + "Enabled", 0f);
+ }
+ EditorUtility.SetDirty(targetMaterial);
+ }
+
+ if (GUILayout.Button("Show Code Example"))
+ {
+ ShowCodeExample();
+ }
+ });
+
+ EditorGUILayout.EndScrollView();
+ }
+
+ void DrawSection(string title, Action content)
+ {
+ GUILayout.Space(10);
+ GUILayout.Label(title, EditorStyles.boldLabel);
+ content();
+ }
+
+ void ShowCodeExample()
+ {
+ EditorUtility.DisplayDialog("Code Example",
+ "// Quick preset:\n" +
+ "VertexDeformationPresets.TubeToPlaneFull(material);\n\n" +
+ "// Custom pipeline:\n" +
+ "VertexDeformationBuilder.Create()\n" +
+ " .For(material)\n" +
+ " .TubeToPlane(Vector3.zero, Vector3.right, Vector3.forward, 1f)\n" +
+ " .NormConversion(2f, 1f, 1f)\n" +
+ " .PlaneToTube(Vector3.zero, Vector3.right, Vector3.forward, 1f)\n" +
+ " .Apply();\n\n" +
+ "// Add your own presets in VertexDeformationPresets class!",
+ "OK");
+ }
+}
diff --git a/Scripts/Fold/Editor/VertexDeformationExamples.cs b/Scripts/Fold/Editor/VertexDeformationExamples.cs
new file mode 100644
index 0000000..705cea9
--- /dev/null
+++ b/Scripts/Fold/Editor/VertexDeformationExamples.cs
@@ -0,0 +1,114 @@
+using UnityEditor;
+using UnityEngine;
+
+public static class VertexDeformationExamples
+{
+ [MenuItem("Tools/Vertex Deformation/Example: Tube to Plane")]
+ static void Example_TubeToPlane()
+ {
+ var mat = Selection.activeObject as Material;
+ if (mat == null)
+ {
+ EditorUtility.DisplayDialog("Error", "Select a material in the Project window", "OK");
+ return;
+ }
+
+ VertexDeformationBuilder.Create()
+ .For(mat)
+ .TubeToPlane(
+ p: Vector3.zero,
+ r: Vector3.right,
+ s: Vector3.forward,
+ t: 1f)
+ .Apply();
+ }
+
+ [MenuItem("Tools/Vertex Deformation/Example: Complex Pipeline")]
+ static void Example_ComplexPipeline()
+ {
+ var mat = Selection.activeObject as Material;
+ if (mat == null)
+ {
+ EditorUtility.DisplayDialog("Error", "Select a material in the Project window", "OK");
+ return;
+ }
+
+ VertexDeformationBuilder.Create()
+ .For(mat)
+ .TubeToPlane(Vector3.zero, Vector3.right, Vector3.forward, 1f)
+ .NormConversion(inputK: 2f, outputK: 1f, t: 1f)
+ .PointAlign(
+ po: new Vector3(0, 0, 0),
+ pp: new Vector3(0, 1, 0),
+ r: Vector3.right,
+ t: 0.5f)
+ .Apply();
+
+ Debug.Log("Applied 3-step deformation pipeline to " + mat.name);
+ }
+
+ [MenuItem("Tools/Vertex Deformation/Example: Add Custom Preset")]
+ static void Example_ShowHowToAddPreset()
+ {
+ EditorUtility.DisplayDialog("How to Add Presets",
+ "1. Open VertexDeformationPresets class\n\n" +
+ "2. Add a new static method:\n\n" +
+ "public static void MyCustomPreset(Material mat) =>\n" +
+ " VertexDeformationBuilder.Create()\n" +
+ " .For(mat)\n" +
+ " .TubeToPlane(...)\n" +
+ " .NormConversion(...)\n" +
+ " .Apply();\n\n" +
+ "3. Add a button in VertexDeformationEditorWindow:\n\n" +
+ "if (GUILayout.Button(\"My Custom Preset\"))\n" +
+ " VertexDeformationPresets.MyCustomPreset(targetMaterial);",
+ "OK");
+ }
+
+ [MenuItem("Tools/Vertex Deformation/Example: All Opcodes")]
+ static void Example_AllOpcodes()
+ {
+ EditorUtility.DisplayDialog("All Available Operations",
+ "TubeToPlane(p, r, s, t)\n" +
+ "PlaneToTube(p, r, s, t)\n" +
+ "PointAlign(po, pp, r, t)\n" +
+ "AxisAlign(po, pp, r, t)\n" +
+ "NormConversion(inputK, outputK, t)\n" +
+ "Seal(A, k, st, t)\n" +
+ "SineWaves()\n" +
+ "FBM()\n\n" +
+ "Custom(opcode, f0, f1, f2, f3, v0, v1, v2, v3)\n\n" +
+ "Chain them with method calls:\n" +
+ ".TubeToPlane(...).NormConversion(...).Apply()",
+ "OK");
+ }
+
+ public static class CustomPresetIdeas
+ {
+ public static void CylinderToPlane(Material mat) =>
+ VertexDeformationBuilder.Create()
+ .For(mat)
+ .TubeToPlane(Vector3.zero, Vector3.up, Vector3.right, 1f)
+ .Apply();
+
+ public static void SphereToBox(Material mat) =>
+ VertexDeformationBuilder.Create()
+ .For(mat)
+ .NormConversion(inputK: 2f, outputK: float.PositiveInfinity, t: 1f)
+ .Apply();
+
+ public static void BoxToSphere(Material mat) =>
+ VertexDeformationBuilder.Create()
+ .For(mat)
+ .NormConversion(inputK: float.PositiveInfinity, outputK: 2f, t: 1f)
+ .Apply();
+
+ public static void TubeUnfoldAndFlatten(Material mat) =>
+ VertexDeformationBuilder.Create()
+ .For(mat)
+ .TubeToPlane(Vector3.zero, Vector3.right, Vector3.forward, 1f)
+ .NormConversion(inputK: 2f, outputK: 1f, t: 0.5f)
+ .Seal(A: 0.1f, k: 2f, st: 0.8f, t: 1f)
+ .Apply();
+ }
+}