Files
kneeboard2/src/apad/mod.rs

601 lines
19 KiB
Rust

mod custom_material;
mod keyboard;
mod otdipcplugin;
use std::f32::consts::{FRAC_PI_2, PI};
use bevy::{
app::{App, Plugin, Startup, Update},
asset::{Assets, Handle, load_internal_asset},
camera::{Camera, Camera3d, RenderTarget, visibility::RenderLayers},
color::{Color, palettes::css},
ecs::{
component::Component,
entity::{ContainsEntity, Entity},
message::{MessageReader, MessageWriter},
query::{With, Without},
resource::Resource,
schedule::{IntoScheduleConfigs, ScheduleLabel},
system::{Commands, Query, Res, ResMut, Single},
},
image::Image,
math::{Dir3, Quat, Vec2, Vec3, primitives::Cuboid},
mesh::{Mesh, Mesh3d},
pbr::{MaterialPlugin, MeshMaterial3d, StandardMaterial},
prelude::{Deref, DerefMut},
render::render_resource::{Extent3d, TextureUsages},
shader::Shader,
transform::components::Transform,
utils::default,
};
use bevy_egui::{
EguiGlobalSettings, EguiInputSet, EguiMultipassSchedule, EguiPlugin, input::EguiInputEvent,
};
use bevy_mod_openxr::session::OxrSession;
use bevy_pkv::{PersistentResourceAppExtensions, PkvStore};
use egui::{
Button, Color32, Layout, Margin, PointerButton, Pos2, Rect, TextEdit, Theme, Ui,
scroll_area::ScrollSource,
};
use openxr::Path;
use serde::{Deserialize, Serialize};
// PUB
pub use crate::apad::otdipcplugin::TabletRotation;
use crate::{
MainCamera,
apad::{
custom_material::{CustomMaterial, FRAGMENT_SHADER_HANDLE, MyCustomMaterial},
keyboard::VirtualKeyboard,
otdipcplugin::{OtdIpcPlugin, PenButtons, PenDelta, PenPosition},
},
egui_pages::*,
vrcontrollerplugin::{
LeftController, LeftControllerActions, RightController, RightControllerActions,
},
vrplugin::Headset,
};
#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]
pub struct WorldspaceContextPass;
#[derive(Resource, Deref)]
pub struct TabletSize(Vec2);
#[derive(Resource, Deref)]
pub struct TabletResolutionScale(f32);
#[derive(Resource, Deref, DerefMut)]
struct PointerImage(Option<Handle<Image>>);
#[derive(Component, Deref, DerefMut)]
pub struct Keyboard(VirtualKeyboard);
#[derive(Component)]
pub struct Kneeboard;
#[derive(Resource, Deref, DerefMut)]
struct Name(String);
#[derive(Resource, Deref, DerefMut)]
struct KeyboardVisible(bool);
#[derive(Resource, Deref, DerefMut)]
struct WasPenButtonPressed(bool);
#[derive(Resource, Deref, DerefMut)]
struct PickedColor(Color32);
#[derive(Component)]
pub struct LookedAt;
#[derive(Resource, Serialize, Deserialize)]
pub struct KneeboardPosition {
position: Vec3,
rotation: Quat,
}
impl Default for KneeboardPosition {
fn default() -> Self {
Self {
position: Vec3::new(0.15, -0.25, -0.40),
rotation: Default::default(),
}
}
}
#[derive(Resource, Serialize, Deserialize, Default)]
pub struct KneeboardNotepad(String);
pub struct APadPlugin;
impl Plugin for APadPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(OtdIpcPlugin)
.add_plugins(EguiPlugin::default())
.add_plugins(MaterialPlugin::<MyCustomMaterial>::default());
app.insert_resource(PkvStore::new("Avii", "Kneeboard"))
.init_persistent_resource::<KneeboardPosition>()
.init_persistent_resource::<KneeboardNotepad>();
app.insert_resource(TabletSize(Vec2::new(210.0, 279.0)));
app.insert_resource(TabletResolutionScale(2.5));
app.insert_resource(PointerImage(None));
app.insert_resource(Name("".to_string()));
app.insert_resource(KeyboardVisible(false));
app.insert_resource(WasPenButtonPressed(false));
app.insert_resource(PickedColor(Color32::WHITE));
app.add_systems(Startup, (setup_pointer, setup_egui.after(setup_pointer)))
.add_systems(
Update,
(
gaze,
position_kneeboard,
move_kneeboard,
sync_camera_with_kneeboard,
)
.chain(),
)
.add_systems(Update, update_pointer.in_set(EguiInputSet::WriteEguiEvents))
.add_systems(Update, render_pointer)
.add_systems(WorldspaceContextPass, update);
load_internal_asset!(
app,
FRAGMENT_SHADER_HANDLE,
"../../assets/shaders/shader.wgsl",
Shader::from_wgsl
);
}
}
fn setup_pointer(
mut images: ResMut<Assets<Image>>,
tablet_size: Res<TabletSize>,
tablet_res: Res<TabletResolutionScale>,
mut pointer: ResMut<PointerImage>,
) {
let image = images.add({
let size = Extent3d {
width: (tablet_size.x * **tablet_res) as u32,
height: (tablet_size.y * **tablet_res) as u32,
depth_or_array_layers: 1,
};
let mut image = Image {
data: Some(vec![0; (size.width * size.height * 4) as usize]),
..default()
};
image.texture_descriptor.usage |= TextureUsages::RENDER_ATTACHMENT;
image.texture_descriptor.size = size;
image
});
pointer.0 = Some(image);
}
fn update_pointer(
tablet_size: Res<TabletSize>,
tablet_res: Res<TabletResolutionScale>,
ent: Single<Entity, With<bevy_egui::EguiContext>>,
mut pen_buttons: MessageReader<PenButtons>,
mut pen_delta: MessageReader<PenDelta>,
mut pen_position: MessageReader<PenPosition>,
mut input_writer: MessageWriter<EguiInputEvent>,
mut pen_button_pressed: ResMut<WasPenButtonPressed>,
) {
for pos in pen_position.read() {
let x = pos.x * tablet_size.x * **tablet_res;
let y = pos.y * tablet_size.y * **tablet_res;
input_writer.write(EguiInputEvent {
context: *ent,
event: egui::Event::PointerMoved(Pos2::new(x, y)),
});
for button in pen_buttons.read() {
if button.a() && button.tip() {
for delta in pen_delta.read() {
input_writer.write(EguiInputEvent {
context: *ent,
event: egui::Event::MouseWheel {
unit: egui::MouseWheelUnit::Line,
delta: egui::Vec2::new((-delta.x) * 20., (-delta.y) * 20.),
modifiers: default(),
},
});
}
continue;
}
// this is making me drop inputs or something
if **pen_button_pressed != button.tip() {
input_writer.write(EguiInputEvent {
context: *ent,
event: egui::Event::PointerButton {
pos: Pos2::new(x, y),
button: PointerButton::Primary,
pressed: button.tip(),
modifiers: Default::default(),
},
});
**pen_button_pressed = button.tip();
}
}
}
}
fn render_pointer(
tablet_size: Res<TabletSize>,
tablet_res: Res<TabletResolutionScale>,
mesh_material: Single<&MeshMaterial3d<MyCustomMaterial>, With<Kneeboard>>,
mut pen_position: MessageReader<PenPosition>,
mut materials: ResMut<Assets<MyCustomMaterial>>,
mut images: ResMut<Assets<Image>>,
mut pointer: ResMut<PointerImage>,
) {
let Some(ref mut pointer) = pointer.0 else {
return;
};
let Some(image) = images.get_mut(&*pointer) else {
return;
};
image.clear(&[0u8; 4]);
for pos in pen_position.read() {
let x = pos.x * tablet_size.x * **tablet_res;
let y = pos.y * tablet_size.y * **tablet_res;
draw_pointer(image, x, y);
if let Some(material) = materials.get_mut(mesh_material.0.id()) {
material.extension.cursor_texture = Some(pointer.clone());
}
}
}
fn draw_pointer(image: &mut Image, x: f32, y: f32) {
let x = x as i32;
let y = y as i32;
let distance = 3;
let length = 8;
// let thickness = 2;
for dx in (x - length - distance - 1)..=(x - distance - 1) {
let mut color = css::BLACK.into();
if dx == (x - length - distance - 1) || dx == (x - distance - 1) {
color = css::WHITE.into();
}
draw_pixel(image, dx, y - 1, css::WHITE.into());
draw_pixel(image, dx, y, color);
draw_pixel(image, dx, y + 1, css::WHITE.into());
}
for dx in (x + distance + 1)..=(x + length + distance + 1) {
let mut color = css::BLACK.into();
if dx == (x + distance + 1) || dx == (x + length + distance + 1) {
color = css::WHITE.into();
}
draw_pixel(image, dx, y - 1, css::WHITE.into());
draw_pixel(image, dx, y, color);
draw_pixel(image, dx, y + 1, css::WHITE.into());
}
for dy in (y - length - distance - 1)..=(y - distance - 1) {
let mut color = css::BLACK.into();
if dy == (y - length - distance - 1) || dy == (y - distance - 1) {
color = css::WHITE.into();
}
draw_pixel(image, x - 1, dy, css::WHITE.into());
draw_pixel(image, x, dy, color);
draw_pixel(image, x + 1, dy, css::WHITE.into());
}
for dy in (y + distance + 1)..=(y + length + distance + 1) {
let mut color = css::BLACK.into();
if dy == (y + distance + 1) || dy == (y + length + distance + 1) {
color = css::WHITE.into();
}
draw_pixel(image, x - 1, dy, css::WHITE.into());
draw_pixel(image, x, dy, color);
draw_pixel(image, x + 1, dy, css::WHITE.into());
}
}
fn draw_pixel(image: &mut Image, x: i32, y: i32, color: Color) {
if x < 0 || x > image.width() as i32 || y < 0 || y > image.height() as i32 {
return;
}
image.set_color_at(x as u32, y as u32, color).ok();
}
fn setup_egui(
mut commands: Commands,
mut images: ResMut<Assets<Image>>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<MyCustomMaterial>>,
mut egui_global_settings: ResMut<EguiGlobalSettings>,
tablet_size: Res<TabletSize>,
tablet_res: Res<TabletResolutionScale>,
pointer: Res<PointerImage>,
) {
egui_global_settings.auto_create_primary_context = false;
egui_global_settings.enable_cursor_icon_updates = false;
let image = images.add({
let size = Extent3d {
width: (tablet_size.x * **tablet_res) as u32,
height: (tablet_size.y * **tablet_res) as u32,
depth_or_array_layers: 1,
};
let mut image = Image {
data: Some(vec![0; (size.width * size.height * 4) as usize]),
..default()
};
image.texture_descriptor.usage |= TextureUsages::RENDER_ATTACHMENT;
image.texture_descriptor.size = size;
image
});
commands.spawn((
Camera3d::default(),
RenderLayers::none(),
Camera::default(),
RenderTarget::Image(image.clone().into()),
EguiMultipassSchedule::new(WorldspaceContextPass),
));
commands.spawn((
Mesh3d(meshes.add(Cuboid::new(
tablet_size.x / 1000.,
tablet_size.y / 1000.,
0.01,
))),
MeshMaterial3d(materials.add(MyCustomMaterial {
base: StandardMaterial {
base_color: css::WHITE.into(),
unlit: true,
..Default::default()
},
extension: CustomMaterial {
ui_texture: Some(image.clone()),
cursor_texture: pointer.clone(),
},
})),
Kneeboard,
));
let keyboard = VirtualKeyboard::default();
commands.spawn(Keyboard(keyboard));
}
fn update(
mut ctx: Single<&mut bevy_egui::EguiContext>,
mut input: Single<&mut bevy_egui::EguiInput>,
mut keyboard: Single<&mut Keyboard>,
mut notepad: ResMut<KneeboardNotepad>,
mo: Res<Overview>,
sr: Res<Sitrep>,
pr: Res<PilotRoster>,
pe: Res<PackageElements>,
ta: Res<ThreatAnalysis>,
sp: Res<Steerpoints>,
cl: Res<Commladder>,
or: Res<Ordnance>,
wt: Res<Weather>,
su: Res<Support>,
ro: Res<RulesOfEngagement>,
ep: Res<Emergency>,
) {
ctx.get_mut().options_mut(|opt| {
opt.input_options.max_click_dist = 24.0;
opt.input_options.max_click_duration = 1.0;
});
let margins = egui::containers::Frame {
inner_margin: Margin::same(15),
..Default::default()
};
let focus = keyboard.is_active(ctx.get_mut());
let height = if focus { 200.0 } else { 0.0 };
egui::containers::CentralPanel::default().show(ctx.get_mut(), |ui| {
egui::containers::TopBottomPanel::bottom("bottom_panel")
.resizable(false)
.height_range(egui::Rangef::new(0., height))
.show_inside(ui, |ui| {
ui.with_layout(Layout::top_down_justified(egui::Align::Center), |ui| {
keyboard.show(ui);
});
});
ui.with_layout(Layout::top_down_justified(egui::Align::LEFT), |ui| {
let rect = ui
.add(TextEdit::multiline(&mut notepad.0).margin(Margin::same(8)))
.rect;
global_theme_preference_switch(ui, &rect);
});
egui::containers::CentralPanel::default()
.frame(margins)
.show_inside(ui, |ui| {
egui::ScrollArea::vertical()
// .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden)
.scroll_source(ScrollSource::MOUSE_WHEEL | ScrollSource::SCROLL_BAR)
.show(ui, |ui| {
overview(ui, &mo);
ui.add(egui::Separator::default().grow(8.0));
sitrep(ui, &sr);
ui.add(egui::Separator::default().grow(8.0));
pilot_roster(ui, &pr);
ui.add(egui::Separator::default().grow(8.0));
package_elements(ui, &pe);
ui.add(egui::Separator::default().grow(8.0));
threat_analysis(ui, &ta);
ui.add(egui::Separator::default().grow(8.0));
steerpoints(ui, &sp);
ui.add(egui::Separator::default().grow(8.0));
commladder(ui, &cl);
ui.add(egui::Separator::default().grow(8.0));
ordnance(ui, &or, &mo);
ui.add(egui::Separator::default().grow(8.0));
weather(ui, &wt);
ui.add(egui::Separator::default().grow(8.0));
support(ui, &su);
ui.add(egui::Separator::default().grow(8.0));
rulesofengagement(ui, &ro);
ui.add(egui::Separator::default().grow(8.0));
emergency(ui, &ep);
});
});
});
keyboard.bump_events(ctx.get_mut(), &mut input.0);
}
pub fn global_theme_preference_switch(ui: &mut Ui, rect: &egui::Rect) {
let theme = ui.ctx().theme();
let icon = if theme == Theme::Dark { "" } else { "🌙" };
let new_theme = if theme == Theme::Dark {
Theme::Light
} else {
Theme::Dark
};
let size = 16.;
let min = egui::pos2(rect.max.x - size, rect.min.y - 2.);
let max = egui::pos2(rect.max.x, size - 2.);
let new_rect = Rect::from_min_max(min, max);
if ui.put(new_rect, Button::new(icon).frame(false)).clicked() {
ui.ctx().set_theme(new_theme);
}
}
fn sync_camera_with_kneeboard(
kneeboard: Query<&Transform, With<Kneeboard>>,
mut cameras: Query<&mut Transform, (With<MainCamera>, Without<Kneeboard>)>,
) {
let Ok(kneeboard) = kneeboard.single() else {
return;
};
let Ok(mut camera) = cameras.single_mut() else {
return;
};
let mut transform = *kneeboard;
transform.translation -= kneeboard.back().normalize() * 0.35;
transform.rotation *= Quat::from_axis_angle(Dir3::X.normalize(), PI)
* Quat::from_axis_angle(Dir3::Z.normalize(), PI);
*camera = transform;
}
fn gaze(
mut commands: Commands,
position: Res<KneeboardPosition>,
kneeboard: Single<Entity, With<Kneeboard>>,
head: Single<&Transform, With<Headset>>,
) {
// let head = head;
// let kneeboard = kneeboard.single().expect("a kneeboard to exist");
let facing = head.forward().normalize();
let to_target = (position.position - head.translation).normalize();
let dot = facing.dot(to_target);
let cos_fov_angle: f32 = (35.0f32 / 2.0f32).to_radians().cos();
commands.entity(kneeboard.entity()).remove::<LookedAt>();
if dot < cos_fov_angle {
commands.entity(kneeboard.entity()).insert(LookedAt);
}
}
#[allow(clippy::type_complexity)]
fn position_kneeboard(
mut transform: Query<(&mut Transform, Option<&LookedAt>), (With<Kneeboard>, Without<Headset>)>,
kneeboard: Res<KneeboardPosition>,
head: Query<&Transform, (With<Headset>, Without<Kneeboard>)>,
) {
let Ok((mut transform, looked_at)) = transform.single_mut() else {
return;
};
let Ok(head) = head.single() else {
return;
};
transform.translation = kneeboard.position;
transform.rotation = kneeboard.rotation;
if looked_at.is_some() {
transform.translation = head.translation;
transform.rotation = head.rotation;
}
}
fn move_kneeboard(
session: Option<Res<OxrSession>>,
left: Option<Res<LeftControllerActions>>,
right: Option<Res<RightControllerActions>>,
left_transform: Query<&Transform, (With<LeftController>, Without<RightController>)>,
right_transform: Query<&Transform, (With<RightController>, Without<LeftController>)>,
mut kneeboard: ResMut<KneeboardPosition>,
) {
let Some(session) = session else {
return;
};
let Some(left) = left else {
return;
};
let Some(right) = right else {
return;
};
let rot_offset: Quat = Quat::from_axis_angle(Vec3::new(1.0, 0.0, 0.0), FRAC_PI_2)
* Quat::from_axis_angle(Vec3::new(0.0, 0.0, 1.0), PI);
if let Ok(trigger_state) = left.squeeze.state(&session, Path::NULL)
&& trigger_state.current_state >= 1.0
{
let Ok(transform) = left_transform.single() else {
return;
};
let pos_offset = transform.up().normalize() * -0.1;
kneeboard.position = transform.translation + pos_offset;
kneeboard.rotation = transform.rotation * rot_offset;
}
if let Ok(trigger_state) = right.squeeze.state(&session, Path::NULL)
&& trigger_state.current_state >= 1.0
{
let Ok(transform) = right_transform.single() else {
return;
};
let pos_offset = transform.up().normalize() * -0.1;
kneeboard.position = transform.translation + pos_offset;
kneeboard.rotation = transform.rotation * rot_offset;
}
}