添加图表功能。

This commit is contained in:
2026-08-21 11:52:08 +08:00
parent b132e8e167
commit 5a28ab5c59
27 changed files with 3102 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 44ce2208ca404bb3a4cbe30198a10bfd
timeCreated: 1787107079
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2575261069f399745ad6b672961c1fcb
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+461
View File
@@ -0,0 +1,461 @@
using System.Collections.Generic;
using UnityEngine;
using XericLibrary.Runtime.UIGraph;
namespace XericUI.XChart.Pseudo3D
{
/// <summary>
/// 伪 3D 立方体背景工具。
///
/// 使用 UILineRendererV2 绘制:
/// 1. 立方体 12 条棱的线框(前向棱使用实色,后向棱可选半透明)
/// 2. 三个可见面上的轴标线网格
///
/// 3D 坐标系统:
/// 立方体中心在原点,半边长 1(即 [-1, 1] 范围)。
/// 通过 pitch/yaw 旋转后进行正交投影(丢弃 Z 轴)得到 2D UI 坐标。
/// 最终通过 scale + offset 映射到 RectTransform 的像素空间。
///
/// 子节点结构:
/// [ChartTool_CubeBg]
/// └─ CubeRenderer (UILineRendererV2)
/// </summary>
[AddComponentMenu("Xeric/XChart/Pseudo3D/Cube Background Tool")]
public class ChartToolCubeBg : XChartToolBase
{
// ── 外观配置 ──────────────────────────────────────────────
/// <summary>立方体半边长(UI 像素)</summary>
public float CubeHalfSize { get; set; } = 150f;
/// <summary>俯仰角(绕 X 轴旋转,度数)</summary>
public float Pitch { get; set; } = 25f;
/// <summary>偏航角(绕 Y 轴旋转,度数)</summary>
public float Yaw { get; set; } = -30f;
/// <summary>前向棱颜色</summary>
public Color FrontEdgeColor { get; set; } = new Color(0.7f, 0.7f, 0.7f, 1f);
/// <summary>后向棱颜色</summary>
public Color BackEdgeColor { get; set; } = new Color(0.4f, 0.4f, 0.4f, 0.4f);
/// <summary>棱线宽度</summary>
public float EdgeThickness { get; set; } = 2f;
/// <summary>网格线颜色</summary>
public Color GridLineColor { get; set; } = new Color(0.5f, 0.5f, 0.5f, 0.3f);
/// <summary>网格线宽度</summary>
public float GridLineThickness { get; set; } = 1f;
/// <summary>每轴网格分割数</summary>
public int GridDivisions { get; set; } = 5;
/// <summary>是否显示后向棱</summary>
public bool ShowBackEdges { get; set; } = true;
/// <summary>是否显示网格标线</summary>
public bool ShowGridLines { get; set; } = true;
/// <summary>立方体中心偏移</summary>
public Vector2 CenterOffset { get; set; } = Vector2.zero;
// ── 内部组件 ──────────────────────────────────────────────
/// <summary>棱线渲染器</summary>
public UILineRendererV2 EdgeRenderer { get; private set; }
/// <summary>网格线渲染器</summary>
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;
/// <summary>投影版本号(每次投影矩阵变化时递增,供外部检测变化)</summary>
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<UILineRendererV2>("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<UILineRendererV2>("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);
}
}
/// <summary>标记投影需要重新计算</summary>
public void MarkProjectionDirty()
{
m_ProjectionDirty = true;
ProjectionVersion++;
}
// ── 3D 投影辅助 ──────────────────────────────────────────
/// <summary>将 3D 点投影到 2D</summary>
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);
}
/// <summary>批量投影 3D 点到 2D</summary>
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;
}
/// <summary>获取当前旋转矩阵</summary>
public Matrix4x4 RotationMatrix => m_RotationMatrix;
/// <summary>获取当前缩放</summary>
public float Scale => m_Scale;
/// <summary>获取当前偏移</summary>
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<LineVertexData>();
var backVerts = new List<LineVertexData>();
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);
}
}
/// <summary>获取可见面索引列表(按可见度排序,最多 3 个)</summary>
private List<int> GetVisibleFaces()
{
var result = new List<int>(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;
}
/// <summary>在指定面上绘制网格线</summary>
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;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 032a5991fdcc71d4491a44248dc05d19
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,261 @@
using UnityEngine;
using XericLibrary.Runtime.UIGraph;
using PrimitiveType = XericLibrary.Runtime.UIGraph.PrimitiveType;
namespace XericUI.XChart.Pseudo3D
{
/// <summary>
/// 散点渲染工具。
///
/// 使用 XScatterPointRenderer 渲染 3D 散点数据在 2D 平面上的投影。
/// 数据点从 3D 立方体空间投影到 UI 坐标,每个点用一个图元表示。
///
/// 子节点结构:
/// [ChartTool_ScatterPoints]
/// └─ PointRenderer (XScatterPointRenderer)
/// </summary>
[AddComponentMenu("Xeric/XChart/Pseudo3D/Scatter Points Tool")]
public class ChartToolScatterPoints : XChartToolBase
{
// ── 配置 ──────────────────────────────────────────────────
/// <summary>点的大小(像素)</summary>
public float PointSize
{
get => m_PointSize;
set { m_PointSize = Mathf.Max(0.5f, value); if (IsInitialized) SyncShapeParams(); }
}
private float m_PointSize = 8f;
/// <summary>图元类型</summary>
public PrimitiveType PointType
{
get => m_PointType;
set { m_PointType = value; if (IsInitialized) SyncShapeParams(); }
}
private PrimitiveType m_PointType = PrimitiveType.Polygon;
/// <summary>尺寸模式</summary>
public SizeMode SizeMode
{
get => m_SizeMode;
set { m_SizeMode = value; if (IsInitialized) SyncShapeParams(); }
}
private SizeMode m_SizeMode = SizeMode.InscribedCircle;
/// <summary>填充颜色</summary>
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);
/// <summary>中心渐变颜色</summary>
public Color CenterColor
{
get => m_CenterColor;
set { m_CenterColor = value; if (IsInitialized) SyncShapeParams(); }
}
private Color m_CenterColor = Color.white;
/// <summary>边框颜色</summary>
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);
/// <summary>边框宽度</summary>
public float BorderThickness
{
get => m_BorderThickness;
set { m_BorderThickness = Mathf.Max(0f, value); if (IsInitialized) SyncShapeParams(); }
}
private float m_BorderThickness = 1f;
/// <summary>多边形边数</summary>
public int SideCount
{
get => m_SideCount;
set { m_SideCount = Mathf.Max(3, value); if (IsInitialized) SyncShapeParams(); }
}
private int m_SideCount = 8;
/// <summary>倒角大小</summary>
public float ChamferSize
{
get => m_ChamferSize;
set { m_ChamferSize = Mathf.Clamp01(value); if (IsInitialized) SyncShapeParams(); }
}
private float m_ChamferSize = 0f;
/// <summary>图元旋转角度</summary>
public float PointAngle
{
get => m_PointAngle;
set { m_PointAngle = value; if (IsInitialized) SyncShapeParams(); }
}
private float m_PointAngle = 0f;
/// <summary>是否使用颜色渐变(按数据范围)</summary>
public bool EnableColorMapping { get; set; } = false;
/// <summary>颜色映射渐变起始色</summary>
public Color ColorMapMin { get; set; } = Color.blue;
/// <summary>颜色映射渐变结束色</summary>
public Color ColorMapMax { get; set; } = Color.red;
/// <summary>颜色映射轴(0=X, 1=Y, 2=Z)</summary>
public int ColorMapAxis { get; set; } = 2;
// ── 内部组件 ──────────────────────────────────────────────
/// <summary>散点渲染器</summary>
public XScatterPointRenderer PointRenderer { get; private set; }
/// <summary>对 CubeBg 工具的引用(用于投影)</summary>
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<XScatterPointRenderer>("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();
}
}
// ── 数据更新 ──────────────────────────────────────────────
/// <summary>
/// 设置要渲染的 3D 点(归一化到 [-1, 1] 立方体空间)。
/// </summary>
public void SetNormalizedPoints(Vector3[] normalizedPoints)
{
m_NormalizedPoints = normalizedPoints ?? new Vector3[0];
m_ProjectedPoints = new Vector2[m_NormalizedPoints.Length];
m_DataDirty = true;
}
/// <summary>标记数据需要重新投影</summary>
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;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 63688f4e07bc3a44197a1fe0cec994a9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,386 @@
using UnityEngine;
using XericLibrary.Runtime.UIGraph;
namespace XericUI.XChart.Pseudo3D
{
/// <summary>
/// 伪 3D 散点图主组件。
///
/// 在 UI 平面上绘制一个带有透视效果的 3D 散点图:
/// - 立方体线框代表三维空间边界
/// - 三个可见面上的标线网格帮助定位
/// - 数据点以图元(多边形/矩形)形式渲染在立方体内部
///
/// 使用方式:
/// 1. 在场景中创建空 GameObject,挂载本组件
/// 2. 设置数据:chart.SetData(xs, ys, zs)
/// 3. 调整外观参数
///
/// 子节点结构:
/// [XPseudo3DScatterChart]
/// └─ [XChartTools]
/// ├─ [ChartTool_CubeBg] ← 立方体线框 + 网格
/// │ ├─ CubeEdgeRenderer (UILineRendererV2)
/// │ └─ CubeGridRenderer (UILineRendererV2)
/// └─ [ChartTool_ScatterPoints]
/// └─ PointRenderer (XScatterPointRenderer)
/// </summary>
[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();
/// <summary>数据模型(只读)</summary>
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<ChartToolCubeBg>("ChartTool_CubeBg", 0);
}
// 创建散点工具(OrderIndex 10,后渲染,在上层)
if (m_ScatterPoints == null)
{
m_ScatterPoints = AddTool<ChartToolScatterPoints>("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
/// <summary>刷新图表(重新投影所有数据点)</summary>
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);
}
}
/// <summary>设置散点数据</summary>
/// <param name="xs">X 坐标数组</param>
/// <param name="ys">Y 坐标数组</param>
/// <param name="zs">Z 坐标数组</param>
public void SetData(double[] xs, double[] ys, double[] zs)
{
m_DataModel.SetPoints(xs, ys, zs);
RefreshChart();
}
/// <summary>设置散点数据(Vector3 数组)</summary>
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);
}
/// <summary>添加一个数据点</summary>
public void AddDataPoint(double x, double y, double z)
{
m_DataModel.AddPoint(x, y, z);
RefreshChart();
}
/// <summary>清空所有数据</summary>
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
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5857501cfde29c94ab1a0aa2896b652b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,155 @@
using UnityEngine;
using XericLibrary.Runtime.UIGraph;
namespace XericUI.XChart.Pseudo3D
{
/// <summary>
/// 散点图元渲染器。
///
/// 继承 PrimitiveCacheRendererBase,支持在一个 MaskableGraphic 中批量渲染多个图元。
/// 所有散点共享同一套形状参数,但位置和颜色可独立设置。
///
/// 相比为每个点创建独立的 UIPrimitiveRenderer,本组件大幅减少 GameObject 数量和 Draw Call。
/// </summary>
[AddComponentMenu("Xeric/XChart/Pseudo3D/Scatter Point Renderer")]
public class XScatterPointRenderer : PrimitiveCacheRendererBase
{
// ── 全局形状参数 ──────────────────────────────────────────
/// <summary>图元类型(多边形 / 矩形)</summary>
public PrimitiveType pointType = PrimitiveType.Polygon;
/// <summary>尺寸模式</summary>
public SizeMode sizeMode = SizeMode.InscribedCircle;
/// <summary>点大小(像素)</summary>
public float pointSize = 8f;
/// <summary>填充颜色</summary>
public Color fillColor = Color.white;
/// <summary>中心颜色(渐变中心)</summary>
public Color centerColor = Color.white;
/// <summary>边框颜色</summary>
public Color borderColor = Color.black;
/// <summary>边框宽度</summary>
public float borderThickness = 1f;
/// <summary>多边形边数(仅 polygon 类型有效)</summary>
[Range(3, 64)]
public int sideCount = 8;
/// <summary>倒角大小</summary>
[Range(0f, 1f)]
public float chamferSize = 0f;
/// <summary>倒角分段数</summary>
[Range(1, 16)]
public int chamferSegments = 4;
/// <summary>旋转角度</summary>
[Range(-180f, 180f)]
public float angle = 0f;
// ── 公共 API ──────────────────────────────────────────────
/// <summary>
/// 批量设置散点位置。
/// 清除所有现有图元,用新位置重建。
/// </summary>
/// <param name="positions">2D 位置数组</param>
/// <param name="colors">可选:每个点的颜色(null 则使用 fillColor)</param>
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);
}
}
/// <summary>
/// 更新单个点的位置(不重建所有点,仅标记脏)。
/// </summary>
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);
}
/// <summary>
/// 更新单个点的颜色
/// </summary>
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);
}
/// <summary>更新全局形状参数并重建所有点</summary>
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
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 91affc3968f75e44fab8e970b6f725f5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: bc03406131f1b1042875d31cbde6832f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+314
View File
@@ -0,0 +1,314 @@
using UnityEngine;
using XericLibrary.Runtime.UIGraph;
using TMPro;
namespace XericUI.XChart.Radar
{
/// <summary>
/// 雷达图网格背景工具。
///
/// 使用 UILineRendererV2 绘制:
/// 1. 同心多边形网格(外圈 + 内圈百分比环)
/// 2. 从中心到各维度的轴线
/// 3. 每个维度端点的 TMP 标签
///
/// 子节点结构:
/// [ChartTool_RadarBg]
/// ├─ GridRenderer (UILineRendererV2) — 网格线
/// ├─ AxisRenderer (UILineRendererV2) — 轴线
/// └─ [Labels] (transform)
/// ├─ Label_0 (TMP_Text)
/// ├─ Label_1 (TMP_Text)
/// └─ ...
/// </summary>
[AddComponentMenu("Xeric/XChart/Radar/Radar Background Tool")]
public class ChartToolRadarBg : XChartToolBase
{
// ── 配置 ──────────────────────────────────────────────────
/// <summary>雷达图半径(像素)</summary>
public float Radius { get; set; } = 120f;
/// <summary>中心位置</summary>
public Vector2 Center { get; set; } = Vector2.zero;
/// <summary>维度数量</summary>
public int DimensionCount { get; set; } = 5;
/// <summary>维度名称列表</summary>
public string[] DimensionNames { get; set; } = new string[0];
/// <summary>网格环数量(含外圈)</summary>
public int RingCount { get; set; } = 4;
/// <summary>网格颜色</summary>
public Color GridColor { get; set; } = new Color(0.5f, 0.5f, 0.5f, 0.3f);
/// <summary>轴线颜色</summary>
public Color AxisColor { get; set; } = new Color(0.5f, 0.5f, 0.5f, 0.2f);
/// <summary>网格线宽</summary>
public float GridThickness { get; set; } = 1f;
/// <summary>轴线宽</summary>
public float AxisThickness { get; set; } = 1f;
/// <summary>是否显示网格</summary>
public bool ShowGrid { get; set; } = true;
/// <summary>是否显示轴线</summary>
public bool ShowAxis { get; set; } = true;
/// <summary>是否显示标签</summary>
public bool ShowLabels { get; set; } = true;
/// <summary>标签字体大小</summary>
public float LabelFontSize { get; set; } = 14f;
/// <summary>标签颜色</summary>
public Color LabelColor { get; set; } = Color.white;
/// <summary>标签相对端点的偏移量(像素)</summary>
public float LabelOffset { get; set; } = 10f;
// ── 内部组件 ──────────────────────────────────────────────
/// <summary>网格线渲染器</summary>
public UILineRendererV2 GridRenderer { get; private set; }
/// <summary>轴线渲染器</summary>
public UILineRendererV2 AxisRenderer { get; private set; }
/// <summary>标签的父变换</summary>
public Transform LabelsRoot { get; private set; }
/// <summary>所有标签文本组件</summary>
public TMP_Text[] LabelTexts { get; private set; }
// ── 内部状态 ──────────────────────────────────────────────
private bool m_DataDirty = true;
// ── 初始化 ────────────────────────────────────────────────
protected override void OnInitialize()
{
// 创建网格线渲染器
GridRenderer = CreateChildComponent<UILineRendererV2>("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<UILineRendererV2>("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);
}
}
/// <summary>确保标签数组长度匹配维度数量</summary>
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<TextMeshProUGUI>();
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;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e57afb5b224b01e4b8ca1fea0a49aa3c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+136
View File
@@ -0,0 +1,136 @@
using UnityEngine;
using XericLibrary.Runtime.UIGraph;
namespace XericUI.XChart.Radar
{
/// <summary>
/// 雷达图数据线框渲染工具。
///
/// 使用 UILineRendererV2 绘制每个数据系列的多边形轮廓。
/// 仅绘制线框,无填充效果。
/// 每个系列一个闭合多边形,颜色和线宽独立配置。
///
/// 子节点结构:
/// [ChartTool_RadarData]
/// └─ DataRenderer (UILineRendererV2) — 所有数据系列的线框
/// </summary>
[AddComponentMenu("Xeric/XChart/Radar/Radar Data Tool")]
public class ChartToolRadarData : XChartToolBase
{
// ── 配置 ──────────────────────────────────────────────────
/// <summary>雷达图半径(像素)</summary>
public float Radius { get; set; } = 120f;
/// <summary>中心位置</summary>
public Vector2 Center { get; set; } = Vector2.zero;
/// <summary>维度数量</summary>
public int DimensionCount { get; set; } = 5;
// ── 内部组件 ──────────────────────────────────────────────
/// <summary>数据线框渲染器</summary>
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<UILineRendererV2>("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);
}
// ── 数据更新 ──────────────────────────────────────────────
/// <summary>
/// 设置数据系列。
/// </summary>
/// <param name="series">数据系列数组</param>
/// <param name="maxValue">当前最大值(用于归一化)</param>
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;
}
/// <summary>标记数据为脏(下次渲染时刷新)</summary>
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;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0f229dbe8b6a83043b6a6d2f4605a47c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,263 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace XericUI.XChart.Radar
{
/// <summary>
/// 雷达图数据模型。
///
/// 管理多个数据系列(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 });
/// </summary>
public class XChartRadarDataModel
{
// ── 数据系列 ──────────────────────────────────────────────
/// <summary>
/// 单个数据系列。
/// </summary>
[Serializable]
public struct RadarSeries
{
/// <summary>系列名称(用于图例)</summary>
public string name;
/// <summary>各维度的值(长度需与 Dimensions 一致)</summary>
public float[] values;
/// <summary>系列颜色</summary>
public Color color;
/// <summary>线宽</summary>
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;
}
}
// ── 数据存储 ──────────────────────────────────────────────
/// <summary>维度名称列表</summary>
public string[] Dimensions { get; private set; }
/// <summary>维度数量</summary>
public int DimensionCount => Dimensions?.Length ?? 0;
/// <summary>数据系列列表</summary>
public IReadOnlyList<RadarSeries> Series => m_Series;
/// <summary>系列数量</summary>
public int SeriesCount => m_Series.Count;
/// <summary>是否有数据系列</summary>
public bool HasSeries => m_Series.Count > 0;
private List<RadarSeries> m_Series = new List<RadarSeries>();
// ── 全局范围 ──────────────────────────────────────────────
/// <summary>所有系列中的全局最大值(用于归一化)</summary>
public float GlobalMaxValue { get; private set; } = 1f;
/// <summary>是否自动缩放(基于 GlobalMaxValue)</summary>
public bool AutoScale { get; set; } = true;
/// <summary>自定义最大值(AutoScale=false 时使用)</summary>
public float CustomMaxValue { get; set; } = 1f;
/// <summary>当前使用的最大值</summary>
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;
// ── 维度操作 ──────────────────────────────────────────────
/// <summary>设置维度名称</summary>
public void SetDimensions(params string[] dimensionNames)
{
Dimensions = dimensionNames ?? new string[0];
MarkDirty(DirtyFlags.DimensionsChanged);
}
/// <summary>设置维度名称(从列表)</summary>
public void SetDimensions(IList<string> 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);
}
// ── 系列操作 ──────────────────────────────────────────────
/// <summary>添加一个数据系列</summary>
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);
}
/// <summary>批量设置数据系列</summary>
public void SetSeries(IList<RadarSeries> series)
{
m_Series.Clear();
if (series != null)
{
foreach (var s in series)
m_Series.Add(s);
}
RecalculateRange();
MarkDirty(DirtyFlags.All);
}
/// <summary>移除指定系列</summary>
public void RemoveSeries(int index)
{
if (index < 0 || index >= m_Series.Count) return;
m_Series.RemoveAt(index);
RecalculateRange();
MarkDirty(DirtyFlags.DataChanged);
}
/// <summary>移除所有系列</summary>
public void ClearAll()
{
m_Series.Clear();
Dimensions = new string[0];
GlobalMaxValue = 1f;
MarkDirty(DirtyFlags.All);
}
/// <summary>获取指定系列</summary>
public RadarSeries GetSeries(int index) => m_Series[index];
/// <summary>更新指定系列的值</summary>
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;
}
// ── 坐标计算 ──────────────────────────────────────────────
/// <summary>
/// 计算指定维度在雷达图上的方向向量(从中心指向端点,单位向量)。
/// 角度从正上方(12点钟方向)开始,顺时针排列。
/// </summary>
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));
}
/// <summary>
/// 计算所有维度的方向向量。
/// </summary>
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;
}
/// <summary>
/// 计算雷达图最大半径时的端点位置。
/// </summary>
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;
}
/// <summary>
/// 计算某个系列的数据点位置(归一化后的值 × 半径)。
/// </summary>
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;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fb161051ad1848e47ab218085166ea3f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+363
View File
@@ -0,0 +1,363 @@
using UnityEngine;
namespace XericUI.XChart.Radar
{
/// <summary>
/// 雷达图主组件。
///
/// 在 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)
/// </summary>
[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();
/// <summary>数据模型(只读)</summary>
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<ChartToolRadarBg>("ChartTool_RadarBg", 0);
if (m_RadarData == null)
m_RadarData = AddTool<ChartToolRadarData>("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
/// <summary>刷新图表</summary>
public void RefreshChart()
{
EnsureToolsCreated();
SyncAllParams();
PushDataToTool();
}
/// <summary>设置维度名称</summary>
public void SetDimensions(params string[] dimensionNames)
{
m_DataModel.SetDimensions(dimensionNames);
// 由 Update 自动同步
}
/// <summary>设置维度名称(从列表)</summary>
public void SetDimensions(System.Collections.Generic.IList<string> dimensionNames)
{
m_DataModel.SetDimensions(dimensionNames);
}
/// <summary>添加一个数据系列(自动分配颜色)</summary>
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);
}
/// <summary>添加一个数据系列(指定颜色)</summary>
public void AddSeries(string name, float[] values, Color color, float lineWidth = 2f)
{
m_DataModel.AddSeries(name, values, color, lineWidth);
}
/// <summary>批量设置数据系列</summary>
public void SetSeries(System.Collections.Generic.IList<XChartRadarDataModel.RadarSeries> series)
{
m_DataModel.SetSeries(series);
}
/// <summary>移除指定系列</summary>
public void RemoveSeries(int index)
{
m_DataModel.RemoveSeries(index);
}
/// <summary>清空所有数据</summary>
public void ClearData()
{
m_DataModel.ClearAll();
RefreshChart();
}
/// <summary>是否自动缩放</summary>
public bool AutoScale
{
get => m_DataModel.AutoScale;
set { m_DataModel.AutoScale = value; RefreshChart(); }
}
/// <summary>自定义最大值(AutoScale=false 时使用)</summary>
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
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2b0e82a8b831b2f41a048413951f28bc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+118
View File
@@ -0,0 +1,118 @@
using UnityEngine;
namespace XericUI.XChart
{
/// <summary>
/// 图表主行为组件基类。
///
/// 标记 [ExecuteAlways] 确保编辑器下生命周期正常执行。
/// 派生类在 Start() 中创建工具并配置数据模型。
/// </summary>
[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<T>(string toolName, int orderIndex = 0)
where T : XChartToolBase, new()
{
if (ToolManager == null) Initialize();
return ToolManager.AddTool<T>(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()
{
// 由派生类按需扩展
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8b059e556e780e14586c63a61c5290d7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+232
View File
@@ -0,0 +1,232 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace XericUI.XChart
{
/// <summary>
/// 3D 散点图数据模型。
///
/// 存储 XYZ 三维坐标点数据,支持脏标记机制。
/// 每个数据点包含 (X, Y, Z) 三个浮点坐标值。
/// </summary>
public class XChartDataModel3D
{
// ── 数据点结构 ────────────────────────────────────────────
/// <summary>
/// 单个数据点。
/// </summary>
[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<DataPoint> m_Points = new List<DataPoint>();
/// <summary>所有数据点列表(只读)</summary>
public IReadOnlyList<DataPoint> Points => m_Points;
/// <summary>数据点数量</summary>
public int PointCount => m_Points.Count;
/// <summary>是否有数据</summary>
public bool HasData => m_Points.Count > 0;
// ── 数据范围(自动追踪) ──────────────────────────────────
/// <summary>X 轴最小值</summary>
public double MinX { get; private set; }
/// <summary>X 轴最大值</summary>
public double MaxX { get; private set; }
/// <summary>Y 轴最小值</summary>
public double MinY { get; private set; }
/// <summary>Y 轴最大值</summary>
public double MaxY { get; private set; }
/// <summary>Z 轴最小值</summary>
public double MinZ { get; private set; }
/// <summary>Z 轴最大值</summary>
public double MaxZ { get; private set; }
/// <summary>X 轴跨度</summary>
public double RangeX => MaxX - MinX;
/// <summary>Y 轴跨度</summary>
public double RangeY => MaxY - MinY;
/// <summary>Z 轴跨度</summary>
public double RangeZ => MaxZ - MinZ;
/// <summary>所有轴的最大跨度(用于归一化)</summary>
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;
// ── 数据操作 ──────────────────────────────────────────────
/// <summary>设置所有数据点(覆盖模式)</summary>
public void SetPoints(IList<DataPoint> points)
{
m_Points.Clear();
if (points != null)
{
foreach (var p in points)
m_Points.Add(p);
}
RecalculateRange();
MarkDirty(DirtyFlags.All);
}
/// <summary>设置原始坐标数据(批量)</summary>
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);
}
/// <summary>添加一个数据点</summary>
public void AddPoint(double x, double y, double z)
{
m_Points.Add(new DataPoint(x, y, z));
UpdateRangeForPoint(x, y, z);
MarkDirty(DirtyFlags.DataChanged);
}
/// <summary>批量添加数据点</summary>
public void AddPoints(IList<DataPoint> points)
{
foreach (var p in points)
{
m_Points.Add(p);
UpdateRangeForPoint(p.x, p.y, p.z);
}
MarkDirty(DirtyFlags.DataChanged);
}
/// <summary>移除指定索引的数据点</summary>
public void RemovePoint(int index)
{
if (index < 0 || index >= m_Points.Count) return;
m_Points.RemoveAt(index);
RecalculateRange();
MarkDirty(DirtyFlags.All);
}
/// <summary>清空所有数据</summary>
public void ClearAll()
{
m_Points.Clear();
MinX = MaxX = MinY = MaxY = MinZ = MaxZ = 0;
MarkDirty(DirtyFlags.All);
}
/// <summary>获取指定索引的数据点</summary>
public DataPoint GetPoint(int index)
{
return m_Points[index];
}
/// <summary>获取所有数据点作为 Vector3 数组</summary>
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;
}
/// <summary>
/// 将数据点归一化到 [-1, 1] 立方体空间。
/// 基于各轴的最大跨度进行等比例缩放,保持比例。
/// </summary>
public Vector3[] GetNormalizedPoints()
{
if (m_Points.Count == 0) return Array.Empty<Vector3>();
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;
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fb482a3fd3d6acf47925c52955c3c709
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+104
View File
@@ -0,0 +1,104 @@
using System;
using UnityEngine;
namespace XericUI.XChart
{
/// <summary>
/// 图表工具抽象基类。
///
/// 每个工具对应一个独立的视觉元素(背景网格、折线、散点等),
/// 由 ChartToolsManager 统一编排生命周期。
///
/// 生命周期顺序:
/// InternalInitialize (构造阶段)
/// → OnInitialize (派生初始化)
/// → OnUpdate (每帧更新)
/// → OnRender (每帧渲染)
/// → OnDispose (销毁)
/// </summary>
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<RectTransform>();
if (RootTransform == null)
RootTransform = rootObject.AddComponent<RectTransform>();
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<RectTransform>();
if (rt == null) rt = child.AddComponent<RectTransform>();
return rt;
}
protected T CreateChildComponent<T>(string childName) where T : Component
{
var child = CreateChild(childName);
var comp = child.gameObject.GetComponent<T>();
if (comp == null) comp = child.gameObject.AddComponent<T>();
return comp;
}
public virtual void SetVisible(bool visible)
{
if (RootObject != null) RootObject.SetActive(visible);
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 457e68fd57ab6ae40a189152b21ce0c2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+158
View File
@@ -0,0 +1,158 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace XericUI.XChart
{
/// <summary>
/// 图表工具管理器。
///
/// 负责管理所有图表渲染工具的注册、生命周期编排、执行顺序控制。
/// 挂在 XChartBase 上,作为工具的中央调度器。
/// </summary>
public class XChartToolsManager
{
// ── 工具列表 ──────────────────────────────────────────────
private List<XChartToolBase> m_Tools = new List<XChartToolBase>(8);
/// <summary>只读工具列表(用于外部遍历)</summary>
public IReadOnlyList<XChartToolBase> Tools => m_Tools;
/// <summary>当前工具数量</summary>
public int ToolCount => m_Tools.Count;
// ── 管理节点 ──────────────────────────────────────────────
/// <summary>所有工具的根父节点(由 XChartBase 指定)</summary>
public Transform ToolsRoot { get; set; }
// ── 版本控制(防溢出) ────────────────────────────────────
private int m_Version;
// ── 添加工具 ──────────────────────────────────────────────
/// <summary>
/// 添加一个图表工具。
/// 自动在 ToolsRoot 下创建同名子 GameObject,挂载并初始化工具。
/// </summary>
public T AddTool<T>(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<T>(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();
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 01fbdb69e67f68c43a8a759ca6b8c5c4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: