update 0.6.4
This commit is contained in:
@@ -1,213 +1,24 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
using XericLibrary.Runtime.Blueprint.QuickGraph;
|
||||
namespace XericLibrary.Runtime.Blueprint
|
||||
{
|
||||
/// <summary>
|
||||
/// 蓝图框选工具 —— 鼠标左键在空白区域拖拽,绘制矩形选框,
|
||||
/// 释放时计算框选范围内所有元素的交集并输出统计日志。
|
||||
/// <para>选框 UGUI Image 默认渲染在所有层之上(SetAsLastSibling)。</para>
|
||||
/// <para>框选结果通过 <see cref="BlueprintToolProvider.GetSelector"/>(Graph).<see cref="SelectorHandle.CurrentSelection"/> 读取。</para>
|
||||
/// <para>
|
||||
/// 选框仅在拖拽距离超过 <see cref="BoxSelectThreshold"/> 后才显示,
|
||||
/// 避免误触显示闪烁。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[BlueprintTool(phase: ToolPhase.PreUpdate, order: 60)]
|
||||
public class BlueprintBoxSelectTool : BlueprintTool
|
||||
{
|
||||
// ---------- 状态 ----------
|
||||
private bool _isBoxSelecting;
|
||||
private Vector2 _dragStartCanvas; // 按下时的画布坐标
|
||||
private Vector2 _dragEndCanvas; // 当前的画布坐标
|
||||
|
||||
/// <summary>选框 GameObject 是否已创建并可见(超过阈值后)</summary>
|
||||
private bool _boxVisible;
|
||||
|
||||
// 选框 UI
|
||||
private GameObject _selectionBoxGo;
|
||||
private Image _selectionBoxImage;
|
||||
private RectTransform _selectionBoxRt;
|
||||
|
||||
// ---------- 配置 ----------
|
||||
|
||||
/// <summary>框选拖拽触发阈值(画布单位)。拖拽距离超过此值后才显示选框并开始框选。</summary>
|
||||
public float BoxSelectThreshold = 10f;
|
||||
|
||||
// ---------- 结果 ----------
|
||||
|
||||
/// <summary>结果:通过 Provider.Selector.CurrentSelection 读取框选结果。</summary>
|
||||
|
||||
// ===== 鼠标按下 =====
|
||||
|
||||
public override void OnPointerDown(GraphInputTool sender, Vector2 canvasPoint, int mouseButton)
|
||||
{
|
||||
if (Graph == null) return;
|
||||
if (mouseButton != 0) return; // 仅左键
|
||||
|
||||
// 仅在空白区域开始框选(HitTest 无结果)
|
||||
var hit = Graph.HitTest(canvasPoint);
|
||||
if (hit != null) return;
|
||||
|
||||
_isBoxSelecting = true;
|
||||
_boxVisible = false;
|
||||
_dragStartCanvas = canvasPoint;
|
||||
_dragEndCanvas = canvasPoint;
|
||||
}
|
||||
|
||||
// ===== 拖拽 =====
|
||||
|
||||
public override void OnPointerDrag(GraphInputTool sender, Vector2 canvasPoint, Vector2 delta, int mouseButton)
|
||||
{
|
||||
if (!_isBoxSelecting || mouseButton != 0) return;
|
||||
|
||||
_dragEndCanvas = canvasPoint;
|
||||
|
||||
// 检查拖拽距离是否超过阈值
|
||||
float threshold = BoxSelectThreshold;
|
||||
float dragDist = Vector2.Distance(_dragStartCanvas, _dragEndCanvas);
|
||||
|
||||
if (!_boxVisible && dragDist > threshold)
|
||||
{
|
||||
// 首次超过阈值 → 创建选框
|
||||
_boxVisible = true;
|
||||
CreateSelectionBox();
|
||||
}
|
||||
|
||||
if (_boxVisible)
|
||||
{
|
||||
UpdateSelectionBox();
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 释放 =====
|
||||
|
||||
public override void OnPointerUp(GraphInputTool sender, Vector2 canvasPoint, int mouseButton)
|
||||
{
|
||||
if (!_isBoxSelecting || mouseButton != 0) return;
|
||||
|
||||
_dragEndCanvas = canvasPoint;
|
||||
|
||||
if (_boxVisible)
|
||||
{
|
||||
FinishSelection();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 未超过阈值 → 点击行为(框选起点处的元素)
|
||||
SelectAtPoint(_dragStartCanvas);
|
||||
}
|
||||
|
||||
_isBoxSelecting = false;
|
||||
}
|
||||
|
||||
// ===== 销毁 =====
|
||||
|
||||
public override void OnDestroy()
|
||||
{
|
||||
DestroySelectionBox();
|
||||
}
|
||||
|
||||
// ===== 选框 UI =====
|
||||
|
||||
private void CreateSelectionBox()
|
||||
{
|
||||
if (Graph?.Canvas == null) return;
|
||||
var root = Graph.Canvas.GetRootTransform();
|
||||
if (root == null) return;
|
||||
|
||||
// 销毁已有的选框(应对重复点击)
|
||||
DestroySelectionBox();
|
||||
|
||||
_selectionBoxGo = new GameObject("__Bp_BoxSelect", typeof(RectTransform), typeof(Image));
|
||||
_selectionBoxGo.hideFlags = HideFlags.HideAndDontSave;
|
||||
|
||||
_selectionBoxRt = _selectionBoxGo.GetComponent<RectTransform>();
|
||||
_selectionBoxRt.SetParent(root, false);
|
||||
|
||||
// 枢轴设为左下角;锚点居中对齐 RenderRoot 枢轴(与 CanvasToLocal 空间一致)
|
||||
_selectionBoxRt.pivot = Vector2.zero;
|
||||
_selectionBoxRt.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
_selectionBoxRt.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
|
||||
_selectionBoxRt.SetAsLastSibling(); // 渲染在最顶层
|
||||
|
||||
_selectionBoxImage = _selectionBoxGo.GetComponent<Image>();
|
||||
_selectionBoxImage.color = new Color(0.2f, 0.5f, 1.0f, 0.15f);
|
||||
var outline = _selectionBoxGo.AddComponent<Outline>();
|
||||
outline.effectColor = Color.white;
|
||||
outline.effectDistance = new Vector2(1, -1);
|
||||
}
|
||||
|
||||
private void UpdateSelectionBox()
|
||||
{
|
||||
if (_selectionBoxRt == null || Graph?.Canvas == null) return;
|
||||
|
||||
// 将画布坐标转为 RenderRoot 本地空间坐标(CanvasToLocal)
|
||||
var startLocal = Graph.Canvas.CanvasToLocal(_dragStartCanvas);
|
||||
var endLocal = Graph.Canvas.CanvasToLocal(_dragEndCanvas);
|
||||
|
||||
// 计算 RectTransform 位置和尺寸(RenderRoot 本地空间)
|
||||
var minX = Mathf.Min(startLocal.x, endLocal.x);
|
||||
var minY = Mathf.Min(startLocal.y, endLocal.y);
|
||||
var maxX = Mathf.Max(startLocal.x, endLocal.x);
|
||||
var maxY = Mathf.Max(startLocal.y, endLocal.y);
|
||||
|
||||
_selectionBoxRt.anchoredPosition = new Vector2(minX, minY);
|
||||
_selectionBoxRt.sizeDelta = new Vector2(maxX - minX, maxY - minY);
|
||||
}
|
||||
|
||||
private void FinishSelection()
|
||||
{
|
||||
UpdateSelectionBox();
|
||||
|
||||
// 计算选框内的元素(画布坐标空间)
|
||||
var selectList = new System.Collections.Generic.List<IBlueprintElement>();
|
||||
var min = new Vector2(
|
||||
Mathf.Min(_dragStartCanvas.x, _dragEndCanvas.x),
|
||||
Mathf.Min(_dragStartCanvas.y, _dragEndCanvas.y));
|
||||
var max = new Vector2(
|
||||
Mathf.Max(_dragStartCanvas.x, _dragEndCanvas.x),
|
||||
Mathf.Max(_dragStartCanvas.y, _dragEndCanvas.y));
|
||||
var selectRect = new Rect(min, max - min);
|
||||
|
||||
var selector = BlueprintToolProvider.GetSelector(Graph);
|
||||
selector.RectHitTest(Graph, selectRect, selectList);
|
||||
|
||||
// 通过当前 Graph 的 SelectorHandle 提交多选
|
||||
selector.CommitMultiSelection(selectList);
|
||||
|
||||
// 销毁选框
|
||||
DestroySelectionBox();
|
||||
}
|
||||
|
||||
private void SelectAtPoint(Vector2 canvasPoint)
|
||||
{
|
||||
if (Graph == null) return;
|
||||
|
||||
var selector = BlueprintToolProvider.GetSelector(Graph);
|
||||
var hit = selector.HitTest(Graph, canvasPoint);
|
||||
if (hit != null)
|
||||
selector.CommitSelection(hit);
|
||||
else
|
||||
selector.SwapToHistory();
|
||||
}
|
||||
|
||||
private void DestroySelectionBox()
|
||||
{
|
||||
if (_selectionBoxGo != null)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if (!Application.isPlaying)
|
||||
Object.DestroyImmediate(_selectionBoxGo);
|
||||
else
|
||||
#endif
|
||||
Object.Destroy(_selectionBoxGo);
|
||||
_selectionBoxGo = null;
|
||||
_selectionBoxRt = null;
|
||||
_selectionBoxImage = null;
|
||||
}
|
||||
}
|
||||
private bool _selecting, _visible; private Vector2 _start, _end; private GameObject _box; private Image _image; private RectTransform _rect; private Outline _outline; private QuickGraphRenderStyleResolver _styles;
|
||||
public override void OnInitialize() { _styles = new QuickGraphRenderStyleResolver(Graph); _styles.Changed += _ => ApplyStyle(); }
|
||||
public override void OnConfigChanged() => _styles?.Refresh();
|
||||
public override void OnPointerDown(GraphInputTool sender, Vector2 point, int button) { if (button != 0 || Graph == null || Graph.HitTest(point) != null) return; _selecting = true; _visible = false; _start = _end = point; }
|
||||
public override void OnPointerDrag(GraphInputTool sender, Vector2 point, Vector2 delta, int button) { if (!_selecting || button != 0) return; _end = point; if (!_visible && Vector2.Distance(_start, _end) > _styles.Snapshot.Selection.DragThreshold) { _visible = true; Create(); } if (_visible) UpdateBox(); }
|
||||
public override void OnPointerUp(GraphInputTool sender, Vector2 point, int button) { if (!_selecting || button != 0) return; _end = point; if (_visible) Finish(); else SelectAt(_start); _selecting = false; }
|
||||
private void Create() { if (Graph?.Canvas == null) return; var root = Graph.Canvas.GetRootTransform(); if (root == null) return; DestroyBox(); _box = new GameObject("__Bp_BoxSelect", typeof(RectTransform), typeof(Image), typeof(Outline)); _box.hideFlags = HideFlags.HideAndDontSave; _rect = _box.GetComponent<RectTransform>(); _rect.SetParent(root, false); _rect.pivot = Vector2.zero; _rect.anchorMin = _rect.anchorMax = new Vector2(.5f, .5f); _rect.SetAsLastSibling(); _image = _box.GetComponent<Image>(); _outline = _box.GetComponent<Outline>(); ApplyStyle(); }
|
||||
private void ApplyStyle() { if (_image == null || _styles == null) return; var s = _styles.Snapshot.Selection; _image.color = s.FillColor; _image.raycastTarget = s.RaycastTarget; _outline.effectColor = s.OutlineColor; _outline.effectDistance = s.OutlineOffset; }
|
||||
private void UpdateBox() { if (_rect == null || Graph?.Canvas == null) return; var a = Graph.Canvas.CanvasToLocal(_start); var b = Graph.Canvas.CanvasToLocal(_end); var min = Vector2.Min(a, b); _rect.anchoredPosition = min; _rect.sizeDelta = Vector2.Max(a, b) - min; }
|
||||
private void Finish() { UpdateBox(); var min = Vector2.Min(_start, _end); var selector = BlueprintToolProvider.GetSelector(Graph); var result = new List<IBlueprintElement>(); selector.RectHitTest(Graph, new Rect(min, Vector2.Max(_start, _end) - min), result); selector.CommitMultiSelection(result); DestroyBox(); }
|
||||
private void SelectAt(Vector2 point) { var selector = BlueprintToolProvider.GetSelector(Graph); var hit = selector.HitTest(Graph, point); if (hit != null) selector.CommitSelection(hit); else selector.SwapToHistory(); }
|
||||
private void DestroyBox() { if (_box == null) return; if (Application.isPlaying) Object.Destroy(_box); else Object.DestroyImmediate(_box); _box = null; _image = null; _rect = null; _outline = null; }
|
||||
public override void OnDestroy() { _styles?.Dispose(); _styles = null; DestroyBox(); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,8 +107,8 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
/// <summary>
|
||||
/// 确保场景中存在 EventSystem 和合适的 InputModule。
|
||||
/// 旧输入系统 → StandaloneInputModule。
|
||||
/// 新输入系统 → InputSystemUIInputModule(由 GraphTheoryBlueprintComponent 创建)。
|
||||
/// 不会重复创建模块(通过 GetComponent 检查)。
|
||||
/// 新输入系统 → InputSystemUIInputModule;无蓝图资产配置时使用模块默认动作兜底。
|
||||
/// 不会覆盖 GraphTheoryBlueprintComponent 已配置的模块。
|
||||
/// </summary>
|
||||
private static void EnsureEventSystemExists()
|
||||
{
|
||||
@@ -119,7 +119,17 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
es = go.AddComponent<EventSystem>();
|
||||
}
|
||||
|
||||
#if !ENABLE_INPUT_SYSTEM
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
if (es.GetComponent<UnityEngine.InputSystem.UI.InputSystemUIInputModule>() == null)
|
||||
{
|
||||
var standalone = es.GetComponent<StandaloneInputModule>();
|
||||
if (standalone != null)
|
||||
Object.DestroyImmediate(standalone);
|
||||
|
||||
var uiModule = es.gameObject.AddComponent<UnityEngine.InputSystem.UI.InputSystemUIInputModule>();
|
||||
uiModule.AssignDefaultActions();
|
||||
}
|
||||
#else
|
||||
if (es.GetComponent<StandaloneInputModule>() == null)
|
||||
{
|
||||
es.gameObject.AddComponent<StandaloneInputModule>();
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
/// 蓝图视口控制器 —— 管理视图状态(Pan/Zoom/范围),提供外部 API 供工具和外部代码读写。
|
||||
/// <para>
|
||||
/// 在 <c>ToolPhase.PreUpdate, order: 40</c> 执行,每帧直接将目标值写入 <see cref="IBlueprintCanvas"/>,
|
||||
/// 不做插值平滑。输入工具通过写入 <see cref="TargetPanOffset"/> / <see cref="TargetZoomLevel"/>
|
||||
/// 不做插值平滑或消费端缩放钳制。输入工具通过写入 <see cref="TargetPanOffset"/> / <see cref="TargetZoomLevel"/>
|
||||
/// 来驱动视图,外部代码可通过 <c>BlueprintToolProvider.GetTool<BlueprintViewportController>(graph)</c>
|
||||
/// 获取实例直接控制视图。
|
||||
/// </para>
|
||||
@@ -23,13 +23,7 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
public Vector2 TargetPanOffset = Vector2.zero;
|
||||
|
||||
/// <summary>目标缩放级别。外部代码直接设置此值即可控制视图缩放。</summary>
|
||||
public float TargetZoomLevel = 1f;
|
||||
|
||||
/// <summary>最小缩放限制。</summary>
|
||||
public float MinZoom = 0.1f;
|
||||
|
||||
/// <summary>最大缩放限制。</summary>
|
||||
public float MaxZoom = 3f;
|
||||
public float TargetZoomLevel;
|
||||
|
||||
// ── 帧间脏检测 ──
|
||||
|
||||
@@ -43,22 +37,19 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
|
||||
var canvas = Graph.Canvas;
|
||||
|
||||
// 首次同步
|
||||
// 首次同步使用样式默认缩放。
|
||||
if (!_initialized)
|
||||
{
|
||||
TargetPanOffset = canvas.PanOffset;
|
||||
TargetZoomLevel = canvas.ZoomLevel;
|
||||
TargetZoomLevel = Graph.GetRenderStyleValue<float>("/quickgraph/input/defaultZoom");
|
||||
_prevPanOffset = TargetPanOffset;
|
||||
_prevZoomLevel = TargetZoomLevel;
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
// 同步范围到 Canvas
|
||||
canvas.MinZoom = MinZoom;
|
||||
canvas.MaxZoom = MaxZoom;
|
||||
|
||||
// 钳制目标值
|
||||
TargetZoomLevel = Mathf.Clamp(TargetZoomLevel, MinZoom, MaxZoom);
|
||||
// 缩放范围由样式写入 Canvas,由 Canvas 负责约束。
|
||||
canvas.MinZoom = Graph.GetRenderStyleValue<float>("/quickgraph/input/minZoom");
|
||||
canvas.MaxZoom = Graph.GetRenderStyleValue<float>("/quickgraph/input/maxZoom");
|
||||
|
||||
// 直接写入 Canvas(无插值)
|
||||
canvas.PanOffset = TargetPanOffset;
|
||||
@@ -77,18 +68,18 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
// 公共 API
|
||||
// ====================================================================
|
||||
|
||||
/// <summary>重置视图到原点,缩放归 1。</summary>
|
||||
/// <summary>重置视图到原点和样式默认缩放。</summary>
|
||||
public void ResetView()
|
||||
{
|
||||
TargetPanOffset = Vector2.zero;
|
||||
TargetZoomLevel = 1f;
|
||||
TargetZoomLevel = Graph.GetRenderStyleValue<float>("/quickgraph/input/defaultZoom");
|
||||
Graph?.MarkDirty();
|
||||
}
|
||||
|
||||
/// <summary>聚焦到某个画布坐标点。</summary>
|
||||
public void FocusOn(Vector2 canvasCenter, float zoom = 1f)
|
||||
/// <summary>聚焦到某个画布坐标点;省略缩放时使用样式聚焦缩放。</summary>
|
||||
public void FocusOn(Vector2 canvasCenter, float? zoom = null)
|
||||
{
|
||||
TargetZoomLevel = Mathf.Clamp(zoom, MinZoom, MaxZoom);
|
||||
TargetZoomLevel = zoom ?? Graph.GetRenderStyleValue<float>("/quickgraph/input/focusZoom");
|
||||
if (Graph?.Canvas != null)
|
||||
{
|
||||
// local = canvas * zoom + pan;令目标画布点落在实际 viewport 的 RenderRoot local 中心。
|
||||
@@ -98,12 +89,14 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
Graph?.MarkDirty();
|
||||
}
|
||||
|
||||
/// <summary>聚焦到一组元素的包围盒区域。</summary>
|
||||
public void FocusOnElements(System.Collections.Generic.IReadOnlyList<IBlueprintElement> elements, float padding = 100f)
|
||||
/// <summary>聚焦到一组元素的包围盒区域,使用样式聚焦边距。</summary>
|
||||
public void FocusOnElements(System.Collections.Generic.IReadOnlyList<IBlueprintElement> elements)
|
||||
{
|
||||
if (elements == null || elements.Count == 0) return;
|
||||
if (Graph?.Canvas == null) return;
|
||||
|
||||
float padding = Graph.GetRenderStyleValue<float>("/quickgraph/input/focusPadding");
|
||||
|
||||
// 计算所有元素的包围盒
|
||||
Rect bounds = elements[0].BoundingBox;
|
||||
for (int i = 1; i < elements.Count; i++)
|
||||
@@ -122,10 +115,9 @@ namespace XericLibrary.Runtime.Blueprint
|
||||
|
||||
// 以实际 viewport 的 RenderRoot local 尺寸适配;再复用 FocusOn 的统一公式。
|
||||
var viewportLocal = Graph.Canvas.GetViewportLocalRect();
|
||||
float zoomX = bounds.width > 0.0001f ? viewportLocal.width / bounds.width : MaxZoom;
|
||||
float zoomY = bounds.height > 0.0001f ? viewportLocal.height / bounds.height : MaxZoom;
|
||||
float zoom = Mathf.Clamp(Mathf.Min(zoomX, zoomY), MinZoom, MaxZoom);
|
||||
FocusOn(bounds.center, zoom);
|
||||
float zoomX = bounds.width > 0.0001f ? viewportLocal.width / bounds.width : Graph.GetRenderStyleValue<float>("/quickgraph/input/maxZoom");
|
||||
float zoomY = bounds.height > 0.0001f ? viewportLocal.height / bounds.height : Graph.GetRenderStyleValue<float>("/quickgraph/input/maxZoom");
|
||||
FocusOn(bounds.center, Mathf.Min(zoomX, zoomY));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user