feat: add entity handles and a bevy channel abstraction

Signed-off-by: Schmarni <marnistromer@gmail.com>
This commit is contained in:
Schmarni
2025-06-28 12:56:40 +02:00
committed by Nova King
parent 85bb21414d
commit 600eab9d2a
9 changed files with 163 additions and 98 deletions

32
src/core/bevy_channel.rs Normal file
View File

@@ -0,0 +1,32 @@
use std::sync::OnceLock;
use bevy::prelude::*;
use tokio::sync::mpsc::{self, error::TryRecvError};
#[derive(Resource)]
pub struct BevyChannelReader<T: Send + Sync + 'static>(mpsc::UnboundedReceiver<T>);
pub struct BevyChannel<T: Send + Sync + 'static>(OnceLock<mpsc::UnboundedSender<T>>);
impl<T: Send + Sync + 'static> BevyChannel<T> {
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<T: Send + Sync + 'static> BevyChannelReader<T> {
pub fn read(&mut self) -> Option<T> {
match self.0.try_recv() {
Ok(v) => Some(v),
Err(TryRecvError::Disconnected) => panic!("bevy channel should never disconnect"),
Err(TryRecvError::Empty) => None,
}
}
}

12
src/core/color.rs Normal file
View File

@@ -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<f32, Rgb<f32, LinearRgb>> {
fn to_bevy(&self) -> bevy::color::Color {
bevy::color::Color::srgba(self.c.r, self.c.g, self.c.b, self.a)
}
}

33
src/core/entity_handle.rs Normal file
View File

@@ -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<BevyChannelReader<Entity>>) {
while let Some(e) = reader.read() {
cmds.entity(e).despawn();
}
}
static DESTROY: BevyChannel<Entity> = 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<Entity> for EntityHandle {
fn from(value: Entity) -> Self {
Self(value)
}
}

View File

@@ -8,3 +8,5 @@ pub mod resource;
pub mod scenegraph;
pub mod task;
pub mod color;
pub mod entity_handle;
pub mod bevy_channel;

View File

@@ -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::<WakeUp>::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<Camera3d>,
),
>,
) {
fn update_cameras(mut camera: Query<&mut Projection, (With<Camera3d>,)>) {
for mut projection in &mut camera {
match projection.deref_mut() {
Projection::Perspective(perspective_projection) => perspective_projection.near = 0.003,

View File

@@ -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<Sound> = Registry::new();
stardust_xr_server_codegen::codegen_audio_protocol!();
pub struct Sound {
space: Arc<Spatial>,
spatial: Arc<Spatial>,
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::<Spatial>().unwrap().clone(),
spatial: node.get_aspect::<Spatial>().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());
}
}
}

View File

@@ -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::<Children>(Visibility::Visible);
}
false => {
cmds.entity(entity)
.insert_recursive::<Children>(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<Spatial>,
data: Mutex<Vec<Line>>,
gen_mesh: AtomicBool,
entity: OnceLock<Entity>,
entity: OnceLock<EntityHandle>,
bounds: Mutex<Aabb>,
}
impl Lines {
@@ -194,7 +219,7 @@ impl Lines {
.set(|node| {
node.get_aspect::<Lines>()
.ok()
.map(|v| v.bounds.lock().clone())
.map(|v| *v.bounds.lock())
.unwrap_or_default()
});

View File

@@ -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<mpsc::UnboundedSender<(Arc<Model>, PathBuf)>> = OnceLock::new();
static LOAD_MODEL: BevyChannel<(Arc<Model>, 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::<MaterialRegistry>();
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<T>(mpsc::UnboundedReceiver<T>);
#[derive(Component)]
struct ModelNode(Weak<Model>);
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::<Children>(Visibility::Visible);
}
false => {
cmds.entity(entity)
cmds.entity(entity.0)
.insert_recursive::<Children>(Visibility::Hidden);
}
}
@@ -64,9 +61,9 @@ fn update_visibillity(mut cmds: Commands) {
fn load_models(
asset_server: Res<AssetServer>,
mut cmds: Commands,
mut mpsc_receiver: ResMut<MpscReceiver<(Arc<Model>, PathBuf)>>,
mut mpsc_receiver: ResMut<BevyChannelReader<(Arc<Model>, 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<Entity>,
mesh_entity: OnceLock<Entity>,
entity: OnceLock<EntityHandle>,
mesh_entity: OnceLock<EntityHandle>,
path: String,
space: Arc<Spatial>,
_model: Weak<Model>,
@@ -541,7 +538,7 @@ impl MaterialRegistry {
pub struct Model {
spatial: Arc<Spatial>,
_resource_id: ResourceID,
bevy_scene_entity: OnceLock<Entity>,
bevy_scene_entity: OnceLock<EntityHandle>,
parts: OnceLock<Vec<Arc<ModelPart>>>,
pre_bound_parts: Mutex<Vec<Arc<ModelPart>>>,
}
@@ -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);

View File

@@ -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<mpsc::UnboundedSender<Arc<Text>>> = OnceLock::new();
#[derive(Resource)]
struct MpscReceiver<T>(mpsc::UnboundedReceiver<T>);
static SPAWN_TEXT: BevyChannel<Arc<Text>> = BevyChannel::new();
pub struct TextNodePlugin;
impl Plugin for TextNodePlugin {
fn build(&self, app: &mut App) {
// Text init stuff
// app.init_asset::<Font>().init_asset_loader::<FontLoader>();
// 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::<MaterialRegistry>();
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<MpscReceiver<Arc<Text>>>,
mut mpsc: ResMut<BevyChannelReader<Arc<Text>>>,
mut cmds: Commands,
mut font_settings: ResMut<FontSettings>,
mut material_registry: ResMut<MaterialRegistry>,
@@ -87,9 +66,9 @@ fn spawn_text(
mut meshes: ResMut<Assets<Mesh>>,
mut font_registry: Local<FontDatabaseRegistry>,
) {
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<Text> = Registry::new();
pub struct Text {
spatial: Arc<Spatial>,
font_path: Option<PathBuf>,
entity: Mutex<Option<Entity>>,
entity: Mutex<Option<EntityHandle>>,
text: Mutex<String>,
data: Mutex<TextStyle>,
}
@@ -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::<Text>()?;
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<Node>, _calling_client: Arc<Client>, text: String) -> Result<()> {
let this_text = node.get_aspect::<Text>()?;
*this_text.text.lock() = text;
_ = SPAWN_TEXT.get().unwrap().send(this_text.clone());
_ = SPAWN_TEXT.send(this_text);
Ok(())
}
}