Compare commits

..

14 Commits

Author SHA1 Message Date
Emilia Allison e88fdb0cdd
feature: Volume management for CSV import
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
Also more likely to be correct elsewhere
2024-08-10 04:15:30 -04:00
Emilia Allison 7771ce1786
refactor: Well struct
most devious refactoring
2024-08-10 02:39:37 -04:00
Emilia Allison ca142ef594
Revert "refactor: Well struct"
This reverts commit 7e321a78c4.
2024-08-10 01:40:25 -04:00
Emilia Allison deac725fdb
Revert "refactor: Well struct in web"
This reverts commit 63790c2145.
2024-08-10 01:40:14 -04:00
Emilia Allison 7031ae33dc
fix: SCSS selector 2024-08-10 01:01:34 -04:00
Emilia Allison 63790c2145
refactor: Well struct in web 2024-08-10 00:58:55 -04:00
Emilia Allison 7e321a78c4
refactor: Well struct
Will require bump to lib version!!
2024-08-10 00:52:50 -04:00
Emilia Allison 06baf0a053
feature: Heatmap enabled indicator 2024-08-09 22:57:50 -04:00
Emilia Allison 52b28aabb1
fix: Button styling updates 2024-08-09 22:35:00 -04:00
Emilia Allison 9e1abdc8bb
fix: Expand transfer name width 2024-08-09 21:07:59 -04:00
Emilia Allison e829a49424
fix: Volume not saved on new transfer creation 2024-08-09 21:01:50 -04:00
Emilia Allison 2ccb84041b
fix: CSS for plate add buttons
Some improvement, still not great
2024-08-09 20:58:27 -04:00
Emilia Allison caf10f10c1
feature: Separate add plate button per type 2024-08-09 20:33:09 -04:00
Emilia Allison 98d2d92b49
Update documentation 2024-08-09 19:39:41 -04:00
27 changed files with 778 additions and 278 deletions

267
Cargo.lock generated
View File

@ -26,6 +26,21 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "anymap" name = "anymap"
version = "1.0.0-beta.2" version = "1.0.0-beta.2"
@ -76,6 +91,12 @@ dependencies = [
"rustc-demangle", "rustc-demangle",
] ]
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]] [[package]]
name = "bincode" name = "bincode"
version = "1.3.3" version = "1.3.3"
@ -118,6 +139,19 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
"num-traits",
"serde",
"windows-targets",
]
[[package]] [[package]]
name = "console_error_panic_hook" name = "console_error_panic_hook"
version = "0.1.7" version = "0.1.7"
@ -128,6 +162,12 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "core-foundation-sys"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]] [[package]]
name = "csv" name = "csv"
version = "1.3.0" version = "1.3.0"
@ -184,6 +224,22 @@ dependencies = [
"syn 2.0.48", "syn 2.0.48",
] ]
[[package]]
name = "deranged"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
"serde",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@ -467,12 +523,24 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.3.5" version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0c62115964e08cb8039170eb33c1d0e2388a256930279edca206fff675f82c3" checksum = "d0c62115964e08cb8039170eb33c1d0e2388a256930279edca206fff675f82c3"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]] [[package]]
name = "http" name = "http"
version = "0.2.11" version = "0.2.11"
@ -484,6 +552,29 @@ dependencies = [
"itoa", "itoa",
] ]
[[package]]
name = "iana-time-zone"
version = "0.1.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "ident_case" name = "ident_case"
version = "1.0.1" version = "1.0.1"
@ -496,7 +587,7 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfd6201e7c30ccb24773cac7efa6fec1e06189d414b7439ce756a481c8bfbf53" checksum = "cfd6201e7c30ccb24773cac7efa6fec1e06189d414b7439ce756a481c8bfbf53"
dependencies = [ dependencies = [
"indexmap", "indexmap 1.9.3",
] ]
[[package]] [[package]]
@ -506,7 +597,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"hashbrown", "hashbrown 0.12.3",
"serde",
]
[[package]]
name = "indexmap"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0"
dependencies = [
"equivalent",
"hashbrown 0.14.5",
"serde",
] ]
[[package]] [[package]]
@ -557,6 +660,21 @@ dependencies = [
"adler", "adler",
] ]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "num_cpus" name = "num_cpus"
version = "1.16.0" version = "1.16.0"
@ -633,7 +751,7 @@ dependencies = [
[[package]] [[package]]
name = "plate-tool-lib" name = "plate-tool-lib"
version = "0.4.0" version = "0.4.1"
dependencies = [ dependencies = [
"csv", "csv",
"getrandom", "getrandom",
@ -643,6 +761,7 @@ dependencies = [
"regex", "regex",
"serde", "serde",
"serde_json", "serde_json",
"serde_with",
"uuid", "uuid",
] ]
@ -669,6 +788,12 @@ dependencies = [
"yewdux", "yewdux",
] ]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.17" version = "0.2.17"
@ -881,6 +1006,36 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_with"
version = "3.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857"
dependencies = [
"base64",
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.3.0",
"serde",
"serde_derive",
"serde_json",
"serde_with_macros",
"time",
]
[[package]]
name = "serde_with_macros"
version = "3.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.9" version = "0.4.9"
@ -938,6 +1093,37 @@ dependencies = [
"syn 2.0.48", "syn 2.0.48",
] ]
[[package]]
name = "time"
version = "0.3.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
dependencies = [
"num-conv",
"time-core",
]
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.36.0" version = "1.36.0"
@ -1145,6 +1331,79 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]] [[package]]
name = "yew" name = "yew"
version = "0.20.0" version = "0.20.0"
@ -1155,7 +1414,7 @@ dependencies = [
"futures", "futures",
"gloo", "gloo",
"implicit-clone", "implicit-clone",
"indexmap", "indexmap 1.9.3",
"js-sys", "js-sys",
"prokio", "prokio",
"rustversion", "rustversion",

View File

@ -1,6 +1,6 @@
# plate-tool # plate-tool
A web-based tool for creating assays for your favorite (acoustic) liquid handler. A web-based tool for creating and visualizing picklists for your favorite (possibly acoustic) liquid handler.
## Table of Contents ## Table of Contents
- [Usage](#Usage) - [Usage](#Usage)
@ -61,8 +61,20 @@ To add a new plate, click the "New Plate" button:
To do so, first note the "File" tab at the top-left of the screen (above the list pane). To do so, first note the "File" tab at the top-left of the screen (above the list pane).
Mouse over this tab, and a few more options will be revealed. Mouse over this tab, and a few more options will be revealed.
We want to export: mouse over export and select "Export as CSV". We want to export: mouse over export and select "Export as CSV".
You will be reminded that this is a one-way export (see JSON export/import below), You will be prompted by your browser to select a location for your file.
and then prompted by your browser to select a location for your file.
As of version `0.4.0`, it is possible to pick a CSV export format:
Mouse over options, then export, then click "Change CSV export type".
In the dialog that opens, select your desired export type.
Currently, plate-tool supports:
- Normal
- This format can be imported by Cellario's cherrypick hook.
- Echo Client
- This format is useful if you want to run a picklist directly from the
Echo Client software.
This will export just the transfers between the currently selected plates;
I assume you'd be using this feature in a non-automation context
and know to load your plates into your Echo yourself.
#### Export as JSON (Saving Your Work) #### Export as JSON (Saving Your Work)
Currently, it is not possible to export to a format produced by other similar software. Currently, it is not possible to export to a format produced by other similar software.
@ -110,6 +122,11 @@ To add a new plate, click the "New Plate" button:
Your browser's console may have guidance as to why parsing failed; Your browser's console may have guidance as to why parsing failed;
plate-tool was probably expecting a different name for a column than was in your file. plate-tool was probably expecting a different name for a column than was in your file.
_Note_: If you find a picklist that Cellario *can* import that plate-tool cannot,
please email me!
Odds are your picklist contains a weird edge case I've not considered,
and I would like to fix that!
### Other Neat Features ### Other Neat Features
#### Taking Pictures of Plates #### Taking Pictures of Plates
@ -119,6 +136,7 @@ To add a new plate, click the "New Plate" button:
and deposit it in your clipboard for you. and deposit it in your clipboard for you.
You can then paste this into PowerPoint, GIMP, or whereever else You can then paste this into PowerPoint, GIMP, or whereever else
you want a pretty picture of a plate. you want a pretty picture of a plate.
I hope this is helpful for arts and crafts.
_NOTE:_ I won't guarantee this feature will work in all contexts; _NOTE:_ I won't guarantee this feature will work in all contexts;
it relies on your browser thinking that you have plate-tool open it relies on your browser thinking that you have plate-tool open
@ -141,7 +159,7 @@ To add a new plate, click the "New Plate" button:
Plate tool is hosted [here](https://ilia.moe/cool-stuff/plate-tool/) for your convenience. Plate tool is hosted [here](https://ilia.moe/cool-stuff/plate-tool/) for your convenience.
However, you're absolutely welcome to host your own instance (even locally). However, you're absolutely welcome to host your own instance (even locally).
Here's how: Here's how:
(_Note:_ ~~If you run Windows you're probably best off doing the following in WSL2~~ You're absolutely fine to install rustup in Powershell, and the subsequent steps should be very similar but likely with different filepaths.) (_Note:_ If you run Windows you're absolutely fine to install rustup in Powershell, and the subsequent steps should be very similar but likely with different filepaths. I haven't personally validated this. )
1. Make sure you have a working Rust toolchain 1. Make sure you have a working Rust toolchain
1. Installing `rustup` is the easiest way to do this. See [their website](https://rustup.rs/), 1. Installing `rustup` is the easiest way to do this. See [their website](https://rustup.rs/),

View File

@ -1,6 +1,6 @@
[package] [package]
name = "plate-tool-lib" name = "plate-tool-lib"
version = "0.4.0" version = "0.4.1"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -15,3 +15,4 @@ csv = "1.2"
getrandom = { version = "0.2", features = ["js"] } getrandom = { version = "0.2", features = ["js"] }
rand = { version = "0.8", features = ["small_rng"] } rand = { version = "0.8", features = ["small_rng"] }
lazy_static = "1.4" lazy_static = "1.4"
serde_with = { version = "3.9.0", features = ["json"] }

View File

@ -7,9 +7,11 @@ use super::{string_well_to_pt, TransferRecord, read_csv};
use crate::plate::{PlateFormat, PlateType}; use crate::plate::{PlateFormat, PlateType};
use crate::plate_instances::PlateInstance; use crate::plate_instances::PlateInstance;
use crate::transfer::Transfer; use crate::transfer::Transfer;
use crate::transfer_volume::{TransferVolume, VolumeMaps};
use crate::Well;
use crate::transfer_region::{CustomRegion, Region, TransferRegion}; use crate::transfer_region::{CustomRegion, Region, TransferRegion};
use std::collections::HashSet; use std::collections::{HashSet, HashMap};
pub struct AutoOutput { pub struct AutoOutput {
pub sources: Vec<PlateInstance>, pub sources: Vec<PlateInstance>,
@ -107,7 +109,7 @@ fn find_unique_plates(records: &[TransferRecord]) -> UniquePlates {
fn guess_plate_size(plate_filtered_records: &[&TransferRecord], which: PlateType) -> PlateFormat { fn guess_plate_size(plate_filtered_records: &[&TransferRecord], which: PlateType) -> PlateFormat {
let mut guess = PlateFormat::W96; // Never guess smaller than 96 let mut guess = PlateFormat::W96; // Never guess smaller than 96
for record in plate_filtered_records { for record in plate_filtered_records {
if let Some((row, col)) = string_well_to_pt(match which { if let Some(Well { row, col }) = string_well_to_pt(match which {
PlateType::Source => &record.source_well, PlateType::Source => &record.source_well,
PlateType::Destination => &record.destination_well, PlateType::Destination => &record.destination_well,
}) { }) {
@ -169,8 +171,14 @@ fn get_transfer_for_pair(
return None; return None;
} }
let mut source_wells: HashSet<(u8, u8)> = HashSet::new(); let mut source_wells: HashSet<Well> = HashSet::new();
let mut destination_wells: HashSet<(u8, u8)> = HashSet::new(); let mut destination_wells: HashSet<Well> = HashSet::new();
// Volume Hash Maps
let mut source_only_volume_map: HashMap<Well, f32> = HashMap::new();
let mut destination_only_volume_map: HashMap<Well, f32> = HashMap::new();
let mut paired_volume_map: HashMap<(Well, Well), f32> = HashMap::new();
for record in filtered_records { for record in filtered_records {
let source_point_opt = string_well_to_pt(&record.source_well); let source_point_opt = string_well_to_pt(&record.source_well);
let destination_point_opt = string_well_to_pt(&record.destination_well); let destination_point_opt = string_well_to_pt(&record.destination_well);
@ -181,10 +189,23 @@ fn get_transfer_for_pair(
source_wells.insert(source_point); source_wells.insert(source_point);
destination_wells.insert(destination_point); destination_wells.insert(destination_point);
source_only_volume_map.entry(source_point).and_modify(|v| *v += record.volume)
.or_insert(record.volume);
destination_only_volume_map.entry(destination_point).and_modify(|v| *v += record.volume)
.or_insert(record.volume);
paired_volume_map.entry((source_point, destination_point)).and_modify(|v| *v += record.volume)
.or_insert_with(|| record.volume); // No idea why this one needs to be different
} }
} }
let source_wells_vec: Vec<(u8, u8)> = source_wells.into_iter().collect(); let source_wells_vec: Vec<Well> = source_wells.into_iter().collect();
let destination_wells_vec: Vec<(u8, u8)> = destination_wells.into_iter().collect(); let destination_wells_vec: Vec<Well> = destination_wells.into_iter().collect();
let transfer_volume: TransferVolume = TransferVolume::WellMap(VolumeMaps {
source_only: source_only_volume_map,
destination_only: destination_only_volume_map,
paired: paired_volume_map,
});
let custom_region: Region = let custom_region: Region =
Region::Custom(CustomRegion::new(source_wells_vec, destination_wells_vec)); Region::Custom(CustomRegion::new(source_wells_vec, destination_wells_vec));
@ -200,10 +221,13 @@ fn get_transfer_for_pair(
let transfer_name = format!("{} to {}", source.name, destination.name); let transfer_name = format!("{} to {}", source.name, destination.name);
Some(Transfer::new( let mut transfer = Transfer::new(
source.clone(), source.clone(),
destination.clone(), destination.clone(),
transfer_region, transfer_region,
transfer_name, transfer_name,
)) );
transfer.volume = transfer_volume;
Some(transfer)
} }

View File

@ -1,7 +1,11 @@
use crate::transfer::Transfer; use crate::transfer_volume::TransferVolume;
use crate::util::*; use crate::util::*;
use crate::{transfer::Transfer, Well};
use super::{alternative_formats::EchoClientTransferRecord, mangle_headers::mangle_headers, transfer_record::TransferRecordDeserializeIntermediate, TransferRecord}; use super::{
alternative_formats::EchoClientTransferRecord, mangle_headers::mangle_headers,
transfer_record::TransferRecordDeserializeIntermediate, TransferRecord,
};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use regex::Regex; use regex::Regex;
use std::error::Error; use std::error::Error;
@ -15,17 +19,32 @@ pub fn transfer_to_records(
let map = tr.transfer_region.calculate_map(); let map = tr.transfer_region.calculate_map();
let mut records: Vec<TransferRecord> = vec![]; let mut records: Vec<TransferRecord> = vec![];
if let TransferVolume::WellMap(wm) = &tr.volume {
log::debug!("{:?}\n{}", wm.paired, wm.paired.len());
}
for s_well in source_wells { for s_well in source_wells {
let dest_wells = map(s_well); let dest_wells = map(s_well);
if let Some(dest_wells) = dest_wells { if let Some(dest_wells) = dest_wells {
for d_well in dest_wells { for d_well in dest_wells {
// Get volume here:
let volume: f32 = match &tr.volume {
TransferVolume::Single(x) => *x, // If all have the same value, use it everywhere
TransferVolume::WellMap(wm) => {
*wm.paired.get(&(s_well, d_well)).unwrap_or(&2.5f32)
}
};
records.push(TransferRecord { records.push(TransferRecord {
source_plate: src_barcode.to_string(), source_plate: src_barcode.to_string(),
source_well: format!("{}{}", num_to_letters(s_well.0).unwrap(), s_well.1), source_well: format!("{}{}", num_to_letters(s_well.row).unwrap(), s_well.col),
destination_plate: dest_barcode.to_string(), destination_plate: dest_barcode.to_string(),
destination_well: format!("{}{}", num_to_letters(d_well.0).unwrap(), d_well.1), destination_well: format!(
volume: tr.volume, "{}{}",
num_to_letters(d_well.row).unwrap(),
d_well.col
),
volume,
concentration: None, concentration: None,
}) })
} }
@ -53,7 +72,7 @@ pub fn records_to_echo_client_csv(trs: Vec<TransferRecord>) -> Result<String, Bo
} }
/// Converts "spreadsheet format" well identification to coordinates /// Converts "spreadsheet format" well identification to coordinates
pub fn string_well_to_pt(input: &str) -> Option<(u8, u8)> { pub fn string_well_to_pt(input: &str) -> Option<Well> {
lazy_static! { lazy_static! {
static ref REGEX: Regex = Regex::new(r"([A-Z,a-z]+)(\d+)").unwrap(); static ref REGEX: Regex = Regex::new(r"([A-Z,a-z]+)(\d+)").unwrap();
@ -62,9 +81,9 @@ pub fn string_well_to_pt(input: &str) -> Option<(u8, u8)> {
} }
if let Some(c1) = REGEX.captures(input) { if let Some(c1) = REGEX.captures(input) {
if let (Some(row), Some(col)) = (letters_to_num(&c1[1]), c1[2].parse::<u8>().ok()) { if let (Some(row), Some(col)) = (letters_to_num(&c1[1]), c1[2].parse::<u8>().ok()) {
return Some((row, col)) return Some(Well { row, col });
} else { } else {
return None return None;
} }
} }
None None

View File

@ -4,7 +4,6 @@ mod auto;
mod mangle_headers; mod mangle_headers;
mod alternative_formats; mod alternative_formats;
pub use transfer_record::volume_default;
pub use transfer_record::TransferRecord; pub use transfer_record::TransferRecord;
pub use conversion::*; pub use conversion::*;

View File

@ -74,7 +74,7 @@ impl From<TransferRecordDeserializeIntermediate> for TransferRecord {
} }
} }
let volume = value.volume.unwrap_or(volume_default()); let volume = value.volume.unwrap_or(2.5f32);
TransferRecord { TransferRecord {
source_plate: value.source_plate, source_plate: value.source_plate,
@ -105,8 +105,3 @@ fn numeric_well_to_alphanumeric(input: u16, pformat: PlateFormat) -> Option<Stri
Some(format!("{}{}", row_str, column)) Some(format!("{}{}", row_str, column))
} }
// Why is this here?
pub fn volume_default() -> f32 {
Transfer::default().volume
}

View File

@ -3,4 +3,8 @@ 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_volume;
pub mod util; pub mod util;
mod well;
pub use well::Well;

View File

@ -1,5 +1,8 @@
use crate::transfer_volume;
use super::plate_instances::*; use super::plate_instances::*;
use super::transfer_region::*; use super::transfer_region::*;
use super::transfer_volume::*;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use uuid::Uuid; use uuid::Uuid;
@ -14,8 +17,7 @@ pub struct Transfer {
#[serde(default = "Uuid::now_v7")] #[serde(default = "Uuid::now_v7")]
pub id: Uuid, pub id: Uuid,
pub transfer_region: TransferRegion, pub transfer_region: TransferRegion,
#[serde(default = "default_volume")] pub volume: TransferVolume,
pub volume: f32,
} }
impl Default for Transfer { impl Default for Transfer {
@ -26,15 +28,11 @@ impl Default for Transfer {
name: "New Transfer".to_string(), name: "New Transfer".to_string(),
id: Default::default(), id: Default::default(),
transfer_region: Default::default(), transfer_region: Default::default(),
volume: default_volume(), volume: Default::default(),
} }
} }
} }
fn default_volume() -> f32 {
2.5f32
}
impl Transfer { impl Transfer {
pub fn new( pub fn new(
source: PlateInstance, source: PlateInstance,
@ -48,7 +46,7 @@ impl Transfer {
name, name,
id: Uuid::now_v7(), id: Uuid::now_v7(),
transfer_region: tr, transfer_region: tr,
volume: 2.5, volume: transfer_volume::TransferVolume::Single(2.5),
} }
} }

View File

@ -1,31 +1,32 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::plate::Plate; use super::plate::Plate;
use crate::Well;
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] #[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
pub struct CustomRegion { pub struct CustomRegion {
src: Vec<(u8, u8)>, src: Vec<Well>,
dest: Vec<(u8, u8)>, dest: Vec<Well>,
} }
impl CustomRegion { impl CustomRegion {
pub fn new(src: Vec<(u8, u8)>, dest: Vec<(u8, u8)>) -> Self { pub fn new(src: Vec<Well>, dest: Vec<Well>) -> Self {
CustomRegion { src, dest } CustomRegion { src, dest }
} }
} }
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] #[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
pub enum Region { pub enum Region {
Rect((u8, u8), (u8, u8)), Rect(Well, Well),
Point((u8, u8)), Point(Well),
Custom(CustomRegion), Custom(CustomRegion),
} }
impl Default for Region { impl Default for Region {
fn default() -> Self { fn default() -> Self {
Region::Point((1, 1)) Region::Point(Well { row: 1, col: 1 })
} }
} }
impl TryFrom<Region> for ((u8, u8), (u8, u8)) { 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> {
if let Region::Rect(c1, c2) = region { if let Region::Rect(c1, c2) = region {
@ -37,12 +38,10 @@ impl TryFrom<Region> for ((u8, u8), (u8, u8)) {
} }
} }
type Corner = (u8, u8);
type Rectangle = (Corner, Corner);
impl Region { impl Region {
pub fn new_custom(transfers: &Vec<Rectangle>) -> Self { pub fn new_custom(transfers: &Vec<(Well, Well)>) -> Self {
let mut src_pts: Vec<(u8, u8)> = Vec::with_capacity(transfers.len()); let mut src_pts: Vec<Well> = Vec::with_capacity(transfers.len());
let mut dest_pts: Vec<(u8, u8)> = Vec::with_capacity(transfers.len()); let mut dest_pts: Vec<Well> = Vec::with_capacity(transfers.len());
for transfer in transfers { for transfer in transfers {
src_pts.push(transfer.0); src_pts.push(transfer.0);
@ -80,10 +79,10 @@ impl Default for TransferRegion {
} }
impl TransferRegion { impl TransferRegion {
pub fn get_source_wells(&self) -> Vec<(u8, u8)> { pub fn get_source_wells(&self) -> Vec<Well> {
match &self.source_region { match &self.source_region {
Region::Rect(c1, c2) => { Region::Rect(c1, c2) => {
let mut wells = Vec::<(u8, u8)>::new(); let mut wells = Vec::<Well>::new();
let (ul, br) = standardize_rectangle(c1, c2); let (ul, br) = standardize_rectangle(c1, c2);
let (interleave_i, interleave_j) = self.interleave_source; let (interleave_i, interleave_j) = self.interleave_source;
// NOTE: This will panic if either is 0! // NOTE: This will panic if either is 0!
@ -93,12 +92,12 @@ impl TransferRegion {
let (interleave_i, interleave_j) = let (interleave_i, interleave_j) =
(i8::max(interleave_i, 1), i8::max(interleave_j, 1)); (i8::max(interleave_i, 1), i8::max(interleave_j, 1));
for i in (ul.0..=br.0).step_by(i8::abs(interleave_i) as usize) { for i in (ul.row..=br.row).step_by(i8::abs(interleave_i) as usize) {
for j in (ul.1..=br.1).step_by(i8::abs(interleave_j) as usize) { for j in (ul.col..=br.col).step_by(i8::abs(interleave_j) as usize) {
// NOTE: It looks like we're ignoring negative interleaves, // NOTE: It looks like we're ignoring negative interleaves,
// because it wouldn't make a difference here---the same // because it wouldn't make a difference here---the same
// wells will still be involved in the transfer. // wells will still be involved in the transfer.
wells.push((i, j)) wells.push(Well { row: i, col: j })
} }
} }
wells wells
@ -108,14 +107,14 @@ impl TransferRegion {
} }
} }
pub fn get_destination_wells(&self) -> Vec<(u8, u8)> { pub fn get_destination_wells(&self) -> Vec<Well> {
match &self.source_region { match &self.source_region {
Region::Custom(c) => c.dest.clone(), Region::Custom(c) => c.dest.clone(),
_ => { _ => {
let map = self.calculate_map(); let map = self.calculate_map();
let source_wells = self.get_source_wells(); let source_wells = self.get_source_wells();
let mut wells = Vec::<(u8, u8)>::new(); let mut wells = Vec::<Well>::new();
for well in source_wells { for well in source_wells {
if let Some(mut dest_wells) = map(well) { if let Some(mut dest_wells) = map(well) {
@ -129,14 +128,14 @@ impl TransferRegion {
} }
#[allow(clippy::type_complexity)] // Resolving gives inherent associated type error #[allow(clippy::type_complexity)] // Resolving gives inherent associated type error
pub fn calculate_map(&self) -> Box<dyn Fn((u8, u8)) -> Option<Vec<(u8, u8)>> + '_> { pub fn calculate_map(&self) -> Box<dyn Fn(Well) -> Option<Vec<Well>> + '_> {
// By validating first, we have a stronger guarantee that // By validating first, we have a stronger guarantee that
// this function will not panic. :) // this function will not panic. :)
// log::debug!("Validating: {:?}", self.validate()); // log::debug!("Validating: {:?}", self.validate());
if let Err(msg) = self.validate() { if let Err(msg) = self.validate() {
eprintln!("{}", msg); eprintln!("{}", msg);
eprintln!("This transfer will be empty."); eprintln!("This transfer will be empty.");
return Box::new(|(_, _)| None); return Box::new(|_| None);
} }
// log::debug!("What is ild? {:?}", self); // log::debug!("What is ild? {:?}", self);
@ -144,10 +143,10 @@ impl TransferRegion {
let il_dest = self.interleave_dest; let il_dest = self.interleave_dest;
let il_source = self.interleave_source; let il_source = self.interleave_source;
let source_corners: ((u8, u8), (u8, u8)) = match self.source_region { let source_corners: (Well, Well) = match self.source_region {
Region::Point((x, y)) => ((x, y), (x, y)), Region::Point(w) => (w, w),
Region::Rect(c1, c2) => (c1, c2), Region::Rect(c1, c2) => (c1, c2),
Region::Custom(_) => ((0, 0), (0, 0)), Region::Custom(_) => (Well { row: 0, col: 0 }, Well { row: 0, col: 0 }),
}; };
let (source_ul, _) = standardize_rectangle(&source_corners.0, &source_corners.1); let (source_ul, _) = standardize_rectangle(&source_corners.0, &source_corners.1);
// This map is not necessarily injective or surjective, // This map is not necessarily injective or surjective,
@ -157,43 +156,44 @@ impl TransferRegion {
// Non-replicate transfers: // Non-replicate transfers:
match &self.dest_region { match &self.dest_region {
Region::Point((x, y)) => { Region::Point(Well { row: x, col: y }) => {
Box::new(move |(i, j)| { Box::new(move |Well { row: i, col: j }| {
if source_wells.contains(&(i, j)) { if source_wells.contains(&Well { row: i, col: j }) {
// Validity here already checked by self.validate() // Validity here already checked by self.validate()
Some(vec![( Some(vec![Well {
x + i row: x + i
.checked_sub(source_ul.0) .checked_sub(source_ul.row)
.expect("Point cannot have been less than UL") .expect("Point cannot have been less than UL")
.checked_div(il_source.0.unsigned_abs()) .checked_div(il_source.0.unsigned_abs())
.expect("Source interleave cannot be 0") .expect("Source interleave cannot be 0")
.mul(il_dest.0.unsigned_abs()), .mul(il_dest.0.unsigned_abs()),
y + j col: y + j
.checked_sub(source_ul.1) .checked_sub(source_ul.col)
.expect("Point cannot have been less than UL") .expect("Point cannot have been less than UL")
.checked_div(il_source.1.unsigned_abs()) .checked_div(il_source.1.unsigned_abs())
.expect("Source interleave cannot be 0") .expect("Source interleave cannot be 0")
.mul(il_dest.1.unsigned_abs()), .mul(il_dest.1.unsigned_abs()),
)]) }])
} else { } else {
None None
} }
}) })
} }
Region::Rect(c1, c2) => { Region::Rect(c1, c2) => {
Box::new(move |(i, j)| { Box::new(move |w| {
if source_wells.contains(&(i, j)) { let Well { row: i, col: j } = w;
if source_wells.contains(&w) {
let possible_destination_wells = create_dense_rectangle(c1, c2); let possible_destination_wells = create_dense_rectangle(c1, c2);
let (d_ul, d_br) = standardize_rectangle(c1, c2); let (d_ul, d_br) = standardize_rectangle(c1, c2);
let (s_ul, s_br) = let (s_ul, s_br) =
standardize_rectangle(&source_corners.0, &source_corners.1); standardize_rectangle(&source_corners.0, &source_corners.1);
let s_dims = ( let s_dims = (
s_br.0.checked_sub(s_ul.0).unwrap() + 1, s_br.row.checked_sub(s_ul.row).unwrap() + 1,
s_br.1.checked_sub(s_ul.1).unwrap() + 1, s_br.col.checked_sub(s_ul.col).unwrap() + 1,
); );
let d_dims = ( let d_dims = (
d_br.0.checked_sub(d_ul.0).unwrap() + 1, d_br.row.checked_sub(d_ul.row).unwrap() + 1,
d_br.1.checked_sub(d_ul.1).unwrap() + 1, d_br.col.checked_sub(d_ul.col).unwrap() + 1,
); );
let number_used_src_wells = ( let number_used_src_wells = (
// Number of used source wells // Number of used source wells
@ -222,34 +222,34 @@ impl TransferRegion {
.unwrap() as u8, .unwrap() as u8,
); );
let i = i let i = i
.saturating_sub(s_ul.0) .saturating_sub(s_ul.row)
.saturating_div(il_source.0.unsigned_abs()); .saturating_div(il_source.0.unsigned_abs());
let j = j let j = j
.saturating_sub(s_ul.1) .saturating_sub(s_ul.col)
.saturating_div(il_source.1.unsigned_abs()); .saturating_div(il_source.1.unsigned_abs());
Some( Some(
possible_destination_wells possible_destination_wells
.into_iter() .into_iter()
.filter(|(x, _)| { .filter(|Well { row: x , ..}| {
x.checked_sub(d_ul.0).unwrap() x.checked_sub(d_ul.row).unwrap()
% (number_used_src_wells.0 * il_dest.0.unsigned_abs()) // Counter along x % (number_used_src_wells.0 * il_dest.0.unsigned_abs()) // Counter along x
== (il_dest.0.unsigned_abs() *i) == (il_dest.0.unsigned_abs() *i)
% (number_used_src_wells.0 * il_dest.0.unsigned_abs()) % (number_used_src_wells.0 * il_dest.0.unsigned_abs())
}) })
.filter(|(_, y)| { .filter(|Well { col: y, .. }| {
y.checked_sub(d_ul.1).unwrap() y.checked_sub(d_ul.col).unwrap()
% (number_used_src_wells.1 * il_dest.1.unsigned_abs()) // Counter along u % (number_used_src_wells.1 * il_dest.1.unsigned_abs()) // Counter along u
== (il_dest.1.unsigned_abs() *j) == (il_dest.1.unsigned_abs() *j)
% (number_used_src_wells.1 * il_dest.1.unsigned_abs()) % (number_used_src_wells.1 * il_dest.1.unsigned_abs())
}) })
.filter(|(x, 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.0).unwrap().div_euclid( x.checked_sub(d_ul.row).unwrap().div_euclid(
number_used_src_wells.0 * il_dest.0.unsigned_abs(), number_used_src_wells.0 * il_dest.0.unsigned_abs(),
) < count.0 ) < count.0
&& y.checked_sub(d_ul.1).unwrap().div_euclid( && y.checked_sub(d_ul.col).unwrap().div_euclid(
number_used_src_wells.1 * il_dest.1.unsigned_abs(), number_used_src_wells.1 * il_dest.1.unsigned_abs(),
) < count.1 ) < count.1
}) })
@ -260,14 +260,14 @@ impl TransferRegion {
} }
}) })
} }
Region::Custom(c) => Box::new(move |(i, j)| { Region::Custom(c) => Box::new(move |Well { row: i, col: j }| {
let src = c.src.clone(); let src = c.src.clone();
let dest = c.dest.clone(); let dest = c.dest.clone();
let points: Vec<(u8, u8)> = src let points: Vec<Well> = src
.iter() .iter()
.enumerate() .enumerate()
.filter(|(_index, (x, y))| *x == i && *y == j) .filter(|(_index, Well { row: x, col: y })| *x == i && *y == j)
.map(|(index, _)| dest[index]) .map(|(index, _)| dest[index])
.collect(); .collect();
if points.is_empty() { if points.is_empty() {
@ -296,15 +296,15 @@ impl TransferRegion {
// later // later
Region::Rect(s1, s2) => { Region::Rect(s1, s2) => {
// Check if all source wells exist: // Check if all source wells exist:
if s1.0 == 0 || s1.1 == 0 || s2.0 == 0 || s2.1 == 0 { if s1.row == 0 || s1.col == 0 || s2.row == 0 || s2.col == 0 {
return Err("Source region is out-of-bounds! (Too small)"); return Err("Source region is out-of-bounds! (Too small)");
} }
// Sufficient to check if the corners are in-bounds // Sufficient to check if the corners are in-bounds
let source_max = self.source_plate.size(); let source_max = self.source_plate.size();
if s1.0 > source_max.0 || s2.0 > source_max.0 { if s1.row > source_max.0 || s2.row > source_max.0 {
return Err("Source region is out-of-bounds! (Too tall)"); return Err("Source region is out-of-bounds! (Too tall)");
} }
if s1.1 > source_max.1 || s2.1 > source_max.1 { if s1.col > source_max.1 || s2.col > source_max.1 {
// log::debug!("s1.1: {}, max.1: {}", s1.1, source_max.1); // log::debug!("s1.1: {}, max.1: {}", s1.1, source_max.1);
return Err("Source region is out-of-bounds! (Too wide)"); return Err("Source region is out-of-bounds! (Too wide)");
} }
@ -325,28 +325,34 @@ impl TransferRegion {
} }
} }
fn create_dense_rectangle(c1: &(u8, u8), c2: &(u8, u8)) -> Vec<(u8, u8)> { fn create_dense_rectangle(c1: &Well, c2: &Well) -> Vec<Well> {
// Creates a vector of every point between two corners // Creates a vector of every point between two corners
let (c1, c2) = standardize_rectangle(c1, c2); let (c1, c2) = standardize_rectangle(c1, c2);
let mut points = Vec::<(u8, u8)>::new(); let mut points = Vec::<Well>::new();
for i in c1.0..=c2.0 { for i in c1.row..=c2.row {
for j in c1.1..=c2.1 { for j in c1.col..=c2.col {
points.push((i, j)); points.push(Well { row: i, col: j });
} }
} }
points points
} }
fn standardize_rectangle(c1: &(u8, u8), c2: &(u8, u8)) -> ((u8, u8), (u8, u8)) { fn standardize_rectangle(c1: &Well, c2: &Well) -> (Well, Well) {
let upper_left_i = u8::min(c1.0, c2.0); let upper_left_i = u8::min(c1.row, c2.row);
let upper_left_j = u8::min(c1.1, c2.1); let upper_left_j = u8::min(c1.col, c2.col);
let bottom_right_i = u8::max(c1.0, c2.0); let bottom_right_i = u8::max(c1.row, c2.row);
let bottom_right_j = u8::max(c1.1, c2.1); let bottom_right_j = u8::max(c1.col, c2.col);
( (
(upper_left_i, upper_left_j), Well {
(bottom_right_i, bottom_right_j), row: upper_left_i,
col: upper_left_j,
},
Well {
row: bottom_right_i,
col: bottom_right_j,
},
) )
} }
@ -363,7 +369,7 @@ impl fmt::Display for TransferRegion {
let mut source_string = String::new(); let mut source_string = String::new();
for i in 1..=source_dims.0 { for i in 1..=source_dims.0 {
for j in 1..=source_dims.1 { for j in 1..=source_dims.1 {
if source_wells.contains(&(i, j)) { if source_wells.contains(&Well { row: i, col: j }) {
source_string.push('x') source_string.push('x')
} else { } else {
source_string.push('.') source_string.push('.')
@ -379,7 +385,7 @@ impl fmt::Display for TransferRegion {
let mut dest_string = String::new(); let mut dest_string = String::new();
for i in 1..=dest_dims.0 { for i in 1..=dest_dims.0 {
for j in 1..=dest_dims.1 { for j in 1..=dest_dims.1 {
if dest_wells.contains(&(i, j)) { if dest_wells.contains(&Well { row: i, col: j }) {
dest_string.push('x') dest_string.push('x')
} else { } else {
dest_string.push('.') dest_string.push('.')

View File

@ -0,0 +1,30 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::Well;
type WellPair = (Well, Well);
#[non_exhaustive]
#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)]
pub enum TransferVolume {
Single(f32),
WellMap(VolumeMaps),
}
#[serde_with::serde_as]
#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)]
pub struct VolumeMaps {
#[serde_as(as = "HashMap<serde_with::json::JsonString, _>")]
pub source_only: HashMap<Well, f32>,
#[serde_as(as = "HashMap<serde_with::json::JsonString, _>")]
pub destination_only: HashMap<Well, f32>,
#[serde_as(as = "HashMap<serde_with::json::JsonString, _>")]
pub paired: HashMap<WellPair, f32>,
}
impl Default for TransferVolume {
fn default() -> Self {
TransferVolume::Single(2.5f32)
}
}

View File

@ -0,0 +1,9 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Debug, Hash)]
pub struct Well {
/// Top to bottom assuming landscape, i.e. lettered
pub row: u8,
/// Left to right assuming landscape, i.e. numbered
pub col: u8,
}

View File

@ -0,0 +1,27 @@
@use "sass:color";
@use "./variables" as *;
$selection-border-width: 2px;
@mixin standard_button {
color: inherit;
display: inline;
margin-left: 3px;
border: 2px solid rgba(1,1,1,0.2);
background: transparent;
user-select: none;
list-style: none;
line-height: 1em;
&::before {
text-align: center;
vertical-align: middle;
}
&:hover {
background: color.change($color-light, $alpha: 0.08);
transition: background 0.1s;
border: $selection-border-width solid color.change($color-light, $alpha:0.3);
}
}

View File

@ -2,6 +2,8 @@
@use "../variables" as *; @use "../variables" as *;
div.plate_container { div.plate_container {
position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-evenly; justify-content: space-evenly;
@ -38,4 +40,22 @@ div.plate_container {
grid-row: 2; grid-row: 2;
} }
} }
div.plate_container--heatmap-notice {
position: absolute;
top: 0.5em;
left: 0.5em;
animation: 1s 1 attention_on_load;
}
}
@keyframes attention_on_load {
from{
color: $color-light;
transform: scale(1.05);
}
to {
color: inherit;
}
} }

View File

@ -1,5 +1,6 @@
@use "sass:color"; @use "sass:color";
@use "../variables" as *; @use "../variables" as *;
@use "../button" as *;
div.transfer_menu { div.transfer_menu {
position: relative; position: relative;
@ -40,6 +41,11 @@ div.transfer_menu {
} }
} }
div.transfer_menu input[type="button"] {
@include standard_button;
}
input { input {
text-align: center; text-align: center;
margin-left: 0.5em; margin-left: 0.5em;
@ -50,10 +56,10 @@ input {
padding: 0; padding: 0;
&[type="text"] { &[type="text"] {
width: 4em; width: 6em;
} }
&[name="name"] { &[name="name"] {
width: 6em; // Override above width: calc(100% - 6em); // Override above
} }
&[type="number"] { &[type="number"] {
width: 2em; width: 2em;

View File

@ -1,5 +1,6 @@
@use "sass:color"; @use "sass:color";
@use "../variables" as *; @use "../variables" as *;
@use "../button" as *;
$selection-border-width: 2px; $selection-border-width: 2px;
@ -53,3 +54,17 @@ div.tree li {
background: color.change($color-light, $alpha: 0.2); background: color.change($color-light, $alpha: 0.2);
} }
} }
div.tree--header {
margin-top: 1%;
display: flex;
align-items: center;
}
div.tree--header button {
@include standard_button;
&::before {
content: "Add";
}
}

View File

@ -12,6 +12,7 @@ use crate::components::states::MainState;
use plate_tool_lib::transfer::Transfer; use plate_tool_lib::transfer::Transfer;
use plate_tool_lib::transfer_region::{Region, TransferRegion}; use plate_tool_lib::transfer_region::{Region, TransferRegion};
use plate_tool_lib::Well;
use plate_tool_lib::csv::{auto, string_well_to_pt, TransferRecord}; use plate_tool_lib::csv::{auto, string_well_to_pt, TransferRecord};
@ -238,7 +239,7 @@ pub fn import_transfer_csv_submit_callback(
let from_dest = from_dest.value(); let from_dest = from_dest.value();
let to_dest = to_dest.value(); let to_dest = to_dest.value();
let records: Vec<((u8, u8), (u8, u8))> = records let records: Vec<(Well, Well)> = records
.iter() .iter()
.filter(|record| record.source_plate == from_source) .filter(|record| record.source_plate == from_source)
.filter(|record| record.destination_plate == from_dest) .filter(|record| record.destination_plate == from_dest)

View File

@ -1,35 +1,27 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
use js_sys::Array;
use wasm_bindgen::{prelude::*, JsCast, JsValue};
use web_sys::{
Blob, HtmlAnchorElement, HtmlDialogElement, HtmlElement, HtmlFormElement, HtmlInputElement, Url,
};
use yew::prelude::*; use yew::prelude::*;
use yewdux::prelude::*; use yewdux::prelude::*;
use crate::components::states::{CurrentTransfer, MainState}; use crate::components::{new_plate_dialog::{NewPlateDialogType}, states::{CurrentTransfer, MainState}};
use crate::state_to_csv;
type NoParamsCallback = Box<dyn Fn(())>; type NoParamsCallback = Box<dyn Fn(())>;
pub fn new_plate_dialog_callback( pub fn new_plate_dialog_callback(
new_plate_dialog_is_open: UseStateHandle<bool>, new_plate_dialog_is_open: UseStateHandle<Option<NewPlateDialogType>>,
) -> NoParamsCallback { ) -> NoParamsCallback {
let new_plate_dialog_is_open = new_plate_dialog_is_open.clone(); let new_plate_dialog_is_open = new_plate_dialog_is_open.clone();
Box::new(move |_| { Box::new(move |_| {
new_plate_dialog_is_open.set(false); new_plate_dialog_is_open.set(None);
}) })
} }
pub fn open_new_plate_dialog_callback( pub fn open_new_plate_dialog_callback(
new_plate_dialog_is_open: UseStateHandle<bool>, new_plate_dialog_is_open: UseStateHandle<Option<NewPlateDialogType>>
) -> NoParamsCallback { ) -> Box<dyn Fn(NewPlateDialogType)> {
let new_plate_dialog_is_open = new_plate_dialog_is_open.clone(); let new_plate_dialog_is_open = new_plate_dialog_is_open.clone();
Box::new(move |_| { Box::new(move |dt| {
new_plate_dialog_is_open.set(true); new_plate_dialog_is_open.set(Some(dt));
}) })
} }

View File

@ -6,7 +6,7 @@ use yew::prelude::*;
use yewdux::prelude::*; use yewdux::prelude::*;
use crate::components::transfer_menu::RegionDisplay; use crate::components::transfer_menu::RegionDisplay;
use plate_tool_lib::{transfer::Transfer, transfer_region::Region}; use plate_tool_lib::{transfer::Transfer, transfer_region::Region, transfer_volume::TransferVolume};
use crate::components::states::{CurrentTransfer, MainState}; use crate::components::states::{CurrentTransfer, MainState};
@ -142,7 +142,7 @@ pub fn on_volume_change_callback(ct_dispatch: Dispatch<CurrentTransfer>) -> Call
.expect("Must have been emitted by input"); .expect("Must have been emitted by input");
if let Ok(num) = input.value().parse::<f32>() { if let Ok(num) = input.value().parse::<f32>() {
ct_dispatch.reduce_mut(|state| { ct_dispatch.reduce_mut(|state| {
state.transfer.volume = num; state.transfer.volume = TransferVolume::Single(num);
}); });
} }
}) })
@ -184,12 +184,18 @@ pub fn save_transfer_button_callback_callback(
.iter() .iter()
.find(|dpi| dpi.get_uuid() == main_state.selected_dest_plate) .find(|dpi| dpi.get_uuid() == main_state.selected_dest_plate)
{ {
let new_transfer = Transfer::new( // Only mutable for volume assignment!
let mut new_transfer = Transfer::new(
spi.clone(), spi.clone(),
dpi.clone(), dpi.clone(),
ct_state.transfer.transfer_region.clone(), ct_state.transfer.transfer_region.clone(),
ct_state.transfer.name.clone(), ct_state.transfer.name.clone(),
); );
// This clone is cheap - we know this is just a single value
// so cloning f32 will be fast.
new_transfer.volume = ct_state.transfer.volume.clone();
main_dispatch.reduce_mut(|state| { main_dispatch.reduce_mut(|state| {
state.transfers.push(new_transfer); state.transfers.push(new_transfer);
state.selected_transfer = state state.selected_transfer = state

View File

@ -2,7 +2,7 @@
use yew::prelude::*; use yew::prelude::*;
use yewdux::prelude::*; use yewdux::prelude::*;
use crate::components::new_plate_dialog::NewPlateDialog; use crate::components::new_plate_dialog::{NewPlateDialog, NewPlateDialogType};
use crate::components::plates::plate_container::PlateContainer; use crate::components::plates::plate_container::PlateContainer;
use crate::components::states::{CurrentTransfer, MainState}; use crate::components::states::{CurrentTransfer, MainState};
use crate::components::transfer_menu::TransferMenu; use crate::components::transfer_menu::TransferMenu;
@ -53,7 +53,7 @@ pub fn MainWindow() -> Html {
main_window_callbacks::change_csv_export_type_callback(main_dispatch) main_window_callbacks::change_csv_export_type_callback(main_dispatch)
}; };
let new_plate_dialog_is_open = use_state_eq(|| false); let new_plate_dialog_is_open: UseStateHandle<Option<NewPlateDialogType>> = use_state_eq(|| None);
let new_plate_dialog_callback = let new_plate_dialog_callback =
main_window_callbacks::new_plate_dialog_callback(new_plate_dialog_is_open.clone()); main_window_callbacks::new_plate_dialog_callback(new_plate_dialog_is_open.clone());
@ -124,8 +124,8 @@ pub fn MainWindow() -> Html {
<TransferMenu /> <TransferMenu />
<PlateContainer source_dims={source_plate_instance} <PlateContainer source_dims={source_plate_instance}
destination_dims={destination_plate_instance}/> destination_dims={destination_plate_instance}/>
if {*new_plate_dialog_is_open} { if {new_plate_dialog_is_open.is_some()} {
<NewPlateDialog close_callback={new_plate_dialog_callback}/> <NewPlateDialog close_callback={new_plate_dialog_callback} dialog_type={new_plate_dialog_is_open.unwrap()}/>
} }
</div> </div>
</> </>

View File

@ -10,6 +10,14 @@ use crate::components::callbacks::new_plate_dialog_callbacks;
#[derive(PartialEq, Properties)] #[derive(PartialEq, Properties)]
pub struct NewPlateDialogProps { pub struct NewPlateDialogProps {
pub close_callback: Callback<()>, pub close_callback: Callback<()>,
pub dialog_type: NewPlateDialogType,
}
#[derive(PartialEq, Clone, Copy, Debug)]
pub enum NewPlateDialogType {
SourceOnly,
DestinationOnly,
Both,
} }
#[function_component] #[function_component]
@ -43,9 +51,15 @@ pub fn NewPlateDialog(props: &NewPlateDialogProps) -> Html {
); );
} }
let header_interior_text = match props.dialog_type {
NewPlateDialogType::Both => "",
NewPlateDialogType::SourceOnly => " source",
NewPlateDialogType::DestinationOnly => " destination",
};
html! { html! {
<dialog ref={dialog_ref} class="dialog new_plate_dialog" onclose={onclose}> <dialog ref={dialog_ref} class="dialog new_plate_dialog" onclose={onclose}>
<h2>{"Create a plate:"}</h2> <h2>{format!("Create a{} plate:", header_interior_text)}</h2>
<form onsubmit={new_plate_callback}> <form onsubmit={new_plate_callback}>
<input type="text" name="new_plate_name" placeholder="Name"/> <input type="text" name="new_plate_name" placeholder="Name"/>
<select name="plate_format"> <select name="plate_format">
@ -58,13 +72,35 @@ pub fn NewPlateDialog(props: &NewPlateDialogProps) -> Html {
<option value="1536">{"1536"}</option> <option value="1536">{"1536"}</option>
<option value="3456">{"3456"}</option> <option value="3456">{"3456"}</option>
</select> </select>
<input type="radio" name="new_plate_type" id="npt_src" value="src" /> { plate_type_selector(props.dialog_type) }
<label for="npt_src">{"Source"}</label>
<input type="radio" name="new_plate_type" id="npt_dest" value="dest" />
<label for="npt_dest">{"Destination"}</label>
<input type="submit" name="new_plate_button" value="Create" /> <input type="submit" name="new_plate_button" value="Create" />
</form> </form>
<form class="modal_close" method="dialog"><button /></form> <form class="modal_close" method="dialog"><button /></form>
</dialog> </dialog>
} }
} }
fn plate_type_selector(t: NewPlateDialogType) -> Html {
match t {
NewPlateDialogType::Both => {
html! {
<>
<input type="radio" name="new_plate_type" id="npt_src" value="src" />
<label for="npt_src">{"Source"}</label>
<input type="radio" name="new_plate_type" id="npt_dest" value="dest" />
<label for="npt_dest">{"Destination"}</label>
</>
}
},
NewPlateDialogType::SourceOnly => {
html! {
<input type="hidden" name="new_plate_type" id="npt_src" value="src" />
}
},
NewPlateDialogType::DestinationOnly => {
html! {
<input type="hidden" name="new_plate_type" id="npt_dest" value="dest" />
}
}
}
}

View File

@ -1,6 +1,8 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
use std::collections::HashMap; use std::collections::HashMap;
use std::ops::Deref;
use plate_tool_lib::transfer_volume::TransferVolume;
use yew::prelude::*; use yew::prelude::*;
use yewdux::prelude::*; use yewdux::prelude::*;
@ -8,6 +10,7 @@ use crate::components::states::{CurrentTransfer, MainState};
use plate_tool_lib::plate::PlateType; use plate_tool_lib::plate::PlateType;
use plate_tool_lib::transfer::Transfer; use plate_tool_lib::transfer::Transfer;
use plate_tool_lib::transfer_region::Region; use plate_tool_lib::transfer_region::Region;
use plate_tool_lib::Well;
// Color Palette for the Source Plates, can be changed here // Color Palette for the Source Plates, can be changed here
use crate::components::plates::util::Palettes; use crate::components::plates::util::Palettes;
@ -32,24 +35,24 @@ pub fn Plate(props: &PlateProps) -> Html {
PlateType::Destination => ct_state.transfer.transfer_region.dest_region.clone(), PlateType::Destination => ct_state.transfer.transfer_region.dest_region.clone(),
}; };
let (pt1, pt2) = match region { let (pt1, pt2) = match region {
Region::Point((x, y)) => ((x, y), (x, y)), Region::Point(Well {row: x, col: y }) => ((x, y), (x, y)),
Region::Rect(c1, c2) => (c1, c2), Region::Rect(c1, c2) => ((c1.row, c1.col ), (c2.row, c2.col)),
Region::Custom(_) => ((0, 0), (0, 0)), Region::Custom(_) => ((0, 0), (0, 0)),
}; };
m_start_handle.set(Some(pt1)); m_start_handle.set(Some(pt1));
m_end_handle.set(Some(pt2)); m_end_handle.set(Some(pt2));
} }
let tooltip_map: HashMap<(u8, u8), Vec<&Transfer>>; let tooltip_map: HashMap<Well, Vec<&Transfer>>;
let volume_map: HashMap<(u8, u8), f32>; let volume_map: HashMap<Well, f32>;
let volume_max: f32; let volume_max: f32;
{ {
let transfers = main_state.transfers.iter().filter(|t| match props.ptype { let transfers = main_state.transfers.iter().filter(|t| match props.ptype {
PlateType::Source => t.source_id == props.source_plate.get_uuid(), PlateType::Source => t.source_id == props.source_plate.get_uuid(),
PlateType::Destination => t.dest_id == props.destination_plate.get_uuid(), PlateType::Destination => t.dest_id == props.destination_plate.get_uuid(),
}); });
let mut tooltip_map_temp: HashMap<(u8, u8), Vec<&Transfer>> = HashMap::new(); let mut tooltip_map_temp: HashMap<Well, Vec<&Transfer>> = HashMap::new();
let mut volume_map_temp: HashMap<(u8,u8), f32> = HashMap::new(); let mut volume_map_temp: HashMap<Well, f32> = HashMap::new();
let mut volume_max_temp: f32 = f32::NEG_INFINITY; let mut volume_max_temp: f32 = f32::NEG_INFINITY;
for transfer in transfers { for transfer in transfers {
let wells = match props.ptype { let wells = match props.ptype {
@ -62,12 +65,23 @@ pub fn Plate(props: &PlateProps) -> Html {
} else { } else {
tooltip_map_temp.insert(well, vec![transfer]); tooltip_map_temp.insert(well, vec![transfer]);
} }
let temp_volume: f32 = match &transfer.volume {
TransferVolume::Single(x) => *x,
TransferVolume::WellMap(wm) => {
*match props.ptype {
PlateType::Source => wm.source_only.get(&well),
PlateType::Destination => wm.destination_only.get(&well)
}.unwrap_or(&2.5f32)
},
_ => unreachable!(),
};
if let Some(val) = volume_map_temp.get_mut(&well) { if let Some(val) = volume_map_temp.get_mut(&well) {
*val += transfer.volume; *val += temp_volume
} else { } else {
volume_map_temp.insert(well, transfer.volume); volume_map_temp.insert(well, temp_volume);
} }
volume_max_temp = f32::max(volume_max_temp, transfer.volume); volume_max_temp = f32::max(volume_max_temp, *volume_map_temp.get(&well).expect("Just added"));
} }
} }
tooltip_map = tooltip_map_temp; tooltip_map = tooltip_map_temp;
@ -135,22 +149,22 @@ pub fn Plate(props: &PlateProps) -> Html {
.map(|j| { .map(|j| {
let color = { let color = {
if !main_state.preferences.volume_heatmap { if !main_state.preferences.volume_heatmap {
tooltip_map.get(&(i,j)) tooltip_map.get(&Well { row: i, col: j })
.and_then(|t| t.last()) .and_then(|t| t.last())
.map(|t| PALETTE.get_ordered(t.get_uuid(), &ordered_ids)) .map(|t| PALETTE.get_ordered(t.get_uuid(), &ordered_ids))
} else { } else {
volume_map.get(&(i,j)) volume_map.get(&Well { row: i, col: j })
.map(|t| PALETTE.get_linear(*t as f64, volume_max as f64)) .map(|t| PALETTE.get_linear(*t as f64, volume_max as f64))
} }
}; };
let title = { let title = {
let mut out = String::new(); let mut out = String::new();
let used_by = tooltip_map.get(&(i,j)).map(|transfers| format!("Used by: {}", transfers.iter().map(|t| t.name.clone()) let used_by = tooltip_map.get(&Well { row: i, col: j }).map(|transfers| format!("Used by: {}", transfers.iter().map(|t| t.name.clone())
.collect::<Vec<_>>().join(", "))); .collect::<Vec<_>>().join(", ")));
if let Some(val) = used_by { if let Some(val) = used_by {
out += &val; out += &val;
} }
let volume_sum = volume_map.get(&(i,j)) let volume_sum = volume_map.get(&Well { row: i, col: j })
.map(|t| format!("Volume: {}", t)); .map(|t| format!("Volume: {}", t));
if let Some(val) = volume_sum { if let Some(val) = volume_sum {
if !out.is_empty() { out += "\n" } if !out.is_empty() { out += "\n" }
@ -162,7 +176,7 @@ pub fn Plate(props: &PlateProps) -> Html {
<PlateCell i={i} j={j} <PlateCell i={i} j={j}
selected={in_rect(*m_start_handle.clone(), *m_end_handle.clone(), (i,j))} selected={in_rect(*m_start_handle.clone(), *m_end_handle.clone(), (i,j))}
mouse={mouse_callback.clone()} mouse={mouse_callback.clone()}
in_transfer={wells.contains(&(i,j)) && main_state.preferences.in_transfer_hashes} in_transfer={wells.contains(&Well { row: i, col: j }) && main_state.preferences.in_transfer_hashes}
color={ color } color={ color }
cell_height={props.cell_height} cell_height={props.cell_height}
title={title} title={title}

View File

@ -41,7 +41,7 @@ pub fn mouseup_callback(
m_stat_handle.set(false); m_stat_handle.set(false);
if let Some(ul) = *m_start_handle { if let Some(ul) = *m_start_handle {
if let Some(br) = *m_end_handle { if let Some(br) = *m_end_handle {
if let Ok(rd) = RegionDisplay::try_from((ul.0, ul.1, br.0, br.1)) { if let Ok(rd) = RegionDisplay::try_from((ul.1, ul.0, br.1, br.0)) {
ct_dispatch.reduce_mut(|state| { ct_dispatch.reduce_mut(|state| {
match ptype { match ptype {
PlateType::Source => state.transfer.transfer_region.source_region = Region::from(&rd), PlateType::Source => state.transfer.transfer_region.source_region = Region::from(&rd),

View File

@ -5,10 +5,12 @@ use yew::prelude::*;
use plate_tool_lib::plate::PlateType; use plate_tool_lib::plate::PlateType;
use plate_tool_lib::plate_instances::PlateInstance; use plate_tool_lib::plate_instances::PlateInstance;
use yewdux::functional::use_store;
// use super::destination_plate::DestinationPlate; // use super::destination_plate::DestinationPlate;
// use super::source_plate::SourcePlate; // use super::source_plate::SourcePlate;
use super::plate::Plate; use super::plate::Plate;
use crate::components::states::MainState;
#[derive(Properties, PartialEq)] #[derive(Properties, PartialEq)]
pub struct PlateContainerProps { pub struct PlateContainerProps {
@ -18,6 +20,8 @@ pub struct PlateContainerProps {
#[function_component] #[function_component]
pub fn PlateContainer(props: &PlateContainerProps) -> Html { pub fn PlateContainer(props: &PlateContainerProps) -> Html {
let (main_state, _) = use_store::<MainState>();
let cell_height = { let cell_height = {
let height = web_sys::window() let height = web_sys::window()
.unwrap() .unwrap()
@ -49,8 +53,15 @@ pub fn PlateContainer(props: &PlateContainerProps) -> Html {
.set_onresize(Some(onresize.as_ref().unchecked_ref())); .set_onresize(Some(onresize.as_ref().unchecked_ref()));
onresize.forget(); // Magic! onresize.forget(); // Magic!
let heatmap_enabled = main_state.preferences.volume_heatmap;
html! { html! {
<div class="plate_container"> <div class="plate_container">
if heatmap_enabled {
<div class="plate_container--heatmap-notice" >
<h3>{"Volume Heatmap Enabled"}</h3>
</div>
}
if let Some(spi) = props.source_dims.clone() { if let Some(spi) = props.source_dims.clone() {
if let Some(dpi) = props.destination_dims.clone() { if let Some(dpi) = props.destination_dims.clone() {
<div class="plate_container--source"> <div class="plate_container--source">

View File

@ -1,6 +1,7 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
use lazy_static::lazy_static; use lazy_static::lazy_static;
use plate_tool_lib::Well;
use regex::Regex; use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use yew::prelude::*; use yew::prelude::*;
@ -8,6 +9,7 @@ use yewdux::prelude::*;
use crate::components::callbacks::transfer_menu_callbacks; use crate::components::callbacks::transfer_menu_callbacks;
use plate_tool_lib::transfer_region::Region; use plate_tool_lib::transfer_region::Region;
use plate_tool_lib::transfer_volume::TransferVolume;
use plate_tool_lib::util::{letters_to_num, num_to_letters}; use plate_tool_lib::util::{letters_to_num, num_to_letters};
use super::states::{CurrentTransfer, MainState}; use super::states::{CurrentTransfer, MainState};
@ -143,7 +145,10 @@ pub fn TransferMenu() -> Html {
<input type="number" name="volume" class="volume_input" <input type="number" name="volume" class="volume_input"
min="0" step="0.1" min="0" step="0.1"
onchange={on_volume_change} onchange={on_volume_change}
value={ct_state.transfer.volume.to_string()}/> value={match ct_state.transfer.volume {
TransferVolume::Single(x) => x.to_string(),
_ => unreachable!(),
}}/>
</div> </div>
} }
<div id="controls"> <div id="controls">
@ -233,10 +238,10 @@ impl TryFrom<&str> for RegionDisplay {
impl From<&Region> for RegionDisplay { impl From<&Region> for RegionDisplay {
fn from(value: &Region) -> Self { fn from(value: &Region) -> Self {
match *value { match *value {
Region::Point((col, row)) => { Region::Point(Well { row, col }) => {
RegionDisplay::try_from((col, row, col, row)).ok().unwrap() RegionDisplay::try_from((col, row, col, row)).ok().unwrap()
} }
Region::Rect(c1, c2) => RegionDisplay::try_from((c1.0, c1.1, c2.0, c2.1)) Region::Rect(c1, c2) => RegionDisplay::try_from((c1.row, c1.col, c2.row, c2.col))
.ok() .ok()
.unwrap(), .unwrap(),
Region::Custom(_) => RegionDisplay { Region::Custom(_) => RegionDisplay {
@ -252,11 +257,11 @@ impl From<&Region> for RegionDisplay {
impl From<&RegionDisplay> for Region { impl From<&RegionDisplay> for Region {
fn from(value: &RegionDisplay) -> Self { fn from(value: &RegionDisplay) -> Self {
if value.col_start == value.col_end && value.row_start == value.row_end { if value.col_start == value.col_end && value.row_start == value.row_end {
Region::Point((value.col_start, value.row_start)) Region::Point(Well { row: value.row_start, col: value.col_start })
} else { } else {
Region::Rect( Region::Rect(
(value.col_start, value.row_start), Well { row: value.row_start, col: value.col_start },
(value.col_end, value.row_end), Well { row: value.row_end, col: value.col_end },
) )
} }
} }

View File

@ -7,12 +7,13 @@ use web_sys::{HtmlDialogElement, HtmlInputElement};
use yew::prelude::*; use yew::prelude::*;
use yewdux::prelude::*; use yewdux::prelude::*;
use crate::components::callbacks::tree_callbacks; use crate::components::callbacks::{main_window_callbacks, tree_callbacks};
use crate::components::new_plate_dialog::NewPlateDialogType;
use crate::components::states::{CurrentTransfer, MainState}; use crate::components::states::{CurrentTransfer, MainState};
#[derive(PartialEq, Properties)] #[derive(PartialEq, Properties)]
pub struct TreeProps { pub struct TreeProps {
pub open_new_plate_callback: Callback<()>, pub open_new_plate_callback: Callback<NewPlateDialogType>,
} }
#[function_component] #[function_component]
@ -98,13 +99,25 @@ pub fn Tree(props: &TreeProps) -> Html {
html! { html! {
<div class="tree"> <div class="tree">
<div id="source-plates"> <div id="source-plates">
<div class="tree--header">
<h3>{"Source Plates:"}</h3> <h3>{"Source Plates:"}</h3>
<button onclick={
let open_new_plate_callback = props.open_new_plate_callback.clone();
move |_| {open_new_plate_callback.emit(NewPlateDialogType::SourceOnly)}
} />
</div>
<ul> <ul>
{source_plates} {source_plates}
</ul> </ul>
</div> </div>
<div id="destination-plates"> <div id="destination-plates">
<div class="tree--header">
<h3>{"Destination Plates:"}</h3> <h3>{"Destination Plates:"}</h3>
<button onclick={
let open_new_plate_callback = props.open_new_plate_callback.clone();
move |_| {open_new_plate_callback.emit(NewPlateDialogType::DestinationOnly)}
} />
</div>
<ul> <ul>
{dest_plates} {dest_plates}
</ul> </ul>
@ -120,14 +133,6 @@ pub fn Tree(props: &TreeProps) -> Html {
delete_button_callback={plate_info_delete_callback}/> delete_button_callback={plate_info_delete_callback}/>
} }
<div id="controls">
<button type="button"
onclick={
let open_new_plate_callback = props.open_new_plate_callback.clone();
move |_| {open_new_plate_callback.emit(())}
}>
{"New Plate"}</button>
</div>
</div> </div>
} }
} }

View File

@ -25,9 +25,9 @@ pub fn plate_test() {
let transfer = transfer_region::TransferRegion { let transfer = transfer_region::TransferRegion {
source_plate: source, source_plate: source,
source_region: transfer_region::Region::Rect((1, 1), (2, 2)), source_region: transfer_region::Region::Rect(Well { row: 1, col: 1 }, Well { row: 2, col: 2 }),
dest_plate: destination, dest_plate: destination,
dest_region: transfer_region::Region::Rect((2, 2), (11, 11)), dest_region: transfer_region::Region::Rect(Well { row: 2, col: 2 }, Well { row: 11, col: 11 }),
interleave_source: (1, 1), interleave_source: (1, 1),
interleave_dest: (3, 3), interleave_dest: (3, 3),
}; };