diff --git a/Cargo.lock b/Cargo.lock index 38e377e..0ea0e54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -97,6 +97,55 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + [[package]] name = "anymap" version = "1.0.0-beta.2" @@ -375,6 +424,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "combine" version = "4.6.7" @@ -713,6 +768,29 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + [[package]] name = "epaint" version = "0.30.0" @@ -1466,6 +1544,12 @@ dependencies = [ "itoa", ] +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "iana-time-zone" version = "0.1.61" @@ -1688,6 +1772,12 @@ dependencies = [ "serde", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" version = "1.0.11" @@ -2332,6 +2422,8 @@ name = "plate-tool-eframe" version = "0.1.0" dependencies = [ "eframe", + "env_logger", + "log", "plate-tool-lib", ] @@ -3115,6 +3207,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.11.0" diff --git a/plate-tool-eframe/Cargo.toml b/plate-tool-eframe/Cargo.toml index f0bc5e9..b86bdab 100644 --- a/plate-tool-eframe/Cargo.toml +++ b/plate-tool-eframe/Cargo.toml @@ -10,3 +10,6 @@ eframe = { version = "0.30", default-features = false, features = [ "glow", "wayland", ]} + +log = "0.4" +env_logger = "0.11" diff --git a/plate-tool-eframe/src/app.rs b/plate-tool-eframe/src/app.rs index 97bc3ae..28ba704 100644 --- a/plate-tool-eframe/src/app.rs +++ b/plate-tool-eframe/src/app.rs @@ -4,9 +4,74 @@ 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, TransferMenuState}; +use crate::transfer_menu::{self, transfer_menu, CurrentTransferState, TransferMenuState}; + +#[derive(Default)] +pub struct MainState { + pub source_plates: Vec, + pub destination_plates: Vec, + pub transfers: Vec, +} + +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 { @@ -23,21 +88,26 @@ impl Default for MainWindowState { #[non_exhaustive] pub struct PlateToolEframe { - format: PlateFormat, source_plate_state: Mutex, destination_plate_state: Mutex, 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 { - format: PlateFormat::W96, 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(), } } } @@ -46,7 +116,10 @@ impl PlateToolEframe { pub fn new(cc: &eframe::CreationContext<'_>) -> Self { // Would load state here - Default::default() + let pte: PlateToolEframe = Default::default(); + pte.transfer_region_cache.generate_cache(&pte.main_state.transfers); + + pte } } @@ -59,39 +132,45 @@ impl eframe::App for PlateToolEframe { */ fn update(&mut self, ctx: &eframe::egui::Context, _frame: &mut eframe::Frame) { + let ordered_ids: Vec = { + let mut ids: Vec = 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() { - } + if ui.button("New").clicked() {} ui.menu_button("Export", |ui| { - if ui.button("Export as CSV").clicked() { - } - if ui.button("Export as JSON").clicked() { - } + 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("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() { - } + 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() { - } + 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"); + ui.toggle_value( + &mut self.main_window_state.show_side_panel, + "Toggle side panel", + ); }); }); }); @@ -100,7 +179,7 @@ impl eframe::App for PlateToolEframe { if self.main_window_state.show_side_panel { egui::SidePanel::left("side_menus").show(ctx, |ui| { ui.vertical(|ui| { - transfer_menu(ui, &mut self.transfer_menu_state); + transfer_menu(ui, &self.current_transfer_state, &mut self.transfer_menu_state); }); }); } @@ -115,13 +194,23 @@ impl eframe::App for PlateToolEframe { }; add_grid( half_height, - PlateFormat::W1536, + 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::W1536, + 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(), ); diff --git a/plate-tool-eframe/src/main.rs b/plate-tool-eframe/src/main.rs index d6e7cab..bec9a89 100644 --- a/plate-tool-eframe/src/main.rs +++ b/plate-tool-eframe/src/main.rs @@ -2,6 +2,9 @@ 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"), diff --git a/plate-tool-eframe/src/plate.rs b/plate-tool-eframe/src/plate.rs index d0fea69..547dd1e 100644 --- a/plate-tool-eframe/src/plate.rs +++ b/plate-tool-eframe/src/plate.rs @@ -1,8 +1,25 @@ 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 = + LazyLock::new(|| egui::Stroke::new(2.0, egui::Color32::from_gray(128))); +static STROKE_CURRENT: LazyLock = + LazyLock::new(|| egui::Stroke::new(2.0, egui::Color32::from_gray(200))); +static STROKE_SELECT: LazyLock = + LazyLock::new(|| egui::Stroke::new(3.0, egui::Color32::from_gray(200))); +static STROKE_HOVER: LazyLock = + LazyLock::new(|| egui::Stroke::new(3.0, egui::Color32::from_gray(255))); + #[derive(Clone, Debug)] pub struct PlateUiState { pub drag_start_position: Option, @@ -19,180 +36,369 @@ impl Default for PlateUiState { #[derive(Clone, Copy, Debug)] struct WellInfo { volume: f32, - color: [f32; 3], + color: [f64; 3], } -pub fn add_grid(size: egui::Vec2, pf: PlateFormat, +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, + 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, - ui: &mut egui::Ui, state: &mut PlateUiState) { - 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; + plate_type: plate_tool_lib::plate::PlateType, + ordered_ids: &[Uuid], + cache: &plate_tool_lib::transfer_region_cache::TransferRegionCache, +) -> Box<[Option]> { + let box_size: usize = rows as usize * columns as usize; + let mut well_infos: Box<[Option]> = vec![None; box_size].into_boxed_slice(); - 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, - 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, - ) -> Box<[Option]> { - let box_size: usize = rows as usize * columns as usize; - let mut well_infos: Box<[Option]> = vec![None; box_size].into_boxed_slice(); - - well_infos - } - - fn add_grid_sub( - size: egui::Vec2, - rows: u8, - columns: u8, - transfers: &Vec, - 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 stroke = egui::Stroke::new(2.0, egui::Color32::from_gray(128)); - 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()); - } - if response.drag_stopped() { - state.drag_start_position = None; - } - - 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); - - // 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, - ); - - // 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 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_filled(center, radius, Color32::DARK_BLUE); - } - } + 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 { - if Some((c_row, c_column)) == hovered_well { - painter.circle_filled(center, radius, Color32::RED); - } + 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)), + ); } - - // Draw stroke on top - painter.circle_stroke(center, radius - 2.0, stroke); } } + } - // 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), - ); + 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_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 = 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 = 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 = { + 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> = { + (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 { - 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), + 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_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, ui, state), - PlateFormat::W384 => add_grid_sub(size, 16, 24, transfers, ui, state), - PlateFormat::W1536 => add_grid_sub(size, 32, 48, transfers, ui, state), + 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!(), } } diff --git a/plate-tool-eframe/src/transfer_menu.rs b/plate-tool-eframe/src/transfer_menu.rs index 9fd3f2a..9c242aa 100644 --- a/plate-tool-eframe/src/transfer_menu.rs +++ b/plate-tool-eframe/src/transfer_menu.rs @@ -1,26 +1,136 @@ -use eframe::egui::{self, pos2, Color32, Rounding}; +use eframe::egui; +use plate_tool_lib::transfer_region::{Region, TransferRegion}; +use std::sync::{Arc, Mutex}; +use std::hash::{DefaultHasher, Hash, Hasher}; -pub struct TransferMenuState { +pub type CurrentTransferState = Arc>; +pub struct CurrentTransferStateInterior { pub transfer_name: String, - pub source_region: String, - pub destination_region: String, - pub source_row_interleave: f32, - pub source_column_interleave: f32, + 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, + source_wells: Option>, + destination_wells: Option>, } - -impl Default for TransferMenuState { +impl Default for CurrentTransferStateInterior { fn default() -> Self { Self { transfer_name: String::default(), - source_region: String::default(), - destination_region: String::default(), - source_row_interleave: 1.0, - source_column_interleave: 1.0, + 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 transfer_menu(ui: &mut egui::Ui, state: &mut TransferMenuState) { +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( @@ -32,36 +142,82 @@ pub fn transfer_menu(ui: &mut egui::Ui, state: &mut TransferMenuState) { ui.horizontal(|ui| { ui.add(egui::Label::new("Source Region")); - ui.add( - egui::TextEdit::singleline(&mut state.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")); - ui.add( - egui::TextEdit::singleline(&mut state.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(0..=30)); + 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(0..=30)); + 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), + ); }); }); }