From 46560ea78f6fd848b243ba3416877fd523ffc9e1 Mon Sep 17 00:00:00 2001
From: LiRuoChen <571244399@qq.com>
Date: Thu, 30 Jul 2026 09:18:26 +0800
Subject: [PATCH] update 0.6.3.7
---
README.md | 14 +-
.../Lit/SceneGroundGraphs.shadergraph.meta | 2 +-
.../Blueprint/BlueprintBackgound_GridLine.mat | 12 +-
.../UISelectionBoxGraphs.shadergraph.meta | 2 +-
.../DefaultXericStyleSheet.xsss | 59 +++
.../DefaultXericStyleSheet.xsss.meta | 10 +
.../QuickGraphGridBackgroundConfig.asset | 10 +-
.../QuickGraphInputConfig.asset | 7 +-
.../QuickGraphNodeRenderConfig.asset | 22 +-
.../QuickGraphRenderStyle.xsss | 41 ++
.../QuickGraphRenderStyle.xsss.meta | 10 +
.../QuickGraphWireRenderConfig.asset | 12 +-
.../Canvas/ScreenSpaceBlueprintCanvas.cs | 24 +-
.../Canvas/WorldSpaceBlueprintCanvas.cs | 19 +-
.../GraphTheoryBlueprintComponent.cs | 118 ++----
.../Blueprint/Input/BlueprintBoxSelectTool.cs | 55 +--
.../Input/BlueprintInputTool_InputManager.cs | 18 +-
.../Input/BlueprintInputTool_InputSystem.cs | 9 +-
.../Input/BlueprintInputTool_UGUI.cs | 29 +-
.../Input/BlueprintViewportController.cs | 131 +++++++
.../Input/BlueprintViewportController.cs.meta | 11 +
.../Blueprint/Input/QuickGraphInputTool.cs | 221 -----------
Runtime/Blueprint/QuickGraph.meta | 8 +
Runtime/Blueprint/QuickGraph/Input.meta | 8 +
.../QuickGraph/Input/QuickGraphInputTool.cs | 198 ++++++++++
.../Input/QuickGraphInputTool.cs.meta | 2 +-
Runtime/Blueprint/QuickGraph/Render.meta | 8 +
.../{ => QuickGraph}/Render/Config.meta | 2 +-
.../Config/QuickGraphGridBackgroundConfig.cs | 14 +-
.../QuickGraphGridBackgroundConfig.cs.meta | 2 +-
.../Render/Config/QuickGraphInputConfig.cs | 18 +-
.../Config/QuickGraphInputConfig.cs.meta | 2 +-
.../Config/QuickGraphNodeRenderConfig.cs | 14 +-
.../Config/QuickGraphNodeRenderConfig.cs.meta | 2 +-
.../Config/QuickGraphWireRenderConfig.cs | 6 +-
.../Config/QuickGraphWireRenderConfig.cs.meta | 2 +-
.../QuickGraph/Render/NodeCanvasRenderTool.cs | 118 ++++++
.../Render/NodeCanvasRenderTool.cs.meta | 2 +-
.../Render/QuickGraphRenderStyleResolver.cs | 69 ++++
.../QuickGraphRenderStyleResolver.cs.meta | 11 +
.../QuickUGUIGraphGridBackgroundTool.cs | 113 ++++++
.../QuickUGUIGraphGridBackgroundTool.cs.meta | 2 +-
.../QuickGraph/Render/WireCanvasRenderTool.cs | 82 ++++
.../Render/WireCanvasRenderTool.cs.meta | 2 +-
.../Render/BlueprintCanvasRenderTool.cs | 355 ++++--------------
.../Blueprint/Render/NodeCanvasRenderTool.cs | 278 --------------
.../QuickUGUIGraphGridBackgroundTool.cs | 162 --------
.../Blueprint/Render/WireCanvasRenderTool.cs | 135 -------
Runtime/XericLibrary.dll | Bin 1601024 -> 1624576 bytes
49 files changed, 1126 insertions(+), 1325 deletions(-)
create mode 100644 Resources/ScriptableObject/DefaultXericStyleSheet.xsss
create mode 100644 Resources/ScriptableObject/DefaultXericStyleSheet.xsss.meta
create mode 100644 Resources/ScriptableObject/QuickGraphRenderStyle.xsss
create mode 100644 Resources/ScriptableObject/QuickGraphRenderStyle.xsss.meta
create mode 100644 Runtime/Blueprint/Input/BlueprintViewportController.cs
create mode 100644 Runtime/Blueprint/Input/BlueprintViewportController.cs.meta
delete mode 100644 Runtime/Blueprint/Input/QuickGraphInputTool.cs
create mode 100644 Runtime/Blueprint/QuickGraph.meta
create mode 100644 Runtime/Blueprint/QuickGraph/Input.meta
create mode 100644 Runtime/Blueprint/QuickGraph/Input/QuickGraphInputTool.cs
rename Runtime/Blueprint/{ => QuickGraph}/Input/QuickGraphInputTool.cs.meta (83%)
create mode 100644 Runtime/Blueprint/QuickGraph/Render.meta
rename Runtime/Blueprint/{ => QuickGraph}/Render/Config.meta (77%)
rename Runtime/Blueprint/{ => QuickGraph}/Render/Config/QuickGraphGridBackgroundConfig.cs (85%)
rename Runtime/Blueprint/{ => QuickGraph}/Render/Config/QuickGraphGridBackgroundConfig.cs.meta (83%)
rename Runtime/Blueprint/{ => QuickGraph}/Render/Config/QuickGraphInputConfig.cs (73%)
rename Runtime/Blueprint/{ => QuickGraph}/Render/Config/QuickGraphInputConfig.cs.meta (83%)
rename Runtime/Blueprint/{ => QuickGraph}/Render/Config/QuickGraphNodeRenderConfig.cs (76%)
rename Runtime/Blueprint/{ => QuickGraph}/Render/Config/QuickGraphNodeRenderConfig.cs.meta (83%)
rename Runtime/Blueprint/{ => QuickGraph}/Render/Config/QuickGraphWireRenderConfig.cs (90%)
rename Runtime/Blueprint/{ => QuickGraph}/Render/Config/QuickGraphWireRenderConfig.cs.meta (83%)
create mode 100644 Runtime/Blueprint/QuickGraph/Render/NodeCanvasRenderTool.cs
rename Runtime/Blueprint/{ => QuickGraph}/Render/NodeCanvasRenderTool.cs.meta (83%)
create mode 100644 Runtime/Blueprint/QuickGraph/Render/QuickGraphRenderStyleResolver.cs
create mode 100644 Runtime/Blueprint/QuickGraph/Render/QuickGraphRenderStyleResolver.cs.meta
create mode 100644 Runtime/Blueprint/QuickGraph/Render/QuickUGUIGraphGridBackgroundTool.cs
rename Runtime/Blueprint/{ => QuickGraph}/Render/QuickUGUIGraphGridBackgroundTool.cs.meta (83%)
create mode 100644 Runtime/Blueprint/QuickGraph/Render/WireCanvasRenderTool.cs
rename Runtime/Blueprint/{ => QuickGraph}/Render/WireCanvasRenderTool.cs.meta (83%)
delete mode 100644 Runtime/Blueprint/Render/NodeCanvasRenderTool.cs
delete mode 100644 Runtime/Blueprint/Render/QuickUGUIGraphGridBackgroundTool.cs
delete mode 100644 Runtime/Blueprint/Render/WireCanvasRenderTool.cs
diff --git a/README.md b/README.md
index df9dd5b..475714f 100644
--- a/README.md
+++ b/README.md
@@ -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**:自动化配置界面的预制体与脚本示例。
+- **轨迹线路**:平面线路绘制与导航示例。
---
diff --git a/Resources/Material/Graph/Lit/SceneGroundGraphs.shadergraph.meta b/Resources/Material/Graph/Lit/SceneGroundGraphs.shadergraph.meta
index d196a94..b507f4b 100644
--- a/Resources/Material/Graph/Lit/SceneGroundGraphs.shadergraph.meta
+++ b/Resources/Material/Graph/Lit/SceneGroundGraphs.shadergraph.meta
@@ -7,4 +7,4 @@ ScriptedImporter:
userData:
assetBundleName:
assetBundleVariant:
- script: {fileID: 11500000, guid: 625f186215c104763be7675aa2d941aa, type: 3}
+ script: {instanceID: 0}
diff --git a/Resources/Material/Graph/UI/Blueprint/BlueprintBackgound_GridLine.mat b/Resources/Material/Graph/UI/Blueprint/BlueprintBackgound_GridLine.mat
index 78c109e..9db45d2 100644
--- a/Resources/Material/Graph/UI/Blueprint/BlueprintBackgound_GridLine.mat
+++ b/Resources/Material/Graph/UI/Blueprint/BlueprintBackgound_GridLine.mat
@@ -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: []
diff --git a/Resources/Material/Graph/UI/General/UISelectionBoxGraphs.shadergraph.meta b/Resources/Material/Graph/UI/General/UISelectionBoxGraphs.shadergraph.meta
index f1382c3..c18ff9a 100644
--- a/Resources/Material/Graph/UI/General/UISelectionBoxGraphs.shadergraph.meta
+++ b/Resources/Material/Graph/UI/General/UISelectionBoxGraphs.shadergraph.meta
@@ -7,4 +7,4 @@ ScriptedImporter:
userData:
assetBundleName:
assetBundleVariant:
- script: {fileID: 11500000, guid: 625f186215c104763be7675aa2d941aa, type: 3}
+ script: {instanceID: 0}
diff --git a/Resources/ScriptableObject/DefaultXericStyleSheet.xsss b/Resources/ScriptableObject/DefaultXericStyleSheet.xsss
new file mode 100644
index 0000000..54ae562
--- /dev/null
+++ b/Resources/ScriptableObject/DefaultXericStyleSheet.xsss
@@ -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
+}
\ No newline at end of file
diff --git a/Resources/ScriptableObject/DefaultXericStyleSheet.xsss.meta b/Resources/ScriptableObject/DefaultXericStyleSheet.xsss.meta
new file mode 100644
index 0000000..8ddc5bf
--- /dev/null
+++ b/Resources/ScriptableObject/DefaultXericStyleSheet.xsss.meta
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: bf2a8de55a33d4243932204e98b72249
+ScriptedImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 2
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+ script: {fileID: 1693830282, guid: 65aae7f37e34be6409e0966d54c89b6d, type: 3}
diff --git a/Resources/ScriptableObject/QuickGraphGridBackgroundConfig.asset b/Resources/ScriptableObject/QuickGraphGridBackgroundConfig.asset
index 619b2cb..887541f 100644
--- a/Resources/ScriptableObject/QuickGraphGridBackgroundConfig.asset
+++ b/Resources/ScriptableObject/QuickGraphGridBackgroundConfig.asset
@@ -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}
diff --git a/Resources/ScriptableObject/QuickGraphInputConfig.asset b/Resources/ScriptableObject/QuickGraphInputConfig.asset
index 2236bb1..030cc6e 100644
--- a/Resources/ScriptableObject/QuickGraphInputConfig.asset
+++ b/Resources/ScriptableObject/QuickGraphInputConfig.asset
@@ -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
diff --git a/Resources/ScriptableObject/QuickGraphNodeRenderConfig.asset b/Resources/ScriptableObject/QuickGraphNodeRenderConfig.asset
index 4e6c43e..7ee9dbc 100644
--- a/Resources/ScriptableObject/QuickGraphNodeRenderConfig.asset
+++ b/Resources/ScriptableObject/QuickGraphNodeRenderConfig.asset
@@ -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
diff --git a/Resources/ScriptableObject/QuickGraphRenderStyle.xsss b/Resources/ScriptableObject/QuickGraphRenderStyle.xsss
new file mode 100644
index 0000000..91bd3c5
--- /dev/null
+++ b/Resources/ScriptableObject/QuickGraphRenderStyle.xsss
@@ -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
+}
diff --git a/Resources/ScriptableObject/QuickGraphRenderStyle.xsss.meta b/Resources/ScriptableObject/QuickGraphRenderStyle.xsss.meta
new file mode 100644
index 0000000..4999e47
--- /dev/null
+++ b/Resources/ScriptableObject/QuickGraphRenderStyle.xsss.meta
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: 499645b6d899d1f4d936ee3eb0eb9b0b
+ScriptedImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 2
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+ script: {fileID: 1693830282, guid: 65aae7f37e34be6409e0966d54c89b6d, type: 3}
diff --git a/Resources/ScriptableObject/QuickGraphWireRenderConfig.asset b/Resources/ScriptableObject/QuickGraphWireRenderConfig.asset
index a2ae408..56792f1 100644
--- a/Resources/ScriptableObject/QuickGraphWireRenderConfig.asset
+++ b/Resources/ScriptableObject/QuickGraphWireRenderConfig.asset
@@ -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
diff --git a/Runtime/Blueprint/Canvas/ScreenSpaceBlueprintCanvas.cs b/Runtime/Blueprint/Canvas/ScreenSpaceBlueprintCanvas.cs
index 1ee9522..0a086a0 100644
--- a/Runtime/Blueprint/Canvas/ScreenSpaceBlueprintCanvas.cs
+++ b/Runtime/Blueprint/Canvas/ScreenSpaceBlueprintCanvas.cs
@@ -19,6 +19,7 @@ namespace XericLibrary.Runtime.Blueprint.Canvas
{
private readonly UnityEngine.Canvas _unityCanvas;
private readonly RectTransform _renderRoot;
+ private readonly RectTransform _viewport;
/// 动态获取摄像机引用(不缓存),
/// 自动适应 Canvas.renderMode 运行时的变化。
@@ -29,10 +30,11 @@ namespace XericLibrary.Runtime.Blueprint.Canvas
///
/// 所属的 Unity UGUI Canvas 组件
/// 蓝图渲染根 RectTransform,所有蓝图层容器和背景挂载于此
- 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;
}
///
@@ -41,11 +43,27 @@ namespace XericLibrary.Runtime.Blueprint.Canvas
/// 以 为参照转换,
/// 自动处理 Canvas 渲染模式和 RenderRoot 的偏移。
///
- 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);
}
///
diff --git a/Runtime/Blueprint/Canvas/WorldSpaceBlueprintCanvas.cs b/Runtime/Blueprint/Canvas/WorldSpaceBlueprintCanvas.cs
index 76b0a82..81a63a2 100644
--- a/Runtime/Blueprint/Canvas/WorldSpaceBlueprintCanvas.cs
+++ b/Runtime/Blueprint/Canvas/WorldSpaceBlueprintCanvas.cs
@@ -35,23 +35,16 @@ namespace XericLibrary.Runtime.Blueprint.Canvas
///
/// 屏幕空间的坐标点
/// 画布空间的坐标点
- 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);
}
///
diff --git a/Runtime/Blueprint/GraphTheoryBlueprintComponent.cs b/Runtime/Blueprint/GraphTheoryBlueprintComponent.cs
index d1a68b7..5a79c7f 100644
--- a/Runtime/Blueprint/GraphTheoryBlueprintComponent.cs
+++ b/Runtime/Blueprint/GraphTheoryBlueprintComponent.cs
@@ -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
{
///
- /// 图论蓝图组件 —— 在一个 UGUI Canvas 上挂载蓝图系统。
- /// 驱动方式:
- /// - 编辑器非运行态:EditorApplication.update → EditorTick
- /// - 运行时:MonoBehaviour.Update
+ /// 图论蓝图组件:Unity Inspector 配置、画布创建及调试入口。
+ /// 帧驱动完全由 承担。
///
[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 _toolConfigAssets = new List();
-
[Header("Test")]
[Tooltip("是否自动生成测试节点")]
[SerializeField] public bool _autoGenerateTestNodes = true;
@@ -81,17 +74,16 @@ namespace XericLibrary.Runtime.Blueprint
/// 当前画布
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();
+ if (selfRt == null)
+ selfRt = gameObject.AddComponent();
+ 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();
+ 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;
diff --git a/Runtime/Blueprint/Input/BlueprintBoxSelectTool.cs b/Runtime/Blueprint/Input/BlueprintBoxSelectTool.cs
index 75db439..0474a2e 100644
--- a/Runtime/Blueprint/Input/BlueprintBoxSelectTool.cs
+++ b/Runtime/Blueprint/Input/BlueprintBoxSelectTool.cs
@@ -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
/// 蓝图框选工具 —— 鼠标左键在空白区域拖拽,绘制矩形选框,
/// 释放时计算框选范围内所有元素的交集并输出统计日志。
/// 选框 UGUI Image 默认渲染在所有层之上(SetAsLastSibling)。
- /// 框选结果保存在 中供其他工具读取。
+ /// 框选结果通过 (Graph). 读取。
///
- /// 选框仅在拖拽距离超过 QuickGraphInputConfig.BoxSelectThreshold 后才显示,
+ /// 选框仅在拖拽距离超过 后才显示,
/// 避免误触显示闪烁。
///
///
[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();
- }
- }
- return _boxConfig;
- }
- }
+ /// 框选拖拽触发阈值(画布单位)。拖拽距离超过此值后才显示选框并开始框选。
+ public float BoxSelectThreshold = 10f;
// ---------- 结果 ----------
- /// 结果:通过 Provider.CurrentSelectElements 读取框选结果。
+ /// 结果:通过 Provider.Selector.CurrentSelection 读取框选结果。
// ===== 鼠标按下 =====
@@ -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();
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()
diff --git a/Runtime/Blueprint/Input/BlueprintInputTool_InputManager.cs b/Runtime/Blueprint/Input/BlueprintInputTool_InputManager.cs
index 2adc9de..e2ebd39 100644
--- a/Runtime/Blueprint/Input/BlueprintInputTool_InputManager.cs
+++ b/Runtime/Blueprint/Input/BlueprintInputTool_InputManager.cs
@@ -10,12 +10,12 @@ namespace XericLibrary.Runtime.Blueprint
/// 通过 KfsStyle + KeyFlippingStampSheet 状态机驱动拖拽识别。
///
[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;
}
///
@@ -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);
}
diff --git a/Runtime/Blueprint/Input/BlueprintInputTool_InputSystem.cs b/Runtime/Blueprint/Input/BlueprintInputTool_InputSystem.cs
index c8c0da6..2a6824f 100644
--- a/Runtime/Blueprint/Input/BlueprintInputTool_InputSystem.cs
+++ b/Runtime/Blueprint/Input/BlueprintInputTool_InputSystem.cs
@@ -11,7 +11,6 @@ namespace XericLibrary.Runtime.Blueprint
/// 不再依赖 BlueprintInputManager 管理器。
///
[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();
- _panCanceled = _ => GraphInputTool.LastPanDirection = Vector2.zero;
- _zoomHandler = ctx => GraphInputTool.LastZoomAxis = ctx.ReadValue();
- _zoomCanceled = _ => GraphInputTool.LastZoomAxis = 0f;
+ _panHandler = ctx => PanDirection = ctx.ReadValue();
+ _panCanceled = _ => PanDirection = Vector2.zero;
+ _zoomHandler = ctx => ZoomAxis = ctx.ReadValue();
+ _zoomCanceled = _ => ZoomAxis = 0f;
_homeHandler = _ => DispatchShortcut(BlueprintInputConstants.FocusHome);
_undoHandler = _ => DispatchShortcut(BlueprintInputConstants.Undo);
_redoHandler = _ => DispatchShortcut(BlueprintInputConstants.Redo);
diff --git a/Runtime/Blueprint/Input/BlueprintInputTool_UGUI.cs b/Runtime/Blueprint/Input/BlueprintInputTool_UGUI.cs
index 92e6400..242d063 100644
--- a/Runtime/Blueprint/Input/BlueprintInputTool_UGUI.cs
+++ b/Runtime/Blueprint/Input/BlueprintInputTool_UGUI.cs
@@ -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;
///
/// 是否通过 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;
}
///
@@ -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)
diff --git a/Runtime/Blueprint/Input/BlueprintViewportController.cs b/Runtime/Blueprint/Input/BlueprintViewportController.cs
new file mode 100644
index 0000000..eebd936
--- /dev/null
+++ b/Runtime/Blueprint/Input/BlueprintViewportController.cs
@@ -0,0 +1,131 @@
+using UnityEngine;
+
+namespace XericLibrary.Runtime.Blueprint
+{
+ ///
+ /// 蓝图视口控制器 —— 管理视图状态(Pan/Zoom/范围),提供外部 API 供工具和外部代码读写。
+ ///
+ /// 在 ToolPhase.PreUpdate, order: 40 执行,每帧直接将目标值写入 ,
+ /// 不做插值平滑。输入工具通过写入 /
+ /// 来驱动视图,外部代码可通过 BlueprintToolProvider.GetTool<BlueprintViewportController>(graph)
+ /// 获取实例直接控制视图。
+ ///
+ ///
+ /// 此工具为通用基础工具(无主题限定),对所有蓝图主题可见。
+ ///
+ ///
+ [BlueprintTool(phase: ToolPhase.PreUpdate, order: 40)]
+ public class BlueprintViewportController : BlueprintTool
+ {
+ // ── 目标值(由输入工具或外部代码写入) ──
+
+ /// 目标平移偏移量。外部代码直接设置此值即可控制视图平移。
+ public Vector2 TargetPanOffset = Vector2.zero;
+
+ /// 目标缩放级别。外部代码直接设置此值即可控制视图缩放。
+ public float TargetZoomLevel = 1f;
+
+ /// 最小缩放限制。
+ public float MinZoom = 0.1f;
+
+ /// 最大缩放限制。
+ 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
+ // ====================================================================
+
+ /// 重置视图到原点,缩放归 1。
+ public void ResetView()
+ {
+ TargetPanOffset = Vector2.zero;
+ TargetZoomLevel = 1f;
+ Graph?.MarkDirty();
+ }
+
+ /// 聚焦到某个画布坐标点。
+ 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();
+ }
+
+ /// 聚焦到一组元素的包围盒区域。
+ public void FocusOnElements(System.Collections.Generic.IReadOnlyList 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);
+ }
+ }
+}
diff --git a/Runtime/Blueprint/Input/BlueprintViewportController.cs.meta b/Runtime/Blueprint/Input/BlueprintViewportController.cs.meta
new file mode 100644
index 0000000..8bdea15
--- /dev/null
+++ b/Runtime/Blueprint/Input/BlueprintViewportController.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f4bb7dd4bbd0e6e4fac82161405eca13
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime/Blueprint/Input/QuickGraphInputTool.cs b/Runtime/Blueprint/Input/QuickGraphInputTool.cs
deleted file mode 100644
index f17ff90..0000000
--- a/Runtime/Blueprint/Input/QuickGraphInputTool.cs
+++ /dev/null
@@ -1,221 +0,0 @@
-using UnityEngine;
-using XericLibrary.Runtime.Blueprint.Render;
-
-namespace XericLibrary.Runtime.Blueprint
-{
- ///
- /// QuickGraph 输入交互工具 —— 处理画布平移、缩放和节点选择。
- /// 中键拖拽 = 平移;左键点击 = 选择;滚轮 = 缩放。
- /// 快捷键(新输入系统):方向键 = 平移,Ctrl+± = 缩放,Ctrl+H = 归位。
- ///
- [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();
- 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;
- }
-
- /// 当前悬停的元素(可能为 null)
- public BlueprintElementBase HoveredElement => _hoveredElement;
-
- /// 当前选中的元素(可能为 null)
- 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 计算)=====
- }
-}
diff --git a/Runtime/Blueprint/QuickGraph.meta b/Runtime/Blueprint/QuickGraph.meta
new file mode 100644
index 0000000..40e3576
--- /dev/null
+++ b/Runtime/Blueprint/QuickGraph.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: f64c16d8bb22cf547992fd67ff9866a6
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime/Blueprint/QuickGraph/Input.meta b/Runtime/Blueprint/QuickGraph/Input.meta
new file mode 100644
index 0000000..2b2f54f
--- /dev/null
+++ b/Runtime/Blueprint/QuickGraph/Input.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 47199cf2fee4eec499be81af4eacd640
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime/Blueprint/QuickGraph/Input/QuickGraphInputTool.cs b/Runtime/Blueprint/QuickGraph/Input/QuickGraphInputTool.cs
new file mode 100644
index 0000000..c17505e
--- /dev/null
+++ b/Runtime/Blueprint/QuickGraph/Input/QuickGraphInputTool.cs
@@ -0,0 +1,198 @@
+using UnityEngine;
+
+namespace XericLibrary.Runtime.Blueprint.QuickGraph
+{
+ ///
+ /// QuickGraph 输入交互工具 —— 处理画布平移、缩放。
+ /// 中键拖拽 = 平移;滚轮 = 缩放。
+ /// 视图状态读写委托给 ,
+ /// 输入参数从 读取。
+ ///
+ [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();
+ return _defaultConfig;
+ }
+ }
+ private static QuickGraphInputConfig _defaultConfig;
+
+ // ---------- 缓存的 ViewportController 引用 ----------
+ private BlueprintViewportController _viewport;
+ private BlueprintViewportController Viewport
+ {
+ get
+ {
+ if (_viewport == null && Graph != null)
+ _viewport = BlueprintToolProvider.GetToolByType(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;
+ }
+
+ /// 当前悬停的元素(可能为 null)
+ public BlueprintElementBase HoveredElement => _hoveredElement;
+
+ /// 当前选中的元素(委托给 SelectorHandle)。
+ 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);
+ }
+ }
+}
diff --git a/Runtime/Blueprint/Input/QuickGraphInputTool.cs.meta b/Runtime/Blueprint/QuickGraph/Input/QuickGraphInputTool.cs.meta
similarity index 83%
rename from Runtime/Blueprint/Input/QuickGraphInputTool.cs.meta
rename to Runtime/Blueprint/QuickGraph/Input/QuickGraphInputTool.cs.meta
index 5376fd1..829af96 100644
--- a/Runtime/Blueprint/Input/QuickGraphInputTool.cs.meta
+++ b/Runtime/Blueprint/QuickGraph/Input/QuickGraphInputTool.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: 1fe1086d7e9750541a9a5de047e05ce6
+guid: 8b96bea36422c0146b9f3365107b8f7b
MonoImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/Runtime/Blueprint/QuickGraph/Render.meta b/Runtime/Blueprint/QuickGraph/Render.meta
new file mode 100644
index 0000000..e77abd6
--- /dev/null
+++ b/Runtime/Blueprint/QuickGraph/Render.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 6d2d1e11f02798943a73c491948cd025
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime/Blueprint/Render/Config.meta b/Runtime/Blueprint/QuickGraph/Render/Config.meta
similarity index 77%
rename from Runtime/Blueprint/Render/Config.meta
rename to Runtime/Blueprint/QuickGraph/Render/Config.meta
index 1dc2c07..3d5f2e1 100644
--- a/Runtime/Blueprint/Render/Config.meta
+++ b/Runtime/Blueprint/QuickGraph/Render/Config.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: c7cfe95b8a3355c4e8e6585d2266fb5b
+guid: 8aa6308c2cd1dd845a8b4810310dce87
folderAsset: yes
DefaultImporter:
externalObjects: {}
diff --git a/Runtime/Blueprint/Render/Config/QuickGraphGridBackgroundConfig.cs b/Runtime/Blueprint/QuickGraph/Render/Config/QuickGraphGridBackgroundConfig.cs
similarity index 85%
rename from Runtime/Blueprint/Render/Config/QuickGraphGridBackgroundConfig.cs
rename to Runtime/Blueprint/QuickGraph/Render/Config/QuickGraphGridBackgroundConfig.cs
index 73b6f99..56dc50e 100644
--- a/Runtime/Blueprint/Render/Config/QuickGraphGridBackgroundConfig.cs
+++ b/Runtime/Blueprint/QuickGraph/Render/Config/QuickGraphGridBackgroundConfig.cs
@@ -1,7 +1,7 @@
using System;
using UnityEngine;
-namespace XericLibrary.Runtime.Blueprint.Render
+namespace XericLibrary.Runtime.Blueprint.QuickGraph
{
///
/// 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);
}
diff --git a/Runtime/Blueprint/Render/Config/QuickGraphGridBackgroundConfig.cs.meta b/Runtime/Blueprint/QuickGraph/Render/Config/QuickGraphGridBackgroundConfig.cs.meta
similarity index 83%
rename from Runtime/Blueprint/Render/Config/QuickGraphGridBackgroundConfig.cs.meta
rename to Runtime/Blueprint/QuickGraph/Render/Config/QuickGraphGridBackgroundConfig.cs.meta
index 39beb13..0a092d7 100644
--- a/Runtime/Blueprint/Render/Config/QuickGraphGridBackgroundConfig.cs.meta
+++ b/Runtime/Blueprint/QuickGraph/Render/Config/QuickGraphGridBackgroundConfig.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: 32753aff001677a4b81e10dfae39b4cb
+guid: 38115e5721db6514bb8b8be6be10f26a
MonoImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/Runtime/Blueprint/Render/Config/QuickGraphInputConfig.cs b/Runtime/Blueprint/QuickGraph/Render/Config/QuickGraphInputConfig.cs
similarity index 73%
rename from Runtime/Blueprint/Render/Config/QuickGraphInputConfig.cs
rename to Runtime/Blueprint/QuickGraph/Render/Config/QuickGraphInputConfig.cs
index 40630d1..39207cc 100644
--- a/Runtime/Blueprint/Render/Config/QuickGraphInputConfig.cs
+++ b/Runtime/Blueprint/QuickGraph/Render/Config/QuickGraphInputConfig.cs
@@ -1,10 +1,10 @@
using System;
using UnityEngine;
-namespace XericLibrary.Runtime.Blueprint.Render
+namespace XericLibrary.Runtime.Blueprint.QuickGraph
{
///
- /// QuickGraph 输入工具配置表 —— 控制平移/缩放插值速率、灵敏度等可调参数。
+ /// QuickGraph 输入工具配置表 —— 控制平移/缩放灵敏度等可调参数。
/// 右键 Project 窗口 → Create/Blueprint/QuickGraph/Input Config 创建资产。
/// 拖入蓝图组件的 ToolConfigAssets 列表即可覆盖默认值。
///
@@ -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;
}
}
diff --git a/Runtime/Blueprint/Render/Config/QuickGraphInputConfig.cs.meta b/Runtime/Blueprint/QuickGraph/Render/Config/QuickGraphInputConfig.cs.meta
similarity index 83%
rename from Runtime/Blueprint/Render/Config/QuickGraphInputConfig.cs.meta
rename to Runtime/Blueprint/QuickGraph/Render/Config/QuickGraphInputConfig.cs.meta
index b8e79bd..8f158ed 100644
--- a/Runtime/Blueprint/Render/Config/QuickGraphInputConfig.cs.meta
+++ b/Runtime/Blueprint/QuickGraph/Render/Config/QuickGraphInputConfig.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: cfce52570fac4f5488becd85ce4f023b
+guid: 4735b93342f4fde4a8ed46dd28b46d4c
MonoImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/Runtime/Blueprint/Render/Config/QuickGraphNodeRenderConfig.cs b/Runtime/Blueprint/QuickGraph/Render/Config/QuickGraphNodeRenderConfig.cs
similarity index 76%
rename from Runtime/Blueprint/Render/Config/QuickGraphNodeRenderConfig.cs
rename to Runtime/Blueprint/QuickGraph/Render/Config/QuickGraphNodeRenderConfig.cs
index 167cea9..1377531 100644
--- a/Runtime/Blueprint/Render/Config/QuickGraphNodeRenderConfig.cs
+++ b/Runtime/Blueprint/QuickGraph/Render/Config/QuickGraphNodeRenderConfig.cs
@@ -1,7 +1,7 @@
using System;
using UnityEngine;
-namespace XericLibrary.Runtime.Blueprint.Render
+namespace XericLibrary.Runtime.Blueprint.QuickGraph
{
///
/// 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;
}
}
diff --git a/Runtime/Blueprint/Render/Config/QuickGraphNodeRenderConfig.cs.meta b/Runtime/Blueprint/QuickGraph/Render/Config/QuickGraphNodeRenderConfig.cs.meta
similarity index 83%
rename from Runtime/Blueprint/Render/Config/QuickGraphNodeRenderConfig.cs.meta
rename to Runtime/Blueprint/QuickGraph/Render/Config/QuickGraphNodeRenderConfig.cs.meta
index 43add16..3ab0745 100644
--- a/Runtime/Blueprint/Render/Config/QuickGraphNodeRenderConfig.cs.meta
+++ b/Runtime/Blueprint/QuickGraph/Render/Config/QuickGraphNodeRenderConfig.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: 8ab3d3829c73815468ed8c5ae306a6ee
+guid: 8038bf45323375b46a3a2820ddfe212e
MonoImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/Runtime/Blueprint/Render/Config/QuickGraphWireRenderConfig.cs b/Runtime/Blueprint/QuickGraph/Render/Config/QuickGraphWireRenderConfig.cs
similarity index 90%
rename from Runtime/Blueprint/Render/Config/QuickGraphWireRenderConfig.cs
rename to Runtime/Blueprint/QuickGraph/Render/Config/QuickGraphWireRenderConfig.cs
index e6c9e1a..244c0a4 100644
--- a/Runtime/Blueprint/Render/Config/QuickGraphWireRenderConfig.cs
+++ b/Runtime/Blueprint/QuickGraph/Render/Config/QuickGraphWireRenderConfig.cs
@@ -2,7 +2,7 @@ using System;
using UnityEngine;
using XericLibrary.Runtime.UIGraph;
-namespace XericLibrary.Runtime.Blueprint.Render
+namespace XericLibrary.Runtime.Blueprint.QuickGraph
{
///
/// 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)]
diff --git a/Runtime/Blueprint/Render/Config/QuickGraphWireRenderConfig.cs.meta b/Runtime/Blueprint/QuickGraph/Render/Config/QuickGraphWireRenderConfig.cs.meta
similarity index 83%
rename from Runtime/Blueprint/Render/Config/QuickGraphWireRenderConfig.cs.meta
rename to Runtime/Blueprint/QuickGraph/Render/Config/QuickGraphWireRenderConfig.cs.meta
index d0b4e5e..de74c64 100644
--- a/Runtime/Blueprint/Render/Config/QuickGraphWireRenderConfig.cs.meta
+++ b/Runtime/Blueprint/QuickGraph/Render/Config/QuickGraphWireRenderConfig.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: 21208cffd6108e04d815bb385f0af3fc
+guid: c95f6e1f1ece40444839f0a29ac371e5
MonoImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/Runtime/Blueprint/QuickGraph/Render/NodeCanvasRenderTool.cs b/Runtime/Blueprint/QuickGraph/Render/NodeCanvasRenderTool.cs
new file mode 100644
index 0000000..4c972f2
--- /dev/null
+++ b/Runtime/Blueprint/QuickGraph/Render/NodeCanvasRenderTool.cs
@@ -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
+ {
+ private QuickGraphNodeRenderConfig _defaultConfig;
+ private QuickGraphRenderStyleResolver _styleResolver;
+ private readonly Dictionary _nodeTexts = new Dictionary();
+ private readonly HashSet _staleTextCandidates = new HashSet();
+ private QuickGraphNodeRenderConfig Config
+ {
+ get
+ {
+ if (ToolConfig is QuickGraphNodeRenderConfig external) return external;
+ if (_defaultConfig == null) _defaultConfig = ScriptableObject.CreateInstance();
+ 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();
+ 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();
+ 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(); 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(); 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(); }
+ }
+}
diff --git a/Runtime/Blueprint/Render/NodeCanvasRenderTool.cs.meta b/Runtime/Blueprint/QuickGraph/Render/NodeCanvasRenderTool.cs.meta
similarity index 83%
rename from Runtime/Blueprint/Render/NodeCanvasRenderTool.cs.meta
rename to Runtime/Blueprint/QuickGraph/Render/NodeCanvasRenderTool.cs.meta
index 8f9e4f3..40aaac2 100644
--- a/Runtime/Blueprint/Render/NodeCanvasRenderTool.cs.meta
+++ b/Runtime/Blueprint/QuickGraph/Render/NodeCanvasRenderTool.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: 86aab959d7a9d7845b904f91e4533ba1
+guid: 76eb4633e3b7b1b47ae35a2a4435663c
MonoImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/Runtime/Blueprint/QuickGraph/Render/QuickGraphRenderStyleResolver.cs b/Runtime/Blueprint/QuickGraph/Render/QuickGraphRenderStyleResolver.cs
new file mode 100644
index 0000000..5ee2912
--- /dev/null
+++ b/Runtime/Blueprint/QuickGraph/Render/QuickGraphRenderStyleResolver.cs
@@ -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; }
+
+ /// 每次样式变更只解析一次;渲染热路径只消费强类型快照。
+ public sealed class QuickGraphRenderStyleResolver : IDisposable
+ {
+ private readonly BlueprintGraph _graph;
+ private readonly Func _nodeFallback;
+ private readonly Func _wireFallback;
+ private readonly Func _gridFallback;
+ public QuickGraphRenderStyleSnapshot Snapshot { get; private set; }
+ public event Action Changed;
+
+ public QuickGraphRenderStyleResolver(BlueprintGraph graph, Func nodeFallback = null, Func wireFallback = null, Func 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(string path, T fallback) { var value = _graph.GetRenderStyle(path); return value == null ? fallback : value.GetValueAs(); }
+ 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; }
+ }
+}
diff --git a/Runtime/Blueprint/QuickGraph/Render/QuickGraphRenderStyleResolver.cs.meta b/Runtime/Blueprint/QuickGraph/Render/QuickGraphRenderStyleResolver.cs.meta
new file mode 100644
index 0000000..3f3f9aa
--- /dev/null
+++ b/Runtime/Blueprint/QuickGraph/Render/QuickGraphRenderStyleResolver.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a2abe7122e63ad946ba485026296788a
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime/Blueprint/QuickGraph/Render/QuickUGUIGraphGridBackgroundTool.cs b/Runtime/Blueprint/QuickGraph/Render/QuickUGUIGraphGridBackgroundTool.cs
new file mode 100644
index 0000000..9abbf1b
--- /dev/null
+++ b/Runtime/Blueprint/QuickGraph/Render/QuickUGUIGraphGridBackgroundTool.cs
@@ -0,0 +1,113 @@
+using UnityEngine;
+using UnityEngine.UI;
+
+namespace XericLibrary.Runtime.Blueprint.QuickGraph
+{
+ /// QuickGraph 网格背景;样式资源材质只绑定不拥有,运行时创建材质才由本工具释放。
+ [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();
+ 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();
+ 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(); rect.anchorMin = Vector2.zero; rect.anchorMax = Vector2.one; rect.offsetMin = Vector2.zero; rect.offsetMax = Vector2.zero;
+ _backgroundImage = go.AddComponent(); _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;
+ }
+ }
+}
diff --git a/Runtime/Blueprint/Render/QuickUGUIGraphGridBackgroundTool.cs.meta b/Runtime/Blueprint/QuickGraph/Render/QuickUGUIGraphGridBackgroundTool.cs.meta
similarity index 83%
rename from Runtime/Blueprint/Render/QuickUGUIGraphGridBackgroundTool.cs.meta
rename to Runtime/Blueprint/QuickGraph/Render/QuickUGUIGraphGridBackgroundTool.cs.meta
index 27f844a..e8ab3dd 100644
--- a/Runtime/Blueprint/Render/QuickUGUIGraphGridBackgroundTool.cs.meta
+++ b/Runtime/Blueprint/QuickGraph/Render/QuickUGUIGraphGridBackgroundTool.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: da293e1f23cfca24b9ca4d40f91d2d8c
+guid: 1ba1fe1f333ee914b962da638a630281
MonoImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/Runtime/Blueprint/QuickGraph/Render/WireCanvasRenderTool.cs b/Runtime/Blueprint/QuickGraph/Render/WireCanvasRenderTool.cs
new file mode 100644
index 0000000..783c7a3
--- /dev/null
+++ b/Runtime/Blueprint/QuickGraph/Render/WireCanvasRenderTool.cs
@@ -0,0 +1,82 @@
+using UnityEngine;
+using XericLibrary.Runtime.Blueprint.Render;
+using XericLibrary.Runtime.UIGraph;
+
+namespace XericLibrary.Runtime.Blueprint.QuickGraph
+{
+ /// 按源节点 Chunk 提交连线的渲染工具;所有局部坐标由基类 submission 提供。
+ [BlueprintTool(phase: ToolPhase.Render, order: 110)]
+ [BlueprintTheme("QuickGraph")]
+ public class WireCanvasRenderTool : BlueprintCanvasRenderTool
+ {
+ 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();
+ 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();
+ 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();
+ }
+ }
+}
diff --git a/Runtime/Blueprint/Render/WireCanvasRenderTool.cs.meta b/Runtime/Blueprint/QuickGraph/Render/WireCanvasRenderTool.cs.meta
similarity index 83%
rename from Runtime/Blueprint/Render/WireCanvasRenderTool.cs.meta
rename to Runtime/Blueprint/QuickGraph/Render/WireCanvasRenderTool.cs.meta
index 0c3a8ec..c070561 100644
--- a/Runtime/Blueprint/Render/WireCanvasRenderTool.cs.meta
+++ b/Runtime/Blueprint/QuickGraph/Render/WireCanvasRenderTool.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: 3881805202e8cd049aad2d55c9f595f1
+guid: e6d0420274cb4d9469b505ea464a1671
MonoImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/Runtime/Blueprint/Render/BlueprintCanvasRenderTool.cs b/Runtime/Blueprint/Render/BlueprintCanvasRenderTool.cs
index 4f50e8a..bfe44fd 100644
--- a/Runtime/Blueprint/Render/BlueprintCanvasRenderTool.cs
+++ b/Runtime/Blueprint/Render/BlueprintCanvasRenderTool.cs
@@ -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
{
- ///
- /// 蓝图画布渲染工具的抽象基类。
- ///
- /// 职责:
- /// 1. 管理 Viewport Transform — 平移/缩放改为驱动 Transform,不触发 Mesh 重建。
- /// 2. 管理网格区块(Chunk)— 按画布坐标分块,每块一个渲染器,避开 65535 顶点限制。
- /// 3. 数据分组 — 脏元素的坐标 ÷ chunkSize → Spiral2DEncode → 按 Chunk 写入渲染器。
- ///
- ///
- /// 派生类只需实现三个抽象方法:
- ///
- /// - — 在 Chunk GameObject 上创建渲染器组件
- /// - — 把元素数据写入该 Chunk 的渲染器
- /// - — 清空 Chunk 渲染器,准备下一帧重写
- ///
- ///
- ///
- /// Chunk 使用的渲染器组件类型
- public abstract class BlueprintCanvasRenderTool : BlueprintTool
- where TChildRenderer : MonoBehaviour
+ /// Chunk renderer 后端基类:仅消费 Graph.ChunkState 的提交,不计算归属或可见性。
+ public abstract class BlueprintCanvasRenderTool : BlueprintTool where TChildRenderer : MonoBehaviour
{
- // ====================================================================
- // 配置
- // ====================================================================
-
- /// 网格区块大小(画布空间单位)。一个 Chunk 覆盖 chunkSize × chunkSize 区域。
- protected int ChunkSize = 400;
-
- /// Chunk 池的最大缓存数量
protected int MaxPoolSize = 100;
-
- // ====================================================================
- // Chunk 数据
- // ====================================================================
-
- /// 活跃区块:Spiral2DEncode ID → 渲染器实例
- protected Dictionary ActiveChunks = new();
-
- /// 缓存池,减少创建/销毁
- protected Stack ChunkPool = new();
-
- /// Chunk 的父级 Transform(通常在 Viewport 下)
+ protected readonly Dictionary ActiveChunks = new Dictionary();
+ protected readonly Stack ChunkPool = new Stack();
protected Transform ChunksParent;
-
- // ====================================================================
- // Viewport 状态
- // ====================================================================
-
- /// 当前同步到的 Viewport Transform
protected RectTransform ViewportRect;
-
private Vector2 _prevPan;
private float _prevZoom;
- // ====================================================================
- // 抽象方法(派生类实现)
- // ====================================================================
-
- /// 在 Chunk GameObject 上创建渲染器组件并返回。
protected abstract TChildRenderer BuildChunkRenderer(GameObject chunkGo);
-
- /// 将元素数据写入指定 Chunk 的渲染器。
- protected abstract void AppendElementData(TChildRenderer renderer, IBlueprintElement element);
-
- /// 清空 Chunk 渲染器的数据,准备下一帧重写。
+ /// 当前模板方法提交;子类不得重新计算 chunk 本地偏移。
+ protected BlueprintElementRenderSubmission CurrentElementSubmission { get; private set; }
+ protected abstract void AppendElementData(TChildRenderer renderer, IBlueprintElement element, ulong chunkID);
protected abstract void ClearChunkData(TChildRenderer renderer);
-
- /// 触发 Chunk 渲染器的脏标记,使其重建 Mesh。
protected abstract void SetChunkDirty(TChildRenderer renderer);
+ protected abstract bool AcceptsElement(IBlueprintElement element);
+ /// 每个非退出 Chunk 提交的开始钩子。
+ protected virtual void BeginChunkSubmission(ulong chunkID, TChildRenderer renderer) { }
+ /// 每个非退出 Chunk 提交的结束钩子。
+ protected virtual void EndChunkSubmission(ulong chunkID, TChildRenderer renderer) { }
+ protected virtual void OnChunkRecycled(ulong chunkID, TChildRenderer renderer) { }
- /// 获取元素用于 Chunk 分组的坐标(通常是 Position 或中心点)。
- protected abstract Vector2 GetElementPosition(IBlueprintElement element);
-
- /// 检查元素是否需要被剔除(例如不在视口内)。
- 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
- // ====================================================================
-
- /// 获取或创建 Viewport Transform。
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();
- ChunksParent = existing;
- return;
+ var go = new GameObject("__Bp_Viewport", typeof(RectTransform)); go.hideFlags = HideFlags.HideAndDontSave;
+ ViewportRect = go.GetComponent(); 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();
- 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;
}
- /// 将画布的 Pan/Zoom 同步到 Viewport Transform。
protected virtual void SyncViewportTransform()
{
- if (ViewportRect == null) return;
-
- var pan = Graph.Canvas.PanOffset;
- var zoom = Graph.Canvas.ZoomLevel;
-
- if (_prevPan != pan)
- {
- ViewportRect.anchoredPosition = -pan;
- _prevPan = pan;
- }
-
- if (!Mathf.Approximately(_prevZoom, zoom))
- {
- ViewportRect.localScale = Vector3.one * zoom;
- _prevZoom = zoom;
- }
+ 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);
- /// 将画布空间坐标转为 Chunk 坐标。
- protected (int cx, int cy) PositionToCoord(Vector2 canvasPos)
+ /// 一次 chunk 提交的标准化坐标、LOD 与版本上下文。
+ 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);
}
- /// Chunk 坐标 → 螺旋曲线 ID。
- 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; }
+ /// 将提交元素相关的 Canvas 空间数据转换为当前 Chunk 局部空间。
+ public Vector2 ToChunkLocal(Vector2 canvasPosition) => canvasPosition - ChunkOrigin;
+ public Rect ToChunkLocal(Rect canvasBounds) => new Rect(canvasBounds.position - ChunkOrigin, canvasBounds.size);
}
-
- /// 螺旋曲线 ID → Chunk 坐标。
- protected void IDToCoord(ulong id, out int cx, out int cy)
+ protected TChildRenderer GetOrCreateChunk(ulong id)
{
- MacroCurveMapping.Spiral2DDecode(id, out cx, out cy);
- }
-
- /// 获取或创建坐标对应的 Chunk 渲染器。
- protected TChildRenderer GetOrCreateChunk(ulong chunkID)
- {
- if (ActiveChunks.TryGetValue(chunkID, out var renderer) && renderer != null)
- return renderer;
-
- IDToCoord(chunkID, out int cx, out int cy);
-
+ 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();
- rt.pivot = Vector2.zero; // 左下角为原点
- rt.anchorMin = Vector2.zero;
- rt.anchorMax = Vector2.zero; // 不拉伸,绝对定位
- rt.anchoredPosition = new Vector2(cx * ChunkSize, cy * ChunkSize);
- rt.sizeDelta = new Vector2(ChunkSize, ChunkSize);
-
- // 获取或创建渲染器
- renderer = go.GetComponent();
- if (renderer == null)
- renderer = BuildChunkRenderer(go);
-
- ActiveChunks[chunkID] = renderer;
- return renderer;
+ 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(); 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() ?? BuildChunkRenderer(go); ActiveChunks[id] = renderer; return renderer;
}
- /// 收集脏元素并按 Chunk 分组。
- protected virtual Dictionary> GroupDirtyElements()
+ private void RecycleChunk(ulong id)
{
- var groups = new Dictionary>();
- var dirtyList = Graph.DirtyFlags.FlushDirty();
-
- foreach (var element in dirtyList)
- {
- if (element == null) continue;
-
- // 剔除
- if (IsElementCulled(element)) continue;
-
- var pos = GetElementPosition(element);
- var (cx, cy) = PositionToCoord(pos);
- ulong id = CoordToID(cx, cy);
-
- if (!groups.TryGetValue(id, out var list))
- {
- list = new List();
- groups[id] = list;
- }
- list.Add(element);
- }
-
- return groups;
+ 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);
}
-
- /// 清理不再包含脏元素的 Chunk。
- protected virtual void CleanupInactiveChunks(Dictionary>.KeyCollection activeIDs)
- {
- var toRemove = new List();
- foreach (var kvp in ActiveChunks)
- {
- if (!System.Linq.Enumerable.Contains(activeIDs, kvp.Key))
- toRemove.Add(kvp.Key);
- }
-
- foreach (var id in toRemove)
- {
- if (!ActiveChunks.TryGetValue(id, out var renderer) || renderer == null)
- continue;
-
- // 入池或销毁
- if (ChunkPool.Count < MaxPoolSize)
- {
- renderer.gameObject.SetActive(false);
- ChunkPool.Push(renderer);
- }
- else
- {
- UnityEngine.Object.Destroy(renderer.gameObject);
- }
-
- ActiveChunks.Remove(id);
- }
- }
-
- /// 销毁所有 Chunk。
protected virtual void DestroyAllChunks()
{
- foreach (var kvp in ActiveChunks)
- {
- if (kvp.Value != null)
- UnityEngine.Object.Destroy(kvp.Value.gameObject);
- }
+ 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); }
}
}
-}
+}
\ No newline at end of file
diff --git a/Runtime/Blueprint/Render/NodeCanvasRenderTool.cs b/Runtime/Blueprint/Render/NodeCanvasRenderTool.cs
deleted file mode 100644
index ec328d8..0000000
--- a/Runtime/Blueprint/Render/NodeCanvasRenderTool.cs
+++ /dev/null
@@ -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
-{
- ///
- /// 节点渲染工具 — 按网格区块管理 BlueprintPrimitiveRenderer,支持 Viewport Transform。
- /// 每个 Chunk 一个 PrimitiveRenderer + 每个节点一个 TMP Text(挂在该 Chunk 下)。
- /// LOD 逻辑保留自 QuickUGUIGraphNodeRenderTool。
- ///
- [BlueprintTool(phase: ToolPhase.Render, order: 100)]
- [BlueprintTheme("QuickGraph")]
- public class NodeCanvasRenderTool : BlueprintCanvasRenderTool
- {
- private enum LodLevel { Minimal, Simplified, Full }
-
- private QuickGraphNodeRenderConfig _defaultConfig;
- private QuickGraphNodeRenderConfig Config
- {
- get
- {
- if (ToolConfig is QuickGraphNodeRenderConfig external) return external;
- if (_defaultConfig == null)
- _defaultConfig = ScriptableObject.CreateInstance();
- return _defaultConfig;
- }
- }
-
- // TMP 文本:节点 → (ChunkID, Text 组件)
- private Dictionary _nodeTexts
- = new Dictionary();
-
- // ====================================================================
- // 抽象实现
- // ====================================================================
-
- protected override BlueprintPrimitiveRenderer BuildChunkRenderer(GameObject chunkGo)
- {
- return chunkGo.AddComponent();
- }
-
- protected override void AppendElementData(BlueprintPrimitiveRenderer renderer, IBlueprintElement element)
- {
- if (!(element is BlueprintNode node)) return;
-
- var cfg = Config;
- float zoom = Graph.Canvas.ZoomLevel;
- LodLevel lod = GetLodLevel();
-
- // 端口位置始终用原始尺寸计算
- CalculatePortPositions(node, cfg.NodeWidth, cfg.NodeHeight);
-
- // 节点尺寸和位置(画布空间,不转换,由 Viewport Transform 统一变换)
- float nw = cfg.NodeWidth;
- float nh = cfg.NodeHeight;
- float border = (lod == LodLevel.Minimal) ? 0f : cfg.BorderThickness;
- Vector2 pos = node.Position;
-
- Color bgColor, borderColor;
- if (lod == LodLevel.Minimal)
- {
- bgColor = node.NodeColor;
- borderColor = node.NodeColor;
- }
- else
- {
- bgColor = node.NodeBGColor;
- borderColor = node.NodeColor;
- }
-
- var primEntry = new PrimitiveCacheEntry
- {
- type = PrimitiveType.Rectangle,
- sizeMode = SizeMode.InscribedEllipse,
- center = pos,
- size = new Vector2(nw, nh),
- angle = 0f,
- @params = new PrimitiveParams
- {
- axisScaleX = 1f,
- axisScaleY = 1f,
- sideCount = 4,
- chamferSize = cfg.ChamferSize,
- chamferSegments = cfg.ChamferSegments,
- bgColor = new Color32(0, 0, 0, 0),
- centerColor = bgColor,
- borderColor = borderColor,
- borderThickness = border,
- }
- };
- renderer.AddPrimitive(primEntry);
-
- // LOD Full 时显示文本
- if (lod == LodLevel.Full)
- UpdateNodeText(node, pos, nw, nh, border, cfg);
- else
- HideNodeText(node);
- }
-
- protected override void ClearChunkData(BlueprintPrimitiveRenderer renderer)
- {
- renderer.ClearAll();
- }
-
- protected override void SetChunkDirty(BlueprintPrimitiveRenderer renderer)
- {
- renderer.SetPrimitiveDirty(0);
- }
-
- protected override Vector2 GetElementPosition(IBlueprintElement element)
- {
- return element is BlueprintNode node ? node.Position : Vector2.zero;
- }
-
- protected override bool IsElementCulled(IBlueprintElement element)
- {
- if (!(element is BlueprintNode node)) return true;
- var viewport = Graph?.Canvas?.GetViewportRect() ?? new Rect();
- float margin = Config.NodeWidth + Config.NodeHeight;
- viewport.xMin -= margin;
- viewport.xMax += margin;
- viewport.yMin -= margin;
- viewport.yMax += margin;
- return !viewport.Contains(node.Position);
- }
-
- // ====================================================================
- // LOD
- // ====================================================================
-
- private LodLevel GetLodLevel()
- {
- var cfg = Config;
- float min = Graph.Canvas.MinZoom;
- float max = Graph.Canvas.MaxZoom;
- float range = max - min;
- float normalized = range > 0.001f ? (Graph.Canvas.ZoomLevel - min) / range : 1f;
-
- if (normalized < cfg.Lod0Threshold) return LodLevel.Minimal;
- if (normalized < cfg.Lod1Threshold) return LodLevel.Simplified;
- return LodLevel.Full;
- }
-
- // ====================================================================
- // 端口位置(和旧渲染工具一致)
- // ====================================================================
-
- private static void CalculatePortPositions(BlueprintNode node, float nw, float nh)
- {
- int inCount = node.InputPorts.Count;
- int outCount = node.OutputPorts.Count;
-
- for (int i = 0; i < inCount; i++)
- {
- float y = nh * (0.5f - (float)(i + 1) / (inCount + 1));
- node.InputPorts[i].RelativePosition = new Vector2(-nw * 0.5f, y);
- }
- for (int i = 0; i < outCount; i++)
- {
- float y = nh * (0.5f - (float)(i + 1) / (outCount + 1));
- node.OutputPorts[i].RelativePosition = new Vector2(nw * 0.5f, y);
- }
- }
-
- // ====================================================================
- // TMP 文本 — 挂到节点所属的 Chunk 下
- // ====================================================================
-
- private void UpdateNodeText(BlueprintNode node, Vector2 pos, float nw, float nh,
- float border, QuickGraphNodeRenderConfig cfg)
- {
- var (cx, cy) = PositionToCoord(pos);
- ulong chunkID = CoordToID(cx, cy);
-
- if (_nodeTexts.TryGetValue(node.ElementId, out var existing))
- {
- // 节点已换 Chunk → 清理旧文本
- if (existing.chunkID != chunkID)
- {
- Object.Destroy(existing.text.gameObject);
- _nodeTexts.Remove(node.ElementId);
- }
- else
- {
- // 复用现有文本
- var tmp = existing.text;
- tmp.text = node.NodeTitle;
- tmp.color = node.NodeTextColor;
- tmp.fontSize = cfg.TitleFontSize;
- if (cfg.TitleFont != null) tmp.font = cfg.TitleFont;
- var rt = tmp.rectTransform;
- rt.anchorMin = Vector2.zero;
- rt.anchorMax = Vector2.zero;
- rt.anchoredPosition = new Vector2(pos.x - cx * ChunkSize, pos.y - cy * ChunkSize);
- rt.sizeDelta = new Vector2(nw - border * 2f, nh - border * 2f);
- rt.SetAsLastSibling();
- tmp.gameObject.SetActive(true);
- return;
- }
- }
-
- // 创建新文本
- var chunkGo = GetOrCreateChunk(chunkID).gameObject;
- var textGo = new GameObject($"__Text_{node.ElementId}", typeof(RectTransform));
- textGo.hideFlags = HideFlags.HideAndDontSave;
- textGo.transform.SetParent(chunkGo.transform, false);
-
- var tmp2 = textGo.AddComponent();
- tmp2.alignment = TextAlignmentOptions.Center;
- tmp2.fontSize = cfg.TitleFontSize;
- tmp2.enableAutoSizing = false;
- tmp2.text = node.NodeTitle;
- tmp2.color = node.NodeTextColor;
- if (cfg.TitleFont != null) tmp2.font = cfg.TitleFont;
-
- var rt2 = tmp2.rectTransform;
- rt2.pivot = new Vector2(0.5f, 0.5f);
- rt2.anchorMin = Vector2.zero; // 相对 Chunk 左下角
- rt2.anchorMax = Vector2.zero;
- // Chunk 原点 = 左下角(cx*size, cy*size),文本相对 Chunk 原点偏移
- rt2.anchoredPosition = new Vector2(pos.x - cx * ChunkSize, pos.y - cy * ChunkSize);
- rt2.sizeDelta = new Vector2(nw - border * 2f, nh - border * 2f);
- rt2.SetAsLastSibling();
-
- _nodeTexts[node.ElementId] = (chunkID, tmp2);
- }
-
- private void HideNodeText(BlueprintNode node)
- {
- if (_nodeTexts.TryGetValue(node.ElementId, out var existing))
- {
- existing.text.gameObject.SetActive(false);
- }
- }
-
- // ====================================================================
- // 清理(重写基类,额外清理 TMP 文本)
- // ====================================================================
-
- protected override void CleanupInactiveChunks(
- Dictionary>.KeyCollection activeIDs)
- {
- // 清理空 Chunk 中的文本
- var deadNodes = new List();
- foreach (var kvp in _nodeTexts)
- {
- ulong chunkID = kvp.Value.chunkID;
- if (!System.Linq.Enumerable.Contains(activeIDs, chunkID))
- deadNodes.Add(kvp.Key);
- }
- foreach (var id in deadNodes)
- {
- if (_nodeTexts.TryGetValue(id, out var entry))
- {
- Object.Destroy(entry.text.gameObject);
- _nodeTexts.Remove(id);
- }
- }
-
- base.CleanupInactiveChunks(activeIDs);
- }
-
- protected override void DestroyAllChunks()
- {
- foreach (var kvp in _nodeTexts)
- {
- if (kvp.Value.text != null)
- Object.Destroy(kvp.Value.text.gameObject);
- }
- _nodeTexts.Clear();
-
- base.DestroyAllChunks();
- }
- }
-}
diff --git a/Runtime/Blueprint/Render/QuickUGUIGraphGridBackgroundTool.cs b/Runtime/Blueprint/Render/QuickUGUIGraphGridBackgroundTool.cs
deleted file mode 100644
index 32d7ebb..0000000
--- a/Runtime/Blueprint/Render/QuickUGUIGraphGridBackgroundTool.cs
+++ /dev/null
@@ -1,162 +0,0 @@
-using UnityEngine;
-using UnityEngine.UI;
-
-namespace XericLibrary.Runtime.Blueprint.Render
-{
- ///
- /// 快速 UGUI 图论网格背景渲染工具。
- /// 在蓝图画布最底层创建一个 Image 组件绘制网格背景,
- /// 每帧将画布的 zoom / pan 同步到 Shader 的 _Transform 属性。
- ///
- /// 背景 Image 在 下锚点撑满,
- /// 只响应画布属性和 dirty flag,不会因画布重建而销毁(见 )。
- /// Shader 参数(_Transform 的 scale/offset)在 中随
- /// ZoomLevel / PanOffset 更新,与画布拖拽共享同一属性源。
- ///
- ///
- /// 材质引用注意:
- /// 必须缓存材质实例()并直接写入该实例。
- /// UGUI 的 Graphic.material getter 可能返回 materialForRendering
- /// 的副本(与 setter 存入的 m_Material 不同),因此 _backgroundImage.material
- /// 不适合用于写 shader 属性。见 Graphic.material Unity 源码。
- ///
- ///
- [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();
- return _defaultConfig;
- }
- }
-
- private Image _backgroundImage;
-
- ///
- /// 缓存的背景材质实例。
- /// 由 创建,终身保留。
- /// shader 属性始终写入此实例,避免 UGUI materialForRendering 副本问题。
- ///
- private Material _backgroundMaterial;
-
- public override void OnInitialize()
- {
- EnsureBackground();
- }
-
- public override void OnRender()
- {
- if (Graph == null) return;
- EnsureBackground();
- SyncMaterialProperties();
- }
-
- ///
- /// 在 RenderRoot 下查找或创建背景 Image。
- /// 创建后终身保留,不随画布重建销毁。
- ///
- 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();
- 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();
- rt.anchorMin = Vector2.zero;
- rt.anchorMax = Vector2.one;
- rt.offsetMin = Vector2.zero;
- rt.offsetMax = Vector2.zero;
-
- _backgroundImage = bgGo.AddComponent();
- _backgroundImage.raycastTarget = true;
- // 关闭 maskable:运行时背景 Image 不参与 UGUI Mask 裁剪,
- // 避免父级 Mask 组件触发 materialForRendering 克隆材质实例。
- // 材质写入(_Transform 等 shader 属性)直通 GPU,不受 IMaterialModifier 干扰。
- _backgroundImage.maskable = false;
-
- ApplyMaterial();
- }
-
- ///
- /// 从配置获取材质并缓存到 ,
- /// 然后赋值给 Image。
- /// 此方法仅执行一次,后续材质写入直接使用缓存的引用。
- ///
- private void ApplyMaterial()
- {
- if (_backgroundImage == null) return;
- if (_backgroundMaterial != null) return;
-
- _backgroundMaterial = Config.GetOrCreateMaterial();
- if (_backgroundMaterial != null)
- _backgroundImage.material = _backgroundMaterial;
- }
-
- ///
- /// 每帧将当前画布 zoom / pan 同步到 Shader,
- /// 使用 缓存写入,确保与 GPU 使用的材质实例一致。
- ///
- 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;
- }
- }
-}
diff --git a/Runtime/Blueprint/Render/WireCanvasRenderTool.cs b/Runtime/Blueprint/Render/WireCanvasRenderTool.cs
deleted file mode 100644
index 8800499..0000000
--- a/Runtime/Blueprint/Render/WireCanvasRenderTool.cs
+++ /dev/null
@@ -1,135 +0,0 @@
-using System.Collections.Generic;
-using UnityEngine;
-using XericLibrary.Runtime.UIGraph;
-
-namespace XericLibrary.Runtime.Blueprint.Render
-{
- ///
- /// 连线渲染工具 — 按网格区块管理 BlueprintCurveRenderer,支持 Viewport Transform。
- /// 连线跟随其源节点(SourcePort.OwnerNode)进入对应 Chunk 的 CurveRenderer。
- ///
- [BlueprintTool(phase: ToolPhase.Render, order: 110)]
- [BlueprintTheme("QuickGraph")]
- public class WireCanvasRenderTool : BlueprintCanvasRenderTool
- {
- private QuickGraphWireRenderConfig _defaultConfig;
- private QuickGraphWireRenderConfig Config
- {
- get
- {
- if (ToolConfig is QuickGraphWireRenderConfig external) return external;
- if (_defaultConfig == null)
- _defaultConfig = ScriptableObject.CreateInstance();
- return _defaultConfig;
- }
- }
-
- // ====================================================================
- // 抽象实现
- // ====================================================================
-
- protected override BlueprintCurveRenderer BuildChunkRenderer(GameObject chunkGo)
- {
- return chunkGo.AddComponent();
- }
-
- protected override void AppendElementData(BlueprintCurveRenderer renderer, IBlueprintElement element)
- {
- if (!(element is BlueprintWire wire)) return;
- if (wire.SourcePort == null || wire.TargetPort == null) return;
- if (wire.SourcePort.OwnerNode == null || wire.TargetPort.OwnerNode == null) return;
-
- var cfg = Config;
-
- // 端点:画布空间坐标(由 Viewport Transform 统一变换)
- Vector2 startPos = wire.SourcePort.GetWorldPosition();
- Vector2 endPos = wire.TargetPort.GetWorldPosition();
-
- // 手柄方向 = 端口法向
- float srcDirX = Mathf.Sign(wire.SourcePort.RelativePosition.x);
- float tgtDirX = Mathf.Sign(wire.TargetPort.RelativePosition.x);
- if (srcDirX == tgtDirX) srcDirX *= -1f;
-
- // 手柄长度(原始值,由 Viewport 统一缩放)
- Vector2 handle1 = startPos + new Vector2(srcDirX * cfg.SourceHandleLength, 0f);
- Vector2 handle2 = endPos + new Vector2(tgtDirX * cfg.TargetHandleLength, 0f);
-
- float ww = cfg.WireWidth;
-
- var ctrlPts = new Vector3[]
- {
- new Vector3(startPos.x, startPos.y, ww),
- new Vector3(handle1.x, handle1.y, ww),
- new Vector3(handle2.x, handle2.y, ww),
- new Vector3(endPos.x, endPos.y, ww),
- };
-
- Color32 srcColor = wire.SourcePort.OwnerNode.NodeColor;
- Color32 tgtColor = wire.TargetPort.OwnerNode.NodeColor;
-
- var arrow = new ArrowHeadData
- {
- shape = cfg.ArrowShape,
- reversed = cfg.ArrowReversed,
- progress = cfg.ArrowProgress,
- width = cfg.ArrowWidth,
- height = cfg.ArrowHeight,
- depthCompensation = cfg.ArrowDepthCompensation,
- color = tgtColor,
- };
-
- int count = ctrlPts.Length;
- var ctrlPts2 = new Vector2[count];
- var widths = new float[count];
- for (int i = 0; i < count; i++)
- {
- ctrlPts2[i] = ctrlPts[i];
- widths[i] = ctrlPts[i].z;
- }
-
- var curveEntry = new CurveCacheEntry
- {
- startColor = srcColor,
- endColor = tgtColor,
- tessellationSegments = cfg.TessellationSegments,
- };
-
- renderer.AddCurve(curveEntry, ctrlPts2, widths, new[] { arrow });
- }
-
- protected override void ClearChunkData(BlueprintCurveRenderer renderer)
- {
- renderer.ClearAll();
- }
-
- protected override void SetChunkDirty(BlueprintCurveRenderer renderer)
- {
- renderer.RebuildAll();
- }
-
- protected override Vector2 GetElementPosition(IBlueprintElement element)
- {
- if (!(element is BlueprintWire wire)) return Vector2.zero;
- // 以源节点的位置作为 Chunk 分组依据
- return wire.SourcePort?.OwnerNode?.Position ?? Vector2.zero;
- }
-
- protected override bool IsElementCulled(IBlueprintElement element)
- {
- if (!(element is BlueprintWire wire)) return true;
- if (wire.SourcePort?.OwnerNode == null || wire.TargetPort?.OwnerNode == null)
- return true;
-
- var viewport = Graph?.Canvas?.GetViewportRect() ?? new Rect();
- float margin = Config.WireWidth * 20f;
- viewport.xMin -= margin;
- viewport.xMax += margin;
- viewport.yMin -= margin;
- viewport.yMax += margin;
-
- // 两端都不在视口内则跳过
- return !viewport.Contains(wire.SourcePort.OwnerNode.Position)
- && !viewport.Contains(wire.TargetPort.OwnerNode.Position);
- }
- }
-}
diff --git a/Runtime/XericLibrary.dll b/Runtime/XericLibrary.dll
index a395d27a51cab120c0e905488ac979a903b186ce..6f19c87501dee9d08338c54aa906711c06fe8b34 100644
GIT binary patch
literal 1624576
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