Compare commits

..

No commits in common. "dev" and "main" have entirely different histories.
dev ... main

78 changed files with 2489 additions and 5114 deletions

1088
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,29 @@
[workspace] [package]
members = ["plate-tool-web", "plate-tool-lib"] name = "plate-tool"
resolver = "2" version = "0.2.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
yew = { version = "0.20.0", features = ["csr"] }
yewdux = "0.9"
wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = ["FormData", "HtmlFormElement",
"HtmlDialogElement", "Blob", "Url", "Window",
"HtmlAnchorElement", "ReadableStream", "HtmlSelectElement", "HtmlOptionElement", "HtmlButtonElement",
"FileReader"] }
js-sys = "0.3"
log = "0.4"
wasm-logger = "0.2"
regex = "1"
lazy_static = "1.4"
uuid = { version = "1.6", features = ["v7", "fast-rng", "macro-diagnostics", "js", "serde"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
csv = "1.2"
getrandom = { version = "0.2", features = ["js"] }
rand = { version = "0.8", features = ["small_rng"] }
[dev-dependencies]
wasm-bindgen-test = "0.3.0"

5
Jenkinsfile vendored
View File

@ -12,20 +12,19 @@ pipeline {
steps { steps {
sh ''' sh '''
. "$HOME/.cargo/env" . "$HOME/.cargo/env"
cd plate-tool-web
trunk build --release --public-url "cool-stuff/$OUTPUT_DIR/" trunk build --release --public-url "cool-stuff/$OUTPUT_DIR/"
''' '''
} }
} }
stage('Archive') { stage('Archive') {
steps { steps {
zip zipFile: "dist.zip", archive: true, dir: "plate-tool-web/dist/" zip zipFile: "dist.zip", archive: true, dir: "dist/"
archiveArtifacts artifacts: "dist.zip", fingerprint: true archiveArtifacts artifacts: "dist.zip", fingerprint: true
} }
} }
stage('Transfer') { stage('Transfer') {
steps { steps {
sh 'echo "put -r plate-tool-web/dist/" | sftp oracle' sh 'echo "put -r dist/" | sftp oracle'
} }
} }
stage('Deploy') { stage('Deploy') {

View File

@ -1,45 +1,29 @@
# plate-tool # plate-tool
A web-based tool for creating and visualizing picklists for your favorite (possibly acoustic) liquid handler. A web-based tool for creating assays for your favorite (acoustic) liquid handler.
## Table of Contents ## Table of Contents
- [Usage](#usage) - [Usage](#Usage)
- [Plates](#plates) - [Installation](#Installation)
- [Adding plates](#adding-plates)
- [Modifying plates](#modifying-plates)
- [Transfers](#transfers)
- [Adding a transfer](#adding-a-transfer)
- [Modifying transfers](#modifying-transfers)
- [Importing and Exporting](#importing-and-Exporting)
- [Export as CSV](#export-as-csv)
- [Export as JSON](#export-as-json)
- [Import from JSON](#import-from-json)
- [Import Transfer from CSV](#import-transfer-from-csv)
- [Other Neat Features](#other-neat-features)
- [Installation](#installation)
## Usage ## Usage
### Plates ### Adding plates
#### Adding plates
When you open plate tool for the first time, When you open plate tool for the first time,
you'll be greeted by a message informing you that no plates are selected. you'll be greeted by a message informing you that no plates are selected.
To add a new plate, click the "Add" button for the corresponding plate type: To add a new plate, click the "New Plate" button:
Once you've added at least one source plate and one destination plate, Once you've added at least one source plate and one destination plate,
click one of each to select them. click one of each to select them.
The right-most pane will now display these plates. The right-most pane will now display these plates.
#### Modifying plates ### Modifying and deleting plates
Suppose you erroneously created a plate, or misspelled its name. Suppose you erroneously created a plate, or misspelled its name.
Double click on that plate in the list (top-left pane) and a new modal will open. Double click on that plate in the list (top-left pane) and a new modal will open.
Here you can rename a plate or delete it. Here you can rename a plate or delete it.
You may also change the format of a plate
(note that this will not delete any data if you accidentally switch to a smaller format).
### Transfers ### Adding a transfer
#### Adding a transfer
Now that you have two plates selected, Now that you have two plates selected,
it's time to add a transfer. it's time to add a transfer.
We can see all of the properties of our transfer in the bottom-left pane. We can see all of the properties of our transfer in the bottom-left pane.
@ -60,7 +44,7 @@ depending on the transfer type and interleave settings.
When all of the settings are to your liking, click the "Save" button. When all of the settings are to your liking, click the "Save" button.
Note that it now appears in the "Transfers" section of the list pane. Note that it now appears in the "Transfers" section of the list pane.
#### Modifying transfers ### Modifying and deleting transfers
If you already saved a transfer and would like to change it, If you already saved a transfer and would like to change it,
click on its entry in the list. click on its entry in the list.
Now change the properties of the transfer as you did during initial creation. Now change the properties of the transfer as you did during initial creation.
@ -75,23 +59,10 @@ Exporting the transfers we have created to a CSV format is the primary (if not s
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 prompted by your browser to select a location for your file. You will be reminded that this is a one-way export (see JSON export/import below),
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: #### Export as JSON (Saving Your Work)
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)
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.
However, you might reasonably want to save a copy of your work However, you might reasonably want to save a copy of your work
either as a backup or to share. either as a backup or to share.
@ -99,8 +70,7 @@ Mouse over the "File" tab, then "Export" as above, then alternatively select "Ex
Your browser will then prompt you to pick a suitable location to save your work as a file. Your browser will then prompt you to pick a suitable location to save your work as a file.
(See note 1 below) (See note 1 below)
#### Import from JSON #### Import from JSON (Recovering Your Work)
##### (Recovering Your Work)
If we want to import one such file, mouse over the "File" tab as before If we want to import one such file, mouse over the "File" tab as before
and select "Import", and finally click "Import from JSON". and select "Import", and finally click "Import from JSON".
This opens a modal where you are prompted to upload (see note 2) This opens a modal where you are prompted to upload (see note 2)
@ -108,6 +78,13 @@ your file; it will then be processed and loaded.
Keep in mind that this will overwrite any work you currently have open, Keep in mind that this will overwrite any work you currently have open,
so you may wish to export first (see above). so you may wish to export first (see above).
#### Import Transfer from CSV (Using a picklist as a transfer)
If you have a CSV generated by another tool (or plate-tool),
you can import it as a single transfer.
To do so, mouse over the "File" tab, then "Import", and finally "Import Transfer from CSV".
When creating transfers via this method, the transfer cannot be edited.
This is useful if you have a pre-existing picklist that you would like to visualize in plate-tool.
_Note 1_: JSON files are plaintext! _Note 1_: JSON files are plaintext!
By default there is little whitespace (this makes comprehending them a challenge) By default there is little whitespace (this makes comprehending them a challenge)
but if we pass it through a "JSON Beautifier" (enter this into your search engine of choice) but if we pass it through a "JSON Beautifier" (enter this into your search engine of choice)
@ -122,28 +99,6 @@ You are welcome to verify (use your browser's developer tools, it should have a
that this application does not "phone home". that this application does not "phone home".
Your data is stored locally (unless you choose to export it and distribute it yourself). Your data is stored locally (unless you choose to export it and distribute it yourself).
#### Import Transfer from CSV
##### (Using a picklist as a transfer)
If you have a CSV generated by another tool (or plate-tool),
you can import it as a single transfer.
To do so, mouse over the "File" tab, then "Import", and finally "Import Transfer from CSV".
When creating transfers via this method, the transfer cannot be edited.
This is useful if you have a pre-existing picklist that you would like to visualize in plate-tool.
You may either manually map plates in a picklist to plates you've already created, or click "Auto" to:
1. Generate all plates in the picklist
2. Generate transfers for all source:destination pairs.
_Note_: If you try to use this feature and no plates are available to select,
there was likely an issue parsing your picklist.
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.
_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
@ -153,7 +108,6 @@ plate-tool will do some magic to take a screenshot of your plate
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
@ -170,24 +124,13 @@ To disable the indicators, mouse over "Options" (top-left of screen), then "Styl
then click "Toggle transfer hashes". then click "Toggle transfer hashes".
To turn them back on, do the exact same thing. To turn them back on, do the exact same thing.
#### Transfer Volume Heatmap (Experimental)
This can be used to verify that all of the wells in a plate will have the same volume
transferred at a glance.
Wells will be colored based on the sum of all transfers using that plate.
To toggle this feature, mouse over "Options", then "Styles", then click "Toggle volume heatmap".
_NOTE_: The scale for the colors is spaced linearly; if you have a well that is being used
significantly more than some others, it may be difficult to see the difference between other wells
with more similar volumes.
If you have a use case that would benefit from a logarithmic scale here, please let me know.
## Installation ## Installation
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 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. ) (_Note:_ If you run Windows you're probably best off doing the following in WSL2)
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/),
@ -197,7 +140,7 @@ Here's how:
2. Install [trunk](https://trunkrs.dev/) 2. Install [trunk](https://trunkrs.dev/)
- Run `cargo install --locked trunk` - Run `cargo install --locked trunk`
3. Clone this repository using git 3. Clone this repository using git
4. Enter the plate-tool-web directory and run `trunk serve` 4. Enter the project directory and run `trunk serve`
- You may need to check where `cargo` is installing binaries by default. For me, they're at `~/.cargo/bin`. - You may need to check where `cargo` is installing binaries by default. For me, they're at `~/.cargo/bin`.
If trunk is not automatically placed in your path, you would then run `/your/path/to/.cargo/bin/trunk serve`. If trunk is not automatically placed in your path, you would then run `/your/path/to/.cargo/bin/trunk serve`.
- You can instead run `trunk build --release` for a more performant binary. - You can instead run `trunk build --release` for a more performant binary.

View File

@ -7,8 +7,6 @@ dialog {
color: $color-dark; color: $color-dark;
background: $color-white; background: $color-white;
padding-right: 5%;
} }
dialog > form[method="dialog"] { dialog > form[method="dialog"] {
@ -37,22 +35,3 @@ dialog > form[method="dialog"] {
} }
} }
} }
.shifted_dialog {
top: 50vh;
left: 50vw;
margin: 0;
}
.close_button {
color: red;
position: absolute;
top: 5%;
right: 2%;
background: rgba(0%,0%,0%,10%);
&:hover {
color: rgb(0%,100%,100%);
background: rgba(0%,0%,0%,80%);
}
}

View File

@ -0,0 +1,18 @@
@use "sass:color";
@use "../variables" as *;
div.plate_container {
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: center;
border: 2px solid $color-dark;
grid-column: right / right;
grid-row: upper / 3;
h2 {
margin-bottom: 1%;
text-align: center;
}
}

View File

@ -57,35 +57,13 @@ td.current_select div.plate_cell_inner {
.W1536 { .W1536 {
th { th {
font-size: 0.9rem; font-size: 0.9rem;
line-height: 0px;
}
tr:first-child {
th {
line-height: inherit;
}
}
td {
padding: 0;
}
td.current_select div.plate_cell_inner {
border: 2px solid black;
} }
} }
.W3456 { .W3456 {
th { th {
font-size: 0.6rem; font-size: 0.9rem;
line-height: 0px; line-height: 0px;
} padding-bottom: 0.4rem;
tr:first-child {
th {
line-height: inherit;
}
}
td {
padding: 0;
}
td.current_select div.plate_cell_inner {
border: 2px solid black;
} }
} }

View File

@ -1,6 +1,5 @@
@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;
@ -41,11 +40,6 @@ 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;
@ -56,10 +50,10 @@ input {
padding: 0; padding: 0;
&[type="text"] { &[type="text"] {
width: 6em; width: 4em;
} }
&[name="name"] { &[name="name"] {
width: calc(100% - 6em); // Override above width: 6em; // Override above
} }
&[type="number"] { &[type="number"] {
width: 2em; width: 2em;

View File

@ -1,6 +1,5 @@
@use "sass:color"; @use "sass:color";
@use "../variables" as *; @use "../variables" as *;
@use "../button" as *;
$selection-border-width: 2px; $selection-border-width: 2px;
@ -54,17 +53,3 @@ 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

@ -30,7 +30,6 @@ div.upper_menu {
margin: 0; margin: 0;
cursor: pointer; cursor: pointer;
font-size: calc($menu-height*0.7); font-size: calc($menu-height*0.7);
width: 100%;
} }
* { * {

View File

@ -4,7 +4,6 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<link data-trunk rel="scss" href="assets/scss/index.scss"> <link data-trunk rel="scss" href="assets/scss/index.scss">
<link data-trunk rel="copy-dir" href="assets/fonts"> <link data-trunk rel="copy-dir" href="assets/fonts">
<link data-trunk rel="rust" data-bin="plate-tool-web" />
<script data-trunk src="assets/js/screenshot_utility.js"></script> <script data-trunk src="assets/js/screenshot_utility.js"></script>
<script data-trunk src="assets/js/html2canvas.js"></script> <script data-trunk src="assets/js/html2canvas.js"></script>
<title>Plate Tool</title> <title>Plate Tool</title>

View File

@ -1,2 +0,0 @@
/target
/dist

View File

@ -1,339 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "aho-corasick"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
dependencies = [
"memchr",
]
[[package]]
name = "atomic"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
[[package]]
name = "bumpalo"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "csv"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe"
dependencies = [
"csv-core",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "csv-core"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70"
dependencies = [
"memchr",
]
[[package]]
name = "getrandom"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
name = "itoa"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
[[package]]
name = "js-sys"
version = "0.3.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "log"
version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "memchr"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "plate-tool-lib"
version = "0.1.0"
dependencies = [
"csv",
"getrandom",
"log",
"rand",
"regex",
"serde",
"serde_json",
"uuid",
]
[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro2"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "regex"
version = "1.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]]
name = "ryu"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
[[package]]
name = "serde"
version = "1.0.196"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.196"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.113"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "syn"
version = "2.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "uuid"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a"
dependencies = [
"atomic",
"getrandom",
"rand",
"serde",
"uuid-macro-internal",
"wasm-bindgen",
]
[[package]]
name = "uuid-macro-internal"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7abb14ae1a50dad63eaa768a458ef43d298cd1bd44951677bd10b732a9ba2a2d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838"

View File

@ -1,19 +0,0 @@
[package]
name = "plate-tool-lib"
version = "0.5.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
log = "0.4"
regex = "1"
uuid = { version = "1.6", features = ["v7", "fast-rng", "macro-diagnostics", "js", "serde"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
csv = "1.2"
getrandom = { version = "0.2", features = ["js"] }
rand = { version = "0.8", features = ["small_rng"] }
lazy_static = "1.4"
serde_with = { version = "3.9.0", features = ["json"] }
serde_yaml = "0.9.34"

View File

@ -1,27 +0,0 @@
use serde::Serialize;
use super::TransferRecord;
/// Format preferred by the Echo Client when using the Pick-List function.
///
/// Note that this does not export plate info!
/// Exports of this type should combine all transfers between the two actively selected plates.
#[derive(Serialize, Debug)]
pub struct EchoClientTransferRecord {
#[serde(rename = "SrcWell")]
pub source_well: String,
#[serde(rename = "DestWell")]
pub destination_well: String,
#[serde(rename = "XferVol")]
pub volume: f32,
}
impl From<TransferRecord> for EchoClientTransferRecord {
fn from(value: TransferRecord) -> Self {
EchoClientTransferRecord {
source_well: value.source_well,
destination_well: value.destination_well,
volume: value.volume,
}
}
}

View File

@ -1,231 +0,0 @@
//! Auto module for importing picklists without creating plates first
//!
//! Before the auto module, it was necessary for a user to create the plates
//! used in an imported transfer before import.
use super::{string_well_to_pt, TransferRecord, read_csv};
use crate::plate::{PlateFormat, PlateType};
use crate::plate_instances::PlateInstance;
use crate::transfer::Transfer;
use crate::transfer_volume::{TransferVolume, VolumeMaps};
use crate::Well;
use crate::transfer_region::{CustomRegion, Region, TransferRegion};
use std::collections::{HashSet, HashMap};
pub struct AutoOutput {
pub sources: Vec<PlateInstance>,
pub destinations: Vec<PlateInstance>,
pub transfers: Vec<Transfer>,
}
/// Two lists of plates that are guaranteed to be unique (no duplicate plates)
struct UniquePlates {
sources: Vec<PlateInstance>,
destinations: Vec<PlateInstance>,
}
const W96_COL_MAX: u8 = 12;
const W96_ROW_MAX: u8 = 8;
const W384_COL_MAX: u8 = 24;
const W384_ROW_MAX: u8 = 16;
const W1536_COL_MAX: u8 = 48;
const W1536_ROW_MAX: u8 = 32;
/// Main function for the auto module
/// Takes a list of pre-deserialized transfer records and generates output
/// This output is 3 lists of:
/// 1. The source plates used in the picklist
/// 2. The destination plates used in the picklist
/// 3. All of the transfers between those plates
pub fn auto(records: &[TransferRecord]) -> AutoOutput {
let unique_plates = find_unique_plates(records);
let transfers = get_transfer_for_all_pairs(records, &unique_plates);
AutoOutput { sources: unique_plates.sources, destinations: unique_plates.destinations, transfers }
}
/// Helper function that reads a CSV and immediately dumps the resulting transfer records
/// into the auto function defined in this module.
pub fn read_csv_auto(data: &str) -> AutoOutput {
let transfer_records = read_csv(data);
auto(&transfer_records)
}
/// Looks through a list of transfer records and generates two lists
/// of all of the unique plates found in those transfers.
/// Worth noting that the Transfer struct requires two associated PlateInstances;
/// these PlateInstance structs are generated here---they are not present in a TransferRecord.
/// See notes on the UniquePlates struct
fn find_unique_plates(records: &[TransferRecord]) -> UniquePlates {
let mut source_names: HashSet<&str> = HashSet::new();
let mut destination_names: HashSet<&str> = HashSet::new();
for record in records {
source_names.insert(&record.source_plate);
destination_names.insert(&record.destination_plate);
}
let mut sources: Vec<PlateInstance> = Vec::with_capacity(source_names.len());
for source_name in source_names {
let filtered_records: Vec<&TransferRecord> = records
.iter()
.filter(|x| x.source_plate == source_name)
.collect();
let format_guess = guess_plate_size(&filtered_records, PlateType::Source);
sources.push(PlateInstance::new(
PlateType::Source,
format_guess,
source_name.to_string(),
))
}
let mut destinations: Vec<PlateInstance> = Vec::with_capacity(destination_names.len());
for destination_name in destination_names {
let filtered_records: Vec<&TransferRecord> = records
.iter()
.filter(|x| x.destination_plate == destination_name)
.collect();
let format_guess = guess_plate_size(&filtered_records, PlateType::Destination);
destinations.push(PlateInstance::new(
PlateType::Destination,
format_guess,
destination_name.to_string(),
))
}
UniquePlates {
sources,
destinations,
}
}
/// Tries to guess the format of a plate given the southeastern-est well used.
/// A picklist does not necessarily contain plate format info; this method guesses
/// based on the well furthest from A1 that exists in a transfer.
/// It's possible for a 1536 well plate to be seen as a 384 well if all of the wells
/// used could have fit in the top-left quadrant of a 384 well plate
/// (i.e. all lower than P24 in this case)
fn guess_plate_size(plate_filtered_records: &[&TransferRecord], which: PlateType) -> PlateFormat {
let mut guess = PlateFormat::W96; // Never guess smaller than 96
for record in plate_filtered_records {
if let Some(Well { row, col }) = string_well_to_pt(match which {
PlateType::Source => &record.source_well,
PlateType::Destination => &record.destination_well,
}) {
if row > W1536_ROW_MAX || col > W1536_COL_MAX {
return PlateFormat::W3456;
} else if row > W384_ROW_MAX || col > W384_COL_MAX {
guess = PlateFormat::W1536;
} else if row > W96_ROW_MAX || col > W96_COL_MAX {
guess = PlateFormat::W384;
}
}
}
guess
}
fn get_transfer_for_all_pairs(
records: &[TransferRecord],
unique_plates: &UniquePlates,
) -> Vec<Transfer> {
let mut transfers: Vec<Transfer> = Vec::new();
for source_instance in &unique_plates.sources {
for destination_instance in &unique_plates.destinations {
if let Some(transfer) =
get_transfer_for_pair(records, source_instance, destination_instance)
{
transfers.push(transfer);
}
}
}
transfers
}
/// Given a source and destination plate, get all of the wells transferred
/// from source to destination and turn this into a Transfer.
///
/// Note that even if you have what looks like two separate transfers,
/// this will join all transfers for a given source-dest pair.
/// For example:
/// Consider expanding all of src:A1 to the column dst:A
/// and src:B1 -> row dst:1
/// If you were making this transfer in plate tool, it would be easiest
/// to do this as two separate transfers.
/// On import, this would be seen as one transfer.
fn get_transfer_for_pair(
records: &[TransferRecord],
source: &PlateInstance,
destination: &PlateInstance,
) -> Option<Transfer> {
let source_name: &str = &source.name;
let destination_name: &str = &destination.name;
let mut filtered_records = records
.iter()
.filter(|x| x.source_plate == source_name && x.destination_plate == destination_name)
.peekable();
filtered_records.peek()?;
let mut source_wells: HashSet<Well> = 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 {
let source_point_opt = string_well_to_pt(&record.source_well);
let destination_point_opt = string_well_to_pt(&record.destination_well);
if source_point_opt.and(destination_point_opt).is_some() {
let source_point = source_point_opt.unwrap();
let destination_point = destination_point_opt.unwrap();
source_wells.insert(source_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<Well> = source_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 =
Region::Custom(CustomRegion::new(source_wells_vec, destination_wells_vec));
let transfer_region = TransferRegion {
source_plate: source.plate,
dest_plate: destination.plate,
interleave_source: (1, 1),
interleave_dest: (1, 1),
source_region: custom_region.clone(),
dest_region: custom_region,
};
let transfer_name = format!("{} to {}", source.name, destination.name);
let mut transfer = Transfer::new(
source.clone(),
destination.clone(),
transfer_region,
transfer_name,
);
transfer.volume = transfer_volume;
Some(transfer)
}

View File

@ -1,109 +0,0 @@
use crate::transfer_volume::TransferVolume;
use crate::util::*;
use crate::{transfer::Transfer, Well};
use super::{
alternative_formats::EchoClientTransferRecord, mangle_headers::mangle_headers,
transfer_record::TransferRecordDeserializeIntermediate, TransferRecord,
};
use lazy_static::lazy_static;
use regex::Regex;
use std::error::Error;
pub fn transfer_to_records(
tr: &Transfer,
src_barcode: &str,
dest_barcode: &str,
) -> Vec<TransferRecord> {
let source_wells = tr.transfer_region.get_source_wells();
let map = tr.transfer_region.calculate_map();
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 {
let dest_wells = map(s_well);
if let Some(dest_wells) = 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 {
source_plate: src_barcode.to_string(),
source_well: format!("{}{}", num_to_letters(s_well.row).unwrap(), s_well.col),
destination_plate: dest_barcode.to_string(),
destination_well: format!(
"{}{}",
num_to_letters(d_well.row).unwrap(),
d_well.col
),
volume,
concentration: None,
})
}
}
}
records
}
pub fn records_to_csv(trs: Vec<TransferRecord>) -> Result<String, Box<dyn Error>> {
let mut wtr = csv::WriterBuilder::new().from_writer(vec![]);
for record in trs {
wtr.serialize(record)?
}
let data = String::from_utf8(wtr.into_inner()?)?;
Ok(data)
}
pub fn records_to_echo_client_csv(trs: Vec<TransferRecord>) -> Result<String, Box<dyn Error>> {
let mut wtr = csv::WriterBuilder::new().from_writer(vec![]);
for record in trs {
wtr.serialize(Into::<EchoClientTransferRecord>::into(record))?
}
let data = String::from_utf8(wtr.into_inner()?)?;
Ok(data)
}
/// Converts "spreadsheet format" well identification to coordinates
pub fn string_well_to_pt(input: &str) -> Option<Well> {
lazy_static! {
static ref REGEX: Regex = Regex::new(r"([A-Z,a-z]+)(\d+)").unwrap();
// Can this be removed?
static ref REGEX_ALT: Regex = Regex::new(r"(\d+)").unwrap();
}
if let Some(c1) = REGEX.captures(input) {
if let (Some(row), Some(col)) = (letters_to_num(&c1[1]), c1[2].parse::<u8>().ok()) {
return Some(Well { row, col });
} else {
return None;
}
}
None
}
pub fn read_csv(data: &str) -> Vec<TransferRecord> {
let modified = mangle_headers(data);
let mut rdr = csv::Reader::from_reader(modified.as_bytes());
let mut records: Vec<TransferRecord> = Vec::new();
for record in rdr.deserialize::<TransferRecordDeserializeIntermediate>() {
match record {
Ok(r) => {
if !r.is_empty() {
records.push(r.into());
}
}
Err(e) => {
log::debug!("{:?}", e);
}
}
}
records
}

View File

@ -1,74 +0,0 @@
//! This contains the `mangle_headers` function and its helpers
//!
//! Mangling headers is necessary to normalize CSV column names before
//! deserialization can be done by Serde.
//! Field detection occurs in `detect_field`.
//!
//! As of lib 0.4.0, unrecognized headers are ignored and left untouched by mangling.
//! However, they are not passed through to any subsequent exports from plate-tool.
pub fn mangle_headers(data: &str) -> String {
let (header, rows) = data.split_at(data.find('\n').unwrap());
let fields = header.trim().split(',');
let mut modified_headers: Vec<String> = Vec::new();
for field in fields {
if let Some(f) = detect_field(field) {
modified_headers.push(f.to_string());
} else {
log::debug!("Unk field: {:?}", field)
}
}
modified_headers.join(",") + "\n" + rows
}
fn detect_field(field: &str) -> Option<Field> {
// NOTE: Don't return none! Consider refactor to -> Field later on.
// Returning none means a field will be dropped, which requires the user to manually drop
// columns from their picklist.
match field.trim().to_lowercase() {
x if x.contains("source") || x.contains("src") => match x {
_ if x.contains("plate") || x.contains("barcode") => Some(Field::SourcePlate),
_ if x.contains("well") => Some(Field::SourceWell),
_ if x.contains("format") || x.contains("fmt") => Some(Field::SourceFormat),
_ => Some(Field::Other(x)), // Retain unknown fields
},
x if x.contains("destination") || x.contains("dest") => match x {
_ if x.contains("plate") || x.contains("barcode") => Some(Field::DestinationPlate),
_ if x.contains("well") => Some(Field::DestinationWell),
_ if x.contains("format") || x.contains("fmt") => Some(Field::DestinationFormat),
_ => Some(Field::Other(x)), // Retain unknown fields
},
x if x.contains("volume") => Some(Field::Volume),
x if x.contains("concentration") => Some(Field::Concentration),
x => Some(Field::Other(x)), // Retain unknown fields
}
}
enum Field {
SourcePlate,
DestinationPlate,
SourceWell,
DestinationWell,
SourceFormat,
DestinationFormat,
Volume,
Concentration,
Other(String),
}
impl ToString for Field {
fn to_string(&self) -> String {
match self {
Field::SourcePlate => "sourceplate".to_string(),
Field::DestinationPlate => "destinationplate".to_string(),
Field::SourceWell => "sourcewell".to_string(),
Field::DestinationWell => "destinationwell".to_string(),
Field::SourceFormat => "sourceformat".to_string(),
Field::DestinationFormat => "destinationformat".to_string(),
Field::Volume => "volume".to_string(),
Field::Concentration => "concentration".to_string(),
Field::Other(x) => x.to_string(),
}
}
}

View File

@ -1,10 +0,0 @@
mod transfer_record;
mod conversion;
mod auto;
mod mangle_headers;
mod alternative_formats;
pub use transfer_record::TransferRecord;
pub use conversion::*;
pub use auto::{auto, read_csv_auto};

View File

@ -1,107 +0,0 @@
use serde::{Deserialize, Serialize};
use crate::{plate::PlateFormat, util::num_to_letters};
/// Represents a single line of a CSV picklist.
/// In practice, this is generated from the deserialize intermediate.
/// A list of `TransferRecord`s can be used to create a `transfer::Transfer` struct;
/// see the `auto` module for this.
#[derive(Serialize, Debug, Clone)]
pub struct TransferRecord {
#[serde(rename = "Source Barcode")]
pub source_plate: String,
#[serde(rename = "Source Well")]
pub source_well: String,
#[serde(rename = "Dest Barcode")]
pub destination_plate: String,
#[serde(rename = "Destination Well")]
pub destination_well: String,
#[serde(rename = "Transfer Volume")]
pub volume: f32,
#[serde(rename = "Concentration")]
pub concentration: Option<f32>,
}
/// The deserialization intermediate is generated from a mangled CSV s.t.
/// the column headers are standardized to those given to the derive macro.
///
/// `TransferRecord` can *always* be generated from the intermediate.
/// Currently there's no reason why the inverse couldn't be true,
/// but there's no reason to convert back into the deserialize-only intermediate.
#[derive(Deserialize, Debug, Clone)]
pub struct TransferRecordDeserializeIntermediate {
#[serde(rename = "sourceplate")]
source_plate: String,
#[serde(rename = "destinationplate")]
destination_plate: String,
#[serde(rename = "sourcewell")]
source_well: String,
#[serde(rename = "sourceformat")]
source_format: Option<String>,
#[serde(rename = "destinationwell")]
destination_well: String,
#[serde(rename = "destinationformat")]
destination_format: Option<String>,
#[serde(rename = "volume")]
volume: Option<f32>,
#[serde(rename = "concentration")]
concentration: Option<f32>,
}
impl From<TransferRecordDeserializeIntermediate> for TransferRecord {
fn from(value: TransferRecordDeserializeIntermediate) -> Self {
let mut source_well: String = value.source_well;
if let Some(pformat) = value
.source_format
.and_then(|x| PlateFormat::try_from(x.as_str()).ok())
{
if let Ok(well_number) = source_well.parse::<u16>() {
if let Some(alphanumeric) = numeric_well_to_alphanumeric(well_number, pformat) {
source_well = alphanumeric;
}
}
}
let mut destination_well: String = value.destination_well;
if let Some(pformat) = value
.destination_format
.and_then(|x| PlateFormat::try_from(x.as_str()).ok())
{
if let Ok(well_number) = destination_well.parse::<u16>() {
if let Some(alphanumeric) = numeric_well_to_alphanumeric(well_number, pformat) {
destination_well = alphanumeric;
}
}
}
let volume = value.volume.unwrap_or(2.5f32);
TransferRecord {
source_plate: value.source_plate,
destination_plate: value.destination_plate,
source_well,
destination_well,
volume,
concentration: value.concentration,
}
}
}
impl TransferRecordDeserializeIntermediate {
/// Used to cull malformed picklist entries that lack required information
pub fn is_empty(&self) -> bool {
self.source_plate.is_empty()
|| self.destination_plate.is_empty()
|| self.source_well.is_empty()
|| self.destination_well.is_empty()
}
}
fn numeric_well_to_alphanumeric(input: u16, pformat: PlateFormat) -> Option<String> {
let column_height: u16 = pformat.size().0 as u16;
let column = input.div_ceil(column_height);
let row = input % column_height;
let row_str = num_to_letters(row as u8)?;
Some(format!("{}{}", row_str, column))
}

View File

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

@ -1,69 +0,0 @@
pub fn letters_to_num(letters: &str) -> Option<u8> {
let mut num: u8 = 0;
for (i, letter) in letters.to_ascii_uppercase().chars().rev().enumerate() {
let n = letter as u8;
if !(65..=90).contains(&n) {
return None;
}
num = num.checked_add((26_i32.pow(i as u32) * (n as i32 - 64)).try_into().ok()?)?;
}
Some(num)
}
pub fn num_to_letters(num: u8) -> Option<String> {
if num == 0 {
return None;
} // Otherwise, we will not return none!
// As another note, we can't represent higher than "IV" anyway;
// thus there's no reason for a loop (26^n with n>1 will NOT occur).
let mut text = "".to_string();
let mut digit1 = num.div_euclid(26u8);
let mut digit2 = num.rem_euclid(26u8);
if digit1 > 0 && digit2 == 0u8 {
digit1 -= 1;
digit2 = 26;
}
if digit1 != 0 {
text.push((64 + digit1) as char)
}
text.push((64 + digit2) as char);
Some(text.to_string())
}
#[cfg(test)]
mod tests {
use super::{letters_to_num, num_to_letters};
#[test]
fn test_letters_to_num() {
assert_eq!(letters_to_num("D"), Some(4));
assert_eq!(letters_to_num("d"), None);
assert_eq!(letters_to_num("AD"), Some(26 + 4));
assert_eq!(letters_to_num("CG"), Some(3 * 26 + 7));
}
#[test]
fn test_num_to_letters() {
println!("27 is {:?}", num_to_letters(27));
assert_eq!(num_to_letters(1), Some("A".to_string()));
assert_eq!(num_to_letters(26), Some("Z".to_string()));
assert_eq!(num_to_letters(27), Some("AA".to_string()));
assert_eq!(num_to_letters(111), Some("DG".to_string()));
}
#[test]
fn test_l2n_and_n2l() {
assert_eq!(
num_to_letters(letters_to_num("A").unwrap()),
Some("A".to_string())
);
assert_eq!(
num_to_letters(letters_to_num("BJ").unwrap()),
Some("BJ".to_string())
);
for i in 1..=255 {
assert_eq!(letters_to_num(&num_to_letters(i).unwrap()), Some(i));
}
}
}

View File

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

@ -1,2 +0,0 @@
/target
/dist

View File

@ -1,36 +0,0 @@
[package]
name = "plate-tool-web"
version = "0.5.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
plate-tool-lib = { path = "../plate-tool-lib" }
yew = { version = "0.21.0", features = ["csr"] }
yewdux = "0.10"
wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = ["FormData", "HtmlFormElement",
"HtmlDialogElement", "Blob", "Url", "Window",
"HtmlAnchorElement", "ReadableStream", "HtmlSelectElement", "HtmlOptionElement", "HtmlButtonElement",
"FileReader", "HtmlCollection"] }
js-sys = "0.3"
log = "0.4"
wasm-logger = "0.2"
regex = "1"
lazy_static = "1.4"
uuid = { version = "1.6", features = ["v7", "fast-rng", "macro-diagnostics", "js", "serde"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
csv = "1.2"
getrandom = { version = "0.2", features = ["js"] }
rand = { version = "0.8", features = ["small_rng"] }
[dev-dependencies]
wasm-bindgen-test = "0.3.0"
[profile.release]
opt-level = 2
[profile.dev.package."*"]
opt-level = 2

View File

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

@ -1,66 +0,0 @@
@use "sass:color";
@use "../variables" as *;
div.plate_container {
position: relative;
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: center;
border: 2px solid $color-dark;
grid-column: right / right;
grid-row: upper / 3;
h2 {
margin-bottom: 1%;
text-align: center;
}
&>div {
display: grid;
grid-template-rows: auto auto;
grid-template-rows: auto auto;
&>h2:nth-of-type(1) {
grid-column: 2;
grid-row: 1;
}
&>h2:nth-of-type(2) {
grid-column: 1;
grid-row: 2;
writing-mode: vertical-rl;
transform: rotate(-180deg);
}
&>div {
grid-column:2;
grid-row: 2;
}
}
div.plate_container--upper-left {
position: absolute;
top: 0.5em;
left: 0.5em;
display: flex;
flex-direction: column;
}
div.plate_container--heatmap-notice {
animation: 1s 1 attention_on_load;
}
}
@keyframes attention_on_load {
from{
color: $color-light;
transform: scale(1.05);
}
to {
color: inherit;
}
}

View File

@ -1,63 +0,0 @@
#![allow(non_snake_case)]
use js_sys::Array;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{
Blob, HtmlAnchorElement, Url,
};
use yew::prelude::*;
use crate::components::states::MainState;
use crate::state_to_csv;
// type NoParamsCallback = Box<dyn Fn(())>;
pub fn export_csv_button_callback(main_state: std::rc::Rc<MainState>) -> Callback<MouseEvent> {
Callback::from(move |_| {
if main_state.transfers.is_empty() {
web_sys::window()
.unwrap()
.alert_with_message("No transfers to export.")
.unwrap();
return;
}
if let Ok(csv) = state_to_csv(&main_state) {
save_str(&csv, "transfers.csv");
}
})
}
pub fn export_json_button_callback(main_state: std::rc::Rc<MainState>) -> Callback<MouseEvent> {
Callback::from(move |_| {
if let Ok(json) = serde_json::to_string(&main_state) {
save_str(&json, "plate-tool-state.json");
} else {
web_sys::window()
.unwrap()
.alert_with_message("Failed to export.")
.unwrap();
}
})
}
fn save_str(data: &str, name: &str) {
let blob =
Blob::new_with_str_sequence(&Array::from_iter(std::iter::once(JsValue::from_str(data))));
if let Ok(blob) = blob {
let url = Url::create_object_url_with_blob(&blob).expect("We have a blob, why not URL?");
// Beneath is the cool hack to download files
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let anchor = document
.create_element("a")
.unwrap()
.dyn_into::<HtmlAnchorElement>()
.unwrap();
anchor.set_download(name);
anchor.set_href(&url);
anchor.click();
}
}

View File

@ -1,306 +0,0 @@
use std::collections::HashSet;
use wasm_bindgen::{prelude::*, JsCast};
use web_sys::{
FileReader, HtmlButtonElement, HtmlDialogElement, HtmlElement, HtmlFormElement,
HtmlInputElement, HtmlOptionElement, HtmlSelectElement,
};
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::states::MainState;
use plate_tool_lib::transfer::Transfer;
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 super::create_close_button;
pub fn import_transfer_csv_input_callback(
main_dispatch: Dispatch<MainState>,
modal: HtmlDialogElement,
) -> Closure<dyn FnMut(Event)> {
Closure::<dyn FnMut(_)>::new(move |e: Event| {
if let Some(input) = e.current_target() {
let input = input
.dyn_into::<HtmlInputElement>()
.expect("We know this is an input.");
if let Some(files) = input.files() {
if let Some(file) = files.get(0) {
let fr = web_sys::FileReader::new().unwrap();
fr.read_as_text(&file).unwrap();
let fr1 = fr.clone(); // Clone to avoid outliving closure
let main_dispatch = main_dispatch.clone(); // Clone to satisfy FnMut
// trait
let modal = modal.clone();
let onload = import_transfer_csv_onload_callback(main_dispatch, fr1, modal);
fr.set_onload(Some(onload.as_ref().unchecked_ref()));
onload.forget(); // Magic (don't touch)
}
}
}
})
}
pub fn import_transfer_csv_callback(main_dispatch: Dispatch<MainState>) -> Callback<MouseEvent> {
Callback::from(move |_| {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let body = document.body().unwrap();
let modal = document
.create_element("dialog")
.unwrap()
.dyn_into::<HtmlDialogElement>()
.unwrap();
modal.set_text_content(Some("Import File:"));
let onclose_callback = {
let modal = modal.clone();
Closure::<dyn FnMut(_)>::new(move |_: Event| {
modal.remove();
})
};
modal.set_onclose(Some(onclose_callback.as_ref().unchecked_ref()));
let close_button = create_close_button(&onclose_callback);
onclose_callback.forget();
modal.append_child(&close_button).unwrap();
let form = document
.create_element("form")
.unwrap()
.dyn_into::<HtmlFormElement>()
.unwrap();
let input = document
.create_element("input")
.unwrap()
.dyn_into::<HtmlInputElement>()
.unwrap();
input.set_type("file");
input.set_accept(".csv");
form.append_child(&input).unwrap();
let input_callback = {
let main_dispatch = main_dispatch.clone();
let modal = modal.clone();
import_transfer_csv_input_callback(main_dispatch, modal)
};
input.set_onchange(Some(input_callback.as_ref().unchecked_ref()));
input_callback.forget(); // Magic straight from the docs, don't touch :(
modal.append_child(&form).unwrap();
body.append_child(&modal).unwrap();
modal.show_modal().unwrap();
})
}
pub fn import_transfer_csv_onload_callback(
main_dispatch: Dispatch<MainState>,
file_reader: FileReader,
modal: HtmlDialogElement,
) -> Closure<dyn FnMut(Event)> {
Closure::<dyn FnMut(_)>::new(move |_: Event| {
if let Some(value) = &file_reader.result().ok().and_then(|v| v.as_string()) {
let records = plate_tool_lib::csv::read_csv(value);
let mut sources: HashSet<String> = HashSet::new();
let mut destinations: HashSet<String> = HashSet::new();
for record in records.iter() {
sources.insert(record.source_plate.clone());
destinations.insert(record.destination_plate.clone());
}
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let auto_button = document
.create_element("button")
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap();
auto_button.set_inner_text("Auto");
let auto_button_callback = auto_callback(main_dispatch.clone(), &records);
auto_button.set_onclick(Some(auto_button_callback.as_ref().unchecked_ref()));
auto_button_callback.forget();
let form = document
.create_element("form")
.unwrap()
.dyn_into::<HtmlFormElement>()
.unwrap();
let from_source = document
.create_element("select")
.unwrap()
.dyn_into::<HtmlSelectElement>()
.unwrap();
for source in sources {
let option = document
.create_element("option")
.unwrap()
.dyn_into::<HtmlOptionElement>()
.unwrap();
option.set_value(&source);
option.set_text(&source);
from_source.append_child(&option).unwrap();
}
let to_source = document
.create_element("select")
.unwrap()
.dyn_into::<HtmlSelectElement>()
.unwrap();
for source in &main_dispatch.get().source_plates {
let option = document
.create_element("option")
.unwrap()
.dyn_into::<HtmlOptionElement>()
.unwrap();
option.set_value(&source.name);
option.set_text(&source.name);
to_source.append_child(&option).unwrap();
}
let from_dest = document
.create_element("select")
.unwrap()
.dyn_into::<HtmlSelectElement>()
.unwrap();
for dest in destinations {
let option = document
.create_element("option")
.unwrap()
.dyn_into::<HtmlOptionElement>()
.unwrap();
option.set_value(&dest);
option.set_text(&dest);
from_dest.append_child(&option).unwrap();
}
let to_dest = document
.create_element("select")
.unwrap()
.dyn_into::<HtmlSelectElement>()
.unwrap();
for dest in &main_dispatch.get().destination_plates {
let option = document
.create_element("option")
.unwrap()
.dyn_into::<HtmlOptionElement>()
.unwrap();
option.set_value(&dest.name);
option.set_text(&dest.name);
to_dest.append_child(&option).unwrap();
}
let submit = document
.create_element("button")
.unwrap()
.dyn_into::<HtmlButtonElement>()
.unwrap();
submit.set_value("Submit");
submit.set_inner_text("Submit");
let submit_callback = {
let main_dispatch = main_dispatch.clone();
let from_source = from_source.clone();
let to_source = to_source.clone();
let from_dest = from_dest.clone();
let to_dest = to_dest.clone();
import_transfer_csv_submit_callback(
main_dispatch,
from_source,
to_source,
from_dest,
to_dest,
records,
)
};
submit.set_onclick(Some(submit_callback.as_ref().unchecked_ref()));
submit_callback.forget();
form.append_child(&from_source).unwrap();
form.append_child(&to_source).unwrap();
form.append_child(&from_dest).unwrap();
form.append_child(&to_dest).unwrap();
modal.append_child(&submit).unwrap();
modal.append_child(&form).unwrap();
modal.append_child(&auto_button).unwrap();
}
})
}
pub fn import_transfer_csv_submit_callback(
main_dispatch: Dispatch<MainState>,
from_source: HtmlSelectElement,
to_source: HtmlSelectElement,
from_dest: HtmlSelectElement,
to_dest: HtmlSelectElement,
records: Vec<TransferRecord>,
) -> Closure<dyn FnMut(Event)> {
Closure::<dyn FnMut(_)>::new(move |_: Event| {
let from_source = from_source.value();
let to_source = to_source.value();
let from_dest = from_dest.value();
let to_dest = to_dest.value();
let records: Vec<(Well, Well)> = records
.iter()
.filter(|record| record.source_plate == from_source)
.filter(|record| record.destination_plate == from_dest)
.map(|record| {
(
string_well_to_pt(&record.source_well).unwrap(),
string_well_to_pt(&record.destination_well).unwrap(),
)
})
.collect();
let spi = main_dispatch
.get()
.source_plates
.iter()
.find(|src| src.name == to_source)
.unwrap()
.clone();
let dpi = main_dispatch
.get()
.destination_plates
.iter()
.find(|dest| dest.name == to_dest)
.unwrap()
.clone();
let custom_region = Region::new_custom(&records);
let transfer_region = TransferRegion {
source_region: custom_region.clone(),
dest_region: custom_region,
interleave_source: (1, 1),
interleave_dest: (1, 1),
source_plate: spi.plate,
dest_plate: dpi.plate,
};
let transfer = Transfer::new(spi, dpi, transfer_region, "Custom Transfer".to_string());
main_dispatch.reduce_mut(|state| {
state.transfers.push(transfer);
state.selected_transfer = state
.transfers
.last()
.expect("An element should have just been added")
.get_uuid();
});
})
}
fn auto_callback(
main_dispatch: Dispatch<MainState>,
records: &[TransferRecord],
) -> Closure<dyn FnMut(Event)> {
let records = Vec::from(records);
Closure::<dyn FnMut(_)>::new(move |_| {
let res = auto(&records);
main_dispatch.reduce_mut(|state| {
state.source_plates.extend(res.sources);
state
.destination_plates
.extend(res.destinations);
state.transfers.extend(res.transfers);
});
})
}

View File

@ -1,96 +0,0 @@
#![allow(non_snake_case)]
use wasm_bindgen::{prelude::*, JsCast};
use web_sys::{HtmlDialogElement, HtmlFormElement, HtmlInputElement};
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::states::MainState;
use super::create_close_button;
pub fn input_json_input_callback(
main_dispatch: Dispatch<MainState>,
modal: HtmlDialogElement,
) -> Closure<dyn FnMut(Event)> {
Closure::<dyn FnMut(_)>::new(move |e: Event| {
if let Some(input) = e.current_target() {
let input = input
.dyn_into::<HtmlInputElement>()
.expect("We know this is an input.");
if let Some(files) = input.files() {
if let Some(file) = files.get(0) {
let fr = web_sys::FileReader::new().unwrap();
fr.read_as_text(&file).unwrap();
let fr1 = fr.clone(); // Clone to avoid outliving closure
let main_dispatch = main_dispatch.clone(); // Clone to satisfy FnMut
// trait
let modal = modal.clone();
let onload = Closure::<dyn FnMut(_)>::new(move |_: Event| {
if let Some(value) = &fr1.result().ok().and_then(|v| v.as_string()) {
let ms = serde_json::from_str::<MainState>(value);
match ms {
Ok(ms) => main_dispatch.set(ms),
Err(e) => log::debug!("{:?}", e),
};
modal.close();
}
});
fr.set_onload(Some(onload.as_ref().unchecked_ref()));
onload.forget(); // Magic (don't touch)
}
}
}
})
}
pub fn import_json_button_callback(main_dispatch: Dispatch<MainState>) -> Callback<MouseEvent> {
Callback::from(move |_| {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let body = document.body().unwrap();
let modal = document
.create_element("dialog")
.unwrap()
.dyn_into::<HtmlDialogElement>()
.unwrap();
modal.set_text_content(Some("Import File:"));
let onclose_callback = {
let modal = modal.clone();
Closure::<dyn FnMut(_)>::new(move |_: Event| {
modal.remove();
})
};
modal.set_onclose(Some(onclose_callback.as_ref().unchecked_ref()));
let close_button = create_close_button(&onclose_callback);
onclose_callback.forget();
modal.append_child(&close_button).unwrap();
let form = document
.create_element("form")
.unwrap()
.dyn_into::<HtmlFormElement>()
.unwrap();
let input = document
.create_element("input")
.unwrap()
.dyn_into::<HtmlInputElement>()
.unwrap();
input.set_type("file");
input.set_accept(".json");
form.append_child(&input).unwrap();
let input_callback = {
let main_dispatch = main_dispatch.clone();
let modal = modal.clone();
input_json_input_callback(main_dispatch, modal)
};
input.set_onchange(Some(input_callback.as_ref().unchecked_ref()));
input_callback.forget(); // Magic straight from the docs, don't touch :(
modal.append_child(&form).unwrap();
body.append_child(&modal).unwrap();
modal.show_modal().unwrap();
})
}

View File

@ -1,18 +0,0 @@
mod export_callbacks;
mod input_json_callbacks;
mod import_csv_callbacks;
mod settings_callbacks;
mod plate_edits_callbacks;
mod util;
pub use util::*;
pub use export_callbacks::*;
pub use input_json_callbacks::*;
pub use import_csv_callbacks::*;
pub use settings_callbacks::*;
pub use plate_edits_callbacks::*;

View File

@ -1,43 +0,0 @@
#![allow(non_snake_case)]
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::{new_plate_dialog::{NewPlateDialogType}, states::{CurrentTransfer, MainState}};
type NoParamsCallback = Box<dyn Fn(())>;
pub fn new_plate_dialog_callback(
new_plate_dialog_is_open: UseStateHandle<Option<NewPlateDialogType>>,
) -> NoParamsCallback {
let new_plate_dialog_is_open = new_plate_dialog_is_open.clone();
Box::new(move |_| {
new_plate_dialog_is_open.set(None);
})
}
pub fn open_new_plate_dialog_callback(
new_plate_dialog_is_open: UseStateHandle<Option<NewPlateDialogType>>
) -> Box<dyn Fn(NewPlateDialogType)> {
let new_plate_dialog_is_open = new_plate_dialog_is_open.clone();
Box::new(move |dt| {
new_plate_dialog_is_open.set(Some(dt));
})
}
pub fn new_button_callback(
main_dispatch: Dispatch<MainState>,
ct_dispatch: Dispatch<CurrentTransfer>,
) -> Callback<web_sys::MouseEvent> {
Callback::from(move |_| {
let window = web_sys::window().unwrap();
let confirm =
window.confirm_with_message("This will reset all plates and transfers. Proceed?");
if let Ok(confirm) = confirm {
if confirm {
main_dispatch.set(MainState::default());
ct_dispatch.set(CurrentTransfer::default());
}
}
})
}

View File

@ -1,116 +0,0 @@
#![allow(non_snake_case)]
use wasm_bindgen::{prelude::*, JsCast};
use web_sys::{
HtmlDialogElement, HtmlFormElement, HtmlOptionElement, HtmlSelectElement,
};
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::states::{CsvExportType, MainState};
use super::create_close_button;
pub fn toggle_in_transfer_hashes_callback(
main_dispatch: Dispatch<MainState>,
) -> Callback<web_sys::MouseEvent> {
let main_dispatch = main_dispatch.clone();
Callback::from(move |_| {
main_dispatch.reduce_mut(|state| {
state.preferences.in_transfer_hashes ^= true;
})
})
}
pub fn toggle_volume_heatmap_callback(
main_dispatch: Dispatch<MainState>,
) -> Callback<web_sys::MouseEvent> {
let main_dispatch = main_dispatch.clone();
Callback::from(move |_| {
main_dispatch.reduce_mut(|state| {
state.preferences.volume_heatmap ^= true;
})
})
}
pub fn toggle_show_current_coordinates_callback(
main_dispatch: Dispatch<MainState>,
) -> Callback<web_sys::MouseEvent> {
let main_dispatch = main_dispatch.clone();
Callback::from(move |_| {
main_dispatch.reduce_mut(|state| {
state.preferences.show_current_coordinates ^= true;
})
})
}
pub fn change_csv_export_type_callback(
main_dispatch: Dispatch<MainState>,
) -> Callback<web_sys::MouseEvent> {
let main_dispatch = main_dispatch.clone();
Callback::from(move |_| {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let body = document.body().unwrap();
let modal = document
.create_element("dialog")
.unwrap()
.dyn_into::<HtmlDialogElement>()
.unwrap();
let onclose_callback = {
let modal = modal.clone();
Closure::<dyn FnMut(_)>::new(move |_: Event| {
modal.remove();
})
};
modal.set_onclose(Some(onclose_callback.as_ref().unchecked_ref()));
modal.set_text_content(Some("CSV Export Type"));
let close_button = create_close_button(&onclose_callback);
onclose_callback.forget();
modal.append_child(&close_button).unwrap();
let form = document
.create_element("form")
.unwrap()
.dyn_into::<HtmlFormElement>()
.unwrap();
let export_type = document
.create_element("select")
.unwrap()
.dyn_into::<HtmlSelectElement>()
.unwrap();
for t in [CsvExportType::Normal, CsvExportType::EchoClient] {
let option = document
.create_element("option")
.unwrap()
.dyn_into::<HtmlOptionElement>()
.unwrap();
option.set_value(&t.to_string());
option.set_text(&t.to_string());
option.set_selected(t == main_dispatch.get().preferences.csv_export_type);
export_type.append_child(&option).unwrap();
}
form.append_child(&export_type).unwrap();
let form_change_callback = {
let export_type = export_type.clone();
let main_dispatch = main_dispatch.clone();
Closure::<dyn FnMut(_)>::new(move |_: Event| {
let current: CsvExportType = export_type.value().as_str().into();
main_dispatch.reduce_mut(|state| {
state.preferences.csv_export_type = current;
});
})};
form.set_onchange(Some(form_change_callback.as_ref().unchecked_ref()));
form_change_callback.forget();
modal.append_child(&form).unwrap();
body.append_child(&modal).unwrap();
modal.show_modal().unwrap();
})
}

View File

@ -1,19 +0,0 @@
#![allow(non_snake_case)]
use wasm_bindgen::{prelude::*, JsCast};
use web_sys::HtmlElement;
use yew::prelude::*;
pub fn create_close_button(close: &Closure<dyn FnMut(Event)>) -> HtmlElement {
let document = web_sys::window().unwrap().document().unwrap();
let close_button = document
.create_element("button")
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap();
close_button.set_text_content(Some(""));
close_button.set_class_name("close_button");
close_button.set_onclick(Some(close.as_ref().unchecked_ref()));
close_button
}

View File

@ -1,4 +0,0 @@
pub mod main_window_callbacks;
pub mod new_plate_dialog_callbacks;
pub mod transfer_menu_callbacks;
pub mod tree_callbacks;

View File

@ -1,61 +0,0 @@
use yew::prelude::*;
use yewdux::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{EventTarget, FormData, HtmlFormElement};
use crate::components::states::MainState;
use plate_tool_lib::plate::*;
use plate_tool_lib::plate_instances::PlateInstance;
pub fn new_plate_callback(
dispatch: Dispatch<MainState>,
close_callback: Callback<()>,
) -> Callback<SubmitEvent> {
Callback::from(move |e: SubmitEvent| {
e.prevent_default();
close_callback.emit(());
let target: Option<EventTarget> = e.target();
let form = target.and_then(|t| t.dyn_into::<HtmlFormElement>().ok());
if let Some(form) = form {
if let Ok(form_data) = FormData::new_with_form(&form) {
let name = form_data.get("new_plate_name").as_string().unwrap();
let format = match form_data.get("plate_format").as_string().unwrap().as_str() {
"6" => PlateFormat::W6,
"12" => PlateFormat::W12,
"24" => PlateFormat::W24,
"48" => PlateFormat::W48,
"96" => PlateFormat::W96,
"384" => PlateFormat::W384,
"1536" => PlateFormat::W1536,
"3456" => PlateFormat::W3456,
_ => unreachable!(),
};
if let Some(pt_string) = form_data.get("new_plate_type").as_string() {
let plate_type = match pt_string.as_str() {
"src" => PlateType::Source,
"dest" => PlateType::Destination,
_ => PlateType::Source,
};
dispatch.reduce_mut(|s| {
if plate_type == PlateType::Source {
s.add_source_plate(PlateInstance::new(PlateType::Source, format, name))
} else {
s.add_dest_plate(PlateInstance::new(
PlateType::Destination,
format,
name,
))
}
});
}
}
}
})
}
pub fn onclose(close_callback: Callback<()>) -> Callback<Event> {
Callback::from(move |_: Event| {
close_callback.emit(());
})
}

View File

@ -1,245 +0,0 @@
use std::rc::Rc;
use uuid::Uuid;
use wasm_bindgen::JsCast;
use web_sys::{EventTarget, HtmlInputElement};
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::transfer_menu::RegionDisplay;
use plate_tool_lib::{transfer::Transfer, transfer_region::Region, transfer_volume::TransferVolume};
use crate::components::states::{CurrentTransfer, MainState};
pub fn on_name_change_callback(ct_dispatch: Dispatch<CurrentTransfer>) -> Callback<Event> {
Callback::from(move |e: Event| {
let target: Option<EventTarget> = e.target();
let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
if let Some(input) = input {
if input.value() == "" {
return;
} // We do not want empty inputs!
ct_dispatch.reduce_mut(|state| {
state.transfer.name = input.value();
});
}
})
}
pub fn on_src_region_change_callback(ct_dispatch: Dispatch<CurrentTransfer>) -> Callback<Event> {
Callback::from(move |e: Event| {
if matches!(
ct_dispatch.get().transfer.transfer_region.source_region,
Region::Custom(_)
) {
return; // Do nothing here!
}
let target: Option<EventTarget> = e.target();
let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
if let Some(input) = input {
if let Ok(rd) = RegionDisplay::try_from(input.value().to_uppercase()) {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.source_region = Region::from(&rd);
});
input.set_custom_validity("");
} else {
input.set_custom_validity("Invalid region.")
}
}
})
}
pub fn on_dest_region_change_callback(ct_dispatch: Dispatch<CurrentTransfer>) -> Callback<Event> {
Callback::from(move |e: Event| {
let target: Option<EventTarget> = e.target();
let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
if let Some(input) = input {
if let Ok(rd) = RegionDisplay::try_from(input.value().to_uppercase()) {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.dest_region = Region::from(&rd);
});
input.set_custom_validity("");
} else {
input.set_custom_validity("Invalid region.")
}
}
})
}
pub fn on_source_interleave_x_change_callback(
ct_dispatch: Dispatch<CurrentTransfer>,
) -> Callback<Event> {
Callback::from(move |e: Event| {
let target: Option<EventTarget> = e.target();
let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
if let Some(input) = input {
if let Ok(num) = input.value().parse::<i8>() {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.interleave_source =
(num, state.transfer.transfer_region.interleave_source.1);
});
}
}
})
}
pub fn on_source_interleave_y_change_callback(
ct_dispatch: Dispatch<CurrentTransfer>,
) -> Callback<Event> {
Callback::from(move |e: Event| {
let target: Option<EventTarget> = e.target();
let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
if let Some(input) = input {
if let Ok(num) = input.value().parse::<i8>() {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.interleave_source =
(state.transfer.transfer_region.interleave_source.0, num);
});
}
}
})
}
pub fn on_dest_interleave_x_change_callback(
ct_dispatch: Dispatch<CurrentTransfer>,
) -> Callback<Event> {
Callback::from(move |e: Event| {
let target: Option<EventTarget> = e.target();
let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
if let Some(input) = input {
if let Ok(num) = input.value().parse::<i8>() {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.interleave_dest =
(num, state.transfer.transfer_region.interleave_dest.1);
});
}
}
})
}
pub fn on_dest_interleave_y_change_callback(
ct_dispatch: Dispatch<CurrentTransfer>,
) -> Callback<Event> {
Callback::from(move |e: Event| {
let target: Option<EventTarget> = e.target();
let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
if let Some(input) = input {
if let Ok(num) = input.value().parse::<i8>() {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.interleave_dest =
(state.transfer.transfer_region.interleave_dest.0, num);
});
}
}
})
}
pub fn on_volume_change_callback(ct_dispatch: Dispatch<CurrentTransfer>) -> Callback<Event> {
Callback::from(move |e: Event| {
let input = e
.target()
.expect("Event must have target")
.dyn_into::<HtmlInputElement>()
.expect("Must have been emitted by input");
if let Ok(num) = input.value().parse::<f32>() {
ct_dispatch.reduce_mut(|state| {
state.transfer.volume = TransferVolume::Single(num);
});
}
})
}
pub fn new_transfer_button_callback_callback(
main_dispatch: Dispatch<MainState>,
main_state: Rc<MainState>,
ct_dispatch: Dispatch<CurrentTransfer>,
) -> Callback<MouseEvent> {
Callback::from(move |_: MouseEvent| {
main_dispatch.reduce_mut(|state| {
state.selected_transfer = Uuid::nil();
});
ct_dispatch.reduce_mut(|state| {
state.transfer = Transfer::default();
state.transfer.source_id = main_state.selected_source_plate;
state.transfer.dest_id = main_state.selected_dest_plate;
});
})
}
pub fn save_transfer_button_callback_callback(
main_dispatch: Dispatch<MainState>,
main_state: Rc<MainState>,
ct_state: Rc<CurrentTransfer>,
new_transfer_button_callback: Callback<MouseEvent>)
-> Callback<MouseEvent> {
Callback::from(move |e: MouseEvent| {
log::debug!("Button pressed");
if main_state.selected_transfer.is_nil() {
if let Some(spi) = main_state
.source_plates
.iter()
.find(|spi| spi.get_uuid() == main_state.selected_source_plate)
{
if let Some(dpi) = main_state
.destination_plates
.iter()
.find(|dpi| dpi.get_uuid() == main_state.selected_dest_plate)
{
// Only mutable for volume assignment!
let mut new_transfer = Transfer::new(
spi.clone(),
dpi.clone(),
ct_state.transfer.transfer_region.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| {
state.transfers.push(new_transfer);
state.selected_transfer = state
.transfers
.last()
.expect("An element should have just been added")
.get_uuid();
});
new_transfer_button_callback.emit(e); // If we just made a new transfer,
// then we should make another on
// save.
}
}
} else if let Some(index) = main_state
.transfers
.iter()
.position(|t| t.get_uuid() == main_state.selected_transfer)
{
main_dispatch.reduce_mut(|state| {
state.transfers[index] = ct_state.transfer.clone();
});
}
})
}
pub fn delete_transfer_button_callback(
main_state: Rc<MainState>,
main_dispatch: Dispatch<MainState>,
ct_state: Rc<CurrentTransfer>,
new_transfer_button_callback: Callback<MouseEvent>
) -> Callback<MouseEvent> {
Callback::from(move |e: MouseEvent| {
if main_state.selected_transfer.is_nil() {
// Maybe reset transfer?
} else if let Some(index) = main_state
.transfers
.iter()
.position(|t| t.get_uuid() == ct_state.transfer.get_uuid())
{
main_dispatch.reduce_mut(|state| {
state.transfers.remove(index);
state.selected_transfer = Uuid::nil();
});
new_transfer_button_callback.emit(e); // We need a new transfer now
}
})
}

View File

@ -1,74 +0,0 @@
use uuid::Uuid;
use wasm_bindgen::JsCast;
use web_sys::{EventTarget, HtmlElement, HtmlInputElement, HtmlOptionElement, HtmlSelectElement};
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::states::MainState;
use plate_tool_lib::plate::PlateFormat;
type NoParamsCallback = Box<dyn Fn(())>;
pub fn open_plate_info_callback(
plate_menu_id: UseStateHandle<Option<Uuid>>,
) -> Callback<MouseEvent> {
Callback::from(move |e: MouseEvent| {
let target: Option<EventTarget> = e.target();
let li = target.and_then(|t| t.dyn_into::<HtmlElement>().ok());
if let Some(li) = li {
if let Ok(id) = li.id().as_str().parse::<u128>() {
plate_menu_id.set(Some(Uuid::from_u128(id)));
}
}
})
}
pub fn plate_info_close_callback(plate_menu_id: UseStateHandle<Option<Uuid>>) -> NoParamsCallback {
Box::new(move |_| {
plate_menu_id.set(None);
})
}
pub fn plate_info_delete_callback(
main_dispatch: Dispatch<MainState>,
plate_menu_id: UseStateHandle<Option<Uuid>>,
) -> NoParamsCallback {
Box::new(move |_| {
if let Some(id) = *plate_menu_id {
main_dispatch.reduce_mut(|state| {
state.del_plate(id);
});
}
})
}
pub fn rename_onchange(id: Uuid, main_dispatch: Dispatch<MainState>) -> Callback<Event> {
Callback::from(move |e: Event| {
log::debug!("Changed name");
let input = e
.target()
.expect("Event must have target")
.dyn_into::<HtmlInputElement>()
.unwrap();
main_dispatch.reduce_mut(|state| state.rename_plate(id, &input.value()))
})
}
pub fn format_onchange(id: Uuid, main_dispatch: Dispatch<MainState>) -> Callback<Event> {
Callback::from(move |e: Event| {
log::debug!("Changing plate format");
let new_format: Option<PlateFormat> = e
.target()
.expect("Event must have target")
.dyn_into::<HtmlSelectElement>()
.unwrap()
.selected_options()
.get_with_index(0)
.map(|el| el.dyn_into::<HtmlOptionElement>().unwrap())
.map(|opt_el| opt_el.value())
.and_then(|value| PlateFormat::try_from(value.as_str()).ok());
if let Some(format) = new_format {
main_dispatch.reduce_mut(|state| state.change_format(id, &format));
}
})
}

View File

@ -1,217 +0,0 @@
use plate_tool_lib::{plate::PlateType};
use uuid::Uuid;
use wasm_bindgen::{prelude::*, JsCast};
use web_sys::{
HtmlDialogElement, HtmlElement, HtmlOptionElement,
HtmlSelectElement,
};
use yew::prelude::*;
use yewdux::prelude::*;
use std::rc::Rc;
use std::str::FromStr;
use crate::components::callbacks::main_window_callbacks::create_close_button;
use crate::components::states::MainState;
pub fn merge_button_callback(main_dispatch: Dispatch<MainState>, id: Uuid) -> Callback<MouseEvent> {
Callback::from(move |_| {
let dialog_opt = create_merge_dialog(main_dispatch.clone(), id);
if let Some(dialog) = dialog_opt {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let body = document.body().unwrap();
let _ = body.append_child(&dialog);
dialog.set_open(true);
}
})
}
fn create_merge_dialog(main_dispatch: Dispatch<MainState>, id: Uuid) -> Option<HtmlDialogElement> {
let other_plates = get_all_plates_except(main_dispatch.clone(), id);
if other_plates.is_empty() {
return None;
}
let dialog = create_dialog();
let select = create_select_for_plates(&other_plates);
dialog.append_child(&select).unwrap();
let select_rc = Rc::new(select);
let ok_button = create_ok_button(main_dispatch.clone(), select_rc.clone(), id);
dialog.append_child(&ok_button).unwrap();
Some(dialog)
}
fn create_dialog() -> HtmlDialogElement {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let dialog = document
.create_element("dialog")
.unwrap()
.dyn_into::<HtmlDialogElement>()
.unwrap();
dialog.set_class_name("dialog shifted_dialog");
let onclose_callback = {
let dialog = dialog.clone();
Closure::<dyn FnMut(_)>::new(move |_: Event| {
dialog.remove();
})
};
dialog.set_onclose(Some(onclose_callback.as_ref().unchecked_ref()));
let close_button = create_close_button(&onclose_callback);
onclose_callback.forget();
dialog.append_child(&close_button).unwrap();
let header = document.create_element("h2").unwrap();
header.set_inner_html("Merge Plates");
dialog.append_child(&header).unwrap();
dialog
}
fn create_ok_button(
main_dispatch: Dispatch<MainState>,
select: Rc<HtmlSelectElement>,
id: Uuid,
) -> HtmlElement {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let button = document
.create_element("button")
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap();
button.set_inner_html("Ok");
let callback = ok_button_callback(main_dispatch, select, id);
button.set_onclick(Some(callback.as_ref().unchecked_ref()));
callback.forget();
button
}
fn ok_button_callback(
main_dispatch: Dispatch<MainState>,
select: Rc<HtmlSelectElement>,
to_id: Uuid,
) -> Closure<dyn FnMut(Event)> {
Closure::<dyn FnMut(_)>::new(move |_: Event| {
let selected = select
.selected_options()
.get_with_index(0)
.map(|el| el.dyn_into::<HtmlOptionElement>().unwrap())
.map(|opt_el| opt_el.value())
.and_then(|uuid_str| Uuid::from_str(&uuid_str).ok());
if let Some(from_id) = selected {
let plate_type = find_plate_type(main_dispatch.clone(), from_id).unwrap();
let binding = main_dispatch.get();
let merge_to = match plate_type {
PlateType::Source => binding.source_plates.iter().find(|x| x.get_uuid() == to_id).unwrap(),
PlateType::Destination => binding
.destination_plates
.iter()
.find(|x| x.get_uuid() == to_id).unwrap(),
};
main_dispatch.reduce_mut(|x| {
let merged_transfers = x.transfers.iter_mut().filter(|x| match plate_type {
PlateType::Source => x.source_id,
PlateType::Destination => x.dest_id
} == from_id);
for transfer in merged_transfers {
match plate_type {
PlateType::Source => {
transfer.source_id = to_id;
transfer.transfer_region.source_plate = merge_to.plate;
},
PlateType::Destination => {
transfer.dest_id = to_id;
transfer.transfer_region.dest_plate = merge_to.plate;
}
}
}
match plate_type {
PlateType::Source => x.source_plates.retain(|y| y.get_uuid() != from_id),
PlateType::Destination => x.destination_plates.retain(|y| y.get_uuid() != from_id)
};
});
}
})
}
fn create_select_for_plates(infos: &Vec<(Uuid, String)>) -> HtmlSelectElement {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let select = document
.create_element("select")
.unwrap()
.dyn_into::<HtmlSelectElement>()
.unwrap();
for info in infos {
let _ = select.append_child(&create_option_for_plate(info));
}
select
}
fn create_option_for_plate(info: &(Uuid, String)) -> HtmlOptionElement {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let opt = document
.create_element("option")
.unwrap()
.dyn_into::<HtmlOptionElement>()
.unwrap();
opt.set_value(&info.0.to_string());
opt.set_inner_text(&info.1);
opt
}
fn get_all_plates_except(main_dispatch: Dispatch<MainState>, id: Uuid) -> Vec<(Uuid, String)> {
let plate_type = find_plate_type(main_dispatch.clone(), id);
if plate_type.is_none() {
return Vec::new();
} // Return early if not found somehow
let plate_type = plate_type.unwrap();
let binding = main_dispatch.get();
let all_plates_iter = match plate_type {
PlateType::Source => binding.source_plates.iter(),
PlateType::Destination => binding.destination_plates.iter(),
};
all_plates_iter
.map(|x| (x.get_uuid(), x.name.to_string()))
.filter(|y| y.0 != id)
.collect()
}
fn find_plate_type(main_dispatch: Dispatch<MainState>, id: Uuid) -> Option<PlateType> {
if main_dispatch
.get()
.source_plates
.iter()
.any(|x| x.get_uuid() == id)
{
Some(PlateType::Source)
} else if main_dispatch
.get()
.destination_plates
.iter()
.any(|x| x.get_uuid() == id)
{
return Some(PlateType::Destination);
} else {
return None;
}
}

View File

@ -1,9 +0,0 @@
mod select_callbacks;
mod info_dialog_callbacks;
mod merge_callbacks;
pub use select_callbacks::*;
pub use info_dialog_callbacks::*;
pub use merge_callbacks::*;

View File

@ -1,83 +0,0 @@
use std::rc::Rc;
use uuid::Uuid;
use wasm_bindgen::JsCast;
use web_sys::{EventTarget, HtmlElement};
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::states::{CurrentTransfer, MainState};
use plate_tool_lib::transfer_region::Region;
pub fn source_plate_select_callback(
main_dispatch: Dispatch<MainState>,
ct_dispatch: Dispatch<CurrentTransfer>,
) -> Callback<MouseEvent> {
Callback::from(move |e: MouseEvent| {
let target: Option<EventTarget> = e.target();
let li = target.and_then(|t| t.dyn_into::<HtmlElement>().ok());
if let Some(li) = li {
if let Ok(id) = li.id().as_str().parse::<u128>() {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.source_region = Region::default();
state.transfer.transfer_region.dest_region = Region::default();
});
main_dispatch.reduce_mut(|state| {
state.selected_source_plate = Uuid::from_u128(id);
state.selected_transfer = Uuid::nil();
});
}
}
})
}
pub fn destination_plate_select_callback(
main_dispatch: Dispatch<MainState>,
ct_dispatch: Dispatch<CurrentTransfer>,
) -> Callback<MouseEvent> {
Callback::from(move |e: MouseEvent| {
let target: Option<EventTarget> = e.target();
let li = target.and_then(|t| t.dyn_into::<HtmlElement>().ok());
if let Some(li) = li {
if let Ok(id) = li.id().as_str().parse::<u128>() {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.source_region = Region::default();
state.transfer.transfer_region.dest_region = Region::default();
});
main_dispatch.reduce_mut(|state| {
state.selected_dest_plate = Uuid::from_u128(id);
state.selected_transfer = Uuid::nil();
});
}
}
})
}
pub fn transfer_select_callback(
main_state: Rc<MainState>,
main_dispatch: Dispatch<MainState>,
ct_dispatch: Dispatch<CurrentTransfer>,
) -> Callback<MouseEvent> {
Callback::from(move |e: MouseEvent| {
let target: Option<EventTarget> = e.target();
let li = target.and_then(|t| t.dyn_into::<HtmlElement>().ok());
if let Some(li) = li {
if let Ok(id) = li.id().as_str().parse::<u128>() {
let id = Uuid::from_u128(id);
if let Some(transfer) = main_state
.transfers
.iter()
.find(|transfer| transfer.get_uuid() == id)
{
main_dispatch.reduce_mut(|state| {
state.selected_source_plate = transfer.source_id;
state.selected_dest_plate = transfer.dest_id;
state.selected_transfer = id;
});
ct_dispatch.reduce_mut(|state| {
state.transfer = transfer.clone();
});
}
}
}
})
}

View File

@ -1,139 +0,0 @@
#![allow(non_snake_case)]
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::new_plate_dialog::{NewPlateDialog, NewPlateDialogType};
use crate::components::plates::plate_container::PlateContainer;
use crate::components::states::{CurrentTransfer, MainState};
use crate::components::transfer_menu::TransferMenu;
use crate::components::tree::Tree;
use plate_tool_lib::plate_instances::PlateInstance;
use crate::components::callbacks::main_window_callbacks;
#[function_component]
pub fn MainWindow() -> Html {
let (main_state, main_dispatch) = use_store::<MainState>();
let (_, ct_dispatch) = use_store::<CurrentTransfer>();
let source_plate_instance: Option<PlateInstance> = main_state
.source_plates
.iter()
.find(|spi| spi.get_uuid() == main_state.selected_source_plate)
.cloned();
if let Some(spi) = source_plate_instance.clone() {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.source_plate = spi.plate;
});
}
let destination_plate_instance: Option<PlateInstance> = main_state
.destination_plates
.iter()
.find(|dpi| dpi.get_uuid() == main_state.selected_dest_plate)
.cloned();
if let Some(dpi) = destination_plate_instance.clone() {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.dest_plate = dpi.plate;
});
}
let toggle_in_transfer_hashes_callback = {
let main_dispatch = main_dispatch.clone();
main_window_callbacks::toggle_in_transfer_hashes_callback(main_dispatch)
};
let toggle_volume_heatmap_callback = {
let main_dispatch = main_dispatch.clone();
main_window_callbacks::toggle_volume_heatmap_callback(main_dispatch)
};
let toggle_show_current_coordinates_callback = {
let main_dispatch = main_dispatch.clone();
main_window_callbacks::toggle_show_current_coordinates_callback(main_dispatch)
};
let change_csv_export_type_callback = {
let main_dispatch = main_dispatch.clone();
main_window_callbacks::change_csv_export_type_callback(main_dispatch)
};
let new_plate_dialog_is_open: UseStateHandle<Option<NewPlateDialogType>> = use_state_eq(|| None);
let new_plate_dialog_callback =
main_window_callbacks::new_plate_dialog_callback(new_plate_dialog_is_open.clone());
let open_new_plate_dialog_callback =
main_window_callbacks::open_new_plate_dialog_callback(new_plate_dialog_is_open.clone());
let new_button_callback = {
let main_dispatch = main_dispatch.clone();
main_window_callbacks::new_button_callback(main_dispatch, ct_dispatch)
};
let export_csv_button_callback = {
let main_state = main_state.clone();
main_window_callbacks::export_csv_button_callback(main_state)
};
let export_json_button_callback =
{ main_window_callbacks::export_json_button_callback(main_state) };
let import_json_button_callback = {
let main_dispatch = main_dispatch.clone();
main_window_callbacks::import_json_button_callback(main_dispatch)
};
let import_transfer_csv_callback =
main_window_callbacks::import_transfer_csv_callback(main_dispatch.clone());
html! {
<>
<div class="upper_menu">
<div class="dropdown">
<button>{"File"}</button>
<button onclick={new_button_callback}>{"New"}</button>
<div class="dropdown-sub">
<button>{"Export"}</button>
<div>
<button onclick={export_csv_button_callback}>{"Export as CSV"}</button>
<button onclick={export_json_button_callback}>{"Export as JSON"}</button>
</div>
</div>
<div class="dropdown-sub">
<button>{"Import"}</button>
<div>
<button onclick={import_json_button_callback}>{"Import from JSON"}</button>
<button onclick={import_transfer_csv_callback}>{"Import Transfer from CSV"}</button>
</div>
</div>
</div>
<div class="dropdown">
<button>{"Options"}</button>
<div class="dropdown-sub">
<button>{"Styles"}</button>
<div>
<button onclick={toggle_in_transfer_hashes_callback}>{"Toggle transfer hashes"}</button>
<button onclick={toggle_volume_heatmap_callback}>{"Toggle volume heatmap"}</button>
<button onclick={toggle_show_current_coordinates_callback}>{"Toggle current coordinates view"}</button>
</div>
</div>
<div class="dropdown-sub">
<button>{"Export"}</button>
<div>
<button onclick={change_csv_export_type_callback}>{"Change CSV export type"}</button>
</div>
</div>
</div>
</div>
<div class="main_container">
<Tree open_new_plate_callback={open_new_plate_dialog_callback}/>
<TransferMenu />
<PlateContainer source_dims={source_plate_instance}
destination_dims={destination_plate_instance}/>
if {new_plate_dialog_is_open.is_some()} {
<NewPlateDialog close_callback={new_plate_dialog_callback} dialog_type={new_plate_dialog_is_open.unwrap()}/>
}
</div>
</>
}
}

View File

@ -1,108 +0,0 @@
use yew::prelude::*;
use yewdux::prelude::*;
use web_sys::HtmlDialogElement;
use crate::components::states::MainState;
use crate::components::callbacks::new_plate_dialog_callbacks;
#[derive(PartialEq, Properties)]
pub struct NewPlateDialogProps {
pub close_callback: Callback<()>,
pub dialog_type: NewPlateDialogType,
}
#[derive(PartialEq, Clone, Copy, Debug)]
pub enum NewPlateDialogType {
SourceOnly,
DestinationOnly,
#[allow(dead_code)]
Both, // Retained old functionality
}
#[function_component]
pub fn NewPlateDialog(props: &NewPlateDialogProps) -> Html {
let (_, dispatch) = use_store::<MainState>();
let new_plate_callback = {
let close_callback = props.close_callback.clone();
new_plate_dialog_callbacks::new_plate_callback(dispatch, close_callback)
};
let onclose = {
let close_callback = props.close_callback.clone();
new_plate_dialog_callbacks::onclose(close_callback)
};
// This whole section is optional, only if you want the backdrop
let dialog_ref = use_node_ref();
{
let dialog_ref = dialog_ref.clone();
use_effect_with(
dialog_ref,
|dialog_ref| {
dialog_ref
.cast::<HtmlDialogElement>()
.unwrap()
.show_modal()
.ok();
},
);
}
let header_interior_text = match props.dialog_type {
NewPlateDialogType::Both => "",
NewPlateDialogType::SourceOnly => " source",
NewPlateDialogType::DestinationOnly => " destination",
};
html! {
<dialog ref={dialog_ref} class="dialog new_plate_dialog" onclose={onclose}>
<h2>{format!("Create a{} plate:", header_interior_text)}</h2>
<form onsubmit={new_plate_callback}>
<input type="text" name="new_plate_name" placeholder="Name"/>
<select name="plate_format">
<option value="6">{"6"}</option>
<option value="12">{"12"}</option>
<option value="24">{"24"}</option>
<option value="48">{"48"}</option>
<option value="96" selected={true}>{"96"}</option>
<option value="384">{"384"}</option>
<option value="1536">{"1536"}</option>
<option value="3456">{"3456"}</option>
</select>
{ plate_type_selector(props.dialog_type) }
<input type="submit" name="new_plate_button" value="Create" />
</form>
<form class="modal_close" method="dialog"><button /></form>
</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,5 +0,0 @@
pub mod plate_container;
mod util;
mod plate_callbacks;
mod plate_data;
mod plate;

View File

@ -1,348 +0,0 @@
#![allow(non_snake_case)]
use std::collections::HashMap;
use plate_tool_lib::transfer_volume::TransferVolume;
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::states::{CurrentTransfer, MainState};
use plate_tool_lib::plate::PlateType;
use plate_tool_lib::transfer::Transfer;
use plate_tool_lib::transfer_region::Region;
use plate_tool_lib::Well;
// Color Palette for the Source Plates, can be changed here
use crate::components::plates::util::Palettes;
const PALETTE: super::util::ColorPalette = Palettes::RAINBOW;
use plate_tool_lib::util::num_to_letters;
use super::plate_data::*;
use super::plate_callbacks;
#[function_component]
pub fn Plate(props: &PlateProps) -> Html {
let (main_state, _) = use_store::<MainState>();
let (ct_state, ct_dispatch) = use_store::<CurrentTransfer>();
let m_start_handle: MStartHandle = use_state_eq(|| None);
let m_end_handle: MEndHandle = use_state_eq(|| None);
let m_stat_handle: MStatHandle = use_state_eq(|| false);
if !(*m_stat_handle) {
let region = match props.ptype {
PlateType::Source => ct_state.transfer.transfer_region.source_region.clone(),
PlateType::Destination => ct_state.transfer.transfer_region.dest_region.clone(),
};
let (pt1, pt2) = match region {
Region::Point(Well {row: x, col: y }) => ((x, y), (x, y)),
Region::Rect(c1, c2) => ((c1.row, c1.col ), (c2.row, c2.col)),
Region::Custom(_) => ((0, 0), (0, 0)),
};
m_start_handle.set(Some(pt1));
m_end_handle.set(Some(pt2));
}
let tooltip_map: HashMap<Well, Vec<&Transfer>>;
let volume_map: HashMap<Well, f32>;
let volume_max: f32;
{
let transfers = main_state.transfers.iter().filter(|t| match props.ptype {
PlateType::Source => t.source_id == props.source_plate.get_uuid(),
PlateType::Destination => t.dest_id == props.destination_plate.get_uuid(),
});
let mut tooltip_map_temp: HashMap<Well, Vec<&Transfer>> = HashMap::new();
let mut volume_map_temp: HashMap<Well, f32> = HashMap::new();
let mut volume_max_temp: f32 = f32::NEG_INFINITY;
for transfer in transfers {
let wells = match props.ptype {
PlateType::Source => transfer.transfer_region.get_source_wells(),
PlateType::Destination => transfer.transfer_region.get_destination_wells(),
};
for well in wells {
if let Some(val) = tooltip_map_temp.get_mut(&well) {
val.push(transfer);
} else {
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!(),
};
// Usage to be used as a volume multiplier; should be in [1, U32_MAX]
// First convert to f64 (u32 can not fit in f32 directly) and then cast
// to round into the closest f32.
let usage: f32 = Into::<f64>::into(count_plate_usage(transfer, &well).unwrap_or(1u32).max(1u32)) as f32;
if let Some(val) = volume_map_temp.get_mut(&well) {
*val += temp_volume * usage;
} else {
volume_map_temp.insert(well, temp_volume * usage);
}
volume_max_temp = f32::max(volume_max_temp, *volume_map_temp.get(&well).expect("Just added"));
}
}
tooltip_map = tooltip_map_temp;
volume_map = volume_map_temp;
volume_max = volume_max_temp;
};
let wells = match props.ptype {
PlateType::Source => ct_state.transfer.transfer_region.get_source_wells(),
PlateType::Destination => ct_state.transfer.transfer_region.get_destination_wells(),
};
let ordered_ids: Vec<uuid::Uuid> = {
let mut ids: Vec<uuid::Uuid> = main_state.transfers.clone().iter().map(|x| x.id).collect();
ids.sort_unstable();
ids
};
let mouse_callback = {
let m_start_handle = m_start_handle.clone();
let m_end_handle = m_end_handle.clone();
let m_stat_handle = m_stat_handle.clone();
let send_coordinates = props.send_coordinates.clone();
plate_callbacks::mouse_callback(m_start_handle, m_end_handle, m_stat_handle, send_coordinates)
};
let mouseup_callback = {
let m_start_handle = m_start_handle.clone();
let m_end_handle = m_end_handle.clone();
plate_callbacks::mouseup_callback(m_start_handle, m_end_handle, m_stat_handle, ct_dispatch, props.ptype)
};
let mouseleave_callback = Callback::clone(&mouseup_callback);
let screenshot_callback = Callback::from(|_| {
let _ = js_sys::eval("copy_screenshot_src()");
});
let width = match props.ptype {
PlateType::Source => props.source_plate.plate.size().1,
PlateType::Destination => props.destination_plate.plate.size().1,
};
let height = match props.ptype {
PlateType::Source => props.source_plate.plate.size().0,
PlateType::Destination => props.destination_plate.plate.size().0,
};
let pformat = match props.ptype {
PlateType::Source => props.source_plate.plate.plate_format,
PlateType::Destination => props.destination_plate.plate.plate_format,
};
let column_header = {
let headers = (1..=width)
.map(|j| {
html! {<th>
{format!("{:0>2}", j)}
</th>}
})
.collect::<Html>();
html! {<tr><th />{ headers }</tr>}
};
let rows =
(1..=height)
.map(|i| {
let row_header = html! {<th>{num_to_letters(i)}</th>};
let row = (1..=width)
.map(|j| {
let color = {
if !main_state.preferences.volume_heatmap {
tooltip_map.get(&Well { row: i, col: j })
.and_then(|t| t.last())
.map(|t| PALETTE.get_ordered(t.get_uuid(), &ordered_ids))
} else {
volume_map.get(&Well { row: i, col: j })
.map(|t| PALETTE.get_linear(*t as f64, volume_max as f64))
}
};
let title = {
let mut out = String::new();
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(", ")));
if let Some(val) = used_by {
out += &val;
}
let volume_sum = volume_map.get(&Well { row: i, col: j })
.map(|t| format!("Volume: {}", t));
if let Some(val) = volume_sum {
if !out.is_empty() { out += "\n" }
out += &val;
}
out
};
html! {
<PlateCell i={i} j={j}
selected={in_rect(*m_start_handle.clone(), *m_end_handle.clone(), (i,j))}
mouse={mouse_callback.clone()}
in_transfer={wells.contains(&Well { row: i, col: j }) && main_state.preferences.in_transfer_hashes}
color={ color }
cell_height={props.cell_height}
title={title}
/>
}
})
.collect::<Html>();
html! {
<tr>
{ row_header }{ row }
</tr>
}
})
.collect::<Html>();
html! {
<div ondblclick={screenshot_callback}
class={classes!{match props.ptype {
PlateType::Source => "source_plate",
PlateType::Destination => "dest_plate",
},
"W".to_owned()+&pformat.to_string()}}>
<table
onmouseup={move |e| {
mouseup_callback.emit(e);
}}
onmouseleave={move |e| {
mouseleave_callback.emit(e);
}}>
{ column_header }
{ rows }
</table>
</div>
}
}
#[derive(PartialEq, Properties)]
pub struct PlateCellProps {
i: u8,
j: u8,
selected: bool,
mouse: Callback<(u8, u8, MouseEventType)>,
in_transfer: Option<bool>,
color: Option<[f64; 3]>,
cell_height: f64,
title: Option<String>,
}
#[function_component]
fn PlateCell(props: &PlateCellProps) -> Html {
let selected_class = match props.selected {
true => Some("current_select"),
false => None,
};
let in_transfer_class = match props.in_transfer {
Some(true) => Some("in_transfer"),
_ => None,
};
let color = props.color.unwrap_or([255.0, 255.0, 255.0]);
let mouse = Callback::clone(&props.mouse);
let mouse2 = Callback::clone(&props.mouse);
let (i, j) = (props.i, props.j);
html! {
<td class={classes!("plate_cell", selected_class, in_transfer_class)}
style={format!("height: {}px;", props.cell_height)}
id={format!("color={:?}", props.color)}
onmousedown={move |_| {
mouse.emit((i,j, MouseEventType::Mousedown))
}}
onmouseenter={move |_| {
mouse2.emit((i,j, MouseEventType::Mouseenter))
}}>
<div class="plate_cell_inner"
style={format!("background: rgba({},{},{},1);", color[0], color[1], color[2])}
title={if let Some(text) = &props.title {
text.clone()
} else {"".to_string()}}/>
</td>
}
}
pub fn in_rect(corner1: Option<(u8, u8)>, corner2: Option<(u8, u8)>, pt: (u8, u8)) -> bool {
if let (Some(c1), Some(c2)) = (corner1, corner2) {
pt.0 <= u8::max(c1.0, c2.0)
&& pt.0 >= u8::min(c1.0, c2.0)
&& pt.1 <= u8::max(c1.1, c2.1)
&& pt.1 >= u8::min(c1.1, c2.1)
} else {
false
}
}
fn count_plate_usage(transfer: &Transfer, well: &Well) -> Option<u32> {
if let Region::Custom(_) = transfer.transfer_region.source_region {
return None;
}
let map = transfer.transfer_region.calculate_map();
map(*well).map(|list| list.len() as u32)
}
#[cfg(test)]
mod tests {
use wasm_bindgen_test::*;
use super::in_rect;
// in_rect tests
#[test]
#[wasm_bindgen_test]
fn test_in_rect1() {
// Test in center of rect
let c1 = (1, 1);
let c2 = (10, 10);
let pt = (5, 5);
assert!(in_rect(Some(c1), Some(c2), pt));
// Order of the corners should not matter:
assert!(in_rect(Some(c2), Some(c1), pt));
}
#[test]
#[wasm_bindgen_test]
fn test_in_rect2() {
// Test on top/bottom edges of rect
let c1 = (1, 1);
let c2 = (10, 10);
let pt1 = (1, 5);
let pt2 = (10, 5);
assert!(in_rect(Some(c1), Some(c2), pt1));
assert!(in_rect(Some(c1), Some(c2), pt2));
// Order of the corners should not matter:
assert!(in_rect(Some(c2), Some(c1), pt1));
assert!(in_rect(Some(c2), Some(c1), pt2));
}
#[test]
#[wasm_bindgen_test]
fn test_in_rect3() {
// Test on left/right edges of rect
let c1 = (1, 1);
let c2 = (10, 10);
let pt1 = (5, 1);
let pt2 = (5, 10);
assert!(in_rect(Some(c1), Some(c2), pt1));
assert!(in_rect(Some(c1), Some(c2), pt2));
// Order of the corners should not matter:
assert!(in_rect(Some(c2), Some(c1), pt1));
assert!(in_rect(Some(c2), Some(c1), pt2));
}
#[test]
#[wasm_bindgen_test]
fn test_in_rect4() {
// Test cases that should fail
let c1 = (1, 1);
let c2 = (10, 10);
let pt1 = (0, 0);
let pt2 = (15, 15);
assert!(!in_rect(Some(c1), Some(c2), pt1));
assert!(!in_rect(Some(c1), Some(c2), pt2));
}
}

View File

@ -1,59 +0,0 @@
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::states::CurrentTransfer;
use plate_tool_lib::transfer_region::Region;
use plate_tool_lib::plate::PlateType;
// Color Palette for the Source Plates, can be changed here
use super::super::transfer_menu::RegionDisplay;
use super::plate_data::*;
pub fn mouse_callback(
m_start_handle: MStartHandle,
m_end_handle: MEndHandle,
m_stat_handle: MStatHandle,
send_coordinates: Option<Callback<(u8,u8)>>
) -> Callback<(u8, u8, MouseEventType)> {
Callback::from(move |(i, j, t)| match t {
MouseEventType::Mousedown => {
m_start_handle.set(Some((i, j)));
m_end_handle.set(Some((i, j)));
m_stat_handle.set(true);
}
MouseEventType::Mouseenter => {
if let Some(cb) = send_coordinates.as_ref() {
cb.emit((i,j));
}
if *m_stat_handle {
m_end_handle.set(Some((i, j)));
}
}
})
}
pub fn mouseup_callback(
m_start_handle: MStartHandle,
m_end_handle: MEndHandle,
m_stat_handle: MStatHandle,
ct_dispatch: Dispatch<CurrentTransfer>,
ptype: PlateType,
) -> Callback<MouseEvent> {
Callback::from(move |_: MouseEvent| {
m_stat_handle.set(false);
if let Some(ul) = *m_start_handle {
if let Some(br) = *m_end_handle {
if let Ok(rd) = RegionDisplay::try_from((ul.1, ul.0, br.1, br.0)) {
ct_dispatch.reduce_mut(|state| {
match ptype {
PlateType::Source => state.transfer.transfer_region.source_region = Region::from(&rd),
PlateType::Destination => state.transfer.transfer_region.dest_region = Region::from(&rd)
};
});
}
}
}
})
}

View File

@ -1,24 +0,0 @@
use yew::prelude::*;
use plate_tool_lib::plate_instances::PlateInstance;
use plate_tool_lib::plate::PlateType;
#[derive(PartialEq, Properties)]
pub struct PlateProps {
pub source_plate: PlateInstance,
pub destination_plate: PlateInstance,
pub cell_height: f64,
pub ptype: PlateType,
pub send_coordinates: Option<Callback<(u8,u8)>>,
}
pub type MStartHandle = UseStateHandle<Option<(u8, u8)>>;
pub type MEndHandle = UseStateHandle<Option<(u8, u8)>>;
pub type MStatHandle = UseStateHandle<bool>;
#[derive(Debug)]
pub enum MouseEventType {
Mousedown,
Mouseenter,
}

View File

@ -1,305 +0,0 @@
#![allow(non_snake_case)]
use lazy_static::lazy_static;
use plate_tool_lib::Well;
use regex::Regex;
use serde::{Deserialize, Serialize};
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::callbacks::transfer_menu_callbacks;
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 super::states::{CurrentTransfer, MainState};
#[function_component]
pub fn TransferMenu() -> Html {
let (main_state, main_dispatch) = use_store::<MainState>();
let (ct_state, ct_dispatch) = use_store::<CurrentTransfer>();
let on_name_change = {
let ct_dispatch = ct_dispatch.clone();
transfer_menu_callbacks::on_name_change_callback(ct_dispatch)
};
let on_src_region_change = {
let ct_dispatch = ct_dispatch.clone();
transfer_menu_callbacks::on_src_region_change_callback(ct_dispatch)
};
let on_dest_region_change = {
let ct_dispatch = ct_dispatch.clone();
transfer_menu_callbacks::on_dest_region_change_callback(ct_dispatch)
};
let on_source_interleave_x_change = {
let ct_dispatch = ct_dispatch.clone();
transfer_menu_callbacks::on_source_interleave_x_change_callback(ct_dispatch)
};
let on_source_interleave_y_change = {
let ct_dispatch = ct_dispatch.clone();
transfer_menu_callbacks::on_source_interleave_y_change_callback(ct_dispatch)
};
let on_dest_interleave_x_change = {
let ct_dispatch = ct_dispatch.clone();
transfer_menu_callbacks::on_dest_interleave_x_change_callback(ct_dispatch)
};
let on_dest_interleave_y_change = {
let ct_dispatch = ct_dispatch.clone();
transfer_menu_callbacks::on_dest_interleave_y_change_callback(ct_dispatch)
};
let on_volume_change = {
let ct_dispatch = ct_dispatch.clone();
transfer_menu_callbacks::on_volume_change_callback(ct_dispatch)
};
let new_transfer_button_callback = {
let main_dispatch = main_dispatch.clone();
let main_state = main_state.clone();
let ct_dispatch = ct_dispatch.clone();
transfer_menu_callbacks::new_transfer_button_callback_callback(
main_dispatch,
main_state,
ct_dispatch,
)
};
let save_transfer_button_callback = {
let main_dispatch = main_dispatch.clone();
let main_state = main_state.clone();
let ct_state = ct_state.clone();
let new_transfer_button_callback = new_transfer_button_callback.clone();
transfer_menu_callbacks::save_transfer_button_callback_callback(
main_dispatch,
main_state,
ct_state,
new_transfer_button_callback,
)
};
let delete_transfer_button_callback = {
let main_state = main_state.clone();
let main_dispatch = main_dispatch.clone();
let ct_state = ct_state.clone();
let new_transfer_button_callback = new_transfer_button_callback.clone();
transfer_menu_callbacks::delete_transfer_button_callback(
main_state,
main_dispatch,
ct_state,
new_transfer_button_callback,
)
};
html! {
<div class="transfer_menu">
<form>
<div>
<label for="name"><h3>{"Name:"}</h3></label>
<input type="text" name="name"
onchange={on_name_change}
value={ct_state.transfer.name.clone()}/>
</div>
// Anything below here is not rendered when a Custom transfer is selected
if !matches!(&ct_state.transfer.transfer_region.source_region, Region::Custom(_)) {
<div>
<label for="src_region"><h3>{"Source Region:"}</h3></label>
<input type="text" name="src_region"
onchange={on_src_region_change}
value={RegionDisplay::from(&ct_state.transfer.transfer_region.source_region).text}/>
</div>
<div>
<label for="dest_region"><h3>{"Destination Region:"}</h3></label>
<input type="text" name="dest_region"
onchange={on_dest_region_change}
value={RegionDisplay::from(&ct_state.transfer.transfer_region.dest_region).text}/>
</div>
<div>
<h3>{"Source Interleave "}</h3>
<label for="source_interleave_x">{"Row:"}</label>
<input type="number" name="source_interleave_x"
onchange={on_source_interleave_x_change}
value={ct_state.transfer.transfer_region.interleave_source.0.to_string()}/>
<label for="source_interleave_y">{"Col:"}</label>
<input type="number" name="source_interleave_y"
onchange={on_source_interleave_y_change}
value={ct_state.transfer.transfer_region.interleave_source.1.to_string()}/>
</div>
<div>
<h3>{"Destination Interleave "}</h3>
<label for="dest_interleave_x">{"Row:"}</label>
<input type="number" name="dest_interleave_x"
onchange={on_dest_interleave_x_change}
value={ct_state.transfer.transfer_region.interleave_dest.0.to_string()}/>
<label for="dest_interleave_y">{"Col:"}</label>
<input type="number" name="dest_interleave_y"
onchange={on_dest_interleave_y_change}
value={ct_state.transfer.transfer_region.interleave_dest.1.to_string()}/>
</div>
<div>
<label for="volume"><h3>{"Volume"}</h3></label>
<input type="number" name="volume" class="volume_input"
min="0" step="0.1"
onchange={on_volume_change}
value={match ct_state.transfer.volume {
TransferVolume::Single(x) => x.to_string(),
_ => unreachable!(),
}}/>
</div>
}
<div id="controls">
<input type="button" name="new_transfer" onclick={new_transfer_button_callback}
value={"New"} />
<input type="button" name="save_transfer" onclick={save_transfer_button_callback}
value={"Save"} />
<input type="button" name="delete_transfer" onclick={delete_transfer_button_callback}
value={"Delete"} />
</div>
</form>
</div>
}
}
#[derive(PartialEq, Eq, Debug, Clone, Default, Serialize, Deserialize)]
pub struct RegionDisplay {
pub text: String,
pub col_start: u8,
pub row_start: u8,
pub col_end: u8,
pub row_end: u8,
}
impl TryFrom<String> for RegionDisplay {
type Error = &'static str;
fn try_from(value: String) -> Result<Self, Self::Error> {
lazy_static! {
static ref REGION_REGEX: Regex = Regex::new(r"([A-Z]+)(\d+):([A-Z]+)(\d+)").unwrap();
}
if let Some(captures) = REGION_REGEX.captures(&value) {
if captures.len() != 5 {
return Err("Not enough capture groups");
}
let col_start = letters_to_num(&captures[1]).ok_or("Column start failed to parse")?;
let col_end = letters_to_num(&captures[3]).ok_or("Column end failed to parse")?;
let row_start: u8 = captures[2]
.parse::<u8>()
.or(Err("Row start failed to parse"))?;
let row_end: u8 = captures[4]
.parse::<u8>()
.or(Err("Row end failed to parse"))?;
Ok(RegionDisplay {
text: value,
col_start,
row_start,
col_end,
row_end,
})
} else {
Err("Regex match failed")
}
}
}
impl TryFrom<&str> for RegionDisplay {
type Error = &'static str;
fn try_from(value: &str) -> Result<Self, Self::Error> {
lazy_static! {
static ref REGION_REGEX: Regex = Regex::new(r"([A-Z]+)(\d+):([A-Z]+)(\d+)").unwrap();
}
if let Some(captures) = REGION_REGEX.captures(value) {
if captures.len() != 5 {
return Err("Not enough capture groups");
}
let col_start = letters_to_num(&captures[1]).ok_or("Column start failed to parse")?;
let col_end = letters_to_num(&captures[3]).ok_or("Column end failed to parse")?;
let row_start: u8 = captures[2]
.parse::<u8>()
.or(Err("Row start failed to parse"))?;
let row_end: u8 = captures[4]
.parse::<u8>()
.or(Err("Row end failed to parse"))?;
Ok(RegionDisplay {
text: value.to_string(),
col_start,
row_start,
col_end,
row_end,
})
} else {
Err("Regex match failed")
}
}
}
impl From<&Region> for RegionDisplay {
fn from(value: &Region) -> Self {
match *value {
Region::Point(Well { row, col }) => {
RegionDisplay::try_from((col, row, col, row)).ok().unwrap()
}
Region::Rect(c1, c2) => RegionDisplay::try_from((c1.row, c1.col, c2.row, c2.col))
.ok()
.unwrap(),
Region::Custom(_) => RegionDisplay {
text: "CUSTOM".to_string(),
col_start: 0,
row_start: 0,
col_end: 0,
row_end: 0,
},
}
}
}
impl From<&RegionDisplay> for Region {
fn from(value: &RegionDisplay) -> Self {
if value.col_start == value.col_end && value.row_start == value.row_end {
Region::Point(Well { row: value.row_start, col: value.col_start })
} else {
Region::Rect(
Well { row: value.row_start, col: value.col_start },
Well { row: value.row_end, col: value.col_end },
)
}
}
}
impl TryFrom<(u8, u8, u8, u8)> for RegionDisplay {
type Error = &'static str;
fn try_from(value: (u8, u8, u8, u8)) -> Result<Self, Self::Error> {
// (Column Start, Row Start, Column End, Row End)
// This can only possibly fail if one of the coordinates is zero...
let cs = num_to_letters(value.0).ok_or("Column start failed to parse")?;
let ce = num_to_letters(value.2).ok_or("Column end failed to parse")?;
Ok(RegionDisplay {
text: format!("{}{}:{}{}", cs, value.1, ce, value.3),
col_start: value.0,
row_start: value.1,
col_end: value.2,
row_end: value.3,
})
}
}
#[cfg(test)]
mod tests {
use wasm_bindgen_test::*;
use super::*;
#[test]
#[wasm_bindgen_test]
fn test_try_from_string_for_regiondisplay() {
let desired = RegionDisplay {
text: "A1:E5".to_string(),
row_start: 1,
row_end: 5,
col_start: 1,
col_end: 5,
};
assert_eq!(desired, "A1:E5".to_string().try_into().unwrap());
}
}

View File

@ -1,99 +0,0 @@
#![allow(non_snake_case)]
mod components;
use std::error::Error;
use components::main_window::MainWindow;
use components::states::MainState;
use plate_tool_lib::csv::*;
use yew::prelude::*;
#[cfg(debug_assertions)]
use plate_tool_lib::*;
#[function_component]
pub fn App() -> Html {
html! {
<MainWindow />
}
}
#[cfg(debug_assertions)]
pub fn plate_test() {
let source = plate::Plate::new(plate::PlateType::Source, plate::PlateFormat::W96);
let destination = plate::Plate::new(plate::PlateType::Destination, plate::PlateFormat::W384);
let transfer = transfer_region::TransferRegion {
source_plate: source,
source_region: transfer_region::Region::Rect(Well { row: 1, col: 1 }, Well { row: 2, col: 2 }),
dest_plate: destination,
dest_region: transfer_region::Region::Rect(Well { row: 2, col: 2 }, Well { row: 11, col: 11 }),
interleave_source: (1, 1),
interleave_dest: (3, 3),
};
println!("{}", transfer);
let sws = transfer.get_source_wells();
let m = transfer.calculate_map();
for w in sws {
println!("{:?} -> {:?}", w, m(w));
}
}
pub fn state_to_csv(state: &MainState) -> Result<String, Box<dyn Error>> {
match state.preferences.csv_export_type {
components::states::CsvExportType::Normal => state_to_csv_regular(state),
components::states::CsvExportType::EchoClient => state_to_csv_echo_client(state)
}
}
fn state_to_csv_regular(state: &MainState) -> Result<String, Box<dyn Error>> {
let mut records: Vec<TransferRecord> = Vec::new();
for transfer in &state.transfers {
let src_barcode = state
.source_plates
.iter()
.find(|spi| spi.get_uuid() == transfer.source_id)
.ok_or("Found unpurged transfer")?;
let dest_barcode = state
.destination_plates
.iter()
.find(|dpi| dpi.get_uuid() == transfer.dest_id)
.ok_or("Found unpurged transfer")?;
records.append(&mut transfer_to_records(
transfer,
&src_barcode.name,
&dest_barcode.name,
))
}
records_to_csv(records)
}
fn state_to_csv_echo_client(state: &MainState) -> Result<String, Box<dyn Error>> {
let current_src = state.selected_source_plate;
let current_dst = state.selected_dest_plate;
let mut records: Vec<TransferRecord> = Vec::new();
for transfer in &state.transfers {
let src_barcode = state
.source_plates
.iter()
.find(|spi| spi.get_uuid() == transfer.source_id)
.ok_or("Found unpurged transfer")?;
let dest_barcode = state
.destination_plates
.iter()
.find(|dpi| dpi.get_uuid() == transfer.dest_id)
.ok_or("Found unpurged transfer")?;
if src_barcode.get_uuid() == current_src && dest_barcode.get_uuid() == current_dst {
records.append(&mut transfer_to_records(
transfer,
&src_barcode.name,
&dest_barcode.name,
))
}
}
records_to_echo_client_csv(records)
}

View File

@ -0,0 +1,528 @@
#![allow(non_snake_case)]
use std::collections::HashSet;
use js_sys::Array;
use lazy_static::lazy_static;
use regex::Regex;
use wasm_bindgen::{prelude::*, JsCast, JsValue};
use web_sys::{
Blob, HtmlAnchorElement, HtmlButtonElement, HtmlDialogElement, HtmlFormElement,
HtmlInputElement, HtmlOptionElement, HtmlSelectElement, Url,
};
use yew::prelude::*;
use yewdux::prelude::*;
use super::new_plate_dialog::NewPlateDialog;
use super::plates::plate_container::PlateContainer;
use super::states::{CurrentTransfer, MainState};
use super::transfer_menu::{letters_to_num, RegionDisplay, TransferMenu};
use super::tree::Tree;
use crate::data::csv::state_to_csv;
use crate::data::plate_instances::PlateInstance;
use crate::data::transfer::Transfer;
use crate::data::transfer_region::{Region, TransferRegion};
#[function_component]
pub fn MainWindow() -> Html {
let (main_state, main_dispatch) = use_store::<MainState>();
let (_, ct_dispatch) = use_store::<CurrentTransfer>();
let source_plate_instance: Option<PlateInstance> = main_state
.source_plates
.iter()
.find(|spi| spi.get_uuid() == main_state.selected_source_plate)
.cloned();
if let Some(spi) = source_plate_instance.clone() {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.source_plate = spi.plate;
});
}
let destination_plate_instance: Option<PlateInstance> = main_state
.destination_plates
.iter()
.find(|dpi| dpi.get_uuid() == main_state.selected_dest_plate)
.cloned();
if let Some(dpi) = destination_plate_instance.clone() {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.dest_plate = dpi.plate;
});
}
let toggle_in_transfer_hashes_callback = {
let main_dispatch = main_dispatch.clone();
Callback::from(move |_| {
main_dispatch.reduce_mut(|state| {
state.preferences.in_transfer_hashes ^= true;
})
})
};
let new_plate_dialog_is_open = use_state_eq(|| false);
let new_plate_dialog_callback = {
let new_plate_dialog_is_open = new_plate_dialog_is_open.clone();
Callback::from(move |_| {
new_plate_dialog_is_open.set(false);
})
};
let open_new_plate_dialog_callback = {
let new_plate_dialog_is_open = new_plate_dialog_is_open.clone();
Callback::from(move |_| {
new_plate_dialog_is_open.set(true);
})
};
let new_button_callback = {
let main_dispatch = main_dispatch.clone();
Callback::from(move |_| {
let window = web_sys::window().unwrap();
let confirm =
window.confirm_with_message("This will reset all plates and transfers. Proceed?");
if let Ok(confirm) = confirm {
if confirm {
main_dispatch.set(MainState::default());
ct_dispatch.set(CurrentTransfer::default());
}
}
})
};
let export_csv_button_callback = {
let main_state = main_state.clone();
Callback::from(move |_| {
if main_state.transfers.is_empty() {
web_sys::window()
.unwrap()
.alert_with_message("No transfers to export.")
.unwrap();
return;
}
web_sys::window().unwrap().alert_with_message("CSV export is currently not importable. Export as JSON if you'd like to back up your work!").unwrap();
if let Ok(csv) = state_to_csv(&main_state) {
save_str(&csv, "transfers.csv");
}
})
};
let export_json_button_callback = {
Callback::from(move |_| {
if let Ok(json) = serde_json::to_string(&main_state) {
save_str(&json, "plate-tool-state.json");
} else {
web_sys::window()
.unwrap()
.alert_with_message("Failed to export.")
.unwrap();
}
})
};
let import_json_button_callback = {
let main_dispatch = main_dispatch.clone();
Callback::from(move |_| {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let body = document.body().unwrap();
let modal = document
.create_element("dialog")
.unwrap()
.dyn_into::<HtmlDialogElement>()
.unwrap();
modal.set_text_content(Some("Import File:"));
let onclose_callback = {
let modal = modal.clone();
Closure::<dyn FnMut(_)>::new(move |_: Event| {
modal.remove();
})
};
modal.set_onclose(Some(onclose_callback.as_ref().unchecked_ref()));
onclose_callback.forget();
let form = document
.create_element("form")
.unwrap()
.dyn_into::<HtmlFormElement>()
.unwrap();
let input = document
.create_element("input")
.unwrap()
.dyn_into::<HtmlInputElement>()
.unwrap();
input.set_type("file");
input.set_accept(".json");
form.append_child(&input).unwrap();
let input_callback = {
let main_dispatch = main_dispatch.clone();
let modal = modal.clone();
Closure::<dyn FnMut(_)>::new(move |e: Event| {
if let Some(input) = e.current_target() {
let input = input
.dyn_into::<HtmlInputElement>()
.expect("We know this is an input.");
if let Some(files) = input.files() {
if let Some(file) = files.get(0) {
let fr = web_sys::FileReader::new().unwrap();
fr.read_as_text(&file).unwrap();
let fr1 = fr.clone(); // Clone to avoid outliving closure
let main_dispatch = main_dispatch.clone(); // Clone to satisfy FnMut
// trait
let modal = modal.clone();
let onload = Closure::<dyn FnMut(_)>::new(move |_: Event| {
if let Some(value) =
&fr1.result().ok().and_then(|v| v.as_string())
{
let ms = serde_json::from_str::<MainState>(value);
match ms {
Ok(ms) => main_dispatch.set(ms),
Err(e) => log::debug!("{:?}", e),
};
modal.close();
}
});
fr.set_onload(Some(onload.as_ref().unchecked_ref()));
onload.forget(); // Magic (don't touch)
}
}
}
})
};
input.set_onchange(Some(input_callback.as_ref().unchecked_ref()));
input_callback.forget(); // Magic straight from the docs, don't touch :(
modal.append_child(&form).unwrap();
body.append_child(&modal).unwrap();
modal.show_modal().unwrap();
})
};
let import_transfer_csv_callback = {
Callback::from(move |_| {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let body = document.body().unwrap();
let modal = document
.create_element("dialog")
.unwrap()
.dyn_into::<HtmlDialogElement>()
.unwrap();
modal.set_text_content(Some("Import File:"));
let onclose_callback = {
let modal = modal.clone();
Closure::<dyn FnMut(_)>::new(move |_: Event| {
modal.remove();
})
};
modal.set_onclose(Some(onclose_callback.as_ref().unchecked_ref()));
onclose_callback.forget();
let form = document
.create_element("form")
.unwrap()
.dyn_into::<HtmlFormElement>()
.unwrap();
let input = document
.create_element("input")
.unwrap()
.dyn_into::<HtmlInputElement>()
.unwrap();
input.set_type("file");
input.set_accept(".csv");
form.append_child(&input).unwrap();
let input_callback = {
let main_dispatch = main_dispatch.clone();
let modal = modal.clone();
Closure::<dyn FnMut(_)>::new(move |e: Event| {
if let Some(input) = e.current_target() {
let input = input
.dyn_into::<HtmlInputElement>()
.expect("We know this is an input.");
if let Some(files) = input.files() {
if let Some(file) = files.get(0) {
let fr = web_sys::FileReader::new().unwrap();
fr.read_as_text(&file).unwrap();
let fr1 = fr.clone(); // Clone to avoid outliving closure
let main_dispatch = main_dispatch.clone(); // Clone to satisfy FnMut
// trait
let modal = modal.clone();
let onload = Closure::<dyn FnMut(_)>::new(move |_: Event| {
if let Some(value) =
&fr1.result().ok().and_then(|v| v.as_string())
{
let mut rdr = csv::Reader::from_reader(value.as_bytes());
let mut records = Vec::new();
for record in
rdr.deserialize::<crate::data::csv::TransferRecord>()
{
match record {
Ok(r) => {
//log::debug!("{:?}", r);
records.push(r);
}
Err(e) => {
log::debug!("{:?}", e);
}
}
}
let mut sources: HashSet<String> = HashSet::new();
let mut destinations: HashSet<String> = HashSet::new();
for record in records.iter() {
sources.insert(record.source_plate.clone());
destinations.insert(record.destination_plate.clone());
}
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let form = document
.create_element("form")
.unwrap()
.dyn_into::<HtmlFormElement>()
.unwrap();
let from_source = document
.create_element("select")
.unwrap()
.dyn_into::<HtmlSelectElement>()
.unwrap();
for source in sources {
let option = document
.create_element("option")
.unwrap()
.dyn_into::<HtmlOptionElement>()
.unwrap();
option.set_value(&source);
option.set_text(&source);
from_source.append_child(&option).unwrap();
}
let to_source = document
.create_element("select")
.unwrap()
.dyn_into::<HtmlSelectElement>()
.unwrap();
for source in &main_dispatch.get().source_plates {
let option = document
.create_element("option")
.unwrap()
.dyn_into::<HtmlOptionElement>()
.unwrap();
option.set_value(&source.name);
option.set_text(&source.name);
to_source.append_child(&option).unwrap();
}
let from_dest = document
.create_element("select")
.unwrap()
.dyn_into::<HtmlSelectElement>()
.unwrap();
for dest in destinations {
let option = document
.create_element("option")
.unwrap()
.dyn_into::<HtmlOptionElement>()
.unwrap();
option.set_value(&dest);
option.set_text(&dest);
from_dest.append_child(&option).unwrap();
}
let to_dest = document
.create_element("select")
.unwrap()
.dyn_into::<HtmlSelectElement>()
.unwrap();
for dest in &main_dispatch.get().destination_plates {
let option = document
.create_element("option")
.unwrap()
.dyn_into::<HtmlOptionElement>()
.unwrap();
option.set_value(&dest.name);
option.set_text(&dest.name);
to_dest.append_child(&option).unwrap();
}
let submit = document
.create_element("button")
.unwrap()
.dyn_into::<HtmlButtonElement>()
.unwrap();
submit.set_value("Submit");
submit.set_inner_text("Submit");
let submit_callback = {
let main_dispatch = main_dispatch.clone();
let from_source = from_source.clone();
let to_source = to_source.clone();
let from_dest = from_dest.clone();
let to_dest = to_dest.clone();
Closure::<dyn FnMut(_)>::new(move |_: Event| {
let from_source = from_source.value();
let to_source = to_source.value();
let from_dest = from_dest.value();
let to_dest = to_dest.value();
lazy_static! {
static ref REGEX: Regex =
Regex::new(r"([A-Z]+)(\d+)").unwrap();
}
let records: Vec<((u8, u8), (u8, u8))> = records
.iter()
.filter(|record| {
record.source_plate == from_source
})
.filter(|record| {
record.destination_plate == from_dest
})
.map(|record| {
let c1 = REGEX
.captures(&record.source_well)
.unwrap();
let c2 = REGEX
.captures(&record.destination_well)
.unwrap();
log::debug!("{} {}", &record.source_well, &record.destination_well);
log::debug!("{},{} {},{}", &c1[1], &c1[2], &c2[1], &c2[2]);
(
(
letters_to_num(&c1[1]).unwrap(),
c1[2].parse::<u8>().unwrap(),
),
(
letters_to_num(&c2[1]).unwrap(),
c2[2].parse::<u8>().unwrap(),
),
)
})
.collect();
let spi = main_dispatch
.get()
.source_plates
.iter()
.find(|src| src.name == to_source)
.unwrap()
.clone();
let dpi = main_dispatch
.get()
.destination_plates
.iter()
.find(|dest| dest.name == to_dest)
.unwrap()
.clone();
let custom_region = Region::new_custom(&records);
let transfer_region = TransferRegion {
source_region: custom_region.clone(),
dest_region: custom_region,
interleave_source: (1, 1),
interleave_dest: (1, 1),
source_plate: spi.plate,
dest_plate: dpi.plate,
};
let transfer = Transfer::new(
spi,
dpi,
transfer_region,
"Custom Transfer".to_string(),
);
main_dispatch.reduce_mut(|state| {
state.transfers.push(transfer);
state.selected_transfer = state
.transfers
.last()
.expect("An element should have just been added")
.get_uuid();
});
})
};
submit.set_onclick(Some(
submit_callback.as_ref().unchecked_ref(),
));
submit_callback.forget();
form.append_child(&from_source).unwrap();
form.append_child(&to_source).unwrap();
form.append_child(&from_dest).unwrap();
form.append_child(&to_dest).unwrap();
modal.append_child(&submit).unwrap();
modal.append_child(&form).unwrap();
}
});
fr.set_onload(Some(onload.as_ref().unchecked_ref()));
onload.forget(); // Magic (don't touch)
}
}
}
})
};
input.set_onchange(Some(input_callback.as_ref().unchecked_ref()));
input_callback.forget(); // Magic straight from the docs, don't touch :(
modal.append_child(&form).unwrap();
body.append_child(&modal).unwrap();
modal.show_modal().unwrap();
})
};
html! {
<>
<div class="upper_menu">
<div class="dropdown">
<button>{"File"}</button>
<button onclick={new_button_callback}>{"New"}</button>
<div class="dropdown-sub">
<button>{"Export"}</button>
<div>
<button onclick={export_csv_button_callback}>{"Export as CSV"}</button>
<button onclick={export_json_button_callback}>{"Export as JSON"}</button>
</div>
</div>
<div class="dropdown-sub">
<button>{"Import"}</button>
<div>
<button onclick={import_json_button_callback}>{"Import from JSON"}</button>
<button onclick={import_transfer_csv_callback}>{"Import Transfer from CSV"}</button>
</div>
</div>
</div>
<div class="dropdown">
<button>{"Options"}</button>
<div class="dropdown-sub">
<button>{"Styles"}</button>
<div>
<button onclick={toggle_in_transfer_hashes_callback}>{"Toggle transfer hashes"}</button>
</div>
</div>
</div>
</div>
<div class="main_container">
<Tree open_new_plate_callback={open_new_plate_dialog_callback}/>
<TransferMenu />
<PlateContainer source_dims={source_plate_instance}
destination_dims={destination_plate_instance}/>
if {*new_plate_dialog_is_open} {
<NewPlateDialog close_callback={new_plate_dialog_callback}/>
}
</div>
</>
}
}
fn save_str(data: &str, name: &str) {
let blob =
Blob::new_with_str_sequence(&Array::from_iter(std::iter::once(JsValue::from_str(data))));
if let Ok(blob) = blob {
let url = Url::create_object_url_with_blob(&blob).expect("We have a blob, why not URL?");
// Beneath is the cool hack to download files
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let anchor = document
.create_element("a")
.unwrap()
.dyn_into::<HtmlAnchorElement>()
.unwrap();
anchor.set_download(name);
anchor.set_href(&url);
anchor.click();
}
}

View File

@ -4,4 +4,3 @@ pub mod plates;
pub mod states; pub mod states;
pub mod transfer_menu; pub mod transfer_menu;
pub mod tree; pub mod tree;
mod callbacks;

View File

@ -0,0 +1,123 @@
use yew::prelude::*;
use yewdux::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{EventTarget, FormData, HtmlDialogElement, HtmlFormElement};
use crate::components::states::MainState;
use crate::data::plate::*;
use crate::data::plate_instances::PlateInstance;
#[derive(PartialEq, Properties)]
pub struct NewPlateDialogProps {
pub close_callback: Callback<()>,
}
#[function_component]
pub fn NewPlateDialog(props: &NewPlateDialogProps) -> Html {
let (_, dispatch) = use_store::<MainState>();
let new_plate_callback = {
let close_callback = props.close_callback.clone();
Callback::from(move |e: SubmitEvent| {
e.prevent_default();
close_callback.emit(());
let target: Option<EventTarget> = e.target();
let form = target.and_then(|t| t.dyn_into::<HtmlFormElement>().ok());
if let Some(form) = form {
if let Ok(form_data) = FormData::new_with_form(&form) {
let name = form_data.get("new_plate_name").as_string().unwrap();
let format = match form_data.get("plate_format").as_string().unwrap().as_str() {
"6" => PlateFormat::W6,
"12" => PlateFormat::W12,
"24" => PlateFormat::W24,
"48" => PlateFormat::W48,
"96" => PlateFormat::W96,
"384" => PlateFormat::W384,
"1536" => PlateFormat::W1536,
"3456" => PlateFormat::W3456,
_ => unreachable!(),
};
if let Some(pt_string) = form_data.get("new_plate_type").as_string() {
let plate_type = match pt_string.as_str() {
"src" => PlateType::Source,
"dest" => PlateType::Destination,
_ => PlateType::Source,
};
dispatch.reduce_mut(|s| {
if plate_type == PlateType::Source {
s.add_source_plate(PlateInstance::new(
PlateType::Source,
format,
name,
))
} else {
s.add_dest_plate(PlateInstance::new(
PlateType::Destination,
format,
name,
))
}
});
}
}
}
})
};
let onclose = {
let close_callback = props.close_callback.clone();
Callback::from(move |_: Event| {
close_callback.emit(());
})
};
// This whole section is optional, only if you want the backdrop
let dialog_ref = use_node_ref();
{
let dialog_ref = dialog_ref.clone();
use_effect_with_deps(
|dialog_ref| {
dialog_ref
.cast::<HtmlDialogElement>()
.unwrap()
.show_modal()
.ok();
},
dialog_ref,
);
}
html! {
<dialog ref={dialog_ref} class="dialog new_plate_dialog" onclose={onclose}>
<h2>{"Create a plate:"}</h2>
<form onsubmit={new_plate_callback}>
<input type="text" name="new_plate_name" placeholder="Name"/>
<select name="plate_format">
<option value="6">{"6"}</option>
<option value="12">{"12"}</option>
<option value="24">{"24"}</option>
<option value="48">{"48"}</option>
<option value="96" selected={true}>{"96"}</option>
<option value="384">{"384"}</option>
<option value="1536">{"1536"}</option>
<option value="3456">{"3456"}</option>
</select>
<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>
<input type="submit" name="new_plate_button" value="Create" />
</form>
<form class="modal_close" method="dialog"><button /></form>
</dialog>
}
}
impl From<&PlateInstance> for String {
fn from(value: &PlateInstance) -> Self {
// Could have other formatting here
format!("{}, {}", value.name, value.plate.plate_format)
}
}

View File

@ -0,0 +1,216 @@
#![allow(non_snake_case)]
use std::collections::HashMap;
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::states::{CurrentTransfer, MainState};
use crate::data::plate_instances::PlateInstance;
use crate::data::transfer::Transfer;
use crate::data::transfer_region::Region;
// Color Palette for the Source Plates, can be changed here
use crate::components::plates::util::Palettes;
const PALETTE: super::util::ColorPalette = Palettes::RAINBOW;
use super::super::transfer_menu::{num_to_letters, RegionDisplay};
#[derive(Properties, PartialEq)]
pub struct DestinationPlateProps {
pub source_plate: PlateInstance,
pub destination_plate: PlateInstance,
pub cell_height: f64,
}
#[function_component]
pub fn DestinationPlate(props: &DestinationPlateProps) -> Html {
let (main_state, _) = use_store::<MainState>();
let (ct_state, ct_dispatch) = use_store::<CurrentTransfer>();
let m_start_handle: UseStateHandle<Option<(u8, u8)>> = use_state_eq(|| None);
let m_end_handle: UseStateHandle<Option<(u8, u8)>> = use_state_eq(|| None);
let m_stat_handle: UseStateHandle<bool> = use_state_eq(|| false);
if !(*m_stat_handle) {
let (pt1, pt2) = match ct_state.transfer.transfer_region.dest_region {
Region::Point((x, y)) => ((x, y), (x, y)),
Region::Rect(c1, c2) => (c1, c2),
Region::Custom(_) => ((0,0), (0,0)),
};
m_start_handle.set(Some(pt1));
m_end_handle.set(Some(pt2));
}
let destination_wells = ct_state.transfer.transfer_region.get_destination_wells();
let ordered_ids: Vec<uuid::Uuid> = {
let mut ids: Vec<uuid::Uuid> = main_state.transfers.clone().iter()
.map(|x| x.id)
.collect();
ids.sort_unstable();
ids
};
let mouse_callback = {
let m_start_handle = m_start_handle.clone();
let m_end_handle = m_end_handle.clone();
let m_stat_handle = m_stat_handle.clone();
Callback::from(move |(i, j, t)| match t {
MouseEventType::Mousedown => {
m_start_handle.set(Some((i, j)));
m_end_handle.set(Some((i, j)));
m_stat_handle.set(true);
}
MouseEventType::Mouseenter => {
if *m_stat_handle {
m_end_handle.set(Some((i, j)));
}
}
})
};
let transfer_map = {
let ts = main_state
.transfers
.iter()
.filter(|t| t.dest_id == props.destination_plate.get_uuid());
let mut tooltip_map: HashMap<(u8, u8), Vec<&Transfer>> = HashMap::new();
for t in ts {
let dws = t.transfer_region.get_destination_wells();
for dw in dws {
if let Some(val) = tooltip_map.get_mut(&dw) {
val.push(t);
} else {
tooltip_map.insert(dw, vec![t]);
}
}
}
tooltip_map
};
let mouseup_callback = {
let m_start_handle = m_start_handle.clone();
let m_end_handle = m_end_handle.clone();
Callback::from(move |_: MouseEvent| {
m_stat_handle.set(false);
if let Some(ul) = *m_start_handle {
if let Some(br) = *m_end_handle {
if let Ok(rd) = RegionDisplay::try_from((ul.0, ul.1, br.0, br.1)) {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.dest_region = Region::from(&rd);
});
}
}
}
})
};
let mouseleave_callback = Callback::clone(&mouseup_callback);
let screenshot_callback = Callback::from(|_| {
let _ = js_sys::eval("copy_screenshot_dest()");
});
let column_header = {
let headers = (1..=props.destination_plate.plate.size().1)
.map(|j| {
html! {<th>{format!("{:0>2}", j)}</th>}
})
.collect::<Html>();
html! {<tr><th />{ headers }</tr>}
};
let rows = (1..=props.destination_plate.plate.size().0)
.map(|i| {
let row_header = html! {<th>{num_to_letters(i)}</th>};
let row = (1..=props.destination_plate.plate.size().1).map(|j| {
html! {
<DestPlateCell i={i} j={j}
selected={super::source_plate::in_rect(*m_start_handle.clone(), *m_end_handle.clone(), (i,j))}
mouse={mouse_callback.clone()}
in_transfer={destination_wells.contains(&(i,j)) && main_state.preferences.in_transfer_hashes}
color={transfer_map.get(&(i,j))
.and_then(|t| t.last())
.map(|t| PALETTE.get_ordered(t.get_uuid(), &ordered_ids))
}
cell_height={props.cell_height}
title={transfer_map.get(&(i,j)).map(|transfers| format!("Used by: {}", transfers.iter().map(|t| t.name.clone())
.collect::<Vec<_>>().join(", ")))}
/>
}
}).collect::<Html>();
html! {
<tr>
{ row_header }{ row }
</tr>
}
})
.collect::<Html>();
html! {
<div ondblclick={screenshot_callback}
class={classes!{"dest_plate",
"W".to_owned()+&props.source_plate.plate.plate_format.to_string()}}>
<table
onmouseup={move |e| {
mouseup_callback.emit(e);
}}
onmouseleave={move |e| {
mouseleave_callback.emit(e);
}}>
{ column_header }{ rows }
</table>
</div>
}
}
#[derive(Debug)]
pub enum MouseEventType {
Mousedown,
Mouseenter,
}
#[derive(Properties, PartialEq)]
pub struct DestPlateCellProps {
pub i: u8,
pub j: u8,
pub selected: bool,
pub mouse: Callback<(u8, u8, MouseEventType)>,
pub in_transfer: Option<bool>,
color: Option<[f64; 3]>,
cell_height: f64,
title: Option<String>,
}
#[function_component]
fn DestPlateCell(props: &DestPlateCellProps) -> Html {
let selected_class = match props.selected {
true => Some("current_select"),
false => None,
};
let in_transfer_class = match props.in_transfer {
Some(true) => Some("in_transfer"),
_ => None,
};
let color = props.color.unwrap_or([255.0, 255.0, 255.0]);
let mouse = Callback::clone(&props.mouse);
let mouse2 = Callback::clone(&props.mouse);
let (i, j) = (props.i, props.j);
html! {
<td class={classes!("plate_cell", selected_class, in_transfer_class)}
style={format!("height: {}px;", props.cell_height)}
onmousedown={move |_| {
mouse.emit((i,j, MouseEventType::Mousedown))
}}
onmouseenter={move |_| {
mouse2.emit((i,j, MouseEventType::Mouseenter))
}}>
<div class="plate_cell_inner"
style={format!("background: rgba({},{},{},1);", color[0], color[1], color[2])}
title={if let Some(text) = &props.title {
text.clone()
} else { "".to_string() }}/>
</td>
}
}

View File

@ -0,0 +1,4 @@
pub mod destination_plate;
pub mod plate_container;
pub mod source_plate;
mod util;

View File

@ -1,17 +1,12 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
use plate_tool_lib::util::num_to_letters;
use wasm_bindgen::prelude::Closure; use wasm_bindgen::prelude::Closure;
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
use yew::prelude::*; use yew::prelude::*;
use plate_tool_lib::plate::PlateType; use crate::data::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 crate::components::states::MainState;
#[derive(Properties, PartialEq)] #[derive(Properties, PartialEq)]
pub struct PlateContainerProps { pub struct PlateContainerProps {
@ -21,8 +16,6 @@ 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()
@ -54,52 +47,19 @@ 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;
let show_current_coordinates = main_state.preferences.show_current_coordinates;
// (Row, Col)
let current_coordinates = use_state(|| (0, 0));
let send_coordinates = {
if show_current_coordinates {
let current_coordinates = current_coordinates.clone();
Some(Callback::from(move |coords| {
current_coordinates.set(coords)
}))
} else {
None
}
};
html! { html! {
<div class="plate_container"> <div class="plate_container">
<div class="plate_container--upper-left" >
if heatmap_enabled {
<div class="plate_container--heatmap-notice" >
<h3>{"Volume Heatmap Enabled"}</h3>
</div>
}
if show_current_coordinates {
<div class="plate_container--location">
<h3>{format!("R:{} C:{}",
num_to_letters(current_coordinates.0).unwrap_or("".to_string()),
current_coordinates.1)}</h3>
</div>
}
</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">
<h2>{spi.name.clone()}</h2> <h2>{spi.name.clone()}</h2>
<h2>{"Source"}</h2> <SourcePlate source_plate={spi.clone()} destination_plate={dpi.clone()}
<Plate source_plate={spi.clone()} destination_plate={dpi.clone()} cell_height={cell_height}/>
cell_height={cell_height} ptype={PlateType::Source}
send_coordinates={send_coordinates.clone()}
/>
</div> </div>
<div class="plate_container--destination"> <div class="plate_container--destination">
<h2>{dpi.name.clone()}</h2> <h2>{dpi.name.clone()}</h2>
<h2>{"Destination"}</h2> <DestinationPlate source_plate={spi.clone()} destination_plate={dpi.clone()}
<Plate source_plate={spi.clone()} destination_plate={dpi.clone()} cell_height={cell_height}/>
cell_height={cell_height} ptype={PlateType::Destination} send_coordinates={send_coordinates.clone()}/>
</div> </div>
} else { } else {
<h2>{"No Destination Plate Selected"}</h2> <h2>{"No Destination Plate Selected"}</h2>

View File

@ -0,0 +1,294 @@
#![allow(non_snake_case)]
use std::collections::HashMap;
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::states::{CurrentTransfer, MainState};
use crate::data::plate_instances::PlateInstance;
use crate::data::transfer::Transfer;
use crate::data::transfer_region::Region;
// Color Palette for the Source Plates, can be changed here
use crate::components::plates::util::Palettes;
const PALETTE: super::util::ColorPalette = Palettes::RAINBOW;
use super::super::transfer_menu::{num_to_letters, RegionDisplay};
#[derive(PartialEq, Properties)]
pub struct SourcePlateProps {
pub source_plate: PlateInstance,
pub destination_plate: PlateInstance,
pub cell_height: f64,
}
#[function_component]
pub fn SourcePlate(props: &SourcePlateProps) -> Html {
let (main_state, _) = use_store::<MainState>();
let (ct_state, ct_dispatch) = use_store::<CurrentTransfer>();
let m_start_handle: UseStateHandle<Option<(u8, u8)>> = use_state_eq(|| None);
let m_end_handle: UseStateHandle<Option<(u8, u8)>> = use_state_eq(|| None);
let m_stat_handle: UseStateHandle<bool> = use_state_eq(|| false);
if !(*m_stat_handle) {
let (pt1, pt2) = match ct_state.transfer.transfer_region.source_region {
Region::Point((x, y)) => ((x, y), (x, y)),
Region::Rect(c1, c2) => (c1, c2),
Region::Custom(_) => ((0,0), (0,0)),
};
m_start_handle.set(Some(pt1));
m_end_handle.set(Some(pt2));
}
let transfer_map = {
let ts = main_state
.transfers
.iter()
.filter(|t| t.source_id == props.source_plate.get_uuid());
let mut tooltip_map: HashMap<(u8, u8), Vec<&Transfer>> = HashMap::new();
for t in ts {
let sws = t.transfer_region.get_source_wells();
for sw in sws {
if let Some(val) = tooltip_map.get_mut(&sw) {
val.push(t);
} else {
tooltip_map.insert(sw, vec![t]);
}
}
}
tooltip_map
};
let source_wells = ct_state.transfer.transfer_region.get_source_wells();
let ordered_ids: Vec<uuid::Uuid> = {
let mut ids: Vec<uuid::Uuid> = main_state.transfers.clone().iter()
.map(|x| x.id)
.collect();
ids.sort_unstable();
ids
};
let mouse_callback = {
let m_start_handle = m_start_handle.clone();
let m_end_handle = m_end_handle.clone();
let m_stat_handle = m_stat_handle.clone();
Callback::from(move |(i, j, t)| match t {
MouseEventType::Mousedown => {
m_start_handle.set(Some((i, j)));
m_end_handle.set(Some((i, j)));
m_stat_handle.set(true);
}
MouseEventType::Mouseenter => {
if *m_stat_handle {
m_end_handle.set(Some((i, j)));
}
}
})
};
let mouseup_callback = {
let m_start_handle = m_start_handle.clone();
let m_end_handle = m_end_handle.clone();
Callback::from(move |_: MouseEvent| {
m_stat_handle.set(false);
if let Some(ul) = *m_start_handle {
if let Some(br) = *m_end_handle {
if let Ok(rd) = RegionDisplay::try_from((ul.0, ul.1, br.0, br.1)) {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.source_region = Region::from(&rd);
});
}
}
}
})
};
let mouseleave_callback = Callback::clone(&mouseup_callback);
let screenshot_callback = Callback::from(|_| {
let _ = js_sys::eval("copy_screenshot_src()");
});
let column_header = {
let headers = (1..=props.source_plate.plate.size().1)
.map(|j| {
html! {<th>
{format!("{:0>2}", j)}
</th>}
})
.collect::<Html>();
html! {<tr><th />{ headers }</tr>}
};
let rows = (1..=props.source_plate.plate.size().0)
.map(|i| {
let row_header = html! {<th>{num_to_letters(i)}</th>};
let row = (1..=props.source_plate.plate.size().1)
.map(|j| {
html! {
<SourcePlateCell i={i} j={j}
selected={in_rect(*m_start_handle.clone(), *m_end_handle.clone(), (i,j))}
mouse={mouse_callback.clone()}
in_transfer={source_wells.contains(&(i,j)) && main_state.preferences.in_transfer_hashes}
color={transfer_map.get(&(i,j))
.and_then(|t| t.last())
.map(|t| PALETTE.get_ordered(t.get_uuid(), &ordered_ids))
}
cell_height={props.cell_height}
title={transfer_map.get(&(i,j)).map(|transfers| format!("Used by: {}", transfers.iter().map(|t| t.name.clone())
.collect::<Vec<_>>().join(", ")))}
/>
}
})
.collect::<Html>();
html! {
<tr>
{ row_header }{ row }
</tr>
}
})
.collect::<Html>();
html! {
<div ondblclick={screenshot_callback}
class={classes!{"source_plate",
"W".to_owned()+&props.source_plate.plate.plate_format.to_string()}}>
<table
onmouseup={move |e| {
mouseup_callback.emit(e);
}}
onmouseleave={move |e| {
mouseleave_callback.emit(e);
}}>
{ column_header }
{ rows }
</table>
</div>
}
}
#[derive(PartialEq, Properties)]
pub struct SourcePlateCellProps {
i: u8,
j: u8,
selected: bool,
mouse: Callback<(u8, u8, MouseEventType)>,
in_transfer: Option<bool>,
color: Option<[f64; 3]>,
cell_height: f64,
title: Option<String>,
}
#[derive(Debug)]
pub enum MouseEventType {
Mousedown,
Mouseenter,
}
#[function_component]
fn SourcePlateCell(props: &SourcePlateCellProps) -> Html {
let selected_class = match props.selected {
true => Some("current_select"),
false => None,
};
let in_transfer_class = match props.in_transfer {
Some(true) => Some("in_transfer"),
_ => None,
};
let color = props.color.unwrap_or([255.0, 255.0, 255.0]);
let mouse = Callback::clone(&props.mouse);
let mouse2 = Callback::clone(&props.mouse);
let (i, j) = (props.i, props.j);
html! {
<td class={classes!("plate_cell", selected_class, in_transfer_class)}
style={format!("height: {}px;", props.cell_height)}
id={format!("color={:?}", props.color)}
onmousedown={move |_| {
mouse.emit((i,j, MouseEventType::Mousedown))
}}
onmouseenter={move |_| {
mouse2.emit((i,j, MouseEventType::Mouseenter))
}}>
<div class="plate_cell_inner"
style={format!("background: rgba({},{},{},1);", color[0], color[1], color[2])}
title={if let Some(text) = &props.title {
text.clone()
} else {"".to_string()}}/>
</td>
}
}
pub fn in_rect(corner1: Option<(u8, u8)>, corner2: Option<(u8, u8)>, pt: (u8, u8)) -> bool {
if let (Some(c1), Some(c2)) = (corner1, corner2) {
pt.0 <= u8::max(c1.0, c2.0)
&& pt.0 >= u8::min(c1.0, c2.0)
&& pt.1 <= u8::max(c1.1, c2.1)
&& pt.1 >= u8::min(c1.1, c2.1)
} else {
false
}
}
#[cfg(test)]
mod tests {
use wasm_bindgen_test::*;
use super::in_rect;
// in_rect tests
#[test]
#[wasm_bindgen_test]
fn test_in_rect1() {
// Test in center of rect
let c1 = (1, 1);
let c2 = (10, 10);
let pt = (5, 5);
assert!(in_rect(Some(c1), Some(c2), pt));
// Order of the corners should not matter:
assert!(in_rect(Some(c2), Some(c1), pt));
}
#[test]
#[wasm_bindgen_test]
fn test_in_rect2() {
// Test on top/bottom edges of rect
let c1 = (1, 1);
let c2 = (10, 10);
let pt1 = (1, 5);
let pt2 = (10, 5);
assert!(in_rect(Some(c1), Some(c2), pt1));
assert!(in_rect(Some(c1), Some(c2), pt2));
// Order of the corners should not matter:
assert!(in_rect(Some(c2), Some(c1), pt1));
assert!(in_rect(Some(c2), Some(c1), pt2));
}
#[test]
#[wasm_bindgen_test]
fn test_in_rect3() {
// Test on left/right edges of rect
let c1 = (1, 1);
let c2 = (10, 10);
let pt1 = (5, 1);
let pt2 = (5, 10);
assert!(in_rect(Some(c1), Some(c2), pt1));
assert!(in_rect(Some(c1), Some(c2), pt2));
// Order of the corners should not matter:
assert!(in_rect(Some(c2), Some(c1), pt1));
assert!(in_rect(Some(c2), Some(c1), pt2));
}
#[test]
#[wasm_bindgen_test]
fn test_in_rect4() {
// Test cases that should fail
let c1 = (1, 1);
let c2 = (10, 10);
let pt1 = (0, 0);
let pt2 = (15, 15);
assert!(!in_rect(Some(c1), Some(c2), pt1));
assert!(!in_rect(Some(c1), Some(c2), pt2));
}
}

View File

@ -2,6 +2,10 @@
// https://iquilezles.org/articles/palettes/ // https://iquilezles.org/articles/palettes/
// http://dev.thi.ng/gradients/ // http://dev.thi.ng/gradients/
use rand::prelude::*;
use rand::rngs::SmallRng;
use lazy_static::lazy_static;
#[derive(Clone, Copy, PartialEq, Debug)] #[derive(Clone, Copy, PartialEq, Debug)]
pub struct ColorPalette { pub struct ColorPalette {
a: [f64; 3], a: [f64; 3],
@ -26,8 +30,7 @@ impl ColorPalette {
] ]
} }
#[allow(dead_code)] // Preserve old implementation for reference pub fn _get_u8(&self, t: u8) -> [f64; 3] {
fn get_u8(&self, t: u8) -> [f64; 3] {
assert!(t > 0, "t must be greater than zero!"); assert!(t > 0, "t must be greater than zero!");
self.get((2f64.powi(-(t.ilog2() as i32))) * (t as f64 + 0.5f64) - 1.0f64) self.get((2f64.powi(-(t.ilog2() as i32))) * (t as f64 + 0.5f64) - 1.0f64)
} }
@ -38,22 +41,17 @@ impl ColorPalette {
// self.get(r.gen_range(0.0..1.0f64)) // self.get(r.gen_range(0.0..1.0f64))
// } // }
pub fn get_ordered(&self, t: uuid::Uuid, ordered_uuids: &[uuid::Uuid]) pub fn get_ordered(&self, t: uuid::Uuid, ordered_uuids: &Vec<uuid::Uuid>)
-> [f64; 3] { -> [f64; 3] {
let index = ordered_uuids.iter().position(|&x| x == t).expect("uuid must be in list of uuids") + 1; let index = ordered_uuids.iter().position(|&x| x == t).expect("uuid must be in list of uuids") + 1;
self.get(Self::space_evenly(index)) return 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 { fn space_evenly(x: usize) -> f64 {
let e: usize = (x.ilog2() + 1) as usize; let e: usize = (x.ilog2() + 1) as usize;
let d: usize = 2usize.pow(e as u32); let d: usize = (2usize.pow(e as u32)) as usize;
let n: usize = (2*x + 1) % d; let n: usize = (2*x + 1) % d;
(n as f64) / (d as f64) return (n as f64) / (d as f64);
} }
} }

View File

@ -2,9 +2,9 @@ use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use yewdux::{prelude::*, storage}; use yewdux::{prelude::*, storage};
use plate_tool_lib::plate::*; use crate::data::plate::*;
use plate_tool_lib::plate_instances::PlateInstance; use crate::data::plate_instances::PlateInstance;
use plate_tool_lib::transfer::Transfer; use crate::data::transfer::Transfer;
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, Store)] #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, Store)]
#[store(storage = "session")] #[store(storage = "session")]
@ -15,51 +15,12 @@ pub struct CurrentTransfer {
#[derive(PartialEq, Clone, Copy, Serialize, Deserialize)] #[derive(PartialEq, Clone, Copy, Serialize, Deserialize)]
pub struct Preferences { pub struct Preferences {
#[serde(default)]
pub in_transfer_hashes: bool, pub in_transfer_hashes: bool,
#[serde(default)]
pub volume_heatmap: bool,
#[serde(default)]
pub csv_export_type: CsvExportType,
#[serde(default)]
pub show_current_coordinates: bool,
} }
impl Default for Preferences { impl Default for Preferences {
fn default() -> Self { fn default() -> Self {
Self { Self { in_transfer_hashes: true }
in_transfer_hashes: true,
volume_heatmap: false,
csv_export_type: CsvExportType::Normal,
show_current_coordinates: false,
}
}
}
#[derive(PartialEq, Clone, Copy, Serialize, Deserialize, Default)]
pub enum CsvExportType {
#[default]
Normal,
EchoClient,
}
impl std::fmt::Display for CsvExportType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CsvExportType::Normal => write!(f, "Normal"),
CsvExportType::EchoClient => write!(f, "Echo Client"),
}
}
}
impl From<&str> for CsvExportType {
fn from(value: &str) -> Self {
match value.trim().to_lowercase().as_str() {
"normal" => CsvExportType::Normal,
"echo client" => CsvExportType::EchoClient,
_ => CsvExportType::default(),
}
} }
} }
@ -78,11 +39,8 @@ pub struct MainState {
} }
impl Store for MainState { impl Store for MainState {
fn new(context: &yewdux::Context) -> Self { fn new() -> Self {
init_listener( init_listener(storage::StorageListener::<Self>::new(storage::Area::Local));
storage::StorageListener::<Self>::new(storage::Area::Local),
context,
);
storage::load(storage::Area::Local) storage::load(storage::Area::Local)
.expect("Unable to load state") .expect("Unable to load state")
@ -150,21 +108,4 @@ impl MainState {
self.destination_plates[index].change_name(new_name.to_string()); self.destination_plates[index].change_name(new_name.to_string());
} }
} }
pub fn change_format(&mut self, id: Uuid, new_format: &PlateFormat) {
if let Some(index) = self
.source_plates
.iter()
.position(|spi| spi.get_uuid() == id)
{
self.source_plates[index].change_format(new_format);
}
if let Some(index) = self
.destination_plates
.iter()
.position(|dpi| dpi.get_uuid() == id)
{
self.destination_plates[index].change_format(new_format);
}
}
} }

View File

@ -0,0 +1,512 @@
#![allow(non_snake_case)]
use lazy_static::lazy_static;
use regex::Regex;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use wasm_bindgen::JsCast;
use web_sys::{EventTarget, HtmlInputElement};
use yew::prelude::*;
use yewdux::prelude::*;
use crate::data::{transfer::Transfer, transfer_region::Region};
use super::states::{CurrentTransfer, MainState};
#[function_component]
pub fn TransferMenu() -> Html {
let (main_state, main_dispatch) = use_store::<MainState>();
let (ct_state, ct_dispatch) = use_store::<CurrentTransfer>();
let on_name_change = {
let ct_dispatch = ct_dispatch.clone();
Callback::from(move |e: Event| {
let target: Option<EventTarget> = e.target();
let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
if let Some(input) = input {
if input.value() == "" {
return;
} // We do not want empty inputs!
ct_dispatch.reduce_mut(|state| {
state.transfer.name = input.value();
});
}
})
};
let on_src_region_change = {
let ct_dispatch = ct_dispatch.clone();
Callback::from(move |e: Event| {
if matches!(ct_dispatch.get().transfer.transfer_region.source_region, Region::Custom(_)) {
return; // Do nothing here!
}
let target: Option<EventTarget> = e.target();
let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
if let Some(input) = input {
if let Ok(rd) = RegionDisplay::try_from(input.value().to_uppercase()) {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.source_region = Region::from(&rd);
});
input.set_custom_validity("");
} else {
input.set_custom_validity("Invalid region.")
}
}
})
};
let on_dest_region_change = {
let ct_dispatch = ct_dispatch.clone();
Callback::from(move |e: Event| {
let target: Option<EventTarget> = e.target();
let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
if let Some(input) = input {
if let Ok(rd) = RegionDisplay::try_from(input.value().to_uppercase()) {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.dest_region = Region::from(&rd);
});
input.set_custom_validity("");
} else {
input.set_custom_validity("Invalid region.")
}
}
})
};
let on_source_interleave_x_change = {
let ct_dispatch = ct_dispatch.clone();
Callback::from(move |e: Event| {
let target: Option<EventTarget> = e.target();
let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
if let Some(input) = input {
if let Ok(num) = input.value().parse::<i8>() {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.interleave_source =
(num, state.transfer.transfer_region.interleave_source.1);
});
}
}
})
};
let on_source_interleave_y_change = {
let ct_dispatch = ct_dispatch.clone();
Callback::from(move |e: Event| {
let target: Option<EventTarget> = e.target();
let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
if let Some(input) = input {
if let Ok(num) = input.value().parse::<i8>() {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.interleave_source =
(state.transfer.transfer_region.interleave_source.0, num);
});
}
}
})
};
let on_dest_interleave_x_change = {
let ct_dispatch = ct_dispatch.clone();
Callback::from(move |e: Event| {
let target: Option<EventTarget> = e.target();
let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
if let Some(input) = input {
if let Ok(num) = input.value().parse::<i8>() {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.interleave_dest =
(num, state.transfer.transfer_region.interleave_dest.1);
});
}
}
})
};
let on_dest_interleave_y_change = {
let ct_dispatch = ct_dispatch.clone();
Callback::from(move |e: Event| {
let target: Option<EventTarget> = e.target();
let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
if let Some(input) = input {
if let Ok(num) = input.value().parse::<i8>() {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.interleave_dest =
(state.transfer.transfer_region.interleave_dest.0, num);
});
}
}
})
};
let on_volume_change = {
let ct_dispatch = ct_dispatch.clone();
Callback::from(move |e: Event| {
let input = e
.target()
.expect("Event must have target")
.dyn_into::<HtmlInputElement>()
.expect("Must have been emitted by input");
if let Ok(num) = input.value().parse::<f32>() {
ct_dispatch.reduce_mut(|state| {
state.transfer.volume = num;
});
}
})
};
let new_transfer_button_callback = {
let main_dispatch = main_dispatch.clone();
let main_state = main_state.clone();
Callback::from(move |_: MouseEvent| {
main_dispatch.reduce_mut(|state| {
state.selected_transfer = Uuid::nil();
});
ct_dispatch.reduce_mut(|state| {
state.transfer = Transfer::default();
state.transfer.source_id = main_state.selected_source_plate;
state.transfer.dest_id = main_state.selected_dest_plate;
});
})
};
let save_transfer_button_callback = {
let main_dispatch = main_dispatch.clone();
let main_state = main_state.clone();
let ct_state = ct_state.clone();
let new_transfer_button_callback = new_transfer_button_callback.clone();
Callback::from(move |e: MouseEvent| {
log::debug!("Button pressed");
if main_state.selected_transfer.is_nil() {
if let Some(spi) = main_state
.source_plates
.iter()
.find(|spi| spi.get_uuid() == main_state.selected_source_plate)
{
if let Some(dpi) = main_state
.destination_plates
.iter()
.find(|dpi| dpi.get_uuid() == main_state.selected_dest_plate)
{
let new_transfer = Transfer::new(
spi.clone(),
dpi.clone(),
ct_state.transfer.transfer_region.clone(),
ct_state.transfer.name.clone(),
);
main_dispatch.reduce_mut(|state| {
state.transfers.push(new_transfer);
state.selected_transfer = state
.transfers
.last()
.expect("An element should have just been added")
.get_uuid();
});
new_transfer_button_callback.emit(e); // If we just made a new transfer,
// then we should make another on
// save.
}
}
} else if let Some(index) = main_state
.transfers
.iter()
.position(|t| t.get_uuid() == main_state.selected_transfer)
{
main_dispatch.reduce_mut(|state| {
state.transfers[index] = ct_state.transfer.clone();
});
}
})
};
let delete_transfer_button_callback = {
let ct_state = ct_state.clone();
let new_callback = new_transfer_button_callback.clone();
Callback::from(move |e: MouseEvent| {
if main_state.selected_transfer.is_nil() {
// Maybe reset transfer?
} else if let Some(index) = main_state
.transfers
.iter()
.position(|t| t.get_uuid() == ct_state.transfer.get_uuid())
{
main_dispatch.reduce_mut(|state| {
state.transfers.remove(index);
state.selected_transfer = Uuid::nil();
});
new_callback.emit(e); // We need a new transfer now
}
})
};
html! {
<div class="transfer_menu">
<form>
<div>
<label for="name"><h3>{"Name:"}</h3></label>
<input type="text" name="name"
onchange={on_name_change}
value={ct_state.transfer.name.clone()}/>
</div>
// Anything below here is not rendered when a Custom transfer is selected
if !matches!(&ct_state.transfer.transfer_region.source_region, Region::Custom(_)) {
<div>
<label for="src_region"><h3>{"Source Region:"}</h3></label>
<input type="text" name="src_region"
onchange={on_src_region_change}
value={RegionDisplay::from(&ct_state.transfer.transfer_region.source_region).text}/>
</div>
<div>
<label for="dest_region"><h3>{"Destination Region:"}</h3></label>
<input type="text" name="dest_region"
onchange={on_dest_region_change}
value={RegionDisplay::from(&ct_state.transfer.transfer_region.dest_region).text}/>
</div>
<div>
<h3>{"Source Interleave "}</h3>
<label for="source_interleave_x">{"Row:"}</label>
<input type="number" name="source_interleave_x"
onchange={on_source_interleave_x_change}
value={ct_state.transfer.transfer_region.interleave_source.0.to_string()}/>
<label for="source_interleave_y">{"Col:"}</label>
<input type="number" name="source_interleave_y"
onchange={on_source_interleave_y_change}
value={ct_state.transfer.transfer_region.interleave_source.1.to_string()}/>
</div>
<div>
<h3>{"Destination Interleave "}</h3>
<label for="dest_interleave_x">{"Row:"}</label>
<input type="number" name="dest_interleave_x"
onchange={on_dest_interleave_x_change}
value={ct_state.transfer.transfer_region.interleave_dest.0.to_string()}/>
<label for="dest_interleave_y">{"Col:"}</label>
<input type="number" name="dest_interleave_y"
onchange={on_dest_interleave_y_change}
value={ct_state.transfer.transfer_region.interleave_dest.1.to_string()}/>
</div>
<div>
<label for="volume"><h3>{"Volume"}</h3></label>
<input type="number" name="volume" class="volume_input"
min="0" step="0.1"
onchange={on_volume_change}
value={ct_state.transfer.volume.to_string()}/>
</div>
}
<div id="controls">
<input type="button" name="new_transfer" onclick={new_transfer_button_callback}
value={"New"} />
<input type="button" name="save_transfer" onclick={save_transfer_button_callback}
value={"Save"} />
<input type="button" name="delete_transfer" onclick={delete_transfer_button_callback}
value={"Delete"} />
</div>
</form>
</div>
}
}
#[derive(PartialEq, Eq, Debug, Clone, Default, Serialize, Deserialize)]
pub struct RegionDisplay {
pub text: String,
pub col_start: u8,
pub row_start: u8,
pub col_end: u8,
pub row_end: u8,
}
impl TryFrom<String> for RegionDisplay {
type Error = &'static str;
fn try_from(value: String) -> Result<Self, Self::Error> {
lazy_static! {
static ref REGION_REGEX: Regex = Regex::new(r"([A-Z]+)(\d+):([A-Z]+)(\d+)").unwrap();
}
if let Some(captures) = REGION_REGEX.captures(&value) {
if captures.len() != 5 {
return Err("Not enough capture groups");
}
let col_start = letters_to_num(&captures[1]).ok_or("Column start failed to parse")?;
let col_end = letters_to_num(&captures[3]).ok_or("Column end failed to parse")?;
let row_start: u8 = captures[2]
.parse::<u8>()
.or(Err("Row start failed to parse"))?;
let row_end: u8 = captures[4]
.parse::<u8>()
.or(Err("Row end failed to parse"))?;
Ok(RegionDisplay {
text: value,
col_start,
row_start,
col_end,
row_end,
})
} else {
Err("Regex match failed")
}
}
}
impl TryFrom<&str> for RegionDisplay {
type Error = &'static str;
fn try_from(value: &str) -> Result<Self, Self::Error> {
lazy_static! {
static ref REGION_REGEX: Regex = Regex::new(r"([A-Z]+)(\d+):([A-Z]+)(\d+)").unwrap();
}
if let Some(captures) = REGION_REGEX.captures(&value) {
if captures.len() != 5 {
return Err("Not enough capture groups");
}
let col_start = letters_to_num(&captures[1]).ok_or("Column start failed to parse")?;
let col_end = letters_to_num(&captures[3]).ok_or("Column end failed to parse")?;
let row_start: u8 = captures[2]
.parse::<u8>()
.or(Err("Row start failed to parse"))?;
let row_end: u8 = captures[4]
.parse::<u8>()
.or(Err("Row end failed to parse"))?;
Ok(RegionDisplay {
text: value.to_string(),
col_start,
row_start,
col_end,
row_end,
})
} else {
Err("Regex match failed")
}
}
}
impl From<&Region> for RegionDisplay {
fn from(value: &Region) -> Self {
match *value {
Region::Point((col, row)) => {
RegionDisplay::try_from((col, row, col, row)).ok().unwrap()
}
Region::Rect(c1, c2) => RegionDisplay::try_from((c1.0, c1.1, c2.0, c2.1))
.ok()
.unwrap(),
Region::Custom(_) => RegionDisplay { text: "CUSTOM".to_string(), col_start: 0, row_start: 0, col_end: 0, row_end: 0 }
}
}
}
impl From<&RegionDisplay> for Region {
fn from(value: &RegionDisplay) -> Self {
if value.col_start == value.col_end && value.row_start == value.row_end {
Region::Point((value.col_start, value.row_start))
} else {
Region::Rect(
(value.col_start, value.row_start),
(value.col_end, value.row_end),
)
}
}
}
impl TryFrom<(u8, u8, u8, u8)> for RegionDisplay {
type Error = &'static str;
fn try_from(value: (u8, u8, u8, u8)) -> Result<Self, Self::Error> {
// (Column Start, Row Start, Column End, Row End)
// This can only possibly fail if one of the coordinates is zero...
let cs = num_to_letters(value.0).ok_or("Column start failed to parse")?;
let ce = num_to_letters(value.2).ok_or("Column end failed to parse")?;
Ok(RegionDisplay {
text: format!("{}{}:{}{}", cs, value.1, ce, value.3),
col_start: value.0,
row_start: value.1,
col_end: value.2,
row_end: value.3,
})
}
}
pub fn letters_to_num(letters: &str) -> Option<u8> {
let mut num: u8 = 0;
for (i, letter) in letters.to_ascii_uppercase().chars().rev().enumerate() {
log::debug!("{}, {}", i, letter);
let n = letter as u8;
if !(65..=90).contains(&n) {
return None;
}
num = num.checked_add((26_i32.pow(i as u32) * (n as i32 - 64)).try_into().ok()?)?;
}
Some(num)
}
pub fn num_to_letters(num: u8) -> Option<String> {
if num == 0 {
return None;
} // Otherwise, we will not return none!
// As another note, we can't represent higher than "IV" anyway;
// thus there's no reason for a loop (26^n with n>1 will NOT occur).
let mut text = "".to_string();
let mut digit1 = num.div_euclid(26u8);
let mut digit2 = num.rem_euclid(26u8);
if digit1 > 0 && digit2 == 0u8 {
digit1 -= 1;
digit2 = 26;
}
if digit1 != 0 {
text.push((64 + digit1) as char)
}
text.push((64 + digit2) as char);
Some(text.to_string())
}
#[cfg(test)]
mod tests {
use wasm_bindgen_test::*;
use super::{letters_to_num, num_to_letters, RegionDisplay};
#[test]
#[wasm_bindgen_test]
fn test_letters_to_num() {
assert_eq!(letters_to_num("D"), Some(4));
assert_eq!(letters_to_num("d"), None);
assert_eq!(letters_to_num("AD"), Some(26 + 4));
assert_eq!(letters_to_num("CG"), Some(3 * 26 + 7));
}
#[test]
#[wasm_bindgen_test]
fn test_num_to_letters() {
println!("27 is {:?}", num_to_letters(27));
assert_eq!(num_to_letters(1), Some("A".to_string()));
assert_eq!(num_to_letters(26), Some("Z".to_string()));
assert_eq!(num_to_letters(27), Some("AA".to_string()));
assert_eq!(num_to_letters(111), Some("DG".to_string()));
}
#[test]
#[wasm_bindgen_test]
fn test_l2n_and_n2l() {
assert_eq!(
num_to_letters(letters_to_num("A").unwrap()),
Some("A".to_string())
);
assert_eq!(
num_to_letters(letters_to_num("BJ").unwrap()),
Some("BJ".to_string())
);
for i in 1..=255 {
assert_eq!(letters_to_num(&num_to_letters(i).unwrap()), Some(i));
}
}
#[test]
#[wasm_bindgen_test]
fn test_try_from_string_for_regiondisplay() {
let desired = RegionDisplay {
text: "A1:E5".to_string(),
row_start: 1,
row_end: 5,
col_start: 1,
col_end: 5,
};
assert_eq!(desired, "A1:E5".to_string().try_into().unwrap());
}
}

View File

@ -1,19 +1,17 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
use plate_tool_lib::plate::PlateFormat;
use uuid::Uuid; use uuid::Uuid;
use wasm_bindgen::JsCast;
use web_sys::{HtmlDialogElement}; use web_sys::{EventTarget, HtmlDialogElement, HtmlElement, HtmlInputElement};
use yew::prelude::*; use yew::prelude::*;
use yewdux::prelude::*; use yewdux::prelude::*;
use crate::components::callbacks::{tree_callbacks};
use crate::components::new_plate_dialog::NewPlateDialogType;
use crate::components::states::{CurrentTransfer, MainState}; use crate::components::states::{CurrentTransfer, MainState};
use crate::data::transfer_region::Region;
#[derive(PartialEq, Properties)] #[derive(PartialEq, Properties)]
pub struct TreeProps { pub struct TreeProps {
pub open_new_plate_callback: Callback<NewPlateDialogType>, pub open_new_plate_callback: Callback<()>,
} }
#[function_component] #[function_component]
@ -24,33 +22,102 @@ pub fn Tree(props: &TreeProps) -> Html {
let open_plate_info_callback = { let open_plate_info_callback = {
let plate_menu_id = plate_modal_id.clone(); let plate_menu_id = plate_modal_id.clone();
tree_callbacks::open_plate_info_callback(plate_menu_id) Callback::from(move |e: MouseEvent| {
let target: Option<EventTarget> = e.target();
let li = target.and_then(|t| t.dyn_into::<HtmlElement>().ok());
if let Some(li) = li {
if let Ok(id) = li.id().as_str().parse::<u128>() {
plate_menu_id.set(Some(Uuid::from_u128(id)));
}
}
})
}; };
let plate_info_close_callback = { let plate_info_close_callback = {
let plate_menu_id = plate_modal_id.clone(); let plate_menu_id = plate_modal_id.clone();
tree_callbacks::plate_info_close_callback(plate_menu_id) Callback::from(move |_| {
plate_menu_id.set(None);
})
}; };
let plate_info_delete_callback = { let plate_info_delete_callback = {
let main_dispatch = main_dispatch.clone(); let dispatch = main_dispatch.clone();
let plate_menu_id = plate_modal_id.clone(); let plate_menu_id = plate_modal_id.clone();
tree_callbacks::plate_info_delete_callback(main_dispatch, plate_menu_id) Callback::from(move |_| {
if let Some(id) = *plate_menu_id {
dispatch.reduce_mut(|state| {
state.del_plate(id);
});
}
})
}; };
let source_plate_select_callback = { let source_plate_select_callback = {
let main_dispatch = main_dispatch.clone(); let main_dispatch = main_dispatch.clone();
let ct_dispatch = ct_dispatch.clone(); let ct_dispatch = ct_dispatch.clone();
tree_callbacks::source_plate_select_callback(main_dispatch, ct_dispatch)
Callback::from(move |e: MouseEvent| {
let target: Option<EventTarget> = e.target();
let li = target.and_then(|t| t.dyn_into::<HtmlElement>().ok());
if let Some(li) = li {
if let Ok(id) = li.id().as_str().parse::<u128>() {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.source_region = Region::default();
state.transfer.transfer_region.dest_region = Region::default();
});
main_dispatch.reduce_mut(|state| {
state.selected_source_plate = Uuid::from_u128(id);
state.selected_transfer = Uuid::nil();
});
}
}
})
}; };
let destination_plate_select_callback = { let destination_plate_select_callback = {
let main_dispatch = main_dispatch.clone(); let main_dispatch = main_dispatch.clone();
let ct_dispatch = ct_dispatch.clone(); let ct_dispatch = ct_dispatch.clone();
tree_callbacks::destination_plate_select_callback(main_dispatch, ct_dispatch)
Callback::from(move |e: MouseEvent| {
let target: Option<EventTarget> = e.target();
let li = target.and_then(|t| t.dyn_into::<HtmlElement>().ok());
if let Some(li) = li {
if let Ok(id) = li.id().as_str().parse::<u128>() {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.source_region = Region::default();
state.transfer.transfer_region.dest_region = Region::default();
});
main_dispatch.reduce_mut(|state| {
state.selected_dest_plate = Uuid::from_u128(id);
state.selected_transfer = Uuid::nil();
});
}
}
})
}; };
let transfer_select_callback = { let transfer_select_callback = {
let main_state = main_state.clone(); let main_state = main_state.clone();
let main_dispatch = main_dispatch.clone();
let ct_dispatch = ct_dispatch.clone(); Callback::from(move |e: MouseEvent| {
tree_callbacks::transfer_select_callback(main_state, main_dispatch, ct_dispatch) let target: Option<EventTarget> = e.target();
let li = target.and_then(|t| t.dyn_into::<HtmlElement>().ok());
if let Some(li) = li {
if let Ok(id) = li.id().as_str().parse::<u128>() {
let id = Uuid::from_u128(id);
if let Some(transfer) = main_state
.transfers
.iter()
.find(|transfer| transfer.get_uuid() == id)
{
main_dispatch.reduce_mut(|state| {
state.selected_source_plate = transfer.source_id;
state.selected_dest_plate = transfer.dest_id;
state.selected_transfer = id;
});
ct_dispatch.reduce_mut(|state| {
state.transfer = transfer.clone();
});
}
}
}
})
}; };
let source_plates = main_state let source_plates = main_state
@ -99,25 +166,13 @@ 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>
@ -133,6 +188,14 @@ 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>
} }
} }
@ -163,36 +226,24 @@ fn PlateInfoModal(props: &PlateInfoModalProps) -> Html {
Some(plate) => plate.name.clone(), Some(plate) => plate.name.clone(),
None => "Not Found".to_string(), None => "Not Found".to_string(),
}; };
let plate_format = plate.map(|p| p.plate.plate_format);
let plate_formats = [PlateFormat::W6,
PlateFormat::W12,
PlateFormat::W24,
PlateFormat::W48,
PlateFormat::W96,
PlateFormat::W384,
PlateFormat::W1536,
PlateFormat::W3456];
let plate_format_options = plate_formats.iter().map(|v| {
let selected = Some(v) == plate_format.as_ref();
html! {
<option value={v.to_string()} selected={selected}>{ v.to_string() }</option>
}
});
let onclose = { let onclose = {
let dialog_close_callback = props.dialog_close_callback.clone(); let dialog_close_callback = props.dialog_close_callback.clone();
move |_| dialog_close_callback.emit(()) move |_| dialog_close_callback.emit(())
}; };
let onclose_secondary = { let rename_onchange = {
let dialog_close_callback = props.dialog_close_callback.clone(); let id = props.id;
move |_| dialog_close_callback.emit(()) Callback::from(move |e: Event| {
log::debug!("Changed name");
let input = e
.target()
.expect("Event must have target")
.dyn_into::<HtmlInputElement>()
.unwrap();
main_dispatch.reduce_mut(|state| state.rename_plate(id, &input.value()))
})
}; };
let rename_onchange = tree_callbacks::rename_onchange(props.id, main_dispatch.clone());
let format_onchange = tree_callbacks::format_onchange(props.id, main_dispatch.clone());
let delete_onclick = { let delete_onclick = {
let delete_button_callback = props.delete_button_callback.clone(); let delete_button_callback = props.delete_button_callback.clone();
let dialog_ref = dialog_ref.clone(); let dialog_ref = dialog_ref.clone();
@ -205,8 +256,7 @@ fn PlateInfoModal(props: &PlateInfoModalProps) -> Html {
{ {
let dialog_ref = dialog_ref.clone(); let dialog_ref = dialog_ref.clone();
use_effect_with( use_effect_with_deps(
dialog_ref,
|dialog_ref| { |dialog_ref| {
dialog_ref dialog_ref
.cast::<HtmlDialogElement>() .cast::<HtmlDialogElement>()
@ -214,25 +264,15 @@ fn PlateInfoModal(props: &PlateInfoModalProps) -> Html {
.show_modal() .show_modal()
.ok(); .ok();
}, },
dialog_ref,
); );
} }
let merge_button_callback = {
let main_dispatch = main_dispatch.clone();
tree_callbacks::merge_button_callback(main_dispatch, props.id)
};
html! { html! {
<dialog ref={dialog_ref} class="dialog" onclose={onclose}> <dialog ref={dialog_ref} class="dialog" onclose={onclose}>
<h2>{"Plate Info"}</h2> <h2>{"Plate Info"}</h2>
<h3>{"Name: "}<input type="text" value={plate_name} onchange={rename_onchange}/></h3> <h3>{"Name: "}<input type="text" value={plate_name} onchange={rename_onchange}/></h3>
<button onclick={delete_onclick}>{"Delete"}</button> <button onclick={delete_onclick}>{"Delete"}</button>
if let Some(_) = plate_format {
<select name="modal_plate_format" onchange={format_onchange}>
{ for plate_format_options }
</select>
}
<button onclick={merge_button_callback} onclick={onclose_secondary}>{"Merge"}</button>
<form class="modal_close" method="dialog"><button /></form> <form class="modal_close" method="dialog"><button /></form>
</dialog> </dialog>
} }

81
src/data/csv.rs Normal file
View File

@ -0,0 +1,81 @@
use crate::components::states::MainState;
use crate::components::transfer_menu::num_to_letters;
use crate::data::transfer::Transfer;
use serde::{Serialize, Deserialize};
use std::error::Error;
#[derive(Serialize, Deserialize, Debug)]
pub struct TransferRecord {
#[serde(rename = "Source Plate")]
pub source_plate: String,
#[serde(rename = "Source Well")]
pub source_well: String,
#[serde(rename = "Dest Plate")]
pub destination_plate: String,
#[serde(rename = "Destination Well")]
pub destination_well: String,
#[serde(rename = "Transfer Volume")]
pub volume: f32,
#[serde(rename = "Concentration")]
pub concentration: Option<f32>,
}
pub fn state_to_csv(state: &MainState) -> Result<String, Box<dyn Error>> {
let mut records: Vec<TransferRecord> = Vec::new();
for transfer in &state.transfers {
let src_barcode = state
.source_plates
.iter()
.find(|spi| spi.get_uuid() == transfer.source_id)
.ok_or("Found unpurged transfer")?;
let dest_barcode = state
.destination_plates
.iter()
.find(|dpi| dpi.get_uuid() == transfer.dest_id)
.ok_or("Found unpurged transfer")?;
records.append(&mut transfer_to_records(
transfer,
&src_barcode.name,
&dest_barcode.name,
))
}
return records_to_csv(records);
}
fn transfer_to_records(
tr: &Transfer,
src_barcode: &str,
dest_barcode: &str,
) -> Vec<TransferRecord> {
let source_wells = tr.transfer_region.get_source_wells();
let map = tr.transfer_region.calculate_map();
let mut records: Vec<TransferRecord> = vec![];
for s_well in source_wells {
let dest_wells = map(s_well);
if let Some(dest_wells) = dest_wells {
for d_well in dest_wells {
records.push(TransferRecord {
source_plate: src_barcode.to_string(),
source_well: format!("{}{}", num_to_letters(s_well.0).unwrap(), s_well.1),
destination_plate: dest_barcode.to_string(),
destination_well: format!("{}{}", num_to_letters(d_well.0).unwrap(), d_well.1),
volume: tr.volume,
concentration: None,
})
}
}
}
records
}
fn records_to_csv(trs: Vec<TransferRecord>) -> Result<String, Box<dyn Error>> {
let mut wtr = csv::WriterBuilder::new().from_writer(vec![]);
for record in trs {
wtr.serialize(record)?
}
let data = String::from_utf8(wtr.into_inner()?)?;
Ok(data)
}

View File

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

View File

@ -60,37 +60,6 @@ impl std::fmt::Display for PlateFormat {
} }
} }
} }
impl TryFrom<&str> for PlateFormat {
type Error = ();
fn try_from(value: &str) -> Result<Self, Self::Error> {
let lower = value.to_lowercase();
match lower.trim() {
"w6" | "6" => Ok(PlateFormat::W6),
"w12" | "12" => Ok(PlateFormat::W12),
"w24" | "24" => Ok(PlateFormat::W24),
"w48" | "48" => Ok(PlateFormat::W48),
"w96" | "96" => Ok(PlateFormat::W96),
"w384" | "384" => Ok(PlateFormat::W384),
"w1536" | "1536" => Ok(PlateFormat::W1536),
"w3456" | "3456" => Ok(PlateFormat::W3456),
_ => Err(())
}
}
}
impl From<&PlateFormat> for u16 {
fn from(value: &PlateFormat) -> Self {
match value {
PlateFormat::W6 => 6,
PlateFormat::W12 => 12,
PlateFormat::W24 => 24,
PlateFormat::W48 => 48,
PlateFormat::W96 => 96,
PlateFormat::W384 => 384,
PlateFormat::W1536 => 1536,
PlateFormat::W3456 => 3456,
}
}
}
impl PlateFormat { impl PlateFormat {
pub fn size(&self) -> (u8, u8) { pub fn size(&self) -> (u8, u8) {

View File

@ -2,7 +2,7 @@ use super::plate::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
#[derive(PartialEq, Clone, Serialize, Deserialize, Debug)] #[derive(PartialEq, Clone, Serialize, Deserialize)]
pub struct PlateInstance { pub struct PlateInstance {
pub plate: Plate, pub plate: Plate,
#[serde(rename = "id_v7")] #[serde(rename = "id_v7")]
@ -30,10 +30,6 @@ impl PlateInstance {
pub fn change_name(&mut self, new_name: String) { pub fn change_name(&mut self, new_name: String) {
self.name = new_name; self.name = new_name;
} }
pub fn change_format(&mut self, new_format: &PlateFormat) {
self.plate.plate_format = *new_format;
}
} }
impl From<Plate> for PlateInstance { impl From<Plate> for PlateInstance {
@ -45,10 +41,3 @@ impl From<Plate> for PlateInstance {
} }
} }
} }
impl From<&PlateInstance> for String {
fn from(value: &PlateInstance) -> Self {
// Could have other formatting here
format!("{}, {}", value.name, value.plate.plate_format)
}
}

View File

@ -1,8 +1,5 @@
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;
@ -17,7 +14,8 @@ 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,
pub volume: TransferVolume, #[serde(default = "default_volume")]
pub volume: f32,
} }
impl Default for Transfer { impl Default for Transfer {
@ -28,11 +26,15 @@ 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::default(), volume: 2.5f32,
} }
} }
} }
fn default_volume() -> f32 {
2.5f32
}
impl Transfer { impl Transfer {
pub fn new( pub fn new(
source: PlateInstance, source: PlateInstance,
@ -46,7 +48,7 @@ impl Transfer {
name, name,
id: Uuid::now_v7(), id: Uuid::now_v7(),
transfer_region: tr, transfer_region: tr,
volume: transfer_volume::TransferVolume::Single(2.5), volume: 2.5,
} }
} }

View File

@ -1,32 +1,27 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::components::transfer_menu::RegionDisplay;
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<Well>, src: Vec<(u8, u8)>,
dest: Vec<Well>, dest: Vec<(u8, u8)>,
}
impl CustomRegion {
pub fn new(src: Vec<Well>, dest: Vec<Well>) -> Self {
CustomRegion { src, dest }
}
} }
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] #[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
pub enum Region { pub enum Region {
Rect(Well, Well), Rect((u8, u8), (u8, u8)),
Point(Well), Point((u8, u8)),
Custom(CustomRegion), Custom(CustomRegion),
} }
impl Default for Region { impl Default for Region {
fn default() -> Self { fn default() -> Self {
Region::Point(Well { row: 1, col: 1 }) Region::Point((1, 1))
} }
} }
impl TryFrom<Region> for (Well, Well) { impl TryFrom<Region> for ((u8, u8), (u8, u8)) {
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,11 +32,10 @@ impl TryFrom<Region> for (Well, Well) {
} }
} }
} }
impl Region { impl Region {
pub fn new_custom(transfers: &Vec<(Well, Well)>) -> Self { pub fn new_custom(transfers: &Vec<((u8, u8), (u8, u8))>) -> Self {
let mut src_pts: Vec<Well> = Vec::with_capacity(transfers.len()); let mut src_pts: Vec<(u8, u8)> = Vec::with_capacity(transfers.len());
let mut dest_pts: Vec<Well> = Vec::with_capacity(transfers.len()); let mut dest_pts: Vec<(u8, u8)> = Vec::with_capacity(transfers.len());
for transfer in transfers { for transfer in transfers {
src_pts.push(transfer.0); src_pts.push(transfer.0);
@ -79,11 +73,11 @@ impl Default for TransferRegion {
} }
impl TransferRegion { impl TransferRegion {
pub fn get_source_wells(&self) -> Vec<Well> { pub fn get_source_wells(&self) -> Vec<(u8, u8)> {
match &self.source_region { match &self.source_region {
Region::Rect(c1, c2) => { Region::Rect(c1, c2) => {
let mut wells = Vec::<Well>::new(); let mut wells = Vec::<(u8, u8)>::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!
// We'll reassign these values (still not mutable) just in case. // We'll reassign these values (still not mutable) just in case.
@ -92,12 +86,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.row..=br.row).step_by(i8::abs(interleave_i) as usize) { for i in (ul.0..=br.0).step_by(i8::abs(interleave_i) as usize) {
for j in (ul.col..=br.col).step_by(i8::abs(interleave_j) as usize) { for j in (ul.1..=br.1).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(Well { row: i, col: j }) wells.push((i, j))
} }
} }
wells wells
@ -107,35 +101,33 @@ impl TransferRegion {
} }
} }
pub fn get_destination_wells(&self) -> Vec<Well> { pub fn get_destination_wells(&self) -> Vec<(u8, u8)> {
match &self.source_region {
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::<Well>::new(); let mut wells = Vec::<(u8, u8)>::new();
// log::debug!("GDW:");
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) {
// log::debug!("Map {:?} to {:?}", well, dest_wells);
wells.append(&mut dest_wells); wells.append(&mut dest_wells);
} }
} }
// log::debug!("GDW END.");
wells wells
} }
}
}
#[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(Well) -> Option<Vec<Well>> + '_> { pub fn calculate_map(&self) -> Box<dyn Fn((u8, u8)) -> Option<Vec<(u8, u8)>> + '_> {
// 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);
@ -143,10 +135,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: (Well, Well) = match self.source_region { let source_corners: ((u8, u8), (u8, u8)) = match self.source_region {
Region::Point(w) => (w, w), Region::Point((x, y)) => ((x, y), (x, y)),
Region::Rect(c1, c2) => (c1, c2), Region::Rect(c1, c2) => (c1, c2),
Region::Custom(_) => (Well { row: 0, col: 0 }, Well { row: 0, col: 0 }), Region::Custom(_) => ((0, 0), (0, 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,
@ -156,46 +148,45 @@ impl TransferRegion {
// Non-replicate transfers: // Non-replicate transfers:
match &self.dest_region { match &self.dest_region {
Region::Point(Well { row: x, col: y }) => { Region::Point((x, y)) => {
Box::new(move |Well { row: i, col: j }| { Box::new(move |(i, j)| {
if source_wells.contains(&Well { row: i, col: j }) { if source_wells.contains(&(i, j)) {
// Validity here already checked by self.validate() // Validity here already checked by self.validate()
Some(vec![Well { Some(vec![(
row: x + i x + i
.checked_sub(source_ul.row) .checked_sub(source_ul.0)
.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()),
col: y + j y + j
.checked_sub(source_ul.col) .checked_sub(source_ul.1)
.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 |w| { Box::new(move |(i, j)| {
let Well { row: i, col: j } = w; if source_wells.contains(&(i, j)) {
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.row.checked_sub(s_ul.row).unwrap() + 1, s_br.0.checked_sub(s_ul.0).unwrap() + 1,
s_br.col.checked_sub(s_ul.col).unwrap() + 1, s_br.1.checked_sub(s_ul.1).unwrap() + 1,
); );
let d_dims = ( let d_dims = (
d_br.row.checked_sub(d_ul.row).unwrap() + 1, d_br.0.checked_sub(d_ul.0).unwrap() + 1,
d_br.col.checked_sub(d_ul.col).unwrap() + 1, d_br.1.checked_sub(d_ul.1).unwrap() + 1,
); );
let number_used_src_wells = ( let N_s = (
// Number of used source wells // Number of used source wells
(s_dims.0 + il_source.0.unsigned_abs() - 1) (s_dims.0 + il_source.0.unsigned_abs() - 1)
.div_euclid(il_source.0.unsigned_abs()), .div_euclid(il_source.0.unsigned_abs()),
@ -206,52 +197,52 @@ impl TransferRegion {
// How many times can we replicate? // How many times can we replicate?
(1..) (1..)
.position(|n| { .position(|n| {
n * number_used_src_wells.0 * il_dest.0.unsigned_abs() n * N_s.0 * il_dest.0.unsigned_abs() - il_dest.0.unsigned_abs()
- il_dest.0.unsigned_abs()
+ 1 + 1
> d_dims.0 > d_dims.0
}) })
.unwrap() as u8, .unwrap() as u8,
(1..) (1..)
.position(|n| { .position(|n| {
n * number_used_src_wells.1 * il_dest.1.unsigned_abs() n * N_s.1 * il_dest.1.unsigned_abs() - il_dest.1.unsigned_abs()
- il_dest.1.unsigned_abs()
+ 1 + 1
> d_dims.1 > d_dims.1
}) })
.unwrap() as u8, .unwrap() as u8,
); );
let i = i let i = i
.saturating_sub(s_ul.row) .saturating_sub(s_ul.0)
.saturating_div(il_source.0.unsigned_abs()); .saturating_div(il_source.0.unsigned_abs());
let j = j let j = j
.saturating_sub(s_ul.col) .saturating_sub(s_ul.1)
.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(|Well { row: x , ..}| { .filter(|(x, _)| {
x.checked_sub(d_ul.row).unwrap() x.checked_sub(d_ul.0).unwrap()
% (number_used_src_wells.0 * il_dest.0.unsigned_abs()) // Counter along x % (N_s.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()) % (N_s.0 * il_dest.0.unsigned_abs())
}) })
.filter(|Well { col: y, .. }| { .filter(|(_, y)| {
y.checked_sub(d_ul.col).unwrap() y.checked_sub(d_ul.1).unwrap()
% (number_used_src_wells.1 * il_dest.1.unsigned_abs()) // Counter along u % (N_s.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()) % (N_s.1 * il_dest.1.unsigned_abs())
}) })
.filter(|Well { row: x, col: y }| { .filter(|(x, 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.0)
number_used_src_wells.0 * il_dest.0.unsigned_abs(), .unwrap()
) < count.0 .div_euclid(N_s.0 * il_dest.0.unsigned_abs())
&& y.checked_sub(d_ul.col).unwrap().div_euclid( < count.0
number_used_src_wells.1 * il_dest.1.unsigned_abs(), && y.checked_sub(d_ul.1)
) < count.1 .unwrap()
.div_euclid(N_s.1 * il_dest.1.unsigned_abs())
< count.1
}) })
.collect(), .collect(),
) )
@ -260,14 +251,14 @@ impl TransferRegion {
} }
}) })
} }
Region::Custom(c) => Box::new(move |Well { row: i, col: j }| { Region::Custom(c) => Box::new(move |(i, j)| {
let src = c.src.clone(); let src = c.src.clone();
let dest = c.dest.clone(); let dest = c.dest.clone();
let points: Vec<Well> = src let points: Vec<(u8, u8)> = src
.iter() .iter()
.enumerate() .enumerate()
.filter(|(_index, Well { row: x, col: y })| *x == i && *y == j) .filter(|(_index, (x, 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 +287,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.row == 0 || s1.col == 0 || s2.row == 0 || s2.col == 0 { if s1.0 == 0 || s1.1 == 0 || s2.0 == 0 || s2.1 == 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.row > source_max.0 || s2.row > source_max.0 { if s1.0 > source_max.0 || s2.0 > 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.col > source_max.1 || s2.col > source_max.1 { if s1.1 > source_max.1 || s2.1 > 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,34 +316,28 @@ impl TransferRegion {
} }
} }
fn create_dense_rectangle(c1: &Well, c2: &Well) -> Vec<Well> { fn create_dense_rectangle(c1: &(u8, u8), c2: &(u8, u8)) -> Vec<(u8, u8)> {
// 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::<Well>::new(); let mut points = Vec::<(u8, u8)>::new();
for i in c1.row..=c2.row { for i in c1.0..=c2.0 {
for j in c1.col..=c2.col { for j in c1.1..=c2.1 {
points.push(Well { row: i, col: j }); points.push((i, j));
} }
} }
points points
} }
fn standardize_rectangle(c1: &Well, c2: &Well) -> (Well, Well) { fn standardize_rectangle(c1: &(u8, u8), c2: &(u8, u8)) -> ((u8, u8), (u8, u8)) {
let upper_left_i = u8::min(c1.row, c2.row); let upper_left_i = u8::min(c1.0, c2.0);
let upper_left_j = u8::min(c1.col, c2.col); let upper_left_j = u8::min(c1.1, c2.1);
let bottom_right_i = u8::max(c1.row, c2.row); let bottom_right_i = u8::max(c1.0, c2.0);
let bottom_right_j = u8::max(c1.col, c2.col); let bottom_right_j = u8::max(c1.1, c2.1);
( (
Well { (upper_left_i, upper_left_j),
row: upper_left_i, (bottom_right_i, bottom_right_j),
col: upper_left_j,
},
Well {
row: bottom_right_i,
col: bottom_right_j,
},
) )
} }
@ -369,7 +354,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(&Well { row: i, col: j }) { if source_wells.contains(&(i, j)) {
source_string.push('x') source_string.push('x')
} else { } else {
source_string.push('.') source_string.push('.')
@ -385,7 +370,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(&Well { row: i, col: j }) { if dest_wells.contains(&(i, j)) {
dest_string.push('x') dest_string.push('x')
} else { } else {
dest_string.push('.') dest_string.push('.')
@ -399,130 +384,134 @@ impl fmt::Display for TransferRegion {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::plate::*; use wasm_bindgen_test::*;
use crate::transfer_region::*;
use crate::data::plate::*;
use crate::data::transfer_region::*;
#[test] #[test]
#[wasm_bindgen_test]
fn test_simple_transfer() { fn test_simple_transfer() {
let source = Plate::new(PlateType::Source, PlateFormat::W96); let source = Plate::new(PlateType::Source, PlateFormat::W96);
let destination = Plate::new(PlateType::Destination, PlateFormat::W384); let destination = Plate::new(PlateType::Destination, PlateFormat::W384);
let transfer1 = TransferRegion { let transfer1 = TransferRegion {
source_plate: source, source_plate: source,
source_region: Region::Rect(Well{ row: 1, col: 1}, Well { row: 1, col: 1 }), source_region: Region::Rect((1, 1), (3, 3)),
dest_plate: destination, dest_plate: destination,
dest_region: Region::Point(Well {row: 3, col: 3}), dest_region: Region::Point((3, 3)),
interleave_source: (1, 1), interleave_source: (1, 1),
interleave_dest: (1, 1), interleave_dest: (1, 1),
}; };
let transfer1_map = transfer1.calculate_map(); let transfer1_map = transfer1.calculate_map();
assert_eq!( assert_eq!(
transfer1_map(Well {row: 1, col: 1}), transfer1_map((1, 1)),
Some(vec! {Well {row: 3, col: 3}}), Some(vec! {(3,3)}),
"Failed basic shift transfer 1" "Failed basic shift transfer 1"
); );
assert_eq!( assert_eq!(
transfer1_map(Well{ row: 1, col: 2}), transfer1_map((1, 2)),
Some(vec! {Well{ row: 3, col: 4}}), Some(vec! {(3,4)}),
"Failed basic shift transfer 2" "Failed basic shift transfer 2"
); );
assert_eq!( assert_eq!(
transfer1_map(Well{ row: 2, col: 2}), transfer1_map((2, 2)),
Some(vec! {Well{ row: 4, col: 4}}), Some(vec! {(4,4)}),
"Failed basic shift transfer 3" "Failed basic shift transfer 3"
); );
let transfer2 = TransferRegion { let transfer2 = TransferRegion {
source_plate: source, source_plate: source,
source_region: Region::Rect(Well{ row: 1, col: 1}, Well{ row: 3, col: 3}), source_region: Region::Rect((1, 1), (3, 3)),
dest_plate: destination, dest_plate: destination,
dest_region: Region::Point(Well{ row: 3, col: 3}), dest_region: Region::Point((3, 3)),
interleave_source: (2, 2), interleave_source: (2, 2),
interleave_dest: (1, 1), interleave_dest: (1, 1),
}; };
let transfer2_map = transfer2.calculate_map(); let transfer2_map = transfer2.calculate_map();
assert_eq!( assert_eq!(
transfer2_map(Well{ row: 1, col: 1}), transfer2_map((1, 1)),
Some(vec! {Well{ row: 3, col: 3}}), Some(vec! {(3,3)}),
"Failed source interleave, type simple 1" "Failed source interleave, type simple 1"
); );
assert_eq!( assert_eq!(
transfer2_map(Well{ row: 1, col: 2}), transfer2_map((1, 2)),
None, None,
"Failed source interleave, type simple 2" "Failed source interleave, type simple 2"
); );
assert_eq!( assert_eq!(
transfer2_map(Well{ row: 2, col: 2}), transfer2_map((2, 2)),
None, None,
"Failed source interleave, type simple 3" "Failed source interleave, type simple 3"
); );
assert_eq!( assert_eq!(
transfer2_map(Well{ row: 3, col: 3}), transfer2_map((3, 3)),
Some(vec! {Well{ row: 4, col: 4}}), Some(vec! {(4,4)}),
"Failed source interleave, type simple 4" "Failed source interleave, type simple 4"
); );
let transfer3 = TransferRegion { let transfer3 = TransferRegion {
source_plate: source, source_plate: source,
source_region: Region::Rect(Well{ row: 1, col: 1}, Well{ row: 3, col: 3}), source_region: Region::Rect((1, 1), (3, 3)),
dest_plate: destination, dest_plate: destination,
dest_region: Region::Point(Well{ row: 3, col: 3}), dest_region: Region::Point((3, 3)),
interleave_source: (1, 1), interleave_source: (1, 1),
interleave_dest: (2, 3), interleave_dest: (2, 3),
}; };
let transfer3_map = transfer3.calculate_map(); let transfer3_map = transfer3.calculate_map();
assert_eq!( assert_eq!(
transfer3_map(Well{ row: 1, col: 1}), transfer3_map((1, 1)),
Some(vec! {Well{ row: 3, col: 3}}), Some(vec! {(3,3)}),
"Failed destination interleave, type simple 1" "Failed destination interleave, type simple 1"
); );
assert_eq!( assert_eq!(
transfer3_map(Well{ row: 2, col: 1}), transfer3_map((2, 1)),
Some(vec! {Well{ row: 5, col: 3}}), Some(vec! {(5,3)}),
"Failed destination interleave, type simple 2" "Failed destination interleave, type simple 2"
); );
assert_eq!( assert_eq!(
transfer3_map(Well{ row: 1, col: 2}), transfer3_map((1, 2)),
Some(vec! {Well{ row: 3, col: 6}}), Some(vec! {(3,6)}),
"Failed destination interleave, type simple 3" "Failed destination interleave, type simple 3"
); );
assert_eq!( assert_eq!(
transfer3_map(Well{ row: 2, col: 2}), transfer3_map((2, 2)),
Some(vec! {Well{ row: 5, col: 6}}), Some(vec! {(5,6)}),
"Failed destination interleave, type simple 4" "Failed destination interleave, type simple 4"
); );
} }
#[test] #[test]
#[wasm_bindgen_test]
fn test_replicate_transfer() { fn test_replicate_transfer() {
let source = Plate::new(PlateType::Source, PlateFormat::W96); let source = Plate::new(PlateType::Source, PlateFormat::W96);
let destination = Plate::new(PlateType::Destination, PlateFormat::W384); let destination = Plate::new(PlateType::Destination, PlateFormat::W384);
let transfer1 = TransferRegion { let transfer1 = TransferRegion {
source_plate: source, source_plate: source,
source_region: Region::Rect(Well{ row: 1, col: 1}, Well{ row: 2, col: 2}), source_region: Region::Rect((1, 1), (2, 2)),
dest_plate: destination, dest_plate: destination,
dest_region: Region::Rect(Well{ row: 2, col: 2}, Well{row:11, col:11}), dest_region: Region::Rect((2, 2), (11, 11)),
interleave_source: (1, 1), interleave_source: (1, 1),
interleave_dest: (3, 3), interleave_dest: (3, 3),
}; };
let transfer1_map = transfer1.calculate_map(); let transfer1_map = transfer1.calculate_map();
assert_eq!( assert_eq!(
transfer1_map(Well{ row: 1, col: 1}), transfer1_map((1, 1)),
Some(vec! {Well{ row: 2, col: 2}, Well{ row: 2, col: 8}, Well{ row: 8, col: 2}, Well{ row: 8, col: 8}}), Some(vec! {(2, 2), (2, 8), (8, 2), (8, 8)}),
"Failed type replicate 1" "Failed type replicate 1"
); );
assert_eq!( assert_eq!(
transfer1_map(Well{ row: 2, col: 1}), transfer1_map((2, 1)),
Some(vec! {Well{ row: 5, col: 2}, Well{ row: 5, col: 8}, Well{ row: 11, col: 2}, Well{ row: 11, col: 8}}), Some(vec! {(5, 2), (5, 8), (11, 2), (11, 8)}),
"Failed type replicate 1" "Failed type replicate 1"
); );
let transfer2 = TransferRegion { let transfer2 = TransferRegion {
source_plate: Plate::new(PlateType::Source, PlateFormat::W384), source_plate: Plate::new(PlateType::Source, PlateFormat::W384),
dest_plate: Plate::new(PlateType::Destination, PlateFormat::W384), dest_plate: Plate::new(PlateType::Destination, PlateFormat::W384),
source_region: Region::Rect(Well{ row: 1, col: 1}, Well{ row: 2, col: 3}), source_region: Region::Rect((1, 1), (2, 3)),
dest_region: Region::Rect(Well{ row: 2, col: 2}, Well{ row: 11, col: 16}), dest_region: Region::Rect((2, 2), (11, 16)),
interleave_source: (1, 1), interleave_source: (1, 1),
interleave_dest: (2, 2), interleave_dest: (2, 2),
}; };
@ -530,69 +519,71 @@ mod tests {
let transfer2_dest = transfer2.get_destination_wells(); let transfer2_dest = transfer2.get_destination_wells();
assert_eq!( assert_eq!(
transfer2_source, transfer2_source,
vec![Well{ row: 1, col: 1}, Well{ row: 1, col: 2}, Well{ row: 1, col: 3}, Well{ row: 2, col: 1}, Well{ row: 2, col: 2}, Well{ row: 2, col: 3}], vec![(1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3)],
"Failed type replicate 2 source" "Failed type replicate 2 source"
); );
assert_eq!( assert_eq!(
transfer2_dest, transfer2_dest,
vec![ vec![
Well{ row: 2, col: 2}, (2, 2),
Well{ row: 2, col: 8}, (2, 8),
Well{ row: 6, col: 2}, (6, 2),
Well{ row: 6, col: 8}, (6, 8),
Well{ row: 2, col: 4}, (2, 4),
Well{ row: 2, col: 10}, (2, 10),
Well{ row: 6, col: 4}, (6, 4),
Well{ row: 6, col: 10}, (6, 10),
Well{ row: 2, col: 6}, (2, 6),
Well{ row: 2, col: 12}, (2, 12),
Well{ row: 6, col: 6}, (6, 6),
Well{ row: 6, col: 12}, (6, 12),
Well{ row: 4, col: 2}, (4, 2),
Well{ row: 4, col: 8}, (4, 8),
Well{ row: 8, col: 2}, (8, 2),
Well{ row: 8, col: 8}, (8, 8),
Well{ row: 4, col: 4}, (4, 4),
Well{ row: 4, col: 10}, (4, 10),
Well{ row: 8, col: 4}, (8, 4),
Well{ row: 8, col: 10}, (8, 10),
Well{ row: 4, col: 6}, (4, 6),
Well{ row: 4, col: 12}, (4, 12),
Well{ row: 8, col: 6}, (8, 6),
Well{ row: 8, col: 12} (8, 12)
], ],
"Failed type replicate 2 destination" "Failed type replicate 2 destination"
); );
} }
#[test] #[test]
#[wasm_bindgen_test]
fn test_pooling_transfer() { fn test_pooling_transfer() {
use std::collections::HashSet;
let transfer1 = TransferRegion { let transfer1 = TransferRegion {
source_plate: Plate::new(PlateType::Source, PlateFormat::W384), source_plate: Plate::new(PlateType::Source, PlateFormat::W384),
dest_plate: Plate::new(PlateType::Destination, PlateFormat::W384), dest_plate: Plate::new(PlateType::Destination, PlateFormat::W384),
source_region: Region::Rect(Well{ row: 1, col: 4}, Well{ row: 3, col: 7}), source_region: Region::Rect((1, 4), (3, 7)),
dest_region: Region::Point(Well{ row: 1, col: 9}), dest_region: Region::Point((1, 9)),
interleave_source: (1, 1), interleave_source: (1, 1),
interleave_dest: (0, 2), interleave_dest: (0, 2),
}; };
//let transfer1_source = transfer1.get_source_wells(); //let transfer1_source = transfer1.get_source_wells();
let transfer1_dest: HashSet<Well> = transfer1.get_destination_wells().into_iter().collect(); let mut transfer1_dest = transfer1.get_destination_wells();
transfer1_dest.sort();
transfer1_dest.dedup(); // Makes our check easier, otherwise we have repeated wells
let transfer1_map = transfer1.calculate_map(); let transfer1_map = transfer1.calculate_map();
// Skipping source check---it's just 12 wells. // Skipping source check---it's just 12 wells.
assert_eq!( assert_eq!(
transfer1_dest, transfer1_dest,
vec![Well{ row: 1, col: 9}, Well{ row: 1, col: 11}, Well{ row: 1, col: 13}, Well{ row: 1, col: 15}].into_iter().collect(), vec![(1, 9), (1, 11), (1, 13), (1, 15)],
"Failed type pool 1 dest" "Failed type pool 1 dest"
); );
assert_eq!( assert_eq!(
transfer1_map(Well{ row: 2, col: 6}), transfer1_map((2, 6)),
Some(vec![Well{ row: 1, col: 13}]), Some(vec![(1, 13)]),
"Failed type pool 1 map 1" "Failed type pool 1 map 1"
); );
assert_eq!( assert_eq!(
transfer1_map(Well{ row: 3, col: 7}), transfer1_map((3, 7)),
Some(vec![Well{ row: 1, col: 15}]), Some(vec![(1, 15)]),
"Failed type pool 1 map 2" "Failed type pool 1 map 2"
); );
} }

37
src/lib.rs Normal file
View File

@ -0,0 +1,37 @@
#![allow(non_snake_case)]
mod components;
mod data;
use components::main_window::MainWindow;
use yew::prelude::*;
#[cfg(debug_assertions)]
use data::*;
#[function_component]
pub fn App() -> Html {
html! {
<MainWindow />
}
}
#[cfg(debug_assertions)]
pub fn plate_test() {
let source = plate::Plate::new(plate::PlateType::Source, plate::PlateFormat::W96);
let destination = plate::Plate::new(plate::PlateType::Destination, plate::PlateFormat::W384);
let transfer = transfer_region::TransferRegion {
source_plate: source,
source_region: transfer_region::Region::Rect((1, 1), (2, 2)),
dest_plate: destination,
dest_region: transfer_region::Region::Rect((2, 2), (11, 11)),
interleave_source: (1, 1),
interleave_dest: (3, 3),
};
println!("{}", transfer);
let sws = transfer.get_source_wells();
let m = transfer.calculate_map();
for w in sws {
println!("{:?} -> {:?}", w, m(w));
}
}

View File

@ -1,4 +1,4 @@
use plate_tool_web::App; use plate_tool::App;
fn main() { fn main() {
wasm_logger::init(wasm_logger::Config::default()); wasm_logger::init(wasm_logger::Config::default());