Compare commits

..

10 Commits

Author SHA1 Message Date
Emilia Allison bbf420080d
big eframe moment
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
perhaps a "Draw the rest of the owl" commit
2025-01-02 21:21:52 -05:00
Emilia Allison b34b02af89
Init function for the transfer region cache 2025-01-02 21:21:34 -05:00
Emilia Allison 8bf11eecca
Useful impls on transfer_region 2025-01-02 21:21:17 -05:00
Emilia Allison 6b7d657760
Convenience const plate types 2025-01-02 21:20:02 -05:00
Emilia Allison 5b45e50f4f
Add Hash impl to lib plate types 2025-01-02 21:19:51 -05:00
Emilia Allison 438bd6b26c
Export uuid from lib 2025-01-02 21:19:18 -05:00
Emilia Allison 12a8e82015
Transfer region cache draft 2025-01-01 15:44:10 -05:00
Emilia Allison 3982d1a7a1
Move palettes to lib 2025-01-01 14:38:07 -05:00
Emilia Allison 2b1792c2ae
eframe work 2025-01-01 14:37:56 -05:00
Emilia Allison dc3ef4830a
Eframe plate
big if true?
2024-12-24 21:54:36 -05:00
15 changed files with 3600 additions and 32 deletions

2421
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -0,0 +1,15 @@
[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

@ -0,0 +1,220 @@
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

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

View File

@ -0,0 +1,18 @@
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

@ -0,0 +1,404 @@
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

@ -0,0 +1,223 @@
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

View File

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

View File

@ -1,6 +1,15 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(PartialEq, Eq, Default, Clone, Copy, Serialize, Deserialize, Debug)] pub const SOURCE_W96: Plate = Plate {
plate_type: PlateType::Source,
plate_format: PlateFormat::W96,
};
pub const DESTINATION_W96: Plate = Plate {
plate_type: PlateType::Destination,
plate_format: PlateFormat::W96,
};
#[derive(PartialEq, Eq, Default, Clone, Copy, Serialize, Deserialize, Debug, Hash)]
pub struct Plate { pub struct Plate {
pub plate_type: PlateType, pub plate_type: PlateType,
pub plate_format: PlateFormat, pub plate_format: PlateFormat,
@ -19,7 +28,7 @@ impl Plate {
} }
} }
#[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Debug)] #[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Debug, Hash)]
pub enum PlateType { pub enum PlateType {
Source, Source,
Destination, Destination,
@ -30,7 +39,7 @@ impl Default for PlateType {
} }
} }
#[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Debug)] #[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Debug, Hash)]
pub enum PlateFormat { pub enum PlateFormat {
W6, W6,
W12, W12,

View File

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

View File

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