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: