Add pressure visual modes and breakout game

This commit is contained in:
lennlouisgeek
2026-06-29 03:13:57 +08:00
parent eab24fc2bf
commit f30ebcf20b
12 changed files with 1739 additions and 165 deletions

View File

@@ -1,3 +1,4 @@
use crate::breakout::{BreakoutGame, control_from_matrix};
use crate::connection::ConnectionManager;
use crate::recording::Recorder;
use crate::render::{ActiveMode, FingerMode, HandGatewayMode};
@@ -18,6 +19,9 @@ use eframe::{egui, egui_wgpu};
use std::sync::Arc;
const DATA_LOG_EVERY_FRAMES: u64 = 30;
const SUMMARY_POINTS_PER_SERIES: usize = 42;
const MIN_DISPLAY_FORCE_N: f32 = 0.1;
const MAX_DISPLAY_FORCE_N: f32 = 25.6;
pub struct EskinDesktopApp {
connect_panel: FloatingPanelState,
@@ -36,6 +40,13 @@ pub struct EskinDesktopApp {
matrix_config_panel: FloatingPanelState,
matrix_config: MatrixConfigState,
signal_history: Vec<f32>,
latest_raw_matrix: Vec<u32>,
latest_matrix_rows: u32,
latest_matrix_cols: u32,
breakout_visible: bool,
breakout_game: BreakoutGame,
context_menu_open: bool,
context_menu_pos: egui::Pos2,
active_mode: ActiveMode,
}
@@ -92,6 +103,13 @@ impl EskinDesktopApp {
),
matrix_config: MatrixConfigState::default(),
signal_history: Vec::with_capacity(128),
latest_raw_matrix: Vec::new(),
latest_matrix_rows: MATRIX_ROWS,
latest_matrix_cols: MATRIX_COLS,
breakout_visible: false,
breakout_game: BreakoutGame::default(),
context_menu_open: false,
context_menu_pos: egui::pos2(24.0, 48.0),
active_mode: ActiveMode::Finger(FingerMode {
rows: 12,
cols: 7,
@@ -101,10 +119,7 @@ impl EskinDesktopApp {
}
}
fn draw_wgpu_background(&mut self, ui: &mut egui::Ui) {
self.update_pressure_matrix();
let rect = ui.max_rect();
fn draw_wgpu_background_rect(&self, ui: &mut egui::Ui, rect: egui::Rect) {
let width = rect.width().max(1.0);
let height = rect.height().max(1.0);
@@ -119,6 +134,50 @@ impl EskinDesktopApp {
));
}
fn draw_workspace(&mut self, ui: &mut egui::Ui) {
if self.breakout_visible {
self.draw_split_workspace(ui);
} else {
self.draw_wgpu_background_rect(ui, ui.max_rect());
}
}
fn draw_split_workspace(&mut self, ui: &mut egui::Ui) {
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 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 right = egui::Rect::from_min_max(
egui::pos2(left.right() + gutter, content.top()),
content.right_bottom(),
);
ui.painter()
.rect_filled(screen, egui::CornerRadius::ZERO, ONE_DARK_PRO.bg);
paint_split_viewport_shell(ui, left, "DOTMATRIX", "压力矩阵 / WGPU");
paint_split_viewport_shell(ui, right, "BREAKOUT", "压力控制打砖块");
let left_body = split_viewport_body(left);
let right_body = split_viewport_body(right);
self.draw_wgpu_background_rect(ui, left_body);
let breakout_control = control_from_matrix(
&self.latest_raw_matrix,
self.latest_matrix_rows,
self.latest_matrix_cols,
);
self.breakout_game.draw_viewport(
ui.ctx(),
right_body,
&mut self.breakout_visible,
breakout_control,
);
}
fn update_pressure_matrix(&mut self) {
if let Some(sample) = self.connection.take_latest_sample() {
normalize_pressure_sample(
@@ -127,14 +186,28 @@ impl EskinDesktopApp {
sample.cols,
&mut self.pressure_matrix,
);
self.latest_raw_matrix.clear();
self.latest_raw_matrix.extend_from_slice(&sample.matrix);
self.latest_matrix_rows = sample.rows;
self.latest_matrix_cols = sample.cols;
// Feed data to recorder
self.recorder.add_frame(&sample.matrix);
// Update signal history (sum of all pressure values)
let total: f32 = sample.matrix.iter().map(|v| *v as f32).sum();
self.signal_history.push(total);
if self.signal_history.len() > 128 {
// JE-Skin summary logic: sum all cells first, then convert raw summary to force.
let raw_total = sample
.matrix
.iter()
.fold(0_u64, |sum, value| sum + *value as u64)
.min(u32::MAX as u64) as u32;
let force = raw_to_g1(raw_total).min(MAX_DISPLAY_FORCE_N);
let force = if force <= MIN_DISPLAY_FORCE_N {
0.0
} else {
force
};
self.signal_history.push(force);
if self.signal_history.len() > SUMMARY_POINTS_PER_SERIES {
self.signal_history.remove(0);
}
@@ -258,12 +331,15 @@ impl EskinDesktopApp {
fn draw_floating_panels(&mut self, ctx: &egui::Context) {
// draw_scene_panel(ctx, &mut self.scene_panel);
if let Some(next_mode) =
draw_config_panel(ctx, &mut self.config_panel, &mut self.config_state)
{
if let Some(next_mode) = draw_config_panel(
ctx,
&mut self.config_panel,
&mut self.config_state,
&self.connection,
) {
self.switch_mode(next_mode);
}
draw_stats_panel(ctx, &mut self.stats_panel);
draw_stats_panel(ctx, &mut self.stats_panel, &self.signal_history);
draw_export_panel(
ctx,
&mut self.export_panel,
@@ -273,32 +349,68 @@ impl EskinDesktopApp {
draw_matrix_config_panel(ctx, &mut self.matrix_config_panel, &mut self.matrix_config);
}
fn draw_panel_context_menu(&mut self, ui: &mut egui::Ui) {
let response = ui.interact(
ui.max_rect(),
egui::Id::new("workspace_panel_context_menu"),
egui::Sense::click(),
);
response.context_menu(|ui| {
ui.label("显示面板");
ui.separator();
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);
ui.separator();
if ui.button("全部显示").clicked() {
self.config_panel.visible = true;
self.export_panel.visible = true;
self.matrix_config_panel.visible = true;
self.stats_panel.visible = true;
ui.close();
}
fn draw_panel_context_menu(&mut self, ctx: &egui::Context) {
let (secondary_clicked, pointer_pos, escape_pressed) = ctx.input(|input| {
(
input.pointer.secondary_clicked(),
input.pointer.interact_pos(),
input.key_pressed(egui::Key::Escape),
)
});
if secondary_clicked {
self.context_menu_open = true;
self.context_menu_pos = pointer_pos.unwrap_or(self.context_menu_pos);
}
if escape_pressed {
self.context_menu_open = false;
}
if !self.context_menu_open {
return;
}
let mut close_menu = false;
let response = egui::Area::new(egui::Id::new("workspace_context_menu_area"))
.fixed_pos(self.context_menu_pos)
.order(egui::Order::Foreground)
.show(ctx, |ui| {
egui::Frame::popup(ui.style()).show(ui, |ui| {
ui.set_min_width(132.0);
ui.label("显示面板");
ui.separator();
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 ui.button("打砖块").clicked() {
self.breakout_visible = true;
close_menu = true;
}
ui.separator();
if ui.button("全部显示").clicked() {
self.config_panel.visible = true;
self.export_panel.visible = true;
self.matrix_config_panel.visible = true;
self.stats_panel.visible = true;
self.breakout_visible = true;
close_menu = true;
}
});
});
let clicked_outside = ctx.input(|input| {
input.pointer.primary_clicked()
&& input
.pointer
.interact_pos()
.is_some_and(|pos| !response.response.rect.contains(pos))
});
if close_menu || clicked_outside {
self.context_menu_open = false;
}
}
fn switch_mode(&mut self, next: SerialMode) {
@@ -337,6 +449,90 @@ fn log_pressure_sample(raw: &[u32], rows: u32, cols: u32) {
);
}
fn raw_to_g1(raw: u32) -> f32 {
const RAW: [u32; 12] = [
0, 21382, 108507, 123183, 147405, 171105, 192395, 250443, 231423, 350560, 396616, 429444,
];
const FORCE_CENTI_N: [f32; 12] = [
0.0, 57.0, 257.0, 357.0, 457.0, 557.0, 657.0, 857.0, 1057.0, 1557.0, 2057.0, 2557.0,
];
if raw <= RAW[0] {
return FORCE_CENTI_N[0] / 100.0;
}
if raw >= RAW[RAW.len() - 1] {
return FORCE_CENTI_N[FORCE_CENTI_N.len() - 1] / 100.0;
}
// Keep the same lookup behavior as JE-Skin so both apps report the same force.
let mut left = 0usize;
let mut right = RAW.len() - 1;
while left + 1 < right {
let mid = (left + right) / 2;
if RAW[mid] <= raw {
left = mid;
} else {
right = mid;
}
}
let raw_left = RAW[left] as f32;
let raw_right = RAW[right] as f32;
let span = (raw_right - raw_left).max(1.0);
let ratio = (raw as f32 - raw_left) / span;
(FORCE_CENTI_N[left] + ratio * (FORCE_CENTI_N[right] - FORCE_CENTI_N[left])) / 100.0
}
fn paint_split_viewport_shell(
ui: &egui::Ui,
rect: egui::Rect,
title: &'static str,
subtitle: &'static str,
) {
let painter = ui.painter();
painter.rect_filled(
rect,
egui::CornerRadius::same(8),
egui::Color32::from_rgb(11, 17, 23),
);
painter.rect_stroke(
rect,
egui::CornerRadius::same(8),
egui::Stroke::new(1.0, ONE_DARK_PRO.border_soft),
egui::StrokeKind::Outside,
);
painter.line_segment(
[
rect.left_top() + egui::vec2(12.0, 34.0),
rect.right_top() + egui::vec2(-12.0, 34.0),
],
egui::Stroke::new(1.0, ONE_DARK_PRO.border_soft),
);
painter.text(
rect.left_top() + egui::vec2(14.0, 16.0),
egui::Align2::LEFT_CENTER,
title,
egui::FontId::monospace(13.0),
ONE_DARK_PRO.accent,
);
painter.text(
rect.left_top() + egui::vec2(122.0, 16.0),
egui::Align2::LEFT_CENTER,
subtitle,
egui::FontId::proportional(12.0),
ONE_DARK_PRO.text_dim,
);
}
fn split_viewport_body(rect: egui::Rect) -> egui::Rect {
egui::Rect::from_min_max(
rect.left_top() + egui::vec2(10.0, 44.0),
rect.right_bottom() - egui::vec2(10.0, 10.0),
)
}
fn normalize_pressure_sample(raw: &[u32], rows: u32, cols: u32, normalized: &mut PressureFrame) {
const RANGE_MIN: f32 = 0.0;
const RANGE_MAX: f32 = 7000.0;
@@ -369,10 +565,11 @@ impl eframe::App for EskinDesktopApp {
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
let ctx = ui.ctx().clone();
self.draw_wgpu_background(ui);
self.draw_panel_context_menu(ui);
self.update_pressure_matrix();
self.draw_workspace(ui);
self.draw_title_bar(ui, frame);
self.draw_floating_panels(&ctx);
self.draw_panel_context_menu(&ctx);
// Keep repainting while the wgpu background is a realtime viewport.
ctx.request_repaint();