use crate::assets::{
AssetServer, Handle, MaterialAsset, MeshAsset, MeshVertex,
};
#[derive(
bevy_ecs::prelude::Component,
Clone,
Debug,
PartialEq,
serde::Serialize,
serde::Deserialize,
)]
#[serde(default)]
pub struct WorldText {
pub text: String,
pub size: f32,
pub color: [f32; 4],
}
impl Default for WorldText {
fn default() -> Self {
Self {
text: String::new(),
size: 0.5,
color: [1.0; 4],
}
}
}
#[derive(bevy_ecs::prelude::Component, Clone, Copy, Debug)]
pub struct WorldTextMesh(pub Handle<MeshAsset>);
const LINE_PITCH: i16 = 10;
const ADVANCE: i16 = 6;
const CAP: i16 = 7;
pub fn text_mesh(text: &str, size: f32) -> MeshAsset {
let pixel = size / f32::from(CAP);
let lines: Vec<&str> = text.lines().collect();
let height = (lines.len() as i16 - 1).max(0) * LINE_PITCH + CAP;
let mut mesh = MeshAsset::default();
for (row, line) in lines.iter().enumerate() {
let baseline =
f32::from(height) / 2.0 - f32::from(CAP + row as i16 * LINE_PITCH);
let width = line.chars().count() as f32 * f32::from(ADVANCE) - 1.0;
let left = -width / 2.0;
for (column, c) in line.chars().enumerate() {
let x = left + (column as i16 * ADVANCE) as f32;
for [l, b, r, t] in crate::fontgen::glyph_pixels(c) {
let corner = |px: i16, py: i16| MeshVertex {
position: [
(x + f32::from(px)) * pixel,
(baseline + f32::from(py)) * pixel,
0.0,
],
normal: [0.0, 0.0, 1.0],
uv: [0.0, 0.0],
tangent: [1.0, 0.0, 0.0, 1.0],
color: [1.0; 4],
};
let first = mesh.vertices.len() as u32;
mesh.vertices.extend([
corner(l, b),
corner(r, b),
corner(r, t),
corner(l, t),
]);
mesh.indices
.extend([0, 1, 2, 0, 2, 3].map(|index| first + index));
}
}
}
mesh
}
fn text_material(
assets: &mut AssetServer,
color: [f32; 4],
) -> Handle<MaterialAsset> {
let name = format!("World Text {color:?}");
if let Some((handle, _)) = assets
.materials
.iter()
.find(|(_, material)| material.name == name)
{
return handle;
}
assets.materials.insert(MaterialAsset {
name,
model: crate::assets::MaterialModel::Unlit,
base_color: color,
..MaterialAsset::default()
})
}
pub(super) fn sync_world_texts(
mut commands: bevy_ecs::prelude::Commands,
mut assets: Option<bevy_ecs::prelude::ResMut<AssetServer>>,
texts: bevy_ecs::prelude::Query<
(
bevy_ecs::prelude::Entity,
&WorldText,
Option<&WorldTextMesh>,
),
bevy_ecs::prelude::Changed<WorldText>,
>,
) {
let Some(assets) = assets.as_deref_mut() else {
return;
};
for (entity, text, owned) in &texts {
let built = text_mesh(&text.text, text.size);
let mesh = match owned.and_then(|owned| {
assets.meshes.get_mut(owned.0).map(|mesh| (owned.0, mesh))
}) {
Some((handle, mesh)) => {
*mesh = built;
handle
}
None => assets.meshes.insert(built),
};
let material = text_material(assets, text.color);
commands.entity(entity).insert((
WorldTextMesh(mesh),
super::MeshRenderer {
mesh,
material,
cast_shadows: false,
receive_shadows: false,
},
));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn text_mesh_draws_each_pixel_run_centered() {
let mesh = text_mesh("I", 0.7);
assert_eq!(mesh.vertices.len(), 7 * 4);
assert_eq!(mesh.indices.len(), 7 * 6);
let (min, max) = mesh.vertices.iter().fold(
([f32::MAX; 2], [f32::MIN; 2]),
|(min, max), v| {
(
[min[0].min(v.position[0]), min[1].min(v.position[1])],
[max[0].max(v.position[0]), max[1].max(v.position[1])],
)
},
);
assert!((max[1] - min[1] - 0.7).abs() < 1e-5, "{min:?} {max:?}");
assert!((min[0] + max[0]).abs() < 1e-5, "{min:?} {max:?}");
assert!((min[1] + max[1]).abs() < 1e-5, "{min:?} {max:?}");
let two = text_mesh("I\nI", 0.7);
assert_eq!(two.vertices.len(), 2 * 7 * 4);
assert!(text_mesh(" ", 1.0).vertices.is_empty());
}
#[test]
fn changing_world_text_rewrites_its_own_mesh() {
let mut app = crate::runtime::App::new();
app.add_plugin(crate::assets::AssetPlugin).unwrap();
let sign = app.spawn((
crate::Transform::default(),
WorldText {
text: "I".into(),
..WorldText::default()
},
));
let tick = |app: &mut crate::runtime::App| {
app.update(std::time::Duration::from_secs_f64(1.0 / 60.0))
.unwrap();
};
tick(&mut app);
let renderer =
*app.world().get::<super::super::MeshRenderer>(sign).unwrap();
app.world_mut().get_mut::<WorldText>(sign).unwrap().text = "II".into();
tick(&mut app);
let world = app.world();
let after = world.get::<super::super::MeshRenderer>(sign).unwrap();
assert_eq!(after.mesh, renderer.mesh, "the same mesh is rewritten");
assert_eq!(after.material, renderer.material);
let assets = world.resource::<AssetServer>();
assert_eq!(
assets.meshes.get(after.mesh).unwrap().vertices.len(),
2 * 7 * 4
);
let material = assets.materials.get(after.material).unwrap();
assert_eq!(material.model, crate::assets::MaterialModel::Unlit);
assert_eq!(material.base_color, [1.0; 4]);
}
}