using System; using System.Collections.Generic; using UnityEngine; using XericUI.XTable.Mapping; namespace XericUI.XTable.Core { public enum XTableBorderAxis : byte { Horizontal, Vertical, } [Serializable] public struct XTableBorderRange { public XTableBorderAxis Axis; public int FixedIndex; public int StartIndex; public int Length; public int StyleIndex; public int EndIndex => StartIndex + Length; } /// /// 表格数据主类——持有块字典,提供单元格级别的读写接口。 /// 块按需创建(SetCell 时自动创建不存在的块),通过 Z 曲线索引映射。 /// [Serializable] public class XTableData : ISerializationCallbackReceiver { /// 全系统唯一默认块尺寸——禁止硬编码字面量 public const int DefaultBlockSize = 32; #region 属性 /// 块内列数(默认 [field: SerializeField] public int BlockSizeX { get; private set; } /// 块内行数(默认 [field: SerializeField] public int BlockSizeY { get; private set; } /// /// 块字典——key = Z 曲线索引(ulong),value = 数据块。 /// 具有哈希表性质,按需创建,键不连续(离散存储)。 /// 注意:Dictionary 不被 Unity 序列化器原生支持,通过 / 代理序列化。 /// [NonSerialized] public Dictionary BlockMap; // ── Dictionary 序列化代理 ── // Unity 无法直接序列化 Dictionary, // 使用两个平行列表在 OnBeforeSerialize/OnAfterDeserialize 中转换。 [SerializeField] private List m_BlockKeys = new List(); [SerializeField] private List m_BlockValues = new List(); [SerializeField] private List m_BorderStyles = new List(); [SerializeField] private List m_BorderRanges = new List(); // 仅用于读取旧资产的外边框序列化代理;迁移后不再写入。 [SerializeField] private List m_OuterBorderRows = new List(); [SerializeField] private List m_OuterBorderCols = new List(); [SerializeField] private List m_OuterBorderValues = new List(); [NonSerialized] private Dictionary<(XTableBorderAxis axis, int fixedIndex), List> m_BorderRangeIndex; /// /// 获取或创建单元格——如果单元格不存在则自动创建空数据。 /// 在编辑器和运行时都是安全的,保证不会返回 null。 /// public XTableCellData GetOrCreateCell(int row, int col) { var data = GetCell(row, col); if (data == null) { data = new XTableCellData(); SetCell(row, col, data); } return data; } /// /// 判断指定单元格是否有数据 /// public bool HasCell(int row, int col) { return GetCell(row, col) != null; } #endregion #region 构造函数 /// /// 创建表格数据实例 /// /// 块内列数,默认 /// 块内行数,默认 public XTableData(int blockSizeX = DefaultBlockSize, int blockSizeY = DefaultBlockSize) { BlockSizeX = blockSizeX > 0 ? blockSizeX : DefaultBlockSize; BlockSizeY = blockSizeY > 0 ? blockSizeY : DefaultBlockSize; BlockMap = new Dictionary(); } #endregion #region 序列化回调 public void OnBeforeSerialize() { if (m_BorderRangeIndex != null) RebuildSerializedBorderRanges(); // Dictionary → 平行列表 m_BlockKeys.Clear(); m_BlockValues.Clear(); if (BlockMap != null) { foreach (var kv in BlockMap) { m_BlockKeys.Add(kv.Key); m_BlockValues.Add(kv.Value); } } } public void OnAfterDeserialize() { if (m_BorderStyles == null) m_BorderStyles = new List(); if (m_BorderRanges == null) m_BorderRanges = new List(); m_BorderRangeIndex = null; if (BlockSizeX <= 0) BlockSizeX = DefaultBlockSize; if (BlockSizeY <= 0) BlockSizeY = DefaultBlockSize; // 平行列表 → Dictionary BlockMap = new Dictionary(); int count = System.Math.Min( m_BlockKeys?.Count ?? 0, m_BlockValues?.Count ?? 0); for (int i = 0; i < count; i++) { if (m_BlockValues[i] != null) BlockMap[m_BlockKeys[i]] = m_BlockValues[i]; } MigrateLegacyBorderCache(); } #endregion #region 核心访问方法 /// /// 通过全局行列坐标获取单元格数据。 /// 先计算块坐标和 Z 索引,查找块字典,再检查合并重定向。 /// /// 单元格数据,不存在则返回 null public XTableCellData GetCell(int row, int col) { XTableCoordinateUtility.CellToBlockCoordinate( row, col, BlockSizeX, BlockSizeY, out int blockRow, out int blockCol, out int localRow, out int localCol); ulong zIndex = XTableBlockMapping.BlockCoordToZIndex(blockRow, blockCol); if (!BlockMap.TryGetValue(zIndex, out XTableBlock block)) return null; // 检查合并重定向 XTableMergeDescriptor merge = block.GetMergeRedirect(localRow, localCol); if (merge != null) { merge.GetRedirectTarget(out ulong targetBlockIndex, out int targetRow, out int targetCol); if (merge.IsCrossBlock) { // 跨块引用:跳转到目标块 if (!BlockMap.TryGetValue(targetBlockIndex, out XTableBlock targetBlock)) return null; return targetBlock.GetCell(targetRow, targetCol); } else { // 本地引用:同块内重定向 return block.GetCell(targetRow, targetCol); } } return block.GetCell(localRow, localCol); } /// /// 尝试获取单元格数据 /// public bool TryGetCell(int row, int col, out XTableCellData data) { data = GetCell(row, col); return data != null; } /// /// 通过全局行列坐标设置单元格数据。 /// 如果对应的块不存在,则按需创建。 /// public void SetCell(int row, int col, XTableCellData data) { XTableCoordinateUtility.CellToBlockCoordinate( row, col, BlockSizeX, BlockSizeY, out int blockRow, out int blockCol, out int localRow, out int localCol); ulong zIndex = XTableBlockMapping.BlockCoordToZIndex(blockRow, blockCol); if (!BlockMap.TryGetValue(zIndex, out XTableBlock block)) { block = new XTableBlock(BlockSizeX, BlockSizeY, blockRow, blockCol); BlockMap[zIndex] = block; } block.SetCell(localRow, localCol, data); } #endregion #region 合并单元格 /// /// 合并从 (startRow, startCol) 开始、跨越 rowSpan 行和 colSpan 列的矩形区域。 /// 合并源为左上角单元格 (startRow, startCol)。 /// 如果合并区域跨越多个块,则每个涉及的块都会创建对应的 MergeDescriptor。 /// public void MergeCells(int startRow, int startCol, int rowSpan, int colSpan) { // 确保合并源单元格存在 XTableCoordinateUtility.CellToBlockCoordinate( startRow, startCol, BlockSizeX, BlockSizeY, out int srcBlockRow, out int srcBlockCol, out int srcLocalRow, out int srcLocalCol); ulong srcZIndex = XTableBlockMapping.BlockCoordToZIndex(srcBlockRow, srcBlockCol); // 遍历合并区域内的所有单元格,分块处理 for (int r = startRow; r < startRow + rowSpan; r++) { for (int c = startCol; c < startCol + colSpan; c++) { // 跳过合并源自身 if (r == startRow && c == startCol) continue; XTableCoordinateUtility.CellToBlockCoordinate( r, c, BlockSizeX, BlockSizeY, out int blockRow, out int blockCol, out int localRow, out int localCol); ulong zIndex = XTableBlockMapping.BlockCoordToZIndex(blockRow, blockCol); // 确保块存在 if (!BlockMap.TryGetValue(zIndex, out XTableBlock block)) { block = new XTableBlock(BlockSizeX, BlockSizeY, blockRow, blockCol); BlockMap[zIndex] = block; } // 计算本块内合并区域的局部起始和跨度 int localStartRow = Math.Max(0, startRow - blockRow * BlockSizeY); int localStartCol = Math.Max(0, startCol - blockCol * BlockSizeX); int blockEndRow = Math.Min((blockRow + 1) * BlockSizeY, startRow + rowSpan) - blockRow * BlockSizeY; int blockEndCol = Math.Min((blockCol + 1) * BlockSizeX, startCol + colSpan) - blockCol * BlockSizeX; // 查找或创建此块的合并描述 XTableMergeDescriptor descriptor = FindOrCreateMergeDescriptor(block, srcZIndex, srcLocalRow, srcLocalCol, zIndex != srcZIndex, localStartRow, localStartCol, blockEndRow - localStartRow, blockEndCol - localStartCol, rowSpan, colSpan); descriptor.MergedCellSet.Add((localRow, localCol)); } } } /// /// 查找或创建合并描述 /// private XTableMergeDescriptor FindOrCreateMergeDescriptor( XTableBlock block, ulong srcZIndex, int srcLocalRow, int srcLocalCol, bool isCrossBlock, int localStartRow, int localStartCol, int blockRowSpan, int blockColSpan, int totalRowSpan, int totalColSpan) { // 查找已有描述:匹配源坐标和跨块标记 for (int i = 0; i < block.MergeDescriptors.Count; i++) { var desc = block.MergeDescriptors[i]; if (desc.IsCrossBlock == isCrossBlock && desc.SourceBlockIndex == srcZIndex && desc.SourceLocalRow == srcLocalRow && desc.SourceLocalCol == srcLocalCol) return desc; } // 创建新描述,设置完整跨度信息 var newDesc = new XTableMergeDescriptor { IsCrossBlock = isCrossBlock, SourceBlockIndex = srcZIndex, SourceLocalRow = srcLocalRow, SourceLocalCol = srcLocalCol, LocalStartRow = localStartRow, LocalStartCol = localStartCol, MergeRowSpan = totalRowSpan, MergeColSpan = totalColSpan }; block.MergeDescriptors.Add(newDesc); return newDesc; } /// /// 清除所有块的全部合并描述 /// public void ClearAllMerges() { foreach (var kv in BlockMap) { if (kv.Value.MergeDescriptors != null) kv.Value.MergeDescriptors.Clear(); } } /// /// 取消指定坐标所在合并——同时处理"点击源坐标"和"点击被合并坐标"两种情况。 /// 遍历所有块,移除所有指向该源的合并描述。 /// public void UnmergeCells(int startRow, int startCol) { XTableCoordinateUtility.CellToBlockCoordinate( startRow, startCol, BlockSizeX, BlockSizeY, out int blockRow, out int blockCol, out int localRow, out int localCol); ulong zIndex = XTableBlockMapping.BlockCoordToZIndex(blockRow, blockCol); // 1) 从这个单元格所在的块开始:移除包含此坐标的合并描述(被合并格) if (BlockMap.TryGetValue(zIndex, out XTableBlock block)) { for (int i = block.MergeDescriptors.Count - 1; i >= 0; i--) { if (block.MergeDescriptors[i].Contains(localRow, localCol)) block.MergeDescriptors.RemoveAt(i); } } // 2) 遍历所有块:移除源坐标为此单元格的合并描述(合并源 / 锚点) // 处理非跨块和跨块两种情况 foreach (var kv in BlockMap) { var b = kv.Value; if (b.MergeDescriptors.Count == 0) continue; for (int i = b.MergeDescriptors.Count - 1; i >= 0; i--) { var desc = b.MergeDescriptors[i]; // 非跨块:源在同一个块内 → 直接比较本地坐标 if (!desc.IsCrossBlock && desc.SourceLocalRow == localRow && desc.SourceLocalCol == localCol && kv.Key == zIndex) { b.MergeDescriptors.RemoveAt(i); } // 跨块:源在另一个块 → 比较 SourceBlockIndex + 源本地坐标 else if (desc.IsCrossBlock && desc.SourceBlockIndex == zIndex && desc.SourceLocalRow == localRow && desc.SourceLocalCol == localCol) { b.MergeDescriptors.RemoveAt(i); } } } } #endregion #region 查询方法 /// 判断指定单元格是否被合并覆盖(非源,会被重定向) public bool IsCellMerged(int row, int col) { XTableCoordinateUtility.CellToBlockCoordinate( row, col, BlockSizeX, BlockSizeY, out int blockRow, out int blockCol, out int localRow, out int localCol); ulong zIndex = XTableBlockMapping.BlockCoordToZIndex(blockRow, blockCol); if (!BlockMap.TryGetValue(zIndex, out XTableBlock block)) return false; return block.GetMergeRedirect(localRow, localCol) != null; } /// 获取任意合并区域单元格所属的完整合并矩形。 public bool TryGetMergeRect(int row, int col, out int sourceRow, out int sourceCol, out int rowSpan, out int colSpan) { sourceRow = row; sourceCol = col; rowSpan = 1; colSpan = 1; XTableCoordinateUtility.CellToBlockCoordinate(row, col, BlockSizeX, BlockSizeY, out int blockRow, out int blockCol, out int localRow, out int localCol); ulong blockIndex = XTableBlockMapping.BlockCoordToZIndex(blockRow, blockCol); if (!BlockMap.TryGetValue(blockIndex, out XTableBlock block)) return false; XTableMergeDescriptor descriptor = block.GetMergeRedirect(localRow, localCol); if (descriptor == null) { foreach (var pair in BlockMap) { foreach (var candidate in pair.Value.MergeDescriptors) { ulong sourceBlock = candidate.IsCrossBlock ? candidate.SourceBlockIndex : pair.Key; if (sourceBlock == blockIndex && candidate.SourceLocalRow == localRow && candidate.SourceLocalCol == localCol) { descriptor = candidate; break; } } if (descriptor != null) break; } } if (descriptor == null) return false; ulong sourceIndex = descriptor.IsCrossBlock ? descriptor.SourceBlockIndex : blockIndex; XTableBlockMapping.ZIndexToBlockCoord(sourceIndex, out int sourceBlockRow, out int sourceBlockCol); sourceRow = sourceBlockRow * BlockSizeY + descriptor.SourceLocalRow; sourceCol = sourceBlockCol * BlockSizeX + descriptor.SourceLocalCol; rowSpan = descriptor.MergeRowSpan; colSpan = descriptor.MergeColSpan; return true; } /// 判断指定单元格是否为合并源(锚点),并返回合并跨度 public bool IsMergeSource(int row, int col, out int rowSpan, out int colSpan) { rowSpan = 1; colSpan = 1; XTableCoordinateUtility.CellToBlockCoordinate( row, col, BlockSizeX, BlockSizeY, out int blockRow, out int blockCol, out int localRow, out int localCol); ulong thisZIndex = XTableBlockMapping.BlockCoordToZIndex(blockRow, blockCol); foreach (var kv in BlockMap) { foreach (var desc in kv.Value.MergeDescriptors) { ulong srcBlockIdx = desc.IsCrossBlock ? desc.SourceBlockIndex : kv.Key; if (srcBlockIdx == thisZIndex && desc.SourceLocalRow == localRow && desc.SourceLocalCol == localCol) { rowSpan = desc.MergeRowSpan; colSpan = desc.MergeColSpan; return true; } } } return false; } /// 判断指定块是否存在 public bool HasBlock(int blockRow, int blockCol) { ulong zIndex = XTableBlockMapping.BlockCoordToZIndex(blockRow, blockCol); return BlockMap.ContainsKey(zIndex); } /// /// 获取块数量 /// public int BlockCount => BlockMap.Count; #endregion #region 边框范围 public IReadOnlyList BorderRanges => m_BorderRanges; /// 对矩形四条外边写入显式样式范围;空样式恢复隐式默认值。 public void SetBorderRect(int rowStart, int rowEnd, int colStart, int colEnd, string ns) { if (rowStart >= rowEnd || colStart >= colEnd) return; SetBorderRange(XTableBorderAxis.Horizontal, rowStart, colStart, colEnd - colStart, ns); SetBorderRange(XTableBorderAxis.Horizontal, rowEnd, colStart, colEnd - colStart, ns); SetBorderRange(XTableBorderAxis.Vertical, colStart, rowStart, rowEnd - rowStart, ns); SetBorderRange(XTableBorderAxis.Vertical, colEnd, rowStart, rowEnd - rowStart, ns); } public string GetBorderRightNs(int row, int col) => row < 0 || col < 0 ? GetOuterBorderNs(row, col) : GetBorderNamespace(XTableBorderAxis.Vertical, col + 1, row); public string GetBorderBottomNs(int row, int col) => row < 0 || col < 0 ? GetOuterBorderNs(row, col) : GetBorderNamespace(XTableBorderAxis.Horizontal, row + 1, col); public void SetBorderRightNs(int row, int col, string ns) { if (row < 0 || col < 0) SetOuterBorderNs(row, col, ns); else SetBorderRange(XTableBorderAxis.Vertical, col + 1, row, 1, ns); } public void SetBorderBottomNs(int row, int col, string ns) { if (row < 0 || col < 0) SetOuterBorderNs(row, col, ns); else SetBorderRange(XTableBorderAxis.Horizontal, row + 1, col, 1, ns); } public string GetOuterBorderNs(int row, int col) { if (row < 0 && col >= 0) return GetBorderNamespace(XTableBorderAxis.Horizontal, 0, col); if (col < 0 && row >= 0) return GetBorderNamespace(XTableBorderAxis.Vertical, 0, row); return null; } public void SetOuterBorderNs(int row, int col, string ns) { if (row < 0 && col >= 0) SetBorderRange(XTableBorderAxis.Horizontal, 0, col, 1, ns); else if (col < 0 && row >= 0) SetBorderRange(XTableBorderAxis.Vertical, 0, row, 1, ns); } public void SetBorderRange(XTableBorderAxis axis, int fixedIndex, int startIndex, int length, string ns) { if (length <= 0) return; EnsureBorderIndex(); var key = (axis, fixedIndex); if (!m_BorderRangeIndex.TryGetValue(key, out var ranges)) { ranges = new List(); m_BorderRangeIndex[key] = ranges; } int end = startIndex + length; var updated = new List(ranges.Count + 1); for (int i = 0; i < ranges.Count; i++) { var old = ranges[i]; if (old.EndIndex <= startIndex || old.StartIndex >= end) { updated.Add(old); continue; } if (old.StartIndex < startIndex) { old.Length = startIndex - old.StartIndex; updated.Add(old); } if (old.EndIndex > end) { int oldEnd = old.EndIndex; old.StartIndex = end; old.Length = oldEnd - end; updated.Add(old); } } if (!string.IsNullOrEmpty(ns)) { updated.Add(new XTableBorderRange { Axis = axis, FixedIndex = fixedIndex, StartIndex = startIndex, Length = length, StyleIndex = GetOrAddBorderStyle(ns) }); } updated.Sort((a, b) => a.StartIndex.CompareTo(b.StartIndex)); MergeAdjacentRanges(updated); ranges.Clear(); ranges.AddRange(updated); RebuildSerializedBorderRanges(); } public string GetBorderNamespace(XTableBorderAxis axis, int fixedIndex, int index) { EnsureBorderIndex(); if (!m_BorderRangeIndex.TryGetValue((axis, fixedIndex), out var ranges)) return null; for (int i = 0; i < ranges.Count; i++) if (index >= ranges[i].StartIndex && index < ranges[i].EndIndex) return GetBorderStyle(ranges[i].StyleIndex); return null; } public void ClearAllBorderCache() { m_BorderStyles.Clear(); m_BorderRanges.Clear(); m_BorderRangeIndex?.Clear(); } private void MigrateLegacyBorderCache() { if (m_BorderRanges.Count > 0 || BlockMap == null) return; int outerCount = Math.Min(m_OuterBorderRows?.Count ?? 0, Math.Min(m_OuterBorderCols?.Count ?? 0, m_OuterBorderValues?.Count ?? 0)); for (int i = 0; i < outerCount; i++) SetOuterBorderNs(m_OuterBorderRows[i], m_OuterBorderCols[i], m_OuterBorderValues[i]); m_OuterBorderRows?.Clear(); m_OuterBorderCols?.Clear(); m_OuterBorderValues?.Clear(); foreach (var pair in BlockMap) { var block = pair.Value; if (block == null) continue; int count = block.BlockSizeX * block.BlockSizeY; for (int i = 0; i < count; i++) { int row = block.BlockRow * block.BlockSizeY + i / block.BlockSizeX; int col = block.BlockCol * block.BlockSizeX + i % block.BlockSizeX; if (block.BorderRightNs != null && i < block.BorderRightNs.Length && !string.IsNullOrEmpty(block.BorderRightNs[i])) SetBorderRange(XTableBorderAxis.Vertical, col + 1, row, 1, block.BorderRightNs[i]); if (block.BorderBottomNs != null && i < block.BorderBottomNs.Length && !string.IsNullOrEmpty(block.BorderBottomNs[i])) SetBorderRange(XTableBorderAxis.Horizontal, row + 1, col, 1, block.BorderBottomNs[i]); } block.BorderRightNs = null; block.BorderBottomNs = null; } } private int GetOrAddBorderStyle(string ns) { for (int i = 0; i < m_BorderStyles.Count; i++) if (m_BorderStyles[i] == ns) return i; m_BorderStyles.Add(ns); return m_BorderStyles.Count - 1; } private string GetBorderStyle(int styleIndex) { return styleIndex >= 0 && styleIndex < m_BorderStyles.Count ? m_BorderStyles[styleIndex] : null; } private void EnsureBorderIndex() { if (m_BorderRangeIndex != null) return; m_BorderRangeIndex = new Dictionary<(XTableBorderAxis axis, int fixedIndex), List>(); for (int i = 0; i < m_BorderRanges.Count; i++) { var range = m_BorderRanges[i]; var key = (range.Axis, range.FixedIndex); if (!m_BorderRangeIndex.TryGetValue(key, out var ranges)) { ranges = new List(); m_BorderRangeIndex[key] = ranges; } ranges.Add(range); } } private void RebuildSerializedBorderRanges() { EnsureBorderIndex(); m_BorderRanges.Clear(); foreach (var pair in m_BorderRangeIndex) m_BorderRanges.AddRange(pair.Value); } private static void MergeAdjacentRanges(List ranges) { for (int i = ranges.Count - 2; i >= 0; i--) { var current = ranges[i]; var next = ranges[i + 1]; if (current.EndIndex == next.StartIndex && current.StyleIndex == next.StyleIndex) { current.Length += next.Length; ranges[i] = current; ranges.RemoveAt(i + 1); } } } #endregion } }