Compare commits

...

13 Commits

10 changed files with 280 additions and 282 deletions

View File

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

View File

@ -4,21 +4,14 @@ use std::collections::HashMap;
#[derive(Clone, PartialEq)]
pub struct FlatExWrapper {
func: Option<FlatEx<f64>>,
func_str: Option<String>,
}
impl FlatExWrapper {
const EMPTY: FlatExWrapper = FlatExWrapper {
func: None,
func_str: None,
};
const EMPTY: FlatExWrapper = FlatExWrapper { func: None };
#[inline]
const fn new(f: FlatEx<f64>) -> Self {
Self {
func: Some(f),
func_str: None,
}
Self { func: Some(f) }
}
#[inline]
@ -34,26 +27,6 @@ impl FlatExWrapper {
.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]
fn partial_iter(&self, n: usize) -> Self {
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
#[inline]
pub const fn is_variable(c: &char) -> bool {

View File

@ -62,46 +62,73 @@ impl BoolSlice {
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) {
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;
} 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;
} 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_var = self.variable;
}
}
const fn splitable(&self, c: &char, other: &BoolSlice, split: &SplitType) -> bool {
if (*c == '*') | (matches!(split, &SplitType::Term) && other.open_parens) {
true
} else if other.closing_parens {
// 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)
{
/// Determines if we should split (insert implicit multiplication) before current char
const fn splitable(&self, c: &char, prev: &BoolSlice, split: &SplitType) -> bool {
// Always split on explicit multiplication
if *c == '*' {
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
}
}

View File

@ -1,7 +1,8 @@
use crate::math_app::AppSettings;
use crate::misc::{EguiHelper, newtons_method_helper, step_helper};
use crate::symbolic::try_symbolic;
use egui::{Checkbox, Context};
use egui_plot::{Bar, BarChart, PlotPoint, PlotUi};
use egui_plot::{Bar, BarChart, PlotPoint, PlotUi, Points};
use epaint::Color32;
use parsing::{AutoComplete, generate_hint};
@ -43,7 +44,7 @@ pub struct FunctionEntry {
/// If displaying derivatives are enabled (note, they are still calculated for other purposes)
pub derivative: bool,
pub nth_derviative: bool,
pub nth_derivative: bool,
pub back_data: Vec<PlotPoint>,
pub integral_data: Option<(Vec<Bar>, f64)>,
@ -64,7 +65,7 @@ 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.nth_derivative.hash(state);
self.curr_nth.hash(state);
self.settings_opened.hash(state);
}
@ -128,7 +129,7 @@ impl Default for FunctionEntry {
raw_func_str: String::new(),
integral: false,
derivative: false,
nth_derviative: false,
nth_derivative: false,
back_data: Vec::new(),
integral_data: None,
derivative_data: Vec::new(),
@ -157,7 +158,7 @@ impl FunctionEntry {
.collapsible(false)
.show(ctx, |ui| {
ui.add(Checkbox::new(
&mut self.nth_derviative,
&mut self.nth_derivative,
"Display Nth Derivative",
));
@ -252,30 +253,36 @@ impl FunctionEntry {
) -> Vec<PlotPoint> {
self.function.generate_derivative(derivative_level);
self.function.generate_derivative(derivative_level + 1);
let newtons_method_output: Vec<f64> = match derivative_level {
0 => newtons_method_helper(
threshold,
range,
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),
),
let data_source = match derivative_level {
0 => self.back_data.as_slice(),
1 => self.derivative_data.as_slice(),
_ => 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()
.map(|x| PlotPoint::new(x, self.function.get(0, x)))
.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`
pub fn calculate(
&mut self,
@ -304,32 +311,17 @@ impl FunctionEntry {
}
if self.back_data.is_empty() {
let data: Vec<PlotPoint> = resolution_iter
.clone()
.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;
self.back_data = self.generate_plot_data(0, &resolution_iter);
debug_assert_eq!(self.back_data.len(), settings.plot_width + 1);
}
if self.derivative_data.is_empty() {
self.function.generate_derivative(1);
let data: Vec<PlotPoint> = resolution_iter
.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;
self.derivative_data = self.generate_plot_data(1, &resolution_iter);
debug_assert_eq!(self.derivative_data.len(), settings.plot_width + 1);
}
if self.nth_derviative && self.nth_derivative_data.is_none() {
let data: Vec<PlotPoint> = resolution_iter
.into_iter()
.map(|x| PlotPoint::new(x, self.function.get(self.curr_nth, x)))
.collect();
if self.nth_derivative && self.nth_derivative_data.is_none() {
let data = self.generate_plot_data(self.curr_nth, &resolution_iter);
debug_assert_eq!(data.len(), settings.plot_width + 1);
self.nth_derivative_data = Some(data);
}
@ -352,7 +344,7 @@ impl FunctionEntry {
self.clear_integral();
}
let threshold: f64 = resolution / 2.0;
let threshold: f64 = f64::EPSILON;
let x_range = settings.min_x..settings.max_x;
// Calculates extrema
@ -385,7 +377,28 @@ impl FunctionEntry {
let step = (settings.max_x - settings.min_x) / (settings.plot_width as f64);
debug_assert!(step > 0.0);
// Plot back data
// Collect special points (extrema and roots) for exclusion from line hover detection
let special_points: Vec<&PlotPoint> = {
let mut points = Vec::new();
if settings.do_extrema {
points.extend(self.extrema_data.iter());
}
if settings.do_roots {
points.extend(self.root_data.iter());
}
points
};
// Helper to check if a point is near any special point
// Uses a radius in x-axis units based on the step size
let exclusion_radius = step * 3.0; // Exclude points within 3 steps of special points
let is_near_special_point = |p: &PlotPoint| -> bool {
special_points
.iter()
.any(|sp| (p.x - sp.x).abs() < exclusion_radius)
};
// Plot back data, filtering out points near special points for better hover detection
if !self.back_data.is_empty() {
if self.integral && (step >= integral_step) {
plot_ui.line(
@ -403,45 +416,78 @@ impl FunctionEntry {
.fill(0.0),
);
}
// Filter out points near special points so cursor snaps to them more easily
let filtered_back_data: Vec<PlotPoint> = self
.back_data
.iter()
.filter(|p| !is_near_special_point(p))
.cloned()
.collect();
plot_ui.line(
self.back_data
.clone()
filtered_back_data
.to_line()
.stroke(egui::Stroke::new(4.0, main_plot_color)),
);
}
// Plot derivative data
// Plot derivative data, also filtering near special points
if self.derivative && !self.derivative_data.is_empty() {
plot_ui.line(self.derivative_data.clone().to_line().color(Color32::GREEN));
let filtered_derivative_data: Vec<PlotPoint> = self
.derivative_data
.iter()
.filter(|p| !is_near_special_point(p))
.cloned()
.collect();
plot_ui.line(filtered_derivative_data.to_line().color(Color32::GREEN));
}
// Plot extrema points
if settings.do_extrema && !self.extrema_data.is_empty() {
plot_ui.points(
self.extrema_data
.clone()
.to_points()
.color(Color32::YELLOW)
.radius(5.0), // Radius of points of Extrema
for point in &self.extrema_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(
Points::new(name, vec![[point.x, point.y]])
.color(Color32::YELLOW)
.radius(5.0),
);
}
}
// Plot roots points
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(
self.root_data
.clone()
.to_points()
Points::new(name, vec![[point.x, point.y]])
.color(Color32::LIGHT_BLUE)
.radius(5.0), // Radius of points of Roots
.radius(5.0),
);
}
}
if self.nth_derviative
&& let Some(ref nth_derviative) = self.nth_derivative_data
if self.nth_derivative
&& 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

View File

@ -12,7 +12,7 @@ mod widgets;
pub use crate::{
function_entry::{FunctionEntry, Riemann},
math_app::AppSettings,
math_app::{AppSettings, MathApp},
misc::{EguiHelper, newtons_method, option_vec_printer, step_helper},
unicode_helper::{to_chars_array, to_unicode_hash},
};

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?)
#[cfg(not(target_arch = "wasm32"))]
fn main() -> eframe::Result<()> {
@ -21,6 +10,6 @@ fn main() -> eframe::Result<()> {
eframe::run_native(
"(Yet-to-be-named) Graphing Software",
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_manager::FunctionManager,
misc::option_vec_printer,
widgets::toggle_button,
};
use eframe::App;
use egui::{
@ -14,7 +15,7 @@ use egui_plot::Plot;
use emath::{Align, Align2};
use epaint::Margin;
use itertools::Itertools;
use std::{io::Read, ops::BitXorAssign};
use std::io::Read;
/// Stores current settings/state of [`MathApp`]
#[derive(Copy, Clone)]
@ -134,15 +135,6 @@ impl MathApp {
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)?;
@ -172,17 +164,6 @@ impl MathApp {
.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")
}
@ -190,19 +171,7 @@ impl MathApp {
// 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
cc.egui_ctx.set_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()
});
cc.egui_ctx.set_fonts({ decompress_fonts() });
// Set dark mode by default
// cc.egui_ctx.set_visuals(crate::style::style());
@ -319,22 +288,20 @@ impl MathApp {
});
ui.horizontal(|ui| {
self.settings.do_extrema.bitxor_assign(
ui.add(Button::new("Extrema"))
.on_hover_text(match self.settings.do_extrema {
true => "Disable Displaying Extrema",
false => "Display Extrema",
})
.clicked(),
toggle_button(
ui,
&mut self.settings.do_extrema,
"Extrema",
"Disable Displaying Extrema",
"Display Extrema",
);
self.settings.do_roots.bitxor_assign(
ui.add(Button::new("Roots"))
.on_hover_text(match self.settings.do_roots {
true => "Disable Displaying Roots",
false => "Display Roots",
})
.clicked(),
toggle_button(
ui,
&mut self.settings.do_roots,
"Roots",
"Disable Displaying Roots",
"Display Roots",
);
});
@ -376,22 +343,21 @@ impl App for MathApp {
// If keyboard input isn't being grabbed, check for key combos
if !ctx.wants_keyboard_input() {
// If `H` key is pressed, toggle Side Panel
self.opened
.side_panel
.bitxor_assign(ctx.input_mut(|x| x.consume_key(egui::Modifiers::NONE, Key::H)));
if ctx.input_mut(|x| x.consume_key(egui::Modifiers::NONE, Key::H)) {
self.opened.side_panel = !self.opened.side_panel;
}
}
// Creates Top bar that contains some general options
TopBottomPanel::top("top_bar").show(ctx, |ui| {
ui.horizontal(|ui| {
// Button in top bar to toggle showing the side panel
self.opened.side_panel.bitxor_assign(
ui.add(Button::new("Panel"))
.on_hover_text(match self.opened.side_panel {
true => "Hide Side Panel",
false => "Show Side Panel",
})
.clicked(),
toggle_button(
ui,
&mut self.opened.side_panel,
"Panel",
"Hide Side Panel",
"Show Side Panel",
);
// Button to add a new function
@ -407,13 +373,12 @@ impl App for MathApp {
}
// Toggles opening the Help window
self.opened.help.bitxor_assign(
ui.add(Button::new("Help"))
.on_hover_text(match self.opened.help {
true => "Close Help Window",
false => "Open Help Window",
})
.clicked(),
toggle_button(
ui,
&mut self.opened.help,
"Help",
"Close Help Window",
"Open Help Window",
);
// Display Area and time of last frame
@ -492,13 +457,8 @@ impl App for MathApp {
.iter()
.map(|(_, func)| func.get_test_result())
.enumerate()
.filter(|(_, error)| error.is_some())
.map(|(i, error)| {
// use unwrap_unchecked as None Errors are already filtered out
unsafe {
format!("(Function #{}) {}\n", i, error.as_ref().unwrap_unchecked())
}
})
.filter_map(|(i, error)| error.as_ref().map(|x| (i, x)))
.map(|(i, error)| format!("(Function #{}) {}\n", i, error))
.join("");
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.signum() != curr.y.signum())
.map(|(start, _)| start.x)
.map(|x| newtons_method(f, f_1, x, range, threshold))
.filter(|x| x.is_some())
.map(|x| unsafe { x.unwrap_unchecked() })
.filter_map(|x| newtons_method(f, f_1, x, range, threshold))
.collect()
}

View File

@ -1,5 +1,6 @@
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
pub fn widgets_ontop<R>(
@ -15,3 +16,19 @@ pub fn widgets_ontop<R>(
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(),
);
}