Compare commits

..

6 Commits

Author SHA1 Message Date
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 2082 additions and 2910 deletions

1
.gitignore vendored
View File

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

1199
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,72 @@ 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"
base64 = "0.22"
log = "0.4"
[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
wasm-bindgen-futures = "=0.4.54"
getrandom = { version = "0.3", features = ["wasm_js"] }
wasm-bindgen-futures = "0.4"
[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,19 +1,18 @@
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>>,
}
impl<'a> FlamegraphProfiler<'a> {
#[allow(dead_code)]
/// Create a new flamegraph profiler with the given sampling frequency
pub fn new(frequency: c_int) -> Self {
FlamegraphProfiler {
frequency,
@@ -22,7 +21,7 @@ impl<'a> FlamegraphProfiler<'a> {
}
}
impl<'a> Profiler for FlamegraphProfiler<'a> {
impl Profiler for FlamegraphProfiler<'_> {
fn start_profiling(&mut self, _benchmark_id: &str, _benchmark_dir: &Path) {
self.active_profiler = Some(ProfilerGuard::new(self.frequency).unwrap());
}
@@ -42,59 +41,3 @@ impl<'a> Profiler for FlamegraphProfiler<'a> {
}
}
}
#[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": 1764730109,
"narHash": "sha256-vNETC0oq6tKJKF8KOGQKKIWRom38m0RwGgi3MPBrRx8=",
"owner": "Titaniumtown",
"repo": "egui",
"rev": "b63c21d70150f1b414370f0f9a8af56e886662f4",
"type": "github"
},
"original": {
"owner": "Titaniumtown",
"repo": "egui",
"rev": "b63c21d70150f1b414370f0f9a8af56e886662f4",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,

113
flake.nix
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,17 +8,14 @@
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
simon-egui = {
url = "github:Titaniumtown/egui/b63c21d70150f1b414370f0f9a8af56e886662f4";
flake = false;
};
};
outputs =
{
self,
nixpkgs,
flake-utils,
rust-overlay,
}:
flake-utils.lib.eachDefaultSystem (
system:
outputs = { self, nixpkgs, flake-utils, rust-overlay, simon-egui }:
flake-utils.lib.eachDefaultSystem (system:
let
overlays = [ (import rust-overlay) ];
pkgs = import nixpkgs {
@@ -26,7 +23,7 @@
};
# Use nightly rust with wasm32 target
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
rustToolchain = pkgs.rust-bin.nightly."2025-05-01".default.override {
targets = [ "wasm32-unknown-unknown" ];
};
@@ -35,17 +32,55 @@
rustc = rustToolchain;
};
# Build wasm-bindgen-cli matching the version in Cargo.lock (0.2.106)
wasm-bindgen-cli = rustPlatform.buildRustPackage rec {
pname = "wasm-bindgen-cli";
version = "0.2.106";
src = pkgs.fetchCrate {
inherit pname version;
hash = "sha256-M6WuGl7EruNopHZbqBpucu4RWz44/MSdv6f0zkYw+44=";
};
cargoHash = "sha256-ElDatyOwdKwHg3bNH/1pcxKI7LXkhsotlDPQjiLHBwA=";
nativeBuildInputs = [ pkgs.pkg-config ];
buildInputs = [ pkgs.openssl ] ++ pkgs.lib.optionals pkgs.stdenv.isDarwin [
pkgs.curl
pkgs.darwin.apple_sdk.frameworks.Security
];
# Tests require network access
doCheck = false;
};
# 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 +95,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 +115,7 @@
runHook postInstall
'';
doCheck = true;
doCheck = false;
};
# Final web package with wasm-bindgen processing
@@ -109,9 +125,9 @@
src = ./.;
nativeBuildInputs = with pkgs; [
nativeBuildInputs = [
wasm-bindgen-cli
binaryen
pkgs.binaryen
];
buildPhase = ''
@@ -126,7 +142,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 +171,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 +192,7 @@
rustToolchain
wasm-bindgen-cli
binaryen
python3Packages.fonttools
rust-analyzer
pkg-config
clang
@@ -177,6 +201,9 @@
libxkbcommon
libGL
wayland
xorg.libX11
xorg.libXcursor
xorg.libXi
];
buildInputs = with pkgs; [
@@ -184,14 +211,14 @@
zstd
];
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath (
with pkgs;
[
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath (with pkgs; [
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

@@ -13,19 +13,13 @@ pub enum Movement {
}
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)]
@@ -36,9 +30,7 @@ pub struct AutoComplete<'a> {
}
impl<'a> Default for AutoComplete<'a> {
fn default() -> AutoComplete<'a> {
AutoComplete::EMPTY
}
fn default() -> AutoComplete<'a> { AutoComplete::EMPTY }
}
impl<'a> AutoComplete<'a> {

View File

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

View File

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

View File

@@ -1,61 +1,17 @@
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
&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| restore_function_names(x, &replacements))
.map(|x| x.replace('\u{1fc93}', "exp")) // Convert back to `exp` text
.collect::<Vec<String>>()
}
@@ -98,81 +54,50 @@ impl BoolSlice {
}
}
const fn is_unmasked_variable(&self) -> bool {
self.variable && !self.masked_var
}
const fn is_unmasked_variable(&self) -> bool { self.variable && !self.masked_var }
const fn is_unmasked_number(&self) -> bool {
self.number && !self.masked_num
}
/// Returns true if this char is a function name letter (not a standalone variable)
const fn is_function_letter(&self) -> bool {
self.letter && !self.is_unmasked_variable()
}
/// Returns true if this is a "term" - something that can be multiplied
const fn is_term(&self) -> bool {
self.is_unmasked_number() || self.is_unmasked_variable() || self.letter
}
const fn is_unmasked_number(&self) -> bool { self.number && !self.masked_num }
const fn calculate_mask(&mut self, other: &BoolSlice) {
if other.masked_num && self.number {
// Propagate number masking through consecutive digits
// 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 {
// Propagate variable masking through consecutive variables
// 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.is_function_letter() {
// After a function letter, mask following numbers/variables as part of function name
} else if other.letter && !other.is_unmasked_variable() {
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 == '*' {
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();
}
// 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
}
}
@@ -276,13 +201,4 @@ fn split_function_test() {
&["2", "sin(π) + 2", "cos(tau)"],
SplitType::Multiplication,
);
// Test that exp() function is properly handled (not split into e*xp)
assert_test("exp(x)", &["exp(x)"], SplitType::Multiplication);
assert_test("2exp(x)", &["2", "exp(x)"], SplitType::Multiplication);
assert_test(
"exp(x)sin(x)",
&["exp(x)", "sin(x)"],
SplitType::Multiplication,
);
}

