├── .gitignore ├── .idea ├── .gitignore ├── inspectionProfiles │ └── Project_Default.xml ├── modules.xml ├── plasmid_check.iml └── vcs.xml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs ├── file_spec.md ├── install_scripts ├── plascad.desktop └── setup_linux_desktop.sh ├── rustfmt.toml ├── screenshots ├── map_aug_24.png ├── primers_aug_24.png ├── protein_aug_24.png └── seq_aug_24.png └── src ├── ab1.rs ├── alignment.rs ├── alignment_map.rs ├── backbones.rs ├── cloning.rs ├── external_websites.rs ├── feature_db_builder.rs ├── feature_db_load.rs ├── file_io ├── ab1.rs ├── ab1_tags.rs ├── genbank.rs ├── mod.rs ├── pcad.rs ├── save.rs └── snapgene.rs ├── gui ├── ab1.rs ├── alignment.rs ├── circle.rs ├── cloning.rs ├── feature_table.rs ├── input.rs ├── ligation.rs ├── lin_maps.rs ├── metadata.rs ├── mod.rs ├── navigation.rs ├── pcr.rs ├── portions.rs ├── primer_table.rs ├── protein.rs ├── save.rs ├── sequence │ ├── feature_overlay.rs │ ├── mod.rs │ ├── primer_overlay.rs │ └── seq_view.rs └── theme.rs ├── ligation.rs ├── main.rs ├── melting_temp_calcs.rs ├── misc_types.rs ├── pcr.rs ├── portions.rs ├── primer.rs ├── primer_metrics.rs ├── protein.rs ├── reading_frame.rs ├── resources ├── icon.ico └── icon.png ├── save_compat.rs ├── solution_helper.rs ├── state.rs ├── tags.rs ├── toxic_proteins.rs └── util.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Rust 2 | /target 3 | Cargo.lock 4 | 5 | # IDE 6 | *.iws 7 | workspace.xml 8 | 9 | 10 | # Saved file formats 11 | *.save 12 | *.pcad 13 | *.fasta 14 | *.fa 15 | *.dna 16 | *.xml 17 | *.pp 18 | 19 | # I'm not sure what is generating these. 20 | *.rs~ 21 | *.md~ -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/plasmid_check.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "plascad" 3 | version = "0.7.8" 4 | edition = "2024" 5 | authors = ["David O'Connor "] 6 | #description = "Tools for plasmid and primer design, PCR, and related." 7 | description = "PlasCAD" 8 | keywords = ["plasmid", "vector", "primer", "biology", "pcr"] 9 | categories = [ 10 | "science", "science::bioinformatics", 11 | ] 12 | repository = "https://github.com/David-OConnor/plascad" 13 | documentation = "https://docs.rs/plascad" 14 | readme = "README.md" 15 | license = "MIT" 16 | exclude = [".gitignore"] 17 | default-run = "plascad" 18 | 19 | 20 | [[bin]] 21 | name = "plascad" 22 | path = "src/main.rs" 23 | 24 | #[[bin]] 25 | #name = "feature_db_builder" 26 | #path = "src/feature_db_builder.rs" 27 | 28 | [dependencies] 29 | eframe = "0.31.0" 30 | egui_extras = "0.31.0" # For tables. 31 | egui-file-dialog = "0.10.0" # For file dialogs. 32 | 33 | chrono = "0.4.41" 34 | 35 | serde = { version = "1.0.216", features = ["derive"] } 36 | num_enum = "0.7.3" # reversing a u8-repr. 37 | 38 | # For FASTA read and write. Note: We can ditch this for a custom parser; FASTA is easy. 39 | # Note, 2024-08-09: Removing this appears to have minimal impact on binary size. 40 | bio = "2.2.0" 41 | quick-xml = {version = "0.37.1", features = ["serialize"]} # For Snapgene feature parsing and writing. 42 | # For parsing and writing GenBank files. todo: This seems to have a lot of sub-deps; Increases binary size a bit. 43 | gb-io = "0.9.0" 44 | 45 | lin_alg = "1.1.8" 46 | na_seq = "0.2.5" 47 | bio_apis = "0.1.0" 48 | 49 | # We use bincode for saving and loading to files using our custom format. 50 | bincode = "2.0.1" 51 | 52 | # For creating JSON payloads for the PDB search API. 53 | serde_json = {version = "1.0.127"} 54 | 55 | # For clipboard operations not supported directly by EGUI 56 | copypasta = "0.10.1" 57 | #winreg = "0.52.0" # For setting up file associations on Windows 58 | 59 | webbrowser = "1.0.1" 60 | # We use strum to iterate over enums. 61 | # todo: Check sup-deps/binary impact etc 62 | strum = "0.27.1" 63 | strum_macros = "0.27.1" 64 | 65 | # We use Flate2 for decompressing BAM files. todo: Remove if you remove that functionality. 66 | flate2 = "1.1.1" 67 | bgzip = "0.3.1" 68 | 69 | 70 | [patch.crates-io] 71 | na_seq = { path = "../na_seq" } 72 | lin_alg = { path = "../../lin_alg" } 73 | bio_apis = { path = "../bio_apis" } 74 | 75 | 76 | 77 | [build-dependencies] 78 | # These are for embedding an application icon, on Windows. 79 | winresource = "0.1.20" 80 | 81 | # https://doc.rust-lang.org/cargo/reference/profiles.html 82 | [profile.release] 83 | strip = true # Strip symbols from binary. Very little improvement in size. 84 | # Optimize for size. May make it tough to use with a debugger. Possibly could make it slower(?); the docs imply 85 | # experimenting is required. 86 | # opt-level = 'z' # Optimize for binary size; gives about 60% of the original size. 87 | # opt-level = 's' # Optimize for binary size; gives about 60% of the original size. 88 | #codegen-units = 1 # Small decrease in binary size; longer compile time. 89 | lto = true # Can produce better optimized code, at the cost of [significantly]longer linking time. 90 | # Very little size improvement. 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2025 David O'Connor 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | // For embedding application icons, in Windows. 2 | 3 | use std::{env, io}; 4 | 5 | use winresource::WindowsResource; 6 | 7 | fn main() -> io::Result<()> { 8 | if env::var_os("CARGO_CFG_WINDOWS").is_some() { 9 | WindowsResource::new() 10 | // This path can be absolute, or relative to your crate root. 11 | .set_icon("src/resources/icon.ico") 12 | .compile()?; 13 | } 14 | Ok(()) 15 | } 16 | -------------------------------------------------------------------------------- /file_spec.md: -------------------------------------------------------------------------------- 1 | # The PlasCAD file format 2 | 3 | This document describes the PlasCAD file format: a compact way of storing DNA sequences, features, primers, metadata, 4 | and related information. It's a binary format divided into discrete packets, and uses the `.pcad` file extension. 5 | Code for implementing can be found in [pcad.rs](src/file_io/pcad.rs). 6 | 7 | Most data structures use [Bincode](https://docs.rs/bincode/latest/bincode/) library. This is convenient for this program's 8 | purposes, but makes external interoperability more challenging. 9 | 10 | Byte order is big endian. 11 | 12 | 13 | ## Components 14 | The starting two bytes of a PlasCAD file are always `0xca`, `0xfe`. 15 | 16 | The remaining bytes are divided into adjacent packets. Packets can be found in any order. 17 | 18 | 19 | ## Packet structure 20 | - **Byte 0**: Always `0x11` 21 | - **Bytes 1-4**: A 32-bit unsigned integer of payload size, in bytes. 22 | - **Byte 5**: An 8-bit unsigned integer that indicates the packet's type. (See the `Packets` sections below for this mapping.) 23 | - **Bytes 6-end**: The payload; how this is encoded depends on packet type. 24 | 25 | ## Packet types 26 | 27 | ### Sequence: 0 28 | Contains a DNA sequence. 29 | 30 | Bytes 0-3: A 32-bit unsigned integer of sequence length, in nucleotides. 31 | Remaining data: Every two bits is a nucleotide. This packet is always a whole number of bytes; bits in the final byte that 32 | would extend past the sequence length are ignored. Bit assignments for nucleotides is as follows: 33 | 34 | - **T**: `0b00` 35 | - **C**: `0b01` 36 | - **A**: `0b10` 37 | - **G**: `0b11` 38 | 39 | This is the same nucleotide mapping as [.2bit format](http://genome.ucsc.edu/FAQ/FAQformat.html#format7). 40 | 41 | #### An example 42 | The sequence `CTGATTTCTG`. This would serialize as follows, using 7 total bytes: 43 | 44 | - **Bytes 0-3**: `[0, 0, 0, 10]`, to indicate the sequence length of 10 nucleodies. 45 | 46 | Three additional bytes to encode the sequence; each byte can fit 4 nucleotides: 47 | - **Byte 4**: `CTGA` `0b01_00_11_10` 48 | - **Byte 5**: `TTTC` `0b00_00_00_01` 49 | - **Byte 6**: `TG` `0b00_11_00_00`. 50 | 51 | On the final byte, note the 0-fill on the right; we know not to encode it as `T` due to the 52 | sequence length. 53 | 54 | 55 | ## Packets, with their associcated integer type 56 | 57 | ### Features: 1 58 | A bincode serialization of a `Vec` 59 | 60 | ### Primers: 2 61 | A bincode serialization of a `Vec` 62 | 63 | ### Metadata: 3 64 | A bincode serialization of a `Metadata` 65 | 66 | ### IonConcentrations: 6 67 | A bincode serialization of a `IonConcentrations` 68 | 69 | ### Portions: 7 70 | A bincode serialization of a `Portions` 71 | 72 | ### PathLoaded: 10 73 | A bincode serialization of a `Option` 74 | 75 | ### Topology: 11 76 | A bincode serialization of a `Topology` 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /install_scripts/plascad.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=PlasCAD 3 | Exec=~/plascad/plascad 4 | Icon=~/plascad/icon.png 5 | Type=Application 6 | Terminal=false 7 | Categories=Development;Science;Biology; 8 | Comment=Plasmid and Primer editing tools -------------------------------------------------------------------------------- /install_scripts/setup_linux_desktop.sh: -------------------------------------------------------------------------------- 1 | # This file sets up a Linux desktop entry, and moves the application to the home directory. 2 | 3 | NAME_UPPER="PlasCAD" 4 | NAME="plascad" 5 | 6 | printf "Moving the ${NAME_UPPER} executable and icon to ~/${NAME}..." 7 | 8 | chmod +x $NAME 9 | 10 | if [ ! -d ~/${NAME} ]; then 11 | mkdir ~/${NAME} 12 | fi 13 | 14 | cp ${NAME} ~/${NAME} 15 | cp icon.png ~/${NAME}/icon.png 16 | 17 | # Update the desktop entry with the absolute path. 18 | sed "s|~|$HOME|g" ${NAME}.desktop > ~/.local/share/applications/${NAME}.desktop 19 | 20 | printf "\nComplete. You can launch ${NAME_UPPER} through the GUI, eg search \"${NAME_UPPER}\", and/or add to favorites.\n" -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "Crate" 2 | group_imports = "StdExternalCrate" -------------------------------------------------------------------------------- /screenshots/map_aug_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/David-OConnor/plascad/21681308368918f9d1ad51517c47de3f990915a5/screenshots/map_aug_24.png -------------------------------------------------------------------------------- /screenshots/primers_aug_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/David-OConnor/plascad/21681308368918f9d1ad51517c47de3f990915a5/screenshots/primers_aug_24.png -------------------------------------------------------------------------------- /screenshots/protein_aug_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/David-OConnor/plascad/21681308368918f9d1ad51517c47de3f990915a5/screenshots/protein_aug_24.png -------------------------------------------------------------------------------- /screenshots/seq_aug_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/David-OConnor/plascad/21681308368918f9d1ad51517c47de3f990915a5/screenshots/seq_aug_24.png -------------------------------------------------------------------------------- /src/ab1.rs: -------------------------------------------------------------------------------- 1 | //! For operations pertaining to AB1 (Applied Biosystem's sequencing) trace sequence data. 2 | 3 | use std::collections::HashMap; 4 | 5 | use bincode::{Decode, Encode}; 6 | use na_seq::Seq; 7 | 8 | /// The data structure representing AB1 data. 9 | #[derive(Clone, Debug, Default, Encode, Decode)] 10 | pub struct SeqRecordAb1 { 11 | pub id: String, 12 | pub name: String, 13 | pub description: String, 14 | pub sequence: Seq, 15 | pub sequence_user: Option, 16 | pub annotations: HashMap, 17 | pub quality: Option>, 18 | pub quality_user: Option>, 19 | pub peak_heights: Vec, 20 | /// Analyzed data, for each channel. 21 | /// G 22 | pub data_ch1: Vec, 23 | /// A 24 | pub data_ch2: Vec, 25 | /// T 26 | pub data_ch3: Vec, 27 | /// C 28 | pub data_ch4: Vec, 29 | /// Peak locations. 30 | pub peak_locations: Vec, 31 | /// Peak locations edited by user. 32 | pub peak_locations_user: Option>, 33 | } 34 | -------------------------------------------------------------------------------- /src/alignment.rs: -------------------------------------------------------------------------------- 1 | use bio::alignment::{ 2 | Alignment, 3 | distance::simd::{bounded_levenshtein, hamming, levenshtein}, 4 | pairwise::Aligner, 5 | }; 6 | use na_seq::{AminoAcid, Nucleotide, Seq, seq_aa_to_u8_lower, seq_to_u8_lower}; 7 | 8 | #[derive(Clone, Copy, PartialEq)] 9 | pub enum AlignmentMode { 10 | Dna, 11 | AminoAcid, 12 | } 13 | 14 | impl Default for AlignmentMode { 15 | fn default() -> Self { 16 | Self::Dna 17 | } 18 | } 19 | 20 | #[derive(Default)] 21 | pub struct AlignmentState { 22 | pub seq_a: Seq, 23 | pub seq_b: Seq, 24 | pub seq_aa_a: Vec, 25 | pub seq_aa_b: Vec, 26 | // todo: Perhaps these inputs are better fit for State_ui. 27 | pub seq_a_input: String, 28 | pub seq_b_input: String, 29 | pub alignment_result: Option, 30 | pub dist_result: Option, 31 | pub text_display: String, // Ie `AlignmentResult::pretty`. 32 | pub mode: AlignmentMode, 33 | } 34 | 35 | #[derive(Clone, Copy)] 36 | /// Use Hamming Distance if: 37 | /// Sequences are of the same length. 38 | /// You're only looking for substitutions and the sequences are already aligned. 39 | /// Fast, low-complexity calculations are a priority. 40 | /// Use Levenshtein Distance if: 41 | /// Sequences vary in length. 42 | /// Insertions, deletions, and substitutions are all meaningful for your comparison. 43 | /// You need a precise measure of distance for downstream processing. 44 | /// Use Bounded Levenshtein Distance if: 45 | /// You have a known tolerance or threshold for "closeness." 46 | /// You want to filter out sequences that are too different without computing the full distance. 47 | /// Speed and scalability are critical considerations. 48 | pub enum DistanceType { 49 | Hamming, 50 | Levenshtein, 51 | /// Inner: u32 52 | BoundedLevenshtein(u32), 53 | } 54 | 55 | // todo: Which dist algo? bounded_levenshtein, hamming, levenshtein? 56 | /// Accepts UTF-8 byte representation of letters. 57 | fn distance(alpha: &[u8], beta: &[u8]) -> u64 { 58 | let dist_type = if alpha.len() == beta.len() { 59 | DistanceType::Hamming 60 | } else { 61 | DistanceType::Levenshtein 62 | }; 63 | 64 | match dist_type { 65 | DistanceType::Hamming => hamming(&alpha, &beta), 66 | DistanceType::Levenshtein => levenshtein(&alpha, &beta) as u64, 67 | DistanceType::BoundedLevenshtein(k) => { 68 | bounded_levenshtein(&alpha, &beta, k).unwrap() as u64 69 | } 70 | } 71 | } 72 | 73 | pub fn distance_nt(alpha: &[Nucleotide], beta: &[Nucleotide]) -> u64 { 74 | let alpha_ = seq_to_u8_lower(alpha); 75 | let beta_ = seq_to_u8_lower(beta); 76 | distance(&alpha_, &beta_) 77 | } 78 | 79 | pub fn distance_aa(alpha: &[AminoAcid], beta: &[AminoAcid]) -> u64 { 80 | let alpha_ = seq_aa_to_u8_lower(alpha); 81 | let beta_ = seq_aa_to_u8_lower(beta); 82 | distance(&alpha_, &beta_) 83 | } 84 | 85 | fn align_pairwise(seq_0: &[u8], seq_1: &[u8]) -> (Alignment, String) { 86 | // todo: Lots of room to configure this. 87 | let score = |a: u8, b: u8| if a == b { 1i32 } else { -1i32 }; 88 | 89 | let mut aligner = Aligner::with_capacity(seq_0.len(), seq_1.len(), -5, -1, &score); 90 | 91 | // todo: Global? Semiglobal? Local? 92 | let result = aligner.semiglobal(seq_0, seq_1); 93 | 94 | let text = result.pretty(seq_0, seq_1, 120); 95 | 96 | (result, text) 97 | } 98 | 99 | pub fn align_pairwise_nt(seq_0: &[Nucleotide], seq_1: &[Nucleotide]) -> (Alignment, String) { 100 | // todo: Lots of room to configure this. 101 | let seq_0_ = seq_to_u8_lower(seq_0); 102 | let seq_1_ = seq_to_u8_lower(seq_1); 103 | 104 | align_pairwise(&seq_0_, &seq_1_) 105 | } 106 | 107 | pub fn align_pairwise_aa(seq_0: &[AminoAcid], seq_1: &[AminoAcid]) -> (Alignment, String) { 108 | // todo: Lots of room to configure this. 109 | let seq_0_ = seq_aa_to_u8_lower(seq_0); 110 | let seq_1_ = seq_aa_to_u8_lower(seq_1); 111 | 112 | align_pairwise(&seq_0_, &seq_1_) 113 | } 114 | -------------------------------------------------------------------------------- /src/alignment_map.rs: -------------------------------------------------------------------------------- 1 | //! Data structures and related code for BAM and SAM alignment map data. 2 | //! [SAM/BAM spec document, CAO Nov 2024](https://samtools.github.io/hts-specs/SAMv1.pdf) 3 | //! 4 | //! BAM format is little endian. 5 | 6 | use std::{ 7 | fs::File, 8 | io, 9 | io::{BufRead, ErrorKind, Read}, 10 | path::Path, 11 | }; 12 | 13 | use bgzip::{BGZFError, BGZFReader, index::BGZFIndex, read::IndexedBGZFReader}; 14 | use flate2::read::{GzDecoder, MultiGzDecoder}; 15 | 16 | /// Syntax helper for parsing multi-byte fields into primitives. 17 | /// 18 | /// Example: `parse_le!(bytes, i32, 5..9);` 19 | #[macro_export] 20 | macro_rules! parse_le { 21 | ($bytes:expr, $t:ty, $range:expr) => {{ <$t>::from_le_bytes($bytes[$range].try_into().unwrap()) }}; 22 | } 23 | 24 | const MAGIC: [u8; 4] = [b'B', b'A', b'M', 1]; 25 | 26 | #[derive(Debug)] 27 | struct RefSeq { 28 | pub name: String, 29 | pub l_ref: u32, 30 | } 31 | 32 | impl RefSeq { 33 | /// Deserialize from BAM. 34 | pub fn from_buf(buf: &[u8]) -> io::Result { 35 | let l_name = parse_le!(buf, u32, 0..4); 36 | let name_end = 4 + l_name as usize; 37 | 38 | Ok(Self { 39 | // todo: Don't unwrap this string parsing; map the error 40 | // name_end - 1: Don't parse the trailing NUL char. (In the spec, this is a null-terminated string) 41 | name: String::from_utf8(buf[4..name_end - 1].to_vec()).unwrap(), 42 | l_ref: parse_le!(buf, u32, name_end..name_end + 4), 43 | }) 44 | } 45 | 46 | /// Serialize to BAM. 47 | pub fn to_buf(&self) -> Vec { 48 | Vec::new() 49 | } 50 | } 51 | 52 | #[derive(Debug)] 53 | struct AuxData { 54 | pub tag: [u8; 2], 55 | pub val_type: u8, 56 | /// corresponds to val_type. 57 | pub value: Vec, // todo: Placeholder 58 | } 59 | 60 | #[derive(Debug)] 61 | struct Alignment { 62 | /// Size of the entire alignment packet, *except the 4-byte block size*. Includes aux data. 63 | pub block_size: u32, 64 | pub ref_id: i32, 65 | pub pos: i32, 66 | pub mapq: u8, 67 | pub bin: u16, 68 | pub n_cigar_op: u16, 69 | pub flag: u16, 70 | pub next_ref_id: i32, 71 | pub next_pos: i32, 72 | pub tlen: i32, 73 | pub read_name: String, 74 | pub cigar: Vec, 75 | pub seq: Vec, // 4-bit encoded 76 | pub qual: String, // todo: Vec? 77 | pub aux_data: Vec, 78 | } 79 | 80 | impl Alignment { 81 | /// Deserialize from BAM. 82 | pub fn from_buf(buf: &[u8]) -> io::Result { 83 | if buf.len() < 36 { 84 | return Err(io::Error::new( 85 | ErrorKind::InvalidData, 86 | "Buffer is too small to contain a valid Alignment record", 87 | )); 88 | } 89 | 90 | let block_size = parse_le!(buf, u32, 0..4); 91 | 92 | println!("Block size: {:?}", block_size); 93 | 94 | let ref_id = parse_le!(buf, i32, 4..8); 95 | let pos = parse_le!(buf, i32, 8..12); 96 | 97 | let l_read_name = buf[12] as usize; 98 | let mapq = buf[13]; 99 | 100 | let bin = parse_le!(buf, u16, 14..16); 101 | let n_cigar_op = parse_le!(buf, u16, 16..18); 102 | let flag = parse_le!(buf, u16, 18..20); 103 | 104 | let l_seq = parse_le!(buf, u32, 20..24) as usize; 105 | 106 | let next_ref_id = parse_le!(buf, i32, 24..28); 107 | let next_pos = parse_le!(buf, i32, 28..32); 108 | let tlen = parse_le!(buf, i32, 32..36); 109 | 110 | let mut i = 36; 111 | // -1: Ommit the trailing null. 112 | println!("Len read name: {:?}", l_read_name); 113 | let read_name = String::from_utf8(buf[i..i + l_read_name - 1].to_vec()) 114 | .map_err(|e| io::Error::new(ErrorKind::InvalidData, e))?; 115 | i += l_read_name; 116 | 117 | let cigar_len = n_cigar_op as usize; 118 | let mut cigar = Vec::new(); 119 | for _ in 0..cigar_len { 120 | let cigar_op = parse_le!(buf, u32, i..i + 4); 121 | cigar.push(cigar_op); 122 | i += 4; 123 | } 124 | 125 | // todo: Figure out how this is actually handled, and set the struct type A/R. 126 | let seq_len = (l_seq + 1) / 2; // Each nucleotide is 4 bits 127 | let seq = buf[i..i + seq_len].to_vec(); 128 | i += seq_len; 129 | 130 | let qual = buf[i..i + l_seq].to_vec(); 131 | i += l_seq; 132 | 133 | let aux_data = Vec::new(); // todo: Parse auxiliary data from the remaining bytes. 134 | 135 | Ok(Self { 136 | block_size, 137 | ref_id, 138 | pos, 139 | mapq, 140 | bin, 141 | n_cigar_op, 142 | flag, 143 | next_ref_id, 144 | next_pos, 145 | tlen, 146 | read_name, 147 | cigar, 148 | seq, 149 | qual: String::new(), // Placeholder until auxiliary data parsing 150 | aux_data, 151 | }) 152 | } 153 | 154 | /// Serialize to BAM. 155 | pub fn to_buf_(&self) -> Vec { 156 | // todo: If desired. 157 | Vec::new() 158 | } 159 | } 160 | 161 | #[derive(Debug)] 162 | /// Represents BAM or SAM data. See the spec document, section 4.2. This maps directly to the BAM format. 163 | /// Fields, and their types, here and in sub-structs are taken directly from this table. 164 | pub struct AlignmentMap { 165 | pub l_text: u32, 166 | pub text: String, 167 | pub n_ref: u32, 168 | pub refs: Vec, 169 | pub alignments: Vec, 170 | } 171 | 172 | impl AlignmentMap { 173 | /// Deserialize an alignment record from a buffer containing BAM data. 174 | pub fn from_buf(buf: &[u8]) -> io::Result { 175 | if buf.len() < 12 { 176 | return Err(io::Error::new( 177 | ErrorKind::InvalidData, 178 | "Buffer is too small to contain a valid header", 179 | )); 180 | } 181 | 182 | let l_text = parse_le!(buf, u32, 4..8); 183 | let text_end = 8 + l_text as usize; 184 | let n_ref = parse_le!(buf, u32, text_end..text_end + 4); 185 | let refs_start = text_end + 4; 186 | 187 | let mut i = refs_start; 188 | let mut refs = Vec::new(); 189 | for _ in 0..n_ref { 190 | let ref_seq = RefSeq::from_buf(buf[i..].try_into().unwrap())?; 191 | i += 9 + ref_seq.name.len(); 192 | 193 | refs.push(ref_seq); 194 | } 195 | 196 | let mut alignments = Vec::new(); 197 | 198 | while i + 1 < buf.len() { 199 | let alignment = Alignment::from_buf(buf[i..].try_into().unwrap())?; 200 | 201 | println!("\n\n Alignment: {:?}", alignment); 202 | 203 | i += 4 + alignment.block_size as usize; 204 | 205 | alignments.push(alignment); 206 | } 207 | 208 | if buf[0..4] != MAGIC { 209 | return Err(io::Error::new( 210 | ErrorKind::InvalidData, 211 | "Incorrect BAM magic.".to_string(), 212 | )); 213 | } 214 | 215 | Ok(Self { 216 | l_text, 217 | // todo: Map the error; don't unwrap. 218 | text: String::from_utf8(buf[8..text_end].to_vec()).unwrap(), 219 | n_ref, 220 | refs, 221 | alignments, 222 | }) 223 | } 224 | 225 | /// Serialize to BAM. 226 | pub fn to_buf(&self) -> Vec { 227 | Vec::new() 228 | } 229 | } 230 | 231 | pub fn import(path: &Path) -> io::Result { 232 | let file = File::open(path)?; 233 | let mut decoder = GzDecoder::new(file); 234 | // let mut decoder = MultiGzDecoder::new(file); 235 | 236 | println!("Decoding file..."); 237 | 238 | let mut buf = Vec::new(); 239 | // let mut buf = [0; 80_000]; 240 | decoder.read_to_end(&mut buf)?; 241 | 242 | println!("Decode complete. Reading..."); 243 | 244 | let header = AlignmentMap::from_buf(&buf); 245 | 246 | header 247 | 248 | // AlignmentMap::from_buf(&buf) 249 | } 250 | 251 | // pub fn import(path: &Path) -> io::Result { 252 | // let file = File::open(path)?; 253 | // 254 | // let mut reader = 255 | // BGZFReader::new(file).unwrap(); // todo: Map error. 256 | // 257 | // // let index = BGZFIndex::from_reader(reader)?; 258 | // 259 | // // println!("Index entries: {:?}", index.entries()); 260 | // 261 | // // let mut reader = 262 | // // IndexedBGZFReader::new(reader, index).unwrap(); // todo: Map error. 263 | // 264 | // // println!("Voffset A: {:?}", reader.bgzf_pos()); 265 | // 266 | // reader.bgzf_seek(0).unwrap(); // todo: Map error. 267 | // // reader.seek(0).unwrap(); // todo: Map error. 268 | // 269 | // // let mut buf = Vec::new(); 270 | // let mut buf = vec![0; 80_000]; 271 | // 272 | // // reader.read_to_end(&mut buf).unwrap(); // todo: Map error. 273 | // reader.read(&mut buf).unwrap(); // todo: Map error. 274 | // 275 | // println!("Voffset: {:?}", reader.bgzf_pos()); 276 | // 277 | // println!("BUf contents: {:?}", &buf[13180..13220]); 278 | // 279 | // AlignmentMap::from_buf(&buf) 280 | // } 281 | 282 | // /// todo: Move to `file_io` A/R. 283 | // /// Note: BAM files are compressed using BGZF, and are therefore split into blocks no larger than 64kb. 284 | // pub fn import(path: &Path) -> io::Result { 285 | // let mut file = File::open(path)?; 286 | // let mut decompressed_data = Vec::new(); 287 | // 288 | // // todo: Organize this A/R. 289 | // 290 | // loop { 291 | // println!("BAM block...\n"); 292 | // // Read BGZF block header (18 bytes) 293 | // let mut block_header = [0u8; 18]; 294 | // if let Err(e) = file.read_exact(&mut block_header) { 295 | // if e.kind() == ErrorKind::UnexpectedEof { 296 | // break; // Reached end of file 297 | // } 298 | // return Err(e); 299 | // } 300 | // 301 | // // Parse block size (last 2 bytes of header, little-endian) 302 | // let block_size = u16::from_le_bytes([block_header[16], block_header[17]]) as usize; 303 | // 304 | // println!("Block size: {:?}", block_size); 305 | // 306 | // if block_size < 18 { 307 | // return Err(io::Error::new( 308 | // ErrorKind::InvalidData, 309 | // "Invalid BGZF block size", 310 | // )); 311 | // } 312 | // 313 | // // Read the block data 314 | // let mut block_data = vec![0u8; block_size - 18]; 315 | // file.read_exact(&mut block_data)?; 316 | // 317 | // // Decompress the block 318 | // let mut decoder = GzDecoder::new(&block_data[..]); 319 | // let mut decompressed_block = Vec::new(); 320 | // decoder.read_to_end(&mut decompressed_block)?; 321 | // 322 | // // Append to the full decompressed buffer 323 | // decompressed_data.extend_from_slice(&decompressed_block); 324 | // } 325 | // 326 | // AlignmentMap::from_buf(&decompressed_data) 327 | // } 328 | 329 | /// Calculate bin given an alignment covering [beg, end) (zero-based, half-closed-half-open) 330 | /// This is adapted from C code in the spec document. 331 | fn reg_to_bin(beg: i32, end: i32) -> i32 { 332 | let end = end - 1; // Adjust end to be inclusive 333 | if beg >> 14 == end >> 14 { 334 | return ((1 << 15) - 1) / 7 + (beg >> 14); 335 | } 336 | if beg >> 17 == end >> 17 { 337 | return ((1 << 12) - 1) / 7 + (beg >> 17); 338 | } 339 | if beg >> 20 == end >> 20 { 340 | return ((1 << 9) - 1) / 7 + (beg >> 20); 341 | } 342 | if beg >> 23 == end >> 23 { 343 | return ((1 << 6) - 1) / 7 + (beg >> 23); 344 | } 345 | if beg >> 26 == end >> 26 { 346 | return ((1 << 3) - 1) / 7 + (beg >> 26); 347 | } 348 | 0 349 | } 350 | 351 | /// Calculate the list of bins that may overlap with region [beg, end) (zero-based) 352 | /// This is adapted from C code in the spec document. 353 | fn reg_to_bins(beg: i32, end: i32, list: &mut Vec) -> usize { 354 | let end = end - 1; // Adjust end to be inclusive 355 | list.push(0); 356 | for k in (1 + (beg >> 26))..=(1 + (end >> 26)) { 357 | list.push(k as u16); 358 | } 359 | for k in (9 + (beg >> 23))..=(9 + (end >> 23)) { 360 | list.push(k as u16); 361 | } 362 | for k in (73 + (beg >> 20))..=(73 + (end >> 20)) { 363 | list.push(k as u16); 364 | } 365 | for k in (585 + (beg >> 17))..=(585 + (end >> 17)) { 366 | list.push(k as u16); 367 | } 368 | for k in (4681 + (beg >> 14))..=(4681 + (end >> 14)) { 369 | list.push(k as u16); 370 | } 371 | list.len() 372 | } 373 | -------------------------------------------------------------------------------- /src/external_websites.rs: -------------------------------------------------------------------------------- 1 | //! For opening the browser to NCBI BLAST, PDB etc. 2 | //! 3 | //! PDB Search API: https://search.rcsb.org/#search-api 4 | //! PDB Data API: https://data.rcsb.org/#data-api 5 | 6 | use bio_apis::ncbi; 7 | 8 | use crate::{Selection, state::State}; 9 | 10 | /// BLAST the selected Feature, primer, or selection. Prioritize the selection. 11 | /// This function handles extracting the sequence to BLAST from possible selections. 12 | pub fn blast(state: &State) { 13 | let data = &state.generic[state.active]; 14 | 15 | let val = match state.ui.text_selection { 16 | Some(sel) => { 17 | // Don't format sel directly, as we insert the bp count downstream for use with feature selections. 18 | Some(( 19 | sel.index_seq(&data.seq), 20 | format!("{}, {}..{}", data.metadata.plasmid_name, sel.start, sel.end), 21 | )) 22 | } 23 | None => match state.ui.selected_item { 24 | Selection::Feature(feat_i) => { 25 | if feat_i >= data.features.len() { 26 | eprintln!("Invalid selected feature"); 27 | None 28 | } else { 29 | let feature = &data.features[feat_i]; 30 | Some((feature.range.index_seq(&data.seq), feature.label.clone())) 31 | } 32 | } 33 | Selection::Primer(prim_i) => { 34 | if prim_i >= data.primers.len() { 35 | eprintln!("Invalid selected primer"); 36 | None 37 | } else { 38 | let primer = &data.primers[prim_i]; 39 | Some((Some(&primer.sequence[..]), primer.name.clone())) 40 | } 41 | } 42 | Selection::None => None, 43 | }, 44 | }; 45 | 46 | // todo: Handle reverse. 47 | 48 | if let Some((seq, name)) = val { 49 | if let Some(s) = seq { 50 | ncbi::open_blast(s, &name); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/feature_db_builder.rs: -------------------------------------------------------------------------------- 1 | //! This is the entrypoint for a standalone program that parse features from GenBank and SnapGene files. 2 | //! It assigns each sequence a label and feature type. 3 | 4 | use std::{ 5 | fs, 6 | path::{Path, PathBuf}, 7 | str::FromStr, 8 | }; 9 | 10 | mod file_io; 11 | mod gui; 12 | mod primer; 13 | mod sequence; // Required due to naviation::Page being in file_io::save 14 | 15 | // mod main; // todo temp 16 | mod feature_db_load; // todo temp 17 | 18 | use sequence::{FeatureType, Seq}; 19 | 20 | use crate::file_io::genbank; 21 | 22 | const SOURCE_DIR: &str = "data"; 23 | 24 | // struct FeatureMapItem { 25 | // feature_name: String, 26 | // feature_type: FeatureType, 27 | // seq: Seq, 28 | // } 29 | 30 | fn collect_files_in_directory(dir: &Path) -> Vec { 31 | let mut files = Vec::new(); 32 | 33 | if dir.is_dir() { 34 | for entry in fs::read_dir(dir).unwrap() { 35 | let path = entry.unwrap().path(); 36 | if path.is_file() { 37 | files.push(path); 38 | } 39 | } 40 | } 41 | 42 | files 43 | } 44 | 45 | /// Combine feature maps that have identical sequences. 46 | fn consolidate(feature_map: &[FeatureMapItem]) -> Vec { 47 | let mut result = Vec::new(); 48 | 49 | result 50 | } 51 | 52 | fn main() { 53 | // todo: all files 54 | let files_genbank = collect_files_in_directory(&PathBuf::from_str(SOURCE_DIR).unwrap()); 55 | let files_snapgene = []; 56 | 57 | let mut feature_maps = Vec::new(); 58 | 59 | for file in files_genbank { 60 | let path = PathBuf::from_str(file).unwrap(); 61 | 62 | if let Ok(data) = genbank::import_genbank(&path) { 63 | for feature in &data.features { 64 | feature_maps.push({ 65 | FeatureMapItem { 66 | name: feature.name.clone(), 67 | feature_type: feature.feature_type, 68 | seq: data.seq[feature.range.0 - 1..feature.range.1], 69 | } 70 | }) 71 | } 72 | } 73 | } 74 | 75 | // todo: SQlite DB? 76 | } 77 | -------------------------------------------------------------------------------- /src/file_io/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module contains code for saving and loading in several file formats. 2 | 3 | use std::{path::Path, sync::Arc}; 4 | 5 | use egui_file_dialog::{FileDialog, FileDialogConfig}; 6 | use na_seq::{Seq, SeqTopology}; 7 | 8 | use crate::{ 9 | file_io::save::{DEFAULT_DNA_FILE, DEFAULT_FASTA_FILE, DEFAULT_GENBANK_FILE, QUICKSAVE_FILE}, 10 | misc_types::{Feature, Metadata}, 11 | primer::Primer, 12 | }; 13 | 14 | pub mod ab1; 15 | mod ab1_tags; 16 | pub mod genbank; 17 | mod pcad; 18 | pub mod save; 19 | pub mod snapgene; 20 | 21 | /// The most important data to store, used by our format, GenBank, and SnapGene. 22 | /// We use this in our main State struct to keep track of this data. 23 | #[derive(Default, Clone)] 24 | pub struct GenericData { 25 | pub seq: Seq, 26 | pub topology: SeqTopology, 27 | pub features: Vec, 28 | pub primers: Vec, 29 | pub metadata: Metadata, 30 | } 31 | 32 | pub struct FileDialogs { 33 | pub save: FileDialog, 34 | pub load: FileDialog, 35 | pub export_fasta: FileDialog, 36 | pub export_genbank: FileDialog, 37 | pub export_dna: FileDialog, 38 | pub cloning_load: FileDialog, 39 | } 40 | 41 | impl Default for FileDialogs { 42 | fn default() -> Self { 43 | // We can't clone `FileDialog`; use this to reduce repetition instead. 44 | let cfg_import = FileDialogConfig { 45 | // todo: Explore other optiosn A/R 46 | ..Default::default() 47 | } 48 | .add_file_filter_extensions("PlasCAD", vec!["pcad"]) 49 | .add_file_filter_extensions("FASTA", vec!["fasta"]) 50 | .add_file_filter_extensions("GenBank", vec!["gb, gbk"]) 51 | .add_file_filter_extensions("SnapGene DNA", vec!["dna"]) 52 | .add_file_filter_extensions( 53 | "PCAD/FASTA/GB/DNA/AB1", 54 | vec!["pcad", "fasta, gb, gbk, dna, ab1"], 55 | ); 56 | 57 | let save = FileDialog::new() 58 | // .add_quick_access("Project", |s| { 59 | // s.add_path("☆ Examples", "examples"); 60 | // }) 61 | .add_save_extension("PlasCAD", "pcad") 62 | .default_save_extension("PlasCAD") 63 | .default_file_name(QUICKSAVE_FILE); 64 | // .id("0"); 65 | 66 | let import = FileDialog::with_config(cfg_import.clone()) 67 | .default_file_filter("PCAD/FASTA/GB/DNA/AB1"); 68 | 69 | let export_fasta = FileDialog::new() 70 | .add_save_extension("FASTA", "fasta") 71 | .default_save_extension("FASTA") 72 | .default_file_name(DEFAULT_FASTA_FILE); 73 | 74 | let export_genbank = FileDialog::new() 75 | .add_save_extension("GenBank", "gb") 76 | .default_save_extension("GenBank") 77 | .default_file_name(DEFAULT_GENBANK_FILE); 78 | 79 | let export_dna = FileDialog::new() 80 | .add_save_extension("SnapGene DNA", "dna") 81 | .default_save_extension("SnapGene DNA") 82 | .default_file_name(DEFAULT_DNA_FILE); 83 | 84 | let cloning_import = 85 | FileDialog::with_config(cfg_import).default_file_filter("PCAD/FASTA/GB/DNA/AB1"); 86 | 87 | Self { 88 | save, 89 | // load: load_, 90 | load: import, 91 | export_fasta, 92 | export_genbank, 93 | export_dna, 94 | cloning_load: cloning_import, 95 | // selected: None, 96 | } 97 | } 98 | } 99 | 100 | /// There doesn't seem to be a clear name in GenBank or Snapgene formats; use the filename. 101 | /// Note: This includes error checking, but this should always pass under normal circumstances. 102 | fn get_filename(path: &Path) -> String { 103 | if let Some(file_name) = path.file_stem() { 104 | file_name 105 | .to_str() 106 | .map(|s| s.to_string()) 107 | .unwrap_or_default() 108 | } else { 109 | String::new() 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/file_io/pcad.rs: -------------------------------------------------------------------------------- 1 | //! This module contains code for saving and loading in our own PCAD format. 2 | //! 3 | //! This is a binary format that uses packets for each of several message types. This sytem 4 | //! should have better backwards compatibility than raw serialization and deserialization using 5 | //! Bincode or similar. 6 | //! 7 | //! Byte encoding is big endian. 8 | 9 | use std::{io, io::ErrorKind}; 10 | 11 | use bincode::config; 12 | use na_seq::{deser_seq_bin, serialize_seq_bin}; 13 | use num_enum::TryFromPrimitive; 14 | 15 | use crate::file_io::save::StateToSave; 16 | 17 | const START_BYTES: [u8; 2] = [0xca, 0xfe]; // Arbitrary, used as a sanity check. 18 | const PACKET_START: u8 = 0x11; 19 | const PACKET_OVERHEAD: usize = 6; // packet start, packet type, message size. 20 | 21 | #[repr(u8)] 22 | #[derive(Clone, Copy, PartialEq, TryFromPrimitive)] 23 | pub enum PacketType { 24 | Sequence = 0, 25 | Features = 1, 26 | Primers = 2, 27 | Metadata = 3, 28 | // IonConcentrations = 6, 29 | Portions = 7, 30 | // PathLoaded = 10, 31 | Topology = 11, 32 | Ab1 = 12, 33 | } 34 | 35 | /// Byte 0: Standard packet start. Bytes 1-4: u32 of payload len. Bytes 5[..]: Payload. 36 | pub struct Packet { 37 | type_: PacketType, 38 | payload: Vec, 39 | } 40 | 41 | impl Packet { 42 | /// Note: Bytes includes this payload, and potentially until the end of the entire file data. 43 | /// We use the length bytes to know when to stop reading. 44 | pub fn from_bytes(bytes: &[u8]) -> io::Result { 45 | if bytes.len() < PACKET_OVERHEAD { 46 | return Err(io::Error::new( 47 | ErrorKind::InvalidData, 48 | "Packet must be at least 2 bytes", 49 | )); 50 | } 51 | 52 | if bytes[0] != PACKET_START { 53 | return Err(io::Error::new( 54 | ErrorKind::InvalidData, 55 | "Invalid packet start byte in PCAD file.", 56 | )); 57 | } 58 | 59 | // todo: This may not be necessary, due to the vec being passed. 60 | let payload_size = u32::from_be_bytes(bytes[1..5].try_into().unwrap()) as usize; 61 | if bytes.len() < PACKET_OVERHEAD + payload_size { 62 | return Err(io::Error::new( 63 | ErrorKind::InvalidData, 64 | "Remaining payload is too short based on read packet len in PCAD file.", 65 | )); 66 | } 67 | 68 | Ok(Self { 69 | type_: bytes[5].try_into().map_err(|_| { 70 | io::Error::new(ErrorKind::InvalidData, "Invalid packet type received") 71 | })?, 72 | payload: bytes[PACKET_OVERHEAD..PACKET_OVERHEAD + payload_size].to_vec(), // todo: This essentially clones? Not ideal. 73 | }) 74 | } 75 | 76 | pub fn to_bytes(&self) -> Vec { 77 | let mut result = vec![PACKET_START]; 78 | 79 | let len = self.payload.len() as u32; 80 | result.extend(&len.to_be_bytes()); 81 | 82 | result.push(self.type_ as u8); 83 | result.extend(&self.payload); 84 | 85 | result 86 | } 87 | } 88 | 89 | impl StateToSave { 90 | /// Serialize state as bytes in the PCAD format, e.g. for file saving. 91 | pub fn to_bytes(&self) -> Vec { 92 | let cfg = config::standard(); 93 | let mut result = Vec::new(); 94 | 95 | result.extend(&START_BYTES); 96 | 97 | // Note: The order we add these packets in doesn't make a difference for loading. 98 | 99 | let seq_packet = Packet { 100 | type_: PacketType::Sequence, 101 | payload: serialize_seq_bin(&self.generic.seq), 102 | }; 103 | 104 | let features_packet = Packet { 105 | type_: PacketType::Features, 106 | payload: bincode::encode_to_vec(&self.generic.features, cfg).unwrap(), 107 | }; 108 | 109 | let primers_packet = Packet { 110 | type_: PacketType::Primers, 111 | payload: bincode::encode_to_vec(&self.generic.primers, cfg).unwrap(), 112 | }; 113 | 114 | let metadata_packet = Packet { 115 | type_: PacketType::Metadata, 116 | payload: bincode::encode_to_vec(&self.generic.metadata, cfg).unwrap(), 117 | }; 118 | 119 | let topology_packet = Packet { 120 | type_: PacketType::Topology, 121 | payload: bincode::encode_to_vec(&self.generic.topology, cfg).unwrap(), 122 | }; 123 | 124 | // let ion_concentrations_packet = Packet { 125 | // type_: PacketType::IonConcentrations, 126 | // payload: bincode::encode_to_vec(&self.ion_concentrations, cfg).unwrap(), 127 | // }; 128 | 129 | let portions_packet = Packet { 130 | type_: PacketType::Portions, 131 | payload: bincode::encode_to_vec(&self.portions, cfg).unwrap(), 132 | }; 133 | 134 | // let path_loaded_packet = Packet { 135 | // type_: PacketType::PathLoaded, 136 | // payload: bincode::encode_to_vec(&self.path_loaded, cfg).unwrap(), 137 | // }; 138 | 139 | let ab1_packet = Packet { 140 | type_: PacketType::Ab1, 141 | payload: bincode::encode_to_vec(&self.ab1_data, cfg).unwrap(), 142 | }; 143 | 144 | result.extend(&seq_packet.to_bytes()); 145 | result.extend(&features_packet.to_bytes()); 146 | result.extend(&primers_packet.to_bytes()); 147 | result.extend(&metadata_packet.to_bytes()); 148 | result.extend(&topology_packet.to_bytes()); 149 | result.extend(&ab1_packet.to_bytes()); 150 | 151 | // result.extend(&ion_concentrations_packet.to_bytes()); 152 | result.extend(&portions_packet.to_bytes()); 153 | // result.extend(&path_loaded_packet.to_bytes()); 154 | 155 | result 156 | } 157 | 158 | /// Deserialize state as bytes in the PCAD format, e.g. for file loading. 159 | pub fn from_bytes(bytes: &[u8]) -> io::Result { 160 | if bytes[0..2] != START_BYTES { 161 | return Err(io::Error::new( 162 | ErrorKind::InvalidData, 163 | "Invalid start bytes in PCAD file.", 164 | )); 165 | } 166 | 167 | let cfg = config::standard(); 168 | let mut result = StateToSave::default(); 169 | 170 | let mut i = START_BYTES.len(); 171 | loop { 172 | if i + PACKET_OVERHEAD > bytes.len() { 173 | break; // End of the packet. 174 | } 175 | 176 | let bytes_remaining = &bytes[i..]; 177 | let packet = match Packet::from_bytes(bytes_remaining) { 178 | Ok(p) => p, 179 | Err(e) => { 180 | eprintln!("Problem opening a packet: {:?}", e); 181 | let payload_size = 182 | u32::from_be_bytes(bytes_remaining[1..5].try_into().unwrap()) as usize; 183 | i += PACKET_OVERHEAD + payload_size; 184 | continue; 185 | } 186 | }; 187 | 188 | i += PACKET_OVERHEAD + packet.payload.len(); 189 | 190 | // Now, add packet data to our result A/R. 191 | match packet.type_ { 192 | PacketType::Sequence => match deser_seq_bin(&packet.payload) { 193 | Ok(v) => result.generic.seq = v, 194 | Err(e) => eprintln!("Error decoding sequence packet: {e}"), 195 | }, 196 | PacketType::Features => match bincode::decode_from_slice(&packet.payload, cfg) { 197 | Ok(v) => result.generic.features = v.0, 198 | Err(e) => eprintln!("Error decoding features packet: {e}"), 199 | }, 200 | PacketType::Primers => match bincode::decode_from_slice(&packet.payload, cfg) { 201 | Ok(v) => result.generic.primers = v.0, 202 | Err(e) => eprintln!("Error decoding primers packet: {e}"), 203 | }, 204 | PacketType::Metadata => match bincode::decode_from_slice(&packet.payload, cfg) { 205 | Ok(v) => result.generic.metadata = v.0, 206 | Err(e) => eprintln!("Error decoding metadata packet: {e}"), 207 | }, 208 | PacketType::Topology => match bincode::decode_from_slice(&packet.payload, cfg) { 209 | Ok(v) => result.generic.topology = v.0, 210 | Err(e) => eprintln!("Error decoding topology packet: {e}"), 211 | }, 212 | // PacketType::IonConcentrations => { 213 | // match bincode::decode_from_slice(&packet.payload, cfg) { 214 | // Ok(v) => result.ion_concentrations = v.0, 215 | // Err(e) => eprintln!("Error decoding ion concentrations packet: {e}"), 216 | // } 217 | // } 218 | PacketType::Portions => match bincode::decode_from_slice(&packet.payload, cfg) { 219 | Ok(v) => result.portions = v.0, 220 | Err(e) => eprintln!("Error decoding portions packet: {e}"), 221 | }, 222 | PacketType::Ab1 => match bincode::decode_from_slice(&packet.payload, cfg) { 223 | Ok(v) => result.ab1_data = v.0, 224 | Err(e) => eprintln!("Error decoding AB1 packet: {e}"), 225 | }, 226 | // PacketType::PathLoaded => match bincode::decode_from_slice(&packet.payload, cfg) { 227 | // Ok(v) => result.path_loaded = v.0, 228 | // Err(e) => eprintln!("Error decoding Seq packet: {e}"), 229 | // }, 230 | } 231 | } 232 | 233 | Ok(result) 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/gui/ab1.rs: -------------------------------------------------------------------------------- 1 | //! Contains code for viewing AB1 sequencing data, e.g. from Sanger sequencing. 2 | 3 | use copypasta::{ClipboardContext, ClipboardProvider}; 4 | use eframe::{ 5 | egui::{ 6 | Align2, Color32, FontFamily, FontId, Frame, Pos2, Rect, RichText, Sense, Shape, Slider, 7 | Stroke, Ui, pos2, vec2, 8 | }, 9 | emath::RectTransform, 10 | epaint::PathShape, 11 | }; 12 | use na_seq::{Nucleotide, seq_to_str_lower}; 13 | 14 | use crate::{ 15 | ab1::SeqRecordAb1, 16 | feature_db_load::find_features, 17 | file_io::GenericData, 18 | gui::{BACKGROUND_COLOR, COL_SPACING, ROW_SPACING}, 19 | misc_types::Metadata, 20 | state::State, 21 | util::merge_feature_sets, 22 | }; 23 | 24 | const NT_COLOR: Color32 = Color32::from_rgb(180, 220, 220); 25 | const NT_WIDTH: f32 = 8.; // pixels 26 | const STROKE_WIDTH_PEAK: f32 = 1.; 27 | const PEAK_WIDTH_DIV2: f32 = 2.; 28 | 29 | // Peak heights are normallized, so that the maximum value is this. 30 | const PEAK_MAX_HEIGHT: f32 = 120.; 31 | 32 | const COLOR_A: Color32 = Color32::from_rgb(20, 220, 20); 33 | const COLOR_C: Color32 = Color32::from_rgb(130, 130, 255); 34 | const COLOR_T: Color32 = Color32::from_rgb(255, 100, 100); 35 | const COLOR_G: Color32 = Color32::from_rgb(200, 200, 200); 36 | 37 | /// This mapping is based off conventions in other software. 38 | fn nt_color_map(nt: Nucleotide) -> Color32 { 39 | match nt { 40 | Nucleotide::A => COLOR_A, 41 | Nucleotide::C => COLOR_C, 42 | Nucleotide::T => COLOR_T, 43 | Nucleotide::G => COLOR_G, 44 | } 45 | } 46 | 47 | /// Map sequence index to a horizontal pixel. 48 | fn index_to_posit(i: usize, num_nts: usize, ui: &Ui) -> f32 { 49 | // todo: Cap if width too small for nt len. Varying zoom, etc. 50 | // let width = ui.available_width(); 51 | // 52 | // let num_nts_disp = width / NT_WIDTH; 53 | 54 | // let scale_factor = width / num_nts as f32; 55 | i as f32 * NT_WIDTH 56 | } 57 | 58 | /// Plot the peaks and confidence values; draw letters 59 | fn plot(data: &SeqRecordAb1, to_screen: &RectTransform, start_i: usize, ui: &mut Ui) -> Vec { 60 | let mut result = Vec::new(); 61 | 62 | let nt_y = 160.; 63 | let plot_y = nt_y - 20.; 64 | 65 | // todo: Don't run this calc every time. 66 | let data_scaler = { 67 | let mut max_peak = 0; 68 | for i in 0..data.data_ch1.len() { 69 | if i > 40 { 70 | // todo: Sloppy performance saver. 71 | continue; 72 | } 73 | 74 | let ch1 = data.data_ch1[i]; 75 | let ch2 = data.data_ch2[i]; 76 | let ch3 = data.data_ch3[i]; 77 | let ch4 = data.data_ch4[i]; 78 | 79 | for ch in [ch1, ch2, ch3, ch4] { 80 | if ch > max_peak { 81 | max_peak = ch; 82 | } 83 | } 84 | } 85 | 86 | PEAK_MAX_HEIGHT / max_peak as f32 87 | }; 88 | 89 | let width = ui.available_width(); 90 | let num_nts_disp = width / NT_WIDTH; 91 | 92 | // Display nucleotides and quality values. 93 | for i_pos in 0..num_nts_disp as usize { 94 | let i = start_i + i_pos; 95 | if data.sequence.is_empty() || i > data.sequence.len() - 1 { 96 | break; 97 | } 98 | 99 | let nt = data.sequence[i]; 100 | 101 | let x_pos = index_to_posit(i_pos, data.sequence.len(), ui); 102 | 103 | if x_pos > ui.available_width() - 15. { 104 | continue; // todo: QC the details, if you need to_screen here etc. 105 | } 106 | 107 | let nt_color = nt_color_map(nt); 108 | 109 | result.push(ui.ctx().fonts(|fonts| { 110 | Shape::text( 111 | fonts, 112 | to_screen * pos2(x_pos, nt_y), 113 | Align2::CENTER_CENTER, 114 | &nt.to_str_lower(), 115 | FontId::new(12., FontFamily::Monospace), 116 | nt_color, 117 | ) 118 | })); 119 | 120 | // todo: Display quality values below. 121 | // let quality = data.quality[i]; // todo: Index check. 122 | } 123 | 124 | // Display data. 125 | for i_pos in 0..num_nts_disp as usize * 4 { 126 | let i = start_i * 4 + i_pos; 127 | if data.data_ch1.is_empty() || i > data.data_ch1.len() - 1 { 128 | break; 129 | } 130 | 131 | let x_pos = index_to_posit(i_pos, data.sequence.len(), ui) / 4.; 132 | if x_pos > ui.available_width() - 15. { 133 | continue; // todo: QC the details, if you need to_screen here etc. 134 | } 135 | 136 | let ch1 = data.data_ch1[i]; 137 | let ch2 = data.data_ch2[i]; 138 | let ch3 = data.data_ch3[i]; 139 | let ch4 = data.data_ch4[i]; 140 | // todo: Index error handling. 141 | 142 | for (ch, color) in [ 143 | (ch1, COLOR_G), 144 | (ch2, COLOR_A), 145 | (ch3, COLOR_T), 146 | (ch4, COLOR_C), 147 | ] { 148 | let stroke = Stroke::new(STROKE_WIDTH_PEAK, color); 149 | 150 | // todo: Autoscale height 151 | let base_pos = pos2(x_pos, plot_y); 152 | 153 | // todo: These may get too thin. 154 | let top_left = base_pos + vec2(-PEAK_WIDTH_DIV2, ch as f32 * -data_scaler); 155 | let top_right = base_pos + vec2(PEAK_WIDTH_DIV2, ch as f32 * -data_scaler); 156 | let bottom_left = base_pos + vec2(-PEAK_WIDTH_DIV2, 0.); 157 | let bottom_right = base_pos + vec2(PEAK_WIDTH_DIV2, 0.); 158 | 159 | result.push(ui.ctx().fonts(|fonts| { 160 | // Shape::Path(PathShape::convex_polygon( 161 | Shape::Path(PathShape::closed_line( 162 | vec![ 163 | to_screen * top_left, 164 | to_screen * bottom_left, 165 | to_screen * bottom_right, 166 | to_screen * top_right, 167 | ], 168 | // color, 169 | stroke, 170 | )) 171 | })); 172 | } 173 | } 174 | 175 | result 176 | } 177 | 178 | pub fn ab1_page(state: &mut State, ui: &mut Ui) { 179 | ui.horizontal(|ui| { 180 | let data = &state.ab1_data[state.active]; 181 | 182 | ui.heading("AB1 sequencing view"); 183 | 184 | ui.add_space(COL_SPACING * 2.); 185 | 186 | if ui.button(RichText::new("🗐 Copy sequence")).on_hover_text("Copy this sequence to the clipboard.").clicked() { 187 | let mut ctx = ClipboardContext::new().unwrap(); 188 | ctx.set_contents(seq_to_str_lower(&data.sequence)).unwrap(); 189 | } 190 | 191 | ui.add_space(COL_SPACING); 192 | 193 | if ui 194 | .button("➕ Create data (e.g. Genbank, PCAD etc) as a new tab.") 195 | .on_hover_text("Create a new non-AB1 data set\ 196 | that may include features, primers, etc. May be edited, and saved to Genbank, PCAD, or SnapGene formats.") 197 | .clicked() 198 | { 199 | // Note: This segment is almost a duplicate of `State::add_tab` and the similar section in `cloning`. 200 | let plasmid_name = match &state.tabs_open[state.active].path { 201 | Some(p) => p.file_name().unwrap().to_str().unwrap_or_default(), 202 | None => "Plasmid from AB1", // This shouldn't happen, I believe. 203 | }.to_owned().replace(".ab1", ""); 204 | 205 | let generic = GenericData { 206 | seq: data.sequence.clone(), 207 | metadata: Metadata { 208 | plasmid_name, 209 | ..Default::default() 210 | }, 211 | ..Default::default() 212 | }; 213 | 214 | 215 | state.generic.push(generic); 216 | 217 | state.portions.push(Default::default()); 218 | state.volatile.push(Default::default()); 219 | state.tabs_open.push(Default::default()); 220 | state.ab1_data.push(Default::default()); 221 | 222 | state.active = state.generic.len() - 1; 223 | 224 | // state.sync_seq_related(None); 225 | 226 | // Annotate. Don't add duplicates. 227 | let features = find_features(&state.get_seq()); 228 | merge_feature_sets(&mut state.generic[state.active].features, &features) 229 | 230 | } 231 | }); 232 | ui.add_space(ROW_SPACING / 2.); 233 | 234 | let data = &state.ab1_data[state.active]; 235 | let width = ui.available_width(); 236 | let num_nts_disp = width / NT_WIDTH; 237 | ui.spacing_mut().slider_width = width - 60.; 238 | 239 | let slider_max = { 240 | let v = data.sequence.len() as isize - num_nts_disp as isize + 1; 241 | if v > 0 { v as usize } else { 0 } 242 | }; 243 | 244 | ui.add(Slider::new(&mut state.ui.ab1_start_i, 0..=slider_max)); 245 | 246 | ui.add_space(ROW_SPACING / 2.); 247 | 248 | let mut shapes = Vec::new(); 249 | 250 | Frame::canvas(ui.style()) 251 | .fill(BACKGROUND_COLOR) 252 | .show(ui, |ui| { 253 | let data = &state.ab1_data[state.active]; 254 | 255 | let (response, _painter) = { 256 | let desired_size = vec2(ui.available_width(), ui.available_height()); 257 | ui.allocate_painter(desired_size, Sense::click()) 258 | }; 259 | 260 | let to_screen = RectTransform::from_to( 261 | // Rect::from_min_size(pos2(0., -VERITICAL_CIRCLE_OFFSET), response.rect.size()), 262 | Rect::from_min_size(Pos2::ZERO, response.rect.size()), 263 | response.rect, 264 | ); 265 | 266 | let rect_size = response.rect.size(); 267 | 268 | shapes.append(&mut plot(data, &to_screen, state.ui.ab1_start_i, ui)); 269 | 270 | ui.painter().extend(shapes); 271 | }); 272 | } 273 | -------------------------------------------------------------------------------- /src/gui/alignment.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::{Color32, FontFamily, FontId, RichText, ScrollArea, TextEdit, Ui}; 2 | use na_seq::{seq_aa_from_str, seq_aa_to_str, seq_from_str, seq_to_str_lower}; 3 | 4 | use crate::{ 5 | alignment::{AlignmentMode, align_pairwise_aa, align_pairwise_nt, distance_aa, distance_nt}, 6 | gui::{ 7 | COL_SPACING, ROW_SPACING, 8 | theme::{COLOR_ACTION, COLOR_INFO}, 9 | }, 10 | state::State, 11 | }; 12 | 13 | fn mode_btn(state: &mut State, mode: AlignmentMode, name: &str, ui: &mut Ui) { 14 | let color = if state.alignment.mode == mode { 15 | Color32::LIGHT_BLUE 16 | } else { 17 | Color32::WHITE 18 | }; 19 | if ui.button(RichText::new(name).color(color)).clicked() { 20 | state.alignment.mode = mode; 21 | 22 | // Reset fields to prevent invalid data. 23 | state.alignment.seq_a = Vec::new(); 24 | state.alignment.seq_aa_a = Vec::new(); 25 | state.alignment.seq_a_input = String::new(); 26 | state.alignment.seq_b = Vec::new(); 27 | state.alignment.seq_aa_b = Vec::new(); 28 | state.alignment.seq_b_input = String::new(); 29 | } 30 | } 31 | 32 | fn input_area(state: &mut State, seq_b: bool, ui: &mut Ui) { 33 | let (seq, seq_aa, seq_input) = if seq_b { 34 | ( 35 | &mut state.alignment.seq_a, 36 | &mut state.alignment.seq_aa_a, 37 | &mut state.alignment.seq_a_input, 38 | ) 39 | } else { 40 | ( 41 | &mut state.alignment.seq_b, 42 | &mut state.alignment.seq_aa_b, 43 | &mut state.alignment.seq_b_input, 44 | ) 45 | }; 46 | 47 | let active_seq = state.generic[state.active].seq.clone(); // Avoid borrowing state in the closure 48 | 49 | ui.horizontal(|ui| { 50 | ui.label(if seq_b { "Seq B" } else { "Seq A" }); 51 | ui.add_space(COL_SPACING); 52 | 53 | if let AlignmentMode::Dna = state.alignment.mode { 54 | if ui 55 | .button(RichText::new("Load active tab's sequence").color(Color32::LIGHT_BLUE)) 56 | .clicked() 57 | { 58 | *seq = active_seq.clone(); // Use the pre-fetched active sequence 59 | *seq_input = seq_to_str_lower(seq); 60 | } 61 | } 62 | }); 63 | 64 | let response = ui.add(TextEdit::multiline(seq_input).desired_width(800.)); 65 | if response.changed() { 66 | match state.alignment.mode { 67 | AlignmentMode::Dna => { 68 | *seq = seq_from_str(seq_input); 69 | *seq_input = seq_to_str_lower(seq); 70 | // Todo: why? 71 | // state.sync_seq_related(None); 72 | } 73 | AlignmentMode::AminoAcid => { 74 | *seq_aa = seq_aa_from_str(seq_input); 75 | *seq_input = seq_aa_to_str(seq_aa); 76 | } 77 | } 78 | } 79 | } 80 | 81 | pub fn alignment_page(state: &mut State, ui: &mut Ui) { 82 | ui.add_space(ROW_SPACING); 83 | 84 | ui.horizontal(|ui| { 85 | ui.heading("Alignment (Work in Progress)"); 86 | 87 | ui.add_space(COL_SPACING * 2.); 88 | 89 | mode_btn(state, AlignmentMode::Dna, "DNA", ui); 90 | mode_btn(state, AlignmentMode::AminoAcid, "Amino acid", ui); 91 | }); 92 | ui.add_space(ROW_SPACING); 93 | 94 | ScrollArea::vertical().id_salt(200).show(ui, |ui| { 95 | input_area(state, false, ui); 96 | ui.add_space(ROW_SPACING); 97 | input_area(state, true, ui); 98 | 99 | ui.add_space(ROW_SPACING); 100 | 101 | if ui 102 | .button(RichText::new("Align").color(COLOR_ACTION)) 103 | .clicked() 104 | { 105 | let alignment = match state.alignment.mode { 106 | AlignmentMode::Dna => { 107 | align_pairwise_nt(&state.alignment.seq_a, &state.alignment.seq_b) 108 | } 109 | AlignmentMode::AminoAcid => { 110 | align_pairwise_aa(&state.alignment.seq_aa_a, &state.alignment.seq_aa_b) 111 | } 112 | }; 113 | 114 | state.alignment.alignment_result = Some(alignment.0); 115 | state.alignment.text_display = alignment.1; 116 | 117 | state.alignment.dist_result = Some(match state.alignment.mode { 118 | AlignmentMode::Dna => distance_nt(&state.alignment.seq_a, &state.alignment.seq_b), 119 | AlignmentMode::AminoAcid => { 120 | distance_aa(&state.alignment.seq_aa_a, &state.alignment.seq_aa_b) 121 | } 122 | }); 123 | } 124 | 125 | ui.add_space(ROW_SPACING); 126 | if let Some(alignment) = &state.alignment.alignment_result { 127 | ui.heading(&format!("Alignment score: {:?}", alignment.score)); 128 | 129 | ui.add_space(ROW_SPACING); 130 | 131 | ui.label( 132 | RichText::new(&state.alignment.text_display) 133 | .color(COLOR_INFO) 134 | .font(FontId::new(16., FontFamily::Monospace)), 135 | ); 136 | } 137 | 138 | if let Some(dist) = &state.alignment.dist_result { 139 | ui.horizontal(|ui| { 140 | ui.heading(&format!("Distance score: {:?}", dist)); 141 | let dist_type_text = if state.alignment.seq_a.len() == state.alignment.seq_b.len() { 142 | "(Hamming)" 143 | } else { 144 | "(Levenshtein)" 145 | }; 146 | ui.label(dist_type_text); 147 | }); 148 | } 149 | }); 150 | } 151 | -------------------------------------------------------------------------------- /src/gui/feature_table.rs: -------------------------------------------------------------------------------- 1 | //! GUI code for the features editor and related. 2 | 3 | use eframe::egui::{ 4 | Color32, ComboBox, CursorIcon, Frame, RichText, ScrollArea, Stroke, TextEdit, Ui, 5 | }; 6 | 7 | use crate::{ 8 | Color, Selection, 9 | gui::{COL_SPACING, ROW_SPACING, int_field, theme::COLOR_ACTION}, 10 | misc_types::{ 11 | Feature, 12 | FeatureDirection::{self, Forward, Reverse}, 13 | FeatureType, 14 | }, 15 | state::State, 16 | util::RangeIncl, 17 | }; 18 | 19 | const LABEL_EDIT_WIDTH: f32 = 140.; 20 | 21 | /// A color selector for use with feature addition and editing. 22 | fn color_picker(val: &mut Option, feature_color: Color, ui: &mut Ui) { 23 | let mut color_override = val.is_some(); 24 | if ui.checkbox(&mut color_override, "").changed() { 25 | if color_override { 26 | // Default to the feature color when checking. 27 | *val = Some(feature_color); 28 | } else { 29 | *val = None; 30 | return; 31 | } 32 | } 33 | 34 | // Only show the color picker if choosing to override. 35 | if val.is_none() { 36 | return; 37 | } 38 | 39 | let (r, g, b) = val.unwrap(); 40 | let mut color = Color32::from_rgb(r, g, b); 41 | if ui.color_edit_button_srgba(&mut color).changed() { 42 | *val = Some((color.r(), color.g(), color.b())); 43 | } 44 | } 45 | 46 | /// A selector for use with feature addition and editing. 47 | /// todo: Generic selector creator? 48 | pub fn direction_picker(val: &mut FeatureDirection, id: usize, ui: &mut Ui) { 49 | ComboBox::from_id_salt(id) 50 | .width(74.) 51 | .selected_text(val.to_string()) 52 | .show_ui(ui, |ui| { 53 | for dir in [FeatureDirection::None, Forward, Reverse] { 54 | ui.selectable_value(val, dir, dir.to_string()); 55 | } 56 | }); 57 | } 58 | 59 | /// A selector for use with feature addition and editing. 60 | /// todo: Generic selector creator? 61 | fn feature_type_picker(val: &mut FeatureType, id: usize, ui: &mut Ui) { 62 | ComboBox::from_id_salt(id) 63 | .width(140.) 64 | .selected_text(val.to_string()) 65 | .show_ui(ui, |ui| { 66 | for feature_type in [ 67 | FeatureType::Generic, 68 | FeatureType::CodingRegion, 69 | FeatureType::Ori, 70 | // FeatureType::RnaPolyBindSite, 71 | FeatureType::RibosomeBindSite, 72 | FeatureType::AntibioticResistance, 73 | FeatureType::LongTerminalRepeat, 74 | FeatureType::Exon, 75 | FeatureType::Transcript, 76 | // todo: Source? 77 | ] { 78 | ui.selectable_value(val, feature_type, feature_type.to_string()); 79 | } 80 | }); 81 | } 82 | 83 | pub fn feature_table(state: &mut State, ui: &mut Ui) { 84 | feature_add_disp(state, ui); 85 | ui.add_space(ROW_SPACING); 86 | 87 | let mut removed = None; 88 | for (i, feature) in state.generic[state.active].features.iter_mut().enumerate() { 89 | let mut border_width = 0.; 90 | if let Selection::Feature(j) = state.ui.selected_item { 91 | if i == j { 92 | border_width = 1.; 93 | } 94 | } 95 | 96 | Frame::none() 97 | .stroke(Stroke::new(border_width, Color32::LIGHT_RED)) 98 | .inner_margin(border_width) 99 | .show(ui, |ui| { 100 | if ui 101 | .heading(RichText::new(&feature.label).color(COLOR_ACTION)) 102 | .on_hover_cursor(CursorIcon::PointingHand) 103 | .clicked() 104 | { 105 | state.ui.selected_item = Selection::Feature(i); 106 | } 107 | 108 | ui.horizontal(|ui| { 109 | int_field(&mut feature.range.start, "Start:", ui); 110 | int_field(&mut feature.range.end, "End:", ui); 111 | 112 | ui.label("Label:"); 113 | ui.add( 114 | TextEdit::singleline(&mut feature.label).desired_width(LABEL_EDIT_WIDTH), 115 | ); 116 | 117 | ui.label("Type:"); 118 | feature_type_picker(&mut feature.feature_type, 100 + i, ui); 119 | 120 | ui.label("Dir:"); 121 | direction_picker(&mut feature.direction, 300 + i, ui); 122 | 123 | ui.label("Custom color:"); 124 | color_picker( 125 | &mut feature.color_override, 126 | feature.feature_type.color(), 127 | ui, 128 | ); 129 | 130 | if ui.button("Add note").clicked() { 131 | feature.notes.push((String::new(), String::new())); 132 | } 133 | 134 | // todo: This section repetative with primers. 135 | let mut selected = false; 136 | if let Selection::Feature(sel_i) = state.ui.selected_item { 137 | if sel_i == i { 138 | selected = true; 139 | } 140 | } 141 | 142 | if selected { 143 | if ui 144 | .button(RichText::new("🔘").color(Color32::GREEN)) 145 | .clicked() 146 | { 147 | state.ui.selected_item = Selection::None; 148 | } 149 | } else if ui.button("🔘").clicked() { 150 | state.ui.selected_item = Selection::Feature(i); 151 | } 152 | 153 | ui.add_space(COL_SPACING); // Less likely to accidentally delete. 154 | 155 | if ui 156 | .button(RichText::new("Delete 🗑").color(Color32::RED)) 157 | .clicked() 158 | { 159 | removed = Some(i); 160 | } 161 | }); 162 | 163 | for (key, value) in &mut feature.notes { 164 | ui.horizontal(|ui| { 165 | ui.label("Note:"); 166 | ui.add(TextEdit::singleline(key).desired_width(140.)); 167 | 168 | ui.label("Value:"); 169 | ui.add(TextEdit::singleline(value).desired_width(600.)); 170 | }); 171 | } 172 | }); 173 | 174 | ui.add_space(ROW_SPACING); 175 | } 176 | if let Some(rem_i) = removed { 177 | state.generic[state.active].features.remove(rem_i); 178 | } 179 | } 180 | 181 | pub fn feature_add_disp(state: &mut State, ui: &mut Ui) { 182 | ui.horizontal(|ui| { 183 | if ui.button("➕ Add feature").clicked() { 184 | if state.ui.feature_add.start_posit == 0 { 185 | state.ui.feature_add.start_posit = 1; 186 | } 187 | if state.ui.feature_add.end_posit == 0 { 188 | state.ui.feature_add.end_posit = 1; 189 | } 190 | 191 | if state.ui.feature_add.start_posit > state.ui.feature_add.end_posit { 192 | std::mem::swap( 193 | &mut state.ui.feature_add.start_posit, 194 | &mut state.ui.feature_add.end_posit, 195 | ); 196 | } 197 | 198 | state.generic[state.active].features.push(Feature { 199 | range: RangeIncl::new( 200 | state.ui.feature_add.start_posit, 201 | state.ui.feature_add.end_posit, 202 | ), 203 | feature_type: FeatureType::Generic, 204 | direction: FeatureDirection::None, 205 | label: state.ui.feature_add.label.clone(), 206 | color_override: None, 207 | notes: Default::default(), 208 | }); 209 | } 210 | }); 211 | } 212 | 213 | pub fn features_page(state: &mut State, ui: &mut Ui) { 214 | ScrollArea::vertical().show(ui, |ui| { 215 | feature_table(state, ui); 216 | }); 217 | } 218 | -------------------------------------------------------------------------------- /src/gui/input.rs: -------------------------------------------------------------------------------- 1 | //! Code related to mouse and keyboard input handling. 2 | 3 | use std::{mem, path::PathBuf}; 4 | 5 | use eframe::egui::{Event, InputState, Key, PointerButton, Ui}; 6 | use na_seq::{Nucleotide, seq_from_str}; 7 | 8 | use crate::{ 9 | StateUi, 10 | file_io::{ 11 | save, 12 | save::{QUICKSAVE_FILE, StateToSave, load_import}, 13 | }, 14 | gui::{ 15 | navigation::{Page, Tab}, 16 | set_window_title, 17 | }, 18 | state::State, 19 | util::RangeIncl, 20 | }; 21 | 22 | /// Handle hotkeys and clicks that affect all pages. 23 | fn handle_global(state: &mut State, ip: &InputState) { 24 | if ip.key_pressed(Key::A) && ip.modifiers.ctrl && !state.ui.text_edit_active { 25 | if !state.get_seq().is_empty() { 26 | state.ui.text_selection = Some(RangeIncl::new(1, state.get_seq().len())) 27 | } 28 | } 29 | 30 | if ip.key_pressed(Key::S) && ip.modifiers.ctrl && !ip.modifiers.shift { 31 | save::save_current_file(state); 32 | } 33 | 34 | if ip.key_pressed(Key::N) && ip.modifiers.ctrl { 35 | state.add_tab(); 36 | state.tabs_open.push(Tab { 37 | path: None, 38 | ab1: false, 39 | }); 40 | } 41 | 42 | if ip.key_pressed(Key::F) && ip.modifiers.ctrl { 43 | state.ui.highlight_search_input = true; 44 | // Disable the cursor, so we don't insert nucleotides while searching! 45 | state.ui.text_cursor_i = None; 46 | } 47 | 48 | if ip.key_pressed(Key::S) && ip.modifiers.ctrl && ip.modifiers.shift { 49 | state.ui.file_dialogs.save.pick_file(); 50 | } 51 | 52 | if ip.key_pressed(Key::O) && ip.modifiers.ctrl { 53 | state.ui.file_dialogs.load.pick_file(); 54 | } 55 | 56 | state.ui.cursor_pos = ip.pointer.hover_pos().map(|pos| (pos.x, pos.y)); 57 | 58 | if ip.pointer.button_clicked(PointerButton::Primary) { 59 | // todo: Not working for fixing our fast-sel off-by-one bug. 60 | // if let Some(i) = &state.ui.cursor_seq_i { 61 | // state.ui.text_selection = Some(*i..=*i); // 1-based indexing. Second value is a placeholder. 62 | // } 63 | 64 | state.ui.click_pending_handle = true; 65 | } 66 | 67 | if ip.pointer.button_double_clicked(PointerButton::Primary) { 68 | state.ui.dblclick_pending_handle = true; 69 | } 70 | } 71 | 72 | /// Handle sequence selection on the sequence page, as when dragging the mouse. 73 | fn handle_seq_selection(state_ui: &mut StateUi, dragging: bool) { 74 | if dragging { 75 | // A drag has started. 76 | if !state_ui.dragging { 77 | state_ui.dragging = true; 78 | // We are handling in the seq view after setting cursor seq i. Still glitchy. 79 | 80 | if let Some(i) = &state_ui.cursor_seq_i { 81 | state_ui.text_selection = Some(RangeIncl::new(*i, *i)); // The end value is a placeholder. 82 | } 83 | } else { 84 | // The drag is in progress; continually update the selection, for visual feedback. 85 | if let Some(i) = &state_ui.cursor_seq_i { 86 | if let Some(sel_range) = &mut state_ui.text_selection { 87 | sel_range.end = *i; 88 | } 89 | } 90 | } 91 | } else { 92 | // A drag has ended. 93 | if state_ui.dragging { 94 | state_ui.dragging = false; 95 | 96 | // This logic allows for reverse-order selecting, ie dragging the cursor from the end to 97 | // the start of the region. Handle in the UI how to display this correctly while the drag is in process, 98 | // since this only resolves once it's complete. 99 | if let Some(sel_range) = &mut state_ui.text_selection { 100 | if sel_range.start > sel_range.end { 101 | mem::swap(&mut sel_range.start, &mut sel_range.end); 102 | } 103 | } 104 | } 105 | } 106 | } 107 | 108 | /// Handles keyboard and mouse input not associated with a widget. 109 | /// todo: MOve to a separate module if this becomes complex. 110 | pub fn handle_input(state: &mut State, ui: &mut Ui) { 111 | let mut reset_window_title = false; // This setup avoids borrow errors. 112 | 113 | ui.ctx().input(|ip| { 114 | // Check for file drop 115 | if let Some(dropped_files) = ip.raw.dropped_files.first() { 116 | if let Some(path) = &dropped_files.path { 117 | if let Some(loaded) = load_import(path) { 118 | state.load(&loaded); 119 | reset_window_title = true; 120 | } 121 | } 122 | } 123 | 124 | handle_global(state, ip); 125 | 126 | if state.ui.text_edit_active && !reset_window_title { 127 | return; 128 | } 129 | 130 | // This event match is not specific to the seqe page 131 | for event in &ip.events { 132 | match event { 133 | Event::Cut => state.copy_seq(), 134 | Event::Copy => state.copy_seq(), 135 | Event::Paste(pasted_text) => {} 136 | _ => (), 137 | } 138 | } 139 | 140 | if let Page::Sequence = state.ui.page { 141 | // This is a bit awk; borrow errors. 142 | let mut move_cursor: Option = None; 143 | // todo: How can we control the rate? 144 | if state.ui.text_cursor_i.is_some() { 145 | if ip.key_pressed(Key::ArrowLeft) { 146 | move_cursor = Some(-1); 147 | } 148 | if ip.key_pressed(Key::ArrowRight) { 149 | move_cursor = Some(1); 150 | } 151 | if ip.key_pressed(Key::ArrowUp) { 152 | move_cursor = Some(-(state.ui.nt_chars_per_row as i32)); 153 | } 154 | if ip.key_pressed(Key::ArrowDown) { 155 | move_cursor = Some(state.ui.nt_chars_per_row as i32); 156 | } 157 | // Escape key: Remove the text cursor. 158 | if ip.key_pressed(Key::Escape) { 159 | move_cursor = None; 160 | state.ui.text_cursor_i = None; 161 | state.ui.text_selection = None; 162 | } 163 | } 164 | 165 | if ip.key_pressed(Key::Escape) { 166 | state.ui.text_selection = None; 167 | } 168 | 169 | // Insert nucleotides A/R. 170 | if let Some(mut i) = state.ui.text_cursor_i { 171 | if !state.ui.seq_edit_lock { 172 | if i > state.get_seq().len() { 173 | i = 0; // todo?? Having an overflow when backspacing near origin. 174 | } 175 | 176 | let i = i + 1; // Insert after this nucleotide; not before. 177 | // Don't allow accidental nt insertion when the user is entering into the search bar. 178 | 179 | // Add NTs. 180 | if ip.key_pressed(Key::A) && !ip.modifiers.ctrl { 181 | state.insert_nucleotides(&[Nucleotide::A], i); 182 | } 183 | if ip.key_pressed(Key::T) { 184 | state.insert_nucleotides(&[Nucleotide::T], i); 185 | } 186 | if ip.key_pressed(Key::C) && !ip.modifiers.ctrl { 187 | state.insert_nucleotides(&[Nucleotide::C], i); 188 | } 189 | if ip.key_pressed(Key::G) { 190 | state.insert_nucleotides(&[Nucleotide::G], i); 191 | } 192 | if ip.key_pressed(Key::Backspace) && i > 1 { 193 | state.remove_nucleotides(RangeIncl::new(i - 2, i - 2)); 194 | } 195 | if ip.key_pressed(Key::Delete) { 196 | state.remove_nucleotides(RangeIncl::new(i - 1, i - 1)); 197 | } 198 | 199 | // Paste nucleotides 200 | for event in &ip.events { 201 | match event { 202 | Event::Cut => { 203 | // state.remove_nucleotides(); 204 | // move_cursor = Some(pasted_text.len() as i32); 205 | } 206 | Event::Copy => {} 207 | Event::Paste(pasted_text) => { 208 | state.insert_nucleotides(&seq_from_str(pasted_text), i); 209 | move_cursor = Some(pasted_text.len() as i32); 210 | } 211 | _ => (), 212 | } 213 | } 214 | } 215 | } 216 | 217 | if !state.ui.seq_edit_lock { 218 | if ip.key_pressed(Key::A) && !ip.modifiers.ctrl { 219 | move_cursor = Some(1); 220 | } 221 | if ip.key_pressed(Key::T) { 222 | move_cursor = Some(1); 223 | } 224 | if ip.key_pressed(Key::C) && !ip.modifiers.ctrl { 225 | move_cursor = Some(1); 226 | } 227 | if ip.key_pressed(Key::G) { 228 | move_cursor = Some(1); 229 | } 230 | 231 | if ip.key_pressed(Key::Backspace) { 232 | move_cursor = Some(-1); 233 | } 234 | } 235 | 236 | if let Some(i) = &mut state.ui.text_cursor_i { 237 | if let Some(amt) = move_cursor { 238 | let val = *i as i32 + amt; 239 | // If val is 0, the cursor is before the first character; this is OK. 240 | // If it's < 0, we will get an overflow when converting to usize. 241 | if val >= 0 { 242 | let new_posit = val as usize; 243 | if new_posit <= state.generic[state.active].seq.len() { 244 | *i = new_posit; 245 | } 246 | } 247 | } 248 | } 249 | 250 | handle_seq_selection(&mut state.ui, ip.pointer.is_decidedly_dragging()); 251 | } 252 | }); 253 | 254 | if reset_window_title { 255 | set_window_title(&state.tabs_open[state.active], ui); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/gui/metadata.rs: -------------------------------------------------------------------------------- 1 | //! Contains references, comments, etc about the plasmid. 2 | 3 | use chrono::NaiveDate; 4 | use eframe::egui::{Color32, RichText, ScrollArea, TextEdit, Ui}; 5 | 6 | use crate::{ 7 | gui::{COL_SPACING, ROW_SPACING}, 8 | misc_types::Metadata, 9 | }; 10 | 11 | const WIDTH_RATIO: f32 = 0.6; 12 | const ROW_HEIGHT: usize = 1; 13 | 14 | const HEADING_COLOR: Color32 = Color32::from_rgb(40, 180, 255); 15 | 16 | /// A convenience function to create a text edit for Option 17 | fn option_edit( 18 | val: &mut Option, 19 | label: &str, 20 | multi: bool, 21 | width_: Option, 22 | ui: &mut Ui, 23 | ) { 24 | ui.horizontal(|ui| { 25 | // ui.allocate_exact_size(Vec2::new(LABEL_WIDTH, 0.0), egui::Sense::hover()); // Reserve space 26 | ui.label(label); 27 | 28 | let width = match width_ { 29 | Some(w) => w, 30 | None => ui.available_width() * WIDTH_RATIO, 31 | }; 32 | 33 | // todo: Way without cloning? 34 | let mut v = val.clone().unwrap_or_default(); 35 | // Don't use these margins if there is a narrow window. 36 | let response = if multi { 37 | ui.add( 38 | TextEdit::multiline(&mut v) 39 | .desired_width(width) 40 | .desired_rows(ROW_HEIGHT), 41 | ) 42 | } else { 43 | ui.add(TextEdit::singleline(&mut v).desired_width(width)) 44 | }; 45 | 46 | if response.changed() { 47 | *val = if !v.is_empty() { 48 | Some(v.to_owned()) 49 | } else { 50 | None 51 | }; 52 | } 53 | }); 54 | 55 | ui.add_space(ROW_SPACING / 2.); 56 | } 57 | 58 | pub fn metadata_page(data: &mut Metadata, ui: &mut Ui) { 59 | // todo: YOu need neat grid alignment. How can we make the labels take up constant space? 60 | 61 | // todo: Examine which fields should be single vs multiline, and the order. 62 | ScrollArea::vertical().show(ui, |ui| { 63 | ui.heading(RichText::new("General:").color(HEADING_COLOR)); 64 | ui.add_space(ROW_SPACING / 2.); 65 | 66 | ui.horizontal(|ui| { 67 | ui.label("Plasmid name:"); 68 | ui.text_edit_singleline(&mut data.plasmid_name); 69 | }); 70 | ui.add_space(ROW_SPACING); 71 | 72 | option_edit(&mut data.definition, "Definition:", false, None, ui); 73 | 74 | ui.horizontal(|ui| { 75 | ui.label("Division:"); 76 | ui.add(TextEdit::singleline(&mut data.division).desired_width(60.)); 77 | ui.add_space(COL_SPACING); 78 | 79 | option_edit( 80 | &mut data.molecule_type, 81 | "Molecule type:", 82 | false, 83 | Some(80.), 84 | ui, 85 | ); 86 | 87 | if let Some(date) = data.date { 88 | ui.add_space(COL_SPACING); 89 | // todo: Allow the date to be editable, and not just when present. 90 | let date = NaiveDate::from_ymd(date.0, date.1.into(), date.2.into()); 91 | ui.label(format!("Date: {}", date)); 92 | } 93 | }); 94 | ui.add_space(ROW_SPACING / 2.); 95 | 96 | option_edit(&mut data.accession, "Accession:", true, None, ui); 97 | option_edit(&mut data.version, "Version:", true, None, ui); 98 | option_edit(&mut data.keywords, "Keywords:", true, None, ui); 99 | option_edit(&mut data.source, "Source:", true, None, ui); 100 | option_edit(&mut data.organism, "Organism:", true, None, ui); 101 | 102 | ui.add_space(ROW_SPACING); 103 | 104 | // pub locus: String, 105 | // pub definition: Option, 106 | // pub accession: Option, 107 | // pub version: Option, 108 | // // pub keywords: Vec, 109 | // pub keywords: Option, // todo vec? 110 | // pub source: Option, 111 | // pub organism: Option, 112 | 113 | ui.heading(RichText::new("References:").color(HEADING_COLOR)); 114 | ui.add_space(ROW_SPACING / 2.); 115 | 116 | for ref_ in &mut data.references { 117 | ui.horizontal(|ui| { 118 | ui.label("Title:"); 119 | ui.add( 120 | TextEdit::multiline(&mut ref_.title) 121 | .desired_width(ui.available_width() * WIDTH_RATIO) 122 | .desired_rows(ROW_HEIGHT), 123 | ); 124 | }); 125 | ui.add_space(ROW_SPACING / 2.); 126 | 127 | ui.horizontal(|ui| { 128 | ui.label("Description:"); 129 | ui.add( 130 | TextEdit::multiline(&mut ref_.description) 131 | .desired_width(ui.available_width() * WIDTH_RATIO) 132 | .desired_rows(ROW_HEIGHT), 133 | ); 134 | }); 135 | ui.add_space(ROW_SPACING / 2.); 136 | 137 | option_edit(&mut ref_.authors, "Authors:", true, None, ui); 138 | option_edit(&mut ref_.consortium, "Consortium:", true, None, ui); 139 | 140 | option_edit(&mut ref_.journal, "Journal:", true, None, ui); 141 | option_edit(&mut ref_.pubmed, "Pubmed:", true, None, ui); 142 | option_edit(&mut ref_.remark, "Remarks:", true, None, ui); 143 | 144 | ui.add_space(ROW_SPACING); 145 | } 146 | 147 | // egui::Shape::hline(2, 2., Stroke::new(2., Color32::WHITE)); 148 | 149 | ui.heading(RichText::new("Comments:").color(HEADING_COLOR)); 150 | ui.add_space(ROW_SPACING); 151 | if ui.button("➕ Add").clicked() { 152 | data.comments.push(String::new()); 153 | } 154 | 155 | for comment in &mut data.comments { 156 | let response = ui.add( 157 | TextEdit::multiline(comment) 158 | .desired_width(ui.available_width() * WIDTH_RATIO) 159 | .desired_rows(ROW_HEIGHT), 160 | ); 161 | // if response.changed() { 162 | // } 163 | 164 | ui.add_space(ROW_SPACING / 2.); 165 | } 166 | }); 167 | } 168 | -------------------------------------------------------------------------------- /src/gui/navigation.rs: -------------------------------------------------------------------------------- 1 | //! This module contains code related to navigation buttons. 2 | 3 | use std::{fmt::Display, path::PathBuf}; 4 | 5 | use bincode::{Decode, Encode}; 6 | use eframe::egui::{Color32, RichText, Ui}; 7 | use na_seq::seq_to_str_lower; 8 | 9 | use crate::{ 10 | gui::{COL_SPACING, ROW_SPACING, select_color_text, set_window_title}, 11 | state::State, 12 | }; 13 | 14 | pub const NAV_BUTTON_COLOR: Color32 = Color32::from_rgb(0, 0, 110); 15 | pub const TAB_BUTTON_COLOR: Color32 = Color32::from_rgb(40, 80, 110); 16 | pub const DEFAULT_TAB_NAME: &str = "New plasmid"; 17 | 18 | // When abbreviating a path, show no more than this many characters. 19 | const PATH_ABBREV_MAX_LEN: usize = 16; 20 | 21 | // todo: Alt name: Path loaded 22 | #[derive(Encode, Decode, Clone, Default, Debug)] 23 | pub struct Tab { 24 | pub path: Option, 25 | pub ab1: bool, // todo: Enum if you add a third category. 26 | } 27 | 28 | /// Used in several GUI components to get data from open tabs. 29 | /// Note: For name, we currently default to file name (with extension), then 30 | /// plasmid name, then a default. See if you want to default to plasmid name. 31 | /// 32 | /// Returns the name, and the tab index. 33 | pub fn get_tab_names( 34 | tabs: &[Tab], 35 | plasmid_names: &[&str], 36 | abbrev_name: bool, 37 | ) -> Vec<(String, usize)> { 38 | let mut result = Vec::new(); 39 | 40 | for (i, p) in tabs.iter().enumerate() { 41 | let name = name_from_path(&p.path, plasmid_names[i], abbrev_name); 42 | result.push((name, i)); 43 | } 44 | 45 | result 46 | } 47 | 48 | #[derive(Clone, Copy, PartialEq, Encode, Decode)] 49 | pub enum Page { 50 | /// Primer design and QC, including for cloning 51 | Sequence, 52 | /// A circular "graphical map" of the plasmid 53 | Map, 54 | Features, 55 | Primers, 56 | Proteins, 57 | /// Determine optimal PCR parameters 58 | Pcr, 59 | Alignment, 60 | Portions, 61 | Metadata, 62 | Ligation, 63 | /// A simplified cloning process, for both PCR and RE-based cloning. 64 | Cloning, 65 | /// i.e. Sanger sequencing data. This page is fundamentally different from the others; 66 | /// it is only for .ab1 files, and is selected automatically, vice from the menu. 67 | Ab1, 68 | } 69 | 70 | impl Default for Page { 71 | fn default() -> Self { 72 | Self::Sequence 73 | } 74 | } 75 | 76 | impl Display for Page { 77 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 78 | let str = match self { 79 | Self::Sequence => "Sequence", 80 | Self::Map => "Map", 81 | Self::Features => "Features", 82 | Self::Primers => "Primers", 83 | Self::Proteins => "Proteins", 84 | Self::Pcr => "PCR", 85 | Self::Alignment => "Align", 86 | Self::Portions => "Mixing", 87 | Self::Metadata => "Data", 88 | Self::Ligation => "Digest", 89 | Self::Cloning => "Clone", 90 | Self::Ab1 => "AB1", 91 | } 92 | .to_owned(); 93 | write!(f, "{}", str) 94 | } 95 | } 96 | 97 | /// Selects which tab (ie file) is active 98 | pub fn tab_selector(state: &mut State, ui: &mut Ui) { 99 | // Note: This assumes generic, paths_loaded etc are always the same length. (Which they *should* be.) 100 | 101 | let mut tab_removed = None; 102 | 103 | ui.horizontal(|ui| { 104 | if ui 105 | .button("New") 106 | .on_hover_text("Create and open a new file. (Ctrl + N)") 107 | .clicked() 108 | { 109 | state.add_tab(); 110 | state.tabs_open.push(Default::default()); 111 | } 112 | ui.add_space(COL_SPACING); 113 | 114 | let plasmid_names: &Vec<_> = &state 115 | .generic 116 | .iter() 117 | .map(|v| v.metadata.plasmid_name.as_str()) 118 | .collect(); 119 | 120 | for (name, i) in get_tab_names(&state.tabs_open, plasmid_names, false) { 121 | // todo: DRY with page selectors. 122 | 123 | let button = ui.button( 124 | select_color_text(&name, i == state.active).background_color(TAB_BUTTON_COLOR), 125 | ); 126 | 127 | if button.clicked() { 128 | state.active = i; 129 | set_window_title(&state.tabs_open[i], ui); 130 | 131 | // todo: Apt state sync fn for this? 132 | state.ui.seq_input = seq_to_str_lower(state.get_seq()); // todo: Move seq_input to an indexed vector? 133 | // todo: Cache these instead? 134 | // state.sync_seq_related(None); 135 | } 136 | 137 | if button.middle_clicked() { 138 | tab_removed = Some(i); 139 | } 140 | 141 | ui.add_space(COL_SPACING / 2.); 142 | } 143 | 144 | // todo: Right-align? 145 | ui.add_space(2. * ROW_SPACING); 146 | 147 | if ui 148 | .button( 149 | RichText::new("Close active tab") 150 | .color(Color32::WHITE) 151 | .background_color(Color32::DARK_RED), 152 | ) 153 | .on_hover_text("Shortcut: Middle click the tab to close it.") 154 | .clicked() 155 | { 156 | tab_removed = Some(state.active) 157 | }; 158 | }); 159 | 160 | if let Some(i) = tab_removed { 161 | state.remove_tab(i); 162 | } 163 | } 164 | 165 | pub fn page_selector(state: &mut State, ui: &mut Ui) { 166 | ui.horizontal(|ui| { 167 | page_button(&mut state.ui.page, Page::Sequence, ui, true); 168 | page_button(&mut state.ui.page, Page::Map, ui, true); 169 | page_button(&mut state.ui.page, Page::Features, ui, true); 170 | page_button(&mut state.ui.page, Page::Primers, ui, true); 171 | page_button(&mut state.ui.page, Page::Proteins, ui, true); 172 | page_button(&mut state.ui.page, Page::Cloning, ui, true); 173 | page_button(&mut state.ui.page, Page::Pcr, ui, true); 174 | page_button(&mut state.ui.page, Page::Alignment, ui, true); 175 | page_button(&mut state.ui.page, Page::Ligation, ui, true); 176 | page_button(&mut state.ui.page, Page::Metadata, ui, true); 177 | page_button(&mut state.ui.page, Page::Portions, ui, true); 178 | }); 179 | } 180 | 181 | pub fn page_button(page_state: &mut T, page: T, ui: &mut Ui, space: bool) { 182 | if ui 183 | .button( 184 | select_color_text(&page.to_string(), *page_state == page) 185 | .background_color(NAV_BUTTON_COLOR), 186 | ) 187 | .clicked() 188 | { 189 | *page_state = page; 190 | } 191 | 192 | if space { 193 | ui.add_space(COL_SPACING / 2.); 194 | } 195 | } 196 | 197 | /// This is used for selecting what is displayed in the sequence view, ie view or edit. 198 | #[derive(Clone, Copy, PartialEq, Encode, Decode)] 199 | pub enum PageSeq { 200 | EditRaw, 201 | // EditSlic, 202 | View, 203 | } 204 | 205 | impl Default for PageSeq { 206 | fn default() -> Self { 207 | Self::View 208 | } 209 | } 210 | 211 | impl Display for PageSeq { 212 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 213 | let str = match self { 214 | Self::EditRaw => "Edit raw", 215 | // Self::EditSlic => "SLIC/FC cloning", 216 | Self::View => "Sequence", 217 | } 218 | .to_owned(); 219 | write!(f, "{}", str) 220 | } 221 | } 222 | 223 | pub fn page_seq_selector(state: &mut State, ui: &mut Ui) { 224 | ui.horizontal(|ui| { 225 | page_button(&mut state.ui.page_seq, PageSeq::EditRaw, ui, true); 226 | page_button(&mut state.ui.page_seq, PageSeq::View, ui, true); 227 | }); 228 | } 229 | 230 | /// This is used for selecting what is displayed above the sequence view, ie various tabular editors. 231 | #[derive(Clone, Copy, PartialEq, Encode, Decode)] 232 | pub enum PageSeqTop { 233 | Primers, 234 | Features, 235 | None, 236 | } 237 | 238 | impl Default for PageSeqTop { 239 | fn default() -> Self { 240 | Self::None 241 | } 242 | } 243 | 244 | impl Display for PageSeqTop { 245 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 246 | let str = match self { 247 | Self::Primers => "Primers", 248 | Self::Features => "Features", 249 | Self::None => "None", 250 | } 251 | .to_owned(); 252 | write!(f, "{}", str) 253 | } 254 | } 255 | 256 | pub fn page_seq_top_selector(state: &mut State, ui: &mut Ui) { 257 | ui.horizontal(|ui| { 258 | ui.label("Display above sequence:"); 259 | 260 | page_button(&mut state.ui.page_seq_top, PageSeqTop::None, ui, true); 261 | page_button(&mut state.ui.page_seq_top, PageSeqTop::Features, ui, true); 262 | page_button(&mut state.ui.page_seq_top, PageSeqTop::Primers, ui, true); 263 | }); 264 | } 265 | 266 | /// A short, descriptive name for a given opened tab. 267 | pub fn name_from_path(path: &Option, plasmid_name: &str, abbrev_name: bool) -> String { 268 | let mut name = match path { 269 | Some(path) => path 270 | .file_name() 271 | .and_then(|name| name.to_str()) 272 | .map(|name_str| name_str.to_string()) 273 | .unwrap(), 274 | None => { 275 | if !plasmid_name.is_empty() { 276 | plasmid_name.to_owned() 277 | } else { 278 | DEFAULT_TAB_NAME.to_owned() 279 | } 280 | } 281 | }; 282 | 283 | if abbrev_name && name.len() > PATH_ABBREV_MAX_LEN { 284 | name = format!("{}...", &name[..PATH_ABBREV_MAX_LEN].to_string()) 285 | } 286 | 287 | name 288 | } 289 | -------------------------------------------------------------------------------- /src/gui/pcr.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use eframe::egui::{Color32, ComboBox, Grid, RichText, TextEdit, Ui, Vec2}; 4 | 5 | use crate::{ 6 | gui::{ 7 | COL_SPACING, ROW_SPACING, lin_maps, 8 | theme::{COLOR_ACTION, COLOR_INFO}, 9 | }, 10 | pcr::{PcrUi, PolymeraseType, TempTime, make_amplicon_tab}, 11 | primer::{Primer, PrimerDirection, TM_TARGET}, 12 | state::State, 13 | util::RangeIncl, 14 | }; 15 | 16 | fn temp_time_disp(tt: &TempTime, label: &str, ui: &mut Ui) { 17 | ui.label(&format!("{label}:")); 18 | ui.label(RichText::new(format!("{:.0}°C", tt.temp)).color(COLOR_INFO)); 19 | ui.label(RichText::new(format!("{}s", tt.time)).color(COLOR_INFO)); 20 | 21 | ui.end_row(); 22 | } 23 | 24 | fn numerical_field(label: &str, val: &mut T, default: T, ui: &mut Ui) -> bool 25 | where 26 | T: ToString + FromStr + Copy, 27 | ::Err: std::fmt::Debug, 28 | { 29 | let mut changed = false; 30 | 31 | ui.label(&format!("{}: ", label)); 32 | let mut entry = val.to_string(); 33 | if ui 34 | .add(TextEdit::singleline(&mut entry).desired_width(30.)) 35 | .changed() 36 | { 37 | *val = entry.parse().unwrap_or(default); 38 | changed = true; 39 | } 40 | 41 | changed 42 | } 43 | 44 | fn primer_dropdown( 45 | val: &mut usize, 46 | primers: &[Primer], 47 | direction: Option, 48 | id: usize, 49 | ui: &mut Ui, 50 | ) { 51 | // Reset primer selected if an invalid one is set. 52 | if *val > primers.len() { 53 | *val = 0; 54 | } 55 | 56 | let primer = &primers[*val]; 57 | 58 | ComboBox::from_id_salt(id) 59 | .width(80.) 60 | .selected_text(&primer.name) 61 | .show_ui(ui, |ui| { 62 | for (i, primer) in primers.iter().enumerate() { 63 | if let Some(dir) = direction { 64 | let mut dir_match = false; 65 | for match_ in &primer.volatile.matches { 66 | if match_.direction == dir { 67 | dir_match = true; 68 | } 69 | } 70 | if !dir_match { 71 | continue; 72 | } 73 | } 74 | 75 | ui.selectable_value(val, i, &primer.name); 76 | } 77 | }); 78 | } 79 | 80 | fn pcr_sim(state: &mut State, ui: &mut Ui) { 81 | let num_primers = state.generic[state.active].primers.len(); 82 | 83 | if state.ui.pcr.primer_fwd >= num_primers || state.ui.pcr.primer_rev >= num_primers { 84 | state.ui.pcr.primer_fwd = 0; 85 | state.ui.pcr.primer_rev = 0; 86 | } 87 | 88 | ui.heading("PCR product generation"); 89 | 90 | lin_maps::seq_lin_disp( 91 | &state.generic[state.active], 92 | false, 93 | state.ui.selected_item, 94 | &Vec::new(), 95 | None, 96 | &state.ui, 97 | &state.volatile[state.active].restriction_enzyme_matches, 98 | &state.restriction_enzyme_lib, 99 | ui, 100 | ); 101 | ui.add_space(ROW_SPACING / 2.); 102 | 103 | if num_primers >= 2 { 104 | ui.horizontal(|ui| { 105 | ui.label("Fwd:"); 106 | primer_dropdown( 107 | &mut state.ui.pcr.primer_fwd, 108 | &state.generic[state.active].primers, 109 | Some(PrimerDirection::Forward), 110 | 2, 111 | ui, 112 | ); 113 | 114 | ui.add_space(COL_SPACING); 115 | 116 | ui.label("Rev:"); 117 | primer_dropdown( 118 | &mut state.ui.pcr.primer_rev, 119 | &state.generic[state.active].primers, 120 | Some(PrimerDirection::Reverse), 121 | 3, 122 | ui, 123 | ); 124 | 125 | ui.add_space(COL_SPACING); 126 | 127 | // let fwd_primer = &state.generic[state.active].primers[state.ui.pcr.primer_fwd]; 128 | // let rev_primer = &state.generic[state.active].primers[state.ui.pcr.primer_rev]; 129 | 130 | // todo: Yikes on this syntax. 131 | if state.ui.pcr.primer_fwd == state.ui.pcr.primer_rev { 132 | ui.label("Select two different primers"); 133 | } else if state.generic[state.active].primers[state.ui.pcr.primer_fwd] 134 | .volatile 135 | .matches 136 | .len() 137 | == 1 138 | && state.generic[state.active].primers[state.ui.pcr.primer_rev] 139 | .volatile 140 | .matches 141 | .len() 142 | == 1 143 | { 144 | if ui 145 | .button(RichText::new("Simulate PCR").color(COLOR_ACTION)) 146 | .clicked() 147 | { 148 | let fwd_primer = 149 | state.generic[state.active].primers[state.ui.pcr.primer_fwd].clone(); 150 | let rev_primer = 151 | state.generic[state.active].primers[state.ui.pcr.primer_rev].clone(); 152 | 153 | // todo: Yikes. 154 | let range_fwd = fwd_primer.volatile.matches[0].range; 155 | let range_rev = rev_primer.volatile.matches[0].range; 156 | 157 | let range_combined = RangeIncl::new(range_fwd.start, range_rev.end); 158 | 159 | let product_seq = if range_combined.start > range_combined.end { 160 | range_combined 161 | .index_seq_wrap(&state.generic[state.active].seq) 162 | .unwrap() 163 | // todo unwrap is dicey. 164 | } else { 165 | range_combined 166 | .index_seq(&state.generic[state.active].seq) 167 | .unwrap() 168 | .to_vec() // todo unwrap is dicey. 169 | }; 170 | 171 | make_amplicon_tab(state, product_seq, range_combined, fwd_primer, rev_primer); 172 | } 173 | } else { 174 | ui.label("There must be exactly one match for each primer"); 175 | } 176 | }); 177 | } else { 178 | ui.label("(Add at least 2 primers to generate a PCR product)"); 179 | } 180 | } 181 | 182 | pub fn pcr_page(state: &mut State, ui: &mut Ui) { 183 | pcr_sim(state, ui); 184 | ui.add_space(ROW_SPACING * 2.); 185 | 186 | ui.horizontal(|ui| { 187 | ui.heading("PCR parameters"); 188 | if !state.generic[state.active].primers.is_empty() { 189 | ui.add_space(COL_SPACING); 190 | 191 | if ui.button("Load from primer: ").clicked() { 192 | let primer = &state.generic[state.active].primers[state.ui.pcr.primer_selected]; // todo: Overflow check? 193 | 194 | if let Some(metrics) = &primer.volatile.metrics { 195 | state.ui.pcr = PcrUi { 196 | primer_tm: metrics.melting_temp, 197 | product_len: state.get_seq().len(), 198 | primer_selected: state.ui.pcr.primer_selected, 199 | ..Default::default() 200 | }; 201 | } 202 | state.sync_pcr(); 203 | } 204 | 205 | primer_dropdown( 206 | &mut state.ui.pcr.primer_selected, 207 | &state.generic[state.active].primers, 208 | None, 209 | 1, 210 | ui, 211 | ); 212 | } 213 | }); 214 | 215 | // pub primer_tm: f32, 216 | // pub product_len: usize, 217 | // pub polymerase_type: PolymeraseType, 218 | // pub num_cycles: u16, 219 | 220 | ui.horizontal(|ui| { 221 | // todo: Allow TM decimals? 222 | // Not using our helper here due to int coercing. 223 | ui.label("Primer TM"); 224 | let mut entry = format!("{:.0}", state.ui.pcr.primer_tm); 225 | let response = ui.add(TextEdit::singleline(&mut entry).desired_width(20.)); 226 | if response.changed() { 227 | state.ui.pcr.primer_tm = entry.parse().unwrap_or(TM_TARGET); 228 | state.sync_pcr(); 229 | } 230 | 231 | // if numerical_field("Primer TM", &mut state.ui.pcr.primer_tm, TM_TARGET, ui) || 232 | if numerical_field( 233 | "Product size (bp)", 234 | &mut state.ui.pcr.product_len, 235 | 1_000, 236 | ui, 237 | ) || numerical_field("# cycles", &mut state.ui.pcr.num_cycles, 30, ui) 238 | { 239 | state.sync_pcr(); 240 | } 241 | 242 | ui.label("Polymerase:"); 243 | let prev_poly = state.ui.pcr.polymerase_type; 244 | ComboBox::from_id_salt(10) 245 | .width(80.) 246 | .selected_text(state.ui.pcr.polymerase_type.to_str()) 247 | .show_ui(ui, |ui| { 248 | ui.selectable_value( 249 | &mut state.ui.pcr.polymerase_type, 250 | PolymeraseType::NormalFidelity, 251 | PolymeraseType::NormalFidelity.to_str(), 252 | ); 253 | ui.selectable_value( 254 | &mut state.ui.pcr.polymerase_type, 255 | PolymeraseType::HighFidelity, 256 | PolymeraseType::HighFidelity.to_str(), 257 | ); 258 | }); 259 | 260 | if state.ui.pcr.polymerase_type != prev_poly { 261 | state.sync_pcr(); 262 | } 263 | }); 264 | 265 | ui.add_space(ROW_SPACING); 266 | 267 | Grid::new(0).spacing(Vec2::new(60., 0.)).show(ui, |ui| { 268 | temp_time_disp(&state.pcr.initial_denaturation, "Initial denaturation", ui); 269 | temp_time_disp(&state.pcr.denaturation, "Denaturation", ui); 270 | temp_time_disp(&state.pcr.annealing, "Annealing", ui); 271 | temp_time_disp(&state.pcr.extension, "Extension", ui); 272 | temp_time_disp(&state.pcr.final_extension, "Final extension", ui); 273 | 274 | ui.label("Number of cycles:".to_string()); 275 | ui.label(format!("{}", state.pcr.num_cycles)); 276 | 277 | ui.end_row(); 278 | }); 279 | } 280 | -------------------------------------------------------------------------------- /src/gui/portions.rs: -------------------------------------------------------------------------------- 1 | //! UI page for mixing portions. (growth media, stock solutions etc) 2 | 3 | use eframe::{ 4 | egui, 5 | egui::{Color32, ComboBox, RichText, TextEdit, Ui}, 6 | }; 7 | 8 | use crate::{ 9 | gui::{ 10 | COL_SPACING, ROW_SPACING, 11 | theme::{COLOR_ACTION, COLOR_INFO}, 12 | }, 13 | portions::{ 14 | MediaPrepInput, PlateSize, PortionsState, Reagent, ReagentPrep, ReagentType, Solution, 15 | media_prep, 16 | }, 17 | }; 18 | // todo: Make a non-gui portions module once this becomes unweildy. 19 | 20 | // todo: Store solutions. Save to file, and mix solutions from other solutions. 21 | 22 | const DEFAULT_REAGENT_MOLARITY: f32 = 1.; 23 | const DEFAULT_TOTAL_VOLUME: f32 = 1.; // L 24 | 25 | fn solutions_disp(portions: &mut PortionsState, ui: &mut Ui) { 26 | let mut sol_removed = None; 27 | for (i, solution) in portions.solutions.iter_mut().enumerate() { 28 | ui.horizontal(|ui| { 29 | // ui.heading(RichText::new(&solution.name).color(Color32::LIGHT_BLUE)); 30 | ui.add(TextEdit::singleline(&mut solution.name).desired_width(200.)); 31 | 32 | ui.label("Total volume (mL):"); 33 | 34 | // todo: Float field, or can we do ml? 35 | let mut val_ml = ((solution.total_volume * 1_000.) as u32).to_string(); 36 | let response = ui.add(TextEdit::singleline(&mut val_ml).desired_width(40.)); 37 | 38 | if response.changed() { 39 | let val_int: u32 = val_ml.parse().unwrap_or_default(); 40 | solution.total_volume = val_int as f32 / 1_000.; 41 | solution.calc_amounts(); 42 | } 43 | 44 | if ui 45 | .button(RichText::new("➕ Add reagent").color(COLOR_ACTION)) 46 | .clicked() 47 | { 48 | solution.reagents.push(Reagent::default()); 49 | } 50 | 51 | if ui 52 | .button(RichText::new("Delete solution 🗑").color(Color32::RED)) 53 | .clicked() 54 | { 55 | sol_removed = Some(i); 56 | } 57 | }); 58 | 59 | ui.add_space(ROW_SPACING / 2.); 60 | 61 | let mut reagent_removed = None; 62 | 63 | for (j, reagent) in solution.reagents.iter_mut().enumerate() { 64 | ui.horizontal(|ui| { 65 | let type_prev = reagent.type_; 66 | ComboBox::from_id_salt(100 + i * 100 + j) 67 | .width(140.) 68 | .selected_text(reagent.type_.to_string()) 69 | .show_ui(ui, |ui| { 70 | for type_ in [ 71 | ReagentType::Custom(0.), 72 | ReagentType::SodiumChloride, 73 | ReagentType::TrisHcl, 74 | ReagentType::Iptg, 75 | ReagentType::SodiumPhosphateMonobasic, 76 | ReagentType::SodiumPhosphateDibasic, 77 | ReagentType::SodiumPhosphateDibasicHeptahydrate, 78 | ReagentType::PotassiumPhosphateMonobasic, 79 | ReagentType::PotassiumPhosphateDibasic, 80 | ReagentType::Imidazole, 81 | ReagentType::Lysozyme, 82 | ReagentType::Mes, 83 | ReagentType::Bes, 84 | ReagentType::Tes, 85 | ReagentType::CitricAcid, 86 | ReagentType::Edta, 87 | ReagentType::HydrochloricAcid, 88 | ReagentType::SodiumHydroxide, 89 | ReagentType::BromophenolBlue, 90 | ReagentType::Dtt, 91 | ReagentType::MagnesiumChloride, 92 | ReagentType::Sds, 93 | ReagentType::Glycine, 94 | ReagentType::Tris, 95 | ] { 96 | ui.selectable_value(&mut reagent.type_, type_, type_.to_string()); 97 | } 98 | 99 | // ui.selectable_value(val, Reagent::Custom(_), Reagent::Custom(_).to_string()); 100 | }); 101 | 102 | if let ReagentType::Custom(weight) = &mut reagent.type_ { 103 | let mut val = format!("{:.2}", weight); 104 | if ui 105 | .add(TextEdit::singleline(&mut val).desired_width(40.)) 106 | .changed() 107 | { 108 | // let molarity_int: u32 = val.parse().unwrap_or_default(); 109 | *weight = val.parse().unwrap_or_default(); 110 | // *v = molarity_int as f32 / 1_000.; 111 | reagent.calc_amount(solution.total_volume); 112 | } 113 | ui.label("g/mol"); 114 | } else { 115 | ui.add_sized( 116 | [80.0, 20.0], 117 | egui::Label::new(format!("{:.2} g/mol", reagent.type_.weight())), 118 | ); 119 | } 120 | 121 | let prep_prev = reagent.prep; 122 | ComboBox::from_id_salt(2000 + i * 100 + j) 123 | .width(80.) 124 | .selected_text(reagent.prep.to_string()) 125 | .show_ui(ui, |ui| { 126 | for prep in [ 127 | ReagentPrep::Mass, 128 | ReagentPrep::Volume(DEFAULT_REAGENT_MOLARITY), 129 | ] { 130 | ui.selectable_value(&mut reagent.prep, prep, prep.to_string()); 131 | } 132 | }); 133 | 134 | // todo: This effect on water volume added? 135 | if let ReagentPrep::Volume(v) = &mut reagent.prep { 136 | ui.label("reagant Molarity (mM):"); 137 | let mut val = ((*v * 1_000.) as u32).to_string(); 138 | if ui 139 | .add(TextEdit::singleline(&mut val).desired_width(40.)) 140 | .changed() 141 | { 142 | let molarity_int: u32 = val.parse().unwrap_or_default(); 143 | *v = molarity_int as f32 / 1_000.; 144 | reagent.calc_amount(solution.total_volume); 145 | } 146 | } 147 | 148 | if type_prev != reagent.type_ || prep_prev != reagent.prep { 149 | reagent.calc_amount(solution.total_volume); 150 | } 151 | 152 | ui.add_space(COL_SPACING / 2.); 153 | ui.label("Molarity (mM):"); 154 | 155 | // Convert to a mM integer. 156 | let mut val = ((reagent.molarity * 1_000.) as u32).to_string(); 157 | if ui 158 | .add(TextEdit::singleline(&mut val).desired_width(40.)) 159 | .changed() 160 | { 161 | let molarity_int: u32 = val.parse().unwrap_or_default(); 162 | reagent.molarity = molarity_int as f32 / 1_000.; 163 | reagent.calc_amount(solution.total_volume); 164 | } 165 | 166 | ui.add_space(COL_SPACING / 2.); 167 | 168 | let result_label = if let ReagentPrep::Volume(_) = reagent.prep { 169 | "Volume" 170 | } else { 171 | "Mass" 172 | }; 173 | 174 | ui.label(result_label); 175 | 176 | ui.add_sized( 177 | [80.0, 20.0], 178 | egui::Label::new( 179 | RichText::new(reagent.amount_calc.to_string()).color(COLOR_INFO), 180 | ), 181 | ); 182 | 183 | ui.add_space(COL_SPACING); 184 | if ui.button(RichText::new("🗑").color(Color32::RED)).clicked() { 185 | reagent_removed = Some(j); 186 | } 187 | }); 188 | } 189 | 190 | if let Some(rem_i) = reagent_removed { 191 | solution.reagents.remove(rem_i); 192 | } 193 | 194 | ui.add_space(ROW_SPACING * 2.); 195 | } 196 | 197 | if let Some(rem_i) = sol_removed { 198 | portions.solutions.remove(rem_i); 199 | } 200 | } 201 | 202 | fn media_disp(portions: &mut PortionsState, ui: &mut Ui) { 203 | let type_prev = portions.media_input.clone(); 204 | let mut run_calc = false; 205 | 206 | ui.horizontal(|ui| { 207 | ComboBox::from_id_salt(3_000) 208 | .width(110.) 209 | .selected_text(portions.media_input.to_string()) 210 | .show_ui(ui, |ui| { 211 | for type_ in [ 212 | MediaPrepInput::Plates((PlateSize::D90, 6)), 213 | MediaPrepInput::Liquid(0.), 214 | ] { 215 | ui.selectable_value( 216 | &mut portions.media_input, 217 | type_.clone(), 218 | type_.to_string(), 219 | ); 220 | } 221 | }); 222 | 223 | ui.add_space(COL_SPACING); 224 | 225 | if portions.media_input != type_prev { 226 | run_calc = true; 227 | } 228 | 229 | match &mut portions.media_input { 230 | MediaPrepInput::Plates((plate_size, num)) => { 231 | ui.label("Plate diameter:"); 232 | 233 | let dia_prev = *plate_size; 234 | ComboBox::from_id_salt(3_001) 235 | .width(70.) 236 | .selected_text(plate_size.to_string()) 237 | .show_ui(ui, |ui| { 238 | for size in [ 239 | PlateSize::D60, 240 | PlateSize::D90, 241 | PlateSize::D100, 242 | PlateSize::D150, 243 | ] { 244 | ui.selectable_value(plate_size, size, size.to_string()); 245 | } 246 | }); 247 | 248 | if *plate_size != dia_prev { 249 | run_calc = true; 250 | } 251 | 252 | ui.label("Num plates:"); 253 | 254 | let mut val = num.to_string(); 255 | if ui 256 | .add(TextEdit::singleline(&mut val).desired_width(40.)) 257 | .changed() 258 | { 259 | *num = val.parse().unwrap_or_default(); 260 | run_calc = true; 261 | } 262 | } 263 | MediaPrepInput::Liquid(volume) => { 264 | ui.label("Volume (mL):"); 265 | 266 | let mut val_ml = (*volume * 1_000.).to_string(); 267 | if ui 268 | .add(TextEdit::singleline(&mut val_ml).desired_width(50.)) 269 | .changed() 270 | { 271 | let val_int: u32 = val_ml.parse().unwrap_or_default(); 272 | *volume = val_int as f32 / 1_000.; 273 | run_calc = true; 274 | } 275 | } 276 | } 277 | }); 278 | 279 | ui.add_space(ROW_SPACING); 280 | 281 | // todo: Adjust units etc A/R for higherh values. 282 | let result = &portions.media_result; 283 | ui.horizontal(|ui| { 284 | ui.label("Water: "); 285 | ui.label(format!("{:.1} mL", result.water * 1_000.)); 286 | ui.add_space(COL_SPACING); 287 | 288 | ui.label("LB: "); 289 | ui.label(format!("{:.2} g", result.food)); 290 | ui.add_space(COL_SPACING); 291 | 292 | if result.agar > 0. { 293 | ui.label("Agar: "); 294 | ui.label(format!("{:.2} g", result.agar)); 295 | ui.add_space(COL_SPACING); 296 | } 297 | 298 | ui.label("Antibiotic (1000×): "); 299 | ui.label(format!("{:.1} μL", result.antibiotic * 1_000.)); 300 | }); 301 | 302 | if run_calc { 303 | portions.media_result = media_prep(&portions.media_input); 304 | } 305 | } 306 | 307 | pub fn portions_page(portions: &mut PortionsState, ui: &mut Ui) { 308 | ui.add_space(ROW_SPACING / 2.); 309 | 310 | ui.horizontal(|ui| { 311 | ui.heading("Mixing portions"); 312 | ui.add_space(COL_SPACING); 313 | 314 | if ui 315 | .button(RichText::new("➕ Add solution").color(COLOR_ACTION)) 316 | .clicked() 317 | { 318 | portions.solutions.push(Solution { 319 | total_volume: DEFAULT_TOTAL_VOLUME, 320 | reagents: vec![Reagent::default()], 321 | ..Default::default() 322 | }); 323 | } 324 | }); 325 | 326 | ui.add_space(ROW_SPACING); 327 | 328 | solutions_disp(portions, ui); 329 | ui.add_space(ROW_SPACING); 330 | 331 | ui.heading("Growth media"); 332 | media_disp(portions, ui); 333 | } 334 | -------------------------------------------------------------------------------- /src/gui/protein.rs: -------------------------------------------------------------------------------- 1 | use bio_apis::rcsb::{self, PdbData}; 2 | use eframe::{ 3 | egui::{ 4 | Align2, Color32, FontFamily, FontId, Frame, Pos2, Rect, RichText, ScrollArea, Sense, Shape, 5 | Stroke, Ui, pos2, vec2, 6 | }, 7 | emath::RectTransform, 8 | epaint::PathShape, 9 | }; 10 | use na_seq::{AaIdent, AminoAcid}; 11 | 12 | use crate::{ 13 | gui::{ 14 | BACKGROUND_COLOR, COL_SPACING, ROW_SPACING, 15 | circle::TICK_COLOR, 16 | theme::{COLOR_ACTION, COLOR_INFO}, 17 | }, 18 | misc_types::FeatureType, 19 | state::State, 20 | }; 21 | 22 | const COLOR_PROT_SEQ: Color32 = Color32::from_rgb(255, 100, 200); 23 | const COLOR_PRE_POST_CODING_SEQ: Color32 = Color32::from_rgb(100, 255, 200); 24 | const FONT_SIZE_SEQ: f32 = 14.; 25 | 26 | const CHART_HEIGHT: f32 = 200.; 27 | const CHART_LINE_WIDTH: f32 = 2.; 28 | const CHART_LINE_COLOR: Color32 = Color32::from_rgb(255, 100, 100); 29 | 30 | const NUM_X_TICKS: usize = 12; 31 | 32 | // todo: Color-code AAs, start/stop codons etc. 33 | 34 | // todo: Eval how cacheing and state is handled. 35 | 36 | /// Convert an AA sequence to an ident string. 37 | fn make_aa_text(seq: &[AminoAcid], aa_ident_disp: AaIdent) -> String { 38 | let mut result = String::new(); 39 | for aa in seq { 40 | let aa_str = format!("{} ", aa.to_str(aa_ident_disp)); 41 | result.push_str(&aa_str); 42 | } 43 | 44 | result 45 | } 46 | 47 | /// Plot a hydrophobicity line plot. 48 | fn hydrophobicity_chart(data: &Vec<(usize, f32)>, ui: &mut Ui) { 49 | let stroke = Stroke::new(CHART_LINE_WIDTH, CHART_LINE_COLOR); 50 | 51 | Frame::canvas(ui.style()) 52 | .fill(BACKGROUND_COLOR) 53 | .show(ui, |ui| { 54 | let width = ui.available_width(); 55 | let (response, _painter) = { 56 | let desired_size = vec2(width, CHART_HEIGHT); 57 | // ui.allocate_painter(desired_size, Sense::click()) 58 | ui.allocate_painter(desired_size, Sense::click_and_drag()) 59 | }; 60 | 61 | let to_screen = RectTransform::from_to( 62 | Rect::from_min_size(Pos2::ZERO, response.rect.size()), 63 | response.rect, 64 | ); 65 | // let from_screen = to_screen.inverse(); 66 | 67 | const MAX_VAL: f32 = 6.; 68 | 69 | let mut points = Vec::new(); 70 | let num_pts = data.len() as f32; 71 | // todo: Consider cacheing the calculations here instead of running this each time. 72 | for pt in data { 73 | points.push( 74 | to_screen 75 | * pos2( 76 | pt.0 as f32 / num_pts * width, 77 | // Offset for 0 baseline. 78 | (pt.1 + MAX_VAL / 2.) / MAX_VAL * CHART_HEIGHT, 79 | ), 80 | ); 81 | } 82 | 83 | let line = Shape::Path(PathShape::line(points, stroke)); 84 | 85 | let mut x_axis = Vec::new(); 86 | 87 | if data.len() == 0 { 88 | return; 89 | } 90 | 91 | let data_range = data[data.len() - 1].0 - data[0].0; 92 | let dr_nt = data_range / NUM_X_TICKS; 93 | 94 | let x_axis_posit = CHART_HEIGHT - 4.; 95 | 96 | for i in 0..NUM_X_TICKS { 97 | let tick_v = data[0].0 + dr_nt * i; 98 | 99 | x_axis.push(ui.ctx().fonts(|fonts| { 100 | Shape::text( 101 | fonts, 102 | to_screen * pos2(i as f32 / NUM_X_TICKS as f32 * width, x_axis_posit), 103 | Align2::CENTER_CENTER, 104 | tick_v.to_string(), 105 | FontId::new(14., FontFamily::Proportional), 106 | TICK_COLOR, 107 | ) 108 | })); 109 | } 110 | 111 | let mut y_axis = Vec::new(); 112 | const NUM_Y_TICKS: usize = 7; 113 | let data_range = 2. * MAX_VAL; 114 | let dr_nt = data_range / NUM_Y_TICKS as f32; 115 | 116 | let y_axis_posit = 4.; 117 | 118 | for i in 0..NUM_Y_TICKS { 119 | let tick_v = -(-MAX_VAL + dr_nt * i as f32) as i8; 120 | 121 | y_axis.push(ui.ctx().fonts(|fonts| { 122 | Shape::text( 123 | fonts, 124 | // to_screen * pos2(y_axis_posit, i as f32 / NUM_Y_TICKS as f32 * CHART_HEIGHT), 125 | to_screen * pos2(y_axis_posit, i as f32 * dr_nt), 126 | Align2::CENTER_CENTER, 127 | tick_v.to_string(), 128 | FontId::new(14., FontFamily::Proportional), 129 | TICK_COLOR, 130 | ) 131 | })); 132 | } 133 | 134 | let center_axis = Shape::line_segment( 135 | [ 136 | to_screen * pos2(0., CHART_HEIGHT / 2.), 137 | to_screen * pos2(width, CHART_HEIGHT / 2.), 138 | ], 139 | Stroke::new(1., TICK_COLOR), 140 | ); 141 | 142 | ui.painter().extend([line]); 143 | ui.painter().extend(x_axis); 144 | // ui.painter().extend(y_axis); 145 | ui.painter().extend([center_axis]); 146 | }); 147 | } 148 | 149 | fn pdb_links(data: &PdbData, ui: &mut Ui) { 150 | ui.horizontal(|ui| { 151 | if ui 152 | .button(RichText::new("PDB").color(COLOR_ACTION)) 153 | .clicked() 154 | { 155 | rcsb::open_overview(&data.rcsb_id); 156 | } 157 | 158 | if ui.button(RichText::new("3D").color(COLOR_ACTION)).clicked() { 159 | rcsb::open_3d_view(&data.rcsb_id); 160 | } 161 | 162 | if ui 163 | .button(RichText::new("Structure").color(COLOR_ACTION)) 164 | .clicked() 165 | { 166 | rcsb::open_structure(&data.rcsb_id); 167 | } 168 | ui.add_space(COL_SPACING / 2.); 169 | 170 | ui.label( 171 | RichText::new(&format!("{}: {}", data.rcsb_id, data.title)) 172 | .color(Color32::LIGHT_BLUE) 173 | .font(FontId::monospace(14.)), 174 | ); 175 | }); 176 | } 177 | 178 | fn draw_proteins(state: &mut State, ui: &mut Ui) { 179 | for protein in &mut state.volatile[state.active].proteins { 180 | ui.horizontal(|ui| { 181 | ui.heading(RichText::new(&protein.feature.label).color(COLOR_INFO)); 182 | 183 | ui.add_space(COL_SPACING); 184 | 185 | if ui 186 | .button(RichText::new("PDB search").color(COLOR_ACTION)) 187 | .clicked() 188 | { 189 | match rcsb::pdb_data_from_seq(&protein.aa_seq) { 190 | Ok(pdb_data) => { 191 | state.ui.pdb_error_received = false; 192 | protein.pdb_data = pdb_data; 193 | } 194 | Err(_) => { 195 | eprintln!("Error fetching PDB results."); 196 | state.ui.pdb_error_received = true; 197 | } 198 | } 199 | } 200 | 201 | if state.ui.pdb_error_received { 202 | ui.label(RichText::new("Error getting PDB results").color(Color32::LIGHT_RED)); 203 | } 204 | // 205 | // if !protein.pdb_ids.is_empty() { 206 | // ui.add_space(COL_SPACING); 207 | // ui.label("Click to open a browser:"); 208 | // } 209 | }); 210 | 211 | if !protein.pdb_data.is_empty() { 212 | for pdb_result in &protein.pdb_data { 213 | // todo: Use a grid layout or similar; take advantage of horizontal space. 214 | pdb_links(pdb_result, ui); 215 | } 216 | ui.add_space(ROW_SPACING / 2.); 217 | } 218 | 219 | ui.horizontal(|ui| { 220 | ui.label(format!( 221 | "Reading frame: {}, Range: {}", 222 | protein.reading_frame_match.frame, protein.reading_frame_match.range 223 | )); 224 | ui.add_space(COL_SPACING); 225 | 226 | if protein.weight != protein.weight_with_prepost { 227 | // Only show this segment if pre and post-coding sequences exist 228 | ui.label(format!( 229 | "(Coding region only): AA len: {} Weight: {:.1}kDa", 230 | protein.aa_seq.len(), 231 | protein.weight, 232 | )); 233 | ui.add_space(COL_SPACING); 234 | } 235 | 236 | ui.label(format!( 237 | "AA len: {} Weight: {:.1}kDa", 238 | protein.aa_seq.len() 239 | + protein.aa_seq_precoding.len() 240 | + protein.aa_seq_postcoding.len(), 241 | protein.weight_with_prepost, 242 | )); 243 | }); 244 | ui.add_space(ROW_SPACING / 2.); 245 | 246 | let aa_text = make_aa_text(&protein.aa_seq, state.ui.aa_ident_disp); 247 | let aa_text_precoding = make_aa_text(&protein.aa_seq_precoding, state.ui.aa_ident_disp); 248 | let aa_text_postcoding = make_aa_text(&protein.aa_seq_postcoding, state.ui.aa_ident_disp); 249 | 250 | if !aa_text_precoding.is_empty() { 251 | ui.label( 252 | RichText::new(aa_text_precoding) 253 | .color(COLOR_PRE_POST_CODING_SEQ) 254 | .font(FontId::new(FONT_SIZE_SEQ, FontFamily::Monospace)), 255 | ); 256 | } 257 | 258 | ui.label( 259 | RichText::new(aa_text) 260 | .color(COLOR_PROT_SEQ) 261 | .font(FontId::new(FONT_SIZE_SEQ, FontFamily::Monospace)), 262 | ); 263 | 264 | if !aa_text_postcoding.is_empty() { 265 | ui.label( 266 | RichText::new(aa_text_postcoding) 267 | .color(COLOR_PRE_POST_CODING_SEQ) 268 | .font(FontId::new(FONT_SIZE_SEQ, FontFamily::Monospace)), 269 | ); 270 | } 271 | 272 | if protein.show_hydropath { 273 | ui.horizontal(|ui| { 274 | ui.heading("Hydrophopathy"); 275 | ui.add_space(COL_SPACING); 276 | 277 | ui.label("High values indicate hydrophobic regions; low ones hydrophilic regions."); 278 | ui.add_space(COL_SPACING); 279 | 280 | if ui.button("Hide hydropathy").clicked() { 281 | protein.show_hydropath = false; 282 | } 283 | }); 284 | 285 | hydrophobicity_chart(&protein.hydropath_data, ui); 286 | } else { 287 | if ui.button("Show hydrophpathy").clicked() { 288 | protein.show_hydropath = true; 289 | } 290 | } 291 | 292 | ui.add_space(ROW_SPACING * 2.); 293 | } 294 | } 295 | 296 | pub fn protein_page(state: &mut State, ui: &mut Ui) { 297 | ui.horizontal(|ui| { 298 | ui.heading("Proteins, from coding regions"); 299 | 300 | ui.add_space(COL_SPACING); 301 | ui.label("One letter ident:"); 302 | let mut one_letter = state.ui.aa_ident_disp == AaIdent::OneLetter; 303 | if ui.checkbox(&mut one_letter, "").changed() { 304 | state.ui.aa_ident_disp = if one_letter { 305 | AaIdent::OneLetter 306 | } else { 307 | AaIdent::ThreeLetters 308 | }; 309 | } 310 | }); 311 | ui.add_space(ROW_SPACING); 312 | 313 | if state.generic[state.active] 314 | .features 315 | .iter() 316 | .any(|f| f.feature_type == FeatureType::CodingRegion) 317 | { 318 | ScrollArea::vertical().id_salt(200).show(ui, |ui| { 319 | draw_proteins(state, ui); 320 | }); 321 | } else { 322 | ui.label("Create one or more Coding Region feature to display proteins here."); 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /src/gui/save.rs: -------------------------------------------------------------------------------- 1 | //! GUI code for saving and loading. Calls business logic in `file_io/save.rs`. 2 | 3 | use std::{env, path::Path}; 4 | 5 | use eframe::egui::Ui; 6 | use egui_file_dialog::FileDialog; 7 | 8 | use crate::{ 9 | file_io::{ 10 | genbank::export_genbank, 11 | save, 12 | save::{StateToSave, export_fasta, load_import}, 13 | snapgene::export_snapgene, 14 | }, 15 | gui::{navigation::Tab, set_window_title}, 16 | state::State, 17 | }; 18 | 19 | fn save_button( 20 | dialog: &mut FileDialog, 21 | plasmid_name: &str, 22 | extension: &str, 23 | text: &str, 24 | hover_text: &str, 25 | ui: &mut Ui, 26 | ) { 27 | if ui.button(text).on_hover_text(hover_text).clicked() { 28 | let filename = { 29 | let name = if plasmid_name.is_empty() { 30 | "a_plasmid".to_string() 31 | } else { 32 | plasmid_name.to_lowercase().replace(' ', "_") 33 | }; 34 | format!("{name}.{extension}") 35 | }; 36 | // save_path.push(Path::new(&filename)); 37 | 38 | dialog.config_mut().default_file_name = filename.to_string(); 39 | dialog.save_file(); 40 | } 41 | } 42 | 43 | fn load_button(dialog: &mut FileDialog, text: &str, hover_text: &str, ui: &mut Ui) { 44 | if ui.button(text).on_hover_text(hover_text).clicked() { 45 | dialog.pick_file(); 46 | } 47 | } 48 | 49 | /// Ui elements for saving and loading data in various file formats. This includes our own format, 50 | /// FASTA, and (eventually) SnapGene's DNA format. 51 | pub fn save_section(state: &mut State, ui: &mut Ui) { 52 | let button_text = if state.tabs_open[state.active].path.is_some() { 53 | "Save" 54 | } else { 55 | "Quicksave" 56 | }; 57 | if ui 58 | .button(button_text) 59 | .on_hover_text("Save data. (Ctrl + S)") 60 | .clicked() 61 | { 62 | save::save_current_file(state); 63 | } 64 | 65 | save_button( 66 | &mut state.ui.file_dialogs.save, 67 | &state.generic[state.active].metadata.plasmid_name, 68 | "pcad", 69 | "Save as", 70 | "Save data in the PlasCAD format. (Ctrl + Shift + S)", 71 | ui, 72 | ); 73 | 74 | load_button( 75 | &mut state.ui.file_dialogs.load, 76 | "Load/Import", 77 | "Load data in the PlasCAD, FASTA, GenBank, or .dna (SnapGene) formats (Ctrl + O)", 78 | ui, 79 | ); 80 | 81 | save_button( 82 | &mut state.ui.file_dialogs.export_fasta, 83 | &state.generic[state.active].metadata.plasmid_name, 84 | "fasta", 85 | "Exp FASTA", 86 | "Export the sequence in the FASTA format. This does not include features or primers.", 87 | ui, 88 | ); 89 | 90 | save_button( 91 | &mut state.ui.file_dialogs.export_genbank, 92 | &state.generic[state.active].metadata.plasmid_name, 93 | "gbk", 94 | "Exp GenBank", 95 | "Export data in the GenBank format.", 96 | ui, 97 | ); 98 | 99 | save_button( 100 | &mut state.ui.file_dialogs.export_dna, 101 | &state.generic[state.active].metadata.plasmid_name, 102 | "dna", 103 | "Exp SnapGene", 104 | "Export data in the .dna (SnapGene) format", 105 | ui, 106 | ); 107 | 108 | // todo: DRY. 109 | let ctx = ui.ctx(); 110 | 111 | state.ui.file_dialogs.save.update(ctx); 112 | state.ui.file_dialogs.load.update(ctx); 113 | state.ui.file_dialogs.export_fasta.update(ctx); 114 | state.ui.file_dialogs.export_genbank.update(ctx); 115 | state.ui.file_dialogs.export_dna.update(ctx); 116 | 117 | let mut sync = false; 118 | 119 | if let Some(path) = state.ui.file_dialogs.load.take_picked() { 120 | sync = true; 121 | if let Some(loaded) = load_import(&path) { 122 | state.load(&loaded); 123 | } 124 | } else if let Some(path) = state.ui.file_dialogs.save.take_picked() { 125 | match StateToSave::from_state(state, state.active).save_to_file(&path) { 126 | Ok(_) => { 127 | state.tabs_open[state.active] = Tab { 128 | path: Some(path.to_owned()), 129 | ab1: false, 130 | }; 131 | set_window_title(&state.tabs_open[state.active], ui); 132 | state.update_save_prefs(); // Save opened tabs. 133 | } 134 | Err(e) => eprintln!("Error saving in PlasCAD format: {:?}", e), 135 | }; 136 | } else if let Some(path) = state.ui.file_dialogs.export_fasta.take_picked() { 137 | match export_fasta( 138 | state.get_seq(), 139 | &state.generic[state.active].metadata.plasmid_name, 140 | &path, 141 | ) { 142 | Ok(_) => { 143 | state.tabs_open[state.active] = Tab { 144 | path: Some(path.to_owned()), 145 | ab1: false, 146 | }; 147 | set_window_title(&state.tabs_open[state.active], ui); 148 | state.update_save_prefs(); // Save opened tabs. 149 | } 150 | Err(e) => eprintln!("Error exporting to FASTA: {:?}", e), 151 | } 152 | } else if let Some(path) = state.ui.file_dialogs.export_genbank.take_picked() { 153 | let mut primer_matches = Vec::new(); 154 | for primer in &state.generic[state.active].primers { 155 | for prim_match in &primer.volatile.matches { 156 | primer_matches.push((prim_match.clone(), primer.name.clone())); 157 | } 158 | } 159 | 160 | match export_genbank(&state.generic[state.active], &primer_matches, &path) { 161 | Ok(_) => { 162 | state.tabs_open[state.active] = Tab { 163 | path: Some(path.to_owned()), 164 | ab1: false, 165 | }; 166 | set_window_title(&state.tabs_open[state.active], ui); 167 | state.update_save_prefs(); // Save opened tabs. 168 | } 169 | Err(e) => eprintln!("Error exporting to GenBank: {:?}", e), 170 | } 171 | } else if let Some(path) = state.ui.file_dialogs.export_dna.take_picked() { 172 | match export_snapgene(&state.generic[state.active], &path) { 173 | Ok(_) => { 174 | state.tabs_open[state.active] = Tab { 175 | path: Some(path.to_owned()), 176 | ab1: false, 177 | }; 178 | set_window_title(&state.tabs_open[state.active], ui); 179 | state.update_save_prefs(); // Save opened tabs. 180 | } 181 | Err(e) => eprintln!("Error exporting to SnapGene: {:?}", e), 182 | }; 183 | } 184 | 185 | if sync { 186 | state.sync_pcr(); 187 | state.sync_primer_metrics(); 188 | state.sync_seq_related(None); 189 | state.sync_portions(); 190 | state.reset_selections(); 191 | 192 | set_window_title(&state.tabs_open[state.active], ui); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/gui/sequence/feature_overlay.rs: -------------------------------------------------------------------------------- 1 | //! This module is related to drawing features on the sequence view. It is similar to `primer_overlay`. 2 | 3 | // todo: Abstract out diffs between this and the primer arrow; avoid repeated code. 4 | 5 | use std::mem; 6 | 7 | use eframe::{ 8 | egui::{Align2, Color32, FontFamily, FontId, Pos2, Shape, Stroke, Ui, pos2}, 9 | epaint::PathShape, 10 | }; 11 | 12 | use crate::{ 13 | Selection, 14 | gui::sequence::{ 15 | primer_overlay::{HEIGHT, LABEL_OFFSET, SLANT_DIV2, STROKE_WIDTH}, 16 | seq_view::{COLOR_CURSOR, NT_WIDTH_PX, SEQ_ROW_SPACING_PX, SeqViewData}, 17 | }, 18 | misc_types::{ 19 | Feature, FeatureDirection, 20 | FeatureDirection::{Forward, Reverse}, 21 | FeatureType, 22 | }, 23 | util::{RangeIncl, get_feature_ranges}, 24 | }; 25 | 26 | const VERTICAL_OFFSET_FEATURE: f32 = 18.; // A fudge factor? 27 | 28 | /// We include this in this module because visually, it is very similar to the overlay. 29 | /// Note: At least for now, selection uses 1-based indexing. 30 | pub fn draw_selection(mut selection: RangeIncl, data: &SeqViewData, ui: &mut Ui) -> Vec { 31 | let mut result = Vec::new(); 32 | 33 | // This reversal should only occur during reverse dragging; it should resolve when dragging is complete. 34 | if selection.start > selection.end { 35 | mem::swap(&mut selection.start, &mut selection.end); 36 | } 37 | 38 | if selection.start < 1 || selection.end > data.seq_len { 39 | eprintln!("Invalid sequence index"); 40 | return result; 41 | } 42 | 43 | // Todo: Cache this, and only update it if row_ranges change. See what else you can optimize 44 | // todo in this way. 45 | let selection_ranges = get_feature_ranges(&selection, &data.row_ranges, data.seq_len); 46 | 47 | let selection_ranges_px: Vec<(Pos2, Pos2)> = selection_ranges 48 | .iter() 49 | .map(|r| (data.seq_i_to_px_rel(r.start), data.seq_i_to_px_rel(r.end))) 50 | .collect(); 51 | 52 | result.append(&mut feature_seq_overlay( 53 | &selection_ranges_px, 54 | FeatureType::Selection, 55 | COLOR_CURSOR, 56 | VERTICAL_OFFSET_FEATURE, 57 | FeatureDirection::None, 58 | "", 59 | true, 60 | ui, 61 | )); 62 | 63 | result 64 | } 65 | 66 | pub fn draw_features( 67 | features: &[Feature], 68 | selected_item: Selection, 69 | data: &SeqViewData, 70 | ui: &mut Ui, 71 | ) -> Vec { 72 | let mut result = Vec::new(); 73 | 74 | for (i, feature) in features.iter().enumerate() { 75 | // Source features generally take up the whole plasmid length. 76 | // Alternative: Filter by features that take up the whole length. 77 | if feature.feature_type == FeatureType::Source { 78 | continue; 79 | } 80 | 81 | if feature.range.start < 1 { 82 | eprintln!("Invalid sequence index"); 83 | continue; // 0 is invalid, in 1-based indexing, and will underflow. 84 | } 85 | 86 | // Todo: Cache this, and only update it if row_ranges change. See what else you can optimize 87 | // todo in this way. 88 | let feature_ranges = get_feature_ranges(&feature.range, &data.row_ranges, data.seq_len); 89 | 90 | let feature_ranges_px: Vec<(Pos2, Pos2)> = feature_ranges 91 | .iter() 92 | .map(|r| (data.seq_i_to_px_rel(r.start), data.seq_i_to_px_rel(r.end))) 93 | .collect(); 94 | 95 | let selected = match selected_item { 96 | Selection::Feature(j) => i == j, 97 | _ => false, 98 | }; 99 | 100 | let (r, g, b) = feature.color(); 101 | let color = Color32::from_rgb(r, g, b); 102 | 103 | result.append(&mut feature_seq_overlay( 104 | &feature_ranges_px, 105 | feature.feature_type, 106 | color, 107 | VERTICAL_OFFSET_FEATURE, 108 | feature.direction, 109 | &feature.label(), 110 | selected, 111 | ui, 112 | )); 113 | } 114 | result 115 | } 116 | 117 | /// Make a visual indicator on the sequence view for a feature, including primers. 118 | /// For use inside a Frame::canvas. 119 | pub fn feature_seq_overlay( 120 | feature_ranges_px: &[(Pos2, Pos2)], 121 | feature_type: FeatureType, 122 | color: Color32, 123 | vertical_offset: f32, 124 | direction: FeatureDirection, 125 | label: &str, 126 | filled: bool, 127 | ui: &mut Ui, 128 | ) -> Vec { 129 | if feature_ranges_px.is_empty() { 130 | return Vec::new(); 131 | } 132 | let stroke = Stroke::new(STROKE_WIDTH, color); 133 | 134 | let color_label = Color32::LIGHT_GREEN; 135 | 136 | let v_offset_rev = 2. * vertical_offset; 137 | 138 | // Apply a vertical offset from the sequence. 139 | let feature_ranges_px: Vec<(Pos2, Pos2)> = feature_ranges_px 140 | .iter() 141 | .map(|(start, end)| { 142 | ( 143 | pos2(start.x, start.y - vertical_offset), 144 | pos2(end.x, end.y - vertical_offset), 145 | ) 146 | }) 147 | .collect(); 148 | 149 | let mut result = Vec::new(); 150 | 151 | // Depends on font size. 152 | let rev_primer_offset = -1.; 153 | 154 | for (i, &(mut start, mut end)) in feature_ranges_px.iter().enumerate() { 155 | // Display the overlay centered around the NT letters, vice above, for non-primer features. 156 | if feature_type != FeatureType::Primer { 157 | start.y += SEQ_ROW_SPACING_PX / 2. - 2.; 158 | end.y += SEQ_ROW_SPACING_PX / 2. - 2.; 159 | } 160 | 161 | let mut top_left = start; 162 | let mut top_right = pos2(end.x + NT_WIDTH_PX, end.y); 163 | let mut bottom_left = pos2(start.x, start.y + HEIGHT); 164 | let mut bottom_right = pos2(end.x + NT_WIDTH_PX, end.y + HEIGHT); 165 | 166 | // Display reverse primers below the sequence; this vertically mirrors. 167 | if feature_type == FeatureType::Primer && direction == Reverse { 168 | top_left.y += 3. * HEIGHT - rev_primer_offset; 169 | top_right.y += 3. * HEIGHT - rev_primer_offset; 170 | bottom_left.y += HEIGHT - rev_primer_offset; 171 | bottom_right.y += HEIGHT - rev_primer_offset; 172 | } 173 | 174 | // Add a slant, if applicable. 175 | match direction { 176 | Forward => { 177 | if i + 1 == feature_ranges_px.len() { 178 | top_right.x -= SLANT_DIV2; 179 | bottom_right.x += SLANT_DIV2; 180 | } 181 | } 182 | Reverse => { 183 | if i == 0 { 184 | top_left.x += SLANT_DIV2; 185 | bottom_left.x -= SLANT_DIV2; 186 | } 187 | } 188 | _ => (), 189 | } 190 | 191 | // let shape = match feature_type { 192 | let shape = if filled { 193 | Shape::Path(PathShape::convex_polygon( 194 | vec![top_left, bottom_left, bottom_right, top_right], 195 | stroke.color, 196 | stroke, 197 | )) 198 | } else { 199 | Shape::Path(PathShape::closed_line( 200 | vec![top_left, bottom_left, bottom_right, top_right], 201 | stroke, 202 | )) 203 | }; 204 | 205 | result.push(shape); 206 | } 207 | 208 | // todo: Examine. 209 | let label_start_x = match direction { 210 | Forward => feature_ranges_px[0].0.x, 211 | Reverse => feature_ranges_px[feature_ranges_px.len() - 1].1.x, 212 | FeatureDirection::None => feature_ranges_px[0].0.x, 213 | } + LABEL_OFFSET; 214 | 215 | let mut label_pos = match direction { 216 | Forward => pos2(label_start_x, feature_ranges_px[0].0.y + LABEL_OFFSET), 217 | Reverse => pos2( 218 | label_start_x, 219 | feature_ranges_px[0].0.y + LABEL_OFFSET + v_offset_rev, 220 | ), 221 | FeatureDirection::None => pos2(label_start_x, feature_ranges_px[0].0.y + LABEL_OFFSET), // todo: Examine 222 | }; 223 | 224 | let label_align = if feature_type == FeatureType::Primer && direction == Reverse { 225 | label_pos.y -= 2.; 226 | Align2::RIGHT_CENTER 227 | } else { 228 | Align2::LEFT_CENTER 229 | }; 230 | 231 | let label = ui.ctx().fonts(|fonts| { 232 | Shape::text( 233 | fonts, 234 | label_pos, 235 | label_align, 236 | label, 237 | FontId::new(13., FontFamily::Proportional), 238 | color_label, 239 | ) 240 | }); 241 | 242 | result.push(label); 243 | result 244 | } 245 | -------------------------------------------------------------------------------- /src/gui/sequence/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module contains GUI code related to the sequence view. 2 | 3 | use eframe::egui::{Color32, Frame, RichText, ScrollArea, TextEdit, Ui, text::CursorRange}; 4 | use na_seq::{seq_complement, seq_from_str, seq_to_str_lower}; 5 | 6 | // todo: monospace font for all seqs. 7 | use crate::gui::{COL_SPACING, ROW_SPACING}; 8 | // todo: monospace font for all seqs. 9 | use crate::misc_types::{Feature, FeatureDirection, MIN_SEARCH_LEN}; 10 | // todo: monospace font for all seqs. 11 | use crate::state::State; 12 | use crate::{ 13 | Selection, 14 | gui::{ 15 | PRIMER_FWD_COLOR, SPLIT_SCREEN_MAX_HEIGHT, 16 | circle::feature_range_sliders, 17 | feature_table::{direction_picker, feature_table}, 18 | navigation::{PageSeq, PageSeqTop, page_seq_selector, page_seq_top_selector}, 19 | primer_table::primer_details, 20 | sequence::seq_view::sequence_vis, 21 | theme::COLOR_ACTION, 22 | }, 23 | primer::{Primer, PrimerData}, 24 | util::RangeIncl, 25 | }; 26 | 27 | mod feature_overlay; 28 | mod primer_overlay; 29 | pub mod seq_view; 30 | 31 | fn seq_editor_raw(state: &mut State, ui: &mut Ui) { 32 | ui.horizontal(|ui| { 33 | ui.heading("Sequence:"); 34 | ui.label(&format!("len: {}", state.ui.seq_input.len())); 35 | }); 36 | 37 | ScrollArea::vertical().id_salt(200).show(ui, |ui| { 38 | let response = ui.add(TextEdit::multiline(&mut state.ui.seq_input).desired_width(800.)); 39 | if response.changed() { 40 | state.generic[state.active].seq = seq_from_str(&state.ui.seq_input); 41 | state.ui.seq_input = seq_to_str_lower(state.get_seq()); 42 | state.sync_seq_related(None); 43 | } 44 | }); 45 | } 46 | 47 | /// Displays text of the feature under the cursor, or selected, as required. 48 | fn feature_text(i: usize, features: &[Feature], seq_len: usize, ui: &mut Ui) { 49 | if i >= features.len() { 50 | eprintln!("Invalid selected feature"); 51 | return; // todo: Ideally set the feature to none. 52 | } 53 | let feature = &features[i]; 54 | 55 | ui.label(&feature.label); 56 | ui.label(feature.location_descrip(seq_len)); 57 | let (r, g, b) = feature.color(); 58 | ui.label(RichText::new(feature.feature_type.to_string()).color(Color32::from_rgb(r, g, b))); 59 | 60 | // todo? 61 | for note in &feature.notes { 62 | // ui.label(&format!("{}: {}", note.0, note.1)); 63 | } 64 | } 65 | 66 | fn primer_text(i: usize, primers: &[Primer], seq_len: usize, ui: &mut Ui) { 67 | if i >= primers.len() { 68 | eprintln!("Invalid selected primer"); 69 | return; // todo: Ideally set the feature to none. 70 | } 71 | let primer = &primers[i]; 72 | 73 | ui.label(&primer.name); 74 | ui.label(&primer.location_descrip()); 75 | // todo: Rev color A/R 76 | ui.label(RichText::new(seq_to_str_lower(&primer.sequence)).color(PRIMER_FWD_COLOR)); 77 | 78 | ui.label(&primer.description.clone().unwrap_or_default()); 79 | } 80 | 81 | /// Add a toolbar to create a feature from selection, if appropriate. 82 | fn feature_from_sel(state: &mut State, ui: &mut Ui) { 83 | if let Some(text_sel) = state.ui.text_selection { 84 | if ui 85 | .button(RichText::new("➕ Add feature from sel").color(COLOR_ACTION)) 86 | .clicked() 87 | { 88 | state.generic[state.active].features.push(Feature { 89 | range: text_sel, 90 | label: state.ui.quick_feature_add_name.clone(), 91 | direction: state.ui.quick_feature_add_dir, 92 | ..Default::default() 93 | }); 94 | 95 | state.ui.text_selection = None; 96 | state.ui.quick_feature_add_name = String::new(); 97 | } 98 | 99 | if ui 100 | .button(RichText::new("➕ Add primer from sel").color(COLOR_ACTION)) 101 | .clicked() 102 | { 103 | // todo: DRY with genbank parsing; common fn A/R. 104 | let seq = state.get_seq(); 105 | let compl = &seq_complement(seq); 106 | let seq_primer = match state.ui.quick_feature_add_dir { 107 | FeatureDirection::Reverse => { 108 | let range = RangeIncl::new( 109 | seq.len() - text_sel.end + 1, 110 | seq.len() - text_sel.start + 1, 111 | ); 112 | 113 | range.index_seq(compl).unwrap_or_default() 114 | } 115 | _ => text_sel.index_seq(seq).unwrap_or_default(), 116 | } 117 | .to_vec(); 118 | 119 | let volatile = PrimerData::new(&seq_primer); 120 | 121 | state.generic[state.active].primers.push(Primer { 122 | sequence: seq_primer, 123 | name: state.ui.quick_feature_add_name.clone(), 124 | description: None, 125 | volatile, 126 | }); 127 | 128 | state.ui.quick_feature_add_name = String::new(); 129 | state.sync_primer_matches(None); 130 | state.sync_primer_metrics(); 131 | } 132 | 133 | direction_picker(&mut state.ui.quick_feature_add_dir, 200, ui); 134 | 135 | ui.label("Name:"); 136 | if ui 137 | .add(TextEdit::singleline(&mut state.ui.quick_feature_add_name).desired_width(80.)) 138 | .gained_focus() 139 | { 140 | state.ui.text_edit_active = true; // Disable character entries in the sequence. 141 | } 142 | 143 | ui.add_space(COL_SPACING) 144 | } 145 | } 146 | 147 | /// Component for the sequence page. 148 | pub fn seq_page(state: &mut State, ui: &mut Ui) { 149 | ui.horizontal(|ui| { 150 | page_seq_top_selector(state, ui); 151 | 152 | ui.add_space(COL_SPACING); 153 | 154 | feature_from_sel(state, ui); 155 | 156 | // Sliders to edit the feature. 157 | feature_range_sliders(state, ui); 158 | }); 159 | 160 | // Limit the top section height. 161 | let screen_height = ui.ctx().available_rect().height(); 162 | let half_screen_height = screen_height / SPLIT_SCREEN_MAX_HEIGHT; 163 | 164 | match state.ui.page_seq_top { 165 | PageSeqTop::Primers => { 166 | Frame::none().show(ui, |ui| { 167 | ScrollArea::vertical() 168 | .max_height(half_screen_height) 169 | .id_salt(69) 170 | .show(ui, |ui| primer_details(state, ui)); 171 | }); 172 | } 173 | PageSeqTop::Features => { 174 | Frame::none().show(ui, |ui| { 175 | ScrollArea::vertical() 176 | .max_height(half_screen_height) 177 | .id_salt(70) 178 | .show(ui, |ui| { 179 | feature_table(state, ui); 180 | }); 181 | }); 182 | } 183 | PageSeqTop::None => (), 184 | } 185 | 186 | ui.add_space(ROW_SPACING); 187 | 188 | ui.horizontal(|ui| { 189 | page_seq_selector(state, ui); 190 | ui.add_space(COL_SPACING); 191 | 192 | ui.label("🔍").on_hover_text( 193 | "Search the sequence and its complement for this term. (Ctrl + F to highlight)", 194 | ); 195 | 196 | // This nonstandard way of adding the text input is required for the auto-highlight on ctrl+F behavior. 197 | let mut output = TextEdit::singleline(&mut state.ui.search_input) 198 | .desired_width(400.) 199 | .show(ui); 200 | let response = output.response; 201 | 202 | if state.ui.highlight_search_input { 203 | state.ui.highlight_search_input = false; 204 | state.ui.text_edit_active = true; // Disable character entries in the sequence. 205 | response.request_focus(); 206 | 207 | output.cursor_range = Some(CursorRange::select_all(&output.galley)); 208 | // todo: Not working 209 | } 210 | 211 | if response.gained_focus() { 212 | state.ui.text_edit_active = true; // Disable character entries in the sequence. 213 | println!("GF"); 214 | } 215 | 216 | if response.changed() { 217 | state.ui.text_edit_active = true; 218 | state.search_seq = seq_from_str(&state.ui.search_input); 219 | state.ui.search_input = seq_to_str_lower(&state.search_seq); // Ensures only valid NTs are present. 220 | 221 | // todo: This still adds a single char, then blanks the cursor... 222 | state.ui.text_cursor_i = None; // Make sure we are not adding chars. 223 | state.sync_search(); 224 | }; 225 | 226 | if state.ui.search_input.len() >= MIN_SEARCH_LEN { 227 | let len = state.volatile[state.active].search_matches.len(); 228 | let text = if len == 1 { 229 | "1 match".to_string() 230 | } else { 231 | format!("{} matches", len) 232 | }; 233 | ui.label(text); 234 | } 235 | 236 | ui.add_space(COL_SPACING); 237 | 238 | let mut feature_to_disp = None; 239 | let mut primer_to_disp = None; 240 | 241 | match state.ui.selected_item { 242 | Selection::Feature(i) => feature_to_disp = Some(i), 243 | Selection::Primer(i) => primer_to_disp = Some(i), 244 | Selection::None => { 245 | if state.ui.feature_hover.is_some() { 246 | feature_to_disp = Some(state.ui.feature_hover.unwrap()); 247 | } 248 | } 249 | } 250 | 251 | if let Some(feature_i) = feature_to_disp { 252 | feature_text( 253 | feature_i, 254 | &state.generic[state.active].features, 255 | state.get_seq().len(), 256 | ui, 257 | ); 258 | } 259 | 260 | if let Some(primer_i) = primer_to_disp { 261 | primer_text( 262 | primer_i, 263 | &state.generic[state.active].primers, 264 | state.get_seq().len(), 265 | ui, 266 | ); 267 | } 268 | }); 269 | 270 | ui.add_space(ROW_SPACING / 2.); 271 | 272 | ScrollArea::vertical() 273 | .id_salt(100) 274 | .show(ui, |ui| match state.ui.page_seq { 275 | PageSeq::EditRaw => { 276 | state.ui.text_cursor_i = None; // prevents double-edits 277 | seq_editor_raw(state, ui); 278 | } 279 | PageSeq::View => { 280 | sequence_vis(state, ui); 281 | } 282 | }); 283 | } 284 | -------------------------------------------------------------------------------- /src/gui/sequence/primer_overlay.rs: -------------------------------------------------------------------------------- 1 | //! This module contains code related to drawing primer arrows in the sequence view. 2 | 3 | use eframe::egui::{Pos2, Shape, Ui}; 4 | 5 | use crate::{ 6 | Selection, 7 | gui::sequence::{feature_overlay, seq_view::SeqViewData}, 8 | misc_types::FeatureType, 9 | primer::{Primer, PrimerDirection}, 10 | util, 11 | util::RangeIncl, 12 | }; 13 | 14 | pub const STROKE_WIDTH: f32 = 2.; 15 | 16 | pub const VERTICAL_OFFSET_PRIMER: f32 = 18.; // A fudge factor? 17 | pub const LABEL_OFFSET: f32 = 7.; 18 | pub const HEIGHT: f32 = 16.; 19 | pub const SLANT: f32 = 12.; // slant different, in pixels, for the arrow. 20 | pub const SLANT_DIV2: f32 = SLANT / 2.; 21 | 22 | /// Add primer arrows to the display. 23 | pub fn draw_primers( 24 | primers: &[Primer], 25 | selected_item: Selection, 26 | data: &SeqViewData, 27 | ui: &mut Ui, 28 | ) -> Vec { 29 | let mut shapes = Vec::new(); 30 | 31 | for (i, primer) in primers.iter().enumerate() { 32 | let primer_matches = &primer.volatile.matches; 33 | 34 | // todo: Do not run these calcs each time. Cache. 35 | for prim_match in primer_matches { 36 | // We currently index primers relative to the end they started. 37 | 38 | // Note: Because if we're displaying above the seq and below, the base of the arrow must match, 39 | // hence the offset. 40 | let seq_range = match prim_match.direction { 41 | PrimerDirection::Forward => { 42 | // todo: Getting an underflow, but not sure why yet. 43 | let end = if prim_match.range.end > 0 { 44 | prim_match.range.end - 1 45 | } else { 46 | prim_match.range.end 47 | }; 48 | RangeIncl::new(prim_match.range.start, end) 49 | } 50 | 51 | PrimerDirection::Reverse => { 52 | RangeIncl::new(prim_match.range.start + 1, prim_match.range.end) 53 | } 54 | }; 55 | 56 | let feature_ranges = 57 | util::get_feature_ranges(&seq_range, &data.row_ranges, data.seq_len); 58 | 59 | let feature_ranges_px: Vec<(Pos2, Pos2)> = feature_ranges 60 | .iter() 61 | .map(|r| (data.seq_i_to_px_rel(r.start), data.seq_i_to_px_rel(r.end))) 62 | .collect(); 63 | 64 | let color = prim_match.direction.color(); 65 | 66 | let selected = match selected_item { 67 | Selection::Primer(j) => i == j, 68 | _ => false, 69 | }; 70 | 71 | // todo: PUt back; temp check on compiling. 72 | shapes.append(&mut feature_overlay::feature_seq_overlay( 73 | &feature_ranges_px, 74 | FeatureType::Primer, 75 | color, 76 | VERTICAL_OFFSET_PRIMER, 77 | (prim_match.direction).into(), 78 | &primer.name, 79 | selected, 80 | ui, 81 | )); 82 | } 83 | } 84 | shapes 85 | } 86 | -------------------------------------------------------------------------------- /src/gui/theme.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::Color32; 2 | 3 | pub const COLOR_ACTION: Color32 = Color32::GOLD; 4 | pub const COLOR_INFO: Color32 = Color32::LIGHT_BLUE; 5 | -------------------------------------------------------------------------------- /src/ligation.rs: -------------------------------------------------------------------------------- 1 | use na_seq::restriction_enzyme::RestrictionEnzyme; 2 | 3 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // Disables the terminal window on Windows, in release mode. 2 | #![cfg_attr( 3 | all(not(debug_assertions), target_os = "windows"), 4 | windows_subsystem = "windows" 5 | )] 6 | 7 | // todo: Build a database of feature sequences. You can find GenBank etc files online (addGene, and other sources) 8 | // todo: and parse common features. 9 | 10 | // todo: Break out Generic into its own mod? 11 | 12 | // todo: 13 | // Focus on tools that allow you to conveniently design plasmid seqs based on source vectors, REs etc. It will make primers, 14 | // choose how to combine sequences etc. so, more towards realistic, product-first workflows. 15 | // 16 | // This will make more sense when you are designing new products. 17 | // For example: consider a lib of Addgene's generic vectors for E. Coli. 18 | // 19 | // The input: your target product: Output: as much we can automate as possible. 20 | 21 | // Reading frame: Guess the frame, and truncate the start based on CodingRegion and Gene feature types? 22 | use std::{env, path::PathBuf, str::FromStr}; 23 | 24 | use bincode::{Decode, Encode}; 25 | use cloning::CloningInsertData; 26 | use copypasta::ClipboardProvider; 27 | use eframe::{ 28 | self, 29 | egui::{self}, 30 | }; 31 | use file_io::save::{QUICKSAVE_FILE, load_import}; 32 | use gui::navigation::{Page, PageSeq}; 33 | use na_seq::{AaIdent, Nucleotide, Seq, restriction_enzyme::RestrictionEnzyme}; 34 | use state::State; 35 | 36 | use crate::{ 37 | backbones::BackboneFilters, 38 | file_io::{FileDialogs, save::DEFAULT_PREFS_FILE}, 39 | gui::{ 40 | WINDOW_HEIGHT, WINDOW_WIDTH, 41 | navigation::{PageSeqTop, Tab}, 42 | }, 43 | misc_types::{FeatureDirection, FeatureType}, 44 | pcr::PcrUi, 45 | primer::TM_TARGET, 46 | util::{RangeIncl, get_window_title}, 47 | }; 48 | 49 | mod ab1; 50 | mod alignment; 51 | mod alignment_map; 52 | mod backbones; 53 | mod cloning; 54 | mod external_websites; 55 | mod feature_db_load; 56 | mod file_io; 57 | mod gui; 58 | mod melting_temp_calcs; 59 | mod misc_types; 60 | mod pcr; 61 | mod portions; 62 | mod primer; 63 | mod primer_metrics; 64 | mod protein; 65 | mod reading_frame; 66 | mod save_compat; 67 | mod solution_helper; 68 | mod state; 69 | mod tags; 70 | mod toxic_proteins; 71 | mod util; 72 | 73 | type Color = (u8, u8, u8); // RGB 74 | 75 | // todo: Eventually, implement a system that automatically checks for changes, and don't 76 | // todo save to disk if there are no changes. 77 | const PREFS_SAVE_INTERVAL: u64 = 60; // Save user preferences this often, in seconds. 78 | 79 | #[derive(Default, Encode, Decode)] 80 | struct StateFeatureAdd { 81 | // This is in 1-based indexing. 82 | start_posit: usize, 83 | end_posit: usize, 84 | feature_type: FeatureType, 85 | direction: FeatureDirection, 86 | label: String, 87 | color: Option, 88 | } 89 | 90 | #[derive(Clone, Encode, Decode)] 91 | /// This Ui struct is used to determine which items on the sequence and map views to show and hide. 92 | struct SeqVisibility { 93 | /// Show or hide restriction enzymes from the sequence view. 94 | show_res: bool, 95 | /// Show and hide primers on 96 | show_primers: bool, 97 | /// todo: Show and hide individual features? 98 | show_features: bool, 99 | show_reading_frame: bool, 100 | } 101 | 102 | impl Default for SeqVisibility { 103 | fn default() -> Self { 104 | Self { 105 | show_res: true, 106 | show_primers: true, 107 | show_features: true, 108 | show_reading_frame: false, 109 | } 110 | } 111 | } 112 | 113 | /// UI state for restriction enzymes. 114 | pub struct ReUi { 115 | /// Inner: RE name 116 | /// todo: This is a trap for multiple tabs. 117 | // res_selected: Vec, 118 | res_selected: Vec, 119 | /// Which tabs' sequences to digest. Note that the index of this vec doesn't matter; the values, which 120 | /// point to indices elsewhere, does. 121 | tabs_selected: Vec, 122 | unique_cutters_only: bool, 123 | /// No blunt ends; must produce overhangs. 124 | sticky_ends_only: bool, 125 | /// Only show REs that are present in at least two sequences. 126 | multiple_seqs: bool, 127 | } 128 | 129 | impl Default for ReUi { 130 | fn default() -> Self { 131 | Self { 132 | res_selected: Default::default(), 133 | tabs_selected: Default::default(), 134 | unique_cutters_only: true, 135 | sticky_ends_only: false, 136 | multiple_seqs: true, 137 | } 138 | } 139 | } 140 | 141 | /// Values defined here generally aren't worth saving to file etc. 142 | struct StateUi { 143 | // todo: Make separate primer cols and primer data; data in state. primer_cols are pre-formatted 144 | // todo to save computation. 145 | page: Page, 146 | page_seq: PageSeq, 147 | page_seq_top: PageSeqTop, 148 | seq_input: String, // todo: Consider moving this to volatile. 149 | pcr: PcrUi, 150 | feature_add: StateFeatureAdd, 151 | // primer_selected: Option, // primer page only. 152 | feature_hover: Option, // todo: Apply similar enum logic to selection: Allow Primer::, Feature::, or None:: 153 | selected_item: Selection, 154 | seq_visibility: SeqVisibility, 155 | hide_map_feature_editor: bool, 156 | /// Mouse cursor 157 | cursor_pos: Option<(f32, f32)>, 158 | /// Mouse cursor 159 | cursor_seq_i: Option, 160 | file_dialogs: FileDialogs, 161 | /// Show or hide the field to change origin 162 | show_origin_change: bool, 163 | new_origin: usize, 164 | /// Text-editing cursor. Used for editing on the sequence view. Chars typed 165 | /// will be inserted after this index. This index is 0-based. 166 | text_cursor_i: Option, 167 | /// We store if we've clicked somewhere separately from the action, as getting a sequence index 168 | /// from cursor positions may be decoupled, and depends on the view. 169 | click_pending_handle: bool, 170 | /// We use this for selecting features from the seq view 171 | dblclick_pending_handle: bool, 172 | cloning_insert: CloningInsertData, 173 | /// Volatile; computed dynamically based on window size. 174 | nt_chars_per_row: usize, 175 | search_input: String, 176 | /// Used to trigger a search focus on hitting ctrl+f 177 | highlight_search_input: bool, 178 | /// Activated when the user selects the search box; disables character insertion. 179 | text_edit_active: bool, 180 | /// This is used for selecting nucleotides on the sequence viewer. 181 | dragging: bool, 182 | /// 1-based indexing. 183 | text_selection: Option, 184 | quick_feature_add_name: String, 185 | quick_feature_add_dir: FeatureDirection, 186 | // todo: Protein ui A/R 187 | aa_ident_disp: AaIdent, 188 | pdb_error_received: bool, 189 | re: ReUi, 190 | backbone_filters: BackboneFilters, 191 | seq_edit_lock: bool, 192 | ab1_start_i: usize, 193 | } 194 | 195 | impl Default for StateUi { 196 | fn default() -> Self { 197 | Self { 198 | page: Default::default(), 199 | page_seq: Default::default(), 200 | page_seq_top: Default::default(), 201 | seq_input: Default::default(), 202 | pcr: Default::default(), 203 | feature_add: Default::default(), 204 | feature_hover: Default::default(), 205 | selected_item: Default::default(), 206 | seq_visibility: Default::default(), 207 | hide_map_feature_editor: true, 208 | cursor_pos: Default::default(), 209 | cursor_seq_i: Default::default(), 210 | file_dialogs: Default::default(), 211 | show_origin_change: Default::default(), 212 | new_origin: Default::default(), 213 | text_cursor_i: Some(0), 214 | click_pending_handle: Default::default(), 215 | dblclick_pending_handle: Default::default(), 216 | cloning_insert: Default::default(), 217 | nt_chars_per_row: Default::default(), 218 | search_input: Default::default(), 219 | highlight_search_input: Default::default(), 220 | text_edit_active: Default::default(), 221 | dragging: Default::default(), 222 | text_selection: Default::default(), 223 | quick_feature_add_name: Default::default(), 224 | quick_feature_add_dir: Default::default(), 225 | aa_ident_disp: AaIdent::OneLetter, 226 | pdb_error_received: false, 227 | re: Default::default(), 228 | backbone_filters: Default::default(), 229 | seq_edit_lock: true, 230 | ab1_start_i: Default::default(), 231 | } 232 | } 233 | } 234 | 235 | #[derive(Clone, Copy, PartialEq, Debug, Encode, Decode)] 236 | pub enum Selection { 237 | Feature(usize), // index 238 | Primer(usize), 239 | None, 240 | } 241 | 242 | impl Default for Selection { 243 | fn default() -> Self { 244 | Self::None 245 | } 246 | } 247 | 248 | fn main() { 249 | let mut state = State::default(); 250 | 251 | // todo: Temp to test BAM 252 | // let am = alignment_map::import(&PathBuf::from_str("../../Desktop/test.bam").unwrap()).unwrap(); 253 | // println!("Alignment map loaded: {:?}", am); 254 | 255 | state.load_prefs(&PathBuf::from_str(DEFAULT_PREFS_FILE).unwrap()); 256 | 257 | // Initial load hierarchy: 258 | // - Path argument (e.g. file association) 259 | // - Last opened files 260 | // - Quicksave 261 | 262 | let mut loaded_from_arg = false; 263 | let (path, window_title_initial) = { 264 | let mut p = PathBuf::from_str(QUICKSAVE_FILE).unwrap(); 265 | 266 | // Windows and possibly other operating systems, if attempting to use your program to natively 267 | // open a file type, will use command line arguments to indicate this. Determine if the program 268 | // is being launched this way, and if so, open the file. 269 | let args: Vec = env::args().collect(); 270 | if args.len() > 1 { 271 | let temp = &args[1]; 272 | p = PathBuf::from_str(temp).unwrap(); 273 | loaded_from_arg = true; 274 | } 275 | 276 | let window_title = get_window_title(&p); 277 | (p, window_title) 278 | }; 279 | 280 | let mut prev_paths_loaded = false; 281 | for tab in &state.tabs_open { 282 | if tab.path.is_some() { 283 | prev_paths_loaded = true; 284 | } 285 | } 286 | 287 | // todo: Consider a standalone method for loaded-from-arg, 288 | 289 | // Load from the argument or quicksave A/R. 290 | if loaded_from_arg || !prev_paths_loaded { 291 | println!("Loading from quicksave or arg: {:?}", path); 292 | if let Some(loaded) = load_import(&path) { 293 | state.load(&loaded); 294 | } 295 | } 296 | 297 | state.sync_seq_related(None); 298 | 299 | state.reset_selections(); 300 | 301 | let icon_bytes: &[u8] = include_bytes!("resources/icon.png"); 302 | let icon_data = eframe::icon_data::from_png_bytes(icon_bytes); 303 | 304 | let options = eframe::NativeOptions { 305 | viewport: egui::ViewportBuilder::default() 306 | .with_inner_size([WINDOW_WIDTH, WINDOW_HEIGHT]) 307 | .with_icon(icon_data.unwrap()), 308 | ..Default::default() 309 | }; 310 | 311 | eframe::run_native( 312 | &window_title_initial, 313 | options, 314 | Box::new(|_cc| Ok(Box::new(state))), 315 | ) 316 | .unwrap(); 317 | } 318 | -------------------------------------------------------------------------------- /src/misc_types.rs: -------------------------------------------------------------------------------- 1 | //! This module contains fundamental data structures, eg related to features, metadata, etc. 2 | 3 | use bincode::{Decode, Encode}; 4 | use na_seq::Nucleotide; 5 | 6 | use crate::{ 7 | Color, 8 | primer::PrimerDirection, 9 | util::{RangeIncl, match_subseq}, 10 | }; 11 | pub const MIN_SEARCH_LEN: usize = 3; 12 | 13 | #[derive(Clone, Copy, PartialEq, Encode, Decode)] 14 | pub enum FeatureType { 15 | Generic, 16 | Gene, 17 | Ori, 18 | RibosomeBindSite, 19 | Promoter, 20 | AntibioticResistance, 21 | /// Note: This one behaves a bit different from the others; we use it here so we can share the feature 22 | /// overlay code. 23 | Primer, 24 | /// Ie, a gene. 25 | CodingRegion, 26 | LongTerminalRepeat, 27 | /// We don't draw these on the map or sequence views; found in GenBank formats (at least), these 28 | /// are the range of the entire sequence. 29 | Source, 30 | Exon, 31 | Transcript, 32 | /// Like Primer, this is not a real feature; we use it to draw the selection highlighted area. 33 | Selection, 34 | /// Ie operators. 35 | ProteinBind, 36 | Terminator, 37 | } 38 | 39 | impl Default for FeatureType { 40 | fn default() -> Self { 41 | Self::Generic 42 | } 43 | } 44 | 45 | impl FeatureType { 46 | /// For displaying in the UI 47 | pub fn to_string(self) -> String { 48 | match self { 49 | Self::Generic => "Generic", 50 | Self::Gene => "Gene", 51 | Self::Ori => "Origin of replication", 52 | Self::RibosomeBindSite => "Ribosome bind site", 53 | Self::Promoter => "Promoter", 54 | Self::AntibioticResistance => "Antibiotic resistance", 55 | Self::Primer => "Primer", 56 | Self::CodingRegion => "Coding region", 57 | Self::LongTerminalRepeat => "Long term repeat", 58 | Self::Source => "Source", 59 | Self::Exon => "Exon", 60 | Self::Transcript => "Transcript", 61 | Self::Selection => "", 62 | Self::ProteinBind => "Operator", 63 | Self::Terminator => "Terminator", 64 | } 65 | .to_owned() 66 | } 67 | 68 | pub fn color(&self) -> Color { 69 | match self { 70 | Self::Generic => (255, 0, 255), 71 | Self::Gene => (255, 128, 128), 72 | Self::Ori => (40, 200, 128), 73 | Self::RibosomeBindSite => (255, 204, 252), 74 | Self::Promoter => (240, 190, 70), 75 | Self::AntibioticResistance => (0, 200, 110), 76 | Self::Primer => (0, 0, 0), // N/A for now at least. 77 | Self::CodingRegion => (100, 200, 255), // N/A for now at least. 78 | Self::LongTerminalRepeat => (150, 200, 255), // N/A for now at least. 79 | Self::Source => (120, 70, 120), 80 | Self::Exon => (255, 255, 180), 81 | Self::Transcript => (180, 255, 180), 82 | Self::Selection => (255, 255, 0), 83 | Self::ProteinBind => (128, 110, 150), 84 | Self::Terminator => (255, 110, 150), 85 | } 86 | } 87 | 88 | /// Parse from a string; we use this for both SnapGene and GenBank. 89 | pub fn from_external_str(v: &str) -> Self { 90 | // todo: Update as required with more 91 | let v = &v.to_lowercase(); 92 | 93 | match v.as_ref() { 94 | "cds" => Self::CodingRegion, 95 | "gene" => Self::Gene, 96 | "rbs" => Self::RibosomeBindSite, 97 | "rep_origin" => Self::Ori, 98 | "promoter" => Self::Promoter, 99 | "primer_bind" => Self::Primer, // todo: This is a bit awk; genbank. 100 | "ltr" => Self::LongTerminalRepeat, 101 | "misc_feature" => Self::Generic, 102 | "source" => Self::Source, 103 | "exon" => Self::Exon, 104 | "transcript" => Self::Transcript, 105 | "protein_bind" => Self::ProteinBind, 106 | "terminator" => Self::Terminator, 107 | _ => Self::Generic, 108 | } 109 | } 110 | 111 | /// Create a string for use with SnapGene and GenBank formats. 112 | pub fn to_external_str(self) -> String { 113 | // todo: Update as required with more 114 | match self { 115 | Self::Generic => "misc_feature", 116 | Self::Gene => "gene", 117 | Self::Ori => "rep_origin", 118 | Self::RibosomeBindSite => "rbs", 119 | Self::Promoter => "promoter", 120 | Self::AntibioticResistance => "antibiotic resistance", // todo 121 | Self::Primer => "primer_bind", 122 | Self::CodingRegion => "cds", 123 | Self::LongTerminalRepeat => "ltr", 124 | Self::Source => "source", 125 | Self::Exon => "exon", 126 | Self::Transcript => "transcript", 127 | Self::Selection => "", 128 | Self::ProteinBind => "protein_bind", 129 | Self::Terminator => "terminator", 130 | } 131 | .to_string() 132 | } 133 | } 134 | 135 | #[derive(Clone, Copy, PartialEq, Encode, Decode)] 136 | pub enum FeatureDirection { 137 | None, 138 | Forward, 139 | Reverse, 140 | } 141 | 142 | impl From for FeatureDirection { 143 | fn from(value: PrimerDirection) -> Self { 144 | match value { 145 | PrimerDirection::Forward => Self::Forward, 146 | PrimerDirection::Reverse => Self::Reverse, 147 | } 148 | } 149 | } 150 | 151 | impl Default for FeatureDirection { 152 | fn default() -> Self { 153 | Self::None 154 | } 155 | } 156 | 157 | impl FeatureDirection { 158 | pub fn to_string(self) -> String { 159 | match self { 160 | Self::None => "None", 161 | Self::Forward => "Forward", 162 | Self::Reverse => "Reverse", 163 | } 164 | .to_owned() 165 | } 166 | } 167 | 168 | #[derive(Clone, Encode, Decode)] 169 | pub struct Feature { 170 | // pub range: (usize, usize), 171 | /// 1-based indexing, inclusive. (Note: Could also use the builtin RangeInclusive.) 172 | pub range: RangeIncl, 173 | pub feature_type: FeatureType, 174 | pub direction: FeatureDirection, 175 | pub label: String, 176 | /// By default, we display features using featuretype-specific color. Allow the user 177 | /// to override this. 178 | pub color_override: Option, 179 | pub notes: Vec<(String, String)>, 180 | } 181 | 182 | impl Default for Feature { 183 | fn default() -> Self { 184 | Self { 185 | range: RangeIncl::new(1, 1), 186 | feature_type: Default::default(), 187 | direction: Default::default(), 188 | label: Default::default(), 189 | color_override: Default::default(), 190 | notes: Default::default(), 191 | } 192 | } 193 | } 194 | 195 | impl Feature { 196 | pub fn label(&self) -> String { 197 | if self.label.is_empty() { 198 | self.feature_type.to_string() 199 | } else { 200 | self.label.clone() 201 | } 202 | } 203 | 204 | /// Get the color to draw; type color, unless overridden. 205 | pub fn color(&self) -> Color { 206 | match self.color_override { 207 | Some(c) => c, 208 | None => self.feature_type.color(), 209 | } 210 | } 211 | 212 | /// Get the feature len, in usize. 213 | pub fn len(&self, seq_len: usize) -> usize { 214 | if self.range.end > self.range.start { 215 | self.range.end - self.range.start + 1 216 | } else { 217 | // ie a wrap through the origin 218 | self.range.end + seq_len - self.range.start + 1 219 | } 220 | } 221 | 222 | /// Formats the indexes, and size of this feature. 223 | pub fn location_descrip(&self, seq_len: usize) -> String { 224 | format!( 225 | "{}..{} {} bp", 226 | self.range.start, 227 | self.range.end, 228 | self.len(seq_len) 229 | ) 230 | } 231 | } 232 | 233 | /// Contains sequence-level metadata. 234 | #[derive(Clone, Default, Encode, Decode)] 235 | pub struct Metadata { 236 | pub plasmid_name: String, 237 | pub comments: Vec, 238 | pub references: Vec, 239 | pub locus: String, 240 | // pub date: Option, 241 | /// We use this to prevent manual encode/decode for the whoel struct. 242 | pub date: Option<(i32, u8, u8)>, // Y M D 243 | pub definition: Option, 244 | pub molecule_type: Option, 245 | pub division: String, 246 | pub accession: Option, 247 | pub version: Option, 248 | // pub keywords: Vec, 249 | pub keywords: Option, // todo vec? 250 | pub source: Option, 251 | pub organism: Option, 252 | } 253 | 254 | /// Based on GenBank's reference format 255 | #[derive(Default, Clone, Encode, Decode)] 256 | pub struct Reference { 257 | pub description: String, 258 | pub authors: Option, 259 | pub consortium: Option, 260 | pub title: String, 261 | pub journal: Option, 262 | pub pubmed: Option, 263 | pub remark: Option, 264 | } 265 | 266 | pub struct SearchMatch { 267 | /// 0-based indexing. 268 | pub range: RangeIncl, 269 | // todo: More A/R 270 | } 271 | 272 | // todo: Should this go to the `seq` library? 273 | /// Find exact matches in the target sequence of our search nucleotides. 274 | /// todo: Optionally support partial matches. 275 | /// todo: 276 | pub fn find_search_matches(seq: &[Nucleotide], search_seq: &[Nucleotide]) -> Vec { 277 | let (mut fwd, mut rev) = match_subseq(search_seq, seq); 278 | 279 | fwd.append(&mut rev); 280 | fwd.into_iter().map(|range| SearchMatch { range }).collect() 281 | } 282 | -------------------------------------------------------------------------------- /src/pcr.rs: -------------------------------------------------------------------------------- 1 | //! This module assists in identifying PCR parameters 2 | 3 | use bincode::{Decode, Encode}; 4 | use na_seq::Seq; 5 | 6 | use crate::{ 7 | gui::navigation::{Page, PageSeq, Tab}, 8 | primer::{Primer, TM_TARGET}, 9 | state::State, 10 | util::RangeIncl, 11 | }; 12 | 13 | #[derive(Clone, Encode, Decode)] 14 | /// Variables for UI fields, for determining PCR parameters. 15 | pub struct PcrUi { 16 | pub primer_tm: f32, 17 | pub product_len: usize, 18 | pub polymerase_type: PolymeraseType, 19 | pub num_cycles: u16, 20 | /// Index from primer data for the load-from-primer system. For storing dropdown state. 21 | pub primer_selected: usize, 22 | /// These are for the PCR product generation 23 | pub primer_fwd: usize, 24 | pub primer_rev: usize, 25 | } 26 | 27 | impl Default for PcrUi { 28 | fn default() -> Self { 29 | Self { 30 | primer_tm: TM_TARGET, 31 | product_len: 1_000, 32 | polymerase_type: Default::default(), 33 | num_cycles: 30, 34 | primer_selected: 0, 35 | primer_fwd: 0, 36 | primer_rev: 0, 37 | } 38 | } 39 | } 40 | 41 | /// This is a common pattern for PCR parameters 42 | #[derive(Default, Encode, Decode)] 43 | pub struct TempTime { 44 | /// In °C 45 | pub temp: f32, 46 | /// In seconds 47 | pub time: u16, 48 | } 49 | 50 | impl TempTime { 51 | pub fn new(temp: f32, time: u16) -> Self { 52 | Self { temp, time } 53 | } 54 | } 55 | 56 | #[derive(Clone, Copy, PartialEq, Encode, Decode)] 57 | pub enum PolymeraseType { 58 | NormalFidelity, 59 | /// Eg Phusion; results in a shorter extension time. 60 | HighFidelity, 61 | } 62 | 63 | impl Default for PolymeraseType { 64 | fn default() -> Self { 65 | Self::NormalFidelity 66 | } 67 | } 68 | 69 | impl PolymeraseType { 70 | pub fn extension_time(&self, product_len: usize) -> u16 { 71 | match self { 72 | Self::NormalFidelity => (60 * product_len / 1_000) as u16, 73 | // 15 - 30. 15 recommended in FastCloning guide. PHusion manual: 15-30s/kb. 74 | Self::HighFidelity => (15 * product_len / 1_000) as u16, // 75 | } 76 | } 77 | 78 | pub fn denaturation(&self) -> TempTime { 79 | match self { 80 | Self::NormalFidelity => TempTime::new(94., 30), 81 | Self::HighFidelity => TempTime::new(98., 10), // pHusion manual: 5-10s. 82 | } 83 | } 84 | 85 | pub fn denaturation_initial(&self) -> TempTime { 86 | match self { 87 | Self::NormalFidelity => TempTime::new(94., 120), 88 | Self::HighFidelity => TempTime::new(98., 30), 89 | } 90 | } 91 | 92 | pub fn to_str(self) -> String { 93 | match self { 94 | Self::NormalFidelity => "Normal fidelity", 95 | Self::HighFidelity => "High fidelity (eg Phusion)", 96 | } 97 | .to_owned() 98 | } 99 | } 100 | 101 | #[derive(Default, Encode, Decode)] 102 | pub struct PcrParams { 103 | pub initial_denaturation: TempTime, 104 | pub denaturation: TempTime, 105 | pub annealing: TempTime, 106 | pub extension: TempTime, 107 | pub final_extension: TempTime, 108 | pub num_cycles: u16, 109 | } 110 | 111 | impl PcrParams { 112 | pub fn new(data: &PcrUi) -> Self { 113 | Self { 114 | initial_denaturation: data.polymerase_type.denaturation_initial(), 115 | denaturation: data.polymerase_type.denaturation(), 116 | // Alternative: Ta = 0.3 x Tm(primer) + 0.7 Tm(product) – 14.9. 117 | // 15-60s. How do we choose?. Phusion manual: 10-30s. 118 | annealing: TempTime::new(data.primer_tm - 5., 30), 119 | // 72 is good if Taq, and Phusion. 120 | extension: TempTime::new(72., data.polymerase_type.extension_time(data.product_len)), 121 | // Alternatively: 5-10 mins? Perhaps 30s per 1kb?) Phusion manual: 5-10m. 122 | final_extension: TempTime::new(72., 60 * 6), 123 | num_cycles: data.num_cycles, 124 | } 125 | } 126 | } 127 | 128 | /// Create a new tab containing of the PCR amplicon. 129 | pub fn make_amplicon_tab( 130 | state: &mut State, 131 | product_seq: Seq, 132 | range: RangeIncl, 133 | fwd_primer: Primer, 134 | rev_primer: Primer, 135 | ) { 136 | let mut product_features = Vec::new(); 137 | for feature in &state.generic[state.active].features { 138 | // todo: Handle circle wraps etc. 139 | if range.start < feature.range.start && range.end > feature.range.end { 140 | let mut product_feature = feature.clone(); 141 | // Find the indexes in the new product sequence. 142 | product_feature.range.start -= range.start - 1; 143 | product_feature.range.end -= range.start - 1; 144 | 145 | product_features.push(product_feature); 146 | } 147 | } 148 | 149 | state.add_tab(); 150 | state.tabs_open.push(Default::default()); 151 | 152 | // if let Some(seq) = range_combined.index_seq(&state.generic.seq) { 153 | state.generic[state.active].seq = product_seq; 154 | 155 | // Include the primers used for PCR, and features that are included in the new segment. 156 | // note that the feature indexes will need to change. 157 | 158 | let product_primers = vec![fwd_primer, rev_primer]; 159 | 160 | state.generic[state.active].features = product_features; 161 | state.generic[state.active].primers = product_primers; 162 | state.generic[state.active].metadata.plasmid_name = "PCR amplicon".to_owned(); 163 | 164 | state.sync_seq_related(None); 165 | // state.sync_primer_metrics(); 166 | 167 | state.ui.page = Page::Sequence; 168 | state.ui.page_seq = PageSeq::View; 169 | } 170 | -------------------------------------------------------------------------------- /src/portions.rs: -------------------------------------------------------------------------------- 1 | //! Code related to mixing portions. Used for performing quick mixing volume calculations. 2 | 3 | // todo: Consider including pKa info, and including tools to balance pH. 4 | 5 | use std::fmt::Display; 6 | 7 | use bincode::{Decode, Encode}; 8 | 9 | #[derive(Clone, Encode, Decode)] 10 | pub struct PortionsState { 11 | pub solutions: Vec, 12 | pub media_input: MediaPrepInput, 13 | pub media_result: MediaPrep, 14 | } 15 | 16 | impl Default for PortionsState { 17 | fn default() -> Self { 18 | let media_input = MediaPrepInput::default(); 19 | let media_result = media_prep(&media_input); 20 | 21 | let mut result = Self { 22 | solutions: Vec::new(), 23 | media_input, 24 | media_result, 25 | }; 26 | 27 | result 28 | } 29 | } 30 | 31 | #[derive(Default, Clone, Encode, Decode)] 32 | pub struct Solution { 33 | pub name: String, 34 | /// Liters 35 | pub total_volume: f32, 36 | pub reagents: Vec, 37 | pub sub_solns: Vec, 38 | /// Volatile; not to be added to directly. 39 | pub reagents_sub_solns: Vec, 40 | } 41 | 42 | impl Solution { 43 | /// Find required amounts (mass or volume) for each reagent. 44 | pub fn calc_amounts(&mut self) { 45 | self.reagents_sub_solns = Vec::new(); 46 | for sub_sol in &self.sub_solns { 47 | for reagent in &sub_sol.reagents { 48 | self.reagents_sub_solns.push(reagent.clone()); 49 | } 50 | } 51 | 52 | for reagent in &mut self.reagents { 53 | reagent.calc_amount(self.total_volume); 54 | } 55 | } 56 | } 57 | 58 | #[derive(Clone, Copy, Encode, Decode)] 59 | pub enum AmountCalculated { 60 | /// grams 61 | Mass(f32), 62 | /// Liters 63 | Volume(f32), 64 | } 65 | 66 | impl Display for AmountCalculated { 67 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 68 | let str = match self { 69 | Self::Mass(v) => { 70 | if *v > 1. { 71 | format!("{:.2} g", v) 72 | } else if *v >= 0.001 { 73 | format!("{:.2} mg", v * 1_000.) 74 | } else { 75 | format!("{:.2} μg", v * 1_000_000.) 76 | } 77 | } 78 | // todo: Be careful about this calc being called frequently. 79 | Self::Volume(v) => { 80 | if *v > 1. { 81 | format!("{:.2} L", v) 82 | } else if *v >= 0.001 { 83 | format!("{:.2} mL", v * 1_000.) 84 | } else { 85 | // todo: If you have precision issues, make your base unit mL. 86 | format!("{:.2} μL", v * 1_000_000.) 87 | } 88 | } 89 | }; 90 | 91 | write!(f, "{}", str) 92 | } 93 | } 94 | 95 | #[derive(Clone, Copy, PartialEq, Encode, Decode)] 96 | pub enum ReagentPrep { 97 | Mass, 98 | /// Inner: Molarity 99 | Volume(f32), 100 | } 101 | 102 | impl Display for ReagentPrep { 103 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 104 | let str = match self { 105 | Self::Mass => "Mass".to_owned(), 106 | // Self::Volume(molarity) => format!("Volume. Molarity: {molarity})"), 107 | Self::Volume(_molarity) => "Volume".to_owned(), 108 | }; 109 | write!(f, "{}", str) 110 | } 111 | } 112 | 113 | #[derive(Clone, Encode, Decode)] 114 | pub struct Reagent { 115 | pub type_: ReagentType, 116 | pub prep: ReagentPrep, 117 | /// Target moles per liter in the solution 118 | pub molarity: f32, 119 | /// Calculated result 120 | pub amount_calc: AmountCalculated, 121 | } 122 | 123 | impl Default for Reagent { 124 | fn default() -> Self { 125 | Self { 126 | type_: ReagentType::Custom(0.), 127 | prep: ReagentPrep::Mass, 128 | molarity: 0., 129 | amount_calc: AmountCalculated::Mass(0.), 130 | } 131 | } 132 | } 133 | 134 | impl Reagent { 135 | pub fn calc_amount(&mut self, total_volume: f32) { 136 | // mol = mol/L x L 137 | let moles_req = self.molarity * total_volume; 138 | 139 | self.amount_calc = match self.prep { 140 | // g = g/mol x mol 141 | ReagentPrep::Mass => AmountCalculated::Mass(self.type_.weight() * moles_req), 142 | // L = mol / mol/L: 143 | ReagentPrep::Volume(reagent_molarity) => { 144 | if reagent_molarity.abs() < 0.00000001 { 145 | AmountCalculated::Volume(0.) 146 | } else { 147 | AmountCalculated::Volume(moles_req / reagent_molarity) 148 | } 149 | } 150 | }; 151 | } 152 | } 153 | 154 | /// A collection of common reagents, where we have molecular weights. 155 | #[derive(Clone, Copy, PartialEq, Encode, Decode)] 156 | pub enum ReagentType { 157 | Custom(f32), // Inner: Molecular weight 158 | /// Index of the solution in state. 159 | Solution(usize), 160 | SodiumChloride, 161 | SodiumPhosphateMonobasic, 162 | SodiumPhosphateDibasic, 163 | SodiumPhosphateDibasicHeptahydrate, 164 | PotassiumPhosphateMonobasic, 165 | PotassiumPhosphateDibasic, 166 | TrisHcl, 167 | Iptg, 168 | Imidazole, 169 | Lysozyme, 170 | Mes, 171 | Bes, 172 | Tes, 173 | CitricAcid, 174 | Edta, 175 | HydrochloricAcid, 176 | // AceticAcid, 177 | SodiumHydroxide, 178 | BromophenolBlue, 179 | Dtt, 180 | MagnesiumChloride, 181 | Glycine, 182 | Sds, 183 | Tris, 184 | } 185 | 186 | impl ReagentType { 187 | /// g/mol 188 | pub fn weight(&self) -> f32 { 189 | match self { 190 | Self::Custom(weight) => *weight, 191 | Self::Solution(_) => 0., // todo? 192 | Self::SodiumChloride => 58.44, 193 | Self::SodiumPhosphateMonobasic => 119.98, 194 | Self::SodiumPhosphateDibasic => 141.96, 195 | Self::SodiumPhosphateDibasicHeptahydrate => 268.10, 196 | Self::PotassiumPhosphateMonobasic => 136.08, 197 | Self::PotassiumPhosphateDibasic => 174.17, 198 | Self::TrisHcl => 121.14, 199 | Self::Iptg => 238.298, 200 | Self::Imidazole => 68.08, 201 | Self::Lysozyme => 14_388., 202 | Self::Mes => 195.24, 203 | Self::Bes => 213.25, 204 | Self::Tes => 229.25, 205 | Self::CitricAcid => 192.12, 206 | Self::Edta => 292.24, 207 | Self::HydrochloricAcid => 36.46, 208 | Self::SodiumHydroxide => 40., 209 | Self::BromophenolBlue => 669.96, 210 | Self::Dtt => 154.25, 211 | Self::MagnesiumChloride => 95.211, 212 | Self::Glycine => 75.07, 213 | Self::Sds => 288.5, 214 | Self::Tris => 121.14, 215 | } 216 | } 217 | } 218 | 219 | impl Display for ReagentType { 220 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 221 | let str = match self { 222 | Self::Custom(_) => "Custom".to_owned(), 223 | Self::Solution(_) => "Solution".to_owned(), // todo 224 | Self::SodiumChloride => "NaCl".to_owned(), 225 | Self::SodiumPhosphateMonobasic => "NaH₂PO₄ (Mono)".to_owned(), 226 | Self::SodiumPhosphateDibasic => "Na₂HPO₄ (Di)".to_owned(), 227 | Self::SodiumPhosphateDibasicHeptahydrate => "Na₂HPO₄·7H₂O".to_owned(), 228 | Self::PotassiumPhosphateMonobasic => "H₂KO₄P".to_owned(), 229 | Self::PotassiumPhosphateDibasic => "HK₂O₄P".to_owned(), 230 | Self::TrisHcl => "Tris-HCl".to_owned(), 231 | Self::Iptg => "IPTG".to_owned(), 232 | Self::Imidazole => "Imidazole".to_owned(), 233 | Self::Lysozyme => "Lysozyme".to_owned(), 234 | Self::Mes => "MES".to_owned(), 235 | Self::Bes => "BES".to_owned(), 236 | Self::Tes => "TES".to_owned(), 237 | Self::CitricAcid => "Citric acid".to_owned(), 238 | Self::Edta => "EDTA".to_owned(), 239 | Self::HydrochloricAcid => "HCl".to_owned(), 240 | Self::SodiumHydroxide => "NaOH".to_owned(), 241 | Self::BromophenolBlue => "Bromophenol blue".to_owned(), 242 | Self::Dtt => "DTT".to_owned(), 243 | Self::MagnesiumChloride => "MgCl₂".to_owned(), 244 | Self::Sds => "SDS".to_owned(), 245 | Self::Glycine => "Glycine".to_owned(), 246 | Self::Tris => "Tris".to_owned(), 247 | }; 248 | 249 | write!(f, "{}", str) 250 | } 251 | } 252 | 253 | #[derive(Clone, Copy, PartialEq, Encode, Decode)] 254 | pub enum PlateSize { 255 | /// 60mm diameter 256 | D60, 257 | D90, 258 | D100, 259 | D150, 260 | } 261 | 262 | impl PlateSize { 263 | /// Nominal amount of liquid. Note that these go with the square of the baseline. We use 7mL for 60mm 264 | /// plates as the baseline. 265 | pub fn volume(&self) -> f32 { 266 | match self { 267 | Self::D60 => 0.007, 268 | Self::D90 => 0.01575, 269 | Self::D100 => 0.01944, 270 | Self::D150 => 0.04375, 271 | } 272 | } 273 | } 274 | 275 | impl Display for PlateSize { 276 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 277 | let str = match self { 278 | Self::D60 => "60mm", 279 | Self::D90 => "90mm", 280 | Self::D100 => "100mm", 281 | Self::D150 => "150mm", 282 | }; 283 | 284 | write!(f, "{}", str) 285 | } 286 | } 287 | 288 | #[derive(Clone, PartialEq, Encode, Decode)] 289 | pub enum MediaPrepInput { 290 | Plates((PlateSize, usize)), // number of plates, 291 | Liquid(f32), // volume 292 | } 293 | 294 | impl Display for MediaPrepInput { 295 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 296 | let str = match self { 297 | Self::Plates(_) => "Plates", 298 | Self::Liquid(_) => "Liquid culture", 299 | }; 300 | 301 | write!(f, "{}", str) 302 | } 303 | } 304 | 305 | impl Default for MediaPrepInput { 306 | fn default() -> Self { 307 | Self::Plates((PlateSize::D90, 6)) 308 | } 309 | } 310 | 311 | #[derive(Clone, Default, Encode, Decode, Debug)] 312 | pub struct MediaPrep { 313 | pub water: f32, // L 314 | pub food: f32, // g 315 | pub agar: f32, // g 316 | pub antibiotic: f32, // mL 317 | } 318 | 319 | /// Returns volume of water, grams of LB, grams of agar, mL of 1000x antibiotics. 320 | pub fn media_prep(input: &MediaPrepInput) -> MediaPrep { 321 | let (volume, agar) = match input { 322 | MediaPrepInput::Plates((plate_size, num)) => { 323 | let volume = plate_size.volume() * *num as f32; 324 | (volume, volume * 15.) 325 | } 326 | MediaPrepInput::Liquid(volume) => (*volume, 0.), 327 | }; 328 | 329 | MediaPrep { 330 | water: volume, 331 | food: volume * 25., 332 | agar, 333 | antibiotic: volume, 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /src/protein.rs: -------------------------------------------------------------------------------- 1 | //! Data related to proteins, eg derived from coding region features, in conjunction with 2 | //! reading frames. 3 | 4 | use bincode::{Decode, Encode}; 5 | use bio_apis::rcsb::PdbData; 6 | use na_seq::{ 7 | Nucleotide, 8 | amino_acids::{AminoAcid, CodingResult}, 9 | }; 10 | 11 | use crate::{ 12 | misc_types::{Feature, FeatureType}, 13 | reading_frame::{ReadingFrame, ReadingFrameMatch, find_orf_matches}, 14 | state::State, 15 | }; 16 | 17 | pub const WATER_WEIGHT: f32 = 18.015; // g/mol. We subtract these when calculating a protein's weight. 18 | 19 | // todo: Adjust A/R. Ideally, let the user customize it. 20 | pub const HYDROPATHY_WINDOW_SIZE: usize = 9; 21 | 22 | #[derive(Encode, Decode)] 23 | pub struct Protein { 24 | pub feature: Feature, // todo: Consider how you want to do this. Index? Only used for creation? 25 | pub reading_frame_match: ReadingFrameMatch, 26 | pub aa_seq: Vec, 27 | pub aa_seq_precoding: Vec, 28 | pub aa_seq_postcoding: Vec, 29 | pub weight: f32, 30 | pub weight_with_prepost: f32, 31 | // window center, value. 32 | /// This type is for storing graphable data used by egui_plot. 33 | // pub hydropath_data: Vec<[f64; 2]>, 34 | pub hydropath_data: Vec<(usize, f32)>, 35 | pub pdb_data: Vec, 36 | // Note: This is more of a UI functionality; here for now. 37 | pub show_hydropath: bool, 38 | } 39 | 40 | pub fn proteins_from_seq( 41 | seq: &[Nucleotide], 42 | features: &[Feature], 43 | cr_orf_matches: &[(usize, ReadingFrameMatch)], 44 | ) -> Vec { 45 | let mut result = Vec::new(); 46 | for (i, feature) in features.iter().enumerate() { 47 | if feature.feature_type != FeatureType::CodingRegion { 48 | continue; 49 | } 50 | for (j, om) in cr_orf_matches { 51 | if *j == i { 52 | if let Some(seq_orf_match_dna) = om.range.index_seq(seq) { 53 | // todo: DRy with feature_db_load. 54 | let len = seq_orf_match_dna.len(); 55 | 56 | let mut aa_seq = Vec::new(); 57 | // We also render AAs included in the reading frame, but not specified in the coding region feature. 58 | let mut aa_seq_precoding = Vec::new(); 59 | let mut aa_seq_postcoding = Vec::new(); 60 | 61 | for i_ in 0..len / 3 { 62 | let i = i_ * 3; // The ORF-modified sequence index. 63 | let i_actual = i + om.range.start; 64 | 65 | let nts = &seq_orf_match_dna[i..i + 3]; 66 | 67 | // Note: We are ignoring stop codons here. 68 | if let CodingResult::AminoAcid(aa) = 69 | AminoAcid::from_codons(nts.try_into().unwrap()) 70 | { 71 | if i_actual < feature.range.start { 72 | aa_seq_precoding.push(aa); 73 | } else if i_actual > feature.range.end { 74 | aa_seq_postcoding.push(aa); 75 | } else { 76 | aa_seq.push(aa); 77 | } 78 | } 79 | } 80 | 81 | // todo: Def cache these weights. 82 | let weight = protein_weight(&aa_seq); 83 | let weight_with_prepost = weight 84 | + protein_weight(&aa_seq_precoding) 85 | + protein_weight(&aa_seq_postcoding); 86 | 87 | let hydropath_data = hydropathy_doolittle(&aa_seq, HYDROPATHY_WINDOW_SIZE); 88 | // Convert to a format accepted by our plotting lib. 89 | // let mut hydropath_data = Vec::new(); 90 | // for (i, val) in hydropath_data_ { 91 | // hydropath_data.push([i as f64, val as f64]); 92 | // } 93 | 94 | result.push(Protein { 95 | feature: feature.clone(), 96 | reading_frame_match: om.clone(), 97 | aa_seq, 98 | aa_seq_precoding, 99 | aa_seq_postcoding, 100 | hydropath_data, 101 | weight, 102 | weight_with_prepost, 103 | show_hydropath: true, 104 | pdb_data: Vec::new(), 105 | }) 106 | } 107 | } 108 | } 109 | } 110 | result 111 | } 112 | 113 | /// Calculates the Kyte-Doolittle value of Hydropathy index. Uses per-AA 114 | /// experimentally-determined hydrophobicity values, averaged over a moving window. 115 | /// 116 | /// https://web.expasy.org/protscale/ 117 | pub fn hydropathy_doolittle(seq: &[AminoAcid], window_size: usize) -> Vec<(usize, f32)> { 118 | if window_size % 2 == 0 { 119 | eprintln!("Window size for KD must be odd"); 120 | return Vec::new(); 121 | } 122 | let mut result = Vec::new(); 123 | 124 | let win_div_2 = window_size / 2; // Rounds down. 125 | 126 | if win_div_2 - 1 >= seq.len() { 127 | eprintln!("Error with window size for hydropathy"); 128 | return result; 129 | } 130 | 131 | // We'll center each window on `i`. 132 | for i in win_div_2..seq.len() - win_div_2 - 1 { 133 | let aas = &seq[i - win_div_2..i + win_div_2 + 1]; 134 | let mut val_this_window = 0.; 135 | for aa in aas { 136 | val_this_window += aa.hydropathicity(); 137 | } 138 | result.push((i, val_this_window / window_size as f32)); 139 | } 140 | 141 | result 142 | } 143 | 144 | /// The TANGO algorithm predicts beta-sheet formation propensity, which is associated with aggregation. 145 | /// It considers factors like amino acid sequence, solvent accessibility, and secondary structure. 146 | pub fn tango_aggregation(seq: &[AminoAcid]) {} 147 | 148 | /// Predicts aggregation-prone regionis by calculating an aggregation score for each AA. 149 | /// todo: Look this up. 150 | pub fn aggrescan(seq: &[AminoAcid]) {} 151 | 152 | // todo: Look up SLIDER aggregation tool as well. 153 | 154 | // todo: Visualize hydrophobic and aggreg-prone regions. 155 | 156 | // todo: Move this somewhere more appropriate, then store in state_volatile. 157 | /// Find the most likely reading frame for each coding region. 158 | pub fn sync_cr_orf_matches(state: &mut State) { 159 | state.volatile[state.active].cr_orf_matches = Vec::new(); 160 | 161 | // These matches are for all frames. 162 | let mut region_matches = Vec::new(); 163 | 164 | for orf in [ 165 | ReadingFrame::Fwd0, 166 | ReadingFrame::Fwd1, 167 | ReadingFrame::Fwd2, 168 | ReadingFrame::Rev0, 169 | ReadingFrame::Rev1, 170 | ReadingFrame::Rev2, 171 | ] { 172 | let mut regions = find_orf_matches(state.get_seq(), orf); 173 | region_matches.append(&mut regions); 174 | } 175 | 176 | for (i, feature) in state.generic[state.active].features.iter().enumerate() { 177 | if feature.feature_type != FeatureType::CodingRegion { 178 | continue; 179 | } 180 | 181 | // Don't show binding tags. 182 | let label_lower = feature.label.to_lowercase().replace(' ', ""); 183 | if label_lower.contains("xhis") || label_lower.contains("×his") { 184 | continue; 185 | } 186 | 187 | // Find the best reading frame match, if there is one. 188 | let mut orf_match = None; 189 | let mut smallest_match = usize::MAX; 190 | for rm in ®ion_matches { 191 | if !rm.range.contains(feature.range.start) || !rm.range.contains(feature.range.end) { 192 | continue; 193 | } 194 | 195 | // If multiple matches contain the range, choose the smallest one. 196 | if rm.range.len() < smallest_match { 197 | smallest_match = rm.range.len(); 198 | orf_match = Some(rm.clone()); 199 | } 200 | } 201 | 202 | if let Some(m) = orf_match { 203 | state.volatile[state.active] 204 | .cr_orf_matches 205 | .push((i, m.clone())); 206 | } 207 | } 208 | } 209 | 210 | /// calculate protein weight in kDa. 211 | pub fn protein_weight(seq: &[AminoAcid]) -> f32 { 212 | if seq.is_empty() { 213 | return 0.; // Avoids underflow. 214 | } 215 | let mut result = 0.; 216 | for aa in seq { 217 | result += aa.weight(); 218 | } 219 | (result - WATER_WEIGHT * (seq.len() - 1) as f32) / 1_000. 220 | } 221 | -------------------------------------------------------------------------------- /src/reading_frame.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use bincode::{Decode, Encode}; 4 | use na_seq::{ 5 | Nucleotide, 6 | Nucleotide::{A, G, T}, 7 | Seq, seq_complement, 8 | }; 9 | 10 | use crate::util::RangeIncl; 11 | 12 | const START_CODON: [Nucleotide; 3] = [A, T, G]; 13 | pub const STOP_CODONS: [[Nucleotide; 3]; 3] = [[T, A, A], [T, A, G], [T, G, A]]; 14 | 15 | /// Of the 6 possible reading frames. 16 | #[derive(Clone, Copy, PartialEq, Debug, Encode, Decode)] 17 | pub enum ReadingFrame { 18 | /// Forward, with 0 offset (This pattern applies for all variants) 19 | Fwd0, 20 | Fwd1, 21 | Fwd2, 22 | Rev0, 23 | Rev1, 24 | Rev2, 25 | } 26 | 27 | impl ReadingFrame { 28 | pub fn offset(&self) -> usize { 29 | match self { 30 | Self::Fwd0 | Self::Rev0 => 0, 31 | Self::Fwd1 | Self::Rev1 => 1, 32 | Self::Fwd2 | Self::Rev2 => 2, 33 | } 34 | } 35 | 36 | pub fn is_reverse(&self) -> bool { 37 | // todo: Enum 38 | !matches!(self, Self::Fwd0 | Self::Fwd1 | Self::Fwd2) 39 | } 40 | 41 | /// Get a seqeuence of the full sequence in the appropriate direction, and with the appropriate offset. 42 | /// The result will always start in frame. It will be trimmed if offset > 0. 43 | /// todo: THis makes a clone. Can we instead do a slice? 44 | pub fn arrange_seq(&self, seq: &[Nucleotide]) -> Seq { 45 | let offset = self.offset(); 46 | 47 | if self.is_reverse() { 48 | seq_complement(seq)[offset..].to_vec() 49 | } else { 50 | seq[offset..].to_vec() 51 | } 52 | } 53 | } 54 | 55 | impl Default for ReadingFrame { 56 | fn default() -> Self { 57 | Self::Fwd0 58 | } 59 | } 60 | 61 | impl Display for ReadingFrame { 62 | /// For use with selector buttons. 63 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 64 | let str = match self { 65 | Self::Fwd0 => "Fwd 0", 66 | Self::Fwd1 => "Fwd 1", 67 | Self::Fwd2 => "Fwd 2", 68 | Self::Rev0 => "Rev 0", 69 | Self::Rev1 => "Rev 1", 70 | Self::Rev2 => "Rev 2", 71 | } 72 | .to_owned(); 73 | write!(f, "{}", str) 74 | } 75 | } 76 | 77 | #[derive(Debug, Clone, Encode, Decode)] 78 | pub struct ReadingFrameMatch { 79 | pub frame: ReadingFrame, 80 | /// Indices are respective to the non-complementary seq, for both forward and reverse reading frames. 81 | pub range: RangeIncl, 82 | } 83 | 84 | /// Find coding regions in a sequence, given a reading frame. 85 | pub fn find_orf_matches(seq: &[Nucleotide], orf: ReadingFrame) -> Vec { 86 | let mut result = Vec::new(); 87 | 88 | let offset = orf.offset(); 89 | let seq_len_full = seq.len(); 90 | 91 | if seq_len_full < 3 { 92 | return result; 93 | } 94 | 95 | let seq_ = orf.arrange_seq(&seq); 96 | let len = seq_.len(); 97 | 98 | let mut frame_open = None; // Inner: Start index. 99 | 100 | for i_ in 0..len / 3 { 101 | let i = i_ * 3; // The actual sequence index. 102 | 103 | let nts = &seq_[i..i + 3]; 104 | 105 | if frame_open.is_none() && nts == START_CODON { 106 | frame_open = Some(i); 107 | // } else if frame_open.is_some() && stop_codons.contains(nts.try_into().unwrap()) { 108 | } else if frame_open.is_some() 109 | && (STOP_CODONS.contains(nts.try_into().unwrap()) || seq_len_full - i <= 3) 110 | { 111 | // If we reach the end of the sequence, consider it closed. 112 | // todo: Handle circular around the origin. Ie, don't auto-close in that case. 113 | // + 1 for our 1-based seq name convention. 114 | // This section's a bit hairy; worked by trial and error. Final indices are respective to 115 | // the non-complementary seq, for both forward and reverse reading frames. 116 | let range = if !orf.is_reverse() { 117 | // todo: We still have wonkiness here. 118 | // RangeIncl::new(frame_open.unwrap() + 1 + offset, i + 2 + offset) 119 | RangeIncl::new(frame_open.unwrap() + 1 + offset, i + 3 + offset) 120 | } else { 121 | RangeIncl::new( 122 | seq_len_full - (i + 2 + offset), 123 | seq_len_full - (frame_open.unwrap() + offset) - 1, 124 | ) 125 | }; 126 | 127 | result.push(ReadingFrameMatch { frame: orf, range }); 128 | frame_open = None; 129 | } 130 | } 131 | 132 | result 133 | } 134 | -------------------------------------------------------------------------------- /src/resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/David-OConnor/plascad/21681308368918f9d1ad51517c47de3f990915a5/src/resources/icon.ico -------------------------------------------------------------------------------- /src/resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/David-OConnor/plascad/21681308368918f9d1ad51517c47de3f990915a5/src/resources/icon.png -------------------------------------------------------------------------------- /src/save_compat.rs: -------------------------------------------------------------------------------- 1 | //! This module contains archived state structs used to open saves from previous versions 2 | //! of this program, and convert them to the latest version. 3 | -------------------------------------------------------------------------------- /src/solution_helper.rs: -------------------------------------------------------------------------------- 1 | //! This module contains code for assisting with mixing common solutions 2 | -------------------------------------------------------------------------------- /src/tags.rs: -------------------------------------------------------------------------------- 1 | //! This module contains info related to tags, like the 6x HIS tag. 2 | 3 | use std::ops::RangeInclusive; 4 | 5 | use na_seq::Nucleotide; 6 | 7 | pub struct TagMatch { 8 | pub lib_index: usize, 9 | // todo: Experimenting with ranges vice (usize, usize) 10 | /// 0-based indexing. 11 | pub seq: RangeInclusive, 12 | } 13 | 14 | pub struct Tag { 15 | pub name: String, 16 | // From the 5' end. 17 | // pub seq: Seq, // todo: You may eventually need Vec. 18 | } 19 | 20 | impl Tag { 21 | pub fn new(name: &str) -> Self { 22 | Self { 23 | name: name.to_owned(), 24 | } 25 | } 26 | } 27 | 28 | // T7 promoter: TAATACGACTCACTATAG 29 | // T7 term: GCTAGTTATTGCTCAGCGG 30 | // T7 term take 2: ctagcataaccccttggggcctctaaacgggtcttgaggggttttttg 31 | 32 | pub fn _find_tag_matches(seq: &[Nucleotide], lib: &[Tag]) -> Vec { 33 | let mut result = Vec::new(); 34 | 35 | result 36 | } 37 | 38 | /// Load a set of common Restriction enzymes. Call this at program start, to load into a state field. 39 | pub fn _load_tag_library() -> Vec { 40 | vec![ 41 | // todo: Hisx6+x, 42 | // todo: T7 promoter 43 | // Tag::new("AatII", vec![G, A, C, G, T, C], 4), 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /src/toxic_proteins.rs: -------------------------------------------------------------------------------- 1 | //! Contains code related to identifying toxic proteins. 2 | 3 | use na_seq::{ 4 | Nucleotide, 5 | amino_acids::{AminoAcid, CodingResult}, 6 | }; 7 | 8 | use crate::protein::proteins_from_seq; 9 | 10 | const GLN_TRACT_SCAN_FRAME: usize = 35; 11 | const GLN_MAX_PORTION: f32 = 0.75; 12 | 13 | #[derive(Clone, Copy, PartialEq)] 14 | enum ToxicStatus { 15 | Pass, 16 | Fail, 17 | } 18 | 19 | // #[derive(Clone, Copy)] 20 | // enum Host { 21 | // Ecoli, 22 | // Aav, 23 | // } 24 | 25 | /// Identify tracts, in all reading frames, of long sequences of Glutamine; this may lead to aggregation, 26 | /// and therefor toxicity. Sequences of 35 Gln in a row are a good indicator of this. 27 | fn check_glu_tracts(seq: &[Nucleotide]) -> ToxicStatus { 28 | let len = seq.len(); 29 | 30 | let mut seq_rev = seq.to_vec(); 31 | seq_rev.reverse(); 32 | 33 | // For each reading frame, create a Vec of amino acids. 34 | for seq_ in &[seq, &seq_rev] { 35 | for rf_offset in 0..2 { 36 | let mut aa_seq = Vec::new(); 37 | 38 | for i_ in 0..len / 3 { 39 | let i = i_ * 3 + rf_offset; // The ORF-modified sequence index. 40 | 41 | if i + 3 >= len { 42 | break; 43 | } 44 | 45 | let nts = &seq_[i..i + 3]; 46 | 47 | // let mut matched = false; 48 | if let CodingResult::AminoAcid(aa) = AminoAcid::from_codons(nts.try_into().unwrap()) 49 | { 50 | // Note: We are ignoring stop codons here. 51 | aa_seq.push(aa) 52 | } 53 | } 54 | 55 | // Now that we have a set of AA for this reading frame, scan for Glu tracts. 56 | // If we find any, return the failure state. 57 | 58 | for start_i in 0..aa_seq.len() - GLN_TRACT_SCAN_FRAME { 59 | let mut num_gln = 0; 60 | for i in start_i..start_i + aa_seq.len() { 61 | if aa_seq[i] == AminoAcid::Gln { 62 | num_gln += 1; 63 | } 64 | } 65 | 66 | if num_gln as f32 / GLN_TRACT_SCAN_FRAME as f32 > GLN_MAX_PORTION { 67 | return ToxicStatus::Fail; 68 | } 69 | } 70 | } 71 | } 72 | 73 | ToxicStatus::Pass 74 | } 75 | --------------------------------------------------------------------------------