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",
"benchmarks",
"bincode",
"cfg-if",
"eframe",
"egui",
"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 }
egui_plot = { version = "0.34.0", default-features = false }
cfg-if = "1"
ruzstd = "0.8"
tracing = "0.1"
itertools = "0.14"

View File

@@ -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<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 {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<PlotPoint> = 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::<Vec<PlotPoint>>()
.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<PlotPoint> = 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::<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

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 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

View File

@@ -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<FreeListAllocator> = LockedAllocator::new(FreeListAllocator::new());
#[global_allocator]
static ALLOCATOR: LockedAllocator<FreeListAllocator> =
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::<HtmlCanvasElement>().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::<HtmlCanvasElement>()
.expect("canvas is not an HtmlCanvasElement");
let web_handle = WebHandle::new();
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")]
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 {
#[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<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");
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!");

View File

@@ -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;