Compare commits
	
		
			25 Commits
		
	
	
		
			beta-relea
			...
			feature/ef
		
	
	| Author | SHA1 | Date | 
|---|---|---|
| 
							
							
								
									
								
								 | 
						5e70a17a00 | |
| 
							
							
								
									
								
								 | 
						7295115506 | |
| 
							
							
								
									
								
								 | 
						4a9bec7d5d | |
| 
							
							
								
									
								
								 | 
						418bf4a79e | |
| 
							
							
								
									
								
								 | 
						e6d80ab8a3 | |
| 
							
							
								
									
								
								 | 
						1d2028959d | |
| 
							
							
								
									
								
								 | 
						26526bb249 | |
| 
							
							
								
									
								
								 | 
						fcbfa6e544 | |
| 
							
							
								
									
								
								 | 
						ff60439dd5 | |
| 
							
							
								
									
								
								 | 
						619f9594cf | |
| 
							
							
								
									
								
								 | 
						2ea2963b22 | |
| 
							
							
								
									
								
								 | 
						949822f26c | |
| 
							
							
								
									
								
								 | 
						453ad9ed35 | |
| 
							
							
								
									
								
								 | 
						d296854580 | |
| 
							
							
								
									
								
								 | 
						ce0e1f2743 | |
| 
							
							
								
									
								
								 | 
						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]
 | 
					[workspace]
 | 
				
			||||||
members = ["plate-tool-web", "plate-tool-lib"]
 | 
					members = ["plate-tool-web", "plate-tool-lib", "plate-tool-eframe"]
 | 
				
			||||||
