update serial and spatial force components

This commit is contained in:
lenn
2026-06-02 16:24:15 +08:00
parent 78c4445b93
commit 79faa67dd8
5 changed files with 458 additions and 172 deletions

View File

@@ -29,7 +29,6 @@
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 = "";
@@ -278,41 +277,25 @@
</div>
{/each}
<div
class="panel-motion-shell"
in:fly={{ x: 180, duration: 340, opacity: 0.08, easing: cubicOut }}
out:fly={{ x: 180, duration: 280, opacity: 0.06, easing: cubicIn }}
>
<SpatialForcePanel
{spatialForce}
{locale}
side="right"
panelIndex={rightPanels.length}
panelCode="ALG"
panelTitle={locale === "zh-CN" ? "本地切向力" : "Local Tangential"}
badgeLabel={locale === "zh-CN" ? "算法" : "ALGO"}
/>
</div>
<div
class="panel-motion-shell"
in:fly={{ x: 180, duration: 340, opacity: 0.08, easing: cubicOut }}
out:fly={{ x: 180, duration: 280, opacity: 0.06, easing: cubicIn }}
>
<SpatialForcePanel
spatialForce={devkitSpatialForce}
{locale}
side="right"
panelIndex={rightPanels.length + 1}
panelCode="DKT"
panelTitle={locale === "zh-CN" ? "DevKit 切向力" : "DevKit Tangential"}
badgeLabel="DEVKIT"
badgeTone="lime"
showMetrics={false}
requireMagnitude={false}
compactMetaText={locale === "zh-CN" ? "等待 DevKit 角度流" : "Waiting for DevKit angle"}
/>
</div>
{#if spatialForce}
<div
class="panel-motion-shell"
in:fly={{ x: 180, duration: 340, opacity: 0.08, easing: cubicOut }}
out:fly={{ x: 180, duration: 280, opacity: 0.06, easing: cubicIn }}
>
<SpatialForcePanel
{spatialForce}
{locale}
side="right"
panelIndex={rightPanels.length}
panelCode="3D"
panelTitle={locale === "zh-CN" ? "三维力" : "3D Force"}
badgeLabel=""
badgeTone="lime"
/>
</div>
{/if}
{#if summaryCurveVisible && summarySide === "right"}
<div

View File

@@ -9,17 +9,7 @@
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)) {
return "--";
}
return value.toFixed(digits);
}
function normalizeAngle(value: number): number {
return ((value % 360) + 360) % 360;
@@ -83,7 +73,7 @@
$: i18n =
locale === "zh-CN"
? {
title: "切向力方向",
title: "三维力",
waiting: "等待数据",
angle: "ANGLE",
heading: "方向角",
@@ -91,7 +81,7 @@
confidence: "置信度"
}
: {
title: "Tangential Direction",
title: "3D Force",
waiting: "Waiting",
angle: "ANGLE",
heading: "Heading",
@@ -100,8 +90,6 @@
};
$: resolvedTitle = panelTitle || i18n.title;
$: resolvedBadgeLabel = badgeLabel || i18n.angle;
$: resolvedCompactMetaText =
compactMetaText || (locale === "zh-CN" ? "仅使用角度流" : "Angle stream only");
$: hasData =
spatialForce !== null &&
@@ -109,35 +97,34 @@
(!requireMagnitude || Number.isFinite(spatialForce.magnitude));
$: angleDeg = hasData ? normalizeAngle(spatialForce?.angleDeg ?? 0) : 0;
$: updateVisualAngle(angleDeg, hasData);
$: magnitude = hasData ? spatialForce?.magnitude ?? 0 : null;
$: confidence = hasData ? (spatialForce?.confidence ?? 0) * 100 : null;
</script>
<article
class="signal-panel spatial-panel side-{side}"
class:is-empty={!hasData}
aria-hidden={false}
style="--panel-index: {panelIndex};"
>
<header class="panel-head">
<div class="head-text">
<p class="panel-code">{panelCode}</p>
<p class="panel-title">{resolvedTitle}</p>
</div>
{#if hasData}
<article
class="signal-panel spatial-panel side-{side}"
aria-label={resolvedTitle}
style="--panel-index: {panelIndex};"
>
<header class="panel-head">
<div class="head-text">
<p class="panel-code">{panelCode}</p>
<p class="panel-title">{resolvedTitle}</p>
</div>
<div class="icon-layer" aria-hidden="true">
<span class={`icon-chip tone-${badgeTone}`}>{resolvedBadgeLabel}</span>
</div>
</header>
{#if resolvedBadgeLabel}
<div class="icon-layer" aria-hidden="true">
<span class={`icon-chip tone-${badgeTone}`}>{resolvedBadgeLabel}</span>
</div>
{/if}
</header>
<div class="panel-body">
<div class="compass-stage">
<div class="compass-core">
<div class="compass-ring compass-ring-outer"></div>
<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="panel-body">
<div class="compass-stage">
<div class="compass-core">
<div class="compass-ring compass-ring-outer"></div>
<div class="compass-ring compass-ring-inner"></div>
<div class="compass-axis axis-horizontal"></div>
<div class="compass-axis axis-vertical"></div>
<div
class="compass-vector"
class:is-snap={snapVector}
@@ -146,44 +133,29 @@
<span class="vector-shaft"></span>
<span class="vector-head"></span>
</div>
{/if}
<div class="compass-center"></div>
<span class="compass-label label-top">90</span>
<span class="compass-label label-right">0</span>
<span class="compass-label label-bottom">270</span>
<span class="compass-label label-left">180</span>
</div>
{#if !hasData}
<div class="empty-state">
<span>{i18n.waiting}</span>
<div class="compass-center"></div>
<span class="compass-label label-top">90</span>
<span class="compass-label label-right">0</span>
<span class="compass-label label-bottom">270</span>
<span class="compass-label label-left">180</span>
</div>
{/if}
</div>
</div>
<div class="angle-stage">
<p class="angle-label">{i18n.heading}</p>
{#if showMetrics}
<p class="angle-meta">{i18n.strength}: {formatValue(magnitude, 2)}</p>
<p class="angle-meta">{i18n.confidence}: {hasData ? `${formatValue(confidence, 0)}%` : "--"}</p>
{:else}
<p class="angle-meta">{resolvedCompactMetaText}</p>
<p class="angle-meta">{hasData ? (locale === "zh-CN" ? "实时对比中" : "Live comparison") : "--"}</p>
{/if}
</div>
</div>
</article>
</article>
{/if}
<style>
.signal-panel {
--offset-x: 12%;
--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;
grid-template-rows: auto 1fr auto;
gap: 0.68rem;
padding: 0.88rem 0.96rem 1rem;
border: 1px solid rgb(var(--hud-border-strong-rgb) / 0.42);
@@ -218,6 +190,16 @@
opacity: 0.82;
}
.spatial-panel::after {
content: "";
display: block;
block-size: 1rem;
}
.spatial-panel {
margin-block-end: clamp(0.8rem, 1.8vh, 1.4rem);
}
.panel-head {
display: flex;
justify-content: space-between;
@@ -277,10 +259,10 @@
.panel-body {
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(10rem, 0.9fr);
grid-template-columns: minmax(0, 1fr);
gap: 0.72rem;
block-size: clamp(12rem, 15.5vw, 15rem);
min-block-size: clamp(12rem, 15.5vw, 15rem);
min-block-size: 5rem;
}
.compass-stage {
@@ -433,76 +415,49 @@
background: linear-gradient(180deg, rgb(var(--hud-surface-deep-rgb) / 0.06), rgb(var(--hud-surface-deep-rgb) / 0.18));
}
.angle-stage {
border: 1px solid rgb(var(--hud-border-rgb) / 0.26);
border-radius: 0.62rem;
padding: 0.9rem 0.85rem;
block-size: 100%;
min-block-size: 0;
overflow: hidden;
background:
linear-gradient(180deg, rgb(var(--hud-surface-rgb) / 0.72), rgb(var(--hud-surface-deep-rgb) / 0.84)),
radial-gradient(circle at 50% 0, rgb(var(--hud-glow-rgb) / 0.05), transparent 58%);
display: grid;
grid-template-rows: auto auto auto;
align-content: center;
justify-items: start;
gap: 0.36rem;
}
.angle-label {
margin: 0;
color: rgb(var(--hud-text-dim-rgb) / 0.82);
font-size: 0.68rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.angle-meta {
margin: 0;
inline-size: 10rem;
min-block-size: 1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: rgb(var(--hud-text-dim-rgb) / 0.84);
font-size: 0.68rem;
font-variant-numeric: tabular-nums;
letter-spacing: 0.05em;
}
@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)));
}
.panel-body {
block-size: clamp(10rem, 13vw, 12rem);
}
}
@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;
}
.panel-body {
block-size: clamp(9.8rem, 12vw, 11.8rem);
}
}
@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;
}
.panel-body {
block-size: clamp(9rem, 10vw, 10.8rem);
min-block-size: clamp(9rem, 10vw, 10.8rem);
block-size: clamp(8rem, 9.5vw, 9.8rem);
}
}
@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;
}
.panel-body {
block-size: clamp(6.5rem, 8vw, 7.5rem);
}
}
@media (max-width: 900px) {

View File

@@ -1092,8 +1092,7 @@
hasSignalData =
signalPanels.length > 0 ||
packet.summary.points.length > 0 ||
spatialForce !== null ||
devkitSpatialForce !== null;
spatialForce !== null;
}
function clearHudPanels(): void {
@@ -1784,22 +1783,19 @@
}
const angleDeg = Number(event.payload.angle);
if (!Number.isFinite(angleDeg)) {
const magnitude = Number(event.payload.magnitude);
const isReportable = event.payload.state > 0 && Number.isFinite(magnitude) && magnitude > 0;
if (!Number.isFinite(angleDeg) || !isReportable) {
clearDevkitSpatialForce();
return;
}
devkitSpatialForce = {
angleDeg,
magnitude: Number.isFinite(event.payload.magnitude) ? event.payload.magnitude : 0,
confidence: event.payload.state
magnitude,
confidence: 0
};
scheduleDevkitSpatialForceClear();
hasSignalData =
signalPanels.length > 0 ||
summary.points.length > 0 ||
spatialForce !== null ||
devkitSpatialForce !== null;
})
.then((unlisten) => {
if (disposed) {
@@ -1929,7 +1925,6 @@
rightPanels={rightSignalPanels}
{pressureMatrix}
{spatialForce}
{devkitSpatialForce}
showConfigPanel={false}
{summary}
on:replaytoggle={handleReplayToggle}