// Copyright (c) ppy Pty Ltd . 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.Colour; 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 : Drawable where T : struct, IComparable, IConvertible, IEquatable { private bool graphNeedsUpdate; private T[]? values; private int[] tiers = Array.Empty(); 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(); set { if (value == values) return; values = value; graphNeedsUpdate = true; } } private IReadOnlyList tierColours; public IReadOnlyList TierColours { get => tierColours; set { tierCount = value.Count; 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(); 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(); } protected override DrawNode CreateDrawNode() => new SegmentedGraphDrawNode(this); protected struct SegmentInfo { /// /// The tier this segment is at. /// public int Tier; /// /// The progress at which this segment starts. /// /// /// The value is a normalized float (from 0 to 1). /// public float Start; /// /// The progress at which this segment ends. /// /// /// The value is a normalized float (from 0 to 1). /// public float End; /// /// The length of this segment. /// /// /// The value is a normalized float (from 0 to 1). /// public float Length => End - Start; public override string ToString() { return $"({Tier}, {Start * 100}%, {End * 100}%)"; } } private class SegmentedGraphDrawNode : DrawNode { public new SegmentedGraph Source => (SegmentedGraph)base.Source; private Texture texture = null!; private IShader shader = null!; private readonly List segments = new List(); private Vector2 drawSize; private readonly List tierColours = new List(); public SegmentedGraphDrawNode(SegmentedGraph 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)); tierColours.Clear(); tierColours.AddRange(Source.tierColours); } 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)), getSegmentColour(segment)); } shader.Unbind(); } private ColourInfo getSegmentColour(SegmentInfo segment) { var segmentColour = new ColourInfo { TopLeft = DrawColourInfo.Colour.Interpolate(new Vector2(segment.Start, 0f)), TopRight = DrawColourInfo.Colour.Interpolate(new Vector2(segment.End, 0f)), BottomLeft = DrawColourInfo.Colour.Interpolate(new Vector2(segment.Start, 1f)), BottomRight = DrawColourInfo.Colour.Interpolate(new Vector2(segment.End, 1f)) }; var tierColour = segment.Tier >= 0 ? tierColours[segment.Tier] : new Colour4(0, 0, 0, 0); segmentColour.ApplyChild(tierColour); return segmentColour; } } protected class SegmentManager : IEnumerable { private readonly List segments = new List(); 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 GetEnumerator() => segments.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } } }