diff --git a/Cargo.toml b/Cargo.toml index b38c533..33657d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,10 +51,16 @@ auto_link_exclude_list = [ ] [dependencies] -bevy = { version = "0.15", features = ["wayland", "mp3", "wav", "trace_tracy"] } -bevy_mod_xr = { git = "https://github.com/Schmarni-Dev/bevy_openxr", branch = "0.15rc" } -bevy_mod_openxr = { git = "https://github.com/Schmarni-Dev/bevy_openxr", branch = "0.15rc" } -bevy_xr_utils = { git = "https://github.com/Schmarni-Dev/bevy_openxr", branch = "0.15rc" } +bevy = { version = "0.15.1", features = [ + "wayland", + "mp3", + "wav", + "trace_tracy", +] } +bevy_mod_xr = { git = "https://github.com/Schmarni-Dev/bevy_openxr", branch = "wgpu_feature_fix" } +bevy_mod_openxr = { git = "https://github.com/Schmarni-Dev/bevy_openxr", branch = "wgpu_feature_fix" } +bevy_xr_utils = { git = "https://github.com/Schmarni-Dev/bevy_openxr", branch = "wgpu_feature_fix" } +bevy_mod_meshtext.git = "https://github.com/Schmarni-Dev/bevy_mod_meshtext" openxr = "0.19" winit = { version = "0.30", default-features = false, features = [] } # small utility thingys @@ -97,6 +103,7 @@ directories = "5.0.1" xkbcommon-rs = "0.1.0" thiserror = "2.0.9" crossbeam-channel = "0.5.14" +atomicow = "1.0.0" # wayland # wayland-scanner = "0.31.2" diff --git a/src/FiraMono-subset.ttf b/src/FiraMono-subset.ttf new file mode 100644 index 0000000..6989c2e Binary files /dev/null and b/src/FiraMono-subset.ttf differ diff --git a/src/main.rs b/src/main.rs index 772dbc1..6fbac34 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,13 +17,14 @@ use bevy::app::{ App, AppExit, PluginGroup, PluginsState, PostUpdate, ScheduleRunnerPlugin, Startup, TerminalCtrlCHandlerPlugin, Update, }; -use bevy::asset::{AssetPlugin, AssetServer, Handle}; +use bevy::asset::{load_internal_binary_asset, AssetApp as _, AssetPlugin, AssetServer, Handle}; use bevy::audio::AudioPlugin; use bevy::color::Color; use bevy::core_pipeline::{CorePipelinePlugin, Skybox}; use bevy::gizmos::GizmoPlugin; use bevy::gltf::GltfPlugin; use bevy::image::Image; +use bevy::input::InputPlugin; use bevy::log::LogPlugin; use bevy::pbr::{PbrPlugin, StandardMaterial}; use bevy::prelude::{ @@ -34,11 +35,13 @@ use bevy::prelude::{ use bevy::render::pipelined_rendering::PipelinedRenderingPlugin; use bevy::render::RenderPlugin; use bevy::scene::ScenePlugin; +use bevy::text::{Font, FontLoader, TextPlugin}; use bevy::time::Time; use bevy::utils::default; use bevy::window::WindowPlugin; use bevy::winit::{EventLoopProxyWrapper, WakeUp, WinitPlugin}; use bevy::{DefaultPlugins, MinimalPlugins}; +use bevy_mod_meshtext::MeshTextPlugin; use bevy_mod_openxr::action_set_syncing::{OxrActionSyncingPlugin, OxrSyncActionSet}; use bevy_mod_openxr::exts::OxrExtensions; use bevy_mod_openxr::features::overlay::{OxrOverlaySessionEvent, OxrOverlaySettings}; @@ -267,6 +270,7 @@ fn bevy_loop( .disable::() .add(TransformPlugin) .add(HierarchyPlugin) + .add(InputPlugin) .add(AccessibilityPlugin); base = match headless { true => { @@ -343,15 +347,25 @@ fn bevy_loop( bevy_app.add_event::(); bevy_app.add_plugins(bevy_xr_utils::hand_gizmos::HandGizmosPlugin); } + bevy_app + .init_asset::() + .init_asset_loader::(); + bevy_app.add_plugins(MeshTextPlugin); bevy_app.add_plugins(StardustBevyPlugin); bevy_app.add_plugins(( BevyLinesPlugin, StardustModelPlugin, StardustHandPlugin, - // StardustTextPlugin, + StardustTextPlugin, StardustSoundPlugin, StardustControllerPlugin, )); + load_internal_binary_asset!( + bevy_app, + Handle::default(), + "FiraMono-subset.ttf", + |bytes: &[u8], _path: String| { Font::try_from_bytes(bytes.to_vec()).unwrap() } + ); #[derive(Resource)] struct SkyTexture(Handle); // Skytex/light stuff @@ -482,7 +496,7 @@ fn bevy_loop( .flatten() .map(|mut waiter| { TOKIO.spawn_blocking(move || { - let _span = debug_span!("eeping").entered(); + let _span = debug_span!("frame eeping").entered(); let result = waiter.wait(); (waiter, result) }) diff --git a/src/nodes/drawable/lines.rs b/src/nodes/drawable/lines.rs index 9e1800a..78574fe 100644 --- a/src/nodes/drawable/lines.rs +++ b/src/nodes/drawable/lines.rs @@ -162,8 +162,9 @@ pub fn draw_all( mut meshes: ResMut>, mut materials: ResMut>, mut cmds: Commands, - hmd: Single<&GlobalTransform, With>, + hmd: Option>>, ) { + let Some(hmd) = hmd else { return }; let material = StandardMaterial { base_color: Color::WHITE, alpha_mode: AlphaMode::Blend, diff --git a/src/nodes/drawable/model.rs b/src/nodes/drawable/model.rs index 362e4dd..df72f23 100644 --- a/src/nodes/drawable/model.rs +++ b/src/nodes/drawable/model.rs @@ -9,14 +9,15 @@ use crate::nodes::spatial::Spatial; use crate::nodes::Node; use crate::DefaultMaterial; use bevy::app::{Plugin, PostUpdate, PreUpdate, Update}; -use bevy::asset::{AssetServer, Assets}; +use bevy::asset::{AssetServer, Assets, Handle}; use bevy::color::{Color, LinearRgba}; use bevy::core::Name; use bevy::gltf::GltfAssetLabel; +use bevy::image::Image; use bevy::math::bounding::Aabb3d; use bevy::pbr::MeshMaterial3d; use bevy::prelude::{ - BuildChildrenTransformExt, Children, Commands, Component, Deref, Entity, Has, + AlphaMode, BuildChildrenTransformExt, Children, Commands, Component, Deref, Entity, Has, HierarchyQueryExt, Parent, Query, Res, ResMut, Resource, Transform, Visibility, With, Without, }; use bevy::reflect::GetField; @@ -42,6 +43,7 @@ impl MaterialParameter { client: &Client, material: &mut DefaultMaterial, parameter_name: &str, + asset_server: &AssetServer, ) { match self { MaterialParameter::Bool(val) => match parameter_name { @@ -58,7 +60,7 @@ impl MaterialParameter { if let Some(field) = material.get_field_mut::(name) { *field = *val; } else { - warn!("unknown bool material parameter name: {name}"); + warn!("unknown i32 material parameter name: {name}"); } } }, @@ -67,16 +69,23 @@ impl MaterialParameter { if let Some(field) = material.get_field_mut::(name) { *field = *val; } else { - warn!("unknown bool material parameter name: {name}"); + warn!("unknown u32 material parameter name: {name}"); } } }, MaterialParameter::Float(val) => match parameter_name { + "cutoff" => { + // should this only set the value if AlphaMode is already AlphaMode::Mask? + material.alpha_mode = AlphaMode::Mask(*val); + } + "metallic" => { + material.metallic = *val; + } name => { if let Some(field) = material.get_field_mut::(name) { *field = *val; } else { - warn!("unknown bool material parameter name: {name}"); + warn!("unknown f32 material parameter name: {name}"); } } }, @@ -85,7 +94,7 @@ impl MaterialParameter { if let Some(field) = material.get_field_mut::(name) { *field = (*val).into(); } else { - warn!("unknown bool material parameter name: {name}"); + warn!("unknown vec2 material parameter name: {name}"); } } }, @@ -94,7 +103,7 @@ impl MaterialParameter { if let Some(field) = material.get_field_mut::(name) { *field = (*val).into(); } else { - warn!("unknown bool material parameter name: {name}"); + warn!("unknown vec3 material parameter name: {name}"); } } }, @@ -102,25 +111,46 @@ impl MaterialParameter { "color" => { material.base_color = LinearRgba::new(val.c.r, val.c.g, val.c.b, val.a).into() } + "emission_factor" => { + material.emissive = LinearRgba::new(val.c.r, val.c.g, val.c.b, val.a) + } name => { if let Some(field) = material.get_field_mut::(name) { *field = LinearRgba::new(val.c.r, val.c.g, val.c.b, val.a).into(); } else { - warn!("unknown bool material parameter name: {name}"); + warn!("unknown color material parameter name: {name}"); } } }, MaterialParameter::Texture(resource) => { - match parameter_name { - name => { - warn!("unknown texture material parameter name: {name}"); - } - } let Some(texture_path) = get_resource_file(resource, client, &[OsStr::new("png"), OsStr::new("jpg")]) else { return; }; + let image = asset_server.load::(texture_path); + match parameter_name { + "diffuse" => { + material.base_color_texture.replace(image); + } + "emission" => { + material.emissive_texture.replace(image); + } + "normal" => { + material.normal_map_texture.replace(image); + } + "occlusion" => { + material.occlusion_texture.replace(image); + } + // TODO: impl metalic and roughness textures, they are combined in bevy + name => { + if let Some(field) = material.get_field_mut::>>(name) { + field.replace(image); + } else { + warn!("unknown texture material parameter name: {name}"); + } + } + } error!("TODO: implement texture changing"); } } @@ -193,16 +223,18 @@ fn update_model_parts( mut part_query: Query<( &mut Transform, &mut MeshMaterial3d, + &mut Visibility, Has, )>, mut cmds: Commands, + asset_server: Res, ) { for model in &models { let Some(model) = model.0.upgrade() else { continue; }; for part in model.parts.lock().iter() { - let Some((entity, (mut transform, mut mat, has_parent))) = part + let Some((entity, (mut transform, mut mat, mut vis, has_parent))) = part .entity .get() .and_then(|e| Some((*e, part_query.get_mut(*e).ok()?))) @@ -213,6 +245,12 @@ fn update_model_parts( cmds.entity(entity).remove_parent_in_place(); } *transform = Transform::from_matrix(part.space.global_transform()); + if let Some(node) = part.space.node() { + *vis = match node.enabled() { + true => Visibility::Inherited, + false => Visibility::Hidden, + } + } // todo: find all materials with identical parameters and batch them into 1 material again 'mat_params: { @@ -231,6 +269,7 @@ fn update_model_parts( &client, &mut new_material, ¶meter_name, + &asset_server, ); } mat.0 = mats.add(new_material); diff --git a/src/nodes/drawable/text.rs b/src/nodes/drawable/text.rs index 9bd75d8..f46c1f9 100644 --- a/src/nodes/drawable/text.rs +++ b/src/nodes/drawable/text.rs @@ -1,61 +1,50 @@ use crate::{ - bevy_plugin::convert_linear_rgba, + bevy_plugin::{convert_linear_rgba, DESTROY_ENTITY}, core::{ client::Client, - destroy_queue, error::{Result, ServerError}, registry::Registry, resource::get_resource_file, }, - nodes::{spatial::Spatial, Aspect, Node}, + nodes::{spatial::Spatial, Node}, DefaultMaterial, }; use bevy::{ app::{App, Plugin, PostUpdate, PreUpdate}, - asset::{AssetServer, Assets, RenderAssetUsages}, - color::Color, - image::Image, + asset::{AssetServer, Assets}, pbr::MeshMaterial3d, - prelude::{ - default, BuildChildren as _, Camera, Camera2d, ChildBuild as _, Commands, Deref, Entity, - Mesh, Mesh3d, Plane3d, Query, Res, ResMut, Resource, Transform, - }, - render::{ - camera::RenderTarget, - render_resource::{Extent3d, TextureDimension, TextureFormat, TextureUsages}, - }, - text::{cosmic_text::Align, JustifyText, TextColor, TextFont}, - ui::{AlignItems, BackgroundColor, FlexDirection, JustifyContent, TargetCamera, Val}, + prelude::{Commands, Deref, Entity, Query, Res, ResMut, Resource, Transform}, }; -use glam::{vec3, Mat4, Vec2, Vec3}; +use bevy_mod_meshtext::{HorizontalLayout, MeshText, MeshTextFont, VerticalLayout}; use once_cell::sync::OnceCell; use parking_lot::Mutex; use std::{ffi::OsStr, path::PathBuf, sync::Arc}; -use tracing::{info, info_span}; +use tracing::info_span; use super::{TextAspect, TextStyle}; static TEXT_REGISTRY: Registry = Registry::new(); -// fn convert_align(x_align: super::XAlign, y_align: super::YAlign) -> bevy::text::Text2d { -// match (x_align, y_align) { -// (super::XAlign::Left, super::YAlign::Top) => TextAlign::TopLeft, -// (super::XAlign::Left, super::YAlign::Center) => TextAlign::CenterLeft, -// (super::XAlign::Left, super::YAlign::Bottom) => TextAlign::BottomLeft, -// (super::XAlign::Center, super::YAlign::Top) => TextAlign::Center, -// (super::XAlign::Center, super::YAlign::Center) => TextAlign::Center, -// (super::XAlign::Center, super::YAlign::Bottom) => TextAlign::BottomCenter, -// (super::XAlign::Right, super::YAlign::Top) => TextAlign::TopRight, -// (super::XAlign::Right, super::YAlign::Center) => TextAlign::CenterRight, -// (super::XAlign::Right, super::YAlign::Bottom) => TextAlign::BottomRight, -// } -// } +const fn convert_align_x(x_align: super::XAlign) -> HorizontalLayout { + match x_align { + super::XAlign::Left => HorizontalLayout::Left, + super::XAlign::Center => HorizontalLayout::Centered, + super::XAlign::Right => HorizontalLayout::Right, + } +} +const fn convert_align_y(y_align: super::YAlign) -> VerticalLayout { + match y_align { + super::YAlign::Top => VerticalLayout::Top, + super::YAlign::Center => VerticalLayout::Centered, + super::YAlign::Bottom => VerticalLayout::Bottom, + } +} pub struct StardustTextPlugin; impl Plugin for StardustTextPlugin { fn build(&self, app: &mut App) { let (tx, rx) = crossbeam_channel::unbounded(); - SPAWN_TEXT_SENDER.set(tx); + _ = SPAWN_TEXT_SENDER.set(tx); app.insert_resource(SpawnTextReader(rx)); app.add_systems(PostUpdate, update_text); app.add_systems(PreUpdate, spawn_text); @@ -65,7 +54,7 @@ impl Plugin for StardustTextPlugin { fn update_text(mut surface_query: Query<(&mut Transform)>) { for text in TEXT_REGISTRY.get_valid_contents() { let Some((mut transform)) = text - .surface + .entity .get() .and_then(|v| surface_query.get_mut(*v).ok()) else { @@ -73,21 +62,13 @@ fn update_text(mut surface_query: Query<(&mut Transform)>) { }; // let data = text.data.lock(); - *transform = Transform::from_matrix( - text.space.global_transform(), // * Mat4::from_scale(vec3( - // data.character_height, - // data.character_height, - // data.character_height, - // )), - ); + *transform = Transform::from_matrix(text.space.global_transform()); } } fn spawn_text( reader: Res, mut cmds: Commands, - mut images: ResMut>, - mut meshes: ResMut>, mut mats: ResMut>, asset_server: Res, ) { @@ -96,99 +77,36 @@ fn spawn_text( let _span2 = info_span!("text data lock").entered(); let data = text.data.lock(); drop(_span2); - let size = Extent3d { - width: (512.0 * data.bounds.as_ref().map(|v| v.bounds.x).unwrap_or(1.0)).floor() as u32, - height: (512.0 * data.bounds.as_ref().map(|v| v.bounds.y).unwrap_or(1.0)).floor() - as u32, - ..default() - }; - - let _span = info_span!("create and setup image").entered(); - // This is the texture that will be rendered to. - let mut image = Image::transparent(); - image.texture_descriptor.format = TextureFormat::Bgra8UnormSrgb; - image.texture_descriptor.dimension = TextureDimension::D2; - image.asset_usage = RenderAssetUsages::default(); - image.resize(size); - // You need to set these texture usage flags in order to use the image as a render target - image.texture_descriptor.usage = TextureUsages::TEXTURE_BINDING - | TextureUsages::COPY_DST - | TextureUsages::RENDER_ATTACHMENT; - - let image_handle = images.add(image); - drop(_span); - - let _span = info_span!("spawn text camera and load font").entered(); - let cam = cmds - .spawn(( - Camera2d, - Camera { - target: RenderTarget::Image(image_handle.clone()), - ..default() - }, - )) - .id(); + let _span2 = info_span!("text str lock").entered(); + let str = text.text.lock().clone(); + drop(_span2); + let mat = mats.add(DefaultMaterial { + base_color: convert_linear_rgba(data.color).into(), + unlit: true, + ..Default::default() + }); let font = text .font_path .as_ref() - .map(|v| asset_server.load(v.as_path())); - drop(_span); + .map(|p| asset_server.load(p.as_path())); + let mut text_entity = cmds.spawn(( + MeshText { + text: atomicow::CowArc::Owned(str), + height: data.character_height, + depth: 0.0, + }, + MeshMaterial3d(mat), + convert_align_x(data.text_align_x), + convert_align_y(data.text_align_y), + )); + if let Some(font) = font { + text_entity.insert(MeshTextFont(font)); + } + + let entity = text_entity.id(); - let _span = info_span!("spawn ui entities").entered(); - let ui_root = cmds - .spawn(( - bevy::ui::Node { - // Cover the whole image - width: Val::Percent(100.), - height: Val::Percent(100.), - flex_direction: FlexDirection::Column, - justify_content: JustifyContent::Center, - align_items: AlignItems::Center, - ..default() - }, - BackgroundColor(Color::NONE), - TargetCamera(cam), - )) - .with_children(|parent| { - parent.spawn(( - bevy::prelude::Text::new(text.text.lock().as_str()), - TextFont { - font: font.unwrap_or_else(|| TextFont::default().font), - font_size: 40.0, - ..default() - }, - TextColor(convert_linear_rgba(data.color).into()), - )); - }) - .id(); - drop(_span); - let _span = info_span!("spawn 3d plane").entered(); - let surface = cmds - .spawn(( - Mesh3d( - meshes.add(Plane3d::new( - Vec3::NEG_Z, - data.bounds - .as_ref() - .map(|v| v.bounds.into()) - .unwrap_or(Vec2::ZERO) - * 0.5, - )), - ), - MeshMaterial3d(mats.add(DefaultMaterial { - base_color_texture: Some(image_handle), - unlit: true, - // would Cutout be enough here? - alpha_mode: bevy::prelude::AlphaMode::Blend, - ..default() - })), - )) - .id(); - drop(_span); let _span = info_span!("setting OneCells").entered(); - text.cam_entity.set(cam); - text.ui_root.set(ui_root); - text.surface.set(surface); + text.entity.set(entity); } } static SPAWN_TEXT_SENDER: OnceCell>> = OnceCell::new(); @@ -198,11 +116,9 @@ struct SpawnTextReader(crossbeam_channel::Receiver>); pub struct Text { space: Arc, font_path: Option, - text: Mutex, + text: Mutex>, data: Mutex, - cam_entity: OnceCell, - ui_root: OnceCell, - surface: OnceCell, + entity: OnceCell, } impl Text { pub fn add_to(node: &Arc, text: String, style: TextStyle) -> Result> { @@ -212,11 +128,9 @@ impl Text { font_path: style.font.as_ref().and_then(|res| { get_resource_file(res, &client, &[OsStr::new("ttf"), OsStr::new("otf")]) }), - text: Mutex::new(text), + text: Mutex::new(text.into()), data: Mutex::new(style), - ui_root: OnceCell::new(), - cam_entity: OnceCell::new(), - surface: OnceCell::new(), + entity: OnceCell::new(), }); node.add_aspect_raw(text.clone()); if let Some(sender) = SPAWN_TEXT_SENDER.get() { @@ -225,79 +139,6 @@ impl Text { Ok(text) } - - // fn draw(&self, token: &MainThreadToken) { - // let style = - // self.style - // .get_or_try_init(|| -> Result { - // let font = self - // .font_path - // .as_deref() - // .and_then(|path| Font::from_file(path).ok()) - // .unwrap_or_default(); - // Ok(SkTextStyle::from_font(font, 1.0, Color32::WHITE)) - // }); - // - // if let Ok(style) = style { - // let text = self.text.lock(); - // let data = self.data.lock(); - // let transform = self.space.global_transform() - // * Mat4::from_scale(vec3( - // data.character_height, - // data.character_height, - // data.character_height, - // )); - // if let Some(bounds) = &data.bounds { - // stereokit_rust::system::Text::add_in( - // token, - // &*text, - // transform, - // Vec2::from(bounds.bounds) / data.character_height, - // match bounds.fit { - // super::TextFit::Wrap => TextFit::Wrap, - // super::TextFit::Clip => TextFit::Clip, - // super::TextFit::Squeeze => TextFit::Squeeze, - // super::TextFit::Exact => TextFit::Exact, - // super::TextFit::Overflow => TextFit::Overflow, - // }, - // Some(*style), - // Some(Color128::new( - // data.color.c.r, - // data.color.c.g, - // data.color.c.b, - // data.color.a, - // )), - // data.bounds - // .as_ref() - // .map(|b| convert_align(b.anchor_align_x, b.anchor_align_y)), - // Some(convert_align(data.text_align_x, data.text_align_y)), - // None, - // None, - // None, - // ); - // } else { - // stereokit_rust::system::Text::add_at( - // token, - // &*text, - // transform, - // Some(*style), - // Some(Color128::new( - // data.color.c.r, - // data.color.c.g, - // data.color.c.b, - // data.color.a, - // )), - // data.bounds - // .as_ref() - // .map(|b| convert_align(b.anchor_align_x, b.anchor_align_y)), - // Some(convert_align(data.text_align_x, data.text_align_y)), - // None, - // None, - // None, - // ); - // } - // } - // } } impl TextAspect for Text { fn set_character_height( @@ -312,12 +153,15 @@ impl TextAspect for Text { fn set_text(node: Arc, _calling_client: Arc, text: String) -> Result<()> { let this_text = node.get_aspect::()?; - *this_text.text.lock() = text; + *this_text.text.lock() = text.into(); Ok(()) } } impl Drop for Text { fn drop(&mut self) { + if let Some(e) = self.entity.get() { + DESTROY_ENTITY.send(*e); + } TEXT_REGISTRY.remove(self); } } diff --git a/src/nodes/mod.rs b/src/nodes/mod.rs index 1dca709..36e70a6 100644 --- a/src/nodes/mod.rs +++ b/src/nodes/mod.rs @@ -13,7 +13,6 @@ use crate::core::error::{Result, ServerError}; use crate::core::queued_mutex::QueuedMutex; use crate::core::registry::Registry; use crate::core::scenegraph::MethodResponseSender; -use parking_lot::Mutex; use portable_atomic::{AtomicBool, Ordering}; use rustc_hash::FxHashMap; use serde::{de::DeserializeOwned, Serialize}; @@ -21,12 +20,12 @@ use spatial::Spatial; use stardust_xr::messenger::MessageSenderHandle; use stardust_xr::scenegraph::ScenegraphError; use stardust_xr::schemas::flex::{deserialize, serialize}; +use tracing::debug_span; use std::any::{Any, TypeId}; use std::fmt::Debug; use std::os::fd::OwnedFd; use std::sync::{Arc, Weak}; use std::vec::Vec; -use tracing::{debug_span, info}; #[derive(Default)] pub struct Message { @@ -345,7 +344,7 @@ impl Aspects { } fn get(&self) -> Result> { self.0 - .lock() + .read_now() .get(&A::ID) .cloned() .map(|a| a.as_any()) diff --git a/src/objects/input/sk_hand.rs b/src/objects/input/sk_hand.rs index c770fb0..2a6f2f3 100644 --- a/src/objects/input/sk_hand.rs +++ b/src/objects/input/sk_hand.rs @@ -165,7 +165,6 @@ fn update_hands( const PINCH_MAX: f32 = 0.11; const PINCH_ACTIVACTION_DISTANCE: f32 = 0.01; -// TODO: handle invalid data // based on https://github.com/StereoKit/StereoKit/blob/ca2be7d45f4f4388e8df7542e9a0313bcc45946e/StereoKitC/hands/input_hand.cpp#L375-L394 fn pinch_activation(joints: &[openxr::HandJointLocation; openxr::HAND_JOINT_COUNT]) -> f32 { let combined_radius = @@ -182,7 +181,6 @@ fn pinch_activation(joints: &[openxr::HandJointLocation; openxr::HAND_JOINT_COUN const GRIP_MAX: f32 = 0.11; const GRIP_ACTIVACTION_DISTANCE: f32 = 0.01; -// TODO: handle invalid data // based on https://github.com/StereoKit/StereoKit/blob/ca2be7d45f4f4388e8df7542e9a0313bcc45946e/StereoKitC/hands/input_hand.cpp#L375-L394 fn grip_activation(joints: &[openxr::HandJointLocation; openxr::HAND_JOINT_COUNT]) -> f32 { let combined_radius = joints[HandBone::RingTip as usize].radius