udpate 0.6.2

This commit is contained in:
2026-07-13 11:44:25 +08:00
parent 923ce1f60d
commit 155641f2ef
87 changed files with 4491 additions and 24 deletions
@@ -0,0 +1,165 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace XericLibrary.Runtime.Blueprint
{
/// <summary>
/// 蓝图框选工具 —— 鼠标左键在空白区域拖拽,绘制矩形选框,
/// 释放时计算框选范围内所有元素的交集并输出统计日志。
/// <para>选框 UGUI Image 默认渲染在所有层之上(SetAsLastSibling)。</para>
/// <para>框选结果保存在 <see cref="SelectedElements"/> 中供其他工具读取。</para>
/// </summary>
[BlueprintTool(phase: ToolPhase.PreUpdate, order: 60)]
[BlueprintTheme("QuickGraph")]
public class BlueprintBoxSelectTool : BlueprintTool
{
// ---------- 状态 ----------
private bool _isBoxSelecting;
private Vector2 _dragStartCanvas; // 按下时的画布坐标
private Vector2 _dragEndCanvas; // 当前的画布坐标
// 选框 UI
private GameObject _selectionBoxGo;
private Image _selectionBoxImage;
private RectTransform _selectionBoxRt;
// ---------- 结果 ----------
/// <summary>本次框选选中的元素列表。</summary>
public List<IBlueprintElement> SelectedElements { get; } = new List<IBlueprintElement>();
// ===== 鼠标按下 =====
public override void OnPointerDown(Vector2 canvasPoint, int mouseButton)
{
if (Graph == null) return;
if (mouseButton != 0) return; // 仅左键
// 仅在空白区域开始框选(HitTest 无结果)
var hit = Graph.HitTest(canvasPoint);
if (hit != null) return;
_isBoxSelecting = true;
_dragStartCanvas = canvasPoint;
_dragEndCanvas = canvasPoint;
// 创建选框 UI
CreateSelectionBox();
}
// ===== 拖拽 =====
public override void OnPointerDrag(Vector2 canvasPoint, Vector2 delta, int mouseButton)
{
if (!_isBoxSelecting || mouseButton != 0) return;
_dragEndCanvas = canvasPoint;
UpdateSelectionBox();
}
// ===== 释放 =====
public override void OnPointerUp(Vector2 canvasPoint, int mouseButton)
{
if (!_isBoxSelecting || mouseButton != 0) return;
_dragEndCanvas = canvasPoint;
FinishSelection();
}
// ===== 销毁 =====
public override void OnDestroy()
{
DestroySelectionBox();
}
// ===== 选框 UI =====
private void CreateSelectionBox()
{
if (Graph?.Canvas == null) return;
var root = Graph.Canvas.GetRootTransform();
if (root == null) return;
_selectionBoxGo = new GameObject("__Bp_BoxSelect", typeof(RectTransform), typeof(Image));
_selectionBoxGo.hideFlags = HideFlags.HideAndDontSave;
_selectionBoxRt = _selectionBoxGo.GetComponent<RectTransform>();
_selectionBoxRt.SetParent(root, false);
_selectionBoxRt.SetAsLastSibling(); // 渲染在最顶层
_selectionBoxImage = _selectionBoxGo.GetComponent<Image>();
_selectionBoxImage.color = new Color(0.2f, 0.5f, 1.0f, 0.15f); // 半透明蓝
// 边框通过 Outline 或额外 Image 实现,直接使用透明填充 + 轮廓不好做,
// 使用两个 Image:填充(当前) + 边框(另一个,1px 白色)
// 为简化,在填充 Image 上挂一个 Outline 组件模拟边框
var outline = _selectionBoxGo.AddComponent<Outline>();
outline.effectColor = Color.white;
outline.effectDistance = new Vector2(1, -1);
}
private void UpdateSelectionBox()
{
if (_selectionBoxRt == null || Graph?.Canvas == null) return;
// 将画布坐标两角转为屏幕坐标
var startScreen = Graph.Canvas.CanvasToScreen(_dragStartCanvas);
var endScreen = Graph.Canvas.CanvasToScreen(_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);
_selectionBoxRt.anchoredPosition = new Vector2(minX, minY);
_selectionBoxRt.sizeDelta = new Vector2(maxX - minX, maxY - minY);
}
private void FinishSelection()
{
_isBoxSelecting = false;
UpdateSelectionBox();
// 计算选框内的元素(画布坐标空间)
SelectedElements.Clear();
var min = new Vector2(
Mathf.Min(_dragStartCanvas.x, _dragEndCanvas.x),
Mathf.Min(_dragStartCanvas.y, _dragEndCanvas.y));
var max = new Vector2(
Mathf.Max(_dragStartCanvas.x, _dragEndCanvas.x),
Mathf.Max(_dragStartCanvas.y, _dragEndCanvas.y));
var selectRect = new Rect(min, max - min);
foreach (var element in Graph.Elements)
{
if (element == null) continue;
if (element.BoundingBox.Overlaps(selectRect))
{
SelectedElements.Add(element);
}
}
// 销毁选框
DestroySelectionBox();
}
private void DestroySelectionBox()
{
if (_selectionBoxGo != null)
{
#if UNITY_EDITOR
if (!Application.isPlaying)
Object.DestroyImmediate(_selectionBoxGo);
else
#endif
Object.Destroy(_selectionBoxGo);
_selectionBoxGo = null;
_selectionBoxRt = null;
_selectionBoxImage = null;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e9d5cdc3f1b8c9840b274a6db73a9fb1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,47 @@
namespace XericLibrary.Runtime.Blueprint
{
/// <summary>
/// 蓝图输入系统的 Action Map / Action 名称常量。
/// 供 Editor 的 InputAction 模板和 Runtime 的输入处理工具共同引用,
/// 避免字符串硬编码散布各处。
/// </summary>
public static class BlueprintInputConstants
{
// ── Action Map ──
public const string MapName = "Blueprint";
// ── 鼠标 / 指针 Actions(名称须与 UnityEngine.InputSystem.UI.InputSystemUIInputModule 官方预设一致)──
/// <summary>画布内指针位置(Vector2)。InputSystemUIInputModule 预设名称。</summary>
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>
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";
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3e718c439f4fa3d449427433ed729467
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,188 @@
#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
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 72f3c53c414fbef4b80270adb10afe1d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,135 @@
#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
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1f94468ab3403984494aa0d980297093
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,110 @@
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,11 @@
fileFormatVersion: 2
guid: 7e1d0771b4cc2aa4692eff637fa5d5af
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,239 @@
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>
/// </summary>
public class BlueprintUGUIInputManager
{
/// <summary>
/// 当前帧鼠标/指针在屏幕空间的位置。
/// 每帧由 <see cref="UpdateMousePosition"/> 刷新。
/// </summary>
public static Vector2 MousePosition { get; private 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 调用。
/// 内部根据条件编译选择鼠标位置读取源。
/// </summary>
public static void UpdateMousePosition()
{
#if ENABLE_INPUT_SYSTEM
MousePosition = UnityEngine.InputSystem.Mouse.current?.position.ReadValue() ?? Vector2.zero;
#else
MousePosition = (Vector2)Input.mousePosition;
#endif
}
/// <summary>
/// 返回当前是否按住指定鼠标按钮。
/// 内部根据条件编译选择检测方式。
/// </summary>
/// <param name="button">0=左键, 1=右键, 2=中键</param>
public static bool IsButtonPressed(int button)
{
#if ENABLE_INPUT_SYSTEM
var mouse = UnityEngine.InputSystem.Mouse.current;
if (mouse == null) return false;
return button switch
{
0 => mouse.leftButton.isPressed,
1 => mouse.rightButton.isPressed,
2 => mouse.middleButton.isPressed,
_ => false,
};
#else
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);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 00688a047bee7ee4b837f07886b45d44
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,226 @@
using UnityEngine;
using XericLibrary.Runtime.Blueprint.Render;
namespace XericLibrary.Runtime.Blueprint
{
/// <summary>
/// QuickGraph 输入交互工具 —— 处理画布平移、缩放和节点选择。
/// <para>中键拖拽 = 平移;左键点击 = 选择;滚轮 = 缩放。</para>
/// <para>快捷键(新输入系统):方向键 = 平移,Ctrl+± = 缩放,Ctrl+H = 归位。</para>
/// </summary>
[BlueprintTool(phase: ToolPhase.PreUpdate, order: 50)]
[BlueprintTheme("QuickGraph")]
public class QuickGraphInputTool : BlueprintTool
{
// ---------- 配置(从 ToolConfigAssets 读取) ----------
private QuickGraphInputConfig Config
{
get
{
if (ToolConfig is QuickGraphInputConfig external)
return external;
if (_defaultConfig == null)
_defaultConfig = ScriptableObject.CreateInstance<QuickGraphInputConfig>();
return _defaultConfig;
}
}
private static QuickGraphInputConfig _defaultConfig;
// ---------- 视图状态 ----------
private Vector2 _targetPanOffset;
private float _targetZoomLevel = 1f;
private bool _viewInitialized;
// 用于检测帧间变化
private Vector2 _prevPanOffset;
private float _prevZoomLevel;
// ---------- 拖拽状态 ----------
private bool _isDragging;
private int _activeDragButton = -1;
private Vector2 _dragStartPan;
// ---------- 选择状态 ----------
private BlueprintElementBase _hoveredElement;
private BlueprintElementBase _selectedElement;
// ===== 更新(平滑插值 + 键盘轮询) =====
public override void OnUpdate(float deltaTime)
{
if (Graph?.Canvas == null) return;
var cfg = Config;
var canvas = Graph.Canvas;
// ── 同步缩放范围 ──
canvas.MinZoom = cfg.MinZoom;
canvas.MaxZoom = cfg.MaxZoom;
if (!_viewInitialized)
{
_targetPanOffset = canvas.PanOffset;
_targetZoomLevel = canvas.ZoomLevel;
_prevPanOffset = _targetPanOffset;
_prevZoomLevel = _targetZoomLevel;
_viewInitialized = true;
}
// ── 键盘轮询(新输入系统 Pan 动作值) ──
_targetPanOffset += BlueprintInputManager.LastPanDirection
* cfg.KeyboardPanSpeed * deltaTime;
// ── 键盘轮询(新输入系统 Zoom 动作值) ──
// zoomInput 对键盘按键是 ±1(按下一下),对轴是 0~1 连续值。
// 统一按一个 scroll notch = 120 缩放,scale = 120 / ZoomDivisor
float zoomInput = BlueprintInputManager.LastZoomAxis;
if (!Mathf.Approximately(zoomInput, 0f))
{
float oldZoom = _targetZoomLevel;
_targetZoomLevel = Mathf.Clamp(
_targetZoomLevel + zoomInput / cfg.ZoomDivisor,
canvas.MinZoom, canvas.MaxZoom);
// 以鼠标焦点为中心缩放(键盘无光标,从鼠标读取)
ApplyZoomFocus(ref _targetPanOffset, _targetZoomLevel, oldZoom);
// 归零,避免持续累加
BlueprintInputManager.LastZoomAxis = 0f;
}
// ── 平滑插值到目标值 ──
canvas.PanOffset = Vector2.Lerp(
canvas.PanOffset, _targetPanOffset, cfg.PanLerpSpeed * deltaTime);
canvas.ZoomLevel = Mathf.Lerp(
canvas.ZoomLevel, _targetZoomLevel, cfg.ZoomLerpSpeed * deltaTime);
// ── 标记脏 ──
if (canvas.PanOffset != _prevPanOffset || !Mathf.Approximately(canvas.ZoomLevel, _prevZoomLevel))
{
Graph.MarkDirty();
_prevPanOffset = canvas.PanOffset;
_prevZoomLevel = canvas.ZoomLevel;
}
}
// ===== 鼠标移动 =====
public override void OnPointerMove(Vector2 canvasPoint, Vector2 delta)
{
if (Graph == null || _isDragging) return;
var hit = Graph.HitTest(canvasPoint);
_hoveredElement = hit as BlueprintElementBase;
}
// ===== 鼠标按下 =====
public override void OnPointerDown(Vector2 canvasPoint, int mouseButton)
{
if (Graph == null) return;
if (mouseButton == 2) // 中键 → 平移
{
_isDragging = true;
_activeDragButton = 2;
_dragStartPan = _targetPanOffset;
}
// 左键点击选择由元素层的 IBlueprintEventHandler.OnPointerDown 处理
}
// ===== 鼠标释放 =====
public override void OnPointerUp(Vector2 canvasPoint, int mouseButton)
{
if (_isDragging && _activeDragButton == mouseButton)
{
_isDragging = false;
_activeDragButton = -1;
}
}
// ===== 拖拽 =====
public override void OnPointerDrag(Vector2 canvasPoint, Vector2 delta, int mouseButton)
{
if (Graph?.Canvas == null) return;
if (mouseButton == 2) // 中键拖拽 = 平移
{
_targetPanOffset += delta * Graph.Canvas.ZoomLevel;
Graph.MarkDirty();
}
}
// ===== 滚轮 =====
public override void OnScroll(Vector2 canvasPoint, Vector2 scrollDelta)
{
if (Graph == null) return;
float oldZoom = _targetZoomLevel;
_targetZoomLevel = Mathf.Clamp(
_targetZoomLevel + scrollDelta.y / Config.ZoomDivisor,
Graph.Canvas.MinZoom, Graph.Canvas.MaxZoom);
if (!Mathf.Approximately(oldZoom, _targetZoomLevel))
{
// 以鼠标光标为中心缩放
_targetPanOffset += canvasPoint * (oldZoom - _targetZoomLevel);
Graph.MarkDirty();
}
}
// ===== 快捷键(仅新输入系统) =====
public override void OnAction(string actionName)
{
if (Graph?.Canvas == null) return;
switch (actionName)
{
case BlueprintInputConstants.FocusHome:
ResetView();
break;
case BlueprintInputConstants.Undo:
break; // TODO
case BlueprintInputConstants.Redo:
break; // TODO
case BlueprintInputConstants.NavigateParent:
break; // TODO
}
}
// ===== 辅助 =====
public void ResetView()
{
_targetPanOffset = Vector2.zero;
_targetZoomLevel = 1f;
Graph?.MarkDirty();
}
private void SelectElement(BlueprintElementBase element)
{
_selectedElement = element;
}
/// <summary>当前悬停的元素(可能为 null)</summary>
public BlueprintElementBase HoveredElement => _hoveredElement;
/// <summary>当前选中的元素(可能为 null)</summary>
public BlueprintElementBase SelectedElement => _selectedElement;
// ===== 以鼠标焦点为中心缩放(键盘调用 — 自行读取鼠标位置) =====
private void ApplyZoomFocus(ref Vector2 panOffset, float newZoom, float oldZoom)
{
if (Graph?.Canvas == null) return;
// 通过 BlueprintUGUIInputManager 获取统一鼠标位置(已被 BlueprintInputReceiver 每帧刷新)
Vector2 canvasCursor = Graph.Canvas.ScreenToCanvas(BlueprintUGUIInputManager.MousePosition);
panOffset += canvasCursor * (oldZoom - newZoom);
}
// =====(旧 ZoomFocusAdjust 已删除 — 替代为 ApplyZoomFocus,OnScroll 直接内联 canvasPoint 计算)=====
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1fe1086d7e9750541a9a5de047e05ce6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: