refactor: 配置面板改造为顶部固定栏 + 压力归一化优化
This commit is contained in:
51
src/app.rs
51
src/app.rs
@@ -217,7 +217,13 @@ impl EskinDesktopApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw_workspace(&mut self, ui: &mut egui::Ui) {
|
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) {
|
fn draw_split_workspace(&mut self, ui: &mut egui::Ui) {
|
||||||
@@ -475,17 +481,6 @@ impl EskinDesktopApp {
|
|||||||
} else {
|
} else {
|
||||||
panel_restore_item(ui, "统计", &mut self.stats_panel);
|
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();
|
ui.separator();
|
||||||
|
|
||||||
if ui
|
if ui
|
||||||
@@ -499,7 +494,6 @@ impl EskinDesktopApp {
|
|||||||
self.export_panel.visible = true;
|
self.export_panel.visible = true;
|
||||||
self.matrix_config_panel.visible = true;
|
self.matrix_config_panel.visible = true;
|
||||||
self.stats_panel.visible = true;
|
self.stats_panel.visible = true;
|
||||||
self.breakout_visible = true;
|
|
||||||
close_menu = true;
|
close_menu = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -676,16 +670,45 @@ fn normalize_pressure_samples(raw: &[u32]) -> PressureSamples {
|
|||||||
fn normalize_pressure_value(value: u32) -> [f32; 2] {
|
fn normalize_pressure_value(value: u32) -> [f32; 2] {
|
||||||
const RANGE_MIN: f32 = 0.0;
|
const RANGE_MIN: f32 = 0.0;
|
||||||
const RANGE_MAX: f32 = 7000.0;
|
const RANGE_MAX: f32 = 7000.0;
|
||||||
|
const DISPLAY_GAMMA: f32 = 0.45;
|
||||||
|
|
||||||
let raw_value = value as f32;
|
let raw_value = value as f32;
|
||||||
let mapped = ((raw_value - RANGE_MIN) / (RANGE_MAX - RANGE_MIN)).clamp(0.0, 1.0);
|
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 {
|
let display_value = if raw_value <= RANGE_MIN + 4.0 {
|
||||||
0.0
|
0.0
|
||||||
} else {
|
} else {
|
||||||
raw_value.round().min(9999.0)
|
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 {
|
impl eframe::App for EskinDesktopApp {
|
||||||
|
|||||||
@@ -1246,11 +1246,11 @@ fn build_hand_palm_dot_instances(
|
|||||||
|
|
||||||
for row in 0..chip.rows {
|
for row in 0..chip.rows {
|
||||||
for col in 0..chip.cols {
|
for col in 0..chip.cols {
|
||||||
let index = (row * chip.cols + col) as usize;
|
|
||||||
let offset = match chip_index {
|
let offset = match chip_index {
|
||||||
0 => HAND_PALM_HORIZONTAL_OFFSET,
|
0 => HAND_PALM_HORIZONTAL_OFFSET,
|
||||||
_ => HAND_PALM_VERTICAL_OFFSET,
|
_ => HAND_PALM_VERTICAL_OFFSET,
|
||||||
};
|
};
|
||||||
|
let index = hand_palm_pressure_index(chip_index, row, col, chip.rows, chip.cols);
|
||||||
let [normalized, display_value] =
|
let [normalized, display_value] =
|
||||||
sample_pressure_at(pressure, offset + index, index);
|
sample_pressure_at(pressure, offset + index, index);
|
||||||
|
|
||||||
@@ -1278,6 +1278,20 @@ fn build_hand_palm_dot_instances(
|
|||||||
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] {
|
fn sample_pressure_at(pressure: &[[f32; 2]], index: usize, fallback_index: usize) -> [f32; 2] {
|
||||||
pressure
|
pressure
|
||||||
.get(index)
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ pub const METRICS: DesignMetrics = DesignMetrics {
|
|||||||
|
|
||||||
pub mod layout {
|
pub mod layout {
|
||||||
pub const TITLE_BAR_HEIGHT: f32 = 36.0;
|
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 CENTER_PANEL_TOP: f32 = 48.0;
|
||||||
pub const LEFT_X: f32 = 24.0;
|
pub const LEFT_X: f32 = 24.0;
|
||||||
pub const RIGHT_X: f32 = 1328.0;
|
pub const RIGHT_X: f32 = 1328.0;
|
||||||
|
|||||||
149
src/ui.rs
149
src/ui.rs
@@ -387,47 +387,134 @@ fn draw_connect_action_row(
|
|||||||
|
|
||||||
pub fn draw_config_panel(
|
pub fn draw_config_panel(
|
||||||
ctx: &egui::Context,
|
ctx: &egui::Context,
|
||||||
panel: &mut FloatingPanelState,
|
_panel: &mut FloatingPanelState,
|
||||||
config: &mut ConfigPanelState,
|
config: &mut ConfigPanelState,
|
||||||
connection: &ConnectionManager,
|
connection: &ConnectionManager,
|
||||||
recorder: &Recorder,
|
recorder: &Recorder,
|
||||||
stats: SerialIoStats,
|
_stats: SerialIoStats,
|
||||||
sample_rate_hz: f32,
|
sample_rate_hz: f32,
|
||||||
render_rate_hz: f32,
|
render_rate_hz: f32,
|
||||||
) -> Option<SerialMode> {
|
) -> Option<SerialMode> {
|
||||||
let mut changed_mode = None;
|
let changed_mode = None;
|
||||||
let conn_state = connection.state();
|
let conn_state = connection.state();
|
||||||
let panel_width = responsive_panel_width(ctx, 560.0, 340.0);
|
|
||||||
config.connected = matches!(
|
config.connected = matches!(
|
||||||
conn_state,
|
conn_state,
|
||||||
ConnectionState::Connected | ConnectionState::Streaming
|
ConnectionState::Connected | ConnectionState::Streaming
|
||||||
);
|
);
|
||||||
panel.visible = true;
|
|
||||||
|
|
||||||
draw_pinned_panel(ctx, panel, "配置", "config_panel", 560.0, 340.0, |ui| {
|
let screen = ctx.content_rect();
|
||||||
ui.set_min_width(panel_width);
|
let bar_rect = egui::Rect::from_min_size(
|
||||||
ui.set_max_width(panel_width);
|
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) {
|
egui::Area::new(egui::Id::new("config_top_bar"))
|
||||||
changed_mode = Some(mode);
|
.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
|
changed_mode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1261,7 +1348,7 @@ fn paint_spatial_force_gauge(
|
|||||||
|
|
||||||
fn draw_pinned_panel(
|
fn draw_pinned_panel(
|
||||||
ctx: &egui::Context,
|
ctx: &egui::Context,
|
||||||
panel: &mut FloatingPanelState,
|
_panel: &mut FloatingPanelState,
|
||||||
title: &'static str,
|
title: &'static str,
|
||||||
id: &'static str,
|
id: &'static str,
|
||||||
preferred_width: f32,
|
preferred_width: f32,
|
||||||
@@ -1270,7 +1357,11 @@ fn draw_pinned_panel(
|
|||||||
) {
|
) {
|
||||||
let panel_width = responsive_panel_width(ctx, preferred_width, min_width);
|
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 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)
|
egui::Window::new(title)
|
||||||
.id(egui::Id::new(id))
|
.id(egui::Id::new(id))
|
||||||
|
|||||||
Reference in New Issue
Block a user