plotter refactor
This commit is contained in:
+30
-307
@@ -1,33 +1,25 @@
|
|||||||
|
mod plot;
|
||||||
|
mod signal;
|
||||||
|
mod tree;
|
||||||
|
|
||||||
use std::{fs, path::Path};
|
use std::{fs, path::Path};
|
||||||
|
|
||||||
use can_dbc::Dbc;
|
use can_dbc::Dbc;
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
use egui::{ScrollArea, Vec2b};
|
|
||||||
use egui_ltreeview::{Action, TreeView};
|
|
||||||
use egui_plot::{AxisHints, Legend, Line, Plot, PlotPoint, PlotPoints, VPlacement};
|
|
||||||
use libsvld::DecodeResult;
|
use libsvld::DecodeResult;
|
||||||
|
|
||||||
|
use crate::plot::show_plot_panel;
|
||||||
|
use crate::signal::{compute_signal, PlottedSignal};
|
||||||
|
use crate::tree::{build_tree, decode_signal_node_id, show_side_panel, MsgNode};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let native_options = eframe::NativeOptions::default();
|
let native_options = eframe::NativeOptions::default();
|
||||||
eframe::run_native("My egui App", native_options, Box::new(|cc| Ok(Box::new(MyEguiApp::new(cc))))).unwrap();
|
eframe::run_native(
|
||||||
}
|
"My egui App",
|
||||||
|
native_options,
|
||||||
struct SigNode {
|
Box::new(|cc| Ok(Box::new(MyEguiApp::new(cc)))),
|
||||||
id: u64,
|
)
|
||||||
name: String,
|
.unwrap();
|
||||||
}
|
|
||||||
|
|
||||||
struct MsgNode {
|
|
||||||
id: u64,
|
|
||||||
label: String,
|
|
||||||
signals: Vec<SigNode>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct PlottedSignal {
|
|
||||||
id: u64,
|
|
||||||
label: String,
|
|
||||||
unit: String,
|
|
||||||
points: Vec<PlotPoint>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MyEguiApp {
|
struct MyEguiApp {
|
||||||
@@ -40,16 +32,14 @@ struct MyEguiApp {
|
|||||||
|
|
||||||
impl MyEguiApp {
|
impl MyEguiApp {
|
||||||
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
||||||
let args: Vec::<_> = std::env::args().collect();
|
let args: Vec<_> = std::env::args().collect();
|
||||||
|
|
||||||
let data =
|
let dbc_text =
|
||||||
fs::read_to_string(Path::new(&args[1])).expect("Unable to read input file");
|
fs::read_to_string(Path::new(&args[1])).expect("Unable to read input file");
|
||||||
let dbc = Dbc::try_from(data.as_str()).expect("Failed to parse dbc file");
|
let dbc = Dbc::try_from(dbc_text.as_str()).expect("Failed to parse dbc file");
|
||||||
|
|
||||||
let asc_path = Path::new(&args[2]);
|
let asc_path = Path::new(&args[2]);
|
||||||
|
|
||||||
let data = libsvld::decode(&dbc, asc_path);
|
let data = libsvld::decode(&dbc, asc_path);
|
||||||
|
|
||||||
let tree = build_tree(&dbc, &data);
|
let tree = build_tree(&dbc, &data);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@@ -60,291 +50,24 @@ impl MyEguiApp {
|
|||||||
cursor_x: None,
|
cursor_x: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_signal(
|
fn apply_toggles(&mut self, toggles: Vec<u64>) {
|
||||||
data: &DecodeResult,
|
for id in toggles {
|
||||||
msg_id: u32,
|
if let Some(pos) = self.plotted.iter().position(|p| p.id == id) {
|
||||||
sig_idx: usize,
|
self.plotted.remove(pos);
|
||||||
id: u64,
|
} else if let Some((msg_id, sig_idx)) = decode_signal_node_id(id) {
|
||||||
) -> Option<PlottedSignal> {
|
if let Some(sig) = compute_signal(&self.data, msg_id, sig_idx, id) {
|
||||||
let meta = data.metadata.get(&msg_id)?;
|
self.plotted.push(sig);
|
||||||
let sig = meta.signals.get(sig_idx)?;
|
}
|
||||||
let frames = data.data.get(&msg_id)?;
|
|
||||||
let mut points = Vec::with_capacity(frames.len());
|
|
||||||
for frame in frames {
|
|
||||||
let bytes = u64::from_le_bytes(frame.data);
|
|
||||||
points.push(PlotPoint {
|
|
||||||
x: frame.timestamp,
|
|
||||||
y: libsvld::motec::extract_signal(bytes, &sig.signal),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Some(PlottedSignal {
|
|
||||||
id,
|
|
||||||
label: sig.name.clone(),
|
|
||||||
unit: sig.unit.clone(),
|
|
||||||
points,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_tree(dbc: &Dbc, data: &DecodeResult) -> Vec<MsgNode> {
|
|
||||||
let mut msgs: Vec<&libsvld::motec::MessageMetadata> = data.metadata.values().collect();
|
|
||||||
msgs.sort_by_key(|m| m.id);
|
|
||||||
msgs.into_iter()
|
|
||||||
.map(|m| {
|
|
||||||
let name = dbc
|
|
||||||
.messages
|
|
||||||
.iter()
|
|
||||||
.find(|x| x.id.raw() == m.id)
|
|
||||||
.map(|x| x.name.as_str())
|
|
||||||
.unwrap_or("?");
|
|
||||||
let label = format!("0x{:X} {}", m.id, name);
|
|
||||||
let signals = m
|
|
||||||
.signals
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, s)| SigNode {
|
|
||||||
id: (1u64 << 63) | ((m.id as u64) << 16) | (i as u64),
|
|
||||||
name: s.name.clone(),
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
MsgNode {
|
|
||||||
id: m.id as u64,
|
|
||||||
label,
|
|
||||||
signals,
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn nearest_y(points: &[PlotPoint], x: f64) -> Option<f64> {
|
|
||||||
if points.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let i = points.partition_point(|p| p.x < x);
|
|
||||||
let cand = [
|
|
||||||
i.checked_sub(1).map(|j| &points[j]),
|
|
||||||
points.get(i),
|
|
||||||
];
|
|
||||||
cand.into_iter()
|
|
||||||
.flatten()
|
|
||||||
.min_by(|a, b| (a.x - x).abs().total_cmp(&(b.x - x).abs()))
|
|
||||||
.map(|p| p.y)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Min/max bucketing: for each horizontal pixel column in [x_min, x_max], emit
|
|
||||||
// the min-y and max-y point so spikes survive decimation. Pushed in x order
|
|
||||||
// (min/max swapped if needed) to keep the line monotonically increasing in x.
|
|
||||||
fn decimate(
|
|
||||||
points: &[PlotPoint],
|
|
||||||
x_min: f64,
|
|
||||||
x_max: f64,
|
|
||||||
buckets: usize,
|
|
||||||
out: &mut Vec<PlotPoint>,
|
|
||||||
) {
|
|
||||||
out.clear();
|
|
||||||
if points.is_empty() || buckets == 0 || !(x_max > x_min) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let lo = points.partition_point(|p| p.x < x_min).saturating_sub(1);
|
|
||||||
let hi = (points.partition_point(|p| p.x <= x_max) + 1).min(points.len());
|
|
||||||
let visible = &points[lo..hi];
|
|
||||||
|
|
||||||
if visible.len() <= buckets * 2 {
|
|
||||||
out.extend_from_slice(visible);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let bucket_w = (x_max - x_min) / buckets as f64;
|
|
||||||
let mut i = 0;
|
|
||||||
for b in 0..buckets {
|
|
||||||
let b_end = x_min + (b as f64 + 1.0) * bucket_w;
|
|
||||||
let start = i;
|
|
||||||
while i < visible.len() && visible[i].x < b_end {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
if start == i {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let slice = &visible[start..i];
|
|
||||||
let mut min_p = slice[0];
|
|
||||||
let mut max_p = slice[0];
|
|
||||||
for p in &slice[1..] {
|
|
||||||
if p.y < min_p.y { min_p = *p; }
|
|
||||||
if p.y > max_p.y { max_p = *p; }
|
|
||||||
}
|
|
||||||
if min_p.x <= max_p.x {
|
|
||||||
out.push(min_p);
|
|
||||||
out.push(max_p);
|
|
||||||
} else {
|
|
||||||
out.push(max_p);
|
|
||||||
out.push(min_p);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl eframe::App for MyEguiApp {
|
impl eframe::App for MyEguiApp {
|
||||||
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||||
egui::Panel::left(egui::Id::new("Signals"))
|
let toggles = show_side_panel(ui, &mut self.filter, &self.tree);
|
||||||
.resizable(true)
|
self.apply_toggles(toggles);
|
||||||
.show_inside(ui, |ui| {
|
show_plot_panel(ui, &self.plotted, &mut self.cursor_x);
|
||||||
ui.add(
|
}
|
||||||
egui::TextEdit::singleline(&mut self.filter)
|
|
||||||
.hint_text("Filter messages/signals")
|
|
||||||
.desired_width(f32::INFINITY),
|
|
||||||
);
|
|
||||||
let needle = self.filter.trim().to_lowercase();
|
|
||||||
let mut toggles: Vec<u64> = Vec::new();
|
|
||||||
ScrollArea::both()
|
|
||||||
.scroll([
|
|
||||||
true,
|
|
||||||
true
|
|
||||||
])
|
|
||||||
.scroll_bar_visibility(
|
|
||||||
egui::scroll_area::ScrollBarVisibility::VisibleWhenNeeded
|
|
||||||
)
|
|
||||||
.show(ui, |ui| {
|
|
||||||
let (_resp, actions) = TreeView::<u64>::new(
|
|
||||||
ui.make_persistent_id("SignalsTree"),
|
|
||||||
)
|
|
||||||
.show(ui, |builder| {
|
|
||||||
for msg in &self.tree {
|
|
||||||
let msg_hit = needle.is_empty()
|
|
||||||
|| msg.label.to_lowercase().contains(&needle);
|
|
||||||
let matching_sigs: Vec<&SigNode> = if msg_hit {
|
|
||||||
msg.signals.iter().collect()
|
|
||||||
} else {
|
|
||||||
msg.signals
|
|
||||||
.iter()
|
|
||||||
.filter(|s| s.name.to_lowercase().contains(&needle))
|
|
||||||
.collect()
|
|
||||||
};
|
|
||||||
if !msg_hit && matching_sigs.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
builder.dir(msg.id, msg.label.as_str());
|
|
||||||
for sig in matching_sigs {
|
|
||||||
builder.leaf(sig.id, sig.name.as_str());
|
|
||||||
}
|
|
||||||
builder.close_dir();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
for action in actions {
|
|
||||||
if let Action::SetSelected(ids) = action {
|
|
||||||
for id in ids {
|
|
||||||
if id & (1u64 << 63) != 0 {
|
|
||||||
toggles.push(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
for id in toggles {
|
|
||||||
if let Some(pos) = self.plotted.iter().position(|p| p.id == id) {
|
|
||||||
self.plotted.remove(pos);
|
|
||||||
} else {
|
|
||||||
let msg_id = ((id >> 16) & 0xFFFF_FFFF) as u32;
|
|
||||||
let sig_idx = (id & 0xFFFF) as usize;
|
|
||||||
if let Some(sig) = compute_signal(&self.data, msg_id, sig_idx, id) {
|
|
||||||
self.plotted.push(sig);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
egui::CentralPanel::default().show_inside(ui, |ui| {
|
|
||||||
if self.plotted.is_empty() {
|
|
||||||
ui.centered_and_justified(|ui| {
|
|
||||||
ui.label("Click a signal in the tree to plot it.");
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group signals by unit, preserving the order each unit first appeared.
|
|
||||||
let mut groups: Vec<(String, Vec<usize>)> = Vec::new();
|
|
||||||
for (i, sig) in self.plotted.iter().enumerate() {
|
|
||||||
if let Some(g) = groups.iter_mut().find(|(u, _)| u == &sig.unit) {
|
|
||||||
g.1.push(i);
|
|
||||||
} else {
|
|
||||||
groups.push((sig.unit.clone(), vec![i]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let full_min = self
|
|
||||||
.plotted
|
|
||||||
.iter()
|
|
||||||
.filter_map(|s| s.points.first().map(|p| p.x))
|
|
||||||
.fold(f64::INFINITY, f64::min);
|
|
||||||
let full_max = self
|
|
||||||
.plotted
|
|
||||||
.iter()
|
|
||||||
.filter_map(|s| s.points.last().map(|p| p.x))
|
|
||||||
.fold(f64::NEG_INFINITY, f64::max);
|
|
||||||
|
|
||||||
let link_group = ui.id().with("plot-link");
|
|
||||||
let n = groups.len();
|
|
||||||
// ui.spacing_mut().item_spacing.y = 0.0;
|
|
||||||
let h = (ui.available_height() / n as f32).max(80.0);
|
|
||||||
|
|
||||||
let cursor_x = self.cursor_x;
|
|
||||||
let mut new_cursor_x: Option<f64> = None;
|
|
||||||
|
|
||||||
for (gi, (unit, idxs)) in groups.iter().enumerate() {
|
|
||||||
let is_first = gi == 0;
|
|
||||||
let is_last = gi + 1 == n;
|
|
||||||
|
|
||||||
let mut x_axes: Vec<AxisHints> = Vec::new();
|
|
||||||
if is_first {
|
|
||||||
x_axes.push(AxisHints::new_x().placement(VPlacement::Top));
|
|
||||||
}
|
|
||||||
if is_last {
|
|
||||||
x_axes.push(AxisHints::new_x().placement(VPlacement::Bottom));
|
|
||||||
}
|
|
||||||
|
|
||||||
let title = if unit.is_empty() { "(no unit)" } else { unit.as_str() };
|
|
||||||
|
|
||||||
let plot = Plot::new(("plot", title))
|
|
||||||
.height(h)
|
|
||||||
.include_x(full_min)
|
|
||||||
.include_x(full_max)
|
|
||||||
.allow_zoom(Vec2b { x: true, y: true })
|
|
||||||
.link_axis(link_group, Vec2b { x: true, y: false })
|
|
||||||
.link_cursor(link_group, Vec2b { x: true, y: false })
|
|
||||||
.legend(Legend::default())
|
|
||||||
.custom_x_axes(x_axes)
|
|
||||||
.custom_y_axes(vec![AxisHints::new_y().min_thickness(48.0)]);
|
|
||||||
|
|
||||||
let plotted = &self.plotted;
|
|
||||||
let plot_resp = plot.show(ui, |pui| {
|
|
||||||
let bounds = pui.plot_bounds();
|
|
||||||
let x_min = bounds.min()[0];
|
|
||||||
let x_max = bounds.max()[0];
|
|
||||||
let px_w = pui.transform().frame().width() as usize;
|
|
||||||
let mut buf = Vec::new();
|
|
||||||
for &i in idxs {
|
|
||||||
let sig = &plotted[i];
|
|
||||||
decimate(&sig.points, x_min, x_max, px_w.max(1), &mut buf);
|
|
||||||
let ppoints = PlotPoints::Owned(std::mem::take(&mut buf));
|
|
||||||
let unit_s = if sig.unit.is_empty() {
|
|
||||||
String::new()
|
|
||||||
} else {
|
|
||||||
format!(" [{}]", sig.unit)
|
|
||||||
};
|
|
||||||
let name = match cursor_x.and_then(|x| nearest_y(&sig.points, x)) {
|
|
||||||
Some(v) => format!("{}: {:.4}{}", sig.label, v, unit_s),
|
|
||||||
None => format!("{}{}", sig.label, unit_s),
|
|
||||||
};
|
|
||||||
pui.line(Line::new(name, ppoints));
|
|
||||||
}
|
|
||||||
|
|
||||||
pui.pointer_coordinate().map(|c| c.x)
|
|
||||||
});
|
|
||||||
if let Some(x) = plot_resp.inner {
|
|
||||||
new_cursor_x = Some(x);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.cursor_x = new_cursor_x;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
use eframe::egui;
|
||||||
|
use egui::Vec2b;
|
||||||
|
use egui_plot::{AxisHints, Legend, Line, Plot, PlotPoints, VPlacement};
|
||||||
|
|
||||||
|
use crate::signal::{decimate, nearest_y, PlottedSignal};
|
||||||
|
|
||||||
|
const MIN_PLOT_HEIGHT: f32 = 80.0;
|
||||||
|
const Y_AXIS_MIN_THICKNESS: f32 = 48.0;
|
||||||
|
|
||||||
|
struct UnitGroup<'a> {
|
||||||
|
unit: &'a str,
|
||||||
|
signals: Vec<&'a PlottedSignal>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Group signals by unit, preserving the order each unit first appeared.
|
||||||
|
fn group_by_unit(signals: &[PlottedSignal]) -> Vec<UnitGroup<'_>> {
|
||||||
|
let mut groups: Vec<UnitGroup<'_>> = Vec::new();
|
||||||
|
for sig in signals {
|
||||||
|
if let Some(g) = groups.iter_mut().find(|g| g.unit == sig.unit) {
|
||||||
|
g.signals.push(sig);
|
||||||
|
} else {
|
||||||
|
groups.push(UnitGroup {
|
||||||
|
unit: &sig.unit,
|
||||||
|
signals: vec![sig],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
groups
|
||||||
|
}
|
||||||
|
|
||||||
|
fn full_x_range(signals: &[PlottedSignal]) -> (f64, f64) {
|
||||||
|
let min = signals
|
||||||
|
.iter()
|
||||||
|
.filter_map(|s| s.points.first().map(|p| p.x))
|
||||||
|
.fold(f64::INFINITY, f64::min);
|
||||||
|
let max = signals
|
||||||
|
.iter()
|
||||||
|
.filter_map(|s| s.points.last().map(|p| p.x))
|
||||||
|
.fold(f64::NEG_INFINITY, f64::max);
|
||||||
|
(min, max)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line_label(sig: &PlottedSignal, cursor_x: Option<f64>) -> String {
|
||||||
|
let unit_suffix = if sig.unit.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!(" [{}]", sig.unit)
|
||||||
|
};
|
||||||
|
match cursor_x.and_then(|x| nearest_y(&sig.points, x)) {
|
||||||
|
Some(v) => format!("{}: {:.4}{}", sig.label, v, unit_suffix),
|
||||||
|
None => format!("{}{}", sig.label, unit_suffix),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_plot_panel(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
plotted: &[PlottedSignal],
|
||||||
|
cursor_x: &mut Option<f64>,
|
||||||
|
) {
|
||||||
|
egui::CentralPanel::default().show_inside(ui, |ui| {
|
||||||
|
if plotted.is_empty() {
|
||||||
|
ui.centered_and_justified(|ui| {
|
||||||
|
ui.label("Click a signal in the tree to plot it.");
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let groups = group_by_unit(plotted);
|
||||||
|
let (full_min, full_max) = full_x_range(plotted);
|
||||||
|
|
||||||
|
let link_group = ui.id().with("plot-link");
|
||||||
|
let plot_count = groups.len();
|
||||||
|
let plot_height =
|
||||||
|
(ui.available_height() / plot_count as f32).max(MIN_PLOT_HEIGHT);
|
||||||
|
|
||||||
|
let prev_cursor_x = *cursor_x;
|
||||||
|
let mut new_cursor_x: Option<f64> = None;
|
||||||
|
|
||||||
|
for (group_index, group) in groups.iter().enumerate() {
|
||||||
|
let is_first = group_index == 0;
|
||||||
|
let is_last = group_index + 1 == plot_count;
|
||||||
|
|
||||||
|
let mut x_axes: Vec<AxisHints> = Vec::new();
|
||||||
|
if is_first {
|
||||||
|
x_axes.push(AxisHints::new_x().placement(VPlacement::Top));
|
||||||
|
}
|
||||||
|
if is_last {
|
||||||
|
x_axes.push(AxisHints::new_x().placement(VPlacement::Bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = if group.unit.is_empty() {
|
||||||
|
"(no unit)"
|
||||||
|
} else {
|
||||||
|
group.unit
|
||||||
|
};
|
||||||
|
|
||||||
|
let plot = Plot::new(("plot", title))
|
||||||
|
.height(plot_height)
|
||||||
|
.include_x(full_min)
|
||||||
|
.include_x(full_max)
|
||||||
|
.allow_zoom(Vec2b { x: true, y: true })
|
||||||
|
.link_axis(link_group, Vec2b { x: true, y: false })
|
||||||
|
.link_cursor(link_group, Vec2b { x: true, y: false })
|
||||||
|
.legend(Legend::default())
|
||||||
|
.custom_x_axes(x_axes)
|
||||||
|
.custom_y_axes(vec![
|
||||||
|
AxisHints::new_y().min_thickness(Y_AXIS_MIN_THICKNESS),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let response = plot.show(ui, |plot_ui| {
|
||||||
|
let bounds = plot_ui.plot_bounds();
|
||||||
|
let x_min = bounds.min()[0];
|
||||||
|
let x_max = bounds.max()[0];
|
||||||
|
let pixel_width = plot_ui.transform().frame().width() as usize;
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
for sig in &group.signals {
|
||||||
|
decimate(&sig.points, x_min, x_max, pixel_width.max(1), &mut buf);
|
||||||
|
let plot_points = PlotPoints::Owned(std::mem::take(&mut buf));
|
||||||
|
let name = line_label(sig, prev_cursor_x);
|
||||||
|
plot_ui.line(Line::new(name, plot_points));
|
||||||
|
}
|
||||||
|
plot_ui.pointer_coordinate().map(|c| c.x)
|
||||||
|
});
|
||||||
|
if let Some(x) = response.inner {
|
||||||
|
new_cursor_x = Some(x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*cursor_x = new_cursor_x;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
use egui_plot::PlotPoint;
|
||||||
|
use libsvld::DecodeResult;
|
||||||
|
|
||||||
|
pub struct PlottedSignal {
|
||||||
|
pub id: u64,
|
||||||
|
pub label: String,
|
||||||
|
pub unit: String,
|
||||||
|
pub points: Vec<PlotPoint>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compute_signal(
|
||||||
|
data: &DecodeResult,
|
||||||
|
msg_id: u32,
|
||||||
|
sig_idx: usize,
|
||||||
|
id: u64,
|
||||||
|
) -> Option<PlottedSignal> {
|
||||||
|
let meta = data.metadata.get(&msg_id)?;
|
||||||
|
let sig = meta.signals.get(sig_idx)?;
|
||||||
|
let frames = data.data.get(&msg_id)?;
|
||||||
|
let mut points = Vec::with_capacity(frames.len());
|
||||||
|
for frame in frames {
|
||||||
|
let bytes = u64::from_le_bytes(frame.data);
|
||||||
|
points.push(PlotPoint {
|
||||||
|
x: frame.timestamp,
|
||||||
|
y: libsvld::motec::extract_signal(bytes, &sig.signal),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Some(PlottedSignal {
|
||||||
|
id,
|
||||||
|
label: sig.name.clone(),
|
||||||
|
unit: sig.unit.clone(),
|
||||||
|
points,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nearest_y(points: &[PlotPoint], x: f64) -> Option<f64> {
|
||||||
|
if points.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let i = points.partition_point(|p| p.x < x);
|
||||||
|
let candidates = [
|
||||||
|
i.checked_sub(1).map(|j| &points[j]),
|
||||||
|
points.get(i),
|
||||||
|
];
|
||||||
|
candidates
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.min_by(|a, b| (a.x - x).abs().total_cmp(&(b.x - x).abs()))
|
||||||
|
.map(|p| p.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Min/max bucketing: for each horizontal pixel column in [x_min, x_max], emit
|
||||||
|
// the min-y and max-y point so spikes survive decimation. Pushed in x order
|
||||||
|
// (min/max swapped if needed) to keep the line monotonically increasing in x.
|
||||||
|
pub fn decimate(
|
||||||
|
points: &[PlotPoint],
|
||||||
|
x_min: f64,
|
||||||
|
x_max: f64,
|
||||||
|
buckets: usize,
|
||||||
|
out: &mut Vec<PlotPoint>,
|
||||||
|
) {
|
||||||
|
out.clear();
|
||||||
|
if points.is_empty() || buckets == 0 || !(x_max > x_min) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let start = points.partition_point(|p| p.x < x_min).saturating_sub(1);
|
||||||
|
let end = (points.partition_point(|p| p.x <= x_max) + 1).min(points.len());
|
||||||
|
let visible = &points[start..end];
|
||||||
|
|
||||||
|
if visible.len() <= buckets * 2 {
|
||||||
|
out.extend_from_slice(visible);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let bucket_w = (x_max - x_min) / buckets as f64;
|
||||||
|
let mut cursor = 0;
|
||||||
|
for bucket in 0..buckets {
|
||||||
|
let bucket_end_x = x_min + (bucket as f64 + 1.0) * bucket_w;
|
||||||
|
let bucket_start = cursor;
|
||||||
|
while cursor < visible.len() && visible[cursor].x < bucket_end_x {
|
||||||
|
cursor += 1;
|
||||||
|
}
|
||||||
|
if bucket_start == cursor {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let slice = &visible[bucket_start..cursor];
|
||||||
|
let mut min_p = slice[0];
|
||||||
|
let mut max_p = slice[0];
|
||||||
|
for p in &slice[1..] {
|
||||||
|
if p.y < min_p.y { min_p = *p; }
|
||||||
|
if p.y > max_p.y { max_p = *p; }
|
||||||
|
}
|
||||||
|
if min_p.x <= max_p.x {
|
||||||
|
out.push(min_p);
|
||||||
|
out.push(max_p);
|
||||||
|
} else {
|
||||||
|
out.push(max_p);
|
||||||
|
out.push(min_p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
use can_dbc::Dbc;
|
||||||
|
use eframe::egui;
|
||||||
|
use egui::ScrollArea;
|
||||||
|
use egui_ltreeview::{Action, TreeView};
|
||||||
|
use libsvld::DecodeResult;
|
||||||
|
|
||||||
|
pub struct SigNode {
|
||||||
|
pub id: u64,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MsgNode {
|
||||||
|
pub id: u64,
|
||||||
|
pub label: String,
|
||||||
|
pub signals: Vec<SigNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const SIGNAL_FLAG: u64 = 1 << 63;
|
||||||
|
|
||||||
|
pub fn signal_node_id(msg_id: u32, sig_idx: usize) -> u64 {
|
||||||
|
SIGNAL_FLAG | ((msg_id as u64) << 16) | (sig_idx as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode_signal_node_id(id: u64) -> Option<(u32, usize)> {
|
||||||
|
if id & SIGNAL_FLAG == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let msg_id = ((id >> 16) & 0xFFFF_FFFF) as u32;
|
||||||
|
let sig_idx = (id & 0xFFFF) as usize;
|
||||||
|
Some((msg_id, sig_idx))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_tree(dbc: &Dbc, data: &DecodeResult) -> Vec<MsgNode> {
|
||||||
|
let mut msgs: Vec<&libsvld::motec::MessageMetadata> = data.metadata.values().collect();
|
||||||
|
msgs.sort_by_key(|m| m.id);
|
||||||
|
msgs.into_iter()
|
||||||
|
.map(|msg| {
|
||||||
|
let name = dbc
|
||||||
|
.messages
|
||||||
|
.iter()
|
||||||
|
.find(|x| x.id.raw() == msg.id)
|
||||||
|
.map(|x| x.name.as_str())
|
||||||
|
.unwrap_or("?");
|
||||||
|
let label = format!("0x{:X} {}", msg.id, name);
|
||||||
|
let signals = msg
|
||||||
|
.signals
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(sig_idx, signal)| SigNode {
|
||||||
|
id: signal_node_id(msg.id, sig_idx),
|
||||||
|
name: signal.name.clone(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
MsgNode {
|
||||||
|
id: msg.id as u64,
|
||||||
|
label,
|
||||||
|
signals,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render the side panel and return any signal IDs the user clicked to toggle.
|
||||||
|
pub fn show_side_panel(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
filter: &mut String,
|
||||||
|
tree: &[MsgNode],
|
||||||
|
) -> Vec<u64> {
|
||||||
|
let mut toggles = Vec::new();
|
||||||
|
egui::Panel::left(egui::Id::new("Signals"))
|
||||||
|
.resizable(true)
|
||||||
|
.show_inside(ui, |ui| {
|
||||||
|
ui.add(
|
||||||
|
egui::TextEdit::singleline(filter)
|
||||||
|
.hint_text("Filter messages/signals")
|
||||||
|
.desired_width(f32::INFINITY),
|
||||||
|
);
|
||||||
|
let needle = filter.trim().to_lowercase();
|
||||||
|
ScrollArea::both()
|
||||||
|
.scroll([true, true])
|
||||||
|
.scroll_bar_visibility(
|
||||||
|
egui::scroll_area::ScrollBarVisibility::VisibleWhenNeeded,
|
||||||
|
)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
let (_resp, actions) = TreeView::<u64>::new(
|
||||||
|
ui.make_persistent_id("SignalsTree"),
|
||||||
|
)
|
||||||
|
.show(ui, |builder| {
|
||||||
|
for msg in tree {
|
||||||
|
let msg_hit = needle.is_empty()
|
||||||
|
|| msg.label.to_lowercase().contains(&needle);
|
||||||
|
let matching: Vec<&SigNode> = if msg_hit {
|
||||||
|
msg.signals.iter().collect()
|
||||||
|
} else {
|
||||||
|
msg.signals
|
||||||
|
.iter()
|
||||||
|
.filter(|s| s.name.to_lowercase().contains(&needle))
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
if !msg_hit && matching.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
builder.dir(msg.id, msg.label.as_str());
|
||||||
|
for sig in matching {
|
||||||
|
builder.leaf(sig.id, sig.name.as_str());
|
||||||
|
}
|
||||||
|
builder.close_dir();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for action in actions {
|
||||||
|
if let Action::SetSelected(ids) = action {
|
||||||
|
for id in ids {
|
||||||
|
if id & SIGNAL_FLAG != 0 {
|
||||||
|
toggles.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
toggles
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user