summaryrefslogtreecommitdiffstats
path: root/Scripts/Fold/Editor/FoldGraphView.cs
diff options
context:
space:
mode:
Diffstat (limited to 'Scripts/Fold/Editor/FoldGraphView.cs')
-rw-r--r--Scripts/Fold/Editor/FoldGraphView.cs239
1 files changed, 204 insertions, 35 deletions
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;
}
}
-