1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 21:27:24 +08:00

Merge pull request #22143 from ItsShamed/ui/segmented-graph

Implement a segmented graph
This commit is contained in:
Dean Herbert 2023-01-12 19:49:05 +09:00 committed by GitHub
commit b3d4da8fc9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 491 additions and 0 deletions

View File

@ -0,0 +1,154 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osuTK;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneSegmentedGraph : OsuTestScene
{
private readonly SegmentedGraph<int> graph;
public TestSceneSegmentedGraph()
{
Children = new Drawable[]
{
graph = new SegmentedGraph<int>(6)
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(1, 0.5f),
},
};
graph.TierColours = new[]
{
Colour4.Red,
Colour4.OrangeRed,
Colour4.Orange,
Colour4.Yellow,
Colour4.YellowGreen,
Colour4.Green
};
AddStep("values from 1-10", () => graph.Values = Enumerable.Range(1, 10).ToArray());
AddStep("values from 1-100", () => graph.Values = Enumerable.Range(1, 100).ToArray());
AddStep("values from 1-500", () => graph.Values = Enumerable.Range(1, 500).ToArray());
AddStep("sin() function of size 100", () => sinFunction());
AddStep("sin() function of size 500", () => sinFunction(500));
AddStep("bumps of size 100", () => bumps());
AddStep("bumps of size 500", () => bumps(500));
AddStep("100 random values", () => randomValues());
AddStep("500 random values", () => randomValues(500));
AddStep("beatmap density with granularity of 200", () => beatmapDensity());
AddStep("beatmap density with granularity of 300", () => beatmapDensity(300));
AddStep("reversed values from 1-10", () => graph.Values = Enumerable.Range(1, 10).Reverse().ToArray());
AddStep("change colour", () =>
{
graph.TierColours = new[]
{
Colour4.White,
Colour4.LightBlue,
Colour4.Aqua,
Colour4.Blue
};
});
AddStep("reset colour", () =>
{
graph.TierColours = new[]
{
Colour4.Red,
Colour4.OrangeRed,
Colour4.Orange,
Colour4.Yellow,
Colour4.YellowGreen,
Colour4.Green
};
});
}
private void sinFunction(int size = 100)
{
const int max_value = 255;
graph.Values = new int[size];
float step = 2 * MathF.PI / size;
float x = 0;
for (int i = 0; i < size; i++)
{
graph.Values[i] = (int)(max_value * MathF.Sin(x));
x += step;
}
}
private void bumps(int size = 100)
{
const int max_value = 255;
graph.Values = new int[size];
float step = 2 * MathF.PI / size;
float x = 0;
for (int i = 0; i < size; i++)
{
graph.Values[i] = (int)(max_value * Math.Abs(MathF.Sin(x)));
x += step;
}
}
private void randomValues(int size = 100)
{
Random rng = new Random();
graph.Values = new int[size];
for (int i = 0; i < size; i++)
{
graph.Values[i] = rng.Next(255);
}
}
private void beatmapDensity(int granularity = 200)
{
var ruleset = new OsuRuleset();
var beatmap = CreateBeatmap(ruleset.RulesetInfo);
IEnumerable<HitObject> objects = beatmap.HitObjects;
// Taken from SongProgressGraph
graph.Values = new int[granularity];
if (!objects.Any())
return;
double firstHit = objects.First().StartTime;
double lastHit = objects.Max(o => o.GetEndTime());
if (lastHit == 0)
lastHit = objects.Last().StartTime;
double interval = (lastHit - firstHit + 1) / granularity;
foreach (var h in objects)
{
double endTime = h.GetEndTime();
Debug.Assert(endTime >= h.StartTime);
int startRange = (int)((h.StartTime - firstHit) / interval);
int endRange = (int)((endTime - firstHit) / interval);
for (int i = startRange; i <= endRange; i++)
graph.Values[i]++;
}
}
}
}

View File

