Compare commits

..

5 Commits

Author SHA1 Message Date
Emilia Allison 619f9594cf
lib fix: do not permit out-of-plate destinations 2025-01-11 09:52:25 -05:00
Emilia Allison 2ea2963b22
Edit transfers 2025-01-11 09:51:52 -05:00
Emilia Allison 949822f26c
no direct slice indexing 2025-01-04 20:23:09 -05:00
Emilia Allison 453ad9ed35
0 indexing is a blight 2025-01-04 20:07:19 -05:00
Emilia Allison d296854580
evil mass commit
please stop doing thisssss
2025-01-04 19:22:53 -05:00
11 changed files with 644 additions and 229 deletions

56
Cargo.lock generated
View File

@ -18,6 +18,16 @@ version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046"
[[package]]
name = "accesskit"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3d3b8f9bae46a948369bc4a03e815d4ed6d616bd00de4051133a5019dc31c5a"
dependencies = [
"enumn",
"serde",
]
[[package]]
name = "addr2line"
version = "0.24.2"
@ -42,6 +52,7 @@ dependencies = [
"cfg-if",
"getrandom",
"once_cell",
"serde",
"version_check",
"zerocopy",
]
@ -215,6 +226,12 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "base64"
version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "base64"
version = "0.22.1"
@ -256,6 +273,9 @@ name = "bitflags"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
dependencies = [
"serde",
]
[[package]]
name = "block"
@ -651,6 +671,7 @@ checksum = "7d72e9c39f6e11a2e922d04a34ec5e7ef522ea3f5a1acfca7a19d16ad5fe50f5"
dependencies = [
"bytemuck",
"emath",
"serde",
]
[[package]]
@ -669,6 +690,7 @@ dependencies = [
"glow 0.16.0",
"glutin",
"glutin-winit",
"home",
"image",
"js-sys",
"log",
@ -679,6 +701,8 @@ dependencies = [
"percent-encoding",
"profiling",
"raw-window-handle",
"ron",
"serde",
"static_assertions",
"wasm-bindgen",
"wasm-bindgen-futures",
@ -695,12 +719,15 @@ version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "252d52224d35be1535d7fd1d6139ce071fb42c9097773e79f7665604f5596b5e"
dependencies = [
"accesskit",
"ahash",
"emath",
"epaint",
"log",
"nohash-hasher",
"profiling",
"ron",
"serde",
]
[[package]]
@ -735,6 +762,7 @@ dependencies = [
"log",
"profiling",
"raw-window-handle",
"serde",
"smithay-clipboard",
"web-time",
"webbrowser",
@ -766,6 +794,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4fe73c1207b864ee40aa0b0c038d6092af1030744678c60188a05c28553515d"
dependencies = [
"bytemuck",
"serde",
]
[[package]]
name = "enumn"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
]
[[package]]
@ -807,6 +847,7 @@ dependencies = [
"nohash-hasher",
"parking_lot",
"profiling",
"serde",
]
[[package]]
@ -2425,6 +2466,7 @@ dependencies = [
"env_logger",
"log",
"plate-tool-lib",
"serde",
]
[[package]]
@ -2693,6 +2735,18 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
[[package]]
name = "ron"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
dependencies = [
"base64 0.21.7",
"bitflags 2.6.0",
"serde",
"serde_derive",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
@ -2823,7 +2877,7 @@ version = "3.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817"
dependencies = [
"base64",
"base64 0.22.1",
"chrono",
"hex",
"indexmap 1.9.3",

View File

@ -9,7 +9,9 @@ eframe = { version = "0.30", default-features = false, features = [
"default_fonts",
"glow",
"wayland",
"persistence",
]}
log = "0.4"
env_logger = "0.11"
serde = { version = "1.0", features = ["derive"] }

View File

@ -4,76 +4,14 @@ 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::main_state::{construct_fake_mainstate, MainState};
use crate::plate::{add_plate, 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],
}
}
use crate::tree::tree;
#[non_exhaustive]
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct MainWindowState {
show_side_panel: bool,
}
@ -87,12 +25,15 @@ impl Default for MainWindowState {
}
#[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)]
transfer_menu_state: TransferMenuState,
#[serde(skip)]
transfer_region_cache: plate_tool_lib::transfer_region_cache::TransferRegionCache,
main_state: MainState,
}
@ -115,35 +56,26 @@ impl Default for PlateToolEframe {
impl PlateToolEframe {
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
// Would load state here
if let Some(storage) = cc.storage {
return eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default();
} else {
let pte: PlateToolEframe = Default::default();
pte.transfer_region_cache.generate_cache(&pte.main_state.transfers);
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!()
};
*/
log::info!("Saving state");
eframe::set_value(storage, eframe::APP_KEY, self);
}
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| {
@ -179,11 +111,31 @@ 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, &self.current_transfer_state, &mut self.transfer_menu_state);
tree(ui, &mut self.main_state, &self.current_transfer_state);
ui.separator();
transfer_menu(
ui,
&self.current_transfer_state,
&mut self.transfer_menu_state,
&mut self.main_state,
&self.transfer_region_cache,
);
});
});
}
// 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();
@ -192,7 +144,8 @@ impl eframe::App for PlateToolEframe {
x.y /= 2.0;
x
};
add_grid(
if self.main_state.get_current_source().is_some() {
add_plate(
half_height,
PlateFormat::W96,
&self.main_state.transfers,
@ -203,7 +156,9 @@ impl eframe::App for PlateToolEframe {
ui,
self.source_plate_state.lock().unwrap().deref_mut(),
);
add_grid(
}
if self.main_state.get_current_destination().is_some() {
add_plate(
half_height,
PlateFormat::W96,
&self.main_state.transfers,
@ -214,6 +169,7 @@ impl eframe::App for PlateToolEframe {
ui,
self.destination_plate_state.lock().unwrap().deref_mut(),
);
}
});
});
}

View File

@ -2,4 +2,5 @@ mod app;
mod plate;
mod tree;
mod transfer_menu;
mod main_state;
pub use app::PlateToolEframe;

View File

@ -0,0 +1,123 @@
use plate_tool_lib::plate_instances;
use plate_tool_lib::plate::PlateFormat;
#[derive(Default, Debug, 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>,
}
impl MainState {
pub fn get_current_source(&self) -> Option<plate_tool_lib::uuid::Uuid> {
self.current_source
}
pub fn get_current_destination(&self) -> Option<plate_tool_lib::uuid::Uuid> {
self.current_destination
}
pub fn get_current_transfer(&self) -> Option<plate_tool_lib::uuid::Uuid> {
self.current_transfer
}
pub fn set_current_source(&mut self, id: plate_tool_lib::uuid::Uuid) -> bool {
if self.source_plates.iter().map(|x| x.get_uuid()).find(|x| *x == id).is_some() {
self.current_source = Some(id);
true
} else {
false
}
}
pub fn set_current_destination(&mut self, id: plate_tool_lib::uuid::Uuid) -> bool {
if self.destination_plates.iter().map(|x| x.get_uuid()).find(|x| *x == id).is_some() {
self.current_destination = Some(id);
true
} else {
false
}
}
pub fn set_current_transfer(&mut self, id: plate_tool_lib::uuid::Uuid) -> bool {
if self.transfers.iter().map(|x| x.get_uuid()).find(|x| *x == id).is_some() {
self.current_transfer = Some(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;
}
}
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,
}
}

View File

