update 0.6.3.7

This commit is contained in:
2026-07-30 09:18:26 +08:00
parent 54644a9f15
commit 46560ea78f
49 changed files with 1126 additions and 1325 deletions
@@ -1,7 +1,6 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using XericLibrary.Runtime.Blueprint.Render;
namespace XericLibrary.Runtime.Blueprint
{
@@ -9,14 +8,13 @@ namespace XericLibrary.Runtime.Blueprint
/// 蓝图框选工具 —— 鼠标左键在空白区域拖拽,绘制矩形选框,
/// 释放时计算框选范围内所有元素的交集并输出统计日志。
/// <para>选框 UGUI Image 默认渲染在所有层之上(SetAsLastSibling)。</para>
/// <para>框选结果保存在 <see cref="SelectedElements"/> 中供其他工具读取。</para>
/// <para>框选结果通过 <see cref="BlueprintToolProvider.GetSelector"/>(Graph).<see cref="SelectorHandle.CurrentSelection"/> 读取。</para>
/// <para>
/// 选框仅在拖拽距离超过 <c>QuickGraphInputConfig.BoxSelectThreshold</c> 后才显示,
/// 选框仅在拖拽距离超过 <see cref="BoxSelectThreshold"/> 后才显示,
/// 避免误触显示闪烁。
/// </para>
/// </summary>
[BlueprintTool(phase: ToolPhase.PreUpdate, order: 60)]
[BlueprintTheme("QuickGraph")]
public class BlueprintBoxSelectTool : BlueprintTool
{
// ---------- 状态 ----------
@@ -34,30 +32,12 @@ namespace XericLibrary.Runtime.Blueprint
// ---------- 配置 ----------
private QuickGraphInputConfig _boxConfig;
private QuickGraphInputConfig BoxConfig
{
get
{
if (_boxConfig == null && Graph != null)
{
foreach (var c in Graph.ConfigAssets)
{
if (c is QuickGraphInputConfig qc) { _boxConfig = qc; break; }
}
if (_boxConfig == null)
{
_boxConfig = ScriptableObject.CreateInstance<QuickGraphInputConfig>();
}
}
return _boxConfig;
}
}
/// <summary>框选拖拽触发阈值(画布单位)。拖拽距离超过此值后才显示选框并开始框选。</summary>
public float BoxSelectThreshold = 10f;
// ---------- 结果 ----------
/// <summary>结果:通过 Provider.CurrentSelectElements 读取框选结果。</summary>
/// <summary>结果:通过 Provider.Selector.CurrentSelection 读取框选结果。</summary>
// ===== 鼠标按下 =====
@@ -85,7 +65,7 @@ namespace XericLibrary.Runtime.Blueprint
_dragEndCanvas = canvasPoint;
// 检查拖拽距离是否超过阈值
float threshold = BoxConfig.BoxSelectThreshold;
float threshold = BoxSelectThreshold;
float dragDist = Vector2.Distance(_dragStartCanvas, _dragEndCanvas);
if (!_boxVisible && dragDist > threshold)
@@ -183,7 +163,7 @@ namespace XericLibrary.Runtime.Blueprint
UpdateSelectionBox();
// 计算选框内的元素(画布坐标空间)
BlueprintToolProvider.CurrentSelectElements.Clear();
var selectList = new System.Collections.Generic.List<IBlueprintElement>();
var min = new Vector2(
Mathf.Min(_dragStartCanvas.x, _dragEndCanvas.x),
Mathf.Min(_dragStartCanvas.y, _dragEndCanvas.y));
@@ -192,14 +172,11 @@ namespace XericLibrary.Runtime.Blueprint
Mathf.Max(_dragStartCanvas.y, _dragEndCanvas.y));
var selectRect = new Rect(min, max - min);
foreach (var element in Graph.Elements)
{
if (element == null) continue;
if (element.BoundingBox.Overlaps(selectRect))
{
BlueprintToolProvider.CurrentSelectElements.Add(element);
}
}
var selector = BlueprintToolProvider.GetSelector(Graph);
selector.RectHitTest(Graph, selectRect, selectList);
// 通过当前 Graph 的 SelectorHandle 提交多选
selector.CommitMultiSelection(selectList);
// 销毁选框
DestroySelectionBox();
@@ -207,12 +184,14 @@ namespace XericLibrary.Runtime.Blueprint
private void SelectAtPoint(Vector2 canvasPoint)
{
BlueprintToolProvider.CurrentSelectElements.Clear();
if (Graph == null) return;
var hit = Graph.HitTest(canvasPoint);
var selector = BlueprintToolProvider.GetSelector(Graph);
var hit = selector.HitTest(Graph, canvasPoint);
if (hit != null)
BlueprintToolProvider.CurrentSelectElements.Add(hit);
selector.CommitSelection(hit);
else
selector.SwapToHistory();
}
private void DestroySelectionBox()
@@ -10,12 +10,12 @@ namespace XericLibrary.Runtime.Blueprint
/// 通过 <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 readonly Vector2[] _lastLocalPos = new Vector2[3];
// ===== 快捷键轮询常量 =====
private static readonly KeyCode[] s_modifierKeys = new[]
@@ -76,16 +76,16 @@ namespace XericLibrary.Runtime.Blueprint
}
// 键盘 Pan / Zoom
GraphInputTool.LastPanDirection = Vector2.zero;
PanDirection = Vector2.zero;
float x = 0f, y = 0f;
if (Input.GetKey(KeyCode.UpArrow) || Input.GetKey(KeyCode.W)) y += 1f;
if (Input.GetKey(KeyCode.DownArrow) || Input.GetKey(KeyCode.S)) y -= 1f;
if (Input.GetKey(KeyCode.LeftArrow) || Input.GetKey(KeyCode.A)) x -= 1f;
if (Input.GetKey(KeyCode.RightArrow) || Input.GetKey(KeyCode.D)) x += 1f;
GraphInputTool.LastPanDirection = new Vector2(x, y);
GraphInputTool.LastZoomAxis = 0f;
if (Input.GetKey(KeyCode.Equals) || Input.GetKey(KeyCode.KeypadPlus)) GraphInputTool.LastZoomAxis = 1f;
if (Input.GetKey(KeyCode.Minus) || Input.GetKey(KeyCode.KeypadMinus)) GraphInputTool.LastZoomAxis = -1f;
PanDirection = new Vector2(x, y);
ZoomAxis = 0f;
if (Input.GetKey(KeyCode.Equals) || Input.GetKey(KeyCode.KeypadPlus)) ZoomAxis = 1f;
if (Input.GetKey(KeyCode.Minus) || Input.GetKey(KeyCode.KeypadMinus)) ZoomAxis = -1f;
}
/// <summary>
@@ -104,6 +104,7 @@ namespace XericLibrary.Runtime.Blueprint
{
var pt = canvas.ScreenToCanvas(Input.mousePosition);
_lastCanvasPos[button] = pt;
_lastLocalPos[button] = canvas.ScreenToLocal(Input.mousePosition);
BlueprintToolProvider.DispatchPointerDown(Graph, this, pt, button);
}
@@ -111,10 +112,13 @@ namespace XericLibrary.Runtime.Blueprint
if (sheet.IsDragging)
{
var current = canvas.ScreenToCanvas(Input.mousePosition);
var currentLocal = canvas.ScreenToLocal(Input.mousePosition);
var delta = current - _lastCanvasPos[button];
PointerLocalDelta = currentLocal - _lastLocalPos[button];
_lastCanvasPos[button] = current;
_lastLocalPos[button] = currentLocal;
if (delta.sqrMagnitude > 0.0001f)
if (delta.sqrMagnitude > 0.0001f || PointerLocalDelta.sqrMagnitude > 0.0001f)
{
BlueprintToolProvider.DispatchPointerDrag(Graph, this, current, delta, button);
}
@@ -11,7 +11,6 @@ namespace XericLibrary.Runtime.Blueprint
/// 不再依赖 <c>BlueprintInputManager</c> 管理器。
/// </summary>
[BlueprintTool(phase: ToolPhase.PreUpdate, order: 1)]
[BlueprintTheme("QuickGraph")]
public class BlueprintInputTool_InputSystem : BlueprintInputTool_UGUI
{
private InputActionAsset _inputActionAsset;
@@ -47,10 +46,10 @@ namespace XericLibrary.Runtime.Blueprint
public override void OnInitialize()
{
// 创建回调委托(确保 Bind/Unbind 使用同一引用)
_panHandler = ctx => GraphInputTool.LastPanDirection = ctx.ReadValue<Vector2>();
_panCanceled = _ => GraphInputTool.LastPanDirection = Vector2.zero;
_zoomHandler = ctx => GraphInputTool.LastZoomAxis = ctx.ReadValue<float>();
_zoomCanceled = _ => GraphInputTool.LastZoomAxis = 0f;
_panHandler = ctx => PanDirection = ctx.ReadValue<Vector2>();
_panCanceled = _ => PanDirection = Vector2.zero;
_zoomHandler = ctx => ZoomAxis = ctx.ReadValue<float>();
_zoomCanceled = _ => ZoomAxis = 0f;
_homeHandler = _ => DispatchShortcut(BlueprintInputConstants.FocusHome);
_undoHandler = _ => DispatchShortcut(BlueprintInputConstants.Undo);
_redoHandler = _ => DispatchShortcut(BlueprintInputConstants.Redo);
@@ -23,8 +23,11 @@ namespace XericLibrary.Runtime.Blueprint
private const string BackgroundGoName = "__Bp_GridBackground";
// 上一帧的鼠标画布坐标(用于计算 PointerMove delta)
// 上一帧坐标:普通交互使用 canvas delta,中键平移使用不受 Pan/Zoom 影响的 local delta。
private Vector2 _lastCanvasPos;
private Vector2 _lastLocalPos;
private Vector2 _dragStartCanvasPos;
private Vector2 _dragStartLocalPos;
/// <summary>
/// 是否通过 EventTrigger 处理鼠标按键/拖拽/滚轮事件。
@@ -37,6 +40,7 @@ namespace XericLibrary.Runtime.Blueprint
public override void OnInitialize()
{
GraphInputTool.RegisterAdapter(Graph, this);
TryBindEventTrigger();
}
@@ -62,18 +66,17 @@ namespace XericLibrary.Runtime.Blueprint
return;
}
var canvasPt = Graph.Canvas != null
? Graph.Canvas.ScreenToCanvas(BlueprintUGUIInputManager.MousePosition)
: Vector2.zero;
var canvasPt = Graph.Canvas.ScreenToCanvas(BlueprintUGUIInputManager.MousePosition);
var localPt = Graph.Canvas.ScreenToLocal(BlueprintUGUIInputManager.MousePosition);
PointerLocalDelta = localPt - _lastLocalPos;
UpdateCursorContext(canvasPt);
// ── 每帧分发 PointerMove ──
var delta = canvasPt - _lastCanvasPos;
if (delta.sqrMagnitude > 0.0001f || _lastCanvasPos == Vector2.zero)
{
BlueprintToolProvider.DispatchPointerMove(Graph, this, canvasPt, delta);
}
_lastCanvasPos = canvasPt;
_lastLocalPos = localPt;
}
/// <summary>
@@ -97,6 +100,7 @@ namespace XericLibrary.Runtime.Blueprint
public override void OnDestroy()
{
GraphInputTool.UnregisterAdapter(Graph, this);
UnbindEventTrigger();
}
@@ -237,13 +241,13 @@ namespace XericLibrary.Runtime.Blueprint
}
}
private Vector2 _dragStartPos;
private bool _hadDragThisClick;
private void OnBeginDragEvent(BaseEventData data)
{
if (!(data is PointerEventData pData)) return;
_dragStartPos = PointerCanvasPos(pData);
_dragStartCanvasPos = PointerCanvasPos(pData);
_dragStartLocalPos = Graph.Canvas.ScreenToLocal(pData.position);
_hadDragThisClick = true;
}
@@ -251,10 +255,13 @@ namespace XericLibrary.Runtime.Blueprint
{
if (!(data is PointerEventData pData)) return;
var current = PointerCanvasPos(pData);
var delta = current - _dragStartPos;
_dragStartPos = current;
var currentLocal = Graph.Canvas.ScreenToLocal(pData.position);
var canvasDelta = current - _dragStartCanvasPos;
PointerLocalDelta = currentLocal - _dragStartLocalPos;
_dragStartCanvasPos = current;
_dragStartLocalPos = currentLocal;
UpdateCursorContext(current);
BlueprintToolProvider.DispatchPointerDrag(Graph, this, current, delta, (int)pData.button);
BlueprintToolProvider.DispatchPointerDrag(Graph, this, current, canvasDelta, (int)pData.button);
}
private void OnEndDragEvent(BaseEventData data)
@@ -0,0 +1,131 @@
using UnityEngine;
namespace XericLibrary.Runtime.Blueprint
{
/// <summary>
/// 蓝图视口控制器 —— 管理视图状态(Pan/Zoom/范围),提供外部 API 供工具和外部代码读写。
/// <para>
/// 在 <c>ToolPhase.PreUpdate, order: 40</c> 执行,每帧直接将目标值写入 <see cref="IBlueprintCanvas"/>,
/// 不做插值平滑。输入工具通过写入 <see cref="TargetPanOffset"/> / <see cref="TargetZoomLevel"/>
/// 来驱动视图,外部代码可通过 <c>BlueprintToolProvider.GetTool&lt;BlueprintViewportController&gt;(graph)</c>
/// 获取实例直接控制视图。
/// </para>
/// <para>
/// 此工具为通用基础工具(无主题限定),对所有蓝图主题可见。
/// </para>
/// </summary>
[BlueprintTool(phase: ToolPhase.PreUpdate, order: 40)]
public class BlueprintViewportController : BlueprintTool
{
// ── 目标值(由输入工具或外部代码写入) ──
/// <summary>目标平移偏移量。外部代码直接设置此值即可控制视图平移。</summary>
public Vector2 TargetPanOffset = Vector2.zero;
/// <summary>目标缩放级别。外部代码直接设置此值即可控制视图缩放。</summary>
public float TargetZoomLevel = 1f;
/// <summary>最小缩放限制。</summary>
public float MinZoom = 0.1f;
/// <summary>最大缩放限制。</summary>
public float MaxZoom = 3f;
// ── 帧间脏检测 ──
private Vector2 _prevPanOffset;
private float _prevZoomLevel;
private bool _initialized;
public override void OnUpdate(float deltaTime)
{
if (Graph?.Canvas == null) return;
var canvas = Graph.Canvas;
// 首次同步
if (!_initialized)
{
TargetPanOffset = canvas.PanOffset;
TargetZoomLevel = canvas.ZoomLevel;
_prevPanOffset = TargetPanOffset;
_prevZoomLevel = TargetZoomLevel;
_initialized = true;
}
// 同步范围到 Canvas
canvas.MinZoom = MinZoom;
canvas.MaxZoom = MaxZoom;
// 钳制目标值
TargetZoomLevel = Mathf.Clamp(TargetZoomLevel, MinZoom, MaxZoom);
// 直接写入 Canvas(无插值)
canvas.PanOffset = TargetPanOffset;
canvas.ZoomLevel = TargetZoomLevel;
// 标记脏
if (TargetPanOffset != _prevPanOffset || !Mathf.Approximately(TargetZoomLevel, _prevZoomLevel))
{
Graph.MarkDirty();
_prevPanOffset = TargetPanOffset;
_prevZoomLevel = TargetZoomLevel;
}
}
// ====================================================================
// 公共 API
// ====================================================================
/// <summary>重置视图到原点,缩放归 1。</summary>
public void ResetView()
{
TargetPanOffset = Vector2.zero;
TargetZoomLevel = 1f;
Graph?.MarkDirty();
}
/// <summary>聚焦到某个画布坐标点。</summary>
public void FocusOn(Vector2 canvasCenter, float zoom = 1f)
{
TargetZoomLevel = Mathf.Clamp(zoom, MinZoom, MaxZoom);
if (Graph?.Canvas != null)
{
// local = canvas * zoom + pan;令目标画布点落在实际 viewport 的 RenderRoot local 中心。
TargetPanOffset = Graph.Canvas.GetViewportLocalRect().center - canvasCenter * TargetZoomLevel;
}
else TargetPanOffset = -canvasCenter * TargetZoomLevel;
Graph?.MarkDirty();
}
/// <summary>聚焦到一组元素的包围盒区域。</summary>
public void FocusOnElements(System.Collections.Generic.IReadOnlyList<IBlueprintElement> elements, float padding = 100f)
{
if (elements == null || elements.Count == 0) return;
if (Graph?.Canvas == null) return;
// 计算所有元素的包围盒
Rect bounds = elements[0].BoundingBox;
for (int i = 1; i < elements.Count; i++)
{
var b = elements[i].BoundingBox;
bounds.xMin = Mathf.Min(bounds.xMin, b.xMin);
bounds.yMin = Mathf.Min(bounds.yMin, b.yMin);
bounds.xMax = Mathf.Max(bounds.xMax, b.xMax);
bounds.yMax = Mathf.Max(bounds.yMax, b.yMax);
}
bounds.xMin -= padding;
bounds.yMin -= padding;
bounds.xMax += padding;
bounds.yMax += padding;
// 以实际 viewport 的 RenderRoot local 尺寸适配;再复用 FocusOn 的统一公式。
var viewportLocal = Graph.Canvas.GetViewportLocalRect();
float zoomX = bounds.width > 0.0001f ? viewportLocal.width / bounds.width : MaxZoom;
float zoomY = bounds.height > 0.0001f ? viewportLocal.height / bounds.height : MaxZoom;
float zoom = Mathf.Clamp(Mathf.Min(zoomX, zoomY), MinZoom, MaxZoom);
FocusOn(bounds.center, zoom);
}
}
}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 1fe1086d7e9750541a9a5de047e05ce6
guid: f4bb7dd4bbd0e6e4fac82161405eca13
MonoImporter:
externalObjects: {}
serializedVersion: 2
@@ -1,221 +0,0 @@
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 += GraphInputTool.LastPanDirection
* cfg.KeyboardPanSpeed * deltaTime;
// ── 键盘 / 滚轮 Zoom ──
float zoomInput = GraphInputTool.LastZoomAxis;
if (!Mathf.Approximately(zoomInput, 0f))
{
float oldZoom = _targetZoomLevel;
float zoomDelta = zoomInput / cfg.ZoomDivisor;
_targetZoomLevel = Mathf.Clamp(
_targetZoomLevel + zoomDelta,
canvas.MinZoom, canvas.MaxZoom);
ApplyZoomFocus(ref _targetPanOffset, _targetZoomLevel, oldZoom);
GraphInputTool.LastZoomAxis = 0f;
}
// ── 平滑插值到目标值 ──
canvas.PanOffset = Vector2.Lerp(
canvas.PanOffset, _targetPanOffset, cfg.PanLerpSpeed * deltaTime);
canvas.ZoomLevel = Mathf.Lerp(
canvas.ZoomLevel, _targetZoomLevel, cfg.ZoomLerpSpeed * deltaTime);
// ── 标记脏 ──
if (canvas.PanOffset != _prevPanOffset || !Mathf.Approximately(canvas.ZoomLevel, _prevZoomLevel))
{
Graph.MarkDirty();
_prevPanOffset = canvas.PanOffset;
_prevZoomLevel = canvas.ZoomLevel;
}
}
// ===== 鼠标移动 =====
public override void OnPointerMove(GraphInputTool sender, Vector2 canvasPoint, Vector2 delta)
{
if (Graph == null || _isDragging) return;
var hit = Graph.HitTest(canvasPoint);
_hoveredElement = hit as BlueprintElementBase;
}
// ===== 鼠标按下 =====
public override void OnPointerDown(GraphInputTool sender, Vector2 canvasPoint, int mouseButton)
{
if (Graph == null) return;
if (mouseButton == 2)
{
_isDragging = true;
_activeDragButton = 2;
_dragStartPan = _targetPanOffset;
}
}
// ===== 鼠标释放 =====
public override void OnPointerUp(GraphInputTool sender, Vector2 canvasPoint, int mouseButton)
{
if (_isDragging && _activeDragButton == mouseButton)
{
_isDragging = false;
_activeDragButton = -1;
}
}
// ===== 拖拽 =====
public override void OnPointerDrag(GraphInputTool sender, Vector2 canvasPoint, Vector2 delta, int mouseButton)
{
if (Graph?.Canvas == null) return;
if (mouseButton == 2)
{
_targetPanOffset += delta * Config.DragPanSpeed;
Graph.MarkDirty();
}
}
// ===== 滚轮 =====
public override void OnScroll(GraphInputTool sender, Vector2 canvasPoint, Vector2 scrollDelta)
{
if (Graph == null) return;
float oldZoom = _targetZoomLevel;
_targetZoomLevel = Mathf.Clamp(
_targetZoomLevel + scrollDelta.y / Config.ZoomDivisor,
Graph.Canvas.MinZoom, Graph.Canvas.MaxZoom);
if (!Mathf.Approximately(oldZoom, _targetZoomLevel))
{
_targetPanOffset += canvasPoint * (oldZoom - _targetZoomLevel);
Graph.MarkDirty();
}
}
// ===== 快捷键 =====
public override void OnAction(GraphInputTool sender, string actionName)
{
if (Graph?.Canvas == null) return;
switch (actionName)
{
case BlueprintInputConstants.FocusHome:
ResetView();
break;
case BlueprintInputConstants.Undo:
break;
case BlueprintInputConstants.Redo:
break;
case BlueprintInputConstants.NavigateParent:
break;
}
}
// ===== 辅助 =====
public void ResetView()
{
_targetPanOffset = Vector2.zero;
_targetZoomLevel = 1f;
Graph?.MarkDirty();
}
private void SelectElement(BlueprintElementBase element)
{
_selectedElement = element;
}
/// <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 计算)=====
}
}