@ -0,0 +1,337 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Textures;
using osuTK;
namespace osu.Game.Graphics.UserInterface
{
public partial class SegmentedGraph<T> : Drawable
where T : struct, IComparable<T>, IConvertible, IEquatable<T>
{
private bool graphNeedsUpdate;
private T[]? values;
private int[] tiers = Array.Empty<int>();
private readonly SegmentManager segments;
private int tierCount;
public SegmentedGraph(int tierCount = 1)
{
this.tierCount = tierCount;
tierColours = new[]
{
new Colour4(0, 0, 0, 0)
};
segments = new SegmentManager(tierCount);
}
public T[] Values
{
get => values ?? Array.Empty<T>();
set
{
if (value == values) return;
values = value;
graphNeedsUpdate = true;
}
}
private Colour4[] tierColours;
public Colour4[] TierColours
{
get => tierColours;
set
{
if (value.Length == 0 || value == tierColours)
return;
tierCount = value.Length;
tierColours = value;
graphNeedsUpdate = true;
}
}
private Texture texture = null!;
private IShader shader = null!;
[BackgroundDependencyLoader]
private void load(IRenderer renderer, ShaderManager shaders)
{
texture = renderer.WhitePixel;
shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE);
}
protected override void Update()
{
base.Update();
if (graphNeedsUpdate)
{
recalculateTiers(values);
recalculateSegments();
Invalidate(Invalidation.DrawNode);
graphNeedsUpdate = false;
}
}
private void recalculateTiers(T[]? arr)
{
if (arr == null || arr.Length == 0)
{
tiers = Array.Empty<int>();
return;
}
float[] floatValues = arr.Select(v => Convert.ToSingle(v)).ToArray();
// Shift values to eliminate negative ones
float min = floatValues.Min();
if (min < 0)
{
for (int i = 0; i < floatValues.Length; i++)
floatValues[i] += Math.Abs(min);
}
// Normalize values
float max = floatValues.Max();
for (int i = 0; i < floatValues.Length; i++)
floatValues[i] /= max;
// Deduce tiers from values
tiers = floatValues.Select(v => (int)Math.Floor(v * tierCount)).ToArray();
}
private void recalculateSegments()
{
segments.Clear();
if (tiers.Length == 0)
{
segments.Add(0, 0, 1);
return;
}
for (int i = 0; i < tiers.Length; i++)
{
for (int tier = 0; tier < tierCount; tier++)
{
if (tier < 0)
continue;
// One tier covers itself and all tiers above it.
// By layering multiple transparent boxes, higher tiers will be brighter.
// If using opaque colors, higher tiers will be on front, covering lower tiers.
if (tiers[i] >= tier)
{
if (!segments.IsTierStarted(tier))
segments.StartSegment(tier, i * 1f / tiers.Length);
}
else
{
if (segments.IsTierStarted(tier))
segments.EndSegment(tier, i * 1f / tiers.Length);
}
}
}
segments.EndAllPendingSegments();
segments.Sort();
}
private Colour4 getTierColour(int tier) => tier >= 0 ? tierColours[tier] : new Colour4(0, 0, 0, 0);
protected override DrawNode CreateDrawNode() => new SegmentedGraphDrawNode(this);
protected struct SegmentInfo
{
/// <summary>
/// The tier this segment is at.
/// </summary>
public int Tier;
/// <summary>
/// The progress at which this segment starts.
/// </summary>
/// <remarks>
/// The value is a normalized float (from 0 to 1).
/// </remarks>
public float Start;
/// <summary>
/// The progress at which this segment ends.
/// </summary>
/// <remarks>
/// The value is a normalized float (from 0 to 1).
/// </remarks>
public float End;
/// <summary>
/// The length of this segment.
/// </summary>
/// <remarks>
/// The value is a normalized float (from 0 to 1).
/// </remarks>
public float Length => End - Start;
public override string ToString()
{
return $"({Tier}, {Start * 100}%, {End * 100}%)";
}
}
private class SegmentedGraphDrawNode : DrawNode
{
public new SegmentedGraph<T> Source => (SegmentedGraph<T>)base.Source;
private Texture texture = null!;
private IShader shader = null!;
private readonly List<SegmentInfo> segments = new List<SegmentInfo>();
private Vector2 drawSize;
public SegmentedGraphDrawNode(SegmentedGraph<T> source)
: base(source)
{
}
public override void ApplyState()
{
base.ApplyState();
texture = Source.texture;
shader = Source.shader;
drawSize = Source.DrawSize;
segments.Clear();
segments.AddRange(Source.segments.Where(s => s.Length * drawSize.X > 1));
}
public override void Draw(IRenderer renderer)
{
base.Draw(renderer);
shader.Bind();
foreach (SegmentInfo segment in segments)
{
Vector2 topLeft = new Vector2(segment.Start * drawSize.X, 0);
Vector2 topRight = new Vector2(segment.End * drawSize.X, 0);
Vector2 bottomLeft = new Vector2(segment.Start * drawSize.X, drawSize.Y);
Vector2 bottomRight = new Vector2(segment.End * drawSize.X, drawSize.Y);
renderer.DrawQuad(
texture,
new Quad(
Vector2Extensions.Transform(topLeft, DrawInfo.Matrix),
Vector2Extensions.Transform(topRight, DrawInfo.Matrix),
Vector2Extensions.Transform(bottomLeft, DrawInfo.Matrix),
Vector2Extensions.Transform(bottomRight, DrawInfo.Matrix)),
Source.getTierColour(segment.Tier));
}
shader.Unbind();
}
}
protected class SegmentManager : IEnumerable<SegmentInfo>
{
private readonly List<SegmentInfo> segments = new List<SegmentInfo>();
private readonly SegmentInfo?[] pendingSegments;
public SegmentManager(int tierCount)
{
pendingSegments = new SegmentInfo?[tierCount];
}
public void StartSegment(int tier, float start)
{
if (pendingSegments[tier] != null)
throw new InvalidOperationException($"Another {nameof(SegmentInfo)} of tier {tier.ToString()} has already been started.");
pendingSegments[tier] = new SegmentInfo
{
Tier = tier,
Start = Math.Clamp(start, 0, 1)
};
}
public void EndSegment(int tier, float end)
{
SegmentInfo? pendingSegment = pendingSegments[tier];
if (pendingSegment == null)
throw new InvalidOperationException($"Cannot end {nameof(SegmentInfo)} of tier {tier.ToString()} that has not been started.");
SegmentInfo segment = pendingSegment.Value;
segment.End = Math.Clamp(end, 0, 1);
segments.Add(segment);
pendingSegments[tier] = null;
}
public void EndAllPendingSegments()
{
foreach (SegmentInfo? pendingSegment in pendingSegments)
{
if (pendingSegment == null)
continue;
SegmentInfo finalizedSegment = pendingSegment.Value;
finalizedSegment.End = 1;
segments.Add(finalizedSegment);
}
}
public void Sort() =>
segments.Sort((a, b) =>
a.Tier != b.Tier
? a.Tier.CompareTo(b.Tier)
: a.Start.CompareTo(b.Start));
public void Add(SegmentInfo segment) => segments.Add(segment);
public void Clear()
{
segments.Clear();
for (int i = 0; i < pendingSegments.Length; i++)
pendingSegments[i] = null;
}
public int Count => segments.Count;
public void Add(int tier, float start, float end)
{
SegmentInfo segment = new SegmentInfo
{
Tier = tier,
Start = Math.Clamp(start, 0, 1),
End = Math.Clamp(end, 0, 1)
};
if (segment.Start > segment.End)
throw new InvalidOperationException("Segment start cannot be after segment end.");
Add(segment);
}
public bool IsTierStarted(int tier) => tier >= 0 && pendingSegments[tier].HasValue;
public IEnumerator<SegmentInfo> GetEnumerator() => segments.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
}