embrace the egui
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
6953
Cargo.lock
generated
Normal file
6953
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
Cargo.toml
Normal file
31
Cargo.toml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
[package]
|
||||||
|
name = "kneeboard2"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
description = "Bevy powered Kneeboard OpenXR Overlay"
|
||||||
|
license = "MIT/Apache-2.0"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bevy = { version = "0.18", features = ["debug"] }
|
||||||
|
# bevy_cef = { path = "../bevy_cef" }
|
||||||
|
# bevy_mod_xr = { git = "https://git.avii.nl/Avii/bevy_oxr.git", version = "0.5.0" }
|
||||||
|
# bevy_mod_openxr = { git = "https://git.avii.nl/Avii/bevy_oxr.git", version = "0.5.0" }
|
||||||
|
|
||||||
|
bevy_mod_xr = { path = "../bevy_oxr/crates/bevy_xr" }
|
||||||
|
bevy_mod_openxr = { path = "../bevy_oxr/crates/bevy_openxr" }
|
||||||
|
|
||||||
|
bevy_pkv = "0.15.0"
|
||||||
|
openxr = "0.21.1"
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
otd-ipc = { path = "../../otd-ipc-client-rs" }
|
||||||
|
|
||||||
|
triple_buffer = "8.1.1"
|
||||||
|
|
||||||
|
bevy_egui = "0.39.1"
|
||||||
|
egui = "0.33.3"
|
||||||
|
|
||||||
|
bms-briefing-parser = { git = "https://github.com/AviiNL/bms-kneeboard-server" }
|
||||||
|
clap = { version = "4.5.60", features = ["derive"] }
|
||||||
|
egui_extras = "0.33.3"
|
||||||
|
encoding_rs_io = "0.1.7"
|
||||||
|
encoding_rs = "0.8.35"
|
||||||
24
assets/shaders/shader.wgsl
Normal file
24
assets/shaders/shader.wgsl
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#import bevy_pbr::forward_io::{VertexOutput, FragmentOutput}
|
||||||
|
|
||||||
|
@group(#{MATERIAL_BIND_GROUP}) @binding(101) var ui_texture: texture_2d<f32>;
|
||||||
|
@group(#{MATERIAL_BIND_GROUP}) @binding(102) var ui_texture_sampler: sampler;
|
||||||
|
@group(#{MATERIAL_BIND_GROUP}) @binding(103) var cursor_texture: texture_2d<f32>;
|
||||||
|
@group(#{MATERIAL_BIND_GROUP}) @binding(104) var cursor_texture_sampler: sampler;
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fragment(
|
||||||
|
in: VertexOutput,
|
||||||
|
) -> FragmentOutput {
|
||||||
|
var out: FragmentOutput;
|
||||||
|
|
||||||
|
// textureSample(material_color_texture, material_color_sampler, in.uv);
|
||||||
|
|
||||||
|
var surface = textureSample(ui_texture, ui_texture_sampler, in.uv);
|
||||||
|
var overlay = textureSample(cursor_texture, cursor_texture_sampler, in.uv);
|
||||||
|
|
||||||
|
var result_rgb = mix(surface.rgb, overlay.rgb, overlay.a);
|
||||||
|
|
||||||
|
out.color = vec4(result_rgb, 1.0);
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
31
src/apad/custom_material.rs
Normal file
31
src/apad/custom_material.rs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
use bevy::{
|
||||||
|
asset::{Asset, Handle},
|
||||||
|
image::Image,
|
||||||
|
pbr::{ExtendedMaterial, MaterialExtension, StandardMaterial},
|
||||||
|
reflect::TypePath,
|
||||||
|
render::render_resource::AsBindGroup,
|
||||||
|
shader::ShaderRef,
|
||||||
|
};
|
||||||
|
|
||||||
|
const SHADER_ASSET_PATH: &str = "shaders/shader.wgsl";
|
||||||
|
|
||||||
|
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
|
||||||
|
pub struct CustomMaterial {
|
||||||
|
#[texture(101)]
|
||||||
|
#[sampler(102)]
|
||||||
|
pub ui_texture: Option<Handle<Image>>,
|
||||||
|
|
||||||
|
#[texture(103)]
|
||||||
|
#[sampler(104)]
|
||||||
|
pub cursor_texture: Option<Handle<Image>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Material trait is very configurable, but comes with sensible defaults for all methods.
|
||||||
|
/// You only need to implement functions for features that need non-default behavior. See the Material api docs for details!
|
||||||
|
impl MaterialExtension for CustomMaterial {
|
||||||
|
fn fragment_shader() -> ShaderRef {
|
||||||
|
SHADER_ASSET_PATH.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type MyCustomMaterial = ExtendedMaterial<StandardMaterial, CustomMaterial>;
|
||||||
1294
src/apad/keyboard.rs
Normal file
1294
src/apad/keyboard.rs
Normal file
File diff suppressed because it is too large
Load Diff
508
src/apad/mod.rs
Normal file
508
src/apad/mod.rs
Normal file
@@ -0,0 +1,508 @@
|
|||||||
|
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},
|
||||||
|
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},
|
||||||
|
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::{Color32, Margin, PointerButton, Pos2};
|
||||||
|
use openxr::Path;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
// PUB
|
||||||
|
pub use crate::apad::otdipcplugin::TabletRotation;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
MainCamera,
|
||||||
|
apad::{
|
||||||
|
custom_material::{CustomMaterial, MyCustomMaterial},
|
||||||
|
keyboard::VirtualKeyboard,
|
||||||
|
otdipcplugin::{OtdIpcPlugin, PenButtons, PenDelta, PenPosition},
|
||||||
|
},
|
||||||
|
egui_pages::{Overview, overview},
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>();
|
||||||
|
|
||||||
|
app.insert_resource(TabletSize(Vec2::new(210.0, 279.0)));
|
||||||
|
app.insert_resource(TabletResolutionScale(2.25));
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.add_systems(Update, pointer.in_set(EguiInputSet::InitReading))
|
||||||
|
.add_systems(WorldspaceContextPass, update);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 pointer(
|
||||||
|
tablet_size: Res<TabletSize>,
|
||||||
|
tablet_res: Res<TabletResolutionScale>,
|
||||||
|
ent: Single<Entity, With<bevy_egui::EguiContext>>,
|
||||||
|
mesh_material: Single<&MeshMaterial3d<MyCustomMaterial>, With<Kneeboard>>,
|
||||||
|
mut pen_buttons: MessageReader<PenButtons>,
|
||||||
|
mut pen_delta: MessageReader<PenDelta>,
|
||||||
|
mut pen_position: MessageReader<PenPosition>,
|
||||||
|
mut input_writer: MessageWriter<EguiInputEvent>,
|
||||||
|
mut materials: ResMut<Assets<MyCustomMaterial>>,
|
||||||
|
mut images: ResMut<Assets<Image>>,
|
||||||
|
mut pointer: ResMut<PointerImage>,
|
||||||
|
mut pen_button_pressed: ResMut<WasPenButtonPressed>,
|
||||||
|
) {
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>,
|
||||||
|
|
||||||
|
mo: Res<Overview>,
|
||||||
|
) {
|
||||||
|
let bgcolor = egui::containers::Frame {
|
||||||
|
fill: egui::Color32::from_rgb(43, 44, 47),
|
||||||
|
inner_margin: Margin {
|
||||||
|
left: 15,
|
||||||
|
right: 15,
|
||||||
|
top: 15,
|
||||||
|
bottom: 15,
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let focus = keyboard.is_active(ctx.get_mut());
|
||||||
|
|
||||||
|
let height = if focus { 200.0 } else { 0.0 };
|
||||||
|
|
||||||
|
egui::containers::CentralPanel::default()
|
||||||
|
.frame(bgcolor)
|
||||||
|
.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.vertical_centered(|ui| {
|
||||||
|
keyboard.show(ui);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||||
|
overview(ui, mo);
|
||||||
|
|
||||||
|
// ..
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
keyboard.bump_events(ctx.get_mut(), &mut input.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
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_click.state(&session, Path::NULL)
|
||||||
|
&& trigger_state.current_state
|
||||||
|
{
|
||||||
|
dbg!("squeeze triggered");
|
||||||
|
|
||||||
|
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_click.state(&session, Path::NULL)
|
||||||
|
&& trigger_state.current_state
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
149
src/apad/otdipcplugin.rs
Normal file
149
src/apad/otdipcplugin.rs
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
#![allow(unused)]
|
||||||
|
|
||||||
|
use bevy::{
|
||||||
|
app::{App, Plugin, PreUpdate},
|
||||||
|
ecs::{
|
||||||
|
message::MessageWriter,
|
||||||
|
resource::Resource,
|
||||||
|
system::{Res, ResMut},
|
||||||
|
},
|
||||||
|
math::Vec2,
|
||||||
|
prelude::Deref,
|
||||||
|
};
|
||||||
|
use otd_ipc::{Message, OtdIpc};
|
||||||
|
use triple_buffer::{Output, triple_buffer};
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
struct OtdChannel(Output<Option<Message>>);
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
struct LastPosition(Option<Vec2>);
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
struct TabletSize(Option<Vec2>);
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
struct MaxPenPressure(Option<u32>);
|
||||||
|
|
||||||
|
#[derive(bevy::ecs::message::Message, Deref, Debug)]
|
||||||
|
pub struct PenPosition(Vec2);
|
||||||
|
|
||||||
|
#[derive(bevy::ecs::message::Message, Deref, Debug)]
|
||||||
|
pub struct PenDelta(Vec2);
|
||||||
|
|
||||||
|
#[derive(bevy::ecs::message::Message, Deref, Debug)]
|
||||||
|
pub struct PenButtons(u32);
|
||||||
|
|
||||||
|
impl PenButtons {
|
||||||
|
pub fn tip(&self) -> bool {
|
||||||
|
self.0 & 1 == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn a(&self) -> bool {
|
||||||
|
self.0 & 2 == 2
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn b(&self) -> bool {
|
||||||
|
self.0 & 4 == 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(bevy::ecs::message::Message)]
|
||||||
|
pub struct PenPressure {
|
||||||
|
pub pressure: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
|
#[allow(unused)]
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub enum TabletRotation {
|
||||||
|
Default,
|
||||||
|
CW,
|
||||||
|
UpsideDown,
|
||||||
|
CCW,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OtdIpcPlugin;
|
||||||
|
|
||||||
|
impl Plugin for OtdIpcPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
let (mut tx, rx) = triple_buffer(&None);
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let otd_ipc = OtdIpc::new("Kneeboard", "master").unwrap();
|
||||||
|
|
||||||
|
for msg in otd_ipc {
|
||||||
|
tx.write(Some(msg));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
app.add_message::<PenDelta>();
|
||||||
|
app.add_message::<PenPosition>();
|
||||||
|
app.add_message::<PenPressure>();
|
||||||
|
app.add_message::<PenButtons>();
|
||||||
|
app.insert_resource(TabletRotation::Default);
|
||||||
|
app.insert_resource(OtdChannel(rx));
|
||||||
|
app.insert_resource(LastPosition(None));
|
||||||
|
app.insert_resource(MaxPenPressure(None));
|
||||||
|
app.insert_resource(TabletSize(None));
|
||||||
|
app.add_systems(PreUpdate, reader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn reader(
|
||||||
|
rotation: Res<TabletRotation>,
|
||||||
|
mut last_position: ResMut<LastPosition>,
|
||||||
|
mut channel: ResMut<OtdChannel>,
|
||||||
|
mut size: ResMut<TabletSize>,
|
||||||
|
mut pressure: ResMut<MaxPenPressure>,
|
||||||
|
mut pressure_writer: MessageWriter<PenPressure>,
|
||||||
|
mut position_writer: MessageWriter<PenPosition>,
|
||||||
|
mut delta_writer: MessageWriter<PenDelta>,
|
||||||
|
mut button_writer: MessageWriter<PenButtons>,
|
||||||
|
) {
|
||||||
|
let Some(msg) = channel.0.read() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match msg {
|
||||||
|
otd_ipc::Message::DeviceInfo(info) => {
|
||||||
|
size.0 = Some(Vec2::new(info.max_x, info.max_y));
|
||||||
|
pressure.0 = Some(info.max_pressure);
|
||||||
|
}
|
||||||
|
otd_ipc::Message::State(state) => {
|
||||||
|
let Some(size) = size.0 else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(pressure) = pressure.0 else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
button_writer.write(PenButtons(state.pen_buttons()));
|
||||||
|
|
||||||
|
let p = state.pressure() as f32 / pressure as f32;
|
||||||
|
|
||||||
|
pressure_writer.write(PenPressure { pressure: p });
|
||||||
|
|
||||||
|
let x = state.x();
|
||||||
|
let y = state.y();
|
||||||
|
|
||||||
|
let loc = match *rotation {
|
||||||
|
TabletRotation::Default => Vec2::new(x / size.x, y / size.y),
|
||||||
|
TabletRotation::CCW => Vec2::new(y / size.y, (size.x - x) / size.x),
|
||||||
|
TabletRotation::CW => Vec2::new((size.y - y) / size.y, x / size.x),
|
||||||
|
TabletRotation::UpsideDown => {
|
||||||
|
Vec2::new((size.x - x) / size.x, (size.y - y) / size.y)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let lpos = last_position.0.unwrap_or(loc);
|
||||||
|
|
||||||
|
delta_writer.write(PenDelta(lpos - loc));
|
||||||
|
|
||||||
|
position_writer.write(PenPosition(loc));
|
||||||
|
|
||||||
|
last_position.0 = Some(loc);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
80
src/egui_pages/mod.rs
Normal file
80
src/egui_pages/mod.rs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
mod overview;
|
||||||
|
|
||||||
|
use std::{fs::File, io::Read, path::PathBuf};
|
||||||
|
|
||||||
|
use bevy::{
|
||||||
|
app::{App, Plugin, Update},
|
||||||
|
ecs::{
|
||||||
|
resource::Resource,
|
||||||
|
system::{Res, ResMut},
|
||||||
|
},
|
||||||
|
time::{Time, Timer, TimerMode},
|
||||||
|
};
|
||||||
|
use encoding_rs::WINDOWS_1252;
|
||||||
|
use encoding_rs_io::DecodeReaderBytesBuilder;
|
||||||
|
pub use overview::*;
|
||||||
|
|
||||||
|
use crate::BriefingPath;
|
||||||
|
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
struct RawBriefing(String);
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
struct RefreshTimer(Timer);
|
||||||
|
|
||||||
|
pub struct BmsPlugin {
|
||||||
|
briefing_path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BmsPlugin {
|
||||||
|
pub fn new(briefing_path: PathBuf) -> Self {
|
||||||
|
Self { briefing_path }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plugin for BmsPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.init_resource::<Overview>();
|
||||||
|
app.init_resource::<RawBriefing>();
|
||||||
|
|
||||||
|
app.insert_resource(BriefingPath(self.briefing_path.clone()));
|
||||||
|
|
||||||
|
let mut timer = Timer::from_seconds(5.0, TimerMode::Repeating);
|
||||||
|
timer.almost_finish();
|
||||||
|
app.insert_resource(RefreshTimer(timer));
|
||||||
|
|
||||||
|
app.add_systems(Update, update_briefing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_briefing(
|
||||||
|
time: Res<Time>,
|
||||||
|
briefing_path: Res<BriefingPath>,
|
||||||
|
mut timer: ResMut<RefreshTimer>,
|
||||||
|
mut briefing: ResMut<RawBriefing>,
|
||||||
|
mut overview: ResMut<Overview>,
|
||||||
|
) {
|
||||||
|
if !timer.0.tick(time.delta()).just_finished() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut path = briefing_path.clone();
|
||||||
|
path.push("briefing.txt");
|
||||||
|
|
||||||
|
let file = File::open(path).unwrap();
|
||||||
|
|
||||||
|
let mut buf = String::new();
|
||||||
|
|
||||||
|
DecodeReaderBytesBuilder::new()
|
||||||
|
.encoding(Some(WINDOWS_1252))
|
||||||
|
.build(file)
|
||||||
|
.read_to_string(&mut buf)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if briefing.0 != buf {
|
||||||
|
*overview = bms_briefing_parser::Overview::from_briefing(&buf).into();
|
||||||
|
|
||||||
|
briefing.0 = buf;
|
||||||
|
println!("Updated...");
|
||||||
|
}
|
||||||
|
}
|
||||||
131
src/egui_pages/overview.rs
Normal file
131
src/egui_pages/overview.rs
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
use bevy::ecs::{resource::Resource, system::Res};
|
||||||
|
use egui::{Label, Ui};
|
||||||
|
use egui_extras::*;
|
||||||
|
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
pub struct Overview {
|
||||||
|
pub callsign: String,
|
||||||
|
pub mission_type: String,
|
||||||
|
pub package_id: i32,
|
||||||
|
pub package_description: String,
|
||||||
|
pub package_mission: String,
|
||||||
|
pub target_area: String,
|
||||||
|
pub time_on_target: String,
|
||||||
|
pub sunrise: String,
|
||||||
|
pub sunset: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<bms_briefing_parser::Overview<'_>> for Overview {
|
||||||
|
fn from(value: bms_briefing_parser::Overview) -> Self {
|
||||||
|
let sunrise = value
|
||||||
|
.sunrise
|
||||||
|
.replace('(', "\n")
|
||||||
|
.to_string()
|
||||||
|
.trim_end_matches(')')
|
||||||
|
.to_string();
|
||||||
|
let sunset = value
|
||||||
|
.sunset
|
||||||
|
.replace('(', "\n")
|
||||||
|
.trim_end_matches(')')
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
callsign: value.callsign.to_string(),
|
||||||
|
mission_type: value.mission_type.to_string(),
|
||||||
|
package_id: value.package_id,
|
||||||
|
package_description: value.package_description.to_string(),
|
||||||
|
package_mission: value.package_mission.to_string(),
|
||||||
|
target_area: value.target_area.to_string(),
|
||||||
|
time_on_target: value.time_on_target.to_string(),
|
||||||
|
sunrise,
|
||||||
|
sunset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// use egui::Response;
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
pub fn overview(ui: &mut Ui, overview: Res<Overview>) {
|
||||||
|
|
||||||
|
let text_height = egui::TextStyle::Body
|
||||||
|
.resolve(ui.style())
|
||||||
|
.size
|
||||||
|
.max(ui.spacing().interact_size.y);
|
||||||
|
|
||||||
|
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.heading(format!("Mission Overview - {}", overview.callsign));
|
||||||
|
});
|
||||||
|
|
||||||
|
let table = TableBuilder::new(ui)
|
||||||
|
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
|
||||||
|
.column(Column::remainder())
|
||||||
|
.column(Column::remainder())
|
||||||
|
.column(Column::remainder())
|
||||||
|
.column(Column::remainder());
|
||||||
|
|
||||||
|
table
|
||||||
|
.striped(true)
|
||||||
|
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
|
||||||
|
.header(20.0, |mut header| {
|
||||||
|
header.col(|ui| {
|
||||||
|
ui.strong("Mission Type");
|
||||||
|
});
|
||||||
|
header.col(|ui| {
|
||||||
|
ui.strong("Package #");
|
||||||
|
});
|
||||||
|
header.col(|ui| {
|
||||||
|
ui.strong("Description");
|
||||||
|
});
|
||||||
|
header.col(|ui| {
|
||||||
|
ui.strong("Mission");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.body(|mut body| {
|
||||||
|
body.row(text_height, |mut row| {
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.add(Label::new(&overview.mission_type).wrap_mode(egui::TextWrapMode::Wrap));
|
||||||
|
});
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.add(Label::new(format!("{}", overview.package_id)).wrap_mode(egui::TextWrapMode::Wrap));
|
||||||
|
});
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.add(Label::new(&overview.package_description).wrap_mode(egui::TextWrapMode::Wrap));
|
||||||
|
});
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.add(Label::new(&overview.package_mission).wrap_mode(egui::TextWrapMode::Wrap));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
body.row(text_height, |mut row| {
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.strong("Target Area");
|
||||||
|
});
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.strong("Time on Target");
|
||||||
|
});
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.strong("Sunrise");
|
||||||
|
});
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.strong("Sunset");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
body.row(text_height, |mut row| {
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.add(Label::new(&overview.target_area).wrap_mode(egui::TextWrapMode::Wrap));
|
||||||
|
});
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.add(Label::new(&overview.time_on_target).wrap_mode(egui::TextWrapMode::Wrap));
|
||||||
|
});
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.add(Label::new(&overview.sunrise).wrap_mode(egui::TextWrapMode::Wrap));
|
||||||
|
});
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.add(Label::new(&overview.sunset).wrap_mode(egui::TextWrapMode::Wrap));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
77
src/main.rs
Normal file
77
src/main.rs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
#![allow(clippy::too_many_arguments)]
|
||||||
|
|
||||||
|
mod apad;
|
||||||
|
mod egui_pages;
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
mod vrcontrollerplugin;
|
||||||
|
#[allow(unused)]
|
||||||
|
mod vrplugin;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
use vrcontrollerplugin::VrControllersPlugin;
|
||||||
|
#[allow(unused)]
|
||||||
|
use vrplugin::VrPlugin;
|
||||||
|
|
||||||
|
use bevy::{
|
||||||
|
prelude::*,
|
||||||
|
window::{PresentMode, WindowResolution},
|
||||||
|
};
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
apad::{APadPlugin, TabletRotation},
|
||||||
|
egui_pages::BmsPlugin,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
#[require(Camera3d)]
|
||||||
|
pub struct MainCamera;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(version, about, long_about = None)]
|
||||||
|
struct Args {
|
||||||
|
/// Directory containing briefing.txt
|
||||||
|
#[clap(trailing_var_arg = true)]
|
||||||
|
briefing_dir: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource, Deref)]
|
||||||
|
pub struct BriefingPath(PathBuf);
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
App::new()
|
||||||
|
// .add_plugins(DefaultPlugins)
|
||||||
|
.add_plugins(VrPlugin)
|
||||||
|
.add_plugins(VrControllersPlugin)
|
||||||
|
.add_plugins(BmsPlugin::new(args.briefing_dir))
|
||||||
|
.add_plugins(APadPlugin)
|
||||||
|
.add_systems(Startup, setup)
|
||||||
|
.insert_resource(TabletRotation::CW)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup(mut commands: Commands, mut window: Query<&mut Window>) {
|
||||||
|
let Ok(mut window) = window.single_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.title = "Kneeboard".to_string();
|
||||||
|
window.resolution = WindowResolution::new(550, 720);
|
||||||
|
window.position = WindowPosition::new(IVec2::new(2560 + 1280, 600));
|
||||||
|
window.present_mode = PresentMode::AutoNoVsync;
|
||||||
|
window.fit_canvas_to_parent = false;
|
||||||
|
window.prevent_default_event_handling = false;
|
||||||
|
|
||||||
|
commands.spawn((
|
||||||
|
Camera {
|
||||||
|
clear_color: ClearColorConfig::Custom(Color::NONE),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
MainCamera,
|
||||||
|
));
|
||||||
|
}
|
||||||
237
src/vrcontrollerplugin.rs
Normal file
237
src/vrcontrollerplugin.rs
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy_mod_openxr::prelude::*;
|
||||||
|
use bevy_mod_xr::session::{XrSessionCreated, session_available};
|
||||||
|
use openxr::{Action, Posef, Vector2f};
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct LeftControllerActions {
|
||||||
|
set: openxr::ActionSet,
|
||||||
|
pose: Action<Posef>,
|
||||||
|
pub thumbstick: Action<Vector2f>,
|
||||||
|
pub trigger: Action<f32>,
|
||||||
|
pub trigger_click: Action<bool>,
|
||||||
|
pub squeeze: Action<f32>,
|
||||||
|
pub squeeze_click: Action<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct RightControllerActions {
|
||||||
|
set: openxr::ActionSet,
|
||||||
|
pose: Action<Posef>,
|
||||||
|
pub thumbstick: Action<Vector2f>,
|
||||||
|
pub trigger: Action<f32>,
|
||||||
|
pub trigger_click: Action<bool>,
|
||||||
|
pub squeeze: Action<f32>,
|
||||||
|
pub squeeze_click: Action<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct LeftController;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct RightController;
|
||||||
|
|
||||||
|
pub struct VrControllersPlugin;
|
||||||
|
|
||||||
|
impl Plugin for VrControllersPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(XrSessionCreated, spawn_left_controller);
|
||||||
|
app.add_systems(XrSessionCreated, spawn_right_controller);
|
||||||
|
|
||||||
|
app.add_systems(XrSessionCreated, attach_set);
|
||||||
|
app.add_systems(
|
||||||
|
PreUpdate,
|
||||||
|
sync_actions
|
||||||
|
.before(OxrActionSetSyncSet)
|
||||||
|
.run_if(openxr_session_running),
|
||||||
|
);
|
||||||
|
|
||||||
|
app.add_systems(Startup, create_actions_left.run_if(session_available));
|
||||||
|
app.add_systems(Startup, create_actions_right.run_if(session_available));
|
||||||
|
|
||||||
|
app.add_systems(
|
||||||
|
OxrSendActionBindings,
|
||||||
|
suggest_action_bindings_left.after(create_actions_left),
|
||||||
|
);
|
||||||
|
app.add_systems(
|
||||||
|
OxrSendActionBindings,
|
||||||
|
suggest_action_bindings_right.after(create_actions_right),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn attach_set(
|
||||||
|
left: Res<LeftControllerActions>,
|
||||||
|
right: Res<RightControllerActions>,
|
||||||
|
mut attach: MessageWriter<OxrAttachActionSet>,
|
||||||
|
) {
|
||||||
|
attach.write(OxrAttachActionSet(left.set.clone()));
|
||||||
|
attach.write(OxrAttachActionSet(right.set.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sync_actions(
|
||||||
|
left: Res<LeftControllerActions>,
|
||||||
|
right: Res<RightControllerActions>,
|
||||||
|
mut sync: MessageWriter<OxrSyncActionSet>,
|
||||||
|
) {
|
||||||
|
sync.write(OxrSyncActionSet(left.set.clone()));
|
||||||
|
sync.write(OxrSyncActionSet(right.set.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn suggest_action_bindings_left(
|
||||||
|
actions: Res<LeftControllerActions>,
|
||||||
|
mut bindings: MessageWriter<OxrSuggestActionBinding>,
|
||||||
|
) {
|
||||||
|
bindings.write(OxrSuggestActionBinding {
|
||||||
|
action: actions.pose.as_raw(),
|
||||||
|
interaction_profile: "/interaction_profiles/bytedance/pico4_controller".into(),
|
||||||
|
bindings: vec!["/user/hand/left/input/grip/pose".into()],
|
||||||
|
});
|
||||||
|
|
||||||
|
bindings.write(OxrSuggestActionBinding {
|
||||||
|
action: actions.trigger.as_raw(),
|
||||||
|
interaction_profile: "/interaction_profiles/bytedance/pico4_controller".into(),
|
||||||
|
bindings: vec!["/user/hand/left/input/trigger/value".into()],
|
||||||
|
});
|
||||||
|
|
||||||
|
bindings.write(OxrSuggestActionBinding {
|
||||||
|
action: actions.trigger_click.as_raw(),
|
||||||
|
interaction_profile: "/interaction_profiles/bytedance/pico4_controller".into(),
|
||||||
|
bindings: vec!["/user/hand/left/input/trigger/click".into()],
|
||||||
|
});
|
||||||
|
|
||||||
|
bindings.write(OxrSuggestActionBinding {
|
||||||
|
action: actions.squeeze.as_raw(),
|
||||||
|
interaction_profile: "/interaction_profiles/bytedance/pico4_controller".into(),
|
||||||
|
bindings: vec!["/user/hand/left/input/squeeze/value".into()],
|
||||||
|
});
|
||||||
|
|
||||||
|
bindings.write(OxrSuggestActionBinding {
|
||||||
|
action: actions.squeeze_click.as_raw(),
|
||||||
|
interaction_profile: "/interaction_profiles/bytedance/pico4_controller".into(),
|
||||||
|
bindings: vec!["/user/hand/left/input/squeeze/click".into()],
|
||||||
|
});
|
||||||
|
|
||||||
|
bindings.write(OxrSuggestActionBinding {
|
||||||
|
action: actions.thumbstick.as_raw(),
|
||||||
|
interaction_profile: "/interaction_profiles/bytedance/pico4_controller".into(),
|
||||||
|
bindings: vec!["/user/hand/left/input/thumbstick".into()],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn suggest_action_bindings_right(
|
||||||
|
actions: Res<RightControllerActions>,
|
||||||
|
mut bindings: MessageWriter<OxrSuggestActionBinding>,
|
||||||
|
) {
|
||||||
|
bindings.write(OxrSuggestActionBinding {
|
||||||
|
action: actions.pose.as_raw(),
|
||||||
|
interaction_profile: "/interaction_profiles/bytedance/pico4_controller".into(),
|
||||||
|
bindings: vec!["/user/hand/right/input/grip/pose".into()],
|
||||||
|
});
|
||||||
|
|
||||||
|
bindings.write(OxrSuggestActionBinding {
|
||||||
|
action: actions.trigger.as_raw(),
|
||||||
|
interaction_profile: "/interaction_profiles/bytedance/pico4_controller".into(),
|
||||||
|
bindings: vec!["/user/hand/right/input/trigger/value".into()],
|
||||||
|
});
|
||||||
|
|
||||||
|
bindings.write(OxrSuggestActionBinding {
|
||||||
|
action: actions.trigger_click.as_raw(),
|
||||||
|
interaction_profile: "/interaction_profiles/bytedance/pico4_controller".into(),
|
||||||
|
bindings: vec!["/user/hand/right/input/trigger/click".into()],
|
||||||
|
});
|
||||||
|
|
||||||
|
bindings.write(OxrSuggestActionBinding {
|
||||||
|
action: actions.squeeze.as_raw(),
|
||||||
|
interaction_profile: "/interaction_profiles/bytedance/pico4_controller".into(),
|
||||||
|
bindings: vec!["/user/hand/right/input/squeeze/value".into()],
|
||||||
|
});
|
||||||
|
|
||||||
|
bindings.write(OxrSuggestActionBinding {
|
||||||
|
action: actions.squeeze_click.as_raw(),
|
||||||
|
interaction_profile: "/interaction_profiles/bytedance/pico4_controller".into(),
|
||||||
|
bindings: vec!["/user/hand/right/input/squeeze/click".into()],
|
||||||
|
});
|
||||||
|
|
||||||
|
bindings.write(OxrSuggestActionBinding {
|
||||||
|
action: actions.thumbstick.as_raw(),
|
||||||
|
interaction_profile: "/interaction_profiles/bytedance/pico4_controller".into(),
|
||||||
|
bindings: vec!["/user/hand/right/input/thumbstick".into()],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_actions_left(instance: Res<OxrInstance>, mut cmds: Commands) {
|
||||||
|
let set = instance
|
||||||
|
.create_action_set("left-controller", "Left Controller", 0)
|
||||||
|
.unwrap();
|
||||||
|
let pose = set.create_action("pose", "Grip Pose", &[]).unwrap();
|
||||||
|
let trigger = set.create_action("trigger", "Trigger", &[]).unwrap();
|
||||||
|
let trigger_click = set
|
||||||
|
.create_action("trigger_click", "Trigger Click", &[])
|
||||||
|
.unwrap();
|
||||||
|
let squeeze = set.create_action("squeeze", "Squeeze", &[]).unwrap();
|
||||||
|
let squeeze_click = set
|
||||||
|
.create_action("squeeze_click", "Squeeze Click", &[])
|
||||||
|
.unwrap();
|
||||||
|
let thumbstick = set.create_action("thumbstick", "Thumbstick", &[]).unwrap();
|
||||||
|
|
||||||
|
cmds.insert_resource(LeftControllerActions {
|
||||||
|
set,
|
||||||
|
pose,
|
||||||
|
thumbstick,
|
||||||
|
trigger,
|
||||||
|
trigger_click,
|
||||||
|
squeeze,
|
||||||
|
squeeze_click,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_actions_right(instance: Res<OxrInstance>, mut cmds: Commands) {
|
||||||
|
let set = instance
|
||||||
|
.create_action_set("right-controller", "Right Controller", 0)
|
||||||
|
.unwrap();
|
||||||
|
let pose = set.create_action("pose", "Grip Pose", &[]).unwrap();
|
||||||
|
let trigger = set.create_action("trigger", "Trigger", &[]).unwrap();
|
||||||
|
let trigger_click = set
|
||||||
|
.create_action("trigger_click", "Trigger Click", &[])
|
||||||
|
.unwrap();
|
||||||
|
let squeeze = set.create_action("squeeze", "Squeeze", &[]).unwrap();
|
||||||
|
let squeeze_click = set
|
||||||
|
.create_action("squeeze_click", "Squeeze Click", &[])
|
||||||
|
.unwrap();
|
||||||
|
let thumbstick = set.create_action("thumbstick", "Thumbstick", &[]).unwrap();
|
||||||
|
|
||||||
|
cmds.insert_resource(RightControllerActions {
|
||||||
|
set,
|
||||||
|
pose,
|
||||||
|
thumbstick,
|
||||||
|
trigger,
|
||||||
|
trigger_click,
|
||||||
|
squeeze,
|
||||||
|
squeeze_click,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_left_controller(
|
||||||
|
left: Res<LeftControllerActions>,
|
||||||
|
mut commands: Commands,
|
||||||
|
session: Res<OxrSession>,
|
||||||
|
) {
|
||||||
|
let space = session
|
||||||
|
.create_action_space(&left.pose, openxr::Path::NULL, Isometry3d::IDENTITY)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
commands.spawn((space, LeftController));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_right_controller(
|
||||||
|
right: Res<RightControllerActions>,
|
||||||
|
mut commands: Commands,
|
||||||
|
session: Res<OxrSession>,
|
||||||
|
) {
|
||||||
|
let space = session
|
||||||
|
.create_action_space(&right.pose, openxr::Path::NULL, Isometry3d::IDENTITY)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
commands.spawn((space, RightController));
|
||||||
|
}
|
||||||
49
src/vrplugin.rs
Normal file
49
src/vrplugin.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
use bevy::{
|
||||||
|
prelude::*,
|
||||||
|
render::render_resource::TextureFormat,
|
||||||
|
window::{PresentMode, WindowResolution},
|
||||||
|
};
|
||||||
|
use bevy_mod_openxr::prelude::*;
|
||||||
|
use bevy_mod_xr::session::XrSessionCreated;
|
||||||
|
|
||||||
|
use crate::vrcontrollerplugin::VrControllersPlugin;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct Headset;
|
||||||
|
|
||||||
|
pub struct VrPlugin;
|
||||||
|
|
||||||
|
impl Plugin for VrPlugin {
|
||||||
|
fn build(&self, app: &mut bevy::app::App) {
|
||||||
|
app.add_plugins(
|
||||||
|
add_xr_plugins(DefaultPlugins)
|
||||||
|
.disable::<HandTrackingPlugin>()
|
||||||
|
.build()
|
||||||
|
.set(OxrInitPlugin {
|
||||||
|
exts: {
|
||||||
|
let mut exts = OxrExtensions::default();
|
||||||
|
exts.extx_overlay = true;
|
||||||
|
exts
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
app.insert_resource(ClearColor(Color::NONE))
|
||||||
|
.insert_resource(OxrSessionConfig {
|
||||||
|
blend_mode_preference: { vec![EnvironmentBlendMode::ALPHA_BLEND] },
|
||||||
|
formats: Some(vec![TextureFormat::Rgba8UnormSrgb]),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
app.add_systems(XrSessionCreated, create_view_space);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_view_space(session: Res<OxrSession>, mut commands: Commands) {
|
||||||
|
let space = session
|
||||||
|
.create_reference_space(openxr::ReferenceSpaceType::VIEW, Isometry3d::IDENTITY)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
commands.spawn((Headset, space.0));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user