@ -20,7 +20,7 @@ static STROKE_SELECT: LazyLock<egui::Stroke> =
static STROKE_HOVER: LazyLock<egui::Stroke> =
LazyLock::new(|| egui::Stroke::new(3.0, egui::Color32::from_gray(255)));
#[derive(Clone, Debug)]
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct PlateUiState {
pub drag_start_position: Option<egui::Pos2>,
}
@ -69,8 +69,18 @@ fn get_hover_well(
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)
get_well_from_pos(
response.hover_pos(),
start_x,
start_y,
radius,
rows,
columns,
true,
)
}
fn get_well_from_pos(
@ -78,19 +88,35 @@ fn get_well_from_pos(
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 x < start_x || y < start_y {
// Check bounds if no saturation
if !saturate && (x < start_x || y < start_y || x > max_width || y > max_height) {
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;
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))
})
@ -109,20 +135,27 @@ fn calculate_shading_for_wells(
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),
plate_tool_lib::plate::PlateType::Source => cache.get_or_calculate_source(transfer),
plate_tool_lib::plate::PlateType::Destination => {
cache.get_or_calculate_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]
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 += 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)),
);
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(
5.0,
PALETTE.get_ordered(transfer.id, ordered_ids),
));
}
}
}
}
@ -138,7 +171,7 @@ fn f64_to_color32(x: [f64; 3]) -> Color32 {
Color32::from_rgb(r, g, b)
}
fn add_grid_sub(
fn add_plate_sub(
size: egui::Vec2,
rows: u8,
columns: u8,
@ -166,15 +199,20 @@ fn add_grid_sub(
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);
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: row + 1,
col: col + 1,
});
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();
@ -189,17 +227,8 @@ fn add_grid_sub(
}
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,
});
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 {
@ -209,6 +238,7 @@ fn add_grid_sub(
}
}
}
}
state.drag_start_position = None;
}
@ -226,9 +256,12 @@ fn add_grid_sub(
}
};
let well_infos = {
let mut well_infos = calculate_shading_for_wells(rows, columns, transfers, plate_type, ordered_ids, cache);
// Get non-active transfer info
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)]>> = {
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())
@ -237,27 +270,33 @@ fn add_grid_sub(
.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())
.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 {
let well_info = &mut well_infos[w.0 * columns as usize + w.1];
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())
+ 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
color: [255.0, 255.0, 255.0],
volume: 1.0,
})
}
}
}
well_infos
};
// Plate Frame
painter.rect_stroke(
egui::Rect {
@ -272,11 +311,11 @@ fn add_grid_sub(
);
// Draw wells
for c_row in 0..rows {
for c_column in 0..columns {
for c_row in 1..=rows {
for c_column in 1..=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,
start_x + radius + 2.0 * radius * (c_column - 1) as f32,
start_y + radius + 2.0 * radius * (c_row - 1) as f32,
);
//
@ -284,7 +323,7 @@ fn add_grid_sub(
//
if let Some(well_info) =
well_infos[c_row as usize * columns as usize + c_column as usize]
well_infos[(c_row - 1) as usize * columns as usize + (c_column - 1) as usize]
{
painter.circle_filled(center, radius * 0.80, f64_to_color32(well_info.color));
}
@ -299,8 +338,8 @@ fn add_grid_sub(
if current_selection.as_ref().is_some_and(|x| {
x.contains_well(
&Well {
row: c_row + 1,
col: c_column + 1,
row: c_row,
col: c_column,
},
None,
)
@ -351,7 +390,7 @@ fn add_grid_sub(
}
}
pub fn add_grid(
pub fn add_plate(
size: egui::Vec2,
pf: PlateFormat,
transfers: &Vec<plate_tool_lib::transfer::Transfer>,
@ -362,11 +401,10 @@ pub fn add_grid(
ui: &mut egui::Ui,
state: &mut PlateUiState,
) {
match pf {
PlateFormat::W96 => add_grid_sub(
add_plate_sub(
size,
8,
12,
pf.rows(),
pf.columns(),
transfers,
plate_type,
ordered_ids,
@ -374,31 +412,5 @@ pub fn add_grid(
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,9 +1,12 @@
use eframe::egui;
use plate_tool_lib::transfer_region::{Region, TransferRegion};
use std::sync::{Arc, Mutex};
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,
@ -15,8 +18,11 @@ pub struct CurrentTransferStateInterior {
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 {
@ -93,11 +99,63 @@ impl CurrentTransferStateInterior {
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),
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();
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::debug!("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()?;
let destination_plate_uuid = ms.get_current_destination()?;
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,
@ -127,6 +185,8 @@ pub fn transfer_menu(
ui: &mut egui::Ui,
state: &CurrentTransferState,
ui_state: &mut TransferMenuState,
main_state: &mut MainState,
transfer_region_cache: &plate_tool_lib::transfer_region_cache::TransferRegionCache,
) {
// Can we reduce the length of this lock pls
let mut state = state.lock().unwrap();
@ -220,4 +280,30 @@ pub fn transfer_menu(
);
});
});
ui.horizontal(|ui| {
if ui.button("Save").clicked() {
if let Some(transfer_uuid) = main_state.get_current_transfer() {
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();
transfer_region_cache.invalidate(&transfer);
}
} else {
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");
transfer_region_cache.add_overwrite(new_transfer);
}
}
}
if ui.button("New").clicked() {
*state = CurrentTransferStateInterior::default();
main_state.set_no_current_transfer();
}
});
}

View File

@ -0,0 +1,103 @@
use eframe::egui;
use crate::{main_state::MainState, transfer_menu::{CurrentTransferState, CurrentTransferStateInterior}};
use std::sync::LazyLock;
static SELECT_COLOR: LazyLock<egui::Color32> =
LazyLock::new(|| egui::Color32::from_hex("#aa0000").unwrap());
pub fn tree(ui: &mut egui::Ui, ms: &mut MainState, cts: &CurrentTransferState) {
// 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 f = {
let mut f = egui::Frame::none();
if ms.get_current_source().is_some_and(|x| x == uuid) {
f = f.fill(*SELECT_COLOR);
}
f
};
f.show(ui, |ui| {
let r = ui.add(
egui::Label::new(name)
.sense(egui::Sense::click())
.selectable(false),
);
if r.clicked() {
new_uuid = Some(uuid);
log::info!("{:?}", uuid);
}
});
}
if let Some(uuid) = new_uuid {
ms.set_current_source(uuid);
}
});
// 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 f = {
let mut f = egui::Frame::none();
if ms.get_current_destination().is_some_and(|x| x == uuid) {
f = f.fill(*SELECT_COLOR);
}
f
};
f.show(ui, |ui| {
let r = ui.add(
egui::Label::new(name)
.sense(egui::Sense::click())
.selectable(false),
);
if r.clicked() {
new_uuid = Some(uuid);
log::info!("{:?}", uuid);
}
});
}
if let Some(uuid) = new_uuid {
ms.set_current_destination(uuid);
}
});
// 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 f = {
let mut f = egui::Frame::none();
if ms.get_current_transfer().is_some_and(|x| x == uuid) {
f = f.fill(*SELECT_COLOR);
}
f
};
f.show(ui, |ui| {
let r = ui.add(
egui::Label::new(name)
.sense(egui::Sense::click())
.selectable(false),
);
if r.clicked() {
new_uuid = Some(uuid);
log::info!("{:?}", 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;
}
}
});
}

View File

@ -114,4 +114,10 @@ impl PlateFormat {
PlateFormat::W3456 => (48, 72),
}
}
pub fn rows(&self) -> u8 {
self.size().0
}
pub fn columns(&self) -> u8 {
self.size().1
}
}

