Compare commits
90 Commits
sgf/improv
...
main
Author | SHA1 | Date |
---|---|---|
Savanni D'Gerinel | 911bc97b69 | |
Savanni D'Gerinel | 019d9e7a6b | |
Savanni D'Gerinel | 8235ef0646 | |
Savanni D'Gerinel | dd861fbbd4 | |
Savanni D'Gerinel | 427c5d2a72 | |
Savanni D'Gerinel | 39391fb2fe | |
Savanni D'Gerinel | 99573ff7cf | |
Savanni D'Gerinel | 5ed39f814a | |
Savanni D'Gerinel | 82ec50f519 | |
Savanni D'Gerinel | 1601d2d806 | |
savanni | 3e297a5986 | |
Savanni D'Gerinel | b0383292fe | |
Savanni D'Gerinel | a0f037c9cd | |
Savanni D'Gerinel | 8e63e5210c | |
Savanni D'Gerinel | db34e69cdf | |
Savanni D'Gerinel | 20623284ed | |
Savanni D'Gerinel | 5d04c84437 | |
Savanni D'Gerinel | 6e26740a40 | |
Savanni D'Gerinel | a56c0d141c | |
Savanni D'Gerinel | 1bc146beaf | |
Savanni D'Gerinel | bb08064b9a | |
Savanni D'Gerinel | f226a83cf6 | |
Savanni D'Gerinel | fc70bb3955 | |
Savanni D'Gerinel | 7b50a71369 | |
Savanni D'Gerinel | 7a7548c78f | |
Savanni D'Gerinel | 9c56e988b2 | |
Savanni D'Gerinel | de35ebb644 | |
Savanni D'Gerinel | 791f2be3c5 | |
Savanni D'Gerinel | 74b7f1c6f7 | |
Savanni D'Gerinel | 9c490a84a4 | |
Savanni D'Gerinel | 724cc1a3f0 | |
Savanni D'Gerinel | 8f71760604 | |
Savanni D'Gerinel | 11abde345e | |
Savanni D'Gerinel | a5b76c8171 | |
Savanni D'Gerinel | 9b23dd5acd | |
Savanni D'Gerinel | 54225ca729 | |
Savanni D'Gerinel | 95b46de7fc | |
Savanni D'Gerinel | caaf9c57c6 | |
Savanni D'Gerinel | 81d452694d | |
Savanni D'Gerinel | 88cf32047b | |
Savanni D'Gerinel | 6cae7dbb0e | |
Savanni D'Gerinel | 80776c65d8 | |
Savanni D'Gerinel | 1c54e0832b | |
Savanni D'Gerinel | aee4528fb3 | |
Savanni D'Gerinel | 0535b6da5a | |
Savanni D'Gerinel | b55324feab | |
Savanni D'Gerinel | 50d8a9670e | |
Savanni D'Gerinel | 9cda35e766 | |
Savanni D'Gerinel | d0f461a5eb | |
Savanni D'Gerinel | 70c013218a | |
Savanni D'Gerinel | 37c7e04820 | |
Savanni D'Gerinel | 291663d4a3 | |
Savanni D'Gerinel | 2b0fc7639e | |
Savanni D'Gerinel | 80d8dedbaf | |
Savanni D'Gerinel | d7a70119c8 | |
Savanni D'Gerinel | 54c4b99ab6 | |
Savanni D'Gerinel | ef5415303b | |
Savanni D'Gerinel | 8d183d6d8c | |
Savanni D'Gerinel | 0b949111d2 | |
Savanni D'Gerinel | 6164cb3b39 | |
Savanni D'Gerinel | 22f0f9061c | |
Savanni D'Gerinel | 0bb5e62f96 | |
Savanni D'Gerinel | 06aedc34bb | |
Savanni D'Gerinel | 84b077e20c | |
Savanni D'Gerinel | fc2e88add2 | |
Savanni D'Gerinel | 15c4ae9bad | |
Savanni D'Gerinel | 7dd531b493 | |
Savanni D'Gerinel | cbfb3f2e37 | |
Savanni D'Gerinel | 9540a2c5bb | |
Savanni D'Gerinel | 6165d65977 | |
Savanni D'Gerinel | 4f8a1636c1 | |
Savanni D'Gerinel | 20b02fbd90 | |
Savanni D'Gerinel | 278ec27b4e | |
Savanni D'Gerinel | 8b7add37c1 | |
Savanni D'Gerinel | 5441a3c441 | |
Savanni D'Gerinel | b1374229f3 | |
Savanni D'Gerinel | bc5042c004 | |
Savanni D'Gerinel | 0534143d6b | |
Savanni D'Gerinel | d7f5269e15 | |
Savanni D'Gerinel | c913e9da37 | |
Savanni D'Gerinel | c50bd652f1 | |
Savanni D'Gerinel | 093e1f7f8a | |
Savanni D'Gerinel | 3c94f906a6 | |
Savanni D'Gerinel | 0aecaee760 | |
Savanni D'Gerinel | baeb458126 | |
Savanni D'Gerinel | da144a58ec | |
Savanni D'Gerinel | f09af67193 | |
Savanni D'Gerinel | 32391a46e7 | |
Savanni D'Gerinel | 0a62c96b0f | |
Savanni D'Gerinel | 78863ee709 |
|
@ -2,10 +2,15 @@
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"authdb",
|
"authdb",
|
||||||
|
# "bike-lights/bike",
|
||||||
|
"bike-lights/core",
|
||||||
|
"bike-lights/simulator",
|
||||||
"changeset",
|
"changeset",
|
||||||
"config",
|
"config",
|
||||||
"config-derive",
|
"config-derive",
|
||||||
"coordinates",
|
"coordinates",
|
||||||
|
"cyberpunk",
|
||||||
|
"cyber-slides",
|
||||||
"cyberpunk-splash",
|
"cyberpunk-splash",
|
||||||
"dashboard",
|
"dashboard",
|
||||||
"emseries",
|
"emseries",
|
||||||
|
@ -27,5 +32,5 @@ members = [
|
||||||
"sgf",
|
"sgf",
|
||||||
"timezone-testing",
|
"timezone-testing",
|
||||||
"tree",
|
"tree",
|
||||||
"visions/server",
|
"visions/server", "gm-dash/server", "halloween-leds"
|
||||||
]
|
]
|
||||||
|
|
|
@ -19,6 +19,8 @@ clap = { version = "4", features = [ "derive" ] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
sha2 = { version = "0.10" }
|
sha2 = { version = "0.10" }
|
||||||
sqlx = { version = "0.7", features = [ "runtime-tokio", "sqlite" ] }
|
sqlx = { version = "0.7", features = [ "runtime-tokio", "sqlite" ] }
|
||||||
|
# sqlformat introduced a mistaken breaking change in 0.2.7
|
||||||
|
sqlformat = { version = "=0.2.6" }
|
||||||
thiserror = { version = "1" }
|
thiserror = { version = "1" }
|
||||||
tokio = { version = "1", features = [ "full" ] }
|
tokio = { version = "1", features = [ "full" ] }
|
||||||
uuid = { version = "0.4", features = [ "serde", "v4" ] }
|
uuid = { version = "0.4", features = [ "serde", "v4" ] }
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
[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"
|
|
@ -0,0 +1,18 @@
|
||||||
|
[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" }
|
|
@ -0,0 +1,244 @@
|
||||||
|
#![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) {
|
||||||
|
*/
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,158 @@
|
||||||
|
$fn = 50;
|
||||||
|
threshold = 0.1;
|
||||||
|
half_threshold = threshold / 2;
|
||||||
|
bevel = 0.5;
|
||||||
|
|
||||||
|
wire_radius = 1;
|
||||||
|
|
||||||
|
wall_thickness = 2;
|
||||||
|
cutout_threshold = 1;
|
||||||
|
|
||||||
|
battery_length = 71;
|
||||||
|
battery_width = 18.75;
|
||||||
|
|
||||||
|
cell_holder_length = battery_length + wall_thickness * 2;
|
||||||
|
cell_holder_width = battery_width + wall_thickness * 2;
|
||||||
|
cell_holder_height = battery_width + wall_thickness;
|
||||||
|
|
||||||
|
battery_contact_thickness = .6;
|
||||||
|
// battery_contact_thickness = 1;
|
||||||
|
battery_contact_width = 11;
|
||||||
|
battery_contact_length = 12.8;
|
||||||
|
battery_contact_spring_height = 10.5;
|
||||||
|
battery_contact_flange_height = 1.9;
|
||||||
|
|
||||||
|
converter_width = 11.25;
|
||||||
|
converter_length = 22.25;
|
||||||
|
converter_height = 5;
|
||||||
|
|
||||||
|
|
||||||
|
include <./common.scad>;
|
||||||
|
|
||||||
|
// box(20, 10, 10);
|
||||||
|
// color("blue", 0.5) cube([10, 20, 10], center = true);
|
||||||
|
|
||||||
|
module cell_cradle(width, height) {
|
||||||
|
difference() {
|
||||||
|
translate([0, 0, -height / 2]) cube([width,
|
||||||
|
wall_thickness,
|
||||||
|
height],
|
||||||
|
center = true);
|
||||||
|
color("red", 1) translate([0, 0, 0])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
cylinder(h = wall_thickness + cutout_threshold,
|
||||||
|
r = width / 2,
|
||||||
|
center = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module cell_box() {
|
||||||
|
union() {
|
||||||
|
channel(cell_holder_length, cell_holder_width, cell_holder_height);
|
||||||
|
translate([0, -battery_length / 6, wall_thickness]) cell_cradle(cell_holder_width, cell_holder_height / 2);
|
||||||
|
translate([0, battery_length / 6, wall_thickness]) cell_cradle(cell_holder_width, cell_holder_height / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module contact_box() {
|
||||||
|
contact_thickness = battery_contact_flange_height * .75;
|
||||||
|
cutout_width = battery_contact_width * .8;
|
||||||
|
// box_thickness = contact_thickness_ + wall_thickness * 2;
|
||||||
|
// box_height = width + wall_thickness;
|
||||||
|
|
||||||
|
difference() {
|
||||||
|
box(wall_thickness * 2 + contact_thickness, cell_holder_width, cell_holder_height);
|
||||||
|
translate([0, contact_thickness, wall_thickness * 2])
|
||||||
|
cube([battery_contact_width,
|
||||||
|
wall_thickness * 2,
|
||||||
|
battery_contact_length + threshold],
|
||||||
|
center = true);
|
||||||
|
|
||||||
|
color("red", 1) translate([0,
|
||||||
|
-(wall_thickness + contact_thickness + threshold) / 2,
|
||||||
|
cell_holder_height / 2])
|
||||||
|
cube([5, wall_thickness + threshold * 2, cell_holder_height], center = true);
|
||||||
|
|
||||||
|
translate([0,
|
||||||
|
-(wall_thickness + contact_thickness + threshold) / 2 - wire_radius,
|
||||||
|
0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
cylinder(h = cell_holder_width, r = wire_radius, center = true);
|
||||||
|
|
||||||
|
color("green", 1) translate([-cell_holder_width / 2, 0, cell_holder_height / 2])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
cylinder(h = 5, r = contact_thickness / 2, center = true);
|
||||||
|
|
||||||
|
color("green", 1) translate([cell_holder_width / 2, 0, cell_holder_height / 2])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
cylinder(h = 5, r = contact_thickness / 2, center = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module battery_slot() {
|
||||||
|
difference() {
|
||||||
|
union() {
|
||||||
|
translate([0, -cell_holder_length / 2, 0]) contact_box();
|
||||||
|
translate([0, wall_thickness, 0]) cell_box();
|
||||||
|
translate([0, cell_holder_length / 2 + wall_thickness * 2, 0])
|
||||||
|
rotate([0, 0, 180])
|
||||||
|
contact_box();
|
||||||
|
}
|
||||||
|
translate([cell_holder_width / 2, 1, 0]) rotate([90, 0, 0]) cylinder(h = cell_holder_length + wall_thickness * 4 + battery_contact_flange_height * 2, r = wire_radius, center = true);
|
||||||
|
translate([-cell_holder_width / 2, 1, 0]) rotate([90, 0, 0]) cylinder(h = cell_holder_length + wall_thickness * 4 + battery_contact_flange_height * 2, r = wire_radius, center = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module converter_box() {
|
||||||
|
box_length = wall_thickness * 2 + converter_height;
|
||||||
|
box_width = cell_holder_width * 2 - wall_thickness;
|
||||||
|
difference() {
|
||||||
|
box(box_length, box_width, cell_holder_height);
|
||||||
|
|
||||||
|
translate([cell_holder_width - wire_radius, 0, 0])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
cylinder(h = box_length, r = wire_radius, center = true);
|
||||||
|
translate([cell_holder_width - wire_radius * 2, 0, 0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
cylinder(h = wall_thickness + threshold, r = wire_radius, center = true);
|
||||||
|
|
||||||
|
translate([-cell_holder_width + wire_radius, 0, 0])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
cylinder(h = box_length, r = wire_radius, center = true);
|
||||||
|
translate([-cell_holder_width + wire_radius * 2, 0, 0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
cylinder(h = wall_thickness + threshold, r = wire_radius, center = true);
|
||||||
|
|
||||||
|
translate([0, -box_length / 2, 0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
cylinder(h = cell_holder_width * 2 + wall_thickness, r = wire_radius, center = true);
|
||||||
|
|
||||||
|
translate([-cell_holder_width * .75, (-box_length + wall_thickness) / 2, 0])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
cylinder(h = wall_thickness * 2, r = wire_radius, center = true);
|
||||||
|
|
||||||
|
translate([cell_holder_width * .75, (-box_length + wall_thickness) / 2, 0])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
cylinder(h = wall_thickness * 2, r = wire_radius, center = true);
|
||||||
|
|
||||||
|
color("red", 1) translate([-box_width / 4, -(converter_height + wall_thickness) / 2, cell_holder_height / 2])
|
||||||
|
cube([5, wall_thickness + threshold * 2, cell_holder_height], center = true);
|
||||||
|
color("red", 1) translate([box_width / 4, -(converter_height + wall_thickness) / 2, cell_holder_height / 2])
|
||||||
|
cube([5, wall_thickness + threshold * 2, cell_holder_height], center = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module battery_case() {
|
||||||
|
union() {
|
||||||
|
translate([-cell_holder_width / 2, 0, 0]) battery_slot();
|
||||||
|
translate([cell_holder_width / 2 - wall_thickness, 0, 0]) battery_slot();
|
||||||
|
translate([-wall_thickness / 2,
|
||||||
|
cell_holder_length / 2 + wall_thickness * 2 + battery_contact_flange_height + wall_thickness * 2 + wall_thickness / 2,
|
||||||
|
0])
|
||||||
|
converter_box();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
battery_case();
|
||||||
|
|
|
@ -0,0 +1,174 @@
|
||||||
|
width = 65;
|
||||||
|
length = 75;
|
||||||
|
height = 16;
|
||||||
|
wall_thickness = 2;
|
||||||
|
guide_thickness = 1;
|
||||||
|
power_width = 21;
|
||||||
|
output_width = 37.5;
|
||||||
|
half_wall_thickness = wall_thickness / 2;
|
||||||
|
standoff_thickness = 10;
|
||||||
|
hole_diameter = 3;
|
||||||
|
// The radius of a nut in mm. However, based on my measurements, I'm not actually sure I have this right. The short height of a nut is 7.86mm. Derive from there.
|
||||||
|
nut_radius = 8.5 * cos(30) / 2;
|
||||||
|
nut_height = 2.69; // mm
|
||||||
|
screw_radius = 2;
|
||||||
|
handlebar_radius = 15;
|
||||||
|
clasp_thickness = 4;
|
||||||
|
clasp_width = 35;
|
||||||
|
circular_face_count = 48;
|
||||||
|
|
||||||
|
module hexagon(r, h) {
|
||||||
|
pi = 3.1415926;
|
||||||
|
polyhedron(
|
||||||
|
points=[
|
||||||
|
[r, 0, 0],
|
||||||
|
[r * cos(60), r * sin(60), 0],
|
||||||
|
[r * cos(120), r * sin(120), 0],
|
||||||
|
[r * cos(180), r * sin(180), 0],
|
||||||
|
[r * cos(240), r * sin(240), 0],
|
||||||
|
[r * cos(300), r * sin(300), 0],
|
||||||
|
|
||||||
|
[r, 0, h],
|
||||||
|
[r * cos(60), r * sin(60), h],
|
||||||
|
[r * cos(120), r * sin(120), h],
|
||||||
|
[r * cos(180), r * sin(180), h],
|
||||||
|
[r * cos(240), r * sin(240), h],
|
||||||
|
[r * cos(300), r * sin(300), h],
|
||||||
|
],
|
||||||
|
faces=[
|
||||||
|
[0, 1, 2, 3, 4, 5],
|
||||||
|
[11, 10, 9, 8, 7, 6],
|
||||||
|
[6, 7, 1, 0],
|
||||||
|
[7, 8, 2, 1],
|
||||||
|
[8, 9, 3, 2],
|
||||||
|
[9, 10, 4, 3],
|
||||||
|
[10, 11, 5, 4],
|
||||||
|
[11, 6, 0, 5],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nut holders are blocks that have a hole drilled through them and a hexagonal-shaped cavity. The idea is to
|
||||||
|
module nut_holder() {
|
||||||
|
difference() {
|
||||||
|
translate([-4.5, -4.5, -2]) cube([9, 9, 4]);
|
||||||
|
union() {
|
||||||
|
translate([0, 0, -1]) hexagon(nut_radius, 2);
|
||||||
|
cylinder(h = 6, r = screw_radius, center = true, $fn = 24);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module screw_hole() {
|
||||||
|
union() {
|
||||||
|
translate([0, 0, 4]) cylinder(h = 2.1, r = screw_radius * 2, center = true, $fn = 24);
|
||||||
|
cylinder(h = 6, r = screw_radius, center = true, $fn = 24);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module base() {
|
||||||
|
cube([width, length, wall_thickness]);
|
||||||
|
}
|
||||||
|
|
||||||
|
module face() {
|
||||||
|
union() {
|
||||||
|
cube([width, length, wall_thickness / 2]);
|
||||||
|
translate([wall_thickness, wall_thickness, wall_thickness / 2]) cube([width-wall_thickness*2, length-wall_thickness*2, wall_thickness / 2]);
|
||||||
|
translate([4.5 + wall_thickness, 4.5 + wall_thickness, 4]) nut_holder();
|
||||||
|
translate([width - 4.5 - wall_thickness, 4.5 + wall_thickness, 4]) nut_holder();
|
||||||
|
translate([width - 4.5 - wall_thickness, length - 4.5 - wall_thickness, 4]) nut_holder();
|
||||||
|
translate([4.5 + wall_thickness, length - 4.5 - wall_thickness, 4]) nut_holder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module wall(length) {
|
||||||
|
cube([length, height, wall_thickness]);
|
||||||
|
}
|
||||||
|
|
||||||
|
module power_wall() {
|
||||||
|
difference() {
|
||||||
|
wall(65);
|
||||||
|
translate([9, 2, -.5]) cube([power_width, height, wall_thickness + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module output_wall() {
|
||||||
|
difference() {
|
||||||
|
wall(65);
|
||||||
|
translate([9, 2, -.5]) cube([output_width, height, wall_thickness + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use hexagons as cutouts into which I can install a hex nut. This isn't quite right yet, but close.
|
||||||
|
// hexagon(nut_radius, 1);
|
||||||
|
|
||||||
|
// cube([standoff_thickness, standoff_thickness, 2]);
|
||||||
|
|
||||||
|
/*
|
||||||
|
difference() {
|
||||||
|
union() {
|
||||||
|
base();
|
||||||
|
rotate([90, 0, 90]) wall(75);
|
||||||
|
// translate([width - wall_thickness, 0, 0]) rotate([90, 0, 90]) wall(length);
|
||||||
|
// rotate([90, 0, 0]) power_wall();
|
||||||
|
// translate([0, length, 0]) rotate([90, 0, 0]) output_wall();
|
||||||
|
// translate([wall_thickness,
|
||||||
|
// wall_thickness,
|
||||||
|
// wall_thickness]) standoff();
|
||||||
|
// translate([width - wall_thickness - standoff_thickness,
|
||||||
|
// wall_thickness,
|
||||||
|
// wall_thickness]) standoff();
|
||||||
|
// translate([wall_thickness,
|
||||||
|
// length - wall_thickness - standoff_thickness,
|
||||||
|
// wall_thickness]) standoff();
|
||||||
|
// translate([width - wall_thickness - standoff_thickness,
|
||||||
|
// length - wall_thickness - standoff_thickness,
|
||||||
|
// wall_thickness]) standoff();
|
||||||
|
}
|
||||||
|
// translate([-half_wall_thickness, -wall_thickness - half_wall_thickness, height - half_wall_thickness]) cube([wall_thickness, length + wall_thickness * 2, wall_thickness]);
|
||||||
|
// translate([width - half_wall_thickness, -wall_thickness - half_wall_thickness, height - half_wall_thickness]) cube([wall_thickness, length + wall_thickness * 2, wall_thickness]);
|
||||||
|
// translate([-half_wall_thickness, -half_wall_thickness, height - half_wall_thickness]) rotate([0, 0, 270]) cube([wall_thickness, width + wall_thickness * 2, wall_thickness]);
|
||||||
|
// translate([-half_wall_thickness, length + half_wall_thickness, height - half_wall_thickness]) rotate([0, 0, 270]) cube([wall_thickness, width + wall_thickness * 2, wall_thickness]);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
module box() {
|
||||||
|
difference() {
|
||||||
|
union() {
|
||||||
|
cube([width, length, wall_thickness * 2]);
|
||||||
|
translate([0, 0, wall_thickness]) rotate([90, 0, 90]) wall(length);
|
||||||
|
translate([width - wall_thickness, 0, wall_thickness]) rotate([90, 0, 90]) wall(length);
|
||||||
|
translate([0, wall_thickness, wall_thickness]) rotate([90, 0, 0]) wall(width);
|
||||||
|
translate([0, length, wall_thickness]) rotate([90, 0, 0]) wall(width);
|
||||||
|
}
|
||||||
|
translate([4.5 + wall_thickness, 4.5 + wall_thickness, 4]) rotate([180, 0, 0]) screw_hole();
|
||||||
|
translate([width - 4.5 - wall_thickness, 4.5 + wall_thickness, 4]) rotate([180, 0, 0]) screw_hole();
|
||||||
|
translate([width - 4.5 - wall_thickness, length - 4.5 - wall_thickness, 4]) rotate([180, 0, 0]) screw_hole();
|
||||||
|
translate([4.5 + wall_thickness, length - 4.5 - wall_thickness, 4]) rotate([180, 0, 0]) screw_hole();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module top_clasp() {
|
||||||
|
difference() {
|
||||||
|
union() {
|
||||||
|
cylinder(h = clasp_width, r = handlebar_radius + clasp_thickness, center = true, $fn = circular_face_count);
|
||||||
|
translate([0, 0, -clasp_width / 2]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||||
|
translate([0, 0, -clasp_width / 2 + 4]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||||
|
translate([0, 0, clasp_width / 2]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||||
|
translate([0, 0, clasp_width / 2 - 4]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||||
|
translate([-handlebar_radius-5, -10, -clasp_width / 2 + 6]) cube([6, 20, clasp_width - 12]);
|
||||||
|
}
|
||||||
|
translate([-0.5, 0, 0]) cylinder(h = clasp_width+2, r = handlebar_radius + 1, center = true, $fn = circular_face_count);
|
||||||
|
translate([-0.5, -handlebar_radius - 10, -clasp_width / 2 - 1]) cube([handlebar_radius + 10, handlebar_radius * 2 + 20, clasp_width + 2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module body() {
|
||||||
|
union() {
|
||||||
|
box();
|
||||||
|
translate([width / 2, length / 2, -5 - handlebar_radius]) rotate([0, 90, 90]) top_clasp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body();
|
||||||
|
translate([width + 10, 0, 0]) face();
|
|
@ -0,0 +1,21 @@
|
||||||
|
handlebar_radius = 15;
|
||||||
|
clasp_thickness = 4;
|
||||||
|
circular_face_count = 48;
|
||||||
|
clasp_width = 35;
|
||||||
|
|
||||||
|
module top_clasp() {
|
||||||
|
difference() {
|
||||||
|
union() {
|
||||||
|
cylinder(h = clasp_width, r = handlebar_radius + clasp_thickness, center = true, $fn = circular_face_count);
|
||||||
|
translate([0, 0, -clasp_width / 2]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||||
|
translate([0, 0, -clasp_width / 2 + 4]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||||
|
translate([0, 0, clasp_width / 2]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||||
|
translate([0, 0, clasp_width / 2 - 4]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||||
|
translate([-handlebar_radius-5, -10, -clasp_width / 2 + 6]) cube([6, 20, clasp_width - 12]);
|
||||||
|
}
|
||||||
|
translate([-0.5, 0, 0]) cylinder(h = clasp_width+2, r = handlebar_radius + 1, center = true, $fn = circular_face_count);
|
||||||
|
translate([-0.5, -handlebar_radius - 10, -clasp_width / 2 - 1]) cube([handlebar_radius + 10, handlebar_radius * 2 + 20, clasp_width + 2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
top_clasp();
|
|
@ -0,0 +1,92 @@
|
||||||
|
|
||||||
|
module hexagon(r, h) {
|
||||||
|
cylinder(r = r, h = h, center = 2, $fn = 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
module pill(length, bevel) {
|
||||||
|
hull() {
|
||||||
|
translate([0, 0, (-length / 2) + bevel]) sphere(r = bevel);
|
||||||
|
translate([0, 0, (length / 2) - bevel]) sphere(r = bevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module rounded_cube(dimensions, bevel = 0) {
|
||||||
|
x = dimensions[0];
|
||||||
|
y = dimensions[1];
|
||||||
|
z = dimensions[2];
|
||||||
|
|
||||||
|
if (bevel > 0) {
|
||||||
|
hull() {
|
||||||
|
translate([-x / 2 + bevel, -y / 2 + bevel, -z / 2 + bevel]) sphere(r = bevel);
|
||||||
|
translate([ x / 2 - bevel, -y / 2 + bevel, -z / 2 + bevel]) sphere(r = bevel);
|
||||||
|
translate([ x / 2 - bevel, y / 2 - bevel, -z / 2 + bevel]) sphere(r = bevel);
|
||||||
|
translate([-x / 2 + bevel, y / 2 - bevel, -z / 2 + bevel]) sphere(r = bevel);
|
||||||
|
translate([-x / 2 + bevel, -y / 2 + bevel, z / 2 - bevel]) sphere(r = bevel);
|
||||||
|
translate([ x / 2 - bevel, -y / 2 + bevel, z / 2 - bevel]) sphere(r = bevel);
|
||||||
|
translate([ x / 2 - bevel, y / 2 - bevel, z / 2 - bevel]) sphere(r = bevel);
|
||||||
|
translate([-x / 2 + bevel, y / 2 - bevel, z / 2 - bevel]) sphere(r = bevel);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cube(dimensions, center = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module box_face(dimensions, bevel = 0) {
|
||||||
|
x = dimensions[0];
|
||||||
|
y = dimensions[1];
|
||||||
|
z = dimensions[2];
|
||||||
|
|
||||||
|
if (bevel > 0) {
|
||||||
|
translate([0, 0, z / 2])
|
||||||
|
hull() {
|
||||||
|
pill(z, bevel);
|
||||||
|
translate([x, 0, 0])
|
||||||
|
pill(z, bevel);
|
||||||
|
translate([x, y, 0])
|
||||||
|
pill(z, bevel);
|
||||||
|
translate([0, y, 0])
|
||||||
|
pill(z, bevel);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cube(dimensions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module channel(length, width, height, bevel) {
|
||||||
|
union() {
|
||||||
|
box_face([length, width, wall_thickness], bevel);
|
||||||
|
|
||||||
|
translate([0, wall_thickness - bevel, bevel])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
box_face([length, height, wall_thickness], bevel);
|
||||||
|
|
||||||
|
translate([0, width + bevel, bevel])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
box_face([length, height, wall_thickness], bevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module box(length, width, height, bevel = 0) {
|
||||||
|
union() {
|
||||||
|
channel(length, width, height, bevel);
|
||||||
|
|
||||||
|
translate([-bevel, 0, bevel])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
box_face([width, height, wall_thickness], bevel);
|
||||||
|
|
||||||
|
translate([length - wall_thickness + bevel, 0, bevel])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
box_face([width, height, wall_thickness], bevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module box_side_slider(length, width, height) {
|
||||||
|
difference() {
|
||||||
|
box_face([width - wall_thickness * 2 + 4, height, wall_thickness], bevel);
|
||||||
|
translate([-1, -1, 1]) cube([4-threshold, height+2, 4-threshold]);
|
||||||
|
color("red") translate([width - wall_thickness * 2 + 1, -1, 1]) cube([4-threshold, height+2, 4-threshold]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,210 @@
|
||||||
|
$fn = 50;
|
||||||
|
threshold = 0.1;
|
||||||
|
|
||||||
|
board_length = 92;
|
||||||
|
board_width = 72;
|
||||||
|
board_height = 21.5;
|
||||||
|
wall_thickness = 4;
|
||||||
|
bevel = 0.5;
|
||||||
|
|
||||||
|
hinge_radius = 2.5;
|
||||||
|
|
||||||
|
case_width = board_width + wall_thickness * 2;
|
||||||
|
case_length = board_length + wall_thickness * 2;
|
||||||
|
case_height = board_height + wall_thickness;
|
||||||
|
|
||||||
|
handlebar_radius = 15;
|
||||||
|
clasp_thickness = 4;
|
||||||
|
circular_face_count = 48;
|
||||||
|
clasp_width = 35;
|
||||||
|
|
||||||
|
include <./common.scad>;
|
||||||
|
|
||||||
|
module top_clasp() {
|
||||||
|
difference() {
|
||||||
|
union() {
|
||||||
|
cylinder(h = clasp_width, r = handlebar_radius + clasp_thickness, center = true, $fn = circular_face_count);
|
||||||
|
translate([0, 0, -clasp_width / 2]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||||
|
translate([0, 0, -clasp_width / 2 + 4]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||||
|
translate([0, 0, clasp_width / 2]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||||
|
translate([0, 0, clasp_width / 2 - 4]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||||
|
translate([-handlebar_radius-5, -10, -clasp_width / 2 + 6]) cube([6, 20, clasp_width - 12]);
|
||||||
|
}
|
||||||
|
translate([-0.5, 0, 0]) cylinder(h = clasp_width+2, r = handlebar_radius + 1, center = true, $fn = circular_face_count);
|
||||||
|
translate([-0.5, -handlebar_radius - 10, -clasp_width / 2 - 1]) cube([handlebar_radius + 10, handlebar_radius * 2 + 20, clasp_width + 2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module hinge(length) {
|
||||||
|
difference() {
|
||||||
|
union() {
|
||||||
|
cube([hinge_radius * 2, length, hinge_radius], center = true);
|
||||||
|
translate([0, 0, -1.5]) rotate([90, 0, 0]) cylinder(h = length, r = hinge_radius, center = true);
|
||||||
|
}
|
||||||
|
translate([0, threshold / 2, -1.5]) rotate([90, 0, 0]) cylinder(h = length + threshold * 2, r = 1, center = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module base_case(length, width, height, bevel = 0) {
|
||||||
|
difference() {
|
||||||
|
union() {
|
||||||
|
channel(length + wall_thickness / 2, width, height, bevel);
|
||||||
|
|
||||||
|
translate([-bevel, 0, bevel])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
box_face([width, height, wall_thickness], bevel);
|
||||||
|
|
||||||
|
// These are the sleds at the bottom of the case that should hold the lower of the two boards down
|
||||||
|
color("blue") translate([0, wall_thickness - 2, wall_thickness + 4]) cube([length - 8, 4, wall_thickness / 2]);
|
||||||
|
color("blue") translate([wall_thickness - 2, wall_thickness - 4, wall_thickness + 4]) cube([4, width, wall_thickness / 2]);
|
||||||
|
color("blue") translate([length - 25, width - wall_thickness * 3 / 2, wall_thickness + 6]) cube([16, wall_thickness, wall_thickness / 2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This makes an indent at the bottom to accomodate solder joins
|
||||||
|
translate([wall_thickness + 2, wall_thickness + 2, wall_thickness / 2]) cube([length, width - wall_thickness * 2 - 4, wall_thickness / 2 + threshold]);
|
||||||
|
|
||||||
|
// This creates a cutout that lets the power plug slide in better.
|
||||||
|
translate([wall_thickness, width - wall_thickness, wall_thickness]) cube([length, 2, 6]);
|
||||||
|
|
||||||
|
// These two put in the slots that should allow the fourth wall to be slotted into place.
|
||||||
|
color("red") translate([length - 1, wall_thickness - 2, 4]) cube([2, 2, height]);
|
||||||
|
color("red") translate([length - 1, width - wall_thickness, 4]) cube([2, 2, height]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module main_case() {
|
||||||
|
hinge_length = board_length / 4;
|
||||||
|
hinge_y_offset = board_width + wall_thickness + hinge_radius;
|
||||||
|
hinge_z_offset = board_height;
|
||||||
|
|
||||||
|
difference() {
|
||||||
|
union() {
|
||||||
|
base_case(case_length,
|
||||||
|
case_width,
|
||||||
|
case_height,
|
||||||
|
bevel);
|
||||||
|
|
||||||
|
translate([-bevel, 0, bevel])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
box_face([case_width, case_height, wall_thickness], bevel);
|
||||||
|
|
||||||
|
translate([0, -hinge_radius - bevel + threshold, hinge_z_offset + bevel])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
hinge(case_length / 4);
|
||||||
|
|
||||||
|
translate([case_length - hinge_length, -hinge_radius - bevel + threshold, hinge_z_offset + bevel])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
hinge(case_length / 4);
|
||||||
|
|
||||||
|
translate([43, case_width, wall_thickness + 8])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
rotate([0, 180, 0])
|
||||||
|
linear_extrude(1)
|
||||||
|
text("lights", size = 3);
|
||||||
|
|
||||||
|
translate([67, case_width, wall_thickness + 8])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
rotate([0, 180, 0])
|
||||||
|
linear_extrude(1)
|
||||||
|
text("left", size = 3);
|
||||||
|
|
||||||
|
translate([55, case_width, wall_thickness + 8])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
rotate([0, 180, 0])
|
||||||
|
linear_extrude(1)
|
||||||
|
text("right", size = 3);
|
||||||
|
// translate([case_length / 2, case_width / 2, -20]) rotate([0, 90, 0]) top_clasp();
|
||||||
|
}
|
||||||
|
|
||||||
|
translate([case_length / 2, case_width / 2, -threshold]) hexagon(4.5, 6);
|
||||||
|
|
||||||
|
# translate([8.5 + wall_thickness, case_width - wall_thickness - threshold, wall_thickness])
|
||||||
|
# cube([60, wall_thickness * 2, 7]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module lamp() {
|
||||||
|
union() {
|
||||||
|
translate([0, 0, -0.5]) cube([12.9 + threshold, 8, 4], center = true);
|
||||||
|
translate([0, 0, .88]) cube([5 + threshold, 5 + threshold, 1.56], center = true);
|
||||||
|
/*
|
||||||
|
translate([0, 0, -1.56]) cube([12.9, 7.6, wall_thickness], center = true);
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module button() {
|
||||||
|
union() {
|
||||||
|
cube([3.5 + threshold, 6.1 + threshold, 4 + threshold], center = true);
|
||||||
|
translate([0, 0, -0.5]) cube([1.2, 7, 3 + threshold], center = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module lid() {
|
||||||
|
lid_width = case_width + hinge_radius * 2 + wall_thickness;
|
||||||
|
hinge_length = case_length / 4;
|
||||||
|
union() {
|
||||||
|
difference() {
|
||||||
|
rounded_cube([case_length,
|
||||||
|
lid_width,
|
||||||
|
wall_thickness],
|
||||||
|
bevel);
|
||||||
|
translate([0, lid_width / 5, 0.4]) lamp();
|
||||||
|
translate([-15, lid_width / 5, 0.4]) lamp();
|
||||||
|
translate([15, lid_width / 5, 0.4]) lamp();
|
||||||
|
translate([-30, lid_width / 5, 0]) button();
|
||||||
|
translate([30, lid_width / 5, 0]) button();
|
||||||
|
|
||||||
|
translate([0, lid_width / 5, -2]) cube([20, 7, 3], center = true);
|
||||||
|
|
||||||
|
color("black") translate([-2, lid_width / 5 - 5, -2]) rotate([0, 0, 90]) rotate([0, 90, 0]) cylinder(h=5, r = 1, center = true, $fn = circular_face_count);
|
||||||
|
color("black") translate([-17, lid_width / 5 - 5, -2]) rotate([0, 0, 90]) rotate([0, 90, 0]) cylinder(h=5, r = 1, center = true, $fn = circular_face_count);
|
||||||
|
color("black") translate([13, lid_width / 5 - 5, -2]) rotate([0, 0, 90]) rotate([0, 90, 0]) cylinder(h=5, r = 1, center = true, $fn = circular_face_count);
|
||||||
|
color("black") translate([-30, lid_width / 5 - 5, -2]) rotate([0, 0, 90]) rotate([0, 90, 0]) cylinder(h=5, r = 1, center = true, $fn = circular_face_count);
|
||||||
|
color("black") translate([30, lid_width / 5 - 5, -2]) rotate([0, 0, 90]) rotate([0, 90, 0]) cylinder(h=5, r = 1, center = true, $fn = circular_face_count);
|
||||||
|
color("black") translate([0, 10, -2]) rotate([0, 90, 0]) cylinder(h = 62, r = 1, center = true, $fn = circular_face_count);
|
||||||
|
|
||||||
|
color("red") translate([-33, 21, -2]) rotate([0, 90, 0]) cylinder(h = 5, r = 1, center = true, $fn = circular_face_count);
|
||||||
|
color("red") translate([-35, 13, -2]) rotate([0, 0, 90]) rotate([0, 90, 0]) cylinder(h = 18, r = 1, center = true, $fn = circular_face_count);
|
||||||
|
color("red") translate([33, 21, -2]) rotate([0, 90, 0]) cylinder(h = 5, r = 1, center = true, $fn = circular_face_count);
|
||||||
|
color("red") translate([35, 13, -2]) rotate([0, 0, 90]) rotate([0, 90, 0]) cylinder(h = 18, r = 1, center = true, $fn = circular_face_count);
|
||||||
|
color("red") translate([0, 5, -2]) rotate([0, 90, 0]) cylinder(h = 70, r = 1, center = true, $fn = circular_face_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
translate([case_length / 2 - hinge_length / 2, lid_width / 2 - wall_thickness / 2 - 0.5, -wall_thickness / 2]) rotate([0, 0, 90]) hinge(hinge_length);
|
||||||
|
translate([-case_length / 2 + hinge_length / 2, lid_width / 2 - wall_thickness / 2 - 0.5, -wall_thickness / 2]) rotate([0, 0, 90]) hinge(hinge_length);
|
||||||
|
|
||||||
|
translate([0, -lid_width / 2 + bevel, -3]) rounded_cube([20, wall_thickness / 2, 10], bevel);
|
||||||
|
color("blue") translate([-9, -lid_width / 2 + 1.5, -6]) rotate([90, 0, 0]) rotate([0, 90, 0]) linear_extrude(18) circle(1, $fn = 3);
|
||||||
|
color("blue") translate([-9, -lid_width / 2 + 1.5, -7]) rotate([90, 0, 0]) rotate([0, 90, 0]) linear_extrude(18) circle(1, $fn = 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module box_side() {
|
||||||
|
box_side_slider(case_length, case_width, case_height);
|
||||||
|
}
|
||||||
|
|
||||||
|
module case_base() {
|
||||||
|
difference() {
|
||||||
|
rounded_cube([case_length, case_width, wall_thickness + 2], bevel = 0.5);
|
||||||
|
translate([wall_thickness, 0, 2]) rounded_cube([case_length + threshold, board_width + threshold, 2 + threshold]);
|
||||||
|
|
||||||
|
// These give a screw-hole in the center which will allow the clamp to be attached
|
||||||
|
translate([0, 0, -1]) hexagon(4.5, 2);
|
||||||
|
translate([0, 0, -wall_thickness / 2]) cylinder(r = 2, h = wall_thickness + threshold, center = true);
|
||||||
|
|
||||||
|
// and now a bit of an indentation to help the clip remain in place
|
||||||
|
translate([0, 0, -4.5]) cube([clasp_width + threshold, clasp_width + threshold, wall_thickness], center = true);
|
||||||
|
|
||||||
|
// here are some grooves along the edges that can be used to piece parts together
|
||||||
|
translate([wall_thickness / 2, case_width / 2 - wall_thickness / 2, wall_thickness / 2])
|
||||||
|
cube([board_length + wall_thickness, wall_thickness / 2, wall_thickness / 2 + threshold], center = true);
|
||||||
|
translate([wall_thickness / 2, -case_width / 2 + wall_thickness / 2, wall_thickness / 2])
|
||||||
|
cube([board_length + wall_thickness, wall_thickness / 2, wall_thickness / 2 + threshold], center = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
|
||||||
|
include <./control_panel.scad>
|
||||||
|
|
||||||
|
/*
|
||||||
|
difference() {
|
||||||
|
color("blue") rounded_cube([5, 5, 5], bevel = 0.5);
|
||||||
|
translate([0, 0, 1]) rounded_cube([4, 4, 4]);
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
|
case_base();
|
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
include <./control_panel.scad>
|
||||||
|
|
||||||
|
lid();
|
||||||
|
// lamp();
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
include <./control_panel.scad>
|
||||||
|
|
||||||
|
box_side();
|
|
@ -0,0 +1,10 @@
|
||||||
|
[package]
|
||||||
|
name = "lights-core"
|
||||||
|
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" }
|
|
@ -0,0 +1,481 @@
|
||||||
|
#![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 => WATER_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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,400 @@
|
||||||
|
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 = [
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
|
||||||
|
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
];
|
||||||
|
|
||||||
|
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,
|
||||||
|
];
|
|
@ -0,0 +1,17 @@
|
||||||
|
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];
|
|
@ -0,0 +1,16 @@
|
||||||
|
[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 = "*" }
|
|
@ -0,0 +1,288 @@
|
||||||
|
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);
|
||||||
|
}
|
|
@ -0,0 +1,491 @@
|
||||||
|
{
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#addr2line@0.24.2": "1hd1i57zxgz08j6h5qrhsnm2fi0bcqvsh389fw400xm3arz2ggnz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#adler2@2.0.0": "09r6drylvgy8vv8k20lnbvwq8gp09h7smfn6h1rxsy15pgh629si",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#adler32@1.2.0": "0d7jq7jsjyhsgbhnfq5fvrlh9j0i9g1fqrl2735ibv5f75yjgqda",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#adler@1.0.2": "1zim79cvzd5yrkzl3nyfx0avijwgk9fqv3yrscdy1cc79ih02qpj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#ahash@0.8.11": "04chdfkls5xmhp1d48gnjsmglbqibizs3bpbj6rsj604m10si7g8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#aho-corasick@1.1.3": "05mrpkvdgp5d20y2p989f187ry9diliijgwrs254fs9s1m1x6q4f",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#allocator-api2@0.2.18": "0kr6lfnxvnj164j1x38g97qjlhb7akppqzvgfs0697140ixbav2w",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#android-tzdata@0.1.1": "1w7ynjxrfs97xg3qlcdns4kgfpwcdv824g611fq32cag4cdr96g9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#android_system_properties@0.1.5": "04b3wrz12837j7mdczqd95b732gw5q7q66cv4yn4646lvccp57l1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#annotate-snippets@0.9.2": "07p8r6jzb7nqydq0kr5pllckqcdxlyld2g275v425axnzffpxbyc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#anstream@0.6.15": "09nm4qj34kiwgzczdvj14x7hgsb235g4sqsay3xsz7zqn4d5rqb4",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#anstyle-parse@0.2.5": "1jy12rvgbldflnb2x7mcww9dcffw1mx22nyv6p3n7d62h0gdwizb",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#anstyle-query@1.1.1": "0aj22iy4pzk6mz745sfrm1ym14r0y892jhcrbs8nkj7nqx9gqdkd",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#anstyle-wincon@3.0.4": "1y2pkvsrdxbcwircahb4wimans2pzmwwxad7ikdhj5lpdqdlxxsv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#anstyle@1.0.8": "1cfmkza63xpn1kkz844mgjwm9miaiz4jkyczmwxzivcsypk1vv0v",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#anyhow@1.0.89": "1xh1vg89n56h6nqikcmgbpmkixjds33492klrp9m96xrbmhgizc6",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#async-channel@1.9.0": "0dbdlkzlncbibd3ij6y6jmvjd0cmdn48ydcfdpfhw09njd93r5c1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#async-channel@2.3.1": "0skvwxj6ysfc6d7bhczz9a2550260g62bm5gl0nmjxxyn007id49",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#async-executor@1.13.1": "1v6w1dbvsmw6cs4dk4lxj5dvrikc6xi479wikwaab2qy3h09mjih",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#async-global-executor@2.4.1": "1762s45cc134d38rrv0hyp41hv4iv6nmx59vswid2p0il8rvdc85",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#async-io@2.3.4": "1s679l7x6ijh8zcxqn5pqgdiyshpy4xwklv86ldm1rhfjll04js4",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#async-lock@3.4.0": "060vh45i809wcqyxzs5g69nqiqah7ydz0hpkcjys9258vqn4fvpz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#async-std@1.13.0": "059nbiyijwbndyrz0050skvlvzhds0dmnl0biwmxwbw055glfd66",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#async-task@4.7.1": "1pp3avr4ri2nbh7s6y9ws0397nkx1zymmcr14sq761ljarh3axcb",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#async-trait@0.1.83": "1p8q8gm4fv2fdka8hwy2w3f8df7p5inixqi7rlmbnky3wmysw73j",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#atoi@2.0.0": "0a05h42fggmy7h0ajjv6m7z72l924i7igbx13hk9d8pyign9k3gj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#atomic-waker@1.1.2": "1h5av1lw56m0jf0fd3bchxq8a30xv0b4wv8s4zkp4s0i7mfvs18m",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#autocfg@0.1.8": "0y4vw4l4izdxq1v0rrhvmlbqvalrqrmk60v1z0dqlgnlbzkl7phd",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#autocfg@1.4.0": "09lz3by90d2hphbq56znag9v87gfpd9gb8nr82hll8z6x2nhprdc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#az@1.2.1": "0ww9k1w3al7x5qmb7f13v3s9c2pg1pdxbs8xshqy6zyrchj4qzkv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#backtrace@0.3.74": "06pfif7nwx66qf2zaanc2fcq7m64i91ki9imw9xd3bnz5hrwp0ld",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#base64@0.21.7": "0rw52yvsk75kar9wgqfwgb414kvil1gn7mqkrhn9zf1537mpsacx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#base64@0.9.3": "0hs62r35bgxslawyrn1vp9rmvrkkm76fqv0vqcwd048vs876r7a8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#base64ct@1.6.0": "0nvdba4jb8aikv60az40x2w1y96sjdq8z3yp09rwzmkhiwv1lg4c",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#bindgen@0.69.5": "1240snlcfj663k04bjsg629g4wx6f83flgbjh5rzpgyagk3864r7",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#bit-set@0.5.3": "1wcm9vxi00ma4rcxkl3pzzjli6ihrpn9cfdi0c5b4cvga2mxs007",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#bit-vec@0.6.3": "1ywqjnv60cdh1slhz67psnp422md6jdliji6alq0gmly2xm9p7rl",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#bit_field@0.10.2": "0qav5rpm4hqc33vmf4vc4r0mh51yjx5vmd9zhih26n9yjs3730nw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#bitflags@1.3.2": "12ki6w8gn1ldq7yz9y680llwk5gmrhrzszaa17g1sbrw2r2qvwxy",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#bitflags@2.6.0": "1pkidwzn3hnxlsl8zizh0bncgbjnw7c41cx7bby26ncbzmiznj5h",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#block-buffer@0.10.4": "0w9sa2ypmrsqqvc20nhwr75wbb5cjr4kkyhpjm1z1lv2kdicfy1h",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#blocking@1.6.1": "1si99l8zp7c4zq87y35ayjgc5c9b60jb8h0k14zfcs679z2l2gvh",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#build_html@2.5.0": "0p4k25yk3v0wf720wl5zcghvc9ik6l7lsh3fz86cq3g7x4nbhpi2",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#bumpalo@3.16.0": "0b015qb4knwanbdlp1x48pkb4pm57b8gidbhhhxr900q2wb6fabr",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#bytemuck@1.18.0": "1bp2s9wn0gjsaygv21nsbfpf854vl897ll6sqpfn3naaannv1fwl",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#byteorder@1.5.0": "0jzncxyf404mwqdbspihyzpkndfgda450l0893pz5xj685cg5l0z",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#bytes@1.7.2": "1wzs7l57iwqmrszdpr2mmqf1b1hgvpxafc30imxhnry0zfl9m3a2",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cairo-rs@0.18.5": "1qjfkcq3mrh3p01nnn71dy3kn99g21xx3j8xcdvzn8ll2pq6x8lc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cairo-sys-rs@0.18.2": "0lfsxl7ylw3phbnwmz3k58j1gnqi6kc2hdc7g3bb7f4hwnl9yp38",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cc@1.1.34": "1j9dh96lpkksmfvjfiqa5nrlswm5l6lj54m5jf7i0iik8l6lgfb7",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cexpr@0.6.0": "0rl77bwhs5p979ih4r0202cn5jrfsrbgrksp40lkfz5vk1x3ib3g",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cfg-expr@0.15.8": "00lgf717pmf5qd2qsxxzs815v6baqg38d6m5i6wlh235p14asryh",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.0": "1za0vb97n4brpzpv8lsbnzmq5r8f2b0cpqqr0sy8h5bn751xxwds",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#chrono-tz-build@0.2.1": "03rmzd69cn7fp0fgkjr5042b3g54s2l941afjm3001ls7kqkjgj3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#chrono-tz@0.8.6": "0vlksnmpb6rd4h55245agnfhphnpslwnq9al3aw3is43dd3f16nm",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#chrono@0.4.38": "009l8vc5p8750vn02z30mblg4pv2qhkbfizhfwmzc6vpy5nr67x2",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#clang-sys@1.8.1": "1x1r9yqss76z8xwpdanw313ss6fniwc1r7dzb5ycjn0ph53kj0hb",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#clap@4.5.20": "1s37v23gcxkjy4800qgnkxkpliz68vslpr5sgn1xar56hmnkfzxr",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#clap_builder@4.5.20": "0m6w10l2f65h3ch0d53lql6p26xxrh20ffipra9ysjsfsjmq1g0r",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#clap_derive@4.5.18": "1ardb26bvcpg72q9myr7yir3a8c83gx7vxk1cccabsd9n73s1ija",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#clap_lex@0.7.2": "15zcrc2fa6ycdzaihxghf48180bnvzsivhf0fmah24bnnaf76qhl",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cloudabi@0.0.3": "0kxcg83jlihy0phnd2g8c2c303px3l2p3pkjz357ll6llnd5pz6x",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#color_quant@1.1.0": "12q1n427h2bbmmm1mnglr57jaz2dj9apk0plcxw7nwqiai7qjyrx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#colorchoice@1.0.2": "1h18ph538y8yjmbpaf8li98l0ifms2xmh3rax9666c5qfjfi3zfk",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#concurrent-queue@2.5.0": "0wrr3mzq2ijdkxwndhf79k952cp4zkz35ray8hvsxl96xrx1k82c",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#const-oid@0.9.6": "1y0jnqaq7p2wvspnx7qj76m7hjcqpz73qzvr9l2p9n2s51vr6if2",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#convert_case@0.6.0": "1jn1pq6fp3rri88zyw6jlhwwgf6qiyc08d6gjv0qypgkl862n67c",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cookie-factory@0.3.3": "18mka6fk3843qq3jw1fdfvzyv05kx7kcmirfbs2vg2kbw9qzm1cq",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cookie@0.17.0": "096c52jg9iq4lfcps2psncswv33fc30mmnaa2sbzzcfcw71kgyvy",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cool_asserts@2.0.3": "1v18dg7ifx41k2f82j3gsnpm1fg9wk5s4zv7sf42c7pnad72b7zf",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#core-foundation-sys@0.8.7": "12w8j73lazxmr1z0h98hf3z623kl8ms7g07jch7n4p8f9nwlhdkp",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#core-foundation@0.9.4": "13zvbbj07yk3b61b8fhwfzhy35535a583irf23vlcg59j7h9bqci",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cpufeatures@0.2.14": "1q3qd9qkw94vs7n5i0y3zz2cqgzcxvdgyb54ryngwmjhfbgrg1k0",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#crc-catalog@2.4.0": "1xg7sz82w3nxp1jfn425fvn1clvbzb3zgblmxsyqpys0dckp9lqr",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#crc32fast@1.4.2": "1czp7vif73b8xslr3c9yxysmh9ws2r8824qda7j47ffs9pcnjxx9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#crc@3.2.1": "0dnn23x68qakzc429s1y9k9y3g8fn5v9jwi63jcz151sngby9rk9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#crossbeam-deque@0.8.5": "03bp38ljx4wj6vvy4fbhx41q8f585zyqix6pncz1mkz93z08qgv1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#crossbeam-epoch@0.9.18": "03j2np8llwf376m3fxqx859mgp9f83hj1w34153c7a9c7i5ar0jv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#crossbeam-queue@0.3.11": "0d8y8y3z48r9javzj67v3p2yfswd278myz1j9vzc4sp7snslc0yz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#crossbeam-utils@0.8.20": "100fksq5mm1n7zj242cclkw6yf7a4a8ix3lvpfkhxvdhbda9kv12",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#crunchy@0.2.2": "1dx9mypwd5mpfbbajm78xcrg5lirqk7934ik980mmaffg3hdm0bs",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#crypto-common@0.1.6": "1cvby95a6xg7kxdz5ln3rl9xh66nz66w46mm3g56ri1z5x815yqv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#data-encoding@2.6.0": "1qnn68n4vragxaxlkqcb1r28d3hhj43wch67lm4rpxlw89wnjmp8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#deflate@0.8.6": "0x6iqlayg129w63999kz97m279m0jj4x4sm6gkqlvmp73y70yxvk",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#der@0.7.9": "1h4vzjfa1lczxdf8avfj9qlwh1qianqlxdy1g5rn762qnvkzhnzm",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#deranged@0.3.11": "1d1ibqqnr5qdrpw8rclwrf1myn3wf0dygl04idf4j2s49ah6yaxl",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#digest@0.10.7": "14p2n6ih29x81akj097lvz7wi9b6b9hvls0lwrv7b6xwyy0s5ncy",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#dimensioned@0.7.0": "09ky8s3higkf677lmyqg30hmj66gpg7hx907s6hfvbk2a9av05r5",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#dimensioned@0.8.0": "15s3j4ry943xqlac63bp81sgdk9s3yilysabzww35j9ibmnaic50",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#displaydoc@0.2.5": "1q0alair462j21iiqwrr21iabkfnb13d6x5w95lkdg21q2xrqdlp",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#dotenvy@0.15.7": "16s3n973n5aqym02692i1npb079n5mb0fwql42ikmwn8wnrrbbqs",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#either@1.13.0": "1w2c1mybrd7vljyxk77y9f4w9dyjrmp3yp82mk7bcm8848fazcb0",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#encoding_rs@0.8.34": "0nagpi1rjqdpvakymwmnlxzq908ncg868lml5b70n08bm82fjpdl",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#env_logger@0.10.2": "1005v71kay9kbz1d5907l0y7vh9qn2fqsp2yfgb8bjvin6m0bm2c",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#equivalent@1.0.1": "1malmx5f4lkfvqasz319lq6gb3ddg19yzf9s8cykfsgzdmyq0hsl",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#errno@0.3.9": "1fi0m0493maq1jygcf1bya9cymz2pc1mqxj26bdv7yjd37v5qk2k",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#etcetera@0.8.0": "0hxrsn75dirbjhwgkdkh0pnpqrnq17ypyhjpjaypgax1hd91nv8k",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#event-listener-strategy@0.5.2": "18f5ri227khkayhv3ndv7yl4rnasgwksl2jhwgafcxzr7324s88g",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#event-listener@2.5.3": "1q4w3pndc518crld6zsqvvpy9lkzwahp2zgza9kbzmmqh9gif1h2",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#event-listener@5.3.1": "1fkm6q4hjn61wl52xyqyyxai0x9w0ngrzi0wf1qsf8vhsadvwck0",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#exr@1.72.0": "195iviimjnp1mdkqrq8hjrfkr0qavpp1p8pq5qvaksa30pv96zc8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#fastrand@2.1.1": "19nyzdq3ha4g173364y2wijmd6jlyms8qx40daqkxsnl458jmh78",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#fdeflate@0.3.5": "1axmgzpgf12yl3x9ymdslqza765la17j17ljv6a4kc143a90y2fq",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#field-offset@0.3.6": "0zq5sssaa2ckmcmxxbly8qgz3sxpb8g1lwv90sdh1z74qif2gqiq",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#fixed@1.28.0": "0nn85j5x8yzx10q49jdzia4yp6pnasnxpnwh0p9aqr7qkfwf1il5",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#flate2@1.0.34": "1w1nf2ap4q1sq1v6v951011wcvljk449ap7q7jnnjf8hvjs8kdd1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#fluent-bundle@0.15.3": "14zl0cjn361is69pb1zry4k2zzh5nzsfv0iz05wccl00x0ga5q3z",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#fluent-langneg@0.13.0": "152yxplc11vmxkslvmaqak9x86xnavnhdqyhrh38ym37jscd0jic",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#fluent-syntax@0.11.1": "0gd3cdvsx9ymbb8hijcsc9wyf8h1pbcbpsafg4ldba56ji30qlra",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#fluent@0.16.1": "0njmdpwz52yjzyp55iik9k6vrixqiy7190d98pk0rgdy0x3n6x5v",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#flume@0.11.0": "10girdbqn77wi802pdh55lwbmymy437k7kklnvj12aaiwaflbb2m",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#fnv@1.0.7": "1hc2mcqha06aibcaza94vbi81j6pr9a1bbxrxjfhc91zin8yr7iz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#foreign-types-shared@0.1.1": "0jxgzd04ra4imjv8jgkmdq59kj8fsz6w4zxsbmlai34h26225c00",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#foreign-types@0.3.2": "1cgk0vyd7r45cj769jym4a6s7vwshvd0z4bqrb92q1fwibmkkwzn",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#form_urlencoded@1.2.1": "0milh8x7nl4f450s3ddhg57a3flcv6yq8hlkyk6fyr3mcb128dp1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#fuchsia-cprng@0.1.1": "1fnkqrbz7ixxzsb04bsz9p0zzazanma8znfdqjvh39n14vapfvx0",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures-channel@0.3.31": "040vpqpqlbk099razq8lyn74m0f161zd0rp36hciqrwcg2zibzrd",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures-core@0.3.31": "0gk6yrxgi5ihfanm2y431jadrll00n5ifhnpx090c2f2q1cr1wh5",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures-executor@0.3.31": "17vcci6mdfzx4gbk0wx64chr2f13wwwpvyf3xd5fb1gmjzcx2a0y",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures-intrusive@0.5.0": "0vwm08d1pli6bdaj0i7xhk3476qlx4pll6i0w03gzdnh7lh0r4qx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures-io@0.3.31": "1ikmw1yfbgvsychmsihdkwa8a1knank2d9a8dk01mbjar9w1np4y",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures-lite@2.3.0": "19gk4my8zhfym6gwnpdjiyv2hw8cc098skkbkhryjdaf0yspwljj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures-macro@0.3.31": "0l1n7kqzwwmgiznn0ywdc5i24z72zvh9q1dwps54mimppi7f6bhn",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures-sink@0.3.31": "1xyly6naq6aqm52d5rh236snm08kw8zadydwqz8bip70s6vzlxg5",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures-task@0.3.31": "124rv4n90f5xwfsm9qw6y99755y021cmi5dhzh253s920z77s3zr",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures-util@0.3.31": "10aa1ar8bgkgbr4wzxlidkqkcxf77gffyj8j7768h831pcaq784z",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures@0.3.31": "0xh8ddbkm9jy8kc5gbvjp9a4b6rqqxvc8471yb2qaz5wm2qhgg35",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gdk-pixbuf-sys@0.18.0": "1xya543c4ffd2n7aiwwrdxsyc9casdbasafi6ixcknafckm3k61z",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gdk-pixbuf@0.18.5": "1v7svvl0g7zybndmis5inaqqgi1mvcc6s1n8rkb31f5zn3qzbqah",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gdk4-sys@0.7.2": "1w7yvir565sjrrw828lss07749hfpfsr19jdjzwivkx36brl7ayv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gdk4@0.7.3": "1xiacc63p73apr033gjrb9dsk0y4yxnsljwfxbwfry41snd03nvy",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#generic-array@0.11.2": "0a7w8w0rg47nmcinnfzv443lcyb8mplwc251p1jyr5xj2yh6wzv6",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#generic-array@0.14.7": "16lyyrzrljfq424c3n8kfwkqihlimmsg5nhshbbp48np3yjrqr45",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#getrandom@0.2.15": "1mzlnrb3dgyd1fb84gvw10pyr8wdqdl4ry4sr64i1s8an66pqmn4",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gif@0.11.4": "01hbw3isapzpzff8l6aw55jnaqx2bcscrbwyf3rglkbbfp397p9y",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gif@0.13.1": "1whrkvdg26gp1r7f95c6800y6ijqw5y0z8rgj6xihpi136dxdciz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gimli@0.31.1": "0gvqc0ramx8szv76jhfd4dms0zyamvlg4whhiz11j34hh3dqxqh7",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gio-sys@0.18.1": "1lip8z35iy9d184x2qwjxlbxi64q9cpayy7v1p5y9xdsa3w6smip",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gio@0.18.4": "0wsc6mnx057s4ailacg99dwgna38dbqli5x7a6y9rdw75x9qzz6l",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#glib-build-tools@0.16.3": "1z73bl10zmxwrv16v4f5wcky1f3z5a2v0hknca54al4k2p5ka695",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#glib-build-tools@0.17.10": "05p7ab2vn8962cbchi7a6hndhvw64nqk4w5kpg5z53iizsgdfrbs",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#glib-build-tools@0.18.0": "0p5c2ayiam5bkp9wvq9f9ihwp06nqs5j801npjlwnhrl8rpwac9l",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#glib-macros@0.18.5": "1p5cla53fcp195zp0hkqpmnn7iwmkdswhy7xh34002bw8y7j5c0b",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#glib-sys@0.18.1": "164qhsfmlzd5mhyxs8123jzbdfldwxbikfpq5cysj3lddbmy4g06",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#glib@0.18.5": "1r8fw0627nmn19bgk3xpmcfngx3wkn7mcpq5a8ma3risx3valg93",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#glob@0.3.1": "16zca52nglanv23q5qrwd5jinw3d3as5ylya6y1pbx47vkxvrynj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gloo-timers@0.3.0": "1519157n7xppkk6pdw5w52vy1llzn5iljkqd7q1h5609jv7l7cdv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gobject-sys@0.18.0": "0i6fhp3m6vs3wkzyc22rk2cqj68qvgddxmpaai34l72da5xi4l08",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#graphene-rs@0.18.1": "00f4q1ra4haap5i7lazwhkdgnb49fs8adk2nm6ki6mjhl76jh8iv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#graphene-sys@0.18.1": "0n8zlg7z26lwpnvlqp1hjlgrs671skqwagdpm7r8i1zwx3748hfc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#grid@0.9.0": "0iswdcxggyxp9m1rz0m7bfg4xacinvn78zp2fgfp0l0079x10d06",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gsk4-sys@0.7.3": "0mbdlm9qi1hql48rr29vsj9vlqwc7gxg67wg1q19z67azwz9xg8j",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gsk4@0.7.3": "0zhzs2dkgiinhgc11akpn2harq3x5n1iq21dnc4h689g3lsqx58d",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gtk4-macros@0.7.2": "0bw3cchiycf7dw1bw4p8946gv38azxy05a5w0ndgcmxnz6fc8znm",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gtk4-sys@0.7.3": "1f2ylskyqkjdik9fij2m46pra4jagnif5xyalbxfk3334fmc9n2l",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gtk4@0.7.3": "0hh8nzglmz94v1m1h6vy8z12m6fr7ia467ry0md5fa4p7sm53sss",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#h2@0.3.26": "1s7msnfv7xprzs6xzfj5sg6p8bjcdpcqcmjjbkd345cyi1x55zl1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#half@2.4.1": "123q4zzw1x4309961i69igzd1wb7pj04aaii3kwasrz3599qrl3d",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hashbrown@0.14.5": "1wa1vy1xs3mp11bn3z9dv0jricgr6a2j0zkf1g19yz3vw4il89z5",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hashbrown@0.15.0": "1yx4xq091s7i6mw6bn77k8cp4jrpcac149xr32rg8szqsj27y20y",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hashlink@0.8.4": "1xy8agkyp0llbqk9fcffc1xblayrrywlyrm2a7v93x8zygm4y2g8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#headers-core@0.2.0": "0ab469xfpd411mc3dhmjhmzrhqikzyj8a17jn5bkj9zfpy0n9xp7",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#headers@0.3.9": "0w62gnwh2p1lml0zqdkrx9dp438881nhz32zrzdy61qa0a9kns06",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#heck@0.4.1": "1a7mqsnycv5z4z5vnv1k34548jzmc0ajic7c1j8jsaspnhw5ql4m",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#heck@0.5.0": "1sjmpsdl8czyh9ywl3qcsfsq9a307dg4ni2vnlwgnzzqhc4y0113",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hermit-abi@0.3.9": "092hxjbjnq5fmz66grd9plxd0sh6ssg5fhgwwwqbrzgzkjwdycfj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hermit-abi@0.4.0": "1k1zwllx6nfq417hy38x4akw1ivlv68ymvnzyxs76ffgsqcskxpv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hex-string@0.1.0": "02sgrgrbp693jv0v5iga7z47y6aj93cq0ia39finby9x17fw53l4",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hex@0.4.3": "0w1a4davm1lgzpamwnba907aysmlrnygbqmfis2mqjx5m552a93z",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hkdf@0.12.4": "1xxxzcarz151p1b858yn5skmhyrvn8fs4ivx5km3i1kjmnr8wpvv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hmac@0.12.1": "0pmbr069sfg76z7wsssfk5ddcqd9ncp79fyz6zcm6yn115yc6jbc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#home@0.5.9": "19grxyg35rqfd802pcc9ys1q3lafzlcjcv2pl2s5q8xpyr5kblg3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#http-body@0.4.6": "1lmyjfk6bqk6k9gkn1dxq770sb78pqbqshga241hr5p995bb5skw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#http@0.2.12": "1w81s4bcbmcj9bjp7mllm8jlz6b31wzvirz8bgpzbqkpwmbvn730",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#http@1.1.0": "0n426lmcxas6h75c2cp25m933pswlrfjz10v91vc62vib2sdvf91",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#httparse@1.9.5": "0ip9v8m9lvgvq1lznl31wvn0ch1v254na7lhid9p29yx9rbx6wbx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#httpdate@1.0.3": "1aa9rd2sac0zhjqh24c9xvir96g188zldkx0hr6dnnlx5904cfyz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#humantime@2.1.0": "1r55pfkkf5v0ji1x6izrjwdq9v6sc7bv99xj6srywcar37xmnfls",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hyper-tls@0.5.0": "01crgy13102iagakf6q4mb75dprzr7ps1gj0l5hxm1cvm7gks66n",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hyper@0.10.16": "0wwjh9p3mzvg3fss2lqz5r7ddcgl1fh9w6my2j69d6k0lbcm41ha",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hyper@0.14.30": "1jayxag79yln1nzyzx652kcy1bikgwssn6c4zrrp5v7s3pbdslm1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#iana-time-zone-haiku@0.1.2": "17r6jmj31chn7xs9698r122mapq85mfnv98bb4pg6spm0si2f67k",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#iana-time-zone@0.1.61": "085jjsls330yj1fnwykfzmb2f10zp6l7w4fhq81ng81574ghhpi3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#idna@0.1.5": "0kl4gs5kaydn4v07c6ka33spm9qdh2np0x7iw7g5zd8z1c7rxw1q",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#idna@0.5.0": "1xhjrcjqq0l5bpzvdgylvpkgk94panxgsirzhjnnqfdgc4a9nkb3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#image@0.23.14": "18gn2f7xp30pf9aqka877knlq308khxqiwjvsccvzaa4f9zcpzr4",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#image@0.24.9": "17gnr6ifnpzvhjf6dwbl9hki8x6bji5mwcqp0048x1jm5yfi742n",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#indent_write@2.2.0": "1hqjp80argdskrhd66g9sh542yxy8qi77j6rc69qd0l7l52rdzhc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#indexmap@2.6.0": "1nmrwn8lbs19gkvhxaawffzbvrpyrb5y3drcrr645x957kz0fybh",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#intl-memoizer@0.5.2": "1nkvql7c7b76axv4g68di1p2m9bnxq1cbn6mlqcawf72zhhf08py",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#intl_pluralrules@7.0.2": "0wprd3h6h8nfj62d8xk71h178q7zfn3srxm787w4sawsqavsg3h7",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#ipnet@2.10.1": "025p9wm94q1w2l13hbbr4cbmfygly3a2ag8g5s618l2jhq4l3hnx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#iron@0.6.1": "1s4mf8395f693nhwsr0znw3j5frzn56gzllypyl50il85p50ily6",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#is-terminal@0.4.13": "0jwgjjz33kkmnwai3nsdk1pz9vb6gkqvw1d1vq7bs3q48kinh7r6",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#is_terminal_polyfill@1.70.1": "1kwfgglh91z33kl0w5i338mfpa3zs0hidq5j4ny4rmjwrikchhvr",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#itertools@0.12.1": "0s95jbb3ndj1lvfxyq5wanc0fm0r6hg6q4ngb92qlfdxvci10ads",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.11": "0nv9cqjwzr3q58qz84dcz63ggc54yhf1yqar1m858m1kfd4g3wa9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#jpeg-decoder@0.1.22": "1wnh0bmmswpgwhgmlizz545x8334nlbmkq8imy9k224ri3am7792",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#jpeg-decoder@0.3.1": "1c1k53svpdyfhibkmm0ir5w0v3qmcmca8xr8vnnmizwf6pdagm7m",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#js-sys@0.3.70": "0yp3rz7vrn9mmqdpkds426r1p9vs6i8mkxx8ryqdfadr0s2q0s0q",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#kv-log-macro@1.0.7": "0zwp4bxkkp87rl7xy2dain77z977rvcry1gmr5bssdbn541v7s0d",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#language-tags@0.2.2": "16hrjdpa827carq5x4b8zhas24d8kg4s16m6nmmn1kb7cr5qh7d9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#lazy_static@1.5.0": "1zk6dqqni0193xg6iijh7i3i44sryglwgvx20spdvwk3r6sbrlmv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#lazycell@1.3.0": "0m8gw7dn30i0zjjpjdyf6pc16c34nl71lpv461mix50x3p70h3c3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#lebe@0.5.2": "1j2l6chx19qpa5gqcw434j83gyskq3g2cnffrbl3842ymlmpq203",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#libadwaita-sys@0.5.3": "16n6xsy6jhbj0jbpz8yvql6c9b89a99v9vhdz5s37mg1inisl42y",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#libadwaita@0.5.3": "174pzn9dwsk8ikvrhx13vkh0zrpvb3rhg9yd2q5d2zjh0q6fgrrg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#libc@0.2.159": "1i9xpia0hn1y8dws7all8rqng6h3lc8ymlgslnljcvm376jrf7an",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#libloading@0.8.5": "194dvczq4sifwkzllfmw0qkgvilpha7m5xy90gd6i446vcpz4ya9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#libm@0.2.8": "0n4hk1rs8pzw8hdfmwn96c4568s93kfxqgcqswr7sajd2diaihjf",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#libspa-sys@0.8.0": "07yh4i5grzbxkchg6dnxlwbdw2wm5jnd7ffbhl77jr0388b9f3dz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#libspa@0.8.0": "044qs48yl0llp2dmrgwxj9y1pgfy09i6fhq661zqqb9a3fwa9wv5",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#libsqlite3-sys@0.27.0": "05pp60ncrmyjlxxjj187808jkvpxm06w5lvvdwwvxd2qrmnj4kng",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#libyml@0.0.5": "106963pwg1gc3165bdlk8bbspmk919gk10vshhqglks3z8m700ik",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#linux-raw-sys@0.4.14": "12gsjgbhhjwywpqcrizv80vrp7p7grsz5laqq773i33wphjsxcvq",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#lock_api@0.4.12": "05qvxa6g27yyva25a5ghsg85apdxkvr77yhkyhapj6r8vnf8pbq7",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#log@0.3.9": "0jq23hhn5h35k7pa8r7wqnsywji6x3wn1q5q7lif5q536if8v7p1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#log@0.4.22": "093vs0wkm1rgyykk7fjbqp2lwizbixac1w52gv109p5r4jh0p9x7",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#logger@0.4.0": "14xlxvkspcfnspjil0xi63qj5cybxn1hjmr5gq8m4v1g9k5p54bc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#matches@0.1.10": "1994402fq4viys7pjhzisj4wcw894l53g798kkm2y74laxk0jci5",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#md-5@0.10.6": "1kvq5rnpm4fzwmyv5nmnxygdhhb2369888a06gdc9pxyrzh7x7nq",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#memchr@2.7.4": "18z32bhxrax0fnjikv475z7ii718hq457qwmaryixfxsl2qrmjkq",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#memoffset@0.9.1": "12i17wh9a9plx869g7j4whf62xw68k5zd4k0k5nh6ys5mszid028",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#mime@0.2.6": "1q1s1ax1gaz8ld3513nvhidfwnik5asbs1ma3hp6inp5dn56nqms",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#mime@0.3.17": "16hkibgvb9klh0w0jk5crr5xv90l3wlf77ggymzjmvl1818vnxv8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#mime_guess@1.8.8": "18qcd5aa3363mb742y7lf39j7ha88pkzbv9ff2qidlsdxsjjjs91",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#mime_guess@2.0.5": "03jmg3yx6j39mg0kayf7w4a886dl3j15y8zs119zw01ccy74zi7p",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#minimal-lexical@0.2.1": "16ppc5g84aijpri4jzv14rvcnslvlpphbszc7zzp6vfkddf4qdb8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#miniz_oxide@0.3.7": "0dblrhgbm0wa8jjl8cjp81akaj36yna92df4z1h9b26n3spal7br",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#miniz_oxide@0.4.4": "0jsfv00hl5rmx1nijn59sr9jmjd4rjnjhh4kdjy8d187iklih9d9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#miniz_oxide@0.7.4": "024wv14aa75cvik7005s5y2nfc8zfidddbd7g55g7sjgnzfl18mq",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#miniz_oxide@0.8.0": "1wadxkg6a6z4lr7kskapj5d8pxlx7cp1ifw4daqnkzqjxych5n72",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#mio@1.0.2": "1v1cnnn44awxbcfm4zlavwgkvbyg7gp5zzjm8mqf1apkrwflvq40",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#modifier@0.1.0": "0n3fmgli1nsskl0whrfzm1gk0rmwwl6pw1q4nb9sqqmn5h8wkxa1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#multer@2.1.0": "1hjiphaypj3phqaj5igrzcia9xfmf4rr4ddigbh8zzb96k1bvb01",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#nary_tree@0.4.3": "1iqray1a716995l9mmvz5sfqrwg9a235bvrkpcn8bcqwjnwfv1pv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#native-tls@0.2.12": "0rkl65z70n7sy4d5w0qa99klg1hr43wx6kcprk4d2n9xr2r4wqd8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#nix@0.27.1": "0ly0kkmij5f0sqz35lx9czlbk6zpihb7yh1bsy4irzwfd2f4xc1f",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#no-std-compat@0.4.1": "132vrf710zsdp40yp1z3kgc2ss8pi0z4gmihsz3y7hl4dpd56f5r",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#nom@7.1.3": "0jha9901wxam390jcf5pfa0qqfrgh8li787jx2ip0yk5b8y9hwyj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#num-bigint-dig@0.8.4": "0lb12df24wgxxbspz4gw1sf1kdqwvpdcpwq4fdlwg4gj41c1k16w",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#num-conv@0.1.0": "1ndiyg82q73783jq18isi71a7mjh56wxrk52rlvyx0mi5z9ibmai",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#num-integer@0.1.46": "13w5g54a9184cqlbsq80rnxw4jj4s0d8wv75jsq5r2lms8gncsbr",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#num-iter@0.1.45": "1gzm7vc5g9qsjjl3bqk9rz1h6raxhygbrcpbfl04swlh0i506a8l",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#num-rational@0.3.2": "01sgiwny9iflyxh2xz02sak71v2isc3x608hfdpwwzxi3j5l5b0j",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19": "0h984rhdkkqd4ny9cif7y2azl3xdfb7768hb9irhpsch4q3gq787",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#num_cpus@1.16.0": "0hra6ihpnh06dvfvz9ipscys0xfqa9ca9hzp384d5m02ssvgqqa1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#object@0.36.5": "0gk8lhbs229c68lapq6w6qmnm4jkj48hrcw5ilfyswy514nhmpxf",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#once_cell@1.20.2": "0xb7rw1aqr7pa4z3b00y7786gyf8awx2gca3md73afy76dzgwq8j",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#openssl-macros@0.1.1": "173xxvfc63rr5ybwqwylsir0vq6xsj4kxiv4hmg4c3vscdmncj59",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#openssl-probe@0.1.5": "1kq18qm48rvkwgcggfkqq6pm948190czqc94d6bm2sir5hq1l0gz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#openssl-sys@0.9.103": "1mi9r5vbgqqwfa2nqlh2m0r1v5abhzjigfbi7ja0mx0xx7p8v7kz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#openssl@0.10.66": "1hfr9ffx67j455aqrmyys3c8l65ngbqrl5qi3v3fi8vhddwg8acm",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pango-sys@0.18.0": "1iaxalcaaj59cl9n10svh4g50v8jrc1a36kd7n9yahx8j7ikfrs3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pango@0.18.3": "1r5ygq7036sv7w32kp8yxr6vgggd54iaavh3yckanmq4xg0px8kw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#parking@2.2.1": "1fnfgmzkfpjd69v4j9x737b1k8pnn054bvzcn5dm3pkgq595d3gk",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#parking_lot@0.12.3": "09ws9g6245iiq8z975h8ycf818a66q3c6zv4b5h8skpm7hc1igzi",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#parking_lot_core@0.9.10": "1y3cf9ld9ijf7i4igwzffcn0xl16dxyn4c5bwgjck1dkgabiyh0y",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#parse-zoneinfo@0.3.1": "093cs8slbd6kyfi6h12isz0mnaayf5ha8szri1xrbqj4inqhaahz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#paste@1.0.15": "02pxffpdqkapy292harq6asfjvadgp1s005fip9ljfsn9fvxgh2p",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pem-rfc7468@0.7.0": "04l4852scl4zdva31c1z6jafbak0ni5pi0j38ml108zwzjdrrcw8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#percent-encoding@1.0.1": "0cgq08v1fvr6bs5fvy390cz830lq4fak8havdasdacxcw790s09i",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#percent-encoding@2.3.1": "0gi8wgx0dcy8rnv1kywdv98lwcx67hz0a0zwpib5v2i08r88y573",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#phf@0.11.2": "1p03rsw66l7naqhpgr1a34r9yzi1gv9jh16g3fsk6wrwyfwdiqmd",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#phf@0.7.24": "066xwv4dr6056a9adlkarwp4n94kbpwngbmd47ngm3cfbyw49nmk",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#phf_codegen@0.11.2": "0nia6h4qfwaypvfch3pnq1nd2qj64dif4a6kai3b7rjrsf49dlz8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#phf_codegen@0.7.24": "0zjiblicfm0nrmr2xxrs6pnf6zz2394wgch6dcbd8jijkq98agmh",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#phf_generator@0.11.2": "1c14pjyxbcpwkdgw109f7581cc5fa3fnkzdq1ikvx7mdq9jcrr28",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#phf_generator@0.7.24": "0qi62gxk3x3whrmw5c4i71406icqk11qmpgln438p6qm7k4lqdh9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#phf_shared@0.11.2": "0azphb0a330ypqx3qvyffal5saqnks0xvl8rj73jlk3qxxgbkz4h",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#phf_shared@0.7.24": "18371fla0vsj7d6d5rlfb747xbr2in11ar9vgv5qna72bnhp2kr3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pin-project-internal@1.1.7": "133mxf5vmvnvw4idw2y2lb5bxsza2xlyfl6psjy7mz3l12nmy3rw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pin-project-lite@0.2.14": "00nx3f04agwjlsmd3mc5rx5haibj2v8q9b52b0kwn63wcv4nz9mx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pin-project@1.1.7": "15cvflrzsgp1zbl5gv37al2r62nl8lc37xkfwf70ql3fji7gcmxy",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pin-utils@0.1.0": "117ir7vslsl2z1a7qzhws4pd01cg2d3338c47swjyvqv2n60v1wb",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#piper@0.2.4": "0rn0mjjm0cwagdkay77wgmz3sqf8fqmv9d9czm79mvr2yj8c9j4n",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pipewire-sys@0.8.0": "04hiy3rl8v3j2dfzp04gr7r8l5azzqqsvqdzwa7sipdij27ii7l4",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pipewire@0.8.0": "1nldg1hz4v0qr26lzdxqpvrac4zbc3pb6436sl392425bjx4brh8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pkcs1@0.7.5": "0zz4mil3nchnxljdfs2k5ab1cjqn7kq5lqp62n9qfix01zqvkzy8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pkcs8@0.10.2": "1dx7w21gvn07azszgqd3ryjhyphsrjrmq5mmz1fbxkj5g0vv4l7r",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pkg-config@0.3.31": "1wk6yp2phl91795ia0lwkr3wl4a9xkrympvhqq8cxk4d75hwhglm",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#plugin@0.2.6": "1q7nghkpvxxr168y2jnzh3w7qc9vfrby9n7ygy3xpj0bj71hsshs",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#png@0.16.8": "1ipl44q3vy4kvx6j296vk7d4v8gvcg203lrkvvixwixq1j98fciw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#png@0.17.14": "1w130qw3cngzppxk1yp3ls2pbw3f0spbzhkbarbnlnm06imd9yaj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#polling@3.7.3": "04b5zdgz0m9ydbzcr3f9a55749gqbj0y89d0nz9nrv0x636r09yc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#powerfmt@0.2.0": "14ckj2xdpkhv3h6l5sdmb9f1d57z8hbfpdldjc2vl5givq2y77j3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#ppv-lite86@0.2.20": "017ax9ssdnpww7nrl1hvqh2lzncpv04nnsibmnw9nxjnaqlpp5bp",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pretty_env_logger@0.5.0": "076w9dnvcpx6d3mdbkqad8nwnsynb7c8haxmscyrz7g3vga28mw6",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#proc-macro-crate@1.3.1": "069r1k56bvgk0f58dm5swlssfcp79im230affwk6d9ck20g04k3z",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#proc-macro-crate@2.0.2": "092x5acqnic14cw6vacqap5kgknq3jn4c6jij9zi6j85839jc3xh",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#proc-macro-error-attr@1.0.4": "0sgq6m5jfmasmwwy8x4mjygx5l7kp8s4j60bv25ckv2j1qc41gm1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#proc-macro-error@1.0.4": "1373bhxaf0pagd8zkyd03kkx6bchzf6g0dkwrwzsnal9z47lj9fs",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.89": "0vlq56v41dsj69pnk7lil7fxvbfid50jnzdn3xnr31g05mkb0fgi",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#proptest@1.5.0": "13gm7mphs95cw4gbgk5qiczkmr68dvcwhp58gmiz33dq2ccm3hml",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#qoi@0.4.1": "00c0wkb112annn2wl72ixyd78mf56p4lxkhlmsggx65l3v3n8vbz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#quick-error@1.2.3": "1q6za3v78hsspisc197bg3g7rpc989qycy8ypr8ap8igv10ikl51",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#quote@1.0.37": "1brklraw2g34bxy9y4q1nbrccn7bv36ylihv12c9vlcii55x7fdm",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand@0.3.23": "0v679h38pjjqj5h4md7v2slsvj6686qgcn7p9fbw3h43iwnk1b34",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand@0.4.6": "14qjfv3gggzhnma20k0sc1jf8y6pplsaq7n1j9ls5c8kf2wl0a2m",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand@0.6.5": "1jl4449jcl4wgmzld6ffwqj5gwxrp8zvx8w573g1z368qg6xlwbd",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand@0.8.5": "013l6931nn7gkc23jz5mm3qdhf93jjf0fg64nz2lp4i51qd8vbrl",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_chacha@0.1.1": "1vxwyzs4fy1ffjc8l00fsyygpiss135irjf7nyxgq2v0lqf3lvam",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_chacha@0.3.1": "123x2adin558xbhvqb8w4f6syjsdkmqff8cxwhmjacpsl1ihmhg6",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_core@0.3.1": "0jzdgszfa4bliigiy4hi66k7fs3gfwi2qxn8vik84ph77fwdwvvs",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_core@0.4.2": "1p09ynysrq1vcdlmcqnapq4qakl2yd1ng3kxh3qscpx09k2a6cww",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_core@0.6.4": "0b4j2v4cb5krak1pv6kakv4sz6xcwbrmy2zckc32hsigbrwy82zc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_hc@0.1.0": "1i0vl8q5ddvvy0x8hf1zxny393miyzxkwqnw31ifg6p0gdy6fh3v",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_isaac@0.1.1": "027flpjr4znx2csxk7gxb7vrf9c7y5mydmvg5az2afgisp4rgnfy",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_jitter@0.1.4": "16z387y46bfz3csc42zxbjq89vcr1axqacncvv8qhyy93p4xarhi",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_os@0.1.3": "0wahppm0s64gkr2vmhcgwc0lij37in1lgfxg5rbgqlz0l5vgcxbv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_pcg@0.1.2": "0i0bdla18a8x4jn1w0fxsbs3jg7ajllz6azmch1zw33r06dv1ydb",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_xorshift@0.1.1": "0p2x8nr00hricpi2m6ca5vysiha7ybnghz79yqhhx6sl4gkfkxyb",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_xorshift@0.3.0": "13vcag7gmqspzyabfl1gr9ykvxd2142q2agrj8dkyjmfqmgg4nyj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rayon-core@1.12.1": "1qpwim68ai5h0j7axa8ai8z0payaawv3id0lrgkqmapx7lx8fr8l",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rayon@1.10.0": "1ylgnzwgllajalr4v00y4kj22klq2jbwllm70aha232iah0sc65l",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rdrand@0.4.0": "1cjq0kwx1bk7jx3kzyciiish5gqsj7620dm43dc52sr8fzmm9037",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#redox_syscall@0.5.7": "07vpgfr6a04k0x19zqr1xdlqm6fncik3zydbdi3f5g3l5k7zwvcv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#regex-automata@0.4.8": "18wd530ndrmygi6xnz3sp345qi0hy2kdbsa89182nwbl6br5i1rn",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#regex-syntax@0.8.5": "0p41p3hj9ww7blnbwbj9h7rwxzxg0c1hvrdycgys8rxyhqqw859b",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#regex@1.11.0": "1n5imk7yxam409ik5nagsjpwqvbg3f0g0mznd5drf549x1g0w81q",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#remove_dir_all@0.5.3": "1rzqbsgkmr053bxxl04vmvsd1njyz0nxvly97aip6aa2cmb15k9s",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#reqwest@0.11.27": "0qjary4hpplpgdi62d2m0xvbn6lnzckwffm0rgkm2x51023m6ryx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rsa@0.9.6": "1z0d1aavfm0v4pv8jqmqhhvvhvblla1ydzlvwykpc3mkzhj523jx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rustc-demangle@0.1.24": "07zysaafgrkzy2rjgwqdj2a8qdpsm6zv6f5pgpk9x0lm40z9b6vi",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rustc-hash@1.1.0": "1qkc5khrmv5pqi5l5ca9p5nl5hs742cagrndhbrlk3dhlrx3zm08",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rustc_version@0.4.1": "14lvdsmr5si5qbqzrajgb6vfn69k0sfygrvfvr2mps26xwi3mjyg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rustix@0.38.37": "04b8f99c2g36gyggf4aphw8742k2b1vls3364n2z493whj5pijwa",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rustls-pemfile@1.0.4": "1324n5bcns0rnw6vywr5agff3rwfvzphi7rmbyzwnv6glkhclx0w",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rusty-fork@0.3.0": "0kxwq5c480gg6q0j3bg4zzyfh2kwmc3v2ba94jw8ncjc8mpcqgfb",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#ryu@1.0.18": "17xx2s8j1lln7iackzd9p0sv546vjq71i779gphjq923vjh5pjzk",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#safemem@0.3.3": "0wp0d2b2284lw11xhybhaszsczpbq1jbdklkxgifldcknmy3nw7g",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#schannel@0.1.26": "1hfip5mdwqcfnmrnkrq9d8zwy6bssmf6rfm2441nk83ghbjpn8h1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#scoped-tls@1.0.1": "15524h04mafihcvfpgxd8f4bgc3k95aclz8grjkg9a0rxcvn9kz1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#scoped_threadpool@0.1.9": "1a26d3lk40s9mrf4imhbik7caahmw2jryhhb6vqv6fplbbgzal8x",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#scopeguard@1.2.0": "0jcz9sd47zlsgcnm1hdw0664krxwb5gczlif4qngj2aif8vky54l",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#security-framework-sys@2.12.0": "1dml0lp9lrvvi01s011lyss5kzzsmakaamdwsxr0431jd4l2jjpa",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#security-framework@2.11.1": "00ldclwx78dm61v7wkach9lcx76awlrv0fdgjdwch4dmy12j4yw9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#self_cell@0.10.3": "0pci3zh23b7dg6jmlxbn8k4plb7hcg5jprd1qiz0rp04p1ilskp1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#self_cell@1.0.4": "0jki9brixzzy032d799xspz1gikc5n2w81w8q8yyn8w6jxpsjsfk",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#semver@1.0.23": "12wqpxfflclbq4dv8sa6gchdh92ahhwn4ci1ls22wlby3h57wsb1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#serde@0.9.15": "1bsla8l5xr9pp5sirkal6mngxcq6q961km88jvf339j5ff8j7dil",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#serde@1.0.210": "0flc0z8wgax1k4j5bf2zyq48bgzyv425jkd5w0i6wbh7f8j5kqy8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#serde_derive@1.0.210": "07yzy4wafk79ps0hmbqmsqh5xjna4pm4q57wc847bb8gl3nh4f94",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.128": "1n43nia50ybpcfmh3gcw4lcc627qsg9nyakzwgkk9pm10xklbxbg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#serde_spanned@0.6.8": "1q89g70azwi4ybilz5jb8prfpa575165lmrffd49vmcf76qpqq47",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#serde_urlencoded@0.7.1": "1zgklbdaysj3230xivihs30qi5vkhigg323a9m62k8jwf4a1qjfk",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#serde_yml@0.0.12": "1p8xwz4znd6fj962y22fdvvv16gb8c0hx4iv5hjplngiidcdvqjr",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#sha1@0.10.6": "1fnnxlfg08xhkmwf2ahv634as30l1i3xhlhkvxflmasi5nd85gz3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#sha2@0.10.8": "1j1x78zk9il95w9iv46dh9wm73r6xrgj32y6lzzw7bxws9dbfgbr",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#shlex@1.3.0": "0r1y6bv26c1scpxvhg2cabimrmwgbp4p3wy6syj9n0c4s3q2znhg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#signal-hook-registry@1.4.2": "1cb5akgq8ajnd5spyn587srvs4n26ryq0p78nswffwhv46sf1sd9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#signature@2.2.0": "1pi9hd5vqfr3q3k49k37z06p7gs5si0in32qia4mmr1dancr6m3p",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#simd-adler32@0.3.7": "1zkq40c3iajcnr5936gjp9jjh1lpzhy44p3dq3fiw75iwr1w2vfn",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#siphasher@0.2.3": "1b53m53l24lyhr505lwqzrpjyq5qfnic71mynrcfvm43rybf938b",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#siphasher@0.3.11": "03axamhmwsrmh0psdw3gf7c0zc4fyl5yjxfifz9qfka6yhkqid9q",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#slab@0.4.9": "0rxvsgir0qw5lkycrqgb1cxsvxzjv9bmx73bk5y42svnzfba94lg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#smallvec@1.13.2": "0rsw5samawl3wsw6glrsb127rx6sh89a8wyikicw6dkdcjd1lpiw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#snowflake@1.3.0": "1wadr7bxdxbmkbqkqsvzan6q1h3mxqpxningi3ss3v9jaav7n817",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#socket2@0.5.7": "070r941wbq76xpy039an4pyiy3rfj7mp7pvibf1rcri9njq5wc6f",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#spin@0.9.8": "0rvam5r0p3a6qhc18scqpvpgb3ckzyqxpgdfyjnghh8ja7byi039",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#spki@0.7.3": "17fj8k5fmx4w9mp27l970clrh5qa7r5sjdvbsln987xhb34dc7nr",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#sqlformat@0.2.6": "14470h40gn0f6jw9xxzbpwh5qy1fgvkhkfz8xjyzgi0cvf9kmfkv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#sqlx-core@0.7.4": "1xiyr35dq10sf7lq00291svcj9wbaaz1ihandjmrng9a6jlmkfi4",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#sqlx-macros-core@0.7.4": "1j7k0fw7n6pgabqnj6cbp8s3rmd3yvqr4chjj878cvd1m99yycsq",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#sqlx-macros@0.7.4": "09rih250868nfkax022y5dyk24a7qfw6scjy3sgalbzb8lihx92f",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#sqlx-mysql@0.7.4": "066lxhb80xgb8r5m2yy3a7ydjvp0b6wsk9s7whwfa83d46817lqy",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#sqlx-postgres@0.7.4": "0zjp30wj4n2f25dnb32vsg6jfpa3gw6dmfd0i5pr4kw91fw4x0kw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#sqlx-sqlite@0.7.4": "1ap0bb2hazbrdgd7mhnckdg9xcchx0k094di9gnhpnhlhh5fyi5j",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#sqlx@0.7.4": "1ahadprvyhjraq0c5712x3kdkp1gkwfm9nikrmcml2h03bzwr8n9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#stringprep@0.1.5": "1cb3jis4h2b767csk272zw92lc6jzfzvh8d6m1cd86yqjb9z6kbv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#strsim@0.11.1": "0kzvqlw8hxqb7y598w1s0hxlnmi84sg5vsipp3yg5na5d1rvba3x",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#subtle@2.6.1": "14ijxaymghbl1p0wql9cib5zlwiina7kall6w7g89csprkgbvhhk",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#syn@1.0.109": "0ds2if4600bd59wsv7jjgfkayfzy3hnazs394kz6zdkmna8l3dkj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#syn@2.0.79": "147mk4sgigmvsb9l8qzj199ygf0fgb0bphwdsghn8205pz82q4w9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#sync_wrapper@0.1.2": "0q01lyj0gr9a93n10nxsn8lwbzq97jqd6b768x17c8f7v7gccir0",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#system-configuration-sys@0.5.0": "1jckxvdr37bay3i9v52izgy52dg690x5xfg3hd394sv2xf4b2px7",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#system-configuration@0.5.1": "1rz0r30xn7fiyqay2dvzfy56cvaa3km74hnbz2d72p97bkf3lfms",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#system-deps@6.2.2": "0j93ryw031n3h8b0nfpj5xwh3ify636xmv8kxianvlyyipmkbrd3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#target-lexicon@0.12.16": "1cg3bnx1gdkdr5hac1hzxy64fhw4g7dqkd0n3dxy5lfngpr1mi31",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tempdir@0.3.7": "1n5n86zxpgd85y0mswrp5cfdisizq2rv3la906g6ipyc03xvbwhm",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tempfile@3.13.0": "0nyagmbd4v5g6nzfydiihcn6l9j1w9bxgzyca5lyzgnhcbyckwph",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#termcolor@1.4.1": "0mappjh3fj3p2nmrg4y7qv94rchwi9mzmgmfflr8p2awdj7lyy86",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#thiserror-impl@1.0.64": "1hvzmjx9iamln854l74qyhs0jl2pg3hhqzpqm9p8gszmf9v4x408",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#thiserror@1.0.64": "114s8lmssxl0c2480s671am88vzlasbaikxbvfv8pyqrq6mzh2nm",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tiff@0.6.1": "0ds48vs919ccxa3fv1www7788pzkvpg434ilqkq7sjb5dmqg8lws",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tiff@0.9.1": "0ghyxlz566dzc3scvgmzys11dhq2ri77kb8sznjakijlxby104xs",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#time-core@0.1.2": "1wx3qizcihw6z151hywfzzyd1y5dl804ydyxci6qm07vbakpr4pg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#time-macros@0.2.18": "1kqwxvfh2jkpg38fy673d6danh1bhcmmbsmffww3mphgail2l99z",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#time@0.1.45": "0nl0pzv9yf56djy8y5dx25nka5pr2q1ivlandb3d24pksgx7ly8v",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#time@0.3.36": "11g8hdpahgrf1wwl2rpsg5nxq3aj7ri6xr672v4qcij6cgjqizax",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tinystr@0.7.6": "0bxqaw7z8r2kzngxlzlgvld1r6jbnwyylyvyjbv1q71rvgaga5wi",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tinyvec@1.8.0": "0f5rf6a2wzyv6w4jmfga9iw7rp9fp5gf4d604xgjsf3d9wgqhpj4",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tinyvec_macros@0.1.1": "081gag86208sc3y6sdkshgw3vysm5d34p431dzw0bshz66ncng0z",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tokio-macros@2.4.0": "0lnpg14h1v3fh2jvnc8cz7cjf0m7z1xgkwfpcyy632g829imjgb9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tokio-native-tls@0.3.1": "1wkfg6zn85zckmv4im7mv20ca6b1vmlib5xwz9p7g19wjfmpdbmv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tokio-stream@0.1.16": "1wc65gprcsyzqlr0k091glswy96kph90i32gffi4ksyh03hnqkjg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tokio-tungstenite@0.21.0": "0f5wj0crsx74rlll97lhw0wk6y12nhdnqvmnjx002hjn08fmcfy8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tokio-util@0.7.12": "0spc0g4irbnf2flgag22gfii87avqzibwfm0si0d1g0k9ijw7rv1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tokio@1.40.0": "166rllhfkyqp0fs7sxn6crv74iizi4wzd3cvxkcpmlk52qip1c72",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#toml@0.8.2": "0g9ysjaqvm2mv8q85xpqfn7hi710hj24sd56k49wyddvvyq8lp8q",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#toml_datetime@0.6.3": "0jsy7v8bdvmzsci6imj8fzgd255fmy5fzp6zsri14yrry7i77nkw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#toml_edit@0.19.15": "08bl7rp5g6jwmfpad9s8jpw8wjrciadpnbaswgywpr9hv9qbfnqv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#toml_edit@0.20.2": "0f7k5svmxw98fhi28jpcyv7ldr2s3c867pjbji65bdxjpd44svir",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tower-service@0.3.3": "1hzfkvkci33ra94xjx64vv3pp0sq346w06fpkcdwjcid7zhvdycd",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tracing-attributes@0.1.27": "1rvb5dn9z6d0xdj14r403z0af0bbaqhg02hq4jc97g5wds6lqw1l",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tracing-core@0.1.32": "0m5aglin3cdwxpvbg6kz0r9r0k31j48n0kcfwsp6l49z26k3svf0",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.40": "1vv48dac9zgj9650pg2b4d0j3w6f3x9gbggf43scq5hrlysklln3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#traitobject@0.1.0": "0yb0n8822mr59j200fyr2fxgzzgqljyxflx9y8bdy3rlaqngilgg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#try-lock@0.2.5": "0jqijrrvm1pyq34zn1jmy2vihd4jcrjlvsh4alkjahhssjnsn8g4",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tungstenite@0.21.0": "1qaphb5kgwgid19p64grhv2b9kxy7f1059yy92l9kwrlx90sdwcy",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#type-map@0.5.0": "17qaga12nkankr7hi2mv43f4lnc78hg480kz6j9zmy4g0h28ddny",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#typeable@0.1.2": "11w8dywgnm32hb291izjvh4zjd037ccnkk77ahk63l913zwzc40l",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#typemap@0.3.3": "1xm1gbvz9qisj1l6d36hrl9pw8imr8ngs6qyanjnsad3h0yfcfv5",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#typenum@1.17.0": "09dqxv69m9lj9zvv6xw5vxaqx15ps0vxyy5myg33i0kbqvq0pzs2",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#typeshare-annotation@1.0.4": "0kx38ah6638pkqq5cac7nmvbg6x43v7fj5jgibla4lj8fv1dc5d6",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#typeshare@1.0.3": "11riglm8incm0vq7ciyd907w1sc6frfn7h7ab0yp8bkcnycp7w84",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unarray@0.1.4": "154smf048k84prsdgh09nkm2n0w0336v84jd4zikyn6v6jrqbspa",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unic-langid-impl@0.9.5": "1rckyn5wqd5h8jxhbzlbbagr459zkzg822r4k5n30jaryv0j4m0a",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unic-langid@0.9.5": "0i2s024frmpfa68lzy8y8vnb1rz3m9v0ga13f7h2afx7f8g9vp93",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unicase@1.4.2": "0cwazh4qsmm9msckjk86zc1z35xg7hjxjykrgjalzdv367w6aivz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unicase@2.7.0": "12gd74j79f94k4clxpf06l99wiv4p30wjr0qm04ihqk9zgdd9lpp",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unicode-bidi@0.3.17": "14vqdsnrm3y5anj6h5zz5s32w88crraycblb88d9k23k9ns7vcas",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.13": "1zm1xylzsdfvm2a5ib9li3g5pp7qnkv4amhspydvgbmd9k6mc6z9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unicode-normalization@0.1.24": "0mnrk809z3ix1wspcqy97ld5wxdb31f3xz6nsvg5qcv289ycjcsh",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unicode-properties@0.1.3": "1l3mbgzwz8g14xcs09p4ww3hjkjcf0i1ih13nsg72bhj8n5jl3z7",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unicode-segmentation@1.12.0": "14qla2jfx74yyb9ds3d2mpwpa4l4lzb9z57c6d2ba511458z5k7n",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unicode-width@0.1.14": "1bzn2zv0gp8xxbxbhifw778a7fc93pa6a1kj24jgg9msj07f7mkx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unicode_categories@0.1.1": "0kp1d7fryxxm7hqywbk88yb9d1avsam9sg76xh36k5qx2arj9v1r",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unsafe-any@0.4.2": "0zwwphsqkw5qaiqmjwngnfpv9ym85qcsyj7adip9qplzjzbn00zk",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#url@1.7.2": "0nim1c90mxpi9wgdw2xh8dqd72vlklwlzam436akcrhjac6pqknx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#url@2.5.2": "0v2dx50mx7xzl9454cl5qmpjnhkbahmn59gd3apyipbgyyylsy12",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#urlencoding@2.1.3": "1nj99jp37k47n0hvaz5fvz7z6jd0sb4ppvfy3nphr1zbnyixpy6s",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#utf-8@0.7.6": "1a9ns3fvgird0snjkd3wbdhwd3zdpc2h5gpyybrfr6ra5pkqxk09",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#utf8parse@0.2.2": "088807qwjq46azicqwbhlmzwrbkz7l4hpw43sdkdyyk524vdxaq6",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#uuid@0.4.0": "0cdj2v6v2yy3zyisij69waksd17cyir1n58kwyk1n622105wbzkw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#uuid@0.8.2": "1dy4ldcp7rnzjy56dxh7d2sgrcvn4q77y0a8r0a48946h66zjp5w",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#uuid@1.10.0": "0503gvp08dh5mnm3f0ffqgisj6x3mbs53dmnn1lm19pga43a1pw1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#value-bag@1.9.0": "00aij8p1n7vcggkb9nxpwx9g5nqzclrf7prd1wpi9c3sscvw312s",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#vcpkg@0.2.15": "09i4nf5y8lig6xgj3f7fyrvzd3nlaw4znrihw8psidvv5yk4xkdc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#version-compare@0.2.0": "12y9262fhjm1wp0aj3mwhads7kv0jz8h168nn5fb8b43nwf9abl5",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#version_check@0.1.5": "1pf91pvj8n6akh7w6j5ypka6aqz08b3qpzgs0ak2kjf4frkiljwi",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#version_check@0.9.5": "0nhhi4i5x89gm911azqbn7avs9mdacw2i3vcz3cnmz3mv4rqz4hb",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#wait-timeout@0.2.0": "1xpkk0j5l9pfmjfh1pi0i89invlavfrd9av5xp0zhxgb29dhy84z",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#want@0.3.1": "03hbfrnvqqdchb5kgxyavb9jabwza0dmh2vw5kg0dq8rxl57d9xz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#warp@0.3.7": "07137zd13lchy5hxpspd0hs6sl19b0fv2zc1chf02nwnzw1d4y23",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#wasi@0.10.0+wasi-snapshot-preview1": "07y3l8mzfzzz4cj09c8y90yak4hpsi9g7pllyzpr6xvwrabka50s",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#wasi@0.11.0+wasi-snapshot-preview1": "08z4hxwkpdpalxjps1ai9y7ihin26y9f476i53dv98v45gkqg3cw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#wasite@0.1.0": "0nw5h9nmcl4fyf4j5d4mfdjfgvwi1cakpi349wc4zrr59wxxinmq",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-backend@0.2.93": "0yypblaf94rdgqs5xw97499xfwgs1096yx026d6h88v563d9dqwx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-futures@0.4.43": "1vf8kmaj95xn5893y1bdlav47y5niq85q5bms9pfj8d6cc7k1sb1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-macro-support@0.2.93": "0dp8w6jmw44srym6l752nkr3hkplyw38a2fxz5f3j1ch9p3l1hxg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-macro@0.2.93": "1kycd1xfx4d9xzqknvzbiqhwb5fzvjqrrn88x692q1vblj8lqp2q",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-shared@0.2.93": "1104bny0hv40jfap3hp8jhs0q4ya244qcrvql39i38xlghq0lan6",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen@0.2.93": "1dfr7pka5kwvky2fx82m9d060p842hc5fyyw8igryikcdb0xybm8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#web-sys@0.3.70": "1h1jspkqnrx1iybwhwhc3qq8c8fn4hy5jcf0wxjry4mxv6pymz96",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#weezl@0.1.8": "10lhndjgs6y5djpg3b420xngcr6jkmv70q8rb1qcicbily35pa2k",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#whoami@1.5.2": "0vdvm6sga4v9515l6glqqfnmzp246nq66dd09cw5ri4fyn3mnb9p",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#winapi-i686-pc-windows-gnu@0.4.0": "1dmpa6mvcvzz16zg6d5vrfy4bxgg541wxrcip7cnshi06v38ffxc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#winapi-util@0.1.9": "1fqhkcl9scd230cnfj8apfficpf5c9vhwnk4yy9xfc1sw69iq8ng",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#winapi-x86_64-pc-windows-gnu@0.4.0": "0gqq64czqb64kskjryj8isp62m2sgvx25yyj3kpc2myh85w24bki",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#winapi@0.3.9": "06gl025x418lchw1wxj64ycr7gha83m44cjr5sarhynd9xkrm0sw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows-core@0.52.0": "1nc3qv7sy24x0nlnb32f7alzpd6f72l4p24vl65vydbyil669ark",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows-sys@0.48.0": "1aan23v5gs7gya1lc46hqn9mdh8yph3fhxmhxlw36pn6pqc28zb7",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows-sys@0.52.0": "0gd3v4ji88490zgb6b5mq5zgbvwv7zx1ibn8v3x83rwcdbryaar8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows-sys@0.59.0": "0fw5672ziw8b3zpmnbp9pdv1famk74f1l9fcbc3zsrzdg56vqf0y",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows-targets@0.48.5": "034ljxqshifs1lan89xwpcy1hp0lhdh4b5n0d2z4fwjx2piacbws",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows-targets@0.52.6": "0wwrx625nwlfp7k93r2rra568gad1mwd888h1jwnl0vfg5r4ywlv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_aarch64_gnullvm@0.48.5": "1n05v7qblg1ci3i567inc7xrkmywczxrs1z3lj3rkkxw18py6f1b",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_aarch64_gnullvm@0.52.6": "1lrcq38cr2arvmz19v32qaggvj8bh1640mdm9c2fr877h0hn591j",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_aarch64_msvc@0.48.5": "1g5l4ry968p73g6bg6jgyvy9lb8fyhcs54067yzxpcpkf44k2dfw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_aarch64_msvc@0.52.6": "0sfl0nysnz32yyfh773hpi49b1q700ah6y7sacmjbqjjn5xjmv09",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_i686_gnu@0.48.5": "0gklnglwd9ilqx7ac3cn8hbhkraqisd0n83jxzf9837nvvkiand7",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_i686_gnu@0.52.6": "02zspglbykh1jh9pi7gn8g1f97jh1rrccni9ivmrfbl0mgamm6wf",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_i686_gnullvm@0.52.6": "0rpdx1537mw6slcpqa0rm3qixmsb79nbhqy5fsm3q2q9ik9m5vhf",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_i686_msvc@0.48.5": "01m4rik437dl9rdf0ndnm2syh10hizvq0dajdkv2fjqcywrw4mcg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_i686_msvc@0.52.6": "0rkcqmp4zzmfvrrrx01260q3xkpzi6fzi2x2pgdcdry50ny4h294",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_gnu@0.48.5": "13kiqqcvz2vnyxzydjh73hwgigsdr2z1xpzx313kxll34nyhmm2k",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_gnu@0.52.6": "0y0sifqcb56a56mvn7xjgs8g43p33mfqkd8wj1yhrgxzma05qyhl",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_gnullvm@0.48.5": "1k24810wfbgz8k48c2yknqjmiigmql6kk3knmddkv8k8g1v54yqb",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_gnullvm@0.52.6": "03gda7zjx1qh8k9nnlgb7m3w3s1xkysg55hkd1wjch8pqhyv5m94",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_msvc@0.48.5": "0f4mdp895kkjh9zv8dxvn4pc10xr7839lf5pa9l0193i2pkgr57d",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_msvc@0.52.6": "1v7rb5cibyzx8vak29pdrk8nx9hycsjs4w0jgms08qk49jl6v7sq",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#winnow@0.5.40": "0xk8maai7gyxda673mmw3pj1hdizy5fpi7287vaywykkk19sk4zm",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#winreg@0.50.0": "1cddmp929k882mdh6i9f2as848f13qqna6czwsqzkh1pqnr5fkjj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#yansi-term@0.1.2": "1w8vjlvxba6yvidqdvxddx3crl6z66h39qxj8xi6aqayw2nk0p7y",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#zerocopy-derive@0.7.35": "0gnf2ap2y92nwdalzz3x7142f2b83sni66l39vxp2ijd6j080kzs",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#zerocopy@0.7.35": "1w36q7b9il2flg0qskapgi9ymgg7p985vniqd09vi0mwib8lz6qv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#zeroize@1.8.1": "1pjdrmjwmszpxfd7r860jx54cyk94qk59x13sc307cvr5256glyf",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#zune-inflate@0.2.54": "00kg24jh3zqa3i6rg6yksnb71bch9yi1casqydl00s7nw8pk7avk"
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
[package]
|
||||||
|
name = "cyber-slides"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-std = "1.13.0"
|
||||||
|
cairo-rs = "0.18"
|
||||||
|
cyberpunk = { path = "../cyberpunk" }
|
||||||
|
gio = "0.18"
|
||||||
|
glib = "0.18"
|
||||||
|
gtk = { version = "0.7", package = "gtk4" }
|
||||||
|
serde = { version = "1.0.210", features = ["derive"] }
|
||||||
|
serde_yml = "0.0.12"
|
|
@ -0,0 +1,416 @@
|
||||||
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
collections::HashMap,
|
||||||
|
fs::File,
|
||||||
|
io::Read,
|
||||||
|
ops::Index,
|
||||||
|
path::Path,
|
||||||
|
rc::Rc,
|
||||||
|
sync::{Arc, RwLock},
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
use cairo::{Context, Rectangle};
|
||||||
|
use cyberpunk::{AsymLine, AsymLineCutout, GlowPen, Pen, Text};
|
||||||
|
use glib::{GString, Object};
|
||||||
|
use gtk::{
|
||||||
|
glib::{self, Propagation},
|
||||||
|
prelude::*,
|
||||||
|
subclass::prelude::*,
|
||||||
|
EventControllerKey,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
const FPS: u64 = 60;
|
||||||
|
const PURPLE: (f64, f64, f64) = (0.7, 0., 1.);
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
enum Position {
|
||||||
|
Top,
|
||||||
|
Middle,
|
||||||
|
Bottom,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
struct Step {
|
||||||
|
text: String,
|
||||||
|
position: Position,
|
||||||
|
transition: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
struct Script(Vec<Step>);
|
||||||
|
|
||||||
|
impl Script {
|
||||||
|
fn from_file(path: &Path) -> Result<Script, serde_yml::Error> {
|
||||||
|
let mut buf: Vec<u8> = Vec::new();
|
||||||
|
let mut f = File::open(path).unwrap();
|
||||||
|
f.read_to_end(&mut buf).unwrap();
|
||||||
|
let script = serde_yml::from_slice(&buf)?;
|
||||||
|
Ok(Self(script))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn iter<'a>(&'a self) -> impl Iterator<Item = &'a Step> {
|
||||||
|
self.0.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
self.0.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Script {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(vec![])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Index<usize> for Script {
|
||||||
|
type Output = Step;
|
||||||
|
|
||||||
|
fn index(&self, index: usize) -> &Self::Output {
|
||||||
|
&self.0[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Fade {
|
||||||
|
text: String,
|
||||||
|
position: Position,
|
||||||
|
duration: Duration,
|
||||||
|
|
||||||
|
start_time: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
trait Animation {
|
||||||
|
fn position(&self) -> Position;
|
||||||
|
|
||||||
|
fn tick(&self, now: Instant, context: &Context, width: f64);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Animation for Fade {
|
||||||
|
fn position(&self) -> Position {
|
||||||
|
self.position.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tick(&self, now: Instant, context: &Context, width: f64) {
|
||||||
|
let total_frames = self.duration.as_secs() * FPS;
|
||||||
|
let alpha_rate: f64 = 1. / total_frames as f64;
|
||||||
|
|
||||||
|
let frames = (now - self.start_time).as_secs_f64() * FPS as f64;
|
||||||
|
let alpha = alpha_rate * frames as f64;
|
||||||
|
|
||||||
|
let text_display = Text::new(self.text.clone(), context, 64., width);
|
||||||
|
let _ = context.move_to(0., text_display.extents().height());
|
||||||
|
let _ = context.set_source_rgba(PURPLE.0, PURPLE.1, PURPLE.2, alpha);
|
||||||
|
text_display.draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CrossFade {
|
||||||
|
old_text: String,
|
||||||
|
new_text: String,
|
||||||
|
position: Position,
|
||||||
|
duration: Duration,
|
||||||
|
|
||||||
|
start_time: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Animation for CrossFade {
|
||||||
|
fn position(&self) -> Position {
|
||||||
|
self.position.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tick(&self, now: Instant, context: &Context, width: f64) {
|
||||||
|
let total_frames = self.duration.as_secs() * FPS;
|
||||||
|
let alpha_rate: f64 = 1. / total_frames as f64;
|
||||||
|
|
||||||
|
let frames = (now - self.start_time).as_secs_f64() * FPS as f64;
|
||||||
|
let alpha = alpha_rate * frames as f64;
|
||||||
|
|
||||||
|
let text_display = Text::new(self.old_text.clone(), context, 64., width);
|
||||||
|
let _ = context.move_to(0., text_display.extents().height());
|
||||||
|
let _ = context.set_source_rgba(PURPLE.0, PURPLE.1, PURPLE.2, 1. - alpha);
|
||||||
|
text_display.draw();
|
||||||
|
|
||||||
|
let text_display = Text::new(self.new_text.clone(), context, 64., width);
|
||||||
|
let _ = context.move_to(0., text_display.extents().height());
|
||||||
|
let _ = context.set_source_rgba(PURPLE.0, PURPLE.1, PURPLE.2, alpha);
|
||||||
|
text_display.draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CyberScreenState {
|
||||||
|
script: Script,
|
||||||
|
idx: Option<usize>,
|
||||||
|
top: Option<Step>,
|
||||||
|
middle: Option<Step>,
|
||||||
|
bottom: Option<Step>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CyberScreenState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
script: Script(vec![]),
|
||||||
|
idx: None,
|
||||||
|
top: None,
|
||||||
|
middle: None,
|
||||||
|
bottom: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CyberScreenState {
|
||||||
|
fn new(script: Script) -> CyberScreenState {
|
||||||
|
let mut s = CyberScreenState::default();
|
||||||
|
s.script = script;
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_page(&mut self) -> Box<dyn Animation> {
|
||||||
|
let idx = match self.idx {
|
||||||
|
None => 0,
|
||||||
|
Some(idx) => {
|
||||||
|
if idx < self.script.len() {
|
||||||
|
idx + 1
|
||||||
|
} else {
|
||||||
|
idx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.idx = Some(idx);
|
||||||
|
let step = self.script[idx].clone();
|
||||||
|
|
||||||
|
let (old, new) = match step.position {
|
||||||
|
Position::Top => {
|
||||||
|
let old = self.top.replace(step.clone());
|
||||||
|
(old, step)
|
||||||
|
}
|
||||||
|
Position::Middle => {
|
||||||
|
let old = self.middle.replace(step.clone());
|
||||||
|
(old, step)
|
||||||
|
}
|
||||||
|
Position::Bottom => {
|
||||||
|
let old = self.bottom.replace(step.clone());
|
||||||
|
(old, step)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match old {
|
||||||
|
Some(old) => Box::new(CrossFade {
|
||||||
|
old_text: old.text.clone(),
|
||||||
|
new_text: new.text.clone(),
|
||||||
|
position: new.position,
|
||||||
|
duration: new.transition,
|
||||||
|
start_time: Instant::now(),
|
||||||
|
}),
|
||||||
|
None => Box::new(Fade {
|
||||||
|
text: new.text.clone(),
|
||||||
|
position: new.position,
|
||||||
|
duration: new.transition,
|
||||||
|
start_time: Instant::now(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct CyberScreenPrivate {
|
||||||
|
state: Rc<RefCell<CyberScreenState>>,
|
||||||
|
// For crossfading to work, I have to detect that there is an old animation in a position, and
|
||||||
|
// replace it with the new one.
|
||||||
|
animations: Rc<RefCell<HashMap<Position, Box<dyn Animation>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for CyberScreenPrivate {
|
||||||
|
const NAME: &'static str = "CyberScreen";
|
||||||
|
type Type = CyberScreen;
|
||||||
|
type ParentType = gtk::DrawingArea;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for CyberScreenPrivate {}
|
||||||
|
impl WidgetImpl for CyberScreenPrivate {}
|
||||||
|
impl DrawingAreaImpl for CyberScreenPrivate {}
|
||||||
|
|
||||||
|
impl CyberScreenPrivate {
|
||||||
|
fn set_script(&self, script: Script) {
|
||||||
|
*self.state.borrow_mut() = CyberScreenState::new(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_page(&self) {
|
||||||
|
let transition = self.state.borrow_mut().next_page();
|
||||||
|
self.animations
|
||||||
|
.borrow_mut()
|
||||||
|
.insert(transition.position(), transition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct CyberScreen(ObjectSubclass<CyberScreenPrivate>) @extends gtk::DrawingArea, gtk::Widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CyberScreen {
|
||||||
|
fn new(script: Script) -> Self {
|
||||||
|
let s: Self = Object::builder().build();
|
||||||
|
s.imp().set_script(script);
|
||||||
|
|
||||||
|
s.set_draw_func({
|
||||||
|
let s = s.clone();
|
||||||
|
move |_, context, width, height| {
|
||||||
|
let now = Instant::now();
|
||||||
|
let _ = context.set_source_rgb(0., 0., 0.);
|
||||||
|
let _ = context.paint();
|
||||||
|
|
||||||
|
let pen = GlowPen::new(width, height, 2., 8., (0.7, 0., 1.));
|
||||||
|
AsymLineCutout {
|
||||||
|
orientation: gtk::Orientation::Horizontal,
|
||||||
|
start_x: 25.,
|
||||||
|
start_y: height as f64 / 7.,
|
||||||
|
start_length: width as f64 / 3.,
|
||||||
|
cutout_length: width as f64 / 3. - 100.,
|
||||||
|
height: 50.,
|
||||||
|
end_length: width as f64 / 3. - 50.,
|
||||||
|
invert: false,
|
||||||
|
}
|
||||||
|
.draw(&pen);
|
||||||
|
pen.stroke();
|
||||||
|
|
||||||
|
AsymLine {
|
||||||
|
orientation: gtk::Orientation::Horizontal,
|
||||||
|
start_x: width as f64 / 4.,
|
||||||
|
start_y: height as f64 * 6. / 7.,
|
||||||
|
start_length: width as f64 * 2. / 3. - 25.,
|
||||||
|
height: 50.,
|
||||||
|
end_length: 0.,
|
||||||
|
invert: false,
|
||||||
|
}
|
||||||
|
.draw(&pen);
|
||||||
|
pen.stroke();
|
||||||
|
|
||||||
|
let tracery = pen.finish();
|
||||||
|
let _ = context.set_source(tracery);
|
||||||
|
let _ = context.paint();
|
||||||
|
|
||||||
|
let mut animations = s.imp().animations.borrow_mut();
|
||||||
|
|
||||||
|
let lr_margin = 50.;
|
||||||
|
let max_width = width as f64 - lr_margin * 2.;
|
||||||
|
let region_height = height as f64 / 5.;
|
||||||
|
|
||||||
|
if let Some(animation) = animations.get(&Position::Top) {
|
||||||
|
let y = height as f64 * 1. / 5.;
|
||||||
|
let surface = context
|
||||||
|
.target()
|
||||||
|
.create_for_rectangle(Rectangle::new(20., y, max_width, region_height))
|
||||||
|
.unwrap();
|
||||||
|
let ctx = Context::new(&surface).unwrap();
|
||||||
|
animation.tick(now, &ctx, max_width);
|
||||||
|
}
|
||||||
|
if let Some(animation) = animations.get(&Position::Middle) {
|
||||||
|
let y = height as f64 * 2. / 5.;
|
||||||
|
let surface = context
|
||||||
|
.target()
|
||||||
|
.create_for_rectangle(Rectangle::new(20., y, max_width, region_height))
|
||||||
|
.unwrap();
|
||||||
|
let ctx = Context::new(&surface).unwrap();
|
||||||
|
animation.tick(now, &ctx, max_width);
|
||||||
|
}
|
||||||
|
if let Some(animation) = animations.get(&Position::Bottom) {
|
||||||
|
let y = height as f64 * 3. / 5.;
|
||||||
|
let surface = context
|
||||||
|
.target()
|
||||||
|
.create_for_rectangle(Rectangle::new(20., y, max_width, region_height))
|
||||||
|
.unwrap();
|
||||||
|
let ctx = Context::new(&surface).unwrap();
|
||||||
|
animation.tick(now, &ctx, max_width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_page(&self) {
|
||||||
|
self.imp().next_page();
|
||||||
|
self.queue_draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let script = Arc::new(RwLock::new(Script::default()));
|
||||||
|
let app = gtk::Application::builder()
|
||||||
|
.application_id("com.luminescent-dreams.cyberpunk-slideshow")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
app.add_main_option(
|
||||||
|
"script",
|
||||||
|
glib::char::Char::from(b's'),
|
||||||
|
glib::OptionFlags::IN_MAIN,
|
||||||
|
glib::OptionArg::String,
|
||||||
|
"",
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
app.connect_handle_local_options({
|
||||||
|
let script = script.clone();
|
||||||
|
move |_, options| {
|
||||||
|
if let Some(script_path) = options.lookup::<String>("script").unwrap() {
|
||||||
|
let mut script = script.write().unwrap();
|
||||||
|
*script = Script::from_file(Path::new(&script_path)).unwrap();
|
||||||
|
-1
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.connect_activate(move |app| {
|
||||||
|
let window = gtk::ApplicationWindow::new(app);
|
||||||
|
let screen = CyberScreen::new(script.read().unwrap().clone());
|
||||||
|
|
||||||
|
let events = EventControllerKey::new();
|
||||||
|
|
||||||
|
events.connect_key_released({
|
||||||
|
let app = app.clone();
|
||||||
|
let window = window.clone();
|
||||||
|
let screen = screen.clone();
|
||||||
|
move |_, key, _, _| {
|
||||||
|
let name = key
|
||||||
|
.name()
|
||||||
|
.map(|s| s.as_str().to_owned())
|
||||||
|
.unwrap_or("".to_owned());
|
||||||
|
match name.as_ref() {
|
||||||
|
"Right" => screen.next_page(),
|
||||||
|
"q" => app.quit(),
|
||||||
|
"Escape" => window.unfullscreen(),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.add_controller(events);
|
||||||
|
|
||||||
|
window.set_child(Some(&screen));
|
||||||
|
window.set_width_request(800);
|
||||||
|
window.set_height_request(600);
|
||||||
|
window.present();
|
||||||
|
|
||||||
|
window.connect_maximized_notify(|window| {
|
||||||
|
window.fullscreen();
|
||||||
|
});
|
||||||
|
|
||||||
|
let _ = glib::spawn_future_local({
|
||||||
|
let screen = screen.clone();
|
||||||
|
async move {
|
||||||
|
loop {
|
||||||
|
screen.queue_draw();
|
||||||
|
async_std::task::sleep(Duration::from_millis(1000 / FPS)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.run();
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ license = "GPL-3.0-only"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
cairo-rs = { version = "0.18" }
|
cairo-rs = { version = "0.18" }
|
||||||
|
cyberpunk = { path = "../cyberpunk" }
|
||||||
gio = { version = "0.18" }
|
gio = { version = "0.18" }
|
||||||
glib = { version = "0.18" }
|
glib = { version = "0.18" }
|
||||||
gtk = { version = "0.7", package = "gtk4" }
|
gtk = { version = "0.7", package = "gtk4" }
|
||||||
|
|
|
@ -2,6 +2,7 @@ use cairo::{
|
||||||
Context, FontSlant, FontWeight, Format, ImageSurface, LineCap, LinearGradient, Pattern,
|
Context, FontSlant, FontWeight, Format, ImageSurface, LineCap, LinearGradient, Pattern,
|
||||||
TextExtents,
|
TextExtents,
|
||||||
};
|
};
|
||||||
|
use cyberpunk::{AsymLine, AsymLineCutout, GlowPen, Pen, SlashMeter};
|
||||||
use glib::Object;
|
use glib::Object;
|
||||||
use gtk::{prelude::*, subclass::prelude::*, EventControllerKey};
|
use gtk::{prelude::*, subclass::prelude::*, EventControllerKey};
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -171,7 +172,7 @@ impl SplashPrivate {
|
||||||
start_y: extents.height() + 10.,
|
start_y: extents.height() + 10.,
|
||||||
start_length: 0.,
|
start_length: 0.,
|
||||||
height: extents.height() / 2.,
|
height: extents.height() / 2.,
|
||||||
total_length: extents.width() + extents.height() / 2.,
|
end_length: 0.,
|
||||||
invert: false,
|
invert: false,
|
||||||
}
|
}
|
||||||
.draw(&pen);
|
.draw(&pen);
|
||||||
|
@ -183,7 +184,7 @@ impl SplashPrivate {
|
||||||
start_y: extents.height() + 60.,
|
start_y: extents.height() + 60.,
|
||||||
start_length: extents.width(),
|
start_length: extents.width(),
|
||||||
height: extents.height() / 2.,
|
height: extents.height() / 2.,
|
||||||
total_length: extents.width() + extents.height() / 2.,
|
end_length: 0.,
|
||||||
invert: false,
|
invert: false,
|
||||||
}
|
}
|
||||||
.draw(&pen);
|
.draw(&pen);
|
||||||
|
@ -208,7 +209,7 @@ impl SplashPrivate {
|
||||||
start_x: 20.,
|
start_x: 20.,
|
||||||
start_y: center_y - 20. - title_height / 2.,
|
start_y: center_y - 20. - title_height / 2.,
|
||||||
start_length,
|
start_length,
|
||||||
total_length: *self.width.borrow() as f64 - 120.,
|
end_length: *self.width.borrow() as f64 - 120. - start_length,
|
||||||
cutout_length: title_width,
|
cutout_length: title_width,
|
||||||
height: title_height,
|
height: title_height,
|
||||||
invert: false,
|
invert: false,
|
||||||
|
@ -243,7 +244,7 @@ impl SplashPrivate {
|
||||||
start_y: *self.height.borrow() as f64 / 2. + 100.,
|
start_y: *self.height.borrow() as f64 / 2. + 100.,
|
||||||
start_length: 400.,
|
start_length: 400.,
|
||||||
height: 50.,
|
height: 50.,
|
||||||
total_length: 650.,
|
end_length: 0.,
|
||||||
invert: true,
|
invert: true,
|
||||||
}
|
}
|
||||||
.draw(&pen);
|
.draw(&pen);
|
||||||
|
@ -258,7 +259,7 @@ impl SplashPrivate {
|
||||||
start_y: *self.height.borrow() as f64 / 2. + 200.,
|
start_y: *self.height.borrow() as f64 / 2. + 200.,
|
||||||
start_length: 600.,
|
start_length: 600.,
|
||||||
height: 50.,
|
height: 50.,
|
||||||
total_length: 650.,
|
end_length: 0.,
|
||||||
invert: false,
|
invert: false,
|
||||||
}
|
}
|
||||||
.draw(&pen);
|
.draw(&pen);
|
||||||
|
@ -419,212 +420,6 @@ impl Splash {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AsymLineCutout {
|
|
||||||
orientation: gtk::Orientation,
|
|
||||||
start_x: f64,
|
|
||||||
start_y: f64,
|
|
||||||
start_length: f64,
|
|
||||||
total_length: f64,
|
|
||||||
cutout_length: f64,
|
|
||||||
height: f64,
|
|
||||||
invert: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsymLineCutout {
|
|
||||||
fn draw(&self, pen: &impl Pen) {
|
|
||||||
let dodge = if self.invert {
|
|
||||||
self.height
|
|
||||||
} else {
|
|
||||||
-self.height
|
|
||||||
};
|
|
||||||
match self.orientation {
|
|
||||||
gtk::Orientation::Horizontal => {
|
|
||||||
pen.move_to(self.start_x, self.start_y);
|
|
||||||
pen.line_to(self.start_x + self.start_length, self.start_y);
|
|
||||||
pen.line_to(
|
|
||||||
self.start_x + self.start_length + self.height,
|
|
||||||
self.start_y + dodge,
|
|
||||||
);
|
|
||||||
pen.line_to(
|
|
||||||
self.start_x + self.start_length + self.height + self.cutout_length,
|
|
||||||
self.start_y + dodge,
|
|
||||||
);
|
|
||||||
pen.line_to(
|
|
||||||
self.start_x
|
|
||||||
+ self.start_length
|
|
||||||
+ self.height
|
|
||||||
+ self.cutout_length
|
|
||||||
+ (self.height / 2.),
|
|
||||||
self.start_y + dodge / 2.,
|
|
||||||
);
|
|
||||||
pen.line_to(self.total_length, self.start_y + dodge / 2.);
|
|
||||||
}
|
|
||||||
gtk::Orientation::Vertical => {
|
|
||||||
pen.move_to(self.start_x, self.start_y);
|
|
||||||
pen.line_to(self.start_x, self.start_y + self.start_length);
|
|
||||||
pen.line_to(
|
|
||||||
self.start_x + dodge,
|
|
||||||
self.start_y + self.start_length + self.height,
|
|
||||||
);
|
|
||||||
pen.line_to(
|
|
||||||
self.start_x + dodge,
|
|
||||||
self.start_y + self.start_length + self.height + self.cutout_length,
|
|
||||||
);
|
|
||||||
pen.line_to(
|
|
||||||
self.start_x + dodge / 2.,
|
|
||||||
self.start_y
|
|
||||||
+ self.start_length
|
|
||||||
+ self.height
|
|
||||||
+ self.cutout_length
|
|
||||||
+ (self.height / 2.),
|
|
||||||
);
|
|
||||||
pen.line_to(self.start_x + dodge / 2., self.total_length);
|
|
||||||
}
|
|
||||||
_ => panic!("unknown orientation"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AsymLine {
|
|
||||||
orientation: gtk::Orientation,
|
|
||||||
start_x: f64,
|
|
||||||
start_y: f64,
|
|
||||||
start_length: f64,
|
|
||||||
height: f64,
|
|
||||||
total_length: f64,
|
|
||||||
invert: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsymLine {
|
|
||||||
fn draw(&self, pen: &impl Pen) {
|
|
||||||
let dodge = if self.invert {
|
|
||||||
self.height
|
|
||||||
} else {
|
|
||||||
-self.height
|
|
||||||
};
|
|
||||||
match self.orientation {
|
|
||||||
gtk::Orientation::Horizontal => {
|
|
||||||
pen.move_to(self.start_x, self.start_y);
|
|
||||||
pen.line_to(self.start_x + self.start_length, self.start_y);
|
|
||||||
pen.line_to(
|
|
||||||
self.start_x + self.start_length + self.height,
|
|
||||||
self.start_y + dodge,
|
|
||||||
);
|
|
||||||
pen.line_to(self.start_x + self.total_length, self.start_y + dodge);
|
|
||||||
}
|
|
||||||
gtk::Orientation::Vertical => {}
|
|
||||||
_ => panic!("unknown orientation"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SlashMeter {
|
|
||||||
orientation: gtk::Orientation,
|
|
||||||
start_x: f64,
|
|
||||||
start_y: f64,
|
|
||||||
count: u8,
|
|
||||||
fill_count: u8,
|
|
||||||
height: f64,
|
|
||||||
length: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SlashMeter {
|
|
||||||
fn draw(&self, context: &Context) {
|
|
||||||
match self.orientation {
|
|
||||||
gtk::Orientation::Horizontal => {
|
|
||||||
let angle: f64 = 0.8;
|
|
||||||
let run = self.height / angle.tan();
|
|
||||||
let width = self.length / (self.count as f64 * 2.);
|
|
||||||
|
|
||||||
for c in 0..self.count {
|
|
||||||
context.set_line_width(1.);
|
|
||||||
|
|
||||||
let start_x = self.start_x + c as f64 * width * 2.;
|
|
||||||
context.move_to(start_x, self.start_y);
|
|
||||||
context.line_to(start_x + run, self.start_y - self.height);
|
|
||||||
context.line_to(start_x + run + width, self.start_y - self.height);
|
|
||||||
context.line_to(start_x + width, self.start_y);
|
|
||||||
context.line_to(start_x, self.start_y);
|
|
||||||
if c < self.fill_count {
|
|
||||||
let _ = context.fill();
|
|
||||||
} else {
|
|
||||||
let _ = context.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
gtk::Orientation::Vertical => {}
|
|
||||||
_ => panic!("unknown orientation"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
trait Pen {
|
|
||||||
fn move_to(&self, x: f64, y: f64);
|
|
||||||
fn line_to(&self, x: f64, y: f64);
|
|
||||||
fn stroke(&self);
|
|
||||||
|
|
||||||
fn finish(self) -> Pattern;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct GlowPen {
|
|
||||||
blur_context: Context,
|
|
||||||
draw_context: Context,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GlowPen {
|
|
||||||
fn new(
|
|
||||||
width: i32,
|
|
||||||
height: i32,
|
|
||||||
line_width: f64,
|
|
||||||
blur_line_width: f64,
|
|
||||||
color: (f64, f64, f64),
|
|
||||||
) -> Self {
|
|
||||||
let blur_context =
|
|
||||||
Context::new(ImageSurface::create(Format::Rgb24, width, height).unwrap()).unwrap();
|
|
||||||
blur_context.set_line_width(blur_line_width);
|
|
||||||
blur_context.set_source_rgba(color.0, color.1, color.2, 0.5);
|
|
||||||
blur_context.push_group();
|
|
||||||
blur_context.set_line_cap(LineCap::Round);
|
|
||||||
|
|
||||||
let draw_context =
|
|
||||||
Context::new(ImageSurface::create(Format::Rgb24, width, height).unwrap()).unwrap();
|
|
||||||
draw_context.set_line_width(line_width);
|
|
||||||
draw_context.set_source_rgb(color.0, color.1, color.2);
|
|
||||||
draw_context.push_group();
|
|
||||||
draw_context.set_line_cap(LineCap::Round);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
blur_context,
|
|
||||||
draw_context,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Pen for GlowPen {
|
|
||||||
fn move_to(&self, x: f64, y: f64) {
|
|
||||||
self.blur_context.move_to(x, y);
|
|
||||||
self.draw_context.move_to(x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn line_to(&self, x: f64, y: f64) {
|
|
||||||
self.blur_context.line_to(x, y);
|
|
||||||
self.draw_context.line_to(x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stroke(&self) {
|
|
||||||
self.blur_context.stroke().expect("to draw the blur line");
|
|
||||||
self.draw_context
|
|
||||||
.stroke()
|
|
||||||
.expect("to draw the regular line");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn finish(self) -> Pattern {
|
|
||||||
let foreground = self.draw_context.pop_group().unwrap();
|
|
||||||
self.blur_context.set_source(foreground).unwrap();
|
|
||||||
self.blur_context.paint().unwrap();
|
|
||||||
self.blur_context.pop_group().unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let app = gtk::Application::builder()
|
let app = gtk::Application::builder()
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
[package]
|
||||||
|
name = "cyberpunk"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cairo-rs = { version = "0.18" }
|
||||||
|
gio = { version = "0.18" }
|
||||||
|
glib = { version = "0.18" }
|
||||||
|
gtk = { version = "0.7", package = "gtk4" }
|
|
@ -0,0 +1,301 @@
|
||||||
|
use cairo::{
|
||||||
|
Context, FontSlant, FontWeight, Format, ImageSurface, LineCap, Pattern,
|
||||||
|
TextExtents,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct AsymLineCutout {
|
||||||
|
pub orientation: gtk::Orientation,
|
||||||
|
pub start_x: f64,
|
||||||
|
pub start_y: f64,
|
||||||
|
pub start_length: f64,
|
||||||
|
pub cutout_length: f64,
|
||||||
|
pub end_length: f64,
|
||||||
|
pub height: f64,
|
||||||
|
pub invert: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsymLineCutout {
|
||||||
|
pub fn draw(&self, pen: &impl Pen) {
|
||||||
|
let dodge = if self.invert {
|
||||||
|
self.height
|
||||||
|
} else {
|
||||||
|
-self.height
|
||||||
|
};
|
||||||
|
match self.orientation {
|
||||||
|
gtk::Orientation::Horizontal => {
|
||||||
|
pen.move_to(self.start_x, self.start_y);
|
||||||
|
pen.line_to(self.start_x + self.start_length, self.start_y);
|
||||||
|
pen.line_to(
|
||||||
|
self.start_x + self.start_length + self.height,
|
||||||
|
self.start_y + dodge,
|
||||||
|
);
|
||||||
|
pen.line_to(
|
||||||
|
self.start_x + self.start_length + self.height + self.cutout_length,
|
||||||
|
self.start_y + dodge,
|
||||||
|
);
|
||||||
|
pen.line_to(
|
||||||
|
self.start_x
|
||||||
|
+ self.start_length
|
||||||
|
+ self.height
|
||||||
|
+ self.cutout_length
|
||||||
|
+ (self.height / 2.),
|
||||||
|
self.start_y + dodge / 2.,
|
||||||
|
);
|
||||||
|
pen.line_to(
|
||||||
|
self.start_x
|
||||||
|
+ self.start_length
|
||||||
|
+ self.height
|
||||||
|
+ self.cutout_length
|
||||||
|
+ (self.height / 2.)
|
||||||
|
+ self.end_length,
|
||||||
|
self.start_y + dodge / 2.,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
gtk::Orientation::Vertical => {
|
||||||
|
pen.move_to(self.start_x, self.start_y);
|
||||||
|
pen.line_to(self.start_x, self.start_y + self.start_length);
|
||||||
|
pen.line_to(
|
||||||
|
self.start_x + dodge,
|
||||||
|
self.start_y + self.start_length + self.height,
|
||||||
|
);
|
||||||
|
pen.line_to(
|
||||||
|
self.start_x + dodge,
|
||||||
|
self.start_y + self.start_length + self.height + self.cutout_length,
|
||||||
|
);
|
||||||
|
pen.line_to(
|
||||||
|
self.start_x + dodge / 2.,
|
||||||
|
self.start_y
|
||||||
|
+ self.start_length
|
||||||
|
+ self.height
|
||||||
|
+ self.cutout_length
|
||||||
|
+ (self.height / 2.),
|
||||||
|
);
|
||||||
|
pen.line_to(
|
||||||
|
self.start_x + dodge / 2.,
|
||||||
|
self.start_y
|
||||||
|
+ self.start_length
|
||||||
|
+ self.height
|
||||||
|
+ self.cutout_length
|
||||||
|
+ (self.height / 2.)
|
||||||
|
+ self.end_length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => panic!("unknown orientation"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Represents an asymetrical line that starts at one location, then a 45-degree angle and then
|
||||||
|
// another line afterwards.
|
||||||
|
pub struct AsymLine {
|
||||||
|
// Will this be drawn left-to-right or up-to-down?
|
||||||
|
pub orientation: gtk::Orientation,
|
||||||
|
|
||||||
|
// Starting address
|
||||||
|
pub start_x: f64,
|
||||||
|
pub start_y: f64,
|
||||||
|
|
||||||
|
// Length of the first segment
|
||||||
|
pub start_length: f64,
|
||||||
|
|
||||||
|
// Height to dodge over to the next section
|
||||||
|
pub height: f64,
|
||||||
|
|
||||||
|
// Total length of the entire line.
|
||||||
|
pub end_length: f64,
|
||||||
|
|
||||||
|
// When normal, the angle dodge is upwards. When inverted, the angle dodge is downwards.
|
||||||
|
pub invert: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsymLine {
|
||||||
|
pub fn draw(&self, pen: &impl Pen) {
|
||||||
|
let dodge = if self.invert {
|
||||||
|
self.height
|
||||||
|
} else {
|
||||||
|
-self.height
|
||||||
|
};
|
||||||
|
match self.orientation {
|
||||||
|
gtk::Orientation::Horizontal => {
|
||||||
|
pen.move_to(self.start_x, self.start_y);
|
||||||
|
pen.line_to(self.start_x + self.start_length, self.start_y);
|
||||||
|
pen.line_to(
|
||||||
|
self.start_x + self.start_length + self.height,
|
||||||
|
self.start_y + dodge,
|
||||||
|
);
|
||||||
|
pen.line_to(
|
||||||
|
self.start_x + self.start_length + self.height + self.end_length,
|
||||||
|
self.start_y + dodge,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
gtk::Orientation::Vertical => {}
|
||||||
|
_ => panic!("unknown orientation"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SlashMeter {
|
||||||
|
pub orientation: gtk::Orientation,
|
||||||
|
pub start_x: f64,
|
||||||
|
pub start_y: f64,
|
||||||
|
pub count: u8,
|
||||||
|
pub fill_count: u8,
|
||||||
|
pub height: f64,
|
||||||
|
pub length: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SlashMeter {
|
||||||
|
pub fn draw(&self, context: &Context) {
|
||||||
|
match self.orientation {
|
||||||
|
gtk::Orientation::Horizontal => {
|
||||||
|
let angle: f64 = 0.8;
|
||||||
|
let run = self.height / angle.tan();
|
||||||
|
let width = self.length / (self.count as f64 * 2.);
|
||||||
|
|
||||||
|
for c in 0..self.count {
|
||||||
|
context.set_line_width(1.);
|
||||||
|
|
||||||
|
let start_x = self.start_x + c as f64 * width * 2.;
|
||||||
|
context.move_to(start_x, self.start_y);
|
||||||
|
context.line_to(start_x + run, self.start_y - self.height);
|
||||||
|
context.line_to(start_x + run + width, self.start_y - self.height);
|
||||||
|
context.line_to(start_x + width, self.start_y);
|
||||||
|
context.line_to(start_x, self.start_y);
|
||||||
|
if c < self.fill_count {
|
||||||
|
let _ = context.fill();
|
||||||
|
} else {
|
||||||
|
let _ = context.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gtk::Orientation::Vertical => {}
|
||||||
|
_ => panic!("unknown orientation"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a pen for drawing a pattern. This is good for complex patterns that may require
|
||||||
|
/// multiple identical steps.
|
||||||
|
pub trait Pen {
|
||||||
|
/// Move the pen to a location.
|
||||||
|
fn move_to(&self, x: f64, y: f64);
|
||||||
|
|
||||||
|
/// Draw a line from the current location to the specified destination.
|
||||||
|
fn line_to(&self, x: f64, y: f64);
|
||||||
|
|
||||||
|
/// Instantiate the line.
|
||||||
|
fn stroke(&self);
|
||||||
|
|
||||||
|
/// Convert all of the drawing into a pattern that can be painted to a drawing context.
|
||||||
|
fn finish(self) -> Pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GlowPen {
|
||||||
|
blur_context: Context,
|
||||||
|
draw_context: Context,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GlowPen {
|
||||||
|
pub fn new(
|
||||||
|
width: i32,
|
||||||
|
height: i32,
|
||||||
|
line_width: f64,
|
||||||
|
blur_line_width: f64,
|
||||||
|
color: (f64, f64, f64),
|
||||||
|
) -> Self {
|
||||||
|
let blur_context =
|
||||||
|
Context::new(ImageSurface::create(Format::Rgb24, width, height).unwrap()).unwrap();
|
||||||
|
blur_context.set_line_width(blur_line_width);
|
||||||
|
blur_context.set_source_rgba(color.0, color.1, color.2, 0.5);
|
||||||
|
blur_context.push_group();
|
||||||
|
blur_context.set_line_cap(LineCap::Round);
|
||||||
|
|
||||||
|
let draw_context =
|
||||||
|
Context::new(ImageSurface::create(Format::Rgb24, width, height).unwrap()).unwrap();
|
||||||
|
draw_context.set_line_width(line_width);
|
||||||
|
draw_context.set_source_rgb(color.0, color.1, color.2);
|
||||||
|
draw_context.push_group();
|
||||||
|
draw_context.set_line_cap(LineCap::Round);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
blur_context,
|
||||||
|
draw_context,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pen for GlowPen {
|
||||||
|
fn move_to(&self, x: f64, y: f64) {
|
||||||
|
self.blur_context.move_to(x, y);
|
||||||
|
self.draw_context.move_to(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line_to(&self, x: f64, y: f64) {
|
||||||
|
self.blur_context.line_to(x, y);
|
||||||
|
self.draw_context.line_to(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stroke(&self) {
|
||||||
|
self.blur_context.stroke().expect("to draw the blur line");
|
||||||
|
self.draw_context
|
||||||
|
.stroke()
|
||||||
|
.expect("to draw the regular line");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(self) -> Pattern {
|
||||||
|
let foreground = self.draw_context.pop_group().unwrap();
|
||||||
|
self.blur_context.set_source(foreground).unwrap();
|
||||||
|
self.blur_context.paint().unwrap();
|
||||||
|
self.blur_context.pop_group().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Text<'a> {
|
||||||
|
content: Vec<String>,
|
||||||
|
context: &'a Context,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Text<'a> {
|
||||||
|
pub fn new(content: String, context: &'a Context, size: f64, width: f64) -> Self {
|
||||||
|
context.select_font_face("Alegreya Sans SC", FontSlant::Normal, FontWeight::Bold);
|
||||||
|
context.set_font_size(size);
|
||||||
|
|
||||||
|
let lines = word_wrap(content, context, width);
|
||||||
|
|
||||||
|
Self { content: lines, context }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extents(&self) -> TextExtents {
|
||||||
|
self.context.text_extents(&self.content[0]).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw(&self) {
|
||||||
|
let mut baseline = 0.;
|
||||||
|
for line in self.content.iter() {
|
||||||
|
baseline += self.context.text_extents(line).unwrap().height() + 10.;
|
||||||
|
self.context.move_to(0., baseline);
|
||||||
|
let _ = self.context.show_text(&line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn word_wrap(content: String, context: &Context, max_width: f64) -> Vec<String> {
|
||||||
|
let mut lines = vec![];
|
||||||
|
let words: Vec<&str> = content.split_whitespace().collect();
|
||||||
|
let mut start: usize = 0;
|
||||||
|
let mut line = String::new();
|
||||||
|
|
||||||
|
for idx in 0..words.len() + 1 {
|
||||||
|
line = words[start..idx].join(" ");
|
||||||
|
let extents = context.text_extents(&line).unwrap();
|
||||||
|
if extents.width() > max_width {
|
||||||
|
let line = words[start..idx-1].join(" ");
|
||||||
|
start = idx-1;
|
||||||
|
lines.push(line.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if line.len() > 0 {
|
||||||
|
lines.push(line);
|
||||||
|
}
|
||||||
|
lines
|
||||||
|
}
|
|
@ -1,12 +1,13 @@
|
||||||
[package]
|
[package]
|
||||||
name = "dashboard"
|
name = "dashboard"
|
||||||
version = "0.1.2"
|
version = "0.1.3"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
adw = { version = "0.5", package = "libadwaita", features = [ "v1_2" ] }
|
adw = { version = "0.5", package = "libadwaita", features = [ "v1_2" ] }
|
||||||
|
async-std = { version = "1.13" }
|
||||||
cairo-rs = { version = "0.18" }
|
cairo-rs = { version = "0.18" }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
fluent-ergonomics = { path = "../fluent-ergonomics/" }
|
fluent-ergonomics = { path = "../fluent-ergonomics/" }
|
||||||
|
@ -17,13 +18,11 @@ gio = { version = "0.18" }
|
||||||
glib = { version = "0.18" }
|
glib = { version = "0.18" }
|
||||||
gdk = { version = "0.7", package = "gdk4" }
|
gdk = { version = "0.7", package = "gdk4" }
|
||||||
gtk = { version = "0.7", package = "gtk4" }
|
gtk = { version = "0.7", package = "gtk4" }
|
||||||
ifc = { path = "../ifc/" }
|
|
||||||
lazy_static = { version = "1.4" }
|
lazy_static = { version = "1.4" }
|
||||||
memorycache = { path = "../memorycache/" }
|
memorycache = { path = "../memorycache/" }
|
||||||
reqwest = { version = "0.11", features = ["json"] }
|
reqwest = { version = "0.11", features = ["json"] }
|
||||||
serde_derive = { version = "1" }
|
|
||||||
serde_json = { version = "1" }
|
serde_json = { version = "1" }
|
||||||
serde = { version = "1" }
|
serde = { version = "1", features = [ "derive" ] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
unic-langid = { version = "0.9" }
|
unic-langid = { version = "0.9" }
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,10 @@ impl ApplicationWindow {
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let date_label = Date::default();
|
let date_label = Date::default();
|
||||||
layout.append(&date_label);
|
let header = adw::HeaderBar::builder()
|
||||||
|
.title_widget(&date_label)
|
||||||
|
.build();
|
||||||
|
layout.append(&header);
|
||||||
|
|
||||||
let events = Events::default();
|
let events = Events::default();
|
||||||
layout.append(&events);
|
layout.append(&events);
|
||||||
|
|
|
@ -1,21 +1,19 @@
|
||||||
use chrono::Datelike;
|
|
||||||
use glib::Object;
|
|
||||||
use gtk::{prelude::*, subclass::prelude::*};
|
|
||||||
use ifc::IFC;
|
|
||||||
use std::{cell::RefCell, rc::Rc};
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
use glib::Object;
|
||||||
|
use gtk::{prelude::*, subclass::prelude::*};
|
||||||
|
|
||||||
pub struct DatePrivate {
|
pub struct DatePrivate {
|
||||||
date: Rc<RefCell<IFC>>,
|
date: Rc<RefCell<NaiveDate>>,
|
||||||
label: Rc<RefCell<gtk::Label>>,
|
label: Rc<RefCell<gtk::Label>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for DatePrivate {
|
impl Default for DatePrivate {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let date = chrono::Local::now().date_naive();
|
let date = chrono::Local::now().date_naive();
|
||||||
let year = date.year();
|
|
||||||
let date = date.with_year(year + 10000).unwrap();
|
|
||||||
Self {
|
Self {
|
||||||
date: Rc::new(RefCell::new(IFC::from(date))),
|
date: Rc::new(RefCell::new(date)),
|
||||||
label: Rc::new(RefCell::new(gtk::Label::new(None))),
|
label: Rc::new(RefCell::new(gtk::Label::new(None))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,19 +50,16 @@ impl Default for Date {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Date {
|
impl Date {
|
||||||
pub fn update_date(&self, date: IFC) {
|
pub fn update_date(&self, date: NaiveDate) {
|
||||||
*self.imp().date.borrow_mut() = date;
|
*self.imp().date.borrow_mut() = date;
|
||||||
self.redraw();
|
self.redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn redraw(&self) {
|
fn redraw(&self) {
|
||||||
let date = self.imp().date.borrow().clone();
|
let date = self.imp().date.borrow();
|
||||||
self.imp().label.borrow_mut().set_text(&format!(
|
self.imp()
|
||||||
"{:?}, {:?} {}, {}",
|
.label
|
||||||
date.weekday(),
|
.borrow_mut()
|
||||||
date.month(),
|
.set_text(&date.format("%Y %B %d").to_string());
|
||||||
date.day(),
|
|
||||||
date.year()
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ use crate::{
|
||||||
};
|
};
|
||||||
use glib::Object;
|
use glib::Object;
|
||||||
use gtk::{prelude::*, subclass::prelude::*};
|
use gtk::{prelude::*, subclass::prelude::*};
|
||||||
use ifc::IFC;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
|
@ -59,19 +58,19 @@ impl Events {
|
||||||
pub fn set_events(&self, events: YearlyEvents, next_event: solstices::Event) {
|
pub fn set_events(&self, events: YearlyEvents, next_event: solstices::Event) {
|
||||||
self.imp()
|
self.imp()
|
||||||
.spring_equinox
|
.spring_equinox
|
||||||
.update_date(IFC::from(events.spring_equinox.date_naive()));
|
.update_date(events.spring_equinox.date_naive());
|
||||||
|
|
||||||
self.imp()
|
self.imp()
|
||||||
.summer_solstice
|
.summer_solstice
|
||||||
.update_date(IFC::from(events.summer_solstice.date_naive()));
|
.update_date(events.summer_solstice.date_naive());
|
||||||
|
|
||||||
self.imp()
|
self.imp()
|
||||||
.autumn_equinox
|
.autumn_equinox
|
||||||
.update_date(IFC::from(events.autumn_equinox.date_naive()));
|
.update_date(events.autumn_equinox.date_naive());
|
||||||
|
|
||||||
self.imp()
|
self.imp()
|
||||||
.winter_solstice
|
.winter_solstice
|
||||||
.update_date(IFC::from(events.winter_solstice.date_naive()));
|
.update_date(events.winter_solstice.date_naive());
|
||||||
|
|
||||||
self.imp().spring_equinox.remove_css_class("highlight");
|
self.imp().spring_equinox.remove_css_class("highlight");
|
||||||
self.imp().summer_solstice.remove_css_class("highlight");
|
self.imp().summer_solstice.remove_css_class("highlight");
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
use chrono::{Datelike, Local, Utc};
|
|
||||||
use geo_types::{Latitude, Longitude};
|
|
||||||
use glib::Sender;
|
|
||||||
use gtk::prelude::*;
|
|
||||||
use ifc::IFC;
|
|
||||||
use std::{
|
use std::{
|
||||||
env,
|
env,
|
||||||
sync::{Arc, RwLock},
|
sync::{Arc, RwLock},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use async_std::channel::Sender;
|
||||||
|
use chrono::{Datelike, Local, Utc};
|
||||||
|
use geo_types::{Latitude, Longitude};
|
||||||
|
use gtk::prelude::*;
|
||||||
|
|
||||||
mod app_window;
|
mod app_window;
|
||||||
use app_window::ApplicationWindow;
|
use app_window::ApplicationWindow;
|
||||||
|
|
||||||
|
@ -102,14 +102,17 @@ pub fn main() {
|
||||||
|
|
||||||
let now = Local::now();
|
let now = Local::now();
|
||||||
let state = State {
|
let state = State {
|
||||||
date: IFC::from(now.date_naive().with_year(now.year() + 10000).unwrap()),
|
date: now.date_naive(),
|
||||||
next_event: EVENTS.next_event(now.with_timezone(&Utc)).unwrap(),
|
next_event: EVENTS.next_event(now.with_timezone(&Utc)).unwrap(),
|
||||||
events: EVENTS.yearly_events(now.year()).unwrap(),
|
events: EVENTS.yearly_events(now.year()).unwrap(),
|
||||||
transit: Some(transit),
|
transit: Some(transit),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(ref gtk_tx) = *core.tx.read().unwrap() {
|
let gtk_tx = core.tx.read().unwrap().clone();
|
||||||
let _ = gtk_tx.send(Message::Refresh(state.clone()));
|
|
||||||
|
if let Some(gtk_tx) = gtk_tx {
|
||||||
|
let state = state.clone();
|
||||||
|
let _ = gtk_tx.send(Message::Refresh(state)).await;
|
||||||
std::thread::sleep(std::time::Duration::from_secs(60));
|
std::thread::sleep(std::time::Duration::from_secs(60));
|
||||||
} else {
|
} else {
|
||||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||||
|
@ -119,21 +122,17 @@ pub fn main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
app.connect_activate(move |app| {
|
app.connect_activate(move |app| {
|
||||||
let (gtk_tx, gtk_rx) =
|
let (gtk_tx, gtk_rx) = async_std::channel::unbounded();
|
||||||
gtk::glib::MainContext::channel::<Message>(gtk::glib::Priority::DEFAULT);
|
|
||||||
|
|
||||||
*core.tx.write().unwrap() = Some(gtk_tx);
|
*core.tx.write().unwrap() = Some(gtk_tx);
|
||||||
|
|
||||||
let window = ApplicationWindow::new(app);
|
let window = ApplicationWindow::new(app);
|
||||||
window.window.present();
|
window.window.present();
|
||||||
|
|
||||||
gtk_rx.attach(None, {
|
glib::spawn_future_local(async move {
|
||||||
let window = window.clone();
|
loop {
|
||||||
move |msg| {
|
let Message::Refresh(state) = gtk_rx.recv().await.unwrap();
|
||||||
let Message::Refresh(state) = msg;
|
window.update_state(state);
|
||||||
ApplicationWindow::update_state(&window, state);
|
|
||||||
|
|
||||||
glib::ControlFlow::Continue
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
// http://astropixels.com/ephemeris/soleq2001.html
|
// http://astropixels.com/ephemeris/soleq2001.html
|
||||||
const SOLSTICE_TEXT: &str = "
|
const SOLSTICE_TEXT: &str = "
|
||||||
|
|
|
@ -2,11 +2,11 @@ use crate::{
|
||||||
solstices::{Event, YearlyEvents},
|
solstices::{Event, YearlyEvents},
|
||||||
soluna_client::SunMoon,
|
soluna_client::SunMoon,
|
||||||
};
|
};
|
||||||
use ifc::IFC;
|
use chrono::NaiveDate;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct State {
|
pub struct State {
|
||||||
pub date: IFC,
|
pub date: NaiveDate,
|
||||||
pub next_event: Event,
|
pub next_event: Event,
|
||||||
pub events: YearlyEvents,
|
pub events: YearlyEvents,
|
||||||
pub transit: Option<SunMoon>,
|
pub transit: Option<SunMoon>,
|
||||||
|
|
|
@ -45,6 +45,7 @@
|
||||||
pkgs.udev
|
pkgs.udev
|
||||||
pkgs.wasm-pack
|
pkgs.wasm-pack
|
||||||
typeshare.packages."x86_64-linux".default
|
typeshare.packages."x86_64-linux".default
|
||||||
|
pkgs.nodePackages_latest.typescript-language-server
|
||||||
];
|
];
|
||||||
LIBCLANG_PATH="${pkgs.llvmPackages.libclang.lib}/lib";
|
LIBCLANG_PATH="${pkgs.llvmPackages.libclang.lib}/lib";
|
||||||
ENV = "dev";
|
ENV = "dev";
|
||||||
|
@ -80,6 +81,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
in rec {
|
in rec {
|
||||||
|
cyber-slides = cargo_nix.workspaceMembers.cyber-slides.build;
|
||||||
cyberpunk-splash = cargo_nix.workspaceMembers.cyberpunk-splash.build;
|
cyberpunk-splash = cargo_nix.workspaceMembers.cyberpunk-splash.build;
|
||||||
dashboard = cargo_nix.workspaceMembers.dashboard.build;
|
dashboard = cargo_nix.workspaceMembers.dashboard.build;
|
||||||
file-service = cargo_nix.workspaceMembers.file-service.build;
|
file-service = cargo_nix.workspaceMembers.file-service.build;
|
||||||
|
@ -89,6 +91,7 @@
|
||||||
all = pkgs.symlinkJoin {
|
all = pkgs.symlinkJoin {
|
||||||
name = "all";
|
name = "all";
|
||||||
paths = [
|
paths = [
|
||||||
|
cyber-slides
|
||||||
cyberpunk-splash
|
cyberpunk-splash
|
||||||
dashboard
|
dashboard
|
||||||
file-service
|
file-service
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
[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"
|
|
@ -0,0 +1,25 @@
|
||||||
|
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(())
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
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;
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
# 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*
|
|
@ -0,0 +1,46 @@
|
||||||
|
# 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/).
|
|
@ -0,0 +1,45 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 3.8 KiB |
|
@ -0,0 +1,43 @@
|
||||||
|
<!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>
|
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 9.4 KiB |
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
|
@ -0,0 +1,5 @@
|
||||||
|
.layout {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
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();
|
||||||
|
});
|
|
@ -0,0 +1,77 @@
|
||||||
|
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;
|
|
@ -0,0 +1,78 @@
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
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;
|
|
@ -0,0 +1,15 @@
|
||||||
|
.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 {
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
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;
|
|
@ -0,0 +1,11 @@
|
||||||
|
.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);
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
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;
|
|
@ -0,0 +1,18 @@
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
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;
|
|
@ -0,0 +1,50 @@
|
||||||
|
: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;
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
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();
|
|
@ -0,0 +1 @@
|
||||||
|
<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>
|
After Width: | Height: | Size: 2.6 KiB |
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="react-scripts" />
|
|
@ -0,0 +1,15 @@
|
||||||
|
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;
|
|
@ -0,0 +1,5 @@
|
||||||
|
// 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';
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ sgf = { path = "../../sgf" }
|
||||||
grid = { version = "0.9" }
|
grid = { version = "0.9" }
|
||||||
serde_json = { version = "1" }
|
serde_json = { version = "1" }
|
||||||
serde = { version = "1", features = [ "derive" ] }
|
serde = { version = "1", features = [ "derive" ] }
|
||||||
|
nary_tree = { version = "0.4" }
|
||||||
thiserror = { version = "1" }
|
thiserror = { version = "1" }
|
||||||
uuid = { version = "0.8", features = ["v4", "serde"] }
|
uuid = { version = "0.8", features = ["v4", "serde"] }
|
||||||
|
|
||||||
|
|
|
@ -81,7 +81,7 @@ impl From<HotseatPlayerRequest> for Player {
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum CoreResponse {
|
pub enum CoreResponse {
|
||||||
Library(library::LibraryResponse),
|
Library(library::LibraryResponse),
|
||||||
Settings(settings::SettingsResponse),
|
Settings(settings::SettingsResponse),
|
||||||
|
@ -115,8 +115,6 @@ pub struct Core {
|
||||||
|
|
||||||
impl Core {
|
impl Core {
|
||||||
pub fn new(config: Config) -> Self {
|
pub fn new(config: Config) -> Self {
|
||||||
println!("config: {:?}", config);
|
|
||||||
|
|
||||||
let library = match config.get::<LibraryPath>() {
|
let library = match config.get::<LibraryPath>() {
|
||||||
Some(ref path) if path.to_path_buf().exists() => {
|
Some(ref path) if path.to_path_buf().exists() => {
|
||||||
Some(Database::open_path(path.to_path_buf()).unwrap())
|
Some(Database::open_path(path.to_path_buf()).unwrap())
|
||||||
|
|
|
@ -42,7 +42,8 @@ impl Database {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
match parse_sgf(&buffer) {
|
match parse_sgf(&buffer) {
|
||||||
Ok(sgfs) => {
|
Ok(sgfs) => {
|
||||||
let mut sgfs = sgfs.into_iter().flatten().collect::<Vec<sgf::GameRecord>>();
|
let mut sgfs =
|
||||||
|
sgfs.into_iter().flatten().collect::<Vec<sgf::GameRecord>>();
|
||||||
games.append(&mut sgfs);
|
games.append(&mut sgfs);
|
||||||
}
|
}
|
||||||
Err(err) => println!("Error parsing {:?}: {:?}", entry.path(), err),
|
Err(err) => println!("Error parsing {:?}: {:?}", entry.path(), err),
|
||||||
|
|
|
@ -213,7 +213,7 @@ impl Goban {
|
||||||
///
|
///
|
||||||
/// assert_eq!(goban.stone(&Coordinate{ row: 3, column: 3 }), Some(Color::Black));
|
/// assert_eq!(goban.stone(&Coordinate{ row: 3, column: 3 }), Some(Color::Black));
|
||||||
/// assert_eq!(goban.stone(&Coordinate{ row: 15, column: 15 }), Some(Color::White));
|
/// assert_eq!(goban.stone(&Coordinate{ row: 15, column: 15 }), Some(Color::White));
|
||||||
/// assert_eq!(goban.stone(&Coordinate{ row: 3, column: 15 }), Some(Color::Black));
|
/// assert_eq!(goban.stone(&Coordinate{ row: 15, column: 3 }), Some(Color::Black));
|
||||||
/// ```
|
/// ```
|
||||||
pub fn apply_moves<'a>(
|
pub fn apply_moves<'a>(
|
||||||
self,
|
self,
|
||||||
|
@ -611,7 +611,6 @@ mod test {
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
println!("{}", board);
|
|
||||||
for (board, coordinate, group, liberties) in test_cases {
|
for (board, coordinate, group, liberties) in test_cases {
|
||||||
assert_eq!(board.group(&coordinate), group.as_ref());
|
assert_eq!(board.group(&coordinate), group.as_ref());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
|
@ -29,5 +29,9 @@ pub mod library;
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
|
|
||||||
mod types;
|
mod types;
|
||||||
pub use types::{BoardError, Color, Config, ConfigOption, LibraryPath, Player, Rank, Size, Tree};
|
pub use types::{
|
||||||
|
BoardError, Color, Config, ConfigOption, DepthTree, LibraryPath, Player, Rank, Size,
|
||||||
|
};
|
||||||
|
|
||||||
|
mod view_models;
|
||||||
|
pub use view_models::GameReviewViewModel;
|
||||||
|
|
|
@ -14,18 +14,18 @@ General Public License for more details.
|
||||||
You should have received a copy of the GNU General Public License along with On the Grid. If not, see <https://www.gnu.org/licenses/>.
|
You should have received a copy of the GNU General Public License along with On the Grid. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use crate::{Core};
|
use crate::Core;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sgf::GameRecord;
|
use sgf::GameRecord;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub enum LibraryRequest {
|
pub enum LibraryRequest {
|
||||||
ListGames
|
ListGames,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum LibraryResponse {
|
pub enum LibraryResponse {
|
||||||
Games(Vec<GameRecord>)
|
Games(Vec<GameRecord>),
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_list_games(model: &Core) -> LibraryResponse {
|
async fn handle_list_games(model: &Core) -> LibraryResponse {
|
||||||
|
@ -39,10 +39,8 @@ async fn handle_list_games(model: &Core) -> LibraryResponse {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub async fn handle(model: &Core, request: LibraryRequest) -> LibraryResponse {
|
pub async fn handle(model: &Core, request: LibraryRequest) -> LibraryResponse {
|
||||||
match request {
|
match request {
|
||||||
LibraryRequest::ListGames => handle_list_games(model).await,
|
LibraryRequest::ListGames => handle_list_games(model).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
use crate::goban::{Coordinate, Goban};
|
use crate::goban::{Coordinate, Goban};
|
||||||
use config::define_config;
|
use config::define_config;
|
||||||
use config_derive::ConfigOption;
|
use config_derive::ConfigOption;
|
||||||
|
use nary_tree::NodeRef;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sgf::GameNode;
|
use sgf::GameTree;
|
||||||
use std::{cell::RefCell, collections::VecDeque, fmt, path::PathBuf, time::Duration};
|
use std::{
|
||||||
|
collections::{HashMap, VecDeque}, fmt, ops::Deref, path::PathBuf, time::Duration
|
||||||
|
};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
define_config! {
|
define_config! {
|
||||||
LibraryPath(LibraryPath),
|
LibraryPath(LibraryPath),
|
||||||
|
@ -229,6 +231,7 @@ impl GameState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
// To properly generate a tree, I need to know how deep to go. Then I can backtrace. Each node
|
// To properly generate a tree, I need to know how deep to go. Then I can backtrace. Each node
|
||||||
// needs to have a depth. Given a tree, the depth of the node is just the distance from the root.
|
// needs to have a depth. Given a tree, the depth of the node is just the distance from the root.
|
||||||
// This seems obvious, but I had to write it to discover how important that fact was.
|
// This seems obvious, but I had to write it to discover how important that fact was.
|
||||||
|
@ -238,33 +241,88 @@ impl GameState {
|
||||||
pub struct Tree<T> {
|
pub struct Tree<T> {
|
||||||
nodes: Vec<Node<T>>,
|
nodes: Vec<Node<T>>,
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
#[derive(Debug)]
|
// https://llimllib.github.io/pymag-trees/
|
||||||
pub struct Node<T> {
|
// I want to take advantage of the Wetherell Shannon algorithm, but I want some variations. In
|
||||||
pub id: usize,
|
// their diagram, they got a tree that looks like this.
|
||||||
node: T,
|
//
|
||||||
parent: Option<usize>,
|
// O
|
||||||
depth: usize,
|
// |\
|
||||||
width: RefCell<Option<usize>>,
|
// O O
|
||||||
children: Vec<usize>,
|
// |\ \ \
|
||||||
|
// O O O O
|
||||||
|
// |\ |\
|
||||||
|
// O O O O
|
||||||
|
//
|
||||||
|
// In the same circumstance, what I want is this:
|
||||||
|
//
|
||||||
|
// O--
|
||||||
|
// | \
|
||||||
|
// O O
|
||||||
|
// |\ |\
|
||||||
|
// O O O O
|
||||||
|
// |\
|
||||||
|
// O O
|
||||||
|
//
|
||||||
|
// In order to keep things from being overly smooshed, I want to ensure that if a branch overlaps
|
||||||
|
// with another branch, there is some extra drawing space. This might actually be similar to adding
|
||||||
|
// the principal that "A parent should be centered over its children".
|
||||||
|
//
|
||||||
|
// So, given a tree, I need to know how many children exist at each level. Then I build parents
|
||||||
|
// atop the children. At level 3, I have four children, and that happens to be the maximum width of
|
||||||
|
// the graph.
|
||||||
|
//
|
||||||
|
// A bottom-up traversal:
|
||||||
|
// - Figure out the number of nodes at each depth
|
||||||
|
|
||||||
|
pub struct DepthTree(nary_tree::Tree<SizeNode>);
|
||||||
|
|
||||||
|
impl Deref for DepthTree {
|
||||||
|
type Target = nary_tree::Tree<SizeNode>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Tree<T> {
|
impl Default for DepthTree {
|
||||||
fn new(root: T) -> Self {
|
fn default() -> Self {
|
||||||
Tree {
|
Self(nary_tree::Tree::new())
|
||||||
nodes: vec![Node {
|
|
||||||
id: 0,
|
|
||||||
node: root,
|
|
||||||
parent: None,
|
|
||||||
depth: 0,
|
|
||||||
width: RefCell::new(None),
|
|
||||||
children: vec![],
|
|
||||||
}],
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SizeNode {
|
||||||
|
/// Use this to map back to the node in the original game tree. This way we know how to
|
||||||
|
/// correspond from a node in the review tree back to there.
|
||||||
|
pub game_node_id: nary_tree::NodeId,
|
||||||
|
|
||||||
|
/// How deep into the tree is this node?
|
||||||
|
depth: usize,
|
||||||
|
|
||||||
|
/// How far from the leftmost margin is this node?
|
||||||
|
width: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SizeNode {
|
||||||
|
pub fn position(&self) -> (usize, usize) {
|
||||||
|
(self.depth, self.width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DepthTree {
|
||||||
|
/*
|
||||||
|
pub fn node(&self, idx: usize) -> &T {
|
||||||
|
&self.nodes[idx].content
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn node(&self, idx: usize) -> &T {
|
pub fn parent(&self, node: &Node<T>) -> Option<&Node<T>> {
|
||||||
&self.nodes[idx].node
|
if let Some(parent_idx) = node.parent {
|
||||||
|
self.nodes.get(parent_idx)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a node to the parent specified by parent_idx. Return the new index. This cannot be used
|
// Add a node to the parent specified by parent_idx. Return the new index. This cannot be used
|
||||||
|
@ -275,7 +333,7 @@ impl<T> Tree<T> {
|
||||||
|
|
||||||
self.nodes.push(Node {
|
self.nodes.push(Node {
|
||||||
id: next_idx,
|
id: next_idx,
|
||||||
node,
|
content: node,
|
||||||
parent: Some(parent_idx),
|
parent: Some(parent_idx),
|
||||||
depth: parent.depth + 1,
|
depth: parent.depth + 1,
|
||||||
width: RefCell::new(None),
|
width: RefCell::new(None),
|
||||||
|
@ -286,12 +344,20 @@ impl<T> Tree<T> {
|
||||||
parent.children.push(next_idx);
|
parent.children.push(next_idx);
|
||||||
next_idx
|
next_idx
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
pub fn max_depth(&self) -> usize {
|
pub fn max_depth(&self) -> usize {
|
||||||
self.nodes.iter().fold(
|
self.0
|
||||||
0,
|
.root()
|
||||||
|max, node| if node.depth > max { node.depth } else { max },
|
.unwrap()
|
||||||
)
|
.traverse_pre_order()
|
||||||
|
.fold(0, |max, node| {
|
||||||
|
if node.data().depth > max {
|
||||||
|
node.data().depth
|
||||||
|
} else {
|
||||||
|
max
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Since I know the width of a node, now I want to figure out its placement in the larger
|
// Since I know the width of a node, now I want to figure out its placement in the larger
|
||||||
|
@ -309,7 +375,9 @@ impl<T> Tree<T> {
|
||||||
// amounts to the position of the parent node.
|
// amounts to the position of the parent node.
|
||||||
//
|
//
|
||||||
// When drawing nodes, I don't know how to persist the level of indent.
|
// When drawing nodes, I don't know how to persist the level of indent.
|
||||||
pub fn position(&self, idx: usize) -> (usize, usize) {
|
|
||||||
|
// unimplemented!()
|
||||||
|
/*
|
||||||
let node = &self.nodes[idx];
|
let node = &self.nodes[idx];
|
||||||
match node.parent {
|
match node.parent {
|
||||||
Some(parent_idx) => {
|
Some(parent_idx) => {
|
||||||
|
@ -320,15 +388,15 @@ impl<T> Tree<T> {
|
||||||
.iter()
|
.iter()
|
||||||
.take_while(|n| **n != node.id)
|
.take_while(|n| **n != node.id)
|
||||||
.fold(0, |acc, n| acc + self.width(*n));
|
.fold(0, |acc, n| acc + self.width(*n));
|
||||||
println!("[{}] sibling width {}", idx, sibling_width);
|
|
||||||
(node.depth, parent_column + sibling_width)
|
(node.depth, parent_column + sibling_width)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Root nodes won't have a parent, so just put them in the first column
|
// Root nodes won't have a parent, so just put them in the first column
|
||||||
None => (0, 0),
|
None => (0, 0),
|
||||||
}
|
}
|
||||||
}
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
// Given a node, do a postorder traversal to figure out the width of the node based on all of
|
// Given a node, do a postorder traversal to figure out the width of the node based on all of
|
||||||
// its children. This is equivalent to the widest of all of its children at all depths.
|
// its children. This is equivalent to the widest of all of its children at all depths.
|
||||||
//
|
//
|
||||||
|
@ -340,7 +408,6 @@ impl<T> Tree<T> {
|
||||||
// My algorithm right now is likely to generate unnecessarily wide trees in a complex game
|
// My algorithm right now is likely to generate unnecessarily wide trees in a complex game
|
||||||
// review.
|
// review.
|
||||||
fn width(&self, id: usize) -> usize {
|
fn width(&self, id: usize) -> usize {
|
||||||
println!("[{}] calculating width", id);
|
|
||||||
let node = &self.nodes[id];
|
let node = &self.nodes[id];
|
||||||
if let Some(width) = *node.width.borrow() {
|
if let Some(width) = *node.width.borrow() {
|
||||||
return width;
|
return width;
|
||||||
|
@ -351,22 +418,107 @@ impl<T> Tree<T> {
|
||||||
.iter()
|
.iter()
|
||||||
.fold(0, |acc, child| acc + self.width(*child));
|
.fold(0, |acc, child| acc + self.width(*child));
|
||||||
let width = if width == 0 { 1 } else { width };
|
let width = if width == 0 { 1 } else { width };
|
||||||
println!("[{}] width: {}", id, width);
|
|
||||||
*node.width.borrow_mut() = Some(width);
|
*node.width.borrow_mut() = Some(width);
|
||||||
|
|
||||||
width
|
width
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
pub fn bfs_iter<'a>(&'a self) -> BFSIter<T> {
|
pub fn bfs_iter(&self) -> BFSIter<'_, SizeNode> {
|
||||||
let mut queue = VecDeque::new();
|
let mut queue = VecDeque::new();
|
||||||
queue.push_back(&self.nodes[0]);
|
queue.push_back(self.0.root().unwrap());
|
||||||
BFSIter { tree: self, queue }
|
BFSIter { queue }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a GameTree> for DepthTree {
|
||||||
|
fn from(tree: &'a GameTree) -> Self {
|
||||||
|
// Like in the conversion from SGF to GameTree, I need to traverse the entire tree one node
|
||||||
|
// at a time, keeping track of node ids as we go. I'm going to go with a depth-first
|
||||||
|
// traversal. When generating each node, I think I want to generate all of the details of
|
||||||
|
// the node as we go.
|
||||||
|
let source_root_node = tree.root();
|
||||||
|
match source_root_node {
|
||||||
|
Some(source_root_node) => {
|
||||||
|
// Do the real work
|
||||||
|
// The id_map indexes from the source tree to the destination tree. Reverse
|
||||||
|
// indexing is accomplished by looking at the node_id in a node in the destination
|
||||||
|
// tree.
|
||||||
|
let mut id_map: HashMap<nary_tree::NodeId, nary_tree::NodeId> = HashMap::new();
|
||||||
|
let mut tree = nary_tree::Tree::new();
|
||||||
|
|
||||||
|
let mut iter = source_root_node.traverse_pre_order();
|
||||||
|
let _ = iter.next().unwrap(); // we already know that the first element to be
|
||||||
|
// returned is the root node, and that the root node
|
||||||
|
// already exists. Otherwise we wouldn't even be in
|
||||||
|
// this branch.
|
||||||
|
|
||||||
|
let dest_root_id = tree.set_root(SizeNode {
|
||||||
|
game_node_id: source_root_node.node_id(),
|
||||||
|
depth: 0,
|
||||||
|
width: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
id_map.insert(source_root_node.node_id(), dest_root_id);
|
||||||
|
|
||||||
|
for source_node in iter {
|
||||||
|
let dest_parent_id = id_map
|
||||||
|
.get(&source_node.parent().unwrap().node_id())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut dest_parent = tree.get_mut(*dest_parent_id).unwrap();
|
||||||
|
|
||||||
|
let new_depth_node = SizeNode {
|
||||||
|
game_node_id: source_node.node_id(),
|
||||||
|
depth: 1 + dest_parent.data().depth,
|
||||||
|
width: dest_parent.data().width,
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_node_id = dest_parent.append(new_depth_node).node_id();
|
||||||
|
|
||||||
|
match tree
|
||||||
|
.get(new_node_id)
|
||||||
|
.unwrap()
|
||||||
|
.prev_sibling()
|
||||||
|
.map(|node| node.data().width)
|
||||||
|
{
|
||||||
|
None => {}
|
||||||
|
Some(previous_width) => {
|
||||||
|
let mut new_node = tree.get_mut(new_node_id).unwrap();
|
||||||
|
new_node.data().width = previous_width + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
let new_node = tree.get_mut(*dest_parent_id).unwrap().append(new_depth_node);
|
||||||
|
let previous_node = new_node.prev_sibling();
|
||||||
|
|
||||||
|
match previous_node {
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
match dest_noderef.prev_sibling() {
|
||||||
|
None => {}
|
||||||
|
Some(mut node) => { dest_noderef.data().width = node.data().width + 1 }
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
id_map.insert(source_node.node_id(), new_node_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self(tree)
|
||||||
|
}
|
||||||
|
None => Self::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
impl<'a> From<&'a GameNode> for Tree<Uuid> {
|
impl<'a> From<&'a GameNode> for Tree<Uuid> {
|
||||||
fn from(root: &'a GameNode) -> Self {
|
fn from(root: &'a GameNode) -> Self {
|
||||||
fn add_subtree<'a>(tree: &mut Tree<Uuid>, parent_idx: usize, node: &'a GameNode) {
|
fn add_subtree(tree: &mut Tree<Uuid>, parent_idx: usize, node: &GameNode) {
|
||||||
let idx = tree.add_node(parent_idx, node.id());
|
let idx = tree.add_node(parent_idx, node.id());
|
||||||
|
|
||||||
let children = match node {
|
let children = match node {
|
||||||
|
@ -393,22 +545,21 @@ impl<'a> From<&'a GameNode> for Tree<Uuid> {
|
||||||
tree
|
tree
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
pub struct BFSIter<'a, T> {
|
pub struct BFSIter<'a, T> {
|
||||||
tree: &'a Tree<T>,
|
queue: VecDeque<nary_tree::NodeRef<'a, T>>,
|
||||||
queue: VecDeque<&'a Node<T>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, T> Iterator for BFSIter<'a, T> {
|
impl<'a, T> Iterator for BFSIter<'a, T> {
|
||||||
type Item = &'a Node<T>;
|
type Item = NodeRef<'a, T>;
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
let retval = self.queue.pop_front();
|
let retval = self.queue.pop_front();
|
||||||
if let Some(ref retval) = retval {
|
if let Some(ref retval) = retval {
|
||||||
retval
|
retval
|
||||||
.children
|
.children()
|
||||||
.iter()
|
.for_each(|noderef| self.queue.push_back(noderef));
|
||||||
.for_each(|idx| self.queue.push_back(&self.tree.nodes[*idx]));
|
|
||||||
}
|
}
|
||||||
retval
|
retval
|
||||||
}
|
}
|
||||||
|
@ -417,8 +568,8 @@ impl<'a, T> Iterator for BFSIter<'a, T> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use cool_asserts::assert_matches;
|
// use sgf::{GameRecord, GameTree, GameType, Move, MoveNode};
|
||||||
use sgf::{Move, MoveNode};
|
use sgf::{GameNode, GameTree, Move, MoveNode};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn current_player_changes_after_move() {
|
fn current_player_changes_after_move() {
|
||||||
|
@ -477,116 +628,131 @@ mod test {
|
||||||
// B G H
|
// B G H
|
||||||
// C I
|
// C I
|
||||||
// D E F
|
// D E F
|
||||||
|
fn branching_tree() -> GameTree {
|
||||||
|
let mut game_tree = GameTree::default();
|
||||||
|
let node_a = game_tree.set_root(GameNode::MoveNode(MoveNode::new(
|
||||||
|
sgf::Color::Black,
|
||||||
|
Move::Move("dp".to_owned()),
|
||||||
|
)));
|
||||||
|
|
||||||
|
let node_b = game_tree
|
||||||
|
.get_mut(node_a)
|
||||||
|
.unwrap()
|
||||||
|
.append(GameNode::MoveNode(MoveNode::new(
|
||||||
|
sgf::Color::Black,
|
||||||
|
Move::Move("dp".to_owned()),
|
||||||
|
)))
|
||||||
|
.node_id();
|
||||||
|
|
||||||
|
let node_c = game_tree
|
||||||
|
.get_mut(node_b)
|
||||||
|
.unwrap()
|
||||||
|
.append(GameNode::MoveNode(MoveNode::new(
|
||||||
|
sgf::Color::Black,
|
||||||
|
Move::Move("dp".to_owned()),
|
||||||
|
)))
|
||||||
|
.node_id();
|
||||||
|
|
||||||
|
let _node_d = game_tree
|
||||||
|
.get_mut(node_c)
|
||||||
|
.unwrap()
|
||||||
|
.append(GameNode::MoveNode(MoveNode::new(
|
||||||
|
sgf::Color::Black,
|
||||||
|
Move::Move("dp".to_owned()),
|
||||||
|
)))
|
||||||
|
.node_id();
|
||||||
|
|
||||||
|
let _node_e = game_tree
|
||||||
|
.get_mut(node_c)
|
||||||
|
.unwrap()
|
||||||
|
.append(GameNode::MoveNode(MoveNode::new(
|
||||||
|
sgf::Color::Black,
|
||||||
|
Move::Move("dp".to_owned()),
|
||||||
|
)))
|
||||||
|
.node_id();
|
||||||
|
|
||||||
|
let _node_f = game_tree
|
||||||
|
.get_mut(node_c)
|
||||||
|
.unwrap()
|
||||||
|
.append(GameNode::MoveNode(MoveNode::new(
|
||||||
|
sgf::Color::Black,
|
||||||
|
Move::Move("dp".to_owned()),
|
||||||
|
)))
|
||||||
|
.node_id();
|
||||||
|
|
||||||
|
let _node_g = game_tree
|
||||||
|
.get_mut(node_a)
|
||||||
|
.unwrap()
|
||||||
|
.append(GameNode::MoveNode(MoveNode::new(
|
||||||
|
sgf::Color::Black,
|
||||||
|
Move::Move("dp".to_owned()),
|
||||||
|
)))
|
||||||
|
.node_id();
|
||||||
|
|
||||||
|
let node_h = game_tree
|
||||||
|
.get_mut(node_a)
|
||||||
|
.unwrap()
|
||||||
|
.append(GameNode::MoveNode(MoveNode::new(
|
||||||
|
sgf::Color::Black,
|
||||||
|
Move::Move("dp".to_owned()),
|
||||||
|
)))
|
||||||
|
.node_id();
|
||||||
|
|
||||||
|
let _ = game_tree
|
||||||
|
.get_mut(node_h)
|
||||||
|
.unwrap()
|
||||||
|
.append(GameNode::MoveNode(MoveNode::new(
|
||||||
|
sgf::Color::Black,
|
||||||
|
Move::Move("dp".to_owned()),
|
||||||
|
)))
|
||||||
|
.node_id();
|
||||||
|
|
||||||
|
game_tree
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn it_can_calculate_depth_from_game_tree() {
|
fn it_can_calculate_depth_from_game_tree() {
|
||||||
let mut node_a = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
let game_tree = branching_tree();
|
||||||
let mut node_b = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
let tree = DepthTree::from(&game_tree);
|
||||||
let mut node_c = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
assert_eq!(
|
||||||
let node_d = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
game_tree.root().unwrap().traverse_pre_order().count(),
|
||||||
let node_e = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
tree.0.root().unwrap().traverse_pre_order().count()
|
||||||
let node_f = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
);
|
||||||
let node_g = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let mut node_h = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_i = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
|
|
||||||
node_c.children.push(GameNode::MoveNode(node_d));
|
|
||||||
node_c.children.push(GameNode::MoveNode(node_e));
|
|
||||||
node_c.children.push(GameNode::MoveNode(node_f));
|
|
||||||
|
|
||||||
node_b.children.push(GameNode::MoveNode(node_c));
|
|
||||||
|
|
||||||
node_h.children.push(GameNode::MoveNode(node_i));
|
|
||||||
|
|
||||||
node_a.children.push(GameNode::MoveNode(node_b));
|
|
||||||
node_a.children.push(GameNode::MoveNode(node_g));
|
|
||||||
node_a.children.push(GameNode::MoveNode(node_h));
|
|
||||||
|
|
||||||
let game_tree = GameNode::MoveNode(node_a);
|
|
||||||
|
|
||||||
let tree = Tree::from(&game_tree);
|
|
||||||
|
|
||||||
assert_eq!(tree.max_depth(), 3);
|
assert_eq!(tree.max_depth(), 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A
|
|
||||||
// B G H
|
|
||||||
// C I
|
|
||||||
// D E F
|
|
||||||
#[test]
|
#[test]
|
||||||
fn it_calculates_horizontal_position_of_nodes() {
|
fn it_calculates_horizontal_position_of_nodes() {
|
||||||
let mut node_a = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
let game_tree = branching_tree();
|
||||||
let mut node_b = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
let tree = DepthTree::from(&game_tree);
|
||||||
let mut node_c = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_d = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_e = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_f = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_g = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let mut node_h = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_i = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
|
|
||||||
node_c.children.push(GameNode::MoveNode(node_d));
|
let node_a = tree.root().unwrap();
|
||||||
node_c.children.push(GameNode::MoveNode(node_e));
|
assert_eq!(node_a.data().position(), (0, 0));
|
||||||
node_c.children.push(GameNode::MoveNode(node_f));
|
|
||||||
|
|
||||||
node_b.children.push(GameNode::MoveNode(node_c));
|
let node_b = node_a.first_child().unwrap();
|
||||||
|
assert_eq!(node_b.data().position(), (1, 0));
|
||||||
|
let node_g = node_b.next_sibling().unwrap();
|
||||||
|
assert_eq!(node_g.data().position(), (1, 1));
|
||||||
|
let node_h = node_g.next_sibling().unwrap();
|
||||||
|
assert_eq!(node_h.data().position(), (1, 2));
|
||||||
|
|
||||||
node_h.children.push(GameNode::MoveNode(node_i));
|
let node_c = node_b.first_child().unwrap();
|
||||||
|
assert_eq!(node_c.data().position(), (2, 0));
|
||||||
|
|
||||||
node_a.children.push(GameNode::MoveNode(node_b));
|
let node_d = node_c.first_child().unwrap();
|
||||||
node_a.children.push(GameNode::MoveNode(node_g));
|
assert_eq!(node_d.data().position(), (3, 0));
|
||||||
node_a.children.push(GameNode::MoveNode(node_h));
|
|
||||||
|
|
||||||
let game_tree = GameNode::MoveNode(node_a);
|
let node_i = node_h.first_child().unwrap();
|
||||||
|
assert_eq!(node_i.data().position(), (2, 2));
|
||||||
|
|
||||||
let tree = Tree::from(&game_tree);
|
/*
|
||||||
|
assert_eq!(tree.position(test_tree.node_c), (2, 0));
|
||||||
assert_eq!(tree.position(2), (2, 0));
|
assert_eq!(tree.position(test_tree.node_b), (1, 0));
|
||||||
assert_eq!(tree.position(1), (1, 0));
|
assert_eq!(tree.position(test_tree.node_a), (0, 0));
|
||||||
assert_eq!(tree.position(0), (0, 0));
|
assert_eq!(tree.position(test_tree.node_d), (3, 1));
|
||||||
assert_eq!(tree.position(4), (3, 1));
|
assert_eq!(tree.position(test_tree.node_e), (3, 2));
|
||||||
assert_eq!(tree.position(5), (3, 2));
|
assert_eq!(tree.position(test_tree.node_f), (1, 3));
|
||||||
assert_eq!(tree.position(6), (1, 3));
|
assert_eq!(tree.position(test_tree.node_g), (1, 4));
|
||||||
assert_eq!(tree.position(7), (1, 4));
|
*/
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn breadth_first_iter() {
|
|
||||||
let mut node_a = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let mut node_b = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let mut node_c = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_d = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_e = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_f = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_g = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let mut node_h = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_i = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
|
|
||||||
node_c.children.push(GameNode::MoveNode(node_d.clone()));
|
|
||||||
node_c.children.push(GameNode::MoveNode(node_e.clone()));
|
|
||||||
node_c.children.push(GameNode::MoveNode(node_f.clone()));
|
|
||||||
|
|
||||||
node_b.children.push(GameNode::MoveNode(node_c.clone()));
|
|
||||||
|
|
||||||
node_h.children.push(GameNode::MoveNode(node_i.clone()));
|
|
||||||
|
|
||||||
node_a.children.push(GameNode::MoveNode(node_b.clone()));
|
|
||||||
node_a.children.push(GameNode::MoveNode(node_g.clone()));
|
|
||||||
node_a.children.push(GameNode::MoveNode(node_h.clone()));
|
|
||||||
|
|
||||||
let game_tree = GameNode::MoveNode(node_a.clone());
|
|
||||||
|
|
||||||
let tree = Tree::from(&game_tree);
|
|
||||||
|
|
||||||
let mut iter = tree.bfs_iter();
|
|
||||||
|
|
||||||
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_a.id));
|
|
||||||
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_b.id));
|
|
||||||
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_g.id));
|
|
||||||
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_h.id));
|
|
||||||
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_c.id));
|
|
||||||
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_i.id));
|
|
||||||
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_d.id));
|
|
||||||
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_e.id));
|
|
||||||
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_f.id));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,281 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
|
||||||
|
|
||||||
|
This file is part of On the Grid.
|
||||||
|
|
||||||
|
On the Grid is free software: you can redistribute it and/or modify it under the terms of
|
||||||
|
the GNU General Public License as published by the Free Software Foundation, either version 3 of
|
||||||
|
the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
On the Grid is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||||
|
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with On the Grid. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Currenty my game review is able to show the current game and its tree. Now, I want to start
|
||||||
|
// tracking where in the tree that I am. This should be a combination of the abstract Tree and the
|
||||||
|
// gameTree. Chances are, if I just keep track of where I am in the abstract tree, I can find the
|
||||||
|
// relevant node in the game tree and then reproduce the line to get to that node.
|
||||||
|
//
|
||||||
|
// Moving through the game review tree shouldn't require a full invocatian. This object, and most
|
||||||
|
// other view models, should be exported to the UI.
|
||||||
|
|
||||||
|
use crate::{types::SizeNode, DepthTree, Goban};
|
||||||
|
use nary_tree::{NodeId, NodeRef};
|
||||||
|
use sgf::{GameRecord, Player};
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
|
struct GameReviewViewModelPrivate {
|
||||||
|
// This is the ID of the current position in the game. The ID is specific to the GameRecord,
|
||||||
|
// not the ReviewTree.
|
||||||
|
current_position: Option<NodeId>,
|
||||||
|
game: GameRecord,
|
||||||
|
review_tree: DepthTree,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct GameReviewViewModel {
|
||||||
|
inner: Arc<RwLock<GameReviewViewModelPrivate>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GameReviewViewModel {
|
||||||
|
pub fn new(game: GameRecord) -> Self {
|
||||||
|
let (review_tree, current_position) = if !game.trees.is_empty() {
|
||||||
|
let review_tree = DepthTree::from(&game.trees[0]);
|
||||||
|
let current_position = game.mainline().unwrap().last().map(|nr| nr.node_id());
|
||||||
|
(review_tree, current_position)
|
||||||
|
} else {
|
||||||
|
(DepthTree::default(), None)
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
inner: Arc::new(RwLock::new(GameReviewViewModelPrivate {
|
||||||
|
current_position,
|
||||||
|
game,
|
||||||
|
review_tree,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn black_player(&self) -> Player {
|
||||||
|
self.inner.read().unwrap().game.black_player.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn white_player(&self) -> Player {
|
||||||
|
self.inner.read().unwrap().game.white_player.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn game_view(&self) -> Goban {
|
||||||
|
let inner = self.inner.read().unwrap();
|
||||||
|
|
||||||
|
let mut path: Vec<NodeId> = vec![];
|
||||||
|
let mut current_id = inner.current_position;
|
||||||
|
while current_id.is_some() {
|
||||||
|
let current = current_id.unwrap();
|
||||||
|
path.push(current);
|
||||||
|
current_id = inner.game.trees[0]
|
||||||
|
.get(current)
|
||||||
|
.unwrap()
|
||||||
|
.parent()
|
||||||
|
.map(|parent| parent.node_id());
|
||||||
|
}
|
||||||
|
|
||||||
|
path.reverse();
|
||||||
|
Goban::default()
|
||||||
|
.apply_moves(
|
||||||
|
path.into_iter()
|
||||||
|
.map(|node_id| inner.game.trees[0].get(node_id).unwrap().data()),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
|
||||||
|
/*
|
||||||
|
if let Some(start) = inner.current_position {
|
||||||
|
let mut current_id = start;
|
||||||
|
let mut path = vec![current_id.clone()];
|
||||||
|
|
||||||
|
while let
|
||||||
|
/*
|
||||||
|
let mut current_node = inner.game.trees[0].get(current_position).unwrap();
|
||||||
|
let mut path = vec![current_node.data()];
|
||||||
|
while let Some(parent) = current_node.parent() {
|
||||||
|
path.push(parent.data());
|
||||||
|
current_node = parent;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
path.reverse();
|
||||||
|
Goban::default().apply_moves(path).unwrap()
|
||||||
|
} else {
|
||||||
|
Goban::default()
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn map_tree<F>(&self, f: F)
|
||||||
|
where
|
||||||
|
F: Fn(NodeRef<'_, SizeNode>, Option<NodeId>),
|
||||||
|
{
|
||||||
|
let inner = self.inner.read().unwrap();
|
||||||
|
|
||||||
|
for node in inner.review_tree.bfs_iter() {
|
||||||
|
f(node, inner.current_position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tree_max_depth(&self) -> usize {
|
||||||
|
self.inner.read().unwrap().review_tree.max_depth()
|
||||||
|
}
|
||||||
|
|
||||||
|
// When moving forward on the tree, I grab the first child by default. I can then just advance
|
||||||
|
// the board state by applying the child.
|
||||||
|
pub fn next_move(&self) {
|
||||||
|
let mut inner = self.inner.write().unwrap();
|
||||||
|
let current_position = inner.current_position.clone();
|
||||||
|
match current_position {
|
||||||
|
Some(current_position) => {
|
||||||
|
let current_id = current_position.clone();
|
||||||
|
let node = inner.game.trees[0].get(current_id).unwrap();
|
||||||
|
if let Some(next_id) = node.first_child().map(|child| child.node_id()) {
|
||||||
|
inner.current_position = Some(next_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
inner.current_position = inner.game.trees[0].root().map(|node| node.node_id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When moving backwards, I jump up to the parent. I'll then rebuild the board state from the
|
||||||
|
// root.
|
||||||
|
pub fn previous_move(&mut self) {
|
||||||
|
let mut inner = self.inner.write().unwrap();
|
||||||
|
if let Some(current_position) = inner.current_position {
|
||||||
|
let current_node = inner.game.trees[0]
|
||||||
|
.get(current_position)
|
||||||
|
.expect("current_position should always correspond to a node in the tree");
|
||||||
|
if let Some(parent_node) = current_node.parent() {
|
||||||
|
inner.current_position = Some(parent_node.node_id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_variant(&self) {
|
||||||
|
println!("move to the next variant amongst the options available");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn previous_variant(&self) {
|
||||||
|
println!("move to the previous variant amongst the options available");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use crate::{Color, Coordinate};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
fn with_game_record<F>(test: F)
|
||||||
|
where
|
||||||
|
F: FnOnce(GameReviewViewModel),
|
||||||
|
{
|
||||||
|
let records = sgf::parse_sgf_file(&Path::new("../../sgf/test_data/branch_test.sgf"))
|
||||||
|
.expect("to successfully load the test file");
|
||||||
|
let record = records[0]
|
||||||
|
.as_ref()
|
||||||
|
.expect("to have successfully loaded the test record");
|
||||||
|
|
||||||
|
let view_model = GameReviewViewModel::new(record.clone());
|
||||||
|
|
||||||
|
test(view_model);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_generates_a_mainline_board() {
|
||||||
|
with_game_record(|view| {
|
||||||
|
let goban = view.game_view();
|
||||||
|
|
||||||
|
for row in 0..18 {
|
||||||
|
for column in 0..18 {
|
||||||
|
if row == 3 && column == 3 {
|
||||||
|
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::White));
|
||||||
|
} else if row == 15 && column == 3 {
|
||||||
|
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::White));
|
||||||
|
} else if row == 3 && column == 15 {
|
||||||
|
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::Black));
|
||||||
|
} else if row == 15 && column == 14 {
|
||||||
|
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::Black));
|
||||||
|
} else {
|
||||||
|
assert_eq!(
|
||||||
|
goban.stone(&Coordinate { row, column }),
|
||||||
|
None,
|
||||||
|
"{} {}",
|
||||||
|
row,
|
||||||
|
column
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_moves_to_the_previous_mainline_move() {
|
||||||
|
with_game_record(|mut view| {
|
||||||
|
view.previous_move();
|
||||||
|
let goban = view.game_view();
|
||||||
|
|
||||||
|
for row in 0..18 {
|
||||||
|
for column in 0..18 {
|
||||||
|
if row == 3 && column == 3 {
|
||||||
|
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::White));
|
||||||
|
} else if row == 3 && column == 15 {
|
||||||
|
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::Black));
|
||||||
|
} else if row == 15 && column == 14 {
|
||||||
|
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::Black));
|
||||||
|
} else {
|
||||||
|
assert_eq!(
|
||||||
|
goban.stone(&Coordinate { row, column }),
|
||||||
|
None,
|
||||||
|
"{} {}",
|
||||||
|
row,
|
||||||
|
column
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_moves_to_the_next_node() {
|
||||||
|
with_game_record(|mut view| {
|
||||||
|
view.previous_move();
|
||||||
|
view.next_move();
|
||||||
|
let goban = view.game_view();
|
||||||
|
|
||||||
|
for row in 0..18 {
|
||||||
|
for column in 0..18 {
|
||||||
|
if row == 3 && column == 3 {
|
||||||
|
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::White));
|
||||||
|
} else if row == 15 && column == 3 {
|
||||||
|
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::White));
|
||||||
|
} else if row == 3 && column == 15 {
|
||||||
|
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::Black));
|
||||||
|
} else if row == 15 && column == 14 {
|
||||||
|
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::Black));
|
||||||
|
} else {
|
||||||
|
assert_eq!(
|
||||||
|
goban.stone(&Coordinate { row, column }),
|
||||||
|
None,
|
||||||
|
"{} {}",
|
||||||
|
row,
|
||||||
|
column
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
|
||||||
|
|
||||||
|
This file is part of On the Grid.
|
||||||
|
|
||||||
|
On the Grid is free software: you can redistribute it and/or modify it under the terms of
|
||||||
|
the GNU General Public License as published by the Free Software Foundation, either version 3 of
|
||||||
|
the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
On the Grid is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||||
|
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with On the Grid. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
mod game_review;
|
||||||
|
pub use game_review::GameReviewViewModel;
|
||||||
|
|
|
@ -3,5 +3,7 @@
|
||||||
<gresource prefix="/com/luminescent-dreams/otg-gtk/">
|
<gresource prefix="/com/luminescent-dreams/otg-gtk/">
|
||||||
<file>wood_texture.jpg</file>
|
<file>wood_texture.jpg</file>
|
||||||
<file>style.css</file>
|
<file>style.css</file>
|
||||||
|
<file>black_stone.png</file>
|
||||||
|
<file>white_stone.png</file>
|
||||||
</gresource>
|
</gresource>
|
||||||
</gresources>
|
</gresources>
|
||||||
|
|
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 350 KiB After Width: | Height: | Size: 698 KiB |
|
@ -14,12 +14,14 @@ GNU General Public License for more details.
|
||||||
You should have received a copy of the GNU General Public License along with On the Grid. If not, see <https://www.gnu.org/licenses/>.
|
You should have received a copy of the GNU General Public License along with On the Grid. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use crate::CoreApi;
|
use crate::{CoreApi, ResourceManager};
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
|
|
||||||
|
use glib::Propagation;
|
||||||
|
use gtk::{gdk::Key, EventControllerKey};
|
||||||
use otg_core::{
|
use otg_core::{
|
||||||
settings::{SettingsRequest, SettingsResponse},
|
settings::{SettingsRequest, SettingsResponse},
|
||||||
CoreRequest, CoreResponse,
|
CoreRequest, CoreResponse, GameReviewViewModel,
|
||||||
};
|
};
|
||||||
use sgf::GameRecord;
|
use sgf::GameRecord;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
@ -58,10 +60,12 @@ pub struct AppWindow {
|
||||||
// Not liking this, but I have to keep track of the settings view model separately from
|
// Not liking this, but I have to keep track of the settings view model separately from
|
||||||
// anything else. I'll have to look into this later.
|
// anything else. I'll have to look into this later.
|
||||||
settings_view_model: Arc<RwLock<Option<SettingsView>>>,
|
settings_view_model: Arc<RwLock<Option<SettingsView>>>,
|
||||||
|
|
||||||
|
resources: ResourceManager,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppWindow {
|
impl AppWindow {
|
||||||
pub fn new(app: &adw::Application, core: CoreApi) -> Self {
|
pub fn new(app: &adw::Application, core: CoreApi, resources: ResourceManager) -> Self {
|
||||||
let window = Self::setup_window(app);
|
let window = Self::setup_window(app);
|
||||||
let overlay = Self::setup_overlay();
|
let overlay = Self::setup_overlay();
|
||||||
let stack = adw::NavigationView::new();
|
let stack = adw::NavigationView::new();
|
||||||
|
@ -77,6 +81,7 @@ impl AppWindow {
|
||||||
overlay,
|
overlay,
|
||||||
core,
|
core,
|
||||||
settings_view_model: Default::default(),
|
settings_view_model: Default::default(),
|
||||||
|
resources,
|
||||||
};
|
};
|
||||||
|
|
||||||
let home = s.setup_home();
|
let home = s.setup_home();
|
||||||
|
@ -88,13 +93,32 @@ impl AppWindow {
|
||||||
|
|
||||||
pub fn open_game_review(&self, game_record: GameRecord) {
|
pub fn open_game_review(&self, game_record: GameRecord) {
|
||||||
let header = adw::HeaderBar::new();
|
let header = adw::HeaderBar::new();
|
||||||
let game_review = GameReview::new(self.core.clone(), game_record);
|
let game_review = GameReview::new(
|
||||||
|
GameReviewViewModel::new(game_record),
|
||||||
|
self.resources.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
let layout = gtk::Box::builder()
|
let layout = gtk::Box::builder()
|
||||||
.orientation(gtk::Orientation::Vertical)
|
.orientation(gtk::Orientation::Vertical)
|
||||||
.build();
|
.build();
|
||||||
layout.append(&header);
|
layout.append(&header);
|
||||||
layout.append(&game_review);
|
layout.append(&game_review.widget());
|
||||||
|
|
||||||
|
// This controller ensures that navigational keypresses get sent to the game review so that
|
||||||
|
// they're not changing the cursor focus in the app.
|
||||||
|
let keypress_controller = EventControllerKey::new();
|
||||||
|
keypress_controller.connect_key_pressed({
|
||||||
|
move |s, key, _, _| {
|
||||||
|
println!("layout keypress: {}", key);
|
||||||
|
if s.forward(&game_review.widget()) {
|
||||||
|
Propagation::Stop
|
||||||
|
} else {
|
||||||
|
Propagation::Proceed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
layout.add_controller(keypress_controller);
|
||||||
|
|
||||||
let page = adw::NavigationPage::builder()
|
let page = adw::NavigationPage::builder()
|
||||||
.can_pop(true)
|
.can_pop(true)
|
||||||
|
|
|
@ -35,14 +35,10 @@ You should have received a copy of the GNU General Public License along with On
|
||||||
// Now, we know what kind of object we have for the current board representation. Let's make use of
|
// Now, we know what kind of object we have for the current board representation. Let's make use of
|
||||||
// that.
|
// that.
|
||||||
|
|
||||||
use crate::perftrace;
|
use crate::{perftrace, Resource, ResourceManager};
|
||||||
|
|
||||||
use glib::Object;
|
use glib::Object;
|
||||||
use gtk::{
|
use gtk::{gdk_pixbuf::Pixbuf, prelude::*, subclass::prelude::*};
|
||||||
prelude::*,
|
|
||||||
subclass::prelude::*,
|
|
||||||
};
|
|
||||||
|
|
||||||
use otg_core::{Color, Coordinate};
|
use otg_core::{Color, Coordinate};
|
||||||
use std::{cell::RefCell, rc::Rc};
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
|
@ -54,6 +50,7 @@ const MARGIN: i32 = 20;
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct GobanPrivate {
|
pub struct GobanPrivate {
|
||||||
board_state: Rc<RefCell<otg_core::Goban>>,
|
board_state: Rc<RefCell<otg_core::Goban>>,
|
||||||
|
resource_manager: Rc<RefCell<Option<ResourceManager>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GobanPrivate {}
|
impl GobanPrivate {}
|
||||||
|
@ -86,10 +83,11 @@ glib::wrapper! {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Goban {
|
impl Goban {
|
||||||
pub fn new(board_state: otg_core::Goban) -> Self {
|
pub fn new(board_state: otg_core::Goban, resources: ResourceManager) -> Self {
|
||||||
let s: Self = Object::builder().build();
|
let s: Self = Object::builder().build();
|
||||||
|
|
||||||
*s.imp().board_state.borrow_mut() = board_state;
|
*s.imp().board_state.borrow_mut() = board_state;
|
||||||
|
*s.imp().resource_manager.borrow_mut() = Some(resources);
|
||||||
s.set_width_request(WIDTH);
|
s.set_width_request(WIDTH);
|
||||||
s.set_height_request(HEIGHT);
|
s.set_height_request(HEIGHT);
|
||||||
|
|
||||||
|
@ -103,43 +101,40 @@ impl Goban {
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
fn redraw(&self, ctx: &cairo::Context, width: i32, height: i32) {
|
pub fn set_board_state(&mut self, board_state: otg_core::Goban) {
|
||||||
println!("{} x {}", width, height);
|
*self.imp().board_state.borrow_mut() = board_state;
|
||||||
/*
|
self.queue_draw();
|
||||||
let wood_texture = resources_lookup_data(
|
}
|
||||||
"/com/luminescent-dreams/otg-gtk/wood_texture.jpg",
|
|
||||||
gio::ResourceLookupFlags::NONE,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let background = ImageReader::new(Cursor::new(wood_texture))
|
fn redraw(&self, ctx: &cairo::Context, width: i32, height: i32) {
|
||||||
.with_guessed_format()
|
let background = self
|
||||||
.unwrap()
|
.imp()
|
||||||
.decode();
|
.resource_manager
|
||||||
let background = background.map(|background| {
|
.borrow()
|
||||||
Pixbuf::from_bytes(
|
.as_ref()
|
||||||
&glib::Bytes::from(background.as_bytes()),
|
.and_then(|r| r.resource("/com/luminescent-dreams/otg-gtk/wood_texture.jpg"));
|
||||||
gtk::gdk_pixbuf::Colorspace::Rgb,
|
|
||||||
false,
|
let black_texture = self
|
||||||
8,
|
.imp()
|
||||||
background.width() as i32,
|
.resource_manager
|
||||||
background.height() as i32,
|
.borrow()
|
||||||
background.to_rgb8().sample_layout().height_stride as i32,
|
.as_ref()
|
||||||
)
|
.and_then(|r| r.resource("/com/luminescent-dreams/otg-gtk/black_stone.png"));
|
||||||
.scale_simple(WIDTH, HEIGHT, InterpType::Nearest)
|
|
||||||
});
|
let white_texture = self
|
||||||
|
.imp()
|
||||||
|
.resource_manager
|
||||||
|
.borrow()
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|r| r.resource("/com/luminescent-dreams/otg-gtk/white_stone.png"));
|
||||||
|
|
||||||
match background {
|
match background {
|
||||||
Ok(Some(ref background)) => {
|
Some(Resource::Image(ref background)) => {
|
||||||
ctx.set_source_pixbuf(background, 0., 0.);
|
ctx.set_source_pixbuf(background, 0., 0.);
|
||||||
ctx.paint().expect("paint should never fail");
|
ctx.paint().expect("paint should never fail");
|
||||||
}
|
}
|
||||||
Ok(None) | Err(_) => ctx.set_source_rgb(0.7, 0.7, 0.7),
|
None => ctx.set_source_rgb(0.7, 0.7, 0.7),
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
ctx.set_source_rgb(0.7, 0.7, 0.7);
|
|
||||||
let _ = ctx.paint();
|
|
||||||
|
|
||||||
let board = self.imp().board_state.borrow();
|
let board = self.imp().board_state.borrow();
|
||||||
|
|
||||||
|
@ -148,12 +143,14 @@ impl Goban {
|
||||||
let hspace_between = ((width - 40) as f64) / ((board.size.width - 1) as f64);
|
let hspace_between = ((width - 40) as f64) / ((board.size.width - 1) as f64);
|
||||||
let vspace_between = ((height - 40) as f64) / ((board.size.height - 1) as f64);
|
let vspace_between = ((height - 40) as f64) / ((board.size.height - 1) as f64);
|
||||||
|
|
||||||
let pen = Pen {
|
let pen = Pen::new(
|
||||||
x_offset: MARGIN as f64,
|
MARGIN as f64,
|
||||||
y_offset: MARGIN as f64,
|
MARGIN as f64,
|
||||||
hspace_between,
|
hspace_between,
|
||||||
vspace_between,
|
vspace_between,
|
||||||
};
|
black_texture,
|
||||||
|
white_texture,
|
||||||
|
);
|
||||||
|
|
||||||
(0..board.size.width).for_each(|col| {
|
(0..board.size.width).for_each(|col| {
|
||||||
ctx.move_to(
|
ctx.move_to(
|
||||||
|
@ -188,8 +185,8 @@ impl Goban {
|
||||||
|
|
||||||
(0..board.size.height).for_each(|row| {
|
(0..board.size.height).for_each(|row| {
|
||||||
(0..board.size.width).for_each(|column| {
|
(0..board.size.width).for_each(|column| {
|
||||||
match board.stone(&Coordinate{ row, column }) {
|
match board.stone(&Coordinate { row, column }) {
|
||||||
None => {},
|
None => {}
|
||||||
Some(Color::White) => pen.stone(ctx, row, column, Color::White, None),
|
Some(Color::White) => pen.stone(ctx, row, column, Color::White, None),
|
||||||
Some(Color::Black) => pen.stone(ctx, row, column, Color::Black, None),
|
Some(Color::Black) => pen.stone(ctx, row, column, Color::Black, None),
|
||||||
}
|
}
|
||||||
|
@ -203,9 +200,37 @@ struct Pen {
|
||||||
y_offset: f64,
|
y_offset: f64,
|
||||||
hspace_between: f64,
|
hspace_between: f64,
|
||||||
vspace_between: f64,
|
vspace_between: f64,
|
||||||
|
black_stone: Option<Pixbuf>,
|
||||||
|
white_stone: Option<Pixbuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Pen {
|
impl Pen {
|
||||||
|
fn new(
|
||||||
|
x_offset: f64,
|
||||||
|
y_offset: f64,
|
||||||
|
hspace_between: f64,
|
||||||
|
vspace_between: f64,
|
||||||
|
black_stone: Option<Resource>,
|
||||||
|
white_stone: Option<Resource>,
|
||||||
|
) -> Self {
|
||||||
|
let black_stone = match black_stone {
|
||||||
|
Some(Resource::Image(img)) => Some(img),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let white_stone = match white_stone {
|
||||||
|
Some(Resource::Image(img)) => Some(img),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
Pen {
|
||||||
|
x_offset,
|
||||||
|
y_offset,
|
||||||
|
hspace_between,
|
||||||
|
vspace_between,
|
||||||
|
black_stone,
|
||||||
|
white_stone,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn star_point(&self, context: &cairo::Context, row: u8, col: u8) {
|
fn star_point(&self, context: &cairo::Context, row: u8, col: u8) {
|
||||||
context.arc(
|
context.arc(
|
||||||
self.x_offset + (col as f64) * self.hspace_between,
|
self.x_offset + (col as f64) * self.hspace_between,
|
||||||
|
@ -217,20 +242,28 @@ impl Pen {
|
||||||
let _ = context.fill();
|
let _ = context.fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stone(
|
fn stone(&self, ctx: &cairo::Context, row: u8, col: u8, color: Color, _liberties: Option<u8>) {
|
||||||
&self,
|
let (x_loc, y_loc) = self.stone_location(row, col);
|
||||||
context: &cairo::Context,
|
|
||||||
row: u8,
|
|
||||||
col: u8,
|
|
||||||
color: Color,
|
|
||||||
liberties: Option<u8>,
|
|
||||||
) {
|
|
||||||
match color {
|
match color {
|
||||||
Color::White => context.set_source_rgb(0.9, 0.9, 0.9),
|
Color::White => match self.white_stone {
|
||||||
Color::Black => context.set_source_rgb(0.0, 0.0, 0.0),
|
Some(ref white_stone) => ctx.set_source_pixbuf(white_stone, x_loc, y_loc),
|
||||||
|
None => ctx.set_source_rgb(0.9, 0.9, 0.9),
|
||||||
|
},
|
||||||
|
Color::Black => match self.black_stone {
|
||||||
|
Some(ref black_stone) => ctx.set_source_pixbuf(black_stone, x_loc, y_loc),
|
||||||
|
None => ctx.set_source_rgb(0.0, 0.0, 0.0),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ctx.paint().expect("paint should never fail");
|
||||||
|
/*
|
||||||
|
match color {
|
||||||
|
Color::White => ctx.set_source_rgb(0.9, 0.9, 0.9),
|
||||||
|
Color::Black => ctx.set_source_rgb(0.0, 0.0, 0.0),
|
||||||
};
|
};
|
||||||
self.draw_stone(context, row, col);
|
self.draw_stone(ctx, row, col);
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
if let Some(liberties) = liberties {
|
if let Some(liberties) = liberties {
|
||||||
let stone_location = self.stone_location(row, col);
|
let stone_location = self.stone_location(row, col);
|
||||||
context.set_source_rgb(1., 0., 1.);
|
context.set_source_rgb(1., 0., 1.);
|
||||||
|
@ -238,28 +271,30 @@ impl Pen {
|
||||||
context.move_to(stone_location.0 - 10., stone_location.1 + 10.);
|
context.move_to(stone_location.0 - 10., stone_location.1 + 10.);
|
||||||
let _ = context.show_text(&format!("{}", liberties));
|
let _ = context.show_text(&format!("{}", liberties));
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
fn ghost_stone(&self, context: &cairo::Context, row: u8, col: u8, color: Color) {
|
fn ghost_stone(&self, ctx: &cairo::Context, row: u8, col: u8, color: Color) {
|
||||||
match color {
|
match color {
|
||||||
Color::White => context.set_source_rgba(0.9, 0.9, 0.9, 0.5),
|
Color::White => ctx.set_source_rgba(0.9, 0.9, 0.9, 0.5),
|
||||||
Color::Black => context.set_source_rgba(0.0, 0.0, 0.0, 0.5),
|
Color::Black => ctx.set_source_rgba(0.0, 0.0, 0.0, 0.5),
|
||||||
};
|
};
|
||||||
self.draw_stone(context, row, col);
|
self.draw_stone(ctx, row, col);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_stone(&self, context: &cairo::Context, row: u8, col: u8) {
|
fn draw_stone(&self, ctx: &cairo::Context, row: u8, col: u8) {
|
||||||
let radius = self.hspace_between / 2. - 2.;
|
let radius = self.hspace_between / 2. - 2.;
|
||||||
let (x_loc, y_loc) = self.stone_location(row, col);
|
let (x_loc, y_loc) = self.stone_location(row, col);
|
||||||
context.arc(x_loc, y_loc, radius, 0.0, 2.0 * std::f64::consts::PI);
|
ctx.arc(x_loc, y_loc, radius, 0.0, 2.0 * std::f64::consts::PI);
|
||||||
let _ = context.fill();
|
let _ = ctx.fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stone_location(&self, row: u8, col: u8) -> (f64, f64) {
|
fn stone_location(&self, row: u8, col: u8) -> (f64, f64) {
|
||||||
|
let radius = self.hspace_between / 2. - 2.;
|
||||||
(
|
(
|
||||||
self.x_offset + (col as f64) * self.hspace_between,
|
self.x_offset + (col as f64) * self.hspace_between - radius,
|
||||||
self.y_offset + (row as f64) * self.vspace_between,
|
self.y_offset + (row as f64) * self.vspace_between - radius,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,48 +15,42 @@ You should have received a copy of the GNU General Public License along with On
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use cairo::Context;
|
use cairo::Context;
|
||||||
use glib::Object;
|
use gtk::prelude::*;
|
||||||
use gtk::{prelude::*, subclass::prelude::*};
|
use otg_core::GameReviewViewModel;
|
||||||
use otg_core::Tree;
|
|
||||||
use sgf::{GameNode, GameRecord};
|
|
||||||
use std::{cell::RefCell, rc::Rc};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
const WIDTH: i32 = 200;
|
const WIDTH: i32 = 200;
|
||||||
const HEIGHT: i32 = 800;
|
const HEIGHT: i32 = 800;
|
||||||
|
|
||||||
#[derive(Default)]
|
const RADIUS: f64 = 7.5;
|
||||||
pub struct ReviewTreePrivate {
|
const HIGHLIGHT_WIDTH: f64 = 4.;
|
||||||
record: Rc<RefCell<Option<GameRecord>>>,
|
const SPACING: f64 = 30.;
|
||||||
tree: Rc<RefCell<Option<Tree<Uuid>>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[glib::object_subclass]
|
#[derive(Clone)]
|
||||||
impl ObjectSubclass for ReviewTreePrivate {
|
pub struct ReviewTree {
|
||||||
const NAME: &'static str = "ReviewTree";
|
widget: gtk::ScrolledWindow,
|
||||||
type Type = ReviewTree;
|
drawing_area: gtk::DrawingArea,
|
||||||
type ParentType = gtk::DrawingArea;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ObjectImpl for ReviewTreePrivate {}
|
view: GameReviewViewModel,
|
||||||
impl WidgetImpl for ReviewTreePrivate {}
|
|
||||||
impl DrawingAreaImpl for ReviewTreePrivate {}
|
|
||||||
|
|
||||||
glib::wrapper! {
|
|
||||||
pub struct ReviewTree(ObjectSubclass<ReviewTreePrivate>) @extends gtk::Widget, gtk::DrawingArea;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ReviewTree {
|
impl ReviewTree {
|
||||||
pub fn new(record: GameRecord) -> Self {
|
pub fn new(view: GameReviewViewModel) -> ReviewTree {
|
||||||
let s: Self = Object::new();
|
let drawing_area = gtk::DrawingArea::new();
|
||||||
|
let widget = gtk::ScrolledWindow::builder().child(&drawing_area).build();
|
||||||
|
|
||||||
*s.imp().tree.borrow_mut() = Some(Tree::from(&record.children[0]));
|
widget.set_width_request(WIDTH);
|
||||||
*s.imp().record.borrow_mut() = Some(record);
|
widget.set_height_request(HEIGHT);
|
||||||
|
|
||||||
s.set_width_request(WIDTH);
|
// TODO: figure out the maximum width of the tree so that we can also set a width request
|
||||||
s.set_height_request(HEIGHT);
|
drawing_area.set_height_request(view.tree_max_depth() as i32 * SPACING as i32);
|
||||||
|
|
||||||
s.set_draw_func({
|
let s = Self {
|
||||||
|
widget,
|
||||||
|
drawing_area,
|
||||||
|
view,
|
||||||
|
};
|
||||||
|
|
||||||
|
s.drawing_area.set_draw_func({
|
||||||
let s = s.clone();
|
let s = s.clone();
|
||||||
move |_, ctx, width, height| {
|
move |_, ctx, width, height| {
|
||||||
s.redraw(ctx, width, height);
|
s.redraw(ctx, width, height);
|
||||||
|
@ -66,170 +60,63 @@ impl ReviewTree {
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn redraw(&self, ctx: &Context, _width: i32, _height: i32) {
|
pub fn queue_draw(&self) {
|
||||||
println!("redraw");
|
self.drawing_area.queue_draw();
|
||||||
let tree: &Option<Tree<Uuid>> = &self.imp().tree.borrow();
|
}
|
||||||
match tree {
|
|
||||||
Some(ref tree) => {
|
fn redraw(&self, ctx: &Context, _width: i32, _height: i32) {
|
||||||
for node in tree.bfs_iter() {
|
#[allow(deprecated)]
|
||||||
// draw a circle given the coordinates of the nodes
|
let context = WidgetExt::style_context(&self.widget);
|
||||||
// I don't know the indent. How do I keep track of that? Do I track the position of
|
#[allow(deprecated)]
|
||||||
// the parent? do I need to just make it more intrinsically a part of the position
|
let foreground_color = context.lookup_color("sidebar_fg_color").unwrap();
|
||||||
// code?
|
#[allow(deprecated)]
|
||||||
ctx.set_source_rgb(0.7, 0.7, 0.7);
|
let accent_color = context.lookup_color("accent_color").unwrap();
|
||||||
let (row, column) = tree.position(node.id);
|
|
||||||
println!("[{}] {} x {}", node.id, row, column);
|
self.view.map_tree(move |node, current| {
|
||||||
let y = (row as f64) * 20. + 10.;
|
let parent = node.parent();
|
||||||
let x = (column as f64) * 20. + 10.;
|
ctx.set_source_rgb(
|
||||||
ctx.arc(x, y, 5., 0., 2. * std::f64::consts::PI);
|
foreground_color.red().into(),
|
||||||
|
foreground_color.green().into(),
|
||||||
|
foreground_color.blue().into(),
|
||||||
|
);
|
||||||
|
let (row, column) = node.data().position();
|
||||||
|
let y = (row as f64) * SPACING + RADIUS * 2.;
|
||||||
|
let x = (column as f64) * SPACING + RADIUS * 2.;
|
||||||
|
ctx.arc(x, y, RADIUS, 0., 2. * std::f64::consts::PI);
|
||||||
|
let _ = ctx.fill();
|
||||||
|
|
||||||
|
if let Some(parent) = parent {
|
||||||
|
ctx.set_line_width(1.);
|
||||||
|
let (row, column) = parent.data().position();
|
||||||
|
let py = (row as f64) * SPACING + RADIUS * 2.;
|
||||||
|
let px = (column as f64) * SPACING + RADIUS * 2.;
|
||||||
|
ctx.move_to(px, py);
|
||||||
|
ctx.line_to(x, y);
|
||||||
let _ = ctx.stroke();
|
let _ = ctx.stroke();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
None => {
|
|
||||||
// if there is no tree present, then there's nothing to draw!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://llimllib.github.io/pymag-trees/
|
if current == Some(node.data().game_node_id) {
|
||||||
// I want to take advantage of the Wetherell Shannon algorithm, but I want some variations. In
|
ctx.set_line_width(HIGHLIGHT_WIDTH);
|
||||||
// their diagram, they got a tree that looks like this.
|
ctx.set_source_rgb(
|
||||||
//
|
accent_color.red().into(),
|
||||||
// O
|
accent_color.green().into(),
|
||||||
// |\
|
accent_color.blue().into(),
|
||||||
// O O
|
|
||||||
// |\ \ \
|
|
||||||
// O O O O
|
|
||||||
// |\ |\
|
|
||||||
// O O O O
|
|
||||||
//
|
|
||||||
// In the same circumstance, what I want is this:
|
|
||||||
//
|
|
||||||
// O--
|
|
||||||
// | \
|
|
||||||
// O O
|
|
||||||
// |\ |\
|
|
||||||
// O O O O
|
|
||||||
// |\
|
|
||||||
// O O
|
|
||||||
//
|
|
||||||
// In order to keep things from being overly smooshed, I want to ensure that if a branch overlaps
|
|
||||||
// with another branch, there is some extra drawing space. This might actually be similar to adding
|
|
||||||
// the principal that "A parent should be centered over its children".
|
|
||||||
//
|
|
||||||
// So, given a tree, I need to know how many children exist at each level. Then I build parents
|
|
||||||
// atop the children. At level 3, I have four children, and that happens to be the maximum width of
|
|
||||||
// the graph.
|
|
||||||
//
|
|
||||||
// A bottom-up traversal:
|
|
||||||
// - Figure out the number of nodes at each depth
|
|
||||||
|
|
||||||
/*
|
|
||||||
struct Tree {
|
|
||||||
width: Vec<usize>, // the total width of the tree at each depth
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
use sgf::{Color, GameNode, Move, MoveNode};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_calculates_width_for_single_node() {
|
|
||||||
let node = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned())));
|
|
||||||
|
|
||||||
assert_eq!(node_width(&node), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_calculates_width_for_node_with_children() {
|
|
||||||
let mut node_a = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_b = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned())));
|
|
||||||
let node_c = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned())));
|
|
||||||
let node_d = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned())));
|
|
||||||
|
|
||||||
node_a.children.push(node_b);
|
|
||||||
node_a.children.push(node_c);
|
|
||||||
node_a.children.push(node_d);
|
|
||||||
|
|
||||||
assert_eq!(node_width(&GameNode::MoveNode(node_a)), 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A
|
|
||||||
// B E
|
|
||||||
// C D
|
|
||||||
#[test]
|
|
||||||
fn it_calculates_width_with_one_deep_child() {
|
|
||||||
let mut node_a = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let mut node_b = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_c = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_d = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_e = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
|
|
||||||
node_b.children.push(GameNode::MoveNode(node_c));
|
|
||||||
node_b.children.push(GameNode::MoveNode(node_d));
|
|
||||||
assert_eq!(node_width(&GameNode::MoveNode(node_b.clone())), 2);
|
|
||||||
|
|
||||||
node_a.children.push(GameNode::MoveNode(node_b));
|
|
||||||
node_a.children.push(GameNode::MoveNode(node_e));
|
|
||||||
assert_eq!(node_width(&GameNode::MoveNode(node_a)), 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A
|
|
||||||
// B G H
|
|
||||||
// C I
|
|
||||||
// D E F
|
|
||||||
#[test]
|
|
||||||
fn it_calculates_a_complex_tree() {
|
|
||||||
let mut node_a = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let mut node_b = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let mut node_c = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_d = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_e = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_f = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_g = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let mut node_h = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_i = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
|
|
||||||
node_c.children.push(GameNode::MoveNode(node_d));
|
|
||||||
node_c.children.push(GameNode::MoveNode(node_e));
|
|
||||||
node_c.children.push(GameNode::MoveNode(node_f));
|
|
||||||
assert_eq!(node_width(&GameNode::MoveNode(node_c.clone())), 3);
|
|
||||||
|
|
||||||
node_b.children.push(GameNode::MoveNode(node_c));
|
|
||||||
assert_eq!(node_width(&GameNode::MoveNode(node_b.clone())), 3);
|
|
||||||
|
|
||||||
node_h.children.push(GameNode::MoveNode(node_i));
|
|
||||||
|
|
||||||
node_a.children.push(GameNode::MoveNode(node_b));
|
|
||||||
node_a.children.push(GameNode::MoveNode(node_g));
|
|
||||||
node_a.children.push(GameNode::MoveNode(node_h));
|
|
||||||
// This should be 4 if I were collapsing levels correctly, but it is 5 until I return to
|
|
||||||
// figure that step out.
|
|
||||||
assert_eq!(node_width(&GameNode::MoveNode(node_a.clone())), 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_nodes_children_get_separate_columns() {
|
|
||||||
let mut node_a = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_b = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned())));
|
|
||||||
let node_c = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned())));
|
|
||||||
let node_d = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned())));
|
|
||||||
|
|
||||||
node_a.children.push(node_b.clone());
|
|
||||||
node_a.children.push(node_c.clone());
|
|
||||||
node_a.children.push(node_d.clone());
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
node_children_columns(&GameNode::MoveNode(node_a)),
|
|
||||||
vec![0, 1, 2]
|
|
||||||
);
|
);
|
||||||
|
ctx.arc(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
RADIUS + HIGHLIGHT_WIDTH / 2.,
|
||||||
|
0.,
|
||||||
|
2. * std::f64::consts::PI,
|
||||||
|
);
|
||||||
|
let _ = ctx.stroke();
|
||||||
|
ctx.set_line_width(2.);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
pub fn widget(&self) -> gtk::Widget {
|
||||||
fn text_renderer() {
|
self.widget.clone().upcast::<gtk::Widget>()
|
||||||
assert!(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,10 +21,12 @@ pub use app_window::AppWindow;
|
||||||
|
|
||||||
mod views;
|
mod views;
|
||||||
|
|
||||||
use async_std::task::{yield_now};
|
use async_std::task::yield_now;
|
||||||
use otg_core::{Core, Observable, CoreRequest, CoreResponse};
|
use gio::resources_lookup_data;
|
||||||
use std::{rc::Rc};
|
use gtk::gdk_pixbuf::{Colorspace, InterpType, Pixbuf};
|
||||||
|
use image::{io::Reader as ImageReader, ImageError};
|
||||||
|
use otg_core::{Core, CoreRequest, CoreResponse, Observable};
|
||||||
|
use std::{cell::RefCell, collections::HashMap, io::Cursor, rc::Rc};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct CoreApi {
|
pub struct CoreApi {
|
||||||
|
@ -37,6 +39,94 @@ impl CoreApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum Resource {
|
||||||
|
Image(Pixbuf),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ResourceManager {
|
||||||
|
resources: Rc<RefCell<HashMap<String, Resource>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ResourceManager {
|
||||||
|
fn default() -> Self {
|
||||||
|
let mut resources = HashMap::new();
|
||||||
|
|
||||||
|
for (path, xres, yres, transparency) in [
|
||||||
|
(
|
||||||
|
"/com/luminescent-dreams/otg-gtk/wood_texture.jpg",
|
||||||
|
840,
|
||||||
|
840,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"/com/luminescent-dreams/otg-gtk/black_stone.png",
|
||||||
|
40,
|
||||||
|
40,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"/com/luminescent-dreams/otg-gtk/white_stone.png",
|
||||||
|
40,
|
||||||
|
40,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
match perftrace(&format!("loading {}", path), || {
|
||||||
|
Self::load_image(path, transparency, xres, yres)
|
||||||
|
}) {
|
||||||
|
Ok(Some(image)) => {
|
||||||
|
resources.insert(path.to_owned(), Resource::Image(image));
|
||||||
|
}
|
||||||
|
Ok(None) => println!("no image in resource bundle for {}", path),
|
||||||
|
Err(err) => println!("failed to load image {}: {}", path, err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
resources: Rc::new(RefCell::new(resources)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResourceManager {
|
||||||
|
pub fn resource(&self, path: &str) -> Option<Resource> {
|
||||||
|
self.resources.borrow().get(path).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_image(
|
||||||
|
path: &str,
|
||||||
|
transparency: bool,
|
||||||
|
width: i32,
|
||||||
|
height: i32,
|
||||||
|
) -> Result<Option<Pixbuf>, ImageError> {
|
||||||
|
let image_bytes = resources_lookup_data(path, gio::ResourceLookupFlags::NONE).unwrap();
|
||||||
|
|
||||||
|
let image = ImageReader::new(Cursor::new(image_bytes))
|
||||||
|
.with_guessed_format()
|
||||||
|
.unwrap()
|
||||||
|
.decode();
|
||||||
|
image.map(|image| {
|
||||||
|
let stride = if transparency {
|
||||||
|
image.to_rgba8().sample_layout().height_stride
|
||||||
|
} else {
|
||||||
|
image.to_rgb8().sample_layout().height_stride
|
||||||
|
};
|
||||||
|
Pixbuf::from_bytes(
|
||||||
|
&glib::Bytes::from(image.as_bytes()),
|
||||||
|
Colorspace::Rgb,
|
||||||
|
transparency,
|
||||||
|
8,
|
||||||
|
image.width() as i32,
|
||||||
|
image.height() as i32,
|
||||||
|
stride as i32,
|
||||||
|
)
|
||||||
|
.scale_simple(width, height, InterpType::Nearest)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn perftrace<F, A>(trace_name: &str, f: F) -> A
|
pub fn perftrace<F, A>(trace_name: &str, f: F) -> A
|
||||||
where
|
where
|
||||||
F: FnOnce() -> A,
|
F: FnOnce() -> A,
|
||||||
|
|
|
@ -4,8 +4,7 @@ use async_std::task::spawn;
|
||||||
use gio::ActionEntry;
|
use gio::ActionEntry;
|
||||||
use otg_core::{Config, ConfigOption, Core, CoreNotification, LibraryPath, Observable};
|
use otg_core::{Config, ConfigOption, Core, CoreNotification, LibraryPath, Observable};
|
||||||
use otg_gtk::{
|
use otg_gtk::{
|
||||||
AppWindow,
|
AppWindow, CoreApi, ResourceManager
|
||||||
CoreApi,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -123,8 +122,9 @@ fn main() {
|
||||||
|
|
||||||
app.connect_activate({
|
app.connect_activate({
|
||||||
move |app| {
|
move |app| {
|
||||||
|
let resources = ResourceManager::default();
|
||||||
let core_api = CoreApi { core: core.clone() };
|
let core_api = CoreApi { core: core.clone() };
|
||||||
let app_window = AppWindow::new(app, core_api);
|
let app_window = AppWindow::new(app, core_api, resources);
|
||||||
|
|
||||||
setup_app_configuration_action(app, app_window.clone());
|
setup_app_configuration_action(app, app_window.clone());
|
||||||
|
|
||||||
|
|
|
@ -22,16 +22,21 @@ You should have received a copy of the GNU General Public License along with On
|
||||||
// I'll get all of the information about the game from the core, and then render everything in the
|
// I'll get all of the information about the game from the core, and then render everything in the
|
||||||
// UI. So this will be a heavy lift on the UI side.
|
// UI. So this will be a heavy lift on the UI side.
|
||||||
|
|
||||||
use crate::{components::{Goban, PlayerCard, ReviewTree}, CoreApi};
|
use std::{cell::RefCell, rc::Rc};
|
||||||
use glib::Object;
|
|
||||||
use gtk::{prelude::*, subclass::prelude::*};
|
|
||||||
use otg_core::Color;
|
|
||||||
use sgf::GameRecord;
|
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
components::{Goban, PlayerCard, ReviewTree},
|
||||||
|
ResourceManager,
|
||||||
|
};
|
||||||
|
use glib::Propagation;
|
||||||
|
use gtk::{gdk::Key, prelude::*, EventControllerKey};
|
||||||
|
use otg_core::{Color, GameReviewViewModel};
|
||||||
|
|
||||||
|
/*
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct GameReviewPrivate {}
|
pub struct GameReviewPrivate {
|
||||||
|
model: Rc<RefCell<Option<GameReviewViewModel>>>,
|
||||||
|
}
|
||||||
|
|
||||||
#[glib::object_subclass]
|
#[glib::object_subclass]
|
||||||
impl ObjectSubclass for GameReviewPrivate {
|
impl ObjectSubclass for GameReviewPrivate {
|
||||||
|
@ -49,16 +54,79 @@ glib::wrapper! {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GameReview {
|
impl GameReview {
|
||||||
pub fn new(_api: CoreApi, record: GameRecord) -> Self {
|
pub fn new(_api: CoreApi, record: GameRecord, resources: ResourceManager) -> Self {
|
||||||
let s: Self = Object::builder().build();
|
let s: Self = Object::builder().build();
|
||||||
|
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct GameReview {
|
||||||
|
widget: gtk::Box,
|
||||||
|
goban: Rc<RefCell<Option<Goban>>>,
|
||||||
|
review_tree: Rc<RefCell<Option<ReviewTree>>>,
|
||||||
|
|
||||||
|
resources: ResourceManager,
|
||||||
|
view: Rc<RefCell<GameReviewViewModel>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GameReview {
|
||||||
|
pub fn new(view: GameReviewViewModel, resources: ResourceManager) -> Self {
|
||||||
|
let widget = gtk::Box::builder().build();
|
||||||
|
|
||||||
|
let view = Rc::new(RefCell::new(view));
|
||||||
|
|
||||||
|
let s = Self {
|
||||||
|
widget,
|
||||||
|
goban: Default::default(),
|
||||||
|
review_tree: Default::default(),
|
||||||
|
resources,
|
||||||
|
view,
|
||||||
|
};
|
||||||
|
|
||||||
|
let keypress_controller = EventControllerKey::new();
|
||||||
|
keypress_controller.connect_key_pressed({
|
||||||
|
let s = s.clone();
|
||||||
|
move |_, key, _, _| {
|
||||||
|
let mut view = s.view.borrow_mut();
|
||||||
|
match key {
|
||||||
|
Key::Down => view.next_move(),
|
||||||
|
Key::Up => view.previous_move(),
|
||||||
|
Key::Left => view.previous_variant(),
|
||||||
|
Key::Right => view.next_variant(),
|
||||||
|
_ => {
|
||||||
|
return Propagation::Proceed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match *s.goban.borrow_mut() {
|
||||||
|
Some(ref mut goban) => goban.set_board_state(view.game_view()),
|
||||||
|
None => {}
|
||||||
|
};
|
||||||
|
match *s.review_tree.borrow() {
|
||||||
|
Some(ref tree) => tree.queue_draw(),
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
Propagation::Stop
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
s.widget.add_controller(keypress_controller);
|
||||||
|
|
||||||
|
s.render();
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&self) {
|
||||||
// It's actually really bad to be just throwing away errors. Panics make everyone unhappy.
|
// It's actually really bad to be just throwing away errors. Panics make everyone unhappy.
|
||||||
// This is not a fatal error, so I'll replace this `unwrap` call with something that
|
// This is not a fatal error, so I'll replace this `unwrap` call with something that
|
||||||
// renders the board and notifies the user of a problem that cannot be resolved.
|
// renders the board and notifies the user of a problem that cannot be resolved.
|
||||||
let board_repr = otg_core::Goban::default()
|
let board_repr = self.view.borrow().game_view();
|
||||||
.apply_moves(record.mainline())
|
let board = Goban::new(board_repr, self.resources.clone());
|
||||||
.unwrap();
|
|
||||||
let board = Goban::new(board_repr);
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
s.attach(&board, 0, 0, 2, 2);
|
s.attach(&board, 0, 0, 2, 2);
|
||||||
|
@ -75,7 +143,7 @@ impl GameReview {
|
||||||
// The review tree needs to know the record for being able to render all of the nodes. Once
|
// The review tree needs to know the record for being able to render all of the nodes. Once
|
||||||
// keyboard input is being handled, the tree will have to be updated on each keystroke in
|
// keyboard input is being handled, the tree will have to be updated on each keystroke in
|
||||||
// order to show the user where they are within the game record.
|
// order to show the user where they are within the game record.
|
||||||
let review_tree = ReviewTree::new(record.clone());
|
let review_tree = ReviewTree::new(self.view.borrow().clone());
|
||||||
|
|
||||||
// I think most keyboard focus is going to end up being handled here in GameReview, as
|
// I think most keyboard focus is going to end up being handled here in GameReview, as
|
||||||
// keystrokes need to affect both the goban and the review tree simultanesouly. Possibly
|
// keystrokes need to affect both the goban and the review tree simultanesouly. Possibly
|
||||||
|
@ -87,14 +155,24 @@ impl GameReview {
|
||||||
.spacing(4)
|
.spacing(4)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
player_information_section.append(&PlayerCard::new(Color::Black, &record.black_player));
|
player_information_section
|
||||||
player_information_section.append(&PlayerCard::new(Color::White, &record.white_player));
|
.append(&PlayerCard::new(Color::Black, &self.view.borrow().black_player()));
|
||||||
|
player_information_section
|
||||||
|
.append(&PlayerCard::new(Color::White, &self.view.borrow().white_player()));
|
||||||
|
|
||||||
s.append(&board);
|
self.widget.append(&board);
|
||||||
sidebar.append(&player_information_section);
|
sidebar.append(&player_information_section);
|
||||||
sidebar.append(&review_tree);
|
sidebar.append(&review_tree.widget());
|
||||||
s.append(&sidebar);
|
self.widget.append(&sidebar);
|
||||||
|
|
||||||
s
|
*self.goban.borrow_mut() = Some(board);
|
||||||
|
*self.review_tree.borrow_mut() = Some(review_tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn redraw(&self) {
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn widget(&self) -> gtk::Widget {
|
||||||
|
self.widget.clone().upcast::<gtk::Widget>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
[build]
|
||||||
|
target = "thumbv6m-none-eabi"
|
||||||
|
|
||||||
|
[target.thumbv6m-none-eabi]
|
||||||
|
rustflags = [
|
||||||
|
"-C", "link-arg=--nmagic",
|
||||||
|
"-C", "link-arg=-Tlink.x",
|
||||||
|
"-C", "llvm-args=--inline-threshold=5",
|
||||||
|
"-C", "no-vectorize-loops",
|
||||||
|
]
|
||||||
|
|
||||||
|
runner = "elf2uf2-rs -d"
|
|
@ -0,0 +1,11 @@
|
||||||
|
[package]
|
||||||
|
name = "halloween-leds"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cortex-m-rt = "0.7.3"
|
||||||
|
embedded-hal = "1.0.0"
|
||||||
|
embedded-io = "0.6.1"
|
||||||
|
panic-halt = "1.0.0"
|
||||||
|
rp-pico = "0.9.0"
|
|
@ -0,0 +1,125 @@
|
||||||
|
#![no_main]
|
||||||
|
#![no_std]
|
||||||
|
|
||||||
|
/// This application demonstrates using a Raspberry Pi Pico to control an individual SK9822 module.
|
||||||
|
/// Keep in mind that the Pico, though it accepts 5V for power, it runs on 3.3V logic. The GPIO
|
||||||
|
/// pins will emit only 3.3 volts, and the SK9822 needs 5V logic. So, make sure that the GPIO pins
|
||||||
|
/// run through a transistor or a logic level lhifter to go from 3.3V logic to 5V logic.
|
||||||
|
use embedded_hal::{delay::DelayNs, spi::SpiBus};
|
||||||
|
use panic_halt as _;
|
||||||
|
use rp_pico::{
|
||||||
|
entry,
|
||||||
|
hal::{
|
||||||
|
clocks::init_clocks_and_plls,
|
||||||
|
fugit::RateExtU32,
|
||||||
|
gpio::{
|
||||||
|
bank0::{Gpio10, Gpio11},
|
||||||
|
FunctionSpi, Pin, PullDown,
|
||||||
|
},
|
||||||
|
spi::Spi,
|
||||||
|
Clock, Sio, Timer, Watchdog,
|
||||||
|
},
|
||||||
|
pac, Pins,
|
||||||
|
};
|
||||||
|
|
||||||
|
const FPS: u32 = 60;
|
||||||
|
const MS_PER_FRAME: u32 = 1000 / FPS;
|
||||||
|
const XOSC_CRYSTAL_FREQ: u32 = 12_000_000; // MHz, https://forums.raspberrypi.com/viewtopic.php?t=356764
|
||||||
|
|
||||||
|
#[entry]
|
||||||
|
unsafe fn main() -> ! {
|
||||||
|
// rp_pico::pac::Peripherals is a reference to physical hardware defined on the Pico.
|
||||||
|
let mut peripherals = pac::Peripherals::take().unwrap();
|
||||||
|
|
||||||
|
// SIO inidcates "Single Cycle IO". I don't know what this means, but it could mean that this
|
||||||
|
// is a class of IO operations that can be run in a single clock cycle, such as switching a
|
||||||
|
// GPIO pin on or off.
|
||||||
|
let sio = Sio::new(peripherals.SIO);
|
||||||
|
|
||||||
|
// Many of the following systems require a watchdog. I do not know what this does, either, but
|
||||||
|
// it may be some failsafe software that will reset operations if the watchdog detects a lack
|
||||||
|
// of activity.
|
||||||
|
let mut watchdog = Watchdog::new(peripherals.WATCHDOG);
|
||||||
|
|
||||||
|
// Here we grab the GPIO pins in bank 0.
|
||||||
|
let pins = Pins::new(
|
||||||
|
peripherals.IO_BANK0,
|
||||||
|
peripherals.PADS_BANK0,
|
||||||
|
sio.gpio_bank0,
|
||||||
|
&mut peripherals.RESETS,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize an abstraction of the clock system with a batch of standard hardware clocks.
|
||||||
|
let clocks = init_clocks_and_plls(
|
||||||
|
XOSC_CRYSTAL_FREQ,
|
||||||
|
peripherals.XOSC,
|
||||||
|
peripherals.CLOCKS,
|
||||||
|
peripherals.PLL_SYS,
|
||||||
|
peripherals.PLL_USB,
|
||||||
|
&mut peripherals.RESETS,
|
||||||
|
&mut watchdog,
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// An abstraction for a timer which we can use to delay the code.
|
||||||
|
let mut timer = Timer::new(peripherals.TIMER, &mut peripherals.RESETS, &clocks);
|
||||||
|
|
||||||
|
// Grab the clock and data pins for SPI1. For Clock pins and for Data pins, there are only two
|
||||||
|
// pins each on the Pico which can function for SPI1.
|
||||||
|
let spi_clk: Pin<Gpio10, FunctionSpi, PullDown> = pins.gpio10.into_function();
|
||||||
|
let spi_sdo: Pin<Gpio11, FunctionSpi, PullDown> = pins.gpio11.into_function();
|
||||||
|
|
||||||
|
// Now, create the SPI function abstraction for SPI1 with spi_clk and spi_sdo.
|
||||||
|
let mut spi = Spi::<_, _, _, 8>::new(peripherals.SPI1, (spi_sdo, spi_clk)).init(
|
||||||
|
&mut peripherals.RESETS,
|
||||||
|
// The SPI system uses the peripheral clock
|
||||||
|
clocks.peripheral_clock.freq(),
|
||||||
|
// Transmit data at a rate of 1Mbit.
|
||||||
|
1_u32.MHz(),
|
||||||
|
// Run with SPI Mode 1. This means that the clock line should start high and that data will
|
||||||
|
// be sampled starting at the first falling edge.
|
||||||
|
embedded_hal::spi::MODE_1,
|
||||||
|
);
|
||||||
|
|
||||||
|
// byte count: 4 for the start frame
|
||||||
|
// 1 * 4 for three lights with 4 bytes per light
|
||||||
|
// 4 for the end frame
|
||||||
|
// = 20 bytes
|
||||||
|
let mut lights: [u8; 12] = [0; 12];
|
||||||
|
// We just skip the first four bytes, because the start frame is four bytes of 0.
|
||||||
|
|
||||||
|
// Set the first byte of the one and only lamp. The first byte follows the pattern of three 1
|
||||||
|
// bits followed by five additional bits that indicate an overall brightness level of the
|
||||||
|
// pixel. The datasheet for the SK9822 doesn't specify the exact effect, but it does mean that
|
||||||
|
// the higher this number is, the brighter 255 means for an given LED in the array. 1 is the
|
||||||
|
// lowest brightness that emits light, and 31 is the highest supported brightness.
|
||||||
|
lights[4] = 0xe0 + 1;
|
||||||
|
// Set the Blue light of the dotstar to 255, assuming the dotstar frame format is RBG. Note
|
||||||
|
// that the standard SK9822 datasheed indicates that the format is BGR. Your mileage may vary.
|
||||||
|
lights[6] = 255;
|
||||||
|
|
||||||
|
// The end frame is four bytes of 255.
|
||||||
|
lights[8] = 0xff;
|
||||||
|
lights[9] = 0xff;
|
||||||
|
lights[10] = 0xff;
|
||||||
|
lights[11] = 0xff;
|
||||||
|
|
||||||
|
|
||||||
|
// The rest of this is just a stock pulsating animation which is slightly brightening and
|
||||||
|
// dimming the *blue* LED (on my set of dotstars).
|
||||||
|
let mut brightness = 1;
|
||||||
|
let mut step = 1;
|
||||||
|
loop {
|
||||||
|
if brightness == 64 && step == 1 {
|
||||||
|
step = -1;
|
||||||
|
} else if brightness == 1 && step == -1 {
|
||||||
|
step = 1;
|
||||||
|
};
|
||||||
|
lights[5] = brightness as u8;
|
||||||
|
brightness = brightness + step;
|
||||||
|
|
||||||
|
let _ = spi.write(lights.as_slice());
|
||||||
|
timer.delay_ms(MS_PER_FRAME);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,3 @@
|
||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "1.77.0"
|
channel = "1.81.0"
|
||||||
targets = [ "wasm32-unknown-unknown" ]
|
targets = [ "wasm32-unknown-unknown", "thumbv6m-none-eabi" ]
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
- text: The distinguishing thing about magic is that it includes some kind of personal element. The person who is performing the magic is relevant to the magic. -- Ted Chang, Marie Brennan
|
||||||
|
position: top
|
||||||
|
transition:
|
||||||
|
secs: 1
|
||||||
|
nanos: 0
|
||||||
|
|
||||||
|
- text: Any sufficiently advanced technology is indistinguishable from magic. -- Arthur C. Clark.
|
||||||
|
position: middle
|
||||||
|
transition:
|
||||||
|
secs: 1
|
||||||
|
nanos: 0
|
||||||
|
|
||||||
|
- text: Science is our Magic.
|
||||||
|
position: bottom
|
||||||
|
transition:
|
||||||
|
secs: 1
|
||||||
|
nanos: 0
|
||||||
|
|
|
@ -9,6 +9,7 @@ edition = "2021"
|
||||||
chrono = { version = "0.4", features = [ "serde" ] }
|
chrono = { version = "0.4", features = [ "serde" ] }
|
||||||
nom = { version = "7" }
|
nom = { version = "7" }
|
||||||
serde = { version = "1", features = [ "derive" ] }
|
serde = { version = "1", features = [ "derive" ] }
|
||||||
|
nary_tree = { version = "0.4" }
|
||||||
thiserror = { version = "1"}
|
thiserror = { version = "1"}
|
||||||
typeshare = { version = "1" }
|
typeshare = { version = "1" }
|
||||||
uuid = { version = "0.8", features = ["v4", "serde"] }
|
uuid = { version = "0.8", features = ["v4", "serde"] }
|
||||||
|
|
460
sgf/src/game.rs
|
@ -2,8 +2,15 @@ use crate::{
|
||||||
parser::{self, Annotation, Evaluation, Move, SetupInstr, Size, UnknownProperty},
|
parser::{self, Annotation, Evaluation, Move, SetupInstr, Size, UnknownProperty},
|
||||||
Color, Date, GameResult, GameType,
|
Color, Date, GameResult, GameType,
|
||||||
};
|
};
|
||||||
|
use nary_tree::{NodeId, NodeMut, NodeRef, Tree};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{collections::HashSet, time::Duration};
|
use std::{
|
||||||
|
collections::{HashMap, HashSet, VecDeque},
|
||||||
|
fmt,
|
||||||
|
fmt::Debug,
|
||||||
|
ops::{Deref, DerefMut},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
@ -32,7 +39,7 @@ pub enum SetupNodeError {
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum GameNodeError {
|
pub enum GameNodeError {
|
||||||
UnsupportedGameNode(MoveNodeError, SetupNodeError),
|
UnsupportedGameNode(MoveNodeError, SetupNodeError, parser::Node),
|
||||||
ConflictingProperty,
|
ConflictingProperty,
|
||||||
ConflictingPosition,
|
ConflictingPosition,
|
||||||
}
|
}
|
||||||
|
@ -52,7 +59,7 @@ pub struct Player {
|
||||||
/// syntax issues, the result of the GameRecord is to have a fully-understood game. However, this
|
/// syntax issues, the result of the GameRecord is to have a fully-understood game. However, this
|
||||||
/// doesn't (yet?) go quite to the level of apply the game type (i.e., this is Go, Chess, Yinsh, or
|
/// doesn't (yet?) go quite to the level of apply the game type (i.e., this is Go, Chess, Yinsh, or
|
||||||
/// whatever).
|
/// whatever).
|
||||||
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct GameRecord {
|
pub struct GameRecord {
|
||||||
pub game_type: GameType,
|
pub game_type: GameType,
|
||||||
|
|
||||||
|
@ -78,7 +85,7 @@ pub struct GameRecord {
|
||||||
pub overtime: Option<String>,
|
pub overtime: Option<String>,
|
||||||
pub transcriber: Option<String>,
|
pub transcriber: Option<String>,
|
||||||
|
|
||||||
pub children: Vec<GameNode>,
|
pub trees: Vec<GameTree>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GameRecord {
|
impl GameRecord {
|
||||||
|
@ -111,55 +118,40 @@ impl GameRecord {
|
||||||
overtime: None,
|
overtime: None,
|
||||||
transcriber: None,
|
transcriber: None,
|
||||||
|
|
||||||
children: vec![],
|
trees: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn nodes(&self) -> Vec<&GameNode> {
|
||||||
|
self.iter().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = &'_ GameNode> {
|
||||||
|
self.trees
|
||||||
|
.iter()
|
||||||
|
.flat_map(|tree| tree.root().unwrap().traverse_pre_order())
|
||||||
|
.map(|nr| nr.data())
|
||||||
|
}
|
||||||
|
|
||||||
/// Generate a list of moves which constitute the main line of the game. This is the game as it
|
/// Generate a list of moves which constitute the main line of the game. This is the game as it
|
||||||
/// was actually played out, and by convention consists of the first node in each list of
|
/// was actually played out, and by convention consists of the first node in each list of
|
||||||
/// children.
|
/// children.
|
||||||
pub fn mainline(&self) -> Vec<&GameNode> {
|
pub fn mainline(&self) -> Option<impl Iterator<Item = NodeRef<'_, GameNode>>> {
|
||||||
let mut moves: Vec<&GameNode> = vec![];
|
if !self.trees.is_empty() {
|
||||||
|
Some(MainlineIter {
|
||||||
let mut next = self.children.get(0);
|
next: self.trees[0].root(),
|
||||||
while let Some(node) = next {
|
tree: &self.trees[0],
|
||||||
// Given that I know that I have a node, and I know that I'm going to push a reference
|
})
|
||||||
// to it onto my final list, I want to get the first of its children. And I want to
|
} else {
|
||||||
// keep doing that until there are no more first children.
|
None
|
||||||
//
|
|
||||||
// Just going to push references onto the list. No need to copy the nodes for this.
|
|
||||||
//
|
|
||||||
// Pushing a reference onto the list implicitely clones the reference, but not the data
|
|
||||||
// it is pointing to. This means that each time through the loop, `next` points to
|
|
||||||
// something else. This isn't being described very well, though, so it's worth
|
|
||||||
// reviewing in the future.
|
|
||||||
moves.push(node);
|
|
||||||
|
|
||||||
next = match node {
|
|
||||||
GameNode::MoveNode(node) => node.children.get(0),
|
|
||||||
GameNode::SetupNode(node) => node.children.get(0),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
moves
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Node for GameRecord {
|
impl TryFrom<parser::Tree> for GameRecord {
|
||||||
fn children<'a>(&'a self) -> Vec<&'a GameNode> {
|
|
||||||
self.children.iter().collect::<Vec<&'a GameNode>>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_child(&mut self, node: GameNode) -> &mut GameNode {
|
|
||||||
self.children.push(node);
|
|
||||||
self.children.last_mut().unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<&parser::Tree> for GameRecord {
|
|
||||||
type Error = GameError;
|
type Error = GameError;
|
||||||
|
|
||||||
fn try_from(tree: &parser::Tree) -> Result<Self, Self::Error> {
|
fn try_from(tree: parser::Tree) -> Result<Self, Self::Error> {
|
||||||
let mut ty = None;
|
let mut ty = None;
|
||||||
let mut size = None;
|
let mut size = None;
|
||||||
let mut black_player = Player {
|
let mut black_player = Player {
|
||||||
|
@ -234,6 +226,7 @@ impl TryFrom<&parser::Tree> for GameRecord {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
s.children = tree
|
s.children = tree
|
||||||
.root
|
.root
|
||||||
.next
|
.next
|
||||||
|
@ -241,33 +234,205 @@ impl TryFrom<&parser::Tree> for GameRecord {
|
||||||
.map(GameNode::try_from)
|
.map(GameNode::try_from)
|
||||||
.collect::<Result<Vec<GameNode>, GameNodeError>>()
|
.collect::<Result<Vec<GameNode>, GameNodeError>>()
|
||||||
.map_err(GameError::InvalidGameNode)?;
|
.map_err(GameError::InvalidGameNode)?;
|
||||||
|
*/
|
||||||
|
|
||||||
|
s.trees = tree
|
||||||
|
.root
|
||||||
|
.next
|
||||||
|
.into_iter()
|
||||||
|
.map(recursive_tree_to_slab_tree)
|
||||||
|
.collect::<Result<Vec<GameTree>, GameError>>()?;
|
||||||
|
|
||||||
Ok(s)
|
Ok(s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn recursive_tree_to_slab_tree(node: parser::Node) -> Result<GameTree, GameError> {
|
||||||
|
let mut slab = Tree::new();
|
||||||
|
let mut nodes: VecDeque<(NodeId, parser::Node)> = VecDeque::new();
|
||||||
|
|
||||||
|
let root_id =
|
||||||
|
slab.set_root(GameNode::try_from(node.clone()).map_err(GameError::InvalidGameNode)?);
|
||||||
|
nodes.push_back((root_id, node));
|
||||||
|
|
||||||
|
// I need to keep track of the current parent, and I need to keep on digging deeper into the
|
||||||
|
// tree. Given that I have the root, I can then easily find out all of the children.
|
||||||
|
//
|
||||||
|
// So, maybe I take the list of children. Assign each one of them to a place in the slab tree.
|
||||||
|
// Then push the child *and* its ID into a dequeue. So long as the dequeue is not empty, I want
|
||||||
|
// to pop a node and its ID from the dequeue. The retrieve the NodeMut for it and work on the
|
||||||
|
// node's children.
|
||||||
|
while let Some((node_id, node)) = nodes.pop_front() {
|
||||||
|
let mut game_node: NodeMut<GameNode> = slab
|
||||||
|
.get_mut(node_id)
|
||||||
|
.expect("invalid node_id when retrieving nodes from the game");
|
||||||
|
// I have a node that is in the tree. Now run across all of its children, adding each one
|
||||||
|
// to the tree and pushing them into the deque along with their IDs.
|
||||||
|
for child in node.next {
|
||||||
|
let slab_child = game_node
|
||||||
|
.append(GameNode::try_from(child.clone()).map_err(GameError::InvalidGameNode)?);
|
||||||
|
nodes.push_back((slab_child.node_id(), child));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(GameTree(slab))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct TreeIter<'a> {
|
||||||
|
queue: VecDeque<NodeRef<'a, &'a GameNode>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
impl<'a> Default for TreeIter<'a> {
|
||||||
|
fn default() -> Self {
|
||||||
|
TreeIter {
|
||||||
|
queue: VecDeque::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
impl<'a> Iterator for TreeIter<'a> {
|
||||||
|
type Item = &'a GameNode;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
let retval = self.queue.pop_front();
|
||||||
|
if let Some(ref retval) = retval {
|
||||||
|
retval
|
||||||
|
.children()
|
||||||
|
.for_each(|node| self.queue.push_back(node));
|
||||||
|
}
|
||||||
|
retval.map(|rv| *rv.data())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GameTree(Tree<GameNode>);
|
||||||
|
|
||||||
|
impl Default for GameTree {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(Tree::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for GameTree {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
match self.0.root() {
|
||||||
|
None => Self(Tree::new()),
|
||||||
|
Some(source_root_node) => {
|
||||||
|
let mut dest = Tree::new();
|
||||||
|
let dest_root_id = dest.set_root(source_root_node.data().clone());
|
||||||
|
|
||||||
|
// In order to add a node to the new tree, I need to know the ID of the parent in
|
||||||
|
// the source tree and the ID of the parent in the destination tree. So I want a
|
||||||
|
// lookup table that maps source IDs to destination IDs. But is that sufficient?
|
||||||
|
// Perhaps I can just keep a mapping from a source noderef to a destination ID.
|
||||||
|
// I don't think I can keep more than one mutable destination node.
|
||||||
|
|
||||||
|
let mut mapping: HashMap<NodeId, NodeId> = HashMap::new();
|
||||||
|
mapping.insert(source_root_node.node_id(), dest_root_id);
|
||||||
|
|
||||||
|
for source_node in source_root_node.traverse_level_order() {
|
||||||
|
match source_node.parent() {
|
||||||
|
None => {}
|
||||||
|
Some(parent) => {
|
||||||
|
let source_node_parent_id = parent.node_id();
|
||||||
|
let target_node_parent_id = mapping.get(&source_node_parent_id).expect("node should have been added to the source to dest mapping when being cloned");
|
||||||
|
|
||||||
|
let mut parent = dest.get_mut(*target_node_parent_id).expect(
|
||||||
|
"destination parent node to exist before reaching potential children",
|
||||||
|
);
|
||||||
|
let dest_id = parent.append(source_node.data().clone()).node_id();
|
||||||
|
mapping.insert(source_node.node_id(), dest_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Self(dest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for GameTree {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
|
||||||
|
self.write_formatted(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for GameTree {
|
||||||
|
type Target = Tree<GameNode>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DerefMut for GameTree {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for GameTree {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
// Get pre-order iterators over both trees, zip them, and ensure that the data contents are
|
||||||
|
// the same between them
|
||||||
|
let left_root = self.root();
|
||||||
|
let right_root = other.root();
|
||||||
|
|
||||||
|
match (left_root, right_root) {
|
||||||
|
(Some(left_root), Some(right_root)) => {
|
||||||
|
for (left_node, right_node) in std::iter::zip(
|
||||||
|
left_root.traverse_pre_order(),
|
||||||
|
right_root.traverse_pre_order(),
|
||||||
|
) {
|
||||||
|
if left_node.data() != right_node.data() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(None, None) => return true,
|
||||||
|
_ => return false,
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MainlineIter<'a> {
|
||||||
|
next: Option<NodeRef<'a, GameNode>>,
|
||||||
|
tree: &'a Tree<GameNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for MainlineIter<'a> {
|
||||||
|
type Item = NodeRef<'a, GameNode>;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
if let Some(next) = self.next.take() {
|
||||||
|
let ret = self.tree.get(next.node_id())?;
|
||||||
|
self.next = next
|
||||||
|
.first_child()
|
||||||
|
.and_then(|child| self.tree.get(child.node_id()));
|
||||||
|
Some(ret)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
||||||
pub enum GameNode {
|
pub enum GameNode {
|
||||||
MoveNode(MoveNode),
|
MoveNode(MoveNode),
|
||||||
SetupNode(SetupNode),
|
SetupNode(SetupNode),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Node {
|
impl fmt::Display for GameNode {
|
||||||
/// Provide a pre-order traversal of all of the nodes in the game tree.
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||||
fn nodes<'a>(&'a self) -> Vec<&'a GameNode> {
|
match self {
|
||||||
self.children()
|
GameNode::MoveNode(_) => write!(f, "MoveNode"),
|
||||||
.iter()
|
GameNode::SetupNode(_) => write!(f, "SetupNode"),
|
||||||
.flat_map(|node| {
|
}
|
||||||
let mut children = node.nodes();
|
|
||||||
let mut v = vec![*node];
|
|
||||||
v.append(&mut children);
|
|
||||||
v
|
|
||||||
})
|
|
||||||
.collect::<Vec<&'a GameNode>>()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn children(&self) -> Vec<&GameNode>;
|
|
||||||
fn add_child(&mut self, node: GameNode) -> &mut GameNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GameNode {
|
impl GameNode {
|
||||||
|
@ -279,70 +444,20 @@ impl GameNode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Node for GameNode {
|
impl TryFrom<parser::Node> for GameNode {
|
||||||
fn children(&self) -> Vec<&GameNode> {
|
|
||||||
match self {
|
|
||||||
GameNode::MoveNode(node) => node.children(),
|
|
||||||
GameNode::SetupNode(node) => node.children(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn nodes(&self) -> Vec<&GameNode> {
|
|
||||||
match self {
|
|
||||||
GameNode::MoveNode(node) => node.nodes(),
|
|
||||||
GameNode::SetupNode(node) => node.nodes(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_child(&mut self, new_node: GameNode) -> &mut GameNode {
|
|
||||||
match self {
|
|
||||||
GameNode::MoveNode(node) => node.add_child(new_node),
|
|
||||||
GameNode::SetupNode(node) => node.add_child(new_node),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<&parser::Node> for GameNode {
|
|
||||||
type Error = GameNodeError;
|
type Error = GameNodeError;
|
||||||
|
|
||||||
fn try_from(n: &parser::Node) -> Result<Self, Self::Error> {
|
fn try_from(n: parser::Node) -> Result<Self, Self::Error> {
|
||||||
// I originally wrote this recursively. However, on an ordinary game of a couple hundred
|
let move_node = MoveNode::try_from(n.clone());
|
||||||
// moves, that meant that I was recursing 500 functions, and that exceeded the stack limit.
|
let setup_node = SetupNode::try_from(n.clone());
|
||||||
// So, instead, I need to unroll everything to non-recursive form.
|
|
||||||
//
|
|
||||||
// So, I can treat each branch of the tree as a single line. Iterate over that line. I can
|
|
||||||
// only use the MoveNode::try_from and SetupNode::try_from if those functions don't
|
|
||||||
// recurse. Instead, I'm going to process just that node, then return to here and process
|
|
||||||
// the children.
|
|
||||||
let move_node = MoveNode::try_from(n);
|
|
||||||
let setup_node = SetupNode::try_from(n);
|
|
||||||
|
|
||||||
// I'm much too tired when writing this. I'm still recursing, but I did cut the number of
|
match (move_node, setup_node) {
|
||||||
// recursions in half. This helps, but it still doesn't guarantee that I'm going to be able
|
(Ok(node), _) => Ok(Self::MoveNode(node)),
|
||||||
// to parse all possible games. So, still, treat each branch of the game as a single line.
|
(Err(_), Ok(node)) => Ok(Self::SetupNode(node)),
|
||||||
// Iterate over that line, don't recurse. Create bookmarks at each branch point, and then
|
|
||||||
// come back to each one.
|
|
||||||
let children = n
|
|
||||||
.next
|
|
||||||
.iter()
|
|
||||||
.map(GameNode::try_from)
|
|
||||||
.collect::<Result<Vec<Self>, Self::Error>>()?;
|
|
||||||
|
|
||||||
let node = match (move_node, setup_node) {
|
|
||||||
(Ok(mut node), _) => {
|
|
||||||
node.children = children;
|
|
||||||
Ok(Self::MoveNode(node))
|
|
||||||
}
|
|
||||||
(Err(_), Ok(mut node)) => {
|
|
||||||
node.children = children;
|
|
||||||
Ok(Self::SetupNode(node))
|
|
||||||
}
|
|
||||||
(Err(move_err), Err(setup_err)) => {
|
(Err(move_err), Err(setup_err)) => {
|
||||||
Err(Self::Error::UnsupportedGameNode(move_err, setup_err))
|
Err(Self::Error::UnsupportedGameNode(move_err, setup_err, n))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}?;
|
|
||||||
|
|
||||||
Ok(node)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -351,7 +466,6 @@ pub struct MoveNode {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub color: Color,
|
pub color: Color,
|
||||||
pub mv: Move,
|
pub mv: Move,
|
||||||
pub children: Vec<GameNode>,
|
|
||||||
|
|
||||||
pub time_left: Option<Duration>,
|
pub time_left: Option<Duration>,
|
||||||
pub moves_left: Option<usize>,
|
pub moves_left: Option<usize>,
|
||||||
|
@ -369,7 +483,6 @@ impl MoveNode {
|
||||||
id: Uuid::new_v4(),
|
id: Uuid::new_v4(),
|
||||||
color,
|
color,
|
||||||
mv,
|
mv,
|
||||||
children: Vec::new(),
|
|
||||||
|
|
||||||
time_left: None,
|
time_left: None,
|
||||||
moves_left: None,
|
moves_left: None,
|
||||||
|
@ -383,21 +496,10 @@ impl MoveNode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Node for MoveNode {
|
impl TryFrom<parser::Node> for MoveNode {
|
||||||
fn children<'a>(&'a self) -> Vec<&'a GameNode> {
|
|
||||||
self.children.iter().collect::<Vec<&'a GameNode>>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_child(&mut self, node: GameNode) -> &mut GameNode {
|
|
||||||
self.children.push(node);
|
|
||||||
self.children.last_mut().unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<&parser::Node> for MoveNode {
|
|
||||||
type Error = MoveNodeError;
|
type Error = MoveNodeError;
|
||||||
|
|
||||||
fn try_from(n: &parser::Node) -> Result<Self, Self::Error> {
|
fn try_from(n: parser::Node) -> Result<Self, Self::Error> {
|
||||||
let s = match n.mv() {
|
let s = match n.mv() {
|
||||||
Some((color, mv)) => {
|
Some((color, mv)) => {
|
||||||
let mut s = Self::new(color, mv);
|
let mut s = Self::new(color, mv);
|
||||||
|
@ -460,7 +562,6 @@ pub struct SetupNode {
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
|
|
||||||
pub positions: Vec<parser::SetupInstr>,
|
pub positions: Vec<parser::SetupInstr>,
|
||||||
pub children: Vec<GameNode>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SetupNode {
|
impl SetupNode {
|
||||||
|
@ -480,26 +581,14 @@ impl SetupNode {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
id: Uuid::new_v4(),
|
id: Uuid::new_v4(),
|
||||||
positions,
|
positions,
|
||||||
children: Vec::new(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Node for SetupNode {
|
impl TryFrom<parser::Node> for SetupNode {
|
||||||
fn children<'a>(&'a self) -> Vec<&'a GameNode> {
|
|
||||||
self.children.iter().collect::<Vec<&'a GameNode>>()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn add_child(&mut self, _node: GameNode) -> &mut GameNode {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<&parser::Node> for SetupNode {
|
|
||||||
type Error = SetupNodeError;
|
type Error = SetupNodeError;
|
||||||
|
|
||||||
fn try_from(n: &parser::Node) -> Result<Self, Self::Error> {
|
fn try_from(n: parser::Node) -> Result<Self, Self::Error> {
|
||||||
match n.setup() {
|
match n.setup() {
|
||||||
Some(elements) => Self::new(elements),
|
Some(elements) => Self::new(elements),
|
||||||
None => Err(Self::Error::NotASetupNode),
|
None => Err(Self::Error::NotASetupNode),
|
||||||
|
@ -507,6 +596,7 @@ impl TryFrom<&parser::Node> for SetupNode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn path_to_node(node: &GameNode, id: Uuid) -> Vec<&GameNode> {
|
pub fn path_to_node(node: &GameNode, id: Uuid) -> Vec<&GameNode> {
|
||||||
if node.id() == id {
|
if node.id() == id {
|
||||||
|
@ -523,6 +613,7 @@ pub fn path_to_node(node: &GameNode, id: Uuid) -> Vec<&GameNode> {
|
||||||
|
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
@ -543,6 +634,7 @@ mod test {
|
||||||
assert_eq!(tree.nodes().len(), 0);
|
assert_eq!(tree.nodes().len(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
#[test]
|
#[test]
|
||||||
fn it_can_add_moves_to_a_game() {
|
fn it_can_add_moves_to_a_game() {
|
||||||
let mut game = GameRecord::new(
|
let mut game = GameRecord::new(
|
||||||
|
@ -555,16 +647,21 @@ mod test {
|
||||||
Player::default(),
|
Player::default(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
let first_move = MoveNode::new(Color::Black, Move::Move("dd".to_owned()));
|
let first_move = MoveNode::new(Color::Black, Move::Move("dd".to_owned()));
|
||||||
let first_ = game.add_child(GameNode::MoveNode(first_move.clone()));
|
let first_ = game.add_child(GameNode::MoveNode(first_move.clone()));
|
||||||
let second_move = MoveNode::new(Color::White, Move::Move("qq".to_owned()));
|
let second_move = MoveNode::new(Color::White, Move::Move("qq".to_owned()));
|
||||||
first_.add_child(GameNode::MoveNode(second_move.clone()));
|
first_.add_child(GameNode::MoveNode(second_move.clone()));
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
let nodes = game.nodes();
|
let nodes = game.nodes();
|
||||||
assert_eq!(nodes.len(), 2);
|
assert_eq!(nodes.len(), 2);
|
||||||
assert_eq!(nodes[0].id(), first_move.id);
|
assert_eq!(nodes[0].id(), first_move.id);
|
||||||
assert_eq!(nodes[1].id(), second_move.id);
|
assert_eq!(nodes[1].id(), second_move.id);
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
#[ignore]
|
#[ignore]
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -588,7 +685,7 @@ mod test {
|
||||||
],
|
],
|
||||||
next: vec![],
|
next: vec![],
|
||||||
};
|
};
|
||||||
assert_matches!(GameNode::try_from(&n), Ok(GameNode::MoveNode(_)));
|
assert_matches!(GameNode::try_from(n), Ok(GameNode::MoveNode(_)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -630,10 +727,10 @@ mod move_node_tests {
|
||||||
],
|
],
|
||||||
next: vec![],
|
next: vec![],
|
||||||
};
|
};
|
||||||
assert_matches!(MoveNode::try_from(&n), Ok(node) => {
|
assert_matches!(MoveNode::try_from(n), Ok(node) => {
|
||||||
assert_eq!(node.color, Color::White);
|
assert_eq!(node.color, Color::White);
|
||||||
assert_eq!(node.mv, Move::Move("dp".to_owned()));
|
assert_eq!(node.mv, Move::Move("dp".to_owned()));
|
||||||
assert_eq!(node.children, vec![]);
|
// assert_eq!(node.children, vec![]);
|
||||||
assert_eq!(node.time_left, Some(Duration::from_secs(176)));
|
assert_eq!(node.time_left, Some(Duration::from_secs(176)));
|
||||||
assert_eq!(node.comments, Some("Comments in the game".to_owned()));
|
assert_eq!(node.comments, Some("Comments in the game".to_owned()));
|
||||||
});
|
});
|
||||||
|
@ -653,7 +750,7 @@ mod move_node_tests {
|
||||||
next: vec![],
|
next: vec![],
|
||||||
};
|
};
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
MoveNode::try_from(&n),
|
MoveNode::try_from(n),
|
||||||
Err(MoveNodeError::IncompatibleProperty(_))
|
Err(MoveNodeError::IncompatibleProperty(_))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -703,7 +800,7 @@ mod path_test {
|
||||||
let (_, games) = parse_collection::<nom::error::VerboseError<&str>>(text).unwrap();
|
let (_, games) = parse_collection::<nom::error::VerboseError<&str>>(text).unwrap();
|
||||||
let games = games
|
let games = games
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|game| GameRecord::try_from(&game).expect("game to parse"))
|
.map(|game| GameRecord::try_from(game).expect("game to parse"))
|
||||||
.collect::<Vec<GameRecord>>();
|
.collect::<Vec<GameRecord>>();
|
||||||
f(games);
|
f(games);
|
||||||
}
|
}
|
||||||
|
@ -722,7 +819,11 @@ mod path_test {
|
||||||
|games| {
|
|games| {
|
||||||
let game = &games[0];
|
let game = &games[0];
|
||||||
|
|
||||||
let moves = game.mainline();
|
let moves = game
|
||||||
|
.mainline()
|
||||||
|
.expect("there should be a mainline in this file")
|
||||||
|
.map(|nr| nr.data())
|
||||||
|
.collect::<Vec<&GameNode>>();
|
||||||
assert_matches!(moves[0], GameNode::MoveNode(node) => {
|
assert_matches!(moves[0], GameNode::MoveNode(node) => {
|
||||||
assert_eq!(node.color, Color::Black);
|
assert_eq!(node.color, Color::Black);
|
||||||
assert_eq!(node.mv, Move::Move("pp".to_owned()));
|
assert_eq!(node.mv, Move::Move("pp".to_owned()));
|
||||||
|
@ -744,7 +845,11 @@ mod path_test {
|
||||||
with_file(std::path::Path::new("test_data/branch_test.sgf"), |games| {
|
with_file(std::path::Path::new("test_data/branch_test.sgf"), |games| {
|
||||||
let game = &games[0];
|
let game = &games[0];
|
||||||
|
|
||||||
let moves = game.mainline();
|
let moves = game
|
||||||
|
.mainline()
|
||||||
|
.expect("there should be a mainline in this file")
|
||||||
|
.map(|nr| nr.data())
|
||||||
|
.collect::<Vec<&GameNode>>();
|
||||||
assert_matches!(moves[1], GameNode::MoveNode(node) => {
|
assert_matches!(moves[1], GameNode::MoveNode(node) => {
|
||||||
assert_eq!(node.color, Color::White);
|
assert_eq!(node.color, Color::White);
|
||||||
assert_eq!(node.mv, Move::Move("dd".to_owned()));
|
assert_eq!(node.mv, Move::Move("dd".to_owned()));
|
||||||
|
@ -791,7 +896,7 @@ mod file_test {
|
||||||
let (_, games) = parse_collection::<nom::error::VerboseError<&str>>(text).unwrap();
|
let (_, games) = parse_collection::<nom::error::VerboseError<&str>>(text).unwrap();
|
||||||
let games = games
|
let games = games
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|game| GameRecord::try_from(&game).expect("game to parse"))
|
.map(|game| GameRecord::try_from(game).expect("game to parse"))
|
||||||
.collect::<Vec<GameRecord>>();
|
.collect::<Vec<GameRecord>>();
|
||||||
f(games);
|
f(games);
|
||||||
}
|
}
|
||||||
|
@ -875,6 +980,7 @@ mod file_test {
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
let children = game.children();
|
let children = game.children();
|
||||||
let node = children.first().unwrap();
|
let node = children.first().unwrap();
|
||||||
assert_matches!(node, GameNode::MoveNode(node) => {
|
assert_matches!(node, GameNode::MoveNode(node) => {
|
||||||
|
@ -892,6 +998,7 @@ mod file_test {
|
||||||
assert_eq!(node.time_left, Some(Duration::from_secs(1765)));
|
assert_eq!(node.time_left, Some(Duration::from_secs(1765)));
|
||||||
assert_eq!(node.comments, None);
|
assert_eq!(node.comments, None);
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
/*
|
/*
|
||||||
let node = node.next().unwrap();
|
let node = node.next().unwrap();
|
||||||
let expected_properties = vec![
|
let expected_properties = vec![
|
||||||
|
@ -912,14 +1019,11 @@ mod file_test {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[ignore]
|
|
||||||
#[test]
|
#[test]
|
||||||
fn it_handles_shuwa_genan_file() {
|
fn it_can_load_a_file_with_multiple_roots() {
|
||||||
with_file(
|
with_file(std::path::Path::new("test_data/multi-tree.sgf"), |games| {
|
||||||
std::path::Path::new("test_data/2019.02.15_shuwa_genan_annotated.sgf"),
|
assert_eq!(games.len(), 1);
|
||||||
|trees| {
|
let game = &games[0];
|
||||||
assert_eq!(trees.len(), 1);
|
|
||||||
let game = &trees[0];
|
|
||||||
assert_eq!(game.game_type, GameType::Go);
|
assert_eq!(game.game_type, GameType::Go);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.board_size,
|
game.board_size,
|
||||||
|
@ -928,23 +1032,25 @@ mod file_test {
|
||||||
height: 19
|
height: 19
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(game.trees.len(), 2);
|
||||||
game.black_player,
|
assert_matches!(game.trees[0].root().unwrap().data(), GameNode::MoveNode(node) => {
|
||||||
Player {
|
assert_eq!(node.color, Color::Black);
|
||||||
name: Some("Honinbo Shuwa".to_owned()),
|
assert_eq!(node.mv, Move::Move("pd".to_owned()));
|
||||||
rank: Some("7P".to_owned()),
|
});
|
||||||
team: None
|
assert_matches!(game.trees[1].root().unwrap().data(), GameNode::MoveNode(node) => {
|
||||||
|
assert_eq!(node.color, Color::Black);
|
||||||
|
assert_eq!(node.mv, Move::Move("pc".to_owned()));
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
);
|
|
||||||
assert_eq!(
|
#[test]
|
||||||
game.white_player,
|
fn it_can_copy_a_game_record() {
|
||||||
Player {
|
with_file(std::path::Path::new("test_data/multi-tree.sgf"), |games| {
|
||||||
name: Some("Inoue(Genan)Inseki".to_owned()),
|
let dest = games.clone();
|
||||||
rank: Some("8P".to_owned()),
|
|
||||||
team: None
|
assert_eq!(games.len(), dest.len());
|
||||||
}
|
assert_eq!(games[0], dest[0]);
|
||||||
);
|
});
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -463,15 +463,4 @@ mod tests {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is a file
|
|
||||||
#[test]
|
|
||||||
fn it_handles_shuwa_genan_file() {
|
|
||||||
with_file(
|
|
||||||
std::path::Path::new("test_data/2020 USGO DDK, Round 1.sgf"),
|
|
||||||
|trees| {
|
|
||||||
assert_eq!(trees.len(), 2);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
mod date;
|
mod date;
|
||||||
|
|
||||||
mod game;
|
mod game;
|
||||||
pub use game::{GameNode, GameRecord, MoveNode, Player};
|
pub use game::{GameNode, GameRecord, GameTree, MoveNode, Player};
|
||||||
|
|
||||||
mod parser;
|
mod parser;
|
||||||
pub use parser::{parse_collection, Move};
|
pub use parser::{parse_collection, Move, Size};
|
||||||
|
|
||||||
mod types;
|
mod types;
|
||||||
pub use types::*;
|
pub use types::*;
|
||||||
|
@ -22,6 +22,7 @@ pub enum Error {
|
||||||
InvalidSgf(VerboseNomError),
|
InvalidSgf(VerboseNomError),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct VerboseNomError(nom::error::VerboseError<String>);
|
pub struct VerboseNomError(nom::error::VerboseError<String>);
|
||||||
|
|
||||||
|
@ -73,7 +74,7 @@ pub fn parse_sgf(input: &str) -> Result<Vec<Result<GameRecord, game::GameError>>
|
||||||
let (_, games) = parse_collection::<nom::error::VerboseError<&str>>(input)?;
|
let (_, games) = parse_collection::<nom::error::VerboseError<&str>>(input)?;
|
||||||
let games = games
|
let games = games
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|game| GameRecord::try_from(&game))
|
.map(GameRecord::try_from)
|
||||||
.collect::<Vec<Result<GameRecord, game::GameError>>>();
|
.collect::<Vec<Result<GameRecord, game::GameError>>>();
|
||||||
|
|
||||||
Ok(games)
|
Ok(games)
|
||||||
|
|
|
@ -302,10 +302,10 @@ impl Move {
|
||||||
Move::Move(s) => {
|
Move::Move(s) => {
|
||||||
if s.len() == 2 {
|
if s.len() == 2 {
|
||||||
let mut parts = s.chars();
|
let mut parts = s.chars();
|
||||||
let row_char = parts.next().unwrap();
|
|
||||||
let row = row_char as u8 - b'a';
|
|
||||||
let column_char = parts.next().unwrap();
|
let column_char = parts.next().unwrap();
|
||||||
let column = column_char as u8 - b'a';
|
let column = column_char as u8 - b'a';
|
||||||
|
let row_char = parts.next().unwrap();
|
||||||
|
let row = row_char as u8 - b'a';
|
||||||
Some((row, column))
|
Some((row, column))
|
||||||
} else {
|
} else {
|
||||||
unimplemented!("moves must contain exactly two characters");
|
unimplemented!("moves must contain exactly two characters");
|
||||||
|
@ -1252,15 +1252,6 @@ mod date_test {
|
||||||
Date::Date(NaiveDate::from_ymd_opt(1996, 12, 28).unwrap())
|
Date::Date(NaiveDate::from_ymd_opt(1996, 12, 28).unwrap())
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_matches!(
|
|
||||||
parse_date_field::<nom::error::VerboseError<&str>>().parse("1842-5-16-18"),
|
|
||||||
Ok((_, date)) => assert_eq!(date, vec![
|
|
||||||
Date::Date(NaiveDate::from_ymd_opt(1842, 5, 16).unwrap()),
|
|
||||||
Date::Date(NaiveDate::from_ymd_opt(1842, 5, 17).unwrap()),
|
|
||||||
Date::Date(NaiveDate::from_ymd_opt(1842, 5, 18).unwrap()),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -56,6 +56,7 @@ pub enum Error {
|
||||||
InvalidSgf(VerboseNomError),
|
InvalidSgf(VerboseNomError),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct VerboseNomError(nom::error::VerboseError<String>);
|
pub struct VerboseNomError(nom::error::VerboseError<String>);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
(;GM[1]FF[4]CA[UTF-8]AP[Sabaki:0.52.2]KM[7.5]SZ[19]DT[2024-04-19](;B[pd](;W[qc];B[qd];W[pc];B[oc];W[ob];B[nc];W[nb];B[mc];W[rd];B[re];W[rc];B[qf])(;W[qf];B[nc];W[rd];B[qc];W[pi]))(;B[pc];W[qe];B[oe];W[pg];B[ld];W[qj]))
|
|
@ -9,6 +9,10 @@ use std::{
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// I need to take what I learned about linked lists and about the other Tree data structure, and
|
||||||
|
// apply it here with arena allocation.
|
||||||
|
//
|
||||||
|
// Also, smarter node allocation and pointer handling in order to avoid clones.
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub enum Tree<T> {
|
pub enum Tree<T> {
|
||||||
#[default]
|
#[default]
|
||||||
|
@ -55,6 +59,16 @@ impl<T> Tree<T> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Do a depth-first-search in order to get the path to a node. Start with a naive recursive
|
||||||
|
// implementation, then switch to a stack-based implementation in order to avoid exceeding the
|
||||||
|
// stack.
|
||||||
|
pub fn path_to<F>(&self, f: F) -> Vec<Node<T>>
|
||||||
|
where
|
||||||
|
F: FnOnce(&T) -> bool + Copy,
|
||||||
|
{
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
/// Convert each node of a tree from type T to type U
|
/// Convert each node of a tree from type T to type U
|
||||||
pub fn map<F, U>(&self, op: F) -> Tree<U>
|
pub fn map<F, U>(&self, op: F) -> Tree<U>
|
||||||
where
|
where
|
||||||
|
@ -146,6 +160,13 @@ impl<T> Node<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T: PartialEq> PartialEq for Node<T> {
|
||||||
|
fn eq(&self, other: &Node<T>) -> bool {
|
||||||
|
self.0.borrow().value == other.0.borrow().value
|
||||||
|
&& self.0.borrow().children == other.0.borrow().children
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -184,4 +205,31 @@ mod tests {
|
||||||
assert!(tree2.find_bfs(|val| *val == "16").is_some());
|
assert!(tree2.find_bfs(|val| *val == "16").is_some());
|
||||||
assert!(tree2.find_bfs(|val| *val == "17").is_some());
|
assert!(tree2.find_bfs(|val| *val == "17").is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn path_to_on_empty_tree_returns_empty() {
|
||||||
|
let tree: Tree<&str> = Tree::default();
|
||||||
|
|
||||||
|
assert_eq!(tree.path_to(|val| *val == "i"), vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A
|
||||||
|
// B G H
|
||||||
|
// C I
|
||||||
|
// D E F
|
||||||
|
#[test]
|
||||||
|
fn it_can_find_a_path_to_a_node() {
|
||||||
|
let (tree, a) = Tree::new("A");
|
||||||
|
let b = a.add_child_value("B");
|
||||||
|
let c = b.add_child_value("C");
|
||||||
|
let _d = c.add_child_value("D");
|
||||||
|
let _e = c.add_child_value("D");
|
||||||
|
let _f = c.add_child_value("D");
|
||||||
|
let _g = a.add_child_value("G");
|
||||||
|
let h = a.add_child_value("H");
|
||||||
|
let i = a.add_child_value("I");
|
||||||
|
|
||||||
|
assert_eq!(tree.path_to(|val| *val == "z"), vec![]);
|
||||||
|
assert_eq!(tree.path_to(|val| *val == "i"), vec![a, h, i]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|