update 0.6.4

This commit is contained in:
2026-08-10 14:17:25 +08:00
parent 2d703efdf4
commit 315e2eaa46
32 changed files with 553 additions and 1278 deletions
@@ -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>包括:
/// - _Transform(scaleX, scaleY, offsetX, offsetY):随画布 zoom / pan 同步更新;
/// - _GridOverlayPower、_GridLineThreshold、_GridExp;
/// - _GridColor、_GridBackgroundColor。</para>
/// 每次渲染时调用以保持与蓝图画布的缩放/平移同步。
/// </summary>
/// <param name="mat">目标材质实例</param>
/// <param name="rectSize">背景板 RectTransform 的像素尺寸 (width, height)</param>
/// <param name="zoom">当前画布缩放级别</param>
/// <param name="panOffset">当前画布平移偏移量</param>
public void ApplyToMaterial(Material mat, Vector2 rectSize, float zoom, Vector2 panOffset)
{
if (mat == null) return;
float sx = rectSize.x / (zoom * GridSize);
float sy = rectSize.y / (zoom * GridSize);
float ox = -(panOffset.x + 0.5f * rectSize.x) / (zoom * GridSize);
float oy = -(panOffset.y + 0.5f * rectSize.y) / (zoom * GridSize);
mat.SetVector("_Transform", new Vector4(sx, sy, ox, oy));
mat.SetFloat("_GridOverlayPower", GridOverlayPower);
mat.SetFloat("_GridLineThreshold", GridLineThreshold);
mat.SetFloat("_GridExp", GridExp);
mat.SetColor("_GridColor", GridColor);
mat.SetColor("_GridBackgroundColor", GridBackgroundColor);
}
/// <summary>
/// 释放此配置创建的材质实例(不释放 OverrideMaterial)。
/// </summary>
public void ReleaseMaterial()
{
if (_cachedMaterial != null)
{
#if UNITY_EDITOR
if (!Application.isPlaying)
DestroyImmediate(_cachedMaterial);
else
#endif
Destroy(_cachedMaterial);
_cachedMaterial = null;
}
}
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 38115e5721db6514bb8b8be6be10f26a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -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,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: 8038bf45323375b46a3a2820ddfe212e
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; 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 = style.UseNodeColorGradient ? wire.SourcePort.OwnerNode.NodeColor : style.Color; Color32 dst = style.UseNodeColorGradient ? wire.TargetPort.OwnerNode.NodeColor : style.Color; 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(); }
}
}