summaryrefslogtreecommitdiffstats
path: root/Scripts
diff options
context:
space:
mode:
authoryum <yum.food.vr@gmail.com>2026-01-08 18:39:56 -0800
committeryum <yum.food.vr@gmail.com>2026-01-08 18:39:56 -0800
commit2f52d06c01682fe421af9ffd227aad0080c1f138 (patch)
tree7ea311510349d38460f7c315dd210f71fa3c0d91 /Scripts
parent317f8527bc1c0aedb234ecae331aeec18aeaf9bd (diff)
Fold: fuck
Diffstat (limited to 'Scripts')
-rw-r--r--Scripts/Fold/Editor/FoldGraph.cs107
-rw-r--r--Scripts/Fold/Editor/FoldGraphView.cs239
2 files changed, 297 insertions, 49 deletions
diff --git a/Scripts/Fold/Editor/FoldGraph.cs b/Scripts/Fold/Editor/FoldGraph.cs
index 5782dc7..c0719d9 100644
--- a/Scripts/Fold/Editor/FoldGraph.cs
+++ b/Scripts/Fold/Editor/FoldGraph.cs
@@ -9,6 +9,59 @@ public class FoldEdge
}
[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();
@@ -19,8 +72,10 @@ public abstract class FoldNodeData
[Serializable]
public abstract class FoldOperationData : FoldNodeData
{
- public abstract IEnumerable<(string name, Type type)> GetInputPorts();
- public abstract FoldNodeSerialized Serialize(FoldGraphView graphView);
+ 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"; }
@@ -31,33 +86,57 @@ public abstract class FoldOperationData : FoldNodeData
[Serializable]
public class AxisAlignNodeData : FoldOperationData
{
- public override string Title => "Axis Align";
- public override IEnumerable<(string, Type)> GetInputPorts() { yield return ("po", typeof(Vector4)); yield return ("pp", typeof(Vector4)); yield return ("r", typeof(Vector4)); yield return ("t", typeof(float)); }
- public override FoldNodeSerialized Serialize(FoldGraphView gv) => new() { opcode = 4, float0 = gv.GetInputValue(guid, "t", 0f), vec0 = gv.GetInputValue(guid, "po", Vector4.zero), vec1 = gv.GetInputValue(guid, "pp", Vector4.zero), vec2 = gv.GetInputValue(guid, "r", Vector4.zero) };
+ 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
{
- public override string Title => "Plane to Tube";
- public override IEnumerable<(string, Type)> GetInputPorts() { yield return ("p", typeof(Vector4)); yield return ("r", typeof(Vector4)); yield return ("s", typeof(Vector4)); yield return ("t", typeof(float)); }
- public override FoldNodeSerialized Serialize(FoldGraphView gv) => new() { opcode = 2, float0 = gv.GetInputValue(guid, "t", 0f), vec0 = gv.GetInputValue(guid, "p", Vector4.zero), vec1 = gv.GetInputValue(guid, "r", Vector4.zero), vec2 = gv.GetInputValue(guid, "s", Vector4.zero) };
+ 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
{
- public override string Title => "Point Align";
- public override IEnumerable<(string, Type)> GetInputPorts() { yield return ("po", typeof(Vector4)); yield return ("pp", typeof(Vector4)); yield return ("r", typeof(Vector4)); yield return ("t", typeof(float)); }
- public override FoldNodeSerialized Serialize(FoldGraphView gv) => new() { opcode = 3, float0 = gv.GetInputValue(guid, "t", 0f), vec0 = gv.GetInputValue(guid, "po", Vector4.zero), vec1 = gv.GetInputValue(guid, "pp", Vector4.zero), vec2 = gv.GetInputValue(guid, "r", Vector4.zero) };
+ 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
{
- public override string Title => "Tube to Plane";
- public override IEnumerable<(string, Type)> GetInputPorts() { yield return ("p", typeof(Vector4)); yield return ("r", typeof(Vector4)); yield return ("s", typeof(Vector4)); yield return ("t", typeof(float)); }
- public override FoldNodeSerialized Serialize(FoldGraphView gv) => new() { opcode = 1, float0 = gv.GetInputValue(guid, "t", 0f), vec0 = gv.GetInputValue(guid, "p", Vector4.zero), vec1 = gv.GetInputValue(guid, "r", Vector4.zero), vec2 = gv.GetInputValue(guid, "s", Vector4.zero) };
+ 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
diff --git a/Scripts/Fold/Editor/FoldGraphView.cs b/Scripts/Fold/Editor/FoldGraphView.cs
index 2c9e933..e01bf4f 100644
--- a/Scripts/Fold/Editor/FoldGraphView.cs
+++ b/Scripts/Fold/Editor/FoldGraphView.cs
@@ -210,6 +210,15 @@ public class FoldGraphView : GraphView
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)
{
@@ -275,9 +284,8 @@ public class FoldGraphView : GraphView
MarkDirty();
}
- public void Reload()
+ public void Reload() => WithGraphChangesSuppressed(() =>
{
- suppressGraphChanges = true;
nodeLookup.Clear();
DeleteElements(graphElements.Where(e => e is Node or Edge).ToList());
@@ -288,8 +296,7 @@ public class FoldGraphView : GraphView
if (o.GetPort(e.outputPortName) is { } op && i.GetPort(e.inputPortName) is { } ip)
AddElement(op.ConnectTo(ip));
}
- suppressGraphChanges = false;
- }
+ });
void OnKeyDown(KeyDownEvent evt)
{
@@ -301,29 +308,29 @@ public class FoldGraphView : GraphView
var sel = selection.OfType<FoldNodeView>().ToList();
if (sel.Count == 0) return;
- suppressGraphChanges = true;
RecordUndo("Duplicate Nodes");
var map = new Dictionary<string, FoldNodeView>();
- foreach (var orig in sel)
+ WithGraphChangesSuppressed(() =>
{
- var clone = JsonUtility.FromJson<FoldNodeData>(JsonUtility.ToJson(orig.Data));
- 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); }
+ 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); }
+ });
- suppressGraphChanges = false;
ClearSelection();
foreach (var n in map.Values) AddToSelection(n);
MarkDirty();
@@ -342,7 +349,7 @@ public class FoldGraphView : GraphView
return view;
}
- internal FoldNodeView CreateNode(Type dataType, Vector2 position)
+ internal FoldNodeView CreateNode(Type dataType, Vector2 position, bool autoComplete = false)
{
if (!suppressGraphChanges) RecordUndo("Create Node");
var data = (FoldNodeData)Activator.CreateInstance(dataType);
@@ -353,6 +360,7 @@ public class FoldGraphView : GraphView
AddElement(view);
view.RefreshExpandedState();
view.RefreshPorts();
+ if (autoComplete) RegisterGeometryOnce(view, () => AutoCreateSingleOptionNodes(view));
MarkDirty();
return view;
}
@@ -366,17 +374,30 @@ public class FoldGraphView : GraphView
public FoldNodeView CreateNodeAndConnect(Port startPort, Type dataType, Vector2 pos)
{
- suppressGraphChanges = true;
- var view = CreateNode(dataType, pos);
- var compat = GetCompatiblePorts(startPort, null).FirstOrDefault(p => p.node == view);
- if (compat != null)
+ RecordUndo("Create Node");
+ FoldNodeView view = null;
+ WithGraphChangesSuppressed(() =>
{
- var (o, i) = startPort.direction == Direction.Output ? (startPort, compat) : (compat, startPort);
- var edge = o.ConnectTo(i);
- AddElement(edge);
- AddEdgeData(edge);
- }
- suppressGraphChanges = false;
+ 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;
}
@@ -401,6 +422,155 @@ public class FoldGraphView : GraphView
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
@@ -447,8 +617,7 @@ class FoldSearchProvider : ScriptableObject, ISearchWindowProvider
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);
+ else graphView.CreateNode(dataType, pos, true);
return true;
}
}
-