Merge pull request #12 from em-ilia/yew

Yew
This commit is contained in:
Emilia Allison 2023-06-02 16:28:02 -04:00 committed by GitHub
commit 09d99e27a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 2639 additions and 1591 deletions

2
.cargo/config.toml Normal file
View File

@ -0,0 +1,2 @@
[build]
target = "wasm32-unknown-unknown"

1719
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -6,10 +6,16 @@ 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
[dependencies] [dependencies]
dioxus = { git = "https://github.com/dioxuslabs/dioxus.git" } yew = { version = "0.20.0", features = ["csr"] }
dioxus-web = { git = "https://github.com/dioxuslabs/dioxus.git" } yewdux = "0.9"
fermi = { git = "https://github.com/dioxuslabs/dioxus" } # Yikes wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = ["FormData", "HtmlFormElement", "HtmlDialogElement"] }
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.3", features = ["v4", "fast-rng", "macro-diagnostics", "js", "serde"] }
serde = { version = "1.0", features = ["derive"] }
[dev-dependencies]
wasm-bindgen-test = "0.3.0"

View File

@ -1,46 +0,0 @@
[application]
# dioxus project name
name = "Plate Tool"
# default platfrom
# you can also use `dioxus serve/build --platform XXX` to use other platform
# value: web | desktop
default_platform = "web"
# Web `build` & `serve` dist path
out_dir = "dist"
# resource (static) file folder
asset_dir = "public"
[web.app]
# HTML title tag content
title = "Plate Tool"
[web.watcher]
watch_path = ["src"]
index_on_404 = true
# include `assets` in web platform
[web.resource]
# CSS style file
style = []
# Javascript code file
script = []
[web.resource.dev]
# Javascript code file
# serve: [dev-server] only
script = []
[application.tools]
# use binaryen.wasm-opt for output Wasm file
# binaryen just will trigger in `web` platform
binaryen = { wasm_opt = true }

View File

@ -6,6 +6,12 @@
font: inherit; font: inherit;
vertical-align: baseline; vertical-align: baseline;
} }
h1, h2, h3 {
padding: 0;
margin: 0;
margin-bottom: 0.4em;
margin-top: 0.4em;
}
div.main_container { div.main_container {
height: 95vh; height: 95vh;
@ -25,6 +31,12 @@ div.tree {
height: 100%; height: 100%;
border: 2px solid green; border: 2px solid green;
} }
div.tree li {
user-select: none;
}
div.tree li.selected {
background: rgba(0,0,150,0.2);
}
div.transfer_menu { div.transfer_menu {
grid-column: left / left; grid-column: left / left;
@ -33,9 +45,19 @@ div.transfer_menu {
height: 100%; height: 100%;
border: 2px solid orange; border: 2px solid orange;
} }
div.transfer_menu input:invalid {
background-color: #faa;
}
div.plate_container { div.plate_container {
border: 2px dashed purple; border: 2px dashed purple;
grid-column: right / right; grid-column: right / right;
grid-row: upper / 3; grid-row: upper / 3;
} }
.dialog {
padding: 1em;
}
.dialog::backdrop {
background: rgba(0,125,255,0.3);
}

40
assets/css/plate.css Normal file
View File

@ -0,0 +1,40 @@
table, tr, td {
user-select: none; /* Prevents dragging issue */
border-spacing: 0px;
}
td.plate_cell {
height: 2vmin;
background: none;
}
div.plate_cell_inner {
aspect-ratio: 1 / 1;
height: 90%;
border-radius: 50%;
border: 2px solid black;
}
td.plate_cell:hover div.plate_cell_inner {
background: black !important;
}
td.plate_cell.in_transfer div.plate_cell_inner::after {
content: "";
width: 100%;
height: 100%;
display: block;
border-radius: 50%;
background-image: repeating-linear-gradient(
45deg,
rgba(0,0,0,0.8),
rgba(0,0,0,0.8) 2px,
transparent 2px,
transparent 4px
);
}
div.source_plate td.current_select div.plate_cell_inner {
background-image: linear-gradient(lightblue, lightblue);
}
div.dest_plate td.current_select div.plate_cell_inner {
background: lightcoral;
}

Binary file not shown.

BIN
assets/fonts/Jost.ttf Normal file

Binary file not shown.

View File

@ -0,0 +1,33 @@
@use "sass:color";
@use "sass:math";
$-color-white: hsl(30 5% 90%);
$-color-dark: color.adjust($-color-white, $lightness: -60%);
$-color-light: hsl(190 80% 30%);
$-ff-serif: "Inconsolata", monospace;
$-ff-sans: "Jost", sans-serif;
$-fs-900: math.div(100rem, 16);
@forward "base" with (
$color-white: $-color-white,
$color-dark: $-color-dark,
$color-light: $-color-light,
$ff-serif: $-ff-serif,
$ff-sans: $-ff-sans,
$fs-900: $-fs-900
);
@font-face {
font-family: "Inconsolata";
src: url("/fonts/Inconsolata.ttf");
font-display: swap;
font-variation-settings: "wdth" 85;
}
@font-face {
font-family: "Jost";
src: url("/fonts/Jost.ttf");
font-display: swap;
}

View File

@ -0,0 +1,214 @@
//
//
// Do Not Change This File!
//
//
@use "sass:math";
/* ----------------- */
/* Custom Properties */
/* ----------------- */
/*
This values are to be overridden
after being injected into
the global scope.
*/
/* colors */
$color-dark: hsl(0 0% 0%) !default; /* Black */
$color-light: hsl(0 0% 50%) !default; /* Gray */
$color-white: hsl(0 0% 100%) !default;/* White */
/* font */
/* Sizes divided by 16 so values given in px */
$fs-900: math.div(125rem, 16) !default;
$fs-800: math.div(75rem, 16) !default;
$fs-700: math.div(56rem, 16) !default;
$fs-600: math.div(32rem, 16) !default;
$fs-500: math.div(28rem, 16) !default;
$fs-400: math.div(24rem, 16) !default;
$fs-300: math.div(18rem, 16) !default;
$fs-200: math.div(16rem, 16) !default;
$ff-serif: serif !default;
$ff-sans-cond: sans-serif !default;
$ff-sans: sans-serif !default;
/* --------------- */
/* Utility Classes */
/* --------------- */
/* Layouts */
.container {
padding-inline: clamp(0.5rem, 4rem, 5rem);
margin-inline: auto;
max-width: 80rem;
}
.flex {
display: flex;
gap: 2rem;
}
.column {
justify-content: center;
align-items: center;
flex-direction: column;
align-content: center;
}
.row {
flex-direction: row;
flex-wrap: wrap;
justify-content: space-evenly;
align-content: baseline;
max-width: 100%;
}
.two-columns {
display: grid;
/* Will shrink to one column, never exceed two!
* The `max` in `minmax` asks that the columns
* be no smaller */
grid-template-columns: repeat(auto-fit, minmax(max(30rem, 40%), 1fr));
column-gap: 1rem;
justify-content: space-evenly;
}
.lock-bottom {
position: fixed;
bottom: 0%;
}
/* Other */
.hr::after { /* Add fake hr after header */
content: '';
position: absolute;
display: block;
clear: both;
width: 100%;
height: 0.15rem;
background-color: black;
}
/* Color Classes */
.bg-dark { background-color: $color-dark; }
.bg-light { background-color: $color-light; }
.bg-white { background-color: $color-white; }
.text-dark { color: $color-dark; }
.text-light { color: $color-light; }
.text-white { color: $color-white; }
/* Font Classes */
.fs-900 { font-size: $fs-900; }
.fs-800 { font-size: $fs-800; }
.fs-700 { font-size: $fs-700; }
.fs-600 { font-size: $fs-600; }
.fs-500 { font-size: $fs-500; }
.fs-400 { font-size: $fs-400; }
.fs-300 { font-size: $fs-300; }
.fs-200 { font-size: $fs-200; }
.ff-serif { font-family: $ff-serif; }
.ff-sans-cond { font-family: $ff-sans-cond; }
.ff-sans { font-family: $ff-sans; }
.uppercase { text-transform: uppercase; }
.lowercase { text-transform: lowercase; }
/* Semantic Tags and Their Classes */
header {
margin-bottom: 3vh;
}
section:not(:last-of-type) {
margin-bottom: 3vh;
}
footer {
margin-top: 3vh;
}
/* ----- */
/* Reset */
/* ----- */
*,
*::before,
*::after {
box-sizing: border-box;
}
body, h1, h2, h3, h4, h5, h6, p {
margin: 0;
}
h1, h2, h3, h4, h5, h6, p {
font-weight: 400;
}
h1, h2, h3 {
line-height: 1.1;
}
body {
font-family: $ff-sans;
font-size: $fs-400;
color: $color-dark;
background-color: $color-white;
line-height: 1.5;
min-height: 100vh;
}
main {
margin-left: 1vw;
margin-top: 1vh;
* {
z-index: 2;
}
}
footer {
z-index: 2;
}
img, picture {
max-width: 100%;
display: block;
}
input, button, textarea, select {
font: inherit;
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behaviour: auto !important;
}
}
/* -------------------- */
/* Non-Reusable Classes */
/* -------------------- */
/* meant for these pages
* only, not to be used
* in practice */
.colors--block {
padding: 3rem 1rem 1rem;
border: 1px solid black;
}

View File

@ -0,0 +1,5 @@
@forward "main_window";
@forward "plate_container";
@forward "plates";
@forward "tree";
@forward "transfer_menu";

View File

@ -0,0 +1,20 @@
div.main_container {
height: 95vh;
width: 98vw;
margin-top: 2.5vh;
margin-left: 1vw;
display: grid;
grid-template-columns: [left] minmax(min-content, 1fr) [right] 2fr;
grid-template-rows: [upper] 2fr [lower] 1fr;
column-gap: 1vw;
row-gap: 1vh;
}
.dialog {
padding: 1em;
}
.dialog::backdrop {
background: rgba(0,125,255,0.3);
}

View File

@ -0,0 +1,18 @@
@use "sass:color";
@use "../variables" as *;
div.plate_container {
display: flex;
flex-direction: column;
justify-content: space-around;
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

@ -0,0 +1,52 @@
div.source_plate, div.dest_plate {
padding: 3px 3px 3px 3px;
}
div.source_plate {
border: 2px solid blue;
}
div.dest_plate {
border: 2px solid red;
}
table, tr, td {
user-select: none; /* Prevents dragging issue */
border-spacing: 0px;
}
td.plate_cell {
height: 2.3vmin;
background: none;
}
div.plate_cell_inner {
aspect-ratio: 1 / 1;
height: 90%;
border-radius: 50%;
border: 2px solid black;
}
td.plate_cell:hover div.plate_cell_inner {
background: black !important;
}
td.plate_cell.in_transfer div.plate_cell_inner::after {
content: "";
width: 100%;
height: 100%;
display: block;
border-radius: 50%;
background-image: repeating-linear-gradient(
45deg,
rgba(0,0,0,0.8),
rgba(0,0,0,0.8) 2px,
transparent 2px,
transparent 4px
);
}
div.source_plate td.current_select div.plate_cell_inner {
background-image: linear-gradient(lightblue, lightblue);
}
div.dest_plate td.current_select div.plate_cell_inner {
background: lightcoral;
}

View File

@ -0,0 +1,61 @@
@use "sass:color";
@use "../variables" as *;
div.transfer_menu {
position: relative;
width: 100%;
height: 100%;
grid-column: left / left;
grid-row: lower / lower;
border: 2px solid $color-dark;
form {
padding-top: 3%;
padding-bottom: 1%;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
label {
display: inline;
* { display: inline; }
}
}
padding-left: 0.5rem;
input:invalid {
background-color: #faa;
}
div#controls {
align-self: flex-end;
input {
padding: 2px 3px 2px 3px;
margin-left: 0;
}
}
}
input {
text-align: center;
margin-left: 0.5em;
margin-right: 0.5em;
margin-top: 1%;
margin-bottom: 1%;
line-height: 1em;
padding: 0;
&[type="text"] {
width: 4em;
}
&[name="name"] {
width: 6em; // Override above
}
&[type="number"] {
width: 2em;
}
}

View File

@ -0,0 +1,55 @@
@use "sass:color";
@use "../variables" as *;
$selection-border-width: 2px;
div.tree {
position: relative;
grid-column: left / left;
grid-row: upper / upper;
width: 100%;
height: 100%;
border: 2px solid $color-dark;
h3 {
margin-left: 0.5rem;
}
div#controls {
position: absolute;
bottom: 2%;
right: 2%;
}
}
div.tree ul {
width: 80%;
margin-left: 10%;
padding: 0;
display: flex;
flex-direction: column;
align-items: stretch;
overflow: scroll;
}
div.tree li {
display: inline;
margin-left: 0;
margin-bottom: 0.4rem;
border: 2px solid transparent;
user-select: none;
list-style: none;
line-height: 1em;
&:hover {
background: color.change($color-light, $alpha: 0.08);
border: $selection-border-width solid color.change($color-light, $alpha:0.3);
}
&.selected {
background: color.change($color-light, $alpha: 0.2);
}
}

View File

@ -0,0 +1,10 @@
// Global Variables
@use "variables" as *;
@use "components";
.fs-900 { font-size: $fs-900; font-weight: 400;}
.fs-800 { font-size: $fs-800; font-weight: 300;}
.fs-700 { font-size: $fs-700; font-weight: 250;}
.fs-600 { font-size: $fs-600; font-weight: 300;}
.fs-500 { font-size: $fs-500; font-weight: 200;}
.fs-400 { font-size: $fs-400; font-weight: 200;}

2
assets/scss/index.scss Normal file
View File

@ -0,0 +1,2 @@
@use "default_theme/main";
@use "default_theme/variables" as *;

9
index.html Normal file
View File

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<link data-trunk rel="scss" href="assets/scss/index.scss">
<link data-trunk rel="copy-dir" href="assets/fonts">
<title>Plate Tool</title>
</head>
</html>

View File

@ -1,22 +1,64 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
use dioxus::prelude::*; use yew::prelude::*;
use yewdux::prelude::*;
use super::new_plate_dialog::NewPlateDialog;
use super::plates::plate_container::PlateContainer; use super::plates::plate_container::PlateContainer;
use super::tree::Tree; use super::states::{CurrentTransfer, MainState};
use super::transfer_menu::TransferMenu; use super::transfer_menu::TransferMenu;
use super::tree::Tree;
static STYLE: &'static str = include_str!("global.css"); use crate::data::plate_instances::PlateInstance;
pub fn MainWindow(cx: Scope) -> Element { #[function_component]
cx.render(rsx! { pub fn MainWindow() -> Html {
style { STYLE }, let (main_state, main_dispatch) = use_store::<MainState>();
div { let (ct_state, ct_dispatch) = use_store::<CurrentTransfer>();
class: "main_container",
Tree {}, let source_plate_instance: Option<PlateInstance> = main_state
TransferMenu {}, .source_plates
PlateContainer { .iter()
source_dims: (24,16), .find(|spi| spi.get_uuid() == main_state.selected_source_plate)
destination_dims: (24,16) .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 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);
})
};
html! {
<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>
}) }
} }

View File

@ -1,4 +1,6 @@
pub mod plates;
pub mod main_window; pub mod main_window;
pub mod tree; pub mod new_plate_dialog;
pub mod plates;
pub mod states;
pub mod transfer_menu; pub mod transfer_menu;
pub mod tree;

View File

@ -0,0 +1,104 @@
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, transfer::Transfer};
#[derive(PartialEq, Properties)]
pub struct NewPlateDialogProps {
pub close_callback: Callback<()>,
}
#[function_component]
pub fn NewPlateDialog(props: &NewPlateDialogProps) -> Html {
let (state, dispatch) = use_store::<MainState>();
let new_plate_callback = {
let dispatch = dispatch.clone();
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() {
"384" => PlateFormat::W384,
"96" => PlateFormat::W96,
_ => PlateFormat::W6,
};
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,
))
}
});
}
}
}
})
};
// 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">
<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="96">{"96"}</option>
<option value="384">{"384"}</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>
</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

