use crate::{ bevy_plugin::{convert_linear_rgba, DESTROY_ENTITY}, core::{ client::Client, error::{Result, ServerError}, registry::Registry, resource::get_resource_file, }, nodes::{spatial::Spatial, Node}, DefaultMaterial, }; use bevy::{ app::{App, Plugin, PostUpdate, PreUpdate}, asset::{AssetServer, Assets}, pbr::MeshMaterial3d, prelude::{Commands, Deref, Entity, Query, Res, ResMut, Resource, Transform}, }; 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_span; use super::{TextAspect, TextStyle}; static TEXT_REGISTRY: Registry = Registry::new(); 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); app.insert_resource(SpawnTextReader(rx)); app.add_systems(PostUpdate, update_text); app.add_systems(PreUpdate, spawn_text); } } fn update_text(mut surface_query: Query<(&mut Transform)>) { for text in TEXT_REGISTRY.get_valid_contents() { let Some((mut transform)) = text .entity .get() .and_then(|v| surface_query.get_mut(*v).ok()) else { continue; }; // let data = text.data.lock(); *transform = Transform::from_matrix(text.space.global_transform()); } } fn spawn_text( reader: Res, mut cmds: Commands, mut mats: ResMut>, asset_server: Res, ) { for text in reader.try_iter() { let _span = info_span!("spawning text").entered(); let _span2 = info_span!("text data lock").entered(); let data = text.data.lock(); drop(_span2); let _span2 = info_span!("text str lock").entered(); let str = text.text.lock().clone(); drop(_span2); let mat = mats.add(DefaultMaterial { color: convert_linear_rgba(data.color).into(), ..Default::default() }); let font = text .font_path .as_ref() .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!("setting OneCells").entered(); text.entity.set(entity); } } static SPAWN_TEXT_SENDER: OnceCell>> = OnceCell::new(); #[derive(Resource, Deref)] struct SpawnTextReader(crossbeam_channel::Receiver>); pub struct Text { space: Arc, font_path: Option, text: Mutex>, data: Mutex, entity: OnceCell, } impl Text { pub fn add_to(node: &Arc, text: String, style: TextStyle) -> Result> { let client = node.get_client().ok_or(ServerError::NoClient)?; let text = TEXT_REGISTRY.add(Text { space: node.get_aspect::().unwrap().clone(), font_path: style.font.as_ref().and_then(|res| { get_resource_file(res, &client, &[OsStr::new("ttf"), OsStr::new("otf")]) }), text: Mutex::new(text.into()), data: Mutex::new(style), entity: OnceCell::new(), }); node.add_aspect_raw(text.clone()); if let Some(sender) = SPAWN_TEXT_SENDER.get() { sender.send(text.clone()); } Ok(text) } } impl TextAspect for Text { fn set_character_height( node: Arc, _calling_client: Arc, height: f32, ) -> Result<()> { let this_text = node.get_aspect::()?; this_text.data.lock().character_height = height; Ok(()) } fn set_text(node: Arc, _calling_client: Arc, text: String) -> Result<()> { let this_text = node.get_aspect::()?; *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); } }