udpate 0.6.2
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
using UnityEngine;
|
||||
using XericLibrary.Runtime.UIGraph;
|
||||
|
||||
namespace XericLibrary.Runtime.Blueprint.Render
|
||||
{
|
||||
/// <summary>
|
||||
/// 最简曲线渲染器 —— 继承 CurveCacheRendererBase,无任何额外设定。
|
||||
/// 供蓝图渲染工具使用,避免 UICurveRenderer 等自带的默认配置干扰。
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class BlueprintCurveRenderer : CurveCacheRendererBase
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 53a46255c1675484489c772b9cd65277
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,14 @@
|
||||
using UnityEngine;
|
||||
using XericLibrary.Runtime.UIGraph;
|
||||
|
||||
namespace XericLibrary.Runtime.Blueprint.Render
|
||||
{
|
||||
/// <summary>
|
||||
/// 最简图元渲染器 —— 继承 PrimitiveCacheRendererBase,无任何额外设定。
|
||||
/// 供蓝图渲染工具使用,避免 UICurveRenderer 等自带的默认配置干扰。
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class BlueprintPrimitiveRenderer : PrimitiveCacheRendererBase
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6292bb3966ffc0049b2f7546debd0a73
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c7cfe95b8a3355c4e8e6585d2266fb5b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,145 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace XericLibrary.Runtime.Blueprint.Render
|
||||
{
|
||||
/// <summary>
|
||||
/// QuickGraph 网格背景配置表 —— 控制 Shader 全部可调参数。
|
||||
/// 材质实例由此配置统一管理,所有调用者通过 <see cref="GetOrCreateMaterial"/> 获取同一实例。
|
||||
/// <see cref="ApplyToMaterial"/> 封装所有材质属性写入(含 _Transform 同步画布平移/缩放)。
|
||||
/// 右键 Project 窗口 → Create/Blueprint/QuickGraph/Grid Background Config 创建资产。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(
|
||||
fileName = "QuickGraphGridBackgroundConfig",
|
||||
menuName = "Xeric Library/Blueprint/QuickGraph/Grid Background Config",
|
||||
order = 3)]
|
||||
[BlueprintTheme("QuickGraph")]
|
||||
public class QuickGraphGridBackgroundConfig : BlueprintToolConfigBase
|
||||
{
|
||||
/// <summary>目标工具类型</summary>
|
||||
public override System.Type TargetToolType
|
||||
{
|
||||
get { return typeof(QuickUGUIGraphGridBackgroundTool); }
|
||||
}
|
||||
|
||||
[Header("材质来源")]
|
||||
[Tooltip("网格 Shader 名称")]
|
||||
public string ShaderName = "XericLibrary/BluePrint/BlueprintBackgound_GridLine";
|
||||
|
||||
[Tooltip("可选:直接指定背景材质。设置后将忽略 ShaderName。")]
|
||||
public Material OverrideMaterial = null;
|
||||
|
||||
[Header("网格参数")]
|
||||
[Tooltip("网格间距(画布单位)。值越小网格越密。")]
|
||||
public float GridSize = 100f;
|
||||
|
||||
[Tooltip("叠加网格密度")]
|
||||
public float GridOverlayPower = 5f;
|
||||
|
||||
[Tooltip("线条阈值。0.5=极粗, 1=极细")]
|
||||
[Range(0.5f, 1f)]
|
||||
public float GridLineThreshold = 0.99f;
|
||||
|
||||
[Tooltip("线条扩展/柔和度。越大线条越宽。")]
|
||||
[Range(0.001f, 1f)]
|
||||
public float GridExp = 0.001f;
|
||||
|
||||
[Header("颜色")]
|
||||
[Tooltip("网格线颜色")]
|
||||
public Color GridColor = Color.white;
|
||||
|
||||
[Tooltip("背景底色")]
|
||||
public Color GridBackgroundColor = Color.black;
|
||||
|
||||
// ─── 运行时缓存的材质实例 ───
|
||||
|
||||
[NonSerialized]
|
||||
private Material _cachedMaterial;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或创建材质实例。
|
||||
/// - 若 OverrideMaterial 不为空,直接返回。
|
||||
/// - 否则按 ShaderName 查找 Shader,创建材质并缓存。
|
||||
/// - 同一配置实例上多次调用返回同一材质对象。
|
||||
/// </summary>
|
||||
public Material GetOrCreateMaterial()
|
||||
{
|
||||
if (OverrideMaterial != null)
|
||||
return OverrideMaterial;
|
||||
|
||||
if (_cachedMaterial != null)
|
||||
return _cachedMaterial;
|
||||
|
||||
if (!string.IsNullOrEmpty(ShaderName))
|
||||
{
|
||||
var shader = Shader.Find(ShaderName);
|
||||
if (shader != null)
|
||||
{
|
||||
_cachedMaterial = new Material(shader);
|
||||
_cachedMaterial.name = "BpGridBackground_Mat";
|
||||
}
|
||||
}
|
||||
|
||||
return _cachedMaterial;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将当前配置的所有参数写入材质。
|
||||
/// <para>包括:
|
||||
/// - _Transform(scaleX, scaleY, offsetX, offsetY):随画布 zoom / pan 同步更新;
|
||||
/// - _GridOverlayPower、_GridLineThreshold、_GridExp;
|
||||
/// - _GridColor、_GridBackgroundColor。</para>
|
||||
/// 每次渲染时调用以保持与蓝图画布的缩放/平移同步。
|
||||
/// </summary>
|
||||
/// <param name="mat">目标材质实例</param>
|
||||
/// <param name="rectSize">背景板 RectTransform 的像素尺寸 (width, height)</param>
|
||||
/// <param name="zoom">当前画布缩放级别</param>
|
||||
/// <param name="panOffset">当前画布平移偏移量</param>
|
||||
public void ApplyToMaterial(Material mat, Vector2 rectSize, float zoom, Vector2 panOffset)
|
||||
{
|
||||
if (mat == null) return;
|
||||
|
||||
// ── _Transform: 基于画布 zoom / pan 同步 ──
|
||||
// Shader 中: texCoord2 = uv * _Transform.xy + _Transform.zw
|
||||
// uv (0,0) = Image 左下角, uv (1,1) = Image 右上角
|
||||
// CanvasToLocal: localPoint = canvasPoint * zoom + panOffset
|
||||
// UV ↔ localPoint: uv = localPoint / rectSize + 0.5f
|
||||
// ^^^^^ pivot(0.5,0.5) 的 UV 偏移
|
||||
// 代入: uv = (canvasPoint * zoom + panOffset) / rectSize + 0.5f
|
||||
// 目标: gridPos = canvasPoint / GridSize = uv * scale + offset
|
||||
// 解出: scale = rectSize / (zoom * GridSize)
|
||||
// offset = -(panOffset + 0.5 * rectSize) / (zoom * GridSize)
|
||||
float sx = rectSize.x / (zoom * GridSize);
|
||||
float sy = rectSize.y / (zoom * GridSize);
|
||||
float ox = -(panOffset.x + 0.5f * rectSize.x) / (zoom * GridSize);
|
||||
float oy = -(panOffset.y + 0.5f * rectSize.y) / (zoom * GridSize);
|
||||
mat.SetVector("_Transform", new Vector4(sx, sy, ox, oy));
|
||||
|
||||
// ── 网格线参数 ──
|
||||
mat.SetFloat("_GridOverlayPower", GridOverlayPower);
|
||||
mat.SetFloat("_GridLineThreshold", GridLineThreshold);
|
||||
mat.SetFloat("_GridExp", GridExp);
|
||||
|
||||
// ── 颜色 ──
|
||||
mat.SetColor("_GridColor", GridColor);
|
||||
mat.SetColor("_GridBackgroundColor", GridBackgroundColor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放此配置创建的材质实例(不释放 OverrideMaterial)。
|
||||
/// </summary>
|
||||
public void ReleaseMaterial()
|
||||
{
|
||||
if (_cachedMaterial != null)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if (!Application.isPlaying)
|
||||
DestroyImmediate(_cachedMaterial);
|
||||
else
|
||||
#endif
|
||||
Destroy(_cachedMaterial);
|
||||
_cachedMaterial = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 32753aff001677a4b81e10dfae39b4cb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace XericLibrary.Runtime.Blueprint.Render
|
||||
{
|
||||
/// <summary>
|
||||
/// QuickGraph 输入工具配置表 —— 控制平移/缩放插值速率、灵敏度等可调参数。
|
||||
/// 右键 Project 窗口 → Create/Blueprint/QuickGraph/Input Config 创建资产。
|
||||
/// 拖入蓝图组件的 ToolConfigAssets 列表即可覆盖默认值。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(
|
||||
fileName = "QuickGraphInputConfig",
|
||||
menuName = "Xeric Library/Blueprint/QuickGraph/Input Config",
|
||||
order = 4)]
|
||||
[BlueprintTheme("QuickGraph")]
|
||||
public class QuickGraphInputConfig : BlueprintToolConfigBase
|
||||
{
|
||||
/// <summary>目标工具类型</summary>
|
||||
public override System.Type TargetToolType
|
||||
{
|
||||
get { return typeof(QuickGraphInputTool); }
|
||||
}
|
||||
|
||||
[Header("平移")]
|
||||
[Tooltip("平移动画插值速率(越大跟随越快,3=适中,10=几乎瞬移)")]
|
||||
[Range(1f, 20f)]
|
||||
public float PanLerpSpeed = 3f;
|
||||
|
||||
[Header("缩放")]
|
||||
[Tooltip("缩放动画插值速率(越大跟随更快)")]
|
||||
[Range(1f, 30f)]
|
||||
public float ZoomLerpSpeed = 8f;
|
||||
|
||||
[Tooltip("滚轮缩放除数。公式:zoom += scrollDelta / ZoomDivisor。\nUnity Input System 的 scrollDelta.y 标准值为 120/格,\n1200 = 每格 +0.1,600 = 每格 +0.2,300 = 每格 +0.4")]
|
||||
[Range(100f, 5000f)]
|
||||
public float ZoomDivisor = 1200f;
|
||||
|
||||
[Header("缩放范围")]
|
||||
[Tooltip("最小缩放级别")]
|
||||
[Range(0.01f, 1f)]
|
||||
public float MinZoom = 0.1f;
|
||||
[Tooltip("最大缩放级别")]
|
||||
[Range(1f, 10f)]
|
||||
public float MaxZoom = 3f;
|
||||
|
||||
[Header("键盘")]
|
||||
[Tooltip("键盘方向键平移速度(画布单位/秒)")]
|
||||
[Range(50f, 2000f)]
|
||||
public float KeyboardPanSpeed = 600f;
|
||||
|
||||
[Tooltip("键盘 +/- 缩放速度(每次帧增量,建议 0.01~0.5)")]
|
||||
[Range(0.005f, 0.5f)]
|
||||
public float KeyboardZoomSpeed = 0.05f;
|
||||
|
||||
[Header("框选")]
|
||||
[Tooltip("框选拖拽触发阈值(像素)")]
|
||||
[Range(1f, 20f)]
|
||||
public float DragThreshold = 5f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cfce52570fac4f5488becd85ce4f023b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace XericLibrary.Runtime.Blueprint.Render
|
||||
{
|
||||
/// <summary>
|
||||
/// QuickGraph 节点渲染配置表 —— 控制节点矩形、边框、圆角等视觉参数。
|
||||
/// 右键 Project 窗口 → Create/Blueprint/QuickGraph/Node Render Config 创建资产。
|
||||
/// 拖入蓝图组件的 ToolConfigAssets 列表即可覆盖默认值。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(
|
||||
fileName = "QuickGraphNodeRenderConfig",
|
||||
menuName = "Xeric Library/Blueprint/QuickGraph/Node Render Config",
|
||||
order = 1)]
|
||||
[BlueprintTheme("QuickGraph")]
|
||||
public class QuickGraphNodeRenderConfig : BlueprintToolConfigBase
|
||||
{
|
||||
/// <summary>目标工具类型</summary>
|
||||
public override System.Type TargetToolType
|
||||
{
|
||||
get { return typeof(QuickUGUIGraphNodeRenderTool); }
|
||||
}
|
||||
|
||||
[Header("尺寸")]
|
||||
[Tooltip("节点宽度(像素)")]
|
||||
public float NodeWidth = 90f;
|
||||
|
||||
[Tooltip("节点高度(像素)")]
|
||||
public float NodeHeight = 130f;
|
||||
|
||||
[Header("边框")]
|
||||
[Tooltip("边框厚度(像素)")]
|
||||
public float BorderThickness = 3f;
|
||||
|
||||
[Header("圆角")]
|
||||
[Tooltip("圆角大小(0~1 归一化)")]
|
||||
[Range(0f, 0.5f)]
|
||||
public float ChamferSize = 0.2f;
|
||||
|
||||
[Tooltip("圆角细分段数")]
|
||||
[Range(1, 16)]
|
||||
public int ChamferSegments = 4;
|
||||
|
||||
[Header("颜色")]
|
||||
[Tooltip("节点默认背景色")]
|
||||
public Color DefaultBackgroundColor = new Color(0.16f, 0.16f, 0.16f);
|
||||
[Tooltip("节点默认文字色")]
|
||||
public Color DefaultTextColor = Color.white;
|
||||
|
||||
[Header("文本")]
|
||||
[Tooltip("节点标题字号")]
|
||||
public float TitleFontSize = 14f;
|
||||
[Tooltip("节点标题字体(留空使用默认 TMP 字体)")]
|
||||
public TMPro.TMP_FontAsset TitleFont;
|
||||
|
||||
[Header("LOD(缩放等级细节)")]
|
||||
[Tooltip("归一化缩放值低于此阈值时触发 LOD 0(极简:纯色节点,无文本)。0~1。默认 0.3。")]
|
||||
[Range(0f, 1f)]
|
||||
public float Lod0Threshold = 0.3f;
|
||||
[Tooltip("归一化缩放值低于此阈值时触发 LOD 1(简化:节点+边框,无文本);高于此值为 LOD 2(完整渲染)。0~1。默认 0.7。")]
|
||||
[Range(0f, 1f)]
|
||||
public float Lod1Threshold = 0.7f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8ab3d3829c73815468ed8c5ae306a6ee
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using XericLibrary.Runtime.UIGraph;
|
||||
|
||||
namespace XericLibrary.Runtime.Blueprint.Render
|
||||
{
|
||||
/// <summary>
|
||||
/// QuickGraph 连线渲染配置表 —— 控制贝塞尔曲线宽度、箭头形状等全部视觉参数。
|
||||
/// 右键 Project 窗口 → Create/Blueprint/QuickGraph/Wire Render Config 创建资产。
|
||||
/// 拖入蓝图组件的 ToolConfigAssets 列表即可覆盖默认值。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(
|
||||
fileName = "QuickGraphWireRenderConfig",
|
||||
menuName = "Xeric Library/Blueprint/QuickGraph/Wire Render Config",
|
||||
order = 2)]
|
||||
[BlueprintTheme("QuickGraph")]
|
||||
public class QuickGraphWireRenderConfig : BlueprintToolConfigBase
|
||||
{
|
||||
/// <summary>目标工具类型</summary>
|
||||
public override System.Type TargetToolType
|
||||
{
|
||||
get { return typeof(QuickUGUIGraphWireRenderTool); }
|
||||
}
|
||||
|
||||
[Header("连线")]
|
||||
[Tooltip("连线宽度(像素)")]
|
||||
[Range(1f, 20f)]
|
||||
public float WireWidth = 5f;
|
||||
|
||||
[Header("贝塞尔手柄")]
|
||||
[Tooltip("源端手柄从端口伸出的距离(画布单位)。数值越大曲线越平缓。")]
|
||||
[Range(10f, 500f)]
|
||||
public float SourceHandleLength = 80f;
|
||||
[Tooltip("目标端手柄从端口伸出的距离(画布单位)。")]
|
||||
[Range(10f, 500f)]
|
||||
public float TargetHandleLength = 80f;
|
||||
|
||||
[Header("箭头")]
|
||||
[Tooltip("箭头形状")]
|
||||
public ArrowShape ArrowShape = ArrowShape.Triangle;
|
||||
|
||||
[Tooltip("箭头是否反向")]
|
||||
public bool ArrowReversed = false;
|
||||
|
||||
[Tooltip("箭头在曲线上的位置(0=起点, 1=终点)")]
|
||||
[Range(0f, 1f)]
|
||||
public float ArrowProgress = 1f;
|
||||
|
||||
[Tooltip("箭头宽度(垂直切线方向,像素)")]
|
||||
[Range(4f, 40f)]
|
||||
public float ArrowWidth = 12f;
|
||||
|
||||
[Tooltip("箭头高度(沿切线方向,像素)")]
|
||||
[Range(4f, 40f)]
|
||||
public float ArrowHeight = 10f;
|
||||
|
||||
[Tooltip("深度补偿:-1=尾部对齐曲线点,0=中心对齐,1=头部对齐曲线点")]
|
||||
[Range(-1f, 1f)]
|
||||
public float ArrowDepthCompensation = -1f;
|
||||
|
||||
[Header("曲线质量")]
|
||||
[Tooltip("贝塞尔曲线细分段数")]
|
||||
[Range(8, 64)]
|
||||
public int TessellationSegments = 32;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 21208cffd6108e04d815bb385f0af3fc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,275 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace XericLibrary.Runtime.Blueprint.Render
|
||||
{
|
||||
/// <summary>
|
||||
/// 层容器条目 —— 一层中挂载的所有渲染组件
|
||||
/// </summary>
|
||||
public sealed class LayerContainerEntry
|
||||
{
|
||||
/// <summary>容器根 GameObject,锚点撑满蓝图</summary>
|
||||
public GameObject Container;
|
||||
|
||||
/// <summary>图元渲染器(节点矩形)</summary>
|
||||
public BlueprintPrimitiveRenderer PrimitiveRenderer;
|
||||
|
||||
/// <summary>曲线渲染器(连线)</summary>
|
||||
public BlueprintCurveRenderer CurveRenderer;
|
||||
|
||||
/// <summary>节点 → TMP Text 映射(挂在此容器下)</summary>
|
||||
public Dictionary<BlueprintNode, TMPro.TMP_Text> NodeTexts
|
||||
= new Dictionary<BlueprintNode, TMPro.TMP_Text>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 共享层容器管理器 —— 管理桶排序中每一层的容器 GameObject。
|
||||
/// 一层一个容器,内部按 sibling 顺序挂载:
|
||||
/// 1. Image 背景(由 GridBackgroundTool 处理,挂 root 下)
|
||||
/// 2. __Curve(连线)
|
||||
/// 3. __Primitive(节点矩形)
|
||||
/// 4. TMP Text(节点文字)
|
||||
/// 各渲染工具通过此管理器共享同一组层容器。
|
||||
///
|
||||
/// 所有创建的 GameObject 默认 HideFlags.HideAndDontSave,
|
||||
/// 避免在编辑器→运行/运行→编辑器切换时残留场景。
|
||||
/// 调试时可通过 GraphTheoryBlueprintComponent._showDebugObjects 强制显示。
|
||||
/// </summary>
|
||||
public class LayerContainerManager
|
||||
{
|
||||
/// <summary>所有蓝图实例共享的层管理器(蓝图实例 → 层管理器)</summary>
|
||||
private static Dictionary<BlueprintGraph, LayerContainerManager> s_Instances
|
||||
= new Dictionary<BlueprintGraph, LayerContainerManager>();
|
||||
|
||||
/// <summary>获取或创建指定蓝图实例的层容器管理器</summary>
|
||||
public static LayerContainerManager GetForGraph(BlueprintGraph graph, Transform renderRoot)
|
||||
{
|
||||
if (s_Instances.TryGetValue(graph, out var mgr) && mgr != null)
|
||||
return mgr;
|
||||
|
||||
mgr = new LayerContainerManager(renderRoot);
|
||||
s_Instances[graph] = mgr;
|
||||
return mgr;
|
||||
}
|
||||
|
||||
/// <summary>释放蓝图实例的层容器</summary>
|
||||
public static void ReleaseForGraph(BlueprintGraph graph)
|
||||
{
|
||||
if (s_Instances.TryGetValue(graph, out var mgr))
|
||||
{
|
||||
mgr.DestroyAll();
|
||||
s_Instances.Remove(graph);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly Transform _renderRoot;
|
||||
private readonly Dictionary<int, LayerContainerEntry> _layers
|
||||
= new Dictionary<int, LayerContainerEntry>();
|
||||
|
||||
private LayerContainerManager(Transform renderRoot)
|
||||
{
|
||||
_renderRoot = renderRoot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定层级的容器条目。不存在则创建。
|
||||
/// </summary>
|
||||
public LayerContainerEntry GetOrCreateLayer(int layer)
|
||||
{
|
||||
if (_layers.TryGetValue(layer, out var entry) && entry != null)
|
||||
return entry;
|
||||
|
||||
// --- 创建容器 ---
|
||||
var container = new GameObject($"__BpLayer_{layer:D2}", typeof(RectTransform));
|
||||
container.hideFlags = HideFlags.HideAndDontSave;
|
||||
container.transform.SetParent(_renderRoot, false);
|
||||
var ctRt = container.GetComponent<RectTransform>();
|
||||
ctRt.anchorMin = Vector2.zero;
|
||||
ctRt.anchorMax = Vector2.one;
|
||||
ctRt.offsetMin = Vector2.zero;
|
||||
ctRt.offsetMax = Vector2.zero;
|
||||
|
||||
entry = new LayerContainerEntry { Container = container };
|
||||
|
||||
// --- 创建连线渲染器(底层) ---
|
||||
var curveGo = new GameObject("__Curve", typeof(RectTransform));
|
||||
curveGo.hideFlags = HideFlags.HideAndDontSave;
|
||||
curveGo.transform.SetParent(container.transform, false);
|
||||
var crRt = curveGo.GetComponent<RectTransform>();
|
||||
crRt.anchorMin = Vector2.zero;
|
||||
crRt.anchorMax = Vector2.one;
|
||||
crRt.offsetMin = Vector2.zero;
|
||||
crRt.offsetMax = Vector2.zero;
|
||||
entry.CurveRenderer = curveGo.AddComponent<BlueprintCurveRenderer>();
|
||||
|
||||
// --- 创建图元渲染器(中层) ---
|
||||
var primGo = new GameObject("__Primitive", typeof(RectTransform));
|
||||
primGo.hideFlags = HideFlags.HideAndDontSave;
|
||||
primGo.transform.SetParent(container.transform, false);
|
||||
var prRt = primGo.GetComponent<RectTransform>();
|
||||
prRt.anchorMin = Vector2.zero;
|
||||
prRt.anchorMax = Vector2.one;
|
||||
prRt.offsetMin = Vector2.zero;
|
||||
prRt.offsetMax = Vector2.zero;
|
||||
entry.PrimitiveRenderer = primGo.AddComponent<BlueprintPrimitiveRenderer>();
|
||||
|
||||
_layers[layer] = entry;
|
||||
ReorderAllContainers();
|
||||
return entry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试获取已存在的层容器(不创建)
|
||||
/// </summary>
|
||||
public LayerContainerEntry TryGetLayer(int layer)
|
||||
{
|
||||
_layers.TryGetValue(layer, out var entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或创建节点文本(挂到指定层容器下)
|
||||
/// </summary>
|
||||
public TMPro.TMP_Text GetOrCreateNodeText(BlueprintNode node, int layer)
|
||||
{
|
||||
var entry = GetOrCreateLayer(layer);
|
||||
if (entry.NodeTexts.TryGetValue(node, out var text) && text != null)
|
||||
return text;
|
||||
|
||||
var textGo = new GameObject($"__Text_{node.ElementId}", typeof(RectTransform));
|
||||
textGo.hideFlags = HideFlags.HideAndDontSave;
|
||||
textGo.transform.SetParent(entry.Container.transform, false);
|
||||
|
||||
var tmp = textGo.AddComponent<TMPro.TextMeshProUGUI>();
|
||||
tmp.alignment = TMPro.TextAlignmentOptions.Center;
|
||||
tmp.fontSize = 12;
|
||||
tmp.enableAutoSizing = false;
|
||||
|
||||
var rt = tmp.rectTransform;
|
||||
rt.pivot = new Vector2(0.5f, 0.5f);
|
||||
rt.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
rt.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
|
||||
// 放到该层最末尾(覆盖矩形之上)
|
||||
tmp.rectTransform.SetAsLastSibling();
|
||||
|
||||
entry.NodeTexts[node] = tmp;
|
||||
return tmp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试获取已存在的节点文本对象,不存在则返回 null。
|
||||
/// </summary>
|
||||
public TMPro.TMP_Text TryGetNodeText(BlueprintNode node, int layer)
|
||||
{
|
||||
if (!_layers.TryGetValue(layer, out var entry) || entry == null) return null;
|
||||
entry.NodeTexts.TryGetValue(node, out var text);
|
||||
return text;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除指定层容器的所有渲染内容(不清除容器结构)
|
||||
/// </summary>
|
||||
public void ClearLayer(int layer)
|
||||
{
|
||||
if (!_layers.TryGetValue(layer, out var entry) || entry == null) return;
|
||||
|
||||
entry.PrimitiveRenderer?.ClearAll();
|
||||
entry.CurveRenderer?.ClearAll();
|
||||
|
||||
foreach (var kvp in entry.NodeTexts)
|
||||
{
|
||||
if (kvp.Value != null)
|
||||
kvp.Value.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理指定层中已不存在的节点的文本对象
|
||||
/// </summary>
|
||||
public void PruneDeadTexts(int layer, HashSet<BlueprintNode> aliveNodes)
|
||||
{
|
||||
if (!_layers.TryGetValue(layer, out var entry) || entry == null) return;
|
||||
|
||||
var dead = new List<BlueprintNode>();
|
||||
foreach (var kvp in entry.NodeTexts)
|
||||
{
|
||||
if (!aliveNodes.Contains(kvp.Key))
|
||||
dead.Add(kvp.Key);
|
||||
}
|
||||
|
||||
foreach (var node in dead)
|
||||
{
|
||||
if (entry.NodeTexts.TryGetValue(node, out var text) && text != null)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if (!Application.isPlaying)
|
||||
Object.DestroyImmediate(text.gameObject);
|
||||
else
|
||||
#endif
|
||||
Object.Destroy(text.gameObject);
|
||||
}
|
||||
entry.NodeTexts.Remove(node);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除超出活跃范围的所有层容器
|
||||
/// </summary>
|
||||
public void PruneLayersAbove(int highestActiveLayer)
|
||||
{
|
||||
var toRemove = new List<int>();
|
||||
foreach (var kvp in _layers)
|
||||
{
|
||||
if (kvp.Key > highestActiveLayer)
|
||||
toRemove.Add(kvp.Key);
|
||||
}
|
||||
|
||||
foreach (int layer in toRemove)
|
||||
{
|
||||
if (_layers.TryGetValue(layer, out var entry) && entry != null)
|
||||
{
|
||||
if (entry.Container != null)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if (!Application.isPlaying)
|
||||
Object.DestroyImmediate(entry.Container);
|
||||
else
|
||||
#endif
|
||||
Object.Destroy(entry.Container);
|
||||
}
|
||||
}
|
||||
_layers.Remove(layer);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 销毁所有层容器
|
||||
/// </summary>
|
||||
public void DestroyAll()
|
||||
{
|
||||
foreach (var kvp in _layers)
|
||||
{
|
||||
if (kvp.Value?.Container != null)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if (!Application.isPlaying)
|
||||
Object.DestroyImmediate(kvp.Value.Container);
|
||||
else
|
||||
#endif
|
||||
Object.Destroy(kvp.Value.Container);
|
||||
}
|
||||
}
|
||||
_layers.Clear();
|
||||
}
|
||||
|
||||
private void ReorderAllContainers()
|
||||
{
|
||||
var sorted = new List<KeyValuePair<int, LayerContainerEntry>>(_layers);
|
||||
sorted.Sort((a, b) => a.Key.CompareTo(b.Key));
|
||||
foreach (var kvp in sorted)
|
||||
kvp.Value.Container.transform.SetAsLastSibling();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 159fb4400ff6e5e4b859ddfd37f60a6c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,141 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace XericLibrary.Runtime.Blueprint.Render
|
||||
{
|
||||
/// <summary>
|
||||
/// 快速 UGUI 图论网格背景渲染工具。
|
||||
/// 在蓝图画布最底层创建一个全屏 Image 组件绘制网格背景,
|
||||
/// 每帧将画布的 zoom / pan 同步到 Shader 的 _Transform 属性。
|
||||
/// 全部可调参数由 QuickGraphGridBackgroundConfig 配置资产提供。
|
||||
/// </summary>
|
||||
[BlueprintTool(phase: ToolPhase.Render, order: 0)]
|
||||
[BlueprintTheme("QuickGraph")]
|
||||
public class QuickUGUIGraphGridBackgroundTool : BlueprintTool
|
||||
{
|
||||
private const string BackgroundGoName = "__Bp_GridBackground";
|
||||
|
||||
private QuickGraphGridBackgroundConfig _defaultConfig;
|
||||
|
||||
private QuickGraphGridBackgroundConfig Config
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ToolConfig is QuickGraphGridBackgroundConfig external)
|
||||
return external;
|
||||
if (_defaultConfig == null)
|
||||
_defaultConfig = ScriptableObject.CreateInstance<QuickGraphGridBackgroundConfig>();
|
||||
return _defaultConfig;
|
||||
}
|
||||
}
|
||||
|
||||
private Image _backgroundImage;
|
||||
private Transform _renderRoot;
|
||||
|
||||
public override void OnInitialize()
|
||||
{
|
||||
EnsureBackground();
|
||||
}
|
||||
|
||||
public override void OnRender()
|
||||
{
|
||||
if (Graph == null) return;
|
||||
EnsureBackground();
|
||||
SyncMaterialProperties();
|
||||
}
|
||||
|
||||
private void EnsureBackground()
|
||||
{
|
||||
if (_backgroundImage != null) return;
|
||||
|
||||
EnsureRenderRoot();
|
||||
if (_renderRoot == null) return;
|
||||
|
||||
var existing = _renderRoot.Find(BackgroundGoName);
|
||||
if (existing != null)
|
||||
{
|
||||
_backgroundImage = existing.GetComponent<Image>();
|
||||
if (_backgroundImage == null)
|
||||
{
|
||||
Object.DestroyImmediate(existing.gameObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplyMaterial();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var bgGo = new GameObject(BackgroundGoName, typeof(RectTransform));
|
||||
bgGo.hideFlags = HideFlags.HideAndDontSave;
|
||||
bgGo.transform.SetParent(_renderRoot, false);
|
||||
bgGo.transform.SetAsFirstSibling();
|
||||
|
||||
var rt = bgGo.GetComponent<RectTransform>();
|
||||
rt.anchorMin = Vector2.zero;
|
||||
rt.anchorMax = Vector2.one;
|
||||
rt.offsetMin = Vector2.zero;
|
||||
rt.offsetMax = Vector2.zero;
|
||||
|
||||
_backgroundImage = bgGo.AddComponent<Image>();
|
||||
_backgroundImage.raycastTarget = true;
|
||||
|
||||
ApplyMaterial();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从配置获取材质并设置到 Image(仅首次执行)。
|
||||
/// </summary>
|
||||
private void ApplyMaterial()
|
||||
{
|
||||
if (_backgroundImage == null) return;
|
||||
var mat = Config.GetOrCreateMaterial();
|
||||
if (mat != null)
|
||||
_backgroundImage.material = mat;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 每帧将当前画布 zoom / pan 同步到 Shader,
|
||||
/// 同时写入配置中所有 Shader 属性。
|
||||
/// </summary>
|
||||
private void SyncMaterialProperties()
|
||||
{
|
||||
if (_backgroundImage == null) return;
|
||||
if (Graph == null || Graph.Canvas == null) return;
|
||||
|
||||
var mat = _backgroundImage.material;
|
||||
if (mat == null) return;
|
||||
|
||||
var rt = _backgroundImage.rectTransform;
|
||||
float zoom = Graph.Canvas.ZoomLevel;
|
||||
Vector2 pan = Graph.Canvas.PanOffset;
|
||||
|
||||
Config.ApplyToMaterial(mat, rt.rect.size, zoom, pan);
|
||||
}
|
||||
|
||||
private void EnsureRenderRoot()
|
||||
{
|
||||
if (_renderRoot != null) return;
|
||||
if (Graph == null || Graph.Canvas == null) return;
|
||||
_renderRoot = Graph.Canvas.GetRootTransform();
|
||||
}
|
||||
|
||||
public override void OnDestroy()
|
||||
{
|
||||
if (_backgroundImage != null && _backgroundImage.gameObject != null)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if (!Application.isPlaying)
|
||||
Object.DestroyImmediate(_backgroundImage.gameObject);
|
||||
else
|
||||
#endif
|
||||
Object.Destroy(_backgroundImage.gameObject);
|
||||
_backgroundImage = null;
|
||||
}
|
||||
|
||||
Config.ReleaseMaterial();
|
||||
_defaultConfig = null;
|
||||
_renderRoot = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: da293e1f23cfca24b9ca4d40f91d2d8c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,264 @@
|
||||
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;
|
||||
|
||||
// --- 当前生效的配置 ---
|
||||
private 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;
|
||||
|
||||
// ── 将配置默认值写入所有节点(config → node property) ──
|
||||
foreach (var node in Graph.Nodes)
|
||||
{
|
||||
ApplyConfigToNode(node, cfg);
|
||||
}
|
||||
|
||||
// 视口裁剪:只渲染画布可见范围内的节点
|
||||
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 = bgColor,
|
||||
centerColor = Color.white,
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将配置默认值写入节点,使渲染工具直接读取节点值。
|
||||
/// config 只控制背景色,边框色和文本色由节点初始化时定义。
|
||||
/// </summary>
|
||||
private static void ApplyConfigToNode(BlueprintNode node, QuickGraphNodeRenderConfig cfg)
|
||||
{
|
||||
node.NodeBGColor = cfg.DefaultBackgroundColor;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 552d9e16260f6704f8a3da72908af509
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,181 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using XericLibrary.Runtime.UIGraph;
|
||||
|
||||
namespace XericLibrary.Runtime.Blueprint.Render
|
||||
{
|
||||
/// <summary>
|
||||
/// 快速 UGUI 图论连线渲染工具。
|
||||
/// 通过共享的 LayerContainerManager 使用与节点相同的层容器,
|
||||
/// 连线绘制在两端节点所在层的较低层中(不遮挡节点)。
|
||||
/// 可调参数由 QuickGraphWireRenderConfig 配置资产提供。
|
||||
/// </summary>
|
||||
[BlueprintTool(phase: ToolPhase.Render, order: 110)]
|
||||
[BlueprintTheme("QuickGraph")]
|
||||
public class QuickUGUIGraphWireRenderTool : BlueprintTool
|
||||
{
|
||||
private QuickGraphWireRenderConfig _defaultConfig;
|
||||
|
||||
private QuickGraphWireRenderConfig Config
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ToolConfig is QuickGraphWireRenderConfig external)
|
||||
return external;
|
||||
if (_defaultConfig == null)
|
||||
_defaultConfig = ScriptableObject.CreateInstance<QuickGraphWireRenderConfig>();
|
||||
return _defaultConfig;
|
||||
}
|
||||
}
|
||||
|
||||
private Transform _renderRoot;
|
||||
private LayerContainerManager _layerMgr;
|
||||
|
||||
public override void OnRender()
|
||||
{
|
||||
if (Graph == null) return;
|
||||
EnsureRenderRoot();
|
||||
if (_renderRoot == null) return;
|
||||
|
||||
var buckets = Graph.RenderLayers;
|
||||
if (buckets == null) return;
|
||||
|
||||
_layerMgr = LayerContainerManager.GetForGraph(Graph, _renderRoot);
|
||||
|
||||
var cfg = Config;
|
||||
int bucketCount = buckets.BucketCount;
|
||||
|
||||
// 视口裁剪
|
||||
var viewport = Graph.Canvas.GetViewportRect();
|
||||
float margin = cfg.WireWidth * 20f;
|
||||
viewport.xMin -= margin;
|
||||
viewport.xMax += margin;
|
||||
viewport.yMin -= margin;
|
||||
viewport.yMax += margin;
|
||||
|
||||
var layerWires = new List<BlueprintWire>[bucketCount];
|
||||
for (int i = 0; i < bucketCount; i++)
|
||||
layerWires[i] = new List<BlueprintWire>();
|
||||
|
||||
foreach (var wire in Graph.Wires)
|
||||
{
|
||||
int srcLayer = wire.SourcePort?.OwnerNode?.RenderLayer ?? 0;
|
||||
int tgtLayer = wire.TargetPort?.OwnerNode?.RenderLayer ?? 0;
|
||||
|
||||
int wireLayer = Mathf.Min(srcLayer, tgtLayer);
|
||||
if (wireLayer < 0 || wireLayer >= bucketCount) wireLayer = 0;
|
||||
|
||||
// 两端都不在视口内则跳过
|
||||
if (wire.SourcePort?.OwnerNode != null && wire.TargetPort?.OwnerNode != null)
|
||||
{
|
||||
if (!viewport.Contains(wire.SourcePort.OwnerNode.Position) &&
|
||||
!viewport.Contains(wire.TargetPort.OwnerNode.Position))
|
||||
continue;
|
||||
}
|
||||
|
||||
layerWires[wireLayer].Add(wire);
|
||||
}
|
||||
|
||||
for (int i = 0; i < bucketCount; i++)
|
||||
{
|
||||
if (layerWires[i].Count > 0)
|
||||
{
|
||||
var entry = _layerMgr.GetOrCreateLayer(i);
|
||||
RenderLayerWires(entry, layerWires[i], cfg, Graph.Canvas);
|
||||
}
|
||||
else
|
||||
{
|
||||
var entry = _layerMgr.TryGetLayer(i);
|
||||
entry?.CurveRenderer?.ClearAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void RenderLayerWires(LayerContainerEntry entry, List<BlueprintWire> wires,
|
||||
QuickGraphWireRenderConfig cfg, IBlueprintCanvas canvas)
|
||||
{
|
||||
entry.CurveRenderer.ClearAll();
|
||||
|
||||
float zoom = canvas.ZoomLevel;
|
||||
|
||||
foreach (var wire in wires)
|
||||
{
|
||||
if (wire.SourcePort == null || wire.TargetPort == null) continue;
|
||||
if (wire.SourcePort.OwnerNode == null || wire.TargetPort.OwnerNode == null) continue;
|
||||
|
||||
Vector2 startPos = canvas.CanvasToLocal(wire.SourcePort.GetWorldPosition());
|
||||
Vector2 endPos = canvas.CanvasToLocal(wire.TargetPort.GetWorldPosition());
|
||||
|
||||
// 手柄方向 = 端口在节点上的朝向
|
||||
// 端口在节点右侧 (RelativePosition.x > 0) → 手柄朝右 (+X)
|
||||
// 端口在节点左侧 (RelativePosition.x < 0) → 手柄朝左 (-X)
|
||||
float srcDirX = Mathf.Sign(wire.SourcePort.RelativePosition.x);
|
||||
float tgtDirX = Mathf.Sign(wire.TargetPort.RelativePosition.x);
|
||||
// 同一侧(如两个端口都在右侧)则方向取反
|
||||
if (srcDirX == tgtDirX) srcDirX *= -1f;
|
||||
|
||||
// 手柄距离 = 配置值 × zoom(画布单位 → local 坐标,与 CanvasToLocal 一致)
|
||||
Vector2 handle1 = startPos + new Vector2(srcDirX * cfg.SourceHandleLength * zoom, 0f);
|
||||
Vector2 handle2 = endPos + new Vector2(tgtDirX * cfg.TargetHandleLength * zoom, 0f);
|
||||
|
||||
float ww = cfg.WireWidth * zoom;
|
||||
|
||||
var ctrlPts = new Vector3[]
|
||||
{
|
||||
new Vector3(startPos.x, startPos.y, ww),
|
||||
new Vector3(handle1.x, handle1.y, ww),
|
||||
new Vector3(handle2.x, handle2.y, ww),
|
||||
new Vector3(endPos.x, endPos.y, ww),
|
||||
};
|
||||
|
||||
Color32 srcColor = wire.SourcePort.OwnerNode.NodeColor;
|
||||
Color32 tgtColor = wire.TargetPort.OwnerNode.NodeColor;
|
||||
|
||||
var arrow = new ArrowHeadData
|
||||
{
|
||||
shape = cfg.ArrowShape,
|
||||
reversed = cfg.ArrowReversed,
|
||||
progress = cfg.ArrowProgress,
|
||||
width = cfg.ArrowWidth * zoom,
|
||||
height = cfg.ArrowHeight * zoom,
|
||||
depthCompensation = cfg.ArrowDepthCompensation,
|
||||
color = tgtColor,
|
||||
};
|
||||
|
||||
int count = ctrlPts.Length;
|
||||
var ctrlPts2 = new Vector2[count];
|
||||
var widths = new float[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
ctrlPts2[i] = ctrlPts[i];
|
||||
widths[i] = ctrlPts[i].z;
|
||||
}
|
||||
|
||||
var curveEntry = new CurveCacheEntry
|
||||
{
|
||||
startColor = srcColor,
|
||||
endColor = tgtColor,
|
||||
tessellationSegments = cfg.TessellationSegments,
|
||||
};
|
||||
|
||||
entry.CurveRenderer.AddCurve(curveEntry, ctrlPts2, widths, new[] { arrow });
|
||||
}
|
||||
|
||||
if (wires.Count > 0)
|
||||
entry.CurveRenderer.RebuildAll();
|
||||
}
|
||||
|
||||
private void EnsureRenderRoot()
|
||||
{
|
||||
if (_renderRoot != null) return;
|
||||
if (Graph.Canvas == null) return;
|
||||
_renderRoot = Graph.Canvas.GetRootTransform();
|
||||
}
|
||||
|
||||
public override void OnDestroy()
|
||||
{
|
||||
_layerMgr = null;
|
||||
_renderRoot = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b98af5cb1bdbbcf4d8b60e620f6b2fdb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user