diff --git a/Cargo.lock b/Cargo.lock index ccac53c..c8dd245 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,6 +30,8 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5351dcebb14b579ccab05f288596b2ae097005be7ee50a7c3d4ca9d0d5a66f6a" dependencies = [ + "enumn", + "serde", "uuid", ] @@ -225,6 +227,7 @@ dependencies = [ "cfg-if", "getrandom 0.3.4", "once_cell", + "serde", "version_check", "zerocopy", ] @@ -1449,6 +1452,7 @@ checksum = "55f6cc0cb3b84a21232c468db972ebcddd34decbf1ff02cdebffd807c13bbd81" dependencies = [ "bytemuck", "emath", + "serde", ] [[package]] @@ -1466,6 +1470,7 @@ dependencies = [ "egui_glow", "glutin", "glutin-winit", + "home", "image", "js-sys", "log", @@ -1477,6 +1482,8 @@ dependencies = [ "pollster", "profiling", "raw-window-handle", + "ron", + "serde", "static_assertions", "wasm-bindgen", "wasm-bindgen-futures", @@ -1501,6 +1508,8 @@ dependencies = [ "log", "nohash-hasher", "profiling", + "ron", + "serde", "smallvec", "unicode-segmentation", ] @@ -1541,6 +1550,7 @@ dependencies = [ "objc2-ui-kit 0.3.2", "profiling", "raw-window-handle", + "serde", "smithay-clipboard", "web-time", "webbrowser", @@ -1597,6 +1607,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a74fbbf7501c430b89df62d102b6bfa02162faaf3e155512c677c9d20f5708d1" dependencies = [ "bytemuck", + "serde", ] [[package]] @@ -1635,6 +1646,17 @@ dependencies = [ "syn", ] +[[package]] +name = "enumn" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "epaint" version = "0.34.2" @@ -1652,6 +1674,7 @@ dependencies = [ "parking_lot", "profiling", "self_cell", + "serde", "skrifa", "smallvec", "vello_cpu", @@ -1868,6 +1891,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" dependencies = [ "bytemuck", + "serde", ] [[package]] @@ -2360,6 +2384,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "htmlparser" version = "0.2.1" @@ -4819,6 +4852,20 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "ron" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4147b952f3f819eca0e99527022f7d6a8d05f111aeb0a62960c74eb283bec8fc" +dependencies = [ + "bitflags 2.11.1", + "once_cell", + "serde", + "serde_derive", + "typeid", + "unicode-ident", +] + [[package]] name = "rowan" version = "0.16.1" @@ -5216,6 +5263,9 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "smithay-client-toolkit" @@ -5461,6 +5511,7 @@ dependencies = [ "egui_plot", "fuzzy-matcher", "libsvld", + "rfd", ] [[package]] @@ -5839,6 +5890,12 @@ dependencies = [ "serde", ] +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typenum" version = "1.20.0" diff --git a/svldplotter/Cargo.toml b/svldplotter/Cargo.toml index a3d6835..0e2cf6f 100644 --- a/svldplotter/Cargo.toml +++ b/svldplotter/Cargo.toml @@ -12,8 +12,12 @@ bench = false libsvld = { path = "../libsvld" } can-dbc = "9.1" -eframe = "0.34" +eframe = { + version = "0.34", + features = ["persistence"] +} egui = "0.34" egui_plot = "0.35" egui_ltreeview = "0.7" fuzzy-matcher = "0.3" +rfd = "0.17" diff --git a/svldplotter/src/main.rs b/svldplotter/src/main.rs index 4498e30..1d2686f 100644 --- a/svldplotter/src/main.rs +++ b/svldplotter/src/main.rs @@ -3,7 +3,13 @@ mod plot_list; mod signal; mod tree; -use std::{fs, path::Path}; +use std::{ + collections::HashMap, + fs, + path::PathBuf, + sync::{mpsc::{channel, Receiver}, Arc}, + thread, +}; use can_dbc::Dbc; use eframe::egui; @@ -20,50 +26,171 @@ use crate::tree::{build_tree, decode_signal_node_id, show_dbc_tree, MsgNode}; fn main() { let native_options = eframe::NativeOptions::default(); eframe::run_native( - "My egui App", + "svldplotter", native_options, - Box::new(|cc| Ok(Box::new(MyEguiApp::new(cc)))), + Box::new(|cc| { + let mut app = MyEguiApp::default(); + // Optional CLI quick-load: `svldplotter [DBC [ASC]]`. Both args + // are optional and intended for dev iteration. + let args: Vec = std::env::args().collect(); + if let Some(dbc) = args.get(1) { + app.load_dbc(PathBuf::from(dbc)); + } + if let Some(asc) = args.get(2) { + app.asc_paths.push(PathBuf::from(asc)); + app.set_active_asc(app.asc_paths.len() - 1, Some(&cc.egui_ctx)); + } + Ok(Box::new(app)) + }), ) .unwrap(); } +#[derive(Default, PartialEq, Eq)] +enum LeftTab { + #[default] + Signals, + Files, +} + +struct DecodeJob { + asc_index: usize, + asc_path: PathBuf, + receiver: Receiver>, +} + +#[derive(Default)] struct MyEguiApp { - data: DecodeResult, + dbc: Option>, + dbc_path: Option, + asc_paths: Vec, + active_asc: Option, + data: Option>, + decode_cache: HashMap>, + pending: Option, tree: Vec, filter: String, plots: Vec, next_plot_id: u64, cursor_x: Option, + left_tab: LeftTab, } impl MyEguiApp { - fn new(_cc: &eframe::CreationContext<'_>) -> Self { - let args: Vec<_> = std::env::args().collect(); - - let dbc_text = - fs::read_to_string(Path::new(&args[1])).expect("Unable to read input file"); - let dbc = Dbc::try_from(dbc_text.as_str()).expect("Failed to parse dbc file"); - - let asc_path = Path::new(&args[2]); - let data = libsvld::decode(&dbc, asc_path); - let tree = build_tree(&dbc, &data); - - Self { - data, - tree, - filter: String::new(), - plots: Vec::new(), - next_plot_id: 1, - cursor_x: None, + fn load_dbc(&mut self, path: PathBuf) { + let Ok(text) = fs::read_to_string(&path) else { + return; + }; + let Ok(dbc) = Dbc::try_from(text.as_str()) else { + return; + }; + self.dbc = Some(Arc::new(dbc)); + self.dbc_path = Some(path); + // DBC change invalidates the decode cache. + self.decode_cache.clear(); + self.pending = None; + self.data = None; + self.tree.clear(); + if let Some(idx) = self.active_asc { + self.start_or_use_cached(idx, None); } } + fn add_asc(&mut self, path: PathBuf) { + self.asc_paths.push(path); + if self.active_asc.is_none() { + self.set_active_asc(self.asc_paths.len() - 1, None); + } + } + + /// Switch to a given ASC. Use the cached DecodeResult if present; + /// otherwise spawn a background decode and show progress via the + /// `pending` job. + fn set_active_asc(&mut self, idx: usize, ctx: Option<&egui::Context>) { + if idx >= self.asc_paths.len() { + return; + } + self.active_asc = Some(idx); + self.start_or_use_cached(idx, ctx); + } + + fn start_or_use_cached(&mut self, idx: usize, ctx: Option<&egui::Context>) { + let Some(dbc) = self.dbc.clone() else { + return; + }; + let path = self.asc_paths[idx].clone(); + if self.decode_cache.contains_key(&path) { + self.activate_decoded(idx); + return; + } + let (tx, rx) = channel(); + let dbc_for_thread = dbc; + let path_for_thread = path.clone(); + let ctx_for_thread = ctx.cloned(); + thread::spawn(move || { + let result = libsvld::decode(&dbc_for_thread, &path_for_thread); + let _ = tx.send(Arc::new(result)); + if let Some(ctx) = ctx_for_thread { + ctx.request_repaint(); + } + }); + self.pending = Some(DecodeJob { + asc_index: idx, + asc_path: path, + receiver: rx, + }); + } + + /// Pull a finished decode out of `pending` and install it. + fn poll_pending(&mut self) { + let Some(job) = self.pending.as_ref() else { + return; + }; + let Ok(result) = job.receiver.try_recv() else { + return; + }; + let asc_index = job.asc_index; + let asc_path = job.asc_path.clone(); + self.pending = None; + self.decode_cache.insert(asc_path, result); + if self.active_asc == Some(asc_index) { + self.activate_decoded(asc_index); + } + } + + /// Point `self.data`/`self.tree` at the cached result for `idx` and + /// re-resolve every plotted signal against the new data. + fn activate_decoded(&mut self, idx: usize) { + let Some(dbc) = self.dbc.as_ref() else { + return; + }; + let path = &self.asc_paths[idx]; + let Some(data) = self.decode_cache.get(path).cloned() else { + return; + }; + self.tree = build_tree(dbc, &data); + // Re-resolve plotted signals so existing plots survive an ASC switch. + for plot in &mut self.plots { + for sig in &mut plot.signals { + if let Some((msg_id, sig_idx)) = decode_signal_node_id(sig.id) { + if let Some(new_sig) = compute_signal(&data, msg_id, sig_idx, sig.id) { + *sig = new_sig; + } + } + } + } + self.data = Some(data); + } + fn handle_dbc_toggles(&mut self, toggles: Vec) { + let Some(data) = self.data.clone() else { + return; + }; for id in toggles { if contains_signal(&self.plots, id) { remove_signal(&mut self.plots, id); } else if let Some((msg_id, sig_idx)) = decode_signal_node_id(id) { - if let Some(sig) = compute_signal(&self.data, msg_id, sig_idx, id) { + if let Some(sig) = compute_signal(&data, msg_id, sig_idx, id) { add_signal_auto(&mut self.plots, &mut self.next_plot_id, sig); } } @@ -82,8 +209,64 @@ impl MyEguiApp { } } +enum FilesAction { + LoadDbc(PathBuf), + AddAsc(PathBuf), + SetActive(usize), +} + +fn show_files_tab( + ui: &mut egui::Ui, + dbc_path: Option<&PathBuf>, + asc_paths: &[PathBuf], + active: Option, +) -> Vec { + let mut actions = Vec::new(); + ui.horizontal(|ui| { + if ui.button("Load DBC").clicked() { + if let Some(p) = rfd::FileDialog::new() + .add_filter("DBC files", &["dbc"]) + .pick_file() + { + actions.push(FilesAction::LoadDbc(p)); + } + } + let label = dbc_path + .map(|p| p.file_name().map(|s| s.to_string_lossy().into_owned()).unwrap_or_default()) + .unwrap_or_else(|| "(none)".to_string()); + ui.label(label); + }); + ui.separator(); + ui.horizontal(|ui| { + if ui.button("Add ASC").clicked() { + if let Some(p) = rfd::FileDialog::new() + .add_filter("ASC files", &["asc"]) + .pick_file() + { + actions.push(FilesAction::AddAsc(p)); + } + } + ui.label(format!("{} file(s)", asc_paths.len())); + }); + egui::ScrollArea::vertical().show(ui, |ui| { + for (i, path) in asc_paths.iter().enumerate() { + let name = path + .file_name() + .map(|s| s.to_string_lossy().into_owned()) + .unwrap_or_else(|| path.display().to_string()); + if ui.selectable_label(active == Some(i), name).clicked() { + actions.push(FilesAction::SetActive(i)); + } + } + }); + actions +} + impl eframe::App for MyEguiApp { fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) { + self.poll_pending(); + + let ctx = ui.ctx().clone(); egui::Panel::left(egui::Id::new("LeftPanel")) .resizable(true) .show_inside(ui, |ui| { @@ -92,13 +275,66 @@ impl eframe::App for MyEguiApp { .default_size(220.0) .show_inside(ui, |ui| show_plot_list(ui, &self.plots)) .inner; - let toggles = egui::CentralPanel::default() - .show_inside(ui, |ui| show_dbc_tree(ui, &mut self.filter, &self.tree)) + let (toggles, files_actions) = egui::CentralPanel::default() + .show_inside(ui, |ui| { + ui.horizontal(|ui| { + ui.selectable_value( + &mut self.left_tab, + LeftTab::Signals, + "Signals", + ); + ui.selectable_value( + &mut self.left_tab, + LeftTab::Files, + "Files", + ); + }); + ui.separator(); + match self.left_tab { + LeftTab::Signals => ( + show_dbc_tree(ui, &mut self.filter, &self.tree), + Vec::new(), + ), + LeftTab::Files => ( + Vec::new(), + show_files_tab( + ui, + self.dbc_path.as_ref(), + &self.asc_paths, + self.active_asc, + ), + ), + } + }) .inner; self.handle_dbc_toggles(toggles); self.handle_plot_actions(plot_actions); + for action in files_actions { + match action { + FilesAction::LoadDbc(p) => self.load_dbc(p), + FilesAction::AddAsc(p) => self.add_asc(p), + FilesAction::SetActive(i) => self.set_active_asc(i, Some(&ctx)), + } + } }); show_plot_panel(ui, &self.plots, &mut self.cursor_x); + + if let Some(job) = self.pending.as_ref() { + let name = job + .asc_path + .file_name() + .map(|s| s.to_string_lossy().into_owned()) + .unwrap_or_else(|| job.asc_path.display().to_string()); + egui::Modal::new(egui::Id::new("DecodingModal")).show(&ctx, |ui| { + ui.set_min_width(280.0); + ui.vertical_centered(|ui| { + ui.heading("Decoding..."); + ui.add_space(6.0); + ui.label(name); + ui.add_space(6.0); + ui.spinner(); + }); + }); + } } } -