Maze / Library /PackageCache /com.unity.inputsystem@1.6.1 /Samples~ /Visualizers /VisualizationHelpers.cs
| using System; | |
| using UnityEngine.InputSystem.Utilities; | |
| using Unity.Collections.LowLevel.Unsafe; | |
| using UnityEngine.InputSystem.LowLevel; | |
| ////REVIEW: for vector2 visualizers of sticks, it could be useful to also visualize deadzones and raw values | |
| namespace UnityEngine.InputSystem.Samples | |
| { | |
| internal static class VisualizationHelpers | |
| { | |
| public enum Axis { X, Y, Z } | |
| public abstract class Visualizer | |
| { | |
| public abstract void OnDraw(Rect rect); | |
| public abstract void AddSample(object value, double time); | |
| } | |
| public abstract class ValueVisualizer<TValue> : Visualizer | |
| where TValue : struct | |
| { | |
| public RingBuffer<TValue> samples; | |
| public RingBuffer<GUIContent> samplesText; | |
| protected ValueVisualizer(int numSamples = 10) | |
| { | |
| samples = new RingBuffer<TValue>(numSamples); | |
| samplesText = new RingBuffer<GUIContent>(numSamples); | |
| } | |
| public override void AddSample(object value, double time) | |
| { | |
| var v = default(TValue); | |
| if (value != null) | |
| { | |
| if (!(value is TValue val)) | |
| throw new ArgumentException( | |
| $"Expecting value of type '{typeof(TValue).Name}' but value of type '{value?.GetType().Name}' instead", | |
| nameof(value)); | |
| v = val; | |
| } | |
| samples.Append(v); | |
| samplesText.Append(new GUIContent(v.ToString())); | |
| } | |
| } | |
| // Visualizes integer and real type primitives. | |
| public class ScalarVisualizer<TValue> : ValueVisualizer<TValue> | |
| where TValue : struct | |
| { | |
| public TValue limitMin; | |
| public TValue limitMax; | |
| public TValue min; | |
| public TValue max; | |
| public ScalarVisualizer(int numSamples = 10) | |
| : base(numSamples) | |
| { | |
| } | |
| public override void OnDraw(Rect rect) | |
| { | |
| // For now, only draw the current value. | |
| DrawRectangle(rect, new Color(1, 1, 1, 0.1f)); | |
| if (samples.count == 0) | |
| return; | |
| var sample = samples[samples.count - 1]; | |
| if (Compare(sample, default) == 0) | |
| return; | |
| if (Compare(limitMin, default) != 0) | |
| { | |
| // Two-way visualization with positive and negative side. | |
| throw new NotImplementedException(); | |
| } | |
| else | |
| { | |
| // One-way visualization with only positive side. | |
| var ratio = Divide(sample, limitMax); | |
| var fillRect = rect; | |
| fillRect.width = rect.width * ratio; | |
| DrawRectangle(fillRect, new Color(0, 1, 0, 0.75f)); | |
| var valuePos = new Vector2(fillRect.xMax, fillRect.y + fillRect.height / 2); | |
| DrawText(samplesText[samples.count - 1], valuePos, ValueTextStyle); | |
| } | |
| } | |
| public override void AddSample(object value, double time) | |
| { | |
| base.AddSample(value, time); | |
| if (value != null) | |
| { | |
| var val = (TValue)value; | |
| if (Compare(min, val) > 0) | |
| min = val; | |
| if (Compare(max, val) < 0) | |
| max = val; | |
| } | |
| } | |
| private static unsafe int Compare(TValue left, TValue right) | |
| { | |
| var leftPtr = UnsafeUtility.AddressOf(ref left); | |
| var rightPtr = UnsafeUtility.AddressOf(ref right); | |
| if (typeof(TValue) == typeof(int)) | |
| return ((int*)leftPtr)->CompareTo(*(int*)rightPtr); | |
| if (typeof(TValue) == typeof(float)) | |
| return ((float*)leftPtr)->CompareTo(*(float*)rightPtr); | |
| throw new NotImplementedException("Scalar value type: " + typeof(TValue).Name); | |
| } | |
| private static unsafe void Subtract(ref TValue left, TValue right) | |
| { | |
| var leftPtr = UnsafeUtility.AddressOf(ref left); | |
| var rightPtr = UnsafeUtility.AddressOf(ref right); | |
| if (typeof(TValue) == typeof(int)) | |
| *(int*)leftPtr = *(int*)leftPtr - *(int*)rightPtr; | |
| if (typeof(TValue) == typeof(float)) | |
| *(float*)leftPtr = *(float*)leftPtr - *(float*)rightPtr; | |
| throw new NotImplementedException("Scalar value type: " + typeof(TValue).Name); | |
| } | |
| private static unsafe float Divide(TValue left, TValue right) | |
| { | |
| var leftPtr = UnsafeUtility.AddressOf(ref left); | |
| var rightPtr = UnsafeUtility.AddressOf(ref right); | |
| if (typeof(TValue) == typeof(int)) | |
| return (float)*(int*)leftPtr / *(int*)rightPtr; | |
| if (typeof(TValue) == typeof(float)) | |
| return *(float*)leftPtr / *(float*)rightPtr; | |
| throw new NotImplementedException("Scalar value type: " + typeof(TValue).Name); | |
| } | |
| } | |
| // Visualizes .current device value | |
| public class CurrentDeviceVisualizer : Visualizer | |
| { | |
| private InputDevice m_CurrentDevice = null; | |
| public override void OnDraw(Rect rect) | |
| { | |
| // For now, only draw the current value. | |
| DrawRectangle(rect, new Color(1, 1, 1, 0.1f)); | |
| var name = m_CurrentDevice != null ? m_CurrentDevice.name : "null"; | |
| DrawText(name, new Vector2(rect.xMin + 4, (rect.yMin + rect.yMax) / 2.0f), ValueTextStyle); | |
| } | |
| public override void AddSample(object value, double time) | |
| { | |
| var device = (InputDevice)value; | |
| if (device is Gamepad) | |
| m_CurrentDevice = Gamepad.current; | |
| else if (device is Mouse) | |
| m_CurrentDevice = Mouse.current; | |
| else if (device is Pen) | |
| m_CurrentDevice = Pen.current; | |
| else if (device is Pointer) // should be last, because it's a base class for Mouse and Pen | |
| m_CurrentDevice = Pointer.current; | |
| else | |
| throw new ArgumentException( | |
| $"Expected device type that implements .current, but got '{device.name}' (deviceId: {device.deviceId}) instead "); | |
| } | |
| } | |
| ////TODO: allow asymmetric center (i.e. center not being a midpoint of rectangle) | |
| ////TODO: enforce proper proportion between X and Y; it's confusing that X and Y can have different units yet have the same length | |
| public class Vector2Visualizer : ValueVisualizer<Vector2> | |
| { | |
| // Our value space extends radially from the center, i.e. we have | |
| // 360 discrete directions. Sampling at that granularity doesn't work | |
| // super well in visualizations so we quantize to 3 degree increments. | |
| public Vector2[] maximums = new Vector2[360 / 3]; | |
| public Vector2 limits = new Vector2(1, 1); | |
| private GUIContent limitsXText; | |
| private GUIContent limitsYText; | |
| public Vector2Visualizer(int numSamples = 10) | |
| : base(numSamples) | |
| { | |
| } | |
| public override void AddSample(object value, double time) | |
| { | |
| base.AddSample(value, time); | |
| if (value != null) | |
| { | |
| // Keep track of radial maximums. | |
| var vector = (Vector2)value; | |
| var angle = Vector2.SignedAngle(Vector2.right, vector); | |
| if (angle < 0) | |
| angle = 360 + angle; | |
| var angleInt = Mathf.FloorToInt(angle) / 3; | |
| if (vector.sqrMagnitude > maximums[angleInt].sqrMagnitude) | |
| maximums[angleInt] = vector; | |
| // Extend limits if value is out of range. | |
| var limitX = Mathf.Max(Mathf.Abs(vector.x), limits.x); | |
| var limitY = Mathf.Max(Mathf.Abs(vector.y), limits.y); | |
| if (!Mathf.Approximately(limitX, limits.x)) | |
| { | |
| limits.x = limitX; | |
| limitsXText = null; | |
| } | |
| if (!Mathf.Approximately(limitY, limits.y)) | |
| { | |
| limits.y = limitY; | |
| limitsYText = null; | |
| } | |
| } | |
| } | |
| public override void OnDraw(Rect rect) | |
| { | |
| DrawRectangle(rect, new Color(1, 1, 1, 0.1f)); | |
| DrawAxis(Axis.X, rect, new Color(0, 1, 0, 0.75f)); | |
| DrawAxis(Axis.Y, rect, new Color(0, 1, 0, 0.75f)); | |
| var sampleCount = samples.count; | |
| if (sampleCount == 0) | |
| return; | |
| // If limits aren't (1,1), show the actual values. | |
| if (limits != new Vector2(1, 1)) | |
| { | |
| if (limitsXText == null) | |
| limitsXText = new GUIContent(limits.x.ToString()); | |
| if (limitsYText == null) | |
| limitsYText = new GUIContent(limits.y.ToString()); | |
| var limitsXSize = ValueTextStyle.CalcSize(limitsXText); | |
| var limitsXPos = new Vector2(rect.x - limitsXSize.x, rect.y - 5); | |
| var limitsYPos = new Vector2(rect.xMax, rect.yMax); | |
| DrawText(limitsXText, limitsXPos, ValueTextStyle); | |
| DrawText(limitsYText, limitsYPos, ValueTextStyle); | |
| } | |
| // Draw maximums. | |
| var numMaximums = 0; | |
| var firstMaximumPos = default(Vector2); | |
| var lastMaximumPos = default(Vector2); | |
| for (var i = 0; i < 360 / 3; ++i) | |
| { | |
| var value = maximums[i]; | |
| if (value == default) | |
| continue; | |
| var valuePos = PixelPosForValue(value, rect); | |
| if (numMaximums > 0) | |
| DrawLine(lastMaximumPos, valuePos, new Color(1, 1, 1, 0.25f)); | |
| else | |
| firstMaximumPos = valuePos; | |
| lastMaximumPos = valuePos; | |
| ++numMaximums; | |
| } | |
| if (numMaximums > 1) | |
| DrawLine(lastMaximumPos, firstMaximumPos, new Color(1, 1, 1, 0.25f)); | |
| // Draw samples. | |
| var alphaStep = 1f / sampleCount; | |
| var alpha = 1f; | |
| for (var i = sampleCount - 1; i >= 0; --i) // Go newest to oldest. | |
| { | |
| var value = samples[i]; | |
| var valueRect = RectForValue(value, rect); | |
| DrawRectangle(valueRect, new Color(1, 0, 0, alpha)); | |
| alpha -= alphaStep; | |
| } | |
| // Print value of most recent sample. Draw last so | |
| // we draw over the other stuff. | |
| var lastSample = samples[sampleCount - 1]; | |
| var lastSamplePos = PixelPosForValue(lastSample, rect); | |
| lastSamplePos.x += 3; | |
| lastSamplePos.y += 3; | |
| DrawText(samplesText[sampleCount - 1], lastSamplePos, ValueTextStyle); | |
| } | |
| private Rect RectForValue(Vector2 value, Rect rect) | |
| { | |
| var pos = PixelPosForValue(value, rect); | |
| return new Rect(pos.x - 1, pos.y - 1, 2, 2); | |
| } | |
| private Vector2 PixelPosForValue(Vector2 value, Rect rect) | |
| { | |
| var center = rect.center; | |
| var x = Mathf.Abs(value.x) / limits.x * Mathf.Sign(value.x); | |
| var y = Mathf.Abs(value.y) / limits.y * Mathf.Sign(value.y) * -1; // GUI Y is upside down. | |
| var xInPixels = x * rect.width / 2; | |
| var yInPixels = y * rect.height / 2; | |
| return new Vector2(center.x + xInPixels, | |
| center.y + yInPixels); | |
| } | |
| } | |
| // Y axis is time, X axis can be multiple visualizations. | |
| public class TimelineVisualizer : Visualizer | |
| { | |
| public bool showLegend { get; set; } | |
| public bool showLimits { get; set; } | |
| public TimeUnit timeUnit { get; set; } = TimeUnit.Seconds; | |
| public GUIContent valueUnit { get; set; } | |
| ////REVIEW: should this be per timeline? | |
| public int timelineCount => m_Timelines != null ? m_Timelines.Length : 0; | |
| public int historyDepth { get; set; } = 100; | |
| public Vector2 limitsY | |
| { | |
| get => m_LimitsY; | |
| set | |
| { | |
| m_LimitsY = value; | |
| m_LimitsYMin = null; | |
| m_LimitsYMax = null; | |
| } | |
| } | |
| public TimelineVisualizer(float totalTimeUnitsShown = 4) | |
| { | |
| m_TotalTimeUnitsShown = totalTimeUnitsShown; | |
| } | |
| public override void OnDraw(Rect rect) | |
| { | |
| var endTime = Time.realtimeSinceStartup; | |
| var startTime = endTime - m_TotalTimeUnitsShown; | |
| var endFrame = InputState.updateCount; | |
| var startFrame = endFrame - (int)m_TotalTimeUnitsShown; | |
| for (var i = 0; i < timelineCount; ++i) | |
| { | |
| var timeline = m_Timelines[i]; | |
| var sampleCount = timeUnit == TimeUnit.Frames | |
| ? timeline.frameSamples.count | |
| : timeline.timeSamples.count; | |
| // Set up clip rect so that we can do stuff like render lines to samples | |
| // falling outside the render rectangle and have them get clipped. | |
| GUI.BeginGroup(rect); | |
| var plotType = timeline.plotType; | |
| var lastPos = default(Vector2); | |
| var timeUnitsPerPixel = rect.width / m_TotalTimeUnitsShown; | |
| var color = m_Timelines[i].color; | |
| for (var n = sampleCount - 1; n >= 0; --n) | |
| { | |
| var sample = timeUnit == TimeUnit.Frames | |
| ? timeline.frameSamples[n].value | |
| : timeline.timeSamples[n].value; | |
| ////TODO: respect limitsY | |
| float y; | |
| if (sample.isEmpty) | |
| y = 0.5f; | |
| else | |
| y = sample.ToSingle(); | |
| y /= limitsY.y; | |
| var deltaTime = timeUnit == TimeUnit.Frames | |
| ? timeline.frameSamples[n].frame - startFrame | |
| : timeline.timeSamples[n].time - startTime; | |
| var pos = new Vector2(deltaTime * timeUnitsPerPixel, rect.height - y * rect.height); | |
| if (plotType == PlotType.LineGraph) | |
| { | |
| if (n != sampleCount - 1) | |
| { | |
| DrawLine(lastPos, pos, color, 2); | |
| if (pos.x < 0) | |
| break; | |
| } | |
| } | |
| else if (plotType == PlotType.BarChart) | |
| { | |
| ////TODO: make rectangles have a progressively stronger hue or saturation | |
| var barRect = new Rect(pos.x, pos.y, timeUnitsPerPixel, y * limitsY.y * rect.height); | |
| DrawRectangle(barRect, color); | |
| } | |
| lastPos = pos; | |
| } | |
| GUI.EndGroup(); | |
| } | |
| if (showLegend && timelineCount > 0) | |
| { | |
| var legendRect = rect; | |
| legendRect.x += rect.width + 2; | |
| legendRect.width = 400; | |
| legendRect.height = ValueTextStyle.CalcHeight(m_Timelines[0].name, 400); | |
| for (var i = 0; i < m_Timelines.Length; ++i) | |
| { | |
| var colorTagRect = legendRect; | |
| colorTagRect.width = 5; | |
| var labelRect = legendRect; | |
| labelRect.x += 8; | |
| labelRect.width -= 8; | |
| DrawRectangle(colorTagRect, m_Timelines[i].color); | |
| DrawText(m_Timelines[i].name, labelRect.position, ValueTextStyle); | |
| legendRect.y += labelRect.height + 2; | |
| } | |
| } | |
| if (showLimits) | |
| { | |
| if (m_LimitsYMax == null) | |
| m_LimitsYMax = new GUIContent(m_LimitsY.y.ToString()); | |
| if (m_LimitsYMin == null) | |
| m_LimitsYMin = new GUIContent(m_LimitsY.x.ToString()); | |
| DrawText(m_LimitsYMax, new Vector2(rect.x + rect.width, rect.y), ValueTextStyle); | |
| DrawText(m_LimitsYMin, new Vector2(rect.x + rect.width, rect.y + rect.height), ValueTextStyle); | |
| } | |
| } | |
| public override void AddSample(object value, double time) | |
| { | |
| if (timelineCount == 0) | |
| throw new InvalidOperationException("Must have set up a timeline first"); | |
| AddSample(0, PrimitiveValue.FromObject(value), (float)time); | |
| } | |
| public int AddTimeline(string name, Color color, PlotType plotType = default) | |
| { | |
| var timeline = new Timeline | |
| { | |
| name = new GUIContent(name), | |
| color = color, | |
| plotType = plotType, | |
| }; | |
| if (timeUnit == TimeUnit.Frames) | |
| timeline.frameSamples = new RingBuffer<FrameSample>(historyDepth); | |
| else | |
| timeline.timeSamples = new RingBuffer<TimeSample>(historyDepth); | |
| var index = timelineCount; | |
| Array.Resize(ref m_Timelines, timelineCount + 1); | |
| m_Timelines[index] = timeline; | |
| return index; | |
| } | |
| public int GetTimeline(string name) | |
| { | |
| for (var i = 0; i < timelineCount; ++i) | |
| if (string.Compare(m_Timelines[i].name.text, name, StringComparison.InvariantCultureIgnoreCase) == 0) | |
| return i; | |
| return -1; | |
| } | |
| // Add a time-based sample. | |
| public void AddSample(int timelineIndex, PrimitiveValue value, float time) | |
| { | |
| m_Timelines[timelineIndex].timeSamples.Append(new TimeSample | |
| { | |
| value = value, | |
| time = time | |
| }); | |
| } | |
| // Add a frame-based sample. | |
| public ref PrimitiveValue GetOrCreateSample(int timelineIndex, int frame) | |
| { | |
| ref var timeline = ref m_Timelines[timelineIndex]; | |
| ref var samples = ref timeline.frameSamples; | |
| var count = samples.count; | |
| if (count > 0) | |
| { | |
| if (samples[count - 1].frame == frame) | |
| return ref samples[count - 1].value; | |
| Debug.Assert(samples[count - 1].frame < frame, "Frame numbers must be ascending"); | |
| } | |
| return ref samples.Append(new FrameSample {frame = frame}).value; | |
| } | |
| private float m_TotalTimeUnitsShown; | |
| private Vector2 m_LimitsY = new Vector2(-1, 1); | |
| private GUIContent m_LimitsYMin; | |
| private GUIContent m_LimitsYMax; | |
| private Timeline[] m_Timelines; | |
| private struct TimeSample | |
| { | |
| public PrimitiveValue value; | |
| public float time; | |
| } | |
| private struct FrameSample | |
| { | |
| public PrimitiveValue value; | |
| public int frame; | |
| } | |
| private struct Timeline | |
| { | |
| public GUIContent name; | |
| public Color color; | |
| public RingBuffer<TimeSample> timeSamples; | |
| public RingBuffer<FrameSample> frameSamples; | |
| public PrimitiveValue minValue; | |
| public PrimitiveValue maxValue; | |
| public PlotType plotType; | |
| } | |
| public enum PlotType | |
| { | |
| LineGraph, | |
| BarChart, | |
| } | |
| public enum TimeUnit | |
| { | |
| Seconds, | |
| Frames, | |
| } | |
| } | |
| public static void DrawAxis(Axis axis, Rect rect, Color color = default, float width = 1) | |
| { | |
| Vector2 start, end, tickOffset; | |
| switch (axis) | |
| { | |
| case Axis.X: | |
| start = new Vector2(rect.x, rect.y + rect.height / 2); | |
| end = new Vector2(start.x + rect.width, rect.y + rect.height / 2); | |
| tickOffset = new Vector2(0, 3); | |
| break; | |
| case Axis.Y: | |
| start = new Vector2(rect.x + rect.width / 2, rect.y); | |
| end = new Vector2(start.x, rect.y + rect.height); | |
| tickOffset = new Vector2(3, 0); | |
| break; | |
| case Axis.Z: | |
| // From bottom left corner to upper right corner. | |
| start = new Vector2(rect.x, rect.yMax); | |
| end = new Vector2(rect.xMax, rect.y); | |
| tickOffset = new Vector2(1.5f, 1.5f); | |
| break; | |
| default: | |
| throw new NotImplementedException(); | |
| } | |
| ////TODO: label limits | |
| DrawLine(start, end, color, width); | |
| DrawLine(start - tickOffset, start + tickOffset, color, width); | |
| DrawLine(end - tickOffset, end + tickOffset, color, width); | |
| } | |
| public static void DrawRectangle(Rect rect, Color color) | |
| { | |
| var savedColor = GUI.color; | |
| GUI.color = color; | |
| GUI.DrawTexture(rect, OnePixTex); | |
| GUI.color = savedColor; | |
| } | |
| public static void DrawText(string text, Vector2 pos, GUIStyle style) | |
| { | |
| var content = new GUIContent(text); | |
| DrawText(content, pos, style); | |
| } | |
| public static void DrawText(GUIContent text, Vector2 pos, GUIStyle style) | |
| { | |
| var content = new GUIContent(text); | |
| var size = style.CalcSize(content); | |
| var rect = new Rect(pos.x, pos.y, size.x, size.y); | |
| style.Draw(rect, content, false, false, false, false); | |
| } | |
| // Adapted from http://wiki.unity3d.com/index.php?title=DrawLine | |
| public static void DrawLine(Vector2 pointA, Vector2 pointB, Color color = default, float width = 1) | |
| { | |
| // Save the current GUI matrix, since we're going to make changes to it. | |
| var matrix = GUI.matrix; | |
| // Store current GUI color, so we can switch it back later, | |
| // and set the GUI color to the color parameter | |
| var savedColor = GUI.color; | |
| GUI.color = color; | |
| // Determine the angle of the line. | |
| var angle = Vector3.Angle(pointB - pointA, Vector2.right); | |
| // Vector3.Angle always returns a positive number. | |
| // If pointB is above pointA, then angle needs to be negative. | |
| if (pointA.y > pointB.y) | |
| angle = -angle; | |
| // Use ScaleAroundPivot to adjust the size of the line. | |
| // We could do this when we draw the texture, but by scaling it here we can use | |
| // non-integer values for the width and length (such as sub 1 pixel widths). | |
| // Note that the pivot point is at +.5 from pointA.y, this is so that the width of the line | |
| // is centered on the origin at pointA. | |
| GUIUtility.ScaleAroundPivot(new Vector2((pointB - pointA).magnitude, width), new Vector2(pointA.x, pointA.y + 0.5f)); | |
| // Set the rotation for the line. | |
| // The angle was calculated with pointA as the origin. | |
| GUIUtility.RotateAroundPivot(angle, pointA); | |
| // Finally, draw the actual line. | |
| // We're really only drawing a 1x1 texture from pointA. | |
| // The matrix operations done with ScaleAroundPivot and RotateAroundPivot will make this | |
| // render with the proper width, length, and angle. | |
| GUI.DrawTexture(new Rect(pointA.x, pointA.y, 1, 1), OnePixTex); | |
| // We're done. Restore the GUI matrix and GUI color to whatever they were before. | |
| GUI.matrix = matrix; | |
| GUI.color = savedColor; | |
| } | |
| private static Texture2D s_OnePixTex; | |
| private static GUIStyle s_ValueTextStyle; | |
| internal static GUIStyle ValueTextStyle | |
| { | |
| get | |
| { | |
| if (s_ValueTextStyle == null) | |
| { | |
| s_ValueTextStyle = new GUIStyle(); | |
| s_ValueTextStyle.fontSize -= 2; | |
| s_ValueTextStyle.normal.textColor = Color.white; | |
| } | |
| return s_ValueTextStyle; | |
| } | |
| } | |
| internal static Texture2D OnePixTex | |
| { | |
| get | |
| { | |
| if (s_OnePixTex == null) | |
| s_OnePixTex = new Texture2D(1, 1); | |
| return s_OnePixTex; | |
| } | |
| } | |
| public struct RingBuffer<TValue> | |
| { | |
| public TValue[] array; | |
| public int head; | |
| public int count; | |
| public RingBuffer(int size) | |
| { | |
| array = new TValue[size]; | |
| head = 0; | |
| count = 0; | |
| } | |
| public ref TValue Append(TValue value) | |
| { | |
| int index; | |
| var bufferSize = array.Length; | |
| if (count < bufferSize) | |
| { | |
| Debug.Assert(head == 0, "Head can't have moved if buffer isn't full yet"); | |
| index = count; | |
| ++count; | |
| } | |
| else | |
| { | |
| // Buffer is full. Bump head. | |
| index = (head + count) % bufferSize; | |
| ++head; | |
| } | |
| array[index] = value; | |
| return ref array[index]; | |
| } | |
| public ref TValue this[int index] | |
| { | |
| get | |
| { | |
| if (index < 0 || index >= count) | |
| throw new ArgumentOutOfRangeException(nameof(index)); | |
| return ref array[(head + index) % array.Length]; | |
| } | |
| } | |
| } | |
| } | |
| } | |