@@ -114,6 +114,22 @@ pub const CURVE_COLORS: [Color; 6] = [ |
| 114 | 114 | }, // teal |
| 115 | 115 | ]; |
| 116 | 116 | |
| 117 | +/// Viewport preset configurations |
| 118 | +#[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 119 | +pub enum ViewportPreset { |
| 120 | + Standard, |
| 121 | + Trig, |
| 122 | + ZoomFit, |
| 123 | +} |
| 124 | + |
| 125 | +/// A plotted function with visibility and label |
| 126 | +#[derive(Debug, Clone)] |
| 127 | +pub struct PlottedFunction { |
| 128 | + pub func: Plottable, |
| 129 | + pub visible: bool, |
| 130 | + pub label: String, |
| 131 | +} |
| 132 | + |
| 117 | 133 | /// 2D graph state and renderer |
| 118 | 134 | pub struct Graph2D { |
| 119 | 135 | /// Current viewport |
@@ -121,7 +137,7 @@ pub struct Graph2D { |
| 121 | 137 | /// Plot configuration |
| 122 | 138 | pub config: PlotConfig, |
| 123 | 139 | /// Functions to plot |
| 124 | | - pub functions: Vec<Plottable>, |
| 140 | + pub functions: Vec<PlottedFunction>, |
| 125 | 141 | /// Trace mode: show cursor position |
| 126 | 142 | pub trace_enabled: bool, |
| 127 | 143 | /// Current trace position (screen coords) |
@@ -152,44 +168,166 @@ impl Graph2D { |
| 152 | 168 | } |
| 153 | 169 | |
| 154 | 170 | /// Add a function to plot |
| 155 | | - pub fn add_function(&mut self, func: Plottable) { |
| 156 | | - self.functions.push(func); |
| 171 | + pub fn add_function(&mut self, func: Plottable, label: String) { |
| 172 | + self.functions.push(PlottedFunction { |
| 173 | + func, |
| 174 | + visible: true, |
| 175 | + label, |
| 176 | + }); |
| 157 | 177 | } |
| 158 | 178 | |
| 159 | 179 | /// Add an explicit function y = f(x) with auto color |
| 160 | 180 | pub fn add_explicit(&mut self, expr: Expr) { |
| 161 | 181 | let color = CURVE_COLORS[self.functions.len() % CURVE_COLORS.len()]; |
| 162 | | - self.functions.push(Plottable::Explicit2D { |
| 163 | | - expr, |
| 164 | | - x_var: "x".to_string(), |
| 165 | | - color, |
| 166 | | - style: LineStyle::Solid, |
| 182 | + let label = format!("y = {}", expr); |
| 183 | + self.functions.push(PlottedFunction { |
| 184 | + func: Plottable::Explicit2D { |
| 185 | + expr, |
| 186 | + x_var: "x".to_string(), |
| 187 | + color, |
| 188 | + style: LineStyle::Solid, |
| 189 | + }, |
| 190 | + visible: true, |
| 191 | + label, |
| 167 | 192 | }); |
| 168 | 193 | } |
| 169 | 194 | |
| 170 | 195 | /// Add an implicit curve F(x,y) = 0 |
| 171 | 196 | pub fn add_implicit(&mut self, expr: Expr) { |
| 172 | 197 | let color = CURVE_COLORS[self.functions.len() % CURVE_COLORS.len()]; |
| 173 | | - self.functions.push(Plottable::Implicit2D { |
| 174 | | - expr, |
| 175 | | - x_var: "x".to_string(), |
| 176 | | - y_var: "y".to_string(), |
| 177 | | - color, |
| 198 | + let label = format!("{} = 0", expr); |
| 199 | + self.functions.push(PlottedFunction { |
| 200 | + func: Plottable::Implicit2D { |
| 201 | + expr, |
| 202 | + x_var: "x".to_string(), |
| 203 | + y_var: "y".to_string(), |
| 204 | + color, |
| 205 | + }, |
| 206 | + visible: true, |
| 207 | + label, |
| 178 | 208 | }); |
| 179 | 209 | } |
| 180 | 210 | |
| 181 | 211 | /// Add a parametric curve (x(t), y(t)) |
| 182 | 212 | pub fn add_parametric(&mut self, x_expr: Expr, y_expr: Expr, t_range: (f64, f64)) { |
| 183 | 213 | let color = CURVE_COLORS[self.functions.len() % CURVE_COLORS.len()]; |
| 184 | | - self.functions.push(Plottable::Parametric2D { |
| 185 | | - x_expr, |
| 186 | | - y_expr, |
| 187 | | - t_var: "t".to_string(), |
| 188 | | - t_range, |
| 189 | | - color, |
| 214 | + let label = format!("({}, {})", x_expr, y_expr); |
| 215 | + self.functions.push(PlottedFunction { |
| 216 | + func: Plottable::Parametric2D { |
| 217 | + x_expr, |
| 218 | + y_expr, |
| 219 | + t_var: "t".to_string(), |
| 220 | + t_range, |
| 221 | + color, |
| 222 | + }, |
| 223 | + visible: true, |
| 224 | + label, |
| 225 | + }); |
| 226 | + } |
| 227 | + |
| 228 | + /// Add a polar curve r = f(theta) |
| 229 | + pub fn add_polar(&mut self, expr: Expr, theta_range: (f64, f64)) { |
| 230 | + let color = CURVE_COLORS[self.functions.len() % CURVE_COLORS.len()]; |
| 231 | + let label = format!("r = {}", expr); |
| 232 | + self.functions.push(PlottedFunction { |
| 233 | + func: Plottable::Polar2D { |
| 234 | + expr, |
| 235 | + theta_var: "theta".to_string(), |
| 236 | + theta_range, |
| 237 | + color, |
| 238 | + style: LineStyle::Solid, |
| 239 | + }, |
| 240 | + visible: true, |
| 241 | + label, |
| 190 | 242 | }); |
| 191 | 243 | } |
| 192 | 244 | |
| 245 | + /// Add a piecewise function |
| 246 | + pub fn add_piecewise(&mut self, pieces: Vec<crate::PiecewisePiece>) { |
| 247 | + let color = CURVE_COLORS[self.functions.len() % CURVE_COLORS.len()]; |
| 248 | + let label = format!("piecewise ({} pieces)", pieces.len()); |
| 249 | + self.functions.push(PlottedFunction { |
| 250 | + func: Plottable::Piecewise2D { |
| 251 | + pieces, |
| 252 | + x_var: "x".to_string(), |
| 253 | + color, |
| 254 | + style: LineStyle::Solid, |
| 255 | + }, |
| 256 | + visible: true, |
| 257 | + label, |
| 258 | + }); |
| 259 | + } |
| 260 | + |
| 261 | + /// Apply a viewport preset |
| 262 | + pub fn apply_viewport_preset(&mut self, preset: ViewportPreset) { |
| 263 | + match preset { |
| 264 | + ViewportPreset::Standard => { |
| 265 | + self.viewport = Viewport2D::default(); |
| 266 | + } |
| 267 | + ViewportPreset::Trig => { |
| 268 | + self.viewport = Viewport2D { |
| 269 | + x_min: -2.0 * std::f64::consts::PI, |
| 270 | + x_max: 2.0 * std::f64::consts::PI, |
| 271 | + y_min: -1.5, |
| 272 | + y_max: 1.5, |
| 273 | + }; |
| 274 | + } |
| 275 | + ViewportPreset::ZoomFit => { |
| 276 | + self.zoom_to_fit(); |
| 277 | + } |
| 278 | + } |
| 279 | + } |
| 280 | + |
| 281 | + /// Auto-zoom to fit visible function outputs |
| 282 | + pub fn zoom_to_fit(&mut self) { |
| 283 | + let mut y_min = f64::INFINITY; |
| 284 | + let mut y_max = f64::NEG_INFINITY; |
| 285 | + let mut evaluator = Evaluator::new(); |
| 286 | + let num_samples = 500; |
| 287 | + let x_range = self.viewport.x_max - self.viewport.x_min; |
| 288 | + let dx = x_range / num_samples as f64; |
| 289 | + |
| 290 | + for pf in &self.functions { |
| 291 | + if !pf.visible { |
| 292 | + continue; |
| 293 | + } |
| 294 | + if let Plottable::Explicit2D { ref expr, ref x_var, .. } = pf.func { |
| 295 | + for i in 0..=num_samples { |
| 296 | + let x = self.viewport.x_min + i as f64 * dx; |
| 297 | + evaluator.set_var(x_var, Expr::Float(x)); |
| 298 | + if let Ok(result) = evaluator.eval(expr) { |
| 299 | + if let Ok(y) = expr_to_f64(&result) { |
| 300 | + if y.is_finite() { |
| 301 | + y_min = y_min.min(y); |
| 302 | + y_max = y_max.max(y); |
| 303 | + } |
| 304 | + } |
| 305 | + } |
| 306 | + } |
| 307 | + } |
| 308 | + } |
| 309 | + |
| 310 | + if y_min.is_finite() && y_max.is_finite() && (y_max - y_min).abs() > 1e-10 { |
| 311 | + let margin = (y_max - y_min) * 0.1; |
| 312 | + self.viewport.y_min = y_min - margin; |
| 313 | + self.viewport.y_max = y_max + margin; |
| 314 | + } |
| 315 | + } |
| 316 | + |
| 317 | + /// Set the color of a function at the given index |
| 318 | + pub fn set_function_color(&mut self, index: usize, color: crate::Color) { |
| 319 | + if let Some(pf) = self.functions.get_mut(index) { |
| 320 | + pf.func.set_color(color); |
| 321 | + } |
| 322 | + } |
| 323 | + |
| 324 | + /// Toggle visibility of function at index |
| 325 | + pub fn toggle_visibility(&mut self, index: usize) { |
| 326 | + if let Some(f) = self.functions.get_mut(index) { |
| 327 | + f.visible = !f.visible; |
| 328 | + } |
| 329 | + } |
| 330 | + |
| 193 | 331 | /// Clear all functions |
| 194 | 332 | pub fn clear_functions(&mut self) { |
| 195 | 333 | self.functions.clear(); |
@@ -282,8 +420,10 @@ impl Graph2D { |
| 282 | 420 | self.draw_axes(ctx, width, height); |
| 283 | 421 | |
| 284 | 422 | // Functions |
| 285 | | - for func in &self.functions { |
| 286 | | - self.draw_function(ctx, func, width, height); |
| 423 | + for pf in &self.functions { |
| 424 | + if pf.visible { |
| 425 | + self.draw_function(ctx, &pf.func, width, height); |
| 426 | + } |
| 287 | 427 | } |
| 288 | 428 | |
| 289 | 429 | // Trace |
@@ -426,6 +566,23 @@ impl Graph2D { |
| 426 | 566 | } => { |
| 427 | 567 | self.draw_parametric(ctx, x_expr, y_expr, t_var, *t_range, *color, width, height); |
| 428 | 568 | } |
| 569 | + Plottable::Polar2D { |
| 570 | + expr, |
| 571 | + theta_var, |
| 572 | + theta_range, |
| 573 | + color, |
| 574 | + style, |
| 575 | + } => { |
| 576 | + self.draw_polar(ctx, expr, theta_var, *theta_range, *color, *style, width, height); |
| 577 | + } |
| 578 | + Plottable::Piecewise2D { |
| 579 | + pieces, |
| 580 | + x_var, |
| 581 | + color, |
| 582 | + style, |
| 583 | + } => { |
| 584 | + self.draw_piecewise(ctx, pieces, x_var, *color, *style, width, height); |
| 585 | + } |
| 429 | 586 | _ => {} |
| 430 | 587 | } |
| 431 | 588 | } |
@@ -671,6 +828,381 @@ impl Graph2D { |
| 671 | 828 | let _ = ctx.stroke(); |
| 672 | 829 | } |
| 673 | 830 | |
| 831 | + fn draw_polar( |
| 832 | + &self, |
| 833 | + ctx: &Context, |
| 834 | + expr: &Expr, |
| 835 | + theta_var: &str, |
| 836 | + theta_range: (f64, f64), |
| 837 | + color: Color, |
| 838 | + style: LineStyle, |
| 839 | + width: u32, |
| 840 | + height: u32, |
| 841 | + ) { |
| 842 | + set_color(ctx, color); |
| 843 | + ctx.set_line_width(self.config.curve_width); |
| 844 | + match style { |
| 845 | + LineStyle::Solid => ctx.set_dash(&[], 0.0), |
| 846 | + LineStyle::Dashed => ctx.set_dash(&[6.0, 4.0], 0.0), |
| 847 | + LineStyle::Dotted => ctx.set_dash(&[2.0, 2.0], 0.0), |
| 848 | + } |
| 849 | + |
| 850 | + let mut evaluator = Evaluator::new(); |
| 851 | + let num_samples = (width as f64 * self.config.samples_per_pixel) as usize; |
| 852 | + let theta_span = theta_range.1 - theta_range.0; |
| 853 | + let dtheta = theta_span / num_samples as f64; |
| 854 | + let mut first = true; |
| 855 | + |
| 856 | + for i in 0..=num_samples { |
| 857 | + let theta = theta_range.0 + i as f64 * dtheta; |
| 858 | + evaluator.set_var(theta_var, Expr::Float(theta)); |
| 859 | + if let Ok(result) = evaluator.eval(expr) { |
| 860 | + if let Ok(r) = expr_to_f64(&result) { |
| 861 | + if r.is_finite() { |
| 862 | + let math_x = r * theta.cos(); |
| 863 | + let math_y = r * theta.sin(); |
| 864 | + let (sx, sy) = self.math_to_screen(math_x, math_y, width, height); |
| 865 | + if first { |
| 866 | + ctx.move_to(sx, sy); |
| 867 | + first = false; |
| 868 | + } else { |
| 869 | + ctx.line_to(sx, sy); |
| 870 | + } |
| 871 | + continue; |
| 872 | + } |
| 873 | + } |
| 874 | + } |
| 875 | + if !first { |
| 876 | + let _ = ctx.stroke(); |
| 877 | + first = true; |
| 878 | + } |
| 879 | + } |
| 880 | + let _ = ctx.stroke(); |
| 881 | + ctx.set_dash(&[], 0.0); |
| 882 | + } |
| 883 | + |
| 884 | + fn draw_piecewise( |
| 885 | + &self, |
| 886 | + ctx: &Context, |
| 887 | + pieces: &[crate::PiecewisePiece], |
| 888 | + x_var: &str, |
| 889 | + color: Color, |
| 890 | + style: LineStyle, |
| 891 | + width: u32, |
| 892 | + height: u32, |
| 893 | + ) { |
| 894 | + set_color(ctx, color); |
| 895 | + ctx.set_line_width(self.config.curve_width); |
| 896 | + match style { |
| 897 | + LineStyle::Solid => ctx.set_dash(&[], 0.0), |
| 898 | + LineStyle::Dashed => ctx.set_dash(&[6.0, 4.0], 0.0), |
| 899 | + LineStyle::Dotted => ctx.set_dash(&[2.0, 2.0], 0.0), |
| 900 | + } |
| 901 | + |
| 902 | + let mut evaluator = Evaluator::new(); |
| 903 | + let num_samples = (width as f64 * self.config.samples_per_pixel) as usize; |
| 904 | + let x_range = self.viewport.x_max - self.viewport.x_min; |
| 905 | + let dx = x_range / num_samples as f64; |
| 906 | + let mut first = true; |
| 907 | + let mut last_valid = false; |
| 908 | + let mut last_piece_idx: Option<usize> = None; |
| 909 | + |
| 910 | + for i in 0..=num_samples { |
| 911 | + let math_x = self.viewport.x_min + i as f64 * dx; |
| 912 | + let piece_idx = pieces.iter().position(|p| p.condition.matches(math_x)); |
| 913 | + |
| 914 | + if piece_idx != last_piece_idx && last_valid { |
| 915 | + let _ = ctx.stroke(); |
| 916 | + first = true; |
| 917 | + last_valid = false; |
| 918 | + } |
| 919 | + last_piece_idx = piece_idx; |
| 920 | + |
| 921 | + if let Some(idx) = piece_idx { |
| 922 | + evaluator.set_var(x_var, Expr::Float(math_x)); |
| 923 | + if let Ok(result) = evaluator.eval(&pieces[idx].expr) { |
| 924 | + if let Ok(math_y) = expr_to_f64(&result) { |
| 925 | + if math_y.is_finite() |
| 926 | + && math_y >= self.viewport.y_min - x_range |
| 927 | + && math_y <= self.viewport.y_max + x_range |
| 928 | + { |
| 929 | + let (sx, sy) = self.math_to_screen(math_x, math_y, width, height); |
| 930 | + if first || !last_valid { |
| 931 | + ctx.move_to(sx, sy); |
| 932 | + first = false; |
| 933 | + } else { |
| 934 | + ctx.line_to(sx, sy); |
| 935 | + } |
| 936 | + last_valid = true; |
| 937 | + continue; |
| 938 | + } |
| 939 | + } |
| 940 | + } |
| 941 | + } |
| 942 | + if last_valid { |
| 943 | + let _ = ctx.stroke(); |
| 944 | + } |
| 945 | + last_valid = false; |
| 946 | + } |
| 947 | + let _ = ctx.stroke(); |
| 948 | + ctx.set_dash(&[], 0.0); |
| 949 | + } |
| 950 | + |
| 951 | + /// Find zeros (x-intercepts) of all visible Explicit2D functions |
| 952 | + pub fn find_zeros(&self) -> Vec<(usize, Vec<(f64, f64)>)> { |
| 953 | + let mut result = Vec::new(); |
| 954 | + for (idx, pf) in self.functions.iter().enumerate() { |
| 955 | + if !pf.visible { |
| 956 | + continue; |
| 957 | + } |
| 958 | + if let Plottable::Explicit2D { ref expr, ref x_var, .. } = pf.func { |
| 959 | + let zeros = self.find_zeros_for_expr(expr, x_var); |
| 960 | + if !zeros.is_empty() { |
| 961 | + result.push((idx, zeros)); |
| 962 | + } |
| 963 | + } |
| 964 | + } |
| 965 | + result |
| 966 | + } |
| 967 | + |
| 968 | + fn find_zeros_for_expr(&self, expr: &Expr, x_var: &str) -> Vec<(f64, f64)> { |
| 969 | + let mut evaluator = Evaluator::new(); |
| 970 | + let mut zeros = Vec::new(); |
| 971 | + let num_samples = 500; |
| 972 | + let x_range = self.viewport.x_max - self.viewport.x_min; |
| 973 | + let dx = x_range / num_samples as f64; |
| 974 | + |
| 975 | + let eval_at = |eval: &mut Evaluator, x: f64| -> Option<f64> { |
| 976 | + eval.set_var(x_var, Expr::Float(x)); |
| 977 | + eval.eval(expr) |
| 978 | + .ok() |
| 979 | + .and_then(|r| expr_to_f64(&r).ok()) |
| 980 | + .filter(|y| y.is_finite()) |
| 981 | + }; |
| 982 | + |
| 983 | + let mut prev: Option<f64> = None; |
| 984 | + let mut prev_x = self.viewport.x_min; |
| 985 | + |
| 986 | + for i in 0..=num_samples { |
| 987 | + let x = self.viewport.x_min + i as f64 * dx; |
| 988 | + if let Some(y) = eval_at(&mut evaluator, x) { |
| 989 | + if let Some(py) = prev { |
| 990 | + if py * y < 0.0 { |
| 991 | + if let Some(zx) = self.bisect(expr, x_var, prev_x, x, 50) { |
| 992 | + if zeros |
| 993 | + .last() |
| 994 | + .map_or(true, |&(lx, _): &(f64, f64)| (lx - zx).abs() > dx * 0.1) |
| 995 | + { |
| 996 | + zeros.push((zx, 0.0)); |
| 997 | + } |
| 998 | + } |
| 999 | + } |
| 1000 | + } |
| 1001 | + prev = Some(y); |
| 1002 | + prev_x = x; |
| 1003 | + } else { |
| 1004 | + prev = None; |
| 1005 | + } |
| 1006 | + } |
| 1007 | + zeros |
| 1008 | + } |
| 1009 | + |
| 1010 | + fn bisect( |
| 1011 | + &self, |
| 1012 | + expr: &Expr, |
| 1013 | + x_var: &str, |
| 1014 | + mut a: f64, |
| 1015 | + mut b: f64, |
| 1016 | + max_iter: usize, |
| 1017 | + ) -> Option<f64> { |
| 1018 | + let mut evaluator = Evaluator::new(); |
| 1019 | + let eval_at = |eval: &mut Evaluator, x: f64| -> Option<f64> { |
| 1020 | + eval.set_var(x_var, Expr::Float(x)); |
| 1021 | + eval.eval(expr) |
| 1022 | + .ok() |
| 1023 | + .and_then(|r| expr_to_f64(&r).ok()) |
| 1024 | + .filter(|y| y.is_finite()) |
| 1025 | + }; |
| 1026 | + |
| 1027 | + let fa = eval_at(&mut evaluator, a)?; |
| 1028 | + if fa.abs() < 1e-12 { |
| 1029 | + return Some(a); |
| 1030 | + } |
| 1031 | + |
| 1032 | + for _ in 0..max_iter { |
| 1033 | + let mid = (a + b) / 2.0; |
| 1034 | + let fm = eval_at(&mut evaluator, mid)?; |
| 1035 | + if fm.abs() < 1e-12 || (b - a).abs() < 1e-12 { |
| 1036 | + return Some(mid); |
| 1037 | + } |
| 1038 | + if fa * fm < 0.0 { |
| 1039 | + b = mid; |
| 1040 | + } else { |
| 1041 | + a = mid; |
| 1042 | + } |
| 1043 | + } |
| 1044 | + Some((a + b) / 2.0) |
| 1045 | + } |
| 1046 | + |
| 1047 | + /// Find intersections between pairs of visible Explicit2D functions |
| 1048 | + pub fn find_intersections(&self) -> Vec<((usize, usize), Vec<(f64, f64)>)> { |
| 1049 | + let mut result = Vec::new(); |
| 1050 | + let explicit_indices: Vec<usize> = self |
| 1051 | + .functions |
| 1052 | + .iter() |
| 1053 | + .enumerate() |
| 1054 | + .filter(|(_, pf)| pf.visible && matches!(pf.func, Plottable::Explicit2D { .. })) |
| 1055 | + .map(|(i, _)| i) |
| 1056 | + .collect(); |
| 1057 | + |
| 1058 | + for i in 0..explicit_indices.len() { |
| 1059 | + for j in (i + 1)..explicit_indices.len() { |
| 1060 | + let idx_i = explicit_indices[i]; |
| 1061 | + let idx_j = explicit_indices[j]; |
| 1062 | + let points = self.find_intersection_points(idx_i, idx_j); |
| 1063 | + if !points.is_empty() { |
| 1064 | + result.push(((idx_i, idx_j), points)); |
| 1065 | + } |
| 1066 | + } |
| 1067 | + } |
| 1068 | + result |
| 1069 | + } |
| 1070 | + |
| 1071 | + fn find_intersection_points(&self, idx_i: usize, idx_j: usize) -> Vec<(f64, f64)> { |
| 1072 | + let (expr_i, var_i) = match &self.functions[idx_i].func { |
| 1073 | + Plottable::Explicit2D { expr, x_var, .. } => (expr, x_var.as_str()), |
| 1074 | + _ => return Vec::new(), |
| 1075 | + }; |
| 1076 | + let (expr_j, _) = match &self.functions[idx_j].func { |
| 1077 | + Plottable::Explicit2D { expr, x_var, .. } => (expr, x_var.as_str()), |
| 1078 | + _ => return Vec::new(), |
| 1079 | + }; |
| 1080 | + |
| 1081 | + let mut eval_i = Evaluator::new(); |
| 1082 | + let mut eval_j = Evaluator::new(); |
| 1083 | + let mut points = Vec::new(); |
| 1084 | + let num_samples = 500; |
| 1085 | + let x_range = self.viewport.x_max - self.viewport.x_min; |
| 1086 | + let dx = x_range / num_samples as f64; |
| 1087 | + |
| 1088 | + let eval_diff = |ei: &mut Evaluator, ej: &mut Evaluator, x: f64| -> Option<f64> { |
| 1089 | + ei.set_var(var_i, Expr::Float(x)); |
| 1090 | + ej.set_var(var_i, Expr::Float(x)); |
| 1091 | + let yi = ei.eval(expr_i).ok().and_then(|r| expr_to_f64(&r).ok())?; |
| 1092 | + let yj = ej.eval(expr_j).ok().and_then(|r| expr_to_f64(&r).ok())?; |
| 1093 | + if yi.is_finite() && yj.is_finite() { |
| 1094 | + Some(yi - yj) |
| 1095 | + } else { |
| 1096 | + None |
| 1097 | + } |
| 1098 | + }; |
| 1099 | + |
| 1100 | + let mut prev_diff: Option<f64> = None; |
| 1101 | + let mut prev_x = self.viewport.x_min; |
| 1102 | + |
| 1103 | + for i in 0..=num_samples { |
| 1104 | + let x = self.viewport.x_min + i as f64 * dx; |
| 1105 | + if let Some(diff) = eval_diff(&mut eval_i, &mut eval_j, x) { |
| 1106 | + if let Some(pd) = prev_diff { |
| 1107 | + if pd * diff < 0.0 { |
| 1108 | + let mut a = prev_x; |
| 1109 | + let mut b = x; |
| 1110 | + for _ in 0..50 { |
| 1111 | + let mid = (a + b) / 2.0; |
| 1112 | + if let Some(dm) = eval_diff(&mut eval_i, &mut eval_j, mid) { |
| 1113 | + if dm.abs() < 1e-12 || (b - a).abs() < 1e-12 { |
| 1114 | + a = mid; |
| 1115 | + b = mid; |
| 1116 | + break; |
| 1117 | + } |
| 1118 | + if pd * dm < 0.0 { |
| 1119 | + b = mid; |
| 1120 | + } else { |
| 1121 | + a = mid; |
| 1122 | + } |
| 1123 | + } else { |
| 1124 | + break; |
| 1125 | + } |
| 1126 | + } |
| 1127 | + let zx = (a + b) / 2.0; |
| 1128 | + eval_i.set_var(var_i, Expr::Float(zx)); |
| 1129 | + if let Ok(r) = eval_i.eval(expr_i) { |
| 1130 | + if let Ok(zy) = expr_to_f64(&r) { |
| 1131 | + if zy.is_finite() |
| 1132 | + && points.last().map_or(true, |&(lx, _): &(f64, f64)| { |
| 1133 | + (lx - zx).abs() > dx * 0.1 |
| 1134 | + }) |
| 1135 | + { |
| 1136 | + points.push((zx, zy)); |
| 1137 | + } |
| 1138 | + } |
| 1139 | + } |
| 1140 | + } |
| 1141 | + } |
| 1142 | + prev_diff = Some(diff); |
| 1143 | + prev_x = x; |
| 1144 | + } else { |
| 1145 | + prev_diff = None; |
| 1146 | + } |
| 1147 | + } |
| 1148 | + points |
| 1149 | + } |
| 1150 | + |
| 1151 | + /// Draw marker circles at given points |
| 1152 | + pub fn draw_markers( |
| 1153 | + &self, |
| 1154 | + ctx: &Context, |
| 1155 | + points: &[(f64, f64)], |
| 1156 | + color: Color, |
| 1157 | + width: u32, |
| 1158 | + height: u32, |
| 1159 | + ) { |
| 1160 | + set_color(ctx, color); |
| 1161 | + for &(mx, my) in points { |
| 1162 | + let (sx, sy) = self.math_to_screen(mx, my, width, height); |
| 1163 | + ctx.arc(sx, sy, 4.0, 0.0, std::f64::consts::TAU); |
| 1164 | + let _ = ctx.fill(); |
| 1165 | + |
| 1166 | + let label = format!("({:.3}, {:.3})", mx, my); |
| 1167 | + ctx.set_font_size(10.0); |
| 1168 | + ctx.move_to(sx + 6.0, sy - 6.0); |
| 1169 | + let _ = ctx.show_text(&label); |
| 1170 | + } |
| 1171 | + } |
| 1172 | + |
| 1173 | + /// Generate a table of (x, f(x)) values for an Explicit2D function |
| 1174 | + pub fn generate_table( |
| 1175 | + &self, |
| 1176 | + func_index: usize, |
| 1177 | + x_min: f64, |
| 1178 | + x_max: f64, |
| 1179 | + step: f64, |
| 1180 | + ) -> Vec<(f64, Option<f64>)> { |
| 1181 | + let pf = match self.functions.get(func_index) { |
| 1182 | + Some(f) => f, |
| 1183 | + None => return Vec::new(), |
| 1184 | + }; |
| 1185 | + let (expr, x_var) = match &pf.func { |
| 1186 | + Plottable::Explicit2D { expr, x_var, .. } => (expr, x_var.as_str()), |
| 1187 | + _ => return Vec::new(), |
| 1188 | + }; |
| 1189 | + |
| 1190 | + let mut evaluator = Evaluator::new(); |
| 1191 | + let mut table = Vec::new(); |
| 1192 | + let mut x = x_min; |
| 1193 | + while x <= x_max + step * 0.01 { |
| 1194 | + evaluator.set_var(x_var, Expr::Float(x)); |
| 1195 | + let y = evaluator |
| 1196 | + .eval(expr) |
| 1197 | + .ok() |
| 1198 | + .and_then(|r| expr_to_f64(&r).ok()) |
| 1199 | + .filter(|v| v.is_finite()); |
| 1200 | + table.push((x, y)); |
| 1201 | + x += step; |
| 1202 | + } |
| 1203 | + table |
| 1204 | + } |
| 1205 | + |
| 674 | 1206 | fn draw_trace(&self, ctx: &Context, sx: f64, sy: f64, width: u32, height: u32) { |
| 675 | 1207 | let (mx, my) = self.screen_to_math(sx, sy, width, height); |
| 676 | 1208 | |