Improve hand mode panels and recording
This commit is contained in:
25
src/app.rs
25
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),
|
||||
|
||||
@@ -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<Vec<i32>>,
|
||||
stats_tx: &Sender<SerialIoStats>,
|
||||
latest_sample: &Arc<Mutex<Option<PressureSample>>>,
|
||||
recorder: Recorder,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
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() {
|
||||
|
||||
@@ -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<Vec<i32>>,
|
||||
stats_tx: Option<&Sender<SerialIoStats>>,
|
||||
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<Vec<i32>>,
|
||||
stats_tx: Option<&Sender<SerialIoStats>>,
|
||||
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<u32> =
|
||||
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<Vec<i32>>,
|
||||
stats_tx: Option<&Sender<SerialIoStats>>,
|
||||
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<u32> =
|
||||
vals.iter().map(|v| (*v).max(0) as u32).collect();
|
||||
recorder.add_frame(&pressures);
|
||||
}
|
||||
let _ = sample_tx.try_send(vals);
|
||||
}
|
||||
}
|
||||
|
||||
15
src/style.rs
15
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::WidgetText>) -> egui::Button<'static> {
|
||||
.min_size(egui::vec2(0.0, METRICS.button_height))
|
||||
}
|
||||
|
||||
pub fn rich_tag_button(
|
||||
label: impl Into<String>,
|
||||
color: impl Into<Color32>,
|
||||
) -> 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::WidgetText>) -> egui::Button<'static> {
|
||||
egui::Button::new(label)
|
||||
.fill(ONE_DARK_PRO.accent)
|
||||
|
||||
83
src/ui.rs
83
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<SerialMode> {
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user