Compare commits
	
		
			10 Commits
		
	
	
		
			0e0d72ec9c
			...
			bbf420080d
		
	
	| Author | SHA1 | Date | 
|---|---|---|
| 
							
							
								
									
								
								 | 
						bbf420080d | |
| 
							
							
								
									
								
								 | 
						b34b02af89 | |
| 
							
							
								
									
								
								 | 
						8bf11eecca | |
| 
							
							
								
									
								
								 | 
						6b7d657760 | |
| 
							
							
								
									
								
								 | 
						5b45e50f4f | |
| 
							
							
								
									
								
								 | 
						438bd6b26c | |
| 
							
							
								
									
								
								 | 
						12a8e82015 | |
| 
							
							
								
									
								
								 | 
						3982d1a7a1 | |
| 
							
							
								
									
								
								 | 
						2b1792c2ae | |
| 
							
							
								
									
								
								 | 
						dc3ef4830a | 
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| 
						 | 
				
			
			@ -1,3 +1,3 @@
 | 
			
		|||
[workspace]
 | 
			
		||||
members = ["plate-tool-web", "plate-tool-lib"]
 | 
			
		||||
members = ["plate-tool-web", "plate-tool-lib", "plate-tool-eframe"]
 | 
			
		||||
resolver = "2"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,2 @@
 | 
			
		|||
[build]
 | 
			
		||||
target = "x86_64-unknown-linux-gnu"
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,15 @@
 | 
			
		|||
[package]
 | 
			
		||||
name = "plate-tool-eframe"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
edition = "2021"
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
plate-tool-lib = { path = "../plate-tool-lib" }
 | 
			
		||||
eframe = { version = "0.30", default-features = false, features = [
 | 
			
		||||
	"default_fonts",
 | 
			
		||||
	"glow",
 | 
			
		||||
	"wayland",
 | 
			
		||||
]}
 | 
			
		||||
 | 
			
		||||
log = "0.4"
 | 
			
		||||
env_logger = "0.11"
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,220 @@
 | 
			
		|||
use std::ops::DerefMut;
 | 
			
		||||
use std::sync::Mutex;
 | 
			
		||||
 | 
			
		||||
use eframe::egui::{self};
 | 
			
		||||
 | 
			
		||||
use plate_tool_lib::plate::PlateFormat;
 | 
			
		||||
use plate_tool_lib::plate_instances;
 | 
			
		||||
 | 
			
		||||
use crate::plate::{add_grid, PlateUiState};
 | 
			
		||||
use crate::transfer_menu::{self, transfer_menu, CurrentTransferState, TransferMenuState};
 | 
			
		||||
 | 
			
		||||
#[derive(Default)]
 | 
			
		||||
pub struct MainState {
 | 
			
		||||
    pub source_plates: Vec<plate_tool_lib::plate_instances::PlateInstance>,
 | 
			
		||||
    pub destination_plates: Vec<plate_tool_lib::plate_instances::PlateInstance>,
 | 
			
		||||
    pub transfers: Vec<plate_tool_lib::transfer::Transfer>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn construct_fake_mainstate() -> MainState {
 | 
			
		||||
    let src_plate: plate_tool_lib::plate_instances::PlateInstance =
 | 
			
		||||
        plate_instances::PlateInstance::new(
 | 
			
		||||
            plate_tool_lib::plate::PlateType::Source,
 | 
			
		||||
            PlateFormat::W96,
 | 
			
		||||
            "src1".to_owned(),
 | 
			
		||||
        );
 | 
			
		||||
    let dest_plate: plate_tool_lib::plate_instances::PlateInstance =
 | 
			
		||||
        plate_instances::PlateInstance::new(
 | 
			
		||||
            plate_tool_lib::plate::PlateType::Destination,
 | 
			
		||||
            PlateFormat::W96,
 | 
			
		||||
            "dest1".to_owned(),
 | 
			
		||||
        );
 | 
			
		||||
    let well_a1 = plate_tool_lib::Well { row: 1, col: 1 };
 | 
			
		||||
    let well_c3 = plate_tool_lib::Well { row: 3, col: 3 };
 | 
			
		||||
    let well_a5 = plate_tool_lib::Well { row: 1, col: 5 };
 | 
			
		||||
 | 
			
		||||
    let transfer1_region: plate_tool_lib::transfer_region::TransferRegion =
 | 
			
		||||
        plate_tool_lib::transfer_region::TransferRegion {
 | 
			
		||||
            source_plate: src_plate.plate,
 | 
			
		||||
            source_region: plate_tool_lib::transfer_region::Region::Rect(well_a1, well_c3),
 | 
			
		||||
            dest_plate: dest_plate.plate,
 | 
			
		||||
            dest_region: plate_tool_lib::transfer_region::Region::Point(well_a1),
 | 
			
		||||
            interleave_source: (1, 1),
 | 
			
		||||
            interleave_dest: (1, 1),
 | 
			
		||||
        };
 | 
			
		||||
    let transfer1: plate_tool_lib::transfer::Transfer = plate_tool_lib::transfer::Transfer::new(
 | 
			
		||||
        src_plate.clone(),
 | 
			
		||||
        dest_plate.clone(),
 | 
			
		||||
        transfer1_region,
 | 
			
		||||
        "Shrimp".to_owned(),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    let transfer2_region: plate_tool_lib::transfer_region::TransferRegion =
 | 
			
		||||
        plate_tool_lib::transfer_region::TransferRegion {
 | 
			
		||||
            source_plate: src_plate.plate,
 | 
			
		||||
            source_region: plate_tool_lib::transfer_region::Region::Rect(well_a1, well_c3),
 | 
			
		||||
            dest_plate: dest_plate.plate,
 | 
			
		||||
            dest_region: plate_tool_lib::transfer_region::Region::Point(well_a5),
 | 
			
		||||
            interleave_source: (1, 1),
 | 
			
		||||
            interleave_dest: (2, 2),
 | 
			
		||||
        };
 | 
			
		||||
    let transfer2: plate_tool_lib::transfer::Transfer = plate_tool_lib::transfer::Transfer::new(
 | 
			
		||||
        src_plate.clone(),
 | 
			
		||||
        dest_plate.clone(),
 | 
			
		||||
        transfer2_region,
 | 
			
		||||
        "Shrimp".to_owned(),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    MainState {
 | 
			
		||||
        source_plates: vec![src_plate],
 | 
			
		||||
        destination_plates: vec![dest_plate],
 | 
			
		||||
        transfers: vec![transfer1, transfer2],
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[non_exhaustive]
 | 
			
		||||
struct MainWindowState {
 | 
			
		||||
    show_side_panel: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Default for MainWindowState {
 | 
			
		||||
    fn default() -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            show_side_panel: true,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[non_exhaustive]
 | 
			
		||||
pub struct PlateToolEframe {
 | 
			
		||||
    source_plate_state: Mutex<PlateUiState>,
 | 
			
		||||
    destination_plate_state: Mutex<PlateUiState>,
 | 
			
		||||
    main_window_state: MainWindowState,
 | 
			
		||||
    current_transfer_state: CurrentTransferState,
 | 
			
		||||
    transfer_menu_state: TransferMenuState,
 | 
			
		||||
    transfer_region_cache: plate_tool_lib::transfer_region_cache::TransferRegionCache,
 | 
			
		||||
    main_state: MainState,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Default for PlateToolEframe {
 | 
			
		||||
    fn default() -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            source_plate_state: Mutex::new(PlateUiState::default()),
 | 
			
		||||
            destination_plate_state: Mutex::new(PlateUiState::default()),
 | 
			
		||||
            main_window_state: MainWindowState::default(),
 | 
			
		||||
            current_transfer_state: CurrentTransferState::default(),
 | 
			
		||||
            transfer_menu_state: TransferMenuState::default(),
 | 
			
		||||
            transfer_region_cache: plate_tool_lib::transfer_region_cache::TransferRegionCache::new(
 | 
			
		||||
            ),
 | 
			
		||||
            main_state: construct_fake_mainstate(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl PlateToolEframe {
 | 
			
		||||
    pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
 | 
			
		||||
        // Would load state here
 | 
			
		||||
 | 
			
		||||
        let pte: PlateToolEframe = Default::default();
 | 
			
		||||
        pte.transfer_region_cache.generate_cache(&pte.main_state.transfers);
 | 
			
		||||
 | 
			
		||||
        pte
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl eframe::App for PlateToolEframe {
 | 
			
		||||
    // State storage
 | 
			
		||||
    /*
 | 
			
		||||
    fn save(&mut self, storage: &mut dyn eframe::Storage) {
 | 
			
		||||
        unimplemented!()
 | 
			
		||||
    };
 | 
			
		||||
    */
 | 
			
		||||
 | 
			
		||||
    fn update(&mut self, ctx: &eframe::egui::Context, _frame: &mut eframe::Frame) {
 | 
			
		||||
        let ordered_ids: Vec<plate_tool_lib::uuid::Uuid> = {
 | 
			
		||||
            let mut ids: Vec<plate_tool_lib::uuid::Uuid> = self
 | 
			
		||||
                .main_state
 | 
			
		||||
                .transfers
 | 
			
		||||
                .clone()
 | 
			
		||||
                .iter()
 | 
			
		||||
                .map(|x| x.id)
 | 
			
		||||
                .collect();
 | 
			
		||||
            ids.sort_unstable();
 | 
			
		||||
            ids
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
 | 
			
		||||
            egui::menu::bar(ui, |ui| {
 | 
			
		||||
                ui.menu_button("File", |ui| {
 | 
			
		||||
                    if ui.button("New").clicked() {}
 | 
			
		||||
                    ui.menu_button("Export", |ui| {
 | 
			
		||||
                        if ui.button("Export as CSV").clicked() {}
 | 
			
		||||
                        if ui.button("Export as JSON").clicked() {}
 | 
			
		||||
                    });
 | 
			
		||||
                    ui.menu_button("Import", |ui| {
 | 
			
		||||
                        if ui.button("Import from JSON").clicked() {}
 | 
			
		||||
                        if ui.button("Import transfer from CSV").clicked() {}
 | 
			
		||||
                    });
 | 
			
		||||
                });
 | 
			
		||||
                ui.menu_button("Options", |ui| {
 | 
			
		||||
                    ui.menu_button("Styles", |ui| {
 | 
			
		||||
                        if ui.button("Toggle transfer hashes").clicked() {}
 | 
			
		||||
                        if ui.button("Toggle volume heatmap").clicked() {}
 | 
			
		||||
                        if ui.button("Toggle current coordinates view").clicked() {}
 | 
			
		||||
                    });
 | 
			
		||||
                    ui.menu_button("Exports", |ui| {
 | 
			
		||||
                        if ui.button("Change CSV export type").clicked() {}
 | 
			
		||||
                    });
 | 
			
		||||
                    ui.menu_button("Windows", |ui| {
 | 
			
		||||
                        ui.toggle_value(
 | 
			
		||||
                            &mut self.main_window_state.show_side_panel,
 | 
			
		||||
                            "Toggle side panel",
 | 
			
		||||
                        );
 | 
			
		||||
                    });
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if self.main_window_state.show_side_panel {
 | 
			
		||||
            egui::SidePanel::left("side_menus").show(ctx, |ui| {
 | 
			
		||||
                ui.vertical(|ui| {
 | 
			
		||||
                    transfer_menu(ui, &self.current_transfer_state, &mut self.transfer_menu_state);
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        egui::CentralPanel::default().show(ctx, |ui| {
 | 
			
		||||
            ui.vertical(|ui| {
 | 
			
		||||
                let available_size = ui.available_size();
 | 
			
		||||
                let half_height = {
 | 
			
		||||
                    let mut x = available_size;
 | 
			
		||||
                    x.y /= 2.0;
 | 
			
		||||
                    x
 | 
			
		||||
                };
 | 
			
		||||
                add_grid(
 | 
			
		||||
                    half_height,
 | 
			
		||||
                    PlateFormat::W96,
 | 
			
		||||
                    &self.main_state.transfers,
 | 
			
		||||
                    plate_tool_lib::plate::PlateType::Source,
 | 
			
		||||
                    &ordered_ids,
 | 
			
		||||
                    &self.transfer_region_cache,
 | 
			
		||||
                    Some(&self.current_transfer_state),
 | 
			
		||||
                    ui,
 | 
			
		||||
                    self.source_plate_state.lock().unwrap().deref_mut(),
 | 
			
		||||
                );
 | 
			
		||||
                add_grid(
 | 
			
		||||
                    half_height,
 | 
			
		||||
                    PlateFormat::W96,
 | 
			
		||||
                    &self.main_state.transfers,
 | 
			
		||||
                    plate_tool_lib::plate::PlateType::Destination,
 | 
			
		||||
                    &ordered_ids,
 | 
			
		||||
                    &self.transfer_region_cache,
 | 
			
		||||
                    Some(&self.current_transfer_state),
 | 
			
		||||
                    ui,
 | 
			
		||||
                    self.destination_plate_state.lock().unwrap().deref_mut(),
 | 
			
		||||
                );
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
mod app;
 | 
			
		||||
mod plate;
 | 
			
		||||
mod tree;
 | 
			
		||||
mod transfer_menu;
 | 
			
		||||
pub use app::PlateToolEframe;
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
use eframe::*;
 | 
			
		||||
use eframe::egui;
 | 
			
		||||
 | 
			
		||||
fn main() -> eframe::Result{
 | 
			
		||||
    env_logger::init();
 | 
			
		||||
    log::info!("Shrimp!");
 | 
			
		||||
 | 
			
		||||
    let native_options = eframe::NativeOptions {
 | 
			
		||||
        viewport: egui::ViewportBuilder::default()
 | 
			
		||||
            .with_title("Shrimp"),
 | 
			
		||||
        ..Default::default()
 | 
			
		||||
    };
 | 
			
		||||
    eframe::run_native(
 | 
			
		||||
        "PlateToolEframe",
 | 
			
		||||
        native_options,
 | 
			
		||||
        Box::new(|cc| Ok(Box::new(plate_tool_eframe::PlateToolEframe::new(cc))))
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,404 @@
 | 
			
		|||
use eframe::egui::{self, pos2, Color32, Rounding};
 | 
			
		||||
use plate_tool_lib::plate::PlateFormat;
 | 
			
		||||
use plate_tool_lib::transfer_region::Region;
 | 
			
		||||
use plate_tool_lib::uuid::Uuid;
 | 
			
		||||
use plate_tool_lib::Well;
 | 
			
		||||
 | 
			
		||||
use crate::transfer_menu::CurrentTransferState;
 | 
			
		||||
 | 
			
		||||
use std::sync::LazyLock;
 | 
			
		||||
 | 
			
		||||
const PALETTE: plate_tool_lib::util::ColorPalette = plate_tool_lib::util::Palettes::RAINBOW;
 | 
			
		||||
 | 
			
		||||
// Stroke types
 | 
			
		||||
static STROKE_DEFAULT: LazyLock<egui::Stroke> =
 | 
			
		||||
    LazyLock::new(|| egui::Stroke::new(2.0, egui::Color32::from_gray(128)));
 | 
			
		||||
static STROKE_CURRENT: LazyLock<egui::Stroke> =
 | 
			
		||||
    LazyLock::new(|| egui::Stroke::new(2.0, egui::Color32::from_gray(200)));
 | 
			
		||||
static STROKE_SELECT: LazyLock<egui::Stroke> =
 | 
			
		||||
    LazyLock::new(|| egui::Stroke::new(3.0, egui::Color32::from_gray(200)));
 | 
			
		||||
static STROKE_HOVER: LazyLock<egui::Stroke> =
 | 
			
		||||
    LazyLock::new(|| egui::Stroke::new(3.0, egui::Color32::from_gray(255)));
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Debug)]
 | 
			
		||||
pub struct PlateUiState {
 | 
			
		||||
    pub drag_start_position: Option<egui::Pos2>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Default for PlateUiState {
 | 
			
		||||
    fn default() -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            drag_start_position: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Copy, Debug)]
 | 
			
		||||
struct WellInfo {
 | 
			
		||||
    volume: f32,
 | 
			
		||||
    color: [f64; 3],
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl WellInfo {
 | 
			
		||||
    fn new(volume: f32, color: [f64; 3]) -> Self {
 | 
			
		||||
        WellInfo { volume, color }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn calculate_grid_params(
 | 
			
		||||
    ul_origin: (f32, f32),
 | 
			
		||||
    tw: f32,
 | 
			
		||||
    th: f32,
 | 
			
		||||
    rows: u8,
 | 
			
		||||
    columns: u8,
 | 
			
		||||
) -> (f32, f32, f32) {
 | 
			
		||||
    // (Start X, Start Y, Radius)
 | 
			
		||||
    const PADDING: f32 = 20.0;
 | 
			
		||||
 | 
			
		||||
    let usable_width = tw - 2.0 * PADDING;
 | 
			
		||||
    let usable_height = th - 2.0 * PADDING;
 | 
			
		||||
 | 
			
		||||
    let shared_width = f32::ceil(usable_width / columns as f32);
 | 
			
		||||
    let shared_height = f32::ceil(usable_height / rows as f32);
 | 
			
		||||
    let radius = f32::min(shared_width, shared_height) / 2.0;
 | 
			
		||||
    (ul_origin.0 + PADDING, ul_origin.1 + PADDING, radius)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn get_hover_well(
 | 
			
		||||
    response: &egui::Response,
 | 
			
		||||
    start_x: f32,
 | 
			
		||||
    start_y: f32,
 | 
			
		||||
    radius: f32,
 | 
			
		||||
) -> Option<(u8, u8)> {
 | 
			
		||||
    get_well_from_pos(response.hover_pos(), start_x, start_y, radius)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn get_well_from_pos(
 | 
			
		||||
    position: Option<egui::Pos2>,
 | 
			
		||||
    start_x: f32,
 | 
			
		||||
    start_y: f32,
 | 
			
		||||
    radius: f32,
 | 
			
		||||
) -> Option<(u8, u8)> {
 | 
			
		||||
    // Some((row, column))
 | 
			
		||||
    position
 | 
			
		||||
        .map(|p| Into::<(f32, f32)>::into(p))
 | 
			
		||||
        .and_then(|(x, y)| {
 | 
			
		||||
            // Check bounds
 | 
			
		||||
            if x < start_x || y < start_y {
 | 
			
		||||
                return None;
 | 
			
		||||
            }
 | 
			
		||||
            // CHECK Bottom Right BOUND
 | 
			
		||||
 | 
			
		||||
            let solved_column: u8 = (x - start_x).div_euclid(radius * 2.0) as u8;
 | 
			
		||||
            let solved_row: u8 = (y - start_y).div_euclid(radius * 2.0) as u8;
 | 
			
		||||
 | 
			
		||||
            Some((solved_row, solved_column))
 | 
			
		||||
        })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn calculate_shading_for_wells(
 | 
			
		||||
    rows: u8,
 | 
			
		||||
    columns: u8,
 | 
			
		||||
    transfers: &Vec<plate_tool_lib::transfer::Transfer>,
 | 
			
		||||
    plate_type: plate_tool_lib::plate::PlateType,
 | 
			
		||||
    ordered_ids: &[Uuid],
 | 
			
		||||
    cache: &plate_tool_lib::transfer_region_cache::TransferRegionCache,
 | 
			
		||||
) -> Box<[Option<WellInfo>]> {
 | 
			
		||||
    let box_size: usize = rows as usize * columns as usize;
 | 
			
		||||
    let mut well_infos: Box<[Option<WellInfo>]> = vec![None; box_size].into_boxed_slice();
 | 
			
		||||
 | 
			
		||||
    for transfer in transfers {
 | 
			
		||||
        let cache_result = match plate_type {
 | 
			
		||||
            plate_tool_lib::plate::PlateType::Source => cache.get_source(transfer),
 | 
			
		||||
            plate_tool_lib::plate::PlateType::Destination => cache.get_destination(transfer),
 | 
			
		||||
        };
 | 
			
		||||
        if let Some(wells) = cache_result {
 | 
			
		||||
            for well in wells.iter().filter(|x| x.row <= rows && x.col <= columns) {
 | 
			
		||||
                if let Some(mut x) =
 | 
			
		||||
                    well_infos[well.row as usize * columns as usize + well.col as usize]
 | 
			
		||||
                {
 | 
			
		||||
                    x.volume += 5.0;
 | 
			
		||||
                    x.color = PALETTE.get_ordered(transfer.id, ordered_ids);
 | 
			
		||||
                } else {
 | 
			
		||||
                    well_infos[well.row as usize * columns as usize + well.col as usize] = Some(
 | 
			
		||||
                        WellInfo::new(5.0, PALETTE.get_ordered(transfer.id, ordered_ids)),
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    well_infos
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn f64_to_color32(x: [f64; 3]) -> Color32 {
 | 
			
		||||
    let r = x[0] as u8;
 | 
			
		||||
    let g = x[1] as u8;
 | 
			
		||||
    let b = x[2] as u8;
 | 
			
		||||
    Color32::from_rgb(r, g, b)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn add_grid_sub(
 | 
			
		||||
    size: egui::Vec2,
 | 
			
		||||
    rows: u8,
 | 
			
		||||
    columns: u8,
 | 
			
		||||
    transfers: &Vec<plate_tool_lib::transfer::Transfer>,
 | 
			
		||||
    plate_type: plate_tool_lib::plate::PlateType,
 | 
			
		||||
    ordered_ids: &[Uuid],
 | 
			
		||||
    cache: &plate_tool_lib::transfer_region_cache::TransferRegionCache,
 | 
			
		||||
    current_transfer_state: Option<&CurrentTransferState>,
 | 
			
		||||
    ui: &mut egui::Ui,
 | 
			
		||||
    state: &mut PlateUiState,
 | 
			
		||||
) {
 | 
			
		||||
    let (response, painter) = ui.allocate_painter(size, egui::Sense::click_and_drag());
 | 
			
		||||
 | 
			
		||||
    let rect = response.rect;
 | 
			
		||||
 | 
			
		||||
    let ul_origin = rect.left_top();
 | 
			
		||||
    let total_width = rect.width();
 | 
			
		||||
    let total_height = rect.height();
 | 
			
		||||
 | 
			
		||||
    let (start_x, start_y, radius) =
 | 
			
		||||
        calculate_grid_params(ul_origin.into(), total_width, total_height, rows, columns);
 | 
			
		||||
 | 
			
		||||
    // Manage clicks and drags
 | 
			
		||||
    if response.drag_started() {
 | 
			
		||||
        state.drag_start_position = Some(response.hover_pos().unwrap());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let drag_start_well = get_well_from_pos(state.drag_start_position, start_x, start_y, radius);
 | 
			
		||||
    let hovered_well = get_hover_well(&response, start_x, start_y, radius);
 | 
			
		||||
 | 
			
		||||
    if response.clicked() {
 | 
			
		||||
        if let Some(cts) = current_transfer_state {
 | 
			
		||||
            let end_well: Option<Well> = hovered_well.map(|(row, col)| Well {
 | 
			
		||||
                row: row + 1,
 | 
			
		||||
                col: col + 1,
 | 
			
		||||
            });
 | 
			
		||||
            if let Some(end_well) = end_well {
 | 
			
		||||
                let new_region = Region::new_from_wells(end_well, None);
 | 
			
		||||
                let mut cts = cts.lock().unwrap();
 | 
			
		||||
                match plate_type {
 | 
			
		||||
                    plate_tool_lib::plate::PlateType::Source => cts.source_region = new_region,
 | 
			
		||||
                    plate_tool_lib::plate::PlateType::Destination => {
 | 
			
		||||
                        cts.destination_region = new_region
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    if response.drag_stopped() {
 | 
			
		||||
        if let Some(cts) = current_transfer_state {
 | 
			
		||||
            let start_well: Well = drag_start_well
 | 
			
		||||
                .map(|(row, col)| Well {
 | 
			
		||||
                    // Lib uses 1-indexing!
 | 
			
		||||
                    row: row + 1,
 | 
			
		||||
                    col: col + 1,
 | 
			
		||||
                })
 | 
			
		||||
                .unwrap();
 | 
			
		||||
            let end_well: Option<Well> = hovered_well.map(|(row, col)| Well {
 | 
			
		||||
                row: row + 1,
 | 
			
		||||
                col: col + 1,
 | 
			
		||||
            });
 | 
			
		||||
            let new_region = Region::new_from_wells(start_well, end_well);
 | 
			
		||||
            let mut cts = cts.lock().unwrap();
 | 
			
		||||
            match plate_type {
 | 
			
		||||
                plate_tool_lib::plate::PlateType::Source => cts.source_region = new_region,
 | 
			
		||||
                plate_tool_lib::plate::PlateType::Destination => {
 | 
			
		||||
                    cts.destination_region = new_region
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        state.drag_start_position = None;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let current_selection: Option<Region> = {
 | 
			
		||||
        if let Some(cts) = current_transfer_state {
 | 
			
		||||
            let cts = cts.lock().unwrap();
 | 
			
		||||
            match plate_type {
 | 
			
		||||
                plate_tool_lib::plate::PlateType::Source => Some(cts.source_region.clone()),
 | 
			
		||||
                plate_tool_lib::plate::PlateType::Destination => {
 | 
			
		||||
                    Some(cts.destination_region.clone())
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            None
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
    let well_infos = {
 | 
			
		||||
        let mut well_infos = calculate_shading_for_wells(rows, columns, transfers, plate_type, ordered_ids, cache);
 | 
			
		||||
        // Get wells in the current transfer to tack on to well_infos separately
 | 
			
		||||
        let current_transfer_wells: Option<Box<[(usize,usize)]>> = {
 | 
			
		||||
            (match plate_type {
 | 
			
		||||
                plate_tool_lib::plate::PlateType::Source => current_transfer_state
 | 
			
		||||
                    .and_then(|x| x.lock().ok())
 | 
			
		||||
                    .map(|mut x| x.get_source_wells()),
 | 
			
		||||
                plate_tool_lib::plate::PlateType::Destination => current_transfer_state
 | 
			
		||||
                    .and_then(|x| x.lock().ok())
 | 
			
		||||
                    .map(|mut x| x.get_destination_wells()),
 | 
			
		||||
            })
 | 
			
		||||
                // Drop back to 0-indexing here
 | 
			
		||||
            .map(|xs| xs.iter().map(|x| (x.row as usize - 1, x.col as usize - 1)).collect())
 | 
			
		||||
        };
 | 
			
		||||
        if let Some(wells) = current_transfer_wells {
 | 
			
		||||
            for w in wells {
 | 
			
		||||
                let well_info = &mut well_infos[w.0 * columns as usize + w.1];
 | 
			
		||||
                let volume = well_info.map(|x| x.volume).unwrap_or(0.0)
 | 
			
		||||
                    + current_transfer_state.and_then(|x| x.lock().ok())
 | 
			
		||||
                    .map(|x| x.volume)
 | 
			
		||||
                    .unwrap_or(0.0);
 | 
			
		||||
 | 
			
		||||
                *well_info = Some(WellInfo {
 | 
			
		||||
                    color: [255.0,255.0,255.0],
 | 
			
		||||
                    volume: 1.0
 | 
			
		||||
                })
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        well_infos
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    // Plate Frame
 | 
			
		||||
    painter.rect_stroke(
 | 
			
		||||
        egui::Rect {
 | 
			
		||||
            min: pos2(start_x, start_y),
 | 
			
		||||
            max: pos2(
 | 
			
		||||
                start_x + 2.0 * radius * columns as f32,
 | 
			
		||||
                start_y + 2.0 * radius * rows as f32,
 | 
			
		||||
            ),
 | 
			
		||||
        },
 | 
			
		||||
        Rounding::default(),
 | 
			
		||||
        *STROKE_DEFAULT,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Draw wells
 | 
			
		||||
    for c_row in 0..rows {
 | 
			
		||||
        for c_column in 0..columns {
 | 
			
		||||
            let center = egui::pos2(
 | 
			
		||||
                start_x + radius + 2.0 * radius * c_column as f32,
 | 
			
		||||
                start_y + radius + 2.0 * radius * c_row as f32,
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            //
 | 
			
		||||
            // Draw fill first
 | 
			
		||||
            //
 | 
			
		||||
 | 
			
		||||
            if let Some(well_info) =
 | 
			
		||||
                well_infos[c_row as usize * columns as usize + c_column as usize]
 | 
			
		||||
            {
 | 
			
		||||
                painter.circle_filled(center, radius * 0.80, f64_to_color32(well_info.color));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            //
 | 
			
		||||
            // Draw stroke on top
 | 
			
		||||
            //
 | 
			
		||||
 | 
			
		||||
            // Base stroke
 | 
			
		||||
            painter.circle_stroke(center, radius * 0.80, *STROKE_DEFAULT);
 | 
			
		||||
 | 
			
		||||
            if current_selection.as_ref().is_some_and(|x| {
 | 
			
		||||
                x.contains_well(
 | 
			
		||||
                    &Well {
 | 
			
		||||
                        row: c_row + 1,
 | 
			
		||||
                        col: c_column + 1,
 | 
			
		||||
                    },
 | 
			
		||||
                    None,
 | 
			
		||||
                )
 | 
			
		||||
            }) {
 | 
			
		||||
                painter.circle_stroke(center, radius * 0.80, *STROKE_CURRENT);
 | 
			
		||||
            }
 | 
			
		||||
            if response.dragged() {
 | 
			
		||||
                if let (Some(hw), Some(dw)) = (hovered_well, drag_start_well) {
 | 
			
		||||
                    if c_column <= u8::max(hw.1, dw.1)
 | 
			
		||||
                        && c_column >= u8::min(hw.1, dw.1)
 | 
			
		||||
                        && c_row <= u8::max(hw.0, dw.0)
 | 
			
		||||
                        && c_row >= u8::min(hw.0, dw.0)
 | 
			
		||||
                    {
 | 
			
		||||
                        painter.circle_stroke(center, radius * 0.80, *STROKE_SELECT);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            if Some((c_row, c_column)) == hovered_well {
 | 
			
		||||
                painter.circle_stroke(center, radius * 0.80, *STROKE_HOVER);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Draw row/column labels
 | 
			
		||||
    for c_row in 0..rows {
 | 
			
		||||
        painter.text(
 | 
			
		||||
            egui::pos2(
 | 
			
		||||
                start_x - 10.0,
 | 
			
		||||
                start_y + radius + 2.0 * radius * c_row as f32,
 | 
			
		||||
            ),
 | 
			
		||||
            egui::Align2::CENTER_CENTER,
 | 
			
		||||
            (c_row + 1).to_string(),
 | 
			
		||||
            egui::FontId::monospace(f32::min(radius, 14.0)),
 | 
			
		||||
            egui::Color32::from_gray(128),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
    for c_column in 0..columns {
 | 
			
		||||
        painter.text(
 | 
			
		||||
            egui::pos2(
 | 
			
		||||
                start_x + radius + 2.0 * radius * c_column as f32,
 | 
			
		||||
                start_y - 12.0,
 | 
			
		||||
            ),
 | 
			
		||||
            egui::Align2::CENTER_CENTER,
 | 
			
		||||
            plate_tool_lib::util::num_to_letters(c_column + 1).unwrap(),
 | 
			
		||||
            egui::FontId::monospace(f32::min(radius, 14.0)),
 | 
			
		||||
            egui::Color32::from_gray(128),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn add_grid(
 | 
			
		||||
    size: egui::Vec2,
 | 
			
		||||
    pf: PlateFormat,
 | 
			
		||||
    transfers: &Vec<plate_tool_lib::transfer::Transfer>,
 | 
			
		||||
    plate_type: plate_tool_lib::plate::PlateType,
 | 
			
		||||
    ordered_ids: &[Uuid],
 | 
			
		||||
    cache: &plate_tool_lib::transfer_region_cache::TransferRegionCache,
 | 
			
		||||
    current_transfer_state: Option<&CurrentTransferState>,
 | 
			
		||||
    ui: &mut egui::Ui,
 | 
			
		||||
    state: &mut PlateUiState,
 | 
			
		||||
) {
 | 
			
		||||
    match pf {
 | 
			
		||||
        PlateFormat::W96 => add_grid_sub(
 | 
			
		||||
            size,
 | 
			
		||||
            8,
 | 
			
		||||
            12,
 | 
			
		||||
            transfers,
 | 
			
		||||
            plate_type,
 | 
			
		||||
            ordered_ids,
 | 
			
		||||
            cache,
 | 
			
		||||
            current_transfer_state,
 | 
			
		||||
            ui,
 | 
			
		||||
            state,
 | 
			
		||||
        ),
 | 
			
		||||
        PlateFormat::W384 => add_grid_sub(
 | 
			
		||||
            size,
 | 
			
		||||
            16,
 | 
			
		||||
            24,
 | 
			
		||||
            transfers,
 | 
			
		||||
            plate_type,
 | 
			
		||||
            ordered_ids,
 | 
			
		||||
            cache,
 | 
			
		||||
            current_transfer_state,
 | 
			
		||||
            ui,
 | 
			
		||||
            state,
 | 
			
		||||
        ),
 | 
			
		||||
        PlateFormat::W1536 => add_grid_sub(
 | 
			
		||||
            size,
 | 
			
		||||
            32,
 | 
			
		||||
            48,
 | 
			
		||||
            transfers,
 | 
			
		||||
            plate_type,
 | 
			
		||||
            ordered_ids,
 | 
			
		||||
            cache,
 | 
			
		||||
            current_transfer_state,
 | 
			
		||||
            ui,
 | 
			
		||||
            state,
 | 
			
		||||
        ),
 | 
			
		||||
        _ => unimplemented!(),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,223 @@
 | 
			
		|||
use eframe::egui;
 | 
			
		||||
use plate_tool_lib::transfer_region::{Region, TransferRegion};
 | 
			
		||||
use std::sync::{Arc, Mutex};
 | 
			
		||||
use std::hash::{DefaultHasher, Hash, Hasher};
 | 
			
		||||
 | 
			
		||||
pub type CurrentTransferState = Arc<Mutex<CurrentTransferStateInterior>>;
 | 
			
		||||
pub struct CurrentTransferStateInterior {
 | 
			
		||||
    pub transfer_name: String,
 | 
			
		||||
    pub source_region: plate_tool_lib::transfer_region::Region,
 | 
			
		||||
    pub destination_region: plate_tool_lib::transfer_region::Region,
 | 
			
		||||
    pub source_plate: plate_tool_lib::plate::Plate,
 | 
			
		||||
    pub destination_plate: plate_tool_lib::plate::Plate,
 | 
			
		||||
    pub source_row_interleave: i8,
 | 
			
		||||
    pub source_column_interleave: i8,
 | 
			
		||||
    pub destination_row_interleave: i8,
 | 
			
		||||
    pub destination_column_interleave: i8,
 | 
			
		||||
    pub volume: f32,
 | 
			
		||||
    transfer_hash: Option<u64>,
 | 
			
		||||
    source_wells: Option<Vec<plate_tool_lib::Well>>,
 | 
			
		||||
    destination_wells: Option<Vec<plate_tool_lib::Well>>,
 | 
			
		||||
}
 | 
			
		||||
impl Default for CurrentTransferStateInterior {
 | 
			
		||||
    fn default() -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            transfer_name: String::default(),
 | 
			
		||||
            source_region: plate_tool_lib::transfer_region::Region::Point(plate_tool_lib::Well {
 | 
			
		||||
                row: 1,
 | 
			
		||||
                col: 1,
 | 
			
		||||
            }),
 | 
			
		||||
            destination_region: plate_tool_lib::transfer_region::Region::Point(
 | 
			
		||||
                plate_tool_lib::Well { row: 1, col: 1 },
 | 
			
		||||
            ),
 | 
			
		||||
            source_plate: plate_tool_lib::plate::SOURCE_W96,
 | 
			
		||||
            destination_plate: plate_tool_lib::plate::DESTINATION_W96,
 | 
			
		||||
            source_row_interleave: 1,
 | 
			
		||||
            source_column_interleave: 1,
 | 
			
		||||
            destination_row_interleave: 1,
 | 
			
		||||
            destination_column_interleave: 1,
 | 
			
		||||
            volume: 5.0,
 | 
			
		||||
            transfer_hash: None,
 | 
			
		||||
            source_wells: None,
 | 
			
		||||
            destination_wells: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
impl CurrentTransferStateInterior {
 | 
			
		||||
    pub fn get_source_wells(&mut self) -> Box<[plate_tool_lib::Well]> {
 | 
			
		||||
        if self.source_wells.is_none() {
 | 
			
		||||
            self.recalulate_wells();
 | 
			
		||||
        }
 | 
			
		||||
        self.recalculate_wells_if_needed();
 | 
			
		||||
        self.source_wells
 | 
			
		||||
            .clone()
 | 
			
		||||
            .expect("Source wells must have been calculated by here.")
 | 
			
		||||
            .into_boxed_slice()
 | 
			
		||||
    }
 | 
			
		||||
    pub fn get_destination_wells(&mut self) -> Box<[plate_tool_lib::Well]> {
 | 
			
		||||
        if self.destination_wells.is_none() {
 | 
			
		||||
            self.recalulate_wells();
 | 
			
		||||
        }
 | 
			
		||||
        self.recalculate_wells_if_needed();
 | 
			
		||||
        self.destination_wells
 | 
			
		||||
            .clone()
 | 
			
		||||
            .expect("Destination wells must have been calculated by here.")
 | 
			
		||||
            .into_boxed_slice()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn recalculate_wells_if_needed(&mut self) {
 | 
			
		||||
        if self.transfer_hash.is_none() {
 | 
			
		||||
            self.recalulate_wells();
 | 
			
		||||
        }
 | 
			
		||||
        if self.transfer_hash.unwrap() != self.calculate_hash() {
 | 
			
		||||
            self.recalulate_wells();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    fn recalulate_wells(&mut self) {
 | 
			
		||||
        let tr = self.generate_transfer_region();
 | 
			
		||||
 | 
			
		||||
        self.source_wells = Some(tr.get_source_wells());
 | 
			
		||||
        self.destination_wells = Some(tr.get_destination_wells());
 | 
			
		||||
        self.transfer_hash = Some(self.calculate_hash());
 | 
			
		||||
    }
 | 
			
		||||
    fn calculate_hash(&self) -> u64 {
 | 
			
		||||
        let mut hasher = DefaultHasher::new();
 | 
			
		||||
        self.generate_transfer_region().hash(&mut hasher);
 | 
			
		||||
        hasher.finish()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn generate_transfer_region(&self) -> TransferRegion {
 | 
			
		||||
        TransferRegion {
 | 
			
		||||
            source_plate: self.source_plate,
 | 
			
		||||
            source_region: self.source_region.clone(),
 | 
			
		||||
            dest_plate: self.destination_plate,
 | 
			
		||||
            dest_region: self.destination_region.clone(),
 | 
			
		||||
            interleave_source: (self.source_row_interleave, self.source_column_interleave),
 | 
			
		||||
            interleave_dest: (self.destination_row_interleave, self.destination_column_interleave),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct TransferMenuState {
 | 
			
		||||
    pub source_region_string: String,
 | 
			
		||||
    pub destination_region_string: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Default for TransferMenuState {
 | 
			
		||||
    fn default() -> Self {
 | 
			
		||||
        TransferMenuState {
 | 
			
		||||
            source_region_string: String::new(),
 | 
			
		||||
            destination_region_string: String::new(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
impl TransferMenuState {
 | 
			
		||||
    fn new_from_cts(cts: &CurrentTransferState) -> Self {
 | 
			
		||||
        let cts = cts.lock().unwrap();
 | 
			
		||||
        let source_region_string = cts.source_region.to_string();
 | 
			
		||||
        let destination_region_string = cts.destination_region.to_string();
 | 
			
		||||
        TransferMenuState {
 | 
			
		||||
            source_region_string,
 | 
			
		||||
            destination_region_string,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn transfer_menu(
 | 
			
		||||
    ui: &mut egui::Ui,
 | 
			
		||||
    state: &CurrentTransferState,
 | 
			
		||||
    ui_state: &mut TransferMenuState,
 | 
			
		||||
) {
 | 
			
		||||
    // Can we reduce the length of this lock pls
 | 
			
		||||
    let mut state = state.lock().unwrap();
 | 
			
		||||
 | 
			
		||||
    ui.horizontal(|ui| {
 | 
			
		||||
        ui.add(egui::Label::new("Name"));
 | 
			
		||||
        ui.add(
 | 
			
		||||
            egui::TextEdit::singleline(&mut state.transfer_name)
 | 
			
		||||
                .hint_text("Transfer Name")
 | 
			
		||||
                .horizontal_align(egui::Align::Center),
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ui.horizontal(|ui| {
 | 
			
		||||
        ui.add(egui::Label::new("Source Region"));
 | 
			
		||||
        let resp = ui.add(
 | 
			
		||||
            egui::TextEdit::singleline(&mut ui_state.source_region_string)
 | 
			
		||||
                .hint_text("Source Region")
 | 
			
		||||
                .char_limit(9)
 | 
			
		||||
                .horizontal_align(egui::Align::Center),
 | 
			
		||||
        );
 | 
			
		||||
        if resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
 | 
			
		||||
            if let Some(new_region) = Region::try_parse_str(&ui_state.source_region_string) {
 | 
			
		||||
                state.source_region = new_region;
 | 
			
		||||
            } else {
 | 
			
		||||
                log::warn!("Invalid source region entered.")
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if !resp.has_focus() && !resp.lost_focus() {
 | 
			
		||||
            ui_state.source_region_string = state.source_region.to_string();
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
    ui.end_row();
 | 
			
		||||
 | 
			
		||||
    ui.horizontal(|ui| {
 | 
			
		||||
        ui.add(egui::Label::new("Destination Region"));
 | 
			
		||||
        let resp = ui.add(
 | 
			
		||||
            egui::TextEdit::singleline(&mut ui_state.destination_region_string)
 | 
			
		||||
                .hint_text("Destination Region")
 | 
			
		||||
                .char_limit(9)
 | 
			
		||||
                .horizontal_align(egui::Align::Center),
 | 
			
		||||
        );
 | 
			
		||||
        if resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
 | 
			
		||||
            if let Some(new_region) = Region::try_parse_str(&ui_state.destination_region_string) {
 | 
			
		||||
                state.destination_region = new_region;
 | 
			
		||||
            } else {
 | 
			
		||||
                log::warn!("Invalid destination region entered.")
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if !resp.has_focus() {
 | 
			
		||||
            ui_state.destination_region_string = state.destination_region.to_string();
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ui.vertical(|ui| {
 | 
			
		||||
        ui.label("Source Interleave");
 | 
			
		||||
        ui.horizontal(|ui| {
 | 
			
		||||
            ui.label("Row: ");
 | 
			
		||||
            ui.add(
 | 
			
		||||
                egui::DragValue::new(&mut state.source_row_interleave)
 | 
			
		||||
                    .fixed_decimals(0)
 | 
			
		||||
                    .range(1..=30)
 | 
			
		||||
                    .speed(0.1),
 | 
			
		||||
            );
 | 
			
		||||
            ui.label("Col: ");
 | 
			
		||||
            ui.add(
 | 
			
		||||
                egui::DragValue::new(&mut state.source_column_interleave)
 | 
			
		||||
                    .fixed_decimals(0)
 | 
			
		||||
                    .range(1..=30)
 | 
			
		||||
                    .speed(0.1),
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ui.vertical(|ui| {
 | 
			
		||||
        ui.label("Destination Interleave");
 | 
			
		||||
        ui.horizontal(|ui| {
 | 
			
		||||
            ui.label("Row: ");
 | 
			
		||||
            ui.add(
 | 
			
		||||
                egui::DragValue::new(&mut state.destination_row_interleave)
 | 
			
		||||
                    .fixed_decimals(0)
 | 
			
		||||
                    .range(0..=30)
 | 
			
		||||
                    .speed(0.1),
 | 
			
		||||
            );
 | 
			
		||||
            ui.label("Col: ");
 | 
			
		||||
            ui.add(
 | 
			
		||||
                egui::DragValue::new(&mut state.destination_column_interleave)
 | 
			
		||||
                    .fixed_decimals(0)
 | 
			
		||||
                    .range(0..=30)
 | 
			
		||||
                    .speed(0.1),
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -3,8 +3,11 @@ pub mod plate;
 | 
			
		|||
pub mod plate_instances;
 | 
			
		||||
pub mod transfer;
 | 
			
		||||
pub mod transfer_region;
 | 
			
		||||
pub mod transfer_region_cache;
 | 
			
		||||
pub mod transfer_volume;
 | 
			
		||||
pub mod util;
 | 
			
		||||
 | 
			
		||||
mod well;
 | 
			
		||||
pub use well::Well;
 | 
			
		||||
 | 
			
		||||
pub use uuid;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,15 @@
 | 
			
		|||
use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
#[derive(PartialEq, Eq, Default, Clone, Copy, Serialize, Deserialize, Debug)]
 | 
			
		||||
pub const SOURCE_W96: Plate = Plate {
 | 
			
		||||
    plate_type: PlateType::Source,
 | 
			
		||||
    plate_format: PlateFormat::W96,
 | 
			
		||||
};
 | 
			
		||||
pub const DESTINATION_W96: Plate = Plate {
 | 
			
		||||
    plate_type: PlateType::Destination,
 | 
			
		||||
    plate_format: PlateFormat::W96,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#[derive(PartialEq, Eq, Default, Clone, Copy, Serialize, Deserialize, Debug, Hash)]
 | 
			
		||||
pub struct Plate {
 | 
			
		||||
    pub plate_type: PlateType,
 | 
			
		||||
    pub plate_format: PlateFormat,
 | 
			
		||||
| 
						 | 
				
			
			@ -19,7 +28,7 @@ impl Plate {
 | 
			
		|||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Debug)]
 | 
			
		||||
#[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Debug, Hash)]
 | 
			
		||||
pub enum PlateType {
 | 
			
		||||
    Source,
 | 
			
		||||
    Destination,
 | 
			
		||||
| 
						 | 
				
			
			@ -30,7 +39,7 @@ impl Default for PlateType {
 | 
			
		|||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Debug)]
 | 
			
		||||
#[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Debug, Hash)]
 | 
			
		||||
pub enum PlateFormat {
 | 
			
		||||
    W6,
 | 
			
		||||
    W12,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,16 @@
 | 
			
		|||
use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
use regex::Regex;
 | 
			
		||||
 | 
			
		||||
use super::plate::Plate;
 | 
			
		||||
use crate::plate::PlateType;
 | 
			
		||||
use crate::util;
 | 
			
		||||
use crate::Well;
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
 | 
			
		||||
use std::fmt;
 | 
			
		||||
use std::sync::LazyLock;
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug, Hash)]
 | 
			
		||||
pub struct CustomRegion {
 | 
			
		||||
    src: Vec<Well>,
 | 
			
		||||
    dest: Vec<Well>,
 | 
			
		||||
| 
						 | 
				
			
			@ -15,7 +22,7 @@ impl CustomRegion {
 | 
			
		|||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
 | 
			
		||||
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug, Hash)]
 | 
			
		||||
pub enum Region {
 | 
			
		||||
    Rect(Well, Well),
 | 
			
		||||
    Point(Well),
 | 
			
		||||
| 
						 | 
				
			
			@ -26,6 +33,28 @@ impl Default for Region {
 | 
			
		|||
        Region::Point(Well { row: 1, col: 1 })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
impl Display for Region {
 | 
			
		||||
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 | 
			
		||||
        match self {
 | 
			
		||||
            Region::Rect(ul, br) => {
 | 
			
		||||
                write!(
 | 
			
		||||
                    f,
 | 
			
		||||
                    "{}{}:{}{}",
 | 
			
		||||
                    util::num_to_letters(ul.col).unwrap(),
 | 
			
		||||
                    ul.row,
 | 
			
		||||
                    util::num_to_letters(br.col).unwrap(),
 | 
			
		||||
                    br.row
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            Region::Point(w) => {
 | 
			
		||||
                write!(f, "{}{}", util::num_to_letters(w.col).unwrap(), w.row)
 | 
			
		||||
            }
 | 
			
		||||
            Region::Custom(..) => {
 | 
			
		||||
                write!(f, "Custom Region")
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
impl TryFrom<Region> for (Well, Well) {
 | 
			
		||||
    type Error = &'static str;
 | 
			
		||||
    fn try_from(region: Region) -> Result<Self, Self::Error> {
 | 
			
		||||
| 
						 | 
				
			
			@ -53,9 +82,76 @@ impl Region {
 | 
			
		|||
            dest: dest_pts,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn new_from_wells(w1: Well, w2: Option<Well>) -> Self {
 | 
			
		||||
        if w2.is_none() {
 | 
			
		||||
            Self::Point(w1)
 | 
			
		||||
        } else {
 | 
			
		||||
            let w2 = w2.unwrap();
 | 
			
		||||
            let (w1, w2) = standardize_rectangle(&w1, &w2);
 | 
			
		||||
            Self::Rect(w1, w2)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn try_parse_str(input: &str) -> Option<Self> {
 | 
			
		||||
        static POINT_REGEX: LazyLock<Regex> =
 | 
			
		||||
            LazyLock::new(|| Regex::new(r"([A-Z,a-z]+)(\d+)").unwrap());
 | 
			
		||||
        static RECT_REGEX: LazyLock<Regex> =
 | 
			
		||||
            LazyLock::new(|| Regex::new(r"([A-Z,a-z]+)(\d+):([A-Z,a-z]+)(\d+)").unwrap());
 | 
			
		||||
 | 
			
		||||
        log::info!("{:?}", input);
 | 
			
		||||
        if let Some(captures) = RECT_REGEX.captures(input) {
 | 
			
		||||
            if let (Some(row1), Some(col1), Some(row2), Some(col2)) = (
 | 
			
		||||
                crate::util::letters_to_num(&captures[1]),
 | 
			
		||||
                captures[2].parse::<u8>().ok(),
 | 
			
		||||
                crate::util::letters_to_num(&captures[3]),
 | 
			
		||||
                captures[4].parse::<u8>().ok(),
 | 
			
		||||
            ) {
 | 
			
		||||
                return Some(Region::Rect(
 | 
			
		||||
                    Well {
 | 
			
		||||
                        row: row1,
 | 
			
		||||
                        col: col1,
 | 
			
		||||
                    },
 | 
			
		||||
                    Well {
 | 
			
		||||
                        row: row2,
 | 
			
		||||
                        col: col2,
 | 
			
		||||
                    },
 | 
			
		||||
                ));
 | 
			
		||||
            } else {
 | 
			
		||||
                return None;
 | 
			
		||||
            }
 | 
			
		||||
        } else if let Some(captures) = POINT_REGEX.captures(input) {
 | 
			
		||||
            if let (Some(row), Some(col)) = (
 | 
			
		||||
                crate::util::letters_to_num(&captures[1]),
 | 
			
		||||
                captures[2].parse::<u8>().ok(),
 | 
			
		||||
            ) {
 | 
			
		||||
                return Some(Region::Point(Well { row, col }));
 | 
			
		||||
            } else {
 | 
			
		||||
                return None;
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            None
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn contains_well(&self, w: &Well, pt: Option<PlateType>) -> bool {
 | 
			
		||||
        match self {
 | 
			
		||||
            Self::Point(x) => x == w,
 | 
			
		||||
            Self::Rect(ul, br) => {
 | 
			
		||||
                w.row <= br.row && w.row >= ul.row && w.col <= br.col && w.col >= ul.col
 | 
			
		||||
            }
 | 
			
		||||
            Self::Custom(xs) => match pt {
 | 
			
		||||
                Some(PlateType::Source) => xs.src.contains(w),
 | 
			
		||||
                Some(PlateType::Destination) => xs.dest.contains(w),
 | 
			
		||||
                None => {
 | 
			
		||||
                    unreachable!("Cannot check if custom contains well without knowing the type")
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(PartialEq, Eq, Clone, Serialize, Deserialize, Debug)]
 | 
			
		||||
#[derive(PartialEq, Eq, Clone, Serialize, Deserialize, Debug, Hash)]
 | 
			
		||||
pub struct TransferRegion {
 | 
			
		||||
    pub source_plate: Plate,
 | 
			
		||||
    pub source_region: Region, // Even if it is just a point, we don't want corners.
 | 
			
		||||
| 
						 | 
				
			
			@ -233,8 +329,10 @@ impl TransferRegion {
 | 
			
		|||
                        let j = j
 | 
			
		||||
                            .saturating_sub(s_ul.col)
 | 
			
		||||
                            .saturating_div(il_source.1.unsigned_abs());
 | 
			
		||||
                        let checked_il_dest = (u8::max(il_dest.0.unsigned_abs(), 1u8),
 | 
			
		||||
                                                u8::max(il_dest.1.unsigned_abs(), 1u8));
 | 
			
		||||
                        let checked_il_dest = (
 | 
			
		||||
                            u8::max(il_dest.0.unsigned_abs(), 1u8),
 | 
			
		||||
                            u8::max(il_dest.1.unsigned_abs(), 1u8),
 | 
			
		||||
                        );
 | 
			
		||||
                        let row_modulus = number_used_src_wells.0 * checked_il_dest.0;
 | 
			
		||||
                        let column_modulus = number_used_src_wells.1 * checked_il_dest.1;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -256,12 +354,12 @@ impl TransferRegion {
 | 
			
		|||
                                .filter(|Well { row: x, col: y }| {
 | 
			
		||||
                                    // How many times have we replicated? < How many are we allowed
 | 
			
		||||
                                    // to replicate?
 | 
			
		||||
                                    x.checked_sub(d_ul.row).unwrap().div_euclid(
 | 
			
		||||
                                        row_modulus
 | 
			
		||||
                                    ) < count.0
 | 
			
		||||
                                        && y.checked_sub(d_ul.col).unwrap().div_euclid(
 | 
			
		||||
                                            column_modulus
 | 
			
		||||
                                        ) < count.1
 | 
			
		||||
                                    x.checked_sub(d_ul.row).unwrap().div_euclid(row_modulus)
 | 
			
		||||
                                        < count.0
 | 
			
		||||
                                        && y.checked_sub(d_ul.col)
 | 
			
		||||
                                            .unwrap()
 | 
			
		||||
                                            .div_euclid(column_modulus)
 | 
			
		||||
                                            < count.1
 | 
			
		||||
                                })
 | 
			
		||||
                                .collect(),
 | 
			
		||||
                        )
 | 
			
		||||
| 
						 | 
				
			
			@ -319,8 +417,8 @@ impl TransferRegion {
 | 
			
		|||
                    return Err("Source region is out-of-bounds! (Too wide)");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if il_dest == (0,0) {
 | 
			
		||||
                    return Err("Refusing to pool both dimensions in a rectangular transfer!\nPlease select a point in the destination plate.")
 | 
			
		||||
                if il_dest == (0, 0) {
 | 
			
		||||
                    return Err("Refusing to pool both dimensions in a rectangular transfer!\nPlease select a point in the destination plate.");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            Region::Custom(_) => return Ok(()),
 | 
			
		||||
| 
						 | 
				
			
			@ -372,7 +470,7 @@ fn standardize_rectangle(c1: &Well, c2: &Well) -> (Well, Well) {
 | 
			
		|||
 | 
			
		||||
#[cfg(debug_assertions)]
 | 
			
		||||
use std::fmt;
 | 
			
		||||
use std::ops::Mul;
 | 
			
		||||
use std::{fmt::Display, ops::Mul};
 | 
			
		||||
 | 
			
		||||
#[cfg(debug_assertions)] // There should be no reason to print a transfer otherwise
 | 
			
		||||
impl fmt::Display for TransferRegion {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,95 @@
 | 
			
		|||
use std::collections::HashMap;
 | 
			
		||||
use std::sync::{Arc, Mutex};
 | 
			
		||||
 | 
			
		||||
use crate::transfer::Transfer;
 | 
			
		||||
use crate::Well;
 | 
			
		||||
 | 
			
		||||
use uuid::Uuid;
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub struct TransferRegionCache {
 | 
			
		||||
    interior: Arc<Mutex<TransferRegionCacheInterior>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl TransferRegionCache {
 | 
			
		||||
    pub fn new() -> Self {
 | 
			
		||||
        TransferRegionCache {
 | 
			
		||||
            interior: Arc::new(Mutex::new(TransferRegionCacheInterior::new()))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn add_overwrite(&self, tr: &Transfer) {
 | 
			
		||||
        let source = tr.transfer_region.get_source_wells();
 | 
			
		||||
        let destination = tr.transfer_region.get_destination_wells();
 | 
			
		||||
        let uuid = tr.id;
 | 
			
		||||
 | 
			
		||||
        if let Ok(mut interior) = self.interior.lock() {
 | 
			
		||||
            interior.cache.insert(uuid, InteriorWellSlices {
 | 
			
		||||
                source: source.into(),
 | 
			
		||||
                destination: destination.into(),
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn generate_cache(&self, trs: &[Transfer]) {
 | 
			
		||||
        for tr in trs.iter() {
 | 
			
		||||
            self.add_overwrite(tr);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn invalidate(&self, tr: &Transfer) {
 | 
			
		||||
        if let Ok(mut interior) = self.interior.lock() {
 | 
			
		||||
            interior.cache.remove(&tr.id);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn get_source(&self, tr: &Transfer) -> Option<Arc<[Well]>> {
 | 
			
		||||
        self.get(tr, true)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn get_destination(&self, tr: &Transfer) -> Option<Arc<[Well]>> {
 | 
			
		||||
        self.get(tr, false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn get(&self, tr: &Transfer, is_source: bool) -> Option<Arc<[Well]>> {
 | 
			
		||||
        if let Ok(interior) = self.interior.lock() {
 | 
			
		||||
            interior.cache.get(&tr.id).map(|x| 
 | 
			
		||||
                if is_source {
 | 
			
		||||
                    x.source.clone()
 | 
			
		||||
                } else {
 | 
			
		||||
                    x.destination.clone()
 | 
			
		||||
                })
 | 
			
		||||
        } else {
 | 
			
		||||
            None
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Clone for TransferRegionCache {
 | 
			
		||||
    fn clone(&self) -> Self {
 | 
			
		||||
        // Clone the interior RC without letting anyone know
 | 
			
		||||
        // shhhh!
 | 
			
		||||
        TransferRegionCache {
 | 
			
		||||
            interior: self.interior.clone(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
struct TransferRegionCacheInterior {
 | 
			
		||||
    cache: HashMap<Uuid, InteriorWellSlices>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl TransferRegionCacheInterior {
 | 
			
		||||
    fn new() -> Self {
 | 
			
		||||
        TransferRegionCacheInterior {
 | 
			
		||||
            cache: HashMap::new()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
struct InteriorWellSlices {
 | 
			
		||||
    source: Arc<[Well]>,
 | 
			
		||||
    destination: Arc<[Well]>,
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -30,6 +30,89 @@ pub fn num_to_letters(num: u8) -> Option<String> {
 | 
			
		|||
    Some(text.to_string())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//
 | 
			
		||||
// Palettes moved from pt-web
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
// Sources:
 | 
			
		||||
// https://iquilezles.org/articles/palettes/
 | 
			
		||||
// http://dev.thi.ng/gradients/
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Copy, PartialEq, Debug)]
 | 
			
		||||
pub struct ColorPalette {
 | 
			
		||||
    a: [f64; 3],
 | 
			
		||||
    b: [f64; 3],
 | 
			
		||||
    c: [f64; 3],
 | 
			
		||||
    d: [f64; 3],
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ColorPalette {
 | 
			
		||||
    pub fn _new(a: [f64; 3], b: [f64; 3], c: [f64; 3], d: [f64; 3]) -> Self {
 | 
			
		||||
        ColorPalette { a, b, c, d }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn get(&self, t: f64) -> [f64; 3] {
 | 
			
		||||
        [
 | 
			
		||||
            (self.a[0] + self.b[0] * f64::cos(std::f64::consts::TAU * (self.c[0] * t + self.d[0])))
 | 
			
		||||
                * 255.0,
 | 
			
		||||
            (self.a[1] + self.b[1] * f64::cos(std::f64::consts::TAU * (self.c[1] * t + self.d[1])))
 | 
			
		||||
                * 255.0,
 | 
			
		||||
            (self.a[2] + self.b[2] * f64::cos(std::f64::consts::TAU * (self.c[2] * t + self.d[2])))
 | 
			
		||||
                * 255.0,
 | 
			
		||||
        ]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[allow(dead_code)] // Preserve old implementation for reference
 | 
			
		||||
    fn get_u8(&self, t: u8) -> [f64; 3] {
 | 
			
		||||
        assert!(t > 0, "t must be greater than zero!");
 | 
			
		||||
        self.get((2f64.powi(-(t.ilog2() as i32))) * (t as f64 + 0.5f64) - 1.0f64)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // pub fn get_uuid(&self, t: uuid::Uuid) -> [f64; 3] {
 | 
			
		||||
    //     // self.get(t.as_u128() as f64 / (u128::MAX) as f64)
 | 
			
		||||
    //     let mut r = SmallRng::seed_from_u64(t.as_u128() as u64);
 | 
			
		||||
    //     self.get(r.gen_range(0.0..1.0f64))
 | 
			
		||||
    // }
 | 
			
		||||
 | 
			
		||||
    pub fn get_ordered(&self, t: uuid::Uuid, ordered_uuids: &[uuid::Uuid])
 | 
			
		||||
        -> [f64; 3] {
 | 
			
		||||
        let index = ordered_uuids.iter().position(|&x| x == t).expect("uuid must be in list of uuids") + 1;
 | 
			
		||||
        self.get(Self::space_evenly(index))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn get_linear(&self, t: f64, max: f64) -> [f64; 3] {
 | 
			
		||||
        let scaled = t / max;
 | 
			
		||||
        self.get(scaled)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn space_evenly(x: usize) -> f64 {
 | 
			
		||||
        let e: usize = (x.ilog2() + 1) as usize;
 | 
			
		||||
        let d: usize = 2usize.pow(e as u32);
 | 
			
		||||
        let n: usize = (2*x + 1) % d;
 | 
			
		||||
        (n as f64) / (d as f64)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[non_exhaustive]
 | 
			
		||||
pub struct Palettes;
 | 
			
		||||
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
impl Palettes {
 | 
			
		||||
    pub const RAINBOW: ColorPalette = ColorPalette {
 | 
			
		||||
        a: [0.500, 0.500, 0.500],
 | 
			
		||||
        b: [0.700, 0.700, 0.700],
 | 
			
		||||
        c: [0.800, 0.800, 0.800],
 | 
			
		||||
        d: [0.000, 0.333, 0.667],
 | 
			
		||||
    };
 | 
			
		||||
    pub const YELLOW_PINK: ColorPalette = ColorPalette {
 | 
			
		||||
        a: [0.500, 0.500, 0.320],
 | 
			
		||||
        b: [0.500, 0.500, 0.500],
 | 
			
		||||
        c: [0.100, 0.500, 0.360],
 | 
			
		||||
        d: [0.000, 0.000, 0.650],
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod tests {
 | 
			
		||||
    use super::{letters_to_num, num_to_letters};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue