-
-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathGraph.cs
426 lines (338 loc) · 13.1 KB
/
Graph.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace cmdwtf.UnityTools.Editor
{
/// <summary>
/// A very feature-light tool to draw simple graphs in the Unity editor.
/// </summary>
public class Graph
{
#region Constants
internal static readonly Color DefaultLineColor = Color.cyan;
internal static readonly Color DefaultFrameColor = Constants.VeryDarkGray;
internal static readonly Color DefaultBackgroundColor = Constants.DarkGray;
internal static readonly Color DefaultGridLinesColor = DefaultFrameColor;
internal static readonly Color DefaultAxisColor = Color.white;
private const float DefaultFrameThicknessPx = 1f;
private const float DefaultLineThickness = 1f;
private const float DefaultLineFocusedThickness = 2f;
private static readonly int DisplayAxisLabelHeightPx = (int)EditorGUIUtility.singleLineHeight;
private const int DisplayAxisLabelWidthPx = 64;
private static readonly Vector2 DisplayAxisLabelValueOffsetPx = new(-12, -DisplayAxisLabelHeightPx / 2f);
private static readonly Vector2 DisplayAxisLabelTimePxRight = new(-DisplayAxisLabelWidthPx, 0);
private static readonly Vector2 DisplayAxisLabelSize = new(DisplayAxisLabelWidthPx, DisplayAxisLabelHeightPx);
#endregion
#region Public Properties
public float FrameThicknessPx { get; set; } = DefaultFrameThicknessPx;
public float LineThickness { get; set; } = DefaultLineThickness;
public float LineFocusedThickness { get; set; } = DefaultLineFocusedThickness;
public Color FrameColor { get; set; } = DefaultFrameColor;
public Color BackgroundFillColor { get; set; } = DefaultBackgroundColor;
public Color GridLinesColor { get; set; } = DefaultGridLinesColor;
public Color HorizontalAxisColor { get; set; } = DefaultAxisColor;
public Color VerticalAxisColor { get; set; } = DefaultAxisColor;
public bool ShouldDrawFrame { get; set; } = true;
public bool ShouldDrawGridLines { get; set; } = false;
public bool ShouldDrawLines { get; set; } = true;
public bool ShouldDrawLabels { get; set; } = true;
public bool ShouldDrawBackground { get; set; } = true;
public bool ShouldDrawHorizontalAxis { get; set; } = true;
public bool ShouldDrawVerticalAxis { get; set; } = true;
public string HorizontalAxisUnits { get; set; } = string.Empty;
public string VerticalAxisUnits { get; set; } = string.Empty;
public Func<object, string> GetHorizontalMidText { private get; set; }
public GUIStyle LabelStyle { get; set; } = new GUIStyle("label");
#endregion
#region Private State
private Rect _frameDrawingPosition;
private Rect _drawingPosition;
private bool _needsLayout;
private readonly Dictionary<string, GraphLine> _lines = new();
private float AllLinesMinimumValue { get; set; }
private float AllLinesMaximumValue { get; set; }
// the logical/expected dimensions of the data to graph
private GraphDimensions _logicalDimensions;
// UI pixel coords to draw graph & others from
private Vector2 _graphBottomLeft;
private Vector2 _graphBottomRight;
private Vector2 _graphTopLeft;
private Vector2 _graphLogicalBottomLeft;
private Vector2 _graphLogicalBottomRight;
private Vector2 _graphLogicalTopLeft;
#endregion
#region Public Interface
public Graph()
: this(0, 1, 0, 1)
{
}
public Graph(float minX, float maxX, float minY, float maxY)
{
SetLogicalRanges(minX, maxX, minY, maxY);
}
public void SetLogicalRanges(float minX, float maxX, float minY, float maxY)
{
if (minX > maxX)
{
throw new ArgumentOutOfRangeException(nameof(minX),
$"{nameof(maxX)} should be greater than {nameof(minX)}.");
}
if (minY > maxY)
{
throw new ArgumentOutOfRangeException(nameof(minY),
$"{nameof(maxY)} should be greater than {nameof(minY)}.");
}
_logicalDimensions = new GraphDimensions()
{
MinimumX = minX, MaximumX = maxX,
MinimumY = minY, MaximumY = maxY,
};
}
public void ExtendLogicalRanges(float minX, float maxX, float minY, float maxY)
{
if (minX > maxX)
{
throw new ArgumentOutOfRangeException(nameof(minX),
$"{nameof(maxX)} should be greater than {nameof(minX)}.");
}
if (minY > maxY)
{
throw new ArgumentOutOfRangeException(nameof(minY),
$"{nameof(maxY)} should be greater than {nameof(minY)}.");
}
// extend the ranges if they are exceeded
_logicalDimensions.MinimumX = Mathf.Min(_logicalDimensions.MinimumX, minX);
_logicalDimensions.MaximumX = Mathf.Min(_logicalDimensions.MaximumX, maxX);
_logicalDimensions.MinimumY = Mathf.Min(_logicalDimensions.MinimumY, minY);
_logicalDimensions.MaximumY = Mathf.Min(_logicalDimensions.MaximumY, maxY);
}
public IEnumerable<T> GetLineUserData<T>()
{
foreach (GraphLine line in _lines.Values)
{
if (line.UserData is T data)
{
yield return data;
}
}
}
public T GetLineUserData<T>(string lineKey)
=> _lines.TryGetValue(lineKey, out GraphLine line)
? (T)line.UserData
: default;
public void UpdateLine(string lineKey, float[] ySamples, Color? lineColor = null, object userData = null)
{
if (!_lines.TryGetValue(lineKey, out GraphLine line))
{
line = new GraphLine();
_lines.Add(lineKey, line);
}
line.UIPoints = new Vector3[ySamples.Length];
line.Visible = true;
line.UserData = userData;
line.Color = lineColor ?? DefaultLineColor;
line.SampleMinimum = Mathf.Min(_logicalDimensions.MinimumY, Mathf.Min(ySamples));
line.SampleMaximum = Mathf.Max(_logicalDimensions.MaximumY, Mathf.Max(ySamples));
// copy the samples to the line data, we will process them next time we layout.
line.OriginalSamples = new float[ySamples.Length];
Array.Copy(ySamples, line.OriginalSamples, ySamples.Length);
_lines[lineKey] = line;
UpdateValueExtremes();
_needsLayout = true;
}
public bool RemoveLine(string lineKey) => _lines.Remove(lineKey);
public IEnumerable<Vector3> GetLineUIPoints(string lineKey)
=> _lines.ContainsKey(lineKey) == false
? null
: _lines[lineKey].UIPoints;
public void UpdateDrawingRect(Rect position)
{
// see if our position changed requiring a new layout.
if (_frameDrawingPosition != position)
{
_needsLayout = true;
}
_frameDrawingPosition = position;
_drawingPosition = position.Shrink(FrameThicknessPx * 2);
if (_needsLayout)
{
PerformLayout();
}
}
public void Draw(Rect position)
{
UpdateDrawingRect(position);
Handles.BeginGUI();
// hold onto original handle color
Color originalHandleColor = Handles.color;
if (ShouldDrawFrame)
{
DrawFrame();
}
if (ShouldDrawBackground)
{
DrawBackground();
}
if (ShouldDrawGridLines)
{
DrawGridLines();
}
if (ShouldDrawHorizontalAxis)
{
Handles.color = HorizontalAxisColor;
Handles.DrawLine(_graphLogicalBottomLeft, _graphLogicalBottomRight);
}
if (ShouldDrawVerticalAxis)
{
Handles.color = VerticalAxisColor;
Handles.DrawLine(_graphBottomLeft, _graphTopLeft);
}
if (ShouldDrawLines)
{
DrawLines(focused: false);
DrawLines(focused: true);
}
// reset the handles' color back to not disturb further drawing
Handles.color = originalHandleColor;
Handles.EndGUI();
// draw our labels last so they render on top of the graph drawn areas
if (ShouldDrawLabels)
{
DrawLabels();
}
}
public void SetAllLinesVisible(bool visible)
{
foreach (GraphLine line in _lines.Values)
{
line.Focused = visible;
line.Visible = visible;
}
}
public void SetLineVisible(string lineKey, bool visible)
{
if (!_lines.TryGetValue(lineKey, out GraphLine line))
{
Debug.LogWarning($"Line for key {lineKey} wasn't found to set visible: {visible}");
return;
}
line.Focused = visible;
line.Visible = visible;
}
public void FocusLines(params string[] lineKeys)
{
if (lineKeys.Length == 1 && lineKeys[0] == null)
{
lineKeys = null;
}
foreach (KeyValuePair<string, GraphLine> kvp in _lines)
{
kvp.Value.Focused = lineKeys == null || lineKeys.Contains(kvp.Key);
}
}
#endregion
#region Private Methods
private Vector2 SampleToUIPoint(float xSample, float ySample)
{
float valueDelta = (AllLinesMaximumValue - AllLinesMinimumValue);
float xResult = Mathf.Lerp(_drawingPosition.xMin, _drawingPosition.xMax, xSample);
float yResult = Mathf.Lerp(_drawingPosition.yMax, _drawingPosition.yMin, (ySample - AllLinesMinimumValue) / valueDelta);
return new Vector2(xResult, yResult);
}
private void PerformLayout()
{
UpdateValueExtremes();
foreach (GraphLine line in _lines.Values)
{
int scanMax = line.OriginalSamples.Length - 1;
for (int scan = 0; scan < line.OriginalSamples.Length; ++scan)
{
float percent = scan / (float)scanMax;
float xSample = Mathf.Lerp(_logicalDimensions.MinimumX, _logicalDimensions.MaximumX, percent);
// convert graph x/y to gui positions and store them.
line.UIPoints[scan] = SampleToUIPoint(xSample, line.OriginalSamples[scan]);
}
}
// calculate pixel coords based on the logical graph size
_graphLogicalBottomLeft = SampleToUIPoint(_logicalDimensions.MinimumX, _logicalDimensions.MinimumY);
_graphLogicalBottomRight = SampleToUIPoint(_logicalDimensions.MaximumX, _logicalDimensions.MinimumY);
_graphLogicalTopLeft = SampleToUIPoint(_logicalDimensions.MinimumX, _logicalDimensions.MaximumY);
// calculate pixel cords based on the actual data
_graphBottomLeft = SampleToUIPoint(_logicalDimensions.MinimumX, AllLinesMinimumValue);
_graphBottomRight = SampleToUIPoint(_logicalDimensions.MaximumX, AllLinesMinimumValue);
_graphTopLeft = SampleToUIPoint(_logicalDimensions.MinimumX, AllLinesMaximumValue);
_needsLayout = false;
}
private void UpdateValueExtremes()
{
if (_lines.Count == 0)
{
AllLinesMaximumValue = _logicalDimensions.MinimumY;
AllLinesMaximumValue = _logicalDimensions.MaximumY;
return;
}
float[] minimumValues = _lines.Values.Select(l => l.SampleMinimum).ToArray();
float[] maximumValues = _lines.Values.Select(l => l.SampleMaximum).ToArray();
AllLinesMinimumValue = MathF.Min(_logicalDimensions.MinimumY,Mathf.Min(minimumValues));
AllLinesMaximumValue = Mathf.Max(_logicalDimensions.MaximumY, Mathf.Max(maximumValues));
}
private void DrawFrame()
=> EditorGUI.DrawRect(_frameDrawingPosition, FrameColor);
private void DrawBackground()
=> EditorGUI.DrawRect(_drawingPosition, BackgroundFillColor);
// #nyi gridlines
private void DrawGridLines() =>
GUI.Label(_drawingPosition, "Gridlines Not Yet Implemented");
private void DrawLines(bool focused)
{
// draw all lines
foreach (GraphLine line in _lines.Values)
{
if (!line.Visible)
{
continue;
}
if (line.Focused != focused)
{
continue;
}
float alpha = line.Focused ? 1f : 0.5f;
Handles.color = line.Color.WithAlpha(alpha);
float thickness = focused ? LineFocusedThickness : LineThickness;
#if UNITY_2020_3_OR_NEWER
Handles.DrawAAPolyLine(thickness, line.UIPoints);
#else
Handles.DrawPolyLine(line.UIPoints)
#endif // UNITY_2020_3_OR_NEWER
}
}
private void DrawLabels()
{
Vector2 baseLineTextLocation = _graphBottomLeft;
Vector2 baseLineTextLocationRight = _graphBottomRight;
GUIStyle midTimeLabelStyle = new(LabelStyle) { alignment = TextAnchor.UpperCenter };
GUIStyle maxTimeLabelStyle = new(LabelStyle) { alignment = TextAnchor.UpperRight };
// ui positions to draw axis labels at
var minYPos = new Rect(_graphLogicalBottomLeft + DisplayAxisLabelValueOffsetPx, DisplayAxisLabelSize);
var targetYPos = new Rect(_graphLogicalTopLeft + DisplayAxisLabelValueOffsetPx, DisplayAxisLabelSize);
var minXPos = new Rect(baseLineTextLocation, DisplayAxisLabelSize);
var midXPos = new Rect(baseLineTextLocation, new Vector2(_drawingPosition.width, DisplayAxisLabelHeightPx));
var maxXPos = new Rect(baseLineTextLocationRight + DisplayAxisLabelTimePxRight, DisplayAxisLabelSize);
// draw x axis (horizontal) labels
GUI.Label(minXPos, $"{_logicalDimensions.MinimumX}{HorizontalAxisUnits}", LabelStyle);
GUI.Label(maxXPos, $"{_logicalDimensions.MaximumX}{HorizontalAxisUnits}", maxTimeLabelStyle);
// draw the bottom middle text, if the user set the callback.
if (GetHorizontalMidText != null)
{
GraphLine focusedLine = _lines.Values.FirstOrDefault(v => v.Focused);
GUI.Label(midXPos, GetHorizontalMidText.Invoke(focusedLine?.UserData), midTimeLabelStyle);
}
// draw y axis (vertical) labels
GUI.Label(minYPos, $"{_logicalDimensions.MinimumY}{VerticalAxisUnits}", LabelStyle);
GUI.Label(targetYPos, $"{_logicalDimensions.MaximumY}{VerticalAxisUnits}", LabelStyle);
}
#endregion
}
}