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