Rust · 42293 bytes Raw Blame History
1 //! 2D plotting engine
2 //!
3 //! Provides function plotting with:
4 //! - Explicit functions: y = f(x)
5 //! - Parametric curves: (x(t), y(t))
6 //! - Coordinate system with axes and grid
7 //! - Pan/zoom interaction
8 //! - Trace mode
9
10 use cairo::Context;
11 use garcalc_cas::{Evaluator, Expr, Symbol};
12
13 use crate::{Color, LineStyle, Plottable, Viewport2D};
14
15 /// Configuration for the plot appearance
16 #[derive(Debug, Clone)]
17 pub struct PlotConfig {
18 /// Background color
19 pub background: Color,
20 /// Axis color
21 pub axis_color: Color,
22 /// Grid color
23 pub grid_color: Color,
24 /// Tick label color
25 pub label_color: Color,
26 /// Whether to show grid
27 pub show_grid: bool,
28 /// Whether to show axis labels
29 pub show_labels: bool,
30 /// Line width for curves
31 pub curve_width: f64,
32 /// Line width for axes
33 pub axis_width: f64,
34 /// Line width for grid
35 pub grid_width: f64,
36 /// Number of samples per pixel for function plotting
37 pub samples_per_pixel: f64,
38 }
39
40 impl Default for PlotConfig {
41 fn default() -> Self {
42 Self {
43 background: Color {
44 r: 30,
45 g: 30,
46 b: 46,
47 a: 255,
48 },
49 axis_color: Color {
50 r: 166,
51 g: 173,
52 b: 200,
53 a: 255,
54 },
55 grid_color: Color {
56 r: 69,
57 g: 71,
58 b: 90,
59 a: 255,
60 },
61 label_color: Color {
62 r: 166,
63 g: 173,
64 b: 200,
65 a: 255,
66 },
67 show_grid: true,
68 show_labels: true,
69 curve_width: 2.0,
70 axis_width: 1.5,
71 grid_width: 0.5,
72 samples_per_pixel: 2.0,
73 }
74 }
75 }
76
77 /// Default curve colors (catppuccin palette)
78 pub const CURVE_COLORS: [Color; 6] = [
79 Color {
80 r: 137,
81 g: 180,
82 b: 250,
83 a: 255,
84 }, // blue
85 Color {
86 r: 166,
87 g: 227,
88 b: 161,
89 a: 255,
90 }, // green
91 Color {
92 r: 249,
93 g: 226,
94 b: 175,
95 a: 255,
96 }, // yellow
97 Color {
98 r: 243,
99 g: 139,
100 b: 168,
101 a: 255,
102 }, // red
103 Color {
104 r: 203,
105 g: 166,
106 b: 247,
107 a: 255,
108 }, // mauve
109 Color {
110 r: 148,
111 g: 226,
112 b: 213,
113 a: 255,
114 }, // teal
115 ];
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
133 /// 2D graph state and renderer
134 pub struct Graph2D {
135 /// Current viewport
136 pub viewport: Viewport2D,
137 /// Plot configuration
138 pub config: PlotConfig,
139 /// Functions to plot
140 pub functions: Vec<PlottedFunction>,
141 /// Trace mode: show cursor position
142 pub trace_enabled: bool,
143 /// Current trace position (screen coords)
144 pub trace_pos: Option<(f64, f64)>,
145 }
146
147 impl Default for Graph2D {
148 fn default() -> Self {
149 Self {
150 viewport: Viewport2D::default(),
151 config: PlotConfig::default(),
152 functions: Vec::new(),
153 trace_enabled: false,
154 trace_pos: None,
155 }
156 }
157 }
158
159 impl Graph2D {
160 /// Create a new graph with default settings
161 pub fn new() -> Self {
162 Self::default()
163 }
164
165 /// Set the viewport
166 pub fn set_viewport(&mut self, viewport: Viewport2D) {
167 self.viewport = viewport;
168 }
169
170 /// Add a function to plot
171 pub fn add_function(&mut self, func: Plottable, label: String) {
172 self.functions.push(PlottedFunction {
173 func,
174 visible: true,
175 label,
176 });
177 }
178
179 /// Add an explicit function y = f(x) with auto color
180 pub fn add_explicit(&mut self, expr: Expr) {
181 let color = CURVE_COLORS[self.functions.len() % CURVE_COLORS.len()];
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,
192 });
193 }
194
195 /// Add an implicit curve F(x,y) = 0
196 pub fn add_implicit(&mut self, expr: Expr) {
197 let color = CURVE_COLORS[self.functions.len() % CURVE_COLORS.len()];
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,
208 });
209 }
210
211 /// Add a parametric curve (x(t), y(t))
212 pub fn add_parametric(&mut self, x_expr: Expr, y_expr: Expr, t_range: (f64, f64)) {
213 let color = CURVE_COLORS[self.functions.len() % CURVE_COLORS.len()];
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,
242 });
243 }
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
331 /// Clear all functions
332 pub fn clear_functions(&mut self) {
333 self.functions.clear();
334 }
335
336 /// Pan the viewport by a screen delta
337 pub fn pan(&mut self, dx: f64, dy: f64, width: u32, height: u32) {
338 let x_range = self.viewport.x_max - self.viewport.x_min;
339 let y_range = self.viewport.y_max - self.viewport.y_min;
340
341 let math_dx = -dx * x_range / width as f64;
342 let math_dy = dy * y_range / height as f64;
343
344 self.viewport.x_min += math_dx;
345 self.viewport.x_max += math_dx;
346 self.viewport.y_min += math_dy;
347 self.viewport.y_max += math_dy;
348 }
349
350 /// Zoom by a factor centered at screen position
351 pub fn zoom(&mut self, factor: f64, cx: f64, cy: f64, width: u32, height: u32) {
352 // Convert screen position to math coordinates
353 let (mx, my) = self.screen_to_math(cx, cy, width, height);
354
355 // Calculate new ranges
356 let x_range = self.viewport.x_max - self.viewport.x_min;
357 let y_range = self.viewport.y_max - self.viewport.y_min;
358 let new_x_range = x_range / factor;
359 let new_y_range = y_range / factor;
360
361 // Calculate relative position of zoom center
362 let rx = (mx - self.viewport.x_min) / x_range;
363 let ry = (my - self.viewport.y_min) / y_range;
364
365 // Set new viewport centered on zoom point
366 self.viewport.x_min = mx - rx * new_x_range;
367 self.viewport.x_max = mx + (1.0 - rx) * new_x_range;
368 self.viewport.y_min = my - ry * new_y_range;
369 self.viewport.y_max = my + (1.0 - ry) * new_y_range;
370 }
371
372 /// Reset viewport to default
373 pub fn reset_viewport(&mut self) {
374 self.viewport = Viewport2D::default();
375 }
376
377 /// Convert screen coordinates to math coordinates
378 pub fn screen_to_math(&self, sx: f64, sy: f64, width: u32, height: u32) -> (f64, f64) {
379 let x_range = self.viewport.x_max - self.viewport.x_min;
380 let y_range = self.viewport.y_max - self.viewport.y_min;
381
382 let mx = self.viewport.x_min + (sx / width as f64) * x_range;
383 let my = self.viewport.y_max - (sy / height as f64) * y_range;
384
385 (mx, my)
386 }
387
388 /// Convert math coordinates to screen coordinates
389 pub fn math_to_screen(&self, mx: f64, my: f64, width: u32, height: u32) -> (f64, f64) {
390 let x_range = self.viewport.x_max - self.viewport.x_min;
391 let y_range = self.viewport.y_max - self.viewport.y_min;
392
393 let sx = ((mx - self.viewport.x_min) / x_range) * width as f64;
394 let sy = ((self.viewport.y_max - my) / y_range) * height as f64;
395
396 (sx, sy)
397 }
398
399 /// Update trace position
400 pub fn set_trace_pos(&mut self, x: f64, y: f64) {
401 self.trace_pos = Some((x, y));
402 }
403
404 /// Render the graph to a Cairo context
405 pub fn render(&self, ctx: &Context, width: u32, height: u32) {
406 let w = width as f64;
407 let h = height as f64;
408
409 // Background
410 set_color(ctx, self.config.background);
411 ctx.rectangle(0.0, 0.0, w, h);
412 let _ = ctx.fill();
413
414 // Grid
415 if self.config.show_grid {
416 self.draw_grid(ctx, width, height);
417 }
418
419 // Axes
420 self.draw_axes(ctx, width, height);
421
422 // Functions
423 for pf in &self.functions {
424 if pf.visible {
425 self.draw_function(ctx, &pf.func, width, height);
426 }
427 }
428
429 // Trace
430 if self.trace_enabled {
431 if let Some((sx, sy)) = self.trace_pos {
432 self.draw_trace(ctx, sx, sy, width, height);
433 }
434 }
435 }
436
437 fn draw_grid(&self, ctx: &Context, width: u32, height: u32) {
438 set_color(ctx, self.config.grid_color);
439 ctx.set_line_width(self.config.grid_width);
440
441 let x_range = self.viewport.x_max - self.viewport.x_min;
442 let y_range = self.viewport.y_max - self.viewport.y_min;
443
444 // Calculate nice grid spacing
445 let x_step = nice_step(x_range / 10.0);
446 let y_step = nice_step(y_range / 10.0);
447
448 // Vertical grid lines
449 let x_start = (self.viewport.x_min / x_step).floor() * x_step;
450 let mut x = x_start;
451 while x <= self.viewport.x_max {
452 let (sx, _) = self.math_to_screen(x, 0.0, width, height);
453 ctx.move_to(sx, 0.0);
454 ctx.line_to(sx, height as f64);
455 x += x_step;
456 }
457
458 // Horizontal grid lines
459 let y_start = (self.viewport.y_min / y_step).floor() * y_step;
460 let mut y = y_start;
461 while y <= self.viewport.y_max {
462 let (_, sy) = self.math_to_screen(0.0, y, width, height);
463 ctx.move_to(0.0, sy);
464 ctx.line_to(width as f64, sy);
465 y += y_step;
466 }
467
468 let _ = ctx.stroke();
469 }
470
471 fn draw_axes(&self, ctx: &Context, width: u32, height: u32) {
472 set_color(ctx, self.config.axis_color);
473 ctx.set_line_width(self.config.axis_width);
474
475 // Y-axis (x = 0)
476 if self.viewport.x_min <= 0.0 && self.viewport.x_max >= 0.0 {
477 let (sx, _) = self.math_to_screen(0.0, 0.0, width, height);
478 ctx.move_to(sx, 0.0);
479 ctx.line_to(sx, height as f64);
480 }
481
482 // X-axis (y = 0)
483 if self.viewport.y_min <= 0.0 && self.viewport.y_max >= 0.0 {
484 let (_, sy) = self.math_to_screen(0.0, 0.0, width, height);
485 ctx.move_to(0.0, sy);
486 ctx.line_to(width as f64, sy);
487 }
488
489 let _ = ctx.stroke();
490
491 // Tick labels
492 if self.config.show_labels {
493 self.draw_tick_labels(ctx, width, height);
494 }
495 }
496
497 fn draw_tick_labels(&self, ctx: &Context, width: u32, height: u32) {
498 set_color(ctx, self.config.label_color);
499 ctx.set_font_size(11.0);
500
501 let x_range = self.viewport.x_max - self.viewport.x_min;
502 let y_range = self.viewport.y_max - self.viewport.y_min;
503
504 let x_step = nice_step(x_range / 10.0);
505 let y_step = nice_step(y_range / 10.0);
506
507 // Get y-axis position for x labels
508 let (_, y_axis_sy) = self.math_to_screen(0.0, 0.0, width, height);
509 let label_y = y_axis_sy.clamp(12.0, height as f64 - 4.0);
510
511 // X-axis labels
512 let x_start = (self.viewport.x_min / x_step).ceil() * x_step;
513 let mut x = x_start;
514 while x <= self.viewport.x_max {
515 if x.abs() > x_step * 0.1 {
516 let (sx, _) = self.math_to_screen(x, 0.0, width, height);
517 let label = format_number(x);
518 ctx.move_to(sx - 10.0, label_y + 12.0);
519 let _ = ctx.show_text(&label);
520 }
521 x += x_step;
522 }
523
524 // Get x-axis position for y labels
525 let (x_axis_sx, _) = self.math_to_screen(0.0, 0.0, width, height);
526 let label_x = x_axis_sx.clamp(4.0, width as f64 - 40.0);
527
528 // Y-axis labels
529 let y_start = (self.viewport.y_min / y_step).ceil() * y_step;
530 let mut y = y_start;
531 while y <= self.viewport.y_max {
532 if y.abs() > y_step * 0.1 {
533 let (_, sy) = self.math_to_screen(0.0, y, width, height);
534 let label = format_number(y);
535 ctx.move_to(label_x + 4.0, sy + 4.0);
536 let _ = ctx.show_text(&label);
537 }
538 y += y_step;
539 }
540 }
541
542 fn draw_function(&self, ctx: &Context, func: &Plottable, width: u32, height: u32) {
543 match func {
544 Plottable::Explicit2D {
545 expr,
546 x_var,
547 color,
548 style,
549 } => {
550 self.draw_explicit(ctx, expr, x_var, *color, *style, width, height);
551 }
552 Plottable::Implicit2D {
553 expr,
554 x_var,
555 y_var,
556 color,
557 } => {
558 self.draw_implicit(ctx, expr, x_var, y_var, *color, width, height);
559 }
560 Plottable::Parametric2D {
561 x_expr,
562 y_expr,
563 t_var,
564 t_range,
565 color,
566 } => {
567 self.draw_parametric(ctx, x_expr, y_expr, t_var, *t_range, *color, width, height);
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 }
586 _ => {}
587 }
588 }
589
590 fn draw_explicit(
591 &self,
592 ctx: &Context,
593 expr: &Expr,
594 x_var: &str,
595 color: Color,
596 style: LineStyle,
597 width: u32,
598 height: u32,
599 ) {
600 set_color(ctx, color);
601 ctx.set_line_width(self.config.curve_width);
602
603 match style {
604 LineStyle::Solid => ctx.set_dash(&[], 0.0),
605 LineStyle::Dashed => ctx.set_dash(&[6.0, 4.0], 0.0),
606 LineStyle::Dotted => ctx.set_dash(&[2.0, 2.0], 0.0),
607 }
608
609 let mut evaluator = Evaluator::new();
610 let _var = Symbol::from(x_var);
611
612 // Sample the function
613 let num_samples = (width as f64 * self.config.samples_per_pixel) as usize;
614 let x_range = self.viewport.x_max - self.viewport.x_min;
615 let dx = x_range / num_samples as f64;
616
617 let mut first = true;
618 let mut last_valid = false;
619
620 for i in 0..=num_samples {
621 let math_x = self.viewport.x_min + i as f64 * dx;
622 evaluator.set_var(x_var, Expr::Float(math_x));
623
624 if let Ok(result) = evaluator.eval(expr) {
625 if let Ok(math_y) = expr_to_f64(&result) {
626 if math_y.is_finite()
627 && math_y >= self.viewport.y_min - x_range
628 && math_y <= self.viewport.y_max + x_range
629 {
630 let (sx, sy) = self.math_to_screen(math_x, math_y, width, height);
631
632 if first || !last_valid {
633 ctx.move_to(sx, sy);
634 first = false;
635 } else {
636 ctx.line_to(sx, sy);
637 }
638 last_valid = true;
639 continue;
640 }
641 }
642 }
643 // Invalid point - break the line
644 if last_valid {
645 let _ = ctx.stroke();
646 }
647 last_valid = false;
648 }
649
650 let _ = ctx.stroke();
651 ctx.set_dash(&[], 0.0);
652 }
653
654 fn draw_parametric(
655 &self,
656 ctx: &Context,
657 x_expr: &Expr,
658 y_expr: &Expr,
659 t_var: &str,
660 t_range: (f64, f64),
661 color: Color,
662 width: u32,
663 height: u32,
664 ) {
665 set_color(ctx, color);
666 ctx.set_line_width(self.config.curve_width);
667
668 let mut evaluator = Evaluator::new();
669
670 let num_samples = (width as f64 * self.config.samples_per_pixel) as usize;
671 let t_span = t_range.1 - t_range.0;
672 let dt = t_span / num_samples as f64;
673
674 let mut first = true;
675
676 for i in 0..=num_samples {
677 let t = t_range.0 + i as f64 * dt;
678 evaluator.set_var(t_var, Expr::Float(t));
679
680 let x_result = evaluator.eval(x_expr);
681 let y_result = evaluator.eval(y_expr);
682
683 if let (Ok(x_val), Ok(y_val)) = (x_result, y_result) {
684 if let (Ok(math_x), Ok(math_y)) = (expr_to_f64(&x_val), expr_to_f64(&y_val)) {
685 if math_x.is_finite() && math_y.is_finite() {
686 let (sx, sy) = self.math_to_screen(math_x, math_y, width, height);
687
688 if first {
689 ctx.move_to(sx, sy);
690 first = false;
691 } else {
692 ctx.line_to(sx, sy);
693 }
694 }
695 }
696 }
697 }
698
699 let _ = ctx.stroke();
700 }
701
702 /// Draw implicit curve F(x,y) = 0 using marching squares
703 fn draw_implicit(
704 &self,
705 ctx: &Context,
706 expr: &Expr,
707 x_var: &str,
708 y_var: &str,
709 color: Color,
710 width: u32,
711 height: u32,
712 ) {
713 set_color(ctx, color);
714 ctx.set_line_width(self.config.curve_width);
715
716 let mut evaluator = Evaluator::new();
717
718 // Grid resolution for marching squares
719 let grid_size = 100; // Number of cells per dimension
720 let x_range = self.viewport.x_max - self.viewport.x_min;
721 let y_range = self.viewport.y_max - self.viewport.y_min;
722 let dx = x_range / grid_size as f64;
723 let dy = y_range / grid_size as f64;
724
725 // Evaluate F(x,y) at grid points
726 let mut values = vec![vec![0.0f64; grid_size + 1]; grid_size + 1];
727 for i in 0..=grid_size {
728 let x = self.viewport.x_min + i as f64 * dx;
729 for j in 0..=grid_size {
730 let y = self.viewport.y_min + j as f64 * dy;
731 evaluator.set_var(x_var, Expr::Float(x));
732 evaluator.set_var(y_var, Expr::Float(y));
733 if let Ok(result) = evaluator.eval(expr) {
734 if let Ok(v) = expr_to_f64(&result) {
735 values[i][j] = if v.is_finite() { v } else { f64::NAN };
736 } else {
737 values[i][j] = f64::NAN;
738 }
739 } else {
740 values[i][j] = f64::NAN;
741 }
742 }
743 }
744
745 // Marching squares: for each cell, draw line segments where F=0
746 for i in 0..grid_size {
747 for j in 0..grid_size {
748 let x0 = self.viewport.x_min + i as f64 * dx;
749 let y0 = self.viewport.y_min + j as f64 * dy;
750 let x1 = x0 + dx;
751 let y1 = y0 + dy;
752
753 let v00 = values[i][j];
754 let v10 = values[i + 1][j];
755 let v01 = values[i][j + 1];
756 let v11 = values[i + 1][j + 1];
757
758 // Skip cells with NaN
759 if v00.is_nan() || v10.is_nan() || v01.is_nan() || v11.is_nan() {
760 continue;
761 }
762
763 // Classify cell corners by sign
764 let s00 = v00 >= 0.0;
765 let s10 = v10 >= 0.0;
766 let s01 = v01 >= 0.0;
767 let s11 = v11 >= 0.0;
768
769 // Build case index (4-bit)
770 let case =
771 (s00 as u8) | ((s10 as u8) << 1) | ((s01 as u8) << 2) | ((s11 as u8) << 3);
772
773 // Linear interpolation to find zero crossing on an edge
774 let interp = |va: f64, vb: f64| -> f64 {
775 if (va - vb).abs() < 1e-15 {
776 0.5
777 } else {
778 va / (va - vb)
779 }
780 };
781
782 // Edge midpoints where contour crosses
783 let e_bottom = || {
784 let t = interp(v00, v10);
785 (x0 + t * dx, y0)
786 };
787 let e_top = || {
788 let t = interp(v01, v11);
789 (x0 + t * dx, y1)
790 };
791 let e_left = || {
792 let t = interp(v00, v01);
793 (x0, y0 + t * dy)
794 };
795 let e_right = || {
796 let t = interp(v10, v11);
797 (x1, y0 + t * dy)
798 };
799
800 // Draw line segments based on marching squares case
801 let draw_line = |p1: (f64, f64), p2: (f64, f64)| {
802 let (sx1, sy1) = self.math_to_screen(p1.0, p1.1, width, height);
803 let (sx2, sy2) = self.math_to_screen(p2.0, p2.1, width, height);
804 ctx.move_to(sx1, sy1);
805 ctx.line_to(sx2, sy2);
806 };
807
808 match case {
809 0 | 15 => {} // All same sign - no contour
810 1 | 14 => draw_line(e_bottom(), e_left()),
811 2 | 13 => draw_line(e_bottom(), e_right()),
812 3 | 12 => draw_line(e_left(), e_right()),
813 4 | 11 => draw_line(e_left(), e_top()),
814 5 | 10 => {
815 // Ambiguous - saddle point. Draw both segments.
816 draw_line(e_bottom(), e_left());
817 draw_line(e_top(), e_right());
818 }
819 6 | 9 => {
820 draw_line(e_bottom(), e_top());
821 }
822 7 | 8 => draw_line(e_top(), e_right()),
823 _ => {}
824 }
825 }
826 }
827
828 let _ = ctx.stroke();
829 }
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
1206 fn draw_trace(&self, ctx: &Context, sx: f64, sy: f64, width: u32, height: u32) {
1207 let (mx, my) = self.screen_to_math(sx, sy, width, height);
1208
1209 // Crosshair
1210 set_color(ctx, self.config.axis_color);
1211 ctx.set_line_width(0.5);
1212 ctx.set_dash(&[4.0, 4.0], 0.0);
1213
1214 ctx.move_to(sx, 0.0);
1215 ctx.line_to(sx, height as f64);
1216 ctx.move_to(0.0, sy);
1217 ctx.line_to(width as f64, sy);
1218 let _ = ctx.stroke();
1219 ctx.set_dash(&[], 0.0);
1220
1221 // Coordinate display
1222 set_color(
1223 ctx,
1224 Color {
1225 r: 30,
1226 g: 30,
1227 b: 46,
1228 a: 200,
1229 },
1230 );
1231 let label = format!("({:.4}, {:.4})", mx, my);
1232 let label_w = label.len() as f64 * 7.0 + 8.0;
1233 let label_h = 18.0;
1234 let lx = (sx + 10.0).min(width as f64 - label_w - 4.0);
1235 let ly = (sy - 24.0).max(4.0);
1236
1237 ctx.rectangle(lx, ly, label_w, label_h);
1238 let _ = ctx.fill();
1239
1240 set_color(ctx, self.config.label_color);
1241 ctx.set_font_size(12.0);
1242 ctx.move_to(lx + 4.0, ly + 13.0);
1243 let _ = ctx.show_text(&label);
1244 }
1245 }
1246
1247 /// Set Cairo source color
1248 fn set_color(ctx: &Context, color: Color) {
1249 ctx.set_source_rgba(
1250 color.r as f64 / 255.0,
1251 color.g as f64 / 255.0,
1252 color.b as f64 / 255.0,
1253 color.a as f64 / 255.0,
1254 );
1255 }
1256
1257 /// Calculate a nice step size for grid/ticks
1258 fn nice_step(rough: f64) -> f64 {
1259 let exp = rough.abs().log10().floor();
1260 let frac = rough / 10f64.powf(exp);
1261
1262 let nice = if frac < 1.5 {
1263 1.0
1264 } else if frac <= 3.0 {
1265 2.0
1266 } else if frac < 7.0 {
1267 5.0
1268 } else {
1269 10.0
1270 };
1271
1272 nice * 10f64.powf(exp)
1273 }
1274
1275 /// Format a number for axis labels
1276 fn format_number(x: f64) -> String {
1277 if x == 0.0 {
1278 return "0".to_string();
1279 }
1280 let abs = x.abs();
1281 if abs >= 1000.0 || abs < 0.01 {
1282 format!("{:.1e}", x)
1283 } else if abs >= 1.0 {
1284 format!("{:.1}", x)
1285 } else {
1286 format!("{:.2}", x)
1287 }
1288 }
1289
1290 /// Convert an Expr to f64
1291 fn expr_to_f64(expr: &Expr) -> Result<f64, ()> {
1292 match expr {
1293 Expr::Integer(n) => Ok(*n as f64),
1294 Expr::Float(x) => Ok(*x),
1295 Expr::Rational(r) => Ok(r.to_f64()),
1296 _ => Err(()),
1297 }
1298 }
1299
1300 #[cfg(test)]
1301 mod tests {
1302 use super::*;
1303
1304 #[test]
1305 fn test_coordinate_transform() {
1306 let graph = Graph2D::new();
1307 let (mx, my) = graph.screen_to_math(250.0, 250.0, 500, 500);
1308 assert!((mx - 0.0).abs() < 0.01);
1309 assert!((my - 0.0).abs() < 0.01);
1310 }
1311
1312 #[test]
1313 fn test_nice_step() {
1314 assert_eq!(nice_step(0.03), 0.02);
1315 assert_eq!(nice_step(0.3), 0.2);
1316 assert_eq!(nice_step(3.0), 2.0);
1317 assert_eq!(nice_step(30.0), 20.0);
1318 }
1319 }
1320