resolver = "2"
 | 
					resolver = "2"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,2 @@
 | 
				
			||||||
 | 
					[build]
 | 
				
			||||||
 | 
					target = "x86_64-unknown-linux-gnu"
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,2 @@
 | 
				
			||||||
 | 
					/target
 | 
				
			||||||
 | 
					/dist
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,29 @@
 | 
				
			||||||
 | 
					[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",
 | 
				
			||||||
 | 
						"persistence",
 | 
				
			||||||
 | 
					]}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					log = "0.4"
 | 
				
			||||||
 | 
					serde = { version = "1.0", features = ["derive"] }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
 | 
				
			||||||
 | 
					env_logger = "0.11"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[target.'cfg(target_arch = "wasm32")'.dependencies]
 | 
				
			||||||
 | 
					wasm-bindgen-futures = "0.4"
 | 
				
			||||||
 | 
					web-sys = "0.3"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[profile.release]
 | 
				
			||||||
 | 
					#opt-level = 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[profile.dev.package."*"]
 | 
				
			||||||
 | 
					#opt-level = 2
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					[build]
 | 
				
			||||||
 | 
					target = "index.html"
 | 
				
			||||||
 | 
					release = true
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,53 @@
 | 
				
			||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html>
 | 
				
			||||||
 | 
					    <head>
 | 
				
			||||||
 | 
					        <meta charset="utf-8" />
 | 
				
			||||||
 | 
					        <link data-trunk rel="rust" data-bin="plate-tool-eframe" />
 | 
				
			||||||
 | 
					        <title>Plate Tool</title>
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					html {
 | 
				
			||||||
 | 
					            /* Remove touch delay: */
 | 
				
			||||||
 | 
					            touch-action: manipulation;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        body {
 | 
				
			||||||
 | 
					            /* Light mode background color for what is not covered by the egui canvas,
 | 
				
			||||||
 | 
					            or where the egui canvas is translucent. */
 | 
				
			||||||
 | 
					            background: #909090;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        @media (prefers-color-scheme: dark) {
 | 
				
			||||||
 | 
					            body {
 | 
				
			||||||
 | 
					                /* Dark mode background color for what is not covered by the egui canvas,
 | 
				
			||||||
 | 
					                or where the egui canvas is translucent. */
 | 
				
			||||||
 | 
					                background: #404040;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /* Allow canvas to fill entire web page: */
 | 
				
			||||||
 | 
					        html,
 | 
				
			||||||
 | 
					        body {
 | 
				
			||||||
 | 
					            overflow: hidden;
 | 
				
			||||||
 | 
					            margin: 0 !important;
 | 
				
			||||||
 | 
					            padding: 0 !important;
 | 
				
			||||||
 | 
					            height: 100%;
 | 
				
			||||||
 | 
					            width: 100%;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /* Make canvas fill entire document: */
 | 
				
			||||||
 | 
					        canvas {
 | 
				
			||||||
 | 
					            margin-right: auto;
 | 
				
			||||||
 | 
					            margin-left: auto;
 | 
				
			||||||
 | 
					            display: block;
 | 
				
			||||||
 | 
					            position: absolute;
 | 
				
			||||||
 | 
					            top: 0;
 | 
				
			||||||
 | 
					            left: 0;
 | 
				
			||||||
 | 
					            width: 100%;
 | 
				
			||||||
 | 
					            height: 100%;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
 | 
					    </head>
 | 
				
			||||||
 | 
					    <body>
 | 
				
			||||||
 | 
					        <canvas id="main_canvas"></canvas>
 | 
				
			||||||
 | 
					    </body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,261 @@
 | 
				
			||||||
 | 
					use std::ops::DerefMut;
 | 
				
			||||||
 | 
					use std::sync::Mutex;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use eframe::egui::{self};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::main_state::{construct_fake_mainstate, MainState};
 | 
				
			||||||
 | 
					use crate::modals::{self, ModalState};
 | 
				
			||||||
 | 
					use crate::plate::{add_plate, PlateDisplayOptions, PlateUiState};
 | 
				
			||||||
 | 
					use crate::transfer_menu::{transfer_menu, CurrentTransferState, TransferMenuState};
 | 
				
			||||||
 | 
					use crate::tree::tree;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[non_exhaustive]
 | 
				
			||||||
 | 
					#[derive(Debug, serde::Serialize, serde::Deserialize)]
 | 
				
			||||||
 | 
					struct MainWindowState {
 | 
				
			||||||
 | 
					    show_side_panel: bool,
 | 
				
			||||||
 | 
					    plate_display_options: PlateDisplayOptions,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Default for MainWindowState {
 | 
				
			||||||
 | 
					    fn default() -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            show_side_panel: true,
 | 
				
			||||||
 | 
					            plate_display_options: PlateDisplayOptions::default(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[non_exhaustive]
 | 
				
			||||||
 | 
					#[derive(Debug, serde::Serialize, serde::Deserialize)]
 | 
				
			||||||
 | 
					pub struct PlateToolEframe {
 | 
				
			||||||
 | 
					    source_plate_state: Mutex<PlateUiState>,
 | 
				
			||||||
 | 
					    destination_plate_state: Mutex<PlateUiState>,
 | 
				
			||||||
 | 
					    main_window_state: MainWindowState,
 | 
				
			||||||
 | 
					    current_transfer_state: CurrentTransferState,
 | 
				
			||||||
 | 
					    #[serde(skip)]
 | 
				
			||||||
 | 
					    modal_state: ModalState,
 | 
				
			||||||
 | 
					    #[serde(skip)]
 | 
				
			||||||
 | 
					    transfer_menu_state: TransferMenuState,
 | 
				
			||||||
 | 
					    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(),
 | 
				
			||||||
 | 
					            modal_state: ModalState::default(),
 | 
				
			||||||
 | 
					            transfer_menu_state: TransferMenuState::default(),
 | 
				
			||||||
 | 
					            main_state: construct_fake_mainstate(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl PlateToolEframe {
 | 
				
			||||||
 | 
					    pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
 | 
				
			||||||
 | 
					        // Would load state here
 | 
				
			||||||
 | 
					        if let Some(storage) = cc.storage {
 | 
				
			||||||
 | 
					            let out: PlateToolEframe =
 | 
				
			||||||
 | 
					                eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default();
 | 
				
			||||||
 | 
					            return out;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            let pte: PlateToolEframe = Default::default();
 | 
				
			||||||
 | 
					            pte.main_state
 | 
				
			||||||
 | 
					                .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) {
 | 
				
			||||||
 | 
					        log::info!("Saving state");
 | 
				
			||||||
 | 
					        eframe::set_value(storage, eframe::APP_KEY, self);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn update(&mut self, ctx: &eframe::egui::Context, _frame: &mut eframe::Frame) {
 | 
				
			||||||
 | 
					        crate::styling::set_visuals(&ctx);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
 | 
				
			||||||
 | 
					            egui::menu::bar(ui, |ui| {
 | 
				
			||||||
 | 
					                ui.menu_button("File", |ui| {
 | 
				
			||||||
 | 
					                    if ui.button("New Plate").clicked() {
 | 
				
			||||||
 | 
					                        crate::modals::open_new_plate_modal(&mut self.modal_state);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    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() {}
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                    if ui.button("Reset All").clicked() {
 | 
				
			||||||
 | 
					                        self.main_state = MainState::default();
 | 
				
			||||||
 | 
					                        self.current_transfer_state = CurrentTransferState::default();
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    if ui.button("Dump State").clicked() {
 | 
				
			||||||
 | 
					                        log::warn!("{:?}\n{:?}", self.main_state, self.current_transfer_state);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					                ui.menu_button("Options", |ui| {
 | 
				
			||||||
 | 
					                    ui.menu_button("Styles", |ui| {
 | 
				
			||||||
 | 
					                        ui.toggle_value(
 | 
				
			||||||
 | 
					                            &mut self
 | 
				
			||||||
 | 
					                                .main_window_state
 | 
				
			||||||
 | 
					                                .plate_display_options
 | 
				
			||||||
 | 
					                                .show_transfer_hashes,
 | 
				
			||||||
 | 
					                            "Toggle transfer hashes",
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
 | 
					                        ui.toggle_value(
 | 
				
			||||||
 | 
					                            &mut self
 | 
				
			||||||
 | 
					                                .main_window_state
 | 
				
			||||||
 | 
					                                .plate_display_options
 | 
				
			||||||
 | 
					                                .show_volume_heatmap,
 | 
				
			||||||
 | 
					                            "Toggle volume heatmap",
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
 | 
					                        ui.toggle_value(
 | 
				
			||||||
 | 
					                            &mut self
 | 
				
			||||||
 | 
					                                .main_window_state
 | 
				
			||||||
 | 
					                                .plate_display_options
 | 
				
			||||||
 | 
					                                .show_coordinates,
 | 
				
			||||||
 | 
					                            "Toggle coordinate highlighting",
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                    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| {
 | 
				
			||||||
 | 
					                    tree(
 | 
				
			||||||
 | 
					                        ui,
 | 
				
			||||||
 | 
					                        &mut self.main_state,
 | 
				
			||||||
 | 
					                        &self.current_transfer_state,
 | 
				
			||||||
 | 
					                        &mut self.modal_state,
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                    ui.separator();
 | 
				
			||||||
 | 
					                    transfer_menu(
 | 
				
			||||||
 | 
					                        ui,
 | 
				
			||||||
 | 
					                        &self.current_transfer_state,
 | 
				
			||||||
 | 
					                        &mut self.transfer_menu_state,
 | 
				
			||||||
 | 
					                        &mut self.main_state,
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // This MUST happen after the menu calls as they can mutate main state
 | 
				
			||||||
 | 
					        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::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
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					                if let Some(source_pi) = self.main_state.get_current_source_plateinstance() {
 | 
				
			||||||
 | 
					                    add_plate(
 | 
				
			||||||
 | 
					                        half_height,
 | 
				
			||||||
 | 
					                        source_pi.plate.plate_format,
 | 
				
			||||||
 | 
					                        self.main_state.get_current_source_transfers(),
 | 
				
			||||||
 | 
					                        plate_tool_lib::plate::PlateType::Source,
 | 
				
			||||||
 | 
					                        &ordered_ids,
 | 
				
			||||||
 | 
					                        &self.main_state.transfer_region_cache,
 | 
				
			||||||
 | 
					                        Some(&self.current_transfer_state),
 | 
				
			||||||
 | 
					                        ui,
 | 
				
			||||||
 | 
					                        self.source_plate_state.lock().unwrap().deref_mut(),
 | 
				
			||||||
 | 
					                        self.main_window_state.plate_display_options,
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                if let Some(destination_pi) =
 | 
				
			||||||
 | 
					                    self.main_state.get_current_destination_plateinstance()
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    add_plate(
 | 
				
			||||||
 | 
					                        half_height,
 | 
				
			||||||
 | 
					                        destination_pi.plate.plate_format,
 | 
				
			||||||
 | 
					                        self.main_state.get_current_destination_transfers(),
 | 
				
			||||||
 | 
					                        plate_tool_lib::plate::PlateType::Destination,
 | 
				
			||||||
 | 
					                        &ordered_ids,
 | 
				
			||||||
 | 
					                        &self.main_state.transfer_region_cache,
 | 
				
			||||||
 | 
					                        Some(&self.current_transfer_state),
 | 
				
			||||||
 | 
					                        ui,
 | 
				
			||||||
 | 
					                        self.destination_plate_state.lock().unwrap().deref_mut(),
 | 
				
			||||||
 | 
					                        self.main_window_state.plate_display_options,
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Modal processing
 | 
				
			||||||
 | 
					        modals::show_modal_if_open(ctx, &mut self.modal_state);
 | 
				
			||||||
 | 
					        match &self.modal_state {
 | 
				
			||||||
 | 
					            modals::ModalState::NewPlateComplete(modals::NewPlateModalComplete(Some(pi))) => {
 | 
				
			||||||
 | 
					                let plate_type = pi.plate.plate_type;
 | 
				
			||||||
 | 
					                match plate_type {
 | 
				
			||||||
 | 
					                    plate_tool_lib::plate::PlateType::Source => {
 | 
				
			||||||
 | 
					                        self.main_state.source_plates.push(pi.clone());
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    plate_tool_lib::plate::PlateType::Destination => {
 | 
				
			||||||
 | 
					                        self.main_state.destination_plates.push(pi.clone());
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                self.modal_state = modals::ModalState::None;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            modals::ModalState::EditPlateComplete(modals::EditPlateModalStateComplete(Some(x))) => {
 | 
				
			||||||
 | 
					                let pi = {
 | 
				
			||||||
 | 
					                    match x.plate_type {
 | 
				
			||||||
 | 
					                        plate_tool_lib::plate::PlateType::Source => self
 | 
				
			||||||
 | 
					                            .main_state
 | 
				
			||||||
 | 
					                            .source_plates
 | 
				
			||||||
 | 
					                            .iter_mut()
 | 
				
			||||||
 | 
					                            .find(|y| y.get_uuid() == x.id),
 | 
				
			||||||
 | 
					                        plate_tool_lib::plate::PlateType::Destination => self
 | 
				
			||||||
 | 
					                            .main_state
 | 
				
			||||||
 | 
					                            .destination_plates
 | 
				
			||||||
 | 
					                            .iter_mut()
 | 
				
			||||||
 | 
					                            .find(|y| y.get_uuid() == x.id),
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					                if let Some(pi) = pi {
 | 
				
			||||||
 | 
					                    pi.name = x.name.to_owned();
 | 
				
			||||||
 | 
					                    pi.change_format(&x.plate_format);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                self.modal_state = modals::ModalState::None;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            ModalState::NewPlateComplete(modals::NewPlateModalComplete(None)) => {
 | 
				
			||||||
 | 
					                self.modal_state = modals::ModalState::None;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            ModalState::EditPlateComplete(modals::EditPlateModalStateComplete(None)) => {
 | 
				
			||||||
 | 
					                self.modal_state = modals::ModalState::None;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            _ => (),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,8 @@
 | 
				
			||||||
 | 
					mod app;
 | 
				
			||||||
 | 
					mod plate;
 | 
				
			||||||
 | 
					mod tree;
 | 
				
			||||||
 | 
					mod transfer_menu;
 | 
				
			||||||
 | 
					mod main_state;
 | 
				
			||||||
 | 
					mod modals;
 | 
				
			||||||
 | 
					mod styling;
 | 
				
			||||||
 | 
					pub use app::PlateToolEframe;
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,48 @@
 | 
				
			||||||
 | 
					use eframe::egui;
 | 
				
			||||||
 | 
					use eframe::*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(not(target_arch = "wasm32"))]
 | 
				
			||||||
 | 
					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)))),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(target_arch = "wasm32")]
 | 
				
			||||||
 | 
					fn main() {
 | 
				
			||||||
 | 
					    use eframe::wasm_bindgen::JsCast as _;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    eframe::WebLogger::init(log::LevelFilter::Info).ok();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let web_options = eframe::WebOptions::default();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    wasm_bindgen_futures::spawn_local(async {
 | 
				
			||||||
 | 
					        let document = web_sys::window()
 | 
				
			||||||
 | 
					            .expect("No window")
 | 
				
			||||||
 | 
					            .document()
 | 
				
			||||||
 | 
					            .expect("No document");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let canvas = document
 | 
				
			||||||
 | 
					            .get_element_by_id("main_canvas")
 | 
				
			||||||
 | 
					            .expect("Canvas id not found")
 | 
				
			||||||
 | 
					            .dyn_into::<web_sys::HtmlCanvasElement>()
 | 
				
			||||||
 | 
					            .expect("Canvas was not a HtmlCanvasElement");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let start_result = eframe::WebRunner::new()
 | 
				
			||||||
 | 
					            .start(
 | 
				
			||||||
 | 
					                canvas,
 | 
				
			||||||
 | 
					                web_options,
 | 
				
			||||||
 | 
					                Box::new(|cc| Ok(Box::new(plate_tool_eframe::PlateToolEframe::new(cc)))),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .await;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,248 @@
 | 
				
			||||||
 | 
					use core::fmt;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use plate_tool_lib::plate::PlateFormat;
 | 
				
			||||||
 | 
					use plate_tool_lib::plate_instances::{self, PlateInstance};
 | 
				
			||||||
 | 
					use plate_tool_lib::transfer_region_cache::TransferRegionCache;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Default, serde::Serialize, serde::Deserialize)]
 | 
				
			||||||
 | 
					pub struct MainState {
 | 
				
			||||||
 | 
					    /// Stores all plates and transfers
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// It is not guaranteed that the current_* variables will refer
 | 
				
			||||||
 | 
					    /// to UUIDs that exist in the lists, only that they did at one time.
 | 
				
			||||||
 | 
					    /// This can happen if a plate or transfer is removed and set_* is not called.
 | 
				
			||||||
 | 
					    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>,
 | 
				
			||||||
 | 
					    current_source: Option<plate_tool_lib::uuid::Uuid>,
 | 
				
			||||||
 | 
					    current_destination: Option<plate_tool_lib::uuid::Uuid>,
 | 
				
			||||||
 | 
					    current_transfer: Option<plate_tool_lib::uuid::Uuid>,
 | 
				
			||||||
 | 
					    #[serde(skip)]
 | 
				
			||||||
 | 
					    pub transfer_region_cache: plate_tool_lib::transfer_region_cache::TransferRegionCache,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					impl MainState {
 | 
				
			||||||
 | 
					    pub fn get_current_source_uuid(&self) -> Option<plate_tool_lib::uuid::Uuid> {
 | 
				
			||||||
 | 
					        self.current_source
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    pub fn get_current_source_plateinstance(
 | 
				
			||||||
 | 
					        &self,
 | 
				
			||||||
 | 
					    ) -> Option<&plate_tool_lib::plate_instances::PlateInstance> {
 | 
				
			||||||
 | 
					        if let Some(id) = self.current_source {
 | 
				
			||||||
 | 
					            self.source_plates.iter().find(|x| x.get_uuid() == id)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            None
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn get_current_destination_uuid(&self) -> Option<plate_tool_lib::uuid::Uuid> {
 | 
				
			||||||
 | 
					        self.current_destination
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    pub fn get_current_destination_plateinstance(
 | 
				
			||||||
 | 
					        &self,
 | 
				
			||||||
 | 
					    ) -> Option<&plate_tool_lib::plate_instances::PlateInstance> {
 | 
				
			||||||
 | 
					        if let Some(id) = self.current_destination {
 | 
				
			||||||
 | 
					            self.destination_plates.iter().find(|x| x.get_uuid() == id)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            None
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn get_current_transfer_uuid(&self) -> Option<plate_tool_lib::uuid::Uuid> {
 | 
				
			||||||
 | 
					        self.current_transfer
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    pub fn get_current_transfer_info(&self) -> Option<&plate_tool_lib::transfer::Transfer> {
 | 
				
			||||||
 | 
					        if let Some(id) = self.current_transfer {
 | 
				
			||||||
 | 
					            self.transfers.iter().find(|x| x.get_uuid() == id)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            None
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    pub fn get_current_source_transfers(&self) -> Option<Vec<&plate_tool_lib::transfer::Transfer>> {
 | 
				
			||||||
 | 
					        let source_uuid = self.get_current_source_uuid();
 | 
				
			||||||
 | 
					        if let Some(source_uuid) = source_uuid {
 | 
				
			||||||
 | 
					            Some(
 | 
				
			||||||
 | 
					                self.transfers
 | 
				
			||||||
 | 
					                    .iter()
 | 
				
			||||||
 | 
					                    .filter(|tr| tr.source_id == source_uuid)
 | 
				
			||||||
 | 
					                    .collect(),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            None
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    pub fn get_current_destination_transfers(&self) -> Option<Vec<&plate_tool_lib::transfer::Transfer>> {
 | 
				
			||||||
 | 
					        let destination_uuid = self.get_current_destination_uuid();
 | 
				
			||||||
 | 
					        if let Some(destination_uuid) = destination_uuid {
 | 
				
			||||||
 | 
					            Some(
 | 
				
			||||||
 | 
					                self.transfers
 | 
				
			||||||
 | 
					                    .iter()
 | 
				
			||||||
 | 
					                    .filter(|tr| tr.dest_id == destination_uuid)
 | 
				
			||||||
 | 
					                    .collect(),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            None
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn set_current_source(&mut self, id: plate_tool_lib::uuid::Uuid) -> bool {
 | 
				
			||||||
 | 
					        if self.check_source_exists(id) {
 | 
				
			||||||
 | 
					            self.current_source = Some(id);
 | 
				
			||||||
 | 
					            self.set_no_current_transfer();
 | 
				
			||||||
 | 
					            true
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            false
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    pub fn set_current_destination(&mut self, id: plate_tool_lib::uuid::Uuid) -> bool {
 | 
				
			||||||
 | 
					        if self.check_destination_exists(id) {
 | 
				
			||||||
 | 
					            self.current_destination = Some(id);
 | 
				
			||||||
 | 
					            self.set_no_current_transfer();
 | 
				
			||||||
 | 
					            true
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            false
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    pub fn set_current_transfer(&mut self, id: plate_tool_lib::uuid::Uuid) -> bool {
 | 
				
			||||||
 | 
					        if let Some(tr) = self.transfers.iter().find(|x| x.get_uuid() == id) {
 | 
				
			||||||
 | 
					            let source_exists = self.check_source_exists(tr.source_id);
 | 
				
			||||||
 | 
					            let destination_exists = self.check_destination_exists(tr.dest_id);
 | 
				
			||||||
 | 
					            if source_exists && destination_exists {
 | 
				
			||||||
 | 
					                self.current_transfer = Some(id);
 | 
				
			||||||
 | 
					                self.current_source = Some(tr.source_id);
 | 
				
			||||||
 | 
					                self.current_destination = Some(tr.dest_id);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            true
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            false
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    pub fn set_no_current_source(&mut self) {
 | 
				
			||||||
 | 
					        self.current_source = None;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    pub fn set_no_current_destination(&mut self) {
 | 
				
			||||||
 | 
					        self.current_destination = None;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    pub fn set_no_current_transfer(&mut self) {
 | 
				
			||||||
 | 
					        self.current_transfer = None;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn check_source_exists(&self, id: plate_tool_lib::uuid::Uuid) -> bool {
 | 
				
			||||||
 | 
					        self.source_plates
 | 
				
			||||||
 | 
					            .iter()
 | 
				
			||||||
 | 
					            .map(|x| x.get_uuid())
 | 
				
			||||||
 | 
					            .find(|x| *x == id)
 | 
				
			||||||
 | 
					            .is_some()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    pub fn get_source_by_uuid(&self, id: plate_tool_lib::uuid::Uuid) -> Option<&PlateInstance> {
 | 
				
			||||||
 | 
					        self.source_plates
 | 
				
			||||||
 | 
					            .iter()
 | 
				
			||||||
 | 
					            .find(|x| x.get_uuid() == id)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn check_destination_exists(&self, id: plate_tool_lib::uuid::Uuid) -> bool {
 | 
				
			||||||
 | 
					        self.destination_plates
 | 
				
			||||||
 | 
					            .iter()
 | 
				
			||||||
 | 
					            .map(|x| x.get_uuid())
 | 
				
			||||||
 | 
					            .find(|x| *x == id)
 | 
				
			||||||
 | 
					            .is_some()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    pub fn get_destination_by_uuid(&self, id: plate_tool_lib::uuid::Uuid) -> Option<&PlateInstance> {
 | 
				
			||||||
 | 
					        self.destination_plates
 | 
				
			||||||
 | 
					            .iter()
 | 
				
			||||||
 | 
					            .find(|x| x.get_uuid() == id)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					impl fmt::Debug for MainState {
 | 
				
			||||||
 | 
					    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
 | 
				
			||||||
 | 
					        #[derive(Debug)]
 | 
				
			||||||
 | 
					        #[allow(dead_code)]
 | 
				
			||||||
 | 
					        struct SelectedFields<'a> {
 | 
				
			||||||
 | 
					            pub source_plates: &'a Vec<plate_tool_lib::plate_instances::PlateInstance>,
 | 
				
			||||||
 | 
					            pub destination_plates: &'a Vec<plate_tool_lib::plate_instances::PlateInstance>,
 | 
				
			||||||
 | 
					            pub transfers: &'a Vec<plate_tool_lib::transfer::Transfer>,
 | 
				
			||||||
 | 
					            current_source: &'a Option<plate_tool_lib::uuid::Uuid>,
 | 
				
			||||||
 | 
					            current_destination: &'a Option<plate_tool_lib::uuid::Uuid>,
 | 
				
			||||||
 | 
					            current_transfer: &'a Option<plate_tool_lib::uuid::Uuid>,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        let Self {
 | 
				
			||||||
 | 
					            source_plates,
 | 
				
			||||||
 | 
					            destination_plates,
 | 
				
			||||||
 | 
					            transfers,
 | 
				
			||||||
 | 
					            current_source,
 | 
				
			||||||
 | 
					            current_destination,
 | 
				
			||||||
 | 
					            current_transfer,
 | 
				
			||||||
 | 
					            transfer_region_cache: _,
 | 
				
			||||||
 | 
					        } = self;
 | 
				
			||||||
 | 
					        fmt::Debug::fmt(
 | 
				
			||||||
 | 
					            &SelectedFields {
 | 
				
			||||||
 | 
					                source_plates,
 | 
				
			||||||
 | 
					                destination_plates,
 | 
				
			||||||
 | 
					                transfers,
 | 
				
			||||||
 | 
					                current_source,
 | 
				
			||||||
 | 
					                current_destination,
 | 
				
			||||||
 | 
					                current_transfer,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            f,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub 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],
 | 
				
			||||||
 | 
					        current_source: None,
 | 
				
			||||||
 | 
					        current_destination: None,
 | 
				
			||||||
 | 
					        current_transfer: None,
 | 
				
			||||||
 | 
					        transfer_region_cache: TransferRegionCache::default(),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,235 @@
 | 
				
			||||||
 | 
					use eframe::egui;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[non_exhaustive]
 | 
				
			||||||
 | 
					#[derive(Debug)]
 | 
				
			||||||
 | 
					pub enum ModalState {
 | 
				
			||||||
 | 
					    NewPlate(NewPlateModalState),
 | 
				
			||||||
 | 
					    NewPlateComplete(NewPlateModalComplete),
 | 
				
			||||||
 | 
					    EditPlate(EditPlateModalState),
 | 
				
			||||||
 | 
					    EditPlateComplete(EditPlateModalStateComplete),
 | 
				
			||||||
 | 
					    None,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					impl Default for ModalState {
 | 
				
			||||||
 | 
					    fn default() -> Self {
 | 
				
			||||||
 | 
					        Self::None
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[non_exhaustive]
 | 
				
			||||||
 | 
					#[derive(Debug)]
 | 
				
			||||||
 | 
					pub struct NewPlateModalState {
 | 
				
			||||||
 | 
					    pub name: String,
 | 
				
			||||||
 | 
					    pub plate_type: plate_tool_lib::plate::PlateType,
 | 
				
			||||||
 | 
					    pub plate_format: plate_tool_lib::plate::PlateFormat,
 | 
				
			||||||
 | 
					    window_open: bool,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					impl Default for NewPlateModalState {
 | 
				
			||||||
 | 
					    fn default() -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            name: String::default(),
 | 
				
			||||||
 | 
					            plate_type: plate_tool_lib::plate::PlateType::default(),
 | 
				
			||||||
 | 
					            plate_format: plate_tool_lib::plate::PlateFormat::default(),
 | 
				
			||||||
 | 
					            window_open: true,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[non_exhaustive]
 | 
				
			||||||
 | 
					#[derive(Debug, Default)]
 | 
				
			||||||
 | 
					pub struct NewPlateModalComplete(pub Option<plate_tool_lib::plate_instances::PlateInstance>);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn show_modal_if_open(ctx: &egui::Context, modal_state: &mut ModalState) {
 | 
				
			||||||
 | 
					    match modal_state {
 | 
				
			||||||
 | 
					        ModalState::NewPlate(_) => show_new_plate_modal(ctx, modal_state),
 | 
				
			||||||
 | 
					        ModalState::EditPlate(_) => show_edit_plate_modal(ctx, modal_state),
 | 
				
			||||||
 | 
					        _ => (),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					fn show_new_plate_modal(ctx: &egui::Context, modal_state: &mut ModalState) {
 | 
				
			||||||
 | 
					    let state: &mut NewPlateModalState = {
 | 
				
			||||||
 | 
					        if let ModalState::NewPlate(x) = modal_state {
 | 
				
			||||||
 | 
					            x
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            *modal_state = ModalState::NewPlate(NewPlateModalState::default());
 | 
				
			||||||
 | 
					            if let ModalState::NewPlate(y) = modal_state {
 | 
				
			||||||
 | 
					                y
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                unreachable!()
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut output: Option<plate_tool_lib::plate_instances::PlateInstance> = None;
 | 
				
			||||||
 | 
					    egui::Window::new("New Plate")
 | 
				
			||||||
 | 
					        .order(egui::Order::Foreground)
 | 
				
			||||||
 | 
					        .collapsible(false)
 | 
				
			||||||
 | 
					        .resizable(false)
 | 
				
			||||||
 | 
					        .open(&mut state.window_open)
 | 
				
			||||||
 | 
					        .show(ctx, |ui| {
 | 
				
			||||||
 | 
					            ui.vertical(|ui| {
 | 
				
			||||||
 | 
					                ui.add(egui::TextEdit::singleline(&mut state.name).hint_text("Plate Name"));
 | 
				
			||||||
 | 
					                egui::ComboBox::from_label("Plate Type")
 | 
				
			||||||
 | 
					                    .selected_text(format!("{:?}", state.plate_type))
 | 
				
			||||||
 | 
					                    .show_ui(ui, |ui| {
 | 
				
			||||||
 | 
					                        ui.selectable_value(
 | 
				
			||||||
 | 
					                            &mut state.plate_type,
 | 
				
			||||||
 | 
					                            plate_tool_lib::plate::PlateType::Source,
 | 
				
			||||||
 | 
					                            "Source",
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
 | 
					                        ui.selectable_value(
 | 
				
			||||||
 | 
					                            &mut state.plate_type,
 | 
				
			||||||
 | 
					                            plate_tool_lib::plate::PlateType::Destination,
 | 
				
			||||||
 | 
					                            "Destination",
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                egui::ComboBox::from_label("Plate Format")
 | 
				
			||||||
 | 
					                    .selected_text(format!("{:?}", state.plate_format))
 | 
				
			||||||
 | 
					                    .show_ui(ui, |ui| {
 | 
				
			||||||
 | 
					                        ui.selectable_value(
 | 
				
			||||||
 | 
					                            &mut state.plate_format,
 | 
				
			||||||
 | 
					                            plate_tool_lib::plate::PlateFormat::W96,
 | 
				
			||||||
 | 
					                            "96W",
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
 | 
					                        ui.selectable_value(
 | 
				
			||||||
 | 
					                            &mut state.plate_format,
 | 
				
			||||||
 | 
					                            plate_tool_lib::plate::PlateFormat::W384,
 | 
				
			||||||
 | 
					                            "384W",
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
 | 
					                        ui.selectable_value(
 | 
				
			||||||
 | 
					                            &mut state.plate_format,
 | 
				
			||||||
 | 
					                            plate_tool_lib::plate::PlateFormat::W1536,
 | 
				
			||||||
 | 
					                            "1536W",
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                if ui.button("Add").clicked() {
 | 
				
			||||||
 | 
					                    output = Some(plate_tool_lib::plate_instances::PlateInstance::new(
 | 
				
			||||||
 | 
					                        state.plate_type,
 | 
				
			||||||
 | 
					                        state.plate_format,
 | 
				
			||||||
 | 
					                        state.name.clone(),
 | 
				
			||||||
 | 
					                    ));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    if state.window_open == false {
 | 
				
			||||||
 | 
					        *modal_state = ModalState::NewPlateComplete(NewPlateModalComplete(None));
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if output.is_some() {
 | 
				
			||||||
 | 
					        *modal_state = ModalState::NewPlateComplete(NewPlateModalComplete(output));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					pub fn open_new_plate_modal(modal_state: &mut ModalState) {
 | 
				
			||||||
 | 
					    // Do not close another modal
 | 
				
			||||||
 | 
					    if matches!(modal_state, ModalState::None) {
 | 
				
			||||||
 | 
					        *modal_state = ModalState::NewPlate(NewPlateModalState::default());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[non_exhaustive]
 | 
				
			||||||
 | 
					#[derive(Debug, Clone)]
 | 
				
			||||||
 | 
					pub struct EditPlateModalState {
 | 
				
			||||||
 | 
					    pub name: String,
 | 
				
			||||||
 | 
					    pub plate_type: plate_tool_lib::plate::PlateType,
 | 
				
			||||||
 | 
					    pub plate_format: plate_tool_lib::plate::PlateFormat,
 | 
				
			||||||
 | 
					    pub id: plate_tool_lib::uuid::Uuid,
 | 
				
			||||||
 | 
					    window_open: bool,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					impl EditPlateModalState {
 | 
				
			||||||
 | 
					    fn new(
 | 
				
			||||||
 | 
					        name: &str,
 | 
				
			||||||
 | 
					        plate_type: plate_tool_lib::plate::PlateType,
 | 
				
			||||||
 | 
					        plate_format: plate_tool_lib::plate::PlateFormat,
 | 
				
			||||||
 | 
					        id: plate_tool_lib::uuid::Uuid,
 | 
				
			||||||
 | 
					    ) -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            name: name.to_owned(),
 | 
				
			||||||
 | 
					            plate_type,
 | 
				
			||||||
 | 
					            plate_format,
 | 
				
			||||||
 | 
					            id,
 | 
				
			||||||
 | 
					            window_open: true,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					#[non_exhaustive]
 | 
				
			||||||
 | 
					#[derive(Debug)]
 | 
				
			||||||
 | 
					pub struct EditPlateModalStateComplete(pub Option<EditPlateModalState>);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn show_edit_plate_modal(ctx: &egui::Context, modal_state: &mut ModalState) {
 | 
				
			||||||
 | 
					    let state: &mut EditPlateModalState = {
 | 
				
			||||||
 | 
					        if let ModalState::EditPlate(x) = modal_state {
 | 
				
			||||||
 | 
					            x
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut should_complete: bool = false;
 | 
				
			||||||
 | 
					    egui::Window::new("Edit Plate")
 | 
				
			||||||
 | 
					        .order(egui::Order::Foreground)
 | 
				
			||||||
 | 
					        .collapsible(false)
 | 
				
			||||||
 | 
					        .resizable(false)
 | 
				
			||||||
 | 
					        .open(&mut state.window_open)
 | 
				
			||||||
 | 
					        .show(ctx, |ui| {
 | 
				
			||||||
 | 
					            ui.vertical(|ui| {
 | 
				
			||||||
 | 
					                ui.add(egui::TextEdit::singleline(&mut state.name).hint_text("Plate Name"));
 | 
				
			||||||
 | 
					                egui::ComboBox::from_label("Plate Format")
 | 
				
			||||||
 | 
					                    .selected_text(format!("{:?}", state.plate_format))
 | 
				
			||||||
 | 
					                    .show_ui(ui, |ui| {
 | 
				
			||||||
 | 
					                        ui.selectable_value(
 | 
				
			||||||
 | 
					                            &mut state.plate_format,
 | 
				
			||||||
 | 
					                            plate_tool_lib::plate::PlateFormat::W96,
 | 
				
			||||||
 | 
					                            "96W",
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
 | 
					                        ui.selectable_value(
 | 
				
			||||||
 | 
					                            &mut state.plate_format,
 | 
				
			||||||
 | 
					                            plate_tool_lib::plate::PlateFormat::W384,
 | 
				
			||||||
 | 
					                            "384W",
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
 | 
					                        ui.selectable_value(
 | 
				
			||||||
 | 
					                            &mut state.plate_format,
 | 
				
			||||||
 | 
					                            plate_tool_lib::plate::PlateFormat::W1536,
 | 
				
			||||||
 | 
					                            "1536W",
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                if ui.button("Save").clicked() {
 | 
				
			||||||
 | 
					                    should_complete = true;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    if state.window_open == false {
 | 
				
			||||||
 | 
					        *modal_state = ModalState::EditPlateComplete(EditPlateModalStateComplete(None));
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if should_complete {
 | 
				
			||||||
 | 
					        *modal_state =
 | 
				
			||||||
 | 
					            ModalState::EditPlateComplete(EditPlateModalStateComplete(Some(state.to_owned())));
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					pub fn open_edit_plate_modal(
 | 
				
			||||||
 | 
					    modal_state: &mut ModalState,
 | 
				
			||||||
 | 
					    name: &str,
 | 
				
			||||||
 | 
					    plate_type: plate_tool_lib::plate::PlateType,
 | 
				
			||||||
 | 
					    plate_format: plate_tool_lib::plate::PlateFormat,
 | 
				
			||||||
 | 
					    id: plate_tool_lib::uuid::Uuid,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    // Do not close another modal
 | 
				
			||||||
 | 
					    if matches!(modal_state, ModalState::None) {
 | 
				
			||||||
 | 
					        *modal_state =
 | 
				
			||||||
 | 
					            ModalState::EditPlate(EditPlateModalState::new(name, plate_type, plate_format, id))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					pub fn open_edit_plate_modal_plateinstance(
 | 
				
			||||||
 | 
					    modal_state: &mut ModalState,
 | 
				
			||||||
 | 
					    plate_instance: &plate_tool_lib::plate_instances::PlateInstance,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    // Do not close another modal
 | 
				
			||||||
 | 
					    if matches!(modal_state, ModalState::None) {
 | 
				
			||||||
 | 
					        *modal_state = ModalState::EditPlate(EditPlateModalState::new(
 | 
				
			||||||
 | 
					            &plate_instance.name,
 | 
				
			||||||
 | 
					            plate_instance.plate.plate_type,
 | 
				
			||||||
 | 
					            plate_instance.plate.plate_format,
 | 
				
			||||||
 | 
					            plate_instance.get_uuid(),
 | 
				
			||||||
 | 
					        ));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,515 @@
 | 
				
			||||||
 | 
					use eframe::egui::{self, pos2, Color32, Rounding};
 | 
				
			||||||
 | 
					use eframe::glow::OFFSET;
 | 
				
			||||||
 | 
					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(80)));
 | 
				
			||||||
 | 
					static STROKE_CURRENT: LazyLock<egui::Stroke> =
 | 
				
			||||||
 | 
					    LazyLock::new(|| egui::Stroke::new(2.0, egui::Color32::from_gray(30)));
 | 
				
			||||||
 | 
					static STROKE_SELECT: LazyLock<egui::Stroke> =
 | 
				
			||||||
 | 
					    LazyLock::new(|| egui::Stroke::new(3.0, egui::Color32::from_gray(30)));
 | 
				
			||||||
 | 
					static STROKE_HOVER: LazyLock<egui::Stroke> =
 | 
				
			||||||
 | 
					    LazyLock::new(|| egui::Stroke::new(3.0, egui::Color32::from_gray(0)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
 | 
				
			||||||
 | 
					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],
 | 
				
			||||||
 | 
					    highlight: bool,
 | 
				
			||||||
 | 
					    fill: bool,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl WellInfo {
 | 
				
			||||||
 | 
					    fn new(volume: f32, color: [f64; 3]) -> Self {
 | 
				
			||||||
 | 
					        WellInfo {
 | 
				
			||||||
 | 
					            volume,
 | 
				
			||||||
 | 
					            color,
 | 
				
			||||||
 | 
					            highlight: false,
 | 
				
			||||||
 | 
					            fill: true,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
 | 
				
			||||||
 | 
					pub struct PlateDisplayOptions {
 | 
				
			||||||
 | 
					    pub show_transfer_hashes: bool,
 | 
				
			||||||
 | 
					    pub show_volume_heatmap: bool,
 | 
				
			||||||
 | 
					    pub show_coordinates: bool,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					impl Default for PlateDisplayOptions {
 | 
				
			||||||
 | 
					    fn default() -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            show_transfer_hashes: true,
 | 
				
			||||||
 | 
					            show_volume_heatmap: false,
 | 
				
			||||||
 | 
					            show_coordinates: true,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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,
 | 
				
			||||||
 | 
					    rows: u8,
 | 
				
			||||||
 | 
					    columns: u8,
 | 
				
			||||||
 | 
					) -> Option<(u8, u8)> {
 | 
				
			||||||
 | 
					    get_well_from_pos(
 | 
				
			||||||
 | 
					        response.hover_pos(),
 | 
				
			||||||
 | 
					        start_x,
 | 
				
			||||||
 | 
					        start_y,
 | 
				
			||||||
 | 
					        radius,
 | 
				
			||||||
 | 
					        rows,
 | 
				
			||||||
 | 
					        columns,
 | 
				
			||||||
 | 
					        true,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn get_well_from_pos(
 | 
				
			||||||
 | 
					    position: Option<egui::Pos2>,
 | 
				
			||||||
 | 
					    start_x: f32,
 | 
				
			||||||
 | 
					    start_y: f32,
 | 
				
			||||||
 | 
					    radius: f32,
 | 
				
			||||||
 | 
					    rows: u8,
 | 
				
			||||||
 | 
					    columns: u8,
 | 
				
			||||||
 | 
					    saturate: bool,
 | 
				
			||||||
 | 
					) -> Option<(u8, u8)> {
 | 
				
			||||||
 | 
					    // cool fact: the bounds of our frame aren't actually the bounds of what counts
 | 
				
			||||||
 | 
					    // as hovering. as a result, we have to make sure that we check these bounds here.
 | 
				
			||||||
 | 
					    // yippee
 | 
				
			||||||
 | 
					    let max_width = start_x + radius * 2.0 * columns as f32;
 | 
				
			||||||
 | 
					    let max_height = start_y + radius * 2.0 * rows as f32;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Some((row, column))
 | 
				
			||||||
 | 
					    position
 | 
				
			||||||
 | 
					        .map(|p| Into::<(f32, f32)>::into(p))
 | 
				
			||||||
 | 
					        .and_then(|(x, y)| {
 | 
				
			||||||
 | 
					            // Check bounds if no saturation
 | 
				
			||||||
 | 
					            if !saturate && (x < start_x || y < start_y || x > max_width || y > max_height) {
 | 
				
			||||||
 | 
					                return None;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let solved_column: u8 = match x {
 | 
				
			||||||
 | 
					                x if (x < start_x) => 1,
 | 
				
			||||||
 | 
					                x if (x > max_width) => columns,
 | 
				
			||||||
 | 
					                _ => (x - start_x).div_euclid(radius * 2.0) as u8 + 1u8,
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            let solved_row: u8 = match y {
 | 
				
			||||||
 | 
					                y if (y < start_y) => 1,
 | 
				
			||||||
 | 
					                y if (y > max_height) => rows,
 | 
				
			||||||
 | 
					                _ => (y - start_y).div_euclid(radius * 2.0) as u8 + 1u8,
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Some((solved_row, solved_column))
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn calculate_shading_for_wells(
 | 
				
			||||||
 | 
					    rows: u8,
 | 
				
			||||||
 | 
					    columns: u8,
 | 
				
			||||||
 | 
					    transfers: Option<Vec<&plate_tool_lib::transfer::Transfer>>,
 | 
				
			||||||
 | 
					    plate_type: plate_tool_lib::plate::PlateType,
 | 
				
			||||||
 | 
					    ordered_ids: &[Uuid],
 | 
				
			||||||
 | 
					    cache: &plate_tool_lib::transfer_region_cache::TransferRegionCache,
 | 
				
			||||||
 | 
					    display_options: PlateDisplayOptions,
 | 
				
			||||||
 | 
					) -> 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();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if let Some(transfers) = transfers {
 | 
				
			||||||
 | 
					        // Needed for palette
 | 
				
			||||||
 | 
					        let max_volume: f32 = transfers
 | 
				
			||||||
 | 
					            .iter()
 | 
				
			||||||
 | 
					            .flat_map(|v| match v.volume {
 | 
				
			||||||
 | 
					                plate_tool_lib::transfer_volume::TransferVolume::Single(x) => Some(x),
 | 
				
			||||||
 | 
					                _ => None,
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .fold(f32::MIN, f32::max)
 | 
				
			||||||
 | 
					            .max(1.0f32); // Otherwise f32::MIN is return value
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for transfer in transfers {
 | 
				
			||||||
 | 
					            let cache_result = match plate_type {
 | 
				
			||||||
 | 
					                plate_tool_lib::plate::PlateType::Source => cache.get_or_calculate_source(transfer),
 | 
				
			||||||
 | 
					                plate_tool_lib::plate::PlateType::Destination => {
 | 
				
			||||||
 | 
					                    cache.get_or_calculate_destination(transfer)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            let volume = if let plate_tool_lib::transfer_volume::TransferVolume::Single(x) =
 | 
				
			||||||
 | 
					                transfer.volume
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                x
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                0.0
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            if let Some(wells) = cache_result {
 | 
				
			||||||
 | 
					                for well in wells.iter().filter(|x| x.row <= rows && x.col <= columns) {
 | 
				
			||||||
 | 
					                    if let Some(Some(mut x)) = well_infos.get_mut(
 | 
				
			||||||
 | 
					                        (well.row - 1) as usize * columns as usize + (well.col - 1) as usize,
 | 
				
			||||||
 | 
					                    ) {
 | 
				
			||||||
 | 
					                        x.volume += volume;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        x.color = if display_options.show_volume_heatmap {
 | 
				
			||||||
 | 
					                            PALETTE.get_linear(volume.into(), max_volume.into())
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            PALETTE.get_ordered(transfer.id, ordered_ids)
 | 
				
			||||||
 | 
					                        };
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        if let Some(mut wi) = well_infos.get_mut(
 | 
				
			||||||
 | 
					                            (well.row - 1) as usize * columns as usize + (well.col - 1) as usize,
 | 
				
			||||||
 | 
					                        ) {
 | 
				
			||||||
 | 
					                            *wi = Some(WellInfo::new(
 | 
				
			||||||
 | 
					                                volume,
 | 
				
			||||||
 | 
					                                if display_options.show_volume_heatmap {
 | 
				
			||||||
 | 
					                                    PALETTE.get_linear(volume.into(), max_volume.into())
 | 
				
			||||||
 | 
					                                } else {
 | 
				
			||||||
 | 
					                                    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 draw_cross(painter: &egui::Painter, center: egui::Pos2, radius: f32, stroke: egui::Stroke) {
 | 
				
			||||||
 | 
					    // Generate points on circle
 | 
				
			||||||
 | 
					    const OFFSET_ARRAY_X: [f32; 4] = [0.71, -0.71, -0.71, 0.71]; // == sin(2pi/8) == cos(2pi/8)
 | 
				
			||||||
 | 
					    const OFFSET_ARRAY_Y: [f32; 4] = [0.71, 0.71, -0.71, -0.71];
 | 
				
			||||||
 | 
					    let radius_adjusted_array_x: [f32; 4] = core::array::from_fn(|x| OFFSET_ARRAY_X[x] * radius);
 | 
				
			||||||
 | 
					    let radius_adjusted_array_y: [f32; 4] = core::array::from_fn(|y| OFFSET_ARRAY_Y[y] * radius);
 | 
				
			||||||
 | 
					    let xs: [f32; 4] = core::array::from_fn(|x| radius_adjusted_array_x[x] + center.x);
 | 
				
			||||||
 | 
					    let ys: [f32; 4] = core::array::from_fn(|y| radius_adjusted_array_y[y] + center.y);
 | 
				
			||||||
 | 
					    let pts: [egui::Pos2; 4] = core::array::from_fn(|i| egui::Pos2::new(xs[i], ys[i]));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    painter.line_segment([pts[0], pts[2]], stroke);
 | 
				
			||||||
 | 
					    painter.line_segment([pts[1], pts[3]], stroke);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn add_plate_sub(
 | 
				
			||||||
 | 
					    size: egui::Vec2,
 | 
				
			||||||
 | 
					    rows: u8,
 | 
				
			||||||
 | 
					    columns: u8,
 | 
				
			||||||
 | 
					    transfers: Option<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,
 | 
				
			||||||
 | 
					    display_options: PlateDisplayOptions,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    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,
 | 
				
			||||||
 | 
					        rows,
 | 
				
			||||||
 | 
					        columns,
 | 
				
			||||||
 | 
					        false,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    let hovered_well = get_hover_well(&response, start_x, start_y, radius, rows, columns);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if response.clicked() {
 | 
				
			||||||
 | 
					        if let Some(cts) = current_transfer_state {
 | 
				
			||||||
 | 
					            let end_well: Option<Well> = hovered_well.map(|(row, col)| Well { row, col });
 | 
				
			||||||
 | 
					            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 {
 | 
				
			||||||
 | 
					            if let Some(start_well) = drag_start_well.map(|(row, col)| Well { row, col }) {
 | 
				
			||||||
 | 
					                let end_well: Option<Well> = hovered_well.map(|(row, col)| Well { row, col });
 | 
				
			||||||
 | 
					                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 = {
 | 
				
			||||||
 | 
					        // Get non-active transfer info
 | 
				
			||||||
 | 
					        let mut well_infos =
 | 
				
			||||||
 | 
					            calculate_shading_for_wells(rows, columns, transfers, plate_type, ordered_ids, cache, display_options);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 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()),
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .map(|xs| {
 | 
				
			||||||
 | 
					                xs.iter()
 | 
				
			||||||
 | 
					                    .map(|x| (x.row as usize, x.col as usize))
 | 
				
			||||||
 | 
					                    .collect()
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        if let Some(wells) = current_transfer_wells {
 | 
				
			||||||
 | 
					            for w in wells {
 | 
				
			||||||
 | 
					                if let Some(mut well_info) =
 | 
				
			||||||
 | 
					                    well_infos.get_mut((w.0 - 1) * columns as usize + (w.1 - 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);
 | 
				
			||||||
 | 
					                    let color = well_info.map(|x| x.color).unwrap_or([255.0, 255.0, 255.0]);
 | 
				
			||||||
 | 
					                    let fill = well_info.map(|x| x.color).is_some();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    *well_info = Some(WellInfo {
 | 
				
			||||||
 | 
					                        color,
 | 
				
			||||||
 | 
					                        volume: 1.0,
 | 
				
			||||||
 | 
					                        fill,
 | 
				
			||||||
 | 
					                        highlight: true,
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        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 1..=rows {
 | 
				
			||||||
 | 
					        for c_column in 1..=columns {
 | 
				
			||||||
 | 
					            let center = egui::pos2(
 | 
				
			||||||
 | 
					                start_x + radius + 2.0 * radius * (c_column - 1) as f32,
 | 
				
			||||||
 | 
					                start_y + radius + 2.0 * radius * (c_row - 1) as f32,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            //
 | 
				
			||||||
 | 
					            // Draw fill first
 | 
				
			||||||
 | 
					            //
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if let Some(well_info) =
 | 
				
			||||||
 | 
					                well_infos[(c_row - 1) as usize * columns as usize + (c_column - 1) as usize]
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                if well_info.fill {
 | 
				
			||||||
 | 
					                    painter.circle_filled(center, radius * 0.80, f64_to_color32(well_info.color));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                if well_info.highlight && display_options.show_transfer_hashes {
 | 
				
			||||||
 | 
					                    draw_cross(&painter, center, radius * 0.80, *STROKE_CURRENT);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            //
 | 
				
			||||||
 | 
					            // 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,
 | 
				
			||||||
 | 
					                        col: c_column,
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    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
 | 
				
			||||||
 | 
					    let default_font = egui::FontId::monospace(f32::min(radius, 14.0));
 | 
				
			||||||
 | 
					    static DEFAULT_TEXT_COLOR: LazyLock<egui::Color32> =
 | 
				
			||||||
 | 
					        LazyLock::new(|| egui::Color32::from_gray(128));
 | 
				
			||||||
 | 
					    static HIGHLIGHT_TEXT_COLOR: LazyLock<egui::Color32> =
 | 
				
			||||||
 | 
					        LazyLock::new(|| egui::Color32::from_gray(0));
 | 
				
			||||||
 | 
					    for c_row in 0..rows {
 | 
				
			||||||
 | 
					        let text_color = {
 | 
				
			||||||
 | 
					            if display_options.show_coordinates && hovered_well.is_some_and(|x| x.0 == c_row + 1) {
 | 
				
			||||||
 | 
					                *HIGHLIGHT_TEXT_COLOR
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                *DEFAULT_TEXT_COLOR
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        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(),
 | 
				
			||||||
 | 
					            default_font.clone(),
 | 
				
			||||||
 | 
					            text_color,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    for c_column in 0..columns {
 | 
				
			||||||
 | 
					        let text_color = {
 | 
				
			||||||
 | 
					            if display_options.show_coordinates && hovered_well.is_some_and(|x| x.1 == c_column + 1)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                *HIGHLIGHT_TEXT_COLOR
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                *DEFAULT_TEXT_COLOR
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        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(),
 | 
				
			||||||
 | 
					            default_font.clone(),
 | 
				
			||||||
 | 
					            text_color,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn add_plate(
 | 
				
			||||||
 | 
					    size: egui::Vec2,
 | 
				
			||||||
 | 
					    pf: PlateFormat,
 | 
				
			||||||
 | 
					    transfers: Option<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,
 | 
				
			||||||
 | 
					    display_options: PlateDisplayOptions,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    add_plate_sub(
 | 
				
			||||||
 | 
					        size,
 | 
				
			||||||
 | 
					        pf.rows(),
 | 
				
			||||||
 | 
					        pf.columns(),
 | 
				
			||||||
 | 
					        transfers,
 | 
				
			||||||
 | 
					        plate_type,
 | 
				
			||||||
 | 
					        ordered_ids,
 | 
				
			||||||
 | 
					        cache,
 | 
				
			||||||
 | 
					        current_transfer_state,
 | 
				
			||||||
 | 
					        ui,
 | 
				
			||||||
 | 
					        state,
 | 
				
			||||||
 | 
					        display_options,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,135 @@
 | 
				
			||||||
 | 
					use std::sync::LazyLock;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use eframe::egui::{
 | 
				
			||||||
 | 
					    self,
 | 
				
			||||||
 | 
					    style::{Selection, Visuals, WidgetVisuals, Widgets},
 | 
				
			||||||
 | 
					    Color32, Stroke,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn set_visuals(ctx: &egui::Context) {
 | 
				
			||||||
 | 
					    let current = ctx.style().visuals.clone();
 | 
				
			||||||
 | 
					    let visuals = Visuals {
 | 
				
			||||||
 | 
					        override_text_color: Some(STANDARD_THEME.text),
 | 
				
			||||||
 | 
					        faint_bg_color: STANDARD_THEME.surface0,
 | 
				
			||||||
 | 
					        extreme_bg_color: STANDARD_THEME.surface0,
 | 
				
			||||||
 | 
					        window_fill: STANDARD_THEME.base,
 | 
				
			||||||
 | 
					        panel_fill: STANDARD_THEME.base,
 | 
				
			||||||
 | 
					        window_stroke: Stroke {
 | 
				
			||||||
 | 
					            color: STANDARD_THEME.overlay1,
 | 
				
			||||||
 | 
					            ..current.window_stroke
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        widgets: Widgets {
 | 
				
			||||||
 | 
					            noninteractive: make_widget_visual(
 | 
				
			||||||
 | 
					                current.widgets.noninteractive,
 | 
				
			||||||
 | 
					                STANDARD_THEME.base,
 | 
				
			||||||
 | 
					                &STANDARD_THEME,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            inactive: make_widget_visual(
 | 
				
			||||||
 | 
					                current.widgets.inactive,
 | 
				
			||||||
 | 
					                STANDARD_THEME.surface0,
 | 
				
			||||||
 | 
					                &STANDARD_THEME,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            hovered: make_widget_visual(
 | 
				
			||||||
 | 
					                current.widgets.hovered,
 | 
				
			||||||
 | 
					                STANDARD_THEME.light_purple,
 | 
				
			||||||
 | 
					                &STANDARD_THEME,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            active: make_widget_visual(
 | 
				
			||||||
 | 
					                current.widgets.active,
 | 
				
			||||||
 | 
					                STANDARD_THEME.surface1,
 | 
				
			||||||
 | 
					                &STANDARD_THEME,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            open: make_widget_visual(
 | 
				
			||||||
 | 
					                current.widgets.open,
 | 
				
			||||||
 | 
					                STANDARD_THEME.surface0,
 | 
				
			||||||
 | 
					                &STANDARD_THEME,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        selection: Selection {
 | 
				
			||||||
 | 
					            bg_fill: STANDARD_THEME.purple,
 | 
				
			||||||
 | 
					            stroke: Stroke {
 | 
				
			||||||
 | 
					                color: STANDARD_THEME.overlay1,
 | 
				
			||||||
 | 
					                ..current.selection.stroke
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        ..current
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    ctx.set_visuals(visuals);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					fn make_widget_visual(current: WidgetVisuals, bg: Color32, theme: &Theme) -> WidgetVisuals {
 | 
				
			||||||
 | 
					    WidgetVisuals {
 | 
				
			||||||
 | 
					        bg_fill: bg,
 | 
				
			||||||
 | 
					        weak_bg_fill: bg,
 | 
				
			||||||
 | 
					        bg_stroke: Stroke {
 | 
				
			||||||
 | 
					            color: theme.overlay1,
 | 
				
			||||||
 | 
					            ..current.bg_stroke
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        fg_stroke: Stroke {
 | 
				
			||||||
 | 
					            color: theme.text,
 | 
				
			||||||
 | 
					            ..current.fg_stroke
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        ..current
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub static STANDARD_THEME: LazyLock<Theme> = LazyLock::new(|| {
 | 
				
			||||||
 | 
					    Theme::standard()
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					#[derive(Debug)]
 | 
				
			||||||
 | 
					pub struct Theme {
 | 
				
			||||||
 | 
					    pub base: Color32,
 | 
				
			||||||
 | 
					    pub surface0: Color32,
 | 
				
			||||||
 | 
					    pub surface1: Color32,
 | 
				
			||||||
 | 
					    pub surface2: Color32,
 | 
				
			||||||
 | 
					    pub overlay0: Color32,
 | 
				
			||||||
 | 
					    pub overlay1: Color32,
 | 
				
			||||||
 | 
					    pub overlay2: Color32,
 | 
				
			||||||
 | 
					    pub text: Color32,
 | 
				
			||||||
 | 
					    pub purple: Color32,
 | 
				
			||||||
 | 
					    pub light_purple: Color32,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					impl Theme {
 | 
				
			||||||
 | 
					    fn standard() -> Self {
 | 
				
			||||||
 | 
					        const BASE_HUE: f32 = 45.0;
 | 
				
			||||||
 | 
					        const BASE_SATURATION: f32 = 40.0;
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            base: hsl_to_color32(BASE_HUE, BASE_SATURATION, 88.0),
 | 
				
			||||||
 | 
					            surface0: hsl_to_color32(BASE_HUE, BASE_SATURATION, 80.0),
 | 
				
			||||||
 | 
					            surface1: hsl_to_color32(BASE_HUE, BASE_SATURATION, 75.0),
 | 
				
			||||||
 | 
					            surface2: hsl_to_color32(BASE_HUE, BASE_SATURATION, 65.0),
 | 
				
			||||||
 | 
					            overlay0: hsl_to_color32(BASE_HUE, BASE_SATURATION, 60.0),
 | 
				
			||||||
 | 
					            overlay1: hsl_to_color32(BASE_HUE, BASE_SATURATION, 55.0),
 | 
				
			||||||
 | 
					            overlay2: hsl_to_color32(BASE_HUE, BASE_SATURATION, 50.0),
 | 
				
			||||||
 | 
					            text: hsl_to_color32(BASE_HUE, BASE_SATURATION, 25.0),
 | 
				
			||||||
 | 
					            purple: hsl_to_color32(270.0, 80.0, 80.0),
 | 
				
			||||||
 | 
					            light_purple: hsl_to_color32(270.0, 70.0, 90.0),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (u8, u8, u8) {
 | 
				
			||||||
 | 
					    let s = s / 100.0;
 | 
				
			||||||
 | 
					    let l = l / 100.0;
 | 
				
			||||||
 | 
					    let chroma = (1.0 - f32::abs(2.0 * l - 1.0)) * s;
 | 
				
			||||||
 | 
					    let h_prime = h / 60.0;
 | 
				
			||||||
 | 
					    let x = chroma * (1.0 - f32::abs((h_prime % 2.0) - 1.0));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let (r1, g1, b1) = match h_prime {
 | 
				
			||||||
 | 
					        _ if h_prime < 1.0 => (chroma, x, 0.0),
 | 
				
			||||||
 | 
					        _ if h_prime >= 1.0 && h_prime < 2.0 => (x, chroma, 0.0),
 | 
				
			||||||
 | 
					        _ if h_prime >= 2.0 && h_prime < 3.0 => (0.0, chroma, x),
 | 
				
			||||||
 | 
					        _ if h_prime >= 3.0 && h_prime < 4.0 => (0.0, x, chroma),
 | 
				
			||||||
 | 
					        _ if h_prime >= 4.0 && h_prime < 5.0 => (x, 0.0, chroma),
 | 
				
			||||||
 | 
					        _ if h_prime >= 5.0 && h_prime < 6.0 => (chroma, 0.0, x),
 | 
				
			||||||
 | 
					        _ => unreachable!(),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let m = l - (chroma/2.0);
 | 
				
			||||||
 | 
					    let (r,g,b) = ((r1 + m) * 255.0, (g1 + m) * 255.0, (b1 + m) * 255.0);
 | 
				
			||||||
 | 
					    (r as u8, g as u8, b as u8)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn hsl_to_color32(h: f32, s: f32, l: f32) -> Color32 {
 | 
				
			||||||
 | 
					    let (r, g, b) = hsl_to_rgb(h, s, l);
 | 
				
			||||||
 | 
					    Color32::from_rgb(r, g, b)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,323 @@
 | 
				
			||||||
 | 
					use eframe::egui;
 | 
				
			||||||
 | 
					use plate_tool_lib::transfer_region::{self, Region, TransferRegion};
 | 
				
			||||||
 | 
					use std::hash::{DefaultHasher, Hash, Hasher};
 | 
				
			||||||
 | 
					use std::sync::{Arc, Mutex};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::main_state::{self, MainState};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub type CurrentTransferState = Arc<Mutex<CurrentTransferStateInterior>>;
 | 
				
			||||||
 | 
					#[derive(Debug, serde::Serialize, serde::Deserialize)]
 | 
				
			||||||
 | 
					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,
 | 
				
			||||||
 | 
					    #[serde(skip)]
 | 
				
			||||||
 | 
					    transfer_hash: Option<u64>,
 | 
				
			||||||
 | 
					    #[serde(skip)]
 | 
				
			||||||
 | 
					    source_wells: Option<Vec<plate_tool_lib::Well>>,
 | 
				
			||||||
 | 
					    #[serde(skip)]
 | 
				
			||||||
 | 
					    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 fn try_new_from_main_state_transfer(ms: &MainState) -> Option<Self> {
 | 
				
			||||||
 | 
					        let transfer_id = ms.get_current_transfer_uuid();
 | 
				
			||||||
 | 
					        let transfer = ms
 | 
				
			||||||
 | 
					            .transfers
 | 
				
			||||||
 | 
					            .iter()
 | 
				
			||||||
 | 
					            .find(|x| Some(x.get_uuid()) == transfer_id);
 | 
				
			||||||
 | 
					        if let Some(transfer) = transfer {
 | 
				
			||||||
 | 
					            let volume: f32 = match transfer.volume {
 | 
				
			||||||
 | 
					                plate_tool_lib::transfer_volume::TransferVolume::Single(x) => x,
 | 
				
			||||||
 | 
					                plate_tool_lib::transfer_volume::TransferVolume::WellMap(_) => {
 | 
				
			||||||
 | 
					                    log::warn!("COOL BUG: I genuinely don't know when this variant is used and honestly assume that it just never is constructed! Anyway, here's main state:\n{:?}", ms);
 | 
				
			||||||
 | 
					                    unreachable!("It better not!");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                _ => unreachable!(),
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            return Some(Self {
 | 
				
			||||||
 | 
					                transfer_name: transfer.name.clone(),
 | 
				
			||||||
 | 
					                source_region: transfer.transfer_region.source_region.clone(),
 | 
				
			||||||
 | 
					                destination_region: transfer.transfer_region.dest_region.clone(),
 | 
				
			||||||
 | 
					                source_plate: transfer.transfer_region.source_plate,
 | 
				
			||||||
 | 
					                destination_plate: transfer.transfer_region.dest_plate,
 | 
				
			||||||
 | 
					                source_row_interleave: transfer.transfer_region.interleave_source.0,
 | 
				
			||||||
 | 
					                source_column_interleave: transfer.transfer_region.interleave_source.1,
 | 
				
			||||||
 | 
					                destination_row_interleave: transfer.transfer_region.interleave_dest.0,
 | 
				
			||||||
 | 
					                destination_column_interleave: transfer.transfer_region.interleave_dest.1,
 | 
				
			||||||
 | 
					                volume,
 | 
				
			||||||
 | 
					                source_wells: None,
 | 
				
			||||||
 | 
					                destination_wells: None,
 | 
				
			||||||
 | 
					                transfer_hash: None,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        None
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn convert_to_transfer(&self, ms: &MainState) -> Option<plate_tool_lib::transfer::Transfer> {
 | 
				
			||||||
 | 
					        let source_plate_uuid = ms.get_current_source_uuid()?;
 | 
				
			||||||
 | 
					        let destination_plate_uuid = ms.get_current_destination_uuid()?;
 | 
				
			||||||
 | 
					        let source_plate_instance = ms.source_plates.iter().find(|x| x.get_uuid() == source_plate_uuid)?;
 | 
				
			||||||
 | 
					        let destination_plate_instance = ms.destination_plates.iter().find(|x| x.get_uuid() == destination_plate_uuid)?;
 | 
				
			||||||
 | 
					        let transfer = Some(plate_tool_lib::transfer::Transfer::new(
 | 
				
			||||||
 | 
					            source_plate_instance.clone(),
 | 
				
			||||||
 | 
					            destination_plate_instance.clone(),
 | 
				
			||||||
 | 
					            self.generate_transfer_region(),
 | 
				
			||||||
 | 
					            self.transfer_name.clone(),
 | 
				
			||||||
 | 
					        ));
 | 
				
			||||||
 | 
					        transfer.map(|mut x| {x.volume = plate_tool_lib::transfer_volume::TransferVolume::Single(self.volume); x})
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug)]
 | 
				
			||||||
 | 
					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,
 | 
				
			||||||
 | 
					    main_state: &mut MainState,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    // Can we reduce the length of this lock pls
 | 
				
			||||||
 | 
					    let cts = state;
 | 
				
			||||||
 | 
					    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),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ui.horizontal(|ui| {
 | 
				
			||||||
 | 
					        if ui.button("Save").clicked() {
 | 
				
			||||||
 | 
					            if let Some(transfer_uuid) = main_state.get_current_transfer_uuid() {
 | 
				
			||||||
 | 
					                log::info!("should change transfer");
 | 
				
			||||||
 | 
					                if let Some(mut transfer) = main_state.transfers.iter_mut().find(|x| x.id == transfer_uuid) {
 | 
				
			||||||
 | 
					                    transfer.transfer_region = state.generate_transfer_region();
 | 
				
			||||||
 | 
					                    transfer.name = state.transfer_name.clone();
 | 
				
			||||||
 | 
					                    main_state.transfer_region_cache.invalidate(&transfer);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else { // Need to make a new transfer
 | 
				
			||||||
 | 
					                if state.transfer_name.is_empty() {
 | 
				
			||||||
 | 
					                    state.transfer_name = "New Transfer".to_string();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                let new_transfer = state.convert_to_transfer(main_state);
 | 
				
			||||||
 | 
					                log::info!("{:?}", new_transfer);
 | 
				
			||||||
 | 
					                if let Some(new_transfer) = new_transfer {
 | 
				
			||||||
 | 
					                    main_state.transfers.push(new_transfer);
 | 
				
			||||||
 | 
					                    let new_transfer = main_state.transfers.last()
 | 
				
			||||||
 | 
					                    .expect("Cannot be empty, just added a transfer");
 | 
				
			||||||
 | 
					                    main_state.transfer_region_cache.add_overwrite(new_transfer);
 | 
				
			||||||
 | 
					                    main_state.set_current_transfer(new_transfer.id);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if ui.button("New").clicked() {
 | 
				
			||||||
 | 
					            *state = CurrentTransferStateInterior::default();
 | 
				
			||||||
 | 
					            set_plates(main_state, &mut state);
 | 
				
			||||||
 | 
					            main_state.set_no_current_transfer();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn set_plates(ms: &MainState, cts: &mut CurrentTransferStateInterior) {
 | 
				
			||||||
 | 
					    if let Some(source_plate) = ms.get_current_source_plateinstance() {
 | 
				
			||||||
 | 
					        cts.source_plate = source_plate.plate;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if let Some(destination_plate) = ms.get_current_destination_plateinstance() {
 | 
				
			||||||
 | 
					        cts.destination_plate = destination_plate.plate;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,185 @@
 | 
				
			||||||
 | 
					use eframe::egui::{self, text_selection::visuals, NumExt};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    main_state::MainState,
 | 
				
			||||||
 | 
					    modals::ModalState,
 | 
				
			||||||
 | 
					    transfer_menu::{CurrentTransferState, CurrentTransferStateInterior},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::sync::LazyLock;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn tree_label(
 | 
				
			||||||
 | 
					    ui: &mut egui::Ui,
 | 
				
			||||||
 | 
					    ms: &MainState,
 | 
				
			||||||
 | 
					    modal_state: &mut ModalState,
 | 
				
			||||||
 | 
					    name: &str,
 | 
				
			||||||
 | 
					    uuid: plate_tool_lib::uuid::Uuid,
 | 
				
			||||||
 | 
					    selected: bool,
 | 
				
			||||||
 | 
					) -> (Option<plate_tool_lib::uuid::Uuid>, bool) {
 | 
				
			||||||
 | 
					    let button_padding = ui.spacing().button_padding;
 | 
				
			||||||
 | 
					    let total_extra = button_padding + button_padding;
 | 
				
			||||||
 | 
					    let wrap_width = ui.available_width() - total_extra.x;
 | 
				
			||||||
 | 
					    let galley =
 | 
				
			||||||
 | 
					        egui::WidgetText::from(name).into_galley(ui, None, wrap_width, egui::TextStyle::Button);
 | 
				
			||||||
 | 
					    let desired_size = {
 | 
				
			||||||
 | 
					        let mut desired_size = total_extra + galley.size();
 | 
				
			||||||
 | 
					        desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
 | 
				
			||||||
 | 
					        desired_size
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    let (rect, response) = ui.allocate_at_least(desired_size, egui::Sense::click());
 | 
				
			||||||
 | 
					    let text_pos = ui
 | 
				
			||||||
 | 
					        .layout()
 | 
				
			||||||
 | 
					        .align_size_within_rect(galley.size(), rect.shrink2(button_padding))
 | 
				
			||||||
 | 
					        .min;
 | 
				
			||||||
 | 
					    let visuals = ui.style().interact_selectable(&response, selected);
 | 
				
			||||||
 | 
					    if selected || response.hovered() {
 | 
				
			||||||
 | 
					        let rect = rect.expand(visuals.expansion);
 | 
				
			||||||
 | 
					        ui.painter().rect(
 | 
				
			||||||
 | 
					            rect,
 | 
				
			||||||
 | 
					            visuals.rounding,
 | 
				
			||||||
 | 
					            visuals.weak_bg_fill,
 | 
				
			||||||
 | 
					            visuals.bg_stroke,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    ui.painter().galley(text_pos, galley, visuals.text_color());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if response.clicked() {
 | 
				
			||||||
 | 
					        return (Some(uuid), false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if response.double_clicked() {
 | 
				
			||||||
 | 
					        return (None, true);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    (None, false)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn tree(
 | 
				
			||||||
 | 
					    ui: &mut egui::Ui,
 | 
				
			||||||
 | 
					    ms: &mut MainState,
 | 
				
			||||||
 | 
					    cts: &CurrentTransferState,
 | 
				
			||||||
 | 
					    modal_state: &mut ModalState,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    // Add all source plates
 | 
				
			||||||
 | 
					    ui.vertical(|ui| {
 | 
				
			||||||
 | 
					        ui.heading("Source Plates");
 | 
				
			||||||
 | 
					        let mut new_uuid: Option<plate_tool_lib::uuid::Uuid> = None;
 | 
				
			||||||
 | 
					        for (name, uuid) in ms.source_plates.iter().map(|x| (&x.name, x.get_uuid())) {
 | 
				
			||||||
 | 
					            let (potential_new_uuid, dbl_clicked) = tree_label(
 | 
				
			||||||
 | 
					                ui,
 | 
				
			||||||
 | 
					                ms,
 | 
				
			||||||
 | 
					                modal_state,
 | 
				
			||||||
 | 
					                name,
 | 
				
			||||||
 | 
					                uuid,
 | 
				
			||||||
 | 
					                ms.get_current_source_uuid().is_some_and(|x| x == uuid),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            if potential_new_uuid.is_some() {
 | 
				
			||||||
 | 
					                new_uuid = potential_new_uuid;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if dbl_clicked {
 | 
				
			||||||
 | 
					                let pi = ms.get_source_by_uuid(uuid);
 | 
				
			||||||
 | 
					                if let Some(pi) = pi {
 | 
				
			||||||
 | 
					                    crate::modals::open_edit_plate_modal_plateinstance(modal_state, &pi);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if let Some(uuid) = new_uuid {
 | 
				
			||||||
 | 
					            let current_source_uuid = ms.get_current_source_uuid();
 | 
				
			||||||
 | 
					            if current_source_uuid.is_some_and(|x| x != uuid) || current_source_uuid.is_none() {
 | 
				
			||||||
 | 
					                ms.set_current_source(uuid);
 | 
				
			||||||
 | 
					                ms.set_no_current_transfer();
 | 
				
			||||||
 | 
					                if let Some(mut cts) = cts.lock().ok() {
 | 
				
			||||||
 | 
					                    *cts = CurrentTransferStateInterior::default();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                set_plates(ms, cts);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Add all destination plates
 | 
				
			||||||
 | 
					    ui.vertical(|ui| {
 | 
				
			||||||
 | 
					        ui.heading("Destination Plates");
 | 
				
			||||||
 | 
					        let mut new_uuid: Option<plate_tool_lib::uuid::Uuid> = None;
 | 
				
			||||||
 | 
					        for (name, uuid) in ms
 | 
				
			||||||
 | 
					            .destination_plates
 | 
				
			||||||
 | 
					            .iter()
 | 
				
			||||||
 | 
					            .map(|x| (&x.name, x.get_uuid()))
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            let (potential_new_uuid, dbl_clicked) = tree_label(
 | 
				
			||||||
 | 
					                ui,
 | 
				
			||||||
 | 
					                ms,
 | 
				
			||||||
 | 
					                modal_state,
 | 
				
			||||||
 | 
					                name,
 | 
				
			||||||
 | 
					                uuid,
 | 
				
			||||||
 | 
					                ms.get_current_destination_uuid().is_some_and(|x| x == uuid),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            if potential_new_uuid.is_some() {
 | 
				
			||||||
 | 
					                new_uuid = potential_new_uuid;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if dbl_clicked {
 | 
				
			||||||
 | 
					                let pi = ms.get_destination_by_uuid(uuid);
 | 
				
			||||||
 | 
					                if let Some(pi) = pi {
 | 
				
			||||||
 | 
					                    crate::modals::open_edit_plate_modal_plateinstance(modal_state, &pi);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if let Some(uuid) = new_uuid {
 | 
				
			||||||
 | 
					            let current_destination_uuid = ms.get_current_destination_uuid();
 | 
				
			||||||
 | 
					            if current_destination_uuid.is_some_and(|x| x != uuid)
 | 
				
			||||||
 | 
					                || current_destination_uuid.is_none()
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                ms.set_current_destination(uuid);
 | 
				
			||||||
 | 
					                ms.set_no_current_transfer();
 | 
				
			||||||
 | 
					                if let Some(mut cts) = cts.lock().ok() {
 | 
				
			||||||
 | 
					                    *cts = CurrentTransferStateInterior::default();
 | 
				
			||||||
 | 
					                    cts.destination_plate = ms
 | 
				
			||||||
 | 
					                        .get_current_destination_plateinstance()
 | 
				
			||||||
 | 
					                        .expect("Just set destination")
 | 
				
			||||||
 | 
					                        .plate;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                set_plates(ms, cts);
 | 
				
			||||||
 | 
					                log::warn!(
 | 
				
			||||||
 | 
					                    "{:?}\n{:?}",
 | 
				
			||||||
 | 
					                    cts,
 | 
				
			||||||
 | 
					                    ms.get_current_destination_plateinstance()
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    // Add all transfers
 | 
				
			||||||
 | 
					    ui.vertical(|ui| {
 | 
				
			||||||
 | 
					        ui.heading("Transfers");
 | 
				
			||||||
 | 
					        let mut new_uuid: Option<plate_tool_lib::uuid::Uuid> = None;
 | 
				
			||||||
 | 
					        for (name, uuid) in ms.transfers.iter().map(|x| (&x.name, x.get_uuid())) {
 | 
				
			||||||
 | 
					            let (potential_new_uuid, _) = tree_label(
 | 
				
			||||||
 | 
					                ui,
 | 
				
			||||||
 | 
					                ms,
 | 
				
			||||||
 | 
					                modal_state,
 | 
				
			||||||
 | 
					                name,
 | 
				
			||||||
 | 
					                uuid,
 | 
				
			||||||
 | 
					                ms.get_current_transfer_uuid().is_some_and(|x| x == uuid),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            if potential_new_uuid.is_some() {
 | 
				
			||||||
 | 
					                new_uuid = potential_new_uuid;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if let Some(uuid) = new_uuid {
 | 
				
			||||||
 | 
					            ms.set_current_transfer(uuid);
 | 
				
			||||||
 | 
					            if let Some(new_cts) =
 | 
				
			||||||
 | 
					                CurrentTransferStateInterior::try_new_from_main_state_transfer(ms)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                *cts.lock().unwrap() = new_cts;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            set_plates(ms, cts);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn set_plates(ms: &MainState, cts: &CurrentTransferState) {
 | 
				
			||||||
 | 
					    if let Some(mut cts) = cts.lock().ok() {
 | 
				
			||||||
 | 
					        if let Some(source_plate) = ms.get_current_source_plateinstance() {
 | 
				
			||||||
 | 
					            cts.source_plate = source_plate.plate;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if let Some(destination_plate) = ms.get_current_destination_plateinstance() {
 | 
				
			||||||
 | 
					            cts.destination_plate = destination_plate.plate;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -3,8 +3,11 @@ pub mod plate;
 | 
				
			||||||
pub mod plate_instances;
 | 
					pub mod plate_instances;
 | 
				
			||||||
pub mod transfer;
 | 
					pub mod transfer;
 | 
				
			||||||
pub mod transfer_region;
 | 
					pub mod transfer_region;
 | 
				
			||||||
 | 
					pub mod transfer_region_cache;
 | 
				
			||||||
pub mod transfer_volume;
 | 
					pub mod transfer_volume;
 | 
				
			||||||
pub mod util;
 | 
					pub mod util;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
mod well;
 | 
					mod well;
 | 
				
			||||||
pub use well::Well;
 | 
					pub use well::Well;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub use uuid;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,15 @@
 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					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 struct Plate {
 | 
				
			||||||
    pub plate_type: PlateType,
 | 
					    pub plate_type: PlateType,
 | 
				
			||||||
    pub plate_format: PlateFormat,
 | 
					    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 {
 | 
					pub enum PlateType {
 | 
				
			||||||
    Source,
 | 
					    Source,
 | 
				
			||||||
    Destination,
 | 
					    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 {
 | 
					pub enum PlateFormat {
 | 
				
			||||||
    W6,
 | 
					    W6,
 | 
				
			||||||
    W12,
 | 
					    W12,
 | 
				
			||||||
| 
						 | 
					@ -105,4 +114,10 @@ impl PlateFormat {
 | 
				
			||||||
            PlateFormat::W3456 => (48, 72),
 | 
					            PlateFormat::W3456 => (48, 72),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    pub fn rows(&self) -> u8 {
 | 
				
			||||||
 | 
					        self.size().0
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    pub fn columns(&self) -> u8 {
 | 
				
			||||||
 | 
					        self.size().1
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,16 @@
 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use regex::Regex;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use super::plate::Plate;
 | 
					use super::plate::Plate;
 | 
				
			||||||
 | 
					use crate::plate::PlateType;
 | 
				
			||||||
 | 
					use crate::util;
 | 
				
			||||||
use crate::Well;
 | 
					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 {
 | 
					pub struct CustomRegion {
 | 
				
			||||||
    src: Vec<Well>,
 | 
					    src: Vec<Well>,
 | 
				
			||||||
    dest: 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 {
 | 
					pub enum Region {
 | 
				
			||||||
    Rect(Well, Well),
 | 
					    Rect(Well, Well),
 | 
				
			||||||
    Point(Well),
 | 
					    Point(Well),
 | 
				
			||||||
| 
						 | 
					@ -26,6 +33,28 @@ impl Default for Region {
 | 
				
			||||||
        Region::Point(Well { row: 1, col: 1 })
 | 
					        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) {
 | 
					impl TryFrom<Region> for (Well, Well) {
 | 
				
			||||||
    type Error = &'static str;
 | 
					    type Error = &'static str;
 | 
				
			||||||
    fn try_from(region: Region) -> Result<Self, Self::Error> {
 | 
					    fn try_from(region: Region) -> Result<Self, Self::Error> {
 | 
				
			||||||
| 
						 | 
					@ -53,9 +82,76 @@ impl Region {
 | 
				
			||||||
            dest: dest_pts,
 | 
					            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(col1), Some(row1), Some(col2), Some(row2)) = (
 | 
				
			||||||
 | 
					                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(col), Some(row)) = (
 | 
				
			||||||
 | 
					                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 struct TransferRegion {
 | 
				
			||||||
    pub source_plate: Plate,
 | 
					    pub source_plate: Plate,
 | 
				
			||||||
    pub source_region: Region, // Even if it is just a point, we don't want corners.
 | 
					    pub source_region: Region, // Even if it is just a point, we don't want corners.
 | 
				
			||||||
| 
						 | 
					@ -154,24 +250,39 @@ impl TransferRegion {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Non-replicate transfers:
 | 
					        // Non-replicate transfers:
 | 
				
			||||||
        match &self.dest_region {
 | 
					        match &self.dest_region {
 | 
				
			||||||
            Region::Point(Well { row: x, col: y }) => {
 | 
					            Region::Point(dest_point) => {
 | 
				
			||||||
                Box::new(move |Well { row: i, col: j }| {
 | 
					                // Breaking from form somewhat, we really should return an entirely different
 | 
				
			||||||
                    if source_wells.contains(&Well { row: i, col: j }) {
 | 
					                // function if a point-dest can't fit the whole source.
 | 
				
			||||||
 | 
					                // If the bottom-right well of the source won't fit in the dest,
 | 
				
			||||||
 | 
					                // we can abort.
 | 
				
			||||||
 | 
					                let source_bottom_right = match self.source_region {
 | 
				
			||||||
 | 
					                    Region::Point(x) => x,
 | 
				
			||||||
 | 
					                    Region::Rect(w1, w2) => standardize_rectangle(&w1, &w2).1,
 | 
				
			||||||
 | 
					                    Region::Custom(_) => unreachable!("A point destination region cannot be paired with a custom source destination?"),
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					                let bottom_right_mapped = Self::point_destination_region_calc(
 | 
				
			||||||
 | 
					                    source_bottom_right,
 | 
				
			||||||
 | 
					                    source_ul,
 | 
				
			||||||
 | 
					                    il_source,
 | 
				
			||||||
 | 
					                    il_dest,
 | 
				
			||||||
 | 
					                    *dest_point,
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					                if bottom_right_mapped.row > self.dest_plate.plate_format.rows()
 | 
				
			||||||
 | 
					                    || bottom_right_mapped.col > self.dest_plate.plate_format.columns()
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    return Box::new(|_| None);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                Box::new(move |input| {
 | 
				
			||||||
 | 
					                    if source_wells.contains(&input) {
 | 
				
			||||||
                        // Validity here already checked by self.validate()
 | 
					                        // Validity here already checked by self.validate()
 | 
				
			||||||
                        Some(vec![Well {
 | 
					                        Some(vec![Self::point_destination_region_calc(
 | 
				
			||||||
                            row: x + i
 | 
					                            input,
 | 
				
			||||||
                                .checked_sub(source_ul.row)
 | 
					                            source_ul,
 | 
				
			||||||
                                .expect("Point cannot have been less than UL")
 | 
					                            il_source,
 | 
				
			||||||
                                .checked_div(il_source.0.unsigned_abs())
 | 
					                            il_dest,
 | 
				
			||||||
                                .expect("Source interleave cannot be 0")
 | 
					                            *dest_point,
 | 
				
			||||||
                                .mul(il_dest.0.unsigned_abs()),
 | 
					                        )])
 | 
				
			||||||
                            col: y + j
 | 
					 | 
				
			||||||
                                .checked_sub(source_ul.col)
 | 
					 | 
				
			||||||
                                .expect("Point cannot have been less than UL")
 | 
					 | 
				
			||||||
                                .checked_div(il_source.1.unsigned_abs())
 | 
					 | 
				
			||||||
                                .expect("Source interleave cannot be 0")
 | 
					 | 
				
			||||||
                                .mul(il_dest.1.unsigned_abs()),
 | 
					 | 
				
			||||||
                        }])
 | 
					 | 
				
			||||||
                    } else {
 | 
					                    } else {
 | 
				
			||||||
                        None
 | 
					                        None
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
| 
						 | 
					@ -233,8 +344,10 @@ impl TransferRegion {
 | 
				
			||||||
                        let j = j
 | 
					                        let j = j
 | 
				
			||||||
                            .saturating_sub(s_ul.col)
 | 
					                            .saturating_sub(s_ul.col)
 | 
				
			||||||
                            .saturating_div(il_source.1.unsigned_abs());
 | 
					                            .saturating_div(il_source.1.unsigned_abs());
 | 
				
			||||||
                        let checked_il_dest = (u8::max(il_dest.0.unsigned_abs(), 1u8),
 | 
					                        let checked_il_dest = (
 | 
				
			||||||
                                                u8::max(il_dest.1.unsigned_abs(), 1u8));
 | 
					                            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 row_modulus = number_used_src_wells.0 * checked_il_dest.0;
 | 
				
			||||||
                        let column_modulus = number_used_src_wells.1 * checked_il_dest.1;
 | 
					                        let column_modulus = number_used_src_wells.1 * checked_il_dest.1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -256,12 +369,12 @@ impl TransferRegion {
 | 
				
			||||||
                                .filter(|Well { row: x, col: y }| {
 | 
					                                .filter(|Well { row: x, col: y }| {
 | 
				
			||||||
                                    // How many times have we replicated? < How many are we allowed
 | 
					                                    // How many times have we replicated? < How many are we allowed
 | 
				
			||||||
                                    // to replicate?
 | 
					                                    // to replicate?
 | 
				
			||||||
                                    x.checked_sub(d_ul.row).unwrap().div_euclid(
 | 
					                                    x.checked_sub(d_ul.row).unwrap().div_euclid(row_modulus)
 | 
				
			||||||
                                        row_modulus
 | 
					                                        < count.0
 | 
				
			||||||
                                    ) < count.0
 | 
					                                        && y.checked_sub(d_ul.col)
 | 
				
			||||||
                                        && y.checked_sub(d_ul.col).unwrap().div_euclid(
 | 
					                                            .unwrap()
 | 
				
			||||||
                                            column_modulus
 | 
					                                            .div_euclid(column_modulus)
 | 
				
			||||||
                                        ) < count.1
 | 
					                                            < count.1
 | 
				
			||||||
                                })
 | 
					                                })
 | 
				
			||||||
                                .collect(),
 | 
					                                .collect(),
 | 
				
			||||||
                        )
 | 
					                        )
 | 
				
			||||||
| 
						 | 
					@ -289,6 +402,33 @@ impl TransferRegion {
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn point_destination_region_calc(
 | 
				
			||||||
 | 
					        input: Well,
 | 
				
			||||||
 | 
					        source_ul: Well,
 | 
				
			||||||
 | 
					        il_source: (i8, i8),
 | 
				
			||||||
 | 
					        il_dest: (i8, i8),
 | 
				
			||||||
 | 
					        dest_point: Well,
 | 
				
			||||||
 | 
					    ) -> Well {
 | 
				
			||||||
 | 
					        Well {
 | 
				
			||||||
 | 
					            row: dest_point.row
 | 
				
			||||||
 | 
					                + input
 | 
				
			||||||
 | 
					                    .row
 | 
				
			||||||
 | 
					                    .checked_sub(source_ul.row)
 | 
				
			||||||
 | 
					                    .expect("Point cannot have been less than UL")
 | 
				
			||||||
 | 
					                    .checked_div(il_source.0.unsigned_abs())
 | 
				
			||||||
 | 
					                    .expect("Source interleave cannot be 0")
 | 
				
			||||||
 | 
					                    .mul(il_dest.0.unsigned_abs()),
 | 
				
			||||||
 | 
					            col: dest_point.col
 | 
				
			||||||
 | 
					                + input
 | 
				
			||||||
 | 
					                    .col
 | 
				
			||||||
 | 
					                    .checked_sub(source_ul.col)
 | 
				
			||||||
 | 
					                    .expect("Point cannot have been less than UL")
 | 
				
			||||||
 | 
					                    .checked_div(il_source.1.unsigned_abs())
 | 
				
			||||||
 | 
					                    .expect("Source interleave cannot be 0")
 | 
				
			||||||
 | 
					                    .mul(il_dest.1.unsigned_abs()),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub fn validate(&self) -> Result<(), &'static str> {
 | 
					    pub fn validate(&self) -> Result<(), &'static str> {
 | 
				
			||||||
        // Checks if the region does anything suspect
 | 
					        // Checks if the region does anything suspect
 | 
				
			||||||
        //
 | 
					        //
 | 
				
			||||||
| 
						 | 
					@ -302,6 +442,12 @@ impl TransferRegion {
 | 
				
			||||||
        let il_dest = self.interleave_dest;
 | 
					        let il_dest = self.interleave_dest;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        match self.source_region {
 | 
					        match self.source_region {
 | 
				
			||||||
 | 
					            /*
 | 
				
			||||||
 | 
					             * Note 04Jan2025:
 | 
				
			||||||
 | 
					             *  I genuinely cannot think of a reason why we should need to validate a source
 | 
				
			||||||
 | 
					             *  point region???
 | 
				
			||||||
 | 
					             *  Like, why would it *not* be in the plate?
 | 
				
			||||||
 | 
					             */
 | 
				
			||||||
            Region::Point(_) => return Ok(()), // Should make sure it's actually in the plate, leave for
 | 
					            Region::Point(_) => return Ok(()), // Should make sure it's actually in the plate, leave for
 | 
				
			||||||
            // later
 | 
					            // later
 | 
				
			||||||
            Region::Rect(s1, s2) => {
 | 
					            Region::Rect(s1, s2) => {
 | 
				
			||||||
| 
						 | 
					@ -311,16 +457,15 @@ impl TransferRegion {
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                // Sufficient to check if the corners are in-bounds
 | 
					                // Sufficient to check if the corners are in-bounds
 | 
				
			||||||
                let source_max = self.source_plate.size();
 | 
					                let source_max = self.source_plate.size();
 | 
				
			||||||
                if s1.row > source_max.0 || s2.row > source_max.0 {
 | 
					                if (s1.row > source_max.0 || s2.row > source_max.0) && il_dest.0 != 0 {
 | 
				
			||||||
                    return Err("Source region is out-of-bounds! (Too tall)");
 | 
					                    return Err("Source region is out-of-bounds! (Too tall)");
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                if s1.col > source_max.1 || s2.col > source_max.1 {
 | 
					                if (s1.col > source_max.1 || s2.col > source_max.1) && il_dest.1 != 0 {
 | 
				
			||||||
                    // log::debug!("s1.1: {}, max.1: {}", s1.1, source_max.1);
 | 
					 | 
				
			||||||
                    return Err("Source region is out-of-bounds! (Too wide)");
 | 
					                    return Err("Source region is out-of-bounds! (Too wide)");
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if il_dest == (0,0) {
 | 
					                if il_dest == (0, 0) {
 | 
				
			||||||
                    return Err("Refusing to pool both dimensions in a rectangular transfer!\nPlease select a point in the destination plate.")
 | 
					                    return Err("Refusing to pool both dimensions in a rectangular transfer!\nPlease select a point in the destination plate.");
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            Region::Custom(_) => return Ok(()),
 | 
					            Region::Custom(_) => return Ok(()),
 | 
				
			||||||
| 
						 | 
					@ -372,7 +517,7 @@ fn standardize_rectangle(c1: &Well, c2: &Well) -> (Well, Well) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[cfg(debug_assertions)]
 | 
					#[cfg(debug_assertions)]
 | 
				
			||||||
use std::fmt;
 | 
					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
 | 
					#[cfg(debug_assertions)] // There should be no reason to print a transfer otherwise
 | 
				
			||||||
impl fmt::Display for TransferRegion {
 | 
					impl fmt::Display for TransferRegion {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,119 @@
 | 
				
			||||||
 | 
					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 Default for TransferRegionCache {
 | 
				
			||||||
 | 
					    fn default() -> Self {
 | 
				
			||||||
 | 
					        Self::new()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					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_or_calculate_source(&self, tr: &Transfer) -> Option<Arc<[Well]>> {
 | 
				
			||||||
 | 
					        if !self.has(tr) {
 | 
				
			||||||
 | 
					            self.add_overwrite(tr);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        self.get_source(tr)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn get_destination(&self, tr: &Transfer) -> Option<Arc<[Well]>> {
 | 
				
			||||||
 | 
					        self.get(tr, false)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    pub fn get_or_calculate_destination(&self, tr: &Transfer) -> Option<Arc<[Well]>> {
 | 
				
			||||||
 | 
					        if !self.has(tr) {
 | 
				
			||||||
 | 
					            self.add_overwrite(tr);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        self.get_destination(tr)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    fn has(&self, tr: &Transfer) -> bool {
 | 
				
			||||||
 | 
					        if let Ok(interior) = self.interior.lock() {
 | 
				
			||||||
 | 
					            interior.cache.contains_key(&tr.get_uuid())
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            false
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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())
 | 
					    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)]
 | 
					#[cfg(test)]
 | 
				
			||||||
mod tests {
 | 
					mod tests {
 | 
				
			||||||
    use super::{letters_to_num, num_to_letters};
 | 
					    use super::{letters_to_num, num_to_letters};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue