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

View File

@ -1,6 +1,6 @@
[package]
name = "plate-tool"
version = "0.2.0"
version = "0.1.0"
edition = "2021"
# 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"
web-sys = { version = "0.3", features = ["FormData", "HtmlFormElement",
"HtmlDialogElement", "Blob", "Url", "Window",
"HtmlAnchorElement", "ReadableStream", "HtmlSelectElement", "HtmlOptionElement", "HtmlButtonElement",
"HtmlAnchorElement", "ReadableStream",
"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"] }
uuid = { version = "1.3", features = ["v4", "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"

View File

@ -54,7 +54,6 @@ To add a new plate, click the "New Plate" button:
### 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.
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.
@ -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),
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 export to a format produced by other similar software.
Currently, it is not possible to import from nor export to a format produced by other similar software.
However, you might reasonably want to save a copy of your work
either as a backup or to share.
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.
(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
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)
your file; it will then be processed and loaded.
Keep in mind that this will overwrite any work you currently have open,
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!
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)

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;
display: flex;
div.dropdown {
margin-right: 2px;

View File

@ -4,8 +4,6 @@
<meta charset="utf-8" />
<link data-trunk rel="scss" href="assets/scss/index.scss">
<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>
</head>
</html>

View File

@ -1,27 +1,18 @@
#![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 web_sys::{Blob, HtmlAnchorElement, HtmlDialogElement, HtmlFormElement, HtmlInputElement, 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::transfer_menu::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 {
@ -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_callback = {
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 main_dispatch = main_dispatch.clone();
Callback::from(move |_| {
let window = web_sys::window().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! {
<>
<div class="upper_menu">
@ -477,22 +190,7 @@ pub fn MainWindow() -> Html {
<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>
<button onclick={import_json_button_callback}>{"Import"}</button>
</div>
</div>
<div class="main_container">

View File

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

View File

@ -34,21 +34,12 @@ pub fn DestinationPlate(props: &DestinationPlateProps) -> Html {
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();
@ -107,11 +98,6 @@ pub fn DestinationPlate(props: &DestinationPlateProps) -> Html {
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| {
@ -128,10 +114,10 @@ pub fn DestinationPlate(props: &DestinationPlateProps) -> 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}
in_transfer={destination_wells.contains(&(i,j))}
color={transfer_map.get(&(i,j))
.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}
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>();
html! {
<div ondblclick={screenshot_callback}
class={classes!{"dest_plate",
<div class={classes!{"dest_plate",
"W".to_owned()+&props.source_plate.plate.plate_format.to_string()}}>
<table
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 {
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));
@ -61,14 +60,6 @@ pub fn SourcePlate(props: &SourcePlateProps) -> Html {
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();
@ -108,10 +99,6 @@ pub fn SourcePlate(props: &SourcePlateProps) -> Html {
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| {
@ -131,10 +118,10 @@ pub fn SourcePlate(props: &SourcePlateProps) -> 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}
in_transfer={source_wells.contains(&(i,j))}
color={transfer_map.get(&(i,j))
.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}
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>();
html! {
<div ondblclick={screenshot_callback}
class={classes!{"source_plate",
<div class={classes!{"source_plate",
"W".to_owned()+&props.source_plate.plate.plate_format.to_string()}}>
<table
onmouseup={move |e| {

View File

@ -2,10 +2,6 @@
// https://iquilezles.org/articles/palettes/
// http://dev.thi.ng/gradients/
use rand::prelude::*;
use rand::rngs::SmallRng;
use lazy_static::lazy_static;
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct ColorPalette {
a: [f64; 3],
@ -35,23 +31,9 @@ impl ColorPalette {
self.get((2f64.powi(-(t.ilog2() as i32))) * (t as f64 + 0.5f64) - 1.0f64)
}
// pub fn get_uuid(&self, t: uuid::Uuid) -> [f64; 3] {
// // self.get(t.as_u128() as f64 / (u128::MAX) as f64)
// let mut r = SmallRng::seed_from_u64(t.as_u128() as u64);
// self.get(r.gen_range(0.0..1.0f64))
// }
pub fn get_ordered(&self, t: uuid::Uuid, ordered_uuids: &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);
pub fn get_uuid(&self, t: uuid::Uuid) -> [f64; 3] {
log::debug!("{}", t.as_u128() as f64 / (u128::MAX) as f64);
self.get(t.as_u128() as f64 / (u128::MAX) as f64)
}
}
@ -72,5 +54,4 @@ impl Palettes {
c: [0.100, 0.500, 0.360],
d: [0.000, 0.000, 0.650],
};
}

View File

@ -13,17 +13,6 @@ pub struct CurrentTransfer {
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)]
#[non_exhaustive]
pub struct MainState {
@ -33,9 +22,6 @@ pub struct MainState {
pub selected_source_plate: Uuid,
pub selected_dest_plate: Uuid,
pub selected_transfer: Uuid,
#[serde(default)]
pub preferences: Preferences,
}
impl Store for MainState {

View File

@ -39,9 +39,6 @@ pub fn TransferMenu() -> Html {
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 {
@ -195,7 +192,7 @@ pub fn TransferMenu() -> Html {
let new_transfer = Transfer::new(
spi.clone(),
dpi.clone(),
ct_state.transfer.transfer_region.clone(),
ct_state.transfer.transfer_region,
ct_state.transfer.name.clone(),
);
main_dispatch.reduce_mut(|state| {
@ -253,8 +250,6 @@ pub fn TransferMenu() -> Html {
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"
@ -296,7 +291,6 @@ pub fn TransferMenu() -> Html {
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"} />
@ -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 {
fn from(value: &Region) -> Self {
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))
.ok()
.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;
for (i, letter) in letters.to_ascii_uppercase().chars().rev().enumerate() {
log::debug!("{}, {}", i, letter);
for (i, letter) in letters.chars().rev().enumerate() {
let n = letter as u8;
if !(65..=90).contains(&n) {
return None;

View File

@ -2,23 +2,23 @@ use crate::components::states::MainState;
use crate::components::transfer_menu::num_to_letters;
use crate::data::transfer::Transfer;
use serde::{Serialize, Deserialize};
use serde::Serialize;
use std::error::Error;
#[derive(Serialize, Deserialize, Debug)]
pub struct TransferRecord {
#[serde(rename = "Source Plate")]
pub source_plate: String,
#[derive(Serialize, Debug)]
struct TransferRecord {
#[serde(rename = "Source Plate Barcode")]
source_plate: String,
#[serde(rename = "Source Well")]
pub source_well: String,
#[serde(rename = "Dest Plate")]
pub destination_plate: String,
source_well: String,
#[serde(rename = "Destination Plate Barcode")]
destination_plate: String,
#[serde(rename = "Destination Well")]
pub destination_well: String,
#[serde(rename = "Transfer Volume")]
pub volume: f32,
destination_well: String,
#[serde(rename = "Volume")]
volume: f32,
#[serde(rename = "Concentration")]
pub concentration: Option<f32>,
concentration: Option<f32>,
}
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)]
pub struct PlateInstance {
pub plate: Plate,
#[serde(rename = "id_v7")]
#[serde(default = "Uuid::now_v7")]
id: Uuid,
pub name: String,
}
@ -18,7 +16,7 @@ impl PlateInstance {
plate_type: sort,
plate_format: format,
},
id: Uuid::now_v7(),
id: Uuid::new_v4(),
name,
}
}
@ -36,7 +34,7 @@ impl From<Plate> for PlateInstance {
fn from(value: Plate) -> Self {
PlateInstance {
plate: value,
id: Uuid::now_v7(),
id: Uuid::new_v4(),
name: "New Plate".to_string(),
}
}

View File

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

View File

@ -1,20 +1,11 @@
use serde::{Deserialize, Serialize};
use crate::components::transfer_menu::RegionDisplay;
use super::plate::Plate;
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
pub struct CustomRegion {
src: Vec<(u8, u8)>,
dest: Vec<(u8, u8)>,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Debug)]
pub enum Region {
Rect((u8, u8), (u8, u8)),
Point((u8, u8)),
Custom(CustomRegion),
}
impl Default for Region {
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 {
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)]
#[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Debug)]
pub struct TransferRegion {
pub source_plate: Plate,
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 {
pub fn get_source_wells(&self) -> Vec<(u8, u8)> {
match &self.source_region {
match self.source_region {
Region::Rect(c1, c2) => {
let mut wells = Vec::<(u8, u8)>::new();
let (ul, br) = standardize_rectangle(&c1, &c2);
@ -96,8 +71,7 @@ impl TransferRegion {
}
wells
}
Region::Point(p) => vec![*p],
Region::Custom(c) => c.src.clone(),
Region::Point(p) => vec![p],
}
}
@ -138,7 +112,6 @@ impl TransferRegion {
let source_corners: ((u8, u8), (u8, u8)) = match self.source_region {
Region::Point((x, y)) => ((x, y), (x, y)),
Region::Rect(c1, c2) => (c1, c2),
Region::Custom(_) => ((0, 0), (0, 0)),
};
let (source_ul, _) = standardize_rectangle(&source_corners.0, &source_corners.1);
// This map is not necessarily injective or surjective,
@ -147,7 +120,7 @@ impl TransferRegion {
// and simple then we *will* have injectivity.
// Non-replicate transfers:
match &self.dest_region {
match self.dest_region {
Region::Point((x, y)) => {
Box::new(move |(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)");
}
}
Region::Custom(_) => return Ok(()),
}
if il_source.0 == 0 || il_dest.1 == 0 {