From da77f2f194ee520cd0320460b0db1aca1dbd07a3 Mon Sep 17 00:00:00 2001 From: lenn Date: Wed, 1 Jul 2026 17:08:07 +0800 Subject: [PATCH] Improve hand mode panels and recording --- src/app.rs | 25 ++++++++++-- src/connection.rs | 7 +++- src/serial_core/serial.rs | 22 +++++++++-- src/style.rs | 15 ++++++- src/ui.rs | 83 ++++++++++++++++++++++++++++----------- 5 files changed, 120 insertions(+), 32 deletions(-) diff --git a/src/app.rs b/src/app.rs index 8f83f55..dfce2d7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -230,9 +230,6 @@ impl EskinDesktopApp { self.latest_matrix_rows = sample.rows; self.latest_matrix_cols = sample.cols; - // Feed data to recorder - self.recorder.add_frame(&sample.matrix); - self.latest_spatial_force = self.force_estimator.analyze(&sample.matrix); // Keep JE-Skin's summary path separate from the optional spatial-force vector. @@ -367,11 +364,18 @@ impl EskinDesktopApp { fn draw_floating_panels(&mut self, ctx: &egui::Context) { // draw_scene_panel(ctx, &mut self.scene_panel); + if self.config_state.mode == SerialMode::Hand && self.stats_panel.visible { + self.config_panel.visible = false; + self.export_panel.visible = false; + self.matrix_config_panel.visible = false; + } + if let Some(next_mode) = draw_config_panel( ctx, &mut self.config_panel, &mut self.config_state, &self.connection, + &self.recorder, ) { self.switch_mode(next_mode); } @@ -430,7 +434,20 @@ impl EskinDesktopApp { panel_restore_item(ui, "配置", &mut self.config_panel); panel_restore_item(ui, "录制", &mut self.export_panel); panel_restore_item(ui, "矩阵", &mut self.matrix_config_panel); - panel_restore_item(ui, "统计", &mut self.stats_panel); + if self.stats_panel.visible { + if ui + .add_sized( + egui::vec2(ui.available_width(), 0.0), + egui::Button::new("隐藏 统计"), + ) + .clicked() + { + self.stats_panel.visible = false; + close_menu = true; + } + } else { + panel_restore_item(ui, "统计", &mut self.stats_panel); + } if ui .add_sized( egui::vec2(ui.available_width(), 0.0), diff --git a/src/connection.rs b/src/connection.rs index fb6ded8..fc600e1 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -4,6 +4,7 @@ use std::time::Duration; use crossbeam_channel::{self, Receiver, Sender, TryRecvError}; +use crate::recording::Recorder; use crate::serial_core::serial::{ SerialIoStats, SerialPortReadWrite, SerialProtocol, run_serial_loop, }; @@ -83,6 +84,7 @@ impl ConnectionManager { cols: u32, baud_rate: u32, protocol: SerialProtocol, + recorder: Recorder, ) { self.disconnect(); self.set_state(ConnectionState::Connecting); @@ -108,6 +110,7 @@ impl ConnectionManager { &sample_tx, &stats_tx, &latest_sample, + recorder, ); if let Err(e) = result { eprintln!("[connection] device loop error: {e}"); @@ -187,9 +190,10 @@ fn run_device_loop( sample_tx: &Sender>, stats_tx: &Sender, latest_sample: &Arc>>, + recorder: Recorder, ) -> Result<(), Box> { let port = serialport::new(port_name, baud_rate) - .timeout(Duration::from_millis(100)) + .timeout(Duration::from_millis(1)) .open()?; *state.lock().unwrap() = ConnectionState::Connected; @@ -205,6 +209,7 @@ fn run_device_loop( cancel_rx, sample_tx, Some(stats_tx), + Some(&recorder), ); if let Ok(mut latest) = latest_sample.lock() { diff --git a/src/serial_core/serial.rs b/src/serial_core/serial.rs index 0a96873..aa837e4 100644 --- a/src/serial_core/serial.rs +++ b/src/serial_core/serial.rs @@ -1,3 +1,4 @@ +use crate::recording::Recorder; use crate::serial_core::codec::Codec; use crate::serial_core::codecs::hand_gateway::{HandGatewayCodec, HandGatewayFrame}; use crate::serial_core::codecs::tactile_a::TactileACodec; @@ -6,7 +7,7 @@ use crossbeam_channel::{Receiver, Sender}; use std::io::{Read, Write}; use std::time::{Duration, Instant}; -const POLL_INTERVAL_MS: u64 = 10; +const POLL_INTERVAL_MS: u64 = 5; const DEFAULT_HAND_GATEWAY_NODE_SAMPLE_COUNTS: &[u16] = &[84, 84, 84, 84, 84, 70, 44]; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] @@ -31,12 +32,15 @@ pub fn run_serial_loop( cancel_rx: &Receiver<()>, sample_tx: &Sender>, stats_tx: Option<&Sender>, + recorder: Option<&Recorder>, ) { match protocol { SerialProtocol::TactileA => { - run_tactile_a_loop(port, rows, cols, cancel_rx, sample_tx, stats_tx) + run_tactile_a_loop(port, rows, cols, cancel_rx, sample_tx, stats_tx, recorder) + } + SerialProtocol::HandGateway => { + run_hand_gateway_loop(port, cancel_rx, sample_tx, stats_tx, recorder) } - SerialProtocol::HandGateway => run_hand_gateway_loop(port, cancel_rx, sample_tx, stats_tx), } } @@ -47,6 +51,7 @@ fn run_tactile_a_loop( cancel_rx: &Receiver<()>, sample_tx: &Sender>, stats_tx: Option<&Sender>, + recorder: Option<&Recorder>, ) { let session_started_at = Instant::now(); let mut codec = TactileACodec::new(cols, rows); @@ -90,6 +95,11 @@ fn run_tactile_a_loop( for frame in frames { if let TactileAFrame::Rep(rep) = frame { if let Ok(vals) = TactileACodec::parse_data_frame(&rep.payload) { + if let Some(recorder) = recorder { + let pressures: Vec = + vals.iter().map(|v| (*v).max(0) as u32).collect(); + recorder.add_frame(&pressures); + } let _ = sample_tx.try_send(vals); } } @@ -116,6 +126,7 @@ fn run_hand_gateway_loop( cancel_rx: &Receiver<()>, sample_tx: &Sender>, stats_tx: Option<&Sender>, + recorder: Option<&Recorder>, ) { let session_started_at = Instant::now(); let mut codec = HandGatewayCodec::new(DEFAULT_HAND_GATEWAY_NODE_SAMPLE_COUNTS); @@ -174,6 +185,11 @@ fn run_hand_gateway_loop( if parse_ok { // println!("[hand-packet-values] samples={}", vals.len()); + if let Some(recorder) = recorder { + let pressures: Vec = + vals.iter().map(|v| (*v).max(0) as u32).collect(); + recorder.add_frame(&pressures); + } let _ = sample_tx.try_send(vals); } } diff --git a/src/style.rs b/src/style.rs index 349c51d..16bd2a2 100644 --- a/src/style.rs +++ b/src/style.rs @@ -1,4 +1,4 @@ -use eframe::egui; +use eframe::egui::{self, Color32}; #[derive(Clone, Copy)] pub struct AppTheme { @@ -213,6 +213,19 @@ pub fn tag_button(label: impl Into) -> egui::Button<'static> { .min_size(egui::vec2(0.0, METRICS.button_height)) } +pub fn rich_tag_button( + label: impl Into, + color: impl Into, +) -> egui::Button<'static> { + let text = egui::RichText::new(label.into()).color(color); + + egui::Button::new(text) + .fill(ONE_DARK_PRO.panel_strong) + .stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.border)) + .corner_radius(egui::CornerRadius::same(4)) + .min_size(egui::vec2(0.0, METRICS.button_height)) +} + pub fn primary_button(label: impl Into) -> egui::Button<'static> { egui::Button::new(label) .fill(ONE_DARK_PRO.accent) diff --git a/src/ui.rs b/src/ui.rs index e754965..5c1f667 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -7,7 +7,7 @@ use crate::{ serial_core::serial::{SerialIoStats, SerialProtocol}, style::{ self, ACCENT_BLUE, ACCENT_GREEN, ACCENT_ORANGE, ACCENT_RED, METRICS, ONE_DARK_PRO, - dim_text, group_frame, layout, panel_frame, tag_button, + dim_text, group_frame, layout, panel_frame, rich_tag_button, tag_button, }, utils::serial_enum, }; @@ -196,7 +196,7 @@ pub fn draw_connect_panel( }); ui.add_space(METRICS.row_gap); - draw_connect_action_row(ui, conn_state, is_connected, config, connection); + draw_connect_action_row(ui, conn_state, is_connected, config, connection, recorder); if is_connected { draw_recording_toolbar(ui, recorder, export_path); @@ -289,6 +289,7 @@ fn draw_connect_action_row( is_connected: bool, config: &ConnectPanelState, connection: &ConnectionManager, + recorder: &Recorder, ) { ui.horizontal(|ui| { let status_text = match conn_state { @@ -324,6 +325,7 @@ fn draw_connect_action_row( config.cols as u32, config.mode.baud_rate(), config.mode.protocol(), + recorder.clone(), ); } } @@ -336,6 +338,7 @@ pub fn draw_config_panel( panel: &mut FloatingPanelState, config: &mut ConfigPanelState, connection: &ConnectionManager, + recorder: &Recorder, ) -> Option { let mut changed_mode = None; let conn_state = connection.state(); @@ -354,7 +357,7 @@ pub fn draw_config_panel( // draw_mode_row(ui, config); ui.separator(); - draw_connection_row(ui, config, connection, conn_state); + draw_connection_row(ui, config, connection, recorder, conn_state); ui.add_space(8.0); // Legacy serial parameter grid is intentionally kept commented for future hardware: // draw_serial_grid(ui, config); @@ -392,6 +395,7 @@ fn draw_connection_row( ui: &mut egui::Ui, config: &mut ConfigPanelState, connection: &ConnectionManager, + recorder: &Recorder, conn_state: ConnectionState, ) { ui.horizontal(|ui| { @@ -460,6 +464,7 @@ fn draw_connection_row( 7, config.mode.baud_rate(), config.mode.protocol(), + recorder.clone(), ); } } @@ -744,10 +749,13 @@ const FORCE_CHART_MAX_N: f32 = 25.6; const FORCE_CHART_SAMPLE_SECONDS: f32 = 0.1; const FORCE_PANEL_ACTIVE_TAIL: usize = 8; -const HAND_FORCE_PANEL_WIDTH: f32 = 260.0; -const HAND_FORCE_PANEL_HEIGHT: f32 = 150.0; +const HAND_FORCE_PANEL_MIN_WIDTH: f32 = 280.0; +const HAND_FORCE_PANEL_MAX_WIDTH: f32 = 500.0; +const HAND_FORCE_PANEL_MIN_HEIGHT: f32 = 140.0; +const HAND_FORCE_PANEL_MAX_HEIGHT: f32 = 190.0; const HAND_FORCE_PANEL_GAP: f32 = 12.0; -const HAND_FORCE_PANEL_BOTTOM_MARGIN: f32 = 24.0; +const HAND_FORCE_PANEL_SIDE_MARGIN: f32 = 24.0; +const HAND_FORCE_PANEL_VERTICAL_MARGIN: f32 = 28.0; const HAND_FORCE_PANEL_TITLES: [(&str, &str); 7] = [ ("T1", "拇指"), ("T2", "食指"), @@ -772,29 +780,49 @@ pub fn draw_hand_force_panels(ctx: &egui::Context, visible: bool, histories: &[V .any(|history| has_recent_resultant_force(history)); let target_visible = visible && hand_active; let screen = ctx.content_rect(); - let first_row_y = screen.bottom() - - HAND_FORCE_PANEL_BOTTOM_MARGIN - - HAND_FORCE_PANEL_HEIGHT * 2.0 - - HAND_FORCE_PANEL_GAP; - let second_row_y = screen.bottom() - HAND_FORCE_PANEL_BOTTOM_MARGIN - HAND_FORCE_PANEL_HEIGHT; + let side_width = ((screen.width() - HAND_FORCE_PANEL_SIDE_MARGIN * 4.0) * 0.25).max(0.0); + let panel_width = side_width + .min(HAND_FORCE_PANEL_MAX_WIDTH) + .max(HAND_FORCE_PANEL_MIN_WIDTH.min(side_width)); + let left_x = screen.left() + HAND_FORCE_PANEL_SIDE_MARGIN; + let right_x = screen.right() - HAND_FORCE_PANEL_SIDE_MARGIN - panel_width; + let left_count = 4usize; + let right_count = HAND_FORCE_PANEL_TITLES.len() - left_count; + let available_height = (screen.height() - HAND_FORCE_PANEL_VERTICAL_MARGIN * 2.0).max(0.0); + let panel_height = ((available_height - (left_count - 1) as f32 * HAND_FORCE_PANEL_GAP) + / left_count as f32) + .clamp( + HAND_FORCE_PANEL_MIN_HEIGHT.min(available_height), + HAND_FORCE_PANEL_MAX_HEIGHT, + ); + let left_height = + left_count as f32 * panel_height + (left_count - 1) as f32 * HAND_FORCE_PANEL_GAP; + let right_height = right_count as f32 * panel_height + + (right_count.saturating_sub(1)) as f32 * HAND_FORCE_PANEL_GAP; + let left_top = screen.center().y - left_height * 0.5; + let right_top = screen.center().y - right_height * 0.5; for index in 0..HAND_FORCE_PANEL_TITLES.len() { let history = histories.get(index).map(Vec::as_slice).unwrap_or(&[]); let (code, title) = HAND_FORCE_PANEL_TITLES[index]; - let row_count: usize = if index < 4 { 4 } else { 3 }; - let row_index = if index < 4 { index } else { index - 4 }; - let total_width = row_count as f32 * HAND_FORCE_PANEL_WIDTH - + (row_count.saturating_sub(1)) as f32 * HAND_FORCE_PANEL_GAP; - let row_left = screen.center().x - total_width * 0.5; - let target_x = - row_left + row_index as f32 * (HAND_FORCE_PANEL_WIDTH + HAND_FORCE_PANEL_GAP); - let target_y = if index < 4 { first_row_y } else { second_row_y }; + let (target_x, target_y) = if index < left_count { + ( + left_x, + left_top + index as f32 * (panel_height + HAND_FORCE_PANEL_GAP), + ) + } else { + let right_index = index - left_count; + ( + right_x, + right_top + right_index as f32 * (panel_height + HAND_FORCE_PANEL_GAP), + ) + }; draw_force_chart_area( ctx, egui::Id::new(format!("hand_force_panel_{index}")), egui::pos2(target_x, target_y), - egui::vec2(HAND_FORCE_PANEL_WIDTH, HAND_FORCE_PANEL_HEIGHT), + egui::vec2(panel_width, panel_height), target_visible, code, title, @@ -1079,7 +1107,10 @@ fn draw_floating_panel( .show(ctx, |ui| { ui.spacing_mut().item_spacing = egui::vec2(METRICS.item_gap, 6.0); ui.horizontal(|ui| { - if ui.add(tag_button("隐藏")).clicked() { + if ui + .add(rich_tag_button("隐藏", style::ONE_DARK_PRO.text_dim)) + .clicked() + { hide_requested = true; } ui.add_space(6.0); @@ -1685,7 +1716,10 @@ pub fn draw_matrix_config_panel( (48, 24, "48×24"), (64, 32, "64×32"), ] { - if ui.add(tag_button(*name)).clicked() { + if ui + .add(rich_tag_button(*name, style::ONE_DARK_PRO.text_dim)) + .clicked() + { config.rows = *r; config.cols = *c; } @@ -1715,7 +1749,10 @@ pub fn draw_matrix_config_panel( ui.add_space(METRICS.row_gap); - if ui.add(tag_button("重置默认")).clicked() { + if ui + .add(rich_tag_button("重置默认", style::ONE_DARK_PRO.text_dim)) + .clicked() + { *config = MatrixConfigState::default(); } });