Compare commits

..

No commits in common. "bbf420080da4874d7edd0226c1b1c6ed6dc58fd8" and "0e0d72ec9cbecfacf58abdfeb94d44d127cc5337" have entirely different histories.

15 changed files with 32 additions and 3600 deletions

2421
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,3 @@
[workspace]
members = ["plate-tool-web", "plate-tool-lib", "plate-tool-eframe"]
members = ["plate-tool-web", "plate-tool-lib"]
resolver = "2"

View File

@ -1,2 +0,0 @@
[build]
target = "x86_64-unknown-linux-gnu"

View File

@ -1,15 +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",
]}
log = "0.4"
env_logger = "0.11"

View File

@ -1,220 +0,0 @@
use std::ops::DerefMut;
use std::sync::Mutex;
use eframe::egui::{self};
use plate_tool_lib::plate::PlateFormat;
use plate_tool_lib::plate_instances;
use crate::plate::{add_grid, PlateUiState};
use crate::transfer_menu::{self, transfer_menu, CurrentTransferState, TransferMenuState};
#[derive(Default)]
pub struct MainState {
pub source_plates: Vec<plate_tool_lib::plate_instances::PlateInstance>,
pub destination_plates: Vec<plate_tool_lib::plate_instances::PlateInstance>,
pub transfers: Vec<plate_tool_lib::transfer::Transfer>,
}
fn construct_fake_mainstate() -> MainState {
let src_plate: plate_tool_lib::plate_instances::PlateInstance =
plate_instances::PlateInstance::new(
plate_tool_lib::plate::PlateType::Source,
PlateFormat::W96,
"src1".to_owned(),
);
let dest_plate: plate_tool_lib::plate_instances::PlateInstance =
plate_instances::PlateInstance::new(
plate_tool_lib::plate::PlateType::Destination,
PlateFormat::W96,
"dest1".to_owned(),
);
let well_a1 = plate_tool_lib::Well { row: 1, col: 1 };
let well_c3 = plate_tool_lib::Well { row: 3, col: 3 };
let well_a5 = plate_tool_lib::Well { row: 1, col: 5 };
let transfer1_region: plate_tool_lib::transfer_region::TransferRegion =
plate_tool_lib::transfer_region::TransferRegion {
source_plate: src_plate.plate,
source_region: plate_tool_lib::transfer_region::Region::Rect(well_a1, well_c3),
dest_plate: dest_plate.plate,
dest_region: plate_tool_lib::transfer_region::Region::Point(well_a1),
interleave_source: (1, 1),
interleave_dest: (1, 1),
};
let transfer1: plate_tool_lib::transfer::Transfer = plate_tool_lib::transfer::Transfer::new(
src_plate.clone(),
dest_plate.clone(),
transfer1_region,
"Shrimp".to_owned(),
);
let transfer2_region: plate_tool_lib::transfer_region::TransferRegion =
plate_tool_lib::transfer_region::TransferRegion {
source_plate: src_plate.plate,
source_region: plate_tool_lib::transfer_region::Region::Rect(well_a1, well_c3),
dest_plate: dest_plate.plate,
dest_region: plate_tool_lib::transfer_region::Region::Point(well_a5),
interleave_source: (1, 1),
interleave_dest: (2, 2),
};
let transfer2: plate_tool_lib::transfer::Transfer = plate_tool_lib::transfer::Transfer::new(
src_plate.clone(),
dest_plate.clone(),
transfer2_region,
"Shrimp".to_owned(),
);
MainState {
source_plates: vec![src_plate],
destination_plates: vec![dest_plate],
transfers: vec![transfer1, transfer2],
}
}
#[non_exhaustive]
struct MainWindowState {
show_side_panel: bool,
}
impl Default for MainWindowState {
fn default() -> Self {
Self {
show_side_panel: true,
}
}
}
#[non_exhaustive]
pub struct PlateToolEframe {
source_plate_state: Mutex<PlateUiState>,
destination_plate_state: Mutex<PlateUiState>,
main_window_state: MainWindowState,
current_transfer_state: CurrentTransferState,
transfer_menu_state: TransferMenuState,
transfer_region_cache: plate_tool_lib::transfer_region_cache::TransferRegionCache,
main_state: MainState,
}
impl Default for PlateToolEframe {
fn default() -> Self {
Self {
source_plate_state: Mutex::new(PlateUiState::default()),
destination_plate_state: Mutex::new(PlateUiState::default()),
main_window_state: MainWindowState::default(),
current_transfer_state: CurrentTransferState::default(),
transfer_menu_state: TransferMenuState::default(),
transfer_region_cache: plate_tool_lib::transfer_region_cache::TransferRegionCache::new(
),
main_state: construct_fake_mainstate(),
}
}
}
impl PlateToolEframe {
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
// Would load state here
let pte: PlateToolEframe = Default::default();
pte.transfer_region_cache.generate_cache(&pte.main_state.transfers);
pte
}
}
impl eframe::App for PlateToolEframe {
// State storage
/*
fn save(&mut self, storage: &mut dyn eframe::Storage) {
unimplemented!()
};
*/
fn update(&mut self, ctx: &eframe::egui::Context, _frame: &mut eframe::Frame) {
let ordered_ids: Vec<plate_tool_lib::uuid::Uuid> = {
let mut ids: Vec<plate_tool_lib::uuid::Uuid> = self
.main_state
.transfers
.clone()
.iter()
.map(|x| x.id)
.collect();
ids.sort_unstable();
ids
};
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
ui.menu_button("File", |ui| {
if ui.button("New").clicked() {}
ui.menu_button("Export", |ui| {
if ui.button("Export as CSV").clicked() {}
if ui.button("Export as JSON").clicked() {}
});
ui.menu_button("Import", |ui| {
if ui.button("Import from JSON").clicked() {}
if ui.button("Import transfer from CSV").clicked() {}
});
});
ui.menu_button("Options", |ui| {
ui.menu_button("Styles", |ui| {
if ui.button("Toggle transfer hashes").clicked() {}
if ui.button("Toggle volume heatmap").clicked() {}
if ui.button("Toggle current coordinates view").clicked() {}
});
ui.menu_button("Exports", |ui| {
if ui.button("Change CSV export type").clicked() {}
});
ui.menu_button("Windows", |ui| {
ui.toggle_value(
&mut self.main_window_state.show_side_panel,
"Toggle side panel",
);
});
});
});
});
if self.main_window_state.show_side_panel {
egui::SidePanel::left("side_menus").show(ctx, |ui| {
ui.vertical(|ui| {
transfer_menu(ui, &self.current_transfer_state, &mut self.transfer_menu_state);
});
});
}
egui::CentralPanel::default().show(ctx, |ui| {
ui.vertical(|ui| {
let available_size = ui.available_size();
let half_height = {
let mut x = available_size;
x.y /= 2.0;
x
};
add_grid(
half_height,
PlateFormat::W96,
&self.main_state.transfers,
plate_tool_lib::plate::PlateType::Source,
&ordered_ids,
&self.transfer_region_cache,
Some(&self.current_transfer_state),
ui,
self.source_plate_state.lock().unwrap().deref_mut(),
);
add_grid(
half_height,
PlateFormat::W96,
&self.main_state.transfers,
plate_tool_lib::plate::PlateType::Destination,
&ordered_ids,
&self.transfer_region_cache,
Some(&self.current_transfer_state),
ui,
self.destination_plate_state.lock().unwrap().deref_mut(),
);
});
});
}
}

View File

@ -1,5 +0,0 @@
mod app;
mod plate;
mod tree;
mod transfer_menu;
pub use app::PlateToolEframe;

View File

@ -1,18 +0,0 @@
use eframe::*;
use eframe::egui;
fn main() -> eframe::Result{
env_logger::init();
log::info!("Shrimp!");
let native_options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_title("Shrimp"),
..Default::default()
};
eframe::run_native(
"PlateToolEframe",
native_options,
Box::new(|cc| Ok(Box::new(plate_tool_eframe::PlateToolEframe::new(cc))))
)
}

View File

@ -1,404 +0,0 @@
use eframe::egui::{self, pos2, Color32, Rounding};
use plate_tool_lib::plate::PlateFormat;
use plate_tool_lib::transfer_region::Region;
use plate_tool_lib::uuid::Uuid;
use plate_tool_lib::Well;
use crate::transfer_menu::CurrentTransferState;
use std::sync::LazyLock;
const PALETTE: plate_tool_lib::util::ColorPalette = plate_tool_lib::util::Palettes::RAINBOW;
// Stroke types
static STROKE_DEFAULT: LazyLock<egui::Stroke> =
LazyLock::new(|| egui::Stroke::new(2.0, egui::Color32::from_gray(128)));
static STROKE_CURRENT: LazyLock<egui::Stroke> =
LazyLock::new(|| egui::Stroke::new(2.0, egui::Color32::from_gray(200)));
static STROKE_SELECT: LazyLock<egui::Stroke> =
LazyLock::new(|| egui::Stroke::new(3.0, egui::Color32::from_gray(200)));
static STROKE_HOVER: LazyLock<egui::Stroke> =
LazyLock::new(|| egui::Stroke::new(3.0, egui::Color32::from_gray(255)));
#[derive(Clone, Debug)]
pub struct PlateUiState {
pub drag_start_position: Option<egui::Pos2>,
}
impl Default for PlateUiState {
fn default() -> Self {
Self {
drag_start_position: None,
}
}
}
#[derive(Clone, Copy, Debug)]
struct WellInfo {
volume: f32,
color: [f64; 3],
}
impl WellInfo {
fn new(volume: f32, color: [f64; 3]) -> Self {
WellInfo { volume, color }
}
}
fn calculate_grid_params(
ul_origin: (f32, f32),
tw: f32,
th: f32,
rows: u8,
columns: u8,
) -> (f32, f32, f32) {
// (Start X, Start Y, Radius)
const PADDING: f32 = 20.0;
let usable_width = tw - 2.0 * PADDING;
let usable_height = th - 2.0 * PADDING;
let shared_width = f32::ceil(usable_width / columns as f32);
let shared_height = f32::ceil(usable_height / rows as f32);
let radius = f32::min(shared_width, shared_height) / 2.0;
(ul_origin.0 + PADDING, ul_origin.1 + PADDING, radius)
}
fn get_hover_well(
response: &egui::Response,
start_x: f32,
start_y: f32,
radius: f32,
) -> Option<(u8, u8)> {
get_well_from_pos(response.hover_pos(), start_x, start_y, radius)
}
fn get_well_from_pos(
position: Option<egui::Pos2>,
start_x: f32,
start_y: f32,
radius: f32,
) -> Option<(u8, u8)> {
// Some((row, column))
position
.map(|p| Into::<(f32, f32)>::into(p))
.and_then(|(x, y)| {
// Check bounds
if x < start_x || y < start_y {
return None;
}
// CHECK Bottom Right BOUND
let solved_column: u8 = (x - start_x).div_euclid(radius * 2.0) as u8;
let solved_row: u8 = (y - start_y).div_euclid(radius * 2.0) as u8;
Some((solved_row, solved_column))
})
}
fn calculate_shading_for_wells(
rows: u8,
columns: u8,
transfers: &Vec<plate_tool_lib::transfer::Transfer>,
plate_type: plate_tool_lib::plate::PlateType,
ordered_ids: &[Uuid],
cache: &plate_tool_lib::transfer_region_cache::TransferRegionCache,
) -> Box<[Option<WellInfo>]> {
let box_size: usize = rows as usize * columns as usize;
let mut well_infos: Box<[Option<WellInfo>]> = vec![None; box_size].into_boxed_slice();
for transfer in transfers {
let cache_result = match plate_type {
plate_tool_lib::plate::PlateType::Source => cache.get_source(transfer),
plate_tool_lib::plate::PlateType::Destination => cache.get_destination(transfer),
};
if let Some(wells) = cache_result {
for well in wells.iter().filter(|x| x.row <= rows && x.col <= columns) {
if let Some(mut x) =
well_infos[well.row as usize * columns as usize + well.col as usize]
{
x.volume += 5.0;
x.color = PALETTE.get_ordered(transfer.id, ordered_ids);
} else {
well_infos[well.row as usize * columns as usize + well.col as usize] = Some(
WellInfo::new(5.0, PALETTE.get_ordered(transfer.id, ordered_ids)),
);
}
}
}
}
well_infos
}
fn f64_to_color32(x: [f64; 3]) -> Color32 {
let r = x[0] as u8;
let g = x[1] as u8;
let b = x[2] as u8;
Color32::from_rgb(r, g, b)
}
fn add_grid_sub(
size: egui::Vec2,
rows: u8,
columns: u8,
transfers: &Vec<plate_tool_lib::transfer::Transfer>,
plate_type: plate_tool_lib::plate::PlateType,
ordered_ids: &[Uuid],
cache: &plate_tool_lib::transfer_region_cache::TransferRegionCache,
current_transfer_state: Option<&CurrentTransferState>,
ui: &mut egui::Ui,
state: &mut PlateUiState,
) {
let (response, painter) = ui.allocate_painter(size, egui::Sense::click_and_drag());
let rect = response.rect;
let ul_origin = rect.left_top();
let total_width = rect.width();
let total_height = rect.height();
let (start_x, start_y, radius) =
calculate_grid_params(ul_origin.into(), total_width, total_height, rows, columns);
// Manage clicks and drags
if response.drag_started() {
state.drag_start_position = Some(response.hover_pos().unwrap());
}
let drag_start_well = get_well_from_pos(state.drag_start_position, start_x, start_y, radius);
let hovered_well = get_hover_well(&response, start_x, start_y, radius);
if response.clicked() {
if let Some(cts) = current_transfer_state {
let end_well: Option<Well> = hovered_well.map(|(row, col)| Well {
row: row + 1,
col: col + 1,
});
if let Some(end_well) = end_well {
let new_region = Region::new_from_wells(end_well, None);
let mut cts = cts.lock().unwrap();
match plate_type {
plate_tool_lib::plate::PlateType::Source => cts.source_region = new_region,
plate_tool_lib::plate::PlateType::Destination => {
cts.destination_region = new_region
}
}
}
}
}
if response.drag_stopped() {
if let Some(cts) = current_transfer_state {
let start_well: Well = drag_start_well
.map(|(row, col)| Well {
// Lib uses 1-indexing!
row: row + 1,
col: col + 1,
})
.unwrap();
let end_well: Option<Well> = hovered_well.map(|(row, col)| Well {
row: row + 1,
col: col + 1,
});
let new_region = Region::new_from_wells(start_well, end_well);
let mut cts = cts.lock().unwrap();
match plate_type {
plate_tool_lib::plate::PlateType::Source => cts.source_region = new_region,
plate_tool_lib::plate::PlateType::Destination => {
cts.destination_region = new_region
}
}
}
state.drag_start_position = None;
}
let current_selection: Option<Region> = {
if let Some(cts) = current_transfer_state {
let cts = cts.lock().unwrap();
match plate_type {
plate_tool_lib::plate::PlateType::Source => Some(cts.source_region.clone()),
plate_tool_lib::plate::PlateType::Destination => {
Some(cts.destination_region.clone())
}
}
} else {
None
}
};
let well_infos = {
let mut well_infos = calculate_shading_for_wells(rows, columns, transfers, plate_type, ordered_ids, cache);
// Get wells in the current transfer to tack on to well_infos separately
let current_transfer_wells: Option<Box<[(usize,usize)]>> = {
(match plate_type {
plate_tool_lib::plate::PlateType::Source => current_transfer_state
.and_then(|x| x.lock().ok())
.map(|mut x| x.get_source_wells()),
plate_tool_lib::plate::PlateType::Destination => current_transfer_state
.and_then(|x| x.lock().ok())
.map(|mut x| x.get_destination_wells()),
})
// Drop back to 0-indexing here
.map(|xs| xs.iter().map(|x| (x.row as usize - 1, x.col as usize - 1)).collect())
};
if let Some(wells) = current_transfer_wells {
for w in wells {
let well_info = &mut well_infos[w.0 * columns as usize + w.1];
let volume = well_info.map(|x| x.volume).unwrap_or(0.0)
+ current_transfer_state.and_then(|x| x.lock().ok())
.map(|x| x.volume)
.unwrap_or(0.0);
*well_info = Some(WellInfo {
color: [255.0,255.0,255.0],
volume: 1.0
})
}
}
well_infos
};
// Plate Frame
painter.rect_stroke(
egui::Rect {
min: pos2(start_x, start_y),
max: pos2(
start_x + 2.0 * radius * columns as f32,
start_y + 2.0 * radius * rows as f32,
),
},
Rounding::default(),
*STROKE_DEFAULT,
);
// Draw wells
for c_row in 0..rows {
for c_column in 0..columns {
let center = egui::pos2(
start_x + radius + 2.0 * radius * c_column as f32,
start_y + radius + 2.0 * radius * c_row as f32,
);
//
// Draw fill first
//
if let Some(well_info) =
well_infos[c_row as usize * columns as usize + c_column as usize]
{
painter.circle_filled(center, radius * 0.80, f64_to_color32(well_info.color));
}
//
// Draw stroke on top
//
// Base stroke
painter.circle_stroke(center, radius * 0.80, *STROKE_DEFAULT);
if current_selection.as_ref().is_some_and(|x| {
x.contains_well(
&Well {
row: c_row + 1,
col: c_column + 1,
},
None,
)
}) {
painter.circle_stroke(center, radius * 0.80, *STROKE_CURRENT);
}
if response.dragged() {
if let (Some(hw), Some(dw)) = (hovered_well, drag_start_well) {
if c_column <= u8::max(hw.1, dw.1)
&& c_column >= u8::min(hw.1, dw.1)
&& c_row <= u8::max(hw.0, dw.0)
&& c_row >= u8::min(hw.0, dw.0)
{
painter.circle_stroke(center, radius * 0.80, *STROKE_SELECT);
}
}
}
if Some((c_row, c_column)) == hovered_well {
painter.circle_stroke(center, radius * 0.80, *STROKE_HOVER);
}
}
}
// Draw row/column labels
for c_row in 0..rows {
painter.text(
egui::pos2(
start_x - 10.0,
start_y + radius + 2.0 * radius * c_row as f32,
),
egui::Align2::CENTER_CENTER,
(c_row + 1).to_string(),
egui::FontId::monospace(f32::min(radius, 14.0)),
egui::Color32::from_gray(128),
);
}
for c_column in 0..columns {
painter.text(
egui::pos2(
start_x + radius + 2.0 * radius * c_column as f32,
start_y - 12.0,
),
egui::Align2::CENTER_CENTER,
plate_tool_lib::util::num_to_letters(c_column + 1).unwrap(),
egui::FontId::monospace(f32::min(radius, 14.0)),
egui::Color32::from_gray(128),
);
}
}
pub fn add_grid(
size: egui::Vec2,
pf: PlateFormat,
transfers: &Vec<plate_tool_lib::transfer::Transfer>,
plate_type: plate_tool_lib::plate::PlateType,
ordered_ids: &[Uuid],
cache: &plate_tool_lib::transfer_region_cache::TransferRegionCache,
current_transfer_state: Option<&CurrentTransferState>,
ui: &mut egui::Ui,
state: &mut PlateUiState,
) {
match pf {
PlateFormat::W96 => add_grid_sub(
size,
8,
12,
transfers,
plate_type,
ordered_ids,
cache,
current_transfer_state,
ui,
state,
),
PlateFormat::W384 => add_grid_sub(
size,
16,
24,
transfers,
plate_type,
ordered_ids,
cache,
current_transfer_state,
ui,
state,
),
PlateFormat::W1536 => add_grid_sub(
size,
32,
48,
transfers,
plate_type,
ordered_ids,
cache,
current_transfer_state,
ui,
state,
),
_ => unimplemented!(),
}
}

View File

@ -1,223 +0,0 @@
use eframe::egui;
use plate_tool_lib::transfer_region::{Region, TransferRegion};
use std::sync::{Arc, Mutex};
use std::hash::{DefaultHasher, Hash, Hasher};
pub type CurrentTransferState = Arc<Mutex<CurrentTransferStateInterior>>;
pub struct CurrentTransferStateInterior {
pub transfer_name: String,
pub source_region: plate_tool_lib::transfer_region::Region,
pub destination_region: plate_tool_lib::transfer_region::Region,
pub source_plate: plate_tool_lib::plate::Plate,
pub destination_plate: plate_tool_lib::plate::Plate,
pub source_row_interleave: i8,
pub source_column_interleave: i8,
pub destination_row_interleave: i8,
pub destination_column_interleave: i8,
pub volume: f32,
transfer_hash: Option<u64>,
source_wells: Option<Vec<plate_tool_lib::Well>>,
destination_wells: Option<Vec<plate_tool_lib::Well>>,
}
impl Default for CurrentTransferStateInterior {
fn default() -> Self {
Self {
transfer_name: String::default(),
source_region: plate_tool_lib::transfer_region::Region::Point(plate_tool_lib::Well {
row: 1,
col: 1,
}),
destination_region: plate_tool_lib::transfer_region::Region::Point(
plate_tool_lib::Well { row: 1, col: 1 },
),
source_plate: plate_tool_lib::plate::SOURCE_W96,
destination_plate: plate_tool_lib::plate::DESTINATION_W96,
source_row_interleave: 1,
source_column_interleave: 1,
destination_row_interleave: 1,
destination_column_interleave: 1,
volume: 5.0,
transfer_hash: None,
source_wells: None,
destination_wells: None,
}
}
}
impl CurrentTransferStateInterior {
pub fn get_source_wells(&mut self) -> Box<[plate_tool_lib::Well]> {
if self.source_wells.is_none() {
self.recalulate_wells();
}
self.recalculate_wells_if_needed();
self.source_wells
.clone()
.expect("Source wells must have been calculated by here.")
.into_boxed_slice()
}
pub fn get_destination_wells(&mut self) -> Box<[plate_tool_lib::Well]> {
if self.destination_wells.is_none() {
self.recalulate_wells();
}
self.recalculate_wells_if_needed();
self.destination_wells
.clone()
.expect("Destination wells must have been calculated by here.")
.into_boxed_slice()
}
fn recalculate_wells_if_needed(&mut self) {
if self.transfer_hash.is_none() {
self.recalulate_wells();
}
if self.transfer_hash.unwrap() != self.calculate_hash() {
self.recalulate_wells();
}
}
fn recalulate_wells(&mut self) {
let tr = self.generate_transfer_region();
self.source_wells = Some(tr.get_source_wells());
self.destination_wells = Some(tr.get_destination_wells());
self.transfer_hash = Some(self.calculate_hash());
}
fn calculate_hash(&self) -> u64 {
let mut hasher = DefaultHasher::new();
self.generate_transfer_region().hash(&mut hasher);
hasher.finish()
}
fn generate_transfer_region(&self) -> TransferRegion {
TransferRegion {
source_plate: self.source_plate,
source_region: self.source_region.clone(),
dest_plate: self.destination_plate,
dest_region: self.destination_region.clone(),
interleave_source: (self.source_row_interleave, self.source_column_interleave),
interleave_dest: (self.destination_row_interleave, self.destination_column_interleave),
}
}
}
pub struct TransferMenuState {
pub source_region_string: String,
pub destination_region_string: String,
}
impl Default for TransferMenuState {
fn default() -> Self {
TransferMenuState {
source_region_string: String::new(),
destination_region_string: String::new(),
}
}
}
impl TransferMenuState {
fn new_from_cts(cts: &CurrentTransferState) -> Self {
let cts = cts.lock().unwrap();
let source_region_string = cts.source_region.to_string();
let destination_region_string = cts.destination_region.to_string();
TransferMenuState {
source_region_string,
destination_region_string,
}
}
}
pub fn transfer_menu(
ui: &mut egui::Ui,
state: &CurrentTransferState,
ui_state: &mut TransferMenuState,
) {
// Can we reduce the length of this lock pls
let mut state = state.lock().unwrap();
ui.horizontal(|ui| {
ui.add(egui::Label::new("Name"));
ui.add(
egui::TextEdit::singleline(&mut state.transfer_name)
.hint_text("Transfer Name")
.horizontal_align(egui::Align::Center),
);
});
ui.horizontal(|ui| {
ui.add(egui::Label::new("Source Region"));
let resp = ui.add(
egui::TextEdit::singleline(&mut ui_state.source_region_string)
.hint_text("Source Region")
.char_limit(9)
.horizontal_align(egui::Align::Center),
);
if resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
if let Some(new_region) = Region::try_parse_str(&ui_state.source_region_string) {
state.source_region = new_region;
} else {
log::warn!("Invalid source region entered.")
}
}
if !resp.has_focus() && !resp.lost_focus() {
ui_state.source_region_string = state.source_region.to_string();
}
});
ui.end_row();
ui.horizontal(|ui| {
ui.add(egui::Label::new("Destination Region"));
let resp = ui.add(
egui::TextEdit::singleline(&mut ui_state.destination_region_string)
.hint_text("Destination Region")
.char_limit(9)
.horizontal_align(egui::Align::Center),
);
if resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
if let Some(new_region) = Region::try_parse_str(&ui_state.destination_region_string) {
state.destination_region = new_region;
} else {
log::warn!("Invalid destination region entered.")
}
}
if !resp.has_focus() {
ui_state.destination_region_string = state.destination_region.to_string();
}
});
ui.vertical(|ui| {
ui.label("Source Interleave");
ui.horizontal(|ui| {
ui.label("Row: ");
ui.add(
egui::DragValue::new(&mut state.source_row_interleave)
.fixed_decimals(0)
.range(1..=30)
.speed(0.1),
);
ui.label("Col: ");
ui.add(
egui::DragValue::new(&mut state.source_column_interleave)
.fixed_decimals(0)
.range(1..=30)
.speed(0.1),
);
});
});
ui.vertical(|ui| {
ui.label("Destination Interleave");
ui.horizontal(|ui| {
ui.label("Row: ");
ui.add(
egui::DragValue::new(&mut state.destination_row_interleave)
.fixed_decimals(0)
.range(0..=30)
.speed(0.1),
);
ui.label("Col: ");
ui.add(
egui::DragValue::new(&mut state.destination_column_interleave)
.fixed_decimals(0)
.range(0..=30)
.speed(0.1),
);
});
});
}

View File

@ -3,11 +3,8 @@ pub mod plate;
pub mod plate_instances;
pub mod transfer;
pub mod transfer_region;
pub mod transfer_region_cache;
pub mod transfer_volume;
pub mod util;
mod well;
pub use well::Well;
pub use uuid;

View File

@ -1,15 +1,6 @@
use serde::{Deserialize, Serialize};
pub const SOURCE_W96: Plate = Plate {
plate_type: PlateType::Source,
plate_format: PlateFormat::W96,
};
pub const DESTINATION_W96: Plate = Plate {
plate_type: PlateType::Destination,
plate_format: PlateFormat::W96,
};
#[derive(PartialEq, Eq, Default, Clone, Copy, Serialize, Deserialize, Debug, Hash)]
#[derive(PartialEq, Eq, Default, Clone, Copy, Serialize, Deserialize, Debug)]
pub struct Plate {
pub plate_type: PlateType,
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 {
Source,
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 {
W6,
W12,

View File

@ -1,16 +1,9 @@
use serde::{Deserialize, Serialize};
use regex::Regex;
use super::plate::Plate;
use crate::plate::PlateType;
use crate::util;
use crate::Well;
use std::fmt;
use std::sync::LazyLock;
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug, Hash)]
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
pub struct CustomRegion {
src: 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 {
Rect(Well, Well),
Point(Well),
@ -33,28 +26,6 @@ impl Default for Region {
Region::Point(Well { row: 1, col: 1 })
}
}
impl Display for Region {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Region::Rect(ul, br) => {
write!(
f,
"{}{}:{}{}",
util::num_to_letters(ul.col).unwrap(),
ul.row,
util::num_to_letters(br.col).unwrap(),
br.row
)
}
Region::Point(w) => {
write!(f, "{}{}", util::num_to_letters(w.col).unwrap(), w.row)
}
Region::Custom(..) => {
write!(f, "Custom Region")
}
}
}
}
impl TryFrom<Region> for (Well, Well) {
type Error = &'static str;
fn try_from(region: Region) -> Result<Self, Self::Error> {
@ -82,76 +53,9 @@ impl Region {
dest: dest_pts,
})
}
pub fn new_from_wells(w1: Well, w2: Option<Well>) -> Self {
if w2.is_none() {
Self::Point(w1)
} else {
let w2 = w2.unwrap();
let (w1, w2) = standardize_rectangle(&w1, &w2);
Self::Rect(w1, w2)
}
}
pub fn try_parse_str(input: &str) -> Option<Self> {
static POINT_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"([A-Z,a-z]+)(\d+)").unwrap());
static RECT_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"([A-Z,a-z]+)(\d+):([A-Z,a-z]+)(\d+)").unwrap());
log::info!("{:?}", input);
if let Some(captures) = RECT_REGEX.captures(input) {
if let (Some(row1), Some(col1), Some(row2), Some(col2)) = (
crate::util::letters_to_num(&captures[1]),
captures[2].parse::<u8>().ok(),
crate::util::letters_to_num(&captures[3]),
captures[4].parse::<u8>().ok(),
) {
return Some(Region::Rect(
Well {
row: row1,
col: col1,
},
Well {
row: row2,
col: col2,
},
));
} else {
return None;
}
} else if let Some(captures) = POINT_REGEX.captures(input) {
if let (Some(row), Some(col)) = (
crate::util::letters_to_num(&captures[1]),
captures[2].parse::<u8>().ok(),
) {
return Some(Region::Point(Well { row, col }));
} else {
return None;
}
} else {
None
}
}
pub fn contains_well(&self, w: &Well, pt: Option<PlateType>) -> bool {
match self {
Self::Point(x) => x == w,
Self::Rect(ul, br) => {
w.row <= br.row && w.row >= ul.row && w.col <= br.col && w.col >= ul.col
}
Self::Custom(xs) => match pt {
Some(PlateType::Source) => xs.src.contains(w),
Some(PlateType::Destination) => xs.dest.contains(w),
None => {
unreachable!("Cannot check if custom contains well without knowing the type")
}
},
}
}
}
#[derive(PartialEq, Eq, Clone, Serialize, Deserialize, Debug, Hash)]
#[derive(PartialEq, Eq, Clone, Serialize, Deserialize, Debug)]
pub struct TransferRegion {
pub source_plate: Plate,
pub source_region: Region, // Even if it is just a point, we don't want corners.
@ -329,10 +233,8 @@ impl TransferRegion {
let j = j
.saturating_sub(s_ul.col)
.saturating_div(il_source.1.unsigned_abs());
let checked_il_dest = (
u8::max(il_dest.0.unsigned_abs(), 1u8),
u8::max(il_dest.1.unsigned_abs(), 1u8),
);
let checked_il_dest = (u8::max(il_dest.0.unsigned_abs(), 1u8),
u8::max(il_dest.1.unsigned_abs(), 1u8));
let row_modulus = number_used_src_wells.0 * checked_il_dest.0;
let column_modulus = number_used_src_wells.1 * checked_il_dest.1;
@ -354,12 +256,12 @@ impl TransferRegion {
.filter(|Well { row: x, col: y }| {
// How many times have we replicated? < How many are we allowed
// to replicate?
x.checked_sub(d_ul.row).unwrap().div_euclid(row_modulus)
< count.0
&& y.checked_sub(d_ul.col)
.unwrap()
.div_euclid(column_modulus)
< count.1
x.checked_sub(d_ul.row).unwrap().div_euclid(
row_modulus
) < count.0
&& y.checked_sub(d_ul.col).unwrap().div_euclid(
column_modulus
) < count.1
})
.collect(),
)
@ -417,8 +319,8 @@ impl TransferRegion {
return Err("Source region is out-of-bounds! (Too wide)");
}
if il_dest == (0, 0) {
return Err("Refusing to pool both dimensions in a rectangular transfer!\nPlease select a point in the destination plate.");
if il_dest == (0,0) {
return Err("Refusing to pool both dimensions in a rectangular transfer!\nPlease select a point in the destination plate.")
}
}
Region::Custom(_) => return Ok(()),
@ -470,7 +372,7 @@ fn standardize_rectangle(c1: &Well, c2: &Well) -> (Well, Well) {
#[cfg(debug_assertions)]
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
impl fmt::Display for TransferRegion {

View File

@ -1,95 +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 TransferRegionCache {
pub fn new() -> Self {
TransferRegionCache {
interior: Arc::new(Mutex::new(TransferRegionCacheInterior::new()))
}
}
pub fn add_overwrite(&self, tr: &Transfer) {
let source = tr.transfer_region.get_source_wells();
let destination = tr.transfer_region.get_destination_wells();
let uuid = tr.id;
if let Ok(mut interior) = self.interior.lock() {
interior.cache.insert(uuid, InteriorWellSlices {
source: source.into(),
destination: destination.into(),
});
}
}
pub fn generate_cache(&self, trs: &[Transfer]) {
for tr in trs.iter() {
self.add_overwrite(tr);
}
}
pub fn invalidate(&self, tr: &Transfer) {
if let Ok(mut interior) = self.interior.lock() {
interior.cache.remove(&tr.id);
}
}
pub fn get_source(&self, tr: &Transfer) -> Option<Arc<[Well]>> {
self.get(tr, true)
}
pub fn get_destination(&self, tr: &Transfer) -> Option<Arc<[Well]>> {
self.get(tr, false)
}
fn get(&self, tr: &Transfer, is_source: bool) -> Option<Arc<[Well]>> {
if let Ok(interior) = self.interior.lock() {
interior.cache.get(&tr.id).map(|x|
if is_source {
x.source.clone()
} else {
x.destination.clone()
})
} else {
None
}
}
}
impl Clone for TransferRegionCache {
fn clone(&self) -> Self {
// Clone the interior RC without letting anyone know
// shhhh!
TransferRegionCache {
interior: self.interior.clone(),
}
}
}
#[derive(Debug)]
struct TransferRegionCacheInterior {
cache: HashMap<Uuid, InteriorWellSlices>,
}
impl TransferRegionCacheInterior {
fn new() -> Self {
TransferRegionCacheInterior {
cache: HashMap::new()
}
}
}
#[derive(Debug)]
struct InteriorWellSlices {
source: Arc<[Well]>,
destination: Arc<[Well]>,
}

View File

@ -30,89 +30,6 @@ pub fn num_to_letters(num: u8) -> Option<String> {
Some(text.to_string())
}
//
// Palettes moved from pt-web
//
// Sources:
// https://iquilezles.org/articles/palettes/
// http://dev.thi.ng/gradients/
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct ColorPalette {
a: [f64; 3],
b: [f64; 3],
c: [f64; 3],
d: [f64; 3],
}
impl ColorPalette {
pub fn _new(a: [f64; 3], b: [f64; 3], c: [f64; 3], d: [f64; 3]) -> Self {
ColorPalette { a, b, c, d }
}
pub fn get(&self, t: f64) -> [f64; 3] {
[
(self.a[0] + self.b[0] * f64::cos(std::f64::consts::TAU * (self.c[0] * t + self.d[0])))
* 255.0,
(self.a[1] + self.b[1] * f64::cos(std::f64::consts::TAU * (self.c[1] * t + self.d[1])))
* 255.0,
(self.a[2] + self.b[2] * f64::cos(std::f64::consts::TAU * (self.c[2] * t + self.d[2])))
* 255.0,
]
}
#[allow(dead_code)] // Preserve old implementation for reference
fn get_u8(&self, t: u8) -> [f64; 3] {
assert!(t > 0, "t must be greater than zero!");
self.get((2f64.powi(-(t.ilog2() as i32))) * (t as f64 + 0.5f64) - 1.0f64)
}
// pub fn get_uuid(&self, t: uuid::Uuid) -> [f64; 3] {
// // self.get(t.as_u128() as f64 / (u128::MAX) as f64)
// let mut r = SmallRng::seed_from_u64(t.as_u128() as u64);
// self.get(r.gen_range(0.0..1.0f64))
// }
pub fn get_ordered(&self, t: uuid::Uuid, ordered_uuids: &[uuid::Uuid])
-> [f64; 3] {
let index = ordered_uuids.iter().position(|&x| x == t).expect("uuid must be in list of uuids") + 1;
self.get(Self::space_evenly(index))
}
pub fn get_linear(&self, t: f64, max: f64) -> [f64; 3] {
let scaled = t / max;
self.get(scaled)
}
fn space_evenly(x: usize) -> f64 {
let e: usize = (x.ilog2() + 1) as usize;
let d: usize = 2usize.pow(e as u32);
let n: usize = (2*x + 1) % d;
(n as f64) / (d as f64)
}
}
#[non_exhaustive]
pub struct Palettes;
#[allow(dead_code)]
impl Palettes {
pub const RAINBOW: ColorPalette = ColorPalette {
a: [0.500, 0.500, 0.500],
b: [0.700, 0.700, 0.700],
c: [0.800, 0.800, 0.800],
d: [0.000, 0.333, 0.667],
};
pub const YELLOW_PINK: ColorPalette = ColorPalette {
a: [0.500, 0.500, 0.320],
b: [0.500, 0.500, 0.500],
c: [0.100, 0.500, 0.360],
d: [0.000, 0.000, 0.650],
};
}
#[cfg(test)]
mod tests {
use super::{letters_to_num, num_to_letters};