From c4bccc17470248861904afcc1bdd29f3a0b46475 Mon Sep 17 00:00:00 2001 From: lenn Date: Mon, 6 Jul 2026 14:07:23 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=85=8D=E7=BD=AE=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF=E6=94=B9=E9=80=A0=E4=B8=BA=E9=A1=B6=E9=83=A8=E5=9B=BA?= =?UTF-8?q?=E5=AE=9A=E6=A0=8F=20+=20=E5=8E=8B=E5=8A=9B=E5=BD=92=E4=B8=80?= =?UTF-8?q?=E5=8C=96=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.rs | 51 ++++++++++++----- src/render.rs | 37 ++++++++++++- src/style.rs | 1 + src/ui.rs | 149 ++++++++++++++++++++++++++++++++++++++++---------- 4 files changed, 194 insertions(+), 44 deletions(-) diff --git a/src/app.rs b/src/app.rs index 684bb9f..801bd56 100644 --- a/src/app.rs +++ b/src/app.rs @@ -217,7 +217,13 @@ impl EskinDesktopApp { } fn draw_workspace(&mut self, ui: &mut egui::Ui) { - self.draw_wgpu_background_rect(ui, ui.max_rect()); + let screen = ui.max_rect(); + let workspace = egui::Rect::from_min_max( + screen.left_top() + + egui::vec2(0.0, layout::TITLE_BAR_HEIGHT + layout::CONFIG_BAR_HEIGHT), + screen.right_bottom(), + ); + self.draw_wgpu_background_rect(ui, workspace); } fn draw_split_workspace(&mut self, ui: &mut egui::Ui) { @@ -475,17 +481,6 @@ impl EskinDesktopApp { } else { panel_restore_item(ui, "统计", &mut self.stats_panel); } - if ui - .add_sized( - egui::vec2(ui.available_width(), 0.0), - egui::Button::new("打砖块"), - ) - .clicked() - { - self.breakout_visible = true; - close_menu = true; - } - ui.separator(); if ui @@ -499,7 +494,6 @@ impl EskinDesktopApp { self.export_panel.visible = true; self.matrix_config_panel.visible = true; self.stats_panel.visible = true; - self.breakout_visible = true; close_menu = true; } }); @@ -676,16 +670,45 @@ fn normalize_pressure_samples(raw: &[u32]) -> PressureSamples { fn normalize_pressure_value(value: u32) -> [f32; 2] { const RANGE_MIN: f32 = 0.0; const RANGE_MAX: f32 = 7000.0; + const DISPLAY_GAMMA: f32 = 0.45; let raw_value = value as f32; let mapped = ((raw_value - RANGE_MIN) / (RANGE_MAX - RANGE_MIN)).clamp(0.0, 1.0); + let intensity = if raw_value <= RANGE_MIN + 4.0 { + 0.0 + } else { + mapped.powf(DISPLAY_GAMMA) + }; let display_value = if raw_value <= RANGE_MIN + 4.0 { 0.0 } else { raw_value.round().min(9999.0) }; - [mapped, display_value] + [intensity, display_value] +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn pressure_normalization_makes_low_hand_values_visible() { + let [zero_intensity, zero_label] = normalize_pressure_value(4); + assert_eq!(zero_intensity, 0.0); + assert_eq!(zero_label, 0.0); + + let [low_intensity, low_label] = normalize_pressure_value(100); + assert!( + low_intensity > 0.10, + "low hand-gateway values should be visible in the wgpu dot matrix" + ); + assert_eq!(low_label, 100.0); + + let [max_intensity, max_label] = normalize_pressure_value(7000); + assert_eq!(max_intensity, 1.0); + assert_eq!(max_label, 7000.0); + } } impl eframe::App for EskinDesktopApp { diff --git a/src/render.rs b/src/render.rs index 4883993..022724c 100644 --- a/src/render.rs +++ b/src/render.rs @@ -1246,11 +1246,11 @@ fn build_hand_palm_dot_instances( for row in 0..chip.rows { for col in 0..chip.cols { - let index = (row * chip.cols + col) as usize; let offset = match chip_index { 0 => HAND_PALM_HORIZONTAL_OFFSET, _ => HAND_PALM_VERTICAL_OFFSET, }; + let index = hand_palm_pressure_index(chip_index, row, col, chip.rows, chip.cols); let [normalized, display_value] = sample_pressure_at(pressure, offset + index, index); @@ -1278,6 +1278,20 @@ fn build_hand_palm_dot_instances( instances } +fn hand_palm_pressure_index( + chip_index: usize, + row: u32, + col: u32, + rows: u32, + cols: u32, +) -> usize { + if chip_index == 0 { + (col * rows + (rows - 1 - row)) as usize + } else { + (row * cols + col) as usize + } +} + fn sample_pressure_at(pressure: &[[f32; 2]], index: usize, fallback_index: usize) -> [f32; 2] { pressure .get(index) @@ -1409,3 +1423,24 @@ impl GlyphInstance { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn hand_palm_horizontal_uses_column_major_pressure_order() { + assert_eq!(hand_palm_pressure_index(0, 0, 0, 5, 14), 4); + assert_eq!(hand_palm_pressure_index(0, 1, 0, 5, 14), 3); + assert_eq!(hand_palm_pressure_index(0, 0, 1, 5, 14), 9); + assert_eq!(hand_palm_pressure_index(0, 4, 13, 5, 14), 65); + } + + #[test] + fn hand_palm_vertical_keeps_row_major_pressure_order() { + assert_eq!(hand_palm_pressure_index(1, 0, 0, 11, 4), 0); + assert_eq!(hand_palm_pressure_index(1, 0, 1, 11, 4), 1); + assert_eq!(hand_palm_pressure_index(1, 1, 0, 11, 4), 4); + assert_eq!(hand_palm_pressure_index(1, 10, 3, 11, 4), 43); + } +} diff --git a/src/style.rs b/src/style.rs index 16bd2a2..96570e6 100644 --- a/src/style.rs +++ b/src/style.rs @@ -61,6 +61,7 @@ pub const METRICS: DesignMetrics = DesignMetrics { pub mod layout { pub const TITLE_BAR_HEIGHT: f32 = 36.0; + pub const CONFIG_BAR_HEIGHT: f32 = 38.0; pub const CENTER_PANEL_TOP: f32 = 48.0; pub const LEFT_X: f32 = 24.0; pub const RIGHT_X: f32 = 1328.0; diff --git a/src/ui.rs b/src/ui.rs index be0769f..0377b65 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -387,47 +387,134 @@ fn draw_connect_action_row( pub fn draw_config_panel( ctx: &egui::Context, - panel: &mut FloatingPanelState, + _panel: &mut FloatingPanelState, config: &mut ConfigPanelState, connection: &ConnectionManager, recorder: &Recorder, - stats: SerialIoStats, + _stats: SerialIoStats, sample_rate_hz: f32, render_rate_hz: f32, ) -> Option { - let mut changed_mode = None; + let changed_mode = None; let conn_state = connection.state(); - let panel_width = responsive_panel_width(ctx, 560.0, 340.0); config.connected = matches!( conn_state, ConnectionState::Connected | ConnectionState::Streaming ); - panel.visible = true; - draw_pinned_panel(ctx, panel, "配置", "config_panel", 560.0, 340.0, |ui| { - ui.set_min_width(panel_width); - ui.set_max_width(panel_width); + let screen = ctx.content_rect(); + let bar_rect = egui::Rect::from_min_size( + egui::pos2(screen.left(), screen.top() + layout::TITLE_BAR_HEIGHT), + egui::vec2(screen.width(), layout::CONFIG_BAR_HEIGHT), + ); - if let Some(mode) = draw_mode_row(ui, config) { - changed_mode = Some(mode); - } + egui::Area::new(egui::Id::new("config_top_bar")) + .fixed_pos(bar_rect.min) + .order(egui::Order::Foreground) + .show(ctx, |ui| { + ui.set_min_size(bar_rect.size()); + ui.painter() + .rect_filled(ui.max_rect(), egui::CornerRadius::ZERO, ONE_DARK_PRO.panel_deep); + ui.painter().line_segment( + [ui.max_rect().left_top(), ui.max_rect().right_top()], + egui::Stroke::new(1.0, ONE_DARK_PRO.border_soft), + ); + ui.painter().line_segment( + [ui.max_rect().left_bottom(), ui.max_rect().right_bottom()], + egui::Stroke::new(1.0, ONE_DARK_PRO.border_soft), + ); + + ui.add_space(5.0); + ui.horizontal(|ui| { + ui.add_space(8.0); + ui.spacing_mut().item_spacing = egui::vec2(8.0, 0.0); + config.mode = SerialMode::Hand; + config.baud_rate = SerialMode::Hand.baud_rate(); + + ui.colored_label(dim_text(), "配置面板"); + ui.add_space(8.0); + ui.colored_label(dim_text(), "模式"); + ui.add_sized( + egui::vec2(102.0, 28.0), + style::mode_button("手掌模块", true), + ); + + ui.add_space(12.0); + ui.label(style::field_label("端口")); + egui::ComboBox::from_id_salt("config_top_bar_ports") + .width(180.0) + .selected_text(if config.port.is_empty() { + "无可用串口".to_owned() + } else { + config.port.clone() + }) + .show_ui(ui, |ui| { + for port in &config.available_ports { + ui.selectable_value(&mut config.port, port.clone(), port.as_str()); + } + }); + + if ui + .add(style::icon_button( + egui::RichText::new("⟳").color(ONE_DARK_PRO.text).size(16.0), + METRICS.icon_button, + )) + .on_hover_text("刷新串口") + .clicked() + { + refresh_config_ports(config); + } + + ui.add_space(10.0); + ui.label(style::value_text(format!("波特率 {}", config.baud_rate))); + ui.add_space(10.0); + ui.colored_label(dim_text(), "状态"); + ui.colored_label( + connection_status_color(conn_state), + connection_status_text(conn_state), + ); + + ui.add_space(8.0); + let is_connected = matches!( + conn_state, + ConnectionState::Connected | ConnectionState::Streaming + ); + let button_text = if is_connected { "断开" } else { "连接" }; + if ui + .add_sized( + egui::vec2(118.0, 28.0), + if is_connected { + style::danger_button(button_text) + } else { + style::primary_button(button_text) + }, + ) + .clicked() + { + if is_connected { + connection.disconnect(); + } else if !config.port.is_empty() { + connection.connect( + &config.port, + 12, + 7, + config.mode.baud_rate(), + config.mode.protocol(), + recorder.clone(), + ); + } + } + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + ui.label(style::value_text(format!("{render_rate_hz:.1} Hz"))); + ui.colored_label(dim_text(), "画面刷新率"); + ui.add_space(18.0); + ui.label(style::value_text(format!("{sample_rate_hz:.1} Hz"))); + ui.colored_label(dim_text(), "采样率"); + }); + }); + }); - // draw_mode_row(ui, config); - ui.separator(); - 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); - // ui.add_space(8.0); - draw_mode_body( - ui, - config, - conn_state, - stats, - sample_rate_hz, - render_rate_hz, - ); - }); changed_mode } @@ -1261,7 +1348,7 @@ fn paint_spatial_force_gauge( fn draw_pinned_panel( ctx: &egui::Context, - panel: &mut FloatingPanelState, + _panel: &mut FloatingPanelState, title: &'static str, id: &'static str, preferred_width: f32, @@ -1270,7 +1357,11 @@ fn draw_pinned_panel( ) { let panel_width = responsive_panel_width(ctx, preferred_width, min_width); let max_height = responsive_panel_height(ctx, ctx.content_rect().height(), 180.0); - let pos = responsive_panel_pos(ctx, panel.default_pos, panel_width); + let viewport = viewport_panel_rect(ctx); + let x = (viewport.center().x - panel_width * 0.5) + .clamp(viewport.left(), (viewport.right() - panel_width).max(viewport.left())); + let y = viewport.top() + layout::TITLE_BAR_HEIGHT + PANEL_VIEWPORT_MARGIN; + let pos = egui::pos2(x, y); egui::Window::new(title) .id(egui::Id::new(id))