diff --git a/Cargo.lock b/Cargo.lock index 31fb865..f3ab887 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -139,10 +139,10 @@ dependencies = [ name = "color_cube" version = "0.1.0" dependencies = [ + "glam 0.27.0", "manifest-dir-macros", "mint", "stardust-xr-fusion", - "stardust-xr-molecules", "tokio", ] @@ -250,6 +250,15 @@ dependencies = [ "mint", ] +[[package]] +name = "glam" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e05e7e6723e3455f4818c7b26e855439f7546cf617ef669d1adedb8669e5cb9" +dependencies = [ + "mint", +] + [[package]] name = "global_counter" version = "0.2.2" @@ -320,15 +329,6 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -[[package]] -name = "lerp" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc56a024593ecbcacb6bb4f8f4ace719eb08ae9b701535640ef3efb0e706260" -dependencies = [ - "num-traits", -] - [[package]] name = "libc" version = "0.2.153" @@ -367,15 +367,6 @@ dependencies = [ "syn 2.0.60", ] -[[package]] -name = "map-range" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bffa558d7f51b549670be5ff6db164cd9be428c035cbf4e3f782db3da66845a5" -dependencies = [ - "num-traits", -] - [[package]] name = "memchr" version = "2.7.2" @@ -852,24 +843,6 @@ dependencies = [ "stardust-xr-schemas", ] -[[package]] -name = "stardust-xr-molecules" -version = "0.45.0" -source = "git+https://github.com/StardustXR/molecules#263e19e10fb4af839809e4d1f79717247909175c" -dependencies = [ - "color-rs", - "glam 0.25.0", - "lazy_static", - "lerp", - "map-range", - "mint", - "rustc-hash", - "serde", - "stardust-xr-fusion", - "tokio", - "tracing", -] - [[package]] name = "stardust-xr-schemas" version = "1.5.3" diff --git a/Cargo.toml b/Cargo.toml index ca48a0e..64b7356 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,4 +12,4 @@ mint = "0.5.9" tokio = { version = "1.37.0", features = ["full"] } stardust-xr-fusion = { git = "https://github.com/StardustXR/core" } -stardust-xr-molecules = { git = "https://github.com/StardustXR/molecules" } +glam = { version = "0.27.0", features = ["mint"] } diff --git a/assets/color_cube.blend b/assets/color_cube.blend index 6f9a753..f8d1154 100644 Binary files a/assets/color_cube.blend and b/assets/color_cube.blend differ diff --git a/assets/color_cube.blend1 b/assets/color_cube.blend1 index d1edeae..ae39d05 100644 Binary files a/assets/color_cube.blend1 and b/assets/color_cube.blend1 differ diff --git a/res/color_cube/color_cube.glb b/res/color_cube/color_cube.glb index 83018a4..6479f5d 100644 Binary files a/res/color_cube/color_cube.glb and b/res/color_cube/color_cube.glb differ diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..33ff77d --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,3 @@ +# this file makes sure the coding conventions that suit the project well are enforced +hard_tabs = true +imports_granularity = "Crate" \ No newline at end of file diff --git a/src/color_cube.rs b/src/color_cube.rs index fc3e8b1..0bd4eaf 100644 --- a/src/color_cube.rs +++ b/src/color_cube.rs @@ -1,83 +1,121 @@ -use stardust_xr_fusion::drawable::Model; +use glam::Vec3; +use stardust_xr_fusion::drawable::{MaterialParameter, Model, ModelPart, ModelPartAspect}; +use stardust_xr_fusion::input::InputMethodAspect; use stardust_xr_fusion::{ - client::{Client, FrameInfo}, - core::values::{rgba_linear, Color, ResourceID}, - fields::BoxField, - input::{InputData, InputHandler, InputHandlerAspect, InputHandlerHandler, InputMethod}, - node::NodeType, - spatial::{SpatialAspect, Transform}, - HandlerWrapper, + client::{Client, FrameInfo}, + core::values::{rgba_linear, Color, ResourceID}, + fields::BoxField, + input::{ + InputData, InputDataType, InputHandler, InputHandlerAspect, InputHandlerHandler, + InputMethod, + }, + node::NodeType, + spatial::{SpatialAspect, Transform}, + HandlerWrapper, }; -use stardust_xr_molecules::{Grabbable, GrabbableSettings}; use std::collections::HashMap; +// you need to handle every type of input method, multimodal after all! +fn is_grabbing(input: &InputData) -> bool { + match &input.input { + InputDataType::Hand(h) => { + // hands have pinch_strength and grab_strenght data but i figured you should know how to get raw joints + Vec3::from(h.index.tip.position).distance(Vec3::from(h.thumb.tip.position)) < 0.005 + } + // pointers and tips (tips are like controllers, pens, spatial cursors) have datamaps full of abstract actions (e.g. select) and raw input info + _ => input.datamap.with_data(|d| d.idx("select").as_f32()) > 0.9, + } +} +fn interact_point(input: &InputData) -> Vec3 { + match &input.input { + InputDataType::Pointer(p) => [0.0; 3].into(), + // since we're outputting a glam vec3 (glam is a math library that's similar to stereokit's) we gotta convert using `Vec3::from()` or `.into()` to convert to the inferred type + InputDataType::Hand(h) => h.index.tip.position.into(), + InputDataType::Tip(t) => t.origin.into(), + } +} + pub struct ColorCube { - model: Model, - field: BoxField, - handler: InputHandler, - inputs: HashMap, - hover_color: Color, + model: Model, + circle: ModelPart, + field: BoxField, + handler: InputHandler, + inputs: HashMap, + hover_color: Color, } - impl ColorCube { - pub async fn create(client: &Client) -> HandlerWrapper { - let model = Model::create( - client.get_root(), - Transform::identity(), - &ResourceID::new_namespaced("color_cube", "color_cube"), - ) - .unwrap(); + pub async fn create(client: &Client) -> HandlerWrapper { + // make the model + let model = Model::create( + // everything in stardust is relative to something else, client root is a great start! + client.get_root(), + // identity transform relative to it, we can assume the root's up direction will match gravity but not its Y rotation + Transform::identity(), + // we want to use a namespaced resource so it can be themed! since this might have its asset swapped you should generally use the model as a base for sizing and positioning of things + &ResourceID::new_namespaced("color_cube", "color_cube"), + ) + // unwrap basically will shut down the program (nicely) if this fails, given it's an essential part of this client that's ok + .unwrap(); + // to set material properties we have to get the model part (same as stereokit model node) + let circle = model.model_part("Circle").unwrap(); - let bounds = model.get_local_bounding_box().await.unwrap(); - let field = BoxField::create(&model, Transform::none(), [0.4; 3]).unwrap(); - let hover_color = rgba_linear!(0.5, 0.5, 0.5, 1.0); + // get the bounding box (center and size) of the model and its children. You have to call `await` because this is async and we don't want to cause the whole program to block waiting for a response. + let bounds = model.get_local_bounding_box().await.unwrap(); + // now make the box field (fields because unlike colliders they have more information than "colliding" or "not colliding"). + let field = BoxField::create( + &model, + // we also use the bounds size and center here + Transform::from_translation(bounds.center), + bounds.size, + ) + .unwrap(); + let hover_color = rgba_linear!(0.5, 0.5, 0.5, 1.0); - let handler = InputHandler::create(&model, Transform::none(), &field).unwrap(); - let color_cube = ColorCube { - model, - field, - handler: handler.alias(), - inputs: Default::default(), - hover_color, - }; - handler.wrap(color_cube).unwrap() - } + // input data is all spatially relative to the handler itself, but you can move the field independent of the handler. Makes certain things like grabbing much easier when you can put the grab input handler at the client root. + let handler = InputHandler::create(&model, Transform::none(), &field).unwrap(); + let color_cube = ColorCube { + model, + circle, + field, + handler: handler.alias(), + inputs: Default::default(), + hover_color, + }; + // because the server could send events for the client to process related to the input handler, we bundle them together in a HandlerWrapper so they are synced up and locked together + handler.wrap(color_cube).unwrap() + } - pub fn update(&mut self, info: &FrameInfo) { - // for (data, method) in self.inputs.iter() { - // data.datamap.with_data(|data| { - // data.idx("color_hover").as_f32() - // }); - // data.captured = true; - // method.capture(&self.handler).unwrap(); - // } + pub fn update(&mut self, info: &FrameInfo) { + // we have all the input methods (e.g. hands) in range queued up in `self.inputs`. - self.inputs.clear(); - } + // iterate through every input method for this frame + for (data, method) in self.inputs.iter() { + if is_grabbing(data) { + // call this method when you want to make sure the hand is only trying to interact with this handler + method.request_capture(&self.handler).unwrap(); + } + // then check every frame until it is successfully captured (but it might not be ever) + if data.captured { + // since we don't want the program to quit if this fails, use `let _ = ` to ignore the result + let _ = self + .circle + .set_material_parameter("color", MaterialParameter::Color(self.hover_color)); + } + // we gotta remove that color too sometime lol + } - // fn hover(size: Vector2, point: Vector3) -> bool { - // point.x.abs() < size.x && point.y.abs() < size.y - // } - // fn hover_action(input: &InputData, state: &State) -> bool { - // match &input.input { - // InputDataType::Pointer(_) => input.distance < 0.0, - // InputDataType::Hand(h) => Self::hover(state.size, h.index.tip.position), - // InputDataType::Tip(t) => Self::hover(state.size, t.origin), - // } - // } - // fn hover_point(&self, input: &InputData) -> Vector3 { - // let hover_point = match &input.input { - // InputDataType::Pointer(p) => [0.0; 3].into(), - // InputDataType::Hand(h) => h.index.tip.position, - // InputDataType::Tip(t) => t.origin, - // }; + // clear the queue for next time + self.inputs.clear(); - // return hover_point; - // } + // we could also use the following to iterate over every input and clear the map at the same time + for (data, method) in self.inputs.drain() {} + } } +// the stardust server sends a message to fusion (the library) which then calls these methods impl InputHandlerHandler for ColorCube { - fn input(&mut self, input: InputMethod, data: InputData) { - self.inputs.insert(data, input); - } + // an input method is sending data to you this frame, when `RootHandler::frame` is called you can assume it's all been sent + fn input(&mut self, input: InputMethod, data: InputData) { + self.inputs.insert(data, input); + } } diff --git a/src/main.rs b/src/main.rs index d0b5e73..cd7c6ab 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,52 +3,46 @@ use color_cube::ColorCube; use manifest_dir_macros::directory_relative_path; use stardust_xr_fusion::{ - client::{Client, FrameInfo, RootHandler}, - input::InputHandler, - HandlerWrapper, + client::{Client, ClientState, FrameInfo, RootHandler}, + input::InputHandler, + HandlerWrapper, }; struct Root(HandlerWrapper); +// rust doesn't have inheritance so we add a trait to a struct to implement functionality impl RootHandler for Root { - fn frame(&mut self, info: FrameInfo) { - self.0.lock_wrapped().update(&info); - // self.grabbable.update(&info).unwrap(); - - // self.input_handler.lock_wrapped().update_actions([ - // &mut self.color_hover, - // ]); - - // for input_data in self - // .color_hover - // .started - - // if self.color_hover.currently_acting.is_empty() { - // self.model.set_local_transform( - // Transform::from_scale([1.1; 3]) - // ).unwrap(); - // } else { - // self.model.set_local_transform( - // Transform::from_scale([1.0; 3]) - // ).unwrap(); - // } - } - - fn save_state(&mut self) -> stardust_xr_fusion::client::ClientState { - todo!("implement save state") - } + fn frame(&mut self, info: FrameInfo) { + // delta time in seconds, very handy :) + dbg!(info.delta); + // elapsed time since the client started, server doesn't send this over but it's calculated locally + dbg!(info.elapsed); + // you need to call .lock_wrapped() because the ColorCube struct is inside a mutex inside the HandlerWrapper + self.0.lock_wrapped().update(&info); + } + // the goal here is to put everything you need in `ClientState` so on next launch you could restore it from `Client::get_state()` + // this todo will make the program panic when the server is about to shut it down meaning it will not be launched on restore + // basically this isn't spatially persistent + fn save_state(&mut self) -> ClientState { + todo!("implement save state") + } } #[tokio::main] async fn main() { - let (client, event_loop) = Client::connect_with_async_loop().await.unwrap(); - client.set_base_prefixes(&[directory_relative_path!("res")]); + // automatically connects to the first stardust server it can + let (client, event_loop) = Client::connect_with_async_loop().await.unwrap(); + // all usages of ResourceID will be relative to the given directory first and foremost excluding themes. This can be overridden using the STARDUST_RES_PREFIXES env var at build time (comma separated absolute paths) + client.set_base_prefixes(&[directory_relative_path!("res")]); - let _root = client - .wrap_root(Root(ColorCube::create(&client).await)) - .unwrap(); + let _root = client + .wrap_root(Root(ColorCube::create(&client).await)) + .unwrap(); - tokio::select! { - _ = tokio::signal::ctrl_c() => (), - _ = event_loop => panic!("server crashed"), - } + // async waits for either thing to happen + tokio::select! { + // maybe someone tried to close the program via ctrl+c + _ = tokio::signal::ctrl_c() => (), + // this is kinda a bug since the server has no official "shutting down" signal + _ = event_loop => panic!("server crashed"), + } }