gardesk/garcalc / 11dc068

Browse files

wire up keybindings and input parsing for new graph features

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
11dc06884a8b864a17597d8ea52d1373f33ea3d1
Parents
ace3ed3
Tree
9d98c07

1 changed file

StatusFile+-
M garcalc/src/app.rs 624 5
garcalc/src/app.rsmodified
@@ -2,7 +2,7 @@
22
 
33
 use anyhow::Result;
44
 use garcalc_cas::{Evaluator, parser};
5
-use garcalc_graph::{Graph2D, Graph3D};
5
+use garcalc_graph::{CameraPreset, Graph2D, Graph3D, COLOR_PALETTE};
66
 use garcalc_ipc::Mode;
77
 use garcalc_math::input::SpecialKey;
88
 use garcalc_math::{
@@ -10,6 +10,7 @@ use garcalc_math::{
1010
 };
1111
 use gartk_core::{InputEvent, Key, Modifiers, MouseButton};
1212
 use gartk_x11::{Connection, EventLoop, EventLoopConfig, Window, WindowConfig};
13
+use std::process::Command;
1314
 use std::time::{Duration, Instant};
1415
 
1516
 use crate::config::Config;
@@ -65,6 +66,40 @@ pub struct App {
6566
     has_focus: bool,
6667
     /// Mouse drag state for graph panning
6768
     drag_start: Option<(f64, f64)>,
69
+    /// Whether to show the function list panel
70
+    show_function_list: bool,
71
+    /// Whether zeros markers are visible
72
+    zeros_visible: bool,
73
+    /// Cached zero points: (func_index, points)
74
+    cached_zeros: Vec<(usize, Vec<(f64, f64)>)>,
75
+    /// Cached intersection points: ((idx_i, idx_j), points)
76
+    cached_intersections: Vec<((usize, usize), Vec<(f64, f64)>)>,
77
+    /// Whether table view is visible
78
+    table_visible: bool,
79
+    /// Which function index to tabulate
80
+    table_func_index: usize,
81
+    /// Table scroll offset
82
+    table_scroll_offset: usize,
83
+    /// Table step size
84
+    table_step: f64,
85
+    /// Cached table data
86
+    table_data: Vec<(f64, Option<f64>)>,
87
+    /// Whether auto-rotate is enabled (3D mode)
88
+    auto_rotate: bool,
89
+    /// Auto-rotate speed (radians per tick)
90
+    auto_rotate_speed: f64,
91
+    /// Whether the viewport settings panel is open (2D graph)
92
+    viewport_panel_open: bool,
93
+    /// Which viewport field is being edited (0=xmin, 1=xmax, 2=ymin, 3=ymax)
94
+    viewport_edit_field: usize,
95
+    /// Buffer for editing viewport values
96
+    viewport_edit_buffer: String,
97
+    /// Whether the color picker popup is open
98
+    color_picker_open: bool,
99
+    /// Which function the color picker targets
100
+    color_picker_func_index: usize,
101
+    /// Whether the 3D settings panel is open
102
+    settings3d_panel_open: bool,
68103
     /// Configuration
69104
     #[allow(dead_code)]
70105
     config: Config,
@@ -136,6 +171,23 @@ impl App {
136171
             last_cursor_blink: Instant::now(),
137172
             has_focus: false,
138173
             drag_start: None,
174
+            show_function_list: false,
175
+            zeros_visible: false,
176
+            cached_zeros: Vec::new(),
177
+            cached_intersections: Vec::new(),
178
+            table_visible: false,
179
+            table_func_index: 0,
180
+            table_scroll_offset: 0,
181
+            table_step: 1.0,
182
+            table_data: Vec::new(),
183
+            auto_rotate: false,
184
+            auto_rotate_speed: 0.02,
185
+            viewport_panel_open: false,
186
+            viewport_edit_field: 0,
187
+            viewport_edit_buffer: String::new(),
188
+            color_picker_open: false,
189
+            color_picker_func_index: 0,
190
+            settings3d_panel_open: false,
139191
             config,
140192
         })
141193
     }
@@ -185,8 +237,30 @@ impl App {
185237
                         }
186238
                     } else if self.mode == Mode::Graph {
187239
                         if mouse_ev.button == Some(MouseButton::Left) {
188
-                            // Left click - start drag for pan
189
-                            self.drag_start = Some((x, y));
240
+                            // Check viewport panel preset clicks
241
+                            if self.viewport_panel_open {
242
+                                if let Some(preset) = self.ui.viewport_preset_hit(x, y) {
243
+                                    self.graph.apply_viewport_preset(preset);
244
+                                    self.viewport_panel_open = false;
245
+                                    self.invalidate_caches();
246
+                                    ev.request_redraw();
247
+                                    // Don't start drag
248
+                                } else {
249
+                                    self.drag_start = Some((x, y));
250
+                                }
251
+                            } else if self.color_picker_open {
252
+                                if let Some(palette_idx) = self.ui.color_picker_hit(x, y) {
253
+                                    self.graph.set_function_color(self.color_picker_func_index, COLOR_PALETTE[palette_idx]);
254
+                                    self.color_picker_open = false;
255
+                                    ev.request_redraw();
256
+                                } else {
257
+                                    self.color_picker_open = false;
258
+                                    self.drag_start = Some((x, y));
259
+                                }
260
+                            } else {
261
+                                // Left click - start drag for pan
262
+                                self.drag_start = Some((x, y));
263
+                            }
190264
                         } else if mouse_ev.button == Some(MouseButton::Right) {
191265
                             // Right click - toggle trace mode
192266
                             self.graph.trace_enabled = !self.graph.trace_enabled;
@@ -279,6 +353,10 @@ impl App {
279353
                     if self.tick_cursor_blink() {
280354
                         ev.request_redraw();
281355
                     }
356
+                    if self.auto_rotate && self.mode == Mode::Graph3D {
357
+                        self.graph3d.camera.rotate(self.auto_rotate_speed, 0.0);
358
+                        ev.request_redraw();
359
+                    }
282360
                 }
283361
                 _ => {}
284362
             }
@@ -356,6 +434,13 @@ impl App {
356434
                 self.mode = Mode::Graph3D;
357435
                 return;
358436
             }
437
+            Key::F4 if self.mode == Mode::Graph => {
438
+                self.table_visible = !self.table_visible;
439
+                if self.table_visible {
440
+                    self.regenerate_table();
441
+                }
442
+                return;
443
+            }
359444
             _ => {}
360445
         }
361446
 
@@ -363,16 +448,55 @@ impl App {
363448
         match key {
364449
             Key::Char('r') if ctrl && self.mode == Mode::Graph => {
365450
                 self.graph.reset_viewport();
451
+                self.invalidate_caches();
366452
                 return;
367453
             }
368454
             Key::Char('c') if ctrl && self.mode == Mode::Graph => {
369455
                 self.graph.clear_functions();
456
+                self.invalidate_caches();
370457
                 return;
371458
             }
372459
             Key::Char('t') if ctrl && self.mode == Mode::Graph => {
373460
                 self.graph.trace_enabled = !self.graph.trace_enabled;
374461
                 return;
375462
             }
463
+            Key::Char('l') if ctrl && self.mode == Mode::Graph => {
464
+                self.show_function_list = !self.show_function_list;
465
+                return;
466
+            }
467
+            Key::Char(c @ '1'..='9') if ctrl && self.mode == Mode::Graph => {
468
+                let idx = (*c as usize) - ('1' as usize);
469
+                self.graph.toggle_visibility(idx);
470
+                self.invalidate_caches();
471
+                return;
472
+            }
473
+            Key::Char('z') if ctrl && self.mode == Mode::Graph => {
474
+                self.zeros_visible = !self.zeros_visible;
475
+                if self.zeros_visible {
476
+                    self.cached_zeros = self.graph.find_zeros();
477
+                    self.cached_intersections = self.graph.find_intersections();
478
+                }
479
+                return;
480
+            }
481
+            // 2D graph: viewport panel
482
+            Key::Char('w') if ctrl && self.mode == Mode::Graph => {
483
+                self.viewport_panel_open = !self.viewport_panel_open;
484
+                if self.viewport_panel_open {
485
+                    self.viewport_edit_field = 0;
486
+                    self.viewport_edit_buffer = format!("{:.4}", self.graph.viewport.x_min);
487
+                }
488
+                return;
489
+            }
490
+            // 2D graph: color picker
491
+            Key::Char('k') if ctrl && self.mode == Mode::Graph => {
492
+                if !self.graph.functions.is_empty() {
493
+                    self.color_picker_open = !self.color_picker_open;
494
+                    self.color_picker_func_index =
495
+                        self.color_picker_func_index.min(self.graph.functions.len().saturating_sub(1));
496
+                }
497
+                return;
498
+            }
499
+            // 3D shortcuts
376500
             Key::Char('r') if ctrl && self.mode == Mode::Graph3D => {
377501
                 self.graph3d.reset_camera();
378502
                 return;
@@ -381,6 +505,39 @@ impl App {
381505
                 self.graph3d.clear_surfaces();
382506
                 return;
383507
             }
508
+            Key::Char('m') if ctrl && self.mode == Mode::Graph3D => {
509
+                self.graph3d.cycle_render_mode();
510
+                return;
511
+            }
512
+            Key::Char('a') if ctrl && self.mode == Mode::Graph3D => {
513
+                self.auto_rotate = !self.auto_rotate;
514
+                return;
515
+            }
516
+            Key::Char('g') if ctrl && self.mode == Mode::Graph3D => {
517
+                self.graph3d.config.show_coord_planes = !self.graph3d.config.show_coord_planes;
518
+                return;
519
+            }
520
+            Key::Char('s') if ctrl && self.mode == Mode::Graph3D => {
521
+                self.settings3d_panel_open = !self.settings3d_panel_open;
522
+                return;
523
+            }
524
+            // 3D camera presets
525
+            Key::Char('1') if ctrl && self.mode == Mode::Graph3D => {
526
+                self.graph3d.camera.apply_preset(CameraPreset::Front);
527
+                return;
528
+            }
529
+            Key::Char('3') if ctrl && self.mode == Mode::Graph3D => {
530
+                self.graph3d.camera.apply_preset(CameraPreset::Side);
531
+                return;
532
+            }
533
+            Key::Char('7') if ctrl && self.mode == Mode::Graph3D => {
534
+                self.graph3d.camera.apply_preset(CameraPreset::Top);
535
+                return;
536
+            }
537
+            Key::Char('5') if ctrl && self.mode == Mode::Graph3D => {
538
+                self.graph3d.camera.apply_preset(CameraPreset::Isometric);
539
+                return;
540
+            }
384541
             _ => {}
385542
         }
386543
 
@@ -389,6 +546,113 @@ impl App {
389546
             return;
390547
         }
391548
 
549
+        // Viewport panel keys (graph mode only)
550
+        if self.viewport_panel_open && self.mode == Mode::Graph {
551
+            match key {
552
+                Key::Tab => {
553
+                    self.commit_viewport_edit();
554
+                    self.viewport_edit_field = (self.viewport_edit_field + 1) % 4;
555
+                    self.load_viewport_edit_buffer();
556
+                    return;
557
+                }
558
+                Key::Return => {
559
+                    self.commit_viewport_edit();
560
+                    self.viewport_panel_open = false;
561
+                    self.invalidate_caches();
562
+                    return;
563
+                }
564
+                Key::Escape => {
565
+                    self.viewport_panel_open = false;
566
+                    return;
567
+                }
568
+                Key::Backspace => {
569
+                    self.viewport_edit_buffer.pop();
570
+                    return;
571
+                }
572
+                Key::Char(c @ ('0'..='9' | '.' | '-')) => {
573
+                    self.viewport_edit_buffer.push(*c);
574
+                    return;
575
+                }
576
+                _ => {}
577
+            }
578
+        }
579
+
580
+        // Color picker keys (graph mode only)
581
+        if self.color_picker_open && self.mode == Mode::Graph {
582
+            if matches!(key, Key::Escape) {
583
+                self.color_picker_open = false;
584
+                return;
585
+            }
586
+        }
587
+
588
+        // 3D settings panel keys
589
+        if self.settings3d_panel_open && self.mode == Mode::Graph3D {
590
+            match key {
591
+                Key::Escape => {
592
+                    self.settings3d_panel_open = false;
593
+                    return;
594
+                }
595
+                Key::Char(']') => {
596
+                    self.graph3d.config.grid_lines = (self.graph3d.config.grid_lines + 5).min(100);
597
+                    return;
598
+                }
599
+                Key::Char('[') => {
600
+                    self.graph3d.config.grid_lines = self.graph3d.config.grid_lines.saturating_sub(5).max(10);
601
+                    return;
602
+                }
603
+                Key::Char('>') => {
604
+                    self.graph3d.config.surface_alpha = (self.graph3d.config.surface_alpha + 0.05).min(1.0);
605
+                    return;
606
+                }
607
+                Key::Char('<') => {
608
+                    self.graph3d.config.surface_alpha = (self.graph3d.config.surface_alpha - 0.05).max(0.1);
609
+                    return;
610
+                }
611
+                Key::Char('c') => {
612
+                    self.graph3d.config.colormap = self.graph3d.config.colormap.next();
613
+                    return;
614
+                }
615
+                Key::Char('m') => {
616
+                    self.graph3d.cycle_render_mode();
617
+                    return;
618
+                }
619
+                _ => {}
620
+            }
621
+        }
622
+
623
+        // Table view keys (graph mode only)
624
+        if self.table_visible && self.mode == Mode::Graph {
625
+            match key {
626
+                Key::Up => {
627
+                    if self.table_scroll_offset > 0 {
628
+                        self.table_scroll_offset -= 1;
629
+                    }
630
+                    return;
631
+                }
632
+                Key::Down => {
633
+                    if self.table_scroll_offset + 20 < self.table_data.len() {
634
+                        self.table_scroll_offset += 1;
635
+                    }
636
+                    return;
637
+                }
638
+                Key::Char('e') if ctrl => {
639
+                    self.export_table_to_clipboard();
640
+                    return;
641
+                }
642
+                Key::Char('[') => {
643
+                    self.table_step = (self.table_step / 2.0).max(0.001);
644
+                    self.regenerate_table();
645
+                    return;
646
+                }
647
+                Key::Char(']') => {
648
+                    self.table_step = (self.table_step * 2.0).min(100.0);
649
+                    self.regenerate_table();
650
+                    return;
651
+                }
652
+                _ => {}
653
+            }
654
+        }
655
+
392656
         // Plain text editor for graph/3D modes
393657
         match key {
394658
             Key::Backspace => {
@@ -759,8 +1023,30 @@ impl App {
7591023
                 let lhs = input[..eq_pos].trim();
7601024
                 let rhs = input[eq_pos + 1..].trim();
7611025
 
762
-                // Check if this is "y = f(x)" (explicit) or implicit
763
-                if lhs == "y" {
1026
+                // Check if this is "r = f(theta)" (polar), "y = f(x)" (explicit), or implicit
1027
+                if lhs == "r" {
1028
+                    match parser::parse(rhs) {
1029
+                        Ok(expr) => {
1030
+                            self.graph.add_polar(expr, (0.0, std::f64::consts::TAU));
1031
+                            self.history.push(HistoryEntry {
1032
+                                input: input.clone(),
1033
+                                result: format!(
1034
+                                    "Added polar curve {}",
1035
+                                    self.graph.functions.len()
1036
+                                ),
1037
+                                error: None,
1038
+                            });
1039
+                            self.invalidate_caches();
1040
+                        }
1041
+                        Err(e) => {
1042
+                            self.history.push(HistoryEntry {
1043
+                                input: input.clone(),
1044
+                                result: String::new(),
1045
+                                error: Some(e.to_string()),
1046
+                            });
1047
+                        }
1048
+                    }
1049
+                } else if lhs == "y" {
7641050
                     // Explicit function y = f(x)
7651051
                     match parser::parse(rhs) {
7661052
                         Ok(expr) => {
@@ -770,6 +1056,7 @@ impl App {
7701056
                                 result: format!("Added function {}", self.graph.functions.len()),
7711057
                                 error: None,
7721058
                             });
1059
+                            self.invalidate_caches();
7731060
                         }
7741061
                         Err(e) => {
7751062
                             self.history.push(HistoryEntry {
@@ -793,6 +1080,7 @@ impl App {
7931080
                                 ),
7941081
                                 error: None,
7951082
                             });
1083
+                            self.invalidate_caches();
7961084
                         }
7971085
                         Err(e) => {
7981086
                             self.history.push(HistoryEntry {
@@ -803,6 +1091,8 @@ impl App {
8031091
                         }
8041092
                     }
8051093
                 }
1094
+            } else if input.starts_with("piecewise(") || input.starts_with("pw(") {
1095
+                self.parse_piecewise(&input);
8061096
             } else if input.contains(',') && (input.contains('t') || input.starts_with('(')) {
8071097
                 // Parametric curve: (x(t), y(t)) or x(t), y(t)
8081098
                 self.parse_parametric(&input);
@@ -816,6 +1106,7 @@ impl App {
8161106
                             result: format!("Added function {}", self.graph.functions.len()),
8171107
                             error: None,
8181108
                         });
1109
+                        self.invalidate_caches();
8191110
                     }
8201111
                     Err(e) => {
8211112
                         self.history.push(HistoryEntry {
@@ -827,6 +1118,97 @@ impl App {
8271118
                 }
8281119
             }
8291120
         } else if self.mode == Mode::Graph3D {
1121
+            // Check for spherical surface: "sphere: expr" or "sph: expr"
1122
+            let sph_prefix = input.strip_prefix("sphere:")
1123
+                .or_else(|| input.strip_prefix("sph:"));
1124
+            if let Some(sph_expr) = sph_prefix {
1125
+                match parser::parse(sph_expr.trim()) {
1126
+                    Ok(expr) => {
1127
+                        self.graph3d.add_spherical(expr);
1128
+                        self.history.push(HistoryEntry {
1129
+                            input: input.clone(),
1130
+                            result: format!("Added spherical surface {}", self.graph3d.surfaces.len()),
1131
+                            error: None,
1132
+                        });
1133
+                    }
1134
+                    Err(e) => {
1135
+                        self.history.push(HistoryEntry {
1136
+                            input: input.clone(),
1137
+                            result: String::new(),
1138
+                            error: Some(e.to_string()),
1139
+                        });
1140
+                    }
1141
+                }
1142
+                self.input.clear();
1143
+                self.cursor = 0;
1144
+                self.history_index = None;
1145
+                return;
1146
+            }
1147
+
1148
+            // Check for cylindrical surface: "cyl: expr" or "cylinder: expr"
1149
+            let cyl_prefix = input.strip_prefix("cyl:")
1150
+                .or_else(|| input.strip_prefix("cylinder:"));
1151
+            if let Some(cyl_expr) = cyl_prefix {
1152
+                match parser::parse(cyl_expr.trim()) {
1153
+                    Ok(expr) => {
1154
+                        self.graph3d.add_cylindrical(expr);
1155
+                        self.history.push(HistoryEntry {
1156
+                            input: input.clone(),
1157
+                            result: format!("Added cylindrical surface {}", self.graph3d.surfaces.len()),
1158
+                            error: None,
1159
+                        });
1160
+                    }
1161
+                    Err(e) => {
1162
+                        self.history.push(HistoryEntry {
1163
+                            input: input.clone(),
1164
+                            result: String::new(),
1165
+                            error: Some(e.to_string()),
1166
+                        });
1167
+                    }
1168
+                }
1169
+                self.input.clear();
1170
+                self.cursor = 0;
1171
+                self.history_index = None;
1172
+                return;
1173
+            }
1174
+
1175
+            // Check for level surface: "level: expr = c"
1176
+            if let Some(level_str) = input.strip_prefix("level:") {
1177
+                let level_str = level_str.trim();
1178
+                if let Some(eq_pos) = level_str.find('=') {
1179
+                    let expr_str = level_str[..eq_pos].trim();
1180
+                    let level_val_str = level_str[eq_pos + 1..].trim();
1181
+                    let level_val = level_val_str.parse::<f64>().unwrap_or(0.0);
1182
+                    match parser::parse(expr_str) {
1183
+                        Ok(expr) => {
1184
+                            self.graph3d.add_level_surface(expr, level_val);
1185
+                            self.history.push(HistoryEntry {
1186
+                                input: input.clone(),
1187
+                                result: format!("Added level surface {}", self.graph3d.surfaces.len()),
1188
+                                error: None,
1189
+                            });
1190
+                        }
1191
+                        Err(e) => {
1192
+                            self.history.push(HistoryEntry {
1193
+                                input: input.clone(),
1194
+                                result: String::new(),
1195
+                                error: Some(e.to_string()),
1196
+                            });
1197
+                        }
1198
+                    }
1199
+                } else {
1200
+                    self.history.push(HistoryEntry {
1201
+                        input: input.clone(),
1202
+                        result: String::new(),
1203
+                        error: Some("Level surface format: level: f(x,y,z) = c".to_string()),
1204
+                    });
1205
+                }
1206
+                self.input.clear();
1207
+                self.cursor = 0;
1208
+                self.history_index = None;
1209
+                return;
1210
+            }
1211
+
8301212
             // Check for parametric surface: (x(u,v), y(u,v), z(u,v))
8311213
             if input.contains(',') && (input.contains('u') || input.starts_with('(')) {
8321214
                 self.parse_parametric_surface(&input);
@@ -951,6 +1333,7 @@ impl App {
9511333
                     result: format!("Added parametric curve {}", self.graph.functions.len()),
9521334
                     error: None,
9531335
                 });
1336
+                self.invalidate_caches();
9541337
             }
9551338
             (Err(e), _) | (_, Err(e)) => {
9561339
                 self.history.push(HistoryEntry {
@@ -1035,6 +1418,227 @@ impl App {
10351418
         }
10361419
     }
10371420
 
1421
+    /// Parse and add a piecewise function
1422
+    fn parse_piecewise(&mut self, input: &str) {
1423
+        let inner = input
1424
+            .trim()
1425
+            .strip_prefix("piecewise(")
1426
+            .or_else(|| input.trim().strip_prefix("pw("))
1427
+            .and_then(|s| s.strip_suffix(')'));
1428
+
1429
+        let inner = match inner {
1430
+            Some(s) => s,
1431
+            None => {
1432
+                self.history.push(HistoryEntry {
1433
+                    input: input.to_string(),
1434
+                    result: String::new(),
1435
+                    error: Some(
1436
+                        "Invalid piecewise syntax. Use: piecewise(expr1, cond1, expr2, cond2, ...)"
1437
+                            .to_string(),
1438
+                    ),
1439
+                });
1440
+                return;
1441
+            }
1442
+        };
1443
+
1444
+        let parts: Vec<&str> = inner.split(',').collect();
1445
+        if parts.len() < 2 || parts.len() % 2 != 0 {
1446
+            self.history.push(HistoryEntry {
1447
+                input: input.to_string(),
1448
+                result: String::new(),
1449
+                error: Some(
1450
+                    "Piecewise needs pairs: expr1, cond1, expr2, cond2, ...".to_string(),
1451
+                ),
1452
+            });
1453
+            return;
1454
+        }
1455
+
1456
+        let mut pieces = Vec::new();
1457
+        for chunk in parts.chunks(2) {
1458
+            let expr_str = chunk[0].trim();
1459
+            let cond_str = chunk[1].trim();
1460
+
1461
+            let expr = match parser::parse(expr_str) {
1462
+                Ok(e) => e,
1463
+                Err(e) => {
1464
+                    self.history.push(HistoryEntry {
1465
+                        input: input.to_string(),
1466
+                        result: String::new(),
1467
+                        error: Some(format!("Parse error in '{}': {}", expr_str, e)),
1468
+                    });
1469
+                    return;
1470
+                }
1471
+            };
1472
+
1473
+            let condition = match Self::parse_condition(cond_str) {
1474
+                Some(c) => c,
1475
+                None => {
1476
+                    self.history.push(HistoryEntry {
1477
+                        input: input.to_string(),
1478
+                        result: String::new(),
1479
+                        error: Some(format!(
1480
+                            "Invalid condition: '{}'. Use: x<0, x>=2, else",
1481
+                            cond_str
1482
+                        )),
1483
+                    });
1484
+                    return;
1485
+                }
1486
+            };
1487
+
1488
+            pieces.push(garcalc_graph::PiecewisePiece { expr, condition });
1489
+        }
1490
+
1491
+        self.graph.add_piecewise(pieces);
1492
+        self.history.push(HistoryEntry {
1493
+            input: input.to_string(),
1494
+            result: format!("Added piecewise function {}", self.graph.functions.len()),
1495
+            error: None,
1496
+        });
1497
+        self.invalidate_caches();
1498
+    }
1499
+
1500
+    fn parse_condition(s: &str) -> Option<garcalc_graph::PiecewiseCondition> {
1501
+        use garcalc_graph::PiecewiseCondition;
1502
+        let s = s.trim();
1503
+
1504
+        if s == "else" || s == "otherwise" {
1505
+            return Some(PiecewiseCondition::Always);
1506
+        }
1507
+
1508
+        // Try range conditions like "0 <= x < 3"
1509
+        if let Some(cond) = Self::parse_range_condition(s) {
1510
+            return Some(cond);
1511
+        }
1512
+
1513
+        // Try "x >= val", "x > val", "x <= val", "x < val"
1514
+        for (op, ctor) in [
1515
+            (
1516
+                ">=",
1517
+                PiecewiseCondition::GreaterEqual as fn(f64) -> PiecewiseCondition,
1518
+            ),
1519
+            (
1520
+                "<=",
1521
+                PiecewiseCondition::LessEqual as fn(f64) -> PiecewiseCondition,
1522
+            ),
1523
+            (
1524
+                ">",
1525
+                PiecewiseCondition::GreaterThan as fn(f64) -> PiecewiseCondition,
1526
+            ),
1527
+            (
1528
+                "<",
1529
+                PiecewiseCondition::LessThan as fn(f64) -> PiecewiseCondition,
1530
+            ),
1531
+        ] {
1532
+            if let Some(rest) = s
1533
+                .strip_prefix("x")
1534
+                .and_then(|r| r.trim_start().strip_prefix(op))
1535
+            {
1536
+                if let Ok(val) = rest.trim().parse::<f64>() {
1537
+                    return Some(ctor(val));
1538
+                }
1539
+            }
1540
+        }
1541
+
1542
+        None
1543
+    }
1544
+
1545
+    fn parse_range_condition(s: &str) -> Option<garcalc_graph::PiecewiseCondition> {
1546
+        use garcalc_graph::PiecewiseCondition;
1547
+        let s = s.trim();
1548
+
1549
+        let x_pos = s.find('x')?;
1550
+        let left = s[..x_pos].trim();
1551
+        let right = s[x_pos + 1..].trim();
1552
+
1553
+        if left.is_empty() || right.is_empty() {
1554
+            return None;
1555
+        }
1556
+
1557
+        let (left_val, left_inclusive) = if let Some(rest) = left.strip_suffix("<=") {
1558
+            (rest.trim().parse::<f64>().ok()?, true)
1559
+        } else if let Some(rest) = left.strip_suffix("<") {
1560
+            (rest.trim().parse::<f64>().ok()?, false)
1561
+        } else {
1562
+            return None;
1563
+        };
1564
+
1565
+        let (right_val, right_inclusive) = if let Some(rest) = right.strip_prefix("<=") {
1566
+            (rest.trim().parse::<f64>().ok()?, true)
1567
+        } else if let Some(rest) = right.strip_prefix("<") {
1568
+            (rest.trim().parse::<f64>().ok()?, false)
1569
+        } else {
1570
+            return None;
1571
+        };
1572
+
1573
+        if left_inclusive && right_inclusive {
1574
+            Some(PiecewiseCondition::BetweenInclusive(left_val, right_val))
1575
+        } else {
1576
+            Some(PiecewiseCondition::Between(left_val, right_val))
1577
+        }
1578
+    }
1579
+
1580
+    fn commit_viewport_edit(&mut self) {
1581
+        if let Ok(val) = self.viewport_edit_buffer.parse::<f64>() {
1582
+            match self.viewport_edit_field {
1583
+                0 => self.graph.viewport.x_min = val,
1584
+                1 => self.graph.viewport.x_max = val,
1585
+                2 => self.graph.viewport.y_min = val,
1586
+                3 => self.graph.viewport.y_max = val,
1587
+                _ => {}
1588
+            }
1589
+        }
1590
+    }
1591
+
1592
+    fn load_viewport_edit_buffer(&mut self) {
1593
+        self.viewport_edit_buffer = match self.viewport_edit_field {
1594
+            0 => format!("{:.4}", self.graph.viewport.x_min),
1595
+            1 => format!("{:.4}", self.graph.viewport.x_max),
1596
+            2 => format!("{:.4}", self.graph.viewport.y_min),
1597
+            3 => format!("{:.4}", self.graph.viewport.y_max),
1598
+            _ => String::new(),
1599
+        };
1600
+    }
1601
+
1602
+    fn invalidate_caches(&mut self) {
1603
+        if self.zeros_visible {
1604
+            self.cached_zeros = self.graph.find_zeros();
1605
+            self.cached_intersections = self.graph.find_intersections();
1606
+        }
1607
+        if self.table_visible {
1608
+            self.regenerate_table();
1609
+        }
1610
+    }
1611
+
1612
+    fn regenerate_table(&mut self) {
1613
+        let x_min = self.graph.viewport.x_min;
1614
+        let x_max = self.graph.viewport.x_max;
1615
+        self.table_data =
1616
+            self.graph
1617
+                .generate_table(self.table_func_index, x_min, x_max, self.table_step);
1618
+        self.table_scroll_offset = 0;
1619
+    }
1620
+
1621
+    fn export_table_to_clipboard(&self) {
1622
+        let mut csv = String::from("x\tf(x)\n");
1623
+        for (x, y) in &self.table_data {
1624
+            match y {
1625
+                Some(yv) => csv.push_str(&format!("{:.6}\t{:.6}\n", x, yv)),
1626
+                None => csv.push_str(&format!("{:.6}\tundefined\n", x)),
1627
+            }
1628
+        }
1629
+        let _ = Command::new("xclip")
1630
+            .args(["-selection", "clipboard"])
1631
+            .stdin(std::process::Stdio::piped())
1632
+            .spawn()
1633
+            .and_then(|mut child| {
1634
+                use std::io::Write;
1635
+                if let Some(ref mut stdin) = child.stdin {
1636
+                    let _ = stdin.write_all(csv.as_bytes());
1637
+                }
1638
+                child.wait()
1639
+            });
1640
+    }
1641
+
10381642
     fn render(&mut self) -> Result<()> {
10391643
         let math_input = if self.mode == Mode::Calculator {
10401644
             Some(&self.math_input)
@@ -1052,6 +1656,21 @@ impl App {
10521656
             &self.graph3d,
10531657
             self.help_modal_open,
10541658
             self.calc_buttons_extended,
1659
+            self.show_function_list,
1660
+            self.zeros_visible,
1661
+            &self.cached_zeros,
1662
+            &self.cached_intersections,
1663
+            self.table_visible,
1664
+            self.table_scroll_offset,
1665
+            self.table_step,
1666
+            &self.table_data,
1667
+            self.viewport_panel_open,
1668
+            self.viewport_edit_field,
1669
+            &self.viewport_edit_buffer,
1670
+            self.color_picker_open,
1671
+            self.color_picker_func_index,
1672
+            self.settings3d_panel_open,
1673
+            self.auto_rotate,
10551674
         )?;
10561675
         Ok(())
10571676
     }