use crate::{ BevyMaterial, core::{ 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}, }, }; use bevy::{platform::collections::HashMap, prelude::*, render::mesh::MeshAabb}; use bevy_mesh_text_3d::{ Align, Attrs, MeshTextPlugin, Settings as FontSettings, generate_meshes, text_glyphs::TextGlyphs, }; 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}; static SPAWN_TEXT: BevyChannel> = BevyChannel::new(); pub struct TextNodePlugin; impl Plugin for TextNodePlugin { fn build(&self, app: &mut App) { // Text init stuff // 1.0 for font size in meters app.add_plugins(MeshTextPlugin::new(1.0)); app.world_mut() .resource_mut::() .font_system .db_mut() .load_system_fonts(); SPAWN_TEXT.init(app); app.init_resource::(); app.add_systems(Update, spawn_text); } } fn spawn_text( mut mpsc: ResMut>>, mut cmds: Commands, mut font_settings: ResMut, mut material_registry: ResMut, mut materials: ResMut>, mut meshes: ResMut>, mut font_registry: Local, ) { while let Some(text) = mpsc.read() { if let Some(entity) = text.entity.lock().take() { cmds.entity(*entity).despawn(); } let style = text.data.lock(); let old_db = text.font_path.clone().map(|p| { let db = font_registry.get(p); mem::swap(font_settings.font_system.db_mut(), db); db }); let attrs = Attrs::new().weight(cosmic_text::Weight::BOLD); let alignment = Some(match style.text_align_x { super::XAlign::Left => Align::Right, super::XAlign::Center => Align::Center, super::XAlign::Right => Align::Left, }); let text_string = text.text.lock().clone(); let mut text_glyphs = TextGlyphs::new( Metrics { font_size: style.character_height, line_height: style.character_height, }, [(text_string.as_str(), attrs.clone())], &attrs, &mut font_settings.font_system, alignment, ); let max_width = style.bounds.as_ref().map(|v| v.bounds.x); let max_height = style.bounds.as_ref().map(|v| v.bounds.x); let (width, _height) = text_glyphs.measure(max_width, max_height, &mut font_settings.font_system); let char_meshes = generate_meshes( bevy_mesh_text_3d::InputText::Simple { text: text_string, material: material_registry.get_handle( BevyMaterial { base_color: style.color.to_bevy(), emissive: Color::WHITE.to_linear(), metallic: 0.0, perceptual_roughness: 1.0, // If alpha is supported on text we need to change this alpha_mode: AlphaMode::Opaque, double_sided: false, ..default() }, &mut materials, ), attrs, }, &mut font_settings, bevy_mesh_text_3d::Parameters { extrusion_depth: 0.0, font_size: style.character_height, line_height: style.character_height, alignment, max_width, max_height, }, &mut meshes, ); if let Some(db) = old_db { mem::swap(font_settings.font_system.db_mut(), db); } let Ok(char_meshes) = char_meshes.inspect_err(|err| error!("unable to create text meshes: {err}")) else { continue; }; let dist = char_meshes.iter().fold(f32::MAX, |dist, v| { dist.min( v.transform.translation.x - meshes .get(&v.mesh) .unwrap() .compute_aabb() .unwrap_or_default() .half_extents .x, ) }); // TODO: text align let letters = char_meshes .into_iter() .map(|v| { cmds.spawn(( Mesh3d(v.mesh), MeshMaterial3d(v.material), // rotation is sus, might be related to the gltf coordinate system Transform::from_rotation(Quat::from_rotation_y(f32::consts::PI)) * Transform::from_xyz( -dist + match style.bounds.as_ref().map(|v| v.anchor_align_x) { Some(XAlign::Center) => width * -0.5, Some(XAlign::Right) => -width, Some(XAlign::Left) => 0.0, None => 0.0, }, 0.0, 0.0, ) * v.transform, )) .id() }) .collect::>(); let entity = cmds .spawn(( Name::new("TextNode"), SpatialNode(Arc::downgrade(&text.spatial)), )) .add_children(&letters) .id(); text.entity.lock().replace(EntityHandle(entity)); } } #[derive(Default)] struct FontDatabaseRegistry(HashMap); impl FontDatabaseRegistry { fn get(&mut self, path: PathBuf) -> &mut cosmic_text::fontdb::Database { self.0.entry(path).or_insert_with_key(|path| { let mut db = cosmic_text::fontdb::Database::new(); if let Err(err) = db.load_font_file(path) { error!("unable to load font file {} {err}", path.to_string_lossy()); }; db }) } } use super::{TextAspect, TextStyle, model::MaterialRegistry}; static TEXT_REGISTRY: Registry = Registry::new(); pub struct Text { spatial: Arc, font_path: Option, entity: Mutex>, text: Mutex, data: Mutex, } impl Text { pub fn add_to(node: &Arc, text: String, style: TextStyle) -> Result> { let client = node.get_client().ok_or_else(|| eyre!("Client not found"))?; let text = TEXT_REGISTRY.add(Text { spatial: 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")]) }), entity: Mutex::new(None), text: Mutex::new(text), data: Mutex::new(style), }); node.add_aspect_raw(text.clone()); _ = SPAWN_TEXT.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; _ = 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.send(this_text); Ok(()) } } impl Drop for Text { fn drop(&mut self) { TEXT_REGISTRY.remove(self); } }