702 lines
27 KiB
C#
702 lines
27 KiB
C#
|
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];
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|