Compare commits

...

2 Commits

Author SHA1 Message Date
4296a6488c crosshair is nice 2026-02-23 01:54:44 +01:00
b82a0eb7d4 feat: drawing and stuff 2026-02-22 20:39:23 +01:00
7 changed files with 683 additions and 112 deletions

113
Cargo.lock generated
View File

@@ -219,9 +219,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.101" version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]] [[package]]
name = "approx" name = "approx"
@@ -1120,7 +1120,6 @@ checksum = "7ef8e4b7e61dfe7719bb03c884dc270cd46a82efb40f93e9933b990c5c190c59"
[[package]] [[package]]
name = "bevy_mod_openxr" name = "bevy_mod_openxr"
version = "0.5.0" version = "0.5.0"
source = "git+https://git.avii.nl/Avii/bevy_oxr.git#7936b53a024b993c53fc7427b5b290bf210978a9"
dependencies = [ dependencies = [
"android_system_properties", "android_system_properties",
"ash", "ash",
@@ -1145,7 +1144,6 @@ dependencies = [
[[package]] [[package]]
name = "bevy_mod_xr" name = "bevy_mod_xr"
version = "0.5.0" version = "0.5.0"
source = "git+https://git.avii.nl/Avii/bevy_oxr.git#7936b53a024b993c53fc7427b5b290bf210978a9"
dependencies = [ dependencies = [
"bevy_app", "bevy_app",
"bevy_camera", "bevy_camera",
@@ -2506,9 +2504,9 @@ checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
[[package]] [[package]]
name = "deranged" name = "deranged"
version = "0.5.6" version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
dependencies = [ dependencies = [
"powerfmt", "powerfmt",
] ]
@@ -2536,6 +2534,12 @@ dependencies = [
"unicode-xid", "unicode-xid",
] ]
[[package]]
name = "deunicode"
version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04"
[[package]] [[package]]
name = "directories" name = "directories"
version = "6.0.0" version = "6.0.0"
@@ -2545,6 +2549,15 @@ dependencies = [
"dirs-sys", "dirs-sys",
] ]
[[package]]
name = "dirs"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
dependencies = [
"dirs-sys",
]
[[package]] [[package]]
name = "dirs-sys" name = "dirs-sys"
version = "0.5.0" version = "0.5.0"
@@ -3186,7 +3199,7 @@ dependencies = [
"log", "log",
"presser", "presser",
"thiserror 1.0.69", "thiserror 1.0.69",
"windows 0.58.0", "windows 0.54.0",
] ]
[[package]] [[package]]
@@ -3656,9 +3669,9 @@ dependencies = [
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.85" version = "0.3.88"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" checksum = "c7e709f3e3d22866f9c25b3aff01af289b18422cc8b4262fb19103ee80fe513d"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"wasm-bindgen", "wasm-bindgen",
@@ -3690,9 +3703,12 @@ dependencies = [
"bevy_mod_openxr", "bevy_mod_openxr",
"bevy_mod_xr", "bevy_mod_xr",
"bevy_pkv", "bevy_pkv",
"crossbeam-channel",
"openxr", "openxr",
"otd-ipc",
"rdev", "rdev",
"serde", "serde",
"triple_buffer",
] ]
[[package]] [[package]]
@@ -4467,6 +4483,17 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "otd-ipc"
version = "0.1.0"
dependencies = [
"bytes",
"dirs",
"serde",
"serde_bytes",
"slug",
]
[[package]] [[package]]
name = "owned_ttf_parser" name = "owned_ttf_parser"
version = "0.25.1" version = "0.25.1"
@@ -5025,7 +5052,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys 0.4.15", "linux-raw-sys 0.4.15",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -5157,6 +5184,16 @@ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]]
name = "serde_bytes"
version = "0.11.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8"
dependencies = [
"serde",
"serde_core",
]
[[package]] [[package]]
name = "serde_core" name = "serde_core"
version = "1.0.228" version = "1.0.228"
@@ -5252,6 +5289,16 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "slug"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724"
dependencies = [
"deunicode",
"wasm-bindgen",
]
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.15.1" version = "1.15.1"
@@ -5392,9 +5439,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.116" version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -5720,6 +5767,15 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "triple_buffer"
version = "8.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "420466259f9fa5decc654c490b9ab538400e5420df8237f84ecbe20368bcf72b"
dependencies = [
"crossbeam-utils",
]
[[package]] [[package]]
name = "ttf-parser" name = "ttf-parser"
version = "0.25.1" version = "0.25.1"
@@ -5941,9 +5997,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.108" version = "0.2.111"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" checksum = "ec1adf1535672f5b7824f817792b1afd731d7e843d2d04ec8f27e8cb51edd8ac"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@@ -5954,9 +6010,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-futures" name = "wasm-bindgen-futures"
version = "0.4.58" version = "0.4.61"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" checksum = "fe88540d1c934c4ec8e6db0afa536876c5441289d7f9f9123d4f065ac1250a6b"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"futures-util", "futures-util",
@@ -5968,9 +6024,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.108" version = "0.2.111"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" checksum = "19e638317c08b21663aed4d2b9a2091450548954695ff4efa75bff5fa546b3b1"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -5978,9 +6034,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.108" version = "0.2.111"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" checksum = "2c64760850114d03d5f65457e96fc988f11f01d38fbaa51b254e4ab5809102af"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2", "proc-macro2",
@@ -5991,9 +6047,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.108" version = "0.2.111"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" checksum = "60eecd4fe26177cfa3339eb00b4a36445889ba3ad37080c2429879718e20ca41"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -6142,9 +6198,9 @@ dependencies = [
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.85" version = "0.3.88"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" checksum = "9d6bb20ed2d9572df8584f6dc81d68a41a625cadc6f15999d649a70ce7e3597a"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
@@ -6654,15 +6710,6 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.61.2" version = "0.61.2"

View File

@@ -8,10 +8,17 @@ license = "MIT/Apache-2.0"
[dependencies] [dependencies]
bevy = { version = "0.18", features = ["debug"] } bevy = { version = "0.18", features = ["debug"] }
bevy_cef = { path = "../bevy_cef" } bevy_cef = { path = "../bevy_cef" }
bevy_mod_xr = { git = "https://git.avii.nl/Avii/bevy_oxr.git", version = "0.5.0" } # 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_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" bevy_pkv = "0.15.0"
# bevy_xr_utils.workspace = true # bevy_xr_utils.workspace = true
openxr = "0.21.1" openxr = "0.21.1"
rdev = { version = "0.5.3", features = ["unstable_grab"] } rdev = { version = "0.5.3", features = ["unstable_grab"] }
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
otd-ipc = { path = "../../otd-ipc-client-rs" }
crossbeam-channel = "0.5.15"
triple_buffer = "8.1.1"

View File

@@ -9,6 +9,7 @@ use openxr::Path;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
MyProcGenImage,
vrcontrollerplugin::{ vrcontrollerplugin::{
LeftController, LeftControllerActions, RightController, RightControllerActions, LeftController, LeftControllerActions, RightController, RightControllerActions,
}, },
@@ -158,18 +159,22 @@ fn spawn_kneeboard(
mut commands: Commands, mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>, mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<WebviewExtendStandardMaterial>>, mut materials: ResMut<Assets<WebviewExtendStandardMaterial>>,
image_handle: Res<MyProcGenImage>,
position: Res<KneeboardPosition>, position: Res<KneeboardPosition>,
) { ) {
commands.spawn(( commands.spawn((
WebviewSource::new("http://localhost:7878/"), // i want this to become dynamic.... from ui or something WebviewSource::new("http://localhost:7878/MOSRPRPETASPCLORWTSURDEP"),
WebviewSize(Vec2::new(210.0 * 3.5, 279.0 * 3.5)), // this can also be dynamic... kinda like rotating phone WebviewSize(Vec2::new(210.0 * 3.5, 279.0 * 3.5)),
Mesh3d(meshes.add(Cuboid::new(0.210, 0.279, 0.01))), Mesh3d(meshes.add(Cuboid::new(0.210, 0.279, 0.01))),
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial { MeshMaterial3d(materials.add(WebviewExtendStandardMaterial {
base: StandardMaterial { base: StandardMaterial {
unlit: true, unlit: true,
..default() ..default()
}, },
..Default::default() extension: WebviewMaterial {
surface: None,
overlay: Some(image_handle.0.clone()),
},
})), })),
Transform::from_translation(position.position).rotate(position.rotation), Transform::from_translation(position.position).rotate(position.rotation),
Kneeboard, Kneeboard,

View File

@@ -1,18 +1,24 @@
//! A simple 3D scene with light shining over a cube sitting on a plane. //! A simple 3D scene with light shining over a cube sitting on a plane.
mod kneeboardplugin; mod kneeboardplugin;
mod otdipcplugin;
mod vrcontrollerplugin; mod vrcontrollerplugin;
mod vrplugin; mod vrplugin;
use bevy_cef::prelude::{Browsers, MeshAabb, WebviewSize, WebviewSource}; use bevy_cef::prelude::WebviewExtendStandardMaterial;
use bevy_mod_openxr::openxr_session_running;
use bevy_mod_xr::camera::XrCamera;
use vrplugin::VrPlugin; use vrplugin::VrPlugin;
use bevy::prelude::*; use bevy::{
asset::RenderAssetUsages,
color::palettes::css,
prelude::*,
render::render_resource::{Extent3d, TextureDimension, TextureFormat},
};
use crate::otdipcplugin::OtdIpcPlugin;
use crate::{ use crate::{
kneeboardplugin::{Kneeboard, KneeboardPlugin, LookedAt}, kneeboardplugin::KneeboardPlugin,
otdipcplugin::{PenButtons, PenPosition, PenPressure},
vrcontrollerplugin::VrControllersPlugin, vrcontrollerplugin::VrControllersPlugin,
}; };
@@ -20,73 +26,477 @@ fn main() {
App::new() App::new()
.add_plugins((VrPlugin, MeshPickingPlugin)) .add_plugins((VrPlugin, MeshPickingPlugin))
.add_plugins(VrControllersPlugin) .add_plugins(VrControllersPlugin)
.add_plugins(OtdIpcPlugin)
.add_plugins(KneeboardPlugin) .add_plugins(KneeboardPlugin)
.insert_resource(ClearColor(Color::NONE)) .insert_resource(ClearColor(Color::NONE))
.add_systems(Update, head_pointer.run_if(openxr_session_running)) .insert_resource(LastPenPos(None))
.insert_resource(PenColor(css::BLACK.into()))
.insert_resource(PenSize(5.0))
.add_systems(Startup, setup)
.add_systems(Update, (color_changer, cursor, draw, plot).chain())
.run(); .run();
} }
#[allow(clippy::type_complexity)] #[derive(Resource)]
fn head_pointer( struct MyProcGenImage(Handle<Image>);
browsers: NonSend<Browsers>,
webviews: Query<(Entity, &WebviewSize), With<WebviewSource>>, #[derive(Resource)]
headset: Query<&GlobalTransform, With<XrCamera>>, struct PenSize(f32);
kneeboard: Query<(&GlobalTransform, Option<&LookedAt>), With<Kneeboard>>,
aabb: MeshAabb, #[derive(Resource)]
struct PenColor(Color);
const IMAGE_WIDTH: i32 = (210.0 * 3.5) as i32;
const IMAGE_HEIGHT: i32 = (279.0 * 3.5) as i32;
fn setup(mut commands: Commands, mut images: ResMut<Assets<Image>>) {
let image = Image::new_fill(
Extent3d {
width: IMAGE_WIDTH as u32,
height: IMAGE_HEIGHT as u32,
depth_or_array_layers: 1,
},
TextureDimension::D2,
// Initialize it with a beige color
&(Srgba::new(1.0, 1.0, 1.0, 0.0).to_u8_array()),
// Use the same encoding as the color we set
TextureFormat::Rgba8UnormSrgb,
RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,
);
let handle = images.add(image);
commands.insert_resource(MyProcGenImage(handle));
commands.insert_resource(CursorBuffer(
[0; IMAGE_WIDTH as usize * IMAGE_HEIGHT as usize * 4],
));
commands.insert_resource(DrawingBuffer(
[0; IMAGE_WIDTH as usize * IMAGE_HEIGHT as usize * 4],
));
}
#[derive(Resource)]
struct CursorBuffer([u8; IMAGE_WIDTH as usize * IMAGE_HEIGHT as usize * 4]);
impl Drawing for CursorBuffer {
fn set_color_at(&mut self, x: i32, y: i32, color: Color) {
if !(0..=IMAGE_WIDTH).contains(&x) || !(0..=IMAGE_HEIGHT).contains(&y) {
return;
}
let index = ((x + (IMAGE_WIDTH * y)) * 4) as usize;
let srgba = Srgba::from(color);
if index >= self.0.len() {
return;
}
self.0[index] = (srgba.red * u8::MAX as f32) as u8;
self.0[index + 1] = (srgba.green * u8::MAX as f32) as u8;
self.0[index + 2] = (srgba.blue * u8::MAX as f32) as u8;
self.0[index + 3] = (srgba.alpha * u8::MAX as f32) as u8;
}
fn clear(&mut self) {
self.0.fill(0);
}
}
#[derive(Resource)]
struct DrawingBuffer([u8; IMAGE_WIDTH as usize * IMAGE_HEIGHT as usize * 4]);
#[derive(Resource)]
pub struct LastPenPos(pub Option<(i32, i32)>);
impl Drawing for DrawingBuffer {
fn set_color_at(&mut self, x: i32, y: i32, color: Color) {
if !(0..=IMAGE_WIDTH).contains(&x) || !(0..=IMAGE_HEIGHT).contains(&y) {
return;
}
let index = ((x + (IMAGE_WIDTH * y)) * 4) as usize;
let srgba = Srgba::from(color);
if index >= self.0.len() {
return;
}
self.0[index] = (srgba.red * u8::MAX as f32) as u8;
self.0[index + 1] = (srgba.green * u8::MAX as f32) as u8;
self.0[index + 2] = (srgba.blue * u8::MAX as f32) as u8;
self.0[index + 3] = (srgba.alpha * u8::MAX as f32) as u8;
}
fn clear(&mut self) {
self.0.fill(0);
}
}
fn color_changer(mut pen_color: ResMut<PenColor>, mut pen_buttons: MessageReader<PenButtons>) {
let Some(buttons) = pen_buttons.read().next() else {
return;
};
pen_color.0 = css::BLACK.into();
if (buttons.state & 2) == 2 {
let mut t = LinearRgba::from(pen_color.0);
t.alpha = 0.0;
pen_color.0 = t.into();
}
if (buttons.state & 4) == 4 {
let mut t = LinearRgba::from(css::DARK_BLUE);
t.alpha = 0.5;
pen_color.0 = t.into();
}
}
fn cursor(
pen_color: Res<PenColor>,
mut buffer: ResMut<CursorBuffer>,
mut pen_size: ResMut<PenSize>,
mut pen_buttons: MessageReader<PenButtons>,
mut pen_position: MessageReader<PenPosition>,
mut pen_pressure: MessageReader<PenPressure>,
) { ) {
let Ok((webview, size)) = webviews.single() else { let mut size: f32 = 1.0;
let mut offset = 20.0;
let c = pen_color.0;
if let Some(buttons) = pen_buttons.read().next()
&& (buttons.state & 2 == 2)
{
offset *= 4.;
};
for penpres in pen_pressure.read() {
// this needs log scaling
let _in = penpres.pressure;
let _out = penpres.pressure.powi(4);
size *= 1. + (_out * offset);
}
pen_size.0 = size.clamp(1.0, 200.0);
let s = pen_size.0 as i32;
let b = css::BLACK.into();
let cs = 10;
let cd = 3;
for penpos in pen_position.read() {
let x = (penpos.position.x * (IMAGE_WIDTH as f32)) as i32;
let y = (penpos.position.y * (IMAGE_HEIGHT as f32)) as i32;
buffer.clear();
draw_line(&mut buffer, x - (cs + cd) - s, y, x - (cd) - s, y, 4.0, b);
draw_line(&mut buffer, x - (cs + cd) - s, y, x - (cd) - s, y, 1.0, c);
draw_line(&mut buffer, x + (cd) + s, y, x + (cs + cd) + s, y, 4.0, b);
draw_line(&mut buffer, x + (cd) + s, y, x + (cs + cd) + s, y, 1.0, c);
draw_line(&mut buffer, x, y - (cs + cd) - s, x, y - (cd) - s, 4.0, b);
draw_line(&mut buffer, x, y - (cs + cd) - s, x, y - (cd) - s, 1.0, c);
draw_line(&mut buffer, x, y + (cs + cd) + s, x, y + (cd) + s, 4.0, b);
draw_line(&mut buffer, x, y + (cs + cd) + s, x, y + (cd) + s, 1.0, c);
draw_filled_circle(&mut buffer, x, y, (pen_size.0 / 2.) as i32, c)
}
}
fn draw(
pen_size: Res<PenSize>,
pen_color: Res<PenColor>,
mut lastloc: ResMut<LastPenPos>,
mut buffer: ResMut<DrawingBuffer>,
mut pen_buttons: MessageReader<PenButtons>,
mut pen_position: MessageReader<PenPosition>,
) {
let Some(buttons) = pen_buttons.read().next() else {
return; return;
}; };
let tex_size = size.0; for penpos in pen_position.read() {
let x = (penpos.position.x * (IMAGE_WIDTH as f32)) as i32;
let y = (penpos.position.y * (IMAGE_HEIGHT as f32)) as i32;
if let Some(gt) = headset.into_iter().next() { let Some(ll) = lastloc.0 else {
let Ok((plane_tf, looked_at)) = kneeboard.single() else { lastloc.0 = Some((x, y));
return; return;
}; };
if looked_at.is_some() { if buttons.state & 1 == 1 {
// this is inverted for some reason wtf draw_line(&mut buffer, ll.0, ll.1, x, y, pen_size.0, pen_color.0);
}
lastloc.0 = Some((x, y));
}
}
trait Drawing {
fn set_color_at(&mut self, px: i32, py: i32, color: Color);
fn clear(&mut self);
}
fn draw_line<T: Drawing + Resource>(
buffer: &mut ResMut<T>,
x1: i32,
y1: i32,
x2: i32,
y2: i32,
thickness: f32,
color: Color,
) {
let (mut x0, mut y0) = (x1, y1);
let (x1, y1) = (x2, y2);
let dx = (x1 - x0).abs();
let dy = -(y1 - y0).abs();
let sx = if x0 < x1 { 1 } else { -1 };
let sy = if y0 < y1 { 1 } else { -1 };
let mut err = dx + dy;
let half_t = (thickness / 2.0).ceil() as i32;
loop {
draw_thick_point(buffer, x0, y0, half_t, color);
if x0 == x1 && y0 == y1 {
break;
}
let e2 = 2 * err;
if e2 >= dy {
err += dy;
x0 += sx;
}
if e2 <= dx {
err += dx;
y0 += sy;
}
}
}
// Draw a small square brush (fast, branch-free inner loop)
fn draw_thick_point<T: Drawing + Resource>(
buffer: &mut ResMut<T>,
cx: i32,
cy: i32,
radius: i32,
color: Color,
) {
draw_filled_circle(buffer, cx, cy, radius, color);
}
fn draw_filled_circle<T: Drawing + Resource>(
buffer: &mut ResMut<T>,
cx: i32,
cy: i32,
radius: i32,
color: Color,
) {
let r2 = radius * radius;
for dy in -radius..=radius {
let y = cy + dy;
if y < 0 {
continue;
}
let dx_limit = ((r2 - dy * dy) as f32).sqrt() as i32;
let start_x = cx - dx_limit;
let end_x = cx + dx_limit;
for x in start_x..=end_x {
if x >= 0 {
buffer.set_color_at(x, y, color);
}
}
}
}
fn draw_circle<T: Drawing + Resource>(
buffer: &mut ResMut<T>,
cx: i32,
cy: i32,
radius: i32,
thickness: f32,
color: Color,
) {
if radius <= 0 {
return; return;
} }
let (min, max) = aabb.calculate_local(webview); let half_t = (thickness / 2.0).max(0.5);
let plane_size = Vec2::new(max.x - min.x, max.y - min.y); let outer_r = radius as f32 + half_t;
let inner_r = (radius as f32 - half_t).max(0.0);
let ray = Ray3d::new(gt.translation(), gt.forward()); let outer_r2 = (outer_r * outer_r) as i32;
let inner_r2 = (inner_r * inner_r) as i32;
let n = plane_tf.forward().as_vec3(); let max_r = outer_r.ceil() as i32;
let Some(t) = ray.intersect_plane(
plane_tf.translation(), for dy in -max_r..=max_r {
InfinitePlane3d::new(plane_tf.forward()), let y = cy + dy;
) else { if y < 0 {
return; continue;
}; }
let hit_world = ray.origin + ray.direction * t;
let local_hit = plane_tf.affine().inverse().transform_point(hit_world); let dy2 = dy * dy;
let local_normal = plane_tf.affine().inverse().transform_vector3(n).normalize(); if dy2 > outer_r2 {
let abs_normal = local_normal.abs(); continue;
let (u_coord, v_coord) = if abs_normal.z > abs_normal.x && abs_normal.z > abs_normal.y { }
(local_hit.x, local_hit.y)
} else if abs_normal.y > abs_normal.x { let outer_dx = ((outer_r2 - dy2) as f32).sqrt() as i32;
(local_hit.x, local_hit.z) let inner_dx = if dy2 < inner_r2 {
((inner_r2 - dy2) as f32).sqrt() as i32
} else { } else {
(local_hit.y, local_hit.z) -1
}; };
let w = plane_size.x; let left_outer = cx - outer_dx;
let h = plane_size.y; let right_outer = cx + outer_dx;
let u = (u_coord + w * 0.5) / w;
let v = (v_coord + h * 0.5) / h;
if !(0.0..=1.0).contains(&u) || !(0.0..=1.0).contains(&v) {
// outside plane bounds
return;
}
let px = (1.0 - u) * tex_size.x;
let py = (1.0 - v) * tex_size.y;
let pos = Vec2::new(px, py); if inner_dx >= 0 {
let left_inner = cx - inner_dx;
let right_inner = cx + inner_dx;
browsers.send_mouse_move(&webview, &[], pos, false); // left segment
for x in left_outer..left_inner {
if x >= 0 {
buffer.set_color_at(x, y, color);
} }
} }
// right segment
for x in (right_inner + 1)..=right_outer {
if x >= 0 {
buffer.set_color_at(x, y, color);
}
}
} else {
// fully filled span (very thin or small radius)
for x in left_outer..=right_outer {
if x >= 0 {
buffer.set_color_at(x, y, color);
}
}
}
}
}
fn plot(
cursor: Res<CursorBuffer>,
buffer: Res<DrawingBuffer>,
mut images: ResMut<Assets<Image>>,
mut materials: ResMut<Assets<WebviewExtendStandardMaterial>>,
webviews: Query<&MeshMaterial3d<WebviewExtendStandardMaterial>>,
my_handle: Res<MyProcGenImage>,
) {
let image = images.get_mut(&my_handle.0).expect("Image not found");
image.clear(&[0, 0, 0, 0]);
for (i, c) in buffer.0.chunks(4).enumerate() {
let x = i as i32 % IMAGE_WIDTH;
let y = i as i32 / IMAGE_WIDTH;
let red: u8 = c[0];
let green = c[1];
let blue = c[2];
let alpha = c[3];
if alpha > 0 {
let color = Color::srgba_u8(red, green, blue, alpha);
image.set_color_at(x as u32, y as u32, color).unwrap();
}
}
for (i, c) in cursor.0.chunks(4).enumerate() {
let x = i as i32 % IMAGE_WIDTH;
let y = i as i32 / IMAGE_WIDTH;
let red: u8 = c[0];
let green = c[1];
let blue = c[2];
let alpha = c[3];
if alpha > 0 {
let color = Color::srgba_u8(red, green, blue, alpha);
image.set_color_at(x as u32, y as u32, color).unwrap();
}
}
// Poke redraw
for handle in webviews {
if let Some(material) = materials.get_mut(handle.id()) {
material.extension.overlay = Some(my_handle.0.clone());
}
}
}
// #[allow(clippy::type_complexity)]
// fn head_pointer(
// browsers: NonSend<Browsers>,
// webviews: Query<(Entity, &WebviewSize), With<WebviewSource>>,
// headset: Query<&GlobalTransform, With<XrCamera>>,
// kneeboard: Query<(&GlobalTransform, Option<&LookedAt>), With<Kneeboard>>,
// aabb: MeshAabb,
// ) {
// let Ok((webview, size)) = webviews.single() else {
// return;
// };
// let tex_size = size.0;
// if let Some(gt) = headset.into_iter().next() {
// let Ok((plane_tf, looked_at)) = kneeboard.single() else {
// return;
// };
// if looked_at.is_some() {
// // this is inverted for some reason wtf
// return;
// }
// let (min, max) = aabb.calculate_local(webview);
// let plane_size = Vec2::new(max.x - min.x, max.y - min.y);
// let ray = Ray3d::new(gt.translation(), gt.forward());
// let n = plane_tf.forward().as_vec3();
// let Some(t) = ray.intersect_plane(
// plane_tf.translation(),
// InfinitePlane3d::new(plane_tf.forward()),
// ) else {
// return;
// };
// let hit_world = ray.origin + ray.direction * t;
// let local_hit = plane_tf.affine().inverse().transform_point(hit_world);
// let local_normal = plane_tf.affine().inverse().transform_vector3(n).normalize();
// let abs_normal = local_normal.abs();
// let (u_coord, v_coord) = if abs_normal.z > abs_normal.x && abs_normal.z > abs_normal.y {
// (local_hit.x, local_hit.y)
// } else if abs_normal.y > abs_normal.x {
// (local_hit.x, local_hit.z)
// } else {
// (local_hit.y, local_hit.z)
// };
// let w = plane_size.x;
// let h = plane_size.y;
// let u = (u_coord + w * 0.5) / w;
// let v = (v_coord + h * 0.5) / h;
// if !(0.0..=1.0).contains(&u) || !(0.0..=1.0).contains(&v) {
// // outside plane bounds
// return;
// }
// let px = (1.0 - u) * tex_size.x;
// let py = (1.0 - v) * tex_size.y;
// let pos = Vec2::new(px, py);
// browsers.send_mouse_move(&webview, &[], pos, false);
// }
// }

100
src/otdipcplugin.rs Normal file
View File

@@ -0,0 +1,100 @@
use bevy::{
app::{App, Plugin, PreUpdate},
ecs::{message::MessageWriter, resource::Resource, system::ResMut},
math::Vec2,
};
use otd_ipc::{Message, OtdIpc};
use triple_buffer::{Output, triple_buffer};
#[derive(Resource)]
struct OtdChannel(Output<Option<Message>>);
#[derive(Resource)]
struct TabletSize(Option<Vec2>);
#[derive(Resource)]
struct MaxPenPressure(Option<u32>);
#[derive(bevy::ecs::message::Message)]
pub struct PenPosition {
pub position: Vec2,
}
#[derive(bevy::ecs::message::Message)]
pub struct PenButtons {
pub state: u32,
}
#[derive(bevy::ecs::message::Message)]
pub struct PenPressure {
pub pressure: f32,
}
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::<PenPosition>();
app.add_message::<PenPressure>();
app.add_message::<PenButtons>();
app.insert_resource(OtdChannel(rx));
app.insert_resource(MaxPenPressure(None));
app.insert_resource(TabletSize(None));
app.add_systems(PreUpdate, reader);
}
}
#[allow(clippy::too_many_arguments)]
fn reader(
mut channel: ResMut<OtdChannel>,
mut size: ResMut<TabletSize>,
mut pressure: ResMut<MaxPenPressure>,
mut pressure_writer: MessageWriter<PenPressure>,
mut position_writer: MessageWriter<PenPosition>,
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: state.pen_buttons(),
});
let p = state.pressure() as f32 / pressure as f32;
pressure_writer.write(PenPressure { pressure: p });
// Make rotation configurable with some resource enum or something
let y = (size.x - state.x()) / size.x;
let x = state.y() / size.y;
let loc = Vec2::new(x, y);
position_writer.write(PenPosition { position: loc });
}
_ => {}
}
}

View File

@@ -46,11 +46,17 @@ impl Plugin for VrControllersPlugin {
.run_if(openxr_session_running), .run_if(openxr_session_running),
); );
app.add_systems(OxrSendActionBindings, suggest_action_bindings_left);
app.add_systems(OxrSendActionBindings, suggest_action_bindings_right);
app.add_systems(Startup, create_actions_left.run_if(session_available)); 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(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),
);
} }
} }

View File

@@ -1,5 +1,6 @@
use bevy::{ use bevy::{
prelude::*, prelude::*,
render::render_resource::TextureFormat,
window::{PresentMode, WindowResolution}, window::{PresentMode, WindowResolution},
}; };
use bevy_mod_openxr::prelude::*; use bevy_mod_openxr::prelude::*;
@@ -41,14 +42,9 @@ impl Plugin for VrPlugin {
); );
app.insert_resource(OxrSessionConfig { app.insert_resource(OxrSessionConfig {
blend_mode_preference: { blend_mode_preference: { vec![EnvironmentBlendMode::ALPHA_BLEND] },
vec![ formats: Some(vec![TextureFormat::Rgba8UnormSrgb]),
EnvironmentBlendMode::ALPHA_BLEND, ..Default::default()
EnvironmentBlendMode::ADDITIVE,
EnvironmentBlendMode::OPAQUE,
]
},
..OxrSessionConfig::default()
}); });
app.add_systems(XrSessionCreated, create_view_space); app.add_systems(XrSessionCreated, create_view_space);