-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathMyHandMenuRadial.cs
357 lines (299 loc) · 13.4 KB
/
MyHandMenuRadial.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
using System;
using StereoKit;
using System.Collections.Generic;
using StereoKit.Framework;
namespace My.Framework
{
/// <summary>This enum specifies how HandMenuItems should behave
/// when selected! This is in addition to the HandMenuItem's
/// callback function.</summary>
/// <summary>A menu that shows up in circle around the user's
/// hand! Selecting an item can perform an action, or even spawn
/// a sub-layer of menu items. This is an easy way to store
/// actions out of the way, yet remain easily accessible to the
/// user.
///
/// The user faces their palm towards their head, and then makes
/// a grip motion to spawn the menu. The user can then perform actions
/// by making fast, direction based motions that are easy to build
/// muscle memory for.</summary>
public class HandMenuRadial : IStepper
{
#region Fields
const float minDist = 0.03f;
const float midDist = 0.065f;
const float maxDist = 0.1f;
const float minScale = 0.05f;
public bool active = false;
Pose menuPose;
Pose destPose;
public TextStyle textStyle = TextStyle.Default;
public TextStyle iconStyle = TextStyle.Default;
public HandRadialLayer[] layers;
Stack<int> navStack = new Stack<int>();
public int activeLayer;
public LinePoint[] circle;
LinePoint[] innerCircle;
float activation = minScale;
float angleOffset = 0;
#endregion
#region Public Methods
/// <summary>Creates a hand menu from the provided array of menu
/// layers! HandMenuRadial is an IStepper, so proper usage is to
/// add it to the Stepper list via `StereoKitApp.AddStepper`.</summary>
/// <param name="menuLayers">Starting layer is always the first one
/// in the list! Layer names in the menu items refer to layer names
/// in this list.</param>
public HandMenuRadial(params HandRadialLayer[] menuLayers)
{
try
{
layers = menuLayers;
circle = new LinePoint[48];
innerCircle = new LinePoint[circle.Length];
// Pre-generate circles for the menu
float step = 360.0f / (circle.Length - 1);
Color color = Color.White * 0.5f;
for (int i = 0; i < circle.Length; i++)
{
Vec3 dir = Vec3.AngleXY(i * step);
circle[i] = new LinePoint(dir * maxDist, color, 2 * Units.mm2m);
innerCircle[i] = new LinePoint(dir * minDist, color, 2 * Units.mm2m);
}
}
catch (Exception ex)
{
Log.Err(ex.Source + ":" + ex.Message);
}
}
/// <summary>Force the hand menu to show at a specific location.
/// This will close the hand menu if it was already open, and resets
/// it to the root menu layer. Also plays an opening sound.</summary>
/// <param name="at">A world space position for the hand menu.</param>
public void Show(Vec3 at)
{
try
{
if (active)
Close();
Default.SoundClick.Play(at);
destPose.position = at;
destPose.orientation = Quat.LookAt(menuPose.position, Input.Head.position);
active = true;
}
catch (Exception ex) { Log.Err(ex.Source + ":" + ex.Message); }
}
/// <summary>Closes the menu if it's open! Plays a closing sound.</summary>
public void Close()
{
try
{
if (!active)
return;
Default.SoundUnclick.Play(menuPose.position);
activation = minScale;
active = false;
angleOffset = 0;
Main.MenuClosed();
}
catch (Exception ex) { Log.Err(ex.Source + ":" + ex.Message); }
}
public void Reset()
{
try
{
Close();
navStack.Clear();
activeLayer = 0;
}
catch (Exception ex) { Log.Err(ex.Source + ":" + ex.Message); }
}
/// <summary>Part of IStepper, you shouldn't be calling this yourself.</summary>
public void Step()
{
try
{
Hand hand = Input.Hand(Handed.Right);
bool facing = HandFacingHead(hand);
//Log.Info(facing + ":" + active);
if (facing && !active)
StepMenuIndicator(hand);
if (active)
StepMenu(hand);
}
catch (Exception ex)
{
Log.Err(ex.Source + ":" + ex.Message);
}
}
/// <summary>HandMenuRadial is always Enabled.</summary>
public bool Enabled => true;
/// <summary>Part of IStepper, you shouldn't be calling this yourself.</summary>
/// <returns>Always returns true.</returns>
public bool Initialize() => true;
/// <summary>Part of IStepper, you shouldn't be calling this yourself.</summary>
public void Shutdown() { }
#endregion
#region Private Methods
void StepMenuIndicator(Hand hand)
{
try {
if (Platform.FilePickerVisible)
return;
// Scale the indicator based on the 'activation' of the grip motion
activation = Math.Max(0.2f, hand.gripActivation * .3f);
menuPose.position = hand.palm.position;
menuPose.orientation = Quat.LookAt(menuPose.position, Input.Head.position);
menuPose.position += menuPose.Forward * U.cm * 1.5f;
bool aligned = Vec3.Dot(hand.palm.orientation * Vec3.Forward, (hand.palm.position - Input.Head.position).Normalized) < -.99f;
// Draw the menu circle!
Hierarchy.Push(menuPose.ToMatrix(activation));
Lines.Add(circle);
Hierarchy.Push(Matrix.S(1.6f));
Text.Add(aligned ? "Grip" : "Align", Matrix.Identity, TextAlign.Center);
Hierarchy.Pop();
Hierarchy.Pop();
Hierarchy.Push(Matrix.TRS(hand.palm.position + (hand.palm.orientation * Vec3.Forward * .03f), hand.palm.orientation, .16f));
Lines.Add(circle);
Hierarchy.Pop();
// And if the user grips, show the menu!
if (hand.IsJustGripped && aligned)
Show(hand[FingerId.Index, JointId.Tip].position);
}
catch (Exception ex) { Log.Err(ex.Source + ":" + ex.Message); }
}
private string[] bits;
private const float menuScale = 1f;
void StepMenu(Hand hand)
{
try
{
// Animate the menu a bit
float time = (Time.Elapsedf * 24);
menuPose.position = Vec3.Lerp(menuPose.position, destPose.position, time);
menuPose.orientation = Quat.Slerp(menuPose.orientation, destPose.orientation, time);
activation = SKMath.Lerp(activation, menuScale, time);
// Pre-calculate some circle traversal values
HandRadialLayer layer = layers[activeLayer];
int count = layer.items.Length;
float step = 360 / count;
float halfStep = step / 2;
// Push the Menu's pose onto the stack, so we can draw, and work
// in local space.
Hierarchy.Push(menuPose.ToMatrix(activation));
// Calculate the status of the menu!
Vec3 tipWorld = hand[FingerId.Index, JointId.Tip].position;
Vec3 tipLocal = Hierarchy.ToLocal(tipWorld);
float selectDist = minDist + ((maxDist - minDist) * .2f);
float magSq = tipLocal.MagnitudeSq;
bool onMenu = tipLocal.z > -0.02f && tipLocal.z < 0.02f;
bool focused = onMenu && magSq > minDist * minDist;
bool selected = onMenu && magSq > selectDist * selectDist;
bool cancel = magSq > maxDist * maxDist;
// Find where our finger is pointing to, and draw that
float fingerAngle = (float)Math.Atan2(tipLocal.y, tipLocal.x) * Units.rad2deg - (layer.startAngle + angleOffset);
while (fingerAngle < 0) fingerAngle += 360;
int angleId = (int)(fingerAngle / step);
float planeDistance = Math.Max(.01f, .05f - Math.Abs(tipLocal.z));
Lines.Add(Vec3.Zero, new Vec3(tipLocal.x, tipLocal.y, 0), Color.White, 0.1f * planeDistance);
// Draw the menu inner and outer circles
Lines.Add(circle);
Lines.Add(innerCircle);
// Now draw each of the menu items!
for (int i = 0; i < count; i++)
{
bits = layer.items[i].name.Split('|');
bits[0] = bits[0].Replace("XXX", Main.actionRequested);
float currAngle = i * step + layer.startAngle + angleOffset;
bool highlightText = focused && angleId == i;
bool highlightLine = highlightText || (focused && (angleId + 1) % count == i);
Vec3 dir = Vec3.AngleXY(currAngle);
Lines.Add(dir * minDist, dir * maxDist, highlightLine ? Color.White : Color.White * 0.5f, highlightLine ? 0.002f : 0.001f);
Text.Add(bits[0], Matrix.TRS(Vec3.AngleXY(currAngle + halfStep) * (midDist + .015f), Quat.FromAngles(0, 0, currAngle + halfStep - 90), highlightText ? 1.2f : 1), textStyle, TextAlign.BottomCenter);
if (bits.Length > 1)
Text.Add(bits[1], Matrix.TRS(Vec3.AngleXY(currAngle + halfStep) * (midDist - .015f), Quat.FromAngles(0, 0, currAngle + halfStep - 90), highlightText ? 1.2f : 1), iconStyle, TextAlign.BottomCenter);
}
// Done with local work
Hierarchy.Pop();
// Execute any actions that were discovered
// But not if we're still in the process of animating, interaction values
// could be pretty incorrect when we're still lerping around.
if (activation < 0.95f) return;
if (selected) SelectItem(angleId, layer.items[angleId], tipWorld, (angleId + 0.5f) * step);
if (cancel) Close();
}
catch (Exception ex)
{
Log.Err(ex.Source + ":" + ex.Message);
}
}
public void SelectLayer(string name)
{
try
{
Default.SoundClick.Play(menuPose.position);
navStack.Push(activeLayer);
activeLayer = Array.FindIndex(layers, l => l.layerName == name);
if (activeLayer == -1)
Log.Err($"Couldn't find hand menu layer named {name}!");
}
catch (Exception ex) { Log.Err(ex.Source + ":" + ex.Message); }
}
public void Back()
{
try
{
Default.SoundUnclick.Play(menuPose.position);
if (navStack.Count > 0)
activeLayer = navStack.Pop();
}
catch (Exception ex) { Log.Err(ex.Source + ":" + ex.Message); }
}
public int lastIdSelected;
void SelectItem(int id, HandMenuItem item, Vec3 at, float fromAngle)
{
try
{
lastIdSelected = id;
if (item.action == HandMenuAction.Close) Close();
else if (item.action == HandMenuAction.Layer) SelectLayer(item.layerName);
else if (item.action == HandMenuAction.Back) Back();
if (item.action == HandMenuAction.Back || item.action == HandMenuAction.Layer)
Reposition(at, fromAngle);
item.callback?.Invoke();
}
catch (Exception ex)
{
Log.Err(ex.Source + ":" + ex.Message);
}
}
void Reposition(Vec3 at, float fromAngle)
{
try {
Plane plane = new Plane(menuPose.position, menuPose.Forward);
destPose.position = plane.Closest(at);
if (layers[activeLayer].backAngle != 0)
{
angleOffset = (fromAngle - layers[activeLayer].backAngle) + 180;
while (angleOffset < 0) angleOffset += 360;
while (angleOffset > 360) angleOffset -= 360;
}
else angleOffset = 0;
}
catch (Exception ex) { Log.Err(ex.Source + ":" + ex.Message); }
}
static public bool HandFacingHead(Hand hand)
{
try {
if (!hand.IsTracked)
return false;
Vec3 palmDirection = hand.palm.Forward.Normalized;
Vec3 directionToHead = (Input.Head.position - hand.palm.position).Normalized;
return Vec3.Dot(palmDirection, directionToHead) > 0.5f;
}
catch (Exception ex) { Log.Err(ex.Source + ":" + ex.Message); }
return false;
}
#endregion
}
}