use bevy::{
asset::Assets, mesh::Mesh3d, pbr::MeshMaterial3d, prelude::*, render::render_resource::Face,
};
use super::{TerminalSystems, TerminalTexture};
#[derive(Clone, Copy, Debug, PartialEq, Component)]
#[require(Transform, Visibility)]
pub struct TerminalWorldQuad {
pub height: f32,
pub unlit: bool,
pub alpha_mode: AlphaMode,
pub double_sided: bool,
pub cull_mode: Option<Face>,
}
impl Default for TerminalWorldQuad {
fn default() -> Self {
Self::new(1.0)
}
}
impl TerminalWorldQuad {
#[must_use]
pub const fn new(height: f32) -> Self {
Self {
height,
unlit: true,
alpha_mode: AlphaMode::Blend,
double_sided: false,
cull_mode: Some(Face::Back),
}
}
#[must_use]
pub const fn two_sided(mut self) -> Self {
self.double_sided = true;
self.cull_mode = None;
self
}
#[must_use]
pub const fn with_unlit(mut self, unlit: bool) -> Self {
self.unlit = unlit;
self
}
#[must_use]
pub const fn with_alpha_mode(mut self, alpha_mode: AlphaMode) -> Self {
self.alpha_mode = alpha_mode;
self
}
#[must_use]
pub fn size_for(&self, size: UVec2) -> Vec2 {
let size = size.max(UVec2::ONE).as_vec2();
Vec2::new(self.height * size.x / size.y, self.height)
}
}
pub(super) fn plugin(app: &mut App) {
app.add_systems(
Update,
sync_world_quads.after(TerminalSystems::Sync).run_if(
resource_exists::<Assets<Mesh>>.and_then(resource_exists::<Assets<StandardMaterial>>),
),
);
}
type QuadChanged = Or<(Changed<TerminalWorldQuad>, Changed<TerminalTexture>)>;
type QuadItem<'w> = (
Entity,
&'w TerminalWorldQuad,
&'w TerminalTexture,
Option<&'w Mesh3d>,
Option<&'w MeshMaterial3d<StandardMaterial>>,
);
fn sync_world_quads(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
quads: Query<QuadItem<'_>, QuadChanged>,
) {
for (entity, quad, texture, mesh, material) in &quads {
let size = quad.size_for(texture.size);
let rectangle = Mesh::from(Rectangle::from_size(size));
match mesh.filter(|mesh| meshes.contains(&mesh.0)) {
Some(mesh) => {
if let Some(mut existing) = meshes.get_mut(&mesh.0) {
*existing = rectangle;
}
}
None => {
commands
.entity(entity)
.insert(Mesh3d(meshes.add(rectangle)));
}
}
let material_handle = match material {
Some(material) if materials.contains(&material.0) => material.0.clone(),
_ => {
let handle = materials.add(StandardMaterial::default());
commands
.entity(entity)
.insert(MeshMaterial3d(handle.clone()));
handle
}
};
let Some(mut material) = materials.get_mut(&material_handle) else {
continue;
};
material.base_color_texture = Some(texture.image.clone());
material.unlit = quad.unlit;
material.alpha_mode = quad.alpha_mode;
material.double_sided = quad.double_sided;
material.cull_mode = quad.cull_mode;
}
}
#[cfg(test)]
mod tests {
use bevy::camera::primitives::MeshAabb;
use super::*;
use crate::{render::Terminal, surface::TerminalSurface};
#[test]
fn quad_follows_the_texture_aspect_and_keeps_its_material() {
let mut app = App::new();
app.add_plugins((
MinimalPlugins,
bevy::asset::AssetPlugin::default(),
bevy::text::TextPlugin,
super::super::TerminalPlugin,
))
.init_asset::<Image>()
.init_asset::<Mesh>()
.init_asset::<StandardMaterial>();
let surface = TerminalSurface::new((4, 2));
let entity = app
.world_mut()
.spawn((Terminal::new(surface.clone()), TerminalWorldQuad::new(2.0)))
.id();
app.update();
app.update();
let world = app.world();
let texture = world.get::<TerminalTexture>(entity).unwrap();
let material = world
.get::<MeshMaterial3d<StandardMaterial>>(entity)
.unwrap()
.clone();
let standard = world
.resource::<Assets<StandardMaterial>>()
.get(&material)
.unwrap();
assert_eq!(standard.base_color_texture, Some(texture.image.clone()));
assert!(standard.unlit);
let mesh = world.get::<Mesh3d>(entity).unwrap().clone();
let expected = TerminalWorldQuad::new(2.0).size_for(texture.size);
assert_eq!(expected.y, 2.0);
let aabb = world
.resource::<Assets<Mesh>>()
.get(&mesh)
.unwrap()
.compute_aabb()
.unwrap();
assert_eq!(aabb.half_extents.truncate() * 2.0, expected);
let emissive = Handle::<Image>::default();
{
let mut materials = app.world_mut().resource_mut::<Assets<StandardMaterial>>();
let mut standard = materials.get_mut(&material).unwrap();
standard.emissive_texture = Some(emissive.clone());
standard.emissive = LinearRgba::rgb(2.0, 0.5, 0.0);
standard.perceptual_roughness = 0.1;
}
app.world_mut()
.get_mut::<TerminalWorldQuad>(entity)
.unwrap()
.cull_mode = None;
surface.update(|update| {
update.resize((8, 2));
});
app.update();
let world = app.world();
let standard = world
.resource::<Assets<StandardMaterial>>()
.get(&material)
.unwrap();
assert_eq!(standard.emissive_texture, Some(emissive));
assert_eq!(standard.emissive, LinearRgba::rgb(2.0, 0.5, 0.0));
assert_eq!(standard.perceptual_roughness, 0.1);
assert_eq!(standard.cull_mode, None);
let doubled = TerminalWorldQuad::new(2.0)
.size_for(world.get::<TerminalTexture>(entity).unwrap().size);
assert!(
(doubled.x - expected.x * 2.0).abs() < 1e-3,
"{doubled:?} vs {expected:?}"
);
assert_eq!(
world.get::<Mesh3d>(entity).unwrap().0,
mesh.0,
"mesh handle reused"
);
assert_eq!(
world
.get::<MeshMaterial3d<StandardMaterial>>(entity)
.unwrap()
.0,
material.0
);
let aabb = world
.resource::<Assets<Mesh>>()
.get(&mesh)
.unwrap()
.compute_aabb()
.unwrap();
assert!((aabb.half_extents.x * 2.0 - doubled.x).abs() < 1e-3);
}
}