Compare commits

..

No commits in common. "c348d111ecbb388e2256843e7ee21061c1a3f9bd" and "6aee3ded2cad7f329ced1d77f2790bfd960425af" have entirely different histories.

18 changed files with 52 additions and 8383 deletions

17
Cargo.lock generated
View File

@ -34,12 +34,6 @@ dependencies = [
"syn 2.0.16", "syn 2.0.16",
] ]
[[package]]
name = "atomic"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.1.0" version = "1.1.0"
@ -561,11 +555,9 @@ name = "plate-tool"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"csv", "csv",
"getrandom",
"js-sys", "js-sys",
"lazy_static", "lazy_static",
"log", "log",
"rand",
"regex", "regex",
"serde", "serde",
"serde_json", "serde_json",
@ -891,11 +883,10 @@ checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.6.1" version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2"
dependencies = [ dependencies = [
"atomic",
"getrandom", "getrandom",
"rand", "rand",
"serde", "serde",
@ -905,9 +896,9 @@ dependencies = [
[[package]] [[package]]
name = "uuid-macro-internal" name = "uuid-macro-internal"
version = "1.6.1" version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f49e7f3f3db8040a100710a11932239fd30697115e2ba4107080d8252939845e" checksum = "3f67b459f42af2e6e1ee213cb9da4dbd022d3320788c3fb3e1b893093f1e45da"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

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

View File

@ -54,7 +54,6 @@ To add a new plate, click the "New Plate" button:
### Importing and Exporting ### Importing and Exporting
#### Export as CSV
Exporting the transfers we have created to a CSV format is the primary (if not sole) usage of Plate Tool. Exporting the transfers we have created to a CSV format is the primary (if not sole) usage of Plate Tool.
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.
@ -62,29 +61,20 @@ To add a new plate, click the "New Plate" button:
You will be reminded that this is a one-way export (see JSON export/import below), 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. and then prompted by your browser to select a location for your file.
#### Export as JSON (Saving Your Work) Currently, it is not possible to import from nor 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.
Mouse over the "File" tab, then "Export" as above, then alternatively select "Export as JSON". Mouse over the "File" tab, then "Export" as above, then alternatively select "Export as JSON".
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 (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".
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)
your file; it will then be processed and loaded. 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)

File diff suppressed because one or more lines are too long

View File

@ -1,25 +0,0 @@
function copy_screenshot(el) {
html2canvas(el).then((canvas) => {
console.log("Copying image to clipboard");
canvas.toBlob((b) => {
try {
navigator.clipboard.write([
new ClipboardItem({
'image/png': b
})
]);
} catch (e) {
console.error("Failed to copy!");
}
});
});
}
function copy_screenshot_dest() {
let plate = document.getElementsByClassName("dest_plate")[0];
copy_screenshot(plate);
}
function copy_screenshot_src() {
let plate = document.getElementsByClassName("source_plate")[0];
copy_screenshot(plate);
}

View File

@ -12,8 +12,6 @@ div.upper_menu {
visibility: inherit; visibility: inherit;
display: flex;
div.dropdown { div.dropdown {
margin-right: 2px; margin-right: 2px;

View File

@ -4,8 +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">
<script data-trunk src="assets/js/screenshot_utility.js"></script>
<script data-trunk src="assets/js/html2canvas.js"></script>
<title>Plate Tool</title> <title>Plate Tool</title>
</head> </head>
</html> </html>

View File

@ -1,27 +1,18 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
use std::collections::HashSet;
use js_sys::Array; use js_sys::Array;
use lazy_static::lazy_static;
use regex::Regex;
use wasm_bindgen::{prelude::*, JsCast, JsValue}; use wasm_bindgen::{prelude::*, JsCast, JsValue};
use web_sys::{ use web_sys::{Blob, HtmlAnchorElement, HtmlDialogElement, HtmlFormElement, HtmlInputElement, Url};
Blob, HtmlAnchorElement, HtmlButtonElement, HtmlDialogElement, HtmlFormElement,
HtmlInputElement, HtmlOptionElement, HtmlSelectElement, Url,
};
use yew::prelude::*; use yew::prelude::*;
use yewdux::prelude::*; use yewdux::prelude::*;
use super::new_plate_dialog::NewPlateDialog; use super::new_plate_dialog::NewPlateDialog;
use super::plates::plate_container::PlateContainer; use super::plates::plate_container::PlateContainer;
use super::states::{CurrentTransfer, MainState}; use super::states::{CurrentTransfer, MainState};
use super::transfer_menu::{letters_to_num, RegionDisplay, TransferMenu}; use super::transfer_menu::TransferMenu;
use super::tree::Tree; use super::tree::Tree;
use crate::data::csv::state_to_csv; use crate::data::csv::state_to_csv;
use crate::data::plate_instances::PlateInstance; use crate::data::plate_instances::PlateInstance;
use crate::data::transfer::Transfer;
use crate::data::transfer_region::{Region, TransferRegion};
#[function_component] #[function_component]
pub fn MainWindow() -> Html { pub fn MainWindow() -> Html {
@ -49,15 +40,6 @@ pub fn MainWindow() -> Html {
}); });
} }
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_is_open = use_state_eq(|| false);
let new_plate_dialog_callback = { let new_plate_dialog_callback = {
let new_plate_dialog_is_open = new_plate_dialog_is_open.clone(); let new_plate_dialog_is_open = new_plate_dialog_is_open.clone();
@ -118,7 +100,6 @@ pub fn MainWindow() -> Html {
}; };
let import_json_button_callback = { let import_json_button_callback = {
let main_dispatch = main_dispatch.clone();
Callback::from(move |_| { Callback::from(move |_| {
let window = web_sys::window().unwrap(); let window = web_sys::window().unwrap();
let document = window.document().unwrap(); let document = window.document().unwrap();
@ -196,274 +177,6 @@ pub fn MainWindow() -> Html {
}) })
}; };
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! { html! {
<> <>
<div class="upper_menu"> <div class="upper_menu">
@ -477,22 +190,7 @@ pub fn MainWindow() -> Html {
<button onclick={export_json_button_callback}>{"Export as JSON"}</button> <button onclick={export_json_button_callback}>{"Export as JSON"}</button>
</div> </div>
</div> </div>
<div class="dropdown-sub"> <button onclick={import_json_button_callback}>{"Import"}</button>
<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> </div>
<div class="main_container"> <div class="main_container">

View File

@ -99,8 +99,8 @@ pub fn NewPlateDialog(props: &NewPlateDialogProps) -> Html {
<option value="12">{"12"}</option> <option value="12">{"12"}</option>
<option value="24">{"24"}</option> <option value="24">{"24"}</option>
<option value="48">{"48"}</option> <option value="48">{"48"}</option>
<option value="96" selected={true}>{"96"}</option> <option value="96">{"96"}</option>
<option value="384">{"384"}</option> <option value="384" selected={true}>{"384"}</option>
<option value="1536">{"1536"}</option> <option value="1536">{"1536"}</option>
<option value="3456">{"3456"}</option> <option value="3456">{"3456"}</option>
</select> </select>

View File

@ -34,21 +34,12 @@ pub fn DestinationPlate(props: &DestinationPlateProps) -> Html {
let (pt1, pt2) = match ct_state.transfer.transfer_region.dest_region { let (pt1, pt2) = match ct_state.transfer.transfer_region.dest_region {
Region::Point((x, y)) => ((x, y), (x, y)), Region::Point((x, y)) => ((x, y), (x, y)),
Region::Rect(c1, c2) => (c1, c2), Region::Rect(c1, c2) => (c1, c2),
Region::Custom(_) => ((0,0), (0,0)),
}; };
m_start_handle.set(Some(pt1)); m_start_handle.set(Some(pt1));
m_end_handle.set(Some(pt2)); m_end_handle.set(Some(pt2));
} }
let destination_wells = ct_state.transfer.transfer_region.get_destination_wells(); 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 mouse_callback = {
let m_start_handle = m_start_handle.clone(); let m_start_handle = m_start_handle.clone();
let m_end_handle = m_end_handle.clone(); let m_end_handle = m_end_handle.clone();
@ -107,11 +98,6 @@ pub fn DestinationPlate(props: &DestinationPlateProps) -> Html {
let mouseleave_callback = Callback::clone(&mouseup_callback); let mouseleave_callback = Callback::clone(&mouseup_callback);
let screenshot_callback = Callback::from(|_| {
let _ = js_sys::eval("copy_screenshot_dest()");
});
let column_header = { let column_header = {
let headers = (1..=props.destination_plate.plate.size().1) let headers = (1..=props.destination_plate.plate.size().1)
.map(|j| { .map(|j| {
@ -128,10 +114,10 @@ pub fn DestinationPlate(props: &DestinationPlateProps) -> Html {
<DestPlateCell i={i} j={j} <DestPlateCell i={i} j={j}
selected={super::source_plate::in_rect(*m_start_handle.clone(), *m_end_handle.clone(), (i,j))} selected={super::source_plate::in_rect(*m_start_handle.clone(), *m_end_handle.clone(), (i,j))}
mouse={mouse_callback.clone()} mouse={mouse_callback.clone()}
in_transfer={destination_wells.contains(&(i,j)) && main_state.preferences.in_transfer_hashes} in_transfer={destination_wells.contains(&(i,j))}
color={transfer_map.get(&(i,j)) color={transfer_map.get(&(i,j))
.and_then(|t| t.last()) .and_then(|t| t.last())
.map(|t| PALETTE.get_ordered(t.get_uuid(), &ordered_ids)) .map(|t| PALETTE.get_uuid(t.get_uuid()))
} }
cell_height={props.cell_height} cell_height={props.cell_height}
title={transfer_map.get(&(i,j)).map(|transfers| format!("Used by: {}", transfers.iter().map(|t| t.name.clone()) title={transfer_map.get(&(i,j)).map(|transfers| format!("Used by: {}", transfers.iter().map(|t| t.name.clone())
@ -148,8 +134,7 @@ pub fn DestinationPlate(props: &DestinationPlateProps) -> Html {
.collect::<Html>(); .collect::<Html>();
html! { html! {
<div ondblclick={screenshot_callback} <div class={classes!{"dest_plate",
class={classes!{"dest_plate",
"W".to_owned()+&props.source_plate.plate.plate_format.to_string()}}> "W".to_owned()+&props.source_plate.plate.plate_format.to_string()}}>
<table <table
onmouseup={move |e| { onmouseup={move |e| {

View File

@ -34,7 +34,6 @@ pub fn SourcePlate(props: &SourcePlateProps) -> Html {
let (pt1, pt2) = match ct_state.transfer.transfer_region.source_region { let (pt1, pt2) = match ct_state.transfer.transfer_region.source_region {
Region::Point((x, y)) => ((x, y), (x, y)), Region::Point((x, y)) => ((x, y), (x, y)),
Region::Rect(c1, c2) => (c1, c2), Region::Rect(c1, c2) => (c1, c2),
Region::Custom(_) => ((0,0), (0,0)),
}; };
m_start_handle.set(Some(pt1)); m_start_handle.set(Some(pt1));
m_end_handle.set(Some(pt2)); m_end_handle.set(Some(pt2));
@ -61,14 +60,6 @@ pub fn SourcePlate(props: &SourcePlateProps) -> Html {
let source_wells = ct_state.transfer.transfer_region.get_source_wells(); 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 mouse_callback = {
let m_start_handle = m_start_handle.clone(); let m_start_handle = m_start_handle.clone();
let m_end_handle = m_end_handle.clone(); let m_end_handle = m_end_handle.clone();
@ -108,10 +99,6 @@ pub fn SourcePlate(props: &SourcePlateProps) -> Html {
let mouseleave_callback = Callback::clone(&mouseup_callback); let mouseleave_callback = Callback::clone(&mouseup_callback);
let screenshot_callback = Callback::from(|_| {
let _ = js_sys::eval("copy_screenshot_src()");
});
let column_header = { let column_header = {
let headers = (1..=props.source_plate.plate.size().1) let headers = (1..=props.source_plate.plate.size().1)
.map(|j| { .map(|j| {
@ -131,10 +118,10 @@ pub fn SourcePlate(props: &SourcePlateProps) -> Html {
<SourcePlateCell i={i} j={j} <SourcePlateCell i={i} j={j}
selected={in_rect(*m_start_handle.clone(), *m_end_handle.clone(), (i,j))} selected={in_rect(*m_start_handle.clone(), *m_end_handle.clone(), (i,j))}
mouse={mouse_callback.clone()} mouse={mouse_callback.clone()}
in_transfer={source_wells.contains(&(i,j)) && main_state.preferences.in_transfer_hashes} in_transfer={source_wells.contains(&(i,j))}
color={transfer_map.get(&(i,j)) color={transfer_map.get(&(i,j))
.and_then(|t| t.last()) .and_then(|t| t.last())
.map(|t| PALETTE.get_ordered(t.get_uuid(), &ordered_ids)) .map(|t| PALETTE.get_uuid(t.get_uuid()))
} }
cell_height={props.cell_height} cell_height={props.cell_height}
title={transfer_map.get(&(i,j)).map(|transfers| format!("Used by: {}", transfers.iter().map(|t| t.name.clone()) title={transfer_map.get(&(i,j)).map(|transfers| format!("Used by: {}", transfers.iter().map(|t| t.name.clone())
@ -152,8 +139,7 @@ pub fn SourcePlate(props: &SourcePlateProps) -> Html {
.collect::<Html>(); .collect::<Html>();
html! { html! {
<div ondblclick={screenshot_callback} <div class={classes!{"source_plate",
class={classes!{"source_plate",
"W".to_owned()+&props.source_plate.plate.plate_format.to_string()}}> "W".to_owned()+&props.source_plate.plate.plate_format.to_string()}}>
<table <table
onmouseup={move |e| { onmouseup={move |e| {

View File

@ -2,10 +2,6 @@
// 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],
@ -35,23 +31,9 @@ impl ColorPalette {
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)
} }
// pub fn get_uuid(&self, t: uuid::Uuid) -> [f64; 3] { pub fn get_uuid(&self, t: uuid::Uuid) -> [f64; 3] {
// // self.get(t.as_u128() as f64 / (u128::MAX) as f64) log::debug!("{}", t.as_u128() as f64 / (u128::MAX) as f64);
// let mut r = SmallRng::seed_from_u64(t.as_u128() as u64); self.get(t.as_u128() as f64 / (u128::MAX) as f64)
// self.get(r.gen_range(0.0..1.0f64))
// }
pub fn get_ordered(&self, t: uuid::Uuid, ordered_uuids: &Vec<uuid::Uuid>)
-> [f64; 3] {
let index = ordered_uuids.iter().position(|&x| x == t).expect("uuid must be in list of uuids") + 1;
return self.get(Self::space_evenly(index))
}
fn space_evenly(x: usize) -> f64 {
let e: usize = (x.ilog2() + 1) as usize;
let d: usize = (2usize.pow(e as u32)) as usize;
let n: usize = (2*x + 1) % d;
return (n as f64) / (d as f64);
} }
} }
@ -72,5 +54,4 @@ impl Palettes {
c: [0.100, 0.500, 0.360], c: [0.100, 0.500, 0.360],
d: [0.000, 0.000, 0.650], d: [0.000, 0.000, 0.650],
}; };
} }

View File

@ -13,17 +13,6 @@ pub struct CurrentTransfer {
pub transfer: Transfer, pub transfer: Transfer,
} }
#[derive(PartialEq, Clone, Copy, Serialize, Deserialize)]
pub struct Preferences {
pub in_transfer_hashes: bool,
}
impl Default for Preferences {
fn default() -> Self {
Self { in_transfer_hashes: true }
}
}
#[derive(Default, PartialEq, Clone, Serialize, Deserialize)] #[derive(Default, PartialEq, Clone, Serialize, Deserialize)]
#[non_exhaustive] #[non_exhaustive]
pub struct MainState { pub struct MainState {
@ -33,9 +22,6 @@ pub struct MainState {
pub selected_source_plate: Uuid, pub selected_source_plate: Uuid,
pub selected_dest_plate: Uuid, pub selected_dest_plate: Uuid,
pub selected_transfer: Uuid, pub selected_transfer: Uuid,
#[serde(default)]
pub preferences: Preferences,
} }
impl Store for MainState { impl Store for MainState {

View File

@ -39,9 +39,6 @@ pub fn TransferMenu() -> Html {
let ct_dispatch = ct_dispatch.clone(); let ct_dispatch = ct_dispatch.clone();
Callback::from(move |e: 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 target: Option<EventTarget> = e.target();
let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok()); let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
if let Some(input) = input { if let Some(input) = input {
@ -195,7 +192,7 @@ pub fn TransferMenu() -> Html {
let new_transfer = Transfer::new( let new_transfer = Transfer::new(
spi.clone(), spi.clone(),
dpi.clone(), dpi.clone(),
ct_state.transfer.transfer_region.clone(), ct_state.transfer.transfer_region,
ct_state.transfer.name.clone(), ct_state.transfer.name.clone(),
); );
main_dispatch.reduce_mut(|state| { main_dispatch.reduce_mut(|state| {
@ -253,8 +250,6 @@ pub fn TransferMenu() -> Html {
onchange={on_name_change} onchange={on_name_change}
value={ct_state.transfer.name.clone()}/> value={ct_state.transfer.name.clone()}/>
</div> </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> <div>
<label for="src_region"><h3>{"Source Region:"}</h3></label> <label for="src_region"><h3>{"Source Region:"}</h3></label>
<input type="text" name="src_region" <input type="text" name="src_region"
@ -296,7 +291,6 @@ pub fn TransferMenu() -> Html {
onchange={on_volume_change} onchange={on_volume_change}
value={ct_state.transfer.volume.to_string()}/> value={ct_state.transfer.volume.to_string()}/>
</div> </div>
}
<div id="controls"> <div id="controls">
<input type="button" name="new_transfer" onclick={new_transfer_button_callback} <input type="button" name="new_transfer" onclick={new_transfer_button_callback}
value={"New"} /> value={"New"} />
@ -350,37 +344,6 @@ impl TryFrom<String> for RegionDisplay {
} }
} }
} }
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 { impl From<&Region> for RegionDisplay {
fn from(value: &Region) -> Self { fn from(value: &Region) -> Self {
match *value { match *value {
@ -390,7 +353,6 @@ impl From<&Region> for RegionDisplay {
Region::Rect(c1, c2) => RegionDisplay::try_from((c1.0, c1.1, c2.0, c2.1)) Region::Rect(c1, c2) => RegionDisplay::try_from((c1.0, c1.1, c2.0, c2.1))
.ok() .ok()
.unwrap(), .unwrap(),
Region::Custom(_) => RegionDisplay { text: "CUSTOM".to_string(), col_start: 0, row_start: 0, col_end: 0, row_end: 0 }
} }
} }
} }
@ -423,10 +385,9 @@ impl TryFrom<(u8, u8, u8, u8)> for RegionDisplay {
}) })
} }
} }
pub fn letters_to_num(letters: &str) -> Option<u8> { fn letters_to_num(letters: &str) -> Option<u8> {
let mut num: u8 = 0; let mut num: u8 = 0;
for (i, letter) in letters.to_ascii_uppercase().chars().rev().enumerate() { for (i, letter) in letters.chars().rev().enumerate() {
log::debug!("{}, {}", i, letter);
let n = letter as u8; let n = letter as u8;
if !(65..=90).contains(&n) { if !(65..=90).contains(&n) {
return None; return None;

View File

@ -2,23 +2,23 @@ use crate::components::states::MainState;
use crate::components::transfer_menu::num_to_letters; use crate::components::transfer_menu::num_to_letters;
use crate::data::transfer::Transfer; use crate::data::transfer::Transfer;
use serde::{Serialize, Deserialize}; use serde::Serialize;
use std::error::Error; use std::error::Error;
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Debug)]
pub struct TransferRecord { struct TransferRecord {
#[serde(rename = "Source Plate")] #[serde(rename = "Source Plate Barcode")]
pub source_plate: String, source_plate: String,
#[serde(rename = "Source Well")] #[serde(rename = "Source Well")]
pub source_well: String, source_well: String,
#[serde(rename = "Dest Plate")] #[serde(rename = "Destination Plate Barcode")]
pub destination_plate: String, destination_plate: String,
#[serde(rename = "Destination Well")] #[serde(rename = "Destination Well")]
pub destination_well: String, destination_well: String,
#[serde(rename = "Transfer Volume")] #[serde(rename = "Volume")]
pub volume: f32, volume: f32,
#[serde(rename = "Concentration")] #[serde(rename = "Concentration")]
pub concentration: Option<f32>, concentration: Option<f32>,
} }
pub fn state_to_csv(state: &MainState) -> Result<String, Box<dyn Error>> { pub fn state_to_csv(state: &MainState) -> Result<String, Box<dyn Error>> {

View File

@ -5,8 +5,6 @@ use uuid::Uuid;
#[derive(PartialEq, Clone, Serialize, Deserialize)] #[derive(PartialEq, Clone, Serialize, Deserialize)]
pub struct PlateInstance { pub struct PlateInstance {
pub plate: Plate, pub plate: Plate,
#[serde(rename = "id_v7")]
#[serde(default = "Uuid::now_v7")]
id: Uuid, id: Uuid,
pub name: String, pub name: String,
} }
@ -18,7 +16,7 @@ impl PlateInstance {
plate_type: sort, plate_type: sort,
plate_format: format, plate_format: format,
}, },
id: Uuid::now_v7(), id: Uuid::new_v4(),
name, name,
} }
} }
@ -36,7 +34,7 @@ impl From<Plate> for PlateInstance {
fn from(value: Plate) -> Self { fn from(value: Plate) -> Self {
PlateInstance { PlateInstance {
plate: value, plate: value,
id: Uuid::now_v7(), id: Uuid::new_v4(),
name: "New Plate".to_string(), name: "New Plate".to_string(),
} }
} }

View File

@ -10,9 +10,7 @@ pub struct Transfer {
pub source_id: Uuid, pub source_id: Uuid,
pub dest_id: Uuid, pub dest_id: Uuid,
pub name: String, pub name: String,
#[serde(rename = "id_v7")] id: Uuid,
#[serde(default = "Uuid::now_v7")]
pub id: Uuid,
pub transfer_region: TransferRegion, pub transfer_region: TransferRegion,
#[serde(default = "default_volume")] #[serde(default = "default_volume")]
pub volume: f32, pub volume: f32,
@ -46,7 +44,7 @@ impl Transfer {
source_id: source.get_uuid(), source_id: source.get_uuid(),
dest_id: dest.get_uuid(), dest_id: dest.get_uuid(),
name, name,
id: Uuid::now_v7(), id: Uuid::new_v4(),
transfer_region: tr, transfer_region: tr,
volume: 2.5, volume: 2.5,
} }

View File

@ -1,20 +1,11 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::components::transfer_menu::RegionDisplay;
use super::plate::Plate; use super::plate::Plate;
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] #[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Debug)]
pub struct CustomRegion {
src: Vec<(u8, u8)>,
dest: Vec<(u8, u8)>,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
pub enum Region { pub enum Region {
Rect((u8, u8), (u8, u8)), Rect((u8, u8), (u8, u8)),
Point((u8, u8)), Point((u8, u8)),
Custom(CustomRegion),
} }
impl Default for Region { impl Default for Region {
fn default() -> Self { fn default() -> Self {
@ -32,24 +23,8 @@ impl TryFrom<Region> for ((u8, u8), (u8, u8)) {
} }
} }
} }
impl Region {
pub fn new_custom(transfers: &Vec<((u8, u8), (u8, u8))>) -> Self {
let mut src_pts: Vec<(u8, u8)> = Vec::with_capacity(transfers.len());
let mut dest_pts: Vec<(u8, u8)> = Vec::with_capacity(transfers.len());
for transfer in transfers { #[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Debug)]
src_pts.push(transfer.0);
dest_pts.push(transfer.1);
}
Region::Custom(CustomRegion {
src: src_pts,
dest: dest_pts,
})
}
}
#[derive(PartialEq, Eq, Clone, Serialize, Deserialize, Debug)]
pub struct TransferRegion { pub struct TransferRegion {
pub source_plate: Plate, pub source_plate: Plate,
pub source_region: Region, // Even if it is just a point, we don't want corners. pub source_region: Region, // Even if it is just a point, we don't want corners.
@ -74,7 +49,7 @@ impl Default for TransferRegion {
impl TransferRegion { impl TransferRegion {
pub fn get_source_wells(&self) -> Vec<(u8, u8)> { 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::<(u8, u8)>::new(); let mut wells = Vec::<(u8, u8)>::new();
let (ul, br) = standardize_rectangle(&c1, &c2); let (ul, br) = standardize_rectangle(&c1, &c2);
@ -96,8 +71,7 @@ impl TransferRegion {
} }
wells wells
} }
Region::Point(p) => vec![*p], Region::Point(p) => vec![p],
Region::Custom(c) => c.src.clone(),
} }
} }
@ -138,7 +112,6 @@ impl TransferRegion {
let source_corners: ((u8, u8), (u8, u8)) = match self.source_region { let source_corners: ((u8, u8), (u8, u8)) = match self.source_region {
Region::Point((x, y)) => ((x, y), (x, y)), Region::Point((x, y)) => ((x, y), (x, y)),
Region::Rect(c1, c2) => (c1, c2), Region::Rect(c1, c2) => (c1, c2),
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,
@ -147,7 +120,7 @@ impl TransferRegion {
// and simple then we *will* have injectivity. // and simple then we *will* have injectivity.
// Non-replicate transfers: // Non-replicate transfers:
match &self.dest_region { match self.dest_region {
Region::Point((x, y)) => { Region::Point((x, y)) => {
Box::new(move |(i, j)| { Box::new(move |(i, j)| {
if source_wells.contains(&(i, j)) { if source_wells.contains(&(i, j)) {
@ -251,22 +224,6 @@ impl TransferRegion {
} }
}) })
} }
Region::Custom(c) => Box::new(move |(i, j)| {
let src = c.src.clone();
let dest = c.dest.clone();
let points: Vec<(u8, u8)> = src
.iter()
.enumerate()
.filter(|(_index, (x, y))| *x == i && *y == j)
.map(|(index, _)| dest[index])
.collect();
if points.is_empty() {
None
} else {
Some(points)
}
}),
} }
} }
@ -300,7 +257,6 @@ impl TransferRegion {
return Err("Source region is out-of-bounds! (Too wide)"); return Err("Source region is out-of-bounds! (Too wide)");
} }
} }
Region::Custom(_) => return Ok(()),
} }
if il_source.0 == 0 || il_dest.1 == 0 { if il_source.0 == 0 || il_dest.1 == 0 {