Compare commits
4 Commits
customer-d
...
0521-4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2828eaafb5 | ||
|
|
821800beb1 | ||
|
|
8533747670 | ||
|
|
160ff54368 |
32
docs/CHANGELOG.md
Normal file
32
docs/CHANGELOG.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# 更新日志
|
||||
|
||||
## 2026-06-09 - 送样分支
|
||||
|
||||
分支:`sample-delivery`
|
||||
|
||||
提交:`160ff54 Prepare sample delivery UI`
|
||||
|
||||
### 显示与交互
|
||||
|
||||
- 合力曲线改为 Canvas 渲染,保留原有 HUD 风格。
|
||||
- 合力曲线 panel 宽度调整为与三维力 panel 一致。
|
||||
- 卸力后合力曲线继续接收并渲染后端 0 值数据,panel 延迟 5 秒消失。
|
||||
- 5 秒内重新施力会取消合力曲线的消失计时。
|
||||
- 三维力 panel 改为卸力后延迟 5 秒消失。
|
||||
- 三维力箭头仍然实时跟随后端数据,卸力后箭头立即消失,不冻结最后方向。
|
||||
|
||||
### 后端数据
|
||||
|
||||
|
||||
- 合力低于显示阈值时,后端不再清空 summary 数组,而是持续写入 `0.0`。
|
||||
- 压力矩阵在低于显示阈值时仍然清零。
|
||||
- 前端可以通过 summary 数组是否存在和最新值大小来判断释放状态。
|
||||
|
||||
### 显示范围
|
||||
|
||||
- 合力与压力数值显示增加最大显示值处理:超过 `25.6` 时显示为 `25.6+`。
|
||||
|
||||
### 验证
|
||||
|
||||
- `npm.cmd run check` 通过,0 errors,保留 13 个既有 warnings。
|
||||
- `cargo check` 通过。
|
||||
104
package-lock.json
generated
104
package-lock.json
generated
@@ -9,7 +9,7 @@
|
||||
"version": "0.4.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/api": "^2.11.0",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"@tauri-apps/plugin-process": "^2.3.1",
|
||||
"@tauri-apps/plugin-updater": "^2.10.1",
|
||||
@@ -19,7 +19,7 @@
|
||||
"@sveltejs/adapter-static": "^3.0.6",
|
||||
"@sveltejs/kit": "^2.9.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@tauri-apps/cli": "^2.11.2",
|
||||
"@types/three": "^0.183.1",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
@@ -1032,9 +1032,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/api": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz",
|
||||
"integrity": "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==",
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.0.tgz",
|
||||
"integrity": "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -1042,9 +1042,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.1.tgz",
|
||||
"integrity": "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g==",
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.2.tgz",
|
||||
"integrity": "sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"bin": {
|
||||
@@ -1058,23 +1058,23 @@
|
||||
"url": "https://opencollective.com/tauri"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tauri-apps/cli-darwin-arm64": "2.10.1",
|
||||
"@tauri-apps/cli-darwin-x64": "2.10.1",
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1",
|
||||
"@tauri-apps/cli-linux-arm64-gnu": "2.10.1",
|
||||
"@tauri-apps/cli-linux-arm64-musl": "2.10.1",
|
||||
"@tauri-apps/cli-linux-riscv64-gnu": "2.10.1",
|
||||
"@tauri-apps/cli-linux-x64-gnu": "2.10.1",
|
||||
"@tauri-apps/cli-linux-x64-musl": "2.10.1",
|
||||
"@tauri-apps/cli-win32-arm64-msvc": "2.10.1",
|
||||
"@tauri-apps/cli-win32-ia32-msvc": "2.10.1",
|
||||
"@tauri-apps/cli-win32-x64-msvc": "2.10.1"
|
||||
"@tauri-apps/cli-darwin-arm64": "2.11.2",
|
||||
"@tauri-apps/cli-darwin-x64": "2.11.2",
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": "2.11.2",
|
||||
"@tauri-apps/cli-linux-arm64-gnu": "2.11.2",
|
||||
"@tauri-apps/cli-linux-arm64-musl": "2.11.2",
|
||||
"@tauri-apps/cli-linux-riscv64-gnu": "2.11.2",
|
||||
"@tauri-apps/cli-linux-x64-gnu": "2.11.2",
|
||||
"@tauri-apps/cli-linux-x64-musl": "2.11.2",
|
||||
"@tauri-apps/cli-win32-arm64-msvc": "2.11.2",
|
||||
"@tauri-apps/cli-win32-ia32-msvc": "2.11.2",
|
||||
"@tauri-apps/cli-win32-x64-msvc": "2.11.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-arm64": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.1.tgz",
|
||||
"integrity": "sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ==",
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.2.tgz",
|
||||
"integrity": "sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1089,9 +1089,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-x64": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.1.tgz",
|
||||
"integrity": "sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw==",
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.2.tgz",
|
||||
"integrity": "sha512-VjYYtZUPqDMLutSfJEyxFE3Bz+DPi7c8wC3imckgvciLDZLq4qwKJxBicg0BXGhXjJsl8vKWgWRFNMPELQ+Xyg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1106,9 +1106,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.1.tgz",
|
||||
"integrity": "sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w==",
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.2.tgz",
|
||||
"integrity": "sha512-yMemD6f4i95AQriS8EazyOFzbE34yjnP16i3IOzpHGQvBoy2DjypFMFBq0NtPuITURv/cOGguRtHR5d79/9CSA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1123,9 +1123,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.1.tgz",
|
||||
"integrity": "sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA==",
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.2.tgz",
|
||||
"integrity": "sha512-cgI91D2wL8GSgoWwZXDqt+DwnuZCP2/bz03QAE4TrhgAKIsrB4hX26W/H1EONPUUNkqrsgeCD0wU6pcNjV/5kw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1143,9 +1143,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.1.tgz",
|
||||
"integrity": "sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg==",
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.2.tgz",
|
||||
"integrity": "sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1163,9 +1163,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.1.tgz",
|
||||
"integrity": "sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw==",
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.2.tgz",
|
||||
"integrity": "sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -1183,9 +1183,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.1.tgz",
|
||||
"integrity": "sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw==",
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.2.tgz",
|
||||
"integrity": "sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1203,9 +1203,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-musl": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.1.tgz",
|
||||
"integrity": "sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ==",
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.2.tgz",
|
||||
"integrity": "sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1223,9 +1223,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.1.tgz",
|
||||
"integrity": "sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg==",
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.2.tgz",
|
||||
"integrity": "sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1240,9 +1240,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.1.tgz",
|
||||
"integrity": "sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw==",
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.2.tgz",
|
||||
"integrity": "sha512-YhjQNZcXfbkCLyazSv1nPnJ9iRFE1wm6kc51FDbU10/Dk09io+6PAGMLjkxnX2GdM0qMnDmTjstY8mTDVvtKeA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -1257,9 +1257,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.1.tgz",
|
||||
"integrity": "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg==",
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.2.tgz",
|
||||
"integrity": "sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/api": "^2.11.0",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"@tauri-apps/plugin-process": "^2.3.1",
|
||||
"@tauri-apps/plugin-updater": "^2.10.1",
|
||||
@@ -25,7 +25,7 @@
|
||||
"@sveltejs/adapter-static": "^3.0.6",
|
||||
"@sveltejs/kit": "^2.9.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@tauri-apps/cli": "^2.11.2",
|
||||
"@types/three": "^0.183.1",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
|
||||
@@ -16,6 +16,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
default = ["multi-dim"]
|
||||
debug = []
|
||||
devkit = ["dep:tonic", "dep:prost", "dep:prost-types", "dep:async-stream", "dep:dirs"]
|
||||
multi-dim = ["dep:ndarray"]
|
||||
|
||||
|
||||
@@ -122,8 +122,7 @@ pub fn serial_enum() -> Result<Vec<String>, SerialError> {
|
||||
.into_iter()
|
||||
.filter_map(|p| {
|
||||
let name = p.port_name;
|
||||
#[cfg(unix)]
|
||||
if !name.contains("USB") {
|
||||
if !should_include_serial_port(&name) {
|
||||
return None;
|
||||
}
|
||||
Some(name)
|
||||
@@ -133,6 +132,21 @@ pub fn serial_enum() -> Result<Vec<String>, SerialError> {
|
||||
Ok(ports)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn should_include_serial_port(name: &str) -> bool {
|
||||
name.to_ascii_lowercase().contains("usb")
|
||||
}
|
||||
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
fn should_include_serial_port(name: &str) -> bool {
|
||||
name.contains("USB")
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn should_include_serial_port(_name: &str) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn serial_connect(
|
||||
app: AppHandle,
|
||||
@@ -185,7 +199,7 @@ pub async fn serial_connect(
|
||||
)
|
||||
.await
|
||||
{
|
||||
eprintln!("serial task exited with error: {error}");
|
||||
log::error!("serial task exited with error: {error}");
|
||||
}
|
||||
|
||||
let manager = task_app.state::<SerialConnectionState>();
|
||||
|
||||
@@ -62,7 +62,6 @@ impl Codec<TestFrame> for TestCodec {
|
||||
continue;
|
||||
}
|
||||
let dts = elapsed_millis(session_started_at);
|
||||
println!("dts_ms: {dts}");
|
||||
frames.push(TestFrame {
|
||||
header: [0xAA, 0x55],
|
||||
cmd: cmd,
|
||||
@@ -244,11 +243,10 @@ mod tests {
|
||||
fn test_read_csv_basic() -> anyhow::Result<()> {
|
||||
let mut rdr = Reader::from_path("recording_20260329_125238.csv")?;
|
||||
let headers = rdr.headers()?;
|
||||
println!("headers: {:?}", headers);
|
||||
|
||||
for result in rdr.records() {
|
||||
let record = result?;
|
||||
println!("record: {:?}", record);
|
||||
let _ = record;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -118,6 +118,10 @@ impl HudChartState {
|
||||
push_summary_point(&mut self.summary_points, value);
|
||||
}
|
||||
|
||||
pub fn clear_summary(&mut self) {
|
||||
self.summary_points.clear();
|
||||
}
|
||||
|
||||
pub fn record_pressure_matrix(&mut self, values: &[i32]) {
|
||||
if values.is_empty() {
|
||||
return;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -317,7 +317,7 @@ where
|
||||
#[cfg(feature = "multi-dim")]
|
||||
{
|
||||
let pzt_values = vals.iter().map(|value| *value as f32).collect::<Vec<f32>>();
|
||||
if let Ok(analysis) = pzt_processor.get_pzt_analysis(&pzt_values) {
|
||||
if let Ok(analysis) = pzt_processor.get_pzt_analysis_at(&pzt_values, frame.dts_ms() as f32) {
|
||||
if PztProcessor::should_report(&analysis) {
|
||||
spatial_force = Some(HudSpatialForce {
|
||||
angle_deg: analysis.angle_deg,
|
||||
@@ -327,12 +327,15 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "devkit")]
|
||||
{
|
||||
let summary = vals.iter().copied().sum::<i32>();
|
||||
let force = raw_to_g1(summary as u32);
|
||||
push_devkit_frame(&app, vals.as_slice(), frame.dts_ms(), force);
|
||||
}
|
||||
// #[cfg(feature = "devkit")]
|
||||
// {
|
||||
// let summary = vals.iter().copied().sum::<i32>();
|
||||
// #[cfg(feature = "debug")]
|
||||
// let force = raw_to_g1(summary as u32);
|
||||
// #[cfg(not(feature = "debug"))]
|
||||
// let force = summary as f64;
|
||||
// push_devkit_frame(&app, vals.as_slice(), frame.dts_ms(), force);
|
||||
// }
|
||||
|
||||
pending_sub_frame = Some(PendingSubFrame {
|
||||
frame: frame.clone(),
|
||||
@@ -355,13 +358,24 @@ fn build_display_values(
|
||||
spatial_force: Option<HudSpatialForce>,
|
||||
) -> Option<Vec<i32>> {
|
||||
let summary = values.iter().copied().sum::<i32>();
|
||||
let force = raw_to_g1(summary as u32);
|
||||
chart_state.record_summary(force as f32);
|
||||
chart_state.record_pressure_matrix(values);
|
||||
chart_state.record_spatial_force(spatial_force);
|
||||
Some(vec![summary])
|
||||
|
||||
let x = raw_to_g1(summary as u32).min(MAX_DISPLAY_FORCE);
|
||||
if x <= MIN_DISPLAY_FORCE {
|
||||
let zero_values = vec![0; values.len()];
|
||||
chart_state.record_summary(0.0);
|
||||
chart_state.record_pressure_matrix(&zero_values);
|
||||
return Some(vec![0]);
|
||||
}
|
||||
|
||||
chart_state.record_pressure_matrix(values);
|
||||
chart_state.record_summary(x as f32);
|
||||
Some(vec![x.round() as i32])
|
||||
}
|
||||
|
||||
const MIN_DISPLAY_FORCE: f64 = 0.1;
|
||||
const MAX_DISPLAY_FORCE: f64 = 25.6;
|
||||
|
||||
#[cfg(feature = "devkit")]
|
||||
fn push_devkit_frame(app: &AppHandle, values: &[i32], dts_ms: u64, resultant_force: f64) {
|
||||
let devkit_state = app.state::<DevKitState>();
|
||||
@@ -416,11 +430,11 @@ fn infer_matrix_shape(len: usize) -> (u32, u32) {
|
||||
|
||||
fn raw_to_g1(raw: u32) -> f64 {
|
||||
const X: [u32; 12] = [
|
||||
0, 84402, 117218, 140176, 159126, 175812, 191484, 208758, 224703, 252448, 302361, 352703,
|
||||
0, 20829, 102371, 132956, 165568, 182033, 217263, 263098, 283747, 365120, 410556, 477190
|
||||
];
|
||||
|
||||
const Y: [f64; 12] = [
|
||||
0.0, 160.0, 260.0, 360.0, 460.0, 560.0, 660.0, 760.0, 860.0, 1060.0, 1560.0, 2060.0,
|
||||
0.0, 57.0, 257.0, 357.0, 457.0, 557.0, 657.0, 857.0, 1057.0, 1557.0, 2057.0, 2557.0
|
||||
];
|
||||
|
||||
let n = X.len();
|
||||
|
||||
@@ -41,9 +41,7 @@
|
||||
"template": "nsis/installer.nsi"
|
||||
}
|
||||
},
|
||||
"resources": [
|
||||
"resources/je-skin-devkit-server.exe"
|
||||
]
|
||||
"resources": []
|
||||
},
|
||||
"plugins": {
|
||||
"updater": {
|
||||
|
||||
@@ -61,6 +61,8 @@
|
||||
export let replayFileName = "";
|
||||
export let replayFrameInfo = "";
|
||||
export let sessionStartedAt: number = Date.now();
|
||||
export let summaryReleasePending = false;
|
||||
export let spatialForcePanelVisible = false;
|
||||
|
||||
let stagePlaneEl: HTMLDivElement | undefined;
|
||||
let panelZoneEl: HTMLDivElement | undefined;
|
||||
@@ -87,7 +89,9 @@
|
||||
$: replaySide = summarySide === "left" ? "right" : "left";
|
||||
$: replayToggleButtonText = replayIsPlaying ? replayPauseLabel : replayPlayLabel;
|
||||
$: replayProgressPercent = Math.round(Math.min(1, Math.max(0, replayProgress)) * 100);
|
||||
$: summaryCurveVisible = summary.points.length > 0 && summary.points.some((value) => Number.isFinite(value) && Math.abs(value) >= 0.0001);
|
||||
$: summaryCurveVisible =
|
||||
summary.points.length > 0 &&
|
||||
(summaryReleasePending || summary.points.some((value) => Number.isFinite(value) && Math.abs(value) >= 0.0001));
|
||||
$: isModelStage = stageViewMode === "model3d";
|
||||
|
||||
function toPxNumber(rawValue: string): number {
|
||||
@@ -276,9 +280,7 @@
|
||||
<SignalChart {panel} panelIndex={index} {locale} />
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
|
||||
{#if spatialForce}
|
||||
{#if spatialForcePanelVisible}
|
||||
<div
|
||||
class="panel-motion-shell"
|
||||
in:fly={{ x: 180, duration: 340, opacity: 0.08, easing: cubicOut }}
|
||||
@@ -311,7 +313,7 @@
|
||||
{sessionStartedAt}
|
||||
isRealtime={!replayHasData}
|
||||
side="right"
|
||||
panelIndex={rightPanels.length}
|
||||
panelIndex={rightPanels.length + (spatialForcePanelVisible ? 1 : 0)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
const CAMERA_TARGET_Z = MATRIX_OFFSET_Z - 0.4;
|
||||
const MATRIX_ROTATION_Y = 0;
|
||||
const rangeStopPositions = [0, 0.25, 0.5, 0.75, 0.875, 1] as const;
|
||||
const maxDisplayForce = 25.6;
|
||||
|
||||
const labelVector = new THREE.Vector3();
|
||||
$: resolvedColorPalette = pressureColorPalettes[colorMapPreset] ?? pressureColorPalettes.emerald;
|
||||
@@ -145,6 +146,10 @@
|
||||
return "--";
|
||||
}
|
||||
|
||||
if (value >= maxDisplayForce) {
|
||||
return `${maxDisplayForce.toFixed(1)}+`;
|
||||
}
|
||||
|
||||
return value.toFixed(1);
|
||||
}
|
||||
|
||||
@@ -672,14 +677,6 @@
|
||||
<span class="stats-key">{viewerI18n.current}</span>
|
||||
<strong class="stats-value">{formatForceStat(stats.current)}</strong>
|
||||
</article>
|
||||
<article class="stats-card">
|
||||
<span class="stats-key">{viewerI18n.max}</span>
|
||||
<strong class="stats-value">{formatForceStat(stats.max)}</strong>
|
||||
</article>
|
||||
<article class="stats-card">
|
||||
<span class="stats-key">{viewerI18n.min}</span>
|
||||
<strong class="stats-value">{formatForceStat(stats.min)}</strong>
|
||||
</article>
|
||||
</div>
|
||||
<p class="stats-note">{statsNote}</p>
|
||||
</section>
|
||||
@@ -768,7 +765,7 @@
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 0.46rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
const viewportWidth = 100;
|
||||
const viewportHeight = 36;
|
||||
const maxDisplayForce = 25.6;
|
||||
|
||||
const toneColorMap: Record<SignalTone, string> = {
|
||||
cyan: "62 232 255",
|
||||
@@ -32,7 +33,15 @@
|
||||
}
|
||||
|
||||
function formatMetric(value: number | null): string {
|
||||
return value === null ? "--" : value.toFixed(1);
|
||||
if (value === null) {
|
||||
return "--";
|
||||
}
|
||||
|
||||
if (value >= maxDisplayForce) {
|
||||
return `${maxDisplayForce.toFixed(1)}+`;
|
||||
}
|
||||
|
||||
return value.toFixed(1);
|
||||
}
|
||||
|
||||
function resolveBounds(seriesCollection: HudSignalPanel["series"]): { min: number; max: number } {
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
return delta === -180 ? 180 : delta;
|
||||
}
|
||||
|
||||
const displayAngleOffsetDeg = 90;
|
||||
const jumpAngleThresholdDeg = 72;
|
||||
|
||||
let visualAngleDeg = 0;
|
||||
@@ -94,17 +95,17 @@
|
||||
$: hasData =
|
||||
spatialForce !== null &&
|
||||
Number.isFinite(spatialForce.angleDeg) &&
|
||||
(!requireMagnitude || Number.isFinite(spatialForce.magnitude));
|
||||
$: angleDeg = hasData ? normalizeAngle(spatialForce?.angleDeg ?? 0) : 0;
|
||||
(!requireMagnitude || (Number.isFinite(spatialForce.magnitude) && Math.abs(spatialForce.magnitude) >= 0.0001));
|
||||
$: angleDeg = hasData ? normalizeAngle((spatialForce?.angleDeg ?? 0) + displayAngleOffsetDeg) : 0;
|
||||
$: updateVisualAngle(angleDeg, hasData);
|
||||
</script>
|
||||
|
||||
{#if hasData}
|
||||
<article
|
||||
<article
|
||||
class="signal-panel spatial-panel side-{side}"
|
||||
class:is-empty={!hasData}
|
||||
aria-label={resolvedTitle}
|
||||
style="--panel-index: {panelIndex};"
|
||||
>
|
||||
>
|
||||
<header class="panel-head">
|
||||
<div class="head-text">
|
||||
<p class="panel-code">{panelCode}</p>
|
||||
@@ -125,6 +126,7 @@
|
||||
<div class="compass-ring compass-ring-inner"></div>
|
||||
<div class="compass-axis axis-horizontal"></div>
|
||||
<div class="compass-axis axis-vertical"></div>
|
||||
{#if hasData}
|
||||
<div
|
||||
class="compass-vector"
|
||||
class:is-snap={snapVector}
|
||||
@@ -133,16 +135,17 @@
|
||||
<span class="vector-shaft"></span>
|
||||
<span class="vector-head"></span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="compass-center"></div>
|
||||
<span class="compass-label label-top">90</span>
|
||||
<span class="compass-label label-right">0</span>
|
||||
<span class="compass-label label-bottom">270</span>
|
||||
<span class="compass-label label-left">180</span>
|
||||
<span class="compass-label label-top">0</span>
|
||||
<span class="compass-label label-right">270</span>
|
||||
<span class="compass-label label-bottom">180</span>
|
||||
<span class="compass-label label-left">90</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</article>
|
||||
{/if}
|
||||
</article>
|
||||
<style>
|
||||
.signal-panel {
|
||||
--offset-x: 12%;
|
||||
@@ -261,14 +264,16 @@
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 0.72rem;
|
||||
block-size: clamp(12rem, 15.5vw, 15rem);
|
||||
block-size: clamp(14rem, 17vw, 16.5rem);
|
||||
min-block-size: 5rem;
|
||||
}
|
||||
|
||||
.compass-stage {
|
||||
position: relative;
|
||||
min-block-size: 0;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
padding: 1.35rem 1.7rem;
|
||||
border: 1px solid rgb(var(--hud-border-strong-rgb) / 0.32);
|
||||
border-radius: 0.62rem;
|
||||
background:
|
||||
@@ -280,7 +285,8 @@
|
||||
|
||||
.compass-core {
|
||||
position: relative;
|
||||
inline-size: min(72%, 13rem);
|
||||
inline-size: min(72%, 12.2rem, calc(100% - 3.4rem));
|
||||
min-inline-size: 6.2rem;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
@@ -379,26 +385,26 @@
|
||||
}
|
||||
|
||||
.label-top {
|
||||
top: -0.9rem;
|
||||
top: 0.35rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.label-right {
|
||||
top: 50%;
|
||||
right: -1rem;
|
||||
right: 0.42rem;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.label-bottom {
|
||||
bottom: -0.9rem;
|
||||
bottom: 0.35rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.label-left {
|
||||
top: 50%;
|
||||
left: -1.35rem;
|
||||
left: 0.42rem;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
@@ -421,7 +427,11 @@
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
block-size: clamp(10rem, 13vw, 12rem);
|
||||
block-size: clamp(12rem, 14.5vw, 14rem);
|
||||
}
|
||||
|
||||
.compass-core {
|
||||
inline-size: min(64%, 10.4rem, calc(100% - 3.4rem));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,7 +442,15 @@
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
block-size: clamp(9.8rem, 12vw, 11.8rem);
|
||||
block-size: clamp(11.2rem, 13.5vw, 13.2rem);
|
||||
}
|
||||
|
||||
.compass-stage {
|
||||
padding-block: 1.2rem;
|
||||
}
|
||||
|
||||
.compass-core {
|
||||
inline-size: min(62%, 10rem, calc(100% - 3.4rem));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,7 +462,16 @@
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
block-size: clamp(8rem, 9.5vw, 9.8rem);
|
||||
block-size: clamp(9.4rem, 11vw, 10.8rem);
|
||||
}
|
||||
|
||||
.compass-stage {
|
||||
padding: 1rem 1.45rem;
|
||||
}
|
||||
|
||||
.compass-core {
|
||||
inline-size: min(58%, 8.2rem, calc(100% - 2.9rem));
|
||||
min-inline-size: 5.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,7 +483,16 @@
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
block-size: clamp(6.5rem, 8vw, 7.5rem);
|
||||
block-size: clamp(7.6rem, 9vw, 8.8rem);
|
||||
}
|
||||
|
||||
.compass-stage {
|
||||
padding: 0.82rem 1.25rem;
|
||||
}
|
||||
|
||||
.compass-core {
|
||||
inline-size: min(54%, 6.4rem, calc(100% - 2.5rem));
|
||||
min-inline-size: 4.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,15 +11,26 @@
|
||||
export let sessionStartedAt: number = Date.now();
|
||||
export let isRealtime = false;
|
||||
|
||||
let canvasEl: HTMLCanvasElement | undefined;
|
||||
let currentTimeSeconds = 0;
|
||||
let timerId: ReturnType<typeof setInterval> | null = null;
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
onMount(() => {
|
||||
timerId = setInterval(() => {
|
||||
currentTimeSeconds = Math.round((Date.now() - sessionStartedAt) / 100) / 10;
|
||||
}, 200);
|
||||
|
||||
if (canvasEl) {
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
drawCanvas(canvasEl, plotPoints, yAxisTicks, xAxisTicks, sampleCount);
|
||||
});
|
||||
resizeObserver.observe(canvasEl);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timerId != null) clearInterval(timerId);
|
||||
resizeObserver?.disconnect();
|
||||
};
|
||||
});
|
||||
|
||||
@@ -33,7 +44,8 @@
|
||||
const plotInsetRight = 4;
|
||||
const plotInsetTop = 4;
|
||||
const plotInsetBottom = 9;
|
||||
const fixedYBounds = { min: 0, max: 25 };
|
||||
const maxDisplayForce = 25.6;
|
||||
const fixedYBounds = { min: 0, max: maxDisplayForce };
|
||||
|
||||
interface CurveSample {
|
||||
x: number;
|
||||
@@ -56,14 +68,6 @@
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
|
||||
function formatValue(value: number | null): string {
|
||||
if (value === null) {
|
||||
return "--";
|
||||
}
|
||||
|
||||
return value.toFixed(1);
|
||||
}
|
||||
|
||||
function formatAxisValue(value: number, axis: "x" | "y"): string {
|
||||
if (!Number.isFinite(value)) {
|
||||
return "--";
|
||||
@@ -225,24 +229,117 @@
|
||||
}));
|
||||
}
|
||||
|
||||
function createLinePath(points: PlotPoint[]): string {
|
||||
if (points.length === 0) {
|
||||
return "";
|
||||
function drawCanvas(
|
||||
canvas: HTMLCanvasElement | undefined,
|
||||
points: PlotPoint[],
|
||||
yTicks: AxisTick[],
|
||||
xTicks: AxisTick[],
|
||||
count: number
|
||||
): void {
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
return points.map((point, index) => `${index === 0 ? "M" : "L"} ${point.x} ${point.y}`).join(" ");
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
function createAreaPath(points: PlotPoint[]): string {
|
||||
if (points.length < 2) {
|
||||
return "";
|
||||
const cssWidth = Math.max(1, canvas.clientWidth);
|
||||
const cssHeight = Math.max(1, canvas.clientHeight);
|
||||
const dpr = typeof window === "undefined" ? 1 : window.devicePixelRatio || 1;
|
||||
const targetWidth = Math.round(cssWidth * dpr);
|
||||
const targetHeight = Math.round(cssHeight * dpr);
|
||||
|
||||
if (canvas.width !== targetWidth || canvas.height !== targetHeight) {
|
||||
canvas.width = targetWidth;
|
||||
canvas.height = targetHeight;
|
||||
}
|
||||
|
||||
const linePath = createLinePath(points);
|
||||
const firstPoint = points[0];
|
||||
context.setTransform(targetWidth / viewportWidth, 0, 0, targetHeight / viewportHeight, 0, 0);
|
||||
context.clearRect(0, 0, viewportWidth, viewportHeight);
|
||||
|
||||
context.save();
|
||||
context.lineWidth = 0.45;
|
||||
context.strokeStyle = "rgb(120 180 150 / 0.16)";
|
||||
for (const tick of yTicks) {
|
||||
context.beginPath();
|
||||
context.moveTo(plotInsetLeft, tick.plotY);
|
||||
context.lineTo(viewportWidth - plotInsetRight, tick.plotY);
|
||||
context.stroke();
|
||||
}
|
||||
context.restore();
|
||||
|
||||
if (points.length >= 2) {
|
||||
const baselineY = viewportHeight - plotInsetBottom;
|
||||
const fill = context.createLinearGradient(0, plotInsetTop, 0, baselineY);
|
||||
fill.addColorStop(0, "rgb(62 232 255 / 0.28)");
|
||||
fill.addColorStop(1, "rgb(62 232 255 / 0.02)");
|
||||
|
||||
context.save();
|
||||
context.beginPath();
|
||||
context.moveTo(points[0].x, points[0].y);
|
||||
for (let index = 1; index < points.length; index += 1) {
|
||||
context.lineTo(points[index].x, points[index].y);
|
||||
}
|
||||
context.lineTo(points[points.length - 1].x, baselineY);
|
||||
context.lineTo(points[0].x, baselineY);
|
||||
context.closePath();
|
||||
context.fillStyle = fill;
|
||||
context.fill();
|
||||
context.restore();
|
||||
}
|
||||
|
||||
if (points.length > 0) {
|
||||
context.save();
|
||||
context.beginPath();
|
||||
context.moveTo(points[0].x, points[0].y);
|
||||
for (let index = 1; index < points.length; index += 1) {
|
||||
context.lineTo(points[index].x, points[index].y);
|
||||
}
|
||||
context.lineWidth = 1.35;
|
||||
context.lineCap = "round";
|
||||
context.lineJoin = "round";
|
||||
context.strokeStyle = "rgb(130 232 255 / 0.96)";
|
||||
context.shadowColor = "rgb(62 232 255 / 0.22)";
|
||||
context.shadowBlur = 4;
|
||||
context.stroke();
|
||||
context.restore();
|
||||
|
||||
const lastPoint = points[points.length - 1];
|
||||
context.save();
|
||||
context.beginPath();
|
||||
context.arc(lastPoint.x, lastPoint.y, 1.7, 0, Math.PI * 2);
|
||||
context.fillStyle = "rgb(73 222 128 / 0.98)";
|
||||
context.shadowColor = "rgb(73 222 128 / 0.3)";
|
||||
context.shadowBlur = 6;
|
||||
context.fill();
|
||||
context.restore();
|
||||
}
|
||||
|
||||
return `${linePath} L ${lastPoint.x} ${viewportHeight - plotInsetBottom} L ${firstPoint.x} ${viewportHeight - plotInsetBottom} Z`;
|
||||
context.save();
|
||||
context.font = "600 3.2px sans-serif";
|
||||
context.textBaseline = "middle";
|
||||
context.shadowColor = "rgb(0 0 0 / 0.3)";
|
||||
context.shadowBlur = 4;
|
||||
context.fillStyle = "rgb(145 185 165 / 0.84)";
|
||||
for (const tick of yTicks) {
|
||||
context.textAlign = "right";
|
||||
context.fillText(tick.label, tick.plotX, tick.plotY + 0.5);
|
||||
}
|
||||
|
||||
context.fillStyle = "rgb(180 220 200 / 0.9)";
|
||||
context.textBaseline = "alphabetic";
|
||||
for (let index = 0; index < xTicks.length; index += 1) {
|
||||
const tick = xTicks[index];
|
||||
context.textAlign = index === 0 ? "left" : index === xTicks.length - 1 ? "right" : "center";
|
||||
context.fillText(tick.label, tick.plotX, tick.plotY);
|
||||
}
|
||||
context.restore();
|
||||
|
||||
if (count === 0) {
|
||||
context.clearRect(0, 0, viewportWidth, viewportHeight);
|
||||
}
|
||||
}
|
||||
|
||||
$: sourceYValues = yValues && yValues.length ? yValues : summary.points;
|
||||
@@ -261,14 +358,9 @@
|
||||
$: xDataBounds = resolveDataBounds(samples.map((sample) => sample.x));
|
||||
$: yDataBounds = resolveDataBounds(samples.map((sample) => sample.y));
|
||||
$: plotPoints = convertPoints(samples, xScaleBounds, yScaleBounds);
|
||||
$: linePath = createLinePath(plotPoints);
|
||||
$: areaPath = createAreaPath(plotPoints);
|
||||
$: lastPoint = plotPoints.length > 0 ? plotPoints[plotPoints.length - 1] : null;
|
||||
$: yAxisTicks = sampleCount > 0 ? buildYAxisTicks(yScaleBounds, yDataBounds) : [];
|
||||
$: xAxisTicks = sampleCount > 0 ? buildXAxisTicks(xScaleBounds) : [];
|
||||
$: latestValue = formatValue(summary.latest);
|
||||
$: minValue = formatValue(summary.min);
|
||||
$: maxValue = formatValue(summary.max);
|
||||
$: drawCanvas(canvasEl, plotPoints, yAxisTicks, xAxisTicks, sampleCount);
|
||||
</script>
|
||||
|
||||
<article
|
||||
@@ -280,62 +372,12 @@
|
||||
<header class="panel-head">
|
||||
<div class="head-text">
|
||||
<p class="panel-code">RF</p>
|
||||
<p class="panel-title">{summary.label}</p>
|
||||
</div>
|
||||
|
||||
<div class="icon-layer" aria-hidden="true">
|
||||
<span class="icon-chip tone-cyan">NOW</span>
|
||||
<span class="icon-chip tone-lime">MIN</span>
|
||||
<span class="icon-chip tone-orange">MAX</span>
|
||||
<p class="panel-title">{locale === "zh-CN" ? "合力" : "Resultant Force"}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="chart-stage">
|
||||
<svg viewBox="0 0 {viewportWidth} {viewportHeight}" preserveAspectRatio="none" role="img" aria-label={summary.label}>
|
||||
<defs>
|
||||
<linearGradient id="summary-fill" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="rgb(62 232 255 / 0.28)" />
|
||||
<stop offset="100%" stop-color="rgb(62 232 255 / 0.02)" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<g class="grid-lines" aria-hidden="true">
|
||||
{#each yAxisTicks as tick (`grid-${tick.value}`)}
|
||||
<line x1={plotInsetLeft} y1={tick.plotY} x2={viewportWidth - plotInsetRight} y2={tick.plotY}></line>
|
||||
{/each}
|
||||
</g>
|
||||
|
||||
{#if areaPath}
|
||||
<path d={areaPath} class="summary-area"></path>
|
||||
{/if}
|
||||
|
||||
{#if linePath}
|
||||
<path d={linePath} class="summary-line"></path>
|
||||
{/if}
|
||||
|
||||
{#if lastPoint}
|
||||
<circle cx={lastPoint.x} cy={lastPoint.y} r="1.7" class="summary-dot"></circle>
|
||||
{/if}
|
||||
|
||||
<g class="axis-labels" aria-hidden="true">
|
||||
{#each yAxisTicks as tick, index (`y-${index}`)}
|
||||
<text class="axis-label y-axis-label" x={tick.plotX} y={tick.plotY + 1.1} text-anchor="end">
|
||||
{tick.label}
|
||||
</text>
|
||||
{/each}
|
||||
|
||||
{#each xAxisTicks as tick, index (`x-${index}`)}
|
||||
<text
|
||||
class="axis-label x-axis-label"
|
||||
x={tick.plotX}
|
||||
y={tick.plotY}
|
||||
text-anchor={index === 0 ? "start" : index === xAxisTicks.length - 1 ? "end" : "middle"}
|
||||
>
|
||||
{tick.label}
|
||||
</text>
|
||||
{/each}
|
||||
</g>
|
||||
</svg>
|
||||
<canvas bind:this={canvasEl} aria-label={summary.label}></canvas>
|
||||
|
||||
{#if sampleCount === 0}
|
||||
<div class="empty-state">
|
||||
@@ -343,24 +385,6 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<footer class="panel-foot">
|
||||
<p class="foot-item">
|
||||
<span class="dot tone-cyan"></span>
|
||||
<span class="metric-text">{i18n.now}</span>
|
||||
<span class="value">{latestValue}</span>
|
||||
</p>
|
||||
<p class="foot-item">
|
||||
<span class="dot tone-lime"></span>
|
||||
<span class="metric-text">{i18n.min}</span>
|
||||
<span class="value">{minValue}</span>
|
||||
</p>
|
||||
<p class="foot-item">
|
||||
<span class="dot tone-orange"></span>
|
||||
<span class="metric-text">{i18n.max}</span>
|
||||
<span class="value">{maxValue}</span>
|
||||
</p>
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
<style>
|
||||
@@ -369,10 +393,13 @@
|
||||
--enter-ms: 1800ms;
|
||||
--fade-ms: 1000ms;
|
||||
overflow: hidden;
|
||||
inline-size: min(100%, clamp(34rem, 44vw, 44rem));
|
||||
inline-size: var(--rail-width, min(100%, clamp(34rem, 44vw, 44rem)));
|
||||
max-inline-size: 100%;
|
||||
box-sizing: border-box;
|
||||
flex: 0 0 var(--rail-width, auto);
|
||||
justify-self: start;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
grid-template-rows: auto 1fr;
|
||||
gap: 0.68rem;
|
||||
padding: 0.88rem 0.96rem 1rem;
|
||||
border: 1px solid rgb(var(--hud-border-strong-rgb) / 0.42);
|
||||
@@ -438,36 +465,6 @@
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.icon-layer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.26rem;
|
||||
}
|
||||
|
||||
.icon-chip {
|
||||
border: 1px solid rgb(var(--hud-border-strong-rgb) / 0.44);
|
||||
border-radius: 999px;
|
||||
padding: 0.08rem 0.36rem;
|
||||
font-size: 0.58rem;
|
||||
letter-spacing: 0.08em;
|
||||
color: rgb(var(--hud-text-main-rgb) / 0.94);
|
||||
background: rgb(var(--hud-surface-rgb) / 0.66);
|
||||
}
|
||||
|
||||
.icon-chip.tone-cyan {
|
||||
border-color: rgb(var(--hud-cyan-rgb) / 0.54);
|
||||
}
|
||||
|
||||
.icon-chip.tone-lime {
|
||||
border-color: rgb(var(--hud-lime-rgb) / 0.56);
|
||||
}
|
||||
|
||||
.icon-chip.tone-orange {
|
||||
border-color: rgb(var(--hud-orange-rgb) / 0.58);
|
||||
}
|
||||
|
||||
.chart-stage {
|
||||
position: relative;
|
||||
block-size: clamp(12rem, 15.5vw, 15rem);
|
||||
@@ -480,53 +477,12 @@
|
||||
radial-gradient(circle at 50% 0, rgb(var(--hud-glow-rgb) / 0.09), transparent 45%);
|
||||
}
|
||||
|
||||
svg {
|
||||
canvas {
|
||||
display: block;
|
||||
inline-size: 100%;
|
||||
block-size: 100%;
|
||||
}
|
||||
|
||||
.grid-lines line {
|
||||
stroke: rgb(var(--hud-border-strong-rgb) / 0.16);
|
||||
stroke-width: 0.45;
|
||||
}
|
||||
|
||||
.summary-area {
|
||||
fill: url(#summary-fill);
|
||||
}
|
||||
|
||||
.summary-line {
|
||||
fill: none;
|
||||
stroke: rgb(var(--hud-cyan-rgb) / 0.96);
|
||||
stroke-width: 1.35;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
filter: drop-shadow(0 0 4px rgb(var(--hud-cyan-rgb) / 0.22));
|
||||
}
|
||||
|
||||
.summary-dot {
|
||||
fill: rgb(var(--hud-lime-rgb) / 0.98);
|
||||
filter: drop-shadow(0 0 6px rgb(var(--hud-lime-rgb) / 0.3));
|
||||
}
|
||||
|
||||
.axis-label {
|
||||
fill: rgb(var(--hud-text-main-rgb) / 0.88);
|
||||
font-size: 3.2px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
text-shadow:
|
||||
0 1px 0 rgb(0 0 0 / 0.46),
|
||||
0 0 4px rgb(0 0 0 / 0.3);
|
||||
}
|
||||
|
||||
.y-axis-label {
|
||||
fill: rgb(var(--hud-text-dim-rgb) / 0.84);
|
||||
}
|
||||
|
||||
.x-axis-label {
|
||||
fill: rgb(var(--hud-text-dim-rgb) / 0.9);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
@@ -540,58 +496,9 @@
|
||||
background: linear-gradient(180deg, rgb(var(--hud-surface-deep-rgb) / 0.06), rgb(var(--hud-surface-deep-rgb) / 0.18));
|
||||
}
|
||||
|
||||
.panel-foot {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 0.8rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.foot-item {
|
||||
margin: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.22rem;
|
||||
color: rgb(var(--hud-text-main-rgb) / 0.9);
|
||||
font-size: 0.68rem;
|
||||
letter-spacing: 0.03em;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.metric-text {
|
||||
color: rgb(var(--hud-text-dim-rgb) / 0.82);
|
||||
text-transform: uppercase;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dot {
|
||||
inline-size: 0.34rem;
|
||||
block-size: 0.34rem;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.dot.tone-cyan {
|
||||
background: rgb(var(--hud-cyan-rgb));
|
||||
}
|
||||
|
||||
.dot.tone-lime {
|
||||
background: rgb(var(--hud-lime-rgb));
|
||||
}
|
||||
|
||||
.dot.tone-orange {
|
||||
background: rgb(var(--hud-orange-rgb));
|
||||
}
|
||||
|
||||
.value {
|
||||
min-inline-size: 2.6rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.signal-panel {
|
||||
inline-size: min(100%, clamp(28rem, 40vw, 38rem));
|
||||
inline-size: var(--rail-width, min(100%, clamp(28rem, 40vw, 38rem)));
|
||||
}
|
||||
|
||||
.chart-stage {
|
||||
@@ -601,7 +508,7 @@
|
||||
|
||||
@media (max-height: 900px) {
|
||||
.signal-panel {
|
||||
inline-size: min(100%, clamp(28rem, 38vw, 36rem));
|
||||
inline-size: var(--rail-width, min(100%, clamp(28rem, 38vw, 36rem)));
|
||||
padding: 0.7rem 0.76rem 0.8rem;
|
||||
}
|
||||
|
||||
@@ -612,7 +519,7 @@
|
||||
|
||||
@media (max-height: 760px) {
|
||||
.signal-panel {
|
||||
inline-size: min(100%, clamp(24rem, 34vw, 30rem));
|
||||
inline-size: var(--rail-width, min(100%, clamp(24rem, 34vw, 30rem)));
|
||||
padding: 0.62rem 0.68rem 0.72rem;
|
||||
gap: 0.48rem;
|
||||
}
|
||||
@@ -624,7 +531,7 @@
|
||||
|
||||
@media (max-height: 680px) {
|
||||
.signal-panel {
|
||||
inline-size: min(100%, clamp(20rem, 28vw, 26rem));
|
||||
inline-size: var(--rail-width, min(100%, clamp(20rem, 28vw, 26rem)));
|
||||
padding: 0.52rem 0.58rem 0.6rem;
|
||||
gap: 0.36rem;
|
||||
}
|
||||
|
||||
@@ -194,6 +194,9 @@
|
||||
const pointsPerSeries = 28;
|
||||
const summaryPointsPerSeries = 42;
|
||||
const signalRenderTickMs = 1200;
|
||||
const summaryReleaseForceEpsilon = 0.1;
|
||||
const releaseClearDelayMs = 5000;
|
||||
const spatialForceMagnitudeEpsilon = 0.0001;
|
||||
const replayDefaultFrameMs = 40;
|
||||
const showSignalPanels = false;
|
||||
const mockToneCycle: SignalTone[] = ["cyan", "lime", "orange", "violet", "gold", "rose"];
|
||||
@@ -288,6 +291,11 @@
|
||||
} | null = null;
|
||||
let devkitStatusTimer: number | null = null;
|
||||
let devkitSpatialForceClearTimer: number | null = null;
|
||||
let summaryClearTimer: number | null = null;
|
||||
let spatialForceClearTimer: number | null = null;
|
||||
let summaryReleaseHidden = true;
|
||||
let spatialForceReleaseHidden = false;
|
||||
let spatialForcePanelVisible = false;
|
||||
let sessionStartedAt: number = Date.now();
|
||||
|
||||
$: uiCopy = copyByLocale[locale];
|
||||
@@ -298,6 +306,7 @@
|
||||
$: rangeScaleStyle = buildRangeScaleStyle(colorMapPreset);
|
||||
$: replayHasData = replayFrames.length > 0;
|
||||
$: replayFrameInfo = replayHasData ? `${replayHasDisplayedFrame ? replayCurrentIndex + 1 : 0}/${replayFrames.length}` : "";
|
||||
$: summaryReleasePending = summaryClearTimer != null;
|
||||
$: fileExplorerTitle =
|
||||
fileExplorerMode === "open" ? uiCopy.fileExplorerImportTitle : uiCopy.fileExplorerExportTitle;
|
||||
$: fileExplorerConfirmLabel =
|
||||
@@ -313,7 +322,11 @@
|
||||
window.clearTimeout(devkitSpatialForceClearTimer);
|
||||
devkitSpatialForceClearTimer = null;
|
||||
}
|
||||
hasSignalData = signalPanels.length > 0 || summary.points.length > 0 || spatialForce !== null;
|
||||
hasSignalData =
|
||||
signalPanels.length > 0 ||
|
||||
summary.points.length > 0 ||
|
||||
spatialForce !== null ||
|
||||
spatialForcePanelVisible;
|
||||
}
|
||||
|
||||
function scheduleDevkitSpatialForceClear(): void {
|
||||
@@ -332,6 +345,54 @@
|
||||
}, 420);
|
||||
}
|
||||
|
||||
function cancelSummaryClear(): void {
|
||||
if (summaryClearTimer != null && typeof window !== "undefined") {
|
||||
window.clearTimeout(summaryClearTimer);
|
||||
}
|
||||
summaryClearTimer = null;
|
||||
}
|
||||
|
||||
function scheduleSummaryClear(): void {
|
||||
if (typeof window === "undefined" || summary.points.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (summaryClearTimer != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
summaryClearTimer = window.setTimeout(() => {
|
||||
summary = buildEmptySummary();
|
||||
summaryClearTimer = null;
|
||||
summaryReleaseHidden = true;
|
||||
hasSignalData = signalPanels.length > 0 || spatialForce !== null || spatialForcePanelVisible;
|
||||
}, releaseClearDelayMs);
|
||||
}
|
||||
|
||||
function cancelSpatialForceClear(): void {
|
||||
if (spatialForceClearTimer != null && typeof window !== "undefined") {
|
||||
window.clearTimeout(spatialForceClearTimer);
|
||||
}
|
||||
spatialForceClearTimer = null;
|
||||
}
|
||||
|
||||
function scheduleSpatialForceClear(): void {
|
||||
if (typeof window === "undefined" || !spatialForcePanelVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (spatialForceClearTimer != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
spatialForceClearTimer = window.setTimeout(() => {
|
||||
spatialForceClearTimer = null;
|
||||
spatialForceReleaseHidden = true;
|
||||
spatialForcePanelVisible = false;
|
||||
hasSignalData = signalPanels.length > 0 || summary.points.length > 0 || spatialForce !== null;
|
||||
}, releaseClearDelayMs);
|
||||
}
|
||||
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
@@ -792,6 +853,10 @@
|
||||
}
|
||||
|
||||
function resetReplayVisualState(): void {
|
||||
cancelSummaryClear();
|
||||
cancelSpatialForceClear();
|
||||
spatialForceReleaseHidden = false;
|
||||
spatialForcePanelVisible = false;
|
||||
pressureMatrix = buildZeroMatrix();
|
||||
spatialForce = null;
|
||||
replayPendingDevkitSeq = null;
|
||||
@@ -831,8 +896,12 @@
|
||||
replayHasDisplayedFrame = true;
|
||||
replayProgress = replayFrames.length > 1 ? safeIndex / (replayFrames.length - 1) : 1;
|
||||
const frame = replayFrames[safeIndex];
|
||||
cancelSummaryClear();
|
||||
cancelSpatialForceClear();
|
||||
spatialForceReleaseHidden = false;
|
||||
pressureMatrix = frameValuesToMatrix(frame.values);
|
||||
spatialForce = frame.spatialForce ?? null;
|
||||
spatialForcePanelVisible = spatialForce !== null;
|
||||
pushReplayFrameToDevkit(frame, safeIndex);
|
||||
signalPanels = buildInactivePanels();
|
||||
summary = buildReplaySummaryAt(safeIndex);
|
||||
@@ -1002,13 +1071,99 @@
|
||||
}
|
||||
|
||||
function isZeroLikeValue(value: number): boolean {
|
||||
return !Number.isFinite(value) || Math.abs(value) < 0.0001;
|
||||
return !Number.isFinite(value) || Math.abs(value) <= summaryReleaseForceEpsilon;
|
||||
}
|
||||
|
||||
function shouldHideSummary(points: number[]): boolean {
|
||||
return points.length === 0 || points.every((value) => isZeroLikeValue(value));
|
||||
}
|
||||
|
||||
function latestSummaryValue(summaryValue: HudSummary): number | null {
|
||||
if (Number.isFinite(summaryValue.latest)) {
|
||||
return summaryValue.latest;
|
||||
}
|
||||
|
||||
const latestPoint = summaryValue.points[summaryValue.points.length - 1];
|
||||
return Number.isFinite(latestPoint) ? latestPoint : null;
|
||||
}
|
||||
|
||||
function hasActiveSummary(summaryValue: HudSummary): boolean {
|
||||
const latest = latestSummaryValue(summaryValue);
|
||||
return latest !== null && !isZeroLikeValue(latest);
|
||||
}
|
||||
|
||||
function hasActiveSpatialForce(spatialForceValue: HudSpatialForce | null): boolean {
|
||||
return (
|
||||
spatialForceValue !== null &&
|
||||
Number.isFinite(spatialForceValue.angleDeg) &&
|
||||
Number.isFinite(spatialForceValue.magnitude) &&
|
||||
Math.abs(spatialForceValue.magnitude) > spatialForceMagnitudeEpsilon
|
||||
);
|
||||
}
|
||||
|
||||
function buildPacketSummary(summaryValue: HudSummary): HudSummary {
|
||||
const nowSeconds = Math.round((Date.now() - sessionStartedAt) / 100) / 10;
|
||||
const pointCount = summaryValue.points.length;
|
||||
const spacing =
|
||||
pointCount > 1 ? Math.min(1.2, nowSeconds / Math.max(pointCount - 1, 1)) : 0;
|
||||
const startX = Math.max(0, nowSeconds - spacing * Math.max(pointCount - 1, 0));
|
||||
const xValues = summaryValue.points.map((_, index) => Math.round((startX + index * spacing) * 10) / 10);
|
||||
return { ...summaryValue, xValues };
|
||||
}
|
||||
|
||||
function estimateSummarySpacing(xValues: number[]): number {
|
||||
const diffs = xValues
|
||||
.slice(1)
|
||||
.map((value, index) => value - xValues[index])
|
||||
.filter((value) => Number.isFinite(value) && value > 0);
|
||||
|
||||
if (diffs.length === 0) {
|
||||
return 1.2;
|
||||
}
|
||||
|
||||
const average = diffs.reduce((sum, value) => sum + value, 0) / diffs.length;
|
||||
return Math.max(0.1, Math.min(1.2, Math.round(average * 10) / 10));
|
||||
}
|
||||
|
||||
function buildContinuousSummary(summaryValue: HudSummary): HudSummary {
|
||||
if (
|
||||
summary.points.length === 0 ||
|
||||
!summary.xValues ||
|
||||
summary.xValues.length !== summary.points.length
|
||||
) {
|
||||
return buildPacketSummary(summaryValue);
|
||||
}
|
||||
|
||||
const pointCount = summaryValue.points.length;
|
||||
const previousXValues = summary.xValues;
|
||||
const previousCount = previousXValues.length;
|
||||
|
||||
if (pointCount === 0) {
|
||||
return buildEmptySummary();
|
||||
}
|
||||
|
||||
if (pointCount < previousCount) {
|
||||
return buildPacketSummary(summaryValue);
|
||||
}
|
||||
|
||||
const spacing = estimateSummarySpacing(previousXValues);
|
||||
let xValues: number[];
|
||||
|
||||
if (pointCount > previousCount) {
|
||||
xValues = previousXValues.slice();
|
||||
while (xValues.length < pointCount) {
|
||||
const previousX = xValues[xValues.length - 1] ?? 0;
|
||||
xValues.push(Math.round((previousX + spacing) * 10) / 10);
|
||||
}
|
||||
} else {
|
||||
xValues = previousXValues.slice(1);
|
||||
const previousX = previousXValues[previousXValues.length - 1] ?? 0;
|
||||
xValues.push(Math.round((previousX + spacing) * 10) / 10);
|
||||
}
|
||||
|
||||
return { ...summaryValue, xValues };
|
||||
}
|
||||
|
||||
function normalizeSummary(summaryValue: HudSummary): HudSummary {
|
||||
return shouldHideSummary(summaryValue.points) ? buildEmptySummary() : summaryValue;
|
||||
}
|
||||
@@ -1076,26 +1231,48 @@
|
||||
return;
|
||||
}
|
||||
signalPanels = showSignalPanels ? packet.panels : buildInactivePanels();
|
||||
const packetHasActiveSummary = hasActiveSummary(packet.summary);
|
||||
if (packet.summary.points.length > 0) {
|
||||
const nowSeconds = Math.round((Date.now() - sessionStartedAt) / 100) / 10;
|
||||
const pointCount = packet.summary.points.length;
|
||||
const spacing =
|
||||
pointCount > 1 ? Math.min(1.2, nowSeconds / Math.max(pointCount - 1, 1)) : 0;
|
||||
const startX = Math.max(0, nowSeconds - spacing * Math.max(pointCount - 1, 0));
|
||||
const xValues = packet.summary.points.map((_, index) => Math.round((startX + index * spacing) * 10) / 10);
|
||||
summary = { ...packet.summary, xValues };
|
||||
if (packetHasActiveSummary) {
|
||||
summaryReleaseHidden = false;
|
||||
summary = buildContinuousSummary(packet.summary);
|
||||
cancelSummaryClear();
|
||||
} else if (summaryReleaseHidden) {
|
||||
summary = buildEmptySummary();
|
||||
} else {
|
||||
summary = packet.summary;
|
||||
summary = buildContinuousSummary(packet.summary);
|
||||
scheduleSummaryClear();
|
||||
}
|
||||
} else if (!summaryReleaseHidden) {
|
||||
scheduleSummaryClear();
|
||||
}
|
||||
pressureMatrix = packet.pressureMatrix;
|
||||
spatialForce = packet.spatialForce ?? null;
|
||||
const nextSpatialForce = packet.spatialForce ?? null;
|
||||
if (packetHasActiveSummary && hasActiveSpatialForce(nextSpatialForce)) {
|
||||
spatialForceReleaseHidden = false;
|
||||
spatialForcePanelVisible = true;
|
||||
spatialForce = nextSpatialForce;
|
||||
cancelSpatialForceClear();
|
||||
} else {
|
||||
spatialForce = null;
|
||||
if (spatialForceReleaseHidden || !spatialForcePanelVisible) {
|
||||
spatialForcePanelVisible = false;
|
||||
}
|
||||
scheduleSpatialForceClear();
|
||||
}
|
||||
hasSignalData =
|
||||
signalPanels.length > 0 ||
|
||||
packet.summary.points.length > 0 ||
|
||||
spatialForce !== null;
|
||||
summary.points.length > 0 ||
|
||||
spatialForce !== null ||
|
||||
spatialForcePanelVisible;
|
||||
}
|
||||
|
||||
function clearHudPanels(): void {
|
||||
cancelSummaryClear();
|
||||
cancelSpatialForceClear();
|
||||
summaryReleaseHidden = true;
|
||||
spatialForceReleaseHidden = false;
|
||||
spatialForcePanelVisible = false;
|
||||
hasSignalData = false;
|
||||
signalPanels = buildInactivePanels();
|
||||
summary = buildEmptySummary();
|
||||
@@ -1815,6 +1992,8 @@
|
||||
return () => {
|
||||
disposed = true;
|
||||
pauseReplayPlayback();
|
||||
cancelSummaryClear();
|
||||
cancelSpatialForceClear();
|
||||
clearDevkitSpatialForce();
|
||||
stopMockFeed?.();
|
||||
unlistenHudStream?.();
|
||||
@@ -1919,6 +2098,8 @@
|
||||
{replayFileName}
|
||||
{replayFrameInfo}
|
||||
{sessionStartedAt}
|
||||
{summaryReleasePending}
|
||||
{spatialForcePanelVisible}
|
||||
resetConfigLabel={uiCopy.resetConfigLabel}
|
||||
applyLiveHint={uiCopy.applyLiveHint}
|
||||
leftPanels={leftSignalPanels}
|
||||
|
||||
Reference in New Issue
Block a user