├── .cargo └── config.toml ├── .github └── workflows │ └── gh-pages.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── docs ├── book.toml ├── logo.png ├── logo.svg ├── screenshot.png └── src │ ├── SUMMARY.md │ ├── algorithm-details.md │ ├── getting-started.md │ └── introduction.md └── src ├── README.md ├── analysis ├── monte_carlo.rs ├── root_sum_square.rs └── structures.rs ├── io ├── dialogs.rs ├── export_csv.rs └── saved_state.rs ├── main.rs └── ui ├── components.rs ├── components ├── area_header.rs ├── area_mc_analysis.rs ├── area_stack_editor.rs ├── editable_label.rs ├── entry_tolerance.rs ├── filter_tolerance.rs ├── form_new_mc_analysis.rs └── form_new_tolerance.rs ├── fonts └── icons.ttf ├── icon.pdn ├── icon.png ├── icons.rs └── style.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | # NOTE: For maximum performance, build using a nightly compiler 2 | # If you are using rust stable, remove the "-Zshare-generics=y" below. 3 | 4 | [target.x86_64-unknown-linux-gnu] 5 | linker = "/usr/bin/clang" 6 | rustflags = ["-Clink-arg=-fuse-ld=lld"] 7 | 8 | # NOTE: you must manually install https://github.com/michaeleisel/zld on mac. you can easily do this with the "brew" package manager: 9 | # `brew install michaeleisel/zld/zld` 10 | [target.x86_64-apple-darwin] 11 | rustflags = ["-C", "link-arg=-fuse-ld=/usr/local/bin/zld", "-Zshare-generics=y"] 12 | 13 | [target.x86_64-pc-windows-msvc] 14 | linker = "rust-lld.exe" 15 | #rustflags = ["-Zshare-generics=y"] 16 | 17 | # Optional: Uncommenting the following improves compile times, but reduces the amount of debug info to 'line number tables only' 18 | # In most cases the gains are negligible, but if you are on macos and have slow compile times you should see significant gains. 19 | #[profile.dev] 20 | #debug = 1 -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Continuous integration 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-18.04 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Setup mdBook 16 | uses: peaceiris/actions-mdbook@v1 17 | with: 18 | mdbook-version: '0.4.5' 19 | # mdbook-version: 'latest' 20 | 21 | - run: mdbook build docs 22 | 23 | - name: Deploy 24 | uses: peaceiris/actions-gh-pages@v3 25 | with: 26 | github_token: ${{ secrets.TOKEN }} 27 | publish_dir: ./docs 28 | 29 | check: 30 | name: Check 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Install gtk3 34 | run: sudo apt-get install libgtk-3-dev 35 | if: ${{ runner.os == 'Linux' }} 36 | 37 | - uses: actions/checkout@v2 38 | - uses: actions-rs/toolchain@v1 39 | with: 40 | profile: minimal 41 | toolchain: stable 42 | override: true 43 | - uses: actions-rs/cargo@v1 44 | with: 45 | command: check 46 | 47 | test: 48 | name: Test Suite 49 | runs-on: ubuntu-latest 50 | steps: 51 | - name: Install gtk3 52 | run: sudo apt-get install libgtk-3-dev 53 | if: ${{ runner.os == 'Linux' }} 54 | 55 | - uses: actions/checkout@v2 56 | - uses: actions-rs/toolchain@v1 57 | with: 58 | profile: minimal 59 | toolchain: stable 60 | override: true 61 | - uses: actions-rs/cargo@v1 62 | with: 63 | command: test 64 | 65 | fmt: 66 | name: Rustfmt 67 | runs-on: ubuntu-latest 68 | steps: 69 | - name: Install gtk3 70 | run: sudo apt-get install libgtk-3-dev 71 | if: ${{ runner.os == 'Linux' }} 72 | 73 | - uses: actions/checkout@v2 74 | - uses: actions-rs/toolchain@v1 75 | with: 76 | profile: minimal 77 | toolchain: stable 78 | override: true 79 | - run: rustup component add rustfmt 80 | - uses: actions-rs/cargo@v1 81 | with: 82 | command: fmt 83 | args: --all -- --check 84 | 85 | clippy: 86 | name: Clippy 87 | runs-on: ubuntu-latest 88 | steps: 89 | - name: Install gtk3 90 | run: sudo apt-get install libgtk-3-dev 91 | if: ${{ runner.os == 'Linux' }} 92 | 93 | - uses: actions/checkout@v2 94 | - uses: actions-rs/toolchain@v1 95 | with: 96 | profile: minimal 97 | toolchain: stable 98 | override: true 99 | - run: rustup component add clippy 100 | - uses: actions-rs/cargo@v1 101 | with: 102 | command: clippy 103 | args: -- -D warnings -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | data.csv 3 | /.vscode 4 | /docs/book 5 | Cargo.lock 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tolstack" 3 | version = "0.2.0" 4 | authors = ["Aevyrie Roessler "] 5 | edition = "2018" 6 | 7 | [profile.release] 8 | #debug = true 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | rand_distr = "0.4.0" 14 | rand = "0.8.1" 15 | statistical = "1.0.0" 16 | num = "0.3.1" 17 | serde = "1.0.106" 18 | serde_derive = "1.0.106" 19 | serde_json = "1.0.51" 20 | toml = "0.5.6" 21 | csv = "1.1.3" 22 | iced = { version = "0.2.0", features = ["async-std",] } 23 | #iced = { git = "https://github.com/hecrj/iced", rev = "c393e450a1e314873667edb32e9ea3775d595c94", features = ["async-std",] } 24 | iced_native = "0.3.0" 25 | #iced_native = { git = "https://github.com/hecrj/iced", rev = "c393e450a1e314873667edb32e9ea3775d595c94" } 26 | iced_graphics = "0.1.0" 27 | #iced_graphics = { git = "https://github.com/hecrj/iced", rev = "c393e450a1e314873667edb32e9ea3775d595c94" } 28 | async-std = "1.0" 29 | directories-next = "2.0.0" 30 | open = "1.4.0" 31 | nfd = "0.0.4" 32 | notify = "5.0.0-pre.2" 33 | colored = "2.0.0" 34 | chrono = "0.4" 35 | webbrowser = "0.5.5" 36 | image = "0.23.12" 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Aevyrie Roessler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | ## Overview 6 | 7 | [![CI](https://img.shields.io/github/workflow/status/aevyrie/tolstack/Continuous%20integration/master)](https://github.com/aevyrie/tolstack/actions?query=workflow%3A%22Continuous+integration%22+branch%3Amaster) 8 | [![dependency status](https://deps.rs/repo/github/aevyrie/tolstack/status.svg)](https://deps.rs/repo/github/aevyrie/tolstack) 9 | [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/aevyrie/tolstack/blob/master/LICENSE) 10 | 11 | 12 | 13 | TolStack is an open source tolerance analysis application for building and analyzing 1D geometric tolerance models. The goal of this tool is to help make tolerance analysis fast, intuitive, and explorable. Built with Rust using [`iced`](https://github.com/hecrj/iced). 14 | 15 | [Read the TolStack user guide](https://aevyrie.github.io/tolstack/book/) 16 | 17 | [Roadmap](https://github.com/aevyrie/tolstack/projects/1) 18 | 19 | ### Disclaimer 20 | 21 | 🚧 This application is in development, untested, unstable, and not ready for general use. 🚧 22 | 23 | This software should only be used by engineers who are able to independently verify the correctness of its output. The software is provided as is, without warranty of any kind. The intent of this software is to aid in the exploration of tolerance analyses, not to replace existing methods of analysis or verification. 24 | 25 | ## Features 26 | 27 | * Build one-dimensional tolerance stackups in a visual editor 28 | * Evaluate and tune your tolerances with: 29 | * Monte Carlo analysis 30 | * RSS analysis 31 | * Export results to CSV 32 | 33 | ### Screenshot 34 | 35 | ![Screenshot](docs/screenshot.png) 36 | 37 | ## Build Instructions 38 | 39 | 1. Install Rust via [Rustup](https://www.rust-lang.org/tools/install). 40 | 2. Clone the repository with `git clone https://github.com/aevyrie/tolstack.git` 41 | 3. From the `tolstack` directory, run `cargo run --release` to build and launch the application with compiler optimizations. 42 | 43 | ### Hardware and Software Requirements 44 | 45 | * Note: make sure your graphics drivers are up to date! 46 | * Linux/Windows: You will need a modern graphics card that supports Vulkan 47 | * Integrated graphics (Intel HDxxx) requires vulkan support, check [here](https://www.intel.com/content/www/us/en/support/articles/000005524/graphics.html) 48 | * MacOS: the backend uses Metal, check [here](https://en.wikipedia.org/wiki/Metal_(API)#Supported_GPUs) for requirements 49 | 50 | ## License 51 | This project is licensed under the [MIT license](https://github.com/aevyrie/tolstack/blob/master/LICENSE). 52 | 53 | ### Contribution 54 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in tolstack by you, shall be licensed as MIT, without any additional terms or conditions. 55 | -------------------------------------------------------------------------------- /docs/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Aevyrie Roessler"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "TolStack User Guide" 7 | -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aevyrie/tolstack/bb8020f5a602ecc0cd25edac11dc0262324dd23f/docs/logo.png -------------------------------------------------------------------------------- /docs/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 40 | 44 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 57 | 61 | 65 | 69 | 74 | 78 | TOLSTACK 91 | TS 104 | 108 | 112 | 116 | 120 | 121 | 125 | TOL 136 | TOLSTACK 147 | 151 | 155 | 159 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aevyrie/tolstack/bb8020f5a602ecc0cd25edac11dc0262324dd23f/docs/screenshot.png -------------------------------------------------------------------------------- /docs/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Introduction](./introduction.md) 4 | - [Getting Started](./getting-started.md) 5 | - [Algorithm Details](./algorithm-details.md) 6 | -------------------------------------------------------------------------------- /docs/src/algorithm-details.md: -------------------------------------------------------------------------------- 1 | # Algorithm Details -------------------------------------------------------------------------------- /docs/src/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started -------------------------------------------------------------------------------- /docs/src/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | ## Overview 4 | 5 | TolStack is an open source tolerance analysis application for building and analyzing 1D geometric tolerance models. The goal of this tool is to help make tolerance analysis fast, intuitive, and explorable. Built with Rust using [`iced`](https://github.com/hecrj/iced). 6 | 7 | ## Use Cases 8 | 9 | One-dimensional tolerance analysis is useful for answering questions like: 10 | * Will this stack of plastic parts and PCBs fit into my enclosure 99.99% of the time? 11 | * When I depress this button, will I make electrical contact with the switch before it bottoms out? 12 | * Knowing the given tolerances of my purchased components, what part tolerances do I need to function? 13 | 14 | ## Features 15 | 16 | * Build one-dimensional tolerance stackups in a visual editor 17 | * Evaluate and tune your tolerances with: 18 | * Monte Carlo analysis 19 | * RSS analysis 20 | * Worst case tolerance analysis 21 | * Export results to CSV 22 | 23 | # Background 24 | 25 | [Tolerance analysis](https://en.wikipedia.org/wiki/Tolerance_analysis) is used in Mechanical Engineering to quantify the accumulated dimensional variation in assemblies of parts. This is used to define part tolerances, and later verify that manufacturing processes are statistically capable of producing parts to this tolerance spec. Generally, the goal is to specify the widest possible tolerances to minimize scrap ($$$) while ensuring any combination of parts within these tolerances still fit together and function. GD&T (ASME Y14.5) is commonly used as the languge to express three-dimensional tolerances. 26 | 27 | This application does not attempt to model all of the tolerances in your assembly, rather, this is a tool to help you model and understand critical tolerance stacks in one dimension. This greatly simplifies the modelling process and generally makes for much clearer, actionable, output. To construct a 1D model, you will need to: 28 | 29 | 1. Determine the target measurement you want to evaluate. 30 | 2. Define an axis to project this measurement onto (often times you can just project onto a plane). 31 | 3. Define the positive and negative directions along your axis - this is very important! 32 | 4. Determine the chain of dimensions needed to define the stackup that results in your target measurement. 33 | 5. Using this chain of dimensions, record the dimensions and tolerances as projected on your axis, making sure the signs are correct. 34 | 35 | # Build Instructions 36 | 37 | 1. Install Rust via [Rustup](https://www.rust-lang.org/tools/install). 38 | 2. Clone the repository with `git clone https://github.com/aevyrie/tolstack.git` 39 | 3. From the `tolstack` directory, run `cargo run --release` to build and launch the application with compiler optimizations. 40 | 41 | ## Hardware Requirements and Software Dependencies 42 | 43 | * Make sure your graphics drivers are up to date! 44 | * Linux/Windows: You will need a modern graphics card that supports Vulkan 45 | * Integrated graphics (Intel HDxxx) requires vulkan support, check [here](https://www.intel.com/content/www/us/en/support/articles/000005524/graphics.html) 46 | * MacOS: the backend uses Metal, check [here](https://en.wikipedia.org/wiki/Metal_(API)#Supported_GPUs) for requirements 47 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # Program Structure 2 | 3 | The GUI is written using [iced](https://github.com/hecrj/iced/). 4 | 5 | * `main.rs` is the entry point of the program, and contians the GUI code. 6 | * The `ui\components` folder contains the (reusable) components that make up the ui 7 | * `ui\style.rs` contains ui stylsheet and hot reloading logic 8 | * The `io` folder contains file save/open dialog logic, as well as the serializable state of the application 9 | * The `analysis` folder holds all of the actual tolerance simulation logic 10 | -------------------------------------------------------------------------------- /src/analysis/monte_carlo.rs: -------------------------------------------------------------------------------- 1 | use super::structures::*; 2 | 3 | use std::error::Error; 4 | use std::sync::mpsc; 5 | use std::thread; 6 | 7 | use rand::prelude::*; 8 | use rand_distr::{StandardNormal, Uniform}; 9 | use statistical::*; 10 | 11 | pub async fn run(state: &State) -> Result> { 12 | // Divide the desired number of iterations into chunks. This is done [1] to avoid floating point 13 | // errors (as the divisor gets large when averaging you lose precision) and [2] to prevent huge 14 | // memory use for large numbers of iterations. This can also be used to tune performance. 15 | let chunk_size = 100000; 16 | let chunks = state.parameters.n_iterations / chunk_size; 17 | let real_iters = chunks * chunk_size; 18 | let mut result = Vec::new(); 19 | let mut result_mean = 0f64; 20 | let mut result_stddev_pos = 0f64; 21 | let mut result_stddev_neg = 0f64; 22 | 23 | for _n in 0..chunks { 24 | // TODO: validate n_iterations is nicely divisible by chunk_size and n_threads. 25 | // Gather samples into a stack that is `chunk_size` long for each Tolerance 26 | let mut stack = compute_stackup(state.tolerance_loop.clone(), chunk_size); 27 | // Sum each 28 | let stack_mean: f64 = mean(&stack); 29 | let stack_stddev_pos = standard_deviation( 30 | &stack 31 | .iter() 32 | .cloned() 33 | .filter(|x| x > &stack_mean) 34 | .collect::>(), 35 | Some(stack_mean), 36 | ); 37 | let stack_stddev_neg = standard_deviation( 38 | &stack 39 | .iter() 40 | .cloned() 41 | .filter(|x| x < &stack_mean) 42 | .collect::>(), 43 | Some(stack_mean), 44 | ); 45 | 46 | result.append(&mut stack); 47 | result_mean += stack_mean; 48 | result_stddev_neg += stack_stddev_neg; 49 | result_stddev_pos += stack_stddev_pos; 50 | } 51 | 52 | //result_mean = result_mean / chunks as f64; 53 | result_stddev_neg /= chunks as f64; 54 | result_stddev_pos /= chunks as f64; 55 | 56 | result_mean = mean(&result); 57 | 58 | let result_tol_pos = result_stddev_pos * state.parameters.assy_sigma; 59 | let result_tol_neg = result_stddev_neg * state.parameters.assy_sigma; 60 | 61 | let worst_case_dim = state.tolerance_loop.iter().fold(0.0, |acc, tol| { 62 | acc + match tol { 63 | Tolerance::Linear(linear) => linear.distance.dim, 64 | Tolerance::Float(_) => 0.0, 65 | } 66 | }); 67 | 68 | let worst_case_pos = state.tolerance_loop.iter().fold(0.0, |acc, tol| { 69 | acc + f64::abs(match tol { 70 | Tolerance::Linear(linear) => linear.distance.tol_pos, 71 | Tolerance::Float(float) => f64::max( 72 | 0.0, 73 | ((float.hole.dim + float.hole.tol_pos) - (float.pin.dim - float.pin.tol_neg)) / 2.0, 74 | ), 75 | }) 76 | }); 77 | 78 | let worst_case_neg = state.tolerance_loop.iter().fold(0.0, |acc, tol| { 79 | acc + f64::abs(match tol { 80 | Tolerance::Linear(linear) => linear.distance.tol_neg, 81 | Tolerance::Float(float) => f64::max( 82 | 0.0, 83 | ((float.hole.dim + float.hole.tol_pos) - (float.pin.dim - float.pin.tol_neg)) / 2.0, 84 | ), 85 | }) 86 | }); 87 | 88 | dbg!( 89 | worst_case_dim, 90 | worst_case_neg, 91 | worst_case_pos, 92 | result_stddev_neg, 93 | result_stddev_pos 94 | ); 95 | 96 | let worst_case_upper = worst_case_dim + worst_case_pos; 97 | let worst_case_lower = worst_case_dim - worst_case_neg; 98 | 99 | Ok(McResults { 100 | mean: result_mean, 101 | tolerance_pos: result_tol_pos, 102 | tolerance_neg: result_tol_neg, 103 | stddev_pos: result_stddev_pos, 104 | stddev_neg: result_stddev_neg, 105 | iterations: real_iters, 106 | worst_case_upper, 107 | worst_case_lower, 108 | }) 109 | } 110 | 111 | impl Tolerance { 112 | #[inline(always)] 113 | pub fn mc_tolerance(&self) -> f64 { 114 | match self { 115 | Tolerance::Linear(val) => val.mc_tolerance(), 116 | Tolerance::Float(val) => val.mc_tolerance(), 117 | } 118 | } 119 | } 120 | 121 | /// Generate a sample for each object in the tolerance collection, n_iterations times. Then sum 122 | /// the results for each iteration, resulting in stackup for that iteration of the simulation. 123 | pub fn compute_stackup(tol_collection: Vec, n_iterations: usize) -> Vec { 124 | // Make a local clone of the tolerance collection so the borrow is not returned while the 125 | // threads are using the collection. 126 | let tc_local = tol_collection; 127 | // Store output in `samples` vector, appending each tol_collection's output 128 | let n_tols = tc_local.len(); 129 | let mut samples: Vec = Vec::with_capacity(n_tols * n_iterations); 130 | let (tx, rx) = mpsc::channel(); 131 | // For each tolerance object generate n samples, dividing the work between multiple threads. 132 | for tol_struct in tc_local { 133 | let n_threads = 4; 134 | for _i in 0..n_threads { 135 | // Create a thread local copy of the thread communication sender for ownership reasons. 136 | let tx_local = mpsc::Sender::clone(&tx); 137 | thread::spawn(move || { 138 | // Make `result` thread local for better performance. 139 | let mut result: Vec = Vec::new(); 140 | for _i in 0..n_iterations / n_threads { 141 | result.push(tol_struct.mc_tolerance()); 142 | } 143 | tx_local.send(result).unwrap(); 144 | }); 145 | } 146 | for _i in 0..n_threads { 147 | samples.extend_from_slice(&rx.recv().unwrap()); 148 | } 149 | } 150 | 151 | let mut result: Vec = Vec::with_capacity(n_iterations); 152 | 153 | for i in 0..n_iterations { 154 | let mut stackup: f64 = 0.0; 155 | for j in 0..n_tols { 156 | stackup += samples[i + j * n_iterations]; 157 | } 158 | result.push(stackup); 159 | } 160 | result 161 | } 162 | 163 | #[allow(dead_code)] 164 | impl Tolerance { 165 | fn compute_multiplier(&mut self) { 166 | match self { 167 | Tolerance::Linear(tol) => tol.compute_multiplier(), 168 | Tolerance::Float(tol) => tol.compute_multiplier(), 169 | } 170 | } 171 | } 172 | 173 | pub trait MonteCarlo: Send + Sync + 'static { 174 | fn mc_tolerance(&self) -> f64; 175 | fn compute_multiplier(&mut self); 176 | //fn get_name(&self) -> &str; 177 | } 178 | impl MonteCarlo for LinearTL { 179 | fn mc_tolerance(&self) -> f64 { 180 | self.distance.dim 181 | + self 182 | .distance 183 | .sample_mc(DistributionParam::Normal, BoundingParam::KeepAll) 184 | } 185 | //fn get_name(&self) -> &str { 186 | // &self.name 187 | //} 188 | fn compute_multiplier(&mut self) { 189 | self.distance.compute_multiplier(); 190 | } 191 | } 192 | impl MonteCarlo for FloatTL { 193 | fn mc_tolerance(&self) -> f64 { 194 | let hole_sample = self 195 | .hole 196 | .sample_mc(DistributionParam::Normal, BoundingParam::KeepAll); 197 | let pin_sample = self 198 | .pin 199 | .sample_mc(DistributionParam::Normal, BoundingParam::KeepAll); 200 | let hole_pin_slop = (hole_sample - pin_sample) / 2.0; 201 | if hole_pin_slop <= 0.0 { 202 | 0.0 203 | } else { 204 | DimTol::new_normal(0.0, hole_pin_slop, hole_pin_slop, self.sigma) 205 | .sample_mc(DistributionParam::Uniform, BoundingParam::KeepAll) 206 | } 207 | } 208 | fn compute_multiplier(&mut self) { 209 | self.hole.compute_multiplier(); 210 | self.pin.compute_multiplier(); 211 | } 212 | //fn get_name(&self) -> &str { 213 | // &self.name 214 | //} 215 | } 216 | 217 | pub enum DistributionParam { 218 | Normal, 219 | Uniform, 220 | } 221 | 222 | #[allow(dead_code)] 223 | pub enum BoundingParam { 224 | DiscardOutOfSpec, 225 | KeepAll, 226 | } 227 | 228 | impl DimTol { 229 | /// Generate a random sample of a given dimension 230 | fn sample_mc(&self, distribution: DistributionParam, bounding: BoundingParam) -> f64 { 231 | match distribution { 232 | DistributionParam::Normal => match bounding { 233 | BoundingParam::DiscardOutOfSpec => self.rand_bound_norm(), 234 | BoundingParam::KeepAll => self.rand_unbound_norm(), 235 | }, 236 | DistributionParam::Uniform => match bounding { 237 | BoundingParam::DiscardOutOfSpec => self.rand_bound_uniform(), 238 | BoundingParam::KeepAll => self.rand_unbounded_uniform(), 239 | }, 240 | } 241 | } 242 | 243 | /// Generate a normally distributed random value, discarding values outside of limits 244 | fn rand_bound_norm(&self) -> f64 { 245 | let mut sample: f64 = thread_rng().sample(StandardNormal); 246 | sample *= self.tol_multiplier; 247 | // TODO: limit number of checks and error out if needed to escape infinite loop 248 | while sample < -self.tol_neg || sample > self.tol_pos { 249 | sample = thread_rng().sample(StandardNormal); 250 | sample *= self.tol_multiplier; 251 | } 252 | sample 253 | } 254 | fn rand_unbound_norm(&self) -> f64 { 255 | let mut sample: f64 = thread_rng().sample(StandardNormal); 256 | sample *= self.tol_multiplier; 257 | sample 258 | } 259 | fn rand_bound_uniform(&self) -> f64 { 260 | let mut sample: f64 = thread_rng().sample(Uniform::new_inclusive(-1.0, 1.0)); 261 | sample = (sample * (self.tol_neg + self.tol_pos)) - self.tol_neg; 262 | // TODO: limit number of checks and error out if needed to escape infinite loop 263 | while sample < -self.tol_neg || sample > self.tol_pos { 264 | sample = thread_rng().sample(StandardNormal); 265 | sample = (sample * (self.tol_neg + self.tol_pos)) - self.tol_neg; 266 | } 267 | sample 268 | } 269 | fn rand_unbounded_uniform(&self) -> f64 { 270 | let mut sample: f64 = thread_rng().sample(Uniform::new_inclusive(-1.0, 1.0)); 271 | sample = (sample * (self.tol_neg + self.tol_pos)) - self.tol_neg; 272 | sample 273 | } 274 | 275 | /// Precompute constant in monte carlo equation 276 | fn compute_multiplier(&mut self) { 277 | self.tol_multiplier = (self.tol_pos + self.tol_neg) / 2.0 / self.sigma; 278 | } 279 | } 280 | 281 | /// Data for testing purposes 282 | #[allow(dead_code)] 283 | pub fn test_data() -> State { 284 | let parameters = Parameters { 285 | assy_sigma: 4.0, 286 | n_iterations: 10000000, 287 | }; 288 | 289 | let mut model = State::new(parameters); 290 | 291 | model.add(Tolerance::Linear(LinearTL::new(DimTol::new_normal( 292 | 65.88, 0.17, 0.17, 3.0, 293 | )))); 294 | 295 | model.add(Tolerance::Float(FloatTL::new( 296 | DimTol::new_normal(2.50, 0.1, 0.0, 3.0), 297 | DimTol::new_normal(3.0, 0.08, 0.22, 3.0), 298 | 3.0, 299 | ))); 300 | 301 | model.add(Tolerance::Float(FloatTL::new( 302 | DimTol::new_normal(2.50, 0.1, 0.0, 3.0), 303 | DimTol::new_normal(3.0, 0.08, 0.22, 3.0), 304 | 3.0, 305 | ))); 306 | 307 | model 308 | } 309 | -------------------------------------------------------------------------------- /src/analysis/root_sum_square.rs: -------------------------------------------------------------------------------- 1 | use super::structures::*; 2 | use std::error::Error; 3 | 4 | pub async fn run(state: &State) -> Result> { 5 | let mean: f64 = state 6 | .tolerance_loop 7 | .iter() 8 | .fold(0.0, |acc, tol| acc + tol.distance()); 9 | let tolerance_neg = state 10 | .tolerance_loop 11 | .iter() 12 | .fold(0.0, |acc, tol| { 13 | acc + match tol { 14 | Tolerance::Linear(linear) => { 15 | (linear.distance.tol_neg / linear.distance.sigma).powi(2) 16 | } 17 | Tolerance::Float(float) => { 18 | let hole_avg = (float.hole.tol_neg + float.hole.tol_pos) / 2.0; 19 | // Divide by two because the hole dim is diametric 20 | let hole_squared = ((hole_avg / 2.0) / float.pin.sigma).powi(2); 21 | let pin_avg = (float.pin.tol_neg + float.pin.tol_pos) / 2.0; 22 | // Divide by two because the pin dim is diametric 23 | let pin_squared = ((pin_avg / 2.0) / float.pin.sigma).powi(2); 24 | hole_squared + pin_squared 25 | } 26 | } 27 | }) 28 | .sqrt() 29 | * state.parameters.assy_sigma; 30 | let tolerance_pos = state 31 | .tolerance_loop 32 | .iter() 33 | .fold(0.0, |acc, tol| { 34 | acc + match tol { 35 | Tolerance::Linear(linear) => { 36 | (linear.distance.tol_pos / linear.distance.sigma).powi(2) 37 | } 38 | Tolerance::Float(float) => { 39 | let hole_avg = (float.hole.tol_neg + float.hole.tol_pos) / 2.0; 40 | let hole_squared = ((hole_avg / 2.0) / float.pin.sigma).powi(2); 41 | let pin_avg = (float.pin.tol_neg + float.pin.tol_pos) / 2.0; 42 | let pin_squared = ((pin_avg / 2.0) / float.pin.sigma).powi(2); 43 | hole_squared + pin_squared 44 | } 45 | } 46 | }) 47 | .sqrt() 48 | * state.parameters.assy_sigma; 49 | 50 | Ok(RssResults::new(mean, tolerance_pos, tolerance_neg)) 51 | } 52 | -------------------------------------------------------------------------------- /src/analysis/structures.rs: -------------------------------------------------------------------------------- 1 | /// Contains structures used to define tolerances in a tolerance loop. 2 | use serde_derive::*; 3 | 4 | #[derive(Copy, Clone, Debug, Default, Deserialize, Serialize, PartialEq)] 5 | pub struct DimTol { 6 | pub dim: f64, 7 | pub tol_pos: f64, 8 | pub tol_neg: f64, 9 | pub tol_multiplier: f64, 10 | pub sigma: f64, 11 | dist: TolDistribution, 12 | } 13 | impl DimTol { 14 | pub fn new_normal(dim: f64, tol_pos: f64, tol_neg: f64, sigma: f64) -> Self { 15 | if tol_neg <= 0.0 || tol_pos <= 0.0 { 16 | panic!("Number supplied to new_normal was negative"); 17 | } 18 | let tol_multiplier: f64 = (tol_pos + tol_neg) / 2.0 / sigma; 19 | // If the tolerances are not equal for a normally distributed tolerance, the tolerance must be normalized. 20 | let (dim_norm, tol_pos_norm, tol_neg_norm) = if (tol_pos - tol_neg) > f64::EPSILON { 21 | let new_dim = (tol_pos + tol_neg) / 2.0; 22 | (dim + (tol_pos + tol_neg) / 2.0, new_dim, new_dim) 23 | } else { 24 | (dim, tol_pos, tol_neg) 25 | }; 26 | DimTol { 27 | dim: dim_norm, 28 | tol_pos: tol_pos_norm, 29 | tol_neg: tol_neg_norm, 30 | tol_multiplier, 31 | sigma, 32 | dist: TolDistribution::Normal, 33 | } 34 | } 35 | } 36 | 37 | #[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq)] 38 | pub enum TolDistribution { 39 | Normal, 40 | } 41 | impl Default for TolDistribution { 42 | fn default() -> Self { 43 | TolDistribution::Normal 44 | } 45 | } 46 | 47 | #[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq)] 48 | pub enum Tolerance { 49 | Linear(LinearTL), 50 | Float(FloatTL), 51 | //Compound(CompoundFloatTL), 52 | } 53 | impl Default for Tolerance { 54 | fn default() -> Self { 55 | Tolerance::Linear(LinearTL::default()) 56 | } 57 | } 58 | impl Tolerance { 59 | pub fn distance(&self) -> f64 { 60 | match self { 61 | Tolerance::Linear(linear) => linear.distance.dim, 62 | Tolerance::Float(_) => 0f64, 63 | } 64 | } 65 | } 66 | 67 | #[derive(Copy, Clone, Debug, Default, Deserialize, Serialize, PartialEq)] 68 | pub struct LinearTL { 69 | pub distance: DimTol, 70 | } 71 | impl LinearTL { 72 | pub fn new(distance: DimTol) -> Self { 73 | LinearTL { distance } 74 | } 75 | } 76 | 77 | #[derive(Copy, Clone, Debug, Default, Deserialize, Serialize, PartialEq)] 78 | pub struct FloatTL { 79 | pub hole: DimTol, 80 | pub pin: DimTol, 81 | pub sigma: f64, 82 | } 83 | impl FloatTL { 84 | pub fn new(hole: DimTol, pin: DimTol, sigma: f64) -> Self { 85 | FloatTL { hole, pin, sigma } 86 | } 87 | } 88 | 89 | /// Structure used to hold simulation input parameters 90 | #[derive(Debug, Clone, Default, Deserialize, Serialize)] 91 | pub struct Parameters { 92 | pub assy_sigma: f64, 93 | pub n_iterations: usize, 94 | } 95 | 96 | #[derive(Debug, Clone, Deserialize, Serialize)] 97 | pub struct AnalysisResults { 98 | monte_carlo: Option, 99 | rss: Option, 100 | } 101 | impl AnalysisResults { 102 | pub fn monte_carlo(&self) -> &Option { 103 | &self.monte_carlo 104 | } 105 | pub fn rss(&self) -> &Option { 106 | &self.rss 107 | } 108 | pub fn export(&self) -> Vec { 109 | if let Some(mc_result) = &self.monte_carlo { 110 | if let Some(rss_result) = &self.rss { 111 | let mut result = Vec::new(); 112 | result.push(mc_result.mean); 113 | result.push(mc_result.tolerance_pos); 114 | result.push(mc_result.tolerance_neg); 115 | result.push(mc_result.stddev_pos); 116 | result.push(mc_result.stddev_neg); 117 | result.push(mc_result.worst_case_lower); 118 | result.push(mc_result.worst_case_upper); 119 | result.push(rss_result.mean); 120 | result.push(rss_result.tolerance_pos); 121 | result.push(rss_result.tolerance_neg); 122 | return result; 123 | } 124 | } 125 | // If no result is generated, return an empty vec 126 | Vec::new() 127 | } 128 | } 129 | impl From<(McResults, RssResults)> for AnalysisResults { 130 | fn from(results: (McResults, RssResults)) -> Self { 131 | let (monte_carlo, rss) = results; 132 | AnalysisResults { 133 | monte_carlo: Some(monte_carlo), 134 | rss: Some(rss), 135 | } 136 | } 137 | } 138 | impl Default for AnalysisResults { 139 | fn default() -> Self { 140 | AnalysisResults { 141 | monte_carlo: None, 142 | rss: None, 143 | } 144 | } 145 | } 146 | 147 | //todo remove pub and add a getter 148 | /// Structure used to hold the output of a Monte Carlo simulaion 149 | #[derive(Debug, Clone, Default, Deserialize, Serialize)] 150 | pub struct McResults { 151 | pub mean: f64, 152 | pub tolerance_pos: f64, 153 | pub tolerance_neg: f64, 154 | pub stddev_pos: f64, 155 | pub stddev_neg: f64, 156 | pub iterations: usize, 157 | pub worst_case_upper: f64, 158 | pub worst_case_lower: f64, 159 | } 160 | impl McResults {} 161 | 162 | /// Structure used to hold the output of an RSS calculation 163 | #[derive(Debug, Clone, Default, Deserialize, Serialize)] 164 | pub struct RssResults { 165 | mean: f64, 166 | tolerance_pos: f64, 167 | tolerance_neg: f64, 168 | } 169 | impl RssResults { 170 | pub fn new(mean: f64, tolerance_pos: f64, tolerance_neg: f64) -> Self { 171 | RssResults { 172 | mean, 173 | tolerance_pos, 174 | tolerance_neg, 175 | } 176 | } 177 | pub fn mean(&self) -> f64 { 178 | self.mean 179 | } 180 | pub fn tolerance_pos(&self) -> f64 { 181 | self.tolerance_pos 182 | } 183 | pub fn tolerance_neg(&self) -> f64 { 184 | self.tolerance_neg 185 | } 186 | } 187 | 188 | /// Holds the working state of the simulation, including inputs and outputs 189 | #[derive(Debug, Clone, Deserialize, Serialize)] 190 | pub struct State { 191 | pub parameters: Parameters, 192 | pub tolerance_loop: Vec, 193 | pub results: AnalysisResults, 194 | } 195 | impl State { 196 | pub fn new(parameters: Parameters) -> Self { 197 | State { 198 | parameters, 199 | tolerance_loop: Vec::new(), 200 | results: AnalysisResults::default(), 201 | } 202 | } 203 | pub fn add(&mut self, tolerance: Tolerance) { 204 | self.tolerance_loop.push(tolerance); 205 | } 206 | pub fn clear_inputs(&mut self) { 207 | self.tolerance_loop = Vec::new(); 208 | } 209 | } 210 | impl Default for State { 211 | fn default() -> Self { 212 | let parameters = Parameters { 213 | assy_sigma: 4.0, 214 | n_iterations: 1000000, 215 | }; 216 | State::new(parameters) 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/io/dialogs.rs: -------------------------------------------------------------------------------- 1 | use nfd::Response; 2 | use std::io; 3 | use std::path::{Path, PathBuf}; 4 | 5 | pub async fn open() -> Result { 6 | let result: nfd::Response = match async { nfd::open_file_dialog(Some("json"), None) }.await { 7 | Ok(result) => result, 8 | Err(_) => { 9 | return Err(io::Error::new( 10 | io::ErrorKind::InvalidData, 11 | "Unable to unwrap data from new file dialog", 12 | )) 13 | } 14 | }; 15 | 16 | let file_string: String = match result { 17 | Response::Okay(file_path) => file_path, 18 | Response::OkayMultiple(_) => { 19 | return Err(io::Error::new( 20 | io::ErrorKind::InvalidInput, 21 | "Multiple files returned when one was expected", 22 | )) 23 | } 24 | Response::Cancel => { 25 | return Err(io::Error::new( 26 | io::ErrorKind::Interrupted, 27 | "User cancelled file open", 28 | )) 29 | } 30 | }; 31 | 32 | let mut result: PathBuf = PathBuf::new(); 33 | result.push(Path::new(&file_string)); 34 | 35 | if result.exists() { 36 | return Ok(result); 37 | } else { 38 | return Err(io::Error::new( 39 | io::ErrorKind::NotFound, 40 | "File does not exist", 41 | )); 42 | } 43 | } 44 | 45 | pub async fn save_as() -> Result { 46 | let result: nfd::Response = match async { nfd::open_save_dialog(Some("json"), None) }.await { 47 | Ok(result) => result, 48 | Err(_) => { 49 | return Err(io::Error::new( 50 | io::ErrorKind::InvalidData, 51 | "Unable to unwrap data from new file dialog", 52 | )) 53 | } 54 | }; 55 | 56 | let file_string: String = match result { 57 | Response::Okay(file_path) => file_path, 58 | Response::OkayMultiple(_) => { 59 | return Err(io::Error::new( 60 | io::ErrorKind::InvalidInput, 61 | "Multiple files returned when one was expected", 62 | )) 63 | } 64 | Response::Cancel => { 65 | return Err(io::Error::new( 66 | io::ErrorKind::Interrupted, 67 | "User cancelled file open", 68 | )) 69 | } 70 | }; 71 | 72 | let mut result: PathBuf = PathBuf::new(); 73 | result.push(Path::new(&file_string)); 74 | 75 | if match result.parent() { 76 | Some(parent) => parent.exists(), 77 | None => false, 78 | } { 79 | return Ok(result); 80 | } else { 81 | return Err(io::Error::new( 82 | io::ErrorKind::NotFound, 83 | "Parent directory does not exist", 84 | )); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/io/export_csv.rs: -------------------------------------------------------------------------------- 1 | use chrono::prelude::*; 2 | use csv::Writer; 3 | 4 | #[derive(Debug, Clone)] 5 | pub enum SaveError { 6 | DirectoryError, 7 | SerializeError, 8 | WriteError, 9 | OpenError, 10 | } 11 | 12 | pub async fn serialize_csv(data: Vec) -> Result<(), SaveError> { 13 | async { 14 | let path = path(); 15 | let mut wtr = Writer::from_path(path.clone()).map_err(|_| SaveError::DirectoryError)?; 16 | for entry in data { 17 | wtr.serialize(entry) 18 | .map_err(|_| SaveError::SerializeError)?; 19 | } 20 | wtr.flush().map_err(|_| SaveError::WriteError)?; 21 | open::that(path).map_err(|_| SaveError::OpenError)?; 22 | Ok(()) 23 | } 24 | .await 25 | } 26 | 27 | fn path() -> std::path::PathBuf { 28 | let now = chrono::offset::Local::now(); 29 | let mut path = std::env::temp_dir(); 30 | path.push(format!( 31 | "tolstack_export_{}_{}_{}_{}_{}_{}.csv", 32 | now.year(), 33 | now.month(), 34 | now.day(), 35 | now.hour(), 36 | now.minute(), 37 | now.second() 38 | )); 39 | 40 | path 41 | } 42 | -------------------------------------------------------------------------------- /src/io/saved_state.rs: -------------------------------------------------------------------------------- 1 | use super::dialogs; 2 | use crate::ui::components::entry_tolerance::ToleranceEntry; 3 | use serde_derive::*; 4 | use std::path::PathBuf; 5 | 6 | #[derive(Debug, Clone, Serialize, Deserialize)] 7 | pub struct SavedState { 8 | pub name: String, 9 | pub tolerances: Vec, 10 | pub n_iteration: usize, 11 | pub assy_sigma: f64, 12 | } 13 | 14 | #[derive(Debug, Clone)] 15 | pub enum LoadError { 16 | FileError, 17 | FormatError, 18 | } 19 | 20 | #[derive(Debug, Clone)] 21 | pub enum SaveError { 22 | DirectoryError, 23 | FileError, 24 | WriteError, 25 | FormatError, 26 | } 27 | 28 | impl Default for SavedState { 29 | fn default() -> Self { 30 | SavedState { 31 | name: "New Project".into(), 32 | tolerances: Vec::new(), 33 | n_iteration: 100000, 34 | assy_sigma: 4.0, 35 | } 36 | } 37 | } 38 | 39 | #[cfg(not(target_arch = "wasm32"))] 40 | impl SavedState { 41 | pub async fn new() -> Result<(Option, SavedState), LoadError> { 42 | Ok(( 43 | None, 44 | SavedState { 45 | name: "New Project".into(), 46 | tolerances: Vec::new(), 47 | n_iteration: 100000, 48 | assy_sigma: 4.0, 49 | }, 50 | )) 51 | } 52 | 53 | pub async fn save(state: SavedState, path: PathBuf) -> Result, SaveError> { 54 | use async_std::prelude::*; 55 | let json = serde_json::to_string_pretty(&state).map_err(|_| SaveError::FormatError)?; 56 | if let Some(dir) = path.parent() { 57 | async_std::fs::create_dir_all(dir) 58 | .await 59 | .map_err(|_| SaveError::DirectoryError)?; 60 | } 61 | { 62 | let mut file = async_std::fs::File::create(&path) 63 | .await 64 | .map_err(|_| SaveError::FileError)?; 65 | file.write_all(json.as_bytes()) 66 | .await 67 | .map_err(|_| SaveError::WriteError)?; 68 | } 69 | 70 | Ok(Some(path)) 71 | } 72 | 73 | pub async fn open() -> Result<(Option, SavedState), LoadError> { 74 | use async_std::prelude::*; 75 | let path = match dialogs::open().await { 76 | Ok(path) => path, 77 | Err(error) => { 78 | println!("{:?}", error); 79 | return Err(LoadError::FileError); 80 | } 81 | }; 82 | let mut contents = String::new(); 83 | let mut file = async_std::fs::File::open(&path) 84 | .await 85 | .map_err(|_| LoadError::FileError)?; 86 | 87 | file.read_to_string(&mut contents) 88 | .await 89 | .map_err(|_| LoadError::FileError)?; 90 | 91 | match serde_json::from_str(&contents).map_err(|_| LoadError::FormatError) { 92 | Ok(data) => Ok((Some(path), data)), 93 | Err(e) => Err(e), 94 | } 95 | } 96 | 97 | pub async fn save_as(state: SavedState) -> Result, SaveError> { 98 | use async_std::prelude::*; 99 | let path = match dialogs::save_as().await { 100 | Ok(path) => path, 101 | Err(error) => { 102 | println!("{:?}", error); 103 | return Err(SaveError::FileError); 104 | } 105 | }; 106 | let path = path.with_extension("json"); 107 | let json = serde_json::to_string_pretty(&state).map_err(|_| SaveError::FormatError)?; 108 | if let Some(dir) = path.parent() { 109 | async_std::fs::create_dir_all(dir) 110 | .await 111 | .map_err(|_| SaveError::DirectoryError)?; 112 | } 113 | { 114 | let mut file = async_std::fs::File::create(&path) 115 | .await 116 | .map_err(|_| SaveError::FileError)?; 117 | file.write_all(json.as_bytes()) 118 | .await 119 | .map_err(|_| SaveError::WriteError)?; 120 | } 121 | 122 | Ok(Some(path)) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //#![windows_subsystem = "windows"] // Tells windows compiler not to show console window 2 | #![warn(clippy::all)] 3 | 4 | mod ui { 5 | pub mod components; 6 | pub mod icons; 7 | pub mod style; 8 | } 9 | 10 | mod analysis { 11 | pub mod monte_carlo; 12 | pub mod root_sum_square; 13 | pub mod structures; 14 | } 15 | 16 | mod io { 17 | pub mod dialogs; 18 | pub mod export_csv; 19 | pub mod saved_state; 20 | } 21 | 22 | use analysis::structures::*; 23 | use io::{export_csv, saved_state::*}; 24 | use ui::{components::*, style}; 25 | 26 | use colored::*; 27 | use iced::{ 28 | keyboard, text_input, time, window, Application, Column, Command, Container, Element, 29 | HorizontalAlignment, Length, Row, Settings, Subscription, Text, 30 | }; 31 | use image::GenericImageView; 32 | 33 | use std::path::PathBuf; 34 | 35 | fn main() { 36 | let bytes = include_bytes!("ui/icon.png"); 37 | let img = image::load_from_memory(bytes).unwrap(); 38 | let img_dims = img.dimensions(); 39 | let img_raw = img.into_rgba8().into_raw(); 40 | let icon = window::Icon::from_rgba(img_raw, img_dims.0, img_dims.1).unwrap(); 41 | 42 | let settings = Settings { 43 | window: window::Settings { 44 | size: (1024, 768), 45 | resizable: true, 46 | decorations: true, 47 | min_size: Some((800, 600)), 48 | max_size: None, 49 | transparent: false, 50 | always_on_top: false, 51 | icon: Some(icon), 52 | }, 53 | antialiasing: true, 54 | ..Default::default() 55 | }; 56 | TolStack::run(settings).unwrap(); 57 | } 58 | 59 | // The state of the application 60 | #[derive(Debug, Clone)] 61 | struct State { 62 | last_save: std::time::Instant, 63 | iss: style::IcedStyleSheet, 64 | header: Header, 65 | stack_editor: StackEditor, 66 | analysis_state: AnalysisState, 67 | dirty: bool, 68 | saving: bool, 69 | file_path: Option, 70 | } 71 | impl Default for State { 72 | fn default() -> Self { 73 | State { 74 | last_save: std::time::Instant::now(), 75 | iss: style::IcedStyleSheet::default(), 76 | header: Header::default(), 77 | stack_editor: StackEditor::default(), 78 | analysis_state: AnalysisState::default(), 79 | dirty: false, 80 | saving: false, 81 | file_path: None, 82 | } 83 | } 84 | } 85 | impl State { 86 | /// Marks the state as having unsaved changes 87 | fn mark_unsaved_changes(&mut self) { 88 | self.dirty = true; 89 | } 90 | fn stack_is_not_empty(&self) -> bool { 91 | self.stack_editor 92 | .tolerances 93 | .iter() 94 | .filter(|x| x.active) 95 | .count() 96 | > 0 97 | } 98 | } 99 | 100 | // Messages - events for users to change the application state 101 | #[derive(Debug, Clone)] 102 | enum Message { 103 | // Subcomponent messages 104 | Header(HeaderAreaMessage), 105 | StackEditor(StackEditorAreaMessage), 106 | Analysis(AnalysisAreaMessage), 107 | // 108 | AutoSave, 109 | Loaded(Result<(Option, SavedState), io::saved_state::LoadError>), 110 | Saved(Result, io::saved_state::SaveError>), 111 | ExportComplete(Result<(), io::export_csv::SaveError>), 112 | EventOccurred(iced_native::Event), 113 | // 114 | StyleUpdateAvailable(bool), 115 | LoadedStyle(Result, style::LoadError>), 116 | StyleSaved(Result<(), style::SaveError>), 117 | // 118 | HelpOpened, 119 | } 120 | 121 | // Loading state wrapper 122 | #[derive(Debug)] 123 | enum TolStack { 124 | Loading, 125 | Loaded(Box), 126 | } 127 | impl Application for TolStack { 128 | type Executor = iced::executor::Default; 129 | type Message = Message; 130 | type Flags = (); 131 | 132 | fn new(_flags: ()) -> (TolStack, Command) { 133 | ( 134 | TolStack::Loading, 135 | Command::perform(SavedState::new(), Message::Loaded), 136 | //Command::perform(SavedState::load(), Message::Loaded), 137 | ) 138 | } 139 | 140 | fn title(&self) -> String { 141 | let dirty = match self { 142 | TolStack::Loading => false, 143 | TolStack::Loaded(state) => state.dirty, 144 | }; 145 | let project_name = match self { 146 | TolStack::Loading => String::from("Loading..."), 147 | TolStack::Loaded(state) => { 148 | if state.stack_editor.title.text.is_empty() { 149 | String::from("New Stack") 150 | } else { 151 | state.stack_editor.title.text.clone() 152 | } 153 | } 154 | }; 155 | let path_str = match self { 156 | TolStack::Loading => String::from(""), 157 | TolStack::Loaded(state) => match &state.file_path { 158 | Some(path) => match path.to_str() { 159 | Some(str) => format!(" - {}", String::from(str)), 160 | None => String::from(""), 161 | }, 162 | None => String::from(""), 163 | }, 164 | }; 165 | 166 | format!( 167 | " {}{}{} - TolStack Tolerance Analysis", 168 | project_name, 169 | if dirty { "*" } else { "" }, 170 | path_str, 171 | ) 172 | } 173 | 174 | // Update logic - how to react to messages sent through the application 175 | fn update(&mut self, message: Message) -> Command { 176 | let is_event = matches!(message, Message::EventOccurred(_)); 177 | if cfg!(debug_assertions) && !is_event { 178 | println!( 179 | "\n\n{}{}\n{:#?}", 180 | chrono::offset::Local::now(), 181 | " MESSAGE RECEIVED:".yellow(), 182 | message 183 | ); 184 | } 185 | match self { 186 | TolStack::Loading => { 187 | match message { 188 | // Take the loaded state and assign to the working state 189 | Message::Loaded(Ok((path, state))) => { 190 | *self = TolStack::Loaded(Box::new(State { 191 | stack_editor: StackEditor::new() 192 | .tolerances(state.tolerances) 193 | .title(state.name), 194 | header: Header::new(), 195 | analysis_state: AnalysisState::new() 196 | .set_inputs(state.n_iteration, state.assy_sigma), 197 | file_path: path, 198 | dirty: false, 199 | saving: false, 200 | ..State::default() 201 | })); 202 | 203 | if cfg!(debug_assertions) { 204 | return Command::perform( 205 | style::IcedStyleSheet::load(), 206 | Message::LoadedStyle, 207 | ); 208 | } else { 209 | return Command::none(); 210 | } 211 | } 212 | 213 | Message::Loaded(Err(_)) => { 214 | *self = TolStack::Loaded(Box::new(State { ..State::default() })); 215 | } 216 | _ => {} 217 | } 218 | 219 | Command::none() 220 | } 221 | 222 | TolStack::Loaded(state) => { 223 | match message { 224 | Message::EventOccurred(iced_native::Event::Keyboard(event)) => { 225 | if let keyboard::Event::KeyPressed { 226 | key_code, 227 | modifiers: _, 228 | } = event 229 | { 230 | if key_code == keyboard::KeyCode::Tab { 231 | for entry in &mut state.stack_editor.tolerances { 232 | match &mut entry.state { 233 | entry_tolerance::State::Idle { 234 | button_edit: _, 235 | button_move_up: _, 236 | button_move_down: _, 237 | } => {} 238 | entry_tolerance::State::Editing { form_tolentry } => { 239 | match &mut **form_tolentry { 240 | FormState::Linear { 241 | button_save: _, 242 | button_delete: _, 243 | description, 244 | dimension, 245 | tolerance_pos, 246 | tolerance_neg, 247 | sigma, 248 | } => { 249 | if description.is_focused() { 250 | *description = text_input::State::default(); 251 | *dimension = text_input::State::focused(); 252 | } else if dimension.is_focused() { 253 | *dimension = text_input::State::default(); 254 | *tolerance_pos = 255 | text_input::State::focused(); 256 | } else if tolerance_pos.is_focused() { 257 | *tolerance_pos = 258 | text_input::State::default(); 259 | *tolerance_neg = 260 | text_input::State::focused(); 261 | } else if tolerance_neg.is_focused() { 262 | *tolerance_neg = 263 | text_input::State::default(); 264 | *sigma = text_input::State::focused(); 265 | } else if sigma.is_focused() { 266 | *sigma = text_input::State::default(); 267 | *description = text_input::State::focused(); 268 | } 269 | } 270 | FormState::Float { 271 | button_save: _, 272 | button_delete: _, 273 | description, 274 | diameter_hole, 275 | diameter_pin, 276 | tolerance_hole_pos, 277 | tolerance_hole_neg, 278 | tolerance_pin_pos, 279 | tolerance_pin_neg, 280 | sigma, 281 | } => { 282 | if description.is_focused() { 283 | *description = text_input::State::default(); 284 | *diameter_hole = 285 | text_input::State::focused(); 286 | } else if diameter_hole.is_focused() { 287 | *diameter_hole = 288 | text_input::State::default(); 289 | *tolerance_hole_pos = 290 | text_input::State::focused(); 291 | } else if tolerance_hole_pos.is_focused() { 292 | *tolerance_hole_pos = 293 | text_input::State::default(); 294 | *tolerance_hole_neg = 295 | text_input::State::focused(); 296 | } else if tolerance_hole_neg.is_focused() { 297 | *tolerance_hole_neg = 298 | text_input::State::default(); 299 | *diameter_pin = 300 | text_input::State::focused(); 301 | } else if diameter_pin.is_focused() { 302 | *diameter_pin = 303 | text_input::State::default(); 304 | *tolerance_pin_pos = 305 | text_input::State::focused(); 306 | } else if tolerance_pin_pos.is_focused() { 307 | *tolerance_pin_pos = 308 | text_input::State::default(); 309 | *tolerance_pin_neg = 310 | text_input::State::focused(); 311 | } else if tolerance_pin_neg.is_focused() { 312 | *tolerance_pin_neg = 313 | text_input::State::default(); 314 | *sigma = text_input::State::focused(); 315 | } else if sigma.is_focused() { 316 | *sigma = text_input::State::default(); 317 | *description = text_input::State::focused(); 318 | } 319 | } 320 | } 321 | } 322 | } 323 | } 324 | } else { 325 | } 326 | } 327 | } 328 | Message::EventOccurred(_) => {} 329 | Message::AutoSave => { 330 | if let Some(path) = &state.file_path { 331 | state.saving = true; 332 | let save_data = SavedState { 333 | name: state.stack_editor.title.text.clone(), 334 | tolerances: state.stack_editor.tolerances.clone(), 335 | n_iteration: state.analysis_state.entry_form.n_iteration, 336 | assy_sigma: state.analysis_state.entry_form.assy_sigma, 337 | }; 338 | return Command::perform( 339 | SavedState::save(save_data, path.clone()), 340 | Message::Saved, 341 | ); 342 | } else { 343 | return Command::none(); 344 | } 345 | } 346 | Message::Header(area_header::HeaderAreaMessage::NewFile) => { 347 | return Command::perform(SavedState::new(), Message::Loaded) 348 | } 349 | Message::Header(area_header::HeaderAreaMessage::OpenFile) => { 350 | return Command::perform(SavedState::open(), Message::Loaded) 351 | } 352 | 353 | Message::Header(area_header::HeaderAreaMessage::SaveFile) => { 354 | let save_data = SavedState { 355 | name: state.stack_editor.title.text.clone(), 356 | tolerances: state.stack_editor.tolerances.clone(), 357 | n_iteration: state.analysis_state.entry_form.n_iteration, 358 | assy_sigma: state.analysis_state.entry_form.assy_sigma, 359 | }; 360 | 361 | match &state.file_path { 362 | Some(path) => { 363 | return Command::perform( 364 | SavedState::save(save_data, path.clone()), 365 | Message::Saved, 366 | ) 367 | } 368 | None => { 369 | return Command::perform( 370 | SavedState::save_as(save_data), 371 | Message::Saved, 372 | ) 373 | } 374 | }; 375 | } 376 | 377 | Message::Header(area_header::HeaderAreaMessage::SaveAsFile) => { 378 | let save_data = SavedState { 379 | name: state.stack_editor.title.text.clone(), 380 | tolerances: state.stack_editor.tolerances.clone(), 381 | n_iteration: state.analysis_state.entry_form.n_iteration, 382 | assy_sigma: state.analysis_state.entry_form.assy_sigma, 383 | }; 384 | 385 | return Command::perform(SavedState::save_as(save_data), Message::Saved); 386 | } 387 | 388 | Message::Header(area_header::HeaderAreaMessage::ExportCSV) => { 389 | return Command::perform( 390 | export_csv::serialize_csv( 391 | state.analysis_state.model_state.results.export(), 392 | ), 393 | Message::ExportComplete, 394 | ) 395 | } 396 | 397 | Message::Header(area_header::HeaderAreaMessage::AddTolLinear) => { 398 | state.mark_unsaved_changes(); 399 | state.stack_editor.update( 400 | area_stack_editor::StackEditorAreaMessage::NewEntryMessage(( 401 | String::from("New Linear Tolerance"), 402 | Tolerance::Linear(LinearTL::default()), 403 | )), 404 | ) 405 | } 406 | 407 | Message::Header(area_header::HeaderAreaMessage::AddTolFloat) => { 408 | state.mark_unsaved_changes(); 409 | state.stack_editor.update( 410 | area_stack_editor::StackEditorAreaMessage::NewEntryMessage(( 411 | String::from("New Float Tolerance"), 412 | Tolerance::Float(FloatTL::default()), 413 | )), 414 | ) 415 | } 416 | 417 | Message::Header(area_header::HeaderAreaMessage::Help) => { 418 | return Command::perform(help(), |_| Message::HelpOpened); 419 | } 420 | 421 | Message::HelpOpened => {} 422 | 423 | Message::ExportComplete(_) => {} 424 | 425 | Message::StackEditor(message) => { 426 | let recompute = matches!(message, area_stack_editor::StackEditorAreaMessage::LabelMessage( 427 | editable_label::Message::FinishEditing, 428 | ) | area_stack_editor::StackEditorAreaMessage::EntryMessage(_, _)); 429 | state.stack_editor.update(message); 430 | if recompute { 431 | state.mark_unsaved_changes(); 432 | return Command::perform(do_nothing(), |_| { 433 | Message::Analysis( 434 | area_mc_analysis::AnalysisAreaMessage::NewMcAnalysisMessage( 435 | form_new_mc_analysis::Message::Calculate, 436 | ), 437 | ) 438 | }); 439 | } 440 | } 441 | 442 | Message::Analysis( 443 | area_mc_analysis::AnalysisAreaMessage::NewMcAnalysisMessage( 444 | form_new_mc_analysis::Message::Calculate, 445 | ), 446 | ) => { 447 | if state.stack_is_not_empty() { 448 | // Clone the contents of the stack editor tolerance list into the monte 449 | // carlo simulation's input tolerance list. 450 | state.analysis_state.input_stack = 451 | state.stack_editor.tolerances.clone(); 452 | // Pass this message into the child so the computation gets kicked off. 453 | let calculate_message = 454 | area_mc_analysis::AnalysisAreaMessage::NewMcAnalysisMessage( 455 | form_new_mc_analysis::Message::Calculate, 456 | ); 457 | return state 458 | .analysis_state 459 | .update(calculate_message) 460 | .map(Message::Analysis); 461 | } 462 | } 463 | 464 | Message::Analysis(message) => { 465 | // TODO collect commands and run at end instead of breaking at match arm. 466 | return state.analysis_state.update(message).map(Message::Analysis); 467 | } 468 | 469 | Message::StyleUpdateAvailable(_) => { 470 | return Command::perform( 471 | style::IcedStyleSheet::load(), 472 | Message::LoadedStyle, 473 | ) 474 | } 475 | 476 | Message::LoadedStyle(Ok(iss)) => { 477 | state.iss = *iss; 478 | } 479 | 480 | Message::LoadedStyle(Err(style::LoadError::FormatError)) => println!( 481 | "\n\n{}{}", 482 | chrono::offset::Local::now(), 483 | " Error loading style file".red() 484 | ), 485 | 486 | Message::LoadedStyle(Err(style::LoadError::FileError)) => { 487 | return Command::perform( 488 | style::IcedStyleSheet::save(state.iss.clone()), 489 | Message::StyleSaved, 490 | ) 491 | } 492 | 493 | Message::StyleSaved(_) => {} 494 | 495 | Message::Saved(save_result) => { 496 | state.saving = false; 497 | match save_result { 498 | Ok(path_result) => { 499 | if let Some(path) = path_result { 500 | state.file_path = Some(path); 501 | state.last_save = std::time::Instant::now(); 502 | } 503 | state.dirty = false; 504 | } 505 | 506 | Err(e) => { 507 | state.dirty = true; 508 | println!("Save failed with {:?}", e); 509 | } 510 | } 511 | } 512 | 513 | Message::Loaded(Ok((path, save_state))) => { 514 | *state = Box::new(State { 515 | stack_editor: StackEditor::new() 516 | .tolerances(save_state.tolerances) 517 | .title(save_state.name), 518 | header: Header::new(), 519 | analysis_state: AnalysisState::new() 520 | .set_inputs(save_state.n_iteration, save_state.assy_sigma), 521 | file_path: path, 522 | dirty: false, 523 | saving: false, 524 | ..State::default() 525 | }); 526 | } 527 | 528 | Message::Loaded(Err(_)) => println!( 529 | "\n\n{}{}", 530 | chrono::offset::Local::now(), 531 | " Error loading save file".red() 532 | ), 533 | } 534 | 535 | Command::none() 536 | } 537 | } 538 | } 539 | 540 | fn subscription(&self) -> Subscription { 541 | match self { 542 | TolStack::Loading => Subscription::none(), 543 | TolStack::Loaded(state) => { 544 | let State { 545 | last_save: _, 546 | iss, 547 | header: _, 548 | stack_editor: _, 549 | analysis_state: _, 550 | dirty, 551 | saving, 552 | file_path, 553 | } = &**state; 554 | let auto_save = if *dirty && !saving && file_path.is_some() 555 | //&& last_save.elapsed().as_secs() > 5 556 | { 557 | time::every(std::time::Duration::from_secs(5)).map(|_| Message::AutoSave) 558 | } else { 559 | Subscription::none() 560 | }; 561 | let style_reload = if cfg!(debug_assertions) { 562 | iss.check_style_file().map(Message::StyleUpdateAvailable) 563 | } else { 564 | Subscription::none() 565 | }; 566 | let tab_field = iced_native::subscription::events().map(Message::EventOccurred); 567 | 568 | Subscription::batch(vec![auto_save, style_reload, tab_field]) 569 | } 570 | } 571 | } 572 | 573 | // View logic - a way to display the state of the application as widgets that can produce messages 574 | fn view(&mut self) -> Element { 575 | match self { 576 | TolStack::Loading => loading_message(), 577 | TolStack::Loaded(state) => { 578 | let State { 579 | last_save: _, 580 | iss, 581 | header, 582 | stack_editor, 583 | analysis_state, 584 | dirty: _, 585 | saving: _, 586 | file_path: _, 587 | } = &mut **state; 588 | let header = header.view(&iss).map(Message::Header); 589 | 590 | let stack_editor = stack_editor.view(&iss).map(Message::StackEditor); 591 | 592 | let analysis_state = analysis_state.view(&iss).map(Message::Analysis); 593 | 594 | let content = Column::new().push( 595 | Row::new() 596 | .push( 597 | Container::new(stack_editor) 598 | .padding(iss.padding(&iss.home_padding)) 599 | .width(Length::Fill), 600 | ) 601 | .push(Container::new(analysis_state).width(Length::Units(400))), 602 | ); 603 | 604 | let gui: Element<_> = Container::new(Column::new().push(header).push(content)) 605 | .style(iss.container(&iss.home_container)) 606 | .into(); 607 | 608 | gui.explain(iced::Color::BLACK) 609 | } 610 | } 611 | } 612 | } 613 | 614 | fn loading_message() -> Element<'static, Message> { 615 | Container::new( 616 | Text::new("Loading...") 617 | .horizontal_alignment(HorizontalAlignment::Center) 618 | .size(50), 619 | ) 620 | .width(Length::Fill) 621 | .height(Length::Fill) 622 | .center_y() 623 | .center_x() 624 | .into() 625 | } 626 | 627 | async fn help() { 628 | webbrowser::open("https://aevyrie.github.io/tolstack/book/getting-started").unwrap(); 629 | } 630 | 631 | async fn do_nothing() {} 632 | -------------------------------------------------------------------------------- /src/ui/components.rs: -------------------------------------------------------------------------------- 1 | // Define submodules of `components` 2 | pub mod area_header; 3 | pub mod area_mc_analysis; 4 | pub mod area_stack_editor; 5 | pub mod editable_label; 6 | pub mod entry_tolerance; 7 | pub mod filter_tolerance; 8 | pub mod form_new_mc_analysis; 9 | //pub mod form_new_tolerance; 10 | 11 | // Re-export components for easier use in main.rs 12 | pub use area_header::*; 13 | pub use area_mc_analysis::*; 14 | pub use area_stack_editor::*; 15 | pub use editable_label::*; 16 | pub use entry_tolerance::*; 17 | pub use filter_tolerance::*; 18 | pub use form_new_mc_analysis::*; 19 | //pub use form_new_tolerance::*; 20 | -------------------------------------------------------------------------------- /src/ui/components/area_header.rs: -------------------------------------------------------------------------------- 1 | use crate::ui::{icons, style}; 2 | use iced::{ 3 | button, Button, Column, Container, Element, HorizontalAlignment, Length, Row, Text, 4 | VerticalAlignment, 5 | }; 6 | 7 | #[derive(Debug, Clone)] 8 | pub enum HeaderAreaMessage { 9 | NewFile, 10 | OpenFile, 11 | SaveFile, 12 | SaveAsFile, 13 | ExportCSV, 14 | AddTolLinear, 15 | AddTolFloat, 16 | Help, 17 | } 18 | 19 | #[derive(Debug, Default, Clone)] 20 | pub struct Header { 21 | button_new: button::State, 22 | button_open: button::State, 23 | button_save: button::State, 24 | button_export: button::State, 25 | button_save_as: button::State, 26 | button_add_tol_linear: button::State, 27 | button_add_tol_float: button::State, 28 | button_help: button::State, 29 | } 30 | impl Header { 31 | pub fn new() -> Self { 32 | Header { 33 | button_new: button::State::new(), 34 | button_open: button::State::new(), 35 | button_save: button::State::new(), 36 | button_export: button::State::new(), 37 | button_save_as: button::State::new(), 38 | button_add_tol_linear: button::State::new(), 39 | button_add_tol_float: button::State::new(), 40 | button_help: button::State::new(), 41 | } 42 | } 43 | /* 44 | pub fn update(&mut self, message: Message) { 45 | let Header { 46 | button_new: _, 47 | button_open: _, 48 | button_save: _, 49 | button_export: _, 50 | button_save_as: _, 51 | button_add_tol_linear: _, 52 | button_add_tol_float: _, 53 | button_help: _, 54 | } = self; 55 | match message { 56 | _ => (), // This message is captured in main.rs 57 | } 58 | } 59 | */ 60 | pub fn view(&mut self, iss: &style::IcedStyleSheet) -> Element { 61 | let Header { 62 | button_new, 63 | button_open, 64 | button_save, 65 | button_export, 66 | button_save_as, 67 | button_add_tol_linear, 68 | button_add_tol_float, 69 | button_help, 70 | } = self; 71 | 72 | let button_new = header_button(button_new, "New\n", icons::new(), iss) 73 | .on_press(HeaderAreaMessage::NewFile); 74 | 75 | let button_open = header_button(button_open, "Open\n", icons::load(), iss) 76 | .on_press(HeaderAreaMessage::OpenFile); 77 | 78 | let button_save = header_button(button_save, "Save\n", icons::save(), iss) 79 | .on_press(HeaderAreaMessage::SaveFile); 80 | 81 | let button_save_as = header_button(button_save_as, "Save As\n", icons::duplicate(), iss) 82 | .on_press(HeaderAreaMessage::SaveAsFile); 83 | 84 | let button_export = header_button(button_export, "Export CSV", icons::export(), iss) 85 | .on_press(HeaderAreaMessage::ExportCSV); 86 | 87 | let button_add_tol_linear = 88 | header_button(button_add_tol_linear, "Add Linear\n", icons::add(), iss) 89 | .on_press(HeaderAreaMessage::AddTolLinear); 90 | 91 | let button_add_tol_float = 92 | header_button(button_add_tol_float, "Add Float\n", icons::add(), iss) 93 | .on_press(HeaderAreaMessage::AddTolFloat); 94 | 95 | let button_help = header_button(button_help, "Help\n", icons::help(), iss) 96 | .on_press(HeaderAreaMessage::Help); 97 | 98 | let ribbon = Container::new( 99 | Row::new() 100 | .push(button_new) 101 | .push(button_open) 102 | .push(button_save) 103 | .push(button_save_as) 104 | .push(button_export) 105 | .push(button_add_tol_linear) 106 | .push(button_add_tol_float) 107 | .push(button_help) 108 | .width(Length::Fill) 109 | .spacing(iss.spacing(&iss.header_button_external_spacing)), 110 | ) 111 | .width(Length::Fill) 112 | .padding(iss.padding(&iss.header_button_padding)) 113 | .style(iss.container(&iss.header_menu_container)); 114 | 115 | let header = Column::new() 116 | .push(ribbon) 117 | //.push( 118 | // Container::new(Column::new().push(project_title_container).max_width(800)) 119 | // .width(Length::Fill) 120 | // .padding(10) 121 | // .center_x(), 122 | //) 123 | .spacing(iss.spacing(&iss.header_spacing)); 124 | 125 | header.into() 126 | } 127 | } 128 | 129 | fn header_button<'a>( 130 | state: &'a mut button::State, 131 | text: &str, 132 | icon: Text, 133 | iss: &style::IcedStyleSheet, 134 | ) -> Button<'a, HeaderAreaMessage> { 135 | Button::new( 136 | state, 137 | Column::new() 138 | .spacing(iss.spacing(&iss.header_button_internal_spacing)) 139 | .push( 140 | Container::new(icon.size(iss.text_size(&iss.header_button_icon_size))) 141 | .center_x() 142 | .center_y() 143 | //.height(Length::Fill) 144 | .width(Length::Fill), 145 | ) 146 | .push( 147 | Container::new( 148 | Text::new(text) 149 | .width(Length::Fill) 150 | .size(iss.text_size(&iss.header_button_text_size)) 151 | .horizontal_alignment(HorizontalAlignment::Center) 152 | .vertical_alignment(VerticalAlignment::Center), 153 | ) 154 | .center_x() 155 | .center_y() 156 | .width(Length::Fill), 157 | ), 158 | ) 159 | .style(iss.button(&iss.header_button_style)) 160 | .height(iss.dimension(&iss.header_button_height)) 161 | .width(iss.dimension(&iss.header_button_width)) 162 | } 163 | -------------------------------------------------------------------------------- /src/ui/components/area_mc_analysis.rs: -------------------------------------------------------------------------------- 1 | use crate::analysis::{monte_carlo, root_sum_square, structures}; 2 | use crate::ui::{components::*, style}; 3 | use iced::{Column, Command, Container, Element, Length, Row, Text}; 4 | 5 | #[derive(Debug, Clone)] 6 | pub enum AnalysisAreaMessage { 7 | NewMcAnalysisMessage(form_new_mc_analysis::Message), 8 | CalculateComplete(Option), 9 | //RunRssCalcs(form_new_mc_analysis::Message), 10 | //RunMonteCarloCalcs(form_new_mc_analysis::Message), 11 | //RssCalcComplete(Option), 12 | //MonteCarloCalcComplete(Option), 13 | } 14 | 15 | #[derive(Debug, Default, Clone)] 16 | pub struct AnalysisState { 17 | pub entry_form: NewMonteCarloAnalysis, 18 | pub model_state: structures::State, 19 | pub input_stack: Vec, 20 | } 21 | impl AnalysisState { 22 | pub fn new() -> Self { 23 | AnalysisState::default() 24 | } 25 | pub fn update(&mut self, message: AnalysisAreaMessage) -> Command { 26 | let AnalysisState { 27 | entry_form, 28 | model_state, 29 | input_stack: _, 30 | } = self; 31 | match message { 32 | AnalysisAreaMessage::NewMcAnalysisMessage(form_new_mc_analysis::Message::Calculate) => { 33 | let simulation_input = self.build_stack(); 34 | if let Some(stack) = simulation_input { 35 | return Command::perform( 36 | AnalysisState::compute(stack), 37 | AnalysisAreaMessage::CalculateComplete, 38 | ); 39 | } 40 | } 41 | AnalysisAreaMessage::NewMcAnalysisMessage(message) => { 42 | entry_form.update(message); 43 | } 44 | AnalysisAreaMessage::CalculateComplete(result) => { 45 | if let Some(result) = result { 46 | model_state.results = result 47 | } 48 | } 49 | } 50 | Command::none() 51 | } 52 | pub fn view(&mut self, iss: &style::IcedStyleSheet) -> Element { 53 | let AnalysisState { 54 | entry_form, 55 | model_state, 56 | input_stack: _, 57 | } = self; 58 | let mc_default = structures::McResults::default(); 59 | let rss_default = structures::RssResults::default(); 60 | 61 | let mc_results = match model_state.results.monte_carlo() { 62 | Some(mc_result) => mc_result, 63 | None => &mc_default, 64 | }; 65 | 66 | let rss_results = match model_state.results.rss() { 67 | Some(rss_results) => rss_results, 68 | None => &rss_default, 69 | }; 70 | 71 | let results_body = Column::new() 72 | .push( 73 | Row::new() 74 | .push(Text::new("Mean:").size(iss.text_size(&iss.results))) 75 | .push( 76 | Text::new(format!("{:.2}", mc_results.mean)) 77 | .size(iss.text_size(&iss.results)), 78 | ) 79 | .spacing(iss.spacing(&iss.mc_results_row_spacing)), 80 | ) 81 | .push( 82 | Row::new() 83 | .push(Text::new("Tolerance (+):").size(iss.text_size(&iss.results))) 84 | .push( 85 | Text::new(format!("{:.2}", mc_results.tolerance_pos)) 86 | .size(iss.text_size(&iss.results)), 87 | ) 88 | .spacing(iss.spacing(&iss.mc_results_row_spacing)), 89 | ) 90 | .push( 91 | Row::new() 92 | .push(Text::new("Tolerance (-):").size(iss.text_size(&iss.results))) 93 | .push( 94 | Text::new(format!("{:.2}", mc_results.tolerance_neg)) 95 | .size(iss.text_size(&iss.results)), 96 | ) 97 | .spacing(iss.spacing(&iss.mc_results_row_spacing)), 98 | ) 99 | .push( 100 | Row::new() 101 | .push(Text::new("Standard Deviation (+):").size(iss.text_size(&iss.results))) 102 | .push( 103 | Text::new(format!("{:.2}", mc_results.stddev_pos)) 104 | .size(iss.text_size(&iss.results)), 105 | ) 106 | .spacing(iss.spacing(&iss.mc_results_row_spacing)), 107 | ) 108 | .push( 109 | Row::new() 110 | .push(Text::new("Standard Deviation (-):").size(iss.text_size(&iss.results))) 111 | .push( 112 | Text::new(format!("{:.2}", mc_results.stddev_neg)) 113 | .size(iss.text_size(&iss.results)), 114 | ) 115 | .spacing(iss.spacing(&iss.mc_results_row_spacing)), 116 | ) 117 | .push( 118 | Row::new() 119 | .push(Text::new("Iterations:").size(iss.text_size(&iss.results))) 120 | .push( 121 | Text::new(format!("{}", mc_results.iterations)) 122 | .size(iss.text_size(&iss.results)), 123 | ) 124 | .spacing(iss.spacing(&iss.mc_results_row_spacing)), 125 | ) 126 | .push( 127 | Row::new() 128 | .push(Text::new("Worst Case Lower:").size(iss.text_size(&iss.results))) 129 | .push( 130 | Text::new(format!("{:.2}", mc_results.worst_case_lower)) 131 | .size(iss.text_size(&iss.results)), 132 | ) 133 | .spacing(iss.spacing(&iss.mc_results_row_spacing)), 134 | ) 135 | .push( 136 | Row::new() 137 | .push(Text::new("Worst Case Upper:").size(iss.text_size(&iss.results))) 138 | .push( 139 | Text::new(format!("{:.2}", mc_results.worst_case_upper)) 140 | .size(iss.text_size(&iss.results)), 141 | ) 142 | .spacing(iss.spacing(&iss.mc_results_row_spacing)), 143 | ) 144 | .push( 145 | Row::new() 146 | .push(Text::new("RSS Mean:").size(iss.text_size(&iss.results))) 147 | .push( 148 | Text::new(format!("{:.2}", rss_results.mean())) 149 | .size(iss.text_size(&iss.results)), 150 | ) 151 | .spacing(iss.spacing(&iss.mc_results_row_spacing)), 152 | ) 153 | .push( 154 | Row::new() 155 | .push(Text::new("RSS Tolerance (+):").size(iss.text_size(&iss.results))) 156 | .push( 157 | Text::new(format!("{:.2}", rss_results.tolerance_pos())) 158 | .size(iss.text_size(&iss.results)), 159 | ) 160 | .spacing(iss.spacing(&iss.mc_results_row_spacing)), 161 | ) 162 | .push( 163 | Row::new() 164 | .push(Text::new("RSS Tolerance (-):").size(iss.text_size(&iss.results))) 165 | .push( 166 | Text::new(format!("{:.2}", rss_results.tolerance_neg())) 167 | .size(iss.text_size(&iss.results)), 168 | ) 169 | .spacing(iss.spacing(&iss.mc_results_row_spacing)), 170 | ) 171 | .spacing(iss.spacing(&iss.mc_results_col_spacing)) 172 | .height(Length::Fill); 173 | 174 | let results_summary = Container::new( 175 | Column::new() 176 | .push( 177 | entry_form 178 | .view(&iss) 179 | .map(AnalysisAreaMessage::NewMcAnalysisMessage), 180 | ) 181 | .push(results_body) 182 | .height(Length::Fill) 183 | .spacing(iss.spacing(&iss.mc_results_col_spacing)), 184 | ) 185 | .padding(10); 186 | 187 | let tol_chain_output = Column::new() 188 | .push( 189 | Container::new(results_summary) 190 | .style(iss.container(&iss.panel_container)) 191 | .padding(iss.padding(&iss.mc_results_container_inner_padding)) 192 | .height(Length::Fill) 193 | .center_x(), 194 | ) 195 | .height(Length::Fill) 196 | .width(Length::Fill); 197 | 198 | tol_chain_output.into() 199 | } 200 | 201 | fn build_stack(&mut self) -> Option { 202 | // Wipe the simulation input (tolerance stack) and build a new one 203 | self.model_state.clear_inputs(); 204 | // Copy over the input parameterscalculate_message 205 | self.model_state.parameters.n_iterations = self.entry_form.n_iteration; 206 | self.model_state.parameters.assy_sigma = self.entry_form.assy_sigma; 207 | // Make sure all active entries are valid 208 | let mut valid = true; 209 | for entry in &self.input_stack { 210 | if entry.active && !entry.valid { 211 | valid = false; 212 | } 213 | } 214 | // Build the tolerance stack 215 | if valid { 216 | for entry in &self.input_stack { 217 | if entry.active { 218 | self.model_state.add(entry.analysis_model) 219 | } 220 | } 221 | Some(self.model_state.clone()) 222 | } else { 223 | None 224 | } 225 | } 226 | 227 | /// Takes a monte carlo simulatio state, constructs a new tolerance model, and runs the simulation 228 | async fn compute(simulation: structures::State) -> Option { 229 | use std::time::Instant; 230 | // Each computation contains an owned simulation state, this allows multiple 231 | // computations to be spawned independently, and run asynchronously 232 | let time_start = Instant::now(); 233 | //todo - change unwrap to a match to prevent panic 234 | let mc_result = monte_carlo::run(&simulation).await.unwrap(); 235 | let rss_result = root_sum_square::run(&simulation).await.unwrap(); 236 | let duration = time_start.elapsed(); 237 | println!("Simulation Duration: {:.3?}", duration,); 238 | let result = (mc_result, rss_result).into(); 239 | Some(result) 240 | } 241 | 242 | pub fn set_inputs(&mut self, n_iterations: usize, assy_sigma: f64) -> Self { 243 | self.entry_form.n_iteration = n_iterations; 244 | self.entry_form.assy_sigma = assy_sigma; 245 | self.clone() 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/ui/components/area_stack_editor.rs: -------------------------------------------------------------------------------- 1 | use crate::analysis::structures::*; 2 | use crate::ui::components::*; 3 | use crate::ui::style; 4 | use arrow::Arrow; 5 | use iced::{ 6 | scrollable, Align, Column, Container, Element, HorizontalAlignment, Length, Row, Scrollable, 7 | Text, 8 | }; 9 | 10 | #[derive(Debug, Clone)] 11 | pub enum StackEditorAreaMessage { 12 | EntryMessage(usize, entry_tolerance::Message), 13 | FilterMessage(filter_tolerance::Message), 14 | NewEntryMessage((String, Tolerance)), 15 | LabelMessage(editable_label::Message), 16 | } 17 | 18 | #[derive(Debug, Default, Clone)] 19 | pub struct StackEditor { 20 | filter: ToleranceFilter, 21 | pub tolerances: Vec, 22 | scroll_state: scrollable::State, 23 | pub title: EditableLabel, 24 | } 25 | impl StackEditor { 26 | pub fn new() -> Self { 27 | StackEditor { 28 | title: EditableLabel::new("New Stack", "Add a name..."), 29 | ..Default::default() 30 | } 31 | } 32 | pub fn title(&mut self, title: String) -> Self { 33 | self.title.text = title; 34 | self.clone() 35 | } 36 | pub fn update(&mut self, message: StackEditorAreaMessage) { 37 | let StackEditor { 38 | filter, 39 | tolerances, 40 | scroll_state: _, 41 | title, 42 | } = self; 43 | match message { 44 | StackEditorAreaMessage::NewEntryMessage(tolerance) => { 45 | let (name, tol) = tolerance; 46 | tolerances.push(ToleranceEntry::new(name, tol).with_editing()); 47 | } 48 | 49 | StackEditorAreaMessage::FilterMessage(message) => { 50 | match &message { 51 | filter_tolerance::Message::FilterChanged(_) => {} 52 | }; 53 | // Once we've processed the filter message in the parent component, pass the 54 | // message into the filter to be processed. 55 | filter.update(message); 56 | } 57 | 58 | StackEditorAreaMessage::EntryMessage(i, message) => { 59 | // Some message `tol_message` from a tolerance entry at index `i` 60 | match &message { 61 | entry_tolerance::Message::EntryDelete => { 62 | tolerances.remove(i); 63 | } 64 | entry_tolerance::Message::EntryMoveUp => { 65 | if i > 0 { 66 | tolerances.swap(i, i - 1) 67 | } 68 | } 69 | entry_tolerance::Message::EntryMoveDown => { 70 | if i < tolerances.len() - 1 { 71 | tolerances.swap(i, i + 1) 72 | } 73 | } 74 | entry_tolerance::Message::EntryFinishEditing => { 75 | if let Some(entry) = tolerances.get_mut(i) { 76 | match &entry.input { 77 | FormValues::Linear { 78 | description: _, 79 | dimension, 80 | tolerance_pos, 81 | tolerance_neg, 82 | sigma, 83 | } => { 84 | let mut sanitized_dimension = 0.0; 85 | let mut sanitized_tolerance_pos = 0.0; 86 | let mut sanitized_tolerance_neg = 0.0; 87 | let mut sanitized_sigma = 0.0; 88 | 89 | entry.valid = true; 90 | 91 | match dimension.parse::() { 92 | Ok(value) => { 93 | sanitized_dimension = value; 94 | } 95 | Err(_) => { 96 | entry.valid = false; 97 | } 98 | } 99 | match tolerance_pos.parse::() { 100 | Ok(value) => { 101 | sanitized_tolerance_pos = value; 102 | } 103 | Err(_) => { 104 | entry.valid = false; 105 | } 106 | } 107 | match tolerance_neg.parse::() { 108 | Ok(value) => { 109 | sanitized_tolerance_neg = value; 110 | } 111 | Err(_) => { 112 | entry.valid = false; 113 | } 114 | } 115 | match sigma.parse::() { 116 | Ok(value) => { 117 | sanitized_sigma = value; 118 | } 119 | Err(_) => { 120 | entry.valid = false; 121 | } 122 | } 123 | if entry.valid { 124 | entry.active = true; 125 | let linear = DimTol::new_normal( 126 | sanitized_dimension, 127 | sanitized_tolerance_pos, 128 | sanitized_tolerance_neg, 129 | sanitized_sigma, 130 | ); 131 | let linear = Tolerance::Linear(LinearTL::new(linear)); 132 | entry.analysis_model = linear; 133 | } else { 134 | entry.active = false; 135 | } 136 | } 137 | FormValues::Float { 138 | description: _, 139 | diameter_hole, 140 | diameter_pin, 141 | tolerance_hole_pos, 142 | tolerance_hole_neg, 143 | tolerance_pin_pos, 144 | tolerance_pin_neg, 145 | sigma, 146 | } => { 147 | let mut sanitized_diameter_hole = 0.0; 148 | let mut sanitized_diameter_pin = 0.0; 149 | let mut sanitized_tolerance_hole_pos = 0.0; 150 | let mut sanitized_tolerance_hole_neg = 0.0; 151 | let mut sanitized_tolerance_pin_pos = 0.0; 152 | let mut sanitized_tolerance_pin_neg = 0.0; 153 | let mut sanitized_sigma = 0.0; 154 | 155 | entry.valid = true; 156 | match diameter_hole.parse::() { 157 | Ok(value) => { 158 | sanitized_diameter_hole = value; 159 | } 160 | Err(_) => { 161 | entry.valid = false; 162 | } 163 | } 164 | match diameter_pin.parse::() { 165 | Ok(value) => { 166 | sanitized_diameter_pin = value; 167 | } 168 | Err(_) => { 169 | entry.valid = false; 170 | } 171 | } 172 | match tolerance_hole_pos.parse::() { 173 | Ok(value) => { 174 | sanitized_tolerance_hole_pos = value; 175 | } 176 | Err(_) => { 177 | entry.valid = false; 178 | } 179 | } 180 | match tolerance_hole_neg.parse::() { 181 | Ok(value) => { 182 | sanitized_tolerance_hole_neg = value; 183 | } 184 | Err(_) => { 185 | entry.valid = false; 186 | } 187 | } 188 | match tolerance_pin_pos.parse::() { 189 | Ok(value) => { 190 | sanitized_tolerance_pin_pos = value; 191 | } 192 | Err(_) => { 193 | entry.valid = false; 194 | } 195 | } 196 | match tolerance_pin_neg.parse::() { 197 | Ok(value) => { 198 | sanitized_tolerance_pin_neg = value; 199 | } 200 | Err(_) => { 201 | entry.valid = false; 202 | } 203 | } 204 | match sigma.parse::() { 205 | Ok(value) => { 206 | sanitized_sigma = value; 207 | } 208 | Err(_) => { 209 | entry.valid = false; 210 | } 211 | } 212 | if entry.valid { 213 | entry.active = true; 214 | let hole = DimTol::new_normal( 215 | sanitized_diameter_hole, 216 | sanitized_tolerance_hole_pos, 217 | sanitized_tolerance_hole_neg, 218 | sanitized_sigma, 219 | ); 220 | let pin = DimTol::new_normal( 221 | sanitized_diameter_pin, 222 | sanitized_tolerance_pin_pos, 223 | sanitized_tolerance_pin_neg, 224 | sanitized_sigma, 225 | ); 226 | let data = Tolerance::Float(FloatTL::new(hole, pin, 3.0)); 227 | //println!("{:#?}",data); 228 | entry.analysis_model = data; 229 | } 230 | } 231 | } 232 | } 233 | } 234 | _ => {} 235 | }; 236 | if let Some(tol) = tolerances.get_mut(i) { 237 | // Once we've processed the entry message in the parent component, pass the 238 | // message into the entry it came from to be processed, after checking that 239 | // the entry exists. 240 | tol.update(message); 241 | } 242 | } 243 | 244 | StackEditorAreaMessage::LabelMessage(label_message) => { 245 | // Pass the message into the title 246 | title.update(label_message); 247 | } 248 | } 249 | } 250 | pub fn view(&mut self, iss: &style::IcedStyleSheet) -> Element { 251 | let StackEditor { 252 | filter, 253 | tolerances, 254 | scroll_state: _, 255 | title, 256 | } = self; 257 | 258 | let filtered_tols = tolerances 259 | .iter() 260 | .filter(|tol| filter.filter_value.matches(tol.analysis_model)); 261 | 262 | // for each tolerance 263 | // add the dimension of each, and record the min and max across all iterations 264 | // this will set the width of the visualization 265 | // take the most negative value, and save the magnitude of this 266 | // for each tolerance, add this magnitude to the dimension, to get the ending location 267 | // save this for the next iteration, where this is the starting point of the next visuaal 268 | // the start and end coordinates of each bar are now available 269 | // push a spacer with a width = fillproportion(startpos) 270 | // push the visual with a width = fill proposrtion(endpos-startpos) 271 | 272 | let mut max = 0.0; 273 | let mut min = 0.0; 274 | let mut stack_total = 0.0; 275 | for tol in tolerances.iter() { 276 | if tol.active && tol.valid { 277 | stack_total += match tol.analysis_model { 278 | Tolerance::Linear(linear) => linear.distance.dim as f32, 279 | Tolerance::Float(_) => 0.0, 280 | }; 281 | min = f32::min(min, stack_total); 282 | max = f32::max(max, stack_total); 283 | } 284 | } 285 | 286 | let visualization_width = max - min; 287 | let mut start = min.abs(); 288 | let mut visualize_positions: Vec> = Vec::new(); 289 | 290 | for tol in tolerances.iter() { 291 | if tol.active && tol.valid { 292 | // could apply a log scale to the length here. 293 | let length = match tol.analysis_model { 294 | Tolerance::Linear(linear) => linear.distance.dim as f32, 295 | Tolerance::Float(_) => 0.0, 296 | }; 297 | let mut viz_start = start; 298 | // The bar needs to have a positive length 299 | let viz_length = length.abs(); 300 | let mut viz_direction = ArrowDirection::Right; 301 | if length < 0.0 { 302 | // Because its direction is flipped, we need to subtract the length so that 303 | // when it's plotted, it will display correctly. 304 | viz_start -= viz_length; 305 | viz_direction = ArrowDirection::Left; 306 | } 307 | visualize_positions.push(Some((viz_start, viz_length, viz_direction))); 308 | start += length; 309 | } else { 310 | visualize_positions.push(None); 311 | } 312 | } 313 | 314 | // Iterate over all tols, calling their .view() function and adding them to a column 315 | let tolerances: Element<_> = if filtered_tols.count() > 0 { 316 | self.tolerances 317 | .iter_mut() 318 | .enumerate() 319 | .filter(|(_, tol)| filter.filter_value.matches(tol.analysis_model)) 320 | .fold( 321 | Column::new().spacing(iss.spacing(&iss.editor_tol_spacing)), 322 | |column, (i, tol)| { 323 | column 324 | .push( 325 | Container::new( 326 | Column::new() 327 | .push( 328 | Container::new(tol.view(&iss).map(move |message| { 329 | // Take the message from the tolerance .view() and map it 330 | // to an `area_stack_editor` Message as an `EntryMessage` 331 | StackEditorAreaMessage::EntryMessage(i, message) 332 | })) 333 | .width(Length::FillPortion(2)), 334 | ) 335 | .push( 336 | Container::new(match visualize_positions[i] { 337 | Some(visualize_position) => { 338 | let spacer_1_len = 339 | (visualize_position.0 * 100.0).round() 340 | as u16; // TODO check the largest negative exponent to determine multiplier **before** rounding 341 | let dim_len = (visualize_position.1 * 100.0) 342 | .round() 343 | as u16; 344 | let spacer_2_len = 345 | ((visualization_width * 100.0).round() 346 | as u16) 347 | - spacer_1_len 348 | - dim_len; 349 | Container::new( 350 | Row::new() 351 | .push(if spacer_1_len > 0 { 352 | Container::new(Row::new()).width( 353 | Length::FillPortion( 354 | spacer_1_len, 355 | ), 356 | ) 357 | } else { 358 | Container::new(Row::new()) 359 | }) 360 | .push(if dim_len > 0 { 361 | Container::new( 362 | Row::new() 363 | .push(Arrow::new( 364 | 8, 365 | 4, 366 | visualize_position.2, 367 | iss.color( 368 | &iss.editor_arrow_color, 369 | ), 370 | )) 371 | .width(Length::Fill), 372 | ) 373 | .width(Length::FillPortion(dim_len)) 374 | .height(Length::Units(10)) 375 | //.style( 376 | // iss.container(&iss.visualization_container), 377 | //) 378 | } else { 379 | Container::new(Row::new()) 380 | .width(Length::Units(2)) 381 | .height(Length::Units(10)) 382 | .style(iss.container( 383 | &iss.visualization_container, 384 | )) 385 | }) 386 | .push(if spacer_2_len > 0 { 387 | Container::new(Row::new()).width( 388 | Length::FillPortion( 389 | spacer_2_len, 390 | ), 391 | ) 392 | } else { 393 | Container::new(Row::new()) 394 | }), 395 | ) 396 | .width(Length::FillPortion(1)) 397 | } 398 | None => Container::new(Row::new()) 399 | .width(Length::FillPortion(1)), 400 | }) 401 | .style(iss.container(&iss.tol_entry_viz_container)) 402 | .padding(iss.padding(&iss.tol_entry_padding)), 403 | ), 404 | ) 405 | .style(iss.container(&iss.tol_entry_container)) 406 | .padding(5), 407 | ) 408 | .spacing(iss.spacing(&iss.editor_content_spacing)) 409 | .align_items(Align::Center) 410 | }, 411 | ) 412 | .into() 413 | } else { 414 | empty_message(match filter.filter_value { 415 | Filter::All => "There are no tolerances in the stack yet.", 416 | Filter::Some(tol) => match tol { 417 | Tolerance::Linear(_) => "No linear tolerances in the stack.", 418 | Tolerance::Float(_) => "No float tolerances in the stack.", 419 | }, 420 | }) 421 | }; 422 | 423 | // For debug purposes: 424 | //let tolerances = tolerances.explain(iced::Color::BLACK); 425 | 426 | let content = Column::new() 427 | .spacing(iss.spacing(&iss.editor_tol_spacing)) 428 | .push(tolerances); 429 | 430 | /* 431 | let stack_title = Text::new("Tolerance Stack") 432 | .width(Length::Fill) 433 | .size(iss.text_size(&iss.editor_title_text_size)) 434 | .horizontal_alignment(HorizontalAlignment::Left); 435 | */ 436 | 437 | let stack_title = title.view(&iss).map(StackEditorAreaMessage::LabelMessage); 438 | 439 | let scrollable_content = Container::new( 440 | Scrollable::new(&mut self.scroll_state) 441 | .height(Length::Fill) 442 | .width(Length::Fill) 443 | .push( 444 | Container::new(content).padding(iss.padding(&iss.editor_scroll_area_padding)), 445 | ), 446 | ) 447 | .padding(iss.padding(&iss.editor_scroll_area_padding_correction)) 448 | .style(iss.container(&iss.editor_scroll_container)) 449 | .height(Length::Fill); 450 | 451 | let filter_controls = filter.view(&iss).map(StackEditorAreaMessage::FilterMessage); 452 | 453 | let tol_stack_area = Container::new( 454 | Column::new() 455 | .push( 456 | Row::new() 457 | .push(stack_title) 458 | .push(filter_controls) 459 | .align_items(Align::Center), 460 | ) 461 | .push(scrollable_content) 462 | .spacing(iss.spacing(&iss.editor_content_spacing)) 463 | .max_width(1000), 464 | ) 465 | .width(Length::Fill) 466 | .center_x(); 467 | 468 | /* 469 | let new_tol_area = Container::new( 470 | Container::new( 471 | self.entry_form 472 | .view(&iss) 473 | .map(move |message| Message::NewEntryMessage(message)), 474 | ) 475 | .padding(iss.padding(&iss.newtol_container_inner_padding)) 476 | .style(iss.container(&iss.panel_container)), 477 | ) 478 | .padding(iss.padding(&iss.newtol_container_outer_padding)) 479 | .width(Length::Fill) 480 | .center_x(); 481 | */ 482 | 483 | tol_stack_area.into() 484 | } 485 | pub fn tolerances(&mut self, tolerances: Vec) -> Self { 486 | self.tolerances = tolerances; 487 | self.clone() 488 | } 489 | } 490 | 491 | fn empty_message(message: &str) -> Element<'static, StackEditorAreaMessage> { 492 | Container::new( 493 | Text::new(message) 494 | //.width(Length::Fill) 495 | .size(25) 496 | .horizontal_alignment(HorizontalAlignment::Center) 497 | .color([0.7, 0.7, 0.7]), 498 | ) 499 | .width(Length::Fill) 500 | .height(Length::Units(200)) 501 | .center_y() 502 | .center_x() 503 | .into() 504 | } 505 | #[derive(Debug, Copy, Clone)] 506 | pub enum ArrowDirection { 507 | Left, 508 | Right, 509 | } 510 | 511 | mod arrow { 512 | // For now, to implement a custom native widget you will need to add 513 | // `iced_native` and `iced_wgpu` to your dependencies. 514 | // 515 | // Then, you simply need to define your widget type and implement the 516 | // `iced_native::Widget` trait with the `iced_wgpu::Renderer`. 517 | // 518 | // Of course, you can choose to make the implementation renderer-agnostic, 519 | // if you wish to, by creating your own `Renderer` trait, which could be 520 | // implemented by `iced_wgpu` and other renderers. 521 | use super::ArrowDirection; 522 | use iced_graphics::{triangle::*, Backend, Defaults, Primitive, Renderer}; 523 | use iced_native::{ 524 | layout, mouse, Element, Hasher, Layout, Length, Point, Size, Vector, Widget, 525 | }; 526 | 527 | pub struct Arrow { 528 | height: u16, 529 | line_width: u16, 530 | direction: ArrowDirection, 531 | color: [f32; 4], 532 | } 533 | 534 | impl Arrow { 535 | pub fn new( 536 | height: u16, 537 | line_width: u16, 538 | direction: ArrowDirection, 539 | color: iced::Color, 540 | ) -> Self { 541 | let color = color.into_linear(); 542 | Self { 543 | height, 544 | line_width, 545 | direction, 546 | color, 547 | } 548 | } 549 | } 550 | 551 | impl Widget> for Arrow 552 | where 553 | B: Backend, 554 | { 555 | fn width(&self) -> Length { 556 | Length::Fill 557 | } 558 | 559 | fn height(&self) -> Length { 560 | Length::Fill 561 | } 562 | 563 | fn layout(&self, _renderer: &Renderer, limits: &layout::Limits) -> layout::Node { 564 | let size = limits.width(Length::Fill).resolve(Size::ZERO); 565 | layout::Node::new(Size::new(size.width, f32::from(self.height))) 566 | } 567 | 568 | fn hash_layout(&self, state: &mut Hasher) { 569 | use std::hash::Hash; 570 | 571 | self.height.hash(state); 572 | } 573 | 574 | fn draw( 575 | &self, 576 | _renderer: &mut Renderer, 577 | _defaults: &Defaults, 578 | layout: Layout<'_>, 579 | _cursor_position: Point, 580 | _viewport: &iced::Rectangle, 581 | ) -> (Primitive, mouse::Interaction) { 582 | let color = self.color; 583 | let height = self.height as f32; 584 | let line_width = self.line_width as f32; 585 | ( 586 | Primitive::Group { 587 | primitives: vec![ 588 | match self.direction { 589 | ArrowDirection::Right => Primitive::Translate { 590 | translation: Vector::new(layout.bounds().x, layout.bounds().y), 591 | content: Box::new(Primitive::Mesh2D { 592 | buffers: Mesh2D { 593 | vertices: vec![ 594 | Vertex2D { 595 | position: [ 596 | 0.0, 597 | -(line_width / 2.0) + (height / 2.0), 598 | ], 599 | color, 600 | }, 601 | Vertex2D { 602 | position: [ 603 | layout.bounds().width - height, 604 | -(line_width / 2.0) + (height / 2.0), 605 | ], 606 | color, 607 | }, 608 | Vertex2D { 609 | position: [ 610 | 0.0, 611 | (line_width / 2.0) + (height / 2.0), 612 | ], 613 | color, 614 | }, 615 | Vertex2D { 616 | position: [ 617 | layout.bounds().width - height, 618 | (line_width / 2.0) + (height / 2.0), 619 | ], 620 | color, 621 | }, 622 | ], 623 | indices: vec![0, 1, 2, 2, 1, 3], 624 | }, 625 | size: Size::new(layout.bounds().width, height * 2.0), 626 | }), 627 | }, 628 | ArrowDirection::Left => Primitive::Translate { 629 | translation: Vector::new(layout.bounds().x, layout.bounds().y), 630 | content: Box::new(Primitive::Mesh2D { 631 | buffers: Mesh2D { 632 | vertices: vec![ 633 | Vertex2D { 634 | position: [ 635 | height, 636 | -(line_width / 2.0) + (height / 2.0), 637 | ], 638 | color, 639 | }, 640 | Vertex2D { 641 | position: [ 642 | layout.bounds().width, 643 | -(line_width / 2.0) + (height / 2.0), 644 | ], 645 | color, 646 | }, 647 | Vertex2D { 648 | position: [ 649 | height, 650 | (line_width / 2.0) + (height / 2.0), 651 | ], 652 | color, 653 | }, 654 | Vertex2D { 655 | position: [ 656 | layout.bounds().width, 657 | (line_width / 2.0) + (height / 2.0), 658 | ], 659 | color, 660 | }, 661 | ], 662 | indices: vec![0, 1, 2, 2, 1, 3], 663 | }, 664 | size: Size::new(layout.bounds().width, height * 2.0), 665 | }), 666 | }, 667 | }, 668 | match self.direction { 669 | ArrowDirection::Right => Primitive::Translate { 670 | translation: Vector::new(layout.bounds().x, layout.bounds().y), 671 | content: Box::new(Primitive::Mesh2D { 672 | buffers: Mesh2D { 673 | vertices: vec![ 674 | Vertex2D { 675 | position: [layout.bounds().width, height / 2.0], 676 | color, 677 | }, 678 | Vertex2D { 679 | position: [ 680 | layout.bounds().width as f32 - height, 681 | height, 682 | ], 683 | color, 684 | }, 685 | Vertex2D { 686 | position: [ 687 | layout.bounds().width as f32 - height, 688 | 0.0, 689 | ], 690 | color, 691 | }, 692 | ], 693 | indices: vec![1, 2, 0], 694 | }, 695 | size: Size::new(layout.bounds().width, height * 2.0), 696 | }), 697 | }, 698 | ArrowDirection::Left => Primitive::Translate { 699 | translation: Vector::new(layout.bounds().x, layout.bounds().y), 700 | content: Box::new(Primitive::Mesh2D { 701 | buffers: Mesh2D { 702 | vertices: vec![ 703 | Vertex2D { 704 | position: [0.0, height / 2.0], 705 | color, 706 | }, 707 | Vertex2D { 708 | position: [height, height], 709 | color, 710 | }, 711 | Vertex2D { 712 | position: [height, 0.0], 713 | color, 714 | }, 715 | ], 716 | indices: vec![1, 2, 0], 717 | }, 718 | size: Size::new(layout.bounds().width, height * 2.0), 719 | }), 720 | }, 721 | }, 722 | ], 723 | }, 724 | mouse::Interaction::default(), 725 | ) 726 | } 727 | } 728 | 729 | impl<'a, Message, B> Into>> for Arrow 730 | where 731 | B: Backend, 732 | { 733 | fn into(self) -> Element<'a, Message, Renderer> { 734 | Element::new(self) 735 | } 736 | } 737 | } 738 | -------------------------------------------------------------------------------- /src/ui/components/editable_label.rs: -------------------------------------------------------------------------------- 1 | use crate::ui::{icons, style}; 2 | use iced::{ 3 | button, text_input, Align, Button, Container, Element, HorizontalAlignment, Length, Row, Text, 4 | TextInput, 5 | }; 6 | use serde_derive::*; 7 | 8 | #[derive(Debug, Clone)] 9 | pub enum State { 10 | Idle { edit_button: button::State }, 11 | Editing { text_input: text_input::State }, 12 | } 13 | impl Default for State { 14 | fn default() -> Self { 15 | State::Idle { 16 | edit_button: button::State::new(), 17 | } 18 | } 19 | } 20 | 21 | #[derive(Debug, Clone)] 22 | pub enum Message { 23 | Edit, 24 | TextEdited(String), 25 | FinishEditing, 26 | } 27 | 28 | #[derive(Debug, Clone, Serialize, Deserialize)] 29 | pub struct EditableLabel { 30 | pub text: String, 31 | pub placeholder: String, 32 | #[serde(skip)] 33 | state: State, 34 | } 35 | impl EditableLabel { 36 | pub fn new, U: Into>(text: T, placeholder: U) -> Self { 37 | EditableLabel { 38 | text: text.into(), 39 | placeholder: placeholder.into(), 40 | state: State::Idle { 41 | edit_button: button::State::new(), 42 | }, 43 | } 44 | } 45 | 46 | pub fn update(&mut self, message: Message) { 47 | match message { 48 | Message::Edit => { 49 | self.state = State::Editing { 50 | text_input: text_input::State::focused(), 51 | }; 52 | } 53 | Message::TextEdited(new_text) => { 54 | self.text = new_text; 55 | } 56 | Message::FinishEditing => { 57 | if !self.text.is_empty() { 58 | self.state = State::Idle { 59 | edit_button: button::State::new(), 60 | } 61 | } 62 | } 63 | } 64 | } 65 | 66 | pub fn view(&mut self, iss: &style::IcedStyleSheet) -> Element { 67 | match &mut self.state { 68 | State::Idle { edit_button } => { 69 | let label = Text::new(self.text.clone()) 70 | .width(Length::Shrink) 71 | .size(iss.text_size(&iss.editor_title_text_size)) 72 | .horizontal_alignment(HorizontalAlignment::Left); 73 | 74 | let row_contents = Row::new() 75 | .spacing(10) 76 | .align_items(Align::Center) 77 | .push(label) 78 | .push( 79 | Button::new(edit_button, icons::edit().size(20)) 80 | .on_press(Message::Edit) 81 | .padding(10) 82 | .style(iss.button(&iss.button_icon)), 83 | ); 84 | 85 | Container::new(row_contents).width(Length::Fill).into() 86 | } 87 | State::Editing { text_input } => { 88 | let text_input = TextInput::new( 89 | text_input, 90 | &self.placeholder, 91 | &self.text, 92 | Message::TextEdited, 93 | ) 94 | .on_submit(Message::FinishEditing) 95 | .padding(10) 96 | .width(Length::Units(500)); 97 | 98 | let row_contents = Row::new() 99 | .spacing(20) 100 | .align_items(Align::Center) 101 | .push(text_input); 102 | Container::new(row_contents).width(Length::Fill).into() 103 | } 104 | } 105 | } 106 | } 107 | impl Default for EditableLabel { 108 | fn default() -> Self { 109 | EditableLabel { 110 | text: String::from(""), 111 | placeholder: String::from("Enter some text..."), 112 | state: State::default(), 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/ui/components/entry_tolerance.rs: -------------------------------------------------------------------------------- 1 | use crate::analysis::structures::*; 2 | use crate::ui::{icons, style}; 3 | use iced::{ 4 | button, text_input, Align, Button, Checkbox, Column, Container, Element, HorizontalAlignment, 5 | Length, Row, Text, TextInput, 6 | }; 7 | use serde_derive::*; 8 | 9 | #[derive(Debug, Clone)] 10 | pub enum State { 11 | Idle { 12 | button_edit: button::State, 13 | button_move_up: button::State, 14 | button_move_down: button::State, 15 | }, 16 | Editing { 17 | form_tolentry: Box, 18 | }, 19 | } 20 | impl Default for State { 21 | fn default() -> Self { 22 | State::Idle { 23 | button_edit: button::State::new(), 24 | button_move_up: button::State::new(), 25 | button_move_down: button::State::new(), 26 | } 27 | } 28 | } 29 | 30 | #[allow(clippy::clippy::large_enum_variant)] 31 | #[derive(Debug, Clone)] 32 | pub enum FormState { 33 | Linear { 34 | button_save: button::State, 35 | button_delete: button::State, 36 | description: text_input::State, 37 | dimension: text_input::State, 38 | tolerance_pos: text_input::State, 39 | tolerance_neg: text_input::State, 40 | sigma: text_input::State, 41 | }, 42 | Float { 43 | button_save: button::State, 44 | button_delete: button::State, 45 | description: text_input::State, 46 | diameter_hole: text_input::State, 47 | diameter_pin: text_input::State, 48 | tolerance_hole_pos: text_input::State, 49 | tolerance_hole_neg: text_input::State, 50 | tolerance_pin_pos: text_input::State, 51 | tolerance_pin_neg: text_input::State, 52 | sigma: text_input::State, 53 | }, 54 | } 55 | impl FormState { 56 | pub fn new(form_type: Tolerance) -> Self { 57 | match form_type { 58 | Tolerance::Linear(_) => FormState::Linear { 59 | button_save: button::State::new(), 60 | button_delete: button::State::new(), 61 | description: text_input::State::new(), 62 | dimension: text_input::State::new(), 63 | tolerance_pos: text_input::State::new(), 64 | tolerance_neg: text_input::State::new(), 65 | sigma: text_input::State::new(), 66 | }, 67 | Tolerance::Float(_) => FormState::Float { 68 | button_save: button::State::new(), 69 | button_delete: button::State::new(), 70 | description: text_input::State::new(), 71 | diameter_hole: text_input::State::new(), 72 | diameter_pin: text_input::State::new(), 73 | tolerance_hole_pos: text_input::State::new(), 74 | tolerance_hole_neg: text_input::State::new(), 75 | tolerance_pin_pos: text_input::State::new(), 76 | tolerance_pin_neg: text_input::State::new(), 77 | sigma: text_input::State::new(), 78 | }, 79 | } 80 | } 81 | pub fn new_focused(form_type: Tolerance) -> Self { 82 | match form_type { 83 | Tolerance::Linear(_) => FormState::Linear { 84 | button_save: button::State::new(), 85 | button_delete: button::State::new(), 86 | description: text_input::State::focused(), 87 | dimension: text_input::State::new(), 88 | tolerance_pos: text_input::State::new(), 89 | tolerance_neg: text_input::State::new(), 90 | sigma: text_input::State::new(), 91 | }, 92 | Tolerance::Float(_) => FormState::Float { 93 | button_save: button::State::new(), 94 | button_delete: button::State::new(), 95 | description: text_input::State::focused(), 96 | diameter_hole: text_input::State::new(), 97 | diameter_pin: text_input::State::new(), 98 | tolerance_hole_pos: text_input::State::new(), 99 | tolerance_hole_neg: text_input::State::new(), 100 | tolerance_pin_pos: text_input::State::new(), 101 | tolerance_pin_neg: text_input::State::new(), 102 | sigma: text_input::State::new(), 103 | }, 104 | } 105 | } 106 | } 107 | 108 | #[derive(Debug, Clone)] 109 | pub enum Message { 110 | // Entry messages 111 | EntryActive(bool), 112 | EntryEdit, 113 | EntryDelete, 114 | EntryFinishEditing, 115 | EntryMoveUp, 116 | EntryMoveDown, 117 | // Shared Field messages 118 | EditedDescription(String), 119 | // Linear entry messages 120 | EditedLinearDimension(String), 121 | EditedLinearTolerancePos(String), 122 | EditedLinearToleranceNeg(String), 123 | EditedLinearSigma(String), 124 | // Float entry messages 125 | EditedFloatDiameterHole(String), 126 | EditedFloatDiameterPin(String), 127 | EditedFloatTolHolePos(String), 128 | EditedFloatTolHoleNeg(String), 129 | EditedFloatTolPinPos(String), 130 | EditedFloatTolPinNeg(String), 131 | EditedFloatSigma(String), 132 | } 133 | 134 | #[derive(Debug, Clone, Serialize, Deserialize)] 135 | pub enum FormValues { 136 | Linear { 137 | description: String, 138 | dimension: String, 139 | tolerance_pos: String, 140 | tolerance_neg: String, 141 | sigma: String, 142 | }, 143 | Float { 144 | description: String, 145 | diameter_hole: String, 146 | diameter_pin: String, 147 | tolerance_hole_pos: String, 148 | tolerance_hole_neg: String, 149 | tolerance_pin_pos: String, 150 | tolerance_pin_neg: String, 151 | sigma: String, 152 | }, 153 | } 154 | 155 | #[derive(Debug, Clone, Serialize, Deserialize)] 156 | pub struct ToleranceEntry { 157 | pub input: FormValues, 158 | pub analysis_model: Tolerance, 159 | pub active: bool, 160 | pub valid: bool, 161 | 162 | #[serde(skip)] 163 | pub state: State, 164 | } 165 | impl ToleranceEntry { 166 | pub fn new(description: String, tolerance: Tolerance) -> Self { 167 | ToleranceEntry { 168 | input: match tolerance { 169 | Tolerance::Linear(_) => FormValues::Linear { 170 | description, 171 | dimension: String::from(""), 172 | tolerance_pos: String::from(""), 173 | tolerance_neg: String::from(""), 174 | sigma: String::from(""), 175 | }, 176 | Tolerance::Float(_) => FormValues::Float { 177 | description, 178 | diameter_hole: String::from(""), 179 | diameter_pin: String::from(""), 180 | tolerance_hole_pos: String::from(""), 181 | tolerance_hole_neg: String::from(""), 182 | tolerance_pin_pos: String::from(""), 183 | tolerance_pin_neg: String::from(""), 184 | sigma: String::from(""), 185 | }, 186 | }, 187 | analysis_model: tolerance, 188 | active: false, 189 | valid: false, 190 | state: State::default(), 191 | } 192 | } 193 | 194 | pub fn with_editing(mut self) -> Self { 195 | self.state = State::Editing { 196 | form_tolentry: Box::new(FormState::new(self.analysis_model)), 197 | }; 198 | self 199 | } 200 | 201 | pub fn update(&mut self, message: Message) { 202 | match message { 203 | Message::EntryActive(is_active) => { 204 | if self.valid { 205 | self.active = is_active 206 | } else { 207 | self.active = false; 208 | } 209 | } 210 | Message::EntryEdit => { 211 | self.state = State::Editing { 212 | form_tolentry: Box::new(FormState::new_focused(self.analysis_model)), 213 | }; 214 | } 215 | Message::EntryFinishEditing => { 216 | if match &self.input { 217 | FormValues::Linear { description, .. } => !description.is_empty(), 218 | FormValues::Float { description, .. } => !description.is_empty(), 219 | } { 220 | self.state = State::default() 221 | } 222 | } 223 | Message::EntryDelete => {} 224 | Message::EntryMoveUp => {} 225 | Message::EntryMoveDown => {} 226 | Message::EditedDescription(input) => { 227 | match &mut self.input { 228 | FormValues::Linear { description, .. } => *description = input, 229 | FormValues::Float { description, .. } => *description = input, 230 | }; 231 | } 232 | Message::EditedLinearDimension(input) => { 233 | if let FormValues::Linear { dimension, .. } = &mut self.input { 234 | *dimension = NumericString::eval(dimension, &input, NumericString::Number) 235 | }; 236 | } 237 | Message::EditedLinearTolerancePos(input) => { 238 | if let FormValues::Linear { tolerance_pos, .. } = &mut self.input { 239 | *tolerance_pos = 240 | NumericString::eval(tolerance_pos, &input, NumericString::Positive) 241 | }; 242 | } 243 | Message::EditedLinearToleranceNeg(input) => { 244 | if let FormValues::Linear { tolerance_neg, .. } = &mut self.input { 245 | *tolerance_neg = 246 | NumericString::eval(tolerance_neg, &input, NumericString::Positive) 247 | }; 248 | } 249 | Message::EditedLinearSigma(input) => { 250 | if let FormValues::Linear { sigma, .. } = &mut self.input { 251 | *sigma = NumericString::eval(sigma, &input, NumericString::Positive) 252 | }; 253 | } 254 | Message::EditedFloatDiameterHole(input) => { 255 | if let FormValues::Float { diameter_hole, .. } = &mut self.input { 256 | *diameter_hole = 257 | NumericString::eval(diameter_hole, &input, NumericString::Positive) 258 | }; 259 | } 260 | Message::EditedFloatDiameterPin(input) => { 261 | if let FormValues::Float { diameter_pin, .. } = &mut self.input { 262 | *diameter_pin = 263 | NumericString::eval(diameter_pin, &input, NumericString::Positive) 264 | }; 265 | } 266 | Message::EditedFloatTolHolePos(input) => { 267 | if let FormValues::Float { 268 | tolerance_hole_pos, .. 269 | } = &mut self.input 270 | { 271 | *tolerance_hole_pos = 272 | NumericString::eval(tolerance_hole_pos, &input, NumericString::Positive) 273 | }; 274 | } 275 | Message::EditedFloatTolHoleNeg(input) => { 276 | if let FormValues::Float { 277 | tolerance_hole_neg, .. 278 | } = &mut self.input 279 | { 280 | *tolerance_hole_neg = 281 | NumericString::eval(tolerance_hole_neg, &input, NumericString::Positive) 282 | }; 283 | } 284 | Message::EditedFloatTolPinPos(input) => { 285 | if let FormValues::Float { 286 | tolerance_pin_pos, .. 287 | } = &mut self.input 288 | { 289 | *tolerance_pin_pos = 290 | NumericString::eval(tolerance_pin_pos, &input, NumericString::Positive) 291 | }; 292 | } 293 | Message::EditedFloatTolPinNeg(input) => { 294 | if let FormValues::Float { 295 | tolerance_pin_neg, .. 296 | } = &mut self.input 297 | { 298 | *tolerance_pin_neg = 299 | NumericString::eval(tolerance_pin_neg, &input, NumericString::Positive) 300 | }; 301 | } 302 | Message::EditedFloatSigma(input) => { 303 | if let FormValues::Float { sigma, .. } = &mut self.input { 304 | *sigma = NumericString::eval(sigma, &input, NumericString::Positive) 305 | }; 306 | } 307 | } 308 | } 309 | 310 | pub fn view(&mut self, iss: &style::IcedStyleSheet) -> Element { 311 | match &mut self.state { 312 | State::Idle { 313 | button_edit, 314 | button_move_up, 315 | button_move_down, 316 | } => { 317 | let checkbox = Checkbox::new( 318 | self.active, 319 | match &self.input { 320 | FormValues::Linear { description, .. } => description, 321 | FormValues::Float { description, .. } => description, 322 | }, 323 | Message::EntryActive, 324 | ) 325 | .width(Length::Fill); 326 | 327 | let summary = Text::new(match self.valid { 328 | true => match self.analysis_model { 329 | Tolerance::Linear(dim) => { 330 | if (dim.distance.tol_neg - dim.distance.tol_pos).abs() < f64::EPSILON { 331 | format!("{} +/- {}", dim.distance.dim, dim.distance.tol_pos) 332 | } else { 333 | format!( 334 | "{} +{}/-{}", 335 | dim.distance.dim, dim.distance.tol_pos, dim.distance.tol_neg 336 | ) 337 | } 338 | } 339 | Tolerance::Float(dim) => { 340 | let hole = if (dim.hole.tol_neg - dim.hole.tol_pos).abs() < f64::EPSILON 341 | { 342 | format!("{} +/- {}", dim.hole.dim, dim.hole.tol_pos) 343 | } else { 344 | format!( 345 | "{} +{}/-{}", 346 | dim.hole.dim, dim.hole.tol_pos, dim.hole.tol_neg 347 | ) 348 | }; 349 | let pin = if (dim.pin.tol_neg - dim.pin.tol_pos).abs() < f64::EPSILON { 350 | format!("{} +/- {}", dim.pin.dim, dim.pin.tol_pos) 351 | } else { 352 | format!("{} +{}/-{}", dim.pin.dim, dim.pin.tol_pos, dim.pin.tol_neg) 353 | }; 354 | format!("Hole: {}\nPin: {}", hole, pin) 355 | } 356 | }, 357 | false => "Incomplete entry".to_string(), 358 | }) 359 | .size(iss.text_size(&iss.tol_entry_summary_text_size)); 360 | 361 | let edit_button = Button::new( 362 | button_edit, 363 | Row::new() 364 | //.push(Text::new("Edit") 365 | // .size(iss.text_size(&iss.tol_entry_button_text_size))) 366 | .push(icons::edit().size(iss.text_size(&iss.tol_entry_button_text_size))) 367 | .spacing(iss.spacing(&iss.tol_entry_button_spacing)), 368 | ) 369 | .on_press(Message::EntryEdit) 370 | .padding(iss.padding(&iss.tol_entry_button_padding)) 371 | .style(iss.button(&iss.button_action)); 372 | 373 | let up_button = Button::new( 374 | button_move_up, 375 | Row::new() 376 | //.push(Text::new("Edit") 377 | // .size(iss.text_size(&iss.tol_entry_button_text_size))) 378 | .push( 379 | icons::up_arrow().size(iss.text_size(&iss.tol_entry_button_text_size)), 380 | ) 381 | .spacing(iss.spacing(&iss.tol_entry_button_spacing)), 382 | ) 383 | .on_press(Message::EntryMoveUp) 384 | .padding(iss.padding(&iss.tol_entry_button_padding)) 385 | .style(iss.button(&iss.button_inactive)); 386 | 387 | let down_button = Button::new( 388 | button_move_down, 389 | Row::new() 390 | //.push(Text::new("Edit") 391 | // .size(iss.text_size(&iss.tol_entry_button_text_size))) 392 | .push( 393 | icons::down_arrow() 394 | .size(iss.text_size(&iss.tol_entry_button_text_size)), 395 | ) 396 | .spacing(iss.spacing(&iss.tol_entry_button_spacing)), 397 | ) 398 | .on_press(Message::EntryMoveDown) 399 | .padding(iss.padding(&iss.tol_entry_button_padding)) 400 | .style(iss.button(&iss.button_inactive)); 401 | 402 | let row_contents = Row::new() 403 | .padding(iss.padding(&iss.tol_entry_padding)) 404 | .spacing(iss.spacing(&iss.tol_entry_spacing)) 405 | .align_items(Align::Center) 406 | .push(checkbox) 407 | .push(summary) 408 | .push(edit_button) 409 | .push(up_button) 410 | .push(down_button); 411 | 412 | //Container::new(row_contents) 413 | // .style(iss.container(&iss.tol_entry_container)) 414 | // .into() 415 | 416 | row_contents.into() 417 | } 418 | State::Editing { form_tolentry } => match &mut **form_tolentry { 419 | FormState::Linear { 420 | button_save, 421 | button_delete, 422 | description, 423 | dimension, 424 | tolerance_pos, 425 | tolerance_neg, 426 | sigma, 427 | } => { 428 | let view_button_save = Button::new( 429 | button_save, 430 | Row::new() 431 | .spacing(10) 432 | .push(icons::check()) 433 | .push(Text::new("Save")), 434 | ) 435 | .on_press(Message::EntryFinishEditing) 436 | .padding(10) 437 | .style(iss.button(&iss.button_constructive)); 438 | 439 | let view_button_delete = Button::new( 440 | button_delete, 441 | Row::new() 442 | .spacing(10) 443 | .push(icons::delete()) 444 | .push(Text::new("Delete")), 445 | ) 446 | .on_press(Message::EntryDelete) 447 | .padding(10) 448 | .style(iss.button(&iss.button_destructive)); 449 | 450 | let view_description = TextInput::new( 451 | description, 452 | "Enter a description", 453 | match &self.input { 454 | FormValues::Linear { description, .. } => description, 455 | _ => "Error: tolerance type mismatch", 456 | }, 457 | Message::EditedDescription, 458 | ) 459 | .on_submit(Message::EntryFinishEditing) 460 | .padding(iss.padding(&iss.tol_edit_field_padding)) 461 | .size(iss.text_size(&iss.tol_edit_field_text_size)); 462 | 463 | let view_dimension = TextInput::new( 464 | dimension, 465 | "Enter a value", 466 | match &self.input { 467 | FormValues::Linear { dimension, .. } => dimension, 468 | _ => "Error: tolerance type mismatch", 469 | }, 470 | Message::EditedLinearDimension, 471 | ) 472 | .on_submit(Message::EntryFinishEditing) 473 | .padding(iss.padding(&iss.tol_edit_field_padding)) 474 | .size(iss.text_size(&iss.tol_edit_field_text_size)); 475 | 476 | let view_tolerance_pos = TextInput::new( 477 | tolerance_pos, 478 | "Enter a value", 479 | match &self.input { 480 | FormValues::Linear { tolerance_pos, .. } => tolerance_pos, 481 | _ => "Error: tolerance type mismatch", 482 | }, 483 | Message::EditedLinearTolerancePos, 484 | ) 485 | .on_submit(Message::EntryFinishEditing) 486 | .padding(iss.padding(&iss.tol_edit_field_padding)) 487 | .size(iss.text_size(&iss.tol_edit_field_text_size)); 488 | 489 | let view_tolerance_neg = TextInput::new( 490 | tolerance_neg, 491 | "Enter a value", 492 | match &self.input { 493 | FormValues::Linear { tolerance_neg, .. } => tolerance_neg, 494 | _ => "Error: tolerance type mismatch", 495 | }, 496 | Message::EditedLinearToleranceNeg, 497 | ) 498 | .on_submit(Message::EntryFinishEditing) 499 | .padding(iss.padding(&iss.tol_edit_field_padding)) 500 | .size(iss.text_size(&iss.tol_edit_field_text_size)); 501 | 502 | let view_sigma = TextInput::new( 503 | sigma, 504 | "Enter a value", 505 | match &self.input { 506 | FormValues::Linear { sigma, .. } => sigma, 507 | _ => "Error: tolerance type mismatch", 508 | }, 509 | Message::EditedLinearSigma, 510 | ) 511 | .on_submit(Message::EntryFinishEditing) 512 | .padding(iss.padding(&iss.tol_edit_field_padding)) 513 | .size(iss.text_size(&iss.tol_edit_field_text_size)); 514 | 515 | let row_header = Row::new() 516 | .push( 517 | Text::new("Editing Linear Tolerance") 518 | .size(iss.text_size(&iss.tol_edit_heading_text_size)) 519 | .width(Length::Fill) 520 | .horizontal_alignment(HorizontalAlignment::Left), 521 | ) 522 | .spacing(iss.spacing(&iss.tol_edit_label_spacing)) 523 | .align_items(Align::Center); 524 | 525 | let row_description = Row::new() 526 | .push(Column::new().width(Length::Units(20))) 527 | .push( 528 | Text::new("Description:") 529 | .size(iss.text_size(&iss.tol_edit_label_text_size)), 530 | ) 531 | .push(view_description) 532 | .spacing(iss.spacing(&iss.tol_edit_label_spacing)) 533 | .align_items(Align::Center); 534 | 535 | let row_dimension = Row::new() 536 | .push(Column::new().width(Length::Units(20))) 537 | .push( 538 | Text::new("Dimension:") 539 | .size(iss.text_size(&iss.tol_edit_label_text_size)), 540 | ) 541 | .push(view_dimension) 542 | .spacing(iss.spacing(&iss.tol_edit_label_spacing)) 543 | .align_items(Align::Center); 544 | 545 | let row_tolerance_pos = Row::new() 546 | .push(Column::new().width(Length::Units(20))) 547 | .push( 548 | Text::new("+ Tolerance:") 549 | .size(iss.text_size(&iss.tol_edit_label_text_size)), 550 | ) 551 | .push(view_tolerance_pos) 552 | .spacing(iss.spacing(&iss.tol_edit_label_spacing)) 553 | .align_items(Align::Center); 554 | 555 | let row_tolerance_neg = Row::new() 556 | .push(Column::new().width(Length::Units(20))) 557 | .push( 558 | Text::new("- Tolerance:") 559 | .size(iss.text_size(&iss.tol_edit_label_text_size)), 560 | ) 561 | .push(view_tolerance_neg) 562 | .spacing(iss.spacing(&iss.tol_edit_label_spacing)) 563 | .align_items(Align::Center); 564 | 565 | let row_sigma = Row::new() 566 | .push(Column::new().width(Length::Units(20))) 567 | .push( 568 | Text::new("Sigma:").size(iss.text_size(&iss.tol_edit_label_text_size)), 569 | ) 570 | .push(view_sigma) 571 | .spacing(iss.spacing(&iss.tol_edit_label_spacing)) 572 | .align_items(Align::Center); 573 | 574 | let row_buttons = Row::new() 575 | .push(view_button_delete) 576 | .push(view_button_save) 577 | .spacing(iss.spacing(&iss.tol_edit_label_spacing)) 578 | .align_items(Align::Center); 579 | 580 | let entry_contents = Column::new() 581 | .push(row_header) 582 | .push(Row::new().height(Length::Units(5))) 583 | .push(row_description) 584 | .push(row_dimension) 585 | .push(row_tolerance_pos) 586 | .push(row_tolerance_neg) 587 | .push(row_sigma) 588 | .push(Row::new().height(Length::Units(5))) 589 | .push(row_buttons) 590 | .spacing(iss.spacing(&iss.tol_edit_vspacing)) 591 | .padding(iss.padding(&iss.tol_edit_padding)); 592 | 593 | Container::new(entry_contents) 594 | .style(iss.container(&iss.tol_entry_container)) 595 | .into() 596 | } 597 | FormState::Float { 598 | button_save, 599 | button_delete, 600 | description, 601 | diameter_hole, 602 | diameter_pin, 603 | tolerance_hole_pos, 604 | tolerance_hole_neg, 605 | tolerance_pin_pos, 606 | tolerance_pin_neg, 607 | sigma, 608 | } => { 609 | let view_button_save = Button::new( 610 | button_save, 611 | Row::new() 612 | .spacing(10) 613 | .push(icons::check()) 614 | .push(Text::new("Save")), 615 | ) 616 | .on_press(Message::EntryFinishEditing) 617 | .padding(10) 618 | .style(iss.button(&iss.button_constructive)); 619 | 620 | let view_button_delete = Button::new( 621 | button_delete, 622 | Row::new() 623 | .spacing(10) 624 | .push(icons::delete()) 625 | .push(Text::new("Delete")), 626 | ) 627 | .on_press(Message::EntryDelete) 628 | .padding(10) 629 | .style(iss.button(&iss.button_destructive)); 630 | 631 | let view_description = TextInput::new( 632 | description, 633 | "Enter a description", 634 | match &self.input { 635 | FormValues::Float { description, .. } => description, 636 | _ => "Error: tolerance type mismatch", 637 | }, 638 | Message::EditedDescription, 639 | ) 640 | .on_submit(Message::EntryFinishEditing) 641 | .padding(iss.padding(&iss.tol_edit_field_padding)) 642 | .size(iss.text_size(&iss.tol_edit_field_text_size)); 643 | 644 | let view_diameter_hole = TextInput::new( 645 | diameter_hole, 646 | "Enter a value", 647 | match &self.input { 648 | FormValues::Float { diameter_hole, .. } => diameter_hole, 649 | _ => "Error: tolerance type mismatch", 650 | }, 651 | Message::EditedFloatDiameterHole, 652 | ) 653 | .on_submit(Message::EntryFinishEditing) 654 | .padding(iss.padding(&iss.tol_edit_field_padding)) 655 | .size(iss.text_size(&iss.tol_edit_field_text_size)); 656 | 657 | let view_diameter_pin = TextInput::new( 658 | diameter_pin, 659 | "Enter a value", 660 | match &self.input { 661 | FormValues::Float { diameter_pin, .. } => diameter_pin, 662 | _ => "Error: tolerance type mismatch", 663 | }, 664 | Message::EditedFloatDiameterPin, 665 | ) 666 | .on_submit(Message::EntryFinishEditing) 667 | .padding(iss.padding(&iss.tol_edit_field_padding)) 668 | .size(iss.text_size(&iss.tol_edit_field_text_size)); 669 | 670 | let view_tolerance_hole_pos = TextInput::new( 671 | tolerance_hole_pos, 672 | "Enter a value", 673 | match &self.input { 674 | FormValues::Float { 675 | tolerance_hole_pos, .. 676 | } => tolerance_hole_pos, 677 | _ => "Error: tolerance type mismatch", 678 | }, 679 | Message::EditedFloatTolHolePos, 680 | ) 681 | .on_submit(Message::EntryFinishEditing) 682 | .padding(iss.padding(&iss.tol_edit_field_padding)) 683 | .size(iss.text_size(&iss.tol_edit_field_text_size)); 684 | 685 | let view_tolerance_hole_neg = TextInput::new( 686 | tolerance_hole_neg, 687 | "Enter a value", 688 | match &self.input { 689 | FormValues::Float { 690 | tolerance_hole_neg, .. 691 | } => tolerance_hole_neg, 692 | _ => "Error: tolerance type mismatch", 693 | }, 694 | Message::EditedFloatTolHoleNeg, 695 | ) 696 | .on_submit(Message::EntryFinishEditing) 697 | .padding(iss.padding(&iss.tol_edit_field_padding)) 698 | .size(iss.text_size(&iss.tol_edit_field_text_size)); 699 | 700 | let view_tolerance_pin_pos = TextInput::new( 701 | tolerance_pin_pos, 702 | "Enter a value", 703 | match &self.input { 704 | FormValues::Float { 705 | tolerance_pin_pos, .. 706 | } => tolerance_pin_pos, 707 | _ => "Error: tolerance type mismatch", 708 | }, 709 | Message::EditedFloatTolPinPos, 710 | ) 711 | .on_submit(Message::EntryFinishEditing) 712 | .padding(iss.padding(&iss.tol_edit_field_padding)) 713 | .size(iss.text_size(&iss.tol_edit_field_text_size)); 714 | 715 | let view_tolerance_pin_neg = TextInput::new( 716 | tolerance_pin_neg, 717 | "Enter a value", 718 | match &self.input { 719 | FormValues::Float { 720 | tolerance_pin_neg, .. 721 | } => tolerance_pin_neg, 722 | _ => "Error: tolerance type mismatch", 723 | }, 724 | Message::EditedFloatTolPinNeg, 725 | ) 726 | .on_submit(Message::EntryFinishEditing) 727 | .padding(iss.padding(&iss.tol_edit_field_padding)) 728 | .size(iss.text_size(&iss.tol_edit_field_text_size)); 729 | 730 | let view_sigma = TextInput::new( 731 | sigma, 732 | "Enter a value", 733 | match &self.input { 734 | FormValues::Float { sigma, .. } => sigma, 735 | _ => "Error: tolerance type mismatch", 736 | }, 737 | Message::EditedFloatSigma, 738 | ) 739 | .on_submit(Message::EntryFinishEditing) 740 | .padding(iss.padding(&iss.tol_edit_field_padding)) 741 | .size(iss.text_size(&iss.tol_edit_field_text_size)); 742 | 743 | let row_header = Row::new() 744 | .push( 745 | Text::new("Editing Float Tolerance") 746 | .size(iss.text_size(&iss.tol_edit_heading_text_size)) 747 | .width(Length::Fill) 748 | .horizontal_alignment(HorizontalAlignment::Left), 749 | ) 750 | .spacing(iss.spacing(&iss.tol_edit_label_spacing)) 751 | .align_items(Align::Center); 752 | 753 | let row_description = Row::new() 754 | .push(Column::new().width(Length::Units(20))) 755 | .push( 756 | Text::new("Description:") 757 | .size(iss.text_size(&iss.tol_edit_label_text_size)), 758 | ) 759 | .push(view_description) 760 | .spacing(iss.spacing(&iss.tol_edit_label_spacing)) 761 | .align_items(Align::Center); 762 | 763 | let row_diameter_hole = Row::new() 764 | .push(Column::new().width(Length::Units(20))) 765 | .push( 766 | Text::new("Hole Diameter:") 767 | .size(iss.text_size(&iss.tol_edit_label_text_size)), 768 | ) 769 | .push(view_diameter_hole) 770 | .spacing(iss.spacing(&iss.tol_edit_label_spacing)) 771 | .align_items(Align::Center); 772 | 773 | let row_diameter_pin = Row::new() 774 | .push(Column::new().width(Length::Units(20))) 775 | .push( 776 | Text::new("Pin Diameter:") 777 | .size(iss.text_size(&iss.tol_edit_label_text_size)), 778 | ) 779 | .push(view_diameter_pin) 780 | .spacing(iss.spacing(&iss.tol_edit_label_spacing)) 781 | .align_items(Align::Center); 782 | 783 | let row_tolerance_hole_pos = Row::new() 784 | .push(Column::new().width(Length::Units(20))) 785 | .push( 786 | Text::new("+ Hole Tolerance:") 787 | .size(iss.text_size(&iss.tol_edit_label_text_size)), 788 | ) 789 | .push(view_tolerance_hole_pos) 790 | .spacing(iss.spacing(&iss.tol_edit_label_spacing)) 791 | .align_items(Align::Center); 792 | 793 | let row_tolerance_hole_neg = Row::new() 794 | .push(Column::new().width(Length::Units(20))) 795 | .push( 796 | Text::new("- Hole Tolerance:") 797 | .size(iss.text_size(&iss.tol_edit_label_text_size)), 798 | ) 799 | .push(view_tolerance_hole_neg) 800 | .spacing(iss.spacing(&iss.tol_edit_label_spacing)) 801 | .align_items(Align::Center); 802 | 803 | let row_tolerance_pin_pos = Row::new() 804 | .push(Column::new().width(Length::Units(20))) 805 | .push( 806 | Text::new("+ Pin Tolerance:") 807 | .size(iss.text_size(&iss.tol_edit_label_text_size)), 808 | ) 809 | .push(view_tolerance_pin_pos) 810 | .spacing(iss.spacing(&iss.tol_edit_label_spacing)) 811 | .align_items(Align::Center); 812 | 813 | let row_tolerance_pin_neg = Row::new() 814 | .push(Column::new().width(Length::Units(20))) 815 | .push( 816 | Text::new("- Pin Tolerance:") 817 | .size(iss.text_size(&iss.tol_edit_label_text_size)), 818 | ) 819 | .push(view_tolerance_pin_neg) 820 | .spacing(iss.spacing(&iss.tol_edit_label_spacing)) 821 | .align_items(Align::Center); 822 | 823 | let row_sigma = Row::new() 824 | .push(Column::new().width(Length::Units(20))) 825 | .push( 826 | Text::new("Sigma:").size(iss.text_size(&iss.tol_edit_label_text_size)), 827 | ) 828 | .push(view_sigma) 829 | .spacing(iss.spacing(&iss.tol_edit_label_spacing)) 830 | .align_items(Align::Center); 831 | 832 | let row_buttons = Row::new() 833 | .push(view_button_delete) 834 | .push(view_button_save) 835 | .spacing(iss.spacing(&iss.tol_edit_label_spacing)) 836 | .align_items(Align::Center); 837 | 838 | let entry_contents = Column::new() 839 | .push(row_header) 840 | .push(Row::new().height(Length::Units(5))) 841 | .push(row_description) 842 | .push(Text::new("Hole Dimensions")) 843 | .push(row_diameter_hole) 844 | .push(row_tolerance_hole_pos) 845 | .push(row_tolerance_hole_neg) 846 | .push(Text::new("Pin Dimensions")) 847 | .push(row_diameter_pin) 848 | .push(row_tolerance_pin_pos) 849 | .push(row_tolerance_pin_neg) 850 | .push(row_sigma) 851 | .push(Row::new().height(Length::Units(5))) 852 | .push(row_buttons) 853 | .spacing(iss.spacing(&iss.tol_edit_vspacing)) 854 | .padding(iss.padding(&iss.tol_edit_padding)); 855 | 856 | Container::new(entry_contents) 857 | .style(iss.container(&iss.tol_entry_container)) 858 | .into() 859 | } 860 | }, 861 | } 862 | } 863 | } 864 | 865 | enum NumericString { 866 | Number, 867 | Positive, 868 | //Negative, 869 | } 870 | impl NumericString { 871 | pub fn eval(old: &str, input: &str, criteria: Self) -> String { 872 | match input.parse::().is_ok() { 873 | true => { 874 | let numeric_input = input.parse::().unwrap(); 875 | if match criteria { 876 | NumericString::Number => true, 877 | NumericString::Positive => numeric_input >= 0.0, 878 | //NumericString::Negative => numeric_input < 0.0, 879 | } || input.is_empty() 880 | || input == "." 881 | { 882 | input.to_string() 883 | } else { 884 | old.to_string() 885 | } 886 | } 887 | false => { 888 | if match criteria { 889 | NumericString::Number => input.is_empty() || input == "-" || input == ".", 890 | //NumericString::Negative => input == "" || input == "-" || input == ".", 891 | NumericString::Positive => input.is_empty() || input == ".", 892 | } { 893 | input.to_string() 894 | } else { 895 | old.to_string() 896 | } 897 | } 898 | } 899 | } 900 | } 901 | -------------------------------------------------------------------------------- /src/ui/components/filter_tolerance.rs: -------------------------------------------------------------------------------- 1 | use crate::analysis::structures::*; 2 | use crate::ui::style; 3 | use iced::{button, Align, Button, Element, Length, Row, Text}; 4 | use serde_derive::*; 5 | 6 | #[derive(Debug, Clone)] 7 | pub enum Message { 8 | FilterChanged(Filter), 9 | } 10 | 11 | #[derive(Debug, Default, Clone)] 12 | pub struct ToleranceFilter { 13 | pub filter_value: Filter, 14 | all_button: button::State, 15 | linear_button: button::State, 16 | float_button: button::State, 17 | compound_button: button::State, 18 | } 19 | impl ToleranceFilter { 20 | pub fn update(&mut self, message: Message) { 21 | let ToleranceFilter { 22 | filter_value, 23 | all_button: _, 24 | linear_button: _, 25 | float_button: _, 26 | compound_button: _, 27 | } = self; 28 | match message { 29 | Message::FilterChanged(filter) => { 30 | *filter_value = filter; 31 | } 32 | } 33 | } 34 | pub fn view(&mut self, iss: &style::IcedStyleSheet) -> Element { 35 | let ToleranceFilter { 36 | filter_value, 37 | all_button, 38 | linear_button, 39 | float_button, 40 | compound_button: _, 41 | } = self; 42 | 43 | let filter_button = |state, label, filter, current_filter| { 44 | let label = Text::new(label).size(16); 45 | let active = filter == current_filter; 46 | let button = Button::new(state, label).style(iss.toggle_button( 47 | active, 48 | &iss.button_active, 49 | &iss.button_inactive, 50 | )); 51 | button.on_press(Message::FilterChanged(filter)).padding(8) 52 | }; 53 | 54 | Row::new() 55 | .spacing(20) 56 | .align_items(Align::Center) 57 | .push( 58 | Row::new() 59 | .width(Length::Shrink) 60 | .spacing(10) 61 | .push(filter_button(all_button, "All", Filter::All, *filter_value)) 62 | .push(filter_button( 63 | linear_button, 64 | "Linear", 65 | Filter::Some(Tolerance::Linear(LinearTL::default())), 66 | *filter_value, 67 | )) 68 | .push(filter_button( 69 | float_button, 70 | "Float", 71 | Filter::Some(Tolerance::Float(FloatTL::default())), 72 | *filter_value, 73 | )), 74 | ) 75 | .into() 76 | } 77 | } 78 | 79 | #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] 80 | pub enum Filter { 81 | All, 82 | Some(Tolerance), 83 | } 84 | impl Filter { 85 | pub fn matches(&self, tol: Tolerance) -> bool { 86 | match self { 87 | Filter::All => true, 88 | Filter::Some(tol_self) => { 89 | std::mem::discriminant(tol_self) == std::mem::discriminant(&tol) 90 | } 91 | } 92 | } 93 | } 94 | impl Default for Filter { 95 | fn default() -> Self { 96 | Filter::All 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/ui/components/form_new_mc_analysis.rs: -------------------------------------------------------------------------------- 1 | //use crate::analysis::*; 2 | use crate::ui::style; 3 | use iced::{button, text_input, Align, Button, Column, Element, Length, Row, Text, TextInput}; 4 | 5 | #[derive(Debug, Clone)] 6 | pub enum Message { 7 | IterEdited(String), 8 | SigmaEdited(String), 9 | Calculate, 10 | //CalculateComplete(Option), 11 | } 12 | 13 | #[derive(Debug, Default, Clone)] 14 | pub struct NewMonteCarloAnalysis { 15 | pub n_iteration: usize, 16 | pub assy_sigma: f64, 17 | state_calculate_button: button::State, 18 | state_input_assy_sigma: text_input::State, 19 | state_input_iterations: text_input::State, 20 | } 21 | impl NewMonteCarloAnalysis { 22 | pub fn update(&mut self, message: Message) { 23 | match message { 24 | Message::IterEdited(input) => { 25 | if input.parse::().is_ok() { 26 | let number = input.parse::().unwrap(); 27 | self.n_iteration = number; 28 | } 29 | } 30 | Message::SigmaEdited(input) => { 31 | if input.parse::().is_ok() { 32 | let number = input.parse::().unwrap(); 33 | self.assy_sigma = number; 34 | } 35 | } 36 | Message::Calculate => {} //Message::CalculateComplete(_) => {} 37 | } 38 | } 39 | pub fn view(&mut self, iss: &style::IcedStyleSheet) -> Element { 40 | let NewMonteCarloAnalysis { 41 | n_iteration, 42 | assy_sigma, 43 | state_calculate_button, 44 | state_input_assy_sigma, 45 | state_input_iterations, 46 | } = self; 47 | let results_header = Column::new() 48 | .push( 49 | Row::new() 50 | .push( 51 | Text::new("Simulation Parameters") 52 | .size(24) 53 | .width(Length::Fill), 54 | ) 55 | .align_items(Align::Center) 56 | .width(Length::Fill), 57 | ) 58 | .push( 59 | Row::new() 60 | .push(Text::new("Iterations")) 61 | .push( 62 | TextInput::new( 63 | state_input_iterations, 64 | "Enter a value...", 65 | &n_iteration.to_string(), 66 | Message::IterEdited, 67 | ) 68 | .padding(10), 69 | ) 70 | .align_items(Align::Center) 71 | .spacing(20), 72 | ) 73 | .push( 74 | Row::new() 75 | .push(Text::new("Assembly Sigma")) 76 | .push( 77 | TextInput::new( 78 | state_input_assy_sigma, 79 | "Enter a value...", 80 | &assy_sigma.to_string(), 81 | Message::SigmaEdited, 82 | ) 83 | .padding(10), 84 | ) 85 | .align_items(Align::Center) 86 | .spacing(20), 87 | ) 88 | .push( 89 | Row::new().push(Column::new().width(Length::Fill)).push( 90 | Button::new( 91 | state_calculate_button, 92 | Row::new() 93 | .spacing(10) 94 | //.push(icons::check()) 95 | .push(Text::new("Run Simulation")), 96 | ) 97 | .style(iss.button(&iss.button_action)) 98 | .padding(10) 99 | .on_press(Message::Calculate), 100 | ), 101 | ) 102 | .spacing(20); 103 | 104 | results_header.into() 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/ui/components/form_new_tolerance.rs: -------------------------------------------------------------------------------- 1 | use crate::analysis::structures::*; 2 | use crate::ui::style; 3 | use iced::{ 4 | button, text_input, Align, Button, Column, Command, Element, HorizontalAlignment, Length, Row, 5 | Text, TextInput, 6 | }; 7 | 8 | #[derive(Debug, Clone)] 9 | pub enum Message { 10 | TolTypeChanged(Tolerance), 11 | TolNameChanged(String), 12 | CreateTol(String, Tolerance), 13 | } 14 | 15 | #[derive(Debug, Default, Clone)] 16 | pub struct NewToleranceEntry { 17 | tolerance_type: Tolerance, 18 | tolerance_text_value: String, 19 | state_linear_button: button::State, 20 | state_float_button: button::State, 21 | state_compound_button: button::State, 22 | state_tolerance_text: text_input::State, 23 | } 24 | impl NewToleranceEntry { 25 | pub fn update(&mut self, message: Message) -> Command { 26 | match message { 27 | Message::TolTypeChanged(value) => { 28 | self.tolerance_type = value; 29 | } 30 | Message::TolNameChanged(value) => { 31 | self.tolerance_text_value = value; 32 | } 33 | Message::CreateTol(_, _) => { 34 | self.tolerance_text_value.clear(); 35 | } 36 | } 37 | Command::none() 38 | } 39 | pub fn view(&mut self, iss: &style::IcedStyleSheet) -> Element { 40 | let NewToleranceEntry { 41 | tolerance_type, 42 | tolerance_text_value, 43 | state_linear_button, 44 | state_float_button, 45 | state_compound_button: _, 46 | state_tolerance_text, 47 | } = self; 48 | 49 | let tolerance_label = Text::new("Add Tolerance") 50 | .width(Length::Fill) 51 | .size(24) 52 | .horizontal_alignment(HorizontalAlignment::Left); 53 | let tolerance_text = TextInput::new( 54 | state_tolerance_text, 55 | "Tolerance name, press enter to add.", 56 | tolerance_text_value, 57 | Message::TolNameChanged, 58 | ) 59 | .padding(10) 60 | .on_submit(Message::CreateTol( 61 | tolerance_text_value.clone(), 62 | tolerance_type.clone(), 63 | )); 64 | 65 | let button = |state, label, tolerance: Tolerance, current_tol: Tolerance| { 66 | let label = Text::new(label).size(18); 67 | let active = tolerance == current_tol; 68 | let button = Button::new(state, label).style(iss.toggle_button( 69 | active, 70 | &iss.button_active, 71 | &iss.button_inactive, 72 | )); 73 | button 74 | .on_press(Message::TolTypeChanged(tolerance)) 75 | .padding(8) 76 | }; 77 | 78 | Row::new() 79 | .push( 80 | Column::new() 81 | .push( 82 | Row::new() 83 | .spacing(20) 84 | .align_items(Align::Center) 85 | .push(tolerance_label) 86 | .push( 87 | Row::new() 88 | .width(Length::Shrink) 89 | .spacing(10) 90 | .push(button( 91 | state_linear_button, 92 | "Linear", 93 | Tolerance::Linear(LinearTL::default()), 94 | self.tolerance_type, 95 | )) 96 | .push(button( 97 | state_float_button, 98 | "Float", 99 | Tolerance::Float(FloatTL::default()), 100 | self.tolerance_type, 101 | )), 102 | ), 103 | ) 104 | .push(tolerance_text) 105 | .spacing(10), 106 | ) 107 | .into() 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/ui/fonts/icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aevyrie/tolstack/bb8020f5a602ecc0cd25edac11dc0262324dd23f/src/ui/fonts/icons.ttf -------------------------------------------------------------------------------- /src/ui/icon.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aevyrie/tolstack/bb8020f5a602ecc0cd25edac11dc0262324dd23f/src/ui/icon.pdn -------------------------------------------------------------------------------- /src/ui/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aevyrie/tolstack/bb8020f5a602ecc0cd25edac11dc0262324dd23f/src/ui/icon.png -------------------------------------------------------------------------------- /src/ui/icons.rs: -------------------------------------------------------------------------------- 1 | use iced::{Font, HorizontalAlignment, Length, Text}; 2 | 3 | const ICONS: Font = Font::External { 4 | name: "Icons", 5 | bytes: include_bytes!("fonts/icons.ttf"), 6 | }; 7 | 8 | fn icon(unicode: char) -> Text { 9 | Text::new(&unicode.to_string()) 10 | .font(ICONS) 11 | .width(Length::Units(20)) 12 | .horizontal_alignment(HorizontalAlignment::Center) 13 | .size(20) 14 | } 15 | 16 | pub fn edit() -> Text { 17 | icon('\u{e803}') 18 | } 19 | 20 | pub fn delete() -> Text { 21 | icon('\u{F1F8}') 22 | } 23 | 24 | pub fn check() -> Text { 25 | icon('\u{e806}') 26 | } 27 | 28 | pub fn save() -> Text { 29 | icon('\u{e800}') 30 | } 31 | 32 | pub fn load() -> Text { 33 | icon('\u{f115}') 34 | } 35 | 36 | pub fn export() -> Text { 37 | icon('\u{e81d}') 38 | } 39 | 40 | pub fn new() -> Text { 41 | icon('\u{e810}') 42 | } 43 | 44 | pub fn add() -> Text { 45 | icon('\u{e80c}') 46 | } 47 | 48 | pub fn duplicate() -> Text { 49 | icon('\u{f0c5}') 50 | } 51 | 52 | pub fn help() -> Text { 53 | icon('\u{f128}') 54 | } 55 | 56 | pub fn up_arrow() -> Text { 57 | icon('\u{e816}') 58 | } 59 | 60 | pub fn down_arrow() -> Text { 61 | icon('\u{e813}') 62 | } 63 | --------------------------------------------------------------------------------