port to egui (WIP)

This commit is contained in:
Simon Gardling
2022-02-23 15:10:08 -05:00
parent eaf858b097
commit 59e4793a1b
15 changed files with 383 additions and 1072 deletions

125
src/chart_manager.rs Normal file
View File

@@ -0,0 +1,125 @@
use crate::misc::{add_asterisks, Cache, Function};
// Manages Chart generation and caching of values
pub struct ChartManager {
function: Function,
min_x: f64,
max_x: f64,
num_interval: usize,
resolution: usize,
back_cache: Cache<Vec<(f64, f64)>>,
front_cache: Cache<(Vec<(f64, f64, f64)>, f64)>,
}
impl ChartManager {
pub fn new(
func_str: String, min_x: f64, max_x: f64, num_interval: usize,
resolution: usize,
) -> Self {
Self {
function: Function::from_string(func_str),
min_x,
max_x,
num_interval,
resolution,
back_cache: Cache::new_empty(),
front_cache: Cache::new_empty(),
}
}
#[inline]
fn draw(
&mut self
) -> (Vec<(f64, f64)>, Vec<(f64, f64, f64)>, f64) {
let absrange = (self.max_x - self.min_x).abs();
let data: Vec<(f64, f64)> = match self.back_cache.is_valid() {
true => self.back_cache.get().clone(),
false => {
let output: Vec<(f64, f64)> = (1..=self.resolution)
.map(|x| ((x as f64 / self.resolution as f64) * absrange) + self.min_x)
.map(|x| (x, self.function.run(x)))
.collect();
self.back_cache.set(output.clone());
output
}
};
let filtered_data: Vec<(f64, f64)> = data
.iter()
.map(|(x, y)| (*x, *y))
.collect();
let (rect_data, area): (Vec<(f64, f64, f64)>, f64) = match self.front_cache.is_valid() {
true => self.front_cache.get().clone(),
false => {
let step = absrange / (self.num_interval as f64);
let output: (Vec<(f64, f64, f64)>, f64) = self.integral_rectangles(step);
self.front_cache.set(output.clone());
output
}
};
(filtered_data, rect_data, area)
}
#[allow(clippy::too_many_arguments)]
pub fn update(
&mut self, func_str_new: String, min_x: f64, max_x: f64, num_interval: usize, resolution: usize,
) -> (Vec<(f64, f64)>, Vec<(f64, f64, f64)>, f64) {
let func_str: String = add_asterisks(func_str_new);
let update_func: bool = !self.function.str_compare(func_str.clone());
let underlying_update = update_func
| (min_x != self.min_x)
| (max_x != self.max_x);
if underlying_update | (self.resolution != resolution) {
self.back_cache.invalidate();
}
if underlying_update | (num_interval != self.num_interval) {
self.front_cache.invalidate();
}
if update_func {
self.function = Function::from_string(func_str);
}
self.min_x = min_x;
self.max_x = max_x;
self.num_interval = num_interval;
self.resolution = resolution;
self.draw()
}
// Creates and does the math for creating all the rectangles under the graph
#[inline]
fn integral_rectangles(&self, step: f64) -> (Vec<(f64, f64, f64)>, f64) {
let data2: Vec<(f64, 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(x);
let tmp2: f64 = self.function.run(x2);
// Chooses the y value who's absolute value is the smallest
let y: f64 = match tmp2.abs() > tmp1.abs() {
true => tmp1,
false => tmp2,
};
(x, x2, y)
})
.filter(|(_, _, y)| !y.is_nan())
.collect();
let area: f64 = data2.iter().map(|(_, _, y)| y * step).sum(); // sum of all rectangles' areas
(data2, area)
}
}

94
src/egui_app.rs Normal file
View File

@@ -0,0 +1,94 @@
use eframe::{egui, epi};
use egui::{plot::{HLine, Line, Plot, Value, Values, Text}, Pos2};
use crate::chart_manager::ChartManager;
use meval::Expr;
use crate::misc::{add_asterisks, Cache, Function};
use egui::{Color32, ColorImage, Ui};
use emath::Rect;
use epaint::{Rounding, RectShape, Stroke};
use egui::widgets::plot::{Bar, BarChart};
pub struct MathApp {
func_str: String,
min_x: f64,
max_x: f64,
num_interval: usize,
resolution: usize,
chart_manager: ChartManager,
}
impl Default for MathApp {
fn default() -> Self {
Self {
func_str: "x^2".to_string(),
min_x: -10.0,
max_x: 10.0,
num_interval: 100,
resolution: 10000,
chart_manager: ChartManager::new("x^2".to_string(), -10.0, 10.0, 100, 10000)
}
}
}
impl epi::App for MathApp {
fn name(&self) -> &str {
"eframe template"
}
/// Called once before the first frame.
fn setup(
&mut self,
_ctx: &egui::Context,
_frame: &epi::Frame,
_storage: Option<&dyn epi::Storage>,
) { }
/// Called each time the UI needs repainting, which may be many times per second.
/// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
fn update(&mut self, ctx: &egui::Context, frame: &epi::Frame) {
let Self {
func_str,
min_x,
max_x,
num_interval,
resolution,
chart_manager
} = self;
egui::SidePanel::left("side_panel").show(ctx, |ui| {
ui.heading("Side Panel");
ui.horizontal(|ui| {
ui.label("Function: ");
ui.text_edit_singleline(func_str);
});
ui.add(egui::Slider::new(min_x, -1000.0..=1000.0).text("Min X"));
ui.add(egui::Slider::new(max_x, *min_x..=1000.0).text("Max X"));
ui.add(egui::Slider::new(num_interval, 0..=usize::MAX).text("Interval"));
});
egui::CentralPanel::default().show(ctx, |ui| {
let (filtered_data, rect_data, area) = chart_manager.update(self.func_str.clone(), self.min_x, self.max_x, self.num_interval, self.resolution);
let filtered_data_values = filtered_data.iter().map(|(x, y)| Value::new(*x, *y)).collect();
let curve = Line::new(Values::from_values(filtered_data_values)).color(Color32::RED);
let bars = rect_data.iter().map(|(_, x2, y)| Bar::new(*x2, *y)).collect();
let barchart = BarChart::new(bars).color(Color32::BLUE);
// ui.label("Graph:");
ui.label(format!("Area: {:.10}", area));
Plot::new("plot")
.view_aspect(1.0)
.include_y(0)
.show(ui, |plot_ui| {
plot_ui.line(curve);
plot_ui.bar_chart(barchart);
});
});
}
}

View File

@@ -1,18 +1,22 @@
#![allow(clippy::unused_unit)] // Fixes clippy keep complaining about wasm_bindgen
#![allow(clippy::type_complexity)] // Clippy, my types are fine.
use meval::Expr;
use plotters::prelude::*;
use plotters_canvas::CanvasBackend;
use std::panic;
use wasm_bindgen::prelude::*;
use web_sys::HtmlCanvasElement;
mod misc;
use crate::misc::{add_asterisks, Cache, ChartOutput, DrawResult, Function};
mod egui_app;
mod chart_manager;
use std::panic;
#[cfg(target_arch = "wasm32")]
use eframe::{epi, egui};
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
#[cfg(target_arch = "wasm32")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
extern "C" {
// Use `js_namespace` here to bind `console.log(..)` instead of just
@@ -21,8 +25,9 @@ extern "C" {
fn log(s: &str);
}
#[wasm_bindgen(start)]
pub fn init() {
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn start(canvas_id: &str) -> Result<(), wasm_bindgen::JsValue> {
log("Initializing...");
// See performance in browser profiler!
@@ -36,244 +41,8 @@ pub fn init() {
log("Initialized console_error_panic_hook!");
log("Finished initializing!");
}
// Manages Chart generation and caching of values
#[wasm_bindgen]
pub struct ChartManager {
function: Function,
min_x: f32,
max_x: f32,
min_y: f32,
max_y: f32,
num_interval: usize,
resolution: usize,
back_cache: Cache<Vec<(f32, f32)>>,
front_cache: Cache<(Vec<(f32, f32, f32)>, f32)>,
}
#[wasm_bindgen]
impl ChartManager {
pub fn new(
func_str: String, min_x: f32, max_x: f32, min_y: f32, max_y: f32, num_interval: usize,
resolution: usize,
) -> Self {
Self {
function: Function::from_string(func_str),
min_x,
max_x,
min_y,
max_y,
num_interval,
resolution,
back_cache: Cache::new_empty(),
front_cache: Cache::new_empty(),
}
}
// Tests function to make sure it's able to be parsed. Returns the string of the Error produced, or an empty string if it runs successfully.
pub fn test_func(function_string: String) -> String {
// Factorials do not work, and it would be really difficult to make them work
if function_string.contains('!') {
return "Factorials are unsupported".to_string();
}
let new_func_str: String = add_asterisks(function_string);
let expr_result = new_func_str.parse();
let expr_error = match &expr_result {
Ok(_) => "".to_string(),
Err(error) => format!("{}", error),
};
if !expr_error.is_empty() {
return expr_error;
}
let expr: Expr = expr_result.unwrap();
let func_result = expr.bind("x");
let func_error = match &func_result {
Ok(_) => "".to_string(),
Err(error) => format!("{}", error),
};
if !func_error.is_empty() {
return func_error;
}
"".to_string()
}
#[inline]
fn draw(
&mut self, element: HtmlCanvasElement, dark_mode: bool,
) -> DrawResult<(impl Fn((i32, i32)) -> Option<(f32, f32)>, f32)> {
log("Drawing...");
let backend = CanvasBackend::with_canvas_object(element).unwrap();
let root = backend.into_drawing_area();
let font: FontDesc = ("sans-serif", 20.0).into();
if dark_mode {
root.fill(&RGBColor(28, 28, 28))?;
} else {
root.fill(&WHITE)?;
}
let mut chart = ChartBuilder::on(&root)
.margin(20.0)
.caption(format!("y={}", self.function.get_string()), font)
.x_label_area_size(30.0)
.y_label_area_size(30.0)
.build_cartesian_2d(self.min_x..self.max_x, self.min_y..self.max_y)?;
if dark_mode {
chart
.configure_mesh()
.x_labels(3)
.y_labels(3)
.light_line_style(&RGBColor(254, 254, 254))
.draw()?;
} else {
chart.configure_mesh().x_labels(3).y_labels(3).draw()?;
}
let absrange = (self.max_x - self.min_x).abs();
let data: Vec<(f32, f32)> = match self.back_cache.is_valid() {
true => self.back_cache.get().clone(),
false => {
log("Updating back_cache");
let output: Vec<(f32, f32)> = (1..=self.resolution)
.map(|x| ((x as f32 / self.resolution as f32) * absrange) + self.min_x)
.map(|x| (x, self.function.run(x)))
.collect();
self.back_cache.set(output.clone());
output
}
};
let filtered_data: Vec<(f32, f32)> = data
.iter()
.filter(|(_, y)| &self.min_y <= y && y <= &self.max_y)
.map(|(x, y)| (*x, *y))
.collect();
chart.draw_series(LineSeries::new(filtered_data, &RED))?;
let (rect_data, area): (Vec<(f32, f32, f32)>, f32) = match self.front_cache.is_valid() {
true => self.front_cache.get().clone(),
false => {
log("Updating front_cache");
let step = absrange / (self.num_interval as f32);
let output: (Vec<(f32, f32, f32)>, f32) = self.integral_rectangles(step);
self.front_cache.set(output.clone());
output
}
};
if self.num_interval <= 200 {
// Draw rectangles
chart.draw_series(
rect_data
.iter()
.map(|(x1, x2, y)| Rectangle::new([(*x2, *y), (*x1, 0.0)], &BLUE)),
)?;
} else {
// Save resources by not graphing rectangles and using an AreaSeries when you can no longer see the rectangles
let capped_data: Vec<(f32, f32)> = data
.iter()
.map(|(x, y)| {
if y.is_nan() {
return (*x, 0.0);
}
let new_y: &f32 = if y > &self.max_y {
&self.max_y
} else if &self.min_y > y {
&self.min_y
} else {
y
};
(*x, *new_y)
})
.collect();
chart.draw_series(AreaSeries::new(capped_data, 0.0, &BLUE))?;
}
root.present()?;
log("Finished Drawing!");
Ok((chart.into_coord_trans(), area))
}
#[allow(clippy::too_many_arguments)]
pub fn update(
&mut self, canvas: HtmlCanvasElement, func_str_new: String, min_x: f32, max_x: f32,
min_y: f32, max_y: f32, num_interval: usize, resolution: usize, dark_mode: bool,
) -> Result<ChartOutput, JsValue> {
let func_str: String = add_asterisks(func_str_new);
let update_func: bool = !self.function.str_compare(func_str.clone());
let underlying_update = update_func
| (min_x != self.min_x)
| (max_x != self.max_x)
| (min_y != self.min_y)
| (max_y != self.max_y);
if underlying_update | (self.resolution != resolution) {
self.back_cache.invalidate();
}
if underlying_update | (num_interval != self.num_interval) {
self.front_cache.invalidate();
}
if update_func {
self.function = Function::from_string(func_str);
}
self.min_x = min_x;
self.max_x = max_x;
self.min_y = min_y;
self.max_y = max_y;
self.num_interval = num_interval;
self.resolution = resolution;
let draw_output = self
.draw(canvas, dark_mode)
.map_err(|err| err.to_string())?;
let map_coord = draw_output.0;
let chart_output = ChartOutput {
convert: Box::new(move |coord| map_coord(coord).map(|(x, y)| (x, y))),
area: draw_output.1,
};
Ok(chart_output)
}
// Creates and does the math for creating all the rectangles under the graph
#[inline]
fn integral_rectangles(&self, step: f32) -> (Vec<(f32, f32, f32)>, f32) {
let data2: Vec<(f32, f32, f32)> = (0..self.num_interval)
.map(|e| {
let x: f32 = ((e as f32) * step) + self.min_x;
// Makes sure rectangles are properly handled on x values below 0
let x2: f32 = match x > 0.0 {
true => x + step,
false => x - step,
};
let tmp1: f32 = self.function.run(x);
let tmp2: f32 = self.function.run(x2);
// Chooses the y value who's absolute value is the smallest
let y: f32 = match tmp2.abs() > tmp1.abs() {
true => tmp1,
false => tmp2,
};
(x, x2, y)
})
.filter(|(_, _, y)| !y.is_nan())
.collect();
let area: f32 = data2.iter().map(|(_, _, y)| y * step).sum(); // sum of all rectangles' areas
(data2, area)
}
log("Starting App...");
let app = egui_app::MathApp::default();
eframe::start_web(canvas_id, Box::new(app))
}

16
src/main.rs Normal file
View File

@@ -0,0 +1,16 @@
use eframe::{epi, egui};
mod egui_app;
mod misc;
mod chart_manager;
#[cfg(not(target_arch = "wasm32"))]
fn main() {
let app = egui_app::MathApp::default();
let options = eframe::NativeOptions {
// Let's show off that we support transparent windows
transparent: true,
drag_and_drop_support: true,
..Default::default()
};
eframe::run_native(Box::new(app), options);
}

View File

@@ -1,7 +1,4 @@
use meval::Expr;
use wasm_bindgen::prelude::*;
pub type DrawResult<T> = Result<T, Box<dyn std::error::Error>>;
/*
EXTREMELY Janky function that tries to put asterisks in the proper places to be parsed. This is so cursed. But it works, and I hopefully won't ever have to touch it again.
@@ -80,6 +77,36 @@ pub fn add_asterisks(function_in: String) -> String {
output_string.replace('π', "pi") // π -> pi
}
// Tests function to make sure it's able to be parsed. Returns the string of the Error produced, or an empty string if it runs successfully.
pub fn test_func(function_string: String) -> String {
// Factorials do not work, and it would be really difficult to make them work
if function_string.contains('!') {
return "Factorials are unsupported".to_string();
}
let new_func_str: String = add_asterisks(function_string);
let expr_result = new_func_str.parse();
let expr_error = match &expr_result {
Ok(_) => "".to_string(),
Err(error) => format!("{}", error),
};
if !expr_error.is_empty() {
return expr_error;
}
let expr: Expr = expr_result.unwrap();
let func_result = expr.bind("x");
let func_error = match &func_result {
Ok(_) => "".to_string(),
Err(error) => format!("{}", error),
};
if !func_error.is_empty() {
return func_error;
}
"".to_string()
}
pub struct Function {
function: Box<dyn Fn(f64) -> f64>,
func_str: String,
@@ -96,41 +123,13 @@ impl Function {
}
#[inline]
pub fn run(&self, x: f32) -> f32 { (self.function)(x as f64) as f32 }
pub fn run(&self, x: f64) -> f64 { (self.function)(x) }
pub fn str_compare(&self, other_string: String) -> bool { self.func_str == other_string }
pub fn get_string(&self) -> String { self.func_str.clone() }
}
/// Result of screen to chart coordinates conversion.
#[wasm_bindgen]
pub struct Point {
pub x: f32,
pub y: f32,
}
#[wasm_bindgen]
impl Point {
#[inline]
pub fn new(x: f32, y: f32) -> Self { Self { x, y } }
}
#[wasm_bindgen]
pub struct ChartOutput {
pub(crate) convert: Box<dyn Fn((i32, i32)) -> Option<(f32, f32)>>,
pub(crate) area: f32,
}
#[wasm_bindgen]
impl ChartOutput {
pub fn get_area(&self) -> f32 { self.area }
pub fn coord(&self, x: i32, y: i32) -> Option<Point> {
(self.convert)((x, y)).map(|(x, y)| Point::new(x, y))
}
}
pub struct Cache<T> {
backing_data: Option<T>,
}