Compare commits

...

51 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
29 changed files with 2965 additions and 1996 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@
/Cargo.lock
perf.data
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]
crate-type = ["cdylib", "rlib"]
[features]
default = ["eframe/wayland"]
[profile.release]
debug = false
codegen-units = 1
@@ -33,64 +36,66 @@ strip = false
[dependencies]
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",
] }
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",
] }
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",
] }
emath = { git = "https://github.com/titaniumtown/egui.git", rev = "b088efb9fa917845ecb54729a0d2fc592d2399e7", default-features = false }
egui_plot = { 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 = { version = "0.34.0", default-features = false }
shadow-rs = { version = "0.12", default-features = false }
const_format = { version = "0.2", default-features = false, features = ["fmt"] }
cfg-if = "1"
ruzstd = "0.5"
ruzstd = "0.8"
tracing = "0.1"
itertools = "0.10"
itertools = "0.14"
static_assertions = "1.1"
bincode = "1.3"
serde = "1"
base64 = "0.21"
base64 = "0.22"
[dev-dependencies]
benchmarks = { path = "./benchmarks" }
[build-dependencies]
shadow-rs = "0.12"
epaint = { git = "https://github.com/titaniumtown/egui.git", rev = "b088efb9fa917845ecb54729a0d2fc592d2399e7", default-features = false, features = [
shadow-rs = "1.4"
epaint = { git = "https://github.com/titaniumtown/egui.git", default-features = false, features = [
"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",
] }
bincode = "1.3"
serde = "1"
serde_json = "1"
zstd = { version = "0.11", default-features = false, features = ["pkg-config"] }
run_script = "0.9"
json5 = "0.4"
itertools = "0.10"
zstd = { version = "0.13", default-features = false, features = ["pkg-config"] }
itertools = "0.14"
allsorts = "0.15"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
instant = "0.1"
tracing-subscriber = "0.3"
getrandom = { version = "0.2" }
[target.'cfg(target_arch = "wasm32")'.dependencies]
instant = { version = "0.1", features = ["wasm-bindgen"] }
lol_alloc = "0.4.0"
lol_alloc = "0.4.1"
wasm-bindgen = { version = "0.2", default-features = false, features = ["std"] }
web-sys = "0.3"
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
wasm-bindgen-futures = "=0.4.54"
[package.metadata.cargo-all-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" }

16
TODO.md
View File

@@ -1,18 +1,20 @@
## TODO:
1. Function management
- 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)
- [Drag and drop support](https://github.com/emilk/egui/discussions/1530) in the UI to re-order functions
- Hide/disable functions
- Prevent user from making too many function entries
- Display function errors as tooltips or a warning box (not preventing the display of the graph)
- Clone functions
a. Integrals between functions (too hard to implement, maybe will shelve)
b. Display intersection between functions (would have to rewrite a lot of the function plotting handling)
c. [Drag and drop support](https://github.com/emilk/egui/discussions/1530) in the UI to re-order functions
d. Hide/disable functions
e. Prevent user from making too many function entries
f. Display function errors as tooltips or a warning box (not preventing the display of the graph)
g. Clone functions
2. Smart display of graph
- Display of intersections between functions
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)
5. Fix integral display
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
8. Fix mobile text input
9. Write custom plotter

115
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::{
collections::BTreeMap,
env,
fs::File,
io::{BufWriter, Write},
path::Path,
sync::Arc,
};
use epaint::{
FontFamily,
text::{FontData, FontDefinitions, FontTweak},
};
use run_script::ScriptOptions;
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/unicode_helper.rs"
));
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 unicodes_formatted = unicodes
.iter()
.map(|u| format!("U+{}", u))
.collect::<Vec<String>>()
.join(",");
std::fs::write(&new_path, &subset_data)
.map_err(|e| format!("Failed to write subset font: {}", e))?;
// Test to see if pyftsubset is found
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!()
Ok(subset_data)
}
fn main() {
// rebuild if new commit or contents of `assets` folder changed
println!("cargo:rerun-if-changed=.git/logs/HEAD");
// rebuild if contents of `assets` folder changed
println!("cargo:rerun-if-changed=assets/*");
shadow_rs::new().expect("Could not initialize shadow_rs");
let mut main_chars: Vec<char> =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzsu0123456789?.,!(){}[]-_=+-/<>'\\ :^*`@#$%&|~;"
.iter()
@@ -104,38 +98,39 @@ fn main() {
font_data: BTreeMap::from([
(
"Ubuntu-Light".to_owned(),
FontData::from_owned(
Arc::new(FontData::from_owned(
font_stripper(
"Ubuntu-Light.ttf",
"ubuntu-light.ttf",
[main_chars, vec!['∫']].concat(),
)
.unwrap(),
),
)),
),
(
"NotoEmoji-Regular".to_owned(),
FontData::from_owned(
Arc::new(FontData::from_owned(
font_stripper(
"NotoEmoji-Regular.ttf",
"noto-emoji.ttf",
vec!['🌞', '🌙', '✖'],
)
.unwrap(),
),
)),
),
(
"emoji-icon-font".to_owned(),
Arc::new(
FontData::from_owned(
font_stripper("emoji-icon-font.ttf", "emoji-icon.ttf", vec!['⚙']).unwrap(),
)
.tweak(FontTweak {
scale: 0.8,
y_offset_factor: 0.07,
y_offset: 0.0,
baseline_offset_factor: -0.0333,
y_offset: -0.0333,
}),
),
),
]),
families: BTreeMap::from([
(

View File

@@ -1,5 +1,5 @@
{
description = "YTBN Graphing Software - Web-compatible graphing calculator";
description = "YTBN Graphing Software";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
@@ -40,12 +40,12 @@
pname = "ytbn-graphing-software-wasm";
version = "0.1.0";
src = "${./.}";
src = ./.;
cargoLock = {
lockFile = ./Cargo.lock;
outputHashes = {
"ecolor-0.25.0" = "sha256-9s5LCngwvIIL43txT6sBs4JlRXqmYt1Kw8hlDnwx+DI=";
"ecolor-0.33.2" = "sha256-jdQK55yKZptadwosrJXIhoQDGNeELQmPExWRsGc0VG0=";
};
};
@@ -60,11 +60,30 @@
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 = ''
runHook preBuild
export HOME=$TMPDIR
cargo build \
--release \
--lib \
@@ -80,7 +99,7 @@
runHook postInstall
'';
doCheck = false;
doCheck = true;
};
# Final web package with wasm-bindgen processing
@@ -107,7 +126,7 @@
# Optimize wasm (enable features used by modern rust wasm targets)
wasm-opt out/ytbn_graphing_software_bg.wasm \
-O2 --fast-math \
-Oz \
--enable-bulk-memory \
--enable-nontrapping-float-to-int \
--enable-sign-ext \
@@ -136,13 +155,6 @@
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
{
@@ -157,7 +169,6 @@
rustToolchain
wasm-bindgen-cli
binaryen
python3Packages.fonttools
rust-analyzer
pkg-config
clang
@@ -166,9 +177,6 @@
libxkbcommon
libGL
wayland
xorg.libX11
xorg.libXcursor
xorg.libXi
];
buildInputs = with pkgs; [
@@ -182,9 +190,6 @@
libxkbcommon
libGL
wayland
xorg.libX11
xorg.libXcursor
xorg.libXi
]
);
};

View File

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

View File

@@ -64,7 +64,8 @@ pub fn compile_hashmap(data: Vec<String>) -> Vec<(String, String)> {
/// Returns a vector of all possible splitting combinations of a strings
#[allow(dead_code)]
fn all_possible_splits(
func: String, seen: &mut HashSet<(String, String)>,
func: String,
seen: &mut HashSet<(String, String)>,
) -> Vec<(String, String)> {
(1..func.len())
.map(|i| {

View File

@@ -4,21 +4,14 @@ use std::collections::HashMap;
#[derive(Clone, PartialEq)]
pub struct FlatExWrapper {
func: Option<FlatEx<f64>>,
func_str: Option<String>,
}
impl FlatExWrapper {
const EMPTY: FlatExWrapper = FlatExWrapper {
func: None,
func_str: None,
};
const EMPTY: FlatExWrapper = FlatExWrapper { func: None };
#[inline]
const fn new(f: FlatEx<f64>) -> Self {
Self {
func: Some(f),
func_str: None,
}
Self { func: Some(f) }
}
#[inline]
@@ -34,33 +27,13 @@ impl FlatExWrapper {
.unwrap_or(f64::NAN)
}
#[inline]
fn partial(&self, x: usize) -> Self {
self.func
.as_ref()
.map(|f| f.clone().partial(x).map(Self::new).unwrap_or(Self::EMPTY))
.unwrap_or(Self::EMPTY)
}
#[inline]
fn get_string(&mut self) -> String {
match self.func_str {
Some(ref func_str) => func_str.clone(),
None => {
let calculated = self.func.as_ref().map(|f| f.unparse()).unwrap_or("");
self.func_str = Some(calculated.to_owned());
calculated.to_owned()
}
}
}
#[inline]
fn partial_iter(&self, n: usize) -> Self {
self.func
.as_ref()
.map(|f| {
f.clone()
.partial_iter((0..=n).map(|_| 0))
.partial_iter((0..n).map(|_| 0))
.map(Self::new)
.unwrap_or(Self::EMPTY)
})
@@ -165,18 +138,6 @@ impl BackingFunction {
}
}
fn prettyify_function_str(func: &str) -> String {
let new_str = func.replace("{x}", "x");
if &new_str == "0/0" {
"Undefined".to_owned()
} else {
new_str
}
}
// pub const VALID_VARIABLES: [char; 3] = ['x', 'e', 'π'];
/// Case insensitive checks for if `c` is a character used to represent a variable
#[inline]
pub const fn is_variable(c: &char) -> bool {

View File

@@ -1,17 +1,61 @@
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> {
// Protect function names that could be incorrectly split
let (protected, replacements) = protect_function_names(input);
split_function_chars(
&input
&protected
.replace("pi", "π") // replace "pi" text with pi symbol
.replace("**", "^") // support alternate manner of expressing exponents
.replace("exp", "\u{1fc93}") // stop-gap solution to fix the `exp` function
.chars()
.collect::<Vec<char>>(),
split,
)
.iter()
.map(|x| x.replace('\u{1fc93}', "exp")) // Convert back to `exp` text
.map(|x| restore_function_names(x, &replacements))
.collect::<Vec<String>>()
}
@@ -54,50 +98,81 @@ impl BoolSlice {
}
}
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
}
/// Returns true if this char is a function name letter (not a standalone variable)
const fn is_function_letter(&self) -> bool {
self.letter && !self.is_unmasked_variable()
}
/// Returns true if this is a "term" - something that can be multiplied
const fn is_term(&self) -> bool {
self.is_unmasked_number() || self.is_unmasked_variable() || self.letter
}
const fn calculate_mask(&mut self, other: &BoolSlice) {
if other.masked_num && self.number {
// If previous char was a masked number, and current char is a number, mask current char's variable status
// Propagate number masking through consecutive digits
self.masked_num = true;
} else if other.masked_var && self.variable {
// If previous char was a masked variable, and current char is a variable, mask current char's variable status
// Propagate variable masking through consecutive variables
self.masked_var = true;
} else if other.letter && !other.is_unmasked_variable() {
} else if other.is_function_letter() {
// After a function letter, mask following numbers/variables as part of function name
self.masked_num = self.number;
self.masked_var = self.variable;
}
}
const fn splitable(&self, c: &char, other: &BoolSlice, split: &SplitType) -> bool {
if (*c == '*') | (matches!(split, &SplitType::Term) && other.open_parens) {
true
} else if other.closing_parens {
// Cases like `)x`, `)2`, and `)(`
return (*c == '(')
| (self.letter && !self.is_unmasked_variable())
| self.is_unmasked_variable()
| self.is_unmasked_number();
} else if *c == '(' {
// Cases like `x(` and `2(`
return (other.is_unmasked_variable() | other.is_unmasked_number()) && !other.letter;
} else if other.is_unmasked_number() {
// Cases like `2x` and `2sin(x)`
return self.is_unmasked_variable() | self.letter;
} else if self.is_unmasked_variable() | self.letter {
// Cases like `e2` and `xx`
return other.is_unmasked_number()
| (other.is_unmasked_variable() && self.is_unmasked_variable())
| other.is_unmasked_variable();
} else if (self.is_unmasked_number() | self.letter | self.is_unmasked_variable())
&& (other.is_unmasked_number() | other.letter)
{
/// Determines if we should split (insert implicit multiplication) before current char
const fn splitable(&self, c: &char, prev: &BoolSlice, split: &SplitType) -> bool {
// Always split on explicit multiplication
if *c == '*' {
return true;
} else {
return self.is_unmasked_number() && other.is_unmasked_variable();
}
// For Term split type, also split after open parens
if matches!(split, &SplitType::Term) && prev.open_parens {
return true;
}
// After closing paren: split before `(`, letters, variables, or numbers
// e.g., `)x`, `)2`, `)(`, `)sin`
if prev.closing_parens {
return *c == '(' || self.is_term();
}
// Before open paren: split if previous was a standalone number or variable
// e.g., `x(`, `2(` but not `sin(`
if *c == '(' {
return (prev.is_unmasked_variable() || prev.is_unmasked_number()) && !prev.letter;
}
// After a number: split before variables or function letters
// e.g., `2x`, `2sin`
if prev.is_unmasked_number() {
return self.is_unmasked_variable() || self.letter;
}
// Current is a variable or letter: split if previous was a number or variable
// e.g., `e2`, `xx`, `xe`
if self.is_unmasked_variable() || self.letter {
return prev.is_unmasked_number() || prev.is_unmasked_variable();
}
// Current is a number after a variable
// e.g., `x2`
if self.is_unmasked_number() && prev.is_unmasked_variable() {
return true;
}
false
}
}
@@ -201,4 +276,13 @@ fn split_function_test() {
&["2", "sin(π) + 2", "cos(tau)"],
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

@@ -87,15 +87,21 @@ impl<'a> std::fmt::Debug for Hint<'a> {
impl<'a> Hint<'a> {
#[inline]
pub const fn is_none(&self) -> bool { matches!(&self, &Hint::None) }
pub const fn is_none(&self) -> bool {
matches!(&self, &Hint::None)
}
#[inline]
#[allow(dead_code)]
pub const fn is_some(&self) -> bool { !self.is_none() }
pub const fn is_some(&self) -> bool {
!self.is_none()
}
#[inline]
#[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]
#[allow(dead_code)]

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

View File

@@ -1,16 +1,14 @@
use crate::math_app::AppSettings;
use crate::misc::{EguiHelper, newtons_method_helper, step_helper};
use crate::symbolic::try_symbolic;
use egui::{Checkbox, Context};
use egui_plot::{Bar, BarChart, PlotPoint, PlotUi};
use egui_plot::{Bar, BarChart, PlotPoint, PlotUi, Points};
use epaint::Color32;
use parsing::{AutoComplete, generate_hint};
use parsing::{BackingFunction, process_func_str};
use serde::{Deserialize, Deserializer, Serialize, Serializer, ser::SerializeStruct};
use std::{
fmt::{self, Debug},
hash::{Hash, Hasher},
};
use std::fmt::{self, Debug};
/// Represents the possible variations of Riemann Sums
#[derive(PartialEq, Eq, Debug, Copy, Clone, Default)]
@@ -34,16 +32,13 @@ pub struct FunctionEntry {
/// The `BackingFunction` instance that is used to generate `f(x)`, `f'(x)`, and `f''(x)`
function: BackingFunction,
/// Stores a function string (that hasn't been processed via `process_func_str`) to display to the user
pub raw_func_str: String,
/// If calculating/displayingintegrals are enabled
pub integral: bool,
/// If displaying derivatives are enabled (note, they are still calculated for other purposes)
pub derivative: bool,
pub nth_derviative: bool,
pub nth_derivative: bool,
pub back_data: Vec<PlotPoint>,
pub integral_data: Option<(Vec<Bar>, f64)>,
@@ -60,23 +55,13 @@ pub struct FunctionEntry {
pub settings_opened: bool,
}
impl Hash for FunctionEntry {
fn hash<H: Hasher>(&self, state: &mut H) {
self.raw_func_str.hash(state);
self.integral.hash(state);
self.nth_derviative.hash(state);
self.curr_nth.hash(state);
self.settings_opened.hash(state);
}
}
impl Serialize for FunctionEntry {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut s = serializer.serialize_struct("FunctionEntry", 4)?;
s.serialize_field("raw_func_str", &self.raw_func_str)?;
s.serialize_field("raw_func_str", &self.autocomplete.string)?;
s.serialize_field("integral", &self.integral)?;
s.serialize_field("derivative", &self.derivative)?;
s.serialize_field("curr_nth", &self.curr_nth)?;
@@ -125,10 +110,9 @@ impl Default for FunctionEntry {
fn default() -> FunctionEntry {
FunctionEntry {
function: BackingFunction::default(),
raw_func_str: String::new(),
integral: false,
derivative: false,
nth_derviative: false,
nth_derivative: false,
back_data: Vec::new(),
integral_data: None,
derivative_data: Vec::new(),
@@ -150,14 +134,14 @@ impl FunctionEntry {
pub fn settings_window(&mut self, ctx: &Context) {
let mut invalidate_nth = false;
egui::Window::new(format!("Settings: {}", self.raw_func_str))
egui::Window::new(format!("Settings: {}", self.func_str()))
.open(&mut self.settings_opened)
.default_pos([200.0, 200.0])
.resizable(false)
.collapsible(false)
.show(ctx, |ui| {
ui.add(Checkbox::new(
&mut self.nth_derviative,
&mut self.nth_derivative,
"Display Nth Derivative",
));
@@ -180,13 +164,32 @@ impl FunctionEntry {
&self.test_result
}
/// Get the raw function string
#[inline]
pub fn func_str(&self) -> &str {
&self.autocomplete.string
}
/// Update function string and test it
pub fn update_string(&mut self, raw_func_str: &str) {
if raw_func_str == self.raw_func_str {
if raw_func_str == self.func_str() {
return;
}
self.raw_func_str = raw_func_str.to_owned();
// Update the autocomplete string (which is now the source of truth for the raw string)
self.autocomplete.update_string(raw_func_str);
self.reparse_function(raw_func_str);
}
/// Re-parse the function from the current autocomplete string.
/// Call this when the autocomplete string was updated externally (e.g., via hint application).
pub fn sync_from_autocomplete(&mut self) {
self.reparse_function(&self.autocomplete.string.clone());
}
/// Internal helper to parse a function string and update internal state
fn reparse_function(&mut self, raw_func_str: &str) {
let processed_func = process_func_str(raw_func_str);
let new_func_result = BackingFunction::new(&processed_func);
@@ -212,17 +215,16 @@ impl FunctionEntry {
) -> (Vec<(f64, f64)>, f64) {
let step = (integral_max_x - integral_min_x) / (integral_num as f64);
// let sum_func = self.get_sum_func(sum);
let data2: Vec<(f64, f64)> = step_helper(integral_num, integral_min_x, step)
let data: Vec<(f64, f64)> = step_helper(integral_num, integral_min_x, step)
.into_iter()
.map(|x| {
let step_offset = step.copysign(x); // store the offset here so it doesn't have to be calculated multiple times
let x2: f64 = x + step_offset;
let step_offset = step.copysign(x);
let x2 = x + step_offset;
let (left_x, right_x) = match x.is_sign_positive() {
true => (x, x2),
false => (x2, x),
let (left_x, right_x) = if x.is_sign_positive() {
(x, x2)
} else {
(x2, x)
};
let y = match sum {
@@ -238,9 +240,9 @@ impl FunctionEntry {
.filter(|(_, y)| y.is_finite())
.collect();
let area = data2.iter().map(move |(_, y)| y * step).sum();
let area = data.iter().map(|(_, y)| y * step).sum();
(data2, area)
(data, area)
}
/// Helps with processing newton's method depending on level of derivative
@@ -252,30 +254,36 @@ impl FunctionEntry {
) -> Vec<PlotPoint> {
self.function.generate_derivative(derivative_level);
self.function.generate_derivative(derivative_level + 1);
let newtons_method_output: Vec<f64> = match derivative_level {
0 => newtons_method_helper(
threshold,
range,
self.back_data.as_slice(),
self.function.get_function_derivative(0),
self.function.get_function_derivative(1),
),
1 => newtons_method_helper(
threshold,
range,
self.derivative_data.as_slice(),
self.function.get_function_derivative(1),
self.function.get_function_derivative(2),
),
let data_source = match derivative_level {
0 => self.back_data.as_slice(),
1 => self.derivative_data.as_slice(),
_ => unreachable!(),
};
newtons_method_output
newtons_method_helper(
threshold,
range,
data_source,
self.function.get_function_derivative(derivative_level),
self.function.get_function_derivative(derivative_level + 1),
)
.into_iter()
.map(|x| PlotPoint::new(x, self.function.get(0, x)))
.collect()
}
/// Generates plot data for a given derivative level over the resolution iterator
fn generate_plot_data(&mut self, derivative: usize, resolution_iter: &[f64]) -> Vec<PlotPoint> {
if derivative > 0 {
self.function.generate_derivative(derivative);
}
resolution_iter
.iter()
.map(|&x| PlotPoint::new(x, self.function.get(derivative, x)))
.collect()
}
/// Does the calculations and stores results in `self`
pub fn calculate(
&mut self,
@@ -304,32 +312,17 @@ impl FunctionEntry {
}
if self.back_data.is_empty() {
let data: Vec<PlotPoint> = resolution_iter
.clone()
.into_iter()
.map(|x| PlotPoint::new(x, self.function.get(0, x)))
.collect();
debug_assert_eq!(data.len(), settings.plot_width + 1);
self.back_data = data;
self.back_data = self.generate_plot_data(0, &resolution_iter);
debug_assert_eq!(self.back_data.len(), settings.plot_width + 1);
}
if self.derivative_data.is_empty() {
self.function.generate_derivative(1);
let data: Vec<PlotPoint> = resolution_iter
.clone()
.into_iter()
.map(|x| PlotPoint::new(x, self.function.get(1, x)))
.collect();
debug_assert_eq!(data.len(), settings.plot_width + 1);
self.derivative_data = data;
self.derivative_data = self.generate_plot_data(1, &resolution_iter);
debug_assert_eq!(self.derivative_data.len(), settings.plot_width + 1);
}
if self.nth_derviative && self.nth_derivative_data.is_none() {
let data: Vec<PlotPoint> = resolution_iter
.into_iter()
.map(|x| PlotPoint::new(x, self.function.get(self.curr_nth, x)))
.collect();
if self.nth_derivative && self.nth_derivative_data.is_none() {
let data = self.generate_plot_data(self.curr_nth, &resolution_iter);
debug_assert_eq!(data.len(), settings.plot_width + 1);
self.nth_derivative_data = Some(data);
}
@@ -352,7 +345,7 @@ impl FunctionEntry {
self.clear_integral();
}
let threshold: f64 = resolution / 2.0;
let threshold: f64 = f64::EPSILON;
let x_range = settings.min_x..settings.max_x;
// Calculates extrema
@@ -385,7 +378,11 @@ impl FunctionEntry {
let step = (settings.max_x - settings.min_x) / (settings.plot_width as f64);
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.integral && (step >= integral_step) {
plot_ui.line(
@@ -403,45 +400,109 @@ impl FunctionEntry {
.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
.clone()
.iter()
.filter(|p| !is_near_special(p))
.cloned()
.collect::<Vec<PlotPoint>>()
.to_line()
.stroke(egui::Stroke::new(4.0, main_plot_color)),
);
} else {
// No filtering needed - use data directly
self.back_data.clone().to_line()
};
plot_ui.line(main_line.stroke(egui::Stroke::new(4.0, main_plot_color)));
}
// Plot derivative data
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
if settings.do_extrema && !self.extrema_data.is_empty() {
plot_ui.points(
self.extrema_data
.clone()
.to_points()
.color(Color32::YELLOW)
.radius(5.0), // Radius of points of Extrema
for point in &self.extrema_data {
let name = format!(
"({}, {})",
try_symbolic(point.x)
.map(|s| s.to_string())
.unwrap_or_else(|| format!("{:.4}", point.x)),
try_symbolic(point.y)
.map(|s| s.to_string())
.unwrap_or_else(|| format!("{:.4}", point.y))
);
plot_ui.points(
Points::new(name, vec![[point.x, point.y]])
.color(Color32::YELLOW)
.radius(5.0),
);
}
}
// Plot roots points
if settings.do_roots && !self.root_data.is_empty() {
for point in &self.root_data {
let name = format!(
"({}, {})",
try_symbolic(point.x)
.map(|s| s.to_string())
.unwrap_or_else(|| format!("{:.4}", point.x)),
try_symbolic(point.y)
.map(|s| s.to_string())
.unwrap_or_else(|| format!("{:.4}", point.y))
);
plot_ui.points(
self.root_data
.clone()
.to_points()
Points::new(name, vec![[point.x, point.y]])
.color(Color32::LIGHT_BLUE)
.radius(5.0), // Radius of points of Roots
.radius(5.0),
);
}
}
if self.nth_derviative
&& let Some(ref nth_derviative) = self.nth_derivative_data
if self.nth_derivative
&& let Some(ref nth_derivative) = self.nth_derivative_data
{
plot_ui.line(nth_derviative.clone().to_line().color(Color32::DARK_RED));
plot_ui.line(nth_derivative.clone().to_line().color(Color32::DARK_RED));
}
// Plot integral data
@@ -449,7 +510,7 @@ impl FunctionEntry {
Some(integral_data) => {
if integral_step > step {
plot_ui.bar_chart(
BarChart::new(integral_data.0.clone())
BarChart::new("integral bar chart", integral_data.0.clone())
.color(Color32::BLUE)
.width(integral_step),
);

View File

@@ -1,63 +1,38 @@
use crate::{
consts::COLORS, function_entry::FunctionEntry, misc::random_u64, widgets::widgets_ontop,
};
use egui::{Button, Id, Key, Modifiers, TextEdit, WidgetText};
use crate::{function_entry::FunctionEntry, widgets::widgets_ontop};
use egui::{Button, Id, Key, Modifiers, Popup, PopupCloseBehavior, TextEdit, WidgetText};
use emath::vec2;
use parsing::Movement;
use serde::ser::SerializeStruct;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use serde::{Deserialize, Serialize};
use std::ops::BitXorAssign;
type Functions = Vec<(Id, FunctionEntry)>;
#[derive(Serialize, Deserialize)]
pub struct FunctionManager {
functions: Functions,
}
impl Default for FunctionManager {
fn default() -> Self {
let mut vec: Functions = Vec::with_capacity(COLORS.len());
vec.push((
Id::new(11414819524356497634_u64), // Random number here to avoid call to crate::misc::random_u64()
FunctionEntry::default(),
));
Self { functions: vec }
let mut d = Self::new();
d.push_empty();
d
}
}
impl Serialize for FunctionManager {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
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()
}
}
#[test]
fn func_manager_roundtrip_serdes() {
let mut func_manager = FunctionManager {
functions: Vec::new(),
};
impl<'de> Deserialize<'de> for FunctionManager {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct Helper(Vec<(Id, FunctionEntry)>);
let helper = Helper::deserialize(deserializer)?;
Ok(FunctionManager {
functions: helper.0.to_vec(),
})
}
func_manager.push_empty();
let ser = bincode::serialize(&func_manager).expect("unable to serialize");
let des: FunctionManager = bincode::deserialize(&ser).expect("unable to deserialize");
assert_eq!(
func_manager.functions[0].1.func_str(),
des.functions[0].1.func_str()
);
}
/// 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 {
#[inline]
fn get_hash(&self) -> u64 {
let mut hasher = DefaultHasher::new();
self.functions.hash(&mut hasher);
hasher.finish()
pub fn new() -> Self {
Self {
functions: Vec::new(),
}
}
/// Displays function entries alongside returning whether or not functions have been modified
pub fn display_entries(&mut self, ui: &mut egui::Ui) -> bool {
let initial_hash = self.get_hash();
let mut changed = false;
let can_remove = self.functions.len() > 1;
let available_width = ui.available_width();
@@ -84,7 +57,6 @@ impl FunctionManager {
let target_size = vec2(available_width, crate::consts::FONT_SIZE);
for (i, (te_id, function)) in self.functions.iter_mut().map(|(a, b)| (*a, b)).enumerate() {
let mut new_string = function.autocomplete.string.clone();
function.update_string(&new_string);
let mut movement: Movement = Movement::default();
@@ -108,11 +80,15 @@ impl FunctionManager {
// Only keep valid chars
new_string.retain(crate::misc::is_valid_char);
// Track if function string changed and update the function
if new_string != function.autocomplete.string {
changed = true;
function.update_string(&new_string);
}
// If not fully open, return here as buttons cannot yet be displayed, therefore the user is inable to mark it for deletion
let animate_bool = ui.ctx().animate_bool(te_id, re.has_focus());
if animate_bool == 1.0 {
function.autocomplete.update_string(&new_string);
if function.autocomplete.hint.is_some() {
// only register up and down arrow movements if hint is type `Hint::Many`
if !function.autocomplete.hint.is_single() {
@@ -137,9 +113,18 @@ impl FunctionManager {
movement = Movement::Complete;
}
// Remember string before movement to detect changes
let string_before = function.autocomplete.string.clone();
// Register movement and apply proper changes
function.autocomplete.register_movement(&movement);
// If the string changed (hint was applied), update the backing function
if function.autocomplete.string != string_before {
function.sync_from_autocomplete();
changed = true;
}
if movement != Movement::Complete
&& let Some(hints) = function.autocomplete.hint.many()
{
@@ -147,7 +132,10 @@ impl FunctionManager {
let autocomplete_popup_id = Id::new("autocomplete popup");
egui::popup_below_widget(ui, autocomplete_popup_id, &re, |ui| {
Popup::from_response(&re)
.id(autocomplete_popup_id)
.close_behavior(PopupCloseBehavior::CloseOnClickOutside)
.show(|ui| {
hints.iter().enumerate().for_each(|(i, candidate)| {
if ui
.selectable_label(i == function.autocomplete.i, *candidate)
@@ -163,19 +151,26 @@ impl FunctionManager {
function
.autocomplete
.apply_hint(hints[function.autocomplete.i]);
// Update the backing function with the new string after hint was applied
function.sync_from_autocomplete();
changed = true;
movement = Movement::Complete;
} else {
ui.memory_mut(|x| x.open_popup(autocomplete_popup_id));
Popup::open_id(ui.ctx(), autocomplete_popup_id);
}
}
// Push cursor to end if needed
if movement == Movement::Complete {
// TODO! proper error handling
let mut state =
unsafe { TextEdit::load_state(ui.ctx(), te_id).unwrap_unchecked() };
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);
}
}
@@ -236,17 +231,16 @@ impl FunctionManager {
// Remove function if the user requests it
if let Some(remove_i_unwrap) = remove_i {
self.functions.remove(remove_i_unwrap);
changed = true;
}
let final_hash = self.get_hash();
initial_hash != final_hash
changed
}
/// Create and push new empty function entry
pub fn push_empty(&mut self) {
self.functions.push((
Id::new(random_u64().expect("unable to generate random id")),
Id::new(format!("function #{}", self.functions.len() + 1)),
FunctionEntry::default(),
));
}

View File

@@ -6,29 +6,30 @@ mod function_entry;
mod function_manager;
mod math_app;
mod misc;
pub mod symbolic;
mod unicode_helper;
mod widgets;
pub use crate::{
function_entry::{FunctionEntry, Riemann},
math_app::AppSettings,
misc::{
EguiHelper, HashBytes, hashed_storage_create, hashed_storage_read, newtons_method,
option_vec_printer, step_helper,
},
math_app::{AppSettings, MathApp},
misc::{EguiHelper, newtons_method, option_vec_printer, step_helper},
unicode_helper::{to_chars_array, to_unicode_hash},
};
cfg_if::cfg_if! {
if #[cfg(target_arch = "wasm32")] {
use wasm_bindgen::prelude::*;
use lol_alloc::{FreeListAllocator, LockedAllocator};
#[global_allocator]
static ALLOCATOR: LockedAllocator<FreeListAllocator> = LockedAllocator::new(FreeListAllocator::new());
// WASM-specific setup
#[cfg(target_arch = "wasm32")]
mod wasm {
use super::math_app;
use eframe::WebRunner;
// use tracing::metadata::LevelFilter;
use lol_alloc::{FreeListAllocator, LockedAllocator};
use wasm_bindgen::prelude::*;
use web_sys::HtmlCanvasElement;
#[global_allocator]
static ALLOCATOR: LockedAllocator<FreeListAllocator> =
LockedAllocator::new(FreeListAllocator::new());
#[derive(Clone)]
#[wasm_bindgen]
pub struct WebHandle {
@@ -41,9 +42,7 @@ cfg_if::cfg_if! {
#[allow(clippy::new_without_default)]
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
// eframe::WebLogger::init(LevelFilter::Debug).ok();
tracing_wasm::set_as_global_default();
Self {
runner: WebRunner::new(),
}
@@ -51,12 +50,15 @@ cfg_if::cfg_if! {
/// Call this once from JavaScript to start your app.
#[wasm_bindgen]
pub async fn start(&self, canvas_id: &str) -> Result<(), wasm_bindgen::JsValue> {
pub async fn start(
&self,
canvas_id: HtmlCanvasElement,
) -> Result<(), wasm_bindgen::JsValue> {
self.runner
.start(
canvas_id,
eframe::WebOptions::default(),
Box::new(|cc| Box::new(math_app::MathApp::new(cc))),
Box::new(|cc| Ok(Box::new(math_app::MathApp::new(cc)))),
)
.await
}
@@ -66,9 +68,20 @@ cfg_if::cfg_if! {
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.unwrap()
}
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?)
#[cfg(not(target_arch = "wasm32"))]
fn main() -> eframe::Result<()> {
@@ -21,6 +10,6 @@ fn main() -> eframe::Result<()> {
eframe::run_native(
"(Yet-to-be-named) Graphing Software",
eframe::NativeOptions::default(),
Box::new(|cc| 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::{
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_manager::FunctionManager,
misc::option_vec_printer,
widgets::toggle_button,
};
use eframe::App;
use egui::{
Button, CentralPanel, Color32, ComboBox, Context, DragValue, Frame, Key, Layout, SidePanel,
TopBottomPanel, Ui, Vec2, Window, style::Margin,
Button, CentralPanel, Color32, ComboBox, Context, CornerRadius, DragValue, Frame, Key, Layout,
SidePanel, TopBottomPanel, Ui, Vec2, Window,
};
use egui_plot::Plot;
use emath::{Align, Align2};
use epaint::Rounding;
use instant::Instant;
use epaint::Margin;
use itertools::Itertools;
use std::{io::Read, ops::BitXorAssign};
use std::io::Read;
/// Stores current settings/state of [`MathApp`]
#[derive(Copy, Clone)]
@@ -74,9 +74,6 @@ struct Opened {
/// Help window
pub help: bool,
/// Info window
pub info: bool,
/// Sidepanel
pub side_panel: bool,
@@ -88,7 +85,6 @@ impl Default for Opened {
fn default() -> Opened {
Self {
help: false,
info: false,
side_panel: true,
welcome: true,
}
@@ -100,8 +96,8 @@ pub struct MathApp {
/// Stores vector of functions
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.
last_info: (Option<String>, Option<String>),
/// Contains the list of Areas calculated
areas: Option<String>,
/// Stores opened windows/elements for later reference
opened: Opened,
@@ -128,109 +124,50 @@ const DATA_NAME: &str = "YTBN-DECOMPRESSED";
#[cfg(target_arch = "wasm32")]
const FUNC_NAME: &str = "YTBN-FUNCTIONS";
impl MathApp {
#[allow(dead_code)] // This is used lol
/// Create new instance of [`MathApp`] and return it
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...");
let start = Instant::now();
cfg_if::cfg_if! {
if #[cfg(target_arch = "wasm32")] {
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
}
}
/// 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 (commit, func_data) = crate::misc::hashed_storage_read(&data)?;
let 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 {
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();
let _ =
ruzstd::StreamingDecoder::new(
&mut const {
include_bytes!(concat!(env!("OUT_DIR"), "/compressed_data")).as_slice()
},
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");
#[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")
}
impl MathApp {
#[allow(dead_code)] // This is used lol
/// Create new instance of [`MathApp`] and return it
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
tracing::info!("Initializing...");
#[cfg(target_arch = "wasm32")]
tracing::info!("Web Info: {:?}", &cc.integration_info.web_info);
tracing::info!("Reading fonts...");
// Initialize fonts
// This used to be in the `update` method, but (after a ton of digging) this actually caused OOMs. that was a pain to debug
cc.egui_ctx.set_fonts({
#[cfg(target = "wasm32")]
if let Some(Ok(data)) =
get_storage_decompressed().map(|data| bincode::deserialize(data.as_slice()))
{
data
} else {
decompress_fonts()
}
cc.egui_ctx.set_fonts(decompress_fonts());
#[cfg(not(target = "wasm32"))]
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());
tracing::info!("Initialized!");
Self {
#[cfg(target_arch = "wasm32")]
@@ -239,7 +176,7 @@ impl MathApp {
#[cfg(not(target_arch = "wasm32"))]
functions: FunctionManager::default(),
last_info: (None, None),
areas: None,
opened: Opened::default(),
settings: AppSettings::default(),
}
@@ -339,22 +276,20 @@ impl MathApp {
});
ui.horizontal(|ui| {
self.settings.do_extrema.bitxor_assign(
ui.add(Button::new("Extrema"))
.on_hover_text(match self.settings.do_extrema {
true => "Disable Displaying Extrema",
false => "Display Extrema",
})
.clicked(),
toggle_button(
ui,
&mut self.settings.do_extrema,
"Extrema",
"Disable Displaying Extrema",
"Display Extrema",
);
self.settings.do_roots.bitxor_assign(
ui.add(Button::new("Roots"))
.on_hover_text(match self.settings.do_roots {
true => "Disable Displaying Roots",
false => "Display Roots",
})
.clicked(),
toggle_button(
ui,
&mut self.settings.do_roots,
"Roots",
"Disable Displaying Roots",
"Display Roots",
);
});
@@ -362,11 +297,8 @@ impl MathApp {
#[cfg(target_arch = "wasm32")]
{
tracing::info!("Saving function data");
use crate::misc::{HashBytes, hashed_storage_create};
let hash: HashBytes =
unsafe { std::mem::transmute::<&str, HashBytes>(build::SHORT_COMMIT) };
let saved_data = hashed_storage_create(
hash,
let saved_data = crate::misc::hashed_storage_create(
&bincode::serialize(&self.functions)
.expect("unable to deserialize functions"),
);
@@ -396,35 +328,24 @@ impl MathApp {
impl App for MathApp {
/// Called each time the UI needs repainting.
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 !ctx.wants_keyboard_input() {
// If `H` key is pressed, toggle Side Panel
self.opened
.side_panel
.bitxor_assign(ctx.input_mut(|x| x.consume_key(egui::Modifiers::NONE, Key::H)));
if ctx.input_mut(|x| x.consume_key(egui::Modifiers::NONE, Key::H)) {
self.opened.side_panel = !self.opened.side_panel;
}
}
// Creates Top bar that contains some general options
TopBottomPanel::top("top_bar").show(ctx, |ui| {
ui.horizontal(|ui| {
// Button in top bar to toggle showing the side panel
self.opened.side_panel.bitxor_assign(
ui.add(Button::new("Panel"))
.on_hover_text(match self.opened.side_panel {
true => "Hide Side Panel",
false => "Show Side Panel",
})
.clicked(),
toggle_button(
ui,
&mut self.opened.side_panel,
"Panel",
"Hide Side Panel",
"Show Side Panel",
);
// Button to add a new function
@@ -440,27 +361,16 @@ impl App for MathApp {
}
// Toggles opening the Help window
self.opened.help.bitxor_assign(
ui.add(Button::new("Help"))
.on_hover_text(match self.opened.help {
true => "Close Help Window",
false => "Open Help Window",
})
.clicked(),
);
// 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(),
toggle_button(
ui,
&mut self.opened.help,
"Help",
"Close Help Window",
"Open Help Window",
);
// 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);
}
});
@@ -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 self.opened.side_panel {
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)
CentralPanel::default()
.frame(Frame {
inner_margin: Margin::symmetric(0.0, 0.0),
rounding: Rounding::ZERO,
inner_margin: Margin::ZERO,
corner_radius: CornerRadius::ZERO,
// fill: crate::style::STYLE.window_fill(),
fill: Color32::from_gray(27),
..Frame::none()
..Frame::NONE
})
.show(ctx, |ui| {
// Display an error if it exists
@@ -549,13 +445,8 @@ impl App for MathApp {
.iter()
.map(|(_, func)| func.get_test_result())
.enumerate()
.filter(|(_, error)| error.is_some())
.map(|(i, error)| {
// use unwrap_unchecked as None Errors are already filtered out
unsafe {
format!("(Function #{}) {}\n", i, error.as_ref().unwrap_unchecked())
}
})
.filter_map(|(i, error)| error.as_ref().map(|x| (i, x)))
.map(|(i, error)| format!("(Function #{}) {}\n", i, error))
.join("");
if !errors_formatted.is_empty() {
@@ -609,15 +500,12 @@ impl App for MathApp {
})
.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())))
} else {
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 emath::Pos2;
use getrandom::getrandom;
use itertools::Itertools;
use parsing::FlatExWrapper;
/// Implements traits that are useful when dealing with Vectors of egui's `Value`
pub trait EguiHelper {
/// Converts to `egui::plot::Values`
fn to_values(self) -> PlotPoints;
fn to_values(self) -> PlotPoints<'static>;
/// Converts to `egui::plot::Line`
fn to_line(self) -> Line;
fn to_line(self) -> Line<'static>;
/// Converts to `egui::plot::Points`
fn to_points(self) -> Points;
fn to_points(self) -> Points<'static>;
/// Converts Vector of Values into vector of tuples
fn to_tuple(self) -> Vec<(f64, f64)>;
@@ -22,18 +22,18 @@ pub trait EguiHelper {
impl EguiHelper for Vec<PlotPoint> {
#[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) })
}
#[inline(always)]
fn to_line(self) -> Line {
Line::new(self.to_values())
fn to_line(self) -> Line<'static> {
Line::new("", self.to_values())
}
#[inline(always)]
fn to_points(self) -> Points {
Points::new(self.to_values())
fn to_points(self) -> Points<'static> {
Points::new("", self.to_values())
}
#[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.signum() != curr.y.signum())
.map(|(start, _)| start.x)
.map(|x| newtons_method(f, f_1, x, range, threshold))
.filter(|x| x.is_some())
.map(|x| unsafe { x.unwrap_unchecked() })
.filter_map(|x| newtons_method(f, f_1, x, range, threshold))
.collect()
}
@@ -151,58 +149,14 @@ pub fn step_helper(max_i: usize, min_x: f64, step: f64) -> Vec<f64> {
.collect()
}
// TODO: use in hovering over points
/// Attempts to see what variable `x` is almost
#[allow(dead_code)]
pub fn almost_variable(x: f64) -> Option<char> {
const EPSILON: f32 = f32::EPSILON * 2.0;
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)
pub fn hashed_storage_create(data: &[u8]) -> String {
general_purpose::STANDARD.encode(data)
}
#[allow(dead_code)]
pub fn hashed_storage_read(data: &str) -> Option<(HashBytes, Vec<u8>)> {
// Decode base64 data
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))
pub fn hashed_storage_read(data: &str) -> Option<Vec<u8>> {
general_purpose::STANDARD.decode(data).ok()
}
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 egui::{Id, InnerResponse};
use egui::{Button, Id, InnerResponse, Ui};
use std::ops::BitXorAssign;
/// Creates an area ontop of a widget with an y offset
pub fn widgets_ontop<R>(
@@ -15,3 +16,19 @@ pub fn widgets_ontop<R>(
area.show(ui.ctx(), |ui| add_contents(ui))
}
/// A toggle button that XORs its state when clicked.
/// Shows different hover text based on current state.
pub fn toggle_button(
ui: &mut Ui,
state: &mut bool,
label: &str,
enabled_tip: &str,
disabled_tip: &str,
) {
state.bitxor_assign(
ui.add(Button::new(label))
.on_hover_text(if *state { enabled_tip } else { disabled_tip })
.clicked(),
);
}

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)
| !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)
| !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)
| !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)
| !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)
| !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)
| !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)");
assert!(function.get_test_result().is_none());
assert_eq!(&function.raw_func_str, "sin(x)");
assert_eq!(function.func_str(), "sin(x)");
function.integral = false;
function.derivative = false;
@@ -271,3 +271,260 @@ fn middle_function() {
fn right_function() {
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]
// fn to_values() {
// use egui::plot::{Value, Values};
@@ -152,7 +124,7 @@ fn newtons_method() {
let data = newtons_method(
&get_flatexwrapper("x^2 -1"),
&get_flatexwrapper("2x"),
&get_flatexwrapper("2*x"),
3.0,
&(0.0..5.0),
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;
display: block;
position: absolute;
top: 0%;
left: 50%;
transform: translate(-50%, 0%);
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>
</head>