Files
XericLibrary-WebGL-Publish/Runtime/Blueprint/Render/QuickUGUIGraphNodeRenderTool.cs
T
lrss3 7f3a12cae0 # update 0.6.2.1
更新了蓝图组件相关的用法。
2026-07-15 10:36:16 +08:00

250 lines
7.3 KiB
C#

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;
}
}
}