Refine UI panels and render mode flow
This commit is contained in:
866
render.md
Normal file
866
render.md
Normal file
@@ -0,0 +1,866 @@
|
|||||||
|
# 渲染流程梳理
|
||||||
|
|
||||||
|
这份文档拆解当前项目里的渲染链路,包括:
|
||||||
|
|
||||||
|
- 压力数据如何从 CPU 进入 GPU
|
||||||
|
- `egui_wgpu::CallbackTrait` 的 `prepare / paint` 两阶段
|
||||||
|
- `BackgroundRenderResources` 负责管理哪些 GPU 资源
|
||||||
|
- `shader.wgsl` 里的各个 shader entry point 分别做什么
|
||||||
|
- `Finger / Hand` 两种模式如何切换渲染路径
|
||||||
|
|
||||||
|
主要相关文件:
|
||||||
|
|
||||||
|
- `src/app.rs`
|
||||||
|
- `src/render.rs`
|
||||||
|
- `src/matrix.rs`
|
||||||
|
- `src/model.rs`
|
||||||
|
- `static/wgsl/shader.wgsl`
|
||||||
|
|
||||||
|
## 总览
|
||||||
|
|
||||||
|
渲染入口在 `EskinDesktopApp::ui`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
self.draw_wgpu_background(ui);
|
||||||
|
self.draw_panel_context_menu(ui);
|
||||||
|
self.draw_title_bar(ui, frame);
|
||||||
|
self.draw_floating_panels(&ctx);
|
||||||
|
```
|
||||||
|
|
||||||
|
顺序很重要:
|
||||||
|
|
||||||
|
1. `draw_wgpu_background` 先绘制整块 WGPU 背景和 3D 内容。
|
||||||
|
2. 右键菜单、标题栏、floating panel 后画,所以会盖在 WGPU 内容上。
|
||||||
|
3. `ctx.request_repaint()` 保证背景持续刷新。
|
||||||
|
|
||||||
|
真正的 WGPU 绘制不是直接在 `app.rs` 里执行,而是通过 egui 的 paint callback:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
ui.painter().add(egui_wgpu::Callback::new_paint_callback(
|
||||||
|
rect,
|
||||||
|
WgpuBackgroundCallback {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
pressure: self.pressure_matrix,
|
||||||
|
active_mode: self.active_mode.clone(),
|
||||||
|
},
|
||||||
|
));
|
||||||
|
```
|
||||||
|
|
||||||
|
可以理解成:`app.rs` 每帧把“当前要画什么”打包成 `WgpuBackgroundCallback`,然后交给 `egui_wgpu` 在合适的 GPU 阶段执行。
|
||||||
|
|
||||||
|
## 数据流
|
||||||
|
|
||||||
|
### 1. 串口数据进入 App
|
||||||
|
|
||||||
|
`update_pressure_matrix` 从 `ConnectionManager` 取最新数据:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
if let Some(sample) = self.connection.take_latest_sample() {
|
||||||
|
normalize_pressure_sample(
|
||||||
|
&sample.matrix,
|
||||||
|
sample.rows,
|
||||||
|
sample.cols,
|
||||||
|
&mut self.pressure_matrix,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
串口原始数据是一个扁平的一维矩阵,元素是整数压力值。
|
||||||
|
|
||||||
|
`normalize_pressure_sample` 会把它转成:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub type PressureFrame = [[f32; 2]; PRESSURE_CELL_COUNT];
|
||||||
|
```
|
||||||
|
|
||||||
|
每个 cell 有两个 `f32`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
[normalized_pressure, display_value]
|
||||||
|
```
|
||||||
|
|
||||||
|
含义:
|
||||||
|
|
||||||
|
| 值 | 作用 |
|
||||||
|
| --- | --- |
|
||||||
|
| `normalized_pressure` | `0.0..=1.0`,用于颜色、大小、亮度。 |
|
||||||
|
| `display_value` | 原始压力值的显示版本,用于数字模式。 |
|
||||||
|
|
||||||
|
当前归一化范围写死在 `app.rs`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
const RANGE_MIN: f32 = 0.0;
|
||||||
|
const RANGE_MAX: f32 = 7000.0;
|
||||||
|
```
|
||||||
|
|
||||||
|
这意味着虽然 `FingerMode / HandGatewayMode` 里已经有 `range` 字段,但实际颜色归一化还没有用到它。
|
||||||
|
|
||||||
|
### 2. App 创建 WGPU Callback
|
||||||
|
|
||||||
|
`draw_wgpu_background` 根据当前 viewport 创建 callback:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
WgpuBackgroundCallback {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
pressure: self.pressure_matrix,
|
||||||
|
active_mode: self.active_mode.clone(),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`WgpuBackgroundCallback` 保存的是“每帧变化的数据”:
|
||||||
|
|
||||||
|
- 当前视口宽度
|
||||||
|
- 当前视口高度
|
||||||
|
- 当前压力帧
|
||||||
|
- 当前渲染模式
|
||||||
|
|
||||||
|
它不保存长期 GPU 资源。长期资源在 `BackgroundRenderResources` 里。
|
||||||
|
|
||||||
|
### 3. Callback Prepare 阶段
|
||||||
|
|
||||||
|
`WgpuBackgroundCallback` 实现了:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
impl egui_wgpu::CallbackTrait for WgpuBackgroundCallback
|
||||||
|
```
|
||||||
|
|
||||||
|
`prepare` 阶段代码:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let resources: &mut BackgroundRenderResources = resources.get_mut().unwrap();
|
||||||
|
resources.prepare(queue, self.width, self.height, &self.pressure);
|
||||||
|
```
|
||||||
|
|
||||||
|
这个阶段主要做 CPU -> GPU 的数据更新:
|
||||||
|
|
||||||
|
1. 根据 viewport aspect ratio 重新计算相机矩阵。
|
||||||
|
2. 把 `MatrixUniform` 写入 `uniform_buffer`。
|
||||||
|
3. 根据当前 `PressureFrame` 更新所有 marker instance。
|
||||||
|
4. 把 `glyph_instances` 写入 `glyph_instance_buffer`。
|
||||||
|
|
||||||
|
这里不会真正 draw,只是准备 GPU buffer。
|
||||||
|
|
||||||
|
### 4. Callback Paint 阶段
|
||||||
|
|
||||||
|
`paint` 阶段代码:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let resources: &BackgroundRenderResources = resources.get().unwrap();
|
||||||
|
resources.paint(render_pass, &self.active_mode);
|
||||||
|
```
|
||||||
|
|
||||||
|
这个阶段才是真正向 `render_pass` 发 draw call。
|
||||||
|
|
||||||
|
`active_mode` 在这里传进去,决定后面画点阵、数字,还是画模型。
|
||||||
|
|
||||||
|
## GPU 资源管理
|
||||||
|
|
||||||
|
`BackgroundRenderResources` 在 `EskinDesktopApp::new` 里创建一次:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
renderer
|
||||||
|
.callback_resources
|
||||||
|
.insert(BackgroundRenderResources::new(
|
||||||
|
&wgpu_state.device,
|
||||||
|
&wgpu_state.queue,
|
||||||
|
&wgpu_state.target_format,
|
||||||
|
MATRIX_ROWS,
|
||||||
|
MATRIX_COLS,
|
||||||
|
));
|
||||||
|
```
|
||||||
|
|
||||||
|
它负责保存长期存在的 GPU 资源:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct BackgroundRenderResources {
|
||||||
|
layout: MatrixLayout,
|
||||||
|
rows: u32,
|
||||||
|
cols: u32,
|
||||||
|
uniform: MatrixUniform,
|
||||||
|
uniform_buffer: wgpu::Buffer,
|
||||||
|
uniform_bind_group: wgpu::BindGroup,
|
||||||
|
background_pipeline: wgpu::RenderPipeline,
|
||||||
|
glyph_pipeline: wgpu::RenderPipeline,
|
||||||
|
dot_pipeline: wgpu::RenderPipeline,
|
||||||
|
model_pipeline: wgpu::RenderPipeline,
|
||||||
|
model: Option<Model>,
|
||||||
|
model_instance_buffer: wgpu::Buffer,
|
||||||
|
glyph_vertex_buffer: wgpu::Buffer,
|
||||||
|
glyph_instance_buffer: wgpu::Buffer,
|
||||||
|
glyph_instances: Vec<GlyphInstance>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
字段含义:
|
||||||
|
|
||||||
|
| 字段 | 作用 |
|
||||||
|
| --- | --- |
|
||||||
|
| `layout` | 压力矩阵的世界空间布局。 |
|
||||||
|
| `rows`, `cols` | 当前 GPU 资源按多少行列创建。 |
|
||||||
|
| `uniform` | CPU 侧 uniform 缓存。 |
|
||||||
|
| `uniform_buffer` | GPU uniform buffer。 |
|
||||||
|
| `uniform_bind_group` | WGSL 里的 `@group(0)`。 |
|
||||||
|
| `background_pipeline` | 全屏背景、底部色条。 |
|
||||||
|
| `glyph_pipeline` | 数字/数值 marker。 |
|
||||||
|
| `dot_pipeline` | 圆点 marker。 |
|
||||||
|
| `model_pipeline` | OBJ 模型渲染。 |
|
||||||
|
| `model` | 加载好的模型和材质。 |
|
||||||
|
| `model_instance_buffer` | 模型实例矩阵。 |
|
||||||
|
| `glyph_vertex_buffer` | 一个 marker quad 的 6 个顶点。 |
|
||||||
|
| `glyph_instance_buffer` | 每个压力点一个 instance。 |
|
||||||
|
| `glyph_instances` | CPU 侧 instance 数组。 |
|
||||||
|
|
||||||
|
## 模式切换
|
||||||
|
|
||||||
|
当前运行时渲染模式:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub enum ActiveMode {
|
||||||
|
Finger(FingerMode),
|
||||||
|
Hand(HandGatewayMode),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`app.rs` 持有:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
active_mode: ActiveMode,
|
||||||
|
```
|
||||||
|
|
||||||
|
UI 层的 `SerialMode` 是用户选择,真正渲染用的是 `ActiveMode`。
|
||||||
|
|
||||||
|
切换逻辑在 `switch_mode`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn switch_mode(&mut self, next: SerialMode) {
|
||||||
|
self.connect_state.mode = next;
|
||||||
|
self.config_state.mode = next;
|
||||||
|
self.connection.disconnect();
|
||||||
|
self.pressure_matrix.fill([0.0, 0.0]);
|
||||||
|
self.data_log_frame = 0;
|
||||||
|
|
||||||
|
self.active_mode = match next {
|
||||||
|
SerialMode::Finger => ActiveMode::Finger(FingerMode {
|
||||||
|
rows: self.matrix_config.rows,
|
||||||
|
cols: self.matrix_config.cols,
|
||||||
|
range: 0..7000,
|
||||||
|
dot: true,
|
||||||
|
}),
|
||||||
|
SerialMode::Hand => ActiveMode::Hand(HandGatewayMode { range: 0..7000 }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这里做了几件事:
|
||||||
|
|
||||||
|
1. 同步 UI 状态。
|
||||||
|
2. 断开当前连接。
|
||||||
|
3. 清空压力帧。
|
||||||
|
4. 重置日志计数。
|
||||||
|
5. 设置新的 `active_mode`。
|
||||||
|
|
||||||
|
这比只改一个 enum 更像真正的互斥生命周期切换。
|
||||||
|
|
||||||
|
## 绘制分发
|
||||||
|
|
||||||
|
`BackgroundRenderResources::paint` 先画背景,再按模式分支:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
render_pass.set_pipeline(&self.background_pipeline);
|
||||||
|
render_pass.draw(0..3, 0..1);
|
||||||
|
|
||||||
|
match active_mode {
|
||||||
|
ActiveMode::Finger(mode) => self.paint_finger(render_pass, mode),
|
||||||
|
ActiveMode::Hand(mode) => self.paint_hand(render_pass, mode),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
当前模式行为:
|
||||||
|
|
||||||
|
| 模式 | 绘制内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| `Finger` | 背景 + 压力 marker。 |
|
||||||
|
| `Hand` | 背景 + 模型。 |
|
||||||
|
|
||||||
|
## Finger 模式
|
||||||
|
|
||||||
|
Finger 模式根据 `dot` 字段选择点模式或数字模式:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let marker_pipeline = if mode.dot {
|
||||||
|
&self.dot_pipeline
|
||||||
|
} else {
|
||||||
|
&self.glyph_pipeline
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
含义:
|
||||||
|
|
||||||
|
| `mode.dot` | pipeline | 效果 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `true` | `dot_pipeline` | 画圆点。 |
|
||||||
|
| `false` | `glyph_pipeline` | 画数字。 |
|
||||||
|
|
||||||
|
两个 pipeline 共用同一套 vertex buffer:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
render_pass.set_vertex_buffer(0, self.glyph_vertex_buffer.slice(..));
|
||||||
|
render_pass.set_vertex_buffer(1, self.glyph_instance_buffer.slice(..));
|
||||||
|
render_pass.draw(0..6, 0..draw_count);
|
||||||
|
```
|
||||||
|
|
||||||
|
解释:
|
||||||
|
|
||||||
|
- vertex buffer 0:一个 quad 的 6 个顶点。
|
||||||
|
- vertex buffer 1:每个压力点一个 instance。
|
||||||
|
- `draw(0..6, 0..draw_count)`:每个 instance 都复用这 6 个顶点。
|
||||||
|
|
||||||
|
## Hand 模式
|
||||||
|
|
||||||
|
Hand 模式当前只画模型:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
render_pass.set_pipeline(&self.model_pipeline);
|
||||||
|
render_pass.set_vertex_buffer(1, self.model_instance_buffer.slice(..));
|
||||||
|
```
|
||||||
|
|
||||||
|
然后遍历 mesh:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
for mesh in &model.meshes {
|
||||||
|
if let Some(material) = model.materials.get(mesh.material) {
|
||||||
|
render_pass.set_bind_group(1, &material.bind_group, &[]);
|
||||||
|
}
|
||||||
|
render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
|
||||||
|
render_pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
|
||||||
|
render_pass.draw_indexed(0..mesh.num_elements, 0, 0..1);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
绑定关系:
|
||||||
|
|
||||||
|
| Slot / Group | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| bind group 0 | `MatrixUniform` |
|
||||||
|
| bind group 1 | 模型纹理和 sampler |
|
||||||
|
| vertex buffer 0 | 模型顶点 |
|
||||||
|
| vertex buffer 1 | 模型实例矩阵 |
|
||||||
|
|
||||||
|
## Matrix 布局
|
||||||
|
|
||||||
|
`src/matrix.rs` 定义压力矩阵如何放到 3D 世界里。
|
||||||
|
|
||||||
|
固定默认尺寸:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub const MATRIX_ROWS: u32 = 12;
|
||||||
|
pub const MATRIX_COLS: u32 = 7;
|
||||||
|
```
|
||||||
|
|
||||||
|
`MatrixLayout::new(rows, cols)` 计算:
|
||||||
|
|
||||||
|
- cell 间距
|
||||||
|
- board 宽度
|
||||||
|
- board 深度
|
||||||
|
- board padding
|
||||||
|
- marker 垂直浮起高度
|
||||||
|
|
||||||
|
`glyph_world_position` 把 `(row, col)` 映射到世界坐标:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
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;
|
||||||
|
```
|
||||||
|
|
||||||
|
返回:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
[
|
||||||
|
x,
|
||||||
|
MATRIX_OFFSET_Y + layout.label_float_offset,
|
||||||
|
MATRIX_OFFSET_Z + z,
|
||||||
|
1.0,
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
所以 marker 是铺在 X/Z 平面上,Y 方向稍微浮起来。
|
||||||
|
|
||||||
|
`build_view_projection` 根据矩阵尺寸创建相机:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let view = glam::Mat4::look_at_rh(eye, target, glam::Vec3::Y);
|
||||||
|
let projection = glam::Mat4::perspective_rh(...);
|
||||||
|
let open_gl_to_wgpu = ...
|
||||||
|
```
|
||||||
|
|
||||||
|
最后的 `open_gl_to_wgpu` 是为了把 OpenGL 风格深度范围转换成 WGPU 需要的 clip space。
|
||||||
|
|
||||||
|
## Uniform
|
||||||
|
|
||||||
|
Rust 侧:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
|
struct MatrixUniform {
|
||||||
|
view_proj: [[f32; 4]; 4],
|
||||||
|
viewport: [f32; 4],
|
||||||
|
glyph: [f32; 4],
|
||||||
|
color: [f32; 4],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
WGSL 侧:
|
||||||
|
|
||||||
|
```wgsl
|
||||||
|
struct MatrixUniform {
|
||||||
|
view_proj: mat4x4f,
|
||||||
|
viewport: vec4f,
|
||||||
|
glyph: vec4f,
|
||||||
|
color: vec4f,
|
||||||
|
}
|
||||||
|
|
||||||
|
@group(0) @binding(0)
|
||||||
|
var<uniform> u: MatrixUniform;
|
||||||
|
```
|
||||||
|
|
||||||
|
字段含义:
|
||||||
|
|
||||||
|
| 字段 | 含义 |
|
||||||
|
| --- | --- |
|
||||||
|
| `view_proj` | 世界坐标到 clip space 的矩阵。 |
|
||||||
|
| `viewport.xy` | 当前 viewport 像素尺寸。 |
|
||||||
|
| `glyph.x` | marker 基础像素大小。 |
|
||||||
|
| `color` | 目前基本没用上,预留字段。 |
|
||||||
|
|
||||||
|
每帧在 `prepare` 里写入 GPU:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
queue.write_buffer(
|
||||||
|
&self.uniform_buffer,
|
||||||
|
0,
|
||||||
|
bytemuck::cast_slice(&[self.uniform]),
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Marker / Glyph 几何
|
||||||
|
|
||||||
|
代码里叫 `glyph`,但它不是字体系统里的 glyph。它更像一个“压力 marker”。
|
||||||
|
|
||||||
|
基础几何是一个 quad:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let glyph_vertices = [
|
||||||
|
[-1.0, -1.0],
|
||||||
|
[ 1.0, -1.0],
|
||||||
|
[-1.0, 1.0],
|
||||||
|
[-1.0, 1.0],
|
||||||
|
[ 1.0, -1.0],
|
||||||
|
[ 1.0, 1.0],
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
这 6 个点组成两个三角形。
|
||||||
|
|
||||||
|
每个压力点对应一个 `GlyphInstance`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
struct GlyphInstance {
|
||||||
|
world_position: [f32; 4],
|
||||||
|
style: [f32; 4],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
WGSL 接收:
|
||||||
|
|
||||||
|
```wgsl
|
||||||
|
struct GlyphInstanceInput {
|
||||||
|
@location(1) world_position: vec4f,
|
||||||
|
@location(2) style: vec4f,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`style` 的当前含义:
|
||||||
|
|
||||||
|
| 分量 | 含义 |
|
||||||
|
| --- | --- |
|
||||||
|
| `style.x` | 归一化压力值。 |
|
||||||
|
| `style.y` | 显示用压力值。 |
|
||||||
|
| `style.z` | 预留。 |
|
||||||
|
| `style.w` | 预留。 |
|
||||||
|
|
||||||
|
## Shader 总览
|
||||||
|
|
||||||
|
所有 pipeline 当前共用一个 WGSL 文件:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
include_str!("../static/wgsl/shader.wgsl")
|
||||||
|
```
|
||||||
|
|
||||||
|
不同 pipeline 选择不同入口函数。
|
||||||
|
|
||||||
|
## Background Shader
|
||||||
|
|
||||||
|
Rust 侧创建:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
create_background_pipeline(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
WGSL entry point:
|
||||||
|
|
||||||
|
```wgsl
|
||||||
|
@vertex
|
||||||
|
fn vs_background(...)
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_background(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
`vs_background` 画一个全屏三角形:
|
||||||
|
|
||||||
|
```wgsl
|
||||||
|
let positions = array<vec2f, 3>(
|
||||||
|
vec2f(-1.0, -3.0),
|
||||||
|
vec2f(3.0, 1.0),
|
||||||
|
vec2f(-1.0, 1.0),
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
一个大三角形覆盖整屏,避免两个三角形拼接时可能出现的边界问题。
|
||||||
|
|
||||||
|
`fs_background` 负责画:
|
||||||
|
|
||||||
|
- 深色垂直渐变
|
||||||
|
- vignette 暗角
|
||||||
|
- 中央氛围带
|
||||||
|
- 底部压力色条
|
||||||
|
- 色条刻度线
|
||||||
|
|
||||||
|
颜色映射来自:
|
||||||
|
|
||||||
|
```wgsl
|
||||||
|
fn sample_range_color(value: f32) -> vec3f
|
||||||
|
```
|
||||||
|
|
||||||
|
色阶大概是:
|
||||||
|
|
||||||
|
1. 蓝青
|
||||||
|
2. 绿色
|
||||||
|
3. 橙色
|
||||||
|
4. 红色
|
||||||
|
|
||||||
|
## Glyph / 数字 Shader
|
||||||
|
|
||||||
|
Rust 侧创建:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
create_glyph_pipeline(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
WGSL entry point:
|
||||||
|
|
||||||
|
```wgsl
|
||||||
|
@vertex
|
||||||
|
fn vs_glyph(...)
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_glyph(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
这是数字模式。
|
||||||
|
|
||||||
|
`vs_glyph` 做几件事:
|
||||||
|
|
||||||
|
1. 把 instance 的 `world_position` 乘 `u.view_proj`,得到 clip space 中心点。
|
||||||
|
2. 从 `instance.style.x` 读取压力强度。
|
||||||
|
3. 根据强度计算 marker 的像素大小。
|
||||||
|
4. 用 `viewport` 把像素大小转换成 NDC 偏移。
|
||||||
|
|
||||||
|
关键代码:
|
||||||
|
|
||||||
|
```wgsl
|
||||||
|
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;
|
||||||
|
```
|
||||||
|
|
||||||
|
这意味着数字不是固定世界尺寸,而是接近固定屏幕像素尺寸,更适合读数。
|
||||||
|
|
||||||
|
`fs_glyph` 做数字绘制:
|
||||||
|
|
||||||
|
```wgsl
|
||||||
|
let alpha = number_alpha(in.local, in.display_value);
|
||||||
|
let color = sample_range_color(in.intensity) * mix(0.82, 1.16, saturate(in.intensity));
|
||||||
|
return vec4f(color, alpha);
|
||||||
|
```
|
||||||
|
|
||||||
|
数字不是字体贴图,而是在 shader 里用七段数码管方式画出来。
|
||||||
|
|
||||||
|
相关函数:
|
||||||
|
|
||||||
|
- `digit_segment_on`
|
||||||
|
- `seven_segment_digit_alpha`
|
||||||
|
- `digit_count`
|
||||||
|
- `digit_at`
|
||||||
|
- `number_alpha`
|
||||||
|
|
||||||
|
## Dot / 点 Shader
|
||||||
|
|
||||||
|
Rust 侧创建:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
create_dot_pipeline(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
WGSL entry point:
|
||||||
|
|
||||||
|
```wgsl
|
||||||
|
@vertex
|
||||||
|
fn vs_dot(...)
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_dot(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
这是圆点模式。
|
||||||
|
|
||||||
|
`vs_dot` 和 `vs_glyph` 类似,也是把世界坐标点展开成屏幕像素大小的 quad。
|
||||||
|
|
||||||
|
点模式的尺寸曲线更小:
|
||||||
|
|
||||||
|
```wgsl
|
||||||
|
let pixel_size = u.glyph.x * mix(0.72, 1.85, shaped);
|
||||||
|
```
|
||||||
|
|
||||||
|
`fs_dot` 画:
|
||||||
|
|
||||||
|
- 圆形核心
|
||||||
|
- 外发光
|
||||||
|
- 小高光
|
||||||
|
- 压力映射颜色
|
||||||
|
|
||||||
|
圆形 alpha 来自:
|
||||||
|
|
||||||
|
```wgsl
|
||||||
|
fn circle_alpha(local: vec2f, radius: f32, softness: f32) -> f32
|
||||||
|
```
|
||||||
|
|
||||||
|
核心片段:
|
||||||
|
|
||||||
|
```wgsl
|
||||||
|
let core = circle_alpha(in.local, 0.42, 0.055);
|
||||||
|
let glow = circle_alpha(in.local, 0.78, 0.18) * intensity * 0.45;
|
||||||
|
let alpha = max(core, glow);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Model Shader
|
||||||
|
|
||||||
|
Rust 侧创建:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
create_model_pipeline(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
WGSL entry point:
|
||||||
|
|
||||||
|
```wgsl
|
||||||
|
@vertex
|
||||||
|
fn vs_model(...)
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_model(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
输入 layout:
|
||||||
|
|
||||||
|
```wgsl
|
||||||
|
@location(0) position: vec3f,
|
||||||
|
@location(1) tex_coords: vec2f,
|
||||||
|
@location(2) normal: vec3f,
|
||||||
|
@location(5) model_0: vec4f,
|
||||||
|
@location(6) model_1: vec4f,
|
||||||
|
@location(7) model_2: vec4f,
|
||||||
|
@location(8) model_3: vec4f,
|
||||||
|
```
|
||||||
|
|
||||||
|
`position / tex_coords / normal` 来自模型 vertex buffer。
|
||||||
|
|
||||||
|
`model_0..model_3` 来自 instance buffer,也就是每个模型实例的 transform matrix。
|
||||||
|
|
||||||
|
`vs_model`:
|
||||||
|
|
||||||
|
1. 重建 model matrix。
|
||||||
|
2. 计算 world position。
|
||||||
|
3. 用 `u.view_proj` 转到 clip space。
|
||||||
|
4. 变换 normal。
|
||||||
|
5. 翻转纹理 Y 坐标。
|
||||||
|
|
||||||
|
`fs_model`:
|
||||||
|
|
||||||
|
1. 采样纹理。
|
||||||
|
2. 计算简单漫反射光照。
|
||||||
|
3. 加一点 rim light。
|
||||||
|
4. 输出不透明颜色。
|
||||||
|
|
||||||
|
```wgsl
|
||||||
|
let light_dir = normalize(vec3f(-0.35, 0.85, 0.45));
|
||||||
|
let diffuse = max(dot(normal, light_dir), 0.0);
|
||||||
|
let rim = pow(1.0 - saturate(abs(normal.y)), 2.0) * 0.16;
|
||||||
|
let base = textureSample(model_texture, model_sampler, in.tex_coords).rgb;
|
||||||
|
let color = base * (0.28 + diffuse * 0.72) + vec3f(0.14, 0.24, 0.32) * rim;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 模型加载
|
||||||
|
|
||||||
|
模型在 `BackgroundRenderResources::new` 里加载一次:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let model =
|
||||||
|
match resources::load_model("cube.obj", device, _queue, &texture_bind_group_layout) {
|
||||||
|
Ok(model) => Some(model),
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!("failed to load cube.obj: {err:#}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
当前模型实例 transform 是写死的:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let model_instance = Instance::new(
|
||||||
|
glam::Vec3::new(0.0, -2.0, 12.0),
|
||||||
|
glam::Quat::from_rotation_y(0.65) * glam::Quat::from_rotation_x(-0.35),
|
||||||
|
)
|
||||||
|
.to_raw();
|
||||||
|
```
|
||||||
|
|
||||||
|
它把模型放到和压力矩阵相同的相机空间里。
|
||||||
|
|
||||||
|
## 当前限制
|
||||||
|
|
||||||
|
### 1. `rows / cols` 还没有真正重建布局
|
||||||
|
|
||||||
|
`FingerMode` 里有:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
rows: u32,
|
||||||
|
cols: u32,
|
||||||
|
```
|
||||||
|
|
||||||
|
但 `BackgroundRenderResources` 的 `layout` 和 instance buffer 是创建时确定的。
|
||||||
|
|
||||||
|
现在 `paint_finger` 只用 rows/cols 限制 draw count:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let draw_count = self.visible_instance_count(mode.rows, mode.cols);
|
||||||
|
```
|
||||||
|
|
||||||
|
所以它能少画一些点,但不会重新计算新尺寸下的空间布局。
|
||||||
|
|
||||||
|
如果后面要支持动态矩阵尺寸,需要:
|
||||||
|
|
||||||
|
1. 尺寸变化时重建 `MatrixLayout` 和 instance buffer。
|
||||||
|
2. 或者一开始分配最大尺寸 buffer,然后每帧按 active rows/cols 更新坐标。
|
||||||
|
|
||||||
|
### 2. `range` 字段还没有真正用起来
|
||||||
|
|
||||||
|
`FingerMode` 和 `HandGatewayMode` 都有:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
range: Range<u32>
|
||||||
|
```
|
||||||
|
|
||||||
|
但现在归一化仍在 `normalize_pressure_sample` 里硬编码为 `0..7000`。
|
||||||
|
|
||||||
|
所以如果要让不同模式有不同色域范围,需要把 range 传入归一化过程,或者把归一化移动到 render/update instance 阶段。
|
||||||
|
|
||||||
|
### 3. `glyph` 命名有点误导
|
||||||
|
|
||||||
|
现在的 `glyph` 实际更像 marker。
|
||||||
|
|
||||||
|
推荐未来改名:
|
||||||
|
|
||||||
|
```text
|
||||||
|
GlyphVertex -> MarkerVertex
|
||||||
|
GlyphInstance -> MarkerInstance
|
||||||
|
glyph_pipeline -> number_marker_pipeline
|
||||||
|
dot_pipeline -> dot_marker_pipeline
|
||||||
|
glyph_vertex_buffer -> marker_vertex_buffer
|
||||||
|
glyph_instance_buffer -> marker_instance_buffer
|
||||||
|
glyph_instances -> marker_instances
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Hand 模式还没有压力叠加
|
||||||
|
|
||||||
|
现在:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
ActiveMode::Hand(mode) => self.paint_hand(render_pass, mode),
|
||||||
|
```
|
||||||
|
|
||||||
|
只画模型,不画压力点。
|
||||||
|
|
||||||
|
如果手掌模式要显示压力映射,需要额外设计“压力矩阵 -> 手模型表面/区域”的映射。
|
||||||
|
|
||||||
|
可能方向:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
ActiveMode::Hand(mode) => {
|
||||||
|
self.paint_hand(render_pass, mode);
|
||||||
|
self.paint_hand_pressure_overlay(render_pass, mode);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
但 overlay 的坐标不应该复用现在的矩阵 X/Z 网格,需要新的手部映射数据。
|
||||||
|
|
||||||
|
## 建议下一步
|
||||||
|
|
||||||
|
把:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct FingerMode {
|
||||||
|
pub rows: u32,
|
||||||
|
pub cols: u32,
|
||||||
|
pub range: Range<u32>,
|
||||||
|
pub dot: bool,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
改成:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub enum MarkerMode {
|
||||||
|
Dot,
|
||||||
|
Number,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FingerMode {
|
||||||
|
pub rows: u32,
|
||||||
|
pub cols: u32,
|
||||||
|
pub range: Range<u32>,
|
||||||
|
pub marker_mode: MarkerMode,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这样比 `dot: bool` 更清楚。
|
||||||
|
|
||||||
|
现在的:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
dot: true
|
||||||
|
```
|
||||||
|
|
||||||
|
需要脑内翻译成“点模式”。
|
||||||
|
|
||||||
|
改成:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
marker_mode: MarkerMode::Dot
|
||||||
|
```
|
||||||
|
|
||||||
|
代码语义会直接很多。
|
||||||
|
|
||||||
232
res/hand.mtl
Normal file
232
res/hand.mtl
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
# Blender 5.1.0 MTL File: 'None'
|
||||||
|
# www.blender.org
|
||||||
|
|
||||||
|
newmtl mat_0
|
||||||
|
Ns 0.000000
|
||||||
|
Ka 1.000000 1.000000 1.000000
|
||||||
|
Kd 0.439636 0.439636 0.439636
|
||||||
|
Ks 0.500000 0.500000 0.500000
|
||||||
|
Ke 0.000000 0.000000 0.000000
|
||||||
|
Ni 1.500000
|
||||||
|
d 1.000000
|
||||||
|
illum 3
|
||||||
|
|
||||||
|
newmtl mat_1
|
||||||
|
Ns 0.000000
|
||||||
|
Ka 1.000000 1.000000 1.000000
|
||||||
|
Kd 0.450786 0.545725 0.745404
|
||||||
|
Ks 0.500000 0.500000 0.500000
|
||||||
|
Ke 0.000000 0.000000 0.000000
|
||||||
|
Ni 1.500000
|
||||||
|
d 1.000000
|
||||||
|
illum 3
|
||||||
|
|
||||||
|
newmtl mat_10
|
||||||
|
Ns 0.000000
|
||||||
|
Ka 1.000000 1.000000 1.000000
|
||||||
|
Kd 0.473532 0.623934 0.545725
|
||||||
|
Ks 0.500000 0.500000 0.500000
|
||||||
|
Ke 0.000000 0.000000 0.000000
|
||||||
|
Ni 1.500000
|
||||||
|
d 1.000000
|
||||||
|
illum 3
|
||||||
|
|
||||||
|
newmtl mat_11
|
||||||
|
Ns 0.000000
|
||||||
|
Ka 1.000000 1.000000 1.000000
|
||||||
|
Kd 0.033105 0.033105 0.033105
|
||||||
|
Ks 0.500000 0.500000 0.500000
|
||||||
|
Ke 0.000000 0.000000 0.000000
|
||||||
|
Ni 1.500000
|
||||||
|
d 1.000000
|
||||||
|
illum 3
|
||||||
|
|
||||||
|
newmtl mat_12
|
||||||
|
Ns 0.000000
|
||||||
|
Ka 1.000000 1.000000 1.000000
|
||||||
|
Kd 0.022170 0.017642 0.015993
|
||||||
|
Ks 0.500000 0.500000 0.500000
|
||||||
|
Ke 0.000000 0.000000 0.000000
|
||||||
|
Ni 1.500000
|
||||||
|
d 1.000000
|
||||||
|
illum 3
|
||||||
|
|
||||||
|
newmtl mat_13
|
||||||
|
Ns 0.000000
|
||||||
|
Ka 1.000000 1.000000 1.000000
|
||||||
|
Kd 0.000000 0.000000 0.000000
|
||||||
|
Ks 0.500000 0.500000 0.500000
|
||||||
|
Ke 0.000000 0.000000 0.000000
|
||||||
|
Ni 1.500000
|
||||||
|
d 1.000000
|
||||||
|
illum 3
|
||||||
|
|
||||||
|
newmtl mat_14
|
||||||
|
Ns 0.000000
|
||||||
|
Ka 1.000000 1.000000 1.000000
|
||||||
|
Kd 0.313971 0.401978 0.428669
|
||||||
|
Ks 0.500000 0.500000 0.500000
|
||||||
|
Ke 0.000000 0.000000 0.000000
|
||||||
|
Ni 1.500000
|
||||||
|
d 1.000000
|
||||||
|
illum 3
|
||||||
|
|
||||||
|
newmtl mat_15
|
||||||
|
Ns 0.000000
|
||||||
|
Ka 1.000000 1.000000 1.000000
|
||||||
|
Kd 1.000000 0.318547 0.000000
|
||||||
|
Ks 0.500000 0.500000 0.500000
|
||||||
|
Ke 0.000000 0.000000 0.000000
|
||||||
|
Ni 1.500000
|
||||||
|
d 1.000000
|
||||||
|
illum 3
|
||||||
|
|
||||||
|
newmtl mat_16
|
||||||
|
Ns 0.000000
|
||||||
|
Ka 1.000000 1.000000 1.000000
|
||||||
|
Kd 0.033105 0.132868 0.033105
|
||||||
|
Ks 0.500000 0.500000 0.500000
|
||||||
|
Ke 0.000000 0.000000 0.000000
|
||||||
|
Ni 1.500000
|
||||||
|
d 1.000000
|
||||||
|
illum 3
|
||||||
|
|
||||||
|
newmtl mat_17
|
||||||
|
Ns 0.000000
|
||||||
|
Ka 1.000000 1.000000 1.000000
|
||||||
|
Kd 0.623934 0.679542 0.806922
|
||||||
|
Ks 0.500000 0.500000 0.500000
|
||||||
|
Ke 0.000000 0.000000 0.000000
|
||||||
|
Ni 1.500000
|
||||||
|
d 1.000000
|
||||||
|
illum 3
|
||||||
|
|
||||||
|
newmtl mat_18
|
||||||
|
Ns 0.000000
|
||||||
|
Ka 1.000000 1.000000 1.000000
|
||||||
|
Kd 0.052861 0.038204 0.031891
|
||||||
|
Ks 0.500000 0.500000 0.500000
|
||||||
|
Ke 0.000000 0.000000 0.000000
|
||||||
|
Ni 1.500000
|
||||||
|
d 1.000000
|
||||||
|
illum 3
|
||||||
|
|
||||||
|
newmtl mat_19
|
||||||
|
Ns 0.000000
|
||||||
|
Ka 1.000000 1.000000 1.000000
|
||||||
|
Kd 1.000000 0.679542 0.644453
|
||||||
|
Ks 0.500000 0.500000 0.500000
|
||||||
|
Ke 0.000000 0.000000 0.000000
|
||||||
|
Ni 1.500000
|
||||||
|
d 1.000000
|
||||||
|
illum 3
|
||||||
|
|
||||||
|
newmtl mat_2
|
||||||
|
Ns 0.000000
|
||||||
|
Ka 1.000000 1.000000 1.000000
|
||||||
|
Kd 1.000000 0.278894 0.278894
|
||||||
|
Ks 0.500000 0.500000 0.500000
|
||||||
|
Ke 0.000000 0.000000 0.000000
|
||||||
|
Ni 1.500000
|
||||||
|
d 1.000000
|
||||||
|
illum 3
|
||||||
|
|
||||||
|
newmtl mat_20
|
||||||
|
Ns 0.000000
|
||||||
|
Ka 1.000000 1.000000 1.000000
|
||||||
|
Kd 1.000000 0.318547 0.033105
|
||||||
|
Ks 0.500000 0.500000 0.500000
|
||||||
|
Ke 0.000000 0.000000 0.000000
|
||||||
|
Ni 1.500000
|
||||||
|
d 1.000000
|
||||||
|
illum 3
|
||||||
|
|
||||||
|
newmtl mat_21
|
||||||
|
Ns 0.000000
|
||||||
|
Ka 1.000000 1.000000 1.000000
|
||||||
|
Kd 1.000000 0.396755 1.000000
|
||||||
|
Ks 0.500000 0.500000 0.500000
|
||||||
|
Ke 0.000000 0.000000 0.000000
|
||||||
|
Ni 1.500000
|
||||||
|
d 1.000000
|
||||||
|
illum 3
|
||||||
|
|
||||||
|
newmtl mat_22
|
||||||
|
Ns 0.000000
|
||||||
|
Ka 1.000000 1.000000 1.000000
|
||||||
|
Kd 0.930111 0.428669 0.230740
|
||||||
|
Ks 0.500000 0.500000 0.500000
|
||||||
|
Ke 0.000000 0.000000 0.000000
|
||||||
|
Ni 1.500000
|
||||||
|
d 1.000000
|
||||||
|
illum 3
|
||||||
|
|
||||||
|
newmtl mat_3
|
||||||
|
Ns 0.000000
|
||||||
|
Ka 1.000000 1.000000 1.000000
|
||||||
|
Kd 0.291771 0.258167 0.127438
|
||||||
|
Ks 0.500000 0.500000 0.500000
|
||||||
|
Ke 0.000000 0.000000 0.000000
|
||||||
|
Ni 1.500000
|
||||||
|
d 1.000000
|
||||||
|
illum 3
|
||||||
|
|
||||||
|
newmtl mat_4
|
||||||
|
Ns 0.000000
|
||||||
|
Ka 1.000000 1.000000 1.000000
|
||||||
|
Kd 0.318547 0.318547 0.318547
|
||||||
|
Ks 0.500000 0.500000 0.500000
|
||||||
|
Ke 0.000000 0.000000 0.000000
|
||||||
|
Ni 1.500000
|
||||||
|
d 1.000000
|
||||||
|
illum 3
|
||||||
|
|
||||||
|
newmtl mat_5
|
||||||
|
Ns 0.000000
|
||||||
|
Ka 1.000000 1.000000 1.000000
|
||||||
|
Kd 0.564712 0.391552 0.266356
|
||||||
|
Ks 0.500000 0.500000 0.500000
|
||||||
|
Ke 0.000000 0.000000 0.000000
|
||||||
|
Ni 1.500000
|
||||||
|
d 1.000000
|
||||||
|
illum 3
|
||||||
|
|
||||||
|
newmtl mat_6
|
||||||
|
Ns 0.000000
|
||||||
|
Ka 1.000000 1.000000 1.000000
|
||||||
|
Kd 0.000000 0.318547 0.000000
|
||||||
|
Ks 0.500000 0.500000 0.500000
|
||||||
|
Ke 0.000000 0.000000 0.000000
|
||||||
|
Ni 1.500000
|
||||||
|
d 1.000000
|
||||||
|
illum 3
|
||||||
|
|
||||||
|
newmtl mat_7
|
||||||
|
Ns 0.000000
|
||||||
|
Ka 1.000000 1.000000 1.000000
|
||||||
|
Kd 1.000000 1.000000 0.000000
|
||||||
|
Ks 0.500000 0.500000 0.500000
|
||||||
|
Ke 0.000000 0.000000 0.000000
|
||||||
|
Ni 1.500000
|
||||||
|
d 1.000000
|
||||||
|
illum 3
|
||||||
|
|
||||||
|
newmtl mat_8
|
||||||
|
Ns 0.000000
|
||||||
|
Ka 1.000000 1.000000 1.000000
|
||||||
|
Kd 0.887891 0.887891 0.887891
|
||||||
|
Ks 0.500000 0.500000 0.500000
|
||||||
|
Ke 0.000000 0.000000 0.000000
|
||||||
|
Ni 1.500000
|
||||||
|
d 1.000000
|
||||||
|
illum 3
|
||||||
|
|
||||||
|
newmtl mat_9
|
||||||
|
Ns 0.000000
|
||||||
|
Ka 1.000000 1.000000 1.000000
|
||||||
|
Kd 0.527115 0.768151 0.737881
|
||||||
|
Ks 0.500000 0.500000 0.500000
|
||||||
|
Ke 0.000000 0.000000 0.000000
|
||||||
|
Ni 1.500000
|
||||||
|
d 1.000000
|
||||||
|
illum 3
|
||||||
1373826
res/hand.obj
Normal file
1373826
res/hand.obj
Normal file
File diff suppressed because it is too large
Load Diff
104
src/app.rs
104
src/app.rs
@@ -1,16 +1,17 @@
|
|||||||
use crate::connection::ConnectionManager;
|
use crate::connection::ConnectionManager;
|
||||||
use crate::recording::Recorder;
|
use crate::recording::Recorder;
|
||||||
use crate::theme::{ONE_DARK_PRO, apply_fonts, apply_theme};
|
use crate::render::{ActiveMode, FingerMode, HandGatewayMode};
|
||||||
|
use crate::style::{ONE_DARK_PRO, apply_fonts, apply_theme, layout};
|
||||||
|
use crate::ui::SerialMode;
|
||||||
use crate::{
|
use crate::{
|
||||||
matrix::{MATRIX_COLS, MATRIX_ROWS},
|
matrix::{MATRIX_COLS, MATRIX_ROWS},
|
||||||
render::{
|
render::{
|
||||||
BackgroundRenderResources, MarkerMode, PRESSURE_CELL_COUNT, PressureFrame,
|
BackgroundRenderResources, PRESSURE_CELL_COUNT, PressureFrame, WgpuBackgroundCallback,
|
||||||
WgpuBackgroundCallback,
|
|
||||||
},
|
},
|
||||||
ui::{
|
ui::{
|
||||||
ConfigPanelState, ConnectPanelState, FloatingPanelState, MatrixConfigState,
|
ConfigPanelState, ConnectPanelState, FloatingPanelState, MatrixConfigState,
|
||||||
draw_config_panel, draw_connect_panel, draw_export_panel, draw_matrix_config_panel,
|
draw_config_panel, draw_connect_panel, draw_export_panel, draw_matrix_config_panel,
|
||||||
draw_scene_panel, draw_stats_panel,
|
draw_stats_panel, panel_restore_item,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use eframe::{egui, egui_wgpu};
|
use eframe::{egui, egui_wgpu};
|
||||||
@@ -35,6 +36,7 @@ pub struct EskinDesktopApp {
|
|||||||
matrix_config_panel: FloatingPanelState,
|
matrix_config_panel: FloatingPanelState,
|
||||||
matrix_config: MatrixConfigState,
|
matrix_config: MatrixConfigState,
|
||||||
signal_history: Vec<f32>,
|
signal_history: Vec<f32>,
|
||||||
|
active_mode: ActiveMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EskinDesktopApp {
|
impl EskinDesktopApp {
|
||||||
@@ -60,21 +62,42 @@ impl EskinDesktopApp {
|
|||||||
));
|
));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
connect_panel: FloatingPanelState::new([0.0, 0.0], [0.0, 0.0]),
|
connect_panel: FloatingPanelState::new([0.0, 0.0], [layout::RIGHT_TAG_X, 48.0]),
|
||||||
connect_state: ConnectPanelState::default(),
|
connect_state: ConnectPanelState::default(),
|
||||||
connection: Arc::new(ConnectionManager::new()),
|
connection: Arc::new(ConnectionManager::new()),
|
||||||
pressure_matrix: [[0.0, 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(
|
||||||
config_panel: FloatingPanelState::new([840.0, 48.0], [128.0, 48.0]),
|
[layout::LEFT_X, layout::TOP_Y],
|
||||||
|
[layout::LEFT_TAG_X, layout::TOP_Y],
|
||||||
|
),
|
||||||
|
config_panel: FloatingPanelState::new(
|
||||||
|
[layout::RIGHT_X, layout::TOP_Y],
|
||||||
|
[layout::RIGHT_TAG_X, layout::TOP_Y],
|
||||||
|
),
|
||||||
config_state: ConfigPanelState::default(),
|
config_state: ConfigPanelState::default(),
|
||||||
stats_panel: FloatingPanelState::new([16.0, 520.0], [240.0, 48.0]),
|
stats_panel: FloatingPanelState::new(
|
||||||
|
[layout::LEFT_X, layout::LEFT_SECOND_Y],
|
||||||
|
[layout::LEFT_TAG_X, layout::LEFT_SECOND_Y],
|
||||||
|
),
|
||||||
recorder: Recorder::full(),
|
recorder: Recorder::full(),
|
||||||
export_panel: FloatingPanelState::new([16.0, 280.0], [16.0, 280.0]),
|
export_panel: FloatingPanelState::new(
|
||||||
|
[layout::LEFT_X, layout::TOP_Y],
|
||||||
|
[layout::LEFT_TAG_X, layout::TOP_Y],
|
||||||
|
),
|
||||||
export_path: String::new(),
|
export_path: String::new(),
|
||||||
matrix_config_panel: FloatingPanelState::new([840.0, 280.0], [400.0, 48.0]),
|
matrix_config_panel: FloatingPanelState::new(
|
||||||
|
[layout::RIGHT_X, layout::RIGHT_SECOND_Y],
|
||||||
|
[layout::RIGHT_TAG_X, layout::RIGHT_SECOND_Y],
|
||||||
|
),
|
||||||
matrix_config: MatrixConfigState::default(),
|
matrix_config: MatrixConfigState::default(),
|
||||||
signal_history: Vec::with_capacity(128),
|
signal_history: Vec::with_capacity(128),
|
||||||
|
active_mode: ActiveMode::Finger(FingerMode {
|
||||||
|
rows: 12,
|
||||||
|
cols: 7,
|
||||||
|
range: 0..7000,
|
||||||
|
dot: true,
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +114,7 @@ impl EskinDesktopApp {
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
pressure: self.pressure_matrix,
|
pressure: self.pressure_matrix,
|
||||||
marker_mode: MarkerMode::Number,
|
active_mode: self.active_mode.clone(),
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -123,7 +146,7 @@ impl EskinDesktopApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw_title_bar(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
fn draw_title_bar(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||||
let title_bar_height = 36.0;
|
let title_bar_height = layout::TITLE_BAR_HEIGHT;
|
||||||
let title_bar_rect = ui
|
let title_bar_rect = ui
|
||||||
.allocate_space(egui::vec2(ui.available_width(), title_bar_height))
|
.allocate_space(egui::vec2(ui.available_width(), title_bar_height))
|
||||||
.1;
|
.1;
|
||||||
@@ -242,8 +265,12 @@ impl EskinDesktopApp {
|
|||||||
&self.recorder,
|
&self.recorder,
|
||||||
&mut self.export_path,
|
&mut self.export_path,
|
||||||
);
|
);
|
||||||
draw_scene_panel(ctx, &mut self.scene_panel);
|
// draw_scene_panel(ctx, &mut self.scene_panel);
|
||||||
draw_config_panel(ctx, &mut self.config_panel, &mut self.config_state);
|
if let Some(next_mode) =
|
||||||
|
draw_config_panel(ctx, &mut self.config_panel, &mut self.config_state)
|
||||||
|
{
|
||||||
|
self.switch_mode(next_mode);
|
||||||
|
}
|
||||||
draw_stats_panel(ctx, &mut self.stats_panel);
|
draw_stats_panel(ctx, &mut self.stats_panel);
|
||||||
draw_export_panel(
|
draw_export_panel(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -253,6 +280,54 @@ impl EskinDesktopApp {
|
|||||||
);
|
);
|
||||||
draw_matrix_config_panel(ctx, &mut self.matrix_config_panel, &mut self.matrix_config);
|
draw_matrix_config_panel(ctx, &mut self.matrix_config_panel, &mut self.matrix_config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn draw_panel_context_menu(&mut self, ui: &mut egui::Ui) {
|
||||||
|
let response = ui.interact(
|
||||||
|
ui.max_rect(),
|
||||||
|
egui::Id::new("workspace_panel_context_menu"),
|
||||||
|
egui::Sense::click(),
|
||||||
|
);
|
||||||
|
|
||||||
|
response.context_menu(|ui| {
|
||||||
|
ui.label("显示面板");
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
panel_restore_item(ui, "连接", &mut self.connect_panel);
|
||||||
|
panel_restore_item(ui, "配置", &mut self.config_panel);
|
||||||
|
panel_restore_item(ui, "录制", &mut self.export_panel);
|
||||||
|
panel_restore_item(ui, "矩阵", &mut self.matrix_config_panel);
|
||||||
|
panel_restore_item(ui, "统计", &mut self.stats_panel);
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
if ui.button("全部显示").clicked() {
|
||||||
|
self.connect_panel.visible = true;
|
||||||
|
self.config_panel.visible = true;
|
||||||
|
self.export_panel.visible = true;
|
||||||
|
self.matrix_config_panel.visible = true;
|
||||||
|
self.stats_panel.visible = true;
|
||||||
|
ui.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn switch_mode(&mut self, next: SerialMode) {
|
||||||
|
self.connect_state.mode = next;
|
||||||
|
self.config_state.mode = next;
|
||||||
|
self.connection.disconnect();
|
||||||
|
self.pressure_matrix.fill([0.0, 0.0]);
|
||||||
|
self.data_log_frame = 0;
|
||||||
|
|
||||||
|
self.active_mode = match next {
|
||||||
|
SerialMode::Finger => ActiveMode::Finger(FingerMode {
|
||||||
|
rows: self.matrix_config.rows,
|
||||||
|
cols: self.matrix_config.cols,
|
||||||
|
range: 0..7000,
|
||||||
|
dot: true,
|
||||||
|
}),
|
||||||
|
SerialMode::Hand => ActiveMode::Hand(HandGatewayMode { range: 0..7000 }),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn log_pressure_sample(raw: &[u32], rows: u32, cols: u32) {
|
fn log_pressure_sample(raw: &[u32], rows: u32, cols: u32) {
|
||||||
@@ -305,6 +380,7 @@ impl eframe::App for EskinDesktopApp {
|
|||||||
let ctx = ui.ctx().clone();
|
let ctx = ui.ctx().clone();
|
||||||
|
|
||||||
self.draw_wgpu_background(ui);
|
self.draw_wgpu_background(ui);
|
||||||
|
self.draw_panel_context_menu(ui);
|
||||||
self.draw_title_bar(ui, frame);
|
self.draw_title_bar(ui, frame);
|
||||||
self.draw_floating_panels(&ctx);
|
self.draw_floating_panels(&ctx);
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ mod recording;
|
|||||||
mod render;
|
mod render;
|
||||||
mod resources;
|
mod resources;
|
||||||
mod serial_core;
|
mod serial_core;
|
||||||
|
mod style;
|
||||||
mod texture;
|
mod texture;
|
||||||
mod theme;
|
|
||||||
mod ui;
|
mod ui;
|
||||||
mod utils;
|
mod utils;
|
||||||
use app::EskinDesktopApp;
|
use app::EskinDesktopApp;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use eframe::{
|
|||||||
egui_wgpu::{self, wgpu},
|
egui_wgpu::{self, wgpu},
|
||||||
wgpu::util::DeviceExt,
|
wgpu::util::DeviceExt,
|
||||||
};
|
};
|
||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
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;
|
||||||
@@ -17,13 +18,26 @@ pub struct WgpuBackgroundCallback {
|
|||||||
pub width: f32,
|
pub width: f32,
|
||||||
pub height: f32,
|
pub height: f32,
|
||||||
pub pressure: PressureFrame,
|
pub pressure: PressureFrame,
|
||||||
pub marker_mode: MarkerMode,
|
pub active_mode: ActiveMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
pub enum MarkerMode {
|
pub enum ActiveMode {
|
||||||
Number,
|
Finger(FingerMode),
|
||||||
Dot,
|
Hand(HandGatewayMode),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub struct FingerMode {
|
||||||
|
pub rows: u32,
|
||||||
|
pub cols: u32,
|
||||||
|
pub range: Range<u32>,
|
||||||
|
pub dot: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub struct HandGatewayMode {
|
||||||
|
pub range: Range<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl egui_wgpu::CallbackTrait for WgpuBackgroundCallback {
|
impl egui_wgpu::CallbackTrait for WgpuBackgroundCallback {
|
||||||
@@ -36,14 +50,7 @@ impl egui_wgpu::CallbackTrait for WgpuBackgroundCallback {
|
|||||||
resources: &mut egui_wgpu::CallbackResources,
|
resources: &mut egui_wgpu::CallbackResources,
|
||||||
) -> Vec<wgpu::CommandBuffer> {
|
) -> Vec<wgpu::CommandBuffer> {
|
||||||
let resources: &mut BackgroundRenderResources = resources.get_mut().unwrap();
|
let resources: &mut BackgroundRenderResources = resources.get_mut().unwrap();
|
||||||
resources.prepare(
|
resources.prepare(queue, self.width, self.height, &self.pressure);
|
||||||
queue,
|
|
||||||
self.width,
|
|
||||||
self.height,
|
|
||||||
&self.pressure,
|
|
||||||
self.marker_mode,
|
|
||||||
);
|
|
||||||
|
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +61,7 @@ impl egui_wgpu::CallbackTrait for WgpuBackgroundCallback {
|
|||||||
resources: &egui_wgpu::CallbackResources,
|
resources: &egui_wgpu::CallbackResources,
|
||||||
) {
|
) {
|
||||||
let resources: &BackgroundRenderResources = resources.get().unwrap();
|
let resources: &BackgroundRenderResources = resources.get().unwrap();
|
||||||
resources.paint(render_pass);
|
resources.paint(render_pass, &self.active_mode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,16 +72,17 @@ pub struct BackgroundRenderResources {
|
|||||||
uniform: MatrixUniform,
|
uniform: MatrixUniform,
|
||||||
uniform_buffer: wgpu::Buffer,
|
uniform_buffer: wgpu::Buffer,
|
||||||
uniform_bind_group: wgpu::BindGroup,
|
uniform_bind_group: wgpu::BindGroup,
|
||||||
|
|
||||||
background_pipeline: wgpu::RenderPipeline,
|
background_pipeline: wgpu::RenderPipeline,
|
||||||
glyph_pipeline: wgpu::RenderPipeline,
|
glyph_pipeline: wgpu::RenderPipeline,
|
||||||
dot_pipeline: wgpu::RenderPipeline,
|
dot_pipeline: wgpu::RenderPipeline,
|
||||||
model_pipeline: wgpu::RenderPipeline,
|
model_pipeline: wgpu::RenderPipeline,
|
||||||
|
|
||||||
model: Option<Model>,
|
model: Option<Model>,
|
||||||
model_instance_buffer: wgpu::Buffer,
|
model_instance_buffer: wgpu::Buffer,
|
||||||
glyph_vertex_buffer: wgpu::Buffer,
|
glyph_vertex_buffer: wgpu::Buffer,
|
||||||
glyph_instance_buffer: wgpu::Buffer,
|
glyph_instance_buffer: wgpu::Buffer,
|
||||||
glyph_instances: Vec<GlyphInstance>,
|
glyph_instances: Vec<GlyphInstance>,
|
||||||
marker_mode: MarkerMode,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BackgroundRenderResources {
|
impl BackgroundRenderResources {
|
||||||
@@ -180,10 +188,10 @@ impl BackgroundRenderResources {
|
|||||||
create_model_pipeline(device, target_format, &shader, &model_pipeline_layout);
|
create_model_pipeline(device, target_format, &shader, &model_pipeline_layout);
|
||||||
|
|
||||||
let model =
|
let model =
|
||||||
match resources::load_model("cube.obj", device, _queue, &texture_bind_group_layout) {
|
match resources::load_model("hand.obj", device, _queue, &texture_bind_group_layout) {
|
||||||
Ok(model) => Some(model),
|
Ok(model) => Some(model),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("failed to load cube.obj: {err:#}");
|
log::warn!("failed to load hand.obj: {err:#}");
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -239,22 +247,13 @@ impl BackgroundRenderResources {
|
|||||||
glyph_vertex_buffer,
|
glyph_vertex_buffer,
|
||||||
glyph_instance_buffer,
|
glyph_instance_buffer,
|
||||||
glyph_instances,
|
glyph_instances,
|
||||||
marker_mode: MarkerMode::Number,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare(
|
fn prepare(&mut self, queue: &wgpu::Queue, width: f32, height: f32, pressure: &PressureFrame) {
|
||||||
&mut self,
|
|
||||||
queue: &wgpu::Queue,
|
|
||||||
width: f32,
|
|
||||||
height: f32,
|
|
||||||
pressure: &PressureFrame,
|
|
||||||
marker_mode: MarkerMode,
|
|
||||||
) {
|
|
||||||
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));
|
||||||
self.marker_mode = marker_mode;
|
|
||||||
queue.write_buffer(
|
queue.write_buffer(
|
||||||
&self.uniform_buffer,
|
&self.uniform_buffer,
|
||||||
0,
|
0,
|
||||||
@@ -275,12 +274,36 @@ impl BackgroundRenderResources {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint(&self, render_pass: &mut wgpu::RenderPass<'_>) {
|
fn paint(&self, render_pass: &mut wgpu::RenderPass<'_>, active_mode: &ActiveMode) {
|
||||||
render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||||
|
|
||||||
render_pass.set_pipeline(&self.background_pipeline);
|
render_pass.set_pipeline(&self.background_pipeline);
|
||||||
render_pass.draw(0..3, 0..1);
|
render_pass.draw(0..3, 0..1);
|
||||||
|
|
||||||
|
match active_mode {
|
||||||
|
ActiveMode::Finger(mode) => self.paint_finger(render_pass, mode),
|
||||||
|
ActiveMode::Hand(mode) => self.paint_hand(render_pass, mode),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint_finger(&self, render_pass: &mut wgpu::RenderPass<'_>, mode: &FingerMode) {
|
||||||
|
let _range = mode.range.clone();
|
||||||
|
let marker_pipeline = if mode.dot {
|
||||||
|
&self.dot_pipeline
|
||||||
|
} else {
|
||||||
|
&self.glyph_pipeline
|
||||||
|
};
|
||||||
|
let draw_count = self.visible_instance_count(mode.rows, mode.cols);
|
||||||
|
|
||||||
|
render_pass.set_pipeline(marker_pipeline);
|
||||||
|
render_pass.set_vertex_buffer(0, self.glyph_vertex_buffer.slice(..));
|
||||||
|
render_pass.set_vertex_buffer(1, self.glyph_instance_buffer.slice(..));
|
||||||
|
render_pass.draw(0..6, 0..draw_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint_hand(&self, render_pass: &mut wgpu::RenderPass<'_>, mode: &HandGatewayMode) {
|
||||||
|
let _range = mode.range.clone();
|
||||||
|
|
||||||
if let Some(model) = &self.model {
|
if let Some(model) = &self.model {
|
||||||
render_pass.set_pipeline(&self.model_pipeline);
|
render_pass.set_pipeline(&self.model_pipeline);
|
||||||
render_pass.set_vertex_buffer(1, self.model_instance_buffer.slice(..));
|
render_pass.set_vertex_buffer(1, self.model_instance_buffer.slice(..));
|
||||||
@@ -294,17 +317,11 @@ impl BackgroundRenderResources {
|
|||||||
render_pass.draw_indexed(0..mesh.num_elements, 0, 0..1);
|
render_pass.draw_indexed(0..mesh.num_elements, 0, 0..1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// let marker_pipeline = match self.marker_mode {
|
fn visible_instance_count(&self, rows: u32, cols: u32) -> u32 {
|
||||||
// MarkerMode::Number => &self.glyph_pipeline,
|
let requested = rows.saturating_mul(cols);
|
||||||
// MarkerMode::Dot => &self.dot_pipeline,
|
requested.min(self.glyph_instances.len() as u32)
|
||||||
// };
|
|
||||||
let marker_pipeline = &self.dot_pipeline;
|
|
||||||
|
|
||||||
render_pass.set_pipeline(marker_pipeline);
|
|
||||||
render_pass.set_vertex_buffer(0, self.glyph_vertex_buffer.slice(..));
|
|
||||||
render_pass.set_vertex_buffer(1, self.glyph_instance_buffer.slice(..));
|
|
||||||
render_pass.draw(0..6, 0..self.glyph_instances.len() as u32);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
319
src/style.rs
Normal file
319
src/style.rs
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
use eframe::egui;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct AppTheme {
|
||||||
|
pub bg: egui::Color32,
|
||||||
|
pub panel: egui::Color32,
|
||||||
|
pub panel_strong: egui::Color32,
|
||||||
|
pub panel_deep: egui::Color32,
|
||||||
|
pub border: egui::Color32,
|
||||||
|
pub border_soft: egui::Color32,
|
||||||
|
pub text: egui::Color32,
|
||||||
|
pub text_dim: egui::Color32,
|
||||||
|
pub text_subtle: egui::Color32,
|
||||||
|
pub accent: egui::Color32,
|
||||||
|
pub accent_hot: egui::Color32,
|
||||||
|
pub radius: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct DesignMetrics {
|
||||||
|
pub row_gap: f32,
|
||||||
|
pub item_gap: f32,
|
||||||
|
pub field_height: f32,
|
||||||
|
pub button_height: f32,
|
||||||
|
pub icon_button: egui::Vec2,
|
||||||
|
pub panel_padding: i8,
|
||||||
|
pub group_padding_x: i8,
|
||||||
|
pub group_padding_y: i8,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const ONE_DARK_PRO: AppTheme = AppTheme {
|
||||||
|
bg: egui::Color32::from_rgb(30, 40, 50),
|
||||||
|
panel: egui::Color32::from_rgb(22, 28, 35),
|
||||||
|
panel_strong: egui::Color32::from_rgb(34, 43, 54),
|
||||||
|
panel_deep: egui::Color32::from_rgb(15, 20, 27),
|
||||||
|
border: egui::Color32::from_rgb(78, 93, 110),
|
||||||
|
border_soft: egui::Color32::from_rgb(50, 62, 76),
|
||||||
|
text: egui::Color32::from_rgb(235, 241, 247),
|
||||||
|
text_dim: egui::Color32::from_rgb(174, 185, 198),
|
||||||
|
text_subtle: egui::Color32::from_rgb(126, 139, 154),
|
||||||
|
accent: egui::Color32::from_rgb(41, 192, 204),
|
||||||
|
accent_hot: egui::Color32::from_rgb(247, 190, 84),
|
||||||
|
radius: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ACCENT_GREEN: egui::Color32 = egui::Color32::from_rgb(126, 203, 137);
|
||||||
|
pub const ACCENT_RED: egui::Color32 = egui::Color32::from_rgb(240, 99, 94);
|
||||||
|
pub const ACCENT_BLUE: egui::Color32 = egui::Color32::from_rgb(96, 170, 240);
|
||||||
|
pub const ACCENT_ORANGE: egui::Color32 = egui::Color32::from_rgb(242, 163, 87);
|
||||||
|
|
||||||
|
pub const METRICS: DesignMetrics = DesignMetrics {
|
||||||
|
row_gap: 10.0,
|
||||||
|
item_gap: 8.0,
|
||||||
|
field_height: 24.0,
|
||||||
|
button_height: 28.0,
|
||||||
|
icon_button: egui::vec2(30.0, 26.0),
|
||||||
|
panel_padding: 12,
|
||||||
|
group_padding_x: 10,
|
||||||
|
group_padding_y: 8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod layout {
|
||||||
|
pub const TITLE_BAR_HEIGHT: f32 = 36.0;
|
||||||
|
pub const CENTER_PANEL_TOP: f32 = 48.0;
|
||||||
|
pub const LEFT_X: f32 = 24.0;
|
||||||
|
pub const RIGHT_X: f32 = 1328.0;
|
||||||
|
pub const TOP_Y: f32 = 56.0;
|
||||||
|
pub const LEFT_SECOND_Y: f32 = 292.0;
|
||||||
|
pub const RIGHT_SECOND_Y: f32 = 388.0;
|
||||||
|
pub const LEFT_TAG_X: f32 = 24.0;
|
||||||
|
pub const RIGHT_TAG_X: f32 = 1810.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_theme(ctx: &egui::Context, theme: &AppTheme) {
|
||||||
|
let mut visuals = egui::Visuals::dark();
|
||||||
|
visuals.override_text_color = Some(theme.text);
|
||||||
|
visuals.panel_fill = theme.bg;
|
||||||
|
visuals.window_fill = theme.panel;
|
||||||
|
visuals.window_stroke = egui::Stroke::new(1.0, theme.border);
|
||||||
|
visuals.extreme_bg_color = theme.panel_deep;
|
||||||
|
visuals.faint_bg_color = theme.panel_strong;
|
||||||
|
visuals.code_bg_color = theme.panel_deep;
|
||||||
|
visuals.warn_fg_color = theme.accent_hot;
|
||||||
|
visuals.error_fg_color = ACCENT_RED;
|
||||||
|
|
||||||
|
visuals.widgets.noninteractive.bg_fill = theme.panel_strong;
|
||||||
|
visuals.widgets.noninteractive.bg_stroke = egui::Stroke::new(1.0, theme.border_soft);
|
||||||
|
visuals.widgets.noninteractive.fg_stroke = egui::Stroke::new(1.0, theme.text);
|
||||||
|
visuals.widgets.inactive.bg_fill = theme.panel_strong;
|
||||||
|
visuals.widgets.inactive.bg_stroke = egui::Stroke::new(1.0, theme.border_soft);
|
||||||
|
visuals.widgets.inactive.fg_stroke = egui::Stroke::new(1.0, theme.text);
|
||||||
|
visuals.widgets.hovered.bg_fill = egui::Color32::from_rgb(44, 57, 70);
|
||||||
|
visuals.widgets.hovered.bg_stroke = egui::Stroke::new(1.0, theme.accent);
|
||||||
|
visuals.widgets.hovered.fg_stroke = egui::Stroke::new(1.0, egui::Color32::WHITE);
|
||||||
|
visuals.widgets.active.bg_fill = theme.accent;
|
||||||
|
visuals.widgets.active.bg_stroke = egui::Stroke::new(1.0, theme.accent_hot);
|
||||||
|
visuals.widgets.active.fg_stroke = egui::Stroke::new(1.0, egui::Color32::WHITE);
|
||||||
|
visuals.widgets.open.bg_fill = egui::Color32::from_rgb(39, 50, 62);
|
||||||
|
visuals.widgets.open.bg_stroke = egui::Stroke::new(1.0, theme.accent);
|
||||||
|
visuals.widgets.open.fg_stroke = egui::Stroke::new(1.0, theme.text);
|
||||||
|
|
||||||
|
visuals.selection.bg_fill = egui::Color32::from_rgb(35, 123, 140);
|
||||||
|
visuals.selection.stroke = egui::Stroke::new(1.0, egui::Color32::WHITE);
|
||||||
|
visuals.hyperlink_color = theme.accent_hot;
|
||||||
|
ctx.set_visuals(visuals);
|
||||||
|
|
||||||
|
let mut style = (*ctx.global_style()).clone();
|
||||||
|
style.spacing.item_spacing = egui::vec2(METRICS.item_gap, 6.0);
|
||||||
|
style.spacing.button_padding = egui::vec2(12.0, 5.0);
|
||||||
|
style.spacing.window_margin = egui::Margin::same(METRICS.panel_padding);
|
||||||
|
style.spacing.interact_size = egui::vec2(72.0, METRICS.field_height);
|
||||||
|
style.spacing.combo_width = 112.0;
|
||||||
|
style.spacing.text_edit_width = 112.0;
|
||||||
|
style.visuals.window_corner_radius = egui::CornerRadius::same(theme.radius);
|
||||||
|
style.visuals.menu_corner_radius = egui::CornerRadius::same(4);
|
||||||
|
style.visuals.window_shadow = egui::epaint::Shadow {
|
||||||
|
offset: [0, 14],
|
||||||
|
blur: 28,
|
||||||
|
spread: 0,
|
||||||
|
color: egui::Color32::from_black_alpha(135),
|
||||||
|
};
|
||||||
|
ctx.set_global_style(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_fonts(ctx: &egui::Context) {
|
||||||
|
let mut fonts = egui::FontDefinitions::default();
|
||||||
|
|
||||||
|
fonts.font_data.insert(
|
||||||
|
"Hack-Bold".to_owned(),
|
||||||
|
egui::FontData::from_static(include_bytes!("../static/Hack-Bold.ttf")).into(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let has_yahei = std::fs::read(r"C:\Windows\Fonts\msyh.ttc")
|
||||||
|
.or_else(|_| std::fs::read(r"C:\Windows\Fonts\msyhbd.ttc"))
|
||||||
|
.map(|font_data| {
|
||||||
|
fonts.font_data.insert(
|
||||||
|
"Microsoft-YaHei".to_owned(),
|
||||||
|
egui::FontData::from_owned(font_data).into(),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.is_ok();
|
||||||
|
|
||||||
|
fonts
|
||||||
|
.families
|
||||||
|
.entry(egui::FontFamily::Proportional)
|
||||||
|
.or_default()
|
||||||
|
.insert(0, "Hack-Bold".to_owned());
|
||||||
|
if has_yahei {
|
||||||
|
fonts
|
||||||
|
.families
|
||||||
|
.entry(egui::FontFamily::Proportional)
|
||||||
|
.or_default()
|
||||||
|
.push("Microsoft-YaHei".to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
fonts
|
||||||
|
.families
|
||||||
|
.entry(egui::FontFamily::Monospace)
|
||||||
|
.or_default()
|
||||||
|
.insert(0, "Hack-Bold".to_owned());
|
||||||
|
if has_yahei {
|
||||||
|
fonts
|
||||||
|
.families
|
||||||
|
.entry(egui::FontFamily::Monospace)
|
||||||
|
.or_default()
|
||||||
|
.push("Microsoft-YaHei".to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.set_fonts(fonts);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn panel_frame(ctx: &egui::Context) -> egui::Frame {
|
||||||
|
let style = ctx.global_style();
|
||||||
|
egui::Frame::window(&style)
|
||||||
|
.fill(ONE_DARK_PRO.panel)
|
||||||
|
.stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.border))
|
||||||
|
.corner_radius(egui::CornerRadius::same(ONE_DARK_PRO.radius))
|
||||||
|
.inner_margin(egui::Margin::same(METRICS.panel_padding))
|
||||||
|
.shadow(egui::epaint::Shadow {
|
||||||
|
offset: [0, 14],
|
||||||
|
blur: 28,
|
||||||
|
spread: 0,
|
||||||
|
color: egui::Color32::from_black_alpha(135),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn center_panel_frame() -> egui::Frame {
|
||||||
|
egui::Frame::new()
|
||||||
|
.fill(ONE_DARK_PRO.panel)
|
||||||
|
.corner_radius(egui::CornerRadius::same(ONE_DARK_PRO.radius))
|
||||||
|
.inner_margin(egui::Margin::symmetric(
|
||||||
|
METRICS.panel_padding + 2,
|
||||||
|
METRICS.panel_padding,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn group_frame() -> egui::Frame {
|
||||||
|
egui::Frame::new()
|
||||||
|
.fill(ONE_DARK_PRO.panel_deep)
|
||||||
|
.stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.border_soft))
|
||||||
|
.corner_radius(egui::CornerRadius::same(4))
|
||||||
|
.inner_margin(egui::Margin::symmetric(
|
||||||
|
METRICS.group_padding_x,
|
||||||
|
METRICS.group_padding_y,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tag_button(label: impl Into<egui::WidgetText>) -> egui::Button<'static> {
|
||||||
|
egui::Button::new(label)
|
||||||
|
.fill(ONE_DARK_PRO.panel_strong)
|
||||||
|
.stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.border))
|
||||||
|
.corner_radius(egui::CornerRadius::same(4))
|
||||||
|
.min_size(egui::vec2(0.0, METRICS.button_height))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn primary_button(label: impl Into<egui::WidgetText>) -> egui::Button<'static> {
|
||||||
|
egui::Button::new(label)
|
||||||
|
.fill(ONE_DARK_PRO.accent)
|
||||||
|
.stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.accent_hot))
|
||||||
|
.corner_radius(egui::CornerRadius::same(4))
|
||||||
|
.min_size(egui::vec2(112.0, METRICS.button_height))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn danger_button(label: impl Into<egui::WidgetText>) -> egui::Button<'static> {
|
||||||
|
egui::Button::new(label)
|
||||||
|
.fill(ACCENT_RED)
|
||||||
|
.stroke(egui::Stroke::new(
|
||||||
|
1.0,
|
||||||
|
egui::Color32::from_rgb(255, 138, 126),
|
||||||
|
))
|
||||||
|
.corner_radius(egui::CornerRadius::same(4))
|
||||||
|
.min_size(egui::vec2(112.0, METRICS.button_height))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn accent_button(
|
||||||
|
label: impl Into<egui::WidgetText>,
|
||||||
|
fill: egui::Color32,
|
||||||
|
) -> egui::Button<'static> {
|
||||||
|
egui::Button::new(label)
|
||||||
|
.fill(fill)
|
||||||
|
.stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.border))
|
||||||
|
.corner_radius(egui::CornerRadius::same(4))
|
||||||
|
.min_size(egui::vec2(0.0, METRICS.button_height))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mode_button(label: &'static str, selected: bool) -> egui::Button<'static> {
|
||||||
|
let fill = if selected {
|
||||||
|
ONE_DARK_PRO.accent
|
||||||
|
} else {
|
||||||
|
ONE_DARK_PRO.panel_strong
|
||||||
|
};
|
||||||
|
let stroke = if selected {
|
||||||
|
ONE_DARK_PRO.accent_hot
|
||||||
|
} else {
|
||||||
|
ONE_DARK_PRO.border_soft
|
||||||
|
};
|
||||||
|
|
||||||
|
egui::Button::new(egui::RichText::new(label).color(egui::Color32::WHITE))
|
||||||
|
.fill(fill)
|
||||||
|
.stroke(egui::Stroke::new(1.0, stroke))
|
||||||
|
.corner_radius(egui::CornerRadius::same(4))
|
||||||
|
.min_size(egui::vec2(96.0, METRICS.button_height))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn icon_button<'a>(icon: impl Into<egui::WidgetText>, size: egui::Vec2) -> egui::Button<'a> {
|
||||||
|
egui::Button::new(icon)
|
||||||
|
.fill(ONE_DARK_PRO.panel_strong)
|
||||||
|
.stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.border))
|
||||||
|
.corner_radius(egui::CornerRadius::same(4))
|
||||||
|
.min_size(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn panel_title(title: &'static str) -> egui::RichText {
|
||||||
|
egui::RichText::new(title)
|
||||||
|
.color(ONE_DARK_PRO.text_dim)
|
||||||
|
.size(13.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn field_label(label: impl Into<String>) -> egui::RichText {
|
||||||
|
egui::RichText::new(label.into())
|
||||||
|
.color(ONE_DARK_PRO.text_dim)
|
||||||
|
.size(12.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn value_text(text: impl Into<String>) -> egui::RichText {
|
||||||
|
egui::RichText::new(text.into())
|
||||||
|
.color(ONE_DARK_PRO.text)
|
||||||
|
.size(12.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn subtle_text(text: impl Into<String>) -> egui::RichText {
|
||||||
|
egui::RichText::new(text.into())
|
||||||
|
.color(ONE_DARK_PRO.text_subtle)
|
||||||
|
.size(12.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dim_text() -> egui::Color32 {
|
||||||
|
ONE_DARK_PRO.text_dim
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn accent_text() -> egui::Color32 {
|
||||||
|
ONE_DARK_PRO.accent_hot
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn status_color_ok() -> egui::Color32 {
|
||||||
|
ACCENT_GREEN
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn status_color_warn() -> egui::Color32 {
|
||||||
|
ONE_DARK_PRO.accent_hot
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn status_color_error() -> egui::Color32 {
|
||||||
|
ACCENT_RED
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn status_color_info() -> egui::Color32 {
|
||||||
|
ACCENT_BLUE
|
||||||
|
}
|
||||||
590
src/ui.rs
590
src/ui.rs
@@ -3,9 +3,9 @@ use eframe::egui;
|
|||||||
use crate::{
|
use crate::{
|
||||||
connection::{ConnectionManager, ConnectionState},
|
connection::{ConnectionManager, ConnectionState},
|
||||||
recording::Recorder,
|
recording::Recorder,
|
||||||
theme::{
|
style::{
|
||||||
ACCENT_BLUE, ACCENT_GREEN, ACCENT_ORANGE, ACCENT_RED, ONE_DARK_PRO, accent_text, dim_text,
|
self, ACCENT_BLUE, ACCENT_GREEN, ACCENT_ORANGE, ACCENT_RED, METRICS, ONE_DARK_PRO,
|
||||||
group_frame, panel_frame, tag_button,
|
accent_text, dim_text, group_frame, layout, panel_frame, tag_button,
|
||||||
},
|
},
|
||||||
utils::serial_enum,
|
utils::serial_enum,
|
||||||
};
|
};
|
||||||
@@ -47,9 +47,8 @@ pub struct ConnectPanelState {
|
|||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum SerialMode {
|
pub enum SerialMode {
|
||||||
SingleModule,
|
Finger,
|
||||||
Manual,
|
Hand,
|
||||||
Model,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -80,7 +79,7 @@ impl FloatingPanelState {
|
|||||||
impl Default for ConfigPanelState {
|
impl Default for ConfigPanelState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
mode: SerialMode::SingleModule,
|
mode: SerialMode::Finger,
|
||||||
port: "COM3".to_owned(),
|
port: "COM3".to_owned(),
|
||||||
baud_rate: 115_200,
|
baud_rate: 115_200,
|
||||||
data_bits: 8,
|
data_bits: 8,
|
||||||
@@ -98,10 +97,10 @@ impl Default for ConfigPanelState {
|
|||||||
|
|
||||||
impl Default for ConnectPanelState {
|
impl Default for ConnectPanelState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let port = serial_enum().unwrap();
|
let port = serial_enum().unwrap_or_default();
|
||||||
let selected_port = port.first().cloned().unwrap_or_default();
|
let selected_port = port.first().cloned().unwrap_or_default();
|
||||||
Self {
|
Self {
|
||||||
mode: SerialMode::SingleModule,
|
mode: SerialMode::Finger,
|
||||||
port,
|
port,
|
||||||
selected_port,
|
selected_port,
|
||||||
duration: 10,
|
duration: 10,
|
||||||
@@ -143,165 +142,162 @@ pub fn draw_connect_panel(
|
|||||||
ConnectionState::Connected | ConnectionState::Streaming
|
ConnectionState::Connected | ConnectionState::Streaming
|
||||||
);
|
);
|
||||||
|
|
||||||
draw_center_floating_panel(ctx, panel, "connect_center_panel", 42.0, "连接", |ui| {
|
draw_center_floating_panel(
|
||||||
ui.set_min_width(320.0);
|
ctx,
|
||||||
|
panel,
|
||||||
|
"connect_center_panel",
|
||||||
|
layout::CENTER_PANEL_TOP,
|
||||||
|
"连接",
|
||||||
|
|ui| {
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
// centered_mode_row(ui, &mut config.mode);
|
||||||
|
ui.add_space(METRICS.row_gap);
|
||||||
|
|
||||||
ui.vertical(|ui| {
|
group_frame().show(ui, |ui| {
|
||||||
let button_width = 96.0;
|
|
||||||
let gap = 8.0;
|
|
||||||
let total_width = button_width * 3.0 + gap * 2.0;
|
|
||||||
let left_space = ((ui.available_width() - total_width) * 0.5).max(0.0);
|
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.add_space(left_space);
|
|
||||||
|
|
||||||
mode_button(ui, &mut config.mode, SerialMode::SingleModule, "单模块");
|
|
||||||
ui.add_space(gap);
|
|
||||||
mode_button(ui, &mut config.mode, SerialMode::Manual, "全手");
|
|
||||||
ui.add_space(gap);
|
|
||||||
mode_button(ui, &mut config.mode, SerialMode::Model, "模型");
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.add_space(6.0);
|
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.add_space(10.0);
|
|
||||||
ui.add(
|
|
||||||
egui::Image::new(egui::include_image!("../static/cpu.png"))
|
|
||||||
.fit_to_exact_size(egui::vec2(72.0, 72.0)),
|
|
||||||
);
|
|
||||||
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.colored_label(ONE_DARK_PRO.text, "串口");
|
ui.add(
|
||||||
egui::ComboBox::from_id_salt("connect_ports")
|
egui::Image::new(egui::include_image!("../static/cpu.png"))
|
||||||
.width(130.0)
|
.fit_to_exact_size(egui::vec2(64.0, 64.0)),
|
||||||
.selected_text(if config.selected_port.is_empty() {
|
|
||||||
"无可用串口".to_owned()
|
|
||||||
} else {
|
|
||||||
config.selected_port.clone()
|
|
||||||
})
|
|
||||||
.show_ui(ui, |ui| {
|
|
||||||
for port in &config.port {
|
|
||||||
let label = port.clone();
|
|
||||||
ui.selectable_value(
|
|
||||||
&mut config.selected_port,
|
|
||||||
port.clone(),
|
|
||||||
label,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if ui
|
|
||||||
.add(
|
|
||||||
egui::Button::new("⟳")
|
|
||||||
.fill(ONE_DARK_PRO.panel_strong)
|
|
||||||
.stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.border_soft))
|
|
||||||
.min_size(egui::vec2(24.0, 20.0)),
|
|
||||||
)
|
|
||||||
.on_hover_text("刷新串口")
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
if let Ok(ports) = serial_enum() {
|
|
||||||
if !ports.contains(&config.selected_port) {
|
|
||||||
config.selected_port =
|
|
||||||
ports.first().cloned().unwrap_or_default();
|
|
||||||
}
|
|
||||||
config.port = ports;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ui.add_space(10.0);
|
|
||||||
|
|
||||||
ui.label("频率");
|
|
||||||
ui.add_sized(
|
|
||||||
egui::vec2(72.0, 20.0),
|
|
||||||
egui::DragValue::new(&mut config.duration).range(1..=120),
|
|
||||||
);
|
);
|
||||||
});
|
ui.add_space(METRICS.item_gap);
|
||||||
|
|
||||||
ui.add_space(10.0);
|
ui.vertical(|ui| {
|
||||||
ui.horizontal(|ui| {
|
draw_connect_port_row(ui, config);
|
||||||
ui.checkbox(&mut config.manual, "手动");
|
ui.add_space(6.0);
|
||||||
|
draw_connect_matrix_row(ui, config);
|
||||||
ui.add_enabled_ui(config.manual, |ui| {
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.colored_label(ONE_DARK_PRO.text_dim, "行");
|
|
||||||
ui.add_sized(
|
|
||||||
egui::vec2(48.0, 20.0),
|
|
||||||
egui::DragValue::new(&mut config.rows).range(1..=64),
|
|
||||||
);
|
|
||||||
ui.colored_label(ONE_DARK_PRO.text_dim, "列");
|
|
||||||
ui.add_sized(
|
|
||||||
egui::vec2(48.0, 20.0),
|
|
||||||
egui::DragValue::new(&mut config.cols).range(1..=64),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
|
ui.add_space(METRICS.row_gap);
|
||||||
|
draw_connect_action_row(ui, conn_state, is_connected, config, connection);
|
||||||
|
|
||||||
|
if is_connected {
|
||||||
|
draw_recording_toolbar(ui, recorder, export_path);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
ui.add_space(8.0);
|
// fn centered_mode_row(ui: &mut egui::Ui, mode: &mut SerialMode) {
|
||||||
|
// let button_width = 96.0;
|
||||||
|
// let gap = METRICS.item_gap;
|
||||||
|
// let total_width = button_width * 3.0 + gap * 2.0;
|
||||||
|
// let left_space = ((ui.available_width() - total_width) * 0.5).max(0.0);
|
||||||
|
|
||||||
// Connection status and button row
|
// ui.horizontal(|ui| {
|
||||||
ui.horizontal(|ui| {
|
// ui.add_space(left_space);
|
||||||
let status_text = match conn_state {
|
// mode_button(ui, mode, SerialMode::Finger, "指尖模块");
|
||||||
ConnectionState::Disconnected => "未连接",
|
// ui.add_space(gap);
|
||||||
ConnectionState::Connecting => "连接中...",
|
// mode_button(ui, mode, SerialMode::Hand, "手掌模块");
|
||||||
ConnectionState::Connected => "已连接",
|
// // ui.add_space(gap);
|
||||||
ConnectionState::Streaming => "采集中",
|
// // mode_button(ui, mode, SerialMode::Model, "模型");
|
||||||
ConnectionState::Error => "连接错误",
|
// });
|
||||||
};
|
// }
|
||||||
let status_color = match conn_state {
|
|
||||||
ConnectionState::Disconnected => ONE_DARK_PRO.text_dim,
|
|
||||||
ConnectionState::Connecting => egui::Color32::from_rgb(200, 180, 60),
|
|
||||||
ConnectionState::Connected => egui::Color32::from_rgb(158, 184, 101),
|
|
||||||
ConnectionState::Streaming => egui::Color32::from_rgb(100, 200, 255),
|
|
||||||
ConnectionState::Error => egui::Color32::from_rgb(255, 98, 82),
|
|
||||||
};
|
|
||||||
ui.colored_label(status_color, status_text);
|
|
||||||
|
|
||||||
let used = 100.0;
|
fn draw_connect_port_row(ui: &mut egui::Ui, config: &mut ConnectPanelState) {
|
||||||
let remaining = (ui.available_width() - used).max(0.0);
|
ui.horizontal(|ui| {
|
||||||
ui.add_space(remaining);
|
ui.label(style::field_label("串口"));
|
||||||
|
egui::ComboBox::from_id_salt("connect_ports")
|
||||||
let btn_label = if is_connected { "断开" } else { "连接" };
|
.width(150.0)
|
||||||
let btn_fill = if is_connected {
|
.selected_text(if config.selected_port.is_empty() {
|
||||||
egui::Color32::from_rgb(180, 60, 60)
|
"无可用串口".to_owned()
|
||||||
} else {
|
} else {
|
||||||
ONE_DARK_PRO.accent
|
config.selected_port.clone()
|
||||||
};
|
})
|
||||||
let btn_stroke = if is_connected {
|
.show_ui(ui, |ui| {
|
||||||
egui::Stroke::new(1.0, egui::Color32::from_rgb(220, 80, 80))
|
for port in &config.port {
|
||||||
} else {
|
ui.selectable_value(&mut config.selected_port, port.clone(), port.as_str());
|
||||||
egui::Stroke::new(1.0, ONE_DARK_PRO.accent_hot)
|
|
||||||
};
|
|
||||||
|
|
||||||
if ui
|
|
||||||
.add(
|
|
||||||
egui::Button::new(
|
|
||||||
egui::RichText::new(btn_label).color(egui::Color32::WHITE),
|
|
||||||
)
|
|
||||||
.fill(btn_fill)
|
|
||||||
.stroke(btn_stroke)
|
|
||||||
.min_size(egui::vec2(96.0, 28.0)),
|
|
||||||
)
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
if is_connected {
|
|
||||||
connection.disconnect();
|
|
||||||
} else if !config.selected_port.is_empty() {
|
|
||||||
connection.connect(
|
|
||||||
&config.selected_port,
|
|
||||||
config.rows as u32,
|
|
||||||
config.cols as u32,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Recording toolbar (visible when connected)
|
if ui
|
||||||
if is_connected {
|
.add(style::icon_button(
|
||||||
draw_recording_toolbar(ui, recorder, export_path);
|
egui::RichText::new("⟳").color(ONE_DARK_PRO.text).size(16.0),
|
||||||
|
METRICS.icon_button,
|
||||||
|
))
|
||||||
|
.on_hover_text("刷新串口")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
if let Ok(ports) = serial_enum() {
|
||||||
|
if !ports.contains(&config.selected_port) {
|
||||||
|
config.selected_port = ports.first().cloned().unwrap_or_default();
|
||||||
|
}
|
||||||
|
config.port = ports;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.add_space(METRICS.item_gap);
|
||||||
|
ui.label(style::field_label("频率"));
|
||||||
|
ui.add_sized(
|
||||||
|
egui::vec2(72.0, METRICS.field_height),
|
||||||
|
egui::DragValue::new(&mut config.duration).range(1..=120),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_connect_matrix_row(ui: &mut egui::Ui, config: &mut ConnectPanelState) {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.checkbox(&mut config.manual, "手动矩阵");
|
||||||
|
|
||||||
|
ui.add_enabled_ui(config.manual, |ui| {
|
||||||
|
ui.add_space(METRICS.item_gap);
|
||||||
|
ui.label(style::field_label("行"));
|
||||||
|
ui.add_sized(
|
||||||
|
egui::vec2(56.0, METRICS.field_height),
|
||||||
|
egui::DragValue::new(&mut config.rows).range(1..=64),
|
||||||
|
);
|
||||||
|
ui.label(style::field_label("列"));
|
||||||
|
ui.add_sized(
|
||||||
|
egui::vec2(56.0, METRICS.field_height),
|
||||||
|
egui::DragValue::new(&mut config.cols).range(1..=64),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_connect_action_row(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
conn_state: ConnectionState,
|
||||||
|
is_connected: bool,
|
||||||
|
config: &ConnectPanelState,
|
||||||
|
connection: &ConnectionManager,
|
||||||
|
) {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
let status_text = match conn_state {
|
||||||
|
ConnectionState::Disconnected => "未连接",
|
||||||
|
ConnectionState::Connecting => "连接中...",
|
||||||
|
ConnectionState::Connected => "已连接",
|
||||||
|
ConnectionState::Streaming => "采集中",
|
||||||
|
ConnectionState::Error => "连接错误",
|
||||||
|
};
|
||||||
|
let status_color = match conn_state {
|
||||||
|
ConnectionState::Disconnected => ONE_DARK_PRO.text_dim,
|
||||||
|
ConnectionState::Connecting => style::status_color_warn(),
|
||||||
|
ConnectionState::Connected => style::status_color_ok(),
|
||||||
|
ConnectionState::Streaming => style::status_color_info(),
|
||||||
|
ConnectionState::Error => style::status_color_error(),
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.colored_label(status_color, status_text);
|
||||||
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||||
|
let button = if is_connected {
|
||||||
|
style::danger_button("断开")
|
||||||
|
} else {
|
||||||
|
style::primary_button("连接")
|
||||||
|
};
|
||||||
|
|
||||||
|
if ui.add(button).clicked() {
|
||||||
|
if is_connected {
|
||||||
|
connection.disconnect();
|
||||||
|
} else if !config.selected_port.is_empty() {
|
||||||
|
connection.connect(
|
||||||
|
&config.selected_port,
|
||||||
|
config.rows as u32,
|
||||||
|
config.cols as u32,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -311,11 +307,16 @@ pub fn draw_config_panel(
|
|||||||
ctx: &egui::Context,
|
ctx: &egui::Context,
|
||||||
panel: &mut FloatingPanelState,
|
panel: &mut FloatingPanelState,
|
||||||
config: &mut ConfigPanelState,
|
config: &mut ConfigPanelState,
|
||||||
) {
|
) -> Option<SerialMode> {
|
||||||
|
let mut changed_mode = None;
|
||||||
draw_floating_panel(ctx, panel, "配置", "config_panel", |ui| {
|
draw_floating_panel(ctx, panel, "配置", "config_panel", |ui| {
|
||||||
ui.set_min_width(560.0);
|
ui.set_min_width(560.0);
|
||||||
|
|
||||||
draw_mode_row(ui, config);
|
if let Some(mode) = draw_mode_row(ui, config) {
|
||||||
|
changed_mode = Some(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw_mode_row(ui, config);
|
||||||
ui.separator();
|
ui.separator();
|
||||||
draw_connection_row(ui, config);
|
draw_connection_row(ui, config);
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
@@ -323,22 +324,30 @@ pub fn draw_config_panel(
|
|||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
draw_mode_body(ui, config);
|
draw_mode_body(ui, config);
|
||||||
});
|
});
|
||||||
|
changed_mode
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_mode_row(ui: &mut egui::Ui, config: &mut ConfigPanelState) {
|
fn draw_mode_row(ui: &mut egui::Ui, config: &mut ConfigPanelState) -> Option<SerialMode> {
|
||||||
|
let mut changed_to = None;
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.colored_label(dim_text(), "模式");
|
ui.colored_label(dim_text(), "模式");
|
||||||
ui.add_space(12.0);
|
ui.add_space(12.0);
|
||||||
|
|
||||||
mode_button(ui, &mut config.mode, SerialMode::SingleModule, "单模块");
|
if mode_button(ui, &mut config.mode, SerialMode::Finger, "指尖模块") {
|
||||||
mode_button(ui, &mut config.mode, SerialMode::Manual, "全手");
|
changed_to = Some(SerialMode::Finger)
|
||||||
mode_button(ui, &mut config.mode, SerialMode::Model, "模型");
|
}
|
||||||
|
if mode_button(ui, &mut config.mode, SerialMode::Hand, "手掌模块") {
|
||||||
|
changed_to = Some(SerialMode::Hand)
|
||||||
|
}
|
||||||
|
// mode_button(ui, &mut config.mode, SerialMode::Model, "模型");
|
||||||
|
|
||||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||||
ui.checkbox(&mut config.auto_reconnect, "自动");
|
ui.checkbox(&mut config.auto_reconnect, "自动");
|
||||||
ui.colored_label(dim_text(), "重连");
|
ui.colored_label(dim_text(), "重连");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
changed_to
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_connection_row(ui: &mut egui::Ui, config: &mut ConfigPanelState) {
|
fn draw_connection_row(ui: &mut egui::Ui, config: &mut ConfigPanelState) {
|
||||||
@@ -348,20 +357,20 @@ fn draw_connection_row(ui: &mut egui::Ui, config: &mut ConfigPanelState) {
|
|||||||
.fit_to_exact_size(egui::vec2(72.0, 72.0)),
|
.fit_to_exact_size(egui::vec2(72.0, 72.0)),
|
||||||
);
|
);
|
||||||
|
|
||||||
ui.add_space(10.0);
|
ui.add_space(METRICS.item_gap);
|
||||||
|
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
ui.label(format!("端口 {}", config.port));
|
ui.label(style::value_text(format!("端口 {}", config.port)));
|
||||||
ui.label(format!("波特率 {}", config.baud_rate));
|
ui.label(style::value_text(format!("波特率 {}", config.baud_rate)));
|
||||||
let status = if config.connected {
|
let status = if config.connected {
|
||||||
"已连接"
|
"已连接"
|
||||||
} else {
|
} else {
|
||||||
"未连接"
|
"未连接"
|
||||||
};
|
};
|
||||||
let status_color = if config.connected {
|
let status_color = if config.connected {
|
||||||
egui::Color32::from_rgb(158, 184, 101)
|
style::status_color_ok()
|
||||||
} else {
|
} else {
|
||||||
egui::Color32::from_rgb(255, 98, 82)
|
style::status_color_error()
|
||||||
};
|
};
|
||||||
ui.colored_label(status_color, status);
|
ui.colored_label(status_color, status);
|
||||||
});
|
});
|
||||||
@@ -370,12 +379,11 @@ fn draw_connection_row(ui: &mut egui::Ui, config: &mut ConfigPanelState) {
|
|||||||
|
|
||||||
let button_text = if config.connected { "断开" } else { "连接" };
|
let button_text = if config.connected { "断开" } else { "连接" };
|
||||||
if ui
|
if ui
|
||||||
.add(
|
.add(if config.connected {
|
||||||
egui::Button::new(button_text)
|
style::danger_button(button_text)
|
||||||
.fill(ONE_DARK_PRO.accent)
|
} else {
|
||||||
.stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.accent_hot))
|
style::primary_button(button_text)
|
||||||
.min_size(egui::vec2(120.0, 30.0)),
|
})
|
||||||
)
|
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
config.connected = !config.connected;
|
config.connected = !config.connected;
|
||||||
@@ -383,7 +391,11 @@ fn draw_connection_row(ui: &mut egui::Ui, config: &mut ConfigPanelState) {
|
|||||||
|
|
||||||
ui.add_space(18.0);
|
ui.add_space(18.0);
|
||||||
ui.colored_label(
|
ui.colored_label(
|
||||||
egui::Color32::from_rgb(158, 184, 101),
|
if config.auto_reconnect {
|
||||||
|
style::status_color_ok()
|
||||||
|
} else {
|
||||||
|
ONE_DARK_PRO.text_dim
|
||||||
|
},
|
||||||
if config.auto_reconnect {
|
if config.auto_reconnect {
|
||||||
"链路保护 开"
|
"链路保护 开"
|
||||||
} else {
|
} else {
|
||||||
@@ -397,39 +409,39 @@ fn draw_serial_grid(ui: &mut egui::Ui, config: &mut ConfigPanelState) {
|
|||||||
group_frame().show(ui, |ui| {
|
group_frame().show(ui, |ui| {
|
||||||
egui::Grid::new("serial_config_grid")
|
egui::Grid::new("serial_config_grid")
|
||||||
.num_columns(4)
|
.num_columns(4)
|
||||||
.spacing(egui::vec2(10.0, 5.0))
|
.spacing(egui::vec2(14.0, 7.0))
|
||||||
.striped(true)
|
.striped(true)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.label("端口");
|
ui.label(style::field_label("端口"));
|
||||||
ui.add_sized(
|
ui.add_sized(
|
||||||
egui::vec2(110.0, 20.0),
|
egui::vec2(110.0, METRICS.field_height),
|
||||||
egui::TextEdit::singleline(&mut config.port),
|
egui::TextEdit::singleline(&mut config.port),
|
||||||
);
|
);
|
||||||
|
|
||||||
ui.label("波特率");
|
ui.label(style::field_label("波特率"));
|
||||||
baud_combo(ui, config);
|
baud_combo(ui, config);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
|
|
||||||
ui.label("数据位");
|
ui.label(style::field_label("数据位"));
|
||||||
ui.add_sized(
|
ui.add_sized(
|
||||||
egui::vec2(70.0, 20.0),
|
egui::vec2(70.0, METRICS.field_height),
|
||||||
egui::DragValue::new(&mut config.data_bits).range(5..=8),
|
egui::DragValue::new(&mut config.data_bits).range(5..=8),
|
||||||
);
|
);
|
||||||
|
|
||||||
ui.label("校验");
|
ui.label(style::field_label("校验"));
|
||||||
parity_combo(ui, config);
|
parity_combo(ui, config);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
|
|
||||||
ui.label("停止位");
|
ui.label(style::field_label("停止位"));
|
||||||
ui.add_sized(
|
ui.add_sized(
|
||||||
egui::vec2(70.0, 20.0),
|
egui::vec2(70.0, METRICS.field_height),
|
||||||
egui::DragValue::new(&mut config.stop_bits).range(1..=2),
|
egui::DragValue::new(&mut config.stop_bits).range(1..=2),
|
||||||
);
|
);
|
||||||
|
|
||||||
ui.label("超时");
|
ui.label(style::field_label("超时"));
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.add_sized(
|
ui.add_sized(
|
||||||
egui::vec2(84.0, 20.0),
|
egui::vec2(84.0, METRICS.field_height),
|
||||||
egui::DragValue::new(&mut config.timeout_ms)
|
egui::DragValue::new(&mut config.timeout_ms)
|
||||||
.range(50..=30_000)
|
.range(50..=30_000)
|
||||||
.speed(50),
|
.speed(50),
|
||||||
@@ -443,11 +455,11 @@ fn draw_serial_grid(ui: &mut egui::Ui, config: &mut ConfigPanelState) {
|
|||||||
|
|
||||||
fn draw_mode_body(ui: &mut egui::Ui, config: &mut ConfigPanelState) {
|
fn draw_mode_body(ui: &mut egui::Ui, config: &mut ConfigPanelState) {
|
||||||
group_frame().show(ui, |ui| match config.mode {
|
group_frame().show(ui, |ui| match config.mode {
|
||||||
SerialMode::SingleModule => {
|
SerialMode::Finger => {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label("模块地址");
|
ui.label(style::field_label("模块地址"));
|
||||||
ui.add_sized(
|
ui.add_sized(
|
||||||
egui::vec2(80.0, 20.0),
|
egui::vec2(80.0, METRICS.field_height),
|
||||||
egui::DragValue::new(&mut config.module_addr).range(1..=247),
|
egui::DragValue::new(&mut config.module_addr).range(1..=247),
|
||||||
);
|
);
|
||||||
ui.add_space(16.0);
|
ui.add_space(16.0);
|
||||||
@@ -464,11 +476,11 @@ fn draw_mode_body(ui: &mut egui::Ui, config: &mut ConfigPanelState) {
|
|||||||
ui.label("0 字节");
|
ui.label("0 字节");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
SerialMode::Manual => {
|
SerialMode::Hand => {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label("发送");
|
ui.label(style::field_label("发送"));
|
||||||
ui.add_sized(
|
ui.add_sized(
|
||||||
egui::vec2(300.0, 20.0),
|
egui::vec2(300.0, METRICS.field_height),
|
||||||
egui::TextEdit::singleline(&mut config.manual_tx),
|
egui::TextEdit::singleline(&mut config.manual_tx),
|
||||||
);
|
);
|
||||||
if ui.add(tag_button("发送")).clicked() {}
|
if ui.add(tag_button("发送")).clicked() {}
|
||||||
@@ -476,18 +488,17 @@ fn draw_mode_body(ui: &mut egui::Ui, config: &mut ConfigPanelState) {
|
|||||||
config.manual_tx.clear();
|
config.manual_tx.clear();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
} // SerialMode::Model => {
|
||||||
SerialMode::Model => {
|
// ui.horizontal(|ui| {
|
||||||
ui.horizontal(|ui| {
|
// ui.label(style::field_label("模型"));
|
||||||
ui.label("模型");
|
// ui.add_sized(
|
||||||
ui.add_sized(
|
// egui::vec2(300.0, METRICS.field_height),
|
||||||
egui::vec2(300.0, 20.0),
|
// egui::TextEdit::singleline(&mut config.model_path),
|
||||||
egui::TextEdit::singleline(&mut config.model_path),
|
// );
|
||||||
);
|
// if ui.add(tag_button("加载")).clicked() {}
|
||||||
if ui.add(tag_button("加载")).clicked() {}
|
// if ui.add(tag_button("运行")).clicked() {}
|
||||||
if ui.add(tag_button("运行")).clicked() {}
|
// });
|
||||||
});
|
// }
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -517,35 +528,25 @@ pub fn icon_button_sized<'a>(
|
|||||||
}
|
}
|
||||||
.fill(ONE_DARK_PRO.panel_strong)
|
.fill(ONE_DARK_PRO.panel_strong)
|
||||||
.stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.border))
|
.stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.border))
|
||||||
.corner_radius(egui::CornerRadius::same(2))
|
.corner_radius(egui::CornerRadius::same(ONE_DARK_PRO.radius))
|
||||||
.min_size(size);
|
.min_size(size);
|
||||||
|
|
||||||
ui.add(button).on_hover_text(tooltip)
|
ui.add(button).on_hover_text(tooltip)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mode_button(ui: &mut egui::Ui, mode: &mut SerialMode, value: SerialMode, label: &'static str) {
|
fn mode_button(
|
||||||
let selected = *mode == value;
|
ui: &mut egui::Ui,
|
||||||
let fill = if selected {
|
mode: &mut SerialMode,
|
||||||
ONE_DARK_PRO.accent
|
value: SerialMode,
|
||||||
} else {
|
label: &'static str,
|
||||||
ONE_DARK_PRO.panel_strong
|
) -> bool {
|
||||||
};
|
let clicked = ui.add(style::mode_button(label, *mode == value)).clicked();
|
||||||
let stroke = if selected {
|
|
||||||
ONE_DARK_PRO.accent_hot
|
|
||||||
} else {
|
|
||||||
ONE_DARK_PRO.border_soft
|
|
||||||
};
|
|
||||||
|
|
||||||
if ui
|
if clicked && *mode != value {
|
||||||
.add(
|
|
||||||
egui::Button::new(egui::RichText::new(label).color(egui::Color32::WHITE))
|
|
||||||
.fill(fill)
|
|
||||||
.stroke(egui::Stroke::new(1.0, stroke))
|
|
||||||
.min_size(egui::vec2(96.0, 24.0)),
|
|
||||||
)
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
*mode = value;
|
*mode = value;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -581,13 +582,13 @@ pub fn draw_stats_panel(ctx: &egui::Context, panel: &mut FloatingPanelState) {
|
|||||||
draw_floating_panel(ctx, panel, "统计", "stats_panel", |ui| {
|
draw_floating_panel(ctx, panel, "统计", "stats_panel", |ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.colored_label(accent_text(), "0.030");
|
ui.colored_label(accent_text(), "0.030");
|
||||||
ui.label("81m:51s");
|
ui.label(style::subtle_text("81m:51s"));
|
||||||
});
|
});
|
||||||
ui.separator();
|
ui.separator();
|
||||||
group_frame().show(ui, |ui| {
|
group_frame().show(ui, |ui| {
|
||||||
ui.label("帧率 / GPU 信息");
|
ui.label(style::field_label("帧率 / GPU 信息"));
|
||||||
ui.label("边界 589.0us");
|
ui.label(style::value_text("边界 589.0us"));
|
||||||
ui.label("簇 12.8ms");
|
ui.label(style::value_text("簇 12.8ms"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -612,12 +613,13 @@ fn draw_floating_panel(
|
|||||||
.resizable(true)
|
.resizable(true)
|
||||||
.frame(panel_frame(ctx))
|
.frame(panel_frame(ctx))
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
|
ui.spacing_mut().item_spacing = egui::vec2(METRICS.item_gap, 6.0);
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if ui.add(tag_button("隐藏")).clicked() {
|
if ui.add(tag_button("隐藏")).clicked() {
|
||||||
hide_requested = true;
|
hide_requested = true;
|
||||||
}
|
}
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
ui.colored_label(dim_text(), title);
|
ui.label(style::panel_title(title));
|
||||||
});
|
});
|
||||||
ui.separator();
|
ui.separator();
|
||||||
add_contents(ui);
|
add_contents(ui);
|
||||||
@@ -647,23 +649,24 @@ fn draw_floating_panel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
panel.visible = open && !hide_requested;
|
panel.visible = open && !hide_requested;
|
||||||
} else {
|
|
||||||
let response = egui::Area::new(egui::Id::new(format!("{id}_tag")))
|
|
||||||
.current_pos(panel.tag_pos)
|
|
||||||
.movable(true)
|
|
||||||
.order(egui::Order::Foreground)
|
|
||||||
.show(ctx, |ui| {
|
|
||||||
ui.set_min_width(86.0);
|
|
||||||
if ui
|
|
||||||
.add(tag_button(format!("▸ {title}")).min_size(egui::vec2(86.0, 22.0)))
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
panel.visible = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
panel.tag_pos = response.response.rect.min;
|
|
||||||
}
|
}
|
||||||
|
// else {
|
||||||
|
// let response = egui::Area::new(egui::Id::new(format!("{id}_tag")))
|
||||||
|
// .current_pos(panel.tag_pos)
|
||||||
|
// .movable(true)
|
||||||
|
// .order(egui::Order::Foreground)
|
||||||
|
// .show(ctx, |ui| {
|
||||||
|
// ui.set_min_width(86.0);
|
||||||
|
// if ui
|
||||||
|
// .add(tag_button(format!("▸ {title}")).min_size(egui::vec2(86.0, 22.0)))
|
||||||
|
// .clicked()
|
||||||
|
// {
|
||||||
|
// panel.visible = true;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// panel.tag_pos = response.response.rect.min;
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_center_floating_panel(
|
fn draw_center_floating_panel(
|
||||||
@@ -674,7 +677,7 @@ fn draw_center_floating_panel(
|
|||||||
collapsed_label: &'static str,
|
collapsed_label: &'static str,
|
||||||
add_contents: impl FnOnce(&mut egui::Ui),
|
add_contents: impl FnOnce(&mut egui::Ui),
|
||||||
) {
|
) {
|
||||||
const PANEL_WIDTH: f32 = 430.0;
|
const PANEL_WIDTH: f32 = 520.0;
|
||||||
const SLIDE_DISTANCE: f32 = 18.0;
|
const SLIDE_DISTANCE: f32 = 18.0;
|
||||||
|
|
||||||
let anim = advance_center_panel_anim(ctx, panel);
|
let anim = advance_center_panel_anim(ctx, panel);
|
||||||
@@ -759,14 +762,10 @@ fn center_panel_shell<R>(
|
|||||||
width: f32,
|
width: f32,
|
||||||
add_contents: impl FnOnce(&mut egui::Ui) -> R,
|
add_contents: impl FnOnce(&mut egui::Ui) -> R,
|
||||||
) -> egui::InnerResponse<R> {
|
) -> egui::InnerResponse<R> {
|
||||||
egui::Frame::new()
|
style::center_panel_frame().show(ui, |ui| {
|
||||||
.fill(ONE_DARK_PRO.panel)
|
ui.set_width(width);
|
||||||
.corner_radius(egui::CornerRadius::same(ONE_DARK_PRO.radius))
|
add_contents(ui)
|
||||||
.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 {
|
fn allocate_center_panel_handle(ui: &mut egui::Ui) -> egui::Rect {
|
||||||
@@ -1059,10 +1058,10 @@ pub fn draw_recording_toolbar(ui: &mut egui::Ui, recorder: &Recorder, export_pat
|
|||||||
let rec_btn = if is_recording {
|
let rec_btn = if is_recording {
|
||||||
tag_button("⏹ 停止")
|
tag_button("⏹ 停止")
|
||||||
} else {
|
} else {
|
||||||
egui::Button::new(egui::RichText::new("● 全量录制").color(egui::Color32::WHITE))
|
style::accent_button(
|
||||||
.fill(ACCENT_RED)
|
egui::RichText::new("● 全量录制").color(egui::Color32::WHITE),
|
||||||
.stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.border))
|
ACCENT_RED,
|
||||||
.corner_radius(egui::CornerRadius::same(2))
|
)
|
||||||
};
|
};
|
||||||
if ui.add(rec_btn).clicked() {
|
if ui.add(rec_btn).clicked() {
|
||||||
if is_recording {
|
if is_recording {
|
||||||
@@ -1078,10 +1077,10 @@ pub fn draw_recording_toolbar(ui: &mut egui::Ui, recorder: &Recorder, export_pat
|
|||||||
let snap_btn = if is_recording {
|
let snap_btn = if is_recording {
|
||||||
tag_button("⏹ 快照停止")
|
tag_button("⏹ 快照停止")
|
||||||
} else {
|
} else {
|
||||||
egui::Button::new(egui::RichText::new("✂ 快照录制").color(egui::Color32::WHITE))
|
style::accent_button(
|
||||||
.fill(ACCENT_ORANGE)
|
egui::RichText::new("✂ 快照录制").color(egui::Color32::WHITE),
|
||||||
.stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.border))
|
ACCENT_ORANGE,
|
||||||
.corner_radius(egui::CornerRadius::same(2))
|
)
|
||||||
};
|
};
|
||||||
if ui.add(snap_btn).clicked() {
|
if ui.add(snap_btn).clicked() {
|
||||||
if is_recording {
|
if is_recording {
|
||||||
@@ -1104,12 +1103,10 @@ pub fn draw_recording_toolbar(ui: &mut egui::Ui, recorder: &Recorder, export_pat
|
|||||||
|
|
||||||
// Export
|
// Export
|
||||||
if ui
|
if ui
|
||||||
.add(
|
.add(style::accent_button(
|
||||||
egui::Button::new(egui::RichText::new("📤 导出CSV").color(egui::Color32::WHITE))
|
egui::RichText::new("📤 导出CSV").color(egui::Color32::WHITE),
|
||||||
.fill(ACCENT_BLUE)
|
ACCENT_BLUE,
|
||||||
.stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.border))
|
))
|
||||||
.corner_radius(egui::CornerRadius::same(2)),
|
|
||||||
)
|
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
let path = if export_path.is_empty() {
|
let path = if export_path.is_empty() {
|
||||||
@@ -1128,12 +1125,10 @@ pub fn draw_recording_toolbar(ui: &mut egui::Ui, recorder: &Recorder, export_pat
|
|||||||
|
|
||||||
// Import
|
// Import
|
||||||
if ui
|
if ui
|
||||||
.add(
|
.add(style::accent_button(
|
||||||
egui::Button::new(egui::RichText::new("📥 导入CSV").color(egui::Color32::WHITE))
|
egui::RichText::new("📥 导入CSV").color(egui::Color32::WHITE),
|
||||||
.fill(ACCENT_GREEN)
|
ACCENT_GREEN,
|
||||||
.stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.border))
|
))
|
||||||
.corner_radius(egui::CornerRadius::same(2)),
|
|
||||||
)
|
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
let import_path = export_path.clone();
|
let import_path = export_path.clone();
|
||||||
@@ -1147,7 +1142,10 @@ pub fn draw_recording_toolbar(ui: &mut egui::Ui, recorder: &Recorder, export_pat
|
|||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.colored_label(dim_text(), "导出路径");
|
ui.colored_label(dim_text(), "导出路径");
|
||||||
ui.add_sized(
|
ui.add_sized(
|
||||||
egui::vec2(ui.available_width() - 80.0, 20.0),
|
egui::vec2(
|
||||||
|
(ui.available_width() - 80.0).max(160.0),
|
||||||
|
METRICS.field_height,
|
||||||
|
),
|
||||||
egui::TextEdit::singleline(export_path).hint_text("eskin_export_*.csv"),
|
egui::TextEdit::singleline(export_path).hint_text("eskin_export_*.csv"),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -1198,25 +1196,24 @@ pub fn draw_matrix_config_panel(
|
|||||||
ui.set_min_width(280.0);
|
ui.set_min_width(280.0);
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.colored_label(dim_text(), "矩阵尺寸");
|
ui.label(style::field_label("矩阵尺寸"));
|
||||||
ui.add_space(8.0);
|
ui.add_space(METRICS.item_gap);
|
||||||
ui.colored_label(ONE_DARK_PRO.text, "行");
|
ui.label(style::value_text("行"));
|
||||||
ui.add_sized(
|
ui.add_sized(
|
||||||
egui::vec2(56.0, 20.0),
|
egui::vec2(56.0, METRICS.field_height),
|
||||||
egui::DragValue::new(&mut config.rows).range(1..=128),
|
egui::DragValue::new(&mut config.rows).range(1..=128),
|
||||||
);
|
);
|
||||||
ui.colored_label(ONE_DARK_PRO.text, "列");
|
ui.label(style::value_text("列"));
|
||||||
ui.add_sized(
|
ui.add_sized(
|
||||||
egui::vec2(56.0, 20.0),
|
egui::vec2(56.0, METRICS.field_height),
|
||||||
egui::DragValue::new(&mut config.cols).range(1..=128),
|
egui::DragValue::new(&mut config.cols).range(1..=128),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.add_space(4.0);
|
ui.add_space(METRICS.item_gap);
|
||||||
|
|
||||||
// Presets
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.colored_label(dim_text(), "预设");
|
ui.label(style::field_label("预设"));
|
||||||
for (r, c, name) in &[
|
for (r, c, name) in &[
|
||||||
(12, 7, "12×7"),
|
(12, 7, "12×7"),
|
||||||
(24, 12, "24×12"),
|
(24, 12, "24×12"),
|
||||||
@@ -1231,31 +1228,40 @@ pub fn draw_matrix_config_panel(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.add_space(4.0);
|
ui.add_space(METRICS.item_gap);
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.colored_label(dim_text(), "色域范围");
|
ui.label(style::field_label("色域范围"));
|
||||||
ui.add_space(8.0);
|
ui.add_space(METRICS.item_gap);
|
||||||
ui.colored_label(ONE_DARK_PRO.text, "最小");
|
ui.label(style::value_text("最小"));
|
||||||
ui.add_sized(
|
ui.add_sized(
|
||||||
egui::vec2(72.0, 20.0),
|
egui::vec2(72.0, METRICS.field_height),
|
||||||
egui::DragValue::new(&mut config.color_min)
|
egui::DragValue::new(&mut config.color_min)
|
||||||
.range(0.0..=10000.0)
|
.range(0.0..=10000.0)
|
||||||
.speed(100.0),
|
.speed(100.0),
|
||||||
);
|
);
|
||||||
ui.colored_label(ONE_DARK_PRO.text, "最大");
|
ui.label(style::value_text("最大"));
|
||||||
ui.add_sized(
|
ui.add_sized(
|
||||||
egui::vec2(72.0, 20.0),
|
egui::vec2(72.0, METRICS.field_height),
|
||||||
egui::DragValue::new(&mut config.color_max)
|
egui::DragValue::new(&mut config.color_max)
|
||||||
.range(1.0..=10000.0)
|
.range(1.0..=10000.0)
|
||||||
.speed(100.0),
|
.speed(100.0),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.add_space(8.0);
|
ui.add_space(METRICS.row_gap);
|
||||||
|
|
||||||
if ui.add(tag_button("重置默认")).clicked() {
|
if ui.add(tag_button("重置默认")).clicked() {
|
||||||
*config = MatrixConfigState::default();
|
*config = MatrixConfigState::default();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn panel_restore_item(ui: &mut egui::Ui, title: &'static str, panel: &mut FloatingPanelState) {
|
||||||
|
if panel.visible {
|
||||||
|
ui.add_enabled(false, egui::Button::new(format!("{title} 已显示")));
|
||||||
|
} else if ui.button(format!("显示 {title}")).clicked() {
|
||||||
|
panel.visible = true;
|
||||||
|
ui.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user