From c70c7151268f00aafb11406406c786ad4904383d Mon Sep 17 00:00:00 2001 From: Simon Gardling Date: Sat, 13 Dec 2025 03:38:04 -0500 Subject: [PATCH] cleanup --- Cargo.lock | 1 - Cargo.toml | 1 - src/function_entry.rs | 159 ++++++++++++++++++++++------------------ src/function_manager.rs | 47 ++++++------ src/lib.rs | 108 +++++++++++++++------------ src/math_app.rs | 74 ++++++++----------- tests/function.rs | 2 +- 7 files changed, 204 insertions(+), 188 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e43020d..ac1e63d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4145,7 +4145,6 @@ dependencies = [ "base64", "benchmarks", "bincode", - "cfg-if", "eframe", "egui", "egui_plot", diff --git a/Cargo.toml b/Cargo.toml index f67c3af..e2af8a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 } egui_plot = { version = "0.34.0", default-features = false } -cfg-if = "1" ruzstd = "0.8" tracing = "0.1" itertools = "0.14" diff --git a/src/function_entry.rs b/src/function_entry.rs index 170a3ee..a8192fe 100644 --- a/src/function_entry.rs +++ b/src/function_entry.rs @@ -8,10 +8,7 @@ use epaint::Color32; use parsing::{AutoComplete, generate_hint}; use parsing::{BackingFunction, process_func_str}; use serde::{Deserialize, Deserializer, Serialize, Serializer, ser::SerializeStruct}; -use std::{ - fmt::{self, Debug}, - hash::{Hash, Hasher}, -}; +use std::fmt::{self, Debug}; /// Represents the possible variations of Riemann Sums #[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)` 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 pub integral: bool, @@ -61,23 +55,13 @@ pub struct FunctionEntry { pub settings_opened: bool, } -impl Hash for FunctionEntry { - fn hash(&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 { fn serialize(&self, serializer: S) -> Result where S: Serializer, { 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("derivative", &self.derivative)?; s.serialize_field("curr_nth", &self.curr_nth)?; @@ -126,7 +110,6 @@ impl Default for FunctionEntry { fn default() -> FunctionEntry { FunctionEntry { function: BackingFunction::default(), - raw_func_str: String::new(), integral: false, derivative: false, nth_derivative: false, @@ -151,7 +134,7 @@ impl FunctionEntry { pub fn settings_window(&mut self, ctx: &Context) { 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) .default_pos([200.0, 200.0]) .resizable(false) @@ -181,13 +164,32 @@ impl FunctionEntry { &self.test_result } + /// Get the raw function string + #[inline] + pub fn func_str(&self) -> &str { + &self.autocomplete.string + } + /// Update function string and test it 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; } - 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 new_func_result = BackingFunction::new(&processed_func); @@ -213,17 +215,16 @@ impl FunctionEntry { ) -> (Vec<(f64, f64)>, f64) { let step = (integral_max_x - integral_min_x) / (integral_num as f64); - // let sum_func = self.get_sum_func(sum); - - let data2: Vec<(f64, f64)> = step_helper(integral_num, integral_min_x, step) + let data: Vec<(f64, f64)> = step_helper(integral_num, integral_min_x, step) .into_iter() .map(|x| { - let step_offset = step.copysign(x); // store the offset here so it doesn't have to be calculated multiple times - let x2: f64 = x + step_offset; + let step_offset = step.copysign(x); + let x2 = x + step_offset; - let (left_x, right_x) = match x.is_sign_positive() { - true => (x, x2), - false => (x2, x), + let (left_x, right_x) = if x.is_sign_positive() { + (x, x2) + } else { + (x2, x) }; let y = match sum { @@ -239,9 +240,9 @@ impl FunctionEntry { .filter(|(_, y)| y.is_finite()) .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 @@ -377,26 +378,9 @@ impl FunctionEntry { let step = (settings.max_x - settings.min_x) / (settings.plot_width as f64); debug_assert!(step > 0.0); - // 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) - }; + // 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() { @@ -417,31 +401,62 @@ impl FunctionEntry { ); } - // Filter out points near special points so cursor snaps to them more easily - let filtered_back_data: Vec = self - .back_data - .iter() - .filter(|p| !is_near_special_point(p)) - .cloned() - .collect(); - - plot_ui.line( - filtered_back_data + // 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 + .iter() + .filter(|p| !is_near_special(p)) + .cloned() + .collect::>() .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() { - let filtered_derivative_data: Vec = self - .derivative_data - .iter() - .filter(|p| !is_near_special_point(p)) - .cloned() - .collect(); + 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::>() + .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 diff --git a/src/function_manager.rs b/src/function_manager.rs index cc2ee50..ebe7a12 100644 --- a/src/function_manager.rs +++ b/src/function_manager.rs @@ -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 emath::vec2; use parsing::Movement; -use serde::ser::SerializeStruct; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use std::collections::hash_map::DefaultHasher; -use std::hash::{Hash, Hasher}; +use serde::{Deserialize, Serialize}; use std::ops::BitXorAssign; 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 des: FunctionManager = bincode::deserialize(&ser).expect("unable to deserialize"); assert_eq!( - func_manager.functions[0].1.raw_func_str, - des.functions[0].1.raw_func_str + func_manager.functions[0].1.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 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 available_width = ui.available_width(); @@ -68,7 +57,6 @@ impl FunctionManager { 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() { let mut new_string = function.autocomplete.string.clone(); - function.update_string(&new_string); let mut movement: Movement = Movement::default(); @@ -92,11 +80,15 @@ impl FunctionManager { // Only keep valid chars 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 let animate_bool = ui.ctx().animate_bool(te_id, re.has_focus()); if animate_bool == 1.0 { - function.autocomplete.update_string(&new_string); - if function.autocomplete.hint.is_some() { // only register up and down arrow movements if hint is type `Hint::Many` if !function.autocomplete.hint.is_single() { @@ -121,9 +113,18 @@ impl FunctionManager { movement = Movement::Complete; } + // Remember string before movement to detect changes + let string_before = function.autocomplete.string.clone(); + // Register movement and apply proper changes 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 && let Some(hints) = function.autocomplete.hint.many() { @@ -153,6 +154,9 @@ impl FunctionManager { function .autocomplete .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; } else { @@ -230,11 +234,10 @@ impl FunctionManager { // Remove function if the user requests it if let Some(remove_i_unwrap) = remove_i { self.functions.remove(remove_i_unwrap); + changed = true; } - let final_hash = self.get_hash(); - - initial_hash != final_hash + changed } /// Create and push new empty function entry diff --git a/src/lib.rs b/src/lib.rs index 7b5a363..df4ce1c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,59 +17,71 @@ pub use crate::{ unicode_helper::{to_chars_array, to_unicode_hash}, }; -cfg_if::cfg_if! { - if #[cfg(target_arch = "wasm32")] { - use wasm_bindgen::prelude::*; - use web_sys::HtmlCanvasElement; +// WASM-specific setup +#[cfg(target_arch = "wasm32")] +mod wasm { + use super::math_app; + use eframe::WebRunner; + use lol_alloc::{FreeListAllocator, LockedAllocator}; + use wasm_bindgen::prelude::*; + use web_sys::HtmlCanvasElement; - use lol_alloc::{FreeListAllocator, LockedAllocator}; - #[global_allocator] - static ALLOCATOR: LockedAllocator = LockedAllocator::new(FreeListAllocator::new()); + #[global_allocator] + static ALLOCATOR: LockedAllocator = + LockedAllocator::new(FreeListAllocator::new()); - use eframe::WebRunner; - // use tracing::metadata::LevelFilter; - #[derive(Clone)] - #[wasm_bindgen] - pub struct WebHandle { - runner: WebRunner, - } + #[derive(Clone)] + #[wasm_bindgen] + pub struct WebHandle { + runner: WebRunner, + } - #[wasm_bindgen] - impl WebHandle { - /// Installs a panic hook, then returns. - #[allow(clippy::new_without_default)] - #[wasm_bindgen(constructor)] - pub fn new() -> Self { - // eframe::WebLogger::init(LevelFilter::Debug).ok(); - tracing_wasm::set_as_global_default(); - - Self { - runner: WebRunner::new(), - } - } - - /// Call this once from JavaScript to start your app. - #[wasm_bindgen] - pub async fn start(&self, canvas_id: HtmlCanvasElement) -> Result<(), wasm_bindgen::JsValue> { - self.runner - .start( - canvas_id, - eframe::WebOptions::default(), - Box::new(|cc| Ok(Box::new(math_app::MathApp::new(cc)))), - ) - .await - } + #[wasm_bindgen] + impl WebHandle { + /// Installs a panic hook, then returns. + #[allow(clippy::new_without_default)] + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + tracing_wasm::set_as_global_default(); + Self { + runner: WebRunner::new(), } + } - #[wasm_bindgen(start)] - pub async fn start() { - tracing::info!("Starting..."); - - let document = web_sys::window().unwrap().document().unwrap(); - let canvas = document.get_element_by_id("canvas").unwrap().dyn_into::().unwrap(); - - let web_handle = WebHandle::new(); - web_handle.start(canvas).await.unwrap() + /// Call this once from JavaScript to start your app. + #[wasm_bindgen] + pub async fn start( + &self, + canvas_id: HtmlCanvasElement, + ) -> Result<(), wasm_bindgen::JsValue> { + self.runner + .start( + canvas_id, + eframe::WebOptions::default(), + Box::new(|cc| Ok(Box::new(math_app::MathApp::new(cc)))), + ) + .await } } + + #[wasm_bindgen(start)] + pub async fn start() { + tracing::info!("Starting..."); + + let document = web_sys::window() + .expect("no window") + .document() + .expect("no document"); + let canvas = document + .get_element_by_id("canvas") + .expect("no canvas element") + .dyn_into::() + .expect("canvas is not an HtmlCanvasElement"); + + let web_handle = WebHandle::new(); + web_handle + .start(canvas) + .await + .expect("failed to start web app"); + } } diff --git a/src/math_app.rs b/src/math_app.rs index c663f0f..b0bbb30 100644 --- a/src/math_app.rs +++ b/src/math_app.rs @@ -124,60 +124,48 @@ const DATA_NAME: &str = "YTBN-DECOMPRESSED"; #[cfg(target_arch = "wasm32")] const FUNC_NAME: &str = "YTBN-FUNCTIONS"; +/// Load functions from localStorage (WASM only) +#[cfg(target_arch = "wasm32")] +fn load_functions() -> Option { + 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 { #[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_if::cfg_if! { - if #[cfg(target_arch = "wasm32")] { - - tracing::info!("Web Info: {:?}", &cc.integration_info.web_info); - - fn load_functions() -> Option { - 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"); - - bincode::deserialize(data.as_slice()).expect("unable to deserialize bincode") - } + #[cfg(target_arch = "wasm32")] + tracing::info!("Web Info: {:?}", &cc.integration_info.web_info); tracing::info!("Reading 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 - 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); + cc.egui_ctx.set_fonts(decompress_fonts()); tracing::info!("Initialized!"); diff --git a/tests/function.rs b/tests/function.rs index 63016eb..da60e34 100644 --- a/tests/function.rs +++ b/tests/function.rs @@ -230,7 +230,7 @@ fn do_test(sum: Riemann, area_target: f64) { { function.update_string("sin(x)"); 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.derivative = false;