Integrate hand gateway and spatial force rendering
This commit is contained in:
126
src/app.rs
126
src/app.rs
@@ -1,5 +1,6 @@
|
||||
use crate::breakout::{BreakoutGame, control_from_matrix};
|
||||
use crate::connection::ConnectionManager;
|
||||
use crate::force::{ForceEstimatorState, HudSpatialForce};
|
||||
use crate::recording::Recorder;
|
||||
use crate::render::{ActiveMode, FingerMode, HandGatewayMode};
|
||||
use crate::style::{ONE_DARK_PRO, apply_fonts, apply_theme, layout};
|
||||
@@ -7,7 +8,8 @@ use crate::ui::SerialMode;
|
||||
use crate::{
|
||||
matrix::{MATRIX_COLS, MATRIX_ROWS},
|
||||
render::{
|
||||
BackgroundRenderResources, PRESSURE_CELL_COUNT, PressureFrame, WgpuBackgroundCallback,
|
||||
BackgroundRenderResources, PRESSURE_CELL_COUNT, PressureFrame, PressureSamples,
|
||||
WgpuBackgroundCallback,
|
||||
},
|
||||
ui::{
|
||||
ConfigPanelState, ConnectPanelState, FloatingPanelState, MatrixConfigState,
|
||||
@@ -18,17 +20,14 @@ use crate::{
|
||||
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,
|
||||
connect_state: ConnectPanelState,
|
||||
connection: Arc<ConnectionManager>,
|
||||
pressure_matrix: PressureFrame,
|
||||
data_log_frame: u64,
|
||||
hand_pressure: PressureSamples,
|
||||
scene_panel: FloatingPanelState,
|
||||
config_panel: FloatingPanelState,
|
||||
config_state: ConfigPanelState,
|
||||
@@ -40,6 +39,8 @@ pub struct EskinDesktopApp {
|
||||
matrix_config_panel: FloatingPanelState,
|
||||
matrix_config: MatrixConfigState,
|
||||
signal_history: Vec<f32>,
|
||||
force_estimator: ForceEstimatorState,
|
||||
latest_spatial_force: Option<HudSpatialForce>,
|
||||
latest_raw_matrix: Vec<u32>,
|
||||
latest_matrix_rows: u32,
|
||||
latest_matrix_cols: u32,
|
||||
@@ -77,7 +78,7 @@ impl EskinDesktopApp {
|
||||
connect_state: ConnectPanelState::default(),
|
||||
connection: Arc::new(ConnectionManager::new()),
|
||||
pressure_matrix: [[0.0, 0.0]; PRESSURE_CELL_COUNT],
|
||||
data_log_frame: 0,
|
||||
hand_pressure: Vec::new(),
|
||||
scene_panel: FloatingPanelState::new(
|
||||
[layout::LEFT_X, layout::TOP_Y],
|
||||
[layout::LEFT_TAG_X, layout::TOP_Y],
|
||||
@@ -103,6 +104,8 @@ impl EskinDesktopApp {
|
||||
),
|
||||
matrix_config: MatrixConfigState::default(),
|
||||
signal_history: Vec::with_capacity(128),
|
||||
force_estimator: ForceEstimatorState::new(),
|
||||
latest_spatial_force: None,
|
||||
latest_raw_matrix: Vec::new(),
|
||||
latest_matrix_rows: MATRIX_ROWS,
|
||||
latest_matrix_cols: MATRIX_COLS,
|
||||
@@ -129,9 +132,40 @@ impl EskinDesktopApp {
|
||||
width,
|
||||
height,
|
||||
pressure: self.pressure_matrix,
|
||||
hand_pressure: self.hand_pressure.clone(),
|
||||
active_mode: self.active_mode.clone(),
|
||||
},
|
||||
));
|
||||
self.paint_spatial_force_overlay(ui, rect);
|
||||
}
|
||||
|
||||
fn paint_spatial_force_overlay(&self, ui: &egui::Ui, rect: egui::Rect) {
|
||||
let Some(force) = self.latest_spatial_force else {
|
||||
return;
|
||||
};
|
||||
|
||||
let painter = ui.painter();
|
||||
let center = rect.center();
|
||||
let magnitude = force.magnitude.clamp(0.0, 1.8);
|
||||
let length = 34.0 + magnitude * 54.0;
|
||||
let angle = force.angle_deg.to_radians();
|
||||
let direction = egui::vec2(angle.cos(), angle.sin());
|
||||
let end = center + direction * length;
|
||||
let side = egui::vec2(-direction.y, direction.x);
|
||||
let color = egui::Color32::from_rgb(255, 196, 54);
|
||||
let glow = egui::Color32::from_rgba_unmultiplied(255, 196, 54, 58);
|
||||
|
||||
painter.line_segment([center, end], egui::Stroke::new(9.0, glow));
|
||||
painter.line_segment([center, end], egui::Stroke::new(2.4, color));
|
||||
painter.line_segment(
|
||||
[end, end - direction * 14.0 + side * 7.0],
|
||||
egui::Stroke::new(2.4, color),
|
||||
);
|
||||
painter.line_segment(
|
||||
[end, end - direction * 14.0 - side * 7.0],
|
||||
egui::Stroke::new(2.4, color),
|
||||
);
|
||||
painter.circle_filled(center, 4.2, color);
|
||||
}
|
||||
|
||||
fn draw_workspace(&mut self, ui: &mut egui::Ui) {
|
||||
@@ -186,6 +220,7 @@ impl EskinDesktopApp {
|
||||
sample.cols,
|
||||
&mut self.pressure_matrix,
|
||||
);
|
||||
self.hand_pressure = normalize_pressure_samples(&sample.matrix);
|
||||
self.latest_raw_matrix.clear();
|
||||
self.latest_raw_matrix.extend_from_slice(&sample.matrix);
|
||||
self.latest_matrix_rows = sample.rows;
|
||||
@@ -194,27 +229,20 @@ impl EskinDesktopApp {
|
||||
// Feed data to recorder
|
||||
self.recorder.add_frame(&sample.matrix);
|
||||
|
||||
// JE-Skin summary logic: sum all cells first, then convert raw summary to force.
|
||||
self.latest_spatial_force = self.force_estimator.analyze(&sample.matrix);
|
||||
|
||||
// Keep JE-Skin's summary path separate from the optional spatial-force vector.
|
||||
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
|
||||
};
|
||||
let force = raw_to_g1(raw_total).min(25.6);
|
||||
let force = if force <= 0.1 { 0.0 } else { force };
|
||||
self.signal_history.push(force);
|
||||
if self.signal_history.len() > SUMMARY_POINTS_PER_SERIES {
|
||||
self.signal_history.remove(0);
|
||||
}
|
||||
|
||||
self.data_log_frame += 1;
|
||||
if self.data_log_frame % DATA_LOG_EVERY_FRAMES == 0 {
|
||||
log_pressure_sample(&sample.matrix, sample.rows, sample.cols);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,7 +367,12 @@ impl EskinDesktopApp {
|
||||
) {
|
||||
self.switch_mode(next_mode);
|
||||
}
|
||||
draw_stats_panel(ctx, &mut self.stats_panel, &self.signal_history);
|
||||
draw_stats_panel(
|
||||
ctx,
|
||||
&mut self.stats_panel,
|
||||
&self.signal_history,
|
||||
self.latest_spatial_force,
|
||||
);
|
||||
draw_export_panel(
|
||||
ctx,
|
||||
&mut self.export_panel,
|
||||
@@ -416,9 +449,13 @@ impl EskinDesktopApp {
|
||||
fn switch_mode(&mut self, next: SerialMode) {
|
||||
self.connect_state.mode = next;
|
||||
self.config_state.mode = next;
|
||||
self.config_state.baud_rate = next.baud_rate();
|
||||
self.connection.disconnect();
|
||||
self.pressure_matrix.fill([0.0, 0.0]);
|
||||
self.data_log_frame = 0;
|
||||
self.hand_pressure.clear();
|
||||
self.force_estimator.reset();
|
||||
self.latest_spatial_force = None;
|
||||
self.signal_history.clear();
|
||||
|
||||
self.active_mode = match next {
|
||||
SerialMode::Finger => ActiveMode::Finger(FingerMode {
|
||||
@@ -432,23 +469,6 @@ impl EskinDesktopApp {
|
||||
}
|
||||
}
|
||||
|
||||
fn log_pressure_sample(raw: &[u32], rows: u32, cols: u32) {
|
||||
let max = raw.iter().copied().max().unwrap_or(0);
|
||||
let sum: u64 = raw.iter().map(|value| *value as u64).sum();
|
||||
let non_zero = raw.iter().filter(|value| **value != 0).count();
|
||||
let preview = raw
|
||||
.iter()
|
||||
.take(12)
|
||||
.map(u32::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
println!(
|
||||
"[pressure] {rows}x{cols} cells={} non_zero={non_zero} max={max} sum={sum} first=[{preview}]",
|
||||
raw.len()
|
||||
);
|
||||
}
|
||||
|
||||
fn raw_to_g1(raw: u32) -> f32 {
|
||||
const RAW: [u32; 12] = [
|
||||
0, 21382, 108507, 123183, 147405, 171105, 192395, 250443, 231423, 350560, 396616, 429444,
|
||||
@@ -465,7 +485,6 @@ fn raw_to_g1(raw: u32) -> f32 {
|
||||
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 {
|
||||
@@ -534,9 +553,6 @@ fn split_viewport_body(rect: egui::Rect) -> egui::Rect {
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
normalized.fill([0.0, 0.0]);
|
||||
|
||||
let src_cols = cols.max(1);
|
||||
@@ -548,19 +564,31 @@ fn normalize_pressure_sample(raw: &[u32], rows: u32, cols: u32, normalized: &mut
|
||||
let src_index = (row * src_cols + col) as usize;
|
||||
let dst_index = (row * MATRIX_COLS + col) as usize;
|
||||
if let Some(value) = raw.get(src_index) {
|
||||
let raw_value = *value as f32;
|
||||
let mapped = ((raw_value - RANGE_MIN) / (RANGE_MAX - RANGE_MIN)).clamp(0.0, 1.0);
|
||||
let display_value = if raw_value <= RANGE_MIN + 4.0 {
|
||||
0.0
|
||||
} else {
|
||||
raw_value.round().min(9999.0)
|
||||
};
|
||||
normalized[dst_index] = [mapped, display_value];
|
||||
normalized[dst_index] = normalize_pressure_value(*value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_pressure_samples(raw: &[u32]) -> PressureSamples {
|
||||
raw.iter().copied().map(normalize_pressure_value).collect()
|
||||
}
|
||||
|
||||
fn normalize_pressure_value(value: u32) -> [f32; 2] {
|
||||
const RANGE_MIN: f32 = 0.0;
|
||||
const RANGE_MAX: f32 = 7000.0;
|
||||
|
||||
let raw_value = value as f32;
|
||||
let mapped = ((raw_value - RANGE_MIN) / (RANGE_MAX - RANGE_MIN)).clamp(0.0, 1.0);
|
||||
let display_value = if raw_value <= RANGE_MIN + 4.0 {
|
||||
0.0
|
||||
} else {
|
||||
raw_value.round().min(9999.0)
|
||||
};
|
||||
|
||||
[mapped, display_value]
|
||||
}
|
||||
|
||||
impl eframe::App for EskinDesktopApp {
|
||||
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
|
||||
let ctx = ui.ctx().clone();
|
||||
|
||||
Reference in New Issue
Block a user