Files

411 lines
15 KiB
C#

using UnityEngine;
using XericLibrary.Runtime.UIGraph;
using PrimitiveType = XericLibrary.Runtime.UIGraph.PrimitiveType;
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>3D 数据模型(只读)</summary>
public XChartDataModel3D DataModel3D => 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);
SetSharedData(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);
EnsureSharedDimensions();
DataModel.TryAddRecord(m_DataModel.PointCount, new[] { (float)x, (float)y, (float)z });
RefreshChart();
}
private void SetSharedData(double[] xs, double[] ys, double[] zs)
{
EnsureSharedDimensions();
DataModel.Clear();
int count = System.Math.Min(System.Math.Min(xs.Length, ys.Length), zs.Length);
for (int i = 0; i < count; i++)
DataModel.TryAddRecord(i, new[] { (float)xs[i], (float)ys[i], (float)zs[i] });
}
private void EnsureSharedDimensions()
{
if (DataModel.NumericDimensionCount == 3) return;
DataModel.SetDimensions(new System.Collections.Generic.List<XChartDimensionDefinition>
{
new XChartDimensionDefinition("x", "X", XChartDimensionKind.Number),
new XChartDimensionDefinition("y", "Y", XChartDimensionKind.Number),
new XChartDimensionDefinition("z", "Z", XChartDimensionKind.Number),
});
}
/// <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
}
}