Compare commits

...

6 Commits

Author SHA1 Message Date
Emilia Allison 62f5291840
Don't eradicate state when I add a new option 2025-11-25 22:29:22 -05:00
Emilia Allison c28ec328f9
Permit toggling well volume display 2025-11-25 22:25:29 -05:00
Emilia Allison 844bd14b4f
Evil bug preventing volumes from adding 2025-11-25 22:22:00 -05:00
Emilia Allison 36d075b49a
Delete transfer button 2025-11-25 22:19:03 -05:00
Emilia Allison 31066dd6f6
Show total volume as text on each well 2025-11-25 21:38:54 -05:00
Emilia Allison 50052fc88d
Small fixes for wasm version
Also, the vibe coded wasm versions of file picking seem to work?
big if true
2025-11-25 21:17:40 -05:00
7 changed files with 181 additions and 43 deletions

23
Cargo.lock generated
View File

@ -1473,6 +1473,7 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97563d71863fb2824b2e974e754a81d19c4a7ec47b09ced8a0e6656b6d54bd1f" checksum = "97563d71863fb2824b2e974e754a81d19c4a7ec47b09ced8a0e6656b6d54bd1f"
dependencies = [ dependencies = [
"futures-channel",
"gloo-events 0.2.0", "gloo-events 0.2.0",
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
@ -1620,6 +1621,8 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
dependencies = [ dependencies = [
"futures-channel",
"futures-core",
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
] ]
@ -2372,7 +2375,7 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
dependencies = [ dependencies = [
"proc-macro-crate 1.3.1", "proc-macro-crate 3.4.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.87", "syn 2.0.87",
@ -2794,10 +2797,16 @@ version = "0.1.0"
dependencies = [ dependencies = [
"eframe", "eframe",
"env_logger", "env_logger",
"gloo-file 0.3.0",
"gloo-timers 0.3.0",
"js-sys",
"log", "log",
"plate-tool-lib", "plate-tool-lib",
"poll-promise",
"rfd", "rfd",
"serde", "serde",
"serde_json",
"wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys", "web-sys",
] ]
@ -2854,6 +2863,18 @@ dependencies = [
"miniz_oxide", "miniz_oxide",
] ]
[[package]]
name = "poll-promise"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f6a58fecbf9da8965bcdb20ce4fd29788d1acee68ddbb64f0ba1b81bccdb7df"
dependencies = [
"document-features",
"static_assertions",
"wasm-bindgen",
"wasm-bindgen-futures",
]
[[package]] [[package]]
name = "polling" name = "polling"
version = "3.7.4" version = "3.7.4"

View File

@ -23,7 +23,20 @@ rfd = "0.15"
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen-futures = "0.4" wasm-bindgen-futures = "0.4"
web-sys = "0.3" web-sys = { version = "0.3", features = [
"Window",
"Document",
"HtmlInputElement",
"HtmlAnchorElement",
"FileList",
"Blob",
"Url"
] }
wasm-bindgen = "0.2"
js-sys = "0.3"
gloo-file = { version = "0.3", features = ["futures"] }
gloo-timers = { version = "0.3", features = ["futures"] }
poll-promise = { version = "0.3", features = ["web"] }
#[profile.release] #[profile.release]
#opt-level = 2 #opt-level = 2

View File

@ -16,8 +16,10 @@ use crate::transfer_menu::{transfer_menu, CurrentTransferState, TransferMenuStat
use crate::tree::tree; use crate::tree::tree;
use crate::upper_menu; use crate::upper_menu;
// Make sure all fields are either skipped by serde or impl default!
#[non_exhaustive] #[non_exhaustive]
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct MainWindowState { pub struct MainWindowState {
pub show_side_panel: bool, pub show_side_panel: bool,
pub plate_display_options: PlateDisplayOptions, pub plate_display_options: PlateDisplayOptions,

View File

@ -1,4 +1,3 @@
#[cfg(not(target_arch = "wasm32"))]
use crate::app; use crate::app;
// Native case, use rfd // Native case, use rfd
@ -49,7 +48,7 @@ pub fn upload_file(filetype: &str, extension: &str) -> app::FileUploadPromise {
// Web case, use web_sys // Web case, use web_sys
// Not tested yet!! // Not tested yet!!
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
pub fn save_file(default_filename: Option<&str>, data: &[u8]) { pub fn save_file(data: &[u8], default_filename: Option<&str>) {
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
use web_sys::{Blob, HtmlAnchorElement, Url}; use web_sys::{Blob, HtmlAnchorElement, Url};
@ -71,3 +70,43 @@ pub fn save_file(default_filename: Option<&str>, data: &[u8]) {
Url::revoke_object_url(&url).unwrap(); Url::revoke_object_url(&url).unwrap();
} }
#[cfg(target_arch = "wasm32")]
pub fn upload_file(filetype: &str, extension: &str) -> app::FileUploadPromise {
use poll_promise;
use wasm_bindgen::JsCast;
use web_sys::{window, HtmlInputElement};
use gloo_file::futures::read_as_bytes;
let extension = extension.to_owned();
app::FileUploadPromise(poll_promise::Promise::spawn_local(async move {
let document = window()?.document()?;
let input: HtmlInputElement = document
.create_element("input")
.ok()?
.dyn_into()
.ok()?;
input.set_type("file");
input.set_accept(&format!(".{}", extension));
input.click();
// Wait for file to be selected
let file_list = loop {
if let Some(files) = input.files() {
if files.length() > 0 {
break files;
}
}
gloo_timers::future::TimeoutFuture::new(100).await;
};
let file = file_list.get(0)?;
let data = read_as_bytes(&file.into()).await.ok()?;
log::info!("Read {} bytes", data.len());
Some(data)
}))
}

View File

@ -5,6 +5,7 @@ use plate_tool_lib::transfer_region::Region;
use plate_tool_lib::uuid::Uuid; use plate_tool_lib::uuid::Uuid;
use plate_tool_lib::Well; use plate_tool_lib::Well;
use crate::main_state;
use crate::transfer_menu::CurrentTransferState; use crate::transfer_menu::CurrentTransferState;
use std::sync::LazyLock; use std::sync::LazyLock;
@ -54,8 +55,10 @@ impl WellInfo {
} }
#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct PlateDisplayOptions { pub struct PlateDisplayOptions {
pub show_transfer_hashes: bool, pub show_transfer_hashes: bool,
pub show_well_volumes: bool,
pub show_volume_heatmap: bool, pub show_volume_heatmap: bool,
pub show_coordinates: bool, pub show_coordinates: bool,
} }
@ -63,6 +66,7 @@ impl Default for PlateDisplayOptions {
fn default() -> Self { fn default() -> Self {
Self { Self {
show_transfer_hashes: true, show_transfer_hashes: true,
show_well_volumes: true,
show_volume_heatmap: false, show_volume_heatmap: false,
show_coordinates: true, show_coordinates: true,
} }
@ -176,37 +180,48 @@ fn calculate_shading_for_wells(
cache.get_or_calculate_destination(transfer) cache.get_or_calculate_destination(transfer)
} }
}; };
let volume = if let plate_tool_lib::transfer_volume::TransferVolume::Single(x) =
transfer.volume
{
x
} else {
0.0
};
if let Some(wells) = cache_result { if let Some(wells) = cache_result {
for well in wells.iter().filter(|x| x.row <= rows && x.col <= columns) { for well in wells.iter().filter(|x| x.row <= rows && x.col <= columns) {
if let Some(Some(mut x)) = well_infos.get_mut( let volume = match &transfer.volume {
plate_tool_lib::transfer_volume::TransferVolume::Single(x) => *x,
plate_tool_lib::transfer_volume::TransferVolume::WellMap(wm) => {
match plate_type {
plate_tool_lib::plate::PlateType::Source => wm.source_only[well],
plate_tool_lib::plate::PlateType::Destination => {
wm.destination_only[well]
}
}
}
_ => 0.0,
};
if let Some(well_info_slot) = well_infos.get_mut(
(well.row - 1) as usize * columns as usize + (well.col - 1) as usize, (well.row - 1) as usize * columns as usize + (well.col - 1) as usize,
) { ) {
x.volume += volume; if let Some(ref mut x) = well_info_slot {
// Well info already existed.
x.volume += volume;
x.color = if display_options.show_volume_heatmap { x.color = if display_options.show_volume_heatmap {
PALETTE.get_linear(volume.into(), max_volume.into()) PALETTE.get_linear(x.volume.into(), max_volume.into())
} else {
PALETTE.get_ordered(transfer.id, ordered_ids)
};
} else { } else {
PALETTE.get_ordered(transfer.id, ordered_ids) // Well info does not already exist, we need to make it.
}; if let Some(mut wi) = well_infos.get_mut(
} else { (well.row - 1) as usize * columns as usize
if let Some(mut wi) = well_infos.get_mut( + (well.col - 1) as usize,
(well.row - 1) as usize * columns as usize + (well.col - 1) as usize, ) {
) { *wi = Some(WellInfo::new(
*wi = Some(WellInfo::new( volume,
volume, if display_options.show_volume_heatmap {
if display_options.show_volume_heatmap { PALETTE.get_linear(volume.into(), max_volume.into())
PALETTE.get_linear(volume.into(), max_volume.into()) } else {
} else { PALETTE.get_ordered(transfer.id, ordered_ids)
PALETTE.get_ordered(transfer.id, ordered_ids) },
}, ));
)); }
} }
} }
} }
@ -325,8 +340,15 @@ fn add_plate_sub(
}; };
let well_infos = { let well_infos = {
// Get non-active transfer info // Get non-active transfer info
let mut well_infos = let mut well_infos = calculate_shading_for_wells(
calculate_shading_for_wells(rows, columns, transfers, plate_type, ordered_ids, cache, display_options); rows,
columns,
transfers,
plate_type,
ordered_ids,
cache,
display_options,
);
// Get wells in the current transfer to tack on to well_infos separately // 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)]>> = {
@ -349,17 +371,13 @@ fn add_plate_sub(
if let Some(mut well_info) = if let Some(mut well_info) =
well_infos.get_mut((w.0 - 1) * columns as usize + (w.1 - 1)) 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) let volume = well_info.map(|x| x.volume).unwrap_or(0.0);
+ current_transfer_state
.and_then(|x| x.lock().ok())
.map(|x| x.volume)
.unwrap_or(0.0);
let color = well_info.map(|x| x.color).unwrap_or([255.0, 255.0, 255.0]); let color = well_info.map(|x| x.color).unwrap_or([255.0, 255.0, 255.0]);
let fill = well_info.map(|x| x.color).is_some(); let fill = well_info.map(|x| x.color).is_some();
*well_info = Some(WellInfo { *well_info = Some(WellInfo {
color, color,
volume: 1.0, volume: volume,
fill, fill,
highlight: true, highlight: true,
}) })
@ -406,6 +424,22 @@ fn add_plate_sub(
} }
} }
// Draw volume text
if display_options.show_well_volumes {
if let Some(well_info) =
well_infos[(c_row - 1) as usize * columns as usize + (c_column - 1) as usize]
{
painter.text(
center,
egui::Align2::CENTER_CENTER,
format!("{:.1}", well_info.volume),
egui::FontId::proportional(radius * 0.6),
egui::Color32::BLACK,
);
}
}
// //
// Draw stroke on top // Draw stroke on top
// //

View File

@ -116,9 +116,7 @@ impl CurrentTransferStateInterior {
if let Some(transfer) = transfer { if let Some(transfer) = transfer {
let volume: f32 = match transfer.volume { let volume: f32 = match transfer.volume {
plate_tool_lib::transfer_volume::TransferVolume::Single(x) => x, plate_tool_lib::transfer_volume::TransferVolume::Single(x) => x,
plate_tool_lib::transfer_volume::TransferVolume::WellMap(_) => { plate_tool_lib::transfer_volume::TransferVolume::WellMap(_) => f32::NAN,
f32::NAN
}
_ => unreachable!(), _ => unreachable!(),
}; };
return Some(Self { return Some(Self {
@ -321,6 +319,13 @@ pub fn transfer_menu(
{ {
transfer.transfer_region = state.generate_transfer_region(); transfer.transfer_region = state.generate_transfer_region();
transfer.name = state.transfer_name.clone(); transfer.name = state.transfer_name.clone();
if matches!(
transfer.volume,
plate_tool_lib::transfer_volume::TransferVolume::Single(_)
) {
transfer.volume =
plate_tool_lib::transfer_volume::TransferVolume::Single(state.volume);
}
main_state.transfer_region_cache.invalidate(&transfer); main_state.transfer_region_cache.invalidate(&transfer);
} }
} else { } else {
@ -346,6 +351,21 @@ pub fn transfer_menu(
set_plates(main_state, &mut state); set_plates(main_state, &mut state);
main_state.set_no_current_transfer(); main_state.set_no_current_transfer();
} }
if ui.button("Delete").clicked() {
if let Some(transfer_uuid) = main_state.get_current_transfer_uuid() {
if let Some(index) = main_state
.transfers
.iter()
.position(|x| x.id == transfer_uuid)
{
let removed_transfer = main_state.transfers.remove(index);
main_state.transfer_region_cache.invalidate(&removed_transfer);
}
}
*state = CurrentTransferStateInterior::default();
set_plates(main_state, &mut state);
main_state.set_no_current_transfer();
}
}); });
} }

View File

@ -127,7 +127,10 @@ fn render_export_menu(
}; };
if let Ok(data) = data { if let Ok(data) = data {
let bytes: &[u8] = data.as_bytes(); let bytes: &[u8] = data.as_bytes();
log::info!("There are {} bytes to be written to chosen file", bytes.len()); log::info!(
"There are {} bytes to be written to chosen file",
bytes.len()
);
save_file(bytes, Some("transfers.csv")); save_file(bytes, Some("transfers.csv"));
} }
} }
@ -158,7 +161,7 @@ fn render_import_menu(
Ok(new_main_state) => { Ok(new_main_state) => {
log::info!("Loaded new main state from file."); log::info!("Loaded new main state from file.");
*main_state = new_main_state *main_state = new_main_state
}, }
Err(e) => log::error!("Failed loading imported state JSON: {}", e), Err(e) => log::error!("Failed loading imported state JSON: {}", e),
} }
} }
@ -176,7 +179,9 @@ fn render_import_menu(
if let Ok(data) = String::from_utf8(data.to_vec()) { if let Ok(data) = String::from_utf8(data.to_vec()) {
let auto_output = plate_tool_lib::csv::read_csv_auto(&data); let auto_output = plate_tool_lib::csv::read_csv_auto(&data);
main_state.source_plates.extend(auto_output.sources); main_state.source_plates.extend(auto_output.sources);
main_state.destination_plates.extend(auto_output.destinations); main_state
.destination_plates
.extend(auto_output.destinations);
main_state.transfers.extend(auto_output.transfers); main_state.transfers.extend(auto_output.transfers);
} }
} }
@ -197,6 +202,10 @@ fn render_options_menu(
&mut main_window_state.plate_display_options.show_transfer_hashes, &mut main_window_state.plate_display_options.show_transfer_hashes,
"Toggle transfer hashes", "Toggle transfer hashes",
); );
ui.toggle_value(
&mut main_window_state.plate_display_options.show_well_volumes,
"Show volumes in wells",
);
ui.toggle_value( ui.toggle_value(
&mut main_window_state.plate_display_options.show_volume_heatmap, &mut main_window_state.plate_display_options.show_volume_heatmap,
"Toggle volume heatmap", "Toggle volume heatmap",