@ -1,40 +1,151 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
use dioxus::prelude::*; use std::rc::Rc;
use yew::prelude::*;
use yewdux::prelude::*;
#[inline_props] use crate::components::states::CurrentTransfer;
pub fn DestinationPlate(cx: Scope, width: u8, height: u8) -> Element { use crate::data::plate_instances::PlateInstance;
cx.render(rsx! { use crate::data::transfer_region::{Region, TransferRegion};
div {
class: "dest_plate", use super::super::transfer_menu::RegionDisplay;
table {
for i in 1..=cx.props.height { #[derive(Properties, PartialEq)]
tr { pub struct DestinationPlateProps {
draggable: "false", pub source_plate: PlateInstance,
for j in 1..=cx.props.width { pub destination_plate: PlateInstance,
DestPlateCell {i: i, j: j}
}
}
},
}
}
})
} }
#[inline_props] #[function_component]
fn DestPlateCell(cx: Scope<PlateCellProps>, i: u8, j: u8, color: Option<String>) -> Element { pub fn DestinationPlate(props: &DestinationPlateProps) -> Html {
let color_string = match color { let (ct_state, ct_dispatch) = use_store::<CurrentTransfer>();
Some(c) => c.clone(), let m_start_handle: UseStateHandle<Option<(u8, u8)>> = use_state_eq(|| None);
None => "None".to_string(), let m_end_handle: UseStateHandle<Option<(u8, u8)>> = use_state_eq(|| None);
let m_stat_handle: UseStateHandle<bool> = use_state_eq(|| false);
let m_start = m_start_handle.clone();
let m_end = m_end_handle.clone();
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),
};
m_start_handle.set(Some(pt1));
m_end_handle.set(Some(pt2));
}
let destination_wells = ct_state.transfer.transfer_region.get_destination_wells();
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)));
}
}
})
}; };
cx.render(rsx! { let mouseup_callback = {
td { let m_start_handle = m_start_handle.clone();
class: "plate_cell", let m_end_handle = m_end_handle.clone();
draggable: "false", let m_stat_handle = m_stat_handle.clone();
style: "background: {color_string}",
div { Callback::from(move |_: MouseEvent| {
class: "plate_cell_inner" 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 rows = (1..=props.destination_plate.plate.size().0)
.map(|i| {
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.clone(), *m_end.clone(), (i,j))}
mouse={mouse_callback.clone()}
in_transfer={destination_wells.contains(&(i,j))}
/>
}
}).collect::<Html>();
html! {
<tr>
{ row }
</tr>
}
})
.collect::<Html>();
html! {
<div class="dest_plate">
<table
onmouseup={move |e| {
mouseup_callback.emit(e);
}}
onmouseleave={move |e| {
mouseleave_callback.emit(e);
}}>
{ 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>,
}
#[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 mouse = Callback::clone(&props.mouse);
let mouse2 = Callback::clone(&props.mouse);
let (i, j) = (props.i.clone(), props.j.clone());
html! {
<td class={classes!("plate_cell", selected_class, in_transfer_class)}
onmousedown={move |_| {
mouse.emit((i,j, MouseEventType::MOUSEDOWN))
}}
onmouseenter={move |_| {
mouse2.emit((i,j, MouseEventType::MOUSEENTER))
}}>
<div class="plate_cell_inner" />
</td>
}
} }

View File

@ -1,3 +1,3 @@
pub mod source_plate;
pub mod destination_plate; pub mod destination_plate;
pub mod plate_container; pub mod plate_container;
pub mod source_plate;

View File

@ -1,22 +0,0 @@
table, tr, td {
user-select: none; /* Prevents dragging issue */
border-spacing: 0px;
}
td.plate_cell {
height: 2vmin;
background: none;
}
div.plate_cell_inner {
aspect-ratio: 1 / 1;
height: 90%;
border-radius: 50%;
border: 2px solid black;
}
td.plate_cell:hover div.plate_cell_inner {
background: black !important;
}
td.current_select div.plate_cell_inner {
background: lightblue !important;
}

View File

@ -1,20 +1,37 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
use dioxus::prelude::*; use yew::prelude::*;
use super::source_plate::SourcePlate;
use crate::data::plate_instances::PlateInstance;
use super::destination_plate::DestinationPlate; use super::destination_plate::DestinationPlate;
use super::source_plate::SourcePlate;
static STYLE: &'static str = include_str!("plate_container.css"); #[derive(Properties, PartialEq)]
pub struct PlateContainerProps {
#[inline_props] pub source_dims: Option<PlateInstance>,
pub fn PlateContainer(cx: Scope, source_dims: (u8,u8), destination_dims: (u8,u8)) -> Element { pub destination_dims: Option<PlateInstance>,
cx.render(rsx! { }
style { STYLE }
div { #[function_component]
class: "plate_container", pub fn PlateContainer(props: &PlateContainerProps) -> Html {
SourcePlate {width: source_dims.0, html! {
height: source_dims.1}, <div class="plate_container">
DestinationPlate {width: destination_dims.0, if let Some(spi) = props.source_dims.clone() {
height: destination_dims.1} if let Some(dpi) = props.destination_dims.clone() {
} <div>
}) <h2>{spi.name.clone()}</h2>
<SourcePlate source_plate={spi.clone()} destination_plate={dpi.clone()} />
</div>
<div>
<h2>{dpi.name.clone()}</h2>
<DestinationPlate source_plate={spi.clone()} destination_plate={dpi.clone()} />
</div>
} else {
<h2>{"No Destination Plate Selected"}</h2>
}
} else {
<h2>{"No Source Plate Selected"}</h2>
}
</div>
}
} }

View File

@ -1,91 +1,159 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
use dioxus::prelude::*;
static STYLE: &'static str = include_str!("plate.css"); use std::rc::Rc;
use yew::prelude::*;
use yewdux::prelude::*;
#[derive(PartialEq, Props)] use crate::components::states::CurrentTransfer;
use crate::data::plate_instances::PlateInstance;
use crate::data::transfer_region::{Region, TransferRegion};
use super::super::transfer_menu::RegionDisplay;
#[derive(PartialEq, Properties)]
pub struct SourcePlateProps { pub struct SourcePlateProps {
width: u8, pub source_plate: PlateInstance,
height: u8, pub destination_plate: PlateInstance,
}
struct SelectionState {
m_start: Option<(u8, u8)>,
m_end: Option<(u8, u8)>,
m_stat: bool,
} }
pub fn SourcePlate(cx: Scope<SourcePlateProps>) -> Element { #[function_component]
use_shared_state_provider(cx, || SelectionState { pub fn SourcePlate(props: &SourcePlateProps) -> Html {
m_start: None, let (ct_state, ct_dispatch) = use_store::<CurrentTransfer>();
m_end: None, let m_start_handle: UseStateHandle<Option<(u8, u8)>> = use_state_eq(|| None);
m_stat: false, let m_end_handle: UseStateHandle<Option<(u8, u8)>> = use_state_eq(|| None);
}); let m_stat_handle: UseStateHandle<bool> = use_state_eq(|| false);
let m_start = m_start_handle.clone();
let m_end = m_end_handle.clone();
cx.render(rsx! { if !(*m_stat_handle) {
div{ let (pt1, pt2) = match ct_state.transfer.transfer_region.source_region {
class: "source_plate", Region::Point((x, y)) => ((x, y), (x, y)),
style { STYLE } Region::Rect(c1, c2) => (c1, c2),
table { };
draggable: "false", m_start_handle.set(Some(pt1));
for i in 1..=cx.props.height { m_end_handle.set(Some(pt2));
tr { }
draggable: "false",
for j in 1..=cx.props.width { let source_wells = ct_state.transfer.transfer_region.get_source_wells();
SourcePlateCell {i: i, j: j}
} 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();
let m_stat_handle = m_stat_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);
});
} }
}, }
} }
} })
})
}
#[inline_props]
fn SourcePlateCell(cx: Scope<PlateCellProps>, i: u8, j: u8, color: Option<String>) -> Element {
let selection_state = use_shared_state::<SelectionState>(cx).unwrap();
let selected = in_rect(
selection_state.read().m_start,
selection_state.read().m_end,
(*i, *j),
);
let selected_class = match selected {
true => "current_select",
false => "",
};
let color_string = match color {
Some(c) => c.clone(),
None => "None".to_string(),
}; };
cx.render(rsx! { let mouseleave_callback = Callback::clone(&mouseup_callback);
td {
class: "plate_cell {selected_class}", let rows = (1..=props.source_plate.plate.size().0)
draggable: "false", .map(|i| {
style: "background: {color_string}", let row = (1..=props.source_plate.plate.size().1)
onmousedown: move |_| { .map(|j| {
selection_state.write().m_start = Some((*i,*j)); html! {
selection_state.write().m_end = None; <SourcePlateCell i={i} j={j}
selection_state.write().m_stat = true; selected={in_rect(*m_start.clone(), *m_end.clone(), (i,j))}
}, mouse={mouse_callback.clone()}
onmouseenter: move |me: MouseEvent| { in_transfer={source_wells.contains(&(i,j))}
if me.data.held_buttons().is_empty() { />
selection_state.write().m_stat = false; }
} })
if selection_state.read().m_stat { .collect::<Html>();
selection_state.write().m_end = Some((*i,*j)) html! {
} <tr>
}, { row }
onmouseup: move |_| { </tr>
selection_state.write().m_stat = false
},
div {
class: "plate_cell_inner"
} }
} })
}) .collect::<Html>();
html! {
<div class="source_plate">
<table
onmouseup={move |e| {
mouseup_callback.emit(e);
}}
onmouseleave={move |e| {
mouseleave_callback.emit(e);
}}>
{ rows }
</table>
</div>
}
} }
fn in_rect(corner1: Option<(u8, u8)>, corner2: Option<(u8, u8)>, pt: (u8, u8)) -> bool { #[derive(PartialEq, Properties)]
pub struct SourcePlateCellProps {
i: u8,
j: u8,
selected: bool,
mouse: Callback<(u8, u8, MouseEventType)>,
in_transfer: Option<bool>,
}
#[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 mouse = Callback::clone(&props.mouse);
let mouse2 = Callback::clone(&props.mouse);
let (i, j) = (props.i.clone(), props.j.clone());
html! {
<td class={classes!("plate_cell", selected_class, in_transfer_class)}
onmousedown={move |_| {
mouse.emit((i,j, MouseEventType::MOUSEDOWN))
}}
onmouseenter={move |_| {
mouse2.emit((i,j, MouseEventType::MOUSEENTER))
}}>
<div class="plate_cell_inner" />
</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) { if let (Some(c1), Some(c2)) = (corner1, corner2) {
return pt.0 <= u8::max(c1.0, c2.0) return pt.0 <= u8::max(c1.0, c2.0)
&& pt.0 >= u8::min(c1.0, c2.0) && pt.0 >= u8::min(c1.0, c2.0)
@ -98,10 +166,13 @@ fn in_rect(corner1: Option<(u8, u8)>, corner2: Option<(u8, u8)>, pt: (u8, u8)) -
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use wasm_bindgen_test::*;
use super::in_rect; use super::in_rect;
// in_rect tests // in_rect tests
#[test] #[test]
#[wasm_bindgen_test]
fn test_in_rect1() { fn test_in_rect1() {
// Test in center of rect // Test in center of rect
let c1 = (1, 1); let c1 = (1, 1);
@ -113,6 +184,7 @@ mod tests {
} }
#[test] #[test]
#[wasm_bindgen_test]
fn test_in_rect2() { fn test_in_rect2() {
// Test on top/bottom edges of rect // Test on top/bottom edges of rect
let c1 = (1, 1); let c1 = (1, 1);
@ -127,6 +199,7 @@ mod tests {
} }
#[test] #[test]
#[wasm_bindgen_test]
fn test_in_rect3() { fn test_in_rect3() {
// Test on left/right edges of rect // Test on left/right edges of rect
let c1 = (1, 1); let c1 = (1, 1);
@ -141,6 +214,7 @@ mod tests {
} }
#[test] #[test]
#[wasm_bindgen_test]
fn test_in_rect4() { fn test_in_rect4() {
// Test cases that should fail // Test cases that should fail
let c1 = (1, 1); let c1 = (1, 1);

90
src/components/states.rs Normal file
View File

@ -0,0 +1,90 @@
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use yewdux::{prelude::*, storage};
use super::transfer_menu::RegionDisplay;
use crate::data::plate::*;
use crate::data::plate_instances::PlateInstance;
use crate::data::transfer::Transfer;
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, Store)]
#[store(storage = "session")]
pub struct CurrentTransfer {
pub transfer: Transfer,
}
#[derive(Default, PartialEq, Clone, Serialize, Deserialize)]
pub struct MainState {
pub source_plates: Vec<PlateInstance>,
pub destination_plates: Vec<PlateInstance>,
pub transfers: Vec<Transfer>,
pub selected_source_plate: Uuid,
pub selected_dest_plate: Uuid,
pub selected_transfer: Uuid,
}
impl Store for MainState {
fn new() -> Self {
init_listener(storage::StorageListener::<Self>::new(storage::Area::Local));
storage::load(storage::Area::Local)
.expect("Unable to load state")
.unwrap_or_default()
/*
Self {
source_plates: Vec::new(),
destination_plates: Vec::new(),
transfers: Vec::new(),
}
*/
}
fn should_notify(&self, old: &Self) -> bool {
self != old
}
}
impl MainState {
pub fn purge_transfers(&mut self) {
// Removes any transfers for which the associated plates are gone
self.transfers = self
.transfers
.iter()
.filter(|tr| {
self.source_plates
.iter()
.any(|spi| spi.get_uuid() == tr.source_id)
&& self
.destination_plates
.iter()
.any(|dpi| dpi.get_uuid() == tr.dest_id)
})
.map(|tr| tr.clone())
.collect();
}
pub fn add_source_plate(&mut self, plate: PlateInstance) {
assert!(plate.plate.plate_type == PlateType::Source);
self.source_plates.push(plate);
}
pub fn add_dest_plate(&mut self, plate: PlateInstance) {
assert!(plate.plate.plate_type == PlateType::Destination);
self.destination_plates.push(plate);
}
pub fn del_plate(&mut self, id: Uuid) {
if let Some(index) = self
.source_plates
.iter()
.position(|spi| spi.get_uuid() == id)
{
self.source_plates.swap_remove(index);
}
if let Some(index) = self
.destination_plates
.iter()
.position(|dpi| dpi.get_uuid() == id)
{
self.destination_plates.swap_remove(index);
}
}
}

View File

@ -1,42 +1,285 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
use dioxus::prelude::*;
use regex::Regex;
use lazy_static::lazy_static;
#[inline_props] use lazy_static::lazy_static;
pub fn TransferMenu(cx: Scope) -> Element { use regex::Regex;
cx.render(rsx! { use serde::{Deserialize, Serialize};
div { use wasm_bindgen::JsCast;
class: "transfer_menu", use web_sys::{EventTarget, HtmlInputElement};
form{ use uuid::Uuid;
label { use yew::prelude::*;
r#for: "src_region", use yewdux::prelude::*;
"Source Region:"
}, use crate::data::{transfer::Transfer, transfer_region::Region};
input {
r#type: "text", use super::states::{CurrentTransfer, MainState};
name: "src_region",
}, #[function_component]
label { pub fn TransferMenu() -> Html {
r#for: "dest_region", let (main_state, main_dispatch) = use_store::<MainState>();
"Destination Region:" let (ct_state, ct_dispatch) = use_store::<CurrentTransfer>();
},
input { let on_name_change = {
r#type: "text", let ct_dispatch = ct_dispatch.clone();
name: "dest_region",
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 {
ct_dispatch.reduce_mut(|state| {
state.transfer.name = input.value().clone();
});
} }
} }
}
}) )};
let on_src_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()) {
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()) {
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 new_transfer_button_callback = {
let main_dispatch = main_dispatch.clone();
let main_state = main_state.clone();
let ct_dispatch = ct_dispatch.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();
Callback::from(move |_: 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,
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();
});
}
}
} 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 main_dispatch = main_dispatch.clone();
let main_state = main_state.clone();
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>
<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 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)] #[derive(PartialEq, Eq, Debug, Clone, Default, Serialize, Deserialize)]
struct RegionDisplay { pub struct RegionDisplay {
text: String, pub text: String,
col_start: u8, pub col_start: u8,
row_start: u8, pub row_start: u8,
col_end: u8, pub col_end: u8,
row_end: u8, pub row_end: u8,
} }
impl TryFrom<String> for RegionDisplay { impl TryFrom<String> for RegionDisplay {
@ -47,28 +290,57 @@ impl TryFrom<String> for RegionDisplay {
static ref REGION_REGEX: Regex = Regex::new(r"([A-Z]+)(\d+):([A-Z]+)(\d+)").unwrap(); static ref REGION_REGEX: Regex = Regex::new(r"([A-Z]+)(\d+):([A-Z]+)(\d+)").unwrap();
} }
if let Some(captures) = REGION_REGEX.captures(&value) { if let Some(captures) = REGION_REGEX.captures(&value) {
if captures.len() != 5 { return Err("Not enough capture groups") } 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_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 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_start: u8 = captures[2]
let row_end: u8 = captures[4].parse::<u8>().or(Err("Row end failed to parse"))?; .parse::<u8>()
.or(Err("Row start failed to parse"))?;
let row_end: u8 = captures[4]
.parse::<u8>()
.or(Err("Row end failed to parse"))?;
return Ok(RegionDisplay { return Ok(RegionDisplay {
text: value, text: value,
col_start, col_start,
row_start, row_start,
col_end, col_end,
row_end, row_end,
}) });
} else { } else {
return Err("Regex match failed") return Err("Regex match failed");
} }
} }
} }
impl TryFrom<(u8,u8,u8,u8)> for RegionDisplay { impl From<&Region> for RegionDisplay {
type Error = &'static str; 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(),
}
}
}
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> { fn try_from(value: (u8, u8, u8, u8)) -> Result<Self, Self::Error> {
// (Column Start, Row Start, Column End, Row End) // (Column Start, Row Start, Column End, Row End)
// This can only possibly fail if one of the coordinates is zero... // 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 cs = num_to_letters(value.0).ok_or("Column start failed to parse")?;
@ -86,15 +358,19 @@ fn letters_to_num(letters: &str) -> Option<u8> {
let mut num: u8 = 0; let mut num: u8 = 0;
for (i, letter) in letters.chars().rev().enumerate() { for (i, letter) in letters.chars().rev().enumerate() {
let n = letter as u8; let n = letter as u8;
if n < 65 || n > 90 { return None } if n < 65 || n > 90 {
num = num.checked_add((26_i32.pow(i as u32)*(n as i32 - 64)).try_into().ok()?)?; return None;
}
num = num.checked_add((26_i32.pow(i as u32) * (n as i32 - 64)).try_into().ok()?)?;
} }
return Some(num) return Some(num);
} }
fn num_to_letters(num: u8) -> Option<String> { fn num_to_letters(num: u8) -> Option<String> {
if num == 0 { return None } // Otherwise, we will not return none! if num == 0 {
// As another note, we can't represent higher than "IV" anyway; return None;
// thus there's no reason for a loop (26^n with n>1 will NOT occur). } // 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 text = "".to_string();
let mut digit1 = num.div_euclid(26u8); let mut digit1 = num.div_euclid(26u8);
let mut digit2 = num.rem_euclid(26u8); let mut digit2 = num.rem_euclid(26u8);
@ -103,26 +379,30 @@ fn num_to_letters(num: u8) -> Option<String> {
digit2 = 26; digit2 = 26;
} }
if digit1 != 0 { if digit1 != 0 {
text.push((64+digit1) as char) text.push((64 + digit1) as char)
} }
text.push((64+digit2) as char); text.push((64 + digit2) as char);
return Some(text.to_string()); return Some(text.to_string());
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use wasm_bindgen_test::*;
use super::{letters_to_num, num_to_letters, RegionDisplay}; use super::{letters_to_num, num_to_letters, RegionDisplay};
#[test] #[test]
#[wasm_bindgen_test]
fn test_letters_to_num() { fn test_letters_to_num() {
assert_eq!(letters_to_num("D"), Some(4)); assert_eq!(letters_to_num("D"), Some(4));
assert_eq!(letters_to_num("d"), None); assert_eq!(letters_to_num("d"), None);
assert_eq!(letters_to_num("AD"), Some(26+4)); assert_eq!(letters_to_num("AD"), Some(26 + 4));
assert_eq!(letters_to_num("CG"), Some(3*26+7)); assert_eq!(letters_to_num("CG"), Some(3 * 26 + 7));
} }
#[test] #[test]
#[wasm_bindgen_test]
fn test_num_to_letters() { fn test_num_to_letters() {
println!("27 is {:?}", num_to_letters(27)); println!("27 is {:?}", num_to_letters(27));
assert_eq!(num_to_letters(1), Some("A".to_string())); assert_eq!(num_to_letters(1), Some("A".to_string()));
@ -132,22 +412,30 @@ mod tests {
} }
#[test] #[test]
#[wasm_bindgen_test]
fn test_l2n_and_n2l() { fn test_l2n_and_n2l() {
assert_eq!(num_to_letters(letters_to_num("A").unwrap()), Some("A".to_string())); assert_eq!(
assert_eq!(num_to_letters(letters_to_num("BJ").unwrap()), Some("BJ".to_string())); 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 { for i in 1..=255 {
assert_eq!(letters_to_num(&num_to_letters(i as u8).unwrap()), Some(i)); assert_eq!(letters_to_num(&num_to_letters(i as u8).unwrap()), Some(i));
} }
} }
#[test] #[test]
#[wasm_bindgen_test]
fn test_try_from_string_for_regiondisplay() { fn test_try_from_string_for_regiondisplay() {
let desired = RegionDisplay { let desired = RegionDisplay {
text: "A1:E5".to_string(), text: "A1:E5".to_string(),
row_start: 1, row_start: 1,
row_end: 5, row_end: 5,
col_start: 1, col_start: 1,
col_end: 5 col_end: 5,
}; };
assert_eq!(desired, "A1:E5".to_string().try_into().unwrap()); assert_eq!(desired, "A1:E5".to_string().try_into().unwrap());
} }

View File

@ -1,11 +1,264 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
use dioxus::prelude::*;
#[inline_props] use uuid::Uuid;
pub fn Tree(cx: Scope) -> Element { use wasm_bindgen::JsCast;
cx.render(rsx! { use web_sys::{EventTarget, HtmlDialogElement, HtmlElement};
div { use yew::prelude::*;
class: "tree", use yewdux::prelude::*;
}
}) use crate::components::states::{CurrentTransfer, MainState};
use crate::components::transfer_menu::RegionDisplay;
use crate::data::transfer_region::Region;
#[derive(PartialEq, Properties)]
pub struct TreeProps {
pub open_new_plate_callback: Callback<()>,
}
#[function_component]
pub fn Tree(props: &TreeProps) -> Html {
let (main_state, main_dispatch) = use_store::<MainState>();
let (ct_state, ct_dispatch) = use_store::<CurrentTransfer>();
let plate_modal_id: UseStateHandle<Option<Uuid>> = use_state(|| None);
let open_plate_info_callback = {
let plate_menu_id = plate_modal_id.clone();
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) = u128::from_str_radix(li.id().as_str(), 10) {
plate_menu_id.set(Some(Uuid::from_u128(id)));
}
}
})
};
let plate_info_close_callback = {
let plate_menu_id = plate_modal_id.clone();
Callback::from(move |_| {
plate_menu_id.set(None);
})
};
let plate_info_delete_callback = {
let dispatch = main_dispatch.clone();
let plate_menu_id = plate_modal_id.clone();
Callback::from(move |_| {
if let Some(id) = *plate_menu_id {
dispatch.reduce_mut(|state| {
state.del_plate(id);
});
}
})
};
let source_plate_select_callback = {
let main_dispatch = main_dispatch.clone();
let ct_dispatch = ct_dispatch.clone();
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) = u128::from_str_radix(li.id().as_str(), 10) {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.source_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 main_dispatch = main_dispatch.clone();
let ct_dispatch = ct_dispatch.clone();
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) = u128::from_str_radix(li.id().as_str(), 10) {
ct_dispatch.reduce_mut(|state| {
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 main_state = main_state.clone();
let main_dispatch = main_dispatch.clone();
let ct_dispatch = ct_dispatch.clone();
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) = u128::from_str_radix(li.id().as_str(), 10) {
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
.source_plates
.iter()
.map(|spi| {
html! { <li id={spi.get_uuid().as_u128().to_string()}
ondblclick={open_plate_info_callback.clone()}
onclick={source_plate_select_callback.clone()}
class={classes!(
if spi.get_uuid() == main_state.selected_source_plate {Some("selected")}
else {None}
)}>
{String::from(spi)}
</li> }
})
.collect::<Html>();
let dest_plates = main_state
.destination_plates
.iter()
.map(|dpi| {
html! { <li id={dpi.get_uuid().as_u128().to_string()}
ondblclick={open_plate_info_callback.clone()}
onclick={destination_plate_select_callback.clone()}
class={classes!(
if dpi.get_uuid() == main_state.selected_dest_plate {Some("selected")}
else {None}
)}> {String::from(dpi)} </li> }
})
.collect::<Html>();
let transfers = main_state
.transfers
.iter()
.map(|transfer| {
html! { <li id={transfer.get_uuid().as_u128().to_string()}
onclick={transfer_select_callback.clone()}
class={classes!(
if transfer.get_uuid() == main_state.selected_transfer {Some("selected")}
else {None})}>
{transfer.name.clone()}
</li>
}
})
.collect::<Html>();
html! {
<div class="tree">
<div id="source-plates">
<h3>{"Source Plates:"}</h3>
<ul>
{source_plates}
</ul>
</div>
<div id="destination-plates">
<h3>{"Destination Plates:"}</h3>
<ul>
{dest_plates}
</ul>
</div>
<div id="transfers">
<h3>{"Transfers:"}</h3>
<ul>
{transfers}
</ul>
</div>
if let Some(id) = *plate_modal_id {
<PlateInfoModal id={id} dialog_close_callback={plate_info_close_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>
}
}
#[derive(PartialEq, Properties)]
struct PlateInfoModalProps {
id: Uuid,
dialog_close_callback: Callback<()>,
delete_button_callback: Callback<()>,
}
#[function_component]
fn PlateInfoModal(props: &PlateInfoModalProps) -> Html {
let (state, dispatch) = use_store::<MainState>();
let dialog_ref = use_node_ref();
let mut plate = state
.source_plates
.iter()
.find(|spi| spi.get_uuid() == props.id);
if plate == None {
plate = state
.destination_plates
.iter()
.find(|dpi| dpi.get_uuid() == props.id);
}
let plate_name = match plate {
Some(plate) => plate.name.clone(),
None => "Not Found".to_string(),
};
let onclose = {
let dialog_close_callback = props.dialog_close_callback.clone();
move |_| dialog_close_callback.emit(())
};
let delete_onclick = {
let delete_button_callback = props.delete_button_callback.clone();
let dialog_ref = dialog_ref.clone();
move |_| {
delete_button_callback.emit(());
dialog_ref.cast::<HtmlDialogElement>().unwrap().close();
}
};
{
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" onclose={onclose}>
<h2>{"Plate Info"}</h2>
<h3>{"Name: "}<input type="text" value={plate_name} /></h3>
<button onclick={delete_onclick}>{"Delete"}</button>
</dialog>
}
} }

View File

@ -1,2 +1,4 @@
pub mod plate; pub mod plate;
pub mod plate_instances;
pub mod transfer;
pub mod transfer_region; pub mod transfer_region;

View File

@ -1,3 +1,6 @@
use serde::{Deserialize, Serialize};
#[derive(PartialEq, Eq, Default, Clone, Copy, Serialize, Deserialize, Debug)]
pub struct Plate { pub struct Plate {
pub plate_type: PlateType, pub plate_type: PlateType,
pub plate_format: PlateFormat, pub plate_format: PlateFormat,
@ -16,11 +19,18 @@ impl Plate {
} }
} }
#[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Debug)]
pub enum PlateType { pub enum PlateType {
Source, Source,
Destination, Destination,
} }
impl Default for PlateType {
fn default() -> Self {
Self::Source
}
}
#[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Debug)]
pub enum PlateFormat { pub enum PlateFormat {
W6, W6,
W12, W12,
@ -31,9 +41,40 @@ pub enum PlateFormat {
W1536, W1536,
W3456, W3456,
} }
impl Default for PlateFormat {
fn default() -> Self {
Self::W96
}
}
impl std::fmt::Display for PlateFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PlateFormat::W6 => write!(f, "6"),
PlateFormat::W12 => write!(f, "12"),
PlateFormat::W24 => write!(f, "24"),
PlateFormat::W48 => write!(f, "48"),
PlateFormat::W96 => write!(f, "96"),
PlateFormat::W384 => write!(f, "384"),
PlateFormat::W1536 => write!(f, "1536"),
PlateFormat::W3456 => write!(f, "3456"),
}
}
}
impl PlateFormat { impl PlateFormat {
pub fn size(&self) -> (u8, u8) { pub fn size(&self) -> (u8, u8) {
/*
match self {
PlateFormat::W6 => (3, 2),
PlateFormat::W12 => (4, 3),
PlateFormat::W24 => (6, 4),
PlateFormat::W48 => (8, 6),
PlateFormat::W96 => (12, 8),
PlateFormat::W384 => (24, 16),
PlateFormat::W1536 => (48, 32),
PlateFormat::W3456 => (72, 48),
}
*/
match self { match self {
PlateFormat::W6 => (2, 3), PlateFormat::W6 => (2, 3),
PlateFormat::W12 => (3, 4), PlateFormat::W12 => (3, 4),
@ -46,10 +87,3 @@ impl PlateFormat {
} }
} }
} }
/*
#[cfg(test)]
mod tests {
use super::{Plate, PlateFormat, PlateType};
}
*/

View File

@ -0,0 +1,41 @@
use super::plate::*;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(PartialEq, Clone, Serialize, Deserialize)]
pub struct PlateInstance {
pub plate: Plate,
id: Uuid,
pub name: String,
}
impl PlateInstance {
pub fn new(sort: PlateType, format: PlateFormat, name: String) -> Self {
PlateInstance {
plate: Plate {
plate_type: sort,
plate_format: format,
},
id: Uuid::new_v4(),
name,
}
}
pub fn get_uuid(&self) -> Uuid {
self.id
}
pub fn change_name(&mut self, new_name: String) {
self.name = new_name;
}
}
impl From<Plate> for PlateInstance {
fn from(value: Plate) -> Self {
PlateInstance {
plate: value,
id: Uuid::new_v4(),
name: "New Plate".to_string(),
}
}
}

35
src/data/transfer.rs Normal file
View File

@ -0,0 +1,35 @@
use super::plate_instances::*;
use super::transfer_region::*;
use serde::Deserialize;
use serde::Serialize;
use uuid::Uuid;
#[derive(PartialEq, Clone, Default, Debug, Serialize, Deserialize)]
pub struct Transfer {
pub source_id: Uuid,
pub dest_id: Uuid,
pub name: String,
id: Uuid,
pub transfer_region: TransferRegion,
}
impl Transfer {
pub fn new(
source: PlateInstance,
dest: PlateInstance,
tr: TransferRegion,
name: String,
) -> Self {
Self {
source_id: source.get_uuid(),
dest_id: dest.get_uuid(),
name,
id: Uuid::new_v4(),
transfer_region: tr,
}
}
pub fn get_uuid(&self) -> Uuid {
self.id
}
}

View File

@ -1,10 +1,17 @@
use serde::{Deserialize, Serialize};
use super::plate::Plate; use super::plate::Plate;
#[derive(Clone, Copy)] #[derive(Clone, Copy, 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)),
} }
impl Default for Region {
fn default() -> Self {
Region::Point((1, 1))
}
}
impl TryFrom<Region> for ((u8, u8), (u8, u8)) { 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> {
@ -17,51 +24,71 @@ impl TryFrom<Region> for ((u8, u8), (u8, u8)) {
} }
} }
pub struct TransferRegion<'a> { #[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Debug)]
pub source_plate: &'a Plate, pub struct TransferRegion {
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.
pub dest_plate: &'a Plate, pub dest_plate: Plate,
pub dest_region: Region, pub dest_region: Region,
pub interleave_source: Option<(i8, i8)>, pub interleave_source: (i8, i8),
pub interleave_dest: Option<(i8, i8)>, pub interleave_dest: (i8, i8),
} }
impl TransferRegion<'_> { impl Default for TransferRegion {
pub fn get_source_wells(&self) -> Vec<(u8, u8)> { fn default() -> Self {
if let Region::Rect(c1, c2) = self.source_region { TransferRegion {
let mut wells = Vec::<(u8, u8)>::new(); source_plate: Plate::default(),
let (ul, br) = standardize_rectangle(&c1, &c2); source_region: Region::default(),
let (interleave_i, interleave_j) = self.interleave_source.unwrap_or((1, 1)); dest_plate: Plate::default(),
// NOTE: This will panic if either is 0! dest_region: Region::default(),
// We'll reassign these values (still not mutable) just in case. interleave_source: (1, 1),
// This behaviour shouldn't be replicated for destination wells interleave_dest: (1, 1),
// because a zero step permits pooling.
let (interleave_i, interleave_j) = (i8::max(interleave_i, 1), i8::max(interleave_j, 1));
for i in (ul.0..=br.0).step_by(i8::abs(interleave_i) as usize) {
for j in (ul.1..=br.1).step_by(i8::abs(interleave_j) as usize) {
// NOTE: It looks like we're ignoring negative interleaves,
// because it wouldn't make a difference here---the same
// wells will still be involved in the transfer.
wells.push((i, j))
}
}
return wells;
} else {
panic!("Source region is just a point!")
} }
} }
}
impl TransferRegion {
pub fn get_source_wells(&self) -> Vec<(u8, u8)> {
match self.source_region {
Region::Rect(c1, c2) => {
let mut wells = Vec::<(u8, u8)>::new();
let (ul, br) = standardize_rectangle(&c1, &c2);
let (interleave_i, interleave_j) = self.interleave_source;
// NOTE: This will panic if either is 0!
// We'll reassign these values (still not mutable) just in case.
// This behaviour shouldn't be replicated for destination wells
// because a zero step permits pooling.
let (interleave_i, interleave_j) =
(i8::max(interleave_i, 1), i8::max(interleave_j, 1));
for i in (ul.0..=br.0).step_by(i8::abs(interleave_i) as usize) {
for j in (ul.1..=br.1).step_by(i8::abs(interleave_j) as usize) {
// NOTE: It looks like we're ignoring negative interleaves,
// because it wouldn't make a difference here---the same
// wells will still be involved in the transfer.
wells.push((i, j))
}
}
return wells;
}
Region::Point(p) => return vec![p],
}
}
pub fn get_destination_wells(&self) -> Vec<(u8, u8)> { pub fn get_destination_wells(&self) -> Vec<(u8, u8)> {
let map = self.calculate_map(); let map = self.calculate_map();
let source_wells = self.get_source_wells(); let source_wells = self.get_source_wells();
let mut wells = Vec::<(u8, u8)>::new(); let mut wells = Vec::<(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.");
return wells; return wells;
} }
@ -69,20 +96,22 @@ impl TransferRegion<'_> {
pub fn calculate_map(&self) -> Box<dyn Fn((u8, u8)) -> Option<Vec<(u8, u8)>> + '_> { 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());
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);
let source_wells = self.get_source_wells(); let source_wells = self.get_source_wells();
let il_dest = self.interleave_dest.unwrap_or((1, 1)); let il_dest = self.interleave_dest;
let il_source = self.interleave_source.unwrap_or((1, 1)); let il_source = self.interleave_source;
let source_corners: ((u8, u8), (u8, u8)) = self let source_corners: ((u8, u8), (u8, u8)) = match self.source_region {
.source_region Region::Point((x, y)) => ((x, y), (x, y)),
.try_into() Region::Rect(c1, c2) => (c1, c2),
.expect("Source region should not be a point"); };
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,
// but we will have these properties in certain cases. // but we will have these properties in certain cases.
@ -116,50 +145,80 @@ impl TransferRegion<'_> {
} }
Region::Rect(c1, c2) => { Region::Rect(c1, c2) => {
return Box::new(move |(i, j)| { return Box::new(move |(i, j)| {
// Because of our call to validate,
// we can assume that our destination region contains
// an integer number of our source regions.
if source_wells.contains(&(i, j)) { if source_wells.contains(&(i, j)) {
// Find points by checking congruence class
let possible_destination_wells = create_dense_rectangle(&c1, &c2); let possible_destination_wells = create_dense_rectangle(&c1, &c2);
let (ds1, _) = standardize_rectangle(&c1, &c2); let (d_ul, d_br) = standardize_rectangle(&c1, &c2);
let (s1, s2) = standardize_rectangle(&source_corners.0, &source_corners.1); let (s_ul, s_br) =
let dims = ( standardize_rectangle(&source_corners.0, &source_corners.1);
s2.0.checked_sub(s1.0).unwrap() + 1, let s_dims = (
s2.1.checked_sub(s1.1).unwrap() + 1, s_br.0.checked_sub(s_ul.0).unwrap() + 1,
s_br.1.checked_sub(s_ul.1).unwrap() + 1,
); );
let d_dims = (
/* d_br.0.checked_sub(d_ul.0).unwrap() + 1,
println!("{} % {} == {}", i, dims.0, i*il_dest.0.abs() as u8 % dims.0); d_br.1.checked_sub(d_ul.1).unwrap() + 1,
for a in ds1.0..=ds2.0 { );
for b in ds1.1..=ds2.1 { let N_s = (
println!("({},{}): {} % {} == {}", a, b, // Number of used source wells
a.checked_sub(ds1.0).unwrap()+1, dims.0, (s_dims.0 + il_source.0.abs() as u8 - 1)
(a.checked_sub(ds1.0).unwrap()+1) % dims.0); .div_euclid(il_source.0.abs() as u8),
} (s_dims.1 + il_source.1.abs() as u8 - 1)
} .div_euclid(il_source.1.abs() as u8),
for a in ds1.0..=ds2.0 { );
for b in ds1.1..=ds2.1 { let D_per_replicate = (
println!("({},{}): {} % {} == {}", a, b, // How many wells are used per replicate?
a.checked_sub(ds1.0).unwrap()+1, il_dest.0.abs() as u8, (N_s.0 * (il_dest.0.abs() as u8)),
(a.checked_sub(ds1.0).unwrap()+1) % il_dest.0.abs() as u8); (N_s.1 * (il_dest.1.abs() as u8)),
} );
} let count = (
*/ // How many times can we replicate?
(1..)
.position(|n| {
n * N_s.0 * il_dest.0.abs() as u8 - il_dest.0.abs() as u8 + 1
> d_dims.0
})
.unwrap() as u8,
(1..)
.position(|n| {
n * N_s.1 * il_dest.1.abs() as u8 - il_dest.1.abs() as u8 + 1
> d_dims.1
})
.unwrap() as u8,
);
let i = i
.saturating_sub(s_ul.0)
.saturating_div(il_source.0.abs() as u8);
let j = j
.saturating_sub(s_ul.1)
.saturating_div(il_source.1.abs() as u8);
// log::debug!("N_s: {:?}, d_dims: {:?}, D_per: {:?}, count: {:?}", N_s, d_dims, D_per_replicate, count);
Some( Some(
possible_destination_wells possible_destination_wells
.into_iter() .into_iter()
.filter(|(x, y)| { .filter(|(x, _)| {
i * il_dest.0.abs() as u8 % dims.0 x.checked_sub(d_ul.0).unwrap()
== (x.checked_sub(ds1.0).unwrap() + 1) % dims.0 % (N_s.0 * il_dest.0.abs() as u8) // Counter along x
&& j * il_dest.1.abs() as u8 % dims.1 == ((il_dest.0.abs() as u8 *i))
== (y.checked_sub(ds1.1).unwrap() + 1) % dims.1 % (N_s.0 * il_dest.0.abs() as u8)
})
.filter(|(_, y)| {
y.checked_sub(d_ul.1).unwrap()
% (N_s.1 * il_dest.1.abs() as u8) // Counter along u
== ((il_dest.1.abs() as u8 *j))
% (N_s.1 * il_dest.1.abs() as u8)
}) })
.filter(|(x, y)| { .filter(|(x, y)| {
(x.checked_sub(ds1.0).unwrap()) % il_dest.0.abs() as u8 == 0 // How many times have we replicated? < How many are we allowed
&& (y.checked_sub(ds1.1).unwrap()) % il_dest.1.abs() as u8 // to replicate?
== 0 x.checked_sub(d_ul.0)
.unwrap()
.div_euclid(N_s.0 * il_dest.0.abs() as u8)
< count.0
&& y.checked_sub(d_ul.1)
.unwrap()
.div_euclid(N_s.1 * il_dest.1.abs() as u8)
< count.1
}) })
.collect(), .collect(),
) )
@ -180,11 +239,12 @@ impl TransferRegion<'_> {
// - Are the wells in the source really there? // - Are the wells in the source really there?
// - In a replication region, do the source lengths divide the destination lengths? // - In a replication region, do the source lengths divide the destination lengths?
// - Are the interleaves valid? // - Are the interleaves valid?
let il_source = self.interleave_source.unwrap_or((1, 1)); let il_source = self.interleave_source;
let il_dest = self.interleave_dest.unwrap_or((1, 1)); let il_dest = self.interleave_dest;
match self.source_region { match self.source_region {
Region::Point(_) => return Err("Source region should not be a point!"), Region::Point(_) => return Ok(()), // Should make sure it's actually in the plate, leave for
// later
Region::Rect(s1, s2) => { Region::Rect(s1, s2) => {
// Check if all source wells exist: // Check if all source wells exist:
if s1.0 == 0 || s1.1 == 0 || s2.0 == 0 || s2.1 == 0 { if s1.0 == 0 || s1.1 == 0 || s2.0 == 0 || s2.1 == 0 {
@ -196,6 +256,7 @@ impl TransferRegion<'_> {
return Err("Source region is out-of-bounds! (Too tall)"); return Err("Source region is out-of-bounds! (Too tall)");
} }
if s1.1 > source_max.1 || s2.1 > source_max.1 { if s1.1 > source_max.1 || s2.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)");
} }
// Check that source lengths divide destination lengths // Check that source lengths divide destination lengths
@ -235,10 +296,8 @@ impl TransferRegion<'_> {
} }
} }
if let Some(source_il) = self.interleave_source { if il_source.0 == 0 || il_dest.1 == 0 {
if source_il.0 == 0 || source_il.1 == 0 { return Err("Source interleave cannot be zero!");
return Err("Source interleave cannot be zero!");
}
} }
// Check if all destination wells exist: // Check if all destination wells exist:
@ -292,7 +351,7 @@ use std::fmt;
use std::ops::Mul; use std::ops::Mul;
#[cfg(debug_assertions)] // There should be no reason to print a transfer otherwise #[cfg(debug_assertions)] // There should be no reason to print a transfer otherwise
impl fmt::Display for TransferRegion<'_> { impl fmt::Display for TransferRegion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Source Plate:")?; writeln!(f, "Source Plate:")?;
let source_dims = self.source_plate.size(); let source_dims = self.source_plate.size();
@ -330,75 +389,131 @@ impl fmt::Display for TransferRegion<'_> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use wasm_bindgen_test::*;
use crate::data::plate::*; use crate::data::plate::*;
use crate::data::transfer_region::*; 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((1, 1), (3, 3)), source_region: Region::Rect((1, 1), (3, 3)),
dest_plate: &destination, dest_plate: destination,
dest_region: Region::Point((3,3)), dest_region: Region::Point((3, 3)),
interleave_source: None, interleave_source: (1, 1),
interleave_dest: None, interleave_dest: (1, 1),
}; };
let transfer1_map = transfer1.calculate_map(); let transfer1_map = transfer1.calculate_map();
assert_eq!(transfer1_map((1,1)), Some(vec!{(3,3)}), "Failed basic shift transfer 1"); assert_eq!(
assert_eq!(transfer1_map((1,2)), Some(vec!{(3,4)}), "Failed basic shift transfer 2"); transfer1_map((1, 1)),
assert_eq!(transfer1_map((2,2)), Some(vec!{(4,4)}), "Failed basic shift transfer 3"); Some(vec! {(3,3)}),
"Failed basic shift transfer 1"
);
assert_eq!(
transfer1_map((1, 2)),
Some(vec! {(3,4)}),
"Failed basic shift transfer 2"
);
assert_eq!(
transfer1_map((2, 2)),
Some(vec! {(4,4)}),
"Failed basic shift transfer 3"
);
let transfer2 = TransferRegion { let transfer2 = TransferRegion {
source_plate: &source, source_plate: source,
source_region: Region::Rect((1, 1), (3, 3)), source_region: Region::Rect((1, 1), (3, 3)),
dest_plate: &destination, dest_plate: destination,
dest_region: Region::Point((3,3)), dest_region: Region::Point((3, 3)),
interleave_source: Some((2,2)), interleave_source: (2, 2),
interleave_dest: None, interleave_dest: (1, 1),
}; };
let transfer2_map = transfer2.calculate_map(); let transfer2_map = transfer2.calculate_map();
assert_eq!(transfer2_map((1,1)), Some(vec!{(3,3)}), "Failed source interleave, type simple 1"); assert_eq!(
assert_eq!(transfer2_map((1,2)), None, "Failed source interleave, type simple 2"); transfer2_map((1, 1)),
assert_eq!(transfer2_map((2,2)), None, "Failed source interleave, type simple 3"); Some(vec! {(3,3)}),
assert_eq!(transfer2_map((3,3)), Some(vec!{(4,4)}), "Failed source interleave, type simple 4"); "Failed source interleave, type simple 1"
);
assert_eq!(
transfer2_map((1, 2)),
None,
"Failed source interleave, type simple 2"
);
assert_eq!(
transfer2_map((2, 2)),
None,
"Failed source interleave, type simple 3"
);
assert_eq!(
transfer2_map((3, 3)),
Some(vec! {(4,4)}),
"Failed source interleave, type simple 4"
);
let transfer3 = TransferRegion { let transfer3 = TransferRegion {
source_plate: &source, source_plate: source,
source_region: Region::Rect((1, 1), (3, 3)), source_region: Region::Rect((1, 1), (3, 3)),
dest_plate: &destination, dest_plate: destination,
dest_region: Region::Point((3,3)), dest_region: Region::Point((3, 3)),
interleave_source: None, interleave_source: (1, 1),
interleave_dest: Some((2,3)), interleave_dest: (2, 3),
}; };
let transfer3_map = transfer3.calculate_map(); let transfer3_map = transfer3.calculate_map();
assert_eq!(transfer3_map((1,1)), Some(vec!{(3,3)}), "Failed destination interleave, type simple 1"); assert_eq!(
assert_eq!(transfer3_map((2,1)), Some(vec!{(5,3)}), "Failed destination interleave, type simple 2"); transfer3_map((1, 1)),
assert_eq!(transfer3_map((1,2)), Some(vec!{(3,6)}), "Failed destination interleave, type simple 3"); Some(vec! {(3,3)}),
assert_eq!(transfer3_map((2,2)), Some(vec!{(5,6)}), "Failed destination interleave, type simple 4"); "Failed destination interleave, type simple 1"
);
assert_eq!(
transfer3_map((2, 1)),
Some(vec! {(5,3)}),
"Failed destination interleave, type simple 2"
);
assert_eq!(
transfer3_map((1, 2)),
Some(vec! {(3,6)}),
"Failed destination interleave, type simple 3"
);
assert_eq!(
transfer3_map((2, 2)),
Some(vec! {(5,6)}),
"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((1, 1), (2, 2)), source_region: Region::Rect((1, 1), (2, 2)),
dest_plate: &destination, dest_plate: destination,
dest_region: Region::Rect((2,2),(11,11)), dest_region: Region::Rect((2, 2), (11, 11)),
interleave_source: None, interleave_source: (1, 1),
interleave_dest: Some((3,3)), interleave_dest: (3, 3),
}; };
let transfer1_map = transfer1.calculate_map(); let transfer1_map = transfer1.calculate_map();
assert_eq!(transfer1_map((1,1)), Some(vec!{(2, 2), (2, 8), (8, 2), (8, 8)}), "Failed type replicate 1"); assert_eq!(
assert_eq!(transfer1_map((2,1)), Some(vec!{(5, 2), (5, 8), (11, 2), (11, 8)}), "Failed type replicate 1"); transfer1_map((1, 1)),
Some(vec! {(2, 2), (2, 8), (8, 2), (8, 8)}),
"Failed type replicate 1"
);
assert_eq!(
transfer1_map((2, 1)),
Some(vec! {(5, 2), (5, 8), (11, 2), (11, 8)}),
"Failed type replicate 1"
);
} }
#[test] #[test]
fn test_pooling_transfer() { #[wasm_bindgen_test]
} fn test_pooling_transfer() {}
} }

