update 0.6.3.7
This commit is contained in:
@@ -66,14 +66,10 @@ Xeric Library 是一个专注代码的 Unity 扩展库。
|
||||
| `PlayerController/` | 第一/三人称及上帝视角控制器 |
|
||||
| `SuperStyleSheet/` | XSSS 样式表运行时 |
|
||||
| `Debugger/` | Gizmos 调试绘制 |
|
||||
| `Nav/` | A* 寻路 |
|
||||
| `Net/` | LAN 局域网通信 |
|
||||
| `Generation/` | 程序化生成(楼梯、平面开孔等) |
|
||||
| `Security/` | 程序流安全控制 |
|
||||
| `CollisionLOD/` | 碰撞体 LOD 系统 |
|
||||
| `Generation/` | 程序化生成(2DUGUI图元,线条,曲线渲染器) |
|
||||
| `XericComponent/` | 组件系统(样条曲线、网格布局、生命周期核心) |
|
||||
| `RegistrationActivation/` | 注册激活码校验 |
|
||||
| `MachineID/` | 机器码验证授权 |
|
||||
| `RegistrationActivation/` | 注册激活码校验(提供局域网授权分发功能,插件本身并无需要授权激活的功能) |
|
||||
|
||||
---
|
||||
|
||||
@@ -91,9 +87,9 @@ Xeric Library 是一个专注代码的 Unity 扩展库。
|
||||
|
||||
## 示例文件
|
||||
|
||||
- **Roslyn 检查器**:位于 `Release/Samples~/XericUnityAnalyzer/`,包含编译后的分析器 DLL 及安装说明。提供组件搜索、命名规范、对象关系、富文本检测等代码分析规则,源码见 `Developer/XericUnityAnalyzer/`。
|
||||
- **配置界面 UI**:`Release/Samples~/ConfigUIElement/`,自动化配置界面的预制体与脚本示例。
|
||||
- **轨迹线路**:`Release/Samples~/HorizonLineOrbit/`,平面线路绘制与导航示例。
|
||||
- **Roslyn 检查器**:,包含编译后的分析器 DLL 及安装说明。提供组件搜索、命名规范、对象关系、富文本检测等代码分析规则。
|
||||
- **配置界面 UI**:自动化配置界面的预制体与脚本示例。
|
||||
- **轨迹线路**:平面线路绘制与导航示例。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -7,4 +7,4 @@ ScriptedImporter:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 11500000, guid: 625f186215c104763be7675aa2d941aa, type: 3}
|
||||
script: {instanceID: 0}
|
||||
|
||||
@@ -9,8 +9,6 @@ Material:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: BlueprintBackgound_GridLine
|
||||
m_Shader: {fileID: 4800000, guid: 2571bd49141e8ec429a8c3ec86b5da5b, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
@@ -19,7 +17,6 @@ Material:
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap: {}
|
||||
disabledShaderPasses: []
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
@@ -33,8 +30,9 @@ Material:
|
||||
- _Float2: 0.95
|
||||
- _Float3: 1
|
||||
- _GridExp: 0.001
|
||||
- _GridLineThreshold: 0.98
|
||||
- _GridLineThreshold: 0.99
|
||||
- _GridOverlayPower: 5
|
||||
- _LodTransthd: 200
|
||||
- _OverlayerMult: 1
|
||||
- _Stencil: 0
|
||||
- _StencilComp: 8
|
||||
@@ -44,8 +42,8 @@ Material:
|
||||
- _UseUIAlphaClip: 0
|
||||
m_Colors:
|
||||
- _Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
- _GridBackgroundColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _GridColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
- _Transform: {r: 19.2, g: 10.8, b: -0, a: -0}
|
||||
- _GridBackgroundColor: {r: 0.8666667, g: 0.8862745, b: 0.93333334, a: 1}
|
||||
- _GridColor: {r: 0.654902, g: 0.67058825, b: 0.7019608, a: 1}
|
||||
- _Transform: {r: 8.90683, g: 7.61659, b: -4.453415, a: -3.808295}
|
||||
- _Vector0: {r: 0.95, g: 1, b: 0, a: 0}
|
||||
m_BuildTextureStacks: []
|
||||
|
||||
@@ -7,4 +7,4 @@ ScriptedImporter:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 11500000, guid: 625f186215c104763be7675aa2d941aa, type: 3}
|
||||
script: {instanceID: 0}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// ==================================================
|
||||
// Xeric Style Sheet (XSSS) - 超级干燥样式表 - 默认样式
|
||||
// ==================================================
|
||||
|
||||
namespace: default
|
||||
|
||||
button/TargetGraphic/ColorTint:UnityEngine.UI.ColorBlock/ {
|
||||
.NormalColor = (1,1,1,1)
|
||||
.HighlightedColor = (0.96,0.96,0.96,1)
|
||||
.PressedColor = (0.78,0.78,0.78,1)
|
||||
.SelectedColor = (0.96,0.96,0.96,1)
|
||||
.DisabledColor = (0.6,0.6,0.6,0.8)
|
||||
}
|
||||
|
||||
button/TargetGraphic/SpriteState:UnityEngine.UI.SpriteState/ {
|
||||
.NormalColor = guid::UnityEngine.Sprite
|
||||
.HighlightedColor = guid::UnityEngine.Sprite
|
||||
.PressedColor = guid::UnityEngine.Sprite
|
||||
.SelectedColor = guid::UnityEngine.Sprite
|
||||
.DisabledColor = guid::UnityEngine.Sprite
|
||||
}
|
||||
|
||||
button/TargetGraphic/Animation/ {
|
||||
.NormalColor = None
|
||||
.HighlightedColor = None
|
||||
.PressedColor = None
|
||||
.SelectedColor = None
|
||||
.DisabledColor = None
|
||||
}
|
||||
|
||||
toggle/TargetGraphic/ColorTint:UnityEngine.UI.ColorBlock/ {
|
||||
.NormalColor = (1,1,1,1)
|
||||
.HighlightedColor = (0.96,0.96,0.96,1)
|
||||
.PressedColor = (0.78,0.78,0.78,1)
|
||||
.SelectedColor = (0.96,0.96,0.96,1)
|
||||
.DisabledColor = (0.6,0.6,0.6,0.8)
|
||||
}
|
||||
|
||||
toggle/TargetGraphic/SpriteState:UnityEngine.UI.SpriteState/ {
|
||||
.NormalColor = guid::UnityEngine.Sprite
|
||||
.HighlightedColor = guid::UnityEngine.Sprite
|
||||
.PressedColor = guid::UnityEngine.Sprite
|
||||
.SelectedColor = guid::UnityEngine.Sprite
|
||||
.DisabledColor = guid::UnityEngine.Sprite
|
||||
}
|
||||
|
||||
toggle/TargetGraphic/Animation/ {
|
||||
.NormalColor = None
|
||||
.HighlightedColor = None
|
||||
.PressedColor = None
|
||||
.SelectedColor = None
|
||||
.DisabledColor = None
|
||||
}
|
||||
|
||||
image/ColorTint:UnityEngine.UI.ColorBlock/ {
|
||||
.NormalColor = (1,1,1,1)
|
||||
.Sprite = guid::UnityEngine.Sprite
|
||||
.Material = guid::UnityEngine.Material
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bf2a8de55a33d4243932204e98b72249
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 1693830282, guid: 65aae7f37e34be6409e0966d54c89b6d, type: 3}
|
||||
@@ -9,14 +9,14 @@ MonoBehaviour:
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 32753aff001677a4b81e10dfae39b4cb, type: 3}
|
||||
m_Script: {fileID: 11500000, guid: 38115e5721db6514bb8b8be6be10f26a, type: 3}
|
||||
m_Name: QuickGraphGridBackgroundConfig
|
||||
m_EditorClassIdentifier:
|
||||
ShaderName: XericLibrary/BluePrint/BlueprintBackgound_GridLine
|
||||
OverrideMaterial: {fileID: 0}
|
||||
GridSize: 100
|
||||
GridOverlayPower: 5
|
||||
GridLineThreshold: 0.95
|
||||
GridExp: 1
|
||||
GridColor: {r: 0.6156863, g: 0.6156863, b: 0.6156863, a: 1}
|
||||
GridBackgroundColor: {r: 0.1764706, g: 0.1764706, b: 0.1764706, a: 1}
|
||||
GridLineThreshold: 0.99
|
||||
GridExp: 0.001
|
||||
GridColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
GridBackgroundColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
|
||||
@@ -9,15 +9,12 @@ MonoBehaviour:
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: cfce52570fac4f5488becd85ce4f023b, type: 3}
|
||||
m_Script: {fileID: 11500000, guid: 4735b93342f4fde4a8ed46dd28b46d4c, type: 3}
|
||||
m_Name: QuickGraphInputConfig
|
||||
m_EditorClassIdentifier:
|
||||
PanLerpSpeed: 8
|
||||
ZoomLerpSpeed: 8.2
|
||||
ZoomDivisor: 53
|
||||
ZoomDivisor: 30
|
||||
DragPanSpeed: 1
|
||||
MinZoom: 0.1
|
||||
MaxZoom: 3
|
||||
KeyboardPanSpeed: 600
|
||||
KeyboardZoomSpeed: 0.05
|
||||
BoxSelectThreshold: 10
|
||||
|
||||
@@ -9,17 +9,17 @@ MonoBehaviour:
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 8ab3d3829c73815468ed8c5ae306a6ee, type: 3}
|
||||
m_Script: {fileID: 11500000, guid: 8038bf45323375b46a3a2820ddfe212e, type: 3}
|
||||
m_Name: QuickGraphNodeRenderConfig
|
||||
m_EditorClassIdentifier:
|
||||
NodeWidth: 120
|
||||
NodeHeight: 80
|
||||
BorderThickness: 5
|
||||
ChamferSize: 0.4
|
||||
ChamferSegments: 7
|
||||
DefaultBackgroundColor: {r: 0.16037738, g: 0.16037738, b: 0.16037738, a: 1}
|
||||
DefaultTextColor: {r: 0.6156863, g: 0.6156863, b: 0.6156863, a: 1}
|
||||
TitleFontSize: 20
|
||||
NodeWidth: 90
|
||||
NodeHeight: 130
|
||||
BorderThickness: 3
|
||||
ChamferSize: 0.2
|
||||
ChamferSegments: 4
|
||||
DefaultBackgroundColor: {r: 0.16, g: 0.16, b: 0.16, a: 1}
|
||||
DefaultTextColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
TitleFontSize: 14
|
||||
TitleFont: {fileID: 0}
|
||||
Lod0Threshold: 0.1
|
||||
Lod1Threshold: 0.15
|
||||
Lod0Threshold: 0.3
|
||||
Lod1Threshold: 0.7
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// QuickGraph runtime render style
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
namespace: quickgraph
|
||||
|
||||
quickgraph/node/ {
|
||||
width = 130:float
|
||||
height = 80:float
|
||||
border = 3:float
|
||||
chamfer = 0.2:float
|
||||
chamferSegments = 4:int
|
||||
titleFontSize = 14:float
|
||||
}
|
||||
|
||||
quickgraph/wire/ {
|
||||
width = 5:float
|
||||
hitRadius = 8:float
|
||||
sourceHandle = 80:float
|
||||
targetHandle = 80:float
|
||||
arrowWidth = 12:float
|
||||
arrowHeight = 10:float
|
||||
arrowProgress = 1:float
|
||||
arrowDepth = -1:float
|
||||
tessellation = 32:int
|
||||
arrowShape = Triangle:XericLibrary.Runtime.UIGraph.ArrowShape
|
||||
arrowReversed = false:bool
|
||||
}
|
||||
|
||||
quickgraph/grid/ {
|
||||
material = guid:c16d74b277ae17c479319990fca9477b:UnityEngine.Material:UnityEngine.Material @ResourceSoftReference:Packages/com.lrss3.deconstruction/Resources/Material/Graph/UI/Blueprint/BlueprintBackgound_GridLine.mat
|
||||
shader = res:XericLibrary/BluePrint/BlueprintBackgound_GridLine:string
|
||||
size = 100:float
|
||||
overlayPower = 5:float
|
||||
threshold = 0.99:float
|
||||
exp = 0.001:float
|
||||
color = #A7ABB3FF:UnityEngine.Color
|
||||
background = #DDE2EEFF:UnityEngine.Color
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 499645b6d899d1f4d936ee3eb0eb9b0b
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 1693830282, guid: 65aae7f37e34be6409e0966d54c89b6d, type: 3}
|
||||
@@ -9,16 +9,16 @@ MonoBehaviour:
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 21208cffd6108e04d815bb385f0af3fc, type: 3}
|
||||
m_Script: {fileID: 11500000, guid: c95f6e1f1ece40444839f0a29ac371e5, type: 3}
|
||||
m_Name: QuickGraphWireRenderConfig
|
||||
m_EditorClassIdentifier:
|
||||
WireWidth: 5
|
||||
SourceHandleLength: 120
|
||||
TargetHandleLength: 120
|
||||
SourceHandleLength: 80
|
||||
TargetHandleLength: 80
|
||||
ArrowShape: 0
|
||||
ArrowReversed: 0
|
||||
ArrowProgress: 1
|
||||
ArrowWidth: 20
|
||||
ArrowHeight: 30
|
||||
ArrowWidth: 12
|
||||
ArrowHeight: 10
|
||||
ArrowDepthCompensation: -1
|
||||
TessellationSegments: 24
|
||||
TessellationSegments: 32
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace XericLibrary.Runtime.Blueprint.Canvas
|
||||
{
|
||||
private readonly UnityEngine.Canvas _unityCanvas;
|
||||
private readonly RectTransform _renderRoot;
|
||||
private readonly RectTransform _viewport;
|
||||
|
||||
/// <summary>动态获取摄像机引用(不缓存),
|
||||
/// 自动适应 Canvas.renderMode 运行时的变化。</summary>
|
||||
@@ -29,10 +30,11 @@ namespace XericLibrary.Runtime.Blueprint.Canvas
|
||||
/// </summary>
|
||||
/// <param name="unityCanvas">所属的 Unity UGUI Canvas 组件</param>
|
||||
/// <param name="renderRoot">蓝图渲染根 RectTransform,所有蓝图层容器和背景挂载于此</param>
|
||||
public ScreenSpaceBlueprintCanvas(UnityEngine.Canvas unityCanvas, RectTransform renderRoot)
|
||||
public ScreenSpaceBlueprintCanvas(UnityEngine.Canvas unityCanvas, RectTransform renderRoot, RectTransform viewport = null)
|
||||
{
|
||||
_unityCanvas = unityCanvas;
|
||||
_renderRoot = renderRoot;
|
||||
_viewport = viewport != null ? viewport : renderRoot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -41,11 +43,27 @@ namespace XericLibrary.Runtime.Blueprint.Canvas
|
||||
/// 以 <see cref="_renderRoot"/> 为参照转换,
|
||||
/// 自动处理 Canvas 渲染模式和 RenderRoot 的偏移。
|
||||
/// </summary>
|
||||
public override Vector2 ScreenToCanvas(Vector2 screenPoint)
|
||||
public override Vector2 ScreenToLocal(Vector2 screenPoint)
|
||||
{
|
||||
RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||||
_renderRoot, screenPoint, WorldCamera, out Vector2 localPoint);
|
||||
return (localPoint - _panOffset) / _zoomLevel;
|
||||
return localPoint;
|
||||
}
|
||||
|
||||
public override Rect GetViewportLocalRect()
|
||||
{
|
||||
if (_viewport == null || _renderRoot == null) return base.GetViewportLocalRect();
|
||||
var corners = new Vector3[4];
|
||||
_viewport.GetWorldCorners(corners);
|
||||
Vector2 min = new Vector2(float.PositiveInfinity, float.PositiveInfinity);
|
||||
Vector2 max = new Vector2(float.NegativeInfinity, float.NegativeInfinity);
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
Vector3 local = _renderRoot.InverseTransformPoint(corners[i]);
|
||||
min = Vector2.Min(min, local);
|
||||
max = Vector2.Max(max, local);
|
||||
}
|
||||
return Rect.MinMaxRect(min.x, min.y, max.x, max.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -35,23 +35,16 @@ namespace XericLibrary.Runtime.Blueprint.Canvas
|
||||
/// </summary>
|
||||
/// <param name="screenPoint">屏幕空间的坐标点</param>
|
||||
/// <returns>画布空间的坐标点</returns>
|
||||
public override Vector2 ScreenToCanvas(Vector2 screenPoint)
|
||||
public override Vector2 ScreenToLocal(Vector2 screenPoint)
|
||||
{
|
||||
if (_camera == null)
|
||||
{
|
||||
return Vector2.zero;
|
||||
}
|
||||
if (_camera == null) return Vector2.zero;
|
||||
|
||||
Ray ray = _camera.ScreenPointToRay(screenPoint);
|
||||
Plane plane = new Plane(-_canvasTransform.forward, _canvasTransform.position);
|
||||
if (plane.Raycast(ray, out float dist))
|
||||
{
|
||||
Vector3 worldPoint = ray.GetPoint(dist);
|
||||
Vector3 local = _canvasTransform.InverseTransformPoint(worldPoint);
|
||||
return (new Vector2(local.x, local.y) - _panOffset) / _zoomLevel;
|
||||
}
|
||||
|
||||
return Vector2.zero;
|
||||
if (!plane.Raycast(ray, out float dist)) return Vector2.zero;
|
||||
Vector3 worldPoint = ray.GetPoint(dist);
|
||||
Vector3 local = _canvasTransform.InverseTransformPoint(worldPoint);
|
||||
return new Vector2(local.x, local.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
using UnityEngine.InputSystem;
|
||||
#endif
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
using XericLibrary.Runtime.Blueprint.Canvas;
|
||||
using XericLibrary.Runtime.Blueprint.Element;
|
||||
|
||||
namespace XericLibrary.Runtime.Blueprint
|
||||
{
|
||||
/// <summary>
|
||||
/// 图论蓝图组件 —— 在一个 UGUI Canvas 上挂载蓝图系统。
|
||||
/// <para>驱动方式:
|
||||
/// - 编辑器非运行态:EditorApplication.update → EditorTick
|
||||
/// - 运行时:MonoBehaviour.Update</para>
|
||||
/// 图论蓝图组件:Unity Inspector 配置、画布创建及调试入口。
|
||||
/// 帧驱动完全由 <see cref="BlueprintGraphSystem"/> 承担。
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
[ExecuteAlways]
|
||||
@@ -29,9 +23,12 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
[Tooltip("世界空间画布的相机引用")]
|
||||
[SerializeField] private Camera _worldCamera;
|
||||
|
||||
[Header("Theme")]
|
||||
[Tooltip("蓝图主题名称。留空使用默认(所有 QuickGraph 工具可见)。")]
|
||||
[SerializeField] private string _themeName = string.Empty;
|
||||
[Tooltip("屏幕空间模式实际可见区域;留空时使用蓝图宿主 RectTransform。")]
|
||||
[SerializeField] private RectTransform _viewportRectTransform;
|
||||
|
||||
[Header("Blueprint Context")]
|
||||
[Tooltip("嵌入式图配置:主题与按 TargetToolType 解析的工具配置资产。")]
|
||||
[SerializeField] private BlueprintGraphContext _context = new BlueprintGraphContext();
|
||||
|
||||
[Header("Input")]
|
||||
[Tooltip("新输入系统 InputActionAsset。留空则仅支持鼠标基础操作(中键拖动平移、滚轮缩放)。")]
|
||||
@@ -63,10 +60,6 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
}
|
||||
}
|
||||
|
||||
[Header("Tool Config Overrides")]
|
||||
[Tooltip("拖拽配置资产到此,渲染时自动按工具类型+主题匹配。")]
|
||||
[SerializeField] private List<BlueprintToolConfigBase> _toolConfigAssets = new List<BlueprintToolConfigBase>();
|
||||
|
||||
[Header("Test")]
|
||||
[Tooltip("是否自动生成测试节点")]
|
||||
[SerializeField] public bool _autoGenerateTestNodes = true;
|
||||
@@ -81,17 +74,16 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
/// <summary>当前画布</summary>
|
||||
public IBlueprintCanvas Canvas { get; private set; }
|
||||
|
||||
public BlueprintGraphContext Context => _context;
|
||||
public string ThemeName
|
||||
{
|
||||
get { return _themeName; }
|
||||
get { return _context?.ThemeName ?? string.Empty; }
|
||||
set
|
||||
{
|
||||
_themeName = value ?? string.Empty;
|
||||
if (Graph != null)
|
||||
{
|
||||
Graph.ThemeName = _themeName;
|
||||
Graph.RefreshTheme();
|
||||
}
|
||||
if (_context == null)
|
||||
_context = new BlueprintGraphContext();
|
||||
_context.ThemeName = value ?? string.Empty;
|
||||
Graph?.RequestContextRefresh(_context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,12 +91,8 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
// 编辑器下提前注册 tick
|
||||
if (!Application.isPlaying)
|
||||
EditorApplication.update += EditorTick;
|
||||
#endif
|
||||
|
||||
if (_context == null)
|
||||
_context = new BlueprintGraphContext();
|
||||
CreateCanvas();
|
||||
CreateGraph();
|
||||
SetupInput();
|
||||
@@ -132,6 +120,18 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
canvasGo.transform.SetParent(transform.parent, false);
|
||||
canvasGo.transform.SetAsLastSibling();
|
||||
transform.SetParent(canvasGo.transform, false);
|
||||
|
||||
// 将组件自身的 RectTransform 撑满整个 Canvas,
|
||||
// 确保 __Bp_RenderRoot 的尺寸与屏幕一致,
|
||||
// 避免 GetViewportRect() 基于默认 (100x100) 尺寸错误裁剪节点。
|
||||
var selfRt = GetComponent<RectTransform>();
|
||||
if (selfRt == null)
|
||||
selfRt = gameObject.AddComponent<RectTransform>();
|
||||
selfRt.anchorMin = Vector2.zero;
|
||||
selfRt.anchorMax = Vector2.one;
|
||||
selfRt.offsetMin = Vector2.zero;
|
||||
selfRt.offsetMax = Vector2.zero;
|
||||
|
||||
parentCanvas = cc;
|
||||
}
|
||||
|
||||
@@ -146,25 +146,17 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
renderRootRt.offsetMin = Vector2.zero;
|
||||
renderRootRt.offsetMax = Vector2.zero;
|
||||
|
||||
// 3) 以父级 Canvas + 渲染根构建画布适配器
|
||||
Canvas = new ScreenSpaceBlueprintCanvas(parentCanvas, renderRootRt);
|
||||
// 3) 以父级 Canvas + 渲染根构建画布适配器;实际视口默认是宿主自身而非 RenderRoot。
|
||||
var hostRect = GetComponent<RectTransform>();
|
||||
Canvas = new ScreenSpaceBlueprintCanvas(parentCanvas, renderRootRt,
|
||||
_viewportRectTransform != null ? _viewportRectTransform : hostRect);
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateGraph()
|
||||
{
|
||||
var system = BlueprintGraphSystem.GlobalInstance;
|
||||
Graph = system.CreateGraph(Canvas);
|
||||
Graph.ConfigAssets = _toolConfigAssets;
|
||||
|
||||
if (!string.IsNullOrEmpty(_themeName))
|
||||
{
|
||||
Graph.ThemeName = _themeName;
|
||||
Graph.RefreshTheme();
|
||||
}
|
||||
|
||||
// 立即执行一次渲染,确保背景等基础设施在首帧 OnUpdate 前就绪
|
||||
Graph.LateUpdate();
|
||||
// System 是唯一帧驱动;Context 在 Initialize/工具构建之前绑定。
|
||||
Graph = BlueprintGraphSystem.GlobalInstance.CreateGraph(Canvas, _context);
|
||||
}
|
||||
|
||||
private void SetupInput()
|
||||
@@ -250,42 +242,14 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
GenerateTestNodes();
|
||||
}
|
||||
|
||||
// ===== 驱动:Update(运行时)=====
|
||||
|
||||
private void Update()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if (!Application.isPlaying) return;
|
||||
#endif
|
||||
|
||||
if (Graph == null) return;
|
||||
Graph.Update();
|
||||
Graph.LateUpdate();
|
||||
SyncDebugVisibility();
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// ===== 驱动:EditorTick(编辑器非运行态)=====
|
||||
|
||||
private void EditorTick()
|
||||
{
|
||||
if (this == null || Graph == null) return;
|
||||
|
||||
Graph.ConfigAssets = _toolConfigAssets;
|
||||
Graph.InvalidateToolConfigCache();
|
||||
|
||||
Graph.Update();
|
||||
Graph.LateUpdate();
|
||||
SyncDebugVisibility();
|
||||
}
|
||||
|
||||
// 组件仅承担 Inspector 配置与调试入口;图的 Update/LateUpdate 只由 BlueprintGraphSystem 驱动。
|
||||
private void OnValidate()
|
||||
{
|
||||
if (Graph == null || Application.isPlaying) return;
|
||||
Graph.ConfigAssets = _toolConfigAssets;
|
||||
Graph.InvalidateToolConfigCache();
|
||||
if (_context == null)
|
||||
_context = new BlueprintGraphContext();
|
||||
Graph?.RequestContextRefresh(_context);
|
||||
SyncDebugVisibility();
|
||||
}
|
||||
#endif
|
||||
|
||||
// ===== 测试节点生成 =====
|
||||
|
||||
@@ -334,10 +298,6 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
EditorApplication.update -= EditorTick;
|
||||
#endif
|
||||
|
||||
if (Graph != null)
|
||||
{
|
||||
var system = BlueprintGraphSystem.LazyInstance;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using XericLibrary.Runtime.Blueprint.Render;
|
||||
|
||||
namespace XericLibrary.Runtime.Blueprint
|
||||
{
|
||||
@@ -9,14 +8,13 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
/// 蓝图框选工具 —— 鼠标左键在空白区域拖拽,绘制矩形选框,
|
||||
/// 释放时计算框选范围内所有元素的交集并输出统计日志。
|
||||
/// <para>选框 UGUI Image 默认渲染在所有层之上(SetAsLastSibling)。</para>
|
||||
/// <para>框选结果保存在 <see cref="SelectedElements"/> 中供其他工具读取。</para>
|
||||
/// <para>框选结果通过 <see cref="BlueprintToolProvider.GetSelector"/>(Graph).<see cref="SelectorHandle.CurrentSelection"/> 读取。</para>
|
||||
/// <para>
|
||||
/// 选框仅在拖拽距离超过 <c>QuickGraphInputConfig.BoxSelectThreshold</c> 后才显示,
|
||||
/// 选框仅在拖拽距离超过 <see cref="BoxSelectThreshold"/> 后才显示,
|
||||
/// 避免误触显示闪烁。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[BlueprintTool(phase: ToolPhase.PreUpdate, order: 60)]
|
||||
[BlueprintTheme("QuickGraph")]
|
||||
public class BlueprintBoxSelectTool : BlueprintTool
|
||||
{
|
||||
// ---------- 状态 ----------
|
||||
@@ -34,30 +32,12 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
|
||||
// ---------- 配置 ----------
|
||||
|
||||
private QuickGraphInputConfig _boxConfig;
|
||||
|
||||
private QuickGraphInputConfig BoxConfig
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_boxConfig == null && Graph != null)
|
||||
{
|
||||
foreach (var c in Graph.ConfigAssets)
|
||||
{
|
||||
if (c is QuickGraphInputConfig qc) { _boxConfig = qc; break; }
|
||||
}
|
||||
if (_boxConfig == null)
|
||||
{
|
||||
_boxConfig = ScriptableObject.CreateInstance<QuickGraphInputConfig>();
|
||||
}
|
||||
}
|
||||
return _boxConfig;
|
||||
}
|
||||
}
|
||||
/// <summary>框选拖拽触发阈值(画布单位)。拖拽距离超过此值后才显示选框并开始框选。</summary>
|
||||
public float BoxSelectThreshold = 10f;
|
||||
|
||||
// ---------- 结果 ----------
|
||||
|
||||
/// <summary>结果:通过 Provider.CurrentSelectElements 读取框选结果。</summary>
|
||||
/// <summary>结果:通过 Provider.Selector.CurrentSelection 读取框选结果。</summary>
|
||||
|
||||
// ===== 鼠标按下 =====
|
||||
|
||||
@@ -85,7 +65,7 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
_dragEndCanvas = canvasPoint;
|
||||
|
||||
// 检查拖拽距离是否超过阈值
|
||||
float threshold = BoxConfig.BoxSelectThreshold;
|
||||
float threshold = BoxSelectThreshold;
|
||||
float dragDist = Vector2.Distance(_dragStartCanvas, _dragEndCanvas);
|
||||
|
||||
if (!_boxVisible && dragDist > threshold)
|
||||
@@ -183,7 +163,7 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
UpdateSelectionBox();
|
||||
|
||||
// 计算选框内的元素(画布坐标空间)
|
||||
BlueprintToolProvider.CurrentSelectElements.Clear();
|
||||
var selectList = new System.Collections.Generic.List<IBlueprintElement>();
|
||||
var min = new Vector2(
|
||||
Mathf.Min(_dragStartCanvas.x, _dragEndCanvas.x),
|
||||
Mathf.Min(_dragStartCanvas.y, _dragEndCanvas.y));
|
||||
@@ -192,14 +172,11 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
Mathf.Max(_dragStartCanvas.y, _dragEndCanvas.y));
|
||||
var selectRect = new Rect(min, max - min);
|
||||
|
||||
foreach (var element in Graph.Elements)
|
||||
{
|
||||
if (element == null) continue;
|
||||
if (element.BoundingBox.Overlaps(selectRect))
|
||||
{
|
||||
BlueprintToolProvider.CurrentSelectElements.Add(element);
|
||||
}
|
||||
}
|
||||
var selector = BlueprintToolProvider.GetSelector(Graph);
|
||||
selector.RectHitTest(Graph, selectRect, selectList);
|
||||
|
||||
// 通过当前 Graph 的 SelectorHandle 提交多选
|
||||
selector.CommitMultiSelection(selectList);
|
||||
|
||||
// 销毁选框
|
||||
DestroySelectionBox();
|
||||
@@ -207,12 +184,14 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
|
||||
private void SelectAtPoint(Vector2 canvasPoint)
|
||||
{
|
||||
BlueprintToolProvider.CurrentSelectElements.Clear();
|
||||
if (Graph == null) return;
|
||||
|
||||
var hit = Graph.HitTest(canvasPoint);
|
||||
var selector = BlueprintToolProvider.GetSelector(Graph);
|
||||
var hit = selector.HitTest(Graph, canvasPoint);
|
||||
if (hit != null)
|
||||
BlueprintToolProvider.CurrentSelectElements.Add(hit);
|
||||
selector.CommitSelection(hit);
|
||||
else
|
||||
selector.SwapToHistory();
|
||||
}
|
||||
|
||||
private void DestroySelectionBox()
|
||||
|
||||
@@ -10,12 +10,12 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
/// 通过 <c>KfsStyle</c> + <c>KeyFlippingStampSheet</c> 状态机驱动拖拽识别。
|
||||
/// </summary>
|
||||
[BlueprintTool(phase: ToolPhase.PreUpdate, order: 1)]
|
||||
[BlueprintTheme("QuickGraph")]
|
||||
public class BlueprintInputTool_InputManager : BlueprintInputTool_UGUI
|
||||
{
|
||||
// ===== 三键的 sheet 状态表 =====
|
||||
private readonly MacroKey.KeyFlippingStampSheet[] _sheets = new MacroKey.KeyFlippingStampSheet[3];
|
||||
private readonly Vector2[] _lastCanvasPos = new Vector2[3];
|
||||
private readonly Vector2[] _lastLocalPos = new Vector2[3];
|
||||
|
||||
// ===== 快捷键轮询常量 =====
|
||||
private static readonly KeyCode[] s_modifierKeys = new[]
|
||||
@@ -76,16 +76,16 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
}
|
||||
|
||||
// 键盘 Pan / Zoom
|
||||
GraphInputTool.LastPanDirection = Vector2.zero;
|
||||
PanDirection = Vector2.zero;
|
||||
float x = 0f, y = 0f;
|
||||
if (Input.GetKey(KeyCode.UpArrow) || Input.GetKey(KeyCode.W)) y += 1f;
|
||||
if (Input.GetKey(KeyCode.DownArrow) || Input.GetKey(KeyCode.S)) y -= 1f;
|
||||
if (Input.GetKey(KeyCode.LeftArrow) || Input.GetKey(KeyCode.A)) x -= 1f;
|
||||
if (Input.GetKey(KeyCode.RightArrow) || Input.GetKey(KeyCode.D)) x += 1f;
|
||||
GraphInputTool.LastPanDirection = new Vector2(x, y);
|
||||
GraphInputTool.LastZoomAxis = 0f;
|
||||
if (Input.GetKey(KeyCode.Equals) || Input.GetKey(KeyCode.KeypadPlus)) GraphInputTool.LastZoomAxis = 1f;
|
||||
if (Input.GetKey(KeyCode.Minus) || Input.GetKey(KeyCode.KeypadMinus)) GraphInputTool.LastZoomAxis = -1f;
|
||||
PanDirection = new Vector2(x, y);
|
||||
ZoomAxis = 0f;
|
||||
if (Input.GetKey(KeyCode.Equals) || Input.GetKey(KeyCode.KeypadPlus)) ZoomAxis = 1f;
|
||||
if (Input.GetKey(KeyCode.Minus) || Input.GetKey(KeyCode.KeypadMinus)) ZoomAxis = -1f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -104,6 +104,7 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
{
|
||||
var pt = canvas.ScreenToCanvas(Input.mousePosition);
|
||||
_lastCanvasPos[button] = pt;
|
||||
_lastLocalPos[button] = canvas.ScreenToLocal(Input.mousePosition);
|
||||
BlueprintToolProvider.DispatchPointerDown(Graph, this, pt, button);
|
||||
}
|
||||
|
||||
@@ -111,10 +112,13 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
if (sheet.IsDragging)
|
||||
{
|
||||
var current = canvas.ScreenToCanvas(Input.mousePosition);
|
||||
var currentLocal = canvas.ScreenToLocal(Input.mousePosition);
|
||||
var delta = current - _lastCanvasPos[button];
|
||||
PointerLocalDelta = currentLocal - _lastLocalPos[button];
|
||||
_lastCanvasPos[button] = current;
|
||||
_lastLocalPos[button] = currentLocal;
|
||||
|
||||
if (delta.sqrMagnitude > 0.0001f)
|
||||
if (delta.sqrMagnitude > 0.0001f || PointerLocalDelta.sqrMagnitude > 0.0001f)
|
||||
{
|
||||
BlueprintToolProvider.DispatchPointerDrag(Graph, this, current, delta, button);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
/// 不再依赖 <c>BlueprintInputManager</c> 管理器。
|
||||
/// </summary>
|
||||
[BlueprintTool(phase: ToolPhase.PreUpdate, order: 1)]
|
||||
[BlueprintTheme("QuickGraph")]
|
||||
public class BlueprintInputTool_InputSystem : BlueprintInputTool_UGUI
|
||||
{
|
||||
private InputActionAsset _inputActionAsset;
|
||||
@@ -47,10 +46,10 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
public override void OnInitialize()
|
||||
{
|
||||
// 创建回调委托(确保 Bind/Unbind 使用同一引用)
|
||||
_panHandler = ctx => GraphInputTool.LastPanDirection = ctx.ReadValue<Vector2>();
|
||||
_panCanceled = _ => GraphInputTool.LastPanDirection = Vector2.zero;
|
||||
_zoomHandler = ctx => GraphInputTool.LastZoomAxis = ctx.ReadValue<float>();
|
||||
_zoomCanceled = _ => GraphInputTool.LastZoomAxis = 0f;
|
||||
_panHandler = ctx => PanDirection = ctx.ReadValue<Vector2>();
|
||||
_panCanceled = _ => PanDirection = Vector2.zero;
|
||||
_zoomHandler = ctx => ZoomAxis = ctx.ReadValue<float>();
|
||||
_zoomCanceled = _ => ZoomAxis = 0f;
|
||||
_homeHandler = _ => DispatchShortcut(BlueprintInputConstants.FocusHome);
|
||||
_undoHandler = _ => DispatchShortcut(BlueprintInputConstants.Undo);
|
||||
_redoHandler = _ => DispatchShortcut(BlueprintInputConstants.Redo);
|
||||
|
||||
@@ -23,8 +23,11 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
|
||||
private const string BackgroundGoName = "__Bp_GridBackground";
|
||||
|
||||
// 上一帧的鼠标画布坐标(用于计算 PointerMove delta)
|
||||
// 上一帧坐标:普通交互使用 canvas delta,中键平移使用不受 Pan/Zoom 影响的 local delta。
|
||||
private Vector2 _lastCanvasPos;
|
||||
private Vector2 _lastLocalPos;
|
||||
private Vector2 _dragStartCanvasPos;
|
||||
private Vector2 _dragStartLocalPos;
|
||||
|
||||
/// <summary>
|
||||
/// 是否通过 EventTrigger 处理鼠标按键/拖拽/滚轮事件。
|
||||
@@ -37,6 +40,7 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
|
||||
public override void OnInitialize()
|
||||
{
|
||||
GraphInputTool.RegisterAdapter(Graph, this);
|
||||
TryBindEventTrigger();
|
||||
}
|
||||
|
||||
@@ -62,18 +66,17 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
return;
|
||||
}
|
||||
|
||||
var canvasPt = Graph.Canvas != null
|
||||
? Graph.Canvas.ScreenToCanvas(BlueprintUGUIInputManager.MousePosition)
|
||||
: Vector2.zero;
|
||||
var canvasPt = Graph.Canvas.ScreenToCanvas(BlueprintUGUIInputManager.MousePosition);
|
||||
var localPt = Graph.Canvas.ScreenToLocal(BlueprintUGUIInputManager.MousePosition);
|
||||
PointerLocalDelta = localPt - _lastLocalPos;
|
||||
UpdateCursorContext(canvasPt);
|
||||
|
||||
// ── 每帧分发 PointerMove ──
|
||||
var delta = canvasPt - _lastCanvasPos;
|
||||
if (delta.sqrMagnitude > 0.0001f || _lastCanvasPos == Vector2.zero)
|
||||
{
|
||||
BlueprintToolProvider.DispatchPointerMove(Graph, this, canvasPt, delta);
|
||||
}
|
||||
_lastCanvasPos = canvasPt;
|
||||
_lastLocalPos = localPt;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -97,6 +100,7 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
|
||||
public override void OnDestroy()
|
||||
{
|
||||
GraphInputTool.UnregisterAdapter(Graph, this);
|
||||
UnbindEventTrigger();
|
||||
}
|
||||
|
||||
@@ -237,13 +241,13 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2 _dragStartPos;
|
||||
private bool _hadDragThisClick;
|
||||
|
||||
private void OnBeginDragEvent(BaseEventData data)
|
||||
{
|
||||
if (!(data is PointerEventData pData)) return;
|
||||
_dragStartPos = PointerCanvasPos(pData);
|
||||
_dragStartCanvasPos = PointerCanvasPos(pData);
|
||||
_dragStartLocalPos = Graph.Canvas.ScreenToLocal(pData.position);
|
||||
_hadDragThisClick = true;
|
||||
}
|
||||
|
||||
@@ -251,10 +255,13 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
{
|
||||
if (!(data is PointerEventData pData)) return;
|
||||
var current = PointerCanvasPos(pData);
|
||||
var delta = current - _dragStartPos;
|
||||
_dragStartPos = current;
|
||||
var currentLocal = Graph.Canvas.ScreenToLocal(pData.position);
|
||||
var canvasDelta = current - _dragStartCanvasPos;
|
||||
PointerLocalDelta = currentLocal - _dragStartLocalPos;
|
||||
_dragStartCanvasPos = current;
|
||||
_dragStartLocalPos = currentLocal;
|
||||
UpdateCursorContext(current);
|
||||
BlueprintToolProvider.DispatchPointerDrag(Graph, this, current, delta, (int)pData.button);
|
||||
BlueprintToolProvider.DispatchPointerDrag(Graph, this, current, canvasDelta, (int)pData.button);
|
||||
}
|
||||
|
||||
private void OnEndDragEvent(BaseEventData data)
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace XericLibrary.Runtime.Blueprint
|
||||
{
|
||||
/// <summary>
|
||||
/// 蓝图视口控制器 —— 管理视图状态(Pan/Zoom/范围),提供外部 API 供工具和外部代码读写。
|
||||
/// <para>
|
||||
/// 在 <c>ToolPhase.PreUpdate, order: 40</c> 执行,每帧直接将目标值写入 <see cref="IBlueprintCanvas"/>,
|
||||
/// 不做插值平滑。输入工具通过写入 <see cref="TargetPanOffset"/> / <see cref="TargetZoomLevel"/>
|
||||
/// 来驱动视图,外部代码可通过 <c>BlueprintToolProvider.GetTool<BlueprintViewportController>(graph)</c>
|
||||
/// 获取实例直接控制视图。
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// 此工具为通用基础工具(无主题限定),对所有蓝图主题可见。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[BlueprintTool(phase: ToolPhase.PreUpdate, order: 40)]
|
||||
public class BlueprintViewportController : BlueprintTool
|
||||
{
|
||||
// ── 目标值(由输入工具或外部代码写入) ──
|
||||
|
||||
/// <summary>目标平移偏移量。外部代码直接设置此值即可控制视图平移。</summary>
|
||||
public Vector2 TargetPanOffset = Vector2.zero;
|
||||
|
||||
/// <summary>目标缩放级别。外部代码直接设置此值即可控制视图缩放。</summary>
|
||||
public float TargetZoomLevel = 1f;
|
||||
|
||||
/// <summary>最小缩放限制。</summary>
|
||||
public float MinZoom = 0.1f;
|
||||
|
||||
/// <summary>最大缩放限制。</summary>
|
||||
public float MaxZoom = 3f;
|
||||
|
||||
// ── 帧间脏检测 ──
|
||||
|
||||
private Vector2 _prevPanOffset;
|
||||
private float _prevZoomLevel;
|
||||
private bool _initialized;
|
||||
|
||||
public override void OnUpdate(float deltaTime)
|
||||
{
|
||||
if (Graph?.Canvas == null) return;
|
||||
|
||||
var canvas = Graph.Canvas;
|
||||
|
||||
// 首次同步
|
||||
if (!_initialized)
|
||||
{
|
||||
TargetPanOffset = canvas.PanOffset;
|
||||
TargetZoomLevel = canvas.ZoomLevel;
|
||||
_prevPanOffset = TargetPanOffset;
|
||||
_prevZoomLevel = TargetZoomLevel;
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
// 同步范围到 Canvas
|
||||
canvas.MinZoom = MinZoom;
|
||||
canvas.MaxZoom = MaxZoom;
|
||||
|
||||
// 钳制目标值
|
||||
TargetZoomLevel = Mathf.Clamp(TargetZoomLevel, MinZoom, MaxZoom);
|
||||
|
||||
// 直接写入 Canvas(无插值)
|
||||
canvas.PanOffset = TargetPanOffset;
|
||||
canvas.ZoomLevel = TargetZoomLevel;
|
||||
|
||||
// 标记脏
|
||||
if (TargetPanOffset != _prevPanOffset || !Mathf.Approximately(TargetZoomLevel, _prevZoomLevel))
|
||||
{
|
||||
Graph.MarkDirty();
|
||||
_prevPanOffset = TargetPanOffset;
|
||||
_prevZoomLevel = TargetZoomLevel;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 公共 API
|
||||
// ====================================================================
|
||||
|
||||
/// <summary>重置视图到原点,缩放归 1。</summary>
|
||||
public void ResetView()
|
||||
{
|
||||
TargetPanOffset = Vector2.zero;
|
||||
TargetZoomLevel = 1f;
|
||||
Graph?.MarkDirty();
|
||||
}
|
||||
|
||||
/// <summary>聚焦到某个画布坐标点。</summary>
|
||||
public void FocusOn(Vector2 canvasCenter, float zoom = 1f)
|
||||
{
|
||||
TargetZoomLevel = Mathf.Clamp(zoom, MinZoom, MaxZoom);
|
||||
if (Graph?.Canvas != null)
|
||||
{
|
||||
// local = canvas * zoom + pan;令目标画布点落在实际 viewport 的 RenderRoot local 中心。
|
||||
TargetPanOffset = Graph.Canvas.GetViewportLocalRect().center - canvasCenter * TargetZoomLevel;
|
||||
}
|
||||
else TargetPanOffset = -canvasCenter * TargetZoomLevel;
|
||||
Graph?.MarkDirty();
|
||||
}
|
||||
|
||||
/// <summary>聚焦到一组元素的包围盒区域。</summary>
|
||||
public void FocusOnElements(System.Collections.Generic.IReadOnlyList<IBlueprintElement> elements, float padding = 100f)
|
||||
{
|
||||
if (elements == null || elements.Count == 0) return;
|
||||
if (Graph?.Canvas == null) return;
|
||||
|
||||
// 计算所有元素的包围盒
|
||||
Rect bounds = elements[0].BoundingBox;
|
||||
for (int i = 1; i < elements.Count; i++)
|
||||
{
|
||||
var b = elements[i].BoundingBox;
|
||||
bounds.xMin = Mathf.Min(bounds.xMin, b.xMin);
|
||||
bounds.yMin = Mathf.Min(bounds.yMin, b.yMin);
|
||||
bounds.xMax = Mathf.Max(bounds.xMax, b.xMax);
|
||||
bounds.yMax = Mathf.Max(bounds.yMax, b.yMax);
|
||||
}
|
||||
|
||||
bounds.xMin -= padding;
|
||||
bounds.yMin -= padding;
|
||||
bounds.xMax += padding;
|
||||
bounds.yMax += padding;
|
||||
|
||||
// 以实际 viewport 的 RenderRoot local 尺寸适配;再复用 FocusOn 的统一公式。
|
||||
var viewportLocal = Graph.Canvas.GetViewportLocalRect();
|
||||
float zoomX = bounds.width > 0.0001f ? viewportLocal.width / bounds.width : MaxZoom;
|
||||
float zoomY = bounds.height > 0.0001f ? viewportLocal.height / bounds.height : MaxZoom;
|
||||
float zoom = Mathf.Clamp(Mathf.Min(zoomX, zoomY), MinZoom, MaxZoom);
|
||||
FocusOn(bounds.center, zoom);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f4bb7dd4bbd0e6e4fac82161405eca13
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,221 +0,0 @@
|
||||
using UnityEngine;
|
||||
using XericLibrary.Runtime.Blueprint.Render;
|
||||
|
||||
namespace XericLibrary.Runtime.Blueprint
|
||||
{
|
||||
/// <summary>
|
||||
/// QuickGraph 输入交互工具 —— 处理画布平移、缩放和节点选择。
|
||||
/// <para>中键拖拽 = 平移;左键点击 = 选择;滚轮 = 缩放。</para>
|
||||
/// <para>快捷键(新输入系统):方向键 = 平移,Ctrl+± = 缩放,Ctrl+H = 归位。</para>
|
||||
/// </summary>
|
||||
[BlueprintTool(phase: ToolPhase.PreUpdate, order: 50)]
|
||||
[BlueprintTheme("QuickGraph")]
|
||||
public class QuickGraphInputTool : BlueprintTool
|
||||
{
|
||||
// ---------- 配置(从 ToolConfigAssets 读取) ----------
|
||||
private QuickGraphInputConfig Config
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ToolConfig is QuickGraphInputConfig external)
|
||||
return external;
|
||||
if (_defaultConfig == null)
|
||||
_defaultConfig = ScriptableObject.CreateInstance<QuickGraphInputConfig>();
|
||||
return _defaultConfig;
|
||||
}
|
||||
}
|
||||
private static QuickGraphInputConfig _defaultConfig;
|
||||
|
||||
// ---------- 视图状态 ----------
|
||||
private Vector2 _targetPanOffset;
|
||||
private float _targetZoomLevel = 1f;
|
||||
private bool _viewInitialized;
|
||||
|
||||
// 用于检测帧间变化
|
||||
private Vector2 _prevPanOffset;
|
||||
private float _prevZoomLevel;
|
||||
|
||||
// ---------- 拖拽状态 ----------
|
||||
private bool _isDragging;
|
||||
private int _activeDragButton = -1;
|
||||
private Vector2 _dragStartPan;
|
||||
|
||||
// ---------- 选择状态 ----------
|
||||
private BlueprintElementBase _hoveredElement;
|
||||
private BlueprintElementBase _selectedElement;
|
||||
|
||||
// ===== 更新(平滑插值 + 键盘轮询) =====
|
||||
|
||||
public override void OnUpdate(float deltaTime)
|
||||
{
|
||||
if (Graph?.Canvas == null) return;
|
||||
|
||||
var cfg = Config;
|
||||
var canvas = Graph.Canvas;
|
||||
|
||||
// ── 同步缩放范围 ──
|
||||
canvas.MinZoom = cfg.MinZoom;
|
||||
canvas.MaxZoom = cfg.MaxZoom;
|
||||
|
||||
if (!_viewInitialized)
|
||||
{
|
||||
_targetPanOffset = canvas.PanOffset;
|
||||
_targetZoomLevel = canvas.ZoomLevel;
|
||||
_prevPanOffset = _targetPanOffset;
|
||||
_prevZoomLevel = _targetZoomLevel;
|
||||
_viewInitialized = true;
|
||||
}
|
||||
|
||||
// ── 键盘 Pan ──
|
||||
_targetPanOffset += GraphInputTool.LastPanDirection
|
||||
* cfg.KeyboardPanSpeed * deltaTime;
|
||||
|
||||
// ── 键盘 / 滚轮 Zoom ──
|
||||
float zoomInput = GraphInputTool.LastZoomAxis;
|
||||
if (!Mathf.Approximately(zoomInput, 0f))
|
||||
{
|
||||
float oldZoom = _targetZoomLevel;
|
||||
float zoomDelta = zoomInput / cfg.ZoomDivisor;
|
||||
_targetZoomLevel = Mathf.Clamp(
|
||||
_targetZoomLevel + zoomDelta,
|
||||
canvas.MinZoom, canvas.MaxZoom);
|
||||
ApplyZoomFocus(ref _targetPanOffset, _targetZoomLevel, oldZoom);
|
||||
GraphInputTool.LastZoomAxis = 0f;
|
||||
}
|
||||
|
||||
// ── 平滑插值到目标值 ──
|
||||
canvas.PanOffset = Vector2.Lerp(
|
||||
canvas.PanOffset, _targetPanOffset, cfg.PanLerpSpeed * deltaTime);
|
||||
canvas.ZoomLevel = Mathf.Lerp(
|
||||
canvas.ZoomLevel, _targetZoomLevel, cfg.ZoomLerpSpeed * deltaTime);
|
||||
|
||||
// ── 标记脏 ──
|
||||
if (canvas.PanOffset != _prevPanOffset || !Mathf.Approximately(canvas.ZoomLevel, _prevZoomLevel))
|
||||
{
|
||||
Graph.MarkDirty();
|
||||
_prevPanOffset = canvas.PanOffset;
|
||||
_prevZoomLevel = canvas.ZoomLevel;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 鼠标移动 =====
|
||||
|
||||
public override void OnPointerMove(GraphInputTool sender, Vector2 canvasPoint, Vector2 delta)
|
||||
{
|
||||
if (Graph == null || _isDragging) return;
|
||||
|
||||
var hit = Graph.HitTest(canvasPoint);
|
||||
_hoveredElement = hit as BlueprintElementBase;
|
||||
}
|
||||
|
||||
// ===== 鼠标按下 =====
|
||||
|
||||
public override void OnPointerDown(GraphInputTool sender, Vector2 canvasPoint, int mouseButton)
|
||||
{
|
||||
if (Graph == null) return;
|
||||
|
||||
if (mouseButton == 2)
|
||||
{
|
||||
_isDragging = true;
|
||||
_activeDragButton = 2;
|
||||
_dragStartPan = _targetPanOffset;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 鼠标释放 =====
|
||||
|
||||
public override void OnPointerUp(GraphInputTool sender, Vector2 canvasPoint, int mouseButton)
|
||||
{
|
||||
if (_isDragging && _activeDragButton == mouseButton)
|
||||
{
|
||||
_isDragging = false;
|
||||
_activeDragButton = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 拖拽 =====
|
||||
|
||||
public override void OnPointerDrag(GraphInputTool sender, Vector2 canvasPoint, Vector2 delta, int mouseButton)
|
||||
{
|
||||
if (Graph?.Canvas == null) return;
|
||||
|
||||
if (mouseButton == 2)
|
||||
{
|
||||
_targetPanOffset += delta * Config.DragPanSpeed;
|
||||
Graph.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 滚轮 =====
|
||||
|
||||
public override void OnScroll(GraphInputTool sender, Vector2 canvasPoint, Vector2 scrollDelta)
|
||||
{
|
||||
if (Graph == null) return;
|
||||
|
||||
float oldZoom = _targetZoomLevel;
|
||||
|
||||
_targetZoomLevel = Mathf.Clamp(
|
||||
_targetZoomLevel + scrollDelta.y / Config.ZoomDivisor,
|
||||
Graph.Canvas.MinZoom, Graph.Canvas.MaxZoom);
|
||||
|
||||
if (!Mathf.Approximately(oldZoom, _targetZoomLevel))
|
||||
{
|
||||
_targetPanOffset += canvasPoint * (oldZoom - _targetZoomLevel);
|
||||
Graph.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 快捷键 =====
|
||||
|
||||
public override void OnAction(GraphInputTool sender, string actionName)
|
||||
{
|
||||
if (Graph?.Canvas == null) return;
|
||||
|
||||
switch (actionName)
|
||||
{
|
||||
case BlueprintInputConstants.FocusHome:
|
||||
ResetView();
|
||||
break;
|
||||
case BlueprintInputConstants.Undo:
|
||||
break;
|
||||
case BlueprintInputConstants.Redo:
|
||||
break;
|
||||
case BlueprintInputConstants.NavigateParent:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 辅助 =====
|
||||
|
||||
public void ResetView()
|
||||
{
|
||||
_targetPanOffset = Vector2.zero;
|
||||
_targetZoomLevel = 1f;
|
||||
Graph?.MarkDirty();
|
||||
}
|
||||
|
||||
private void SelectElement(BlueprintElementBase element)
|
||||
{
|
||||
_selectedElement = element;
|
||||
}
|
||||
|
||||
/// <summary>当前悬停的元素(可能为 null)</summary>
|
||||
public BlueprintElementBase HoveredElement => _hoveredElement;
|
||||
|
||||
/// <summary>当前选中的元素(可能为 null)</summary>
|
||||
public BlueprintElementBase SelectedElement => _selectedElement;
|
||||
|
||||
// ===== 以鼠标焦点为中心缩放(键盘调用 — 自行读取鼠标位置) =====
|
||||
|
||||
private void ApplyZoomFocus(ref Vector2 panOffset, float newZoom, float oldZoom)
|
||||
{
|
||||
if (Graph?.Canvas == null) return;
|
||||
|
||||
// 通过 BlueprintUGUIInputManager 获取统一鼠标位置(已被 BlueprintInputReceiver 每帧刷新)
|
||||
Vector2 canvasCursor = Graph.Canvas.ScreenToCanvas(BlueprintUGUIInputManager.MousePosition);
|
||||
panOffset += canvasCursor * (oldZoom - newZoom);
|
||||
}
|
||||
|
||||
// =====(旧 ZoomFocusAdjust 已删除 — 替代为 ApplyZoomFocus,OnScroll 直接内联 canvasPoint 计算)=====
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f64c16d8bb22cf547992fd67ff9866a6
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 47199cf2fee4eec499be81af4eacd640
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,198 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace XericLibrary.Runtime.Blueprint.QuickGraph
|
||||
{
|
||||
/// <summary>
|
||||
/// QuickGraph 输入交互工具 —— 处理画布平移、缩放。
|
||||
/// <para>中键拖拽 = 平移;滚轮 = 缩放。</para>
|
||||
/// <para>视图状态读写委托给 <see cref="BlueprintViewportController"/>,
|
||||
/// 输入参数从 <see cref="QuickGraphInputConfig"/> 读取。</para>
|
||||
/// </summary>
|
||||
[BlueprintTool(phase: ToolPhase.PreUpdate, order: 50)]
|
||||
[BlueprintTheme("QuickGraph")]
|
||||
public class QuickGraphInputTool : BlueprintTool
|
||||
{
|
||||
// ---------- 配置(从 ToolConfigAssets 读取) ----------
|
||||
private QuickGraphInputConfig Config
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ToolConfig is QuickGraphInputConfig external)
|
||||
return external;
|
||||
if (_defaultConfig == null)
|
||||
_defaultConfig = ScriptableObject.CreateInstance<QuickGraphInputConfig>();
|
||||
return _defaultConfig;
|
||||
}
|
||||
}
|
||||
private static QuickGraphInputConfig _defaultConfig;
|
||||
|
||||
// ---------- 缓存的 ViewportController 引用 ----------
|
||||
private BlueprintViewportController _viewport;
|
||||
private BlueprintViewportController Viewport
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_viewport == null && Graph != null)
|
||||
_viewport = BlueprintToolProvider.GetToolByType<BlueprintViewportController>(Graph);
|
||||
return _viewport;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 拖拽状态 ----------
|
||||
private bool _isDragging;
|
||||
private int _activeDragButton = -1;
|
||||
|
||||
private Vector2 senderPanDirection() => GraphInputTool.GetAdapter(Graph)?.PanDirection ?? Vector2.zero;
|
||||
private float senderZoomAxis() => GraphInputTool.GetAdapter(Graph)?.ZoomAxis ?? 0f;
|
||||
|
||||
// ---------- 选择状态 ----------
|
||||
private BlueprintElementBase _hoveredElement;
|
||||
|
||||
public override void OnPointerMove(GraphInputTool sender, Vector2 canvasPoint, Vector2 delta)
|
||||
{
|
||||
if (Graph == null || _isDragging) return;
|
||||
|
||||
var hit = Graph.HitTest(canvasPoint);
|
||||
_hoveredElement = hit as BlueprintElementBase;
|
||||
}
|
||||
|
||||
/// <summary>当前悬停的元素(可能为 null)</summary>
|
||||
public BlueprintElementBase HoveredElement => _hoveredElement;
|
||||
|
||||
/// <summary>当前选中的元素(委托给 SelectorHandle)。</summary>
|
||||
public BlueprintElementBase SelectedElement =>
|
||||
BlueprintToolProvider.GetSelector(Graph).PrimarySelection as BlueprintElementBase;
|
||||
|
||||
public override void OnConfigChanged()
|
||||
{
|
||||
var viewport = Viewport;
|
||||
if (viewport == null) return;
|
||||
|
||||
var cfg = Config;
|
||||
viewport.MinZoom = cfg.MinZoom;
|
||||
viewport.MaxZoom = cfg.MaxZoom;
|
||||
viewport.TargetZoomLevel = Mathf.Clamp(viewport.TargetZoomLevel, cfg.MinZoom, cfg.MaxZoom);
|
||||
|
||||
// ViewportController 是 Canvas Pan/Zoom 的唯一提交者;这里只更新 controller 的范围与目标。
|
||||
Graph?.MarkDirty();
|
||||
}
|
||||
|
||||
public override void OnUpdate(float deltaTime)
|
||||
{
|
||||
if (Graph?.Canvas == null) return;
|
||||
var viewport = Viewport;
|
||||
if (viewport == null) return;
|
||||
|
||||
var cfg = Config;
|
||||
|
||||
// ── 同步缩放范围 ──
|
||||
viewport.MinZoom = cfg.MinZoom;
|
||||
viewport.MaxZoom = cfg.MaxZoom;
|
||||
|
||||
// ── 键盘 Pan ──
|
||||
viewport.TargetPanOffset += senderPanDirection() * cfg.KeyboardPanSpeed * deltaTime;
|
||||
|
||||
// ── 键盘 Zoom ──
|
||||
float zoomInput = senderZoomAxis();
|
||||
if (!Mathf.Approximately(zoomInput, 0f))
|
||||
{
|
||||
float oldZoom = viewport.TargetZoomLevel;
|
||||
float zoomDelta = zoomInput / cfg.ZoomDivisor;
|
||||
viewport.TargetZoomLevel = Mathf.Clamp(
|
||||
viewport.TargetZoomLevel + zoomDelta,
|
||||
viewport.MinZoom, viewport.MaxZoom);
|
||||
ApplyZoomFocus(oldZoom);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 鼠标按下 =====
|
||||
|
||||
public override void OnPointerDown(GraphInputTool sender, Vector2 canvasPoint, int mouseButton)
|
||||
{
|
||||
if (Graph == null) return;
|
||||
|
||||
if (mouseButton == 2)
|
||||
{
|
||||
_isDragging = true;
|
||||
_activeDragButton = 2;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 鼠标释放 =====
|
||||
|
||||
public override void OnPointerUp(GraphInputTool sender, Vector2 canvasPoint, int mouseButton)
|
||||
{
|
||||
if (_isDragging && _activeDragButton == mouseButton)
|
||||
{
|
||||
_isDragging = false;
|
||||
_activeDragButton = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 拖拽 =====
|
||||
|
||||
public override void OnPointerDrag(GraphInputTool sender, Vector2 canvasPoint, Vector2 delta, int mouseButton)
|
||||
{
|
||||
var viewport = Viewport;
|
||||
if (viewport == null) return;
|
||||
|
||||
if (mouseButton == 2)
|
||||
{
|
||||
// canvas delta 在本帧 Pan 提交后会反向跳变;仅使用 RenderRoot local 平面 delta。
|
||||
viewport.TargetPanOffset += (sender?.PointerLocalDelta ?? Vector2.zero) * Config.DragPanSpeed;
|
||||
Graph.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 滚轮 =====
|
||||
|
||||
public override void OnScroll(GraphInputTool sender, Vector2 canvasPoint, Vector2 scrollDelta)
|
||||
{
|
||||
var viewport = Viewport;
|
||||
if (viewport == null) return;
|
||||
|
||||
float oldZoom = viewport.TargetZoomLevel;
|
||||
|
||||
viewport.TargetZoomLevel = Mathf.Clamp(
|
||||
viewport.TargetZoomLevel + scrollDelta.y / Config.ZoomDivisor,
|
||||
viewport.MinZoom, viewport.MaxZoom);
|
||||
|
||||
if (!Mathf.Approximately(oldZoom, viewport.TargetZoomLevel))
|
||||
{
|
||||
viewport.TargetPanOffset += canvasPoint * (oldZoom - viewport.TargetZoomLevel);
|
||||
Graph.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 快捷键 =====
|
||||
|
||||
public override void OnAction(GraphInputTool sender, string actionName)
|
||||
{
|
||||
if (Graph?.Canvas == null) return;
|
||||
|
||||
switch (actionName)
|
||||
{
|
||||
case BlueprintInputConstants.FocusHome:
|
||||
Viewport?.ResetView();
|
||||
break;
|
||||
case BlueprintInputConstants.Undo:
|
||||
break;
|
||||
case BlueprintInputConstants.Redo:
|
||||
break;
|
||||
case BlueprintInputConstants.NavigateParent:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 以鼠标焦点为中心缩放(键盘调用 — 自行读取鼠标位置) =====
|
||||
|
||||
private void ApplyZoomFocus(float oldZoom)
|
||||
{
|
||||
var viewport = Viewport;
|
||||
if (viewport == null || Graph?.Canvas == null) return;
|
||||
|
||||
Vector2 canvasCursor = Graph.Canvas.ScreenToCanvas(BlueprintUGUIInputManager.MousePosition);
|
||||
viewport.TargetPanOffset += canvasCursor * (oldZoom - viewport.TargetZoomLevel);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1fe1086d7e9750541a9a5de047e05ce6
|
||||
guid: 8b96bea36422c0146b9f3365107b8f7b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6d2d1e11f02798943a73c491948cd025
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c7cfe95b8a3355c4e8e6585d2266fb5b
|
||||
guid: 8aa6308c2cd1dd845a8b4810310dce87
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
+1
-13
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace XericLibrary.Runtime.Blueprint.Render
|
||||
namespace XericLibrary.Runtime.Blueprint.QuickGraph
|
||||
{
|
||||
/// <summary>
|
||||
/// QuickGraph 网格背景配置表 —— 控制 Shader 全部可调参数。
|
||||
@@ -99,28 +99,16 @@ namespace XericLibrary.Runtime.Blueprint.Render
|
||||
{
|
||||
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);
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 32753aff001677a4b81e10dfae39b4cb
|
||||
guid: 38115e5721db6514bb8b8be6be10f26a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
+2
-16
@@ -1,10 +1,10 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace XericLibrary.Runtime.Blueprint.Render
|
||||
namespace XericLibrary.Runtime.Blueprint.QuickGraph
|
||||
{
|
||||
/// <summary>
|
||||
/// QuickGraph 输入工具配置表 —— 控制平移/缩放插值速率、灵敏度等可调参数。
|
||||
/// QuickGraph 输入工具配置表 —— 控制平移/缩放灵敏度等可调参数。
|
||||
/// 右键 Project 窗口 → Create/Blueprint/QuickGraph/Input Config 创建资产。
|
||||
/// 拖入蓝图组件的 ToolConfigAssets 列表即可覆盖默认值。
|
||||
/// </summary>
|
||||
@@ -21,16 +21,7 @@ namespace XericLibrary.Runtime.Blueprint.Render
|
||||
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;
|
||||
@@ -56,10 +47,5 @@ namespace XericLibrary.Runtime.Blueprint.Render
|
||||
[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
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cfce52570fac4f5488becd85ce4f023b
|
||||
guid: 4735b93342f4fde4a8ed46dd28b46d4c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
+6
-8
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace XericLibrary.Runtime.Blueprint.Render
|
||||
namespace XericLibrary.Runtime.Blueprint.QuickGraph
|
||||
{
|
||||
/// <summary>
|
||||
/// QuickGraph 节点渲染配置表 —— 控制节点矩形、边框、圆角等视觉参数。
|
||||
@@ -50,12 +50,10 @@ namespace XericLibrary.Runtime.Blueprint.Render
|
||||
[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;
|
||||
[Header("已弃用的 LOD 兼容字段")]
|
||||
[Obsolete("节点 LOD 已迁移至 BlueprintRenderLodConfig(绝对 Zoom + 滞回),此字段不再参与渲染。")]
|
||||
[HideInInspector] public float Lod0Threshold = 0.3f;
|
||||
[Obsolete("节点 LOD 已迁移至 BlueprintRenderLodConfig(绝对 Zoom + 滞回),此字段不再参与渲染。")]
|
||||
[HideInInspector] public float Lod1Threshold = 0.7f;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8ab3d3829c73815468ed8c5ae306a6ee
|
||||
guid: 8038bf45323375b46a3a2820ddfe212e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
+5
-1
@@ -2,7 +2,7 @@ using System;
|
||||
using UnityEngine;
|
||||
using XericLibrary.Runtime.UIGraph;
|
||||
|
||||
namespace XericLibrary.Runtime.Blueprint.Render
|
||||
namespace XericLibrary.Runtime.Blueprint.QuickGraph
|
||||
{
|
||||
/// <summary>
|
||||
/// QuickGraph 连线渲染配置表 —— 控制贝塞尔曲线宽度、箭头形状等全部视觉参数。
|
||||
@@ -24,6 +24,10 @@ namespace XericLibrary.Runtime.Blueprint.Render
|
||||
[Range(1f, 20f)]
|
||||
public float WireWidth = 5f;
|
||||
|
||||
[Tooltip("线条额外点击命中半径(画布单位);实际半径至少为线宽的一半)。")]
|
||||
[Range(0f, 40f)]
|
||||
public float HitTestRadius = 8f;
|
||||
|
||||
[Header("贝塞尔手柄")]
|
||||
[Tooltip("源端手柄从端口伸出的距离(画布单位)。数值越大曲线越平缓。")]
|
||||
[Range(10f, 500f)]
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 21208cffd6108e04d815bb385f0af3fc
|
||||
guid: c95f6e1f1ece40444839f0a29ac371e5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
@@ -0,0 +1,118 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
using XericLibrary.Runtime.Blueprint.Render;
|
||||
using XericLibrary.Runtime.UIGraph;
|
||||
using PrimitiveType = XericLibrary.Runtime.UIGraph.PrimitiveType;
|
||||
|
||||
namespace XericLibrary.Runtime.Blueprint.QuickGraph
|
||||
{
|
||||
[BlueprintTool(phase: ToolPhase.Render, order: 100)]
|
||||
[BlueprintTheme("QuickGraph")]
|
||||
public class NodeCanvasRenderTool : BlueprintCanvasRenderTool<BlueprintPrimitiveRenderer>
|
||||
{
|
||||
private QuickGraphNodeRenderConfig _defaultConfig;
|
||||
private QuickGraphRenderStyleResolver _styleResolver;
|
||||
private readonly Dictionary<string, (ulong chunkID, TMP_Text text)> _nodeTexts = new Dictionary<string, (ulong, TMP_Text)>();
|
||||
private readonly HashSet<string> _staleTextCandidates = new HashSet<string>();
|
||||
private QuickGraphNodeRenderConfig Config
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ToolConfig is QuickGraphNodeRenderConfig external) return external;
|
||||
if (_defaultConfig == null) _defaultConfig = ScriptableObject.CreateInstance<QuickGraphNodeRenderConfig>();
|
||||
return _defaultConfig;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnInitialize() { base.OnInitialize(); _styleResolver = new QuickGraphRenderStyleResolver(Graph, nodeFallback: ConfigStyle); SyncAllNodeGeometry(); }
|
||||
public override void OnConfigChanged()
|
||||
{
|
||||
_styleResolver?.Refresh(); SyncAllNodeGeometry();
|
||||
base.OnConfigChanged();
|
||||
}
|
||||
private QuickGraphNodeRenderStyle ConfigStyle()
|
||||
{
|
||||
var cfg = Config;
|
||||
return new QuickGraphNodeRenderStyle { Width = cfg.NodeWidth, Height = cfg.NodeHeight, Border = cfg.BorderThickness, Chamfer = cfg.ChamferSize, ChamferSegments = cfg.ChamferSegments, TitleFontSize = cfg.TitleFontSize };
|
||||
}
|
||||
protected override BlueprintPrimitiveRenderer BuildChunkRenderer(GameObject go) => go.AddComponent<BlueprintPrimitiveRenderer>();
|
||||
protected override void ClearChunkData(BlueprintPrimitiveRenderer renderer) => renderer.ClearAll();
|
||||
protected override void SetChunkDirty(BlueprintPrimitiveRenderer renderer) => renderer.SetPrimitiveDirty(0);
|
||||
protected override bool AcceptsElement(IBlueprintElement element) => element is BlueprintNode;
|
||||
|
||||
protected override void AppendElementData(BlueprintPrimitiveRenderer renderer, IBlueprintElement element, ulong chunkID)
|
||||
{
|
||||
if (!(element is BlueprintNode node)) return;
|
||||
var cfg = Config; var style = _styleResolver != null ? _styleResolver.Snapshot.Node : new QuickGraphNodeRenderStyle { Width=cfg.NodeWidth, Height=cfg.NodeHeight, Border=cfg.BorderThickness, Chamfer=cfg.ChamferSize, ChamferSegments=cfg.ChamferSegments, TitleFontSize=cfg.TitleFontSize }; var lod = CurrentElementSubmission.LOD;
|
||||
CalculatePortPositions(node, style.Width, style.Height);
|
||||
var local = CurrentElementSubmission.ChunkLocalPosition;
|
||||
float border = lod == BlueprintLodLevel.Minimal ? 0f : style.Border;
|
||||
Color bg = lod == BlueprintLodLevel.Minimal ? node.NodeColor : node.NodeBGColor;
|
||||
Color edge = node.NodeColor;
|
||||
renderer.AddPrimitive(new PrimitiveCacheEntry { type = PrimitiveType.Rectangle, sizeMode = SizeMode.InscribedEllipse, center = local, size = new Vector2(style.Width, style.Height), @params = new PrimitiveParams { axisScaleX = 1f, axisScaleY = 1f, sideCount = 4, chamferSize = style.Chamfer, chamferSegments = style.ChamferSegments, bgColor = new Color32(0,0,0,0), centerColor = bg, borderColor = edge, borderThickness = border } });
|
||||
if (lod == BlueprintLodLevel.Full) UpdateNodeText(node, chunkID, local, style.Width, style.Height, border, style.TitleFontSize, cfg); else HideNodeText(node.ElementId);
|
||||
}
|
||||
private void SyncAllNodeGeometry()
|
||||
{
|
||||
if (Graph == null) return;
|
||||
var cfg = Config; var style = _styleResolver != null ? _styleResolver.Snapshot.Node : new QuickGraphNodeRenderStyle { Width=cfg.NodeWidth, Height=cfg.NodeHeight };
|
||||
foreach (var node in Graph.Nodes)
|
||||
{
|
||||
var oldSize = node.NodeSize;
|
||||
CalculatePortPositions(node, style.Width, style.Height);
|
||||
if (oldSize != node.NodeSize) Graph.UpdateNodeGeometry(node);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CalculatePortPositions(BlueprintNode node, float width, float height)
|
||||
{
|
||||
// Position 语义为视觉中心;渲染配置尺寸必须同步给数据模型,供 BoundingBox / RTree 使用。
|
||||
node.NodeSize = new Vector2(width, height);
|
||||
for (int i=0;i<node.InputPorts.Count;i++) node.InputPorts[i].RelativePosition = new Vector2(-width*.5f, height*(.5f-(float)(i+1)/(node.InputPorts.Count+1)));
|
||||
for (int i=0;i<node.OutputPorts.Count;i++) node.OutputPorts[i].RelativePosition = new Vector2(width*.5f, height*(.5f-(float)(i+1)/(node.OutputPorts.Count+1)));
|
||||
}
|
||||
protected override void BeginChunkSubmission(ulong chunk, BlueprintPrimitiveRenderer renderer)
|
||||
{
|
||||
foreach (var pair in _nodeTexts)
|
||||
if (pair.Value.chunkID == chunk) _staleTextCandidates.Add(pair.Key);
|
||||
}
|
||||
protected override void EndChunkSubmission(ulong chunk, BlueprintPrimitiveRenderer renderer)
|
||||
{
|
||||
var remove = new List<string>();
|
||||
foreach (var id in _staleTextCandidates)
|
||||
if (_nodeTexts.TryGetValue(id, out var item) && item.chunkID == chunk) remove.Add(id);
|
||||
foreach (var id in remove)
|
||||
{
|
||||
if (_nodeTexts[id].text != null) Object.Destroy(_nodeTexts[id].text.gameObject);
|
||||
_nodeTexts.Remove(id);
|
||||
}
|
||||
_staleTextCandidates.Clear();
|
||||
}
|
||||
private void UpdateNodeText(BlueprintNode node, ulong chunk, Vector2 local, float width, float height, float border, float titleFontSize, QuickGraphNodeRenderConfig cfg)
|
||||
{
|
||||
_staleTextCandidates.Remove(node.ElementId);
|
||||
TMP_Text text;
|
||||
if (_nodeTexts.TryGetValue(node.ElementId, out var old) && old.chunkID == chunk && old.text != null) text = old.text;
|
||||
else
|
||||
{
|
||||
if (old.text != null) Object.Destroy(old.text.gameObject);
|
||||
var go = new GameObject($"__Text_{node.ElementId}", typeof(RectTransform)); go.hideFlags = HideFlags.HideAndDontSave; go.transform.SetParent(rendererTransform(chunk), false);
|
||||
text = go.AddComponent<TextMeshProUGUI>(); text.alignment = TextAlignmentOptions.Center; _nodeTexts[node.ElementId] = (chunk, text);
|
||||
}
|
||||
text.text=node.NodeTitle; text.color=node.NodeTextColor; text.fontSize=titleFontSize; if(cfg.TitleFont!=null) text.font=cfg.TitleFont;
|
||||
var rt=text.rectTransform; var chunkPivot = text.transform.parent is RectTransform parent ? parent.pivot : new Vector2(.5f, .5f);
|
||||
rt.anchorMin=chunkPivot;rt.anchorMax=chunkPivot;rt.pivot=new Vector2(.5f,.5f);rt.anchoredPosition=local;rt.sizeDelta=new Vector2(width-border*2,height-border*2);text.gameObject.SetActive(true);rt.SetAsLastSibling();
|
||||
}
|
||||
private Transform rendererTransform(ulong chunk) => GetOrCreateChunk(chunk).transform;
|
||||
private void HideNodeText(string id) { if (_nodeTexts.TryGetValue(id,out var item) && item.text != null) item.text.gameObject.SetActive(false); }
|
||||
protected override void OnChunkRecycled(ulong chunk, BlueprintPrimitiveRenderer renderer)
|
||||
{
|
||||
_staleTextCandidates.Clear();
|
||||
var remove=new List<string>(); foreach(var pair in _nodeTexts) if(pair.Value.chunkID==chunk) remove.Add(pair.Key);
|
||||
foreach(var id in remove) { if(_nodeTexts[id].text!=null) Object.Destroy(_nodeTexts[id].text.gameObject); _nodeTexts.Remove(id); }
|
||||
}
|
||||
protected override void DestroyAllChunks() { foreach(var pair in _nodeTexts) if(pair.Value.text!=null) Object.Destroy(pair.Value.text.gameObject); _nodeTexts.Clear(); base.DestroyAllChunks(); }
|
||||
public override void OnDestroy() { _styleResolver?.Dispose(); _styleResolver = null; base.OnDestroy(); }
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 86aab959d7a9d7845b904f91e4533ba1
|
||||
guid: 76eb4633e3b7b1b47ae35a2a4435663c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
@@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using XericLibrary.Runtime.Blueprint;
|
||||
using XericLibrary.Runtime.SuperStyleSheet;
|
||||
using XericLibrary.Runtime.UIGraph;
|
||||
|
||||
namespace XericLibrary.Runtime.Blueprint.QuickGraph
|
||||
{
|
||||
[Flags] public enum QuickGraphRenderStyleChange { None = 0, NodeGeometry = 1, NodeAppearance = 2, WireGeometry = 4, WireAppearance = 8, GridAppearance = 16, All = NodeGeometry | NodeAppearance | WireGeometry | WireAppearance | GridAppearance }
|
||||
public struct QuickGraphNodeRenderStyle { public float Width, Height, Border, Chamfer; public int ChamferSegments; public float TitleFontSize; }
|
||||
public struct QuickGraphWireRenderStyle { public float Width, HitRadius, SourceHandle, TargetHandle, ArrowWidth, ArrowHeight, ArrowProgress, ArrowDepth; public int Tessellation; public ArrowShape ArrowShape; public bool ArrowReversed; }
|
||||
public struct QuickGraphGridRenderStyle { public Material Material; public string ShaderName; public float Size, OverlayPower, Threshold, Exp; public Color Color, Background; }
|
||||
public sealed class QuickGraphRenderStyleSnapshot { public int Revision; public QuickGraphRenderStyleChange Changes; public QuickGraphNodeRenderStyle Node; public QuickGraphWireRenderStyle Wire; public QuickGraphGridRenderStyle Grid; }
|
||||
|
||||
/// <summary>每次样式变更只解析一次;渲染热路径只消费强类型快照。</summary>
|
||||
public sealed class QuickGraphRenderStyleResolver : IDisposable
|
||||
{
|
||||
private readonly BlueprintGraph _graph;
|
||||
private readonly Func<QuickGraphNodeRenderStyle> _nodeFallback;
|
||||
private readonly Func<QuickGraphWireRenderStyle> _wireFallback;
|
||||
private readonly Func<QuickGraphGridRenderStyle> _gridFallback;
|
||||
public QuickGraphRenderStyleSnapshot Snapshot { get; private set; }
|
||||
public event Action<QuickGraphRenderStyleSnapshot> Changed;
|
||||
|
||||
public QuickGraphRenderStyleResolver(BlueprintGraph graph, Func<QuickGraphNodeRenderStyle> nodeFallback = null, Func<QuickGraphWireRenderStyle> wireFallback = null, Func<QuickGraphGridRenderStyle> gridFallback = null)
|
||||
{
|
||||
_graph = graph;
|
||||
_nodeFallback = nodeFallback ?? DefaultNode;
|
||||
_wireFallback = wireFallback ?? DefaultWire;
|
||||
_gridFallback = gridFallback ?? DefaultGrid;
|
||||
_graph.RenderStyleChanged += Refresh;
|
||||
StyleManager.OnStylesReloaded += Refresh;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private static QuickGraphNodeRenderStyle DefaultNode() => new QuickGraphNodeRenderStyle { Width = 90f, Height = 130f, Border = 3f, Chamfer = .2f, ChamferSegments = 4, TitleFontSize = 14f };
|
||||
private static QuickGraphWireRenderStyle DefaultWire() => new QuickGraphWireRenderStyle { Width = 5f, HitRadius = 8f, SourceHandle = 80f, TargetHandle = 80f, ArrowWidth = 12f, ArrowHeight = 10f, ArrowProgress = 1f, ArrowDepth = -1f, Tessellation = 32, ArrowShape = ArrowShape.Triangle };
|
||||
private static QuickGraphGridRenderStyle DefaultGrid() => new QuickGraphGridRenderStyle { ShaderName = "XericLibrary/BluePrint/BlueprintBackgound_GridLine", Size = 100f, OverlayPower = 5f, Threshold = .99f, Exp = .001f, Color = Color.white, Background = Color.black };
|
||||
|
||||
// 样式项存在即优先使用;只有项缺失时才使用旧 ToolConfig 回退值。
|
||||
private T Value<T>(string path, T fallback) { var value = _graph.GetRenderStyle(path); return value == null ? fallback : value.GetValueAs<T>(); }
|
||||
public void Refresh()
|
||||
{
|
||||
var node = _nodeFallback();
|
||||
node = new QuickGraphNodeRenderStyle { Width = Value("/quickgraph/node/width", node.Width), Height = Value("/quickgraph/node/height", node.Height), Border = Value("/quickgraph/node/border", node.Border), Chamfer = Value("/quickgraph/node/chamfer", node.Chamfer), ChamferSegments = Value("/quickgraph/node/chamferSegments", node.ChamferSegments), TitleFontSize = Value("/quickgraph/node/titleFontSize", node.TitleFontSize) };
|
||||
var wire = _wireFallback();
|
||||
wire = new QuickGraphWireRenderStyle { Width = Value("/quickgraph/wire/width", wire.Width), HitRadius = Value("/quickgraph/wire/hitRadius", wire.HitRadius), SourceHandle = Value("/quickgraph/wire/sourceHandle", wire.SourceHandle), TargetHandle = Value("/quickgraph/wire/targetHandle", wire.TargetHandle), ArrowWidth = Value("/quickgraph/wire/arrowWidth", wire.ArrowWidth), ArrowHeight = Value("/quickgraph/wire/arrowHeight", wire.ArrowHeight), ArrowProgress = Value("/quickgraph/wire/arrowProgress", wire.ArrowProgress), ArrowDepth = Value("/quickgraph/wire/arrowDepth", wire.ArrowDepth), Tessellation = Value("/quickgraph/wire/tessellation", wire.Tessellation), ArrowShape = Value("/quickgraph/wire/arrowShape", wire.ArrowShape), ArrowReversed = Value("/quickgraph/wire/arrowReversed", wire.ArrowReversed) };
|
||||
var grid = _gridFallback();
|
||||
grid = new QuickGraphGridRenderStyle { Material = Value("/quickgraph/grid/material", grid.Material), ShaderName = Value("/quickgraph/grid/shader", grid.ShaderName), Size = Value("/quickgraph/grid/size", grid.Size), OverlayPower = Value("/quickgraph/grid/overlayPower", grid.OverlayPower), Threshold = Value("/quickgraph/grid/threshold", grid.Threshold), Exp = Value("/quickgraph/grid/exp", grid.Exp), Color = Value("/quickgraph/grid/color", grid.Color), Background = Value("/quickgraph/grid/background", grid.Background) };
|
||||
|
||||
var old = Snapshot;
|
||||
var changes = old == null ? QuickGraphRenderStyleChange.All : Classify(old, node, wire, grid);
|
||||
Snapshot = new QuickGraphRenderStyleSnapshot { Revision = old == null ? 1 : old.Revision + 1, Changes = changes, Node = node, Wire = wire, Grid = grid };
|
||||
if (changes != QuickGraphRenderStyleChange.None) Changed?.Invoke(Snapshot);
|
||||
}
|
||||
|
||||
private static QuickGraphRenderStyleChange Classify(QuickGraphRenderStyleSnapshot old, QuickGraphNodeRenderStyle node, QuickGraphWireRenderStyle wire, QuickGraphGridRenderStyle grid)
|
||||
{
|
||||
QuickGraphRenderStyleChange changes = QuickGraphRenderStyleChange.None;
|
||||
if (old.Node.Width != node.Width || old.Node.Height != node.Height) changes |= QuickGraphRenderStyleChange.NodeGeometry;
|
||||
if (old.Node.Border != node.Border || old.Node.Chamfer != node.Chamfer || old.Node.ChamferSegments != node.ChamferSegments || old.Node.TitleFontSize != node.TitleFontSize) changes |= QuickGraphRenderStyleChange.NodeAppearance;
|
||||
if (old.Wire.Width != wire.Width || old.Wire.HitRadius != wire.HitRadius || old.Wire.SourceHandle != wire.SourceHandle || old.Wire.TargetHandle != wire.TargetHandle) changes |= QuickGraphRenderStyleChange.WireGeometry;
|
||||
if (old.Wire.ArrowWidth != wire.ArrowWidth || old.Wire.ArrowHeight != wire.ArrowHeight || old.Wire.ArrowProgress != wire.ArrowProgress || old.Wire.ArrowDepth != wire.ArrowDepth || old.Wire.Tessellation != wire.Tessellation || old.Wire.ArrowShape != wire.ArrowShape || old.Wire.ArrowReversed != wire.ArrowReversed) changes |= QuickGraphRenderStyleChange.WireAppearance;
|
||||
if (old.Grid.Material != grid.Material || old.Grid.ShaderName != grid.ShaderName || old.Grid.Size != grid.Size || old.Grid.OverlayPower != grid.OverlayPower || old.Grid.Threshold != grid.Threshold || old.Grid.Exp != grid.Exp || old.Grid.Color != grid.Color || old.Grid.Background != grid.Background) changes |= QuickGraphRenderStyleChange.GridAppearance;
|
||||
return changes;
|
||||
}
|
||||
public void Dispose() { _graph.RenderStyleChanged -= Refresh; StyleManager.OnStylesReloaded -= Refresh; Changed = null; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a2abe7122e63ad946ba485026296788a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,113 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace XericLibrary.Runtime.Blueprint.QuickGraph
|
||||
{
|
||||
/// <summary>QuickGraph 网格背景;样式资源材质只绑定不拥有,运行时创建材质才由本工具释放。</summary>
|
||||
[BlueprintTool(phase: ToolPhase.Render, order: 0)]
|
||||
[BlueprintTheme("QuickGraph")]
|
||||
public class QuickUGUIGraphGridBackgroundTool : BlueprintTool
|
||||
{
|
||||
private const string BackgroundGoName = "__Bp_GridBackground";
|
||||
private QuickGraphGridBackgroundConfig _defaultConfig;
|
||||
private QuickGraphRenderStyleResolver _styleResolver;
|
||||
private Image _backgroundImage;
|
||||
private Material _backgroundMaterial;
|
||||
private QuickGraphGridBackgroundConfig _ownedMaterialConfig;
|
||||
private string _ownedShaderName;
|
||||
private QuickGraphGridBackgroundConfig Config
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ToolConfig is QuickGraphGridBackgroundConfig external) return external;
|
||||
if (_defaultConfig == null) _defaultConfig = ScriptableObject.CreateInstance<QuickGraphGridBackgroundConfig>();
|
||||
return _defaultConfig;
|
||||
}
|
||||
}
|
||||
public override void OnInitialize()
|
||||
{
|
||||
_styleResolver = new QuickGraphRenderStyleResolver(Graph, gridFallback: ConfigStyle);
|
||||
_styleResolver.Changed += OnStyleChanged;
|
||||
EnsureBackground(); RebindMaterial(_styleResolver.Snapshot.Grid, true);
|
||||
}
|
||||
public override void OnRender()
|
||||
{
|
||||
if (Graph == null) return;
|
||||
EnsureBackground();
|
||||
var style = _styleResolver != null ? _styleResolver.Snapshot.Grid : ConfigStyle();
|
||||
RebindMaterial(style, false);
|
||||
ApplyStyle(style);
|
||||
}
|
||||
public override void OnConfigChanged()
|
||||
{
|
||||
_styleResolver?.Refresh();
|
||||
if (_styleResolver == null) RebindMaterial(ConfigStyle(), true);
|
||||
Graph?.MarkDirty();
|
||||
}
|
||||
private QuickGraphGridRenderStyle ConfigStyle()
|
||||
{
|
||||
var cfg = Config;
|
||||
return new QuickGraphGridRenderStyle { Material = cfg.OverrideMaterial, ShaderName = cfg.ShaderName, Size = cfg.GridSize, OverlayPower = cfg.GridOverlayPower, Threshold = cfg.GridLineThreshold, Exp = cfg.GridExp, Color = cfg.GridColor, Background = cfg.GridBackgroundColor };
|
||||
}
|
||||
private void OnStyleChanged(QuickGraphRenderStyleSnapshot snapshot)
|
||||
{
|
||||
if ((snapshot.Changes & QuickGraphRenderStyleChange.GridAppearance) == 0) return;
|
||||
RebindMaterial(snapshot.Grid, true);
|
||||
Graph?.MarkDirty();
|
||||
}
|
||||
private void EnsureBackground()
|
||||
{
|
||||
if (_backgroundImage != null || 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) return;
|
||||
if (existing != null) Object.DestroyImmediate(existing.gameObject);
|
||||
var go = new GameObject(BackgroundGoName, typeof(RectTransform)); go.hideFlags = HideFlags.HideAndDontSave; go.transform.SetParent(root, false); go.transform.SetAsFirstSibling();
|
||||
var rect = go.GetComponent<RectTransform>(); rect.anchorMin = Vector2.zero; rect.anchorMax = Vector2.one; rect.offsetMin = Vector2.zero; rect.offsetMax = Vector2.zero;
|
||||
_backgroundImage = go.AddComponent<Image>(); _backgroundImage.raycastTarget = true; _backgroundImage.maskable = false;
|
||||
}
|
||||
private void RebindMaterial(QuickGraphGridRenderStyle style, bool force)
|
||||
{
|
||||
if (_backgroundImage == null) return;
|
||||
Material target = style.Material;
|
||||
QuickGraphGridBackgroundConfig owner = null;
|
||||
string ownedShader = null;
|
||||
if (target == null)
|
||||
{
|
||||
// 旧 Config 仅在 style 未提供共享材质时参与回退;其缓存材质为唯一可释放对象。
|
||||
if (style.ShaderName == Config.ShaderName) { target = Config.GetOrCreateMaterial(); owner = Config.OverrideMaterial == null ? Config : null; }
|
||||
else if (!string.IsNullOrEmpty(style.ShaderName))
|
||||
{
|
||||
if (_backgroundMaterial != null && _ownedShaderName == style.ShaderName) return;
|
||||
var shader = Shader.Find(style.ShaderName); if (shader != null) { target = new Material(shader) { name = "BpGridBackground_StyleMat" }; ownedShader = style.ShaderName; }
|
||||
}
|
||||
}
|
||||
if (target == _backgroundMaterial) return;
|
||||
ReleaseOwnedMaterial();
|
||||
_backgroundMaterial = target; _ownedMaterialConfig = owner; _ownedShaderName = ownedShader;
|
||||
if (_backgroundImage != null) _backgroundImage.material = _backgroundMaterial;
|
||||
}
|
||||
private void ApplyStyle(QuickGraphGridRenderStyle style)
|
||||
{
|
||||
if (_backgroundMaterial == null || Graph?.Canvas == null || _backgroundImage == null) return;
|
||||
var size = _backgroundImage.rectTransform.rect.size; var zoom = Graph.Canvas.ZoomLevel; var pan = Graph.Canvas.PanOffset; var gridSize = Mathf.Max(.0001f, style.Size);
|
||||
_backgroundMaterial.SetVector("_Transform", new Vector4(size.x / (zoom * gridSize), size.y / (zoom * gridSize), -(pan.x + .5f * size.x) / (zoom * gridSize), -(pan.y + .5f * size.y) / (zoom * gridSize)));
|
||||
_backgroundMaterial.SetFloat("_GridOverlayPower", style.OverlayPower); _backgroundMaterial.SetFloat("_GridLineThreshold", style.Threshold); _backgroundMaterial.SetFloat("_GridExp", style.Exp);
|
||||
_backgroundMaterial.SetColor("_GridColor", style.Color); _backgroundMaterial.SetColor("_GridBackgroundColor", style.Background);
|
||||
}
|
||||
private void ReleaseOwnedMaterial()
|
||||
{
|
||||
if (_backgroundImage != null) _backgroundImage.material = null;
|
||||
if (_ownedMaterialConfig != null) _ownedMaterialConfig.ReleaseMaterial();
|
||||
else if (_backgroundMaterial != null && !string.IsNullOrEmpty(_ownedShaderName)) { if (Application.isPlaying) Object.Destroy(_backgroundMaterial); else Object.DestroyImmediate(_backgroundMaterial); }
|
||||
_backgroundMaterial = null; _ownedMaterialConfig = null; _ownedShaderName = null;
|
||||
}
|
||||
public override void OnDestroy()
|
||||
{
|
||||
if (_styleResolver != null) _styleResolver.Changed -= OnStyleChanged;
|
||||
_styleResolver?.Dispose(); _styleResolver = null; ReleaseOwnedMaterial();
|
||||
if (_backgroundImage != null) { if (Application.isPlaying) Object.Destroy(_backgroundImage.gameObject); else Object.DestroyImmediate(_backgroundImage.gameObject); _backgroundImage = null; }
|
||||
_defaultConfig = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: da293e1f23cfca24b9ca4d40f91d2d8c
|
||||
guid: 1ba1fe1f333ee914b962da638a630281
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
@@ -0,0 +1,82 @@
|
||||
using UnityEngine;
|
||||
using XericLibrary.Runtime.Blueprint.Render;
|
||||
using XericLibrary.Runtime.UIGraph;
|
||||
|
||||
namespace XericLibrary.Runtime.Blueprint.QuickGraph
|
||||
{
|
||||
/// <summary>按源节点 Chunk 提交连线的渲染工具;所有局部坐标由基类 submission 提供。</summary>
|
||||
[BlueprintTool(phase: ToolPhase.Render, order: 110)]
|
||||
[BlueprintTheme("QuickGraph")]
|
||||
public class WireCanvasRenderTool : BlueprintCanvasRenderTool<BlueprintCurveRenderer>
|
||||
{
|
||||
private QuickGraphWireRenderConfig _defaultConfig;
|
||||
private QuickGraphRenderStyleResolver _styleResolver;
|
||||
private int _appliedStyleRevision = -1;
|
||||
private QuickGraphWireRenderConfig Config
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ToolConfig is QuickGraphWireRenderConfig external) return external;
|
||||
if (_defaultConfig == null) _defaultConfig = ScriptableObject.CreateInstance<QuickGraphWireRenderConfig>();
|
||||
return _defaultConfig;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnInitialize()
|
||||
{
|
||||
base.OnInitialize();
|
||||
_styleResolver = new QuickGraphRenderStyleResolver(Graph, wireFallback: ConfigStyle);
|
||||
_styleResolver.Changed += OnStyleChanged;
|
||||
ApplyWireGeometry(_styleResolver.Snapshot.Wire, true);
|
||||
}
|
||||
public override void OnConfigChanged()
|
||||
{
|
||||
_styleResolver?.Refresh();
|
||||
if (_styleResolver == null) ApplyWireGeometry(ConfigStyle(), true);
|
||||
base.OnConfigChanged();
|
||||
}
|
||||
private QuickGraphWireRenderStyle ConfigStyle()
|
||||
{
|
||||
var cfg = Config;
|
||||
return new QuickGraphWireRenderStyle { Width = cfg.WireWidth, HitRadius = cfg.HitTestRadius, SourceHandle = cfg.SourceHandleLength, TargetHandle = cfg.TargetHandleLength, ArrowShape = cfg.ArrowShape, ArrowReversed = cfg.ArrowReversed, ArrowProgress = cfg.ArrowProgress, ArrowWidth = cfg.ArrowWidth, ArrowHeight = cfg.ArrowHeight, ArrowDepth = cfg.ArrowDepthCompensation, Tessellation = cfg.TessellationSegments };
|
||||
}
|
||||
private void OnStyleChanged(QuickGraphRenderStyleSnapshot snapshot)
|
||||
{
|
||||
if ((snapshot.Changes & (QuickGraphRenderStyleChange.WireGeometry | QuickGraphRenderStyleChange.WireAppearance)) == 0) return;
|
||||
ApplyWireGeometry(snapshot.Wire, (snapshot.Changes & QuickGraphRenderStyleChange.WireGeometry) != 0);
|
||||
Graph?.MarkAllElementsDirty();
|
||||
}
|
||||
private void ApplyWireGeometry(QuickGraphWireRenderStyle style, bool force)
|
||||
{
|
||||
if (Graph == null || (!force && _appliedStyleRevision == _styleResolver.Snapshot.Revision)) return;
|
||||
Graph.SetWireGeometrySettings(new BlueprintWireGeometrySettings { SourceHandleLength = style.SourceHandle, TargetHandleLength = style.TargetHandle, WireWidth = style.Width, HitTestRadius = style.HitRadius });
|
||||
foreach (var wire in Graph.Wires) Graph.UpdateWireGeometry(wire);
|
||||
_appliedStyleRevision = _styleResolver != null ? _styleResolver.Snapshot.Revision : _appliedStyleRevision + 1;
|
||||
}
|
||||
|
||||
protected override BlueprintCurveRenderer BuildChunkRenderer(GameObject chunkGo) => chunkGo.AddComponent<BlueprintCurveRenderer>();
|
||||
protected override void ClearChunkData(BlueprintCurveRenderer renderer) => renderer.ClearAll();
|
||||
protected override void SetChunkDirty(BlueprintCurveRenderer renderer) => renderer.RebuildAll();
|
||||
protected override bool AcceptsElement(IBlueprintElement element) => element is BlueprintWire;
|
||||
protected override void AppendElementData(BlueprintCurveRenderer renderer, IBlueprintElement element, ulong chunkID)
|
||||
{
|
||||
if (!(element is BlueprintWire wire) || wire.SourcePort == null || wire.TargetPort == null || wire.SourcePort.OwnerNode == null || wire.TargetPort.OwnerNode == null) return;
|
||||
var style = _styleResolver != null ? _styleResolver.Snapshot.Wire : ConfigStyle();
|
||||
wire.GetControlPoints(out var startCanvas, out var handle1Canvas, out var handle2Canvas, out var endCanvas);
|
||||
var submission = CurrentElementSubmission;
|
||||
var ctrlPts = new[] { submission.ToChunkLocal(startCanvas), submission.ToChunkLocal(handle1Canvas), submission.ToChunkLocal(handle2Canvas), submission.ToChunkLocal(endCanvas) };
|
||||
var widths = new[] { style.Width, style.Width, style.Width, style.Width };
|
||||
Color32 srcColor = wire.SourcePort.OwnerNode.NodeColor;
|
||||
Color32 tgtColor = wire.TargetPort.OwnerNode.NodeColor;
|
||||
var arrow = new ArrowHeadData { shape = style.ArrowShape, reversed = style.ArrowReversed, progress = style.ArrowProgress, width = style.ArrowWidth, height = style.ArrowHeight, depthCompensation = style.ArrowDepth, color = tgtColor };
|
||||
var entry = new CurveCacheEntry { startColor = srcColor, endColor = tgtColor, tessellationSegments = Mathf.Max(2, Mathf.RoundToInt(style.Tessellation * Graph.CurrentLodSettings.WireTessellationMultiplier)) };
|
||||
renderer.AddCurve(entry, ctrlPts, widths, new[] { arrow });
|
||||
}
|
||||
public override void OnDestroy()
|
||||
{
|
||||
if (_styleResolver != null) _styleResolver.Changed -= OnStyleChanged;
|
||||
_styleResolver?.Dispose(); _styleResolver = null;
|
||||
base.OnDestroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3881805202e8cd049aad2d55c9f595f1
|
||||
guid: e6d0420274cb4d9469b505ea464a1671
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
@@ -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,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,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,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Reference in New Issue
Block a user