242 lines
6.3 KiB
Rust
242 lines
6.3 KiB
Rust
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<Arc<Text>> = 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::<FontSettings>()
|
|
.font_system
|
|
.db_mut()
|
|
.load_system_fonts();
|
|
|
|
SPAWN_TEXT.init(app);
|
|
app.init_resource::<MaterialRegistry>();
|
|
app.add_systems(Update, spawn_text);
|
|
}
|
|
}
|
|
|
|
fn spawn_text(
|
|
mut mpsc: ResMut<BevyChannelReader<Arc<Text>>>,
|
|
mut cmds: Commands,
|
|
mut font_settings: ResMut<FontSettings>,
|
|
mut material_registry: ResMut<MaterialRegistry>,
|
|
mut materials: ResMut<Assets<BevyMaterial>>,
|
|
mut meshes: ResMut<Assets<Mesh>>,
|
|
mut font_registry: Local<FontDatabaseRegistry>,
|
|
) {
|
|
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::<Vec<_>>();
|
|
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<PathBuf, cosmic_text::fontdb::Database>);
|
|
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<Text> = Registry::new();
|
|
|
|
pub struct Text {
|
|
spatial: Arc<Spatial>,
|
|
font_path: Option<PathBuf>,
|
|
entity: Mutex<Option<EntityHandle>>,
|
|
text: Mutex<String>,
|
|
data: Mutex<TextStyle>,
|
|
}
|
|
impl Text {
|
|
pub fn add_to(node: &Arc<Node>, text: String, style: TextStyle) -> Result<Arc<Text>> {
|
|
let client = node.get_client().ok_or_else(|| eyre!("Client not found"))?;
|
|
let text = TEXT_REGISTRY.add(Text {
|
|
spatial: node.get_aspect::<Spatial>().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<Node>,
|
|
_calling_client: Arc<Client>,
|
|
height: f32,
|
|
) -> Result<()> {
|
|
let this_text = node.get_aspect::<Text>()?;
|
|
this_text.data.lock().character_height = height;
|
|
_ = 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.send(this_text);
|
|
Ok(())
|
|
}
|
|
}
|
|
impl Drop for Text {
|
|
fn drop(&mut self) {
|
|
TEXT_REGISTRY.remove(self);
|
|
}
|
|
}
|