1411 lines
55 KiB
C#
1411 lines
55 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.UIElements;
|
|
using EasyChart;
|
|
|
|
namespace EasyChart.Layers
|
|
{
|
|
public class LineSeriesRenderer : BaseSeriesRenderer
|
|
{
|
|
private readonly List<SeriesData> _orderedVisiblePoints = new List<SeriesData>();
|
|
private readonly Dictionary<long, float> _hoverAlpha = new Dictionary<long, float>();
|
|
private readonly List<long> _tmpHoverKeys = new List<long>(16);
|
|
|
|
private readonly HashSet<long> _categoryHover = new HashSet<long>();
|
|
private readonly HashSet<long> _categoryHoverTmp = new HashSet<long>();
|
|
|
|
private int _categoryHoverIndex = int.MinValue;
|
|
|
|
private readonly HashSet<long> _activeHoverKeys = new HashSet<long>();
|
|
|
|
// Renderer for Line and Scatter charts
|
|
public LineSeriesRenderer()
|
|
{
|
|
schedule.Execute(OnUpdate).Every(16);
|
|
}
|
|
|
|
public override void ClearHover()
|
|
{
|
|
bool changed = false;
|
|
|
|
if (_categoryHover.Count > 0)
|
|
{
|
|
_categoryHover.Clear();
|
|
changed = true;
|
|
}
|
|
|
|
if (_categoryHoverTmp.Count > 0)
|
|
{
|
|
_categoryHoverTmp.Clear();
|
|
}
|
|
|
|
if (_categoryHoverIndex != int.MinValue)
|
|
{
|
|
_categoryHoverIndex = int.MinValue;
|
|
changed = true;
|
|
}
|
|
|
|
if (_hoverAlpha.Count > 0)
|
|
{
|
|
_hoverAlpha.Clear();
|
|
changed = true;
|
|
}
|
|
|
|
if (changed) MarkDirtyRepaint();
|
|
}
|
|
|
|
private static bool HasTextureFillAnimation(TextureFillSettings fill)
|
|
{
|
|
return fill != null && fill.animationType != TextureFillAnimationType.None;
|
|
}
|
|
|
|
private static void EnsureLineSettingsInitialized(LineSettings settings)
|
|
{
|
|
if (settings == null) return;
|
|
if (settings.stroke == null) settings.stroke = new LineStrokeSettings();
|
|
if (settings.point == null) settings.point = new PointSettings();
|
|
if (settings.hover == null) settings.hover = new HoverHighlightSettings();
|
|
if (settings.area == null) settings.area = new AreaFillSettings();
|
|
}
|
|
|
|
private bool IsCartesianTransposed()
|
|
{
|
|
if (Data == null) return false;
|
|
AxisId xAxisId = (Data.Cartesian != null) ? Data.Cartesian.xAxisId : AxisId.XBottom;
|
|
AxisId yAxisId = (Data.Cartesian != null) ? Data.Cartesian.yAxisId : AxisId.YLeft;
|
|
|
|
var xAxis = GetAxisConfig(xAxisId);
|
|
var yAxis = GetAxisConfig(yAxisId);
|
|
var xDim = xAxis != null ? xAxis.axisType : AxisType.Category;
|
|
var yDim = yAxis != null ? yAxis.axisType : AxisType.Value;
|
|
return xDim == AxisType.Value && yDim == AxisType.Category;
|
|
}
|
|
|
|
protected override Vector2 GetPixelPos(Vector2 point, float width, float height)
|
|
{
|
|
if (IsCartesianTransposed())
|
|
{
|
|
return base.GetPixelPos(new Vector2(point.y, point.x), width, height);
|
|
}
|
|
return base.GetPixelPos(point, width, height);
|
|
}
|
|
|
|
private float GetEffectivePixelClipX(float width)
|
|
{
|
|
float pixelClipX = Mathf.Clamp01(_animationProgress) * width;
|
|
if (width <= 0f) return pixelClipX;
|
|
|
|
// When category axis windowing is active (labels count > visible span),
|
|
// preload one extra item offscreen to avoid missing the edge segment.
|
|
if (_animationProgress >= 0.999f)
|
|
{
|
|
AxisId xAxisId = (Data != null && Data.Cartesian != null) ? Data.Cartesian.xAxisId : AxisId.XBottom;
|
|
var xAxis = GetAxisConfig(xAxisId);
|
|
if (xAxis != null && xAxis.axisType == AxisType.Category && xAxis.labels != null && xAxis.labels.Count > 0)
|
|
{
|
|
int labelCount = xAxis.labels.Count;
|
|
int span = Mathf.RoundToInt(_xMax - _xMin + 1);
|
|
if (span < 1) span = 1;
|
|
if (labelCount > span)
|
|
{
|
|
return width * 2f;
|
|
}
|
|
}
|
|
}
|
|
|
|
return pixelClipX;
|
|
}
|
|
|
|
private void DrawPointMarkerWithAlpha(MeshGenerationContext context, Painter2D painter, Vector2 pos, float radius, TextureFillSettings fill, Color defaultColor, float alpha, Vector2 uvOffsetAdd)
|
|
{
|
|
if (radius <= 0f) return;
|
|
if (alpha <= 0.001f) return;
|
|
|
|
UnpackTextureFill(fill, defaultColor, out var tex, out var tiling, out var offset, out var color);
|
|
|
|
bool hasPro = ProPackage.IsInstalled;
|
|
if (hasPro && fill != null && fill.animationType == TextureFillAnimationType.TextureScale)
|
|
{
|
|
tiling = fill.tiling;
|
|
offset = fill.offset;
|
|
}
|
|
|
|
offset += uvOffsetAdd;
|
|
color.a *= alpha;
|
|
if (color.a <= 0.001f) return;
|
|
|
|
float r = radius;
|
|
if (tex != null) r *= EvalTextureScaleSizeMul(fill);
|
|
|
|
if (tex != null)
|
|
{
|
|
DrawTexturedQuad(
|
|
context,
|
|
new Rect(pos.x - r, pos.y - r, r * 2f, r * 2f),
|
|
tex,
|
|
tiling,
|
|
offset,
|
|
color,
|
|
true);
|
|
}
|
|
else
|
|
{
|
|
painter.fillColor = color;
|
|
painter.BeginPath();
|
|
painter.Arc(pos, r, 0, 360);
|
|
painter.Fill();
|
|
}
|
|
}
|
|
|
|
private void UpdateCategoryHover(int index, bool isCategoryAxis)
|
|
{
|
|
if (!isCategoryAxis)
|
|
{
|
|
if (_categoryHover.Count > 0 || _categoryHoverIndex != int.MinValue)
|
|
{
|
|
_categoryHover.Clear();
|
|
_categoryHoverIndex = int.MinValue;
|
|
MarkDirtyRepaint();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (_categoryHoverIndex == index) return;
|
|
|
|
_categoryHoverIndex = index;
|
|
_categoryHoverTmp.Clear();
|
|
|
|
if (Data == null || Data.Series == null) return;
|
|
|
|
for (int si = 0; si < Data.Series.Count; si++)
|
|
{
|
|
var serie = Data.Series[si];
|
|
if (serie == null || !serie.visible) continue;
|
|
if (serie.type != SerieType.Line) continue;
|
|
if (serie.settings is not LineSettings settings) continue;
|
|
EnsureLineSettingsInitialized(settings);
|
|
if (settings.hover == null || !settings.hover.enabled) continue;
|
|
|
|
var points = serie.seriesData;
|
|
if (points == null) continue;
|
|
|
|
for (int pi = 0; pi < points.Count; pi++)
|
|
{
|
|
var p = points[pi];
|
|
if (p == null) continue;
|
|
if (!Mathf.Approximately(p.x, index)) continue;
|
|
_categoryHoverTmp.Add(GetHoverKey(si, pi));
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!_categoryHover.SetEquals(_categoryHoverTmp))
|
|
{
|
|
_categoryHover.Clear();
|
|
foreach (var k in _categoryHoverTmp) _categoryHover.Add(k);
|
|
MarkDirtyRepaint();
|
|
}
|
|
}
|
|
|
|
private static long GetHoverKey(int serieIndex, int pointIndex)
|
|
{
|
|
return ((long)serieIndex << 32) ^ (uint)pointIndex;
|
|
}
|
|
|
|
private float GetHoverAlpha(int serieIndex, int pointIndex)
|
|
{
|
|
return _hoverAlpha.TryGetValue(GetHoverKey(serieIndex, pointIndex), out float a) ? a : 0f;
|
|
}
|
|
|
|
private void DrawHoverSegmentOverlay(Painter2D painter, Serie serie, LineSettings settings, float width, float height, int pointIndex, float alpha)
|
|
{
|
|
if (painter == null) return;
|
|
if (serie == null || serie.seriesData == null) return;
|
|
if (settings == null || settings.hover == null) return;
|
|
|
|
if (alpha <= 0.001f) return;
|
|
if (pointIndex < 0 || pointIndex >= serie.seriesData.Count) return;
|
|
|
|
float pixelClipX = GetEffectivePixelClipX(width);
|
|
|
|
int prev = pointIndex - 1;
|
|
int next = pointIndex + 1;
|
|
if (prev < 0 && next >= serie.seriesData.Count) return;
|
|
|
|
Color c = settings.hover.lineColor;
|
|
c.a *= alpha;
|
|
if (c.a <= 0.001f) return;
|
|
|
|
painter.strokeColor = c;
|
|
painter.lineWidth = Mathf.Max(settings.stroke != null ? settings.stroke.width : 1f, 1f) * Mathf.Max(1f, settings.hover.scale);
|
|
|
|
LineType lineType = (settings.stroke != null) ? settings.stroke.lineType : LineType.Straight;
|
|
|
|
bool DrawClippedLine(Vector2 aPos, Vector2 bPos)
|
|
{
|
|
if (aPos.x > pixelClipX && bPos.x > pixelClipX) return false;
|
|
|
|
if (aPos.x > pixelClipX)
|
|
{
|
|
// Start already clipped out
|
|
return false;
|
|
}
|
|
|
|
if (bPos.x > pixelClipX)
|
|
{
|
|
float denom = (bPos.x - aPos.x);
|
|
if (Mathf.Abs(denom) <= 0.00001f) return false;
|
|
float t = Mathf.Clamp01((pixelClipX - aPos.x) / denom);
|
|
bPos = Vector2.Lerp(aPos, bPos, t);
|
|
painter.BeginPath();
|
|
painter.MoveTo(aPos);
|
|
painter.LineTo(bPos);
|
|
painter.Stroke();
|
|
return false;
|
|
}
|
|
|
|
painter.BeginPath();
|
|
painter.MoveTo(aPos);
|
|
painter.LineTo(bPos);
|
|
painter.Stroke();
|
|
return true;
|
|
}
|
|
|
|
void DrawSmoothClipped(Vector2 p0, Vector2 p1)
|
|
{
|
|
float cpDistance = (p1.x - p0.x) * 0.5f;
|
|
Vector2 cp1 = p0 + new Vector2(cpDistance, 0);
|
|
Vector2 cp2 = p1 - new Vector2(cpDistance, 0);
|
|
|
|
int samples = 10;
|
|
Vector2 prevP = p0;
|
|
if (prevP.x > pixelClipX) return;
|
|
|
|
painter.BeginPath();
|
|
painter.MoveTo(prevP);
|
|
|
|
for (int i = 1; i <= samples; i++)
|
|
{
|
|
float t = (float)i / samples;
|
|
Vector2 curP = CubicBezier(p0, cp1, cp2, p1, t);
|
|
|
|
if (curP.x > pixelClipX)
|
|
{
|
|
float denom = (curP.x - prevP.x);
|
|
if (Mathf.Abs(denom) > 0.00001f)
|
|
{
|
|
float tc = Mathf.Clamp01((pixelClipX - prevP.x) / denom);
|
|
Vector2 clipP = Vector2.Lerp(prevP, curP, tc);
|
|
painter.LineTo(clipP);
|
|
}
|
|
break;
|
|
}
|
|
|
|
painter.LineTo(curP);
|
|
prevP = curP;
|
|
}
|
|
|
|
painter.Stroke();
|
|
}
|
|
|
|
void DrawSegment(int aIdx, int bIdx)
|
|
{
|
|
if (aIdx < 0 || bIdx < 0) return;
|
|
if (aIdx >= serie.seriesData.Count || bIdx >= serie.seriesData.Count) return;
|
|
|
|
var aDp = serie.seriesData[aIdx];
|
|
var bDp = serie.seriesData[bIdx];
|
|
if (aDp == null || bDp == null) return;
|
|
|
|
Vector2 aPos = GetPixelPos(new Vector2(aDp.x, aDp.value), width, height);
|
|
Vector2 bPos = GetPixelPos(new Vector2(bDp.x, bDp.value), width, height);
|
|
|
|
if (lineType == LineType.Step)
|
|
{
|
|
// Match Step building logic used in line rendering: prev -> (curr.x, prev.y) -> curr
|
|
Vector2 mid = new Vector2(bPos.x, aPos.y);
|
|
if (!DrawClippedLine(aPos, mid)) return;
|
|
DrawClippedLine(mid, bPos);
|
|
return;
|
|
}
|
|
|
|
if (lineType == LineType.Smooth)
|
|
{
|
|
DrawSmoothClipped(aPos, bPos);
|
|
return;
|
|
}
|
|
|
|
DrawClippedLine(aPos, bPos);
|
|
}
|
|
|
|
DrawSegment(prev, pointIndex);
|
|
DrawSegment(pointIndex, next);
|
|
}
|
|
|
|
private bool TryGetXAxisCategoryInfo(out AxisConfig axis, out int labelCount, out bool cellCenter)
|
|
{
|
|
axis = null;
|
|
labelCount = 0;
|
|
cellCenter = false;
|
|
|
|
AxisId xAxisId = (Data != null && Data.Cartesian != null) ? Data.Cartesian.xAxisId : AxisId.XBottom;
|
|
axis = GetAxisConfig(xAxisId);
|
|
if (axis == null || axis.axisType != AxisType.Category) return false;
|
|
if (axis.labels == null || axis.labels.Count <= 0) return false;
|
|
|
|
labelCount = axis.labels.Count;
|
|
cellCenter = axis.labelPlacement == CategoryLabelPlacement.CellCenter;
|
|
return true;
|
|
}
|
|
|
|
public override bool GetTooltip(TooltipContext context, List<TooltipItem> items, ref Vector2? cursorPosition, ref string categoryLabel)
|
|
{
|
|
if (Data == null || Data.Series == null) return false;
|
|
|
|
bool transposed = IsCartesianTransposed();
|
|
|
|
float width = context.Width;
|
|
float height = context.Height;
|
|
float areaX = context.LocalPos.x;
|
|
float areaY = context.LocalPos.y;
|
|
|
|
if (width <= 0 || height <= 0) return false;
|
|
|
|
float categoryPixel = transposed ? areaY : areaX;
|
|
float categorySize = transposed ? height : width;
|
|
float categoryMin = transposed ? _yMin : _xMin;
|
|
float categoryMax = transposed ? _yMax : _xMax;
|
|
|
|
AxisId xAxisId2 = (Data != null && Data.Cartesian != null) ? Data.Cartesian.xAxisId : AxisId.XBottom;
|
|
bool xOnTop = xAxisId2 == AxisId.XTop;
|
|
|
|
float ratioRaw = Mathf.Clamp01(categoryPixel / categorySize);
|
|
float ratio = (transposed && xOnTop == false) ? (1.0f - ratioRaw) : ratioRaw;
|
|
float dataCategory = ratio * (categoryMax - categoryMin) + categoryMin;
|
|
|
|
List<string> labels = null;
|
|
if (Data != null && Data.Axes != null)
|
|
{
|
|
AxisId xAxisId = (Data != null && Data.Cartesian != null) ? Data.Cartesian.xAxisId : AxisId.XBottom;
|
|
AxisId yAxisId = (Data != null && Data.Cartesian != null) ? Data.Cartesian.yAxisId : AxisId.YLeft;
|
|
AxisId categoryAxisId = transposed ? yAxisId : xAxisId;
|
|
|
|
for (int i = 0; i < Data.Axes.Count; i++)
|
|
{
|
|
var a = Data.Axes[i];
|
|
if (a != null && a.id == categoryAxisId && a.axisType == AxisType.Category)
|
|
{
|
|
labels = a.labels;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool isCategoryAxis = labels != null && labels.Count > 0;
|
|
bool cellCenter = false;
|
|
if (isCategoryAxis)
|
|
{
|
|
AxisId xAxisId = (Data != null && Data.Cartesian != null) ? Data.Cartesian.xAxisId : AxisId.XBottom;
|
|
AxisId yAxisId = (Data != null && Data.Cartesian != null) ? Data.Cartesian.yAxisId : AxisId.YLeft;
|
|
AxisId categoryAxisId = transposed ? yAxisId : xAxisId;
|
|
var axis = GetAxisConfig(categoryAxisId);
|
|
cellCenter = axis != null && axis.labelPlacement == CategoryLabelPlacement.CellCenter;
|
|
}
|
|
|
|
int index;
|
|
if (isCategoryAxis && cellCenter)
|
|
{
|
|
float idxF = (ratio * labels.Count) - 0.5f + categoryMin;
|
|
index = Mathf.RoundToInt(idxF);
|
|
}
|
|
else
|
|
{
|
|
index = Mathf.RoundToInt(dataCategory);
|
|
}
|
|
|
|
// Validate index for Category Axis
|
|
if (isCategoryAxis)
|
|
{
|
|
if (index < 0 || index >= labels.Count)
|
|
{
|
|
UpdateCategoryHover(int.MinValue, false);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
UpdateCategoryHover(index, isCategoryAxis);
|
|
|
|
// Set shared cursor position
|
|
if (cursorPosition == null)
|
|
{
|
|
if (isCategoryAxis)
|
|
{
|
|
// Snap to index
|
|
float t;
|
|
if (cellCenter)
|
|
{
|
|
t = (index - categoryMin + 0.5f) / Mathf.Max(1, labels.Count);
|
|
}
|
|
else
|
|
{
|
|
t = (float)(index - categoryMin) / (categoryMax - categoryMin);
|
|
}
|
|
|
|
if (float.IsNaN(t)) t = 0f;
|
|
t = Mathf.Clamp01(t);
|
|
|
|
float snapPixel = transposed
|
|
? (xOnTop ? (t * categorySize) : ((1.0f - t) * categorySize))
|
|
: (t * categorySize);
|
|
cursorPosition = transposed ? new Vector2(0, snapPixel) : new Vector2(snapPixel, 0);
|
|
}
|
|
else
|
|
{
|
|
// Continuous axis: Cursor follows mouse or snaps to closest point?
|
|
// For now, let's follow mouse X for smooth feel
|
|
cursorPosition = transposed ? new Vector2(0, areaY) : new Vector2(areaX, 0);
|
|
}
|
|
}
|
|
|
|
// Set Category Label if not already set
|
|
if (string.IsNullOrEmpty(categoryLabel))
|
|
{
|
|
if (isCategoryAxis && index < labels.Count && index >= 0)
|
|
{
|
|
categoryLabel = labels[index];
|
|
}
|
|
else if (!isCategoryAxis)
|
|
{
|
|
// Show value for continuous axis
|
|
AxisId xAxisId = (Data != null && Data.Cartesian != null) ? Data.Cartesian.xAxisId : AxisId.XBottom;
|
|
AxisId yAxisId = (Data != null && Data.Cartesian != null) ? Data.Cartesian.yAxisId : AxisId.YLeft;
|
|
AxisId axisId = transposed ? yAxisId : xAxisId;
|
|
var axis = GetAxisConfig(axisId);
|
|
categoryLabel = FormatAxisValue(dataCategory, axis, 2);
|
|
}
|
|
}
|
|
|
|
bool hit = false;
|
|
|
|
// Find values
|
|
foreach (var serie in Data.Series)
|
|
{
|
|
if (!serie.visible) continue;
|
|
if (serie.type != SerieType.Line) continue;
|
|
|
|
Color seriesColor = Color.white;
|
|
|
|
LineSettings settings = serie.settings as LineSettings;
|
|
if (settings == null) continue;
|
|
EnsureLineSettingsInitialized(settings);
|
|
seriesColor = settings.stroke.color;
|
|
|
|
bool found = false;
|
|
Vector2 val = Vector2.zero;
|
|
|
|
if (isCategoryAxis)
|
|
{
|
|
// Look for exact index match
|
|
if (serie.seriesData == null) continue;
|
|
foreach (var p in serie.seriesData)
|
|
{
|
|
if (p == null) continue;
|
|
if (Mathf.Approximately(p.x, index))
|
|
{
|
|
val = new Vector2(p.x, p.value);
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Look for closest point in X
|
|
float minDist = float.MaxValue;
|
|
// Define a search radius in data units relative to range
|
|
float range = categoryMax - categoryMin;
|
|
float threshold = (range > 0) ? range * 0.05f : 0.5f; // 5% range threshold
|
|
|
|
if (serie.seriesData == null) continue;
|
|
foreach (var p in serie.seriesData)
|
|
{
|
|
if (p == null) continue;
|
|
float dist = Mathf.Abs(p.x - dataCategory);
|
|
if (dist < minDist && dist < threshold)
|
|
{
|
|
minDist = dist;
|
|
val = new Vector2(p.x, p.value);
|
|
found = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (found)
|
|
{
|
|
items.Add(new TooltipItem
|
|
{
|
|
Name = serie.name,
|
|
Value = FormatAxisValue(
|
|
val.y,
|
|
GetAxisConfig((Data != null && Data.Cartesian != null) ? Data.Cartesian.yAxisId : AxisId.YLeft),
|
|
Mathf.Clamp(serie.labelSettings != null ? serie.labelSettings.decimalPlaces : 2, 0, 8)
|
|
),
|
|
Color = seriesColor
|
|
});
|
|
hit = true;
|
|
}
|
|
}
|
|
return hit;
|
|
}
|
|
|
|
private void OnUpdate()
|
|
{
|
|
if (panel == null) return;
|
|
if (Data == null || Data.Series == null) return;
|
|
|
|
bool dirty = false;
|
|
|
|
if (ProPackage.IsInstalled)
|
|
{
|
|
for (int i = 0; i < Data.Series.Count; i++)
|
|
{
|
|
var s = Data.Series[i];
|
|
if (s == null || !s.visible) continue;
|
|
if (s.type != SerieType.Line) continue;
|
|
if (s.settings is not LineSettings settings) continue;
|
|
EnsureLineSettingsInitialized(settings);
|
|
|
|
if (HasTextureFillAnimation(settings.stroke != null ? settings.stroke.textureFill : null)) { dirty = true; break; }
|
|
if (HasTextureFillAnimation(settings.area != null ? settings.area.textureFill : null)) { dirty = true; break; }
|
|
if (settings.point != null && HasTextureFillAnimation(settings.point.textureFill)) { dirty = true; break; }
|
|
if (settings.hover != null)
|
|
{
|
|
if (HasTextureFillAnimation(settings.hover.textureFill)) { dirty = true; break; }
|
|
if (settings.hover.point != null && HasTextureFillAnimation(settings.hover.point.textureFill)) { dirty = true; break; }
|
|
}
|
|
}
|
|
}
|
|
|
|
if (_hoverAlpha.Count > 0 || _categoryHover.Count > 0)
|
|
{
|
|
float dt = Time.deltaTime;
|
|
float speed = 12f;
|
|
|
|
// Active hover keys come from category hover (cursor line)
|
|
_activeHoverKeys.Clear();
|
|
foreach (var k in _categoryHover) _activeHoverKeys.Add(k);
|
|
|
|
foreach (var k in _activeHoverKeys)
|
|
{
|
|
if (!_hoverAlpha.ContainsKey(k)) _hoverAlpha[k] = 0f;
|
|
}
|
|
|
|
if (_hoverAlpha.Count > 0)
|
|
{
|
|
_tmpHoverKeys.Clear();
|
|
foreach (var k in _hoverAlpha.Keys)
|
|
{
|
|
_tmpHoverKeys.Add(k);
|
|
}
|
|
|
|
for (int i = 0; i < _tmpHoverKeys.Count; i++)
|
|
{
|
|
long k = _tmpHoverKeys[i];
|
|
float current = _hoverAlpha[k];
|
|
float target = _activeHoverKeys.Contains(k) ? 1f : 0f;
|
|
float next = Mathf.MoveTowards(current, target, speed * dt);
|
|
if (!Mathf.Approximately(next, current))
|
|
{
|
|
_hoverAlpha[k] = next;
|
|
dirty = true;
|
|
}
|
|
if (target <= 0f && next <= 0.0001f)
|
|
{
|
|
_hoverAlpha.Remove(k);
|
|
dirty = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (dirty) MarkDirtyRepaint();
|
|
}
|
|
|
|
protected override void OnGenerateVisualContent(MeshGenerationContext context)
|
|
{
|
|
if (Data == null || Data.Series == null) return;
|
|
|
|
var width = contentRect.width;
|
|
var height = contentRect.height;
|
|
if (width <= 0 || height <= 0) return;
|
|
|
|
var painter = context.painter2D;
|
|
|
|
for (int si = 0; si < Data.Series.Count; si++)
|
|
{
|
|
var serie = Data.Series[si];
|
|
if (serie == null || !serie.visible || serie.seriesData == null || serie.seriesData.Count == 0) continue;
|
|
if (serie.type != SerieType.Line) continue;
|
|
|
|
LineSettings settings = serie.settings as LineSettings;
|
|
if (settings == null) continue;
|
|
EnsureLineSettingsInitialized(settings);
|
|
|
|
// Draw Area
|
|
if (settings.area.show)
|
|
{
|
|
if (settings.area.textureFill != null && settings.area.textureFill.texture != null)
|
|
{
|
|
DrawAreaWithTexture(context, serie, settings, width, height);
|
|
}
|
|
else
|
|
{
|
|
DrawArea(painter, serie, settings, width, height);
|
|
}
|
|
}
|
|
|
|
// Draw Line
|
|
bool useStrokeTexture = settings.stroke != null && settings.stroke.textureFill != null && settings.stroke.textureFill.texture != null;
|
|
if (useStrokeTexture)
|
|
{
|
|
DrawLineWithTexture(context, serie, settings, width, height);
|
|
}
|
|
else
|
|
{
|
|
DrawLine(painter, serie, settings, width, height);
|
|
}
|
|
|
|
if (settings.hover != null && settings.hover.enabled)
|
|
{
|
|
// CursorLine category hover: may include multiple series at the same category index
|
|
if (_categoryHover.Count > 0)
|
|
{
|
|
foreach (var k in _categoryHover)
|
|
{
|
|
int hoveredSerieIndex = (int)(k >> 32);
|
|
if (hoveredSerieIndex != si) continue;
|
|
int hoveredPointIndex = (int)(uint)k;
|
|
float a = GetHoverAlpha(hoveredSerieIndex, hoveredPointIndex);
|
|
DrawHoverSegmentOverlay(painter, serie, settings, width, height, hoveredPointIndex, a);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Draw Points
|
|
bool basePointShow = settings.point != null && settings.point.show;
|
|
bool hoverPointShow = settings.hover != null && settings.hover.enabled && settings.hover.point != null && settings.hover.point.show;
|
|
if (basePointShow || hoverPointShow)
|
|
{
|
|
DrawPoints(context, painter, si, serie, settings, width, height);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DrawLineWithTexture(MeshGenerationContext context, Serie serie, LineSettings settings, float width, float height)
|
|
{
|
|
if (context.Equals(default(MeshGenerationContext))) return;
|
|
if (serie == null || serie.seriesData == null || serie.seriesData.Count < 2) return;
|
|
if (settings == null || settings.stroke == null || settings.stroke.textureFill == null) return;
|
|
|
|
UnpackTextureFill(settings.stroke.textureFill, settings.stroke.color, out var tex, out var tiling, out var uvOffset, out var tint);
|
|
if (tex == null) return;
|
|
|
|
if (tint.a <= 0.001f) return;
|
|
|
|
float lineWidth = Mathf.Max(0.1f, settings.stroke.width);
|
|
float halfW = lineWidth * 0.5f;
|
|
var lineType = settings.stroke.lineType;
|
|
float pixelClipX = GetEffectivePixelClipX(width);
|
|
|
|
var ordered = GetOrderedVisiblePoints(serie);
|
|
if (ordered == null || ordered.Count < 2) return;
|
|
|
|
var points = new List<Vector2>(serie.seriesData.Count * 2);
|
|
|
|
bool AddPoint(Vector2 p)
|
|
{
|
|
points.Add(p);
|
|
return true;
|
|
}
|
|
|
|
bool AddClipped(Vector2 p)
|
|
{
|
|
if (points.Count == 0)
|
|
{
|
|
if (p.x > pixelClipX) return false;
|
|
return AddPoint(p);
|
|
}
|
|
|
|
var last = points[points.Count - 1];
|
|
if (p.x <= pixelClipX)
|
|
{
|
|
return AddPoint(p);
|
|
}
|
|
|
|
float denom = (p.x - last.x);
|
|
if (Mathf.Abs(denom) <= 0.00001f)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
float t = Mathf.Clamp01((pixelClipX - last.x) / denom);
|
|
Vector2 clipped = Vector2.Lerp(last, p, t);
|
|
AddPoint(clipped);
|
|
return false;
|
|
}
|
|
|
|
if (lineType == LineType.Step)
|
|
{
|
|
for (int i = 0; i < ordered.Count; i++)
|
|
{
|
|
var dp = ordered[i];
|
|
if (dp == null) continue;
|
|
|
|
Vector2 pos = GetPixelPos(new Vector2(dp.x, dp.value), width, height);
|
|
if (i > 0)
|
|
{
|
|
var prev = ordered[i - 1];
|
|
if (prev == null) continue;
|
|
Vector2 prevPos = GetPixelPos(new Vector2(prev.x, prev.value), width, height);
|
|
if (!AddClipped(new Vector2(pos.x, prevPos.y))) break;
|
|
}
|
|
if (!AddClipped(pos)) break;
|
|
}
|
|
}
|
|
else if (lineType == LineType.Smooth)
|
|
{
|
|
var first = ordered[0];
|
|
if (first == null) return;
|
|
if (!AddClipped(GetPixelPos(new Vector2(first.x, first.value), width, height))) return;
|
|
|
|
for (int i = 0; i < ordered.Count - 1; i++)
|
|
{
|
|
var dp0 = ordered[i];
|
|
var dp1 = ordered[i + 1];
|
|
if (dp0 == null || dp1 == null) continue;
|
|
Vector2 p0 = GetPixelPos(new Vector2(dp0.x, dp0.value), width, height);
|
|
Vector2 p1 = GetPixelPos(new Vector2(dp1.x, dp1.value), width, height);
|
|
|
|
if (p0.x > pixelClipX) break;
|
|
|
|
float cpDistance = (p1.x - p0.x) * 0.5f;
|
|
Vector2 cp1 = p0 + new Vector2(cpDistance, 0);
|
|
Vector2 cp2 = p1 - new Vector2(cpDistance, 0);
|
|
|
|
int samples = 10;
|
|
bool finished = false;
|
|
for (int t = 1; t <= samples; t++)
|
|
{
|
|
float u = (float)t / samples;
|
|
Vector2 curvePoint = CubicBezier(p0, cp1, cp2, p1, u);
|
|
if (!AddClipped(curvePoint)) { finished = true; break; }
|
|
}
|
|
if (finished) break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0; i < ordered.Count; i++)
|
|
{
|
|
var dp = ordered[i];
|
|
if (dp == null) continue;
|
|
if (!AddClipped(GetPixelPos(new Vector2(dp.x, dp.value), width, height))) break;
|
|
}
|
|
}
|
|
|
|
if (points.Count < 2) return;
|
|
|
|
float totalLen = 0f;
|
|
for (int i = 1; i < points.Count; i++)
|
|
{
|
|
totalLen += Vector2.Distance(points[i - 1], points[i]);
|
|
}
|
|
if (totalLen <= 0.00001f) totalLen = 1f;
|
|
|
|
int segmentCount = points.Count - 1;
|
|
int vertexCount = points.Count * 2;
|
|
int indexCount = segmentCount * 6;
|
|
var mesh = context.Allocate(vertexCount, indexCount, tex);
|
|
|
|
var normals = new Vector2[points.Count];
|
|
for (int i = 0; i < points.Count; i++)
|
|
{
|
|
Vector2 n;
|
|
if (i == 0)
|
|
{
|
|
Vector2 d = points[1] - points[0];
|
|
n = new Vector2(-d.y, d.x);
|
|
}
|
|
else if (i == points.Count - 1)
|
|
{
|
|
Vector2 d = points[i] - points[i - 1];
|
|
n = new Vector2(-d.y, d.x);
|
|
}
|
|
else
|
|
{
|
|
Vector2 d0 = points[i] - points[i - 1];
|
|
Vector2 d1 = points[i + 1] - points[i];
|
|
Vector2 n0 = new Vector2(-d0.y, d0.x);
|
|
Vector2 n1 = new Vector2(-d1.y, d1.x);
|
|
n = n0 + n1;
|
|
}
|
|
|
|
if (n.sqrMagnitude <= 0.00001f) n = Vector2.up;
|
|
normals[i] = n.normalized;
|
|
}
|
|
|
|
float accLen = 0f;
|
|
for (int i = 0; i < points.Count; i++)
|
|
{
|
|
if (i > 0) accLen += Vector2.Distance(points[i - 1], points[i]);
|
|
float u = (accLen / totalLen) * tiling.x + uvOffset.x;
|
|
|
|
Vector2 p = points[i];
|
|
Vector2 n = normals[i] * halfW;
|
|
Vector2 a = p + n;
|
|
Vector2 b = p - n;
|
|
|
|
mesh.SetNextVertex(new Vertex { position = new Vector3(a.x, a.y, Vertex.nearZ), tint = tint, uv = new Vector2(u, tiling.y + uvOffset.y) });
|
|
mesh.SetNextVertex(new Vertex { position = new Vector3(b.x, b.y, Vertex.nearZ), tint = tint, uv = new Vector2(u, uvOffset.y) });
|
|
}
|
|
|
|
for (int i = 0; i < segmentCount; i++)
|
|
{
|
|
ushort a0 = (ushort)(i * 2);
|
|
ushort b0 = (ushort)(i * 2 + 1);
|
|
ushort a1 = (ushort)((i + 1) * 2);
|
|
ushort b1 = (ushort)((i + 1) * 2 + 1);
|
|
|
|
mesh.SetNextIndex(a0); mesh.SetNextIndex(b0); mesh.SetNextIndex(a1);
|
|
mesh.SetNextIndex(a1); mesh.SetNextIndex(b0); mesh.SetNextIndex(b1);
|
|
}
|
|
}
|
|
|
|
private void DrawScatterPoints(MeshGenerationContext context, Painter2D painter, Serie serie, PointSettings point, float width, float height)
|
|
{
|
|
if (point == null) return;
|
|
|
|
var fill = point.textureFill;
|
|
UnpackTextureFill(fill, out var pointTex, out var tiling, out var offset, out var color);
|
|
float size = point.size;
|
|
float radius = size / 2.0f;
|
|
float pixelClipX = width * _animationProgress;
|
|
|
|
if (pointTex != null)
|
|
{
|
|
if (serie.seriesData == null) return;
|
|
foreach (var p in serie.seriesData)
|
|
{
|
|
if (p == null) continue;
|
|
Vector2 pos = GetPixelPos(new Vector2(p.x, p.value), width, height);
|
|
if (pos.x > pixelClipX) continue;
|
|
|
|
DrawTexturedQuad(
|
|
context,
|
|
new Rect(pos.x - radius, pos.y - radius, radius * 2f, radius * 2f),
|
|
pointTex,
|
|
tiling,
|
|
offset,
|
|
color,
|
|
true);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
painter.fillColor = color;
|
|
if (serie.seriesData == null) return;
|
|
foreach (var dataPoint in serie.seriesData)
|
|
{
|
|
if (dataPoint == null) continue;
|
|
Vector2 pos = GetPixelPos(new Vector2(dataPoint.x, dataPoint.value), width, height);
|
|
if (pos.x <= pixelClipX)
|
|
{
|
|
painter.BeginPath();
|
|
painter.Arc(pos, radius, 0, 360);
|
|
painter.Fill();
|
|
}
|
|
}
|
|
}
|
|
|
|
private List<SeriesData> GetOrderedVisiblePoints(Serie serie)
|
|
{
|
|
_orderedVisiblePoints.Clear();
|
|
if (serie == null || serie.seriesData == null || serie.seriesData.Count == 0) return _orderedVisiblePoints;
|
|
|
|
AxisId xAxisId = (Data != null && Data.Cartesian != null) ? Data.Cartesian.xAxisId : AxisId.XBottom;
|
|
var xAxis = GetAxisConfig(xAxisId);
|
|
bool xIsCategory = xAxis != null
|
|
&& xAxis.axisType == AxisType.Category
|
|
&& xAxis.labels != null
|
|
&& xAxis.labels.Count > 0;
|
|
|
|
if (!xIsCategory)
|
|
{
|
|
_orderedVisiblePoints.AddRange(serie.seriesData);
|
|
return _orderedVisiblePoints;
|
|
}
|
|
|
|
int labelCount = xAxis.labels.Count;
|
|
bool wraps = _xMax >= labelCount;
|
|
float xMin = _xMin;
|
|
float xMax = _xMax;
|
|
|
|
int span = Mathf.RoundToInt(_xMax - _xMin + 1);
|
|
if (span < 1) span = 1;
|
|
int preloadExtra = labelCount > span ? 1 : 0;
|
|
xMax += preloadExtra;
|
|
|
|
bool effectiveWraps = wraps || xMax >= labelCount;
|
|
|
|
for (int i = 0; i < serie.seriesData.Count; i++)
|
|
{
|
|
var dp = serie.seriesData[i];
|
|
if (dp == null) continue;
|
|
|
|
float xVal = dp.x;
|
|
if (effectiveWraps && xVal < xMin)
|
|
{
|
|
xVal += labelCount;
|
|
}
|
|
|
|
if (xVal < xMin - 0.0001f) continue;
|
|
if (xVal > xMax + 0.0001f) continue;
|
|
|
|
_orderedVisiblePoints.Add(dp);
|
|
}
|
|
|
|
if (_orderedVisiblePoints.Count > 1)
|
|
{
|
|
_orderedVisiblePoints.Sort((a, b) =>
|
|
{
|
|
float ax = a != null ? a.x : 0f;
|
|
float bx = b != null ? b.x : 0f;
|
|
|
|
if (effectiveWraps)
|
|
{
|
|
if (ax < xMin) ax += labelCount;
|
|
if (bx < xMin) bx += labelCount;
|
|
}
|
|
|
|
return ax.CompareTo(bx);
|
|
});
|
|
}
|
|
|
|
return _orderedVisiblePoints;
|
|
}
|
|
|
|
private void DrawLine(Painter2D painter, Serie serie, LineSettings settings, float width, float height)
|
|
{
|
|
painter.lineWidth = settings.stroke.width;
|
|
var strokeColor = settings.stroke.color;
|
|
strokeColor.a *= 1f;
|
|
if (strokeColor.a <= 0.001f) return;
|
|
painter.strokeColor = strokeColor;
|
|
var lineType = settings.stroke.lineType;
|
|
painter.BeginPath();
|
|
|
|
var points = GetOrderedVisiblePoints(serie);
|
|
if (points == null || points.Count == 0) return;
|
|
|
|
Vector2 prevPos = Vector2.zero;
|
|
|
|
// Animation Clip X
|
|
float clipX = _xMin + (_xMax - _xMin) * _animationProgress;
|
|
// Convert to pixels (approximate, since GetPixelPos handles range)
|
|
// Better to check pixel X directly?
|
|
// GetPixelPos maps xMin to 0 and xMax to width (usually).
|
|
float pixelClipX = GetEffectivePixelClipX(width);
|
|
|
|
for (int i = 0; i < points.Count; i++)
|
|
{
|
|
var dp = points[i];
|
|
if (dp == null) continue;
|
|
Vector2 pos = GetPixelPos(new Vector2(dp.x, dp.value), width, height);
|
|
|
|
if (i == 0)
|
|
{
|
|
if (pos.x > pixelClipX)
|
|
{
|
|
// First point is already beyond clip? Just move there?
|
|
// Or don't draw anything?
|
|
// If data is sorted by X, and we animate X, we shouldn't see it.
|
|
// But if X range is large and we start mid-way?
|
|
// Let's assume sorted X for "Reveal" effect.
|
|
break;
|
|
}
|
|
painter.MoveTo(pos);
|
|
}
|
|
else
|
|
{
|
|
if (pos.x > pixelClipX)
|
|
{
|
|
// Clipping needed
|
|
// Calculate t based on X
|
|
float t = Mathf.Clamp01((pixelClipX - prevPos.x) / (pos.x - prevPos.x));
|
|
|
|
if (lineType == LineType.Step)
|
|
{
|
|
Vector2 corner = new Vector2(pos.x, prevPos.y);
|
|
if (corner.x > pixelClipX)
|
|
{
|
|
// Clip at horizontal part
|
|
painter.LineTo(new Vector2(pixelClipX, prevPos.y));
|
|
}
|
|
else
|
|
{
|
|
// Draw horizontal, then clip vertical
|
|
painter.LineTo(corner);
|
|
// Vertical part is at pos.x (which is > pixelClipX), so we shouldn't have reached here if corner.x > pixelClipX?
|
|
// Wait, corner.x == pos.x.
|
|
// If pos.x > pixelClipX, then corner.x > pixelClipX.
|
|
// So we always fall into the first block?
|
|
// Step line: (prev.x, prev.y) -> (pos.x, prev.y) -> (pos.x, pos.y)
|
|
// If pos.x > pixelClipX, the horizontal line crosses the clip boundary.
|
|
painter.LineTo(new Vector2(pixelClipX, prevPos.y));
|
|
}
|
|
}
|
|
else if (lineType == LineType.Smooth)
|
|
{
|
|
float cpDistance = (pos.x - prevPos.x) * 0.5f;
|
|
Vector2 cp1 = prevPos + new Vector2(cpDistance, 0);
|
|
Vector2 cp2 = pos - new Vector2(cpDistance, 0);
|
|
|
|
// Split Bezier at t
|
|
// De Casteljau
|
|
Vector2 q0 = Vector2.Lerp(prevPos, cp1, t);
|
|
Vector2 q1 = Vector2.Lerp(cp1, cp2, t);
|
|
Vector2 q2 = Vector2.Lerp(cp2, pos, t);
|
|
Vector2 r0 = Vector2.Lerp(q0, q1, t);
|
|
Vector2 r1 = Vector2.Lerp(q1, q2, t);
|
|
Vector2 s0 = Vector2.Lerp(r0, r1, t); // This is the point at t
|
|
|
|
painter.BezierCurveTo(q0, r0, s0);
|
|
}
|
|
else // Straight
|
|
{
|
|
Vector2 target = Vector2.Lerp(prevPos, pos, t);
|
|
painter.LineTo(target);
|
|
}
|
|
|
|
// Stop drawing after clipping
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
// Fully visible segment
|
|
if (lineType == LineType.Step)
|
|
{
|
|
painter.LineTo(new Vector2(pos.x, prevPos.y));
|
|
painter.LineTo(pos);
|
|
}
|
|
else if (lineType == LineType.Smooth)
|
|
{
|
|
float cpDistance = (pos.x - prevPos.x) * 0.5f;
|
|
Vector2 cp1 = prevPos + new Vector2(cpDistance, 0);
|
|
Vector2 cp2 = pos - new Vector2(cpDistance, 0);
|
|
painter.BezierCurveTo(cp1, cp2, pos);
|
|
}
|
|
else
|
|
{
|
|
painter.LineTo(pos);
|
|
}
|
|
}
|
|
}
|
|
prevPos = pos;
|
|
}
|
|
painter.Stroke();
|
|
}
|
|
|
|
private void DrawArea(Painter2D painter, Serie serie, LineSettings settings, float width, float height)
|
|
{
|
|
UnpackTextureFill(settings.area.textureFill, out var _areaTex, out var _areaTiling, out var _areaOffset, out var _areaColor);
|
|
|
|
_areaColor.a *= 1f;
|
|
if (_areaColor.a <= 0.001f) return;
|
|
painter.fillColor = _areaColor;
|
|
var lineType = settings.stroke.lineType;
|
|
painter.BeginPath();
|
|
|
|
var points = GetOrderedVisiblePoints(serie);
|
|
if (points == null || points.Count < 2) return;
|
|
|
|
var firstDp = points[0];
|
|
if (firstDp == null) return;
|
|
|
|
Vector2 firstPos = GetPixelPos(new Vector2(firstDp.x, firstDp.value), width, height);
|
|
float pixelClipX = GetEffectivePixelClipX(width);
|
|
|
|
if (firstPos.x > pixelClipX) return;
|
|
|
|
float bottomY = height;
|
|
|
|
painter.MoveTo(new Vector2(firstPos.x, bottomY));
|
|
painter.LineTo(firstPos);
|
|
|
|
Vector2 prevPos = firstPos;
|
|
Vector2 lastDrawnPos = firstPos;
|
|
|
|
for (int i = 1; i < points.Count; i++)
|
|
{
|
|
var dp = points[i];
|
|
if (dp == null) continue;
|
|
Vector2 pos = GetPixelPos(new Vector2(dp.x, dp.value), width, height);
|
|
|
|
if (pos.x > pixelClipX)
|
|
{
|
|
// Clip
|
|
float t = Mathf.Clamp01((pixelClipX - prevPos.x) / (pos.x - prevPos.x));
|
|
|
|
if (lineType == LineType.Step)
|
|
{
|
|
painter.LineTo(new Vector2(pixelClipX, prevPos.y));
|
|
lastDrawnPos = new Vector2(pixelClipX, prevPos.y);
|
|
}
|
|
else if (lineType == LineType.Smooth)
|
|
{
|
|
float cpDistance = (pos.x - prevPos.x) * 0.5f;
|
|
Vector2 cp1 = prevPos + new Vector2(cpDistance, 0);
|
|
Vector2 cp2 = pos - new Vector2(cpDistance, 0);
|
|
|
|
Vector2 q0 = Vector2.Lerp(prevPos, cp1, t);
|
|
Vector2 q1 = Vector2.Lerp(cp1, cp2, t);
|
|
Vector2 q2 = Vector2.Lerp(cp2, pos, t);
|
|
Vector2 r0 = Vector2.Lerp(q0, q1, t);
|
|
Vector2 r1 = Vector2.Lerp(q1, q2, t);
|
|
Vector2 s0 = Vector2.Lerp(r0, r1, t);
|
|
|
|
painter.BezierCurveTo(q0, r0, s0);
|
|
lastDrawnPos = s0;
|
|
}
|
|
else
|
|
{
|
|
Vector2 target = Vector2.Lerp(prevPos, pos, t);
|
|
painter.LineTo(target);
|
|
lastDrawnPos = target;
|
|
}
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
if (lineType == LineType.Step)
|
|
{
|
|
painter.LineTo(new Vector2(pos.x, prevPos.y));
|
|
painter.LineTo(pos);
|
|
}
|
|
else if (lineType == LineType.Smooth)
|
|
{
|
|
float cpDistance = (pos.x - prevPos.x) * 0.5f;
|
|
Vector2 cp1 = prevPos + new Vector2(cpDistance, 0);
|
|
Vector2 cp2 = pos - new Vector2(cpDistance, 0);
|
|
painter.BezierCurveTo(cp1, cp2, pos);
|
|
}
|
|
else
|
|
{
|
|
painter.LineTo(pos);
|
|
}
|
|
lastDrawnPos = pos;
|
|
}
|
|
|
|
prevPos = pos;
|
|
}
|
|
|
|
painter.LineTo(new Vector2(lastDrawnPos.x, bottomY));
|
|
painter.ClosePath();
|
|
painter.Fill();
|
|
}
|
|
|
|
private void DrawAreaWithTexture(MeshGenerationContext context, Serie serie, LineSettings settings, float width, float height)
|
|
{
|
|
var areaFill = settings.area.textureFill;
|
|
UnpackTextureFill(areaFill, out var areaTex, out var areaTiling, out var areaOffset, out var areaColor);
|
|
if (areaTex == null)
|
|
{
|
|
DrawArea(context.painter2D, serie, settings, width, height);
|
|
return;
|
|
}
|
|
|
|
areaColor.a *= 1f;
|
|
if (areaColor.a <= 0.001f) return;
|
|
|
|
// Simple clipping for textured area: just limit vertices?
|
|
// Textured mesh generation is complex to clip perfectly without re-tessellating.
|
|
// But we can just limit the number of vertices we generate.
|
|
|
|
float pixelClipX = GetEffectivePixelClipX(width);
|
|
|
|
// ... (Rest of Texture logic, but break if pos.x > pixelClipX)
|
|
// Re-implementing lightly to include clip check:
|
|
|
|
var points = GetOrderedVisiblePoints(serie);
|
|
if (points == null || points.Count < 2) return;
|
|
|
|
List<Vector2> topVertices = new List<Vector2>();
|
|
|
|
// Generate full path then clip? Or clip during generation.
|
|
// Clip during generation is better.
|
|
|
|
// Helper to add vertex if within bounds, or add clipped and stop
|
|
bool AddClipped(Vector2 p)
|
|
{
|
|
if (p.x <= pixelClipX)
|
|
{
|
|
topVertices.Add(p);
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
// Calculate intersection with last point
|
|
if (topVertices.Count > 0)
|
|
{
|
|
Vector2 last = topVertices[topVertices.Count - 1];
|
|
if (Mathf.Abs(p.x - last.x) > 0.001f)
|
|
{
|
|
float t = (pixelClipX - last.x) / (p.x - last.x);
|
|
Vector2 clipped = Vector2.Lerp(last, p, t);
|
|
topVertices.Add(clipped);
|
|
}
|
|
}
|
|
return false; // Stop
|
|
}
|
|
}
|
|
|
|
if (settings.stroke.lineType == LineType.Step)
|
|
{
|
|
for (int i = 0; i < points.Count; i++)
|
|
{
|
|
var dp = points[i];
|
|
if (dp == null) continue;
|
|
Vector2 pos = GetPixelPos(new Vector2(dp.x, dp.value), width, height);
|
|
if (i > 0)
|
|
{
|
|
var prevDp = points[i - 1];
|
|
if (prevDp == null) continue;
|
|
Vector2 prevPos = GetPixelPos(new Vector2(prevDp.x, prevDp.value), width, height);
|
|
if (!AddClipped(new Vector2(pos.x, prevPos.y))) break;
|
|
}
|
|
if (!AddClipped(pos)) break;
|
|
}
|
|
}
|
|
else if (settings.stroke.lineType == LineType.Smooth)
|
|
{
|
|
if (points.Count >= 2)
|
|
{
|
|
var first = points[0];
|
|
if (first == null) return;
|
|
if (!AddClipped(GetPixelPos(new Vector2(first.x, first.value), width, height))) return;
|
|
|
|
for (int i = 0; i < points.Count - 1; i++)
|
|
{
|
|
var dp0 = points[i];
|
|
var dp1 = points[i + 1];
|
|
if (dp0 == null || dp1 == null) continue;
|
|
Vector2 p0 = GetPixelPos(new Vector2(dp0.x, dp0.value), width, height);
|
|
Vector2 p1 = GetPixelPos(new Vector2(dp1.x, dp1.value), width, height);
|
|
|
|
if (p0.x > pixelClipX) break;
|
|
|
|
float cpDistance = (p1.x - p0.x) * 0.5f;
|
|
Vector2 cp1 = p0 + new Vector2(cpDistance, 0);
|
|
Vector2 cp2 = p1 - new Vector2(cpDistance, 0);
|
|
|
|
int samples = 10;
|
|
bool finished = false;
|
|
for (int t = 1; t <= samples; t++)
|
|
{
|
|
float u = (float)t / samples;
|
|
Vector2 curvePoint = CubicBezier(p0, cp1, cp2, p1, u);
|
|
if (!AddClipped(curvePoint)) { finished = true; break; }
|
|
}
|
|
if (finished) break;
|
|
}
|
|
}
|
|
}
|
|
else // Straight
|
|
{
|
|
foreach(var p in points)
|
|
{
|
|
if (p == null) continue;
|
|
if (!AddClipped(GetPixelPos(new Vector2(p.x, p.value), width, height))) break;
|
|
}
|
|
}
|
|
|
|
// Mesh Generation (Same as before, just using clipped topVertices)
|
|
if (topVertices.Count < 2) return;
|
|
|
|
DrawTexturedVerticalStrip(
|
|
context,
|
|
topVertices,
|
|
height,
|
|
areaTex,
|
|
areaTiling,
|
|
areaOffset,
|
|
areaColor,
|
|
true);
|
|
}
|
|
|
|
private Vector2 CubicBezier(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, float t)
|
|
{
|
|
float u = 1 - t;
|
|
float tt = t * t;
|
|
float uu = u * u;
|
|
float uuu = uu * u;
|
|
float ttt = tt * t;
|
|
|
|
Vector2 p = uuu * p0;
|
|
p += 3 * uu * t * p1;
|
|
p += 3 * u * tt * p2;
|
|
p += ttt * p3;
|
|
|
|
return p;
|
|
}
|
|
|
|
private void DrawPoints(MeshGenerationContext context, Painter2D painter, int serieIndex, Serie serie, LineSettings settings, float width, float height)
|
|
{
|
|
var point = settings.point;
|
|
float pixelClipX = GetEffectivePixelClipX(width);
|
|
|
|
if (serie.seriesData == null) return;
|
|
bool hoverEnabled = settings.hover != null && settings.hover.enabled;
|
|
bool overridePoint = hoverEnabled && settings.hover.point != null;
|
|
var hoverPoint = overridePoint ? settings.hover.point : null;
|
|
|
|
for (int pi = 0; pi < serie.seriesData.Count; pi++)
|
|
{
|
|
var dataPoint = serie.seriesData[pi];
|
|
if (dataPoint == null) continue;
|
|
Vector2 pos = GetPixelPos(new Vector2(dataPoint.x, dataPoint.value), width, height);
|
|
if (pos.x > pixelClipX) continue;
|
|
|
|
float a = hoverEnabled ? GetHoverAlpha(serieIndex, pi) : 0f;
|
|
|
|
if (overridePoint)
|
|
{
|
|
bool baseShow = point != null && point.show;
|
|
bool hoverShow = hoverPoint != null && hoverPoint.show;
|
|
if (!baseShow && (!hoverShow || a <= 0.001f)) continue;
|
|
|
|
float baseRadius = baseShow ? point.size * 0.5f : 0f;
|
|
float targetRadius = hoverShow ? hoverPoint.size * 0.5f : 0f;
|
|
float radius = Mathf.Lerp(baseRadius, targetRadius, a);
|
|
|
|
if (baseShow && (1f - a) > 0.001f)
|
|
{
|
|
DrawPointMarkerWithAlpha(context, painter, pos, radius, point.textureFill, Color.white, (1f - a), Vector2.zero);
|
|
}
|
|
|
|
if (hoverShow && a > 0.001f)
|
|
{
|
|
DrawPointMarkerWithAlpha(context, painter, pos, radius, hoverPoint.textureFill, Color.white, a, Vector2.zero);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (point == null || !point.show) continue;
|
|
float radius = point.size * 0.5f;
|
|
DrawPointMarkerWithAlpha(context, painter, pos, radius, point.textureFill, Color.white, 1f, Vector2.zero);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |