diff --git a/src/core/bevy_channel.rs b/src/core/bevy_channel.rs new file mode 100644 index 0000000..0e86d8e --- /dev/null +++ b/src/core/bevy_channel.rs @@ -0,0 +1,32 @@ +use std::sync::OnceLock; + +use bevy::prelude::*; +use tokio::sync::mpsc::{self, error::TryRecvError}; + +#[derive(Resource)] +pub struct BevyChannelReader(mpsc::UnboundedReceiver); +pub struct BevyChannel(OnceLock>); +impl BevyChannel { + pub const fn new() -> Self { + Self(OnceLock::new()) + } + pub fn init(&self, app: &mut App) { + let (tx, rx) = mpsc::unbounded_channel(); + self.0.set(tx).unwrap(); + app.insert_resource(BevyChannelReader(rx)); + } + pub fn send(&self, msg: T) -> Option<()> { + self.0.get()?.send(msg).ok() + } +} + +impl BevyChannelReader { + pub fn read(&mut self) -> Option { + match self.0.try_recv() { + Ok(v) => Some(v), + Err(TryRecvError::Disconnected) => panic!("bevy channel should never disconnect"), + Err(TryRecvError::Empty) => None, + } + } +} + diff --git a/src/core/color.rs b/src/core/color.rs new file mode 100644 index 0000000..98cd8c0 --- /dev/null +++ b/src/core/color.rs @@ -0,0 +1,12 @@ +use stardust_xr::values::color::{AlphaColor, Rgb, color_space::LinearRgb}; + +pub trait ColorConvert { + fn to_bevy(&self) -> bevy::color::Color; +} +// even tho its supposed to be linear the values have to be interpreted as Srgba to produce the +// correct result because StereoKit used Srgba while it was assumed that is uses linear rgba +impl ColorConvert for AlphaColor> { + fn to_bevy(&self) -> bevy::color::Color { + bevy::color::Color::srgba(self.c.r, self.c.g, self.c.b, self.a) + } +} diff --git a/src/core/entity_handle.rs b/src/core/entity_handle.rs new file mode 100644 index 0000000..c4d7876 --- /dev/null +++ b/src/core/entity_handle.rs @@ -0,0 +1,33 @@ +use bevy::prelude::*; + +use super::bevy_channel::{BevyChannel, BevyChannelReader}; +pub struct EntityHandlePlugin; + +impl Plugin for EntityHandlePlugin { + fn build(&self, app: &mut App) { + DESTROY.init(app); + app.add_systems(PreUpdate, despawn); + } +} + +fn despawn(mut cmds: Commands, mut reader: ResMut>) { + while let Some(e) = reader.read() { + cmds.entity(e).despawn(); + } +} + +static DESTROY: BevyChannel = BevyChannel::new(); +#[derive(Deref, DerefMut, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct EntityHandle(pub Entity); +impl Drop for EntityHandle { + fn drop(&mut self) { + if DESTROY.send(self.0).is_none() { + error!("Entity Destroy channel not open"); + } + } +} +impl From for EntityHandle { + fn from(value: Entity) -> Self { + Self(value) + } +} diff --git a/src/core/mod.rs b/src/core/mod.rs index d2385de..ead1b89 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -8,3 +8,5 @@ pub mod resource; pub mod scenegraph; pub mod task; pub mod color; +pub mod entity_handle; +pub mod bevy_channel; diff --git a/src/main.rs b/src/main.rs index c35999b..1f88cac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,9 +16,6 @@ use bevy::app::{App, TerminalCtrlCHandlerPlugin}; use bevy::asset::{AssetMetaCheck, UnapprovedPathMode}; use bevy::audio::AudioPlugin; use bevy::core_pipeline::CorePipelinePlugin; -use bevy::core_pipeline::oit::{ - OrderIndependentTransparencyPlugin, OrderIndependentTransparencySettings, -}; use bevy::diagnostic::DiagnosticsPlugin; use bevy::ecs::schedule::{ExecutorKind, ScheduleLabel}; use bevy::gizmos::GizmoPlugin; @@ -46,6 +43,7 @@ use bevy_mod_xr::hand_debug_gizmos::HandGizmosPlugin; use bevy_mod_xr::session::{XrFirst, XrHandleEvents}; use clap::Parser; use core::client::{Client, tick_internal_client}; +use core::entity_handle::EntityHandlePlugin; use core::task; use directories::ProjectDirs; use nodes::drawable::lines::LinesNodePlugin; @@ -283,18 +281,11 @@ fn bevy_loop( ..default() }); let mut plugins = MinimalPlugins - .build().add(DiagnosticsPlugin) + .build() + .add(DiagnosticsPlugin) .add(TransformPlugin) .add(InputPlugin) - /* .add(AccessibilityPlugin) */; - // TODO: figure out headless - // { - // plugins = plugins.add(WindowPlugin::default()).add({ - // let mut winit = WinitPlugin::::default(); - // winit.run_on_any_thread = true; - // winit - // }); - // } + .add(AccessibilityPlugin); plugins = plugins .add(TerminalCtrlCHandlerPlugin) // bevy_mod_openxr will replace this, TODO: figure out how to mix this with @@ -315,8 +306,7 @@ fn bevy_loop( .add(ScenePlugin) .add(GltfPlugin::default()) .add(AudioPlugin::default()) - .add(GizmoPlugin) - .add(AccessibilityPlugin); + .add(GizmoPlugin); let mut task_pool_plugin = TaskPoolPlugin::default(); // make tokio work let handle = tokio::runtime::Handle::current(); @@ -396,15 +386,17 @@ fn bevy_loop( app.insert_resource(ObjectRegistryRes(object_registry)); app.add_plugins((RemotePlugin::default(), RemoteHttpPlugin::default())); // the Stardust server plugins + // infra plugins + app.add_plugins(EntityHandlePlugin); + // node plugins app.add_plugins(( SpatialNodePlugin, ModelNodePlugin, TextNodePlugin, LinesNodePlugin, - PlaySpacePlugin, - HandPlugin, - ControllerPlugin, )); + // object plugins + app.add_plugins((PlaySpacePlugin, HandPlugin, ControllerPlugin)); app.add_systems(PostStartup, move || { ready_notifier.notify_waiters(); }); @@ -418,14 +410,7 @@ fn bevy_loop( app.run(); } -fn update_cameras( - mut camera: Query< - &mut Projection, - ( - With, - ), - >, -) { +fn update_cameras(mut camera: Query<&mut Projection, (With,)>) { for mut projection in &mut camera { match projection.deref_mut() { Projection::Perspective(perspective_projection) => perspective_projection.near = 0.003, diff --git a/src/nodes/audio.rs b/src/nodes/audio.rs index dd74d29..d048dd8 100644 --- a/src/nodes/audio.rs +++ b/src/nodes/audio.rs @@ -14,12 +14,20 @@ use std::ops::DerefMut; use std::sync::{Arc, OnceLock}; use std::{ffi::OsStr, path::PathBuf}; use stereokit_rust::sound::{Sound as SkSound, SoundInst}; +use bevy::prelude::*; + +pub struct AudioNodePlugin; +impl Plugin for AudioNodePlugin { + fn build(&self, app: &mut App) { + todo!() + } +} static SOUND_REGISTRY: Registry = Registry::new(); stardust_xr_server_codegen::codegen_audio_protocol!(); pub struct Sound { - space: Arc, + spatial: Arc, volume: f32, pending_audio_path: PathBuf, @@ -37,7 +45,7 @@ impl Sound { ) .ok_or_else(|| eyre!("Resource not found"))?; let sound = Sound { - space: node.get_aspect::().unwrap().clone(), + spatial: node.get_aspect::().unwrap().clone(), volume: 1.0, pending_audio_path, sk_sound: OnceLock::new(), @@ -64,7 +72,7 @@ impl Sound { self.instance.lock().replace(instance); } if let Some(instance) = self.instance.lock().deref_mut() { - instance.position(self.space.global_transform().w_axis.xyz()); + instance.position(self.spatial.global_transform().w_axis.xyz()); } } } diff --git a/src/nodes/drawable/lines.rs b/src/nodes/drawable/lines.rs index 1ce6677..33e63cd 100644 --- a/src/nodes/drawable/lines.rs +++ b/src/nodes/drawable/lines.rs @@ -1,6 +1,9 @@ use super::{Line, LinesAspect}; use crate::{ - core::{client::Client, color::ColorConvert, error::Result, registry::Registry}, + core::{ + client::Client, color::ColorConvert, entity_handle::EntityHandle, error::Result, + registry::Registry, + }, nodes::{ Node, spatial::{Spatial, SpatialNode}, @@ -26,7 +29,25 @@ pub struct LinesNodePlugin; impl Plugin for LinesNodePlugin { fn build(&self, app: &mut App) { - app.add_systems(Update, build_line_mesh); + app.add_systems(Update, (build_line_mesh, update_visibillity).chain()); + } +} + +fn update_visibillity(mut cmds: Commands) { + for lines in LINES_REGISTRY.get_valid_contents().into_iter() { + let Some(entity) = lines.entity.get().map(|e| **e) else { + continue; + }; + match lines.spatial.node().map(|n| n.enabled()).unwrap_or(false) { + true => { + cmds.entity(entity) + .insert_recursive::(Visibility::Visible); + } + false => { + cmds.entity(entity) + .insert_recursive::(Visibility::Hidden); + } + } } } @@ -132,8 +153,12 @@ fn build_line_mesh( } match lines.entity.get() { - Some(e) => cmds.entity(*e), - None => cmds.spawn(SpatialNode(Arc::downgrade(&lines.spatial))), + Some(e) => cmds.entity(**e), + None => { + let e = cmds.spawn(SpatialNode(Arc::downgrade(&lines.spatial))); + _ = lines.entity.set(e.id().into()); + e + } } .insert(( Mesh3d(meshes.add(mesh)), @@ -182,7 +207,7 @@ pub struct Lines { spatial: Arc, data: Mutex>, gen_mesh: AtomicBool, - entity: OnceLock, + entity: OnceLock, bounds: Mutex, } impl Lines { @@ -194,7 +219,7 @@ impl Lines { .set(|node| { node.get_aspect::() .ok() - .map(|v| v.bounds.lock().clone()) + .map(|v| *v.bounds.lock()) .unwrap_or_default() }); diff --git a/src/nodes/drawable/model.rs b/src/nodes/drawable/model.rs index 480dcae..38fbce3 100644 --- a/src/nodes/drawable/model.rs +++ b/src/nodes/drawable/model.rs @@ -1,7 +1,9 @@ use super::{MODEL_PART_ASPECT_ALIAS_INFO, MaterialParameter, ModelAspect, ModelPartAspect}; use crate::bail; +use crate::core::bevy_channel::{BevyChannel, BevyChannelReader}; use crate::core::client::Client; use crate::core::color::ColorConvert as _; +use crate::core::entity_handle::EntityHandle; use crate::core::error::Result; use crate::core::registry::Registry; use crate::core::resource::get_resource_file; @@ -19,17 +21,14 @@ use std::ffi::OsStr; use std::hash::{Hash, Hasher}; use std::path::PathBuf; use std::sync::{Arc, OnceLock, Weak}; -use tokio::sync::mpsc; -static LOAD_MODEL: OnceLock, PathBuf)>> = OnceLock::new(); +static LOAD_MODEL: BevyChannel<(Arc, PathBuf)> = BevyChannel::new(); pub struct ModelNodePlugin; impl Plugin for ModelNodePlugin { fn build(&self, app: &mut App) { - let (tx, rx) = mpsc::unbounded_channel(); - LOAD_MODEL.set(tx).unwrap(); + LOAD_MODEL.init(app); app.init_resource::(); - app.insert_resource(MpscReceiver(rx)); app.add_systems(Update, load_models); app.add_systems( PostUpdate, @@ -38,23 +37,21 @@ impl Plugin for ModelNodePlugin { } } -#[derive(Resource)] -struct MpscReceiver(mpsc::UnboundedReceiver); #[derive(Component)] struct ModelNode(Weak); fn update_visibillity(mut cmds: Commands) { for model in MODEL_REGISTRY.get_valid_contents().into_iter() { - let Some(entity) = model.bevy_scene_entity.get().copied() else { + let Some(entity) = model.bevy_scene_entity.get() else { continue; }; match model.spatial.node().map(|n| n.enabled()).unwrap_or(false) { true => { - cmds.entity(entity) + cmds.entity(entity.0) .insert_recursive::(Visibility::Visible); } false => { - cmds.entity(entity) + cmds.entity(entity.0) .insert_recursive::(Visibility::Hidden); } } @@ -64,9 +61,9 @@ fn update_visibillity(mut cmds: Commands) { fn load_models( asset_server: Res, mut cmds: Commands, - mut mpsc_receiver: ResMut, PathBuf)>>, + mut mpsc_receiver: ResMut, PathBuf)>>, ) { - while let Ok((model, path)) = mpsc_receiver.0.try_recv() { + while let Some((model, path)) = mpsc_receiver.read() { // idk of the asset label is the correct approach here let handle = asset_server.load(GltfAssetLabel::Scene(0).from_asset(path)); let entity = cmds @@ -76,7 +73,7 @@ fn load_models( SpatialNode(Arc::downgrade(&model.spatial)), )) .id(); - model.bevy_scene_entity.set(entity).unwrap(); + model.bevy_scene_entity.set(entity.into()).unwrap(); } } @@ -92,7 +89,7 @@ fn apply_materials( .filter_map(|p| p.parts.get()) .flatten() { - let Ok(mut mesh_mat) = query.get_mut(*model_part.mesh_entity.get().unwrap()) else { + let Ok(mut mesh_mat) = query.get_mut(**model_part.mesh_entity.get().unwrap()) else { continue; }; if let Some(material) = model_part.pending_material_replacement.lock().take() { @@ -214,8 +211,8 @@ fn gen_model_parts( .flat_map(|v| v.iter()) .find(|e| has_mesh.get(*e).unwrap_or(false))?; _ = model_part.bounds.set(aabb); - _ = model_part.entity.set(entity); - _ = model_part.mesh_entity.set(mesh_entity); + _ = model_part.entity.set(entity.into()); + _ = model_part.mesh_entity.set(mesh_entity.into()); parts.push(model_part.clone()); Some(model_part) }, @@ -460,8 +457,8 @@ impl Material { } pub struct ModelPart { - entity: OnceLock, - mesh_entity: OnceLock, + entity: OnceLock, + mesh_entity: OnceLock, path: String, space: Arc, _model: Weak, @@ -541,7 +538,7 @@ impl MaterialRegistry { pub struct Model { spatial: Arc, _resource_id: ResourceID, - bevy_scene_entity: OnceLock, + bevy_scene_entity: OnceLock, parts: OnceLock>>, pre_bound_parts: Mutex>>, } @@ -562,8 +559,6 @@ impl Model { parts: OnceLock::new(), }); LOAD_MODEL - .get() - .unwrap() .send((model.clone(), pending_model_path)) .unwrap(); MODEL_REGISTRY.add_raw(&model); diff --git a/src/nodes/drawable/text.rs b/src/nodes/drawable/text.rs index b205feb..a701625 100644 --- a/src/nodes/drawable/text.rs +++ b/src/nodes/drawable/text.rs @@ -1,12 +1,9 @@ use crate::{ core::{ - client::Client, color::ColorConvert, error::Result, registry::Registry, - resource::get_resource_file, + bevy_channel::{BevyChannel, BevyChannelReader}, client::Client, color::ColorConvert, entity_handle::EntityHandle, error::Result, registry::Registry, resource::get_resource_file }, nodes::{ - Node, - drawable::XAlign, - spatial::{Spatial, SpatialNode}, + drawable::XAlign, spatial::{Spatial, SpatialNode}, Node }, }; use bevy::{platform::collections::HashMap, prelude::*}; @@ -19,31 +16,15 @@ use color_eyre::eyre::eyre; use core::f32; use cosmic_text::Metrics; use parking_lot::Mutex; -use std::{ - ffi::OsStr, - mem, - path::PathBuf, - sync::{Arc, OnceLock}, -}; -use tokio::sync::mpsc; +use std::{ffi::OsStr, mem, path::PathBuf, sync::Arc}; -static SPAWN_TEXT: OnceLock>> = OnceLock::new(); - -#[derive(Resource)] -struct MpscReceiver(mpsc::UnboundedReceiver); +static SPAWN_TEXT: BevyChannel> = BevyChannel::new(); pub struct TextNodePlugin; impl Plugin for TextNodePlugin { fn build(&self, app: &mut App) { // Text init stuff - // app.init_asset::().init_asset_loader::(); - // load_internal_binary_asset!( - // app, - // Handle::default(), - // "assets/FiraMono-subset.ttf", - // |bytes: &[u8], _path: String| { Font::try_from_bytes(bytes.to_vec()).unwrap() } - // ); // 1.0 for font size in meters app.add_plugins(MeshTextPlugin::new(1.0)); app.world_mut() @@ -52,17 +33,15 @@ impl Plugin for TextNodePlugin { .db_mut() .load_system_fonts(); - let (tx, rx) = mpsc::unbounded_channel(); - SPAWN_TEXT.set(tx).unwrap(); + SPAWN_TEXT.init(app); app.init_resource::(); - app.insert_resource(MpscReceiver(rx)); app.add_systems(Update, (spawn_text, update_visibillity).chain()); } } fn update_visibillity(mut cmds: Commands) { for text in TEXT_REGISTRY.get_valid_contents().into_iter() { - let Some(entity) = text.entity.lock().as_ref().copied() else { + let Some(entity) = text.entity.lock().as_deref().copied() else { continue; }; match text.spatial.node().map(|n| n.enabled()).unwrap_or(false) { @@ -79,7 +58,7 @@ fn update_visibillity(mut cmds: Commands) { } fn spawn_text( - mut mpsc: ResMut>>, + mut mpsc: ResMut>>, mut cmds: Commands, mut font_settings: ResMut, mut material_registry: ResMut, @@ -87,9 +66,9 @@ fn spawn_text( mut meshes: ResMut>, mut font_registry: Local, ) { - while let Ok(text) = mpsc.0.try_recv() { + while let Some(text) = mpsc.read() { if let Some(entity) = text.entity.lock().take() { - cmds.entity(entity).despawn(); + cmds.entity(*entity).despawn(); } let style = text.data.lock(); let old_db = text.font_path.clone().map(|p| { @@ -157,17 +136,11 @@ fn spawn_text( }; let dist = meshes.iter().fold(f32::MAX, |dist, v| { dist.min(v.transform.translation.x) - // if dist > v.transform.translation.x { - // v.transform.translation.x - // } else { - // dist - // } }); // TODO: text align let letters = meshes .into_iter() .map(|v| { - // info!("{:?}", v.transform); cmds.spawn(( Mesh3d(v.mesh), MeshMaterial3d(v.material), @@ -177,7 +150,7 @@ fn spawn_text( -dist + match style.bounds.as_ref().map(|v| v.anchor_align_x) { Some(XAlign::Center) => width * -0.5, - Some(XAlign::Right) => width * -1.0, + Some(XAlign::Right) => -width, Some(XAlign::Left) => 0.0, None => 0.0, }, @@ -192,7 +165,7 @@ fn spawn_text( .spawn((SpatialNode(Arc::downgrade(&text.spatial)),)) .add_children(&letters) .id(); - text.entity.lock().replace(entity); + text.entity.lock().replace(EntityHandle(entity)); } } @@ -217,7 +190,7 @@ static TEXT_REGISTRY: Registry = Registry::new(); pub struct Text { spatial: Arc, font_path: Option, - entity: Mutex>, + entity: Mutex>, text: Mutex, data: Mutex, } @@ -235,7 +208,7 @@ impl Text { data: Mutex::new(style), }); node.add_aspect_raw(text.clone()); - _ = SPAWN_TEXT.get().unwrap().send(text.clone()); + _ = SPAWN_TEXT.send(text.clone()); Ok(text) } @@ -248,14 +221,14 @@ impl TextAspect for Text { ) -> Result<()> { let this_text = node.get_aspect::()?; this_text.data.lock().character_height = height; - _ = SPAWN_TEXT.get().unwrap().send(this_text.clone()); + _ = SPAWN_TEXT.send(this_text); Ok(()) } fn set_text(node: Arc, _calling_client: Arc, text: String) -> Result<()> { let this_text = node.get_aspect::()?; *this_text.text.lock() = text; - _ = SPAWN_TEXT.get().unwrap().send(this_text.clone()); + _ = SPAWN_TEXT.send(this_text); Ok(()) } }