Add animated force panels
This commit is contained in:
271
src/ui.rs
271
src/ui.rs
@@ -697,84 +697,181 @@ pub fn draw_stats_panel(
|
||||
ctx: &egui::Context,
|
||||
panel: &mut FloatingPanelState,
|
||||
force_history: &[f32],
|
||||
spatial_force: Option<HudSpatialForce>,
|
||||
_spatial_force: Option<HudSpatialForce>,
|
||||
) {
|
||||
draw_floating_panel(ctx, panel, "统计", "stats_panel", |ui| {
|
||||
ui.set_min_width(320.0);
|
||||
draw_resultant_force_chart(ui, force_history, spatial_force);
|
||||
});
|
||||
const PANEL_WIDTH: f32 = 420.0;
|
||||
const PANEL_HEIGHT: f32 = 380.0;
|
||||
const PANEL_OUTSIDE_GAP: f32 = 18.0;
|
||||
|
||||
let force_active = has_recent_resultant_force(force_history);
|
||||
let target_visible = panel.visible && force_active;
|
||||
let anim = ctx.animate_bool(egui::Id::new("stats_panel_force_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 eased = ease_in_out(anim);
|
||||
let screen = ctx.content_rect();
|
||||
let target_x = panel.default_pos.x;
|
||||
let hidden_x = if target_x < screen.center().x {
|
||||
screen.left() - PANEL_WIDTH - PANEL_OUTSIDE_GAP
|
||||
} else {
|
||||
screen.right() + PANEL_OUTSIDE_GAP
|
||||
};
|
||||
let x = egui::lerp(hidden_x..=target_x, eased);
|
||||
let y = panel.default_pos.y;
|
||||
|
||||
egui::Area::new(egui::Id::new("stats_panel"))
|
||||
.fixed_pos(egui::pos2(x, y))
|
||||
.order(egui::Order::Foreground)
|
||||
.show(ctx, |ui| {
|
||||
panel_frame(ctx).show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing = egui::vec2(METRICS.item_gap, 6.0);
|
||||
ui.set_min_width(PANEL_WIDTH);
|
||||
ui.set_min_height(PANEL_HEIGHT);
|
||||
draw_force_chart_panel_contents(ui, "RF", "合力", force_history, PANEL_HEIGHT);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const FORCE_CHART_MAX_N: f32 = 25.6;
|
||||
const FORCE_CHART_SAMPLE_SECONDS: f32 = 0.1;
|
||||
const FORCE_PANEL_ACTIVE_TAIL: usize = 8;
|
||||
|
||||
fn draw_resultant_force_chart(
|
||||
ui: &mut egui::Ui,
|
||||
const HAND_FORCE_PANEL_WIDTH: f32 = 260.0;
|
||||
const HAND_FORCE_PANEL_HEIGHT: f32 = 150.0;
|
||||
const HAND_FORCE_PANEL_GAP: f32 = 12.0;
|
||||
const HAND_FORCE_PANEL_BOTTOM_MARGIN: f32 = 24.0;
|
||||
const HAND_FORCE_PANEL_TITLES: [(&str, &str); 7] = [
|
||||
("T1", "拇指"),
|
||||
("T2", "食指"),
|
||||
("T3", "中指"),
|
||||
("T4", "无名指"),
|
||||
("T5", "小指"),
|
||||
("P1", "掌心横区"),
|
||||
("P2", "掌心纵区"),
|
||||
];
|
||||
|
||||
fn has_recent_resultant_force(values: &[f32]) -> bool {
|
||||
values
|
||||
.iter()
|
||||
.rev()
|
||||
.take(FORCE_PANEL_ACTIVE_TAIL)
|
||||
.any(|value| *value > 0.0)
|
||||
}
|
||||
|
||||
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 first_row_y = screen.bottom()
|
||||
- HAND_FORCE_PANEL_BOTTOM_MARGIN
|
||||
- HAND_FORCE_PANEL_HEIGHT * 2.0
|
||||
- HAND_FORCE_PANEL_GAP;
|
||||
let second_row_y = screen.bottom() - HAND_FORCE_PANEL_BOTTOM_MARGIN - HAND_FORCE_PANEL_HEIGHT;
|
||||
|
||||
for index in 0..HAND_FORCE_PANEL_TITLES.len() {
|
||||
let history = histories.get(index).map(Vec::as_slice).unwrap_or(&[]);
|
||||
let (code, title) = HAND_FORCE_PANEL_TITLES[index];
|
||||
let row_count: usize = if index < 4 { 4 } else { 3 };
|
||||
let row_index = if index < 4 { index } else { index - 4 };
|
||||
let total_width = row_count as f32 * HAND_FORCE_PANEL_WIDTH
|
||||
+ (row_count.saturating_sub(1)) as f32 * HAND_FORCE_PANEL_GAP;
|
||||
let row_left = screen.center().x - total_width * 0.5;
|
||||
let target_x =
|
||||
row_left + row_index as f32 * (HAND_FORCE_PANEL_WIDTH + HAND_FORCE_PANEL_GAP);
|
||||
let target_y = if index < 4 { first_row_y } else { second_row_y };
|
||||
|
||||
draw_force_chart_area(
|
||||
ctx,
|
||||
egui::Id::new(format!("hand_force_panel_{index}")),
|
||||
egui::pos2(target_x, target_y),
|
||||
egui::vec2(HAND_FORCE_PANEL_WIDTH, HAND_FORCE_PANEL_HEIGHT),
|
||||
target_visible,
|
||||
code,
|
||||
title,
|
||||
history,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_force_chart_area(
|
||||
ctx: &egui::Context,
|
||||
id: egui::Id,
|
||||
target_pos: egui::Pos2,
|
||||
size: egui::Vec2,
|
||||
target_visible: bool,
|
||||
code: &'static str,
|
||||
title: &'static str,
|
||||
values: &[f32],
|
||||
spatial_force: Option<HudSpatialForce>,
|
||||
) {
|
||||
let latest = values.last().copied().unwrap_or(0.0);
|
||||
let max = values.iter().copied().fold(0.0_f32, f32::max);
|
||||
let active_values = values.iter().copied().filter(|value| *value > 0.0);
|
||||
let min = active_values.fold(None, |best: Option<f32>, value| {
|
||||
Some(best.map_or(value, |current| current.min(value)))
|
||||
});
|
||||
let anim = ctx.animate_bool(id.with("enter"), target_visible);
|
||||
|
||||
group_frame().show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.colored_label(ACCENT_BLUE, "RF");
|
||||
ui.label(style::panel_title("合力"));
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
ui.colored_label(ACCENT_GREEN, format!("{latest:.1} N"));
|
||||
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 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);
|
||||
|
||||
egui::Area::new(id)
|
||||
.fixed_pos(egui::pos2(target_pos.x, y))
|
||||
.order(egui::Order::Foreground)
|
||||
.show(ctx, |ui| {
|
||||
panel_frame(ctx).show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing = egui::vec2(METRICS.item_gap, 6.0);
|
||||
ui.set_min_width(size.x);
|
||||
ui.set_min_height(size.y);
|
||||
draw_force_chart_panel_contents(ui, code, title, values, size.y);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ui.add_space(4.0);
|
||||
ui.horizontal(|ui| {
|
||||
force_metric(ui, "峰值", max);
|
||||
ui.separator();
|
||||
force_metric(ui, "低值", min.unwrap_or(0.0));
|
||||
ui.separator();
|
||||
ui.colored_label(dim_text(), format!("{} 点", values.len()));
|
||||
fn draw_force_chart_panel_contents(
|
||||
ui: &mut egui::Ui,
|
||||
code: &'static str,
|
||||
title: &'static str,
|
||||
values: &[f32],
|
||||
content_height: f32,
|
||||
) {
|
||||
let latest = values.last().copied().unwrap_or(0.0);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.colored_label(ACCENT_BLUE, code);
|
||||
ui.label(style::panel_title(title));
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
ui.colored_label(ACCENT_GREEN, format!("{latest:.1} N"));
|
||||
});
|
||||
|
||||
ui.add_space(6.0);
|
||||
paint_resultant_force_chart(ui, values);
|
||||
|
||||
ui.add_space(8.0);
|
||||
draw_spatial_force_readout(ui, spatial_force);
|
||||
});
|
||||
|
||||
ui.add_space(6.0);
|
||||
let chart_height = (content_height - 32.0).max(90.0);
|
||||
paint_resultant_force_chart(ui, values, chart_height);
|
||||
}
|
||||
|
||||
fn draw_spatial_force_readout(ui: &mut egui::Ui, spatial_force: Option<HudSpatialForce>) {
|
||||
let Some(force) = spatial_force else {
|
||||
ui.horizontal(|ui| {
|
||||
ui.colored_label(dim_text(), "3D");
|
||||
ui.label(style::subtle_text("等待三维力数据"));
|
||||
});
|
||||
return;
|
||||
};
|
||||
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.colored_label(ACCENT_BLUE, "3D");
|
||||
ui.label(style::value_text(format!("angle {:.0}°", force.angle_deg)));
|
||||
ui.separator();
|
||||
ui.label(style::value_text(format!("mag {:.2}", force.magnitude)));
|
||||
});
|
||||
}
|
||||
|
||||
fn force_metric(ui: &mut egui::Ui, label: &'static str, value: f32) {
|
||||
ui.colored_label(dim_text(), label);
|
||||
ui.label(style::value_text(format!("{value:.1} N")));
|
||||
}
|
||||
|
||||
fn paint_resultant_force_chart(ui: &mut egui::Ui, values: &[f32]) {
|
||||
const CHART_HEIGHT: f32 = 138.0;
|
||||
fn paint_resultant_force_chart(ui: &mut egui::Ui, values: &[f32], chart_height: f32) {
|
||||
const CHART_POINTS: usize = 42;
|
||||
const CHART_WINDOW_SECONDS: f32 = CHART_POINTS as f32 * FORCE_CHART_SAMPLE_SECONDS;
|
||||
|
||||
let width = ui.available_width().max(280.0);
|
||||
let (rect, _) = ui.allocate_exact_size(egui::vec2(width, CHART_HEIGHT), egui::Sense::hover());
|
||||
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);
|
||||
let time = ui.ctx().input(|input| input.time) as f32;
|
||||
|
||||
painter.rect_filled(rect, radius, egui::Color32::from_rgb(10, 18, 22));
|
||||
painter.rect_stroke(
|
||||
@@ -789,7 +886,7 @@ fn paint_resultant_force_chart(ui: &mut egui::Ui, values: &[f32]) {
|
||||
rect.right_bottom() - egui::vec2(10.0, 24.0),
|
||||
);
|
||||
|
||||
paint_force_grid(&painter, rect, chart_rect);
|
||||
paint_force_grid(&painter, rect, chart_rect, time, CHART_WINDOW_SECONDS);
|
||||
|
||||
if values.is_empty() {
|
||||
painter.text(
|
||||
@@ -804,12 +901,13 @@ fn paint_resultant_force_chart(ui: &mut egui::Ui, values: &[f32]) {
|
||||
|
||||
ui.ctx().request_repaint();
|
||||
|
||||
let time = ui.ctx().input(|input| input.time) as f32;
|
||||
let enter = ui
|
||||
.ctx()
|
||||
.animate_bool(egui::Id::new("resultant_force_chart_enter"), true);
|
||||
let slide_offset = (1.0 - enter) * -chart_rect.width() * 0.65;
|
||||
let plot_rect = chart_rect.translate(egui::vec2(slide_offset, 0.0));
|
||||
let tick_phase = (time / FORCE_CHART_SAMPLE_SECONDS).fract();
|
||||
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 start_slot = CHART_POINTS.saturating_sub(values.len());
|
||||
@@ -852,7 +950,13 @@ fn paint_resultant_force_chart(ui: &mut egui::Ui, values: &[f32]) {
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_force_grid(painter: &egui::Painter, rect: egui::Rect, chart_rect: egui::Rect) {
|
||||
fn paint_force_grid(
|
||||
painter: &egui::Painter,
|
||||
rect: egui::Rect,
|
||||
chart_rect: egui::Rect,
|
||||
time: f32,
|
||||
window_seconds: f32,
|
||||
) {
|
||||
for tick in [25.0, 20.0, 15.0, 10.0, 5.0, 0.0] {
|
||||
let normalized = (tick / FORCE_CHART_MAX_N).clamp(0.0, 1.0);
|
||||
let y = chart_rect.bottom() - normalized * chart_rect.height();
|
||||
@@ -883,20 +987,25 @@ fn paint_force_grid(painter: &egui::Painter, rect: egui::Rect, chart_rect: egui:
|
||||
);
|
||||
}
|
||||
|
||||
painter.text(
|
||||
egui::pos2(chart_rect.left(), rect.bottom() - 11.0),
|
||||
egui::Align2::LEFT_CENTER,
|
||||
"-4.2s",
|
||||
egui::FontId::monospace(9.0),
|
||||
ONE_DARK_PRO.text_subtle,
|
||||
);
|
||||
painter.text(
|
||||
egui::pos2(chart_rect.right(), rect.bottom() - 11.0),
|
||||
egui::Align2::RIGHT_CENTER,
|
||||
"now",
|
||||
egui::FontId::monospace(9.0),
|
||||
ONE_DARK_PRO.text_subtle,
|
||||
);
|
||||
let start_time = (time - window_seconds).max(0.0);
|
||||
for index in 0..=5 {
|
||||
let fraction = index as f32 / 5.0;
|
||||
let x = chart_rect.left() + fraction * chart_rect.width();
|
||||
let label_time = start_time + fraction * (time - start_time);
|
||||
let align = match index {
|
||||
0 => egui::Align2::LEFT_CENTER,
|
||||
5 => egui::Align2::RIGHT_CENTER,
|
||||
_ => egui::Align2::CENTER_CENTER,
|
||||
};
|
||||
|
||||
painter.text(
|
||||
egui::pos2(x, rect.bottom() - 11.0),
|
||||
align,
|
||||
format!("{label_time:.1}s"),
|
||||
egui::FontId::monospace(9.0),
|
||||
ONE_DARK_PRO.text_subtle,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_force_sweep(painter: &egui::Painter, chart_rect: egui::Rect, time: f32) {
|
||||
@@ -1613,9 +1722,15 @@ pub fn draw_matrix_config_panel(
|
||||
}
|
||||
|
||||
pub fn panel_restore_item(ui: &mut egui::Ui, title: &'static str, panel: &mut FloatingPanelState) {
|
||||
let button_size = egui::vec2(ui.available_width(), 0.0);
|
||||
if panel.visible {
|
||||
ui.add_enabled(false, egui::Button::new(format!("{title} 已显示")));
|
||||
} else if ui.button(format!("显示 {title}")).clicked() {
|
||||
ui.add_enabled_ui(false, |ui| {
|
||||
ui.add_sized(button_size, egui::Button::new(format!("{title} 已显示")));
|
||||
});
|
||||
} else if ui
|
||||
.add_sized(button_size, egui::Button::new(format!("显示 {title}")))
|
||||
.clicked()
|
||||
{
|
||||
panel.visible = true;
|
||||
ui.close();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user