Files
ichni_Creator_Studio/Assets/Scripts/Editor Tools/NodeScript/NodeManager.cs

1179 lines
48 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Ichni.RhythmGame;
using TMPro;
using UniRx;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
using UnityEngine.UI;
using UnityEngine.UI.Extensions;
namespace Ichni.NodeScript
{
public class NodeManager : MonoBehaviour
{
public static NodeManager Instance;
public List<NodeObject> allNodes = new();
public GameObject nodeObjectPrefab;
Canvas canvas;
UILineRenderer dragLine;
public float wireThickness = 5f;
ConnectorSlot dragSource;
bool isDraggingWire;
// 中键拖拽面板
Vector2 _panelDragOrigin;
bool _isPanning;
// 单步调试
bool _debugMode;
int _debugStep;
public class WireConnection { public ConnectorSlot from, to; public UILineRenderer line; public bool selected; }
public List<WireConnection> connections = new();
public RectTransform canvasArea;
public Camera refCamera;
public Transform contextMenuRoot;
public Transform NodeArea;
public GameObject contextMenuItemPrefab;
public GameObject uiInputPrefab;
public GameElement startElement { get; private set; }
// ---- 命名变量存储 ----
Dictionary<string, object> _variables = new();
// ---- 脏元素收集,图执行完后统一 Refresh ----
HashSet<GameElement> _dirtyElements = new();
public void MarkDirty(GameElement e) { if (e != null) _dirtyElements.Add(e); }
// ---- 生命周期调度表 ----
HashSet<NodeBase> triggerTable = new();
HashSet<NodeBase> runtimeTable = new();
public T GetVariable<T>(string name)
{
if (_variables.TryGetValue(name, out var v) && v is T tv) return tv;
return default;
}
public void SetVariable<T>(string name, T value) { _variables[name] = value; }
public bool TryGetVariable<T>(string name, out T v)
{
if (_variables.TryGetValue(name, out var obj) && obj is T tv) { v = tv; return true; }
v = default; return false;
}
void Awake()
{
if (Instance != null && Instance != this) { Destroy(this); return; }
Instance = this;
canvas = GetComponentInParent<Canvas>();
}
void OnDestroy()
{
if (Instance == this) Instance = null;
foreach (var w in connections) { if (w.line != null && w.line.gameObject != null) Destroy(w.line.gameObject); }
connections.Clear();
if (dragLine != null && dragLine.gameObject != null) Destroy(dragLine.gameObject);
}
void Start()
{
if (refCamera == null) refCamera = canvas != null ? canvas.worldCamera : Camera.main;
dragLine = CreateWire("DragWire", Color.white);
dragLine.raycastTarget = false;
dragLine.gameObject.SetActive(false);
if (contextMenuRoot != null) contextMenuRoot.gameObject.SetActive(false);
}
/// <summary>外部实例化 Manager 后调用,传入入口 GameElement 并自动创建 Start 节点</summary>
public void Init(GameElement element)
{
startElement = element;
allNodes = FindObjectsByType<NodeObject>(FindObjectsSortMode.None).ToList();
if (!allNodes.Any(n => n.nodeBase is NodeStart))
{
var data = new NodeStart { NodeName = "Start", boundElement = element };
data.InitConnectors();
var go = nodeObjectPrefab != null ? Instantiate(nodeObjectPrefab, NodeArea) : new GameObject("Start", typeof(RectTransform));
var no = go.GetComponent<NodeObject>(); if (no == null) no = go.AddComponent<NodeObject>();
no.nodeBase = data;
go.GetComponent<RectTransform>().anchoredPosition = new Vector2(-400, 0);
no.Init(); allNodes.Add(no);
}
}
List<NodeObject> selectedNodes = new();
(List<(Type type, Dictionary<string, object> fields)> nodes,
List<(int fromIdx, int toIdx, string outName, string inName)> wires) clipboard = (new(), new());
void Update()
{
if (Keyboard.current == null) return;
var mousePos = Mouse.current.position.ReadValue();
// ---- 手动线悬停 / 点击检测 ----
WireConnection clickedWire = UpdateWireHover();
// ---- 空位点击 → 取消所有选中(线点击已在上一步处理) ----
if (Mouse.current.leftButton.wasPressedThisFrame && clickedWire == null)
{
var ped = new PointerEventData(EventSystem.current) { position = mousePos };
var hits = new List<RaycastResult>(); EventSystem.current.RaycastAll(ped, hits);
bool hitUI = hits.Any(r =>
r.gameObject.GetComponent<NodeObject>() != null ||
r.gameObject.GetComponent<ConnectorSlot>() != null);
if (!hitUI) DeselectAll();
}
// ---- 中键拖拽整个节点面板 ----
if (Mouse.current.middleButton.wasPressedThisFrame)
{
_panelDragOrigin = mousePos;
_isPanning = true;
}
if (_isPanning && Mouse.current.middleButton.isPressed)
{
var delta = mousePos - _panelDragOrigin;
_panelDragOrigin = mousePos;
var nodeAreaRt = NodeArea as RectTransform;
if (nodeAreaRt != null)
nodeAreaRt.anchoredPosition += delta / (canvas != null ? canvas.scaleFactor : 1f);
RefreshAllLines();
}
if (Mouse.current.middleButton.wasReleasedThisFrame)
_isPanning = false;
// Esc → 退出调试
if (Keyboard.current.escapeKey.wasPressedThisFrame && _debugMode)
DebugReset();
// Shift+Enter → 单步
if (Keyboard.current.leftShiftKey.isPressed && Keyboard.current.enterKey.wasPressedThisFrame)
{
if (!_debugMode) DebugInit();
DebugStep();
}
// Ctrl+Enter → Run保留
if (Keyboard.current.leftCtrlKey.isPressed && Keyboard.current.enterKey.wasPressedThisFrame)
RunGraph();
// Ctrl+RightClick → Create Node
if (Keyboard.current.leftCtrlKey.isPressed && Mouse.current.rightButton.wasPressedThisFrame)
TryShowContextMenu(Mouse.current.position.ReadValue());
// Delete → Remove selected
if (Keyboard.current.deleteKey.wasPressedThisFrame)
DeleteSelected();
// Ctrl+C → Copy
if (Keyboard.current.leftCtrlKey.isPressed && Keyboard.current.cKey.wasPressedThisFrame)
CopySelected();
// Ctrl+V → Paste
if (Keyboard.current.leftCtrlKey.isPressed && Keyboard.current.vKey.wasPressedThisFrame)
PasteClipboard();
// F5 → 拓扑预览
if (Keyboard.current.f5Key.wasPressedThisFrame)
PreviewOrder();
// // F1 → Save
// if (Keyboard.current.f1Key.wasPressedThisFrame)
// SaveToFile();
// // F2 → Load
// if (Keyboard.current.f2Key.wasPressedThisFrame)
// LoadFromFile();
}
// ========== 选中 ==========
void DeselectAll()
{
foreach (var n in selectedNodes) n.Selected = false;
selectedNodes.Clear();
foreach (var w in connections) w.selected = false;
}
public void SelectNode(NodeObject node, PointerEventData e)
{
bool multi = e != null && (e.button == PointerEventData.InputButton.Left);
bool shift = Keyboard.current != null && Keyboard.current.leftShiftKey.isPressed;
if (!shift)
{
foreach (var n in selectedNodes) n.Selected = false;
selectedNodes.Clear();
}
if (selectedNodes.Contains(node))
{
node.Selected = false;
selectedNodes.Remove(node);
}
else
{
node.Selected = true;
selectedNodes.Add(node);
}
}
// ========== 删除 ==========
void DeleteSelected()
{
// 1) 删选中线
for (int i = connections.Count - 1; i >= 0; i--)
{
var w = connections[i];
if (w.selected) RemoveConnection(w);
}
// 2) 删选中节点(含其关联线)
foreach (var node in selectedNodes.ToList())
{
for (int i = connections.Count - 1; i >= 0; i--)
{
if (connections[i].from?.ownerNode == node || connections[i].to?.ownerNode == node)
RemoveConnection(connections[i]);
}
allNodes.Remove(node);
Destroy(node.gameObject);
}
selectedNodes.Clear();
CleanupConnections();
}
void RemoveConnection(WireConnection w)
{
// 断开 out→in 数据连接
if (w.from?.connectorOut is OutputAny outAny && w.to?.connectorIn is InputAny inAny)
{
outAny.DisconnectAny(inAny);
inAny.DisconnectAny();
}
else if (w.from?.connectorOut is OutputAny outAny2 && w.to?.connectorIn != null)
{
// OutputAny → Input<T>: input 侧调 DisconnectAny
var inType = w.to.connectorIn.GetType();
if (inType.IsGenericType && inType.GetGenericTypeDefinition() == typeof(Input<>))
inType.GetMethod("DisconnectAny")?.Invoke(w.to.connectorIn, null);
}
else if (w.from?.connectorOut != null && w.to?.connectorIn is InputAny inAny2)
{
inAny2.DisconnectAny();
}
else if (w.from?.connectorOut != null && w.to?.connectorIn != null)
{
var outType = w.from.connectorOut.GetType();
var inType = w.to.connectorIn.GetType();
if (outType.IsGenericType && outType.GetGenericTypeDefinition() == typeof(Output<>))
{
var discMethod = outType.GetMethod("Disconnect", new[] { inType });
discMethod?.Invoke(w.from.connectorOut, new[] { w.to.connectorIn });
}
if (inType.IsGenericType && inType.GetGenericTypeDefinition() == typeof(Input<>))
{
var discMethod = inType.GetMethod("Disconnect", new[] { outType });
discMethod?.Invoke(w.to.connectorIn, new[] { w.from.connectorOut });
}
}
// 销毁线视觉
if (w.line != null) { if (w.line.gameObject != null) Destroy(w.line.gameObject); }
w.from = null; w.to = null; w.line = null;
connections.Remove(w);
}
void CleanupConnections()
{
for (int i = connections.Count - 1; i >= 0; i--)
{
var w = connections[i];
bool dead = w.from == null || w.to == null || w.line == null
|| w.from.ownerNode == null || w.to.ownerNode == null
|| w.from.connectorOut == null || w.to.connectorIn == null;
if (dead)
{
if (w.line != null && w.line.gameObject != null) Destroy(w.line.gameObject);
connections.RemoveAt(i);
}
}
}
// ========== 保存 / 加载 ==========
[Serializable] public class GraphData { public string startElementGuid; public List<NodeData> nodes = new(); public List<WireData> wires = new(); }
[Serializable] public class NodeData { public string typeName; public float posX, posY; public List<FieldPair> fieldValues = new(); }
[Serializable] public class WireData { public int fromNodeIdx, toNodeIdx; public string fromOutput, toInput; }
[Serializable] public struct FieldPair { public string key; public string json; public FieldPair(string k, string j) { key = k; json = j; } }
static string SavePath => Application.streamingAssetsPath + "/NodeScript/";
static string SaveFile => SavePath + "graph.json";
public static string GetSavePath(string name) => SavePath + name + ".json";
public GraphData SaveGraph()
{
var g = new GraphData();
g.startElementGuid = startElement?.elementGuid.ToString();
var idxMap = new Dictionary<NodeObject, int>();
for (int i = 0; i < allNodes.Count; i++)
{
var n = allNodes[i]; idxMap[n] = i;
var nd = new NodeData
{
typeName = n.nodeBase.GetType().FullName,
posX = n.GetComponent<RectTransform>().anchoredPosition.x,
posY = n.GetComponent<RectTransform>().anchoredPosition.y
};
foreach (var f in n.nodeBase.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance))
{
if (typeof(IInput).IsAssignableFrom(f.FieldType) || typeof(IOutput).IsAssignableFrom(f.FieldType))
continue;
if (System.Attribute.IsDefined(f, typeof(NonSerializedAttribute))) continue;
nd.fieldValues.Add(new FieldPair(f.Name, JsonUtility.ToJson(f.GetValue(n.nodeBase))));
}
g.nodes.Add(nd);
}
foreach (var w in connections)
{
if (w.from?.ownerNode == null || w.to?.ownerNode == null) continue;
g.wires.Add(new WireData
{
fromNodeIdx = idxMap.GetValueOrDefault(w.from.ownerNode, -1),
toNodeIdx = idxMap.GetValueOrDefault(w.to.ownerNode, -1),
fromOutput = w.from.connectorOut?.Name,
toInput = w.to.connectorIn?.Name
});
}
return g;
}
public void SaveToFile(string name = null)
{
string path = string.IsNullOrEmpty(name) ? SaveFile : GetSavePath(name);
System.IO.Directory.CreateDirectory(SavePath);
var json = JsonUtility.ToJson(SaveGraph(), true);
System.IO.File.WriteAllText(path, json);
Debug.Log("[NodeManager] Saved to " + path);
}
public void LoadFromFile(string name = null)
{
string path = string.IsNullOrEmpty(name) ? SaveFile : GetSavePath(name);
if (!System.IO.File.Exists(path))
{
Debug.LogWarning("[NodeManager] No save file at " + path);
return;
}
// 清空
foreach (var n in allNodes) Destroy(n.gameObject);
allNodes.Clear();
foreach (var w in connections) { if (w.line != null) Destroy(w.line.gameObject); }
connections.Clear();
selectedNodes.Clear();
var json = System.IO.File.ReadAllText(path);
var g = JsonUtility.FromJson<GraphData>(json);
// 重建节点
var newNodes = new List<NodeObject>();
foreach (var nd in g.nodes)
{
var type = Type.GetType(nd.typeName) ?? AppDomain.CurrentDomain.GetAssemblies()
.Select(a => a.GetType(nd.typeName)).FirstOrDefault(t => t != null);
if (type == null) { Debug.LogWarning("[NodeManager] Type not found: " + nd.typeName); continue; }
var data = (NodeBase)Activator.CreateInstance(type);
data.NodeName = type.Name;
data.InitConnectors();
// 恢复字段值
foreach (var fp in nd.fieldValues)
{
var f = type.GetField(fp.key, BindingFlags.Public | BindingFlags.Instance);
if (f != null && (f.FieldType.IsValueType || f.FieldType == typeof(string)))
try { f.SetValue(data, JsonUtility.FromJson(fp.json, f.FieldType)); } catch { }
}
var go = nodeObjectPrefab != null ? Instantiate(nodeObjectPrefab, transform) : new GameObject(data.NodeName, typeof(RectTransform));
var no = go.GetComponent<NodeObject>(); if (no == null) no = go.AddComponent<NodeObject>();
no.nodeBase = data;
go.GetComponent<RectTransform>().anchoredPosition = new Vector2(nd.posX, nd.posY);
no.Init(); allNodes.Add(no); newNodes.Add(no);
}
// 重建连线
foreach (var wd in g.wires)
{
if (wd.fromNodeIdx < 0 || wd.toNodeIdx < 0) continue;
if (wd.fromNodeIdx >= newNodes.Count || wd.toNodeIdx >= newNodes.Count) continue;
var srcSlots = newNodes[wd.fromNodeIdx].GetComponentsInChildren<ConnectorSlot>();
var dstSlots = newNodes[wd.toNodeIdx].GetComponentsInChildren<ConnectorSlot>();
var src = srcSlots.FirstOrDefault(s => !s.isInput && s.connectorOut?.Name == wd.fromOutput);
var dst = dstSlots.FirstOrDefault(s => s.isInput && s.connectorIn?.Name == wd.toInput);
if (src != null && dst != null) TryConnect(src, dst);
}
// 绑定 Start 节点的 element
var sel = EditorManager.instance?.operationManager?.currentSelectedElements;
if (sel != null && sel.Count > 0)
{
foreach (var n in allNodes)
{
if (n.nodeBase is NodeStart start)
{
start.boundElement = sel[0];
startElement = sel[0];
}
}
}
Debug.Log("[NodeManager] Loaded " + allNodes.Count + " nodes, " + connections.Count + " wires");
}
// ========== 复制 ==========
void CopySelected()
{
clipboard.nodes.Clear(); clipboard.wires.Clear();
var idxMap = new Dictionary<NodeObject, int>();
for (int i = 0; i < selectedNodes.Count; i++)
{
var node = selectedNodes[i]; idxMap[node] = i;
var fields = new Dictionary<string, object>();
foreach (var f in node.nodeBase.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance))
{
if (!typeof(IInput).IsAssignableFrom(f.FieldType) && !typeof(IOutput).IsAssignableFrom(f.FieldType))
fields[f.Name] = f.GetValue(node.nodeBase);
}
clipboard.nodes.Add((node.nodeBase.GetType(), fields));
}
// 复制选中节点之间的连线
foreach (var w in connections)
{
if (w.selected || (w.from != null && w.to != null
&& selectedNodes.Contains(w.from.ownerNode) && selectedNodes.Contains(w.to.ownerNode)))
{
int fi = idxMap.GetValueOrDefault(w.from.ownerNode, -1);
int ti = idxMap.GetValueOrDefault(w.to.ownerNode, -1);
if (fi >= 0 && ti >= 0)
clipboard.wires.Add((fi, ti, w.from.connectorOut?.Name, w.to.connectorIn?.Name));
}
}
}
void PasteClipboard()
{
var pos = Mouse.current.position.ReadValue();
var newNodes = new List<NodeObject>();
foreach (var (type, fields) in clipboard.nodes)
{
var node = CreateNodeInstance(type, pos);
foreach (var kv in fields)
{
var f = type.GetField(kv.Key, BindingFlags.Public | BindingFlags.Instance);
if (f != null && f.FieldType == kv.Value?.GetType())
f.SetValue(node.nodeBase, kv.Value);
}
newNodes.Add(node);
pos += new Vector2(30, -30);
}
// 重连粘贴节点之间的线
foreach (var (fi, ti, outName, inName) in clipboard.wires)
{
if (fi >= newNodes.Count || ti >= newNodes.Count) continue;
var srcSlots = newNodes[fi].GetComponentsInChildren<ConnectorSlot>();
var dstSlots = newNodes[ti].GetComponentsInChildren<ConnectorSlot>();
var src = srcSlots.FirstOrDefault(s => !s.isInput && s.connectorOut?.Name == outName);
var dst = dstSlots.FirstOrDefault(s => s.isInput && s.connectorIn?.Name == inName);
if (src != null && dst != null) TryConnect(src, dst);
}
}
NodeObject CreateNodeInstance(Type nodeType, Vector2 screenPos, bool allowDuplicate = true)
{
// Start / Entry 全局唯一
if (!allowDuplicate && allNodes.Any(n => n.nodeBase.GetType() == nodeType))
{
Debug.LogWarning("[NodeManager] " + nodeType.Name + " already exists");
return null;
}
var data = (NodeBase)Activator.CreateInstance(nodeType);
data.NodeName = nodeType.Name;
data.InitConnectors();
GameObject go = nodeObjectPrefab != null ? Instantiate(nodeObjectPrefab, NodeArea) : new GameObject(data.NodeName, typeof(RectTransform));
var n = go.GetComponent<NodeObject>(); if (n == null) n = go.AddComponent<NodeObject>();
n.nodeBase = data;
RectTransform rt = go.GetComponent<RectTransform>();
var parentRt = NodeArea as RectTransform;
if (parentRt == null) parentRt = (RectTransform)transform;
RectTransformUtility.ScreenPointToLocalPointInRectangle(parentRt, screenPos, refCamera, out var lp);
rt.anchoredPosition = lp;
n.Init(); allNodes.Add(n);
return n;
}
// ---- coord ----
Vector2 ToLocal(RectTransform rt)
{
Vector2 screen;
if (canvas == null || canvas.renderMode == RenderMode.ScreenSpaceOverlay) screen = rt.position;
else screen = RectTransformUtility.WorldToScreenPoint(canvas.worldCamera, rt.position);
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvas.transform as RectTransform, screen, canvas.worldCamera, out Vector2 local);
return local;
}
Vector2 ToLocal(Vector2 sp) { RectTransformUtility.ScreenPointToLocalPointInRectangle(canvas.transform as RectTransform, sp, canvas.worldCamera, out var l); return l; }
// ---- wire drag ----
public void StartWireDrag(ConnectorSlot slot, PointerEventData e)
{
if (slot.isInput || slot.connectorOut == null) return;
dragSource = slot; isDraggingWire = true;
dragLine.color = slot.connectorOut.ConnectorColor;
dragLine.gameObject.SetActive(true);
dragLine.Points = new[] { ToLocal(slot.connectorRect), ToLocal(e.position) };
}
public void UpdateWireDrag(PointerEventData e) { if (isDraggingWire) dragLine.Points = new[] { ToLocal(dragSource.connectorRect), ToLocal(e.position) }; }
public void EndWireDrag(PointerEventData e)
{
if (!isDraggingWire) return;
dragLine.gameObject.SetActive(false);
var t = HitTestSlot(e, true);
if (t != null && t != dragSource) TryConnect(dragSource, t);
dragSource = null; isDraggingWire = false;
}
// ---- connect ----
void TryConnect(ConnectorSlot src, ConnectorSlot dst)
{
if (src.connectorOut == null || dst.connectorIn == null) return;
if (src.ownerNode == dst.ownerNode) return;
// 环检测:从 dst 出发沿已有连线 DFS看能否回到 src
if (WouldCreateCycle(src.ownerNode.nodeBase, dst.ownerNode.nodeBase))
{
Debug.LogWarning($"[NodeManager] Connection rejected: would create cycle ({src.ownerNode.nodeBase.NodeName} -> {dst.ownerNode.nodeBase.NodeName})");
return;
}
// 类型检查:处理 InputAny / OutputAny
var outType = src.connectorOut.DataType;
var inType = dst.connectorIn.DataType;
// InputAny 未锁定 → 允许连接,锁定类型
if (dst.connectorIn is InputAny inAny && inAny.DataType == null)
{
// OK: 类型将从 src 锁定
}
// InputAny 已锁定 → 检查兼容
else if (dst.connectorIn is InputAny inAnyLocked && inAnyLocked.DataType != null)
{
if (!TypesCompatible(outType, inAnyLocked.DataType))
{
Debug.LogWarning($"[NodeManager] Type mismatch: {outType?.Name} -> InputAny(locked={inAnyLocked.DataType.Name})");
return;
}
}
// 标准泛型端口 → 兼容匹配
else if (!TypesCompatible(outType, inType))
{
Debug.LogWarning($"[NodeManager] Type mismatch: {outType?.Name} != {inType?.Name}");
return;
}
// 实际连接
if (src.connectorOut is OutputAny outAny && dst.connectorIn is InputAny inAny2)
{
outAny.ConnectAny(inAny2);
inAny2.ConnectAny(outAny);
}
else if (src.connectorOut is OutputAny outAny3)
{
var inT = dst.connectorIn.GetType();
if (inT.IsGenericType && inT.GetGenericTypeDefinition() == typeof(Input<>))
{
// OutputAny → Input<T>: 通过 ConnectAny 桥接
var connectAny = inT.GetMethod("ConnectAny", new[] { typeof(IOutput) });
connectAny?.Invoke(dst.connectorIn, new[] { (IOutput)outAny3 });
}
else
{
outAny3.ConnectAny(dst.connectorIn as InputAny);
(dst.connectorIn as InputAny)?.ConnectAny(outAny3);
}
}
else if (dst.connectorIn is InputAny inAny4)
{
// Output<T> → InputAny
inAny4.ConnectAny(src.connectorOut);
}
else
{
// Standard Output<T> → Input<T>
var outReal = src.connectorOut.GetType();
var inReal = dst.connectorIn.GetType();
var connectMethod = outReal.GetMethods(BindingFlags.Instance | BindingFlags.Public)
.FirstOrDefault(m => m.Name == "Connect" && m.GetParameters().Length == 1);
var connectInMethod = inReal.GetMethods(BindingFlags.Instance | BindingFlags.Public)
.FirstOrDefault(m => m.Name == "Connect" && m.GetParameters().Length == 1);
if (connectMethod == null || connectInMethod == null)
{
Debug.LogError($"[Wire] Reflection failed: {outReal.Name}.Connect({inReal.Name})");
return;
}
connectMethod.Invoke(src.connectorOut, new[] { dst.connectorIn });
connectInMethod.Invoke(dst.connectorIn, new[] { src.connectorOut });
}
// 刷新连接点外观InputAny 锁定后颜色会变)
dst.RefreshAppearance();
var line = CreateWire("W_" + connections.Count, src.connectorOut.ConnectorColor);
line.gameObject.SetActive(true);
var pts = new[] { ToLocal(src.connectorRect), ToLocal(dst.connectorRect) };
line.Points = pts;
FitRectToLine(line, pts);
connections.Add(new WireConnection { from = src, to = dst, line = line });
// 连线后重算 L
ComputeLValues();
}
ConnectorSlot HitTestSlot(PointerEventData e, bool isInput)
{
var r = new List<RaycastResult>(); EventSystem.current.RaycastAll(e, r);
return r.Select(x => x.gameObject.GetComponent<ConnectorSlot>()).FirstOrDefault(s => s != null && s.isInput == isInput && (isInput ? s.connectorIn != null : s.connectorOut != null));
}
public void RefreshAllLines()
{
foreach (var w in connections)
if (w.from != null && w.to != null)
{
var pts = new[] { ToLocal(w.from.connectorRect), ToLocal(w.to.connectorRect) };
w.line.Points = pts;
FitRectToLine(w.line, pts);
}
}
void FitRectToLine(UILineRenderer line, Vector2[] pts)
{
if (pts.Length < 2) return;
var rt = line.GetComponent<RectTransform>();
var min = pts[0]; var max = pts[0];
for (int i = 1; i < pts.Length; i++)
{
min = Vector2.Min(min, pts[i]);
max = Vector2.Max(max, pts[i]);
}
float pad = wireThickness + 4f;
Vector2 offset = min - new Vector2(pad, pad);
// 将 Points 从 canvas 绝对坐标转为相对 RectTransform 的局部坐标
var localPts = new Vector2[pts.Length];
for (int i = 0; i < pts.Length; i++)
localPts[i] = pts[i] - offset;
line.Points = localPts;
rt.anchoredPosition = offset;
rt.sizeDelta = max - min + new Vector2(pad * 2f, pad * 2f);
}
UILineRenderer CreateWire(string n, Color c)
{
var g = new GameObject(n, typeof(RectTransform));
g.transform.SetParent(canvas.transform, false);
var l = g.AddComponent<UILineRenderer>(); l.color = c; l.LineThickness = wireThickness;
l.RelativeSize = false; l.BezierMode = UILineRenderer.BezierType.Quick;
l.Points = new Vector2[] { Vector2.zero, Vector2.zero };
l.raycastTarget = false;
var h = g.AddComponent<WireClickHandler>();
h.mgr = this; h.line = l;
return l;
}
public void ToggleWireSelection(UILineRenderer line, bool shift)
{
var wc = connections.Find(w => w.line == line);
if (wc == null) return;
if (!shift) { foreach (var w in connections) w.selected = false; }
wc.selected = !wc.selected;
}
WireConnection UpdateWireHover()
{
var canvasMouse = ToLocal(Mouse.current.position.ReadValue());
float hoverThreshold = wireThickness + 6f;
WireConnection hovered = null;
float bestDist = hoverThreshold;
foreach (var w in connections)
{
if (w.line == null) continue;
var pts = w.line.Points;
if (pts == null || pts.Length < 2) continue;
var rtPos = w.line.GetComponent<RectTransform>().anchoredPosition;
for (int i = 1; i < pts.Length; i++)
{
float d = DistToSegment(canvasMouse, rtPos + pts[i - 1], rtPos + pts[i]);
if (d < bestDist) { bestDist = d; hovered = w; }
}
}
foreach (var w in connections)
{
if (w.line == null) continue;
w.line.LineThickness = w.selected ? wireThickness * 2f
: (w == hovered) ? wireThickness + 3f
: wireThickness;
}
if (Mouse.current.leftButton.wasPressedThisFrame && hovered != null)
ToggleWireSelection(hovered.line, Keyboard.current.leftShiftKey.isPressed);
return Mouse.current.leftButton.wasPressedThisFrame ? hovered : null;
}
static float DistToSegment(Vector2 p, Vector2 a, Vector2 b)
{
Vector2 ab = b - a;
float lenSq = ab.sqrMagnitude;
if (lenSq < 0.001f) return Vector2.Distance(p, a);
float t = Mathf.Clamp01(Vector2.Dot(p - a, ab) / lenSq);
return Vector2.Distance(p, a + t * ab);
}
// ---- 单步调试 ----
void DebugInit()
{
_debugMode = true;
_debugStep = 0;
_dirtyElements.Clear();
ComputeLValues();
foreach (var n in allNodes)
n.nodeBase.Status = NodeStatus.Ready;
triggerTable.Clear();
runtimeTable.Clear();
foreach (var n in allNodes)
triggerTable.Add(n.nodeBase);
UpdateAllStatusDisplay();
Debug.Log($"[NodeManager] Debug init — {allNodes.Count} nodes ready. Shift+Enter to step.");
}
void DebugStep()
{
if (!_debugMode) return;
if (triggerTable.Count == 0 && runtimeTable.Count == 0)
{
Debug.Log($"[NodeManager] Debug done — all nodes completed in {_debugStep} steps.");
DebugReset();
return;
}
// 记录本周期开始前 runtime 中的节点
var before = new HashSet<NodeBase>(runtimeTable);
_debugStep++;
RunCycle();
// 详细日志
var sb = new System.Text.StringBuilder();
sb.AppendLine($"<color=cyan>=== Step {_debugStep} ===</color>");
foreach (var n in before)
{
var icon = n.Status == NodeStatus.Complete ? "✓" : n.Status == NodeStatus.Hang ? "⏳" : "▶";
var color = n.Status == NodeStatus.Complete ? "green" : n.Status == NodeStatus.Hang ? "yellow" : "white";
sb.AppendLine($" {icon} <color={color}>{n.NodeName}</color> (L:{n.L}) → {n.Status}");
}
sb.AppendLine($" triggers pending: {triggerTable.Count}, still running: {runtimeTable.Count}");
Debug.Log(sb.ToString());
UpdateAllStatusDisplay();
if (triggerTable.Count == 0 && runtimeTable.Count == 0)
{
Observable.NextFrame(FrameCountType.EndOfFrame)
.Subscribe(_ =>
{
foreach (var e in _dirtyElements)
if (e is IBaseElement be) be.Refresh();
_dirtyElements.Clear();
});
}
}
void DebugReset()
{
_debugMode = false;
_debugStep = 0;
triggerTable.Clear();
runtimeTable.Clear();
foreach (var n in allNodes)
n.nodeBase.Status = NodeStatus.Ready;
UpdateAllStatusDisplay();
Debug.Log("[NodeManager] Debug reset.");
}
void PreviewOrder()
{
ComputeLValues();
var byLayer = allNodes.GroupBy(n => n.nodeBase.L)
.OrderBy(g => g.Key)
.ToList();
var sb = new System.Text.StringBuilder();
sb.AppendLine("<color=cyan>═══ Topological Order (BFS layers) ═══</color>");
foreach (var layer in byLayer)
{
var names = string.Join(", ", layer.Select(n => $"{n.nodeBase.NodeName}(L:{n.nodeBase.L})"));
var fanOut = layer.Sum(n => connections.Count(w => w.from?.ownerNode == n));
sb.AppendLine($" <color=yellow>Layer {layer.Key}</color> ({layer.Count()} nodes, {fanOut} downstream wires): {names}");
}
var isolated = allNodes.Where(n => n.nodeBase.L < 0).ToList();
if (isolated.Count > 0)
{
sb.AppendLine($" <color=red>Unreachable</color> ({isolated.Count} nodes): {string.Join(", ", isolated.Select(n => n.nodeBase.NodeName))}");
}
var totalConnections = connections.Count;
var cycles = totalConnections - (allNodes.Count - isolated.Count); // rough estimate
sb.AppendLine($" Total: {allNodes.Count} nodes, {totalConnections} wires, {byLayer.Count} layers");
Debug.Log(sb.ToString());
UpdateAllStatusDisplay();
}
void UpdateAllStatusDisplay()
{
foreach (var n in allNodes)
n.UpdateStatusDisplay();
}
// ---- run (lifecycle) ----
public void RunGraph()
{
_dirtyElements.Clear();
ComputeLValues();
triggerTable.Clear();
runtimeTable.Clear();
// 所有节点初始进入触发表
foreach (var n in allNodes)
{
n.nodeBase.Status = NodeStatus.Ready;
triggerTable.Add(n.nodeBase);
}
Debug.Log("[NodeManager] === START (lifecycle) ===");
int maxIter = 10000;
int iter = 0;
while ((triggerTable.Count > 0 || runtimeTable.Count > 0) && iter++ < maxIter)
RunCycle();
if (iter >= maxIter)
Debug.LogWarning("[NodeManager] exceeded max iterations — possible infinite loop");
else
Debug.Log($"<color=green>[NodeManager] Run complete — {iter} cycles, {allNodes.Count(n => n.nodeBase.Status == NodeStatus.Complete)} nodes completed</color>");
// 统一 Refresh 脏元素
UpdateAllStatusDisplay();
Observable.NextFrame(FrameCountType.EndOfFrame)
.Subscribe(_ =>
{
foreach (var e in _dirtyElements)
if (e is IBaseElement be) be.Refresh();
_dirtyElements.Clear();
});
}
void RunCycle()
{
// 触发表并入运行时表
foreach (var t in triggerTable)
{
if (t.Status == NodeStatus.Complete) continue;
t.Status = NodeStatus.Ready;
runtimeTable.Add(t);
}
triggerTable.Clear();
var toRemove = new List<NodeBase>();
foreach (var node in runtimeTable)
{
if (node.Status == NodeStatus.Complete)
{
toRemove.Add(node);
continue;
}
var result = node.Loop();
if (result.Triggers != null)
{
foreach (var t in result.Triggers)
triggerTable.Add(t);
}
if (result.TriggerDownstream)
{
foreach (var w in connections)
{
if (w.from?.ownerNode?.nodeBase == node && w.to?.ownerNode?.nodeBase != null)
{
var downstream = w.to.ownerNode.nodeBase;
if (downstream.Status != NodeStatus.Complete)
triggerTable.Add(downstream);
}
}
}
if (result.RemoveFromRuntime)
{
node.Status = NodeStatus.Complete;
toRemove.Add(node);
}
}
foreach (var r in toRemove)
runtimeTable.Remove(r);
}
/// <summary>BFS 计算所有节点到 Start 的最短距离 L仅用于显示</summary>
public void ComputeLValues()
{
foreach (var n in allNodes)
n.nodeBase.L = -1;
var queue = new Queue<NodeBase>();
foreach (var n in allNodes)
{
if (n.nodeBase is NodeStart || n.nodeBase is NodeEntry)
{
n.nodeBase.L = 0;
queue.Enqueue(n.nodeBase);
}
}
while (queue.Count > 0)
{
var cur = queue.Dequeue();
int nextL = cur.L + 1;
foreach (var w in connections)
{
if (w.from?.ownerNode?.nodeBase == cur && w.to?.ownerNode?.nodeBase != null)
{
var downstream = w.to.ownerNode.nodeBase;
if (downstream.L == -1)
{
downstream.L = nextL;
queue.Enqueue(downstream);
}
}
}
}
// 刷新所有节点标题显示 L 值
foreach (var n in allNodes)
n.UpdateLDisplay();
}
static bool IsNumeric(Type t) => t == typeof(float) || t == typeof(int);
static bool TypesCompatible(Type from, Type to)
{
if (from == null || to == null) return true; // untyped → allow
if (from == to) return true;
if (IsNumeric(from) && IsNumeric(to)) return true; // int ↔ float
return false;
}
/// <summary>尝试添加 from→to 边是否会成环:从 to 出发 DFS看能否回到 from</summary>
bool WouldCreateCycle(NodeBase from, NodeBase to)
{
if (from == null || to == null) return true;
var visited = new HashSet<NodeBase>();
var stack = new Stack<NodeBase>();
stack.Push(to);
while (stack.Count > 0)
{
var cur = stack.Pop();
if (cur == from) return true;
if (!visited.Add(cur)) continue;
foreach (var w in connections)
{
if (w.from?.ownerNode?.nodeBase == cur && w.to?.ownerNode?.nodeBase != null)
stack.Push(w.to.ownerNode.nodeBase);
}
}
return false;
}
// ---- context menu ----
RectTransform _menuContent;
string _searchFilter = "";
void EnsureScrollMenu()
{
if (contextMenuRoot == null) return;
if (_menuContent != null) return;
var rootRt = contextMenuRoot as RectTransform;
rootRt.sizeDelta = new Vector2(350, 850);
var scroll = contextMenuRoot.gameObject.GetComponent<ScrollRect>();
if (scroll == null) scroll = contextMenuRoot.gameObject.AddComponent<ScrollRect>();
var mask = contextMenuRoot.gameObject.GetComponent<Mask>();
if (mask == null) mask = contextMenuRoot.gameObject.AddComponent<Mask>();
var img = contextMenuRoot.gameObject.GetComponent<Image>();
if (img == null) { img = contextMenuRoot.gameObject.AddComponent<Image>(); img.color = new Color(0.1f, 0.1f, 0.1f, 0.95f); }
// ---- 搜索栏(固定在顶部,不随滚动)----
if (uiInputPrefab != null)
{
var searchGo = Instantiate(uiInputPrefab, contextMenuRoot);
searchGo.name = "Search";
var searchRt = searchGo.GetComponent<RectTransform>();
searchRt.anchorMin = new Vector2(0, 1); searchRt.anchorMax = new Vector2(1, 1);
searchRt.pivot = new Vector2(0.5f, 1);
searchRt.anchoredPosition = new Vector2(0, -5);
searchRt.sizeDelta = new Vector2(-10, 30);
var searchInput = searchGo.GetComponentInChildren<TMP_InputField>();
if (searchInput != null)
{
searchInput.onValueChanged.AddListener(v =>
{
_searchFilter = v;
BuildContextMenu();
});
}
}
// ---- 滚动区域 ----
var viewport = new GameObject("Viewport", typeof(RectTransform), typeof(Image), typeof(Mask));
viewport.transform.SetParent(contextMenuRoot, false);
var vpRt = viewport.GetComponent<RectTransform>();
vpRt.anchorMin = new Vector2(0, 0); vpRt.anchorMax = new Vector2(1, 1);
vpRt.offsetMin = new Vector2(0, 0); vpRt.offsetMax = new Vector2(0, -40); // 给搜索栏让位
var content = new GameObject("Content", typeof(RectTransform), typeof(VerticalLayoutGroup), typeof(ContentSizeFitter));
content.transform.SetParent(viewport.transform, false);
var ctRt = content.GetComponent<RectTransform>();
ctRt.anchorMin = new Vector2(0, 1); ctRt.anchorMax = new Vector2(1, 1);
ctRt.pivot = new Vector2(0.5f, 1); ctRt.sizeDelta = new Vector2(0, 0);
var vlg = content.GetComponent<VerticalLayoutGroup>();
vlg.childForceExpandWidth = true; vlg.childForceExpandHeight = false;
vlg.childControlWidth = true; vlg.childControlHeight = false;
var csf = content.GetComponent<ContentSizeFitter>();
csf.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
scroll.viewport = vpRt;
scroll.content = ctRt;
scroll.horizontal = false;
scroll.vertical = true;
_menuContent = ctRt;
}
void TryShowContextMenu(Vector2 sp)
{
if (canvasArea == null || contextMenuRoot == null) return;
if (!RectTransformUtility.RectangleContainsScreenPoint(canvasArea, sp, refCamera)) return;
EnsureScrollMenu();
_searchFilter = "";
BuildContextMenu();
RectTransformUtility.ScreenPointToLocalPointInRectangle((RectTransform)contextMenuRoot.parent, sp, refCamera, out var lp);
contextMenuRoot.gameObject.SetActive(true);
(contextMenuRoot as RectTransform).anchoredPosition = lp;
}
void BuildContextMenu()
{
if (contextMenuRoot == null || _menuContent == null) return;
foreach (Transform t in _menuContent) Destroy(t.gameObject);
GetNodeTypes(); // ensure cached
var filter = _searchFilter?.ToLower() ?? "";
foreach (var nt in _cachedSorted)
{
if (!string.IsNullOrEmpty(filter) && !nt.Value.ToLower().Contains(filter)) continue;
var item = Instantiate(contextMenuItemPrefab, _menuContent);
var l = item.GetComponentInChildren<TMP_Text>(); if (l != null) l.text = nt.Value;
var b = item.GetComponentInChildren<Button>();
if (b != null) { var tt = nt.Key; b.onClick.AddListener(() => { CreateNode(tt, Mouse.current.position.ReadValue()); contextMenuRoot.gameObject.SetActive(false); }); }
}
}
void CreateNode(Type nodeType, Vector2 sp)
{
bool dup = nodeType != typeof(NodeStart) && nodeType != typeof(NodeEntry);
CreateNodeInstance(nodeType, sp, dup);
}
static readonly Type[] GenericExpandTypes = { typeof(float), typeof(int), typeof(Vector2), typeof(Vector3), typeof(GameElement), typeof(List<GameElement>) };
static Dictionary<Type, string> _cachedTypes;
static List<KeyValuePair<Type, string>> _cachedSorted;
static Dictionary<Type, string> GetNodeTypes()
{
if (_cachedTypes != null) return _cachedTypes;
var d = new Dictionary<Type, string>();
foreach (var a in AppDomain.CurrentDomain.GetAssemblies())
{
Type[] ts; try { ts = a.GetTypes(); } catch { continue; }
foreach (var t in ts)
{
if (t.IsAbstract || !typeof(NodeBase).IsAssignableFrom(t)) continue;
if (t.IsGenericTypeDefinition)
{ foreach (var T in GenericExpandTypes) { var c = t.MakeGenericType(T); d[c] = t.Name.Split('`')[0] + "<" + T.Name + ">"; } }
else { var nm = t.Name; if (t.IsGenericType) nm = t.Name.Split('`')[0] + "<" + string.Join(",", t.GetGenericArguments().Select(a => a.Name)) + ">"; d[t] = nm; }
}
}
_cachedTypes = d;
_cachedSorted = d.OrderBy(kv => kv.Value).ToList();
return d;
}
}
class WireClickHandler : MonoBehaviour
{
public NodeManager mgr;
public UILineRenderer line;
}
}