Compare commits
26 Commits
main
...
ray-tracer
Author | SHA1 | Date |
---|---|---|
Savanni D'Gerinel | 4d67ea4af2 | |
Savanni D'Gerinel | f347e2e47d | |
Savanni D'Gerinel | 324d37f858 | |
Savanni D'Gerinel | 59dfaf1696 | |
Savanni D'Gerinel | 2fbb468830 | |
Savanni D'Gerinel | fa4ec059f7 | |
Savanni D'Gerinel | 4f47d65ba5 | |
Savanni D'Gerinel | f15fa9dd48 | |
Savanni D'Gerinel | af75bc20c8 | |
Savanni D'Gerinel | b07925a2c3 | |
Savanni D'Gerinel | 40bfe6d74f | |
Savanni D'Gerinel | af7d8680a0 | |
Savanni D'Gerinel | 8e4f6b06e6 | |
Savanni D'Gerinel | bd899e3a2e | |
Savanni D'Gerinel | 7be0baba53 | |
Savanni D'Gerinel | a2aa132886 | |
Savanni D'Gerinel | 971206d325 | |
Savanni D'Gerinel | c2777e2a70 | |
Savanni D'Gerinel | 2569a48792 | |
Savanni D'Gerinel | 15d87fbde6 | |
Savanni D'Gerinel | 3c8536deb6 | |
Savanni D'Gerinel | d0a8be63e9 | |
Savanni D'Gerinel | 2a38ca38e1 | |
Savanni D'Gerinel | 39c947b461 | |
Savanni D'Gerinel | e23a4aacab | |
Savanni D'Gerinel | 01f3e05235 |
File diff suppressed because it is too large
Load Diff
|
@ -2,9 +2,6 @@
|
|||
resolver = "2"
|
||||
members = [
|
||||
"authdb",
|
||||
"bike-lights/bike",
|
||||
"bike-lights/core",
|
||||
"bike-lights/simulator",
|
||||
"changeset",
|
||||
"config",
|
||||
"config-derive",
|
||||
|
@ -30,5 +27,5 @@ members = [
|
|||
"sgf",
|
||||
"timezone-testing",
|
||||
"tree",
|
||||
"visions/server", "gm-dash/server",
|
||||
"visions/server", "ray-tracer",
|
||||
]
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
[build]
|
||||
target = "thumbv6m-none-eabi"
|
||||
|
||||
[target.thumbv6m-none-eabi]
|
||||
rustflags = [
|
||||
"-C", "link-arg=--nmagic",
|
||||
"-C", "link-arg=-Tlink.x",
|
||||
"-C", "inline-threshold=5",
|
||||
"-C", "no-vectorize-loops",
|
||||
]
|
||||
|
||||
runner = "elf2uf2-rs -d"
|
|
@ -1,18 +0,0 @@
|
|||
[package]
|
||||
name = "bike"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
az = { version = "1" }
|
||||
cortex-m-rt = { version = "0.7.3" }
|
||||
cortex-m = { version = "0.7.7" }
|
||||
embedded-alloc = { version = "0.5.1" }
|
||||
embedded-hal = { version = "0.2.7" }
|
||||
fixed = { version = "1" }
|
||||
fugit = { version = "0.3.7" }
|
||||
lights-core = { path = "../core" }
|
||||
panic-halt = { version = "0.2.0" }
|
||||
rp-pico = { version = "0.8.0" }
|
|
@ -1,241 +0,0 @@
|
|||
#![no_main]
|
||||
#![no_std]
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
use alloc::boxed::Box;
|
||||
use az::*;
|
||||
use core::cell::RefCell;
|
||||
use cortex_m::delay::Delay;
|
||||
use embedded_alloc::Heap;
|
||||
use embedded_hal::{blocking::spi::Write, digital::v2::InputPin, digital::v2::OutputPin};
|
||||
use fixed::types::I16F16;
|
||||
use fugit::RateExtU32;
|
||||
use lights_core::{App, BodyPattern, DashboardPattern, Event, Instant, FPS, UI};
|
||||
use panic_halt as _;
|
||||
use rp_pico::{
|
||||
entry,
|
||||
hal::{
|
||||
clocks::init_clocks_and_plls,
|
||||
gpio::{FunctionSio, Pin, PinId, PullDown, PullUp, SioInput, SioOutput},
|
||||
pac::{CorePeripherals, Peripherals},
|
||||
spi::{Enabled, Spi, SpiDevice, ValidSpiPinout},
|
||||
watchdog::Watchdog,
|
||||
Clock, Sio,
|
||||
},
|
||||
Pins,
|
||||
};
|
||||
|
||||
#[global_allocator]
|
||||
static HEAP: Heap = Heap::empty();
|
||||
|
||||
const LIGHT_SCALE: I16F16 = I16F16::lit("256.0");
|
||||
const DASHBOARD_BRIGHTESS: u8 = 1;
|
||||
const BODY_BRIGHTNESS: u8 = 8;
|
||||
|
||||
struct DebouncedButton<P: PinId> {
|
||||
debounce: Instant,
|
||||
pin: Pin<P, FunctionSio<SioInput>, PullUp>,
|
||||
}
|
||||
|
||||
impl<P: PinId> DebouncedButton<P> {
|
||||
fn new(pin: Pin<P, FunctionSio<SioInput>, PullUp>) -> Self {
|
||||
Self {
|
||||
debounce: Instant((0 as u32).into()),
|
||||
pin,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_low(&self, time: Instant) -> bool {
|
||||
if time <= self.debounce {
|
||||
return false;
|
||||
}
|
||||
self.pin.is_low().unwrap_or(false)
|
||||
}
|
||||
|
||||
fn set_debounce(&mut self, time: Instant) {
|
||||
self.debounce = time + Instant((250 as u32).into());
|
||||
}
|
||||
}
|
||||
|
||||
struct BikeUI<
|
||||
D: SpiDevice,
|
||||
P: ValidSpiPinout<D>,
|
||||
LeftId: PinId,
|
||||
RightId: PinId,
|
||||
PreviousId: PinId,
|
||||
NextId: PinId,
|
||||
BrakeId: PinId,
|
||||
> {
|
||||
spi: RefCell<Spi<Enabled, D, P, 8>>,
|
||||
left_blinker_button: DebouncedButton<LeftId>,
|
||||
right_blinker_button: DebouncedButton<RightId>,
|
||||
previous_animation_button: DebouncedButton<PreviousId>,
|
||||
next_animation_button: DebouncedButton<NextId>,
|
||||
brake_sensor: Pin<BrakeId, FunctionSio<SioInput>, PullUp>,
|
||||
|
||||
brake_enabled: bool,
|
||||
}
|
||||
|
||||
impl<
|
||||
D: SpiDevice,
|
||||
P: ValidSpiPinout<D>,
|
||||
LeftId: PinId,
|
||||
RightId: PinId,
|
||||
PreviousId: PinId,
|
||||
NextId: PinId,
|
||||
BrakeId: PinId,
|
||||
> BikeUI<D, P, LeftId, RightId, PreviousId, NextId, BrakeId>
|
||||
{
|
||||
fn new(
|
||||
spi: Spi<Enabled, D, P, 8>,
|
||||
left_blinker_button: Pin<LeftId, FunctionSio<SioInput>, PullUp>,
|
||||
right_blinker_button: Pin<RightId, FunctionSio<SioInput>, PullUp>,
|
||||
previous_animation_button: Pin<PreviousId, FunctionSio<SioInput>, PullUp>,
|
||||
next_animation_button: Pin<NextId, FunctionSio<SioInput>, PullUp>,
|
||||
brake_sensor: Pin<BrakeId, FunctionSio<SioInput>, PullUp>,
|
||||
) -> Self {
|
||||
Self {
|
||||
spi: RefCell::new(spi),
|
||||
left_blinker_button: DebouncedButton::new(left_blinker_button),
|
||||
right_blinker_button: DebouncedButton::new(right_blinker_button),
|
||||
previous_animation_button: DebouncedButton::new(previous_animation_button),
|
||||
next_animation_button: DebouncedButton::new(next_animation_button),
|
||||
brake_sensor,
|
||||
|
||||
brake_enabled: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<
|
||||
D: SpiDevice,
|
||||
P: ValidSpiPinout<D>,
|
||||
LeftId: PinId,
|
||||
RightId: PinId,
|
||||
PreviousId: PinId,
|
||||
NextId: PinId,
|
||||
BrakeId: PinId,
|
||||
> UI for BikeUI<D, P, LeftId, RightId, PreviousId, NextId, BrakeId>
|
||||
{
|
||||
fn check_event(&mut self, current_time: Instant) -> Option<Event> {
|
||||
if self.brake_sensor.is_high().unwrap_or(true) && !self.brake_enabled {
|
||||
self.brake_enabled = true;
|
||||
Some(Event::Brake)
|
||||
} else if self.brake_sensor.is_low().unwrap_or(false) && self.brake_enabled {
|
||||
self.brake_enabled = false;
|
||||
Some(Event::BrakeRelease)
|
||||
} else if self.left_blinker_button.is_low(current_time) {
|
||||
self.left_blinker_button.set_debounce(current_time);
|
||||
Some(Event::LeftBlinker)
|
||||
} else if self.right_blinker_button.is_low(current_time) {
|
||||
self.right_blinker_button.set_debounce(current_time);
|
||||
Some(Event::RightBlinker)
|
||||
} else if self.previous_animation_button.is_low(current_time) {
|
||||
self.previous_animation_button.set_debounce(current_time);
|
||||
Some(Event::PreviousPattern)
|
||||
} else if self.next_animation_button.is_low(current_time) {
|
||||
self.next_animation_button.set_debounce(current_time);
|
||||
Some(Event::NextPattern)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn update_lights(&self, dashboard_lights: DashboardPattern, body_lights: BodyPattern) {
|
||||
let mut lights: [u8; 260] = [0; 260];
|
||||
lights[256] = 0xff;
|
||||
lights[257] = 0xff;
|
||||
lights[258] = 0xff;
|
||||
lights[259] = 0xff;
|
||||
for (idx, rgb) in dashboard_lights.iter().enumerate() {
|
||||
lights[(idx + 1) * 4 + 0] = 0xe0 + DASHBOARD_BRIGHTESS;
|
||||
lights[(idx + 1) * 4 + 1] = (I16F16::from(rgb.r) * LIGHT_SCALE).saturating_as();
|
||||
lights[(idx + 1) * 4 + 2] = (I16F16::from(rgb.b) * LIGHT_SCALE).saturating_as();
|
||||
lights[(idx + 1) * 4 + 3] = (I16F16::from(rgb.g) * LIGHT_SCALE).saturating_as();
|
||||
}
|
||||
for (idx, rgb) in body_lights.iter().enumerate() {
|
||||
lights[(idx + 4) * 4 + 0] = 0xe0 + BODY_BRIGHTNESS;
|
||||
lights[(idx + 4) * 4 + 1] = (I16F16::from(rgb.b) * LIGHT_SCALE).saturating_as();
|
||||
lights[(idx + 4) * 4 + 2] = (I16F16::from(rgb.g) * LIGHT_SCALE).saturating_as();
|
||||
lights[(idx + 4) * 4 + 3] = (I16F16::from(rgb.r) * LIGHT_SCALE).saturating_as();
|
||||
}
|
||||
let mut spi = self.spi.borrow_mut();
|
||||
spi.write(lights.as_slice());
|
||||
}
|
||||
}
|
||||
|
||||
#[entry]
|
||||
fn main() -> ! {
|
||||
{
|
||||
use core::mem::MaybeUninit;
|
||||
const HEAP_SIZE: usize = 8096;
|
||||
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
|
||||
unsafe { HEAP.init(HEAP_MEM.as_ptr() as usize, HEAP_SIZE) }
|
||||
}
|
||||
|
||||
let mut pac = Peripherals::take().unwrap();
|
||||
let core = CorePeripherals::take().unwrap();
|
||||
let sio = Sio::new(pac.SIO);
|
||||
let mut watchdog = Watchdog::new(pac.WATCHDOG);
|
||||
|
||||
let pins = Pins::new(
|
||||
pac.IO_BANK0,
|
||||
pac.PADS_BANK0,
|
||||
sio.gpio_bank0,
|
||||
&mut pac.RESETS,
|
||||
);
|
||||
|
||||
let clocks = init_clocks_and_plls(
|
||||
12_000_000u32,
|
||||
pac.XOSC,
|
||||
pac.CLOCKS,
|
||||
pac.PLL_SYS,
|
||||
pac.PLL_USB,
|
||||
&mut pac.RESETS,
|
||||
&mut watchdog,
|
||||
)
|
||||
.ok()
|
||||
.unwrap();
|
||||
|
||||
let mut delay = Delay::new(core.SYST, clocks.system_clock.freq().to_Hz());
|
||||
let mut spi_clk = pins.gpio10.into_function();
|
||||
let mut spi_sdo = pins.gpio11.into_function();
|
||||
let spi = Spi::<_, _, _, 8>::new(pac.SPI1, (spi_sdo, spi_clk));
|
||||
let mut spi = spi.init(
|
||||
&mut pac.RESETS,
|
||||
clocks.peripheral_clock.freq(),
|
||||
1_u32.MHz(),
|
||||
embedded_hal::spi::MODE_1,
|
||||
);
|
||||
|
||||
let left_blinker_button = pins.gpio16.into_pull_up_input();
|
||||
let right_blinker_button = pins.gpio17.into_pull_up_input();
|
||||
let previous_animation_button = pins.gpio27.into_pull_up_input();
|
||||
let next_animation_button = pins.gpio26.into_pull_up_input();
|
||||
let brake_sensor = pins.gpio18.into_pull_up_input();
|
||||
|
||||
let mut led_pin = pins.led.into_push_pull_output();
|
||||
|
||||
let ui = BikeUI::new(
|
||||
spi,
|
||||
left_blinker_button,
|
||||
right_blinker_button,
|
||||
previous_animation_button,
|
||||
next_animation_button,
|
||||
brake_sensor,
|
||||
);
|
||||
|
||||
let mut app = App::new(Box::new(ui));
|
||||
|
||||
led_pin.set_high();
|
||||
|
||||
let mut time = Instant::default();
|
||||
let delay_ms = 1000 / (FPS as u32);
|
||||
loop {
|
||||
app.tick(time);
|
||||
|
||||
delay.delay_ms(delay_ms);
|
||||
time = time + Instant(delay_ms.into());
|
||||
}
|
||||
}
|
|
@ -1,481 +0,0 @@
|
|||
#![no_std]
|
||||
|
||||
extern crate alloc;
|
||||
use alloc::boxed::Box;
|
||||
use az::*;
|
||||
use core::{
|
||||
clone::Clone,
|
||||
cmp::PartialEq,
|
||||
default::Default,
|
||||
ops::{Add, Sub},
|
||||
option::Option,
|
||||
};
|
||||
use fixed::types::{I48F16, I8F8, U128F0, U16F0};
|
||||
|
||||
mod patterns;
|
||||
pub use patterns::*;
|
||||
|
||||
mod types;
|
||||
pub use types::{BodyPattern, DashboardPattern, RGB};
|
||||
|
||||
fn calculate_frames(starting_time: U128F0, now: U128F0) -> U16F0 {
|
||||
let frames_128 = (now - starting_time) / U128F0::from(FPS);
|
||||
(frames_128 % U128F0::from(U16F0::MAX)).cast()
|
||||
}
|
||||
|
||||
fn calculate_slope(start: I8F8, end: I8F8, frames: U16F0) -> I8F8 {
|
||||
let slope_i16f16 = (I48F16::from(end) - I48F16::from(start)) / I48F16::from(frames);
|
||||
slope_i16f16.saturating_as()
|
||||
}
|
||||
|
||||
fn linear_ease(value: I8F8, frames: U16F0, slope: I8F8) -> I8F8 {
|
||||
let value_i16f16 = I48F16::from(value) + I48F16::from(frames) * I48F16::from(slope);
|
||||
value_i16f16.saturating_as()
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
|
||||
pub struct Instant(pub U128F0);
|
||||
|
||||
impl Default for Instant {
|
||||
fn default() -> Self {
|
||||
Self(U128F0::from(0 as u8))
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for Instant {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, r: Self) -> Self::Output {
|
||||
Self(self.0 + r.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for Instant {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, r: Self) -> Self::Output {
|
||||
Self(self.0 - r.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub const FPS: u8 = 30;
|
||||
|
||||
pub trait UI {
|
||||
fn check_event(&mut self, current_time: Instant) -> Option<Event>;
|
||||
fn update_lights(&self, dashboard_lights: DashboardPattern, body_lights: BodyPattern);
|
||||
}
|
||||
|
||||
pub trait Animation {
|
||||
fn tick(&mut self, time: Instant) -> (DashboardPattern, BodyPattern);
|
||||
}
|
||||
|
||||
/*
|
||||
pub struct DefaultAnimation {}
|
||||
|
||||
impl Animation for DefaultAnimation {
|
||||
fn tick(&mut self, _: Instant) -> (DashboardPattern, BodyPattern) {
|
||||
(WATER_DASHBOARD, WATER_BODY)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
pub struct Fade {
|
||||
starting_dashboard: DashboardPattern,
|
||||
starting_lights: BodyPattern,
|
||||
|
||||
start_time: Instant,
|
||||
dashboard_slope: [RGB<I8F8>; 3],
|
||||
body_slope: [RGB<I8F8>; 60],
|
||||
frames: U16F0,
|
||||
}
|
||||
|
||||
impl Fade {
|
||||
fn new(
|
||||
dashboard: DashboardPattern,
|
||||
lights: BodyPattern,
|
||||
ending_dashboard: DashboardPattern,
|
||||
ending_lights: BodyPattern,
|
||||
frames: U16F0,
|
||||
time: Instant,
|
||||
) -> Self {
|
||||
let mut dashboard_slope = [Default::default(); 3];
|
||||
let mut body_slope = [Default::default(); 60];
|
||||
for i in 0..3 {
|
||||
let slope = RGB {
|
||||
r: calculate_slope(dashboard[i].r, ending_dashboard[i].r, frames),
|
||||
g: calculate_slope(dashboard[i].g, ending_dashboard[i].g, frames),
|
||||
b: calculate_slope(dashboard[i].b, ending_dashboard[i].b, frames),
|
||||
};
|
||||
dashboard_slope[i] = slope;
|
||||
}
|
||||
|
||||
for i in 0..60 {
|
||||
let slope = RGB {
|
||||
r: calculate_slope(lights[i].r, ending_lights[i].r, frames),
|
||||
g: calculate_slope(lights[i].g, ending_lights[i].g, frames),
|
||||
b: calculate_slope(lights[i].b, ending_lights[i].b, frames),
|
||||
};
|
||||
body_slope[i] = slope;
|
||||
}
|
||||
|
||||
Self {
|
||||
starting_dashboard: dashboard,
|
||||
starting_lights: lights,
|
||||
start_time: time,
|
||||
dashboard_slope,
|
||||
body_slope,
|
||||
frames,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Animation for Fade {
|
||||
fn tick(&mut self, time: Instant) -> (DashboardPattern, BodyPattern) {
|
||||
let mut frames = calculate_frames(self.start_time.0, time.0);
|
||||
if frames > self.frames {
|
||||
frames = self.frames
|
||||
}
|
||||
let mut dashboard_pattern: DashboardPattern = OFF_DASHBOARD;
|
||||
let mut body_pattern: BodyPattern = OFF_BODY;
|
||||
|
||||
for i in 0..3 {
|
||||
dashboard_pattern[i].r = linear_ease(
|
||||
self.starting_dashboard[i].r,
|
||||
frames,
|
||||
self.dashboard_slope[i].r,
|
||||
);
|
||||
dashboard_pattern[i].g = linear_ease(
|
||||
self.starting_dashboard[i].g,
|
||||
frames,
|
||||
self.dashboard_slope[i].g,
|
||||
);
|
||||
dashboard_pattern[i].b = linear_ease(
|
||||
self.starting_dashboard[i].b,
|
||||
frames,
|
||||
self.dashboard_slope[i].b,
|
||||
);
|
||||
}
|
||||
|
||||
for i in 0..60 {
|
||||
body_pattern[i].r =
|
||||
linear_ease(self.starting_lights[i].r, frames, self.body_slope[i].r);
|
||||
body_pattern[i].g =
|
||||
linear_ease(self.starting_lights[i].g, frames, self.body_slope[i].g);
|
||||
body_pattern[i].b =
|
||||
linear_ease(self.starting_lights[i].b, frames, self.body_slope[i].b);
|
||||
}
|
||||
|
||||
(dashboard_pattern, body_pattern)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum FadeDirection {
|
||||
Transition,
|
||||
FadeIn,
|
||||
FadeOut,
|
||||
}
|
||||
|
||||
pub enum BlinkerDirection {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
pub struct Blinker {
|
||||
transition: Fade,
|
||||
fade_in: Fade,
|
||||
fade_out: Fade,
|
||||
direction: FadeDirection,
|
||||
|
||||
start_time: Instant,
|
||||
frames: U16F0,
|
||||
}
|
||||
|
||||
impl Blinker {
|
||||
fn new(
|
||||
starting_dashboard: DashboardPattern,
|
||||
starting_body: BodyPattern,
|
||||
direction: BlinkerDirection,
|
||||
time: Instant,
|
||||
) -> Self {
|
||||
let mut ending_dashboard = OFF_DASHBOARD.clone();
|
||||
|
||||
match direction {
|
||||
BlinkerDirection::Left => {
|
||||
ending_dashboard[0].r = LEFT_BLINKER_DASHBOARD[0].r;
|
||||
ending_dashboard[0].g = LEFT_BLINKER_DASHBOARD[0].g;
|
||||
ending_dashboard[0].b = LEFT_BLINKER_DASHBOARD[0].b;
|
||||
}
|
||||
BlinkerDirection::Right => {
|
||||
ending_dashboard[2].r = RIGHT_BLINKER_DASHBOARD[2].r;
|
||||
ending_dashboard[2].g = RIGHT_BLINKER_DASHBOARD[2].g;
|
||||
ending_dashboard[2].b = RIGHT_BLINKER_DASHBOARD[2].b;
|
||||
}
|
||||
}
|
||||
|
||||
let mut ending_body = OFF_BODY.clone();
|
||||
match direction {
|
||||
BlinkerDirection::Left => {
|
||||
for i in 0..30 {
|
||||
ending_body[i].r = LEFT_BLINKER_BODY[i].r;
|
||||
ending_body[i].g = LEFT_BLINKER_BODY[i].g;
|
||||
ending_body[i].b = LEFT_BLINKER_BODY[i].b;
|
||||
}
|
||||
}
|
||||
BlinkerDirection::Right => {
|
||||
for i in 30..60 {
|
||||
ending_body[i].r = RIGHT_BLINKER_BODY[i].r;
|
||||
ending_body[i].g = RIGHT_BLINKER_BODY[i].g;
|
||||
ending_body[i].b = RIGHT_BLINKER_BODY[i].b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Blinker {
|
||||
transition: Fade::new(
|
||||
starting_dashboard.clone(),
|
||||
starting_body.clone(),
|
||||
ending_dashboard.clone(),
|
||||
ending_body.clone(),
|
||||
BLINKER_FRAMES,
|
||||
time,
|
||||
),
|
||||
fade_in: Fade::new(
|
||||
OFF_DASHBOARD.clone(),
|
||||
OFF_BODY.clone(),
|
||||
ending_dashboard.clone(),
|
||||
ending_body.clone(),
|
||||
BLINKER_FRAMES,
|
||||
time,
|
||||
),
|
||||
fade_out: Fade::new(
|
||||
ending_dashboard.clone(),
|
||||
ending_body.clone(),
|
||||
OFF_DASHBOARD.clone(),
|
||||
OFF_BODY.clone(),
|
||||
BLINKER_FRAMES,
|
||||
time,
|
||||
),
|
||||
direction: FadeDirection::Transition,
|
||||
start_time: time,
|
||||
frames: BLINKER_FRAMES,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Animation for Blinker {
|
||||
fn tick(&mut self, time: Instant) -> (DashboardPattern, BodyPattern) {
|
||||
let frames = calculate_frames(self.start_time.0, time.0);
|
||||
if frames > self.frames {
|
||||
match self.direction {
|
||||
FadeDirection::Transition => {
|
||||
self.direction = FadeDirection::FadeOut;
|
||||
self.fade_out.start_time = time;
|
||||
}
|
||||
FadeDirection::FadeIn => {
|
||||
self.direction = FadeDirection::FadeOut;
|
||||
self.fade_out.start_time = time;
|
||||
}
|
||||
FadeDirection::FadeOut => {
|
||||
self.direction = FadeDirection::FadeIn;
|
||||
self.fade_in.start_time = time;
|
||||
}
|
||||
}
|
||||
self.start_time = time;
|
||||
}
|
||||
|
||||
match self.direction {
|
||||
FadeDirection::Transition => self.transition.tick(time),
|
||||
FadeDirection::FadeIn => self.fade_in.tick(time),
|
||||
FadeDirection::FadeOut => self.fade_out.tick(time),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Event {
|
||||
Brake,
|
||||
BrakeRelease,
|
||||
LeftBlinker,
|
||||
NextPattern,
|
||||
PreviousPattern,
|
||||
RightBlinker,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Pattern {
|
||||
Water,
|
||||
GayPride,
|
||||
TransPride,
|
||||
}
|
||||
|
||||
impl Pattern {
|
||||
fn previous(&self) -> Pattern {
|
||||
match self {
|
||||
Pattern::Water => Pattern::TransPride,
|
||||
Pattern::GayPride => Pattern::Water,
|
||||
Pattern::TransPride => Pattern::GayPride,
|
||||
}
|
||||
}
|
||||
|
||||
fn next(&self) -> Pattern {
|
||||
match self {
|
||||
Pattern::Water => Pattern::GayPride,
|
||||
Pattern::GayPride => Pattern::TransPride,
|
||||
Pattern::TransPride => Pattern::Water,
|
||||
}
|
||||
}
|
||||
|
||||
fn dashboard(&self) -> DashboardPattern {
|
||||
match self {
|
||||
Pattern::Water => WATER_DASHBOARD,
|
||||
Pattern::GayPride => PRIDE_DASHBOARD,
|
||||
Pattern::TransPride => TRANS_PRIDE_DASHBOARD,
|
||||
}
|
||||
}
|
||||
|
||||
fn body(&self) -> BodyPattern {
|
||||
match self {
|
||||
Pattern::Water => OFF_BODY,
|
||||
Pattern::GayPride => PRIDE_BODY,
|
||||
Pattern::TransPride => TRANS_PRIDE_BODY,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum State {
|
||||
Pattern(Pattern),
|
||||
Brake,
|
||||
LeftBlinker,
|
||||
RightBlinker,
|
||||
BrakeLeftBlinker,
|
||||
BrakeRightBlinker,
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
ui: Box<dyn UI>,
|
||||
state: State,
|
||||
home_pattern: Pattern,
|
||||
current_animation: Box<dyn Animation>,
|
||||
dashboard_lights: DashboardPattern,
|
||||
lights: BodyPattern,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(ui: Box<dyn UI>) -> Self {
|
||||
let pattern = Pattern::Water;
|
||||
Self {
|
||||
ui,
|
||||
state: State::Pattern(pattern),
|
||||
home_pattern: pattern,
|
||||
current_animation: Box::new(Fade::new(
|
||||
OFF_DASHBOARD,
|
||||
OFF_BODY,
|
||||
pattern.dashboard(),
|
||||
pattern.body(),
|
||||
DEFAULT_FRAMES,
|
||||
Instant((0 as u32).into()),
|
||||
)),
|
||||
dashboard_lights: OFF_DASHBOARD,
|
||||
lights: OFF_BODY,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_animation(&mut self, time: Instant) {
|
||||
match self.state {
|
||||
State::Pattern(ref pattern) => {
|
||||
self.current_animation = Box::new(Fade::new(
|
||||
self.dashboard_lights.clone(),
|
||||
self.lights.clone(),
|
||||
pattern.dashboard(),
|
||||
pattern.body(),
|
||||
DEFAULT_FRAMES,
|
||||
time,
|
||||
))
|
||||
}
|
||||
State::Brake => {
|
||||
self.current_animation = Box::new(Fade::new(
|
||||
self.dashboard_lights.clone(),
|
||||
self.lights.clone(),
|
||||
BRAKES_DASHBOARD,
|
||||
BRAKES_BODY,
|
||||
BRAKES_FRAMES,
|
||||
time,
|
||||
));
|
||||
}
|
||||
State::LeftBlinker => {
|
||||
self.current_animation = Box::new(Blinker::new(
|
||||
self.dashboard_lights.clone(),
|
||||
self.lights.clone(),
|
||||
BlinkerDirection::Left,
|
||||
time,
|
||||
));
|
||||
}
|
||||
State::RightBlinker => {
|
||||
self.current_animation = Box::new(Blinker::new(
|
||||
self.dashboard_lights.clone(),
|
||||
self.lights.clone(),
|
||||
BlinkerDirection::Right,
|
||||
time,
|
||||
));
|
||||
}
|
||||
State::BrakeLeftBlinker => (),
|
||||
State::BrakeRightBlinker => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn update_state(&mut self, event: Event) {
|
||||
match event {
|
||||
Event::Brake => {
|
||||
if self.state == State::Brake {
|
||||
self.state = State::Pattern(self.home_pattern);
|
||||
} else {
|
||||
self.state = State::Brake;
|
||||
}
|
||||
}
|
||||
Event::BrakeRelease => self.state = State::Pattern(self.home_pattern),
|
||||
Event::LeftBlinker => match self.state {
|
||||
State::Brake => self.state = State::BrakeLeftBlinker,
|
||||
State::BrakeLeftBlinker => self.state = State::Brake,
|
||||
State::LeftBlinker => self.state = State::Pattern(self.home_pattern),
|
||||
_ => self.state = State::LeftBlinker,
|
||||
},
|
||||
Event::NextPattern => match self.state {
|
||||
State::Pattern(ref pattern) => {
|
||||
self.home_pattern = pattern.next();
|
||||
self.state = State::Pattern(self.home_pattern);
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
Event::PreviousPattern => match self.state {
|
||||
State::Pattern(ref pattern) => {
|
||||
self.home_pattern = pattern.previous();
|
||||
self.state = State::Pattern(self.home_pattern);
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
Event::RightBlinker => match self.state {
|
||||
State::Brake => self.state = State::BrakeRightBlinker,
|
||||
State::BrakeRightBlinker => self.state = State::Brake,
|
||||
State::RightBlinker => self.state = State::Pattern(self.home_pattern),
|
||||
_ => self.state = State::RightBlinker,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tick(&mut self, time: Instant) {
|
||||
match self.ui.check_event(time) {
|
||||
Some(event) => {
|
||||
self.update_state(event);
|
||||
self.update_animation(time);
|
||||
}
|
||||
None => {}
|
||||
};
|
||||
|
||||
let (dashboard, lights) = self.current_animation.tick(time);
|
||||
self.dashboard_lights = dashboard.clone();
|
||||
self.lights = lights.clone();
|
||||
self.ui.update_lights(dashboard, lights);
|
||||
}
|
||||
}
|
|
@ -1,333 +0,0 @@
|
|||
use crate::{BodyPattern, DashboardPattern, RGB};
|
||||
use fixed::types::{I8F8, U16F0};
|
||||
|
||||
pub const RGB_OFF: RGB<I8F8> = RGB {
|
||||
r: I8F8::lit("0"),
|
||||
g: I8F8::lit("0"),
|
||||
b: I8F8::lit("0"),
|
||||
};
|
||||
|
||||
pub const RGB_WHITE: RGB<I8F8> = RGB {
|
||||
r: I8F8::lit("1"),
|
||||
g: I8F8::lit("1"),
|
||||
b: I8F8::lit("1"),
|
||||
};
|
||||
|
||||
pub const BRAKES_RED: RGB<I8F8> = RGB {
|
||||
r: I8F8::lit("1"),
|
||||
g: I8F8::lit("0"),
|
||||
b: I8F8::lit("0"),
|
||||
};
|
||||
|
||||
pub const BLINKER_AMBER: RGB<I8F8> = RGB {
|
||||
r: I8F8::lit("1"),
|
||||
g: I8F8::lit("0.15"),
|
||||
b: I8F8::lit("0"),
|
||||
};
|
||||
|
||||
pub const PRIDE_RED: RGB<I8F8> = RGB {
|
||||
r: I8F8::lit("0.95"),
|
||||
g: I8F8::lit("0.00"),
|
||||
b: I8F8::lit("0.00"),
|
||||
};
|
||||
|
||||
pub const PRIDE_ORANGE: RGB<I8F8> = RGB {
|
||||
r: I8F8::lit("1.0"),
|
||||
g: I8F8::lit("0.25"),
|
||||
b: I8F8::lit("0"),
|
||||
};
|
||||
|
||||
pub const PRIDE_YELLOW: RGB<I8F8> = RGB {
|
||||
r: I8F8::lit("1.0"),
|
||||
g: I8F8::lit("0.85"),
|
||||
b: I8F8::lit("0"),
|
||||
};
|
||||
|
||||
pub const PRIDE_GREEN: RGB<I8F8> = RGB {
|
||||
r: I8F8::lit("0"),
|
||||
g: I8F8::lit("0.95"),
|
||||
b: I8F8::lit("0.05"),
|
||||
};
|
||||
|
||||
pub const PRIDE_INDIGO: RGB<I8F8> = RGB {
|
||||
r: I8F8::lit("0.04"),
|
||||
g: I8F8::lit("0.15"),
|
||||
b: I8F8::lit("0.55"),
|
||||
};
|
||||
|
||||
pub const PRIDE_VIOLET: RGB<I8F8> = RGB {
|
||||
r: I8F8::lit("0.75"),
|
||||
g: I8F8::lit("0.0"),
|
||||
b: I8F8::lit("0.80"),
|
||||
};
|
||||
|
||||
pub const TRANS_BLUE: RGB<I8F8> = RGB {
|
||||
r: I8F8::lit("0.06"),
|
||||
g: I8F8::lit("0.41"),
|
||||
b: I8F8::lit("0.98"),
|
||||
};
|
||||
|
||||
pub const TRANS_PINK: RGB<I8F8> = RGB {
|
||||
r: I8F8::lit("0.96"),
|
||||
g: I8F8::lit("0.16"),
|
||||
b: I8F8::lit("0.32"),
|
||||
};
|
||||
|
||||
pub const WATER_1: RGB<I8F8> = RGB {
|
||||
r: I8F8::lit("0.0"),
|
||||
g: I8F8::lit("0.0"),
|
||||
b: I8F8::lit("0.75"),
|
||||
};
|
||||
|
||||
pub const WATER_2: RGB<I8F8> = RGB {
|
||||
r: I8F8::lit("0.8"),
|
||||
g: I8F8::lit("0.8"),
|
||||
b: I8F8::lit("0.8"),
|
||||
};
|
||||
|
||||
pub const WATER_3: RGB<I8F8> = RGB {
|
||||
r: I8F8::lit("0.00"),
|
||||
g: I8F8::lit("0.75"),
|
||||
b: I8F8::lit("0.75"),
|
||||
};
|
||||
|
||||
pub const OFF_DASHBOARD: DashboardPattern = [RGB_OFF; 3];
|
||||
pub const OFF_BODY: BodyPattern = [RGB_OFF; 60];
|
||||
|
||||
pub const DEFAULT_FRAMES: U16F0 = U16F0::lit("30");
|
||||
|
||||
pub const WATER_DASHBOARD: DashboardPattern = [WATER_1, WATER_2, WATER_3];
|
||||
|
||||
pub const WATER_BODY: BodyPattern = [RGB_OFF; 60];
|
||||
|
||||
pub const PRIDE_DASHBOARD: DashboardPattern = [PRIDE_RED, PRIDE_GREEN, PRIDE_INDIGO];
|
||||
|
||||
pub const PRIDE_BODY: BodyPattern = [
|
||||
// Left Side
|
||||
// Red
|
||||
PRIDE_RED,
|
||||
PRIDE_RED,
|
||||
PRIDE_RED,
|
||||
PRIDE_RED,
|
||||
PRIDE_RED,
|
||||
// Orange
|
||||
PRIDE_ORANGE,
|
||||
PRIDE_ORANGE,
|
||||
PRIDE_ORANGE,
|
||||
PRIDE_ORANGE,
|
||||
PRIDE_ORANGE,
|
||||
// Yellow
|
||||
PRIDE_YELLOW,
|
||||
PRIDE_YELLOW,
|
||||
PRIDE_YELLOW,
|
||||
PRIDE_YELLOW,
|
||||
PRIDE_YELLOW,
|
||||
// Green
|
||||
PRIDE_GREEN,
|
||||
PRIDE_GREEN,
|
||||
PRIDE_GREEN,
|
||||
PRIDE_GREEN,
|
||||
PRIDE_GREEN,
|
||||
// Indigo
|
||||
PRIDE_INDIGO,
|
||||
PRIDE_INDIGO,
|
||||
PRIDE_INDIGO,
|
||||
PRIDE_INDIGO,
|
||||
PRIDE_INDIGO,
|
||||
// Violet
|
||||
PRIDE_VIOLET,
|
||||
PRIDE_VIOLET,
|
||||
PRIDE_VIOLET,
|
||||
PRIDE_VIOLET,
|
||||
PRIDE_VIOLET,
|
||||
// Right Side
|
||||
// Violet
|
||||
PRIDE_VIOLET,
|
||||
PRIDE_VIOLET,
|
||||
PRIDE_VIOLET,
|
||||
PRIDE_VIOLET,
|
||||
PRIDE_VIOLET,
|
||||
// Indigo
|
||||
PRIDE_INDIGO,
|
||||
PRIDE_INDIGO,
|
||||
PRIDE_INDIGO,
|
||||
PRIDE_INDIGO,
|
||||
PRIDE_INDIGO,
|
||||
// Green
|
||||
PRIDE_GREEN,
|
||||
PRIDE_GREEN,
|
||||
PRIDE_GREEN,
|
||||
PRIDE_GREEN,
|
||||
PRIDE_GREEN,
|
||||
// Yellow
|
||||
PRIDE_YELLOW,
|
||||
PRIDE_YELLOW,
|
||||
PRIDE_YELLOW,
|
||||
PRIDE_YELLOW,
|
||||
PRIDE_YELLOW,
|
||||
// Orange
|
||||
PRIDE_ORANGE,
|
||||
PRIDE_ORANGE,
|
||||
PRIDE_ORANGE,
|
||||
PRIDE_ORANGE,
|
||||
PRIDE_ORANGE,
|
||||
// Red
|
||||
PRIDE_RED,
|
||||
PRIDE_RED,
|
||||
PRIDE_RED,
|
||||
PRIDE_RED,
|
||||
PRIDE_RED,
|
||||
];
|
||||
|
||||
pub const TRANS_PRIDE_DASHBOARD: DashboardPattern = [TRANS_BLUE, RGB_WHITE, TRANS_PINK];
|
||||
|
||||
pub const TRANS_PRIDE_BODY: BodyPattern = [
|
||||
// Left Side
|
||||
TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_PINK, TRANS_PINK,
|
||||
TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK, RGB_WHITE, RGB_WHITE, RGB_WHITE, RGB_WHITE,
|
||||
RGB_WHITE, RGB_WHITE, TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK,
|
||||
TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE,
|
||||
// Right side
|
||||
TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_PINK, TRANS_PINK,
|
||||
TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK, RGB_WHITE, RGB_WHITE, RGB_WHITE, RGB_WHITE,
|
||||
RGB_WHITE, RGB_WHITE, TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK,
|
||||
TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE,
|
||||
];
|
||||
|
||||
pub const BRAKES_FRAMES: U16F0 = U16F0::lit("15");
|
||||
|
||||
pub const BRAKES_DASHBOARD: DashboardPattern = [BRAKES_RED; 3];
|
||||
|
||||
pub const BRAKES_BODY: BodyPattern = [BRAKES_RED; 60];
|
||||
|
||||
pub const BLINKER_FRAMES: U16F0 = U16F0::lit("10");
|
||||
|
||||
pub const LEFT_BLINKER_DASHBOARD: DashboardPattern = [BLINKER_AMBER, RGB_OFF, RGB_OFF];
|
||||
|
||||
pub const LEFT_BLINKER_BODY: BodyPattern = [
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
];
|
||||
|
||||
pub const RIGHT_BLINKER_DASHBOARD: DashboardPattern = [RGB_OFF, RGB_OFF, BLINKER_AMBER];
|
||||
|
||||
pub const RIGHT_BLINKER_BODY: BodyPattern = [
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
RGB_OFF,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
BLINKER_AMBER,
|
||||
];
|
|
@ -1,17 +0,0 @@
|
|||
use core::default::Default;
|
||||
use fixed::types::I8F8;
|
||||
|
||||
#[derive(Clone, Copy, Default, Debug)]
|
||||
pub struct RGB<T> {
|
||||
pub r: T,
|
||||
pub g: T,
|
||||
pub b: T,
|
||||
}
|
||||
|
||||
const DASHBOARD_LIGHT_COUNT: usize = 3;
|
||||
|
||||
pub type DashboardPattern = [RGB<I8F8>; DASHBOARD_LIGHT_COUNT];
|
||||
|
||||
const BODY_LIGHT_COUNT: usize = 60;
|
||||
|
||||
pub type BodyPattern = [RGB<I8F8>; BODY_LIGHT_COUNT];
|
|
@ -1,16 +0,0 @@
|
|||
[package]
|
||||
name = "simulator"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
adw = { version = "0.5", package = "libadwaita", features = [ "v1_2" ] }
|
||||
cairo-rs = { version = "0.18" }
|
||||
fixed = { version = "1" }
|
||||
gio = { version = "0.18" }
|
||||
glib = { version = "0.18" }
|
||||
gtk = { version = "0.7", package = "gtk4", features = [ "v4_8" ] }
|
||||
lights-core = { path = "../core" }
|
||||
pango = { version = "*" }
|
|
@ -1,288 +0,0 @@
|
|||
use adw::prelude::*;
|
||||
use fixed::types::{I8F8, U128F0};
|
||||
use glib::{Object, Sender};
|
||||
use gtk::subclass::prelude::*;
|
||||
use lights_core::{
|
||||
App, BodyPattern, DashboardPattern, Event, Instant, FPS, OFF_BODY, OFF_DASHBOARD, RGB, UI,
|
||||
};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
env,
|
||||
rc::Rc,
|
||||
sync::mpsc::{Receiver, TryRecvError},
|
||||
};
|
||||
|
||||
const WIDTH: i32 = 640;
|
||||
const HEIGHT: i32 = 480;
|
||||
|
||||
pub struct Update {
|
||||
dashboard: DashboardPattern,
|
||||
lights: BodyPattern,
|
||||
}
|
||||
|
||||
pub struct DashboardLightsPrivate {
|
||||
lights: Rc<RefCell<DashboardPattern>>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for DashboardLightsPrivate {
|
||||
const NAME: &'static str = "DashboardLights";
|
||||
type Type = DashboardLights;
|
||||
type ParentType = gtk::DrawingArea;
|
||||
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
lights: Rc::new(RefCell::new(OFF_DASHBOARD)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for DashboardLightsPrivate {}
|
||||
impl WidgetImpl for DashboardLightsPrivate {}
|
||||
impl DrawingAreaImpl for DashboardLightsPrivate {}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct DashboardLights(ObjectSubclass<DashboardLightsPrivate>) @extends gtk::DrawingArea, gtk::Widget;
|
||||
}
|
||||
|
||||
impl DashboardLights {
|
||||
pub fn new() -> Self {
|
||||
let s: Self = Object::builder().build();
|
||||
|
||||
s.set_width_request(WIDTH);
|
||||
s.set_height_request(100);
|
||||
|
||||
s.set_draw_func({
|
||||
let s = s.clone();
|
||||
move |_, context, width, _| {
|
||||
let start = width as f64 / 2. - 150.;
|
||||
let lights = s.imp().lights.borrow();
|
||||
for i in 0..3 {
|
||||
context.set_source_rgb(
|
||||
lights[i].r.into(),
|
||||
lights[i].g.into(),
|
||||
lights[i].b.into(),
|
||||
);
|
||||
context.rectangle(start + 100. * i as f64, 10., 80., 80.);
|
||||
let _ = context.fill();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
s
|
||||
}
|
||||
|
||||
pub fn set_lights(&self, lights: DashboardPattern) {
|
||||
*self.imp().lights.borrow_mut() = lights;
|
||||
self.queue_draw();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BikeLightsPrivate {
|
||||
lights: Rc<RefCell<BodyPattern>>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for BikeLightsPrivate {
|
||||
const NAME: &'static str = "BikeLights";
|
||||
type Type = BikeLights;
|
||||
type ParentType = gtk::DrawingArea;
|
||||
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
lights: Rc::new(RefCell::new(OFF_BODY)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for BikeLightsPrivate {}
|
||||
impl WidgetImpl for BikeLightsPrivate {}
|
||||
impl DrawingAreaImpl for BikeLightsPrivate {}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct BikeLights(ObjectSubclass<BikeLightsPrivate>) @extends gtk::DrawingArea, gtk::Widget;
|
||||
}
|
||||
|
||||
impl BikeLights {
|
||||
pub fn new() -> Self {
|
||||
let s: Self = Object::builder().build();
|
||||
|
||||
s.set_width_request(WIDTH);
|
||||
s.set_height_request(640);
|
||||
|
||||
let center = WIDTH as f64 / 2.;
|
||||
|
||||
s.set_draw_func({
|
||||
let s = s.clone();
|
||||
move |_, context, _, _| {
|
||||
let lights = s.imp().lights.borrow();
|
||||
for i in 0..30 {
|
||||
context.set_source_rgb(
|
||||
lights[i].r.into(),
|
||||
lights[i].g.into(),
|
||||
lights[i].b.into(),
|
||||
);
|
||||
context.rectangle(center - 45., 5. + 20. * i as f64, 15., 15.);
|
||||
let _ = context.fill();
|
||||
}
|
||||
for i in 0..30 {
|
||||
context.set_source_rgb(
|
||||
lights[i + 30].r.into(),
|
||||
lights[i + 30].g.into(),
|
||||
lights[i + 30].b.into(),
|
||||
);
|
||||
context.rectangle(center + 15., 5. + 20. * (30. - (i + 1) as f64), 15., 15.);
|
||||
let _ = context.fill();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
s
|
||||
}
|
||||
|
||||
pub fn set_lights(&self, lights: [RGB<I8F8>; 60]) {
|
||||
*self.imp().lights.borrow_mut() = lights;
|
||||
self.queue_draw();
|
||||
}
|
||||
}
|
||||
|
||||
struct GTKUI {
|
||||
tx: Sender<Update>,
|
||||
rx: Receiver<Event>,
|
||||
}
|
||||
|
||||
impl UI for GTKUI {
|
||||
fn check_event(&mut self, _: Instant) -> Option<Event> {
|
||||
match self.rx.try_recv() {
|
||||
Ok(event) => Some(event),
|
||||
Err(TryRecvError::Empty) => None,
|
||||
Err(TryRecvError::Disconnected) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_lights(&self, dashboard_lights: DashboardPattern, lights: BodyPattern) {
|
||||
self.tx
|
||||
.send(Update {
|
||||
dashboard: dashboard_lights,
|
||||
lights,
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let adw_app = adw::Application::builder()
|
||||
.application_id("com.luminescent-dreams.bike-light-simulator")
|
||||
.build();
|
||||
|
||||
adw_app.connect_activate(move |adw_app| {
|
||||
let (update_tx, update_rx) =
|
||||
gtk::glib::MainContext::channel::<Update>(gtk::glib::Priority::DEFAULT);
|
||||
let (event_tx, event_rx) = std::sync::mpsc::channel();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let mut bike_app = App::new(Box::new(GTKUI {
|
||||
tx: update_tx,
|
||||
rx: event_rx,
|
||||
}));
|
||||
loop {
|
||||
bike_app.tick(Instant(U128F0::from(
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis(),
|
||||
)));
|
||||
std::thread::sleep(std::time::Duration::from_millis(1000 / (FPS as u64)));
|
||||
}
|
||||
});
|
||||
|
||||
let window = adw::ApplicationWindow::builder()
|
||||
.application(adw_app)
|
||||
.default_width(WIDTH)
|
||||
.default_height(HEIGHT)
|
||||
.build();
|
||||
let layout = gtk::Box::builder()
|
||||
.orientation(gtk::Orientation::Vertical)
|
||||
.build();
|
||||
let controls = gtk::Box::builder()
|
||||
.orientation(gtk::Orientation::Horizontal)
|
||||
.build();
|
||||
|
||||
let dashboard_lights = DashboardLights::new();
|
||||
let bike_lights = BikeLights::new();
|
||||
|
||||
let left_button = gtk::Button::builder().label("L").build();
|
||||
let brake_button = gtk::Button::builder().label("Brakes").build();
|
||||
let right_button = gtk::Button::builder().label("R").build();
|
||||
|
||||
left_button.connect_clicked({
|
||||
let event_tx = event_tx.clone();
|
||||
move |_| {
|
||||
let _ = event_tx.send(Event::LeftBlinker);
|
||||
}
|
||||
});
|
||||
|
||||
brake_button.connect_clicked({
|
||||
let event_tx = event_tx.clone();
|
||||
move |_| {
|
||||
let _ = event_tx.send(Event::Brake);
|
||||
}
|
||||
});
|
||||
|
||||
right_button.connect_clicked({
|
||||
let event_tx = event_tx.clone();
|
||||
move |_| {
|
||||
let _ = event_tx.send(Event::RightBlinker);
|
||||
}
|
||||
});
|
||||
|
||||
controls.append(&left_button);
|
||||
controls.append(&brake_button);
|
||||
controls.append(&right_button);
|
||||
layout.append(&controls);
|
||||
|
||||
let pattern_controls = gtk::Box::builder()
|
||||
.orientation(gtk::Orientation::Horizontal)
|
||||
.build();
|
||||
|
||||
let previous_pattern = gtk::Button::builder().label("Previous").build();
|
||||
let next_pattern = gtk::Button::builder().label("Next").build();
|
||||
|
||||
previous_pattern.connect_clicked({
|
||||
let event_tx = event_tx.clone();
|
||||
move |_| {
|
||||
let _ = event_tx.send(Event::PreviousPattern);
|
||||
}
|
||||
});
|
||||
|
||||
next_pattern.connect_clicked({
|
||||
let event_tx = event_tx.clone();
|
||||
move |_| {
|
||||
let _ = event_tx.send(Event::NextPattern);
|
||||
}
|
||||
});
|
||||
|
||||
pattern_controls.append(&previous_pattern);
|
||||
pattern_controls.append(&next_pattern);
|
||||
layout.append(&pattern_controls);
|
||||
|
||||
layout.append(&dashboard_lights);
|
||||
layout.append(&bike_lights);
|
||||
|
||||
update_rx.attach(None, {
|
||||
let dashboard_lights = dashboard_lights.clone();
|
||||
let bike_lights = bike_lights.clone();
|
||||
move |Update { dashboard, lights }| {
|
||||
dashboard_lights.set_lights(dashboard);
|
||||
bike_lights.set_lights(lights);
|
||||
glib::ControlFlow::Continue
|
||||
}
|
||||
});
|
||||
|
||||
window.set_content(Some(&layout));
|
||||
window.present();
|
||||
});
|
||||
|
||||
let args: Vec<String> = env::args().collect();
|
||||
ApplicationExtManual::run_with_args(&adw_app, &args);
|
||||
}
|
|
@ -45,7 +45,6 @@
|
|||
pkgs.udev
|
||||
pkgs.wasm-pack
|
||||
typeshare.packages."x86_64-linux".default
|
||||
pkgs.nodePackages_latest.typescript-language-server
|
||||
];
|
||||
LIBCLANG_PATH="${pkgs.llvmPackages.libclang.lib}/lib";
|
||||
ENV = "dev";
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
[package]
|
||||
name = "gm-dash"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
pipewire = "0.8.0"
|
||||
serde = { version = "1.0.209", features = ["alloc", "derive"] }
|
||||
serde_json = "1.0.127"
|
||||
tokio = { version = "1.39.3", features = ["full"] }
|
||||
warp = "0.3.7"
|
|
@ -1,25 +0,0 @@
|
|||
use pipewire::{context::Context, main_loop::MainLoop};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mainloop = MainLoop::new(None)?;
|
||||
let context = Context::new(&mainloop)?;
|
||||
let core = context.connect(None)?;
|
||||
let registry = core.get_registry()?;
|
||||
|
||||
let _listener = registry
|
||||
.add_listener_local()
|
||||
.global(|global| {
|
||||
if global.props.and_then(|p| p.get("media.class")) == Some("Audio/Sink"){
|
||||
println!(
|
||||
"\t{:?} {:?}",
|
||||
global.props.and_then(|p| p.get("node.description")),
|
||||
global.props.and_then(|p| p.get("media.class"))
|
||||
);
|
||||
}
|
||||
})
|
||||
.register();
|
||||
|
||||
mainloop.run();
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
use pipewire::{context::Context, main_loop::MainLoop};
|
||||
use std::{
|
||||
net::{Ipv6Addr, SocketAddrV6},
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
use tokio::task::spawn_blocking;
|
||||
use warp::{serve, Filter};
|
||||
|
||||
struct State_ {
|
||||
device_list: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct State {
|
||||
internal: Arc<RwLock<State_>>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn new() -> State {
|
||||
let internal = State_ {
|
||||
device_list: vec![],
|
||||
};
|
||||
State {
|
||||
internal: Arc::new(RwLock::new(internal)),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_audio(&self, device: String) {
|
||||
let mut st = self.internal.write().unwrap();
|
||||
(*st).device_list.push(device);
|
||||
}
|
||||
|
||||
fn audio_devices(&self) -> Vec<String> {
|
||||
let st = self.internal.read().unwrap();
|
||||
(*st).device_list.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for State {
|
||||
fn default() -> State {
|
||||
State::new()
|
||||
}
|
||||
}
|
||||
|
||||
async fn server_main(state: State) {
|
||||
let localhost: Ipv6Addr = "::1".parse().unwrap();
|
||||
let server_addr = SocketAddrV6::new(localhost, 3001, 0, 0);
|
||||
|
||||
let root = warp::path!().map(|| "ok".to_string());
|
||||
let list_output_devices = warp::path!("output_devices").map({
|
||||
let state = state.clone();
|
||||
move || {
|
||||
let devices = state.audio_devices();
|
||||
serde_json::to_string(&devices).unwrap()
|
||||
}
|
||||
});
|
||||
|
||||
let routes = root.or(list_output_devices);
|
||||
|
||||
serve(routes).run(server_addr).await;
|
||||
}
|
||||
|
||||
fn handle_add_audio_device(state: State, props: &pipewire::spa::utils::dict::DictRef)
|
||||
{
|
||||
if props.get("media.class") == Some("Audio/Sink") {
|
||||
if let Some(device_name) = props.get("node.description") {
|
||||
state.add_audio(device_name.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pipewire_loop(state: State) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mainloop = MainLoop::new(None)?;
|
||||
let context = Context::new(&mainloop)?;
|
||||
let core = context.connect(None)?;
|
||||
let registry = core.get_registry()?;
|
||||
|
||||
let _listener = registry
|
||||
.add_listener_local()
|
||||
.global({
|
||||
let state = state.clone();
|
||||
move |global_data| {
|
||||
if let Some(props) = global_data.props {
|
||||
handle_add_audio_device(state.clone(), props);
|
||||
}
|
||||
}
|
||||
})
|
||||
.register();
|
||||
|
||||
mainloop.run();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pipewire_main(state: State) {
|
||||
pipewire_loop(state).expect("pipewire should not error");
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let state = State::default();
|
||||
|
||||
spawn_blocking({
|
||||
let state = state.clone();
|
||||
move || pipewire_main(state)
|
||||
});
|
||||
|
||||
server_main(state.clone()).await;
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
|
@ -1,46 +0,0 @@
|
|||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
File diff suppressed because it is too large
Load Diff
|
@ -1,45 +0,0 @@
|
|||
{
|
||||
"name": "ui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.18.105",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"classnames": "^2.5.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.26.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"typescript": "^4.9.5",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 3.8 KiB |
|
@ -1,43 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
Binary file not shown.
Before Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 9.4 KiB |
|
@ -1,25 +0,0 @@
|
|||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
|
@ -1,5 +0,0 @@
|
|||
.layout {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import Dashboard from './Dashboard';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<Dashboard />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
|
@ -1,77 +0,0 @@
|
|||
import './Dashboard.css';
|
||||
import Card from './components/Card/Card';
|
||||
import Launcher from './components/Launcher/Launcher';
|
||||
import Launchpad from './components/Launchpad/Launchpad';
|
||||
|
||||
const LightThemes = () => <Card name="Light Themes">
|
||||
<Launchpad
|
||||
exclusive={true}
|
||||
options={[
|
||||
{ title: "Dark reds" },
|
||||
{ title: "Watery" },
|
||||
{ title: "Sunset" },
|
||||
{ title: "Darkness" },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
const LightSetup = () => <div> </div>
|
||||
|
||||
interface LightProps {
|
||||
name: string,
|
||||
}
|
||||
|
||||
const Light = ({ name }: LightProps) => <div> <p> {name} </p> </div>
|
||||
|
||||
const Tracks = () => <Card name="Tracks">
|
||||
<Launchpad
|
||||
exclusive={false}
|
||||
options={[
|
||||
{ title: "City BGM" },
|
||||
{ title: "Chat on the streets" },
|
||||
{ title: "Abandoned structure" },
|
||||
{ title: "Water dripping" },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
interface TrackProps {
|
||||
name: string,
|
||||
}
|
||||
const Track = ({ name }: TrackProps) => <Launcher title={name} />
|
||||
|
||||
const Presets = () => <Card name="Presets">
|
||||
<Launchpad
|
||||
exclusive={true}
|
||||
options={[
|
||||
{ title: "Gilcrest Falls day" },
|
||||
{ title: "Gilcrest Falls night" },
|
||||
{ title: "Empty colony" },
|
||||
{ title: "Surk colony" },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
interface PresetProps {
|
||||
name: string
|
||||
}
|
||||
|
||||
// const Scene = ({ name }: PresetProps) => <Launcher title={name} activated={false} />
|
||||
|
||||
const SceneEditor = () => <div> </div>
|
||||
|
||||
const Dashboard = () => {
|
||||
return (
|
||||
<div className="app">
|
||||
<div className="layout">
|
||||
<Presets />
|
||||
<div>
|
||||
<LightThemes />
|
||||
<Tracks />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Dashboard;
|
|
@ -1,78 +0,0 @@
|
|||
.palette {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.palette div {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 1px solid black;
|
||||
border-radius: 5px;
|
||||
margin: 1em;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.palette-purple div.item-1 {
|
||||
background-color: var(--purple-1);
|
||||
}
|
||||
|
||||
.palette-purple div.item-2 {
|
||||
background-color: var(--purple-2);
|
||||
}
|
||||
|
||||
.palette-purple div.item-3 {
|
||||
background-color: var(--purple-3);
|
||||
}
|
||||
|
||||
.palette-purple div.item-4 {
|
||||
background-color: var(--purple-4);
|
||||
}
|
||||
|
||||
.palette-purple div.item-5 {
|
||||
background-color: var(--purple-5);
|
||||
}
|
||||
|
||||
.palette-blue div.item-1 {
|
||||
background-color: var(--blue-1);
|
||||
}
|
||||
|
||||
.palette-blue div.item-2 {
|
||||
background-color: var(--blue-2);
|
||||
}
|
||||
|
||||
.palette-blue div.item-3 {
|
||||
background-color: var(--blue-3);
|
||||
}
|
||||
|
||||
.palette-blue div.item-4 {
|
||||
background-color: var(--blue-4);
|
||||
}
|
||||
|
||||
.palette-blue div.item-5 {
|
||||
background-color: var(--blue-5);
|
||||
}
|
||||
|
||||
.palette-grey div.item-1 {
|
||||
background-color: var(--grey-1);
|
||||
}
|
||||
|
||||
.palette-grey div.item-2 {
|
||||
background-color: var(--grey-2);
|
||||
}
|
||||
|
||||
.palette-grey div.item-3 {
|
||||
background-color: var(--grey-3);
|
||||
}
|
||||
|
||||
.palette-grey div.item-4 {
|
||||
background-color: var(--grey-4);
|
||||
}
|
||||
|
||||
.palette-grey div.item-5 {
|
||||
background-color: var(--grey-5);
|
||||
}
|
||||
|
||||
.design_horizontal {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
import './Design.css'
|
||||
import Launchpad from './components/Launchpad/Launchpad'
|
||||
|
||||
const PaletteGrey = () => <div className="palette palette-grey">
|
||||
<div className="item-1" />
|
||||
<div className="item-2" />
|
||||
<div className="item-3" />
|
||||
<div className="item-4" />
|
||||
<div className="item-5" />
|
||||
</div>
|
||||
|
||||
const PalettePurple = () => <div className="palette palette-purple">
|
||||
<div className="item-1" />
|
||||
<div className="item-2" />
|
||||
<div className="item-3" />
|
||||
<div className="item-4" />
|
||||
<div className="item-5" />
|
||||
</div>
|
||||
|
||||
const PaletteBlue = () => <div className="palette palette-blue">
|
||||
<div className="item-1" />
|
||||
<div className="item-2" />
|
||||
<div className="item-3" />
|
||||
<div className="item-4" />
|
||||
<div className="item-5" />
|
||||
</div>
|
||||
|
||||
const Launchpads = () => <div className="design_horizontal">
|
||||
<Launchpad
|
||||
exclusive={true}
|
||||
options={[
|
||||
{ title: "Grey" },
|
||||
{ title: "Purple" },
|
||||
{ title: "Blue" },
|
||||
]}
|
||||
/>
|
||||
|
||||
<Launchpad
|
||||
exclusive={false}
|
||||
flow={"vertical"}
|
||||
options={[
|
||||
{ title: "Grey" },
|
||||
{ title: "Purple" },
|
||||
{ title: "Blue" },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
const Design = () => <div>
|
||||
<PaletteGrey />
|
||||
<PalettePurple />
|
||||
<PaletteBlue />
|
||||
<Launchpads />
|
||||
</div>
|
||||
|
||||
|
||||
export default Design;
|
|
@ -1,15 +0,0 @@
|
|||
.card {
|
||||
border: 1px solid black;
|
||||
border-radius: 5px;
|
||||
margin: var(--spacer-l);
|
||||
box-shadow: 4px 4px 4px 0px var(--shadow-1),
|
||||
8px 8px 8px 0px var(--shadow-2);
|
||||
}
|
||||
|
||||
.card__title {
|
||||
color: var(--title-color);
|
||||
}
|
||||
|
||||
.card__body {
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import { PropsWithChildren } from 'react';
|
||||
import './Card.css';
|
||||
|
||||
interface CardProps {
|
||||
name: string,
|
||||
}
|
||||
|
||||
const Card = ({ name, children }: PropsWithChildren<CardProps>) => (
|
||||
<div className="card">
|
||||
<h1 className="card__title"> {name} </h1>
|
||||
<div className="card__body">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default Card;
|
|
@ -1,11 +0,0 @@
|
|||
.activator {
|
||||
border: 1px solid black;
|
||||
border-radius: 5px;
|
||||
margin: var(--spacer-m);
|
||||
padding: var(--spacer-s);
|
||||
box-shadow: var(--shadow-deep);
|
||||
}
|
||||
|
||||
.activator_enabled {
|
||||
box-shadow: var(--activator-ring);
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
import './Launcher.css';
|
||||
import React from 'react';
|
||||
|
||||
export interface LauncherProps {
|
||||
title: string,
|
||||
icon?: string,
|
||||
activated?: boolean,
|
||||
onSelected?: (key: string) => void,
|
||||
}
|
||||
|
||||
const Launcher = ({ title, activated = false, onSelected = (key) => {} }: LauncherProps) => {
|
||||
const classnames = activated ? "activator activator_enabled" : "activator";
|
||||
console.log("classnames ", activated, classnames);
|
||||
return (
|
||||
<div className={classnames} onClick={() => onSelected(title)}>
|
||||
<p> {title} </p>
|
||||
</div>)
|
||||
}
|
||||
|
||||
export default Launcher;
|
|
@ -1,18 +0,0 @@
|
|||
.launchpad {
|
||||
display: flex;
|
||||
border: var(--border);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow-depression);
|
||||
margin: var(--spacer-l);
|
||||
}
|
||||
|
||||
.launchpad_horizontal-flow {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.launchpad_vertical-flow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
import React from 'react';
|
||||
import './Launchpad.css';
|
||||
import Launcher, { LauncherProps } from '../Launcher/Launcher';
|
||||
import classnames from 'classnames';
|
||||
|
||||
export interface Selectable {
|
||||
onSelected?: (key: string) => void;
|
||||
}
|
||||
|
||||
export type Flow = "horizontal" | "vertical";
|
||||
|
||||
interface LaunchpadProps {
|
||||
exclusive: boolean;
|
||||
flow?: Flow;
|
||||
options: Array<LauncherProps>;
|
||||
}
|
||||
|
||||
const exclusiveSelect = (state: { [key: string]: boolean }, targetId: string) => {
|
||||
console.log("running exclusiveSelect on ", targetId);
|
||||
return { [targetId]: true };
|
||||
}
|
||||
|
||||
const multiSelect = (state: { [key: string]: boolean }, targetId: string) => {
|
||||
if (state[targetId]) {
|
||||
return { ...state, [targetId]: false };
|
||||
} else {
|
||||
return { ...state, [targetId]: true };
|
||||
}
|
||||
}
|
||||
|
||||
const Launchpad = ({ flow = "horizontal", options, exclusive }: LaunchpadProps) => {
|
||||
const [selected, dispatch] = React.useReducer(exclusive ? exclusiveSelect : multiSelect, {});
|
||||
|
||||
let classOptions = [ "launchpad" ];
|
||||
|
||||
if (flow === "horizontal") {
|
||||
classOptions.push("launchpad_horizontal-flow");
|
||||
} else {
|
||||
classOptions.push("launchpad_vertical-flow");
|
||||
}
|
||||
|
||||
let tiedOptions = options.map(option =>
|
||||
<Launcher key={option.title} title={option.title} onSelected={(key: string) => dispatch(key)} activated={selected[option.title]} />
|
||||
);
|
||||
|
||||
return (<div className={classnames(classOptions)}> {tiedOptions} </div>)
|
||||
}
|
||||
|
||||
export default Launchpad;
|
|
@ -1,50 +0,0 @@
|
|||
:root {
|
||||
--purple-1: hsl(265, 50%, 25%);
|
||||
--purple-2: hsl(265, 60%, 35%);
|
||||
--purple-3: hsl(265, 70%, 45%);
|
||||
--purple-4: hsl(265, 80%, 55%);
|
||||
--purple-5: hsl(265, 90%, 60%);
|
||||
|
||||
--blue-1: hsl(210, 50%, 25%);
|
||||
--blue-2: hsl(210, 60%, 35%);
|
||||
--blue-3: hsl(210, 70%, 45%);
|
||||
--blue-4: hsl(210, 80%, 55%);
|
||||
--blue-5: hsl(210, 90%, 65%);
|
||||
|
||||
--grey-1: hsl(210, 0%, 25%);
|
||||
--grey-2: hsl(210, 0%, 40%);
|
||||
--grey-3: hsl(210, 0%, 55%);
|
||||
--grey-4: hsl(210, 0%, 70%);
|
||||
--grey-5: hsl(210, 0%, 85%);
|
||||
|
||||
--title-color: var(--grey-1);
|
||||
|
||||
--border: 1px solid var(--purple-1);
|
||||
--activator-ring: 0px 0px 8px 4px var(--blue-4);
|
||||
--shadow-depression: inset 1px 1px 2px 0px var(--purple-1);
|
||||
--shadow-shallow: 1px 1px 2px 0px var(--purple-1);
|
||||
--shadow-deep: 2px 2px 4px 0px var(--purple-1),
|
||||
4px 4px 8px 0px var(--purple-2);
|
||||
|
||||
--border-radius: 8px;
|
||||
|
||||
--spacer-xs: 2px;
|
||||
--spacer-s: 4px;
|
||||
--spacer-m: 8px;
|
||||
--spacer-l: 12px;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: var(--grey-5);
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import Dashboard from './Dashboard';
|
||||
import Design from './Design';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <Dashboard />,
|
||||
},
|
||||
{
|
||||
path: "/design",
|
||||
element: <Design />,
|
||||
},
|
||||
]);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
Before Width: | Height: | Size: 2.6 KiB |
|
@ -1 +0,0 @@
|
|||
/// <reference types="react-scripts" />
|
|
@ -1,15 +0,0 @@
|
|||
import { ReportHandler } from 'web-vitals';
|
||||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
|
@ -1,5 +0,0 @@
|
|||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
|
@ -1,26 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
|
@ -1,10 +1,12 @@
|
|||
[package]
|
||||
name = "lights-core"
|
||||
name = "ray-tracer"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
az = { version = "1" }
|
||||
fixed = { version = "1" }
|
||||
rayon = "1.10.0"
|
||||
|
||||
[[bin]]
|
||||
name = "projectile"
|
|
@ -0,0 +1,53 @@
|
|||
use ray_tracer::{types::*, PPM};
|
||||
use std::{fs::File, io::Write};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Projectile {
|
||||
position: Point,
|
||||
velocity: Vector,
|
||||
}
|
||||
|
||||
struct Environment {
|
||||
gravity: Vector,
|
||||
wind: Vector,
|
||||
}
|
||||
|
||||
fn tick(env: &Environment, proj: &Projectile) -> Projectile {
|
||||
let position = proj.position + proj.velocity;
|
||||
let velocity = proj.velocity + env.gravity + env.wind;
|
||||
Projectile { position, velocity }
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut canvas = Canvas::new(900, 550);
|
||||
|
||||
let start = Projectile {
|
||||
position: Point::new(0., 1., 0.),
|
||||
velocity: Vector::new(1., 5., 0.).normalize() * 5.,
|
||||
};
|
||||
|
||||
let e = Environment {
|
||||
gravity: Vector::new(0., -0.1, 0.),
|
||||
wind: Vector::new(-0.01, 0., 0.),
|
||||
};
|
||||
|
||||
let mut p = start;
|
||||
while p.position.y() > 0. {
|
||||
p = tick(&e, &p);
|
||||
|
||||
let x = p.position.x().round() as usize;
|
||||
let y = p.position.y().round() as usize;
|
||||
if x > 1 && x < 900 && y > 1 && y < 550 {
|
||||
*canvas.pixel_mut(x, 550 - y - 1) = Color::new(1., 1., 0.);
|
||||
*canvas.pixel_mut(x+1, 550 - y - 1) = Color::new(1., 1., 0.);
|
||||
*canvas.pixel_mut(x-1, 550 - y - 1) = Color::new(1., 1., 0.);
|
||||
*canvas.pixel_mut(x, 550 - y) = Color::new(1., 1., 0.);
|
||||
*canvas.pixel_mut(x, 550 - y - 2) = Color::new(1., 1., 0.);
|
||||
}
|
||||
}
|
||||
|
||||
let ppm = PPM::from(&canvas);
|
||||
|
||||
let mut file = File::create("projectile.ppm").unwrap();
|
||||
let _ = file.write(ppm.as_bytes());
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
use ray_tracer::{types::*, PPM};
|
||||
use rayon::prelude::*;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::Write,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
const SIZE: usize = 1000;
|
||||
|
||||
const EPSILON: f64 = 0.00001;
|
||||
|
||||
fn eq_f64(l: f64, r: f64) -> bool {
|
||||
(l - r).abs() < EPSILON
|
||||
}
|
||||
|
||||
|
||||
fn render(
|
||||
camera: &Point,
|
||||
light: &PointLight,
|
||||
sphere: &Sphere,
|
||||
wall_z: f64,
|
||||
wall_size: f64,
|
||||
filename: &str,
|
||||
) {
|
||||
let canvas = Arc::new(RwLock::new(Canvas::new(SIZE, SIZE)));
|
||||
|
||||
let pixel_size = wall_size / SIZE as f64;
|
||||
let half = wall_size / 2.;
|
||||
|
||||
let ray = Ray::new(camera.clone(), (Point::new(0., 0., wall_z) - camera).normalize());
|
||||
let xs = ray.intersect(sphere);
|
||||
/*
|
||||
match xs.hit() {
|
||||
Some(hit) => {
|
||||
let point = ray.position(hit.t);
|
||||
let normal = hit.object.normal_at(&point);
|
||||
let eye = -ray.direction;
|
||||
let color = hit.object.material().lighting(&light, &point, &eye, &normal);
|
||||
println!("{:?}", color);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
*/
|
||||
|
||||
(0..SIZE).into_par_iter().for_each(|x| {
|
||||
(0..SIZE).into_par_iter().for_each(|y| {
|
||||
let world_x = -half + pixel_size * x as f64;
|
||||
let world_y = half - pixel_size * y as f64;
|
||||
let position = Point::new(world_x, world_y, wall_z);
|
||||
|
||||
let ray = Ray::new(camera.clone(), (position - camera).normalize());
|
||||
let xs = ray.intersect(sphere);
|
||||
match xs.hit() {
|
||||
Some(hit) => {
|
||||
let point = ray.position(hit.t);
|
||||
let normal = hit.object.normal_at(&point);
|
||||
let eye = -ray.direction;
|
||||
|
||||
let color = hit
|
||||
.object
|
||||
.material()
|
||||
.lighting(&light, &point, &eye, &normal);
|
||||
*canvas.write().unwrap().pixel_mut(x, y) = color;
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let ppm = PPM::from(&*canvas.read().unwrap());
|
||||
let mut file = File::create(filename).unwrap();
|
||||
let _ = file.write(ppm.as_bytes());
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut sphere = Sphere::default();
|
||||
|
||||
let mut material = Material::default();
|
||||
material.color = Color::new(1., 0.2, 1.);
|
||||
*sphere.material_mut() = material;
|
||||
|
||||
let camera = Point::new(0., 0., -5.);
|
||||
let wall_z = 10.;
|
||||
let wall_size = 7.;
|
||||
let light = PointLight::new(Point::new(-5., 5., -10.), Color::new(1., 1., 1.));
|
||||
|
||||
render(&camera, &light, &sphere, wall_z, wall_size, "sphere_1.ppm");
|
||||
/*
|
||||
{
|
||||
let light = PointLight::new(Point::new(0., 0., -10.), Color::new(1., 1., 1.));
|
||||
println!("Light at {:0.2} {:0.2} {:0.2}", light.position.x(), light.position.y(), light.position.z());
|
||||
render(&camera, &light, &sphere, wall_z, wall_size, "sphere_1.ppm");
|
||||
println!();
|
||||
}
|
||||
|
||||
{
|
||||
let light = PointLight::new(Point::new(-2., 0., -10.), Color::new(1., 1., 1.));
|
||||
println!("Light at {:0.2} {:0.2} {:0.2}", light.position.x(), light.position.y(), light.position.z());
|
||||
render(&camera, &light, &sphere, wall_z, wall_size, "sphere_2.ppm");
|
||||
println!();
|
||||
}
|
||||
|
||||
{
|
||||
let light = PointLight::new(Point::new(2., 0., -10.), Color::new(1., 1., 1.));
|
||||
println!("Light at {:0.2} {:0.2} {:0.2}", light.position.x(), light.position.y(), light.position.z());
|
||||
render(&camera, &light, &sphere, wall_z, wall_size, "sphere_3.ppm");
|
||||
println!();
|
||||
}
|
||||
|
||||
{
|
||||
let light = PointLight::new(Point::new(0., -2., -10.), Color::new(1., 1., 1.));
|
||||
println!("Light at {:0.2} {:0.2} {:0.2}", light.position.x(), light.position.y(), light.position.z());
|
||||
render(&camera, &light, &sphere, wall_z, wall_size, "sphere_4.ppm");
|
||||
println!();
|
||||
}
|
||||
|
||||
{
|
||||
let light = PointLight::new(Point::new(0., 2., -10.), Color::new(1., 1., 1.));
|
||||
println!("Light at {:0.2} {:0.2} {:0.2}", light.position.x(), light.position.y(), light.position.z());
|
||||
render(&camera, &light, &sphere, wall_z, wall_size, "sphere_5.ppm");
|
||||
println!();
|
||||
}
|
||||
*/
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
mod ppm;
|
||||
pub mod transforms;
|
||||
pub mod types;
|
||||
|
||||
pub use ppm::PPM;
|
|
@ -0,0 +1,118 @@
|
|||
use crate::types::Canvas;
|
||||
|
||||
fn color_float_to_int(val: f64) -> u8 {
|
||||
(val * 256.).clamp(0., 255.) as u8
|
||||
}
|
||||
|
||||
fn join_to_line_limit(data: impl IntoIterator<Item = String>) -> Vec<String> {
|
||||
let mut lines = vec![];
|
||||
|
||||
let mut line = String::new();
|
||||
for element in data {
|
||||
if line.is_empty() {
|
||||
line = line + &element;
|
||||
} else if line.len() + 1 + element.len() < 70 {
|
||||
line = line + " " + &element;
|
||||
} else {
|
||||
lines.push(line);
|
||||
line = element;
|
||||
}
|
||||
}
|
||||
|
||||
if !line.is_empty() {
|
||||
lines.push(line);
|
||||
}
|
||||
|
||||
lines
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PPM(String);
|
||||
|
||||
impl From<&Canvas> for PPM {
|
||||
fn from(c: &Canvas) -> Self {
|
||||
// let v = vec![0.; c.width() * c.height() * 3];
|
||||
let header = format!("P3\n{} {}\n255\n", c.width(), c.height());
|
||||
|
||||
let mut data = vec![];
|
||||
for y in 0..c.height() {
|
||||
let mut row = vec![];
|
||||
for x in 0..c.width() {
|
||||
let pixel = c.pixel(x, y);
|
||||
row.push(color_float_to_int(pixel.red()).to_string());
|
||||
row.push(color_float_to_int(pixel.green()).to_string());
|
||||
row.push(color_float_to_int(pixel.blue()).to_string());
|
||||
}
|
||||
let mut lines = join_to_line_limit(row);
|
||||
data.append(&mut lines);
|
||||
}
|
||||
|
||||
let data = data.join("\n");
|
||||
|
||||
Self(format!("{header}{data}\n"))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for PPM {
|
||||
type Target = String;
|
||||
fn deref(&self) -> &String {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::types::Color;
|
||||
|
||||
#[test]
|
||||
fn construct_ppm_header() {
|
||||
let c = Canvas::new(5, 3);
|
||||
let ppm = PPM::from(&c);
|
||||
|
||||
assert!(ppm.starts_with(
|
||||
"P3\n\
|
||||
5 3\n\
|
||||
255\n"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn construct_full_ppm_data() {
|
||||
let mut c = Canvas::new(5, 3);
|
||||
*c.pixel_mut(0, 0) = Color::new(1.5, 0., 0.);
|
||||
*c.pixel_mut(2, 1) = Color::new(0., 0.5, 0.);
|
||||
*c.pixel_mut(4, 2) = Color::new(-0.5, 0., 1.);
|
||||
|
||||
let expected = "P3\n\
|
||||
5 3\n\
|
||||
255\n\
|
||||
255 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n\
|
||||
0 0 0 0 0 0 0 128 0 0 0 0 0 0 0\n\
|
||||
0 0 0 0 0 0 0 0 0 0 0 0 0 0 255\n";
|
||||
|
||||
let ppm = PPM::from(&c);
|
||||
assert_eq!(*ppm, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ppm_line_length_limit() {
|
||||
let mut c = Canvas::new(10, 2);
|
||||
for y in 0..2 {
|
||||
for x in 0..10 {
|
||||
*c.pixel_mut(x, y) = Color::new(1., 0.8, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
let expected = "P3\n\
|
||||
10 2\n\
|
||||
255\n\
|
||||
255 204 153 255 204 153 255 204 153 255 204 153 255 204 153 255 204\n\
|
||||
153 255 204 153 255 204 153 255 204 153 255 204 153\n\
|
||||
255 204 153 255 204 153 255 204 153 255 204 153 255 204 153 255 204\n\
|
||||
153 255 204 153 255 204 153 255 204 153 255 204 153\n";
|
||||
|
||||
let ppm = PPM::from(&c);
|
||||
assert_eq!(*ppm, expected);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,219 @@
|
|||
use crate::types::Matrix;
|
||||
|
||||
pub fn translation(x: f64, y: f64, z: f64) -> Matrix {
|
||||
Matrix::from([
|
||||
[1., 0., 0., x],
|
||||
[0., 1., 0., y],
|
||||
[0., 0., 1., z],
|
||||
[0., 0., 0., 1.],
|
||||
])
|
||||
}
|
||||
|
||||
pub fn scaling(x: f64, y: f64, z: f64) -> Matrix {
|
||||
Matrix::from([
|
||||
[x, 0., 0., 0.],
|
||||
[0., y, 0., 0.],
|
||||
[0., 0., z, 0.],
|
||||
[0., 0., 0., 1.],
|
||||
])
|
||||
}
|
||||
|
||||
pub fn rotation_x(r: f64) -> Matrix {
|
||||
Matrix::from([
|
||||
[1., 0., 0., 0.],
|
||||
[0., r.cos(), -r.sin(), 0.],
|
||||
[0., r.sin(), r.cos(), 0.],
|
||||
[0., 0., 0., 1.],
|
||||
])
|
||||
}
|
||||
|
||||
pub fn rotation_y(r: f64) -> Matrix {
|
||||
Matrix::from([
|
||||
[r.cos(), 0., r.sin(), 0.],
|
||||
[0., 1., 0., 0.],
|
||||
[-r.sin(), 0., r.cos(), 0.],
|
||||
[0., 0., 0., 1.],
|
||||
])
|
||||
}
|
||||
|
||||
pub fn rotation_z(r: f64) -> Matrix {
|
||||
Matrix::from([
|
||||
[r.cos(), -r.sin(), 0., 0.],
|
||||
[r.sin(), r.cos(), 0., 0.],
|
||||
[0., 0., 1., 0.],
|
||||
[0., 0., 0., 1.],
|
||||
])
|
||||
}
|
||||
|
||||
pub fn shearing(
|
||||
x_by_y: f64,
|
||||
x_by_z: f64,
|
||||
y_by_x: f64,
|
||||
y_by_z: f64,
|
||||
z_by_x: f64,
|
||||
z_by_y: f64,
|
||||
) -> Matrix {
|
||||
Matrix::from([
|
||||
[1., x_by_y, x_by_z, 0.],
|
||||
[y_by_x, 1., y_by_z, 0.],
|
||||
[z_by_x, z_by_y, 1., 0.],
|
||||
[0., 0., 0., 1.],
|
||||
])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::types::{Point, Vector};
|
||||
use std::f64::consts::PI;
|
||||
|
||||
#[test]
|
||||
fn multiply_by_translation_matrix() {
|
||||
let tr = translation(5., -3., 2.);
|
||||
let p = Point::new(-3., 4., 5.);
|
||||
assert_eq!(&tr * p, Point::new(2., 1., 7.));
|
||||
|
||||
let inv = tr.inverse();
|
||||
assert_eq!(inv * p, Point::new(-8., 7., 3.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn translation_does_not_affect_vectors() {
|
||||
let tr = translation(5., -3., 2.);
|
||||
let v = Vector::new(-3., 4., 5.);
|
||||
assert_eq!(tr * v, v);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaling_matrix_applied_to_point() {
|
||||
let tr = scaling(2., 3., 4.);
|
||||
let p = Point::new(-4., 6., 8.);
|
||||
let v = Vector::new(-4., 6., 8.);
|
||||
|
||||
assert_eq!(&tr * p, Point::new(-8., 18., 32.));
|
||||
assert_eq!(tr * v, Vector::new(-8., 18., 32.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reflection_is_scaling_by_negative_value() {
|
||||
let tr = scaling(-1., 1., 1.);
|
||||
let p = Point::new(2., 3., 4.);
|
||||
assert_eq!(tr * p, Point::new(-2., 3., 4.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rotate_point_around_x() {
|
||||
let p = Point::new(0., 1., 0.);
|
||||
let half_quarter = rotation_x(PI / 4.);
|
||||
let full_quarter = rotation_x(PI / 2.);
|
||||
assert_eq!(
|
||||
half_quarter * p,
|
||||
Point::new(0., 2_f64.sqrt() / 2., 2_f64.sqrt() / 2.)
|
||||
);
|
||||
assert_eq!(full_quarter * p, Point::new(0., 0., 1.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inverse_rotates_opposite_direction() {
|
||||
let p = Point::new(0., 1., 0.);
|
||||
let half_quarter = rotation_x(PI / 4.);
|
||||
let inv = half_quarter.inverse();
|
||||
assert_eq!(
|
||||
inv * p,
|
||||
Point::new(0., 2_f64.sqrt() / 2., -2_f64.sqrt() / 2.)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rotate_around_y() {
|
||||
let p = Point::new(0., 0., 1.);
|
||||
let half_quarter = rotation_y(PI / 4.);
|
||||
let full_quarter = rotation_y(PI / 2.);
|
||||
assert_eq!(
|
||||
half_quarter * p,
|
||||
Point::new(2_f64.sqrt() / 2., 0., 2_f64.sqrt() / 2.)
|
||||
);
|
||||
assert_eq!(full_quarter * p, Point::new(1., 0., 0.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rotate_around_z() {
|
||||
let p = Point::new(0., 1., 0.);
|
||||
let half_quarter = rotation_z(PI / 4.);
|
||||
let full_quarter = rotation_z(PI / 2.);
|
||||
assert_eq!(
|
||||
half_quarter * p,
|
||||
Point::new(-2_f64.sqrt() / 2., 2_f64.sqrt() / 2., 0.)
|
||||
);
|
||||
assert_eq!(full_quarter * p, Point::new(-1., 0., 0.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shear_moves_x_proportionally_to_y() {
|
||||
let transform = shearing(1., 0., 0., 0., 0., 0.);
|
||||
let p = Point::new(2., 3., 4.);
|
||||
assert_eq!(transform * p, Point::new(5., 3., 4.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shear_moves_x_proportionally_to_z() {
|
||||
let transform = shearing(0., 1., 0., 0., 0., 0.);
|
||||
let p = Point::new(2., 3., 4.);
|
||||
assert_eq!(transform * p, Point::new(6., 3., 4.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shear_moves_y_proportionally_to_x() {
|
||||
let transform = shearing(0., 0., 1., 0., 0., 0.);
|
||||
let p = Point::new(2., 3., 4.);
|
||||
assert_eq!(transform * p, Point::new(2., 5., 4.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shear_moves_y_proportionally_to_z() {
|
||||
let transform = shearing(0., 0., 0., 1., 0., 0.);
|
||||
let p = Point::new(2., 3., 4.);
|
||||
assert_eq!(transform * p, Point::new(2., 7., 4.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shear_moves_z_proportionally_to_x() {
|
||||
let transform = shearing(0., 0., 0., 0., 1., 0.);
|
||||
let p = Point::new(2., 3., 4.);
|
||||
assert_eq!(transform * p, Point::new(2., 3., 6.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shear_moves_z_proportionally_to_y() {
|
||||
let transform = shearing(0., 0., 0., 0., 0., 1.);
|
||||
let p = Point::new(2., 3., 4.);
|
||||
assert_eq!(transform * p, Point::new(2., 3., 7.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn individual_transformations_are_applied_in_sequence() {
|
||||
let p = Point::new(1., 0., 1.);
|
||||
let rotation = rotation_x(PI / 2.);
|
||||
let scale = scaling(5., 5., 5.);
|
||||
let translate = translation(10., 5., 7.);
|
||||
|
||||
let p2 = rotation * p;
|
||||
assert_eq!(p2, Point::new(1., -1., 0.));
|
||||
|
||||
let p3 = scale * p2;
|
||||
assert_eq!(p3, Point::new(5., -5., 0.));
|
||||
|
||||
let p4 = translate * p3;
|
||||
assert_eq!(p4, Point::new(15., 0., 7.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chained_translations() {
|
||||
let p = Point::new(1., 0., 1.);
|
||||
let rotation = rotation_x(PI / 2.);
|
||||
let scale = scaling(5., 5., 5.);
|
||||
let translate = translation(10., 5., 7.);
|
||||
let tr = translate * scale * rotation;
|
||||
assert_eq!(tr * p, Point::new(15., 0., 7.));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
use crate::types::Color;
|
||||
|
||||
pub struct Canvas {
|
||||
width: usize,
|
||||
height: usize,
|
||||
pixels: Vec<Color>,
|
||||
}
|
||||
|
||||
impl Canvas {
|
||||
pub fn new(width: usize, height: usize) -> Self {
|
||||
Self {
|
||||
width,
|
||||
height,
|
||||
pixels: vec![Color::default(); width * height],
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn width(&self) -> usize {
|
||||
self.width
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn height(&self) -> usize {
|
||||
self.height
|
||||
}
|
||||
|
||||
pub fn pixel(&self, x: usize, y: usize) -> &Color {
|
||||
&self.pixels[self.addr(y, x)]
|
||||
}
|
||||
|
||||
pub fn pixel_mut(&mut self, x: usize, y: usize) -> &mut Color {
|
||||
let addr = self.addr(y, x);
|
||||
&mut self.pixels[addr]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn addr(&self, y: usize, x: usize) -> usize {
|
||||
let val = y * self.width() + x;
|
||||
if val >= self.pixels.len() {
|
||||
eprintln!("[{val}] out of range: [{x}, {y}]");
|
||||
}
|
||||
val
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn addresses() {
|
||||
let c = Canvas::new(5, 3);
|
||||
|
||||
assert_eq!(c.addr(0, 0), 0);
|
||||
assert_eq!(c.addr(1, 0), 5);
|
||||
assert_eq!(c.addr(2, 0), 10);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
use crate::types::Tuple;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Color(Tuple);
|
||||
|
||||
impl Color {
|
||||
pub fn new(red: f64, green: f64, blue: f64) -> Self {
|
||||
Self(Tuple(red, green, blue, 0.))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn red(&self) -> f64 {
|
||||
self.0.0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn green(&self) -> f64 {
|
||||
self.0.1
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn blue(&self) -> f64 {
|
||||
self.0.2
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Color {
|
||||
fn default() -> Self {
|
||||
Self::new(0., 0., 0.)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Tuple> for Color {
|
||||
fn from(tuple: Tuple) -> Self {
|
||||
assert_eq!(tuple.3, 0.0);
|
||||
Self(tuple)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for Color {
|
||||
type Target = Tuple;
|
||||
fn deref(&self) -> &Tuple {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Add for Color {
|
||||
type Output = Color;
|
||||
fn add(self, r: Self) -> Self {
|
||||
Color::from(self.0 + r.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Sub for Color {
|
||||
type Output = Color;
|
||||
fn sub(self, r: Self) -> Self {
|
||||
Color::from(self.0 - r.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Neg for Color {
|
||||
type Output = Color;
|
||||
fn neg(self) -> Self::Output {
|
||||
let mut t = -self.0;
|
||||
t.0 = 0.;
|
||||
Color::from(t)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul for Color {
|
||||
type Output = Color;
|
||||
fn mul(self, r: Color) -> Self {
|
||||
let red = self.red() * r.red();
|
||||
let green = self.green() * r.green();
|
||||
let blue = self.blue() * r.blue();
|
||||
Self::new(red, green, blue)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<f64> for Color {
|
||||
type Output = Color;
|
||||
fn mul(self, r: f64) -> Self {
|
||||
Color::from(self.0 * r)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
use std::cmp::Ordering;
|
||||
use super::Sphere;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Intersection<'a> {
|
||||
pub t: f64,
|
||||
pub object: &'a Sphere,
|
||||
}
|
||||
|
||||
pub struct Intersections<'a>(Vec<Intersection<'a>>);
|
||||
|
||||
impl<'a> Intersections<'a> {
|
||||
pub fn is_empty(&'a self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
pub fn len(&'a self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
pub fn hit(&'a self) -> Option<&Intersection<'a>> {
|
||||
self.0.iter().find(|i| i.t >= 0.)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> std::ops::Index<usize> for Intersections<'a> {
|
||||
type Output = Intersection<'a>;
|
||||
fn index(&self, idx: usize) -> &Intersection<'a> {
|
||||
&self.0[idx]
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Vec<Intersection<'a>>> for Intersections<'a> {
|
||||
fn from(mut v: Vec<Intersection<'a>>) -> Self {
|
||||
v.sort_by(|l, r| l.t.partial_cmp(&r.t).unwrap_or(Ordering::Equal));
|
||||
Self(v)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
use super::{Color, Point};
|
||||
|
||||
pub struct PointLight {
|
||||
pub position: Point,
|
||||
pub intensity: Color,
|
||||
}
|
||||
|
||||
impl PointLight {
|
||||
pub fn new(position: Point, intensity: Color) -> Self {
|
||||
Self { position, intensity }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
use super::{eq_f64, Color, Point, PointLight, Vector};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Material {
|
||||
pub color: Color,
|
||||
pub ambient: f64,
|
||||
pub diffuse: f64,
|
||||
pub specular: f64,
|
||||
pub shininess: f64,
|
||||
}
|
||||
|
||||
impl Default for Material {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
color: Color::new(1., 1., 1.),
|
||||
ambient: 0.1,
|
||||
diffuse: 0.9,
|
||||
specular: 0.9,
|
||||
shininess: 200.,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Material {
|
||||
pub fn lighting(
|
||||
&self,
|
||||
light: &PointLight,
|
||||
position: &Point,
|
||||
eye: &Vector,
|
||||
normal_v: &Vector,
|
||||
) -> Color {
|
||||
// let debug = eq_f64(position.x(), -0.09344)
|
||||
// || eq_f64(position.x(), 0.09344) && eq_f64(position.y(), 0.)
|
||||
// || eq_f64(position.y(), -0.09344)
|
||||
// || eq_f64(position.y(), 0.09344) && eq_f64(position.x(), 0.);
|
||||
// if debug {
|
||||
// println!(
|
||||
// "Debugging {:0.5} {:0.5} {:0.5}",
|
||||
// position.x(),
|
||||
// position.y(),
|
||||
// position.z()
|
||||
// );
|
||||
// }
|
||||
let effective_color = self.color * light.intensity;
|
||||
|
||||
let light_v = (light.position - position).normalize();
|
||||
let ambient = effective_color * self.ambient;
|
||||
|
||||
let light_dot_normal = light_v.dot(&normal_v);
|
||||
let (diffuse, specular) = if light_dot_normal < 0. {
|
||||
(Color::new(0., 0., 0.), Color::new(0., 0., 0.))
|
||||
} else {
|
||||
let diffuse = effective_color * self.diffuse * light_dot_normal;
|
||||
|
||||
let reflect_v = (-light_v).reflect(&normal_v);
|
||||
let reflect_dot_eye = reflect_v.dot(&eye);
|
||||
|
||||
let specular = if reflect_dot_eye <= 0. {
|
||||
Color::new(0., 0., 0.)
|
||||
} else {
|
||||
let factor = reflect_dot_eye.powf(self.shininess);
|
||||
light.intensity * self.specular * factor
|
||||
};
|
||||
|
||||
(diffuse, specular)
|
||||
};
|
||||
|
||||
ambient + diffuse + specular
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::types::{Color, Point, PointLight, Vector};
|
||||
|
||||
#[test]
|
||||
fn lighting_with_eye_between_light_and_surface() {
|
||||
let m = Material::default();
|
||||
let position = Point::default();
|
||||
|
||||
let eyev = Vector::new(0., 0., -1.);
|
||||
let normalv = Vector::new(0., 0., -1.);
|
||||
let light = PointLight::new(Point::new(0., 0., -10.), Color::new(1., 1., 1.));
|
||||
|
||||
assert_eq!(
|
||||
m.lighting(&light, &position, &eyev, &normalv),
|
||||
Color::new(1.9, 1.9, 1.9)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lighting_with_eye_between_light_and_surface_eye_offset_45() {
|
||||
let m = Material::default();
|
||||
let position = Point::default();
|
||||
|
||||
let eyev = Vector::new(0., 2_f64.sqrt() / 2., -2_f64.sqrt() / 2.);
|
||||
let normalv = Vector::new(0., 0., -1.);
|
||||
let light = PointLight::new(Point::new(0., 0., -10.), Color::new(1., 1., 1.));
|
||||
|
||||
assert_eq!(
|
||||
m.lighting(&light, &position, &eyev, &normalv),
|
||||
Color::new(1., 1., 1.)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lighting_with_eye_between_light_and_surface_light_offset_45() {
|
||||
let m = Material::default();
|
||||
let position = Point::default();
|
||||
|
||||
let eyev = Vector::new(0., 0., -1.);
|
||||
let normalv = Vector::new(0., 0., -1.);
|
||||
let light = PointLight::new(Point::new(0., 10., -10.), Color::new(1., 1., 1.));
|
||||
|
||||
assert_eq!(
|
||||
m.lighting(&light, &position, &eyev, &normalv),
|
||||
Color::new(0.7364, 0.7364, 0.7364)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lighting_eye_in_path_of_reflection() {
|
||||
let m = Material::default();
|
||||
let position = Point::default();
|
||||
|
||||
// let eyev = Vector::new(0., -2_f64.sqrt() / 2., -2_f64.sqrt() / 2.);
|
||||
let eyev = Vector::new(0., -10., -10.).normalize();
|
||||
let normalv = Vector::new(0., 0., -1.);
|
||||
let light = PointLight::new(Point::new(0., 10., -10.), Color::new(1., 1., 1.));
|
||||
|
||||
assert_eq!(
|
||||
m.lighting(&light, &position, &eyev, &normalv),
|
||||
Color::new(1.6364, 1.6364, 1.6364)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lighting_with_light_behind_surface() {
|
||||
let m = Material::default();
|
||||
let position = Point::default();
|
||||
|
||||
let eyev = Vector::new(0., 0., -1.);
|
||||
let normalv = Vector::new(0., 0., -1.);
|
||||
let light = PointLight::new(Point::new(0., 0., 10.), Color::new(1., 1., 1.));
|
||||
|
||||
assert_eq!(
|
||||
m.lighting(&light, &position, &eyev, &normalv),
|
||||
Color::new(0.1, 0.1, 0.1)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,544 @@
|
|||
use std::cmp::Ordering;
|
||||
|
||||
use crate::types::{eq_f64, Point, Tuple, Vector};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Matrix {
|
||||
size: usize,
|
||||
values: Vec<f64>,
|
||||
}
|
||||
|
||||
impl Matrix {
|
||||
pub fn new(size: usize) -> Self {
|
||||
Self {
|
||||
size,
|
||||
values: vec![0.; size * size],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn identity() -> Self {
|
||||
Self::from([
|
||||
[1., 0., 0., 0.],
|
||||
[0., 1., 0., 0.],
|
||||
[0., 0., 1., 0.],
|
||||
[0., 0., 0., 1.],
|
||||
])
|
||||
}
|
||||
|
||||
pub fn cell(&self, row: usize, column: usize) -> f64 {
|
||||
self.values[self.addr(row, column)]
|
||||
}
|
||||
|
||||
pub fn cell_mut(&mut self, row: usize, column: usize) -> &mut f64 {
|
||||
let addr = self.addr(row, column);
|
||||
&mut self.values[addr]
|
||||
}
|
||||
|
||||
pub fn transpose(&self) -> Self {
|
||||
let mut m = Self::new(self.size);
|
||||
|
||||
for row in 0..self.size {
|
||||
for column in 0..self.size {
|
||||
*m.cell_mut(row, column) = self.cell(column, row);
|
||||
}
|
||||
}
|
||||
|
||||
m
|
||||
}
|
||||
|
||||
pub fn is_invertible(&self) -> bool {
|
||||
!eq_f64(self.determinant(), 0.)
|
||||
}
|
||||
|
||||
pub fn inverse(&self) -> Self {
|
||||
let mut m = Matrix::new(self.size);
|
||||
let determinant = self.determinant();
|
||||
for row in 0..self.size {
|
||||
for column in 0..self.size {
|
||||
*m.cell_mut(row, column) = self.cofactor(row, column);
|
||||
}
|
||||
}
|
||||
|
||||
let mut m = m.transpose();
|
||||
for row in 0..self.size {
|
||||
for column in 0..self.size {
|
||||
let value = m.cell(row, column);
|
||||
*m.cell_mut(row, column) = value / determinant;
|
||||
}
|
||||
}
|
||||
|
||||
m
|
||||
}
|
||||
|
||||
fn determinant(&self) -> f64 {
|
||||
// TODO: optimization may not be necessary, but this can be optimized by memoizing
|
||||
// submatrices and cofactors.
|
||||
if self.size == 2 {
|
||||
self.cell(0, 0) * self.cell(1, 1) - self.cell(0, 1) * self.cell(1, 0)
|
||||
} else if self.size == 3 {
|
||||
self.cell(0, 0) * self.cofactor(0, 0)
|
||||
+ self.cell(0, 1) * self.cofactor(0, 1)
|
||||
+ self.cell(0, 2) * self.cofactor(0, 2)
|
||||
} else if self.size == 4 {
|
||||
self.cell(0, 0) * self.cofactor(0, 0)
|
||||
+ self.cell(0, 1) * self.cofactor(0, 1)
|
||||
+ self.cell(0, 2) * self.cofactor(0, 2)
|
||||
+ self.cell(0, 3) * self.cofactor(0, 3)
|
||||
} else {
|
||||
0.
|
||||
}
|
||||
}
|
||||
|
||||
fn submatrix(&self, row: usize, column: usize) -> Self {
|
||||
let mut m = Self::new(self.size - 1);
|
||||
|
||||
for r in 0..self.size {
|
||||
for c in 0..self.size {
|
||||
let dest_r = match r.cmp(&row) {
|
||||
Ordering::Less => r,
|
||||
Ordering::Greater => r - 1,
|
||||
Ordering::Equal => continue,
|
||||
};
|
||||
let dest_c = match c.cmp(&column) {
|
||||
Ordering::Less => c,
|
||||
Ordering::Greater => c - 1,
|
||||
Ordering::Equal => continue,
|
||||
};
|
||||
*m.cell_mut(dest_r, dest_c) = self.cell(r, c);
|
||||
}
|
||||
}
|
||||
|
||||
m
|
||||
}
|
||||
|
||||
fn minor(&self, row: usize, column: usize) -> f64 {
|
||||
self.submatrix(row, column).determinant()
|
||||
}
|
||||
|
||||
fn cofactor(&self, row: usize, column: usize) -> f64 {
|
||||
if (row + column) % 2 == 0 {
|
||||
self.minor(row, column)
|
||||
} else {
|
||||
-self.minor(row, column)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn addr(&self, row: usize, column: usize) -> usize {
|
||||
row * self.size + column
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[[f64; 2]; 2]> for Matrix {
|
||||
fn from(s: [[f64; 2]; 2]) -> Self {
|
||||
Self {
|
||||
size: 2,
|
||||
values: s.concat(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[[f64; 3]; 3]> for Matrix {
|
||||
fn from(s: [[f64; 3]; 3]) -> Self {
|
||||
Self {
|
||||
size: 3,
|
||||
values: s.concat(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[[f64; 4]; 4]> for Matrix {
|
||||
fn from(s: [[f64; 4]; 4]) -> Self {
|
||||
Self {
|
||||
size: 4,
|
||||
values: s.concat(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Matrix {
|
||||
fn eq(&self, rside: &Matrix) -> bool {
|
||||
if self.size != rside.size {
|
||||
return false;
|
||||
};
|
||||
|
||||
self.values
|
||||
.iter()
|
||||
.zip(rside.values.iter())
|
||||
.all(|(l, r)| eq_f64(*l, *r))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<&Matrix> for &Matrix {
|
||||
type Output = Matrix;
|
||||
fn mul(self, rside: &Matrix) -> Matrix {
|
||||
assert_eq!(self.size, 4);
|
||||
assert_eq!(rside.size, 4);
|
||||
|
||||
let mut m = Matrix::new(self.size);
|
||||
for row in 0..4 {
|
||||
for column in 0..4 {
|
||||
*m.cell_mut(row, column) = self.cell(row, 0) * rside.cell(0, column)
|
||||
+ self.cell(row, 1) * rside.cell(1, column)
|
||||
+ self.cell(row, 2) * rside.cell(2, column)
|
||||
+ self.cell(row, 3) * rside.cell(3, column);
|
||||
}
|
||||
}
|
||||
|
||||
m
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<Matrix> for &Matrix {
|
||||
type Output = Matrix;
|
||||
fn mul(self, rside: Matrix) -> Matrix {
|
||||
self * &rside
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<&Matrix> for Matrix {
|
||||
type Output = Matrix;
|
||||
fn mul(self, rside: &Matrix) -> Matrix {
|
||||
&self * rside
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul for Matrix {
|
||||
type Output = Matrix;
|
||||
fn mul(self, rside: Matrix) -> Matrix {
|
||||
&self * &rside
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<Tuple> for &Matrix {
|
||||
type Output = Tuple;
|
||||
fn mul(self, rside: Tuple) -> Tuple {
|
||||
assert_eq!(self.size, 4);
|
||||
|
||||
let mut t = [0.; 4];
|
||||
|
||||
for (row, item) in t.iter_mut().enumerate() {
|
||||
*item = self.cell(row, 0) * rside.0
|
||||
+ self.cell(row, 1) * rside.1
|
||||
+ self.cell(row, 2) * rside.2
|
||||
+ self.cell(row, 3) * rside.3;
|
||||
}
|
||||
|
||||
Tuple::from(t)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<Tuple> for Matrix {
|
||||
type Output = Tuple;
|
||||
fn mul(self, rside: Tuple) -> Tuple {
|
||||
&self * rside
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<&Point> for &Matrix {
|
||||
type Output = Point;
|
||||
fn mul(self, rside: &Point) -> Point {
|
||||
let t: Tuple = **rside;
|
||||
Point::from(self * t)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<Point> for &Matrix {
|
||||
type Output = Point;
|
||||
fn mul(self, rside: Point) -> Point {
|
||||
self * &rside
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<&Point> for Matrix {
|
||||
type Output = Point;
|
||||
fn mul(self, rside: &Point) -> Point {
|
||||
&self * rside
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<Point> for Matrix {
|
||||
type Output = Point;
|
||||
fn mul(self, rside: Point) -> Point {
|
||||
&self * rside
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<Vector> for Matrix {
|
||||
type Output = Vector;
|
||||
fn mul(self, rside: Vector) -> Vector {
|
||||
Vector::from(self * *rside)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_constructs_a_matrix() {
|
||||
let m = Matrix::from([[-3., 5.], [1., -2.]]);
|
||||
assert_eq!(m.cell(0, 0), -3.);
|
||||
assert_eq!(m.cell(0, 1), 5.);
|
||||
assert_eq!(m.cell(1, 0), 1.);
|
||||
assert_eq!(m.cell(1, 1), -2.);
|
||||
|
||||
let m = Matrix::from([[-3., 5., 0.], [1., -2., -7.], [0., 1., 1.]]);
|
||||
assert_eq!(m.cell(0, 0), -3.);
|
||||
assert_eq!(m.cell(1, 1), -2.);
|
||||
assert_eq!(m.cell(2, 2), 1.);
|
||||
|
||||
let m = Matrix::from([
|
||||
[1., 2., 3., 4.],
|
||||
[5.5, 6.5, 7.5, 8.5],
|
||||
[9., 10., 11., 12.],
|
||||
[13.5, 14.5, 15.5, 16.5],
|
||||
]);
|
||||
assert_eq!(m.cell(0, 0), 1.);
|
||||
assert_eq!(m.cell(0, 3), 4.);
|
||||
assert_eq!(m.cell(1, 0), 5.5);
|
||||
assert_eq!(m.cell(1, 2), 7.5);
|
||||
assert_eq!(m.cell(2, 2), 11.);
|
||||
assert_eq!(m.cell(3, 0), 13.5);
|
||||
assert_eq!(m.cell(3, 2), 15.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_compares_two_matrices() {
|
||||
let a = Matrix::from([
|
||||
[1., 2., 3., 4.],
|
||||
[5., 6., 7., 8.],
|
||||
[9., 8., 7., 6.],
|
||||
[5., 4., 3., 2.],
|
||||
]);
|
||||
let b = Matrix::from([
|
||||
[1., 2., 3., 4.],
|
||||
[5., 6., 7., 8.],
|
||||
[9., 8., 7., 6.],
|
||||
[5., 4., 3., 2.],
|
||||
]);
|
||||
let c = Matrix::from([
|
||||
[2., 3., 4., 5.],
|
||||
[6., 7., 8., 9.],
|
||||
[8., 7., 6., 5.],
|
||||
[4., 3., 2., 1.],
|
||||
]);
|
||||
|
||||
assert_eq!(a, b);
|
||||
assert_ne!(a, c);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiply_two_matrices() {
|
||||
let a = Matrix::from([
|
||||
[1., 2., 3., 4.],
|
||||
[5., 6., 7., 8.],
|
||||
[9., 8., 7., 6.],
|
||||
[5., 4., 3., 2.],
|
||||
]);
|
||||
|
||||
let b = Matrix::from([
|
||||
[-2., 1., 2., 3.],
|
||||
[3., 2., 1., -1.],
|
||||
[4., 3., 6., 5.],
|
||||
[1., 2., 7., 8.],
|
||||
]);
|
||||
|
||||
let expected = Matrix::from([
|
||||
[20., 22., 50., 48.],
|
||||
[44., 54., 114., 108.],
|
||||
[40., 58., 110., 102.],
|
||||
[16., 26., 46., 42.],
|
||||
]);
|
||||
|
||||
assert_eq!(a * b, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiply_matrix_by_tuple() {
|
||||
let a = Matrix::from([
|
||||
[1., 2., 3., 4.],
|
||||
[2., 4., 4., 2.],
|
||||
[8., 6., 4., 1.],
|
||||
[0., 0., 0., 1.],
|
||||
]);
|
||||
let b = Tuple(1., 2., 3., 1.);
|
||||
assert_eq!(a * b, Tuple(18., 24., 33., 1.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identity_matrix() {
|
||||
let a = Matrix::from([
|
||||
[0., 1., 2., 4.],
|
||||
[1., 2., 4., 8.],
|
||||
[2., 4., 8., 16.],
|
||||
[4., 8., 16., 32.],
|
||||
]);
|
||||
|
||||
assert_eq!(&a * Matrix::identity(), a);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transpose_a_matrix() {
|
||||
let a = Matrix::from([
|
||||
[0., 9., 3., 0.],
|
||||
[9., 8., 0., 8.],
|
||||
[1., 8., 5., 3.],
|
||||
[0., 0., 5., 8.],
|
||||
]);
|
||||
|
||||
let expected = Matrix::from([
|
||||
[0., 9., 1., 0.],
|
||||
[9., 8., 8., 0.],
|
||||
[3., 0., 5., 5.],
|
||||
[0., 8., 3., 8.],
|
||||
]);
|
||||
|
||||
assert_eq!(a.transpose(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calculates_2x2_determinant() {
|
||||
let m = Matrix::from([[1., 5.], [-3., 2.]]);
|
||||
assert_eq!(m.determinant(), 17.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calculates_3x3_determinant() {
|
||||
let m = Matrix::from([[1., 2., 6.], [-5., 8., -4.], [2., 6., 4.]]);
|
||||
assert_eq!(m.cofactor(0, 0), 56.);
|
||||
assert_eq!(m.cofactor(0, 1), 12.);
|
||||
assert_eq!(m.cofactor(0, 2), -46.);
|
||||
assert_eq!(m.determinant(), -196.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calculates_4x4_determinant() {
|
||||
let m = Matrix::from([
|
||||
[-2., -8., 3., 5.],
|
||||
[-3., 1., 7., 3.],
|
||||
[1., 2., -9., 6.],
|
||||
[-6., 7., 7., -9.],
|
||||
]);
|
||||
assert_eq!(m.cofactor(0, 0), 690.);
|
||||
assert_eq!(m.cofactor(0, 1), 447.);
|
||||
assert_eq!(m.cofactor(0, 2), 210.);
|
||||
assert_eq!(m.cofactor(0, 3), 51.);
|
||||
assert_eq!(m.determinant(), -4071.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calculates_submatrix() {
|
||||
let m = Matrix::from([[1., 5., 0.], [-3., 2., 7.], [0., 6., -3.]]);
|
||||
let expected = Matrix::from([[-3., 2.], [0., 6.]]);
|
||||
assert_eq!(m.submatrix(0, 2), expected);
|
||||
|
||||
let m = Matrix::from([
|
||||
[-6., 1., 1., 6.],
|
||||
[-8., 5., 8., 6.],
|
||||
[-1., 0., 8., 2.],
|
||||
[-7., 1., -1., 1.],
|
||||
]);
|
||||
let expected = Matrix::from([[-6., 1., 6.], [-8., 8., 6.], [-7., -1., 1.]]);
|
||||
assert_eq!(m.submatrix(2, 1), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calculates_minors_and_cofactors() {
|
||||
let m = Matrix::from([[3., 5., 0.], [2., -1., -7.], [6., -1., 5.]]);
|
||||
assert_eq!(m.submatrix(1, 0).determinant(), 25.);
|
||||
assert_eq!(m.minor(0, 0), -12.);
|
||||
assert_eq!(m.cofactor(0, 0), -12.);
|
||||
assert_eq!(m.minor(1, 0), 25.);
|
||||
assert_eq!(m.cofactor(1, 0), -25.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invert_4x4_matrix() {
|
||||
let m = Matrix::from([
|
||||
[6., 4., 4., 4.],
|
||||
[5., 5., 7., 6.],
|
||||
[4., -9., 3., -7.],
|
||||
[9., 1., 7., -6.],
|
||||
]);
|
||||
|
||||
assert_eq!(m.determinant(), -2120.);
|
||||
assert!(m.is_invertible());
|
||||
|
||||
let m = Matrix::from([
|
||||
[-4., 2., -2., -3.],
|
||||
[9., 6., 2., 6.],
|
||||
[0., -5., 1., -5.],
|
||||
[0., 0., 0., 0.],
|
||||
]);
|
||||
assert_eq!(m.determinant(), 0.);
|
||||
assert!(!m.is_invertible());
|
||||
|
||||
let m = Matrix::from([
|
||||
[-5., 2., 6., -8.],
|
||||
[1., -5., 1., 8.],
|
||||
[7., 7., -6., -7.],
|
||||
[1., -3., 7., 4.],
|
||||
]);
|
||||
let expected = Matrix::from([
|
||||
[0.21805, 0.45113, 0.24060, -0.04511],
|
||||
[-0.80827, -1.45677, -0.44361, 0.52068],
|
||||
[-0.07895, -0.22368, -0.05263, 0.19737],
|
||||
[-0.52256, -0.81391, -0.30075, 0.30639],
|
||||
]);
|
||||
|
||||
assert_eq!(m.determinant(), 532.);
|
||||
assert_eq!(m.cofactor(2, 3), -160.);
|
||||
assert!(eq_f64(expected.cell(3, 2), -160. / 532.));
|
||||
assert_eq!(m.cofactor(3, 2), 105.);
|
||||
assert!(eq_f64(expected.cell(2, 3), 105. / 532.));
|
||||
|
||||
assert_eq!(m.inverse(), expected);
|
||||
|
||||
let m = Matrix::from([
|
||||
[8., -5., 9., 2.],
|
||||
[7., 5., 6., 1.],
|
||||
[-6., 0., 9., 6.],
|
||||
[-3., 0., -9., -4.],
|
||||
]);
|
||||
let expected = Matrix::from([
|
||||
[-0.15385, -0.15385, -0.28205, -0.53846],
|
||||
[-0.07692, 0.12308, 0.02564, 0.03077],
|
||||
[0.35897, 0.35897, 0.43590, 0.92308],
|
||||
[-0.69231, -0.69231, -0.76923, -1.92308],
|
||||
]);
|
||||
assert_eq!(m.inverse(), expected);
|
||||
|
||||
let m = Matrix::from([
|
||||
[9., 3., 0., 9.],
|
||||
[-5., -2., -6., -3.],
|
||||
[-4., 9., 6., 4.],
|
||||
[-7., 6., 6., 2.],
|
||||
]);
|
||||
let expected = Matrix::from([
|
||||
[-0.04074, -0.07778, 0.14444, -0.22222],
|
||||
[-0.07778, 0.03333, 0.36667, -0.33333],
|
||||
[-0.02901, -0.14630, -0.10926, 0.12963],
|
||||
[0.17778, 0.06667, -0.26667, 0.33333],
|
||||
]);
|
||||
assert_eq!(m.inverse(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiply_product_by_inverse() {
|
||||
let a = Matrix::from([
|
||||
[3., -9., 7., 3.],
|
||||
[3., -8., 2., -9.],
|
||||
[-4., 4., 4., 1.],
|
||||
[-6., 5., -1., 1.],
|
||||
]);
|
||||
let b = Matrix::from([
|
||||
[8., 2., 2., 2.],
|
||||
[3., -1., 7., 0.],
|
||||
[7., 0., 5., 4.],
|
||||
[6., -2., 0., 5.],
|
||||
]);
|
||||
let c = &a * &b;
|
||||
assert_eq!(c * b.inverse(), a);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,185 @@
|
|||
mod canvas;
|
||||
mod color;
|
||||
mod intersections;
|
||||
mod light;
|
||||
mod material;
|
||||
mod matrix;
|
||||
mod point;
|
||||
mod ray;
|
||||
mod sphere;
|
||||
mod tuple;
|
||||
mod vector;
|
||||
|
||||
pub use canvas::Canvas;
|
||||
pub use color::Color;
|
||||
pub use intersections::{Intersections, Intersection};
|
||||
pub use light::PointLight;
|
||||
pub use material::Material;
|
||||
pub use matrix::Matrix;
|
||||
pub use point::Point;
|
||||
pub use ray::Ray;
|
||||
pub use sphere::Sphere;
|
||||
pub use tuple::Tuple;
|
||||
pub use vector::Vector;
|
||||
|
||||
const EPSILON: f64 = 0.00001;
|
||||
|
||||
fn eq_f64(l: f64, r: f64) -> bool {
|
||||
(l - r).abs() < EPSILON
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn eq_f64_compares_values() {
|
||||
assert!(eq_f64(1.0, 1.0));
|
||||
assert!(eq_f64(0.9994, 0.9994));
|
||||
assert!(eq_f64(0.9999994, 0.9999995));
|
||||
assert!(!eq_f64(0.9995, 0.9994));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_two_tuples() {
|
||||
let a = Tuple(3., -2., 5., 1.);
|
||||
let b = Tuple(-2., 3., 1., 0.);
|
||||
assert_eq!(a + b, Tuple(1., 1., 6., 1.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subtracts_two_tuples() {
|
||||
let a = Tuple(3., 2., 1., 1.);
|
||||
let b = Tuple(5., 6., 7., 1.);
|
||||
assert_eq!(a - b, Tuple(-2., -4., -6., 0.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adds_point_and_vector() {
|
||||
let a = Point::new(3., -2., 5.);
|
||||
let b = Vector::new(-2., 3., 1.);
|
||||
assert_eq!(a + b, Point::new(1., 1., 6.,));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subtracts_two_points() {
|
||||
let a = Point::new(3., 2., 1.);
|
||||
let b = Point::new(5., 6., 7.);
|
||||
assert_eq!(a - b, Vector::new(-2., -4., -6.,));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subtracts_vector_from_point() {
|
||||
let a = Point::new(3., 2., 1.);
|
||||
let b = Vector::new(5., 6., 7.);
|
||||
assert_eq!(a - b, Point::new(-2., -4., -6.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subtracts_two_vectors() {
|
||||
let a = Vector::new(3., 2., 1.);
|
||||
let b = Vector::new(5., 6., 7.);
|
||||
assert_eq!(a - b, Vector::new(-2., -4., -6.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_negates_primitives() {
|
||||
assert_eq!(-Tuple(1., 2., 3., 4.), Tuple(-1., -2., -3., -4.),);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiply_tuple_by_scalar() {
|
||||
assert_eq!(Tuple(1., -2., 3., -4.) * 3.5, Tuple(3.5, -7., 10.5, -14.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn divide_tuple_by_scalar() {
|
||||
assert_eq!(Tuple(1., -2., 3., -4.) / 2., Tuple(0.5, -1., 1.5, -2.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn magnitude_of_vector() {
|
||||
assert_eq!(Vector::new(1., 0., 0.).magnitude(), 1.);
|
||||
assert_eq!(Vector::new(0., 1., 0.).magnitude(), 1.);
|
||||
assert_eq!(Vector::new(0., 0., 1.).magnitude(), 1.);
|
||||
assert_eq!(Vector::new(1., 2., 3.).magnitude(), 14_f64.sqrt());
|
||||
assert_eq!(Vector::new(-1., -2., -3.).magnitude(), 14_f64.sqrt());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_vector() {
|
||||
assert_eq!(Vector::new(4., 0., 0.).normalize(), Vector::new(1., 0., 0.));
|
||||
assert_eq!(
|
||||
Vector::new(1., 2., 3.).normalize(),
|
||||
Vector::new(0.26726, 0.53452, 0.80178)
|
||||
);
|
||||
assert_eq!(Vector::new(1., 2., 3.).normalize().magnitude(), 1.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dot_product() {
|
||||
assert_eq!(Vector::new(1., 2., 3.).dot(&Vector::new(2., 3., 4.)), 20.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cross_product() {
|
||||
assert_eq!(
|
||||
Vector::new(1., 2., 3.).cross(&Vector::new(2., 3., 4.)),
|
||||
Vector::new(-1., 2., -1.)
|
||||
);
|
||||
assert_eq!(
|
||||
Vector::new(2., 3., 4.).cross(&Vector::new(1., 2., 3.)),
|
||||
Vector::new(1., -2., 1.)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiply_colors() {
|
||||
let c1 = Color::new(1., 0.2, 0.4);
|
||||
let c2 = Color::new(0.9, 1., 0.1);
|
||||
|
||||
assert_eq!(c1 * c2, Color::new(0.9, 0.2, 0.04));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_creates_a_canvas() {
|
||||
let c = Canvas::new(10, 20);
|
||||
assert_eq!(c.width(), 10);
|
||||
assert_eq!(c.height(), 20);
|
||||
for row in 0..20 {
|
||||
for col in 0..10 {
|
||||
assert_eq!(*c.pixel(row, col), Color::default());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_can_write_pixel() {
|
||||
let mut c = Canvas::new(10, 20);
|
||||
let red = Color::new(1., 0., 0.);
|
||||
*c.pixel_mut(2, 3) = red;
|
||||
assert_eq!(*c.pixel(2, 3), red);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reflect_vector_approaching_at_45() {
|
||||
let v = Vector::new(1., -1., 0.);
|
||||
let n = Vector::new(0., 1., 0.);
|
||||
assert_eq!(v.reflect(&n), Vector::new(1., 1., 0.));
|
||||
|
||||
let v = Vector::new(-1., -1., 0.);
|
||||
let n = Vector::new(1., 0., 0.);
|
||||
assert_eq!(v.reflect(&n), Vector::new(1., -1., 0.));
|
||||
|
||||
let v = Vector::new(1., 0., -1.);
|
||||
let n = Vector::new(0., 0., 1.);
|
||||
assert_eq!(v.reflect(&n), Vector::new(1., 0., 1.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reflect_off_slanted_surface() {
|
||||
let v = Vector::new(0., -1., 0.);
|
||||
let n = Vector::new(2_f64.sqrt() / 2., 2_f64.sqrt() / 2., 0.);
|
||||
assert_eq!(v.reflect(&n), Vector::new(1., 0., 0.));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
use crate::types::{Tuple, Vector};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Point(Tuple);
|
||||
|
||||
impl Point {
|
||||
pub fn new(x: f64, y: f64, z: f64) -> Self {
|
||||
Self(Tuple(x, y, z, 1.0))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn x(&self) -> f64 {
|
||||
self.0.0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn y(&self) -> f64 {
|
||||
self.0.1
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn z(&self) -> f64 {
|
||||
self.0.2
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Point {
|
||||
fn default() -> Self {
|
||||
Self::new(0., 0., 0.)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Tuple> for Point {
|
||||
fn from(tuple: Tuple) -> Self {
|
||||
assert_eq!(tuple.3, 1.0);
|
||||
Self(tuple)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for Point {
|
||||
type Target = Tuple;
|
||||
fn deref(&self) -> &Tuple {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Add<Vector> for Point {
|
||||
type Output = Point;
|
||||
fn add(self, r: Vector) -> Self::Output {
|
||||
Point::from(self.0 + *r)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Sub<&Point> for &Point {
|
||||
type Output = Vector;
|
||||
fn sub(self, r: &Point) -> Self::Output {
|
||||
Vector::from(self.0 - r.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Sub<Point> for &Point {
|
||||
type Output = Vector;
|
||||
fn sub(self, r: Point) -> Self::Output {
|
||||
self - &r
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Sub<&Point> for Point {
|
||||
type Output = Vector;
|
||||
fn sub(self, r: &Point) -> Self::Output {
|
||||
&self - r
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Sub<Point> for Point {
|
||||
type Output = Vector;
|
||||
fn sub(self, r: Point) -> Self::Output {
|
||||
&self - &r
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Sub<Vector> for Point {
|
||||
type Output = Point;
|
||||
fn sub(self, r: Vector) -> Self::Output {
|
||||
Point::from(self.0 - *r)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Neg for Point {
|
||||
type Output = Point;
|
||||
fn neg(self) -> Self::Output {
|
||||
let mut t = -self.0;
|
||||
t.3 = 1.;
|
||||
Point::from(t)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,191 @@
|
|||
use super::{Intersection, Intersections, Matrix, Point, Sphere, Vector};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Ray {
|
||||
origin: Point,
|
||||
pub direction: Vector,
|
||||
}
|
||||
|
||||
impl Ray {
|
||||
pub fn new(origin: Point, direction: Vector) -> Self {
|
||||
Self { origin, direction }
|
||||
}
|
||||
|
||||
pub fn position(&self, t: f64) -> Point {
|
||||
self.origin + self.direction * t
|
||||
}
|
||||
|
||||
pub fn intersect<'a>(&self, s: &'a Sphere) -> Intersections<'a> {
|
||||
let r2 = self.transform(s.transformation().inverse());
|
||||
let sphere_to_ray = r2.origin - Point::new(0., 0., 0.);
|
||||
let a = r2.direction.dot(&r2.direction);
|
||||
let b = 2. * r2.direction.dot(&sphere_to_ray);
|
||||
let c = sphere_to_ray.dot(&sphere_to_ray) - 1.;
|
||||
|
||||
let discriminant = b * b - 4. * a * c;
|
||||
if discriminant < 0. {
|
||||
return vec![].into();
|
||||
}
|
||||
|
||||
let t1 = (-b - discriminant.sqrt()) / (2. * a);
|
||||
let t2 = (-b + discriminant.sqrt()) / (2. * a);
|
||||
|
||||
vec![
|
||||
Intersection { t: t1, object: s },
|
||||
Intersection { t: t2, object: s },
|
||||
]
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn transform(&self, m: Matrix) -> Self {
|
||||
Self {
|
||||
origin: &m * self.origin,
|
||||
direction: m * self.direction,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::transforms::{scaling, translation};
|
||||
|
||||
#[test]
|
||||
fn computing_point_from_distance() {
|
||||
let r = Ray::new(Point::new(2., 3., 4.), Vector::new(1., 0., 0.));
|
||||
assert_eq!(r.position(0.), Point::new(2., 3., 4.));
|
||||
assert_eq!(r.position(1.), Point::new(3., 3., 4.));
|
||||
assert_eq!(r.position(-1.), Point::new(1., 3., 4.));
|
||||
assert_eq!(r.position(2.5), Point::new(4.5, 3., 4.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ray_intersects_sphere_at_two_points() {
|
||||
let r = Ray::new(Point::new(0., 0., -5.), Vector::new(0., 0., 1.));
|
||||
let s = Sphere::default();
|
||||
let xs = r.intersect(&s);
|
||||
assert_eq!(xs.len(), 2);
|
||||
assert_eq!(xs[0].t, 4.);
|
||||
assert_eq!(xs[1].t, 6.);
|
||||
assert_eq!(*xs[0].object, s);
|
||||
assert_eq!(*xs[1].object, s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ray_tangents_sphere_at_one_point() {
|
||||
let r = Ray::new(Point::new(0., 1., -5.), Vector::new(0., 0., 1.));
|
||||
let s = Sphere::default();
|
||||
let xs = r.intersect(&s);
|
||||
assert_eq!(xs.len(), 2);
|
||||
assert_eq!(xs[0].t, 5.);
|
||||
assert_eq!(xs[1].t, 5.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ray_misses_the_sphere() {
|
||||
let r = Ray::new(Point::new(0., 2., -5.), Vector::new(0., 0., 1.));
|
||||
let s = Sphere::default();
|
||||
let xs = r.intersect(&s);
|
||||
assert_eq!(xs.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ray_originates_inside_the_sphere() {
|
||||
let r = Ray::new(Point::new(0., 0., 0.), Vector::new(0., 0., 1.));
|
||||
let s = Sphere::default();
|
||||
let xs = r.intersect(&s);
|
||||
assert_eq!(xs.len(), 2);
|
||||
assert_eq!(xs[0].t, -1.);
|
||||
assert_eq!(xs[1].t, 1.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sphere_is_behind_the_ray() {
|
||||
let r = Ray::new(Point::new(0., 0., 5.), Vector::new(0., 0., 1.));
|
||||
let s = Sphere::default();
|
||||
let xs = r.intersect(&s);
|
||||
assert_eq!(xs.len(), 2);
|
||||
assert_eq!(xs[0].t, -6.);
|
||||
assert_eq!(xs[1].t, -4.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hit_all_intersections_are_positive() {
|
||||
let s = Sphere::default();
|
||||
let i1 = Intersection { t: 1., object: &s };
|
||||
let i2 = Intersection { t: 2., object: &s };
|
||||
let xs = Intersections::from(vec![i1.clone(), i2.clone()]);
|
||||
|
||||
assert_eq!(xs.hit(), Some(&i1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hit_some_intersections_are_negative() {
|
||||
let s = Sphere::default();
|
||||
let i1 = Intersection { t: -1., object: &s };
|
||||
let i2 = Intersection { t: 1., object: &s };
|
||||
let xs = Intersections::from(vec![i1, i2.clone()]);
|
||||
|
||||
assert_eq!(xs.hit(), Some(&i2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hit_all_intersections_are_negative() {
|
||||
let s = Sphere::default();
|
||||
let i1 = Intersection { t: -2., object: &s };
|
||||
let i2 = Intersection { t: -1., object: &s };
|
||||
let xs = Intersections::from(vec![i1, i2]);
|
||||
|
||||
assert_eq!(xs.hit(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hit_is_always_lowest_nonnegative() {
|
||||
let s = Sphere::default();
|
||||
let i1 = Intersection { t: 5., object: &s };
|
||||
let i2 = Intersection { t: 7., object: &s };
|
||||
let i3 = Intersection { t: -3., object: &s };
|
||||
let i4 = Intersection { t: 2., object: &s };
|
||||
let xs = Intersections::from(vec![i1, i2, i3, i4.clone()]);
|
||||
|
||||
assert_eq!(xs.hit(), Some(&i4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn translate_a_ray() {
|
||||
let r = Ray::new(Point::new(1., 2., 3.), Vector::new(0., 1., 0.));
|
||||
let m = translation(3., 4., 5.);
|
||||
let r2 = r.transform(m);
|
||||
assert_eq!(r2.origin, Point::new(4., 6., 8.));
|
||||
assert_eq!(r2.direction, Vector::new(0., 1., 0.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scale_a_ray() {
|
||||
let r = Ray::new(Point::new(1., 2., 3.), Vector::new(0., 1., 0.));
|
||||
let m = scaling(2., 3., 4.);
|
||||
let r2 = r.transform(m);
|
||||
assert_eq!(r2.origin, Point::new(2., 6., 12.));
|
||||
assert_eq!(r2.direction, Vector::new(0., 3., 0.,));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersect_scaled_sphere_with_ray() {
|
||||
let r = Ray::new(Point::new(0., 0., -5.), Vector::new(0., 0., 1.));
|
||||
let mut s = Sphere::default();
|
||||
*s.transformation_mut() = scaling(2., 2., 2.);
|
||||
let xs = r.intersect(&s);
|
||||
assert_eq!(xs.len(), 2);
|
||||
assert_eq!(xs[0].t, 3.);
|
||||
assert_eq!(xs[1].t, 7.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersect_translated_sphere_with_ray() {
|
||||
let r = Ray::new(Point::new(0., 0., -5.), Vector::new(0., 0., 1.));
|
||||
let mut s = Sphere::default();
|
||||
*s.transformation_mut() = translation(5., 0., 0.);
|
||||
let xs = r.intersect(&s);
|
||||
assert_eq!(xs.len(), 0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
use super::{Matrix, Point, Vector, Material };
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Sphere {
|
||||
origin: Point,
|
||||
transformation: Matrix,
|
||||
material: Material,
|
||||
}
|
||||
|
||||
impl Sphere {
|
||||
pub fn transformation(&self) -> &Matrix {
|
||||
&self.transformation
|
||||
}
|
||||
|
||||
pub fn transformation_mut(&mut self) -> &mut Matrix{
|
||||
&mut self.transformation
|
||||
}
|
||||
|
||||
pub fn material(&self) -> &Material {
|
||||
&self.material
|
||||
}
|
||||
|
||||
pub fn material_mut(&mut self) -> &mut Material {
|
||||
&mut self.material
|
||||
}
|
||||
|
||||
pub fn normal_at(&self, world_point: &Point) -> Vector {
|
||||
let inverted_transform = self.transformation.inverse();
|
||||
let object_point = &inverted_transform * world_point;
|
||||
let object_normal = (object_point - Point::new(0., 0., 0.)).normalize();
|
||||
let mut world_normal = inverted_transform.transpose() * *object_normal;
|
||||
world_normal.3 = 0.;
|
||||
Vector::from(world_normal).normalize()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Sphere {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
origin: Point::new(0., 0., 0.),
|
||||
transformation: Matrix::identity(),
|
||||
material: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::transforms::{rotation_z, scaling, translation};
|
||||
|
||||
#[test]
|
||||
fn sphere_has_default_transformation() {
|
||||
let s = Sphere::default();
|
||||
assert_eq!(s.transformation(), &Matrix::identity());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_a_spheres_transformation() {
|
||||
let mut s = Sphere::default();
|
||||
let t = translation(2., 3., 4.);
|
||||
*s.transformation_mut() = t.clone();
|
||||
assert_eq!(*s.transformation(), t);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_of_sphere_on_x() {
|
||||
let s = Sphere::default();
|
||||
let n = s.normal_at(&Point::new(1., 0., 0.));
|
||||
assert_eq!(n, Vector::new(1., 0., 0.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_of_sphere_on_y() {
|
||||
let s = Sphere::default();
|
||||
let n = s.normal_at(&Point::new(0., 1., 0.));
|
||||
assert_eq!(n, Vector::new(0., 1., 0.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_of_sphere_on_z() {
|
||||
let s = Sphere::default();
|
||||
let n = s.normal_at(&Point::new(0., 0., 1.));
|
||||
assert_eq!(n, Vector::new(0., 0., 1.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_of_sphere_on_nonaxial() {
|
||||
let s = Sphere::default();
|
||||
let n = s.normal_at(&Point::new(3_f64.sqrt() / 3., 3_f64.sqrt() / 3., 3_f64.sqrt() / 3.));
|
||||
assert_eq!(n, Vector::new(3_f64.sqrt() / 3., 3_f64.sqrt() / 3., 3_f64.sqrt() / 3.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_is_normalized() {
|
||||
let s = Sphere::default();
|
||||
let n = s.normal_at(&Point::new(3_f64.sqrt() / 3., 3_f64.sqrt() / 3., 3_f64.sqrt() / 3.));
|
||||
assert_eq!(n.normalize(), n);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_normal_of_translated_sphere() {
|
||||
let mut s = Sphere::default();
|
||||
*s.transformation_mut() = translation(0., 1., 0.);
|
||||
let n = s.normal_at(&Point::new(0., 1.70711, -0.70711));
|
||||
assert_eq!(n, Vector::new(0., 0.70711, -0.70711));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_normal_of_transformed_sphere() {
|
||||
let mut s = Sphere::default();
|
||||
*s.transformation_mut() = scaling(1., 0.5, 1.) * rotation_z(std::f64::consts::PI / 5.);
|
||||
let n = s.normal_at(&Point::new(0., 2_f64.sqrt() / 2., -2_f64.sqrt() / 2.));
|
||||
assert_eq!(n, Vector::new(0., 0.97014, -0.24254));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
use crate::types::eq_f64;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Tuple(
|
||||
pub f64, // x or red
|
||||
pub f64, // y or green
|
||||
pub f64, // z or blue
|
||||
pub f64, // w, the flag which
|
||||
// indicates point vs vec, or alpha
|
||||
);
|
||||
|
||||
impl Tuple {
|
||||
pub fn dot(&self, r: &Tuple) -> f64 {
|
||||
self.0 * r.0 + self.1 * r.1 + self.2 * r.2 + self.3 * r.3
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[f64; 4]> for Tuple {
|
||||
fn from(source: [f64; 4]) -> Self {
|
||||
Self(source[0], source[1], source[2], source[3])
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Tuple {
|
||||
fn eq(&self, r: &Tuple) -> bool {
|
||||
eq_f64(self.0, r.0) && eq_f64(self.1, r.1) && eq_f64(self.2, r.2) && eq_f64(self.3, r.3)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Add<&Tuple> for &Tuple {
|
||||
type Output = Tuple;
|
||||
fn add(self, r: &Tuple) -> Self::Output {
|
||||
Tuple(self.0 + r.0, self.1 + r.1, self.2 + r.2, self.3 + r.3)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Add<Tuple> for Tuple {
|
||||
type Output = Tuple;
|
||||
fn add(self, r: Tuple) -> Self::Output {
|
||||
&self + &r
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Sub<&Tuple> for &Tuple {
|
||||
type Output = Tuple;
|
||||
fn sub(self, r: &Tuple) -> Self::Output {
|
||||
Tuple(self.0 - r.0, self.1 - r.1, self.2 - r.2, self.3 - r.3)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Sub<Tuple> for Tuple {
|
||||
type Output = Tuple;
|
||||
fn sub(self, r: Tuple) -> Self::Output {
|
||||
&self - &r
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Neg for &Tuple {
|
||||
type Output = Tuple;
|
||||
fn neg(self) -> Self::Output {
|
||||
Tuple(-self.0, -self.1, -self.2, -self.3)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Neg for Tuple {
|
||||
type Output = Tuple;
|
||||
fn neg(self) -> Self::Output {
|
||||
-&self
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<f64> for &Tuple {
|
||||
type Output = Tuple;
|
||||
fn mul(self, scalar: f64) -> Self::Output {
|
||||
Tuple(
|
||||
self.0 * scalar,
|
||||
self.1 * scalar,
|
||||
self.2 * scalar,
|
||||
self.3 * scalar,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<f64> for Tuple {
|
||||
type Output = Tuple;
|
||||
#[allow(clippy::op_ref)]
|
||||
fn mul(self, scalar: f64) -> Self::Output {
|
||||
&self * scalar
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Div<f64> for &Tuple {
|
||||
type Output = Tuple;
|
||||
fn div(self, scalar: f64) -> Self::Output {
|
||||
Tuple(
|
||||
self.0 / scalar,
|
||||
self.1 / scalar,
|
||||
self.2 / scalar,
|
||||
self.3 / scalar,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Div<f64> for Tuple {
|
||||
type Output = Tuple;
|
||||
#[allow(clippy::op_ref)]
|
||||
fn div(self, scalar: f64) -> Self::Output {
|
||||
&self / scalar
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
use crate::types::Tuple;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Vector(Tuple);
|
||||
|
||||
impl Vector {
|
||||
pub fn new(x: f64, y: f64, z: f64) -> Self {
|
||||
Self(Tuple(x, y, z, 0.0))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn x(&self) -> f64 {
|
||||
self.0.0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn y(&self) -> f64 {
|
||||
self.0.1
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn z(&self) -> f64 {
|
||||
self.0.2
|
||||
}
|
||||
|
||||
pub fn magnitude(&self) -> f64 {
|
||||
(self.x() * self.x() + self.y() * self.y() + self.z() * self.z()).sqrt()
|
||||
}
|
||||
|
||||
pub fn normalize(&self) -> Self {
|
||||
let mag = self.magnitude();
|
||||
Self::new(self.x() / mag, self.y() / mag, self.z() / mag)
|
||||
}
|
||||
|
||||
pub fn dot(&self, r: &Vector) -> f64 {
|
||||
self.0.dot(r)
|
||||
}
|
||||
|
||||
pub fn cross(&self, r: &Vector) -> Self {
|
||||
let x = self.y() * r.z() - self.z() * r.y();
|
||||
let y = self.z() * r.x() - self.x() * r.z();
|
||||
let z = self.x() * r.y() - self.y() * r.x();
|
||||
Self::new(x, y, z)
|
||||
}
|
||||
|
||||
pub fn reflect(&self, n: &Vector) -> Self {
|
||||
self - (n * 2. * self.dot(n))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Vector {
|
||||
fn default() -> Self {
|
||||
Self::new(0., 0., 0.)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Tuple> for Vector {
|
||||
fn from(tuple: Tuple) -> Self {
|
||||
assert_eq!(tuple.3, 0.0);
|
||||
Self(tuple)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for Vector {
|
||||
type Target = Tuple;
|
||||
fn deref(&self) -> &Tuple {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Add for Vector {
|
||||
type Output = Vector;
|
||||
fn add(self, r: Self) -> Self::Output {
|
||||
Vector::from(self.0 + r.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Sub<&Vector> for &Vector {
|
||||
type Output = Vector;
|
||||
fn sub(self, r: &Vector) -> Self::Output {
|
||||
Vector::from(self.0 - r.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Sub<Vector> for &Vector {
|
||||
type Output = Vector;
|
||||
fn sub(self, r: Vector) -> Self::Output {
|
||||
self - &r
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Sub<&Vector> for Vector {
|
||||
type Output = Vector;
|
||||
fn sub(self, r: &Vector) -> Self::Output {
|
||||
&self - r
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Sub for Vector {
|
||||
type Output = Vector;
|
||||
fn sub(self, r: Vector) -> Self::Output {
|
||||
&self - &r
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Neg for Vector {
|
||||
type Output = Vector;
|
||||
fn neg(self) -> Self::Output {
|
||||
Vector::from(-self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<f64> for &Vector {
|
||||
type Output = Vector;
|
||||
fn mul(self, r: f64) -> Self::Output {
|
||||
Vector::from(self.0 * r)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<f64> for Vector {
|
||||
type Output = Vector;
|
||||
fn mul(self, r: f64) -> Self {
|
||||
Vector::from(self.0 * r)
|
||||
}
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
[toolchain]
|
||||
channel = "1.77.0"
|
||||
targets = [ "wasm32-unknown-unknown", "thumbv6m-none-eabi" ]
|
||||
targets = [ "wasm32-unknown-unknown" ]
|
||||
|
|
Loading…
Reference in New Issue