diff --git a/lighthouse-client/examples/snake.rs b/lighthouse-client/examples/snake.rs index 81b3d7f..2bbce6e 100644 --- a/lighthouse-client/examples/snake.rs +++ b/lighthouse-client/examples/snake.rs @@ -1,7 +1,7 @@ use clap::Parser; use futures::{Stream, lock::Mutex, StreamExt}; -use lighthouse_client::{Lighthouse, Result, TokioWebSocket, LIGHTHOUSE_URL, protocol::{Authentication, Color, Delta, Frame, Pos, ServerMessage, LIGHTHOUSE_RECT, LIGHTHOUSE_SIZE}}; -use lighthouse_protocol::Model; +use lighthouse_client::{Lighthouse, Result, TokioWebSocket, LIGHTHOUSE_URL, protocol::{Authentication, Color, Frame, ServerMessage, LIGHTHOUSE_RECT, LIGHTHOUSE_SIZE}}; +use lighthouse_protocol::{Delta, InputEvent, KeyEvent, Pos}; use tracing::{info, debug}; use tokio::{task, time}; use std::{collections::{VecDeque, HashSet}, sync::Arc, time::Duration}; @@ -13,14 +13,14 @@ const SNAKE_INITIAL_LENGTH: usize = 3; #[derive(Debug, PartialEq, Eq, Clone)] struct Snake { - fields: VecDeque, - dir: Delta, + fields: VecDeque>, + dir: Delta, } impl Snake { fn from_initial_length(length: usize) -> Self { - let mut pos: Pos = LIGHTHOUSE_RECT.sample_random().unwrap(); - let dir = Delta::random_cardinal(); + let mut pos: Pos = LIGHTHOUSE_RECT.sample_random().unwrap(); + let dir = Delta::::random_cardinal(); let mut fields = VecDeque::new(); for _ in 0..length { @@ -31,9 +31,9 @@ impl Snake { Self { fields, dir } } - fn head(&self) -> Pos { *self.fields.front().unwrap() } + fn head(&self) -> Pos { *self.fields.front().unwrap() } - fn back(&self) -> Pos { *self.fields.back().unwrap() } + fn back(&self) -> Pos { *self.fields.back().unwrap() } fn grow(&mut self) { self.fields.push_back(LIGHTHOUSE_RECT.wrap(self.back() - self.dir)); @@ -49,7 +49,7 @@ impl Snake { self.fields.iter().collect::>().len() < self.fields.len() } - fn rotate_head(&mut self, dir: Delta) { + fn rotate_head(&mut self, dir: Delta) { self.dir = dir; } @@ -63,7 +63,7 @@ impl Snake { self.fields.len() } - fn random_fruit_pos(&self) -> Option { + fn random_fruit_pos(&self) -> Option> { let fields = self.fields.iter().collect::>(); if fields.len() >= LIGHTHOUSE_SIZE { None @@ -81,7 +81,7 @@ impl Snake { #[derive(Debug, PartialEq, Eq, Clone)] struct State { snake: Snake, - fruit: Pos, + fruit: Pos, } impl State { @@ -142,16 +142,16 @@ async fn run_updater(lh: Lighthouse, shared_state: Arc>> + Unpin, shared_state: Arc>) -> Result<()> { +async fn run_controller(mut stream: impl Stream>> + Unpin, shared_state: Arc>) -> Result<()> { while let Some(msg) = stream.next().await { - if let Model::InputEvent(event) = msg?.payload { - if event.is_down { + match msg?.payload { + InputEvent::Key(KeyEvent { key, down, .. }) if down => { // Map the key code to a direction vector - let opt_dir = match event.key { - Some(37) => Some(Delta::LEFT), - Some(38) => Some(Delta::UP), - Some(39) => Some(Delta::RIGHT), - Some(40) => Some(Delta::DOWN), + let opt_dir = match key.as_str() { + "ArrowLeft" => Some(Delta::::LEFT), + "ArrowUp" => Some(Delta::::UP), + "ArrowRight" => Some(Delta::::RIGHT), + "ArrowDown" => Some(Delta::::DOWN), _ => None, }; @@ -162,6 +162,7 @@ async fn run_controller(mut stream: impl Stream {}, } } @@ -193,7 +194,7 @@ async fn main() -> Result<()> { let lh = Lighthouse::connect_with_tokio_to(&args.url, auth).await?; info!("Connected to the Lighthouse server"); - let stream = lh.stream_model().await?; + let stream = lh.stream_input().await?; let updater_handle = task::spawn(run_updater(lh, state.clone())); let controller_handle = task::spawn(run_controller(stream, state)); diff --git a/lighthouse-client/src/lighthouse.rs b/lighthouse-client/src/lighthouse.rs index 95bf3f1..992c197 100644 --- a/lighthouse-client/src/lighthouse.rs +++ b/lighthouse-client/src/lighthouse.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, fmt::Debug, sync::{atomic::{AtomicI32, Ordering} use async_tungstenite::tungstenite::{Message, self}; use futures::{prelude::*, channel::mpsc::{Sender, self}, stream::{SplitSink, SplitStream}, lock::Mutex}; -use lighthouse_protocol::{Authentication, ClientMessage, DirectoryTree, Frame, LaserMetrics, Model, ServerMessage, Value, Verb}; +use lighthouse_protocol::{Authentication, ClientMessage, DirectoryTree, Frame, InputEvent, LaserMetrics, Model, ServerMessage, Value, Verb}; use serde::{Deserialize, Serialize}; use stream_guard::GuardStreamExt; use tracing::{warn, error, debug, info}; @@ -130,6 +130,25 @@ impl Lighthouse self.stream(&["user".into(), username, "model".into()], ()).await } + /// Sends an input event to the user's input endpoint. + /// + /// Note that this is the new API which not all clients may support. + pub async fn put_input(&self, payload: InputEvent) -> Result> { + let username = self.authentication.username.clone(); + self.put(&["user".into(), username, "input".into()], payload).await + } + + /// Streams input events from the user's input endpoint. + /// + /// Note that this is the new API which not all clients may support (in LUNA + /// disabling the legacy mode will send events to this endpoint). If your + /// client or library does not support this, you may need to `stream_model` + /// and parse `LegacyInputEvent`s from there. + pub async fn stream_input(&self) -> Result>>> { + let username = self.authentication.username.clone(); + self.stream(&["user".into(), username, "input".into()], ()).await + } + /// Fetches lamp server metrics. pub async fn get_laser_metrics(&self) -> Result> { self.get(&["metrics", "laser"]).await diff --git a/lighthouse-protocol/Cargo.toml b/lighthouse-protocol/Cargo.toml index cb83196..cb2fe5e 100644 --- a/lighthouse-protocol/Cargo.toml +++ b/lighthouse-protocol/Cargo.toml @@ -14,3 +14,6 @@ rand = "0.8" rmpv = { version = "1.0.1", features = ["with-serde"] } serde = { version = "1.0", features = ["derive"] } serde_with = "3.4" + +[dev-dependencies] +serde_json = "1.0" diff --git a/lighthouse-protocol/src/constants.rs b/lighthouse-protocol/src/constants.rs index 09aca2a..90da312 100644 --- a/lighthouse-protocol/src/constants.rs +++ b/lighthouse-protocol/src/constants.rs @@ -1,4 +1,4 @@ -use crate::{Rect, Pos, Delta}; +use crate::{Rect, Vec2, Zero}; /// The number of rows of the lighthouse. pub const LIGHTHOUSE_ROWS: usize = 14; @@ -9,4 +9,4 @@ pub const LIGHTHOUSE_SIZE: usize = LIGHTHOUSE_ROWS * LIGHTHOUSE_COLS; /// The total number of bytes in a lighthouse frame. pub const LIGHTHOUSE_BYTES: usize = LIGHTHOUSE_SIZE * 3; /// The rectangle of valid coordinates on the lighthouse. -pub const LIGHTHOUSE_RECT: Rect = Rect::new(Pos::ZERO, Delta::new(LIGHTHOUSE_COLS as i32, LIGHTHOUSE_ROWS as i32)); +pub const LIGHTHOUSE_RECT: Rect = Rect::new(Vec2::ZERO, Vec2::new(LIGHTHOUSE_COLS as i32, LIGHTHOUSE_ROWS as i32)); diff --git a/lighthouse-protocol/src/frame.rs b/lighthouse-protocol/src/frame.rs index 244d63f..8a2c188 100644 --- a/lighthouse-protocol/src/frame.rs +++ b/lighthouse-protocol/src/frame.rs @@ -51,16 +51,16 @@ impl Frame { } } -impl Index for Frame { +impl Index> for Frame { type Output = Color; - fn index(&self, pos: Pos) -> &Color { + fn index(&self, pos: Pos) -> &Color { &self.pixels[LIGHTHOUSE_RECT.index_of(pos)] } } -impl IndexMut for Frame { - fn index_mut(&mut self, pos: Pos) -> &mut Color { +impl IndexMut> for Frame { + fn index_mut(&mut self, pos: Pos) -> &mut Color { &mut self.pixels[LIGHTHOUSE_RECT.index_of(pos)] } } diff --git a/lighthouse-protocol/src/input/event_source.rs b/lighthouse-protocol/src/input/event_source.rs new file mode 100644 index 0000000..6621bc1 --- /dev/null +++ b/lighthouse-protocol/src/input/event_source.rs @@ -0,0 +1,9 @@ +use serde::{Deserialize, Serialize}; + +/// An identifier that is unique per client + device combo. +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(untagged)] +pub enum EventSource { + String(String), + Int(i32), +} diff --git a/lighthouse-protocol/src/input/gamepad_axis_event.rs b/lighthouse-protocol/src/input/gamepad_axis_event.rs new file mode 100644 index 0000000..bd2f4db --- /dev/null +++ b/lighthouse-protocol/src/input/gamepad_axis_event.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; + +/// An axis event on a gamepad. +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[serde(tag = "control", rename_all = "camelCase")] +pub struct GamepadAxisEvent { + /// The axis index. + pub index: usize, + /// The value of the axis (between -1.0 and 1.0, modeled after the Web Gamepad API). + pub value: f64, +} diff --git a/lighthouse-protocol/src/input/gamepad_button_event.rs b/lighthouse-protocol/src/input/gamepad_button_event.rs new file mode 100644 index 0000000..daf0ac2 --- /dev/null +++ b/lighthouse-protocol/src/input/gamepad_button_event.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; + +/// A button event on a gamepad. +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[serde(tag = "control", rename_all = "camelCase")] +pub struct GamepadButtonEvent { + /// The button index. + pub index: usize, + /// Whether the button is pressed. + pub down: bool, + /// The value of the button (between 0.0 and 1.0, modeled after the Web Gamepad API). + pub value: f64, +} diff --git a/lighthouse-protocol/src/input/gamepad_control_event.rs b/lighthouse-protocol/src/input/gamepad_control_event.rs new file mode 100644 index 0000000..9006bd2 --- /dev/null +++ b/lighthouse-protocol/src/input/gamepad_control_event.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; + +use super::{GamepadAxisEvent, GamepadButtonEvent}; + +/// A control-specific event on a gamepad. +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[serde(tag = "control", rename_all = "camelCase")] +pub enum GamepadControlEvent { + Button(GamepadButtonEvent), + Axis(GamepadAxisEvent), +} diff --git a/lighthouse-protocol/src/input/gamepad_event.rs b/lighthouse-protocol/src/input/gamepad_event.rs new file mode 100644 index 0000000..0eee81e --- /dev/null +++ b/lighthouse-protocol/src/input/gamepad_event.rs @@ -0,0 +1,14 @@ +use serde::{Deserialize, Serialize}; + +use super::{EventSource, GamepadControlEvent}; + +/// A gamepad/controller event. +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct GamepadEvent { + /// The client identifier. Also unique per gamepad. + pub source: EventSource, + /// The control-specific info. + #[serde(flatten)] + pub control: GamepadControlEvent, +} diff --git a/lighthouse-protocol/src/input/input_event.rs b/lighthouse-protocol/src/input/input_event.rs new file mode 100644 index 0000000..7e6f8c2 --- /dev/null +++ b/lighthouse-protocol/src/input/input_event.rs @@ -0,0 +1,98 @@ +use serde::{Deserialize, Serialize}; + +use super::{GamepadEvent, KeyEvent, MouseEvent}; + +/// A user input event, as generated by the new frontend (LUNA). +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum InputEvent { + Key(KeyEvent), + Mouse(MouseEvent), + Gamepad(GamepadEvent), +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use crate::{EventSource, GamepadAxisEvent, GamepadButtonEvent, GamepadControlEvent, GamepadEvent, InputEvent, KeyEvent, MouseButton, MouseEvent, Pos}; + + #[test] + fn key_event() { + assert_eq!( + serde_json::from_value::(json!({ + "type": "key", + "source": 0, + "down": true, + "key": "ArrowUp", + })).unwrap(), + InputEvent::Key(KeyEvent { + source: EventSource::Int(0), + down: true, + key: "ArrowUp".into(), + }) + ); + } + + #[test] + fn mouse_event() { + assert_eq!( + serde_json::from_value::(json!({ + "type": "mouse", + "source": 1, + "button": "left", + "pos": { + "x": 2, + "y": 4, + }, + })).unwrap(), + InputEvent::Mouse(MouseEvent { + source: EventSource::Int(1), + button: MouseButton::Left, + pos: Pos::new(2.0, 4.0), + }) + ); + } + + #[test] + fn gamepad_button_event() { + assert_eq!( + serde_json::from_value::(json!({ + "type": "gamepad", + "source": 1, + "control": "button", + "index": 42, + "down": true, + "value": 0.25, + })).unwrap(), + InputEvent::Gamepad(GamepadEvent { + source: EventSource::Int(1), + control: GamepadControlEvent::Button(GamepadButtonEvent { + index: 42, + down: true, + value: 0.25, + }), + }) + ); + } + + #[test] + fn gamepad_axis_event() { + assert_eq!( + serde_json::from_value::(json!({ + "type": "gamepad", + "source": 1, + "control": "axis", + "index": 42, + "value": 0.25, + })).unwrap(), + InputEvent::Gamepad(GamepadEvent { + source: EventSource::Int(1), + control: GamepadControlEvent::Axis(GamepadAxisEvent { + index: 42, + value: 0.25, + }), + }) + ); + } +} diff --git a/lighthouse-protocol/src/input/key_event.rs b/lighthouse-protocol/src/input/key_event.rs new file mode 100644 index 0000000..647b3ec --- /dev/null +++ b/lighthouse-protocol/src/input/key_event.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; + +use super::EventSource; + +/// A keyboard event. +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct KeyEvent { + /// The client identifier. + pub source: EventSource, + /// Whether the key was pressed. + pub down: bool, + /// The key pressed, see the docs on JS's `KeyboardEvent.key` for details. + pub key: String, // TODO: Extract stronger `Key` type +} diff --git a/lighthouse-protocol/src/input_event.rs b/lighthouse-protocol/src/input/legacy_input_event.rs similarity index 65% rename from lighthouse-protocol/src/input_event.rs rename to lighthouse-protocol/src/input/legacy_input_event.rs index 3f000d1..8f92d75 100644 --- a/lighthouse-protocol/src/input_event.rs +++ b/lighthouse-protocol/src/input/legacy_input_event.rs @@ -1,8 +1,9 @@ use serde::{Serialize, Deserialize}; -/// A key/controller input event. +/// A keyboard/controller input event, as generated by the new frontend (LUNA) +/// in "Legacy Mode" (or the old website). #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)] -pub struct InputEvent { +pub struct LegacyInputEvent { #[serde(rename = "src")] pub source: i32, pub key: Option, diff --git a/lighthouse-protocol/src/input/mod.rs b/lighthouse-protocol/src/input/mod.rs new file mode 100644 index 0000000..bf5765f --- /dev/null +++ b/lighthouse-protocol/src/input/mod.rs @@ -0,0 +1,21 @@ +mod event_source; +mod gamepad_axis_event; +mod gamepad_button_event; +mod gamepad_control_event; +mod gamepad_event; +mod input_event; +mod key_event; +mod legacy_input_event; +mod mouse_button; +mod mouse_event; + +pub use event_source::*; +pub use gamepad_axis_event::*; +pub use gamepad_button_event::*; +pub use gamepad_control_event::*; +pub use gamepad_event::*; +pub use input_event::*; +pub use key_event::*; +pub use legacy_input_event::*; +pub use mouse_button::*; +pub use mouse_event::*; diff --git a/lighthouse-protocol/src/input/mouse_button.rs b/lighthouse-protocol/src/input/mouse_button.rs new file mode 100644 index 0000000..13967bd --- /dev/null +++ b/lighthouse-protocol/src/input/mouse_button.rs @@ -0,0 +1,12 @@ +use serde::{Deserialize, Serialize}; + +/// A mouse button. +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "camelCase")] +pub enum MouseButton { + Left, + Middle, + Right, + #[serde(untagged)] + Unknown(String), +} diff --git a/lighthouse-protocol/src/input/mouse_event.rs b/lighthouse-protocol/src/input/mouse_event.rs new file mode 100644 index 0000000..d09ece0 --- /dev/null +++ b/lighthouse-protocol/src/input/mouse_event.rs @@ -0,0 +1,17 @@ +use serde::{Deserialize, Serialize}; + +use crate::Pos; + +use super::{EventSource, MouseButton}; + +/// A mouse event. +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct MouseEvent { + /// The client identifier. + pub source: EventSource, + /// The mouse button. + pub button: MouseButton, + /// The mouse position. + pub pos: Pos, +} diff --git a/lighthouse-protocol/src/lib.rs b/lighthouse-protocol/src/lib.rs index da03808..f3a4b94 100644 --- a/lighthouse-protocol/src/lib.rs +++ b/lighthouse-protocol/src/lib.rs @@ -2,7 +2,7 @@ mod authentication; mod client_message; mod constants; mod frame; -mod input_event; +mod input; mod payload; mod server_message; mod utils; @@ -12,7 +12,7 @@ pub use authentication::*; pub use client_message::*; pub use constants::*; pub use frame::*; -pub use input_event::*; +pub use input::*; pub use payload::*; pub use server_message::*; pub use utils::*; diff --git a/lighthouse-protocol/src/payload/model.rs b/lighthouse-protocol/src/payload/model.rs index e82de35..35ab65a 100644 --- a/lighthouse-protocol/src/payload/model.rs +++ b/lighthouse-protocol/src/payload/model.rs @@ -1,11 +1,11 @@ use serde::{Serialize, Deserialize}; -use crate::{Frame, InputEvent}; +use crate::{Frame, LegacyInputEvent}; /// The payload of a model message. #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] #[serde(untagged)] pub enum Model { Frame(Frame), - InputEvent(InputEvent), + InputEvent(LegacyInputEvent), } diff --git a/lighthouse-protocol/src/utils/mod.rs b/lighthouse-protocol/src/utils/mod.rs index caddcba..c637d92 100644 --- a/lighthouse-protocol/src/utils/mod.rs +++ b/lighthouse-protocol/src/utils/mod.rs @@ -1,11 +1,15 @@ mod color; -mod delta; -mod pos; mod rect; +mod rem_euclid; mod rotation; +mod unity; +mod vec2; +mod zero; pub use color::*; -pub use delta::*; -pub use pos::*; pub use rect::*; +pub use rem_euclid::*; pub use rotation::*; +pub use unity::*; +pub use vec2::*; +pub use zero::*; diff --git a/lighthouse-protocol/src/utils/pos.rs b/lighthouse-protocol/src/utils/pos.rs deleted file mode 100644 index a383e7e..0000000 --- a/lighthouse-protocol/src/utils/pos.rs +++ /dev/null @@ -1,62 +0,0 @@ -use std::{fmt, ops::{Add, Sub, AddAssign, SubAssign}}; - -use crate::Delta; - -/// A position on the integer grid. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub struct Pos { - pub x: i32, - pub y: i32, -} - -impl Pos { - /// The origin. - pub const ZERO: Self = Self::new(0, 0); - - /// Creates a mew position. - pub const fn new(x: i32, y: i32) -> Self { - Self { x, y } - } -} - -impl fmt::Display for Pos { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "({}, {})", self.x, self.y) - } -} - -impl Add for Pos { - type Output = Pos; - - fn add(self, rhs: Delta) -> Self { - Self::new(self.x + rhs.dx, self.y + rhs.dy) - } -} - -impl Sub for Pos { - type Output = Delta; - - fn sub(self, rhs: Self) -> Delta { - Delta::new(self.x - rhs.x, self.y - rhs.y) - } -} - -impl Sub for Pos { - type Output = Pos; - - fn sub(self, rhs: Delta) -> Self { - self + (-rhs) - } -} - -impl AddAssign for Pos { - fn add_assign(&mut self, rhs: Delta) { - *self = *self + rhs; - } -} - -impl SubAssign for Pos { - fn sub_assign(&mut self, rhs: Delta) { - *self = *self - rhs; - } -} diff --git a/lighthouse-protocol/src/utils/rect.rs b/lighthouse-protocol/src/utils/rect.rs index f3e9f65..c9eb8cc 100644 --- a/lighthouse-protocol/src/utils/rect.rs +++ b/lighthouse-protocol/src/utils/rect.rs @@ -1,82 +1,100 @@ -use std::ops::Range; +use std::{fmt::Debug, ops::{Add, Mul, Range, Sub}}; use rand::{Rng, seq::IteratorRandom, thread_rng}; -use crate::{Pos, Delta, LIGHTHOUSE_COLS}; +use crate::{Vec2, LIGHTHOUSE_COLS}; + +use super::{RemEuclid, Zero}; /// A rectangle on the integer grid. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub struct Rect { - pub origin: Pos, - pub size: Delta, +pub struct Rect { + pub origin: Vec2, + pub size: Vec2, } -impl Rect { +impl Rect { /// Creates a new rectangle. - pub const fn new(origin: Pos, size: Delta) -> Self { + pub const fn new(origin: Vec2, size: Vec2) -> Self { Self { origin, size } } +} - /// The range of x values. - pub const fn x_range(self) -> Range { - self.origin.x..(self.origin.x + self.size.dx) +impl Rect where T: Copy { + /// The rectangle's width. + pub const fn width(self) -> T { + self.size.x } - /// The range of y values. - pub const fn y_range(self) -> Range { - self.origin.y..(self.origin.y + self.size.dy) + /// The rectangle's height. + pub const fn height(self) -> T { + self.size.y } +} - /// Checks whether the rectangle contains the given position. - pub const fn contains(self, pos: Pos) -> bool { - pos.x >= self.origin.x && pos.x < self.origin.x + self.width() - && pos.y >= self.origin.y && pos.y < self.origin.y + self.height() +impl Rect where T: Mul + Copy { + /// The rectangle's area. + pub fn area(self) -> T { + self.width() * self.height() } +} - /// Converts a position to an index. - pub fn index_of(self, pos: Pos) -> usize { - debug_assert!(self.contains(pos)); - let relative = pos - self.origin; - relative.dy as usize * LIGHTHOUSE_COLS + relative.dx as usize +impl Rect where T: RemEuclid + Copy { + /// Wraps a value to the rectangle's bounds. + pub fn wrap(self, pos: Vec2) -> Vec2 { + Vec2::new( + pos.x.rem_euclid(self.width()), + pos.y.rem_euclid(self.height()), + ) } +} +impl Rect where T: Zero + Eq + Copy { /// Whether this rectangle is empty. - pub const fn is_empty(self) -> bool { - self.size.dx == 0 && self.size.dy == 0 + pub fn is_empty(self) -> bool { + self.size.x == T::ZERO && self.size.y == T::ZERO + } +} + +impl Rect where T: Add + Copy { + /// The range of x values. + pub fn x_range(self) -> Range { + self.origin.x..(self.origin.x + self.size.x) } + /// The range of y values. + pub fn y_range(self) -> Range { + self.origin.y..(self.origin.y + self.size.y) + } +} + +impl Rect where T: Add + Copy, Range: IteratorRandom { /// Samples a random position within the rectangle with the given rng. - pub fn sample_random_with(self, rng: &mut impl Rng) -> Option { + pub fn sample_random_with(self, rng: &mut impl Rng) -> Option> { let x = self.x_range().choose(rng)?; let y = self.y_range().choose(rng)?; - Some(Pos::new(x, y)) + Some(Vec2::::new(x, y)) } /// Samples a random position within the rectangle. - pub fn sample_random(self) -> Option { + pub fn sample_random(self) -> Option> { self.sample_random_with(&mut thread_rng()) } +} - /// The rectangle's width. - pub const fn width(self) -> i32 { - self.size.dx - } - - /// The rectangle's height. - pub const fn height(self) -> i32 { - self.size.dy - } - - /// The rectangle's area. - pub const fn area(self) -> i32 { - self.width() * self.height() +impl Rect where T: Add + Ord + Copy { + /// Checks whether the rectangle contains the given position. + pub fn contains(self, pos: Vec2) -> bool { + pos.x >= self.origin.x && pos.x < self.origin.x + self.width() + && pos.y >= self.origin.y && pos.y < self.origin.y + self.height() } +} - /// Wraps a value to the rectangle's bounds. - pub const fn wrap(self, pos: Pos) -> Pos { - Pos::new( - pos.x.rem_euclid(self.width()), - pos.y.rem_euclid(self.height()), - ) +impl Rect where T: Add + Sub + TryInto + Ord + Copy, T::Error: Debug { + /// Converts a position to an index. + pub fn index_of(self, pos: Vec2) -> usize { + debug_assert!(self.contains(pos)); + let relative = pos - self.origin; + relative.y.try_into().unwrap() * LIGHTHOUSE_COLS + relative.x.try_into().unwrap() } } diff --git a/lighthouse-protocol/src/utils/rem_euclid.rs b/lighthouse-protocol/src/utils/rem_euclid.rs new file mode 100644 index 0000000..1167338 --- /dev/null +++ b/lighthouse-protocol/src/utils/rem_euclid.rs @@ -0,0 +1,20 @@ +/// A type that supports Euclidean modulo. +pub trait RemEuclid { + fn rem_euclid(self, rhs: Self) -> Self; +} + +macro_rules! impl_rem_euclid { + ($($tys:ty),*) => { + $(impl RemEuclid for $tys { + fn rem_euclid(self, rhs: Self) -> Self { + <$tys>::rem_euclid(self, rhs) + } + })* + }; +} + +impl_rem_euclid!( + u8, u16, u32, u64, u128, usize, + i8, i16, i32, i64, i128, isize, + f32, f64 +); diff --git a/lighthouse-protocol/src/utils/rotation.rs b/lighthouse-protocol/src/utils/rotation.rs index 788e978..1d43823 100644 --- a/lighthouse-protocol/src/utils/rotation.rs +++ b/lighthouse-protocol/src/utils/rotation.rs @@ -1,40 +1,44 @@ -use std::ops::Mul; +use std::ops::{Add, Mul, Neg}; use rand::{thread_rng, Rng}; -use crate::Delta; +use crate::Vec2; + +use super::{Unity, Zero}; + +// TODO: Rename this to Mat2? /// An 2D rotation that is representable using an integer matrix. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct Rotation { +pub struct Rotation { /// The integer matrix representing the corresponding linear transformation. - matrix: [i32; 4], + matrix: [T; 4], } -impl Rotation { +impl Rotation where T: Zero + Unity + Neg { /// The identity rotation. pub const IDENTITY: Self = Self::new([ - 1, 0, - 0, 1, + T::ONE, T::ZERO, + T::ZERO, T::ONE, ]); /// The rotation by 90° clockwise. pub const CW_90: Self = Self::new([ - 0, -1, - 1, 0, + T::ZERO, T::NEG_ONE, + T::ONE, T::ZERO, ]); /// The rotation by 180° clockwise or counter-clockwise. pub const CW_180: Self = Self::new([ - -1, 0, - 0, -1, + T::NEG_ONE, T::ZERO, + T::ZERO, T::NEG_ONE, ]); /// The rotation by 270° clockwise (or 90° counter-clockwise). pub const CW_270: Self = Self::new([ - 0, 1, - -1, 0, + T::ZERO, T::ONE, + T::NEG_ONE, T::ZERO, ]); /// Creates a new rotation from the given matrix. - pub const fn new(matrix: [i32; 4]) -> Self { + pub const fn new(matrix: [T; 4]) -> Self { Self { matrix } } @@ -55,8 +59,8 @@ impl Rotation { } } -impl Mul for Rotation { - type Output = Rotation; +impl Mul for Rotation where T: Zero + Unity + Neg + Add + Mul + Copy { + type Output = Self; fn mul(self, rhs: Self) -> Self { // Standard 2x2 matrix multiplication @@ -69,40 +73,40 @@ impl Mul for Rotation { } } -impl Mul for Rotation { - type Output = Delta; +impl Mul> for Rotation where T: Zero + Unity + Neg + Add + Mul + Copy { + type Output = Vec2; - fn mul(self, rhs: Delta) -> Delta { + fn mul(self, rhs: Vec2) -> Vec2 { // Standard matrix-vector multiplication - Delta::new( - self.matrix[0] * rhs.dx + self.matrix[1] * rhs.dy, - self.matrix[2] * rhs.dx + self.matrix[3] * rhs.dy , + Vec2::new( + self.matrix[0] * rhs.x + self.matrix[1] * rhs.y, + self.matrix[2] * rhs.x + self.matrix[3] * rhs.y , ) } } #[cfg(test)] mod tests { - use crate::Delta; + use crate::Vec2; use super::Rotation; #[test] fn rotation() { - assert_eq!(Rotation::IDENTITY * Delta::new(4, -3), Delta::new(4, -3)); - assert_eq!(Rotation::CW_90 * Delta::new(2, 3), Delta::new(-3, 2)); - assert_eq!(Rotation::CW_90 * Delta::RIGHT, Delta::DOWN); - assert_eq!(Rotation::CW_90 * Delta::DOWN, Delta::LEFT); - assert_eq!(Rotation::CW_90 * Delta::LEFT, Delta::UP); - assert_eq!(Rotation::CW_90 * Delta::UP, Delta::RIGHT); + assert_eq!(Rotation::IDENTITY * Vec2::new(4, -3), Vec2::new(4, -3)); + assert_eq!(Rotation::CW_90 * Vec2::new(2, 3), Vec2::new(-3, 2)); + assert_eq!(Rotation::CW_90 * Vec2::::RIGHT, Vec2::DOWN); + assert_eq!(Rotation::CW_90 * Vec2::::DOWN, Vec2::LEFT); + assert_eq!(Rotation::CW_90 * Vec2::::LEFT, Vec2::UP); + assert_eq!(Rotation::CW_90 * Vec2::::UP, Vec2::RIGHT); } #[test] fn matmul() { - assert_eq!(Rotation::IDENTITY * Rotation::IDENTITY, Rotation::IDENTITY); - assert_eq!(Rotation::IDENTITY * Rotation::CW_90, Rotation::CW_90); - assert_eq!(Rotation::CW_90 * Rotation::CW_90, Rotation::CW_180); - assert_eq!(Rotation::CW_90 * Rotation::CW_180, Rotation::CW_270); - assert_eq!(Rotation::CW_180 * Rotation::CW_180, Rotation::IDENTITY); + assert_eq!(Rotation::IDENTITY * Rotation::::IDENTITY, Rotation::IDENTITY); + assert_eq!(Rotation::IDENTITY * Rotation::::CW_90, Rotation::CW_90); + assert_eq!(Rotation::CW_90 * Rotation::::CW_90, Rotation::CW_180); + assert_eq!(Rotation::CW_90 * Rotation::::CW_180, Rotation::CW_270); + assert_eq!(Rotation::CW_180 * Rotation::::CW_180, Rotation::IDENTITY); } } diff --git a/lighthouse-protocol/src/utils/unity.rs b/lighthouse-protocol/src/utils/unity.rs new file mode 100644 index 0000000..7723f0f --- /dev/null +++ b/lighthouse-protocol/src/utils/unity.rs @@ -0,0 +1,33 @@ +/// A type that has positive and negative unit values. +pub trait Unity { + /// The unit value. + const ONE: Self; + /// The negative unit value. + const NEG_ONE: Self; +} + +macro_rules! impl_int_unity { + ($($tys:ty),*) => { + $(impl Unity for $tys { + const ONE: Self = 1; + const NEG_ONE: Self = -1; + })* + }; +} + +macro_rules! impl_float_unity { + ($($tys:ty),*) => { + $(impl Unity for $tys { + const ONE: Self = 1.0; + const NEG_ONE: Self = -1.0; + })* + }; +} + +impl_int_unity!( + i8, i16, i32, i64, i128, isize +); + +impl_float_unity!( + f32, f64 +); diff --git a/lighthouse-protocol/src/utils/vec2.rs b/lighthouse-protocol/src/utils/vec2.rs new file mode 100644 index 0000000..eb516c6 --- /dev/null +++ b/lighthouse-protocol/src/utils/vec2.rs @@ -0,0 +1,110 @@ +use std::{fmt, ops::{Add, AddAssign, Neg, Sub, SubAssign}}; + +use rand::{thread_rng, Rng}; +use serde::{Deserialize, Serialize}; + +use super::{Unity, Zero}; + +/// A 2D vector. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Vec2 { + pub x: T, + pub y: T, +} + +impl Vec2 { + /// Creates a mew position. + pub const fn new(x: T, y: T) -> Self { + Self { x, y } + } + + /// Maps a function over the vector. + pub fn map(self, mut f: impl FnMut(T) -> U) -> Vec2 { + Vec2 { + x: f(self.x), + y: f(self.y), + } + } +} + +impl Zero for Vec2 where T: Zero { + /// The origin. + const ZERO: Self = Self::new(T::ZERO, T::ZERO); +} + +impl Vec2 where T: Zero + Unity { + /// The vector pointing one pixel to the left. + pub const LEFT: Self = Self::new(T::NEG_ONE, T::ZERO); + /// The vector pointing one pixel up. + pub const UP: Self = Self::new(T::ZERO, T::NEG_ONE); + /// The vector pointing one pixel to the right. + pub const RIGHT: Self = Self::new(T::ONE, T::ZERO); + /// The vector pointing one pixel down. + pub const DOWN: Self = Self::new(T::ZERO, T::ONE); + + /// Randomly one of the four cardinal rotations with the given rng. + pub fn random_cardinal_with(rng: &mut impl Rng) -> Self { + match rng.gen_range(0..4) { + 0 => Self::LEFT, + 1 => Self::UP, + 2 => Self::RIGHT, + 3 => Self::DOWN, + _ => unreachable!(), + } + } + + /// Randomly one of the four cardinal rotations with the thread-local rng. + pub fn random_cardinal() -> Self { + Self::random_cardinal_with(&mut thread_rng()) + } +} + +impl fmt::Display for Vec2 where T: fmt::Display { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "({}, {})", self.x, self.y) + } +} + +impl Add for Vec2 where T: Add { + type Output = Self; + + fn add(self, rhs: Vec2) -> Self { + Self::new(self.x + rhs.x, self.y + rhs.y) + } +} + +impl Neg for Vec2 where T: Neg { + type Output = Self; + + fn neg(self) -> Self { + Self::new(-self.x, -self.y) + } +} + +impl Sub for Vec2 where T: Sub { + type Output = Self; + + fn sub(self, rhs: Vec2) -> Self { + Self::new(self.x - rhs.x, self.y - rhs.y) + } +} + +impl AddAssign for Vec2 where T: AddAssign { + fn add_assign(&mut self, rhs: Vec2) { + self.x += rhs.x; + self.y += rhs.y; + } +} + +impl SubAssign for Vec2 where T: SubAssign { + fn sub_assign(&mut self, rhs: Vec2) { + self.x -= rhs.x; + self.y -= rhs.y; + } +} + +/// A type alias that semantically expresses a position. +pub type Pos = Vec2; + +/// A type alias that semantically expresses an offset/delta. +pub type Delta = Vec2; diff --git a/lighthouse-protocol/src/utils/zero.rs b/lighthouse-protocol/src/utils/zero.rs new file mode 100644 index 0000000..da92df2 --- /dev/null +++ b/lighthouse-protocol/src/utils/zero.rs @@ -0,0 +1,30 @@ +/// A type that has a zero value. +pub trait Zero { + /// The zero value. + const ZERO: Self; +} + +macro_rules! impl_int_zero { + ($($tys:ty),*) => { + $(impl Zero for $tys { + const ZERO: Self = 0; + })* + }; +} + +macro_rules! impl_float_zero { + ($($tys:ty),*) => { + $(impl Zero for $tys { + const ZERO: Self = 0.0; + })* + }; +} + +impl_int_zero!( + u8, u16, u32, u64, u128, usize, + i8, i16, i32, i64, i128, isize +); + +impl_float_zero!( + f32, f64 +);