From a002e4d738535e6ca779c71231f7b84864b9a8d0 Mon Sep 17 00:00:00 2001 From: Silas Bartha Date: Tue, 4 Jun 2024 15:00:16 -0400 Subject: Refactored + Renamed + Added Docs --- src/display/components.rs | 72 ++++++++++++++++++++++++++++ src/display/mod.rs | 8 ++++ src/display/resources.rs | 43 +++++++++++++++++ src/display/systems.rs | 116 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 239 insertions(+) create mode 100644 src/display/components.rs create mode 100644 src/display/mod.rs create mode 100644 src/display/resources.rs create mode 100644 src/display/systems.rs (limited to 'src/display') diff --git a/src/display/components.rs b/src/display/components.rs new file mode 100644 index 0000000..4459325 --- /dev/null +++ b/src/display/components.rs @@ -0,0 +1,72 @@ +use bevy::{prelude::*, render::render_resource::{Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages}}; +use bevy_dither_post_process::components::DitherPostProcessSettings; +use bevy_framebuffer_extract::{components::{ExtractFramebufferBundle, FramebufferExtractDestination}, render_assets::FramebufferExtractSource}; + +/// Marker component for terminal display +#[derive(Component)] +pub struct TerminalDisplay; + +/// Bundle for terminal display, contains a handle to an image to be used as a render target to +/// render to the terminal +#[derive(Bundle)] +pub struct TerminalDisplayBundle { + _terminal_display: TerminalDisplay, + extract_framebuffer_bundle: ExtractFramebufferBundle, + dither_post_process_settings: DitherPostProcessSettings, + image_handle: Handle, +} + +impl TerminalDisplayBundle { + /// Create a new terminal display with the given dither level. A higher level exponentially + /// increases the size of the bayer matrix used in the ordered dithering calculations. If in + /// doubt, 3 is a good starting value to test with. + pub fn new(dither_level: u32, asset_server: &AssetServer) -> Self { + let terminal_size = crossterm::terminal::size().unwrap(); + let size = Extent3d { + width: (terminal_size.0 as u32) * 2, + height: (terminal_size.1 as u32) * 4, + depth_or_array_layers: 1, + }; + + let mut image = Image { + texture_descriptor: TextureDescriptor { + label: None, + size, + dimension: TextureDimension::D2, + format: TextureFormat::R8Unorm, + mip_level_count: 1, + sample_count: 1, + usage: TextureUsages::TEXTURE_BINDING + | TextureUsages::COPY_SRC + | TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }, + ..default() + }; + + image.resize(size); + let image_handle = asset_server.add(image); + + let framebuffer_extract_source = + asset_server.add(FramebufferExtractSource(image_handle.clone())); + + Self { + _terminal_display: TerminalDisplay, + extract_framebuffer_bundle: ExtractFramebufferBundle { + source: framebuffer_extract_source, + dest: FramebufferExtractDestination::default(), + }, + image_handle, + dither_post_process_settings: DitherPostProcessSettings::new( + dither_level, + asset_server, + ), + } + } + + /// Retrieves the handle to this display's target image. Anything written here will be + /// displayed. + pub fn image_handle(&self) -> Handle { + self.image_handle.clone() + } +} diff --git a/src/display/mod.rs b/src/display/mod.rs new file mode 100644 index 0000000..ae45445 --- /dev/null +++ b/src/display/mod.rs @@ -0,0 +1,8 @@ +/// Components for this module +pub mod components; + +/// Resources for this module +pub mod resources; + +/// Systems for this module +pub(crate) mod systems; diff --git a/src/display/resources.rs b/src/display/resources.rs new file mode 100644 index 0000000..7321aea --- /dev/null +++ b/src/display/resources.rs @@ -0,0 +1,43 @@ +use std::io::{stdout, Stdout}; + +use bevy::prelude::*; +use crossterm::{ + event::{ + DisableMouseCapture, EnableMouseCapture, KeyboardEnhancementFlags, + PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags, + }, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + ExecutableCommand, +}; +use ratatui::backend::CrosstermBackend; + +/// Ratatui terminal instance. Enters alternate screen when constructed, and exits once dropped. +#[derive(Resource)] +pub struct Terminal(pub ratatui::Terminal>); + +impl Default for Terminal { + fn default() -> Self { + stdout().execute(EnterAlternateScreen).unwrap(); + stdout().execute(EnableMouseCapture).unwrap(); + stdout() + .execute(PushKeyboardEnhancementFlags( + KeyboardEnhancementFlags::REPORT_EVENT_TYPES, + )) + .unwrap(); + enable_raw_mode().unwrap(); + let mut terminal = ratatui::Terminal::new(CrosstermBackend::new(stdout())) + .expect("Failed to create terminal"); + terminal.clear().expect("Failed to clear terminal"); + Self(terminal) + } +} + +impl Drop for Terminal { + fn drop(&mut self) { + let mut stdout = stdout(); + let _ = stdout.execute(PopKeyboardEnhancementFlags); + let _ = stdout.execute(DisableMouseCapture); + let _ = stdout.execute(LeaveAlternateScreen); + let _ = disable_raw_mode(); + } +} diff --git a/src/display/systems.rs b/src/display/systems.rs new file mode 100644 index 0000000..a768af5 --- /dev/null +++ b/src/display/systems.rs @@ -0,0 +1,116 @@ +use bevy::{ + prelude::*, + render::render_resource::{Extent3d, TextureFormat}, +}; +use crossterm::event::Event; +use bevy_framebuffer_extract::{ + components::FramebufferExtractDestination, render_assets::FramebufferExtractSource, +}; +use ratatui::{ + style::Stylize, + widgets::{Paragraph, Wrap}, +}; + +use crate::input::events::TerminalInputEvent; + +use super::resources::Terminal; + +const BRAILLE_CODE_MIN: u16 = 0x2800; +const BRAILLE_CODE_MAX: u16 = 0x28FF; + +/// 0 3 +/// 1 4 +/// 2 5 +/// 6 7 +const BRAILLE_DOT_BIT_POSITIONS: [u8; 8] = [0, 1, 2, 6, 3, 4, 5, 7]; + +/// Prints out the contents of a render image to the terminal as braille characters +pub fn print_to_terminal( + mut terminal: ResMut, + image_exports: Query<&FramebufferExtractDestination>, +) { + for image_export in image_exports.iter() { + let mut image = image_export + .0 + .lock() + .expect("Failed to get lock on output texture"); + if image.texture_descriptor.format != TextureFormat::R8Unorm { + warn_once!("Extracted framebuffer texture is not R8Unorm format. Will attempt conversion, but consider changing your render texture's format."); + info_once!("{:?}", image); + match image.convert(TextureFormat::R8Unorm) { + Some(img) => *image = img, + None => error_once!( + "Could not convert to R8Unorm texture format. Unexpected output may occur." + ), + }; + } + + let mut output_buffer = Vec::::new(); + let width = image.width(); + let height = image.height(); + let data = &image.data; + for character_y in (0..height).step_by(4) { + for character_x in (0..width).step_by(2) { + let mut mask: u8 = 0; + for offset_x in 0..2 { + for offset_y in 0..4 { + let x = character_x + offset_x; + let y = character_y + offset_y; + if x < width && y < height && data[(y * width + x) as usize] == 0xFF { + mask |= 1 + << (BRAILLE_DOT_BIT_POSITIONS[(offset_x * 4 + offset_y) as usize]); + } + } + } + output_buffer.push(braille_char(mask)); + } + } + + let string = output_buffer.into_iter().collect::(); + terminal + .0 + .draw(|frame| { + frame.render_widget( + Paragraph::new(string) + .white() + .bold() + .wrap(Wrap { trim: true }), + frame.size(), + ); + }) + .expect("Failed to draw terminal frame"); + } +} + +/// Utility function to convert a u8 into the corresponding braille character +fn braille_char(mask: u8) -> char { + match char::from_u32((BRAILLE_CODE_MIN + mask as u16) as u32) { + Some(character) => { + if character as u16 > BRAILLE_CODE_MAX { + panic!("Number too big!") + } + character + } + None => panic!("Error converting character!"), + } +} + +/// Watches for terminal resize events and resizes the render image accordingly +pub fn resize_handling( + mut images: ResMut>, + mut sources: ResMut>, + mut event_reader: EventReader, +) { + for event in event_reader.read() { + if let Event::Resize(w, h) = event.0 { + for source in sources.iter_mut() { + let image = images.get_mut(&source.1 .0).unwrap(); + image.resize(Extent3d { + width: w as u32 * 2, + height: h as u32 * 4, + depth_or_array_layers: 1, + }); + } + } + } +} -- cgit v1.2.3