3 Commits
Author SHA1 Message Date
lrss3 d9f9b2a90f update 0.6.4.1 2026-08-13 15:40:08 +08:00
lrss3 315e2eaa46 update 0.6.4 2026-08-10 14:17:25 +08:00
lrss3 2d703efdf4 update 0.6.3.8 2026-07-31 14:39:11 +08:00
37 changed files with 636 additions and 1286 deletions
+10
View File
@@ -2,10 +2,20 @@
## [Unrealse]
* 添加win10+右下角消息框
* 修复文件对话框中筛选器格式
## [0.6.4] 2026-08-10
* 将XericLibrary提供的数据类型命名空间移动到Runtime下。这可能会带来一些兼容性问题,但现在Type成员太多了,再不调整这个遗留问题就不好解决了。
* 添加图表数据结构
* 添加windows7+的应用程序图表进度条功能。
* 恢复引导接口的使用。
* 修复excel在一些环境缺失的电脑上会因为zip调用437报错的问题(旧unity还是需要自己手动解决)。
部分提交来自尚未合并到主分支的提交(此次提交来自 work2 -> Blueprint5
* 重写蓝图嵌入样式系统的结构
* 重写样式表系统结构,旧样式表结构可能存在部分不兼容的情况,但是提供了可以扩展的解析器用法。
# [0.6.3] 2026-07-16
Binary file not shown.
@@ -32,6 +32,9 @@ Material:
- _GridExp: 0.001
- _GridLineThreshold: 0.99
- _GridOverlayPower: 5
- _LineShapeScale: 1.1
- _LodTransitionEnd: 1
- _LodTransitionStart: -0.8
- _LodTransthd: 200
- _OverlayerMult: 1
- _Stencil: 0
@@ -41,9 +44,10 @@ Material:
- _StencilWriteMask: 255
- _UseUIAlphaClip: 0
m_Colors:
- _CellCenter: {r: 0.5, g: 0.5, b: 0, a: 0}
- _Color: {r: 1, g: 1, b: 1, a: 1}
- _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}
- _GridColor: {r: 0.8666667, g: 0.8862745, b: 0.93333334, a: 1}
- _Transform: {r: 9.787723, g: 8.369877, b: -4.381715, a: -3.846801}
- _Vector0: {r: 0.95, g: 1, b: 0, a: 0}
m_BuildTextureStacks: []
@@ -1,59 +1,24 @@
// ==================================================
// Xeric Style Sheet (XSSS) - 超级干燥样式表 - 默认样式
// Xeric Style Sheet (XSSS) - 默认 UI 样式
// ==================================================
// 规范声明:路径:组件类型/.字段路径 = 值:值类型 @标记:参数
// 未配置实际资源时,不保留空 guid: 或 None 占位声明。
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)
}
// UnityEngine.UI.Button 的 ColorBlock
Button:UnityEngine.UI.Button/.colors.normalColor = #FFFFFFFF:UnityEngine.Color
Button:UnityEngine.UI.Button/.colors.highlightedColor = #F5F5F5FF:UnityEngine.Color
Button:UnityEngine.UI.Button/.colors.pressedColor = #C7C7C7FF:UnityEngine.Color
Button:UnityEngine.UI.Button/.colors.selectedColor = #F5F5F5FF:UnityEngine.Color
Button:UnityEngine.UI.Button/.colors.disabledColor = #999999CC:UnityEngine.Color
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
}
// UnityEngine.UI.Toggle 的 ColorBlock。
Toggle:UnityEngine.UI.Toggle/.colors.normalColor = #FFFFFFFF:UnityEngine.Color
Toggle:UnityEngine.UI.Toggle/.colors.highlightedColor = #F5F5F5FF:UnityEngine.Color
Toggle:UnityEngine.UI.Toggle/.colors.pressedColor = #C7C7C7FF:UnityEngine.Color
Toggle:UnityEngine.UI.Toggle/.colors.selectedColor = #F5F5F5FF:UnityEngine.Color
Toggle:UnityEngine.UI.Toggle/.colors.disabledColor = #999999CC:UnityEngine.Color
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
}
// UnityEngine.UI.Image 的默认色。
Image:UnityEngine.UI.Image/.color = #FFFFFFFF:UnityEngine.Color
@@ -1,22 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
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.99
GridExp: 0.001
GridColor: {r: 1, g: 1, b: 1, a: 1}
GridBackgroundColor: {r: 0, g: 0, b: 0, a: 1}
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 092ad6102a8bc354bb081e3ee26e5ed6
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -1,20 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4735b93342f4fde4a8ed46dd28b46d4c, type: 3}
m_Name: QuickGraphInputConfig
m_EditorClassIdentifier:
ZoomDivisor: 30
DragPanSpeed: 1
MinZoom: 0.1
MaxZoom: 3
KeyboardPanSpeed: 600
KeyboardZoomSpeed: 0.05
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 3f441c61e0e252146a114c471a77c507
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -1,25 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 8038bf45323375b46a3a2820ddfe212e, type: 3}
m_Name: QuickGraphNodeRenderConfig
m_EditorClassIdentifier:
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.3
Lod1Threshold: 0.7
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: cdebd6b7f25eb334093a428b6414b0ce
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -1,41 +1,102 @@
// 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
width = 130:float @MinMaxRange:0,4096
height = 80:float @MinMaxRange:0,4096
border = 3:float @MinMaxRange:0,1024
chamfer = 0.2:float @MinMaxRange:0,1
chamferSegments = 4:int @MinMaxRange:0,128,1
titleFontSize = 20:float @MinMaxRange:0,512
titleAlignment = Center:TMPro.TextAlignmentOptions
showTitle/full = true:bool
outerColor = #00000000:UnityEngine.Color
axisScaleX = 1:float @MinMaxRange:0,100
axisScaleY = 1:float @MinMaxRange:0,100
sideCount = 4:int @MinMaxRange:0,128,1
fillColor = #333333FF:UnityEngine.Color
borderColor = #FFFFFFFF:UnityEngine.Color
titleColor = #FFFFFFFF:UnityEngine.Color
}
quickgraph/port/ {
inputSide = -0.5:float @MinMaxRange:-1,1
outputSide = 0.5:float @MinMaxRange:-1,1
topRatio = 0.5:float @MinMaxRange:-1,1
bottomRatio = -0.5:float @MinMaxRange:-1,1
}
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
width = 5:float @MinMaxRange:0,4096
hitRadius = 8:float @MinMaxRange:0,4096
sourceHandle = 80:float @MinMaxRange:0,4096
targetHandle = 80:float @MinMaxRange:0,4096
arrowWidth = 12:float @MinMaxRange:0,4096
arrowHeight = 10:float @MinMaxRange:0,4096
arrowProgress = 1:float @MinMaxRange:0,1
arrowDepth = -1:float @MinMaxRange:-4096,4096
tessellation = 32:int @MinMaxRange:0,1024,1
minTessellation = 2:int @MinMaxRange:0,1024,1
arrowShape = Triangle:XericLibrary.Runtime.UIGraph.ArrowShape
arrowReversed = false:bool
useNodeColorGradient = true:bool
color = #FFFFFFFF:UnityEngine.Color
}
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
size = 100:float @MinMaxRange:0.000001,1000000
overlayPower = 5:float @MinMaxRange:0,1000000
threshold = 0.99:float @MinMaxRange:0,1
exp = 0.001:float @MinMaxRange:0,1000000
color = #A7ABB3FF:UnityEngine.Color
background = #DDE2EEFF:UnityEngine.Color
raycastTarget = true:bool
maskable = false:bool
lodTransitionThreshold = 200:float @MinMaxRange:0,1000000
lodTransitionStart = -0.8:float @MinMaxRange:-100,100
lodTransitionEnd = 1:float @MinMaxRange:-100,100
cellCenter = (0.5,0.5):UnityEngine.Vector2
lineShapeScale = 1.1:float @MinMaxRange:0,100
}
quickgraph/input/ {
zoomDivisor = 100:float @MinMaxRange:0.000001,1000000
dragPanSpeed = 1:float @MinMaxRange:0,1000000
minZoom = 0.2:float @MinMaxRange:0,1000000
maxZoom = 2:float @MinMaxRange:0,1000000
defaultZoom = 1:float @MinMaxRange:0,1000000
focusZoom = 1:float @MinMaxRange:0,1000000
focusPadding = 100:float @MinMaxRange:0,1000000
keyboardPanSpeed = 100:float @MinMaxRange:0,1000000
keyboardZoomSpeed = 0.01:float @MinMaxRange:0,1000000
panMouseButton = 2:int @MinMaxRange:0,2,1
}
quickgraph/lod/minimal/ {
enterZoom = 0:float @MinMaxRange:0,1000000
exitZoom = 0:float @MinMaxRange:0,1000000
wireTessellationMultiplier = 0.35:float @MinMaxRange:0,100
nodeBorder = 0:float @MinMaxRange:0,1024
useNodeColorAsBackground = true:bool
showTitle = false:bool
}
quickgraph/lod/simplified/ {
enterZoom = 0.65:float @MinMaxRange:0,1000000
exitZoom = 0.55:float @MinMaxRange:0,1000000
wireTessellationMultiplier = 0.65:float @MinMaxRange:0,100
nodeBorder = 3:float @MinMaxRange:0,1024
useNodeColorAsBackground = false:bool
showTitle = false:bool
}
quickgraph/lod/full/ {
enterZoom = 1.15:float @MinMaxRange:0,1000000
exitZoom = 1:float @MinMaxRange:0,1000000
wireTessellationMultiplier = 1:float @MinMaxRange:0,100
nodeBorder = 3:float @MinMaxRange:0,1024
useNodeColorAsBackground = false:bool
showTitle = true:bool
}
quickgraph/selection/ {
dragThreshold = 10:float @MinMaxRange:0,1000000
fillColor = #3380FF26:UnityEngine.Color
outlineColor = #FFFFFFFF:UnityEngine.Color
outlineOffset = (1,-1):UnityEngine.Vector2
raycastTarget = false:bool
}
@@ -1,24 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c95f6e1f1ece40444839f0a29ac371e5, type: 3}
m_Name: QuickGraphWireRenderConfig
m_EditorClassIdentifier:
WireWidth: 5
SourceHandleLength: 80
TargetHandleLength: 80
ArrowShape: 0
ArrowReversed: 0
ArrowProgress: 1
ArrowWidth: 12
ArrowHeight: 10
ArrowDepthCompensation: -1
TessellationSegments: 32
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: aad533644ae5c8b4bb61786c413d4844
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -50,8 +50,10 @@ namespace XericLibrary.Runtime.Blueprint
#endif
_inputActions = value;
#if ENABLE_INPUT_SYSTEM
if (Graph != null && value is InputActionAsset newAsset)
if (Graph != null)
{
var newAsset = value as InputActionAsset;
EnsureEventSystem(newAsset);
var newTool = BlueprintToolProvider.GetToolByType<BlueprintInputTool_InputSystem>(Graph);
if (newTool != null)
newTool.InputActionAsset = newAsset;
@@ -162,14 +164,12 @@ namespace XericLibrary.Runtime.Blueprint
private void SetupInput()
{
#if ENABLE_INPUT_SYSTEM
if (_inputActions is InputActionAsset asset)
{
EnsureEventSystem(asset);
// 将 InputActionAsset 传递给输入工具(由工具自身管理绑定)
var tool = BlueprintToolProvider.GetToolByType<BlueprintInputTool_InputSystem>(Graph);
if (tool != null)
tool.InputActionAsset = asset;
}
var asset = _inputActions as InputActionAsset;
EnsureEventSystem(asset);
// 将 InputActionAsset 传递给输入工具(由工具自身管理绑定)。
var tool = BlueprintToolProvider.GetToolByType<BlueprintInputTool_InputSystem>(Graph);
if (tool != null)
tool.InputActionAsset = asset;
#endif
}
@@ -192,29 +192,64 @@ namespace XericLibrary.Runtime.Blueprint
}
#if ENABLE_INPUT_SYSTEM
if (actionsAsset != null)
var uiModule = eventSys.GetComponent<UnityEngine.InputSystem.UI.InputSystemUIInputModule>();
if (uiModule == null)
{
var uiModule = eventSys.GetComponent<UnityEngine.InputSystem.UI.InputSystemUIInputModule>();
if (uiModule == null)
{
var standalone = eventSys.GetComponent<StandaloneInputModule>();
if (standalone != null)
DestroyImmediate(standalone);
var standalone = eventSys.GetComponent<StandaloneInputModule>();
if (standalone != null)
DestroyImmediate(standalone);
uiModule = eventSys.gameObject.AddComponent<UnityEngine.InputSystem.UI.InputSystemUIInputModule>();
}
uiModule.actionsAsset = actionsAsset;
uiModule = eventSys.gameObject.AddComponent<UnityEngine.InputSystem.UI.InputSystemUIInputModule>();
}
if (actionsAsset != null)
ConfigureBlueprintUiActions(uiModule, actionsAsset);
else
#endif
{
if (eventSys.GetComponent<StandaloneInputModule>() == null)
{
eventSys.gameObject.AddComponent<StandaloneInputModule>();
}
// 未提供蓝图资产时使用 Input System 内置 UI 动作,仍可产生指针事件。
uiModule.actionsAsset = null;
uiModule.AssignDefaultActions();
}
#else
if (eventSys.GetComponent<StandaloneInputModule>() == null)
{
eventSys.gameObject.AddComponent<StandaloneInputModule>();
}
#endif
}
#if ENABLE_INPUT_SYSTEM
/// <summary>
/// 将 Blueprint map 的鼠标动作显式映射到 UI 模块。
/// InputSystemUIInputModule 不会仅凭 actionsAsset 自动推断这些引用。
/// </summary>
private static void ConfigureBlueprintUiActions(
UnityEngine.InputSystem.UI.InputSystemUIInputModule uiModule,
InputActionAsset actionsAsset)
{
var map = actionsAsset.FindActionMap(BlueprintInputConstants.MapName);
var point = map?.FindAction("Point");
var leftClick = map?.FindAction("LeftClick");
var rightClick = map?.FindAction("RightClick");
var middleClick = map?.FindAction("MiddleClick");
var scrollWheel = map?.FindAction("ScrollWheel");
if (point == null || leftClick == null || rightClick == null ||
middleClick == null || scrollWheel == null)
{
uiModule.actionsAsset = null;
uiModule.AssignDefaultActions();
return;
}
uiModule.actionsAsset = actionsAsset;
uiModule.point = InputActionReference.Create(point);
uiModule.leftClick = InputActionReference.Create(leftClick);
uiModule.rightClick = InputActionReference.Create(rightClick);
uiModule.middleClick = InputActionReference.Create(middleClick);
uiModule.scrollWheel = InputActionReference.Create(scrollWheel);
}
#endif
// ===== 调试可见性 =====
private void SyncDebugVisibility()
+14 -203
View File
@@ -1,213 +1,24 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using XericLibrary.Runtime.Blueprint.QuickGraph;
namespace XericLibrary.Runtime.Blueprint
{
/// <summary>
/// 蓝图框选工具 —— 鼠标左键在空白区域拖拽,绘制矩形选框,
/// 释放时计算框选范围内所有元素的交集并输出统计日志。
/// <para>选框 UGUI Image 默认渲染在所有层之上(SetAsLastSibling)。</para>
/// <para>框选结果通过 <see cref="BlueprintToolProvider.GetSelector"/>(Graph).<see cref="SelectorHandle.CurrentSelection"/> 读取。</para>
/// <para>
/// 选框仅在拖拽距离超过 <see cref="BoxSelectThreshold"/> 后才显示,
/// 避免误触显示闪烁。
/// </para>
/// </summary>
[BlueprintTool(phase: ToolPhase.PreUpdate, order: 60)]
public class BlueprintBoxSelectTool : BlueprintTool
{
// ---------- 状态 ----------
private bool _isBoxSelecting;
private Vector2 _dragStartCanvas; // 按下时的画布坐标
private Vector2 _dragEndCanvas; // 当前的画布坐标
/// <summary>选框 GameObject 是否已创建并可见(超过阈值后)</summary>
private bool _boxVisible;
// 选框 UI
private GameObject _selectionBoxGo;
private Image _selectionBoxImage;
private RectTransform _selectionBoxRt;
// ---------- 配置 ----------
/// <summary>框选拖拽触发阈值(画布单位)。拖拽距离超过此值后才显示选框并开始框选。</summary>
public float BoxSelectThreshold = 10f;
// ---------- 结果 ----------
/// <summary>结果:通过 Provider.Selector.CurrentSelection 读取框选结果。</summary>
// ===== 鼠标按下 =====
public override void OnPointerDown(GraphInputTool sender, Vector2 canvasPoint, int mouseButton)
{
if (Graph == null) return;
if (mouseButton != 0) return; // 仅左键
// 仅在空白区域开始框选(HitTest 无结果)
var hit = Graph.HitTest(canvasPoint);
if (hit != null) return;
_isBoxSelecting = true;
_boxVisible = false;
_dragStartCanvas = canvasPoint;
_dragEndCanvas = canvasPoint;
}
// ===== 拖拽 =====
public override void OnPointerDrag(GraphInputTool sender, Vector2 canvasPoint, Vector2 delta, int mouseButton)
{
if (!_isBoxSelecting || mouseButton != 0) return;
_dragEndCanvas = canvasPoint;
// 检查拖拽距离是否超过阈值
float threshold = BoxSelectThreshold;
float dragDist = Vector2.Distance(_dragStartCanvas, _dragEndCanvas);
if (!_boxVisible && dragDist > threshold)
{
// 首次超过阈值 → 创建选框
_boxVisible = true;
CreateSelectionBox();
}
if (_boxVisible)
{
UpdateSelectionBox();
}
}
// ===== 释放 =====
public override void OnPointerUp(GraphInputTool sender, Vector2 canvasPoint, int mouseButton)
{
if (!_isBoxSelecting || mouseButton != 0) return;
_dragEndCanvas = canvasPoint;
if (_boxVisible)
{
FinishSelection();
}
else
{
// 未超过阈值 → 点击行为(框选起点处的元素)
SelectAtPoint(_dragStartCanvas);
}
_isBoxSelecting = false;
}
// ===== 销毁 =====
public override void OnDestroy()
{
DestroySelectionBox();
}
// ===== 选框 UI =====
private void CreateSelectionBox()
{
if (Graph?.Canvas == null) return;
var root = Graph.Canvas.GetRootTransform();
if (root == null) return;
// 销毁已有的选框(应对重复点击)
DestroySelectionBox();
_selectionBoxGo = new GameObject("__Bp_BoxSelect", typeof(RectTransform), typeof(Image));
_selectionBoxGo.hideFlags = HideFlags.HideAndDontSave;
_selectionBoxRt = _selectionBoxGo.GetComponent<RectTransform>();
_selectionBoxRt.SetParent(root, false);
// 枢轴设为左下角;锚点居中对齐 RenderRoot 枢轴(与 CanvasToLocal 空间一致)
_selectionBoxRt.pivot = Vector2.zero;
_selectionBoxRt.anchorMin = new Vector2(0.5f, 0.5f);
_selectionBoxRt.anchorMax = new Vector2(0.5f, 0.5f);
_selectionBoxRt.SetAsLastSibling(); // 渲染在最顶层
_selectionBoxImage = _selectionBoxGo.GetComponent<Image>();
_selectionBoxImage.color = new Color(0.2f, 0.5f, 1.0f, 0.15f);
var outline = _selectionBoxGo.AddComponent<Outline>();
outline.effectColor = Color.white;
outline.effectDistance = new Vector2(1, -1);
}
private void UpdateSelectionBox()
{
if (_selectionBoxRt == null || Graph?.Canvas == null) return;
// 将画布坐标转为 RenderRoot 本地空间坐标(CanvasToLocal
var startLocal = Graph.Canvas.CanvasToLocal(_dragStartCanvas);
var endLocal = Graph.Canvas.CanvasToLocal(_dragEndCanvas);
// 计算 RectTransform 位置和尺寸(RenderRoot 本地空间)
var minX = Mathf.Min(startLocal.x, endLocal.x);
var minY = Mathf.Min(startLocal.y, endLocal.y);
var maxX = Mathf.Max(startLocal.x, endLocal.x);
var maxY = Mathf.Max(startLocal.y, endLocal.y);
_selectionBoxRt.anchoredPosition = new Vector2(minX, minY);
_selectionBoxRt.sizeDelta = new Vector2(maxX - minX, maxY - minY);
}
private void FinishSelection()
{
UpdateSelectionBox();
// 计算选框内的元素(画布坐标空间)
var selectList = new System.Collections.Generic.List<IBlueprintElement>();
var min = new Vector2(
Mathf.Min(_dragStartCanvas.x, _dragEndCanvas.x),
Mathf.Min(_dragStartCanvas.y, _dragEndCanvas.y));
var max = new Vector2(
Mathf.Max(_dragStartCanvas.x, _dragEndCanvas.x),
Mathf.Max(_dragStartCanvas.y, _dragEndCanvas.y));
var selectRect = new Rect(min, max - min);
var selector = BlueprintToolProvider.GetSelector(Graph);
selector.RectHitTest(Graph, selectRect, selectList);
// 通过当前 Graph 的 SelectorHandle 提交多选
selector.CommitMultiSelection(selectList);
// 销毁选框
DestroySelectionBox();
}
private void SelectAtPoint(Vector2 canvasPoint)
{
if (Graph == null) return;
var selector = BlueprintToolProvider.GetSelector(Graph);
var hit = selector.HitTest(Graph, canvasPoint);
if (hit != null)
selector.CommitSelection(hit);
else
selector.SwapToHistory();
}
private void DestroySelectionBox()
{
if (_selectionBoxGo != null)
{
#if UNITY_EDITOR
if (!Application.isPlaying)
Object.DestroyImmediate(_selectionBoxGo);
else
#endif
Object.Destroy(_selectionBoxGo);
_selectionBoxGo = null;
_selectionBoxRt = null;
_selectionBoxImage = null;
}
}
private bool _selecting, _visible; private Vector2 _start, _end; private GameObject _box; private Image _image; private RectTransform _rect; private Outline _outline; private QuickGraphRenderStyleResolver _styles;
public override void OnInitialize() { _styles = new QuickGraphRenderStyleResolver(Graph); _styles.Changed += _ => ApplyStyle(); }
public override void OnConfigChanged() => _styles?.Refresh();
public override void OnPointerDown(GraphInputTool sender, Vector2 point, int button) { if (button != 0 || Graph == null || Graph.HitTest(point) != null) return; _selecting = true; _visible = false; _start = _end = point; }
public override void OnPointerDrag(GraphInputTool sender, Vector2 point, Vector2 delta, int button) { if (!_selecting || button != 0) return; _end = point; if (!_visible && Vector2.Distance(_start, _end) > _styles.Snapshot.Selection.DragThreshold) { _visible = true; Create(); } if (_visible) UpdateBox(); }
public override void OnPointerUp(GraphInputTool sender, Vector2 point, int button) { if (!_selecting || button != 0) return; _end = point; if (_visible) Finish(); else SelectAt(_start); _selecting = false; }
private void Create() { if (Graph?.Canvas == null) return; var root = Graph.Canvas.GetRootTransform(); if (root == null) return; DestroyBox(); _box = new GameObject("__Bp_BoxSelect", typeof(RectTransform), typeof(Image), typeof(Outline)); _box.hideFlags = HideFlags.HideAndDontSave; _rect = _box.GetComponent<RectTransform>(); _rect.SetParent(root, false); _rect.pivot = Vector2.zero; _rect.anchorMin = _rect.anchorMax = new Vector2(.5f, .5f); _rect.SetAsLastSibling(); _image = _box.GetComponent<Image>(); _outline = _box.GetComponent<Outline>(); ApplyStyle(); }
private void ApplyStyle() { if (_image == null || _styles == null) return; var s = _styles.Snapshot.Selection; _image.color = s.FillColor; _image.raycastTarget = s.RaycastTarget; _outline.effectColor = s.OutlineColor; _outline.effectDistance = s.OutlineOffset; }
private void UpdateBox() { if (_rect == null || Graph?.Canvas == null) return; var a = Graph.Canvas.CanvasToLocal(_start); var b = Graph.Canvas.CanvasToLocal(_end); var min = Vector2.Min(a, b); _rect.anchoredPosition = min; _rect.sizeDelta = Vector2.Max(a, b) - min; }
private void Finish() { UpdateBox(); var min = Vector2.Min(_start, _end); var selector = BlueprintToolProvider.GetSelector(Graph); var result = new List<IBlueprintElement>(); selector.RectHitTest(Graph, new Rect(min, Vector2.Max(_start, _end) - min), result); selector.CommitMultiSelection(result); DestroyBox(); }
private void SelectAt(Vector2 point) { var selector = BlueprintToolProvider.GetSelector(Graph); var hit = selector.HitTest(Graph, point); if (hit != null) selector.CommitSelection(hit); else selector.SwapToHistory(); }
private void DestroyBox() { if (_box == null) return; if (Application.isPlaying) Object.Destroy(_box); else Object.DestroyImmediate(_box); _box = null; _image = null; _rect = null; _outline = null; }
public override void OnDestroy() { _styles?.Dispose(); _styles = null; DestroyBox(); }
}
}
@@ -107,8 +107,8 @@ namespace XericLibrary.Runtime.Blueprint
/// <summary>
/// 确保场景中存在 EventSystem 和合适的 InputModule。
/// 旧输入系统 → StandaloneInputModule。
/// 新输入系统 → InputSystemUIInputModule(由 GraphTheoryBlueprintComponent 创建)
/// 不会重复创建模块(通过 GetComponent 检查)
/// 新输入系统 → InputSystemUIInputModule;无蓝图资产配置时使用模块默认动作兜底
/// 不会覆盖 GraphTheoryBlueprintComponent 已配置的模块
/// </summary>
private static void EnsureEventSystemExists()
{
@@ -119,7 +119,17 @@ namespace XericLibrary.Runtime.Blueprint
es = go.AddComponent<EventSystem>();
}
#if !ENABLE_INPUT_SYSTEM
#if ENABLE_INPUT_SYSTEM
if (es.GetComponent<UnityEngine.InputSystem.UI.InputSystemUIInputModule>() == null)
{
var standalone = es.GetComponent<StandaloneInputModule>();
if (standalone != null)
Object.DestroyImmediate(standalone);
var uiModule = es.gameObject.AddComponent<UnityEngine.InputSystem.UI.InputSystemUIInputModule>();
uiModule.AssignDefaultActions();
}
#else
if (es.GetComponent<StandaloneInputModule>() == null)
{
es.gameObject.AddComponent<StandaloneInputModule>();
@@ -6,7 +6,7 @@ namespace XericLibrary.Runtime.Blueprint
/// 蓝图视口控制器 —— 管理视图状态(Pan/Zoom/范围),提供外部 API 供工具和外部代码读写。
/// <para>
/// 在 <c>ToolPhase.PreUpdate, order: 40</c> 执行,每帧直接将目标值写入 <see cref="IBlueprintCanvas"/>
/// 不做插值平滑。输入工具通过写入 <see cref="TargetPanOffset"/> / <see cref="TargetZoomLevel"/>
/// 不做插值平滑或消费端缩放钳制。输入工具通过写入 <see cref="TargetPanOffset"/> / <see cref="TargetZoomLevel"/>
/// 来驱动视图,外部代码可通过 <c>BlueprintToolProvider.GetTool&lt;BlueprintViewportController&gt;(graph)</c>
/// 获取实例直接控制视图。
/// </para>
@@ -23,13 +23,7 @@ namespace XericLibrary.Runtime.Blueprint
public Vector2 TargetPanOffset = Vector2.zero;
/// <summary>目标缩放级别。外部代码直接设置此值即可控制视图缩放。</summary>
public float TargetZoomLevel = 1f;
/// <summary>最小缩放限制。</summary>
public float MinZoom = 0.1f;
/// <summary>最大缩放限制。</summary>
public float MaxZoom = 3f;
public float TargetZoomLevel;
// ── 帧间脏检测 ──
@@ -43,22 +37,19 @@ namespace XericLibrary.Runtime.Blueprint
var canvas = Graph.Canvas;
// 首次同步
// 首次同步使用样式默认缩放。
if (!_initialized)
{
TargetPanOffset = canvas.PanOffset;
TargetZoomLevel = canvas.ZoomLevel;
TargetZoomLevel = Graph.GetRenderStyleValue<float>("/quickgraph/input/defaultZoom");
_prevPanOffset = TargetPanOffset;
_prevZoomLevel = TargetZoomLevel;
_initialized = true;
}
// 同步范围到 Canvas
canvas.MinZoom = MinZoom;
canvas.MaxZoom = MaxZoom;
// 钳制目标值
TargetZoomLevel = Mathf.Clamp(TargetZoomLevel, MinZoom, MaxZoom);
// 缩放范围由样式写入 Canvas,由 Canvas 负责约束。
canvas.MinZoom = Graph.GetRenderStyleValue<float>("/quickgraph/input/minZoom");
canvas.MaxZoom = Graph.GetRenderStyleValue<float>("/quickgraph/input/maxZoom");
// 直接写入 Canvas(无插值)
canvas.PanOffset = TargetPanOffset;
@@ -77,18 +68,18 @@ namespace XericLibrary.Runtime.Blueprint
// 公共 API
// ====================================================================
/// <summary>重置视图到原点,缩放归 1。</summary>
/// <summary>重置视图到原点和样式默认缩放。</summary>
public void ResetView()
{
TargetPanOffset = Vector2.zero;
TargetZoomLevel = 1f;
TargetZoomLevel = Graph.GetRenderStyleValue<float>("/quickgraph/input/defaultZoom");
Graph?.MarkDirty();
}
/// <summary>聚焦到某个画布坐标点。</summary>
public void FocusOn(Vector2 canvasCenter, float zoom = 1f)
/// <summary>聚焦到某个画布坐标点;省略缩放时使用样式聚焦缩放。</summary>
public void FocusOn(Vector2 canvasCenter, float? zoom = null)
{
TargetZoomLevel = Mathf.Clamp(zoom, MinZoom, MaxZoom);
TargetZoomLevel = zoom ?? Graph.GetRenderStyleValue<float>("/quickgraph/input/focusZoom");
if (Graph?.Canvas != null)
{
// local = canvas * zoom + pan;令目标画布点落在实际 viewport 的 RenderRoot local 中心。
@@ -98,12 +89,14 @@ namespace XericLibrary.Runtime.Blueprint
Graph?.MarkDirty();
}
/// <summary>聚焦到一组元素的包围盒区域。</summary>
public void FocusOnElements(System.Collections.Generic.IReadOnlyList<IBlueprintElement> elements, float padding = 100f)
/// <summary>聚焦到一组元素的包围盒区域,使用样式聚焦边距。</summary>
public void FocusOnElements(System.Collections.Generic.IReadOnlyList<IBlueprintElement> elements)
{
if (elements == null || elements.Count == 0) return;
if (Graph?.Canvas == null) return;
float padding = Graph.GetRenderStyleValue<float>("/quickgraph/input/focusPadding");
// 计算所有元素的包围盒
Rect bounds = elements[0].BoundingBox;
for (int i = 1; i < elements.Count; i++)
@@ -122,10 +115,9 @@ namespace XericLibrary.Runtime.Blueprint
// 以实际 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);
float zoomX = bounds.width > 0.0001f ? viewportLocal.width / bounds.width : Graph.GetRenderStyleValue<float>("/quickgraph/input/maxZoom");
float zoomY = bounds.height > 0.0001f ? viewportLocal.height / bounds.height : Graph.GetRenderStyleValue<float>("/quickgraph/input/maxZoom");
FocusOn(bounds.center, Mathf.Min(zoomX, zoomY));
}
}
}
@@ -1,198 +1,19 @@
using UnityEngine;
namespace XericLibrary.Runtime.Blueprint.QuickGraph
{
/// <summary>
/// QuickGraph 输入交互工具 —— 处理画布平移、缩放。
/// <para>中键拖拽 = 平移;滚轮 = 缩放。</para>
/// <para>视图状态读写委托给 <see cref="BlueprintViewportController"/>
/// 输入参数从 <see cref="QuickGraphInputConfig"/> 读取。</para>
/// </summary>
[BlueprintTool(phase: ToolPhase.PreUpdate, order: 50)]
[BlueprintTheme("QuickGraph")]
[BlueprintTool(phase: ToolPhase.PreUpdate, order: 50)] [BlueprintTheme("QuickGraph")]
public class QuickGraphInputTool : BlueprintTool
{
// ---------- 配置(从 ToolConfigAssets 读取) ----------
private QuickGraphInputConfig Config
{
get
{
if (ToolConfig is QuickGraphInputConfig external)
return external;
if (_defaultConfig == null)
_defaultConfig = ScriptableObject.CreateInstance<QuickGraphInputConfig>();
return _defaultConfig;
}
}
private static QuickGraphInputConfig _defaultConfig;
// ---------- 缓存的 ViewportController 引用 ----------
private BlueprintViewportController _viewport;
private BlueprintViewportController Viewport
{
get
{
if (_viewport == null && Graph != null)
_viewport = BlueprintToolProvider.GetToolByType<BlueprintViewportController>(Graph);
return _viewport;
}
}
// ---------- 拖拽状态 ----------
private bool _isDragging;
private int _activeDragButton = -1;
private Vector2 senderPanDirection() => GraphInputTool.GetAdapter(Graph)?.PanDirection ?? Vector2.zero;
private float senderZoomAxis() => GraphInputTool.GetAdapter(Graph)?.ZoomAxis ?? 0f;
// ---------- 选择状态 ----------
private BlueprintElementBase _hoveredElement;
public override void OnPointerMove(GraphInputTool sender, Vector2 canvasPoint, Vector2 delta)
{
if (Graph == null || _isDragging) return;
var hit = Graph.HitTest(canvasPoint);
_hoveredElement = hit as BlueprintElementBase;
}
/// <summary>当前悬停的元素(可能为 null</summary>
public BlueprintElementBase HoveredElement => _hoveredElement;
/// <summary>当前选中的元素(委托给 SelectorHandle)。</summary>
public BlueprintElementBase SelectedElement =>
BlueprintToolProvider.GetSelector(Graph).PrimarySelection as BlueprintElementBase;
public override void OnConfigChanged()
{
var viewport = Viewport;
if (viewport == null) return;
var cfg = Config;
viewport.MinZoom = cfg.MinZoom;
viewport.MaxZoom = cfg.MaxZoom;
viewport.TargetZoomLevel = Mathf.Clamp(viewport.TargetZoomLevel, cfg.MinZoom, cfg.MaxZoom);
// ViewportController 是 Canvas Pan/Zoom 的唯一提交者;这里只更新 controller 的范围与目标。
Graph?.MarkDirty();
}
public override void OnUpdate(float deltaTime)
{
if (Graph?.Canvas == null) return;
var viewport = Viewport;
if (viewport == null) return;
var cfg = Config;
// ── 同步缩放范围 ──
viewport.MinZoom = cfg.MinZoom;
viewport.MaxZoom = cfg.MaxZoom;
// ── 键盘 Pan ──
viewport.TargetPanOffset += senderPanDirection() * cfg.KeyboardPanSpeed * deltaTime;
// ── 键盘 Zoom ──
float zoomInput = senderZoomAxis();
if (!Mathf.Approximately(zoomInput, 0f))
{
float oldZoom = viewport.TargetZoomLevel;
float zoomDelta = zoomInput / cfg.ZoomDivisor;
viewport.TargetZoomLevel = Mathf.Clamp(
viewport.TargetZoomLevel + zoomDelta,
viewport.MinZoom, viewport.MaxZoom);
ApplyZoomFocus(oldZoom);
}
}
// ===== 鼠标按下 =====
public override void OnPointerDown(GraphInputTool sender, Vector2 canvasPoint, int mouseButton)
{
if (Graph == null) return;
if (mouseButton == 2)
{
_isDragging = true;
_activeDragButton = 2;
}
}
// ===== 鼠标释放 =====
public override void OnPointerUp(GraphInputTool sender, Vector2 canvasPoint, int mouseButton)
{
if (_isDragging && _activeDragButton == mouseButton)
{
_isDragging = false;
_activeDragButton = -1;
}
}
// ===== 拖拽 =====
public override void OnPointerDrag(GraphInputTool sender, Vector2 canvasPoint, Vector2 delta, int mouseButton)
{
var viewport = Viewport;
if (viewport == null) return;
if (mouseButton == 2)
{
// canvas delta 在本帧 Pan 提交后会反向跳变;仅使用 RenderRoot local 平面 delta。
viewport.TargetPanOffset += (sender?.PointerLocalDelta ?? Vector2.zero) * Config.DragPanSpeed;
Graph.MarkDirty();
}
}
// ===== 滚轮 =====
public override void OnScroll(GraphInputTool sender, Vector2 canvasPoint, Vector2 scrollDelta)
{
var viewport = Viewport;
if (viewport == null) return;
float oldZoom = viewport.TargetZoomLevel;
viewport.TargetZoomLevel = Mathf.Clamp(
viewport.TargetZoomLevel + scrollDelta.y / Config.ZoomDivisor,
viewport.MinZoom, viewport.MaxZoom);
if (!Mathf.Approximately(oldZoom, viewport.TargetZoomLevel))
{
viewport.TargetPanOffset += canvasPoint * (oldZoom - viewport.TargetZoomLevel);
Graph.MarkDirty();
}
}
// ===== 快捷键 =====
public override void OnAction(GraphInputTool sender, string actionName)
{
if (Graph?.Canvas == null) return;
switch (actionName)
{
case BlueprintInputConstants.FocusHome:
Viewport?.ResetView();
break;
case BlueprintInputConstants.Undo:
break;
case BlueprintInputConstants.Redo:
break;
case BlueprintInputConstants.NavigateParent:
break;
}
}
// ===== 以鼠标焦点为中心缩放(键盘调用 — 自行读取鼠标位置) =====
private void ApplyZoomFocus(float oldZoom)
{
var viewport = Viewport;
if (viewport == null || Graph?.Canvas == null) return;
Vector2 canvasCursor = Graph.Canvas.ScreenToCanvas(BlueprintUGUIInputManager.MousePosition);
viewport.TargetPanOffset += canvasCursor * (oldZoom - viewport.TargetZoomLevel);
}
private QuickGraphRenderStyleResolver _styles; private BlueprintViewportController _viewport; private bool _dragging; private int _button = -1; private BlueprintViewportController Viewport => _viewport ?? (_viewport = BlueprintToolProvider.GetToolByType<BlueprintViewportController>(Graph));
private Vector2 Pan => GraphInputTool.GetAdapter(Graph)?.PanDirection ?? Vector2.zero; private float Zoom => GraphInputTool.GetAdapter(Graph)?.ZoomAxis ?? 0f;
public override void OnInitialize() { _styles = new QuickGraphRenderStyleResolver(Graph); _styles.Changed += _ => ApplyViewport(); ApplyViewport(); }
public override void OnConfigChanged() => _styles?.Refresh(); private void ApplyViewport() { if (Viewport == null || _styles == null) return; Graph?.MarkDirty(); }
public override void OnUpdate(float dt) { if (Graph?.Canvas == null || Viewport == null || _styles == null) return; var s = _styles.Snapshot.Input; ApplyViewport(); Viewport.TargetPanOffset += Pan * s.KeyboardPanSpeed * dt; var input = Zoom; if (!Mathf.Approximately(input, 0f)) { var old = Viewport.TargetZoomLevel; Viewport.TargetZoomLevel += input * s.KeyboardZoomSpeed; ApplyFocus(old); } }
public override void OnPointerDown(GraphInputTool sender, Vector2 point, int button) { if (_styles != null && button == _styles.Snapshot.Input.PanMouseButton) { _dragging = true; _button = button; } }
public override void OnPointerUp(GraphInputTool sender, Vector2 point, int button) { if (_dragging && _button == button) { _dragging = false; _button = -1; } }
public override void OnPointerDrag(GraphInputTool sender, Vector2 point, Vector2 delta, int button) { if (_dragging && _button == button && Viewport != null) { Viewport.TargetPanOffset += (sender?.PointerLocalDelta ?? Vector2.zero) * _styles.Snapshot.Input.DragPanSpeed; Graph.MarkDirty(); } }
public override void OnScroll(GraphInputTool sender, Vector2 point, Vector2 delta) { if (Viewport == null || _styles == null) return; var old = Viewport.TargetZoomLevel; Viewport.TargetZoomLevel += delta.y / _styles.Snapshot.Input.ZoomDivisor; if (!Mathf.Approximately(old, Viewport.TargetZoomLevel)) { Viewport.TargetPanOffset += point * (old - Viewport.TargetZoomLevel); Graph.MarkDirty(); } }
private void ApplyFocus(float old) { if (Viewport == null || Graph?.Canvas == null) return; var cursor = Graph.Canvas.ScreenToCanvas(BlueprintUGUIInputManager.MousePosition); Viewport.TargetPanOffset += cursor * (old - Viewport.TargetZoomLevel); }
public override void OnDestroy() { _styles?.Dispose(); _styles = null; }
}
}
@@ -1,133 +0,0 @@
using System;
using UnityEngine;
namespace XericLibrary.Runtime.Blueprint.QuickGraph
{
/// <summary>
/// QuickGraph 网格背景配置表 —— 控制 Shader 全部可调参数。
/// 材质实例由此配置统一管理,所有调用者通过 <see cref="GetOrCreateMaterial"/> 获取同一实例。
/// <see cref="ApplyToMaterial"/> 封装所有材质属性写入(含 _Transform 同步画布平移/缩放)。
/// 右键 Project 窗口 → Create/Blueprint/QuickGraph/Grid Background Config 创建资产。
/// </summary>
[CreateAssetMenu(
fileName = "QuickGraphGridBackgroundConfig",
menuName = "Xeric Library/Blueprint/QuickGraph/Grid Background Config",
order = 3)]
[BlueprintTheme("QuickGraph")]
public class QuickGraphGridBackgroundConfig : BlueprintToolConfigBase
{
/// <summary>目标工具类型</summary>
public override System.Type TargetToolType
{
get { return typeof(QuickUGUIGraphGridBackgroundTool); }
}
[Header("材质来源")]
[Tooltip("网格 Shader 名称")]
public string ShaderName = "XericLibrary/BluePrint/BlueprintBackgound_GridLine";
[Tooltip("可选:直接指定背景材质。设置后将忽略 ShaderName。")]
public Material OverrideMaterial = null;
[Header("网格参数")]
[Tooltip("网格间距(画布单位)。值越小网格越密。")]
public float GridSize = 100f;
[Tooltip("叠加网格密度")]
public float GridOverlayPower = 5f;
[Tooltip("线条阈值。0.5=极粗, 1=极细")]
[Range(0.5f, 1f)]
public float GridLineThreshold = 0.99f;
[Tooltip("线条扩展/柔和度。越大线条越宽。")]
[Range(0.001f, 1f)]
public float GridExp = 0.001f;
[Header("颜色")]
[Tooltip("网格线颜色")]
public Color GridColor = Color.white;
[Tooltip("背景底色")]
public Color GridBackgroundColor = Color.black;
// ─── 运行时缓存的材质实例 ───
[NonSerialized]
private Material _cachedMaterial;
/// <summary>
/// 获取或创建材质实例。
/// - 若 OverrideMaterial 不为空,直接返回。
/// - 否则按 ShaderName 查找 Shader,创建材质并缓存。
/// - 同一配置实例上多次调用返回同一材质对象。
/// </summary>
public Material GetOrCreateMaterial()
{
if (OverrideMaterial != null)
return OverrideMaterial;
if (_cachedMaterial != null)
return _cachedMaterial;
if (!string.IsNullOrEmpty(ShaderName))
{
var shader = Shader.Find(ShaderName);
if (shader != null)
{
_cachedMaterial = new Material(shader);
_cachedMaterial.name = "BpGridBackground_Mat";
}
}
return _cachedMaterial;
}
/// <summary>
/// 将当前配置的所有参数写入材质。
/// <para>包括:
/// - _TransformscaleX, scaleY, offsetX, offsetY):随画布 zoom / pan 同步更新;
/// - _GridOverlayPower、_GridLineThreshold、_GridExp
/// - _GridColor、_GridBackgroundColor。</para>
/// 每次渲染时调用以保持与蓝图画布的缩放/平移同步。
/// </summary>
/// <param name="mat">目标材质实例</param>
/// <param name="rectSize">背景板 RectTransform 的像素尺寸 (width, height)</param>
/// <param name="zoom">当前画布缩放级别</param>
/// <param name="panOffset">当前画布平移偏移量</param>
public void ApplyToMaterial(Material mat, Vector2 rectSize, float zoom, Vector2 panOffset)
{
if (mat == null) return;
float sx = rectSize.x / (zoom * GridSize);
float sy = rectSize.y / (zoom * GridSize);
float ox = -(panOffset.x + 0.5f * rectSize.x) / (zoom * GridSize);
float oy = -(panOffset.y + 0.5f * rectSize.y) / (zoom * GridSize);
mat.SetVector("_Transform", new Vector4(sx, sy, ox, oy));
mat.SetFloat("_GridOverlayPower", GridOverlayPower);
mat.SetFloat("_GridLineThreshold", GridLineThreshold);
mat.SetFloat("_GridExp", GridExp);
mat.SetColor("_GridColor", GridColor);
mat.SetColor("_GridBackgroundColor", GridBackgroundColor);
}
/// <summary>
/// 释放此配置创建的材质实例(不释放 OverrideMaterial)。
/// </summary>
public void ReleaseMaterial()
{
if (_cachedMaterial != null)
{
#if UNITY_EDITOR
if (!Application.isPlaying)
DestroyImmediate(_cachedMaterial);
else
#endif
Destroy(_cachedMaterial);
_cachedMaterial = null;
}
}
}
}
@@ -1,51 +0,0 @@
using System;
using UnityEngine;
namespace XericLibrary.Runtime.Blueprint.QuickGraph
{
/// <summary>
/// QuickGraph 输入工具配置表 —— 控制平移/缩放灵敏度等可调参数。
/// 右键 Project 窗口 → Create/Blueprint/QuickGraph/Input Config 创建资产。
/// 拖入蓝图组件的 ToolConfigAssets 列表即可覆盖默认值。
/// </summary>
[CreateAssetMenu(
fileName = "QuickGraphInputConfig",
menuName = "Xeric Library/Blueprint/QuickGraph/Input Config",
order = 4)]
[BlueprintTheme("QuickGraph")]
public class QuickGraphInputConfig : BlueprintToolConfigBase
{
/// <summary>目标工具类型</summary>
public override System.Type TargetToolType
{
get { return typeof(QuickGraphInputTool); }
}
[Header("缩放")]
[Tooltip("滚轮缩放除数。公式:zoom += scrollDelta / ZoomDivisor。\n旧输入系统 scrollDelta.y 约 ±3,默认 30 = 每格 ±0.1")]
[Range(1f, 200f)]
public float ZoomDivisor = 30f;
[Header("拖拽平移")]
[Tooltip("中键拖拽平移速度系数。1=鼠标移动 1px,画布移动 1px(经缩放校正)")]
[Range(0.1f, 10f)]
public float DragPanSpeed = 1f;
[Header("缩放范围")]
[Tooltip("最小缩放级别")]
[Range(0.01f, 1f)]
public float MinZoom = 0.1f;
[Tooltip("最大缩放级别")]
[Range(1f, 10f)]
public float MaxZoom = 3f;
[Header("键盘")]
[Tooltip("键盘方向键平移速度(画布单位/秒)")]
[Range(50f, 2000f)]
public float KeyboardPanSpeed = 600f;
[Tooltip("键盘 +/- 缩放速度(与 ZoomDivisor 配合,公式:zoom += 1 / ZoomDivisor")]
[Range(0.005f, 0.5f)]
public float KeyboardZoomSpeed = 0.05f;
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 4735b93342f4fde4a8ed46dd28b46d4c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,59 +0,0 @@
using System;
using UnityEngine;
namespace XericLibrary.Runtime.Blueprint.QuickGraph
{
/// <summary>
/// QuickGraph 节点渲染配置表 —— 控制节点矩形、边框、圆角等视觉参数。
/// 右键 Project 窗口 → Create/Blueprint/QuickGraph/Node Render Config 创建资产。
/// 拖入蓝图组件的 ToolConfigAssets 列表即可覆盖默认值。
/// </summary>
[CreateAssetMenu(
fileName = "QuickGraphNodeRenderConfig",
menuName = "Xeric Library/Blueprint/QuickGraph/Node Render Config",
order = 1)]
[BlueprintTheme("QuickGraph")]
public class QuickGraphNodeRenderConfig : BlueprintToolConfigBase
{
/// <summary>目标工具类型</summary>
public override System.Type TargetToolType => typeof(NodeCanvasRenderTool);
[Header("尺寸")]
[Tooltip("节点宽度(像素)")]
public float NodeWidth = 90f;
[Tooltip("节点高度(像素)")]
public float NodeHeight = 130f;
[Header("边框")]
[Tooltip("边框厚度(像素)")]
public float BorderThickness = 3f;
[Header("圆角")]
[Tooltip("圆角大小(0~1 归一化)")]
[Range(0f, 0.5f)]
public float ChamferSize = 0.2f;
[Tooltip("圆角细分段数")]
[Range(1, 16)]
public int ChamferSegments = 4;
[Header("颜色")]
[Tooltip("节点默认背景色")]
public Color DefaultBackgroundColor = new Color(0.16f, 0.16f, 0.16f);
[Tooltip("节点默认文字色")]
public Color DefaultTextColor = Color.white;
[Header("文本")]
[Tooltip("节点标题字号")]
public float TitleFontSize = 14f;
[Tooltip("节点标题字体(留空使用默认 TMP 字体)")]
public TMPro.TMP_FontAsset TitleFont;
[Header("已弃用的 LOD 兼容字段")]
[Obsolete("节点 LOD 已迁移至 BlueprintRenderLodConfig(绝对 Zoom + 滞回),此字段不再参与渲染。")]
[HideInInspector] public float Lod0Threshold = 0.3f;
[Obsolete("节点 LOD 已迁移至 BlueprintRenderLodConfig(绝对 Zoom + 滞回),此字段不再参与渲染。")]
[HideInInspector] public float Lod1Threshold = 0.7f;
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 8038bf45323375b46a3a2820ddfe212e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,67 +0,0 @@
using System;
using UnityEngine;
using XericLibrary.Runtime.UIGraph;
namespace XericLibrary.Runtime.Blueprint.QuickGraph
{
/// <summary>
/// QuickGraph 连线渲染配置表 —— 控制贝塞尔曲线宽度、箭头形状等全部视觉参数。
/// 右键 Project 窗口 → Create/Blueprint/QuickGraph/Wire Render Config 创建资产。
/// 拖入蓝图组件的 ToolConfigAssets 列表即可覆盖默认值。
/// </summary>
[CreateAssetMenu(
fileName = "QuickGraphWireRenderConfig",
menuName = "Xeric Library/Blueprint/QuickGraph/Wire Render Config",
order = 2)]
[BlueprintTheme("QuickGraph")]
public class QuickGraphWireRenderConfig : BlueprintToolConfigBase
{
/// <summary>目标工具类型</summary>
public override System.Type TargetToolType => typeof(WireCanvasRenderTool);
[Header("连线")]
[Tooltip("连线宽度(像素)")]
[Range(1f, 20f)]
public float WireWidth = 5f;
[Tooltip("线条额外点击命中半径(画布单位);实际半径至少为线宽的一半)。")]
[Range(0f, 40f)]
public float HitTestRadius = 8f;
[Header("贝塞尔手柄")]
[Tooltip("源端手柄从端口伸出的距离(画布单位)。数值越大曲线越平缓。")]
[Range(10f, 500f)]
public float SourceHandleLength = 80f;
[Tooltip("目标端手柄从端口伸出的距离(画布单位)。")]
[Range(10f, 500f)]
public float TargetHandleLength = 80f;
[Header("箭头")]
[Tooltip("箭头形状")]
public ArrowShape ArrowShape = ArrowShape.Triangle;
[Tooltip("箭头是否反向")]
public bool ArrowReversed = false;
[Tooltip("箭头在曲线上的位置(0=起点, 1=终点)")]
[Range(0f, 1f)]
public float ArrowProgress = 1f;
[Tooltip("箭头宽度(垂直切线方向,像素)")]
[Range(4f, 40f)]
public float ArrowWidth = 12f;
[Tooltip("箭头高度(沿切线方向,像素)")]
[Range(4f, 40f)]
public float ArrowHeight = 10f;
[Tooltip("深度补偿:-1=尾部对齐曲线点,0=中心对齐,1=头部对齐曲线点")]
[Range(-1f, 1f)]
public float ArrowDepthCompensation = -1f;
[Header("曲线质量")]
[Tooltip("贝塞尔曲线细分段数")]
[Range(8, 64)]
public int TessellationSegments = 32;
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: c95f6e1f1ece40444839f0a29ac371e5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -7,35 +7,27 @@ using PrimitiveType = XericLibrary.Runtime.UIGraph.PrimitiveType;
namespace XericLibrary.Runtime.Blueprint.QuickGraph
{
[BlueprintTool(phase: ToolPhase.Render, order: 100)]
[BlueprintTheme("QuickGraph")]
[BlueprintTool(phase: ToolPhase.Render, order: 100)] [BlueprintTheme("QuickGraph")]
public class NodeCanvasRenderTool : BlueprintCanvasRenderTool<BlueprintPrimitiveRenderer>
{
private QuickGraphNodeRenderConfig _defaultConfig;
private QuickGraphRenderStyleResolver _styleResolver;
private QuickGraphRenderStyleResolver _styles;
private readonly Dictionary<string, (ulong chunkID, TMP_Text text)> _nodeTexts = new Dictionary<string, (ulong, TMP_Text)>();
private readonly HashSet<string> _staleTextCandidates = new HashSet<string>();
private QuickGraphNodeRenderConfig Config
public override void OnInitialize()
{
get
{
if (ToolConfig is QuickGraphNodeRenderConfig external) return external;
if (_defaultConfig == null) _defaultConfig = ScriptableObject.CreateInstance<QuickGraphNodeRenderConfig>();
return _defaultConfig;
}
base.OnInitialize();
_styles = new QuickGraphRenderStyleResolver(Graph);
_styles.Changed += _ => SyncAllNodeGeometry();
SyncAllNodeGeometry();
}
public override void OnInitialize() { base.OnInitialize(); _styleResolver = new QuickGraphRenderStyleResolver(Graph, nodeFallback: ConfigStyle); SyncAllNodeGeometry(); }
public override void OnConfigChanged()
{
_styleResolver?.Refresh(); SyncAllNodeGeometry();
_styles?.Refresh();
base.OnConfigChanged();
}
private QuickGraphNodeRenderStyle ConfigStyle()
{
var cfg = Config;
return new QuickGraphNodeRenderStyle { Width = cfg.NodeWidth, Height = cfg.NodeHeight, Border = cfg.BorderThickness, Chamfer = cfg.ChamferSize, ChamferSegments = cfg.ChamferSegments, TitleFontSize = cfg.TitleFontSize };
}
protected override BlueprintPrimitiveRenderer BuildChunkRenderer(GameObject go) => go.AddComponent<BlueprintPrimitiveRenderer>();
protected override void ClearChunkData(BlueprintPrimitiveRenderer renderer) => renderer.ClearAll();
protected override void SetChunkDirty(BlueprintPrimitiveRenderer renderer) => renderer.SetPrimitiveDirty(0);
@@ -43,76 +35,161 @@ namespace XericLibrary.Runtime.Blueprint.QuickGraph
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);
if (!(element is BlueprintNode node))
return;
var graphStyle = _styles.Snapshot;
var nodeStyle = graphStyle.Node;
var lod = graphStyle.GetLod(CurrentElementSubmission.LOD);
CalculatePortPositions(node, nodeStyle.Width, nodeStyle.Height, graphStyle.Port);
float border = lod.NodeBorder;
Color fillColor = node.GetStyleValue<Color>(QuickGraphStylePaths.NodeFillColor);
Color borderColor = node.GetStyleValue<Color>(QuickGraphStylePaths.NodeBorderColor);
Color titleColor = node.GetStyleValue<Color>(QuickGraphStylePaths.NodeTitleColor);
Color centerColor = lod.UseNodeColorAsBackground ? borderColor : fillColor;
renderer.AddPrimitive(new PrimitiveCacheEntry
{
type = PrimitiveType.Rectangle,
sizeMode = SizeMode.InscribedEllipse,
center = CurrentElementSubmission.ChunkLocalPosition,
size = new Vector2(nodeStyle.Width, nodeStyle.Height),
@params = new PrimitiveParams
{
axisScaleX = nodeStyle.AxisScaleX,
axisScaleY = nodeStyle.AxisScaleY,
sideCount = nodeStyle.SideCount,
chamferSize = nodeStyle.Chamfer,
chamferSegments = nodeStyle.ChamferSegments,
bgColor = nodeStyle.OuterColor,
centerColor = centerColor,
borderColor = borderColor,
borderThickness = border
}
});
if (lod.ShowTitle && (CurrentElementSubmission.LOD != BlueprintLodLevel.Full || nodeStyle.ShowTitleFull))
UpdateNodeText(node, chunkID, CurrentElementSubmission.ChunkLocalPosition, nodeStyle, border, titleColor);
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 };
if (Graph == null || _styles == null)
return;
var style = _styles.Snapshot;
foreach (var node in Graph.Nodes)
{
var oldSize = node.NodeSize;
CalculatePortPositions(node, style.Width, style.Height);
if (oldSize != node.NodeSize) Graph.UpdateNodeGeometry(node);
var old = node.NodeSize;
CalculatePortPositions(node, style.Node.Width, style.Node.Height, style.Port);
if (old != node.NodeSize)
Graph.UpdateNodeGeometry(node);
}
}
private static void CalculatePortPositions(BlueprintNode node, float width, float height)
private static void CalculatePortPositions(BlueprintNode node, float width, float height, QuickGraphPortRenderStyle style)
{
// Position 语义为视觉中心;渲染配置尺寸必须同步给数据模型,供 BoundingBox / RTree 使用。
node.NodeSize = new Vector2(width, height);
for (int i=0;i<node.InputPorts.Count;i++) node.InputPorts[i].RelativePosition = new Vector2(-width*.5f, height*(.5f-(float)(i+1)/(node.InputPorts.Count+1)));
for (int i=0;i<node.OutputPorts.Count;i++) node.OutputPorts[i].RelativePosition = new Vector2(width*.5f, height*(.5f-(float)(i+1)/(node.OutputPorts.Count+1)));
for (int i = 0; i < node.InputPorts.Count; i++)
node.InputPorts[i].RelativePosition = new Vector2(width * style.InputSide, height * Mathf.Lerp(style.TopRatio, style.BottomRatio, node.InputPorts.Count == 1 ? .5f : (float)i / (node.InputPorts.Count - 1)));
for (int i = 0; i < node.OutputPorts.Count; i++)
node.OutputPorts[i].RelativePosition = new Vector2(width * style.OutputSide, height * Mathf.Lerp(style.TopRatio, style.BottomRatio, node.OutputPorts.Count == 1 ? .5f : (float)i / (node.OutputPorts.Count - 1)));
}
protected override void BeginChunkSubmission(ulong chunk, BlueprintPrimitiveRenderer renderer)
{
foreach (var pair in _nodeTexts)
if (pair.Value.chunkID == chunk) _staleTextCandidates.Add(pair.Key);
if (pair.Value.chunkID == chunk)
_staleTextCandidates.Add(pair.Key);
}
protected override void EndChunkSubmission(ulong chunk, BlueprintPrimitiveRenderer renderer)
{
var remove = new List<string>();
foreach (var id in _staleTextCandidates)
if (_nodeTexts.TryGetValue(id, out var item) && item.chunkID == chunk) remove.Add(id);
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);
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)
private void UpdateNodeText(BlueprintNode node, ulong chunk, Vector2 local, QuickGraphNodeRenderStyle style, float border, Color titleColor)
{
_staleTextCandidates.Remove(node.ElementId);
TMP_Text text;
if (_nodeTexts.TryGetValue(node.ElementId, out var old) && old.chunkID == chunk && old.text != null) text = old.text;
if (_nodeTexts.TryGetValue(node.ElementId, out var old) && old.chunkID == chunk && old.text != null)
{
text = old.text;
}
else
{
if (old.text != null) Object.Destroy(old.text.gameObject);
var go = new GameObject($"__Text_{node.ElementId}", typeof(RectTransform)); go.hideFlags = HideFlags.HideAndDontSave; go.transform.SetParent(rendererTransform(chunk), false);
text = go.AddComponent<TextMeshProUGUI>(); text.alignment = TextAlignmentOptions.Center; _nodeTexts[node.ElementId] = (chunk, text);
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(GetOrCreateChunk(chunk).transform, false);
text = go.AddComponent<TextMeshProUGUI>();
_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();
text.text = node.NodeTitle;
text.color = titleColor;
text.fontSize = style.TitleFontSize;
text.font = style.TitleFont;
text.alignment = style.TitleAlignment;
var rt = text.rectTransform;
var pivot = text.transform.parent is RectTransform parent ? parent.pivot : new Vector2(.5f, .5f);
rt.anchorMin = pivot;
rt.anchorMax = pivot;
rt.pivot = new Vector2(.5f, .5f);
rt.anchoredPosition = local;
rt.sizeDelta = new Vector2(style.Width - border * 2, style.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); }
private void HideNodeText(string id)
{
if (_nodeTexts.TryGetValue(id, out var item) && item.text != null)
item.text.gameObject.SetActive(false);
}
protected override void OnChunkRecycled(ulong chunk, BlueprintPrimitiveRenderer renderer)
{
_staleTextCandidates.Clear();
var remove=new List<string>(); foreach(var pair in _nodeTexts) if(pair.Value.chunkID==chunk) remove.Add(pair.Key);
foreach(var id in remove) { if(_nodeTexts[id].text!=null) Object.Destroy(_nodeTexts[id].text.gameObject); _nodeTexts.Remove(id); }
var remove = new List<string>();
foreach (var pair in _nodeTexts)
if (pair.Value.chunkID == chunk)
remove.Add(pair.Key);
foreach (var id in remove)
{
if (_nodeTexts[id].text != null)
Object.Destroy(_nodeTexts[id].text.gameObject);
_nodeTexts.Remove(id);
}
}
protected override void DestroyAllChunks()
{
foreach (var pair in _nodeTexts)
if (pair.Value.text != null)
Object.Destroy(pair.Value.text.gameObject);
_nodeTexts.Clear();
base.DestroyAllChunks();
}
public override void OnDestroy()
{
_styles?.Dispose();
_styles = null;
base.OnDestroy();
}
protected override void DestroyAllChunks() { foreach(var pair in _nodeTexts) if(pair.Value.text!=null) Object.Destroy(pair.Value.text.gameObject); _nodeTexts.Clear(); base.DestroyAllChunks(); }
public override void OnDestroy() { _styleResolver?.Dispose(); _styleResolver = null; base.OnDestroy(); }
}
}
@@ -1,4 +1,7 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using TMPro;
using UnityEngine;
using XericLibrary.Runtime.Blueprint;
using XericLibrary.Runtime.SuperStyleSheet;
@@ -6,64 +9,81 @@ 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; }
[Flags] public enum QuickGraphRenderStyleChange { None = 0, NodeGeometry = 1, NodeAppearance = 2, PortLayout = 4, WireGeometry = 8, WireAppearance = 16, GridAppearance = 32, Input = 64, Lod = 128, Selection = 256, All = NodeGeometry | NodeAppearance | PortLayout | WireGeometry | WireAppearance | GridAppearance | Input | Lod | Selection }
public struct QuickGraphNodeRenderStyle { public float Width, Height, Border, Chamfer, TitleFontSize, AxisScaleX, AxisScaleY; public int ChamferSegments, SideCount; public TMP_FontAsset TitleFont; public TextAlignmentOptions TitleAlignment; public bool ShowTitleFull; public Color OuterColor; }
public struct QuickGraphPortRenderStyle { public float InputSide, OutputSide, TopRatio, BottomRatio; }
public struct QuickGraphWireRenderStyle { public float Width, HitRadius, SourceHandle, TargetHandle, ArrowWidth, ArrowHeight, ArrowProgress, ArrowDepth; public int Tessellation, MinTessellation; public ArrowShape ArrowShape; public bool ArrowReversed, UseNodeColorGradient; public Color Color; }
public struct QuickGraphGridRenderStyle { public Material Material; public string ShaderName; public float Size, OverlayPower, Threshold, Exp, LodTransitionThreshold, LodTransitionStart, LodTransitionEnd, LineShapeScale; public Color Color, Background; public bool RaycastTarget, Maskable; public Vector2 CellCenter; }
public struct QuickGraphInputRenderStyle { public float ZoomDivisor, DragPanSpeed, MinZoom, MaxZoom, DefaultZoom, FocusZoom, FocusPadding, KeyboardPanSpeed, KeyboardZoomSpeed; public int PanMouseButton; }
public struct QuickGraphLodRenderStyle { public float EnterZoom, ExitZoom, WireTessellationMultiplier, NodeBorder; public bool UseNodeColorAsBackground, ShowTitle; }
public struct QuickGraphSelectionRenderStyle { public float DragThreshold; public Color FillColor, OutlineColor; public Vector2 OutlineOffset; public bool RaycastTarget; }
public sealed class QuickGraphRenderStyleSnapshot { public int Revision; public QuickGraphRenderStyleChange Changes; public QuickGraphNodeRenderStyle Node; public QuickGraphPortRenderStyle Port; public QuickGraphWireRenderStyle Wire; public QuickGraphGridRenderStyle Grid; public QuickGraphInputRenderStyle Input; public QuickGraphLodRenderStyle MinimalLod, SimplifiedLod, FullLod; public QuickGraphSelectionRenderStyle Selection; public QuickGraphLodRenderStyle GetLod(BlueprintLodLevel level) => level == BlueprintLodLevel.Minimal ? MinimalLod : level == BlueprintLodLevel.Simplified ? SimplifiedLod : FullLod; }
/// <summary>每次样式变更只解析一次;渲染热路径只消费强类型快照。</summary>
public sealed class QuickGraphRenderStyleResolver : IDisposable
{
private readonly BlueprintGraph _graph;
private readonly Func<QuickGraphNodeRenderStyle> _nodeFallback;
private readonly Func<QuickGraphWireRenderStyle> _wireFallback;
private readonly Func<QuickGraphGridRenderStyle> _gridFallback;
private static readonly ConditionalWeakTable<BlueprintGraph, ValidationState> _validationStates = new ConditionalWeakTable<BlueprintGraph, ValidationState>();
private sealed class ValidationState { public string LastError; }
public QuickGraphRenderStyleSnapshot Snapshot { get; private set; }
public event Action<QuickGraphRenderStyleSnapshot> Changed;
public QuickGraphRenderStyleResolver(BlueprintGraph graph) { _graph = graph; _graph.RenderStyleChanged += Refresh; StyleManager.OnStylesReloaded += Refresh; Refresh(); }
private T Value<T>(string path) => _graph.GetRenderStyleValue<T>(path);
private QuickGraphLodRenderStyle Lod(string name) => new QuickGraphLodRenderStyle { EnterZoom = Value<float>(QuickGraphStylePaths.Lod(name, "enterZoom")), ExitZoom = Value<float>(QuickGraphStylePaths.Lod(name, "exitZoom")), WireTessellationMultiplier = Value<float>(QuickGraphStylePaths.Lod(name, "wireTessellationMultiplier")), NodeBorder = Value<float>(QuickGraphStylePaths.Lod(name, "nodeBorder")), UseNodeColorAsBackground = Value<bool>(QuickGraphStylePaths.Lod(name, "useNodeColorAsBackground")), ShowTitle = Value<bool>(QuickGraphStylePaths.Lod(name, "showTitle")) };
public QuickGraphRenderStyleResolver(BlueprintGraph graph, Func<QuickGraphNodeRenderStyle> nodeFallback = null, Func<QuickGraphWireRenderStyle> wireFallback = null, Func<QuickGraphGridRenderStyle> gridFallback = null)
{
_graph = graph;
_nodeFallback = nodeFallback ?? DefaultNode;
_wireFallback = wireFallback ?? DefaultWire;
_gridFallback = gridFallback ?? DefaultGrid;
_graph.RenderStyleChanged += Refresh;
StyleManager.OnStylesReloaded += Refresh;
Refresh();
}
private static QuickGraphNodeRenderStyle DefaultNode() => new QuickGraphNodeRenderStyle { Width = 90f, Height = 130f, Border = 3f, Chamfer = .2f, ChamferSegments = 4, TitleFontSize = 14f };
private static QuickGraphWireRenderStyle DefaultWire() => new QuickGraphWireRenderStyle { Width = 5f, HitRadius = 8f, SourceHandle = 80f, TargetHandle = 80f, ArrowWidth = 12f, ArrowHeight = 10f, ArrowProgress = 1f, ArrowDepth = -1f, Tessellation = 32, ArrowShape = ArrowShape.Triangle };
private static QuickGraphGridRenderStyle DefaultGrid() => new QuickGraphGridRenderStyle { ShaderName = "XericLibrary/BluePrint/BlueprintBackgound_GridLine", Size = 100f, OverlayPower = 5f, Threshold = .99f, Exp = .001f, Color = Color.white, Background = Color.black };
// 样式项存在即优先使用;只有项缺失时才使用旧 ToolConfig 回退值。
private T Value<T>(string path, T fallback) { var value = _graph.GetRenderStyle(path); return value == null ? fallback : value.GetValueAs<T>(); }
public void Refresh()
{
var node = _nodeFallback();
node = new QuickGraphNodeRenderStyle { Width = Value("/quickgraph/node/width", node.Width), Height = Value("/quickgraph/node/height", node.Height), Border = Value("/quickgraph/node/border", node.Border), Chamfer = Value("/quickgraph/node/chamfer", node.Chamfer), ChamferSegments = Value("/quickgraph/node/chamferSegments", node.ChamferSegments), TitleFontSize = Value("/quickgraph/node/titleFontSize", node.TitleFontSize) };
var wire = _wireFallback();
wire = new QuickGraphWireRenderStyle { Width = Value("/quickgraph/wire/width", wire.Width), HitRadius = Value("/quickgraph/wire/hitRadius", wire.HitRadius), SourceHandle = Value("/quickgraph/wire/sourceHandle", wire.SourceHandle), TargetHandle = Value("/quickgraph/wire/targetHandle", wire.TargetHandle), ArrowWidth = Value("/quickgraph/wire/arrowWidth", wire.ArrowWidth), ArrowHeight = Value("/quickgraph/wire/arrowHeight", wire.ArrowHeight), ArrowProgress = Value("/quickgraph/wire/arrowProgress", wire.ArrowProgress), ArrowDepth = Value("/quickgraph/wire/arrowDepth", wire.ArrowDepth), Tessellation = Value("/quickgraph/wire/tessellation", wire.Tessellation), ArrowShape = Value("/quickgraph/wire/arrowShape", wire.ArrowShape), ArrowReversed = Value("/quickgraph/wire/arrowReversed", wire.ArrowReversed) };
var grid = _gridFallback();
grid = new QuickGraphGridRenderStyle { Material = Value("/quickgraph/grid/material", grid.Material), ShaderName = Value("/quickgraph/grid/shader", grid.ShaderName), Size = Value("/quickgraph/grid/size", grid.Size), OverlayPower = Value("/quickgraph/grid/overlayPower", grid.OverlayPower), Threshold = Value("/quickgraph/grid/threshold", grid.Threshold), Exp = Value("/quickgraph/grid/exp", grid.Exp), Color = Value("/quickgraph/grid/color", grid.Color), Background = Value("/quickgraph/grid/background", grid.Background) };
var old = Snapshot;
var changes = old == null ? QuickGraphRenderStyleChange.All : Classify(old, node, wire, grid);
Snapshot = new QuickGraphRenderStyleSnapshot { Revision = old == null ? 1 : old.Revision + 1, Changes = changes, Node = node, Wire = wire, Grid = grid };
if (changes != QuickGraphRenderStyleChange.None) Changed?.Invoke(Snapshot);
StyleManager.EnsureNamespaceLoaded(_graph.RenderStyleContext == null ? "default" : _graph.RenderStyleContext.BaseNamespace);
var node = new QuickGraphNodeRenderStyle { Width = Value<float>(QuickGraphStylePaths.NodeWidth), Height = Value<float>(QuickGraphStylePaths.NodeHeight), Border = Value<float>(QuickGraphStylePaths.NodeBorder), Chamfer = Value<float>(QuickGraphStylePaths.NodeChamfer), ChamferSegments = Value<int>(QuickGraphStylePaths.NodeChamferSegments), TitleFontSize = Value<float>(QuickGraphStylePaths.NodeTitleFontSize), TitleFont = Value<TMP_FontAsset>(QuickGraphStylePaths.NodeTitleFont), TitleAlignment = Value<TextAlignmentOptions>(QuickGraphStylePaths.NodeTitleAlignment), ShowTitleFull = Value<bool>(QuickGraphStylePaths.NodeShowTitleFull), OuterColor = Value<Color>(QuickGraphStylePaths.NodeOuterColor), AxisScaleX = Value<float>(QuickGraphStylePaths.NodeAxisScaleX), AxisScaleY = Value<float>(QuickGraphStylePaths.NodeAxisScaleY), SideCount = Value<int>(QuickGraphStylePaths.NodeSideCount) };
var port = new QuickGraphPortRenderStyle { InputSide = Value<float>(QuickGraphStylePaths.PortInputSide), OutputSide = Value<float>(QuickGraphStylePaths.PortOutputSide), TopRatio = Value<float>(QuickGraphStylePaths.PortTopRatio), BottomRatio = Value<float>(QuickGraphStylePaths.PortBottomRatio) };
var wire = new QuickGraphWireRenderStyle { Width = Value<float>(QuickGraphStylePaths.WireWidth), HitRadius = Value<float>(QuickGraphStylePaths.WireHitRadius), SourceHandle = Value<float>(QuickGraphStylePaths.WireSourceHandle), TargetHandle = Value<float>(QuickGraphStylePaths.WireTargetHandle), ArrowWidth = Value<float>(QuickGraphStylePaths.WireArrowWidth), ArrowHeight = Value<float>(QuickGraphStylePaths.WireArrowHeight), ArrowProgress = Value<float>(QuickGraphStylePaths.WireArrowProgress), ArrowDepth = Value<float>(QuickGraphStylePaths.WireArrowDepth), Tessellation = Value<int>(QuickGraphStylePaths.WireTessellation), MinTessellation = Value<int>(QuickGraphStylePaths.WireMinTessellation), ArrowShape = Value<ArrowShape>(QuickGraphStylePaths.WireArrowShape), ArrowReversed = Value<bool>(QuickGraphStylePaths.WireArrowReversed), UseNodeColorGradient = Value<bool>(QuickGraphStylePaths.WireUseNodeColorGradient), Color = Value<Color>(QuickGraphStylePaths.WireColor) };
var grid = new QuickGraphGridRenderStyle { Material = Value<Material>(QuickGraphStylePaths.GridMaterial), ShaderName = Value<string>(QuickGraphStylePaths.GridShader), Size = Value<float>(QuickGraphStylePaths.GridSize), OverlayPower = Value<float>(QuickGraphStylePaths.GridOverlayPower), Threshold = Value<float>(QuickGraphStylePaths.GridThreshold), Exp = Value<float>(QuickGraphStylePaths.GridExp), Color = Value<Color>(QuickGraphStylePaths.GridColor), Background = Value<Color>(QuickGraphStylePaths.GridBackground), RaycastTarget = Value<bool>(QuickGraphStylePaths.GridRaycastTarget), Maskable = Value<bool>(QuickGraphStylePaths.GridMaskable), LodTransitionThreshold = Value<float>(QuickGraphStylePaths.GridLodTransitionThreshold), LodTransitionStart = Value<float>(QuickGraphStylePaths.GridLodTransitionStart), LodTransitionEnd = Value<float>(QuickGraphStylePaths.GridLodTransitionEnd), CellCenter = Value<Vector2>(QuickGraphStylePaths.GridCellCenter), LineShapeScale = Value<float>(QuickGraphStylePaths.GridLineShapeScale) };
var input = new QuickGraphInputRenderStyle { ZoomDivisor = Value<float>(QuickGraphStylePaths.InputZoomDivisor), DragPanSpeed = Value<float>(QuickGraphStylePaths.InputDragPanSpeed), MinZoom = Value<float>(QuickGraphStylePaths.InputMinZoom), MaxZoom = Value<float>(QuickGraphStylePaths.InputMaxZoom), DefaultZoom = Value<float>(QuickGraphStylePaths.InputDefaultZoom), FocusZoom = Value<float>(QuickGraphStylePaths.InputFocusZoom), FocusPadding = Value<float>(QuickGraphStylePaths.InputFocusPadding), KeyboardPanSpeed = Value<float>(QuickGraphStylePaths.InputKeyboardPanSpeed), KeyboardZoomSpeed = Value<float>(QuickGraphStylePaths.InputKeyboardZoomSpeed), PanMouseButton = Value<int>(QuickGraphStylePaths.InputPanMouseButton) };
var selection = new QuickGraphSelectionRenderStyle { DragThreshold = Value<float>(QuickGraphStylePaths.SelectionDragThreshold), FillColor = Value<Color>(QuickGraphStylePaths.SelectionFillColor), OutlineColor = Value<Color>(QuickGraphStylePaths.SelectionOutlineColor), OutlineOffset = Value<Vector2>(QuickGraphStylePaths.SelectionOutlineOffset), RaycastTarget = Value<bool>(QuickGraphStylePaths.SelectionRaycastTarget) };
var minimalLod = Lod("minimal"); var simplifiedLod = Lod("simplified"); var fullLod = Lod("full"); var old = Snapshot;
var changes = old == null ? QuickGraphRenderStyleChange.All : Changes(old, node, port, wire, grid, input, minimalLod, simplifiedLod, fullLod, selection);
Snapshot = new QuickGraphRenderStyleSnapshot { Revision = old == null ? 1 : old.Revision + 1, Changes = changes, Node = node, Port = port, Wire = wire, Grid = grid, Input = input, MinimalLod = minimalLod, SimplifiedLod = simplifiedLod, FullLod = fullLod, Selection = selection };
Validate(node, wire, grid, input);
Changed?.Invoke(Snapshot);
}
private static QuickGraphRenderStyleChange Classify(QuickGraphRenderStyleSnapshot old, QuickGraphNodeRenderStyle node, QuickGraphWireRenderStyle wire, QuickGraphGridRenderStyle grid)
private void Validate(QuickGraphNodeRenderStyle node, QuickGraphWireRenderStyle wire, QuickGraphGridRenderStyle grid, QuickGraphInputRenderStyle input)
{
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;
var errors = new List<string>();
Require(errors, QuickGraphStylePaths.NodeWidth); Require(errors, QuickGraphStylePaths.NodeHeight); Require(errors, QuickGraphStylePaths.WireWidth); Require(errors, QuickGraphStylePaths.WireTessellation); Require(errors, QuickGraphStylePaths.WireMinTessellation); Require(errors, QuickGraphStylePaths.GridSize); Require(errors, QuickGraphStylePaths.InputMinZoom); Require(errors, QuickGraphStylePaths.InputMaxZoom); Require(errors, QuickGraphStylePaths.InputDefaultZoom);
if (!Has(QuickGraphStylePaths.GridMaterial) && !Has(QuickGraphStylePaths.GridShader)) errors.Add($"缺少关键路径 path={QuickGraphStylePaths.GridMaterial} 或 path={QuickGraphStylePaths.GridShader} value=<missing> expected=至少提供一个可用的 Grid 材质或 Shader");
if (node.Width <= 0f) errors.Add($"无效节点宽度 path={QuickGraphStylePaths.NodeWidth} value={node.Width} expected=> 0");
if (node.Height <= 0f) errors.Add($"无效节点高度 path={QuickGraphStylePaths.NodeHeight} value={node.Height} expected=> 0");
if (wire.Width <= 0f) errors.Add($"无效连线宽度 path={QuickGraphStylePaths.WireWidth} value={wire.Width} expected=> 0");
if (wire.MinTessellation < 2 || wire.Tessellation < wire.MinTessellation) errors.Add($"无效连线细分 path={QuickGraphStylePaths.WireTessellation} value={wire.Tessellation}; path={QuickGraphStylePaths.WireMinTessellation} value={wire.MinTessellation} expected=tessellation >= minTessellation >= 2");
if (grid.Size <= 0f) errors.Add($"无效网格尺寸 path={QuickGraphStylePaths.GridSize} value={grid.Size} expected=> 0");
if (input.MinZoom <= 0f || input.MaxZoom <= input.MinZoom || input.DefaultZoom < input.MinZoom || input.DefaultZoom > input.MaxZoom) errors.Add($"无效缩放范围 path={QuickGraphStylePaths.InputMinZoom} value={input.MinZoom}; path={QuickGraphStylePaths.InputMaxZoom} value={input.MaxZoom}; path={QuickGraphStylePaths.InputDefaultZoom} value={input.DefaultZoom} expected=0 < minZoom <= defaultZoom <= maxZoom");
bool materialUsable = grid.Material != null && grid.Material.shader != null;
bool shaderUsable = !string.IsNullOrEmpty(grid.ShaderName) && Shader.Find(grid.ShaderName) != null;
if (!materialUsable && !shaderUsable) errors.Add($"Grid 材质与 Shader 不可用 path={QuickGraphStylePaths.GridMaterial} value={grid.Material}; path={QuickGraphStylePaths.GridShader} value={grid.ShaderName ?? "<null>"} expected=有效 Material(含 shader) 或可由 Shader.Find 找到的 Shader 名称");
string error = errors.Count == 0 ? null : $"[QuickGraph] 样式快照无效 GraphId={_graph.GraphId} ThemeName={_graph.ThemeName} Namespace={Namespace}:\n - " + string.Join("\n - ", errors);
var state = _validationStates.GetOrCreateValue(_graph);
if (error == state.LastError) return;
state.LastError = error;
if (error != null) Debug.LogError(error);
}
private string Namespace => _graph.RenderStyleContext == null || string.IsNullOrEmpty(_graph.RenderStyleContext.BaseNamespace) ? "default" : _graph.RenderStyleContext.BaseNamespace;
private bool Has(string path) { return _graph.TryGetRenderStyle(path, out _); }
private void Require(List<string> errors, string path) { if (!Has(path)) errors.Add($"缺少关键路径 path={path} value=<missing> expected=定义可解析的样式值"); }
private static QuickGraphRenderStyleChange Changes(QuickGraphRenderStyleSnapshot old, QuickGraphNodeRenderStyle node, QuickGraphPortRenderStyle port, QuickGraphWireRenderStyle wire, QuickGraphGridRenderStyle grid, QuickGraphInputRenderStyle input, QuickGraphLodRenderStyle minimalLod, QuickGraphLodRenderStyle simplifiedLod, QuickGraphLodRenderStyle fullLod, QuickGraphSelectionRenderStyle selection)
{
var changes = QuickGraphRenderStyleChange.None;
if (!Equals(old.Node, node)) changes |= QuickGraphRenderStyleChange.NodeGeometry | QuickGraphRenderStyleChange.NodeAppearance;
if (!Equals(old.Port, port)) changes |= QuickGraphRenderStyleChange.PortLayout;
if (!Equals(old.Wire, wire)) changes |= QuickGraphRenderStyleChange.WireGeometry | QuickGraphRenderStyleChange.WireAppearance;
if (!Equals(old.Grid, grid)) changes |= QuickGraphRenderStyleChange.GridAppearance;
if (!Equals(old.Input, input)) changes |= QuickGraphRenderStyleChange.Input;
if (!Equals(old.MinimalLod, minimalLod) || !Equals(old.SimplifiedLod, simplifiedLod) || !Equals(old.FullLod, fullLod)) changes |= QuickGraphRenderStyleChange.Lod;
if (!Equals(old.Selection, selection)) changes |= QuickGraphRenderStyleChange.Selection;
return changes;
}
public void Dispose() { _graph.RenderStyleChanged -= Refresh; StyleManager.OnStylesReloaded -= Refresh; Changed = null; }
}
}
}
@@ -0,0 +1,90 @@
namespace XericLibrary.Runtime.Blueprint.QuickGraph
{
/// <summary>
/// QuickGraph XSSS 路径的唯一声明处。
/// 命名空间决定主题,路径仅描述视觉/交互要素。
/// </summary>
public static class QuickGraphStylePaths
{
public const string NodeRoot = "/quickgraph/node/";
public const string NodeWidth = NodeRoot + "width";
public const string NodeHeight = NodeRoot + "height";
public const string NodeBorder = NodeRoot + "border";
public const string NodeChamfer = NodeRoot + "chamfer";
public const string NodeChamferSegments = NodeRoot + "chamferSegments";
public const string NodeTitleFontSize = NodeRoot + "titleFontSize";
public const string NodeTitleFont = NodeRoot + "titleFont";
public const string NodeTitleAlignment = NodeRoot + "titleAlignment";
public const string NodeShowTitleFull = NodeRoot + "showTitle/full";
public const string NodeOuterColor = NodeRoot + "outerColor";
public const string NodeAxisScaleX = NodeRoot + "axisScaleX";
public const string NodeAxisScaleY = NodeRoot + "axisScaleY";
public const string NodeSideCount = NodeRoot + "sideCount";
public const string NodeFillColor = NodeRoot + "fillColor";
public const string NodeBorderColor = NodeRoot + "borderColor";
public const string NodeTitleColor = NodeRoot + "titleColor";
public const string PortRoot = "/quickgraph/port/";
public const string PortInputSide = PortRoot + "inputSide";
public const string PortOutputSide = PortRoot + "outputSide";
public const string PortTopRatio = PortRoot + "topRatio";
public const string PortBottomRatio = PortRoot + "bottomRatio";
public const string WireRoot = "/quickgraph/wire/";
public const string WireWidth = WireRoot + "width";
public const string WireHitRadius = WireRoot + "hitRadius";
public const string WireSourceHandle = WireRoot + "sourceHandle";
public const string WireTargetHandle = WireRoot + "targetHandle";
public const string WireArrowWidth = WireRoot + "arrowWidth";
public const string WireArrowHeight = WireRoot + "arrowHeight";
public const string WireArrowProgress = WireRoot + "arrowProgress";
public const string WireArrowDepth = WireRoot + "arrowDepth";
public const string WireTessellation = WireRoot + "tessellation";
public const string WireMinTessellation = WireRoot + "minTessellation";
public const string WireArrowShape = WireRoot + "arrowShape";
public const string WireArrowReversed = WireRoot + "arrowReversed";
public const string WireUseNodeColorGradient = WireRoot + "useNodeColorGradient";
public const string WireColor = WireRoot + "color";
public const string GridRoot = "/quickgraph/grid/";
public const string GridMaterial = GridRoot + "material";
public const string GridShader = GridRoot + "shader";
public const string GridSize = GridRoot + "size";
public const string GridOverlayPower = GridRoot + "overlayPower";
public const string GridThreshold = GridRoot + "threshold";
public const string GridExp = GridRoot + "exp";
public const string GridColor = GridRoot + "color";
public const string GridBackground = GridRoot + "background";
public const string GridRaycastTarget = GridRoot + "raycastTarget";
public const string GridMaskable = GridRoot + "maskable";
public const string GridLodTransitionThreshold = GridRoot + "lodTransitionThreshold";
public const string GridLodTransitionStart = GridRoot + "lodTransitionStart";
public const string GridLodTransitionEnd = GridRoot + "lodTransitionEnd";
public const string GridCellCenter = GridRoot + "cellCenter";
public const string GridLineShapeScale = GridRoot + "lineShapeScale";
public const string InputRoot = "/quickgraph/input/";
public const string InputZoomDivisor = InputRoot + "zoomDivisor";
public const string InputDragPanSpeed = InputRoot + "dragPanSpeed";
public const string InputMinZoom = InputRoot + "minZoom";
public const string InputMaxZoom = InputRoot + "maxZoom";
public const string InputDefaultZoom = InputRoot + "defaultZoom";
public const string InputFocusZoom = InputRoot + "focusZoom";
public const string InputFocusPadding = InputRoot + "focusPadding";
public const string InputKeyboardPanSpeed = InputRoot + "keyboardPanSpeed";
public const string InputKeyboardZoomSpeed = InputRoot + "keyboardZoomSpeed";
public const string InputPanMouseButton = InputRoot + "panMouseButton";
public const string SelectionRoot = "/quickgraph/selection/";
public const string SelectionDragThreshold = SelectionRoot + "dragThreshold";
public const string SelectionFillColor = SelectionRoot + "fillColor";
public const string SelectionOutlineColor = SelectionRoot + "outlineColor";
public const string SelectionOutlineOffset = SelectionRoot + "outlineOffset";
public const string SelectionRaycastTarget = SelectionRoot + "raycastTarget";
public static string Lod(string level, string property)
{
return "/quickgraph/lod/" + level + "/" + property;
}
}
}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 38115e5721db6514bb8b8be6be10f26a
guid: 46bc7416448ac4d488bdeb2359b46692
MonoImporter:
externalObjects: {}
serializedVersion: 2
@@ -1,113 +1,18 @@
using UnityEngine;
using UnityEngine.UI;
namespace XericLibrary.Runtime.Blueprint.QuickGraph
{
/// <summary>QuickGraph 网格背景;样式资源材质只绑定不拥有,运行时创建材质才由本工具释放。</summary>
[BlueprintTool(phase: ToolPhase.Render, order: 0)]
[BlueprintTheme("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<QuickGraphGridBackgroundConfig>();
return _defaultConfig;
}
}
public override void OnInitialize()
{
_styleResolver = new QuickGraphRenderStyleResolver(Graph, gridFallback: ConfigStyle);
_styleResolver.Changed += OnStyleChanged;
EnsureBackground(); RebindMaterial(_styleResolver.Snapshot.Grid, true);
}
public override void OnRender()
{
if (Graph == null) return;
EnsureBackground();
var style = _styleResolver != null ? _styleResolver.Snapshot.Grid : ConfigStyle();
RebindMaterial(style, false);
ApplyStyle(style);
}
public override void OnConfigChanged()
{
_styleResolver?.Refresh();
if (_styleResolver == null) RebindMaterial(ConfigStyle(), true);
Graph?.MarkDirty();
}
private QuickGraphGridRenderStyle ConfigStyle()
{
var cfg = Config;
return new QuickGraphGridRenderStyle { Material = cfg.OverrideMaterial, ShaderName = cfg.ShaderName, Size = cfg.GridSize, OverlayPower = cfg.GridOverlayPower, Threshold = cfg.GridLineThreshold, Exp = cfg.GridExp, Color = cfg.GridColor, Background = cfg.GridBackgroundColor };
}
private void OnStyleChanged(QuickGraphRenderStyleSnapshot snapshot)
{
if ((snapshot.Changes & QuickGraphRenderStyleChange.GridAppearance) == 0) return;
RebindMaterial(snapshot.Grid, true);
Graph?.MarkDirty();
}
private void EnsureBackground()
{
if (_backgroundImage != null || Graph?.Canvas?.GetRootTransform() == null) return;
var root = Graph.Canvas.GetRootTransform(); var existing = root.Find(BackgroundGoName);
if (existing != null) _backgroundImage = existing.GetComponent<Image>();
if (_backgroundImage != null) return;
if (existing != null) Object.DestroyImmediate(existing.gameObject);
var go = new GameObject(BackgroundGoName, typeof(RectTransform)); go.hideFlags = HideFlags.HideAndDontSave; go.transform.SetParent(root, false); go.transform.SetAsFirstSibling();
var rect = go.GetComponent<RectTransform>(); rect.anchorMin = Vector2.zero; rect.anchorMax = Vector2.one; rect.offsetMin = Vector2.zero; rect.offsetMax = Vector2.zero;
_backgroundImage = go.AddComponent<Image>(); _backgroundImage.raycastTarget = true; _backgroundImage.maskable = false;
}
private void RebindMaterial(QuickGraphGridRenderStyle style, bool force)
{
if (_backgroundImage == null) return;
Material target = style.Material;
QuickGraphGridBackgroundConfig owner = null;
string ownedShader = null;
if (target == null)
{
// 旧 Config 仅在 style 未提供共享材质时参与回退;其缓存材质为唯一可释放对象。
if (style.ShaderName == Config.ShaderName) { target = Config.GetOrCreateMaterial(); owner = Config.OverrideMaterial == null ? Config : null; }
else if (!string.IsNullOrEmpty(style.ShaderName))
{
if (_backgroundMaterial != null && _ownedShaderName == style.ShaderName) return;
var shader = Shader.Find(style.ShaderName); if (shader != null) { target = new Material(shader) { name = "BpGridBackground_StyleMat" }; ownedShader = style.ShaderName; }
}
}
if (target == _backgroundMaterial) return;
ReleaseOwnedMaterial();
_backgroundMaterial = target; _ownedMaterialConfig = owner; _ownedShaderName = ownedShader;
if (_backgroundImage != null) _backgroundImage.material = _backgroundMaterial;
}
private void ApplyStyle(QuickGraphGridRenderStyle style)
{
if (_backgroundMaterial == null || Graph?.Canvas == null || _backgroundImage == null) return;
var size = _backgroundImage.rectTransform.rect.size; var zoom = Graph.Canvas.ZoomLevel; var pan = Graph.Canvas.PanOffset; var gridSize = Mathf.Max(.0001f, style.Size);
_backgroundMaterial.SetVector("_Transform", new Vector4(size.x / (zoom * gridSize), size.y / (zoom * gridSize), -(pan.x + .5f * size.x) / (zoom * gridSize), -(pan.y + .5f * size.y) / (zoom * gridSize)));
_backgroundMaterial.SetFloat("_GridOverlayPower", style.OverlayPower); _backgroundMaterial.SetFloat("_GridLineThreshold", style.Threshold); _backgroundMaterial.SetFloat("_GridExp", style.Exp);
_backgroundMaterial.SetColor("_GridColor", style.Color); _backgroundMaterial.SetColor("_GridBackgroundColor", style.Background);
}
private void ReleaseOwnedMaterial()
{
if (_backgroundImage != null) _backgroundImage.material = null;
if (_ownedMaterialConfig != null) _ownedMaterialConfig.ReleaseMaterial();
else if (_backgroundMaterial != null && !string.IsNullOrEmpty(_ownedShaderName)) { if (Application.isPlaying) Object.Destroy(_backgroundMaterial); else Object.DestroyImmediate(_backgroundMaterial); }
_backgroundMaterial = null; _ownedMaterialConfig = null; _ownedShaderName = null;
}
public override void OnDestroy()
{
if (_styleResolver != null) _styleResolver.Changed -= OnStyleChanged;
_styleResolver?.Dispose(); _styleResolver = null; ReleaseOwnedMaterial();
if (_backgroundImage != null) { if (Application.isPlaying) Object.Destroy(_backgroundImage.gameObject); else Object.DestroyImmediate(_backgroundImage.gameObject); _backgroundImage = null; }
_defaultConfig = null;
}
private const string Name = "__Bp_GridBackground"; private QuickGraphRenderStyleResolver _styles; private Image _image; private Material _material; private string _ownedShader; private bool _transformErrorLogged; private bool _materialUnavailableErrorLogged;
public override void OnInitialize() { _styles = new QuickGraphRenderStyleResolver(Graph); _styles.Changed += _ => Graph?.MarkDirty(); Ensure(); Rebind(_styles.Snapshot.Grid); }
public override void OnConfigChanged() => _styles?.Refresh();
public override void OnRender() { Ensure(); if (_styles == null) return; var style = _styles.Snapshot.Grid; Rebind(style); Apply(style); }
private void Ensure() { if (_image != null || Graph?.Canvas?.GetRootTransform() == null) return; var root = Graph.Canvas.GetRootTransform(); var existing = root.Find(Name); if (existing != null) _image = existing.GetComponent<Image>(); if (_image == null) { if (existing != null) Object.DestroyImmediate(existing.gameObject); var go = new GameObject(Name, typeof(RectTransform), typeof(Image)); go.hideFlags = HideFlags.HideAndDontSave; go.transform.SetParent(root, false); go.transform.SetAsFirstSibling(); var rt = go.GetComponent<RectTransform>(); rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one; rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero; _image = go.GetComponent<Image>(); } }
private void Rebind(QuickGraphGridRenderStyle style) { if (_image == null) return; Material target = style.Material; string owned = null; if (target == null && !string.IsNullOrEmpty(style.ShaderName)) { if (_material != null && _ownedShader == style.ShaderName) return; var shader = Shader.Find(style.ShaderName); if (shader != null) { target = new Material(shader) { name = "BpGridBackground_StyleMat" }; owned = style.ShaderName; } } if (target == null) { if (!_materialUnavailableErrorLogged) { Debug.LogError($"[QuickGraph] 网格 material 与 shader 均不可用: GraphId={Graph?.GraphId}, material={style.Material}, shader=\"{style.ShaderName}\"。"); _materialUnavailableErrorLogged = true; } } else _materialUnavailableErrorLogged = false; if (target == _material) return; ReleaseMaterial(); _material = target; _ownedShader = owned; _image.material = target; }
private void Apply(QuickGraphGridRenderStyle style) { if (_image == null) return; _image.raycastTarget = style.RaycastTarget; _image.maskable = style.Maskable; if (_material == null || Graph?.Canvas == null) return; var size = _image.rectTransform.rect.size; var zoom = Graph.Canvas.ZoomLevel; if (style.Size != 0f && zoom != 0f) { var pan = Graph.Canvas.PanOffset; _material.SetVector("_Transform", new Vector4(size.x / (zoom * style.Size), size.y / (zoom * style.Size), -(pan.x + .5f * size.x) / (zoom * style.Size), -(pan.y + .5f * size.y) / (zoom * style.Size))); _transformErrorLogged = false; } else if (!_transformErrorLogged) { Debug.LogError("[QuickGraph] 网格样式 size 或 zoom 为 0,未写入 _Transform。"); _transformErrorLogged = true; } _material.SetFloat("_GridOverlayPower", style.OverlayPower); _material.SetFloat("_GridLineThreshold", style.Threshold); _material.SetFloat("_GridExp", style.Exp); _material.SetColor("_GridColor", style.Color); _material.SetColor("_GridBackgroundColor", style.Background); _material.SetFloat("_LodTransthd", style.LodTransitionThreshold); _material.SetFloat("_LodTransitionStart", style.LodTransitionStart); _material.SetFloat("_LodTransitionEnd", style.LodTransitionEnd); _material.SetVector("_CellCenter", style.CellCenter); _material.SetFloat("_LineShapeScale", style.LineShapeScale); }
private void ReleaseMaterial() { if (_image != null) _image.material = null; if (_material != null && _ownedShader != null) { if (Application.isPlaying) Object.Destroy(_material); else Object.DestroyImmediate(_material); } _material = null; _ownedShader = null; }
public override void OnDestroy() { _styles?.Dispose(); ReleaseMaterial(); if (_image != null) { if (Application.isPlaying) Object.Destroy(_image.gameObject); else Object.DestroyImmediate(_image.gameObject); } _image = null; }
}
}
@@ -1,82 +1,21 @@
using System.Collections.Generic;
using UnityEngine;
using XericLibrary.Runtime.Blueprint.Render;
using XericLibrary.Runtime.UIGraph;
namespace XericLibrary.Runtime.Blueprint.QuickGraph
{
/// <summary>按源节点 Chunk 提交连线的渲染工具;所有局部坐标由基类 submission 提供。</summary>
[BlueprintTool(phase: ToolPhase.Render, order: 110)]
[BlueprintTheme("QuickGraph")]
[BlueprintTool(phase: ToolPhase.Render, order: 110)] [BlueprintTheme("QuickGraph")]
public class WireCanvasRenderTool : BlueprintCanvasRenderTool<BlueprintCurveRenderer>
{
private QuickGraphWireRenderConfig _defaultConfig;
private QuickGraphRenderStyleResolver _styleResolver;
private int _appliedStyleRevision = -1;
private QuickGraphWireRenderConfig Config
{
get
{
if (ToolConfig is QuickGraphWireRenderConfig external) return external;
if (_defaultConfig == null) _defaultConfig = ScriptableObject.CreateInstance<QuickGraphWireRenderConfig>();
return _defaultConfig;
}
}
public override void OnInitialize()
{
base.OnInitialize();
_styleResolver = new QuickGraphRenderStyleResolver(Graph, wireFallback: ConfigStyle);
_styleResolver.Changed += OnStyleChanged;
ApplyWireGeometry(_styleResolver.Snapshot.Wire, true);
}
public override void OnConfigChanged()
{
_styleResolver?.Refresh();
if (_styleResolver == null) ApplyWireGeometry(ConfigStyle(), true);
base.OnConfigChanged();
}
private QuickGraphWireRenderStyle ConfigStyle()
{
var cfg = Config;
return new QuickGraphWireRenderStyle { Width = cfg.WireWidth, HitRadius = cfg.HitTestRadius, SourceHandle = cfg.SourceHandleLength, TargetHandle = cfg.TargetHandleLength, ArrowShape = cfg.ArrowShape, ArrowReversed = cfg.ArrowReversed, ArrowProgress = cfg.ArrowProgress, ArrowWidth = cfg.ArrowWidth, ArrowHeight = cfg.ArrowHeight, ArrowDepth = cfg.ArrowDepthCompensation, Tessellation = cfg.TessellationSegments };
}
private void OnStyleChanged(QuickGraphRenderStyleSnapshot snapshot)
{
if ((snapshot.Changes & (QuickGraphRenderStyleChange.WireGeometry | QuickGraphRenderStyleChange.WireAppearance)) == 0) return;
ApplyWireGeometry(snapshot.Wire, (snapshot.Changes & QuickGraphRenderStyleChange.WireGeometry) != 0);
Graph?.MarkAllElementsDirty();
}
private void ApplyWireGeometry(QuickGraphWireRenderStyle style, bool force)
{
if (Graph == null || (!force && _appliedStyleRevision == _styleResolver.Snapshot.Revision)) return;
Graph.SetWireGeometrySettings(new BlueprintWireGeometrySettings { SourceHandleLength = style.SourceHandle, TargetHandleLength = style.TargetHandle, WireWidth = style.Width, HitTestRadius = style.HitRadius });
foreach (var wire in Graph.Wires) Graph.UpdateWireGeometry(wire);
_appliedStyleRevision = _styleResolver != null ? _styleResolver.Snapshot.Revision : _appliedStyleRevision + 1;
}
protected override BlueprintCurveRenderer BuildChunkRenderer(GameObject chunkGo) => chunkGo.AddComponent<BlueprintCurveRenderer>();
protected override void ClearChunkData(BlueprintCurveRenderer renderer) => renderer.ClearAll();
protected override void SetChunkDirty(BlueprintCurveRenderer renderer) => renderer.RebuildAll();
protected override bool AcceptsElement(IBlueprintElement element) => element is BlueprintWire;
protected override void AppendElementData(BlueprintCurveRenderer renderer, IBlueprintElement element, ulong chunkID)
{
if (!(element is BlueprintWire wire) || wire.SourcePort == null || wire.TargetPort == null || wire.SourcePort.OwnerNode == null || wire.TargetPort.OwnerNode == null) return;
var style = _styleResolver != null ? _styleResolver.Snapshot.Wire : ConfigStyle();
wire.GetControlPoints(out var startCanvas, out var handle1Canvas, out var handle2Canvas, out var endCanvas);
var submission = CurrentElementSubmission;
var ctrlPts = new[] { submission.ToChunkLocal(startCanvas), submission.ToChunkLocal(handle1Canvas), submission.ToChunkLocal(handle2Canvas), submission.ToChunkLocal(endCanvas) };
var widths = new[] { style.Width, style.Width, style.Width, style.Width };
Color32 srcColor = wire.SourcePort.OwnerNode.NodeColor;
Color32 tgtColor = wire.TargetPort.OwnerNode.NodeColor;
var arrow = new ArrowHeadData { shape = style.ArrowShape, reversed = style.ArrowReversed, progress = style.ArrowProgress, width = style.ArrowWidth, height = style.ArrowHeight, depthCompensation = style.ArrowDepth, color = tgtColor };
var entry = new CurveCacheEntry { startColor = srcColor, endColor = tgtColor, tessellationSegments = Mathf.Max(2, Mathf.RoundToInt(style.Tessellation * Graph.CurrentLodSettings.WireTessellationMultiplier)) };
renderer.AddCurve(entry, ctrlPts, widths, new[] { arrow });
}
public override void OnDestroy()
{
if (_styleResolver != null) _styleResolver.Changed -= OnStyleChanged;
_styleResolver?.Dispose(); _styleResolver = null;
base.OnDestroy();
}
private QuickGraphRenderStyleResolver _styles; private int _revision = -1;
private readonly HashSet<string> _invalidWireWarningLogged = new HashSet<string>();
public override void OnInitialize() { base.OnInitialize(); _styles = new QuickGraphRenderStyleResolver(Graph); _styles.Changed += OnStyleChanged; ApplyGeometry(_styles.Snapshot.Wire, true); }
public override void OnConfigChanged() => _styles?.Refresh();
private void OnStyleChanged(QuickGraphRenderStyleSnapshot snapshot) { ApplyGeometry(snapshot.Wire, true); Graph?.MarkAllElementsDirty(); }
private void ApplyGeometry(QuickGraphWireRenderStyle style, bool force) { if (Graph == null || (!force && _revision == _styles.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); _revision = _styles.Snapshot.Revision; }
protected override BlueprintCurveRenderer BuildChunkRenderer(GameObject go) => go.AddComponent<BlueprintCurveRenderer>(); protected override void ClearChunkData(BlueprintCurveRenderer renderer) => renderer.ClearAll(); protected override void SetChunkDirty(BlueprintCurveRenderer renderer) => renderer.RebuildAll(); protected override bool AcceptsElement(IBlueprintElement element) => element is BlueprintWire;
protected override void AppendElementData(BlueprintCurveRenderer renderer, IBlueprintElement element, ulong chunkID) { if (!(element is BlueprintWire wire)) return; if (wire.SourcePort == null || wire.TargetPort == null || wire.SourcePort.OwnerNode == null || wire.TargetPort.OwnerNode == null) { ReportInvalidWire(wire); return; } var style = _styles.Snapshot.Wire; bool useNodeColorGradient = wire.GetStyleValue<bool>(QuickGraphStylePaths.WireUseNodeColorGradient); Color wireColor = wire.GetStyleValue<Color>(QuickGraphStylePaths.WireColor); wire.GetControlPoints(out var a, out var b, out var c, out var d); var submit = CurrentElementSubmission; var points = new[] { submit.ToChunkLocal(a), submit.ToChunkLocal(b), submit.ToChunkLocal(c), submit.ToChunkLocal(d) }; Color32 src = useNodeColorGradient ? wire.SourcePort.OwnerNode.NodeColor : wireColor; Color32 dst = useNodeColorGradient ? wire.TargetPort.OwnerNode.NodeColor : wireColor; var arrow = new ArrowHeadData { shape = style.ArrowShape, reversed = style.ArrowReversed, progress = style.ArrowProgress, width = style.ArrowWidth, height = style.ArrowHeight, depthCompensation = style.ArrowDepth, color = dst }; renderer.AddCurve(new CurveCacheEntry { startColor = src, endColor = dst, tessellationSegments = Mathf.RoundToInt(style.Tessellation * Graph.CurrentLodSettings.WireTessellationMultiplier) }, points, new[] { style.Width, style.Width, style.Width, style.Width }, new[] { arrow }); }
private void ReportInvalidWire(BlueprintWire wire) { if (!_invalidWireWarningLogged.Add(wire.ElementId)) return; var missing = new List<string>(); if (wire.SourcePort == null) missing.Add("SourcePort"); else if (wire.SourcePort.OwnerNode == null) missing.Add("SourcePort.OwnerNode"); if (wire.TargetPort == null) missing.Add("TargetPort"); else if (wire.TargetPort.OwnerNode == null) missing.Add("TargetPort.OwnerNode"); Debug.LogWarning($"[WireCanvasRenderTool] 跳过无效 wire: GraphId={Graph.GraphId}, WireId={wire.ElementId}, missing={string.Join(", ", missing)}。"); }
public override void OnDestroy() { if (_styles != null) _styles.Changed -= OnStyleChanged; _styles?.Dispose(); _styles = null; _invalidWireWarningLogged.Clear(); base.OnDestroy(); }
}
}
@@ -14,6 +14,8 @@ namespace XericLibrary.Runtime.Blueprint.Render
protected RectTransform ViewportRect;
private Vector2 _prevPan;
private float _prevZoom;
private readonly HashSet<ulong> _chunkRendererFailureLogged = new HashSet<ulong>();
private readonly HashSet<ulong> _zeroAcceptedSubmissionLogged = new HashSet<ulong>();
protected abstract TChildRenderer BuildChunkRenderer(GameObject chunkGo);
/// <summary>当前模板方法提交;子类不得重新计算 chunk 本地偏移。</summary>
@@ -40,13 +42,21 @@ namespace XericLibrary.Runtime.Blueprint.Render
{
if (submission.Kind == ChunkSubmissionKind.Exited) { RecycleChunk(submission.ChunkId); continue; }
var renderer = GetOrCreateChunk(submission.ChunkId);
if (renderer == null) continue;
if (renderer == null)
{
if (_chunkRendererFailureLogged.Add(submission.ChunkId))
Debug.LogError($"[BlueprintCanvasRenderTool] 创建 chunk renderer 失败: renderer={GetType().Name}, GraphId={Graph.GraphId}, chunk={submission.ChunkId}。");
continue;
}
ClearChunkData(renderer);
BeginChunkSubmission(submission.ChunkId, renderer);
var session = new RenderSession(Graph, submission);
int acceptedCount = 0;
if (submission.Elements != null)
foreach (var element in submission.Elements)
if (AcceptsElement(element)) { CurrentElementSubmission = session.CreateElementSubmission(element); AppendElementData(renderer, element, submission.ChunkId); }
if (AcceptsElement(element)) { acceptedCount++; CurrentElementSubmission = session.CreateElementSubmission(element); AppendElementData(renderer, element, submission.ChunkId); }
if (submission.Elements != null && submission.Elements.Count != 0 && acceptedCount == 0 && _zeroAcceptedSubmissionLogged.Add(submission.ChunkId))
Debug.LogWarning($"[BlueprintCanvasRenderTool] chunk 提交未接受任何元素: renderer={GetType().Name}, GraphId={Graph.GraphId}, chunk={submission.ChunkId}, elements={submission.Elements.Count}。");
CurrentElementSubmission = null;
EndChunkSubmission(submission.ChunkId, renderer);
SetChunkDirty(renderer);
@@ -123,11 +133,15 @@ namespace XericLibrary.Runtime.Blueprint.Render
if (ChunkPool.Count < MaxPoolSize) { renderer.gameObject.SetActive(false); ChunkPool.Push(renderer); }
else UnityEngine.Object.Destroy(renderer.gameObject);
ActiveChunks.Remove(id);
_chunkRendererFailureLogged.Remove(id);
_zeroAcceptedSubmissionLogged.Remove(id);
}
protected virtual void DestroyAllChunks()
{
foreach (var pair in ActiveChunks) if (pair.Value != null) UnityEngine.Object.Destroy(pair.Value.gameObject);
ActiveChunks.Clear();
_chunkRendererFailureLogged.Clear();
_zeroAcceptedSubmissionLogged.Clear();
while (ChunkPool.Count > 0) { var item = ChunkPool.Pop(); if (item != null) UnityEngine.Object.Destroy(item.gameObject); }
}
}
+73 -4
View File
@@ -1,15 +1,19 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using XericLibrary.Runtime.MacroLibrary;
using XericLibrary.Runtime.XericComponent;
namespace Deconstruction.Runtime
{
/// <summary>
/// Xeric 库的运行辅助类库
/// 程序运行辅助
/// </summary>
public static class XericHelper
{
#region
private static object _pusher = new object();
private static bool _isMinimized = false;
private static bool _isPrased = false;
/// <summary>
/// 离开出游戏模式(在编辑模式时仅退出)
@@ -26,6 +30,71 @@ namespace Deconstruction.Runtime
#endif
}
#endregion
/// <summary>
/// 最小化窗口
/// </summary>
public static void MinimizedWindow()
{
_isMinimized = true;
MacroForm.MinimizeWindow();
}
/// <summary>
/// 从最小化恢复窗口
/// </summary>
/// <returns></returns>
public static bool RestoreWindow()
{
if (!_isMinimized) return false;
_isMinimized = false;
MacroForm.RestoreWindow();
return true;
}
/// <summary>
/// 全屏
/// </summary>
public static void FullScreen()
{
if (RestoreWindow())
XericLifeCycleCore.SubmitTask(_pusher,
f => Screen.fullScreenMode = FullScreenMode.ExclusiveFullScreen,
phase: LifecyclePhase.Coroutine);
Screen.fullScreenMode = FullScreenMode.ExclusiveFullScreen;
}
/// <summary>
/// 全屏无边框窗口
/// </summary>
public static void FullScreenWindow()
{
if (RestoreWindow())
XericLifeCycleCore.SubmitTask(_pusher,
f => Screen.fullScreenMode = FullScreenMode.FullScreenWindow,
phase: LifecyclePhase.Coroutine);
Screen.fullScreenMode = FullScreenMode.FullScreenWindow;
}
/// <summary>
/// 最大化窗口
/// </summary>
public static void MaximizedWindow()
{
if (RestoreWindow())
XericLifeCycleCore.SubmitTask(_pusher,
f => Screen.fullScreenMode = FullScreenMode.MaximizedWindow,
phase: LifecyclePhase.Coroutine);
Screen.fullScreenMode = FullScreenMode.MaximizedWindow;
}
/// <summary>
/// 当前是否最小化
/// </summary>
/// <returns></returns>
public static bool IsMinimized()
{
// return UnityEditor.EditorApplication.isPaused;
return _isMinimized;
}
}
}
}
Binary file not shown.
Binary file not shown.
-7
View File
@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 5f497a3b416599744ba515971811704f
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+1 -1
View File
File diff suppressed because one or more lines are too long