update 0.6.3.5
This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using XericLibrary.Runtime.MacroLibrary;
|
||||
|
||||
namespace XericLibrary.Runtime.Blueprint
|
||||
{
|
||||
/// <summary>
|
||||
/// 蓝图画布渲染工具的抽象基类。
|
||||
/// <para>
|
||||
/// 职责:
|
||||
/// 1. 管理 Viewport Transform — 平移/缩放改为驱动 Transform,不触发 Mesh 重建。
|
||||
/// 2. 管理网格区块(Chunk)— 按画布坐标分块,每块一个渲染器,避开 65535 顶点限制。
|
||||
/// 3. 数据分组 — 脏元素的坐标 ÷ chunkSize → Spiral2DEncode → 按 Chunk 写入渲染器。
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// 派生类只需实现三个抽象方法:
|
||||
/// <list type="bullet">
|
||||
/// <item><see cref="BuildChunkRenderer"/> — 在 Chunk GameObject 上创建渲染器组件</item>
|
||||
/// <item><see cref="AppendElementData"/> — 把元素数据写入该 Chunk 的渲染器</item>
|
||||
/// <item><see cref="ClearChunk"/> — 清空 Chunk 渲染器,准备下一帧重写</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <typeparam name="TChildRenderer">Chunk 使用的渲染器组件类型</typeparam>
|
||||
public abstract class BlueprintCanvasRenderTool<TChildRenderer> : BlueprintTool
|
||||
where TChildRenderer : MonoBehaviour
|
||||
{
|
||||
// ====================================================================
|
||||
// 配置
|
||||
// ====================================================================
|
||||
|
||||
/// <summary>网格区块大小(画布空间单位)。一个 Chunk 覆盖 chunkSize × chunkSize 区域。</summary>
|
||||
protected int ChunkSize = 400;
|
||||
|
||||
/// <summary>Chunk 池的最大缓存数量</summary>
|
||||
protected int MaxPoolSize = 100;
|
||||
|
||||
// ====================================================================
|
||||
// Chunk 数据
|
||||
// ====================================================================
|
||||
|
||||
/// <summary>活跃区块:Spiral2DEncode ID → 渲染器实例</summary>
|
||||
protected Dictionary<ulong, TChildRenderer> ActiveChunks = new();
|
||||
|
||||
/// <summary>缓存池,减少创建/销毁</summary>
|
||||
protected Stack<TChildRenderer> ChunkPool = new();
|
||||
|
||||
/// <summary>Chunk 的父级 Transform(通常在 Viewport 下)</summary>
|
||||
protected Transform ChunksParent;
|
||||
|
||||
// ====================================================================
|
||||
// Viewport 状态
|
||||
// ====================================================================
|
||||
|
||||
/// <summary>当前同步到的 Viewport Transform</summary>
|
||||
protected RectTransform ViewportRect;
|
||||
|
||||
private Vector2 _prevPan;
|
||||
private float _prevZoom;
|
||||
|
||||
// ====================================================================
|
||||
// 抽象方法(派生类实现)
|
||||
// ====================================================================
|
||||
|
||||
/// <summary>在 Chunk GameObject 上创建渲染器组件并返回。</summary>
|
||||
protected abstract TChildRenderer BuildChunkRenderer(GameObject chunkGo);
|
||||
|
||||
/// <summary>将元素数据写入指定 Chunk 的渲染器。</summary>
|
||||
protected abstract void AppendElementData(TChildRenderer renderer, IBlueprintElement element);
|
||||
|
||||
/// <summary>清空 Chunk 渲染器的数据,准备下一帧重写。</summary>
|
||||
protected abstract void ClearChunkData(TChildRenderer renderer);
|
||||
|
||||
/// <summary>触发 Chunk 渲染器的脏标记,使其重建 Mesh。</summary>
|
||||
protected abstract void SetChunkDirty(TChildRenderer renderer);
|
||||
|
||||
/// <summary>获取元素用于 Chunk 分组的坐标(通常是 Position 或中心点)。</summary>
|
||||
protected abstract Vector2 GetElementPosition(IBlueprintElement element);
|
||||
|
||||
/// <summary>检查元素是否需要被剔除(例如不在视口内)。</summary>
|
||||
protected abstract bool IsElementCulled(IBlueprintElement element);
|
||||
|
||||
// ====================================================================
|
||||
// 生命周期
|
||||
// ====================================================================
|
||||
|
||||
public override void OnInitialize()
|
||||
{
|
||||
EnsureViewport();
|
||||
}
|
||||
|
||||
public override void OnRender()
|
||||
{
|
||||
if (Graph?.Canvas == null) return;
|
||||
|
||||
// 1. 确保层级存在
|
||||
EnsureViewport();
|
||||
|
||||
// 2. 同步 Viewport Transform(O(1),不重建 Mesh)
|
||||
SyncViewportTransform();
|
||||
|
||||
// 3. 检查是否有脏元素
|
||||
if (Graph.DirtyFlags == null || !Graph.DirtyFlags.HasDirty)
|
||||
return;
|
||||
|
||||
// 4. 收集并分组脏元素
|
||||
var groups = GroupDirtyElements();
|
||||
|
||||
if (groups.Count == 0) return;
|
||||
|
||||
// 5. 写入 Chunk 渲染器
|
||||
foreach (var kvp in groups)
|
||||
{
|
||||
var renderer = GetOrCreateChunk(kvp.Key);
|
||||
if (renderer == null) continue;
|
||||
|
||||
ClearChunkData(renderer);
|
||||
foreach (var element in kvp.Value)
|
||||
AppendElementData(renderer, element);
|
||||
|
||||
SetChunkDirty(renderer);
|
||||
}
|
||||
|
||||
// 6. 清理空 Chunk
|
||||
CleanupInactiveChunks(groups.Keys);
|
||||
}
|
||||
|
||||
public override void OnDestroy()
|
||||
{
|
||||
DestroyAllChunks();
|
||||
ViewportRect = null;
|
||||
ChunksParent = null;
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// Viewport
|
||||
// ====================================================================
|
||||
|
||||
/// <summary>获取或创建 Viewport Transform。</summary>
|
||||
protected virtual void EnsureViewport()
|
||||
{
|
||||
if (ViewportRect != null) return;
|
||||
if (Graph?.Canvas == null) return;
|
||||
|
||||
var root = Graph.Canvas.GetRootTransform();
|
||||
if (root == null) return;
|
||||
|
||||
// 查找已有的 __Bp_Viewport
|
||||
var existing = root.Find("__Bp_Viewport");
|
||||
if (existing != null)
|
||||
{
|
||||
ViewportRect = existing.GetComponent<RectTransform>();
|
||||
ChunksParent = existing;
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建 Viewport
|
||||
var vpGo = new GameObject("__Bp_Viewport", typeof(RectTransform));
|
||||
vpGo.hideFlags = HideFlags.HideAndDontSave;
|
||||
vpGo.transform.SetParent(root, false);
|
||||
vpGo.transform.SetAsFirstSibling(); // 在 GridBackground 之下
|
||||
|
||||
ViewportRect = vpGo.GetComponent<RectTransform>();
|
||||
ViewportRect.anchorMin = Vector2.zero;
|
||||
ViewportRect.anchorMax = Vector2.one;
|
||||
ViewportRect.offsetMin = Vector2.zero;
|
||||
ViewportRect.offsetMax = Vector2.zero;
|
||||
|
||||
ChunksParent = ViewportRect;
|
||||
|
||||
// 记录初始状态
|
||||
_prevPan = Graph.Canvas.PanOffset;
|
||||
_prevZoom = Graph.Canvas.ZoomLevel;
|
||||
ViewportRect.anchoredPosition = -_prevPan;
|
||||
ViewportRect.localScale = Vector3.one * _prevZoom;
|
||||
}
|
||||
|
||||
/// <summary>将画布的 Pan/Zoom 同步到 Viewport Transform。</summary>
|
||||
protected virtual void SyncViewportTransform()
|
||||
{
|
||||
if (ViewportRect == null) return;
|
||||
|
||||
var pan = Graph.Canvas.PanOffset;
|
||||
var zoom = Graph.Canvas.ZoomLevel;
|
||||
|
||||
if (_prevPan != pan)
|
||||
{
|
||||
ViewportRect.anchoredPosition = -pan;
|
||||
_prevPan = pan;
|
||||
}
|
||||
|
||||
if (!Mathf.Approximately(_prevZoom, zoom))
|
||||
{
|
||||
ViewportRect.localScale = Vector3.one * zoom;
|
||||
_prevZoom = zoom;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// Chunk 管理
|
||||
// ====================================================================
|
||||
|
||||
/// <summary>将画布空间坐标转为 Chunk 坐标。</summary>
|
||||
protected (int cx, int cy) PositionToCoord(Vector2 canvasPos)
|
||||
{
|
||||
return (
|
||||
Mathf.FloorToInt(canvasPos.x / ChunkSize),
|
||||
Mathf.FloorToInt(canvasPos.y / ChunkSize)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Chunk 坐标 → 螺旋曲线 ID。</summary>
|
||||
protected ulong CoordToID(int cx, int cy)
|
||||
{
|
||||
return MacroCurveMapping.Spiral2DEncode(cx, cy);
|
||||
}
|
||||
|
||||
/// <summary>螺旋曲线 ID → Chunk 坐标。</summary>
|
||||
protected void IDToCoord(ulong id, out int cx, out int cy)
|
||||
{
|
||||
MacroCurveMapping.Spiral2DDecode(id, out cx, out cy);
|
||||
}
|
||||
|
||||
/// <summary>获取或创建坐标对应的 Chunk 渲染器。</summary>
|
||||
protected TChildRenderer GetOrCreateChunk(ulong chunkID)
|
||||
{
|
||||
if (ActiveChunks.TryGetValue(chunkID, out var renderer) && renderer != null)
|
||||
return renderer;
|
||||
|
||||
IDToCoord(chunkID, out int cx, out int cy);
|
||||
|
||||
GameObject go;
|
||||
|
||||
// 从池中取或新建
|
||||
if (ChunkPool.Count > 0)
|
||||
{
|
||||
go = ChunkPool.Pop().gameObject;
|
||||
go.SetActive(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
go = new GameObject($"Chunk_{cx}_{cy}", typeof(RectTransform));
|
||||
go.hideFlags = HideFlags.HideAndDontSave;
|
||||
go.transform.SetParent(ChunksParent, false);
|
||||
}
|
||||
|
||||
// 定位 Chunk
|
||||
var rt = go.GetComponent<RectTransform>();
|
||||
rt.pivot = Vector2.zero; // 左下角为原点
|
||||
rt.anchorMin = Vector2.zero;
|
||||
rt.anchorMax = Vector2.zero; // 不拉伸,绝对定位
|
||||
rt.anchoredPosition = new Vector2(cx * ChunkSize, cy * ChunkSize);
|
||||
rt.sizeDelta = new Vector2(ChunkSize, ChunkSize);
|
||||
|
||||
// 获取或创建渲染器
|
||||
renderer = go.GetComponent<TChildRenderer>();
|
||||
if (renderer == null)
|
||||
renderer = BuildChunkRenderer(go);
|
||||
|
||||
ActiveChunks[chunkID] = renderer;
|
||||
return renderer;
|
||||
}
|
||||
|
||||
/// <summary>收集脏元素并按 Chunk 分组。</summary>
|
||||
protected virtual Dictionary<ulong, List<IBlueprintElement>> GroupDirtyElements()
|
||||
{
|
||||
var groups = new Dictionary<ulong, List<IBlueprintElement>>();
|
||||
var dirtyList = Graph.DirtyFlags.FlushDirty();
|
||||
|
||||
foreach (var element in dirtyList)
|
||||
{
|
||||
if (element == null) continue;
|
||||
|
||||
// 剔除
|
||||
if (IsElementCulled(element)) continue;
|
||||
|
||||
var pos = GetElementPosition(element);
|
||||
var (cx, cy) = PositionToCoord(pos);
|
||||
ulong id = CoordToID(cx, cy);
|
||||
|
||||
if (!groups.TryGetValue(id, out var list))
|
||||
{
|
||||
list = new List<IBlueprintElement>();
|
||||
groups[id] = list;
|
||||
}
|
||||
list.Add(element);
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
/// <summary>清理不再包含脏元素的 Chunk。</summary>
|
||||
protected virtual void CleanupInactiveChunks(Dictionary<ulong, List<IBlueprintElement>>.KeyCollection activeIDs)
|
||||
{
|
||||
var toRemove = new List<ulong>();
|
||||
foreach (var kvp in ActiveChunks)
|
||||
{
|
||||
if (!System.Linq.Enumerable.Contains(activeIDs, kvp.Key))
|
||||
toRemove.Add(kvp.Key);
|
||||
}
|
||||
|
||||
foreach (var id in toRemove)
|
||||
{
|
||||
if (!ActiveChunks.TryGetValue(id, out var renderer) || renderer == null)
|
||||
continue;
|
||||
|
||||
// 入池或销毁
|
||||
if (ChunkPool.Count < MaxPoolSize)
|
||||
{
|
||||
renderer.gameObject.SetActive(false);
|
||||
ChunkPool.Push(renderer);
|
||||
}
|
||||
else
|
||||
{
|
||||
UnityEngine.Object.Destroy(renderer.gameObject);
|
||||
}
|
||||
|
||||
ActiveChunks.Remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>销毁所有 Chunk。</summary>
|
||||
protected virtual void DestroyAllChunks()
|
||||
{
|
||||
foreach (var kvp in ActiveChunks)
|
||||
{
|
||||
if (kvp.Value != null)
|
||||
UnityEngine.Object.Destroy(kvp.Value.gameObject);
|
||||
}
|
||||
ActiveChunks.Clear();
|
||||
|
||||
while (ChunkPool.Count > 0)
|
||||
{
|
||||
var pooled = ChunkPool.Pop();
|
||||
if (pooled != null)
|
||||
UnityEngine.Object.Destroy(pooled.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b98af5cb1bdbbcf4d8b60e620f6b2fdb
|
||||
guid: 4938c1e0b3dda58429017cf4e45f1d5b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
@@ -16,10 +16,7 @@ namespace XericLibrary.Runtime.Blueprint.Render
|
||||
public class QuickGraphNodeRenderConfig : BlueprintToolConfigBase
|
||||
{
|
||||
/// <summary>目标工具类型</summary>
|
||||
public override System.Type TargetToolType
|
||||
{
|
||||
get { return typeof(QuickUGUIGraphNodeRenderTool); }
|
||||
}
|
||||
public override System.Type TargetToolType => typeof(NodeCanvasRenderTool);
|
||||
|
||||
[Header("尺寸")]
|
||||
[Tooltip("节点宽度(像素)")]
|
||||
|
||||
@@ -17,10 +17,7 @@ namespace XericLibrary.Runtime.Blueprint.Render
|
||||
public class QuickGraphWireRenderConfig : BlueprintToolConfigBase
|
||||
{
|
||||
/// <summary>目标工具类型</summary>
|
||||
public override System.Type TargetToolType
|
||||
{
|
||||
get { return typeof(QuickUGUIGraphWireRenderTool); }
|
||||
}
|
||||
public override System.Type TargetToolType => typeof(WireCanvasRenderTool);
|
||||
|
||||
[Header("连线")]
|
||||
[Tooltip("连线宽度(像素)")]
|
||||
|
||||
@@ -1,275 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace XericLibrary.Runtime.Blueprint.Render
|
||||
{
|
||||
/// <summary>
|
||||
/// 层容器条目 —— 一层中挂载的所有渲染组件
|
||||
/// </summary>
|
||||
public sealed class LayerContainerEntry
|
||||
{
|
||||
/// <summary>容器根 GameObject,锚点撑满蓝图</summary>
|
||||
public GameObject Container;
|
||||
|
||||
/// <summary>图元渲染器(节点矩形)</summary>
|
||||
public BlueprintPrimitiveRenderer PrimitiveRenderer;
|
||||
|
||||
/// <summary>曲线渲染器(连线)</summary>
|
||||
public BlueprintCurveRenderer CurveRenderer;
|
||||
|
||||
/// <summary>节点 → TMP Text 映射(挂在此容器下)</summary>
|
||||
public Dictionary<BlueprintNode, TMPro.TMP_Text> NodeTexts
|
||||
= new Dictionary<BlueprintNode, TMPro.TMP_Text>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 共享层容器管理器 —— 管理桶排序中每一层的容器 GameObject。
|
||||
/// 一层一个容器,内部按 sibling 顺序挂载:
|
||||
/// 1. Image 背景(由 GridBackgroundTool 处理,挂 root 下)
|
||||
/// 2. __Curve(连线)
|
||||
/// 3. __Primitive(节点矩形)
|
||||
/// 4. TMP Text(节点文字)
|
||||
/// 各渲染工具通过此管理器共享同一组层容器。
|
||||
///
|
||||
/// 所有创建的 GameObject 默认 HideFlags.HideAndDontSave,
|
||||
/// 避免在编辑器→运行/运行→编辑器切换时残留场景。
|
||||
/// 调试时可通过 GraphTheoryBlueprintComponent._showDebugObjects 强制显示。
|
||||
/// </summary>
|
||||
public class LayerContainerManager
|
||||
{
|
||||
/// <summary>所有蓝图实例共享的层管理器(蓝图实例 → 层管理器)</summary>
|
||||
private static Dictionary<BlueprintGraph, LayerContainerManager> s_Instances
|
||||
= new Dictionary<BlueprintGraph, LayerContainerManager>();
|
||||
|
||||
/// <summary>获取或创建指定蓝图实例的层容器管理器</summary>
|
||||
public static LayerContainerManager GetForGraph(BlueprintGraph graph, Transform renderRoot)
|
||||
{
|
||||
if (s_Instances.TryGetValue(graph, out var mgr) && mgr != null)
|
||||
return mgr;
|
||||
|
||||
mgr = new LayerContainerManager(renderRoot);
|
||||
s_Instances[graph] = mgr;
|
||||
return mgr;
|
||||
}
|
||||
|
||||
/// <summary>释放蓝图实例的层容器</summary>
|
||||
public static void ReleaseForGraph(BlueprintGraph graph)
|
||||
{
|
||||
if (s_Instances.TryGetValue(graph, out var mgr))
|
||||
{
|
||||
mgr.DestroyAll();
|
||||
s_Instances.Remove(graph);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly Transform _renderRoot;
|
||||
private readonly Dictionary<int, LayerContainerEntry> _layers
|
||||
= new Dictionary<int, LayerContainerEntry>();
|
||||
|
||||
private LayerContainerManager(Transform renderRoot)
|
||||
{
|
||||
_renderRoot = renderRoot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定层级的容器条目。不存在则创建。
|
||||
/// </summary>
|
||||
public LayerContainerEntry GetOrCreateLayer(int layer)
|
||||
{
|
||||
if (_layers.TryGetValue(layer, out var entry) && entry != null)
|
||||
return entry;
|
||||
|
||||
// --- 创建容器 ---
|
||||
var container = new GameObject($"__BpLayer_{layer:D2}", typeof(RectTransform));
|
||||
container.hideFlags = HideFlags.HideAndDontSave;
|
||||
container.transform.SetParent(_renderRoot, false);
|
||||
var ctRt = container.GetComponent<RectTransform>();
|
||||
ctRt.anchorMin = Vector2.zero;
|
||||
ctRt.anchorMax = Vector2.one;
|
||||
ctRt.offsetMin = Vector2.zero;
|
||||
ctRt.offsetMax = Vector2.zero;
|
||||
|
||||
entry = new LayerContainerEntry { Container = container };
|
||||
|
||||
// --- 创建连线渲染器(底层) ---
|
||||
var curveGo = new GameObject("__Curve", typeof(RectTransform));
|
||||
curveGo.hideFlags = HideFlags.HideAndDontSave;
|
||||
curveGo.transform.SetParent(container.transform, false);
|
||||
var crRt = curveGo.GetComponent<RectTransform>();
|
||||
crRt.anchorMin = Vector2.zero;
|
||||
crRt.anchorMax = Vector2.one;
|
||||
crRt.offsetMin = Vector2.zero;
|
||||
crRt.offsetMax = Vector2.zero;
|
||||
entry.CurveRenderer = curveGo.AddComponent<BlueprintCurveRenderer>();
|
||||
|
||||
// --- 创建图元渲染器(中层) ---
|
||||
var primGo = new GameObject("__Primitive", typeof(RectTransform));
|
||||
primGo.hideFlags = HideFlags.HideAndDontSave;
|
||||
primGo.transform.SetParent(container.transform, false);
|
||||
var prRt = primGo.GetComponent<RectTransform>();
|
||||
prRt.anchorMin = Vector2.zero;
|
||||
prRt.anchorMax = Vector2.one;
|
||||
prRt.offsetMin = Vector2.zero;
|
||||
prRt.offsetMax = Vector2.zero;
|
||||
entry.PrimitiveRenderer = primGo.AddComponent<BlueprintPrimitiveRenderer>();
|
||||
|
||||
_layers[layer] = entry;
|
||||
ReorderAllContainers();
|
||||
return entry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试获取已存在的层容器(不创建)
|
||||
/// </summary>
|
||||
public LayerContainerEntry TryGetLayer(int layer)
|
||||
{
|
||||
_layers.TryGetValue(layer, out var entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或创建节点文本(挂到指定层容器下)
|
||||
/// </summary>
|
||||
public TMPro.TMP_Text GetOrCreateNodeText(BlueprintNode node, int layer)
|
||||
{
|
||||
var entry = GetOrCreateLayer(layer);
|
||||
if (entry.NodeTexts.TryGetValue(node, out var text) && text != null)
|
||||
return text;
|
||||
|
||||
var textGo = new GameObject($"__Text_{node.ElementId}", typeof(RectTransform));
|
||||
textGo.hideFlags = HideFlags.HideAndDontSave;
|
||||
textGo.transform.SetParent(entry.Container.transform, false);
|
||||
|
||||
var tmp = textGo.AddComponent<TMPro.TextMeshProUGUI>();
|
||||
tmp.alignment = TMPro.TextAlignmentOptions.Center;
|
||||
tmp.fontSize = 12;
|
||||
tmp.enableAutoSizing = false;
|
||||
|
||||
var rt = tmp.rectTransform;
|
||||
rt.pivot = new Vector2(0.5f, 0.5f);
|
||||
rt.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
rt.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
|
||||
// 放到该层最末尾(覆盖矩形之上)
|
||||
tmp.rectTransform.SetAsLastSibling();
|
||||
|
||||
entry.NodeTexts[node] = tmp;
|
||||
return tmp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试获取已存在的节点文本对象,不存在则返回 null。
|
||||
/// </summary>
|
||||
public TMPro.TMP_Text TryGetNodeText(BlueprintNode node, int layer)
|
||||
{
|
||||
if (!_layers.TryGetValue(layer, out var entry) || entry == null) return null;
|
||||
entry.NodeTexts.TryGetValue(node, out var text);
|
||||
return text;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除指定层容器的所有渲染内容(不清除容器结构)
|
||||
/// </summary>
|
||||
public void ClearLayer(int layer)
|
||||
{
|
||||
if (!_layers.TryGetValue(layer, out var entry) || entry == null) return;
|
||||
|
||||
entry.PrimitiveRenderer?.ClearAll();
|
||||
entry.CurveRenderer?.ClearAll();
|
||||
|
||||
foreach (var kvp in entry.NodeTexts)
|
||||
{
|
||||
if (kvp.Value != null)
|
||||
kvp.Value.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理指定层中已不存在的节点的文本对象
|
||||
/// </summary>
|
||||
public void PruneDeadTexts(int layer, HashSet<BlueprintNode> aliveNodes)
|
||||
{
|
||||
if (!_layers.TryGetValue(layer, out var entry) || entry == null) return;
|
||||
|
||||
var dead = new List<BlueprintNode>();
|
||||
foreach (var kvp in entry.NodeTexts)
|
||||
{
|
||||
if (!aliveNodes.Contains(kvp.Key))
|
||||
dead.Add(kvp.Key);
|
||||
}
|
||||
|
||||
foreach (var node in dead)
|
||||
{
|
||||
if (entry.NodeTexts.TryGetValue(node, out var text) && text != null)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if (!Application.isPlaying)
|
||||
Object.DestroyImmediate(text.gameObject);
|
||||
else
|
||||
#endif
|
||||
Object.Destroy(text.gameObject);
|
||||
}
|
||||
entry.NodeTexts.Remove(node);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除超出活跃范围的所有层容器
|
||||
/// </summary>
|
||||
public void PruneLayersAbove(int highestActiveLayer)
|
||||
{
|
||||
var toRemove = new List<int>();
|
||||
foreach (var kvp in _layers)
|
||||
{
|
||||
if (kvp.Key > highestActiveLayer)
|
||||
toRemove.Add(kvp.Key);
|
||||
}
|
||||
|
||||
foreach (int layer in toRemove)
|
||||
{
|
||||
if (_layers.TryGetValue(layer, out var entry) && entry != null)
|
||||
{
|
||||
if (entry.Container != null)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if (!Application.isPlaying)
|
||||
Object.DestroyImmediate(entry.Container);
|
||||
else
|
||||
#endif
|
||||
Object.Destroy(entry.Container);
|
||||
}
|
||||
}
|
||||
_layers.Remove(layer);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 销毁所有层容器
|
||||
/// </summary>
|
||||
public void DestroyAll()
|
||||
{
|
||||
foreach (var kvp in _layers)
|
||||
{
|
||||
if (kvp.Value?.Container != null)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if (!Application.isPlaying)
|
||||
Object.DestroyImmediate(kvp.Value.Container);
|
||||
else
|
||||
#endif
|
||||
Object.Destroy(kvp.Value.Container);
|
||||
}
|
||||
}
|
||||
_layers.Clear();
|
||||
}
|
||||
|
||||
private void ReorderAllContainers()
|
||||
{
|
||||
var sorted = new List<KeyValuePair<int, LayerContainerEntry>>(_layers);
|
||||
sorted.Sort((a, b) => a.Key.CompareTo(b.Key));
|
||||
foreach (var kvp in sorted)
|
||||
kvp.Value.Container.transform.SetAsLastSibling();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
using XericLibrary.Runtime.UIGraph;
|
||||
using PrimitiveType = XericLibrary.Runtime.UIGraph.PrimitiveType;
|
||||
|
||||
namespace XericLibrary.Runtime.Blueprint.Render
|
||||
{
|
||||
/// <summary>
|
||||
/// 节点渲染工具 — 按网格区块管理 BlueprintPrimitiveRenderer,支持 Viewport Transform。
|
||||
/// <para>每个 Chunk 一个 PrimitiveRenderer + 每个节点一个 TMP Text(挂在该 Chunk 下)。</para>
|
||||
/// LOD 逻辑保留自 QuickUGUIGraphNodeRenderTool。
|
||||
/// </summary>
|
||||
[BlueprintTool(phase: ToolPhase.Render, order: 100)]
|
||||
[BlueprintTheme("QuickGraph")]
|
||||
public class NodeCanvasRenderTool : BlueprintCanvasRenderTool<BlueprintPrimitiveRenderer>
|
||||
{
|
||||
private enum LodLevel { Minimal, Simplified, Full }
|
||||
|
||||
private QuickGraphNodeRenderConfig _defaultConfig;
|
||||
private QuickGraphNodeRenderConfig Config
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ToolConfig is QuickGraphNodeRenderConfig external) return external;
|
||||
if (_defaultConfig == null)
|
||||
_defaultConfig = ScriptableObject.CreateInstance<QuickGraphNodeRenderConfig>();
|
||||
return _defaultConfig;
|
||||
}
|
||||
}
|
||||
|
||||
// TMP 文本:节点 → (ChunkID, Text 组件)
|
||||
private Dictionary<string, (ulong chunkID, TMP_Text text)> _nodeTexts
|
||||
= new Dictionary<string, (ulong, TMP_Text)>();
|
||||
|
||||
// ====================================================================
|
||||
// 抽象实现
|
||||
// ====================================================================
|
||||
|
||||
protected override BlueprintPrimitiveRenderer BuildChunkRenderer(GameObject chunkGo)
|
||||
{
|
||||
return chunkGo.AddComponent<BlueprintPrimitiveRenderer>();
|
||||
}
|
||||
|
||||
protected override void AppendElementData(BlueprintPrimitiveRenderer renderer, IBlueprintElement element)
|
||||
{
|
||||
if (!(element is BlueprintNode node)) return;
|
||||
|
||||
var cfg = Config;
|
||||
float zoom = Graph.Canvas.ZoomLevel;
|
||||
LodLevel lod = GetLodLevel();
|
||||
|
||||
// 端口位置始终用原始尺寸计算
|
||||
CalculatePortPositions(node, cfg.NodeWidth, cfg.NodeHeight);
|
||||
|
||||
// 节点尺寸和位置(画布空间,不转换,由 Viewport Transform 统一变换)
|
||||
float nw = cfg.NodeWidth;
|
||||
float nh = cfg.NodeHeight;
|
||||
float border = (lod == LodLevel.Minimal) ? 0f : cfg.BorderThickness;
|
||||
Vector2 pos = node.Position;
|
||||
|
||||
Color bgColor, borderColor;
|
||||
if (lod == LodLevel.Minimal)
|
||||
{
|
||||
bgColor = node.NodeColor;
|
||||
borderColor = node.NodeColor;
|
||||
}
|
||||
else
|
||||
{
|
||||
bgColor = node.NodeBGColor;
|
||||
borderColor = node.NodeColor;
|
||||
}
|
||||
|
||||
var primEntry = new PrimitiveCacheEntry
|
||||
{
|
||||
type = PrimitiveType.Rectangle,
|
||||
sizeMode = SizeMode.InscribedEllipse,
|
||||
center = pos,
|
||||
size = new Vector2(nw, nh),
|
||||
angle = 0f,
|
||||
@params = new PrimitiveParams
|
||||
{
|
||||
axisScaleX = 1f,
|
||||
axisScaleY = 1f,
|
||||
sideCount = 4,
|
||||
chamferSize = cfg.ChamferSize,
|
||||
chamferSegments = cfg.ChamferSegments,
|
||||
bgColor = new Color32(0, 0, 0, 0),
|
||||
centerColor = bgColor,
|
||||
borderColor = borderColor,
|
||||
borderThickness = border,
|
||||
}
|
||||
};
|
||||
renderer.AddPrimitive(primEntry);
|
||||
|
||||
// LOD Full 时显示文本
|
||||
if (lod == LodLevel.Full)
|
||||
UpdateNodeText(node, pos, nw, nh, border, cfg);
|
||||
else
|
||||
HideNodeText(node);
|
||||
}
|
||||
|
||||
protected override void ClearChunkData(BlueprintPrimitiveRenderer renderer)
|
||||
{
|
||||
renderer.ClearAll();
|
||||
}
|
||||
|
||||
protected override void SetChunkDirty(BlueprintPrimitiveRenderer renderer)
|
||||
{
|
||||
renderer.SetPrimitiveDirty(0);
|
||||
}
|
||||
|
||||
protected override Vector2 GetElementPosition(IBlueprintElement element)
|
||||
{
|
||||
return element is BlueprintNode node ? node.Position : Vector2.zero;
|
||||
}
|
||||
|
||||
protected override bool IsElementCulled(IBlueprintElement element)
|
||||
{
|
||||
if (!(element is BlueprintNode node)) return true;
|
||||
var viewport = Graph?.Canvas?.GetViewportRect() ?? new Rect();
|
||||
float margin = Config.NodeWidth + Config.NodeHeight;
|
||||
viewport.xMin -= margin;
|
||||
viewport.xMax += margin;
|
||||
viewport.yMin -= margin;
|
||||
viewport.yMax += margin;
|
||||
return !viewport.Contains(node.Position);
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// LOD
|
||||
// ====================================================================
|
||||
|
||||
private LodLevel GetLodLevel()
|
||||
{
|
||||
var cfg = Config;
|
||||
float min = Graph.Canvas.MinZoom;
|
||||
float max = Graph.Canvas.MaxZoom;
|
||||
float range = max - min;
|
||||
float normalized = range > 0.001f ? (Graph.Canvas.ZoomLevel - min) / range : 1f;
|
||||
|
||||
if (normalized < cfg.Lod0Threshold) return LodLevel.Minimal;
|
||||
if (normalized < cfg.Lod1Threshold) return LodLevel.Simplified;
|
||||
return LodLevel.Full;
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 端口位置(和旧渲染工具一致)
|
||||
// ====================================================================
|
||||
|
||||
private static void CalculatePortPositions(BlueprintNode node, float nw, float nh)
|
||||
{
|
||||
int inCount = node.InputPorts.Count;
|
||||
int outCount = node.OutputPorts.Count;
|
||||
|
||||
for (int i = 0; i < inCount; i++)
|
||||
{
|
||||
float y = nh * (0.5f - (float)(i + 1) / (inCount + 1));
|
||||
node.InputPorts[i].RelativePosition = new Vector2(-nw * 0.5f, y);
|
||||
}
|
||||
for (int i = 0; i < outCount; i++)
|
||||
{
|
||||
float y = nh * (0.5f - (float)(i + 1) / (outCount + 1));
|
||||
node.OutputPorts[i].RelativePosition = new Vector2(nw * 0.5f, y);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// TMP 文本 — 挂到节点所属的 Chunk 下
|
||||
// ====================================================================
|
||||
|
||||
private void UpdateNodeText(BlueprintNode node, Vector2 pos, float nw, float nh,
|
||||
float border, QuickGraphNodeRenderConfig cfg)
|
||||
{
|
||||
var (cx, cy) = PositionToCoord(pos);
|
||||
ulong chunkID = CoordToID(cx, cy);
|
||||
|
||||
if (_nodeTexts.TryGetValue(node.ElementId, out var existing))
|
||||
{
|
||||
// 节点已换 Chunk → 清理旧文本
|
||||
if (existing.chunkID != chunkID)
|
||||
{
|
||||
Object.Destroy(existing.text.gameObject);
|
||||
_nodeTexts.Remove(node.ElementId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 复用现有文本
|
||||
var tmp = existing.text;
|
||||
tmp.text = node.NodeTitle;
|
||||
tmp.color = node.NodeTextColor;
|
||||
tmp.fontSize = cfg.TitleFontSize;
|
||||
if (cfg.TitleFont != null) tmp.font = cfg.TitleFont;
|
||||
var rt = tmp.rectTransform;
|
||||
rt.anchorMin = Vector2.zero;
|
||||
rt.anchorMax = Vector2.zero;
|
||||
rt.anchoredPosition = new Vector2(pos.x - cx * ChunkSize, pos.y - cy * ChunkSize);
|
||||
rt.sizeDelta = new Vector2(nw - border * 2f, nh - border * 2f);
|
||||
rt.SetAsLastSibling();
|
||||
tmp.gameObject.SetActive(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新文本
|
||||
var chunkGo = GetOrCreateChunk(chunkID).gameObject;
|
||||
var textGo = new GameObject($"__Text_{node.ElementId}", typeof(RectTransform));
|
||||
textGo.hideFlags = HideFlags.HideAndDontSave;
|
||||
textGo.transform.SetParent(chunkGo.transform, false);
|
||||
|
||||
var tmp2 = textGo.AddComponent<TextMeshProUGUI>();
|
||||
tmp2.alignment = TextAlignmentOptions.Center;
|
||||
tmp2.fontSize = cfg.TitleFontSize;
|
||||
tmp2.enableAutoSizing = false;
|
||||
tmp2.text = node.NodeTitle;
|
||||
tmp2.color = node.NodeTextColor;
|
||||
if (cfg.TitleFont != null) tmp2.font = cfg.TitleFont;
|
||||
|
||||
var rt2 = tmp2.rectTransform;
|
||||
rt2.pivot = new Vector2(0.5f, 0.5f);
|
||||
rt2.anchorMin = Vector2.zero; // 相对 Chunk 左下角
|
||||
rt2.anchorMax = Vector2.zero;
|
||||
// Chunk 原点 = 左下角(cx*size, cy*size),文本相对 Chunk 原点偏移
|
||||
rt2.anchoredPosition = new Vector2(pos.x - cx * ChunkSize, pos.y - cy * ChunkSize);
|
||||
rt2.sizeDelta = new Vector2(nw - border * 2f, nh - border * 2f);
|
||||
rt2.SetAsLastSibling();
|
||||
|
||||
_nodeTexts[node.ElementId] = (chunkID, tmp2);
|
||||
}
|
||||
|
||||
private void HideNodeText(BlueprintNode node)
|
||||
{
|
||||
if (_nodeTexts.TryGetValue(node.ElementId, out var existing))
|
||||
{
|
||||
existing.text.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 清理(重写基类,额外清理 TMP 文本)
|
||||
// ====================================================================
|
||||
|
||||
protected override void CleanupInactiveChunks(
|
||||
Dictionary<ulong, List<IBlueprintElement>>.KeyCollection activeIDs)
|
||||
{
|
||||
// 清理空 Chunk 中的文本
|
||||
var deadNodes = new List<string>();
|
||||
foreach (var kvp in _nodeTexts)
|
||||
{
|
||||
ulong chunkID = kvp.Value.chunkID;
|
||||
if (!System.Linq.Enumerable.Contains(activeIDs, chunkID))
|
||||
deadNodes.Add(kvp.Key);
|
||||
}
|
||||
foreach (var id in deadNodes)
|
||||
{
|
||||
if (_nodeTexts.TryGetValue(id, out var entry))
|
||||
{
|
||||
Object.Destroy(entry.text.gameObject);
|
||||
_nodeTexts.Remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
base.CleanupInactiveChunks(activeIDs);
|
||||
}
|
||||
|
||||
protected override void DestroyAllChunks()
|
||||
{
|
||||
foreach (var kvp in _nodeTexts)
|
||||
{
|
||||
if (kvp.Value.text != null)
|
||||
Object.Destroy(kvp.Value.text.gameObject);
|
||||
}
|
||||
_nodeTexts.Clear();
|
||||
|
||||
base.DestroyAllChunks();
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 159fb4400ff6e5e4b859ddfd37f60a6c
|
||||
guid: 86aab959d7a9d7845b904f91e4533ba1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
@@ -1,249 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
using XericLibrary.Runtime.UIGraph;
|
||||
using PrimitiveType = XericLibrary.Runtime.UIGraph.PrimitiveType;
|
||||
|
||||
namespace XericLibrary.Runtime.Blueprint.Render
|
||||
{
|
||||
/// <summary>
|
||||
/// 快速 UGUI 图论节点样式渲染工具。
|
||||
/// 通过共享的 LayerContainerManager 管理每层容器,
|
||||
/// 计算端口相对位置并更新 TMP 文本显示。
|
||||
/// 可调参数由 QuickGraphNodeRenderConfig 配置资产提供,
|
||||
/// 拖入蓝图组件即可覆盖默认值。
|
||||
///
|
||||
/// <para>LOD 模式(由 zoom 决定):
|
||||
/// - LOD 0:节点纯色填充(bg=borderColor),无边框无文本
|
||||
/// - LOD 1:正常节点+边框,无文本
|
||||
/// - LOD 2:完整渲染(含文本)
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[BlueprintTool(phase: ToolPhase.Render, order: 100)]
|
||||
[BlueprintTheme("QuickGraph")]
|
||||
public class QuickUGUIGraphNodeRenderTool : BlueprintTool
|
||||
{
|
||||
private enum LodLevel { Minimal, Simplified, Full }
|
||||
|
||||
// --- 默认配置(代码内建,ScriptableObject.CreateInstance 创建) ---
|
||||
private QuickGraphNodeRenderConfig _defaultConfig;
|
||||
|
||||
// --- 当前生效的配置 ---
|
||||
public QuickGraphNodeRenderConfig Config
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ToolConfig is QuickGraphNodeRenderConfig external)
|
||||
return external;
|
||||
if (_defaultConfig == null)
|
||||
_defaultConfig = ScriptableObject.CreateInstance<QuickGraphNodeRenderConfig>();
|
||||
return _defaultConfig;
|
||||
}
|
||||
}
|
||||
|
||||
private Transform _renderRoot;
|
||||
private LayerContainerManager _layerMgr;
|
||||
|
||||
public override void OnRender()
|
||||
{
|
||||
if (Graph == null) return;
|
||||
EnsureRenderRoot();
|
||||
if (_renderRoot == null) return;
|
||||
|
||||
var buckets = Graph.RenderLayers;
|
||||
if (buckets == null) return;
|
||||
|
||||
_layerMgr = LayerContainerManager.GetForGraph(Graph, _renderRoot);
|
||||
|
||||
var cfg = Config;
|
||||
|
||||
// 视口裁剪:只渲染画布可见范围内的节点
|
||||
var viewport = Graph.Canvas.GetViewportRect();
|
||||
float margin = cfg.NodeWidth + cfg.NodeHeight; // 扩展一点避免边缘抖动
|
||||
viewport.xMin -= margin;
|
||||
viewport.xMax += margin;
|
||||
viewport.yMin -= margin;
|
||||
viewport.yMax += margin;
|
||||
|
||||
int bucketCount = buckets.BucketCount;
|
||||
var layerNodes = new List<BlueprintNode>[bucketCount];
|
||||
for (int i = 0; i < bucketCount; i++)
|
||||
layerNodes[i] = new List<BlueprintNode>();
|
||||
|
||||
foreach (var node in Graph.Nodes)
|
||||
{
|
||||
int layer = node.RenderLayer;
|
||||
if (layer < 0 || layer >= bucketCount) continue;
|
||||
// 视口裁剪过滤
|
||||
if (!viewport.Contains(node.Position)) continue;
|
||||
layerNodes[layer].Add(node);
|
||||
}
|
||||
|
||||
int highestActiveLayer = -1;
|
||||
for (int i = 0; i < bucketCount; i++)
|
||||
{
|
||||
if (layerNodes[i].Count > 0)
|
||||
{
|
||||
highestActiveLayer = i;
|
||||
var entry = _layerMgr.GetOrCreateLayer(i);
|
||||
RenderLayerNodes(entry, layerNodes[i], i, cfg);
|
||||
}
|
||||
else
|
||||
{
|
||||
var entry = _layerMgr.TryGetLayer(i);
|
||||
entry?.PrimitiveRenderer?.ClearAll();
|
||||
// 空层也要回收残留的 TMP 文本(被视口裁剪掉的节点)
|
||||
_layerMgr.PruneDeadTexts(i, new HashSet<BlueprintNode>());
|
||||
}
|
||||
}
|
||||
|
||||
_layerMgr.PruneLayersAbove(highestActiveLayer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据归一化缩放值计算 LOD 等级。
|
||||
/// normalizedZoom = (zoom - MinZoom) / (MaxZoom - MinZoom),将 zoom 映射到 [0, 1]。
|
||||
/// </summary>
|
||||
private LodLevel GetLodLevel(QuickGraphNodeRenderConfig cfg)
|
||||
{
|
||||
float min = Graph.Canvas.MinZoom;
|
||||
float max = Graph.Canvas.MaxZoom;
|
||||
float range = max - min;
|
||||
float normalized = range > 0.001f ? (Graph.Canvas.ZoomLevel - min) / range : 1f;
|
||||
|
||||
if (normalized < cfg.Lod0Threshold) return LodLevel.Minimal;
|
||||
if (normalized < cfg.Lod1Threshold) return LodLevel.Simplified;
|
||||
return LodLevel.Full;
|
||||
}
|
||||
|
||||
private void RenderLayerNodes(LayerContainerEntry entry, List<BlueprintNode> nodes, int layer,
|
||||
QuickGraphNodeRenderConfig cfg)
|
||||
{
|
||||
if (entry.PrimitiveRenderer != null)
|
||||
entry.PrimitiveRenderer.ClearAll();
|
||||
|
||||
var aliveNodes = new HashSet<BlueprintNode>(nodes);
|
||||
|
||||
float zoom = Graph.Canvas.ZoomLevel;
|
||||
LodLevel lod = GetLodLevel(cfg);
|
||||
|
||||
float nw = cfg.NodeWidth * zoom;
|
||||
float nh = cfg.NodeHeight * zoom;
|
||||
float border = (lod == LodLevel.Minimal) ? 0f : cfg.BorderThickness * zoom;
|
||||
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
node.NodeSize = new Vector2(nw, nh);
|
||||
// 端口始终用未缩放尺寸计算(连线系统的坐标基准)
|
||||
CalculatePortPositions(node, cfg.NodeWidth, cfg.NodeHeight);
|
||||
|
||||
var localPos = Graph.Canvas.CanvasToLocal(node.Position);
|
||||
|
||||
Color bgColor, borderColor;
|
||||
if (lod == LodLevel.Minimal)
|
||||
{
|
||||
// LOD 0:纯色块,bg = 边框色, 无边框
|
||||
bgColor = node.NodeColor;
|
||||
borderColor = node.NodeColor;
|
||||
}
|
||||
else
|
||||
{
|
||||
bgColor = node.NodeBGColor;
|
||||
borderColor = node.NodeColor;
|
||||
}
|
||||
|
||||
var primEntry = new PrimitiveCacheEntry
|
||||
{
|
||||
type = PrimitiveType.Rectangle,
|
||||
sizeMode = SizeMode.InscribedEllipse,
|
||||
center = localPos,
|
||||
size = new Vector2(nw, nh),
|
||||
angle = 0f,
|
||||
@params = new PrimitiveParams
|
||||
{
|
||||
axisScaleX = 1f,
|
||||
axisScaleY = 1f,
|
||||
sideCount = 4,
|
||||
chamferSize = cfg.ChamferSize,
|
||||
chamferSegments = cfg.ChamferSegments,
|
||||
bgColor = new Color32(0,0,0,0),
|
||||
centerColor = bgColor,
|
||||
borderColor = borderColor,
|
||||
borderThickness = border,
|
||||
}
|
||||
};
|
||||
entry.PrimitiveRenderer.AddPrimitive(primEntry);
|
||||
|
||||
// LOD >= Full 时才显示节点文本
|
||||
if (lod == LodLevel.Full)
|
||||
UpdateNodeText(node, layer, nw, nh, border, cfg);
|
||||
else
|
||||
HideNodeText(node, layer);
|
||||
}
|
||||
|
||||
entry.PrimitiveRenderer.SetPrimitiveDirty(0);
|
||||
_layerMgr.PruneDeadTexts(layer, aliveNodes);
|
||||
}
|
||||
|
||||
private static void CalculatePortPositions(BlueprintNode node, float nw, float nh)
|
||||
{
|
||||
int inCount = node.InputPorts.Count;
|
||||
int outCount = node.OutputPorts.Count;
|
||||
|
||||
for (int i = 0; i < inCount; i++)
|
||||
{
|
||||
float y = nh * (0.5f - (float)(i + 1) / (inCount + 1));
|
||||
node.InputPorts[i].RelativePosition = new Vector2(-nw * 0.5f, y);
|
||||
}
|
||||
|
||||
for (int i = 0; i < outCount; i++)
|
||||
{
|
||||
float y = nh * (0.5f - (float)(i + 1) / (outCount + 1));
|
||||
node.OutputPorts[i].RelativePosition = new Vector2(nw * 0.5f, y);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateNodeText(BlueprintNode node, int layer, float nw, float nh, float border,
|
||||
QuickGraphNodeRenderConfig cfg)
|
||||
{
|
||||
var tmp = _layerMgr.GetOrCreateNodeText(node, layer);
|
||||
float zoom = Graph.Canvas.ZoomLevel;
|
||||
|
||||
tmp.text = node.NodeTitle;
|
||||
tmp.color = node.NodeTextColor;
|
||||
tmp.fontSize = cfg.TitleFontSize * Mathf.Max(zoom, 0.5f);
|
||||
if (cfg.TitleFont != null)
|
||||
tmp.font = cfg.TitleFont;
|
||||
|
||||
var rt = tmp.rectTransform;
|
||||
rt.anchoredPosition = Graph.Canvas.CanvasToLocal(node.Position);
|
||||
rt.sizeDelta = new Vector2(nw - border * 2f, nh - border * 2f);
|
||||
|
||||
rt.SetAsLastSibling();
|
||||
tmp.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
/// <summary>隐藏节点的 TMP 文本(不销毁,复用)。</summary>
|
||||
private void HideNodeText(BlueprintNode node, int layer)
|
||||
{
|
||||
var tmp = _layerMgr.TryGetNodeText(node, layer);
|
||||
if (tmp != null)
|
||||
tmp.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
private void EnsureRenderRoot()
|
||||
{
|
||||
if (_renderRoot != null) return;
|
||||
if (Graph.Canvas == null) return;
|
||||
_renderRoot = Graph.Canvas.GetRootTransform();
|
||||
}
|
||||
|
||||
public override void OnDestroy()
|
||||
{
|
||||
if (Graph != null)
|
||||
LayerContainerManager.ReleaseForGraph(Graph);
|
||||
_layerMgr = null;
|
||||
_renderRoot = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using XericLibrary.Runtime.UIGraph;
|
||||
|
||||
namespace XericLibrary.Runtime.Blueprint.Render
|
||||
{
|
||||
/// <summary>
|
||||
/// 快速 UGUI 图论连线渲染工具。
|
||||
/// 通过共享的 LayerContainerManager 使用与节点相同的层容器,
|
||||
/// 连线绘制在两端节点所在层的较低层中(不遮挡节点)。
|
||||
/// 可调参数由 QuickGraphWireRenderConfig 配置资产提供。
|
||||
/// </summary>
|
||||
[BlueprintTool(phase: ToolPhase.Render, order: 110)]
|
||||
[BlueprintTheme("QuickGraph")]
|
||||
public class QuickUGUIGraphWireRenderTool : BlueprintTool
|
||||
{
|
||||
private QuickGraphWireRenderConfig _defaultConfig;
|
||||
|
||||
private QuickGraphWireRenderConfig Config
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ToolConfig is QuickGraphWireRenderConfig external)
|
||||
return external;
|
||||
if (_defaultConfig == null)
|
||||
_defaultConfig = ScriptableObject.CreateInstance<QuickGraphWireRenderConfig>();
|
||||
return _defaultConfig;
|
||||
}
|
||||
}
|
||||
|
||||
private Transform _renderRoot;
|
||||
private LayerContainerManager _layerMgr;
|
||||
|
||||
public override void OnRender()
|
||||
{
|
||||
if (Graph == null) return;
|
||||
EnsureRenderRoot();
|
||||
if (_renderRoot == null) return;
|
||||
|
||||
var buckets = Graph.RenderLayers;
|
||||
if (buckets == null) return;
|
||||
|
||||
_layerMgr = LayerContainerManager.GetForGraph(Graph, _renderRoot);
|
||||
|
||||
var cfg = Config;
|
||||
int bucketCount = buckets.BucketCount;
|
||||
|
||||
// 视口裁剪
|
||||
var viewport = Graph.Canvas.GetViewportRect();
|
||||
float margin = cfg.WireWidth * 20f;
|
||||
viewport.xMin -= margin;
|
||||
viewport.xMax += margin;
|
||||
viewport.yMin -= margin;
|
||||
viewport.yMax += margin;
|
||||
|
||||
var layerWires = new List<BlueprintWire>[bucketCount];
|
||||
for (int i = 0; i < bucketCount; i++)
|
||||
layerWires[i] = new List<BlueprintWire>();
|
||||
|
||||
foreach (var wire in Graph.Wires)
|
||||
{
|
||||
int srcLayer = wire.SourcePort?.OwnerNode?.RenderLayer ?? 0;
|
||||
int tgtLayer = wire.TargetPort?.OwnerNode?.RenderLayer ?? 0;
|
||||
|
||||
int wireLayer = Mathf.Min(srcLayer, tgtLayer);
|
||||
if (wireLayer < 0 || wireLayer >= bucketCount) wireLayer = 0;
|
||||
|
||||
// 两端都不在视口内则跳过
|
||||
if (wire.SourcePort?.OwnerNode != null && wire.TargetPort?.OwnerNode != null)
|
||||
{
|
||||
if (!viewport.Contains(wire.SourcePort.OwnerNode.Position) &&
|
||||
!viewport.Contains(wire.TargetPort.OwnerNode.Position))
|
||||
continue;
|
||||
}
|
||||
|
||||
layerWires[wireLayer].Add(wire);
|
||||
}
|
||||
|
||||
for (int i = 0; i < bucketCount; i++)
|
||||
{
|
||||
if (layerWires[i].Count > 0)
|
||||
{
|
||||
var entry = _layerMgr.GetOrCreateLayer(i);
|
||||
RenderLayerWires(entry, layerWires[i], cfg, Graph.Canvas);
|
||||
}
|
||||
else
|
||||
{
|
||||
var entry = _layerMgr.TryGetLayer(i);
|
||||
entry?.CurveRenderer?.ClearAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void RenderLayerWires(LayerContainerEntry entry, List<BlueprintWire> wires,
|
||||
QuickGraphWireRenderConfig cfg, IBlueprintCanvas canvas)
|
||||
{
|
||||
entry.CurveRenderer.ClearAll();
|
||||
|
||||
float zoom = canvas.ZoomLevel;
|
||||
|
||||
foreach (var wire in wires)
|
||||
{
|
||||
if (wire.SourcePort == null || wire.TargetPort == null) continue;
|
||||
if (wire.SourcePort.OwnerNode == null || wire.TargetPort.OwnerNode == null) continue;
|
||||
|
||||
Vector2 startPos = canvas.CanvasToLocal(wire.SourcePort.GetWorldPosition());
|
||||
Vector2 endPos = canvas.CanvasToLocal(wire.TargetPort.GetWorldPosition());
|
||||
|
||||
// 手柄方向 = 端口在节点上的朝向
|
||||
// 端口在节点右侧 (RelativePosition.x > 0) → 手柄朝右 (+X)
|
||||
// 端口在节点左侧 (RelativePosition.x < 0) → 手柄朝左 (-X)
|
||||
float srcDirX = Mathf.Sign(wire.SourcePort.RelativePosition.x);
|
||||
float tgtDirX = Mathf.Sign(wire.TargetPort.RelativePosition.x);
|
||||
// 同一侧(如两个端口都在右侧)则方向取反
|
||||
if (srcDirX == tgtDirX) srcDirX *= -1f;
|
||||
|
||||
// 手柄距离 = 配置值 × zoom(画布单位 → local 坐标,与 CanvasToLocal 一致)
|
||||
Vector2 handle1 = startPos + new Vector2(srcDirX * cfg.SourceHandleLength * zoom, 0f);
|
||||
Vector2 handle2 = endPos + new Vector2(tgtDirX * cfg.TargetHandleLength * zoom, 0f);
|
||||
|
||||
float ww = cfg.WireWidth * zoom;
|
||||
|
||||
var ctrlPts = new Vector3[]
|
||||
{
|
||||
new Vector3(startPos.x, startPos.y, ww),
|
||||
new Vector3(handle1.x, handle1.y, ww),
|
||||
new Vector3(handle2.x, handle2.y, ww),
|
||||
new Vector3(endPos.x, endPos.y, ww),
|
||||
};
|
||||
|
||||
Color32 srcColor = wire.SourcePort.OwnerNode.NodeColor;
|
||||
Color32 tgtColor = wire.TargetPort.OwnerNode.NodeColor;
|
||||
|
||||
var arrow = new ArrowHeadData
|
||||
{
|
||||
shape = cfg.ArrowShape,
|
||||
reversed = cfg.ArrowReversed,
|
||||
progress = cfg.ArrowProgress,
|
||||
width = cfg.ArrowWidth * zoom,
|
||||
height = cfg.ArrowHeight * zoom,
|
||||
depthCompensation = cfg.ArrowDepthCompensation,
|
||||
color = tgtColor,
|
||||
};
|
||||
|
||||
int count = ctrlPts.Length;
|
||||
var ctrlPts2 = new Vector2[count];
|
||||
var widths = new float[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
ctrlPts2[i] = ctrlPts[i];
|
||||
widths[i] = ctrlPts[i].z;
|
||||
}
|
||||
|
||||
var curveEntry = new CurveCacheEntry
|
||||
{
|
||||
startColor = srcColor,
|
||||
endColor = tgtColor,
|
||||
tessellationSegments = cfg.TessellationSegments,
|
||||
};
|
||||
|
||||
entry.CurveRenderer.AddCurve(curveEntry, ctrlPts2, widths, new[] { arrow });
|
||||
}
|
||||
|
||||
if (wires.Count > 0)
|
||||
entry.CurveRenderer.RebuildAll();
|
||||
}
|
||||
|
||||
private void EnsureRenderRoot()
|
||||
{
|
||||
if (_renderRoot != null) return;
|
||||
if (Graph.Canvas == null) return;
|
||||
_renderRoot = Graph.Canvas.GetRootTransform();
|
||||
}
|
||||
|
||||
public override void OnDestroy()
|
||||
{
|
||||
_layerMgr = null;
|
||||
_renderRoot = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using XericLibrary.Runtime.UIGraph;
|
||||
|
||||
namespace XericLibrary.Runtime.Blueprint.Render
|
||||
{
|
||||
/// <summary>
|
||||
/// 连线渲染工具 — 按网格区块管理 BlueprintCurveRenderer,支持 Viewport Transform。
|
||||
/// <para>连线跟随其源节点(SourcePort.OwnerNode)进入对应 Chunk 的 CurveRenderer。</para>
|
||||
/// </summary>
|
||||
[BlueprintTool(phase: ToolPhase.Render, order: 110)]
|
||||
[BlueprintTheme("QuickGraph")]
|
||||
public class WireCanvasRenderTool : BlueprintCanvasRenderTool<BlueprintCurveRenderer>
|
||||
{
|
||||
private QuickGraphWireRenderConfig _defaultConfig;
|
||||
private QuickGraphWireRenderConfig Config
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ToolConfig is QuickGraphWireRenderConfig external) return external;
|
||||
if (_defaultConfig == null)
|
||||
_defaultConfig = ScriptableObject.CreateInstance<QuickGraphWireRenderConfig>();
|
||||
return _defaultConfig;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 抽象实现
|
||||
// ====================================================================
|
||||
|
||||
protected override BlueprintCurveRenderer BuildChunkRenderer(GameObject chunkGo)
|
||||
{
|
||||
return chunkGo.AddComponent<BlueprintCurveRenderer>();
|
||||
}
|
||||
|
||||
protected override void AppendElementData(BlueprintCurveRenderer renderer, IBlueprintElement element)
|
||||
{
|
||||
if (!(element is BlueprintWire wire)) return;
|
||||
if (wire.SourcePort == null || wire.TargetPort == null) return;
|
||||
if (wire.SourcePort.OwnerNode == null || wire.TargetPort.OwnerNode == null) return;
|
||||
|
||||
var cfg = Config;
|
||||
|
||||
// 端点:画布空间坐标(由 Viewport Transform 统一变换)
|
||||
Vector2 startPos = wire.SourcePort.GetWorldPosition();
|
||||
Vector2 endPos = wire.TargetPort.GetWorldPosition();
|
||||
|
||||
// 手柄方向 = 端口法向
|
||||
float srcDirX = Mathf.Sign(wire.SourcePort.RelativePosition.x);
|
||||
float tgtDirX = Mathf.Sign(wire.TargetPort.RelativePosition.x);
|
||||
if (srcDirX == tgtDirX) srcDirX *= -1f;
|
||||
|
||||
// 手柄长度(原始值,由 Viewport 统一缩放)
|
||||
Vector2 handle1 = startPos + new Vector2(srcDirX * cfg.SourceHandleLength, 0f);
|
||||
Vector2 handle2 = endPos + new Vector2(tgtDirX * cfg.TargetHandleLength, 0f);
|
||||
|
||||
float ww = cfg.WireWidth;
|
||||
|
||||
var ctrlPts = new Vector3[]
|
||||
{
|
||||
new Vector3(startPos.x, startPos.y, ww),
|
||||
new Vector3(handle1.x, handle1.y, ww),
|
||||
new Vector3(handle2.x, handle2.y, ww),
|
||||
new Vector3(endPos.x, endPos.y, ww),
|
||||
};
|
||||
|
||||
Color32 srcColor = wire.SourcePort.OwnerNode.NodeColor;
|
||||
Color32 tgtColor = wire.TargetPort.OwnerNode.NodeColor;
|
||||
|
||||
var arrow = new ArrowHeadData
|
||||
{
|
||||
shape = cfg.ArrowShape,
|
||||
reversed = cfg.ArrowReversed,
|
||||
progress = cfg.ArrowProgress,
|
||||
width = cfg.ArrowWidth,
|
||||
height = cfg.ArrowHeight,
|
||||
depthCompensation = cfg.ArrowDepthCompensation,
|
||||
color = tgtColor,
|
||||
};
|
||||
|
||||
int count = ctrlPts.Length;
|
||||
var ctrlPts2 = new Vector2[count];
|
||||
var widths = new float[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
ctrlPts2[i] = ctrlPts[i];
|
||||
widths[i] = ctrlPts[i].z;
|
||||
}
|
||||
|
||||
var curveEntry = new CurveCacheEntry
|
||||
{
|
||||
startColor = srcColor,
|
||||
endColor = tgtColor,
|
||||
tessellationSegments = cfg.TessellationSegments,
|
||||
};
|
||||
|
||||
renderer.AddCurve(curveEntry, ctrlPts2, widths, new[] { arrow });
|
||||
}
|
||||
|
||||
protected override void ClearChunkData(BlueprintCurveRenderer renderer)
|
||||
{
|
||||
renderer.ClearAll();
|
||||
}
|
||||
|
||||
protected override void SetChunkDirty(BlueprintCurveRenderer renderer)
|
||||
{
|
||||
renderer.RebuildAll();
|
||||
}
|
||||
|
||||
protected override Vector2 GetElementPosition(IBlueprintElement element)
|
||||
{
|
||||
if (!(element is BlueprintWire wire)) return Vector2.zero;
|
||||
// 以源节点的位置作为 Chunk 分组依据
|
||||
return wire.SourcePort?.OwnerNode?.Position ?? Vector2.zero;
|
||||
}
|
||||
|
||||
protected override bool IsElementCulled(IBlueprintElement element)
|
||||
{
|
||||
if (!(element is BlueprintWire wire)) return true;
|
||||
if (wire.SourcePort?.OwnerNode == null || wire.TargetPort?.OwnerNode == null)
|
||||
return true;
|
||||
|
||||
var viewport = Graph?.Canvas?.GetViewportRect() ?? new Rect();
|
||||
float margin = Config.WireWidth * 20f;
|
||||
viewport.xMin -= margin;
|
||||
viewport.xMax += margin;
|
||||
viewport.yMin -= margin;
|
||||
viewport.yMax += margin;
|
||||
|
||||
// 两端都不在视口内则跳过
|
||||
return !viewport.Contains(wire.SourcePort.OwnerNode.Position)
|
||||
&& !viewport.Contains(wire.TargetPort.OwnerNode.Position);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 552d9e16260f6704f8a3da72908af509
|
||||
guid: 3881805202e8cd049aad2d55c9f595f1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
Reference in New Issue
Block a user