Compare commits

...

7 Commits

Author SHA1 Message Date
Emilia Allison 877460d0aa
Utility for copying plates as image 2023-12-29 18:50:01 -05:00
Emilia Allison 9a9c3e2fca
Change wording in options menu
Father suggests that this wording is more clear to the end user.
I agree!
2023-12-29 17:46:38 -05:00
Emilia Allison ba7c9f5ee9
Set default plate format to 96 well 2023-12-29 17:44:51 -05:00
Emilia Allison c79ace2d36
Implement in_transfer hashes toggle in plates 2023-12-29 15:57:30 -05:00
Emilia Allison 4356f681bc
Preferences menu and toggle for in_transfer hashes 2023-12-29 15:55:23 -05:00
Emilia Allison f24cd4ba6e
Add preferences struct to main state 2023-12-29 15:46:30 -05:00
Emilia Allison db9b86ea18
Squashed commit of the following:
commit 5e1137c460
Author: Emilia <contact@emiliaallison.com>
Date:   Fri Dec 29 18:03:00 2023 -0500

    Fix: indexing error w.r.t. logarithm argument

commit 535b14a586
Author: Emilia <contact@emiliaallison.com>
Date:   Fri Dec 29 18:02:00 2023 -0500

    Space colors evenly, consistently, etc

    Colors should now:
    	- Not change if new transfers are added
    	- Be evenly spaced throughout the palette
    	- Be persistent across refreshes

commit 6e08f47955
Author: Emilia <contact@emiliaallison.com>
Date:   Fri Dec 29 18:01:00 2023 -0500

    Add palette function for ordered ids

    Given an id and a list of sorted ids, yields a color

commit 88e838e102
Author: Emilia <contact@emiliaallison.com>
Date:   Fri Dec 29 18:00:00 2023 -0500

    Switch to v7 UUIDs from v4

    v7 UUIDs are timestamp based and thus we can establish a useful
    total ordering over them; will base colors on this
2023-12-29 15:38:38 -05:00
14 changed files with 7962 additions and 21 deletions

15
Cargo.lock generated
View File

@ -34,6 +34,12 @@ dependencies = [
"syn 2.0.16",
]
[[package]]
name = "atomic"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
[[package]]
name = "autocfg"
version = "1.1.0"
@ -885,10 +891,11 @@ checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
[[package]]
name = "uuid"
version = "1.3.3"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2"
checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560"
dependencies = [
"atomic",
"getrandom",
"rand",
"serde",
@ -898,9 +905,9 @@ dependencies = [
[[package]]
name = "uuid-macro-internal"
version = "1.3.3"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f67b459f42af2e6e1ee213cb9da4dbd022d3320788c3fb3e1b893093f1e45da"
checksum = "f49e7f3f3db8040a100710a11932239fd30697115e2ba4107080d8252939845e"
dependencies = [
"proc-macro2",
"quote",

View File

@ -18,7 +18,7 @@ log = "0.4"
wasm-logger = "0.2"
regex = "1"
lazy_static = "1.4"
uuid = { version = "1.3", features = ["v4", "fast-rng", "macro-diagnostics", "js", "serde"] }
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"

7830
assets/js/html2canvas.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,24 @@
function copy_screenshot(el) {
html2canvas(el).then((canvas) => {
console.log("Copying image to clipboard");
let data = canvas.toDataURL();
const textArea = document.createElement("textarea");
textArea.value = data;
document.body.prepend(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
});
}
function copy_screenshot_dest() {
let plate = document.getElementsByClassName("dest_plate")[0];
copy_screenshot(plate);
}
function copy_screenshot_src() {
let plate = document.getElementsByClassName("source_plate")[0];
copy_screenshot(plate);
}

View File

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

View File

@ -4,6 +4,8 @@
<meta charset="utf-8" />
<link data-trunk rel="scss" href="assets/scss/index.scss">
<link data-trunk rel="copy-dir" href="assets/fonts">
<script data-trunk src="assets/js/screenshot_utility.js"></script>
<script data-trunk src="assets/js/html2canvas.js"></script>
<title>Plate Tool</title>
</head>
</html>

View File

@ -49,6 +49,15 @@ pub fn MainWindow() -> Html {
});
}
let toggle_in_transfer_hashes_callback = {
let main_dispatch = main_dispatch.clone();
Callback::from(move |_| {
main_dispatch.reduce_mut(|state| {
state.preferences.in_transfer_hashes ^= true;
})
})
};
let new_plate_dialog_is_open = use_state_eq(|| false);
let new_plate_dialog_callback = {
let new_plate_dialog_is_open = new_plate_dialog_is_open.clone();
@ -476,6 +485,15 @@ pub fn MainWindow() -> Html {
</div>
</div>
</div>
<div class="dropdown">
<button>{"Options"}</button>
<div class="dropdown-sub">
<button>{"Styles"}</button>
<div>
<button onclick={toggle_in_transfer_hashes_callback}>{"Toggle transfer hashes"}</button>
</div>
</div>
</div>
</div>
<div class="main_container">
<Tree open_new_plate_callback={open_new_plate_dialog_callback}/>

View File

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

View File

@ -41,6 +41,14 @@ pub fn DestinationPlate(props: &DestinationPlateProps) -> Html {
}
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();
@ -99,6 +107,11 @@ pub fn DestinationPlate(props: &DestinationPlateProps) -> Html {
let mouseleave_callback = Callback::clone(&mouseup_callback);
let screenshot_callback = Callback::from(|_| {
let _ = js_sys::eval("copy_screenshot_dest()");
});
let column_header = {
let headers = (1..=props.destination_plate.plate.size().1)
.map(|j| {
@ -115,10 +128,10 @@ pub fn DestinationPlate(props: &DestinationPlateProps) -> Html {
<DestPlateCell i={i} j={j}
selected={super::source_plate::in_rect(*m_start_handle.clone(), *m_end_handle.clone(), (i,j))}
mouse={mouse_callback.clone()}
in_transfer={destination_wells.contains(&(i,j))}
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_uuid(t.get_uuid()))
.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())
@ -135,7 +148,8 @@ pub fn DestinationPlate(props: &DestinationPlateProps) -> Html {
.collect::<Html>();
html! {
<div class={classes!{"dest_plate",
<div ondblclick={screenshot_callback}
class={classes!{"dest_plate",
"W".to_owned()+&props.source_plate.plate.plate_format.to_string()}}>
<table
onmouseup={move |e| {

View File

@ -61,6 +61,14 @@ pub fn SourcePlate(props: &SourcePlateProps) -> Html {
let source_wells = ct_state.transfer.transfer_region.get_source_wells();
let ordered_ids: Vec<uuid::Uuid> = {
let mut ids: Vec<uuid::Uuid> = main_state.transfers.clone().iter()
.map(|x| x.id)
.collect();
ids.sort_unstable();
ids
};
let mouse_callback = {
let m_start_handle = m_start_handle.clone();
let m_end_handle = m_end_handle.clone();
@ -100,6 +108,10 @@ pub fn SourcePlate(props: &SourcePlateProps) -> Html {
let mouseleave_callback = Callback::clone(&mouseup_callback);
let screenshot_callback = Callback::from(|_| {
let _ = js_sys::eval("copy_screenshot_src()");
});
let column_header = {
let headers = (1..=props.source_plate.plate.size().1)
.map(|j| {
@ -119,10 +131,10 @@ pub fn SourcePlate(props: &SourcePlateProps) -> Html {
<SourcePlateCell i={i} j={j}
selected={in_rect(*m_start_handle.clone(), *m_end_handle.clone(), (i,j))}
mouse={mouse_callback.clone()}
in_transfer={source_wells.contains(&(i,j))}
in_transfer={source_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_uuid(t.get_uuid()))
.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())
@ -140,7 +152,8 @@ pub fn SourcePlate(props: &SourcePlateProps) -> Html {
.collect::<Html>();
html! {
<div class={classes!{"source_plate",
<div ondblclick={screenshot_callback}
class={classes!{"source_plate",
"W".to_owned()+&props.source_plate.plate.plate_format.to_string()}}>
<table
onmouseup={move |e| {

View File

@ -35,10 +35,23 @@ impl ColorPalette {
self.get((2f64.powi(-(t.ilog2() as i32))) * (t as f64 + 0.5f64) - 1.0f64)
}
pub fn get_uuid(&self, t: uuid::Uuid) -> [f64; 3] {
// self.get(t.as_u128() as f64 / (u128::MAX) as f64)
let mut r = SmallRng::seed_from_u64(t.as_u128() as u64);
self.get(r.gen_range(0.0..1.0f64))
// pub fn get_uuid(&self, t: uuid::Uuid) -> [f64; 3] {
// // self.get(t.as_u128() as f64 / (u128::MAX) as f64)
// let mut r = SmallRng::seed_from_u64(t.as_u128() as u64);
// self.get(r.gen_range(0.0..1.0f64))
// }
pub fn get_ordered(&self, t: uuid::Uuid, ordered_uuids: &Vec<uuid::Uuid>)
-> [f64; 3] {
let index = ordered_uuids.iter().position(|&x| x == t).expect("uuid must be in list of uuids") + 1;
return self.get(Self::space_evenly(index))
}
fn space_evenly(x: usize) -> f64 {
let e: usize = (x.ilog2() + 1) as usize;
let d: usize = (2usize.pow(e as u32)) as usize;
let n: usize = (2*x + 1) % d;
return (n as f64) / (d as f64);
}
}

View File

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

View File

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

View File

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