This commit is contained in:
2025-12-13 03:38:04 -05:00
parent f2d0d27345
commit c70c715126
7 changed files with 204 additions and 188 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

@@ -8,10 +8,7 @@ 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)]
@@ -35,9 +32,6 @@ 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,
@@ -61,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_derivative.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)?;
@@ -126,7 +110,6 @@ 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_derivative: false, nth_derivative: false,
@@ -151,7 +134,7 @@ 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)
@@ -181,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);
@@ -213,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 {
@@ -239,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
@@ -377,26 +378,9 @@ 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);
// Collect special points (extrema and roots) for exclusion from line hover detection // Check if we have any special points that need exclusion zones
let special_points: Vec<&PlotPoint> = { let has_special_points = (settings.do_extrema && !self.extrema_data.is_empty())
let mut points = Vec::new(); || (settings.do_roots && !self.root_data.is_empty());
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 // 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() {
@@ -417,31 +401,62 @@ impl FunctionEntry {
); );
} }
// Filter out points near special points so cursor snaps to them more easily // Only filter when there are special points to avoid
let filtered_back_data: Vec<PlotPoint> = self let main_line = if has_special_points {
.back_data let exclusion_radius = step * 3.0;
let is_near_special = |p: &PlotPoint| {
(settings.do_extrema
&& self
.extrema_data
.iter() .iter()
.filter(|p| !is_near_special_point(p)) .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
.iter()
.filter(|p| !is_near_special(p))
.cloned() .cloned()
.collect(); .collect::<Vec<PlotPoint>>()
plot_ui.line(
filtered_back_data
.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, also filtering near special points // Plot derivative data
if self.derivative && !self.derivative_data.is_empty() { if self.derivative && !self.derivative_data.is_empty() {
let filtered_derivative_data: Vec<PlotPoint> = self let derivative_line = if has_special_points {
.derivative_data let exclusion_radius = step * 3.0;
let is_near_special = |p: &PlotPoint| {
(settings.do_extrema
&& self
.extrema_data
.iter() .iter()
.filter(|p| !is_near_special_point(p)) .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() .cloned()
.collect(); .collect::<Vec<PlotPoint>>()
.to_line()
} else {
self.derivative_data.clone().to_line()
};
plot_ui.line(filtered_derivative_data.to_line().color(Color32::GREEN)); plot_ui.line(derivative_line.color(Color32::GREEN));
} }
// Plot extrema points // Plot extrema points

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, 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()
{ {
@@ -153,6 +154,9 @@ impl FunctionManager {
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 {
@@ -230,11 +234,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

@@ -17,17 +17,19 @@ pub use crate::{
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

@@ -124,60 +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";
impl MathApp { /// Load functions from localStorage (WASM only)
#[allow(dead_code)] // This is used lol #[cfg(target_arch = "wasm32")]
/// Create new instance of [`MathApp`] and return it fn load_functions() -> Option<FunctionManager> {
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
tracing::info!("Initializing...");
cfg_if::cfg_if! {
if #[cfg(target_arch = "wasm32")] {
tracing::info!("Web Info: {:?}", &cc.integration_info.web_info);
fn load_functions() -> Option<FunctionManager> {
let data = get_localstorage().get_item(FUNC_NAME).ok()??; let data = get_localstorage().get_item(FUNC_NAME).ok()??;
let func_data = crate::misc::hashed_storage_read(&data)?; let func_data = crate::misc::hashed_storage_read(&data)?;
tracing::info!("Reading previous function data"); tracing::info!("Reading previous function data");
if let Ok(Some(function_manager)) = bincode::deserialize(&func_data) { match bincode::deserialize(&func_data) {
return Some(function_manager); Ok(Some(function_manager)) => Some(function_manager),
} else { _ => {
tracing::info!("Unable to load functionManager instance"); tracing::info!("Unable to load functionManager instance");
return None; None
} }
} }
}
} fn decompress_fonts() -> epaint::text::FontDefinitions {
}
fn decompress_fonts() -> epaint::text::FontDefinitions {
let mut data = Vec::new(); let mut data = Vec::new();
let _ =
ruzstd::decoding::StreamingDecoder::new( ruzstd::decoding::StreamingDecoder::new(
&mut const { const { include_bytes!(concat!(env!("OUT_DIR"), "/compressed_data")).as_slice() },
include_bytes!(concat!(env!("OUT_DIR"), "/compressed_data")).as_slice()
},
) )
.expect("unable to decode compressed data") .expect("unable to decode compressed data")
.read_to_end(&mut data) .read_to_end(&mut data)
.expect("unable to read compressed data"); .expect("unable to read compressed data");
bincode::deserialize(data.as_slice()).expect("unable to deserialize bincode") bincode::deserialize(data.as_slice()).expect("unable to deserialize bincode")
} }
impl MathApp {
#[allow(dead_code)] // This is used lol
/// Create new instance of [`MathApp`] and return it
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
tracing::info!("Initializing...");
#[cfg(target_arch = "wasm32")]
tracing::info!("Web Info: {:?}", &cc.integration_info.web_info);
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({ decompress_fonts() }); cc.egui_ctx.set_fonts(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!");

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;