diff --git a/src/lib/components/CenterStage.svelte b/src/lib/components/CenterStage.svelte
index 896d090..54110c0 100644
--- a/src/lib/components/CenterStage.svelte
+++ b/src/lib/components/CenterStage.svelte
@@ -29,6 +29,7 @@
export let summary: HudSummary;
export let pressureMatrix: number[] | null = null;
export let spatialForce: HudSpatialForce | null = null;
+ export let devkitSpatialForce: HudSpatialForce | null = null;
export let showConfigPanel = false;
export let configPanelTitle = "";
export let configPanelHint = "";
@@ -327,6 +328,29 @@
{locale}
side="right"
panelIndex={rightPanels.length}
+ panelCode="ALG"
+ panelTitle={locale === "zh-CN" ? "本地切向力" : "Local Tangential"}
+ badgeLabel={locale === "zh-CN" ? "算法" : "ALGO"}
+ />
+
+
+
+
diff --git a/src/lib/components/SpatialForcePanel.svelte b/src/lib/components/SpatialForcePanel.svelte
index 6476cd1..551294a 100644
--- a/src/lib/components/SpatialForcePanel.svelte
+++ b/src/lib/components/SpatialForcePanel.svelte
@@ -5,6 +5,13 @@
export let side: "left" | "right" = "right";
export let panelIndex = 0;
export let locale: "zh-CN" | "en-US" = "zh-CN";
+ export let panelCode = "TAN";
+ export let panelTitle = "";
+ export let badgeLabel = "";
+ export let badgeTone: "cyan" | "lime" | "orange" = "cyan";
+ export let showMetrics = true;
+ export let requireMagnitude = true;
+ export let compactMetaText = "";
function formatValue(value: number | null, digits = 1): string {
if (value === null || !Number.isFinite(value)) {
@@ -91,11 +98,15 @@
strength: "Strength",
confidence: "Confidence"
};
+ $: resolvedTitle = panelTitle || i18n.title;
+ $: resolvedBadgeLabel = badgeLabel || i18n.angle;
+ $: resolvedCompactMetaText =
+ compactMetaText || (locale === "zh-CN" ? "仅使用角度流" : "Angle stream only");
$: hasData =
spatialForce !== null &&
Number.isFinite(spatialForce.angleDeg) &&
- Number.isFinite(spatialForce.magnitude);
+ (!requireMagnitude || Number.isFinite(spatialForce.magnitude));
$: angleDeg = hasData ? normalizeAngle(spatialForce?.angleDeg ?? 0) : 0;
$: updateVisualAngle(angleDeg, hasData);
$: magnitude = hasData ? spatialForce?.magnitude ?? 0 : null;
@@ -110,12 +121,12 @@
>
@@ -152,8 +163,13 @@
{i18n.heading}
-
{i18n.strength}: {formatValue(magnitude, 2)}
-
{i18n.confidence}: {hasData ? `${formatValue(confidence, 0)}%` : "--"}
+ {#if showMetrics}
+
{i18n.strength}: {formatValue(magnitude, 2)}
+
{i18n.confidence}: {hasData ? `${formatValue(confidence, 0)}%` : "--"}
+ {:else}
+
{resolvedCompactMetaText}
+
{hasData ? (locale === "zh-CN" ? "实时对比中" : "Live comparison") : "--"}
+ {/if}
@@ -251,6 +267,14 @@
border-color: rgb(var(--hud-cyan-rgb) / 0.54);
}
+ .icon-chip.tone-lime {
+ border-color: rgb(var(--hud-lime-rgb) / 0.54);
+ }
+
+ .icon-chip.tone-orange {
+ border-color: rgb(var(--hud-orange-rgb) / 0.54);
+ }
+
.panel-body {
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(10rem, 0.9fr);
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 258f62a..7bb9edb 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -46,6 +46,13 @@
dtsMs: number;
}
+ interface DevKitPztAngleEvent {
+ seq: number;
+ timestampMs: number;
+ dtsMs: number;
+ angle: number;
+ }
+
const copyByLocale: Record = {
"zh-CN": {
appName: "JE-Skin",
@@ -230,6 +237,7 @@
let summary: HudSummary = buildEmptySummary();
let pressureMatrix: number[] | null = null;
let spatialForce: HudSpatialForce | null = null;
+ let devkitSpatialForce: HudSpatialForce | null = null;
let matrixRows = 12;
let matrixCols = 7;
let rangeMin = DEFAULT_PRESSURE_RANGE_MIN;
@@ -270,6 +278,7 @@
rowsKept: number;
} | null = null;
let devkitStatusTimer: number | null = null;
+ let devkitSpatialForceClearTimer: number | null = null;
let sessionStartedAt: number = Date.now();
$: uiCopy = copyByLocale[locale];
@@ -297,6 +306,31 @@
return typeof window !== "undefined" && "__TAURI_INTERNALS__" in window;
}
+ function clearDevkitSpatialForce(): void {
+ devkitSpatialForce = null;
+ if (devkitSpatialForceClearTimer != null && typeof window !== "undefined") {
+ window.clearTimeout(devkitSpatialForceClearTimer);
+ devkitSpatialForceClearTimer = null;
+ }
+ hasSignalData = signalPanels.length > 0 || summary.points.length > 0 || spatialForce !== null;
+ }
+
+ function scheduleDevkitSpatialForceClear(): void {
+ if (typeof window === "undefined") {
+ return;
+ }
+
+ if (devkitSpatialForceClearTimer != null) {
+ window.clearTimeout(devkitSpatialForceClearTimer);
+ }
+
+ devkitSpatialForceClearTimer = window.setTimeout(() => {
+ devkitSpatialForce = null;
+ devkitSpatialForceClearTimer = null;
+ hasSignalData = signalPanels.length > 0 || summary.points.length > 0 || spatialForce !== null;
+ }, 420);
+ }
+
function clamp(value: number, min: number, max: number): number {
return Math.min(max, Math.max(min, value));
}
@@ -720,6 +754,7 @@
function resetReplayVisualState(): void {
pressureMatrix = buildZeroMatrix();
spatialForce = null;
+ clearDevkitSpatialForce();
signalPanels = buildInactivePanels();
summary = buildEmptySummary();
hasSignalData = false;
@@ -756,6 +791,7 @@
replayProgress = replayFrames.length > 1 ? safeIndex / (replayFrames.length - 1) : 1;
pressureMatrix = frameValuesToMatrix(replayFrames[safeIndex].values);
spatialForce = null;
+ clearDevkitSpatialForce();
signalPanels = buildInactivePanels();
summary = buildReplaySummaryAt(safeIndex);
hasSignalData = true;
@@ -1011,7 +1047,11 @@
}
pressureMatrix = packet.pressureMatrix;
spatialForce = packet.spatialForce ?? null;
- hasSignalData = signalPanels.length > 0 || packet.summary.points.length > 0 || spatialForce !== null;
+ hasSignalData =
+ signalPanels.length > 0 ||
+ packet.summary.points.length > 0 ||
+ spatialForce !== null ||
+ devkitSpatialForce !== null;
}
function clearHudPanels(): void {
@@ -1020,6 +1060,7 @@
summary = buildEmptySummary();
pressureMatrix = null;
spatialForce = null;
+ clearDevkitSpatialForce();
}
function startMockFeed(push: (packet: HudPacket) => void): () => void {
@@ -1794,12 +1835,25 @@
.catch((error) => {
console.error("Failed to listen for hud_stream:", error);
});
- void listen<{ seq: number; timestampMs: number; dtsMs: number; angle: number }>(
- "devkit_pzt_angle",
- (event) => {
- console.log("[devkit_pzt_angle]", event.payload);
+ void listen("devkit_pzt_angle", (event) => {
+ const angleDeg = Number(event.payload.angle);
+ if (!Number.isFinite(angleDeg)) {
+ clearDevkitSpatialForce();
+ return;
}
- )
+
+ devkitSpatialForce = {
+ angleDeg,
+ magnitude: 0,
+ confidence: 0
+ };
+ scheduleDevkitSpatialForceClear();
+ hasSignalData =
+ signalPanels.length > 0 ||
+ summary.points.length > 0 ||
+ spatialForce !== null ||
+ devkitSpatialForce !== null;
+ })
.then((unlisten) => {
if (disposed) {
unlisten();
@@ -1818,6 +1872,7 @@
return () => {
disposed = true;
pauseReplayPlayback();
+ clearDevkitSpatialForce();
stopMockFeed?.();
unlistenHudStream?.();
unlistenDevkitPztAngle?.();
@@ -1937,6 +1992,7 @@
rightPanels={rightSignalPanels}
{pressureMatrix}
{spatialForce}
+ {devkitSpatialForce}
showConfigPanel={isConfigPanelOpen}
showPrecisionTestPanel={isPrecisionTestOpen}
{summary}