feat: 添加 README,更新 .gitignore,移除 JE-Skin/eskin-finger-sdk
- 添加 README.md 项目文档 - 更新 .gitignore 排除 JE-Skin/、eskin-finger-sdk/ 及构建产物 - 从 git 跟踪中移除 JE-Skin 和 eskin-finger-sdk Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1 +1,10 @@
|
|||||||
/target
|
/target
|
||||||
|
JE-Skin/
|
||||||
|
eskin-finger-sdk/
|
||||||
|
*.err
|
||||||
|
*.out
|
||||||
|
*.exe
|
||||||
|
*.pdb
|
||||||
|
*.d
|
||||||
|
*.rlib
|
||||||
|
*.rmeta
|
||||||
|
|||||||
1
JE-Skin
1
JE-Skin
Submodule JE-Skin deleted from 59e9203363
42
README.md
Normal file
42
README.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Eskin Model Player
|
||||||
|
|
||||||
|
实时压力矩阵可视化桌面应用,用于连接 E-Skin 传感器设备并通过串口接收压力数据,以热力图方式实时渲染。
|
||||||
|
|
||||||
|
## 功能
|
||||||
|
|
||||||
|
- 串口连接 E-Skin 传感器(921600 baud)
|
||||||
|
- 实时压力矩阵热力图渲染(wgpu)
|
||||||
|
- 自定义无边框窗口,macOS 风格标题栏
|
||||||
|
- 浮动面板:连接管理、场景视图、配置、数据统计
|
||||||
|
|
||||||
|
## 依赖
|
||||||
|
|
||||||
|
- Rust 2024 edition
|
||||||
|
- [eframe](https://crates.io/crates/eframe) 0.34(egui + wgpu)
|
||||||
|
- [serialport](https://crates.io/crates/serialport) 4.9
|
||||||
|
|
||||||
|
## 构建与运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run --release
|
||||||
|
```
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── main.rs # 入口,创建 eframe 窗口
|
||||||
|
├── app.rs # 应用主循环与面板调度
|
||||||
|
├── connection.rs # 串口连接管理(后台线程)
|
||||||
|
├── serial_core/ # 串口协议编解码
|
||||||
|
│ ├── serial.rs # 串口读写循环
|
||||||
|
│ ├── codec.rs # 帧编解码器
|
||||||
|
│ ├── frame.rs # 帧结构定义
|
||||||
|
│ └── ...
|
||||||
|
├── render.rs # wgpu 渲染管线(背景 + 数字叠加)
|
||||||
|
├── matrix.rs # 矩阵布局与坐标变换
|
||||||
|
├── ui.rs # egui 浮动面板 UI
|
||||||
|
├── theme.rs # 深色工程主题
|
||||||
|
├── shader.wgsl # WGSL 着色器
|
||||||
|
└── utils.rs # 工具函数
|
||||||
|
```
|
||||||
Submodule eskin-finger-sdk deleted from aa1b312290
154
src/app.rs
154
src/app.rs
@@ -5,7 +5,9 @@ use crate::connection::ConnectionManager;
|
|||||||
use crate::theme::{ENGINEERING_DARK, apply_fonts, apply_theme};
|
use crate::theme::{ENGINEERING_DARK, apply_fonts, apply_theme};
|
||||||
use crate::{
|
use crate::{
|
||||||
matrix::{MATRIX_COLS, MATRIX_ROWS},
|
matrix::{MATRIX_COLS, MATRIX_ROWS},
|
||||||
render::{BackgroundRenderResources, PRESSURE_CELL_COUNT, WgpuBackgroundCallback},
|
render::{
|
||||||
|
BackgroundRenderResources, PRESSURE_CELL_COUNT, PressureFrame, WgpuBackgroundCallback,
|
||||||
|
},
|
||||||
ui::{
|
ui::{
|
||||||
ConfigPanelState, ConnectPanelState, FloatingPanelState, draw_config_panel,
|
ConfigPanelState, ConnectPanelState, FloatingPanelState, draw_config_panel,
|
||||||
draw_connect_panel, draw_scene_panel, draw_stats_panel,
|
draw_connect_panel, draw_scene_panel, draw_stats_panel,
|
||||||
@@ -18,7 +20,7 @@ pub struct EskinDesktopApp {
|
|||||||
connect_panel: FloatingPanelState,
|
connect_panel: FloatingPanelState,
|
||||||
connect_state: ConnectPanelState,
|
connect_state: ConnectPanelState,
|
||||||
connection: Arc<ConnectionManager>,
|
connection: Arc<ConnectionManager>,
|
||||||
pressure_matrix: [f32; PRESSURE_CELL_COUNT],
|
pressure_matrix: PressureFrame,
|
||||||
data_log_frame: u64,
|
data_log_frame: u64,
|
||||||
scene_panel: FloatingPanelState,
|
scene_panel: FloatingPanelState,
|
||||||
config_panel: FloatingPanelState,
|
config_panel: FloatingPanelState,
|
||||||
@@ -51,7 +53,7 @@ impl EskinDesktopApp {
|
|||||||
connect_panel: FloatingPanelState::new([0.0, 0.0], [0.0, 0.0]),
|
connect_panel: FloatingPanelState::new([0.0, 0.0], [0.0, 0.0]),
|
||||||
connect_state: ConnectPanelState::default(),
|
connect_state: ConnectPanelState::default(),
|
||||||
connection: Arc::new(ConnectionManager::new()),
|
connection: Arc::new(ConnectionManager::new()),
|
||||||
pressure_matrix: [0.0; PRESSURE_CELL_COUNT],
|
pressure_matrix: [[0.0, 0.0]; PRESSURE_CELL_COUNT],
|
||||||
data_log_frame: 0,
|
data_log_frame: 0,
|
||||||
scene_panel: FloatingPanelState::new([16.0, 48.0], [16.0, 48.0]),
|
scene_panel: FloatingPanelState::new([16.0, 48.0], [16.0, 48.0]),
|
||||||
config_panel: FloatingPanelState::new([840.0, 48.0], [128.0, 48.0]),
|
config_panel: FloatingPanelState::new([840.0, 48.0], [128.0, 48.0]),
|
||||||
@@ -92,15 +94,115 @@ impl EskinDesktopApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_toolbar(&mut self, ui: &mut egui::Ui) {
|
fn draw_title_bar(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||||
egui::Panel::top("main_menu").show_inside(ui, |ui| {
|
let title_bar_height = 36.0;
|
||||||
ui.horizontal(|ui| {
|
let title_bar_rect = ui
|
||||||
ui.checkbox(&mut self.connect_panel.visible, "连接");
|
.allocate_space(egui::vec2(ui.available_width(), title_bar_height))
|
||||||
ui.checkbox(&mut self.scene_panel.visible, "场景");
|
.1;
|
||||||
ui.checkbox(&mut self.config_panel.visible, "配置");
|
|
||||||
ui.checkbox(&mut self.stats_panel.visible, "统计");
|
// Paint background
|
||||||
});
|
ui.painter().rect_filled(
|
||||||
});
|
title_bar_rect,
|
||||||
|
egui::CornerRadius::ZERO,
|
||||||
|
ENGINEERING_DARK.panel_deep,
|
||||||
|
);
|
||||||
|
ui.painter().line_segment(
|
||||||
|
[title_bar_rect.left_bottom(), title_bar_rect.right_bottom()],
|
||||||
|
egui::Stroke::new(1.0, ENGINEERING_DARK.border),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Drag-to-move: double-click to maximize, drag to move
|
||||||
|
let title_bar_response = ui.interact(
|
||||||
|
title_bar_rect,
|
||||||
|
egui::Id::new("title_bar"),
|
||||||
|
egui::Sense::click_and_drag(),
|
||||||
|
);
|
||||||
|
if title_bar_response.drag_started() {
|
||||||
|
ui.ctx().send_viewport_cmd(egui::ViewportCommand::StartDrag);
|
||||||
|
}
|
||||||
|
if title_bar_response.double_clicked() {
|
||||||
|
let is_maximized = ui.input(|i| i.viewport().maximized.unwrap_or(false));
|
||||||
|
ui.ctx()
|
||||||
|
.send_viewport_cmd(egui::ViewportCommand::Maximized(!is_maximized));
|
||||||
|
}
|
||||||
|
|
||||||
|
// macOS traffic light buttons (close / minimize / maximize)
|
||||||
|
let btn_size = 14.0;
|
||||||
|
let btn_spacing = 8.0;
|
||||||
|
let btns_start = title_bar_rect.left_center() + egui::vec2(12.0, 0.0);
|
||||||
|
|
||||||
|
let btn_close_center = btns_start;
|
||||||
|
let btn_min_center = btns_start + egui::vec2(btn_size + btn_spacing, 0.0);
|
||||||
|
let btn_max_center = btns_start + egui::vec2((btn_size + btn_spacing) * 2.0, 0.0);
|
||||||
|
|
||||||
|
// Close (red)
|
||||||
|
let close_rect =
|
||||||
|
egui::Rect::from_center_size(btn_close_center, egui::vec2(btn_size, btn_size));
|
||||||
|
let close_resp = ui.interact(close_rect, egui::Id::new("btn_close"), egui::Sense::click());
|
||||||
|
let close_color = egui::Color32::from_rgb(255, 95, 86);
|
||||||
|
ui.painter()
|
||||||
|
.circle_filled(btn_close_center, btn_size / 2.0, close_color);
|
||||||
|
// Draw × when hovered
|
||||||
|
if close_resp.hovered() {
|
||||||
|
ui.painter().line_segment(
|
||||||
|
[
|
||||||
|
btn_close_center + egui::vec2(-3.0, -3.0),
|
||||||
|
btn_close_center + egui::vec2(3.0, 3.0),
|
||||||
|
],
|
||||||
|
egui::Stroke::new(1.5, egui::Color32::from_rgb(80, 0, 0)),
|
||||||
|
);
|
||||||
|
ui.painter().line_segment(
|
||||||
|
[
|
||||||
|
btn_close_center + egui::vec2(3.0, -3.0),
|
||||||
|
btn_close_center + egui::vec2(-3.0, 3.0),
|
||||||
|
],
|
||||||
|
egui::Stroke::new(1.5, egui::Color32::from_rgb(80, 0, 0)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if close_resp.clicked() {
|
||||||
|
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minimize (yellow)
|
||||||
|
let min_rect = egui::Rect::from_center_size(btn_min_center, egui::vec2(btn_size, btn_size));
|
||||||
|
let min_resp = ui.interact(min_rect, egui::Id::new("btn_min"), egui::Sense::click());
|
||||||
|
let min_color = egui::Color32::from_rgb(255, 189, 46);
|
||||||
|
ui.painter()
|
||||||
|
.circle_filled(btn_min_center, btn_size / 2.0, min_color);
|
||||||
|
if min_resp.hovered() {
|
||||||
|
ui.painter().line_segment(
|
||||||
|
[
|
||||||
|
btn_min_center + egui::vec2(-3.0, 0.0),
|
||||||
|
btn_min_center + egui::vec2(3.0, 0.0),
|
||||||
|
],
|
||||||
|
egui::Stroke::new(1.5, egui::Color32::from_rgb(120, 80, 0)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if min_resp.clicked() {
|
||||||
|
ui.ctx()
|
||||||
|
.send_viewport_cmd(egui::ViewportCommand::Minimized(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maximize (green)
|
||||||
|
let max_rect = egui::Rect::from_center_size(btn_max_center, egui::vec2(btn_size, btn_size));
|
||||||
|
let max_resp = ui.interact(max_rect, egui::Id::new("btn_max"), egui::Sense::click());
|
||||||
|
let max_color = egui::Color32::from_rgb(39, 201, 63);
|
||||||
|
ui.painter()
|
||||||
|
.circle_filled(btn_max_center, btn_size / 2.0, max_color);
|
||||||
|
if max_resp.hovered() {
|
||||||
|
let s = 3.0;
|
||||||
|
ui.painter().rect_stroke(
|
||||||
|
egui::Rect::from_center_size(btn_max_center, egui::vec2(s * 2.0, s * 2.0)),
|
||||||
|
egui::CornerRadius::same(1),
|
||||||
|
egui::Stroke::new(1.5, egui::Color32::from_rgb(0, 80, 10)),
|
||||||
|
egui::StrokeKind::Outside,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if max_resp.clicked() {
|
||||||
|
let is_maximized = ui.input(|i| i.viewport().maximized.unwrap_or(false));
|
||||||
|
ui.ctx()
|
||||||
|
.send_viewport_cmd(egui::ViewportCommand::Maximized(!is_maximized));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_floating_panels(&mut self, ctx: &egui::Context) {
|
fn draw_floating_panels(&mut self, ctx: &egui::Context) {
|
||||||
@@ -133,18 +235,11 @@ fn log_pressure_sample(raw: &[u32], rows: u32, cols: u32) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_pressure_sample(
|
fn normalize_pressure_sample(raw: &[u32], rows: u32, cols: u32, normalized: &mut PressureFrame) {
|
||||||
raw: &[u32],
|
const RANGE_MIN: f32 = 0.0;
|
||||||
rows: u32,
|
const RANGE_MAX: f32 = 7000.0;
|
||||||
cols: u32,
|
|
||||||
normalized: &mut [f32; PRESSURE_CELL_COUNT],
|
|
||||||
) {
|
|
||||||
normalized.fill(0.0);
|
|
||||||
let max_value = raw.iter().copied().max().unwrap_or(0);
|
|
||||||
|
|
||||||
if max_value == 0 {
|
normalized.fill([0.0, 0.0]);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let src_cols = cols.max(1);
|
let src_cols = cols.max(1);
|
||||||
let copy_rows = MATRIX_ROWS.min(rows);
|
let copy_rows = MATRIX_ROWS.min(rows);
|
||||||
@@ -155,18 +250,25 @@ fn normalize_pressure_sample(
|
|||||||
let src_index = (row * src_cols + col) as usize;
|
let src_index = (row * src_cols + col) as usize;
|
||||||
let dst_index = (row * MATRIX_COLS + col) as usize;
|
let dst_index = (row * MATRIX_COLS + col) as usize;
|
||||||
if let Some(value) = raw.get(src_index) {
|
if let Some(value) = raw.get(src_index) {
|
||||||
normalized[dst_index] = (*value as f32 / max_value as f32).clamp(0.0, 1.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)
|
||||||
|
};
|
||||||
|
normalized[dst_index] = [mapped, display_value];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl eframe::App for EskinDesktopApp {
|
impl eframe::App for EskinDesktopApp {
|
||||||
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
|
||||||
let ctx = ui.ctx().clone();
|
let ctx = ui.ctx().clone();
|
||||||
|
|
||||||
self.draw_wgpu_background(ui);
|
self.draw_wgpu_background(ui);
|
||||||
self.draw_toolbar(ui);
|
self.draw_title_bar(ui, frame);
|
||||||
self.draw_floating_panels(&ctx);
|
self.draw_floating_panels(&ctx);
|
||||||
|
|
||||||
// Keep repainting while the wgpu background is a realtime viewport.
|
// Keep repainting while the wgpu background is a realtime viewport.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use crossbeam_channel::{self, Receiver, Sender, TryRecvError};
|
use crossbeam_channel::{self, Receiver, Sender, TryRecvError};
|
||||||
|
|
||||||
use crate::serial_core::serial::{run_serial_loop, SerialPortReadWrite};
|
use crate::serial_core::serial::{SerialPortReadWrite, run_serial_loop};
|
||||||
|
|
||||||
/// Connection state visible to the UI.
|
/// Connection state visible to the UI.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -115,11 +115,7 @@ impl ConnectionManager {
|
|||||||
let rows = 12u32;
|
let rows = 12u32;
|
||||||
let cols = 7u32;
|
let cols = 7u32;
|
||||||
let matrix = vals.iter().map(|v| (*v).max(0) as u32).collect();
|
let matrix = vals.iter().map(|v| (*v).max(0) as u32).collect();
|
||||||
last = Some(PressureSample {
|
last = Some(PressureSample { matrix, rows, cols });
|
||||||
matrix,
|
|
||||||
rows,
|
|
||||||
cols,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
Err(TryRecvError::Empty) => break,
|
Err(TryRecvError::Empty) => break,
|
||||||
Err(TryRecvError::Disconnected) => break,
|
Err(TryRecvError::Disconnected) => break,
|
||||||
@@ -174,11 +170,7 @@ fn run_device_loop(
|
|||||||
Ok(vals) => {
|
Ok(vals) => {
|
||||||
// Store latest
|
// Store latest
|
||||||
let matrix = vals.iter().map(|v| (*v).max(0) as u32).collect();
|
let matrix = vals.iter().map(|v| (*v).max(0) as u32).collect();
|
||||||
*latest.lock().unwrap() = Some(PressureSample {
|
*latest.lock().unwrap() = Some(PressureSample { matrix, rows, cols });
|
||||||
matrix,
|
|
||||||
rows,
|
|
||||||
cols,
|
|
||||||
});
|
|
||||||
// Forward
|
// Forward
|
||||||
let _ = outer_tx.try_send(vals);
|
let _ = outer_tx.try_send(vals);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ fn main() -> eframe::Result<()> {
|
|||||||
renderer: eframe::Renderer::Wgpu,
|
renderer: eframe::Renderer::Wgpu,
|
||||||
viewport: egui::ViewportBuilder::default()
|
viewport: egui::ViewportBuilder::default()
|
||||||
.with_inner_size([1920.0, 1080.0])
|
.with_inner_size([1920.0, 1080.0])
|
||||||
.with_min_inner_size([1280.0, 720.0]),
|
.with_min_inner_size([1280.0, 720.0])
|
||||||
|
.with_decorations(false),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ const MIN_BOARD_PADDING: f32 = 2.6;
|
|||||||
const MAX_BOARD_PADDING: f32 = 6.8;
|
const MAX_BOARD_PADDING: f32 = 6.8;
|
||||||
const MATRIX_OFFSET_Y: f32 = -2.4;
|
const MATRIX_OFFSET_Y: f32 = -2.4;
|
||||||
const MATRIX_OFFSET_Z: f32 = 12.0;
|
const MATRIX_OFFSET_Z: f32 = 12.0;
|
||||||
const HEIGHT_SCALE: f32 = 10.6;
|
|
||||||
const BASE_HEIGHT: f32 = 0.12;
|
|
||||||
const CAMERA_FOV: f32 = 36.0;
|
const CAMERA_FOV: f32 = 36.0;
|
||||||
const CAMERA_DISTANCE_MIN: f32 = 30.0;
|
const CAMERA_DISTANCE_MIN: f32 = 30.0;
|
||||||
const CAMERA_DISTANCE_MAX: f32 = 122.0;
|
const CAMERA_DISTANCE_MAX: f32 = 122.0;
|
||||||
@@ -87,14 +85,13 @@ pub fn glyph_world_position(
|
|||||||
pressure: f32,
|
pressure: f32,
|
||||||
) -> ([f32; 4], f32) {
|
) -> ([f32; 4], f32) {
|
||||||
let normalized = pressure.clamp(0.0, 1.0);
|
let normalized = pressure.clamp(0.0, 1.0);
|
||||||
let height = BASE_HEIGHT + normalized.powf(0.9) * HEIGHT_SCALE;
|
|
||||||
let x = (col as f32 - cols as f32 / 2.0 + 0.5) * layout.cell_spacing;
|
let x = (col as f32 - cols as f32 / 2.0 + 0.5) * layout.cell_spacing;
|
||||||
let z = (row as f32 - rows as f32 / 2.0 + 0.5) * layout.cell_spacing;
|
let z = (row as f32 - rows as f32 / 2.0 + 0.5) * layout.cell_spacing;
|
||||||
|
|
||||||
(
|
(
|
||||||
[
|
[
|
||||||
x,
|
x,
|
||||||
MATRIX_OFFSET_Y + height + layout.label_float_offset,
|
MATRIX_OFFSET_Y + layout.label_float_offset,
|
||||||
MATRIX_OFFSET_Z + z,
|
MATRIX_OFFSET_Z + z,
|
||||||
1.0,
|
1.0,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ use crate::matrix::{MatrixLayout, build_view_projection, glyph_world_position};
|
|||||||
|
|
||||||
pub const PRESSURE_CELL_COUNT: usize =
|
pub const PRESSURE_CELL_COUNT: usize =
|
||||||
(crate::matrix::MATRIX_ROWS * crate::matrix::MATRIX_COLS) as usize;
|
(crate::matrix::MATRIX_ROWS * crate::matrix::MATRIX_COLS) as usize;
|
||||||
|
pub type PressureFrame = [[f32; 2]; PRESSURE_CELL_COUNT];
|
||||||
|
|
||||||
pub struct WgpuBackgroundCallback {
|
pub struct WgpuBackgroundCallback {
|
||||||
pub width: f32,
|
pub width: f32,
|
||||||
pub height: f32,
|
pub height: f32,
|
||||||
pub pressure: [f32; PRESSURE_CELL_COUNT],
|
pub pressure: PressureFrame,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl egui_wgpu::CallbackTrait for WgpuBackgroundCallback {
|
impl egui_wgpu::CallbackTrait for WgpuBackgroundCallback {
|
||||||
@@ -126,7 +127,8 @@ impl BackgroundRenderResources {
|
|||||||
usage: wgpu::BufferUsages::VERTEX,
|
usage: wgpu::BufferUsages::VERTEX,
|
||||||
});
|
});
|
||||||
|
|
||||||
let glyph_instances = build_glyph_instances(rows, cols, &layout, &[]);
|
let glyph_instances =
|
||||||
|
build_glyph_instances(rows, cols, &layout, &[[0.0, 0.0]; PRESSURE_CELL_COUNT]);
|
||||||
let glyph_instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
let glyph_instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||||
label: Some("Pressure Glyph Instance Buffer"),
|
label: Some("Pressure Glyph Instance Buffer"),
|
||||||
contents: bytemuck::cast_slice(&glyph_instances),
|
contents: bytemuck::cast_slice(&glyph_instances),
|
||||||
@@ -148,7 +150,7 @@ impl BackgroundRenderResources {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare(&mut self, queue: &wgpu::Queue, width: f32, height: f32, pressure: &[f32]) {
|
fn prepare(&mut self, queue: &wgpu::Queue, width: f32, height: f32, pressure: &PressureFrame) {
|
||||||
let aspect = width / height.max(1.0);
|
let aspect = width / height.max(1.0);
|
||||||
self.uniform =
|
self.uniform =
|
||||||
MatrixUniform::new(width, height, build_view_projection(aspect, &self.layout));
|
MatrixUniform::new(width, height, build_view_projection(aspect, &self.layout));
|
||||||
@@ -255,19 +257,19 @@ fn build_glyph_instances(
|
|||||||
rows: u32,
|
rows: u32,
|
||||||
cols: u32,
|
cols: u32,
|
||||||
layout: &MatrixLayout,
|
layout: &MatrixLayout,
|
||||||
pressure: &[f32],
|
pressure: &PressureFrame,
|
||||||
) -> Vec<GlyphInstance> {
|
) -> Vec<GlyphInstance> {
|
||||||
let mut instances = Vec::with_capacity((rows * cols) as usize);
|
let mut instances = Vec::with_capacity((rows * cols) as usize);
|
||||||
|
|
||||||
for row in 0..rows {
|
for row in 0..rows {
|
||||||
for col in 0..cols {
|
for col in 0..cols {
|
||||||
let index = (row * cols + col) as usize;
|
let index = (row * cols + col) as usize;
|
||||||
let normalized = pressure.get(index).copied().unwrap_or(0.0);
|
let [normalized, display_value] = pressure.get(index).copied().unwrap_or([0.0, 0.0]);
|
||||||
let (world_position, normalized) =
|
let (world_position, normalized) =
|
||||||
glyph_world_position(row, col, rows, cols, layout, normalized);
|
glyph_world_position(row, col, rows, cols, layout, normalized);
|
||||||
instances.push(GlyphInstance {
|
instances.push(GlyphInstance {
|
||||||
world_position,
|
world_position,
|
||||||
style: [normalized, 0.0, 0.0, 0.0],
|
style: [normalized, display_value, 0.0, 0.0],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -280,17 +282,17 @@ fn update_glyph_instances(
|
|||||||
rows: u32,
|
rows: u32,
|
||||||
cols: u32,
|
cols: u32,
|
||||||
layout: &MatrixLayout,
|
layout: &MatrixLayout,
|
||||||
pressure: &[f32],
|
pressure: &PressureFrame,
|
||||||
) {
|
) {
|
||||||
for row in 0..rows {
|
for row in 0..rows {
|
||||||
for col in 0..cols {
|
for col in 0..cols {
|
||||||
let index = (row * cols + col) as usize;
|
let index = (row * cols + col) as usize;
|
||||||
let normalized = pressure.get(index).copied().unwrap_or(0.0);
|
let [normalized, display_value] = pressure.get(index).copied().unwrap_or([0.0, 0.0]);
|
||||||
let (world_position, normalized) =
|
let (world_position, normalized) =
|
||||||
glyph_world_position(row, col, rows, cols, layout, normalized);
|
glyph_world_position(row, col, rows, cols, layout, normalized);
|
||||||
if let Some(instance) = instances.get_mut(index) {
|
if let Some(instance) = instances.get_mut(index) {
|
||||||
instance.world_position = world_position;
|
instance.world_position = world_position;
|
||||||
instance.style = [normalized, 0.0, 0.0, 0.0];
|
instance.style = [normalized, display_value, 0.0, 0.0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,11 +30,7 @@ impl TactileACodec {
|
|||||||
.chunks_exact(2)
|
.chunks_exact(2)
|
||||||
.map(|chunk| {
|
.map(|chunk| {
|
||||||
let raw = u16::from_le_bytes([chunk[0], chunk[1]]) as i32;
|
let raw = u16::from_le_bytes([chunk[0], chunk[1]]) as i32;
|
||||||
if raw < 15 {
|
if raw < 15 { 0 } else { raw }
|
||||||
0
|
|
||||||
} else {
|
|
||||||
raw
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<i32>>();
|
.collect::<Vec<i32>>();
|
||||||
|
|
||||||
|
|||||||
165
src/shader.wgsl
165
src/shader.wgsl
@@ -25,6 +25,7 @@ struct GlyphVertexOutput {
|
|||||||
@builtin(position) clip_position: vec4f,
|
@builtin(position) clip_position: vec4f,
|
||||||
@location(0) local: vec2f,
|
@location(0) local: vec2f,
|
||||||
@location(1) intensity: f32,
|
@location(1) intensity: f32,
|
||||||
|
@location(2) display_value: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn saturate(value: f32) -> f32 {
|
fn saturate(value: f32) -> f32 {
|
||||||
@@ -43,25 +44,27 @@ fn rotate2(point: vec2f, angle: f32) -> vec2f {
|
|||||||
return vec2f(point.x * c - point.y * s, point.x * s + point.y * c);
|
return vec2f(point.x * c - point.y * s, point.x * s + point.y * c);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn rect_alpha(point: vec2f, center: vec2f, half_size: vec2f) -> f32 {
|
||||||
|
let delta = abs(point - center) - half_size;
|
||||||
|
let outside = length(max(delta, vec2f(0.0, 0.0)));
|
||||||
|
let inside = min(max(delta.x, delta.y), 0.0);
|
||||||
|
let dist = outside + inside;
|
||||||
|
return 1.0 - smoothstep(0.015, 0.045, dist);
|
||||||
|
}
|
||||||
|
|
||||||
fn range_stop_color(index: u32) -> vec3f {
|
fn range_stop_color(index: u32) -> vec3f {
|
||||||
switch index {
|
switch index {
|
||||||
case 0u: {
|
case 0u: {
|
||||||
return vec3f(0.263, 0.667, 0.420);
|
return vec3f(0.140, 0.690, 0.890);
|
||||||
}
|
}
|
||||||
case 1u: {
|
case 1u: {
|
||||||
return vec3f(0.094, 0.735, 0.875);
|
return vec3f(0.250, 0.760, 0.380);
|
||||||
}
|
}
|
||||||
case 2u: {
|
case 2u: {
|
||||||
return vec3f(0.298, 0.349, 0.957);
|
return vec3f(1.000, 0.670, 0.180);
|
||||||
}
|
|
||||||
case 3u: {
|
|
||||||
return vec3f(0.812, 0.294, 0.851);
|
|
||||||
}
|
|
||||||
case 4u: {
|
|
||||||
return vec3f(0.957, 0.337, 0.333);
|
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
return vec3f(1.000, 0.596, 0.204);
|
return vec3f(1.000, 0.255, 0.190);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,43 +72,115 @@ fn range_stop_color(index: u32) -> vec3f {
|
|||||||
fn sample_range_color(value: f32) -> vec3f {
|
fn sample_range_color(value: f32) -> vec3f {
|
||||||
let t = saturate(value);
|
let t = saturate(value);
|
||||||
|
|
||||||
if (t <= 0.25) {
|
if (t <= 0.33) {
|
||||||
let local = smoothstep(0.0, 0.25, t);
|
let local = smoothstep(0.0, 0.33, t);
|
||||||
return mix(range_stop_color(0u), range_stop_color(1u), local);
|
return mix(range_stop_color(0u), range_stop_color(1u), local);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (t <= 0.5) {
|
if (t <= 0.66) {
|
||||||
let local = smoothstep(0.25, 0.5, t);
|
let local = smoothstep(0.33, 0.66, t);
|
||||||
return mix(range_stop_color(1u), range_stop_color(2u), local);
|
return mix(range_stop_color(1u), range_stop_color(2u), local);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (t <= 0.75) {
|
let local = smoothstep(0.66, 1.0, t);
|
||||||
let local = smoothstep(0.5, 0.75, t);
|
return mix(range_stop_color(2u), range_stop_color(3u), local);
|
||||||
return mix(range_stop_color(2u), range_stop_color(3u), local);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (t <= 0.875) {
|
|
||||||
let local = smoothstep(0.75, 0.875, t);
|
|
||||||
return mix(range_stop_color(3u), range_stop_color(4u), local);
|
|
||||||
}
|
|
||||||
|
|
||||||
let local = smoothstep(0.875, 1.0, t);
|
|
||||||
return mix(range_stop_color(4u), range_stop_color(5u), local);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn digit_zero_alpha(local: vec2f) -> f32 {
|
fn digit_segment_on(digit: u32, segment: u32) -> bool {
|
||||||
let tilted = rotate2(local, -0.18);
|
switch digit {
|
||||||
let oval = vec2f(tilted.x / 0.48, tilted.y / 0.75);
|
case 0u: { return segment != 6u; }
|
||||||
let aa = 0.030;
|
case 1u: { return segment == 1u || segment == 2u; }
|
||||||
let outer = 1.0 - smoothstep(0.93 - aa, 0.93 + aa, length(oval));
|
case 2u: { return segment == 0u || segment == 1u || segment == 6u || segment == 4u || segment == 3u; }
|
||||||
let inner = 1.0 - smoothstep(0.57 - aa, 0.57 + aa, length(oval));
|
case 3u: { return segment == 0u || segment == 1u || segment == 6u || segment == 2u || segment == 3u; }
|
||||||
let ring = saturate(outer - inner);
|
case 4u: { return segment == 5u || segment == 6u || segment == 1u || segment == 2u; }
|
||||||
let slash = 1.0 - smoothstep(
|
case 5u: { return segment == 0u || segment == 5u || segment == 6u || segment == 2u || segment == 3u; }
|
||||||
0.036,
|
case 6u: { return segment == 0u || segment == 5u || segment == 4u || segment == 3u || segment == 2u || segment == 6u; }
|
||||||
0.066,
|
case 7u: { return segment == 0u || segment == 1u || segment == 2u; }
|
||||||
capsule_distance(tilted, vec2f(-0.16, 0.43), vec2f(0.18, -0.43)),
|
case 8u: { return true; }
|
||||||
);
|
default: { return segment == 0u || segment == 1u || segment == 2u || segment == 3u || segment == 5u || segment == 6u; }
|
||||||
return max(ring, slash * 0.88);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn seven_segment_digit_alpha(local: vec2f, digit: u32) -> f32 {
|
||||||
|
var alpha = 0.0;
|
||||||
|
if (digit_segment_on(digit, 0u)) {
|
||||||
|
alpha = max(alpha, rect_alpha(local, vec2f(0.0, 0.70), vec2f(0.38, 0.078)));
|
||||||
|
}
|
||||||
|
if (digit_segment_on(digit, 1u)) {
|
||||||
|
alpha = max(alpha, rect_alpha(local, vec2f(0.39, 0.36), vec2f(0.078, 0.335)));
|
||||||
|
}
|
||||||
|
if (digit_segment_on(digit, 2u)) {
|
||||||
|
alpha = max(alpha, rect_alpha(local, vec2f(0.39, -0.36), vec2f(0.078, 0.335)));
|
||||||
|
}
|
||||||
|
if (digit_segment_on(digit, 3u)) {
|
||||||
|
alpha = max(alpha, rect_alpha(local, vec2f(0.0, -0.70), vec2f(0.38, 0.078)));
|
||||||
|
}
|
||||||
|
if (digit_segment_on(digit, 4u)) {
|
||||||
|
alpha = max(alpha, rect_alpha(local, vec2f(-0.39, -0.36), vec2f(0.078, 0.335)));
|
||||||
|
}
|
||||||
|
if (digit_segment_on(digit, 5u)) {
|
||||||
|
alpha = max(alpha, rect_alpha(local, vec2f(-0.39, 0.36), vec2f(0.078, 0.335)));
|
||||||
|
}
|
||||||
|
if (digit_segment_on(digit, 6u)) {
|
||||||
|
alpha = max(alpha, rect_alpha(local, vec2f(0.0, 0.0), vec2f(0.35, 0.075)));
|
||||||
|
}
|
||||||
|
return alpha;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn digit_count(value: u32) -> u32 {
|
||||||
|
if (value >= 1000u) {
|
||||||
|
return 4u;
|
||||||
|
}
|
||||||
|
if (value >= 100u) {
|
||||||
|
return 3u;
|
||||||
|
}
|
||||||
|
if (value >= 10u) {
|
||||||
|
return 2u;
|
||||||
|
}
|
||||||
|
return 1u;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn digit_at(value: u32, slot: u32, count: u32) -> u32 {
|
||||||
|
if (count == 4u) {
|
||||||
|
switch slot {
|
||||||
|
case 0u: { return (value / 1000u) % 10u; }
|
||||||
|
case 1u: { return (value / 100u) % 10u; }
|
||||||
|
case 2u: { return (value / 10u) % 10u; }
|
||||||
|
default: { return value % 10u; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (count == 3u) {
|
||||||
|
switch slot {
|
||||||
|
case 0u: { return (value / 100u) % 10u; }
|
||||||
|
case 1u: { return (value / 10u) % 10u; }
|
||||||
|
default: { return value % 10u; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (count == 2u) {
|
||||||
|
return select(value % 10u, (value / 10u) % 10u, slot == 0u);
|
||||||
|
}
|
||||||
|
return value % 10u;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn number_alpha(local: vec2f, display_value: f32) -> f32 {
|
||||||
|
let value = min(u32(max(display_value + 0.5, 0.0)), 9999u);
|
||||||
|
let count = digit_count(value);
|
||||||
|
let count_f = f32(count);
|
||||||
|
let slot_width = 1.74 / count_f;
|
||||||
|
let start_x = -slot_width * (count_f - 1.0) * 0.5;
|
||||||
|
var alpha = 0.0;
|
||||||
|
|
||||||
|
for (var slot = 0u; slot < 4u; slot = slot + 1u) {
|
||||||
|
if (slot < count) {
|
||||||
|
let center_x = start_x + f32(slot) * slot_width;
|
||||||
|
let digit_local = vec2f((local.x - center_x) / (slot_width * 0.78), local.y / 0.92);
|
||||||
|
let digit = digit_at(value, slot, count);
|
||||||
|
let in_slot = step(abs(local.x - center_x), slot_width * 0.48);
|
||||||
|
alpha = max(alpha, seven_segment_digit_alpha(digit_local, digit) * in_slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return alpha;
|
||||||
}
|
}
|
||||||
|
|
||||||
@vertex
|
@vertex
|
||||||
@@ -149,8 +224,8 @@ fn fs_background(@builtin(position) frag_coord: vec4f) -> @location(0) vec4f {
|
|||||||
|
|
||||||
let tick_area = step(abs(local.x), half_size.x) * step(abs(local.y), half_size.y + 8.0);
|
let tick_area = step(abs(local.x), half_size.x) * step(abs(local.y), half_size.y + 8.0);
|
||||||
if (tick_area > 0.5) {
|
if (tick_area > 0.5) {
|
||||||
let ticks = array<f32, 6>(0.0, 0.25, 0.5, 0.75, 0.875, 1.0);
|
let ticks = array<f32, 4>(0.0, 0.33, 0.66, 1.0);
|
||||||
for (var index = 0u; index < 6u; index = index + 1u) {
|
for (var index = 0u; index < 4u; index = index + 1u) {
|
||||||
let tick_x = (ticks[index] - 0.5) * track_width;
|
let tick_x = (ticks[index] - 0.5) * track_width;
|
||||||
let tick = step(abs(local.x - tick_x), 1.0) * step(abs(local.y), half_size.y + 7.0);
|
let tick = step(abs(local.x - tick_x), 1.0) * step(abs(local.y), half_size.y + 7.0);
|
||||||
color = max(color, vec3f(0.78, 0.84, 0.90) * tick);
|
color = max(color, vec3f(0.78, 0.84, 0.90) * tick);
|
||||||
@@ -163,19 +238,21 @@ fn fs_background(@builtin(position) frag_coord: vec4f) -> @location(0) vec4f {
|
|||||||
@vertex
|
@vertex
|
||||||
fn vs_glyph(vertex: GlyphVertexInput, instance: GlyphInstanceInput) -> GlyphVertexOutput {
|
fn vs_glyph(vertex: GlyphVertexInput, instance: GlyphInstanceInput) -> GlyphVertexOutput {
|
||||||
let center = u.view_proj * vec4f(instance.world_position.xyz, 1.0);
|
let center = u.view_proj * vec4f(instance.world_position.xyz, 1.0);
|
||||||
let pixel_size = u.glyph.x * mix(0.92, 1.10, saturate(instance.style.x));
|
let shaped = pow(saturate(instance.style.x), 0.9);
|
||||||
|
let pixel_size = u.glyph.x * mix(1.08, 2.20, shaped);
|
||||||
let ndc_offset = vertex.local * vec2f(pixel_size / u.viewport.x, pixel_size / u.viewport.y) * 2.0;
|
let ndc_offset = vertex.local * vec2f(pixel_size / u.viewport.x, pixel_size / u.viewport.y) * 2.0;
|
||||||
|
|
||||||
var out: GlyphVertexOutput;
|
var out: GlyphVertexOutput;
|
||||||
out.clip_position = vec4f(center.xy + ndc_offset * center.w, center.z, center.w);
|
out.clip_position = vec4f(center.xy + ndc_offset * center.w, center.z, center.w);
|
||||||
out.local = vertex.local;
|
out.local = vertex.local;
|
||||||
out.intensity = instance.style.x;
|
out.intensity = instance.style.x;
|
||||||
|
out.display_value = instance.style.y;
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@fragment
|
@fragment
|
||||||
fn fs_glyph(in: GlyphVertexOutput) -> @location(0) vec4f {
|
fn fs_glyph(in: GlyphVertexOutput) -> @location(0) vec4f {
|
||||||
let alpha = digit_zero_alpha(in.local);
|
let alpha = number_alpha(in.local, in.display_value);
|
||||||
let color = sample_range_color(in.intensity) * 1.08;
|
let color = sample_range_color(in.intensity) * mix(0.82, 1.16, saturate(in.intensity));
|
||||||
return vec4f(color, alpha);
|
return vec4f(color, alpha);
|
||||||
}
|
}
|
||||||
|
|||||||
306
src/ui.rs
306
src/ui.rs
@@ -10,6 +10,9 @@ pub struct FloatingPanelState {
|
|||||||
pub visible: bool,
|
pub visible: bool,
|
||||||
default_pos: egui::Pos2,
|
default_pos: egui::Pos2,
|
||||||
tag_pos: egui::Pos2,
|
tag_pos: egui::Pos2,
|
||||||
|
center_anim: f32,
|
||||||
|
center_anim_target: bool,
|
||||||
|
center_anim_last_time: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ConfigPanelState {
|
pub struct ConfigPanelState {
|
||||||
@@ -63,6 +66,9 @@ impl FloatingPanelState {
|
|||||||
visible: true,
|
visible: true,
|
||||||
default_pos: egui::pos2(default_pos[0], default_pos[1]),
|
default_pos: egui::pos2(default_pos[0], default_pos[1]),
|
||||||
tag_pos: egui::pos2(tag_pos[0], tag_pos[1]),
|
tag_pos: egui::pos2(tag_pos[0], tag_pos[1]),
|
||||||
|
center_anim: 1.0,
|
||||||
|
center_anim_target: true,
|
||||||
|
center_anim_last_time: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,7 +137,7 @@ pub fn draw_connect_panel(
|
|||||||
ConnectionState::Connected | ConnectionState::Streaming
|
ConnectionState::Connected | ConnectionState::Streaming
|
||||||
);
|
);
|
||||||
|
|
||||||
draw_center_floating_panel(ctx, panel, "connect_center_panel", 64.0, |ui| {
|
draw_center_floating_panel(ctx, panel, "connect_center_panel", 42.0, "连接", |ui| {
|
||||||
ui.set_min_width(320.0);
|
ui.set_min_width(320.0);
|
||||||
|
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
@@ -654,18 +660,308 @@ fn draw_center_floating_panel(
|
|||||||
panel: &mut FloatingPanelState,
|
panel: &mut FloatingPanelState,
|
||||||
id: &'static str,
|
id: &'static str,
|
||||||
top_offset: f32,
|
top_offset: f32,
|
||||||
|
collapsed_label: &'static str,
|
||||||
add_contents: impl FnOnce(&mut egui::Ui),
|
add_contents: impl FnOnce(&mut egui::Ui),
|
||||||
) {
|
) {
|
||||||
if !panel.visible {
|
const PANEL_WIDTH: f32 = 430.0;
|
||||||
|
const SLIDE_DISTANCE: f32 = 18.0;
|
||||||
|
|
||||||
|
let anim = advance_center_panel_anim(ctx, panel);
|
||||||
|
let eased = ease_in_out(anim);
|
||||||
|
|
||||||
|
if !panel.visible || anim < 0.28 {
|
||||||
|
let handle_y = top_offset - (1.0 - eased) * 2.0;
|
||||||
|
egui::Area::new(egui::Id::new(format!("{id}_handle")))
|
||||||
|
.anchor(egui::Align2::CENTER_TOP, egui::vec2(0.0, handle_y))
|
||||||
|
.order(egui::Order::Tooltip)
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
let handle_rect = ui
|
||||||
|
.allocate_exact_size(egui::vec2(112.0, 24.0), egui::Sense::hover())
|
||||||
|
.0;
|
||||||
|
if integrated_handle(ui, handle_rect, collapsed_label, false, "打开连接面板")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
panel.visible = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if anim <= 0.02 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
egui::Area::new(egui::Id::new(id))
|
egui::Area::new(egui::Id::new(id))
|
||||||
.anchor(egui::Align2::CENTER_TOP, egui::vec2(0.0, top_offset))
|
.anchor(
|
||||||
.order(egui::Order::Foreground)
|
egui::Align2::CENTER_TOP,
|
||||||
|
egui::vec2(0.0, top_offset - (1.0 - eased) * SLIDE_DISTANCE),
|
||||||
|
)
|
||||||
|
.order(egui::Order::Tooltip)
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
panel_frame(ctx).show(ui, |ui| {
|
let response = center_panel_shell(ui, PANEL_WIDTH, |ui| {
|
||||||
|
ui.set_width(PANEL_WIDTH);
|
||||||
add_contents(ui);
|
add_contents(ui);
|
||||||
|
ui.add_space(6.0);
|
||||||
|
let handle_rect = allocate_center_panel_handle(ui);
|
||||||
|
if integrated_handle(ui, handle_rect, "", true, "收起连接面板").clicked() {
|
||||||
|
panel.visible = false;
|
||||||
|
}
|
||||||
|
handle_rect
|
||||||
});
|
});
|
||||||
|
paint_integrated_center_panel(ui, response.response.rect, response.inner, true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn advance_center_panel_anim(ctx: &egui::Context, panel: &mut FloatingPanelState) -> f32 {
|
||||||
|
let target = if panel.visible { 1.0 } else { 0.0 };
|
||||||
|
let now = ctx.input(|input| input.time);
|
||||||
|
let delta_time = panel
|
||||||
|
.center_anim_last_time
|
||||||
|
.map(|last_time| (now - last_time) as f32)
|
||||||
|
.unwrap_or(1.0 / 60.0)
|
||||||
|
.clamp(0.0, 0.05);
|
||||||
|
|
||||||
|
panel.center_anim_last_time = Some(now);
|
||||||
|
panel.center_anim_target = panel.visible;
|
||||||
|
|
||||||
|
let speed = 7.5;
|
||||||
|
let step = speed * delta_time;
|
||||||
|
if panel.center_anim < target {
|
||||||
|
panel.center_anim = (panel.center_anim + step).min(target);
|
||||||
|
} else if panel.center_anim > target {
|
||||||
|
panel.center_anim = (panel.center_anim - step).max(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (panel.center_anim - target).abs() > 0.001 {
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
panel.center_anim
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ease_in_out(t: f32) -> f32 {
|
||||||
|
let t = t.clamp(0.0, 1.0);
|
||||||
|
t * t * (3.0 - 2.0 * t)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn center_panel_shell<R>(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
width: f32,
|
||||||
|
add_contents: impl FnOnce(&mut egui::Ui) -> R,
|
||||||
|
) -> egui::InnerResponse<R> {
|
||||||
|
egui::Frame::new()
|
||||||
|
.fill(ENGINEERING_DARK.panel)
|
||||||
|
.corner_radius(egui::CornerRadius::same(ENGINEERING_DARK.radius))
|
||||||
|
.inner_margin(egui::Margin::symmetric(10, 8))
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.set_width(width);
|
||||||
|
add_contents(ui)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn allocate_center_panel_handle(ui: &mut egui::Ui) -> egui::Rect {
|
||||||
|
let handle_width = 112.0;
|
||||||
|
let handle_height = 24.0;
|
||||||
|
let left_space = ((ui.available_width() - handle_width) * 0.5).max(0.0);
|
||||||
|
let mut handle_rect = egui::Rect::NOTHING;
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.add_space(left_space);
|
||||||
|
let (rect, _) = ui.allocate_exact_size(
|
||||||
|
egui::vec2(handle_width, handle_height),
|
||||||
|
egui::Sense::hover(),
|
||||||
|
);
|
||||||
|
handle_rect = rect;
|
||||||
|
});
|
||||||
|
|
||||||
|
handle_rect
|
||||||
|
}
|
||||||
|
|
||||||
|
fn integrated_handle(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
rect: egui::Rect,
|
||||||
|
label: &str,
|
||||||
|
expanded: bool,
|
||||||
|
tooltip: &'static str,
|
||||||
|
) -> egui::Response {
|
||||||
|
let response = ui
|
||||||
|
.interact(
|
||||||
|
rect.expand2(egui::vec2(12.0, 5.0)),
|
||||||
|
ui.id().with(("center_panel_handle", expanded)),
|
||||||
|
egui::Sense::click(),
|
||||||
|
)
|
||||||
|
.on_hover_text(tooltip);
|
||||||
|
|
||||||
|
paint_integrated_handle(ui, rect, label, expanded, response.hovered());
|
||||||
|
response
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint_integrated_handle(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
rect: egui::Rect,
|
||||||
|
label: &str,
|
||||||
|
expanded: bool,
|
||||||
|
hovered: bool,
|
||||||
|
) {
|
||||||
|
let center = rect.center();
|
||||||
|
if !expanded {
|
||||||
|
let fill = if hovered {
|
||||||
|
ENGINEERING_DARK.panel_strong
|
||||||
|
} else {
|
||||||
|
egui::Color32::from_rgb(31, 41, 52)
|
||||||
|
};
|
||||||
|
let points = vec![
|
||||||
|
egui::pos2(rect.left() - 12.0, rect.top() - 1.0),
|
||||||
|
egui::pos2(rect.right() + 12.0, rect.top() - 1.0),
|
||||||
|
egui::pos2(rect.right() + 3.0, rect.bottom() + 8.0),
|
||||||
|
egui::pos2(rect.left() - 3.0, rect.bottom() + 8.0),
|
||||||
|
];
|
||||||
|
ui.painter().add(egui::Shape::convex_polygon(
|
||||||
|
points.clone(),
|
||||||
|
fill,
|
||||||
|
egui::Stroke::new(1.0, ENGINEERING_DARK.border),
|
||||||
|
));
|
||||||
|
ui.painter().line_segment(
|
||||||
|
[points[2], points[3]],
|
||||||
|
egui::Stroke::new(1.0, ENGINEERING_DARK.border_soft),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let arrow = if expanded { -1.0 } else { 1.0 };
|
||||||
|
let arrow_center_x = if label.is_empty() {
|
||||||
|
center.x
|
||||||
|
} else {
|
||||||
|
rect.right() - 18.0
|
||||||
|
};
|
||||||
|
let arrow_center = egui::pos2(arrow_center_x, center.y);
|
||||||
|
let arrow_color = if hovered {
|
||||||
|
ENGINEERING_DARK.accent_hot
|
||||||
|
} else {
|
||||||
|
ENGINEERING_DARK.text_dim
|
||||||
|
};
|
||||||
|
ui.painter().line_segment(
|
||||||
|
[
|
||||||
|
arrow_center + egui::vec2(-6.0, -2.0 * arrow),
|
||||||
|
arrow_center + egui::vec2(0.0, 4.0 * arrow),
|
||||||
|
],
|
||||||
|
egui::Stroke::new(1.7, arrow_color),
|
||||||
|
);
|
||||||
|
ui.painter().line_segment(
|
||||||
|
[
|
||||||
|
arrow_center + egui::vec2(6.0, -2.0 * arrow),
|
||||||
|
arrow_center + egui::vec2(0.0, 4.0 * arrow),
|
||||||
|
],
|
||||||
|
egui::Stroke::new(1.7, arrow_color),
|
||||||
|
);
|
||||||
|
|
||||||
|
if !label.is_empty() {
|
||||||
|
let galley = ui.fonts_mut(|fonts| {
|
||||||
|
fonts.layout_no_wrap(
|
||||||
|
label.to_owned(),
|
||||||
|
egui::FontId::proportional(12.0),
|
||||||
|
ENGINEERING_DARK.text,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
ui.painter().galley(
|
||||||
|
egui::pos2(rect.left() + 14.0, center.y - galley.size().y * 0.5 - 1.0),
|
||||||
|
galley,
|
||||||
|
ENGINEERING_DARK.text,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint_integrated_center_panel(
|
||||||
|
ui: &egui::Ui,
|
||||||
|
rect: egui::Rect,
|
||||||
|
handle_rect: egui::Rect,
|
||||||
|
handle_on_bottom: bool,
|
||||||
|
) {
|
||||||
|
let painter = ui.painter();
|
||||||
|
let stroke = egui::Stroke::new(1.0, ENGINEERING_DARK.border);
|
||||||
|
let accent_stroke = egui::Stroke::new(1.2, ENGINEERING_DARK.border_soft);
|
||||||
|
let top = rect.top();
|
||||||
|
let left = rect.left();
|
||||||
|
let right = rect.right();
|
||||||
|
let bottom = rect.bottom();
|
||||||
|
let radius = ENGINEERING_DARK.radius as f32;
|
||||||
|
let tab_left = handle_rect.left() - 18.0;
|
||||||
|
let tab_right = handle_rect.right() + 18.0;
|
||||||
|
let tab_recess = if handle_on_bottom {
|
||||||
|
handle_rect.top() - 2.0
|
||||||
|
} else {
|
||||||
|
handle_rect.bottom() + 2.0
|
||||||
|
};
|
||||||
|
|
||||||
|
if handle_on_bottom {
|
||||||
|
painter.line_segment(
|
||||||
|
[
|
||||||
|
egui::pos2(left + radius, top),
|
||||||
|
egui::pos2(right - radius, top),
|
||||||
|
],
|
||||||
|
stroke,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
painter.line_segment(
|
||||||
|
[egui::pos2(left + radius, top), egui::pos2(tab_left, top)],
|
||||||
|
stroke,
|
||||||
|
);
|
||||||
|
painter.line_segment(
|
||||||
|
[egui::pos2(tab_right, top), egui::pos2(right - radius, top)],
|
||||||
|
stroke,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
painter.line_segment(
|
||||||
|
[
|
||||||
|
egui::pos2(left, top + radius),
|
||||||
|
egui::pos2(left, bottom - radius),
|
||||||
|
],
|
||||||
|
stroke,
|
||||||
|
);
|
||||||
|
painter.line_segment(
|
||||||
|
[
|
||||||
|
egui::pos2(right, top + radius),
|
||||||
|
egui::pos2(right, bottom - radius),
|
||||||
|
],
|
||||||
|
stroke,
|
||||||
|
);
|
||||||
|
if handle_on_bottom {
|
||||||
|
painter.line_segment(
|
||||||
|
[
|
||||||
|
egui::pos2(left + radius, bottom),
|
||||||
|
egui::pos2(tab_left, bottom),
|
||||||
|
],
|
||||||
|
stroke,
|
||||||
|
);
|
||||||
|
painter.line_segment(
|
||||||
|
[
|
||||||
|
egui::pos2(tab_right, bottom),
|
||||||
|
egui::pos2(right - radius, bottom),
|
||||||
|
],
|
||||||
|
stroke,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
painter.line_segment(
|
||||||
|
[
|
||||||
|
egui::pos2(left + radius, bottom),
|
||||||
|
egui::pos2(right - radius, bottom),
|
||||||
|
],
|
||||||
|
stroke,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let tab_points = if handle_on_bottom {
|
||||||
|
vec![
|
||||||
|
egui::pos2(tab_left, bottom),
|
||||||
|
egui::pos2(handle_rect.left() - 8.0, tab_recess),
|
||||||
|
egui::pos2(handle_rect.right() + 8.0, tab_recess),
|
||||||
|
egui::pos2(tab_right, bottom),
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
vec![
|
||||||
|
egui::pos2(tab_left, top),
|
||||||
|
egui::pos2(handle_rect.left() - 8.0, tab_recess),
|
||||||
|
egui::pos2(handle_rect.right() + 8.0, tab_recess),
|
||||||
|
egui::pos2(tab_right, top),
|
||||||
|
]
|
||||||
|
};
|
||||||
|
painter.add(egui::Shape::line(tab_points, accent_stroke));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user