Compare commits

..

17 Commits

Author SHA1 Message Date
Emilia Allison e71a72bff9
Merge branch 'dev' into beta-release
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
2024-02-13 20:44:29 -05:00
Emilia Allison 9366d29202
lib: fix stupid typo 2024-02-13 20:32:42 -05:00
Emilia Allison c2a64d679a
web: auto button 2024-02-13 20:27:58 -05:00
Emilia Allison 1aa4c1b7bb
web: use new utility function in callback 2024-02-13 19:58:14 -05:00
Emilia Allison 6c8533f7a6
lib: auto creation from csv 2024-02-13 19:56:47 -05:00
Emilia Allison d7c98d37a2
Reorg csv in lib 2024-02-13 18:33:42 -05:00
Emilia Allison bfa1fef9d8
update readme 2024-02-13 18:23:22 -05:00
Emilia Allison b79c377793
Change plate format 2024-02-11 22:04:03 -05:00
Emilia Allison 9db0d4aa19
lib migration pt4 2024-02-11 20:49:52 -05:00
Emilia Allison 4d82ea5567
lib migration pt3 2024-02-11 20:03:29 -05:00
Emilia Allison 7d35959ac2
lib migration pt 2 2024-02-11 19:46:43 -05:00
Emilia Allison ffc81f505b
lib migration pt1 2024-02-11 19:32:30 -05:00
Emilia Allison a8b72c0a7a
huge clippy 2024-02-10 22:00:39 -05:00
Emilia Allison 338a7b98c7
the refactor i've wanted most
SourcePlate and DestinationPlate are now the same
why did i not do this originally omg
2024-02-10 21:57:30 -05:00
Emilia Allison 0625854895
More robust CSV parsing
Permit lowercases, alternative column names
2024-02-10 18:00:47 -05:00
Emilia Allison 8a3bbc8b92
Refactor import_csv callbacks 2024-02-10 17:25:04 -05:00
Emilia Allison de42499444
Close buttons on imports
Resolves #3
2024-02-10 17:18:28 -05:00
59 changed files with 1740 additions and 1091 deletions

488
Cargo.lock generated
View File

@ -3,10 +3,25 @@
version = 3
[[package]]
name = "aho-corasick"
version = "1.0.1"
name = "addr2line"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04"
checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
dependencies = [
"gimli",
]
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "aho-corasick"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
dependencies = [
"memchr",
]
@ -25,13 +40,13 @@ checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c"
[[package]]
name = "async-trait"
version = "0.1.68"
version = "0.1.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842"
checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.16",
"syn 2.0.48",
]
[[package]]
@ -46,6 +61,21 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "backtrace"
version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
dependencies = [
"addr2line",
"cc",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
]
[[package]]
name = "bincode"
version = "1.3.3"
@ -63,9 +93,24 @@ checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9"
[[package]]
name = "bumpalo"
version = "3.12.2"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c6ed94e98ecff0c12dd1b04c15ec0d7d9458ca8fe806cea6f12954efe74c63b"
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
[[package]]
name = "bytes"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
[[package]]
name = "cc"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
dependencies = [
"libc",
]
[[package]]
name = "cfg-if"
@ -85,9 +130,9 @@ dependencies = [
[[package]]
name = "csv"
version = "1.2.2"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "626ae34994d3d8d668f4269922248239db4ae42d538b14c398b74a52208e8086"
checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe"
dependencies = [
"csv-core",
"itoa",
@ -97,18 +142,18 @@ dependencies = [
[[package]]
name = "csv-core"
version = "0.1.10"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70"
dependencies = [
"memchr",
]
[[package]]
name = "darling"
version = "0.13.4"
version = "0.20.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c"
checksum = "fc5d6b04b3fd0ba9926f945895de7d806260a2d7431ba82e7edaecb043c4c6b8"
dependencies = [
"darling_core",
"darling_macro",
@ -116,27 +161,27 @@ dependencies = [
[[package]]
name = "darling_core"
version = "0.13.4"
version = "0.20.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610"
checksum = "04e48a959bcd5c761246f5d090ebc2fbf7b9cd527a492b07a67510c108f1e7e3"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 1.0.109",
"syn 2.0.48",
]
[[package]]
name = "darling_macro"
version = "0.13.4"
version = "0.20.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
checksum = "1d1545d67a2149e1d93b7e5c7752dce5a7426eb5d1357ddcfd89336b94444f77"
dependencies = [
"darling_core",
"quote",
"syn 1.0.109",
"syn 2.0.48",
]
[[package]]
@ -147,18 +192,18 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "form_urlencoded"
version = "1.1.0"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
dependencies = [
"percent-encoding",
]
[[package]]
name = "futures"
version = "0.3.28"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
dependencies = [
"futures-channel",
"futures-core",
@ -170,9 +215,9 @@ dependencies = [
[[package]]
name = "futures-channel"
version = "0.3.28"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
dependencies = [
"futures-core",
"futures-sink",
@ -180,44 +225,44 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.28"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
[[package]]
name = "futures-io"
version = "0.3.28"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
[[package]]
name = "futures-macro"
version = "0.3.28"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.16",
"syn 2.0.48",
]
[[package]]
name = "futures-sink"
version = "0.3.28"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
[[package]]
name = "futures-task"
version = "0.3.28"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
[[package]]
name = "futures-util"
version = "0.3.28"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [
"futures-channel",
"futures-core",
@ -233,9 +278,9 @@ dependencies = [
[[package]]
name = "getrandom"
version = "0.2.9"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4"
checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
dependencies = [
"cfg-if",
"js-sys",
@ -245,10 +290,16 @@ dependencies = [
]
[[package]]
name = "gloo"
version = "0.8.0"
name = "gimli"
version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a4bef6b277b3ab073253d4bca60761240cf8d6998f4bd142211957b69a61b20"
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]]
name = "gloo"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28999cda5ef6916ffd33fb4a7b87e1de633c47c0dc6d97905fee1cdaa142b94d"
dependencies = [
"gloo-console",
"gloo-dialogs",
@ -310,9 +361,9 @@ dependencies = [
[[package]]
name = "gloo-history"
version = "0.1.3"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd451019e0b7a2b8a7a7b23e74916601abf1135c54664e57ff71dcc26dfcdeb7"
checksum = "85725d90bf0ed47063b3930ef28e863658a7905989e9929a8708aab74a1d5e7f"
dependencies = [
"gloo-events",
"gloo-utils",
@ -326,14 +377,15 @@ dependencies = [
[[package]]
name = "gloo-net"
version = "0.2.6"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9902a044653b26b99f7e3693a42f171312d9be8b26b5697bd1e43ad1f8a35e10"
checksum = "a66b4e3c7d9ed8d315fd6b97c8b1f74a7c6ecbbc2320e65ae7ed38b7068cc620"
dependencies = [
"futures-channel",
"futures-core",
"futures-sink",
"gloo-utils",
"http",
"js-sys",
"pin-project",
"serde",
@ -381,9 +433,9 @@ dependencies = [
[[package]]
name = "gloo-utils"
version = "0.1.6"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8e8fc851e9c7b9852508bc6e3f690f452f474417e8545ec9857b7f7377036b5"
checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e"
dependencies = [
"js-sys",
"serde",
@ -417,11 +469,19 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hermit-abi"
version = "0.2.6"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
checksum = "d0c62115964e08cb8039170eb33c1d0e2388a256930279edca206fff675f82c3"
[[package]]
name = "http"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb"
dependencies = [
"libc",
"bytes",
"fnv",
"itoa",
]
[[package]]
@ -432,9 +492,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "implicit-clone"
version = "0.3.5"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40fc102e70475c320b185cd18c1e48bba2d7210b63970a4d581ef903e4368ef7"
checksum = "cfd6201e7c30ccb24773cac7efa6fec1e06189d414b7439ce756a481c8bfbf53"
dependencies = [
"indexmap",
]
@ -451,15 +511,15 @@ dependencies = [
[[package]]
name = "itoa"
version = "1.0.6"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
[[package]]
name = "js-sys"
version = "0.3.63"
version = "0.3.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790"
checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee"
dependencies = [
"wasm-bindgen",
]
@ -472,72 +532,87 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.144"
version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "log"
version = "0.4.17"
version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
"cfg-if",
]
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "memchr"
version = "2.5.0"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
[[package]]
name = "miniz_oxide"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
dependencies = [
"adler",
]
[[package]]
name = "num_cpus"
version = "1.15.0"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi",
"libc",
]
[[package]]
name = "once_cell"
version = "1.17.1"
name = "object"
version = "0.32.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "percent-encoding"
version = "2.2.0"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pin-project"
version = "1.1.0"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c95a7476719eab1e366eaf73d0260af3021184f18177925b07f54b30089ceead"
checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.0"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07"
checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.16",
"syn 2.0.48",
]
[[package]]
name = "pin-project-lite"
version = "0.2.9"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
[[package]]
name = "pin-utils"
@ -557,7 +632,22 @@ dependencies = [
]
[[package]]
name = "plate-tool"
name = "plate-tool-lib"
version = "0.1.0"
dependencies = [
"csv",
"getrandom",
"lazy_static",
"log",
"rand",
"regex",
"serde",
"serde_json",
"uuid",
]
[[package]]
name = "plate-tool-web"
version = "0.2.0"
dependencies = [
"csv",
@ -565,6 +655,7 @@ dependencies = [
"js-sys",
"lazy_static",
"log",
"plate-tool-lib",
"rand",
"regex",
"serde",
@ -620,9 +711,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.58"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
"unicode-ident",
]
@ -646,9 +737,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.27"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
@ -685,9 +776,21 @@ dependencies = [
[[package]]
name = "regex"
version = "1.8.1"
version = "1.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370"
checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd"
dependencies = [
"aho-corasick",
"memchr",
@ -696,21 +799,27 @@ dependencies = [
[[package]]
name = "regex-syntax"
version = "0.7.1"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]]
name = "rustc-demangle"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustversion"
version = "1.0.12"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06"
checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
[[package]]
name = "ryu"
version = "1.0.13"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
[[package]]
name = "scoped-tls"
@ -720,18 +829,18 @@ checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "serde"
version = "1.0.163"
version = "1.0.196"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2"
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde-wasm-bindgen"
version = "0.4.5"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3b4c031cd0d9014307d82b8abf653c0290fbdaeb4c02d00c63cf52f728628bf"
checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e"
dependencies = [
"js-sys",
"serde",
@ -740,20 +849,20 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.163"
version = "1.0.196"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e"
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.16",
"syn 2.0.48",
]
[[package]]
name = "serde_json"
version = "1.0.96"
version = "1.0.113"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
dependencies = [
"itoa",
"ryu",
@ -774,9 +883,9 @@ dependencies = [
[[package]]
name = "slab"
version = "0.4.8"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d"
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [
"autocfg",
]
@ -800,9 +909,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.16"
version = "2.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01"
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
dependencies = [
"proc-macro2",
"quote",
@ -811,33 +920,32 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.40"
version = "1.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac"
checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.40"
version = "1.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.16",
"syn 2.0.48",
]
[[package]]
name = "tokio"
version = "1.28.1"
version = "1.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0aa32867d44e6f2ce3385e89dceb990188b8bb0fb25b0cf576647a6f98ac5105"
checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
dependencies = [
"autocfg",
"backtrace",
"pin-project-lite",
"windows-sys",
]
[[package]]
@ -853,11 +961,10 @@ dependencies = [
[[package]]
name = "tracing"
version = "0.1.37"
version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
dependencies = [
"cfg-if",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
@ -865,35 +972,35 @@ dependencies = [
[[package]]
name = "tracing-attributes"
version = "0.1.24"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74"
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.16",
"syn 2.0.48",
]
[[package]]
name = "tracing-core"
version = "0.1.31"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
dependencies = [
"once_cell",
]
[[package]]
name = "unicode-ident"
version = "1.0.8"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "uuid"
version = "1.6.1"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560"
checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a"
dependencies = [
"atomic",
"getrandom",
@ -905,13 +1012,13 @@ dependencies = [
[[package]]
name = "uuid-macro-internal"
version = "1.6.1"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f49e7f3f3db8040a100710a11932239fd30697115e2ba4107080d8252939845e"
checksum = "7abb14ae1a50dad63eaa768a458ef43d298cd1bd44951677bd10b732a9ba2a2d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.16",
"syn 2.0.48",
]
[[package]]
@ -928,9 +1035,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.86"
version = "0.2.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73"
checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
@ -938,24 +1045,24 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.86"
version = "0.2.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb"
checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.16",
"syn 2.0.48",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.36"
version = "0.4.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d1985d03709c53167ce907ff394f5316aa22cb4e12761295c5dc57dacb6297e"
checksum = "877b9c3f61ceea0e56331985743b13f3d25c406a7098d45180fb5f09bc19ed97"
dependencies = [
"cfg-if",
"js-sys",
@ -965,9 +1072,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.86"
version = "0.2.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258"
checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@ -975,28 +1082,28 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.86"
version = "0.2.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8"
checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.16",
"syn 2.0.48",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.86"
version = "0.2.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93"
checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838"
[[package]]
name = "wasm-bindgen-test"
version = "0.3.36"
version = "0.3.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e636f3a428ff62b3742ebc3c70e254dfe12b8c2b469d688ea59cdd4abcf502"
checksum = "143ddeb4f833e2ed0d252e618986e18bfc7b0e52f2d28d77d05b2f045dd8eb61"
dependencies = [
"console_error_panic_hook",
"js-sys",
@ -1008,12 +1115,13 @@ dependencies = [
[[package]]
name = "wasm-bindgen-test-macro"
version = "0.3.36"
version = "0.3.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f18c1fad2f7c4958e7bcce014fa212f59a65d5e3721d0f77e6c0b27ede936ba3"
checksum = "a5211b7550606857312bba1d978a8ec75692eae187becc5e680444fffc5e6f89"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]]
@ -1029,80 +1137,14 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.63"
version = "0.3.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bdd9ef4e984da1187bf8110c5cf5b845fbc87a23602cdf912386a76fcd3a7c2"
checksum = "96565907687f7aceb35bc5fc03770a8a0471d82e479f25832f54a0e3f4b28446"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
[[package]]
name = "windows_i686_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
[[package]]
name = "windows_i686_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "yew"
version = "0.20.0"
@ -1145,9 +1187,9 @@ dependencies = [
[[package]]
name = "yewdux"
version = "0.9.2"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "653ba356bc60d1804c28ec6cc8ddac2741c686bde2a65074d07faba735914464"
checksum = "3c528544a814a0beb30059c8340eb78c88160141db788112b51a5a08098cef2d"
dependencies = [
"anymap",
"async-trait",
@ -1164,13 +1206,13 @@ dependencies = [
[[package]]
name = "yewdux-macros"
version = "0.9.2"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25bcd923aceaa85cb4affad8657cc36e3d6b6932740e711574182f7817492739"
checksum = "d22b1832d3e3eaa61a5c2ecd40affa876507457180d1e599143368c1c3317c2d"
dependencies = [
"darling",
"proc-macro-error",
"proc-macro2",
"quote",
"syn 1.0.109",
"syn 2.0.48",
]

View File

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

View File

@ -78,13 +78,6 @@ To add a new plate, click the "New Plate" button:
Keep in mind that this will overwrite any work you currently have open,
so you may wish to export first (see above).
#### Import Transfer from CSV (Using a picklist as a transfer)
If you have a CSV generated by another tool (or plate-tool),
you can import it as a single transfer.
To do so, mouse over the "File" tab, then "Import", and finally "Import Transfer from CSV".
When creating transfers via this method, the transfer cannot be edited.
This is useful if you have a pre-existing picklist that you would like to visualize in plate-tool.
_Note 1_: JSON files are plaintext!
By default there is little whitespace (this makes comprehending them a challenge)
but if we pass it through a "JSON Beautifier" (enter this into your search engine of choice)
@ -99,6 +92,18 @@ To add a new plate, click the "New Plate" button:
that this application does not "phone home".
Your data is stored locally (unless you choose to export it and distribute it yourself).
#### Import Transfer from CSV (Using a picklist as a transfer)
If you have a CSV generated by another tool (or plate-tool),
you can import it as a single transfer.
To do so, mouse over the "File" tab, then "Import", and finally "Import Transfer from CSV".
When creating transfers via this method, the transfer cannot be edited.
This is useful if you have a pre-existing picklist that you would like to visualize in plate-tool.
_Note_: If you try to use this feature and no plates are available to select,
there was likely an issue parsing your picklist.
Your browser's console may have guidance as to why parsing failed;
plate-tool was probably expecting a different name for a column than was in your file.
### Other Neat Features
#### Taking Pictures of Plates
@ -130,7 +135,7 @@ To add a new plate, click the "New Plate" button:
Plate tool is hosted [here](https://ilia.moe/cool-stuff/plate-tool/) for your convenience.
However, you're absolutely welcome to host your own instance (even locally).
Here's how:
(_Note:_ If you run Windows you're probably best off doing the following in WSL2)
(_Note:_ ~~If you run Windows you're probably best off doing the following in WSL2~~ You're absolutely fine to install rustup in Powershell, and the subsequent steps should be very similar but likely with different filepaths.)
1. Make sure you have a working Rust toolchain
1. Installing `rustup` is the easiest way to do this. See [their website](https://rustup.rs/),
@ -140,7 +145,7 @@ Here's how:
2. Install [trunk](https://trunkrs.dev/)
- Run `cargo install --locked trunk`
3. Clone this repository using git
4. Enter the project directory and run `trunk serve`
4. Enter the plate-tool-web directory and run `trunk serve`
- You may need to check where `cargo` is installing binaries by default. For me, they're at `~/.cargo/bin`.
If trunk is not automatically placed in your path, you would then run `/your/path/to/.cargo/bin/trunk serve`.
- You can instead run `trunk build --release` for a more performant binary.

View File

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

2
plate-tool-lib/.gitignore vendored Normal file
View File

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

339
plate-tool-lib/Cargo.lock generated Normal file
View File

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

17
plate-tool-lib/Cargo.toml Normal file
View File

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

View File

@ -0,0 +1,173 @@
use super::{string_well_to_pt, TransferRecord, read_csv};
use crate::plate::{PlateFormat, PlateType};
use crate::plate_instances::PlateInstance;
use crate::transfer::Transfer;
use crate::transfer_region::{CustomRegion, Region, TransferRegion};
use std::collections::HashSet;
pub struct AutoOutput {
pub sources: Vec<PlateInstance>,
pub destinations: Vec<PlateInstance>,
pub transfers: Vec<Transfer>,
}
struct UniquePlates {
sources: Vec<PlateInstance>,
destinations: Vec<PlateInstance>,
}
const W96_COL_MAX: u8 = 12;
const W96_ROW_MAX: u8 = 8;
const W384_COL_MAX: u8 = 24;
const W384_ROW_MAX: u8 = 16;
const W1536_COL_MAX: u8 = 48;
const W1536_ROW_MAX: u8 = 32;
pub fn auto(records: &[TransferRecord]) -> AutoOutput {
let unique_plates = find_unique_plates(records);
let transfers = get_transfer_for_all_pairs(records, &unique_plates);
AutoOutput { sources: unique_plates.sources, destinations: unique_plates.destinations, transfers }
}
pub fn read_csv_auto(data: &str) -> AutoOutput {
let transfer_records = read_csv(data);
auto(&transfer_records)
}
fn find_unique_plates(records: &[TransferRecord]) -> UniquePlates {
let mut source_names: HashSet<&str> = HashSet::new();
let mut destination_names: HashSet<&str> = HashSet::new();
for record in records {
source_names.insert(&record.source_plate);
destination_names.insert(&record.destination_plate);
}
let mut sources: Vec<PlateInstance> = Vec::with_capacity(source_names.len());
for source_name in source_names {
let filtered_records: Vec<&TransferRecord> = records
.iter()
.filter(|x| x.source_plate == source_name)
.collect();
let format_guess = guess_plate_size(&filtered_records, PlateType::Source);
sources.push(PlateInstance::new(
PlateType::Source,
format_guess,
source_name.to_string(),
))
}
let mut destinations: Vec<PlateInstance> = Vec::with_capacity(destination_names.len());
for destination_name in destination_names {
let filtered_records: Vec<&TransferRecord> = records
.iter()
.filter(|x| x.destination_plate == destination_name)
.collect();
let format_guess = guess_plate_size(&filtered_records, PlateType::Destination);
destinations.push(PlateInstance::new(
PlateType::Destination,
format_guess,
destination_name.to_string(),
))
}
UniquePlates {
sources,
destinations,
}
}
fn guess_plate_size(plate_filtered_records: &[&TransferRecord], which: PlateType) -> PlateFormat {
let mut guess = PlateFormat::W96; // Never guess smaller than 96
for record in plate_filtered_records {
if let Some((row, col)) = string_well_to_pt(match which {
PlateType::Source => &record.source_well,
PlateType::Destination => &record.destination_well,
}) {
if row > W1536_ROW_MAX || col > W1536_COL_MAX {
return PlateFormat::W3456;
} else if row > W384_ROW_MAX || col > W384_COL_MAX {
guess = PlateFormat::W1536;
} else if row > W96_ROW_MAX || col > W96_COL_MAX {
guess = PlateFormat::W384;
}
}
}
guess
}
fn get_transfer_for_all_pairs(
records: &[TransferRecord],
unique_plates: &UniquePlates,
) -> Vec<Transfer> {
let mut transfers: Vec<Transfer> = Vec::new();
for source_instance in &unique_plates.sources {
for destination_instance in &unique_plates.destinations {
if let Some(transfer) =
get_transfer_for_pair(records, source_instance, destination_instance)
{
transfers.push(transfer);
}
}
}
transfers
}
fn get_transfer_for_pair(
records: &[TransferRecord],
source: &PlateInstance,
destination: &PlateInstance,
) -> Option<Transfer> {
let source_name: &str = &source.name;
let destination_name: &str = &destination.name;
let mut filtered_records = records
.iter()
.filter(|x| x.source_plate == source_name && x.destination_plate == destination_name)
.peekable();
if filtered_records.peek().is_none() {
return None;
}
let mut source_wells: HashSet<(u8, u8)> = HashSet::new();
let mut destination_wells: HashSet<(u8, u8)> = HashSet::new();
for record in filtered_records {
let source_point_opt = string_well_to_pt(&record.source_well);
let destination_point_opt = string_well_to_pt(&record.destination_well);
if source_point_opt.and(destination_point_opt).is_some() {
let source_point = source_point_opt.unwrap();
let destination_point = destination_point_opt.unwrap();
source_wells.insert(source_point);
destination_wells.insert(destination_point);
}
}
let source_wells_vec: Vec<(u8, u8)> = source_wells.into_iter().collect();
let destination_wells_vec: Vec<(u8, u8)> = destination_wells.into_iter().collect();
let custom_region: Region =
Region::Custom(CustomRegion::new(source_wells_vec, destination_wells_vec));
let transfer_region = TransferRegion {
source_plate: source.plate,
dest_plate: destination.plate,
interleave_source: (1, 1),
interleave_dest: (1, 1),
source_region: custom_region.clone(),
dest_region: custom_region,
};
let transfer_name = format!("{} to {}", source.name, destination.name);
Some(Transfer::new(
source.clone(),
destination.clone(),
transfer_region,
transfer_name,
))
}

View File

@ -0,0 +1,80 @@
use crate::transfer::Transfer;
use crate::util::*;
use super::TransferRecord;
use lazy_static::lazy_static;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::error::Error;
pub fn transfer_to_records(
tr: &Transfer,
src_barcode: &str,
dest_barcode: &str,
) -> Vec<TransferRecord> {
let source_wells = tr.transfer_region.get_source_wells();
let map = tr.transfer_region.calculate_map();
let mut records: Vec<TransferRecord> = vec![];
for s_well in source_wells {
let dest_wells = map(s_well);
if let Some(dest_wells) = dest_wells {
for d_well in dest_wells {
records.push(TransferRecord {
source_plate: src_barcode.to_string(),
source_well: format!("{}{}", num_to_letters(s_well.0).unwrap(), s_well.1),
destination_plate: dest_barcode.to_string(),
destination_well: format!("{}{}", num_to_letters(d_well.0).unwrap(), d_well.1),
volume: tr.volume,
concentration: None,
})
}
}
}
records
}
pub fn records_to_csv(trs: Vec<TransferRecord>) -> Result<String, Box<dyn Error>> {
let mut wtr = csv::WriterBuilder::new().from_writer(vec![]);
for record in trs {
wtr.serialize(record)?
}
let data = String::from_utf8(wtr.into_inner()?)?;
Ok(data)
}
pub fn string_well_to_pt(input: &str) -> Option<(u8, u8)> {
lazy_static! {
static ref REGEX: Regex = Regex::new(r"([A-Z,a-z]+)(\d+)").unwrap();
}
if let Some(c1) = REGEX.captures(input) {
if let (Some(row), Some(col)) = (letters_to_num(&c1[1]), c1[2].parse::<u8>().ok()) {
Some((row, col))
} else {
None
}
} else {
None
}
}
pub fn read_csv(data: &str) -> Vec<TransferRecord> {
let (header, data) = data.split_at(data.find('\n').unwrap());
let modified: String = header.to_lowercase() + data;
let mut rdr = csv::Reader::from_reader(modified.as_bytes());
let mut records: Vec<TransferRecord> = Vec::new();
for record in rdr.deserialize::<TransferRecord>() {
match record {
Ok(r) => {
//log::debug!("{:?}", r);
records.push(r);
}
Err(e) => {
log::debug!("{:?}", e);
}
}
}
records
}

View File

@ -0,0 +1,9 @@
mod transfer_record;
mod conversion;
mod auto;
pub use transfer_record::volume_default;
pub use transfer_record::TransferRecord;
pub use conversion::*;
pub use auto::{auto, read_csv_auto};

View File

@ -0,0 +1,33 @@
use serde::{Deserialize, Serialize};
use crate::transfer::Transfer;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TransferRecord {
#[serde(rename = "Source Plate", alias = "source plate", alias = "src plate")]
pub source_plate: String,
#[serde(rename = "Source Well", alias = "source well", alias = "src well")]
pub source_well: String,
#[serde(
rename = "Dest Plate",
alias = "dest plate",
alias = "destination plate"
)]
pub destination_plate: String,
#[serde(
rename = "Destination Well",
alias = "destination well",
alias = "dest well"
)]
pub destination_well: String,
#[serde(rename = "Transfer Volume", alias = "transfer volume")]
#[serde(default = "volume_default")]
pub volume: f32,
#[serde(rename = "Concentration", alias = "concentration")]
pub concentration: Option<f32>,
}
pub fn volume_default() -> f32 {
Transfer::default().volume
}

View File

@ -3,3 +3,4 @@ pub mod plate;
pub mod plate_instances;
pub mod transfer;
pub mod transfer_region;
pub mod util;

View File

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

View File

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

View File

@ -26,7 +26,7 @@ impl Default for Transfer {
name: "New Transfer".to_string(),
id: Default::default(),
transfer_region: Default::default(),
volume: 2.5f32,
volume: default_volume(),
}
}
}

View File

@ -1,7 +1,5 @@
use serde::{Deserialize, Serialize};
use crate::components::transfer_menu::RegionDisplay;
use super::plate::Plate;
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
@ -10,6 +8,12 @@ pub struct CustomRegion {
dest: Vec<(u8, u8)>,
}
impl CustomRegion {
pub fn new(src: Vec<(u8, u8)>, dest: Vec<(u8, u8)>) -> Self {
CustomRegion { src, dest }
}
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
pub enum Region {
Rect((u8, u8), (u8, u8)),
@ -32,8 +36,11 @@ impl TryFrom<Region> for ((u8, u8), (u8, u8)) {
}
}
}
type Corner = (u8, u8);
type Rectangle = (Corner, Corner);
impl Region {
pub fn new_custom(transfers: &Vec<((u8, u8), (u8, u8))>) -> Self {
pub fn new_custom(transfers: &Vec<Rectangle>) -> Self {
let mut src_pts: Vec<(u8, u8)> = Vec::with_capacity(transfers.len());
let mut dest_pts: Vec<(u8, u8)> = Vec::with_capacity(transfers.len());
@ -77,7 +84,7 @@ impl TransferRegion {
match &self.source_region {
Region::Rect(c1, c2) => {
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;
// NOTE: This will panic if either is 0!
// We'll reassign these values (still not mutable) just in case.
@ -174,8 +181,8 @@ impl TransferRegion {
Region::Rect(c1, c2) => {
Box::new(move |(i, j)| {
if source_wells.contains(&(i, j)) {
let possible_destination_wells = create_dense_rectangle(&c1, &c2);
let (d_ul, d_br) = standardize_rectangle(&c1, &c2);
let possible_destination_wells = create_dense_rectangle(c1, c2);
let (d_ul, d_br) = standardize_rectangle(c1, c2);
let (s_ul, s_br) =
standardize_rectangle(&source_corners.0, &source_corners.1);
let s_dims = (
@ -186,7 +193,7 @@ impl TransferRegion {
d_br.0.checked_sub(d_ul.0).unwrap() + 1,
d_br.1.checked_sub(d_ul.1).unwrap() + 1,
);
let N_s = (
let number_used_src_wells = (
// Number of used source wells
(s_dims.0 + il_source.0.unsigned_abs() - 1)
.div_euclid(il_source.0.unsigned_abs()),
@ -197,14 +204,14 @@ impl TransferRegion {
// How many times can we replicate?
(1..)
.position(|n| {
n * N_s.0 * il_dest.0.unsigned_abs() - il_dest.0.unsigned_abs()
n * number_used_src_wells.0 * il_dest.0.unsigned_abs() - il_dest.0.unsigned_abs()
+ 1
> d_dims.0
})
.unwrap() as u8,
(1..)
.position(|n| {
n * N_s.1 * il_dest.1.unsigned_abs() - il_dest.1.unsigned_abs()
n * number_used_src_wells.1 * il_dest.1.unsigned_abs() - il_dest.1.unsigned_abs()
+ 1
> d_dims.1
})
@ -222,26 +229,26 @@ impl TransferRegion {
.into_iter()
.filter(|(x, _)| {
x.checked_sub(d_ul.0).unwrap()
% (N_s.0 * il_dest.0.unsigned_abs()) // Counter along x
% (number_used_src_wells.0 * il_dest.0.unsigned_abs()) // Counter along x
== (il_dest.0.unsigned_abs() *i)
% (N_s.0 * il_dest.0.unsigned_abs())
% (number_used_src_wells.0 * il_dest.0.unsigned_abs())
})
.filter(|(_, y)| {
y.checked_sub(d_ul.1).unwrap()
% (N_s.1 * il_dest.1.unsigned_abs()) // Counter along u
% (number_used_src_wells.1 * il_dest.1.unsigned_abs()) // Counter along u
== (il_dest.1.unsigned_abs() *j)
% (N_s.1 * il_dest.1.unsigned_abs())
% (number_used_src_wells.1 * il_dest.1.unsigned_abs())
})
.filter(|(x, y)| {
// How many times have we replicated? < How many are we allowed
// to replicate?
x.checked_sub(d_ul.0)
.unwrap()
.div_euclid(N_s.0 * il_dest.0.unsigned_abs())
.div_euclid(number_used_src_wells.0 * il_dest.0.unsigned_abs())
< count.0
&& y.checked_sub(d_ul.1)
.unwrap()
.div_euclid(N_s.1 * il_dest.1.unsigned_abs())
.div_euclid(number_used_src_wells.1 * il_dest.1.unsigned_abs())
< count.1
})
.collect(),

View File

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

2
plate-tool-web/.gitignore vendored Normal file
View File

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

30
plate-tool-web/Cargo.toml Normal file
View File

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

View File

@ -35,3 +35,16 @@ dialog > form[method="dialog"] {
}
}
}
.close_button {
color: red;
position: absolute;
top: 5%;
right: 2%;
background: rgba(0%,0%,0%,10%);
&:hover {
color: rgb(0%,100%,100%);
background: rgba(0%,0%,0%,80%);
}
}

View File

@ -0,0 +1,41 @@
@use "sass:color";
@use "../variables" as *;
div.plate_container {
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: center;
border: 2px solid $color-dark;
grid-column: right / right;
grid-row: upper / 3;
h2 {
margin-bottom: 1%;
text-align: center;
}
&>div {
display: grid;
grid-template-rows: auto auto;
grid-template-rows: auto auto;
&>h2:nth-of-type(1) {
grid-column: 2;
grid-row: 1;
}
&>h2:nth-of-type(2) {
grid-column: 1;
grid-row: 2;
writing-mode: vertical-rl;
transform: rotate(-180deg);
}
&>div {
grid-column:2;
grid-row: 2;
}
}
}

View File

@ -1,122 +1,23 @@
#![allow(non_snake_case)]
use std::collections::HashSet;
use js_sys::Array;
use lazy_static::lazy_static;
use regex::Regex;
use wasm_bindgen::{prelude::*, JsCast, JsValue};
use wasm_bindgen::{prelude::*, JsCast};
use web_sys::{
Blob, FileReader, HtmlAnchorElement, HtmlButtonElement, HtmlDialogElement, HtmlFormElement,
HtmlInputElement, HtmlOptionElement, HtmlSelectElement, Url,
FileReader, HtmlButtonElement, HtmlDialogElement, HtmlElement, HtmlFormElement,
HtmlInputElement, HtmlOptionElement, HtmlSelectElement,
};
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::states::{CurrentTransfer, MainState};
use crate::components::transfer_menu::letters_to_num;
use crate::components::states::MainState;
use crate::data::transfer::Transfer;
use crate::data::transfer_region::{Region, TransferRegion};
use plate_tool_lib::transfer::Transfer;
use plate_tool_lib::transfer_region::{Region, TransferRegion};
use crate::data::csv::{state_to_csv, TransferRecord};
use plate_tool_lib::csv::{auto, string_well_to_pt, TransferRecord};
type NoParamsCallback = Box<dyn Fn(()) -> ()>;
use super::main_window_callbacks::create_close_button;
pub fn toggle_in_transfer_hashes_callback(
main_dispatch: Dispatch<MainState>,
) -> Callback<web_sys::MouseEvent> {
let main_dispatch = main_dispatch.clone();
Callback::from(move |_| {
main_dispatch.reduce_mut(|state| {
state.preferences.in_transfer_hashes ^= true;
})
})
}
pub fn new_plate_dialog_callback(
new_plate_dialog_is_open: UseStateHandle<bool>,
) -> NoParamsCallback {
let new_plate_dialog_is_open = new_plate_dialog_is_open.clone();
Box::new(move |_| {
new_plate_dialog_is_open.set(false);
})
}
pub fn open_new_plate_dialog_callback(
new_plate_dialog_is_open: UseStateHandle<bool>,
) -> NoParamsCallback {
let new_plate_dialog_is_open = new_plate_dialog_is_open.clone();
Box::new(move |_| {
new_plate_dialog_is_open.set(true);
})
}
pub fn new_button_callback(
main_dispatch: Dispatch<MainState>,
ct_dispatch: Dispatch<CurrentTransfer>,
) -> Callback<web_sys::MouseEvent> {
Callback::from(move |_| {
let window = web_sys::window().unwrap();
let confirm =
window.confirm_with_message("This will reset all plates and transfers. Proceed?");
if let Ok(confirm) = confirm {
if confirm {
main_dispatch.set(MainState::default());
ct_dispatch.set(CurrentTransfer::default());
}
}
})
}
fn save_str(data: &str, name: &str) {
let blob =
Blob::new_with_str_sequence(&Array::from_iter(std::iter::once(JsValue::from_str(data))));
if let Ok(blob) = blob {
let url = Url::create_object_url_with_blob(&blob).expect("We have a blob, why not URL?");
// Beneath is the cool hack to download files
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let anchor = document
.create_element("a")
.unwrap()
.dyn_into::<HtmlAnchorElement>()
.unwrap();
anchor.set_download(name);
anchor.set_href(&url);
anchor.click();
}
}
pub fn export_csv_button_callback(main_state: std::rc::Rc<MainState>) -> Callback<MouseEvent> {
Callback::from(move |_| {
if main_state.transfers.is_empty() {
web_sys::window()
.unwrap()
.alert_with_message("No transfers to export.")
.unwrap();
return;
}
web_sys::window().unwrap().alert_with_message("CSV export is currently not importable. Export as JSON if you'd like to back up your work!").unwrap();
if let Ok(csv) = state_to_csv(&main_state) {
save_str(&csv, "transfers.csv");
}
})
}
pub fn export_json_button_callback(main_state: std::rc::Rc<MainState>) -> Callback<MouseEvent> {
Callback::from(move |_| {
if let Ok(json) = serde_json::to_string(&main_state) {
save_str(&json, "plate-tool-state.json");
} else {
web_sys::window()
.unwrap()
.alert_with_message("Failed to export.")
.unwrap();
}
})
}
pub fn input_json_input_callback(
pub fn import_transfer_csv_input_callback(
main_dispatch: Dispatch<MainState>,
modal: HtmlDialogElement,
) -> Closure<dyn FnMut(Event)> {
@ -133,16 +34,7 @@ pub fn input_json_input_callback(
let main_dispatch = main_dispatch.clone(); // Clone to satisfy FnMut
// trait
let modal = modal.clone();
let onload = Closure::<dyn FnMut(_)>::new(move |_: Event| {
if let Some(value) = &fr1.result().ok().and_then(|v| v.as_string()) {
let ms = serde_json::from_str::<MainState>(value);
match ms {
Ok(ms) => main_dispatch.set(ms),
Err(e) => log::debug!("{:?}", e),
};
modal.close();
}
});
let onload = import_transfer_csv_onload_callback(main_dispatch, fr1, modal);
fr.set_onload(Some(onload.as_ref().unchecked_ref()));
onload.forget(); // Magic (don't touch)
}
@ -151,7 +43,7 @@ pub fn input_json_input_callback(
})
}
pub fn import_json_button_callback(main_dispatch: Dispatch<MainState>) -> Callback<MouseEvent> {
pub fn import_transfer_csv_callback(main_dispatch: Dispatch<MainState>) -> Callback<MouseEvent> {
Callback::from(move |_| {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
@ -169,7 +61,10 @@ pub fn import_json_button_callback(main_dispatch: Dispatch<MainState>) -> Callba
})
};
modal.set_onclose(Some(onclose_callback.as_ref().unchecked_ref()));
let close_button = create_close_button(&onclose_callback);
onclose_callback.forget();
modal.append_child(&close_button).unwrap();
let form = document
.create_element("form")
@ -182,13 +77,13 @@ pub fn import_json_button_callback(main_dispatch: Dispatch<MainState>) -> Callba
.dyn_into::<HtmlInputElement>()
.unwrap();
input.set_type("file");
input.set_accept(".json");
input.set_accept(".csv");
form.append_child(&input).unwrap();
let input_callback = {
let main_dispatch = main_dispatch.clone();
let modal = modal.clone();
input_json_input_callback(main_dispatch, modal)
import_transfer_csv_input_callback(main_dispatch, modal)
};
input.set_onchange(Some(input_callback.as_ref().unchecked_ref()));
input_callback.forget(); // Magic straight from the docs, don't touch :(
@ -199,83 +94,6 @@ pub fn import_json_button_callback(main_dispatch: Dispatch<MainState>) -> Callba
})
}
pub fn import_transfer_csv_submit_callback(
main_dispatch: Dispatch<MainState>,
from_source: HtmlSelectElement,
to_source: HtmlSelectElement,
from_dest: HtmlSelectElement,
to_dest: HtmlSelectElement,
records: Vec<TransferRecord>,
) -> Closure<dyn FnMut(Event)> {
Closure::<dyn FnMut(_)>::new(move |_: Event| {
let from_source = from_source.value();
let to_source = to_source.value();
let from_dest = from_dest.value();
let to_dest = to_dest.value();
lazy_static! {
static ref REGEX: Regex = Regex::new(r"([A-Z]+)(\d+)").unwrap();
}
let records: Vec<((u8, u8), (u8, u8))> = records
.iter()
.filter(|record| record.source_plate == from_source)
.filter(|record| record.destination_plate == from_dest)
.map(|record| {
let c1 = REGEX.captures(&record.source_well).unwrap();
let c2 = REGEX.captures(&record.destination_well).unwrap();
log::debug!("{} {}", &record.source_well, &record.destination_well);
log::debug!("{},{} {},{}", &c1[1], &c1[2], &c2[1], &c2[2]);
(
(
letters_to_num(&c1[1]).unwrap(),
c1[2].parse::<u8>().unwrap(),
),
(
letters_to_num(&c2[1]).unwrap(),
c2[2].parse::<u8>().unwrap(),
),
)
})
.collect();
let spi = main_dispatch
.get()
.source_plates
.iter()
.find(|src| src.name == to_source)
.unwrap()
.clone();
let dpi = main_dispatch
.get()
.destination_plates
.iter()
.find(|dest| dest.name == to_dest)
.unwrap()
.clone();
let custom_region = Region::new_custom(&records);
let transfer_region = TransferRegion {
source_region: custom_region.clone(),
dest_region: custom_region,
interleave_source: (1, 1),
interleave_dest: (1, 1),
source_plate: spi.plate,
dest_plate: dpi.plate,
};
let transfer = Transfer::new(spi, dpi, transfer_region, "Custom Transfer".to_string());
main_dispatch.reduce_mut(|state| {
state.transfers.push(transfer);
state.selected_transfer = state
.transfers
.last()
.expect("An element should have just been added")
.get_uuid();
});
})
}
pub fn import_transfer_csv_onload_callback(
main_dispatch: Dispatch<MainState>,
file_reader: FileReader,
@ -283,19 +101,7 @@ pub fn import_transfer_csv_onload_callback(
) -> Closure<dyn FnMut(Event)> {
Closure::<dyn FnMut(_)>::new(move |_: Event| {
if let Some(value) = &file_reader.result().ok().and_then(|v| v.as_string()) {
let mut rdr = csv::Reader::from_reader(value.as_bytes());
let mut records = Vec::new();
for record in rdr.deserialize::<crate::data::csv::TransferRecord>() {
match record {
Ok(r) => {
//log::debug!("{:?}", r);
records.push(r);
}
Err(e) => {
log::debug!("{:?}", e);
}
}
}
let records = plate_tool_lib::csv::read_csv(value);
let mut sources: HashSet<String> = HashSet::new();
let mut destinations: HashSet<String> = HashSet::new();
@ -306,6 +112,17 @@ pub fn import_transfer_csv_onload_callback(
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let auto_button = document
.create_element("button")
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap();
auto_button.set_inner_text("Auto");
let auto_button_callback = auto_callback(main_dispatch.clone(), &records);
auto_button.set_onclick(Some(auto_button_callback.as_ref().unchecked_ref()));
auto_button_callback.forget();
let form = document
.create_element("form")
.unwrap()
@ -402,32 +219,87 @@ pub fn import_transfer_csv_onload_callback(
form.append_child(&to_dest).unwrap();
modal.append_child(&submit).unwrap();
modal.append_child(&form).unwrap();
modal.append_child(&auto_button).unwrap();
}
})
}
pub fn import_transfer_csv_input_callback(
pub fn import_transfer_csv_submit_callback(
main_dispatch: Dispatch<MainState>,
modal: HtmlDialogElement,
from_source: HtmlSelectElement,
to_source: HtmlSelectElement,
from_dest: HtmlSelectElement,
to_dest: HtmlSelectElement,
records: Vec<TransferRecord>,
) -> Closure<dyn FnMut(Event)> {
Closure::<dyn FnMut(_)>::new(move |e: Event| {
if let Some(input) = e.current_target() {
let input = input
.dyn_into::<HtmlInputElement>()
.expect("We know this is an input.");
if let Some(files) = input.files() {
if let Some(file) = files.get(0) {
let fr = web_sys::FileReader::new().unwrap();
fr.read_as_text(&file).unwrap();
let fr1 = fr.clone(); // Clone to avoid outliving closure
let main_dispatch = main_dispatch.clone(); // Clone to satisfy FnMut
// trait
let modal = modal.clone();
let onload = import_transfer_csv_onload_callback(main_dispatch, fr1, modal);
fr.set_onload(Some(onload.as_ref().unchecked_ref()));
onload.forget(); // Magic (don't touch)
}
}
}
Closure::<dyn FnMut(_)>::new(move |_: Event| {
let from_source = from_source.value();
let to_source = to_source.value();
let from_dest = from_dest.value();
let to_dest = to_dest.value();
let records: Vec<((u8, u8), (u8, u8))> = records
.iter()
.filter(|record| record.source_plate == from_source)
.filter(|record| record.destination_plate == from_dest)
.map(|record| {
(
string_well_to_pt(&record.source_well).unwrap(),
string_well_to_pt(&record.destination_well).unwrap(),
)
})
.collect();
let spi = main_dispatch
.get()
.source_plates
.iter()
.find(|src| src.name == to_source)
.unwrap()
.clone();
let dpi = main_dispatch
.get()
.destination_plates
.iter()
.find(|dest| dest.name == to_dest)
.unwrap()
.clone();
let custom_region = Region::new_custom(&records);
let transfer_region = TransferRegion {
source_region: custom_region.clone(),
dest_region: custom_region,
interleave_source: (1, 1),
interleave_dest: (1, 1),
source_plate: spi.plate,
dest_plate: dpi.plate,
};
let transfer = Transfer::new(spi, dpi, transfer_region, "Custom Transfer".to_string());
main_dispatch.reduce_mut(|state| {
state.transfers.push(transfer);
state.selected_transfer = state
.transfers
.last()
.expect("An element should have just been added")
.get_uuid();
});
})
}
fn auto_callback(
main_dispatch: Dispatch<MainState>,
records: &[TransferRecord],
) -> Closure<dyn FnMut(Event)> {
let records = Vec::from(records);
Closure::<dyn FnMut(_)>::new(move |_| {
let res = auto(&records);
main_dispatch.reduce_mut(|state| {
state.source_plates.extend(res.sources.into_iter());
state
.destination_plates
.extend(res.destinations.into_iter());
state.transfers.extend(res.transfers.into_iter());
});
})
}

View File

@ -0,0 +1,221 @@
#![allow(non_snake_case)]
use js_sys::Array;
use wasm_bindgen::{prelude::*, JsCast, JsValue};
use web_sys::{
Blob, HtmlAnchorElement, HtmlDialogElement, HtmlElement,
HtmlFormElement, HtmlInputElement, Url,
};
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::states::{CurrentTransfer, MainState};
use crate::state_to_csv;
type NoParamsCallback = Box<dyn Fn(())>;
pub fn create_close_button(close: &Closure<dyn FnMut(Event)>) -> HtmlElement {
let document = web_sys::window().unwrap().document().unwrap();
let close_button = document
.create_element("button")
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap();
close_button.set_text_content(Some(""));
close_button.set_class_name("close_button");
close_button.set_onclick(Some(close.as_ref().unchecked_ref()));
close_button
}
pub fn toggle_in_transfer_hashes_callback(
main_dispatch: Dispatch<MainState>,
) -> Callback<web_sys::MouseEvent> {
let main_dispatch = main_dispatch.clone();
Callback::from(move |_| {
main_dispatch.reduce_mut(|state| {
state.preferences.in_transfer_hashes ^= true;
})
})
}
pub fn new_plate_dialog_callback(
new_plate_dialog_is_open: UseStateHandle<bool>,
) -> NoParamsCallback {
let new_plate_dialog_is_open = new_plate_dialog_is_open.clone();
Box::new(move |_| {
new_plate_dialog_is_open.set(false);
})
}
pub fn open_new_plate_dialog_callback(
new_plate_dialog_is_open: UseStateHandle<bool>,
) -> NoParamsCallback {
let new_plate_dialog_is_open = new_plate_dialog_is_open.clone();
Box::new(move |_| {
new_plate_dialog_is_open.set(true);
})
}
pub fn new_button_callback(
main_dispatch: Dispatch<MainState>,
ct_dispatch: Dispatch<CurrentTransfer>,
) -> Callback<web_sys::MouseEvent> {
Callback::from(move |_| {
let window = web_sys::window().unwrap();
let confirm =
window.confirm_with_message("This will reset all plates and transfers. Proceed?");
if let Ok(confirm) = confirm {
if confirm {
main_dispatch.set(MainState::default());
ct_dispatch.set(CurrentTransfer::default());
}
}
})
}
fn save_str(data: &str, name: &str) {
let blob =
Blob::new_with_str_sequence(&Array::from_iter(std::iter::once(JsValue::from_str(data))));
if let Ok(blob) = blob {
let url = Url::create_object_url_with_blob(&blob).expect("We have a blob, why not URL?");
// Beneath is the cool hack to download files
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let anchor = document
.create_element("a")
.unwrap()
.dyn_into::<HtmlAnchorElement>()
.unwrap();
anchor.set_download(name);
anchor.set_href(&url);
anchor.click();
}
}
pub fn export_csv_button_callback(main_state: std::rc::Rc<MainState>) -> Callback<MouseEvent> {
Callback::from(move |_| {
if main_state.transfers.is_empty() {
web_sys::window()
.unwrap()
.alert_with_message("No transfers to export.")
.unwrap();
return;
}
web_sys::window().unwrap().alert_with_message("CSV export is currently not importable. Export as JSON if you'd like to back up your work!").unwrap();
if let Ok(csv) = state_to_csv(&main_state) {
save_str(&csv, "transfers.csv");
}
})
}
pub fn export_json_button_callback(main_state: std::rc::Rc<MainState>) -> Callback<MouseEvent> {
Callback::from(move |_| {
if let Ok(json) = serde_json::to_string(&main_state) {
save_str(&json, "plate-tool-state.json");
} else {
web_sys::window()
.unwrap()
.alert_with_message("Failed to export.")
.unwrap();
}
})
}
pub fn input_json_input_callback(
main_dispatch: Dispatch<MainState>,
modal: HtmlDialogElement,
) -> Closure<dyn FnMut(Event)> {
Closure::<dyn FnMut(_)>::new(move |e: Event| {
if let Some(input) = e.current_target() {
let input = input
.dyn_into::<HtmlInputElement>()
.expect("We know this is an input.");
if let Some(files) = input.files() {
if let Some(file) = files.get(0) {
let fr = web_sys::FileReader::new().unwrap();
fr.read_as_text(&file).unwrap();
let fr1 = fr.clone(); // Clone to avoid outliving closure
let main_dispatch = main_dispatch.clone(); // Clone to satisfy FnMut
// trait
let modal = modal.clone();
let onload = Closure::<dyn FnMut(_)>::new(move |_: Event| {
if let Some(value) = &fr1.result().ok().and_then(|v| v.as_string()) {
let ms = serde_json::from_str::<MainState>(value);
match ms {
Ok(ms) => main_dispatch.set(ms),
Err(e) => log::debug!("{:?}", e),
};
modal.close();
}
});
fr.set_onload(Some(onload.as_ref().unchecked_ref()));
onload.forget(); // Magic (don't touch)
}
}
}
})
}
pub fn import_json_button_callback(main_dispatch: Dispatch<MainState>) -> Callback<MouseEvent> {
Callback::from(move |_| {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let body = document.body().unwrap();
let modal = document
.create_element("dialog")
.unwrap()
.dyn_into::<HtmlDialogElement>()
.unwrap();
modal.set_text_content(Some("Import File:"));
let onclose_callback = {
let modal = modal.clone();
Closure::<dyn FnMut(_)>::new(move |_: Event| {
modal.remove();
})
};
modal.set_onclose(Some(onclose_callback.as_ref().unchecked_ref()));
let close_button = create_close_button(&onclose_callback);
onclose_callback.forget();
modal.append_child(&close_button).unwrap();
let form = document
.create_element("form")
.unwrap()
.dyn_into::<HtmlFormElement>()
.unwrap();
let input = document
.create_element("input")
.unwrap()
.dyn_into::<HtmlInputElement>()
.unwrap();
input.set_type("file");
input.set_accept(".json");
form.append_child(&input).unwrap();
let input_callback = {
let main_dispatch = main_dispatch.clone();
let modal = modal.clone();
input_json_input_callback(main_dispatch, modal)
};
input.set_onchange(Some(input_callback.as_ref().unchecked_ref()));
input_callback.forget(); // Magic straight from the docs, don't touch :(
modal.append_child(&form).unwrap();
body.append_child(&modal).unwrap();
modal.show_modal().unwrap();
})
}
pub use super::import_csv_callbacks::import_transfer_csv_callback;
pub use super::import_csv_callbacks::import_transfer_csv_submit_callback;
pub use super::import_csv_callbacks::import_transfer_csv_onload_callback;
pub use super::import_csv_callbacks::import_transfer_csv_input_callback;

View File

@ -2,3 +2,4 @@ pub mod main_window_callbacks;
pub mod new_plate_dialog_callbacks;
pub mod transfer_menu_callbacks;
pub mod tree_callbacks;
mod import_csv_callbacks;

View File

@ -5,8 +5,8 @@ use wasm_bindgen::JsCast;
use web_sys::{EventTarget, FormData, HtmlFormElement};
use crate::components::states::MainState;
use crate::data::plate::*;
use crate::data::plate_instances::PlateInstance;
use plate_tool_lib::plate::*;
use plate_tool_lib::plate_instances::PlateInstance;
pub fn new_plate_callback(
dispatch: Dispatch<MainState>,

View File

@ -6,7 +6,7 @@ use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::transfer_menu::RegionDisplay;
use crate::data::{transfer::Transfer, transfer_region::Region};
use plate_tool_lib::{transfer::Transfer, transfer_region::Region};
use crate::components::states::{CurrentTransfer, MainState};

View File

@ -1,14 +1,14 @@
use std::rc::Rc;
use uuid::Uuid;
use wasm_bindgen::JsCast;
use web_sys::{EventTarget, HtmlElement};
use web_sys::{EventTarget, HtmlElement, HtmlInputElement, HtmlSelectElement, HtmlOptionElement};
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::states::{CurrentTransfer, MainState};
use crate::data::transfer_region::Region;
use plate_tool_lib::{transfer_region::Region, plate::PlateFormat};
type NoParamsCallback = Box<dyn Fn(()) -> ()>;
type NoParamsCallback = Box<dyn Fn(())>;
pub fn open_plate_info_callback(
plate_menu_id: UseStateHandle<Option<Uuid>>,
@ -45,6 +45,38 @@ pub fn plate_info_delete_callback(
})
}
pub fn rename_onchange(id: Uuid, main_dispatch: Dispatch<MainState>) -> Callback<Event> {
Callback::from(move |e: Event| {
log::debug!("Changed name");
let input = e
.target()
.expect("Event must have target")
.dyn_into::<HtmlInputElement>()
.unwrap();
main_dispatch.reduce_mut(|state| state.rename_plate(id, &input.value()))
})
}
pub fn format_onchange(id: Uuid, main_dispatch: Dispatch<MainState>) -> Callback<Event> {
Callback::from(move |e: Event| {
log::debug!("Changing plate format");
let new_format: Option<PlateFormat> = e.target()
.expect("Event must have target")
.dyn_into::<HtmlSelectElement>()
.unwrap()
.selected_options()
.get_with_index(0)
.map(|el| {
el.dyn_into::<HtmlOptionElement>().unwrap()
})
.map(|opt_el| opt_el.value())
.and_then(|value| PlateFormat::try_from(value.as_str()).ok());
if let Some(format) = new_format {
main_dispatch.reduce_mut(|state| state.change_format(id, &format));
}
})
}
pub fn source_plate_select_callback(
main_dispatch: Dispatch<MainState>,
ct_dispatch: Dispatch<CurrentTransfer>,

View File

@ -1,8 +1,4 @@
#![allow(non_snake_case)]
use js_sys::Array;
use wasm_bindgen::{prelude::*, JsCast, JsValue};
use web_sys::{Blob, HtmlAnchorElement, HtmlDialogElement, HtmlFormElement, HtmlInputElement, Url};
use yew::prelude::*;
use yewdux::prelude::*;
@ -12,7 +8,7 @@ use crate::components::states::{CurrentTransfer, MainState};
use crate::components::transfer_menu::TransferMenu;
use crate::components::tree::Tree;
use crate::data::plate_instances::PlateInstance;
use plate_tool_lib::plate_instances::PlateInstance;
use crate::components::callbacks::main_window_callbacks;
@ -72,53 +68,8 @@ pub fn MainWindow() -> Html {
main_window_callbacks::import_json_button_callback(main_dispatch)
};
let import_transfer_csv_callback = {
Callback::from(move |_| {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let body = document.body().unwrap();
let modal = document
.create_element("dialog")
.unwrap()
.dyn_into::<HtmlDialogElement>()
.unwrap();
modal.set_text_content(Some("Import File:"));
let onclose_callback = {
let modal = modal.clone();
Closure::<dyn FnMut(_)>::new(move |_: Event| {
modal.remove();
})
};
modal.set_onclose(Some(onclose_callback.as_ref().unchecked_ref()));
onclose_callback.forget();
let form = document
.create_element("form")
.unwrap()
.dyn_into::<HtmlFormElement>()
.unwrap();
let input = document
.create_element("input")
.unwrap()
.dyn_into::<HtmlInputElement>()
.unwrap();
input.set_type("file");
input.set_accept(".csv");
form.append_child(&input).unwrap();
let input_callback = {
let main_dispatch = main_dispatch.clone();
let modal = modal.clone();
main_window_callbacks::import_transfer_csv_input_callback(main_dispatch, modal)
};
input.set_onchange(Some(input_callback.as_ref().unchecked_ref()));
input_callback.forget(); // Magic straight from the docs, don't touch :(
modal.append_child(&form).unwrap();
body.append_child(&modal).unwrap();
modal.show_modal().unwrap();
})
};
let import_transfer_csv_callback =
main_window_callbacks::import_transfer_csv_callback(main_dispatch.clone());
html! {
<>
@ -163,22 +114,3 @@ pub fn MainWindow() -> Html {
</>
}
}
fn save_str(data: &str, name: &str) {
let blob =
Blob::new_with_str_sequence(&Array::from_iter(std::iter::once(JsValue::from_str(data))));
if let Ok(blob) = blob {
let url = Url::create_object_url_with_blob(&blob).expect("We have a blob, why not URL?");
// Beneath is the cool hack to download files
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let anchor = document
.create_element("a")
.unwrap()
.dyn_into::<HtmlAnchorElement>()
.unwrap();
anchor.set_download(name);
anchor.set_href(&url);
anchor.click();
}
}

View File

@ -4,7 +4,6 @@ use yewdux::prelude::*;
use web_sys::HtmlDialogElement;
use crate::components::states::MainState;
use crate::data::plate_instances::PlateInstance;
use crate::components::callbacks::new_plate_dialog_callbacks;
@ -69,10 +68,3 @@ pub fn NewPlateDialog(props: &NewPlateDialogProps) -> Html {
</dialog>
}
}
impl From<&PlateInstance> for String {
fn from(value: &PlateInstance) -> Self {
// Could have other formatting here
format!("{}, {}", value.name, value.plate.plate_format)
}
}

View File

@ -0,0 +1,5 @@
pub mod plate_container;
mod util;
mod plate_callbacks;
mod plate_data;
mod plate;

View File

@ -5,33 +5,33 @@ use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::states::{CurrentTransfer, MainState};
use crate::data::plate_instances::PlateInstance;
use crate::data::transfer::Transfer;
use crate::data::transfer_region::Region;
use plate_tool_lib::plate::PlateType;
use plate_tool_lib::transfer::Transfer;
use plate_tool_lib::transfer_region::Region;
// Color Palette for the Source Plates, can be changed here
use crate::components::plates::util::Palettes;
const PALETTE: super::util::ColorPalette = Palettes::RAINBOW;
use super::super::transfer_menu::{num_to_letters, RegionDisplay};
use plate_tool_lib::util::num_to_letters;
#[derive(PartialEq, Properties)]
pub struct SourcePlateProps {
pub source_plate: PlateInstance,
pub destination_plate: PlateInstance,
pub cell_height: f64,
}
use super::plate_data::*;
use super::plate_callbacks;
#[function_component]
pub fn SourcePlate(props: &SourcePlateProps) -> Html {
pub fn Plate(props: &PlateProps) -> Html {
let (main_state, _) = use_store::<MainState>();
let (ct_state, ct_dispatch) = use_store::<CurrentTransfer>();
let m_start_handle: UseStateHandle<Option<(u8, u8)>> = use_state_eq(|| None);
let m_end_handle: UseStateHandle<Option<(u8, u8)>> = use_state_eq(|| None);
let m_stat_handle: UseStateHandle<bool> = use_state_eq(|| false);
let m_start_handle: MStartHandle = use_state_eq(|| None);
let m_end_handle: MEndHandle = use_state_eq(|| None);
let m_stat_handle: MStatHandle = use_state_eq(|| false);
if !(*m_stat_handle) {
let (pt1, pt2) = match ct_state.transfer.transfer_region.source_region {
let region = match props.ptype {
PlateType::Source => ct_state.transfer.transfer_region.source_region.clone(),
PlateType::Destination => ct_state.transfer.transfer_region.dest_region.clone(),
};
let (pt1, pt2) = match region {
Region::Point((x, y)) => ((x, y), (x, y)),
Region::Rect(c1, c2) => (c1, c2),
Region::Custom(_) => ((0, 0), (0, 0)),
@ -40,31 +40,35 @@ pub fn SourcePlate(props: &SourcePlateProps) -> Html {
m_end_handle.set(Some(pt2));
}
let transfer_map = {
let ts = main_state
.transfers
.iter()
.filter(|t| t.source_id == props.source_plate.get_uuid());
let tooltip_map = {
let transfers = main_state.transfers.iter().filter(|t| match props.ptype {
PlateType::Source => t.source_id == props.source_plate.get_uuid(),
PlateType::Destination => t.dest_id == props.destination_plate.get_uuid(),
});
let mut tooltip_map: HashMap<(u8, u8), Vec<&Transfer>> = HashMap::new();
for t in ts {
let sws = t.transfer_region.get_source_wells();
for sw in sws {
if let Some(val) = tooltip_map.get_mut(&sw) {
val.push(t);
for transfer in transfers {
let wells = match props.ptype {
PlateType::Source => transfer.transfer_region.get_source_wells(),
PlateType::Destination => transfer.transfer_region.get_destination_wells(),
};
for well in wells {
if let Some(val) = tooltip_map.get_mut(&well) {
val.push(transfer);
} else {
tooltip_map.insert(sw, vec![t]);
tooltip_map.insert(well, vec![transfer]);
}
}
}
tooltip_map
};
let source_wells = ct_state.transfer.transfer_region.get_source_wells();
let wells = match props.ptype {
PlateType::Source => ct_state.transfer.transfer_region.get_source_wells(),
PlateType::Destination => ct_state.transfer.transfer_region.get_destination_wells(),
};
let ordered_ids: Vec<uuid::Uuid> = {
let mut ids: Vec<uuid::Uuid> = main_state.transfers.clone().iter()
.map(|x| x.id)
.collect();
let mut ids: Vec<uuid::Uuid> = main_state.transfers.clone().iter().map(|x| x.id).collect();
ids.sort_unstable();
ids
};
@ -73,37 +77,13 @@ pub fn SourcePlate(props: &SourcePlateProps) -> Html {
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)));
}
}
})
plate_callbacks::mouse_callback(m_start_handle, m_end_handle, m_stat_handle)
};
let mouseup_callback = {
let m_start_handle = m_start_handle.clone();
let m_end_handle = m_end_handle.clone();
Callback::from(move |_: MouseEvent| {
m_stat_handle.set(false);
if let Some(ul) = *m_start_handle {
if let Some(br) = *m_end_handle {
if let Ok(rd) = RegionDisplay::try_from((ul.0, ul.1, br.0, br.1)) {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.source_region = Region::from(&rd);
});
}
}
}
})
plate_callbacks::mouseup_callback(m_start_handle, m_end_handle, m_stat_handle, ct_dispatch)
};
let mouseleave_callback = Callback::clone(&mouseup_callback);
@ -112,8 +92,20 @@ pub fn SourcePlate(props: &SourcePlateProps) -> Html {
let _ = js_sys::eval("copy_screenshot_src()");
});
let width = match props.ptype {
PlateType::Source => props.source_plate.plate.size().1,
PlateType::Destination => props.destination_plate.plate.size().1,
};
let height = match props.ptype {
PlateType::Source => props.source_plate.plate.size().0,
PlateType::Destination => props.destination_plate.plate.size().0,
};
let pformat = match props.ptype {
PlateType::Source => props.source_plate.plate.plate_format,
PlateType::Destination => props.destination_plate.plate.plate_format,
};
let column_header = {
let headers = (1..=props.source_plate.plate.size().1)
let headers = (1..=width)
.map(|j| {
html! {<th>
{format!("{:0>2}", j)}
@ -122,22 +114,23 @@ pub fn SourcePlate(props: &SourcePlateProps) -> Html {
.collect::<Html>();
html! {<tr><th />{ headers }</tr>}
};
let rows = (1..=props.source_plate.plate.size().0)
let rows =
(1..=height)
.map(|i| {
let row_header = html! {<th>{num_to_letters(i)}</th>};
let row = (1..=props.source_plate.plate.size().1)
let row = (1..=width)
.map(|j| {
html! {
<SourcePlateCell i={i} j={j}
<PlateCell i={i} j={j}
selected={in_rect(*m_start_handle.clone(), *m_end_handle.clone(), (i,j))}
mouse={mouse_callback.clone()}
in_transfer={source_wells.contains(&(i,j)) && main_state.preferences.in_transfer_hashes}
color={transfer_map.get(&(i,j))
in_transfer={wells.contains(&(i,j)) && main_state.preferences.in_transfer_hashes}
color={tooltip_map.get(&(i,j))
.and_then(|t| t.last())
.map(|t| PALETTE.get_ordered(t.get_uuid(), &ordered_ids))
}
cell_height={props.cell_height}
title={transfer_map.get(&(i,j)).map(|transfers| format!("Used by: {}", transfers.iter().map(|t| t.name.clone())
title={tooltip_map.get(&(i,j)).map(|transfers| format!("Used by: {}", transfers.iter().map(|t| t.name.clone())
.collect::<Vec<_>>().join(", ")))}
/>
}
@ -153,8 +146,11 @@ pub fn SourcePlate(props: &SourcePlateProps) -> Html {
html! {
<div ondblclick={screenshot_callback}
class={classes!{"source_plate",
"W".to_owned()+&props.source_plate.plate.plate_format.to_string()}}>
class={classes!{match props.ptype {
PlateType::Source => "source_plate",
PlateType::Destination => "dest_plate",
},
"W".to_owned()+&pformat.to_string()}}>
<table
onmouseup={move |e| {
mouseup_callback.emit(e);
@ -170,7 +166,7 @@ pub fn SourcePlate(props: &SourcePlateProps) -> Html {
}
#[derive(PartialEq, Properties)]
pub struct SourcePlateCellProps {
pub struct PlateCellProps {
i: u8,
j: u8,
selected: bool,
@ -180,14 +176,9 @@ pub struct SourcePlateCellProps {
cell_height: f64,
title: Option<String>,
}
#[derive(Debug)]
pub enum MouseEventType {
Mousedown,
Mouseenter,
}
#[function_component]
fn SourcePlateCell(props: &SourcePlateCellProps) -> Html {
fn PlateCell(props: &PlateCellProps) -> Html {
let selected_class = match props.selected {
true => Some("current_select"),
false => None,

View File

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

View File

@ -3,10 +3,12 @@ use wasm_bindgen::prelude::Closure;
use wasm_bindgen::JsCast;
use yew::prelude::*;
use crate::data::plate_instances::PlateInstance;
use plate_tool_lib::plate::PlateType;
use plate_tool_lib::plate_instances::PlateInstance;
use super::destination_plate::DestinationPlate;
use super::source_plate::SourcePlate;
// use super::destination_plate::DestinationPlate;
// use super::source_plate::SourcePlate;
use super::plate::Plate;
#[derive(Properties, PartialEq)]
pub struct PlateContainerProps {
@ -53,13 +55,15 @@ pub fn PlateContainer(props: &PlateContainerProps) -> Html {
if let Some(dpi) = props.destination_dims.clone() {
<div class="plate_container--source">
<h2>{spi.name.clone()}</h2>
<SourcePlate source_plate={spi.clone()} destination_plate={dpi.clone()}
cell_height={cell_height}/>
<h2>{"Source"}</h2>
<Plate source_plate={spi.clone()} destination_plate={dpi.clone()}
cell_height={cell_height} ptype={PlateType::Source}/>
</div>
<div class="plate_container--destination">
<h2>{dpi.name.clone()}</h2>
<DestinationPlate source_plate={spi.clone()} destination_plate={dpi.clone()}
cell_height={cell_height}/>
<h2>{"Destination"}</h2>
<Plate source_plate={spi.clone()} destination_plate={dpi.clone()}
cell_height={cell_height} ptype={PlateType::Destination}/>
</div>
} else {
<h2>{"No Destination Plate Selected"}</h2>

View File

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

View File

@ -2,10 +2,6 @@
// https://iquilezles.org/articles/palettes/
// http://dev.thi.ng/gradients/
use rand::prelude::*;
use rand::rngs::SmallRng;
use lazy_static::lazy_static;
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct ColorPalette {
a: [f64; 3],
@ -41,17 +37,17 @@ impl ColorPalette {
// self.get(r.gen_range(0.0..1.0f64))
// }
pub fn get_ordered(&self, t: uuid::Uuid, ordered_uuids: &Vec<uuid::Uuid>)
pub fn get_ordered(&self, t: uuid::Uuid, ordered_uuids: &[uuid::Uuid])
-> [f64; 3] {
let index = ordered_uuids.iter().position(|&x| x == t).expect("uuid must be in list of uuids") + 1;
return self.get(Self::space_evenly(index))
self.get(Self::space_evenly(index))
}
fn space_evenly(x: usize) -> f64 {
let e: usize = (x.ilog2() + 1) as usize;
let d: usize = (2usize.pow(e as u32)) as usize;
let d: usize = 2usize.pow(e as u32);
let n: usize = (2*x + 1) % d;
return (n as f64) / (d as f64);
(n as f64) / (d as f64)
}
}

View File

@ -2,9 +2,9 @@ use serde::{Deserialize, Serialize};
use uuid::Uuid;
use yewdux::{prelude::*, storage};
use crate::data::plate::*;
use crate::data::plate_instances::PlateInstance;
use crate::data::transfer::Transfer;
use plate_tool_lib::plate::*;
use plate_tool_lib::plate_instances::PlateInstance;
use plate_tool_lib::transfer::Transfer;
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, Store)]
#[store(storage = "session")]
@ -108,4 +108,13 @@ impl MainState {
self.destination_plates[index].change_name(new_name.to_string());
}
}
pub fn change_format(&mut self, id: Uuid, new_format: &PlateFormat) {
if let Some(index) = self.source_plates.iter().position(|spi| spi.get_uuid() == id) {
self.source_plates[index].change_format(new_format);
}
if let Some(index) = self.destination_plates.iter().position(|dpi| dpi.get_uuid() == id) {
self.destination_plates[index].change_format(new_format);
}
}
}

View File

@ -7,7 +7,8 @@ use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::callbacks::transfer_menu_callbacks;
use crate::data::transfer_region::Region;
use plate_tool_lib::transfer_region::Region;
use plate_tool_lib::util::{letters_to_num, num_to_letters};
use super::states::{CurrentTransfer, MainState};
@ -205,7 +206,7 @@ impl TryFrom<&str> for RegionDisplay {
lazy_static! {
static ref REGION_REGEX: Regex = Regex::new(r"([A-Z]+)(\d+):([A-Z]+)(\d+)").unwrap();
}
if let Some(captures) = REGION_REGEX.captures(&value) {
if let Some(captures) = REGION_REGEX.captures(value) {
if captures.len() != 5 {
return Err("Not enough capture groups");
}
@ -277,79 +278,12 @@ impl TryFrom<(u8, u8, u8, u8)> for RegionDisplay {
})
}
}
pub fn letters_to_num(letters: &str) -> Option<u8> {
let mut num: u8 = 0;
for (i, letter) in letters.to_ascii_uppercase().chars().rev().enumerate() {
log::debug!("{}, {}", i, letter);
let n = letter as u8;
if !(65..=90).contains(&n) {
return None;
}
num = num.checked_add((26_i32.pow(i as u32) * (n as i32 - 64)).try_into().ok()?)?;
}
Some(num)
}
pub fn num_to_letters(num: u8) -> Option<String> {
if num == 0 {
return None;
} // Otherwise, we will not return none!
// As another note, we can't represent higher than "IV" anyway;
// thus there's no reason for a loop (26^n with n>1 will NOT occur).
let mut text = "".to_string();
let mut digit1 = num.div_euclid(26u8);
let mut digit2 = num.rem_euclid(26u8);
if digit1 > 0 && digit2 == 0u8 {
digit1 -= 1;
digit2 = 26;
}
if digit1 != 0 {
text.push((64 + digit1) as char)
}
text.push((64 + digit2) as char);
Some(text.to_string())
}
#[cfg(test)]
mod tests {
use wasm_bindgen_test::*;
use super::{letters_to_num, num_to_letters, RegionDisplay};
#[test]
#[wasm_bindgen_test]
fn test_letters_to_num() {
assert_eq!(letters_to_num("D"), Some(4));
assert_eq!(letters_to_num("d"), None);
assert_eq!(letters_to_num("AD"), Some(26 + 4));
assert_eq!(letters_to_num("CG"), Some(3 * 26 + 7));
}
#[test]
#[wasm_bindgen_test]
fn test_num_to_letters() {
println!("27 is {:?}", num_to_letters(27));
assert_eq!(num_to_letters(1), Some("A".to_string()));
assert_eq!(num_to_letters(26), Some("Z".to_string()));
assert_eq!(num_to_letters(27), Some("AA".to_string()));
assert_eq!(num_to_letters(111), Some("DG".to_string()));
}
#[test]
#[wasm_bindgen_test]
fn test_l2n_and_n2l() {
assert_eq!(
num_to_letters(letters_to_num("A").unwrap()),
Some("A".to_string())
);
assert_eq!(
num_to_letters(letters_to_num("BJ").unwrap()),
Some("BJ".to_string())
);
for i in 1..=255 {
assert_eq!(letters_to_num(&num_to_letters(i).unwrap()), Some(i));
}
}
use super::*;
#[test]
#[wasm_bindgen_test]

View File

@ -1,13 +1,14 @@
#![allow(non_snake_case)]
use plate_tool_lib::plate::PlateFormat;
use uuid::Uuid;
use wasm_bindgen::JsCast;
use web_sys::{HtmlDialogElement, HtmlInputElement};
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::states::{CurrentTransfer, MainState};
use crate::components::callbacks::tree_callbacks;
use crate::components::states::{CurrentTransfer, MainState};
#[derive(PartialEq, Properties)]
pub struct TreeProps {
@ -157,23 +158,32 @@ fn PlateInfoModal(props: &PlateInfoModalProps) -> Html {
Some(plate) => plate.name.clone(),
None => "Not Found".to_string(),
};
let plate_format = plate.map(|p| p.plate.plate_format);
let plate_formats = vec![
PlateFormat::W6,
PlateFormat::W12,
PlateFormat::W24,
PlateFormat::W48,
PlateFormat::W96,
PlateFormat::W384,
PlateFormat::W1536,
PlateFormat::W3456,
];
let plate_format_options = plate_formats.iter().map(|v| {
let selected = Some(v) == plate_format.as_ref();
html!{
<option value={v.to_string()} selected={selected}>{ v.to_string() }</option>
}
});
let onclose = {
let dialog_close_callback = props.dialog_close_callback.clone();
move |_| dialog_close_callback.emit(())
};
let rename_onchange = {
let id = props.id;
Callback::from(move |e: Event| {
log::debug!("Changed name");
let input = e
.target()
.expect("Event must have target")
.dyn_into::<HtmlInputElement>()
.unwrap();
main_dispatch.reduce_mut(|state| state.rename_plate(id, &input.value()))
})
};
let rename_onchange = tree_callbacks::rename_onchange(props.id, main_dispatch.clone());
let format_onchange = tree_callbacks::format_onchange(props.id, main_dispatch.clone());
let delete_onclick = {
let delete_button_callback = props.delete_button_callback.clone();
@ -204,6 +214,11 @@ fn PlateInfoModal(props: &PlateInfoModalProps) -> Html {
<h2>{"Plate Info"}</h2>
<h3>{"Name: "}<input type="text" value={plate_name} onchange={rename_onchange}/></h3>
<button onclick={delete_onclick}>{"Delete"}</button>
if let Some(_) = plate_format {
<select name="modal_plate_format" onchange={format_onchange}>
{ for plate_format_options }
</select>
}
<form class="modal_close" method="dialog"><button /></form>
</dialog>
}

View File

@ -1,12 +1,15 @@
#![allow(non_snake_case)]
mod components;
mod data;
use std::error::Error;
use components::main_window::MainWindow;
use components::states::MainState;
use plate_tool_lib::csv::*;
use yew::prelude::*;
#[cfg(debug_assertions)]
use data::*;
use plate_tool_lib::*;
#[function_component]
pub fn App() -> Html {
@ -35,3 +38,25 @@ pub fn plate_test() {
println!("{:?} -> {:?}", w, m(w));
}
}
pub fn state_to_csv(state: &MainState) -> Result<String, Box<dyn Error>> {
let mut records: Vec<TransferRecord> = Vec::new();
for transfer in &state.transfers {
let src_barcode = state
.source_plates
.iter()
.find(|spi| spi.get_uuid() == transfer.source_id)
.ok_or("Found unpurged transfer")?;
let dest_barcode = state
.destination_plates
.iter()
.find(|dpi| dpi.get_uuid() == transfer.dest_id)
.ok_or("Found unpurged transfer")?;
records.append(&mut transfer_to_records(
transfer,
&src_barcode.name,
&dest_barcode.name,
))
}
records_to_csv(records)
}

View File

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

View File

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

View File

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

View File

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