diff --git a/TODO.md b/TODO.md index 2ae9de9..2e877bf 100644 --- a/TODO.md +++ b/TODO.md @@ -3,4 +3,12 @@ 2. Proper support for dynamic chart display size. 3. Fix (very rare) crashes in parsing strings (`TextDecoder.decode: Decoding failed.`) 4. Multiple functions in one graph. + - Backend support + - Generation of data + - Management + - Handle by IDs + - UI + - Dynamically create inputs + - Different colors + - Better Handling of area and integrals 5. Non `y=` functions. \ No newline at end of file diff --git a/src/chart_manager.rs b/src/chart_manager.rs deleted file mode 100644 index a22595c..0000000 --- a/src/chart_manager.rs +++ /dev/null @@ -1,130 +0,0 @@ -use crate::misc::{add_asterisks, Function}; - -pub enum UpdateType { - Full, - Front, - Back, - None, -} - -// Manages Chart generation and caching of values -pub struct ChartManager { - function: Function, - min_x: f64, - max_x: f64, - min_x_back: f64, - max_x_back: f64, - num_interval: usize, - resolution: usize, -} - -impl ChartManager { - pub fn new( - func_str: String, min_x: f64, max_x: f64, min_x_back: f64, max_x_back: f64, - num_interval: usize, resolution: usize, - ) -> Self { - Self { - function: Function::from_string(func_str), - min_x, - max_x, - min_x_back, - max_x_back, - num_interval, - resolution, - } - } - - #[inline] - pub fn draw_back(&mut self) -> Vec<(f64, f64)> { - let absrange = (self.max_x_back - self.min_x_back).abs(); - let output: Vec<(f64, f64)> = (1..=self.resolution) - .map(|x| ((x as f64 / self.resolution as f64) * absrange) + self.min_x_back) - .map(|x| (x, self.function.run(x))) - .collect(); - output - } - - #[inline] - pub fn draw_front(&mut self) -> (Vec<(f64, f64)>, f64) { - self.integral_rectangles(self.get_step()) - } - - #[inline] - pub fn get_step(&self) -> f64 { (self.max_x - self.min_x).abs() / (self.num_interval as f64) } - - #[allow(clippy::too_many_arguments)] - pub fn update( - &mut self, func_str_new: String, min_x: f64, max_x: f64, min_x_back: f64, max_x_back: f64, - num_interval: usize, resolution: usize, - ) -> UpdateType { - let func_str: String = add_asterisks(func_str_new); - let update_func: bool = !self.function.str_compare(func_str.clone()); - - let update_back = - update_func | (min_x_back != self.min_x_back) | (max_x_back != self.max_x_back); - let update_front = update_func - | (min_x != self.min_x) - | (max_x != self.max_x) - | (self.resolution != resolution) - | (num_interval != self.num_interval); - - if update_func { - self.function = Function::from_string(func_str); - } - - self.min_x = min_x; - self.max_x = max_x; - self.min_x_back = min_x_back; - self.max_x_back = max_x_back; - self.num_interval = num_interval; - self.resolution = resolution; - - if update_back && update_front { - UpdateType::Full - } else if update_back { - UpdateType::Back - } else if update_front { - UpdateType::Front - } else { - UpdateType::None - } - } - - // Creates and does the math for creating all the rectangles under the graph - #[inline] - fn integral_rectangles(&self, step: f64) -> (Vec<(f64, f64)>, f64) { - let half_step = step / 2.0; - let data2: Vec<(f64, f64)> = (0..self.num_interval) - .map(|e| { - let x: f64 = ((e as f64) * step) + self.min_x; - - // Makes sure rectangles are properly handled on x values below 0 - let x2: f64 = match x > 0.0 { - true => x + step, - false => x - step, - }; - - let tmp1: f64 = self.function.run_func(x); - let tmp2: f64 = self.function.run_func(x2); - - // Chooses the y value who's absolute value is the smallest - let mut output = match tmp2.abs() > tmp1.abs() { - true => (x, tmp1), - false => (x2, tmp2), - }; - - // Applies `half_step` in order to make the bar graph display properly - if output.0 > 0.0 { - output.0 += half_step; - } else { - output.0 -= half_step; - } - - output - }) - .filter(|(_, y)| !y.is_nan()) - .collect(); - let area: f64 = data2.iter().map(|(_, y)| y * step).sum(); // sum of all rectangles' areas - (data2, area) - } -} diff --git a/src/egui_app.rs b/src/egui_app.rs index 969a807..05e9a5c 100644 --- a/src/egui_app.rs +++ b/src/egui_app.rs @@ -1,7 +1,7 @@ use std::ops::RangeInclusive; -use crate::chart_manager::{ChartManager, UpdateType}; -use crate::misc::{digits_precision, test_func, Cache}; +use crate::function::Function; +use crate::misc::{digits_precision, test_func}; use eframe::{egui, epi}; use egui::plot::{Line, Plot, Value, Values}; use egui::widgets::plot::{Bar, BarChart}; @@ -12,98 +12,60 @@ use git_version::git_version; const GIT_VERSION: &str = git_version!(); // Sets some hard-coded limits to the application -const NUM_INTERVAL_RANGE: RangeInclusive = 10..=1000000; +const INTEGRAL_NUM_RANGE: RangeInclusive = 10..=1000000; const MIN_X_TOTAL: f64 = -1000.0; const MAX_X_TOTAL: f64 = 1000.0; const X_RANGE: RangeInclusive = MIN_X_TOTAL..=MAX_X_TOTAL; pub struct MathApp { - func_str: String, + functions: Vec, min_x: f64, max_x: f64, // Currently really unused. But once fully implemented it will represent the full graph's min_x and max_x, being seperate from min_x and max_x for the intergral. - min_x_graph: f64, - max_x_graph: f64, + integral_min_x: f64, + integral_max_x: f64, - num_interval: usize, - resolution: usize, - chart_manager: ChartManager, - back_cache: Cache>, - front_cache: Cache<(Vec, f64)>, + integral_num: usize, } impl Default for MathApp { #[inline] fn default() -> Self { - let def_func = "x^2".to_string(); + let def_func_str = "x^2".to_string(); let def_min_x = -10.0; let def_max_x = 10.0; let def_interval: usize = 1000; - let def_resolution: usize = 10000; + + let def_funcs: Vec = vec![Function::new( + def_func_str, + def_min_x, + def_max_x, + true, + Some(def_min_x), + Some(def_max_x), + Some(def_interval), + )]; Self { - func_str: def_func.clone(), + functions: def_funcs, min_x: def_min_x, max_x: def_max_x, - min_x_graph: def_min_x, - max_x_graph: def_max_x, - num_interval: def_interval, - resolution: def_resolution, - chart_manager: ChartManager::new( - def_func, - def_min_x, - def_max_x, - def_min_x, - def_max_x, - def_interval, - def_resolution, - ), - back_cache: Cache::new_empty(), - front_cache: Cache::new_empty(), + integral_min_x: def_min_x, + integral_max_x: def_max_x, + integral_num: def_interval, } } } impl MathApp { #[inline] - fn get_back(&mut self) -> Line { - let data = if self.back_cache.is_valid() { - self.back_cache.get().clone() - } else { - let data = self.chart_manager.draw_back(); - let data_values: Vec = data.iter().map(|(x, y)| Value::new(*x, *y)).collect(); - self.back_cache.set(data_values.clone()); - data_values - }; - Line::new(Values::from_values(data)).color(Color32::RED) - } - - #[inline] - fn get_front(&mut self) -> (Vec, f64) { - if self.front_cache.is_valid() { - let cache = self.front_cache.get(); - let vec_bars: Vec = cache.0.to_vec(); - (vec_bars, cache.1) - } else { - let (data, area) = self.chart_manager.draw_front(); - let bars: Vec = data.iter().map(|(x, y)| Bar::new(*x, *y)).collect(); - - let output = (bars, area); - self.front_cache.set(output.clone()); - output - } - } - - #[inline] - fn get_data(&mut self) -> (Line, Vec, f64) { - let (bars, area) = self.get_front(); - (self.get_back(), bars, area) + pub fn get_step(&self) -> f64 { + (self.integral_min_x - self.integral_max_x).abs() / (self.integral_num as f64) } } impl epi::App for MathApp { - // The name of the program (displayed when running natively as the window title) fn name(&self) -> &str { "Integral Demonstration" } @@ -118,16 +80,12 @@ impl epi::App for MathApp { #[inline] fn update(&mut self, ctx: &egui::Context, _frame: &epi::Frame) { let Self { - func_str, + functions, min_x, max_x, - min_x_graph, - max_x_graph, - num_interval, - resolution, - chart_manager, - back_cache, - front_cache, + integral_min_x, + integral_max_x, + integral_num, } = self; // Note: This Instant implementation does not show microseconds when using wasm. @@ -146,21 +104,28 @@ impl epi::App for MathApp { ui.label("- signum, min, max"); }); + let mut func_new_strings: Vec = Vec::new(); let mut parse_error: String = "".to_string(); egui::SidePanel::left("side_panel") .resizable(false) .show(ctx, |ui| { ui.heading("Side Panel"); - ui.horizontal(|ui| { - ui.label("Function: "); - ui.text_edit_singleline(func_str); - }); + for function in functions.iter() { + let mut func_str = function.get_string(); + ui.horizontal(|ui| { + ui.label("Function: "); + ui.text_edit_singleline(&mut func_str); + }); - let func_test_output = test_func(func_str.clone()); - if !func_test_output.is_empty() { - parse_error = func_test_output; + let func_test_output = test_func(func_str.clone()); + if !func_test_output.is_empty() { + parse_error += &func_test_output; + } + + func_new_strings.push(func_str); } + let min_x_old = *min_x; let min_x_response = ui.add(egui::Slider::new(min_x, X_RANGE.clone()).text("Min X")); @@ -178,11 +143,11 @@ impl epi::App for MathApp { *min_x = -10.0; *max_x = 10.0; } - *min_x_graph = *min_x; - *max_x_graph = *max_x; + *integral_min_x = *min_x; + *integral_max_x = *max_x; } - ui.add(egui::Slider::new(num_interval, NUM_INTERVAL_RANGE).text("Interval")); + ui.add(egui::Slider::new(integral_num, INTEGRAL_NUM_RANGE).text("Interval")); // Opensource and Licensing information ui.horizontal(|ui| { @@ -210,30 +175,13 @@ impl epi::App for MathApp { ui.label(GIT_VERSION); } }); - }); - if parse_error.is_empty() { - let do_update = chart_manager.update( - func_str.clone(), - *min_x, - *max_x, - *min_x_graph, - *max_x_graph, - *num_interval, - *resolution, - ); - - // Invalidates caches according to what settings were changed - match do_update { - UpdateType::Full => { - back_cache.invalidate(); - front_cache.invalidate(); + let mut i: usize = 0; + for function in functions.iter_mut() { + function.update(func_new_strings[i].clone(), *min_x, *max_x, true, Some(*integral_min_x), Some(*integral_max_x), Some(*integral_num)); + i += 1; } - UpdateType::Back => back_cache.invalidate(), - UpdateType::Front => front_cache.invalidate(), - _ => {} - } - } + }); egui::CentralPanel::default().show(ctx, |ui| { if !parse_error.is_empty() { @@ -241,26 +189,33 @@ impl epi::App for MathApp { return; } - let (curve, bars, area) = self.get_data(); - - let bar_chart = BarChart::new(bars) - .color(Color32::BLUE) - .width(self.chart_manager.get_step()); - + let step = self.get_step(); + let mut area_list: Vec = Vec::new(); Plot::new("plot") .view_aspect(1.0) .data_aspect(1.0) .include_y(0) .show(ui, |plot_ui| { - plot_ui.line(curve); - plot_ui.bar_chart(bar_chart); + for function in self.functions.iter_mut() { + let output = function.run(); + let back = output.get_back(); + plot_ui.line(Line::new(Values::from_values(back)).color(Color32::RED)); + + if output.has_integral() { + let (bars, area) = output.get_front(); + let bar_chart = + BarChart::new(bars.clone()).color(Color32::BLUE).width(step); + plot_ui.bar_chart(bar_chart); + area_list.push(digits_precision(area, 8)) + } + } }); let duration = start.elapsed(); ui.label(format!( - "Area: {} Took: {:?}", - digits_precision(area, 8), + "Area: {:?} Took: {:?}", + area_list.clone(), duration )); }); diff --git a/src/function.rs b/src/function.rs index 09cfc25..15ade42 100644 --- a/src/function.rs +++ b/src/function.rs @@ -1,9 +1,9 @@ -use meval::Expr; +use crate::misc::Cache; use egui::plot::Value; use egui::widgets::plot::Bar; +use meval::Expr; -const RESOLUTION: f64 = 1000.0; - +pub const RESOLUTION: f64 = 1000.0; // Struct that stores and manages the output of a function pub struct FunctionOutput { @@ -16,16 +16,22 @@ pub struct FunctionOutput { impl FunctionOutput { #[inline] - pub fn new(back: Vec, front: Option<(Vec, f64)>) -> Self { - Self { - back, - front, + pub fn new(back: Vec, front: Option<(Vec, f64)>) -> Self { Self { back, front } } + + #[inline] + pub fn get_back(&self) -> Vec { self.back.clone() } + + #[inline] + pub fn get_front(&self) -> (Vec, f64) { + match &self.front { + Some(x) => (x.0.clone(), x.1.clone()), + None => panic!(""), } } #[inline] - fn has_integral(&self) -> bool { - match self.front { + pub fn has_integral(&self) -> bool { + match &self.front { Some(x) => true, None => false, } @@ -37,6 +43,7 @@ pub struct Function { func_str: String, min_x: f64, max_x: f64, + back_cache: Cache>, front_cache: Cache<(Vec, f64)>, @@ -47,8 +54,10 @@ pub struct Function { } impl Function { - pub fn new(func_str: String, min_x: f64, max_x: f64, integral: bool, integral_min_x: Option, integral_max_x: Option, integral_num: Option) -> Self { - + pub fn new( + func_str: String, min_x: f64, max_x: f64, integral: bool, integral_min_x: Option, + integral_max_x: Option, integral_num: Option, + ) -> Self { // Makes sure proper arguments are passed when integral is enabled if integral { if integral_min_x.is_none() { @@ -80,7 +89,7 @@ impl Function { }, integral_num: match integral_num { Some(x) => x, - None => f64::NAN, + None => 0, }, } } @@ -90,15 +99,24 @@ impl Function { fn run_func(&self, x: f64) -> f64 { (self.function)(x) } #[inline] - pub fn update(&mut self, func_str: String, min_x: f64, max_x: f64, integral: bool, integral_min_x: Option, integral_max_x: Option, integral_num: Option) { - + pub fn update( + &mut self, func_str: String, min_x: f64, max_x: f64, integral: bool, + integral_min_x: Option, integral_max_x: Option, integral_num: Option, + ) { // If the function string changes, just wipe and restart from scratch if func_str != self.func_str { - *self = Self::new(func_str, min_x, max_x, integral, integral_min_x, integral_max_x, integral_num); + *self = Self::new( + func_str, + min_x, + max_x, + integral, + integral_min_x, + integral_max_x, + integral_num, + ); return; } - if (min_x != self.min_x) | (max_x != self.max_x) { self.back_cache.invalidate(); self.min_x = min_x; @@ -115,38 +133,81 @@ impl Function { panic!("Invalid arguments: integral_num is None, but integral is enabled.") } - if (integral_min_x != Some(self.integral_min_x)) | (integral_max_x != Some(self.integral_max_x)) | (integral_num != Some(self.integral_num)) { + if (integral_min_x != Some(self.integral_min_x)) + | (integral_max_x != Some(self.integral_max_x)) + | (integral_num != Some(self.integral_num)) + { self.front_cache.invalidate(); self.integral_min_x = integral_min_x.expect(""); self.integral_max_x = integral_max_x.expect(""); self.integral_num = integral_num.expect(""); } } - } + #[inline] + pub fn get_step(&self) -> f64 { + (self.integral_min_x - self.integral_max_x).abs() / (self.integral_num as f64) + } + + #[inline] + pub fn is_integral(&self) -> bool { self.integral } #[inline] pub fn run(&mut self) -> FunctionOutput { - let absrange = (self.max_x - self.min_x).abs(); - let output: Vec<(f64, f64)> = (1..=self.resolution) - .map(|x| ((x as f64 / RESOLUTION) * absrange) + self.min_x_back) - .map(|x| (x, self.function.run(x))) - .collect(); - output + let front_values: Vec = match self.back_cache.is_valid() { + false => { + let absrange = (self.max_x - self.min_x).abs(); + let front_data: Vec<(f64, f64)> = (1..=(RESOLUTION as usize)) + .map(|x| ((x as f64 / RESOLUTION) * absrange) + self.min_x) + .map(|x| (x, self.run_func(x))) + .collect(); + let output: Vec = + front_data.iter().map(|(x, y)| Value::new(*x, *y)).collect(); + self.back_cache.set(output.clone()); + output + } + true => self.back_cache.get().clone(), + }; + + if self.integral { + let back_bars: (Vec, f64) = match self.front_cache.is_valid() { + false => { + let (data, area) = self.integral_rectangles(); + let bars: Vec = data.iter().map(|(x, y)| Bar::new(*x, *y)).collect(); + + let output = (bars, area); + self.front_cache.set(output.clone()); + output + } + true => { + let cache = self.front_cache.get(); + let vec_bars: Vec = cache.0.to_vec(); + (vec_bars, cache.1) + } + }; + FunctionOutput::new(front_values, Some(back_bars)) + } else { + FunctionOutput::new(front_values, None) + } } + #[inline] + pub fn get_string(&self) -> String { self.func_str.clone() } + #[inline] pub fn str_compare(&self, other_string: String) -> bool { self.func_str == other_string } - #[inline] - pub fn get_step(&self) -> f64 { (self.integral_min_x - self.integral_max_x).abs() / (self.num_interval as f64) } - // Creates and does the math for creating all the rectangles under the graph #[inline] - fn integral_rectangles(&self, step: f64) -> (Vec<(f64, f64)>, f64) { + fn integral_rectangles(&self) -> (Vec<(f64, f64)>, f64) { + if !self.integral { + panic!("integral_rectangles called, but self.integral is false!"); + } + let step = self.get_step(); + let half_step = step / 2.0; - let data2: Vec<(f64, f64)> = (0..self.num_interval) + let data2: Vec<(f64, f64)> = (0..self.integral_num) .map(|e| { let x: f64 = ((e as f64) * step) + self.integral_min_x; @@ -179,4 +240,4 @@ impl Function { let area: f64 = data2.iter().map(|(_, y)| y * step).sum(); // sum of all rectangles' areas (data2, area) } -} \ No newline at end of file +} diff --git a/src/lib.rs b/src/lib.rs index 0fe484c..28fae50 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,9 @@ #![allow(clippy::unused_unit)] // Fixes clippy keep complaining about wasm_bindgen #![allow(clippy::type_complexity)] // Clippy, my types are fine. -mod chart_manager; mod egui_app; -mod misc; mod function; +mod misc; #[cfg(target_arch = "wasm32")] use wasm_bindgen::prelude::*; diff --git a/src/main.rs b/src/main.rs index 653586c..a8f225f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,7 @@ mod egui_app; -// These 2 are needed for rust-analyzer to work in vscode. -mod chart_manager; -mod misc; mod function; +mod misc; // For running the program natively! (Because why not?) #[cfg(not(target_arch = "wasm32"))]