1179 lines
48 KiB
C#
1179 lines
48 KiB
C#
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;
|
||
}
|
||
}
|