openrr_gui/
velocity_sender.rs

1#![allow(deprecated)] // TODO: RetainedImage is deprecated in egui 0.23, but there is no good alternative.
2
3use arci::{BaseVelocity, MoveBase};
4use eframe::egui;
5use egui_extras::RetainedImage;
6use tracing::{debug, error};
7
8const DEFAULT_ACTIVE_VELOCITY_VALUE: f64 = 0.25;
9
10/// Launches GUI that send base velocity from GUI to the given `move_base`.
11#[cfg(not(target_family = "wasm"))]
12pub fn velocity_sender<M>(move_base: M) -> Result<(), crate::Error>
13where
14    M: MoveBase + 'static,
15{
16    let native_options = eframe::NativeOptions {
17        viewport: egui::ViewportBuilder::default()
18            .with_inner_size([400.0, 400.0])
19            .with_icon(
20                eframe::icon_data::from_png_bytes(include_bytes!("../assets/icon/openrr.png"))
21                    .unwrap(),
22            ),
23        ..eframe::NativeOptions::default()
24    };
25    eframe::run_native(
26        "Velocity Sender",
27        native_options,
28        Box::new(|_cc| Ok(Box::new(VelocitySender::new(move_base)))),
29    )
30    .map_err(|e| crate::Error::Other(e.to_string()))?; // eframe::Error is not Send
31    Ok(())
32}
33
34struct VelocitySender<M>
35where
36    M: MoveBase + 'static,
37{
38    move_base: M,
39
40    velocity: BaseVelocity,
41    show_velocity: bool,
42
43    up_button: RetainedImage,
44    down_button: RetainedImage,
45    left_button: RetainedImage,
46    right_button: RetainedImage,
47    counterclockwise_button: RetainedImage,
48    clockwise_button: RetainedImage,
49}
50
51impl<M> VelocitySender<M>
52where
53    M: MoveBase + 'static,
54{
55    fn new(move_base: M) -> Self {
56        macro_rules! button_image {
57            ($alt:expr, $img_path:expr $(,)?) => {
58                RetainedImage::from_image_bytes(
59                    $alt,
60                    include_bytes!(concat!("../assets/material-design-icons/", $img_path)),
61                )
62                .unwrap()
63            };
64        }
65
66        Self {
67            move_base,
68            velocity: BaseVelocity {
69                x: 0.0,
70                y: 0.0,
71                theta: 0.0,
72            },
73            up_button: button_image!("↑", "baseline_arrow_upward_white_24dp.png"),
74            down_button: button_image!("↓", "baseline_arrow_downward_white_24dp.png"),
75            left_button: button_image!("←", "baseline_arrow_back_white_24dp.png"),
76            right_button: button_image!("→", "baseline_arrow_forward_white_24dp.png"),
77            // ↺
78            counterclockwise_button: button_image!("CCW", "baseline_undo_white_24dp_rotate90.png"),
79            // ↻
80            clockwise_button: button_image!("CW", "baseline_redo_white_24dp_rotate270.png"),
81            show_velocity: true,
82        }
83    }
84}
85
86impl<M> eframe::App for VelocitySender<M>
87where
88    M: MoveBase + 'static,
89{
90    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
91        egui::CentralPanel::default().show(ctx, |ui| {
92            egui::warn_if_debug_build(ui);
93
94            ui.add_space(20.0);
95            ui.horizontal(|ui| {
96                ui.add_space(180.0);
97                if ui
98                    .add(
99                        egui::Button::image((
100                            self.up_button.texture_id(ctx),
101                            self.up_button.size_vec2(),
102                        ))
103                        .selected(self.velocity.x > 0.0),
104                    )
105                    .clicked()
106                {
107                    if self.velocity.x > 0.0 {
108                        self.velocity.x = 0.0;
109                    } else {
110                        self.velocity.x = DEFAULT_ACTIVE_VELOCITY_VALUE;
111                    }
112                }
113            });
114            ui.horizontal(|ui| {
115                ui.add_space(100.0);
116                if ui
117                    .add(
118                        egui::Button::image((
119                            self.clockwise_button.texture_id(ctx),
120                            self.clockwise_button.size_vec2(),
121                        ))
122                        .selected(self.velocity.theta < 0.0),
123                    )
124                    .clicked()
125                {
126                    if self.velocity.theta < 0.0 {
127                        self.velocity.theta = 0.0;
128                    } else {
129                        self.velocity.theta = -DEFAULT_ACTIVE_VELOCITY_VALUE;
130                    }
131                }
132                if ui
133                    .add(
134                        egui::Button::image((
135                            self.left_button.texture_id(ctx),
136                            self.left_button.size_vec2(),
137                        ))
138                        .selected(self.velocity.y > 0.0),
139                    )
140                    .clicked()
141                {
142                    if self.velocity.y > 0.0 {
143                        self.velocity.y = 0.0;
144                    } else {
145                        self.velocity.y = DEFAULT_ACTIVE_VELOCITY_VALUE;
146                    }
147                }
148                if ui.button("Stop").clicked() {
149                    self.velocity.x = 0.0;
150                    self.velocity.y = 0.0;
151                    self.velocity.theta = 0.0;
152                }
153                if ui
154                    .add(
155                        egui::Button::image((
156                            self.right_button.texture_id(ctx),
157                            self.right_button.size_vec2(),
158                        ))
159                        .selected(self.velocity.y < 0.0),
160                    )
161                    .clicked()
162                {
163                    if self.velocity.y < 0.0 {
164                        self.velocity.y = 0.0;
165                    } else {
166                        self.velocity.y = -DEFAULT_ACTIVE_VELOCITY_VALUE;
167                    }
168                }
169                if ui
170                    .add(
171                        egui::Button::image((
172                            self.counterclockwise_button.texture_id(ctx),
173                            self.counterclockwise_button.size_vec2(),
174                        ))
175                        .selected(self.velocity.theta > 0.0),
176                    )
177                    .clicked()
178                {
179                    if self.velocity.theta > 0.0 {
180                        self.velocity.theta = 0.0;
181                    } else {
182                        self.velocity.theta = DEFAULT_ACTIVE_VELOCITY_VALUE;
183                    }
184                }
185            });
186            ui.horizontal(|ui| {
187                ui.add_space(180.0);
188                if ui
189                    .add(
190                        egui::Button::image((
191                            self.down_button.texture_id(ctx),
192                            self.down_button.size_vec2(),
193                        ))
194                        .selected(self.velocity.x < 0.0),
195                    )
196                    .clicked()
197                {
198                    if self.velocity.x < 0.0 {
199                        self.velocity.x = 0.0;
200                    } else {
201                        self.velocity.x = -DEFAULT_ACTIVE_VELOCITY_VALUE;
202                    }
203                }
204            });
205
206            ui.add_space(100.0);
207            ui.horizontal(|ui| {
208                ui.add_space(140.0);
209                ui.vertical(|ui| {
210                    if self.show_velocity {
211                        ui.add(egui::Slider::new(&mut self.velocity.x, -1.0..=1.0).text("x"));
212                        ui.add(egui::Slider::new(&mut self.velocity.y, -1.0..=1.0).text("y"));
213                        ui.add(egui::Slider::new(&mut self.velocity.theta, -1.0..=1.0).text("θ"));
214                    }
215                });
216            });
217
218            ui.add_space(20.0);
219            ui.horizontal(|ui| {
220                ui.add_space(240.0);
221                ui.checkbox(&mut self.show_velocity, "Show details");
222            });
223
224            // TODO: Regularly wake up Ui threads.
225            debug!(?self.velocity, "send_velocity");
226            if let Err(e) = self.move_base.send_velocity(&self.velocity) {
227                error!("{e}");
228                ui.colored_label(ui.visuals().error_fg_color, format!("Error: {e:#}"));
229            }
230        });
231    }
232}
233
234#[cfg(test)]
235mod test {
236    use arci::DummyMoveBase;
237    use assert_approx_eq::assert_approx_eq;
238
239    use super::*;
240
241    #[test]
242    fn test_velocity_sender() {
243        let move_base = DummyMoveBase::new();
244
245        let velocity_sender = VelocitySender::new(move_base);
246
247        assert_approx_eq!(velocity_sender.velocity.x, 0.0);
248        assert_approx_eq!(velocity_sender.velocity.y, 0.0);
249        assert_approx_eq!(velocity_sender.velocity.theta, 0.0);
250    }
251}