View File

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

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,36 +265,30 @@ 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),
)
newtons_method_output
.into_iter()
.map(|x| PlotPoint::new(x, self.function.get(0, x)))
.collect()
}
/// Generates plot data for a given derivative level over the resolution iterator
fn generate_plot_data(&mut self, derivative: usize, resolution_iter: &[f64]) -> Vec<PlotPoint> {
if derivative > 0 {
self.function.generate_derivative(derivative);
}
resolution_iter
.iter()
.map(|&x| PlotPoint::new(x, self.function.get(derivative, x)))
.collect()
}
/// Does the calculations and stores results in `self`
pub fn calculate(
&mut self,
@@ -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]])
self.extrema_data
.clone()
.to_points()
.color(Color32::YELLOW)
.radius(5.0),
.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]])
self.root_data
.clone()
.to_points()
.color(Color32::LIGHT_BLUE)
.radius(5.0),
.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,71 @@
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::{create_id, get_u64_id, 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((
create_id(11414819524356497634), // 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)| (get_u64_id(*id), func.clone()))
.collect::<Vec<(u64, 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<(u64, FunctionEntry)>);
let helper = Helper::deserialize(deserializer)?;
Ok(FunctionManager {
functions: helper
.0
.iter()
.cloned()
.map(|(id, func)| (create_id(id), func))
.collect::<Vec<(Id, FunctionEntry)>>(),
})
}
}
/// Function that creates button that's used with the `button_area`
@@ -41,22 +74,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 +121,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 +163,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| {
if let Some(popup_response) = Popup::menu(&re).show(|ui| {
hints.iter().enumerate().for_each(|(i, candidate)| {
if ui
.selectable_label(i == function.autocomplete.i, *candidate)
.clicked()
{
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 +204,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, create_id(i as u64), &re, Y_OFFSET, |ui| {
ui.horizontal(|ui| {
// There's more than 1 function! Functions can now be deleted
if ui
@@ -190,6 +215,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 +297,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)),
create_id(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((
create_id(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,30 +6,29 @@ 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};
cfg_if::cfg_if! {
if #[cfg(target_arch = "wasm32")] {
use wasm_bindgen::prelude::*;
use web_sys::HtmlCanvasElement;
use lol_alloc::{FreeListAllocator, LockedAllocator};
#[global_allocator]
static ALLOCATOR: LockedAllocator<FreeListAllocator> =
LockedAllocator::new(FreeListAllocator::new());
static ALLOCATOR: LockedAllocator<FreeListAllocator> = LockedAllocator::new(FreeListAllocator::new());
use eframe::WebRunner;
// use tracing::metadata::LevelFilter;
#[derive(Clone)]
#[wasm_bindgen]
pub struct WebHandle {
@@ -42,7 +41,9 @@ mod wasm {
#[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(),
}
@@ -50,13 +51,10 @@ mod wasm {
/// Call this once from JavaScript to start your app.
#[wasm_bindgen]
pub async fn start(
&self,
canvas_id: HtmlCanvasElement,
) -> Result<(), wasm_bindgen::JsValue> {
pub async fn start(&self, canvas: web_sys::HtmlCanvasElement) -> Result<(), wasm_bindgen::JsValue> {
self.runner
.start(
canvas_id,
canvas,
eframe::WebOptions::default(),
Box::new(|cc| Ok(Box::new(math_app::MathApp::new(cc)))),
)
@@ -68,20 +66,16 @@ mod wasm {
pub async fn start() {
tracing::info!("Starting...");
let document = web_sys::window()
.expect("no window")
.document()
.expect("no document");
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("no canvas element")
.dyn_into::<HtmlCanvasElement>()
.expect("canvas is not an HtmlCanvasElement");
.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
.expect("failed to start web app");
web_handle.start(canvas).await.unwrap()
}
}
}

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,114 @@ 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)?;
impl MathApp {
#[allow(dead_code)] // This is used lol
/// Create new instance of [`MathApp`] and return it
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
#[cfg(threading)]
tracing::info!("Threading: Enabled");
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");
#[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 decompress_fonts() -> epaint::text::FontDefinitions {
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)?;
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();
ruzstd::decoding::StreamingDecoder::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");
bincode::deserialize(data.as_slice()).expect("unable to deserialize bincode")
}
impl MathApp {
#[allow(dead_code)] // This is used lol
/// Create new instance of [`MathApp`] and return it
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
tracing::info!("Initializing...");
#[cfg(target_arch = "wasm32")]
tracing::info!("Web Info: {:?}", &cc.integration_info.web_info);
{
tracing::info!("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 +251,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,9 +262,7 @@ 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| {
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
@@ -200,11 +274,7 @@ impl MathApp {
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::Left, "Left");
ui.selectable_value(
&mut self.settings.riemann_sum,
Riemann::Middle,
@@ -276,20 +346,31 @@ impl MathApp {
});
ui.horizontal(|ui| {
toggle_button(
ui,
&mut self.settings.do_extrema,
"Extrema",
"Disable Displaying Extrema",
"Display Extrema",
self.settings.do_extrema.bitxor_assign(
ui.add(Button::new("Extrema"))
.on_hover_text(match self.settings.do_extrema {
true => "Disable Displaying Extrema",
false => "Display Extrema",
})
.clicked(),
);
toggle_button(
ui,
&mut self.settings.do_roots,
"Roots",
"Disable Displaying Roots",
"Display Roots",
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(),
);
});
@@ -297,8 +378,11 @@ impl MathApp {
#[cfg(target_arch = "wasm32")]
{
tracing::info!("Saving function data");
let saved_data = crate::misc::hashed_storage_create(
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"),
);
@@ -328,24 +412,35 @@ impl MathApp {
impl App for MathApp {
/// Called each time the UI needs repainting.
fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
// start timer
let start = if self.opened.info {
Some(Instant::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 +456,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 +529,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 +558,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 +602,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,4 @@
use base64::Engine;
use base64::engine::general_purpose;
use egui::Id;
use egui_plot::{Line, PlotPoint, PlotPoints, Points};
use emath::Pos2;
use itertools::Itertools;
@@ -63,6 +62,14 @@ impl Offset for Pos2 {
}
}
pub const fn create_id(x: u64) -> Id {
unsafe { std::mem::transmute::<u64, Id>(x) }
}
pub const fn get_u64_id(id: Id) -> u64 {
unsafe { std::mem::transmute::<Id, u64>(id) }
}
/*
/// Rounds f64 to `n` decimal places
pub fn decimal_round(x: f64, n: usize) -> f64 {
@@ -91,7 +98,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 +158,55 @@ 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 {
unsafe { std::mem::transmute::<Vec<u8>, String>([hashbytes.to_vec(), data.to_vec()].concat()) }
}
#[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, &[u8])> {
// Make sure data is long enough to decode
if HASH_LENGTH >= data.len() {
return None;
}
// Transmute data into slice
let decoded_1: &[u8] = unsafe { std::mem::transmute::<&str, &[u8]>(data) };
// Return hash and decoded data
Some((
unsafe { *(decoded_1[..HASH_LENGTH].as_ptr() as *const HashBytes) },
&decoded_1[HASH_LENGTH..],
))
}
/// 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 +214,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>