# update 0.6.2.1

更新了蓝图组件相关的用法。
This commit is contained in:
2026-07-15 10:36:16 +08:00
parent 155641f2ef
commit 7f3a12cae0
25 changed files with 1102 additions and 877 deletions
@@ -4,103 +4,88 @@ namespace XericLibrary.Runtime.Blueprint.Canvas
{
/// <summary>
/// 屏幕空间蓝图画布实现,基于 UGUI Canvas。
/// <code>
/// 使用 RectTransformUtility 处理屏幕坐标和画布坐标之间的转换。
/// <para>
/// 所有坐标转换统一通过 <see cref="RectTransformUtility"/> 完成,
/// 以 <see cref="_renderRoot"/> RectTransform 为坐标基准,
/// 不依赖蓝图自身的 RectTransform 是否撑满全屏。
/// </para>
/// <para>
/// 支持 ScreenSpaceOverlay / ScreenSpaceCamera / WorldSpace 三种渲染模式。
/// 由外层的 Mask(__Bp_GridBackground)裁剪,确保蓝图节点不溢出视图区域。
/// 缩放范围:0.1x ~ 3x。
/// </code>
/// </para>
/// </summary>
public class ScreenSpaceBlueprintCanvas : BlueprintCanvasBase
{
private readonly UnityEngine.Canvas _unityCanvas;
private readonly RectTransform _rectTransform;
private readonly RectTransform _renderRoot;
/// <summary>动态获取摄像机引用(不缓存),
/// 自动适应 Canvas.renderMode 运行时的变化。</summary>
private Camera WorldCamera => _unityCanvas != null ? _unityCanvas.worldCamera : null;
/// <summary>
/// 构造屏幕空间蓝图画布
/// </summary>
/// <param name="unityCanvas">Unity UGUI Canvas 组件</param>
public ScreenSpaceBlueprintCanvas(UnityEngine.Canvas unityCanvas)
/// <param name="unityCanvas">所属的 Unity UGUI Canvas 组件</param>
/// <param name="renderRoot">蓝图渲染根 RectTransform,所有蓝图层容器和背景挂载于此</param>
public ScreenSpaceBlueprintCanvas(UnityEngine.Canvas unityCanvas, RectTransform renderRoot)
{
_unityCanvas = unityCanvas;
_rectTransform = unityCanvas.GetComponent<RectTransform>();
_renderRoot = renderRoot;
}
/// <summary>
/// 将屏幕坐标转换为画布坐标
/// 将屏幕坐标转换为画布坐标。
/// 通过 <see cref="RectTransformUtility.ScreenPointToLocalPointInRectangle"/>
/// 以 <see cref="_renderRoot"/> 为参照转换,
/// 自动处理 Canvas 渲染模式和 RenderRoot 的偏移。
/// </summary>
/// <param name="screenPoint">屏幕空间的坐标点</param>
/// <returns>画布空间的坐标点</returns>
public override Vector2 ScreenToCanvas(Vector2 screenPoint)
{
RectTransformUtility.ScreenPointToLocalPointInRectangle(
_rectTransform, screenPoint, _unityCanvas.worldCamera, out Vector2 localPoint);
_renderRoot, screenPoint, WorldCamera, out Vector2 localPoint);
return (localPoint - _panOffset) / _zoomLevel;
}
/// <summary>
/// 将画布坐标转换为屏幕坐标
/// 将画布坐标转换为屏幕坐标。
/// 先计算 RenderRoot 本地空间位置,再通过 RectTransformUtility 转换为屏幕坐标。
/// </summary>
/// <param name="canvasPoint">画布空间的坐标点</param>
/// <returns>屏幕空间的坐标点</returns>
public override Vector2 CanvasToScreen(Vector2 canvasPoint)
{
Vector2 local = canvasPoint * _zoomLevel + _panOffset;
Vector3 world = _rectTransform.TransformPoint(local);
if (_unityCanvas.worldCamera != null)
{
return _unityCanvas.worldCamera.WorldToScreenPoint(world);
}
return world;
Vector3 world = _renderRoot.TransformPoint(local);
return RectTransformUtility.WorldToScreenPoint(WorldCamera, world);
}
/// <summary>
/// 将画布坐标转换为世界坐标
/// <code>
/// 屏幕空间画布的世界空间转换依赖于 Canvas 的渲染模式:
/// Screen Space - Overlay 时直接使用 localPoint 作为屏幕坐标;
/// Screen Space - Camera 时通过 camera 进行坐标转换。
/// </code>
/// 将画布坐标转换为世界坐标。
/// </summary>
/// <param name="canvasPoint">画布空间的坐标点</param>
/// <returns>世界空间的坐标点</returns>
public override Vector3 CanvasToWorld(Vector2 canvasPoint)
{
Vector2 local = canvasPoint * _zoomLevel + _panOffset;
Vector3 world = _rectTransform.TransformPoint(local);
if (_unityCanvas.worldCamera != null)
{
Vector3 screenPoint = _unityCanvas.worldCamera.WorldToScreenPoint(world);
return _unityCanvas.worldCamera.ScreenToWorldPoint(screenPoint);
}
return world;
return _renderRoot.TransformPoint(local);
}
/// <summary>
/// 将世界坐标转换为画布坐标
/// 将世界坐标转换为画布坐标。
/// 先转换为屏幕坐标,再通过 RectTransformUtility 转换为 RenderRoot 本地坐标。
/// </summary>
/// <param name="worldPoint">世界空间的坐标点</param>
/// <returns>画布空间的坐标点</returns>
public override Vector2 WorldToCanvas(Vector3 worldPoint)
{
if (_unityCanvas.worldCamera != null)
{
Vector3 screenPoint = _unityCanvas.worldCamera.WorldToScreenPoint(worldPoint);
RectTransformUtility.ScreenPointToLocalPointInRectangle(
_rectTransform, screenPoint, _unityCanvas.worldCamera, out Vector2 localPoint);
return (localPoint - _panOffset) / _zoomLevel;
}
Vector3 local = _rectTransform.InverseTransformPoint(worldPoint);
return (new Vector2(local.x, local.y) - _panOffset) / _zoomLevel;
Vector3 screenPoint = RectTransformUtility.WorldToScreenPoint(WorldCamera, worldPoint);
RectTransformUtility.ScreenPointToLocalPointInRectangle(
_renderRoot, screenPoint, WorldCamera, out Vector2 localPoint);
return (localPoint - _panOffset) / _zoomLevel;
}
/// <summary>
/// 获取渲染根 Transform(UGUI Canvas 的 RectTransform)
/// 获取渲染根 Transform(蓝图层容器和背景的父级)
/// </summary>
public override Transform GetRootTransform()
{
return _rectTransform;
return _renderRoot;
}
/// <summary>
@@ -110,5 +95,10 @@ namespace XericLibrary.Runtime.Blueprint.Canvas
{
get { return _unityCanvas; }
}
/// <summary>
/// Canvas 组件未被销毁,画布可正常使用。
/// </summary>
public override bool IsAlive => _unityCanvas != null;
}
}
@@ -100,5 +100,10 @@ namespace XericLibrary.Runtime.Blueprint.Canvas
{
return _canvasTransform;
}
/// <summary>
/// Transform 组件未被销毁,画布可正常使用。
/// </summary>
public override bool IsAlive => _canvasTransform != null;
}
}
@@ -14,7 +14,6 @@ namespace XericLibrary.Runtime.Blueprint
{
/// <summary>
/// 图论蓝图组件 —— 在一个 UGUI Canvas 上挂载蓝图系统。
/// 使用 ExecuteAlways 在编辑器中也会运行,方便预览。
/// <para>驱动方式:
/// - 编辑器非运行态:EditorApplication.update → EditorTick
/// - 运行时:MonoBehaviour.Update</para>
@@ -45,13 +44,21 @@ namespace XericLibrary.Runtime.Blueprint
{
if (_inputActions == value) return;
#if ENABLE_INPUT_SYSTEM
if (_inputActions is InputActionAsset oldAsset)
BlueprintInputManager.UnregisterAsset(oldAsset);
if (Graph != null && _inputActions is InputActionAsset oldAsset)
{
var oldTool = BlueprintToolProvider.GetToolByType<BlueprintInputTool_InputSystem>(Graph);
if (oldTool != null && oldTool.InputActionAsset == oldAsset)
oldTool.InputActionAsset = null;
}
#endif
_inputActions = value;
#if ENABLE_INPUT_SYSTEM
if (value is InputActionAsset newAsset)
BlueprintInputManager.RegisterAsset(newAsset);
if (Graph != null && value is InputActionAsset newAsset)
{
var newTool = BlueprintToolProvider.GetToolByType<BlueprintInputTool_InputSystem>(Graph);
if (newTool != null)
newTool.InputActionAsset = newAsset;
}
#endif
}
}
@@ -62,7 +69,7 @@ namespace XericLibrary.Runtime.Blueprint
[Header("Test")]
[Tooltip("是否自动生成测试节点")]
[SerializeField] private bool _autoGenerateTestNodes = true;
[SerializeField] public bool _autoGenerateTestNodes = true;
[Header("Debug")]
[Tooltip("显示所有隐藏的运行时对象(HideFlags),用于调试。默认关闭。")]
@@ -111,16 +118,36 @@ namespace XericLibrary.Runtime.Blueprint
}
else
{
var unityCanvas = GetComponent<UnityEngine.Canvas>();
if (unityCanvas == null)
// 1) 找父级 Canvas(不在自身挂 Canvas)
var parentCanvas = GetComponentInParent<UnityEngine.Canvas>();
if (parentCanvas == null)
{
unityCanvas = gameObject.AddComponent<UnityEngine.Canvas>();
unityCanvas.renderMode = RenderMode.ScreenSpaceOverlay;
// 场景中无 Canvas → 创建全屏 Canvas,将自身移入
var canvasGo = new GameObject("Blueprint Canvas",
typeof(UnityEngine.Canvas),
typeof(UnityEngine.UI.CanvasScaler),
typeof(UnityEngine.UI.GraphicRaycaster));
var cc = canvasGo.GetComponent<UnityEngine.Canvas>();
cc.renderMode = RenderMode.ScreenSpaceOverlay;
canvasGo.transform.SetParent(transform.parent, false);
canvasGo.transform.SetAsLastSibling();
transform.SetParent(canvasGo.transform, false);
parentCanvas = cc;
}
if (GetComponent<UnityEngine.UI.GraphicRaycaster>() == null)
gameObject.AddComponent<UnityEngine.UI.GraphicRaycaster>();
Canvas = new ScreenSpaceBlueprintCanvas(unityCanvas);
// 2) 创建渲染根子项(所有蓝图层挂载于此,由外层的 Mask 裁剪)
var renderRootGo = new GameObject("__Bp_RenderRoot",
typeof(RectTransform));
renderRootGo.hideFlags = HideFlags.DontSave;
var renderRootRt = renderRootGo.GetComponent<RectTransform>();
renderRootRt.SetParent(transform, false);
renderRootRt.anchorMin = Vector2.zero;
renderRootRt.anchorMax = Vector2.one;
renderRootRt.offsetMin = Vector2.zero;
renderRootRt.offsetMax = Vector2.zero;
// 3) 以父级 Canvas + 渲染根构建画布适配器
Canvas = new ScreenSpaceBlueprintCanvas(parentCanvas, renderRootRt);
}
}
@@ -143,16 +170,15 @@ namespace XericLibrary.Runtime.Blueprint
private void SetupInput()
{
#if ENABLE_INPUT_SYSTEM
EnsureEventSystem(_inputActions as InputActionAsset);
if (_inputActions is InputActionAsset asset)
{
BlueprintInputManager.RegisterAsset(asset);
EnsureEventSystem(asset);
// 将 InputActionAsset 传递给输入工具(由工具自身管理绑定)
var tool = BlueprintToolProvider.GetToolByType<BlueprintInputTool_InputSystem>(Graph);
if (tool != null)
tool.InputActionAsset = asset;
}
#else
EnsureEventSystem(null);
#endif
BlueprintInputManager.SetFocusedGraph(Graph);
}
#if ENABLE_INPUT_SYSTEM
@@ -190,7 +216,7 @@ namespace XericLibrary.Runtime.Blueprint
else
#endif
{
if (eventSys.currentInputModule == null)
if (eventSys.GetComponent<StandaloneInputModule>() == null)
{
eventSys.gameObject.AddComponent<StandaloneInputModule>();
}
@@ -277,15 +303,12 @@ namespace XericLibrary.Runtime.Blueprint
var node1 = new GraphTheoryNode("Node 1", Color.red);
node1.Position = new Vector2(startX, centerY);
node1.NodeSize = new Vector2(90, 130);
var node2 = new GraphTheoryNode("Node 2", Color.green);
node2.Position = new Vector2(startX + spacing, centerY);
node2.NodeSize = new Vector2(90, 130);
var node3 = new GraphTheoryNode("Node 3", Color.blue);
node3.Position = new Vector2(startX + spacing * 2, centerY);
node3.NodeSize = new Vector2(90, 130);
Graph.AddNode(node1);
Graph.AddNode(node2);
@@ -295,12 +318,10 @@ namespace XericLibrary.Runtime.Blueprint
Graph.TryConnectPorts(node2.OutputPorts[0], node3.InputPorts[0]);
var node4 = new GraphTheoryNode("Node 4", Color.yellow);
node4.Position = new Vector2(startX + spacing, centerY + branchOffset);
node4.NodeSize = new Vector2(90, 130);
node4.Position = new Vector2(startX + spacing * 2, centerY + branchOffset);
var node5 = new GraphTheoryNode("Node 5", Color.cyan);
node5.Position = new Vector2(startX + spacing, centerY - branchOffset);
node5.NodeSize = new Vector2(90, 130);
node5.Position = new Vector2(startX + spacing * 2, centerY - branchOffset);
Graph.AddNode(node4);
Graph.AddNode(node5);
@@ -316,12 +337,6 @@ namespace XericLibrary.Runtime.Blueprint
#if UNITY_EDITOR
EditorApplication.update -= EditorTick;
#endif
BlueprintInputManager.SetFocusedGraph(null);
#if ENABLE_INPUT_SYSTEM
if (_inputActions is InputActionAsset asset)
BlueprintInputManager.UnregisterAsset(asset);
#endif
if (Graph != null)
{
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using XericLibrary.Runtime.Blueprint.Render;
namespace XericLibrary.Runtime.Blueprint
{
@@ -9,6 +10,10 @@ namespace XericLibrary.Runtime.Blueprint
/// 释放时计算框选范围内所有元素的交集并输出统计日志。
/// <para>选框 UGUI Image 默认渲染在所有层之上(SetAsLastSibling)。</para>
/// <para>框选结果保存在 <see cref="SelectedElements"/> 中供其他工具读取。</para>
/// <para>
/// 选框仅在拖拽距离超过 <c>QuickGraphInputConfig.BoxSelectThreshold</c> 后才显示,
/// 避免误触显示闪烁。
/// </para>
/// </summary>
[BlueprintTool(phase: ToolPhase.PreUpdate, order: 60)]
[BlueprintTheme("QuickGraph")]
@@ -19,19 +24,44 @@ namespace XericLibrary.Runtime.Blueprint
private Vector2 _dragStartCanvas; // 按下时的画布坐标
private Vector2 _dragEndCanvas; // 当前的画布坐标
/// <summary>选框 GameObject 是否已创建并可见(超过阈值后)</summary>
private bool _boxVisible;
// 选框 UI
private GameObject _selectionBoxGo;
private Image _selectionBoxImage;
private RectTransform _selectionBoxRt;
// ---------- 配置 ----------
private QuickGraphInputConfig _boxConfig;
private QuickGraphInputConfig BoxConfig
{
get
{
if (_boxConfig == null && Graph != null)
{
foreach (var c in Graph.ConfigAssets)
{
if (c is QuickGraphInputConfig qc) { _boxConfig = qc; break; }
}
if (_boxConfig == null)
{
_boxConfig = ScriptableObject.CreateInstance<QuickGraphInputConfig>();
}
}
return _boxConfig;
}
}
// ---------- 结果 ----------
/// <summary>本次框选选中的元素列表。</summary>
public List<IBlueprintElement> SelectedElements { get; } = new List<IBlueprintElement>();
/// <summary>结果:通过 Provider.CurrentSelectElements 读取框选结果。</summary>
// ===== 鼠标按下 =====
public override void OnPointerDown(Vector2 canvasPoint, int mouseButton)
public override void OnPointerDown(GraphInputTool sender, Vector2 canvasPoint, int mouseButton)
{
if (Graph == null) return;
if (mouseButton != 0) return; // 仅左键
@@ -41,31 +71,55 @@ namespace XericLibrary.Runtime.Blueprint
if (hit != null) return;
_isBoxSelecting = true;
_boxVisible = false;
_dragStartCanvas = canvasPoint;
_dragEndCanvas = canvasPoint;
// 创建选框 UI
CreateSelectionBox();
}
// ===== 拖拽 =====
public override void OnPointerDrag(Vector2 canvasPoint, Vector2 delta, int mouseButton)
public override void OnPointerDrag(GraphInputTool sender, Vector2 canvasPoint, Vector2 delta, int mouseButton)
{
if (!_isBoxSelecting || mouseButton != 0) return;
_dragEndCanvas = canvasPoint;
UpdateSelectionBox();
// 检查拖拽距离是否超过阈值
float threshold = BoxConfig.BoxSelectThreshold;
float dragDist = Vector2.Distance(_dragStartCanvas, _dragEndCanvas);
if (!_boxVisible && dragDist > threshold)
{
// 首次超过阈值 → 创建选框
_boxVisible = true;
CreateSelectionBox();
}
if (_boxVisible)
{
UpdateSelectionBox();
}
}
// ===== 释放 =====
public override void OnPointerUp(Vector2 canvasPoint, int mouseButton)
public override void OnPointerUp(GraphInputTool sender, Vector2 canvasPoint, int mouseButton)
{
if (!_isBoxSelecting || mouseButton != 0) return;
_dragEndCanvas = canvasPoint;
FinishSelection();
if (_boxVisible)
{
FinishSelection();
}
else
{
// 未超过阈值 → 点击行为(框选起点处的元素)
SelectAtPoint(_dragStartCanvas);
}
_isBoxSelecting = false;
}
// ===== 销毁 =====
@@ -83,18 +137,24 @@ namespace XericLibrary.Runtime.Blueprint
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); // 半透明蓝
// 边框通过 Outline 或额外 Image 实现,直接使用透明填充 + 轮廓不好做,
// 使用两个 Image:填充(当前) + 边框(另一个,1px 白色)
// 为简化,在填充 Image 上挂一个 Outline 组件模拟边框
_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);
@@ -104,15 +164,15 @@ namespace XericLibrary.Runtime.Blueprint
{
if (_selectionBoxRt == null || Graph?.Canvas == null) return;
// 将画布坐标两角转为屏幕坐标
var startScreen = Graph.Canvas.CanvasToScreen(_dragStartCanvas);
var endScreen = Graph.Canvas.CanvasToScreen(_dragEndCanvas);
// 将画布坐标转为 RenderRoot 本地空间坐标(CanvasToLocal)
var startLocal = Graph.Canvas.CanvasToLocal(_dragStartCanvas);
var endLocal = Graph.Canvas.CanvasToLocal(_dragEndCanvas);
// 计算 RectTransform 位置和尺寸(屏幕空间)
var minX = Mathf.Min(startScreen.x, endScreen.x);
var minY = Mathf.Min(startScreen.y, endScreen.y);
var maxX = Mathf.Max(startScreen.x, endScreen.x);
var maxY = Mathf.Max(startScreen.y, endScreen.y);
// 计算 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);
@@ -120,11 +180,10 @@ namespace XericLibrary.Runtime.Blueprint
private void FinishSelection()
{
_isBoxSelecting = false;
UpdateSelectionBox();
// 计算选框内的元素(画布坐标空间)
SelectedElements.Clear();
BlueprintToolProvider.CurrentSelectElements.Clear();
var min = new Vector2(
Mathf.Min(_dragStartCanvas.x, _dragEndCanvas.x),
Mathf.Min(_dragStartCanvas.y, _dragEndCanvas.y));
@@ -138,7 +197,7 @@ namespace XericLibrary.Runtime.Blueprint
if (element == null) continue;
if (element.BoundingBox.Overlaps(selectRect))
{
SelectedElements.Add(element);
BlueprintToolProvider.CurrentSelectElements.Add(element);
}
}
@@ -146,6 +205,16 @@ namespace XericLibrary.Runtime.Blueprint
DestroySelectionBox();
}
private void SelectAtPoint(Vector2 canvasPoint)
{
BlueprintToolProvider.CurrentSelectElements.Clear();
if (Graph == null) return;
var hit = Graph.HitTest(canvasPoint);
if (hit != null)
BlueprintToolProvider.CurrentSelectElements.Add(hit);
}
private void DestroySelectionBox()
{
if (_selectionBoxGo != null)
@@ -1,47 +1,84 @@
using XericLibrary.Runtime.MacroLibrary;
using UnityEngine;
namespace XericLibrary.Runtime.Blueprint
{
/// <summary>
/// 蓝图输入系统的 Action Map / Action 名称常量。
/// 供 Editor 的 InputAction 模板和 Runtime 的输入处理工具共同引用,
/// 避免字符串硬编码散布各处。
/// 蓝图输入系统的 Action Map / Action 名称常量与全局快捷键方案。
/// <para>
/// 快捷键通过 <see cref="MacroKey.SetShortcut(string, string)"/> 注册别名→组合键映射,
/// 由具体输入工具(如 <c>BlueprintInputTool_InputManager</c>)在初始化时通过
/// <see cref="ShortcutTree.RegisterAlias(string, Action)"/> 绑定实际动作后生效。
/// </para>
/// </summary>
public static class BlueprintInputConstants
{
// ── Action Map ──
public const string MapName = "Blueprint";
// ── 鼠标 / 指针 Actions(名称须与 UnityEngine.InputSystem.UI.InputSystemUIInputModule 官方预设一致)──
/// <summary>画布内指针位置(Vector2)。InputSystemUIInputModule 预设名称。</summary>
// ── 鼠标 / 指针 Actions ──
public const string Point = "Point";
/// <summary>滚轮滚动(Vector2)。InputSystemUIInputModule 预设名称。</summary>
public const string ScrollWheel = "ScrollWheel";
/// <summary>左键点击(Button)。InputSystemUIInputModule 预设名称。</summary>
public const string LeftClick = "LeftClick";
/// <summary>右键点击(Button)。InputSystemUIInputModule 预设名称。</summary>
public const string RightClick = "RightClick";
/// <summary>中键点击(Button)。InputSystemUIInputModule 预设名称。</summary>
public const string MiddleClick = "MiddleClick";
// ── 功能键 Actions ──
/// <summary>左 Shift(Button)</summary>
public const string Shift = "Shift";
/// <summary>左 Ctrl(Button)</summary>
public const string Ctrl = "Ctrl";
/// <summary>左 Alt(Button)</summary>
public const string Alt = "Alt";
// ── 快捷键组合 Actions(仅新输入系统)──
/// <summary>平移画布(Vector2,4方向)</summary>
// ── 快捷键组合 Actions ──
public const string Pan = "Pan";
/// <summary>缩放画布(float,滚轮 + Ctrl 组合)</summary>
public const string Zoom = "Zoom";
/// <summary>定位到核心节点</summary>
public const string FocusHome = "FocusHome";
/// <summary>撤销</summary>
public const string Undo = "Undo";
/// <summary>重做</summary>
public const string Redo = "Redo";
/// <summary>进入父级</summary>
public const string NavigateParent = "NavigateParent";
public const string DeleteSelected = "DeleteSelected";
public const string SelectAll = "SelectAll";
public const string Duplicate = "Duplicate";
public const string Copy = "Copy";
public const string Paste = "Paste";
public const string Cut = "Cut";
public const string Save = "Save";
// ===== 全局快捷键方案 =====
//
// 使用 MacroKey 的静态实例管理快捷键别名→组合键映射。
// 具体工具初始化时通过 MacroKey.ShortcutTreeInstance.RegisterAlias / RebindAlias
// 绑定实际动作委托。
/// <summary>全局快捷键树(只读,操作请通过 MacroKey 静态方法)。</summary>
public static ShortcutTree GlobalShortcuts => MacroKey.ShortcutTreeInstance;
// 静态构造器:在类首次加载时自动注册标准快捷键方案
static BlueprintInputConstants()
{
RegisterStandardShortcuts();
}
private static void RegisterStandardShortcuts()
{
MacroKey.SetShortcut(FocusHome, MacroKey.Combo(false, false, false, (char)KeyCode.Home));
MacroKey.SetShortcut(FocusHome, MacroKey.Combo(false, false, false, (char)KeyCode.F));
MacroKey.SetShortcut(Undo, MacroKey.Combo(false, true, false, 'Z'));
MacroKey.SetShortcut(Redo, MacroKey.Combo(true, true, false, 'Z'));
MacroKey.SetShortcut(Redo, MacroKey.Combo(false, true, false, 'Y'));
MacroKey.SetShortcut(DeleteSelected, MacroKey.Combo(false, false, false, (char)KeyCode.Delete));
MacroKey.SetShortcut(DeleteSelected, MacroKey.Combo(false, false, false, (char)KeyCode.Backspace));
MacroKey.SetShortcut(SelectAll, MacroKey.Combo(false, true, false, 'A'));
MacroKey.SetShortcut(Duplicate, MacroKey.Combo(false, true, false, 'D'));
MacroKey.SetShortcut(Copy, MacroKey.Combo(false, true, false, 'C'));
MacroKey.SetShortcut(Paste, MacroKey.Combo(false, true, false, 'V'));
MacroKey.SetShortcut(Cut, MacroKey.Combo(false, true, false, 'X'));
MacroKey.SetShortcut(Save, MacroKey.Combo(false, true, false, 'S'));
}
public static readonly string[] ActionNames = new[]
{
FocusHome, Undo, Redo, NavigateParent,
DeleteSelected, SelectAll, Duplicate, Copy, Paste, Cut, Save
};
}
}
@@ -1,188 +0,0 @@
#if ENABLE_INPUT_SYSTEM
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
namespace XericLibrary.Runtime.Blueprint
{
/// <summary>
/// 蓝图输入管理器(新输入系统)—— 管理 InputActionAsset 的引用计数与启用/停用。
/// <para>
/// 多个蓝图可能引用同一个 InputActionAsset,管理器确保:
/// - 首个蓝图注册时启用 asset 并绑定 Action 回调;
/// - 后续蓝图注册仅增加计数,不重复启用;
/// - 蓝图注销时减少计数,计数归零时才解绑并禁用 asset。
/// </para>
/// <para>
/// 蓝图组件在 Awake 时调用 <see cref="RegisterAsset"/>,OnDestroy 时调用
/// <see cref="UnregisterAsset"/>;运行时动态更换 asset 也通过这两方法。
/// </para>
/// </summary>
public static class BlueprintInputManager
{
/// <summary>asset → 引用计数</summary>
private static Dictionary<InputActionAsset, int> _refCounts
= new Dictionary<InputActionAsset, int>();
/// <summary>asset → 已绑定回调的 ActionMap</summary>
private static Dictionary<InputActionAsset, InputActionMap> _activeMaps
= new Dictionary<InputActionAsset, InputActionMap>();
// ===== 快捷键最新值(供 QuickGraphInputTool.OnUpdate 轮询) =====
/// <summary>Pan 动作最新的 Vector2 方向值(在 performed 中写入,canceled 中归零)。</summary>
public static Vector2 LastPanDirection;
/// <summary>Zoom 动作最新的 float 轴值(在 performed 中写入,canceled 中归零)。</summary>
public static float LastZoomAxis;
// 缓存的回调引用(确保 Bind/Unbind 使用同一委托实例)
private static readonly System.Action<InputAction.CallbackContext> s_panHandler
= ctx => LastPanDirection = ctx.ReadValue<Vector2>();
private static readonly System.Action<InputAction.CallbackContext> s_panCanceled
= _ => LastPanDirection = Vector2.zero;
private static readonly System.Action<InputAction.CallbackContext> s_zoomHandler
= ctx => LastZoomAxis = ctx.ReadValue<float>();
private static readonly System.Action<InputAction.CallbackContext> s_zoomCanceled
= _ => LastZoomAxis = 0f;
private static readonly System.Action<InputAction.CallbackContext> s_homeHandler
= _ => OnActionReceived(BlueprintInputConstants.FocusHome);
private static readonly System.Action<InputAction.CallbackContext> s_undoHandler
= _ => OnActionReceived(BlueprintInputConstants.Undo);
private static readonly System.Action<InputAction.CallbackContext> s_redoHandler
= _ => OnActionReceived(BlueprintInputConstants.Redo);
private static readonly System.Action<InputAction.CallbackContext> s_parentHandler
= _ => OnActionReceived(BlueprintInputConstants.NavigateParent);
// ===== 当前焦点蓝图(快捷键分发目标) =====
private static BlueprintGraph s_focusedGraph;
/// <summary>设置当前焦点蓝图(快捷键分发目标)。</summary>
public static void SetFocusedGraph(BlueprintGraph graph)
{
s_focusedGraph = graph;
}
// ===== 注册 / 注销 =====
/// <summary>
/// 注册一个蓝图对 InputActionAsset 的引用。
/// 若 asset 尚未启用,则启用并绑定快捷键回调。
/// 允许多个蓝图共享同一 asset。
/// </summary>
public static void RegisterAsset(InputActionAsset asset)
{
if (asset == null) return;
if (_refCounts.TryGetValue(asset, out int count))
{
_refCounts[asset] = count + 1;
return;
}
_refCounts[asset] = 1;
var map = asset.FindActionMap(BlueprintInputConstants.MapName);
if (map != null)
{
map.Enable();
BindActions(map);
_activeMaps[asset] = map;
}
}
/// <summary>
/// 注销一个蓝图对 InputActionAsset 的引用。
/// 引用计数归零时解绑回调并禁用 asset。
/// </summary>
public static void UnregisterAsset(InputActionAsset asset)
{
if (asset == null) return;
if (!_refCounts.TryGetValue(asset, out int count)) return;
count--;
if (count > 0)
{
_refCounts[asset] = count;
return;
}
_refCounts.Remove(asset);
if (_activeMaps.TryGetValue(asset, out var map))
{
UnbindActions(map);
map.Disable();
_activeMaps.Remove(asset);
}
}
// ===== Action 回调 =====
private static void BindActions(InputActionMap map)
{
Bind(map, BlueprintInputConstants.Pan, s_panHandler);
BindCancelled(map, BlueprintInputConstants.Pan, s_panCanceled);
Bind(map, BlueprintInputConstants.Zoom, s_zoomHandler);
BindCancelled(map, BlueprintInputConstants.Zoom, s_zoomCanceled);
Bind(map, BlueprintInputConstants.FocusHome,s_homeHandler);
Bind(map, BlueprintInputConstants.Undo, s_undoHandler);
Bind(map, BlueprintInputConstants.Redo, s_redoHandler);
Bind(map, BlueprintInputConstants.NavigateParent, s_parentHandler);
}
private static void UnbindActions(InputActionMap map)
{
Unbind(map, BlueprintInputConstants.Pan, s_panHandler);
UnbindCancelled(map, BlueprintInputConstants.Pan, s_panCanceled);
Unbind(map, BlueprintInputConstants.Zoom, s_zoomHandler);
UnbindCancelled(map, BlueprintInputConstants.Zoom, s_zoomCanceled);
Unbind(map, BlueprintInputConstants.FocusHome,s_homeHandler);
Unbind(map, BlueprintInputConstants.Undo, s_undoHandler);
Unbind(map, BlueprintInputConstants.Redo, s_redoHandler);
Unbind(map, BlueprintInputConstants.NavigateParent, s_parentHandler);
}
private static void Bind(InputActionMap map, string name, System.Action<InputAction.CallbackContext> handler)
{
var action = map.FindAction(name);
if (action != null)
action.performed += handler;
}
private static void Unbind(InputActionMap map, string name, System.Action<InputAction.CallbackContext> handler)
{
var action = map.FindAction(name);
if (action != null)
action.performed -= handler;
}
private static void BindCancelled(InputActionMap map, string name, System.Action<InputAction.CallbackContext> handler)
{
var action = map.FindAction(name);
if (action != null)
action.canceled += handler;
}
private static void UnbindCancelled(InputActionMap map, string name, System.Action<InputAction.CallbackContext> handler)
{
var action = map.FindAction(name);
if (action != null)
action.canceled -= handler;
}
private static void OnActionReceived(string actionName)
{
if (s_focusedGraph != null)
{
s_focusedGraph.MarkDirty();
BlueprintToolProvider.DispatchAction(s_focusedGraph, actionName);
}
}
}
}
#endif
@@ -1,135 +0,0 @@
#if !ENABLE_INPUT_SYSTEM
using UnityEngine;
namespace XericLibrary.Runtime.Blueprint
{
/// <summary>
/// 蓝图输入管理器(旧输入系统)—— 通过旧输入系统轮询鼠标和键盘事件。
/// <para>
/// 提供与 <c>BlueprintInputManager.InputSystem.cs</c> 相同的静态 API 签名,
/// 但底层使用 <see cref="Input.GetMouseButton"/>/<see cref="Input.GetKey"/> 等旧 API,
/// 不依赖任何新输入系统类型。
/// </para>
/// <para>
/// 鼠标点击、拖拽、右键等事件通过 <see cref="PollMouseEvents"/> 轮询后
/// 分发到 <see cref="BlueprintToolProvider"/> 的 DispatchXxx 方法。
/// 键盘 Pan/Zoom 值通过 <see cref="PollKeyboard"/> 轮询后由
/// <see cref="QuickGraphInputTool"/> 在 OnUpdate 中读取。
/// </para>
/// </summary>
public static class BlueprintInputManager
{
/// <summary>
/// 当前焦点蓝图(快捷键分发目标)。
/// 由 <see cref="SetFocusedGraph"/> 设置。
/// </summary>
private static BlueprintGraph s_focusedGraph;
/// <summary>设置当前焦点蓝图。</summary>
public static void SetFocusedGraph(BlueprintGraph graph)
{
s_focusedGraph = graph;
}
/// <summary>
/// 注册 InputActionAsset(旧系统空操作)。
/// 仅保留签名以兼容编译。
/// </summary>
public static void RegisterAsset(Object asset)
{
// 旧输入系统:不管理 InputActionAsset
}
/// <summary>
/// 注销 InputActionAsset(旧系统空操作)。
/// 仅保留签名以兼容编译。
/// </summary>
public static void UnregisterAsset(Object asset)
{
// 旧输入系统:不管理 InputActionAsset
}
// ===== 键盘轮询值(供 QuickGraphInputTool.OnUpdate 读取) =====
/// <summary>
/// 方向键 Pan 方向值(每帧由 <see cref="PollKeyboard"/> 更新)。
/// </summary>
public static Vector2 LastPanDirection;
/// <summary>
/// +/- 键 Zoom 轴值(每帧由 <see cref="PollKeyboard"/> 更新)。
/// </summary>
public static float LastZoomAxis;
// ===== 事件轮询 =====
/// <summary>
/// 轮询鼠标按键事件。
/// 每帧在 Update 中调用,检测鼠标按下/释放并分发到 <see cref="BlueprintToolProvider"/>。
/// </summary>
/// <param name="mouseButton">0=左键, 1=右键, 2=中键</param>
public static void PollMouseEvents(int mouseButton)
{
if (s_focusedGraph?.Canvas == null) return;
s_focusedGraph.BeginFrame();
if (Input.GetMouseButtonDown(mouseButton))
{
var canvasPoint = s_focusedGraph.Canvas.ScreenToCanvas(Input.mousePosition);
BlueprintToolProvider.DispatchPointerDown(s_focusedGraph, canvasPoint, mouseButton);
}
if (Input.GetMouseButtonUp(mouseButton))
{
var canvasPoint = s_focusedGraph.Canvas.ScreenToCanvas(Input.mousePosition);
BlueprintToolProvider.DispatchPointerUp(s_focusedGraph, canvasPoint, mouseButton);
}
}
/// <summary>
/// 轮询鼠标滚轮事件。
/// 每帧在 Update 中调用,检测滚轮增量并分发到 <see cref="BlueprintToolProvider"/>。
/// </summary>
public static void PollScrollWheel()
{
if (s_focusedGraph?.Canvas == null) return;
s_focusedGraph.BeginFrame();
float scrollY = Input.mouseScrollDelta.y;
if (!Mathf.Approximately(scrollY, 0f))
{
var canvasPoint = s_focusedGraph.Canvas.ScreenToCanvas(Input.mousePosition);
var scrollDelta = new Vector2(0f, scrollY);
BlueprintToolProvider.DispatchScroll(s_focusedGraph, canvasPoint, scrollDelta);
}
}
/// <summary>
/// 轮询键盘快捷键(方向键 Pan、+/- Zoom)。
/// 每帧在 Update 中调用,更新 <see cref="LastPanDirection"/> 和 <see cref="LastZoomAxis"/>。
/// </summary>
public static void PollKeyboard()
{
LastPanDirection = Vector2.zero;
if (Input.GetKey(KeyCode.UpArrow) || Input.GetKey(KeyCode.W))
LastPanDirection.y += 1f;
if (Input.GetKey(KeyCode.DownArrow) || Input.GetKey(KeyCode.S))
LastPanDirection.y -= 1f;
if (Input.GetKey(KeyCode.LeftArrow) || Input.GetKey(KeyCode.A))
LastPanDirection.x -= 1f;
if (Input.GetKey(KeyCode.RightArrow) || Input.GetKey(KeyCode.D))
LastPanDirection.x += 1f;
LastZoomAxis = 0f;
if (Input.GetKey(KeyCode.Equals) || Input.GetKey(KeyCode.KeypadPlus))
LastZoomAxis = 1f;
if (Input.GetKey(KeyCode.Minus) || Input.GetKey(KeyCode.KeypadMinus))
LastZoomAxis = -1f;
}
}
}
#endif
@@ -1,110 +0,0 @@
using UnityEngine;
namespace XericLibrary.Runtime.Blueprint
{
/// <summary>
/// 蓝图输入接收工具 —— 继承自 <see cref="BlueprintTool"/>,由 BlueprintToolProvider 统一管理生命周期。
/// <para>
/// 职责:
/// 1. 创建并驱动 <see cref="BlueprintUGUIInputManager"/>,绑定 UGUI EventTrigger 到背景 Image;
/// 2. 在 <see cref="OnUpdate"/> 中统一刷新鼠标位置、检测指针进入/离开背景区域;
/// 3. 将画布内的指针移动事件分发给 <see cref="BlueprintToolProvider"/>。
/// </para>
/// <para>
/// 鼠标按键按下/释放/拖拽/滚轮等由 <see cref="BlueprintUGUIInputManager"/> 通过 EventTrigger 回调直接分发。
/// </para>
/// </summary>
[BlueprintTool(phase: ToolPhase.PreUpdate, order: 0)]
[BlueprintTheme("QuickGraph")]
public class BlueprintInputReceiver : BlueprintTool
{
private BlueprintUGUIInputManager _uguiManager;
private bool _initialized;
private Vector2 _lastMousePos;
private bool _isInside;
// ===== 生命周期 =====
public override void OnUpdate(float deltaTime)
{
if (Graph == null) return;
// ── 每帧开始:刷新帧缓存 ──
Graph.BeginFrame();
// ── 初始化 UGUI 管理器 ──
if (!_initialized)
{
_uguiManager = new BlueprintUGUIInputManager(Graph);
_initialized = true;
}
// ── 更新鼠标位置(统一入口) ──
BlueprintUGUIInputManager.UpdateMousePosition();
BlueprintToolProvider.MousePosition = BlueprintUGUIInputManager.MousePosition;
// ── 延迟绑定 EventTrigger ──
if (!_uguiManager.IsBound)
{
_uguiManager.TryBind();
return;
}
var mp = BlueprintUGUIInputManager.MousePosition;
// ── 检测指针进入/离开背景 ──
bool overBg = IsScreenPointOverBackground(mp);
if (overBg && !_isInside)
{
_isInside = true;
BlueprintToolProvider.DispatchPointerEnter(Graph);
}
else if (!overBg && _isInside)
{
_isInside = false;
BlueprintToolProvider.DispatchPointerExit(Graph);
}
if (!_isInside) return;
// ── 指针移动(轮询,EventTrigger 无通用移动事件) ──
if (Vector2.Distance(_lastMousePos, mp) < 0.001f) return;
var cur = ScreenToCanvas(mp);
var prev = (_lastMousePos == Vector2.zero) ? cur : ScreenToCanvas(_lastMousePos);
var delta = cur - prev;
_lastMousePos = mp;
BlueprintToolProvider.DispatchPointerMove(Graph, cur, delta);
}
public override void OnDestroy()
{
if (_uguiManager != null)
{
_uguiManager.Unbind();
_uguiManager = null;
}
_initialized = false;
_isInside = false;
_lastMousePos = Vector2.zero;
}
// ===== 辅助 =====
/// <summary>检查屏幕坐标是否落在背景 RectTransform 内。</summary>
private bool IsScreenPointOverBackground(Vector2 screenPoint)
{
var rt = _uguiManager?.BgRectTransform;
if (rt == null) return false;
return RectTransformUtility.RectangleContainsScreenPoint(rt, screenPoint, null);
}
/// <summary>屏幕坐标 → 画布坐标。</summary>
private Vector2 ScreenToCanvas(Vector2 screenPos)
{
return Graph?.Canvas != null ? Graph.Canvas.ScreenToCanvas(screenPos) : screenPos;
}
}
}
@@ -0,0 +1,196 @@
#if !ENABLE_INPUT_SYSTEM
using XericLibrary.Runtime.MacroLibrary;
using UnityEngine;
namespace XericLibrary.Runtime.Blueprint
{
/// <summary>
/// 蓝图输入工具(旧输入系统)—— 自身完成全部鼠标/键盘/快捷键轮询,
/// 通过 <c>KfsStyle</c> + <c>KeyFlippingStampSheet</c> 状态机驱动拖拽识别。
/// </summary>
[BlueprintTool(phase: ToolPhase.PreUpdate, order: 1)]
[BlueprintTheme("QuickGraph")]
public class BlueprintInputTool_InputManager : BlueprintInputTool_UGUI
{
// ===== 三键的 sheet 状态表 =====
private readonly MacroKey.KeyFlippingStampSheet[] _sheets = new MacroKey.KeyFlippingStampSheet[3];
private readonly Vector2[] _lastCanvasPos = new Vector2[3];
// ===== 快捷键轮询常量 =====
private static readonly KeyCode[] s_modifierKeys = new[]
{
KeyCode.LeftControl, KeyCode.RightControl,
KeyCode.LeftShift, KeyCode.RightShift,
KeyCode.LeftAlt, KeyCode.RightAlt,
};
// ===== 生命周期 =====
public override void OnInitialize()
{
UseEventTriggerForMouse = false;
base.OnInitialize();
// 初始化三键的 sheet(button → KfsStyle → KeyFlippingStampSheet)
for (int i = 0; i < 3; i++)
{
var style = i.AsKfsStyle();
_sheets[i] = style.GetKeySheet();
}
RegisterShortcutHandlers();
}
public override void OnUpdate(float deltaTime)
{
// 基类:BeginFrame + 鼠标位置 + 光标上下文 + PointerMove
base.OnUpdate(deltaTime);
// 鼠标按键 / 滚轮 / 键盘 Pan/Zoom 轮询
PollAll();
// 快捷键轮询
PollShortcuts();
}
// ===== 统一轮询 =====
private void PollAll()
{
if (Graph?.Canvas == null) return;
var canvas = Graph.Canvas;
// 三键(由 sheet 状态机驱动)
PollMouseButtonSheet(canvas, 0);
PollMouseButtonSheet(canvas, 1);
PollMouseButtonSheet(canvas, 2);
// 滚轮
float scrollY = Input.mouseScrollDelta.y;
if (!Mathf.Approximately(scrollY, 0f))
{
BlueprintToolProvider.DispatchScroll(Graph, this,
canvas.ScreenToCanvas(Input.mousePosition),
new Vector2(0f, scrollY));
}
// 键盘 Pan / Zoom
GraphInputTool.LastPanDirection = Vector2.zero;
float x = 0f, y = 0f;
if (Input.GetKey(KeyCode.UpArrow) || Input.GetKey(KeyCode.W)) y += 1f;
if (Input.GetKey(KeyCode.DownArrow) || Input.GetKey(KeyCode.S)) y -= 1f;
if (Input.GetKey(KeyCode.LeftArrow) || Input.GetKey(KeyCode.A)) x -= 1f;
if (Input.GetKey(KeyCode.RightArrow) || Input.GetKey(KeyCode.D)) x += 1f;
GraphInputTool.LastPanDirection = new Vector2(x, y);
GraphInputTool.LastZoomAxis = 0f;
if (Input.GetKey(KeyCode.Equals) || Input.GetKey(KeyCode.KeypadPlus)) GraphInputTool.LastZoomAxis = 1f;
if (Input.GetKey(KeyCode.Minus) || Input.GetKey(KeyCode.KeypadMinus)) GraphInputTool.LastZoomAxis = -1f;
}
/// <summary>
/// 使用 sheet 状态机驱动单个鼠标按键的点击/拖拽/释放事件。
/// 每帧调用 <c>AppendKeyState_PositionTriggle</c> 更新状态,
/// 然后根据 <c>IsPressed / IsDragging / IsDropping / IsReleased</c> 分发到 Provider。
/// </summary>
private void PollMouseButtonSheet(IBlueprintCanvas canvas, int button)
{
var sheet = _sheets[button];
bool pressed = Input.GetMouseButton(button);
sheet.AppendKeyState_PositionTriggle(pressed, Input.mousePosition, false, 0.1f);
// ── 按下 ──
if (sheet.IsPressed)
{
var pt = canvas.ScreenToCanvas(Input.mousePosition);
_lastCanvasPos[button] = pt;
BlueprintToolProvider.DispatchPointerDown(Graph, this, pt, button);
}
// ── 拖拽(含首次拖拽帧) ──
if (sheet.IsDragging)
{
var current = canvas.ScreenToCanvas(Input.mousePosition);
var delta = current - _lastCanvasPos[button];
_lastCanvasPos[button] = current;
if (delta.sqrMagnitude > 0.0001f)
{
BlueprintToolProvider.DispatchPointerDrag(Graph, this, current, delta, button);
}
}
// ── 释放 ──
if (sheet.IsDropping || sheet.IsReleased)
{
var pt = canvas.ScreenToCanvas(Input.mousePosition);
bool isEmptyClick = sheet.IsReleased && Graph?.HitTest(pt) == null;
BlueprintToolProvider.DispatchPointerUp(Graph, this, pt, button, isEmptyClick);
}
}
// ===== 快捷键动作注册 =====
private void RegisterShortcutHandlers()
{
var tree = MacroKey.ShortcutTreeInstance;
RegisterAction(tree, BlueprintInputConstants.FocusHome);
RegisterAction(tree, BlueprintInputConstants.Undo);
RegisterAction(tree, BlueprintInputConstants.Redo);
RegisterAction(tree, BlueprintInputConstants.DeleteSelected);
RegisterAction(tree, BlueprintInputConstants.SelectAll);
RegisterAction(tree, BlueprintInputConstants.Duplicate);
RegisterAction(tree, BlueprintInputConstants.Copy);
RegisterAction(tree, BlueprintInputConstants.Paste);
RegisterAction(tree, BlueprintInputConstants.Cut);
RegisterAction(tree, BlueprintInputConstants.Save);
}
private void RegisterAction(ShortcutTree tree, string alias)
{
tree.RegisterAlias(alias, () => BlueprintToolProvider.DispatchAction(Graph, this, alias));
tree.RebindAlias(alias, () => BlueprintToolProvider.DispatchAction(Graph, this, alias));
}
// ===== 快捷键轮询 =====
private void PollShortcuts()
{
if (!Input.anyKeyDown) return;
foreach (KeyCode key in System.Enum.GetValues(typeof(KeyCode)))
{
if (IsModifier(key)) continue;
if (!Input.GetKeyDown(key)) continue;
bool ctrl = Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl);
bool shift = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift);
bool alt = Input.GetKey(KeyCode.LeftAlt) || Input.GetKey(KeyCode.RightAlt);
char keyChar = KeyCodeToChar(key);
MacroKey.ShortcutTreeInstance.CallShortcut(shift, ctrl, alt, keyChar);
break;
}
}
private static bool IsModifier(KeyCode key)
{
foreach (var mod in s_modifierKeys)
if (key == mod) return true;
return false;
}
private static char KeyCodeToChar(KeyCode key)
{
if (key >= KeyCode.A && key <= KeyCode.Z)
return (char)('A' + (key - KeyCode.A));
if (key >= KeyCode.Alpha0 && key <= KeyCode.Alpha9)
return (char)('0' + (key - KeyCode.Alpha0));
if (key >= KeyCode.Keypad0 && key <= KeyCode.Keypad9)
return (char)('0' + (key - KeyCode.Keypad0));
return (char)key;
}
}
}
#endif
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 7e1d0771b4cc2aa4692eff637fa5d5af
guid: 53cd1e0c9567eee4a8ca246adee6510f
MonoImporter:
externalObjects: {}
serializedVersion: 2
@@ -0,0 +1,160 @@
#if ENABLE_INPUT_SYSTEM
using XericLibrary.Runtime.MacroLibrary;
using UnityEngine;
using UnityEngine.InputSystem;
namespace XericLibrary.Runtime.Blueprint
{
/// <summary>
/// 蓝图输入工具(新输入系统)—— 自身管理 InputActionAsset 绑定和快捷键回调,
/// 不再依赖 <c>BlueprintInputManager</c> 管理器。
/// </summary>
[BlueprintTool(phase: ToolPhase.PreUpdate, order: 1)]
[BlueprintTheme("QuickGraph")]
public class BlueprintInputTool_InputSystem : BlueprintInputTool_UGUI
{
private InputActionAsset _inputActionAsset;
private InputActionMap _activeMap;
// 缓存的动作回调实例
private System.Action<InputAction.CallbackContext> _panHandler;
private System.Action<InputAction.CallbackContext> _panCanceled;
private System.Action<InputAction.CallbackContext> _zoomHandler;
private System.Action<InputAction.CallbackContext> _zoomCanceled;
private System.Action<InputAction.CallbackContext> _homeHandler;
private System.Action<InputAction.CallbackContext> _undoHandler;
private System.Action<InputAction.CallbackContext> _redoHandler;
private System.Action<InputAction.CallbackContext> _parentHandler;
// ===== InputActionAsset 属性 =====
public InputActionAsset InputActionAsset
{
get => _inputActionAsset;
set
{
if (_inputActionAsset == value) return;
UnbindCurrent();
_inputActionAsset = value;
if (_inputActionAsset != null && Graph != null)
BindCurrent();
}
}
// ===== 生命周期 =====
public override void OnInitialize()
{
// 创建回调委托(确保 Bind/Unbind 使用同一引用)
_panHandler = ctx => GraphInputTool.LastPanDirection = ctx.ReadValue<Vector2>();
_panCanceled = _ => GraphInputTool.LastPanDirection = Vector2.zero;
_zoomHandler = ctx => GraphInputTool.LastZoomAxis = ctx.ReadValue<float>();
_zoomCanceled = _ => GraphInputTool.LastZoomAxis = 0f;
_homeHandler = _ => DispatchShortcut(BlueprintInputConstants.FocusHome);
_undoHandler = _ => DispatchShortcut(BlueprintInputConstants.Undo);
_redoHandler = _ => DispatchShortcut(BlueprintInputConstants.Redo);
_parentHandler = _ => DispatchShortcut(BlueprintInputConstants.NavigateParent);
base.OnInitialize();
RegisterShortcutHandlers();
}
public override void OnDestroy()
{
UnbindCurrent();
base.OnDestroy();
}
// ===== ActionMap 绑定/解绑 =====
private void BindCurrent()
{
if (_inputActionAsset == null) return;
var map = _inputActionAsset.FindActionMap(BlueprintInputConstants.MapName);
if (map == null) return;
map.Enable();
Bind(map, BlueprintInputConstants.Pan, _panHandler);
BindCancel(map, BlueprintInputConstants.Pan, _panCanceled);
Bind(map, BlueprintInputConstants.Zoom, _zoomHandler);
BindCancel(map, BlueprintInputConstants.Zoom, _zoomCanceled);
Bind(map, BlueprintInputConstants.FocusHome, _homeHandler);
Bind(map, BlueprintInputConstants.Undo, _undoHandler);
Bind(map, BlueprintInputConstants.Redo, _redoHandler);
Bind(map, BlueprintInputConstants.NavigateParent, _parentHandler);
_activeMap = map;
}
private void UnbindCurrent()
{
if (_activeMap == null) return;
Unbind(_activeMap, BlueprintInputConstants.Pan, _panHandler);
UnbindCancel(_activeMap, BlueprintInputConstants.Pan, _panCanceled);
Unbind(_activeMap, BlueprintInputConstants.Zoom, _zoomHandler);
UnbindCancel(_activeMap, BlueprintInputConstants.Zoom, _zoomCanceled);
Unbind(_activeMap, BlueprintInputConstants.FocusHome, _homeHandler);
Unbind(_activeMap, BlueprintInputConstants.Undo, _undoHandler);
Unbind(_activeMap, BlueprintInputConstants.Redo, _redoHandler);
Unbind(_activeMap, BlueprintInputConstants.NavigateParent, _parentHandler);
_activeMap.Disable();
_activeMap = null;
}
private static void Bind(InputActionMap map, string name, System.Action<InputAction.CallbackContext> handler)
{
var action = map.FindAction(name);
if (action != null) action.performed += handler;
}
private static void Unbind(InputActionMap map, string name, System.Action<InputAction.CallbackContext> handler)
{
var action = map.FindAction(name);
if (action != null) action.performed -= handler;
}
private static void BindCancel(InputActionMap map, string name, System.Action<InputAction.CallbackContext> handler)
{
var action = map.FindAction(name);
if (action != null) action.canceled += handler;
}
private static void UnbindCancel(InputActionMap map, string name, System.Action<InputAction.CallbackContext> handler)
{
var action = map.FindAction(name);
if (action != null) action.canceled -= handler;
}
private void DispatchShortcut(string actionName)
{
if (Graph == null) return;
Graph.MarkDirty();
BlueprintToolProvider.DispatchAction(Graph, this, actionName);
}
// ===== 快捷键树注册 =====
private void RegisterShortcutHandlers()
{
var tree = MacroKey.ShortcutTreeInstance;
RegisterAction(tree, BlueprintInputConstants.Undo);
RegisterAction(tree, BlueprintInputConstants.Redo);
RegisterAction(tree, BlueprintInputConstants.FocusHome);
RegisterAction(tree, BlueprintInputConstants.DeleteSelected);
RegisterAction(tree, BlueprintInputConstants.SelectAll);
RegisterAction(tree, BlueprintInputConstants.Duplicate);
RegisterAction(tree, BlueprintInputConstants.Copy);
RegisterAction(tree, BlueprintInputConstants.Paste);
RegisterAction(tree, BlueprintInputConstants.Cut);
RegisterAction(tree, BlueprintInputConstants.Save);
}
private void RegisterAction(ShortcutTree tree, string alias)
{
tree.RegisterAlias(alias, () => BlueprintToolProvider.DispatchAction(Graph, this, alias));
tree.RebindAlias(alias, () => BlueprintToolProvider.DispatchAction(Graph, this, alias));
}
}
}
#endif
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 72f3c53c414fbef4b80270adb10afe1d
guid: 43b72153b4945b14da4fb0464d0a2e22
MonoImporter:
externalObjects: {}
serializedVersion: 2
@@ -0,0 +1,278 @@
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace XericLibrary.Runtime.Blueprint
{
/// <summary>
/// 蓝图输入工具基类(UGUI)—— 注册 EventTrigger 到蓝图背景上监听鼠标事件,
/// 并将事件发布到 <see cref="BlueprintToolProvider"/>,同时记录光标上下文。
/// <para>
/// 注意:此基类本身不被注册为独立工具(无 <c>[BlueprintTool]</c> 属性)。
/// 由子类 <c>BlueprintInputTool_InputSystem</c> 和 <c>BlueprintInputTool_InputManager</c>
/// 通过继承获得 UGUI 绑定能力。
/// </para>
/// </summary>
public class BlueprintInputTool_UGUI : GraphInputTool
{
// ===== EventTrigger 绑定 =====
private Transform _bgTransform;
private EventTrigger _eventTrigger;
private RectTransform _bgRectTransform;
private const string BackgroundGoName = "__Bp_GridBackground";
// 上一帧的鼠标画布坐标(用于计算 PointerMove delta)
private Vector2 _lastCanvasPos;
/// <summary>
/// 是否通过 EventTrigger 处理鼠标按键/拖拽/滚轮事件。
/// 新输入系统路径下为 true(默认),旧输入系统路径下为 false(由 PollUpdate 轮询)。
/// 子类可在 <see cref="OnInitialize"/> 中修改此值。
/// </summary>
protected bool UseEventTriggerForMouse { get; set; } = true;
// ===== 生命周期 =====
public override void OnInitialize()
{
TryBindEventTrigger();
}
public override void OnUpdate(float deltaTime)
{
if (Graph == null) return;
// 检查底层的 Unity Canvas 组件是否仍存活
// Graph?.Canvas 是 C# 接口引用(非 Unity Object),不适用 Unity 的假空检测
if (Graph.Canvas == null || !Graph.Canvas.IsAlive)
return;
Graph.BeginFrame();
BlueprintUGUIInputManager.UpdateMousePosition();
BlueprintToolProvider.MousePosition = BlueprintUGUIInputManager.MousePosition;
// 每帧检查 EventTrigger 是否有效(应对 Editor 重收集工具场景)
if (_eventTrigger == null || !IsEventTriggerValid())
{
_eventTrigger = null; // 强制重新绑定
TryBindEventTrigger();
return;
}
var canvasPt = Graph.Canvas != null
? Graph.Canvas.ScreenToCanvas(BlueprintUGUIInputManager.MousePosition)
: Vector2.zero;
UpdateCursorContext(canvasPt);
// ── 每帧分发 PointerMove ──
var delta = canvasPt - _lastCanvasPos;
if (delta.sqrMagnitude > 0.0001f || _lastCanvasPos == Vector2.zero)
{
BlueprintToolProvider.DispatchPointerMove(Graph, this, canvasPt, delta);
}
_lastCanvasPos = canvasPt;
}
/// <summary>
/// 检查已绑定的 EventTrigger 实例是否仍然有效(回调仍指向本实例)。
/// </summary>
private bool IsEventTriggerValid()
{
if (_eventTrigger == null) return false;
var triggersList = _eventTrigger.triggers;
if (triggersList == null || triggersList.Count == 0) return false;
var callbacks = triggersList[0].callback;
if (callbacks == null) return false;
int persistentCount = callbacks.GetPersistentEventCount();
if (persistentCount > 0)
{
var target = callbacks.GetPersistentTarget(0);
return target != null && target.Equals(this);
}
return true;
}
public override void OnDestroy()
{
UnbindEventTrigger();
}
/// <summary>
/// 确保场景中存在 EventSystem 和合适的 InputModule。
/// 旧输入系统 → StandaloneInputModule。
/// 新输入系统 → InputSystemUIInputModule(由 GraphTheoryBlueprintComponent 创建)。
/// 不会重复创建模块(通过 GetComponent 检查)。
/// </summary>
private static void EnsureEventSystemExists()
{
var es = Object.FindObjectOfType<EventSystem>();
if (es == null)
{
var go = new GameObject("EventSystem");
es = go.AddComponent<EventSystem>();
}
#if !ENABLE_INPUT_SYSTEM
if (es.GetComponent<StandaloneInputModule>() == null)
{
es.gameObject.AddComponent<StandaloneInputModule>();
}
#endif
}
// ===== EventTrigger 绑定/解绑 =====
private void TryBindEventTrigger()
{
if (_eventTrigger != null) return;
if (Graph?.Canvas == null || !Graph.Canvas.IsAlive) return;
var root = Graph.Canvas.GetRootTransform();
if (root == null) return;
_bgTransform = root.Find(BackgroundGoName);
if (_bgTransform == null) return;
_bgRectTransform = _bgTransform as RectTransform;
// ── 自动创建 EventSystem(含合适 InputModule)──
EnsureEventSystemExists();
_eventTrigger = _bgTransform.gameObject.GetComponent<EventTrigger>();
if (_eventTrigger == null)
{
_eventTrigger = _bgTransform.gameObject.AddComponent<EventTrigger>();
}
else
_eventTrigger.triggers.Clear();
BindAllEntries();
}
private void UnbindEventTrigger()
{
if (_eventTrigger != null)
{
_eventTrigger.triggers.Clear();
_eventTrigger = null;
}
_bgTransform = null;
_bgRectTransform = null;
}
private void BindAllEntries()
{
if (_eventTrigger == null) return;
// ── 始终绑定的基础回调(指针进入/离开) ──
AddEntry(EventTriggerType.PointerEnter, OnPointerEnterEvent);
AddEntry(EventTriggerType.PointerExit, OnPointerExitEvent);
// ── 鼠标按键/拖拽/滚轮(仅在 UseEventTriggerForMouse 时绑定) ──
if (UseEventTriggerForMouse)
{
AddEntry(EventTriggerType.PointerDown, OnPointerDownEvent);
AddEntry(EventTriggerType.PointerUp, OnPointerUpEvent);
AddEntry(EventTriggerType.BeginDrag, OnBeginDragEvent);
AddEntry(EventTriggerType.Drag, OnDragEvent);
AddEntry(EventTriggerType.EndDrag, OnEndDragEvent);
AddEntry(EventTriggerType.Scroll, OnScrollEvent);
}
}
private void AddEntry(EventTriggerType type, UnityEngine.Events.UnityAction<BaseEventData> callback)
{
var entry = new EventTrigger.Entry { eventID = type };
entry.callback.AddListener(callback);
_eventTrigger.triggers.Add(entry);
}
// ===== 坐标转换 =====
private Vector2 ScreenToCanvas(Vector2 screenPos)
{
return Graph?.Canvas != null ? Graph.Canvas.ScreenToCanvas(screenPos) : screenPos;
}
private Vector2 PointerCanvasPos(PointerEventData data)
{
return ScreenToCanvas(data.position);
}
// ===== EventTrigger 回调(以 self 作为 sender 派发) =====
private void OnPointerEnterEvent(BaseEventData _)
{
BlueprintToolProvider.DispatchPointerEnter(Graph, this);
}
private void OnPointerExitEvent(BaseEventData _)
{
BlueprintToolProvider.DispatchPointerExit(Graph, this);
}
private void OnPointerDownEvent(BaseEventData data)
{
if (!(data is PointerEventData pData)) return;
var pt = PointerCanvasPos(pData);
Graph?.MarkDirty();
UpdateCursorContext(pt);
BlueprintToolProvider.DispatchPointerDown(Graph, this, pt, (int)pData.button);
}
private void OnPointerUpEvent(BaseEventData data)
{
if (!(data is PointerEventData pData)) return;
var pt = PointerCanvasPos(pData);
UpdateCursorContext(pt);
// 有拖拽的释放由 OnEndDragEvent 处理,这里只响应无拖拽的点击释放
if (!_hadDragThisClick)
{
bool isEmptyClick = Graph?.HitTest(pt) == null;
BlueprintToolProvider.DispatchPointerUp(Graph, this, pt, (int)pData.button, isEmptyClick);
}
}
private Vector2 _dragStartPos;
private bool _hadDragThisClick;
private void OnBeginDragEvent(BaseEventData data)
{
if (!(data is PointerEventData pData)) return;
_dragStartPos = PointerCanvasPos(pData);
_hadDragThisClick = true;
}
private void OnDragEvent(BaseEventData data)
{
if (!(data is PointerEventData pData)) return;
var current = PointerCanvasPos(pData);
var delta = current - _dragStartPos;
_dragStartPos = current;
UpdateCursorContext(current);
BlueprintToolProvider.DispatchPointerDrag(Graph, this, current, delta, (int)pData.button);
}
private void OnEndDragEvent(BaseEventData data)
{
if (!(data is PointerEventData pData)) return;
var pt = PointerCanvasPos(pData);
UpdateCursorContext(pt);
BlueprintToolProvider.DispatchPointerUp(Graph, this, pt, (int)pData.button);
}
private void OnScrollEvent(BaseEventData data)
{
if (!(data is PointerEventData pData)) return;
var pt = PointerCanvasPos(pData);
var scroll = new Vector2(-pData.scrollDelta.x, -pData.scrollDelta.y);
UpdateCursorContext(pt);
Graph?.MarkDirty();
BlueprintToolProvider.DispatchScroll(Graph, this, pt, scroll);
}
}
}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 1f94468ab3403984494aa0d980297093
guid: ff8955aaf6e6c2447ab2c9c264a4fc05
MonoImporter:
externalObjects: {}
serializedVersion: 2
@@ -1,45 +1,19 @@
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace XericLibrary.Runtime.Blueprint
{
/// <summary>
/// 蓝图 UGUI 输入管理器 —— 负责绑定 UGUI EventTrigger 到背景 Image,
/// 将 UGUI 指针事件转发到 <see cref="BlueprintToolProvider"/> 的分发方法。
/// <para>
/// 该类仅处理与 UGUI EventTrigger 相关的绑定逻辑,不感知新旧输入系统的差异。
/// 由 <see cref="BlueprintInputReceiver"/> 创建并在 OnUpdate 中驱动。
/// </para>
/// 蓝图 UGUI 输入工具静态辅助类。
/// 提供跨新旧输入系统的统一鼠标位置和按钮状态查询。
/// </summary>
public class BlueprintUGUIInputManager
public static class BlueprintUGUIInputManager
{
/// <summary>
/// 当前帧鼠标/指针在屏幕空间的位置。
/// 每帧由 <see cref="UpdateMousePosition"/> 刷新。
/// </summary>
public static Vector2 MousePosition { get; private set; }
/// <summary>当前帧鼠标/指针在屏幕空间的位置,每帧由 BlueprintInputTool_UGUI 更新。</summary>
public static Vector2 MousePosition { get; internal set; }
/// <summary>
/// 当前帧鼠标/指针是否处于新输入系统模式。
/// <c>true</c> = 新输入系统(<c>ENABLE_INPUT_SYSTEM</c> 已定义);
/// <c>false</c> = 旧输入系统。
/// </summary>
public static bool IsNewInputSystem
{
get
{
#if ENABLE_INPUT_SYSTEM
return true;
#else
return false;
#endif
}
}
/// <summary>
/// 更新静态鼠标位置。每帧由 BlueprintInputReceiver 的 OnUpdate 调用。
/// 内部根据条件编译选择鼠标位置读取源。
/// 更新静态鼠标位置。
/// 由 <see cref="BlueprintInputTool_UGUI"/> 每帧在 OnUpdate 中调用。
/// </summary>
public static void UpdateMousePosition()
{
@@ -52,7 +26,6 @@ namespace XericLibrary.Runtime.Blueprint
/// <summary>
/// 返回当前是否按住指定鼠标按钮。
/// 内部根据条件编译选择检测方式。
/// </summary>
/// <param name="button">0=左键, 1=右键, 2=中键</param>
public static bool IsButtonPressed(int button)
@@ -71,169 +44,5 @@ namespace XericLibrary.Runtime.Blueprint
return Input.GetMouseButton(button);
#endif
}
// ===== 实例部分 =====
private BlueprintGraph _graph;
private Transform _bgTransform;
private EventTrigger _eventTrigger;
private bool _isBound;
// 背景 GameObject 名称(与 QuickUGUIGraphGridBackgroundTool 一致)
private const string BackgroundGoName = "__Bp_GridBackground";
// ===== 生命周期 =====
/// <summary>初始化 UGUI 输入管理器并绑定到指定蓝图。</summary>
public BlueprintUGUIInputManager(BlueprintGraph graph)
{
_graph = graph;
}
/// <summary>
/// 尝试绑定 EventTrigger 到背景 Image。
/// 返回 <c>true</c> 表示绑定成功,<c>false</c> 表示背景尚未创建。
/// </summary>
public bool TryBind()
{
if (_isBound) return true;
if (_graph?.Canvas == null) return false;
var root = _graph.Canvas.GetRootTransform();
if (root == null) return false;
_bgTransform = root.Find(BackgroundGoName);
if (_bgTransform == null) return false;
_eventTrigger = _bgTransform.gameObject.GetComponent<EventTrigger>();
if (_eventTrigger == null)
_eventTrigger = _bgTransform.gameObject.AddComponent<EventTrigger>();
else
_eventTrigger.triggers.Clear();
BindEventTrigger();
_isBound = true;
return true;
}
/// <summary>解绑并清理 EventTrigger。</summary>
public void Unbind()
{
if (_eventTrigger != null)
{
_eventTrigger.triggers.Clear();
_eventTrigger = null;
}
_bgTransform = null;
_isBound = false;
}
/// <summary>是否已绑定。</summary>
public bool IsBound => _isBound;
/// <summary>背景 RectTransform(用于屏幕点包含检测)。</summary>
public RectTransform BgRectTransform => _bgTransform as RectTransform;
// ===== EventTrigger 绑定 =====
private void BindEventTrigger()
{
if (_eventTrigger == null) return;
AddEntry(EventTriggerType.PointerEnter, OnEventPointerEnter);
AddEntry(EventTriggerType.PointerExit, OnEventPointerExit);
AddEntry(EventTriggerType.PointerDown, OnEventPointerDown);
AddEntry(EventTriggerType.PointerUp, OnEventPointerUp);
AddEntry(EventTriggerType.BeginDrag, OnEventBeginDrag);
AddEntry(EventTriggerType.Drag, OnEventDrag);
AddEntry(EventTriggerType.EndDrag, OnEventEndDrag);
AddEntry(EventTriggerType.Scroll, OnEventScroll);
}
private void AddEntry(EventTriggerType eventType, UnityEngine.Events.UnityAction<BaseEventData> callback)
{
var entry = new EventTrigger.Entry { eventID = eventType };
entry.callback.AddListener(callback);
_eventTrigger.triggers.Add(entry);
}
// ===== 坐标转换 =====
private Vector2 ScreenToCanvas(Vector2 screenPos)
{
return _graph?.Canvas != null ? _graph.Canvas.ScreenToCanvas(screenPos) : screenPos;
}
private Vector2 ScreenToCanvas(PointerEventData data)
{
return ScreenToCanvas(data.position);
}
// ===== EventTrigger 回调 =====
private void OnEventPointerEnter(BaseEventData _)
{
if (_graph != null)
BlueprintToolProvider.DispatchPointerEnter(_graph);
}
private void OnEventPointerExit(BaseEventData _)
{
if (_graph != null)
BlueprintToolProvider.DispatchPointerExit(_graph);
}
private void OnEventPointerDown(BaseEventData data)
{
if (_graph == null || !(data is PointerEventData pData)) return;
_graph.BeginFrame();
var pt = ScreenToCanvas(pData);
_graph.MarkDirty();
BlueprintToolProvider.DispatchPointerDown(_graph, pt, (int)pData.button);
}
private void OnEventPointerUp(BaseEventData data)
{
if (_graph == null || !(data is PointerEventData pData)) return;
var pt = ScreenToCanvas(pData);
BlueprintToolProvider.DispatchPointerUp(_graph, pt, (int)pData.button);
}
// 拖拽状态
private int _dragButton = -1;
private Vector2 _dragStartPos;
private void OnEventBeginDrag(BaseEventData data)
{
if (_graph == null || !(data is PointerEventData pData)) return;
_dragButton = (int)pData.button;
_dragStartPos = ScreenToCanvas(pData);
}
private void OnEventDrag(BaseEventData data)
{
if (_graph == null || !(data is PointerEventData pData)) return;
var current = ScreenToCanvas(pData);
var delta = current - _dragStartPos;
_dragStartPos = current;
BlueprintToolProvider.DispatchPointerDrag(_graph, current, delta, _dragButton);
}
private void OnEventEndDrag(BaseEventData data)
{
if (_graph == null || !(data is PointerEventData pData)) return;
BlueprintToolProvider.DispatchPointerUp(_graph, ScreenToCanvas(pData), (int)pData.button);
_dragButton = -1;
}
private void OnEventScroll(BaseEventData data)
{
if (_graph == null || !(data is PointerEventData pData)) return;
_graph.BeginFrame();
var pt = ScreenToCanvas(pData);
var scroll = new Vector2(-pData.scrollDelta.x, -pData.scrollDelta.y);
_graph.MarkDirty();
BlueprintToolProvider.DispatchScroll(_graph, pt, scroll);
}
}
}
+20 -25
View File
@@ -66,24 +66,21 @@ namespace XericLibrary.Runtime.Blueprint
_viewInitialized = true;
}
// ── 键盘轮询(新输入系统 Pan 动作值) ──
_targetPanOffset += BlueprintInputManager.LastPanDirection
// ── 键盘 Pan ──
_targetPanOffset += GraphInputTool.LastPanDirection
* cfg.KeyboardPanSpeed * deltaTime;
// ── 键盘轮询(新输入系统 Zoom 动作值) ──
// zoomInput 对键盘按键是 ±1(按下一下),对轴是 0~1 连续值。
// 统一按一个 scroll notch = 120 缩放,scale = 120 / ZoomDivisor
float zoomInput = BlueprintInputManager.LastZoomAxis;
// ── 键盘 / 滚轮 Zoom ──
float zoomInput = GraphInputTool.LastZoomAxis;
if (!Mathf.Approximately(zoomInput, 0f))
{
float oldZoom = _targetZoomLevel;
float zoomDelta = zoomInput / cfg.ZoomDivisor;
_targetZoomLevel = Mathf.Clamp(
_targetZoomLevel + zoomInput / cfg.ZoomDivisor,
_targetZoomLevel + zoomDelta,
canvas.MinZoom, canvas.MaxZoom);
// 以鼠标焦点为中心缩放(键盘无光标,从鼠标读取)
ApplyZoomFocus(ref _targetPanOffset, _targetZoomLevel, oldZoom);
// 归零,避免持续累加
BlueprintInputManager.LastZoomAxis = 0f;
GraphInputTool.LastZoomAxis = 0f;
}
// ── 平滑插值到目标值 ──
@@ -103,7 +100,7 @@ namespace XericLibrary.Runtime.Blueprint
// ===== 鼠标移动 =====
public override void OnPointerMove(Vector2 canvasPoint, Vector2 delta)
public override void OnPointerMove(GraphInputTool sender, Vector2 canvasPoint, Vector2 delta)
{
if (Graph == null || _isDragging) return;
@@ -113,22 +110,21 @@ namespace XericLibrary.Runtime.Blueprint
// ===== 鼠标按下 =====
public override void OnPointerDown(Vector2 canvasPoint, int mouseButton)
public override void OnPointerDown(GraphInputTool sender, Vector2 canvasPoint, int mouseButton)
{
if (Graph == null) return;
if (mouseButton == 2) // 中键 → 平移
if (mouseButton == 2)
{
_isDragging = true;
_activeDragButton = 2;
_dragStartPan = _targetPanOffset;
}
// 左键点击选择由元素层的 IBlueprintEventHandler.OnPointerDown 处理
}
// ===== 鼠标释放 =====
public override void OnPointerUp(Vector2 canvasPoint, int mouseButton)
public override void OnPointerUp(GraphInputTool sender, Vector2 canvasPoint, int mouseButton)
{
if (_isDragging && _activeDragButton == mouseButton)
{
@@ -139,20 +135,20 @@ namespace XericLibrary.Runtime.Blueprint
// ===== 拖拽 =====
public override void OnPointerDrag(Vector2 canvasPoint, Vector2 delta, int mouseButton)
public override void OnPointerDrag(GraphInputTool sender, Vector2 canvasPoint, Vector2 delta, int mouseButton)
{
if (Graph?.Canvas == null) return;
if (mouseButton == 2) // 中键拖拽 = 平移
if (mouseButton == 2)
{
_targetPanOffset += delta * Graph.Canvas.ZoomLevel;
_targetPanOffset += delta * Config.DragPanSpeed;
Graph.MarkDirty();
}
}
// ===== 滚轮 =====
public override void OnScroll(Vector2 canvasPoint, Vector2 scrollDelta)
public override void OnScroll(GraphInputTool sender, Vector2 canvasPoint, Vector2 scrollDelta)
{
if (Graph == null) return;
@@ -164,15 +160,14 @@ namespace XericLibrary.Runtime.Blueprint
if (!Mathf.Approximately(oldZoom, _targetZoomLevel))
{
// 以鼠标光标为中心缩放
_targetPanOffset += canvasPoint * (oldZoom - _targetZoomLevel);
Graph.MarkDirty();
}
}
// ===== 快捷键(仅新输入系统) =====
// ===== 快捷键 =====
public override void OnAction(string actionName)
public override void OnAction(GraphInputTool sender, string actionName)
{
if (Graph?.Canvas == null) return;
@@ -182,11 +177,11 @@ namespace XericLibrary.Runtime.Blueprint
ResetView();
break;
case BlueprintInputConstants.Undo:
break; // TODO
break;
case BlueprintInputConstants.Redo:
break; // TODO
break;
case BlueprintInputConstants.NavigateParent:
break; // TODO
break;
}
}
@@ -31,9 +31,14 @@ namespace XericLibrary.Runtime.Blueprint.Render
[Range(1f, 30f)]
public float ZoomLerpSpeed = 8f;
[Tooltip("滚轮缩放除数。公式:zoom += scrollDelta / ZoomDivisor。\nUnity Input System 的 scrollDelta.y 标准值为 120/格,\n1200 = 每格 +0.1,600 = 每格 +0.2,300 = 每格 +0.4")]
[Range(100f, 5000f)]
public float ZoomDivisor = 1200f;
[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("最小缩放级别")]
@@ -48,13 +53,13 @@ namespace XericLibrary.Runtime.Blueprint.Render
[Range(50f, 2000f)]
public float KeyboardPanSpeed = 600f;
[Tooltip("键盘 +/- 缩放速度(每次帧增量,建议 0.01~0.5)")]
[Tooltip("键盘 +/- 缩放速度(与 ZoomDivisor 配合,公式:zoom += 1 / ZoomDivisor)")]
[Range(0.005f, 0.5f)]
public float KeyboardZoomSpeed = 0.05f;
[Header("框选")]
[Tooltip("框选拖拽触发阈值(像素)")]
[Range(1f, 20f)]
public float DragThreshold = 5f;
[Tooltip("框选拖拽触发阈值(画布单位)。拖拽距离超过此值后才显示选框并开始框选")]
[Range(1f, 100f)]
public float BoxSelectThreshold = 10f;
}
}
@@ -5,9 +5,21 @@ namespace XericLibrary.Runtime.Blueprint.Render
{
/// <summary>
/// 快速 UGUI 图论网格背景渲染工具。
/// 在蓝图画布最底层创建一个全屏 Image 组件绘制网格背景,
/// 在蓝图画布最底层创建一个 Image 组件绘制网格背景,
/// 每帧将画布的 zoom / pan 同步到 Shader 的 _Transform 属性。
/// 全部可调参数由 QuickGraphGridBackgroundConfig 配置资产提供。
/// <para>
/// 背景 Image 在 <see cref="__Bp_RenderRoot"/> 下锚点撑满,
/// 只响应画布属性和 dirty flag,不会因画布重建而销毁(见 <see cref="OnInitialize"/>)。
/// Shader 参数(<c>_Transform</c> 的 scale/offset)在 <see cref="OnRender"/> 中随
/// <c>ZoomLevel / PanOffset</c> 更新,与画布拖拽共享同一属性源。
/// </para>
/// <para>
/// <b>材质引用注意:</b>
/// 必须缓存材质实例(<see cref="_backgroundMaterial"/>)并直接写入该实例。
/// UGUI 的 <c>Graphic.material</c> getter 可能返回 <c>materialForRendering</c>
/// 的副本(与 setter 存入的 <c>m_Material</c> 不同),因此 <c>_backgroundImage.material</c>
/// 不适合用于写 shader 属性。见 <c>Graphic.material</c> Unity 源码。
/// </para>
/// </summary>
[BlueprintTool(phase: ToolPhase.Render, order: 0)]
[BlueprintTheme("QuickGraph")]
@@ -30,7 +42,13 @@ namespace XericLibrary.Runtime.Blueprint.Render
}
private Image _backgroundImage;
private Transform _renderRoot;
/// <summary>
/// 缓存的背景材质实例。
/// 由 <see cref="Config.GetOrCreateMaterial"/> 创建,终身保留。
/// shader 属性始终写入此实例,避免 UGUI <c>materialForRendering</c> 副本问题。
/// </summary>
private Material _backgroundMaterial;
public override void OnInitialize()
{
@@ -44,14 +62,18 @@ namespace XericLibrary.Runtime.Blueprint.Render
SyncMaterialProperties();
}
/// <summary>
/// 在 RenderRoot 下查找或创建背景 Image。
/// 创建后终身保留,不随画布重建销毁。
/// </summary>
private void EnsureBackground()
{
if (_backgroundImage != null) return;
if (Graph?.Canvas?.GetRootTransform() == null) return;
EnsureRenderRoot();
if (_renderRoot == null) return;
var root = Graph.Canvas.GetRootTransform();
var existing = _renderRoot.Find(BackgroundGoName);
var existing = root.Find(BackgroundGoName);
if (existing != null)
{
_backgroundImage = existing.GetComponent<Image>();
@@ -68,7 +90,7 @@ namespace XericLibrary.Runtime.Blueprint.Render
var bgGo = new GameObject(BackgroundGoName, typeof(RectTransform));
bgGo.hideFlags = HideFlags.HideAndDontSave;
bgGo.transform.SetParent(_renderRoot, false);
bgGo.transform.SetParent(root, false);
bgGo.transform.SetAsFirstSibling();
var rt = bgGo.GetComponent<RectTransform>();
@@ -79,45 +101,44 @@ namespace XericLibrary.Runtime.Blueprint.Render
_backgroundImage = bgGo.AddComponent<Image>();
_backgroundImage.raycastTarget = true;
// 关闭 maskable:运行时背景 Image 不参与 UGUI Mask 裁剪,
// 避免父级 Mask 组件触发 materialForRendering 克隆材质实例。
// 材质写入(_Transform 等 shader 属性)直通 GPU,不受 IMaterialModifier 干扰。
_backgroundImage.maskable = false;
ApplyMaterial();
}
/// <summary>
/// 从配置获取材质并设置到 Image(仅首次执行)。
/// 从配置获取材质并缓存到 <see cref="_backgroundMaterial"/>,
/// 然后赋值给 Image。
/// 此方法仅执行一次,后续材质写入直接使用缓存的引用。
/// </summary>
private void ApplyMaterial()
{
if (_backgroundImage == null) return;
var mat = Config.GetOrCreateMaterial();
if (mat != null)
_backgroundImage.material = mat;
if (_backgroundMaterial != null) return;
_backgroundMaterial = Config.GetOrCreateMaterial();
if (_backgroundMaterial != null)
_backgroundImage.material = _backgroundMaterial;
}
/// <summary>
/// 每帧将当前画布 zoom / pan 同步到 Shader,
/// 同时写入配置中所有 Shader 属性。
/// 使用 <see cref="_backgroundMaterial"/> 缓存写入,确保与 GPU 使用的材质实例一致。
/// </summary>
private void SyncMaterialProperties()
{
if (_backgroundImage == null) return;
if (_backgroundMaterial == null) return;
if (Graph == null || Graph.Canvas == null) return;
var mat = _backgroundImage.material;
if (mat == null) return;
var rt = _backgroundImage.rectTransform;
float zoom = Graph.Canvas.ZoomLevel;
Vector2 pan = Graph.Canvas.PanOffset;
Config.ApplyToMaterial(mat, rt.rect.size, zoom, pan);
}
private void EnsureRenderRoot()
{
if (_renderRoot != null) return;
if (Graph == null || Graph.Canvas == null) return;
_renderRoot = Graph.Canvas.GetRootTransform();
Config.ApplyToMaterial(_backgroundMaterial, rt.rect.size, zoom, pan);
}
public override void OnDestroy()
@@ -133,9 +154,9 @@ namespace XericLibrary.Runtime.Blueprint.Render
_backgroundImage = null;
}
_backgroundMaterial = null;
Config.ReleaseMaterial();
_defaultConfig = null;
_renderRoot = null;
}
}
}
@@ -29,7 +29,7 @@ namespace XericLibrary.Runtime.Blueprint.Render
private QuickGraphNodeRenderConfig _defaultConfig;
// --- 当前生效的配置 ---
private QuickGraphNodeRenderConfig Config
public QuickGraphNodeRenderConfig Config
{
get
{
@@ -57,12 +57,6 @@ namespace XericLibrary.Runtime.Blueprint.Render
var cfg = Config;
// ── 将配置默认值写入所有节点(config → node property) ──
foreach (var node in Graph.Nodes)
{
ApplyConfigToNode(node, cfg);
}
// 视口裁剪:只渲染画布可见范围内的节点
var viewport = Graph.Canvas.GetViewportRect();
float margin = cfg.NodeWidth + cfg.NodeHeight; // 扩展一点避免边缘抖动
@@ -172,8 +166,8 @@ namespace XericLibrary.Runtime.Blueprint.Render
sideCount = 4,
chamferSize = cfg.ChamferSize,
chamferSegments = cfg.ChamferSegments,
bgColor = bgColor,
centerColor = Color.white,
bgColor = new Color32(0,0,0,0),
centerColor = bgColor,
borderColor = borderColor,
borderThickness = border,
}
@@ -237,15 +231,6 @@ namespace XericLibrary.Runtime.Blueprint.Render
tmp.gameObject.SetActive(false);
}
/// <summary>
/// 将配置默认值写入节点,使渲染工具直接读取节点值。
/// config 只控制背景色,边框色和文本色由节点初始化时定义。
/// </summary>
private static void ApplyConfigToNode(BlueprintNode node, QuickGraphNodeRenderConfig cfg)
{
node.NodeBGColor = cfg.DefaultBackgroundColor;
}
private void EnsureRenderRoot()
{
if (_renderRoot != null) return;
Binary file not shown.