Compare commits
2 Commits
master
...
waic-finge
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9efe360a1d | ||
|
|
71d314ac44 |
140
src/app.rs
140
src/app.rs
@@ -13,16 +13,19 @@ use crate::{
|
|||||||
},
|
},
|
||||||
ui::{
|
ui::{
|
||||||
ConfigPanelState, ConnectPanelState, FloatingPanelState, MatrixConfigState,
|
ConfigPanelState, ConnectPanelState, FloatingPanelState, MatrixConfigState,
|
||||||
draw_config_panel, draw_export_panel, draw_hand_force_panels, draw_matrix_config_panel,
|
draw_config_panel, draw_spatial_force_panel, draw_stats_panel, panel_restore_item,
|
||||||
draw_stats_panel, panel_restore_item,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use eframe::{egui, egui_wgpu};
|
use eframe::{egui, egui_wgpu};
|
||||||
use std::sync::Arc;
|
use std::{
|
||||||
|
sync::Arc,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
const SUMMARY_POINTS_PER_SERIES: usize = 42;
|
const SUMMARY_POINTS_PER_SERIES: usize = 42;
|
||||||
const HAND_FORCE_PANEL_COUNT: usize = 7;
|
const HAND_FORCE_PANEL_COUNT: usize = 7;
|
||||||
const HAND_FORCE_SEGMENT_COUNTS: [usize; HAND_FORCE_PANEL_COUNT] = [84, 84, 84, 84, 84, 70, 44];
|
const HAND_FORCE_SEGMENT_COUNTS: [usize; HAND_FORCE_PANEL_COUNT] = [84, 84, 84, 84, 84, 70, 44];
|
||||||
|
const RATE_UPDATE_INTERVAL: Duration = Duration::from_millis(500);
|
||||||
|
|
||||||
pub struct EskinDesktopApp {
|
pub struct EskinDesktopApp {
|
||||||
connect_panel: FloatingPanelState,
|
connect_panel: FloatingPanelState,
|
||||||
@@ -44,16 +47,61 @@ pub struct EskinDesktopApp {
|
|||||||
hand_signal_histories: [Vec<f32>; HAND_FORCE_PANEL_COUNT],
|
hand_signal_histories: [Vec<f32>; HAND_FORCE_PANEL_COUNT],
|
||||||
force_estimator: ForceEstimatorState,
|
force_estimator: ForceEstimatorState,
|
||||||
latest_spatial_force: Option<HudSpatialForce>,
|
latest_spatial_force: Option<HudSpatialForce>,
|
||||||
|
spatial_force_panel_force: Option<HudSpatialForce>,
|
||||||
latest_raw_matrix: Vec<u32>,
|
latest_raw_matrix: Vec<u32>,
|
||||||
latest_matrix_rows: u32,
|
latest_matrix_rows: u32,
|
||||||
latest_matrix_cols: u32,
|
latest_matrix_cols: u32,
|
||||||
breakout_visible: bool,
|
breakout_visible: bool,
|
||||||
breakout_game: BreakoutGame,
|
breakout_game: BreakoutGame,
|
||||||
|
live_rates: LiveRateMeters,
|
||||||
context_menu_open: bool,
|
context_menu_open: bool,
|
||||||
context_menu_pos: egui::Pos2,
|
context_menu_pos: egui::Pos2,
|
||||||
active_mode: ActiveMode,
|
active_mode: ActiveMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct LiveRateMeters {
|
||||||
|
window_started_at: Instant,
|
||||||
|
window_rx_frames: u64,
|
||||||
|
window_render_frames: u32,
|
||||||
|
sample_rate_hz: f32,
|
||||||
|
render_rate_hz: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LiveRateMeters {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
window_started_at: Instant::now(),
|
||||||
|
window_rx_frames: 0,
|
||||||
|
window_render_frames: 0,
|
||||||
|
sample_rate_hz: 0.0,
|
||||||
|
render_rate_hz: 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tick_render(&mut self, total_rx_frames: u64) {
|
||||||
|
if total_rx_frames < self.window_rx_frames {
|
||||||
|
self.window_rx_frames = total_rx_frames;
|
||||||
|
self.sample_rate_hz = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.window_render_frames = self.window_render_frames.saturating_add(1);
|
||||||
|
|
||||||
|
let now = Instant::now();
|
||||||
|
let elapsed = now.duration_since(self.window_started_at);
|
||||||
|
if elapsed < RATE_UPDATE_INTERVAL {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let seconds = elapsed.as_secs_f32().max(0.001);
|
||||||
|
let rx_delta = total_rx_frames.saturating_sub(self.window_rx_frames);
|
||||||
|
self.sample_rate_hz = rx_delta as f32 / seconds;
|
||||||
|
self.render_rate_hz = self.window_render_frames as f32 / seconds;
|
||||||
|
self.window_started_at = now;
|
||||||
|
self.window_rx_frames = total_rx_frames;
|
||||||
|
self.window_render_frames = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl EskinDesktopApp {
|
impl EskinDesktopApp {
|
||||||
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
||||||
egui_extras::install_image_loaders(&cc.egui_ctx);
|
egui_extras::install_image_loaders(&cc.egui_ctx);
|
||||||
@@ -110,11 +158,13 @@ impl EskinDesktopApp {
|
|||||||
hand_signal_histories: std::array::from_fn(|_| Vec::with_capacity(128)),
|
hand_signal_histories: std::array::from_fn(|_| Vec::with_capacity(128)),
|
||||||
force_estimator: ForceEstimatorState::new(),
|
force_estimator: ForceEstimatorState::new(),
|
||||||
latest_spatial_force: None,
|
latest_spatial_force: None,
|
||||||
|
spatial_force_panel_force: None,
|
||||||
latest_raw_matrix: Vec::new(),
|
latest_raw_matrix: Vec::new(),
|
||||||
latest_matrix_rows: MATRIX_ROWS,
|
latest_matrix_rows: MATRIX_ROWS,
|
||||||
latest_matrix_cols: MATRIX_COLS,
|
latest_matrix_cols: MATRIX_COLS,
|
||||||
breakout_visible: false,
|
breakout_visible: false,
|
||||||
breakout_game: BreakoutGame::default(),
|
breakout_game: BreakoutGame::default(),
|
||||||
|
live_rates: LiveRateMeters::new(),
|
||||||
context_menu_open: false,
|
context_menu_open: false,
|
||||||
context_menu_pos: egui::pos2(24.0, 48.0),
|
context_menu_pos: egui::pos2(24.0, 48.0),
|
||||||
active_mode: ActiveMode::Finger(FingerMode {
|
active_mode: ActiveMode::Finger(FingerMode {
|
||||||
@@ -173,19 +223,23 @@ impl EskinDesktopApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw_workspace(&mut self, ui: &mut egui::Ui) {
|
fn draw_workspace(&mut self, ui: &mut egui::Ui) {
|
||||||
|
let screen = ui.max_rect();
|
||||||
|
let workspace = egui::Rect::from_min_max(
|
||||||
|
screen.left_top() + egui::vec2(0.0, layout::WORKSPACE_TOP),
|
||||||
|
screen.right_bottom(),
|
||||||
|
);
|
||||||
|
|
||||||
if self.breakout_visible {
|
if self.breakout_visible {
|
||||||
self.draw_split_workspace(ui);
|
self.draw_split_workspace(ui, workspace);
|
||||||
} else {
|
} else {
|
||||||
self.draw_wgpu_background_rect(ui, ui.max_rect());
|
ui.painter()
|
||||||
|
.rect_filled(screen, egui::CornerRadius::ZERO, ONE_DARK_PRO.bg);
|
||||||
|
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, content: egui::Rect) {
|
||||||
let screen = ui.max_rect();
|
let screen = ui.max_rect();
|
||||||
let content = egui::Rect::from_min_max(
|
|
||||||
screen.left_top() + egui::vec2(0.0, layout::TITLE_BAR_HEIGHT),
|
|
||||||
screen.right_bottom(),
|
|
||||||
);
|
|
||||||
let gutter = 10.0;
|
let gutter = 10.0;
|
||||||
let half_width = (content.width() - gutter).max(1.0) * 0.5;
|
let half_width = (content.width() - gutter).max(1.0) * 0.5;
|
||||||
let left = egui::Rect::from_min_size(content.min, egui::vec2(half_width, content.height()));
|
let left = egui::Rect::from_min_size(content.min, egui::vec2(half_width, content.height()));
|
||||||
@@ -362,13 +416,16 @@ impl EskinDesktopApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_floating_panels(&mut self, ctx: &egui::Context) {
|
fn draw_floating_panels(
|
||||||
// draw_scene_panel(ctx, &mut self.scene_panel);
|
&mut self,
|
||||||
// if self.config_state.mode == SerialMode::Hand && self.stats_panel.visible {
|
ctx: &egui::Context,
|
||||||
// self.config_panel.visible = false;
|
stats: crate::serial_core::serial::SerialIoStats,
|
||||||
// self.export_panel.visible = false;
|
) {
|
||||||
// self.matrix_config_panel.visible = false;
|
self.scene_panel.visible = false;
|
||||||
// }
|
self.connect_panel.visible = false;
|
||||||
|
self.export_panel.visible = false;
|
||||||
|
self.matrix_config_panel.visible = false;
|
||||||
|
self.context_menu_open = false;
|
||||||
|
|
||||||
if let Some(next_mode) = draw_config_panel(
|
if let Some(next_mode) = draw_config_panel(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -376,29 +433,48 @@ impl EskinDesktopApp {
|
|||||||
&mut self.config_state,
|
&mut self.config_state,
|
||||||
&self.connection,
|
&self.connection,
|
||||||
&self.recorder,
|
&self.recorder,
|
||||||
|
stats,
|
||||||
|
self.live_rates.sample_rate_hz,
|
||||||
|
self.live_rates.render_rate_hz,
|
||||||
) {
|
) {
|
||||||
|
if next_mode == SerialMode::Hand {
|
||||||
|
self.config_state.mode = SerialMode::Finger;
|
||||||
|
self.connect_state.mode = SerialMode::Finger;
|
||||||
|
self.config_state.baud_rate = SerialMode::Finger.baud_rate();
|
||||||
|
self.active_mode = ActiveMode::Finger(FingerMode {
|
||||||
|
rows: self.matrix_config.rows,
|
||||||
|
cols: self.matrix_config.cols,
|
||||||
|
range: 0..7000,
|
||||||
|
dot: true,
|
||||||
|
});
|
||||||
|
self.breakout_visible = true;
|
||||||
|
} else {
|
||||||
self.switch_mode(next_mode);
|
self.switch_mode(next_mode);
|
||||||
}
|
}
|
||||||
match self.config_state.mode {
|
}
|
||||||
SerialMode::Finger => {
|
|
||||||
|
if self.breakout_visible {
|
||||||
|
self.stats_panel.visible = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.config_state.mode == SerialMode::Finger {
|
||||||
|
self.stats_panel.visible = true;
|
||||||
draw_stats_panel(
|
draw_stats_panel(
|
||||||
ctx,
|
ctx,
|
||||||
&mut self.stats_panel,
|
&mut self.stats_panel,
|
||||||
&self.signal_history,
|
&self.signal_history,
|
||||||
self.latest_spatial_force,
|
self.latest_spatial_force,
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
self.stats_panel.visible = false;
|
||||||
}
|
}
|
||||||
SerialMode::Hand => {
|
|
||||||
draw_hand_force_panels(ctx, self.stats_panel.visible, &self.hand_signal_histories);
|
if let Some(force) = self.latest_spatial_force {
|
||||||
|
self.spatial_force_panel_force = Some(force);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
draw_export_panel(
|
draw_spatial_force_panel(ctx, self.spatial_force_panel_force, &self.signal_history);
|
||||||
ctx,
|
|
||||||
&mut self.export_panel,
|
|
||||||
&self.recorder,
|
|
||||||
&mut self.export_path,
|
|
||||||
);
|
|
||||||
draw_matrix_config_panel(ctx, &mut self.matrix_config_panel, &mut self.matrix_config);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_panel_context_menu(&mut self, ctx: &egui::Context) {
|
fn draw_panel_context_menu(&mut self, ctx: &egui::Context) {
|
||||||
@@ -499,6 +575,7 @@ impl EskinDesktopApp {
|
|||||||
self.hand_pressure.clear();
|
self.hand_pressure.clear();
|
||||||
self.force_estimator.reset();
|
self.force_estimator.reset();
|
||||||
self.latest_spatial_force = None;
|
self.latest_spatial_force = None;
|
||||||
|
self.spatial_force_panel_force = None;
|
||||||
self.signal_history.clear();
|
self.signal_history.clear();
|
||||||
self.hand_signal_histories
|
self.hand_signal_histories
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
@@ -663,12 +740,13 @@ fn normalize_pressure_value(value: u32) -> [f32; 2] {
|
|||||||
impl eframe::App for EskinDesktopApp {
|
impl eframe::App for EskinDesktopApp {
|
||||||
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
|
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
|
||||||
let ctx = ui.ctx().clone();
|
let ctx = ui.ctx().clone();
|
||||||
|
let stats = self.connection.stats();
|
||||||
|
self.live_rates.tick_render(stats.rx_frames);
|
||||||
|
|
||||||
self.update_pressure_matrix();
|
self.update_pressure_matrix();
|
||||||
self.draw_workspace(ui);
|
self.draw_workspace(ui);
|
||||||
self.draw_title_bar(ui, frame);
|
self.draw_title_bar(ui, frame);
|
||||||
self.draw_floating_panels(&ctx);
|
self.draw_floating_panels(&ctx, stats);
|
||||||
self.draw_panel_context_menu(&ctx);
|
|
||||||
|
|
||||||
// Keep repainting while the wgpu background is a realtime viewport.
|
// Keep repainting while the wgpu background is a realtime viewport.
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ enum BreakoutPhase {
|
|||||||
Idle,
|
Idle,
|
||||||
Running,
|
Running,
|
||||||
Paused,
|
Paused,
|
||||||
|
Won,
|
||||||
Over,
|
Over,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +47,6 @@ pub struct BreakoutGame {
|
|||||||
score: u32,
|
score: u32,
|
||||||
combo: u32,
|
combo: u32,
|
||||||
lives: u32,
|
lives: u32,
|
||||||
level: u32,
|
|
||||||
last_time: Option<f64>,
|
last_time: Option<f64>,
|
||||||
previous_pause_gesture: bool,
|
previous_pause_gesture: bool,
|
||||||
pause_locked_until: f64,
|
pause_locked_until: f64,
|
||||||
@@ -63,7 +63,6 @@ impl Default for BreakoutGame {
|
|||||||
score: 0,
|
score: 0,
|
||||||
combo: 0,
|
combo: 0,
|
||||||
lives: 3,
|
lives: 3,
|
||||||
level: 1,
|
|
||||||
last_time: None,
|
last_time: None,
|
||||||
previous_pause_gesture: false,
|
previous_pause_gesture: false,
|
||||||
pause_locked_until: 0.0,
|
pause_locked_until: 0.0,
|
||||||
@@ -150,7 +149,6 @@ impl BreakoutGame {
|
|||||||
status_chip(ui, "分数", self.score.to_string(), ACCENT_GREEN);
|
status_chip(ui, "分数", self.score.to_string(), ACCENT_GREEN);
|
||||||
status_chip(ui, "连击", self.combo.to_string(), ACCENT_ORANGE);
|
status_chip(ui, "连击", self.combo.to_string(), ACCENT_ORANGE);
|
||||||
status_chip(ui, "生命", self.lives.to_string(), ACCENT_RED);
|
status_chip(ui, "生命", self.lives.to_string(), ACCENT_RED);
|
||||||
status_chip(ui, "关卡", self.level.to_string(), ACCENT_BLUE);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.add_space(7.0);
|
ui.add_space(7.0);
|
||||||
@@ -191,12 +189,11 @@ impl BreakoutGame {
|
|||||||
self.score = 0;
|
self.score = 0;
|
||||||
self.combo = 0;
|
self.combo = 0;
|
||||||
self.lives = 3;
|
self.lives = 3;
|
||||||
self.level = 1;
|
|
||||||
self.rebuild_bricks();
|
self.rebuild_bricks();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start(&mut self) {
|
fn start(&mut self) {
|
||||||
if self.phase == BreakoutPhase::Over {
|
if self.phase == BreakoutPhase::Over || self.phase == BreakoutPhase::Won {
|
||||||
self.reset();
|
self.reset();
|
||||||
}
|
}
|
||||||
if self.phase != BreakoutPhase::Running {
|
if self.phase != BreakoutPhase::Running {
|
||||||
@@ -219,7 +216,10 @@ impl BreakoutGame {
|
|||||||
let threshold = pressure_pause_threshold();
|
let threshold = pressure_pause_threshold();
|
||||||
let active = top_force >= threshold;
|
let active = top_force >= threshold;
|
||||||
if active && !self.previous_pause_gesture && now >= self.pause_locked_until {
|
if active && !self.previous_pause_gesture && now >= self.pause_locked_until {
|
||||||
if self.phase == BreakoutPhase::Idle || self.phase == BreakoutPhase::Over {
|
if matches!(
|
||||||
|
self.phase,
|
||||||
|
BreakoutPhase::Idle | BreakoutPhase::Over | BreakoutPhase::Won
|
||||||
|
) {
|
||||||
self.start();
|
self.start();
|
||||||
} else {
|
} else {
|
||||||
self.toggle_pause();
|
self.toggle_pause();
|
||||||
@@ -249,7 +249,7 @@ impl BreakoutGame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn launch_ball(&mut self) {
|
fn launch_ball(&mut self) {
|
||||||
let direction = if self.level % 2 == 0 { -0.24 } else { 0.24 };
|
let direction = 0.24;
|
||||||
self.ball_pos = egui::pos2(self.paddle_x, PADDLE_Y - PADDLE_H - BALL_RADIUS);
|
self.ball_pos = egui::pos2(self.paddle_x, PADDLE_Y - PADDLE_H - BALL_RADIUS);
|
||||||
self.ball_vel = egui::vec2(direction, -1.0).normalized() * BALL_SPEED;
|
self.ball_vel = egui::vec2(direction, -1.0).normalized() * BALL_SPEED;
|
||||||
}
|
}
|
||||||
@@ -324,9 +324,8 @@ impl BreakoutGame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if self.bricks.iter().all(|brick| !brick.alive) {
|
if self.bricks.iter().all(|brick| !brick.alive) {
|
||||||
self.level += 1;
|
self.phase = BreakoutPhase::Won;
|
||||||
self.rebuild_bricks();
|
self.ball_vel = egui::Vec2::ZERO;
|
||||||
self.launch_ball();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,6 +357,7 @@ impl BreakoutGame {
|
|||||||
BreakoutPhase::Idle => "待机",
|
BreakoutPhase::Idle => "待机",
|
||||||
BreakoutPhase::Running => "运行",
|
BreakoutPhase::Running => "运行",
|
||||||
BreakoutPhase::Paused => "暂停",
|
BreakoutPhase::Paused => "暂停",
|
||||||
|
BreakoutPhase::Won => "过关",
|
||||||
BreakoutPhase::Over => "结束",
|
BreakoutPhase::Over => "结束",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -387,6 +387,7 @@ impl BreakoutGame {
|
|||||||
|
|
||||||
if self.phase == BreakoutPhase::Idle
|
if self.phase == BreakoutPhase::Idle
|
||||||
|| self.phase == BreakoutPhase::Paused
|
|| self.phase == BreakoutPhase::Paused
|
||||||
|
|| self.phase == BreakoutPhase::Won
|
||||||
|| self.phase == BreakoutPhase::Over
|
|| self.phase == BreakoutPhase::Over
|
||||||
{
|
{
|
||||||
paint_center_overlay(&painter, rect, self.phase);
|
paint_center_overlay(&painter, rect, self.phase);
|
||||||
@@ -403,6 +404,10 @@ pub fn control_from_matrix(raw: &[u32], rows: u32, cols: u32) -> BreakoutControl
|
|||||||
let cols = cols.max(1) as usize;
|
let cols = cols.max(1) as usize;
|
||||||
let sample_rows = rows.min(2);
|
let sample_rows = rows.min(2);
|
||||||
let sample_cols = cols.min(2);
|
let sample_cols = cols.min(2);
|
||||||
|
let top_gesture_rows = rows.min(1);
|
||||||
|
let top_gesture_cols = (cols / 3).clamp(1, cols);
|
||||||
|
let top_gesture_col_start = (cols - top_gesture_cols) / 2;
|
||||||
|
let top_gesture_col_end = top_gesture_col_start + top_gesture_cols;
|
||||||
let avg = |row_start: usize, row_end: usize, col_start: usize, col_end: usize| -> f32 {
|
let avg = |row_start: usize, row_end: usize, col_start: usize, col_end: usize| -> f32 {
|
||||||
let mut sum = 0.0;
|
let mut sum = 0.0;
|
||||||
let mut count = 0.0;
|
let mut count = 0.0;
|
||||||
@@ -430,7 +435,12 @@ pub fn control_from_matrix(raw: &[u32], rows: u32, cols: u32) -> BreakoutControl
|
|||||||
|
|
||||||
let left_force = tl + bl;
|
let left_force = tl + bl;
|
||||||
let right_force = tr + br;
|
let right_force = tr + br;
|
||||||
let top_force = tl + tr;
|
let top_force = avg(
|
||||||
|
0,
|
||||||
|
top_gesture_rows,
|
||||||
|
top_gesture_col_start,
|
||||||
|
top_gesture_col_end,
|
||||||
|
);
|
||||||
let span = 1200.0_f32.max((PRESSURE_RANGE_MAX - PRESSURE_RANGE_MIN) * 0.22);
|
let span = 1200.0_f32.max((PRESSURE_RANGE_MAX - PRESSURE_RANGE_MIN) * 0.22);
|
||||||
let raw_axis = ((right_force - left_force) / span).clamp(-1.0, 1.0);
|
let raw_axis = ((right_force - left_force) / span).clamp(-1.0, 1.0);
|
||||||
let axis = if raw_axis.abs() < 0.045 {
|
let axis = if raw_axis.abs() < 0.045 {
|
||||||
@@ -485,6 +495,7 @@ fn status_color(phase: BreakoutPhase) -> egui::Color32 {
|
|||||||
BreakoutPhase::Idle => ONE_DARK_PRO.text_dim,
|
BreakoutPhase::Idle => ONE_DARK_PRO.text_dim,
|
||||||
BreakoutPhase::Running => ACCENT_GREEN,
|
BreakoutPhase::Running => ACCENT_GREEN,
|
||||||
BreakoutPhase::Paused => ACCENT_ORANGE,
|
BreakoutPhase::Paused => ACCENT_ORANGE,
|
||||||
|
BreakoutPhase::Won => ACCENT_GREEN,
|
||||||
BreakoutPhase::Over => ACCENT_RED,
|
BreakoutPhase::Over => ACCENT_RED,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -607,6 +618,7 @@ fn paint_center_overlay(painter: &egui::Painter, rect: egui::Rect, phase: Breako
|
|||||||
let (title, detail) = match phase {
|
let (title, detail) = match phase {
|
||||||
BreakoutPhase::Idle => ("按压顶部或点击开始", "左右分区压力控制挡板"),
|
BreakoutPhase::Idle => ("按压顶部或点击开始", "左右分区压力控制挡板"),
|
||||||
BreakoutPhase::Paused => ("已暂停", "再次按压顶部、P 或点击暂停继续"),
|
BreakoutPhase::Paused => ("已暂停", "再次按压顶部、P 或点击暂停继续"),
|
||||||
|
BreakoutPhase::Won => ("恭喜过关", "按压顶部、点击或空格重新开始"),
|
||||||
BreakoutPhase::Over => ("游戏结束", "点击或空格重开"),
|
BreakoutPhase::Over => ("游戏结束", "点击或空格重开"),
|
||||||
BreakoutPhase::Running => ("", ""),
|
BreakoutPhase::Running => ("", ""),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const DEFAULT_HAND_GATEWAY_NODE_SAMPLE_COUNTS: &[u16] = &[84, 84, 84, 84, 84, 70
|
|||||||
pub struct SerialIoStats {
|
pub struct SerialIoStats {
|
||||||
pub rx_bytes: u64,
|
pub rx_bytes: u64,
|
||||||
pub tx_bytes: u64,
|
pub tx_bytes: u64,
|
||||||
|
pub rx_frames: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -101,6 +102,8 @@ fn run_tactile_a_loop(
|
|||||||
recorder.add_frame(&pressures);
|
recorder.add_frame(&pressures);
|
||||||
}
|
}
|
||||||
let _ = sample_tx.try_send(vals);
|
let _ = sample_tx.try_send(vals);
|
||||||
|
io_stats.rx_frames += 1;
|
||||||
|
publish_stats(stats_tx, io_stats);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,6 +194,8 @@ fn run_hand_gateway_loop(
|
|||||||
recorder.add_frame(&pressures);
|
recorder.add_frame(&pressures);
|
||||||
}
|
}
|
||||||
let _ = sample_tx.try_send(vals);
|
let _ = sample_tx.try_send(vals);
|
||||||
|
io_stats.rx_frames += 1;
|
||||||
|
publish_stats(stats_tx, io_stats);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ 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 = 48.0;
|
||||||
|
pub const WORKSPACE_TOP: f32 = TITLE_BAR_HEIGHT + CONFIG_BAR_HEIGHT;
|
||||||
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;
|
||||||
|
|||||||
419
src/ui.rs
419
src/ui.rs
@@ -157,7 +157,7 @@ fn responsive_panel_width(ctx: &egui::Context, preferred: f32, min_width: f32) -
|
|||||||
|
|
||||||
fn responsive_panel_height(ctx: &egui::Context, preferred: f32, min_height: f32) -> f32 {
|
fn responsive_panel_height(ctx: &egui::Context, preferred: f32, min_height: f32) -> f32 {
|
||||||
let screen = ctx.content_rect();
|
let screen = ctx.content_rect();
|
||||||
let available = (screen.height() - layout::TITLE_BAR_HEIGHT - PANEL_VIEWPORT_MARGIN * 2.0)
|
let available = (screen.height() - layout::WORKSPACE_TOP - PANEL_VIEWPORT_MARGIN * 2.0)
|
||||||
.max(min_height.min(screen.height()));
|
.max(min_height.min(screen.height()));
|
||||||
let height_cap = (screen.height() * 0.72).max(min_height.min(available));
|
let height_cap = (screen.height() * 0.72).max(min_height.min(available));
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ fn responsive_panel_pos(
|
|||||||
panel_width: f32,
|
panel_width: f32,
|
||||||
) -> egui::Pos2 {
|
) -> egui::Pos2 {
|
||||||
let rect = viewport_panel_rect(ctx);
|
let rect = viewport_panel_rect(ctx);
|
||||||
let min_y = rect.top() + layout::TITLE_BAR_HEIGHT + PANEL_VIEWPORT_MARGIN;
|
let min_y = rect.top() + layout::WORKSPACE_TOP + PANEL_VIEWPORT_MARGIN;
|
||||||
let max_x = (rect.right() - panel_width).max(rect.left());
|
let max_x = (rect.right() - panel_width).max(rect.left());
|
||||||
let preferred_right_side = preferred.x > rect.center().x;
|
let preferred_right_side = preferred.x > rect.center().x;
|
||||||
let x = if preferred_right_side {
|
let x = if preferred_right_side {
|
||||||
@@ -391,36 +391,236 @@ pub fn draw_config_panel(
|
|||||||
config: &mut ConfigPanelState,
|
config: &mut ConfigPanelState,
|
||||||
connection: &ConnectionManager,
|
connection: &ConnectionManager,
|
||||||
recorder: &Recorder,
|
recorder: &Recorder,
|
||||||
|
stats: SerialIoStats,
|
||||||
|
sample_rate_hz: f32,
|
||||||
|
render_rate_hz: f32,
|
||||||
) -> Option<SerialMode> {
|
) -> Option<SerialMode> {
|
||||||
let mut changed_mode = None;
|
let mut changed_mode = None;
|
||||||
let conn_state = connection.state();
|
let conn_state = connection.state();
|
||||||
let stats = connection.stats();
|
let screen = ctx.content_rect();
|
||||||
let panel_width = responsive_panel_width(ctx, 560.0, 340.0);
|
let bar_rect = egui::Rect::from_min_size(
|
||||||
|
screen.left_top() + egui::vec2(0.0, layout::TITLE_BAR_HEIGHT),
|
||||||
|
egui::vec2(screen.width(), layout::CONFIG_BAR_HEIGHT),
|
||||||
|
);
|
||||||
|
|
||||||
config.connected = matches!(
|
config.connected = matches!(
|
||||||
conn_state,
|
conn_state,
|
||||||
ConnectionState::Connected | ConnectionState::Streaming
|
ConnectionState::Connected | ConnectionState::Streaming
|
||||||
);
|
);
|
||||||
|
panel.visible = true;
|
||||||
|
|
||||||
draw_floating_panel(ctx, panel, "配置", "config_panel", 560.0, 340.0, |ui| {
|
egui::Area::new(egui::Id::new("config_panel_bar"))
|
||||||
ui.set_min_width(panel_width);
|
.fixed_pos(bar_rect.min)
|
||||||
ui.set_max_width(panel_width);
|
.order(egui::Order::Foreground)
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
ui.set_min_size(bar_rect.size());
|
||||||
|
ui.set_max_size(bar_rect.size());
|
||||||
|
|
||||||
if let Some(mode) = draw_mode_row(ui, config) {
|
config_bar_frame().show(ui, |ui| {
|
||||||
|
ui.set_min_width(bar_rect.width());
|
||||||
|
ui.set_max_width(bar_rect.width());
|
||||||
|
ui.spacing_mut().item_spacing = egui::vec2(METRICS.item_gap, 6.0);
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label(style::panel_title("配置面板"));
|
||||||
|
ui.add_space(12.0);
|
||||||
|
|
||||||
|
if let Some(mode) = draw_config_bar_mode(ui, config) {
|
||||||
changed_mode = Some(mode);
|
changed_mode = Some(mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// draw_mode_row(ui, config);
|
ui.add_space(12.0);
|
||||||
ui.separator();
|
draw_config_bar_connection(ui, config, connection, recorder, conn_state);
|
||||||
draw_connection_row(ui, config, connection, recorder, conn_state);
|
|
||||||
ui.add_space(8.0);
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||||
// Legacy serial parameter grid is intentionally kept commented for future hardware:
|
draw_config_bar_rates(ui, stats, sample_rate_hz, render_rate_hz);
|
||||||
// draw_serial_grid(ui, config);
|
|
||||||
// ui.add_space(8.0);
|
|
||||||
draw_mode_body(ui, config, conn_state, stats);
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
changed_mode
|
changed_mode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn config_bar_frame() -> egui::Frame {
|
||||||
|
egui::Frame::new()
|
||||||
|
.fill(ONE_DARK_PRO.panel_deep)
|
||||||
|
.inner_margin(egui::Margin::symmetric(8, 6))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_config_bar_mode(ui: &mut egui::Ui, config: &mut ConfigPanelState) -> Option<SerialMode> {
|
||||||
|
let mut changed_to = None;
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.colored_label(dim_text(), "模式");
|
||||||
|
|
||||||
|
if mode_button(ui, &mut config.mode, SerialMode::Finger, "指尖模块") {
|
||||||
|
changed_to = Some(SerialMode::Finger);
|
||||||
|
}
|
||||||
|
if mode_button(ui, &mut config.mode, SerialMode::Hand, "游戏体验") {
|
||||||
|
changed_to = Some(SerialMode::Hand);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
changed_to
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_config_bar_connection(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
config: &mut ConfigPanelState,
|
||||||
|
connection: &ConnectionManager,
|
||||||
|
recorder: &Recorder,
|
||||||
|
conn_state: ConnectionState,
|
||||||
|
) {
|
||||||
|
ui.colored_label(dim_text(), "端口");
|
||||||
|
egui::ComboBox::from_id_salt("config_bar_ports")
|
||||||
|
.width(170.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),
|
||||||
|
egui::vec2(30.0, METRICS.field_height),
|
||||||
|
))
|
||||||
|
.on_hover_text("刷新串口")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
refresh_config_ports(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.add_space(4.0);
|
||||||
|
ui.colored_label(dim_text(), "波特率");
|
||||||
|
ui.label(style::value_text(config.baud_rate.to_string()));
|
||||||
|
|
||||||
|
ui.add_space(4.0);
|
||||||
|
ui.colored_label(dim_text(), "状态");
|
||||||
|
ui.colored_label(
|
||||||
|
connection_status_color(conn_state),
|
||||||
|
connection_status_text(conn_state),
|
||||||
|
);
|
||||||
|
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
|
let is_connected = matches!(
|
||||||
|
conn_state,
|
||||||
|
ConnectionState::Connected | ConnectionState::Streaming
|
||||||
|
);
|
||||||
|
let button_text = if is_connected { "断开" } else { "连接" };
|
||||||
|
if ui
|
||||||
|
.add_sized(
|
||||||
|
egui::vec2(88.0, METRICS.button_height),
|
||||||
|
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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_config_bar_rates(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
_stats: SerialIoStats,
|
||||||
|
sample_rate_hz: f32,
|
||||||
|
render_rate_hz: f32,
|
||||||
|
) {
|
||||||
|
ui.label(style::value_text(format!("{render_rate_hz:.1} Hz")));
|
||||||
|
ui.colored_label(dim_text(), "画面刷新率");
|
||||||
|
ui.add_space(14.0);
|
||||||
|
ui.label(style::value_text(format!("{sample_rate_hz:.1} Hz")));
|
||||||
|
ui.colored_label(dim_text(), "采样率");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_spatial_force_panel(
|
||||||
|
ctx: &egui::Context,
|
||||||
|
force: Option<HudSpatialForce>,
|
||||||
|
force_history: &[f32],
|
||||||
|
) {
|
||||||
|
let Some(force) = force else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let target_visible = has_recent_resultant_force(force_history);
|
||||||
|
let anim = ctx.animate_bool(egui::Id::new("spatial_force_panel_enter"), target_visible);
|
||||||
|
if anim <= 0.01 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let target_anim = if target_visible { 1.0 } else { 0.0 };
|
||||||
|
if (anim - target_anim).abs() > 0.001 {
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
let alpha = (anim * 242.0).round() as u8;
|
||||||
|
let panel_width = responsive_panel_width(ctx, 380.0, 260.0);
|
||||||
|
let panel_height = responsive_panel_height(ctx, 340.0, 220.0);
|
||||||
|
let viewport = viewport_panel_rect(ctx);
|
||||||
|
let pos = egui::pos2(
|
||||||
|
viewport.right() - panel_width,
|
||||||
|
viewport.bottom() - panel_height - PANEL_VIEWPORT_MARGIN,
|
||||||
|
);
|
||||||
|
|
||||||
|
egui::Area::new(egui::Id::new("spatial_force_panel"))
|
||||||
|
.fixed_pos(pos)
|
||||||
|
.constrain_to(viewport)
|
||||||
|
.order(egui::Order::Foreground)
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
faded_panel_frame(ctx, alpha).show(ui, |ui| {
|
||||||
|
ui.set_min_width(panel_width);
|
||||||
|
ui.set_max_width(panel_width);
|
||||||
|
ui.set_min_height(panel_height);
|
||||||
|
|
||||||
|
let text_dim = color_alpha(ONE_DARK_PRO.text_dim, alpha);
|
||||||
|
let accent = color_alpha(ACCENT_ORANGE, alpha);
|
||||||
|
let green = color_alpha(ACCENT_GREEN, alpha);
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.colored_label(accent, "3D FORCE");
|
||||||
|
ui.label(egui::RichText::new("三维力方向").color(text_dim).size(17.0));
|
||||||
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||||
|
ui.colored_label(
|
||||||
|
green,
|
||||||
|
egui::RichText::new("LIVE").color(green).size(12.0),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
let gauge_size = (panel_height - 52.0).min(panel_width - 24.0).max(168.0);
|
||||||
|
let (rect, _) = ui.allocate_exact_size(
|
||||||
|
egui::vec2(gauge_size, gauge_size),
|
||||||
|
egui::Sense::hover(),
|
||||||
|
);
|
||||||
|
paint_spatial_force_gauge(ui.painter(), rect, force, alpha);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn draw_mode_row(ui: &mut egui::Ui, config: &mut ConfigPanelState) -> Option<SerialMode> {
|
fn draw_mode_row(ui: &mut egui::Ui, config: &mut ConfigPanelState) -> Option<SerialMode> {
|
||||||
let mut changed_to = None;
|
let mut changed_to = None;
|
||||||
ui.horizontal_wrapped(|ui| {
|
ui.horizontal_wrapped(|ui| {
|
||||||
@@ -430,7 +630,7 @@ fn draw_mode_row(ui: &mut egui::Ui, config: &mut ConfigPanelState) -> Option<Ser
|
|||||||
if mode_button(ui, &mut config.mode, SerialMode::Finger, "指尖模块") {
|
if mode_button(ui, &mut config.mode, SerialMode::Finger, "指尖模块") {
|
||||||
changed_to = Some(SerialMode::Finger)
|
changed_to = Some(SerialMode::Finger)
|
||||||
}
|
}
|
||||||
if mode_button(ui, &mut config.mode, SerialMode::Hand, "手掌模块") {
|
if mode_button(ui, &mut config.mode, SerialMode::Hand, "游戏体验") {
|
||||||
changed_to = Some(SerialMode::Hand)
|
changed_to = Some(SerialMode::Hand)
|
||||||
}
|
}
|
||||||
// mode_button(ui, &mut config.mode, SerialMode::Model, "模型");
|
// mode_button(ui, &mut config.mode, SerialMode::Model, "模型");
|
||||||
@@ -609,47 +809,22 @@ fn draw_mode_body(
|
|||||||
config: &mut ConfigPanelState,
|
config: &mut ConfigPanelState,
|
||||||
conn_state: ConnectionState,
|
conn_state: ConnectionState,
|
||||||
stats: SerialIoStats,
|
stats: SerialIoStats,
|
||||||
|
sample_rate_hz: f32,
|
||||||
|
render_rate_hz: f32,
|
||||||
) {
|
) {
|
||||||
group_frame().show(ui, |ui| match config.mode {
|
let _ = (config.mode, conn_state, stats);
|
||||||
SerialMode::Finger => {
|
draw_realtime_rate_row(ui, sample_rate_hz, render_rate_hz);
|
||||||
// Legacy module-address controls for older devices:
|
}
|
||||||
// ui.horizontal(|ui| {
|
|
||||||
// ui.label(style::field_label("模块地址"));
|
fn draw_realtime_rate_row(ui: &mut egui::Ui, sample_rate_hz: f32, render_rate_hz: f32) {
|
||||||
// ui.add_sized(
|
group_frame().show(ui, |ui| {
|
||||||
// egui::vec2(80.0, METRICS.field_height),
|
ui.horizontal_wrapped(|ui| {
|
||||||
// egui::DragValue::new(&mut config.module_addr).range(1..=247),
|
ui.colored_label(dim_text(), "实时采样率");
|
||||||
// );
|
ui.label(style::value_text(format!("{sample_rate_hz:.1} Hz")));
|
||||||
// ui.add_space(16.0);
|
ui.add_space(18.0);
|
||||||
// if ui.add(tag_button("读取信息")).clicked() {}
|
ui.colored_label(dim_text(), "画面刷新率");
|
||||||
// if ui.add(tag_button("探测")).clicked() {}
|
ui.label(style::value_text(format!("{render_rate_hz:.1} Hz")));
|
||||||
// });
|
});
|
||||||
draw_status_bytes_row(ui, conn_state, stats);
|
|
||||||
}
|
|
||||||
SerialMode::Hand => {
|
|
||||||
// Legacy manual-TX controls for older hand-module debugging:
|
|
||||||
// ui.horizontal(|ui| {
|
|
||||||
// ui.label(style::field_label("发送"));
|
|
||||||
// ui.add_sized(
|
|
||||||
// egui::vec2(300.0, METRICS.field_height),
|
|
||||||
// egui::TextEdit::singleline(&mut config.manual_tx),
|
|
||||||
// );
|
|
||||||
// if ui.add(tag_button("发送")).clicked() {}
|
|
||||||
// if ui.add(tag_button("清空")).clicked() {
|
|
||||||
// config.manual_tx.clear();
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
draw_status_bytes_row(ui, conn_state, stats);
|
|
||||||
} // SerialMode::Model => {
|
|
||||||
// ui.horizontal(|ui| {
|
|
||||||
// ui.label(style::field_label("模型"));
|
|
||||||
// ui.add_sized(
|
|
||||||
// egui::vec2(300.0, METRICS.field_height),
|
|
||||||
// egui::TextEdit::singleline(&mut config.model_path),
|
|
||||||
// );
|
|
||||||
// if ui.add(tag_button("加载")).clicked() {}
|
|
||||||
// if ui.add(tag_button("运行")).clicked() {}
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -799,7 +974,7 @@ pub fn draw_stats_panel(
|
|||||||
let target_pos = egui::pos2(
|
let target_pos = egui::pos2(
|
||||||
viewport.left(),
|
viewport.left(),
|
||||||
(viewport.bottom() - panel_height - PANEL_VIEWPORT_MARGIN * 2.0)
|
(viewport.bottom() - panel_height - PANEL_VIEWPORT_MARGIN * 2.0)
|
||||||
.max(viewport.top() + layout::TITLE_BAR_HEIGHT + PANEL_VIEWPORT_MARGIN),
|
.max(viewport.top() + layout::WORKSPACE_TOP + PANEL_VIEWPORT_MARGIN),
|
||||||
);
|
);
|
||||||
let target_x = target_pos.x;
|
let target_x = target_pos.x;
|
||||||
let hidden_x = if target_x < screen.center().x {
|
let hidden_x = if target_x < screen.center().x {
|
||||||
@@ -872,7 +1047,7 @@ pub fn draw_hand_force_panels(ctx: &egui::Context, visible: bool, histories: &[V
|
|||||||
let left_count = 4usize;
|
let left_count = 4usize;
|
||||||
let right_count = HAND_FORCE_PANEL_TITLES.len() - left_count;
|
let right_count = HAND_FORCE_PANEL_TITLES.len() - left_count;
|
||||||
let available_height =
|
let available_height =
|
||||||
(screen.height() - layout::TITLE_BAR_HEIGHT - vertical_margin * 2.0).max(0.0);
|
(screen.height() - layout::WORKSPACE_TOP - vertical_margin * 2.0).max(0.0);
|
||||||
let panel_height = ((available_height - (left_count - 1) as f32 * gap) / left_count as f32)
|
let panel_height = ((available_height - (left_count - 1) as f32 * gap) / left_count as f32)
|
||||||
.clamp(
|
.clamp(
|
||||||
HAND_FORCE_PANEL_MIN_HEIGHT.min(available_height),
|
HAND_FORCE_PANEL_MIN_HEIGHT.min(available_height),
|
||||||
@@ -881,7 +1056,7 @@ pub fn draw_hand_force_panels(ctx: &egui::Context, visible: bool, histories: &[V
|
|||||||
let left_height = left_count as f32 * panel_height + (left_count - 1) as f32 * gap;
|
let left_height = left_count as f32 * panel_height + (left_count - 1) as f32 * gap;
|
||||||
let right_height =
|
let right_height =
|
||||||
right_count as f32 * panel_height + (right_count.saturating_sub(1)) as f32 * gap;
|
right_count as f32 * panel_height + (right_count.saturating_sub(1)) as f32 * gap;
|
||||||
let min_top = screen.top() + layout::TITLE_BAR_HEIGHT + vertical_margin;
|
let min_top = screen.top() + layout::WORKSPACE_TOP + vertical_margin;
|
||||||
let left_top = (screen.center().y - left_height * 0.5).max(min_top);
|
let left_top = (screen.center().y - left_height * 0.5).max(min_top);
|
||||||
let right_top = (screen.center().y - right_height * 0.5).max(min_top);
|
let right_top = (screen.center().y - right_height * 0.5).max(min_top);
|
||||||
|
|
||||||
@@ -1015,9 +1190,7 @@ fn paint_resultant_force_chart(ui: &mut egui::Ui, values: &[f32], chart_height:
|
|||||||
.ctx()
|
.ctx()
|
||||||
.animate_bool(egui::Id::new("resultant_force_chart_enter"), true);
|
.animate_bool(egui::Id::new("resultant_force_chart_enter"), true);
|
||||||
let slide_offset = (1.0 - enter) * -chart_rect.width() * 0.65;
|
let slide_offset = (1.0 - enter) * -chart_rect.width() * 0.65;
|
||||||
let tick_phase = (time / FORCE_CHART_SAMPLE_SECONDS).fract();
|
let plot_rect = chart_rect.translate(egui::vec2(slide_offset, 0.0));
|
||||||
let scroll_offset = -tick_phase * chart_rect.width() / CHART_POINTS.max(1) as f32;
|
|
||||||
let plot_rect = chart_rect.translate(egui::vec2(slide_offset + scroll_offset, 0.0));
|
|
||||||
|
|
||||||
let mut points = Vec::with_capacity(values.len());
|
let mut points = Vec::with_capacity(values.len());
|
||||||
let start_slot = CHART_POINTS.saturating_sub(values.len());
|
let start_slot = CHART_POINTS.saturating_sub(values.len());
|
||||||
@@ -1144,6 +1317,126 @@ fn color_alpha(color: egui::Color32, alpha: u8) -> egui::Color32 {
|
|||||||
egui::Color32::from_rgba_premultiplied(color.r(), color.g(), color.b(), alpha)
|
egui::Color32::from_rgba_premultiplied(color.r(), color.g(), color.b(), alpha)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn faded_panel_frame(ctx: &egui::Context, alpha: u8) -> egui::Frame {
|
||||||
|
let style = ctx.global_style();
|
||||||
|
egui::Frame::window(&style)
|
||||||
|
.fill(color_alpha(ONE_DARK_PRO.panel, alpha))
|
||||||
|
.stroke(egui::Stroke::new(
|
||||||
|
1.0,
|
||||||
|
color_alpha(ONE_DARK_PRO.border, alpha),
|
||||||
|
))
|
||||||
|
.corner_radius(egui::CornerRadius::same(ONE_DARK_PRO.radius))
|
||||||
|
.inner_margin(egui::Margin::same(METRICS.panel_padding))
|
||||||
|
.shadow(egui::epaint::Shadow {
|
||||||
|
offset: [0, 14],
|
||||||
|
blur: 28,
|
||||||
|
spread: 0,
|
||||||
|
color: egui::Color32::from_black_alpha((alpha as f32 * 0.45) as u8),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint_spatial_force_gauge(
|
||||||
|
painter: &egui::Painter,
|
||||||
|
rect: egui::Rect,
|
||||||
|
force: HudSpatialForce,
|
||||||
|
alpha: u8,
|
||||||
|
) {
|
||||||
|
let center = rect.center();
|
||||||
|
let radius = rect.width().min(rect.height()) * 0.42;
|
||||||
|
let angle = force.angle_deg.to_radians();
|
||||||
|
let direction = egui::vec2(angle.cos(), angle.sin());
|
||||||
|
let magnitude = force.magnitude.clamp(0.0, 1.0);
|
||||||
|
let end = center + direction * (radius * (0.35 + magnitude * 0.65));
|
||||||
|
let side = egui::vec2(-direction.y, direction.x);
|
||||||
|
|
||||||
|
painter.circle_stroke(
|
||||||
|
center,
|
||||||
|
radius,
|
||||||
|
egui::Stroke::new(1.0, color_alpha(ONE_DARK_PRO.border, alpha)),
|
||||||
|
);
|
||||||
|
painter.circle_stroke(
|
||||||
|
center,
|
||||||
|
radius * 0.55,
|
||||||
|
egui::Stroke::new(1.0, color_alpha(ONE_DARK_PRO.border_soft, alpha / 2)),
|
||||||
|
);
|
||||||
|
painter.line_segment(
|
||||||
|
[
|
||||||
|
center + egui::vec2(-radius, 0.0),
|
||||||
|
center + egui::vec2(radius, 0.0),
|
||||||
|
],
|
||||||
|
egui::Stroke::new(1.0, color_alpha(ONE_DARK_PRO.border_soft, alpha / 3)),
|
||||||
|
);
|
||||||
|
painter.line_segment(
|
||||||
|
[
|
||||||
|
center + egui::vec2(0.0, -radius),
|
||||||
|
center + egui::vec2(0.0, radius),
|
||||||
|
],
|
||||||
|
egui::Stroke::new(1.0, color_alpha(ONE_DARK_PRO.border_soft, alpha / 3)),
|
||||||
|
);
|
||||||
|
painter.circle_filled(
|
||||||
|
end,
|
||||||
|
12.0,
|
||||||
|
color_alpha(ACCENT_ORANGE, (alpha as f32 * 0.16) as u8),
|
||||||
|
);
|
||||||
|
painter.line_segment(
|
||||||
|
[center, end],
|
||||||
|
egui::Stroke::new(4.0, color_alpha(ACCENT_ORANGE, alpha)),
|
||||||
|
);
|
||||||
|
painter.line_segment(
|
||||||
|
[end, end - direction * 13.0 + side * 7.0],
|
||||||
|
egui::Stroke::new(3.0, color_alpha(ACCENT_ORANGE, alpha)),
|
||||||
|
);
|
||||||
|
painter.line_segment(
|
||||||
|
[end, end - direction * 13.0 - side * 7.0],
|
||||||
|
egui::Stroke::new(3.0, color_alpha(ACCENT_ORANGE, alpha)),
|
||||||
|
);
|
||||||
|
painter.circle_filled(center, 4.5, color_alpha(ONE_DARK_PRO.text_dim, alpha));
|
||||||
|
painter.circle_filled(end, 5.5, color_alpha(ACCENT_GREEN, alpha));
|
||||||
|
painter.circle_filled(end, 2.2, color_alpha(egui::Color32::WHITE, alpha));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_pinned_panel(
|
||||||
|
ctx: &egui::Context,
|
||||||
|
panel: &mut FloatingPanelState,
|
||||||
|
title: &'static str,
|
||||||
|
id: &'static str,
|
||||||
|
preferred_width: f32,
|
||||||
|
min_width: f32,
|
||||||
|
add_contents: impl FnOnce(&mut egui::Ui),
|
||||||
|
) {
|
||||||
|
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);
|
||||||
|
|
||||||
|
egui::Window::new(title)
|
||||||
|
.id(egui::Id::new(id))
|
||||||
|
.current_pos(pos)
|
||||||
|
.max_width(panel_width)
|
||||||
|
.max_height(max_height)
|
||||||
|
.constrain_to(viewport_panel_rect(ctx))
|
||||||
|
.title_bar(false)
|
||||||
|
.resizable(false)
|
||||||
|
.frame(panel_frame(ctx))
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
ui.spacing_mut().item_spacing = egui::vec2(METRICS.item_gap, 6.0);
|
||||||
|
ui.set_min_width(panel_width);
|
||||||
|
ui.set_max_width(panel_width);
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label(style::panel_title(title));
|
||||||
|
});
|
||||||
|
ui.separator();
|
||||||
|
let content_max_height = (max_height - 60.0).max(120.0);
|
||||||
|
egui::ScrollArea::vertical()
|
||||||
|
.max_height(content_max_height)
|
||||||
|
.auto_shrink([false, true])
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.set_min_width(panel_width);
|
||||||
|
ui.set_max_width(panel_width);
|
||||||
|
add_contents(ui);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn draw_floating_panel(
|
fn draw_floating_panel(
|
||||||
ctx: &egui::Context,
|
ctx: &egui::Context,
|
||||||
panel: &mut FloatingPanelState,
|
panel: &mut FloatingPanelState,
|
||||||
|
|||||||
Reference in New Issue
Block a user