添加图表功能。
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 44ce2208ca404bb3a4cbe30198a10bfd
|
||||
timeCreated: 1787107079
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2575261069f399745ad6b672961c1fcb
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bc03406131f1b1042875d31cbde6832f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2b0e82a8b831b2f41a048413951f28bc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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()
|
||||
{
|
||||
// 由派生类按需扩展
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8b059e556e780e14586c63a61c5290d7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fb482a3fd3d6acf47925c52955c3c709
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 457e68fd57ab6ae40a189152b21ce0c2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 01fbdb69e67f68c43a8a759ca6b8c5c4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user