implement intesections + misc function options
This commit is contained in:
parent
bccb19cecc
commit
d95b29fb7f
6
TODO.md
6
TODO.md
@ -1,14 +1,8 @@
|
||||
## TODO:
|
||||
1. Function management
|
||||
- Integrals between functions (too hard to implement, maybe will shelve)
|
||||
- Display intersection between functions (would have to rewrite a lot of the function plotting handling)
|
||||
- [Drag and drop support](https://github.com/emilk/egui/discussions/1530) in the UI to re-order functions
|
||||
- Hide/disable functions
|
||||
- Prevent user from making too many function entries
|
||||
- Display function errors as tooltips or a warning box (not preventing the display of the graph)
|
||||
- Clone functions
|
||||
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
|
||||
|
||||
7
build.rs
7
build.rs
@ -131,7 +131,12 @@ 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,
|
||||
|
||||
@ -45,6 +45,9 @@ pub struct FunctionEntry {
|
||||
|
||||
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)>,
|
||||
pub derivative_data: Vec<PlotPoint>,
|
||||
@ -67,6 +70,7 @@ impl Hash for FunctionEntry {
|
||||
self.nth_derviative.hash(state);
|
||||
self.curr_nth.hash(state);
|
||||
self.settings_opened.hash(state);
|
||||
self.visible.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,11 +79,12 @@ impl Serialize for FunctionEntry {
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut s = serializer.serialize_struct("FunctionEntry", 4)?;
|
||||
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()
|
||||
}
|
||||
@ -96,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)?;
|
||||
@ -115,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)
|
||||
}
|
||||
@ -129,6 +141,7 @@ impl Default for FunctionEntry {
|
||||
integral: false,
|
||||
derivative: false,
|
||||
nth_derviative: false,
|
||||
visible: true,
|
||||
back_data: Vec::new(),
|
||||
integral_data: None,
|
||||
derivative_data: Vec::new(),
|
||||
@ -374,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;
|
||||
}
|
||||
|
||||
|
||||
@ -86,9 +86,14 @@ impl FunctionManager {
|
||||
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();
|
||||
@ -116,6 +121,19 @@ impl FunctionManager {
|
||||
// Only keep valid chars
|
||||
new_string.retain(crate::misc::is_valid_char);
|
||||
|
||||
// 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 {
|
||||
@ -197,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(
|
||||
@ -240,6 +299,26 @@ impl FunctionManager {
|
||||
self.functions.remove(remove_i_unwrap);
|
||||
}
|
||||
|
||||
// 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
|
||||
@ -253,6 +332,16 @@ impl FunctionManager {
|
||||
));
|
||||
}
|
||||
|
||||
/// 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)
|
||||
|
||||
@ -2,7 +2,7 @@ use crate::{
|
||||
consts::{BUILD_INFO, COLORS, DEFAULT_INTEGRAL_NUM, DEFAULT_MAX_X, DEFAULT_MIN_X, build},
|
||||
function_entry::Riemann,
|
||||
function_manager::FunctionManager,
|
||||
misc::option_vec_printer,
|
||||
misc::{EguiHelper, find_intersections, option_vec_printer},
|
||||
};
|
||||
use eframe::App;
|
||||
use egui::{
|
||||
@ -13,7 +13,7 @@ use egui_plot::Plot;
|
||||
|
||||
use emath::{Align, Align2};
|
||||
use epaint::{CornerRadius, Margin};
|
||||
use itertools::Itertools;
|
||||
|
||||
use std::{io::Read, ops::BitXorAssign};
|
||||
use web_time::Instant;
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
@ -108,6 +112,9 @@ pub struct MathApp {
|
||||
|
||||
/// Stores settings (pretty self-explanatory)
|
||||
settings: AppSettings,
|
||||
|
||||
/// Stores intersection points between functions
|
||||
intersections: Vec<egui_plot::PlotPoint>,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
@ -246,6 +253,7 @@ impl MathApp {
|
||||
last_info: (None, None),
|
||||
opened: Opened::default(),
|
||||
settings: AppSettings::default(),
|
||||
intersections: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -354,6 +362,15 @@ impl MathApp {
|
||||
})
|
||||
.clicked(),
|
||||
);
|
||||
|
||||
self.settings.do_intersections.bitxor_assign(
|
||||
ui.add(Button::new("Intersections"))
|
||||
.on_hover_text(match self.settings.do_intersections {
|
||||
true => "Disable Displaying Intersections",
|
||||
false => "Display Intersections between functions",
|
||||
})
|
||||
.clicked(),
|
||||
);
|
||||
});
|
||||
|
||||
if self.functions.display_entries(ui) {
|
||||
@ -530,7 +547,7 @@ impl App for MathApp {
|
||||
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,
|
||||
@ -540,29 +557,6 @@ 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(|(_, error)| error.is_some())
|
||||
.map(|(i, error)| {
|
||||
// use unwrap_unchecked as None Errors are already filtered out
|
||||
unsafe {
|
||||
format!("(Function #{}) {}\n", i, error.as_ref().unwrap_unchecked())
|
||||
}
|
||||
})
|
||||
.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;
|
||||
@ -607,6 +601,41 @@ impl App for MathApp {
|
||||
})
|
||||
.collect();
|
||||
|
||||
// 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 {
|
||||
|
||||
44
src/misc.rs
44
src/misc.rs
@ -214,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()
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ fn app_settings_constructor(
|
||||
integral_num,
|
||||
do_extrema: false,
|
||||
do_roots: false,
|
||||
do_intersections: false,
|
||||
plot_width: pixel_width,
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user