stardust_xr_fusion/
spatial.rs

1//! Nodes that represent spatial objects and zones to manipulate certain spatials from other clients.
2//!
3//! Spatials are part of most nodes such as fields and models, but can be created on their own.
4//! They include a parent, transform, and zoneable boolean.
5//! They're an infinitely small point in space with a translation, rotation, and scale, so they're invisible.
6//!
7//! In Stardust, everything is relative to something else spatially.
8//! In the case of creating your first spatials in your client, it'll be relative to the HMD or the client's root.
9//! Clients can be spawned in with a root at a spatial's transform using the `StartupSettings` node.
10//!
11//! Zones are nodes that can transform any spatial inside their field with the zoneable property set to true.
12//! They're very useful for grabbing large collections of objects at once and arranging them into a grid or for workspaces.
13//! Zones can set the transform of any spatials they see.
14//! Zones can capture spatials, temporarily parenting them to the zone until they are released.
15//! Zones can see zoneable spatials if they're closer to the surface of the field than any zone that captured them, so no zones can steal and hoard them.
16
17pub use crate::protocol::spatial::*;
18use crate::{client::ClientHandle, fields::FieldAspect, node::NodeResult};
19use stardust_xr::values::*;
20use std::{hash::Hash, sync::Arc};
21
22impl Transform {
23	pub const fn none() -> Self {
24		Transform {
25			translation: None,
26			rotation: None,
27			scale: None,
28		}
29	}
30	pub const fn identity() -> Self {
31		Transform {
32			translation: Some(Vector3 {
33				x: 0.0,
34				y: 0.0,
35				z: 0.0,
36			}),
37			rotation: Some(Quaternion {
38				v: Vector3 {
39					x: 0.0,
40					y: 0.0,
41					z: 0.0,
42				},
43				s: 1.0,
44			}),
45			scale: Some(Vector3 {
46				x: 1.0,
47				y: 1.0,
48				z: 1.0,
49			}),
50		}
51	}
52
53	pub fn from_translation(translation: impl Into<Vector3<f32>>) -> Self {
54		Transform {
55			translation: Some(translation.into()),
56			rotation: None,
57			scale: None,
58		}
59	}
60	pub fn from_rotation(rotation: impl Into<Quaternion>) -> Self {
61		Transform {
62			translation: None,
63			rotation: Some(rotation.into()),
64			scale: None,
65		}
66	}
67	pub fn from_scale(scale: impl Into<Vector3<f32>>) -> Self {
68		Transform {
69			translation: None,
70			rotation: None,
71			scale: Some(scale.into()),
72		}
73	}
74
75	pub fn from_translation_rotation(
76		translation: impl Into<Vector3<f32>>,
77		rotation: impl Into<Quaternion>,
78	) -> Self {
79		Transform {
80			translation: Some(translation.into()),
81			rotation: Some(rotation.into()),
82			scale: None,
83		}
84	}
85	pub fn from_rotation_scale(
86		rotation: impl Into<Quaternion>,
87		scale: impl Into<Vector3<f32>>,
88	) -> Self {
89		Transform {
90			translation: None,
91			rotation: Some(rotation.into()),
92			scale: Some(scale.into()),
93		}
94	}
95
96	pub fn from_translation_scale(
97		translation: impl Into<Vector3<f32>>,
98		scale: impl Into<Vector3<f32>>,
99	) -> Self {
100		Transform {
101			translation: Some(translation.into()),
102			rotation: None,
103			scale: Some(scale.into()),
104		}
105	}
106
107	pub fn from_translation_rotation_scale(
108		translation: impl Into<Vector3<f32>>,
109		rotation: impl Into<Quaternion>,
110		scale: impl Into<Vector3<f32>>,
111	) -> Self {
112		Transform {
113			translation: Some(translation.into()),
114			rotation: Some(rotation.into()),
115			scale: Some(scale.into()),
116		}
117	}
118}
119impl Copy for Transform {}
120impl Hash for Transform {
121	fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
122		if let Some(translation) = &self.translation {
123			translation.x.to_bits().hash(state);
124			translation.y.to_bits().hash(state);
125			translation.z.to_bits().hash(state);
126		}
127		if let Some(rotation) = &self.rotation {
128			rotation.v.x.to_bits().hash(state);
129			rotation.v.y.to_bits().hash(state);
130			rotation.v.z.to_bits().hash(state);
131			rotation.s.to_bits().hash(state);
132		}
133		if let Some(scale) = &self.scale {
134			scale.x.to_bits().hash(state);
135			scale.y.to_bits().hash(state);
136			scale.z.to_bits().hash(state);
137		}
138	}
139}
140
141impl SpatialRef {
142	pub async fn import(client: &Arc<ClientHandle>, uid: u64) -> NodeResult<Self> {
143		import_spatial_ref(client, uid).await
144	}
145}
146
147impl Spatial {
148	pub fn create(
149		spatial_parent: &impl SpatialRefAspect,
150		transform: Transform,
151		zoneable: bool,
152	) -> NodeResult<Self> {
153		let client = spatial_parent.client();
154		create_spatial(
155			client,
156			client.generate_id(),
157			spatial_parent,
158			transform,
159			zoneable,
160		)
161	}
162}
163
164impl Zone {
165	pub fn create(
166		spatial_parent: &impl SpatialRefAspect,
167		transform: Transform,
168		field: &impl FieldAspect,
169	) -> NodeResult<Self> {
170		let client = spatial_parent.client();
171		create_zone(
172			client,
173			client.generate_id(),
174			spatial_parent,
175			transform,
176			field,
177		)
178	}
179}
180
181// TODO: write tests to ensure transform order and such is correct
182
183#[tokio::test]
184async fn fusion_spatial() {
185	use crate::Client;
186	let client = Client::connect().await.expect("Couldn't connect");
187	let spatial = Spatial::create(
188		client.get_root(),
189		Transform::from_translation_scale([1.0, 0.5, 0.1], [0.5, 0.5, 0.5]),
190		false,
191	)
192	.unwrap();
193	let bounding_box = spatial
194		.get_relative_bounding_box(client.get_root())
195		.await
196		.unwrap();
197	assert_eq!(bounding_box.center, [1.0, 0.5, 0.1].into());
198	assert_eq!(bounding_box.size, [0.0; 3].into());
199}
200
201#[tokio::test]
202async fn fusion_spatial_import_export() {
203	use crate::Client;
204	let client = Client::connect().await.expect("Couldn't connect");
205	let exported = Spatial::create(
206		client.get_root(),
207		Transform::from_translation_scale([1.0, 0.5, 0.1], [0.5, 0.5, 0.5]),
208		false,
209	)
210	.unwrap();
211	let uid = exported.export_spatial().await.unwrap();
212	let imported = SpatialRef::import(&client.handle(), uid).await.unwrap();
213	let relative_transform = imported.get_transform(&exported).await.unwrap();
214	assert_eq!(relative_transform, Transform::identity());
215}
216
217#[tokio::test]
218async fn fusion_zone() {
219	use crate::node::NodeType;
220	use crate::root::*;
221	let mut client = crate::Client::connect().await.expect("Couldn't connect");
222
223	let root = crate::spatial::Spatial::create(client.get_root(), Transform::none(), true).unwrap();
224
225	let gyro_gem = stardust_xr::values::ResourceID::new_namespaced("fusion", "gyro_gem");
226	let _model = crate::drawable::Model::create(&root, Transform::none(), &gyro_gem).unwrap();
227
228	let field = crate::fields::Field::create(
229		client.get_root(),
230		Transform::identity(),
231		crate::fields::Shape::Sphere(0.1),
232	)
233	.unwrap();
234
235	let mut zone_spatials: rustc_hash::FxHashMap<u64, SpatialRef> = Default::default();
236
237	let zone = Zone::create(client.get_root(), Transform::none(), &field).unwrap();
238
239	let event_loop = client.sync_event_loop(|client, stop| {
240		while let Some(event) = client.get_root().recv_root_event() {
241			match event {
242				RootEvent::Ping { response } => {
243					response.send_ok(());
244				}
245				RootEvent::Frame { info: _ } => zone.update().unwrap(),
246				RootEvent::SaveState { response } => response.send_ok(ClientState::default()),
247			}
248		}
249		while let Some(zone_event) = zone.recv_zone_event() {
250			match zone_event {
251				ZoneEvent::Enter { spatial } => {
252					println!("Spatial {spatial:?} entered zone");
253					zone.capture(&spatial).unwrap();
254					zone_spatials.insert(spatial.id(), spatial);
255				}
256				ZoneEvent::Capture { spatial } => {
257					println!("Spatial {spatial:?} was captured");
258					zone.release(&spatial).unwrap();
259				}
260				ZoneEvent::Release { id } => {
261					println!("Spatial {id} was released");
262					root.set_local_transform(Transform::from_translation([0.0, 1.0, 0.0]))
263						.unwrap();
264					zone.update().unwrap();
265				}
266				ZoneEvent::Leave { id } => {
267					println!("Spatial {id} left zone");
268					stop.stop();
269				}
270			}
271		}
272	});
273	tokio::time::timeout(std::time::Duration::from_secs(1), event_loop)
274		.await
275		.unwrap()
276		.unwrap();
277}