fix: 修复打砖块游戏碰撞穿透bug,添加渐进提速机制
This commit is contained in:
@@ -33,7 +33,6 @@
|
||||
export let rangeLabel = "";
|
||||
export let rangeMinLabel = "";
|
||||
export let rangeMaxLabel = "";
|
||||
export let colorMapLabel = "";
|
||||
export let resetConfigLabel = "";
|
||||
export let applyLiveHint = "";
|
||||
export let matrixRows = 12;
|
||||
@@ -42,7 +41,6 @@
|
||||
export let rangeMax = DEFAULT_PRESSURE_RANGE_MAX;
|
||||
export let colorMapPreset: PressureColorMapPreset = "emerald";
|
||||
export let matrixDisplayMode: MatrixDisplayMode = "dots";
|
||||
export let colorMapOptions: HudColorMapOption[] = [];
|
||||
export let replaySectionLabel = "";
|
||||
export let replayPlayLabel = "";
|
||||
export let replayPauseLabel = "";
|
||||
@@ -56,6 +54,7 @@
|
||||
export let replayFileName = "";
|
||||
export let replayFrameInfo = "";
|
||||
export let showPrecisionTestPanel = false;
|
||||
export let sessionStartedAt: number = Date.now();
|
||||
|
||||
let stagePlaneEl: HTMLDivElement | undefined;
|
||||
let panelZoneEl: HTMLDivElement | undefined;
|
||||
@@ -195,6 +194,7 @@
|
||||
{rangeMax}
|
||||
{colorMapPreset}
|
||||
{matrixDisplayMode}
|
||||
{locale}
|
||||
showStatsPanel={true}
|
||||
/>
|
||||
{/key}
|
||||
@@ -225,6 +225,7 @@
|
||||
{rangeMax}
|
||||
{colorMapPreset}
|
||||
{matrixDisplayMode}
|
||||
{locale}
|
||||
showStatsPanel={true}
|
||||
/>
|
||||
{/key}
|
||||
@@ -246,9 +247,6 @@
|
||||
{rangeLabel}
|
||||
{rangeMinLabel}
|
||||
{rangeMaxLabel}
|
||||
{colorMapLabel}
|
||||
bind:colorMapPreset
|
||||
{colorMapOptions}
|
||||
resetLabel={resetConfigLabel}
|
||||
{applyLiveHint}
|
||||
on:close={() => dispatch("configclose")}
|
||||
@@ -267,7 +265,7 @@
|
||||
in:fly={{ x: -180, duration: 340, opacity: 0.08, easing: cubicOut }}
|
||||
out:fly={{ x: -180, duration: 280, opacity: 0.06, easing: cubicIn }}
|
||||
>
|
||||
<SignalChart {panel} panelIndex={index} />
|
||||
<SignalChart {panel} panelIndex={index} {locale} />
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@@ -281,6 +279,9 @@
|
||||
{summary}
|
||||
xValues={summary.xValues ?? null}
|
||||
yValues={summary.points}
|
||||
{locale}
|
||||
{sessionStartedAt}
|
||||
isRealtime={!replayHasData}
|
||||
side="left"
|
||||
panelIndex={leftPanels.length}
|
||||
/>
|
||||
@@ -298,7 +299,7 @@
|
||||
in:fly={{ x: 180, duration: 340, opacity: 0.08, easing: cubicOut }}
|
||||
out:fly={{ x: 180, duration: 280, opacity: 0.06, easing: cubicIn }}
|
||||
>
|
||||
<SignalChart {panel} panelIndex={index} />
|
||||
<SignalChart {panel} panelIndex={index} {locale} />
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@@ -312,6 +313,9 @@
|
||||
{summary}
|
||||
xValues={summary.xValues ?? null}
|
||||
yValues={summary.points}
|
||||
{locale}
|
||||
{sessionStartedAt}
|
||||
isRealtime={!replayHasData}
|
||||
side="right"
|
||||
panelIndex={rightPanels.length}
|
||||
/>
|
||||
@@ -396,7 +400,7 @@
|
||||
}
|
||||
|
||||
.stage-canvas-plane {
|
||||
--rail-width: clamp(17.5rem, 23vw, 21.5rem);
|
||||
--rail-width: clamp(20rem, 27vw, 26rem);
|
||||
--rail-edge-inset: clamp(0.35rem, 1vw, 0.9rem);
|
||||
--safe-gap: clamp(0.35rem, 0.9vw, 0.85rem);
|
||||
--panel-zone-top: clamp(6.4rem, 11.8vh, 8rem);
|
||||
@@ -754,7 +758,7 @@
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.stage-canvas-plane {
|
||||
--rail-width: clamp(14.2rem, 28vw, 16.4rem);
|
||||
--rail-width: clamp(17rem, 32vw, 22rem);
|
||||
--rail-edge-inset: clamp(0.25rem, 0.7vw, 0.55rem);
|
||||
--safe-gap: clamp(0.2rem, 0.75vw, 0.45rem);
|
||||
--panel-zone-top: clamp(6rem, 11.2vh, 7.2rem);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { DEFAULT_PRESSURE_RANGE_MAX, DEFAULT_PRESSURE_RANGE_MIN } from "$lib/config/pressure-range";
|
||||
import type { HudColorMapOption, PressureColorMapPreset } from "$lib/types/hud";
|
||||
|
||||
export let title = "";
|
||||
export let hint = "";
|
||||
@@ -11,15 +10,12 @@
|
||||
export let rangeLabel = "";
|
||||
export let rangeMinLabel = "";
|
||||
export let rangeMaxLabel = "";
|
||||
export let colorMapLabel = "";
|
||||
export let resetLabel = "";
|
||||
export let applyLiveHint = "";
|
||||
export let matrixRows = 12;
|
||||
export let matrixCols = 7;
|
||||
export let rangeMin = DEFAULT_PRESSURE_RANGE_MIN;
|
||||
export let rangeMax = DEFAULT_PRESSURE_RANGE_MAX;
|
||||
export let colorMapPreset: PressureColorMapPreset = "emerald";
|
||||
export let colorMapOptions: HudColorMapOption[] = [];
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
close: void;
|
||||
@@ -78,24 +74,17 @@
|
||||
matrixCols = size;
|
||||
}
|
||||
|
||||
function applyColorMapPreset(id: PressureColorMapPreset): void {
|
||||
colorMapPreset = id;
|
||||
}
|
||||
|
||||
function resetDefaults(): void {
|
||||
matrixRows = 12;
|
||||
matrixCols = 7;
|
||||
rangeMin = DEFAULT_PRESSURE_RANGE_MIN;
|
||||
rangeMax = DEFAULT_PRESSURE_RANGE_MAX;
|
||||
colorMapPreset = "emerald";
|
||||
}
|
||||
|
||||
function handleSubmit(): void {
|
||||
dispatch("close");
|
||||
}
|
||||
|
||||
$: selectedColorMap = colorMapOptions.find((option) => option.id === colorMapPreset) ?? colorMapOptions[0];
|
||||
|
||||
$: {
|
||||
const nextRows = normalizeGridValue(matrixRows);
|
||||
if (nextRows !== matrixRows) {
|
||||
@@ -190,31 +179,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-section">
|
||||
<div class="section-head">
|
||||
<p class="section-title">{colorMapLabel}</p>
|
||||
<p class="section-note">{selectedColorMap?.label ?? colorMapPreset}</p>
|
||||
</div>
|
||||
|
||||
<div class="palette-row" role="group" aria-label={colorMapLabel}>
|
||||
{#each colorMapOptions as option}
|
||||
<button
|
||||
type="button"
|
||||
class="palette-btn"
|
||||
class:is-active={colorMapPreset === option.id}
|
||||
on:click={() => applyColorMapPreset(option.id)}
|
||||
>
|
||||
<span
|
||||
class="palette-preview"
|
||||
style={`--palette-stop-0: ${option.previewStops[0]}; --palette-stop-1: ${option.previewStops[1]}; --palette-stop-2: ${option.previewStops[2]};`}
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="palette-name">{option.label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="config-foot">
|
||||
<p class="live-note">{applyLiveHint}</p>
|
||||
<button type="button" class="reset-btn" on:click={resetDefaults}>{resetLabel}</button>
|
||||
@@ -327,15 +291,8 @@
|
||||
gap: 0.42rem;
|
||||
}
|
||||
|
||||
.palette-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.48rem;
|
||||
}
|
||||
|
||||
.preset-btn,
|
||||
.reset-btn,
|
||||
.palette-btn {
|
||||
.reset-btn {
|
||||
border: 1px solid rgb(var(--hud-border-rgb) / 0.28);
|
||||
border-radius: 999px;
|
||||
padding: 0.38rem 0.72rem;
|
||||
@@ -358,48 +315,6 @@
|
||||
box-shadow: inset 0 1px 0 rgb(var(--hud-border-strong-rgb) / 0.14), 0 0 16px rgb(var(--hud-glow-alt-rgb) / 0.14);
|
||||
}
|
||||
|
||||
.palette-btn {
|
||||
display: grid;
|
||||
gap: 0.34rem;
|
||||
min-height: 4rem;
|
||||
padding: 0.52rem 0.56rem 0.58rem;
|
||||
border-radius: 0.74rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.palette-btn.is-active {
|
||||
border-color: rgb(var(--hud-lime-rgb) / 0.48);
|
||||
background: linear-gradient(180deg, rgb(var(--hud-surface-alt-rgb) / 0.96), rgb(var(--hud-surface-rgb) / 0.92));
|
||||
color: rgb(var(--hud-text-main-rgb) / 0.98);
|
||||
box-shadow: inset 0 1px 0 rgb(var(--hud-border-strong-rgb) / 0.14), 0 0 16px rgb(var(--hud-glow-alt-rgb) / 0.14);
|
||||
}
|
||||
|
||||
.palette-preview {
|
||||
display: block;
|
||||
inline-size: 100%;
|
||||
block-size: 0.74rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgb(255 255 255 / 0.08);
|
||||
background:
|
||||
linear-gradient(
|
||||
90deg,
|
||||
var(--palette-stop-0) 0%,
|
||||
var(--palette-stop-1) 52%,
|
||||
var(--palette-stop-2) 100%
|
||||
),
|
||||
linear-gradient(180deg, rgb(255 255 255 / 0.08), transparent 55%);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgb(255 255 255 / 0.1),
|
||||
0 0 12px rgb(0 0 0 / 0.14);
|
||||
}
|
||||
|
||||
.palette-name {
|
||||
color: inherit;
|
||||
font-size: 0.74rem;
|
||||
line-height: 1.1;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.field-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
@@ -457,9 +372,5 @@
|
||||
.field-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.palette-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -2,8 +2,6 @@
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
export let running = false;
|
||||
export let port = 50051;
|
||||
export let framesSent = 0;
|
||||
export let filterLiftEnabled = true;
|
||||
export let saveAsXlsx = false;
|
||||
export let locale: "zh-CN" | "en-US" = "zh-CN";
|
||||
@@ -22,51 +20,52 @@
|
||||
togglexlsx: void;
|
||||
}>();
|
||||
|
||||
$: labels = locale === "zh-CN"
|
||||
? {
|
||||
title: "DevKit 配置",
|
||||
status: "状态",
|
||||
connected: "已连接",
|
||||
disconnected: "未连接",
|
||||
port: "端口",
|
||||
framesSent: "已发送帧",
|
||||
filterLift: "导出过滤抬起",
|
||||
filterLiftHint: "导出 CSV 后自动调用 Python 做梯度过滤,过滤掉抬起的小值数据",
|
||||
saveXlsx: "以 xlsx 保存",
|
||||
saveXlsxHint: "Python 处理后输出 xlsx 格式并删除源 CSV 文件",
|
||||
lastResult: "最近一次处理",
|
||||
output: "输出文件",
|
||||
groups: "分组数",
|
||||
mean: "均值",
|
||||
threshold: "阈值",
|
||||
rows: "行数",
|
||||
kept: "保留行数",
|
||||
}
|
||||
: {
|
||||
title: "DevKit Config",
|
||||
status: "Status",
|
||||
connected: "Connected",
|
||||
disconnected: "Disconnected",
|
||||
port: "Port",
|
||||
framesSent: "Frames sent",
|
||||
filterLift: "Filter lift on export",
|
||||
filterLiftHint: "After CSV export, automatically call Python to filter out small values",
|
||||
saveXlsx: "Save as xlsx",
|
||||
saveXlsxHint: "Python outputs xlsx format and deletes the source CSV file",
|
||||
lastResult: "Last process",
|
||||
output: "Output",
|
||||
groups: "Groups",
|
||||
mean: "Mean",
|
||||
threshold: "Threshold",
|
||||
rows: "Rows",
|
||||
kept: "Kept rows",
|
||||
};
|
||||
$: labels =
|
||||
locale === "zh-CN"
|
||||
? {
|
||||
title: "开发工具配置",
|
||||
close: "关闭",
|
||||
status: "状态",
|
||||
connected: "已连接",
|
||||
disconnected: "未连接",
|
||||
filterLift: "导出后过滤抬起",
|
||||
filterLiftHint: "导出 CSV 时自动过滤掉抬起阶段的小值数据。",
|
||||
saveXlsx: "保存为 xlsx",
|
||||
saveXlsxHint: "将导出文件转换为 xlsx 格式。",
|
||||
lastResult: "最近一次处理",
|
||||
output: "输出文件",
|
||||
groups: "分组数",
|
||||
mean: "均值",
|
||||
threshold: "阈值",
|
||||
rows: "总行数",
|
||||
kept: "保留行数",
|
||||
rowsFlow: "行数变化"
|
||||
}
|
||||
: {
|
||||
title: "DevKit Config",
|
||||
close: "Close",
|
||||
status: "Status",
|
||||
connected: "Connected",
|
||||
disconnected: "Disconnected",
|
||||
filterLift: "Filter lift on export",
|
||||
filterLiftHint: "Automatically filter out small values from lift-off phases during CSV export.",
|
||||
saveXlsx: "Save as xlsx",
|
||||
saveXlsxHint: "Convert exported file to xlsx format.",
|
||||
lastResult: "Last process",
|
||||
output: "Output",
|
||||
groups: "Groups",
|
||||
mean: "Mean",
|
||||
threshold: "Threshold",
|
||||
rows: "Rows",
|
||||
kept: "Kept rows",
|
||||
rowsFlow: "Rows flow"
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="dk-panel">
|
||||
<header class="dk-head">
|
||||
<h3 class="dk-title">{labels.title}</h3>
|
||||
<button type="button" class="dk-close" on:click={() => dispatch("close")} aria-label="Close">
|
||||
<button type="button" class="dk-close" on:click={() => dispatch("close")} aria-label={labels.close}>
|
||||
<span></span><span></span>
|
||||
</button>
|
||||
</header>
|
||||
@@ -77,18 +76,6 @@
|
||||
<span class="dk-label">{labels.status}</span>
|
||||
<span class="dk-value">{running ? labels.connected : labels.disconnected}</span>
|
||||
</div>
|
||||
{#if running}
|
||||
<div class="dk-info-grid">
|
||||
<div class="dk-info">
|
||||
<span class="dk-info-label">{labels.port}</span>
|
||||
<span class="dk-info-value">:{port}</span>
|
||||
</div>
|
||||
<div class="dk-info">
|
||||
<span class="dk-info-label">{labels.framesSent}</span>
|
||||
<span class="dk-info-value">{framesSent}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section class="dk-section">
|
||||
@@ -132,8 +119,8 @@
|
||||
<span class="dk-result-value">{lastProcessResult.threshold.toFixed(3)}</span>
|
||||
</div>
|
||||
<div class="dk-result-item">
|
||||
<span class="dk-result-label">{labels.rows}</span>
|
||||
<span class="dk-result-value">{lastProcessResult.rowsTotal} → {lastProcessResult.rowsKept}</span>
|
||||
<span class="dk-result-label">{labels.rowsFlow}</span>
|
||||
<span class="dk-result-value">{lastProcessResult.rowsTotal} -> {lastProcessResult.rowsKept}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -236,30 +223,6 @@
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.dk-info-grid {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.dk-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.dk-info-label {
|
||||
color: rgb(var(--hud-text-dim-rgb) / 0.7);
|
||||
font-size: 0.56rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.dk-info-value {
|
||||
color: rgb(var(--hud-text-main-rgb) / 0.94);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dk-toggle {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
export let matrixDisplayMode: MatrixDisplayMode = "dots";
|
||||
export let summary: HudSummary | null = null;
|
||||
export let showStatsPanel = true;
|
||||
export let locale: "zh-CN" | "en-US" = "zh-CN";
|
||||
|
||||
let viewerEl: HTMLDivElement | undefined;
|
||||
let canvasEl: HTMLCanvasElement | undefined;
|
||||
@@ -131,8 +132,13 @@
|
||||
$: resolvedRangeMin = resolvedRange.min;
|
||||
$: resolvedRangeMax = resolvedRange.max;
|
||||
$: matrixLayout = buildMatrixLayout(resolvedMatrixRows, resolvedMatrixCols);
|
||||
$: statsModeLabel = matrixDisplayMode === "dots" ? "dot pulse" : "numeric pulse";
|
||||
$: statsNote = `${resolvedMatrixRows}x${resolvedMatrixCols} / force range ${resolvedRangeMin}-${resolvedRangeMax} / ${statsModeLabel}`;
|
||||
$: statsModeLabel = matrixDisplayMode === "dots"
|
||||
? (locale === "zh-CN" ? "点阵脉冲" : "dot pulse")
|
||||
: (locale === "zh-CN" ? "数字脉冲" : "numeric pulse");
|
||||
$: statsNote = `${resolvedMatrixRows}x${resolvedMatrixCols} / ${locale === "zh-CN" ? "力量范围" : "force range"} ${resolvedRangeMin}-${resolvedRangeMax} / ${statsModeLabel}`;
|
||||
$: viewerI18n = locale === "zh-CN"
|
||||
? { title: "合力", current: "当前合力", max: "最大合力", min: "最小合力" }
|
||||
: { title: "Resultant Force", current: "Current RF", max: "Max RF", min: "Min RF" };
|
||||
|
||||
function formatForceStat(value: number | null): string {
|
||||
if (value == null || !Number.isFinite(value)) {
|
||||
@@ -660,18 +666,18 @@
|
||||
{#if showStatsPanel}
|
||||
<div class="viewer-controls">
|
||||
<section class="stats-panel" aria-label="Pressure Summary">
|
||||
<p class="stats-label">Resultant Force</p>
|
||||
<p class="stats-label">{viewerI18n.title}</p>
|
||||
<div class="stats-grid">
|
||||
<article class="stats-card stats-card-wide">
|
||||
<span class="stats-key">Current RF</span>
|
||||
<span class="stats-key">{viewerI18n.current}</span>
|
||||
<strong class="stats-value">{formatForceStat(stats.current)}</strong>
|
||||
</article>
|
||||
<article class="stats-card">
|
||||
<span class="stats-key">Max RF</span>
|
||||
<span class="stats-key">{viewerI18n.max}</span>
|
||||
<strong class="stats-value">{formatForceStat(stats.max)}</strong>
|
||||
</article>
|
||||
<article class="stats-card">
|
||||
<span class="stats-key">Min RF</span>
|
||||
<span class="stats-key">{viewerI18n.min}</span>
|
||||
<strong class="stats-value">{formatForceStat(stats.min)}</strong>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
|
||||
export let panel: HudSignalPanel;
|
||||
export let panelIndex = 0;
|
||||
export let locale: "zh-CN" | "en-US" = "zh-CN";
|
||||
|
||||
$: signalI18n = locale === "zh-CN"
|
||||
? { now: "当前", max: "最大", min: "最小", total: "合计" }
|
||||
: { now: "Now", max: "Max", min: "Min", total: "TOTAL" };
|
||||
|
||||
const viewportWidth = 100;
|
||||
const viewportHeight = 36;
|
||||
@@ -110,7 +115,7 @@
|
||||
|
||||
<div class="icon-layer" aria-hidden="true">
|
||||
{#each panel.icons as icon (icon.id)}
|
||||
<span class="icon-chip tone-{icon.tone}">{icon.label}</span>
|
||||
<span class="icon-chip tone-{icon.tone}">{icon.label === "TOTAL" ? signalI18n.total : icon.label}</span>
|
||||
{/each}
|
||||
</div>
|
||||
</header>
|
||||
@@ -136,17 +141,17 @@
|
||||
<footer class="panel-foot">
|
||||
<p class="foot-item">
|
||||
<span class="dot tone-cyan"></span>
|
||||
<span class="metric-label">Now</span>
|
||||
<span class="metric-label">{signalI18n.now}</span>
|
||||
<span class="value">{latestValue}</span>
|
||||
</p>
|
||||
<p class="foot-item">
|
||||
<span class="dot tone-lime"></span>
|
||||
<span class="metric-label">Max</span>
|
||||
<span class="metric-label">{signalI18n.max}</span>
|
||||
<span class="value">{maxValue}</span>
|
||||
</p>
|
||||
<p class="foot-item">
|
||||
<span class="dot tone-orange"></span>
|
||||
<span class="metric-label">Min</span>
|
||||
<span class="metric-label">{signalI18n.min}</span>
|
||||
<span class="value">{minValue}</span>
|
||||
</p>
|
||||
</footer>
|
||||
@@ -158,7 +163,7 @@
|
||||
--enter-ms: 1800ms;
|
||||
--fade-ms: 1000ms;
|
||||
overflow: hidden;
|
||||
inline-size: min(100%, clamp(16.8rem, 23vw, 22rem));
|
||||
inline-size: min(100%, clamp(19rem, 27vw, 26rem));
|
||||
aspect-ratio: 1.44 / 1;
|
||||
min-block-size: 11.8rem;
|
||||
justify-self: start;
|
||||
@@ -388,7 +393,7 @@
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.signal-panel {
|
||||
inline-size: min(100%, clamp(14rem, 30vw, 17rem));
|
||||
inline-size: min(100%, clamp(16rem, 32vw, 21rem));
|
||||
aspect-ratio: 1.5 / 1;
|
||||
min-block-size: 10.1rem;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import type { HudSummary } from "$lib/types/hud";
|
||||
|
||||
export let summary: HudSummary;
|
||||
@@ -6,6 +7,25 @@
|
||||
export let panelIndex = 0;
|
||||
export let xValues: number[] | null = null;
|
||||
export let yValues: number[] | null = null;
|
||||
export let locale: "zh-CN" | "en-US" = "zh-CN";
|
||||
export let sessionStartedAt: number = Date.now();
|
||||
export let isRealtime = false;
|
||||
|
||||
let currentTimeSeconds = 0;
|
||||
let timerId: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
onMount(() => {
|
||||
timerId = setInterval(() => {
|
||||
currentTimeSeconds = Math.round((Date.now() - sessionStartedAt) / 100) / 10;
|
||||
}, 200);
|
||||
return () => {
|
||||
if (timerId != null) clearInterval(timerId);
|
||||
};
|
||||
});
|
||||
|
||||
$: i18n = locale === "zh-CN"
|
||||
? { now: "当前", min: "最小", max: "最大", waiting: "等待数据" }
|
||||
: { now: "Now", min: "Min", max: "Max", waiting: "Waiting" };
|
||||
|
||||
const viewportWidth = 120;
|
||||
const viewportHeight = 48;
|
||||
@@ -50,7 +70,12 @@
|
||||
}
|
||||
|
||||
if (axis === "x") {
|
||||
return String(Math.round(value));
|
||||
if (value < 60) {
|
||||
return `${value.toFixed(1)}s`;
|
||||
}
|
||||
const mins = Math.floor(value / 60);
|
||||
const secs = value - mins * 60;
|
||||
return `${mins}:${secs.toFixed(0).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
return `${Math.round(value)} N`;
|
||||
@@ -104,14 +129,51 @@
|
||||
return [];
|
||||
}
|
||||
|
||||
let previousX = 0;
|
||||
|
||||
return rawYValues.map((rawY, index) => {
|
||||
const x = rawXValues[index];
|
||||
const y = Number.isFinite(rawY) ? Number(rawY) : 0;
|
||||
const resolvedX = Number.isFinite(x) ? Number(x) : index + 1;
|
||||
return { x: resolvedX, y };
|
||||
const fallbackX = index === 0 ? 0 : previousX + 1;
|
||||
const resolvedX = Number.isFinite(x) ? Number(x) : fallbackX;
|
||||
const normalizedX = index === 0 ? resolvedX : Math.max(resolvedX, previousX);
|
||||
previousX = normalizedX;
|
||||
return { x: normalizedX, y };
|
||||
});
|
||||
}
|
||||
|
||||
function resolveXScaleBounds(
|
||||
samples: CurveSample[],
|
||||
currentSeconds: number,
|
||||
realtime: boolean
|
||||
): { min: number; max: number } {
|
||||
if (samples.length === 0) {
|
||||
return { min: 0, max: 1 };
|
||||
}
|
||||
|
||||
const values = samples.map((sample) => sample.x);
|
||||
const dataBounds = resolveBounds(values);
|
||||
|
||||
if (!realtime) {
|
||||
return dataBounds;
|
||||
}
|
||||
|
||||
const firstX = samples[0].x;
|
||||
const lastX = samples[samples.length - 1].x;
|
||||
const axisMax = Math.max(lastX, currentSeconds);
|
||||
const positiveDiffs = samples
|
||||
.slice(1)
|
||||
.map((sample, index) => sample.x - samples[index].x)
|
||||
.filter((diff) => diff > 0);
|
||||
const averageSpacing =
|
||||
positiveDiffs.length > 0 ? positiveDiffs.reduce((sum, diff) => sum + diff, 0) / positiveDiffs.length : 1;
|
||||
const dataSpan = Math.max(lastX - firstX, 0);
|
||||
const windowSpan = Math.max(dataSpan, averageSpacing * Math.max(samples.length - 1, 1), 1);
|
||||
const axisMin = Math.max(0, axisMax - windowSpan);
|
||||
|
||||
return resolveBounds([axisMin, axisMax]);
|
||||
}
|
||||
|
||||
function convertPoints(
|
||||
samples: CurveSample[],
|
||||
xBounds: { min: number; max: number },
|
||||
@@ -146,14 +208,14 @@
|
||||
}));
|
||||
}
|
||||
|
||||
function buildXAxisTicks(samples: CurveSample[], xScaleBounds: { min: number; max: number }): AxisTick[] {
|
||||
if (!samples.length) {
|
||||
function buildXAxisTicks(xScaleBounds: { min: number; max: number }): AxisTick[] {
|
||||
if (!Number.isFinite(xScaleBounds.min) || !Number.isFinite(xScaleBounds.max)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const first = samples[0].x;
|
||||
const middle = samples[Math.floor((samples.length - 1) / 2)].x;
|
||||
const last = samples[samples.length - 1].x;
|
||||
const first = xScaleBounds.min;
|
||||
const middle = xScaleBounds.min + (xScaleBounds.max - xScaleBounds.min) / 2;
|
||||
const last = xScaleBounds.max;
|
||||
const tickValues = [first, middle, last];
|
||||
return tickValues.map((value) => ({
|
||||
value,
|
||||
@@ -185,9 +247,16 @@
|
||||
|
||||
$: sourceYValues = yValues && yValues.length ? yValues : summary.points;
|
||||
$: sourceXValues = xValues && xValues.length ? xValues : summary.xValues ?? [];
|
||||
$: samples = buildSamples(sourceYValues, sourceXValues);
|
||||
$: samples = (() => {
|
||||
const base = buildSamples(sourceYValues, sourceXValues);
|
||||
if (isRealtime && base.length > 0 && currentTimeSeconds > 0) {
|
||||
const lastSample = base[base.length - 1];
|
||||
base[base.length - 1] = { ...lastSample, x: Math.max(lastSample.x, currentTimeSeconds) };
|
||||
}
|
||||
return base;
|
||||
})();
|
||||
$: sampleCount = samples.length;
|
||||
$: xScaleBounds = resolveBounds(samples.map((sample) => sample.x));
|
||||
$: xScaleBounds = resolveXScaleBounds(samples, currentTimeSeconds, isRealtime);
|
||||
$: yScaleBounds = fixedYBounds;
|
||||
$: xDataBounds = resolveDataBounds(samples.map((sample) => sample.x));
|
||||
$: yDataBounds = resolveDataBounds(samples.map((sample) => sample.y));
|
||||
@@ -196,7 +265,7 @@
|
||||
$: areaPath = createAreaPath(plotPoints);
|
||||
$: lastPoint = plotPoints.length > 0 ? plotPoints[plotPoints.length - 1] : null;
|
||||
$: yAxisTicks = sampleCount > 0 ? buildYAxisTicks(yScaleBounds, yDataBounds) : [];
|
||||
$: xAxisTicks = sampleCount > 0 ? buildXAxisTicks(samples, xScaleBounds) : [];
|
||||
$: xAxisTicks = sampleCount > 0 ? buildXAxisTicks(xScaleBounds) : [];
|
||||
$: latestValue = formatValue(summary.latest);
|
||||
$: minValue = formatValue(summary.min);
|
||||
$: maxValue = formatValue(summary.max);
|
||||
@@ -270,7 +339,7 @@
|
||||
|
||||
{#if sampleCount === 0}
|
||||
<div class="empty-state">
|
||||
<span>Waiting</span>
|
||||
<span>{i18n.waiting}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -278,17 +347,17 @@
|
||||
<footer class="panel-foot">
|
||||
<p class="foot-item">
|
||||
<span class="dot tone-cyan"></span>
|
||||
<span class="metric-text">Now</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">Min</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">Max</span>
|
||||
<span class="metric-text">{i18n.max}</span>
|
||||
<span class="value">{maxValue}</span>
|
||||
</p>
|
||||
</footer>
|
||||
@@ -300,12 +369,10 @@
|
||||
--enter-ms: 1800ms;
|
||||
--fade-ms: 1000ms;
|
||||
overflow: hidden;
|
||||
inline-size: min(100%, clamp(29rem, 38vw, 37rem));
|
||||
aspect-ratio: 1.42 / 1;
|
||||
min-block-size: 20.5rem;
|
||||
inline-size: min(100%, clamp(34rem, 44vw, 44rem));
|
||||
justify-self: start;
|
||||
display: grid;
|
||||
grid-template-rows: auto auto auto;
|
||||
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);
|
||||
@@ -404,6 +471,7 @@
|
||||
.chart-stage {
|
||||
position: relative;
|
||||
block-size: clamp(12rem, 15.5vw, 15rem);
|
||||
min-block-size: 5rem;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgb(var(--hud-border-strong-rgb) / 0.32);
|
||||
border-radius: 0.62rem;
|
||||
@@ -474,25 +542,29 @@
|
||||
|
||||
.panel-foot {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
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.28rem;
|
||||
gap: 0.22rem;
|
||||
color: rgb(var(--hud-text-main-rgb) / 0.9);
|
||||
font-size: 0.76rem;
|
||||
letter-spacing: 0.04em;
|
||||
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 {
|
||||
@@ -519,16 +591,18 @@
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.signal-panel {
|
||||
inline-size: min(100%, clamp(24rem, 36vw, 31rem));
|
||||
aspect-ratio: 1.48 / 1;
|
||||
min-block-size: 17rem;
|
||||
inline-size: min(100%, clamp(28rem, 40vw, 38rem));
|
||||
}
|
||||
|
||||
.chart-stage {
|
||||
block-size: clamp(10rem, 13vw, 12rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 900px) {
|
||||
.signal-panel {
|
||||
inline-size: min(100%, clamp(24rem, 33vw, 30rem));
|
||||
min-block-size: 16.8rem;
|
||||
inline-size: min(100%, clamp(28rem, 38vw, 36rem));
|
||||
padding: 0.7rem 0.76rem 0.8rem;
|
||||
}
|
||||
|
||||
.chart-stage {
|
||||
@@ -538,46 +612,31 @@
|
||||
|
||||
@media (max-height: 760px) {
|
||||
.signal-panel {
|
||||
inline-size: min(100%, clamp(21rem, 29vw, 26rem));
|
||||
min-block-size: 14.4rem;
|
||||
padding: 0.7rem 0.76rem 0.8rem;
|
||||
}
|
||||
|
||||
.panel-foot {
|
||||
margin-block-start: 0.28rem;
|
||||
inline-size: min(100%, clamp(24rem, 34vw, 30rem));
|
||||
padding: 0.62rem 0.68rem 0.72rem;
|
||||
gap: 0.48rem;
|
||||
}
|
||||
|
||||
.chart-stage {
|
||||
block-size: clamp(8.3rem, 9.6vw, 9.8rem);
|
||||
block-size: clamp(8rem, 9.5vw, 9.8rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 680px) {
|
||||
.signal-panel {
|
||||
inline-size: min(100%, clamp(18.5rem, 24vw, 22.5rem));
|
||||
min-block-size: 12.4rem;
|
||||
padding: 0.62rem 0.66rem 0.68rem;
|
||||
}
|
||||
|
||||
.panel-head {
|
||||
margin-block-end: 0.26rem;
|
||||
}
|
||||
|
||||
.panel-foot {
|
||||
margin-block-start: 0.18rem;
|
||||
gap: 0.56rem;
|
||||
inline-size: min(100%, clamp(20rem, 28vw, 26rem));
|
||||
padding: 0.52rem 0.58rem 0.6rem;
|
||||
gap: 0.36rem;
|
||||
}
|
||||
|
||||
.chart-stage {
|
||||
block-size: clamp(7rem, 7.8vw, 8rem);
|
||||
block-size: clamp(6.5rem, 8vw, 7.5rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.signal-panel {
|
||||
inline-size: 100%;
|
||||
aspect-ratio: 1.7 / 1;
|
||||
min-block-size: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user