Compare commits

..

16 Commits

Author SHA1 Message Date
ac6265eae7 address egui deprecation warnings 2025-12-13 03:41:10 -05:00
c70c715126 cleanup 2025-12-13 03:38:04 -05:00
f2d0d27345 parsing: improve function handling 2025-12-13 03:33:20 -05:00
41b50eb893 remove decompress font cache 2025-12-05 23:54:31 -05:00
e497987573 fix target wasm cfg 2025-12-05 23:50:24 -05:00
65ab0c6a1f FunctionEntry: make newtons_method threshold smaller (fixes symbolicrepr) + prioritize special points 2025-12-05 22:37:05 -05:00
f6a09fe449 start to integrate symbolic points 2025-12-05 21:05:15 -05:00
b08a727fe3 remove some unwrap_unchecked 2025-12-05 20:42:24 -05:00
5480522ddb cargo fmt 2025-12-05 20:38:41 -05:00
07858b229f parsing: remove unused code from FlatExWrapper 2025-12-05 20:38:31 -05:00
3288752dfb parser: simplify BoolSlice logic with descriptive helper methods 2025-12-05 20:38:23 -05:00
3305227ffe refactor: simplify Newton's method helper by extracting data source selection 2025-12-05 20:38:17 -05:00
df05601e26 fix: correct typo nth_derviative -> nth_derivative 2025-12-05 20:38:11 -05:00
48fd49e386 deduplicate declarations in main.rs and lib.rs 2025-12-05 20:37:53 -05:00
c7760e2123 FunctionEntry: generate_plot_data helper 2025-12-05 20:37:36 -05:00
2d7c987f11 math_app: extract toggle_button helper to reduce UI code duplication 2025-12-05 20:37:24 -05:00
14 changed files with 506 additions and 442 deletions

1
Cargo.lock generated
View File

@@ -4145,7 +4145,6 @@ dependencies = [
"base64", "base64",
"benchmarks", "benchmarks",
"bincode", "bincode",
"cfg-if",
"eframe", "eframe",
"egui", "egui",
"egui_plot", "egui_plot",

View File

@@ -48,7 +48,6 @@ epaint = { git = "https://github.com/titaniumtown/egui.git", default-features =
emath = { git = "https://github.com/titaniumtown/egui.git", default-features = false } emath = { git = "https://github.com/titaniumtown/egui.git", default-features = false }
egui_plot = { version = "0.34.0", default-features = false } egui_plot = { version = "0.34.0", default-features = false }
cfg-if = "1"
ruzstd = "0.8" ruzstd = "0.8"
tracing = "0.1" tracing = "0.1"
itertools = "0.14" itertools = "0.14"

View File

@@ -6,8 +6,8 @@ use allsorts::{
tag, tag,
}; };
use epaint::{ use epaint::{
text::{FontData, FontDefinitions, FontTweak},
FontFamily, FontFamily,
text::{FontData, FontDefinitions, FontTweak},
}; };
use std::{ use std::{
collections::BTreeMap, collections::BTreeMap,

View File

@@ -4,21 +4,14 @@ use std::collections::HashMap;
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub struct FlatExWrapper { pub struct FlatExWrapper {
func: Option<FlatEx<f64>>, func: Option<FlatEx<f64>>,
func_str: Option<String>,
} }
impl FlatExWrapper { impl FlatExWrapper {
const EMPTY: FlatExWrapper = FlatExWrapper { const EMPTY: FlatExWrapper = FlatExWrapper { func: None };
func: None,
func_str: None,
};
#[inline] #[inline]
const fn new(f: FlatEx<f64>) -> Self { const fn new(f: FlatEx<f64>) -> Self {
Self { Self { func: Some(f) }
func: Some(f),
func_str: None,
}
} }
#[inline] #[inline]
@@ -34,26 +27,6 @@ impl FlatExWrapper {
.unwrap_or(f64::NAN) .unwrap_or(f64::NAN)
} }
#[inline]
fn partial(&self, x: usize) -> Self {
self.func
.as_ref()
.map(|f| f.clone().partial(x).map(Self::new).unwrap_or(Self::EMPTY))
.unwrap_or(Self::EMPTY)
}
#[inline]
fn get_string(&mut self) -> String {
match self.func_str {
Some(ref func_str) => func_str.clone(),
None => {
let calculated = self.func.as_ref().map(|f| f.unparse()).unwrap_or("");
self.func_str = Some(calculated.to_owned());
calculated.to_owned()
}
}
}
#[inline] #[inline]
fn partial_iter(&self, n: usize) -> Self { fn partial_iter(&self, n: usize) -> Self {
self.func self.func
@@ -165,18 +138,6 @@ impl BackingFunction {
} }
} }
fn prettyify_function_str(func: &str) -> String {
let new_str = func.replace("{x}", "x");
if &new_str == "0/0" {
"Undefined".to_owned()
} else {
new_str
}
}
// pub const VALID_VARIABLES: [char; 3] = ['x', 'e', 'π'];
/// Case insensitive checks for if `c` is a character used to represent a variable /// Case insensitive checks for if `c` is a character used to represent a variable
#[inline] #[inline]
pub const fn is_variable(c: &char) -> bool { pub const fn is_variable(c: &char) -> bool {

View File

@@ -1,17 +1,61 @@
use crate::parsing::is_variable; use crate::parsing::is_variable;
use crate::SUPPORTED_FUNCTIONS;
/// Protect function names that start with variable characters (like 'e' in 'exp')
/// by replacing them with a placeholder during parsing, then restoring them after.
/// This prevents incorrect splitting like "exp" -> "e" * "xp".
fn protect_function_names(input: &str) -> (String, Vec<(&'static str, String)>) {
let mut result = input.to_string();
let mut replacements = Vec::new();
// Only protect functions that start with a variable character
// Sort by length descending to replace longer matches first (e.g., "exp" before "e")
let mut funcs: Vec<&'static str> = SUPPORTED_FUNCTIONS
.iter()
.filter(|&&func| {
func.chars()
.next()
.map(|c| is_variable(&c))
.unwrap_or(false)
})
.copied()
.collect();
funcs.sort_by(|a, b| b.len().cmp(&a.len()));
for func in funcs {
// Use a placeholder made of letters that will be treated as a function name
// The placeholder won't be split because it's all letters
let placeholder = format!("zzzfn{}", replacements.len());
result = result.replace(func, &placeholder);
replacements.push((func, placeholder));
}
(result, replacements)
}
/// Restore protected function names from their placeholders
fn restore_function_names(input: &str, replacements: &[(&'static str, String)]) -> String {
let mut result = input.to_string();
for (func, placeholder) in replacements {
result = result.replace(placeholder, func);
}
result
}
pub fn split_function(input: &str, split: SplitType) -> Vec<String> { pub fn split_function(input: &str, split: SplitType) -> Vec<String> {
// Protect function names that could be incorrectly split
let (protected, replacements) = protect_function_names(input);
split_function_chars( split_function_chars(
&input &protected
.replace("pi", "π") // replace "pi" text with pi symbol .replace("pi", "π") // replace "pi" text with pi symbol
.replace("**", "^") // support alternate manner of expressing exponents .replace("**", "^") // support alternate manner of expressing exponents
.replace("exp", "\u{1fc93}") // stop-gap solution to fix the `exp` function
.chars() .chars()
.collect::<Vec<char>>(), .collect::<Vec<char>>(),
split, split,
) )
.iter() .iter()
.map(|x| x.replace('\u{1fc93}', "exp")) // Convert back to `exp` text .map(|x| restore_function_names(x, &replacements))
.collect::<Vec<String>>() .collect::<Vec<String>>()
} }
@@ -62,46 +106,73 @@ impl BoolSlice {
self.number && !self.masked_num self.number && !self.masked_num
} }
/// Returns true if this char is a function name letter (not a standalone variable)
const fn is_function_letter(&self) -> bool {
self.letter && !self.is_unmasked_variable()
}
/// Returns true if this is a "term" - something that can be multiplied
const fn is_term(&self) -> bool {
self.is_unmasked_number() || self.is_unmasked_variable() || self.letter
}
const fn calculate_mask(&mut self, other: &BoolSlice) { const fn calculate_mask(&mut self, other: &BoolSlice) {
if other.masked_num && self.number { if other.masked_num && self.number {
// If previous char was a masked number, and current char is a number, mask current char's variable status // Propagate number masking through consecutive digits
self.masked_num = true; self.masked_num = true;
} else if other.masked_var && self.variable { } else if other.masked_var && self.variable {
// If previous char was a masked variable, and current char is a variable, mask current char's variable status // Propagate variable masking through consecutive variables
self.masked_var = true; self.masked_var = true;
} else if other.letter && !other.is_unmasked_variable() { } else if other.is_function_letter() {
// After a function letter, mask following numbers/variables as part of function name
self.masked_num = self.number; self.masked_num = self.number;
self.masked_var = self.variable; self.masked_var = self.variable;
} }
} }
const fn splitable(&self, c: &char, other: &BoolSlice, split: &SplitType) -> bool { /// Determines if we should split (insert implicit multiplication) before current char
if (*c == '*') | (matches!(split, &SplitType::Term) && other.open_parens) { const fn splitable(&self, c: &char, prev: &BoolSlice, split: &SplitType) -> bool {
true // Always split on explicit multiplication
} else if other.closing_parens { if *c == '*' {
// Cases like `)x`, `)2`, and `)(`
return (*c == '(')
| (self.letter && !self.is_unmasked_variable())
| self.is_unmasked_variable()
| self.is_unmasked_number();
} else if *c == '(' {
// Cases like `x(` and `2(`
return (other.is_unmasked_variable() | other.is_unmasked_number()) && !other.letter;
} else if other.is_unmasked_number() {
// Cases like `2x` and `2sin(x)`
return self.is_unmasked_variable() | self.letter;
} else if self.is_unmasked_variable() | self.letter {
// Cases like `e2` and `xx`
return other.is_unmasked_number()
| (other.is_unmasked_variable() && self.is_unmasked_variable())
| other.is_unmasked_variable();
} else if (self.is_unmasked_number() | self.letter | self.is_unmasked_variable())
&& (other.is_unmasked_number() | other.letter)
{
return true; return true;
} else {
return self.is_unmasked_number() && other.is_unmasked_variable();
} }
// For Term split type, also split after open parens
if matches!(split, &SplitType::Term) && prev.open_parens {
return true;
}
// After closing paren: split before `(`, letters, variables, or numbers
// e.g., `)x`, `)2`, `)(`, `)sin`
if prev.closing_parens {
return *c == '(' || self.is_term();
}
// Before open paren: split if previous was a standalone number or variable
// e.g., `x(`, `2(` but not `sin(`
if *c == '(' {
return (prev.is_unmasked_variable() || prev.is_unmasked_number()) && !prev.letter;
}
// After a number: split before variables or function letters
// e.g., `2x`, `2sin`
if prev.is_unmasked_number() {
return self.is_unmasked_variable() || self.letter;
}
// Current is a variable or letter: split if previous was a number or variable
// e.g., `e2`, `xx`, `xe`
if self.is_unmasked_variable() || self.letter {
return prev.is_unmasked_number() || prev.is_unmasked_variable();
}
// Current is a number after a variable
// e.g., `x2`
if self.is_unmasked_number() && prev.is_unmasked_variable() {
return true;
}
false
} }
} }
@@ -205,4 +276,13 @@ fn split_function_test() {
&["2", "sin(π) + 2", "cos(tau)"], &["2", "sin(π) + 2", "cos(tau)"],
SplitType::Multiplication, SplitType::Multiplication,
); );
// Test that exp() function is properly handled (not split into e*xp)
assert_test("exp(x)", &["exp(x)"], SplitType::Multiplication);
assert_test("2exp(x)", &["2", "exp(x)"], SplitType::Multiplication);
assert_test(
"exp(x)sin(x)",
&["exp(x)", "sin(x)"],
SplitType::Multiplication,
);
} }

View File

@@ -1,16 +1,14 @@
use crate::math_app::AppSettings; use crate::math_app::AppSettings;
use crate::misc::{EguiHelper, newtons_method_helper, step_helper}; use crate::misc::{EguiHelper, newtons_method_helper, step_helper};
use crate::symbolic::try_symbolic;
use egui::{Checkbox, Context}; use egui::{Checkbox, Context};
use egui_plot::{Bar, BarChart, PlotPoint, PlotUi}; use egui_plot::{Bar, BarChart, PlotPoint, PlotUi, Points};
use epaint::Color32; use epaint::Color32;
use parsing::{AutoComplete, generate_hint}; use parsing::{AutoComplete, generate_hint};
use parsing::{BackingFunction, process_func_str}; use parsing::{BackingFunction, process_func_str};
use serde::{Deserialize, Deserializer, Serialize, Serializer, ser::SerializeStruct}; use serde::{Deserialize, Deserializer, Serialize, Serializer, ser::SerializeStruct};
use std::{ use std::fmt::{self, Debug};
fmt::{self, Debug},
hash::{Hash, Hasher},
};
/// Represents the possible variations of Riemann Sums /// Represents the possible variations of Riemann Sums
#[derive(PartialEq, Eq, Debug, Copy, Clone, Default)] #[derive(PartialEq, Eq, Debug, Copy, Clone, Default)]
@@ -34,16 +32,13 @@ pub struct FunctionEntry {
/// The `BackingFunction` instance that is used to generate `f(x)`, `f'(x)`, and `f''(x)` /// The `BackingFunction` instance that is used to generate `f(x)`, `f'(x)`, and `f''(x)`
function: BackingFunction, function: BackingFunction,
/// Stores a function string (that hasn't been processed via `process_func_str`) to display to the user
pub raw_func_str: String,
/// If calculating/displayingintegrals are enabled /// If calculating/displayingintegrals are enabled
pub integral: bool, pub integral: bool,
/// If displaying derivatives are enabled (note, they are still calculated for other purposes) /// If displaying derivatives are enabled (note, they are still calculated for other purposes)
pub derivative: bool, pub derivative: bool,
pub nth_derviative: bool, pub nth_derivative: bool,
pub back_data: Vec<PlotPoint>, pub back_data: Vec<PlotPoint>,
pub integral_data: Option<(Vec<Bar>, f64)>, pub integral_data: Option<(Vec<Bar>, f64)>,
@@ -60,23 +55,13 @@ pub struct FunctionEntry {
pub settings_opened: bool, pub settings_opened: bool,
} }
impl Hash for FunctionEntry {
fn hash<H: Hasher>(&self, state: &mut H) {
self.raw_func_str.hash(state);
self.integral.hash(state);
self.nth_derviative.hash(state);
self.curr_nth.hash(state);
self.settings_opened.hash(state);
}
}
impl Serialize for FunctionEntry { impl Serialize for FunctionEntry {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where where
S: Serializer, S: Serializer,
{ {
let mut s = serializer.serialize_struct("FunctionEntry", 4)?; let mut s = serializer.serialize_struct("FunctionEntry", 4)?;
s.serialize_field("raw_func_str", &self.raw_func_str)?; s.serialize_field("raw_func_str", &self.autocomplete.string)?;
s.serialize_field("integral", &self.integral)?; s.serialize_field("integral", &self.integral)?;
s.serialize_field("derivative", &self.derivative)?; s.serialize_field("derivative", &self.derivative)?;
s.serialize_field("curr_nth", &self.curr_nth)?; s.serialize_field("curr_nth", &self.curr_nth)?;
@@ -125,10 +110,9 @@ impl Default for FunctionEntry {
fn default() -> FunctionEntry { fn default() -> FunctionEntry {
FunctionEntry { FunctionEntry {
function: BackingFunction::default(), function: BackingFunction::default(),
raw_func_str: String::new(),
integral: false, integral: false,
derivative: false, derivative: false,
nth_derviative: false, nth_derivative: false,
back_data: Vec::new(), back_data: Vec::new(),
integral_data: None, integral_data: None,
derivative_data: Vec::new(), derivative_data: Vec::new(),
@@ -150,14 +134,14 @@ impl FunctionEntry {
pub fn settings_window(&mut self, ctx: &Context) { pub fn settings_window(&mut self, ctx: &Context) {
let mut invalidate_nth = false; let mut invalidate_nth = false;
egui::Window::new(format!("Settings: {}", self.raw_func_str)) egui::Window::new(format!("Settings: {}", self.func_str()))
.open(&mut self.settings_opened) .open(&mut self.settings_opened)
.default_pos([200.0, 200.0]) .default_pos([200.0, 200.0])
.resizable(false) .resizable(false)
.collapsible(false) .collapsible(false)
.show(ctx, |ui| { .show(ctx, |ui| {
ui.add(Checkbox::new( ui.add(Checkbox::new(
&mut self.nth_derviative, &mut self.nth_derivative,
"Display Nth Derivative", "Display Nth Derivative",
)); ));
@@ -180,13 +164,32 @@ impl FunctionEntry {
&self.test_result &self.test_result
} }
/// Get the raw function string
#[inline]
pub fn func_str(&self) -> &str {
&self.autocomplete.string
}
/// Update function string and test it /// Update function string and test it
pub fn update_string(&mut self, raw_func_str: &str) { pub fn update_string(&mut self, raw_func_str: &str) {
if raw_func_str == self.raw_func_str { if raw_func_str == self.func_str() {
return; return;
} }
self.raw_func_str = raw_func_str.to_owned(); // Update the autocomplete string (which is now the source of truth for the raw string)
self.autocomplete.update_string(raw_func_str);
self.reparse_function(raw_func_str);
}
/// Re-parse the function from the current autocomplete string.
/// Call this when the autocomplete string was updated externally (e.g., via hint application).
pub fn sync_from_autocomplete(&mut self) {
self.reparse_function(&self.autocomplete.string.clone());
}
/// Internal helper to parse a function string and update internal state
fn reparse_function(&mut self, raw_func_str: &str) {
let processed_func = process_func_str(raw_func_str); let processed_func = process_func_str(raw_func_str);
let new_func_result = BackingFunction::new(&processed_func); let new_func_result = BackingFunction::new(&processed_func);
@@ -212,17 +215,16 @@ impl FunctionEntry {
) -> (Vec<(f64, f64)>, f64) { ) -> (Vec<(f64, f64)>, f64) {
let step = (integral_max_x - integral_min_x) / (integral_num as f64); let step = (integral_max_x - integral_min_x) / (integral_num as f64);
// let sum_func = self.get_sum_func(sum); let data: Vec<(f64, f64)> = step_helper(integral_num, integral_min_x, step)
let data2: Vec<(f64, f64)> = step_helper(integral_num, integral_min_x, step)
.into_iter() .into_iter()
.map(|x| { .map(|x| {
let step_offset = step.copysign(x); // store the offset here so it doesn't have to be calculated multiple times let step_offset = step.copysign(x);
let x2: f64 = x + step_offset; let x2 = x + step_offset;
let (left_x, right_x) = match x.is_sign_positive() { let (left_x, right_x) = if x.is_sign_positive() {
true => (x, x2), (x, x2)
false => (x2, x), } else {
(x2, x)
}; };
let y = match sum { let y = match sum {
@@ -238,9 +240,9 @@ impl FunctionEntry {
.filter(|(_, y)| y.is_finite()) .filter(|(_, y)| y.is_finite())
.collect(); .collect();
let area = data2.iter().map(move |(_, y)| y * step).sum(); let area = data.iter().map(|(_, y)| y * step).sum();
(data2, area) (data, area)
} }
/// Helps with processing newton's method depending on level of derivative /// Helps with processing newton's method depending on level of derivative
@@ -252,30 +254,36 @@ impl FunctionEntry {
) -> Vec<PlotPoint> { ) -> Vec<PlotPoint> {
self.function.generate_derivative(derivative_level); self.function.generate_derivative(derivative_level);
self.function.generate_derivative(derivative_level + 1); self.function.generate_derivative(derivative_level + 1);
let newtons_method_output: Vec<f64> = match derivative_level {
0 => newtons_method_helper( let data_source = match derivative_level {
threshold, 0 => self.back_data.as_slice(),
range, 1 => self.derivative_data.as_slice(),
self.back_data.as_slice(),
self.function.get_function_derivative(0),
self.function.get_function_derivative(1),
),
1 => newtons_method_helper(
threshold,
range,
self.derivative_data.as_slice(),
self.function.get_function_derivative(1),
self.function.get_function_derivative(2),
),
_ => unreachable!(), _ => unreachable!(),
}; };
newtons_method_output newtons_method_helper(
threshold,
range,
data_source,
self.function.get_function_derivative(derivative_level),
self.function.get_function_derivative(derivative_level + 1),
)
.into_iter() .into_iter()
.map(|x| PlotPoint::new(x, self.function.get(0, x))) .map(|x| PlotPoint::new(x, self.function.get(0, x)))
.collect() .collect()
} }
/// Generates plot data for a given derivative level over the resolution iterator
fn generate_plot_data(&mut self, derivative: usize, resolution_iter: &[f64]) -> Vec<PlotPoint> {
if derivative > 0 {
self.function.generate_derivative(derivative);
}
resolution_iter
.iter()
.map(|&x| PlotPoint::new(x, self.function.get(derivative, x)))
.collect()
}
/// Does the calculations and stores results in `self` /// Does the calculations and stores results in `self`
pub fn calculate( pub fn calculate(
&mut self, &mut self,
@@ -304,32 +312,17 @@ impl FunctionEntry {
} }
if self.back_data.is_empty() { if self.back_data.is_empty() {
let data: Vec<PlotPoint> = resolution_iter self.back_data = self.generate_plot_data(0, &resolution_iter);
.clone() debug_assert_eq!(self.back_data.len(), settings.plot_width + 1);
.into_iter()
.map(|x| PlotPoint::new(x, self.function.get(0, x)))
.collect();
debug_assert_eq!(data.len(), settings.plot_width + 1);
self.back_data = data;
} }
if self.derivative_data.is_empty() { if self.derivative_data.is_empty() {
self.function.generate_derivative(1); self.derivative_data = self.generate_plot_data(1, &resolution_iter);
let data: Vec<PlotPoint> = resolution_iter debug_assert_eq!(self.derivative_data.len(), settings.plot_width + 1);
.clone()
.into_iter()
.map(|x| PlotPoint::new(x, self.function.get(1, x)))
.collect();
debug_assert_eq!(data.len(), settings.plot_width + 1);
self.derivative_data = data;
} }
if self.nth_derviative && self.nth_derivative_data.is_none() { if self.nth_derivative && self.nth_derivative_data.is_none() {
let data: Vec<PlotPoint> = resolution_iter let data = self.generate_plot_data(self.curr_nth, &resolution_iter);
.into_iter()
.map(|x| PlotPoint::new(x, self.function.get(self.curr_nth, x)))
.collect();
debug_assert_eq!(data.len(), settings.plot_width + 1); debug_assert_eq!(data.len(), settings.plot_width + 1);
self.nth_derivative_data = Some(data); self.nth_derivative_data = Some(data);
} }
@@ -352,7 +345,7 @@ impl FunctionEntry {
self.clear_integral(); self.clear_integral();
} }
let threshold: f64 = resolution / 2.0; let threshold: f64 = f64::EPSILON;
let x_range = settings.min_x..settings.max_x; let x_range = settings.min_x..settings.max_x;
// Calculates extrema // Calculates extrema
@@ -385,7 +378,11 @@ impl FunctionEntry {
let step = (settings.max_x - settings.min_x) / (settings.plot_width as f64); let step = (settings.max_x - settings.min_x) / (settings.plot_width as f64);
debug_assert!(step > 0.0); debug_assert!(step > 0.0);
// Plot back data // Check if we have any special points that need exclusion zones
let has_special_points = (settings.do_extrema && !self.extrema_data.is_empty())
|| (settings.do_roots && !self.root_data.is_empty());
// Plot back data, filtering out points near special points for better hover detection
if !self.back_data.is_empty() { if !self.back_data.is_empty() {
if self.integral && (step >= integral_step) { if self.integral && (step >= integral_step) {
plot_ui.line( plot_ui.line(
@@ -403,45 +400,109 @@ impl FunctionEntry {
.fill(0.0), .fill(0.0),
); );
} }
plot_ui.line(
// Only filter when there are special points to avoid
let main_line = if has_special_points {
let exclusion_radius = step * 3.0;
let is_near_special = |p: &PlotPoint| {
(settings.do_extrema
&& self
.extrema_data
.iter()
.any(|sp| (p.x - sp.x).abs() < exclusion_radius))
|| (settings.do_roots
&& self
.root_data
.iter()
.any(|sp| (p.x - sp.x).abs() < exclusion_radius))
};
self.back_data self.back_data
.clone() .iter()
.filter(|p| !is_near_special(p))
.cloned()
.collect::<Vec<PlotPoint>>()
.to_line() .to_line()
.stroke(egui::Stroke::new(4.0, main_plot_color)), } else {
); // No filtering needed - use data directly
self.back_data.clone().to_line()
};
plot_ui.line(main_line.stroke(egui::Stroke::new(4.0, main_plot_color)));
} }
// Plot derivative data // Plot derivative data
if self.derivative && !self.derivative_data.is_empty() { if self.derivative && !self.derivative_data.is_empty() {
plot_ui.line(self.derivative_data.clone().to_line().color(Color32::GREEN)); let derivative_line = if has_special_points {
let exclusion_radius = step * 3.0;
let is_near_special = |p: &PlotPoint| {
(settings.do_extrema
&& self
.extrema_data
.iter()
.any(|sp| (p.x - sp.x).abs() < exclusion_radius))
|| (settings.do_roots
&& self
.root_data
.iter()
.any(|sp| (p.x - sp.x).abs() < exclusion_radius))
};
self.derivative_data
.iter()
.filter(|p| !is_near_special(p))
.cloned()
.collect::<Vec<PlotPoint>>()
.to_line()
} else {
self.derivative_data.clone().to_line()
};
plot_ui.line(derivative_line.color(Color32::GREEN));
} }
// Plot extrema points // Plot extrema points
if settings.do_extrema && !self.extrema_data.is_empty() { if settings.do_extrema && !self.extrema_data.is_empty() {
plot_ui.points( for point in &self.extrema_data {
self.extrema_data let name = format!(
.clone() "({}, {})",
.to_points() try_symbolic(point.x)
.color(Color32::YELLOW) .map(|s| s.to_string())
.radius(5.0), // Radius of points of Extrema .unwrap_or_else(|| format!("{:.4}", point.x)),
try_symbolic(point.y)
.map(|s| s.to_string())
.unwrap_or_else(|| format!("{:.4}", point.y))
); );
plot_ui.points(
Points::new(name, vec![[point.x, point.y]])
.color(Color32::YELLOW)
.radius(5.0),
);
}
} }
// Plot roots points // Plot roots points
if settings.do_roots && !self.root_data.is_empty() { if settings.do_roots && !self.root_data.is_empty() {
for point in &self.root_data {
let name = format!(
"({}, {})",
try_symbolic(point.x)
.map(|s| s.to_string())
.unwrap_or_else(|| format!("{:.4}", point.x)),
try_symbolic(point.y)
.map(|s| s.to_string())
.unwrap_or_else(|| format!("{:.4}", point.y))
);
plot_ui.points( plot_ui.points(
self.root_data Points::new(name, vec![[point.x, point.y]])
.clone()
.to_points()
.color(Color32::LIGHT_BLUE) .color(Color32::LIGHT_BLUE)
.radius(5.0), // Radius of points of Roots .radius(5.0),
); );
} }
}
if self.nth_derviative if self.nth_derivative
&& let Some(ref nth_derviative) = self.nth_derivative_data && let Some(ref nth_derivative) = self.nth_derivative_data
{ {
plot_ui.line(nth_derviative.clone().to_line().color(Color32::DARK_RED)); plot_ui.line(nth_derivative.clone().to_line().color(Color32::DARK_RED));
} }
// Plot integral data // Plot integral data

View File

@@ -1,11 +1,8 @@
use crate::{consts::COLORS, function_entry::FunctionEntry, widgets::widgets_ontop}; use crate::{function_entry::FunctionEntry, widgets::widgets_ontop};
use egui::{Button, Id, Key, Modifiers, PopupCloseBehavior, TextEdit, WidgetText}; use egui::{Button, Id, Key, Modifiers, Popup, PopupCloseBehavior, TextEdit, WidgetText};
use emath::vec2; use emath::vec2;
use parsing::Movement; use parsing::Movement;
use serde::ser::SerializeStruct; use serde::{Deserialize, Serialize};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::ops::BitXorAssign; use std::ops::BitXorAssign;
type Functions = Vec<(Id, FunctionEntry)>; type Functions = Vec<(Id, FunctionEntry)>;
@@ -33,8 +30,8 @@ fn func_manager_roundtrip_serdes() {
let ser = bincode::serialize(&func_manager).expect("unable to serialize"); let ser = bincode::serialize(&func_manager).expect("unable to serialize");
let des: FunctionManager = bincode::deserialize(&ser).expect("unable to deserialize"); let des: FunctionManager = bincode::deserialize(&ser).expect("unable to deserialize");
assert_eq!( assert_eq!(
func_manager.functions[0].1.raw_func_str, func_manager.functions[0].1.func_str(),
des.functions[0].1.raw_func_str des.functions[0].1.func_str()
); );
} }
@@ -50,17 +47,9 @@ impl FunctionManager {
} }
} }
#[inline]
fn get_hash(&self) -> u64 {
let mut hasher = DefaultHasher::new();
self.functions.hash(&mut hasher);
hasher.finish()
}
/// Displays function entries alongside returning whether or not functions have been modified /// Displays function entries alongside returning whether or not functions have been modified
pub fn display_entries(&mut self, ui: &mut egui::Ui) -> bool { pub fn display_entries(&mut self, ui: &mut egui::Ui) -> bool {
let initial_hash = self.get_hash(); let mut changed = false;
let can_remove = self.functions.len() > 1; let can_remove = self.functions.len() > 1;
let available_width = ui.available_width(); let available_width = ui.available_width();
@@ -68,7 +57,6 @@ impl FunctionManager {
let target_size = vec2(available_width, crate::consts::FONT_SIZE); let target_size = vec2(available_width, crate::consts::FONT_SIZE);
for (i, (te_id, function)) in self.functions.iter_mut().map(|(a, b)| (*a, b)).enumerate() { for (i, (te_id, function)) in self.functions.iter_mut().map(|(a, b)| (*a, b)).enumerate() {
let mut new_string = function.autocomplete.string.clone(); let mut new_string = function.autocomplete.string.clone();
function.update_string(&new_string);
let mut movement: Movement = Movement::default(); let mut movement: Movement = Movement::default();
@@ -92,11 +80,15 @@ impl FunctionManager {
// Only keep valid chars // Only keep valid chars
new_string.retain(crate::misc::is_valid_char); new_string.retain(crate::misc::is_valid_char);
// Track if function string changed and update the function
if new_string != function.autocomplete.string {
changed = true;
function.update_string(&new_string);
}
// If not fully open, return here as buttons cannot yet be displayed, therefore the user is inable to mark it for deletion // If not fully open, return here as buttons cannot yet be displayed, therefore the user is inable to mark it for deletion
let animate_bool = ui.ctx().animate_bool(te_id, re.has_focus()); let animate_bool = ui.ctx().animate_bool(te_id, re.has_focus());
if animate_bool == 1.0 { if animate_bool == 1.0 {
function.autocomplete.update_string(&new_string);
if function.autocomplete.hint.is_some() { if function.autocomplete.hint.is_some() {
// only register up and down arrow movements if hint is type `Hint::Many` // only register up and down arrow movements if hint is type `Hint::Many`
if !function.autocomplete.hint.is_single() { if !function.autocomplete.hint.is_single() {
@@ -121,9 +113,18 @@ impl FunctionManager {
movement = Movement::Complete; movement = Movement::Complete;
} }
// Remember string before movement to detect changes
let string_before = function.autocomplete.string.clone();
// Register movement and apply proper changes // Register movement and apply proper changes
function.autocomplete.register_movement(&movement); function.autocomplete.register_movement(&movement);
// If the string changed (hint was applied), update the backing function
if function.autocomplete.string != string_before {
function.sync_from_autocomplete();
changed = true;
}
if movement != Movement::Complete if movement != Movement::Complete
&& let Some(hints) = function.autocomplete.hint.many() && let Some(hints) = function.autocomplete.hint.many()
{ {
@@ -131,12 +132,10 @@ impl FunctionManager {
let autocomplete_popup_id = Id::new("autocomplete popup"); let autocomplete_popup_id = Id::new("autocomplete popup");
egui::popup_below_widget( Popup::from_response(&re)
ui, .id(autocomplete_popup_id)
autocomplete_popup_id, .close_behavior(PopupCloseBehavior::CloseOnClickOutside)
&re, .show(|ui| {
PopupCloseBehavior::CloseOnClickOutside,
|ui| {
hints.iter().enumerate().for_each(|(i, candidate)| { hints.iter().enumerate().for_each(|(i, candidate)| {
if ui if ui
.selectable_label(i == function.autocomplete.i, *candidate) .selectable_label(i == function.autocomplete.i, *candidate)
@@ -146,17 +145,19 @@ impl FunctionManager {
function.autocomplete.i = i; function.autocomplete.i = i;
} }
}); });
}, });
);
if clicked { if clicked {
function function
.autocomplete .autocomplete
.apply_hint(hints[function.autocomplete.i]); .apply_hint(hints[function.autocomplete.i]);
// Update the backing function with the new string after hint was applied
function.sync_from_autocomplete();
changed = true;
movement = Movement::Complete; movement = Movement::Complete;
} else { } else {
ui.memory_mut(|x| x.open_popup(autocomplete_popup_id)); Popup::open_id(ui.ctx(), autocomplete_popup_id);
} }
} }
@@ -230,11 +231,10 @@ impl FunctionManager {
// Remove function if the user requests it // Remove function if the user requests it
if let Some(remove_i_unwrap) = remove_i { if let Some(remove_i_unwrap) = remove_i {
self.functions.remove(remove_i_unwrap); self.functions.remove(remove_i_unwrap);
changed = true;
} }
let final_hash = self.get_hash(); changed
initial_hash != final_hash
} }
/// Create and push new empty function entry /// Create and push new empty function entry

View File

@@ -12,22 +12,24 @@ mod widgets;
pub use crate::{ pub use crate::{
function_entry::{FunctionEntry, Riemann}, function_entry::{FunctionEntry, Riemann},
math_app::AppSettings, math_app::{AppSettings, MathApp},
misc::{EguiHelper, newtons_method, option_vec_printer, step_helper}, misc::{EguiHelper, newtons_method, option_vec_printer, step_helper},
unicode_helper::{to_chars_array, to_unicode_hash}, unicode_helper::{to_chars_array, to_unicode_hash},
}; };
cfg_if::cfg_if! { // WASM-specific setup
if #[cfg(target_arch = "wasm32")] { #[cfg(target_arch = "wasm32")]
mod wasm {
use super::math_app;
use eframe::WebRunner;
use lol_alloc::{FreeListAllocator, LockedAllocator};
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use web_sys::HtmlCanvasElement; use web_sys::HtmlCanvasElement;
use lol_alloc::{FreeListAllocator, LockedAllocator};
#[global_allocator] #[global_allocator]
static ALLOCATOR: LockedAllocator<FreeListAllocator> = LockedAllocator::new(FreeListAllocator::new()); static ALLOCATOR: LockedAllocator<FreeListAllocator> =
LockedAllocator::new(FreeListAllocator::new());
use eframe::WebRunner;
// use tracing::metadata::LevelFilter;
#[derive(Clone)] #[derive(Clone)]
#[wasm_bindgen] #[wasm_bindgen]
pub struct WebHandle { pub struct WebHandle {
@@ -40,9 +42,7 @@ cfg_if::cfg_if! {
#[allow(clippy::new_without_default)] #[allow(clippy::new_without_default)]
#[wasm_bindgen(constructor)] #[wasm_bindgen(constructor)]
pub fn new() -> Self { pub fn new() -> Self {
// eframe::WebLogger::init(LevelFilter::Debug).ok();
tracing_wasm::set_as_global_default(); tracing_wasm::set_as_global_default();
Self { Self {
runner: WebRunner::new(), runner: WebRunner::new(),
} }
@@ -50,7 +50,10 @@ cfg_if::cfg_if! {
/// Call this once from JavaScript to start your app. /// Call this once from JavaScript to start your app.
#[wasm_bindgen] #[wasm_bindgen]
pub async fn start(&self, canvas_id: HtmlCanvasElement) -> Result<(), wasm_bindgen::JsValue> { pub async fn start(
&self,
canvas_id: HtmlCanvasElement,
) -> Result<(), wasm_bindgen::JsValue> {
self.runner self.runner
.start( .start(
canvas_id, canvas_id,
@@ -65,11 +68,20 @@ cfg_if::cfg_if! {
pub async fn start() { pub async fn start() {
tracing::info!("Starting..."); tracing::info!("Starting...");
let document = web_sys::window().unwrap().document().unwrap(); let document = web_sys::window()
let canvas = document.get_element_by_id("canvas").unwrap().dyn_into::<HtmlCanvasElement>().unwrap(); .expect("no window")
.document()
.expect("no document");
let canvas = document
.get_element_by_id("canvas")
.expect("no canvas element")
.dyn_into::<HtmlCanvasElement>()
.expect("canvas is not an HtmlCanvasElement");
let web_handle = WebHandle::new(); let web_handle = WebHandle::new();
web_handle.start(canvas).await.unwrap() web_handle
} .start(canvas)
.await
.expect("failed to start web app");
} }
} }

View File

@@ -1,14 +1,3 @@
#[macro_use]
extern crate static_assertions;
mod consts;
mod function_entry;
mod function_manager;
mod math_app;
mod misc;
mod unicode_helper;
mod widgets;
// For running the program natively! (Because why not?) // For running the program natively! (Because why not?)
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
fn main() -> eframe::Result<()> { fn main() -> eframe::Result<()> {
@@ -21,6 +10,6 @@ fn main() -> eframe::Result<()> {
eframe::run_native( eframe::run_native(
"(Yet-to-be-named) Graphing Software", "(Yet-to-be-named) Graphing Software",
eframe::NativeOptions::default(), eframe::NativeOptions::default(),
Box::new(|cc| Ok(Box::new(math_app::MathApp::new(cc)))), Box::new(|cc| Ok(Box::new(ytbn_graphing_software::MathApp::new(cc)))),
) )
} }

View File

@@ -3,6 +3,7 @@ use crate::{
function_entry::Riemann, function_entry::Riemann,
function_manager::FunctionManager, function_manager::FunctionManager,
misc::option_vec_printer, misc::option_vec_printer,
widgets::toggle_button,
}; };
use eframe::App; use eframe::App;
use egui::{ use egui::{
@@ -14,7 +15,7 @@ use egui_plot::Plot;
use emath::{Align, Align2}; use emath::{Align, Align2};
use epaint::Margin; use epaint::Margin;
use itertools::Itertools; use itertools::Itertools;
use std::{io::Read, ops::BitXorAssign}; use std::io::Read;
/// Stores current settings/state of [`MathApp`] /// Stores current settings/state of [`MathApp`]
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
@@ -123,92 +124,48 @@ const DATA_NAME: &str = "YTBN-DECOMPRESSED";
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
const FUNC_NAME: &str = "YTBN-FUNCTIONS"; const FUNC_NAME: &str = "YTBN-FUNCTIONS";
/// Load functions from localStorage (WASM only)
#[cfg(target_arch = "wasm32")]
fn load_functions() -> Option<FunctionManager> {
let data = get_localstorage().get_item(FUNC_NAME).ok()??;
let func_data = crate::misc::hashed_storage_read(&data)?;
tracing::info!("Reading previous function data");
match bincode::deserialize(&func_data) {
Ok(Some(function_manager)) => Some(function_manager),
_ => {
tracing::info!("Unable to load functionManager instance");
None
}
}
}
fn decompress_fonts() -> epaint::text::FontDefinitions {
let mut data = Vec::new();
ruzstd::decoding::StreamingDecoder::new(
const { include_bytes!(concat!(env!("OUT_DIR"), "/compressed_data")).as_slice() },
)
.expect("unable to decode compressed data")
.read_to_end(&mut data)
.expect("unable to read compressed data");
bincode::deserialize(data.as_slice()).expect("unable to deserialize bincode")
}
impl MathApp { impl MathApp {
#[allow(dead_code)] // This is used lol #[allow(dead_code)] // This is used lol
/// Create new instance of [`MathApp`] and return it /// Create new instance of [`MathApp`] and return it
pub fn new(cc: &eframe::CreationContext<'_>) -> Self { pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
tracing::info!("Initializing..."); tracing::info!("Initializing...");
cfg_if::cfg_if! { #[cfg(target_arch = "wasm32")]
if #[cfg(target_arch = "wasm32")] {
tracing::info!("Web Info: {:?}", &cc.integration_info.web_info); tracing::info!("Web Info: {:?}", &cc.integration_info.web_info);
fn get_storage_decompressed() -> Option<Vec<u8>> {
let data = get_localstorage().get_item(DATA_NAME).ok()??;
let cached_data = crate::misc::hashed_storage_read(&data)?;
tracing::info!("Reading decompression cache. Bytes: {}", cached_data.len());
return Some(cached_data);
}
fn load_functions() -> Option<FunctionManager> {
let data = get_localstorage().get_item(FUNC_NAME).ok()??;
let func_data = crate::misc::hashed_storage_read(&data)?;
tracing::info!("Reading previous function data");
if let Ok(Some(function_manager)) = bincode::deserialize(&func_data) {
return Some(function_manager);
} else {
tracing::info!("Unable to load functionManager instance");
return None;
}
}
}
}
fn decompress_fonts() -> epaint::text::FontDefinitions {
let mut data = Vec::new();
let _ =
ruzstd::decoding::StreamingDecoder::new(
&mut const {
include_bytes!(concat!(env!("OUT_DIR"), "/compressed_data")).as_slice()
},
)
.expect("unable to decode compressed data")
.read_to_end(&mut data)
.expect("unable to read compressed data");
#[cfg(target = "wasm32")]
{
tracing::info!("Setting decompression cache");
let saved_data = hashed_storage_create(data);
tracing::info!("Bytes: {}", saved_data.len());
get_localstorage()
.set_item(DATA_NAME, saved_data)
.expect("failed to set local storage cache");
}
bincode::deserialize(data.as_slice()).expect("unable to deserialize bincode")
}
tracing::info!("Reading fonts..."); tracing::info!("Reading fonts...");
// Initialize fonts // Initialize fonts
// This used to be in the `update` method, but (after a ton of digging) this actually caused OOMs. that was a pain to debug // This used to be in the `update` method, but (after a ton of digging) this actually caused OOMs. that was a pain to debug
cc.egui_ctx.set_fonts({ cc.egui_ctx.set_fonts(decompress_fonts());
#[cfg(target = "wasm32")]
if let Some(Ok(data)) =
get_storage_decompressed().map(|data| bincode::deserialize(data.as_slice()))
{
data
} else {
decompress_fonts()
}
#[cfg(not(target = "wasm32"))]
decompress_fonts()
});
// Set dark mode by default
// cc.egui_ctx.set_visuals(crate::style::style());
// Set spacing
// cc.egui_ctx.set_spacing(crate::style::SPACING);
tracing::info!("Initialized!"); tracing::info!("Initialized!");
@@ -319,22 +276,20 @@ impl MathApp {
}); });
ui.horizontal(|ui| { ui.horizontal(|ui| {
self.settings.do_extrema.bitxor_assign( toggle_button(
ui.add(Button::new("Extrema")) ui,
.on_hover_text(match self.settings.do_extrema { &mut self.settings.do_extrema,
true => "Disable Displaying Extrema", "Extrema",
false => "Display Extrema", "Disable Displaying Extrema",
}) "Display Extrema",
.clicked(),
); );
self.settings.do_roots.bitxor_assign( toggle_button(
ui.add(Button::new("Roots")) ui,
.on_hover_text(match self.settings.do_roots { &mut self.settings.do_roots,
true => "Disable Displaying Roots", "Roots",
false => "Display Roots", "Disable Displaying Roots",
}) "Display Roots",
.clicked(),
); );
}); });
@@ -376,22 +331,21 @@ impl App for MathApp {
// If keyboard input isn't being grabbed, check for key combos // If keyboard input isn't being grabbed, check for key combos
if !ctx.wants_keyboard_input() { if !ctx.wants_keyboard_input() {
// If `H` key is pressed, toggle Side Panel // If `H` key is pressed, toggle Side Panel
self.opened if ctx.input_mut(|x| x.consume_key(egui::Modifiers::NONE, Key::H)) {
.side_panel self.opened.side_panel = !self.opened.side_panel;
.bitxor_assign(ctx.input_mut(|x| x.consume_key(egui::Modifiers::NONE, Key::H))); }
} }
// Creates Top bar that contains some general options // Creates Top bar that contains some general options
TopBottomPanel::top("top_bar").show(ctx, |ui| { TopBottomPanel::top("top_bar").show(ctx, |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
// Button in top bar to toggle showing the side panel // Button in top bar to toggle showing the side panel
self.opened.side_panel.bitxor_assign( toggle_button(
ui.add(Button::new("Panel")) ui,
.on_hover_text(match self.opened.side_panel { &mut self.opened.side_panel,
true => "Hide Side Panel", "Panel",
false => "Show Side Panel", "Hide Side Panel",
}) "Show Side Panel",
.clicked(),
); );
// Button to add a new function // Button to add a new function
@@ -407,13 +361,12 @@ impl App for MathApp {
} }
// Toggles opening the Help window // Toggles opening the Help window
self.opened.help.bitxor_assign( toggle_button(
ui.add(Button::new("Help")) ui,
.on_hover_text(match self.opened.help { &mut self.opened.help,
true => "Close Help Window", "Help",
false => "Open Help Window", "Close Help Window",
}) "Open Help Window",
.clicked(),
); );
// Display Area and time of last frame // Display Area and time of last frame
@@ -492,13 +445,8 @@ impl App for MathApp {
.iter() .iter()
.map(|(_, func)| func.get_test_result()) .map(|(_, func)| func.get_test_result())
.enumerate() .enumerate()
.filter(|(_, error)| error.is_some()) .filter_map(|(i, error)| error.as_ref().map(|x| (i, x)))
.map(|(i, error)| { .map(|(i, error)| format!("(Function #{}) {}\n", i, error))
// use unwrap_unchecked as None Errors are already filtered out
unsafe {
format!("(Function #{}) {}\n", i, error.as_ref().unwrap_unchecked())
}
})
.join(""); .join("");
if !errors_formatted.is_empty() { if !errors_formatted.is_empty() {

View File

@@ -91,9 +91,7 @@ pub fn newtons_method_helper(
.filter(|(prev, curr)| prev.y.is_finite() && curr.y.is_finite()) .filter(|(prev, curr)| prev.y.is_finite() && curr.y.is_finite())
.filter(|(prev, curr)| prev.y.signum() != curr.y.signum()) .filter(|(prev, curr)| prev.y.signum() != curr.y.signum())
.map(|(start, _)| start.x) .map(|(start, _)| start.x)
.map(|x| newtons_method(f, f_1, x, range, threshold)) .filter_map(|x| newtons_method(f, f_1, x, range, threshold))
.filter(|x| x.is_some())
.map(|x| unsafe { x.unwrap_unchecked() })
.collect() .collect()
} }

View File

@@ -1,5 +1,6 @@
use crate::misc::Offset; use crate::misc::Offset;
use egui::{Id, InnerResponse}; use egui::{Button, Id, InnerResponse, Ui};
use std::ops::BitXorAssign;
/// Creates an area ontop of a widget with an y offset /// Creates an area ontop of a widget with an y offset
pub fn widgets_ontop<R>( pub fn widgets_ontop<R>(
@@ -15,3 +16,19 @@ pub fn widgets_ontop<R>(
area.show(ui.ctx(), |ui| add_contents(ui)) area.show(ui.ctx(), |ui| add_contents(ui))
} }
/// A toggle button that XORs its state when clicked.
/// Shows different hover text based on current state.
pub fn toggle_button(
ui: &mut Ui,
state: &mut bool,
label: &str,
enabled_tip: &str,
disabled_tip: &str,
) {
state.bitxor_assign(
ui.add(Button::new(label))
.on_hover_text(if *state { enabled_tip } else { disabled_tip })
.clicked(),
);
}

View File

@@ -230,7 +230,7 @@ fn do_test(sum: Riemann, area_target: f64) {
{ {
function.update_string("sin(x)"); function.update_string("sin(x)");
assert!(function.get_test_result().is_none()); assert!(function.get_test_result().is_none());
assert_eq!(&function.raw_func_str, "sin(x)"); assert_eq!(function.func_str(), "sin(x)");
function.integral = false; function.integral = false;
function.derivative = false; function.derivative = false;