diff --git a/Cargo.lock b/Cargo.lock index 651a34f..ed6302b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -382,6 +382,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "az" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" + [[package]] name = "base64" version = "0.22.1" @@ -2664,6 +2670,29 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "embedded-graphics" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e8da660bb0c829b34a56a965490597f82a55e767b91f9543be80ce8ccb416fe" +dependencies = [ + "az", + "byteorder", + "embedded-graphics-core", + "float-cmp", + "micromath", +] + +[[package]] +name = "embedded-graphics-core" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95743bef3ff70fcba3930246c4e6872882bbea0dcc6da2ca860112e0cd4bd09f" +dependencies = [ + "az", + "byteorder", +] + [[package]] name = "encase" version = "0.12.0" @@ -2839,6 +2868,15 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + [[package]] name = "fnv" version = "1.0.7" @@ -3199,7 +3237,7 @@ dependencies = [ "log", "presser", "thiserror 1.0.69", - "windows 0.54.0", + "windows 0.58.0", ] [[package]] @@ -3704,6 +3742,8 @@ dependencies = [ "bevy_mod_xr", "bevy_pkv", "crossbeam-channel", + "embedded-graphics", + "embedded-graphics-core", "openxr", "otd-ipc", "rdev", @@ -3904,6 +3944,12 @@ dependencies = [ "paste", ] +[[package]] +name = "micromath" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c8dda44ff03a2f238717214da50f65d5a53b45cd213a7370424ffdb6fae815" + [[package]] name = "minimal-lexical" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 9d1f015..9d556c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,3 +22,5 @@ serde = { version = "1.0.228", features = ["derive"] } otd-ipc = { path = "../../otd-ipc-client-rs" } crossbeam-channel = "0.5.15" triple_buffer = "8.1.1" +embedded-graphics = "0.8.2" +embedded-graphics-core = "0.4.1" diff --git a/src/drawingplugin.rs b/src/drawingplugin.rs new file mode 100644 index 0000000..2c210c1 --- /dev/null +++ b/src/drawingplugin.rs @@ -0,0 +1,233 @@ +mod drawtarget; +mod rgba; +mod uiplugin; + +use bevy_cef::prelude::WebviewExtendStandardMaterial; + +use bevy::{ + asset::RenderAssetUsages, + color::palettes::css, + prelude::*, + render::render_resource::{Extent3d, TextureDimension, TextureFormat}, +}; +use embedded_graphics::prelude::DrawTarget; + +use crate::{ + drawingplugin::drawtarget::{DrawableLayer, Visible}, + otdipcplugin::OtdIpcPlugin, +}; +use crate::{ + drawingplugin::uiplugin::UiPlugin, + otdipcplugin::{PenButtons, PenPosition, PenPressure}, +}; + +const IMAGE_WIDTH: usize = (210.0 * 3.5) as usize; +const IMAGE_HEIGHT: usize = (279.0 * 3.5) as usize; + +#[derive(Resource)] +pub struct MyProcGenImage(pub(crate) Handle); + +#[derive(Component)] +struct CursorBuffer; + +#[derive(Component)] +struct DrawingBuffer; + +#[derive(Resource)] +struct PenSize(f32); + +#[derive(Resource)] +struct PenColor(Color); + +#[derive(Resource)] +pub struct LastPenPos(pub Option<(i32, i32)>); + +pub struct DrawingPlugin; + +impl Plugin for DrawingPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(OtdIpcPlugin); + app.add_plugins(UiPlugin); + + app.insert_resource(ClearColor(Color::NONE)) + .insert_resource(LastPenPos(None)) + .insert_resource(PenColor(css::BLACK.into())) + .insert_resource(PenSize(5.0)); + + app.add_systems(Startup, setup) + .add_systems(Update, (cursor, draw, plot)); + } +} + +fn setup(mut commands: Commands, mut images: ResMut>) { + let image = Image::new_fill( + Extent3d { + width: IMAGE_WIDTH as u32, + height: IMAGE_HEIGHT as u32, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + &(Srgba::new(1.0, 1.0, 1.0, 0.0).to_u8_array()), + TextureFormat::Rgba8UnormSrgb, + RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD, + ); + + let handle = images.add(image); + commands.insert_resource(MyProcGenImage(handle)); + commands.spawn(( + CursorBuffer, + DrawableLayer::new(IMAGE_WIDTH, IMAGE_HEIGHT, drawtarget::Layer::Drawing), + Visible, + )); + commands.spawn(( + DrawingBuffer, + DrawableLayer::new(IMAGE_WIDTH, IMAGE_HEIGHT, drawtarget::Layer::Cursor), + Visible, + )); +} + +// fn color_changer(mut pen_color: ResMut, mut pen_buttons: MessageReader) { +// let Some(buttons) = pen_buttons.read().next() else { +// return; +// }; + +// pen_color.0 = css::BLACK.into(); + +// if buttons.a() { +// let mut t = LinearRgba::from(pen_color.0); +// t.alpha = 0.0; +// pen_color.0 = t.into(); +// } +// } + +fn cursor( + pen_color: Res, + mut buffer: Query<&mut DrawableLayer, With>, + mut pen_size: ResMut, + mut pen_buttons: MessageReader, + mut pen_position: MessageReader, + mut pen_pressure: MessageReader, +) { + let Ok(mut buffer) = buffer.single_mut() else { + return; + }; + + 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.a() + { + offset *= 4.; + }; + + for penpres in pen_pressure.read() { + 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.x * (buffer.width as f32)) as i32; + let y = (penpos.y * (buffer.height as f32)) as i32; + + buffer.clear(Srgba::new(0.0, 0.0, 0.0, 0.0).into()).ok(); + buffer.draw_stroke(x - (cs + cd) - s, y, x - (cd) - s, y, 4.0, b); + buffer.draw_stroke(x - (cs + cd) - s, y, x - (cd) - s, y, 1.0, c); + buffer.draw_stroke(x + (cd) + s, y, x + (cs + cd) + s, y, 4.0, b); + buffer.draw_stroke(x + (cd) + s, y, x + (cs + cd) + s, y, 1.0, c); + + buffer.draw_stroke(x, y - (cs + cd) - s, x, y - (cd) - s, 4.0, b); + buffer.draw_stroke(x, y - (cs + cd) - s, x, y - (cd) - s, 1.0, c); + buffer.draw_stroke(x, y + (cs + cd) + s, x, y + (cd) + s, 4.0, b); + buffer.draw_stroke(x, y + (cs + cd) + s, x, y + (cd) + s, 1.0, c); + } +} + +fn draw( + pen_size: Res, + pen_color: Res, + ui: Query<&Visible, With>, + mut lastloc: ResMut, + mut buffer: Query<&mut DrawableLayer, With>, + mut pen_buttons: MessageReader, + mut pen_position: MessageReader, +) { + if ui.single().is_ok() { + lastloc.0 = None; + return; // if the ui layer is visible, dont draw + } + + let Ok(mut buffer) = buffer.single_mut() else { + return; + }; + + let Some(buttons) = pen_buttons.read().next() else { + return; + }; + + for penpos in pen_position.read() { + let x = (penpos.x * (buffer.width as f32)) as i32; + let y = (penpos.y * (buffer.height as f32)) as i32; + + let Some(ll) = lastloc.0 else { + lastloc.0 = Some((x, y)); + return; + }; + + if buttons.tip() { + buffer.draw_stroke(ll.0, ll.1, x, y, pen_size.0, pen_color.0); + } + + lastloc.0 = Some((x, y)); + } +} + +fn plot( + buffers: Query<&DrawableLayer, With>, + mut images: ResMut>, + mut materials: ResMut>, + webviews: Query<&MeshMaterial3d>, + my_handle: Res, +) { + let image = images.get_mut(&my_handle.0).expect("Image not found"); + image.clear(&[0, 0, 0, 0]); + + let sorted = buffers + .iter() + .sort_by::<&DrawableLayer>(|a, b| b.order.cmp(&a.order)); + + for layer in sorted { + for (i, c) in layer.buffer.chunks(4).enumerate() { + let x = i % layer.width; + let y = i / layer.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()); + } + } +} diff --git a/src/drawingplugin/drawtarget.rs b/src/drawingplugin/drawtarget.rs new file mode 100644 index 0000000..426e01d --- /dev/null +++ b/src/drawingplugin/drawtarget.rs @@ -0,0 +1,342 @@ +use bevy::{color::Color, ecs::component::Component}; +use embedded_graphics::{pixelcolor::Rgb888, prelude::*, primitives::*}; + +use crate::drawingplugin::rgba::Rgba; + +#[allow(unused)] +#[derive(PartialEq, Eq, PartialOrd, Ord)] +pub enum Layer { + Drawing, + Ui, + Cursor, +} + +#[derive(Component)] +pub struct Visible; + +#[derive(Component)] +pub struct DrawableLayer { + pub order: Layer, + pub width: usize, + pub height: usize, + pub buffer: Vec, +} + +impl DrawableLayer { + pub fn new(width: usize, height: usize, order: Layer) -> Self { + let buffer = vec![0; width * height * 4]; + Self { + order, + width, + height, + buffer, + } + } +} + +#[allow(unused)] +impl DrawableLayer { + pub fn draw_stroke( + &mut self, + 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 { + self.draw_filled_circle(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; + } + } + } + + pub fn set_color_at(&mut self, x: i32, y: i32, color: Color) { + Pixel(Point::new(x, y), super::rgba::Rgba::from(color.to_srgba())) + .draw(self) + .ok(); + } + + pub fn draw_line(&mut self, x1: i32, y1: i32, x2: i32, y2: i32, thickness: f32, color: Color) { + Line::new(Point::new(x1, y1), Point::new(x2, y2)) + .into_styled(PrimitiveStyle::with_stroke( + Rgba::from(color.to_srgba()), + thickness as u32, + )) + .draw(self) + .ok(); + } + + pub fn draw_circle(&mut self, cx: i32, cy: i32, radius: i32, thickness: f32, color: Color) { + Circle::new(Point::new(cx - radius, cy - radius), (radius * 2) as u32) + .into_styled(PrimitiveStyle::with_stroke( + Rgba::from(color.to_srgba()), + thickness as u32, + )) + .draw(self) + .ok(); + } + + pub fn draw_filled_circle(&mut self, cx: i32, cy: i32, radius: i32, color: Color) { + Circle::new(Point::new(cx - radius, cy - radius), (radius * 2) as u32) + .into_styled(PrimitiveStyle::with_fill(Rgba::from(color.to_srgba()))) + .draw(self) + .ok(); + } + + pub fn draw_rectangle(&mut self, x: i32, y: i32, w: u32, h: u32, thickness: f32, color: Color) { + Rectangle::new(Point::new(x, y), Size::new(w, h)) + .into_styled(PrimitiveStyle::with_stroke( + Rgba::from(color.to_srgba()), + thickness as u32, + )) + .draw(self) + .ok(); + } + + pub fn draw_filled_rectangle(&mut self, x: i32, y: i32, w: u32, h: u32, color: Color) { + Rectangle::new(Point::new(x, y), Size::new(w, h)) + .into_styled(PrimitiveStyle::with_fill(Rgba::from(color.to_srgba()))) + .draw(self) + .ok(); + } +} + +impl Dimensions for DrawableLayer { + fn bounding_box(&self) -> embedded_graphics::primitives::Rectangle { + Rectangle::new( + Point::new(0, 0), + Size::new(self.width as u32, self.height as u32), + ) + } +} + +impl DrawTarget for DrawableLayer { + type Color = Rgba; + type Error = Box; + + fn draw_iter(&mut self, pixels: I) -> Result<(), Self::Error> + where + I: IntoIterator>, + { + for p in pixels { + let x = p.0.x as usize; + let y = p.0.y as usize; + + if !(0..=self.width).contains(&x) || !(0..=self.height).contains(&y) { + continue; + } + + let index = (x + (self.width * y)) * 4; + + if index >= self.buffer.len() { + continue; + } + + self.buffer[index] = p.1.r(); + self.buffer[index + 1] = p.1.g(); + self.buffer[index + 2] = p.1.b(); + self.buffer[index + 3] = p.1.a(); + } + + Ok(()) + } +} + +// pub fn set_color_at(&mut self, x: i32, y: i32, color: Color) { +// let x: usize = x as usize; +// let y: usize = y as usize; + +// if !(0..=self.width).contains(&x) || !(0..=self.height).contains(&y) { +// return; +// } + +// let index = (x + (self.width * y)) * 4; +// let srgba = Srgba::from(color); + +// if index >= self.buffer.len() { +// return; +// } + +// self.buffer[index] = (srgba.red * u8::MAX as f32) as u8; +// self.buffer[index + 1] = (srgba.green * u8::MAX as f32) as u8; +// self.buffer[index + 2] = (srgba.blue * u8::MAX as f32) as u8; +// self.buffer[index + 3] = (srgba.alpha * u8::MAX as f32) as u8; +// } + +// pub fn clear(&mut self) { +// self.buffer.fill(0); +// } + +// pub fn draw_line(&mut self, 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 { +// self.draw_thick_point(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) +// pub fn draw_thick_point(&mut self, cx: i32, cy: i32, radius: i32, color: Color) { +// self.draw_filled_circle(cx, cy, radius, color); +// } + +// pub fn draw_filled_circle(&mut self, 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 { +// self.set_color_at(x, y, color); +// } +// } +// } +// } + +// pub fn draw_circle(&mut self, cx: i32, cy: i32, radius: i32, thickness: f32, color: Color) { +// if radius <= 0 { +// return; +// } + +// let half_t = (thickness / 2.0).max(0.5); +// let outer_r = radius as f32 + half_t; +// let inner_r = (radius as f32 - half_t).max(0.0); + +// let outer_r2 = (outer_r * outer_r) as i32; +// let inner_r2 = (inner_r * inner_r) as i32; + +// let max_r = outer_r.ceil() as i32; + +// for dy in -max_r..=max_r { +// let y = cy + dy; +// if y < 0 { +// continue; +// } + +// let dy2 = dy * dy; +// if dy2 > outer_r2 { +// continue; +// } + +// let outer_dx = ((outer_r2 - dy2) as f32).sqrt() as i32; +// let inner_dx = if dy2 < inner_r2 { +// ((inner_r2 - dy2) as f32).sqrt() as i32 +// } else { +// -1 +// }; + +// let left_outer = cx - outer_dx; +// let right_outer = cx + outer_dx; + +// if inner_dx >= 0 { +// let left_inner = cx - inner_dx; +// let right_inner = cx + inner_dx; + +// // left segment +// for x in left_outer..left_inner { +// if x >= 0 { +// self.set_color_at(x, y, color); +// } +// } + +// // right segment +// for x in (right_inner + 1)..=right_outer { +// if x >= 0 { +// self.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 { +// self.set_color_at(x, y, color); +// } +// } +// } +// } +// } + +// pub fn draw_rectangle(&mut self, x: i32, y: i32, w: u32, h: u32, thickness: f32, color: Color) { +// let w = w as i32; +// let h = h as i32; + +// self.draw_line(x, y, x + w, y, thickness, color); +// self.draw_line(x + w, y, x + w, y + h, thickness, color); +// self.draw_line(x, y + h, x + w, y + h, thickness, color); +// self.draw_line(x, y, x, y + h, thickness, color); +// } + +// pub fn draw_filled_rectangle( +// &mut self, +// x: i32, +// y: i32, +// w: u32, +// h: u32, +// thickness: f32, +// color: Color, +// ) { +// let w = w as i32; +// let h = h as i32; + +// for i in 0..h { +// self.draw_line(x, y + i, x + w, y + i, thickness, color); +// } +// } +// } diff --git a/src/drawingplugin/rgba.rs b/src/drawingplugin/rgba.rs new file mode 100644 index 0000000..07b6307 --- /dev/null +++ b/src/drawingplugin/rgba.rs @@ -0,0 +1,94 @@ +use bevy::color::Srgba; +use embedded_graphics_core::pixelcolor::*; + +/// Simple RGBA color wrapper. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Rgba(C, u8); + +#[allow(unused)] +#[inline(always)] +fn mul_blend_u8(delta: u32, a: u32) -> u32 { + // Exact (delta * a) / 255 using the div255 trick (no slow integer division). + // Valid for 0..=65535 inputs; see Hacker's Delight 10-16. + let t = delta * a + 128; + (t + (t >> 8)) >> 8 +} + +#[allow(unused)] +impl Rgba { + /// Create a new RGBA color. + pub const fn new(color: C, alpha: u8) -> Self { + Self(color, alpha) + } + + /// Get the color component. + pub const fn rgb(&self) -> C { + self.0 + } + + pub fn r(&self) -> u8 { + self.0.r() + } + + pub fn g(&self) -> u8 { + self.0.g() + } + + pub fn b(&self) -> u8 { + self.0.b() + } + + /// Get the alpha component (0..=255). + pub const fn a(&self) -> u8 { + self.1 + } +} + +impl PixelColor for Rgba { + type Raw = C::Raw; +} + +#[allow(unused)] +pub trait Blend { + fn blend(&self, bg: T) -> T; +} + +impl Blend for Rgba { + #[inline(always)] + fn blend(&self, bg: Rgb888) -> Rgb888 { + let a = self.a() as u32; + if a == 0 { + return bg; + } + if a == 255 { + return self.rgb(); + } + + let fr = self.rgb().r() as u32; + let fg = self.rgb().g() as u32; + let fb = self.rgb().b() as u32; + + let br = bg.r() as u32; + let bgc = bg.g() as u32; + let bb = bg.b() as u32; + + let r = (br + mul_blend_u8(fr.wrapping_sub(br), a)) as u8; + let g = (bgc + mul_blend_u8(fg.wrapping_sub(bgc), a)) as u8; + let b = (bb + mul_blend_u8(fb.wrapping_sub(bb), a)) as u8; + + Rgb888::new(r, g, b) + } +} + +impl From for Rgba { + fn from(value: Srgba) -> Self { + Self( + Rgb888::new( + (value.red * 255.) as u8, + (value.green * 255.) as u8, + (value.blue * 255.) as u8, + ), + (value.alpha * 255.) as u8, + ) + } +} diff --git a/src/drawingplugin/uiplugin.rs b/src/drawingplugin/uiplugin.rs new file mode 100644 index 0000000..e3125ff --- /dev/null +++ b/src/drawingplugin/uiplugin.rs @@ -0,0 +1,271 @@ +use std::f32; + +use bevy::{color::palettes::css, prelude::*}; +use embedded_graphics::prelude::DrawTarget; + +use crate::{ + drawingplugin::{DrawingBuffer, IMAGE_HEIGHT, IMAGE_WIDTH, PenColor, drawtarget::Visible}, + otdipcplugin::{PenButtons, PenPosition}, +}; + +use super::drawtarget::{DrawableLayer, Layer}; + +#[derive(Component)] +pub struct UiBuffer; + +#[derive(Component, Deref)] +pub struct UiPosition(Vec2); + +pub struct UiPlugin; + +impl Plugin for UiPlugin { + fn build(&self, app: &mut App) { + app.add_systems(Startup, setup).add_systems( + Update, + (show_hide, ui_on_show, ui_on_hide, color_changer, clear, ui), + ); + } +} + +fn setup(mut commands: Commands) { + commands.spawn(( + UiBuffer, + DrawableLayer::new(IMAGE_WIDTH, IMAGE_HEIGHT, Layer::Ui), + )); +} + +fn show_hide( + mut commands: Commands, + mut pen_buttons: MessageReader, + ent: Query>, +) { + let Ok(ent) = ent.single() else { + return; + }; + + let Some(buttons) = pen_buttons.read().next() else { + return; + }; + + if buttons.b() { + commands.entity(ent).insert(Visible); + } else { + commands.entity(ent).remove::(); + } +} + +fn ui_on_show( + mut commands: Commands, + ent: Query>, + mut pen_position: MessageReader, +) { + let Ok(ent) = ent.single() else { + return; + }; + + let Some(pos) = pen_position.read().next() else { + return; + }; + + commands.entity(ent).insert(UiPosition(**pos)); +} + +fn ui_on_hide(mut commands: Commands, mut removed: RemovedComponents) { + let Some(ent) = removed.read().next() else { + return; + }; + + // We should probably ensure that `ent` has a `UiBuffer` on it, + // atm, we're the only one using Visible, so fine for now. + + commands + .entity(ent) + .insert(UiPosition(Vec2::new(-1000., -1000.))); +} + +fn ui( + // pen_color: Res, + mut buffer: Query<(&mut DrawableLayer, &UiPosition), With>, + // mut pen_buttons: MessageReader, + // mut pen_position: MessageReader, +) { + let Ok((mut buffer, base_pos)) = buffer.single_mut() else { + return; + }; + let w = buffer.width as f32; + let h = buffer.height as f32; + let x = (base_pos.x * w) as i32; + let y = (base_pos.y * h) as i32; + buffer.clear(Srgba::new(0.0, 0.0, 0.0, 0.0).into()).ok(); + + color_wheel(&mut buffer, x, y, 100); + buffer.draw_circle(x, y, 100, 5.0, css::BLACK.into()); + buffer.draw_circle(x, y, 125, 3.0, css::BLACK.into()); +} + +#[allow(clippy::type_complexity)] +fn clear( + buffer: Query< + (&DrawableLayer, &UiPosition), + (With, With, Without), + >, + mut draw_buffer: Query<&mut DrawableLayer, (With, Without)>, + mut pen_position: MessageReader, + mut pen_buttons: MessageReader, +) { + let Ok((buffer, base_pos)) = buffer.single() else { + return; + }; + + let Ok(mut draw_buffer) = draw_buffer.single_mut() else { + return; + }; + + let Some(pos) = pen_position.read().next() else { + return; + }; + + let Some(buttons) = pen_buttons.read().next() else { + return; + }; + + if !buttons.tip() { + return; + } + + let w = buffer.width as f32; + let h = buffer.height as f32; + let ui_x = base_pos.x * w; + let ui_y = base_pos.y * h; + + let pen_x = pos.x * w; + let pen_y = pos.y * h; + + let delta = Vec2::new(pen_x - ui_x, pen_y - ui_y); + let radius = 100.; + + let r_sq = radius * radius; + let dist_sq = delta.length_squared(); + + if dist_sq <= (r_sq * 1.5) { + return; + } + + draw_buffer.clear(Srgba::rgba_u8(0, 0, 0, 0).into()).ok(); +} + +#[allow(clippy::type_complexity)] +fn color_changer( + buffer: Query<(&DrawableLayer, &UiPosition), (With, With)>, + mut pen_color: ResMut, + mut pen_position: MessageReader, + mut pen_buttons: MessageReader, +) { + let Ok((buffer, base_pos)) = buffer.single() else { + return; + }; + + let Some(pos) = pen_position.read().next() else { + return; + }; + + let Some(buttons) = pen_buttons.read().next() else { + return; + }; + + if !buttons.tip() { + return; + } + + let w = buffer.width as f32; + let h = buffer.height as f32; + let ui_x = base_pos.x * w; + let ui_y = base_pos.y * h; + + let pen_x = pos.x * w; + let pen_y = pos.y * h; + + let delta = Vec2::new(pen_x - ui_x, pen_y - ui_y); + let radius = 100.; + + let r_sq = radius * radius; + let dist_sq = delta.length_squared(); + + if dist_sq > (r_sq * 1.5) { + return; + } + + pen_color.0 = color_from_pos(delta.x, delta.y, radius); +} + +fn color_wheel(buffer: &mut DrawableLayer, cx: i32, cy: i32, radius: i32) { + for dy in -radius..=radius { + for dx in -radius..=radius { + let x = cx + dx; + let y = cy + dy; + + buffer.set_color_at(x, y, color_from_pos(dx as f32, dy as f32, radius as f32)); + } + } +} + +fn color_from_pos(x: f32, y: f32, radius: f32) -> Color { + let r_sq = radius * radius; + let dist_sq = x * x + y * y; + + if dist_sq > r_sq { + return Color::srgba(0.0, 0.0, 0.0, 0.0); + } + + // Distance normalized (0.0 → 1.0) + let dist = dist_sq.sqrt() / radius; + + // Angle in radians → [0, 1] + let angle = y.atan2(x); // -PI..PI + let hue = (angle + std::f32::consts::PI) / (2.0 * std::f32::consts::PI); + + let (r, g, b) = hsl_to_rgb(hue, 1.0, dist); + + Color::srgba(r, g, b, 1.0) +} + +fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (f32, f32, f32) { + if s == 0.0 { + return (l, l, l); + } + + fn hue_to_rgb(p: f32, q: f32, mut t: f32) -> f32 { + if t < 0.0 { + t += 1.0; + } + if t > 1.0 { + t -= 1.0; + } + + if t < 1.0 / 6.0 { + return p + (q - p) * 6.0 * t; + } + if t < 1.0 / 2.0 { + return q; + } + if t < 2.0 / 3.0 { + return p + (q - p) * (2.0 / 3.0 - t) * 6.0; + } + + p + } + + let q = if l < 0.5 { + l * (1.0 + s) + } else { + l + s - l * s + }; + + let p = 2.0 * l - q; + + let r = hue_to_rgb(p, q, h + 1.0 / 3.0); + let g = hue_to_rgb(p, q, h); + let b = hue_to_rgb(p, q, h - 1.0 / 3.0); + + (r, g, b) +} diff --git a/src/kneeboardplugin.rs b/src/kneeboardplugin.rs index 340f91b..5df4bac 100644 --- a/src/kneeboardplugin.rs +++ b/src/kneeboardplugin.rs @@ -9,7 +9,7 @@ use openxr::Path; use serde::{Deserialize, Serialize}; use crate::{ - MyProcGenImage, + drawingplugin::MyProcGenImage, vrcontrollerplugin::{ LeftController, LeftControllerActions, RightController, RightControllerActions, }, diff --git a/src/main.rs b/src/main.rs index a227aa4..79e432f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,502 +1,25 @@ //! A simple 3D scene with light shining over a cube sitting on a plane. +mod drawingplugin; mod kneeboardplugin; mod otdipcplugin; mod vrcontrollerplugin; mod vrplugin; -use bevy_cef::prelude::WebviewExtendStandardMaterial; use vrplugin::VrPlugin; -use bevy::{ - asset::RenderAssetUsages, - color::palettes::css, - prelude::*, - render::render_resource::{Extent3d, TextureDimension, TextureFormat}, -}; +use bevy::prelude::*; -use crate::otdipcplugin::OtdIpcPlugin; use crate::{ - kneeboardplugin::KneeboardPlugin, - otdipcplugin::{PenButtons, PenPosition, PenPressure}, + drawingplugin::DrawingPlugin, kneeboardplugin::KneeboardPlugin, vrcontrollerplugin::VrControllersPlugin, }; fn main() { App::new() - .add_plugins((VrPlugin, MeshPickingPlugin)) + .add_plugins(VrPlugin) .add_plugins(VrControllersPlugin) - .add_plugins(OtdIpcPlugin) + .add_plugins(DrawingPlugin) .add_plugins(KneeboardPlugin) - .insert_resource(ClearColor(Color::NONE)) - .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(); } - -#[derive(Resource)] -struct MyProcGenImage(Handle); - -#[derive(Resource)] -struct PenSize(f32); - -#[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>) { - 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, mut pen_buttons: MessageReader) { - 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, - mut buffer: ResMut, - mut pen_size: ResMut, - mut pen_buttons: MessageReader, - mut pen_position: MessageReader, - mut pen_pressure: MessageReader, -) { - 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, - pen_color: Res, - mut lastloc: ResMut, - mut buffer: ResMut, - mut pen_buttons: MessageReader, - mut pen_position: MessageReader, -) { - let Some(buttons) = pen_buttons.read().next() else { - return; - }; - - 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; - - let Some(ll) = lastloc.0 else { - lastloc.0 = Some((x, y)); - return; - }; - - if buttons.state & 1 == 1 { - 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( - buffer: &mut ResMut, - 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( - buffer: &mut ResMut, - cx: i32, - cy: i32, - radius: i32, - color: Color, -) { - draw_filled_circle(buffer, cx, cy, radius, color); -} - -fn draw_filled_circle( - buffer: &mut ResMut, - 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( - buffer: &mut ResMut, - cx: i32, - cy: i32, - radius: i32, - thickness: f32, - color: Color, -) { - if radius <= 0 { - return; - } - - let half_t = (thickness / 2.0).max(0.5); - let outer_r = radius as f32 + half_t; - let inner_r = (radius as f32 - half_t).max(0.0); - - let outer_r2 = (outer_r * outer_r) as i32; - let inner_r2 = (inner_r * inner_r) as i32; - - let max_r = outer_r.ceil() as i32; - - for dy in -max_r..=max_r { - let y = cy + dy; - if y < 0 { - continue; - } - - let dy2 = dy * dy; - if dy2 > outer_r2 { - continue; - } - - let outer_dx = ((outer_r2 - dy2) as f32).sqrt() as i32; - let inner_dx = if dy2 < inner_r2 { - ((inner_r2 - dy2) as f32).sqrt() as i32 - } else { - -1 - }; - - let left_outer = cx - outer_dx; - let right_outer = cx + outer_dx; - - if inner_dx >= 0 { - let left_inner = cx - inner_dx; - let right_inner = cx + inner_dx; - - // 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, - buffer: Res, - mut images: ResMut>, - mut materials: ResMut>, - webviews: Query<&MeshMaterial3d>, - my_handle: Res, -) { - 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, -// webviews: Query<(Entity, &WebviewSize), With>, -// headset: Query<&GlobalTransform, With>, -// kneeboard: Query<(&GlobalTransform, Option<&LookedAt>), With>, -// 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); -// } -// } diff --git a/src/otdipcplugin.rs b/src/otdipcplugin.rs index 29718cc..0b60e29 100644 --- a/src/otdipcplugin.rs +++ b/src/otdipcplugin.rs @@ -2,6 +2,7 @@ use bevy::{ app::{App, Plugin, PreUpdate}, ecs::{message::MessageWriter, resource::Resource, system::ResMut}, math::Vec2, + prelude::Deref, }; use otd_ipc::{Message, OtdIpc}; use triple_buffer::{Output, triple_buffer}; @@ -15,14 +16,24 @@ struct TabletSize(Option); #[derive(Resource)] struct MaxPenPressure(Option); -#[derive(bevy::ecs::message::Message)] -pub struct PenPosition { - pub position: Vec2, -} +#[derive(bevy::ecs::message::Message, Deref)] +pub struct PenPosition(Vec2); -#[derive(bevy::ecs::message::Message)] -pub struct PenButtons { - pub state: u32, +#[derive(bevy::ecs::message::Message, Deref)] +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)] @@ -79,9 +90,7 @@ fn reader( return; }; - button_writer.write(PenButtons { - state: state.pen_buttons(), - }); + button_writer.write(PenButtons(state.pen_buttons())); let p = state.pressure() as f32 / pressure as f32; @@ -93,7 +102,7 @@ fn reader( let loc = Vec2::new(x, y); - position_writer.write(PenPosition { position: loc }); + position_writer.write(PenPosition(loc)); } _ => {} }