From d82050b0c10c4e385697e50932b7999f8d750c83 Mon Sep 17 00:00:00 2001 From: Cole Gentry Date: Thu, 16 Jul 2026 10:58:06 -0400 Subject: [PATCH 01/18] chore: remove stale editor backup files from src/ui --- src/ui/export.rs.bak | 1580 ------------------------ src/ui/histogram.rs.backup | 2399 ------------------------------------ 2 files changed, 3979 deletions(-) delete mode 100644 src/ui/export.rs.bak delete mode 100644 src/ui/histogram.rs.backup diff --git a/src/ui/export.rs.bak b/src/ui/export.rs.bak deleted file mode 100644 index d917299..0000000 --- a/src/ui/export.rs.bak +++ /dev/null @@ -1,1580 +0,0 @@ -//! Chart export functionality (PNG, PDF). - -use printpdf::path::{PaintMode, WindingOrder}; -use printpdf::*; -use rust_i18n::t; -use std::fs::File; -use std::io::BufWriter; - -// Use fully qualified path to disambiguate from printpdf's image module -use ::image::{Rgba, RgbaImage}; - -use crate::analytics; -use crate::app::UltraLogApp; -use crate::normalize::normalize_channel_name_with_custom; -use crate::state::HistogramMode; - -impl UltraLogApp { - /// Export the current chart view as PNG - pub fn export_chart_png(&mut self) { - // Show save dialog - let Some(path) = rfd::FileDialog::new() - .add_filter("PNG Image", &["png"]) - .set_file_name("ultralog_chart.png") - .save_file() - else { - return; - }; - - // Create a simple chart representation as image - match self.render_chart_to_png(&path) { - Ok(_) => { - analytics::track_export("png"); - self.show_toast_success(&t!("toast.export_png_success")); - } - Err(e) => self.show_toast_error(&t!("toast.export_failed", error = e.to_string())), - } - } - - /// Export the current chart view as PDF - pub fn export_chart_pdf(&mut self) { - // Show save dialog - let Some(path) = rfd::FileDialog::new() - .add_filter("PDF Document", &["pdf"]) - .set_file_name("ultralog_chart.pdf") - .save_file() - else { - return; - }; - - match self.render_chart_to_pdf(&path) { - Ok(_) => { - analytics::track_export("pdf"); - self.show_toast_success(&t!("toast.export_pdf_success")); - } - Err(e) => self.show_toast_error(&t!("toast.export_failed", error = e.to_string())), - } - } - - /// Render chart data to PNG file - fn render_chart_to_png( - &self, - path: &std::path::Path, - ) -> Result<(), Box> { - let width = 1920u32; - let height = 1080u32; - - // Create image buffer - let mut imgbuf = RgbaImage::new(width, height); - - // Fill with dark background - for pixel in imgbuf.pixels_mut() { - *pixel = Rgba([30, 30, 30, 255]); - } - - // Draw chart area background - let chart_left = 80u32; - let chart_right = width - 40; - let chart_top = 60u32; - let chart_bottom = height - 80; - - for y in chart_top..chart_bottom { - for x in chart_left..chart_right { - imgbuf.put_pixel(x, y, Rgba([40, 40, 40, 255])); - } - } - - // Get time range - let Some((min_time, max_time)) = self.time_range else { - return Err("No time range available".into()); - }; - - let time_span = max_time - min_time; - if time_span <= 0.0 { - return Err("Invalid time range".into()); - } - - let chart_width = (chart_right - chart_left) as f64; - let chart_height = (chart_bottom - chart_top) as f64; - - // Draw each channel - for selected in self.get_selected_channels() { - let color = self.get_channel_color(selected.color_index); - let pixel_color = Rgba([color[0], color[1], color[2], 255]); - - // Get channel data - if selected.file_index >= self.files.len() { - continue; - } - let file = &self.files[selected.file_index]; - let times = file.log.get_times_as_f64(); - let data = file.log.get_channel_data(selected.channel_index); - - if data.is_empty() { - continue; - } - - // Find min/max for normalization - let mut data_min = f64::MAX; - let mut data_max = f64::MIN; - for &val in &data { - data_min = data_min.min(val); - data_max = data_max.max(val); - } - - let data_range = if (data_max - data_min).abs() < 0.0001 { - 1.0 - } else { - data_max - data_min - }; - - // Draw data points as lines - let mut prev_x: Option = None; - let mut prev_y: Option = None; - - for (&time, &value) in times.iter().zip(data.iter()) { - // Skip points outside time range - if time < min_time || time > max_time { - continue; - } - - let x_ratio = (time - min_time) / time_span; - let y_ratio = (value - data_min) / data_range; - - let x = chart_left + (x_ratio * chart_width) as u32; - let y = chart_bottom - (y_ratio * chart_height) as u32; - - // Draw line from previous point - if let (Some(px), Some(py)) = (prev_x, prev_y) { - draw_line(&mut imgbuf, px, py, x, y, pixel_color); - } - - prev_x = Some(x); - prev_y = Some(y); - } - } - - // Save the image - imgbuf.save(path)?; - - Ok(()) - } - - /// Render chart data to PDF file - fn render_chart_to_pdf( - &self, - path: &std::path::Path, - ) -> Result<(), Box> { - // Create PDF document (A4 landscape) - let (doc, page1, layer1) = - PdfDocument::new("UltraLog Chart Export", Mm(297.0), Mm(210.0), "Chart"); - - let current_layer = doc.get_page(page1).get_layer(layer1); - - // Get time range - let Some((min_time, max_time)) = self.time_range else { - return Err("No time range available".into()); - }; - - let time_span = max_time - min_time; - if time_span <= 0.0 { - return Err("Invalid time range".into()); - } - - // Chart dimensions in mm (A4 landscape with margins) - let margin: f64 = 20.0; - let chart_left: f64 = margin; - let chart_right: f64 = 297.0 - margin; - let chart_bottom: f64 = margin + 20.0; // Leave room for time labels - let chart_top: f64 = 210.0 - margin - 30.0; // Leave room for title - - let chart_width: f64 = chart_right - chart_left; - let chart_height: f64 = chart_top - chart_bottom; - - // Draw title - let font = doc.add_builtin_font(BuiltinFont::HelveticaBold)?; - current_layer.use_text( - "UltraLog Chart Export", - 16.0, - Mm(margin as f32), - Mm(200.0), - &font, - ); - - // Draw subtitle with file info - let font_regular = doc.add_builtin_font(BuiltinFont::Helvetica)?; - if let Some(file) = self.files.first() { - let subtitle = format!( - "{} | {} channels selected | Time: {:.1}s - {:.1}s", - file.name, - self.get_selected_channels().len(), - min_time, - max_time - ); - current_layer.use_text(&subtitle, 10.0, Mm(margin as f32), Mm(192.0), &font_regular); - } - - // Draw chart border - let border_color = Color::Rgb(Rgb::new(0.3, 0.3, 0.3, None)); - current_layer.set_outline_color(border_color); - current_layer.set_outline_thickness(0.5); - - let border = Line { - points: vec![ - ( - Point::new(Mm(chart_left as f32), Mm(chart_bottom as f32)), - false, - ), - ( - Point::new(Mm(chart_right as f32), Mm(chart_bottom as f32)), - false, - ), - ( - Point::new(Mm(chart_right as f32), Mm(chart_top as f32)), - false, - ), - ( - Point::new(Mm(chart_left as f32), Mm(chart_top as f32)), - false, - ), - ], - is_closed: true, - }; - current_layer.add_line(border); - - // Draw each channel - for selected in self.get_selected_channels() { - let color_rgb = self.get_channel_color(selected.color_index); - let line_color = Color::Rgb(Rgb::new( - color_rgb[0] as f32 / 255.0, - color_rgb[1] as f32 / 255.0, - color_rgb[2] as f32 / 255.0, - None, - )); - - current_layer.set_outline_color(line_color); - current_layer.set_outline_thickness(0.75); - - // Get channel data - if selected.file_index >= self.files.len() { - continue; - } - let file = &self.files[selected.file_index]; - let times = file.log.get_times_as_f64(); - let data = file.log.get_channel_data(selected.channel_index); - - if data.is_empty() { - continue; - } - - // Find min/max for normalization - let mut data_min = f64::MAX; - let mut data_max = f64::MIN; - for &val in &data { - data_min = data_min.min(val); - data_max = data_max.max(val); - } - - let data_range = if (data_max - data_min).abs() < 0.0001 { - 1.0 - } else { - data_max - data_min - }; - - // Build line points (downsample for PDF) - let mut points: Vec<(Point, bool)> = Vec::new(); - let step = (times.len() / 500).max(1); // Max ~500 points per channel - - for (i, (&time, &value)) in times.iter().zip(data.iter()).enumerate() { - if i % step != 0 { - continue; - } - - if time < min_time || time > max_time { - continue; - } - - let x_ratio = (time - min_time) / time_span; - let y_ratio = (value - data_min) / data_range; - - let x = chart_left + x_ratio * chart_width; - let y = chart_bottom + y_ratio * chart_height; - - points.push((Point::new(Mm(x as f32), Mm(y as f32)), false)); - } - - if points.len() >= 2 { - let line = Line { - points, - is_closed: false, - }; - current_layer.add_line(line); - } - } - - // Draw legend - let legend_y = chart_bottom - 12.0; - let mut legend_x = chart_left; - - for selected in self.get_selected_channels() { - let color_rgb = self.get_channel_color(selected.color_index); - let text_color = Color::Rgb(Rgb::new( - color_rgb[0] as f32 / 255.0, - color_rgb[1] as f32 / 255.0, - color_rgb[2] as f32 / 255.0, - None, - )); - - // Get display name (normalized or original based on setting) - let channel_name = selected.channel.name(); - let display_name = if self.field_normalization { - normalize_channel_name_with_custom(&channel_name, Some(&self.custom_normalizations)) - } else { - channel_name - }; - - current_layer.set_fill_color(text_color); - current_layer.use_text( - &display_name, - 8.0, - Mm(legend_x as f32), - Mm(legend_y as f32), - &font_regular, - ); - - legend_x += 40.0; - if legend_x > chart_right - 40.0 { - break; // Don't overflow - } - } - - // Save PDF - let file = File::create(path)?; - let mut writer = BufWriter::new(file); - doc.save(&mut writer)?; - - Ok(()) - } - - /// Export the current histogram view as PNG - pub fn export_histogram_png(&mut self) { - // Show save dialog - let Some(path) = rfd::FileDialog::new() - .add_filter("PNG Image", &["png"]) - .set_file_name("ultralog_histogram.png") - .save_file() - else { - return; - }; - - match self.render_histogram_to_png(&path) { - Ok(_) => { - analytics::track_export("histogram_png"); - self.show_toast_success(&t!("toast.histogram_exported_png")); - } - Err(e) => self.show_toast_error(&t!("toast.export_failed", error = e.to_string())), - } - } - - /// Export the current scatter plot view as PNG - pub fn export_scatter_plot_png(&mut self) { - // Show save dialog - let Some(path) = rfd::FileDialog::new() - .add_filter("PNG Image", &["png"]) - .set_file_name("ultralog_scatter_plot.png") - .save_file() - else { - return; - }; - - match self.render_scatter_plot_to_png(&path) { - Ok(_) => { - analytics::track_export("scatter_plot_png"); - self.show_toast_success(&t!("toast.scatter_exported_png")); - } - Err(e) => self.show_toast_error(&t!("toast.export_failed", error = e.to_string())), - } - } - - /// Export the current scatter plot view as PDF - pub fn export_scatter_plot_pdf(&mut self) { - // Show save dialog - let Some(path) = rfd::FileDialog::new() - .add_filter("PDF Document", &["pdf"]) - .set_file_name("ultralog_scatter_plot.pdf") - .save_file() - else { - return; - }; - - match self.render_scatter_plot_to_pdf(&path) { - Ok(_) => { - analytics::track_export("scatter_plot_pdf"); - self.show_toast_success(&t!("toast.scatter_exported_pdf")); - } - Err(e) => self.show_toast_error(&t!("toast.export_failed", error = e.to_string())), - } - } - - /// Export the current histogram view as PDF - pub fn export_histogram_pdf(&mut self) { - // Show save dialog - let Some(path) = rfd::FileDialog::new() - .add_filter("PDF Document", &["pdf"]) - .set_file_name("ultralog_histogram.pdf") - .save_file() - else { - return; - }; - - match self.render_histogram_to_pdf(&path) { - Ok(_) => { - analytics::track_export("histogram_pdf"); - self.show_toast_success(&t!("toast.histogram_exported_pdf")); - } - Err(e) => self.show_toast_error(&t!("toast.export_failed", error = e.to_string())), - } - } - - /// Render histogram to PDF file - fn render_histogram_to_pdf( - &self, - path: &std::path::Path, - ) -> Result<(), Box> { - // Get tab and file data - let tab_idx = self.active_tab.ok_or("No active tab")?; - let config = &self.tabs[tab_idx].histogram_state.config; - let file_idx = self.tabs[tab_idx].file_index; - - if file_idx >= self.files.len() { - return Err("Invalid file index".into()); - } - - let file = &self.files[file_idx]; - let mode = config.mode; - let (grid_cols, grid_rows) = config.effective_grid_size(); - - let x_idx = config.x_channel.ok_or("X axis not selected")?; - let y_idx = config.y_channel.ok_or("Y axis not selected")?; - let z_idx = if mode == HistogramMode::AverageZ { - config - .z_channel - .ok_or("Z axis not selected for Average mode")? - } else { - 0 // unused - }; - - // Get channel data - let x_data = file.log.get_channel_data(x_idx); - let y_data = file.log.get_channel_data(y_idx); - let z_data = if mode == HistogramMode::AverageZ { - Some(file.log.get_channel_data(z_idx)) - } else { - None - }; - - if x_data.is_empty() || y_data.is_empty() { - return Err("No data available".into()); - } - - // Calculate data bounds - let x_min = x_data.iter().cloned().fold(f64::MAX, f64::min); - let x_max = x_data.iter().cloned().fold(f64::MIN, f64::max); - let y_min = y_data.iter().cloned().fold(f64::MAX, f64::min); - let y_max = y_data.iter().cloned().fold(f64::MIN, f64::max); - - let x_range = if (x_max - x_min).abs() < f64::EPSILON { - 1.0 - } else { - x_max - x_min - }; - let y_range = if (y_max - y_min).abs() < f64::EPSILON { - 1.0 - } else { - y_max - y_min - }; - - // Build histogram grid - let mut hit_counts = vec![vec![0u32; grid_size]; grid_size]; - let mut z_sums = vec![vec![0.0f64; grid_size]; grid_size]; - - for i in 0..x_data.len() { - let x_bin = (((x_data[i] - x_min) / x_range) * (grid_size - 1) as f64).round() as usize; - let y_bin = (((y_data[i] - y_min) / y_range) * (grid_size - 1) as f64).round() as usize; - let x_bin = x_bin.min(grid_size - 1); - let y_bin = y_bin.min(grid_size - 1); - - hit_counts[y_bin][x_bin] += 1; - if let Some(ref z) = z_data { - z_sums[y_bin][x_bin] += z[i]; - } - } - - // Calculate cell values and find min/max for color scaling - let mut cell_values = vec![vec![None::; grid_size]; grid_size]; - let mut min_value: f64 = f64::MAX; - let mut max_value: f64 = f64::MIN; - - for y_bin in 0..grid_size { - for x_bin in 0..grid_size { - let hits = hit_counts[y_bin][x_bin]; - if hits > 0 { - let value = match mode { - HistogramMode::HitCount => hits as f64, - HistogramMode::AverageZ => z_sums[y_bin][x_bin] / hits as f64, - }; - cell_values[y_bin][x_bin] = Some(value); - min_value = min_value.min(value); - max_value = max_value.max(value); - } - } - } - - let value_range = if (max_value - min_value).abs() < f64::EPSILON { - 1.0 - } else { - max_value - min_value - }; - - // Get channel names - let x_name = file.log.channels[x_idx].name(); - let y_name = file.log.channels[y_idx].name(); - let z_name = if mode == HistogramMode::AverageZ { - file.log.channels[z_idx].name() - } else { - "Hit Count".to_string() - }; - - // Create PDF document (A4 landscape) - let (doc, page1, layer1) = PdfDocument::new( - "UltraLog Histogram Export", - Mm(297.0), - Mm(210.0), - "Histogram", - ); - - let current_layer = doc.get_page(page1).get_layer(layer1); - - // Chart dimensions in mm (A4 landscape with margins) - let margin: f64 = 20.0; - let axis_margin: f64 = 25.0; - let chart_left: f64 = margin + axis_margin; - let chart_right: f64 = 250.0; // Leave room for legend - let chart_bottom: f64 = margin + axis_margin; - let chart_top: f64 = 210.0 - margin - 30.0; - - let chart_width: f64 = chart_right - chart_left; - let chart_height: f64 = chart_top - chart_bottom; - - let cell_width = chart_width / grid_size as f64; - let cell_height = chart_height / grid_size as f64; - - // Draw title - let font = doc.add_builtin_font(BuiltinFont::HelveticaBold)?; - current_layer.use_text( - "UltraLog Histogram Export", - 16.0, - Mm(margin as f32), - Mm(200.0), - &font, - ); - - // Draw subtitle - let font_regular = doc.add_builtin_font(BuiltinFont::Helvetica)?; - let subtitle = format!( - "{} | Grid: {}x{} | Mode: {}", - file.name, - grid_size, - grid_size, - if mode == HistogramMode::HitCount { - "Hit Count" - } else { - "Average Z" - } - ); - current_layer.use_text(&subtitle, 10.0, Mm(margin as f32), Mm(192.0), &font_regular); - - // Draw axis labels - let axis_subtitle = format!("X: {} | Y: {} | Z: {}", x_name, y_name, z_name); - current_layer.use_text( - &axis_subtitle, - 9.0, - Mm(margin as f32), - Mm(186.0), - &font_regular, - ); - - // Draw histogram cells - for y_bin in 0..grid_size { - for x_bin in 0..grid_size { - let cell_x = chart_left + x_bin as f64 * cell_width; - let cell_y = chart_bottom + y_bin as f64 * cell_height; - - if let Some(value) = cell_values[y_bin][x_bin] { - // Calculate color - let normalized = if mode == HistogramMode::HitCount && max_value > 1.0 { - (value.ln() / max_value.ln()).clamp(0.0, 1.0) - } else { - ((value - min_value) / value_range).clamp(0.0, 1.0) - }; - let color = Self::get_pdf_heat_color(normalized); - - current_layer.set_fill_color(color); - - // Draw filled rectangle - let rect = printpdf::Polygon { - rings: vec![vec![ - (Point::new(Mm(cell_x as f32), Mm(cell_y as f32)), false), - ( - Point::new(Mm((cell_x + cell_width) as f32), Mm(cell_y as f32)), - false, - ), - ( - Point::new( - Mm((cell_x + cell_width) as f32), - Mm((cell_y + cell_height) as f32), - ), - false, - ), - ( - Point::new(Mm(cell_x as f32), Mm((cell_y + cell_height) as f32)), - false, - ), - ]], - mode: PaintMode::Fill, - winding_order: WindingOrder::NonZero, - }; - current_layer.add_polygon(rect); - - // Draw cell value text (only for smaller grids) - if grid_size <= 32 { - let text = if mode == HistogramMode::HitCount { - format!("{}", hit_counts[y_bin][x_bin]) - } else { - format!("{:.1}", value) - }; - - // Calculate text color based on brightness - let brightness = normalized; - let text_color = if brightness > 0.5 { - Color::Rgb(Rgb::new(0.0, 0.0, 0.0, None)) // Black - } else { - Color::Rgb(Rgb::new(1.0, 1.0, 1.0, None)) // White - }; - - current_layer.set_fill_color(text_color); - let font_size = if grid_size <= 16 { 6.0 } else { 4.0 }; - current_layer.use_text( - &text, - font_size, - Mm((cell_x + cell_width / 2.0 - 2.0) as f32), - Mm((cell_y + cell_height / 2.0 - 1.0) as f32), - &font_regular, - ); - } - } - } - } - - // Draw grid lines - let grid_color = Color::Rgb(Rgb::new(0.4, 0.4, 0.4, None)); - current_layer.set_outline_color(grid_color); - current_layer.set_outline_thickness(0.25); - - for i in 0..=grid_size { - let x = chart_left + i as f64 * cell_width; - let y = chart_bottom + i as f64 * cell_height; - - // Vertical line - let vline = Line { - points: vec![ - (Point::new(Mm(x as f32), Mm(chart_bottom as f32)), false), - (Point::new(Mm(x as f32), Mm(chart_top as f32)), false), - ], - is_closed: false, - }; - current_layer.add_line(vline); - - // Horizontal line - let hline = Line { - points: vec![ - (Point::new(Mm(chart_left as f32), Mm(y as f32)), false), - (Point::new(Mm(chart_right as f32), Mm(y as f32)), false), - ], - is_closed: false, - }; - current_layer.add_line(hline); - } - - // Draw axis value labels - let label_color = Color::Rgb(Rgb::new(0.0, 0.0, 0.0, None)); - current_layer.set_fill_color(label_color); - - // Y axis labels - for i in 0..=4 { - let t = i as f64 / 4.0; - let value = y_min + t * y_range; - let y_pos = chart_bottom + t * chart_height; - current_layer.use_text( - format!("{:.0}", value), - 7.0, - Mm((chart_left - 12.0) as f32), - Mm((y_pos - 1.0) as f32), - &font_regular, - ); - } - - // X axis labels - for i in 0..=4 { - let t = i as f64 / 4.0; - let value = x_min + t * x_range; - let x_pos = chart_left + t * chart_width; - current_layer.use_text( - format!("{:.0}", value), - 7.0, - Mm((x_pos - 4.0) as f32), - Mm((chart_bottom - 8.0) as f32), - &font_regular, - ); - } - - // Draw legend (color scale) - let legend_left: f64 = 260.0; - let legend_width: f64 = 15.0; - let legend_bottom: f64 = chart_bottom; - let legend_height: f64 = chart_height; - - // Draw legend title - current_layer.set_fill_color(Color::Rgb(Rgb::new(0.0, 0.0, 0.0, None))); - let legend_title = if mode == HistogramMode::HitCount { - "Hits" - } else { - "Value" - }; - current_layer.use_text( - legend_title, - 9.0, - Mm(legend_left as f32), - Mm((legend_bottom + legend_height + 5.0) as f32), - &font, - ); - - // Draw color gradient bar - let gradient_steps = 30; - let step_height = legend_height / gradient_steps as f64; - - for i in 0..gradient_steps { - let t = i as f64 / gradient_steps as f64; - let color = Self::get_pdf_heat_color(t); - current_layer.set_fill_color(color); - - let y = legend_bottom + i as f64 * step_height; - let rect = printpdf::Polygon { - rings: vec![vec![ - (Point::new(Mm(legend_left as f32), Mm(y as f32)), false), - ( - Point::new(Mm((legend_left + legend_width) as f32), Mm(y as f32)), - false, - ), - ( - Point::new( - Mm((legend_left + legend_width) as f32), - Mm((y + step_height + 0.5) as f32), - ), - false, - ), - ( - Point::new(Mm(legend_left as f32), Mm((y + step_height + 0.5) as f32)), - false, - ), - ]], - mode: PaintMode::Fill, - winding_order: WindingOrder::NonZero, - }; - current_layer.add_polygon(rect); - } - - // Draw legend min/max labels - current_layer.set_fill_color(Color::Rgb(Rgb::new(0.0, 0.0, 0.0, None))); - let min_label = if mode == HistogramMode::HitCount { - "0".to_string() - } else { - format!("{:.1}", min_value) - }; - let max_label = if mode == HistogramMode::HitCount { - format!("{:.0}", max_value) - } else { - format!("{:.1}", max_value) - }; - - current_layer.use_text( - &min_label, - 7.0, - Mm((legend_left + legend_width + 3.0) as f32), - Mm(legend_bottom as f32), - &font_regular, - ); - current_layer.use_text( - &max_label, - 7.0, - Mm((legend_left + legend_width + 3.0) as f32), - Mm((legend_bottom + legend_height - 3.0) as f32), - &font_regular, - ); - - // Draw statistics - let stats_y = legend_bottom - 15.0; - current_layer.use_text( - format!("Total Points: {}", x_data.len()), - 8.0, - Mm(legend_left as f32), - Mm(stats_y as f32), - &font_regular, - ); - - // Save PDF - let file = File::create(path)?; - let mut writer = BufWriter::new(file); - doc.save(&mut writer)?; - - Ok(()) - } - - /// Get a PDF color from the heat map gradient based on normalized value (0-1) - fn get_pdf_heat_color(normalized: f64) -> Color { - const HEAT_COLORS: &[[u8; 3]] = &[ - [0, 0, 80], // Dark blue (0.0) - [0, 0, 180], // Blue - [0, 100, 255], // Light blue - [0, 200, 255], // Cyan - [0, 255, 200], // Cyan-green - [0, 255, 100], // Green - [100, 255, 0], // Yellow-green - [200, 255, 0], // Yellow - [255, 200, 0], // Orange - [255, 100, 0], // Red-orange - [255, 0, 0], // Red (1.0) - ]; - - let t = normalized.clamp(0.0, 1.0); - let scaled = t * (HEAT_COLORS.len() - 1) as f64; - let idx = scaled.floor() as usize; - let frac = scaled - idx as f64; - - if idx >= HEAT_COLORS.len() - 1 { - let c = HEAT_COLORS[HEAT_COLORS.len() - 1]; - return Color::Rgb(Rgb::new( - c[0] as f32 / 255.0, - c[1] as f32 / 255.0, - c[2] as f32 / 255.0, - None, - )); - } - - let c1 = HEAT_COLORS[idx]; - let c2 = HEAT_COLORS[idx + 1]; - - let r = (c1[0] as f64 + (c2[0] as f64 - c1[0] as f64) * frac) / 255.0; - let g = (c1[1] as f64 + (c2[1] as f64 - c1[1] as f64) * frac) / 255.0; - let b = (c1[2] as f64 + (c2[2] as f64 - c1[2] as f64) * frac) / 255.0; - - Color::Rgb(Rgb::new(r as f32, g as f32, b as f32, None)) - } - - /// Get a PNG color from the heat map gradient based on normalized value (0-1) - fn get_png_heat_color(normalized: f64) -> Rgba { - const HEAT_COLORS: &[[u8; 3]] = &[ - [0, 0, 80], // Dark blue (0.0) - [0, 0, 180], // Blue - [0, 100, 255], // Light blue - [0, 200, 255], // Cyan - [0, 255, 200], // Cyan-green - [0, 255, 100], // Green - [100, 255, 0], // Yellow-green - [200, 255, 0], // Yellow - [255, 200, 0], // Orange - [255, 100, 0], // Red-orange - [255, 0, 0], // Red (1.0) - ]; - - let t = normalized.clamp(0.0, 1.0); - let scaled = t * (HEAT_COLORS.len() - 1) as f64; - let idx = scaled.floor() as usize; - let frac = scaled - idx as f64; - - if idx >= HEAT_COLORS.len() - 1 { - let c = HEAT_COLORS[HEAT_COLORS.len() - 1]; - return Rgba([c[0], c[1], c[2], 255]); - } - - let c1 = HEAT_COLORS[idx]; - let c2 = HEAT_COLORS[idx + 1]; - - let r = (c1[0] as f64 + (c2[0] as f64 - c1[0] as f64) * frac) as u8; - let g = (c1[1] as f64 + (c2[1] as f64 - c1[1] as f64) * frac) as u8; - let b = (c1[2] as f64 + (c2[2] as f64 - c1[2] as f64) * frac) as u8; - - Rgba([r, g, b, 255]) - } - - /// Render histogram to PNG file - fn render_histogram_to_png( - &self, - path: &std::path::Path, - ) -> Result<(), Box> { - // Get tab and file data - let tab_idx = self.active_tab.ok_or("No active tab")?; - let config = &self.tabs[tab_idx].histogram_state.config; - let file_idx = self.tabs[tab_idx].file_index; - - if file_idx >= self.files.len() { - return Err("Invalid file index".into()); - } - - let file = &self.files[file_idx]; - let mode = config.mode; - let (grid_cols, grid_rows) = config.effective_grid_size(); - - let x_idx = config.x_channel.ok_or("X axis not selected")?; - let y_idx = config.y_channel.ok_or("Y axis not selected")?; - let z_idx = if mode == HistogramMode::AverageZ { - config - .z_channel - .ok_or("Z axis not selected for Average mode")? - } else { - 0 // unused - }; - - // Get channel data - let x_data = file.log.get_channel_data(x_idx); - let y_data = file.log.get_channel_data(y_idx); - let z_data = if mode == HistogramMode::AverageZ { - Some(file.log.get_channel_data(z_idx)) - } else { - None - }; - - if x_data.is_empty() || y_data.is_empty() { - return Err("No data available".into()); - } - - // Calculate data bounds - let x_min = x_data.iter().cloned().fold(f64::MAX, f64::min); - let x_max = x_data.iter().cloned().fold(f64::MIN, f64::max); - let y_min = y_data.iter().cloned().fold(f64::MAX, f64::min); - let y_max = y_data.iter().cloned().fold(f64::MIN, f64::max); - - let x_range = if (x_max - x_min).abs() < f64::EPSILON { - 1.0 - } else { - x_max - x_min - }; - let y_range = if (y_max - y_min).abs() < f64::EPSILON { - 1.0 - } else { - y_max - y_min - }; - - // Build histogram grid - let mut hit_counts = vec![vec![0u32; grid_size]; grid_size]; - let mut z_sums = vec![vec![0.0f64; grid_size]; grid_size]; - - for i in 0..x_data.len() { - let x_bin = (((x_data[i] - x_min) / x_range) * (grid_size - 1) as f64).round() as usize; - let y_bin = (((y_data[i] - y_min) / y_range) * (grid_size - 1) as f64).round() as usize; - let x_bin = x_bin.min(grid_size - 1); - let y_bin = y_bin.min(grid_size - 1); - - hit_counts[y_bin][x_bin] += 1; - if let Some(ref z) = z_data { - z_sums[y_bin][x_bin] += z[i]; - } - } - - // Calculate cell values and find min/max for color scaling - let mut cell_values = vec![vec![None::; grid_size]; grid_size]; - let mut min_value: f64 = f64::MAX; - let mut max_value: f64 = f64::MIN; - - for y_bin in 0..grid_size { - for x_bin in 0..grid_size { - let hits = hit_counts[y_bin][x_bin]; - if hits > 0 { - let value = match mode { - HistogramMode::HitCount => hits as f64, - HistogramMode::AverageZ => z_sums[y_bin][x_bin] / hits as f64, - }; - cell_values[y_bin][x_bin] = Some(value); - min_value = min_value.min(value); - max_value = max_value.max(value); - } - } - } - - let value_range = if (max_value - min_value).abs() < f64::EPSILON { - 1.0 - } else { - max_value - min_value - }; - - // Image dimensions - let width = 1920u32; - let height = 1080u32; - let margin = 80u32; - let legend_width = 100u32; - - let chart_left = margin; - let chart_right = width - margin - legend_width; - let chart_top = margin; - let chart_bottom = height - margin; - - let chart_width = chart_right - chart_left; - let chart_height = chart_bottom - chart_top; - let cell_width = chart_width as f64 / grid_size as f64; - let cell_height = chart_height as f64 / grid_size as f64; - - // Create image buffer - let mut imgbuf = RgbaImage::new(width, height); - - // Fill with dark background - for pixel in imgbuf.pixels_mut() { - *pixel = Rgba([30, 30, 30, 255]); - } - - // Draw histogram cells - #[allow(clippy::needless_range_loop)] - for y_bin in 0..grid_size { - for x_bin in 0..grid_size { - if let Some(value) = cell_values[y_bin][x_bin] { - // Calculate color - let normalized = if mode == HistogramMode::HitCount && max_value > 1.0 { - (value.ln() / max_value.ln()).clamp(0.0, 1.0) - } else { - ((value - min_value) / value_range).clamp(0.0, 1.0) - }; - let color = Self::get_png_heat_color(normalized); - - // Calculate cell position (Y inverted - higher values at top) - let cell_x = chart_left as f64 + x_bin as f64 * cell_width; - let cell_y = chart_bottom as f64 - (y_bin + 1) as f64 * cell_height; - - // Fill cell - for py in 0..(cell_height.ceil() as u32) { - for px in 0..(cell_width.ceil() as u32) { - let x = (cell_x as u32 + px).min(width - 1); - let y = (cell_y as u32 + py).min(height - 1); - imgbuf.put_pixel(x, y, color); - } - } - } - } - } - - // Draw grid lines - let grid_color = Rgba([80, 80, 80, 255]); - for i in 0..=grid_size { - let x = chart_left + (i as f64 * cell_width) as u32; - let y = chart_top + (i as f64 * cell_height) as u32; - - // Vertical line - for py in chart_top..chart_bottom { - if x < width { - imgbuf.put_pixel(x, py, grid_color); - } - } - - // Horizontal line - for px in chart_left..chart_right { - if y < height { - imgbuf.put_pixel(px, y, grid_color); - } - } - } - - // Draw color scale legend - let legend_left = width - margin - legend_width + 20; - let legend_bar_width = 20u32; - let legend_height = chart_height; - - for i in 0..legend_height { - let t = i as f64 / legend_height as f64; - let color = Self::get_png_heat_color(t); - - for px in 0..legend_bar_width { - let x = legend_left + px; - let y = chart_bottom - i; - if x < width && y < height { - imgbuf.put_pixel(x, y, color); - } - } - } - - // Save the image - imgbuf.save(path)?; - - Ok(()) - } - - /// Render scatter plot to PNG file (exports both left and right plots) - fn render_scatter_plot_to_png( - &self, - path: &std::path::Path, - ) -> Result<(), Box> { - let tab_idx = self.active_tab.ok_or("No active tab")?; - - // Image dimensions (wider to fit two plots) - let width = 2560u32; - let height = 1080u32; - let margin = 60u32; - let gap = 40u32; - - let plot_width = (width - 2 * margin - gap) / 2; - let plot_height = height - 2 * margin; - - // Create image buffer - let mut imgbuf = RgbaImage::new(width, height); - - // Fill with dark background - for pixel in imgbuf.pixels_mut() { - *pixel = Rgba([30, 30, 30, 255]); - } - - // Render left plot - let left_config = &self.tabs[tab_idx].scatter_plot_state.left; - let left_rect = (margin, margin, plot_width, plot_height); - self.render_scatter_plot_to_image(&mut imgbuf, left_config, left_rect, tab_idx)?; - - // Render right plot - let right_config = &self.tabs[tab_idx].scatter_plot_state.right; - let right_rect = (margin + plot_width + gap, margin, plot_width, plot_height); - self.render_scatter_plot_to_image(&mut imgbuf, right_config, right_rect, tab_idx)?; - - // Save the image - imgbuf.save(path)?; - - Ok(()) - } - - /// Helper to render a single scatter plot to an image region - fn render_scatter_plot_to_image( - &self, - imgbuf: &mut RgbaImage, - config: &crate::state::ScatterPlotConfig, - rect: (u32, u32, u32, u32), - tab_idx: usize, - ) -> Result<(), Box> { - let (left, top, width, height) = rect; - let right = left + width; - let bottom = top + height; - - let file_idx = config.file_index.unwrap_or(self.tabs[tab_idx].file_index); - - // Check if we have valid axis selections - let (x_idx, y_idx) = match (config.x_channel, config.y_channel) { - (Some(x), Some(y)) => (x, y), - _ => { - // Draw placeholder - let placeholder_color = Rgba([80, 80, 80, 255]); - for y in top..bottom { - for x in left..right { - imgbuf.put_pixel(x, y, placeholder_color); - } - } - return Ok(()); - } - }; - - if file_idx >= self.files.len() { - return Err("Invalid file index".into()); - } - - let file = &self.files[file_idx]; - let x_data = file.log.get_channel_data(x_idx); - let y_data = file.log.get_channel_data(y_idx); - - if x_data.is_empty() || y_data.is_empty() || x_data.len() != y_data.len() { - return Err("No data available".into()); - } - - // Calculate data bounds - let x_min = x_data.iter().cloned().fold(f64::MAX, f64::min); - let x_max = x_data.iter().cloned().fold(f64::MIN, f64::max); - let y_min = y_data.iter().cloned().fold(f64::MAX, f64::min); - let y_max = y_data.iter().cloned().fold(f64::MIN, f64::max); - - let x_range = if (x_max - x_min).abs() < f64::EPSILON { - 1.0 - } else { - x_max - x_min - }; - let y_range = if (y_max - y_min).abs() < f64::EPSILON { - 1.0 - } else { - y_max - y_min - }; - - // Build 2D histogram (512 bins like the UI) - const HEATMAP_BINS: usize = 512; - let mut histogram = vec![vec![0u32; HEATMAP_BINS]; HEATMAP_BINS]; - let mut max_hits: u32 = 0; - - for (&x, &y) in x_data.iter().zip(y_data.iter()) { - let x_bin = (((x - x_min) / x_range) * (HEATMAP_BINS - 1) as f64).round() as usize; - let y_bin = (((y - y_min) / y_range) * (HEATMAP_BINS - 1) as f64).round() as usize; - - let x_bin = x_bin.min(HEATMAP_BINS - 1); - let y_bin = y_bin.min(HEATMAP_BINS - 1); - - histogram[y_bin][x_bin] += 1; - max_hits = max_hits.max(histogram[y_bin][x_bin]); - } - - // Fill background with black - let bg_color = Rgba([0, 0, 0, 255]); - for y in top..bottom { - for x in left..right { - imgbuf.put_pixel(x, y, bg_color); - } - } - - let cell_width = width as f64 / HEATMAP_BINS as f64; - let cell_height = height as f64 / HEATMAP_BINS as f64; - - // Draw heatmap cells - #[allow(clippy::needless_range_loop)] - for y_bin in 0..HEATMAP_BINS { - for x_bin in 0..HEATMAP_BINS { - let hits = histogram[y_bin][x_bin]; - if hits > 0 { - // Normalize using log scale - let normalized = if max_hits > 1 { - (hits as f64).ln() / (max_hits as f64).ln() - } else { - 1.0 - }; - let color = Self::get_png_heat_color(normalized); - - // Calculate cell position (Y inverted) - let cell_x = left as f64 + x_bin as f64 * cell_width; - let cell_y = bottom as f64 - (y_bin + 1) as f64 * cell_height; - - // Fill cell - for py in 0..(cell_height.ceil() as u32 + 1) { - for px in 0..(cell_width.ceil() as u32 + 1) { - let x = (cell_x as u32 + px).min(right - 1); - let y = (cell_y as u32 + py).min(bottom - 1); - if x >= left && y >= top { - imgbuf.put_pixel(x, y, color); - } - } - } - } - } - } - - Ok(()) - } - - /// Render scatter plot to PDF file - fn render_scatter_plot_to_pdf( - &self, - path: &std::path::Path, - ) -> Result<(), Box> { - let tab_idx = self.active_tab.ok_or("No active tab")?; - - // Create PDF document (A4 landscape) - let (doc, page1, layer1) = PdfDocument::new( - "UltraLog Scatter Plot Export", - Mm(297.0), - Mm(210.0), - "Scatter Plot", - ); - - let current_layer = doc.get_page(page1).get_layer(layer1); - - // Draw title - let font = doc.add_builtin_font(BuiltinFont::HelveticaBold)?; - let font_regular = doc.add_builtin_font(BuiltinFont::Helvetica)?; - - current_layer.use_text( - "UltraLog Scatter Plot Export", - 16.0, - Mm(20.0), - Mm(200.0), - &font, - ); - - // Subtitle with file info - if let Some(file_idx) = self.selected_file { - if file_idx < self.files.len() { - let file = &self.files[file_idx]; - current_layer.use_text(&file.name, 10.0, Mm(20.0), Mm(192.0), &font_regular); - } - } - - // Layout: two plots side by side - let margin: f64 = 20.0; - let gap: f64 = 15.0; - let plot_width: f64 = (297.0 - 2.0 * margin - gap) / 2.0; - let plot_height: f64 = 150.0; - let plot_top: f64 = 180.0; - let plot_bottom: f64 = plot_top - plot_height; - - // Render left plot - let left_config = &self.tabs[tab_idx].scatter_plot_state.left; - let left_rect = (margin, plot_bottom, plot_width, plot_height); - self.render_scatter_plot_to_pdf_region( - ¤t_layer, - &font_regular, - left_config, - left_rect, - tab_idx, - )?; - - // Render right plot - let right_config = &self.tabs[tab_idx].scatter_plot_state.right; - let right_rect = ( - margin + plot_width + gap, - plot_bottom, - plot_width, - plot_height, - ); - self.render_scatter_plot_to_pdf_region( - ¤t_layer, - &font_regular, - right_config, - right_rect, - tab_idx, - )?; - - // Save PDF - let file = File::create(path)?; - let mut writer = BufWriter::new(file); - doc.save(&mut writer)?; - - Ok(()) - } - - /// Helper to render a single scatter plot to a PDF region - fn render_scatter_plot_to_pdf_region( - &self, - layer: &printpdf::PdfLayerReference, - font: &printpdf::IndirectFontRef, - config: &crate::state::ScatterPlotConfig, - rect: (f64, f64, f64, f64), - tab_idx: usize, - ) -> Result<(), Box> { - let (left, bottom, width, height) = rect; - let right = left + width; - let top = bottom + height; - - let file_idx = config.file_index.unwrap_or(self.tabs[tab_idx].file_index); - - // Check if we have valid axis selections - let (x_idx, y_idx) = match (config.x_channel, config.y_channel) { - (Some(x), Some(y)) => (x, y), - _ => { - // Draw placeholder text - layer.use_text( - "No axes selected", - 10.0, - Mm((left + width / 2.0 - 15.0) as f32), - Mm((bottom + height / 2.0) as f32), - font, - ); - return Ok(()); - } - }; - - if file_idx >= self.files.len() { - return Err("Invalid file index".into()); - } - - let file = &self.files[file_idx]; - let x_data = file.log.get_channel_data(x_idx); - let y_data = file.log.get_channel_data(y_idx); - - if x_data.is_empty() || y_data.is_empty() || x_data.len() != y_data.len() { - return Err("No data available".into()); - } - - // Get channel names for labels - let x_name = file.log.channels[x_idx].name(); - let y_name = file.log.channels[y_idx].name(); - - // Draw axis labels - layer.use_text( - format!("{} vs {}", y_name, x_name), - 9.0, - Mm(left as f32), - Mm((top + 3.0) as f32), - font, - ); - - // Calculate data bounds - let x_min = x_data.iter().cloned().fold(f64::MAX, f64::min); - let x_max = x_data.iter().cloned().fold(f64::MIN, f64::max); - let y_min = y_data.iter().cloned().fold(f64::MAX, f64::min); - let y_max = y_data.iter().cloned().fold(f64::MIN, f64::max); - - let x_range = if (x_max - x_min).abs() < f64::EPSILON { - 1.0 - } else { - x_max - x_min - }; - let y_range = if (y_max - y_min).abs() < f64::EPSILON { - 1.0 - } else { - y_max - y_min - }; - - // Build 2D histogram (use smaller bins for PDF) - const PDF_BINS: usize = 64; - let mut histogram = vec![vec![0u32; PDF_BINS]; PDF_BINS]; - let mut max_hits: u32 = 0; - - for (&x, &y) in x_data.iter().zip(y_data.iter()) { - let x_bin = (((x - x_min) / x_range) * (PDF_BINS - 1) as f64).round() as usize; - let y_bin = (((y - y_min) / y_range) * (PDF_BINS - 1) as f64).round() as usize; - - let x_bin = x_bin.min(PDF_BINS - 1); - let y_bin = y_bin.min(PDF_BINS - 1); - - histogram[y_bin][x_bin] += 1; - max_hits = max_hits.max(histogram[y_bin][x_bin]); - } - - let cell_width = width / PDF_BINS as f64; - let cell_height = height / PDF_BINS as f64; - - // Draw heatmap cells - #[allow(clippy::needless_range_loop)] - for y_bin in 0..PDF_BINS { - for x_bin in 0..PDF_BINS { - let hits = histogram[y_bin][x_bin]; - if hits > 0 { - // Normalize using log scale - let normalized = if max_hits > 1 { - (hits as f64).ln() / (max_hits as f64).ln() - } else { - 1.0 - }; - let color = Self::get_pdf_heat_color(normalized); - - layer.set_fill_color(color); - - let cell_x = left + x_bin as f64 * cell_width; - let cell_y = bottom + y_bin as f64 * cell_height; - - let rect = printpdf::Polygon { - rings: vec![vec![ - (Point::new(Mm(cell_x as f32), Mm(cell_y as f32)), false), - ( - Point::new(Mm((cell_x + cell_width) as f32), Mm(cell_y as f32)), - false, - ), - ( - Point::new( - Mm((cell_x + cell_width) as f32), - Mm((cell_y + cell_height) as f32), - ), - false, - ), - ( - Point::new(Mm(cell_x as f32), Mm((cell_y + cell_height) as f32)), - false, - ), - ]], - mode: PaintMode::Fill, - winding_order: WindingOrder::NonZero, - }; - layer.add_polygon(rect); - } - } - } - - // Draw border - let border_color = Color::Rgb(Rgb::new(0.5, 0.5, 0.5, None)); - layer.set_outline_color(border_color); - layer.set_outline_thickness(0.5); - - let border = Line { - points: vec![ - (Point::new(Mm(left as f32), Mm(bottom as f32)), false), - (Point::new(Mm(right as f32), Mm(bottom as f32)), false), - (Point::new(Mm(right as f32), Mm(top as f32)), false), - (Point::new(Mm(left as f32), Mm(top as f32)), false), - ], - is_closed: true, - }; - layer.add_line(border); - - // Draw axis labels - let label_color = Color::Rgb(Rgb::new(0.0, 0.0, 0.0, None)); - layer.set_fill_color(label_color); - - // X axis labels - for i in 0..=4 { - let t = i as f64 / 4.0; - let value = x_min + t * x_range; - let x_pos = left + t * width; - layer.use_text( - format!("{:.0}", value), - 6.0, - Mm((x_pos - 3.0) as f32), - Mm((bottom - 5.0) as f32), - font, - ); - } - - // Y axis labels - for i in 0..=4 { - let t = i as f64 / 4.0; - let value = y_min + t * y_range; - let y_pos = bottom + t * height; - layer.use_text( - format!("{:.0}", value), - 6.0, - Mm((left - 10.0) as f32), - Mm((y_pos - 1.0) as f32), - font, - ); - } - - Ok(()) - } -} - -/// Draw a line between two points using Bresenham's algorithm -fn draw_line(img: &mut RgbaImage, x0: u32, y0: u32, x1: u32, y1: u32, color: Rgba) { - let dx = (x1 as i32 - x0 as i32).abs(); - let dy = -(y1 as i32 - y0 as i32).abs(); - let sx: i32 = if x0 < x1 { 1 } else { -1 }; - let sy: i32 = if y0 < y1 { 1 } else { -1 }; - let mut err = dx + dy; - - let mut x = x0 as i32; - let mut y = y0 as i32; - - let (width, height) = img.dimensions(); - - loop { - if x >= 0 && x < width as i32 && y >= 0 && y < height as i32 { - img.put_pixel(x as u32, y as u32, color); - } - - if x == x1 as i32 && y == y1 as i32 { - break; - } - - let e2 = 2 * err; - if e2 >= dy { - err += dy; - x += sx; - } - if e2 <= dx { - err += dx; - y += sy; - } - } -} diff --git a/src/ui/histogram.rs.backup b/src/ui/histogram.rs.backup deleted file mode 100644 index 77a6073..0000000 --- a/src/ui/histogram.rs.backup +++ /dev/null @@ -1,2399 +0,0 @@ -//! Histogram / 2D heatmap view for analyzing channel distributions. -//! -//! This module provides a histogram view where users can visualize -//! relationships between channels as a 2D grid with configurable -//! cell coloring based on average Z-value or hit count. - -use eframe::egui; -use rust_i18n::t; - -use crate::app::UltraLogApp; -use crate::normalize::sort_channels_by_priority; -use crate::state::{ - HistogramMode, PastedTable, SampleFilter, SelectedHistogramCell, TableOperation, -}; - -/// Heat map color gradient from blue (low) to red (high) -const HEAT_COLORS: &[[u8; 3]] = &[ - [0, 0, 80], // Dark blue (0.0) - [0, 0, 180], // Blue - [0, 100, 255], // Light blue - [0, 200, 255], // Cyan - [0, 255, 200], // Cyan-green - [0, 255, 100], // Green - [100, 255, 0], // Yellow-green - [200, 255, 0], // Yellow - [255, 200, 0], // Orange - [255, 100, 0], // Red-orange - [255, 0, 0], // Red (1.0) -]; - -/// Margin for axis labels and titles -const AXIS_LABEL_MARGIN_LEFT: f32 = 75.0; -const AXIS_LABEL_MARGIN_BOTTOM: f32 = 45.0; -const AXIS_LABEL_MARGIN_TOP: f32 = 10.0; -const AXIS_LABEL_MARGIN_RIGHT: f32 = 25.0; - -/// Height reserved for legend at bottom -const LEGEND_HEIGHT: f32 = 55.0; - -/// Current position indicator color (cyan, matches chart cursor) -const CURSOR_COLOR: egui::Color32 = egui::Color32::from_rgb(0, 255, 255); - -/// Cell highlight color for current position -const CELL_HIGHLIGHT_COLOR: egui::Color32 = egui::Color32::WHITE; - -/// Selected cell highlight color -const SELECTED_CELL_COLOR: egui::Color32 = egui::Color32::from_rgb(255, 165, 0); // Orange - -/// Crosshair color for cursor tracking during playback -const CURSOR_CROSSHAIR_COLOR: egui::Color32 = egui::Color32::from_rgb(128, 128, 128); // Grey - -/// Maximum length for axis labels before truncation -const MAX_AXIS_LABEL_LENGTH: usize = 20; - -/// Calculate which bin a normalized value (0.0 to 1.0) falls into -/// Uses floor-based calculation for consistent cell boundaries -#[inline] -fn calculate_bin(normalized: f32, grid_size: usize) -> usize { - ((normalized * grid_size as f32).floor() as usize).min(grid_size - 1) -} - -/// Calculate which bin a data value falls into given the data range -#[inline] -fn calculate_data_bin(value: f64, min: f64, range: f64, grid_size: usize) -> usize { - let normalized = ((value - min) / range) as f32; - calculate_bin(normalized.clamp(0.0, 1.0), grid_size) -} - -/// Truncate a string to max length with ellipsis -fn truncate_label(s: &str, max_len: usize) -> String { - if s.len() <= max_len { - s.to_string() - } else { - format!("{}…", &s[..max_len - 1]) - } -} - -/// Calculate relative luminance for WCAG contrast ratio -/// Uses the sRGB colorspace formula from WCAG 2.1 -fn calculate_luminance(color: egui::Color32) -> f64 { - let r = linearize_channel(color.r()); - let g = linearize_channel(color.g()); - let b = linearize_channel(color.b()); - 0.2126 * r + 0.7152 * g + 0.0722 * b -} - -/// Linearize an sRGB channel value (0-255) to linear RGB -fn linearize_channel(value: u8) -> f64 { - let v = value as f64 / 255.0; - if v <= 0.03928 { - v / 12.92 - } else { - ((v + 0.055) / 1.055).powf(2.4) - } -} - -/// Calculate WCAG contrast ratio between two colors -fn contrast_ratio(color1: egui::Color32, color2: egui::Color32) -> f64 { - let l1 = calculate_luminance(color1); - let l2 = calculate_luminance(color2); - let (lighter, darker) = if l1 > l2 { (l1, l2) } else { (l2, l1) }; - (lighter + 0.05) / (darker + 0.05) -} - -/// Get the best text color (black or white) for AAA compliance on given background -/// Returns the color that provides the highest contrast ratio -fn get_aaa_text_color(background: egui::Color32) -> egui::Color32 { - let white_contrast = contrast_ratio(egui::Color32::WHITE, background); - let black_contrast = contrast_ratio(egui::Color32::BLACK, background); - - // Choose whichever provides better contrast - // AAA requires 7:1 for normal text, but we pick the better option regardless - if white_contrast >= black_contrast { - egui::Color32::WHITE - } else { - egui::Color32::BLACK - } -} - -impl UltraLogApp { - /// Main entry point: render the histogram view - pub fn render_histogram_view(&mut self, ui: &mut egui::Ui) { - if self.active_tab.is_none() || self.files.is_empty() { - ui.centered_and_justified(|ui| { - ui.label( - egui::RichText::new(t!("histogram.no_file_loaded")) - .size(self.scaled_font(20.0)) - .color(egui::Color32::GRAY), - ); - }); - return; - } - - // Render tab bar - self.render_tab_bar(ui); - ui.add_space(10.0); - - // Controls are now in the sidebar (tool properties panel) - // Render the histogram grid - self.render_histogram_grid(ui); - } - - /// Render the control panel with axis selectors, mode toggle, and grid size - /// This is now called from the tool properties sidebar panel - pub(crate) fn render_histogram_controls(&mut self, ui: &mut egui::Ui) { - let Some(tab_idx) = self.active_tab else { - return; - }; - let file_idx = self.tabs[tab_idx].file_index; - - if file_idx >= self.files.len() { - return; - } - - let file = &self.files[file_idx]; - let base_channel_count = file.log.channels.len(); - - // Sort channels for dropdown (including computed channels) - let mut sorted_channels = sort_channels_by_priority( - base_channel_count, - |idx| file.log.channels[idx].name(), - self.field_normalization, - Some(&self.custom_normalizations), - ); - - // Add computed channels to the dropdown - if let Some(computed_channels) = self.file_computed_channels.get(&file_idx) { - for (computed_idx, computed) in computed_channels.iter().enumerate() { - let channel_idx = base_channel_count + computed_idx; - // Add with is_priority=false (computed channels listed after prioritized channels) - sorted_channels.push(( - channel_idx, - format!("📊 {}", &computed.template.name), - false, - )); - } - } - - // Get current selections - let config = &self.tabs[tab_idx].histogram_state.config; - let current_x = config.x_channel; - let current_y = config.y_channel; - let current_z = config.z_channel; - let current_mode = config.mode; - let current_grid_size = config.grid_size; - let current_custom_grid = config.custom_grid_size; - let current_min_hits = config.min_hits_filter; - let current_custom_x = config.custom_x_range; - let current_custom_y = config.custom_y_range; - let has_pasted_table = config.pasted_table.is_some(); - let current_table_op = config.table_operation; - let current_show_compare = config.show_comparison_view; - - // Calculate dynamic data bounds for use as defaults when unchecking Auto - let (dynamic_x_min, dynamic_x_max) = if let Some(x_idx) = current_x { - let x_data = self.get_channel_data(file_idx, x_idx); - if !x_data.is_empty() { - let min = x_data.iter().cloned().fold(f64::MAX, f64::min); - let max = x_data.iter().cloned().fold(f64::MIN, f64::max); - (min, max) - } else { - (0.0, 100.0) - } - } else { - (0.0, 100.0) - }; - let (dynamic_y_min, dynamic_y_max) = if let Some(y_idx) = current_y { - let y_data = self.get_channel_data(file_idx, y_idx); - if !y_data.is_empty() { - let min = y_data.iter().cloned().fold(f64::MAX, f64::min); - let max = y_data.iter().cloned().fold(f64::MIN, f64::max); - (min, max) - } else { - (0.0, 100.0) - } - } else { - (0.0, 100.0) - }; - - // Build channel name lookup - let channel_names: std::collections::HashMap = sorted_channels - .iter() - .map(|(idx, name, _)| (*idx, name.clone())) - .collect(); - - // Track selections for deferred updates - let mut new_x: Option = None; - let mut new_y: Option = None; - let mut new_z: Option = None; - let mut new_mode: Option = None; - let mut new_custom_grid: Option = None; - let mut new_min_hits: Option = None; - let mut new_custom_x: Option> = None; - let mut new_custom_y: Option> = None; - let mut new_table_op: Option = None; - let mut new_show_compare: Option = None; - let mut clear_pasted_table = false; - let mut do_paste = false; - let mut sample_filter_updates: Vec<(usize, SampleFilter)> = Vec::new(); - let mut sample_filters_to_remove: Vec = Vec::new(); - let mut new_sample_filter: Option = None; - - // Pre-compute scaled font sizes - let font_14 = self.scaled_font(14.0); - let font_15 = self.scaled_font(15.0); - - ui.horizontal(|ui| { - // X Axis selector - ui.label(egui::RichText::new(t!("histogram.x_axis")).size(font_15)); - egui::ComboBox::from_id_salt("histogram_x") - .selected_text( - egui::RichText::new( - current_x - .and_then(|i| channel_names.get(&i).map(|n| n.as_str())) - .unwrap_or("Select..."), - ) - .size(font_14), - ) - .width(160.0) - .show_ui(ui, |ui| { - for (idx, name, _) in &sorted_channels { - if ui - .selectable_label( - current_x == Some(*idx), - egui::RichText::new(name).size(font_14), - ) - .clicked() - { - new_x = Some(*idx); - } - } - }); - - ui.add_space(16.0); - - // Y Axis selector - ui.label(egui::RichText::new(t!("histogram.y_axis")).size(font_15)); - egui::ComboBox::from_id_salt("histogram_y") - .selected_text( - egui::RichText::new( - current_y - .and_then(|i| channel_names.get(&i).map(|n| n.as_str())) - .unwrap_or("Select..."), - ) - .size(font_14), - ) - .width(160.0) - .show_ui(ui, |ui| { - for (idx, name, _) in &sorted_channels { - if ui - .selectable_label( - current_y == Some(*idx), - egui::RichText::new(name).size(font_14), - ) - .clicked() - { - new_y = Some(*idx); - } - } - }); - - ui.add_space(16.0); - - // Z Axis selector (only enabled in AverageZ mode) - let z_enabled = current_mode == HistogramMode::AverageZ; - ui.add_enabled_ui(z_enabled, |ui| { - ui.label(egui::RichText::new(t!("histogram.z_axis")).size(font_15)); - egui::ComboBox::from_id_salt("histogram_z") - .selected_text( - egui::RichText::new( - current_z - .and_then(|i| channel_names.get(&i).map(|n| n.as_str())) - .unwrap_or("Select..."), - ) - .size(font_14), - ) - .width(160.0) - .show_ui(ui, |ui| { - for (idx, name, _) in &sorted_channels { - if ui - .selectable_label( - current_z == Some(*idx), - egui::RichText::new(name).size(font_14), - ) - .clicked() - { - new_z = Some(*idx); - } - } - }); - }); - - ui.add_space(20.0); - - // Grid size selector - show both presets and custom input - ui.label(egui::RichText::new(t!("histogram.grid")).size(font_15)); - - // Show current effective grid size - let effective_grid = if current_custom_grid > 0 { - current_custom_grid - } else { - current_grid_size.size() - }; - - // Custom grid size input - let mut grid_value = effective_grid as i32; - let drag = egui::DragValue::new(&mut grid_value) - .range(4..=256) - .speed(1.0) - .suffix("×"); - if ui.add(drag).changed() { - new_custom_grid = Some(grid_value.clamp(4, 256) as usize); - } - - ui.add_space(20.0); - - // Mode toggle - ui.label(egui::RichText::new(t!("histogram.mode")).size(font_15)); - if ui - .selectable_label( - current_mode == HistogramMode::AverageZ, - egui::RichText::new(t!("histogram.average_z")).size(font_14), - ) - .clicked() - { - new_mode = Some(HistogramMode::AverageZ); - } - if ui - .selectable_label( - current_mode == HistogramMode::HitCount, - egui::RichText::new(t!("histogram.hit_count")).size(font_14), - ) - .clicked() - { - new_mode = Some(HistogramMode::HitCount); - } - - ui.add_space(20.0); - - // Min hits filter - ui.label(egui::RichText::new(t!("histogram.min_hits")).size(font_15)); - let mut min_hits_value = current_min_hits as i32; - let min_hits_drag = egui::DragValue::new(&mut min_hits_value) - .range(0..=1000) - .speed(1.0); - if ui.add(min_hits_drag).changed() { - new_min_hits = Some(min_hits_value.max(0) as u32); - } - }); - - // Second row: Custom ranges - ui.horizontal(|ui| { - ui.add_space(4.0); - - // X Range - ui.label(egui::RichText::new(t!("histogram.x_range")).size(font_14)); - let mut x_auto = current_custom_x.is_none(); - if ui.checkbox(&mut x_auto, t!("histogram.auto")).changed() { - if x_auto { - new_custom_x = Some(None); - } else { - // When unchecking Auto, initialize with current dynamic values - new_custom_x = Some(Some((dynamic_x_min, dynamic_x_max))); - } - } - if !x_auto { - let (mut x_min, mut x_max) = - current_custom_x.unwrap_or((dynamic_x_min, dynamic_x_max)); - ui.add(egui::DragValue::new(&mut x_min).speed(1.0).prefix("Min: ")); - ui.add(egui::DragValue::new(&mut x_max).speed(1.0).prefix("Max: ")); - if x_max > x_min { - new_custom_x = Some(Some((x_min, x_max))); - } - } - - ui.add_space(16.0); - - // Y Range - ui.label(egui::RichText::new(t!("histogram.y_range")).size(font_14)); - let mut y_auto = current_custom_y.is_none(); - if ui.checkbox(&mut y_auto, t!("histogram.auto")).changed() { - if y_auto { - new_custom_y = Some(None); - } else { - // When unchecking Auto, initialize with current dynamic values - new_custom_y = Some(Some((dynamic_y_min, dynamic_y_max))); - } - } - if !y_auto { - let (mut y_min, mut y_max) = - current_custom_y.unwrap_or((dynamic_y_min, dynamic_y_max)); - ui.add(egui::DragValue::new(&mut y_min).speed(1.0).prefix("Min: ")); - ui.add(egui::DragValue::new(&mut y_max).speed(1.0).prefix("Max: ")); - if y_max > y_min { - new_custom_y = Some(Some((y_min, y_max))); - } - } - - ui.add_space(20.0); - - // Copy to clipboard button - if ui.button(format!("📋 {}", t!("histogram.copy"))).clicked() { - self.copy_histogram_to_clipboard(tab_idx); - } - - // Paste from clipboard button - if ui.button(format!("📥 {}", t!("histogram.paste"))).clicked() { - do_paste = true; - } - }); - - // Sample Filters section (collapsible) - let current_sample_filters = config.sample_filters.clone(); - egui::CollapsingHeader::new( - egui::RichText::new(format!("🔍 {}", t!("histogram.sample_filters"))).size(font_15), - ) - .default_open(false) - .show(ui, |ui| { - ui.add_space(4.0); - - if current_sample_filters.is_empty() { - ui.label( - egui::RichText::new(t!("histogram.no_filters")) - .size(font_14) - .color(egui::Color32::GRAY), - ); - } else { - // Show active filters - for (filter_idx, filter) in current_sample_filters.iter().enumerate() { - ui.horizontal(|ui| { - // Enable/disable checkbox - let mut enabled = filter.enabled; - if ui.checkbox(&mut enabled, "").changed() { - let mut updated_filter = filter.clone(); - updated_filter.enabled = enabled; - sample_filter_updates.push((filter_idx, updated_filter)); - } - - // Channel name - ui.label( - egui::RichText::new(&filter.channel_name) - .size(font_14) - .color(if enabled { - egui::Color32::WHITE - } else { - egui::Color32::GRAY - }), - ); - - ui.add_space(8.0); - - // Min value - let mut min_val = filter.min_value.unwrap_or(f64::NEG_INFINITY); - let min_enabled = filter.min_value.is_some(); - ui.add_enabled_ui(enabled, |ui| { - let mut use_min = min_enabled; - if ui.checkbox(&mut use_min, t!("histogram.filter_min")).changed() - || (use_min - && ui - .add( - egui::DragValue::new(&mut min_val) - .speed(1.0) - .range(f64::NEG_INFINITY..=f64::INFINITY), - ) - .changed()) - { - let mut updated_filter = filter.clone(); - updated_filter.min_value = if use_min { Some(min_val) } else { None }; - sample_filter_updates.push((filter_idx, updated_filter)); - } - }); - - ui.add_space(4.0); - - // Max value - let mut max_val = filter.max_value.unwrap_or(f64::INFINITY); - let max_enabled = filter.max_value.is_some(); - ui.add_enabled_ui(enabled, |ui| { - let mut use_max = max_enabled; - if ui.checkbox(&mut use_max, t!("histogram.filter_max")).changed() - || (use_max - && ui - .add( - egui::DragValue::new(&mut max_val) - .speed(1.0) - .range(f64::NEG_INFINITY..=f64::INFINITY), - ) - .changed()) - { - let mut updated_filter = filter.clone(); - updated_filter.max_value = if use_max { Some(max_val) } else { None }; - sample_filter_updates.push((filter_idx, updated_filter)); - } - }); - - ui.add_space(8.0); - - // Remove button - if ui - .button(egui::RichText::new("❌").size(font_14)) - .on_hover_text(t!("histogram.remove_filter")) - .clicked() - { - sample_filters_to_remove.push(filter_idx); - } - }); - } - } - - ui.add_space(8.0); - - // Add filter button with channel selector - ui.label(egui::RichText::new(t!("histogram.add_filter")).size(font_14)); - egui::ComboBox::from_id_salt("add_sample_filter") - .selected_text( - egui::RichText::new(t!("histogram.filter_channel")) - .size(font_14), - ) - .width(200.0) - .show_ui(ui, |ui| { - for (idx, name, _) in &sorted_channels { - if ui - .selectable_label(false, egui::RichText::new(name).size(font_14)) - .clicked() - { - new_sample_filter = Some(SampleFilter::new(*idx, name.clone())); - } - } - }); - }); - - ui.horizontal(|ui| { - // Show comparison controls if a table is pasted - if has_pasted_table { - ui.add_space(12.0); - ui.separator(); - ui.add_space(12.0); - - // Operation selector - ui.label(egui::RichText::new(t!("histogram.operation")).size(font_14)); - - if ui - .selectable_label(current_table_op == TableOperation::Add, "+") - .clicked() - { - new_table_op = Some(TableOperation::Add); - } - if ui - .selectable_label(current_table_op == TableOperation::Subtract, "−") - .clicked() - { - new_table_op = Some(TableOperation::Subtract); - } - if ui - .selectable_label(current_table_op == TableOperation::Multiply, "×") - .clicked() - { - new_table_op = Some(TableOperation::Multiply); - } - if ui - .selectable_label(current_table_op == TableOperation::Divide, "÷") - .clicked() - { - new_table_op = Some(TableOperation::Divide); - } - - ui.add_space(8.0); - - // Toggle comparison view - let mut show_compare = current_show_compare; - if ui - .checkbox(&mut show_compare, t!("histogram.compare")) - .changed() - { - new_show_compare = Some(show_compare); - } - - ui.add_space(8.0); - - // Clear pasted table button - if ui.button(format!("❌ {}", t!("histogram.clear"))).clicked() { - clear_pasted_table = true; - } - - ui.add_space(8.0); - - // Copy Result button (for pasting back into ECU) - if ui - .button(format!("📋 {}", t!("histogram.copy_result"))) - .clicked() - { - self.copy_result_to_clipboard(tab_idx); - } - } - }); - - // Apply deferred updates - let config = &mut self.tabs[tab_idx].histogram_state.config; - if let Some(x) = new_x { - config.x_channel = Some(x); - config.selected_cell = None; // Clear selection on axis change - } - if let Some(y) = new_y { - config.y_channel = Some(y); - config.selected_cell = None; - } - if let Some(z) = new_z { - config.z_channel = Some(z); - config.selected_cell = None; - } - if let Some(mode) = new_mode { - config.mode = mode; - config.selected_cell = None; - } - if let Some(size) = new_custom_grid { - config.custom_grid_size = size; - config.selected_cell = None; - } - if let Some(min_hits) = new_min_hits { - config.min_hits_filter = min_hits; - } - if let Some(range) = new_custom_x { - config.custom_x_range = range; - config.selected_cell = None; - } - if let Some(range) = new_custom_y { - config.custom_y_range = range; - config.selected_cell = None; - } - if let Some(op) = new_table_op { - config.table_operation = op; - } - if let Some(show) = new_show_compare { - config.show_comparison_view = show; - } - if clear_pasted_table { - config.pasted_table = None; - config.show_comparison_view = false; - } - - // Apply sample filter updates - for (idx, updated_filter) in sample_filter_updates { - if idx < config.sample_filters.len() { - config.sample_filters[idx] = updated_filter; - } - } - - // Remove filters (in reverse order to preserve indices) - for idx in sample_filters_to_remove.iter().rev() { - if *idx < config.sample_filters.len() { - config.sample_filters.remove(*idx); - } - } - - // Add new filter - if let Some(filter) = new_sample_filter { - config.sample_filters.push(filter); - } - - // Handle paste after all config updates are done - if do_paste { - self.paste_table_from_clipboard(tab_idx); - } - } - - /// Render the histogram grid with current position indicator - fn render_histogram_grid(&mut self, ui: &mut egui::Ui) { - let Some(tab_idx) = self.active_tab else { - return; - }; - - let config = &self.tabs[tab_idx].histogram_state.config; - let file_idx = self.tabs[tab_idx].file_index; - let mode = config.mode; - let grid_size = config.effective_grid_size(); - let min_hits_filter = config.min_hits_filter; - let custom_x_range = config.custom_x_range; - let custom_y_range = config.custom_y_range; - let sample_filters = config.sample_filters.clone(); - let show_comparison = config.show_comparison_view && config.pasted_table.is_some(); - let pasted_table = config.pasted_table.clone(); - let table_operation = config.table_operation; - - // Pre-compute scaled font sizes for use in closures - let font_10 = self.scaled_font(10.0); - let font_12 = self.scaled_font(12.0); - let font_13 = self.scaled_font(13.0); - let font_16 = self.scaled_font(16.0); - - // Check valid axis selections - let (x_idx, y_idx) = match (config.x_channel, config.y_channel) { - (Some(x), Some(y)) => (x, y), - _ => { - let available = ui.available_size(); - let (rect, _) = ui.allocate_exact_size(available, egui::Sense::hover()); - ui.painter().text( - rect.center(), - egui::Align2::CENTER_CENTER, - "Select X and Y axes", - egui::FontId::proportional(font_16), - egui::Color32::GRAY, - ); - return; - } - }; - - // Check Z axis for AverageZ mode - let z_idx = if mode == HistogramMode::AverageZ { - match config.z_channel { - Some(z) => Some(z), - None => { - let available = ui.available_size(); - let (rect, _) = ui.allocate_exact_size(available, egui::Sense::hover()); - ui.painter().text( - rect.center(), - egui::Align2::CENTER_CENTER, - "Select Z axis for Average mode", - egui::FontId::proportional(font_16), - egui::Color32::GRAY, - ); - return; - } - } - } else { - None - }; - - if file_idx >= self.files.len() { - return; - } - - let file = &self.files[file_idx]; - let x_data = self.get_channel_data(file_idx, x_idx); - let y_data = self.get_channel_data(file_idx, y_idx); - let z_data = z_idx.map(|z| self.get_channel_data(file_idx, z)); - - if x_data.is_empty() || y_data.is_empty() || x_data.len() != y_data.len() { - return; - } - - // Calculate data bounds (use custom ranges if set, otherwise auto from data) - let (x_min, x_max) = match custom_x_range { - Some((min, max)) if max > min => (min, max), - _ => { - let min = x_data.iter().cloned().fold(f64::MAX, f64::min); - let max = x_data.iter().cloned().fold(f64::MIN, f64::max); - (min, max) - } - }; - let (y_min, y_max) = match custom_y_range { - Some((min, max)) if max > min => (min, max), - _ => { - let min = y_data.iter().cloned().fold(f64::MAX, f64::min); - let max = y_data.iter().cloned().fold(f64::MIN, f64::max); - (min, max) - } - }; - - let x_range = if (x_max - x_min).abs() < f64::EPSILON { - 1.0 - } else { - x_max - x_min - }; - let y_range = if (y_max - y_min).abs() < f64::EPSILON { - 1.0 - } else { - y_max - y_min - }; - - // Pre-fetch filter channel data for efficiency - let filter_data: Vec<(&SampleFilter, Vec)> = sample_filters - .iter() - .filter(|f| f.enabled) - .map(|f| (f, self.get_channel_data(file_idx, f.channel_idx))) - .collect(); - - // Build histogram grid with full statistics - let mut hit_counts = vec![vec![0u32; grid_size]; grid_size]; - let mut z_sums = vec![vec![0.0f64; grid_size]; grid_size]; - let mut z_sum_sq = vec![vec![0.0f64; grid_size]; grid_size]; - let mut z_mins = vec![vec![f64::MAX; grid_size]; grid_size]; - let mut z_maxs = vec![vec![f64::MIN; grid_size]; grid_size]; - - 'sample_loop: for i in 0..x_data.len() { - // Check all sample filters (AND logic) - for (filter, data) in &filter_data { - if i >= data.len() { - continue 'sample_loop; - } - let val = data[i]; - if let Some(min) = filter.min_value { - if val < min { - continue 'sample_loop; - } - } - if let Some(max) = filter.max_value { - if val > max { - continue 'sample_loop; - } - } - } - - // Skip samples outside custom range (if set) - if custom_x_range.is_some() && (x_data[i] < x_min || x_data[i] > x_max) { - continue; - } - if custom_y_range.is_some() && (y_data[i] < y_min || y_data[i] > y_max) { - continue; - } - - let x_bin = calculate_data_bin(x_data[i], x_min, x_range, grid_size); - let y_bin = calculate_data_bin(y_data[i], y_min, y_range, grid_size); - - hit_counts[y_bin][x_bin] += 1; - if let Some(ref z) = z_data { - let z_val = z[i]; - z_sums[y_bin][x_bin] += z_val; - z_sum_sq[y_bin][x_bin] += z_val * z_val; - z_mins[y_bin][x_bin] = z_mins[y_bin][x_bin].min(z_val); - z_maxs[y_bin][x_bin] = z_maxs[y_bin][x_bin].max(z_val); - } - } - - // Calculate cell values and find min/max for color scaling - let mut cell_values = vec![vec![None::; grid_size]; grid_size]; - let mut min_value: f64 = f64::MAX; - let mut max_value: f64 = f64::MIN; - - for y_bin in 0..grid_size { - for x_bin in 0..grid_size { - let hits = hit_counts[y_bin][x_bin]; - if hits > 0 { - let value = match mode { - HistogramMode::HitCount => hits as f64, - HistogramMode::AverageZ => z_sums[y_bin][x_bin] / hits as f64, - }; - cell_values[y_bin][x_bin] = Some(value); - min_value = min_value.min(value); - max_value = max_value.max(value); - } - } - } - - // Handle case where all values are the same - let value_range = if (max_value - min_value).abs() < f64::EPSILON { - 1.0 - } else { - max_value - min_value - }; - - // Comparison view: show Histogram, Pasted Table, and Result side-by-side - if show_comparison { - if let Some(ref pasted) = pasted_table { - // Resample pasted table to match grid size - let resampled = Self::resample_table(pasted, grid_size); - let pasted_values: Vec>> = resampled - .iter() - .map(|row| row.iter().map(|&v| Some(v)).collect()) - .collect(); - - // Calculate result by applying operation - let result_values = - Self::apply_table_operation(&cell_values, &resampled, table_operation); - - // Find min/max for pasted and result - let mut pasted_min = f64::MAX; - let mut pasted_max = f64::MIN; - for row in &resampled { - for &v in row { - pasted_min = pasted_min.min(v); - pasted_max = pasted_max.max(v); - } - } - - let mut result_min = f64::MAX; - let mut result_max = f64::MIN; - for row in &result_values { - for val in row.iter().flatten() { - result_min = result_min.min(*val); - result_max = result_max.max(*val); - } - } - - // Allocate space for comparison view - let available = ui.available_size(); - let chart_height = (available.y - LEGEND_HEIGHT - 40.0).max(200.0); - let panel_width = (available.x - 20.0) / 3.0; - - let (full_rect, _response) = ui.allocate_exact_size( - egui::vec2(available.x, chart_height + 30.0), - egui::Sense::hover(), - ); - - let painter = ui.painter_at(full_rect); - - // Draw three panels - let panel1_rect = egui::Rect::from_min_size( - full_rect.min, - egui::vec2(panel_width, chart_height + 30.0), - ); - let panel2_rect = egui::Rect::from_min_size( - egui::pos2(full_rect.left() + panel_width + 10.0, full_rect.top()), - egui::vec2(panel_width, chart_height + 30.0), - ); - let panel3_rect = egui::Rect::from_min_size( - egui::pos2( - full_rect.left() + 2.0 * (panel_width + 10.0), - full_rect.top(), - ), - egui::vec2(panel_width, chart_height + 30.0), - ); - - // Render the three panels - Self::render_mini_heat_map( - &painter, - panel1_rect, - &cell_values, - min_value, - max_value, - &t!("histogram.comparison_histogram"), - font_13, - Some(&hit_counts), - min_hits_filter, - ); - - Self::render_mini_heat_map( - &painter, - panel2_rect, - &pasted_values, - pasted_min, - pasted_max, - &t!("histogram.comparison_pasted"), - font_13, - None, - 0, - ); - - let op_symbol = match table_operation { - TableOperation::Add => "+", - TableOperation::Subtract => "−", - TableOperation::Multiply => "×", - TableOperation::Divide => "÷", - }; - Self::render_mini_heat_map( - &painter, - panel3_rect, - &result_values, - result_min, - result_max, - &format!("{} ({})", t!("histogram.comparison_result"), op_symbol), - font_13, - None, - 0, - ); - - // Render legend with comparison info - ui.add_space(8.0); - ui.horizontal(|ui| { - ui.label( - egui::RichText::new(format!( - "{}: {:.1} to {:.1}", - t!("histogram.comparison_histogram"), - min_value, - max_value - )) - .size(font_12) - .color(egui::Color32::WHITE), - ); - ui.add_space(16.0); - ui.label( - egui::RichText::new(format!( - "{}: {:.1} to {:.1}", - t!("histogram.comparison_pasted"), - pasted_min, - pasted_max - )) - .size(font_12) - .color(egui::Color32::WHITE), - ); - ui.add_space(16.0); - ui.label( - egui::RichText::new(format!( - "{}: {:.1} to {:.1}", - t!("histogram.comparison_result"), - result_min, - result_max - )) - .size(font_12) - .color(egui::Color32::WHITE), - ); - }); - - return; - } - } - - // Allocate space for the grid - let available = ui.available_size(); - let chart_size = egui::vec2(available.x, (available.y - LEGEND_HEIGHT).max(200.0)); - let (full_rect, response) = ui.allocate_exact_size(chart_size, egui::Sense::click()); - - // Create inner plot rect with margins - let plot_rect = egui::Rect::from_min_max( - egui::pos2( - full_rect.left() + AXIS_LABEL_MARGIN_LEFT, - full_rect.top() + AXIS_LABEL_MARGIN_TOP, - ), - egui::pos2( - full_rect.right() - AXIS_LABEL_MARGIN_RIGHT, - full_rect.bottom() - AXIS_LABEL_MARGIN_BOTTOM, - ), - ); - - let painter = ui.painter_at(full_rect); - painter.rect_filled(plot_rect, 0.0, egui::Color32::BLACK); - - let cell_width = plot_rect.width() / grid_size as f32; - let cell_height = plot_rect.height() / grid_size as f32; - - // Get selected cell for highlighting - let selected_cell = self.tabs[tab_idx] - .histogram_state - .config - .selected_cell - .clone(); - - // Draw grid cells with values - for y_bin in 0..grid_size { - for x_bin in 0..grid_size { - let cell_x = plot_rect.left() + x_bin as f32 * cell_width; - let cell_y = plot_rect.bottom() - (y_bin + 1) as f32 * cell_height; - let cell_rect = egui::Rect::from_min_size( - egui::pos2(cell_x, cell_y), - egui::vec2(cell_width, cell_height), - ); - - let hits = hit_counts[y_bin][x_bin]; - - // Skip cells below minimum hits threshold (draw grayed out) - if min_hits_filter > 0 && hits < min_hits_filter { - painter.rect_filled(cell_rect, 0.0, egui::Color32::from_rgb(30, 30, 30)); - continue; - } - - if let Some(value) = cell_values[y_bin][x_bin] { - // Normalize to 0-1 for color scaling - let normalized = if mode == HistogramMode::HitCount && max_value > 1.0 { - (value.ln() / max_value.ln()).clamp(0.0, 1.0) - } else { - ((value - min_value) / value_range).clamp(0.0, 1.0) - }; - let color = Self::get_histogram_color(normalized); - - painter.rect_filled(cell_rect, 0.0, color); - - // Draw value text in center of cell (only if cell is large enough) - if cell_width > 25.0 && cell_height > 18.0 { - let text = if mode == HistogramMode::HitCount { - format!("{}", hit_counts[y_bin][x_bin]) - } else { - format!("{:.1}", value) - }; - - // Choose text color for AAA contrast compliance - let text_color = get_aaa_text_color(color); - - let base_font_size = if grid_size <= 16 { - 11.0 - } else if grid_size <= 32 { - 9.0 - } else { - 7.0 - }; - let cell_font_size = self.scaled_font(base_font_size); - - painter.text( - cell_rect.center(), - egui::Align2::CENTER_CENTER, - text, - egui::FontId::proportional(cell_font_size), - text_color, - ); - } - } - } - } - - // Draw grid lines - let grid_color = egui::Color32::from_rgb(60, 60, 60); - for i in 0..=grid_size { - let x = plot_rect.left() + i as f32 * cell_width; - let y = plot_rect.top() + i as f32 * cell_height; - painter.line_segment( - [ - egui::pos2(x, plot_rect.top()), - egui::pos2(x, plot_rect.bottom()), - ], - egui::Stroke::new(0.5, grid_color), - ); - painter.line_segment( - [ - egui::pos2(plot_rect.left(), y), - egui::pos2(plot_rect.right(), y), - ], - egui::Stroke::new(0.5, grid_color), - ); - } - - // Get channel names for axis labels (handles computed channels) - let base_channel_count = file.log.channels.len(); - let x_channel_name = if x_idx < base_channel_count { - file.log.channels[x_idx].name() - } else { - let computed_idx = x_idx - base_channel_count; - self.file_computed_channels - .get(&file_idx) - .and_then(|c| c.get(computed_idx)) - .map(|c| c.template.name.clone()) - .unwrap_or_else(|| "Unknown".to_string()) - }; - let y_channel_name = if y_idx < base_channel_count { - file.log.channels[y_idx].name() - } else { - let computed_idx = y_idx - base_channel_count; - self.file_computed_channels - .get(&file_idx) - .and_then(|c| c.get(computed_idx)) - .map(|c| c.template.name.clone()) - .unwrap_or_else(|| "Unknown".to_string()) - }; - - // Draw axis labels - let text_color = egui::Color32::from_rgb(200, 200, 200); - let axis_title_color = egui::Color32::from_rgb(255, 255, 255); - - // Y axis value labels - for i in 0..=4 { - let t = i as f64 / 4.0; - let value = y_min + t * y_range; - let y_pos = plot_rect.bottom() - t as f32 * plot_rect.height(); - painter.text( - egui::pos2(plot_rect.left() - 8.0, y_pos), - egui::Align2::RIGHT_CENTER, - format!("{:.1}", value), - egui::FontId::proportional(font_10), - text_color, - ); - } - - // Y axis title (wrapped vertically on word boundaries) - let y_title_x = full_rect.left() + 12.0; - let y_title_center_y = plot_rect.center().y; - - // Wrap long names on word boundaries for vertical display - let y_title_display = if y_channel_name.len() > MAX_AXIS_LABEL_LENGTH { - // Try to wrap on spaces, showing up to 2 lines - let words: Vec<&str> = y_channel_name.split_whitespace().collect(); - let mut lines = Vec::new(); - let mut current_line = String::new(); - - for word in words { - if current_line.is_empty() { - current_line = word.to_string(); - } else if current_line.len() + 1 + word.len() <= MAX_AXIS_LABEL_LENGTH { - current_line.push(' '); - current_line.push_str(word); - } else { - lines.push(current_line); - current_line = word.to_string(); - if lines.len() >= 2 { - // Truncate if more than 2 lines - current_line = truncate_label(¤t_line, MAX_AXIS_LABEL_LENGTH); - break; - } - } - } - if !current_line.is_empty() { - lines.push(current_line); - } - lines.join("\n") - } else { - y_channel_name.clone() - }; - - // Calculate text height for multi-line labels - let line_count = y_title_display.lines().count() as f32; - let line_height = font_13 * 1.2; - let total_height = line_count * line_height; - let y_start = y_title_center_y - total_height / 2.0 + line_height / 2.0; - - // Draw each line centered - let mut combined_rect: Option = None; - for (i, line) in y_title_display.lines().enumerate() { - let line_y = y_start + i as f32 * line_height; - let rect = painter.text( - egui::pos2(y_title_x, line_y), - egui::Align2::CENTER_CENTER, - line, - egui::FontId::proportional(font_13), - axis_title_color, - ); - combined_rect = Some(combined_rect.map_or(rect, |r| r.union(rect))); - } - - // Show full name as tooltip if wrapped or truncated - if y_title_display != y_channel_name || y_title_display.contains('\n') { - if let Some(rect) = combined_rect { - let y_title_response = - ui.interact(rect, ui.id().with("y_title_tooltip"), egui::Sense::hover()); - y_title_response.on_hover_text(&y_channel_name); - } - } - - // X axis value labels - for i in 0..=4 { - let t = i as f64 / 4.0; - let value = x_min + t * x_range; - let x_pos = plot_rect.left() + t as f32 * plot_rect.width(); - painter.text( - egui::pos2(x_pos, plot_rect.bottom() + 5.0), - egui::Align2::CENTER_TOP, - format!("{:.0}", value), - egui::FontId::proportional(font_10), - text_color, - ); - } - - // X axis title (truncate long names for consistency) - let x_title_x = plot_rect.center().x; - let x_title_y = full_rect.bottom() - 8.0; - let x_title_truncated = truncate_label(&x_channel_name, MAX_AXIS_LABEL_LENGTH); - let x_title_rect = painter.text( - egui::pos2(x_title_x, x_title_y), - egui::Align2::CENTER_CENTER, - &x_title_truncated, - egui::FontId::proportional(font_13), - axis_title_color, - ); - // Show full name as tooltip if truncated - if x_title_truncated != x_channel_name { - let x_title_response = ui.interact( - x_title_rect, - ui.id().with("x_title_tooltip"), - egui::Sense::hover(), - ); - x_title_response.on_hover_text(&x_channel_name); - } - - // Draw selected cell highlight - if let Some(ref sel) = selected_cell { - if sel.x_bin < grid_size && sel.y_bin < grid_size { - let sel_x = plot_rect.left() + sel.x_bin as f32 * cell_width; - let sel_y = plot_rect.bottom() - (sel.y_bin + 1) as f32 * cell_height; - let sel_rect = egui::Rect::from_min_size( - egui::pos2(sel_x, sel_y), - egui::vec2(cell_width, cell_height), - ); - let stroke = egui::Stroke::new(3.0, SELECTED_CELL_COLOR); - painter.line_segment([sel_rect.left_top(), sel_rect.right_top()], stroke); - painter.line_segment([sel_rect.left_bottom(), sel_rect.right_bottom()], stroke); - painter.line_segment([sel_rect.left_top(), sel_rect.left_bottom()], stroke); - painter.line_segment([sel_rect.right_top(), sel_rect.right_bottom()], stroke); - } - } - - // Draw current position indicator (cursor time) - if let Some(cursor_record) = self.get_cursor_record() { - if cursor_record < x_data.len() { - let cursor_x = x_data[cursor_record]; - let cursor_y = y_data[cursor_record]; - - let rel_x = ((cursor_x - x_min) / x_range) as f32; - let rel_y = ((cursor_y - y_min) / y_range) as f32; - - if (0.0..=1.0).contains(&rel_x) && (0.0..=1.0).contains(&rel_y) { - let pos_x = plot_rect.left() + rel_x * plot_rect.width(); - let pos_y = plot_rect.bottom() - rel_y * plot_rect.height(); - - // Draw grey crosshairs tracking the cursor position - painter.line_segment( - [ - egui::pos2(pos_x, plot_rect.top()), - egui::pos2(pos_x, plot_rect.bottom()), - ], - egui::Stroke::new(1.0, CURSOR_CROSSHAIR_COLOR), - ); - painter.line_segment( - [ - egui::pos2(plot_rect.left(), pos_y), - egui::pos2(plot_rect.right(), pos_y), - ], - egui::Stroke::new(1.0, CURSOR_CROSSHAIR_COLOR), - ); - - // Highlight the cell containing the cursor - let cell_x_bin = calculate_bin(rel_x, grid_size); - let cell_y_bin = calculate_bin(rel_y, grid_size); - - let highlight_x = plot_rect.left() + cell_x_bin as f32 * cell_width; - let highlight_y = plot_rect.bottom() - (cell_y_bin + 1) as f32 * cell_height; - - let highlight_rect = egui::Rect::from_min_size( - egui::pos2(highlight_x, highlight_y), - egui::vec2(cell_width, cell_height), - ); - let stroke = egui::Stroke::new(2.0, CELL_HIGHLIGHT_COLOR); - painter.line_segment( - [highlight_rect.left_top(), highlight_rect.right_top()], - stroke, - ); - painter.line_segment( - [highlight_rect.left_bottom(), highlight_rect.right_bottom()], - stroke, - ); - painter.line_segment( - [highlight_rect.left_top(), highlight_rect.left_bottom()], - stroke, - ); - painter.line_segment( - [highlight_rect.right_top(), highlight_rect.right_bottom()], - stroke, - ); - - // Draw circle at exact position - painter.circle_filled(egui::pos2(pos_x, pos_y), 6.0, CURSOR_COLOR); - painter.circle_stroke( - egui::pos2(pos_x, pos_y), - 6.0, - egui::Stroke::new(2.0, egui::Color32::WHITE), - ); - } - } - } - - // Handle hover tooltip - if let Some(pos) = response.hover_pos() { - if plot_rect.contains(pos) { - let rel_x = (pos.x - plot_rect.left()) / plot_rect.width(); - let rel_y = 1.0 - (pos.y - plot_rect.top()) / plot_rect.height(); - - if (0.0..=1.0).contains(&rel_x) && (0.0..=1.0).contains(&rel_y) { - let x_val = x_min + rel_x as f64 * x_range; - let y_val = y_min + rel_y as f64 * y_range; - - let x_bin = calculate_bin(rel_x, grid_size); - let y_bin = calculate_bin(rel_y, grid_size); - - let hits = hit_counts[y_bin][x_bin]; - let cell_value = cell_values[y_bin][x_bin]; - - // Truncate channel names for tooltip display - let x_label = truncate_label(&x_channel_name, 10); - let y_label = truncate_label(&y_channel_name, 10); - - let tooltip_text = match mode { - HistogramMode::HitCount => { - format!( - "{}: {:.1}\n{}: {:.1}\nHits: {}", - x_label, x_val, y_label, y_val, hits - ) - } - HistogramMode::AverageZ => { - let avg = cell_value - .map(|v| format!("{:.2}", v)) - .unwrap_or("-".to_string()); - format!( - "{}: {:.1}\n{}: {:.1}\nAvg: {}\nHits: {}", - x_label, x_val, y_label, y_val, avg, hits - ) - } - }; - - // Draw hover crosshairs - let hover_x = plot_rect.left() + rel_x * plot_rect.width(); - let hover_y = plot_rect.top() + (1.0 - rel_y) * plot_rect.height(); - let crosshair_color = egui::Color32::from_rgb(255, 255, 0); - - painter.line_segment( - [ - egui::pos2(hover_x, plot_rect.top()), - egui::pos2(hover_x, plot_rect.bottom()), - ], - egui::Stroke::new(1.0, crosshair_color), - ); - painter.line_segment( - [ - egui::pos2(plot_rect.left(), hover_y), - egui::pos2(plot_rect.right(), hover_y), - ], - egui::Stroke::new(1.0, crosshair_color), - ); - - painter.text( - egui::pos2(plot_rect.right() - 10.0, plot_rect.top() + 15.0), - egui::Align2::RIGHT_TOP, - tooltip_text, - egui::FontId::proportional(font_12), - egui::Color32::WHITE, - ); - } - } - } - - // Handle click to select cell - if response.clicked() { - if let Some(pos) = response.interact_pointer_pos() { - if plot_rect.contains(pos) { - let rel_x = (pos.x - plot_rect.left()) / plot_rect.width(); - let rel_y = 1.0 - (pos.y - plot_rect.top()) / plot_rect.height(); - - if (0.0..=1.0).contains(&rel_x) && (0.0..=1.0).contains(&rel_y) { - let x_bin = calculate_bin(rel_x, grid_size); - let y_bin = calculate_bin(rel_y, grid_size); - - let hits = hit_counts[y_bin][x_bin]; - - // Calculate cell value ranges - let bin_width_x = x_range / grid_size as f64; - let bin_width_y = y_range / grid_size as f64; - let cell_x_min = x_min + x_bin as f64 * bin_width_x; - let cell_x_max = cell_x_min + bin_width_x; - let cell_y_min = y_min + y_bin as f64 * bin_width_y; - let cell_y_max = cell_y_min + bin_width_y; - - // Calculate statistics - let mean = if hits > 0 { - z_sums[y_bin][x_bin] / hits as f64 - } else { - 0.0 - }; - - let variance = if hits > 1 { - let n = hits as f64; - (z_sum_sq[y_bin][x_bin] - (z_sums[y_bin][x_bin].powi(2) / n)) - / (n - 1.0) - } else { - 0.0 - }; - - let std_dev = variance.sqrt(); - let cell_weight = z_sums[y_bin][x_bin]; - - let minimum = if hits > 0 && z_mins[y_bin][x_bin] != f64::MAX { - z_mins[y_bin][x_bin] - } else { - 0.0 - }; - - let maximum = if hits > 0 && z_maxs[y_bin][x_bin] != f64::MIN { - z_maxs[y_bin][x_bin] - } else { - 0.0 - }; - - let selected = SelectedHistogramCell { - x_bin, - y_bin, - x_range: (cell_x_min, cell_x_max), - y_range: (cell_y_min, cell_y_max), - hit_count: hits, - cell_weight, - variance, - std_dev, - minimum, - mean, - maximum, - }; - - self.tabs[tab_idx].histogram_state.config.selected_cell = Some(selected); - } - } - } - } - - // Render legend with selected cell info - ui.add_space(8.0); - let selected_cell = self.tabs[tab_idx] - .histogram_state - .config - .selected_cell - .as_ref(); - self.render_histogram_legend(ui, min_value, max_value, mode, selected_cell); - } - - /// Get a color from the heat map gradient based on normalized value (0-1) - fn get_histogram_color(normalized: f64) -> egui::Color32 { - let t = normalized.clamp(0.0, 1.0); - let scaled = t * (HEAT_COLORS.len() - 1) as f64; - let idx = scaled.floor() as usize; - let frac = scaled - idx as f64; - - if idx >= HEAT_COLORS.len() - 1 { - let c = HEAT_COLORS[HEAT_COLORS.len() - 1]; - return egui::Color32::from_rgb(c[0], c[1], c[2]); - } - - let c1 = HEAT_COLORS[idx]; - let c2 = HEAT_COLORS[idx + 1]; - - let r = (c1[0] as f64 + (c2[0] as f64 - c1[0] as f64) * frac) as u8; - let g = (c1[1] as f64 + (c2[1] as f64 - c1[1] as f64) * frac) as u8; - let b = (c1[2] as f64 + (c2[2] as f64 - c1[2] as f64) * frac) as u8; - - egui::Color32::from_rgb(r, g, b) - } - - /// Render the legend with color scale and cell statistics - fn render_histogram_legend( - &self, - ui: &mut egui::Ui, - min_value: f64, - max_value: f64, - mode: HistogramMode, - selected_cell: Option<&SelectedHistogramCell>, - ) { - let font_12 = self.scaled_font(12.0); - let font_13 = self.scaled_font(13.0); - - ui.horizontal(|ui| { - ui.add_space(4.0); - - // Color scale legend - egui::Frame::NONE - .fill(egui::Color32::from_rgba_unmultiplied(30, 30, 30, 220)) - .corner_radius(4) - .inner_margin(8.0) - .show(ui, |ui| { - ui.horizontal(|ui| { - let label = match mode { - HistogramMode::HitCount => "Hits:", - HistogramMode::AverageZ => "Value:", - }; - ui.label( - egui::RichText::new(label) - .size(font_13) - .color(egui::Color32::WHITE), - ); - - // Color gradient bar - let (rect, _) = - ui.allocate_exact_size(egui::vec2(120.0, 18.0), egui::Sense::hover()); - - let painter = ui.painter(); - let steps = 30; - let step_width = rect.width() / steps as f32; - - for i in 0..steps { - let t = i as f64 / steps as f64; - let color = Self::get_histogram_color(t); - let x = rect.left() + i as f32 * step_width; - painter.rect_filled( - egui::Rect::from_min_size( - egui::pos2(x, rect.top()), - egui::vec2(step_width + 1.0, rect.height()), - ), - 0.0, - color, - ); - } - - ui.add_space(6.0); - let range_text = if mode == HistogramMode::HitCount { - format!("0-{:.0}", max_value) - } else { - format!("{:.1}-{:.1}", min_value, max_value) - }; - ui.label( - egui::RichText::new(range_text) - .size(font_13) - .color(egui::Color32::WHITE), - ); - }); - }); - - ui.add_space(16.0); - - // Cell statistics panel (only shown when a cell is selected) - if let Some(cell) = selected_cell { - egui::Frame::NONE - .fill(egui::Color32::from_rgba_unmultiplied(30, 30, 30, 220)) - .corner_radius(4) - .inner_margin(8.0) - .stroke(egui::Stroke::new(1.0, SELECTED_CELL_COLOR)) - .show(ui, |ui| { - ui.horizontal(|ui| { - // Cell identifier - ui.label( - egui::RichText::new(format!( - "Cell [{}, {}]", - cell.x_bin, cell.y_bin - )) - .size(font_13) - .color(SELECTED_CELL_COLOR), - ); - ui.add_space(12.0); - - // Key statistics inline - let stat_color = egui::Color32::from_rgb(180, 180, 180); - let value_color = egui::Color32::WHITE; - - ui.label(egui::RichText::new("Hits:").size(font_12).color(stat_color)); - ui.label( - egui::RichText::new(format!("{}", cell.hit_count)) - .size(font_12) - .color(value_color), - ); - - // Only show Z-related statistics in AverageZ mode - if mode == HistogramMode::AverageZ { - ui.add_space(8.0); - - ui.label( - egui::RichText::new("Mean:").size(font_12).color(stat_color), - ); - ui.label( - egui::RichText::new(format!("{:.2}", cell.mean)) - .size(font_12) - .color(value_color), - ); - ui.add_space(8.0); - - ui.label( - egui::RichText::new("Min:").size(font_12).color(stat_color), - ); - ui.label( - egui::RichText::new(format!("{:.2}", cell.minimum)) - .size(font_12) - .color(value_color), - ); - ui.add_space(8.0); - - ui.label( - egui::RichText::new("Max:").size(font_12).color(stat_color), - ); - ui.label( - egui::RichText::new(format!("{:.2}", cell.maximum)) - .size(font_12) - .color(value_color), - ); - ui.add_space(8.0); - - ui.label(egui::RichText::new("σ:").size(font_12).color(stat_color)); - ui.label( - egui::RichText::new(format!("{:.2}", cell.std_dev)) - .size(font_12) - .color(value_color), - ); - } - }); - }); - } else { - // Hint when no cell is selected - egui::Frame::NONE - .fill(egui::Color32::from_rgba_unmultiplied(30, 30, 30, 220)) - .corner_radius(4) - .inner_margin(8.0) - .show(ui, |ui| { - ui.label( - egui::RichText::new("Click a cell to view statistics") - .size(font_12) - .italics() - .color(egui::Color32::GRAY), - ); - }); - } - }); - } - - /// Render histogram cell statistics in sidebar - pub fn render_histogram_stats(&self, ui: &mut egui::Ui) { - let Some(tab_idx) = self.active_tab else { - return; - }; - - let config = &self.tabs[tab_idx].histogram_state.config; - let selected = &config.selected_cell; - let mode = config.mode; - - // Pre-compute scaled font sizes - let font_12 = self.scaled_font(12.0); - let font_13 = self.scaled_font(13.0); - let font_14 = self.scaled_font(14.0); - - ui.add_space(10.0); - ui.separator(); - ui.add_space(5.0); - - ui.label( - egui::RichText::new("Cell Statistics") - .size(font_14) - .strong() - .color(egui::Color32::WHITE), - ); - - ui.add_space(5.0); - - if let Some(cell) = selected { - egui::Frame::NONE - .fill(egui::Color32::from_rgba_unmultiplied(40, 40, 40, 200)) - .corner_radius(4) - .inner_margin(8.0) - .stroke(egui::Stroke::new(1.0, SELECTED_CELL_COLOR)) - .show(ui, |ui| { - ui.horizontal(|ui| { - ui.label( - egui::RichText::new(format!("Cell [{}, {}]", cell.x_bin, cell.y_bin)) - .size(font_13) - .color(SELECTED_CELL_COLOR), - ); - }); - - ui.add_space(4.0); - - // Build stats list - always show range and hit count - let mut stats: Vec<(&str, String)> = vec![ - ( - "X Range", - format!("{:.2} - {:.2}", cell.x_range.0, cell.x_range.1), - ), - ( - "Y Range", - format!("{:.2} - {:.2}", cell.y_range.0, cell.y_range.1), - ), - ("Hit Count", format!("{}", cell.hit_count)), - ]; - - // Only show Z-related stats in AverageZ mode - if mode == HistogramMode::AverageZ { - stats.extend([ - ("Cell Weight", format!("{:.4}", cell.cell_weight)), - ("Mean", format!("{:.4}", cell.mean)), - ("Minimum", format!("{:.4}", cell.minimum)), - ("Maximum", format!("{:.4}", cell.maximum)), - ("Variance", format!("{:.4}", cell.variance)), - ("Std Dev", format!("{:.4}", cell.std_dev)), - ]); - } - - for (label, value) in stats { - ui.horizontal(|ui| { - ui.label( - egui::RichText::new(format!("{}:", label)) - .size(font_12) - .color(egui::Color32::from_rgb(180, 180, 180)), - ); - ui.with_layout( - egui::Layout::right_to_left(egui::Align::Center), - |ui| { - ui.label( - egui::RichText::new(&value) - .size(font_12) - .color(egui::Color32::WHITE), - ); - }, - ); - }); - } - - ui.add_space(4.0); - - if ui.small_button("Clear Selection").clicked() { - // We can't mutate here, set a flag instead - } - }); - } else { - ui.label( - egui::RichText::new("Click a cell to view statistics") - .size(font_12) - .italics() - .color(egui::Color32::GRAY), - ); - } - } - - /// Resample a pasted table to a target grid size using bilinear interpolation - #[allow(clippy::needless_range_loop)] - fn resample_table(table: &PastedTable, target_size: usize) -> Vec> { - let src_rows = table.data.len(); - let src_cols = if src_rows > 0 { table.data[0].len() } else { 0 }; - - if src_rows == 0 || src_cols == 0 { - return vec![vec![0.0; target_size]; target_size]; - } - - // If dimensions match, just reverse Y (pasted table has 0 = top, we need 0 = bottom) - if src_rows == target_size && src_cols == target_size { - let mut result = Vec::with_capacity(target_size); - for y in 0..target_size { - result.push(table.data[src_rows - 1 - y].clone()); - } - return result; - } - - let mut result = vec![vec![0.0; target_size]; target_size]; - - for target_y in 0..target_size { - for target_x in 0..target_size { - // Map target coordinates to source coordinates - let src_x = - target_x as f64 * (src_cols - 1) as f64 / (target_size - 1).max(1) as f64; - let src_y = - target_y as f64 * (src_rows - 1) as f64 / (target_size - 1).max(1) as f64; - - // Bilinear interpolation - let x0 = src_x.floor() as usize; - let y0 = src_y.floor() as usize; - let x1 = (x0 + 1).min(src_cols - 1); - let y1 = (y0 + 1).min(src_rows - 1); - - let fx = src_x - x0 as f64; - let fy = src_y - y0 as f64; - - // Note: y index in pasted table is reversed (0 = top, but we want 0 = bottom) - let src_y0 = src_rows - 1 - y0; - let src_y1 = src_rows - 1 - y1; - - let v00 = table - .data - .get(src_y0) - .and_then(|r| r.get(x0)) - .copied() - .unwrap_or(0.0); - let v10 = table - .data - .get(src_y0) - .and_then(|r| r.get(x1)) - .copied() - .unwrap_or(0.0); - let v01 = table - .data - .get(src_y1) - .and_then(|r| r.get(x0)) - .copied() - .unwrap_or(0.0); - let v11 = table - .data - .get(src_y1) - .and_then(|r| r.get(x1)) - .copied() - .unwrap_or(0.0); - - let value = v00 * (1.0 - fx) * (1.0 - fy) - + v10 * fx * (1.0 - fy) - + v01 * (1.0 - fx) * fy - + v11 * fx * fy; - - result[target_y][target_x] = value; - } - } - - result - } - - /// Apply an operation between histogram values and pasted table values - /// For cells where histogram has no data but pasted does, use 0 for histogram value - fn apply_table_operation( - hist_values: &[Vec>], - pasted_values: &[Vec], - operation: TableOperation, - ) -> Vec>> { - let rows = hist_values.len(); - let cols = if rows > 0 { hist_values[0].len() } else { 0 }; - - let mut result = vec![vec![None; cols]; rows]; - - for y in 0..rows { - for x in 0..cols { - let pasted_val = pasted_values - .get(y) - .and_then(|r| r.get(x)) - .copied() - .unwrap_or(0.0); - - // Only skip if pasted value is effectively zero (no data there either) - if pasted_val.abs() < f64::EPSILON && hist_values[y][x].is_none() { - continue; - } - - let hist_val = hist_values[y][x].unwrap_or(0.0); - let res_val = match operation { - TableOperation::Add => hist_val + pasted_val, - TableOperation::Subtract => hist_val - pasted_val, - TableOperation::Multiply => hist_val * pasted_val, - TableOperation::Divide => { - if pasted_val.abs() > f64::EPSILON { - hist_val / pasted_val - } else { - 0.0 - } - } - }; - result[y][x] = Some(res_val); - } - } - - result - } - - /// Render a mini heat map panel (used in comparison view) - /// hit_counts is optional - when provided, cells below min_hits_filter are grayed out - #[allow(clippy::too_many_arguments)] - fn render_mini_heat_map( - painter: &egui::Painter, - rect: egui::Rect, - values: &[Vec>], - min_value: f64, - max_value: f64, - title: &str, - font_size: f32, - hit_counts: Option<&[Vec]>, - min_hits_filter: u32, - ) { - let grid_size = values.len(); - if grid_size == 0 { - return; - } - - // Draw title - painter.text( - egui::pos2(rect.center().x, rect.top() + 15.0), - egui::Align2::CENTER_CENTER, - title, - egui::FontId::proportional(font_size), - egui::Color32::WHITE, - ); - - // Calculate plot area - let plot_rect = egui::Rect::from_min_max( - egui::pos2(rect.left() + 10.0, rect.top() + 30.0), - egui::pos2(rect.right() - 10.0, rect.bottom() - 10.0), - ); - - painter.rect_filled(plot_rect, 0.0, egui::Color32::BLACK); - - let cell_width = plot_rect.width() / grid_size as f32; - let cell_height = plot_rect.height() / grid_size as f32; - - let value_range = if (max_value - min_value).abs() < f64::EPSILON { - 1.0 - } else { - max_value - min_value - }; - - for (y_bin, row) in values.iter().enumerate().take(grid_size) { - for (x_bin, cell_value) in row.iter().enumerate().take(grid_size) { - let cell_x = plot_rect.left() + x_bin as f32 * cell_width; - let cell_y = plot_rect.bottom() - (y_bin + 1) as f32 * cell_height; - let cell_rect = egui::Rect::from_min_size( - egui::pos2(cell_x, cell_y), - egui::vec2(cell_width, cell_height), - ); - - // Check min hits filter if hit_counts provided - if let Some(hits) = hit_counts { - if min_hits_filter > 0 { - let cell_hits = hits - .get(y_bin) - .and_then(|r| r.get(x_bin)) - .copied() - .unwrap_or(0); - if cell_hits < min_hits_filter { - painter.rect_filled( - cell_rect, - 0.0, - egui::Color32::from_rgb(30, 30, 30), - ); - continue; - } - } - } - - if let Some(value) = cell_value { - let normalized = ((value - min_value) / value_range).clamp(0.0, 1.0); - let color = Self::get_histogram_color(normalized); - painter.rect_filled(cell_rect, 0.0, color); - - // Draw value text if cell is large enough - if cell_width > 20.0 && cell_height > 14.0 { - let text = format!("{:.1}", value); - let text_color = get_aaa_text_color(color); - let text_size = if grid_size <= 16 { 9.0 } else { 7.0 }; - painter.text( - cell_rect.center(), - egui::Align2::CENTER_CENTER, - text, - egui::FontId::proportional(text_size), - text_color, - ); - } - } else { - painter.rect_filled(cell_rect, 0.0, egui::Color32::from_rgb(30, 30, 30)); - } - } - } - - // Draw grid lines - let grid_color = egui::Color32::from_rgb(60, 60, 60); - for i in 0..=grid_size { - let x = plot_rect.left() + i as f32 * cell_width; - let y = plot_rect.top() + i as f32 * cell_height; - painter.line_segment( - [ - egui::pos2(x, plot_rect.top()), - egui::pos2(x, plot_rect.bottom()), - ], - egui::Stroke::new(0.5, grid_color), - ); - painter.line_segment( - [ - egui::pos2(plot_rect.left(), y), - egui::pos2(plot_rect.right(), y), - ], - egui::Stroke::new(0.5, grid_color), - ); - } - } - - /// Copy histogram data to clipboard as TSV (tab-separated values) - fn copy_histogram_to_clipboard(&self, tab_idx: usize) { - let config = &self.tabs[tab_idx].histogram_state.config; - let file_idx = self.tabs[tab_idx].file_index; - - if file_idx >= self.files.len() { - return; - } - - let (x_idx, y_idx) = match (config.x_channel, config.y_channel) { - (Some(x), Some(y)) => (x, y), - _ => return, - }; - - let grid_size = config.effective_grid_size(); - let mode = config.mode; - let z_idx = config.z_channel; - - let x_data = self.get_channel_data(file_idx, x_idx); - let y_data = self.get_channel_data(file_idx, y_idx); - let z_data = z_idx.map(|z| self.get_channel_data(file_idx, z)); - - if x_data.is_empty() || y_data.is_empty() { - return; - } - - // Calculate data bounds - let (x_min, x_max) = match config.custom_x_range { - Some((min, max)) if max > min => (min, max), - _ => { - let min = x_data.iter().cloned().fold(f64::MAX, f64::min); - let max = x_data.iter().cloned().fold(f64::MIN, f64::max); - (min, max) - } - }; - let (y_min, y_max) = match config.custom_y_range { - Some((min, max)) if max > min => (min, max), - _ => { - let min = y_data.iter().cloned().fold(f64::MAX, f64::min); - let max = y_data.iter().cloned().fold(f64::MIN, f64::max); - (min, max) - } - }; - - let x_range = if (x_max - x_min).abs() < f64::EPSILON { - 1.0 - } else { - x_max - x_min - }; - let y_range = if (y_max - y_min).abs() < f64::EPSILON { - 1.0 - } else { - y_max - y_min - }; - - // Build histogram grid - let mut hit_counts = vec![vec![0u32; grid_size]; grid_size]; - let mut z_sums = vec![vec![0.0f64; grid_size]; grid_size]; - - for i in 0..x_data.len() { - let x_bin = calculate_data_bin(x_data[i], x_min, x_range, grid_size); - let y_bin = calculate_data_bin(y_data[i], y_min, y_range, grid_size); - hit_counts[y_bin][x_bin] += 1; - if let Some(ref z) = z_data { - z_sums[y_bin][x_bin] += z[i]; - } - } - - // Generate TSV string - let mut tsv = String::new(); - - // Header row: X breakpoints - tsv.push('\t'); // Empty cell for Y label column - for x_bin in 0..grid_size { - let x_val = x_min + (x_bin as f64 + 0.5) * (x_range / grid_size as f64); - tsv.push_str(&format!("{:.1}", x_val)); - if x_bin < grid_size - 1 { - tsv.push('\t'); - } - } - tsv.push('\n'); - - // Data rows (from top to bottom, so reverse Y order) - for y_bin in (0..grid_size).rev() { - // Y label - let y_val = y_min + (y_bin as f64 + 0.5) * (y_range / grid_size as f64); - tsv.push_str(&format!("{:.1}\t", y_val)); - - for x_bin in 0..grid_size { - let value = match mode { - HistogramMode::HitCount => hit_counts[y_bin][x_bin] as f64, - HistogramMode::AverageZ => { - let hits = hit_counts[y_bin][x_bin]; - if hits > 0 { - z_sums[y_bin][x_bin] / hits as f64 - } else { - 0.0 - } - } - }; - tsv.push_str(&format!("{:.2}", value)); - if x_bin < grid_size - 1 { - tsv.push('\t'); - } - } - tsv.push('\n'); - } - - // Copy to clipboard - if let Ok(mut clipboard) = arboard::Clipboard::new() { - let _ = clipboard.set_text(tsv); - } - } - - /// Copy the result of histogram + pasted table operation to clipboard - fn copy_result_to_clipboard(&self, tab_idx: usize) { - let config = &self.tabs[tab_idx].histogram_state.config; - let file_idx = self.tabs[tab_idx].file_index; - - if file_idx >= self.files.len() { - return; - } - - // Need a pasted table to have a result - let pasted_table = match &config.pasted_table { - Some(table) => table, - None => return, - }; - - let (x_idx, y_idx) = match (config.x_channel, config.y_channel) { - (Some(x), Some(y)) => (x, y), - _ => return, - }; - - let grid_size = config.effective_grid_size(); - let mode = config.mode; - let z_idx = config.z_channel; - let table_operation = config.table_operation; - - let x_data = self.get_channel_data(file_idx, x_idx); - let y_data = self.get_channel_data(file_idx, y_idx); - let z_data = z_idx.map(|z| self.get_channel_data(file_idx, z)); - - if x_data.is_empty() || y_data.is_empty() { - return; - } - - // Calculate data bounds - let (x_min, x_max) = match config.custom_x_range { - Some((min, max)) if max > min => (min, max), - _ => { - let min = x_data.iter().cloned().fold(f64::MAX, f64::min); - let max = x_data.iter().cloned().fold(f64::MIN, f64::max); - (min, max) - } - }; - let (y_min, y_max) = match config.custom_y_range { - Some((min, max)) if max > min => (min, max), - _ => { - let min = y_data.iter().cloned().fold(f64::MAX, f64::min); - let max = y_data.iter().cloned().fold(f64::MIN, f64::max); - (min, max) - } - }; - - let x_range = if (x_max - x_min).abs() < f64::EPSILON { - 1.0 - } else { - x_max - x_min - }; - let y_range = if (y_max - y_min).abs() < f64::EPSILON { - 1.0 - } else { - y_max - y_min - }; - - // Build histogram grid - let mut hit_counts = vec![vec![0u32; grid_size]; grid_size]; - let mut z_sums = vec![vec![0.0f64; grid_size]; grid_size]; - - for i in 0..x_data.len() { - let x_bin = calculate_data_bin(x_data[i], x_min, x_range, grid_size); - let y_bin = calculate_data_bin(y_data[i], y_min, y_range, grid_size); - hit_counts[y_bin][x_bin] += 1; - if let Some(ref z) = z_data { - z_sums[y_bin][x_bin] += z[i]; - } - } - - // Build histogram values - let hist_values: Vec>> = (0..grid_size) - .map(|y_bin| { - (0..grid_size) - .map(|x_bin| { - let hits = hit_counts[y_bin][x_bin]; - if hits > 0 { - Some(match mode { - HistogramMode::HitCount => hits as f64, - HistogramMode::AverageZ => z_sums[y_bin][x_bin] / hits as f64, - }) - } else { - None - } - }) - .collect() - }) - .collect(); - - // Resample pasted table and apply operation - let resampled = Self::resample_table(pasted_table, grid_size); - let result_values = Self::apply_table_operation(&hist_values, &resampled, table_operation); - - // Generate TSV string with pasted table's breakpoints - let mut tsv = String::new(); - - // Header row: X breakpoints (use pasted table's breakpoints if they match grid size) - tsv.push('\t'); // Empty cell for Y label column - for x_bin in 0..grid_size { - let x_val = if !pasted_table.x_breakpoints.is_empty() - && pasted_table.x_breakpoints.len() == grid_size - { - pasted_table.x_breakpoints[x_bin] - } else { - x_min + (x_bin as f64 + 0.5) * (x_range / grid_size as f64) - }; - tsv.push_str(&format!("{:.1}", x_val)); - if x_bin < grid_size - 1 { - tsv.push('\t'); - } - } - tsv.push('\n'); - - // Data rows (from top to bottom, so reverse Y order) - for y_bin in (0..grid_size).rev() { - // Y label (use pasted table's breakpoints if they match grid size) - let y_val = if !pasted_table.y_breakpoints.is_empty() - && pasted_table.y_breakpoints.len() == grid_size - { - pasted_table.y_breakpoints[grid_size - 1 - y_bin] - } else { - y_min + (y_bin as f64 + 0.5) * (y_range / grid_size as f64) - }; - tsv.push_str(&format!("{:.1}\t", y_val)); - - for (x_bin, val) in result_values[y_bin].iter().enumerate().take(grid_size) { - let value = val.unwrap_or(0.0); - tsv.push_str(&format!("{:.2}", value)); - if x_bin < grid_size - 1 { - tsv.push('\t'); - } - } - tsv.push('\n'); - } - - // Copy to clipboard - if let Ok(mut clipboard) = arboard::Clipboard::new() { - let _ = clipboard.set_text(tsv); - } - } - - /// Paste table data from clipboard for comparison operations - fn paste_table_from_clipboard(&mut self, tab_idx: usize) { - let clipboard_text = match arboard::Clipboard::new() { - Ok(mut clipboard) => match clipboard.get_text() { - Ok(text) => text, - Err(_) => return, - }, - Err(_) => return, - }; - - // Parse TSV - let lines: Vec<&str> = clipboard_text.lines().collect(); - if lines.is_empty() { - return; - } - - // Helper to parse numbers that may have comma thousands separators (e.g., "1,234.56") - let parse_number = |s: &str| -> Option { - let cleaned = s.trim().replace(',', ""); - cleaned.parse::().ok() - }; - - // Try to parse first row as X breakpoints - let first_row: Vec<&str> = lines[0].split('\t').collect(); - let x_start = if first_row - .first() - .map(|s| s.trim().is_empty()) - .unwrap_or(true) - { - 1 - } else { - 0 - }; - let x_breakpoints: Vec = first_row[x_start..] - .iter() - .filter_map(|s| parse_number(s)) - .collect(); - - let mut y_breakpoints = Vec::new(); - let mut data = Vec::new(); - - for line in &lines[1..] { - let cells: Vec<&str> = line.split('\t').collect(); - if cells.is_empty() { - continue; - } - - // First cell is Y breakpoint - if let Some(y_val) = parse_number(cells[0]) { - y_breakpoints.push(y_val); - } - - // Remaining cells are data - let row: Vec = cells[1..].iter().filter_map(|s| parse_number(s)).collect(); - if !row.is_empty() { - data.push(row); - } - } - - if data.is_empty() { - return; - } - - let original_rows = data.len(); - let original_cols = data.first().map(|r| r.len()).unwrap_or(0); - - let pasted_table = PastedTable { - data, - x_breakpoints, - y_breakpoints, - original_rows, - original_cols, - is_resampled: false, - }; - - self.tabs[tab_idx].histogram_state.config.pasted_table = Some(pasted_table); - self.tabs[tab_idx] - .histogram_state - .config - .show_comparison_view = true; - } -} From 2a946058bccaaf4a1fef26660045f7a87f088665 Mon Sep 17 00:00:00 2001 From: Cole Gentry Date: Thu, 16 Jul 2026 10:58:06 -0400 Subject: [PATCH 02/18] chore(deps): refresh Cargo.lock with semver-compatible updates --- Cargo.lock | 1546 +++++++++++++++++++++++++--------------------------- 1 file changed, 739 insertions(+), 807 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c348a0c..4b2fc09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "accesskit" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5351dcebb14b579ccab05f288596b2ae097005be7ee50a7c3d4ca9d0d5a66f6a" +checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320" dependencies = [ "enumn", "serde", @@ -26,8 +26,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", - "cipher", - "cpufeatures", + "cipher 0.4.4", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" +dependencies = [ + "cipher 0.5.2", + "cpubits", + "cpufeatures 0.3.0", ] [[package]] @@ -67,9 +78,9 @@ checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" [[package]] name = "alloc-stdlib" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" dependencies = [ "alloc-no-stdlib", ] @@ -80,7 +91,7 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ba21c7f883cc76d76a41910815b1b212e1ca7870af2bd551565282140660ca5" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "bitreader", "brotli-decompressor", "byteorder", @@ -105,23 +116,21 @@ dependencies = [ [[package]] name = "android-activity" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" dependencies = [ "android-properties", - "bitflags 2.11.0", + "bitflags 2.13.1", "cc", - "cesu8", "jni", - "jni-sys", "libc", "log", "ndk", "ndk-context", "ndk-sys", "num_enum", - "thiserror 1.0.69", + "thiserror 2.0.18", ] [[package]] @@ -141,9 +150,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.101" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "arboard" @@ -167,18 +176,18 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.8.2" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" dependencies = [ "rustversion", ] [[package]] name = "arrayvec" -version = "0.7.6" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" [[package]] name = "as-raw-xcb-connection" @@ -194,7 +203,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.119", ] [[package]] @@ -205,15 +214,15 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "axum" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core", "bytes", @@ -224,7 +233,7 @@ dependencies = [ "http-body-util", "hyper", "hyper-util", - "itoa 1.0.17", + "itoa 1.0.18", "matchit", "memchr", "mime", @@ -320,9 +329,9 @@ checksum = "8259ebd5ee48a37fd8931116405456fd67efbb5435b4789e45bdbccf5d7dea7e" [[package]] name = "base62" -version = "2.2.3" +version = "2.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adf9755786e27479693dedd3271691a92b5e242ab139cacb9fb8e7fb5381111" +checksum = "cd637ac531c60eb7fbc4684dc061c2d7d90d73d758181aa02eeff0464b9eee4b" [[package]] name = "base64" @@ -362,9 +371,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" dependencies = [ "serde_core", ] @@ -387,6 +396,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", + "zeroize", +] + [[package]] name = "block-padding" version = "0.3.3" @@ -416,9 +435,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "5.0.0" +version = "5.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -426,19 +445,19 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" dependencies = [ "memchr", - "serde", + "serde_core", ] [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytecount" @@ -448,22 +467,22 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] name = "bytemuck" -version = "1.25.0" +version = "1.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +checksum = "d6aedf8ae72766347502cf3cb4f41cf5e9cc37d28bee90f1fdaaae15f9cf9424" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.10.2" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +checksum = "f65693059b6b9c588b9f62fed1cedbf0a8b805631457ea162d68f0de186f3de5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.119", ] [[package]] @@ -480,9 +499,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "bzip2" @@ -499,7 +518,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "log", "polling", "rustix 0.38.44", @@ -513,9 +532,9 @@ version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "polling", - "rustix 1.1.3", + "rustix 1.1.4", "slab", "tracing", ] @@ -539,7 +558,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" dependencies = [ "calloop 0.14.4", - "rustix 1.1.3", + "rustix 1.1.4", "wayland-backend", "wayland-client", ] @@ -550,14 +569,14 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" dependencies = [ - "cipher", + "cipher 0.4.4", ] [[package]] name = "cc" -version = "1.2.56" +version = "1.2.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" dependencies = [ "find-msvc-tools", "jobserver", @@ -565,12 +584,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cfg-if" version = "1.0.4" @@ -594,9 +607,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -612,8 +625,18 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", - "inout", + "crypto-common 0.1.7", + "inout 0.1.4", +] + +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "crypto-common 0.2.2", + "inout 0.2.2", ] [[package]] @@ -625,6 +648,12 @@ dependencies = [ "error-code", ] +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "codespan-reporting" version = "0.13.1" @@ -636,9 +665,9 @@ dependencies = [ [[package]] name = "color" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18ef4657441fb193b65f34dc39b3781f0dfec23d3bd94d0eeb4e88cde421edb" +checksum = "2ec7c5eb7a16992b1904d76c517d170ab353b0e0b3d5a0c81a8a0cd1037893cf" dependencies = [ "bytemuck", ] @@ -662,6 +691,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "constant_time_eq" version = "0.4.2" @@ -753,6 +788,12 @@ dependencies = [ "libc", ] +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -762,6 +803,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -773,9 +823,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -783,18 +833,18 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "crunchy" @@ -812,6 +862,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "cssparser" version = "0.27.2" @@ -836,7 +895,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.116", + "syn 2.0.119", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", ] [[package]] @@ -865,7 +933,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.116", + "syn 2.0.119", ] [[package]] @@ -876,22 +944,21 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn 2.0.116", + "syn 2.0.119", ] [[package]] name = "deflate64" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" +checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" [[package]] name = "deranged" -version = "0.5.6" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ - "powerfmt", "serde_core", ] @@ -905,7 +972,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.116", + "syn 2.0.119", ] [[package]] @@ -914,9 +981,21 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", - "subtle", + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", + "ctutils", + "zeroize", ] [[package]] @@ -937,7 +1016,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -948,11 +1027,11 @@ checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" [[package]] name = "dispatch2" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "block2 0.6.2", "libc", "objc2 0.6.4", @@ -960,20 +1039,20 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.119", ] [[package]] name = "dlib" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" dependencies = [ "libloading", ] @@ -1026,14 +1105,14 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a8bfa975b1aec2145850fcaa1c6fe269a16578c44705a532ae3edc92b8881c7" dependencies = [ - "cipher", + "cipher 0.4.4", ] [[package]] name = "ecolor" -version = "0.34.1" +version = "0.34.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "137c0ce4ce4152ff7e223a7ce22ee1057cdff61fce0a45c32459c3ccec64868d" +checksum = "a05fbfa222ffb51989d5ccf33e5f7aebfcf96c5023413856b0c3618a7f79896e" dependencies = [ "bytemuck", "emath", @@ -1042,9 +1121,9 @@ dependencies = [ [[package]] name = "eframe" -version = "0.34.1" +version = "0.34.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6e995b8e434d65aefd12c4519221be3e8f38efd77804ef39ca10553f4ad7063" +checksum = "f98fe83b2589105b69dd25ca1e0fa2135a6e864d502fd8e08978f937e128cfef" dependencies = [ "ahash", "bytemuck", @@ -1080,13 +1159,13 @@ dependencies = [ [[package]] name = "egui" -version = "0.34.1" +version = "0.34.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f34aaf627da598dfadd64b0fee6101d22e9c451d1e5348157312720b7f459f0f" +checksum = "42112be0ae157289312b92b3dfaf20e911b5a3c4c65d4aab0e7c47fbc0ce16e3" dependencies = [ "accesskit", "ahash", - "bitflags 2.11.0", + "bitflags 2.13.1", "emath", "epaint", "log", @@ -1100,9 +1179,9 @@ dependencies = [ [[package]] name = "egui-wgpu" -version = "0.34.1" +version = "0.34.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71033ff78b041c9c363450f4498ff95468ef3ecbcc71a62f67036a6207d98fa4" +checksum = "9f0c0559ac5598a1b887a6206dccbab7e3e6246c57cb00ae287262bd44776c9c" dependencies = [ "ahash", "bytemuck", @@ -1120,9 +1199,9 @@ dependencies = [ [[package]] name = "egui-winit" -version = "0.34.1" +version = "0.34.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11a2881b2bf1a305e413e644af63f836737a33d85077705ff808e88f902ff742" +checksum = "967c5b323625d46d46a59b5daba3fef742248d27693cc18972458619858c4239" dependencies = [ "arboard", "bytemuck", @@ -1142,9 +1221,9 @@ dependencies = [ [[package]] name = "egui_extras" -version = "0.34.1" +version = "0.34.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bfc6870c68d3f254e33aca8200095d422e09edacb0f365f79fe23a5ba10963" +checksum = "598d8675f6fd9088db8a93d8c7aacda936b2f3d0c2b0660ad1744a45b5caf922" dependencies = [ "ahash", "egui", @@ -1157,9 +1236,9 @@ dependencies = [ [[package]] name = "egui_glow" -version = "0.34.1" +version = "0.34.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3b28d39ab6c0cac238190e6cb1e8c9047d02cb470ab942a7a3302e4cb3a8e74" +checksum = "62b652957fa7e1ab01e8fecbfbf4e35f6e43a53fa98af8a562b50d5403cd44b9" dependencies = [ "bytemuck", "egui", @@ -1185,15 +1264,15 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "emath" -version = "0.34.1" +version = "0.34.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a05cd8bdf3b598488c627ca97c7fe8909448ffa26278dd3c7e535cdb554d721" +checksum = "b53f0d33a479321da6b0caa71366c9f67e8a2c149762d90bdc0d16e601ee8ecb" dependencies = [ "bytemuck", "serde", @@ -1225,7 +1304,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.119", ] [[package]] @@ -1236,14 +1315,14 @@ checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.119", ] [[package]] name = "epaint" -version = "0.34.1" +version = "0.34.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04f3017dd67f147a697ee0c8484fb568fd9553e2a0c114be5020dbbc11962841" +checksum = "6675898a291ec212fc3df04f537d177fce8496120244590e6359dcaa4c25da79" dependencies = [ "ahash", "bytemuck", @@ -1264,9 +1343,9 @@ dependencies = [ [[package]] name = "epaint_default_fonts" -version = "0.34.1" +version = "0.34.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3b85a2bb775a3ab02d077a65cc31575c11b2584581913253cc11ce49f48bba" +checksum = "f8970033a4282a7bcf899b38b5ed3a58b732fe093d03785d58648515d8d309da" [[package]] name = "equivalent" @@ -1281,7 +1360,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1301,23 +1380,9 @@ dependencies = [ [[package]] name = "fax" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" -dependencies = [ - "fax_derive", -] - -[[package]] -name = "fax_derive" -version = "0.2.0" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.116", -] +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" [[package]] name = "fdeflate" @@ -1339,13 +1404,12 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.27" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" dependencies = [ "cfg-if", "libc", - "libredox", ] [[package]] @@ -1371,12 +1435,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "foldhash" version = "0.2.0" @@ -1385,9 +1443,9 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "font-types" -version = "0.11.1" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73829a7b5c91198af28a99159b7ae4afbb252fb906159ff7f189f3a2ceaa3df2" +checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" dependencies = [ "bytemuck", "serde", @@ -1411,7 +1469,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.119", ] [[package]] @@ -1501,7 +1559,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.119", ] [[package]] @@ -1558,7 +1616,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ - "rustix 1.1.3", + "rustix 1.1.4", "windows-link", ] @@ -1593,23 +1651,21 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", - "wasip2", - "wasip3", + "r-efi 6.0.0", "wasm-bindgen", ] @@ -1638,9 +1694,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "globset" -version = "0.4.18" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +checksum = "e47d37d2ae4464254884b60ab7071be2b876a9c35b696bd018ddcc76847309cd" dependencies = [ "aho-corasick", "bstr", @@ -1678,7 +1734,7 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "cfg_aliases", "cgl", "dispatch2", @@ -1764,21 +1820,18 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ - "foldhash 0.1.5", + "foldhash", ] [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "foldhash 0.2.0", -] +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -1812,11 +1865,11 @@ checksum = "9040319a6910b901d5d49cbada4a99db52836a1b63228a05f7e2b7f8feef89b1" [[package]] name = "hmac" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" dependencies = [ - "digest", + "digest 0.11.3", ] [[package]] @@ -1844,19 +1897,19 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", - "itoa 1.0.17", + "itoa 1.0.18", ] [[package]] name = "http-body" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", "http", @@ -1864,9 +1917,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" dependencies = [ "bytes", "futures-core", @@ -1887,11 +1940,20 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", @@ -1901,7 +1963,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 1.0.17", + "itoa 1.0.18", "pin-project-lite", "smallvec", "tokio", @@ -1971,12 +2033,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -1984,9 +2047,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1997,9 +2060,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -2011,15 +2074,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -2031,15 +2094,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -2050,12 +2113,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "ident_case" version = "1.0.1" @@ -2075,9 +2132,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -2085,9 +2142,9 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.25" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +checksum = "d4ffa3a0547a138e59ddd6fa3b7c672ed47e6ad6a3cd177984ff1116aa5ba742" dependencies = [ "crossbeam-deque", "globset", @@ -2101,9 +2158,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.9" +version = "0.25.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" dependencies = [ "bytemuck", "byteorder-lite", @@ -2115,14 +2172,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", + "hashbrown 0.17.1", ] [[package]] @@ -2135,6 +2190,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", +] + [[package]] name = "is-docker" version = "0.2.0" @@ -2180,49 +2244,86 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jni" -version = "0.21.1" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" dependencies = [ - "cesu8", "cfg-if", "combine", - "jni-sys", + "jni-macros", + "jni-sys 0.4.1", "log", - "thiserror 1.0.69", + "simd_cesu8", + "thiserror 2.0.18", "walkdir", - "windows-sys 0.45.0", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", ] [[package]] name = "jni-sys" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] [[package]] name = "jobserver" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.3", "libc", ] [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -2246,12 +2347,13 @@ dependencies = [ [[package]] name = "kurbo" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7564e90fe3c0d5771e1f0bc95322b21baaeaa0d9213fa6a0b61c99f8b17b3bfb" +checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2" dependencies = [ "arrayvec", "euclid", + "polycool", "smallvec", ] @@ -2261,23 +2363,17 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libbz2-rs-sys" -version = "0.2.2" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" +checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" [[package]] name = "libc" -version = "0.2.182" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libloading" @@ -2297,13 +2393,14 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "libc", - "redox_syscall 0.7.1", + "plain", + "redox_syscall 0.9.0", ] [[package]] @@ -2330,15 +2427,15 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "litrs" @@ -2357,9 +2454,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "lopdf" @@ -2367,22 +2464,22 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f560f57dfb9142a02d673e137622fd515d4231e51feb8b4af28d92647d83f35b" dependencies = [ - "aes", - "bitflags 2.11.0", + "aes 0.8.4", + "bitflags 2.13.1", "cbc", "ecb", "encoding_rs", "flate2", "getrandom 0.3.4", "indexmap", - "itoa 1.0.17", + "itoa 1.0.18", "log", "md-5", "nom 8.0.0", "nom_locate", - "rand 0.9.2", + "rand 0.9.5", "rangemap", - "sha2", + "sha2 0.10.9", "stringprep", "thiserror 2.0.18", "time", @@ -2392,17 +2489,17 @@ dependencies = [ [[package]] name = "lru" -version = "0.16.3" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" [[package]] name = "lzma-rust2" -version = "0.16.2" +version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47bb1e988e6fb779cf720ad431242d3f03167c1b3f2b1aae7f1a94b2495b36ae" +checksum = "ca93e534d1142d1d0dcca6d25fe302508a5dfb40b302802904577725ea0b695b" dependencies = [ - "sha2", + "sha2 0.11.0", ] [[package]] @@ -2444,20 +2541,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", ] [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memmap2" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" dependencies = [ "libc", ] @@ -2511,9 +2608,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "wasi 0.11.1+wasi-snapshot-preview1", @@ -2532,9 +2629,9 @@ dependencies = [ [[package]] name = "moxcms" -version = "0.7.11" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" dependencies = [ "num-traits", "pxfm", @@ -2542,13 +2639,13 @@ dependencies = [ [[package]] name = "naga" -version = "29.0.1" +version = "29.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2630921705b9b01dcdd0b6864b9562ca3c1951eecd0f0c4f5f04f61e412647" +checksum = "b2bf919621e7975acb27d881bae2fb993e0d45c8e0446e85e6272971e00dc8df" dependencies = [ "arrayvec", "bit-set", - "bitflags 2.11.0", + "bitflags 2.13.1", "cfg-if", "cfg_aliases", "codespan-reporting", @@ -2571,8 +2668,8 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.11.0", - "jni-sys", + "bitflags 2.13.1", + "jni-sys 0.3.1", "log", "ndk-sys", "num_enum", @@ -2592,7 +2689,7 @@ version = "0.6.0+11769913" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" dependencies = [ - "jni-sys", + "jni-sys 0.3.1", ] [[package]] @@ -2641,9 +2738,9 @@ dependencies = [ [[package]] name = "normpath" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf23ab2b905654b4cb177e30b629937b3868311d4e1cba859f899c041046e69b" +checksum = "b9985ef7269fa99f3b12437bb698381da2428743ab90f20393f399fa14cab21a" dependencies = [ "windows-sys 0.61.2", ] @@ -2654,14 +2751,14 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-traits" @@ -2675,9 +2772,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" dependencies = [ "num_enum_derive", "rustversion", @@ -2685,14 +2782,14 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.119", ] [[package]] @@ -2726,7 +2823,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -2742,7 +2839,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "block2 0.6.2", "objc2 0.6.4", "objc2-core-foundation", @@ -2756,7 +2853,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", @@ -2780,7 +2877,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -2792,7 +2889,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "dispatch2", "objc2 0.6.4", ] @@ -2803,7 +2900,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "dispatch2", "objc2 0.6.4", "objc2-core-foundation", @@ -2846,7 +2943,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "block2 0.5.1", "dispatch", "libc", @@ -2859,7 +2956,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "block2 0.6.2", "libc", "objc2 0.6.4", @@ -2872,7 +2969,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "objc2 0.6.4", "objc2-core-foundation", ] @@ -2895,7 +2992,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -2907,7 +3004,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -2930,7 +3027,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "block2 0.5.1", "objc2 0.5.2", "objc2-cloud-kit", @@ -2951,7 +3048,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "objc2 0.6.4", "objc2-core-foundation", "objc2-foundation 0.3.2", @@ -2974,7 +3071,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", @@ -2983,19 +3080,18 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "open" -version = "5.3.3" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" +checksum = "a0b3d059e795d52b8a72fef45658620edd4d9c359b338564aa14391ffa511ed5" dependencies = [ "is-wsl", "libc", - "pathdiff", ] [[package]] @@ -3006,9 +3102,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "orbclient" -version = "0.3.50" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ad2c6bae700b7aa5d1cc30c59bdd3a1c180b09dbaea51e2ae2b8e1cf211fdd" +checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" dependencies = [ "libc", "libredox", @@ -3035,7 +3131,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.119", ] [[package]] @@ -3063,15 +3159,9 @@ dependencies = [ [[package]] name = "pastey" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec" - -[[package]] -name = "pathdiff" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" [[package]] name = "pathfinder_geometry" @@ -3085,28 +3175,28 @@ dependencies = [ [[package]] name = "pathfinder_simd" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf9027960355bf3afff9841918474a81a5f972ac6d226d518060bba758b5ad57" +checksum = "4500030c302e4af1d423f36f3b958d1aecb6c04184356ed5a833bf6b60435777" dependencies = [ "rustc_version", ] [[package]] name = "pbkdf2" -version = "0.12.2" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" dependencies = [ - "digest", + "digest 0.11.3", "hmac", ] [[package]] name = "peniko" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2b6aadb221872732e87d465213e9be5af2849b0e8cc5300a8ba98fffa2e00a" +checksum = "839c8299360d2e998bdb106dc0a6cd71dcc5f4df51df1b620361bf50e283cca6" dependencies = [ "bytemuck", "color", @@ -3169,7 +3259,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared 0.11.3", - "rand 0.8.5", + "rand 0.8.7", ] [[package]] @@ -3196,7 +3286,7 @@ dependencies = [ "phf_shared 0.11.3", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.119", "unicase", ] @@ -3215,41 +3305,47 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ - "siphasher 1.0.2", + "siphasher 1.0.3", "unicase", ] [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.119", ] [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "png" @@ -3257,7 +3353,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "crc32fast", "fdeflate", "flate2", @@ -3280,7 +3376,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 1.1.3", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -3290,6 +3386,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" +[[package]] +name = "polycool" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6" +dependencies = [ + "arrayvec", +] + [[package]] name = "portable-atomic" version = "1.13.1" @@ -3298,18 +3403,18 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -3341,16 +3446,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.116", -] - [[package]] name = "printpdf" version = "0.9.1" @@ -3379,11 +3474,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.23.10+spec-1.0.0", + "toml_edit 0.25.13+spec-1.1.0", ] [[package]] @@ -3427,18 +3522,15 @@ dependencies = [ [[package]] name = "profiling" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" [[package]] name = "pxfm" -version = "0.1.27" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" -dependencies = [ - "num-traits", -] +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" [[package]] name = "quick-error" @@ -3448,18 +3540,18 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.38.4" +version = "0.39.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.44" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -3470,6 +3562,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.7.3" @@ -3486,18 +3584,18 @@ dependencies = [ [[package]] name = "rand" -version = "0.8.5" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ "rand_core 0.6.4", ] [[package]] name = "rand" -version = "0.9.2" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -3579,9 +3677,9 @@ checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -3622,16 +3720,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", ] [[package]] name = "redox_syscall" -version = "0.7.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" +checksum = "c5102a6aaa05aa011a238e178e6bca86d2cb56fc9f586d37cb80f5bca6e07759" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", ] [[package]] @@ -3662,14 +3760,14 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.119", ] [[package]] name = "regex" -version = "1.12.3" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -3679,9 +3777,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" dependencies = [ "aho-corasick", "memchr", @@ -3690,9 +3788,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "renderdoc-sys" @@ -3757,7 +3855,7 @@ dependencies = [ "http-body-util", "pastey", "pin-project-lite", - "rand 0.9.2", + "rand 0.9.5", "rmcp-macros", "schemars", "serde", @@ -3782,16 +3880,16 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.116", + "syn 2.0.119", ] [[package]] name = "ron" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4147b952f3f819eca0e99527022f7d6a8d05f111aeb0a62960c74eb283bec8fc" +checksum = "81116b9531d61eabc41aeb228e4b6b2435bcca3233b98cf3b3077d4e6e9debb3" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "once_cell", "serde", "serde_derive", @@ -3848,7 +3946,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "syn 2.0.116", + "syn 2.0.119", ] [[package]] @@ -3869,7 +3967,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "siphasher 1.0.2", + "siphasher 1.0.3", "toml 0.8.23", "triomphe", ] @@ -3882,9 +3980,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -3901,7 +3999,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "errno", "libc", "linux-raw-sys 0.4.15", @@ -3910,22 +4008,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "errno", "libc", - "linux-raw-sys 0.11.0", - "windows-sys 0.59.0", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" dependencies = [ "log", "once_cell", @@ -3938,9 +4036,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" dependencies = [ "zeroize", ] @@ -3958,9 +4056,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "ryu" @@ -4000,7 +4098,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.116", + "syn 2.0.119", ] [[package]] @@ -4037,15 +4135,15 @@ dependencies = [ [[package]] name = "self_cell" -version = "1.2.2" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" +checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -4074,7 +4172,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.119", ] [[package]] @@ -4085,16 +4183,16 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.119", ] [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ - "itoa 1.0.17", + "itoa 1.0.18", "memchr", "serde", "serde_core", @@ -4107,7 +4205,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" dependencies = [ - "itoa 1.0.17", + "itoa 1.0.18", "serde", "serde_core", ] @@ -4123,9 +4221,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -4137,7 +4235,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.17", + "itoa 1.0.18", "ryu", "serde", ] @@ -4149,7 +4247,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ "indexmap", - "itoa 1.0.17", + "itoa 1.0.18", "ryu", "serde", "unsafe-libyaml", @@ -4162,7 +4260,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59e2dd588bf1597a252c3b920e0143eb99b0f76e4e082f4c92ce34fbc9e71ddd" dependencies = [ "indexmap", - "itoa 1.0.17", + "itoa 1.0.18", "libyml", "memchr", "ryu", @@ -4182,13 +4280,13 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -4198,8 +4296,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -4213,9 +4322,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook-registry" @@ -4229,9 +4338,25 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "siphasher" @@ -4241,9 +4366,9 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "siphasher" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "skrifa" @@ -4272,9 +4397,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" dependencies = [ "serde", ] @@ -4285,7 +4410,7 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "calloop 0.13.0", "calloop-wayland-source 0.3.0", "cursor-icon", @@ -4310,14 +4435,14 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "calloop 0.14.4", "calloop-wayland-source 0.4.1", "cursor-icon", "libc", "log", "memmap2", - "rustix 1.1.3", + "rustix 1.1.4", "thiserror 2.0.18", "wayland-backend", "wayland-client", @@ -4353,19 +4478,19 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "sse-stream" -version = "0.2.1" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb4dc4d33c68ec1f27d386b5610a351922656e1fdf5c05bbaad930cd1519479a" +checksum = "39f24a9b78c40b90817bbcd1821c74ddfd74916aadd29403d001532a9195532d" dependencies = [ "bytes", "futures-util", @@ -4456,7 +4581,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.116", + "syn 2.0.119", ] [[package]] @@ -4468,7 +4593,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.119", ] [[package]] @@ -4490,9 +4615,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.116" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", @@ -4513,7 +4638,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.119", ] [[package]] @@ -4530,9 +4655,9 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.45" +version = "0.4.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" dependencies = [ "filetime", "libc", @@ -4582,7 +4707,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.119", ] [[package]] @@ -4593,23 +4718,23 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.119", ] [[package]] name = "thread_local" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" dependencies = [ "cfg-if", ] [[package]] name = "tiff" -version = "0.10.3" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" dependencies = [ "fax", "flate2", @@ -4621,12 +4746,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.47" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" dependencies = [ "deranged", - "itoa 1.0.17", "js-sys", "num-conv", "powerfmt", @@ -4637,15 +4761,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" dependencies = [ "num-conv", "time-core", @@ -4653,9 +4777,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -4663,9 +4787,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" dependencies = [ "tinyvec_macros", ] @@ -4678,9 +4802,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.50.0" +version = "1.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "317fafbbe3f02fc663dad00ea6186197de963cd4190e86a26d8d0fae095539af" dependencies = [ "bytes", "libc", @@ -4695,13 +4819,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.119", ] [[package]] @@ -4742,17 +4866,17 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.12+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" dependencies = [ "indexmap", "serde_core", - "serde_spanned 1.0.4", - "toml_datetime 0.7.5+spec-1.1.0", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow", + "winnow 1.0.4", ] [[package]] @@ -4766,9 +4890,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.5+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] @@ -4784,28 +4908,28 @@ dependencies = [ "serde_spanned 0.6.9", "toml_datetime 0.6.11", "toml_write", - "winnow", + "winnow 0.7.15", ] [[package]] name = "toml_edit" -version = "0.23.10+spec-1.0.0" +version = "0.25.13+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ "indexmap", - "toml_datetime 0.7.5+spec-1.1.0", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow", + "winnow 1.0.4", ] [[package]] name = "toml_parser" -version = "1.0.9+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow", + "winnow 1.0.4", ] [[package]] @@ -4816,9 +4940,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "toml_writer" -version = "1.0.6+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tower" @@ -4868,7 +4992,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.119", ] [[package]] @@ -4894,9 +5018,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "nu-ansi-term", "sharded-slab", @@ -4908,9 +5032,9 @@ dependencies = [ [[package]] name = "triomphe" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" +checksum = "b40688ea6389c8171614b25491f71d4a27946e0c7ce2da1c6de27e25abf1a0ae" dependencies = [ "arc-swap", "serde", @@ -4929,7 +5053,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" dependencies = [ - "rustc-hash 2.1.1", + "rustc-hash 2.1.3", ] [[package]] @@ -4946,9 +5070,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "ucd-trie" @@ -5052,9 +5176,9 @@ checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-width" @@ -5062,12 +5186,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -5144,11 +5262,11 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.21.0" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ - "getrandom 0.4.1", + "getrandom 0.4.3", "js-sys", "serde_core", "wasm-bindgen", @@ -5216,27 +5334,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -5247,23 +5356,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.58" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5271,69 +5376,35 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.119", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.11.0", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "wayland-backend" -version = "0.3.12" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fee64194ccd96bf648f42a65a7e589547096dfa702f7cadef84347b66ad164f9" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" dependencies = [ "cc", "downcast-rs", - "rustix 1.1.3", + "rustix 1.1.4", "scoped-tls", "smallvec", "wayland-sys", @@ -5341,12 +5412,12 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.12" +version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" dependencies = [ - "bitflags 2.11.0", - "rustix 1.1.3", + "bitflags 2.13.1", + "rustix 1.1.4", "wayland-backend", "wayland-scanner", ] @@ -5357,29 +5428,29 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "cursor-icon", "wayland-backend", ] [[package]] name = "wayland-cursor" -version = "0.31.12" +version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5864c4b5b6064b06b1e8b74ead4a98a6c45a285fe7a0e784d24735f011fdb078" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" dependencies = [ - "rustix 1.1.3", + "rustix 1.1.4", "wayland-client", "xcursor", ] [[package]] name = "wayland-protocols" -version = "0.32.10" +version = "0.32.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baeda9ffbcfc8cd6ddaade385eaf2393bd2115a69523c735f12242353c3df4f3" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "wayland-backend", "wayland-client", "wayland-scanner", @@ -5391,7 +5462,7 @@ version = "20250721.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "wayland-backend", "wayland-client", "wayland-protocols", @@ -5400,11 +5471,11 @@ dependencies = [ [[package]] name = "wayland-protocols-misc" -version = "0.3.10" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791c58fdeec5406aa37169dd815327d1e47f334219b523444bc26d70ceb4c34e" +checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "wayland-backend", "wayland-client", "wayland-protocols", @@ -5413,11 +5484,11 @@ dependencies = [ [[package]] name = "wayland-protocols-plasma" -version = "0.3.10" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa98634619300a535a9a97f338aed9a5ff1e01a461943e8346ff4ae26007306b" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "wayland-backend", "wayland-client", "wayland-protocols", @@ -5426,11 +5497,11 @@ dependencies = [ [[package]] name = "wayland-protocols-wlr" -version = "0.3.10" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9597cdf02cf0c34cd5823786dce6b5ae8598f05c2daf5621b6e178d4f7345f3" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "wayland-backend", "wayland-client", "wayland-protocols", @@ -5439,9 +5510,9 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.8" +version = "0.31.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" dependencies = [ "proc-macro2", "quick-xml", @@ -5450,9 +5521,9 @@ dependencies = [ [[package]] name = "wayland-sys" -version = "0.31.8" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6dbfc3ac5ef974c92a2235805cc0114033018ae1290a72e474aa8b28cbbdfd" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" dependencies = [ "dlib", "log", @@ -5462,9 +5533,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.85" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", @@ -5482,9 +5553,9 @@ dependencies = [ [[package]] name = "webbrowser" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f00bb839c1cf1e3036066614cbdcd035ecf215206691ea646aa3c60a24f68f2" +checksum = "0fc95580916af1e68ff6a7be07446fc5db73ebf71cf092de939bbf5f7e189f72" dependencies = [ "core-foundation 0.10.1", "jni", @@ -5498,9 +5569,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.6" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" dependencies = [ "rustls-pki-types", ] @@ -5513,12 +5584,12 @@ checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" [[package]] name = "wgpu" -version = "29.0.1" +version = "29.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c239a9a747bbd379590985bac952c2e53cb19873f7072b3370c6a6a8e06837" +checksum = "76e8840e1ba2881d4cbb18d2147627a56af426ff064c0401eb0c8410c6325d07" dependencies = [ "arrayvec", - "bitflags 2.11.0", + "bitflags 2.13.1", "bytemuck", "cfg-if", "cfg_aliases", @@ -5541,14 +5612,14 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "29.0.1" +version = "29.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e80ac6cf1895df6342f87d975162108f9d98772a0d74bc404ab7304ac29469e" +checksum = "2f519832254e56965a9940c4af57dcb75f702b6f6fa4a0b172f685395843a4d7" dependencies = [ "arrayvec", "bit-set", "bit-vec", - "bitflags 2.11.0", + "bitflags 2.13.1", "bytemuck", "cfg_aliases", "document-features", @@ -5572,20 +5643,20 @@ dependencies = [ [[package]] name = "wgpu-core-deps-windows-linux-android" -version = "29.0.0" +version = "29.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "725d5c006a8c02967b6d93ef04f6537ec4593313e330cfe86d9d3f946eb90f28" +checksum = "4e592c1bbef6ad047647ae6e666ebd8cee7a32bb4544d9700ec96cbf73230257" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-hal" -version = "29.0.1" +version = "29.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a47aef47636562f3937285af4c44b4b5b404b46577471411cc5313a921da7e" +checksum = "97ace1c17727311c22a46e4e3faf56ea6de81af99dcc839bdfb54857b94d448d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "cfg-if", "cfg_aliases", "libloading", @@ -5602,9 +5673,9 @@ dependencies = [ [[package]] name = "wgpu-naga-bridge" -version = "29.0.1" +version = "29.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4684f4410da0cf95a4cb63bb5edaac022461dedb6adf0b64d0d9b5f6890d51" +checksum = "95226013f547544b223281cd16a4fb549aa9dcb562adbda0faae4c73ffbbc161" dependencies = [ "naga", "wgpu-types", @@ -5612,11 +5683,11 @@ dependencies = [ [[package]] name = "wgpu-types" -version = "29.0.1" +version = "29.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec2675540fb1a5cfa5ef122d3d5f390e2c75711a0b946410f2d6ac3a0f77d1f6" +checksum = "84bf84cd9ca8ca45e2b223a3868f1adf9bfc0c66aeac212e76ee7e40fdadf8f5" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "bytemuck", "js-sys", "log", @@ -5646,7 +5717,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5676,7 +5747,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.119", ] [[package]] @@ -5687,7 +5758,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.119", ] [[package]] @@ -5714,15 +5785,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -5759,21 +5821,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -5807,12 +5854,6 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -5825,12 +5866,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -5843,12 +5878,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -5873,12 +5902,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -5891,12 +5914,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -5909,12 +5926,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -5927,12 +5938,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -5954,7 +5959,7 @@ dependencies = [ "ahash", "android-activity", "atomic-waker", - "bitflags 2.11.0", + "bitflags 2.13.1", "block2 0.5.1", "bytemuck", "calloop 0.13.0", @@ -5998,116 +6003,43 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] [[package]] -name = "winresource" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e287ced0f21cd11f4035fe946fd3af145f068d1acb708afd248100f89ec7432d" -dependencies = [ - "toml 0.9.12+spec-1.1.0", - "version_check", -] - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck 0.5.0", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck 0.5.0", - "indexmap", - "prettyplease", - "syn 2.0.116", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" +name = "winnow" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.116", - "wit-bindgen-core", - "wit-bindgen-rust", + "memchr", ] [[package]] -name = "wit-component" -version = "0.244.0" +name = "winresource" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +checksum = "0986a8b1d586b7d3e4fe3d9ea39fb451ae22869dcea4aa109d287a374d866087" dependencies = [ - "anyhow", - "bitflags 2.11.0", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", + "toml 1.1.3+spec-1.1.0", + "version_check", ] [[package]] -name = "wit-parser" -version = "0.244.0" +name = "wit-bindgen" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "x11-dl" @@ -6131,7 +6063,7 @@ dependencies = [ "libc", "libloading", "once_cell", - "rustix 1.1.3", + "rustix 1.1.4", "x11rb-protocol", ] @@ -6148,7 +6080,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix 1.1.3", + "rustix 1.1.4", ] [[package]] @@ -6163,7 +6095,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "dlib", "log", "once_cell", @@ -6196,9 +6128,9 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -6207,68 +6139,68 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.119", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.119", ] [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.119", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -6277,9 +6209,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -6288,28 +6220,28 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.119", ] [[package]] name = "zip" -version = "8.5.0" +version = "8.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2726508a48f38dceb22b35ecbbd2430efe34ff05c62bd3285f965d7911b33464" +checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" dependencies = [ - "aes", + "aes 0.9.1", "bzip2", "constant_time_eq", "crc32fast", "deflate64", "flate2", - "getrandom 0.4.1", + "getrandom 0.4.3", "hmac", "indexmap", "lzma-rust2", @@ -6326,15 +6258,15 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.6.3" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" +checksum = "b142a20ec14a91d5bc708c1dc21b080c550113d8aa77afa29635673a65dd02c5" [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zopfli" @@ -6378,15 +6310,15 @@ dependencies = [ [[package]] name = "zune-core" -version = "0.4.12" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" [[package]] name = "zune-jpeg" -version = "0.4.21" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" dependencies = [ "zune-core", ] From 14ff56e8b9f09b1ec1b77482f8e8efcf40f97f76 Mon Sep 17 00:00:00 2001 From: Cole Gentry Date: Thu, 16 Jul 2026 10:58:07 -0400 Subject: [PATCH 03/18] perf(build): enable codegen-units=1 for release builds --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index a2247ff..11de0f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -107,6 +107,7 @@ objc2-foundation = "0.3.2" opt-level = 3 lto = true strip = true +codegen-units = 1 # Faster compile times for debug builds [profile.dev] From 0d53657c643b10bf82cd6d38754cd567b9a7bb88 Mon Sep 17 00:00:00 2001 From: Cole Gentry Date: Thu, 16 Jul 2026 10:58:45 -0400 Subject: [PATCH 04/18] perf(app): event-driven IPC repaint via callback instead of 10Hz poll; apply dark visuals once at startup; surface loader-thread panics as an error toast instead of hanging in Loading state --- src/app.rs | 123 +++++++++++++++++++++++++--------------------- src/ipc/server.rs | 32 ++++++++++-- 2 files changed, 97 insertions(+), 58 deletions(-) diff --git a/src/app.rs b/src/app.rs index c861edb..7d0f520 100644 --- a/src/app.rs +++ b/src/app.rs @@ -277,6 +277,9 @@ impl UltraLogApp { // Apply fonts cc.egui_ctx.set_fonts(fonts); + // Apply the dark theme once at startup (the app is dark-only) + cc.egui_ctx.set_visuals(egui::Visuals::dark()); + // Load user settings and set locale let user_settings = UserSettings::load(); rust_i18n::set_locale(user_settings.language.locale_code()); @@ -290,9 +293,13 @@ impl UltraLogApp { ..Self::default() }; - // Start the IPC server for MCP integration + // Start the IPC server for MCP integration. Commands wake the GUI + // via request_repaint instead of a fixed polling interval. let mut ipc_port = crate::ipc::DEFAULT_IPC_PORT; - match IpcServer::start() { + let repaint_ctx = cc.egui_ctx.clone(); + match IpcServer::start_with_repaint(std::sync::Arc::new(move || { + repaint_ctx.request_repaint() + })) { Ok(server) => { ipc_port = server.port(); tracing::info!("MCP IPC server started on port {}", ipc_port); @@ -663,53 +670,61 @@ impl UltraLogApp { /// Check for completed background loads fn check_loading_complete(&mut self) { if let Some(receiver) = &self.load_receiver { - if let Ok(result) = receiver.try_recv() { - match result { - LoadResult::Success(file) => { - let file_index = self.files.len(); - let file_name = file.name.clone(); - - // Track file load for analytics - let ecu_type_str = format!("{:?}", file.ecu_type); - let file_size = std::fs::metadata(&file.path).map(|m| m.len()).unwrap_or(0); - analytics::track_file_loaded(&ecu_type_str, file_size); - - // Compute time range for this file - let times = file.log.get_times_as_f64(); - let file_time_range = - if let (Some(&first), Some(&last)) = (times.first(), times.last()) { - Some((first, last)) - } else { - None - }; - - self.files.push(*file); - self.selected_file = Some(file_index); - self.update_time_range(); - - // Create a new tab for this file with its time range - let mut tab = Tab::new(file_index, file_name); - tab.time_range = file_time_range; - // Initialize cursor to start of file - if let Some((min_time, _)) = file_time_range { - tab.cursor_time = Some(min_time); - tab.cursor_record = Some(0); - } - self.tabs.push(tab); - self.active_tab = Some(self.tabs.len() - 1); + let result = match receiver.try_recv() { + Ok(result) => result, + Err(std::sync::mpsc::TryRecvError::Empty) => return, + // The loader thread died (e.g. a parser panic) without + // sending a result — surface an error instead of spinning + // in the Loading state forever. + Err(std::sync::mpsc::TryRecvError::Disconnected) => { + LoadResult::Error("File loading failed unexpectedly".to_string()) + } + }; + match result { + LoadResult::Success(file) => { + let file_index = self.files.len(); + let file_name = file.name.clone(); + + // Track file load for analytics + let ecu_type_str = format!("{:?}", file.ecu_type); + let file_size = std::fs::metadata(&file.path).map(|m| m.len()).unwrap_or(0); + analytics::track_file_loaded(&ecu_type_str, file_size); + + // Compute time range for this file + let times = file.log.get_times_as_f64(); + let file_time_range = + if let (Some(&first), Some(&last)) = (times.first(), times.last()) { + Some((first, last)) + } else { + None + }; + + self.files.push(*file); + self.selected_file = Some(file_index); + self.update_time_range(); + + // Create a new tab for this file with its time range + let mut tab = Tab::new(file_index, file_name); + tab.time_range = file_time_range; + // Initialize cursor to start of file + if let Some((min_time, _)) = file_time_range { + tab.cursor_time = Some(min_time); + tab.cursor_record = Some(0); + } + self.tabs.push(tab); + self.active_tab = Some(self.tabs.len() - 1); - self.show_toast_success(&t!("toast.file_loaded")); + self.show_toast_success(&t!("toast.file_loaded")); - // Switch to Channels panel so user can select channels - self.active_panel = ActivePanel::ToolProperties; - } - LoadResult::Error(e) => { - self.show_toast_error(&format!("Error: {}", e)); - } + // Switch to Channels panel so user can select channels + self.active_panel = ActivePanel::ToolProperties; + } + LoadResult::Error(e) => { + self.show_toast_error(&format!("Error: {}", e)); } - self.load_receiver = None; - self.loading_state = LoadingState::Idle; } + self.load_receiver = None; + self.loading_state = LoadingState::Idle; } } @@ -1988,7 +2003,7 @@ impl UltraLogApp { // ======================================================================== /// Process pending IPC commands from the MCP server - fn process_ipc_commands(&mut self) { + fn process_ipc_commands(&mut self, ctx: &egui::Context) { // Collect commands first to avoid borrowing issues let mut pending_commands = Vec::new(); @@ -2003,6 +2018,12 @@ impl UltraLogApp { } } + // If we hit the batch cap, more commands may be queued; their arrival + // repaints may have coalesced into this frame, so schedule another. + if pending_commands.len() == 10 { + ctx.request_repaint(); + } + // Now process the collected commands for (command, response_sender) in pending_commands { let response = self.handle_ipc_command(command); @@ -2045,10 +2066,7 @@ impl eframe::App for UltraLogApp { self.handle_keyboard_shortcuts(ctx); // Handle IPC commands from MCP server - self.process_ipc_commands(); - - // Apply dark theme - ctx.set_visuals(egui::Visuals::dark()); + self.process_ipc_commands(ctx); // Request repaint while loading or updating (for spinner animation) if matches!(self.loading_state, LoadingState::Loading(_)) @@ -2060,11 +2078,6 @@ impl eframe::App for UltraLogApp { ctx.request_repaint(); } - // When MCP server is active, request repaint at 10Hz to poll for IPC commands - if self.ipc_server.is_some() { - ctx.request_repaint_after(std::time::Duration::from_millis(100)); - } - // Toast notifications self.render_toast(ctx); diff --git a/src/ipc/server.rs b/src/ipc/server.rs index 15d9e7d..d456943 100644 --- a/src/ipc/server.rs +++ b/src/ipc/server.rs @@ -6,11 +6,16 @@ use std::io::{BufRead, BufReader, Write}; use std::net::{TcpListener, TcpStream}; use std::sync::mpsc::{self, Receiver, Sender}; +use std::sync::Arc; use std::thread; use super::commands::{IpcCommand, IpcResponse}; use super::DEFAULT_IPC_PORT; +/// Callback invoked when a command arrives, so the GUI can wake up +/// (request a repaint) instead of polling on a timer. +pub type RepaintCallback = Arc; + /// IPC Server that listens for commands from the MCP server pub struct IpcServer { /// Receiver for incoming commands (polled by the GUI) @@ -27,8 +32,18 @@ impl IpcServer { Self::start_on_port(DEFAULT_IPC_PORT) } + /// Start a new IPC server on the default port with a repaint callback + /// invoked whenever a command arrives (event-driven GUI wake-up). + pub fn start_with_repaint(repaint: RepaintCallback) -> Result { + Self::start_inner(DEFAULT_IPC_PORT, Some(repaint)) + } + /// Start a new IPC server on a specific port pub fn start_on_port(port: u16) -> Result { + Self::start_inner(port, None) + } + + fn start_inner(port: u16, repaint: Option) -> Result { let listener = TcpListener::bind(format!("127.0.0.1:{}", port)) .map_err(|e| format!("Failed to bind to port {}: {}", port, e))?; @@ -36,7 +51,7 @@ impl IpcServer { // Spawn the listener thread thread::spawn(move || { - Self::listener_loop(listener, command_tx); + Self::listener_loop(listener, command_tx, repaint); }); tracing::info!("IPC server started on port {}", port); @@ -64,14 +79,19 @@ impl IpcServer { } /// Main listener loop (runs in background thread) - fn listener_loop(listener: TcpListener, command_tx: Sender<(IpcCommand, Sender)>) { + fn listener_loop( + listener: TcpListener, + command_tx: Sender<(IpcCommand, Sender)>, + repaint: Option, + ) { loop { match listener.accept() { Ok((stream, addr)) => { tracing::info!("MCP client connected from {}", addr); let tx = command_tx.clone(); + let repaint = repaint.clone(); thread::spawn(move || { - Self::handle_connection(stream, tx); + Self::handle_connection(stream, tx, repaint); }); } Err(e) => { @@ -86,6 +106,7 @@ impl IpcServer { fn handle_connection( mut stream: TcpStream, command_tx: Sender<(IpcCommand, Sender)>, + repaint: Option, ) { let peer_addr = stream.peer_addr().ok(); @@ -130,6 +151,11 @@ impl IpcServer { break; } + // Wake the GUI so it processes the command immediately + if let Some(repaint) = &repaint { + repaint(); + } + // Wait for the response from the GUI let response = match response_rx.recv_timeout(std::time::Duration::from_secs(30)) { Ok(resp) => resp, From 5c37458683226b9c379884e9d9d87c3f6b22ebb8 Mon Sep 17 00:00:00 2001 From: Cole Gentry Date: Thu, 16 Jul 2026 10:58:45 -0400 Subject: [PATCH 05/18] =?UTF-8?q?fix(speeduino):=20bounds-check=20v2=20MLG?= =?UTF-8?q?=20header=20before=20reads=20=E2=80=94=20truncated=20v2=20files?= =?UTF-8?q?=20panicked=20with=20index=20out=20of=20bounds;=20also=20route?= =?UTF-8?q?=20parser=20debug=20output=20through=20tracing=20instead=20of?= =?UTF-8?q?=20eprintln?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/parsers/speeduino.rs | 81 +++++++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 35 deletions(-) diff --git a/src/parsers/speeduino.rs b/src/parsers/speeduino.rs index ece77dc..f632bce 100644 --- a/src/parsers/speeduino.rs +++ b/src/parsers/speeduino.rs @@ -99,7 +99,6 @@ impl Speeduino { } // Header requires at least 22 bytes (v1): 6 (magic) + 2 (version) + 4 (timestamp) // + 2 (info_data_start) + 4 (data_begin) + 2 (record_length) + 2 (num_fields) - // v2 needs 24 bytes (info_data_start is 4 bytes), checked implicitly by field reads if data.len() < 22 { return Err("MLG file header truncated".into()); } @@ -112,9 +111,15 @@ impl Speeduino { let is_v2 = format_version == 2; let field_length = if is_v2 { 89 } else { 55 }; - eprintln!( - "DEBUG: MLG format version: {}, field_length: {}", - format_version, field_length + // v2 headers are 2 bytes longer (info_data_start is int32, not int16) + if is_v2 && data.len() < 24 { + return Err("MLG v2 file header truncated".into()); + } + + tracing::debug!( + "MLG format version: {}, field_length: {}", + format_version, + field_length ); // Read timestamp (int32, big-endian) @@ -149,9 +154,10 @@ impl Speeduino { let num_fields = u16::from_be_bytes([data[offset], data[offset + 1]]) as usize; offset += 2; - eprintln!( + tracing::debug!( "DEBUG: num_fields: {}, data_begin_index: {}", - num_fields, data_begin_index + num_fields, + data_begin_index ); // Validate bounds before parsing @@ -245,11 +251,16 @@ impl Speeduino { }); } - eprintln!("DEBUG: Parsed {} channels", channels.len()); + tracing::debug!("DEBUG: Parsed {} channels", channels.len()); for (idx, ch) in channels.iter().enumerate() { - eprintln!( + tracing::debug!( " [{}] {} ({}) type={} scale={} transform={}", - idx, ch.name, ch.unit, ch.field_type, ch.scale, ch.transform + idx, + ch.name, + ch.unit, + ch.field_type, + ch.scale, + ch.transform ); } @@ -336,7 +347,7 @@ impl Speeduino { // Check if we have enough data for this record BEFORE adding timestamp if offset + required_bytes > data.len() { - eprintln!( + tracing::debug!( "DEBUG: Not enough data for complete record at offset {} (need {}, have {})", offset, required_bytes, @@ -438,7 +449,7 @@ impl Speeduino { } else if block_type == 1 { // Marker record - skip marker message (50 bytes) if offset + 50 > data.len() { - eprintln!( + tracing::debug!( "DEBUG: Not enough data for marker block at offset {} (need 50, have {})", offset, data.len() - offset @@ -447,7 +458,7 @@ impl Speeduino { } offset += 50; } else { - eprintln!( + tracing::debug!( "DEBUG: Unknown block type {} at offset {}", block_type, offset - 3 @@ -456,19 +467,19 @@ impl Speeduino { } } - eprintln!("DEBUG: Parsed {} data records", data_records.len()); - eprintln!("DEBUG: Times vector length: {}", times.len()); + tracing::debug!("DEBUG: Parsed {} data records", data_records.len()); + tracing::debug!("DEBUG: Times vector length: {}", times.len()); // Debug: Check if timestamps are monotonically increasing if times.len() > 1 { - eprintln!("DEBUG: Timestamp analysis:"); + tracing::debug!("DEBUG: Timestamp analysis:"); let mut non_monotonic_count = 0; let mut prev_time: f64 = 0.0; for (i, &t) in times.iter().enumerate() { if i > 0 && t < prev_time { non_monotonic_count += 1; if non_monotonic_count <= 5 { - eprintln!( + tracing::debug!( " Non-monotonic at index {}: {} -> {} (delta: {:.3})", i, prev_time, @@ -480,37 +491,37 @@ impl Speeduino { prev_time = t; } if non_monotonic_count > 0 { - eprintln!(" Total non-monotonic jumps: {}", non_monotonic_count); + tracing::debug!(" Total non-monotonic jumps: {}", non_monotonic_count); } else { - eprintln!(" All timestamps are monotonically increasing"); + tracing::debug!(" All timestamps are monotonically increasing"); } // Print first 10 and last 5 timestamps - eprintln!("DEBUG: First 10 timestamps:"); + tracing::debug!("DEBUG: First 10 timestamps:"); for (i, t) in times.iter().take(10).enumerate() { - eprintln!(" [{}] {}", i, t); + tracing::debug!(" [{}] {}", i, t); } if times.len() > 15 { - eprintln!("DEBUG: Last 5 timestamps:"); + tracing::debug!("DEBUG: Last 5 timestamps:"); for (i, t) in times.iter().skip(times.len() - 5).enumerate() { - eprintln!(" [{}] {}", times.len() - 5 + i, t); + tracing::debug!(" [{}] {}", times.len() - 5 + i, t); } } } // Debug: Show first few records to verify data structure if !data_records.is_empty() { - eprintln!("DEBUG: First record (time={}):", times[0]); + tracing::debug!("DEBUG: First record (time={}):", times[0]); for (idx, val) in data_records[0].iter().enumerate() { if idx < channels.len() { - eprintln!(" [{}] {} = {:.3}", idx, channels[idx].name, val.as_f64()); + tracing::debug!(" [{}] {} = {:.3}", idx, channels[idx].name, val.as_f64()); } } if data_records.len() > 1 { - eprintln!("DEBUG: Second record (time={}):", times[1]); + tracing::debug!("DEBUG: Second record (time={}):", times[1]); for (idx, val) in data_records[1].iter().enumerate() { if idx < channels.len() { - eprintln!(" [{}] {} = {:.3}", idx, channels[idx].name, val.as_f64()); + tracing::debug!(" [{}] {} = {:.3}", idx, channels[idx].name, val.as_f64()); } } } @@ -724,7 +735,7 @@ mod tests { Ok(d) => d, Err(_) => { // Skip test if example file is not available - eprintln!("Skipping test: {} not found", file_path); + tracing::debug!("Skipping test: {} not found", file_path); return; } }; @@ -768,10 +779,10 @@ mod tests { } // Print some debug info - eprintln!("Parsed {} channels", log.channels.len()); - eprintln!("Parsed {} data records", log.data.len()); + tracing::debug!("Parsed {} channels", log.channels.len()); + tracing::debug!("Parsed {} data records", log.data.len()); if !log.times.is_empty() { - eprintln!( + tracing::debug!( "Time range: {:.3}s to {:.3}s", log.times[0], log.times[log.times.len() - 1] @@ -787,7 +798,7 @@ mod tests { Ok(d) => d, Err(_) => { // Skip test if example file is not available - eprintln!("Skipping test: {} not found", file_path); + tracing::debug!("Skipping test: {} not found", file_path); return; } }; @@ -822,8 +833,8 @@ mod tests { ); } - eprintln!("Parsed {} channels from rusEFI log", log.channels.len()); - eprintln!("Parsed {} data records", log.data.len()); + tracing::debug!("Parsed {} channels from rusEFI log", log.channels.len()); + tracing::debug!("Parsed {} data records", log.data.len()); } #[test] @@ -841,7 +852,7 @@ mod tests { let data = match std::fs::read(file_path) { Ok(d) => d, Err(_) => { - eprintln!("Skipping {}: file not found", file_path); + tracing::debug!("Skipping {}: file not found", file_path); continue; } }; @@ -862,7 +873,7 @@ mod tests { file_path, avg_dt ); - eprintln!( + tracing::debug!( "{}: {} records, total {:.3}s, avg dt {:.4}s", file_path, log.times.len(), From caa67e58cc225f1b4d0f351d2cefadb2a2786f06 Mon Sep 17 00:00:00 2001 From: Cole Gentry Date: Thu, 16 Jul 2026 11:01:14 -0400 Subject: [PATCH 06/18] =?UTF-8?q?fix(app):=20reindex=20analysis=5Fresults?= =?UTF-8?q?=20when=20a=20file=20is=20removed=20=E2=80=94=20cached=20result?= =?UTF-8?q?s=20attached=20to=20the=20wrong=20file=20after=20deletion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/app.rs b/src/app.rs index 7d0f520..89d3588 100644 --- a/src/app.rs +++ b/src/app.rs @@ -976,6 +976,18 @@ impl UltraLogApp { } self.file_computed_channels = new_computed_channels; + // Clear analysis results for this file and update indices + self.analysis_results.remove(&index); + let mut new_analysis_results = HashMap::new(); + for (key, value) in self.analysis_results.drain() { + if key > index { + new_analysis_results.insert(key - 1, value); + } else { + new_analysis_results.insert(key, value); + } + } + self.analysis_results = new_analysis_results; + // Update file indices for remaining tabs and their channels for tab in &mut self.tabs { if tab.file_index > index { From 16fbe0fbf79f3e6c184cfa1d74adc07df970435f Mon Sep 17 00:00:00 2001 From: Cole Gentry Date: Thu, 16 Jul 2026 11:01:14 -0400 Subject: [PATCH 07/18] =?UTF-8?q?fix(normalize):=20remove=20ambiguous=20Sp?= =?UTF-8?q?eed=20alias=20from=20RPM=20=E2=80=94=20it=20collided=20with=20V?= =?UTF-8?q?ehicle=20Speed=20in=20the=20reverse=20map,=20making=20normaliza?= =?UTF-8?q?tion=20nondeterministic=20across=20launches?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/normalize.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/normalize.rs b/src/normalize.rs index 441f2cd..0ddc4e6 100644 --- a/src/normalize.rs +++ b/src/normalize.rs @@ -235,7 +235,6 @@ static NORMALIZATION_MAP: LazyLock>> = vec![ "RPM", "rpm", - "Speed", "PCS RPM4", "Engine RPM4", "RPM_INC_RPM", From aad3c1ca4ff7e8c3dbb50c0d9a18c860939ee01a Mon Sep 17 00:00:00 2001 From: Cole Gentry Date: Thu, 16 Jul 2026 11:01:15 -0400 Subject: [PATCH 08/18] =?UTF-8?q?fix(haltech):=20preserve=20column=20align?= =?UTF-8?q?ment=20when=20a=20field=20fails=20to=20parse=20=E2=80=94=20unpa?= =?UTF-8?q?rseable=20values=20previously=20compacted=20the=20row=20left,?= =?UTF-8?q?=20misaligning=20every=20later=20column;=20now=20last-known-val?= =?UTF-8?q?ue=20is=20substituted=20like=20the=20ECUMaster=20parser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/parsers/haltech.rs | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/parsers/haltech.rs b/src/parsers/haltech.rs index 596b7d6..ddd1c96 100644 --- a/src/parsers/haltech.rs +++ b/src/parsers/haltech.rs @@ -366,8 +366,10 @@ impl Parseable for Haltech { } // Phase 2: Parse data rows in parallel - // Each row is parsed independently, returning (timestamp, values) - let parsed_rows: Vec<(f64, Vec)> = data_lines + // Each row is parsed independently, returning (timestamp, values). + // Unparseable fields (blank, "N/A", text gears) become None so the + // row keeps positional alignment with the channel list. + let parsed_rows: Vec<(f64, Vec>)> = data_lines .par_iter() .filter_map(|line| { let parts: Vec<&str> = line.split(',').collect(); @@ -380,10 +382,10 @@ impl Parseable for Haltech { let timestamp_secs = Self::parse_timestamp(timestamp_str)?; // Parse remaining values and apply unit conversions - let values: Vec = parts[1..] + let values: Vec> = parts[1..] .iter() .enumerate() - .filter_map(|(idx, v)| { + .map(|(idx, v)| { let v = v.trim(); let raw_value: f64 = v.parse().ok()?; @@ -405,7 +407,9 @@ impl Parseable for Haltech { }) .collect(); - // Phase 3: Post-process results (sequential for ordering) + // Phase 3: Post-process results (sequential for ordering). + // Fill unparseable fields with the last known value for that column + // (0.0 before the first valid sample), matching the ECUMaster parser. let data_count = parsed_rows.len(); let mut times: Vec = Vec::with_capacity(data_count); let mut data: Vec> = Vec::with_capacity(data_count); @@ -413,10 +417,24 @@ impl Parseable for Haltech { if !parsed_rows.is_empty() { // First timestamp is the base for relative times let first_timestamp = parsed_rows[0].0; + let mut last_values: Vec = vec![Value::Float(0.0); channels.len()]; for (timestamp, values) in parsed_rows { + let row: Vec = values + .into_iter() + .enumerate() + .map(|(idx, value)| match value { + Some(v) => { + if let Some(slot) = last_values.get_mut(idx) { + *slot = v; + } + v + } + None => last_values.get(idx).copied().unwrap_or(Value::Float(0.0)), + }) + .collect(); times.push(timestamp - first_timestamp); - data.push(values); + data.push(row); } } From bb7a118fd30d0ae72b14d28ae685f87e5f28714c Mon Sep 17 00:00:00 2001 From: Cole Gentry Date: Thu, 16 Jul 2026 11:04:38 -0400 Subject: [PATCH 09/18] =?UTF-8?q?feat(settings):=20persist=20unit=20prefer?= =?UTF-8?q?ences,=20font=20scale,=20colorblind=20mode,=20field=20normaliza?= =?UTF-8?q?tion,=20cursor=20tracking,=20auto-update=20check,=20and=20custo?= =?UTF-8?q?m=20channel=20mappings=20=E2=80=94=20previously=20all=20of=20th?= =?UTF-8?q?ese=20silently=20reset=20every=20launch;=20synced=20via=20efram?= =?UTF-8?q?e's=20auto-save=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.rs | 33 +++++++++++++++++++++++++++++++++ src/settings.rs | 37 ++++++++++++++++++++++++++++++++++++- src/state.rs | 2 +- src/units.rs | 21 +++++++++++---------- 4 files changed, 81 insertions(+), 12 deletions(-) diff --git a/src/app.rs b/src/app.rs index 89d3588..d70a939 100644 --- a/src/app.rs +++ b/src/app.rs @@ -290,6 +290,13 @@ impl UltraLogApp { scroll_to_zoom: user_settings.scroll_to_zoom, show_grid: user_settings.show_grid, grid_opacity: user_settings.grid_opacity, + unit_preferences: user_settings.unit_preferences.clone(), + font_scale: user_settings.font_scale, + color_blind_mode: user_settings.color_blind_mode, + field_normalization: user_settings.field_normalization, + cursor_tracking: user_settings.cursor_tracking, + auto_check_updates: user_settings.auto_check_updates, + custom_normalizations: user_settings.custom_normalizations.clone(), ..Self::default() }; @@ -2049,6 +2056,32 @@ impl UltraLogApp { // ============================================================================ impl eframe::App for UltraLogApp { + /// Called by eframe on the auto-save interval (~30s) and at shutdown. + /// Syncs live preference fields into UserSettings and persists on change, + /// so toggles made anywhere in the UI survive a restart. + fn save(&mut self, _storage: &mut dyn eframe::Storage) { + let settings = UserSettings { + version: self.user_settings.version, + language: self.language, + scroll_to_zoom: self.scroll_to_zoom, + show_grid: self.show_grid, + grid_opacity: self.grid_opacity, + unit_preferences: self.unit_preferences.clone(), + font_scale: self.font_scale, + color_blind_mode: self.color_blind_mode, + field_normalization: self.field_normalization, + cursor_tracking: self.cursor_tracking, + auto_check_updates: self.auto_check_updates, + custom_normalizations: self.custom_normalizations.clone(), + }; + if settings != self.user_settings { + self.user_settings = settings; + if let Err(e) = self.user_settings.save() { + tracing::warn!("Failed to save settings: {}", e); + } + } + } + fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { // Exit if update installation requires it (updater script is waiting) if self.should_exit_for_update { diff --git a/src/settings.rs b/src/settings.rs index 0c1d51a..c4847c9 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -3,12 +3,15 @@ //! This module handles loading and saving user preferences across sessions. use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use std::path::PathBuf; use crate::i18n::Language; +use crate::state::FontScale; +use crate::units::UnitPreferences; /// User settings that persist across sessions -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct UserSettings { /// Settings file version for migration support #[serde(default = "default_version")] @@ -26,6 +29,27 @@ pub struct UserSettings { /// before egui_plot's distance-based fade #[serde(default = "default_grid_opacity")] pub grid_opacity: u8, + /// Display unit selections (temperature, pressure, speed, ...) + #[serde(default)] + pub unit_preferences: UnitPreferences, + /// UI font scale + #[serde(default)] + pub font_scale: FontScale, + /// Use the colorblind-friendly chart palette + #[serde(default)] + pub color_blind_mode: bool, + /// Normalize ECU-specific channel names to standardized names + #[serde(default = "default_true")] + pub field_normalization: bool, + /// Keep the chart view locked to the cursor during playback + #[serde(default = "default_true")] + pub cursor_tracking: bool, + /// Check for updates on startup + #[serde(default = "default_true")] + pub auto_check_updates: bool, + /// User-defined channel-name normalization mappings (source -> display) + #[serde(default)] + pub custom_normalizations: HashMap, } fn default_version() -> u32 { @@ -40,6 +64,10 @@ fn default_grid_opacity() -> u8 { 255 } +fn default_true() -> bool { + true +} + impl Default for UserSettings { fn default() -> Self { Self { @@ -48,6 +76,13 @@ impl Default for UserSettings { scroll_to_zoom: false, show_grid: default_show_grid(), grid_opacity: default_grid_opacity(), + unit_preferences: UnitPreferences::default(), + font_scale: FontScale::default(), + color_blind_mode: false, + field_normalization: default_true(), + cursor_tracking: default_true(), + auto_check_updates: default_true(), + custom_normalizations: HashMap::new(), } } } diff --git a/src/state.rs b/src/state.rs index 2c43b85..8801ed8 100644 --- a/src/state.rs +++ b/src/state.rs @@ -270,7 +270,7 @@ impl ActivePanel { } /// Font scale preference for UI elements -#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)] +#[derive(Clone, Copy, PartialEq, Eq, Default, Debug, serde::Serialize, serde::Deserialize)] pub enum FontScale { /// Smaller fonts (0.85x) Small, diff --git a/src/units.rs b/src/units.rs index 1e50e86..17d88e2 100644 --- a/src/units.rs +++ b/src/units.rs @@ -3,8 +3,9 @@ //! This module provides user-configurable unit preferences for displaying //! ECU log data in various measurement systems (metric, imperial, etc.). +use serde::{Deserialize, Serialize}; /// Temperature unit preference -#[derive(Clone, Copy, Debug, Default, PartialEq)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)] pub enum TemperatureUnit { Kelvin, #[default] @@ -32,7 +33,7 @@ impl TemperatureUnit { } /// Pressure unit preference -#[derive(Clone, Copy, Debug, Default, PartialEq)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)] pub enum PressureUnit { #[default] KPa, @@ -60,7 +61,7 @@ impl PressureUnit { } /// Speed unit preference -#[derive(Clone, Copy, Debug, Default, PartialEq)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)] pub enum SpeedUnit { #[default] KmH, @@ -85,7 +86,7 @@ impl SpeedUnit { } /// Distance unit preference -#[derive(Clone, Copy, Debug, Default, PartialEq)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)] pub enum DistanceUnit { #[default] Kilometers, @@ -110,7 +111,7 @@ impl DistanceUnit { } /// Fuel economy unit preference -#[derive(Clone, Copy, Debug, Default, PartialEq)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)] pub enum FuelEconomyUnit { #[default] LPer100Km, @@ -150,7 +151,7 @@ impl FuelEconomyUnit { } /// Volume unit preference -#[derive(Clone, Copy, Debug, Default, PartialEq)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)] pub enum VolumeUnit { #[default] Liters, @@ -175,7 +176,7 @@ impl VolumeUnit { } /// Flow rate unit preference -#[derive(Clone, Copy, Debug, Default, PartialEq)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)] pub enum FlowUnit { #[default] CcPerMin, @@ -201,7 +202,7 @@ impl FlowUnit { } /// Acceleration unit preference -#[derive(Clone, Copy, Debug, Default, PartialEq)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)] pub enum AccelerationUnit { #[default] MPerS2, @@ -230,7 +231,7 @@ impl AccelerationUnit { /// ECU logs may output mixture data as either AFR (Air Fuel Ratio, e.g. 14.7 for stoich gasoline) /// or Lambda (normalized ratio, e.g. 1.0 for stoich). This preference controls which format /// is displayed regardless of what the source ECU outputs. -#[derive(Clone, Copy, Debug, Default, PartialEq)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)] pub enum AfrLambdaUnit { #[default] AFR, @@ -266,7 +267,7 @@ impl AfrLambdaUnit { } /// User preferences for display units -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct UnitPreferences { pub temperature: TemperatureUnit, pub pressure: PressureUnit, From 6865c1ebbecc4002f98a48130c79ffd4ec217534 Mon Sep 17 00:00:00 2001 From: Cole Gentry Date: Thu, 16 Jul 2026 11:12:26 -0400 Subject: [PATCH 10/18] =?UTF-8?q?perf(chart):=20cache=20downsampled=20char?= =?UTF-8?q?t=20points=20per=20channel=20keyed=20on=20the=20quantized=20vie?= =?UTF-8?q?wport=20=E2=80=94=20the=20O(samples)=20downsample=20previously?= =?UTF-8?q?=20re-ran=20for=20every=20channel=20every=20frame;=20now=20it?= =?UTF-8?q?=20only=20recomputes=20when=20the=20anchored=20bucket=20grid=20?= =?UTF-8?q?shifts.=20Cache=20is=20invalidated=20on=20file=20removal=20and?= =?UTF-8?q?=20computed-channel=20deletion=20(minmax=20cache=20too,=20same?= =?UTF-8?q?=20index-shift=20staleness)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.rs | 21 ++++++- src/ipc/handler.rs | 4 ++ src/state.rs | 22 +++++++ src/ui/chart.rs | 119 +++++++++++++++++++++-------------- tests/core/settings_tests.rs | 4 ++ 5 files changed, 119 insertions(+), 51 deletions(-) diff --git a/src/app.rs b/src/app.rs index d70a939..5e7d9f6 100644 --- a/src/app.rs +++ b/src/app.rs @@ -26,9 +26,10 @@ use crate::parsers::{ }; use crate::settings::UserSettings; use crate::state::{ - ActivePanel, ActiveTool, CacheKey, FontScale, LoadResult, LoadedFile, LoadingState, PlotArea, - ScatterPlotConfig, ScatterPlotState, SelectedChannel, Tab, ToastType, CHART_COLORS, - COLORBLIND_COLORS, MAX_CHANNELS, MAX_CHANNELS_PER_PLOT, MAX_TOTAL_CHANNELS, MIN_PLOT_HEIGHT, + ActivePanel, ActiveTool, CacheKey, DownsampleCache, FontScale, LoadResult, LoadedFile, + LoadingState, PlotArea, ScatterPlotConfig, ScatterPlotState, SelectedChannel, Tab, ToastType, + CHART_COLORS, COLORBLIND_COLORS, MAX_CHANNELS, MAX_CHANNELS_PER_PLOT, MAX_TOTAL_CHANNELS, + MIN_PLOT_HEIGHT, }; use crate::units::UnitPreferences; use crate::updater::{DownloadResult, UpdateCheckResult, UpdateState}; @@ -58,6 +59,12 @@ pub struct UltraLogApp { /// with zoom level instead of being fixed at MAX_CHART_POINTS over the /// full log range. Keyed by plot_area_id (0 in single-plot mode). pub(crate) chart_last_x_bounds: HashMap, + /// Cached downsampled chart points per (file_index, channel_index), + /// tagged with the viewport key they were computed for. Avoids re-running + /// the O(samples) downsample every frame; recomputed only when the + /// quantized viewport changes. Must be cleared whenever underlying + /// channel data changes (file removal, computed-channel edits). + pub(crate) downsample_cache: DownsampleCache, /// Current cursor position in seconds (timeline feature) pub(crate) cursor_time: Option, /// Total time range across all loaded files (min, max) @@ -189,6 +196,7 @@ impl Default for UltraLogApp { loading_state: LoadingState::Idle, minmax_cache: HashMap::new(), chart_last_x_bounds: HashMap::new(), + downsample_cache: DownsampleCache::new(), cursor_time: None, time_range: None, cursor_record: None, @@ -971,6 +979,9 @@ impl UltraLogApp { // removed picks fresh bounds from whatever data remains. self.chart_last_x_bounds.clear(); + // File indices shift, so cached downsampled points are stale. + self.downsample_cache.clear(); + // Clear computed channels for this file and update indices self.file_computed_channels.remove(&index); let mut new_computed_channels = HashMap::new(); @@ -1647,6 +1658,10 @@ impl UltraLogApp { if let Some(channels) = self.file_computed_channels.get_mut(&file_idx) { if index < channels.len() { channels.remove(index); + // Later computed channels shift down one index, so + // cached downsampled points and min/max no longer line up. + self.downsample_cache.clear(); + self.minmax_cache.clear(); } } } diff --git a/src/ipc/handler.rs b/src/ipc/handler.rs index 8663752..3f2baaa 100644 --- a/src/ipc/handler.rs +++ b/src/ipc/handler.rs @@ -473,6 +473,10 @@ impl UltraLogApp { .position(|c| c.name().eq_ignore_ascii_case(name)) { computed.remove(pos); + // Later computed channels shift down one index, so + // cached downsampled points and min/max no longer line up. + self.downsample_cache.clear(); + self.minmax_cache.clear(); } } } diff --git a/src/state.rs b/src/state.rs index 8801ed8..4852ad3 100644 --- a/src/state.rs +++ b/src/state.rs @@ -204,6 +204,28 @@ pub struct CacheKey { pub plot_area_id: usize, } +/// Cached downsampled chart points per (file_index, channel_index), tagged +/// with the viewport key they were computed for. +pub type DownsampleCache = + std::collections::HashMap<(usize, usize), (DownsampleViewKey, Vec<[f64; 2]>)>; + +/// Identifies which viewport a cached set of downsampled chart points was +/// computed for. The bucketed downsampler anchors bucket boundaries at +/// multiples of `bucket_size` from t=0, so its output is fully determined +/// by the first bucket index and the bucket width. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum DownsampleViewKey { + /// Full-range LTTB downsample (no viewport bounds available) + Full, + /// Anchored min/max-per-bucket downsample over a padded viewport + Bucketed { + /// Index of the first bucket (floor of viewport start / bucket size) + k_lo: i64, + /// Bit pattern of the f64 bucket width (bitwise-comparable) + bucket_bits: u64, + }, +} + // ============================================================================ // Tool/View Types // ============================================================================ diff --git a/src/ui/chart.rs b/src/ui/chart.rs index a8d6b98..6409b8e 100644 --- a/src/ui/chart.rs +++ b/src/ui/chart.rs @@ -7,8 +7,8 @@ use rust_i18n::t; use crate::app::UltraLogApp; use crate::normalize::normalize_channel_name_with_custom; use crate::state::{ - PlotArea, SelectedChannel, CHART_COLORS, COLORBLIND_COLORS, MAX_CHART_POINTS, MIN_PLOT_HEIGHT, - PLOT_RESIZE_HANDLE_HEIGHT, + DownsampleViewKey, PlotArea, SelectedChannel, CHART_COLORS, COLORBLIND_COLORS, + MAX_CHART_POINTS, MIN_PLOT_HEIGHT, PLOT_RESIZE_HANDLE_HEIGHT, }; /// Sensitivity multiplier for scroll-to-zoom (higher = faster zoom per scroll tick). @@ -885,9 +885,38 @@ impl UltraLogApp { return None; } - let full_lttb = || Self::downsample_lttb(times, data, MAX_CHART_POINTS); - let downsampled = match viewport { + // Derive the cache key for this viewport. The bucketed downsampler + // below is anchored to a fixed grid, so its output only changes when + // the first bucket index or the bucket width changes — steady frames + // (idle, cursor moves within a bucket) reuse the cached points. + let n_buckets = (MAX_CHART_POINTS / 2).max(1); + let view_key = match viewport { Some((vmin, vmax)) if vmax > vmin => { + let pad = (vmax - vmin) * 0.1; + let padded_span = (vmax - vmin) + 2.0 * pad; + let bucket_size = padded_span / n_buckets as f64; + if bucket_size <= 0.0 { + DownsampleViewKey::Full + } else { + let raw_lo = vmin - pad; + DownsampleViewKey::Bucketed { + k_lo: (raw_lo / bucket_size).floor() as i64, + bucket_bits: bucket_size.to_bits(), + } + } + } + _ => DownsampleViewKey::Full, + }; + + if let Some((cached_key, points)) = self.downsample_cache.get(&(file_index, channel_index)) + { + if *cached_key == view_key { + return Some(points.clone()); + } + } + + let downsampled = match view_key { + DownsampleViewKey::Bucketed { k_lo, bucket_bits } => { // Anchored min/max-per-bucket downsampling. Bucket // boundaries are at multiples of `bucket_size` from t=0, // so during cursor-tracked playback samples slide through @@ -895,65 +924,59 @@ impl UltraLogApp { // Without this anchoring, LTTB-by-index re-selects a // different "best peak" per frame and the curve jitters // at far zoom-out. - let pad = (vmax - vmin) * 0.1; - let padded_span = (vmax - vmin) + 2.0 * pad; - let n_buckets = (MAX_CHART_POINTS / 2).max(1); - let bucket_size = padded_span / n_buckets as f64; - if bucket_size <= 0.0 { - full_lttb() - } else { - let raw_lo = vmin - pad; - let k_lo = (raw_lo / bucket_size).floor() as i64; - let mut points: Vec<[f64; 2]> = Vec::with_capacity(MAX_CHART_POINTS); - let mut idx = times.partition_point(|&t| t < k_lo as f64 * bucket_size); - for k in 0..n_buckets as i64 { - let bucket_end = (k_lo + k + 1) as f64 * bucket_size; - let mut end_idx = idx; - while end_idx < times.len() && times[end_idx] < bucket_end { - end_idx += 1; - } - if end_idx > idx { - let mut min_i = idx; - let mut max_i = idx; - for i in idx..end_idx { - if data[i] < data[min_i] { - min_i = i; - } - if data[i] > data[max_i] { - max_i = i; - } + let bucket_size = f64::from_bits(bucket_bits); + let mut points: Vec<[f64; 2]> = Vec::with_capacity(MAX_CHART_POINTS); + let mut idx = times.partition_point(|&t| t < k_lo as f64 * bucket_size); + for k in 0..n_buckets as i64 { + let bucket_end = (k_lo + k + 1) as f64 * bucket_size; + let mut end_idx = idx; + while end_idx < times.len() && times[end_idx] < bucket_end { + end_idx += 1; + } + if end_idx > idx { + let mut min_i = idx; + let mut max_i = idx; + for i in idx..end_idx { + if data[i] < data[min_i] { + min_i = i; } - if min_i == max_i { - points.push([times[min_i], data[min_i]]); - } else if min_i < max_i { - points.push([times[min_i], data[min_i]]); - points.push([times[max_i], data[max_i]]); - } else { - points.push([times[max_i], data[max_i]]); - points.push([times[min_i], data[min_i]]); + if data[i] > data[max_i] { + max_i = i; } } - idx = end_idx; + if min_i == max_i { + points.push([times[min_i], data[min_i]]); + } else if min_i < max_i { + points.push([times[min_i], data[min_i]]); + points.push([times[max_i], data[max_i]]); + } else { + points.push([times[max_i], data[max_i]]); + points.push([times[min_i], data[min_i]]); + } } - points + idx = end_idx; } + points } - _ => full_lttb(), + DownsampleViewKey::Full => Self::downsample_lttb(times, data, MAX_CHART_POINTS), }; let range = (max_y - min_y).abs(); // Constant channels (range ≈ 0) get parked at the middle of the // overlay strip so they remain visible instead of pinning to the // bottom edge — matches the prior `normalize_points` behavior. - if range < f64::EPSILON { - return Some(downsampled.into_iter().map(|p| [p[0], 0.5]).collect()); - } - Some( + let points: Vec<[f64; 2]> = if range < f64::EPSILON { + downsampled.into_iter().map(|p| [p[0], 0.5]).collect() + } else { downsampled .into_iter() .map(|p| [p[0], (p[1] - min_y) / range]) - .collect(), - ) + .collect() + }; + + self.downsample_cache + .insert((file_index, channel_index), (view_key, points.clone())); + Some(points) } /// Normalize values to 0-1 range for overlay display diff --git a/tests/core/settings_tests.rs b/tests/core/settings_tests.rs index 49e038b..cb7895b 100644 --- a/tests/core/settings_tests.rs +++ b/tests/core/settings_tests.rs @@ -145,6 +145,7 @@ fn test_settings_grid_fields_roundtrip() { scroll_to_zoom: false, show_grid: false, grid_opacity: 64, + ..UserSettings::default() }; let json = serde_json::to_string(&original).unwrap(); @@ -162,6 +163,7 @@ fn test_settings_roundtrip() { scroll_to_zoom: false, show_grid: true, grid_opacity: 255, + ..UserSettings::default() }; let json = serde_json::to_string(&original).unwrap(); @@ -180,6 +182,7 @@ fn test_settings_roundtrip_all_languages() { scroll_to_zoom: false, show_grid: true, grid_opacity: 255, + ..UserSettings::default() }; let json = serde_json::to_string(&settings).unwrap(); @@ -281,6 +284,7 @@ fn test_settings_clone() { scroll_to_zoom: false, show_grid: true, grid_opacity: 255, + ..UserSettings::default() }; let cloned = original.clone(); From 3d7be8a0a7e5c5541e680b443272a540fc9eaaf1 Mon Sep 17 00:00:00 2001 From: Cole Gentry Date: Thu, 16 Jul 2026 11:17:00 -0400 Subject: [PATCH 11/18] =?UTF-8?q?fix(app):=20queue=20file=20loads=20instea?= =?UTF-8?q?d=20of=20dropping=20in-flight=20results=20=E2=80=94=20opening?= =?UTF-8?q?=20a=20second=20file=20while=20one=20was=20loading=20silently?= =?UTF-8?q?=20lost=20the=20first;=20multi-file=20drag-drop=20now=20loads?= =?UTF-8?q?=20every=20file=20instead=20of=20only=20the=20first.=20Also=20a?= =?UTF-8?q?dd=20documented-but-missing=20shortcuts:=20Esc=20stops=20playba?= =?UTF-8?q?ck,=20Cmd+E=20exports=20the=20current=20view=20as=20PNG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.rs | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/src/app.rs b/src/app.rs index 5e7d9f6..3fcb0e4 100644 --- a/src/app.rs +++ b/src/app.rs @@ -50,6 +50,10 @@ pub struct UltraLogApp { last_drop_time: Option, /// Channel for receiving loaded files from background thread load_receiver: Option>, + /// Files queued to load once the current background load finishes. + /// Starting a second load while one is in flight would drop the first + /// receiver and silently lose its result. + pending_loads: Vec, /// Current loading state pub(crate) loading_state: LoadingState, /// Cache for channel min/max values (avoids O(n) scans) @@ -193,6 +197,7 @@ impl Default for UltraLogApp { toast_message: None, last_drop_time: None, load_receiver: None, + pending_loads: Vec::new(), loading_state: LoadingState::Idle, minmax_cache: HashMap::new(), chart_last_x_bounds: HashMap::new(), @@ -377,6 +382,15 @@ impl UltraLogApp { return; } + // A load is already in flight — queue this one instead of + // overwriting the receiver (which would lose the first result). + if self.load_receiver.is_some() { + if !self.pending_loads.contains(&path) { + self.pending_loads.push(path); + } + return; + } + let filename = path .file_name() .map(|n| n.to_string_lossy().to_string()) @@ -740,6 +754,12 @@ impl UltraLogApp { } self.load_receiver = None; self.loading_state = LoadingState::Idle; + + // Start the next queued load, if any + if !self.pending_loads.is_empty() { + let next = self.pending_loads.remove(0); + self.start_loading_file(next); + } } } @@ -1863,8 +1883,8 @@ impl UltraLogApp { if !dropped_files.is_empty() { self.last_drop_time = Some(std::time::Instant::now()); - // Only load first file for now (could queue multiple) - if let Some(path) = dropped_files.into_iter().next() { + // First file loads immediately; the rest queue behind it + for path in dropped_files { self.start_loading_file(path); } } @@ -2013,6 +2033,22 @@ impl UltraLogApp { return; } + // Escape - stop playback + if i.key_pressed(egui::Key::Escape) { + self.stop_playback(); + return; + } + + // ⌘E - export current view as PNG + if cmd && i.key_pressed(egui::Key::E) { + match self.active_tool { + ActiveTool::LogViewer => self.export_chart_png(), + ActiveTool::ScatterPlot => self.export_scatter_plot_png(), + ActiveTool::Histogram => self.export_histogram_png(), + } + return; + } + // Spacebar to toggle play/pause if i.key_pressed(egui::Key::Space) { self.is_playing = !self.is_playing; From f186ecf406387b0edf40e245f8845d071b5665a0 Mon Sep 17 00:00:00 2001 From: Cole Gentry Date: Thu, 16 Jul 2026 11:17:00 -0400 Subject: [PATCH 12/18] =?UTF-8?q?fix(adapters):=20sanitize=20API-supplied?= =?UTF-8?q?=20vendor/id=20into=20a=20filename-safe=20slug=20before=20writi?= =?UTF-8?q?ng=20spec=20cache=20files=20=E2=80=94=20a=20hostile=20or=20corr?= =?UTF-8?q?upt=20API=20response=20could=20otherwise=20escape=20the=20cache?= =?UTF-8?q?=20directory=20via=20path=20separators?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/adapters/cache.rs | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/adapters/cache.rs b/src/adapters/cache.rs index 33ccf63..bf70ef9 100644 --- a/src/adapters/cache.rs +++ b/src/adapters/cache.rs @@ -203,6 +203,26 @@ pub fn load_cached_adapters() -> Option> { } } +/// Build a safe cache filename from API-supplied vendor/id fields. +/// Restricts to a filename-safe slug so a hostile or corrupted API response +/// can't escape the cache directory (e.g. `../`) via `Path::join`. +fn spec_cache_filename(vendor: &str, id: &str) -> String { + let slug = |s: &str| -> String { + s.chars() + .map(|c| { + if c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' { + c + } else { + '_' + } + }) + .collect::() + .trim_matches('.') + .to_string() + }; + format!("{}-{}.json", slug(vendor), slug(id)) +} + /// Save adapters to cache pub fn save_adapters_to_cache(adapters: &[AdapterSpec]) -> Result<(), CacheError> { let cache_dir = ensure_cache_dirs()?; @@ -217,7 +237,7 @@ pub fn save_adapters_to_cache(adapters: &[AdapterSpec]) -> Result<(), CacheError // Save each adapter for adapter in adapters { - let filename = format!("{}-{}.json", adapter.vendor, adapter.id); + let filename = spec_cache_filename(&adapter.vendor, &adapter.id); let path = adapters_dir.join(&filename); let content = serde_json::to_string_pretty(adapter) @@ -289,7 +309,7 @@ pub fn save_protocols_to_cache(protocols: &[ProtocolSpec]) -> Result<(), CacheEr // Save each protocol for protocol in protocols { - let filename = format!("{}-{}.json", protocol.vendor, protocol.id); + let filename = spec_cache_filename(&protocol.vendor, &protocol.id); let path = protocols_dir.join(&filename); let content = serde_json::to_string_pretty(protocol) From 2e227c4eba8bb72bfdef80b4010890d78bb91b2e Mon Sep 17 00:00:00 2001 From: Cole Gentry Date: Thu, 16 Jul 2026 11:17:00 -0400 Subject: [PATCH 13/18] =?UTF-8?q?fix(haltech):=20handle=20midnight=20rollo?= =?UTF-8?q?ver=20in=20wall-clock=20timestamps=20=E2=80=94=20sessions=20cro?= =?UTF-8?q?ssing=20midnight=20produced=20non-monotonic=20times,=20breaking?= =?UTF-8?q?=20the=20binary=20search=20used=20by=20time-shifted=20computed?= =?UTF-8?q?=20channels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/parsers/haltech.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/parsers/haltech.rs b/src/parsers/haltech.rs index ddd1c96..1151455 100644 --- a/src/parsers/haltech.rs +++ b/src/parsers/haltech.rs @@ -418,8 +418,20 @@ impl Parseable for Haltech { // First timestamp is the base for relative times let first_timestamp = parsed_rows[0].0; let mut last_values: Vec = vec![Value::Float(0.0); channels.len()]; + // Timestamps are wall-clock time-of-day, so a session crossing + // midnight wraps from ~86400 back to 0. Accumulate a 24h offset + // on each backwards jump (>1s guard skips timestamp jitter) to + // keep `times` monotonic — computed-channel time-shift lookups + // binary-search this vector and require sorted order. + let mut prev_raw = f64::NEG_INFINITY; + let mut day_offset = 0.0_f64; for (timestamp, values) in parsed_rows { + if timestamp + 1.0 < prev_raw { + day_offset += 86_400.0; + } + prev_raw = timestamp; + let timestamp = timestamp + day_offset; let row: Vec = values .into_iter() .enumerate() From 6b63439c331458b895ee43a6296f03f05455d478 Mon Sep 17 00:00:00 2001 From: Cole Gentry Date: Thu, 16 Jul 2026 11:20:45 -0400 Subject: [PATCH 14/18] =?UTF-8?q?perf(scatter):=20cache=20the=20heatmap=20?= =?UTF-8?q?histogram=20per=20axis=20selection=20=E2=80=94=20previously=20t?= =?UTF-8?q?wo=20full=20channel=20copies=20plus=20a=20512x512=20histogram?= =?UTF-8?q?=20rebuild=20ran=20every=20frame;=20now=20only=20on=20selection?= =?UTF-8?q?=20change?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.rs | 13 ++++-- src/state.rs | 15 +++++++ src/ui/scatter_plot.rs | 93 ++++++++++++++++++++++++++++++------------ 3 files changed, 91 insertions(+), 30 deletions(-) diff --git a/src/app.rs b/src/app.rs index 3fcb0e4..9b604f2 100644 --- a/src/app.rs +++ b/src/app.rs @@ -27,9 +27,9 @@ use crate::parsers::{ use crate::settings::UserSettings; use crate::state::{ ActivePanel, ActiveTool, CacheKey, DownsampleCache, FontScale, LoadResult, LoadedFile, - LoadingState, PlotArea, ScatterPlotConfig, ScatterPlotState, SelectedChannel, Tab, ToastType, - CHART_COLORS, COLORBLIND_COLORS, MAX_CHANNELS, MAX_CHANNELS_PER_PLOT, MAX_TOTAL_CHANNELS, - MIN_PLOT_HEIGHT, + LoadingState, PlotArea, ScatterHistogramCache, ScatterPlotConfig, ScatterPlotState, + SelectedChannel, Tab, ToastType, CHART_COLORS, COLORBLIND_COLORS, MAX_CHANNELS, + MAX_CHANNELS_PER_PLOT, MAX_TOTAL_CHANNELS, MIN_PLOT_HEIGHT, }; use crate::units::UnitPreferences; use crate::updater::{DownloadResult, UpdateCheckResult, UpdateState}; @@ -69,6 +69,8 @@ pub struct UltraLogApp { /// quantized viewport changes. Must be cleared whenever underlying /// channel data changes (file removal, computed-channel edits). pub(crate) downsample_cache: DownsampleCache, + /// Cached scatter-plot heatmap histograms. Cleared on file removal. + pub(crate) scatter_histogram_cache: ScatterHistogramCache, /// Current cursor position in seconds (timeline feature) pub(crate) cursor_time: Option, /// Total time range across all loaded files (min, max) @@ -202,6 +204,7 @@ impl Default for UltraLogApp { minmax_cache: HashMap::new(), chart_last_x_bounds: HashMap::new(), downsample_cache: DownsampleCache::new(), + scatter_histogram_cache: ScatterHistogramCache::new(), cursor_time: None, time_range: None, cursor_record: None, @@ -999,8 +1002,10 @@ impl UltraLogApp { // removed picks fresh bounds from whatever data remains. self.chart_last_x_bounds.clear(); - // File indices shift, so cached downsampled points are stale. + // File indices shift, so cached downsampled points and scatter + // histograms are stale. self.downsample_cache.clear(); + self.scatter_histogram_cache.clear(); // Clear computed channels for this file and update indices self.file_computed_channels.remove(&index); diff --git a/src/state.rs b/src/state.rs index 4852ad3..9a6e4e6 100644 --- a/src/state.rs +++ b/src/state.rs @@ -209,6 +209,21 @@ pub struct CacheKey { pub type DownsampleCache = std::collections::HashMap<(usize, usize), (DownsampleViewKey, Vec<[f64; 2]>)>; +/// Precomputed 2D histogram for the scatter-plot heatmap. Channel data is +/// immutable once a file is loaded, so this only needs rebuilding when the +/// axis selection changes (cache is cleared on file removal). +pub struct ScatterHistogram { + pub bins: Vec>, + pub max_hits: u32, + pub x_min: f64, + pub x_max: f64, + pub y_min: f64, + pub y_max: f64, +} + +/// Scatter heatmap cache keyed by (file_index, x_channel, y_channel) +pub type ScatterHistogramCache = std::collections::HashMap<(usize, usize, usize), ScatterHistogram>; + /// Identifies which viewport a cached set of downsampled chart points was /// computed for. The bucketed downsampler anchors bucket boundaries at /// multiples of `bucket_size` from t=0, so its output is fully determined diff --git a/src/ui/scatter_plot.rs b/src/ui/scatter_plot.rs index 7626626..f5a2a73 100644 --- a/src/ui/scatter_plot.rs +++ b/src/ui/scatter_plot.rs @@ -8,7 +8,7 @@ use rust_i18n::t; use crate::app::UltraLogApp; use crate::normalize::{normalize_channel_name_with_custom, sort_channels_by_priority}; -use crate::state::{ScatterPlotConfig, SelectedHeatmapPoint}; +use crate::state::{ScatterHistogram, ScatterPlotConfig, SelectedHeatmapPoint}; /// Heat map color gradient from blue (low) to red (high) const HEAT_COLORS: &[[u8; 3]] = &[ @@ -182,19 +182,75 @@ impl UltraLogApp { return; } - let file = &self.files[file_idx]; - let x_data = file.log.get_channel_data(x_idx); - let y_data = file.log.get_channel_data(y_idx); + // Channel data is immutable once loaded, so the histogram only needs + // building when the (file, x, y) selection is first shown — not every + // frame. The cache is cleared when files are removed. + let cache_key = (file_idx, x_idx, y_idx); + if !self.scatter_histogram_cache.contains_key(&cache_key) { + let file = &self.files[file_idx]; + let x_data = file.log.get_channel_data(x_idx); + let y_data = file.log.get_channel_data(y_idx); - if x_data.is_empty() || y_data.is_empty() || x_data.len() != y_data.len() { - return; + if x_data.is_empty() || y_data.is_empty() || x_data.len() != y_data.len() { + return; + } + + // Calculate data bounds + let x_min = x_data.iter().cloned().fold(f64::MAX, f64::min); + let x_max = x_data.iter().cloned().fold(f64::MIN, f64::max); + let y_min = y_data.iter().cloned().fold(f64::MAX, f64::min); + let y_max = y_data.iter().cloned().fold(f64::MIN, f64::max); + + let x_range = if (x_max - x_min).abs() < f64::EPSILON { + 1.0 + } else { + x_max - x_min + }; + let y_range = if (y_max - y_min).abs() < f64::EPSILON { + 1.0 + } else { + y_max - y_min + }; + + // Build 2D histogram (count hits in each bin) + let mut bins = vec![vec![0u32; HEATMAP_BINS]; HEATMAP_BINS]; + let mut max_hits: u32 = 0; + + for (&x, &y) in x_data.iter().zip(y_data.iter()) { + let x_bin = (((x - x_min) / x_range) * (HEATMAP_BINS - 1) as f64).round() as usize; + let y_bin = (((y - y_min) / y_range) * (HEATMAP_BINS - 1) as f64).round() as usize; + + let x_bin = x_bin.min(HEATMAP_BINS - 1); + let y_bin = y_bin.min(HEATMAP_BINS - 1); + + bins[y_bin][x_bin] += 1; + max_hits = max_hits.max(bins[y_bin][x_bin]); + } + + self.scatter_histogram_cache.insert( + cache_key, + ScatterHistogram { + bins, + max_hits, + x_min, + x_max, + y_min, + y_max, + }, + ); } - // Calculate data bounds - let x_min = x_data.iter().cloned().fold(f64::MAX, f64::min); - let x_max = x_data.iter().cloned().fold(f64::MIN, f64::max); - let y_min = y_data.iter().cloned().fold(f64::MAX, f64::min); - let y_max = y_data.iter().cloned().fold(f64::MIN, f64::max); + let Some(cached) = self.scatter_histogram_cache.get(&cache_key) else { + return; + }; + let (x_min, x_max, y_min, y_max, max_hits) = ( + cached.x_min, + cached.x_max, + cached.y_min, + cached.y_max, + cached.max_hits, + ); + let histogram = &cached.bins; let x_range = if (x_max - x_min).abs() < f64::EPSILON { 1.0 @@ -207,21 +263,6 @@ impl UltraLogApp { y_max - y_min }; - // Build 2D histogram (count hits in each bin) - let mut histogram = vec![vec![0u32; HEATMAP_BINS]; HEATMAP_BINS]; - let mut max_hits: u32 = 0; - - for (&x, &y) in x_data.iter().zip(y_data.iter()) { - let x_bin = (((x - x_min) / x_range) * (HEATMAP_BINS - 1) as f64).round() as usize; - let y_bin = (((y - y_min) / y_range) * (HEATMAP_BINS - 1) as f64).round() as usize; - - let x_bin = x_bin.min(HEATMAP_BINS - 1); - let y_bin = y_bin.min(HEATMAP_BINS - 1); - - histogram[y_bin][x_bin] += 1; - max_hits = max_hits.max(histogram[y_bin][x_bin]); - } - // Allocate space for the heatmap (with click detection), reserving space for legend let available = ui.available_size(); let chart_size = egui::vec2(available.x, (available.y - LEGEND_HEIGHT).max(100.0)); From 4d1587c4e5761d5bc0a98cd06125d91d1eceb9df Mon Sep 17 00:00:00 2001 From: Cole Gentry Date: Thu, 16 Jul 2026 11:21:56 -0400 Subject: [PATCH 15/18] test: add regression coverage for haltech column alignment + midnight rollover and speeduino truncated v2 header --- tests/parsers/haltech_tests.rs | 74 ++++++++++++++++++++++++++++++++ tests/parsers/speeduino_tests.rs | 14 ++++++ 2 files changed, 88 insertions(+) diff --git a/tests/parsers/haltech_tests.rs b/tests/parsers/haltech_tests.rs index d7ec565..6ee2c64 100644 --- a/tests/parsers/haltech_tests.rs +++ b/tests/parsers/haltech_tests.rs @@ -516,3 +516,77 @@ fn test_haltech_find_channel_index() { let not_found = log.find_channel_index("NonExistentChannel12345"); assert_eq!(not_found, None); } + +// ============================================ +// Regression Tests +// ============================================ + +#[test] +fn test_haltech_unparseable_value_keeps_column_alignment() { + // A non-numeric value in one column must not shift later columns left; + // it should be filled with the last known value for that column. + let sample = r#"%DataLog% +DataLogVersion : 1.1 + +Channel : A +Type : Raw +ID : 0 +DisplayMaxMin : 100, 0 + +Channel : B +Type : Raw +ID : 1 +DisplayMaxMin : 100, 0 + +Channel : C +Type : Raw +ID : 2 +DisplayMaxMin : 100, 0 + +00:00:00.000,1,2,3 +00:00:00.100,4,N/A,6 +00:00:00.200,7,8,9 +"#; + + let parser = Haltech; + let log = parser.parse(sample).expect("Should parse"); + + assert_eq!(log.data.len(), 3); + // Row 1: B is unparseable -> last known (2); C must stay 6, not shift + assert_eq!(log.data[1][0].as_f64(), 4.0); + assert_eq!(log.data[1][1].as_f64(), 2.0); + assert_eq!(log.data[1][2].as_f64(), 6.0); + assert_eq!(log.data[2][1].as_f64(), 8.0); +} + +#[test] +fn test_haltech_midnight_rollover_keeps_times_monotonic() { + let sample = r#"%DataLog% +DataLogVersion : 1.1 + +Channel : A +Type : Raw +ID : 0 +DisplayMaxMin : 100, 0 + +23:59:59.000,1 +23:59:59.500,2 +00:00:00.500,3 +00:00:01.000,4 +"#; + + let parser = Haltech; + let log = parser.parse(sample).expect("Should parse"); + + let times: Vec = log.times.clone(); + assert_eq!(times.len(), 4); + for pair in times.windows(2) { + assert!( + pair[1] > pair[0], + "times must be monotonic across midnight: {:?}", + times + ); + } + // 23:59:59.000 -> 00:00:00.500 is 1.5s elapsed + assert!((times[2] - 1.5).abs() < 1e-9); +} diff --git a/tests/parsers/speeduino_tests.rs b/tests/parsers/speeduino_tests.rs index eb609b4..86e79f1 100644 --- a/tests/parsers/speeduino_tests.rs +++ b/tests/parsers/speeduino_tests.rs @@ -465,3 +465,17 @@ fn test_speeduino_large_file_performance() { "Parsing should complete in reasonable time" ); } + +#[test] +fn test_speeduino_truncated_v2_header_errors_not_panics() { + // 22 bytes: magic (6) + version=2 (2) + 14 filler bytes. A v2 header + // needs 24 bytes; this must return Err instead of panicking. + let mut data = Vec::new(); + data.extend_from_slice(b"MLVLG\0"); + data.extend_from_slice(&2i16.to_be_bytes()); + data.extend_from_slice(&[0u8; 14]); + assert_eq!(data.len(), 22); + + let result = ultralog::parsers::speeduino::Speeduino::parse_binary(&data); + assert!(result.is_err()); +} From e4aae9c7a30a505ce95fdb405c971222545a389e Mon Sep 17 00:00:00 2001 From: Cole Gentry Date: Thu, 16 Jul 2026 11:22:02 -0400 Subject: [PATCH 16/18] chore(release): bump version to 2.11.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4b2fc09..c85a7a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5082,7 +5082,7 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "ultralog" -version = "2.10.1" +version = "2.11.0" dependencies = [ "anyhow", "arboard", diff --git a/Cargo.toml b/Cargo.toml index 11de0f9..1ced0ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ultralog" -version = "2.10.1" +version = "2.11.0" edition = "2021" description = "A high-performance ECU log viewer written in Rust" authors = ["Cole Gentry"] From ca55443e90198e1fec4c4eda12c7eaba6b242a63 Mon Sep 17 00:00:00 2001 From: Cole Gentry Date: Thu, 16 Jul 2026 11:28:12 -0400 Subject: [PATCH 17/18] =?UTF-8?q?docs:=20bring=20CLAUDE.md=20up=20to=20dat?= =?UTF-8?q?e=20with=20the=20current=20codebase=20=E2=80=94=20analysis/ipc/?= =?UTF-8?q?mcp=20modules,=20all=2014=20parsers,=20new=20UI=20panels,=20set?= =?UTF-8?q?tings-persistence=20and=20cache-invalidation=20contracts,=20key?= =?UTF-8?q?board=20shortcuts,=20current=20dependencies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 221 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 181 insertions(+), 40 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b19a4c8..2c80027 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,43 +51,74 @@ cargo clippy -- -D warnings ```text src/ ├── main.rs # Application entry point -├── lib.rs # Library exports and module declarations -├── app.rs # Main application state and eframe::App impl -├── state.rs # Core data types and constants -├── units.rs # Unit preference types and conversions -├── normalize.rs # Field name normalization system -├── computed.rs # Computed channels data types and library -├── expression.rs # Formula parsing and evaluation engine -├── updater.rs # Auto-update functionality -├── analytics.rs # Privacy-respecting analytics +├── lib.rs # Library exports and module declarations +├── app.rs # Main application state and eframe::App impl +├── state.rs # Core data types and constants +├── settings.rs # UserSettings — persisted user preferences (see Settings Persistence below) +├── units.rs # Unit preference types and conversions +├── normalize.rs # Field name normalization system +├── computed.rs # Computed channels data types and library +├── expression.rs # Formula parsing and evaluation engine +├── updater.rs # Auto-update functionality +├── analytics.rs # Privacy-respecting analytics +├── i18n.rs # rust-i18n language/locale selection ├── adapters/ │ ├── mod.rs # Adapter module exports │ ├── types.rs # OpenECU Alliance spec types (AdapterSpec, ProtocolSpec, etc.) │ ├── api.rs # API client for fetching specs from openecualliance.org │ ├── cache.rs # Local disk cache at {app_data_dir}/UltraLog/oecua_specs/ │ └── registry.rs # Spec registry with fallback chain (cache -> embedded -> API) +├── analysis/ +│ ├── mod.rs # Analyzer trait + shared analysis framework +│ ├── afr.rs # AFR/Lambda analysis (fuel trim drift CUSUM, rich/lean zones) +│ ├── derived.rs # Derived metrics (Volumetric Efficiency, injector duty cycle, ...) +│ ├── filters.rs # Signal-processing filters (moving average, etc.) +│ └── statistics.rs # Descriptive statistics and correlation analysis +├── ipc/ +│ ├── mod.rs # IPC module exports, DEFAULT_IPC_PORT +│ ├── commands.rs # IpcCommand/IpcResponse wire types shared with mcp/client.rs +│ ├── handler.rs # UltraLogApp command handlers (executes commands from the MCP server) +│ └── server.rs # TCP IPC server — background thread, wakes the GUI via a repaint callback +├── mcp/ +│ ├── mod.rs # MCP module exports, DEFAULT_MCP_PORT +│ ├── client.rs # TCP client the MCP server uses to talk to the running GUI's IPC server +│ └── server.rs # Embedded MCP HTTP server (rmcp + axum) for Claude Desktop integration ├── parsers/ -│ ├── mod.rs # Parser module exports -│ ├── types.rs # Core parser types (Log, Channel, Value, etc.) -│ ├── haltech.rs # Haltech ECU log parser -│ ├── ecumaster.rs # ECUMaster EMU Pro CSV parser -│ ├── romraider.rs # RomRaider CSV parser -│ ├── speeduino.rs # Speeduino/rusEFI MLG binary parser -│ ├── aim.rs # AiM XRK/DRK binary parser -│ ├── link.rs # Link ECU LLG binary parser -│ └── woolich.rs # Woolich Racing Tuned CSV parser +│ ├── mod.rs # Parser module exports +│ ├── types.rs # Core parser types (Log, Channel, Value, EcuType, etc.) +│ ├── haltech.rs # Haltech ECU log parser +│ ├── ecumaster.rs # ECUMaster EMU Pro CSV parser +│ ├── romraider.rs # RomRaider CSV parser +│ ├── speeduino.rs # Speeduino/rusEFI MLG binary parser +│ ├── aim.rs # AiM XRK/DRK binary parser +│ ├── link.rs # Link ECU LLG binary parser +│ ├── woolich.rs # Woolich Racing Tuned CSV parser (motorcycle ECUs) +│ ├── emerald.rs # Emerald K6/M3D ECU (.lg1/.lg2) binary parser +│ ├── locomotive.rs # Locomotive datalogger CSV parser (TimeStamp/Customer/UnitNumber header) +│ ├── megasquirt.rs # MegaSquirt (MS1/MS2/MS3) TunerStudio CSV parser +│ ├── motorsport_electronics.rs # Motorsport Electronics ME221/ME442 (ME Tuner) CSV parser +│ ├── bluedriver.rs # BlueDriver OBD-II scan tool CSV parser +│ └── dynamicefi.rs # DynamicEFI EBL WhatsUp (GM TBI) CSV parser └── ui/ ├── mod.rs # UI module exports - ├── sidebar.rs # File list and view options panel - ├── channels.rs # Channel selection and cards + ├── activity_bar.rs # VS Code-style vertical icon strip for panel navigation + ├── side_panel.rs # Container that routes to the active panel by activity-bar selection + ├── files_panel.rs # File management, loading, and file list (current files panel) + ├── tools_panel.rs # Analysis tools, computed channels library, export options + ├── settings_panel.rs # Consolidated settings (display, units, normalization, updates) + ├── tool_properties_panel.rs # Dynamic panel showing controls for the active tool (channels / histogram / scatter) + ├── analysis_panel.rs # Window for running analysis algorithms (src/analysis) on the active log + ├── sidebar.rs # Legacy files panel, superseded by files_panel (kept, not wired into render path) + ├── channels.rs # Selected-channel cards; legacy channel-picker superseded by tool_properties_panel ├── chart.rs # Chart rendering, legends, LTTB algorithm ├── timeline.rs # Timeline scrubber and playback controls ├── menu.rs # Menu bar (Units, Tools, Help menus) ├── toast.rs # Toast notification system ├── icons.rs # Custom icon drawing utilities ├── tab_bar.rs # Multi-file tab interface - ├── tool_switcher.rs # Switch between chart and scatter plot + ├── tool_switcher.rs # Switch between Log Viewer, Scatter Plot, and Histogram tools ├── scatter_plot.rs # XY scatter plot visualization + ├── histogram.rs # 2D histogram/heatmap view for channel distributions ├── export.rs # PNG and PDF export functionality ├── normalization_editor.rs # Custom field mapping editor ├── computed_channels_manager.rs # Computed channels library UI @@ -106,9 +137,13 @@ src/ - **`state.rs`** - Core data types: - `LoadedFile` - Represents a parsed log file - `SelectedChannel` - A channel selected for visualization - - `CacheKey`, `LoadResult`, `LoadingState` - Internal state types + - `CacheKey`, `LoadResult`, `LoadingState`, `DownsampleViewKey` - Internal state types - Color palette constants (`CHART_COLORS`, `COLORBLIND_COLORS`) +- **`settings.rs`** - `UserSettings` persistence (see [Settings Persistence Contract](#settings-persistence-contract) below) + +- **`i18n.rs`** - Internationalization: the `Language` enum and locale selection for `rust-i18n` + - **`units.rs`** - Unit preference system: - Enums for each unit type (Temperature, Pressure, Speed, etc.) - `UnitPreferences` struct for storing user selections @@ -169,20 +204,51 @@ The adapter system integrates with the **OpenECU Alliance** specification ecosys - **Metadata lookup** - Provides channel metadata (category, unit, min/max, precision) - Thread-safe `RwLock` for dynamic spec updates without restart -### UI Modules (src/ui/) +### Analysis System (src/analysis/) + +Trait-based framework (`Analyzer`) for algorithms that process log data and can feed results back into the computed-channels system: + +- **`afr.rs`** - AFR/Lambda analysis: fuel trim drift detection (CUSUM), rich/lean zone detection, AFR deviation. Supports both AFR and Lambda units with automatic detection. +- **`derived.rs`** - Derived engine metrics: Volumetric Efficiency (from MAF or MAP/IAT speed-density), injector duty cycle, and similar calculated values. +- **`filters.rs`** - Signal-processing filters for smoothing and noise reduction (e.g., moving average). +- **`statistics.rs`** - Descriptive statistics and correlation analysis. + +The `analysis_panel.rs` UI module (see below) hosts these analyzers, with category tabs and configurable parameters per algorithm. -UI rendering is split into focused modules that implement methods on `UltraLogApp`: +### IPC + MCP System (src/ipc/, src/mcp/) -- **`sidebar.rs`** - Left panel: file list, drop zone, view options (cursor tracking, colorblind mode, field normalization) -- **`channels.rs`** - Right panel: channel list with search, selected channel cards, computed channels +UltraLog embeds an MCP (Model Context Protocol) HTTP server so Claude Desktop can drive the running GUI — select channels, add computed channels, and query log data. + +- **`mcp/server.rs`** - The MCP server itself (`rmcp` + `axum`), served over HTTP at `http://localhost:52385/mcp` (`DEFAULT_MCP_PORT`). This is what Claude Desktop connects to. +- **`mcp/client.rs`** - A TCP client the MCP server uses internally to talk to the GUI's IPC server. Each command opens a fresh TCP connection (no persistent/stale connections). +- **`ipc/server.rs`** - A TCP server (`DEFAULT_IPC_PORT` = 52384) that runs in a background thread inside the GUI process and receives commands from `mcp/client.rs`. +- **`ipc/commands.rs`** - The shared `IpcCommand`/`IpcResponse` wire types. +- **`ipc/handler.rs`** - `UltraLogApp` methods that execute each `IpcCommand` against live app state. + +**Load-bearing contract:** the IPC server wakes the GUI via a repaint callback (`request_repaint()`), not a polling timer — see `IpcServer::start_with_repaint` in `src/ipc/server.rs` and its wiring in `UltraLogApp::new`. Incoming commands are drained in `UltraLogApp::process_ipc_commands`, which caps processing at **10 commands per frame** to avoid blocking the UI thread; if more are queued, it requests another repaint to continue next frame. + +### UI Modules (src/ui/) + +UI rendering is split into focused modules that implement methods on `UltraLogApp`. The current layout is a VS Code-style activity bar + side panel; a couple of pre-activity-bar modules remain in the tree but are superseded (noted below): + +- **`activity_bar.rs`** - Vertical icon strip on the far left for switching between side-panel sections (Files / Tool Properties / Tools / Settings) +- **`side_panel.rs`** - Container that routes to the panel selected in the activity bar +- **`files_panel.rs`** - File management, loading, and file list (current files panel — see `sidebar.rs` note below) +- **`tools_panel.rs`** - Analysis tools, computed channels library, and export options, inline in the side panel +- **`settings_panel.rs`** - Consolidated settings: display, units, normalization, updates +- **`tool_properties_panel.rs`** - Dynamic panel showing controls for the active tool: channel selection for Log Viewer, axis/grid/filter controls for Histogram, controls for Scatter Plot +- **`analysis_panel.rs`** - Window for running `src/analysis` algorithms (filters, statistics, AFR, derived metrics) against the active log, with category tabs +- **`sidebar.rs`** - Legacy file list/drop-zone panel, superseded by `files_panel.rs` (module still compiles but its render methods are not called from `app.rs`) +- **`channels.rs`** - Selected-channel cards (`render_selected_channels`, still active) plus a legacy channel-picker list (`render_channel_selection`) superseded by `tool_properties_panel.rs` - **`chart.rs`** - Main chart with egui_plot, min/max legend overlay, LTTB downsampling, normalization - **`timeline.rs`** - Bottom panel: playback controls (play/pause/stop), speed selector, timeline scrubber -- **`menu.rs`** - Top menu bar with Units submenu (8 unit categories), Tools menu, and Help menu +- **`menu.rs`** - Top menu bar (File, Edit, View, Help; Units submenu with 8 unit categories) - **`toast.rs`** - Toast notification overlay for user feedback - **`icons.rs`** - Custom icon drawing (upload icon for drop zone) - **`tab_bar.rs`** - Chrome-style tabs for multi-file support -- **`tool_switcher.rs`** - Toggle between chart view and scatter plot view +- **`tool_switcher.rs`** - Switch between Log Viewer, Scatter Plot, and Histogram tools - **`scatter_plot.rs`** - XY scatter plot for channel correlation analysis +- **`histogram.rs`** - 2D histogram/heatmap view of channel distributions, with configurable cell coloring (average Z-value or hit count) - **`export.rs`** - PNG and PDF export with chart rendering - **`normalization_editor.rs`** - Dialog for creating custom field name mappings - **`computed_channels_manager.rs`** - Window for managing computed channel library @@ -201,6 +267,14 @@ The parser system uses a trait-based design for supporting multiple ECU formats: - **`parsers/aim.rs`** - AiM XRK/DRK binary format parser for motorsport data loggers - **`parsers/link.rs`** - Link ECU LLG binary format parser - **`parsers/woolich.rs`** - Woolich Racing Tuned CSV parser (HH:MM:SS.mmm timestamps, boolean channels) +- **`parsers/emerald.rs`** - Emerald K6/M3D ECU binary parser (`.lg2` channel definitions + `.lg1` 24-byte timestamped records) +- **`parsers/locomotive.rs`** - Locomotive datalogger CSV parser (detected via `TimeStamp:` / `Customer:` header lines, day-of-week-prefixed data rows) +- **`parsers/megasquirt.rs`** - MegaSquirt (MS1/MS2/MS3/MS3Pro) TunerStudio CSV parser +- **`parsers/motorsport_electronics.rs`** - Motorsport Electronics ME221/ME442 (ME Tuner) CSV parser +- **`parsers/bluedriver.rs`** - BlueDriver OBD-II scan tool CSV parser (UTF-16 with BOM) +- **`parsers/dynamicefi.rs`** - DynamicEFI EBL WhatsUp CSV parser (modified GM TBI systems) + +Note: `EcuType` also reserves `Aem`, `MaxxEcu`, and `MotEc` variants for formats without an implemented parser yet (no `detect()`/`Parseable` wiring in `app.rs`) — don't assume these are supported just because the enum has a slot for them. **Supported ECU Systems:** @@ -211,17 +285,27 @@ The parser system uses a trait-based design for supporting multiple ECU formats: - rusEFI (MLG binary) - AiM (XRK/DRK binary) - Link ECU (LLG binary) +- Emerald K6/M3D (.lg1/.lg2 binary) +- MegaSquirt MS1/MS2/MS3 (TunerStudio CSV export) - Motorsport Electronics ME221/ME442 (ME Tuner CSV export) - Woolich Racing Tuned (WRT CSV export — motorcycle ECUs) +- BlueDriver (OBD-II scan tool CSV export) +- DynamicEFI EBL WhatsUp (modified GM TBI CSV export) +- Locomotive datalogger (CSV export) To add a new ECU format: -1. Create a new module in `src/parsers/` (e.g., `megasquirt.rs`) +1. Create a new module in `src/parsers/` (e.g., `newformat.rs`) 2. Define format-specific channel types and metadata structs 3. Implement the `Parseable` trait -4. Add enum variants to `Channel`, `Meta`, and wire up in `mod.rs` +4. Add enum variants to `Channel`, `Meta`, `EcuType`, and wire up in `mod.rs` 5. Update detection logic in `app.rs` file loading +**Haltech parser load-bearing behaviors** (`src/parsers/haltech.rs`, added for wall-clock-timestamped exports): + +- **Last-known-value substitution** - Unparseable/blank fields are filled with the last successfully parsed value for that column (`0.0` before the first valid sample), matching the approach used in `parsers/ecumaster.rs`. This preserves column alignment instead of shifting subsequent columns when a field fails to parse. +- **Midnight rollover handling** - Timestamps are wall-clock time-of-day, so a logging session that crosses midnight wraps from ~86400 back toward 0. The parser tracks the previous raw timestamp and accumulates a 24h (`86_400.0`) offset on each backwards jump greater than 1 second (the 1s guard avoids tripping on timestamp jitter), keeping `times` monotonically increasing. This is the same technique used in `parsers/woolich.rs` (`Log Time` column) — monotonic times are required because computed-channel time-shift lookups binary-search the `times` vector. + ### Data Flow **Startup and Spec Loading:** @@ -253,13 +337,34 @@ To add a new ECU format: OpenECU Alliance API → Update cache → Rebuild normalization maps → Ready for next startup ``` +### Settings Persistence Contract + +`UserSettings` (`src/settings.rs`) is a `Serialize`/`Deserialize` struct persisted as JSON at `{app_data_dir}/UltraLog/settings.json` (or `.../ultralog/settings.json` on Linux). It currently covers: `language`, `scroll_to_zoom`, `show_grid`, `grid_opacity`, `unit_preferences`, `font_scale`, `color_blind_mode`, `field_normalization`, `cursor_tracking`, `auto_check_updates`, and `custom_normalizations`. + +- **Load** - `UltraLogApp::new` calls `UserSettings::load()` and copies each field into the corresponding live `UltraLogApp` field (e.g. `app.color_blind_mode = user_settings.color_blind_mode`). +- **Save** - `UltraLogApp` implements `eframe::App::save`, which eframe calls on its auto-save interval (~30s) and again at shutdown. `save()` rebuilds a `UserSettings` from the current live fields, and only writes to disk if it differs from the last-loaded/saved value. +- **Load-bearing invariant:** because the sync is manual field-by-field (not `#[derive]` magic), **any new persisted preference must be added in three places**: the field on the `UserSettings` struct, the copy in `UltraLogApp::new`, and the reconstruction in `eframe::App::save` in `src/app.rs`. Missing any one of the three means the preference silently resets on restart or never saves at all. + +### Chart/Scatter Cache Invalidation Contract + +Several `UltraLogApp` fields cache expensive per-file, per-channel computations across frames: + +- **`downsample_cache`** (`(file_index, channel_index) -> (DownsampleViewKey, Vec<[f64; 2]>)`) - LTTB-downsampled chart points, tagged with the view key (zoom/bucket state) they were computed for. Read/written in `src/ui/chart.rs`. +- **`minmax_cache`** (`CacheKey -> (f64, f64)`) - Per-channel min/max for the legend overlay. +- **`scatter_histogram_cache`** (keyed by `(file, x_channel, y_channel)`) - Precomputed 2D histogram buckets for the scatter/heatmap view (`src/ui/scatter_plot.rs`). + +**Load-bearing invariant:** these caches are keyed by index (file index / channel index), not by identity. Any code path that mutates or reindexes channel data — removing a file, or removing/editing a computed channel (which shifts later computed channels' indices down) — **must clear the relevant caches**, or stale entries will silently render the wrong data against a different channel's index. See `remove_computed_channel` and the file-removal path in `src/app.rs` for the current call sites (`self.downsample_cache.clear()`, `self.minmax_cache.clear()`, `self.scatter_histogram_cache.clear()`). + ## Key Features -- **Multi-ECU Support** - Supports Haltech, ECUMaster, RomRaider, Speeduino, rusEFI, AiM, and Link ECU formats +- **Multi-ECU Support** - Haltech, ECUMaster, RomRaider, Speeduino, rusEFI, AiM, Link, Emerald, MegaSquirt, Motorsport Electronics, Woolich Racing Tuned, BlueDriver, DynamicEFI, and Locomotive log formats - **Computed Channels** - Create virtual channels from mathematical formulas with time-shifting (e.g., `RPM[-1]`, `Boost@-0.5s`) +- **Analysis Algorithms** - AFR/Lambda drift and zone detection, derived metrics (VE, injector duty cycle), signal filters, and descriptive statistics (`src/analysis/`) +- **Claude Desktop / MCP Integration** - Embedded MCP server (`src/mcp/`) lets Claude control the running app over `http://localhost:52385/mcp` — select channels, add computed channels, query log data - **Unit Preferences** - Users can select display units for temperature, pressure, speed, distance, fuel economy, volume, flow rate, and acceleration - **Field Normalization** - Maps ECU-specific channel names to standardized names for cross-ECU comparison - **Scatter Plot Tool** - XY scatter plot for analyzing channel correlations +- **Histogram Tool** - 2D heatmap view of channel distributions - **Export Options** - Export charts as PNG or PDF - **Colorblind Mode** - Wong's optimized color palette for accessibility - **Playback** - Play through log data at 0.25x to 8x speed @@ -267,25 +372,53 @@ To add a new ECU format: - **Min/Max Legend** - Shows peak values for each channel - **Initial Zoom** - Charts start zoomed to first 60 seconds for better initial view - **Multi-File Tabs** - Chrome-style tabs for working with multiple log files simultaneously +- **Internationalization** - `rust-i18n`-backed language selection (`src/i18n.rs`) +- **Settings Persistence** - Unit/display/normalization preferences survive restarts (see [Settings Persistence Contract](#settings-persistence-contract)) - **Auto-Update** - Automatic update checking and installation +### Keyboard Shortcuts + +Handled in `UltraLogApp::handle_keyboard_shortcuts` (`src/app.rs`); ignored while a text field has focus. + +- **Cmd/Ctrl+O** - Open file +- **Cmd/Ctrl+W** - Close current tab +- **Cmd/Ctrl+,** - Open Settings panel +- **Cmd/Ctrl+1/2/3** - Switch tool (Log Viewer / Scatter Plot / Histogram) +- **Cmd/Ctrl+Shift+F/C/T** - Switch side panel (Files / Tool Properties / Tools) +- **Arrow Left/Right** - Step cursor one record (Shift = 10 records) +- **Home/End** - Jump cursor to start/end of log +- **Escape** - Stop playback +- **Cmd/Ctrl+E** - Export the current view (chart, scatter plot, or histogram) as PNG +- **Space** - Toggle play/pause + ## Key Dependencies -- **eframe/egui** (0.33) - Native GUI framework -- **egui_plot** (0.34) - Charting/plotting -- **rfd** (0.16) - Native file dialogs +- **eframe/egui** (0.34) - Native GUI framework +- **egui_plot** (0.35) - Charting/plotting +- **rfd** (0.17) - Native file dialogs - **open** (5) - Cross-platform URL/email opening -- **strum** (0.27) - Enum string conversion for channel types +- **strum** (0.28) - Enum string conversion for channel types - **regex** (1.12) - Log file parsing - **meval** (0.2) - Mathematical expression evaluation for computed channels -- **ureq** (3.0) - HTTP client for auto-updates and OpenECU Alliance API +- **ureq** (3.3) - HTTP client for auto-updates and OpenECU Alliance API - **semver** (1.0) - Version comparison -- **serde_yaml** (0.9) - YAML parsing for adapter/protocol specs -- **printpdf** (0.7) - PDF generation +- **serde_yml** (0.0.12) - YAML parsing for adapter/protocol specs (replaces deprecated `serde_yaml`) +- **printpdf** (0.9) - PDF generation - **image** (0.25) - PNG export - **memmap2** (0.9) - Memory-mapped file loading for large files - **rayon** (1.11) - Parallel iteration for parsing -- **dirs** (5.0) - Cross-platform app data directory detection for cache +- **dirs** (6.0) - Cross-platform app data directory detection for cache and settings +- **rmcp** (0.12) - MCP server implementation (`src/mcp/`), used with `axum` as the HTTP transport +- **tokio** (1) - Async runtime backing the MCP server +- **axum** (0.8) - HTTP server framework for the embedded MCP endpoint +- **schemars** (1.0) - JSON schema generation for MCP tool definitions +- **arboard** (3.6) - Clipboard support (histogram copy/paste) +- **rust-i18n** (3.1) - Internationalization / locale strings +- **uuid** (1.0) - Anonymous user ID generation for analytics +- **chrono** (0.4) - Date/time parsing (Locomotive, Woolich timestamps) +- **encoding_rs** (0.8) - Character encoding detection/conversion (e.g., UTF-16 BlueDriver exports) +- **thiserror** / **anyhow** - Error handling +- **tracing** / **tracing-subscriber** - Structured logging ## Test Data @@ -295,4 +428,12 @@ Example log files are in `exampleLogs/` organized by ECU type: - `exampleLogs/aim/` - AiM XRK/DRK files - `exampleLogs/link/` - Link ECU LLG files - `exampleLogs/woolich/` - Woolich Racing Tuned CSV exports -- Additional formats for parser testing +- `exampleLogs/emerald/` - Emerald K6/M3D `.lg1`/`.lg2` files +- `exampleLogs/megasquirt/` - MegaSquirt TunerStudio CSV exports +- `exampleLogs/motorsportElectronics/` - Motorsport Electronics ME Tuner CSV exports +- `exampleLogs/bluedriver/` - BlueDriver OBD-II CSV exports +- `exampleLogs/dynamicEFI/` - DynamicEFI EBL WhatsUp CSV exports +- `exampleLogs/locomotive/` - Locomotive datalogger CSV exports +- `exampleLogs/rusefi/`, `exampleLogs/speeduino/` - MLG binary logs +- `exampleLogs/ecumaster/`, `exampleLogs/RomRaider/` - CSV exports +- Additional directories (`aem/`, `HondaTuningStudio/`) hold sample data for formats without an implemented parser yet — don't assume a directory's presence means the format is supported From 05e0c996d0f9682efc0a0851be83890e10166336 Mon Sep 17 00:00:00 2001 From: Cole Gentry Date: Thu, 16 Jul 2026 11:54:05 -0400 Subject: [PATCH 18/18] fix: address PR #75 review feedback --- src/state.rs | 8 +++++--- src/ui/chart.rs | 21 ++++++++++++++++----- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/state.rs b/src/state.rs index 9a6e4e6..0c90daa 100644 --- a/src/state.rs +++ b/src/state.rs @@ -204,10 +204,12 @@ pub struct CacheKey { pub plot_area_id: usize, } -/// Cached downsampled chart points per (file_index, channel_index), tagged -/// with the viewport key they were computed for. +/// Cached downsampled chart points per (file_index, channel_index, +/// plot_area_id), tagged with the viewport key they were computed for. +/// Keyed per plot area so a channel shown in two stacked plots with +/// different viewports doesn't evict its own entry every frame. pub type DownsampleCache = - std::collections::HashMap<(usize, usize), (DownsampleViewKey, Vec<[f64; 2]>)>; + std::collections::HashMap<(usize, usize, usize), (DownsampleViewKey, Vec<[f64; 2]>)>; /// Precomputed 2D histogram for the scatter-plot heatmap. Channel data is /// immutable once a file is loaded, so this only needs rebuilding when the diff --git a/src/ui/chart.rs b/src/ui/chart.rs index 6409b8e..cfe46c6 100644 --- a/src/ui/chart.rs +++ b/src/ui/chart.rs @@ -95,7 +95,12 @@ impl UltraLogApp { let chart_points: Vec>> = selected_channels .iter() .map(|selected| { - self.compute_viewport_points(selected.file_index, selected.channel_index, viewport) + self.compute_viewport_points( + selected.file_index, + selected.channel_index, + 0, + viewport, + ) }) .collect(); @@ -511,7 +516,12 @@ impl UltraLogApp { let chart_points: Vec>> = channels .iter() .map(|selected| { - self.compute_viewport_points(selected.file_index, selected.channel_index, viewport) + self.compute_viewport_points( + selected.file_index, + selected.channel_index, + plot_area_id, + viewport, + ) }) .collect(); @@ -870,6 +880,7 @@ impl UltraLogApp { &mut self, file_index: usize, channel_index: usize, + plot_area_id: usize, viewport: Option<(f64, f64)>, ) -> Option> { // Resolve min/max first so the mutable borrow on the cache ends before @@ -908,8 +919,8 @@ impl UltraLogApp { _ => DownsampleViewKey::Full, }; - if let Some((cached_key, points)) = self.downsample_cache.get(&(file_index, channel_index)) - { + let cache_key = (file_index, channel_index, plot_area_id); + if let Some((cached_key, points)) = self.downsample_cache.get(&cache_key) { if *cached_key == view_key { return Some(points.clone()); } @@ -975,7 +986,7 @@ impl UltraLogApp { }; self.downsample_cache - .insert((file_index, channel_index), (view_key, points.clone())); + .insert(cache_key, (view_key, points.clone())); Some(points) }