Files
ichni_Creator_Studio/Assets/Scripts/Editor Tools/NodeScript/NodeManager.cs
2026-05-23 21:05:16 +08:00

1179 lines
48 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}