diff --git a/src/app.rs b/src/app.rs index c5abde0..db090e8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -6,7 +6,7 @@ use crate::render::{ActiveMode, FingerMode, HandGatewayMode}; use crate::style::{self, ONE_DARK_PRO, apply_fonts, apply_theme, dim_text, layout}; use crate::ui::SerialMode; use crate::{ - matrix::{MATRIX_COLS, MATRIX_ROWS}, + matrix::{MATRIX_COLS, MATRIX_ROWS, UNFOLDED_SENSOR_SEGMENT_COUNTS}, render::{ BackgroundRenderResources, PRESSURE_CELL_COUNT, PressureFrame, PressureSamples, WgpuBackgroundCallback, @@ -21,8 +21,8 @@ use eframe::{egui, egui_wgpu}; use std::sync::Arc; const SUMMARY_POINTS_PER_SERIES: usize = 42; -const HAND_FORCE_PANEL_COUNT: usize = 7; -const HAND_FORCE_SEGMENT_COUNTS: [usize; HAND_FORCE_PANEL_COUNT] = [84, 84, 84, 84, 84, 70, 44]; +const HAND_FORCE_PANEL_COUNT: usize = UNFOLDED_SENSOR_SEGMENT_COUNTS.len(); +const HAND_FORCE_SEGMENT_COUNTS: [usize; HAND_FORCE_PANEL_COUNT] = UNFOLDED_SENSOR_SEGMENT_COUNTS; pub struct EskinDesktopApp { connect_panel: FloatingPanelState, @@ -678,7 +678,7 @@ fn draw_config_bar_mode(ui: &mut egui::Ui, config: &mut ConfigPanelState) -> Opt // if mode_button(ui, &mut config.mode, SerialMode::Finger3D, "3D指尖") { // changed_to = Some(SerialMode::Finger3D); // } - if mode_button(ui, &mut config.mode, SerialMode::Hand, "展示手掌") { + if mode_button(ui, &mut config.mode, SerialMode::Hand, "270°展开") { changed_to = Some(SerialMode::Hand); } }); diff --git a/src/matrix.rs b/src/matrix.rs index 03650fa..8cf8a00 100644 --- a/src/matrix.rs +++ b/src/matrix.rs @@ -1,5 +1,7 @@ pub const MATRIX_ROWS: u32 = 12; pub const MATRIX_COLS: u32 = 7; +pub const UNFOLDED_SENSOR_COUNT: usize = 104; +pub const UNFOLDED_SENSOR_SEGMENT_COUNTS: [usize; 10] = [8, 3, 5, 8, 10, 44, 3, 5, 8, 10]; const BASE_MATRIX_SPAN: f32 = 24.0; const MATRIX_SPAN_GROWTH: f32 = 0.6; diff --git a/src/render.rs b/src/render.rs index 9d25a2a..8101d4c 100644 --- a/src/render.rs +++ b/src/render.rs @@ -1,5 +1,5 @@ use crate::{ - matrix::{MatrixLayout, build_view_projection, glyph_world_position}, + matrix::{MatrixLayout, UNFOLDED_SENSOR_COUNT, build_view_projection, glyph_world_position}, model::{AlphaMode, InstanceRaw, ModelVertex, Vertex}, resources, texture, }; @@ -70,26 +70,96 @@ const HAND_TIP_MATRICES: [HandTipMatrix; 5] = [ }, ]; -const HAND_PALM_CHIPS: [HandPalmChip; 2] = [ +const UNFOLDED_CANVAS_SIZE: [f32; 2] = [850.0, 750.0]; +const UNFOLDED_CELL_SPACING_PX: f32 = 42.0; +const UNFOLDED_SENSOR_CHIPS: [HandPalmChip; 10] = [ + // Top cap: 2 rows x 4 columns. HandPalmChip { - center_px: [538.0, 608.0], - size_px: [248.0, 82.0], - angle_rad: 0.06, - rows: 5, - cols: 14, + center_px: [425.0, 165.0], + size_px: [176.0, 88.0], + angle_rad: 0.0, + rows: 2, + cols: 4, + sample_offset: 0, + }, + // Left wing, authored from the outside towards the 11x4 center block. + HandPalmChip { + center_px: [140.0, 578.0], + size_px: [46.0, 132.0], + angle_rad: 0.0, + rows: 3, + cols: 1, + sample_offset: 8, }, HandPalmChip { - center_px: [606.0, 780.0], - size_px: [72.0, 214.0], - angle_rad: 0.05, + center_px: [188.0, 534.0], + size_px: [46.0, 220.0], + angle_rad: 0.0, + rows: 5, + cols: 1, + sample_offset: 11, + }, + HandPalmChip { + center_px: [236.0, 468.0], + size_px: [46.0, 352.0], + angle_rad: 0.0, + rows: 8, + cols: 1, + sample_offset: 16, + }, + HandPalmChip { + center_px: [284.0, 424.0], + size_px: [46.0, 440.0], + angle_rad: 0.0, + rows: 10, + cols: 1, + sample_offset: 24, + }, + // Center spine: 11 rows x 4 columns. + HandPalmChip { + center_px: [425.0, 444.0], + size_px: [176.0, 484.0], + angle_rad: 0.0, rows: 11, cols: 4, + sample_offset: 34, + }, + // Right wing mirrors the left wing. Data remains ordered 3, 5, 8, 10. + HandPalmChip { + center_px: [710.0, 578.0], + size_px: [46.0, 132.0], + angle_rad: 0.0, + rows: 3, + cols: 1, + sample_offset: 78, + }, + HandPalmChip { + center_px: [662.0, 534.0], + size_px: [46.0, 220.0], + angle_rad: 0.0, + rows: 5, + cols: 1, + sample_offset: 81, + }, + HandPalmChip { + center_px: [614.0, 468.0], + size_px: [46.0, 352.0], + angle_rad: 0.0, + rows: 8, + cols: 1, + sample_offset: 86, + }, + HandPalmChip { + center_px: [566.0, 424.0], + size_px: [46.0, 440.0], + angle_rad: 0.0, + rows: 10, + cols: 1, + sample_offset: 94, }, ]; const HAND_FINGER_SENSOR_CELLS: usize = 12 * 7; -const HAND_PALM_HORIZONTAL_OFFSET: usize = HAND_FINGER_SENSOR_CELLS * 5; -const HAND_PALM_VERTICAL_OFFSET: usize = HAND_PALM_HORIZONTAL_OFFSET + 5 * 14; // Each entry pins one miniature matrix to a fingertip in hand.png. // Coordinates are authored in source-image pixels so they are easy to tune by eye. @@ -100,14 +170,15 @@ struct HandTipMatrix { angle_rad: f32, } -// Palm chips follow the hand layout: one horizontal 5x14 matrix and one vertical -// 11x4 matrix, rendered as dark inset chip tiles on the palm. +// One block in the flat 270-degree sensor layout. Coordinates use a dedicated +// 850x750 canvas so the unfolded shape stays prominent across window sizes. struct HandPalmChip { center_px: [f32; 2], size_px: [f32; 2], angle_rad: f32, rows: u32, cols: u32, + sample_offset: usize, } impl egui_wgpu::CallbackTrait for WgpuBackgroundCallback { @@ -156,7 +227,6 @@ pub struct BackgroundRenderResources { dot_pipeline: wgpu::RenderPipeline, hand_membrane_pipeline: wgpu::RenderPipeline, hand_dot_pipeline: wgpu::RenderPipeline, - hand_palm_chip_pipeline: wgpu::RenderPipeline, hand_palm_dot_pipeline: wgpu::RenderPipeline, hand_image_bind_group: wgpu::BindGroup, hand_image_texture: texture::Texture, @@ -168,8 +238,6 @@ pub struct BackgroundRenderResources { hand_membrane_instances: Vec, hand_dot_instance_buffer: wgpu::Buffer, hand_dot_instances: Vec, - hand_palm_chip_instance_buffer: wgpu::Buffer, - hand_palm_chip_instances: Vec, hand_palm_dot_instance_buffer: wgpu::Buffer, hand_palm_dot_instances: Vec, render_options: RenderOptions, @@ -277,10 +345,7 @@ impl BackgroundRenderResources { build_view_projection(1.0, &layout), surface_is_srgb, render_options, - [ - hand_image_texture.width as f32, - hand_image_texture.height as f32, - ], + UNFOLDED_CANVAS_SIZE, ); let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("Pressure Matrix Uniform Buffer"), @@ -497,8 +562,6 @@ impl BackgroundRenderResources { create_hand_membrane_pipeline(device, target_format, &shader, &pipeline_layout); let hand_dot_pipeline = create_hand_dot_pipeline(device, target_format, &shader, &pipeline_layout); - let hand_palm_chip_pipeline = - create_hand_palm_chip_pipeline(device, target_format, &shader, &pipeline_layout); let hand_palm_dot_pipeline = create_hand_palm_dot_pipeline(device, target_format, &shader, &pipeline_layout); @@ -551,23 +614,8 @@ impl BackgroundRenderResources { contents: bytemuck::cast_slice(&hand_dot_instances), usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, }); - let hand_palm_chip_instances = build_hand_palm_chip_instances( - hand_image_texture.width as f32, - hand_image_texture.height as f32, - ); - let hand_palm_chip_instance_buffer = - device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("Hand Palm Chip Instance Buffer"), - contents: bytemuck::cast_slice(&hand_palm_chip_instances), - usage: wgpu::BufferUsages::VERTEX, - }); - let hand_palm_dot_instances = build_hand_palm_dot_instances( - rows, - cols, - hand_image_texture.width as f32, - hand_image_texture.height as f32, - &[[0.0, 0.0]; PRESSURE_CELL_COUNT], - ); + let hand_palm_dot_instances = + build_hand_palm_dot_instances(rows, cols, &[[0.0, 0.0]; PRESSURE_CELL_COUNT]); let hand_palm_dot_instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("Hand Palm Chip Dot Instance Buffer"), @@ -589,7 +637,6 @@ impl BackgroundRenderResources { dot_pipeline, hand_membrane_pipeline, hand_dot_pipeline, - hand_palm_chip_pipeline, hand_palm_dot_pipeline, hand_image_bind_group, hand_image_texture, @@ -600,8 +647,6 @@ impl BackgroundRenderResources { hand_membrane_instances, hand_dot_instance_buffer, hand_dot_instances, - hand_palm_chip_instance_buffer, - hand_palm_chip_instances, hand_palm_dot_instance_buffer, hand_palm_dot_instances, render_options, @@ -623,10 +668,7 @@ impl BackgroundRenderResources { build_view_projection(aspect, &self.layout), self.surface_is_srgb, self.render_options, - [ - self.hand_image_texture.width as f32, - self.hand_image_texture.height as f32, - ], + UNFOLDED_CANVAS_SIZE, ); queue.write_buffer( &self.uniform_buffer, @@ -653,8 +695,8 @@ impl BackgroundRenderResources { hand_pressure }; - // Hand mode uses UV-anchored fingertip matrices over hand.png. - // Rebuild their instance positions here so pressure colors update every frame. + // Keep legacy fingertip buffers current while both hardware modes share + // the same renderer resources. self.hand_dot_instances = build_hand_dot_instances( self.rows, self.cols, @@ -668,15 +710,9 @@ impl BackgroundRenderResources { bytemuck::cast_slice(&self.hand_dot_instances), ); - // Palm chips reuse the same live 12x7 pressure frame, but draw it as - // embedded micro-pixels inside dark chip tiles. - self.hand_palm_dot_instances = build_hand_palm_dot_instances( - self.rows, - self.cols, - self.hand_image_texture.width as f32, - self.hand_image_texture.height as f32, - hand_pressure, - ); + // Rebuild the 104-cell unfolded layout with the latest gateway samples. + self.hand_palm_dot_instances = + build_hand_palm_dot_instances(self.rows, self.cols, hand_pressure); queue.write_buffer( &self.hand_palm_dot_instance_buffer, 0, @@ -692,12 +728,7 @@ impl BackgroundRenderResources { match active_mode { ActiveMode::Finger(mode) => self.paint_finger(render_pass, mode), - ActiveMode::Hand(mode) => { - render_pass.set_pipeline(&self.hand_image_pipeline); - render_pass.set_bind_group(1, &self.hand_image_bind_group, &[]); - render_pass.draw(0..6, 0..1); - self.paint_hand(render_pass, mode); - } + ActiveMode::Hand(mode) => self.paint_hand(render_pass, mode), } } @@ -719,22 +750,7 @@ impl BackgroundRenderResources { fn paint_hand(&self, render_pass: &mut wgpu::RenderPass<'_>, mode: &HandGatewayMode) { let _range = mode.range.clone(); - // First draw the translucent sensor membranes, then draw live pressure beads on their grid. - render_pass.set_pipeline(&self.hand_membrane_pipeline); - render_pass.set_vertex_buffer(0, self.glyph_vertex_buffer.slice(..)); - render_pass.set_vertex_buffer(1, self.hand_membrane_instance_buffer.slice(..)); - render_pass.draw(0..6, 0..self.hand_membrane_instances.len() as u32); - - render_pass.set_pipeline(&self.hand_dot_pipeline); - render_pass.set_vertex_buffer(0, self.glyph_vertex_buffer.slice(..)); - render_pass.set_vertex_buffer(1, self.hand_dot_instance_buffer.slice(..)); - render_pass.draw(0..6, 0..self.hand_dot_instances.len() as u32); - - render_pass.set_pipeline(&self.hand_palm_chip_pipeline); - render_pass.set_vertex_buffer(0, self.glyph_vertex_buffer.slice(..)); - render_pass.set_vertex_buffer(1, self.hand_palm_chip_instance_buffer.slice(..)); - render_pass.draw(0..6, 0..self.hand_palm_chip_instances.len() as u32); - + // Match Finger mode: draw only the 104 independent pressure dots. render_pass.set_pipeline(&self.hand_palm_dot_pipeline); render_pass.set_vertex_buffer(0, self.glyph_vertex_buffer.slice(..)); render_pass.set_vertex_buffer(1, self.hand_palm_dot_instance_buffer.slice(..)); @@ -1072,39 +1088,6 @@ fn create_hand_dot_pipeline( }) } -fn create_hand_palm_chip_pipeline( - device: &wgpu::Device, - target_format: &wgpu::TextureFormat, - shader: &wgpu::ShaderModule, - layout: &wgpu::PipelineLayout, -) -> wgpu::RenderPipeline { - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("Hand Palm Embedded Chip Pipeline"), - layout: Some(layout), - vertex: wgpu::VertexState { - module: shader, - entry_point: Some("vs_hand_palm_chip"), - compilation_options: Default::default(), - buffers: &[GlyphVertex::desc(), GlyphInstance::desc()], - }, - fragment: Some(wgpu::FragmentState { - module: shader, - entry_point: Some("fs_hand_palm_chip"), - compilation_options: Default::default(), - targets: &[Some(wgpu::ColorTargetState { - format: *target_format, - blend: Some(wgpu::BlendState::ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - })], - }), - primitive: wgpu::PrimitiveState::default(), - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - multiview_mask: None, - cache: None, - }) -} - fn create_hand_palm_dot_pipeline( device: &wgpu::Device, target_format: &wgpu::TextureFormat, @@ -1156,28 +1139,6 @@ fn build_hand_membrane_instances(image_width: f32, image_height: f32) -> Vec Vec { - HAND_PALM_CHIPS - .iter() - .map(|chip| { - let uv_x = chip.center_px[0] / image_width.max(1.0); - let uv_y = chip.center_px[1] / image_height.max(1.0); - - GlyphInstance { - // Palm chip shaders read xy as hand.png UV. - world_position: [uv_x, uv_y, 0.0, 1.0], - // Store chip angle, source-image pixel size, and matrix shape for the shader grid. - style: [ - chip.angle_rad, - chip.size_px[0], - chip.size_px[1], - (chip.rows * 100 + chip.cols) as f32, - ], - } - }) - .collect() -} - fn build_hand_dot_instances( rows: u32, cols: u32, @@ -1225,44 +1186,27 @@ fn build_hand_dot_instances( fn build_hand_palm_dot_instances( _rows: u32, _cols: u32, - image_width: f32, - image_height: f32, pressure: &[[f32; 2]], ) -> Vec { - let chip_dot_count: usize = HAND_PALM_CHIPS + let chip_dot_count: usize = UNFOLDED_SENSOR_CHIPS .iter() .map(|chip| (chip.rows * chip.cols) as usize) .sum(); + debug_assert_eq!(chip_dot_count, UNFOLDED_SENSOR_COUNT); let mut instances = Vec::with_capacity(chip_dot_count); - for (chip_index, chip) in HAND_PALM_CHIPS.into_iter().enumerate() { - let cos = chip.angle_rad.cos(); - let sin = chip.angle_rad.sin(); - // Leave a bevel around the chip so the matrix reads as embedded pixels. - let active_size = [chip.size_px[0] * 0.72, chip.size_px[1] * 0.76]; - + for chip in UNFOLDED_SENSOR_CHIPS { for row in 0..chip.rows { for col in 0..chip.cols { let index = (row * chip.cols + col) as usize; - let offset = match chip_index { - 0 => HAND_PALM_HORIZONTAL_OFFSET, - _ => HAND_PALM_VERTICAL_OFFSET, - }; let [normalized, display_value] = - sample_pressure_at(pressure, offset + index, index); - - let local_x = - (col as f32 - chip.cols as f32 / 2.0 + 0.5) / chip.cols as f32 * active_size[0]; - let local_y = - (row as f32 - chip.rows as f32 / 2.0 + 0.5) / chip.rows as f32 * active_size[1]; - - let x = chip.center_px[0] + local_x * cos - local_y * sin; - let y = chip.center_px[1] + local_x * sin + local_y * cos; + sample_pressure_at(pressure, chip.sample_offset + index, index); + let [x, y] = unfolded_cell_position(&chip, row, col); instances.push(GlyphInstance { world_position: [ - x / image_width.max(1.0), - y / image_height.max(1.0), + x / UNFOLDED_CANVAS_SIZE[0], + y / UNFOLDED_CANVAS_SIZE[1], 0.0, 1.0, ], @@ -1275,6 +1219,22 @@ fn build_hand_palm_dot_instances( instances } +fn unfolded_cell_position(chip: &HandPalmChip, row: u32, col: u32) -> [f32; 2] { + let cos = chip.angle_rad.cos(); + let sin = chip.angle_rad.sin(); + // Author every region on the same point grid. Using the last row as the + // vertical anchor keeps the stepped wing columns exactly bottom-aligned. + let bottom_center_y = chip.center_px[1] + chip.size_px[1] * 0.5 - 22.0; + let local_x = (col as f32 - chip.cols as f32 / 2.0 + 0.5) * UNFOLDED_CELL_SPACING_PX; + let y_from_bottom = (chip.rows.saturating_sub(row + 1)) as f32 * UNFOLDED_CELL_SPACING_PX; + let local_y = -y_from_bottom; + + [ + chip.center_px[0] + local_x * cos - local_y * sin, + bottom_center_y + local_x * sin + local_y * cos, + ] +} + fn sample_pressure_at(pressure: &[[f32; 2]], index: usize, fallback_index: usize) -> [f32; 2] { pressure .get(index) @@ -1283,6 +1243,122 @@ fn sample_pressure_at(pressure: &[[f32; 2]], index: usize, fallback_index: usize .unwrap_or([0.0, 0.0]) } +#[cfg(test)] +mod unfolded_layout_tests { + use super::*; + use crate::matrix::UNFOLDED_SENSOR_SEGMENT_COUNTS; + + #[test] + fn unfolded_layout_has_104_contiguous_samples() { + let mut expected_offset = 0usize; + + for chip in UNFOLDED_SENSOR_CHIPS { + assert_eq!(chip.sample_offset, expected_offset); + expected_offset += (chip.rows * chip.cols) as usize; + } + + assert_eq!(expected_offset, UNFOLDED_SENSOR_COUNT); + } + + #[test] + fn unfolded_layout_matches_requested_shapes() { + let shapes = UNFOLDED_SENSOR_CHIPS.map(|chip| (chip.rows, chip.cols)); + let sample_counts = UNFOLDED_SENSOR_CHIPS.map(|chip| (chip.rows * chip.cols) as usize); + + assert_eq!( + shapes, + [ + (2, 4), + (3, 1), + (5, 1), + (8, 1), + (10, 1), + (11, 4), + (3, 1), + (5, 1), + (8, 1), + (10, 1), + ] + ); + assert_eq!(sample_counts, UNFOLDED_SENSOR_SEGMENT_COUNTS); + } + + #[test] + fn unfolded_wings_are_mirrored_around_center() { + let center_x = UNFOLDED_SENSOR_CHIPS[5].center_px[0]; + + for (left, right) in UNFOLDED_SENSOR_CHIPS[1..5] + .iter() + .zip(UNFOLDED_SENSOR_CHIPS[6..10].iter()) + { + assert_eq!((left.rows, left.cols), (right.rows, right.cols)); + assert_eq!(left.center_px[1], right.center_px[1]); + assert!( + ((center_x - left.center_px[0]) - (right.center_px[0] - center_x)).abs() < 0.01 + ); + } + } + + #[test] + fn unfolded_layout_fits_its_canvas() { + for chip in UNFOLDED_SENSOR_CHIPS { + let half_width = chip.size_px[0] * 0.5; + let half_height = chip.size_px[1] * 0.5; + + assert!(chip.center_px[0] - half_width >= 0.0); + assert!(chip.center_px[0] + half_width <= UNFOLDED_CANVAS_SIZE[0]); + assert!(chip.center_px[1] - half_height >= 0.0); + assert!(chip.center_px[1] + half_height <= UNFOLDED_CANVAS_SIZE[1]); + } + } + + #[test] + fn unfolded_wing_rows_share_one_bottom_baseline() { + let wing_indices = [1usize, 2, 3, 4, 6, 7, 8, 9]; + let expected_y = unfolded_cell_position( + &UNFOLDED_SENSOR_CHIPS[wing_indices[0]], + UNFOLDED_SENSOR_CHIPS[wing_indices[0]].rows - 1, + 0, + )[1]; + + for index in wing_indices { + let chip = &UNFOLDED_SENSOR_CHIPS[index]; + let bottom_y = unfolded_cell_position(chip, chip.rows - 1, 0)[1]; + assert!((bottom_y - expected_y).abs() < 0.01); + } + } + + #[test] + fn unfolded_wings_are_compact_with_more_space_at_center() { + let left_x = UNFOLDED_SENSOR_CHIPS[1..5] + .iter() + .map(|chip| chip.center_px[0]) + .collect::>(); + let wing_gap = left_x[1] - left_x[0]; + assert!((wing_gap - 48.0).abs() < 0.01); + assert!( + left_x + .windows(2) + .all(|pair| (pair[1] - pair[0] - wing_gap).abs() < 0.01) + ); + + let center_left_x = unfolded_cell_position(&UNFOLDED_SENSOR_CHIPS[5], 0, 0)[0]; + let left_inner_x = unfolded_cell_position(&UNFOLDED_SENSOR_CHIPS[4], 0, 0)[0]; + assert!(center_left_x - left_inner_x > wing_gap); + } + + #[test] + fn unfolded_top_block_sits_close_to_center_block() { + let top = &UNFOLDED_SENSOR_CHIPS[0]; + let center = &UNFOLDED_SENSOR_CHIPS[5]; + let top_bottom_y = unfolded_cell_position(top, top.rows - 1, 0)[1]; + let center_top_y = unfolded_cell_position(center, 0, 0)[1]; + + assert!(center_top_y > top_bottom_y); + assert!(center_top_y - top_bottom_y < UNFOLDED_CELL_SPACING_PX * 2.0); + } +} + fn build_glyph_instances( rows: u32, cols: u32, diff --git a/src/serial_core/codecs/hand_gateway.rs b/src/serial_core/codecs/hand_gateway.rs index 8dabe55..46f61db 100644 --- a/src/serial_core/codecs/hand_gateway.rs +++ b/src/serial_core/codecs/hand_gateway.rs @@ -127,14 +127,14 @@ pub struct HandGatewayCodec { } impl HandGatewayCodec { - pub fn new(node_sample_counts: &[u16]) -> Self { + pub fn new(node_sample_counts: &[usize]) -> Self { let nodes = node_sample_counts .iter() .enumerate() .map(|(index, &sample_count)| { HandGatewayNodeConfig::new( 1u32.checked_shl(index as u32).unwrap_or(0), - sample_count as usize, + sample_count, ) }) .collect(); diff --git a/src/serial_core/serial.rs b/src/serial_core/serial.rs index bc55360..0cc037d 100644 --- a/src/serial_core/serial.rs +++ b/src/serial_core/serial.rs @@ -1,3 +1,4 @@ +use crate::matrix::UNFOLDED_SENSOR_SEGMENT_COUNTS; use crate::recording::Recorder; use crate::serial_core::codec::Codec; use crate::serial_core::codecs::hand_gateway::{HandGatewayCodec, HandGatewayFrame}; @@ -8,7 +9,9 @@ use std::io::{Read, Write}; use std::time::{Duration, Instant}; const POLL_INTERVAL_MS: u64 = 5; -const DEFAULT_HAND_GATEWAY_NODE_SAMPLE_COUNTS: &[u16] = &[84, 84, 84, 84, 84, 70, 44]; +// 270-degree flat layout: +// top 2x4, left 3/5/8/10x1, center 11x4, right 3/5/8/10x1. +const DEFAULT_HAND_GATEWAY_NODE_SAMPLE_COUNTS: &[usize] = &UNFOLDED_SENSOR_SEGMENT_COUNTS; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub struct SerialIoStats { diff --git a/src/ui.rs b/src/ui.rs index 432e334..7c49a8c 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -483,7 +483,7 @@ fn draw_config_bar_mode(ui: &mut egui::Ui, config: &mut ConfigPanelState) -> Opt if mode_button(ui, &mut config.mode, SerialMode::Finger, "指尖模块") { changed_to = Some(SerialMode::Finger) } - if mode_button(ui, &mut config.mode, SerialMode::Hand, "手掌模块") { + if mode_button(ui, &mut config.mode, SerialMode::Hand, "270°展开") { changed_to = Some(SerialMode::Hand) } // mode_button(ui, &mut config.mode, SerialMode::Model, "模型"); @@ -969,14 +969,17 @@ const HAND_FORCE_PANEL_MAX_HEIGHT: f32 = 190.0; const HAND_FORCE_PANEL_GAP: f32 = 12.0; const HAND_FORCE_PANEL_SIDE_MARGIN: f32 = 24.0; const HAND_FORCE_PANEL_VERTICAL_MARGIN: f32 = 28.0; -const HAND_FORCE_PANEL_TITLES: [(&str, &str); 7] = [ - ("T1", "拇指"), - ("T2", "食指"), - ("T3", "中指"), - ("T4", "无名指"), - ("T5", "小指"), - ("P1", "掌心横区"), - ("P2", "掌心纵区"), +const HAND_FORCE_PANEL_TITLES: [(&str, &str); 10] = [ + ("T", "顶部 2×4"), + ("L3", "左侧 3×1"), + ("L5", "左侧 5×1"), + ("L8", "左侧 8×1"), + ("L10", "左侧 10×1"), + ("C", "中央 11×4"), + ("R3", "右侧 3×1"), + ("R5", "右侧 5×1"), + ("R8", "右侧 8×1"), + ("R10", "右侧 10×1"), ]; fn has_recent_resultant_force(values: &[f32]) -> bool { @@ -1002,7 +1005,7 @@ pub fn draw_hand_force_panels(ctx: &egui::Context, visible: bool, histories: &[V .max(HAND_FORCE_PANEL_MIN_WIDTH.min(side_width)); let left_x = screen.left() + side_margin; let right_x = screen.right() - side_margin - panel_width; - let left_count = 4usize; + let left_count = 5usize; let right_count = HAND_FORCE_PANEL_TITLES.len() - left_count; let available_height = (screen.height() - layout::TITLE_BAR_HEIGHT - vertical_margin * 2.0).max(0.0); diff --git a/static/wgsl/shader.wgsl b/static/wgsl/shader.wgsl index 1797361..1a95df0 100644 --- a/static/wgsl/shader.wgsl +++ b/static/wgsl/shader.wgsl @@ -349,10 +349,8 @@ fn circle_alpha(local: vec2f, radius: f32, softness: f32) -> f32 { return 1.0 - smoothstep(radius, radius + softness, dist); } -// Convert a point authored in hand.png UV space into clip space. -// This mirrors fs_hand_image's aspect-fit math, so fingertip dots stay attached -// to the same image pixels when the app window changes shape. -fn hand_image_uv_to_clip(image_uv: vec2f) -> vec2f { +// Convert a point authored in sensor-canvas UV space into aspect-fitted clip space. +fn sensor_canvas_uv_to_clip(image_uv: vec2f) -> vec2f { let viewport_aspect = u.viewport.x / max(u.viewport.y, 1.0); let image_aspect = u.image.x / max(u.image.y, 1.0); @@ -420,7 +418,7 @@ fn vs_hand_membrane(vertex: DotVertexInput, instance: DotInstanceInput) -> HandM let image_uv = (center_px + rotate_2d(local_px, angle)) / max(u.image.xy, vec2f(1.0, 1.0)); var out: HandMembraneVertexOutput; - out.clip_position = vec4f(hand_image_uv_to_clip(image_uv), 0.0, 1.0); + out.clip_position = vec4f(sensor_canvas_uv_to_clip(image_uv), 0.0, 1.0); out.local = vertex.local; return out; } @@ -470,7 +468,7 @@ fn vs_hand_dot(vertex: DotVertexInput, instance: DotInstanceInput) -> DotVertexO let shaped = smoothstep(0.0, 1.0, intensity); // Hand instances store hand.png UV in world_position.xy instead of 3D world space. - let center = hand_image_uv_to_clip(instance.world_position.xy); + let center = sensor_canvas_uv_to_clip(instance.world_position.xy); // Hand fingertip matrices are much smaller than the full Finger view. // Keep each bead below the local cell spacing so the 12x7 matrix remains visibly separated. let pixel_size = u.glyph.x * mix(0.22, 0.34, shaped); @@ -500,75 +498,14 @@ fn fs_hand_dot(in: DotVertexOutput) -> @location(0) vec4f { return output_color(color, max(core, halo)); } -fn chip_pixel_alpha(local: vec2f, half_size: f32, softness: f32) -> f32 { - let q = abs(local) - vec2f(half_size, half_size); - let dist = length(max(q, vec2f(0.0, 0.0))) + min(max(q.x, q.y), 0.0); - return 1.0 - smoothstep(0.0, softness, dist); -} - -struct HandPalmChipVertexOutput { - @builtin(position) clip_position: vec4f, - @location(0) local: vec2f, - @location(1) grid: vec2f, -} - -@vertex -fn vs_hand_palm_chip(vertex: DotVertexInput, instance: DotInstanceInput) -> HandPalmChipVertexOutput { - let center_px = instance.world_position.xy * u.image.xy; - let size_px = instance.style.yz; - let angle = instance.style.x; - let packed_shape = instance.style.w; - let shape_rows = floor(packed_shape / 100.0); - let shape_cols = max(packed_shape - shape_rows * 100.0, 1.0); - let local_px = vertex.local * size_px * 0.5; - let image_uv = (center_px + rotate_2d(local_px, angle)) / max(u.image.xy, vec2f(1.0, 1.0)); - - var out: HandPalmChipVertexOutput; - out.clip_position = vec4f(hand_image_uv_to_clip(image_uv), 0.0, 1.0); - out.local = vertex.local; - out.grid = vec2f(shape_cols, max(shape_rows, 1.0)); - return out; -} - -@fragment -fn fs_hand_palm_chip(in: HandPalmChipVertexOutput) -> @location(0) vec4f { - // Dark rounded tile: this is the inset chip body sitting inside the palm surface. - let panel = rounded_rect_alpha(in.local, 0.10, 0.040); - let inset = rounded_rect_alpha(in.local * vec2f(1.10, 1.08), 0.08, 0.052); - let rim = clamp(panel - inset * 0.72, 0.0, 1.0); - - // Inactive chip pixels use the chip's real hand layout: - // horizontal 14 columns x 5 rows, or vertical 4 columns x 11 rows. - let uv = clamp(in.local * 0.5 + vec2f(0.5, 0.5), vec2f(0.0, 0.0), vec2f(1.0, 1.0)); - let cell = abs(fract(uv * in.grid) - vec2f(0.5, 0.5)); - let micro_pixel = 1.0 - smoothstep(0.105, 0.178, length(cell * vec2f(1.04, 0.94))); - - let top_bevel = smoothstep(-0.96, -0.18, -in.local.y) * 0.16; - let lower_shadow = smoothstep(0.20, 0.92, in.local.y) * 0.22; - let side_bevel = smoothstep(0.58, 0.96, abs(in.local.x)) * 0.12; - let scan = (0.5 + 0.5 * sin((uv.y * 36.0 + uv.x * 7.0) * 6.28318)) * 0.026; - - let base = vec3f(0.004, 0.012, 0.018); - let glass = vec3f(0.012, 0.048, 0.064); - let pixel_color = vec3f(0.075, 0.300, 0.360); - let rim_color = vec3f(0.060, 0.560, 0.670); - let color = base * (0.92 - lower_shadow) - + glass * (0.52 + top_bevel + side_bevel + scan) - + pixel_color * micro_pixel * 0.70 - + rim_color * rim * 0.60; - - let alpha = panel * (0.64 + micro_pixel * 0.18 + rim * 0.20); - return output_color(color, alpha); -} - @vertex fn vs_hand_palm_dot(vertex: DotVertexInput, instance: DotInstanceInput) -> DotVertexOutput { let intensity = saturate(instance.style.x); let shaped = smoothstep(0.0, 1.0, intensity); - // Palm chip pixels are deliberately smaller than fingertip beads so they read as a chip matrix. - let center = hand_image_uv_to_clip(instance.world_position.xy); - let pixel_size = u.glyph.x * mix(0.13, 0.25, shaped); + // Use the same on-screen point size as Finger mode. + let center = sensor_canvas_uv_to_clip(instance.world_position.xy); + let pixel_size = u.glyph.x * mix(1.07, 2.23, shaped); let ndc_offset = vertex.local * vec2f(pixel_size / u.viewport.x, pixel_size / u.viewport.y) * 2.0; var out: DotVertexOutput; @@ -581,16 +518,11 @@ fn vs_hand_palm_dot(vertex: DotVertexInput, instance: DotInstanceInput) -> DotVe @fragment fn fs_hand_palm_dot(in: DotVertexOutput) -> @location(0) vec4f { let intensity = saturate(in.intensity); - let pixel = chip_pixel_alpha(in.local, 0.52, 0.070); - let glow = circle_alpha(in.local, 0.95, 0.22) * intensity * 0.36; + let base_color = sample_range_color(intensity); - let cold = vec3f(0.070, 0.340, 0.360); - let gradient = sample_range_color(intensity); - let color = mix(cold, gradient, smoothstep(0.0, 0.20, intensity)) - * (0.58 + intensity * 1.04) - + gradient * glow * 0.72; + let alpha = circle_alpha(in.local, 0.46, 0.045); + let color = base_color * mix(0.86, 1.06, intensity); - let alpha = max(pixel * (0.20 + intensity * 0.76), glow); return output_color(color, alpha); }