Compare commits
6 Commits
customer-d
...
0528-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da424d1637 | ||
|
|
2c6fee9aad | ||
|
|
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",
|
"version": "0.4.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2.11.0",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"@tauri-apps/plugin-process": "^2.3.1",
|
"@tauri-apps/plugin-process": "^2.3.1",
|
||||||
"@tauri-apps/plugin-updater": "^2.10.1",
|
"@tauri-apps/plugin-updater": "^2.10.1",
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"@sveltejs/adapter-static": "^3.0.6",
|
"@sveltejs/adapter-static": "^3.0.6",
|
||||||
"@sveltejs/kit": "^2.9.0",
|
"@sveltejs/kit": "^2.9.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||||
"@tauri-apps/cli": "^2",
|
"@tauri-apps/cli": "^2.11.2",
|
||||||
"@types/three": "^0.183.1",
|
"@types/three": "^0.183.1",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
@@ -1032,9 +1032,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tauri-apps/api": {
|
"node_modules/@tauri-apps/api": {
|
||||||
"version": "2.10.1",
|
"version": "2.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.0.tgz",
|
||||||
"integrity": "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==",
|
"integrity": "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==",
|
||||||
"license": "Apache-2.0 OR MIT",
|
"license": "Apache-2.0 OR MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -1042,9 +1042,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tauri-apps/cli": {
|
"node_modules/@tauri-apps/cli": {
|
||||||
"version": "2.10.1",
|
"version": "2.11.2",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.2.tgz",
|
||||||
"integrity": "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g==",
|
"integrity": "sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0 OR MIT",
|
"license": "Apache-2.0 OR MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -1058,23 +1058,23 @@
|
|||||||
"url": "https://opencollective.com/tauri"
|
"url": "https://opencollective.com/tauri"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@tauri-apps/cli-darwin-arm64": "2.10.1",
|
"@tauri-apps/cli-darwin-arm64": "2.11.2",
|
||||||
"@tauri-apps/cli-darwin-x64": "2.10.1",
|
"@tauri-apps/cli-darwin-x64": "2.11.2",
|
||||||
"@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1",
|
"@tauri-apps/cli-linux-arm-gnueabihf": "2.11.2",
|
||||||
"@tauri-apps/cli-linux-arm64-gnu": "2.10.1",
|
"@tauri-apps/cli-linux-arm64-gnu": "2.11.2",
|
||||||
"@tauri-apps/cli-linux-arm64-musl": "2.10.1",
|
"@tauri-apps/cli-linux-arm64-musl": "2.11.2",
|
||||||
"@tauri-apps/cli-linux-riscv64-gnu": "2.10.1",
|
"@tauri-apps/cli-linux-riscv64-gnu": "2.11.2",
|
||||||
"@tauri-apps/cli-linux-x64-gnu": "2.10.1",
|
"@tauri-apps/cli-linux-x64-gnu": "2.11.2",
|
||||||
"@tauri-apps/cli-linux-x64-musl": "2.10.1",
|
"@tauri-apps/cli-linux-x64-musl": "2.11.2",
|
||||||
"@tauri-apps/cli-win32-arm64-msvc": "2.10.1",
|
"@tauri-apps/cli-win32-arm64-msvc": "2.11.2",
|
||||||
"@tauri-apps/cli-win32-ia32-msvc": "2.10.1",
|
"@tauri-apps/cli-win32-ia32-msvc": "2.11.2",
|
||||||
"@tauri-apps/cli-win32-x64-msvc": "2.10.1"
|
"@tauri-apps/cli-win32-x64-msvc": "2.11.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tauri-apps/cli-darwin-arm64": {
|
"node_modules/@tauri-apps/cli-darwin-arm64": {
|
||||||
"version": "2.10.1",
|
"version": "2.11.2",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.2.tgz",
|
||||||
"integrity": "sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ==",
|
"integrity": "sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1089,9 +1089,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tauri-apps/cli-darwin-x64": {
|
"node_modules/@tauri-apps/cli-darwin-x64": {
|
||||||
"version": "2.10.1",
|
"version": "2.11.2",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.2.tgz",
|
||||||
"integrity": "sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw==",
|
"integrity": "sha512-VjYYtZUPqDMLutSfJEyxFE3Bz+DPi7c8wC3imckgvciLDZLq4qwKJxBicg0BXGhXjJsl8vKWgWRFNMPELQ+Xyg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1106,9 +1106,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
|
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
|
||||||
"version": "2.10.1",
|
"version": "2.11.2",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.2.tgz",
|
||||||
"integrity": "sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w==",
|
"integrity": "sha512-yMemD6f4i95AQriS8EazyOFzbE34yjnP16i3IOzpHGQvBoy2DjypFMFBq0NtPuITURv/cOGguRtHR5d79/9CSA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -1123,9 +1123,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
|
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
|
||||||
"version": "2.10.1",
|
"version": "2.11.2",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.2.tgz",
|
||||||
"integrity": "sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA==",
|
"integrity": "sha512-cgI91D2wL8GSgoWwZXDqt+DwnuZCP2/bz03QAE4TrhgAKIsrB4hX26W/H1EONPUUNkqrsgeCD0wU6pcNjV/5kw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1143,9 +1143,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
|
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
|
||||||
"version": "2.10.1",
|
"version": "2.11.2",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.2.tgz",
|
||||||
"integrity": "sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg==",
|
"integrity": "sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1163,9 +1163,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
|
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
|
||||||
"version": "2.10.1",
|
"version": "2.11.2",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.2.tgz",
|
||||||
"integrity": "sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw==",
|
"integrity": "sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
@@ -1183,9 +1183,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
|
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
|
||||||
"version": "2.10.1",
|
"version": "2.11.2",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.2.tgz",
|
||||||
"integrity": "sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw==",
|
"integrity": "sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1203,9 +1203,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tauri-apps/cli-linux-x64-musl": {
|
"node_modules/@tauri-apps/cli-linux-x64-musl": {
|
||||||
"version": "2.10.1",
|
"version": "2.11.2",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.2.tgz",
|
||||||
"integrity": "sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ==",
|
"integrity": "sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1223,9 +1223,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
|
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
|
||||||
"version": "2.10.1",
|
"version": "2.11.2",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.2.tgz",
|
||||||
"integrity": "sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg==",
|
"integrity": "sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1240,9 +1240,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
|
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
|
||||||
"version": "2.10.1",
|
"version": "2.11.2",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.2.tgz",
|
||||||
"integrity": "sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw==",
|
"integrity": "sha512-YhjQNZcXfbkCLyazSv1nPnJ9iRFE1wm6kc51FDbU10/Dk09io+6PAGMLjkxnX2GdM0qMnDmTjstY8mTDVvtKeA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@@ -1257,9 +1257,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
|
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
|
||||||
"version": "2.10.1",
|
"version": "2.11.2",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.2.tgz",
|
||||||
"integrity": "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg==",
|
"integrity": "sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2.11.0",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"@tauri-apps/plugin-process": "^2.3.1",
|
"@tauri-apps/plugin-process": "^2.3.1",
|
||||||
"@tauri-apps/plugin-updater": "^2.10.1",
|
"@tauri-apps/plugin-updater": "^2.10.1",
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"@sveltejs/adapter-static": "^3.0.6",
|
"@sveltejs/adapter-static": "^3.0.6",
|
||||||
"@sveltejs/kit": "^2.9.0",
|
"@sveltejs/kit": "^2.9.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||||
"@tauri-apps/cli": "^2",
|
"@tauri-apps/cli": "^2.11.2",
|
||||||
"@types/three": "^0.183.1",
|
"@types/three": "^0.183.1",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["multi-dim"]
|
default = ["multi-dim"]
|
||||||
|
debug = []
|
||||||
devkit = ["dep:tonic", "dep:prost", "dep:prost-types", "dep:async-stream", "dep:dirs"]
|
devkit = ["dep:tonic", "dep:prost", "dep:prost-types", "dep:async-stream", "dep:dirs"]
|
||||||
multi-dim = ["dep:ndarray"]
|
multi-dim = ["dep:ndarray"]
|
||||||
|
|
||||||
|
|||||||
@@ -122,8 +122,7 @@ pub fn serial_enum() -> Result<Vec<String>, SerialError> {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|p| {
|
.filter_map(|p| {
|
||||||
let name = p.port_name;
|
let name = p.port_name;
|
||||||
#[cfg(unix)]
|
if !should_include_serial_port(&name) {
|
||||||
if !name.contains("USB") {
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
Some(name)
|
Some(name)
|
||||||
@@ -133,6 +132,21 @@ pub fn serial_enum() -> Result<Vec<String>, SerialError> {
|
|||||||
Ok(ports)
|
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]
|
#[tauri::command]
|
||||||
pub async fn serial_connect(
|
pub async fn serial_connect(
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
@@ -185,7 +199,7 @@ pub async fn serial_connect(
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
eprintln!("serial task exited with error: {error}");
|
log::error!("serial task exited with error: {error}");
|
||||||
}
|
}
|
||||||
|
|
||||||
let manager = task_app.state::<SerialConnectionState>();
|
let manager = task_app.state::<SerialConnectionState>();
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ impl Codec<TestFrame> for TestCodec {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let dts = elapsed_millis(session_started_at);
|
let dts = elapsed_millis(session_started_at);
|
||||||
println!("dts_ms: {dts}");
|
|
||||||
frames.push(TestFrame {
|
frames.push(TestFrame {
|
||||||
header: [0xAA, 0x55],
|
header: [0xAA, 0x55],
|
||||||
cmd: cmd,
|
cmd: cmd,
|
||||||
@@ -244,11 +243,10 @@ mod tests {
|
|||||||
fn test_read_csv_basic() -> anyhow::Result<()> {
|
fn test_read_csv_basic() -> anyhow::Result<()> {
|
||||||
let mut rdr = Reader::from_path("recording_20260329_125238.csv")?;
|
let mut rdr = Reader::from_path("recording_20260329_125238.csv")?;
|
||||||
let headers = rdr.headers()?;
|
let headers = rdr.headers()?;
|
||||||
println!("headers: {:?}", headers);
|
|
||||||
|
|
||||||
for result in rdr.records() {
|
for result in rdr.records() {
|
||||||
let record = result?;
|
let record = result?;
|
||||||
println!("record: {:?}", record);
|
let _ = record;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -118,6 +118,10 @@ impl HudChartState {
|
|||||||
push_summary_point(&mut self.summary_points, value);
|
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]) {
|
pub fn record_pressure_matrix(&mut self, values: &[i32]) {
|
||||||
if values.is_empty() {
|
if values.is_empty() {
|
||||||
return;
|
return;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -317,7 +317,7 @@ where
|
|||||||
#[cfg(feature = "multi-dim")]
|
#[cfg(feature = "multi-dim")]
|
||||||
{
|
{
|
||||||
let pzt_values = vals.iter().map(|value| *value as f32).collect::<Vec<f32>>();
|
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) {
|
if PztProcessor::should_report(&analysis) {
|
||||||
spatial_force = Some(HudSpatialForce {
|
spatial_force = Some(HudSpatialForce {
|
||||||
angle_deg: analysis.angle_deg,
|
angle_deg: analysis.angle_deg,
|
||||||
@@ -327,12 +327,15 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(feature = "devkit")]
|
// #[cfg(feature = "devkit")]
|
||||||
{
|
// {
|
||||||
let summary = vals.iter().copied().sum::<i32>();
|
// let summary = vals.iter().copied().sum::<i32>();
|
||||||
let force = raw_to_g1(summary as u32);
|
// #[cfg(feature = "debug")]
|
||||||
push_devkit_frame(&app, vals.as_slice(), frame.dts_ms(), force);
|
// 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 {
|
pending_sub_frame = Some(PendingSubFrame {
|
||||||
frame: frame.clone(),
|
frame: frame.clone(),
|
||||||
@@ -355,13 +358,24 @@ fn build_display_values(
|
|||||||
spatial_force: Option<HudSpatialForce>,
|
spatial_force: Option<HudSpatialForce>,
|
||||||
) -> Option<Vec<i32>> {
|
) -> Option<Vec<i32>> {
|
||||||
let summary = values.iter().copied().sum::<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);
|
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")]
|
#[cfg(feature = "devkit")]
|
||||||
fn push_devkit_frame(app: &AppHandle, values: &[i32], dts_ms: u64, resultant_force: f64) {
|
fn push_devkit_frame(app: &AppHandle, values: &[i32], dts_ms: u64, resultant_force: f64) {
|
||||||
let devkit_state = app.state::<DevKitState>();
|
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 {
|
fn raw_to_g1(raw: u32) -> f64 {
|
||||||
const X: [u32; 12] = [
|
const X: [u32; 12] = [
|
||||||
0, 84402, 117218, 140176, 159126, 175812, 191484, 208758, 224703, 252448, 302361, 352703,
|
0, 21113, 100633, 130204, 156407, 179002, 202500, 244028, 278357, 347117, 399394, 439688
|
||||||
];
|
];
|
||||||
|
|
||||||
const Y: [f64; 12] = [
|
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();
|
let n = X.len();
|
||||||
|
|||||||
@@ -41,9 +41,7 @@
|
|||||||
"template": "nsis/installer.nsi"
|
"template": "nsis/installer.nsi"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"resources": [
|
"resources": []
|
||||||
"resources/je-skin-devkit-server.exe"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"updater": {
|
"updater": {
|
||||||
|
|||||||
@@ -61,6 +61,8 @@
|
|||||||
export let replayFileName = "";
|
export let replayFileName = "";
|
||||||
export let replayFrameInfo = "";
|
export let replayFrameInfo = "";
|
||||||
export let sessionStartedAt: number = Date.now();
|
export let sessionStartedAt: number = Date.now();
|
||||||
|
export let summaryReleasePending = false;
|
||||||
|
export let spatialForcePanelVisible = false;
|
||||||
|
|
||||||
let stagePlaneEl: HTMLDivElement | undefined;
|
let stagePlaneEl: HTMLDivElement | undefined;
|
||||||
let panelZoneEl: HTMLDivElement | undefined;
|
let panelZoneEl: HTMLDivElement | undefined;
|
||||||
@@ -87,7 +89,9 @@
|
|||||||
$: replaySide = summarySide === "left" ? "right" : "left";
|
$: replaySide = summarySide === "left" ? "right" : "left";
|
||||||
$: replayToggleButtonText = replayIsPlaying ? replayPauseLabel : replayPlayLabel;
|
$: replayToggleButtonText = replayIsPlaying ? replayPauseLabel : replayPlayLabel;
|
||||||
$: replayProgressPercent = Math.round(Math.min(1, Math.max(0, replayProgress)) * 100);
|
$: 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";
|
$: isModelStage = stageViewMode === "model3d";
|
||||||
|
|
||||||
function toPxNumber(rawValue: string): number {
|
function toPxNumber(rawValue: string): number {
|
||||||
@@ -276,9 +280,7 @@
|
|||||||
<SignalChart {panel} panelIndex={index} {locale} />
|
<SignalChart {panel} panelIndex={index} {locale} />
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
{#if spatialForcePanelVisible}
|
||||||
|
|
||||||
{#if spatialForce}
|
|
||||||
<div
|
<div
|
||||||
class="panel-motion-shell"
|
class="panel-motion-shell"
|
||||||
in:fly={{ x: 180, duration: 340, opacity: 0.08, easing: cubicOut }}
|
in:fly={{ x: 180, duration: 340, opacity: 0.08, easing: cubicOut }}
|
||||||
@@ -311,7 +313,7 @@
|
|||||||
{sessionStartedAt}
|
{sessionStartedAt}
|
||||||
isRealtime={!replayHasData}
|
isRealtime={!replayHasData}
|
||||||
side="right"
|
side="right"
|
||||||
panelIndex={rightPanels.length}
|
panelIndex={rightPanels.length + (spatialForcePanelVisible ? 1 : 0)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@
|
|||||||
const CAMERA_TARGET_Z = MATRIX_OFFSET_Z - 0.4;
|
const CAMERA_TARGET_Z = MATRIX_OFFSET_Z - 0.4;
|
||||||
const MATRIX_ROTATION_Y = 0;
|
const MATRIX_ROTATION_Y = 0;
|
||||||
const rangeStopPositions = [0, 0.25, 0.5, 0.75, 0.875, 1] as const;
|
const rangeStopPositions = [0, 0.25, 0.5, 0.75, 0.875, 1] as const;
|
||||||
|
const maxDisplayForce = 25.6;
|
||||||
|
|
||||||
const labelVector = new THREE.Vector3();
|
const labelVector = new THREE.Vector3();
|
||||||
$: resolvedColorPalette = pressureColorPalettes[colorMapPreset] ?? pressureColorPalettes.emerald;
|
$: resolvedColorPalette = pressureColorPalettes[colorMapPreset] ?? pressureColorPalettes.emerald;
|
||||||
@@ -145,6 +146,10 @@
|
|||||||
return "--";
|
return "--";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (value >= maxDisplayForce) {
|
||||||
|
return `${maxDisplayForce.toFixed(1)}+`;
|
||||||
|
}
|
||||||
|
|
||||||
return value.toFixed(1);
|
return value.toFixed(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -672,14 +677,6 @@
|
|||||||
<span class="stats-key">{viewerI18n.current}</span>
|
<span class="stats-key">{viewerI18n.current}</span>
|
||||||
<strong class="stats-value">{formatForceStat(stats.current)}</strong>
|
<strong class="stats-value">{formatForceStat(stats.current)}</strong>
|
||||||
</article>
|
</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>
|
</div>
|
||||||
<p class="stats-note">{statsNote}</p>
|
<p class="stats-note">{statsNote}</p>
|
||||||
</section>
|
</section>
|
||||||
@@ -768,7 +765,7 @@
|
|||||||
|
|
||||||
.stats-grid {
|
.stats-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: minmax(0, 1fr);
|
||||||
gap: 0.46rem;
|
gap: 0.46rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
const viewportWidth = 100;
|
const viewportWidth = 100;
|
||||||
const viewportHeight = 36;
|
const viewportHeight = 36;
|
||||||
|
const maxDisplayForce = 25.6;
|
||||||
|
|
||||||
const toneColorMap: Record<SignalTone, string> = {
|
const toneColorMap: Record<SignalTone, string> = {
|
||||||
cyan: "62 232 255",
|
cyan: "62 232 255",
|
||||||
@@ -32,7 +33,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatMetric(value: number | null): string {
|
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 } {
|
function resolveBounds(seriesCollection: HudSignalPanel["series"]): { min: number; max: number } {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
return delta === -180 ? 180 : delta;
|
return delta === -180 ? 180 : delta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const displayAngleOffsetDeg = 90;
|
||||||
const jumpAngleThresholdDeg = 72;
|
const jumpAngleThresholdDeg = 72;
|
||||||
|
|
||||||
let visualAngleDeg = 0;
|
let visualAngleDeg = 0;
|
||||||
@@ -94,14 +95,14 @@
|
|||||||
$: hasData =
|
$: hasData =
|
||||||
spatialForce !== null &&
|
spatialForce !== null &&
|
||||||
Number.isFinite(spatialForce.angleDeg) &&
|
Number.isFinite(spatialForce.angleDeg) &&
|
||||||
(!requireMagnitude || Number.isFinite(spatialForce.magnitude));
|
(!requireMagnitude || (Number.isFinite(spatialForce.magnitude) && Math.abs(spatialForce.magnitude) >= 0.0001));
|
||||||
$: angleDeg = hasData ? normalizeAngle(spatialForce?.angleDeg ?? 0) : 0;
|
$: angleDeg = hasData ? normalizeAngle((spatialForce?.angleDeg ?? 0) + displayAngleOffsetDeg) : 0;
|
||||||
$: updateVisualAngle(angleDeg, hasData);
|
$: updateVisualAngle(angleDeg, hasData);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if hasData}
|
|
||||||
<article
|
<article
|
||||||
class="signal-panel spatial-panel side-{side}"
|
class="signal-panel spatial-panel side-{side}"
|
||||||
|
class:is-empty={!hasData}
|
||||||
aria-label={resolvedTitle}
|
aria-label={resolvedTitle}
|
||||||
style="--panel-index: {panelIndex};"
|
style="--panel-index: {panelIndex};"
|
||||||
>
|
>
|
||||||
@@ -125,6 +126,7 @@
|
|||||||
<div class="compass-ring compass-ring-inner"></div>
|
<div class="compass-ring compass-ring-inner"></div>
|
||||||
<div class="compass-axis axis-horizontal"></div>
|
<div class="compass-axis axis-horizontal"></div>
|
||||||
<div class="compass-axis axis-vertical"></div>
|
<div class="compass-axis axis-vertical"></div>
|
||||||
|
{#if hasData}
|
||||||
<div
|
<div
|
||||||
class="compass-vector"
|
class="compass-vector"
|
||||||
class:is-snap={snapVector}
|
class:is-snap={snapVector}
|
||||||
@@ -133,16 +135,17 @@
|
|||||||
<span class="vector-shaft"></span>
|
<span class="vector-shaft"></span>
|
||||||
<span class="vector-head"></span>
|
<span class="vector-head"></span>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
<div class="compass-center"></div>
|
<div class="compass-center"></div>
|
||||||
<span class="compass-label label-top">90</span>
|
<span class="compass-label label-top">0</span>
|
||||||
<span class="compass-label label-right">0</span>
|
<span class="compass-label label-right">270</span>
|
||||||
<span class="compass-label label-bottom">270</span>
|
<span class="compass-label label-bottom">180</span>
|
||||||
<span class="compass-label label-left">180</span>
|
<span class="compass-label label-left">90</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
{/if}
|
|
||||||
<style>
|
<style>
|
||||||
.signal-panel {
|
.signal-panel {
|
||||||
--offset-x: 12%;
|
--offset-x: 12%;
|
||||||
@@ -261,14 +264,16 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
gap: 0.72rem;
|
gap: 0.72rem;
|
||||||
block-size: clamp(12rem, 15.5vw, 15rem);
|
block-size: clamp(14rem, 17vw, 16.5rem);
|
||||||
min-block-size: 5rem;
|
min-block-size: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.compass-stage {
|
.compass-stage {
|
||||||
position: relative;
|
position: relative;
|
||||||
min-block-size: 0;
|
min-block-size: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
padding: 1.35rem 1.7rem;
|
||||||
border: 1px solid rgb(var(--hud-border-strong-rgb) / 0.32);
|
border: 1px solid rgb(var(--hud-border-strong-rgb) / 0.32);
|
||||||
border-radius: 0.62rem;
|
border-radius: 0.62rem;
|
||||||
background:
|
background:
|
||||||
@@ -280,7 +285,8 @@
|
|||||||
|
|
||||||
.compass-core {
|
.compass-core {
|
||||||
position: relative;
|
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;
|
aspect-ratio: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,26 +385,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.label-top {
|
.label-top {
|
||||||
top: -0.9rem;
|
top: 0.35rem;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.label-right {
|
.label-right {
|
||||||
top: 50%;
|
top: 50%;
|
||||||
right: -1rem;
|
right: 0.42rem;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.label-bottom {
|
.label-bottom {
|
||||||
bottom: -0.9rem;
|
bottom: 0.35rem;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.label-left {
|
.label-left {
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: -1.35rem;
|
left: 0.42rem;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,7 +427,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.panel-body {
|
.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 {
|
.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 {
|
.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 {
|
.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 sessionStartedAt: number = Date.now();
|
||||||
export let isRealtime = false;
|
export let isRealtime = false;
|
||||||
|
|
||||||
|
let canvasEl: HTMLCanvasElement | undefined;
|
||||||
let currentTimeSeconds = 0;
|
let currentTimeSeconds = 0;
|
||||||
let timerId: ReturnType<typeof setInterval> | null = null;
|
let timerId: ReturnType<typeof setInterval> | null = null;
|
||||||
|
let resizeObserver: ResizeObserver | null = null;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
timerId = setInterval(() => {
|
timerId = setInterval(() => {
|
||||||
currentTimeSeconds = Math.round((Date.now() - sessionStartedAt) / 100) / 10;
|
currentTimeSeconds = Math.round((Date.now() - sessionStartedAt) / 100) / 10;
|
||||||
}, 200);
|
}, 200);
|
||||||
|
|
||||||
|
if (canvasEl) {
|
||||||
|
resizeObserver = new ResizeObserver(() => {
|
||||||
|
drawCanvas(canvasEl, plotPoints, yAxisTicks, xAxisTicks, sampleCount);
|
||||||
|
});
|
||||||
|
resizeObserver.observe(canvasEl);
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (timerId != null) clearInterval(timerId);
|
if (timerId != null) clearInterval(timerId);
|
||||||
|
resizeObserver?.disconnect();
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -33,7 +44,8 @@
|
|||||||
const plotInsetRight = 4;
|
const plotInsetRight = 4;
|
||||||
const plotInsetTop = 4;
|
const plotInsetTop = 4;
|
||||||
const plotInsetBottom = 9;
|
const plotInsetBottom = 9;
|
||||||
const fixedYBounds = { min: 0, max: 25 };
|
const maxDisplayForce = 25.6;
|
||||||
|
const fixedYBounds = { min: 0, max: maxDisplayForce };
|
||||||
|
|
||||||
interface CurveSample {
|
interface CurveSample {
|
||||||
x: number;
|
x: number;
|
||||||
@@ -56,14 +68,6 @@
|
|||||||
return Math.min(max, Math.max(min, value));
|
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 {
|
function formatAxisValue(value: number, axis: "x" | "y"): string {
|
||||||
if (!Number.isFinite(value)) {
|
if (!Number.isFinite(value)) {
|
||||||
return "--";
|
return "--";
|
||||||
@@ -225,24 +229,117 @@
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function createLinePath(points: PlotPoint[]): string {
|
function drawCanvas(
|
||||||
if (points.length === 0) {
|
canvas: HTMLCanvasElement | undefined,
|
||||||
return "";
|
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 {
|
const cssWidth = Math.max(1, canvas.clientWidth);
|
||||||
if (points.length < 2) {
|
const cssHeight = Math.max(1, canvas.clientHeight);
|
||||||
return "";
|
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);
|
context.setTransform(targetWidth / viewportWidth, 0, 0, targetHeight / viewportHeight, 0, 0);
|
||||||
const firstPoint = points[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];
|
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;
|
$: sourceYValues = yValues && yValues.length ? yValues : summary.points;
|
||||||
@@ -261,14 +358,9 @@
|
|||||||
$: xDataBounds = resolveDataBounds(samples.map((sample) => sample.x));
|
$: xDataBounds = resolveDataBounds(samples.map((sample) => sample.x));
|
||||||
$: yDataBounds = resolveDataBounds(samples.map((sample) => sample.y));
|
$: yDataBounds = resolveDataBounds(samples.map((sample) => sample.y));
|
||||||
$: plotPoints = convertPoints(samples, xScaleBounds, yScaleBounds);
|
$: 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) : [];
|
$: yAxisTicks = sampleCount > 0 ? buildYAxisTicks(yScaleBounds, yDataBounds) : [];
|
||||||
$: xAxisTicks = sampleCount > 0 ? buildXAxisTicks(xScaleBounds) : [];
|
$: xAxisTicks = sampleCount > 0 ? buildXAxisTicks(xScaleBounds) : [];
|
||||||
$: latestValue = formatValue(summary.latest);
|
$: drawCanvas(canvasEl, plotPoints, yAxisTicks, xAxisTicks, sampleCount);
|
||||||
$: minValue = formatValue(summary.min);
|
|
||||||
$: maxValue = formatValue(summary.max);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<article
|
<article
|
||||||
@@ -280,62 +372,12 @@
|
|||||||
<header class="panel-head">
|
<header class="panel-head">
|
||||||
<div class="head-text">
|
<div class="head-text">
|
||||||
<p class="panel-code">RF</p>
|
<p class="panel-code">RF</p>
|
||||||
<p class="panel-title">{summary.label}</p>
|
<p class="panel-title">{locale === "zh-CN" ? "合力" : "Resultant Force"}</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>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="chart-stage">
|
<div class="chart-stage">
|
||||||
<svg viewBox="0 0 {viewportWidth} {viewportHeight}" preserveAspectRatio="none" role="img" aria-label={summary.label}>
|
<canvas bind:this={canvasEl} aria-label={summary.label}></canvas>
|
||||||
<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>
|
|
||||||
|
|
||||||
{#if sampleCount === 0}
|
{#if sampleCount === 0}
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
@@ -343,24 +385,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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>
|
</article>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -369,10 +393,13 @@
|
|||||||
--enter-ms: 1800ms;
|
--enter-ms: 1800ms;
|
||||||
--fade-ms: 1000ms;
|
--fade-ms: 1000ms;
|
||||||
overflow: hidden;
|
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;
|
justify-self: start;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto 1fr auto;
|
grid-template-rows: auto 1fr;
|
||||||
gap: 0.68rem;
|
gap: 0.68rem;
|
||||||
padding: 0.88rem 0.96rem 1rem;
|
padding: 0.88rem 0.96rem 1rem;
|
||||||
border: 1px solid rgb(var(--hud-border-strong-rgb) / 0.42);
|
border: 1px solid rgb(var(--hud-border-strong-rgb) / 0.42);
|
||||||
@@ -438,36 +465,6 @@
|
|||||||
letter-spacing: 0.05em;
|
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 {
|
.chart-stage {
|
||||||
position: relative;
|
position: relative;
|
||||||
block-size: clamp(12rem, 15.5vw, 15rem);
|
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%);
|
radial-gradient(circle at 50% 0, rgb(var(--hud-glow-rgb) / 0.09), transparent 45%);
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
canvas {
|
||||||
display: block;
|
display: block;
|
||||||
inline-size: 100%;
|
inline-size: 100%;
|
||||||
block-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 {
|
.empty-state {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
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));
|
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) {
|
@media (max-width: 1180px) {
|
||||||
.signal-panel {
|
.signal-panel {
|
||||||
inline-size: min(100%, clamp(28rem, 40vw, 38rem));
|
inline-size: var(--rail-width, min(100%, clamp(28rem, 40vw, 38rem)));
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-stage {
|
.chart-stage {
|
||||||
@@ -601,7 +508,7 @@
|
|||||||
|
|
||||||
@media (max-height: 900px) {
|
@media (max-height: 900px) {
|
||||||
.signal-panel {
|
.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;
|
padding: 0.7rem 0.76rem 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -612,7 +519,7 @@
|
|||||||
|
|
||||||
@media (max-height: 760px) {
|
@media (max-height: 760px) {
|
||||||
.signal-panel {
|
.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;
|
padding: 0.62rem 0.68rem 0.72rem;
|
||||||
gap: 0.48rem;
|
gap: 0.48rem;
|
||||||
}
|
}
|
||||||
@@ -624,7 +531,7 @@
|
|||||||
|
|
||||||
@media (max-height: 680px) {
|
@media (max-height: 680px) {
|
||||||
.signal-panel {
|
.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;
|
padding: 0.52rem 0.58rem 0.6rem;
|
||||||
gap: 0.36rem;
|
gap: 0.36rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,6 +194,9 @@
|
|||||||
const pointsPerSeries = 28;
|
const pointsPerSeries = 28;
|
||||||
const summaryPointsPerSeries = 42;
|
const summaryPointsPerSeries = 42;
|
||||||
const signalRenderTickMs = 1200;
|
const signalRenderTickMs = 1200;
|
||||||
|
const summaryReleaseForceEpsilon = 0.1;
|
||||||
|
const releaseClearDelayMs = 5000;
|
||||||
|
const spatialForceMagnitudeEpsilon = 0.0001;
|
||||||
const replayDefaultFrameMs = 40;
|
const replayDefaultFrameMs = 40;
|
||||||
const showSignalPanels = false;
|
const showSignalPanels = false;
|
||||||
const mockToneCycle: SignalTone[] = ["cyan", "lime", "orange", "violet", "gold", "rose"];
|
const mockToneCycle: SignalTone[] = ["cyan", "lime", "orange", "violet", "gold", "rose"];
|
||||||
@@ -288,6 +291,11 @@
|
|||||||
} | null = null;
|
} | null = null;
|
||||||
let devkitStatusTimer: number | null = null;
|
let devkitStatusTimer: number | null = null;
|
||||||
let devkitSpatialForceClearTimer: 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();
|
let sessionStartedAt: number = Date.now();
|
||||||
|
|
||||||
$: uiCopy = copyByLocale[locale];
|
$: uiCopy = copyByLocale[locale];
|
||||||
@@ -298,6 +306,7 @@
|
|||||||
$: rangeScaleStyle = buildRangeScaleStyle(colorMapPreset);
|
$: rangeScaleStyle = buildRangeScaleStyle(colorMapPreset);
|
||||||
$: replayHasData = replayFrames.length > 0;
|
$: replayHasData = replayFrames.length > 0;
|
||||||
$: replayFrameInfo = replayHasData ? `${replayHasDisplayedFrame ? replayCurrentIndex + 1 : 0}/${replayFrames.length}` : "";
|
$: replayFrameInfo = replayHasData ? `${replayHasDisplayedFrame ? replayCurrentIndex + 1 : 0}/${replayFrames.length}` : "";
|
||||||
|
$: summaryReleasePending = summaryClearTimer != null;
|
||||||
$: fileExplorerTitle =
|
$: fileExplorerTitle =
|
||||||
fileExplorerMode === "open" ? uiCopy.fileExplorerImportTitle : uiCopy.fileExplorerExportTitle;
|
fileExplorerMode === "open" ? uiCopy.fileExplorerImportTitle : uiCopy.fileExplorerExportTitle;
|
||||||
$: fileExplorerConfirmLabel =
|
$: fileExplorerConfirmLabel =
|
||||||
@@ -313,7 +322,11 @@
|
|||||||
window.clearTimeout(devkitSpatialForceClearTimer);
|
window.clearTimeout(devkitSpatialForceClearTimer);
|
||||||
devkitSpatialForceClearTimer = null;
|
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 {
|
function scheduleDevkitSpatialForceClear(): void {
|
||||||
@@ -332,6 +345,54 @@
|
|||||||
}, 420);
|
}, 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 {
|
function clamp(value: number, min: number, max: number): number {
|
||||||
return Math.min(max, Math.max(min, value));
|
return Math.min(max, Math.max(min, value));
|
||||||
}
|
}
|
||||||
@@ -792,6 +853,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resetReplayVisualState(): void {
|
function resetReplayVisualState(): void {
|
||||||
|
cancelSummaryClear();
|
||||||
|
cancelSpatialForceClear();
|
||||||
|
spatialForceReleaseHidden = false;
|
||||||
|
spatialForcePanelVisible = false;
|
||||||
pressureMatrix = buildZeroMatrix();
|
pressureMatrix = buildZeroMatrix();
|
||||||
spatialForce = null;
|
spatialForce = null;
|
||||||
replayPendingDevkitSeq = null;
|
replayPendingDevkitSeq = null;
|
||||||
@@ -831,8 +896,12 @@
|
|||||||
replayHasDisplayedFrame = true;
|
replayHasDisplayedFrame = true;
|
||||||
replayProgress = replayFrames.length > 1 ? safeIndex / (replayFrames.length - 1) : 1;
|
replayProgress = replayFrames.length > 1 ? safeIndex / (replayFrames.length - 1) : 1;
|
||||||
const frame = replayFrames[safeIndex];
|
const frame = replayFrames[safeIndex];
|
||||||
|
cancelSummaryClear();
|
||||||
|
cancelSpatialForceClear();
|
||||||
|
spatialForceReleaseHidden = false;
|
||||||
pressureMatrix = frameValuesToMatrix(frame.values);
|
pressureMatrix = frameValuesToMatrix(frame.values);
|
||||||
spatialForce = frame.spatialForce ?? null;
|
spatialForce = frame.spatialForce ?? null;
|
||||||
|
spatialForcePanelVisible = spatialForce !== null;
|
||||||
pushReplayFrameToDevkit(frame, safeIndex);
|
pushReplayFrameToDevkit(frame, safeIndex);
|
||||||
signalPanels = buildInactivePanels();
|
signalPanels = buildInactivePanels();
|
||||||
summary = buildReplaySummaryAt(safeIndex);
|
summary = buildReplaySummaryAt(safeIndex);
|
||||||
@@ -1002,13 +1071,99 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isZeroLikeValue(value: number): boolean {
|
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 {
|
function shouldHideSummary(points: number[]): boolean {
|
||||||
return points.length === 0 || points.every((value) => isZeroLikeValue(value));
|
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 {
|
function normalizeSummary(summaryValue: HudSummary): HudSummary {
|
||||||
return shouldHideSummary(summaryValue.points) ? buildEmptySummary() : summaryValue;
|
return shouldHideSummary(summaryValue.points) ? buildEmptySummary() : summaryValue;
|
||||||
}
|
}
|
||||||
@@ -1076,26 +1231,48 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
signalPanels = showSignalPanels ? packet.panels : buildInactivePanels();
|
signalPanels = showSignalPanels ? packet.panels : buildInactivePanels();
|
||||||
|
const packetHasActiveSummary = hasActiveSummary(packet.summary);
|
||||||
if (packet.summary.points.length > 0) {
|
if (packet.summary.points.length > 0) {
|
||||||
const nowSeconds = Math.round((Date.now() - sessionStartedAt) / 100) / 10;
|
if (packetHasActiveSummary) {
|
||||||
const pointCount = packet.summary.points.length;
|
summaryReleaseHidden = false;
|
||||||
const spacing =
|
summary = buildContinuousSummary(packet.summary);
|
||||||
pointCount > 1 ? Math.min(1.2, nowSeconds / Math.max(pointCount - 1, 1)) : 0;
|
cancelSummaryClear();
|
||||||
const startX = Math.max(0, nowSeconds - spacing * Math.max(pointCount - 1, 0));
|
} else if (summaryReleaseHidden) {
|
||||||
const xValues = packet.summary.points.map((_, index) => Math.round((startX + index * spacing) * 10) / 10);
|
summary = buildEmptySummary();
|
||||||
summary = { ...packet.summary, xValues };
|
|
||||||
} else {
|
} else {
|
||||||
summary = packet.summary;
|
summary = buildContinuousSummary(packet.summary);
|
||||||
|
scheduleSummaryClear();
|
||||||
|
}
|
||||||
|
} else if (!summaryReleaseHidden) {
|
||||||
|
scheduleSummaryClear();
|
||||||
}
|
}
|
||||||
pressureMatrix = packet.pressureMatrix;
|
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 =
|
hasSignalData =
|
||||||
signalPanels.length > 0 ||
|
signalPanels.length > 0 ||
|
||||||
packet.summary.points.length > 0 ||
|
summary.points.length > 0 ||
|
||||||
spatialForce !== null;
|
spatialForce !== null ||
|
||||||
|
spatialForcePanelVisible;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearHudPanels(): void {
|
function clearHudPanels(): void {
|
||||||
|
cancelSummaryClear();
|
||||||
|
cancelSpatialForceClear();
|
||||||
|
summaryReleaseHidden = true;
|
||||||
|
spatialForceReleaseHidden = false;
|
||||||
|
spatialForcePanelVisible = false;
|
||||||
hasSignalData = false;
|
hasSignalData = false;
|
||||||
signalPanels = buildInactivePanels();
|
signalPanels = buildInactivePanels();
|
||||||
summary = buildEmptySummary();
|
summary = buildEmptySummary();
|
||||||
@@ -1815,6 +1992,8 @@
|
|||||||
return () => {
|
return () => {
|
||||||
disposed = true;
|
disposed = true;
|
||||||
pauseReplayPlayback();
|
pauseReplayPlayback();
|
||||||
|
cancelSummaryClear();
|
||||||
|
cancelSpatialForceClear();
|
||||||
clearDevkitSpatialForce();
|
clearDevkitSpatialForce();
|
||||||
stopMockFeed?.();
|
stopMockFeed?.();
|
||||||
unlistenHudStream?.();
|
unlistenHudStream?.();
|
||||||
@@ -1919,6 +2098,8 @@
|
|||||||
{replayFileName}
|
{replayFileName}
|
||||||
{replayFrameInfo}
|
{replayFrameInfo}
|
||||||
{sessionStartedAt}
|
{sessionStartedAt}
|
||||||
|
{summaryReleasePending}
|
||||||
|
{spatialForcePanelVisible}
|
||||||
resetConfigLabel={uiCopy.resetConfigLabel}
|
resetConfigLabel={uiCopy.resetConfigLabel}
|
||||||
applyLiveHint={uiCopy.applyLiveHint}
|
applyLiveHint={uiCopy.applyLiveHint}
|
||||||
leftPanels={leftSignalPanels}
|
leftPanels={leftSignalPanels}
|
||||||
|
|||||||
Reference in New Issue
Block a user