Compare commits

..

9 Commits

Author SHA1 Message Date
d592c74654 don't do sketchy egui Id stuff 2025-12-03 03:43:25 -05:00
d5bc1bed11 fix localstorage error 2025-12-03 02:03:15 -05:00
cef372377b fix build 2025-12-03 02:03:11 -05:00
d641610fbf remove .vscode and .github 2025-12-02 23:47:44 -05:00
e005675bb8 nix + web stuff
Some checks failed
CI / Check (push) Has been cancelled
CI / Tests (push) Has been cancelled
2025-12-02 23:44:36 -05:00
e62be3080f remove shell scripts 2025-12-02 23:13:57 -05:00
d95b29fb7f implement intesections + misc function options 2025-12-02 23:10:43 -05:00
bccb19cecc cargo clippy + fmt 2025-12-02 22:41:44 -05:00
dd377c1659 update EVERYTHING and rebase egui and depdencies 2025-12-02 22:40:08 -05:00
33 changed files with 2024 additions and 2853 deletions

1
.gitignore vendored
View File

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

1150
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@
name = "ytbn_graphing_software"
version = "0.1.0"
edition = "2024"
rust-version = "1.88"
license = "AGPL-3.0"
repository = "https://github.com/Titaniumtown/YTBN-Graphing-Software"
description = "Crossplatform (and web-compatible) graphing calculator"
@@ -9,9 +10,6 @@ description = "Crossplatform (and web-compatible) graphing calculator"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
default = ["eframe/wayland"]
[profile.release]
debug = false
codegen-units = 1
@@ -36,66 +34,74 @@ strip = false
[dependencies]
parsing = { path = "./parsing" }
eframe = { git = "https://github.com/titaniumtown/egui.git", default-features = false, features = [
eframe = { path = "../simon-egui/crates/eframe", default-features = false, features = [
"glow",
"x11",
] }
egui = { git = "https://github.com/titaniumtown/egui.git", default-features = false, features = [
egui = { path = "../simon-egui/crates/egui", default-features = false, features = [
"serde",
] }
epaint = { git = "https://github.com/titaniumtown/egui.git", default-features = false , features = [
epaint = { path = "../simon-egui/crates/epaint", default-features = false, features = [
"bytemuck",
] }
emath = { git = "https://github.com/titaniumtown/egui.git", default-features = false }
egui_plot = { version = "0.34.0", default-features = false }
emath = { path = "../simon-egui/crates/emath", default-features = false }
egui_plot = { git = "https://github.com/emilk/egui_plot.git", default-features = false }
shadow-rs = { version = "0.38", default-features = false }
const_format = { version = "0.2", default-features = false, features = ["fmt"] }
cfg-if = "1"
ruzstd = "0.8"
tracing = "0.1"
itertools = "0.14"
static_assertions = "1.1"
bincode = "1.3"
serde = "1"
log = "0.4"
base64 = "0.22"
[dev-dependencies]
benchmarks = { path = "./benchmarks" }
# Note: benchmarks are in a separate crate - run with:
# cd benchmarks && cargo bench
[build-dependencies]
shadow-rs = "1.4"
epaint = { git = "https://github.com/titaniumtown/egui.git", default-features = false, features = [
shadow-rs = "0.38"
epaint = { path = "../simon-egui/crates/epaint", default-features = false, features = [
"bytemuck",
] }
egui = { git = "https://github.com/titaniumtown/egui.git", default-features = false, features = [
egui = { path = "../simon-egui/crates/egui", default-features = false, features = [
"serde",
] }
bincode = "1.3"
serde = "1"
serde_json = "1"
zstd = { version = "0.13", default-features = false, features = ["pkg-config"] }
zstd = { version = "0.13", default-features = false }
run_script = "0.10"
json5 = "0.4"
itertools = "0.14"
allsorts = "0.15"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
web-time = "1.1"
tracing-subscriber = "0.3"
getrandom = { version = "0.3" }
[target.'cfg(target_arch = "wasm32")'.dependencies]
lol_alloc = "0.4.1"
web-time = "1.1"
lol_alloc = "0.4"
wasm-bindgen = { version = "0.2", default-features = false, features = ["std"] }
web-sys = "0.3"
tracing-wasm = "0.2"
# pinned to 0.4.54 in order to be compatible with nixos's wasm-bindgen-cli version
getrandom = { version = "0.3", features = ["wasm_js"] }
# pinned to 0.4.54 because nix provides up to 0.2.104 wasm-bindgen-cli
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" }
egui = { path = "../simon-egui/crates/egui" }
epaint = { path = "../simon-egui/crates/epaint" }
emath = { path = "../simon-egui/crates/emath" }
ecolor = { path = "../simon-egui/crates/ecolor" }
eframe = { path = "../simon-egui/crates/eframe" }
egui-winit = { path = "../simon-egui/crates/egui-winit" }
egui_glow = { path = "../simon-egui/crates/egui_glow" }
egui-wgpu = { path = "../simon-egui/crates/egui-wgpu" }

View File

@@ -4,4 +4,4 @@
<img src="assets/logo.svg" alt="logo" width="200"/>
### 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://graphing.gardling.com), 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://titaniumtown.github.io), as well as a native application.

12
TODO.md
View File

@@ -1,20 +1,12 @@
## TODO:
1. Function management
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
- Integrals between functions (too hard to implement, maybe will shelve)
- Prevent user from making too many function entries
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

View File

@@ -1,12 +1,18 @@
[package]
name = "benchmarks"
version = "0.1.0"
edition = "2021"
edition = "2024"
rust-version = "1.88"
license = "AGPL-3.0"
[lib]
bench = false
[[bench]]
name = "split_function"
harness = false
[dependencies]
pprof = { version = "0.9", features = ["flamegraph"] }
criterion = "0.3"
pprof = { version = "0.14", features = ["flamegraph"] }
criterion = { version = "0.5", features = ["html_reports"] }
parsing = { path = "../parsing" }

View File

@@ -0,0 +1,46 @@
use criterion::{criterion_group, criterion_main, Criterion};
use parsing::split_function_chars;
use std::time::Duration;
fn custom_criterion() -> Criterion {
Criterion::default()
.warm_up_time(Duration::from_millis(250))
.sample_size(1000)
}
fn mutli_split_function(c: &mut Criterion) {
let data_chars = vec![
"sin(x)cos(x)",
"x^2",
"2x",
"log10(x)",
"E^x",
"xxxxx",
"xsin(x)",
"(2x+1)(3x+1)",
"x**2",
"pipipipipipix",
"pi(2x+1)",
"(2x+1)pi",
]
.iter()
.map(|a| a.chars().collect::<Vec<char>>())
.collect::<Vec<Vec<char>>>();
let mut group = c.benchmark_group("split_function");
for entry in data_chars {
group.bench_function(entry.iter().collect::<String>(), |b| {
b.iter(|| {
split_function_chars(&entry, parsing::SplitType::Multiplication);
})
});
}
group.finish();
}
criterion_group! {
name = benches;
config = custom_criterion();
targets = mutli_split_function
}
criterion_main!(benches);

View File

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

112
build.rs
View File

@@ -1,14 +1,3 @@
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,
@@ -18,56 +7,76 @@ use std::{
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 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 unicodes: Vec<String> = unicodes.iter().map(|c| to_unicode_hash(*c)).collect();
let new_path = [&env::var("OUT_DIR").unwrap(), out].concat();
std::fs::write(&new_path, &subset_data)
.map_err(|e| format!("Failed to write subset font: {}", e))?;
let unicodes_formatted = unicodes
.iter()
.map(|u| format!("U+{}", u))
.collect::<Vec<String>>()
.join(",");
Ok(subset_data)
// 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!()
}
fn main() {
// rebuild if contents of `assets` folder changed
// rebuild if new commit or contents of `assets` folder changed
println!("cargo:rerun-if-changed=.git/logs/HEAD");
println!("cargo:rerun-if-changed=assets/*");
shadow_rs::ShadowBuilder::builder()
.build()
.expect("Could not initialize shadow_rs");
let mut main_chars: Vec<char> =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzsu0123456789?.,!(){}[]-_=+-/<>'\\ :^*`@#$%&|~;"
.iter()
@@ -122,12 +131,17 @@ fn main() {
"emoji-icon-font".to_owned(),
Arc::new(
FontData::from_owned(
font_stripper("emoji-icon-font.ttf", "emoji-icon.ttf", vec!['⚙']).unwrap(),
font_stripper(
"emoji-icon-font.ttf",
"emoji-icon.ttf",
vec!['⚙', '⎘', '👁', '○', '⬆', '⬇', '⚠'],
)
.unwrap(),
)
.tweak(FontTweak {
scale: 0.8,
y_offset_factor: 0.07,
y_offset: -0.0333,
y_offset: 0.0,
}),
),
),

20
flake.lock generated
View File

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

View File

@@ -1,5 +1,5 @@
{
description = "YTBN Graphing Software";
description = "YTBN Graphing Software - Web-compatible graphing calculator";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
@@ -8,6 +8,10 @@
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
simon-egui = {
url = "github:Titaniumtown/egui/217d3f250c499ae88c02930b26cacb945c4a2369";
flake = false;
};
};
outputs =
@@ -16,6 +20,7 @@
nixpkgs,
flake-utils,
rust-overlay,
simon-egui,
}:
flake-utils.lib.eachDefaultSystem (
system:
@@ -35,17 +40,33 @@
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
wasmLib = rustPlatform.buildRustPackage {
pname = "ytbn-graphing-software-wasm";
version = "0.1.0";
src = ./.;
src = combinedSrc;
sourceRoot = "${combinedSrc.name}/integral_site_rust";
cargoLock = {
lockFile = ./Cargo.lock;
outputHashes = {
"ecolor-0.33.2" = "sha256-jdQK55yKZptadwosrJXIhoQDGNeELQmPExWRsGc0VG0=";
"egui_plot-0.34.0" = "sha256-lk0yeljsvkHzF0eLD5llQ+05DycPqG2jGzhBvQ0X6Qw=";
};
};
@@ -60,30 +81,11 @@
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 \
@@ -99,7 +101,7 @@
runHook postInstall
'';
doCheck = true;
doCheck = false;
};
# Final web package with wasm-bindgen processing
@@ -126,7 +128,7 @@
# Optimize wasm (enable features used by modern rust wasm targets)
wasm-opt out/ytbn_graphing_software_bg.wasm \
-Oz \
-O2 --fast-math \
--enable-bulk-memory \
--enable-nontrapping-float-to-int \
--enable-sign-ext \
@@ -155,6 +157,13 @@
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
{
@@ -169,6 +178,7 @@
rustToolchain
wasm-bindgen-cli
binaryen
python3Packages.fonttools
rust-analyzer
pkg-config
clang
@@ -177,6 +187,9 @@
libxkbcommon
libGL
wayland
xorg.libX11
xorg.libXcursor
xorg.libXi
];
buildInputs = with pkgs; [
@@ -190,6 +203,9 @@
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.13" }
exmex = {version = "0.20.5", features = ["partial"]}
phf = { version = "0.11" }
exmex = {version = "0.17.5", features = ["partial"]}
[build-dependencies]
phf_codegen = { version = "0.13" }
phf_codegen = { version = "0.11" }
[package.metadata.cargo-all-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
const SUPPORTED_FUNCTIONS: [&str; 22] = [
"abs", "signum", "sin", "cos", "tan", "asin", "acos", "atan", "sinh", "cosh", "tanh", "floor",
"round", "ceil", "trunc", "fract", "exp", "sqrt", "cbrt", "ln", "log2", "log10",
"abs", "signum", "sin", "cos", "tan", "asin", "acos", "atan", "sinh", "cosh", "tanh", "floor",
"round", "ceil", "trunc", "fract", "exp", "sqrt", "cbrt", "ln", "log2", "log10",
];
fn main() {
println!("cargo:rerun-if-changed=src/*");
println!("cargo:rerun-if-changed=src/*");
generate_hashmap();
generate_hashmap();
}
fn generate_hashmap() {
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 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 string_hashmap =
compile_hashmap(SUPPORTED_FUNCTIONS.iter().map(|a| a.to_string()).collect());
let string_hashmap =
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() {
hashmap.entry(key, value);
}
for (key, value) in string_hashmap.iter() {
hashmap.entry(key, value);
}
write!(
&mut file,
"static COMPLETION_HASHMAP: phf::Map<&'static str, Hint> = {};",
hashmap.build()
)
.expect("Could not write to file");
write!(
&mut file,
"static COMPLETION_HASHMAP: phf::Map<&'static str, Hint> = {};",
hashmap.build()
)
.expect("Could not write to file");
write!(
&mut file,
"#[allow(dead_code)] pub const SUPPORTED_FUNCTIONS: [&str; {}] = {:?};",
SUPPORTED_FUNCTIONS.len(),
SUPPORTED_FUNCTIONS.to_vec()
)
.expect("Could not write to file");
write!(
&mut file,
"#[allow(dead_code)] pub const SUPPORTED_FUNCTIONS: [&str; {}] = {:?};",
SUPPORTED_FUNCTIONS.len(),
SUPPORTED_FUNCTIONS.to_vec()
)
.expect("Could not write to file");
}
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/autocomplete_hashmap.rs"
env!("CARGO_MANIFEST_DIR"),
"/src/autocomplete_hashmap.rs"
));

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,288 +1,204 @@
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(
&protected
.replace("pi", "π") // replace "pi" text with pi symbol
.replace("**", "^") // support alternate manner of expressing exponents
.chars()
.collect::<Vec<char>>(),
split,
)
.iter()
.map(|x| restore_function_names(x, &replacements))
.collect::<Vec<String>>()
split_function_chars(
&input
.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
.collect::<Vec<String>>()
}
// Specifies how to split a function
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum SplitType {
Multiplication,
Term,
Multiplication,
Term,
}
/// Used to store info about a character
struct BoolSlice {
closing_parens: bool,
open_parens: bool,
number: bool,
letter: bool,
variable: bool,
masked_num: bool,
masked_var: bool,
closing_parens: bool,
open_parens: bool,
number: bool,
letter: bool,
variable: bool,
masked_num: bool,
masked_var: bool,
}
impl BoolSlice {
const fn from_char(c: &char, prev_masked_num: bool, prev_masked_var: bool) -> Self {
let isnumber = c.is_ascii_digit();
let isvariable = is_variable(c);
Self {
closing_parens: *c == ')',
open_parens: *c == '(',
number: isnumber,
letter: c.is_ascii_alphabetic(),
variable: isvariable,
masked_num: match isnumber {
true => prev_masked_num,
false => false,
},
masked_var: match isvariable {
true => prev_masked_var,
false => false,
},
}
}
const fn from_char(c: &char, prev_masked_num: bool, prev_masked_var: bool) -> Self {
let isnumber = c.is_ascii_digit();
let isvariable = is_variable(c);
Self {
closing_parens: *c == ')',
open_parens: *c == '(',
number: isnumber,
letter: c.is_ascii_alphabetic(),
variable: isvariable,
masked_num: match isnumber {
true => prev_masked_num,
false => false,
},
masked_var: match isvariable {
true => prev_masked_var,
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 }
/// 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()
}
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
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;
}
}
/// 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 {
// Propagate number masking through consecutive digits
self.masked_num = true;
} else if other.masked_var && self.variable {
// Propagate variable masking through consecutive variables
self.masked_var = true;
} 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;
}
}
/// 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;
}
// 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
}
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)
{
return true;
} else {
return self.is_unmasked_number() && other.is_unmasked_variable();
}
}
}
// 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> {
// Catch some basic cases
match chars.len() {
0 => return Vec::new(),
1 => return vec![chars[0].to_string()],
_ => {}
}
// Catch some basic cases
match chars.len() {
0 => return Vec::new(),
1 => return vec![chars[0].to_string()],
_ => {}
}
// Resulting split-up data
let mut data: Vec<String> = std::vec::from_elem(chars[0].to_string(), 1);
// Resulting split-up data
let mut data: Vec<String> = std::vec::from_elem(chars[0].to_string(), 1);
// Setup first char here
let mut prev_char: BoolSlice = BoolSlice::from_char(&chars[0], false, false);
// Setup first char here
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
for c in chars.iter().skip(1) {
// Set data about current character
let mut curr_c = BoolSlice::from_char(c, prev_char.masked_num, prev_char.masked_var);
// Iterate through all chars excluding the first one
for c in chars.iter().skip(1) {
// Set data about current character
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
if curr_c.splitable(c, &prev_char, &split) {
// create new buffer
data.push(String::new());
last = unsafe { data.last_mut().unwrap_unchecked() };
}
// Append split
if curr_c.splitable(c, &prev_char, &split) {
// create new buffer
data.push(String::new());
last = unsafe { data.last_mut().unwrap_unchecked() };
}
// Exclude asterisks
if c != &'*' {
last.push(*c);
}
// Exclude asterisks
if c != &'*' {
last.push(*c);
}
// Move current character data to `prev_char`
prev_char = curr_c;
}
// Move current character data to `prev_char`
prev_char = curr_c;
}
data
data
}
#[cfg(test)]
fn assert_test(input: &str, expected: &[&str], split: SplitType) {
let output = split_function(input, split);
let expected_owned = expected
.iter()
.map(|&x| x.to_owned())
.collect::<Vec<String>>();
if output != expected_owned {
panic!(
"split type: {:?} of {} resulted in {:?} not {:?}",
split, input, output, expected
);
}
let output = split_function(input, split);
let expected_owned = expected
.iter()
.map(|&x| x.to_owned())
.collect::<Vec<String>>();
if output != expected_owned {
panic!(
"split type: {:?} of {} resulted in {:?} not {:?}",
split, input, output, expected
);
}
}
#[test]
fn split_function_test() {
assert_test(
"sin(x)cos(x)",
&["sin(x)", "cos(x)"],
SplitType::Multiplication,
);
assert_test(
"sin(x)cos(x)",
&["sin(x)", "cos(x)"],
SplitType::Multiplication,
);
assert_test(
"tanh(cos(x)xx)cos(x)",
&["tanh(cos(x)", "x", "x)", "cos(x)"],
SplitType::Multiplication,
);
assert_test(
"tanh(cos(x)xx)cos(x)",
&["tanh(cos(x)", "x", "x)", "cos(x)"],
SplitType::Multiplication,
);
assert_test(
"tanh(sin(cos(x)xsin(x)))",
&["tanh(sin(cos(x)", "x", "sin(x)))"],
SplitType::Multiplication,
);
assert_test(
"tanh(sin(cos(x)xsin(x)))",
&["tanh(sin(cos(x)", "x", "sin(x)))"],
SplitType::Multiplication,
);
// 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("sin(2pi)", &["sin(2", "π)"], SplitType::Multiplication);
assert_test("2sin(pi)", &["2", "sin(π)"], SplitType::Multiplication);
assert_test(
"2sin(3(4 + 5))",
&["2", "sin(3", "(4 + 5))"],
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(
"2sin(π) + 2cos(tau)",
&["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,
);
// 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("sin(2pi)", &["sin(2", "π)"], SplitType::Multiplication);
assert_test("2sin(pi)", &["2", "sin(π)"], SplitType::Multiplication);
assert_test(
"2sin(3(4 + 5))",
&["2", "sin(3", "(4 + 5))"],
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(
"2sin(π) + 2cos(tau)",
&["2", "sin(π) + 2", "cos(tau)"],
SplitType::Multiplication,
);
}

View File

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

30
shell.nix Normal file
View File

@@ -0,0 +1,30 @@
{
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,4 +1,18 @@
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,14 +1,16 @@
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, Points};
use egui_plot::{Bar, BarChart, PlotPoint, PlotUi};
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};
use std::{
fmt::{self, Debug},
hash::{Hash, Hasher},
};
/// Represents the possible variations of Riemann Sums
#[derive(PartialEq, Eq, Debug, Copy, Clone, Default)]
@@ -32,13 +34,19 @@ 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_derivative: bool,
pub nth_derviative: bool,
/// If the function is visible on the graph
pub visible: bool,
pub back_data: Vec<PlotPoint>,
pub integral_data: Option<(Vec<Bar>, f64)>,
@@ -55,16 +63,28 @@ 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);
self.visible.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.autocomplete.string)?;
let mut s = serializer.serialize_struct("FunctionEntry", 5)?;
s.serialize_field("raw_func_str", &self.raw_func_str)?;
s.serialize_field("integral", &self.integral)?;
s.serialize_field("derivative", &self.derivative)?;
s.serialize_field("curr_nth", &self.curr_nth)?;
s.serialize_field("visible", &self.visible)?;
s.end()
}
@@ -81,6 +101,12 @@ impl<'de> Deserialize<'de> for FunctionEntry {
integral: bool,
derivative: bool,
curr_nth: usize,
#[serde(default = "default_visible")]
visible: bool,
}
fn default_visible() -> bool {
true
}
let helper = Helper::deserialize(deserializer)?;
@@ -100,6 +126,7 @@ impl<'de> Deserialize<'de> for FunctionEntry {
new_func_entry.integral = helper.integral;
new_func_entry.derivative = helper.derivative;
new_func_entry.curr_nth = helper.curr_nth;
new_func_entry.visible = helper.visible;
Ok(new_func_entry)
}
@@ -110,9 +137,11 @@ impl Default for FunctionEntry {
fn default() -> FunctionEntry {
FunctionEntry {
function: BackingFunction::default(),
raw_func_str: String::new(),
integral: false,
derivative: false,
nth_derivative: false,
nth_derviative: false,
visible: true,
back_data: Vec::new(),
integral_data: None,
derivative_data: Vec::new(),
@@ -134,14 +163,14 @@ impl FunctionEntry {
pub fn settings_window(&mut self, ctx: &Context) {
let mut invalidate_nth = false;
egui::Window::new(format!("Settings: {}", self.func_str()))
egui::Window::new(format!("Settings: {}", self.raw_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_derivative,
&mut self.nth_derviative,
"Display Nth Derivative",
));
@@ -164,32 +193,13 @@ 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.func_str() {
if raw_func_str == self.raw_func_str {
return;
}
// 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) {
self.raw_func_str = raw_func_str.to_owned();
let processed_func = process_func_str(raw_func_str);
let new_func_result = BackingFunction::new(&processed_func);
@@ -215,16 +225,17 @@ impl FunctionEntry {
) -> (Vec<(f64, f64)>, f64) {
let step = (integral_max_x - integral_min_x) / (integral_num as f64);
let data: Vec<(f64, f64)> = step_helper(integral_num, integral_min_x, step)
// let sum_func = self.get_sum_func(sum);
let data2: Vec<(f64, f64)> = step_helper(integral_num, integral_min_x, step)
.into_iter()
.map(|x| {
let step_offset = step.copysign(x);
let x2 = x + step_offset;
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 (left_x, right_x) = if x.is_sign_positive() {
(x, x2)
} else {
(x2, x)
let (left_x, right_x) = match x.is_sign_positive() {
true => (x, x2),
false => (x2, x),
};
let y = match sum {
@@ -240,9 +251,9 @@ impl FunctionEntry {
.filter(|(_, y)| y.is_finite())
.collect();
let area = data.iter().map(|(_, y)| y * step).sum();
let area = data2.iter().map(move |(_, y)| y * step).sum();
(data, area)
(data2, area)
}
/// Helps with processing newton's method depending on level of derivative
@@ -254,33 +265,27 @@ impl FunctionEntry {
) -> Vec<PlotPoint> {
self.function.generate_derivative(derivative_level);
self.function.generate_derivative(derivative_level + 1);
let data_source = match derivative_level {
0 => self.back_data.as_slice(),
1 => self.derivative_data.as_slice(),
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),
),
_ => unreachable!(),
};
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)))
newtons_method_output
.into_iter()
.map(|x| PlotPoint::new(x, self.function.get(0, x)))
.collect()
}
@@ -312,17 +317,32 @@ impl FunctionEntry {
}
if self.back_data.is_empty() {
self.back_data = self.generate_plot_data(0, &resolution_iter);
debug_assert_eq!(self.back_data.len(), settings.plot_width + 1);
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;
}
if self.derivative_data.is_empty() {
self.derivative_data = self.generate_plot_data(1, &resolution_iter);
debug_assert_eq!(self.derivative_data.len(), settings.plot_width + 1);
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;
}
if self.nth_derivative && self.nth_derivative_data.is_none() {
let data = self.generate_plot_data(self.curr_nth, &resolution_iter);
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();
debug_assert_eq!(data.len(), settings.plot_width + 1);
self.nth_derivative_data = Some(data);
}
@@ -345,7 +365,7 @@ impl FunctionEntry {
self.clear_integral();
}
let threshold: f64 = f64::EPSILON;
let threshold: f64 = resolution / 2.0;
let x_range = settings.min_x..settings.max_x;
// Calculates extrema
@@ -367,7 +387,7 @@ impl FunctionEntry {
settings: &AppSettings,
main_plot_color: Color32,
) -> Option<f64> {
if self.test_result.is_some() | self.function.is_none() {
if self.test_result.is_some() | self.function.is_none() | !self.visible {
return None;
}
@@ -378,11 +398,7 @@ impl FunctionEntry {
let step = (settings.max_x - settings.min_x) / (settings.plot_width as f64);
debug_assert!(step > 0.0);
// 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
// Plot back data
if !self.back_data.is_empty() {
if self.integral && (step >= integral_step) {
plot_ui.line(
@@ -395,114 +411,50 @@ impl FunctionEntry {
.cloned()
.collect::<Vec<PlotPoint>>()
.to_line()
.stroke(epaint::Stroke::NONE)
.stroke((0.0, Color32::TRANSPARENT))
.color(Color32::from_rgb(4, 4, 255))
.fill(0.0),
);
}
// 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))
};
plot_ui.line(
self.back_data
.iter()
.filter(|p| !is_near_special(p))
.cloned()
.collect::<Vec<PlotPoint>>()
.clone()
.to_line()
} 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)));
.stroke((4.0, main_plot_color)),
);
}
// Plot derivative data
if self.derivative && !self.derivative_data.is_empty() {
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_ui.line(self.derivative_data.clone().to_line().color(Color32::GREEN));
}
// Plot extrema points
if settings.do_extrema && !self.extrema_data.is_empty() {
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_ui.points(
self.extrema_data
.clone()
.to_points()
.color(Color32::YELLOW)
.radius(5.0), // Radius of points of Extrema
);
}
// 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(
Points::new(name, vec![[point.x, point.y]])
.color(Color32::LIGHT_BLUE)
.radius(5.0),
);
}
plot_ui.points(
self.root_data
.clone()
.to_points()
.color(Color32::LIGHT_BLUE)
.radius(5.0), // Radius of points of Roots
);
}
if self.nth_derivative
&& let Some(ref nth_derivative) = self.nth_derivative_data
if self.nth_derviative
&& let Some(ref nth_derviative) = self.nth_derivative_data
{
plot_ui.line(nth_derivative.clone().to_line().color(Color32::DARK_RED));
plot_ui.line(nth_derviative.clone().to_line().color(Color32::DARK_RED));
}
// Plot integral data
@@ -510,7 +462,7 @@ impl FunctionEntry {
Some(integral_data) => {
if integral_step > step {
plot_ui.bar_chart(
BarChart::new("integral bar chart", integral_data.0.clone())
BarChart::new("integral", integral_data.0.clone())
.color(Color32::BLUE)
.width(integral_step),
);

View File

@@ -1,38 +1,63 @@
use crate::{function_entry::FunctionEntry, widgets::widgets_ontop};
use egui::{Button, Id, Key, Modifiers, Popup, PopupCloseBehavior, TextEdit, WidgetText};
use crate::{
consts::COLORS, function_entry::FunctionEntry, misc::random_u64, widgets::widgets_ontop,
};
use egui::{Button, Id, Key, Modifiers, Popup, TextEdit, WidgetText};
use emath::vec2;
use parsing::Movement;
use serde::{Deserialize, Serialize};
use serde::ser::SerializeStruct;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
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 d = Self::new();
d.push_empty();
d
let mut vec: Functions = Vec::with_capacity(COLORS.len());
vec.push((
Id::NULL.with(11414819524356497634_u64), // Random number here to avoid call to crate::misc::random_u64()
FunctionEntry::default(),
));
Self { functions: vec }
}
}
#[test]
fn func_manager_roundtrip_serdes() {
let mut func_manager = FunctionManager {
functions: Vec::new(),
};
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()
}
}
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()
);
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(),
})
}
}
/// Function that creates button that's used with the `button_area`
@@ -41,22 +66,30 @@ fn button_area_button<'a>(text: impl Into<WidgetText>) -> Button<'a> {
}
impl FunctionManager {
pub fn new() -> Self {
Self {
functions: Vec::new(),
}
#[inline]
fn get_hash(&self) -> u64 {
let mut hasher = DefaultHasher::new();
self.functions.hash(&mut hasher);
hasher.finish()
}
/// Displays function entries alongside returning whether or not functions have been modified
pub fn display_entries(&mut self, ui: &mut egui::Ui) -> bool {
let mut changed = false;
let initial_hash = self.get_hash();
let can_remove = self.functions.len() > 1;
let can_add = self.functions.len() < COLORS.len();
let num_functions = self.functions.len();
let available_width = ui.available_width();
let mut remove_i: Option<usize> = None;
let mut clone_i: Option<usize> = None;
let mut move_up_i: Option<usize> = None;
let mut move_down_i: Option<usize> = None;
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();
@@ -80,15 +113,24 @@ 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);
// Display error indicator with tooltip if there's a parsing error
if let Some(error) = function.get_test_result() {
ui.horizontal(|ui| {
ui.label(egui::RichText::new("").color(egui::Color32::YELLOW))
.on_hover_text(error);
ui.label(
egui::RichText::new(error)
.color(egui::Color32::LIGHT_RED)
.small(),
);
});
}
// 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() {
@@ -113,64 +155,39 @@ 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()
{
let mut clicked = false;
let selected_i = function.autocomplete.i;
let autocomplete_popup_id = Id::new("autocomplete popup");
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)
.clicked()
{
clicked = true;
function.autocomplete.i = i;
}
});
if let Some(popup_response) = Popup::menu(&re).show(|ui| {
hints.iter().enumerate().for_each(|(i, candidate)| {
if ui.selectable_label(i == selected_i, *candidate).clicked() {
clicked = true;
function.autocomplete.i = i;
}
});
if clicked {
}) && clicked
{
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 {
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() };
if movement == Movement::Complete
&& let Some(mut state) = TextEdit::load_state(ui.ctx(), te_id)
{
let ccursor = egui::text::CCursor::new(function.autocomplete.string.len());
state
.cursor
.set_char_range(Some(egui::text::CCursorRange::one(ccursor)));
TextEdit::store_state(ui.ctx(), te_id, state);
}
}
@@ -179,7 +196,7 @@ impl FunctionManager {
const BUTTONS_Y_OFFSET: f32 = 1.32;
const Y_OFFSET: f32 = crate::consts::FONT_SIZE * BUTTONS_Y_OFFSET;
widgets_ontop(ui, Id::new(i), &re, Y_OFFSET, |ui| {
widgets_ontop(ui, Id::NULL.with(i), &re, Y_OFFSET, |ui| {
ui.horizontal(|ui| {
// There's more than 1 function! Functions can now be deleted
if ui
@@ -190,6 +207,47 @@ impl FunctionManager {
remove_i = Some(i);
}
// Toggle visibility
function.visible.bitxor_assign(
ui.add(button_area_button(if function.visible {
"👁"
} else {
""
}))
.on_hover_text(match function.visible {
true => "Hide Function",
false => "Show Function",
})
.clicked(),
);
// Clone function
if ui
.add_enabled(can_add, button_area_button(""))
.on_hover_text("Clone Function")
.clicked()
{
clone_i = Some(i);
}
// Move up (only if not first)
if ui
.add_enabled(i > 0, button_area_button(""))
.on_hover_text("Move Up")
.clicked()
{
move_up_i = Some(i);
}
// Move down (only if not last)
if ui
.add_enabled(i < num_functions - 1, button_area_button(""))
.on_hover_text("Move Down")
.clicked()
{
move_down_i = Some(i);
}
ui.add_enabled_ui(function.is_some(), |ui| {
// Toggle integral being enabled or not
function.integral.bitxor_assign(
@@ -231,20 +289,51 @@ 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;
}
changed
// Clone function if the user requests it
if let Some(clone_i_unwrap) = clone_i {
let cloned = self.functions[clone_i_unwrap].1.clone();
self.push_cloned(cloned);
}
// Move function up if the user requests it
if let Some(i) = move_up_i
&& i > 0
{
self.functions.swap(i, i - 1);
}
// Move function down if the user requests it
if let Some(i) = move_down_i
&& i < self.functions.len() - 1
{
self.functions.swap(i, i + 1);
}
let final_hash = self.get_hash();
initial_hash != final_hash
}
/// Create and push new empty function entry
pub fn push_empty(&mut self) {
self.functions.push((
Id::new(format!("function #{}", self.functions.len() + 1)),
Id::NULL.with(random_u64().expect("unable to generate random id")),
FunctionEntry::default(),
));
}
/// Push a cloned function entry
pub fn push_cloned(&mut self, mut entry: FunctionEntry) {
// Reset settings_opened so the cloned function doesn't have settings open
entry.settings_opened = false;
self.functions.push((
Id::NULL.with(random_u64().expect("unable to generate random id")),
entry,
));
}
/// Detect if any functions are using integrals
pub fn any_using_integral(&self) -> bool {
self.functions.iter().any(|(_, func)| func.integral)

View File

@@ -6,82 +6,76 @@ 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, MathApp},
misc::{EguiHelper, newtons_method, option_vec_printer, step_helper},
math_app::AppSettings,
misc::{
EguiHelper, HashBytes, hashed_storage_create, hashed_storage_read, newtons_method,
option_vec_printer, step_helper,
},
unicode_helper::{to_chars_array, to_unicode_hash},
};
// WASM-specific setup
#[cfg(target_arch = "wasm32")]
mod wasm {
use super::math_app;
use eframe::WebRunner;
use lol_alloc::{FreeListAllocator, LockedAllocator};
use wasm_bindgen::prelude::*;
use web_sys::HtmlCanvasElement;
cfg_if::cfg_if! {
if #[cfg(target_arch = "wasm32")] {
use wasm_bindgen::prelude::*;
#[global_allocator]
static ALLOCATOR: LockedAllocator<FreeListAllocator> =
LockedAllocator::new(FreeListAllocator::new());
use lol_alloc::{FreeListAllocator, LockedAllocator};
#[global_allocator]
static ALLOCATOR: LockedAllocator<FreeListAllocator> = LockedAllocator::new(FreeListAllocator::new());
#[derive(Clone)]
#[wasm_bindgen]
pub struct WebHandle {
runner: WebRunner,
}
use eframe::WebRunner;
// use tracing::metadata::LevelFilter;
#[derive(Clone)]
#[wasm_bindgen]
pub struct WebHandle {
runner: WebRunner,
}
#[wasm_bindgen]
impl WebHandle {
/// Installs a panic hook, then returns.
#[allow(clippy::new_without_default)]
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
tracing_wasm::set_as_global_default();
Self {
runner: WebRunner::new(),
#[wasm_bindgen]
impl WebHandle {
/// Installs a panic hook, then returns.
#[allow(clippy::new_without_default)]
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
// eframe::WebLogger::init(LevelFilter::Debug).ok();
tracing_wasm::set_as_global_default();
Self {
runner: WebRunner::new(),
}
}
/// Call this once from JavaScript to start your app.
#[wasm_bindgen]
pub async fn start(&self, canvas: web_sys::HtmlCanvasElement) -> Result<(), wasm_bindgen::JsValue> {
self.runner
.start(
canvas,
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 window = web_sys::window().expect("no global window exists");
let document = window.document().expect("should have a document on window");
let canvas = document
.get_element_by_id("canvas")
.expect("should have a canvas element with id 'canvas'")
.dyn_into::<web_sys::HtmlCanvasElement>()
.expect("canvas element should be an HtmlCanvasElement");
let web_handle = WebHandle::new();
web_handle.start(canvas).await.unwrap()
}
/// Call this once from JavaScript to start your app.
#[wasm_bindgen]
pub async fn start(
&self,
canvas_id: HtmlCanvasElement,
) -> Result<(), wasm_bindgen::JsValue> {
self.runner
.start(
canvas_id,
eframe::WebOptions::default(),
Box::new(|cc| Ok(Box::new(math_app::MathApp::new(cc)))),
)
.await
}
}
#[wasm_bindgen(start)]
pub async fn start() {
tracing::info!("Starting...");
let document = web_sys::window()
.expect("no window")
.document()
.expect("no document");
let canvas = document
.get_element_by_id("canvas")
.expect("no canvas element")
.dyn_into::<HtmlCanvasElement>()
.expect("canvas is not an HtmlCanvasElement");
let web_handle = WebHandle::new();
web_handle
.start(canvas)
.await
.expect("failed to start web app");
}
}

View File

@@ -1,6 +1,17 @@
#[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<()> {
fn main() -> eframe::Result {
let subscriber = tracing_subscriber::FmtSubscriber::builder()
.with_max_level(tracing::Level::INFO)
.finish();
@@ -10,6 +21,6 @@ fn main() -> eframe::Result<()> {
eframe::run_native(
"(Yet-to-be-named) Graphing Software",
eframe::NativeOptions::default(),
Box::new(|cc| Ok(Box::new(ytbn_graphing_software::MathApp::new(cc)))),
Box::new(|cc| Ok(Box::new(math_app::MathApp::new(cc)))),
)
}

View File

@@ -1,21 +1,21 @@
use crate::{
consts::{COLORS, DEFAULT_INTEGRAL_NUM, DEFAULT_MAX_X, DEFAULT_MIN_X},
consts::{BUILD_INFO, COLORS, DEFAULT_INTEGRAL_NUM, DEFAULT_MAX_X, DEFAULT_MIN_X, build},
function_entry::Riemann,
function_manager::FunctionManager,
misc::option_vec_printer,
widgets::toggle_button,
misc::{EguiHelper, find_intersections, option_vec_printer},
};
use eframe::App;
use egui::{
Button, CentralPanel, Color32, ComboBox, Context, CornerRadius, DragValue, Frame, Key, Layout,
SidePanel, TopBottomPanel, Ui, Vec2, Window,
Button, CentralPanel, Color32, ComboBox, Context, DragValue, Frame, Key, Layout, Panel, Ui,
Vec2, Window,
};
use egui_plot::Plot;
use emath::{Align, Align2};
use epaint::Margin;
use itertools::Itertools;
use std::io::Read;
use epaint::{CornerRadius, Margin};
use std::{io::Read, ops::BitXorAssign};
use web_time::Instant;
/// Stores current settings/state of [`MathApp`]
#[derive(Copy, Clone)]
@@ -47,6 +47,9 @@ pub struct AppSettings {
/// Stores whether or not displaying roots is enabled
pub do_roots: bool,
/// Stores whether or not displaying intersections between functions is enabled
pub do_intersections: bool,
/// Stores current plot pixel width
pub plot_width: usize,
}
@@ -64,6 +67,7 @@ impl Default for AppSettings {
integral_num: DEFAULT_INTEGRAL_NUM,
do_extrema: true,
do_roots: true,
do_intersections: true,
plot_width: 0,
}
}
@@ -74,6 +78,9 @@ struct Opened {
/// Help window
pub help: bool,
/// Info window
pub info: bool,
/// Sidepanel
pub side_panel: bool,
@@ -85,6 +92,7 @@ impl Default for Opened {
fn default() -> Opened {
Self {
help: false,
info: false,
side_panel: true,
welcome: true,
}
@@ -96,14 +104,17 @@ pub struct MathApp {
/// Stores vector of functions
functions: FunctionManager,
/// Contains the list of Areas calculated
areas: Option<String>,
/// 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>),
/// Stores opened windows/elements for later reference
opened: Opened,
/// Stores settings (pretty self-explanatory)
settings: AppSettings,
/// Stores intersection points between functions
intersections: Vec<egui_plot::PlotPoint>,
}
#[cfg(target_arch = "wasm32")]
@@ -124,50 +135,115 @@ const DATA_NAME: &str = "YTBN-DECOMPRESSED";
#[cfg(target_arch = "wasm32")]
const FUNC_NAME: &str = "YTBN-FUNCTIONS";
/// Load functions from localStorage (WASM only)
#[cfg(target_arch = "wasm32")]
fn load_functions() -> Option<FunctionManager> {
let data = get_localstorage().get_item(FUNC_NAME).ok()??;
let func_data = crate::misc::hashed_storage_read(&data)?;
tracing::info!("Reading previous function data");
match bincode::deserialize(&func_data) {
Ok(Some(function_manager)) => Some(function_manager),
_ => {
tracing::info!("Unable to load functionManager instance");
None
}
}
}
fn decompress_fonts() -> epaint::text::FontDefinitions {
let mut data = Vec::new();
ruzstd::decoding::StreamingDecoder::new(
const { include_bytes!(concat!(env!("OUT_DIR"), "/compressed_data")).as_slice() },
)
.expect("unable to decode compressed data")
.read_to_end(&mut data)
.expect("unable to read compressed data");
bincode::deserialize(data.as_slice()).expect("unable to deserialize bincode")
}
impl MathApp {
#[allow(dead_code)] // This is used lol
/// Create new instance of [`MathApp`] and return it
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
tracing::info!("Initializing...");
#[cfg(threading)]
tracing::info!("Threading: Enabled");
#[cfg(target_arch = "wasm32")]
tracing::info!("Web Info: {:?}", &cc.integration_info.web_info);
#[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.to_vec());
} else {
None
}
}
fn load_functions() -> Option<FunctionManager> {
let data = get_localstorage().get_item(FUNC_NAME).ok()??;
if crate::misc::HASH_LENGTH >= data.len() {
return None;
}
// TODO: stabilize FunctionManager serialize so it can persist across builds
let (commit, func_data) = crate::misc::hashed_storage_read(&data)?;
let func_data: &[u8] = &func_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::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_arch = "wasm32")]
{
tracing::info!("Setting decompression cache");
// Convert SHORT_COMMIT string to fixed-size byte array
let commit_bytes = build::SHORT_COMMIT.as_bytes();
let mut commit: crate::misc::HashBytes = [0u8; crate::misc::HASH_LENGTH];
let len = commit_bytes.len().min(crate::misc::HASH_LENGTH);
commit[..len].copy_from_slice(&commit_bytes[..len]);
let saved_data = crate::misc::hashed_storage_create(commit, &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...");
// Initialize fonts
// This used to be in the `update` method, but (after a ton of digging) this actually caused OOMs. that was a pain to debug
cc.egui_ctx.set_fonts(decompress_fonts());
cc.egui_ctx.set_fonts({
#[cfg(target_arch = "wasm32")]
if let Some(Ok(data)) =
get_storage_decompressed().map(|data| bincode::deserialize(data.as_slice()))
{
data
} else {
decompress_fonts()
}
tracing::info!("Initialized!");
#[cfg(not(target_arch = "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());
Self {
#[cfg(target_arch = "wasm32")]
@@ -176,9 +252,10 @@ impl MathApp {
#[cfg(not(target_arch = "wasm32"))]
functions: FunctionManager::default(),
areas: None,
last_info: (None, None),
opened: Opened::default(),
settings: AppSettings::default(),
intersections: Vec::new(),
}
}
@@ -186,166 +263,185 @@ impl MathApp {
fn side_panel(&mut self, ctx: &Context) {
// Side Panel which contains vital options to the operation of the application
// (such as adding functions and other options)
SidePanel::left("side_panel")
.resizable(false)
.show(ctx, |ui| {
let any_using_integral = self.functions.any_using_integral();
let prev_sum = self.settings.riemann_sum;
// ComboBox for selecting what Riemann sum type to use
ui.add_enabled_ui(any_using_integral, |ui| {
let spacing_mut = ui.spacing_mut();
Panel::left("side_panel").resizable(false).show(ctx, |ui| {
let any_using_integral = self.functions.any_using_integral();
let prev_sum = self.settings.riemann_sum;
// ComboBox for selecting what Riemann sum type to use
ui.add_enabled_ui(any_using_integral, |ui| {
let spacing_mut = ui.spacing_mut();
spacing_mut.item_spacing.x = 1.0;
spacing_mut.interact_size *= 0.5;
ComboBox::from_label("Riemann Sum")
.selected_text(self.settings.riemann_sum.to_string())
.show_ui(ui, |ui| {
ui.selectable_value(
&mut self.settings.riemann_sum,
Riemann::Left,
"Left",
);
ui.selectable_value(
&mut self.settings.riemann_sum,
Riemann::Middle,
"Middle",
);
ui.selectable_value(
&mut self.settings.riemann_sum,
Riemann::Right,
"Right",
);
});
let riemann_changed = prev_sum != self.settings.riemann_sum;
let min_x_old = self.settings.integral_min_x;
let max_x_old = self.settings.integral_max_x;
let (min_x_changed, max_x_changed) = ui
.horizontal(|ui: &mut Ui| {
// let spacing_mut = ui.spacing_mut();
// spacing_mut.item_spacing = Vec2::new(1.0, 0.0);
// spacing_mut.interact_size *= 0.5;
ui.label("Integral: [");
let min_x_changed = ui
.add(DragValue::new(&mut self.settings.integral_min_x))
.changed();
ui.label(",");
let max_x_changed = ui
.add(DragValue::new(&mut self.settings.integral_max_x))
.changed();
ui.label("]");
(min_x_changed, max_x_changed)
})
.inner;
// Checks integral bounds, and if they are invalid, fix them
if self.settings.integral_min_x >= self.settings.integral_max_x {
if max_x_changed {
self.settings.integral_max_x = max_x_old;
} else if min_x_changed {
self.settings.integral_min_x = min_x_old;
} else {
// No clue how this would happen, but just in case
self.settings.integral_min_x = DEFAULT_MIN_X;
self.settings.integral_max_x = DEFAULT_MAX_X;
}
}
// Number of Rectangles for Riemann sum
let integral_num_changed = ui
.horizontal(|ui| {
let spacing_mut = ui.spacing_mut();
spacing_mut.item_spacing.x = 1.5;
ui.label("Interval:");
ui.add(DragValue::new(&mut self.settings.integral_num))
.changed()
})
.inner;
if integral_num_changed {
self.settings.integral_num = self.settings.integral_num.clamp(0, 500000);
}
self.settings.integral_changed = any_using_integral
&& (max_x_changed | min_x_changed | integral_num_changed | riemann_changed);
});
ui.horizontal(|ui| {
toggle_button(
ui,
&mut self.settings.do_extrema,
"Extrema",
"Disable Displaying Extrema",
"Display Extrema",
);
toggle_button(
ui,
&mut self.settings.do_roots,
"Roots",
"Disable Displaying Roots",
"Display Roots",
);
});
if self.functions.display_entries(ui) {
#[cfg(target_arch = "wasm32")]
{
tracing::info!("Saving function data");
let saved_data = crate::misc::hashed_storage_create(
&bincode::serialize(&self.functions)
.expect("unable to deserialize functions"),
spacing_mut.item_spacing.x = 1.0;
spacing_mut.interact_size *= 0.5;
ComboBox::from_label("Riemann Sum")
.selected_text(self.settings.riemann_sum.to_string())
.show_ui(ui, |ui| {
ui.selectable_value(&mut self.settings.riemann_sum, Riemann::Left, "Left");
ui.selectable_value(
&mut self.settings.riemann_sum,
Riemann::Middle,
"Middle",
);
// tracing::info!("Bytes: {}", saved_data.len());
get_localstorage()
.set_item(FUNC_NAME, &saved_data)
.expect("failed to set local function storage");
}
}
// Only render if there's enough space
if ui.available_height() > crate::consts::FONT_SIZE {
ui.with_layout(Layout::bottom_up(Align::Min), |ui| {
// Contents put in reverse order from bottom to top due to the 'buttom_up' layout
// Hyperlink to project's github
ui.hyperlink_to(
"I'm Open Source!",
"https://github.com/Titaniumtown/YTBN-Graphing-Software",
ui.selectable_value(
&mut self.settings.riemann_sum,
Riemann::Right,
"Right",
);
});
let riemann_changed = prev_sum != self.settings.riemann_sum;
let min_x_old = self.settings.integral_min_x;
let max_x_old = self.settings.integral_max_x;
let (min_x_changed, max_x_changed) = ui
.horizontal(|ui: &mut Ui| {
// let spacing_mut = ui.spacing_mut();
// spacing_mut.item_spacing = Vec2::new(1.0, 0.0);
// spacing_mut.interact_size *= 0.5;
ui.label("Integral: [");
let min_x_changed = ui
.add(DragValue::new(&mut self.settings.integral_min_x))
.changed();
ui.label(",");
let max_x_changed = ui
.add(DragValue::new(&mut self.settings.integral_max_x))
.changed();
ui.label("]");
(min_x_changed, max_x_changed)
})
.inner;
// Checks integral bounds, and if they are invalid, fix them
if self.settings.integral_min_x >= self.settings.integral_max_x {
if max_x_changed {
self.settings.integral_max_x = max_x_old;
} else if min_x_changed {
self.settings.integral_min_x = min_x_old;
} else {
// No clue how this would happen, but just in case
self.settings.integral_min_x = DEFAULT_MIN_X;
self.settings.integral_max_x = DEFAULT_MAX_X;
}
}
// Number of Rectangles for Riemann sum
let integral_num_changed = ui
.horizontal(|ui| {
let spacing_mut = ui.spacing_mut();
spacing_mut.item_spacing.x = 1.5;
ui.label("Interval:");
ui.add(DragValue::new(&mut self.settings.integral_num))
.changed()
})
.inner;
if integral_num_changed {
self.settings.integral_num = self.settings.integral_num.clamp(0, 500000);
}
self.settings.integral_changed = any_using_integral
&& (max_x_changed | min_x_changed | integral_num_changed | riemann_changed);
});
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(),
);
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(),
);
self.settings.do_intersections.bitxor_assign(
ui.add(Button::new("Intersections"))
.on_hover_text(match self.settings.do_intersections {
true => "Disable Displaying Intersections",
false => "Display Intersections between functions",
})
.clicked(),
);
});
if self.functions.display_entries(ui) {
#[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,
&bincode::serialize(&self.functions)
.expect("unable to deserialize functions"),
);
// tracing::info!("Bytes: {}", saved_data.len());
get_localstorage()
.set_item(FUNC_NAME, &saved_data)
.expect("failed to set local function storage");
}
}
// Only render if there's enough space
if ui.available_height() > crate::consts::FONT_SIZE {
ui.with_layout(Layout::bottom_up(Align::Min), |ui| {
// Contents put in reverse order from bottom to top due to the 'buttom_up' layout
// Hyperlink to project's github
ui.hyperlink_to(
"I'm Open Source!",
"https://github.com/Titaniumtown/YTBN-Graphing-Software",
);
});
}
});
}
}
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::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
if ctx.input_mut(|x| x.consume_key(egui::Modifiers::NONE, Key::H)) {
self.opened.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
TopBottomPanel::top("top_bar").show(ctx, |ui| {
Panel::top("top_bar").show(ctx, |ui| {
ui.horizontal(|ui| {
// Button in top bar to toggle showing the side panel
toggle_button(
ui,
&mut self.opened.side_panel,
"Panel",
"Hide Side Panel",
"Show 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(),
);
// Button to add a new function
@@ -361,16 +457,27 @@ impl App for MathApp {
}
// Toggles opening the Help window
toggle_button(
ui,
&mut self.opened.help,
"Help",
"Close Help Window",
"Open 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(),
);
// Display Area and time of last frame
if let Some(ref area) = self.areas {
if let Some(ref area) = self.last_info.0 {
ui.label(area);
}
});
@@ -423,12 +530,26 @@ 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);
}
// Central panel which contains the central plot (or an error created when parsing)
// Central panel which contains the central plot
CentralPanel::default()
.frame(Frame {
inner_margin: Margin::ZERO,
@@ -438,31 +559,13 @@ impl App for MathApp {
..Frame::NONE
})
.show(ctx, |ui| {
// Display an error if it exists
let errors_formatted: String = self
.functions
.get_entries()
.iter()
.map(|(_, func)| func.get_test_result())
.enumerate()
.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() {
ui.centered_and_justified(|ui| {
ui.heading(errors_formatted);
});
return;
}
let available_width: usize = (ui.available_width() as usize) + 1; // Used in later logic
let width_changed = available_width != self.settings.plot_width;
self.settings.plot_width = available_width;
// Create and setup plot
Plot::new("plot")
.set_margin_fraction(Vec2::ZERO)
.set_margin_fraction(emath::Vec2::ZERO)
.data_aspect(1.0)
.include_y(0)
.show(ui, |plot_ui| {
@@ -500,12 +603,50 @@ impl App for MathApp {
})
.collect();
self.areas = if area.iter().any(|e| e.is_some()) {
// Calculate and display intersections between functions
if self.settings.do_intersections {
let entries = self.functions.get_entries();
let visible_entries: Vec<_> = entries
.iter()
.filter(|(_, f)| f.visible && f.is_some())
.collect();
// Clear previous intersections
self.intersections.clear();
// Find intersections between all pairs of visible functions
for i in 0..visible_entries.len() {
for j in (i + 1)..visible_entries.len() {
let (_, func1) = visible_entries[i];
let (_, func2) = visible_entries[j];
let mut intersections =
find_intersections(&func1.back_data, &func2.back_data);
self.intersections.append(&mut intersections);
}
}
// Display intersection points
if !self.intersections.is_empty() {
plot_ui.points(
self.intersections
.clone()
.to_points()
.color(Color32::from_rgb(255, 105, 180)) // Hot pink for visibility
.radius(6.0),
);
}
}
self.last_info.0 = 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: {}us", a.elapsed().as_micros()));
}
}

View File

@@ -1,5 +1,3 @@
use base64::Engine;
use base64::engine::general_purpose;
use egui_plot::{Line, PlotPoint, PlotPoints, Points};
use emath::Pos2;
use itertools::Itertools;
@@ -91,7 +89,9 @@ 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)
.filter_map(|x| newtons_method(f, f_1, x, range, threshold))
.map(|x| newtons_method(f, f_1, x, range, threshold))
.filter(|x| x.is_some())
.map(|x| unsafe { x.unwrap_unchecked() })
.collect()
}
@@ -149,14 +149,58 @@ 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 hashed_storage_create(data: &[u8]) -> String {
general_purpose::STANDARD.encode(data)
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 {
// Use base64 encoding to safely store binary data in localStorage
let combined: Vec<u8> = [hashbytes.to_vec(), data.to_vec()].concat();
base64::encode(combined)
}
#[allow(dead_code)]
pub fn hashed_storage_read(data: &str) -> Option<Vec<u8>> {
general_purpose::STANDARD.decode(data).ok()
pub fn hashed_storage_read(data: &str) -> Option<(HashBytes, Vec<u8>)> {
// Decode base64 data
let decoded = base64::decode(data).ok()?;
// Make sure data is long enough to decode
if HASH_LENGTH >= decoded.len() {
return None;
}
// Extract hash and data
let mut hash: HashBytes = [0u8; HASH_LENGTH];
hash.copy_from_slice(&decoded[..HASH_LENGTH]);
let data_part = decoded[HASH_LENGTH..].to_vec();
Some((hash, data_part))
}
/// 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::fill(&mut buf)?;
// Merge buffer into u64
Ok(u64::from_be_bytes(buf))
}
include!(concat!(env!("OUT_DIR"), "/valid_chars.rs"));
@@ -164,3 +208,47 @@ include!(concat!(env!("OUT_DIR"), "/valid_chars.rs"));
pub fn is_valid_char(c: char) -> bool {
c.is_alphanumeric() | VALID_EXTRA_CHARS.contains(&c)
}
/// Find intersection points between two functions given their plotted data
/// Returns a vector of PlotPoints where the functions intersect
pub fn find_intersections(data1: &[PlotPoint], data2: &[PlotPoint]) -> Vec<PlotPoint> {
if data1.is_empty() || data2.is_empty() || data1.len() != data2.len() {
return Vec::new();
}
// Calculate difference between functions at each x point
let differences: Vec<(f64, f64)> = data1
.iter()
.zip(data2.iter())
.filter(|(p1, p2)| p1.y.is_finite() && p2.y.is_finite())
.map(|(p1, p2)| (p1.x, p1.y - p2.y))
.collect();
// Find where sign changes (intersection points)
differences
.iter()
.tuple_windows()
.filter(|((_, diff1), (_, diff2))| diff1.signum() != diff2.signum())
.map(|((x1, diff1), (x2, diff2))| {
// Linear interpolation to find approximate x of intersection
let t = diff1.abs() / (diff1.abs() + diff2.abs());
let x = x1 + t * (x2 - x1);
// Find corresponding y values and average them for the intersection point
// We need to interpolate y values from both functions
let y1_at_x1 = data1
.iter()
.find(|p| (p.x - x1).abs() < f64::EPSILON)
.map(|p| p.y)
.unwrap_or(0.0);
let y1_at_x2 = data1
.iter()
.find(|p| (p.x - x2).abs() < f64::EPSILON)
.map(|p| p.y)
.unwrap_or(0.0);
let y = y1_at_x1 + t * (y1_at_x2 - y1_at_x1);
PlotPoint::new(x, y)
})
.collect()
}

View File

@@ -1,210 +0,0 @@
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,6 +1,5 @@
use crate::misc::Offset;
use egui::{Button, Id, InnerResponse, Ui};
use std::ops::BitXorAssign;
use egui::{Id, InnerResponse};
/// Creates an area ontop of a widget with an y offset
pub fn widgets_ontop<R>(
@@ -16,19 +15,3 @@ 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

@@ -19,6 +19,7 @@ fn app_settings_constructor(
integral_num,
do_extrema: false,
do_roots: false,
do_intersections: false,
plot_width: pixel_width,
}
}
@@ -84,7 +85,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: {:?}", DERIVATIVE_TARGET, a);
panic!("Expected: {:?}\nGot: {:?}", a, DERIVATIVE_TARGET);
}
}
@@ -98,7 +99,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: {:?}", BACK_TARGET, a_1);
panic!("Expected: {:?}\nGot: {:?}", a_1, BACK_TARGET);
}
}
}
@@ -131,7 +132,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: {:?}", b, a);
panic!("Expected: {:?}\nGot: {:?}", a, b);
}
}
@@ -160,7 +161,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: {:?}", b_1, a_1);
panic!("Expected: {:?}\nGot: {:?}", a_1, b_1);
}
}
}
@@ -193,7 +194,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: {:?}", b, a);
panic!("Expected: {:?}\nGot: {:?}", a, b);
}
}
@@ -222,7 +223,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: {:?}", b_1, a_1);
panic!("Expected: {:?}\nGot: {:?}", a_1, b_1);
}
}
}
@@ -230,7 +231,7 @@ fn do_test(sum: Riemann, area_target: f64) {
{
function.update_string("sin(x)");
assert!(function.get_test_result().is_none());
assert_eq!(function.func_str(), "sin(x)");
assert_eq!(&function.raw_func_str, "sin(x)");
function.integral = false;
function.derivative = false;
@@ -271,260 +272,3 @@ 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,6 +60,34 @@ 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};
@@ -115,8 +143,9 @@ fn option_vec_printer() {
fn newtons_method() {
use parsing::BackingFunction;
use parsing::FlatExWrapper;
use parsing::process_func_str;
fn get_flatexwrapper(func: &str) -> FlatExWrapper {
let mut backing_func = BackingFunction::new(func).unwrap();
let backing_func = BackingFunction::new(&process_func_str(func)).unwrap();
backing_func.get_function_derivative(0).clone()
}
@@ -124,7 +153,7 @@ fn newtons_method() {
let data = newtons_method(
&get_flatexwrapper("x^2 -1"),
&get_flatexwrapper("2*x"),
&get_flatexwrapper("2x"),
3.0,
&(0.0..5.0),
f64::EPSILON,

View File

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