stardust_xr_fusion/
input.rs

1//! Spatial input using the Spatial Universal Interaction System (SUIS).
2//!
3//! Input methods are nodes that represent a 3D pointer, hand, or tip (single point of interaction, like a controller).
4//! They contain a datamap which is a flexbuffer map with non-spatial data like button/trackpad/grip.
5//!
6//! Input handlers are nodes that represent an object that can react to spatial input.
7//! They have a field attached that is important data for the SUIS to determine what should get input.
8//! On input (`InputHandlerHandler::input`) the input data's spatial components is relative to the input handler itself.
9//! The return value for `InputHandlerHandler::input` is `true` if you want to capture the input method.
10//! Capturing an input method is useful to indicate that only that handler should get its input.
11//! For example when grabbing you don't want your hand to press buttons if you're grabbing the object through them.
12//! Input handlers should account for the occasional case where their field is closer than an input handler that captured a method by filtering out interactions that are triggered the same frame the input method first becomes visible.
13//! Capturing an input method may be delayed a frame or 2.
14
15use crate::{
16	fields::FieldAspect,
17	node::NodeResult,
18	spatial::{SpatialRefAspect, Transform},
19};
20use glam::{FloatExt, Quat, Vec3A, vec3a};
21use stardust_xr::values::*;
22use std::hash::Hash;
23
24pub use crate::protocol::input::*;
25
26impl InputMethod {
27	pub fn create(
28		spatial_parent: &impl SpatialRefAspect,
29		transform: Transform,
30		input_type: InputDataType,
31		datamap: &Datamap,
32	) -> NodeResult<Self> {
33		let client = spatial_parent.client();
34		create_input_method(
35			client,
36			client.generate_id(),
37			spatial_parent,
38			transform,
39			input_type,
40			datamap,
41		)
42	}
43}
44impl InputHandler {
45	pub fn create(
46		spatial_parent: &impl SpatialRefAspect,
47		transform: Transform,
48		field: &impl FieldAspect,
49	) -> NodeResult<Self> {
50		let client = spatial_parent.client();
51		create_input_handler(
52			client,
53			client.generate_id(),
54			spatial_parent,
55			transform,
56			field,
57		)
58	}
59}
60impl Default for Joint {
61	fn default() -> Self {
62		Joint {
63			position: [0.0; 3].into(),
64			rotation: Quat::IDENTITY.into(),
65			radius: 0.0,
66			distance: 0.0,
67		}
68	}
69}
70impl Default for Finger {
71	fn default() -> Self {
72		Finger {
73			tip: Default::default(),
74			distal: Default::default(),
75			intermediate: Default::default(),
76			proximal: Default::default(),
77			metacarpal: Default::default(),
78		}
79	}
80}
81impl Default for Thumb {
82	fn default() -> Self {
83		Thumb {
84			tip: Default::default(),
85			distal: Default::default(),
86			proximal: Default::default(),
87			metacarpal: Default::default(),
88		}
89	}
90}
91impl Default for Hand {
92	fn default() -> Self {
93		Hand {
94			right: Default::default(),
95			thumb: Default::default(),
96			index: Default::default(),
97			middle: Default::default(),
98			ring: Default::default(),
99			little: Default::default(),
100			palm: Default::default(),
101			wrist: Default::default(),
102			elbow: Default::default(),
103		}
104	}
105}
106impl Default for Pointer {
107	fn default() -> Self {
108		Pointer {
109			origin: [0.0; 3].into(),
110			orientation: Quat::IDENTITY.into(),
111			deepest_point: [0.0; 3].into(),
112		}
113	}
114}
115impl Default for Tip {
116	fn default() -> Self {
117		Tip {
118			origin: [0.0; 3].into(),
119			orientation: Quat::IDENTITY.into(),
120		}
121	}
122}
123
124// these heuristics made possible by https://github.com/ultraleap/UnityPlugin/blob/1c49cc1205ef3cae8b27b8e24e1fcf84fdad721c/Packages/Tracking/Core/Runtime/Scripts/Utils/HandUtils.cs
125// thank you Leap Motion!! you will be missed :(
126impl Finger {
127	/// length of finger from knuckle to tip
128	pub fn length(&self) -> f32 {
129		let proximal_position: Vec3A = self.proximal.position.into();
130		let intermediate_position: Vec3A = self.intermediate.position.into();
131		let distal_position: Vec3A = self.distal.position.into();
132		let tip_position: Vec3A = self.tip.position.into();
133
134		proximal_position.distance(intermediate_position)
135			+ intermediate_position.distance(distal_position)
136			+ distal_position.distance(tip_position)
137	}
138
139	pub fn direction(&self) -> Vector3<f32> {
140		let proximal_position: Vec3A = self.proximal.position.into();
141		let tip_position: Vec3A = self.tip.position.into();
142
143		(tip_position - proximal_position).normalize().into()
144	}
145}
146
147impl Thumb {
148	/// length of finger from knuckle to tip
149	pub fn length(&self) -> f32 {
150		let proximal_position: Vec3A = self.proximal.position.into();
151		let distal_position: Vec3A = self.distal.position.into();
152		let tip_position: Vec3A = self.tip.position.into();
153
154		proximal_position.distance(distal_position) + distal_position.distance(tip_position)
155	}
156
157	pub fn direction(&self) -> Vector3<f32> {
158		let proximal_position: Vec3A = self.proximal.position.into();
159		let tip_position: Vec3A = self.tip.position.into();
160
161		(tip_position - proximal_position).normalize().into()
162	}
163}
164
165impl Hand {
166	/// The direction vector pointing out of the palm
167	pub fn palm_normal(&self) -> Vector3<f32> {
168		(Quat::from(self.palm.rotation) * vec3a(0.0, -1.0, 0.0)).into()
169	}
170	/// The direction vector pointing from the palm to thumb
171	pub fn radial_axis(&self) -> Vector3<f32> {
172		(Quat::from(self.palm.rotation)
173			* if self.right {
174				vec3a(-1.0, 0.0, 0.0)
175			} else {
176				vec3a(1.0, 0.0, 0.0)
177			})
178		.into()
179	}
180	/// The direction vector pointing from the palm towards fingers.
181	pub fn distal_axis(&self) -> Vector3<f32> {
182		(Quat::from(self.palm.rotation) * vec3a(0.0, 0.0, -1.0)).into()
183	}
184
185	pub fn finger_curl(&self, finger: &Finger) -> f32 {
186		let distal_axis: Vec3A = self.distal_axis().into();
187		let direction: Vec3A = finger.direction().into();
188		direction.dot(-distal_axis).remap(-1.0, 1.0, 0.0, 1.0)
189	}
190
191	pub fn thumb_curl(&self) -> f32 {
192		let radial_axis: Vec3A = self.radial_axis().into();
193		let thumb_direction: Vec3A = self.thumb.direction().into();
194		thumb_direction.dot(-radial_axis).remap(-1.0, 1.0, 0.0, 1.0)
195	}
196
197	pub fn pinch_distance(&self, finger: &Finger) -> f32 {
198		let thumb_tip: Vec3A = self.thumb.tip.position.into();
199		let index_tip: Vec3A = finger.tip.position.into();
200		thumb_tip.distance(index_tip)
201	}
202
203	/// Unstabilized pinch position
204	pub fn pinch_position(&self) -> Vector3<f32> {
205		let thumb_tip: Vec3A = self.thumb.tip.position.into();
206		let index_tip: Vec3A = self.index.tip.position.into();
207
208		((2.0 * thumb_tip + index_tip) * 0.3333333).into()
209	}
210
211	/// Predicted Pinch Position without influence from the thumb or index tip.
212	/// Useful for calculating extremely stable pinch calculations.
213	/// Not good for visualising the pinch point - recommended to use PredictedPinchPosition instead
214	pub fn stable_pinch_position(&self) -> Vector3<f32> {
215		let index_knuckle: Vec3A = self.index.proximal.position.into();
216
217		let index_length = self.index.length();
218
219		let radial_axis: Vec3A = self.radial_axis().into();
220		let palm_normal: Vec3A = self.palm_normal().into();
221		let distal_axis: Vec3A = self.distal_axis().into();
222
223		let stable_pinch_position = index_knuckle
224			+ (palm_normal * index_length * 0.85)
225			+ (distal_axis * index_length * 0.20)
226			+ (radial_axis * index_length * 0.20);
227
228		stable_pinch_position.into()
229	}
230
231	/// A decent approximation of where the hand will pinch even if index and thumb are far apart.
232	pub fn predicted_pinch_position(&self) -> Vector3<f32> {
233		let thumb_tip: Vec3A = self.thumb.tip.position.into();
234		let index_tip: Vec3A = self.index.tip.position.into();
235
236		let index_knuckle: Vec3A = self.index.proximal.position.into();
237		let index_length = self.index.length();
238
239		let radial_axis: Vec3A = self.radial_axis().into();
240		let palm_normal: Vec3A = self.palm_normal().into();
241		let distal_axis: Vec3A = self.distal_axis().into();
242
243		let thumb_influence = (thumb_tip - index_knuckle)
244			.normalize()
245			.dot(radial_axis)
246			.remap(0.0, 1.0, 0.5, 0.0);
247
248		let mut predicted_pinch_point = index_knuckle
249			+ palm_normal * index_length * 0.85
250			+ distal_axis * index_length * 0.20
251			+ radial_axis * index_length * 0.20;
252
253		predicted_pinch_point = predicted_pinch_point.lerp(thumb_tip, thumb_influence);
254		predicted_pinch_point = predicted_pinch_point.lerp(index_tip, 0.15);
255
256		predicted_pinch_point.into()
257	}
258
259	fn hand_scale(&self) -> f32 {
260		let index_metacarpal: Vec3A = self.index.metacarpal.position.into();
261		let index_proximal: Vec3A = self.index.proximal.position.into();
262
263		let middle_metacarpal: Vec3A = self.middle.metacarpal.position.into();
264		let middle_proximal: Vec3A = self.middle.proximal.position.into();
265
266		let ring_metacarpal: Vec3A = self.ring.metacarpal.position.into();
267		let ring_proximal: Vec3A = self.ring.proximal.position.into();
268
269		let little_metacarpal: Vec3A = self.little.metacarpal.position.into();
270		let little_proximal: Vec3A = self.little.proximal.position.into();
271
272		let index_metacarpal_length = index_metacarpal.distance(index_proximal);
273		let middle_metacarpal_length = middle_metacarpal.distance(middle_proximal);
274		let ring_metacarpal_length = ring_metacarpal.distance(ring_proximal);
275		let little_metacarpal_length = little_metacarpal.distance(little_proximal);
276
277		let mut scale = 0.0;
278
279		scale += index_metacarpal_length / 0.06812;
280		scale += middle_metacarpal_length / 0.06460;
281		scale += ring_metacarpal_length / 0.05800;
282		scale += little_metacarpal_length / 0.05369;
283
284		scale / 4.0
285	}
286
287	/// Confidence value from 0-1 of how strong this hand is pinching.
288	pub fn pinch_strength(&self) -> f32 {
289		let thumb_tip: Vec3A = self.thumb.tip.position.into();
290		let index_tip: Vec3A = self.index.tip.position.into();
291		let middle_tip: Vec3A = self.middle.tip.position.into();
292		let ring_tip: Vec3A = self.ring.tip.position.into();
293		let little_tip: Vec3A = self.little.tip.position.into();
294
295		let min_distance = index_tip
296			.distance_squared(thumb_tip)
297			.min(middle_tip.distance_squared(thumb_tip))
298			.min(ring_tip.distance_squared(thumb_tip))
299			.min(little_tip.distance_squared(thumb_tip))
300			.sqrt();
301
302		let scale = self.hand_scale();
303		let distance_zero = 0.0600 * scale;
304		let distance_one = 0.0220 * scale;
305
306		((min_distance - distance_zero) / (distance_one - distance_zero)).clamp(0.0, 1.0)
307	}
308
309	/// Confidence value from 0-1 of how strong this hand is making a fist.
310	pub fn fist_strength(&self) -> f32 {
311		let radial_axis: Vec3A = self.radial_axis().into();
312		let distal_axis: Vec3A = self.distal_axis().into();
313
314		let thumb_direction: Vec3A = self.thumb.direction().into();
315		let index_direction: Vec3A = self.index.direction().into();
316		let middle_direction: Vec3A = self.middle.direction().into();
317		let ring_direction: Vec3A = self.ring.direction().into();
318		let little_direction: Vec3A = self.little.direction().into();
319
320		(thumb_direction.dot(-radial_axis)
321			+ index_direction.dot(-distal_axis)
322			+ middle_direction.dot(-distal_axis)
323			+ ring_direction.dot(-distal_axis)
324			+ little_direction.dot(-distal_axis))
325		.remap(-5.0, 5.0, 0.0, 1.0)
326	}
327}
328
329impl Pointer {
330	pub fn direction(&self) -> Vector3<f32> {
331		(Quat::from(self.orientation) * vec3a(0.0, 0.0, -1.0)).into()
332	}
333}
334
335impl Hash for InputData {
336	fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
337		self.id.hash(state)
338	}
339}
340impl PartialEq for InputData {
341	fn eq(&self, other: &Self) -> bool {
342		self.id.eq(&other.id)
343	}
344}
345impl Eq for InputData {}
346
347#[tokio::test]
348async fn fusion_input_handler() {
349	use crate::Client;
350
351	let mut client = Client::connect().await.expect("Couldn't connect");
352
353	let field = super::fields::Field::create(
354		client.get_root(),
355		Transform::identity(),
356		crate::fields::Shape::Sphere(0.1),
357	)
358	.unwrap();
359	let _input_handler =
360		InputHandler::create(client.get_root(), Transform::none(), &field).unwrap();
361
362	client
363		.sync_event_loop(|_, _| {
364			while let Some(input_event) = _input_handler.recv_input_handler_event() {
365				match input_event {
366					InputHandlerEvent::Input { methods: _, data } => on_input(data),
367				}
368			}
369		})
370		.await
371		.unwrap();
372
373	fn on_input(data: Vec<InputData>) {
374		for data in data {
375			dbg!(data.id);
376			dbg!(data.distance);
377			match &data.input {
378				InputDataType::Pointer(_) => {
379					println!("Pointer input");
380				}
381				InputDataType::Hand(_) => {
382					println!("Hand input");
383					data.datamap.with_data(|datamap| {
384						dbg!(
385							datamap
386								.iter_keys()
387								.zip(datamap.iter_values())
388								.collect::<Vec<_>>()
389						);
390						let _ = dbg!(datamap.idx("right").get_bool());
391					});
392				}
393				InputDataType::Tip(_) => {
394					println!("Tip input");
395				}
396			}
397		}
398	}
399}
400
401#[tokio::test]
402async fn fusion_pointer_input_method() {
403	use crate::Client;
404	use crate::drawable::Model;
405	use crate::root::*;
406	use crate::spatial::SpatialAspect;
407
408	let mut client = Client::connect().await.expect("Couldn't connect");
409
410	let mut fbb = stardust_xr::schemas::flex::flexbuffers::Builder::default();
411	fbb.start_map();
412	let pointer = InputMethod::create(
413		client.get_root(),
414		Transform::none(),
415		InputDataType::Pointer(Pointer::default()),
416		&Datamap::from_typed(PointerData::default()).unwrap(),
417	)
418	.unwrap();
419	let _model = Model::create(
420		&pointer,
421		Transform::from_rotation_scale(
422			glam::Quat::from_rotation_x(std::f32::consts::PI * 0.5),
423			[0.1; 3],
424		),
425		&stardust_xr::values::ResourceID::new_namespaced("fusion", "cursor_spike"),
426	)
427	.unwrap();
428	let mut datamap = PointerData::default();
429
430	#[derive(Default, serde::Serialize, serde::Deserialize)]
431	struct PointerData {
432		grab: f32,
433		select: f32,
434	}
435
436	client
437		.sync_event_loop(|client, _| {
438			while let Some(root_event) = client.get_root().recv_root_event() {
439				match root_event {
440					RootEvent::Ping { response } => {
441						response.send_ok(());
442					}
443					RootEvent::Frame { info } => {
444						let (sin, cos) = info.elapsed.sin_cos();
445						pointer
446							.set_local_transform(Transform::from_translation([
447								sin * 0.1,
448								0.0,
449								cos * 0.1,
450							]))
451							.unwrap();
452
453						datamap.grab = sin;
454						pointer
455							.set_datamap(&Datamap::from_typed(&datamap).unwrap())
456							.unwrap();
457					}
458					RootEvent::SaveState { response } => response.send_ok(Default::default()),
459				}
460			}
461		})
462		.await
463		.unwrap();
464}
465
466#[tokio::test]
467async fn fusion_tip_input_method() {
468	use crate::Client;
469	use crate::drawable::Model;
470	use crate::root::*;
471	use crate::spatial::SpatialAspect;
472
473	let mut client = Client::connect().await.expect("Couldn't connect");
474
475	let tip = InputMethod::create(
476		client.get_root(),
477		Transform::none(),
478		InputDataType::Tip(Tip::default()),
479		&Datamap::from_typed(TipData::default()).unwrap(),
480	)
481	.unwrap();
482
483	fn summon_model(parent: &impl SpatialAspect, rotation: glam::Quat) -> Model {
484		Model::create(
485			parent,
486			Transform::from_rotation_scale(rotation, [0.1; 3]),
487			&stardust_xr::values::ResourceID::new_namespaced("fusion", "cursor_spike"),
488		)
489		.unwrap()
490	}
491
492	struct Cursor {
493		top: Model,
494		bottom: Model,
495		left: Model,
496		right: Model,
497		forward: Model,
498		backward: Model,
499	}
500	#[derive(Default, serde::Serialize, serde::Deserialize)]
501	struct TipData {
502		grab: f32,
503		select: f32,
504	}
505	let mut datamap = TipData::default();
506
507	client
508		.sync_event_loop(|client, _| {
509			while let Some(root_event) = client.get_root().recv_root_event() {
510				match root_event {
511					RootEvent::Ping { response } => {
512						response.send_ok(());
513					}
514					RootEvent::Frame { info } => {
515						let (sin, cos) = info.elapsed.sin_cos();
516						tip.set_local_transform(Transform::from_translation([
517							sin * 0.1,
518							0.0,
519							cos * 0.1,
520						]))
521						.unwrap();
522
523						datamap.grab = sin;
524						tip.set_datamap(&Datamap::from_typed(&datamap).unwrap())
525							.unwrap();
526					}
527					RootEvent::SaveState { response } => response.send_ok(Default::default()),
528				}
529			}
530		})
531		.await
532		.unwrap();
533}