Compare commits

..

7 Commits

15 changed files with 1200 additions and 567 deletions

1
.gitignore vendored
View File

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

340
Cargo.lock generated
View File

@ -128,6 +128,12 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "as-raw-xcb-connection"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b"
[[package]] [[package]]
name = "atomic-waker" name = "atomic-waker"
version = "1.1.2" version = "1.1.2"
@ -190,6 +196,21 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "bit-set"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@ -291,6 +312,18 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "calloop-wayland-source"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20"
dependencies = [
"calloop 0.13.0",
"rustix 0.38.44",
"wayland-backend",
"wayland-client",
]
[[package]] [[package]]
name = "calloop-wayland-source" name = "calloop-wayland-source"
version = "0.4.1" version = "0.4.1"
@ -369,7 +402,7 @@ checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"textwrap", "textwrap",
"unicode-width", "unicode-width 0.1.14",
] ]
[[package]] [[package]]
@ -381,6 +414,15 @@ dependencies = [
"error-code", "error-code",
] ]
[[package]]
name = "codespan-reporting"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81"
dependencies = [
"unicode-width 0.2.2",
]
[[package]] [[package]]
name = "combine" name = "combine"
version = "4.6.7" version = "4.6.7"
@ -714,6 +756,7 @@ dependencies = [
"bytemuck", "bytemuck",
"document-features", "document-features",
"egui", "egui",
"egui-wgpu",
"egui-winit", "egui-winit",
"egui_glow", "egui_glow",
"glow", "glow",
@ -756,6 +799,25 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
] ]
[[package]]
name = "egui-wgpu"
version = "0.33.2"
source = "git+https://github.com/titaniumtown/egui.git#63106bc9faab805197ba88820d6f11bc8c5c4657"
dependencies = [
"ahash",
"bytemuck",
"document-features",
"egui",
"epaint",
"log",
"profiling",
"thiserror 2.0.17",
"type-map",
"web-time",
"wgpu",
"winit",
]
[[package]] [[package]]
name = "egui-winit" name = "egui-winit"
version = "0.33.2" version = "0.33.2"
@ -789,6 +851,7 @@ dependencies = [
"profiling", "profiling",
"wasm-bindgen", "wasm-bindgen",
"web-sys", "web-sys",
"winit",
] ]
[[package]] [[package]]
@ -941,6 +1004,12 @@ dependencies = [
"miniz_oxide", "miniz_oxide",
] ]
[[package]]
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]] [[package]]
name = "foreign-types" name = "foreign-types"
version = "0.5.0" version = "0.5.0"
@ -1085,6 +1154,7 @@ dependencies = [
"cgl", "cgl",
"dispatch2", "dispatch2",
"glutin_egl_sys", "glutin_egl_sys",
"glutin_glx_sys",
"glutin_wgl_sys", "glutin_wgl_sys",
"libloading", "libloading",
"objc2 0.6.3", "objc2 0.6.3",
@ -1093,7 +1163,9 @@ dependencies = [
"objc2-foundation 0.3.2", "objc2-foundation 0.3.2",
"once_cell", "once_cell",
"raw-window-handle", "raw-window-handle",
"wayland-sys",
"windows-sys 0.52.0", "windows-sys 0.52.0",
"x11-dl",
] ]
[[package]] [[package]]
@ -1118,6 +1190,16 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "glutin_glx_sys"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185"
dependencies = [
"gl_generator",
"x11-dl",
]
[[package]] [[package]]
name = "glutin_wgl_sys" name = "glutin_wgl_sys"
version = "0.6.1" version = "0.6.1"
@ -1141,6 +1223,7 @@ checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"crunchy", "crunchy",
"num-traits",
"zerocopy", "zerocopy",
] ]
@ -1149,6 +1232,9 @@ name = "hashbrown"
version = "0.16.1" version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"foldhash",
]
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
@ -1165,6 +1251,12 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "hexf-parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
[[package]] [[package]]
name = "iana-time-zone" name = "iana-time-zone"
version = "0.1.64" version = "0.1.64"
@ -1485,6 +1577,12 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "libm"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
[[package]] [[package]]
name = "libredox" name = "libredox"
version = "0.1.10" version = "0.1.10"
@ -1609,6 +1707,31 @@ dependencies = [
"pxfm", "pxfm",
] ]
[[package]]
name = "naga"
version = "27.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "066cf25f0e8b11ee0df221219010f213ad429855f57c494f995590c861a9a7d8"
dependencies = [
"arrayvec",
"bit-set",
"bitflags 2.10.0",
"cfg-if",
"cfg_aliases",
"codespan-reporting",
"half 2.7.1",
"hashbrown",
"hexf-parse",
"indexmap",
"libm",
"log",
"num-traits",
"once_cell",
"rustc-hash 1.1.0",
"thiserror 2.0.17",
"unicode-ident",
]
[[package]] [[package]]
name = "ndk" name = "ndk"
version = "0.9.0" version = "0.9.0"
@ -1746,6 +1869,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"libm",
] ]
[[package]] [[package]]
@ -2286,6 +2410,21 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "portable-atomic"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
[[package]]
name = "portable-atomic-util"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
dependencies = [
"portable-atomic",
]
[[package]] [[package]]
name = "potential_utf" name = "potential_utf"
version = "0.1.4" version = "0.1.4"
@ -2500,6 +2639,12 @@ version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "renderdoc-sys"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
[[package]] [[package]]
name = "rgb" name = "rgb"
version = "0.8.52" version = "0.8.52"
@ -2524,6 +2669,18 @@ version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.44" version = "0.38.44"
@ -2718,6 +2875,31 @@ version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "smithay-client-toolkit"
version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016"
dependencies = [
"bitflags 2.10.0",
"calloop 0.13.0",
"calloop-wayland-source 0.3.0",
"cursor-icon",
"libc",
"log",
"memmap2 0.9.9",
"rustix 0.38.44",
"thiserror 1.0.69",
"wayland-backend",
"wayland-client",
"wayland-csd-frame",
"wayland-cursor",
"wayland-protocols",
"wayland-protocols-wlr",
"wayland-scanner",
"xkeysym",
]
[[package]] [[package]]
name = "smithay-client-toolkit" name = "smithay-client-toolkit"
version = "0.20.0" version = "0.20.0"
@ -2726,7 +2908,7 @@ checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"calloop 0.14.3", "calloop 0.14.3",
"calloop-wayland-source", "calloop-wayland-source 0.4.1",
"cursor-icon", "cursor-icon",
"libc", "libc",
"log", "log",
@ -2752,7 +2934,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226"
dependencies = [ dependencies = [
"libc", "libc",
"smithay-client-toolkit", "smithay-client-toolkit 0.20.0",
"wayland-backend", "wayland-backend",
] ]
@ -2856,7 +3038,7 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
dependencies = [ dependencies = [
"unicode-width", "unicode-width 0.1.14",
] ]
[[package]] [[package]]
@ -3057,6 +3239,15 @@ dependencies = [
"static_assertions", "static_assertions",
] ]
[[package]]
name = "type-map"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90"
dependencies = [
"rustc-hash 2.1.1",
]
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.19.0" version = "1.19.0"
@ -3087,6 +3278,12 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "unicode-width"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
version = "0.2.6" version = "0.2.6"
@ -3318,6 +3515,19 @@ dependencies = [
"wayland-scanner", "wayland-scanner",
] ]
[[package]]
name = "wayland-protocols-plasma"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a07a14257c077ab3279987c4f8bb987851bf57081b93710381daea94f2c2c032"
dependencies = [
"bitflags 2.10.0",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-scanner",
]
[[package]] [[package]]
name = "wayland-protocols-wlr" name = "wayland-protocols-wlr"
version = "0.3.9" version = "0.3.9"
@ -3396,6 +3606,102 @@ version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
[[package]]
name = "wgpu"
version = "27.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfe68bac7cde125de7a731c3400723cadaaf1703795ad3f4805f187459cd7a77"
dependencies = [
"arrayvec",
"bitflags 2.10.0",
"cfg-if",
"cfg_aliases",
"document-features",
"hashbrown",
"log",
"portable-atomic",
"profiling",
"raw-window-handle",
"smallvec",
"static_assertions",
"wgpu-core",
"wgpu-hal",
"wgpu-types",
]
[[package]]
name = "wgpu-core"
version = "27.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27a75de515543b1897b26119f93731b385a19aea165a1ec5f0e3acecc229cae7"
dependencies = [
"arrayvec",
"bit-set",
"bit-vec",
"bitflags 2.10.0",
"bytemuck",
"cfg_aliases",
"document-features",
"hashbrown",
"indexmap",
"log",
"naga",
"once_cell",
"parking_lot",
"portable-atomic",
"profiling",
"raw-window-handle",
"rustc-hash 1.1.0",
"smallvec",
"thiserror 2.0.17",
"wgpu-core-deps-windows-linux-android",
"wgpu-hal",
"wgpu-types",
]
[[package]]
name = "wgpu-core-deps-windows-linux-android"
version = "27.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71197027d61a71748e4120f05a9242b2ad142e3c01f8c1b47707945a879a03c3"
dependencies = [
"wgpu-hal",
]
[[package]]
name = "wgpu-hal"
version = "27.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b21cb61c57ee198bc4aff71aeadff4cbb80b927beb912506af9c780d64313ce"
dependencies = [
"bitflags 2.10.0",
"cfg-if",
"cfg_aliases",
"libloading",
"log",
"naga",
"portable-atomic",
"portable-atomic-util",
"raw-window-handle",
"renderdoc-sys",
"thiserror 2.0.17",
"wgpu-types",
]
[[package]]
name = "wgpu-types"
version = "27.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afdcf84c395990db737f2dd91628706cb31e86d72e53482320d368e52b5da5eb"
dependencies = [
"bitflags 2.10.0",
"bytemuck",
"js-sys",
"log",
"thiserror 2.0.17",
"web-sys",
]
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
@ -3723,10 +4029,12 @@ version = "0.30.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c66d4b9ed69c4009f6321f762d6e61ad8a2389cd431b97cb1e146812e9e6c732" checksum = "c66d4b9ed69c4009f6321f762d6e61ad8a2389cd431b97cb1e146812e9e6c732"
dependencies = [ dependencies = [
"ahash",
"android-activity", "android-activity",
"atomic-waker", "atomic-waker",
"bitflags 2.10.0", "bitflags 2.10.0",
"block2", "block2",
"bytemuck",
"calloop 0.13.0", "calloop 0.13.0",
"cfg_aliases", "cfg_aliases",
"concurrent-queue", "concurrent-queue",
@ -3736,24 +4044,33 @@ dependencies = [
"dpi", "dpi",
"js-sys", "js-sys",
"libc", "libc",
"memmap2 0.9.9",
"ndk", "ndk",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-app-kit 0.2.2", "objc2-app-kit 0.2.2",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
"objc2-ui-kit", "objc2-ui-kit",
"orbclient", "orbclient",
"percent-encoding",
"pin-project", "pin-project",
"raw-window-handle", "raw-window-handle",
"redox_syscall 0.4.1", "redox_syscall 0.4.1",
"rustix 0.38.44", "rustix 0.38.44",
"smithay-client-toolkit 0.19.2",
"smol_str", "smol_str",
"tracing", "tracing",
"unicode-segmentation", "unicode-segmentation",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-protocols-plasma",
"web-sys", "web-sys",
"web-time", "web-time",
"windows-sys 0.52.0", "windows-sys 0.52.0",
"x11-dl",
"x11rb",
"xkbcommon-dl", "xkbcommon-dl",
] ]
@ -3778,13 +4095,28 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
[[package]]
name = "x11-dl"
version = "2.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f"
dependencies = [
"libc",
"once_cell",
"pkg-config",
]
[[package]] [[package]]
name = "x11rb" name = "x11rb"
version = "0.13.2" version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
dependencies = [ dependencies = [
"as-raw-xcb-connection",
"gethostname", "gethostname",
"libc",
"libloading",
"once_cell",
"rustix 1.1.2", "rustix 1.1.2",
"x11rb-protocol", "x11rb-protocol",
] ]

View File

@ -9,6 +9,10 @@ description = "Crossplatform (and web-compatible) graphing calculator"
[lib] [lib]
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
[features]
# Enable x11/wayland support for running tests on native targets
native-test = ["eframe/x11", "eframe/wayland"]
[profile.release] [profile.release]
debug = false debug = false
codegen-units = 1 codegen-units = 1

16
TODO.md
View File

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

View File

@ -59,8 +59,42 @@
buildInputs = with pkgs; [ buildInputs = with pkgs; [
openssl openssl
zstd zstd
# Required for running tests with native windowing support
libxkbcommon
libGL
wayland
xorg.libX11
xorg.libXcursor
xorg.libXi
xorg.libXrandr
]; ];
# 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
xorg.libX11
xorg.libXcursor
xorg.libXi
xorg.libXrandr
]
);
in
''
runHook preCheck
export HOME=$TMPDIR
export LD_LIBRARY_PATH="${libPath}:$LD_LIBRARY_PATH"
cargo test --workspace --features native-test
runHook postCheck
'';
buildPhase = '' buildPhase = ''
runHook preBuild runHook preBuild
@ -81,7 +115,7 @@
runHook postInstall runHook postInstall
''; '';
doCheck = false; doCheck = true;
}; };
# Final web package with wasm-bindgen processing # Final web package with wasm-bindgen processing

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,6 +21,6 @@ fn main() -> eframe::Result<()> {
eframe::run_native( eframe::run_native(
"(Yet-to-be-named) Graphing Software", "(Yet-to-be-named) Graphing Software",
eframe::NativeOptions::default(), eframe::NativeOptions::default(),
Box::new(|cc| Box::new(math_app::MathApp::new(cc))), Box::new(|cc| Ok(Box::new(math_app::MathApp::new(cc)))),
) )
} }

View File

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

View File

@ -152,7 +152,7 @@ fn newtons_method() {
let data = newtons_method( let data = newtons_method(
&get_flatexwrapper("x^2 -1"), &get_flatexwrapper("x^2 -1"),
&get_flatexwrapper("2x"), &get_flatexwrapper("2*x"),
3.0, 3.0,
&(0.0..5.0), &(0.0..5.0),
f64::EPSILON, f64::EPSILON,