Integrate hand fingertip force overlays
This commit is contained in:
267
src/ui.rs
267
src/ui.rs
@@ -413,8 +413,11 @@ pub fn draw_config_panel(
|
||||
.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().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),
|
||||
@@ -426,93 +429,93 @@ pub fn draw_config_panel(
|
||||
|
||||
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.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.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.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());
|
||||
}
|
||||
}
|
||||
|
||||
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(), "采样率");
|
||||
});
|
||||
|
||||
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(), "采样率");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
changed_mode
|
||||
@@ -954,14 +957,17 @@ pub fn draw_stats_panel(
|
||||
const FORCE_CHART_MAX_N: f32 = 25.6;
|
||||
const FORCE_CHART_SAMPLE_SECONDS: f32 = 0.1;
|
||||
const FORCE_PANEL_ACTIVE_TAIL: usize = 8;
|
||||
const FORCE_PANEL_EXIT_DELAY_SECONDS: f64 = 3.0;
|
||||
|
||||
const HAND_FORCE_PANEL_MIN_WIDTH: f32 = 220.0;
|
||||
const HAND_FORCE_PANEL_MAX_WIDTH: f32 = 500.0;
|
||||
const HAND_FORCE_PANEL_MIN_HEIGHT: f32 = 104.0;
|
||||
const HAND_FORCE_PANEL_MAX_HEIGHT: f32 = 190.0;
|
||||
const HAND_FORCE_PANEL_GAP: f32 = 12.0;
|
||||
const HAND_FORCE_PANEL_MIN_WIDTH: f32 = 160.0;
|
||||
const HAND_FORCE_PANEL_MAX_WIDTH: f32 = 560.0;
|
||||
const HAND_FORCE_PANEL_MIN_HEIGHT: f32 = 72.0;
|
||||
const HAND_FORCE_PANEL_MAX_HEIGHT: f32 = 230.0;
|
||||
const HAND_FORCE_PANEL_GAP: f32 = 20.0;
|
||||
const HAND_FORCE_PANEL_SIDE_MARGIN: f32 = 24.0;
|
||||
const HAND_FORCE_PANEL_VERTICAL_MARGIN: f32 = 28.0;
|
||||
const HAND_FORCE_CENTER_MIN_WIDTH: f32 = 420.0;
|
||||
const HAND_FORCE_CENTER_MAX_WIDTH: f32 = 820.0;
|
||||
const HAND_FORCE_PANEL_TITLES: [(&str, &str); 7] = [
|
||||
("T1", "拇指"),
|
||||
("T2", "食指"),
|
||||
@@ -981,15 +987,14 @@ fn has_recent_resultant_force(values: &[f32]) -> bool {
|
||||
}
|
||||
|
||||
pub fn draw_hand_force_panels(ctx: &egui::Context, visible: bool, histories: &[Vec<f32>]) {
|
||||
let hand_active = histories
|
||||
.iter()
|
||||
.any(|history| has_recent_resultant_force(history));
|
||||
let target_visible = visible && hand_active;
|
||||
let screen = ctx.content_rect();
|
||||
let side_margin = HAND_FORCE_PANEL_SIDE_MARGIN.min((screen.width() * 0.025).max(10.0));
|
||||
let vertical_margin = HAND_FORCE_PANEL_VERTICAL_MARGIN.min((screen.height() * 0.04).max(10.0));
|
||||
let gap = HAND_FORCE_PANEL_GAP.min((screen.height() * 0.018).max(6.0));
|
||||
let side_width = ((screen.width() - side_margin * 4.0) * 0.25).max(0.0);
|
||||
let gap = HAND_FORCE_PANEL_GAP.min((screen.height() * 0.024).max(12.0));
|
||||
let reserved_center_width = (screen.width() * 0.42)
|
||||
.clamp(HAND_FORCE_CENTER_MIN_WIDTH, HAND_FORCE_CENTER_MAX_WIDTH)
|
||||
.min((screen.width() - side_margin * 2.0).max(0.0));
|
||||
let side_width = ((screen.width() - reserved_center_width - side_margin * 2.0) * 0.5).max(0.0);
|
||||
let panel_width = side_width
|
||||
.min(HAND_FORCE_PANEL_MAX_WIDTH)
|
||||
.max(HAND_FORCE_PANEL_MIN_WIDTH.min(side_width));
|
||||
@@ -997,17 +1002,16 @@ pub fn draw_hand_force_panels(ctx: &egui::Context, visible: bool, histories: &[V
|
||||
let right_x = screen.right() - side_margin - panel_width;
|
||||
let left_count = 4usize;
|
||||
let right_count = HAND_FORCE_PANEL_TITLES.len() - left_count;
|
||||
let available_height =
|
||||
(screen.height() - layout::TITLE_BAR_HEIGHT - vertical_margin * 2.0).max(0.0);
|
||||
let chrome_height = layout::TITLE_BAR_HEIGHT + layout::CONFIG_BAR_HEIGHT;
|
||||
let available_height = (screen.height() - chrome_height - vertical_margin * 2.0).max(0.0);
|
||||
let panel_height = ((available_height - (left_count - 1) as f32 * gap) / left_count as f32)
|
||||
.clamp(
|
||||
HAND_FORCE_PANEL_MIN_HEIGHT.min(available_height),
|
||||
HAND_FORCE_PANEL_MAX_HEIGHT,
|
||||
);
|
||||
.max(0.0)
|
||||
.min(HAND_FORCE_PANEL_MAX_HEIGHT)
|
||||
.max(HAND_FORCE_PANEL_MIN_HEIGHT.min(available_height / left_count as f32));
|
||||
let left_height = left_count as f32 * panel_height + (left_count - 1) as f32 * gap;
|
||||
let right_height =
|
||||
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() + chrome_height + vertical_margin;
|
||||
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);
|
||||
|
||||
@@ -1029,7 +1033,8 @@ pub fn draw_hand_force_panels(ctx: &egui::Context, visible: bool, histories: &[V
|
||||
egui::Id::new(format!("hand_force_panel_{index}")),
|
||||
egui::pos2(target_x, target_y),
|
||||
egui::vec2(panel_width, panel_height),
|
||||
target_visible,
|
||||
index < left_count,
|
||||
visible,
|
||||
code,
|
||||
title,
|
||||
history,
|
||||
@@ -1042,12 +1047,35 @@ fn draw_force_chart_area(
|
||||
id: egui::Id,
|
||||
target_pos: egui::Pos2,
|
||||
size: egui::Vec2,
|
||||
from_left: bool,
|
||||
target_visible: bool,
|
||||
code: &'static str,
|
||||
title: &'static str,
|
||||
values: &[f32],
|
||||
) {
|
||||
let anim = ctx.animate_bool(id.with("enter"), target_visible);
|
||||
let now = ctx.input(|input| input.time);
|
||||
let active = target_visible && has_recent_resultant_force(values);
|
||||
let state_id = id.with("active_state");
|
||||
let mut last_active_at = ctx.data_mut(|data| data.get_temp::<f64>(state_id));
|
||||
if active {
|
||||
last_active_at = Some(now);
|
||||
ctx.data_mut(|data| data.insert_temp(state_id, now));
|
||||
} else if !target_visible {
|
||||
last_active_at = None;
|
||||
ctx.data_mut(|data| data.remove::<f64>(state_id));
|
||||
}
|
||||
|
||||
let target_visible =
|
||||
last_active_at.is_some_and(|active_at| now - active_at <= FORCE_PANEL_EXIT_DELAY_SECONDS);
|
||||
let anim_id = id.with("enter");
|
||||
let anim_seeded_id = id.with("enter_seeded");
|
||||
let anim_seeded = ctx.data_mut(|data| data.get_temp::<bool>(anim_seeded_id).unwrap_or(false));
|
||||
if target_visible && !anim_seeded {
|
||||
ctx.animate_bool(anim_id, false);
|
||||
ctx.data_mut(|data| data.insert_temp(anim_seeded_id, true));
|
||||
}
|
||||
|
||||
let anim = ctx.animate_bool(anim_id, target_visible);
|
||||
|
||||
if anim <= 0.01 {
|
||||
return;
|
||||
@@ -1059,20 +1087,27 @@ fn draw_force_chart_area(
|
||||
}
|
||||
|
||||
let eased = ease_in_out(anim);
|
||||
let hidden_y = ctx.content_rect().bottom() + HAND_FORCE_PANEL_GAP;
|
||||
let y = egui::lerp(hidden_y..=target_pos.y, eased);
|
||||
let hidden_x = if from_left {
|
||||
ctx.content_rect().left() - size.x - HAND_FORCE_PANEL_GAP
|
||||
} else {
|
||||
ctx.content_rect().right() + HAND_FORCE_PANEL_GAP
|
||||
};
|
||||
let x = egui::lerp(hidden_x..=target_pos.x, eased);
|
||||
|
||||
egui::Area::new(id)
|
||||
.fixed_pos(egui::pos2(target_pos.x, y))
|
||||
.fixed_pos(egui::pos2(x, target_pos.y))
|
||||
.constrain_to(viewport_panel_rect(ctx))
|
||||
.order(egui::Order::Foreground)
|
||||
.show(ctx, |ui| {
|
||||
panel_frame(ctx).show(ui, |ui| {
|
||||
let padding = METRICS.panel_padding as f32;
|
||||
let inner_width = (size.x - padding * 2.0).max(80.0);
|
||||
let inner_height = (size.y - padding * 2.0).max(48.0);
|
||||
ui.spacing_mut().item_spacing = egui::vec2(METRICS.item_gap, 6.0);
|
||||
ui.set_min_width(size.x);
|
||||
ui.set_max_width(size.x);
|
||||
ui.set_min_height(size.y);
|
||||
draw_force_chart_panel_contents(ui, code, title, values, size.y);
|
||||
ui.set_min_width(inner_width);
|
||||
ui.set_max_width(inner_width);
|
||||
ui.set_min_height(inner_height);
|
||||
draw_force_chart_panel_contents(ui, code, title, values, inner_height);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1095,7 +1130,7 @@ fn draw_force_chart_panel_contents(
|
||||
});
|
||||
|
||||
ui.add_space(6.0);
|
||||
let chart_height = (content_height - 32.0).max(90.0);
|
||||
let chart_height = (content_height - 32.0).max(36.0);
|
||||
paint_resultant_force_chart(ui, values, chart_height);
|
||||
}
|
||||
|
||||
@@ -1103,7 +1138,7 @@ fn paint_resultant_force_chart(ui: &mut egui::Ui, values: &[f32], chart_height:
|
||||
const CHART_POINTS: usize = 42;
|
||||
const CHART_WINDOW_SECONDS: f32 = CHART_POINTS as f32 * FORCE_CHART_SAMPLE_SECONDS;
|
||||
|
||||
let width = ui.available_width().max(180.0);
|
||||
let width = ui.available_width().max(80.0);
|
||||
let (rect, _) = ui.allocate_exact_size(egui::vec2(width, chart_height), egui::Sense::hover());
|
||||
let painter = ui.painter_at(rect);
|
||||
let radius = egui::CornerRadius::same(5);
|
||||
@@ -1358,8 +1393,10 @@ fn draw_pinned_panel(
|
||||
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 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 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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user