View File

@ -250,24 +250,39 @@ impl TransferRegion {
// Non-replicate transfers:
match &self.dest_region {
Region::Point(Well { row: x, col: y }) => {
Box::new(move |Well { row: i, col: j }| {
if source_wells.contains(&Well { row: i, col: j }) {
Region::Point(dest_point) => {
// Breaking from form somewhat, we really should return an entirely different
// function if a point-dest can't fit the whole source.
// If the bottom-right well of the source won't fit in the dest,
// we can abort.
let source_bottom_right = match self.source_region {
Region::Point(x) => x,
Region::Rect(w1, w2) => standardize_rectangle(&w1, &w2).1,
Region::Custom(_) => unreachable!("A point destination region cannot be paired with a custom source destination?"),
};
let bottom_right_mapped = Self::point_destination_region_calc(
source_bottom_right,
source_ul,
il_source,
il_dest,
*dest_point,
);
if bottom_right_mapped.row > self.dest_plate.plate_format.rows()
|| bottom_right_mapped.col > self.dest_plate.plate_format.columns()
{
return Box::new(|_| None);
}
Box::new(move |input| {
if source_wells.contains(&input) {
// Validity here already checked by self.validate()
Some(vec![Well {
row: x + i
.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: 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()),
}])
Some(vec![Self::point_destination_region_calc(
input,
source_ul,
il_source,
il_dest,
*dest_point,
)])
} else {
None
}
@ -387,6 +402,33 @@ impl TransferRegion {
}
}
fn point_destination_region_calc(
input: Well,
source_ul: Well,
il_source: (i8, i8),
il_dest: (i8, i8),
dest_point: Well,
) -> Well {
Well {
row: dest_point.row
+ input
.row
.checked_sub(source_ul.row)
.expect("Point cannot have been less than UL")
.checked_div(il_source.0.unsigned_abs())
.expect("Source interleave cannot be 0")
.mul(il_dest.0.unsigned_abs()),
col: dest_point.col
+ input
.col
.checked_sub(source_ul.col)
.expect("Point cannot have been less than UL")
.checked_div(il_source.1.unsigned_abs())
.expect("Source interleave cannot be 0")
.mul(il_dest.1.unsigned_abs()),
}
}
pub fn validate(&self) -> Result<(), &'static str> {
// Checks if the region does anything suspect
//
@ -400,6 +442,12 @@ impl TransferRegion {
let il_dest = self.interleave_dest;
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
// later
Region::Rect(s1, s2) => {

View File

@ -11,6 +11,11 @@ pub struct TransferRegionCache {
interior: Arc<Mutex<TransferRegionCacheInterior>>,
}
impl Default for TransferRegionCache {
fn default() -> Self {
Self::new()
}
}
impl TransferRegionCache {
pub fn new() -> Self {
TransferRegionCache {
@ -46,10 +51,22 @@ impl TransferRegionCache {
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() {
@ -63,6 +80,13 @@ impl TransferRegionCache {
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 {