Prepare sample delivery UI
This commit is contained in:
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",
|
||||
|
||||
@@ -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,
|
||||
@@ -355,11 +355,81 @@ 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])
|
||||
|
||||
match ad_to_x(summary as f64) {
|
||||
Some(x) => {
|
||||
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])
|
||||
}
|
||||
None => {
|
||||
chart_state.record_pressure_matrix(values);
|
||||
chart_state.record_summary(MAX_DISPLAY_FORCE as f32);
|
||||
Some(vec![MAX_DISPLAY_FORCE.round() as i32])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MIN_DISPLAY_FORCE: f64 = 0.1;
|
||||
const MAX_DISPLAY_FORCE: f64 = 25.6;
|
||||
|
||||
fn ad_to_x(ad: f64) -> Option<f64> {
|
||||
const CUBIC_LIMIT: f64 = 6.57;
|
||||
const QUADRATIC_A: f64 = -377.8;
|
||||
const QUADRATIC_B: f64 = 26040.0;
|
||||
const EPSILON: f64 = 0.000_001;
|
||||
|
||||
let cubic_min = ad_from_cubic_x(0.0);
|
||||
if ad <= cubic_min {
|
||||
return Some(0.0);
|
||||
}
|
||||
|
||||
let cubic_threshold = ad_from_cubic_x(CUBIC_LIMIT);
|
||||
if ad <= cubic_threshold {
|
||||
return Some(solve_monotonic_ad(ad, 0.0, CUBIC_LIMIT, ad_from_cubic_x));
|
||||
}
|
||||
|
||||
let quadratic_vertex = -QUADRATIC_B / (2.0 * QUADRATIC_A);
|
||||
let quadratic_max = ad_from_quadratic_x(quadratic_vertex);
|
||||
if ad - EPSILON > quadratic_max {
|
||||
return Some(MAX_DISPLAY_FORCE);
|
||||
}
|
||||
|
||||
Some(solve_monotonic_ad(
|
||||
ad,
|
||||
CUBIC_LIMIT,
|
||||
quadratic_vertex,
|
||||
ad_from_quadratic_x,
|
||||
))
|
||||
}
|
||||
|
||||
fn solve_monotonic_ad(ad: f64, mut low: f64, mut high: f64, f: fn(f64) -> f64) -> f64 {
|
||||
for _ in 0..80 {
|
||||
let mid = (low + high) / 2.0;
|
||||
if f(mid) < ad {
|
||||
low = mid;
|
||||
} else {
|
||||
high = mid;
|
||||
}
|
||||
}
|
||||
|
||||
(low + high) / 2.0
|
||||
}
|
||||
|
||||
fn ad_from_cubic_x(x: f64) -> f64 {
|
||||
-5.732 * x.powi(3) - 131.5 * x.powi(2) + 31980.0 * x + 13490.0
|
||||
}
|
||||
|
||||
fn ad_from_quadratic_x(x: f64) -> f64 {
|
||||
-377.8 * x.powi(2) + 26040.0 * x + 51120.0
|
||||
}
|
||||
|
||||
#[cfg(feature = "devkit")]
|
||||
@@ -414,6 +484,7 @@ fn infer_matrix_shape(len: usize) -> (u32, u32) {
|
||||
(best.0 as u32, best.1 as u32)
|
||||
}
|
||||
|
||||
#[cfg(feature = "devkit")]
|
||||
fn raw_to_g1(raw: u32) -> f64 {
|
||||
const X: [u32; 12] = [
|
||||
0, 84402, 117218, 140176, 159126, 175812, 191484, 208758, 224703, 252448, 302361, 352703,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 } {
|
||||
|
||||
@@ -94,17 +94,17 @@
|
||||
$: hasData =
|
||||
spatialForce !== null &&
|
||||
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;
|
||||
$: 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 +125,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,6 +134,7 @@
|
||||
<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>
|
||||
@@ -140,9 +142,9 @@
|
||||
<span class="compass-label label-left">180</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</article>
|
||||
{/if}
|
||||
</article>
|
||||
<style>
|
||||
.signal-panel {
|
||||
--offset-x: 12%;
|
||||
@@ -261,14 +263,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 +284,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 +384,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 +426,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 +441,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 +461,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 +482,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;
|
||||
@@ -61,6 +73,10 @@
|
||||
return "--";
|
||||
}
|
||||
|
||||
if (value >= maxDisplayForce) {
|
||||
return `${maxDisplayForce.toFixed(1)}+`;
|
||||
}
|
||||
|
||||
return value.toFixed(1);
|
||||
}
|
||||
|
||||
@@ -225,24 +241,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,11 +370,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) : [];
|
||||
$: drawCanvas(canvasEl, plotPoints, yAxisTicks, xAxisTicks, sampleCount);
|
||||
$: latestValue = formatValue(summary.latest);
|
||||
$: minValue = formatValue(summary.min);
|
||||
$: maxValue = formatValue(summary.max);
|
||||
@@ -291,51 +398,7 @@
|
||||
</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">
|
||||
@@ -369,7 +432,10 @@
|
||||
--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;
|
||||
@@ -480,53 +546,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;
|
||||
@@ -591,7 +616,7 @@
|
||||
|
||||
@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 +626,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 +637,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 +649,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 = 2.0;
|
||||
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 = false;
|
||||
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 = false;
|
||||
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