diff --git a/Runtime/Blueprint/Render/BlueprintCanvasRenderTool.cs b/Runtime/Blueprint/Render/BlueprintCanvasRenderTool.cs
new file mode 100644
index 0000000..4f50e8a
--- /dev/null
+++ b/Runtime/Blueprint/Render/BlueprintCanvasRenderTool.cs
@@ -0,0 +1,341 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+using XericLibrary.Runtime.MacroLibrary;
+
+namespace XericLibrary.Runtime.Blueprint
+{
+ ///
+ /// 蓝图画布渲染工具的抽象基类。
+ ///
+ /// 职责:
+ /// 1. 管理 Viewport Transform — 平移/缩放改为驱动 Transform,不触发 Mesh 重建。
+ /// 2. 管理网格区块(Chunk)— 按画布坐标分块,每块一个渲染器,避开 65535 顶点限制。
+ /// 3. 数据分组 — 脏元素的坐标 ÷ chunkSize → Spiral2DEncode → 按 Chunk 写入渲染器。
+ ///
+ ///
+ /// 派生类只需实现三个抽象方法:
+ ///
+ /// - — 在 Chunk GameObject 上创建渲染器组件
+ /// - — 把元素数据写入该 Chunk 的渲染器
+ /// - — 清空 Chunk 渲染器,准备下一帧重写
+ ///
+ ///
+ ///
+ /// Chunk 使用的渲染器组件类型
+ public abstract class BlueprintCanvasRenderTool : BlueprintTool
+ where TChildRenderer : MonoBehaviour
+ {
+ // ====================================================================
+ // 配置
+ // ====================================================================
+
+ /// 网格区块大小(画布空间单位)。一个 Chunk 覆盖 chunkSize × chunkSize 区域。
+ protected int ChunkSize = 400;
+
+ /// Chunk 池的最大缓存数量
+ protected int MaxPoolSize = 100;
+
+ // ====================================================================
+ // Chunk 数据
+ // ====================================================================
+
+ /// 活跃区块:Spiral2DEncode ID → 渲染器实例
+ protected Dictionary ActiveChunks = new();
+
+ /// 缓存池,减少创建/销毁
+ protected Stack ChunkPool = new();
+
+ /// Chunk 的父级 Transform(通常在 Viewport 下)
+ protected Transform ChunksParent;
+
+ // ====================================================================
+ // Viewport 状态
+ // ====================================================================
+
+ /// 当前同步到的 Viewport Transform
+ protected RectTransform ViewportRect;
+
+ private Vector2 _prevPan;
+ private float _prevZoom;
+
+ // ====================================================================
+ // 抽象方法(派生类实现)
+ // ====================================================================
+
+ /// 在 Chunk GameObject 上创建渲染器组件并返回。
+ protected abstract TChildRenderer BuildChunkRenderer(GameObject chunkGo);
+
+ /// 将元素数据写入指定 Chunk 的渲染器。
+ protected abstract void AppendElementData(TChildRenderer renderer, IBlueprintElement element);
+
+ /// 清空 Chunk 渲染器的数据,准备下一帧重写。
+ protected abstract void ClearChunkData(TChildRenderer renderer);
+
+ /// 触发 Chunk 渲染器的脏标记,使其重建 Mesh。
+ protected abstract void SetChunkDirty(TChildRenderer renderer);
+
+ /// 获取元素用于 Chunk 分组的坐标(通常是 Position 或中心点)。
+ protected abstract Vector2 GetElementPosition(IBlueprintElement element);
+
+ /// 检查元素是否需要被剔除(例如不在视口内)。
+ 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
+ // ====================================================================
+
+ /// 获取或创建 Viewport Transform。
+ 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();
+ 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();
+ 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;
+ }
+
+ /// 将画布的 Pan/Zoom 同步到 Viewport Transform。
+ 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 管理
+ // ====================================================================
+
+ /// 将画布空间坐标转为 Chunk 坐标。
+ protected (int cx, int cy) PositionToCoord(Vector2 canvasPos)
+ {
+ return (
+ Mathf.FloorToInt(canvasPos.x / ChunkSize),
+ Mathf.FloorToInt(canvasPos.y / ChunkSize)
+ );
+ }
+
+ /// Chunk 坐标 → 螺旋曲线 ID。
+ protected ulong CoordToID(int cx, int cy)
+ {
+ return MacroCurveMapping.Spiral2DEncode(cx, cy);
+ }
+
+ /// 螺旋曲线 ID → Chunk 坐标。
+ protected void IDToCoord(ulong id, out int cx, out int cy)
+ {
+ MacroCurveMapping.Spiral2DDecode(id, out cx, out cy);
+ }
+
+ /// 获取或创建坐标对应的 Chunk 渲染器。
+ 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();
+ 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();
+ if (renderer == null)
+ renderer = BuildChunkRenderer(go);
+
+ ActiveChunks[chunkID] = renderer;
+ return renderer;
+ }
+
+ /// 收集脏元素并按 Chunk 分组。
+ protected virtual Dictionary> GroupDirtyElements()
+ {
+ var groups = new Dictionary>();
+ 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();
+ groups[id] = list;
+ }
+ list.Add(element);
+ }
+
+ return groups;
+ }
+
+ /// 清理不再包含脏元素的 Chunk。
+ protected virtual void CleanupInactiveChunks(Dictionary>.KeyCollection activeIDs)
+ {
+ var toRemove = new List();
+ 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);
+ }
+ }
+
+ /// 销毁所有 Chunk。
+ 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);
+ }
+ }
+ }
+}
diff --git a/Runtime/Blueprint/Render/QuickUGUIGraphWireRenderTool.cs.meta b/Runtime/Blueprint/Render/BlueprintCanvasRenderTool.cs.meta
similarity index 83%
rename from Runtime/Blueprint/Render/QuickUGUIGraphWireRenderTool.cs.meta
rename to Runtime/Blueprint/Render/BlueprintCanvasRenderTool.cs.meta
index 504d661..6632c68 100644
--- a/Runtime/Blueprint/Render/QuickUGUIGraphWireRenderTool.cs.meta
+++ b/Runtime/Blueprint/Render/BlueprintCanvasRenderTool.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: b98af5cb1bdbbcf4d8b60e620f6b2fdb
+guid: 4938c1e0b3dda58429017cf4e45f1d5b
MonoImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/Runtime/Blueprint/Render/Config/QuickGraphNodeRenderConfig.cs b/Runtime/Blueprint/Render/Config/QuickGraphNodeRenderConfig.cs
index f191bfd..167cea9 100644
--- a/Runtime/Blueprint/Render/Config/QuickGraphNodeRenderConfig.cs
+++ b/Runtime/Blueprint/Render/Config/QuickGraphNodeRenderConfig.cs
@@ -16,10 +16,7 @@ namespace XericLibrary.Runtime.Blueprint.Render
public class QuickGraphNodeRenderConfig : BlueprintToolConfigBase
{
/// 目标工具类型
- public override System.Type TargetToolType
- {
- get { return typeof(QuickUGUIGraphNodeRenderTool); }
- }
+ public override System.Type TargetToolType => typeof(NodeCanvasRenderTool);
[Header("尺寸")]
[Tooltip("节点宽度(像素)")]
diff --git a/Runtime/Blueprint/Render/Config/QuickGraphWireRenderConfig.cs b/Runtime/Blueprint/Render/Config/QuickGraphWireRenderConfig.cs
index c295213..e6c9e1a 100644
--- a/Runtime/Blueprint/Render/Config/QuickGraphWireRenderConfig.cs
+++ b/Runtime/Blueprint/Render/Config/QuickGraphWireRenderConfig.cs
@@ -17,10 +17,7 @@ namespace XericLibrary.Runtime.Blueprint.Render
public class QuickGraphWireRenderConfig : BlueprintToolConfigBase
{
/// 目标工具类型
- public override System.Type TargetToolType
- {
- get { return typeof(QuickUGUIGraphWireRenderTool); }
- }
+ public override System.Type TargetToolType => typeof(WireCanvasRenderTool);
[Header("连线")]
[Tooltip("连线宽度(像素)")]
diff --git a/Runtime/Blueprint/Render/LayerContainerManager.cs b/Runtime/Blueprint/Render/LayerContainerManager.cs
deleted file mode 100644
index c86796f..0000000
--- a/Runtime/Blueprint/Render/LayerContainerManager.cs
+++ /dev/null
@@ -1,275 +0,0 @@
-using System.Collections.Generic;
-using UnityEngine;
-using UnityEngine.UI;
-
-namespace XericLibrary.Runtime.Blueprint.Render
-{
- ///
- /// 层容器条目 —— 一层中挂载的所有渲染组件
- ///
- public sealed class LayerContainerEntry
- {
- /// 容器根 GameObject,锚点撑满蓝图
- public GameObject Container;
-
- /// 图元渲染器(节点矩形)
- public BlueprintPrimitiveRenderer PrimitiveRenderer;
-
- /// 曲线渲染器(连线)
- public BlueprintCurveRenderer CurveRenderer;
-
- /// 节点 → TMP Text 映射(挂在此容器下)
- public Dictionary NodeTexts
- = new Dictionary();
- }
-
- ///
- /// 共享层容器管理器 —— 管理桶排序中每一层的容器 GameObject。
- /// 一层一个容器,内部按 sibling 顺序挂载:
- /// 1. Image 背景(由 GridBackgroundTool 处理,挂 root 下)
- /// 2. __Curve(连线)
- /// 3. __Primitive(节点矩形)
- /// 4. TMP Text(节点文字)
- /// 各渲染工具通过此管理器共享同一组层容器。
- ///
- /// 所有创建的 GameObject 默认 HideFlags.HideAndDontSave,
- /// 避免在编辑器→运行/运行→编辑器切换时残留场景。
- /// 调试时可通过 GraphTheoryBlueprintComponent._showDebugObjects 强制显示。
- ///
- public class LayerContainerManager
- {
- /// 所有蓝图实例共享的层管理器(蓝图实例 → 层管理器)
- private static Dictionary s_Instances
- = new Dictionary();
-
- /// 获取或创建指定蓝图实例的层容器管理器
- 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;
- }
-
- /// 释放蓝图实例的层容器
- 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 _layers
- = new Dictionary();
-
- private LayerContainerManager(Transform renderRoot)
- {
- _renderRoot = renderRoot;
- }
-
- ///
- /// 获取指定层级的容器条目。不存在则创建。
- ///
- 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();
- 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();
- crRt.anchorMin = Vector2.zero;
- crRt.anchorMax = Vector2.one;
- crRt.offsetMin = Vector2.zero;
- crRt.offsetMax = Vector2.zero;
- entry.CurveRenderer = curveGo.AddComponent();
-
- // --- 创建图元渲染器(中层) ---
- var primGo = new GameObject("__Primitive", typeof(RectTransform));
- primGo.hideFlags = HideFlags.HideAndDontSave;
- primGo.transform.SetParent(container.transform, false);
- var prRt = primGo.GetComponent();
- prRt.anchorMin = Vector2.zero;
- prRt.anchorMax = Vector2.one;
- prRt.offsetMin = Vector2.zero;
- prRt.offsetMax = Vector2.zero;
- entry.PrimitiveRenderer = primGo.AddComponent();
-
- _layers[layer] = entry;
- ReorderAllContainers();
- return entry;
- }
-
- ///
- /// 尝试获取已存在的层容器(不创建)
- ///
- public LayerContainerEntry TryGetLayer(int layer)
- {
- _layers.TryGetValue(layer, out var entry);
- return entry;
- }
-
- ///
- /// 获取或创建节点文本(挂到指定层容器下)
- ///
- 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();
- 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;
- }
-
- ///
- /// 尝试获取已存在的节点文本对象,不存在则返回 null。
- ///
- 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;
- }
-
- ///
- /// 清除指定层容器的所有渲染内容(不清除容器结构)
- ///
- 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);
- }
- }
-
- ///
- /// 清理指定层中已不存在的节点的文本对象
- ///
- public void PruneDeadTexts(int layer, HashSet aliveNodes)
- {
- if (!_layers.TryGetValue(layer, out var entry) || entry == null) return;
-
- var dead = new List();
- 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);
- }
- }
-
- ///
- /// 移除超出活跃范围的所有层容器
- ///
- public void PruneLayersAbove(int highestActiveLayer)
- {
- var toRemove = new List();
- 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);
- }
- }
-
- ///
- /// 销毁所有层容器
- ///
- 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>(_layers);
- sorted.Sort((a, b) => a.Key.CompareTo(b.Key));
- foreach (var kvp in sorted)
- kvp.Value.Container.transform.SetAsLastSibling();
- }
- }
-}
diff --git a/Runtime/Blueprint/Render/NodeCanvasRenderTool.cs b/Runtime/Blueprint/Render/NodeCanvasRenderTool.cs
new file mode 100644
index 0000000..ec328d8
--- /dev/null
+++ b/Runtime/Blueprint/Render/NodeCanvasRenderTool.cs
@@ -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
+{
+ ///
+ /// 节点渲染工具 — 按网格区块管理 BlueprintPrimitiveRenderer,支持 Viewport Transform。
+ /// 每个 Chunk 一个 PrimitiveRenderer + 每个节点一个 TMP Text(挂在该 Chunk 下)。
+ /// LOD 逻辑保留自 QuickUGUIGraphNodeRenderTool。
+ ///
+ [BlueprintTool(phase: ToolPhase.Render, order: 100)]
+ [BlueprintTheme("QuickGraph")]
+ public class NodeCanvasRenderTool : BlueprintCanvasRenderTool
+ {
+ 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();
+ return _defaultConfig;
+ }
+ }
+
+ // TMP 文本:节点 → (ChunkID, Text 组件)
+ private Dictionary _nodeTexts
+ = new Dictionary();
+
+ // ====================================================================
+ // 抽象实现
+ // ====================================================================
+
+ protected override BlueprintPrimitiveRenderer BuildChunkRenderer(GameObject chunkGo)
+ {
+ return chunkGo.AddComponent();
+ }
+
+ 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();
+ 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>.KeyCollection activeIDs)
+ {
+ // 清理空 Chunk 中的文本
+ var deadNodes = new List();
+ 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();
+ }
+ }
+}
diff --git a/Runtime/Blueprint/Render/LayerContainerManager.cs.meta b/Runtime/Blueprint/Render/NodeCanvasRenderTool.cs.meta
similarity index 83%
rename from Runtime/Blueprint/Render/LayerContainerManager.cs.meta
rename to Runtime/Blueprint/Render/NodeCanvasRenderTool.cs.meta
index bed7e08..8f9e4f3 100644
--- a/Runtime/Blueprint/Render/LayerContainerManager.cs.meta
+++ b/Runtime/Blueprint/Render/NodeCanvasRenderTool.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: 159fb4400ff6e5e4b859ddfd37f60a6c
+guid: 86aab959d7a9d7845b904f91e4533ba1
MonoImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/Runtime/Blueprint/Render/QuickUGUIGraphNodeRenderTool.cs b/Runtime/Blueprint/Render/QuickUGUIGraphNodeRenderTool.cs
deleted file mode 100644
index f6d8368..0000000
--- a/Runtime/Blueprint/Render/QuickUGUIGraphNodeRenderTool.cs
+++ /dev/null
@@ -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
-{
- ///
- /// 快速 UGUI 图论节点样式渲染工具。
- /// 通过共享的 LayerContainerManager 管理每层容器,
- /// 计算端口相对位置并更新 TMP 文本显示。
- /// 可调参数由 QuickGraphNodeRenderConfig 配置资产提供,
- /// 拖入蓝图组件即可覆盖默认值。
- ///
- /// LOD 模式(由 zoom 决定):
- /// - LOD 0:节点纯色填充(bg=borderColor),无边框无文本
- /// - LOD 1:正常节点+边框,无文本
- /// - LOD 2:完整渲染(含文本)
- ///
- ///
- [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();
- 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[bucketCount];
- for (int i = 0; i < bucketCount; i++)
- layerNodes[i] = new List();
-
- 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());
- }
- }
-
- _layerMgr.PruneLayersAbove(highestActiveLayer);
- }
-
- ///
- /// 根据归一化缩放值计算 LOD 等级。
- /// normalizedZoom = (zoom - MinZoom) / (MaxZoom - MinZoom),将 zoom 映射到 [0, 1]。
- ///
- 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 nodes, int layer,
- QuickGraphNodeRenderConfig cfg)
- {
- if (entry.PrimitiveRenderer != null)
- entry.PrimitiveRenderer.ClearAll();
-
- var aliveNodes = new HashSet(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);
- }
-
- /// 隐藏节点的 TMP 文本(不销毁,复用)。
- 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;
- }
- }
-}
diff --git a/Runtime/Blueprint/Render/QuickUGUIGraphWireRenderTool.cs b/Runtime/Blueprint/Render/QuickUGUIGraphWireRenderTool.cs
deleted file mode 100644
index 1ed8917..0000000
--- a/Runtime/Blueprint/Render/QuickUGUIGraphWireRenderTool.cs
+++ /dev/null
@@ -1,181 +0,0 @@
-using System.Collections.Generic;
-using UnityEngine;
-using XericLibrary.Runtime.UIGraph;
-
-namespace XericLibrary.Runtime.Blueprint.Render
-{
- ///
- /// 快速 UGUI 图论连线渲染工具。
- /// 通过共享的 LayerContainerManager 使用与节点相同的层容器,
- /// 连线绘制在两端节点所在层的较低层中(不遮挡节点)。
- /// 可调参数由 QuickGraphWireRenderConfig 配置资产提供。
- ///
- [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();
- 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[bucketCount];
- for (int i = 0; i < bucketCount; i++)
- layerWires[i] = new List();
-
- 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 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;
- }
- }
-}
diff --git a/Runtime/Blueprint/Render/WireCanvasRenderTool.cs b/Runtime/Blueprint/Render/WireCanvasRenderTool.cs
new file mode 100644
index 0000000..8800499
--- /dev/null
+++ b/Runtime/Blueprint/Render/WireCanvasRenderTool.cs
@@ -0,0 +1,135 @@
+using System.Collections.Generic;
+using UnityEngine;
+using XericLibrary.Runtime.UIGraph;
+
+namespace XericLibrary.Runtime.Blueprint.Render
+{
+ ///
+ /// 连线渲染工具 — 按网格区块管理 BlueprintCurveRenderer,支持 Viewport Transform。
+ /// 连线跟随其源节点(SourcePort.OwnerNode)进入对应 Chunk 的 CurveRenderer。
+ ///
+ [BlueprintTool(phase: ToolPhase.Render, order: 110)]
+ [BlueprintTheme("QuickGraph")]
+ public class WireCanvasRenderTool : BlueprintCanvasRenderTool
+ {
+ private QuickGraphWireRenderConfig _defaultConfig;
+ private QuickGraphWireRenderConfig Config
+ {
+ get
+ {
+ if (ToolConfig is QuickGraphWireRenderConfig external) return external;
+ if (_defaultConfig == null)
+ _defaultConfig = ScriptableObject.CreateInstance();
+ return _defaultConfig;
+ }
+ }
+
+ // ====================================================================
+ // 抽象实现
+ // ====================================================================
+
+ protected override BlueprintCurveRenderer BuildChunkRenderer(GameObject chunkGo)
+ {
+ return chunkGo.AddComponent();
+ }
+
+ 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);
+ }
+ }
+}
diff --git a/Runtime/Blueprint/Render/QuickUGUIGraphNodeRenderTool.cs.meta b/Runtime/Blueprint/Render/WireCanvasRenderTool.cs.meta
similarity index 83%
rename from Runtime/Blueprint/Render/QuickUGUIGraphNodeRenderTool.cs.meta
rename to Runtime/Blueprint/Render/WireCanvasRenderTool.cs.meta
index 1b4768a..0c3a8ec 100644
--- a/Runtime/Blueprint/Render/QuickUGUIGraphNodeRenderTool.cs.meta
+++ b/Runtime/Blueprint/Render/WireCanvasRenderTool.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: 552d9e16260f6704f8a3da72908af509
+guid: 3881805202e8cd049aad2d55c9f595f1
MonoImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/Runtime/LitJSON.dll b/Runtime/LitJSON.dll
deleted file mode 100644
index 3f22b63..0000000
Binary files a/Runtime/LitJSON.dll and /dev/null differ
diff --git a/Runtime/LitJSON.dll.meta b/Runtime/LitJSON.dll.meta
deleted file mode 100644
index 1391323..0000000
--- a/Runtime/LitJSON.dll.meta
+++ /dev/null
@@ -1,33 +0,0 @@
-fileFormatVersion: 2
-guid: af972e12c8e41814188a98b92de42e0c
-PluginImporter:
- externalObjects: {}
- serializedVersion: 2
- iconMap: {}
- executionOrder: {}
- defineConstraints: []
- isPreloaded: 0
- isOverridable: 1
- isExplicitlyReferenced: 0
- validateReferences: 1
- platformData:
- - first:
- Any:
- second:
- enabled: 1
- settings: {}
- - first:
- Editor: Editor
- second:
- enabled: 0
- settings:
- DefaultValueInitialized: true
- - first:
- Windows Store Apps: WindowsStoreApps
- second:
- enabled: 0
- settings:
- CPU: AnyCPU
- userData:
- assetBundleName:
- assetBundleVariant:
diff --git a/Runtime/XericLibrary.dll b/Runtime/XericLibrary.dll
index 0812d4e..a5aeec9 100644
Binary files a/Runtime/XericLibrary.dll and b/Runtime/XericLibrary.dll differ