update 0.6.3.7
This commit is contained in:
@@ -1,341 +1,134 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using XericLibrary.Runtime.MacroLibrary;
|
||||
|
||||
namespace XericLibrary.Runtime.Blueprint
|
||||
namespace XericLibrary.Runtime.Blueprint.Render
|
||||
{
|
||||
/// <summary>
|
||||
/// 蓝图画布渲染工具的抽象基类。
|
||||
/// <para>
|
||||
/// 职责:
|
||||
/// 1. 管理 Viewport Transform — 平移/缩放改为驱动 Transform,不触发 Mesh 重建。
|
||||
/// 2. 管理网格区块(Chunk)— 按画布坐标分块,每块一个渲染器,避开 65535 顶点限制。
|
||||
/// 3. 数据分组 — 脏元素的坐标 ÷ chunkSize → Spiral2DEncode → 按 Chunk 写入渲染器。
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// 派生类只需实现三个抽象方法:
|
||||
/// <list type="bullet">
|
||||
/// <item><see cref="BuildChunkRenderer"/> — 在 Chunk GameObject 上创建渲染器组件</item>
|
||||
/// <item><see cref="AppendElementData"/> — 把元素数据写入该 Chunk 的渲染器</item>
|
||||
/// <item><see cref="ClearChunk"/> — 清空 Chunk 渲染器,准备下一帧重写</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <typeparam name="TChildRenderer">Chunk 使用的渲染器组件类型</typeparam>
|
||||
public abstract class BlueprintCanvasRenderTool<TChildRenderer> : BlueprintTool
|
||||
where TChildRenderer : MonoBehaviour
|
||||
/// <summary>Chunk renderer 后端基类:仅消费 Graph.ChunkState 的提交,不计算归属或可见性。</summary>
|
||||
public abstract class BlueprintCanvasRenderTool<TChildRenderer> : BlueprintTool where TChildRenderer : MonoBehaviour
|
||||
{
|
||||
// ====================================================================
|
||||
// 配置
|
||||
// ====================================================================
|
||||
|
||||
/// <summary>网格区块大小(画布空间单位)。一个 Chunk 覆盖 chunkSize × chunkSize 区域。</summary>
|
||||
protected int ChunkSize = 400;
|
||||
|
||||
/// <summary>Chunk 池的最大缓存数量</summary>
|
||||
protected int MaxPoolSize = 100;
|
||||
|
||||
// ====================================================================
|
||||
// Chunk 数据
|
||||
// ====================================================================
|
||||
|
||||
/// <summary>活跃区块:Spiral2DEncode ID → 渲染器实例</summary>
|
||||
protected Dictionary<ulong, TChildRenderer> ActiveChunks = new();
|
||||
|
||||
/// <summary>缓存池,减少创建/销毁</summary>
|
||||
protected Stack<TChildRenderer> ChunkPool = new();
|
||||
|
||||
/// <summary>Chunk 的父级 Transform(通常在 Viewport 下)</summary>
|
||||
protected readonly Dictionary<ulong, TChildRenderer> ActiveChunks = new Dictionary<ulong, TChildRenderer>();
|
||||
protected readonly Stack<TChildRenderer> ChunkPool = new Stack<TChildRenderer>();
|
||||
protected Transform ChunksParent;
|
||||
|
||||
// ====================================================================
|
||||
// Viewport 状态
|
||||
// ====================================================================
|
||||
|
||||
/// <summary>当前同步到的 Viewport Transform</summary>
|
||||
protected RectTransform ViewportRect;
|
||||
|
||||
private Vector2 _prevPan;
|
||||
private float _prevZoom;
|
||||
|
||||
// ====================================================================
|
||||
// 抽象方法(派生类实现)
|
||||
// ====================================================================
|
||||
|
||||
/// <summary>在 Chunk GameObject 上创建渲染器组件并返回。</summary>
|
||||
protected abstract TChildRenderer BuildChunkRenderer(GameObject chunkGo);
|
||||
|
||||
/// <summary>将元素数据写入指定 Chunk 的渲染器。</summary>
|
||||
protected abstract void AppendElementData(TChildRenderer renderer, IBlueprintElement element);
|
||||
|
||||
/// <summary>清空 Chunk 渲染器的数据,准备下一帧重写。</summary>
|
||||
/// <summary>当前模板方法提交;子类不得重新计算 chunk 本地偏移。</summary>
|
||||
protected BlueprintElementRenderSubmission CurrentElementSubmission { get; private set; }
|
||||
protected abstract void AppendElementData(TChildRenderer renderer, IBlueprintElement element, ulong chunkID);
|
||||
protected abstract void ClearChunkData(TChildRenderer renderer);
|
||||
|
||||
/// <summary>触发 Chunk 渲染器的脏标记,使其重建 Mesh。</summary>
|
||||
protected abstract void SetChunkDirty(TChildRenderer renderer);
|
||||
protected abstract bool AcceptsElement(IBlueprintElement element);
|
||||
/// <summary>每个非退出 Chunk 提交的开始钩子。</summary>
|
||||
protected virtual void BeginChunkSubmission(ulong chunkID, TChildRenderer renderer) { }
|
||||
/// <summary>每个非退出 Chunk 提交的结束钩子。</summary>
|
||||
protected virtual void EndChunkSubmission(ulong chunkID, TChildRenderer renderer) { }
|
||||
protected virtual void OnChunkRecycled(ulong chunkID, TChildRenderer renderer) { }
|
||||
|
||||
/// <summary>获取元素用于 Chunk 分组的坐标(通常是 Position 或中心点)。</summary>
|
||||
protected abstract Vector2 GetElementPosition(IBlueprintElement element);
|
||||
|
||||
/// <summary>检查元素是否需要被剔除(例如不在视口内)。</summary>
|
||||
protected abstract bool IsElementCulled(IBlueprintElement element);
|
||||
|
||||
// ====================================================================
|
||||
// 生命周期
|
||||
// ====================================================================
|
||||
|
||||
public override void OnInitialize()
|
||||
{
|
||||
EnsureViewport();
|
||||
}
|
||||
|
||||
public override void OnInitialize() => EnsureViewport();
|
||||
public override void OnConfigChanged() => Graph?.MarkAllElementsDirty();
|
||||
public override void OnRender()
|
||||
{
|
||||
if (Graph?.Canvas == null) return;
|
||||
|
||||
// 1. 确保层级存在
|
||||
if (Graph?.Canvas == null || Graph.ChunkState == null) return;
|
||||
EnsureViewport();
|
||||
|
||||
// 2. 同步 Viewport Transform(O(1),不重建 Mesh)
|
||||
if (ViewportRect == null) return;
|
||||
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)
|
||||
foreach (var submission in Graph.ChunkState.Submissions)
|
||||
{
|
||||
var renderer = GetOrCreateChunk(kvp.Key);
|
||||
if (submission.Kind == ChunkSubmissionKind.Exited) { RecycleChunk(submission.ChunkId); continue; }
|
||||
var renderer = GetOrCreateChunk(submission.ChunkId);
|
||||
if (renderer == null) continue;
|
||||
|
||||
ClearChunkData(renderer);
|
||||
foreach (var element in kvp.Value)
|
||||
AppendElementData(renderer, element);
|
||||
|
||||
BeginChunkSubmission(submission.ChunkId, renderer);
|
||||
var session = new RenderSession(Graph, submission);
|
||||
if (submission.Elements != null)
|
||||
foreach (var element in submission.Elements)
|
||||
if (AcceptsElement(element)) { CurrentElementSubmission = session.CreateElementSubmission(element); AppendElementData(renderer, element, submission.ChunkId); }
|
||||
CurrentElementSubmission = null;
|
||||
EndChunkSubmission(submission.ChunkId, renderer);
|
||||
SetChunkDirty(renderer);
|
||||
}
|
||||
|
||||
// 6. 清理空 Chunk
|
||||
CleanupInactiveChunks(groups.Keys);
|
||||
}
|
||||
|
||||
public override void OnDestroy()
|
||||
{
|
||||
DestroyAllChunks();
|
||||
ViewportRect = null;
|
||||
ChunksParent = null;
|
||||
DestroyAllChunks(); ViewportRect = null; ChunksParent = null;
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// Viewport
|
||||
// ====================================================================
|
||||
|
||||
/// <summary>获取或创建 Viewport Transform。</summary>
|
||||
protected virtual void EnsureViewport()
|
||||
{
|
||||
if (ViewportRect != null) return;
|
||||
if (Graph?.Canvas == null) return;
|
||||
|
||||
if (ViewportRect != null || 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 = root.Find("__Bp_Viewport") as RectTransform;
|
||||
if (ViewportRect == null)
|
||||
{
|
||||
ViewportRect = existing.GetComponent<RectTransform>();
|
||||
ChunksParent = existing;
|
||||
return;
|
||||
var go = new GameObject("__Bp_Viewport", typeof(RectTransform)); go.hideFlags = HideFlags.HideAndDontSave;
|
||||
ViewportRect = go.GetComponent<RectTransform>(); ViewportRect.SetParent(root, false);
|
||||
}
|
||||
|
||||
// 创建 Viewport
|
||||
var vpGo = new GameObject("__Bp_Viewport", typeof(RectTransform));
|
||||
vpGo.hideFlags = HideFlags.HideAndDontSave;
|
||||
vpGo.transform.SetParent(root, false);
|
||||
vpGo.transform.SetAsFirstSibling(); // 在 GridBackground 之下
|
||||
|
||||
ViewportRect = vpGo.GetComponent<RectTransform>();
|
||||
ViewportRect.anchorMin = Vector2.zero;
|
||||
ViewportRect.anchorMax = Vector2.one;
|
||||
ViewportRect.offsetMin = Vector2.zero;
|
||||
ViewportRect.offsetMax = Vector2.zero;
|
||||
|
||||
ChunksParent = ViewportRect;
|
||||
|
||||
// 记录初始状态
|
||||
_prevPan = Graph.Canvas.PanOffset;
|
||||
_prevZoom = Graph.Canvas.ZoomLevel;
|
||||
ViewportRect.anchoredPosition = -_prevPan;
|
||||
ViewportRect.localScale = Vector3.one * _prevZoom;
|
||||
var rootRect = root as RectTransform;
|
||||
var pivot = rootRect != null ? rootRect.pivot : new Vector2(.5f, .5f);
|
||||
ViewportRect.anchorMin = pivot; ViewportRect.anchorMax = pivot; ViewportRect.pivot = pivot;
|
||||
ViewportRect.anchoredPosition = Vector2.zero; ViewportRect.sizeDelta = rootRect != null ? rootRect.rect.size : Vector2.zero;
|
||||
ChunksParent = ViewportRect; _prevPan = Graph.Canvas.PanOffset; _prevZoom = Graph.Canvas.ZoomLevel;
|
||||
ViewportRect.anchoredPosition = _prevPan; ViewportRect.localScale = Vector3.one * _prevZoom;
|
||||
}
|
||||
|
||||
/// <summary>将画布的 Pan/Zoom 同步到 Viewport Transform。</summary>
|
||||
protected virtual void SyncViewportTransform()
|
||||
{
|
||||
if (ViewportRect == null) return;
|
||||
|
||||
var pan = Graph.Canvas.PanOffset;
|
||||
var zoom = Graph.Canvas.ZoomLevel;
|
||||
|
||||
if (_prevPan != pan)
|
||||
{
|
||||
ViewportRect.anchoredPosition = -pan;
|
||||
_prevPan = pan;
|
||||
}
|
||||
|
||||
if (!Mathf.Approximately(_prevZoom, zoom))
|
||||
{
|
||||
ViewportRect.localScale = Vector3.one * zoom;
|
||||
_prevZoom = zoom;
|
||||
}
|
||||
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 管理
|
||||
// ====================================================================
|
||||
protected Vector2 ChunkOrigin(ulong id) => Graph.ChunkState.GetChunkOrigin(id);
|
||||
|
||||
/// <summary>将画布空间坐标转为 Chunk 坐标。</summary>
|
||||
protected (int cx, int cy) PositionToCoord(Vector2 canvasPos)
|
||||
/// <summary>一次 chunk 提交的标准化坐标、LOD 与版本上下文。</summary>
|
||||
protected sealed class RenderSession
|
||||
{
|
||||
return (
|
||||
Mathf.FloorToInt(canvasPos.x / ChunkSize),
|
||||
Mathf.FloorToInt(canvasPos.y / ChunkSize)
|
||||
);
|
||||
private readonly BlueprintGraph _graph; private readonly ChunkSubmission _submission; private readonly Vector2 _origin; private readonly Rect _bounds;
|
||||
public RenderSession(BlueprintGraph graph, ChunkSubmission submission) { _graph=graph; _submission=submission; _origin=graph.ChunkState.GetChunkOrigin(submission.ChunkId); _bounds=graph.ChunkState.GetChunkRect(submission.ChunkId); }
|
||||
public BlueprintElementRenderSubmission CreateElementSubmission(IBlueprintElement element) => new BlueprintElementRenderSubmission(element, element.BoundingBox.center, element.BoundingBox, _origin, _bounds, _graph.CurrentLod, _submission.ContentRevision, _submission.LodRevision);
|
||||
}
|
||||
|
||||
/// <summary>Chunk 坐标 → 螺旋曲线 ID。</summary>
|
||||
protected ulong CoordToID(int cx, int cy)
|
||||
public sealed class BlueprintElementRenderSubmission
|
||||
{
|
||||
return MacroCurveMapping.Spiral2DEncode(cx, cy);
|
||||
public readonly IBlueprintElement Element; public readonly Vector2 CanvasPosition, ChunkOrigin, ChunkLocalPosition; public readonly Rect CanvasBounds, ChunkLocalBounds; public readonly BlueprintLodLevel LOD; public readonly int ContentRevision, LodRevision;
|
||||
public BlueprintElementRenderSubmission(IBlueprintElement element, Vector2 canvasPosition, Rect canvasBounds, Vector2 chunkOrigin, Rect chunkBounds, BlueprintLodLevel lod, int contentRevision, int lodRevision)
|
||||
{ Element=element; CanvasPosition=canvasPosition; CanvasBounds=canvasBounds; ChunkOrigin=chunkOrigin; ChunkLocalPosition=canvasPosition-chunkOrigin; ChunkLocalBounds=new Rect(canvasBounds.position-chunkOrigin, canvasBounds.size); LOD=lod; ContentRevision=contentRevision; LodRevision=lodRevision; }
|
||||
/// <summary>将提交元素相关的 Canvas 空间数据转换为当前 Chunk 局部空间。</summary>
|
||||
public Vector2 ToChunkLocal(Vector2 canvasPosition) => canvasPosition - ChunkOrigin;
|
||||
public Rect ToChunkLocal(Rect canvasBounds) => new Rect(canvasBounds.position - ChunkOrigin, canvasBounds.size);
|
||||
}
|
||||
|
||||
/// <summary>螺旋曲线 ID → Chunk 坐标。</summary>
|
||||
protected void IDToCoord(ulong id, out int cx, out int cy)
|
||||
protected TChildRenderer GetOrCreateChunk(ulong id)
|
||||
{
|
||||
MacroCurveMapping.Spiral2DDecode(id, out cx, out cy);
|
||||
}
|
||||
|
||||
/// <summary>获取或创建坐标对应的 Chunk 渲染器。</summary>
|
||||
protected TChildRenderer GetOrCreateChunk(ulong chunkID)
|
||||
{
|
||||
if (ActiveChunks.TryGetValue(chunkID, out var renderer) && renderer != null)
|
||||
return renderer;
|
||||
|
||||
IDToCoord(chunkID, out int cx, out int cy);
|
||||
|
||||
if (ActiveChunks.TryGetValue(id, out var renderer) && renderer != null) return renderer;
|
||||
BlueprintChunkState.DecodeCoord(id, out int x, out int y);
|
||||
GameObject go;
|
||||
|
||||
// 从池中取或新建
|
||||
if (ChunkPool.Count > 0)
|
||||
{
|
||||
go = ChunkPool.Pop().gameObject;
|
||||
go.SetActive(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
go = new GameObject($"Chunk_{cx}_{cy}", typeof(RectTransform));
|
||||
go.hideFlags = HideFlags.HideAndDontSave;
|
||||
go.transform.SetParent(ChunksParent, false);
|
||||
}
|
||||
|
||||
// 定位 Chunk
|
||||
var rt = go.GetComponent<RectTransform>();
|
||||
rt.pivot = Vector2.zero; // 左下角为原点
|
||||
rt.anchorMin = Vector2.zero;
|
||||
rt.anchorMax = Vector2.zero; // 不拉伸,绝对定位
|
||||
rt.anchoredPosition = new Vector2(cx * ChunkSize, cy * ChunkSize);
|
||||
rt.sizeDelta = new Vector2(ChunkSize, ChunkSize);
|
||||
|
||||
// 获取或创建渲染器
|
||||
renderer = go.GetComponent<TChildRenderer>();
|
||||
if (renderer == null)
|
||||
renderer = BuildChunkRenderer(go);
|
||||
|
||||
ActiveChunks[chunkID] = renderer;
|
||||
return renderer;
|
||||
if (ChunkPool.Count > 0) { go = ChunkPool.Pop().gameObject; go.SetActive(true); }
|
||||
else { go = new GameObject("Chunk_" + x + "_" + y, typeof(RectTransform)); go.hideFlags = HideFlags.HideAndDontSave; go.transform.SetParent(ChunksParent, false); }
|
||||
var rt = go.GetComponent<RectTransform>(); var pivot = ViewportRect.pivot;
|
||||
rt.anchorMin = pivot; rt.anchorMax = pivot; rt.pivot = pivot; rt.anchoredPosition = ChunkOrigin(id);
|
||||
rt.sizeDelta = new Vector2(Graph.ChunkState.ChunkSize, Graph.ChunkState.ChunkSize);
|
||||
renderer = go.GetComponent<TChildRenderer>() ?? BuildChunkRenderer(go); ActiveChunks[id] = renderer; return renderer;
|
||||
}
|
||||
|
||||
/// <summary>收集脏元素并按 Chunk 分组。</summary>
|
||||
protected virtual Dictionary<ulong, List<IBlueprintElement>> GroupDirtyElements()
|
||||
private void RecycleChunk(ulong id)
|
||||
{
|
||||
var groups = new Dictionary<ulong, List<IBlueprintElement>>();
|
||||
var dirtyList = Graph.DirtyFlags.FlushDirty();
|
||||
|
||||
foreach (var element in dirtyList)
|
||||
{
|
||||
if (element == null) continue;
|
||||
|
||||
// 剔除
|
||||
if (IsElementCulled(element)) continue;
|
||||
|
||||
var pos = GetElementPosition(element);
|
||||
var (cx, cy) = PositionToCoord(pos);
|
||||
ulong id = CoordToID(cx, cy);
|
||||
|
||||
if (!groups.TryGetValue(id, out var list))
|
||||
{
|
||||
list = new List<IBlueprintElement>();
|
||||
groups[id] = list;
|
||||
}
|
||||
list.Add(element);
|
||||
}
|
||||
|
||||
return groups;
|
||||
if (!ActiveChunks.TryGetValue(id, out var renderer) || renderer == null) { ActiveChunks.Remove(id); return; }
|
||||
OnChunkRecycled(id, renderer);
|
||||
if (ChunkPool.Count < MaxPoolSize) { renderer.gameObject.SetActive(false); ChunkPool.Push(renderer); }
|
||||
else UnityEngine.Object.Destroy(renderer.gameObject);
|
||||
ActiveChunks.Remove(id);
|
||||
}
|
||||
|
||||
/// <summary>清理不再包含脏元素的 Chunk。</summary>
|
||||
protected virtual void CleanupInactiveChunks(Dictionary<ulong, List<IBlueprintElement>>.KeyCollection activeIDs)
|
||||
{
|
||||
var toRemove = new List<ulong>();
|
||||
foreach (var kvp in ActiveChunks)
|
||||
{
|
||||
if (!System.Linq.Enumerable.Contains(activeIDs, kvp.Key))
|
||||
toRemove.Add(kvp.Key);
|
||||
}
|
||||
|
||||
foreach (var id in toRemove)
|
||||
{
|
||||
if (!ActiveChunks.TryGetValue(id, out var renderer) || renderer == null)
|
||||
continue;
|
||||
|
||||
// 入池或销毁
|
||||
if (ChunkPool.Count < MaxPoolSize)
|
||||
{
|
||||
renderer.gameObject.SetActive(false);
|
||||
ChunkPool.Push(renderer);
|
||||
}
|
||||
else
|
||||
{
|
||||
UnityEngine.Object.Destroy(renderer.gameObject);
|
||||
}
|
||||
|
||||
ActiveChunks.Remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>销毁所有 Chunk。</summary>
|
||||
protected virtual void DestroyAllChunks()
|
||||
{
|
||||
foreach (var kvp in ActiveChunks)
|
||||
{
|
||||
if (kvp.Value != null)
|
||||
UnityEngine.Object.Destroy(kvp.Value.gameObject);
|
||||
}
|
||||
foreach (var pair in ActiveChunks) if (pair.Value != null) UnityEngine.Object.Destroy(pair.Value.gameObject);
|
||||
ActiveChunks.Clear();
|
||||
|
||||
while (ChunkPool.Count > 0)
|
||||
{
|
||||
var pooled = ChunkPool.Pop();
|
||||
if (pooled != null)
|
||||
UnityEngine.Object.Destroy(pooled.gameObject);
|
||||
}
|
||||
while (ChunkPool.Count > 0) { var item = ChunkPool.Pop(); if (item != null) UnityEngine.Object.Destroy(item.gameObject); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c7cfe95b8a3355c4e8e6585d2266fb5b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,145 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 32753aff001677a4b81e10dfae39b4cb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,65 +0,0 @@
|
||||
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。\n旧输入系统 scrollDelta.y 约 ±3,默认 30 = 每格 ±0.1")]
|
||||
[Range(1f, 200f)]
|
||||
public float ZoomDivisor = 30f;
|
||||
|
||||
[Header("拖拽平移")]
|
||||
[Tooltip("中键拖拽平移速度系数。1=鼠标移动 1px,画布移动 1px(经缩放校正)")]
|
||||
[Range(0.1f, 10f)]
|
||||
public float DragPanSpeed = 1f;
|
||||
|
||||
[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("键盘 +/- 缩放速度(与 ZoomDivisor 配合,公式:zoom += 1 / ZoomDivisor)")]
|
||||
[Range(0.005f, 0.5f)]
|
||||
public float KeyboardZoomSpeed = 0.05f;
|
||||
|
||||
[Header("框选")]
|
||||
[Tooltip("框选拖拽触发阈值(画布单位)。拖拽距离超过此值后才显示选框并开始框选")]
|
||||
[Range(1f, 100f)]
|
||||
public float BoxSelectThreshold = 10f;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cfce52570fac4f5488becd85ce4f023b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,61 +0,0 @@
|
||||
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 => typeof(NodeCanvasRenderTool);
|
||||
|
||||
[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;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8ab3d3829c73815468ed8c5ae306a6ee
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,63 +0,0 @@
|
||||
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 => typeof(WireCanvasRenderTool);
|
||||
|
||||
[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;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 21208cffd6108e04d815bb385f0af3fc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,278 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
using XericLibrary.Runtime.UIGraph;
|
||||
using PrimitiveType = XericLibrary.Runtime.UIGraph.PrimitiveType;
|
||||
|
||||
namespace XericLibrary.Runtime.Blueprint.Render
|
||||
{
|
||||
/// <summary>
|
||||
/// 节点渲染工具 — 按网格区块管理 BlueprintPrimitiveRenderer,支持 Viewport Transform。
|
||||
/// <para>每个 Chunk 一个 PrimitiveRenderer + 每个节点一个 TMP Text(挂在该 Chunk 下)。</para>
|
||||
/// LOD 逻辑保留自 QuickUGUIGraphNodeRenderTool。
|
||||
/// </summary>
|
||||
[BlueprintTool(phase: ToolPhase.Render, order: 100)]
|
||||
[BlueprintTheme("QuickGraph")]
|
||||
public class NodeCanvasRenderTool : BlueprintCanvasRenderTool<BlueprintPrimitiveRenderer>
|
||||
{
|
||||
private enum LodLevel { Minimal, Simplified, Full }
|
||||
|
||||
private QuickGraphNodeRenderConfig _defaultConfig;
|
||||
private QuickGraphNodeRenderConfig Config
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ToolConfig is QuickGraphNodeRenderConfig external) return external;
|
||||
if (_defaultConfig == null)
|
||||
_defaultConfig = ScriptableObject.CreateInstance<QuickGraphNodeRenderConfig>();
|
||||
return _defaultConfig;
|
||||
}
|
||||
}
|
||||
|
||||
// TMP 文本:节点 → (ChunkID, Text 组件)
|
||||
private Dictionary<string, (ulong chunkID, TMP_Text text)> _nodeTexts
|
||||
= new Dictionary<string, (ulong, TMP_Text)>();
|
||||
|
||||
// ====================================================================
|
||||
// 抽象实现
|
||||
// ====================================================================
|
||||
|
||||
protected override BlueprintPrimitiveRenderer BuildChunkRenderer(GameObject chunkGo)
|
||||
{
|
||||
return chunkGo.AddComponent<BlueprintPrimitiveRenderer>();
|
||||
}
|
||||
|
||||
protected override void AppendElementData(BlueprintPrimitiveRenderer renderer, IBlueprintElement element)
|
||||
{
|
||||
if (!(element is BlueprintNode node)) return;
|
||||
|
||||
var cfg = Config;
|
||||
float zoom = Graph.Canvas.ZoomLevel;
|
||||
LodLevel lod = GetLodLevel();
|
||||
|
||||
// 端口位置始终用原始尺寸计算
|
||||
CalculatePortPositions(node, cfg.NodeWidth, cfg.NodeHeight);
|
||||
|
||||
// 节点尺寸和位置(画布空间,不转换,由 Viewport Transform 统一变换)
|
||||
float nw = cfg.NodeWidth;
|
||||
float nh = cfg.NodeHeight;
|
||||
float border = (lod == LodLevel.Minimal) ? 0f : cfg.BorderThickness;
|
||||
Vector2 pos = node.Position;
|
||||
|
||||
Color bgColor, borderColor;
|
||||
if (lod == LodLevel.Minimal)
|
||||
{
|
||||
bgColor = node.NodeColor;
|
||||
borderColor = node.NodeColor;
|
||||
}
|
||||
else
|
||||
{
|
||||
bgColor = node.NodeBGColor;
|
||||
borderColor = node.NodeColor;
|
||||
}
|
||||
|
||||
var primEntry = new PrimitiveCacheEntry
|
||||
{
|
||||
type = PrimitiveType.Rectangle,
|
||||
sizeMode = SizeMode.InscribedEllipse,
|
||||
center = pos,
|
||||
size = new Vector2(nw, nh),
|
||||
angle = 0f,
|
||||
@params = new PrimitiveParams
|
||||
{
|
||||
axisScaleX = 1f,
|
||||
axisScaleY = 1f,
|
||||
sideCount = 4,
|
||||
chamferSize = cfg.ChamferSize,
|
||||
chamferSegments = cfg.ChamferSegments,
|
||||
bgColor = new Color32(0, 0, 0, 0),
|
||||
centerColor = bgColor,
|
||||
borderColor = borderColor,
|
||||
borderThickness = border,
|
||||
}
|
||||
};
|
||||
renderer.AddPrimitive(primEntry);
|
||||
|
||||
// LOD Full 时显示文本
|
||||
if (lod == LodLevel.Full)
|
||||
UpdateNodeText(node, pos, nw, nh, border, cfg);
|
||||
else
|
||||
HideNodeText(node);
|
||||
}
|
||||
|
||||
protected override void ClearChunkData(BlueprintPrimitiveRenderer renderer)
|
||||
{
|
||||
renderer.ClearAll();
|
||||
}
|
||||
|
||||
protected override void SetChunkDirty(BlueprintPrimitiveRenderer renderer)
|
||||
{
|
||||
renderer.SetPrimitiveDirty(0);
|
||||
}
|
||||
|
||||
protected override Vector2 GetElementPosition(IBlueprintElement element)
|
||||
{
|
||||
return element is BlueprintNode node ? node.Position : Vector2.zero;
|
||||
}
|
||||
|
||||
protected override bool IsElementCulled(IBlueprintElement element)
|
||||
{
|
||||
if (!(element is BlueprintNode node)) return true;
|
||||
var viewport = Graph?.Canvas?.GetViewportRect() ?? new Rect();
|
||||
float margin = Config.NodeWidth + Config.NodeHeight;
|
||||
viewport.xMin -= margin;
|
||||
viewport.xMax += margin;
|
||||
viewport.yMin -= margin;
|
||||
viewport.yMax += margin;
|
||||
return !viewport.Contains(node.Position);
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// LOD
|
||||
// ====================================================================
|
||||
|
||||
private LodLevel GetLodLevel()
|
||||
{
|
||||
var cfg = Config;
|
||||
float min = Graph.Canvas.MinZoom;
|
||||
float max = Graph.Canvas.MaxZoom;
|
||||
float range = max - min;
|
||||
float normalized = range > 0.001f ? (Graph.Canvas.ZoomLevel - min) / range : 1f;
|
||||
|
||||
if (normalized < cfg.Lod0Threshold) return LodLevel.Minimal;
|
||||
if (normalized < cfg.Lod1Threshold) return LodLevel.Simplified;
|
||||
return LodLevel.Full;
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 端口位置(和旧渲染工具一致)
|
||||
// ====================================================================
|
||||
|
||||
private static void CalculatePortPositions(BlueprintNode node, float nw, float nh)
|
||||
{
|
||||
int inCount = node.InputPorts.Count;
|
||||
int outCount = node.OutputPorts.Count;
|
||||
|
||||
for (int i = 0; i < inCount; i++)
|
||||
{
|
||||
float y = nh * (0.5f - (float)(i + 1) / (inCount + 1));
|
||||
node.InputPorts[i].RelativePosition = new Vector2(-nw * 0.5f, y);
|
||||
}
|
||||
for (int i = 0; i < outCount; i++)
|
||||
{
|
||||
float y = nh * (0.5f - (float)(i + 1) / (outCount + 1));
|
||||
node.OutputPorts[i].RelativePosition = new Vector2(nw * 0.5f, y);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// TMP 文本 — 挂到节点所属的 Chunk 下
|
||||
// ====================================================================
|
||||
|
||||
private void UpdateNodeText(BlueprintNode node, Vector2 pos, float nw, float nh,
|
||||
float border, QuickGraphNodeRenderConfig cfg)
|
||||
{
|
||||
var (cx, cy) = PositionToCoord(pos);
|
||||
ulong chunkID = CoordToID(cx, cy);
|
||||
|
||||
if (_nodeTexts.TryGetValue(node.ElementId, out var existing))
|
||||
{
|
||||
// 节点已换 Chunk → 清理旧文本
|
||||
if (existing.chunkID != chunkID)
|
||||
{
|
||||
Object.Destroy(existing.text.gameObject);
|
||||
_nodeTexts.Remove(node.ElementId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 复用现有文本
|
||||
var tmp = existing.text;
|
||||
tmp.text = node.NodeTitle;
|
||||
tmp.color = node.NodeTextColor;
|
||||
tmp.fontSize = cfg.TitleFontSize;
|
||||
if (cfg.TitleFont != null) tmp.font = cfg.TitleFont;
|
||||
var rt = tmp.rectTransform;
|
||||
rt.anchorMin = Vector2.zero;
|
||||
rt.anchorMax = Vector2.zero;
|
||||
rt.anchoredPosition = new Vector2(pos.x - cx * ChunkSize, pos.y - cy * ChunkSize);
|
||||
rt.sizeDelta = new Vector2(nw - border * 2f, nh - border * 2f);
|
||||
rt.SetAsLastSibling();
|
||||
tmp.gameObject.SetActive(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新文本
|
||||
var chunkGo = GetOrCreateChunk(chunkID).gameObject;
|
||||
var textGo = new GameObject($"__Text_{node.ElementId}", typeof(RectTransform));
|
||||
textGo.hideFlags = HideFlags.HideAndDontSave;
|
||||
textGo.transform.SetParent(chunkGo.transform, false);
|
||||
|
||||
var tmp2 = textGo.AddComponent<TextMeshProUGUI>();
|
||||
tmp2.alignment = TextAlignmentOptions.Center;
|
||||
tmp2.fontSize = cfg.TitleFontSize;
|
||||
tmp2.enableAutoSizing = false;
|
||||
tmp2.text = node.NodeTitle;
|
||||
tmp2.color = node.NodeTextColor;
|
||||
if (cfg.TitleFont != null) tmp2.font = cfg.TitleFont;
|
||||
|
||||
var rt2 = tmp2.rectTransform;
|
||||
rt2.pivot = new Vector2(0.5f, 0.5f);
|
||||
rt2.anchorMin = Vector2.zero; // 相对 Chunk 左下角
|
||||
rt2.anchorMax = Vector2.zero;
|
||||
// Chunk 原点 = 左下角(cx*size, cy*size),文本相对 Chunk 原点偏移
|
||||
rt2.anchoredPosition = new Vector2(pos.x - cx * ChunkSize, pos.y - cy * ChunkSize);
|
||||
rt2.sizeDelta = new Vector2(nw - border * 2f, nh - border * 2f);
|
||||
rt2.SetAsLastSibling();
|
||||
|
||||
_nodeTexts[node.ElementId] = (chunkID, tmp2);
|
||||
}
|
||||
|
||||
private void HideNodeText(BlueprintNode node)
|
||||
{
|
||||
if (_nodeTexts.TryGetValue(node.ElementId, out var existing))
|
||||
{
|
||||
existing.text.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 清理(重写基类,额外清理 TMP 文本)
|
||||
// ====================================================================
|
||||
|
||||
protected override void CleanupInactiveChunks(
|
||||
Dictionary<ulong, List<IBlueprintElement>>.KeyCollection activeIDs)
|
||||
{
|
||||
// 清理空 Chunk 中的文本
|
||||
var deadNodes = new List<string>();
|
||||
foreach (var kvp in _nodeTexts)
|
||||
{
|
||||
ulong chunkID = kvp.Value.chunkID;
|
||||
if (!System.Linq.Enumerable.Contains(activeIDs, chunkID))
|
||||
deadNodes.Add(kvp.Key);
|
||||
}
|
||||
foreach (var id in deadNodes)
|
||||
{
|
||||
if (_nodeTexts.TryGetValue(id, out var entry))
|
||||
{
|
||||
Object.Destroy(entry.text.gameObject);
|
||||
_nodeTexts.Remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
base.CleanupInactiveChunks(activeIDs);
|
||||
}
|
||||
|
||||
protected override void DestroyAllChunks()
|
||||
{
|
||||
foreach (var kvp in _nodeTexts)
|
||||
{
|
||||
if (kvp.Value.text != null)
|
||||
Object.Destroy(kvp.Value.text.gameObject);
|
||||
}
|
||||
_nodeTexts.Clear();
|
||||
|
||||
base.DestroyAllChunks();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 86aab959d7a9d7845b904f91e4533ba1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,162 +0,0 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace XericLibrary.Runtime.Blueprint.Render
|
||||
{
|
||||
/// <summary>
|
||||
/// 快速 UGUI 图论网格背景渲染工具。
|
||||
/// 在蓝图画布最底层创建一个 Image 组件绘制网格背景,
|
||||
/// 每帧将画布的 zoom / pan 同步到 Shader 的 _Transform 属性。
|
||||
/// <para>
|
||||
/// 背景 Image 在 <see cref="__Bp_RenderRoot"/> 下锚点撑满,
|
||||
/// 只响应画布属性和 dirty flag,不会因画布重建而销毁(见 <see cref="OnInitialize"/>)。
|
||||
/// Shader 参数(<c>_Transform</c> 的 scale/offset)在 <see cref="OnRender"/> 中随
|
||||
/// <c>ZoomLevel / PanOffset</c> 更新,与画布拖拽共享同一属性源。
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>材质引用注意:</b>
|
||||
/// 必须缓存材质实例(<see cref="_backgroundMaterial"/>)并直接写入该实例。
|
||||
/// UGUI 的 <c>Graphic.material</c> getter 可能返回 <c>materialForRendering</c>
|
||||
/// 的副本(与 setter 存入的 <c>m_Material</c> 不同),因此 <c>_backgroundImage.material</c>
|
||||
/// 不适合用于写 shader 属性。见 <c>Graphic.material</c> Unity 源码。
|
||||
/// </para>
|
||||
/// </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;
|
||||
|
||||
/// <summary>
|
||||
/// 缓存的背景材质实例。
|
||||
/// 由 <see cref="Config.GetOrCreateMaterial"/> 创建,终身保留。
|
||||
/// shader 属性始终写入此实例,避免 UGUI <c>materialForRendering</c> 副本问题。
|
||||
/// </summary>
|
||||
private Material _backgroundMaterial;
|
||||
|
||||
public override void OnInitialize()
|
||||
{
|
||||
EnsureBackground();
|
||||
}
|
||||
|
||||
public override void OnRender()
|
||||
{
|
||||
if (Graph == null) return;
|
||||
EnsureBackground();
|
||||
SyncMaterialProperties();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在 RenderRoot 下查找或创建背景 Image。
|
||||
/// 创建后终身保留,不随画布重建销毁。
|
||||
/// </summary>
|
||||
private void EnsureBackground()
|
||||
{
|
||||
if (_backgroundImage != null) return;
|
||||
if (Graph?.Canvas?.GetRootTransform() == null) return;
|
||||
|
||||
var root = Graph.Canvas.GetRootTransform();
|
||||
|
||||
var existing = root.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(root, 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;
|
||||
// 关闭 maskable:运行时背景 Image 不参与 UGUI Mask 裁剪,
|
||||
// 避免父级 Mask 组件触发 materialForRendering 克隆材质实例。
|
||||
// 材质写入(_Transform 等 shader 属性)直通 GPU,不受 IMaterialModifier 干扰。
|
||||
_backgroundImage.maskable = false;
|
||||
|
||||
ApplyMaterial();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从配置获取材质并缓存到 <see cref="_backgroundMaterial"/>,
|
||||
/// 然后赋值给 Image。
|
||||
/// 此方法仅执行一次,后续材质写入直接使用缓存的引用。
|
||||
/// </summary>
|
||||
private void ApplyMaterial()
|
||||
{
|
||||
if (_backgroundImage == null) return;
|
||||
if (_backgroundMaterial != null) return;
|
||||
|
||||
_backgroundMaterial = Config.GetOrCreateMaterial();
|
||||
if (_backgroundMaterial != null)
|
||||
_backgroundImage.material = _backgroundMaterial;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 每帧将当前画布 zoom / pan 同步到 Shader,
|
||||
/// 使用 <see cref="_backgroundMaterial"/> 缓存写入,确保与 GPU 使用的材质实例一致。
|
||||
/// </summary>
|
||||
private void SyncMaterialProperties()
|
||||
{
|
||||
if (_backgroundImage == null) return;
|
||||
if (_backgroundMaterial == null) return;
|
||||
if (Graph == null || Graph.Canvas == null) return;
|
||||
|
||||
var rt = _backgroundImage.rectTransform;
|
||||
float zoom = Graph.Canvas.ZoomLevel;
|
||||
Vector2 pan = Graph.Canvas.PanOffset;
|
||||
|
||||
Config.ApplyToMaterial(_backgroundMaterial, rt.rect.size, zoom, pan);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
_backgroundMaterial = null;
|
||||
Config.ReleaseMaterial();
|
||||
_defaultConfig = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: da293e1f23cfca24b9ca4d40f91d2d8c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,135 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using XericLibrary.Runtime.UIGraph;
|
||||
|
||||
namespace XericLibrary.Runtime.Blueprint.Render
|
||||
{
|
||||
/// <summary>
|
||||
/// 连线渲染工具 — 按网格区块管理 BlueprintCurveRenderer,支持 Viewport Transform。
|
||||
/// <para>连线跟随其源节点(SourcePort.OwnerNode)进入对应 Chunk 的 CurveRenderer。</para>
|
||||
/// </summary>
|
||||
[BlueprintTool(phase: ToolPhase.Render, order: 110)]
|
||||
[BlueprintTheme("QuickGraph")]
|
||||
public class WireCanvasRenderTool : BlueprintCanvasRenderTool<BlueprintCurveRenderer>
|
||||
{
|
||||
private QuickGraphWireRenderConfig _defaultConfig;
|
||||
private QuickGraphWireRenderConfig Config
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ToolConfig is QuickGraphWireRenderConfig external) return external;
|
||||
if (_defaultConfig == null)
|
||||
_defaultConfig = ScriptableObject.CreateInstance<QuickGraphWireRenderConfig>();
|
||||
return _defaultConfig;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 抽象实现
|
||||
// ====================================================================
|
||||
|
||||
protected override BlueprintCurveRenderer BuildChunkRenderer(GameObject chunkGo)
|
||||
{
|
||||
return chunkGo.AddComponent<BlueprintCurveRenderer>();
|
||||
}
|
||||
|
||||
protected override void AppendElementData(BlueprintCurveRenderer renderer, IBlueprintElement element)
|
||||
{
|
||||
if (!(element is BlueprintWire wire)) return;
|
||||
if (wire.SourcePort == null || wire.TargetPort == null) return;
|
||||
if (wire.SourcePort.OwnerNode == null || wire.TargetPort.OwnerNode == null) return;
|
||||
|
||||
var cfg = Config;
|
||||
|
||||
// 端点:画布空间坐标(由 Viewport Transform 统一变换)
|
||||
Vector2 startPos = wire.SourcePort.GetWorldPosition();
|
||||
Vector2 endPos = wire.TargetPort.GetWorldPosition();
|
||||
|
||||
// 手柄方向 = 端口法向
|
||||
float srcDirX = Mathf.Sign(wire.SourcePort.RelativePosition.x);
|
||||
float tgtDirX = Mathf.Sign(wire.TargetPort.RelativePosition.x);
|
||||
if (srcDirX == tgtDirX) srcDirX *= -1f;
|
||||
|
||||
// 手柄长度(原始值,由 Viewport 统一缩放)
|
||||
Vector2 handle1 = startPos + new Vector2(srcDirX * cfg.SourceHandleLength, 0f);
|
||||
Vector2 handle2 = endPos + new Vector2(tgtDirX * cfg.TargetHandleLength, 0f);
|
||||
|
||||
float ww = cfg.WireWidth;
|
||||
|
||||
var ctrlPts = new Vector3[]
|
||||
{
|
||||
new Vector3(startPos.x, startPos.y, ww),
|
||||
new Vector3(handle1.x, handle1.y, ww),
|
||||
new Vector3(handle2.x, handle2.y, ww),
|
||||
new Vector3(endPos.x, endPos.y, ww),
|
||||
};
|
||||
|
||||
Color32 srcColor = wire.SourcePort.OwnerNode.NodeColor;
|
||||
Color32 tgtColor = wire.TargetPort.OwnerNode.NodeColor;
|
||||
|
||||
var arrow = new ArrowHeadData
|
||||
{
|
||||
shape = cfg.ArrowShape,
|
||||
reversed = cfg.ArrowReversed,
|
||||
progress = cfg.ArrowProgress,
|
||||
width = cfg.ArrowWidth,
|
||||
height = cfg.ArrowHeight,
|
||||
depthCompensation = cfg.ArrowDepthCompensation,
|
||||
color = tgtColor,
|
||||
};
|
||||
|
||||
int count = ctrlPts.Length;
|
||||
var ctrlPts2 = new Vector2[count];
|
||||
var widths = new float[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
ctrlPts2[i] = ctrlPts[i];
|
||||
widths[i] = ctrlPts[i].z;
|
||||
}
|
||||
|
||||
var curveEntry = new CurveCacheEntry
|
||||
{
|
||||
startColor = srcColor,
|
||||
endColor = tgtColor,
|
||||
tessellationSegments = cfg.TessellationSegments,
|
||||
};
|
||||
|
||||
renderer.AddCurve(curveEntry, ctrlPts2, widths, new[] { arrow });
|
||||
}
|
||||
|
||||
protected override void ClearChunkData(BlueprintCurveRenderer renderer)
|
||||
{
|
||||
renderer.ClearAll();
|
||||
}
|
||||
|
||||
protected override void SetChunkDirty(BlueprintCurveRenderer renderer)
|
||||
{
|
||||
renderer.RebuildAll();
|
||||
}
|
||||
|
||||
protected override Vector2 GetElementPosition(IBlueprintElement element)
|
||||
{
|
||||
if (!(element is BlueprintWire wire)) return Vector2.zero;
|
||||
// 以源节点的位置作为 Chunk 分组依据
|
||||
return wire.SourcePort?.OwnerNode?.Position ?? Vector2.zero;
|
||||
}
|
||||
|
||||
protected override bool IsElementCulled(IBlueprintElement element)
|
||||
{
|
||||
if (!(element is BlueprintWire wire)) return true;
|
||||
if (wire.SourcePort?.OwnerNode == null || wire.TargetPort?.OwnerNode == null)
|
||||
return true;
|
||||
|
||||
var viewport = Graph?.Canvas?.GetViewportRect() ?? new Rect();
|
||||
float margin = Config.WireWidth * 20f;
|
||||
viewport.xMin -= margin;
|
||||
viewport.xMax += margin;
|
||||
viewport.yMin -= margin;
|
||||
viewport.yMax += margin;
|
||||
|
||||
// 两端都不在视口内则跳过
|
||||
return !viewport.Contains(wire.SourcePort.OwnerNode.Position)
|
||||
&& !viewport.Contains(wire.TargetPort.OwnerNode.Position);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3881805202e8cd049aad2d55c9f595f1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user