commit
09d99e27a0
|
@ -0,0 +1,2 @@
|
||||||
|
[build]
|
||||||
|
target = "wasm32-unknown-unknown"
|
File diff suppressed because it is too large
Load Diff
12
Cargo.toml
12
Cargo.toml
|
@ -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"
|
||||||
|
|
46
Dioxus.toml
46
Dioxus.toml
|
@ -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 }
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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.
Binary file not shown.
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
@forward "main_window";
|
||||||
|
@forward "plate_container";
|
||||||
|
@forward "plates";
|
||||||
|
@forward "tree";
|
||||||
|
@forward "transfer_menu";
|
|
@ -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);
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;}
|
|
@ -0,0 +1,2 @@
|
||||||
|
@use "default_theme/main";
|
||||||
|
@use "default_theme/variables" as *;
|
|
@ -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>
|
|
@ -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>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn DestinationPlate(props: &DestinationPlateProps) -> Html {
|
||||||
|
let (ct_state, ct_dispatch) = use_store::<CurrentTransfer>();
|
||||||
|
let m_start_handle: UseStateHandle<Option<(u8, u8)>> = use_state_eq(|| None);
|
||||||
|
let m_end_handle: UseStateHandle<Option<(u8, u8)>> = use_state_eq(|| None);
|
||||||
|
let m_stat_handle: UseStateHandle<bool> = use_state_eq(|| false);
|
||||||
|
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)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
#[inline_props]
|
|
||||||
fn DestPlateCell(cx: Scope<PlateCellProps>, i: u8, j: u8, color: Option<String>) -> Element {
|
|
||||||
let color_string = match color {
|
|
||||||
Some(c) => c.clone(),
|
|
||||||
None => "None".to_string(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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 {
|
||||||
|
pub source_dims: Option<PlateInstance>,
|
||||||
|
pub destination_dims: Option<PlateInstance>,
|
||||||
|
}
|
||||||
|
|
||||||
#[inline_props]
|
#[function_component]
|
||||||
pub fn PlateContainer(cx: Scope, source_dims: (u8,u8), destination_dims: (u8,u8)) -> Element {
|
pub fn PlateContainer(props: &PlateContainerProps) -> Html {
|
||||||
cx.render(rsx! {
|
html! {
|
||||||
style { STYLE }
|
<div class="plate_container">
|
||||||
div {
|
if let Some(spi) = props.source_dims.clone() {
|
||||||
class: "plate_container",
|
if let Some(dpi) = props.destination_dims.clone() {
|
||||||
SourcePlate {width: source_dims.0,
|
<div>
|
||||||
height: source_dims.1},
|
<h2>{spi.name.clone()}</h2>
|
||||||
DestinationPlate {width: destination_dims.0,
|
<SourcePlate source_plate={spi.clone()} destination_plate={dpi.clone()} />
|
||||||
height: destination_dims.1}
|
</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>
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
if !(*m_stat_handle) {
|
||||||
|
let (pt1, pt2) = match ct_state.transfer.transfer_region.source_region {
|
||||||
|
Region::Point((x, y)) => ((x, y), (x, y)),
|
||||||
|
Region::Rect(c1, c2) => (c1, c2),
|
||||||
|
};
|
||||||
|
m_start_handle.set(Some(pt1));
|
||||||
|
m_end_handle.set(Some(pt2));
|
||||||
|
}
|
||||||
|
|
||||||
|
let source_wells = ct_state.transfer.transfer_region.get_source_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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.render(rsx! {
|
|
||||||
div{
|
|
||||||
class: "source_plate",
|
|
||||||
style { STYLE }
|
|
||||||
table {
|
|
||||||
draggable: "false",
|
|
||||||
for i in 1..=cx.props.height {
|
|
||||||
tr {
|
|
||||||
draggable: "false",
|
|
||||||
for j in 1..=cx.props.width {
|
|
||||||
SourcePlateCell {i: i, j: j}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
#[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 {
|
|
||||||
selection_state.write().m_end = Some((*i,*j))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onmouseup: move |_| {
|
|
||||||
selection_state.write().m_stat = false
|
|
||||||
},
|
|
||||||
div {
|
|
||||||
class: "plate_cell_inner"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.collect::<Html>();
|
||||||
|
html! {
|
||||||
|
<tr>
|
||||||
|
{ row }
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.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);
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,23 +290,52 @@ 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 From<&Region> for RegionDisplay {
|
||||||
|
fn from(value: &Region) -> Self {
|
||||||
|
match *value {
|
||||||
|
Region::Point((col, row)) => {
|
||||||
|
RegionDisplay::try_from((col, row, col, row)).ok().unwrap()
|
||||||
|
}
|
||||||
|
Region::Rect(c1, c2) => RegionDisplay::try_from((c1.0, c1.1, c2.0, c2.1))
|
||||||
|
.ok()
|
||||||
|
.unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 {
|
impl TryFrom<(u8, u8, u8, u8)> for RegionDisplay {
|
||||||
type Error = &'static str;
|
type Error = &'static str;
|
||||||
|
@ -86,13 +358,17 @@ 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 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
num = num.checked_add((26_i32.pow(i as u32) * (n as i32 - 64)).try_into().ok()?)?;
|
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 {
|
||||||
|
return None;
|
||||||
|
} // Otherwise, we will not return none!
|
||||||
// As another note, we can't represent higher than "IV" anyway;
|
// 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).
|
// 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();
|
||||||
|
@ -112,9 +388,12 @@ fn num_to_letters(num: u8) -> Option<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);
|
||||||
|
@ -123,6 +402,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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};
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
|
@ -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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,26 +24,42 @@ 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 {
|
||||||
|
fn default() -> Self {
|
||||||
|
TransferRegion {
|
||||||
|
source_plate: Plate::default(),
|
||||||
|
source_region: Region::default(),
|
||||||
|
dest_plate: Plate::default(),
|
||||||
|
dest_region: Region::default(),
|
||||||
|
interleave_source: (1, 1),
|
||||||
|
interleave_dest: (1, 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransferRegion {
|
||||||
pub fn get_source_wells(&self) -> Vec<(u8, u8)> {
|
pub fn get_source_wells(&self) -> Vec<(u8, u8)> {
|
||||||
if let Region::Rect(c1, c2) = self.source_region {
|
match self.source_region {
|
||||||
|
Region::Rect(c1, c2) => {
|
||||||
let mut wells = Vec::<(u8, u8)>::new();
|
let mut wells = Vec::<(u8, u8)>::new();
|
||||||
let (ul, br) = standardize_rectangle(&c1, &c2);
|
let (ul, br) = standardize_rectangle(&c1, &c2);
|
||||||
let (interleave_i, interleave_j) = self.interleave_source.unwrap_or((1, 1));
|
let (interleave_i, interleave_j) = self.interleave_source;
|
||||||
// NOTE: This will panic if either is 0!
|
// NOTE: This will panic if either is 0!
|
||||||
// We'll reassign these values (still not mutable) just in case.
|
// We'll reassign these values (still not mutable) just in case.
|
||||||
// This behaviour shouldn't be replicated for destination wells
|
// This behaviour shouldn't be replicated for destination wells
|
||||||
// because a zero step permits pooling.
|
// because a zero step permits pooling.
|
||||||
let (interleave_i, interleave_j) = (i8::max(interleave_i, 1), i8::max(interleave_j, 1));
|
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 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) {
|
for j in (ul.1..=br.1).step_by(i8::abs(interleave_j) as usize) {
|
||||||
|
@ -47,21 +70,25 @@ impl TransferRegion<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return wells;
|
return wells;
|
||||||
} else {
|
}
|
||||||
panic!("Source region is just a point!")
|
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,11 +296,9 @@ 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:
|
||||||
// NOT IMPLEMENTED
|
// NOT IMPLEMENTED
|
||||||
|
@ -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() {}
|
||||||
}
|
}
|
||||||
|
|
21
src/lib.rs
21
src/lib.rs
|
@ -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,12 +21,12 @@ 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();
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue