Compare commits
3 Commits
feature/ef
...
beta-relea
Author | SHA1 | Date |
---|---|---|
Emilia Allison | 53336c410c | |
Emilia Allison | 711af96d29 | |
Emilia Allison | f7c3069145 |
File diff suppressed because it is too large
Load Diff
|
@ -1,3 +1,3 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["plate-tool-web", "plate-tool-lib", "plate-tool-eframe"]
|
members = ["plate-tool-web", "plate-tool-lib"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
[build]
|
|
||||||
target = "x86_64-unknown-linux-gnu"
|
|
|
@ -1,2 +0,0 @@
|
||||||
/target
|
|
||||||
/dist
|
|
|
@ -1,29 +0,0 @@
|
||||||
[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
|
|
|
@ -1,3 +0,0 @@
|
||||||
[build]
|
|
||||||
target = "index.html"
|
|
||||||
release = true
|
|
|
@ -1,53 +0,0 @@
|
||||||
<!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>
|
|
|
@ -1,261 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
mod app;
|
|
||||||
mod plate;
|
|
||||||
mod tree;
|
|
||||||
mod transfer_menu;
|
|
||||||
mod main_state;
|
|
||||||
mod modals;
|
|
||||||
mod styling;
|
|
||||||
pub use app::PlateToolEframe;
|
|
|
@ -1,48 +0,0 @@
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,248 +0,0 @@
|
||||||
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(),
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,235 +0,0 @@
|
||||||
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(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,515 +0,0 @@
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,135 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
|
@ -1,323 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,185 +0,0 @@
|
||||||
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,11 +3,8 @@ 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,15 +1,6 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub const SOURCE_W96: Plate = Plate {
|
#[derive(PartialEq, Eq, Default, Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
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,
|
||||||
|
@ -28,7 +19,7 @@ impl Plate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Debug, Hash)]
|
#[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
pub enum PlateType {
|
pub enum PlateType {
|
||||||
Source,
|
Source,
|
||||||
Destination,
|
Destination,
|
||||||
|
@ -39,7 +30,7 @@ impl Default for PlateType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Debug, Hash)]
|
#[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
pub enum PlateFormat {
|
pub enum PlateFormat {
|
||||||
W6,
|
W6,
|
||||||
W12,
|
W12,
|
||||||
|
@ -114,10 +105,4 @@ 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,16 +1,9 @@
|
||||||
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;
|
||||||
|
|
||||||
use std::fmt;
|
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
|
||||||
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>,
|
||||||
|
@ -22,7 +15,7 @@ impl CustomRegion {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug, Hash)]
|
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
|
||||||
pub enum Region {
|
pub enum Region {
|
||||||
Rect(Well, Well),
|
Rect(Well, Well),
|
||||||
Point(Well),
|
Point(Well),
|
||||||
|
@ -33,28 +26,6 @@ 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> {
|
||||||
|
@ -82,76 +53,9 @@ 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, Hash)]
|
#[derive(PartialEq, Eq, Clone, Serialize, Deserialize, Debug)]
|
||||||
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.
|
||||||
|
@ -250,39 +154,24 @@ impl TransferRegion {
|
||||||
|
|
||||||
// Non-replicate transfers:
|
// Non-replicate transfers:
|
||||||
match &self.dest_region {
|
match &self.dest_region {
|
||||||
Region::Point(dest_point) => {
|
Region::Point(Well { row: x, col: y }) => {
|
||||||
// Breaking from form somewhat, we really should return an entirely different
|
Box::new(move |Well { row: i, col: j }| {
|
||||||
// function if a point-dest can't fit the whole source.
|
if source_wells.contains(&Well { row: i, col: j }) {
|
||||||
// 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![Self::point_destination_region_calc(
|
Some(vec![Well {
|
||||||
input,
|
row: x + i
|
||||||
source_ul,
|
.checked_sub(source_ul.row)
|
||||||
il_source,
|
.expect("Point cannot have been less than UL")
|
||||||
il_dest,
|
.checked_div(il_source.0.unsigned_abs())
|
||||||
*dest_point,
|
.expect("Source interleave cannot be 0")
|
||||||
)])
|
.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
|
||||||
}
|
}
|
||||||
|
@ -344,10 +233,8 @@ 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 = (
|
let checked_il_dest = (u8::max(il_dest.0.unsigned_abs(), 1u8),
|
||||||
u8::max(il_dest.0.unsigned_abs(), 1u8),
|
u8::max(il_dest.1.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;
|
||||||
|
|
||||||
|
@ -369,12 +256,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(row_modulus)
|
x.checked_sub(d_ul.row).unwrap().div_euclid(
|
||||||
< count.0
|
row_modulus
|
||||||
&& y.checked_sub(d_ul.col)
|
) < count.0
|
||||||
.unwrap()
|
&& y.checked_sub(d_ul.col).unwrap().div_euclid(
|
||||||
.div_euclid(column_modulus)
|
column_modulus
|
||||||
< count.1
|
) < count.1
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
)
|
)
|
||||||
|
@ -402,33 +289,6 @@ 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
|
||||||
//
|
//
|
||||||
|
@ -442,12 +302,6 @@ 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) => {
|
||||||
|
@ -457,15 +311,16 @@ 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) && il_dest.0 != 0 {
|
if s1.row > source_max.0 || s2.row > source_max.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) && il_dest.1 != 0 {
|
if s1.col > source_max.1 || s2.col > source_max.1 {
|
||||||
|
// 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(()),
|
||||||
|
@ -517,7 +372,7 @@ fn standardize_rectangle(c1: &Well, c2: &Well) -> (Well, Well) {
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::{fmt::Display, ops::Mul};
|
use std::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 {
|
||||||
|
|
|
@ -1,119 +0,0 @@
|
||||||
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,89 +30,6 @@ 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};
|
||||||
|
|
|
@ -6,13 +6,17 @@ div.plate_container {
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-evenly;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
border: 2px solid $color-dark;
|
border: 2px solid $color-dark;
|
||||||
grid-column: right / right;
|
grid-column: right / right;
|
||||||
grid-row: upper / 3;
|
grid-row: upper / 3;
|
||||||
|
|
||||||
|
@media (min-height: 900px) {
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
margin-bottom: 1%;
|
margin-bottom: 1%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
|
@ -53,6 +53,41 @@ td.current_select div.plate_cell_inner {
|
||||||
|
|
||||||
|
|
||||||
// Styles for specific plate types:
|
// Styles for specific plate types:
|
||||||
|
.W96 {
|
||||||
|
th {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 0px;
|
||||||
|
}
|
||||||
|
tr:first-child {
|
||||||
|
th {
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
td.current_select div.plate_cell_inner {
|
||||||
|
border: 2px solid black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.W384 {
|
||||||
|
th {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 0px;
|
||||||
|
}
|
||||||
|
tr:first-child {
|
||||||
|
th {
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
td.current_select div.plate_cell_inner {
|
||||||
|
border: 2px solid black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.W1536 {
|
.W1536 {
|
||||||
th {
|
th {
|
||||||
|
|
|
@ -13,6 +13,8 @@ div.tree {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border: 2px solid $color-dark;
|
border: 2px solid $color-dark;
|
||||||
|
|
||||||
|
overflow: scroll;
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue