表格添加四叉树渲染优化逻辑。

This commit is contained in:
2026-06-30 19:53:12 +08:00
parent f7e489e924
commit 40fbfed45c
7 changed files with 1104 additions and 189 deletions
+331
View File
@@ -0,0 +1,331 @@
using System.Collections.Generic;
using UnityEngine;
namespace XericUI.XTable.Core
{
/// <summary>
/// 表格四叉树构建用的单元格元数据。
/// </summary>
public struct XTableQuadtreeCell
{
public int Row;
public int Col;
public string Namespace;
}
/// <summary>
/// 四叉树查询结果——一个均匀象限的可见部分。
/// </summary>
public struct QuadrantResult
{
/// <summary>该象限在表格像素空间中的区域(左上角坐标系,y 向下为正)</summary>
public Rect Region;
/// <summary>统一命名空间(空字符串 = 空区域)</summary>
public string Namespace;
/// <summary>该象限包含的单元格坐标列表</summary>
public List<(int row, int col)> Cells;
}
/// <summary>
/// 表格四叉树——基于单元格 <see cref="XTableQuadtreeCell.Namespace"/> 进行递归区域分割。
/// 若一个象限内存在不同命名空间的单元格,则继续分裂,直到所有差异分开。
/// 渲染阶段通过 <see cref="QueryVisible"/> 获取可见范围内的所有均匀象限。
/// </summary>
public class XTableQuadtree
{
#region 内部节点
private class Node
{
/// <summary>当前象限在表格像素空间中的区域</summary>
public Rect Region;
/// <summary>
/// null = 非均匀节点(有子节点),非 null = 均匀叶子节点。
/// 空字符串 "" 表示该区域无任何单元格。
/// </summary>
public string UniformNamespace;
/// <summary>子象限(null 表示叶子节点)</summary>
public Node[] Children;
/// <summary>该均匀象限包含的单元格坐标(仅叶子均匀节点有效)</summary>
public List<(int, int)> Cells;
public bool IsUniform => UniformNamespace != null;
}
#endregion
#region 字段
private Node m_Root;
private float[] m_RowHeights;
private float[] m_ColWidths;
/// <summary>四叉树是否已构建完成</summary>
public bool IsBuilt { get; private set; }
#endregion
#region 构建
/// <summary>
/// 全量构建四叉树。
/// </summary>
/// <param name="totalWidth">表格总宽度(像素)</param>
/// <param name="totalHeight">表格总高度(像素)</param>
/// <param name="rowHeights">行高数组(索引 = 行号)</param>
/// <param name="colWidths">列宽数组(索引 = 列号)</param>
/// <param name="cells">所有有数据的单元格元数据列表</param>
public void Build(float totalWidth, float totalHeight,
float[] rowHeights, float[] colWidths,
List<XTableQuadtreeCell> cells)
{
if (rowHeights == null || colWidths == null || cells == null)
{
IsBuilt = false;
return;
}
m_RowHeights = rowHeights;
m_ColWidths = colWidths;
m_Root = new Node { Region = new Rect(0, 0, totalWidth, totalHeight) };
int maxRow = rowHeights.Length - 1;
int maxCol = colWidths.Length - 1;
if (maxRow >= 0 && maxCol >= 0)
BuildNode(m_Root, cells, 0, maxRow, 0, maxCol);
else
m_Root.UniformNamespace = "";
IsBuilt = true;
}
/// <summary>强制重建(外部入口,通常由表格组件在数据/布局变更后调用)</summary>
public void ForceRebuild(float totalWidth, float totalHeight,
float[] rowHeights, float[] colWidths,
List<XTableQuadtreeCell> cells)
{
Clear();
Build(totalWidth, totalHeight, rowHeights, colWidths, cells);
}
private void BuildNode(Node node, List<XTableQuadtreeCell> allCells,
int minRow, int maxRow, int minCol, int maxCol)
{
// 收集当前行列范围内的单元格
var cellsInRange = new List<XTableQuadtreeCell>();
for (int i = 0; i < allCells.Count; i++)
{
var cell = allCells[i];
if (cell.Row >= minRow && cell.Row <= maxRow &&
cell.Col >= minCol && cell.Col <= maxCol)
cellsInRange.Add(cell);
}
// 空区域
if (cellsInRange.Count == 0)
{
node.UniformNamespace = "";
return;
}
// 检查是否所有单元格命名空间一致
string firstNs = NormalizeNs(cellsInRange[0].Namespace);
bool allSame = true;
for (int i = 1; i < cellsInRange.Count; i++)
{
if (NormalizeNs(cellsInRange[i].Namespace) != firstNs)
{
allSame = false;
break;
}
}
if (allSame)
{
node.UniformNamespace = firstNs;
node.Cells = new List<(int, int)>(cellsInRange.Count);
for (int i = 0; i < cellsInRange.Count; i++)
node.Cells.Add((cellsInRange[i].Row, cellsInRange[i].Col));
return;
}
// 无法继续分裂(单行单列),强制标记为均匀
if (minRow >= maxRow && minCol >= maxCol)
{
node.UniformNamespace = firstNs;
node.Cells = new List<(int, int)>(cellsInRange.Count);
for (int i = 0; i < cellsInRange.Count; i++)
node.Cells.Add((cellsInRange[i].Row, cellsInRange[i].Col));
return;
}
// 分裂:在行列索引空间取中点
int midRow = (minRow + maxRow) / 2;
int midCol = (minCol + maxCol) / 2;
node.Children = new Node[4];
// 左上 (minRow..midRow, minCol..midCol)
node.Children[0] = MakeChild(minRow, midRow, minCol, midCol);
BuildNode(node.Children[0], allCells, minRow, midRow, minCol, midCol);
// 右上 (minRow..midRow, midCol+1..maxCol)
node.Children[1] = MakeChild(minRow, midRow, midCol + 1, maxCol);
BuildNode(node.Children[1], allCells, minRow, midRow, midCol + 1, maxCol);
// 左下 (midRow+1..maxRow, minCol..midCol)
node.Children[2] = MakeChild(midRow + 1, maxRow, minCol, midCol);
BuildNode(node.Children[2], allCells, midRow + 1, maxRow, minCol, midCol);
// 右下 (midRow+1..maxRow, midCol+1..maxCol)
node.Children[3] = MakeChild(midRow + 1, maxRow, midCol + 1, maxCol);
BuildNode(node.Children[3], allCells, midRow + 1, maxRow, midCol + 1, maxCol);
}
private Node MakeChild(int minRow, int maxRow, int minCol, int maxCol)
{
float x = GetColLeftX(minCol);
float y = GetRowTopY(minRow);
float w = GetColRightX(maxCol) - x;
float h = GetRowBottomY(maxRow) - y;
return new Node { Region = new Rect(x, y, w, h) };
}
#endregion
#region 查询
/// <summary>
/// 查询可见矩形内的所有均匀象限。
/// </summary>
/// <param name="visibleRect">可见矩形(左上角坐标系)</param>
/// <param name="results">结果列表(复用外部容器)</param>
public void QueryVisible(Rect visibleRect, List<QuadrantResult> results)
{
if (results == null)
results = new List<QuadrantResult>();
if (m_Root == null)
return;
QueryNode(m_Root, visibleRect, results);
}
private void QueryNode(Node node, Rect visibleRect, List<QuadrantResult> results)
{
// 不相交则跳过
if (!RectIntersects(node.Region, visibleRect))
return;
if (node.IsUniform)
{
// 裁剪到可见范围
Rect clipped = RectIntersection(node.Region, visibleRect);
if (clipped.width > 0 && clipped.height > 0)
{
results.Add(new QuadrantResult
{
Region = clipped,
Namespace = node.UniformNamespace,
Cells = node.Cells
});
}
return;
}
// 非均匀节点:递归查询子节点
if (node.Children != null)
{
for (int i = 0; i < node.Children.Length; i++)
{
if (node.Children[i] != null)
QueryNode(node.Children[i], visibleRect, results);
}
}
}
#endregion
#region 清理
/// <summary>清空四叉树</summary>
public void Clear()
{
m_Root = null;
m_RowHeights = null;
m_ColWidths = null;
IsBuilt = false;
}
#endregion
#region 坐标计算
private float GetRowTopY(int row)
{
float y = 0;
for (int i = 0; i < row && i < m_RowHeights.Length; i++)
y += m_RowHeights[i];
return y;
}
private float GetRowBottomY(int row)
{
float y = 0;
for (int i = 0; i <= row && i < m_RowHeights.Length; i++)
y += m_RowHeights[i];
return y;
}
private float GetColLeftX(int col)
{
float x = 0;
for (int i = 0; i < col && i < m_ColWidths.Length; i++)
x += m_ColWidths[i];
return x;
}
private float GetColRightX(int col)
{
float x = 0;
for (int i = 0; i <= col && i < m_ColWidths.Length; i++)
x += m_ColWidths[i];
return x;
}
#endregion
#region 工具
/// <summary>命名空间规范化:null、空串、默认命名空间统一为 "xeric_table_default"</summary>
private static string NormalizeNs(string ns)
{
if (string.IsNullOrEmpty(ns) || ns == "xeric_table_default")
return "xeric_table_default";
return ns;
}
private static bool RectIntersects(Rect a, Rect b)
{
return a.x < b.xMax && a.xMax > b.x &&
a.y < b.yMax && a.yMax > b.y;
}
private static Rect RectIntersection(Rect a, Rect b)
{
float x = Mathf.Max(a.x, b.x);
float y = Mathf.Max(a.y, b.y);
float xMax = Mathf.Min(a.xMax, b.xMax);
float yMax = Mathf.Min(a.yMax, b.yMax);
return new Rect(x, y, Mathf.Max(0, xMax - x), Mathf.Max(0, yMax - y));
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 90a1712e28eeec248b95350379e42764
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,194 @@
using System;
using UnityEngine;
namespace XericUI.XTable.Core
{
/// <summary>
/// 表格选择句柄——统一管理单选/多选状态。
/// 外部可创建此句柄并通过表格的 SetSelectionHandle 或直接操作句柄来控制选区。
/// 选区变更时触发 OnSelectionChanged 事件,表格监听此事件进行重绘。
/// </summary>
public class XTableSelectionHandle
{
#region 属性
/// <summary>起始行(多选锚点行,单选当前行)</summary>
public int StartRow { get; private set; } = -1;
/// <summary>起始列(多选锚点列,单选当前列)</summary>
public int StartCol { get; private set; } = -1;
/// <summary>结束行(仅多选有效,等于 StartRow 时为单选)</summary>
public int EndRow { get; private set; } = -1;
/// <summary>结束列(仅多选有效,等于 StartCol 时为单选)</summary>
public int EndCol { get; private set; } = -1;
/// <summary>是否有有效选择</summary>
public bool HasSelection => StartRow >= 0 && StartCol >= 0;
/// <summary>是否为多选(起止不同)</summary>
public bool HasMultiSelection =>
HasSelection &&
(StartRow != EndRow || StartCol != EndCol);
/// <summary>选中区域最小行</summary>
public int MinRow => HasSelection ? Mathf.Min(StartRow, EndRow) : -1;
/// <summary>选中区域最大行</summary>
public int MaxRow => HasSelection ? Mathf.Max(StartRow, EndRow) : -1;
/// <summary>选中区域最小列</summary>
public int MinCol => HasSelection ? Mathf.Min(StartCol, EndCol) : -1;
/// <summary>选中区域最大列</summary>
public int MaxCol => HasSelection ? Mathf.Max(StartCol, EndCol) : -1;
#endregion
#region 事件
/// <summary>选区变化时触发</summary>
public event Action<XTableSelectionHandle> OnSelectionChanged;
#endregion
#region 公开方法
/// <summary>
/// 单选一个单元格(同时清除多选范围)
/// </summary>
public void Select(int row, int col)
{
StartRow = row;
StartCol = col;
EndRow = row;
EndCol = col;
NotifyChanged();
}
/// <summary>
/// 多选一个范围
/// </summary>
public void Select(int startRow, int startCol, int endRow, int endCol)
{
StartRow = startRow;
StartCol = startCol;
EndRow = endRow;
EndCol = endCol;
NotifyChanged();
}
/// <summary>
/// 扩展多选范围到目标格(保持原有锚点)
/// </summary>
public void ExtendTo(int row, int col)
{
if (!HasSelection)
{
Select(row, col);
return;
}
EndRow = row;
EndCol = col;
NotifyChanged();
}
/// <summary>
/// 清除选择
/// <summary>清除选择</summary>
public void Clear()
{
if (!HasSelection) return;
StartRow = -1;
StartCol = -1;
EndRow = -1;
EndCol = -1;
NotifyChanged();
}
/// <summary>将选区裁剪到 [0, maxRow] × [0, maxCol] 范围</summary>
public void ClampTo(int maxRow, int maxCol)
{
if (!HasSelection) return;
int oldStartRow = StartRow, oldStartCol = StartCol;
int oldEndRow = EndRow, oldEndCol = EndCol;
StartRow = Mathf.Clamp(StartRow, -1, maxRow);
StartCol = Mathf.Clamp(StartCol, -1, maxCol);
EndRow = Mathf.Clamp(EndRow, -1, maxRow);
EndCol = Mathf.Clamp(EndCol, -1, maxCol);
if (StartRow < 0 || StartCol < 0)
Clear();
else if (oldStartRow != StartRow || oldStartCol != StartCol ||
oldEndRow != EndRow || oldEndCol != EndCol)
NotifyChanged();
}
/// <summary>
/// 判断指定坐标是否在选中范围内
/// </summary>
public bool IsInRange(int row, int col)
{
if (!HasSelection) return false;
int minR = Mathf.Min(StartRow, EndRow);
int maxR = Mathf.Max(StartRow, EndRow);
int minC = Mathf.Min(StartCol, EndCol);
int maxC = Mathf.Max(StartCol, EndCol);
return row >= minR && row <= maxR && col >= minC && col <= maxC;
}
/// <summary>左移一格</summary>
public int MoveLeft(int maxCol = 0)
{
if (!HasSelection) return -1;
int newCol = Mathf.Max(maxCol, StartCol - 1);
if (newCol != StartCol)
Select(StartRow, newCol);
return newCol;
}
/// <summary>右移一格</summary>
public int MoveRight(int maxCol)
{
if (!HasSelection) return -1;
int newCol = Mathf.Min(maxCol, StartCol + 1);
if (newCol != StartCol)
Select(StartRow, newCol);
return newCol;
}
/// <summary>上移一格</summary>
public int MoveUp(int maxRow = 0)
{
if (!HasSelection) return -1;
int newRow = Mathf.Max(maxRow, StartRow - 1);
if (newRow != StartRow)
Select(newRow, StartCol);
return newRow;
}
/// <summary>下移一格</summary>
public int MoveDown(int maxRow)
{
if (!HasSelection) return -1;
int newRow = Mathf.Min(maxRow, StartRow + 1);
if (newRow != StartRow)
Select(newRow, StartCol);
return newRow;
}
#endregion
#region 内部
private void NotifyChanged()
{
OnSelectionChanged?.Invoke(this);
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 872c2bc5f6f1a3f47a655bdf059656bd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: