diff --git a/Runtime/XChart.meta b/Runtime/XChart.meta new file mode 100644 index 0000000..d73ec59 --- /dev/null +++ b/Runtime/XChart.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 44ce2208ca404bb3a4cbe30198a10bfd +timeCreated: 1787107079 \ No newline at end of file diff --git a/Runtime/XChart/Pseudo3D.meta b/Runtime/XChart/Pseudo3D.meta new file mode 100644 index 0000000..f136e1a --- /dev/null +++ b/Runtime/XChart/Pseudo3D.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2575261069f399745ad6b672961c1fcb +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XChart/Pseudo3D/ChartToolCubeBg.cs b/Runtime/XChart/Pseudo3D/ChartToolCubeBg.cs new file mode 100644 index 0000000..a4b3349 --- /dev/null +++ b/Runtime/XChart/Pseudo3D/ChartToolCubeBg.cs @@ -0,0 +1,461 @@ +using System.Collections.Generic; +using UnityEngine; +using XericLibrary.Runtime.UIGraph; + +namespace XericUI.XChart.Pseudo3D +{ + /// + /// 伪 3D 立方体背景工具。 + /// + /// 使用 UILineRendererV2 绘制: + /// 1. 立方体 12 条棱的线框(前向棱使用实色,后向棱可选半透明) + /// 2. 三个可见面上的轴标线网格 + /// + /// 3D 坐标系统: + /// 立方体中心在原点,半边长 1(即 [-1, 1] 范围)。 + /// 通过 pitch/yaw 旋转后进行正交投影(丢弃 Z 轴)得到 2D UI 坐标。 + /// 最终通过 scale + offset 映射到 RectTransform 的像素空间。 + /// + /// 子节点结构: + /// [ChartTool_CubeBg] + /// └─ CubeRenderer (UILineRendererV2) + /// + [AddComponentMenu("Xeric/XChart/Pseudo3D/Cube Background Tool")] + public class ChartToolCubeBg : XChartToolBase + { + // ── 外观配置 ────────────────────────────────────────────── + + /// 立方体半边长(UI 像素) + public float CubeHalfSize { get; set; } = 150f; + + /// 俯仰角(绕 X 轴旋转,度数) + public float Pitch { get; set; } = 25f; + + /// 偏航角(绕 Y 轴旋转,度数) + public float Yaw { get; set; } = -30f; + + /// 前向棱颜色 + public Color FrontEdgeColor { get; set; } = new Color(0.7f, 0.7f, 0.7f, 1f); + + /// 后向棱颜色 + public Color BackEdgeColor { get; set; } = new Color(0.4f, 0.4f, 0.4f, 0.4f); + + /// 棱线宽度 + public float EdgeThickness { get; set; } = 2f; + + /// 网格线颜色 + public Color GridLineColor { get; set; } = new Color(0.5f, 0.5f, 0.5f, 0.3f); + + /// 网格线宽度 + public float GridLineThickness { get; set; } = 1f; + + /// 每轴网格分割数 + public int GridDivisions { get; set; } = 5; + + /// 是否显示后向棱 + public bool ShowBackEdges { get; set; } = true; + + /// 是否显示网格标线 + public bool ShowGridLines { get; set; } = true; + + /// 立方体中心偏移 + public Vector2 CenterOffset { get; set; } = Vector2.zero; + + // ── 内部组件 ────────────────────────────────────────────── + + /// 棱线渲染器 + public UILineRendererV2 EdgeRenderer { get; private set; } + + /// 网格线渲染器 + public UILineRendererV2 GridRenderer { get; private set; } + + // ── 3D 管线状态 ────────────────────────────────────────── + + // 8 个立方体顶点(中心在原点,半边长 1) + private static readonly Vector3[] s_CubeVertices = new Vector3[] + { + new Vector3(-1, -1, -1), // 0: 后下左 + new Vector3( 1, -1, -1), // 1: 后下右 + new Vector3( 1, 1, -1), // 2: 后上右 + new Vector3(-1, 1, -1), // 3: 后上左 + new Vector3(-1, -1, 1), // 4: 前下左 + new Vector3( 1, -1, 1), // 5: 前下右 + new Vector3( 1, 1, 1), // 6: 前上右 + new Vector3(-1, 1, 1), // 7: 前上左 + }; + + // 12 条棱(顶点索引对) + private static readonly int[,] s_CubeEdges = new int[12, 2] + { + {0,1}, {1,2}, {2,3}, {3,0}, // 后面 + {4,5}, {5,6}, {6,7}, {7,4}, // 前面 + {0,4}, {1,5}, {2,6}, {3,7}, // 连接棱 + }; + + // 6 个面的顶点索引 + 法线 + private struct FaceDef { public int[] verts; public Vector3 normal; } + private static readonly FaceDef[] s_Faces = new FaceDef[] + { + new FaceDef { verts = new int[] {4,5,6,7}, normal = new Vector3(0, 0, 1) }, // 前 + new FaceDef { verts = new int[] {0,1,2,3}, normal = new Vector3(0, 0,-1) }, // 后 + new FaceDef { verts = new int[] {1,5,6,2}, normal = new Vector3(1, 0, 0) }, // 右 + new FaceDef { verts = new int[] {0,4,7,3}, normal = new Vector3(-1,0, 0) }, // 左 + new FaceDef { verts = new int[] {2,6,7,3}, normal = new Vector3(0, 1, 0) }, // 上 + new FaceDef { verts = new int[] {0,1,5,4}, normal = new Vector3(0,-1, 0) }, // 下 + }; + + // 每个面由两个轴定义(用于网格线方向) + private static readonly Vector3[] s_FaceAxisA = new Vector3[] + { + new Vector3(1,0,0), new Vector3(1,0,0), // 前/后: X 轴 + new Vector3(0,1,0), new Vector3(0,1,0), // 右/左: Y 轴 + new Vector3(1,0,0), new Vector3(1,0,0), // 上/下: X 轴 + }; + private static readonly Vector3[] s_FaceAxisB = new Vector3[] + { + new Vector3(0,1,0), new Vector3(0,1,0), // 前/后: Y 轴 + new Vector3(0,0,1), new Vector3(0,0,1), // 右/左: Z 轴 + new Vector3(0,0,1), new Vector3(0,0,1), // 上/下: Z 轴 + }; + + // ── 投影状态 ────────────────────────────────────────────── + + private Vector3[] m_RotatedVerts = new Vector3[8]; // 旋转后的 3D 顶点 + private Vector2[] m_ProjectedVerts = new Vector2[8]; // 投影后的 2D 顶点 + private bool m_ProjectionDirty = true; + + /// 投影版本号(每次投影矩阵变化时递增,供外部检测变化) + public int ProjectionVersion { get; private set; } = 0; + + private Matrix4x4 m_RotationMatrix; + private float m_Scale; + private Vector2 m_Offset; + + // ── 初始化 ──────────────────────────────────────────────── + + protected override void OnInitialize() + { + // 创建棱线渲染器 + EdgeRenderer = CreateChildComponent("CubeEdgeRenderer"); + if (EdgeRenderer != null) + { + EdgeRenderer.defaultColor = FrontEdgeColor; + EdgeRenderer.defaultThickness = EdgeThickness; + var rt = EdgeRenderer.rectTransform; + rt.anchorMin = Vector2.zero; + rt.anchorMax = Vector2.one; + rt.offsetMin = Vector2.zero; + rt.offsetMax = Vector2.zero; + } + + // 创建网格线渲染器 + GridRenderer = CreateChildComponent("CubeGridRenderer"); + if (GridRenderer != null) + { + GridRenderer.defaultColor = GridLineColor; + GridRenderer.defaultThickness = GridLineThickness; + var rt = GridRenderer.rectTransform; + rt.anchorMin = Vector2.zero; + rt.anchorMax = Vector2.one; + rt.offsetMin = Vector2.zero; + rt.offsetMax = Vector2.zero; + } + + SetVisible(true); + } + + // ── 更新投影矩阵 ────────────────────────────────────────── + + private void UpdateProjection() + { + if (!m_ProjectionDirty) return; + m_ProjectionDirty = false; + + // 1. 构建旋转矩阵:先绕 X 轴(pitch),再绕 Y 轴(yaw) + float pitchRad = Pitch * Mathf.Deg2Rad; + float yawRad = Yaw * Mathf.Deg2Rad; + m_RotationMatrix = Matrix4x4.Rotate( + Quaternion.Euler(pitchRad * Mathf.Rad2Deg, yawRad * Mathf.Rad2Deg, 0)); + + // 2. 旋转所有顶点 + for (int i = 0; i < 8; i++) + m_RotatedVerts[i] = m_RotationMatrix.MultiplyPoint3x4(s_CubeVertices[i]); + + // 3. 计算投影后的 2D 范围(丢弃 Z) + float minX = float.MaxValue, maxX = float.MinValue; + float minY = float.MaxValue, maxY = float.MinValue; + for (int i = 0; i < 8; i++) + { + float px = m_RotatedVerts[i].x; + float py = m_RotatedVerts[i].y; + if (px < minX) minX = px; + if (px > maxX) maxX = px; + if (py < minY) minY = py; + if (py > maxY) maxY = py; + } + + // 4. 计算缩放和偏移,使立方体适配 CubeHalfSize + float rangeX = maxX - minX; + float rangeY = maxY - minY; + float maxRange = Mathf.Max(rangeX, rangeY); + m_Scale = (maxRange > 0.001f) ? (CubeHalfSize * 2f / maxRange) : 1f; + + // 中心对齐到 CenterOffset + float centerX = (minX + maxX) * 0.5f; + float centerY = (minY + maxY) * 0.5f; + m_Offset = new Vector2( + CenterOffset.x - centerX * m_Scale, + CenterOffset.y - centerY * m_Scale); + + // 5. 计算最终投影坐标 + for (int i = 0; i < 8; i++) + { + m_ProjectedVerts[i] = new Vector2( + m_RotatedVerts[i].x * m_Scale + m_Offset.x, + m_RotatedVerts[i].y * m_Scale + m_Offset.y); + } + } + + /// 标记投影需要重新计算 + public void MarkProjectionDirty() + { + m_ProjectionDirty = true; + ProjectionVersion++; + } + + // ── 3D 投影辅助 ────────────────────────────────────────── + + /// 将 3D 点投影到 2D + public Vector2 ProjectPoint3D(Vector3 point3d) + { + UpdateProjection(); + Vector3 rotated = m_RotationMatrix.MultiplyPoint3x4(point3d); + return new Vector2(rotated.x * m_Scale + m_Offset.x, + rotated.y * m_Scale + m_Offset.y); + } + + /// 批量投影 3D 点到 2D + public Vector2[] ProjectPoints3D(Vector3[] points3d) + { + UpdateProjection(); + var result = new Vector2[points3d.Length]; + for (int i = 0; i < points3d.Length; i++) + { + Vector3 rotated = m_RotationMatrix.MultiplyPoint3x4(points3d[i]); + result[i] = new Vector2(rotated.x * m_Scale + m_Offset.x, + rotated.y * m_Scale + m_Offset.y); + } + return result; + } + + /// 获取当前旋转矩阵 + public Matrix4x4 RotationMatrix => m_RotationMatrix; + + /// 获取当前缩放 + public float Scale => m_Scale; + + /// 获取当前偏移 + public Vector2 Offset => m_Offset; + + // ── 渲染 ────────────────────────────────────────────────── + + public override void OnRender() + { + base.OnRender(); + if (EdgeRenderer == null) return; + + UpdateProjection(); + + // 绘制棱线 + DrawEdges(); + + // 绘制网格标线 + if (GridRenderer != null) + { + if (ShowGridLines) + { + DrawGridLines(); + GridRenderer.RootObject.SetActive(true); + } + else + { + GridRenderer.RootObject.SetActive(false); + } + } + } + + // ── 棱线绘制 ────────────────────────────────────────────── + + private void DrawEdges() + { + EdgeRenderer.ClearAll(); + + // 确定前向棱:棱的两个顶点中,Z 值较大的为前向 + // 更准确:棱所属的面中,法线 Z > 0 的面为可见面 + // 简单方法:棱的两个顶点的 rotated Z 平均值 > 0 为前向 + var frontVerts = new List(); + var backVerts = new List(); + + for (int e = 0; e < 12; e++) + { + int v0 = s_CubeEdges[e, 0]; + int v1 = s_CubeEdges[e, 1]; + float avgZ = (m_RotatedVerts[v0].z + m_RotatedVerts[v1].z) * 0.5f; + bool isFront = avgZ >= 0; + + var list = isFront ? frontVerts : backVerts; + int startIdx = list.Count; + + list.Add(new LineVertexData + { + position = m_ProjectedVerts[v0], + color = isFront ? FrontEdgeColor : BackEdgeColor, + thickness = EdgeThickness, + userData = Vector4.zero, + }); + list.Add(new LineVertexData + { + position = m_ProjectedVerts[v1], + color = isFront ? FrontEdgeColor : BackEdgeColor, + thickness = EdgeThickness, + userData = Vector4.zero, + }); + } + + // 先画后向棱(在底层),再画前向棱(在上层) + if (ShowBackEdges && backVerts.Count > 0) + { + // 每两个顶点构成一条线段 + for (int i = 0; i < backVerts.Count; i += 2) + { + EdgeRenderer.DrawLine( + backVerts[i].position, + backVerts[i + 1].position, + backVerts[i].color, + backVerts[i].thickness); + } + } + + for (int i = 0; i < frontVerts.Count; i += 2) + { + EdgeRenderer.DrawLine( + frontVerts[i].position, + frontVerts[i + 1].position, + frontVerts[i].color, + frontVerts[i].thickness); + } + } + + // ── 网格标线绘制 ────────────────────────────────────────── + + private void DrawGridLines() + { + GridRenderer.ClearAll(); + + // 获取 3 个可见面(法线 Z 值最大的 3 个面) + var visibleFaces = GetVisibleFaces(); + if (visibleFaces.Count == 0) return; + + foreach (int faceIdx in visibleFaces) + { + DrawGridLinesOnFace(faceIdx); + } + } + + /// 获取可见面索引列表(按可见度排序,最多 3 个) + private List GetVisibleFaces() + { + var result = new List(3); + var faceScores = new (int index, float score)[6]; + + for (int i = 0; i < 6; i++) + { + // 将面法线旋转到当前视角 + Vector3 rotatedNormal = m_RotationMatrix.MultiplyVector(s_Faces[i].normal); + // Z 分量越大,越面向用户 + faceScores[i] = (i, rotatedNormal.z); + } + + // 按 Z 值降序排列(Z 越大越面向用户) + System.Array.Sort(faceScores, (a, b) => b.score.CompareTo(a.score)); + + // 取前 3 个且 Z > 0 的面 + for (int i = 0; i < 3 && faceScores[i].score > 0; i++) + result.Add(faceScores[i].index); + + return result; + } + + /// 在指定面上绘制网格线 + private void DrawGridLinesOnFace(int faceIdx) + { + var face = s_Faces[faceIdx]; + Vector3 axisA = s_FaceAxisA[faceIdx]; + Vector3 axisB = s_FaceAxisB[faceIdx]; + + // 面的四个顶点(在立方体局部坐标中) + Vector3 v0 = s_CubeVertices[face.verts[0]]; + Vector3 v1 = s_CubeVertices[face.verts[1]]; + Vector3 v2 = s_CubeVertices[face.verts[2]]; + Vector3 v3 = s_CubeVertices[face.verts[3]]; + + // 面的范围 + float minA = float.MaxValue, maxA = float.MinValue; + float minB = float.MaxValue, maxB = float.MinValue; + + // 计算顶点在轴上的投影 + float GetAxisValue(Vector3 v, Vector3 axis) => Vector3.Dot(v, axis); + + for (int i = 0; i < 4; i++) + { + var v = s_CubeVertices[face.verts[i]]; + float va = GetAxisValue(v, axisA); + float vb = GetAxisValue(v, axisB); + if (va < minA) minA = va; + if (va > maxA) maxA = va; + if (vb < minB) minB = vb; + if (vb > maxB) maxB = vb; + } + + // 沿 A 轴画线(平行于 B 轴) + float stepA = (maxA - minA) / GridDivisions; + float stepB = (maxB - minB) / GridDivisions; + + // 沿 A 轴方向的网格线 + for (int i = 0; i <= GridDivisions; i++) + { + float aVal = minA + i * stepA; + // 线的两个端点 + Vector3 p1 = face.normal + axisA * aVal + axisB * minB; + Vector3 p2 = face.normal + axisA * aVal + axisB * maxB; + + Vector2 pp1 = ProjectPoint3D(p1); + Vector2 pp2 = ProjectPoint3D(p2); + + // 只绘制落在面范围内的线 + GridRenderer.DrawLine(pp1, pp2, GridLineColor, GridLineThickness); + } + + // 沿 B 轴方向的网格线 + for (int i = 0; i <= GridDivisions; i++) + { + float bVal = minB + i * stepB; + Vector3 p1 = face.normal + axisA * minA + axisB * bVal; + Vector3 p2 = face.normal + axisA * maxA + axisB * bVal; + + Vector2 pp1 = ProjectPoint3D(p1); + Vector2 pp2 = ProjectPoint3D(p2); + + GridRenderer.DrawLine(pp1, pp2, GridLineColor, GridLineThickness); + } + } + + // ── 清理 ────────────────────────────────────────────────── + + protected override void OnDispose() + { + EdgeRenderer = null; + GridRenderer = null; + } + } +} \ No newline at end of file diff --git a/Runtime/XChart/Pseudo3D/ChartToolCubeBg.cs.meta b/Runtime/XChart/Pseudo3D/ChartToolCubeBg.cs.meta new file mode 100644 index 0000000..1ac400d --- /dev/null +++ b/Runtime/XChart/Pseudo3D/ChartToolCubeBg.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 032a5991fdcc71d4491a44248dc05d19 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XChart/Pseudo3D/ChartToolScatterPoints.cs b/Runtime/XChart/Pseudo3D/ChartToolScatterPoints.cs new file mode 100644 index 0000000..4ce1251 --- /dev/null +++ b/Runtime/XChart/Pseudo3D/ChartToolScatterPoints.cs @@ -0,0 +1,261 @@ +using UnityEngine; +using XericLibrary.Runtime.UIGraph; +using PrimitiveType = XericLibrary.Runtime.UIGraph.PrimitiveType; + +namespace XericUI.XChart.Pseudo3D +{ + /// + /// 散点渲染工具。 + /// + /// 使用 XScatterPointRenderer 渲染 3D 散点数据在 2D 平面上的投影。 + /// 数据点从 3D 立方体空间投影到 UI 坐标,每个点用一个图元表示。 + /// + /// 子节点结构: + /// [ChartTool_ScatterPoints] + /// └─ PointRenderer (XScatterPointRenderer) + /// + [AddComponentMenu("Xeric/XChart/Pseudo3D/Scatter Points Tool")] + public class ChartToolScatterPoints : XChartToolBase + { + // ── 配置 ────────────────────────────────────────────────── + + /// 点的大小(像素) + public float PointSize + { + get => m_PointSize; + set { m_PointSize = Mathf.Max(0.5f, value); if (IsInitialized) SyncShapeParams(); } + } + private float m_PointSize = 8f; + + /// 图元类型 + public PrimitiveType PointType + { + get => m_PointType; + set { m_PointType = value; if (IsInitialized) SyncShapeParams(); } + } + private PrimitiveType m_PointType = PrimitiveType.Polygon; + + /// 尺寸模式 + public SizeMode SizeMode + { + get => m_SizeMode; + set { m_SizeMode = value; if (IsInitialized) SyncShapeParams(); } + } + private SizeMode m_SizeMode = SizeMode.InscribedCircle; + + /// 填充颜色 + public Color PointColor + { + get => m_PointColor; + set { m_PointColor = value; if (IsInitialized) SyncShapeParams(); } + } + private Color m_PointColor = new Color(0.33f, 0.67f, 1f, 1f); + + /// 中心渐变颜色 + public Color CenterColor + { + get => m_CenterColor; + set { m_CenterColor = value; if (IsInitialized) SyncShapeParams(); } + } + private Color m_CenterColor = Color.white; + + /// 边框颜色 + public Color BorderColor + { + get => m_BorderColor; + set { m_BorderColor = value; if (IsInitialized) SyncShapeParams(); } + } + private Color m_BorderColor = new Color(0.2f, 0.2f, 0.2f, 1f); + + /// 边框宽度 + public float BorderThickness + { + get => m_BorderThickness; + set { m_BorderThickness = Mathf.Max(0f, value); if (IsInitialized) SyncShapeParams(); } + } + private float m_BorderThickness = 1f; + + /// 多边形边数 + public int SideCount + { + get => m_SideCount; + set { m_SideCount = Mathf.Max(3, value); if (IsInitialized) SyncShapeParams(); } + } + private int m_SideCount = 8; + + /// 倒角大小 + public float ChamferSize + { + get => m_ChamferSize; + set { m_ChamferSize = Mathf.Clamp01(value); if (IsInitialized) SyncShapeParams(); } + } + private float m_ChamferSize = 0f; + + /// 图元旋转角度 + public float PointAngle + { + get => m_PointAngle; + set { m_PointAngle = value; if (IsInitialized) SyncShapeParams(); } + } + private float m_PointAngle = 0f; + + /// 是否使用颜色渐变(按数据范围) + public bool EnableColorMapping { get; set; } = false; + + /// 颜色映射渐变起始色 + public Color ColorMapMin { get; set; } = Color.blue; + + /// 颜色映射渐变结束色 + public Color ColorMapMax { get; set; } = Color.red; + + /// 颜色映射轴(0=X, 1=Y, 2=Z) + public int ColorMapAxis { get; set; } = 2; + + // ── 内部组件 ────────────────────────────────────────────── + + /// 散点渲染器 + public XScatterPointRenderer PointRenderer { get; private set; } + + /// 对 CubeBg 工具的引用(用于投影) + public ChartToolCubeBg CubeBg { get; set; } + + // ── 数据缓存 ────────────────────────────────────────────── + + private Vector3[] m_NormalizedPoints = new Vector3[0]; + private Vector2[] m_ProjectedPoints = new Vector2[0]; + private bool m_DataDirty = true; + private int m_LastProjectionVersion = -1; + + // ── 初始化 ──────────────────────────────────────────────── + + protected override void OnInitialize() + { + PointRenderer = CreateChildComponent("PointRenderer"); + if (PointRenderer != null) + { + var rt = PointRenderer.rectTransform; + rt.anchorMin = Vector2.zero; + rt.anchorMax = Vector2.one; + rt.offsetMin = Vector2.zero; + rt.offsetMax = Vector2.zero; + } + + SyncShapeParams(); + SetVisible(true); + } + + // ── 形状参数同步 ────────────────────────────────────────── + + private void SyncShapeParams() + { + if (PointRenderer == null) return; + PointRenderer.pointType = m_PointType; + PointRenderer.sizeMode = m_SizeMode; + PointRenderer.pointSize = m_PointSize; + PointRenderer.fillColor = m_PointColor; + PointRenderer.centerColor = m_CenterColor; + PointRenderer.borderColor = m_BorderColor; + PointRenderer.borderThickness = m_BorderThickness; + PointRenderer.sideCount = m_SideCount; + PointRenderer.chamferSize = m_ChamferSize; + PointRenderer.chamferSegments = 4; + PointRenderer.angle = m_PointAngle; + + // 如果已有数据,触发形状重建 + if (m_NormalizedPoints.Length > 0) + { + PointRenderer.UpdateShapeParams(); + } + } + + // ── 数据更新 ────────────────────────────────────────────── + + /// + /// 设置要渲染的 3D 点(归一化到 [-1, 1] 立方体空间)。 + /// + public void SetNormalizedPoints(Vector3[] normalizedPoints) + { + m_NormalizedPoints = normalizedPoints ?? new Vector3[0]; + m_ProjectedPoints = new Vector2[m_NormalizedPoints.Length]; + m_DataDirty = true; + } + + /// 标记数据需要重新投影 + public void MarkDataDirty() + { + m_DataDirty = true; + } + + // ── 渲染 ────────────────────────────────────────────────── + + public override void OnRender() + { + base.OnRender(); + if (PointRenderer == null || CubeBg == null) return; + if (m_NormalizedPoints.Length == 0) return; + + bool projectionChanged = CubeBg.ProjectionVersion != m_LastProjectionVersion; + + // 如果数据脏了或立方体投影发生了变化,重新投影所有点 + if (m_DataDirty || projectionChanged) + { + m_LastProjectionVersion = CubeBg.ProjectionVersion; + + // 使用 CubeBg 的投影能力将 3D 点投影到 2D + for (int i = 0; i < m_NormalizedPoints.Length; i++) + { + m_ProjectedPoints[i] = CubeBg.ProjectPoint3D(m_NormalizedPoints[i]); + } + + // 颜色映射 + if (EnableColorMapping) + { + ApplyColorMapping(); + } + + // 批量更新到渲染器 + if (EnableColorMapping && m_PointColors != null) + { + PointRenderer.SetPoints(m_ProjectedPoints, m_PointColors); + } + else + { + PointRenderer.SetPoints(m_ProjectedPoints); + } + + m_DataDirty = false; + } + } + + // ── 颜色映射 ────────────────────────────────────────────── + + private Color[] m_PointColors; + + private void ApplyColorMapping() + { + if (m_NormalizedPoints.Length == 0) return; + + if (m_PointColors == null || m_PointColors.Length != m_NormalizedPoints.Length) + m_PointColors = new Color[m_NormalizedPoints.Length]; + + for (int i = 0; i < m_NormalizedPoints.Length; i++) + { + float value = m_NormalizedPoints[i][ColorMapAxis]; + // 从 [-1, 1] 映射到 [0, 1] + float t = Mathf.Clamp01((value + 1f) * 0.5f); + m_PointColors[i] = Color.Lerp(ColorMapMin, ColorMapMax, t); + } + } + + // ── 清理 ────────────────────────────────────────────────── + + protected override void OnDispose() + { + PointRenderer = null; + CubeBg = null; + m_NormalizedPoints = null; + m_ProjectedPoints = null; + m_PointColors = null; + } + } +} \ No newline at end of file diff --git a/Runtime/XChart/Pseudo3D/ChartToolScatterPoints.cs.meta b/Runtime/XChart/Pseudo3D/ChartToolScatterPoints.cs.meta new file mode 100644 index 0000000..09668d5 --- /dev/null +++ b/Runtime/XChart/Pseudo3D/ChartToolScatterPoints.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 63688f4e07bc3a44197a1fe0cec994a9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XChart/Pseudo3D/XPseudo3DScatterChart.cs b/Runtime/XChart/Pseudo3D/XPseudo3DScatterChart.cs new file mode 100644 index 0000000..bd2fad6 --- /dev/null +++ b/Runtime/XChart/Pseudo3D/XPseudo3DScatterChart.cs @@ -0,0 +1,386 @@ +using UnityEngine; +using XericLibrary.Runtime.UIGraph; + +namespace XericUI.XChart.Pseudo3D +{ + /// + /// 伪 3D 散点图主组件。 + /// + /// 在 UI 平面上绘制一个带有透视效果的 3D 散点图: + /// - 立方体线框代表三维空间边界 + /// - 三个可见面上的标线网格帮助定位 + /// - 数据点以图元(多边形/矩形)形式渲染在立方体内部 + /// + /// 使用方式: + /// 1. 在场景中创建空 GameObject,挂载本组件 + /// 2. 设置数据:chart.SetData(xs, ys, zs) + /// 3. 调整外观参数 + /// + /// 子节点结构: + /// [XPseudo3DScatterChart] + /// └─ [XChartTools] + /// ├─ [ChartTool_CubeBg] ← 立方体线框 + 网格 + /// │ ├─ CubeEdgeRenderer (UILineRendererV2) + /// │ └─ CubeGridRenderer (UILineRendererV2) + /// └─ [ChartTool_ScatterPoints] + /// └─ PointRenderer (XScatterPointRenderer) + /// + [AddComponentMenu("Xeric/XChart/Pseudo3D/Pseudo3D Scatter Chart")] + [ExecuteAlways] + public class XPseudo3DScatterChart : XChartBase + { + // ── 串行化字段(Inspector 可编辑) ──────────────────────── + + [Header("立方体外观")] + [SerializeField] private float m_CubeHalfSize = 150f; + [SerializeField] private float m_Pitch = 25f; + [SerializeField] private float m_Yaw = -30f; + [SerializeField] private Color m_FrontEdgeColor = new Color(0.7f, 0.7f, 0.7f, 1f); + [SerializeField] private Color m_BackEdgeColor = new Color(0.4f, 0.4f, 0.4f, 0.3f); + [SerializeField] private float m_EdgeThickness = 2f; + [SerializeField] private Color m_GridLineColor = new Color(0.5f, 0.5f, 0.5f, 0.25f); + [SerializeField] private int m_GridDivisions = 5; + [SerializeField] private bool m_ShowBackEdges = true; + [SerializeField] private bool m_ShowGridLines = true; + + [Header("散点外观")] + [SerializeField] private PrimitiveType m_PointType = PrimitiveType.Polygon; + [SerializeField] private SizeMode m_PointSizeMode = SizeMode.InscribedCircle; + [SerializeField] private float m_PointSize = 8f; + [SerializeField] private Color m_PointColor = new Color(0.33f, 0.67f, 1f, 1f); + [SerializeField] private Color m_PointCenterColor = Color.white; + [SerializeField] private Color m_PointBorderColor = new Color(0.2f, 0.2f, 0.2f, 1f); + [SerializeField] private float m_PointBorderThickness = 1f; + [SerializeField] private int m_PointSideCount = 8; + [SerializeField] private float m_PointChamfer = 0f; + [SerializeField] private float m_PointAngle = 0f; + + [Header("颜色映射")] + [SerializeField] private bool m_EnableColorMapping = false; + [SerializeField] private Color m_ColorMapMin = Color.blue; + [SerializeField] private Color m_ColorMapMax = Color.red; + [SerializeField] private int m_ColorMapAxis = 2; + + [Header("数据")] + [SerializeField] private bool m_AutoUpdate = true; + + // ── 工具引用 ────────────────────────────────────────────── + + private ChartToolCubeBg m_CubeBg; + private ChartToolScatterPoints m_ScatterPoints; + + // ── 数据模型 ────────────────────────────────────────────── + + private XChartDataModel3D m_DataModel = new XChartDataModel3D(); + + /// 数据模型(只读) + public XChartDataModel3D DataModel => m_DataModel; + + // ── 初始化 ──────────────────────────────────────────────── + + protected override void OnEnable() + { + base.OnEnable(); + EnsureToolsCreated(); + } + + private void Start() + { + EnsureToolsCreated(); + SyncAllParams(); + RefreshChart(); + } + + private void EnsureToolsCreated() + { + if (m_CubeBg != null && m_ScatterPoints != null) return; + + // 创建立方体背景工具(OrderIndex 0,先渲染) + if (m_CubeBg == null) + { + m_CubeBg = AddTool("ChartTool_CubeBg", 0); + } + + // 创建散点工具(OrderIndex 10,后渲染,在上层) + if (m_ScatterPoints == null) + { + m_ScatterPoints = AddTool("ChartTool_ScatterPoints", 10); + m_ScatterPoints.CubeBg = m_CubeBg; + } + } + + // ── 参数同步 ────────────────────────────────────────────── + + private void SyncAllParams() + { + SyncCubeParams(); + SyncPointParams(); + } + + private void SyncCubeParams() + { + if (m_CubeBg == null) return; + m_CubeBg.CubeHalfSize = m_CubeHalfSize; + m_CubeBg.Pitch = m_Pitch; + m_CubeBg.Yaw = m_Yaw; + m_CubeBg.FrontEdgeColor = m_FrontEdgeColor; + m_CubeBg.BackEdgeColor = m_BackEdgeColor; + m_CubeBg.EdgeThickness = m_EdgeThickness; + m_CubeBg.GridLineColor = m_GridLineColor; + m_CubeBg.GridDivisions = m_GridDivisions; + m_CubeBg.ShowBackEdges = m_ShowBackEdges; + m_CubeBg.ShowGridLines = m_ShowGridLines; + m_CubeBg.MarkProjectionDirty(); + + // 投影变化时,散点需要重新投影 + if (m_ScatterPoints != null) + m_ScatterPoints.MarkDataDirty(); + } + + private void SyncPointParams() + { + if (m_ScatterPoints == null) return; + m_ScatterPoints.PointType = m_PointType; + m_ScatterPoints.SizeMode = m_PointSizeMode; + m_ScatterPoints.PointSize = m_PointSize; + m_ScatterPoints.PointColor = m_PointColor; + m_ScatterPoints.CenterColor = m_PointCenterColor; + m_ScatterPoints.BorderColor = m_PointBorderColor; + m_ScatterPoints.BorderThickness = m_PointBorderThickness; + m_ScatterPoints.SideCount = m_PointSideCount; + m_ScatterPoints.ChamferSize = m_PointChamfer; + m_ScatterPoints.PointAngle = m_PointAngle; + m_ScatterPoints.EnableColorMapping = m_EnableColorMapping; + m_ScatterPoints.ColorMapMin = m_ColorMapMin; + m_ScatterPoints.ColorMapMax = m_ColorMapMax; + m_ScatterPoints.ColorMapAxis = m_ColorMapAxis; + } + + // ── 每帧更新 ────────────────────────────────────────────── + + protected override void Update() + { + base.Update(); + + if (m_AutoUpdate && m_DataModel.IsDirty(XChartDataModel3D.DirtyFlags.DataChanged)) + { + RefreshChart(); + m_DataModel.ClearDirty(XChartDataModel3D.DirtyFlags.DataChanged); + } + } + + // ── 公共 API ────────────────────────────────────────────── + + #region 公共 API + + /// 刷新图表(重新投影所有数据点) + public void RefreshChart() + { + EnsureToolsCreated(); + + // 标记立方体投影需要重新计算 + if (m_CubeBg != null) + m_CubeBg.MarkProjectionDirty(); + + // 获取归一化数据点并传递给散点工具 + if (m_ScatterPoints != null && m_DataModel.HasData) + { + var normalized = m_DataModel.GetNormalizedPoints(); + m_ScatterPoints.SetNormalizedPoints(normalized); + m_ScatterPoints.MarkDataDirty(); + } + else if (m_ScatterPoints != null) + { + m_ScatterPoints.SetNormalizedPoints(null); + } + } + + /// 设置散点数据 + /// X 坐标数组 + /// Y 坐标数组 + /// Z 坐标数组 + public void SetData(double[] xs, double[] ys, double[] zs) + { + m_DataModel.SetPoints(xs, ys, zs); + RefreshChart(); + } + + /// 设置散点数据(Vector3 数组) + public void SetData(Vector3[] points) + { + var xs = new double[points.Length]; + var ys = new double[points.Length]; + var zs = new double[points.Length]; + for (int i = 0; i < points.Length; i++) + { + xs[i] = points[i].x; + ys[i] = points[i].y; + zs[i] = points[i].z; + } + SetData(xs, ys, zs); + } + + /// 添加一个数据点 + public void AddDataPoint(double x, double y, double z) + { + m_DataModel.AddPoint(x, y, z); + RefreshChart(); + } + + /// 清空所有数据 + public void ClearData() + { + m_DataModel.ClearAll(); + RefreshChart(); + } + + #endregion + + // ── 外观属性(运行时属性) ───────────────────────────────── + + #region 运行时属性 + + public float CubeHalfSize + { + get => m_CubeHalfSize; + set { m_CubeHalfSize = value; SyncCubeParams(); } + } + + public float Pitch + { + get => m_Pitch; + set { m_Pitch = value; SyncCubeParams(); } + } + + public float Yaw + { + get => m_Yaw; + set { m_Yaw = value; SyncCubeParams(); } + } + + public Color FrontEdgeColor + { + get => m_FrontEdgeColor; + set { m_FrontEdgeColor = value; SyncCubeParams(); } + } + + public Color BackEdgeColor + { + get => m_BackEdgeColor; + set { m_BackEdgeColor = value; SyncCubeParams(); } + } + + public float EdgeThickness + { + get => m_EdgeThickness; + set { m_EdgeThickness = value; SyncCubeParams(); } + } + + public Color GridLineColor + { + get => m_GridLineColor; + set { m_GridLineColor = value; SyncCubeParams(); } + } + + public int GridDivisions + { + get => m_GridDivisions; + set { m_GridDivisions = Mathf.Max(1, value); SyncCubeParams(); } + } + + public bool ShowBackEdges + { + get => m_ShowBackEdges; + set { m_ShowBackEdges = value; SyncCubeParams(); } + } + + public bool ShowGridLines + { + get => m_ShowGridLines; + set { m_ShowGridLines = value; SyncCubeParams(); } + } + + public float PointSize + { + get => m_PointSize; + set { m_PointSize = Mathf.Max(0.5f, value); SyncPointParams(); } + } + + public PrimitiveType PointType + { + get => m_PointType; + set { m_PointType = value; SyncPointParams(); } + } + + public Color PointColor + { + get => m_PointColor; + set { m_PointColor = value; SyncPointParams(); } + } + + public int PointSideCount + { + get => m_PointSideCount; + set { m_PointSideCount = Mathf.Max(3, value); SyncPointParams(); } + } + + public float PointChamfer + { + get => m_PointChamfer; + set { m_PointChamfer = Mathf.Clamp01(value); SyncPointParams(); } + } + + public bool EnableColorMapping + { + get => m_EnableColorMapping; + set { m_EnableColorMapping = value; SyncPointParams(); } + } + + public Color ColorMapMin + { + get => m_ColorMapMin; + set { m_ColorMapMin = value; SyncPointParams(); } + } + + public Color ColorMapMax + { + get => m_ColorMapMax; + set { m_ColorMapMax = value; SyncPointParams(); } + } + + public int ColorMapAxis + { + get => m_ColorMapAxis; + set { m_ColorMapAxis = Mathf.Clamp(value, 0, 2); SyncPointParams(); } + } + + #endregion + + // ── 编辑器 ──────────────────────────────────────────────── + +#if UNITY_EDITOR + private void OnValidate() + { + if (m_GridDivisions < 1) m_GridDivisions = 1; + if (m_PointSideCount < 3) m_PointSideCount = 3; + if (m_PointSize < 0.5f) m_PointSize = 0.5f; + if (m_CubeHalfSize < 10f) m_CubeHalfSize = 10f; + if (m_ColorMapAxis < 0) m_ColorMapAxis = 0; + if (m_ColorMapAxis > 2) m_ColorMapAxis = 2; + + // 编辑器下延迟同步,确保 Awake/Start 已执行 + if (isActiveAndEnabled && m_CubeBg != null) + { + UnityEditor.EditorApplication.delayCall += () => + { + if (this != null && isActiveAndEnabled) + { + SyncAllParams(); + RefreshChart(); + } + }; + } + } +#endif + } +} \ No newline at end of file diff --git a/Runtime/XChart/Pseudo3D/XPseudo3DScatterChart.cs.meta b/Runtime/XChart/Pseudo3D/XPseudo3DScatterChart.cs.meta new file mode 100644 index 0000000..61b32b2 --- /dev/null +++ b/Runtime/XChart/Pseudo3D/XPseudo3DScatterChart.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5857501cfde29c94ab1a0aa2896b652b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XChart/Pseudo3D/XScatterPointRenderer.cs b/Runtime/XChart/Pseudo3D/XScatterPointRenderer.cs new file mode 100644 index 0000000..90797ac --- /dev/null +++ b/Runtime/XChart/Pseudo3D/XScatterPointRenderer.cs @@ -0,0 +1,155 @@ +using UnityEngine; +using XericLibrary.Runtime.UIGraph; + +namespace XericUI.XChart.Pseudo3D +{ + /// + /// 散点图元渲染器。 + /// + /// 继承 PrimitiveCacheRendererBase,支持在一个 MaskableGraphic 中批量渲染多个图元。 + /// 所有散点共享同一套形状参数,但位置和颜色可独立设置。 + /// + /// 相比为每个点创建独立的 UIPrimitiveRenderer,本组件大幅减少 GameObject 数量和 Draw Call。 + /// + [AddComponentMenu("Xeric/XChart/Pseudo3D/Scatter Point Renderer")] + public class XScatterPointRenderer : PrimitiveCacheRendererBase + { + // ── 全局形状参数 ────────────────────────────────────────── + + /// 图元类型(多边形 / 矩形) + public PrimitiveType pointType = PrimitiveType.Polygon; + + /// 尺寸模式 + public SizeMode sizeMode = SizeMode.InscribedCircle; + + /// 点大小(像素) + public float pointSize = 8f; + + /// 填充颜色 + public Color fillColor = Color.white; + + /// 中心颜色(渐变中心) + public Color centerColor = Color.white; + + /// 边框颜色 + public Color borderColor = Color.black; + + /// 边框宽度 + public float borderThickness = 1f; + + /// 多边形边数(仅 polygon 类型有效) + [Range(3, 64)] + public int sideCount = 8; + + /// 倒角大小 + [Range(0f, 1f)] + public float chamferSize = 0f; + + /// 倒角分段数 + [Range(1, 16)] + public int chamferSegments = 4; + + /// 旋转角度 + [Range(-180f, 180f)] + public float angle = 0f; + + // ── 公共 API ────────────────────────────────────────────── + + /// + /// 批量设置散点位置。 + /// 清除所有现有图元,用新位置重建。 + /// + /// 2D 位置数组 + /// 可选:每个点的颜色(null 则使用 fillColor) + public void SetPoints(Vector2[] positions, Color[] colors = null) + { + ClearAll(); + + for (int i = 0; i < positions.Length; i++) + { + Color c = (colors != null && i < colors.Length) ? colors[i] : fillColor; + + var entry = new PrimitiveCacheEntry + { + type = pointType, + sizeMode = sizeMode, + center = positions[i], + size = new Vector2(pointSize, pointSize), + angle = angle, + @params = new PrimitiveParams + { + axisScaleX = 1f, + axisScaleY = 1f, + sideCount = sideCount, + chamferSize = chamferSize, + chamferSegments = chamferSegments, + bgColor = c, + centerColor = centerColor, + borderColor = borderColor, + borderThickness = borderThickness, + }, + }; + + AddPrimitive(entry); + } + } + + /// + /// 更新单个点的位置(不重建所有点,仅标记脏)。 + /// + public void UpdatePointPosition(int index, Vector2 position) + { + if (index < 0 || index >= m_Primitives.Count) return; + + var entry = m_Primitives[index]; + entry.center = position; + m_Primitives[index] = entry; + SetPrimitiveDirty(index); + } + + /// + /// 更新单个点的颜色 + /// + public void UpdatePointColor(int index, Color color) + { + if (index < 0 || index >= m_Primitives.Count) return; + + var entry = m_Primitives[index]; + entry.@params.bgColor = color; + m_Primitives[index] = entry; + SetPrimitiveDirty(index); + } + + /// 更新全局形状参数并重建所有点 + public void UpdateShapeParams() + { + for (int i = 0; i < m_Primitives.Count; i++) + { + var entry = m_Primitives[i]; + entry.type = pointType; + entry.sizeMode = sizeMode; + entry.size = new Vector2(pointSize, pointSize); + entry.angle = angle; + entry.@params.sideCount = sideCount; + entry.@params.chamferSize = chamferSize; + entry.@params.chamferSegments = chamferSegments; + entry.@params.borderThickness = borderThickness; + entry.@params.borderColor = borderColor; + entry.@params.centerColor = centerColor; + m_Primitives[i] = entry; + } + RebuildAll(); + } + + // ── 编辑器 ──────────────────────────────────────────────── + +#if UNITY_EDITOR + protected override void OnValidate() + { + base.OnValidate(); + if (sideCount < 3) sideCount = 3; + if (pointSize < 0.5f) pointSize = 0.5f; + } +#endif + } +} \ No newline at end of file diff --git a/Runtime/XChart/Pseudo3D/XScatterPointRenderer.cs.meta b/Runtime/XChart/Pseudo3D/XScatterPointRenderer.cs.meta new file mode 100644 index 0000000..e6da78b --- /dev/null +++ b/Runtime/XChart/Pseudo3D/XScatterPointRenderer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 91affc3968f75e44fab8e970b6f725f5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XChart/Radar.meta b/Runtime/XChart/Radar.meta new file mode 100644 index 0000000..8234620 --- /dev/null +++ b/Runtime/XChart/Radar.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: bc03406131f1b1042875d31cbde6832f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XChart/Radar/ChartToolRadarBg.cs b/Runtime/XChart/Radar/ChartToolRadarBg.cs new file mode 100644 index 0000000..a5cf00a --- /dev/null +++ b/Runtime/XChart/Radar/ChartToolRadarBg.cs @@ -0,0 +1,314 @@ +using UnityEngine; +using XericLibrary.Runtime.UIGraph; +using TMPro; + +namespace XericUI.XChart.Radar +{ + /// + /// 雷达图网格背景工具。 + /// + /// 使用 UILineRendererV2 绘制: + /// 1. 同心多边形网格(外圈 + 内圈百分比环) + /// 2. 从中心到各维度的轴线 + /// 3. 每个维度端点的 TMP 标签 + /// + /// 子节点结构: + /// [ChartTool_RadarBg] + /// ├─ GridRenderer (UILineRendererV2) — 网格线 + /// ├─ AxisRenderer (UILineRendererV2) — 轴线 + /// └─ [Labels] (transform) + /// ├─ Label_0 (TMP_Text) + /// ├─ Label_1 (TMP_Text) + /// └─ ... + /// + [AddComponentMenu("Xeric/XChart/Radar/Radar Background Tool")] + public class ChartToolRadarBg : XChartToolBase + { + // ── 配置 ────────────────────────────────────────────────── + + /// 雷达图半径(像素) + public float Radius { get; set; } = 120f; + + /// 中心位置 + public Vector2 Center { get; set; } = Vector2.zero; + + /// 维度数量 + public int DimensionCount { get; set; } = 5; + + /// 维度名称列表 + public string[] DimensionNames { get; set; } = new string[0]; + + /// 网格环数量(含外圈) + public int RingCount { get; set; } = 4; + + /// 网格颜色 + public Color GridColor { get; set; } = new Color(0.5f, 0.5f, 0.5f, 0.3f); + + /// 轴线颜色 + public Color AxisColor { get; set; } = new Color(0.5f, 0.5f, 0.5f, 0.2f); + + /// 网格线宽 + public float GridThickness { get; set; } = 1f; + + /// 轴线宽 + public float AxisThickness { get; set; } = 1f; + + /// 是否显示网格 + public bool ShowGrid { get; set; } = true; + + /// 是否显示轴线 + public bool ShowAxis { get; set; } = true; + + /// 是否显示标签 + public bool ShowLabels { get; set; } = true; + + /// 标签字体大小 + public float LabelFontSize { get; set; } = 14f; + + /// 标签颜色 + public Color LabelColor { get; set; } = Color.white; + + /// 标签相对端点的偏移量(像素) + public float LabelOffset { get; set; } = 10f; + + // ── 内部组件 ────────────────────────────────────────────── + + /// 网格线渲染器 + public UILineRendererV2 GridRenderer { get; private set; } + + /// 轴线渲染器 + public UILineRendererV2 AxisRenderer { get; private set; } + + /// 标签的父变换 + public Transform LabelsRoot { get; private set; } + + /// 所有标签文本组件 + public TMP_Text[] LabelTexts { get; private set; } + + // ── 内部状态 ────────────────────────────────────────────── + + private bool m_DataDirty = true; + + // ── 初始化 ──────────────────────────────────────────────── + + protected override void OnInitialize() + { + // 创建网格线渲染器 + GridRenderer = CreateChildComponent("GridRenderer"); + if (GridRenderer != null) + { + var rt = GridRenderer.rectTransform; + rt.anchorMin = Vector2.zero; + rt.anchorMax = Vector2.one; + rt.offsetMin = Vector2.zero; + rt.offsetMax = Vector2.zero; + } + + // 创建轴线渲染器 + AxisRenderer = CreateChildComponent("AxisRenderer"); + if (AxisRenderer != null) + { + var rt = AxisRenderer.rectTransform; + rt.anchorMin = Vector2.zero; + rt.anchorMax = Vector2.one; + rt.offsetMin = Vector2.zero; + rt.offsetMax = Vector2.zero; + } + + // 创建标签父节点 + var labelsObj = CreateChild("Labels"); + LabelsRoot = labelsObj; + + SetVisible(true); + } + + // ── 标记脏 ──────────────────────────────────────────────── + + public void MarkDirty() + { + m_DataDirty = true; + } + + // ── 渲染 ────────────────────────────────────────────────── + + public override void OnRender() + { + base.OnRender(); + if (!m_DataDirty) return; + m_DataDirty = false; + + DrawGrid(); + DrawAxis(); + UpdateLabels(); + } + + // ── 网格绘制 ────────────────────────────────────────────── + + private void DrawGrid() + { + if (GridRenderer == null) return; + GridRenderer.ClearAll(); + + if (!ShowGrid || DimensionCount < 3) return; + + // 预计算所有维度的方向 + int dims = DimensionCount; + var dirs = XChartRadarDataModel.GetDimensionDirections(dims); + + // 绘制同心多边形环 + for (int ring = 1; ring <= RingCount; ring++) + { + float r = Radius * ring / RingCount; + + // 构建多边形顶点 + var pts = new Vector2[dims + 1]; + for (int i = 0; i < dims; i++) + pts[i] = Center + dirs[i] * r; + pts[dims] = pts[0]; // 闭合 + + // 绘制每个线段 + for (int i = 0; i < dims; i++) + { + GridRenderer.DrawLine(pts[i], pts[i + 1], + GridColor, GridThickness); + } + } + } + + // ── 轴线绘制 ────────────────────────────────────────────── + + private void DrawAxis() + { + if (AxisRenderer == null) return; + AxisRenderer.ClearAll(); + + if (!ShowAxis || DimensionCount < 3) return; + + int dims = DimensionCount; + var dirs = XChartRadarDataModel.GetDimensionDirections(dims); + + for (int i = 0; i < dims; i++) + { + Vector2 end = Center + dirs[i] * Radius; + AxisRenderer.DrawLine(Center, end, AxisColor, AxisThickness); + } + } + + // ── 标签管理 ────────────────────────────────────────────── + + private void UpdateLabels() + { + if (LabelsRoot == null) return; + + int dims = DimensionCount; + var dirs = XChartRadarDataModel.GetDimensionDirections(dims); + + // 确保标签数量匹配 + EnsureLabelCount(dims); + + bool show = ShowLabels && dims >= 3; + + for (int i = 0; i < dims; i++) + { + var label = LabelTexts[i]; + if (label == null) continue; + + label.gameObject.SetActive(show); + + if (!show) continue; + + // 设置文本 + string name = (i < DimensionNames.Length && DimensionNames[i] != null) + ? DimensionNames[i] + : $"Dim {i}"; + label.text = name; + label.fontSize = LabelFontSize; + label.color = LabelColor; + + // 计算标签位置:端点向外偏移 + Vector2 endPos = Center + dirs[i] * Radius; + Vector2 labelPos = endPos + dirs[i] * LabelOffset; + + // 对齐方式:根据方向决定 + float angle = Mathf.Atan2(dirs[i].y, dirs[i].x) * Mathf.Rad2Deg; + if (angle > -45f && angle <= 45f) + label.alignment = TextAlignmentOptions.Left; + else if (angle > 45f && angle <= 135f) + label.alignment = TextAlignmentOptions.Bottom; + else if (angle > 135f || angle <= -135f) + label.alignment = TextAlignmentOptions.Right; + else + label.alignment = TextAlignmentOptions.Top; + + // 应用位置 + var rt = label.rectTransform; + rt.anchoredPosition = labelPos; + rt.anchorMin = new Vector2(0.5f, 0.5f); + rt.anchorMax = new Vector2(0.5f, 0.5f); + rt.pivot = new Vector2(0.5f, 0.5f); + } + + // 隐藏多余的标签 + for (int i = dims; i < LabelTexts.Length; i++) + { + if (LabelTexts[i] != null) + LabelTexts[i].gameObject.SetActive(false); + } + } + + /// 确保标签数组长度匹配维度数量 + private void EnsureLabelCount(int count) + { + if (LabelTexts != null && LabelTexts.Length == count) + return; + + // 清理旧的标签 + if (LabelTexts != null) + { + foreach (var label in LabelTexts) + { + if (label != null && label.gameObject != null) + { + if (Application.isPlaying) + Object.Destroy(label.gameObject); + else + Object.DestroyImmediate(label.gameObject); + } + } + } + + LabelTexts = new TMP_Text[count]; + + for (int i = 0; i < count; i++) + { + var labelObj = new GameObject($"Label_{i}"); + labelObj.transform.SetParent(LabelsRoot, false); + labelObj.hideFlags = CurrentFlags; + + var tmp = labelObj.AddComponent(); + tmp.text = ""; + tmp.fontSize = LabelFontSize; + tmp.color = LabelColor; + tmp.alignment = TextAlignmentOptions.Center; + + var rt = tmp.rectTransform; + rt.anchorMin = new Vector2(0.5f, 0.5f); + rt.anchorMax = new Vector2(0.5f, 0.5f); + rt.pivot = new Vector2(0.5f, 0.5f); + rt.sizeDelta = new Vector2(100f, 24f); + + LabelTexts[i] = tmp; + } + } + + // ── 清理 ────────────────────────────────────────────────── + + protected override void OnDispose() + { + GridRenderer = null; + AxisRenderer = null; + LabelsRoot = null; + LabelTexts = null; + } + } +} \ No newline at end of file diff --git a/Runtime/XChart/Radar/ChartToolRadarBg.cs.meta b/Runtime/XChart/Radar/ChartToolRadarBg.cs.meta new file mode 100644 index 0000000..bd46c71 --- /dev/null +++ b/Runtime/XChart/Radar/ChartToolRadarBg.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e57afb5b224b01e4b8ca1fea0a49aa3c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XChart/Radar/ChartToolRadarData.cs b/Runtime/XChart/Radar/ChartToolRadarData.cs new file mode 100644 index 0000000..b74c5f6 --- /dev/null +++ b/Runtime/XChart/Radar/ChartToolRadarData.cs @@ -0,0 +1,136 @@ +using UnityEngine; +using XericLibrary.Runtime.UIGraph; + +namespace XericUI.XChart.Radar +{ + /// + /// 雷达图数据线框渲染工具。 + /// + /// 使用 UILineRendererV2 绘制每个数据系列的多边形轮廓。 + /// 仅绘制线框,无填充效果。 + /// 每个系列一个闭合多边形,颜色和线宽独立配置。 + /// + /// 子节点结构: + /// [ChartTool_RadarData] + /// └─ DataRenderer (UILineRendererV2) — 所有数据系列的线框 + /// + [AddComponentMenu("Xeric/XChart/Radar/Radar Data Tool")] + public class ChartToolRadarData : XChartToolBase + { + // ── 配置 ────────────────────────────────────────────────── + + /// 雷达图半径(像素) + public float Radius { get; set; } = 120f; + + /// 中心位置 + public Vector2 Center { get; set; } = Vector2.zero; + + /// 维度数量 + public int DimensionCount { get; set; } = 5; + + // ── 内部组件 ────────────────────────────────────────────── + + /// 数据线框渲染器 + public UILineRendererV2 DataRenderer { get; private set; } + + // ── 数据缓存 ────────────────────────────────────────────── + + private XChartRadarDataModel.RadarSeries[] m_Series = new XChartRadarDataModel.RadarSeries[0]; + private float m_MaxValue = 1f; + private bool m_DataDirty = true; + + // ── 初始化 ──────────────────────────────────────────────── + + protected override void OnInitialize() + { + DataRenderer = CreateChildComponent("DataRenderer"); + if (DataRenderer != null) + { + var rt = DataRenderer.rectTransform; + rt.anchorMin = Vector2.zero; + rt.anchorMax = Vector2.one; + rt.offsetMin = Vector2.zero; + rt.offsetMax = Vector2.zero; + } + + SetVisible(true); + } + + // ── 数据更新 ────────────────────────────────────────────── + + /// + /// 设置数据系列。 + /// + /// 数据系列数组 + /// 当前最大值(用于归一化) + public void SetSeries(XChartRadarDataModel.RadarSeries[] series, float maxValue) + { + m_Series = series ?? new XChartRadarDataModel.RadarSeries[0]; + m_MaxValue = maxValue > 0.001f ? maxValue : 1f; + m_DataDirty = true; + } + + /// 标记数据为脏(下次渲染时刷新) + public void MarkDirty() + { + m_DataDirty = true; + } + + // ── 渲染 ────────────────────────────────────────────────── + + public override void OnRender() + { + base.OnRender(); + if (!m_DataDirty) return; + m_DataDirty = false; + + if (DataRenderer == null) return; + DataRenderer.ClearAll(); + + if (DimensionCount < 3 || m_Series.Length == 0) return; + + // 预计算方向 + var dirs = XChartRadarDataModel.GetDimensionDirections(DimensionCount); + + // 绘制每个数据系列的多边形轮廓 + for (int si = 0; si < m_Series.Length; si++) + { + var series = m_Series[si]; + if (series.values == null || series.values.Length == 0) continue; + + DrawSeriesPolygon(series, dirs); + } + } + + private void DrawSeriesPolygon(XChartRadarDataModel.RadarSeries series, Vector2[] dirs) + { + int dims = DimensionCount; + float invMax = 1f / m_MaxValue; + + // 计算多边形顶点 + var pts = new Vector2[dims + 1]; + for (int i = 0; i < dims; i++) + { + float val = (i < series.values.Length) ? series.values[i] : 0f; + float r = Mathf.Clamp01(val * invMax) * Radius; + pts[i] = Center + dirs[i] * r; + } + pts[dims] = pts[0]; // 闭合 + + // 绘制闭合多边形线框 + for (int i = 0; i < dims; i++) + { + DataRenderer.DrawLine(pts[i], pts[i + 1], + series.color, series.lineWidth); + } + } + + // ── 清理 ────────────────────────────────────────────────── + + protected override void OnDispose() + { + DataRenderer = null; + m_Series = null; + } + } +} \ No newline at end of file diff --git a/Runtime/XChart/Radar/ChartToolRadarData.cs.meta b/Runtime/XChart/Radar/ChartToolRadarData.cs.meta new file mode 100644 index 0000000..4f312cc --- /dev/null +++ b/Runtime/XChart/Radar/ChartToolRadarData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0f229dbe8b6a83043b6a6d2f4605a47c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XChart/Radar/XChartRadarDataModel.cs b/Runtime/XChart/Radar/XChartRadarDataModel.cs new file mode 100644 index 0000000..d5e20fa --- /dev/null +++ b/Runtime/XChart/Radar/XChartRadarDataModel.cs @@ -0,0 +1,263 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace XericUI.XChart.Radar +{ + /// + /// 雷达图数据模型。 + /// + /// 管理多个数据系列(RadarSeries),每个系列对应一个雷达多边形。 + /// 所有系列共享同一组维度名称(Dimensions)。 + /// + /// 使用示例: + /// var model = new XChartRadarDataModel(); + /// model.SetDimensions("速度", "力量", "防御", "敏捷", "魔力"); + /// model.AddSeries("战士", new float[] { 0.8f, 0.9f, 0.7f, 0.4f, 0.2f }); + /// model.AddSeries("法师", new float[] { 0.2f, 0.3f, 0.4f, 0.6f, 0.9f }); + /// + public class XChartRadarDataModel + { + // ── 数据系列 ────────────────────────────────────────────── + + /// + /// 单个数据系列。 + /// + [Serializable] + public struct RadarSeries + { + /// 系列名称(用于图例) + public string name; + + /// 各维度的值(长度需与 Dimensions 一致) + public float[] values; + + /// 系列颜色 + public Color color; + + /// 线宽 + public float lineWidth; + + public RadarSeries(string name, float[] values, Color color, float lineWidth = 2f) + { + this.name = name; + this.values = values; + this.color = color; + this.lineWidth = lineWidth; + } + } + + // ── 数据存储 ────────────────────────────────────────────── + + /// 维度名称列表 + public string[] Dimensions { get; private set; } + + /// 维度数量 + public int DimensionCount => Dimensions?.Length ?? 0; + + /// 数据系列列表 + public IReadOnlyList Series => m_Series; + + /// 系列数量 + public int SeriesCount => m_Series.Count; + + /// 是否有数据系列 + public bool HasSeries => m_Series.Count > 0; + + private List m_Series = new List(); + + // ── 全局范围 ────────────────────────────────────────────── + + /// 所有系列中的全局最大值(用于归一化) + public float GlobalMaxValue { get; private set; } = 1f; + + /// 是否自动缩放(基于 GlobalMaxValue) + public bool AutoScale { get; set; } = true; + + /// 自定义最大值(AutoScale=false 时使用) + public float CustomMaxValue { get; set; } = 1f; + + /// 当前使用的最大值 + public float MaxValue => AutoScale ? GlobalMaxValue : CustomMaxValue; + + // ── 脏标记 ──────────────────────────────────────────────── + + [Flags] + public enum DirtyFlags + { + None = 0, + DataChanged = 1 << 0, + DimensionsChanged = 1 << 1, + All = DataChanged | DimensionsChanged, + } + + public DirtyFlags CurrentDirty { get; private set; } + + public void MarkDirty(DirtyFlags flag) { CurrentDirty |= flag; } + public void ClearDirty(DirtyFlags flag) { CurrentDirty &= ~flag; } + public bool IsDirty(DirtyFlags flag) => (CurrentDirty & flag) != 0; + + // ── 维度操作 ────────────────────────────────────────────── + + /// 设置维度名称 + public void SetDimensions(params string[] dimensionNames) + { + Dimensions = dimensionNames ?? new string[0]; + MarkDirty(DirtyFlags.DimensionsChanged); + } + + /// 设置维度名称(从列表) + public void SetDimensions(IList dimensionNames) + { + if (dimensionNames == null) + { + Dimensions = new string[0]; + } + else + { + Dimensions = new string[dimensionNames.Count]; + for (int i = 0; i < dimensionNames.Count; i++) + Dimensions[i] = dimensionNames[i]; + } + MarkDirty(DirtyFlags.DimensionsChanged); + } + + // ── 系列操作 ────────────────────────────────────────────── + + /// 添加一个数据系列 + public void AddSeries(string name, float[] values, Color? color = null, float lineWidth = 2f) + { + if (DimensionCount == 0) return; + + // 截断或补齐 + int count = Math.Min(values.Length, DimensionCount); + var trimmed = new float[DimensionCount]; + Array.Copy(values, trimmed, count); + + m_Series.Add(new RadarSeries(name, trimmed, color ?? Color.white, lineWidth)); + RecalculateRange(); + MarkDirty(DirtyFlags.DataChanged); + } + + /// 批量设置数据系列 + public void SetSeries(IList series) + { + m_Series.Clear(); + if (series != null) + { + foreach (var s in series) + m_Series.Add(s); + } + RecalculateRange(); + MarkDirty(DirtyFlags.All); + } + + /// 移除指定系列 + public void RemoveSeries(int index) + { + if (index < 0 || index >= m_Series.Count) return; + m_Series.RemoveAt(index); + RecalculateRange(); + MarkDirty(DirtyFlags.DataChanged); + } + + /// 移除所有系列 + public void ClearAll() + { + m_Series.Clear(); + Dimensions = new string[0]; + GlobalMaxValue = 1f; + MarkDirty(DirtyFlags.All); + } + + /// 获取指定系列 + public RadarSeries GetSeries(int index) => m_Series[index]; + + /// 更新指定系列的值 + public void UpdateSeries(int index, float[] values) + { + if (index < 0 || index >= m_Series.Count) return; + var s = m_Series[index]; + int count = Math.Min(values.Length, DimensionCount); + s.values = new float[DimensionCount]; + Array.Copy(values, s.values, count); + m_Series[index] = s; + RecalculateRange(); + MarkDirty(DirtyFlags.DataChanged); + } + + // ── 范围计算 ────────────────────────────────────────────── + + private void RecalculateRange() + { + float maxVal = 0f; + for (int si = 0; si < m_Series.Count; si++) + { + var vals = m_Series[si].values; + if (vals == null) continue; + for (int vi = 0; vi < vals.Length; vi++) + { + if (vals[vi] > maxVal) maxVal = vals[vi]; + } + } + GlobalMaxValue = maxVal > 0.001f ? maxVal : 1f; + } + + // ── 坐标计算 ────────────────────────────────────────────── + + /// + /// 计算指定维度在雷达图上的方向向量(从中心指向端点,单位向量)。 + /// 角度从正上方(12点钟方向)开始,顺时针排列。 + /// + public static Vector2 GetDimensionDirection(int index, int total) + { + if (total <= 0) return Vector2.up; + float angle = 90f - index * (360f / total); + float rad = angle * Mathf.Deg2Rad; + return new Vector2(Mathf.Cos(rad), Mathf.Sin(rad)); + } + + /// + /// 计算所有维度的方向向量。 + /// + public static Vector2[] GetDimensionDirections(int total) + { + var dirs = new Vector2[total]; + for (int i = 0; i < total; i++) + dirs[i] = GetDimensionDirection(i, total); + return dirs; + } + + /// + /// 计算雷达图最大半径时的端点位置。 + /// + public static Vector2[] GetOuterPoints(Vector2 center, float radius, int dimensionCount) + { + var pts = new Vector2[dimensionCount]; + for (int i = 0; i < dimensionCount; i++) + { + Vector2 dir = GetDimensionDirection(i, dimensionCount); + pts[i] = center + dir * radius; + } + return pts; + } + + /// + /// 计算某个系列的数据点位置(归一化后的值 × 半径)。 + /// + public static Vector2[] GetSeriesPoints(Vector2 center, float radius, + float[] values, float maxValue, int dimensionCount) + { + var pts = new Vector2[dimensionCount]; + float invMax = maxValue > 0.001f ? 1f / maxValue : 1f; + for (int i = 0; i < dimensionCount; i++) + { + Vector2 dir = GetDimensionDirection(i, dimensionCount); + float val = (i < values.Length) ? values[i] : 0f; + float r = Mathf.Clamp01(val * invMax) * radius; + pts[i] = center + dir * r; + } + return pts; + } + } +} \ No newline at end of file diff --git a/Runtime/XChart/Radar/XChartRadarDataModel.cs.meta b/Runtime/XChart/Radar/XChartRadarDataModel.cs.meta new file mode 100644 index 0000000..870eb9b --- /dev/null +++ b/Runtime/XChart/Radar/XChartRadarDataModel.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fb161051ad1848e47ab218085166ea3f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XChart/Radar/XRadarChart.cs b/Runtime/XChart/Radar/XRadarChart.cs new file mode 100644 index 0000000..e878368 --- /dev/null +++ b/Runtime/XChart/Radar/XRadarChart.cs @@ -0,0 +1,363 @@ +using UnityEngine; + +namespace XericUI.XChart.Radar +{ + /// + /// 雷达图主组件。 + /// + /// 在 UI 平面上绘制一个或多个雷达图数据系列。 + /// 每个系列对应一个闭合多边形线框,共享同一组维度轴。 + /// 每个维度端点显示 TMP 标签。 + /// + /// 使用方式: + /// 1. 在场景中创建空 GameObject,挂载本组件 + /// 2. 设置维度:chart.SetDimensions("速度", "力量", "防御", "敏捷", "魔力") + /// 3. 添加数据:chart.AddSeries("战士", new float[] { 0.8f, 0.9f, 0.7f, 0.4f, 0.2f }) + /// + /// 子节点结构: + /// [XRadarChart] + /// └─ [XChartTools] + /// ├─ [ChartTool_RadarBg] (OrderIndex=0) ← 网格 + 轴线 + 标签 + /// │ ├─ GridRenderer (UILineRendererV2) + /// │ ├─ AxisRenderer (UILineRendererV2) + /// │ └─ [Labels] + /// │ ├─ Label_0 (TMP_Text) + /// │ ├─ Label_1 (TMP_Text) + /// │ └─ ... + /// └─ [ChartTool_RadarData] (OrderIndex=10) ← 数据线框 + /// └─ DataRenderer (UILineRendererV2) + /// + [AddComponentMenu("Xeric/XChart/Radar/Radar Chart")] + [ExecuteAlways] + public class XRadarChart : XChartBase + { + // ── 串行化字段 ──────────────────────────────────────────── + + [Header("雷达图外观")] + [SerializeField] private float m_Radius = 120f; + [SerializeField] private Vector2 m_CenterOffset = Vector2.zero; + + [Header("网格")] + [SerializeField] private bool m_ShowGrid = true; + [SerializeField] private int m_RingCount = 4; + [SerializeField] private Color m_GridColor = new Color(0.5f, 0.5f, 0.5f, 0.3f); + [SerializeField] private float m_GridThickness = 1f; + + [Header("轴线")] + [SerializeField] private bool m_ShowAxis = true; + [SerializeField] private Color m_AxisColor = new Color(0.5f, 0.5f, 0.5f, 0.2f); + [SerializeField] private float m_AxisThickness = 1f; + + [Header("标签")] + [SerializeField] private bool m_ShowLabels = true; + [SerializeField] private float m_LabelFontSize = 14f; + [SerializeField] private Color m_LabelColor = Color.white; + [SerializeField] private float m_LabelOffset = 10f; + + [Header("默认系列外观")] + [SerializeField] private Color m_DefaultSeriesColor = new Color(0.33f, 0.67f, 1f, 1f); + [SerializeField] private float m_DefaultLineWidth = 2f; + + [Header("自动更新")] + [SerializeField] private bool m_AutoUpdate = true; + + // ── 工具引用 ────────────────────────────────────────────── + + private ChartToolRadarBg m_RadarBg; + private ChartToolRadarData m_RadarData; + + // ── 数据模型 ────────────────────────────────────────────── + + private XChartRadarDataModel m_DataModel = new XChartRadarDataModel(); + + /// 数据模型(只读) + public XChartRadarDataModel DataModel => m_DataModel; + + // ── 预置颜色轮(用于自动分配颜色) ──────────────────────── + + private static readonly Color[] s_ColorWheel = new Color[] + { + new Color(0.33f, 0.67f, 1f, 1f), // 蓝 + new Color(1f, 0.33f, 0.33f, 1f), // 红 + new Color(0.33f, 1f, 0.33f, 1f), // 绿 + new Color(1f, 0.67f, 0f, 1f), // 橙 + new Color(0.67f, 0.33f, 1f, 1f), // 紫 + new Color(1f, 0.33f, 0.67f, 1f), // 粉 + new Color(0f, 0.67f, 1f, 1f), // 天蓝 + new Color(0.5f, 1f, 0.5f, 1f), // 亮绿 + }; + + // ── 初始化 ──────────────────────────────────────────────── + + protected override void OnEnable() + { + base.OnEnable(); + EnsureToolsCreated(); + } + + private void Start() + { + EnsureToolsCreated(); + SyncAllParams(); + RefreshChart(); + } + + private void EnsureToolsCreated() + { + if (m_RadarBg != null && m_RadarData != null) return; + + if (m_RadarBg == null) + m_RadarBg = AddTool("ChartTool_RadarBg", 0); + + if (m_RadarData == null) + m_RadarData = AddTool("ChartTool_RadarData", 10); + } + + // ── 参数同步 ────────────────────────────────────────────── + + private void SyncAllParams() + { + SyncBgParams(); + SyncDataParams(); + } + + private void SyncBgParams() + { + if (m_RadarBg == null) return; + m_RadarBg.Radius = m_Radius; + m_RadarBg.Center = m_CenterOffset; + m_RadarBg.DimensionCount = m_DataModel.DimensionCount; + m_RadarBg.DimensionNames = m_DataModel.Dimensions; + m_RadarBg.RingCount = m_RingCount; + m_RadarBg.GridColor = m_GridColor; + m_RadarBg.GridThickness = m_GridThickness; + m_RadarBg.AxisColor = m_AxisColor; + m_RadarBg.AxisThickness = m_AxisThickness; + m_RadarBg.ShowGrid = m_ShowGrid; + m_RadarBg.ShowAxis = m_ShowAxis; + m_RadarBg.ShowLabels = m_ShowLabels; + m_RadarBg.LabelFontSize = m_LabelFontSize; + m_RadarBg.LabelColor = m_LabelColor; + m_RadarBg.LabelOffset = m_LabelOffset; + m_RadarBg.MarkDirty(); + } + + private void SyncDataParams() + { + if (m_RadarData == null) return; + m_RadarData.Radius = m_Radius; + m_RadarData.Center = m_CenterOffset; + m_RadarData.DimensionCount = m_DataModel.DimensionCount; + m_RadarData.MarkDirty(); + } + + // ── 每帧更新 ────────────────────────────────────────────── + + protected override void Update() + { + base.Update(); + + if (m_AutoUpdate) + { + if (m_DataModel.IsDirty(XChartRadarDataModel.DirtyFlags.DimensionsChanged)) + { + // 维度变化:需要同步背景和数据 + SyncBgParams(); + SyncDataParams(); + PushDataToTool(); + m_DataModel.ClearDirty(XChartRadarDataModel.DirtyFlags.DimensionsChanged); + } + else if (m_DataModel.IsDirty(XChartRadarDataModel.DirtyFlags.DataChanged)) + { + // 数据变化:只需要更新数据 + SyncDataParams(); + PushDataToTool(); + m_DataModel.ClearDirty(XChartRadarDataModel.DirtyFlags.DataChanged); + } + } + } + + // ── 数据推送 ────────────────────────────────────────────── + + private void PushDataToTool() + { + if (m_RadarData == null) return; + + // 将数据模型的系列列表转为数组 + int count = m_DataModel.SeriesCount; + var seriesArray = new XChartRadarDataModel.RadarSeries[count]; + for (int i = 0; i < count; i++) + seriesArray[i] = m_DataModel.GetSeries(i); + + m_RadarData.SetSeries(seriesArray, m_DataModel.MaxValue); + } + + // ── 公共 API ────────────────────────────────────────────── + + #region 公共 API + + /// 刷新图表 + public void RefreshChart() + { + EnsureToolsCreated(); + SyncAllParams(); + PushDataToTool(); + } + + /// 设置维度名称 + public void SetDimensions(params string[] dimensionNames) + { + m_DataModel.SetDimensions(dimensionNames); + // 由 Update 自动同步 + } + + /// 设置维度名称(从列表) + public void SetDimensions(System.Collections.Generic.IList dimensionNames) + { + m_DataModel.SetDimensions(dimensionNames); + } + + /// 添加一个数据系列(自动分配颜色) + public void AddSeries(string name, float[] values) + { + Color color = s_ColorWheel[m_DataModel.SeriesCount % s_ColorWheel.Length]; + m_DataModel.AddSeries(name, values, color, m_DefaultLineWidth); + } + + /// 添加一个数据系列(指定颜色) + public void AddSeries(string name, float[] values, Color color, float lineWidth = 2f) + { + m_DataModel.AddSeries(name, values, color, lineWidth); + } + + /// 批量设置数据系列 + public void SetSeries(System.Collections.Generic.IList series) + { + m_DataModel.SetSeries(series); + } + + /// 移除指定系列 + public void RemoveSeries(int index) + { + m_DataModel.RemoveSeries(index); + } + + /// 清空所有数据 + public void ClearData() + { + m_DataModel.ClearAll(); + RefreshChart(); + } + + /// 是否自动缩放 + public bool AutoScale + { + get => m_DataModel.AutoScale; + set { m_DataModel.AutoScale = value; RefreshChart(); } + } + + /// 自定义最大值(AutoScale=false 时使用) + public float CustomMaxValue + { + get => m_DataModel.CustomMaxValue; + set { m_DataModel.CustomMaxValue = Mathf.Max(0.01f, value); RefreshChart(); } + } + + #endregion + + // ── 运行时属性 ──────────────────────────────────────────── + + #region 运行时属性 + + public float Radius + { + get => m_Radius; + set { m_Radius = Mathf.Max(10f, value); SyncBgParams(); SyncDataParams(); } + } + + public Vector2 CenterOffset + { + get => m_CenterOffset; + set { m_CenterOffset = value; SyncBgParams(); SyncDataParams(); } + } + + public bool ShowGrid + { + get => m_ShowGrid; + set { m_ShowGrid = value; SyncBgParams(); } + } + + public int RingCount + { + get => m_RingCount; + set { m_RingCount = Mathf.Max(1, value); SyncBgParams(); } + } + + public Color GridColor + { + get => m_GridColor; + set { m_GridColor = value; SyncBgParams(); } + } + + public bool ShowAxis + { + get => m_ShowAxis; + set { m_ShowAxis = value; SyncBgParams(); } + } + + public Color AxisColor + { + get => m_AxisColor; + set { m_AxisColor = value; SyncBgParams(); } + } + + public bool ShowLabels + { + get => m_ShowLabels; + set { m_ShowLabels = value; SyncBgParams(); } + } + + public float LabelFontSize + { + get => m_LabelFontSize; + set { m_LabelFontSize = Mathf.Max(4f, value); SyncBgParams(); } + } + + public Color LabelColor + { + get => m_LabelColor; + set { m_LabelColor = value; SyncBgParams(); } + } + + public float LabelOffset + { + get => m_LabelOffset; + set { m_LabelOffset = value; SyncBgParams(); } + } + + #endregion + + // ── 编辑器 ──────────────────────────────────────────────── + +#if UNITY_EDITOR + private void OnValidate() + { + if (m_Radius < 10f) m_Radius = 10f; + if (m_RingCount < 1) m_RingCount = 1; + if (m_LabelFontSize < 4f) m_LabelFontSize = 4f; + + if (isActiveAndEnabled && m_RadarBg != null) + { + UnityEditor.EditorApplication.delayCall += () => + { + if (this != null && isActiveAndEnabled) + { + SyncAllParams(); + PushDataToTool(); + } + }; + } + } +#endif + } +} \ No newline at end of file diff --git a/Runtime/XChart/Radar/XRadarChart.cs.meta b/Runtime/XChart/Radar/XRadarChart.cs.meta new file mode 100644 index 0000000..ddaa65e --- /dev/null +++ b/Runtime/XChart/Radar/XRadarChart.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2b0e82a8b831b2f41a048413951f28bc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XChart/XChartBase.cs b/Runtime/XChart/XChartBase.cs new file mode 100644 index 0000000..83f4cd8 --- /dev/null +++ b/Runtime/XChart/XChartBase.cs @@ -0,0 +1,118 @@ +using UnityEngine; + +namespace XericUI.XChart +{ + /// + /// 图表主行为组件基类。 + /// + /// 标记 [ExecuteAlways] 确保编辑器下生命周期正常执行。 + /// 派生类在 Start() 中创建工具并配置数据模型。 + /// + [AddComponentMenu("Xeric/XChart/Chart Base")] + [ExecuteAlways] + public abstract class XChartBase : MonoBehaviour + { + [Header("工具管理")] + [SerializeField] private bool m_ShowDebug; + [SerializeField] private bool m_HideAllTools; + + public XChartToolsManager ToolManager { get; private set; } + public Transform ToolsRoot { get; private set; } + + private bool m_IsInitialized; + + // ── hideFlags ───────────────────────────────────────────── + + private const HideFlags k_EditorFlags = HideFlags.DontSaveInEditor; + private const HideFlags k_PlayFlags = HideFlags.HideAndDontSave; + + internal static HideFlags CurrentFlags => + Application.isPlaying ? k_PlayFlags : k_EditorFlags; + + // ── 生命周期 ────────────────────────────────────────────── + + private void Awake() + { + Initialize(); + } + + private void OnEnable() + { + if (!m_IsInitialized) Initialize(); + } + + private void Initialize() + { + if (m_IsInitialized) return; + if (ToolManager != null) return; + + var flags = CurrentFlags; + + var toolsObj = new GameObject("[XChartTools]"); + toolsObj.transform.SetParent(transform, false); + toolsObj.hideFlags = flags; + ToolsRoot = toolsObj.transform; + + ToolManager = new XChartToolsManager + { + ToolsRoot = ToolsRoot, + }; + + m_IsInitialized = true; + } + + protected virtual void Update() + { + if (!m_IsInitialized || ToolManager == null) return; + ToolManager.UpdateAll(); + } + + protected virtual void LateUpdate() + { + if (!m_IsInitialized || ToolManager == null) return; + ToolManager.RenderAll(); + } + + // ── 工具管理 ────────────────────────────────────────────── + + public T AddTool(string toolName, int orderIndex = 0) + where T : XChartToolBase, new() + { + if (ToolManager == null) Initialize(); + return ToolManager.AddTool(toolName, orderIndex); + } + + public bool RemoveTool(string toolName) + { + if (ToolManager == null) return false; + return ToolManager.RemoveToolByName(toolName); + } + + public void RemoveAllTools() => ToolManager?.RemoveAllTools(); + + // ── 清理 ────────────────────────────────────────────────── + + protected virtual void OnDestroy() + { + if (ToolManager != null) + { + ToolManager.Dispose(); + ToolManager = null; + } + if (ToolsRoot != null) + { + if (Application.isPlaying) + Destroy(ToolsRoot.gameObject); + else + DestroyImmediate(ToolsRoot.gameObject); + ToolsRoot = null; + } + m_IsInitialized = false; + } + + private void OnValidate() + { + // 由派生类按需扩展 + } + } +} \ No newline at end of file diff --git a/Runtime/XChart/XChartBase.cs.meta b/Runtime/XChart/XChartBase.cs.meta new file mode 100644 index 0000000..085f61a --- /dev/null +++ b/Runtime/XChart/XChartBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8b059e556e780e14586c63a61c5290d7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XChart/XChartDataModel.cs b/Runtime/XChart/XChartDataModel.cs new file mode 100644 index 0000000..3a39611 --- /dev/null +++ b/Runtime/XChart/XChartDataModel.cs @@ -0,0 +1,232 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace XericUI.XChart +{ + /// + /// 3D 散点图数据模型。 + /// + /// 存储 XYZ 三维坐标点数据,支持脏标记机制。 + /// 每个数据点包含 (X, Y, Z) 三个浮点坐标值。 + /// + public class XChartDataModel3D + { + // ── 数据点结构 ──────────────────────────────────────────── + + /// + /// 单个数据点。 + /// + [Serializable] + public struct DataPoint + { + public double x; + public double y; + public double z; + + public DataPoint(double x, double y, double z) + { + this.x = x; + this.y = y; + this.z = z; + } + + public Vector3 ToVector3() => new Vector3((float)x, (float)y, (float)z); + } + + // ── 数据存储 ────────────────────────────────────────────── + + private List m_Points = new List(); + + /// 所有数据点列表(只读) + public IReadOnlyList Points => m_Points; + + /// 数据点数量 + public int PointCount => m_Points.Count; + + /// 是否有数据 + public bool HasData => m_Points.Count > 0; + + // ── 数据范围(自动追踪) ────────────────────────────────── + + /// X 轴最小值 + public double MinX { get; private set; } + /// X 轴最大值 + public double MaxX { get; private set; } + /// Y 轴最小值 + public double MinY { get; private set; } + /// Y 轴最大值 + public double MaxY { get; private set; } + /// Z 轴最小值 + public double MinZ { get; private set; } + /// Z 轴最大值 + public double MaxZ { get; private set; } + + /// X 轴跨度 + public double RangeX => MaxX - MinX; + /// Y 轴跨度 + public double RangeY => MaxY - MinY; + /// Z 轴跨度 + public double RangeZ => MaxZ - MinZ; + + /// 所有轴的最大跨度(用于归一化) + public double MaxRange => Math.Max(Math.Max(RangeX, RangeY), RangeZ); + + // ── 脏标记 ──────────────────────────────────────────────── + + [Flags] + public enum DirtyFlags + { + None = 0, + DataChanged = 1 << 0, + RangeChanged = 1 << 1, + All = DataChanged | RangeChanged, + } + + public DirtyFlags CurrentDirty { get; private set; } + + public void MarkDirty(DirtyFlags flag) + { + CurrentDirty |= flag; + } + + public void ClearDirty(DirtyFlags flag) + { + CurrentDirty &= ~flag; + } + + public bool IsDirty(DirtyFlags flag) => (CurrentDirty & flag) != 0; + + // ── 数据操作 ────────────────────────────────────────────── + + /// 设置所有数据点(覆盖模式) + public void SetPoints(IList points) + { + m_Points.Clear(); + if (points != null) + { + foreach (var p in points) + m_Points.Add(p); + } + RecalculateRange(); + MarkDirty(DirtyFlags.All); + } + + /// 设置原始坐标数据(批量) + public void SetPoints(double[] xs, double[] ys, double[] zs) + { + m_Points.Clear(); + int count = Math.Min(Math.Min(xs.Length, ys.Length), zs.Length); + for (int i = 0; i < count; i++) + m_Points.Add(new DataPoint(xs[i], ys[i], zs[i])); + RecalculateRange(); + MarkDirty(DirtyFlags.All); + } + + /// 添加一个数据点 + public void AddPoint(double x, double y, double z) + { + m_Points.Add(new DataPoint(x, y, z)); + UpdateRangeForPoint(x, y, z); + MarkDirty(DirtyFlags.DataChanged); + } + + /// 批量添加数据点 + public void AddPoints(IList points) + { + foreach (var p in points) + { + m_Points.Add(p); + UpdateRangeForPoint(p.x, p.y, p.z); + } + MarkDirty(DirtyFlags.DataChanged); + } + + /// 移除指定索引的数据点 + public void RemovePoint(int index) + { + if (index < 0 || index >= m_Points.Count) return; + m_Points.RemoveAt(index); + RecalculateRange(); + MarkDirty(DirtyFlags.All); + } + + /// 清空所有数据 + public void ClearAll() + { + m_Points.Clear(); + MinX = MaxX = MinY = MaxY = MinZ = MaxZ = 0; + MarkDirty(DirtyFlags.All); + } + + /// 获取指定索引的数据点 + public DataPoint GetPoint(int index) + { + return m_Points[index]; + } + + /// 获取所有数据点作为 Vector3 数组 + public Vector3[] GetPointsAsVector3() + { + var result = new Vector3[m_Points.Count]; + for (int i = 0; i < m_Points.Count; i++) + result[i] = m_Points[i].ToVector3(); + return result; + } + + /// + /// 将数据点归一化到 [-1, 1] 立方体空间。 + /// 基于各轴的最大跨度进行等比例缩放,保持比例。 + /// + public Vector3[] GetNormalizedPoints() + { + if (m_Points.Count == 0) return Array.Empty(); + double range = MaxRange; + if (range < double.Epsilon) range = 1.0; + + double cx = (MinX + MaxX) * 0.5; + double cy = (MinY + MaxY) * 0.5; + double cz = (MinZ + MaxZ) * 0.5; + + var result = new Vector3[m_Points.Count]; + for (int i = 0; i < m_Points.Count; i++) + { + result[i] = new Vector3( + (float)((m_Points[i].x - cx) / range * 2.0), + (float)((m_Points[i].y - cy) / range * 2.0), + (float)((m_Points[i].z - cz) / range * 2.0)); + } + return result; + } + + // ── 范围计算 ────────────────────────────────────────────── + + private void RecalculateRange() + { + if (m_Points.Count == 0) + { + MinX = MaxX = MinY = MaxY = MinZ = MaxZ = 0; + return; + } + + MinX = MaxX = m_Points[0].x; + MinY = MaxY = m_Points[0].y; + MinZ = MaxZ = m_Points[0].z; + + for (int i = 1; i < m_Points.Count; i++) + { + UpdateRangeForPoint(m_Points[i].x, m_Points[i].y, m_Points[i].z); + } + } + + private void UpdateRangeForPoint(double x, double y, double z) + { + if (x < MinX) MinX = x; + if (x > MaxX) MaxX = x; + if (y < MinY) MinY = y; + if (y > MaxY) MaxY = y; + if (z < MinZ) MinZ = z; + if (z > MaxZ) MaxZ = z; + } + } +} \ No newline at end of file diff --git a/Runtime/XChart/XChartDataModel.cs.meta b/Runtime/XChart/XChartDataModel.cs.meta new file mode 100644 index 0000000..c0908e9 --- /dev/null +++ b/Runtime/XChart/XChartDataModel.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fb482a3fd3d6acf47925c52955c3c709 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XChart/XChartToolBase.cs b/Runtime/XChart/XChartToolBase.cs new file mode 100644 index 0000000..2befe31 --- /dev/null +++ b/Runtime/XChart/XChartToolBase.cs @@ -0,0 +1,104 @@ +using System; +using UnityEngine; + +namespace XericUI.XChart +{ + /// + /// 图表工具抽象基类。 + /// + /// 每个工具对应一个独立的视觉元素(背景网格、折线、散点等), + /// 由 ChartToolsManager 统一编排生命周期。 + /// + /// 生命周期顺序: + /// InternalInitialize (构造阶段) + /// → OnInitialize (派生初始化) + /// → OnUpdate (每帧更新) + /// → OnRender (每帧渲染) + /// → OnDispose (销毁) + /// + public abstract class XChartToolBase : IDisposable + { + // ── 编辑器/运行时统一 hideFlags ──────────────────────────── + + private const HideFlags k_EditorFlags = HideFlags.DontSaveInEditor; + private const HideFlags k_PlayFlags = HideFlags.HideAndDontSave; + + protected static HideFlags CurrentFlags => + Application.isPlaying ? k_PlayFlags : k_EditorFlags; + + // ── 基础属性 ────────────────────────────────────────────── + + protected XChartToolsManager Manager { get; private set; } + public int OrderIndex { get; set; } + public GameObject RootObject { get; private set; } + public RectTransform RootTransform { get; private set; } + public string ToolName { get; private set; } + public bool IsEnabled { get; set; } = true; + public bool IsInitialized { get; protected set; } + + // ── 构造 ────────────────────────────────────────────────── + + protected XChartToolBase() { } + + internal void InternalInitialize(XChartToolsManager manager, string name, + GameObject rootObject, int orderIndex) + { + Manager = manager ?? throw new ArgumentNullException(nameof(manager)); + ToolName = name; + RootObject = rootObject ?? throw new ArgumentNullException(nameof(rootObject)); + RootTransform = rootObject.GetComponent(); + if (RootTransform == null) + RootTransform = rootObject.AddComponent(); + OrderIndex = orderIndex; + + rootObject.hideFlags = CurrentFlags; + + OnInitialize(); + IsInitialized = true; + } + + // ── 生命周期 ────────────────────────────────────────────── + + protected virtual void OnInitialize() { } + public virtual void OnUpdate() { } + public virtual void OnRender() { } + protected virtual void OnDispose() { } + + public void Dispose() + { + OnDispose(); + if (RootObject != null) + { + UnityEngine.Object.DestroyImmediate(RootObject); + RootObject = null; + } + IsInitialized = false; + } + + // ── 子组件管理 ──────────────────────────────────────────── + + protected RectTransform CreateChild(string childName) + { + var child = new GameObject(childName); + child.transform.SetParent(RootTransform, false); + child.hideFlags = CurrentFlags; + + var rt = child.GetComponent(); + if (rt == null) rt = child.AddComponent(); + return rt; + } + + protected T CreateChildComponent(string childName) where T : Component + { + var child = CreateChild(childName); + var comp = child.gameObject.GetComponent(); + if (comp == null) comp = child.gameObject.AddComponent(); + return comp; + } + + public virtual void SetVisible(bool visible) + { + if (RootObject != null) RootObject.SetActive(visible); + } + } +} \ No newline at end of file diff --git a/Runtime/XChart/XChartToolBase.cs.meta b/Runtime/XChart/XChartToolBase.cs.meta new file mode 100644 index 0000000..fc7147a --- /dev/null +++ b/Runtime/XChart/XChartToolBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 457e68fd57ab6ae40a189152b21ce0c2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XChart/XChartToolsManager.cs b/Runtime/XChart/XChartToolsManager.cs new file mode 100644 index 0000000..d0d0f5f --- /dev/null +++ b/Runtime/XChart/XChartToolsManager.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace XericUI.XChart +{ + /// + /// 图表工具管理器。 + /// + /// 负责管理所有图表渲染工具的注册、生命周期编排、执行顺序控制。 + /// 挂在 XChartBase 上,作为工具的中央调度器。 + /// + public class XChartToolsManager + { + // ── 工具列表 ────────────────────────────────────────────── + + private List m_Tools = new List(8); + + /// 只读工具列表(用于外部遍历) + public IReadOnlyList Tools => m_Tools; + + /// 当前工具数量 + public int ToolCount => m_Tools.Count; + + // ── 管理节点 ────────────────────────────────────────────── + + /// 所有工具的根父节点(由 XChartBase 指定) + public Transform ToolsRoot { get; set; } + + // ── 版本控制(防溢出) ──────────────────────────────────── + + private int m_Version; + + // ── 添加工具 ────────────────────────────────────────────── + + /// + /// 添加一个图表工具。 + /// 自动在 ToolsRoot 下创建同名子 GameObject,挂载并初始化工具。 + /// + public T AddTool(string toolName, int orderIndex = 0) + where T : XChartToolBase, new() + { + if (ToolsRoot == null) + throw new InvalidOperationException("ToolsRoot 未设置,无法添加工具"); + + var flags = Application.isPlaying + ? HideFlags.HideAndDontSave + : HideFlags.DontSaveInEditor; + + var rootObject = new GameObject(toolName); + rootObject.transform.SetParent(ToolsRoot, false); + rootObject.hideFlags = flags; + + var tool = new T(); + tool.InternalInitialize(this, toolName, rootObject, orderIndex); + + // 按 OrderIndex 插入到有序位置 + int insertIdx = m_Tools.Count; + for (int i = 0; i < m_Tools.Count; i++) + { + if (m_Tools[i].OrderIndex > orderIndex) + { + insertIdx = i; + break; + } + } + m_Tools.Insert(insertIdx, tool); + + rootObject.transform.SetSiblingIndex(insertIdx); + m_Version++; + return tool; + } + + // ── 移除工具 ────────────────────────────────────────────── + + public void RemoveTool(XChartToolBase tool) + { + if (tool == null || !m_Tools.Contains(tool)) return; + tool.Dispose(); + m_Tools.Remove(tool); + m_Version++; + } + + public bool RemoveToolByName(string toolName) + { + for (int i = m_Tools.Count - 1; i >= 0; i--) + { + if (m_Tools[i].ToolName == toolName) + { + RemoveTool(m_Tools[i]); + return true; + } + } + return false; + } + + public void RemoveAllTools() + { + for (int i = m_Tools.Count - 1; i >= 0; i--) + m_Tools[i].Dispose(); + m_Tools.Clear(); + m_Version++; + } + + // ── 查找工具 ────────────────────────────────────────────── + + public T FindTool(string toolName = null) where T : XChartToolBase + { + for (int i = 0; i < m_Tools.Count; i++) + { + if (m_Tools[i] is T tool) + { + if (toolName == null || tool.ToolName == toolName) + return tool; + } + } + return null; + } + + // ── 生命周期驱动 ────────────────────────────────────────── + + public void UpdateAll() + { + for (int i = 0; i < m_Tools.Count; i++) + { + var tool = m_Tools[i]; + if (!tool.IsEnabled || !tool.IsInitialized) continue; + + int snap = m_Version; + tool.OnUpdate(); + if (snap != m_Version && (i >= m_Tools.Count || m_Tools[i] != tool)) + i--; + } + } + + public void RenderAll() + { + for (int i = 0; i < m_Tools.Count; i++) + { + var tool = m_Tools[i]; + if (!tool.IsEnabled || !tool.IsInitialized) continue; + + int snap = m_Version; + tool.OnRender(); + if (snap != m_Version && (i >= m_Tools.Count || m_Tools[i] != tool)) + i--; + } + } + + // ── 清理 ────────────────────────────────────────────────── + + public void Dispose() + { + RemoveAllTools(); + m_Tools.Clear(); + } + } +} \ No newline at end of file diff --git a/Runtime/XChart/XChartToolsManager.cs.meta b/Runtime/XChart/XChartToolsManager.cs.meta new file mode 100644 index 0000000..d5dc5a2 --- /dev/null +++ b/Runtime/XChart/XChartToolsManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 01fbdb69e67f68c43a8a759ca6b8c5c4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: