Compare commits

...

53 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
dab002bd15 remove old messages 2025-12-05 14:11:09 -05:00
f9480fac7c don't rebuild on new commit 2025-12-05 14:06:58 -05:00
44fea82aaa font subsetting: pyftsubset -> allsorts 2025-12-05 14:06:47 -05:00
e9b8433117 removed hashed_storage test 2025-12-05 13:52:40 -05:00
957c286e59 cleanup + remove Instant + remove Info window 2025-12-05 13:50:55 -05:00
63bd73e444 cleanup 2025-12-05 13:13:03 -05:00
c9eff77dff parsing: cargo upgrade 2025-12-05 12:41:32 -05:00
9677e8f8b4 symbolic: init 2025-12-05 12:17:18 -05:00
8a5d9f1cd5 delete shell.nix 2025-12-05 12:02:22 -05:00
53d90b7328 FunctionManager: create new() 2025-12-05 11:56:46 -05:00
b59f214c67 enable wayland by default 2025-12-05 11:56:34 -05:00
ba2e782af5 fix flake test build 2025-12-05 01:07:43 -05:00
8ee03d953f FunctionManager: fix recursion issue 2025-12-05 00:52:02 -05:00
f218ff26c7 rename wayland feature 2025-12-05 00:50:01 -05:00
24f99f4cb8 FunctionManager: cleanup Default impl 2025-12-05 00:43:36 -05:00
905820384a FunctionManager: improve serdes pattern 2025-12-05 00:43:20 -05:00
d6cb0fba1a upgrade cargo dependencies 2025-12-04 18:47:42 -05:00
134f11c628 egui_plot: don't use git version 2025-12-04 18:33:19 -05:00
66f0bd5b02 cleanup flake.nix 2025-12-04 02:52:18 -05:00
abfe5480e5 function tests: add extrema and root tests 2025-12-03 20:28:22 -05:00
31181513d0 .gitignore add result (for nix stuff) 2025-12-03 20:07:35 -05:00
7252d37763 fix newton's method test 2025-12-03 20:07:11 -05:00
fe01277f7b add package tests 2025-12-03 20:04:25 -05:00
e96fcdbe99 fix function tests 2025-12-03 20:03:59 -05:00
9d96977785 parser: cargo fmt 2025-12-03 19:54:16 -05:00
2378f719a7 TODO thing 2025-12-03 19:00:49 -05:00
7f9a962ff7 update egui with hint fix 2025-12-03 18:19:27 -05:00
8b7e3b3009 fix build on newer egui 2025-12-03 16:43:58 -05:00
74813f5f13 misc: use lifetimes for EguiHelper 2025-12-03 16:25:27 -05:00
4a4bce90d0 update egui patch 2025-12-03 16:07:47 -05:00
fabfc6de31 build.rs: fix FontData and FontTweak 2025-12-03 16:07:40 -05:00
7dc48f1b33 fix egui build 2025-12-03 16:02:02 -05:00
c0a05464f3 cargo update 2025-12-03 15:01:47 -05:00
84e283f5c2 high-level update to newer egui 2025-12-03 15:00:42 -05:00
faee5488df possibly fix the canvas 2025-12-03 14:52:08 -05:00
a21fc048ad README.md: add graphing.gardling.com link 2025-12-03 14:08:39 -05:00
10742d0cc4 cleanup flake.nix 2025-12-03 13:12:49 -05:00
31 changed files with 2967 additions and 2037 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@
/Cargo.lock /Cargo.lock
perf.data perf.data
flamegraph.svg flamegraph.svg
result

1758
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,9 @@ description = "Crossplatform (and web-compatible) graphing calculator"
[lib] [lib]
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
[features]
default = ["eframe/wayland"]
[profile.release] [profile.release]
debug = false debug = false
codegen-units = 1 codegen-units = 1
@@ -33,64 +36,66 @@ strip = false
[dependencies] [dependencies]
parsing = { path = "./parsing" } parsing = { path = "./parsing" }
eframe = { git = "https://github.com/titaniumtown/egui.git", rev = "b088efb9fa917845ecb54729a0d2fc592d2399e7", default-features = false, features = [ eframe = { git = "https://github.com/titaniumtown/egui.git", default-features = false, features = [
"glow", "glow",
] } ] }
egui = { git = "https://github.com/titaniumtown/egui.git", rev = "b088efb9fa917845ecb54729a0d2fc592d2399e7", default-features = false, features = [ egui = { git = "https://github.com/titaniumtown/egui.git", default-features = false, features = [
"serde", "serde",
] } ] }
epaint = { git = "https://github.com/titaniumtown/egui.git", rev = "b088efb9fa917845ecb54729a0d2fc592d2399e7", default-features = false , features = [ epaint = { git = "https://github.com/titaniumtown/egui.git", default-features = false , features = [
"bytemuck", "bytemuck",
] } ] }
emath = { git = "https://github.com/titaniumtown/egui.git", rev = "b088efb9fa917845ecb54729a0d2fc592d2399e7", default-features = false } emath = { git = "https://github.com/titaniumtown/egui.git", default-features = false }
egui_plot = { git = "https://github.com/titaniumtown/egui.git", rev = "b088efb9fa917845ecb54729a0d2fc592d2399e7", default-features = false } egui_plot = { version = "0.34.0", default-features = false }
ruzstd = "0.8"
shadow-rs = { version = "0.12", default-features = false }
const_format = { version = "0.2", default-features = false, features = ["fmt"] }
cfg-if = "1"
ruzstd = "0.5"
tracing = "0.1" tracing = "0.1"
itertools = "0.10" itertools = "0.14"
static_assertions = "1.1" static_assertions = "1.1"
bincode = "1.3" bincode = "1.3"
serde = "1" serde = "1"
base64 = "0.21" base64 = "0.22"
[dev-dependencies] [dev-dependencies]
benchmarks = { path = "./benchmarks" } benchmarks = { path = "./benchmarks" }
[build-dependencies] [build-dependencies]
shadow-rs = "0.12" shadow-rs = "1.4"
epaint = { git = "https://github.com/titaniumtown/egui.git", rev = "b088efb9fa917845ecb54729a0d2fc592d2399e7", default-features = false, features = [ epaint = { git = "https://github.com/titaniumtown/egui.git", default-features = false, features = [
"bytemuck", "bytemuck",
] } ] }
egui = { git = "https://github.com/titaniumtown/egui.git", rev = "b088efb9fa917845ecb54729a0d2fc592d2399e7", default-features = false, features = [ egui = { git = "https://github.com/titaniumtown/egui.git", default-features = false, features = [
"serde", "serde",
] } ] }
bincode = "1.3" bincode = "1.3"
serde = "1" serde = "1"
serde_json = "1" serde_json = "1"
zstd = { version = "0.11", default-features = false, features = ["pkg-config"] } zstd = { version = "0.13", default-features = false, features = ["pkg-config"] }
run_script = "0.9" itertools = "0.14"
json5 = "0.4" allsorts = "0.15"
itertools = "0.10"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
instant = "0.1"
tracing-subscriber = "0.3" tracing-subscriber = "0.3"
getrandom = { version = "0.2" }
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
instant = { version = "0.1", features = ["wasm-bindgen"] } lol_alloc = "0.4.1"
lol_alloc = "0.4.0"
wasm-bindgen = { version = "0.2", default-features = false, features = ["std"] } wasm-bindgen = { version = "0.2", default-features = false, features = ["std"] }
web-sys = "0.3" web-sys = "0.3"
tracing-wasm = "0.2" tracing-wasm = "0.2"
getrandom = { version = "0.2", features = ["js"] }
# pinned to 0.4.54 in order to be compatible with nixos's wasm-bindgen-cli version # pinned to 0.4.54 in order to be compatible with nixos's wasm-bindgen-cli version
wasm-bindgen-futures = "=0.4.54" wasm-bindgen-futures = "=0.4.54"
[package.metadata.cargo-all-features] [package.metadata.cargo-all-features]
skip_optional_dependencies = true #don't test optional dependencies, only features skip_optional_dependencies = true #don't test optional dependencies, only features
# various dependency patches fix issues with nix complication such as:
# ````
# ln: failed to create symbolic link '/nix/store/plh3y4gfxgwcacjccv72f551y1k89x75-cargo-vendor-dir/ecolor-0.33.2/qb43vsx43av6kf4h9y4bsmisvbjlcxd1-ecolor-0.33.2': Permission denied
# ````
[patch.crates-io]
egui = { git = "https://github.com/titaniumtown/egui.git" }
eframe = { git = "https://github.com/titaniumtown/egui.git" }
epaint = { git = "https://github.com/titaniumtown/egui.git" }
emath = { git = "https://github.com/titaniumtown/egui.git" }
ecolor = { git = "https://github.com/titaniumtown/egui.git" }

View File

@@ -4,4 +4,4 @@
<img src="assets/logo.svg" alt="logo" width="200"/> <img src="assets/logo.svg" alt="logo" width="200"/>
### What is this? ### What is this?
The aim of this project is to provide a [Desmos](https://www.desmos.com/)-like graphing experience. This project is written in [Rust](https://www.rust-lang.org/) and takes advantage of [egui](https://github.com/emilk/egui) as it's graphical backend. Meaning that this program can be compiled targeting either the web, [such as here](https://titaniumtown.github.io), as well as a native application. The aim of this project is to provide a [Desmos](https://www.desmos.com/)-like graphing experience. This project is written in [Rust](https://www.rust-lang.org/) and takes advantage of [egui](https://github.com/emilk/egui) as it's graphical backend. Meaning that this program can be compiled targeting either the web, [such as here](https://graphing.gardling.com), as well as a native application.

16
TODO.md
View File

@@ -1,18 +1,20 @@
## TODO: ## TODO:
1. Function management 1. Function management
- Integrals between functions (too hard to implement, maybe will shelve) a. Integrals between functions (too hard to implement, maybe will shelve)
- Display intersection between functions (would have to rewrite a lot of the function plotting handling) b. Display intersection between functions (would have to rewrite a lot of the function plotting handling)
- [Drag and drop support](https://github.com/emilk/egui/discussions/1530) in the UI to re-order functions c. [Drag and drop support](https://github.com/emilk/egui/discussions/1530) in the UI to re-order functions
- Hide/disable functions d. Hide/disable functions
- Prevent user from making too many function entries e. Prevent user from making too many function entries
- Display function errors as tooltips or a warning box (not preventing the display of the graph) f. Display function errors as tooltips or a warning box (not preventing the display of the graph)
- Clone functions g. Clone functions
2. Smart display of graph 2. Smart display of graph
- Display of intersections between functions - Display of intersections between functions
3. Allow constants in min/max integral input (like pi or euler's number) 3. Allow constants in min/max integral input (like pi or euler's number)
4. Sliding values for functions (like a user-interactable slider that adjusts a variable in the function, like desmos) 4. Sliding values for functions (like a user-interactable slider that adjusts a variable in the function, like desmos)
5. Fix integral display 5. Fix integral display
6. Better handling of roots and extrema finding 6. Better handling of roots and extrema finding
a. For instance, persistance, the roots shouldn't be recalculated for each movement of the viewport
b. If applicable, the roots/extrema should be expressed in terms of constants such as a root of a number, pi, or something else.
7. Add closing animation for function entry 7. Add closing animation for function entry
8. Fix mobile text input 8. Fix mobile text input
9. Write custom plotter 9. Write custom plotter

View File

@@ -8,81 +8,81 @@ use criterion::{criterion_group, criterion_main, Criterion};
use pprof::ProfilerGuard; use pprof::ProfilerGuard;
pub struct FlamegraphProfiler<'a> { pub struct FlamegraphProfiler<'a> {
frequency: c_int, frequency: c_int,
active_profiler: Option<ProfilerGuard<'a>>, active_profiler: Option<ProfilerGuard<'a>>,
} }
impl<'a> FlamegraphProfiler<'a> { impl<'a> FlamegraphProfiler<'a> {
#[allow(dead_code)] #[allow(dead_code)]
pub fn new(frequency: c_int) -> Self { pub fn new(frequency: c_int) -> Self {
FlamegraphProfiler { FlamegraphProfiler {
frequency, frequency,
active_profiler: None, active_profiler: None,
} }
} }
} }
impl<'a> Profiler for FlamegraphProfiler<'a> { impl<'a> Profiler for FlamegraphProfiler<'a> {
fn start_profiling(&mut self, _benchmark_id: &str, _benchmark_dir: &Path) { fn start_profiling(&mut self, _benchmark_id: &str, _benchmark_dir: &Path) {
self.active_profiler = Some(ProfilerGuard::new(self.frequency).unwrap()); self.active_profiler = Some(ProfilerGuard::new(self.frequency).unwrap());
} }
fn stop_profiling(&mut self, _benchmark_id: &str, benchmark_dir: &Path) { fn stop_profiling(&mut self, _benchmark_id: &str, benchmark_dir: &Path) {
std::fs::create_dir_all(benchmark_dir).unwrap(); std::fs::create_dir_all(benchmark_dir).unwrap();
let flamegraph_path = benchmark_dir.join("flamegraph.svg"); let flamegraph_path = benchmark_dir.join("flamegraph.svg");
let flamegraph_file = File::create(&flamegraph_path) let flamegraph_file = File::create(&flamegraph_path)
.expect("File system error while creating flamegraph.svg"); .expect("File system error while creating flamegraph.svg");
if let Some(profiler) = self.active_profiler.take() { if let Some(profiler) = self.active_profiler.take() {
profiler profiler
.report() .report()
.build() .build()
.unwrap() .unwrap()
.flamegraph(flamegraph_file) .flamegraph(flamegraph_file)
.expect("Error writing flamegraph"); .expect("Error writing flamegraph");
} }
} }
} }
#[allow(dead_code)] // this infact IS used by benchmarks #[allow(dead_code)] // this infact IS used by benchmarks
fn custom_criterion() -> Criterion { fn custom_criterion() -> Criterion {
Criterion::default() Criterion::default()
.warm_up_time(Duration::from_millis(250)) .warm_up_time(Duration::from_millis(250))
.sample_size(1000) .sample_size(1000)
} }
#[allow(dead_code)] // this infact IS used by benchmarks #[allow(dead_code)] // this infact IS used by benchmarks
fn custom_criterion_flamegraph() -> Criterion { fn custom_criterion_flamegraph() -> Criterion {
custom_criterion().with_profiler(FlamegraphProfiler::new(100)) custom_criterion().with_profiler(FlamegraphProfiler::new(100))
} }
fn mutli_split_function(c: &mut Criterion) { fn mutli_split_function(c: &mut Criterion) {
let data_chars = vec![ let data_chars = vec![
"sin(x)cos(x)", "sin(x)cos(x)",
"x^2", "x^2",
"2x", "2x",
"log10(x)", "log10(x)",
"E^x", "E^x",
"xxxxx", "xxxxx",
"xsin(x)", "xsin(x)",
"(2x+1)(3x+1)", "(2x+1)(3x+1)",
"x**2", "x**2",
"pipipipipipix", "pipipipipipix",
"pi(2x+1)", "pi(2x+1)",
"(2x+1)pi", "(2x+1)pi",
] ]
.iter() .iter()
.map(|a| a.chars().collect::<Vec<char>>()) .map(|a| a.chars().collect::<Vec<char>>())
.collect::<Vec<Vec<char>>>(); .collect::<Vec<Vec<char>>>();
let mut group = c.benchmark_group("split_function"); let mut group = c.benchmark_group("split_function");
for entry in data_chars { for entry in data_chars {
group.bench_function(entry.iter().collect::<String>(), |b| { group.bench_function(entry.iter().collect::<String>(), |b| {
b.iter(|| { b.iter(|| {
split_function_chars(&entry, SplitType::Multiplication); split_function_chars(&entry, SplitType::Multiplication);
}) })
}); });
} }
group.finish(); group.finish();
} }
// Uncomment to enable flamegraph profiling // Uncomment to enable flamegraph profiling

129
build.rs
View File

@@ -1,79 +1,73 @@
use allsorts::{
binary::read::ReadScope,
font::{Font, MatchingPresentation},
font_data::FontData as AllsortsFontData,
subset::subset,
tag,
};
use epaint::{
FontFamily,
text::{FontData, FontDefinitions, FontTweak},
};
use std::{ use std::{
collections::BTreeMap, collections::BTreeMap,
env, env,
fs::File, fs::File,
io::{BufWriter, Write}, io::{BufWriter, Write},
path::Path, path::Path,
sync::Arc,
}; };
use epaint::{
FontFamily,
text::{FontData, FontDefinitions, FontTweak},
};
use run_script::ScriptOptions;
include!(concat!( include!(concat!(
env!("CARGO_MANIFEST_DIR"), env!("CARGO_MANIFEST_DIR"),
"/src/unicode_helper.rs" "/src/unicode_helper.rs"
)); ));
fn font_stripper(from: &str, out: &str, unicodes: Vec<char>) -> Result<Vec<u8>, String> { fn font_stripper(from: &str, out: &str, unicodes: Vec<char>) -> Result<Vec<u8>, String> {
let unicodes: Vec<String> = unicodes.iter().map(|c| to_unicode_hash(*c)).collect(); let font_path = format!("{}/assets/{}", env!("CARGO_MANIFEST_DIR"), from);
let font_data = std::fs::read(&font_path).map_err(|e| e.to_string())?;
let scope = ReadScope::new(&font_data);
let font_file = scope
.read::<AllsortsFontData>()
.map_err(|e| format!("Failed to read font data: {}", e))?;
let provider = font_file
.table_provider(0)
.map_err(|e| format!("Failed to get table provider: {}", e))?;
let mut font = Font::new(provider).map_err(|e| format!("Failed to create font: {:?}", e))?;
let mut glyph_ids = Vec::new();
for &ch in &unicodes {
let mut glyph_ids_curr = font
.map_glyphs(
&ch.to_string(),
tag::LATN,
MatchingPresentation::NotRequired,
)
.into_iter()
.map(|glyph| glyph.glyph_index)
.collect();
glyph_ids.append(&mut glyph_ids_curr);
}
// Include .notdef glyph
glyph_ids.push(0);
glyph_ids.sort();
glyph_ids.dedup();
let subset_data = subset(&font.font_table_provider, &glyph_ids)
.map_err(|e| format!("Failed to subset font: {}", e))?;
let new_path = [&env::var("OUT_DIR").unwrap(), out].concat(); let new_path = [&env::var("OUT_DIR").unwrap(), out].concat();
let unicodes_formatted = unicodes std::fs::write(&new_path, &subset_data)
.iter() .map_err(|e| format!("Failed to write subset font: {}", e))?;
.map(|u| format!("U+{}", u))
.collect::<Vec<String>>()
.join(",");
// Test to see if pyftsubset is found Ok(subset_data)
let pyftsubset_detect = run_script::run("whereis pyftsubset", &(vec![]), &ScriptOptions::new());
match pyftsubset_detect {
Ok((_i, s1, _s2)) => {
if s1 == "pyftsubset: " {
return Err(String::from("pyftsubset not found"));
}
}
// It was not, return an error and abort
Err(x) => return Err(x.to_string()),
}
let script_result = run_script::run(
&format!(
"pyftsubset {}/assets/{} --unicodes={}
mv {}/assets/{} {}",
env!("CARGO_MANIFEST_DIR"),
from,
unicodes_formatted,
env!("CARGO_MANIFEST_DIR"),
from.replace(".ttf", ".subset.ttf"),
new_path
),
&(vec![]),
&ScriptOptions::new(),
);
if let Ok((_, _, error)) = script_result {
if error.is_empty() {
return Ok(std::fs::read(new_path).unwrap());
} else {
return Err(error);
}
} else if let Err(error) = script_result {
return Err(error.to_string());
}
unreachable!()
} }
fn main() { fn main() {
// rebuild if new commit or contents of `assets` folder changed // rebuild if contents of `assets` folder changed
println!("cargo:rerun-if-changed=.git/logs/HEAD");
println!("cargo:rerun-if-changed=assets/*"); println!("cargo:rerun-if-changed=assets/*");
shadow_rs::new().expect("Could not initialize shadow_rs");
let mut main_chars: Vec<char> = let mut main_chars: Vec<char> =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzsu0123456789?.,!(){}[]-_=+-/<>'\\ :^*`@#$%&|~;" b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzsu0123456789?.,!(){}[]-_=+-/<>'\\ :^*`@#$%&|~;"
.iter() .iter()
@@ -104,37 +98,38 @@ fn main() {
font_data: BTreeMap::from([ font_data: BTreeMap::from([
( (
"Ubuntu-Light".to_owned(), "Ubuntu-Light".to_owned(),
FontData::from_owned( Arc::new(FontData::from_owned(
font_stripper( font_stripper(
"Ubuntu-Light.ttf", "Ubuntu-Light.ttf",
"ubuntu-light.ttf", "ubuntu-light.ttf",
[main_chars, vec!['∫']].concat(), [main_chars, vec!['∫']].concat(),
) )
.unwrap(), .unwrap(),
), )),
), ),
( (
"NotoEmoji-Regular".to_owned(), "NotoEmoji-Regular".to_owned(),
FontData::from_owned( Arc::new(FontData::from_owned(
font_stripper( font_stripper(
"NotoEmoji-Regular.ttf", "NotoEmoji-Regular.ttf",
"noto-emoji.ttf", "noto-emoji.ttf",
vec!['🌞', '🌙', '✖'], vec!['🌞', '🌙', '✖'],
) )
.unwrap(), .unwrap(),
), )),
), ),
( (
"emoji-icon-font".to_owned(), "emoji-icon-font".to_owned(),
FontData::from_owned( Arc::new(
font_stripper("emoji-icon-font.ttf", "emoji-icon.ttf", vec!['⚙']).unwrap(), FontData::from_owned(
) font_stripper("emoji-icon-font.ttf", "emoji-icon.ttf", vec!['⚙']).unwrap(),
.tweak(FontTweak { )
scale: 0.8, .tweak(FontTweak {
y_offset_factor: 0.07, scale: 0.8,
y_offset: 0.0, y_offset_factor: 0.07,
baseline_offset_factor: -0.0333, y_offset: -0.0333,
}), }),
),
), ),
]), ]),
families: BTreeMap::from([ families: BTreeMap::from([

20
flake.lock generated
View File

@@ -38,8 +38,7 @@
"inputs": { "inputs": {
"flake-utils": "flake-utils", "flake-utils": "flake-utils",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay", "rust-overlay": "rust-overlay"
"simon-egui": "simon-egui"
} }
}, },
"rust-overlay": { "rust-overlay": {
@@ -62,23 +61,6 @@
"type": "github" "type": "github"
} }
}, },
"simon-egui": {
"flake": false,
"locked": {
"lastModified": 1764730109,
"narHash": "sha256-vNETC0oq6tKJKF8KOGQKKIWRom38m0RwGgi3MPBrRx8=",
"owner": "Titaniumtown",
"repo": "egui",
"rev": "b63c21d70150f1b414370f0f9a8af56e886662f4",
"type": "github"
},
"original": {
"owner": "Titaniumtown",
"repo": "egui",
"rev": "b63c21d70150f1b414370f0f9a8af56e886662f4",
"type": "github"
}
},
"systems": { "systems": {
"locked": { "locked": {
"lastModified": 1681028828, "lastModified": 1681028828,

View File

@@ -1,5 +1,5 @@
{ {
description = "YTBN Graphing Software - Web-compatible graphing calculator"; description = "YTBN Graphing Software";
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
@@ -8,10 +8,6 @@
url = "github:oxalica/rust-overlay"; url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
simon-egui = {
url = "github:Titaniumtown/egui/b63c21d70150f1b414370f0f9a8af56e886662f4";
flake = false;
};
}; };
outputs = outputs =
@@ -20,7 +16,6 @@
nixpkgs, nixpkgs,
flake-utils, flake-utils,
rust-overlay, rust-overlay,
simon-egui,
}: }:
flake-utils.lib.eachDefaultSystem ( flake-utils.lib.eachDefaultSystem (
system: system:
@@ -40,33 +35,17 @@
rustc = rustToolchain; rustc = rustToolchain;
}; };
# Create a combined source with the main project and dependencies
combinedSrc = pkgs.stdenv.mkDerivation {
name = "ytbn-combined-src";
phases = [ "installPhase" ];
installPhase = ''
mkdir -p $out/integral_site_rust
mkdir -p $out/simon-egui
cp -r ${./.}/* $out/integral_site_rust/
cp -r ${simon-egui}/* $out/simon-egui/
chmod -R u+w $out
'';
};
# Build the wasm library using rustPlatform # Build the wasm library using rustPlatform
wasmLib = rustPlatform.buildRustPackage { wasmLib = rustPlatform.buildRustPackage {
pname = "ytbn-graphing-software-wasm"; pname = "ytbn-graphing-software-wasm";
version = "0.1.0"; version = "0.1.0";
src = combinedSrc; src = ./.;
sourceRoot = "${combinedSrc.name}/integral_site_rust";
cargoLock = { cargoLock = {
lockFile = ./Cargo.lock; lockFile = ./Cargo.lock;
outputHashes = { outputHashes = {
"ecolor-0.25.0" = "sha256-9s5LCngwvIIL43txT6sBs4JlRXqmYt1Kw8hlDnwx+DI="; "ecolor-0.33.2" = "sha256-jdQK55yKZptadwosrJXIhoQDGNeELQmPExWRsGc0VG0=";
}; };
}; };
@@ -81,11 +60,30 @@
zstd zstd
]; ];
# Run all tests on native target before building wasm
# Note: Tests run without --release because the release profile uses
# panic=abort which is incompatible with the test harness.
checkPhase =
let
libPath = pkgs.lib.makeLibraryPath (
with pkgs;
[
libxkbcommon
libGL
wayland
]
);
in
''
runHook preCheck
export LD_LIBRARY_PATH="${libPath}:$LD_LIBRARY_PATH"
cargo test --workspace
runHook postCheck
'';
buildPhase = '' buildPhase = ''
runHook preBuild runHook preBuild
export HOME=$TMPDIR
cargo build \ cargo build \
--release \ --release \
--lib \ --lib \
@@ -101,7 +99,7 @@
runHook postInstall runHook postInstall
''; '';
doCheck = false; doCheck = true;
}; };
# Final web package with wasm-bindgen processing # Final web package with wasm-bindgen processing
@@ -128,7 +126,7 @@
# Optimize wasm (enable features used by modern rust wasm targets) # Optimize wasm (enable features used by modern rust wasm targets)
wasm-opt out/ytbn_graphing_software_bg.wasm \ wasm-opt out/ytbn_graphing_software_bg.wasm \
-O2 --fast-math \ -Oz \
--enable-bulk-memory \ --enable-bulk-memory \
--enable-nontrapping-float-to-int \ --enable-nontrapping-float-to-int \
--enable-sign-ext \ --enable-sign-ext \
@@ -157,13 +155,6 @@
runHook postInstall runHook postInstall
''; '';
meta = with pkgs.lib; {
description = "Web-compatible graphing calculator similar to Desmos";
homepage = "https://github.com/Titaniumtown/YTBN-Graphing-Software";
license = licenses.agpl3Only;
platforms = platforms.all;
};
}; };
in in
{ {
@@ -178,7 +169,6 @@
rustToolchain rustToolchain
wasm-bindgen-cli wasm-bindgen-cli
binaryen binaryen
python3Packages.fonttools
rust-analyzer rust-analyzer
pkg-config pkg-config
clang clang
@@ -187,9 +177,6 @@
libxkbcommon libxkbcommon
libGL libGL
wayland wayland
xorg.libX11
xorg.libXcursor
xorg.libXi
]; ];
buildInputs = with pkgs; [ buildInputs = with pkgs; [
@@ -203,9 +190,6 @@
libxkbcommon libxkbcommon
libGL libGL
wayland wayland
xorg.libX11
xorg.libXcursor
xorg.libXi
] ]
); );
}; };

View File

@@ -10,11 +10,11 @@ description = "Parsing library for YTBN-Graphing-Software"
[lib] [lib]
[dependencies] [dependencies]
phf = { version = "0.11" } phf = { version = "0.13" }
exmex = {version = "0.17.5", features = ["partial"]} exmex = {version = "0.20.5", features = ["partial"]}
[build-dependencies] [build-dependencies]
phf_codegen = { version = "0.11" } phf_codegen = { version = "0.13" }
[package.metadata.cargo-all-features] [package.metadata.cargo-all-features]
skip_optional_dependencies = true #don't test optional dependencies, only features skip_optional_dependencies = true #don't test optional dependencies, only features

View File

@@ -5,46 +5,46 @@ use std::path::Path;
/// REMEMBER TO UPDATE THIS IF EXMEX ADDS NEW FUNCTIONS /// REMEMBER TO UPDATE THIS IF EXMEX ADDS NEW FUNCTIONS
const SUPPORTED_FUNCTIONS: [&str; 22] = [ const SUPPORTED_FUNCTIONS: [&str; 22] = [
"abs", "signum", "sin", "cos", "tan", "asin", "acos", "atan", "sinh", "cosh", "tanh", "floor", "abs", "signum", "sin", "cos", "tan", "asin", "acos", "atan", "sinh", "cosh", "tanh", "floor",
"round", "ceil", "trunc", "fract", "exp", "sqrt", "cbrt", "ln", "log2", "log10", "round", "ceil", "trunc", "fract", "exp", "sqrt", "cbrt", "ln", "log2", "log10",
]; ];
fn main() { fn main() {
println!("cargo:rerun-if-changed=src/*"); println!("cargo:rerun-if-changed=src/*");
generate_hashmap(); generate_hashmap();
} }
fn generate_hashmap() { fn generate_hashmap() {
let path = Path::new(&env::var("OUT_DIR").unwrap()).join("codegen.rs"); let path = Path::new(&env::var("OUT_DIR").unwrap()).join("codegen.rs");
let mut file = BufWriter::new(File::create(path).expect("Could not create file")); let mut file = BufWriter::new(File::create(path).expect("Could not create file"));
let string_hashmap = let string_hashmap =
compile_hashmap(SUPPORTED_FUNCTIONS.iter().map(|a| a.to_string()).collect()); compile_hashmap(SUPPORTED_FUNCTIONS.iter().map(|a| a.to_string()).collect());
let mut hashmap = phf_codegen::Map::new(); let mut hashmap = phf_codegen::Map::new();
for (key, value) in string_hashmap.iter() { for (key, value) in string_hashmap.iter() {
hashmap.entry(key, value); hashmap.entry(key, value);
} }
write!( write!(
&mut file, &mut file,
"static COMPLETION_HASHMAP: phf::Map<&'static str, Hint> = {};", "static COMPLETION_HASHMAP: phf::Map<&'static str, Hint> = {};",
hashmap.build() hashmap.build()
) )
.expect("Could not write to file"); .expect("Could not write to file");
write!( write!(
&mut file, &mut file,
"#[allow(dead_code)] pub const SUPPORTED_FUNCTIONS: [&str; {}] = {:?};", "#[allow(dead_code)] pub const SUPPORTED_FUNCTIONS: [&str; {}] = {:?};",
SUPPORTED_FUNCTIONS.len(), SUPPORTED_FUNCTIONS.len(),
SUPPORTED_FUNCTIONS.to_vec() SUPPORTED_FUNCTIONS.to_vec()
) )
.expect("Could not write to file"); .expect("Could not write to file");
} }
include!(concat!( include!(concat!(
env!("CARGO_MANIFEST_DIR"), env!("CARGO_MANIFEST_DIR"),
"/src/autocomplete_hashmap.rs" "/src/autocomplete_hashmap.rs"
)); ));

View File

@@ -4,113 +4,113 @@ use crate::{generate_hint, Hint, HINT_EMPTY};
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
pub enum Movement { pub enum Movement {
Complete, Complete,
#[allow(dead_code)] #[allow(dead_code)]
Down, Down,
#[allow(dead_code)] #[allow(dead_code)]
Up, Up,
None, None,
} }
impl Movement { impl Movement {
pub const fn is_none(&self) -> bool { pub const fn is_none(&self) -> bool {
matches!(&self, &Self::None) matches!(&self, &Self::None)
} }
pub const fn is_complete(&self) -> bool { pub const fn is_complete(&self) -> bool {
matches!(&self, &Self::Complete) matches!(&self, &Self::Complete)
} }
} }
impl Default for Movement { impl Default for Movement {
fn default() -> Self { fn default() -> Self {
Self::None Self::None
} }
} }
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub struct AutoComplete<'a> { pub struct AutoComplete<'a> {
pub i: usize, pub i: usize,
pub hint: &'a Hint<'a>, pub hint: &'a Hint<'a>,
pub string: String, pub string: String,
} }
impl<'a> Default for AutoComplete<'a> { impl<'a> Default for AutoComplete<'a> {
fn default() -> AutoComplete<'a> { fn default() -> AutoComplete<'a> {
AutoComplete::EMPTY AutoComplete::EMPTY
} }
} }
impl<'a> AutoComplete<'a> { impl<'a> AutoComplete<'a> {
pub const EMPTY: AutoComplete<'a> = Self { pub const EMPTY: AutoComplete<'a> = Self {
i: 0, i: 0,
hint: &HINT_EMPTY, hint: &HINT_EMPTY,
string: String::new(), string: String::new(),
}; };
#[allow(dead_code)] #[allow(dead_code)]
pub fn update_string(&mut self, string: &str) { pub fn update_string(&mut self, string: &str) {
if self.string != string { if self.string != string {
// catch empty strings here to avoid call to `generate_hint` and unnecessary logic // catch empty strings here to avoid call to `generate_hint` and unnecessary logic
if string.is_empty() { if string.is_empty() {
*self = Self::EMPTY; *self = Self::EMPTY;
} else { } else {
self.string = string.to_owned(); self.string = string.to_owned();
self.do_update_logic(); self.do_update_logic();
} }
} }
} }
/// Runs update logic assuming that a change to `self.string` has been made /// Runs update logic assuming that a change to `self.string` has been made
fn do_update_logic(&mut self) { fn do_update_logic(&mut self) {
self.i = 0; self.i = 0;
self.hint = generate_hint(&self.string); self.hint = generate_hint(&self.string);
} }
#[allow(dead_code)] #[allow(dead_code)]
pub fn register_movement(&mut self, movement: &Movement) { pub fn register_movement(&mut self, movement: &Movement) {
if movement.is_none() | self.hint.is_none() { if movement.is_none() | self.hint.is_none() {
return; return;
} }
match self.hint { match self.hint {
Hint::Many(hints) => { Hint::Many(hints) => {
// Impossible for plural hints to be singular or non-existant // Impossible for plural hints to be singular or non-existant
debug_assert!(hints.len() > 1); // check on debug debug_assert!(hints.len() > 1); // check on debug
match movement { match movement {
Movement::Up => { Movement::Up => {
// Wrap self.i to maximum `i` value if needed // Wrap self.i to maximum `i` value if needed
if self.i == 0 { if self.i == 0 {
self.i = hints.len() - 1; self.i = hints.len() - 1;
} else { } else {
self.i -= 1; self.i -= 1;
} }
} }
Movement::Down => { Movement::Down => {
// Add one, if resulting value is above maximum `i` value, set `i` to 0 // Add one, if resulting value is above maximum `i` value, set `i` to 0
self.i += 1; self.i += 1;
if self.i > (hints.len() - 1) { if self.i > (hints.len() - 1) {
self.i = 0; self.i = 0;
} }
} }
Movement::Complete => { Movement::Complete => {
self.apply_hint(unsafe { hints.get_unchecked(self.i) }); self.apply_hint(unsafe { hints.get_unchecked(self.i) });
} }
_ => unsafe { unreachable_unchecked() }, _ => unsafe { unreachable_unchecked() },
} }
} }
Hint::Single(hint) => { Hint::Single(hint) => {
if movement.is_complete() { if movement.is_complete() {
self.apply_hint(hint); self.apply_hint(hint);
} }
} }
Hint::None => unsafe { unreachable_unchecked() }, Hint::None => unsafe { unreachable_unchecked() },
} }
} }
pub fn apply_hint(&mut self, hint: &str) { pub fn apply_hint(&mut self, hint: &str) {
self.string.push_str(hint); self.string.push_str(hint);
self.do_update_logic(); self.do_update_logic();
} }
} }

View File

@@ -3,81 +3,82 @@ use std::collections::HashSet;
/// https://www.dotnetperls.com/sort-rust /// https://www.dotnetperls.com/sort-rust
fn compare_len_reverse_alpha(a: &String, b: &String) -> Ordering { fn compare_len_reverse_alpha(a: &String, b: &String) -> Ordering {
match a.len().cmp(&b.len()) { match a.len().cmp(&b.len()) {
Ordering::Equal => b.cmp(a), Ordering::Equal => b.cmp(a),
order => order, order => order,
} }
} }
/// Generates hashmap (well really a vector of tuple of strings that are then turned into a hashmap by phf) /// Generates hashmap (well really a vector of tuple of strings that are then turned into a hashmap by phf)
#[allow(dead_code)] #[allow(dead_code)]
pub fn compile_hashmap(data: Vec<String>) -> Vec<(String, String)> { pub fn compile_hashmap(data: Vec<String>) -> Vec<(String, String)> {
let mut seen = HashSet::new(); let mut seen = HashSet::new();
let tuple_list_1: Vec<(String, String)> = data let tuple_list_1: Vec<(String, String)> = data
.iter() .iter()
.map(|e| e.to_string() + "(") .map(|e| e.to_string() + "(")
.flat_map(|func| all_possible_splits(func, &mut seen)) .flat_map(|func| all_possible_splits(func, &mut seen))
.collect(); .collect();
let keys: Vec<&String> = tuple_list_1.iter().map(|(a, _)| a).collect(); let keys: Vec<&String> = tuple_list_1.iter().map(|(a, _)| a).collect();
let mut output: Vec<(String, String)> = Vec::new(); let mut output: Vec<(String, String)> = Vec::new();
let mut seen_3: HashSet<String> = HashSet::new(); let mut seen_3: HashSet<String> = HashSet::new();
for (key, value) in tuple_list_1.iter() { for (key, value) in tuple_list_1.iter() {
if seen_3.contains(key) { if seen_3.contains(key) {
continue; continue;
} }
seen_3.insert(key.clone()); seen_3.insert(key.clone());
let count_keys = keys.iter().filter(|a| a == &&key).count(); let count_keys = keys.iter().filter(|a| a == &&key).count();
match count_keys.cmp(&1usize) { match count_keys.cmp(&1usize) {
Ordering::Less => { Ordering::Less => {
panic!("Number of values for {key} is 0!"); panic!("Number of values for {key} is 0!");
} }
Ordering::Greater => { Ordering::Greater => {
let mut multi_data = tuple_list_1 let mut multi_data = tuple_list_1
.iter() .iter()
.filter(|(a, _)| a == key) .filter(|(a, _)| a == key)
.map(|(_, b)| b) .map(|(_, b)| b)
.collect::<Vec<&String>>(); .collect::<Vec<&String>>();
multi_data.sort_unstable_by(|a, b| compare_len_reverse_alpha(a, b)); multi_data.sort_unstable_by(|a, b| compare_len_reverse_alpha(a, b));
output.push((key.clone(), format!("Hint::Many(&{:?})", multi_data))); output.push((key.clone(), format!("Hint::Many(&{:?})", multi_data)));
} }
Ordering::Equal => output.push((key.clone(), format!(r#"Hint::Single("{}")"#, value))), Ordering::Equal => output.push((key.clone(), format!(r#"Hint::Single("{}")"#, value))),
} }
} }
// sort // sort
output.sort_unstable_by(|a, b| { output.sort_unstable_by(|a, b| {
let new_a = format!(r#"("{}", {})"#, a.0, a.1); let new_a = format!(r#"("{}", {})"#, a.0, a.1);
let new_b = format!(r#"("{}", {})"#, b.0, b.1); let new_b = format!(r#"("{}", {})"#, b.0, b.1);
compare_len_reverse_alpha(&new_b, &new_a) compare_len_reverse_alpha(&new_b, &new_a)
}); });
output output
} }
/// Returns a vector of all possible splitting combinations of a strings /// Returns a vector of all possible splitting combinations of a strings
#[allow(dead_code)] #[allow(dead_code)]
fn all_possible_splits( fn all_possible_splits(
func: String, seen: &mut HashSet<(String, String)>, func: String,
seen: &mut HashSet<(String, String)>,
) -> Vec<(String, String)> { ) -> Vec<(String, String)> {
(1..func.len()) (1..func.len())
.map(|i| { .map(|i| {
let (first, last) = func.split_at(i); let (first, last) = func.split_at(i);
(first.to_string(), last.to_string()) (first.to_string(), last.to_string())
}) })
.flat_map(|(first, last)| { .flat_map(|(first, last)| {
if seen.contains(&(first.clone(), last.clone())) { if seen.contains(&(first.clone(), last.clone())) {
return None; return None;
} }
seen.insert((first.to_string(), last.to_string())); seen.insert((first.to_string(), last.to_string()));
Some((first, last)) Some((first, last))
}) })
.collect::<Vec<(String, String)>>() .collect::<Vec<(String, String)>>()
} }

View File

@@ -5,9 +5,9 @@ mod splitting;
mod suggestions; mod suggestions;
pub use crate::{ pub use crate::{
autocomplete::{AutoComplete, Movement}, autocomplete::{AutoComplete, Movement},
autocomplete_hashmap::compile_hashmap, autocomplete_hashmap::compile_hashmap,
parsing::{process_func_str, BackingFunction, FlatExWrapper}, parsing::{process_func_str, BackingFunction, FlatExWrapper},
splitting::{split_function, split_function_chars, SplitType}, splitting::{split_function, split_function_chars, SplitType},
suggestions::{generate_hint, get_last_term, Hint, HINT_EMPTY, SUPPORTED_FUNCTIONS}, suggestions::{generate_hint, get_last_term, Hint, HINT_EMPTY, SUPPORTED_FUNCTIONS},
}; };

View File

@@ -3,192 +3,153 @@ 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]
const fn is_none(&self) -> bool { const fn is_none(&self) -> bool {
self.func.is_none() self.func.is_none()
} }
#[inline] #[inline]
pub fn eval(&self, x: &[f64]) -> f64 { pub fn eval(&self, x: &[f64]) -> f64 {
self.func self.func
.as_ref() .as_ref()
.map(|f| f.eval(x).unwrap_or(f64::NAN)) .map(|f| f.eval(x).unwrap_or(f64::NAN))
.unwrap_or(f64::NAN) .unwrap_or(f64::NAN)
} }
#[inline] #[inline]
fn partial(&self, x: usize) -> Self { fn partial_iter(&self, n: usize) -> Self {
self.func self.func
.as_ref() .as_ref()
.map(|f| f.clone().partial(x).map(Self::new).unwrap_or(Self::EMPTY)) .map(|f| {
.unwrap_or(Self::EMPTY) f.clone()
} .partial_iter((0..n).map(|_| 0))
.map(Self::new)
#[inline] .unwrap_or(Self::EMPTY)
fn get_string(&mut self) -> String { })
match self.func_str { .unwrap_or(Self::EMPTY)
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
.as_ref()
.map(|f| {
f.clone()
.partial_iter((0..=n).map(|_| 0))
.map(Self::new)
.unwrap_or(Self::EMPTY)
})
.unwrap_or(Self::EMPTY)
}
} }
impl Default for FlatExWrapper { impl Default for FlatExWrapper {
fn default() -> FlatExWrapper { fn default() -> FlatExWrapper {
FlatExWrapper::EMPTY FlatExWrapper::EMPTY
} }
} }
/// Function that includes f(x), f'(x), f'(x)'s string representation, and f''(x) /// Function that includes f(x), f'(x), f'(x)'s string representation, and f''(x)
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub struct BackingFunction { pub struct BackingFunction {
/// f(x) /// f(x)
function: FlatExWrapper, function: FlatExWrapper,
/// Temporary cache for nth derivative /// Temporary cache for nth derivative
nth_derivative: HashMap<usize, FlatExWrapper>, nth_derivative: HashMap<usize, FlatExWrapper>,
} }
impl Default for BackingFunction { impl Default for BackingFunction {
fn default() -> Self { fn default() -> Self {
Self::new("").unwrap() Self::new("").unwrap()
} }
} }
impl BackingFunction { impl BackingFunction {
pub const fn is_none(&self) -> bool { pub const fn is_none(&self) -> bool {
self.function.is_none() self.function.is_none()
} }
/// Create new [`BackingFunction`] instance /// Create new [`BackingFunction`] instance
pub fn new(func_str: &str) -> Result<Self, String> { pub fn new(func_str: &str) -> Result<Self, String> {
if func_str.is_empty() { if func_str.is_empty() {
return Ok(Self { return Ok(Self {
function: FlatExWrapper::EMPTY, function: FlatExWrapper::EMPTY,
nth_derivative: HashMap::new(), nth_derivative: HashMap::new(),
}); });
} }
let function = FlatExWrapper::new({ let function = FlatExWrapper::new({
let parse_result = exmex::parse::<f64>(func_str); let parse_result = exmex::parse::<f64>(func_str);
match &parse_result { match &parse_result {
Err(e) => return Err(e.to_string()), Err(e) => return Err(e.to_string()),
Ok(ok_result) => { Ok(ok_result) => {
let var_names = ok_result.var_names().to_vec(); let var_names = ok_result.var_names().to_vec();
if var_names != ["x"] { if var_names != ["x"] {
let var_names_not_x: Vec<&String> = var_names let var_names_not_x: Vec<&String> = var_names
.iter() .iter()
.filter(|ele| ele != &"x") .filter(|ele| ele != &"x")
.collect::<Vec<&String>>(); .collect::<Vec<&String>>();
return Err(format!( return Err(format!(
"Error: invalid variable{}", "Error: invalid variable{}",
match var_names_not_x.len() { match var_names_not_x.len() {
1 => String::from(": ") + var_names_not_x[0].as_str(), 1 => String::from(": ") + var_names_not_x[0].as_str(),
_ => format!("s: {:?}", var_names_not_x), _ => format!("s: {:?}", var_names_not_x),
} }
)); ));
} }
} }
} }
unsafe { parse_result.unwrap_unchecked() } unsafe { parse_result.unwrap_unchecked() }
}); });
Ok(Self { Ok(Self {
function, function,
nth_derivative: HashMap::new(), nth_derivative: HashMap::new(),
}) })
} }
// TODO rewrite this logic, it's a mess // TODO rewrite this logic, it's a mess
pub fn generate_derivative(&mut self, derivative: usize) { pub fn generate_derivative(&mut self, derivative: usize) {
if derivative == 0 { if derivative == 0 {
return; return;
} }
if !self.nth_derivative.contains_key(&derivative) { if !self.nth_derivative.contains_key(&derivative) {
let new_func = self.function.partial_iter(derivative); let new_func = self.function.partial_iter(derivative);
self.nth_derivative.insert(derivative, new_func.clone()); self.nth_derivative.insert(derivative, new_func.clone());
} }
} }
pub fn get_function_derivative(&self, derivative: usize) -> &FlatExWrapper { pub fn get_function_derivative(&self, derivative: usize) -> &FlatExWrapper {
if derivative == 0 { if derivative == 0 {
return &self.function; return &self.function;
} else { } else {
return self return self
.nth_derivative .nth_derivative
.get(&derivative) .get(&derivative)
.unwrap_or(&FlatExWrapper::EMPTY); .unwrap_or(&FlatExWrapper::EMPTY);
} }
} }
pub fn get(&mut self, derivative: usize, x: f64) -> f64 { pub fn get(&mut self, derivative: usize, x: f64) -> f64 {
self.get_function_derivative(derivative).eval(&[x]) self.get_function_derivative(derivative).eval(&[x])
} }
} }
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 {
let c = c.to_ascii_lowercase(); let c = c.to_ascii_lowercase();
(c == 'x') | (c == 'e') | (c == 'π') (c == 'x') | (c == 'e') | (c == 'π')
} }
/// Adds asterisks where needed in a function /// Adds asterisks where needed in a function
pub fn process_func_str(function_in: &str) -> String { pub fn process_func_str(function_in: &str) -> String {
if function_in.is_empty() { if function_in.is_empty() {
return String::new(); return String::new();
} }
crate::split_function(function_in, crate::SplitType::Multiplication).join("*") crate::split_function(function_in, crate::SplitType::Multiplication).join("*")
} }

View File

@@ -1,204 +1,288 @@
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> {
split_function_chars( // Protect function names that could be incorrectly split
&input let (protected, replacements) = protect_function_names(input);
.replace("pi", "π") // replace "pi" text with pi symbol
.replace("**", "^") // support alternate manner of expressing exponents split_function_chars(
.replace("exp", "\u{1fc93}") // stop-gap solution to fix the `exp` function &protected
.chars() .replace("pi", "π") // replace "pi" text with pi symbol
.collect::<Vec<char>>(), .replace("**", "^") // support alternate manner of expressing exponents
split, .chars()
) .collect::<Vec<char>>(),
.iter() split,
.map(|x| x.replace('\u{1fc93}', "exp")) // Convert back to `exp` text )
.collect::<Vec<String>>() .iter()
.map(|x| restore_function_names(x, &replacements))
.collect::<Vec<String>>()
} }
// Specifies how to split a function // Specifies how to split a function
#[derive(PartialEq, Debug, Copy, Clone)] #[derive(PartialEq, Debug, Copy, Clone)]
pub enum SplitType { pub enum SplitType {
Multiplication, Multiplication,
Term, Term,
} }
/// Used to store info about a character /// Used to store info about a character
struct BoolSlice { struct BoolSlice {
closing_parens: bool, closing_parens: bool,
open_parens: bool, open_parens: bool,
number: bool, number: bool,
letter: bool, letter: bool,
variable: bool, variable: bool,
masked_num: bool, masked_num: bool,
masked_var: bool, masked_var: bool,
} }
impl BoolSlice { impl BoolSlice {
const fn from_char(c: &char, prev_masked_num: bool, prev_masked_var: bool) -> Self { const fn from_char(c: &char, prev_masked_num: bool, prev_masked_var: bool) -> Self {
let isnumber = c.is_ascii_digit(); let isnumber = c.is_ascii_digit();
let isvariable = is_variable(c); let isvariable = is_variable(c);
Self { Self {
closing_parens: *c == ')', closing_parens: *c == ')',
open_parens: *c == '(', open_parens: *c == '(',
number: isnumber, number: isnumber,
letter: c.is_ascii_alphabetic(), letter: c.is_ascii_alphabetic(),
variable: isvariable, variable: isvariable,
masked_num: match isnumber { masked_num: match isnumber {
true => prev_masked_num, true => prev_masked_num,
false => false, false => false,
}, },
masked_var: match isvariable { masked_var: match isvariable {
true => prev_masked_var, true => prev_masked_var,
false => false, false => false,
}, },
} }
} }
const fn is_unmasked_variable(&self) -> bool { self.variable && !self.masked_var } const fn is_unmasked_variable(&self) -> bool {
self.variable && !self.masked_var
}
const fn is_unmasked_number(&self) -> bool { self.number && !self.masked_num } const fn is_unmasked_number(&self) -> bool {
self.number && !self.masked_num
}
const fn calculate_mask(&mut self, other: &BoolSlice) { /// Returns true if this char is a function name letter (not a standalone variable)
if other.masked_num && self.number { const fn is_function_letter(&self) -> bool {
// If previous char was a masked number, and current char is a number, mask current char's variable status self.letter && !self.is_unmasked_variable()
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
self.masked_var = true;
} else if other.letter && !other.is_unmasked_variable() {
self.masked_num = self.number;
self.masked_var = self.variable;
}
}
const fn splitable(&self, c: &char, other: &BoolSlice, split: &SplitType) -> bool { /// Returns true if this is a "term" - something that can be multiplied
if (*c == '*') | (matches!(split, &SplitType::Term) && other.open_parens) { const fn is_term(&self) -> bool {
true self.is_unmasked_number() || self.is_unmasked_variable() || self.letter
} else if other.closing_parens { }
// Cases like `)x`, `)2`, and `)(`
return (*c == '(') const fn calculate_mask(&mut self, other: &BoolSlice) {
| (self.letter && !self.is_unmasked_variable()) if other.masked_num && self.number {
| self.is_unmasked_variable() // Propagate number masking through consecutive digits
| self.is_unmasked_number(); self.masked_num = true;
} else if *c == '(' { } else if other.masked_var && self.variable {
// Cases like `x(` and `2(` // Propagate variable masking through consecutive variables
return (other.is_unmasked_variable() | other.is_unmasked_number()) && !other.letter; self.masked_var = true;
} else if other.is_unmasked_number() { } else if other.is_function_letter() {
// Cases like `2x` and `2sin(x)` // After a function letter, mask following numbers/variables as part of function name
return self.is_unmasked_variable() | self.letter; self.masked_num = self.number;
} else if self.is_unmasked_variable() | self.letter { self.masked_var = self.variable;
// Cases like `e2` and `xx` }
return other.is_unmasked_number() }
| (other.is_unmasked_variable() && self.is_unmasked_variable())
| other.is_unmasked_variable(); /// Determines if we should split (insert implicit multiplication) before current char
} else if (self.is_unmasked_number() | self.letter | self.is_unmasked_variable()) const fn splitable(&self, c: &char, prev: &BoolSlice, split: &SplitType) -> bool {
&& (other.is_unmasked_number() | other.letter) // Always split on explicit multiplication
{ if *c == '*' {
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
}
} }
// Splits a function (which is represented as an array of characters) based off of the value of SplitType // Splits a function (which is represented as an array of characters) based off of the value of SplitType
pub fn split_function_chars(chars: &[char], split: SplitType) -> Vec<String> { pub fn split_function_chars(chars: &[char], split: SplitType) -> Vec<String> {
// Catch some basic cases // Catch some basic cases
match chars.len() { match chars.len() {
0 => return Vec::new(), 0 => return Vec::new(),
1 => return vec![chars[0].to_string()], 1 => return vec![chars[0].to_string()],
_ => {} _ => {}
} }
// Resulting split-up data // Resulting split-up data
let mut data: Vec<String> = std::vec::from_elem(chars[0].to_string(), 1); let mut data: Vec<String> = std::vec::from_elem(chars[0].to_string(), 1);
// Setup first char here // Setup first char here
let mut prev_char: BoolSlice = BoolSlice::from_char(&chars[0], false, false); let mut prev_char: BoolSlice = BoolSlice::from_char(&chars[0], false, false);
let mut last = unsafe { data.last_mut().unwrap_unchecked() }; let mut last = unsafe { data.last_mut().unwrap_unchecked() };
// Iterate through all chars excluding the first one // Iterate through all chars excluding the first one
for c in chars.iter().skip(1) { for c in chars.iter().skip(1) {
// Set data about current character // Set data about current character
let mut curr_c = BoolSlice::from_char(c, prev_char.masked_num, prev_char.masked_var); let mut curr_c = BoolSlice::from_char(c, prev_char.masked_num, prev_char.masked_var);
curr_c.calculate_mask(&prev_char); curr_c.calculate_mask(&prev_char);
// Append split // Append split
if curr_c.splitable(c, &prev_char, &split) { if curr_c.splitable(c, &prev_char, &split) {
// create new buffer // create new buffer
data.push(String::new()); data.push(String::new());
last = unsafe { data.last_mut().unwrap_unchecked() }; last = unsafe { data.last_mut().unwrap_unchecked() };
} }
// Exclude asterisks // Exclude asterisks
if c != &'*' { if c != &'*' {
last.push(*c); last.push(*c);
} }
// Move current character data to `prev_char` // Move current character data to `prev_char`
prev_char = curr_c; prev_char = curr_c;
} }
data data
} }
#[cfg(test)] #[cfg(test)]
fn assert_test(input: &str, expected: &[&str], split: SplitType) { fn assert_test(input: &str, expected: &[&str], split: SplitType) {
let output = split_function(input, split); let output = split_function(input, split);
let expected_owned = expected let expected_owned = expected
.iter() .iter()
.map(|&x| x.to_owned()) .map(|&x| x.to_owned())
.collect::<Vec<String>>(); .collect::<Vec<String>>();
if output != expected_owned { if output != expected_owned {
panic!( panic!(
"split type: {:?} of {} resulted in {:?} not {:?}", "split type: {:?} of {} resulted in {:?} not {:?}",
split, input, output, expected split, input, output, expected
); );
} }
} }
#[test] #[test]
fn split_function_test() { fn split_function_test() {
assert_test( assert_test(
"sin(x)cos(x)", "sin(x)cos(x)",
&["sin(x)", "cos(x)"], &["sin(x)", "cos(x)"],
SplitType::Multiplication, SplitType::Multiplication,
); );
assert_test( assert_test(
"tanh(cos(x)xx)cos(x)", "tanh(cos(x)xx)cos(x)",
&["tanh(cos(x)", "x", "x)", "cos(x)"], &["tanh(cos(x)", "x", "x)", "cos(x)"],
SplitType::Multiplication, SplitType::Multiplication,
); );
assert_test( assert_test(
"tanh(sin(cos(x)xsin(x)))", "tanh(sin(cos(x)xsin(x)))",
&["tanh(sin(cos(x)", "x", "sin(x)))"], &["tanh(sin(cos(x)", "x", "sin(x)))"],
SplitType::Multiplication, SplitType::Multiplication,
); );
// Some test cases from https://github.com/GraphiteEditor/Graphite/blob/2515620a77478e57c255cd7d97c13cc7065dd99d/frontend/wasm/src/editor_api.rs#L829-L840 // Some test cases from https://github.com/GraphiteEditor/Graphite/blob/2515620a77478e57c255cd7d97c13cc7065dd99d/frontend/wasm/src/editor_api.rs#L829-L840
assert_test("2pi", &["2", "π"], SplitType::Multiplication); assert_test("2pi", &["2", "π"], SplitType::Multiplication);
assert_test("sin(2pi)", &["sin(2", "π)"], SplitType::Multiplication); assert_test("sin(2pi)", &["sin(2", "π)"], SplitType::Multiplication);
assert_test("2sin(pi)", &["2", "sin(π)"], SplitType::Multiplication); assert_test("2sin(pi)", &["2", "sin(π)"], SplitType::Multiplication);
assert_test( assert_test(
"2sin(3(4 + 5))", "2sin(3(4 + 5))",
&["2", "sin(3", "(4 + 5))"], &["2", "sin(3", "(4 + 5))"],
SplitType::Multiplication, SplitType::Multiplication,
); );
assert_test("3abs(-4)", &["3", "abs(-4)"], SplitType::Multiplication); assert_test("3abs(-4)", &["3", "abs(-4)"], SplitType::Multiplication);
assert_test("-1(4)", &["-1", "(4)"], SplitType::Multiplication); assert_test("-1(4)", &["-1", "(4)"], SplitType::Multiplication);
assert_test("(-1)4", &["(-1)", "4"], SplitType::Multiplication); assert_test("(-1)4", &["(-1)", "4"], SplitType::Multiplication);
assert_test( assert_test(
"(((-1)))(4)", "(((-1)))(4)",
&["(((-1)))", "(4)"], &["(((-1)))", "(4)"],
SplitType::Multiplication, SplitType::Multiplication,
); );
assert_test( assert_test(
"2sin(π) + 2cos(tau)", "2sin(π) + 2cos(tau)",
&["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

@@ -14,106 +14,112 @@ macro_rules! test_print {
/// Generate a hint based on the input `input`, returns an `Option<String>` /// Generate a hint based on the input `input`, returns an `Option<String>`
pub fn generate_hint<'a>(input: &str) -> &'a Hint<'a> { pub fn generate_hint<'a>(input: &str) -> &'a Hint<'a> {
if input.is_empty() { if input.is_empty() {
&HINT_EMPTY &HINT_EMPTY
} else { } else {
let chars: Vec<char> = input.chars().collect::<Vec<char>>(); let chars: Vec<char> = input.chars().collect::<Vec<char>>();
let key = get_last_term(&chars); let key = get_last_term(&chars);
match key { match key {
Some(key) => { Some(key) => {
if let Some(hint) = COMPLETION_HASHMAP.get(&key) { if let Some(hint) = COMPLETION_HASHMAP.get(&key) {
return hint; return hint;
} }
} }
None => { None => {
return &Hint::None; return &Hint::None;
} }
} }
let mut open_parens: usize = 0; let mut open_parens: usize = 0;
let mut closed_parens: usize = 0; let mut closed_parens: usize = 0;
chars.iter().for_each(|chr| match *chr { chars.iter().for_each(|chr| match *chr {
'(' => open_parens += 1, '(' => open_parens += 1,
')' => closed_parens += 1, ')' => closed_parens += 1,
_ => {} _ => {}
}); });
if open_parens > closed_parens { if open_parens > closed_parens {
return &HINT_CLOSED_PARENS; return &HINT_CLOSED_PARENS;
} }
&Hint::None &Hint::None
} }
} }
pub fn get_last_term(chars: &[char]) -> Option<String> { pub fn get_last_term(chars: &[char]) -> Option<String> {
if chars.is_empty() { if chars.is_empty() {
return None; return None;
} }
let mut result = split_function_chars(chars, SplitType::Term); let mut result = split_function_chars(chars, SplitType::Term);
result.pop() result.pop()
} }
#[derive(PartialEq, Clone, Copy)] #[derive(PartialEq, Clone, Copy)]
pub enum Hint<'a> { pub enum Hint<'a> {
Single(&'a str), Single(&'a str),
Many(&'a [&'a str]), Many(&'a [&'a str]),
None, None,
} }
impl<'a> std::fmt::Display for Hint<'a> { impl<'a> std::fmt::Display for Hint<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Hint::Single(single_data) => { Hint::Single(single_data) => {
write!(f, "{}", single_data) write!(f, "{}", single_data)
} }
Hint::Many(multi_data) => { Hint::Many(multi_data) => {
write!(f, "{:?}", multi_data) write!(f, "{:?}", multi_data)
} }
Hint::None => { Hint::None => {
write!(f, "None") write!(f, "None")
} }
} }
} }
} }
impl<'a> std::fmt::Debug for Hint<'a> { impl<'a> std::fmt::Debug for Hint<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Display::fmt(self, f) std::fmt::Display::fmt(self, f)
} }
} }
impl<'a> Hint<'a> { impl<'a> Hint<'a> {
#[inline] #[inline]
pub const fn is_none(&self) -> bool { matches!(&self, &Hint::None) } pub const fn is_none(&self) -> bool {
matches!(&self, &Hint::None)
}
#[inline] #[inline]
#[allow(dead_code)] #[allow(dead_code)]
pub const fn is_some(&self) -> bool { !self.is_none() } pub const fn is_some(&self) -> bool {
!self.is_none()
}
#[inline] #[inline]
#[allow(dead_code)] #[allow(dead_code)]
pub const fn is_single(&self) -> bool { matches!(&self, &Hint::Single(_)) } pub const fn is_single(&self) -> bool {
matches!(&self, &Hint::Single(_))
}
#[inline] #[inline]
#[allow(dead_code)] #[allow(dead_code)]
pub const fn single(&self) -> Option<&str> { pub const fn single(&self) -> Option<&str> {
match self { match self {
Hint::Single(data) => Some(data), Hint::Single(data) => Some(data),
_ => None, _ => None,
} }
} }
#[inline] #[inline]
#[allow(dead_code)] #[allow(dead_code)]
pub const fn many(&self) -> Option<&[&str]> { pub const fn many(&self) -> Option<&[&str]> {
match self { match self {
Hint::Many(data) => Some(data), Hint::Many(data) => Some(data),
_ => None, _ => None,
} }
} }
} }
include!(concat!(env!("OUT_DIR"), "/codegen.rs")); include!(concat!(env!("OUT_DIR"), "/codegen.rs"));

View File

@@ -1,30 +0,0 @@
{
pkgs ? import <nixpkgs> { },
}:
pkgs.mkShell rec {
libs = with pkgs; [
# wayland
libxkbcommon
libGL
wayland
libx11
libxcursor
libxi
clang
];
nativeBuildInputs =
with pkgs;
[
rustc
cargo
rust-analyzer
python3Packages.fonttools
]
++ libs;
# add libs to path
LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath libs}";
}

View File

@@ -1,18 +1,4 @@
use const_format::formatc;
use epaint::Color32; use epaint::Color32;
use shadow_rs::shadow;
shadow!(build);
/// Constant string that has a string containing information about the build.
pub const BUILD_INFO: &str = formatc!(
"Commit: {} ({})\nBuild Date: {}\nPackage Version: {}\nRust Channel: {}\nRust Version: {}",
&build::SHORT_COMMIT,
&build::BRANCH,
&build::BUILD_TIME,
&build::PKG_VERSION,
&build::RUST_CHANNEL,
&build::RUST_VERSION,
);
pub const FONT_SIZE: f32 = 14.0; pub const FONT_SIZE: f32 = 14.0;

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,27 +254,33 @@ 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(
.into_iter() threshold,
.map(|x| PlotPoint::new(x, self.function.get(0, x))) 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() .collect()
} }
@@ -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() {
plot_ui.points( for point in &self.root_data {
self.root_data let name = format!(
.clone() "({}, {})",
.to_points() try_symbolic(point.x)
.color(Color32::LIGHT_BLUE) .map(|s| s.to_string())
.radius(5.0), // Radius of points of Roots .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::LIGHT_BLUE)
.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
@@ -449,7 +510,7 @@ impl FunctionEntry {
Some(integral_data) => { Some(integral_data) => {
if integral_step > step { if integral_step > step {
plot_ui.bar_chart( plot_ui.bar_chart(
BarChart::new(integral_data.0.clone()) BarChart::new("integral bar chart", integral_data.0.clone())
.color(Color32::BLUE) .color(Color32::BLUE)
.width(integral_step), .width(integral_step),
); );

View File

@@ -1,63 +1,38 @@
use crate::{ use crate::{function_entry::FunctionEntry, widgets::widgets_ontop};
consts::COLORS, function_entry::FunctionEntry, misc::random_u64, widgets::widgets_ontop, use egui::{Button, Id, Key, Modifiers, Popup, PopupCloseBehavior, TextEdit, WidgetText};
};
use egui::{Button, Id, Key, Modifiers, 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)>;
#[derive(Serialize, Deserialize)]
pub struct FunctionManager { pub struct FunctionManager {
functions: Functions, functions: Functions,
} }
impl Default for FunctionManager { impl Default for FunctionManager {
fn default() -> Self { fn default() -> Self {
let mut vec: Functions = Vec::with_capacity(COLORS.len()); let mut d = Self::new();
vec.push(( d.push_empty();
Id::new(11414819524356497634_u64), // Random number here to avoid call to crate::misc::random_u64() d
FunctionEntry::default(),
));
Self { functions: vec }
} }
} }
impl Serialize for FunctionManager { #[test]
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn func_manager_roundtrip_serdes() {
where let mut func_manager = FunctionManager {
S: Serializer, functions: Vec::new(),
{ };
let mut s = serializer.serialize_struct("FunctionManager", 1)?;
s.serialize_field(
"data",
&self
.functions
.iter()
.map(|(id, func)| (*id, func.clone()))
.collect::<Vec<(Id, FunctionEntry)>>(),
)?;
s.end()
}
}
impl<'de> Deserialize<'de> for FunctionManager { func_manager.push_empty();
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> let ser = bincode::serialize(&func_manager).expect("unable to serialize");
where let des: FunctionManager = bincode::deserialize(&ser).expect("unable to deserialize");
D: Deserializer<'de>, assert_eq!(
{ func_manager.functions[0].1.func_str(),
#[derive(Deserialize)] des.functions[0].1.func_str()
struct Helper(Vec<(Id, FunctionEntry)>); );
let helper = Helper::deserialize(deserializer)?;
Ok(FunctionManager {
functions: helper.0.to_vec(),
})
}
} }
/// Function that creates button that's used with the `button_area` /// Function that creates button that's used with the `button_area`
@@ -66,17 +41,15 @@ fn button_area_button<'a>(text: impl Into<WidgetText>) -> Button<'a> {
} }
impl FunctionManager { impl FunctionManager {
#[inline] pub fn new() -> Self {
fn get_hash(&self) -> u64 { Self {
let mut hasher = DefaultHasher::new(); functions: Vec::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();
@@ -84,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();
@@ -108,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() {
@@ -137,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()
{ {
@@ -147,35 +132,45 @@ impl FunctionManager {
let autocomplete_popup_id = Id::new("autocomplete popup"); let autocomplete_popup_id = Id::new("autocomplete popup");
egui::popup_below_widget(ui, autocomplete_popup_id, &re, |ui| { Popup::from_response(&re)
hints.iter().enumerate().for_each(|(i, candidate)| { .id(autocomplete_popup_id)
if ui .close_behavior(PopupCloseBehavior::CloseOnClickOutside)
.selectable_label(i == function.autocomplete.i, *candidate) .show(|ui| {
.clicked() hints.iter().enumerate().for_each(|(i, candidate)| {
{ if ui
clicked = true; .selectable_label(i == function.autocomplete.i, *candidate)
function.autocomplete.i = i; .clicked()
} {
clicked = true;
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);
} }
} }
// Push cursor to end if needed // Push cursor to end if needed
if movement == Movement::Complete { if movement == Movement::Complete {
// TODO! proper error handling
let mut state = let mut state =
unsafe { TextEdit::load_state(ui.ctx(), te_id).unwrap_unchecked() }; unsafe { TextEdit::load_state(ui.ctx(), te_id).unwrap_unchecked() };
let ccursor = egui::text::CCursor::new(function.autocomplete.string.len()); let ccursor = egui::text::CCursor::new(function.autocomplete.string.len());
state.set_ccursor_range(Some(egui::text::CCursorRange::one(ccursor))); state
.cursor
.set_char_range(Some(egui::text::CCursorRange::one(ccursor)));
TextEdit::store_state(ui.ctx(), te_id, state); TextEdit::store_state(ui.ctx(), te_id, state);
} }
} }
@@ -236,17 +231,16 @@ 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
pub fn push_empty(&mut self) { pub fn push_empty(&mut self) {
self.functions.push(( self.functions.push((
Id::new(random_u64().expect("unable to generate random id")), Id::new(format!("function #{}", self.functions.len() + 1)),
FunctionEntry::default(), FunctionEntry::default(),
)); ));
} }

View File

@@ -6,69 +6,82 @@ mod function_entry;
mod function_manager; mod function_manager;
mod math_app; mod math_app;
mod misc; mod misc;
pub mod symbolic;
mod unicode_helper; mod unicode_helper;
mod widgets; mod widgets;
pub use crate::{ pub use crate::{
function_entry::{FunctionEntry, Riemann}, function_entry::{FunctionEntry, Riemann},
math_app::AppSettings, math_app::{AppSettings, MathApp},
misc::{ misc::{EguiHelper, newtons_method, option_vec_printer, step_helper},
EguiHelper, HashBytes, hashed_storage_create, hashed_storage_read, 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")]
use wasm_bindgen::prelude::*; 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]
#[global_allocator] static ALLOCATOR: LockedAllocator<FreeListAllocator> =
static ALLOCATOR: LockedAllocator<FreeListAllocator> = LockedAllocator::new(FreeListAllocator::new()); LockedAllocator::new(FreeListAllocator::new());
use eframe::WebRunner; #[derive(Clone)]
// use tracing::metadata::LevelFilter; #[wasm_bindgen]
#[derive(Clone)] pub struct WebHandle {
#[wasm_bindgen] runner: WebRunner,
pub struct WebHandle { }
runner: WebRunner,
}
#[wasm_bindgen] #[wasm_bindgen]
impl WebHandle { impl WebHandle {
/// Installs a panic hook, then returns. /// Installs a panic hook, then returns.
#[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 {
runner: WebRunner::new(),
Self {
runner: WebRunner::new(),
}
}
/// Call this once from JavaScript to start your app.
#[wasm_bindgen]
pub async fn start(&self, canvas_id: &str) -> Result<(), wasm_bindgen::JsValue> {
self.runner
.start(
canvas_id,
eframe::WebOptions::default(),
Box::new(|cc| Box::new(math_app::MathApp::new(cc))),
)
.await
}
} }
}
#[wasm_bindgen(start)] /// Call this once from JavaScript to start your app.
pub async fn start() { #[wasm_bindgen]
tracing::info!("Starting..."); pub async fn start(
&self,
canvas_id: HtmlCanvasElement,
let web_handle = WebHandle::new(); ) -> Result<(), wasm_bindgen::JsValue> {
web_handle.start("canvas").await.unwrap() 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

@@ -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| Box::new(math_app::MathApp::new(cc))), Box::new(|cc| Ok(Box::new(ytbn_graphing_software::MathApp::new(cc)))),
) )
} }

View File

@@ -1,21 +1,21 @@
use crate::{ use crate::{
consts::{BUILD_INFO, COLORS, DEFAULT_INTEGRAL_NUM, DEFAULT_MAX_X, DEFAULT_MIN_X, build}, consts::{COLORS, DEFAULT_INTEGRAL_NUM, DEFAULT_MAX_X, DEFAULT_MIN_X},
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::{
Button, CentralPanel, Color32, ComboBox, Context, DragValue, Frame, Key, Layout, SidePanel, Button, CentralPanel, Color32, ComboBox, Context, CornerRadius, DragValue, Frame, Key, Layout,
TopBottomPanel, Ui, Vec2, Window, style::Margin, SidePanel, TopBottomPanel, Ui, Vec2, Window,
}; };
use egui_plot::Plot; use egui_plot::Plot;
use emath::{Align, Align2}; use emath::{Align, Align2};
use epaint::Rounding; use epaint::Margin;
use instant::Instant;
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)]
@@ -74,9 +74,6 @@ struct Opened {
/// Help window /// Help window
pub help: bool, pub help: bool,
/// Info window
pub info: bool,
/// Sidepanel /// Sidepanel
pub side_panel: bool, pub side_panel: bool,
@@ -88,7 +85,6 @@ impl Default for Opened {
fn default() -> Opened { fn default() -> Opened {
Self { Self {
help: false, help: false,
info: false,
side_panel: true, side_panel: true,
welcome: true, welcome: true,
} }
@@ -100,8 +96,8 @@ pub struct MathApp {
/// Stores vector of functions /// Stores vector of functions
functions: FunctionManager, functions: FunctionManager,
/// Contains the list of Areas calculated (the vector of f64) and time it took for the last frame (the Duration). Stored in a Tuple. /// Contains the list of Areas calculated
last_info: (Option<String>, Option<String>), areas: Option<String>,
/// Stores opened windows/elements for later reference /// Stores opened windows/elements for later reference
opened: Opened, opened: Opened,
@@ -128,109 +124,50 @@ 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 {
#[cfg(threading)]
tracing::info!("Threading: Enabled");
#[cfg(not(threading))]
tracing::info!("Threading: Disabled");
tracing::info!("commit: {}", build::SHORT_COMMIT);
tracing::info!("Initializing..."); tracing::info!("Initializing...");
let start = Instant::now();
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 (commit, cached_data) = crate::misc::hashed_storage_read(&data)?;
if commit == unsafe { std::mem::transmute::<&str, crate::misc::HashBytes>(build::SHORT_COMMIT) } {
tracing::info!("Reading decompression cache. Bytes: {}", cached_data.len());
return Some(cached_data);
} else {
None
}
}
fn load_functions() -> Option<FunctionManager> {
let data = get_localstorage().get_item(FUNC_NAME).ok()??;
let (commit, func_data) = crate::misc::hashed_storage_read(&data)?;
if commit == unsafe { std::mem::transmute::<&str, &[u8]>(build::SHORT_COMMIT) } {
tracing::info!("Reading previous function data");
let function_manager: FunctionManager = bincode::deserialize(&func_data).ok()?;
return Some(function_manager);
} else {
None
}
}
}
}
fn decompress_fonts() -> epaint::text::FontDefinitions {
let mut data = Vec::new();
let _ =
ruzstd::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 commit: crate::misc::HashBytes = const {
unsafe {
std::mem::transmute::<&str, crate::misc::HashBytes>(build::SHORT_COMMIT)
}
};
let saved_data = commit.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"))] tracing::info!("Initialized!");
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! Took: {:?}", start.elapsed());
Self { Self {
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
@@ -239,7 +176,7 @@ impl MathApp {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
functions: FunctionManager::default(), functions: FunctionManager::default(),
last_info: (None, None), areas: None,
opened: Opened::default(), opened: Opened::default(),
settings: AppSettings::default(), settings: AppSettings::default(),
} }
@@ -339,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(),
); );
}); });
@@ -362,11 +297,8 @@ impl MathApp {
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
{ {
tracing::info!("Saving function data"); tracing::info!("Saving function data");
use crate::misc::{HashBytes, hashed_storage_create};
let hash: HashBytes = let saved_data = crate::misc::hashed_storage_create(
unsafe { std::mem::transmute::<&str, HashBytes>(build::SHORT_COMMIT) };
let saved_data = hashed_storage_create(
hash,
&bincode::serialize(&self.functions) &bincode::serialize(&self.functions)
.expect("unable to deserialize functions"), .expect("unable to deserialize functions"),
); );
@@ -396,35 +328,24 @@ impl MathApp {
impl App for MathApp { impl App for MathApp {
/// Called each time the UI needs repainting. /// Called each time the UI needs repainting.
fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) { fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
// start timer
let start = if self.opened.info {
Some(instant::Instant::now())
} else {
// if disabled, clear the stored formatted time
self.last_info.1 = None;
None
};
// 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
@@ -440,27 +361,16 @@ 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(),
);
// Toggles opening the Info window
self.opened.info.bitxor_assign(
ui.add(Button::new("Info"))
.on_hover_text(match self.opened.info {
true => "Close Info Window",
false => "Open Info Window",
})
.clicked(),
); );
// Display Area and time of last frame // Display Area and time of last frame
if let Some(ref area) = self.last_info.0 { if let Some(ref area) = self.areas {
ui.label(area); ui.label(area);
} }
}); });
@@ -513,20 +423,6 @@ impl App for MathApp {
} }
} }
// Window with information about the build and current commit
Window::new("Info")
.open(&mut self.opened.info)
.default_pos([200.0, 200.0])
.resizable(false)
.collapsible(false)
.show(ctx, |ui| {
ui.add(egui::Label::new(BUILD_INFO));
if let Some(ref took) = self.last_info.1 {
ui.label(took);
}
});
// If side panel is enabled, show it. // If side panel is enabled, show it.
if self.opened.side_panel { if self.opened.side_panel {
self.side_panel(ctx); self.side_panel(ctx);
@@ -535,11 +431,11 @@ impl App for MathApp {
// Central panel which contains the central plot (or an error created when parsing) // Central panel which contains the central plot (or an error created when parsing)
CentralPanel::default() CentralPanel::default()
.frame(Frame { .frame(Frame {
inner_margin: Margin::symmetric(0.0, 0.0), inner_margin: Margin::ZERO,
rounding: Rounding::ZERO, corner_radius: CornerRadius::ZERO,
// fill: crate::style::STYLE.window_fill(), // fill: crate::style::STYLE.window_fill(),
fill: Color32::from_gray(27), fill: Color32::from_gray(27),
..Frame::none() ..Frame::NONE
}) })
.show(ctx, |ui| { .show(ctx, |ui| {
// Display an error if it exists // Display an error if it exists
@@ -549,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() {
@@ -609,15 +500,12 @@ impl App for MathApp {
}) })
.collect(); .collect();
self.last_info.0 = if area.iter().any(|e| e.is_some()) { self.areas = if area.iter().any(|e| e.is_some()) {
Some(format!("Area: {}", option_vec_printer(area.as_slice()))) Some(format!("Area: {}", option_vec_printer(area.as_slice())))
} else { } else {
None None
}; };
}); });
}); });
// Calculate and store the last time it took to draw the frame
self.last_info.1 = start.map(|a| format!("Took: {}ms", a.elapsed().as_micros()));
} }
} }

View File

@@ -1,20 +1,20 @@
use base64::{Engine as _, engine::general_purpose}; use base64::Engine;
use base64::engine::general_purpose;
use egui_plot::{Line, PlotPoint, PlotPoints, Points}; use egui_plot::{Line, PlotPoint, PlotPoints, Points};
use emath::Pos2; use emath::Pos2;
use getrandom::getrandom;
use itertools::Itertools; use itertools::Itertools;
use parsing::FlatExWrapper; use parsing::FlatExWrapper;
/// Implements traits that are useful when dealing with Vectors of egui's `Value` /// Implements traits that are useful when dealing with Vectors of egui's `Value`
pub trait EguiHelper { pub trait EguiHelper {
/// Converts to `egui::plot::Values` /// Converts to `egui::plot::Values`
fn to_values(self) -> PlotPoints; fn to_values(self) -> PlotPoints<'static>;
/// Converts to `egui::plot::Line` /// Converts to `egui::plot::Line`
fn to_line(self) -> Line; fn to_line(self) -> Line<'static>;
/// Converts to `egui::plot::Points` /// Converts to `egui::plot::Points`
fn to_points(self) -> Points; fn to_points(self) -> Points<'static>;
/// Converts Vector of Values into vector of tuples /// Converts Vector of Values into vector of tuples
fn to_tuple(self) -> Vec<(f64, f64)>; fn to_tuple(self) -> Vec<(f64, f64)>;
@@ -22,18 +22,18 @@ pub trait EguiHelper {
impl EguiHelper for Vec<PlotPoint> { impl EguiHelper for Vec<PlotPoint> {
#[inline(always)] #[inline(always)]
fn to_values(self) -> PlotPoints { fn to_values(self) -> PlotPoints<'static> {
PlotPoints::from(unsafe { std::mem::transmute::<Vec<PlotPoint>, Vec<[f64; 2]>>(self) }) PlotPoints::from(unsafe { std::mem::transmute::<Vec<PlotPoint>, Vec<[f64; 2]>>(self) })
} }
#[inline(always)] #[inline(always)]
fn to_line(self) -> Line { fn to_line(self) -> Line<'static> {
Line::new(self.to_values()) Line::new("", self.to_values())
} }
#[inline(always)] #[inline(always)]
fn to_points(self) -> Points { fn to_points(self) -> Points<'static> {
Points::new(self.to_values()) Points::new("", self.to_values())
} }
#[inline(always)] #[inline(always)]
@@ -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()
} }
@@ -151,58 +149,14 @@ pub fn step_helper(max_i: usize, min_x: f64, step: f64) -> Vec<f64> {
.collect() .collect()
} }
// TODO: use in hovering over points
/// Attempts to see what variable `x` is almost
#[allow(dead_code)] #[allow(dead_code)]
pub fn almost_variable(x: f64) -> Option<char> { pub fn hashed_storage_create(data: &[u8]) -> String {
const EPSILON: f32 = f32::EPSILON * 2.0; general_purpose::STANDARD.encode(data)
if emath::almost_equal(x as f32, std::f32::consts::E, EPSILON) {
Some('e')
} else if emath::almost_equal(x as f32, std::f32::consts::PI, EPSILON) {
Some('π')
} else {
None
}
}
pub const HASH_LENGTH: usize = 8;
/// Represents bytes used to represent hash info
pub type HashBytes = [u8; HASH_LENGTH];
#[allow(dead_code)]
pub fn hashed_storage_create(hashbytes: HashBytes, data: &[u8]) -> String {
let combined_data = [hashbytes.to_vec(), data.to_vec()].concat();
general_purpose::STANDARD.encode(combined_data)
} }
#[allow(dead_code)] #[allow(dead_code)]
pub fn hashed_storage_read(data: &str) -> Option<(HashBytes, Vec<u8>)> { pub fn hashed_storage_read(data: &str) -> Option<Vec<u8>> {
// Decode base64 data general_purpose::STANDARD.decode(data).ok()
let decoded_bytes = general_purpose::STANDARD.decode(data).ok()?;
// Make sure data is long enough to decode
if HASH_LENGTH > decoded_bytes.len() {
return None;
}
// Split hash and data
let (hash_bytes, data_bytes) = decoded_bytes.split_at(HASH_LENGTH);
// Convert hash bytes to HashBytes
let hash: HashBytes = hash_bytes.try_into().ok()?;
Some((hash, data_bytes.to_vec()))
}
/// Creates and returns random u64
pub fn random_u64() -> Result<u64, getrandom::Error> {
// Buffer of 8 `u8`s that are later merged into one u64
let mut buf = [0u8; 8];
// Populate buffer with random values
getrandom(&mut buf)?;
// Merge buffer into u64
Ok(u64::from_be_bytes(buf))
} }
include!(concat!(env!("OUT_DIR"), "/valid_chars.rs")); include!(concat!(env!("OUT_DIR"), "/valid_chars.rs"));

210
src/symbolic.rs Normal file
View File

@@ -0,0 +1,210 @@
use std::fmt;
/// Maximum denominator to consider when checking for rational approximations.
const MAX_DENOMINATOR: i64 = 12;
/// Maximum coefficient to consider for multiples of special constants.
const MAX_COEFFICIENT: i64 = 12;
/// Represents a symbolic mathematical value.
#[derive(Debug, Clone, PartialEq)]
pub struct SymbolicValue {
/// The original numeric value
value: f64,
/// The symbolic representation
repr: SymbolicRepr,
}
/// The type of symbolic representation.
#[derive(Debug, Clone, PartialEq)]
enum SymbolicRepr {
/// An integer value
Integer(i64),
/// A simple fraction: numerator / denominator
Fraction { numerator: i64, denominator: i64 },
/// A multiple of a constant: (numerator / denominator) * constant
ConstantMultiple {
numerator: i64,
denominator: i64,
constant: Constant,
},
}
/// Known mathematical constants.
#[derive(Debug, Clone, Copy, PartialEq)]
enum Constant {
Pi,
E,
Sqrt(i64),
}
impl Constant {
fn value(self) -> f64 {
match self {
Constant::Pi => std::f64::consts::PI,
Constant::E => std::f64::consts::E,
Constant::Sqrt(n) => (n as f64).sqrt(),
}
}
fn name(self) -> String {
match self {
Constant::Pi => "pi".to_string(),
Constant::E => "e".to_string(),
Constant::Sqrt(n) => format!("sqrt({})", n),
}
}
}
/// All constants to try, in order of priority.
const CONSTANTS: &[Constant] = &[
Constant::Pi,
Constant::E,
Constant::Sqrt(2),
Constant::Sqrt(3),
Constant::Sqrt(5),
Constant::Sqrt(6),
Constant::Sqrt(7),
];
impl SymbolicValue {
/// Returns the original numeric value.
pub fn numeric_value(&self) -> f64 {
self.value
}
}
impl fmt::Display for SymbolicValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.repr {
SymbolicRepr::Integer(n) => write!(f, "{}", n),
SymbolicRepr::Fraction {
numerator,
denominator,
} => write!(f, "{}/{}", numerator, denominator),
SymbolicRepr::ConstantMultiple {
numerator,
denominator,
constant,
} => format_constant_multiple(f, *numerator, *denominator, &constant.name()),
}
}
}
/// Helper function to format a constant multiple like "2pi/3" or "-pi/2"
fn format_constant_multiple(
f: &mut fmt::Formatter<'_>,
numerator: i64,
denominator: i64,
constant: &str,
) -> fmt::Result {
let sign = if numerator < 0 { "-" } else { "" };
let abs_num = numerator.abs();
match (abs_num, denominator) {
(1, 1) => write!(f, "{}{}", sign, constant),
(_, 1) => write!(f, "{}{}{}", sign, abs_num, constant),
(1, _) => write!(f, "{}{}/{}", sign, constant, denominator),
(_, _) => write!(f, "{}{}{}/{}", sign, abs_num, constant, denominator),
}
}
/// Attempts to find a symbolic representation for the given numeric value.
///
/// Returns `Some(SymbolicValue)` if the value can be represented symbolically,
/// or `None` if no suitable symbolic representation is found.
///
/// # Examples
///
/// ```
/// use ytbn_graphing_software::symbolic::try_symbolic;
/// use std::f64::consts::PI;
///
/// let sym = try_symbolic(PI).unwrap();
/// assert_eq!(sym.to_string(), "pi");
///
/// let sym = try_symbolic(PI / 2.0).unwrap();
/// assert_eq!(sym.to_string(), "pi/2");
/// ```
pub fn try_symbolic(x: f64) -> Option<SymbolicValue> {
if !x.is_finite() {
return None;
}
// Check for zero
if x.abs() < f64::EPSILON {
return Some(SymbolicValue {
value: x,
repr: SymbolicRepr::Integer(0),
});
}
// Try each constant in order of preference
for &constant in CONSTANTS {
if let Some(repr) = try_constant_multiple(x, constant) {
return Some(SymbolicValue { value: x, repr });
}
}
// Fall back to rational approximation
try_rational(x).map(|repr| SymbolicValue { value: x, repr })
}
/// Try to represent x as (numerator/denominator) * constant
fn try_constant_multiple(x: f64, constant: Constant) -> Option<SymbolicRepr> {
let c = constant.value();
for denom in 1..=MAX_DENOMINATOR {
let num_f = x * (denom as f64) / c;
let num = num_f.round() as i64;
// Skip if coefficient is zero or too large
if num == 0 || num.abs() > MAX_COEFFICIENT * denom {
continue;
}
let expected = (num as f64) * c / (denom as f64);
if (x - expected).abs() < f64::EPSILON {
let g = gcd(num.abs(), denom);
return Some(SymbolicRepr::ConstantMultiple {
numerator: num / g,
denominator: denom / g,
constant,
});
}
}
None
}
/// Try to represent x as a simple fraction: numerator/denominator
fn try_rational(x: f64) -> Option<SymbolicRepr> {
for denom in 1..=MAX_DENOMINATOR {
let num_f = x * (denom as f64);
let num = num_f.round() as i64;
if (x - (num as f64) / (denom as f64)).abs() < f64::EPSILON {
let g = gcd(num.abs(), denom);
let (num, denom) = (num / g, denom / g);
return Some(if denom == 1 {
SymbolicRepr::Integer(num)
} else {
SymbolicRepr::Fraction {
numerator: num,
denominator: denom,
}
});
}
}
None
}
/// Compute the greatest common divisor using Euclidean algorithm.
fn gcd(mut a: i64, mut b: i64) -> i64 {
while b != 0 {
(a, b) = (b, a % b);
}
a
}

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

@@ -84,7 +84,7 @@ fn do_test(sum: Riemann, area_target: f64) {
if !emath::almost_equal(a[i].0 as f32, DERIVATIVE_TARGET[i].0 as f32, f32::EPSILON) if !emath::almost_equal(a[i].0 as f32, DERIVATIVE_TARGET[i].0 as f32, f32::EPSILON)
| !emath::almost_equal(a[i].1 as f32, DERIVATIVE_TARGET[i].1 as f32, f32::EPSILON) | !emath::almost_equal(a[i].1 as f32, DERIVATIVE_TARGET[i].1 as f32, f32::EPSILON)
{ {
panic!("Expected: {:?}\nGot: {:?}", a, DERIVATIVE_TARGET); panic!("Expected: {:?}\nGot: {:?}", DERIVATIVE_TARGET, a);
} }
} }
@@ -98,7 +98,7 @@ fn do_test(sum: Riemann, area_target: f64) {
if !emath::almost_equal(a_1[i].0 as f32, BACK_TARGET[i].0 as f32, f32::EPSILON) if !emath::almost_equal(a_1[i].0 as f32, BACK_TARGET[i].0 as f32, f32::EPSILON)
| !emath::almost_equal(a_1[i].1 as f32, BACK_TARGET[i].1 as f32, f32::EPSILON) | !emath::almost_equal(a_1[i].1 as f32, BACK_TARGET[i].1 as f32, f32::EPSILON)
{ {
panic!("Expected: {:?}\nGot: {:?}", a_1, BACK_TARGET); panic!("Expected: {:?}\nGot: {:?}", BACK_TARGET, a_1);
} }
} }
} }
@@ -131,7 +131,7 @@ fn do_test(sum: Riemann, area_target: f64) {
if !emath::almost_equal(a[i].0 as f32, b[i].0 as f32, f32::EPSILON) if !emath::almost_equal(a[i].0 as f32, b[i].0 as f32, f32::EPSILON)
| !emath::almost_equal(a[i].1 as f32, b[i].1 as f32, f32::EPSILON) | !emath::almost_equal(a[i].1 as f32, b[i].1 as f32, f32::EPSILON)
{ {
panic!("Expected: {:?}\nGot: {:?}", a, b); panic!("Expected: {:?}\nGot: {:?}", b, a);
} }
} }
@@ -160,7 +160,7 @@ fn do_test(sum: Riemann, area_target: f64) {
if !emath::almost_equal(a_1[i].0 as f32, b_1[i].0 as f32, f32::EPSILON) if !emath::almost_equal(a_1[i].0 as f32, b_1[i].0 as f32, f32::EPSILON)
| !emath::almost_equal(a_1[i].1 as f32, b_1[i].1 as f32, f32::EPSILON) | !emath::almost_equal(a_1[i].1 as f32, b_1[i].1 as f32, f32::EPSILON)
{ {
panic!("Expected: {:?}\nGot: {:?}", a_1, b_1); panic!("Expected: {:?}\nGot: {:?}", b_1, a_1);
} }
} }
} }
@@ -193,7 +193,7 @@ fn do_test(sum: Riemann, area_target: f64) {
if !emath::almost_equal(a[i].0 as f32, b[i].0 as f32, f32::EPSILON) if !emath::almost_equal(a[i].0 as f32, b[i].0 as f32, f32::EPSILON)
| !emath::almost_equal(a[i].1 as f32, b[i].1 as f32, f32::EPSILON) | !emath::almost_equal(a[i].1 as f32, b[i].1 as f32, f32::EPSILON)
{ {
panic!("Expected: {:?}\nGot: {:?}", a, b); panic!("Expected: {:?}\nGot: {:?}", b, a);
} }
} }
@@ -222,7 +222,7 @@ fn do_test(sum: Riemann, area_target: f64) {
if !emath::almost_equal(a_1[i].0 as f32, b_1[i].0 as f32, f32::EPSILON) if !emath::almost_equal(a_1[i].0 as f32, b_1[i].0 as f32, f32::EPSILON)
| !emath::almost_equal(a_1[i].1 as f32, b_1[i].1 as f32, f32::EPSILON) | !emath::almost_equal(a_1[i].1 as f32, b_1[i].1 as f32, f32::EPSILON)
{ {
panic!("Expected: {:?}\nGot: {:?}", a_1, b_1); panic!("Expected: {:?}\nGot: {:?}", b_1, a_1);
} }
} }
} }
@@ -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;
@@ -271,3 +271,260 @@ fn middle_function() {
fn right_function() { fn right_function() {
do_test(Riemann::Right, 0.8800000000000001); do_test(Riemann::Right, 0.8800000000000001);
} }
#[test]
fn test_extrema() {
let mut settings = app_settings_constructor(Riemann::Middle, -2.0, 2.0, 100, 100, -2.0, 2.0);
settings.do_extrema = true;
let mut function = FunctionEntry::default();
function.update_string("x^2 - 4"); // Parabola with vertex at (0, -4)
function.integral = false;
function.derivative = false;
function.calculate(true, true, false, settings);
// For f(x) = x^2 - 4, f'(x) = 2x
// Extrema occurs where f'(x) = 0, so at x = 0
assert!(!function.extrema_data.is_empty());
// Should have exactly one extremum at x = 0
assert_eq!(function.extrema_data.len(), 1);
let extremum = function.extrema_data[0];
assert!(emath::almost_equal(extremum.x as f32, 0.0, f32::EPSILON));
assert!(emath::almost_equal(extremum.y as f32, -4.0, f32::EPSILON));
}
#[test]
fn test_extrema_multiple() {
let mut settings = app_settings_constructor(Riemann::Middle, -3.0, 3.0, 200, 200, -3.0, 3.0);
settings.do_extrema = true;
let mut function = FunctionEntry::default();
function.update_string("x^3 - 3*x"); // Cubic with local max and min
function.integral = false;
function.derivative = false;
function.calculate(true, true, false, settings);
// For f(x) = x^3 - 3x, f'(x) = 3x^2 - 3
// Extrema occur where f'(x) = 0, so at x = ±1
assert!(!function.extrema_data.is_empty());
// Should have exactly two extrema
assert_eq!(function.extrema_data.len(), 2);
// Sort by x coordinate for consistent testing
let mut extrema = function.extrema_data.clone();
extrema.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap());
// First extremum at x = -1, f(-1) = -1 + 3 = 2
assert!(emath::almost_equal(extrema[0].x as f32, -1.0, 0.01));
assert!(emath::almost_equal(extrema[0].y as f32, 2.0, 0.01));
// Second extremum at x = 1, f(1) = 1 - 3 = -2
assert!(emath::almost_equal(extrema[1].x as f32, 1.0, 0.01));
assert!(emath::almost_equal(extrema[1].y as f32, -2.0, 0.01));
}
#[test]
fn test_extrema_disabled() {
let mut settings = app_settings_constructor(Riemann::Middle, -2.0, 2.0, 100, 100, -2.0, 2.0);
settings.do_extrema = false; // Disable extrema
let mut function = FunctionEntry::default();
function.update_string("x^2 - 4");
function.integral = false;
function.derivative = false;
function.calculate(true, true, false, settings);
// Extrema data should be empty when disabled
assert!(function.extrema_data.is_empty());
}
#[test]
fn test_roots() {
let mut settings = app_settings_constructor(Riemann::Middle, -3.0, 3.0, 200, 200, -3.0, 3.0);
settings.do_roots = true;
let mut function = FunctionEntry::default();
function.update_string("x^2 - 4"); // Parabola crossing x-axis at ±2
function.integral = false;
function.derivative = false;
function.calculate(true, true, false, settings);
// For f(x) = x^2 - 4, roots occur where x^2 = 4, so at x = ±2
assert!(!function.root_data.is_empty());
// Should have exactly two roots
assert_eq!(function.root_data.len(), 2);
// Sort by x coordinate for consistent testing
let mut roots = function.root_data.clone();
roots.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap());
// First root at x = -2
assert!(emath::almost_equal(roots[0].x as f32, -2.0, 0.01));
assert!(emath::almost_equal(roots[0].y as f32, 0.0, 0.001));
// Second root at x = 2
assert!(emath::almost_equal(roots[1].x as f32, 2.0, 0.01));
assert!(emath::almost_equal(roots[1].y as f32, 0.0, 0.001));
}
#[test]
fn test_roots_single() {
let mut settings = app_settings_constructor(Riemann::Middle, -2.0, 2.0, 100, 100, -2.0, 2.0);
settings.do_roots = true;
let mut function = FunctionEntry::default();
function.update_string("x - 1"); // Linear function crossing x-axis at x = 1
function.integral = false;
function.derivative = false;
function.calculate(true, true, false, settings);
// For f(x) = x - 1, root occurs at x = 1
assert!(!function.root_data.is_empty());
// Should have exactly one root
assert_eq!(function.root_data.len(), 1);
let root = function.root_data[0];
assert!(emath::almost_equal(root.x as f32, 1.0, 0.01));
assert!(emath::almost_equal(root.y as f32, 0.0, f32::EPSILON));
}
#[test]
fn test_roots_disabled() {
let mut settings = app_settings_constructor(Riemann::Middle, -3.0, 3.0, 200, 200, -3.0, 3.0);
settings.do_roots = false; // Disable roots
let mut function = FunctionEntry::default();
function.update_string("x^2 - 4");
function.integral = false;
function.derivative = false;
function.calculate(true, true, false, settings);
// Root data should be empty when disabled
assert!(function.root_data.is_empty());
}
#[test]
fn test_extrema_and_roots_together() {
let mut settings = app_settings_constructor(Riemann::Middle, -3.0, 3.0, 200, 200, -3.0, 3.0);
settings.do_extrema = true;
settings.do_roots = true;
let mut function = FunctionEntry::default();
function.update_string("x^2 - 1"); // Parabola with vertex at (0, -1) and roots at ±1
function.integral = false;
function.derivative = false;
function.calculate(true, true, false, settings);
// Should have one extremum at x = 0
assert!(!function.extrema_data.is_empty());
assert_eq!(function.extrema_data.len(), 1);
let extremum = function.extrema_data[0];
assert!(emath::almost_equal(extremum.x as f32, 0.0, 0.01));
assert!(emath::almost_equal(extremum.y as f32, -1.0, 0.01));
// Should have two roots at x = ±1
assert!(!function.root_data.is_empty());
assert_eq!(function.root_data.len(), 2);
let mut roots = function.root_data.clone();
roots.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap());
assert!(emath::almost_equal(roots[0].x as f32, -1.0, 0.01));
assert!(emath::almost_equal(roots[1].x as f32, 1.0, 0.01));
}
#[test]
fn test_extrema_no_extrema() {
let mut settings = app_settings_constructor(Riemann::Middle, -2.0, 2.0, 100, 100, -2.0, 2.0);
settings.do_extrema = true;
let mut function = FunctionEntry::default();
function.update_string("x"); // Linear function has no extrema
function.integral = false;
function.derivative = false;
function.calculate(true, true, false, settings);
// Linear function should have no extrema
assert!(function.extrema_data.is_empty());
}
#[test]
fn test_roots_no_roots() {
let mut settings = app_settings_constructor(Riemann::Middle, -2.0, 2.0, 100, 100, -2.0, 2.0);
settings.do_roots = true;
let mut function = FunctionEntry::default();
function.update_string("x^2 + 1"); // Parabola that never crosses x-axis
function.integral = false;
function.derivative = false;
function.calculate(true, true, false, settings);
// Function that never crosses x-axis should have no roots
assert!(function.root_data.is_empty());
}
#[test]
fn test_extrema_and_roots_with_trig() {
let mut settings = app_settings_constructor(Riemann::Middle, -4.0, 4.0, 300, 300, -4.0, 4.0);
settings.do_extrema = true;
settings.do_roots = true;
let mut function = FunctionEntry::default();
function.update_string("sin(x)"); // Sine function has extrema at odd multiples of π/2
function.integral = false;
function.derivative = false;
function.calculate(true, true, false, settings);
// Sine function should have extrema in the given range
assert!(!function.extrema_data.is_empty());
// Should have multiple extrema (local max/min)
assert!(function.extrema_data.len() >= 2);
// Check that extrema are at approximately the right locations
// Local max at π/2 ≈ 1.57, local min at 3π/2 ≈ 4.71 (outside range)
// Local min at -π/2 ≈ -1.57, local max at -3π/2 ≈ -4.71 (outside range)
let extrema_x: Vec<f32> = function.extrema_data.iter().map(|p| p.x as f32).collect();
// Should have extrema near ±π/2
assert!(
extrema_x
.iter()
.any(|&x| emath::almost_equal(x, std::f32::consts::PI / 2.0, 0.1))
);
assert!(
extrema_x
.iter()
.any(|&x| emath::almost_equal(x, -std::f32::consts::PI / 2.0, 0.1))
);
let roots_x: Vec<f32> = function.root_data.iter().map(|p| p.x as f32).collect();
assert!(
roots_x
.iter()
.any(|&x| emath::almost_equal(x, std::f32::consts::PI, 0.1))
);
assert!(
roots_x
.iter()
.any(|&x| emath::almost_equal(x, -std::f32::consts::PI, 0.1))
);
assert!(roots_x.iter().any(|&x| emath::almost_equal(x, 0.0, 0.1)));
}

View File

@@ -60,34 +60,6 @@ fn option_vec_printer() {
} }
} }
#[test]
fn hashed_storage() {
use ytbn_graphing_software::{hashed_storage_create, hashed_storage_read};
let commit = "abcdefeg".chars().map(|c| c as u8).collect::<Vec<u8>>();
let data = "really cool data"
.chars()
.map(|c| c as u8)
.collect::<Vec<u8>>();
let storage_tmp: [u8; 8] = commit
.as_slice()
.try_into()
.expect("cannot turn into [u8; 8]");
let storage = hashed_storage_create(storage_tmp, data.as_slice());
let read = hashed_storage_read(&storage);
assert_eq!(
read.map(|(a, b)| (a.to_vec(), b.to_vec())),
Some((commit.to_vec(), data.to_vec()))
);
}
#[test]
fn invalid_hashed_storage() {
use ytbn_graphing_software::hashed_storage_read;
assert_eq!(hashed_storage_read("aaaa"), None);
}
// #[test] // #[test]
// fn to_values() { // fn to_values() {
// use egui::plot::{Value, Values}; // use egui::plot::{Value, Values};
@@ -152,7 +124,7 @@ fn newtons_method() {
let data = newtons_method( let data = newtons_method(
&get_flatexwrapper("x^2 -1"), &get_flatexwrapper("x^2 -1"),
&get_flatexwrapper("2x"), &get_flatexwrapper("2*x"),
3.0, 3.0,
&(0.0..5.0), &(0.0..5.0),
f64::EPSILON, f64::EPSILON,

229
tests/symbolic.rs Normal file
View File

@@ -0,0 +1,229 @@
use std::f64::consts::{E, PI, SQRT_2};
use ytbn_graphing_software::symbolic::try_symbolic;
#[test]
fn exact_pi() {
let result = try_symbolic(PI);
assert!(result.is_some());
let sym = result.unwrap();
assert_eq!(sym.to_string(), "pi");
}
#[test]
fn multiples_of_pi() {
// 2*pi
let result = try_symbolic(2.0 * PI);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "2pi");
// 3*pi
let result = try_symbolic(3.0 * PI);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "3pi");
// -pi
let result = try_symbolic(-PI);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "-pi");
// -2*pi
let result = try_symbolic(-2.0 * PI);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "-2pi");
}
#[test]
fn fractions_of_pi() {
// pi/2
let result = try_symbolic(PI / 2.0);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "pi/2");
// pi/3
let result = try_symbolic(PI / 3.0);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "pi/3");
// pi/4
let result = try_symbolic(PI / 4.0);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "pi/4");
// pi/6
let result = try_symbolic(PI / 6.0);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "pi/6");
// 2pi/3
let result = try_symbolic(2.0 * PI / 3.0);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "2pi/3");
// 3pi/4
let result = try_symbolic(3.0 * PI / 4.0);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "3pi/4");
// 5pi/6
let result = try_symbolic(5.0 * PI / 6.0);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "5pi/6");
// -pi/2
let result = try_symbolic(-PI / 2.0);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "-pi/2");
}
#[test]
fn exact_e() {
let result = try_symbolic(E);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "e");
}
#[test]
fn multiples_of_e() {
// 2e
let result = try_symbolic(2.0 * E);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "2e");
// -e
let result = try_symbolic(-E);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "-e");
}
#[test]
fn sqrt_2() {
let result = try_symbolic(SQRT_2);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "sqrt(2)");
// -sqrt(2)
let result = try_symbolic(-SQRT_2);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "-sqrt(2)");
// 2*sqrt(2)
let result = try_symbolic(2.0 * SQRT_2);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "2sqrt(2)");
}
#[test]
fn sqrt_3() {
let sqrt_3 = 3.0_f64.sqrt();
let result = try_symbolic(sqrt_3);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "sqrt(3)");
// sqrt(3)/2 - common in trigonometry
let result = try_symbolic(sqrt_3 / 2.0);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "sqrt(3)/2");
}
#[test]
fn simple_fractions() {
// 1/2
let result = try_symbolic(0.5);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "1/2");
// 1/3
let result = try_symbolic(1.0 / 3.0);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "1/3");
// 2/3
let result = try_symbolic(2.0 / 3.0);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "2/3");
// 1/4
let result = try_symbolic(0.25);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "1/4");
// 3/4
let result = try_symbolic(0.75);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "3/4");
// -1/2
let result = try_symbolic(-0.5);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "-1/2");
}
#[test]
fn integers() {
// 0
let result = try_symbolic(0.0);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "0");
// 1
let result = try_symbolic(1.0);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "1");
// -1
let result = try_symbolic(-1.0);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "-1");
// 5
let result = try_symbolic(5.0);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "5");
}
#[test]
fn non_symbolic_values() {
// Some arbitrary irrational number that isn't special
let result = try_symbolic(1.234567890123);
assert!(result.is_none());
// A number that's close to but not quite pi
let result = try_symbolic(3.15);
assert!(result.is_none());
}
#[test]
fn numeric_value() {
// SymbolicValue should provide the original numeric value
let sym = try_symbolic(PI).unwrap();
assert!((sym.numeric_value() - PI).abs() < 1e-10);
let sym = try_symbolic(PI / 2.0).unwrap();
assert!((sym.numeric_value() - PI / 2.0).abs() < 1e-10);
}
#[test]
fn zero() {
let result = try_symbolic(0.0);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "0");
// Also test -0.0
let result = try_symbolic(-0.0);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "0");
}
#[test]
fn special_trig_values() {
// Common values that appear in trigonometry
// sin(pi/4) = cos(pi/4) = sqrt(2)/2
let result = try_symbolic(SQRT_2 / 2.0);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "sqrt(2)/2");
// sin(pi/6) = cos(pi/3) = 1/2
let result = try_symbolic(0.5);
assert!(result.is_some());
assert_eq!(result.unwrap().to_string(), "1/2");
}

View File

@@ -56,9 +56,10 @@
margin-left: auto; margin-left: auto;
display: block; display: block;
position: absolute; position: absolute;
top: 0%; top: 0;
left: 50%; left: 0;
transform: translate(-50%, 0%); width: 100%;
height: 100%;
} }
</style> </style>
</head> </head>