View File

@ -3,17 +3,16 @@ mod components;
mod data; mod data;
use components::main_window::MainWindow; use components::main_window::MainWindow;
use dioxus::prelude::*; use yew::prelude::*;
use fermi::*;
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
use data::*; use data::*;
pub fn App(cx: Scope) -> Element { #[function_component]
use_init_atom_root(cx); pub fn App() -> Html {
cx.render(rsx! { html! {
MainWindow {} <MainWindow />
}) }
} }
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
@ -22,17 +21,17 @@ pub fn plate_test() {
let destination = plate::Plate::new(plate::PlateType::Destination, plate::PlateFormat::W384); let destination = plate::Plate::new(plate::PlateType::Destination, plate::PlateFormat::W384);
let transfer = transfer_region::TransferRegion { let transfer = transfer_region::TransferRegion {
source_plate: &source, source_plate: source,
source_region: transfer_region::Region::Rect((1, 1), (2, 2)), source_region: transfer_region::Region::Rect((1, 1), (2, 2)),
dest_plate: &destination, dest_plate: destination,
dest_region: transfer_region::Region::Rect((2,2),(11,11)), dest_region: transfer_region::Region::Rect((2, 2), (11, 11)),
interleave_source: None, interleave_source: (1, 1),
interleave_dest: Some((3,3)), interleave_dest: (3, 3),
}; };
println!("{}", transfer); println!("{}", transfer);
let sws = transfer.get_source_wells(); let sws = transfer.get_source_wells();
let m = transfer.calculate_map(); let m = transfer.calculate_map();
for w in sws { for w in sws {
println!("{:?} -> {:?}", w,m(w)); println!("{:?} -> {:?}", w, m(w));
} }
} }

View File

@ -3,9 +3,10 @@ use plate_tool::plate_test;
use plate_tool::App; use plate_tool::App;
use wasm_logger; use wasm_logger;
use yew::prelude::*;
fn main() { fn main() {
wasm_logger::init(wasm_logger::Config::default()); wasm_logger::init(wasm_logger::Config::default());
dioxus_web::launch(App); yew::Renderer::<App>::new().render();
//plate_test(); //plate_test();
} }