Integrate hand fingertip force overlays
This commit is contained in:
173
src/app.rs
173
src/app.rs
@@ -1,6 +1,6 @@
|
||||
use crate::breakout::{BreakoutGame, control_from_matrix};
|
||||
use crate::connection::ConnectionManager;
|
||||
use crate::force::{ForceEstimatorState, HudSpatialForce};
|
||||
use crate::connection::{ConnectionManager, ConnectionState};
|
||||
use crate::force::{ForceEstimatorState, HAND_FINGERTIP_COUNT, HudSpatialForce};
|
||||
use crate::recording::Recorder;
|
||||
use crate::render::{ActiveMode, FingerMode, HandGatewayMode};
|
||||
use crate::style::{ONE_DARK_PRO, apply_fonts, apply_theme, layout};
|
||||
@@ -25,7 +25,16 @@ use std::{
|
||||
const SUMMARY_POINTS_PER_SERIES: usize = 42;
|
||||
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_IMAGE_SIZE_PX: [f32; 2] = [971.0, 1117.0];
|
||||
const HAND_FINGERTIP_FORCE_ANCHORS_PX: [[f32; 2]; HAND_FINGERTIP_COUNT] = [
|
||||
[265.0, 495.0],
|
||||
[359.0, 260.0],
|
||||
[482.0, 228.0],
|
||||
[596.0, 265.0],
|
||||
[693.0, 370.0],
|
||||
];
|
||||
const RATE_UPDATE_INTERVAL: Duration = Duration::from_millis(500);
|
||||
const DATA_IDLE_PANEL_EXIT_DELAY: Duration = Duration::from_secs(3);
|
||||
|
||||
pub struct EskinDesktopApp {
|
||||
connect_panel: FloatingPanelState,
|
||||
@@ -47,10 +56,12 @@ pub struct EskinDesktopApp {
|
||||
hand_signal_histories: [Vec<f32>; HAND_FORCE_PANEL_COUNT],
|
||||
force_estimator: ForceEstimatorState,
|
||||
latest_spatial_force: Option<HudSpatialForce>,
|
||||
latest_hand_spatial_forces: [Option<HudSpatialForce>; HAND_FINGERTIP_COUNT],
|
||||
spatial_force_panel_force: Option<HudSpatialForce>,
|
||||
latest_raw_matrix: Vec<u32>,
|
||||
latest_matrix_rows: u32,
|
||||
latest_matrix_cols: u32,
|
||||
last_sample_at: Option<Instant>,
|
||||
breakout_visible: bool,
|
||||
breakout_game: BreakoutGame,
|
||||
live_rates: LiveRateMeters,
|
||||
@@ -158,10 +169,12 @@ impl EskinDesktopApp {
|
||||
hand_signal_histories: std::array::from_fn(|_| Vec::with_capacity(128)),
|
||||
force_estimator: ForceEstimatorState::new(),
|
||||
latest_spatial_force: None,
|
||||
latest_hand_spatial_forces: [None; HAND_FINGERTIP_COUNT],
|
||||
spatial_force_panel_force: None,
|
||||
latest_raw_matrix: Vec::new(),
|
||||
latest_matrix_rows: MATRIX_ROWS,
|
||||
latest_matrix_cols: MATRIX_COLS,
|
||||
last_sample_at: None,
|
||||
breakout_visible: false,
|
||||
breakout_game: BreakoutGame::default(),
|
||||
live_rates: LiveRateMeters::new(),
|
||||
@@ -185,35 +198,85 @@ impl EskinDesktopApp {
|
||||
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) {
|
||||
if !self.recent_sample_is_fresh() {
|
||||
return;
|
||||
}
|
||||
|
||||
if matches!(self.active_mode, ActiveMode::Hand(_)) {
|
||||
self.paint_hand_spatial_force_overlay(ui, rect);
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
paint_spatial_force_arrow(ui.painter(), center, force, 34.0, 54.0);
|
||||
}
|
||||
|
||||
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 paint_hand_spatial_force_overlay(&self, ui: &egui::Ui, rect: egui::Rect) {
|
||||
for (force, anchor_px) in self
|
||||
.latest_hand_spatial_forces
|
||||
.iter()
|
||||
.zip(HAND_FINGERTIP_FORCE_ANCHORS_PX)
|
||||
{
|
||||
let Some(force) = force else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let center = hand_image_pixel_to_screen(rect, anchor_px);
|
||||
paint_spatial_force_arrow(ui.painter(), center, *force, 20.0, 34.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn strongest_hand_spatial_force(&self) -> Option<HudSpatialForce> {
|
||||
self.latest_hand_spatial_forces
|
||||
.iter()
|
||||
.flatten()
|
||||
.copied()
|
||||
.max_by(|a, b| a.magnitude.total_cmp(&b.magnitude))
|
||||
}
|
||||
|
||||
fn analyze_spatial_force(&mut self, values: &[u32]) {
|
||||
if matches!(self.config_state.mode, SerialMode::Hand) {
|
||||
self.latest_hand_spatial_forces = self.force_estimator.analyze_fingertips(values);
|
||||
self.latest_spatial_force = self.strongest_hand_spatial_force();
|
||||
} else {
|
||||
self.latest_spatial_force = self.force_estimator.analyze(values);
|
||||
self.latest_hand_spatial_forces = [None; HAND_FINGERTIP_COUNT];
|
||||
}
|
||||
}
|
||||
|
||||
fn connection_has_live_data(&self) -> bool {
|
||||
matches!(
|
||||
self.connection.state(),
|
||||
ConnectionState::Connected | ConnectionState::Streaming
|
||||
)
|
||||
}
|
||||
|
||||
fn recent_sample_is_fresh(&self) -> bool {
|
||||
self.last_sample_at
|
||||
.is_some_and(|sample_at| sample_at.elapsed() <= DATA_IDLE_PANEL_EXIT_DELAY)
|
||||
}
|
||||
|
||||
fn hand_force_panels_visible(&self) -> bool {
|
||||
self.connection_has_live_data() && self.recent_sample_is_fresh()
|
||||
}
|
||||
|
||||
fn sync_disconnected_live_state(&mut self) {
|
||||
if self.connection_has_live_data() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.last_sample_at = None;
|
||||
self.latest_spatial_force = None;
|
||||
self.latest_hand_spatial_forces = [None; HAND_FINGERTIP_COUNT];
|
||||
self.spatial_force_panel_force = None;
|
||||
}
|
||||
|
||||
fn draw_workspace(&mut self, ui: &mut egui::Ui) {
|
||||
@@ -264,6 +327,7 @@ impl EskinDesktopApp {
|
||||
|
||||
fn update_pressure_matrix(&mut self) {
|
||||
if let Some(sample) = self.connection.take_latest_sample() {
|
||||
self.last_sample_at = Some(Instant::now());
|
||||
normalize_pressure_sample(
|
||||
&sample.matrix,
|
||||
sample.rows,
|
||||
@@ -276,7 +340,7 @@ impl EskinDesktopApp {
|
||||
self.latest_matrix_rows = sample.rows;
|
||||
self.latest_matrix_cols = sample.cols;
|
||||
|
||||
self.latest_spatial_force = self.force_estimator.analyze(&sample.matrix);
|
||||
self.analyze_spatial_force(&sample.matrix);
|
||||
|
||||
// Keep JE-Skin's summary path separate from the optional spatial-force vector.
|
||||
let raw_total = sample
|
||||
@@ -431,7 +495,11 @@ impl EskinDesktopApp {
|
||||
}
|
||||
|
||||
self.stats_panel.visible = false;
|
||||
draw_hand_force_panels(ctx, true, &self.hand_signal_histories);
|
||||
draw_hand_force_panels(
|
||||
ctx,
|
||||
self.hand_force_panels_visible(),
|
||||
&self.hand_signal_histories,
|
||||
);
|
||||
}
|
||||
|
||||
fn draw_panel_context_menu(&mut self, ctx: &egui::Context) {
|
||||
@@ -518,8 +586,10 @@ impl EskinDesktopApp {
|
||||
self.connection.disconnect();
|
||||
self.pressure_matrix.fill([0.0, 0.0]);
|
||||
self.hand_pressure.clear();
|
||||
self.last_sample_at = None;
|
||||
self.force_estimator.reset();
|
||||
self.latest_spatial_force = None;
|
||||
self.latest_hand_spatial_forces = [None; HAND_FINGERTIP_COUNT];
|
||||
self.spatial_force_panel_force = None;
|
||||
self.signal_history.clear();
|
||||
self.hand_signal_histories
|
||||
@@ -538,6 +608,60 @@ impl EskinDesktopApp {
|
||||
}
|
||||
}
|
||||
|
||||
fn hand_image_pixel_to_screen(rect: egui::Rect, point_px: [f32; 2]) -> egui::Pos2 {
|
||||
let image_uv = egui::vec2(
|
||||
point_px[0] / HAND_IMAGE_SIZE_PX[0].max(1.0),
|
||||
point_px[1] / HAND_IMAGE_SIZE_PX[1].max(1.0),
|
||||
);
|
||||
let viewport_aspect = rect.width() / rect.height().max(1.0);
|
||||
let image_aspect = HAND_IMAGE_SIZE_PX[0] / HAND_IMAGE_SIZE_PX[1].max(1.0);
|
||||
let mut screen_uv = image_uv;
|
||||
|
||||
if viewport_aspect > image_aspect {
|
||||
let image_width = image_aspect / viewport_aspect;
|
||||
screen_uv.x = image_uv.x * image_width + (1.0 - image_width) * 0.5;
|
||||
} else {
|
||||
let image_height = viewport_aspect / image_aspect;
|
||||
screen_uv.y = image_uv.y * image_height + (1.0 - image_height) * 0.5;
|
||||
}
|
||||
|
||||
egui::pos2(
|
||||
rect.left() + screen_uv.x * rect.width(),
|
||||
rect.top() + screen_uv.y * rect.height(),
|
||||
)
|
||||
}
|
||||
|
||||
fn paint_spatial_force_arrow(
|
||||
painter: &egui::Painter,
|
||||
center: egui::Pos2,
|
||||
force: HudSpatialForce,
|
||||
base_length: f32,
|
||||
magnitude_scale: f32,
|
||||
) {
|
||||
let magnitude = force.magnitude.clamp(0.0, 1.8);
|
||||
let length = base_length + magnitude * magnitude_scale;
|
||||
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 head_length = (length * 0.24).clamp(8.0, 14.0);
|
||||
let head_width = head_length * 0.5;
|
||||
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(8.0, glow));
|
||||
painter.line_segment([center, end], egui::Stroke::new(2.4, color));
|
||||
painter.line_segment(
|
||||
[end, end - direction * head_length + side * head_width],
|
||||
egui::Stroke::new(2.4, color),
|
||||
);
|
||||
painter.line_segment(
|
||||
[end, end - direction * head_length - side * head_width],
|
||||
egui::Stroke::new(2.4, color),
|
||||
);
|
||||
painter.circle_filled(center, 3.8, color);
|
||||
}
|
||||
|
||||
fn update_hand_signal_histories(
|
||||
histories: &mut [Vec<f32>; HAND_FORCE_PANEL_COUNT],
|
||||
raw_values: &[u32],
|
||||
@@ -718,6 +842,7 @@ impl eframe::App for EskinDesktopApp {
|
||||
self.live_rates.tick_render(stats.rx_frames);
|
||||
|
||||
self.update_pressure_matrix();
|
||||
self.sync_disconnected_live_state();
|
||||
self.draw_workspace(ui);
|
||||
self.draw_title_bar(ui, frame);
|
||||
self.draw_floating_panels(&ctx, stats);
|
||||
|
||||
Reference in New Issue
Block a user