Optimize realtime charts for tablet
This commit is contained in:
@@ -461,6 +461,28 @@
|
|||||||
const heightField = new Float32Array(instanceCount);
|
const heightField = new Float32Array(instanceCount);
|
||||||
const compactField = new Uint16Array(instanceCount);
|
const compactField = new Uint16Array(instanceCount);
|
||||||
let lastFrameAt = performance.now();
|
let lastFrameAt = performance.now();
|
||||||
|
let lastStatsCurrent: number | null = null;
|
||||||
|
let lastStatsMax: number | null = null;
|
||||||
|
let lastStatsMin: number | null = null;
|
||||||
|
|
||||||
|
const syncStats = () => {
|
||||||
|
const nextCurrent = summary?.latest ?? null;
|
||||||
|
const nextMax = summary?.max ?? null;
|
||||||
|
const nextMin = summary?.min ?? null;
|
||||||
|
|
||||||
|
if (nextCurrent === lastStatsCurrent && nextMax === lastStatsMax && nextMin === lastStatsMin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastStatsCurrent = nextCurrent;
|
||||||
|
lastStatsMax = nextMax;
|
||||||
|
lastStatsMin = nextMin;
|
||||||
|
stats = {
|
||||||
|
current: nextCurrent,
|
||||||
|
max: nextMax,
|
||||||
|
min: nextMin
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const drawOverlay = () => {
|
const drawOverlay = () => {
|
||||||
if (!viewerEl || !overlayEl) {
|
if (!viewerEl || !overlayEl) {
|
||||||
@@ -623,12 +645,7 @@
|
|||||||
|
|
||||||
renderer.render(scene, camera);
|
renderer.render(scene, camera);
|
||||||
drawOverlay();
|
drawOverlay();
|
||||||
|
syncStats();
|
||||||
stats = {
|
|
||||||
current: summary?.latest ?? null,
|
|
||||||
max: summary?.max ?? null,
|
|
||||||
min: summary?.min ?? null
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@@ -182,8 +182,7 @@
|
|||||||
transition:
|
transition:
|
||||||
opacity var(--fade-ms) cubic-bezier(0.18, 0.88, 0.3, 1),
|
opacity var(--fade-ms) cubic-bezier(0.18, 0.88, 0.3, 1),
|
||||||
transform var(--enter-ms) cubic-bezier(0.2, 0.9, 0.28, 1),
|
transform var(--enter-ms) cubic-bezier(0.2, 0.9, 0.28, 1),
|
||||||
border-color 460ms ease,
|
border-color 460ms ease;
|
||||||
filter 760ms ease;
|
|
||||||
transition-delay: calc(var(--panel-index) * 140ms);
|
transition-delay: calc(var(--panel-index) * 140ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,8 +299,6 @@
|
|||||||
stroke-width: 1.3;
|
stroke-width: 1.3;
|
||||||
stroke-linecap: round;
|
stroke-linecap: round;
|
||||||
stroke-linejoin: round;
|
stroke-linejoin: round;
|
||||||
filter: drop-shadow(0 0 2px rgb(0 0 0 / 0.42));
|
|
||||||
will-change: d;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.series-line.tone-cyan {
|
.series-line.tone-cyan {
|
||||||
@@ -460,10 +457,6 @@
|
|||||||
box-shadow: inset 0 0 0 1px rgb(var(--hud-border-strong-rgb) / 0.08), 0 0 8px rgb(var(--hud-glow-rgb) / 0.1);
|
box-shadow: inset 0 0 0 1px rgb(var(--hud-border-strong-rgb) / 0.08), 0 0 8px rgb(var(--hud-glow-rgb) / 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.series-line {
|
|
||||||
filter: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scan-haze {
|
.scan-haze {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,22 +11,6 @@
|
|||||||
export let sessionStartedAt: number = Date.now();
|
export let sessionStartedAt: number = Date.now();
|
||||||
export let isRealtime = false;
|
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 viewportWidth = 120;
|
||||||
const viewportHeight = 48;
|
const viewportHeight = 48;
|
||||||
const plotInsetLeft = 13;
|
const plotInsetLeft = 13;
|
||||||
@@ -34,6 +18,8 @@
|
|||||||
const plotInsetTop = 4;
|
const plotInsetTop = 4;
|
||||||
const plotInsetBottom = 9;
|
const plotInsetBottom = 9;
|
||||||
const fixedYBounds = { min: 0, max: 25 };
|
const fixedYBounds = { min: 0, max: 25 };
|
||||||
|
const maxCanvasDpr = 1.5;
|
||||||
|
const minDrawIntervalMs = 66;
|
||||||
|
|
||||||
interface CurveSample {
|
interface CurveSample {
|
||||||
x: number;
|
x: number;
|
||||||
@@ -45,23 +31,25 @@
|
|||||||
y: number;
|
y: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AxisTick {
|
let canvasEl: HTMLCanvasElement | undefined;
|
||||||
value: number;
|
let chartStageEl: HTMLDivElement | undefined;
|
||||||
label: string;
|
let currentTimeSeconds = 0;
|
||||||
plotX: number;
|
let timerId: ReturnType<typeof setInterval> | null = null;
|
||||||
plotY: number;
|
let resizeObserver: ResizeObserver | null = null;
|
||||||
}
|
let drawRequestId: number | null = null;
|
||||||
|
let lastDrawAt = 0;
|
||||||
|
let mounted = false;
|
||||||
|
|
||||||
|
$: i18n = locale === "zh-CN"
|
||||||
|
? { now: "当前", min: "最小", max: "最大", waiting: "等待数据" }
|
||||||
|
: { now: "Now", min: "Min", max: "Max", waiting: "Waiting" };
|
||||||
|
|
||||||
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatValue(value: number | null): string {
|
function formatValue(value: number | null): string {
|
||||||
if (value === null) {
|
return value === null ? "--" : value.toFixed(1);
|
||||||
return "--";
|
|
||||||
}
|
|
||||||
|
|
||||||
return value.toFixed(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatAxisValue(value: number, axis: "x" | "y"): string {
|
function formatAxisValue(value: number, axis: "x" | "y"): string {
|
||||||
@@ -73,6 +61,7 @@
|
|||||||
if (value < 60) {
|
if (value < 60) {
|
||||||
return `${value.toFixed(1)}s`;
|
return `${value.toFixed(1)}s`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mins = Math.floor(value / 60);
|
const mins = Math.floor(value / 60);
|
||||||
const secs = value - mins * 60;
|
const secs = value - mins * 60;
|
||||||
return `${mins}:${secs.toFixed(0).padStart(2, "0")}`;
|
return `${mins}:${secs.toFixed(0).padStart(2, "0")}`;
|
||||||
@@ -81,17 +70,6 @@
|
|||||||
return `${Math.round(value)} N`;
|
return `${Math.round(value)} N`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveDataBounds(values: number[]): { min: number; max: number } {
|
|
||||||
if (values.length === 0) {
|
|
||||||
return { min: 0, max: 1 };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
min: Math.min(...values),
|
|
||||||
max: Math.max(...values)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveBounds(values: number[]): { min: number; max: number } {
|
function resolveBounds(values: number[]): { min: number; max: number } {
|
||||||
if (values.length === 0) {
|
if (values.length === 0) {
|
||||||
return { min: 0, max: 1 };
|
return { min: 0, max: 1 };
|
||||||
@@ -108,34 +86,23 @@
|
|||||||
return { min, max };
|
return { min, max };
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapXToViewport(value: number, bounds: { min: number; max: number }): number {
|
function buildSamples(rawYValues: number[], rawXValues: number[], currentSeconds: number): CurveSample[] {
|
||||||
const span = bounds.max - bounds.min;
|
|
||||||
const chartWidth = viewportWidth - plotInsetLeft - plotInsetRight;
|
|
||||||
const ratio = span <= 0 ? 0.5 : (value - bounds.min) / span;
|
|
||||||
const mappedX = plotInsetLeft + ratio * chartWidth;
|
|
||||||
return Math.round(clamp(mappedX, plotInsetLeft, viewportWidth - plotInsetRight) * 100) / 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapYToViewport(value: number, bounds: { min: number; max: number }): number {
|
|
||||||
const span = bounds.max - bounds.min;
|
|
||||||
const chartHeight = viewportHeight - plotInsetTop - plotInsetBottom;
|
|
||||||
const ratio = span <= 0 ? 0.5 : (value - bounds.min) / span;
|
|
||||||
const mappedY = viewportHeight - plotInsetBottom - ratio * chartHeight;
|
|
||||||
return Math.round(clamp(mappedY, plotInsetTop, viewportHeight - plotInsetBottom) * 100) / 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildSamples(rawYValues: number[], rawXValues: number[]): CurveSample[] {
|
|
||||||
if (!rawYValues.length) {
|
if (!rawYValues.length) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
let previousX = 0;
|
const hasUsableXValues = rawXValues.length === rawYValues.length;
|
||||||
|
const realtimeSpacing = isRealtime
|
||||||
|
? Math.max(currentSeconds / Math.max(rawYValues.length - 1, 1), 0.1)
|
||||||
|
: 1;
|
||||||
|
const realtimeStart = isRealtime ? Math.max(0, currentSeconds - realtimeSpacing * (rawYValues.length - 1)) : 0;
|
||||||
|
let previousX = realtimeStart;
|
||||||
|
|
||||||
return rawYValues.map((rawY, index) => {
|
return rawYValues.map((rawY, index) => {
|
||||||
const x = rawXValues[index];
|
|
||||||
const y = Number.isFinite(rawY) ? Number(rawY) : 0;
|
const y = Number.isFinite(rawY) ? Number(rawY) : 0;
|
||||||
const fallbackX = index === 0 ? 0 : previousX + 1;
|
const rawX = rawXValues[index];
|
||||||
const resolvedX = Number.isFinite(x) ? Number(x) : fallbackX;
|
const fallbackX = isRealtime ? realtimeStart + index * realtimeSpacing : index + 1;
|
||||||
|
const resolvedX = hasUsableXValues && Number.isFinite(rawX) ? Number(rawX) : fallbackX;
|
||||||
const normalizedX = index === 0 ? resolvedX : Math.max(resolvedX, previousX);
|
const normalizedX = index === 0 ? resolvedX : Math.max(resolvedX, previousX);
|
||||||
previousX = normalizedX;
|
previousX = normalizedX;
|
||||||
return { x: normalizedX, y };
|
return { x: normalizedX, y };
|
||||||
@@ -143,132 +110,274 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveXScaleBounds(
|
function resolveXScaleBounds(
|
||||||
samples: CurveSample[],
|
samplesValue: CurveSample[],
|
||||||
currentSeconds: number,
|
currentSeconds: number,
|
||||||
realtime: boolean
|
realtime: boolean
|
||||||
): { min: number; max: number } {
|
): { min: number; max: number } {
|
||||||
if (samples.length === 0) {
|
if (samplesValue.length === 0) {
|
||||||
return { min: 0, max: 1 };
|
return { min: 0, max: 1 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const values = samples.map((sample) => sample.x);
|
|
||||||
const dataBounds = resolveBounds(values);
|
|
||||||
|
|
||||||
if (!realtime) {
|
if (!realtime) {
|
||||||
return dataBounds;
|
return resolveBounds(samplesValue.map((sample) => sample.x));
|
||||||
}
|
}
|
||||||
|
|
||||||
const firstX = samples[0].x;
|
const firstX = samplesValue[0].x;
|
||||||
const lastX = samples[samples.length - 1].x;
|
const lastX = samplesValue[samplesValue.length - 1].x;
|
||||||
const axisMax = Math.max(lastX, currentSeconds);
|
const axisMax = Math.max(lastX, currentSeconds);
|
||||||
const positiveDiffs = samples
|
const dataSpan = Math.max(lastX - firstX, 1);
|
||||||
.slice(1)
|
const axisMin = Math.max(0, axisMax - dataSpan);
|
||||||
.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]);
|
return resolveBounds([axisMin, axisMax]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mapXToViewport(value: number, bounds: { min: number; max: number }): number {
|
||||||
|
const span = bounds.max - bounds.min;
|
||||||
|
const chartWidth = viewportWidth - plotInsetLeft - plotInsetRight;
|
||||||
|
const ratio = span <= 0 ? 0.5 : (value - bounds.min) / span;
|
||||||
|
return clamp(plotInsetLeft + ratio * chartWidth, plotInsetLeft, viewportWidth - plotInsetRight);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapYToViewport(value: number, bounds: { min: number; max: number }): number {
|
||||||
|
const span = bounds.max - bounds.min;
|
||||||
|
const chartHeight = viewportHeight - plotInsetTop - plotInsetBottom;
|
||||||
|
const ratio = span <= 0 ? 0.5 : (value - bounds.min) / span;
|
||||||
|
return clamp(viewportHeight - plotInsetBottom - ratio * chartHeight, plotInsetTop, viewportHeight - plotInsetBottom);
|
||||||
|
}
|
||||||
|
|
||||||
function convertPoints(
|
function convertPoints(
|
||||||
samples: CurveSample[],
|
samplesValue: CurveSample[],
|
||||||
xBounds: { min: number; max: number },
|
xBounds: { min: number; max: number },
|
||||||
yBounds: { min: number; max: number }
|
yBounds: { min: number; max: number }
|
||||||
): PlotPoint[] {
|
): PlotPoint[] {
|
||||||
if (samples.length === 0) {
|
if (samplesValue.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (samples.length === 1) {
|
if (samplesValue.length === 1) {
|
||||||
return [{ x: viewportWidth / 2, y: viewportHeight / 2 }];
|
return [{ x: viewportWidth / 2, y: viewportHeight / 2 }];
|
||||||
}
|
}
|
||||||
|
|
||||||
return samples.map((sample) => {
|
return samplesValue.map((sample) => ({
|
||||||
return {
|
x: mapXToViewport(sample.x, xBounds),
|
||||||
x: mapXToViewport(sample.x, xBounds),
|
y: mapYToViewport(sample.y, yBounds)
|
||||||
y: mapYToViewport(sample.y, yBounds)
|
}));
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildYAxisTicks(
|
function scaleCanvas(context: CanvasRenderingContext2D): { width: number; height: number; dpr: number } | null {
|
||||||
yScaleBounds: { min: number; max: number },
|
if (!canvasEl || !chartStageEl) {
|
||||||
_yDataBounds: { min: number; max: number }
|
return null;
|
||||||
): AxisTick[] {
|
}
|
||||||
|
|
||||||
|
const width = chartStageEl.clientWidth;
|
||||||
|
const height = chartStageEl.clientHeight;
|
||||||
|
const dpr = Math.min(window.devicePixelRatio || 1, maxCanvasDpr);
|
||||||
|
|
||||||
|
if (width <= 0 || height <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextWidth = Math.round(width * dpr);
|
||||||
|
const nextHeight = Math.round(height * dpr);
|
||||||
|
if (canvasEl.width !== nextWidth || canvasEl.height !== nextHeight) {
|
||||||
|
canvasEl.width = nextWidth;
|
||||||
|
canvasEl.height = nextHeight;
|
||||||
|
canvasEl.style.width = `${width}px`;
|
||||||
|
canvasEl.style.height = `${height}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.setTransform((width * dpr) / viewportWidth, 0, 0, (height * dpr) / viewportHeight, 0, 0);
|
||||||
|
return { width, height, dpr };
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawGrid(context: CanvasRenderingContext2D, yBounds: { min: number; max: number }): void {
|
||||||
const tickValues = [25, 20, 15, 10, 5, 0];
|
const tickValues = [25, 20, 15, 10, 5, 0];
|
||||||
return tickValues.map((value) => ({
|
|
||||||
value,
|
|
||||||
label: formatAxisValue(value, "y"),
|
|
||||||
plotX: plotInsetLeft - 1.8,
|
|
||||||
plotY: mapYToViewport(value, yScaleBounds)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildXAxisTicks(xScaleBounds: { min: number; max: number }): AxisTick[] {
|
context.save();
|
||||||
if (!Number.isFinite(xScaleBounds.min) || !Number.isFinite(xScaleBounds.max)) {
|
context.lineWidth = 0.45;
|
||||||
return [];
|
context.strokeStyle = "rgb(128 170 180 / 0.18)";
|
||||||
|
context.fillStyle = "rgb(190 216 220 / 0.78)";
|
||||||
|
context.font = "600 3.2px system-ui, sans-serif";
|
||||||
|
context.textBaseline = "middle";
|
||||||
|
|
||||||
|
for (const tick of tickValues) {
|
||||||
|
const y = mapYToViewport(tick, yBounds);
|
||||||
|
context.beginPath();
|
||||||
|
context.moveTo(plotInsetLeft, y);
|
||||||
|
context.lineTo(viewportWidth - plotInsetRight, y);
|
||||||
|
context.stroke();
|
||||||
|
|
||||||
|
context.textAlign = "right";
|
||||||
|
context.fillText(formatAxisValue(tick, "y"), plotInsetLeft - 1.8, y + 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
const first = xScaleBounds.min;
|
context.restore();
|
||||||
const middle = xScaleBounds.min + (xScaleBounds.max - xScaleBounds.min) / 2;
|
|
||||||
const last = xScaleBounds.max;
|
|
||||||
const tickValues = [first, middle, last];
|
|
||||||
return tickValues.map((value) => ({
|
|
||||||
value,
|
|
||||||
label: formatAxisValue(value, "x"),
|
|
||||||
plotX: mapXToViewport(value, xScaleBounds),
|
|
||||||
plotY: viewportHeight - 0.9
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createLinePath(points: PlotPoint[]): string {
|
function drawXAxis(context: CanvasRenderingContext2D, xBounds: { min: number; max: number }): void {
|
||||||
if (points.length === 0) {
|
const first = xBounds.min;
|
||||||
return "";
|
const middle = xBounds.min + (xBounds.max - xBounds.min) / 2;
|
||||||
}
|
const last = xBounds.max;
|
||||||
|
const ticks = [first, middle, last];
|
||||||
|
|
||||||
return points.map((point, index) => `${index === 0 ? "M" : "L"} ${point.x} ${point.y}`).join(" ");
|
context.save();
|
||||||
|
context.fillStyle = "rgb(190 216 220 / 0.82)";
|
||||||
|
context.font = "600 3.2px system-ui, sans-serif";
|
||||||
|
context.textBaseline = "alphabetic";
|
||||||
|
|
||||||
|
ticks.forEach((tick, index) => {
|
||||||
|
const x = mapXToViewport(tick, xBounds);
|
||||||
|
context.textAlign = index === 0 ? "left" : index === ticks.length - 1 ? "right" : "center";
|
||||||
|
context.fillText(formatAxisValue(tick, "x"), x, viewportHeight - 0.9);
|
||||||
|
});
|
||||||
|
|
||||||
|
context.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
function createAreaPath(points: PlotPoint[]): string {
|
function drawArea(context: CanvasRenderingContext2D, points: PlotPoint[]): void {
|
||||||
if (points.length < 2) {
|
if (points.length < 2) {
|
||||||
return "";
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const linePath = createLinePath(points);
|
|
||||||
const firstPoint = points[0];
|
const firstPoint = points[0];
|
||||||
const lastPoint = points[points.length - 1];
|
const lastPoint = points[points.length - 1];
|
||||||
|
const gradient = context.createLinearGradient(0, plotInsetTop, 0, viewportHeight - plotInsetBottom);
|
||||||
|
gradient.addColorStop(0, "rgb(62 232 255 / 0.28)");
|
||||||
|
gradient.addColorStop(1, "rgb(62 232 255 / 0.02)");
|
||||||
|
|
||||||
return `${linePath} L ${lastPoint.x} ${viewportHeight - plotInsetBottom} L ${firstPoint.x} ${viewportHeight - plotInsetBottom} Z`;
|
context.save();
|
||||||
|
context.beginPath();
|
||||||
|
context.moveTo(firstPoint.x, firstPoint.y);
|
||||||
|
for (let index = 1; index < points.length; index += 1) {
|
||||||
|
context.lineTo(points[index].x, points[index].y);
|
||||||
|
}
|
||||||
|
context.lineTo(lastPoint.x, viewportHeight - plotInsetBottom);
|
||||||
|
context.lineTo(firstPoint.x, viewportHeight - plotInsetBottom);
|
||||||
|
context.closePath();
|
||||||
|
context.fillStyle = gradient;
|
||||||
|
context.fill();
|
||||||
|
context.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawLine(context: CanvasRenderingContext2D, points: PlotPoint[]): void {
|
||||||
|
if (!points.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.save();
|
||||||
|
context.lineWidth = 1.35;
|
||||||
|
context.lineCap = "round";
|
||||||
|
context.lineJoin = "round";
|
||||||
|
context.strokeStyle = "rgb(62 232 255 / 0.96)";
|
||||||
|
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.stroke();
|
||||||
|
|
||||||
|
const lastPoint = points[points.length - 1];
|
||||||
|
context.fillStyle = "rgb(133 255 68 / 0.98)";
|
||||||
|
context.beginPath();
|
||||||
|
context.arc(lastPoint.x, lastPoint.y, 1.7, 0, Math.PI * 2);
|
||||||
|
context.fill();
|
||||||
|
context.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawCanvas(): void {
|
||||||
|
if (!canvasEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = canvasEl.getContext("2d", { alpha: true });
|
||||||
|
if (!context) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scaled = scaleCanvas(context);
|
||||||
|
if (!scaled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.clearRect(0, 0, viewportWidth, viewportHeight);
|
||||||
|
|
||||||
|
if (sampleCount === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawGrid(context, yScaleBounds);
|
||||||
|
drawArea(context, plotPoints);
|
||||||
|
drawLine(context, plotPoints);
|
||||||
|
drawXAxis(context, xScaleBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleDraw(): void {
|
||||||
|
if (!mounted || typeof window === "undefined") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drawRequestId != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawRequestId = window.requestAnimationFrame((timestamp) => {
|
||||||
|
drawRequestId = null;
|
||||||
|
|
||||||
|
if (lastDrawAt > 0 && timestamp - lastDrawAt < minDrawIntervalMs) {
|
||||||
|
scheduleDraw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastDrawAt = timestamp;
|
||||||
|
drawCanvas();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$: sourceYValues = yValues && yValues.length ? yValues : summary.points;
|
$: sourceYValues = yValues && yValues.length ? yValues : summary.points;
|
||||||
$: sourceXValues = xValues && xValues.length ? xValues : summary.xValues ?? [];
|
$: sourceXValues = xValues && xValues.length ? xValues : summary.xValues ?? [];
|
||||||
$: samples = (() => {
|
$: samples = buildSamples(sourceYValues, sourceXValues, currentTimeSeconds);
|
||||||
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;
|
$: sampleCount = samples.length;
|
||||||
$: xScaleBounds = resolveXScaleBounds(samples, currentTimeSeconds, isRealtime);
|
$: xScaleBounds = resolveXScaleBounds(samples, currentTimeSeconds, isRealtime);
|
||||||
$: yScaleBounds = fixedYBounds;
|
$: yScaleBounds = fixedYBounds;
|
||||||
$: xDataBounds = resolveDataBounds(samples.map((sample) => sample.x));
|
|
||||||
$: 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) : [];
|
|
||||||
$: xAxisTicks = sampleCount > 0 ? buildXAxisTicks(xScaleBounds) : [];
|
|
||||||
$: latestValue = formatValue(summary.latest);
|
$: latestValue = formatValue(summary.latest);
|
||||||
$: minValue = formatValue(summary.min);
|
$: minValue = formatValue(summary.min);
|
||||||
$: maxValue = formatValue(summary.max);
|
$: maxValue = formatValue(summary.max);
|
||||||
|
$: {
|
||||||
|
sampleCount;
|
||||||
|
plotPoints;
|
||||||
|
xScaleBounds;
|
||||||
|
locale;
|
||||||
|
scheduleDraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
mounted = true;
|
||||||
|
currentTimeSeconds = Math.round((Date.now() - sessionStartedAt) / 100) / 10;
|
||||||
|
scheduleDraw();
|
||||||
|
|
||||||
|
timerId = setInterval(() => {
|
||||||
|
if (!isRealtime) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTimeSeconds = Math.round((Date.now() - sessionStartedAt) / 100) / 10;
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
if (chartStageEl) {
|
||||||
|
resizeObserver = new ResizeObserver(() => scheduleDraw());
|
||||||
|
resizeObserver.observe(chartStageEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mounted = false;
|
||||||
|
if (timerId != null) clearInterval(timerId);
|
||||||
|
if (drawRequestId != null) window.cancelAnimationFrame(drawRequestId);
|
||||||
|
resizeObserver?.disconnect();
|
||||||
|
timerId = null;
|
||||||
|
drawRequestId = null;
|
||||||
|
resizeObserver = null;
|
||||||
|
};
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<article
|
<article
|
||||||
@@ -290,52 +399,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="chart-stage">
|
<div class="chart-stage" bind:this={chartStageEl}>
|
||||||
<svg viewBox="0 0 {viewportWidth} {viewportHeight}" preserveAspectRatio="none" role="img" aria-label={summary.label}>
|
<canvas class="summary-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">
|
||||||
@@ -389,8 +454,7 @@
|
|||||||
transition:
|
transition:
|
||||||
opacity var(--fade-ms) cubic-bezier(0.18, 0.88, 0.3, 1),
|
opacity var(--fade-ms) cubic-bezier(0.18, 0.88, 0.3, 1),
|
||||||
transform var(--enter-ms) cubic-bezier(0.2, 0.9, 0.28, 1),
|
transform var(--enter-ms) cubic-bezier(0.2, 0.9, 0.28, 1),
|
||||||
border-color 460ms ease,
|
border-color 460ms ease;
|
||||||
filter 760ms ease;
|
|
||||||
transition-delay: calc(var(--panel-index) * 140ms);
|
transition-delay: calc(var(--panel-index) * 140ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,53 +544,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 {
|
.summary-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;
|
||||||
@@ -641,14 +664,6 @@
|
|||||||
box-shadow: inset 0 0 0 1px rgb(var(--hud-border-strong-rgb) / 0.08), 0 0 8px rgb(var(--hud-glow-rgb) / 0.1);
|
box-shadow: inset 0 0 0 1px rgb(var(--hud-border-strong-rgb) / 0.08), 0 0 8px rgb(var(--hud-glow-rgb) / 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-line {
|
|
||||||
filter: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-dot {
|
|
||||||
filter: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-stage {
|
.chart-stage {
|
||||||
background: linear-gradient(180deg, rgb(var(--hud-surface-alt-rgb) / 0.78), rgb(var(--hud-surface-deep-rgb) / 0.88));
|
background: linear-gradient(180deg, rgb(var(--hud-surface-alt-rgb) / 0.78), rgb(var(--hud-surface-deep-rgb) / 0.88));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,6 +191,7 @@
|
|||||||
const pointsPerSeries = 28;
|
const pointsPerSeries = 28;
|
||||||
const summaryPointsPerSeries = 42;
|
const summaryPointsPerSeries = 42;
|
||||||
const signalRenderTickMs = 1200;
|
const signalRenderTickMs = 1200;
|
||||||
|
const hudRealtimeRenderMs = 33;
|
||||||
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"];
|
||||||
@@ -285,6 +286,9 @@
|
|||||||
} | null = null;
|
} | null = null;
|
||||||
let devkitStatusTimer: number | null = null;
|
let devkitStatusTimer: number | null = null;
|
||||||
let sessionStartedAt: number = Date.now();
|
let sessionStartedAt: number = Date.now();
|
||||||
|
let pendingHudPacket: HudPacket | null = null;
|
||||||
|
let hudFrameRequestId: number | null = null;
|
||||||
|
let lastHudRenderAt = 0;
|
||||||
|
|
||||||
$: uiCopy = copyByLocale[locale];
|
$: uiCopy = copyByLocale[locale];
|
||||||
$: configLinks = buildConfigLinks(
|
$: configLinks = buildConfigLinks(
|
||||||
@@ -810,12 +814,10 @@
|
|||||||
const safeIndex = clamp(index, 0, replayFrames.length - 1);
|
const safeIndex = clamp(index, 0, replayFrames.length - 1);
|
||||||
const startIndex = Math.max(0, safeIndex - summaryPointsPerSeries + 1);
|
const startIndex = Math.max(0, safeIndex - summaryPointsPerSeries + 1);
|
||||||
const points: number[] = [];
|
const points: number[] = [];
|
||||||
const xSeconds: number[] = [];
|
|
||||||
for (let cursor = startIndex; cursor <= safeIndex; cursor += 1) {
|
for (let cursor = startIndex; cursor <= safeIndex; cursor += 1) {
|
||||||
points.push(replayFrameTotal(replayFrames[cursor]));
|
points.push(replayFrameTotal(replayFrames[cursor]));
|
||||||
xSeconds.push(replayFrames[cursor].dtsMs / 1000);
|
|
||||||
}
|
}
|
||||||
return buildSummary(points, xSeconds);
|
return buildSummary(points);
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyReplayFrame(index: number): void {
|
function applyReplayFrame(index: number): void {
|
||||||
@@ -828,7 +830,9 @@
|
|||||||
replayHasDisplayedFrame = true;
|
replayHasDisplayedFrame = true;
|
||||||
replayProgress = replayFrames.length > 1 ? safeIndex / (replayFrames.length - 1) : 1;
|
replayProgress = replayFrames.length > 1 ? safeIndex / (replayFrames.length - 1) : 1;
|
||||||
pressureMatrix = frameValuesToMatrix(replayFrames[safeIndex].values);
|
pressureMatrix = frameValuesToMatrix(replayFrames[safeIndex].values);
|
||||||
signalPanels = buildInactivePanels();
|
if (signalPanels.length > 0) {
|
||||||
|
signalPanels = buildInactivePanels();
|
||||||
|
}
|
||||||
summary = buildReplaySummaryAt(safeIndex);
|
summary = buildReplaySummaryAt(safeIndex);
|
||||||
hasSignalData = true;
|
hasSignalData = true;
|
||||||
}
|
}
|
||||||
@@ -987,7 +991,6 @@
|
|||||||
function buildEmptySummary(): HudSummary {
|
function buildEmptySummary(): HudSummary {
|
||||||
return {
|
return {
|
||||||
label: "Resultant Force",
|
label: "Resultant Force",
|
||||||
xValues: [],
|
|
||||||
points: [],
|
points: [],
|
||||||
latest: null,
|
latest: null,
|
||||||
min: null,
|
min: null,
|
||||||
@@ -1007,19 +1010,13 @@
|
|||||||
return shouldHideSummary(summaryValue.points) ? buildEmptySummary() : summaryValue;
|
return shouldHideSummary(summaryValue.points) ? buildEmptySummary() : summaryValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSummary(points: number[], xValues: number[] = []): HudSummary {
|
function buildSummary(points: number[]): HudSummary {
|
||||||
if (points.length === 0) {
|
if (points.length === 0) {
|
||||||
return buildEmptySummary();
|
return buildEmptySummary();
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedXValues = points.map((_, index) => {
|
|
||||||
const x = xValues[index];
|
|
||||||
return Number.isFinite(x) ? Number(x) : index + 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label: "Resultant Force",
|
label: "Resultant Force",
|
||||||
xValues: resolvedXValues,
|
|
||||||
points,
|
points,
|
||||||
latest: points[points.length - 1],
|
latest: points[points.length - 1],
|
||||||
min: Math.min(...points),
|
min: Math.min(...points),
|
||||||
@@ -1044,21 +1041,13 @@
|
|||||||
? summaryValue.points[summaryValue.points.length - 1]
|
? summaryValue.points[summaryValue.points.length - 1]
|
||||||
: randomBetween(280, 1600);
|
: randomBetween(280, 1600);
|
||||||
const next = Math.round(clamp(previous + randomBetween(-160, 160), 120, 2400) * 10) / 10;
|
const next = Math.round(clamp(previous + randomBetween(-160, 160), 120, 2400) * 10) / 10;
|
||||||
const nowSeconds = Math.round((Date.now() - sessionStartedAt) / 100) / 10;
|
|
||||||
const previousXValues =
|
|
||||||
summaryValue.xValues && summaryValue.xValues.length === summaryValue.points.length
|
|
||||||
? summaryValue.xValues
|
|
||||||
: summaryValue.points.map((_, index) => nowSeconds);
|
|
||||||
const points =
|
const points =
|
||||||
summaryValue.points.length >= summaryPointsPerSeries
|
summaryValue.points.length >= summaryPointsPerSeries
|
||||||
? summaryValue.points.slice(1)
|
? summaryValue.points.slice(1)
|
||||||
: summaryValue.points.slice();
|
: summaryValue.points.slice();
|
||||||
const xValues =
|
|
||||||
previousXValues.length >= summaryPointsPerSeries ? previousXValues.slice(1) : previousXValues.slice();
|
|
||||||
|
|
||||||
points.push(next);
|
points.push(next);
|
||||||
xValues.push(nowSeconds);
|
return buildSummary(points);
|
||||||
return buildSummary(points, xValues);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildInactivePanels(): HudSignalPanel[] {
|
function buildInactivePanels(): HudSignalPanel[] {
|
||||||
@@ -1069,23 +1058,66 @@
|
|||||||
if (replayHasData) {
|
if (replayHasData) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
signalPanels = showSignalPanels ? packet.panels : buildInactivePanels();
|
if (showSignalPanels) {
|
||||||
if (packet.summary.points.length > 0) {
|
signalPanels = packet.panels;
|
||||||
const nowSeconds = Math.round((Date.now() - sessionStartedAt) / 100) / 10;
|
} else if (signalPanels.length > 0) {
|
||||||
const pointCount = packet.summary.points.length;
|
signalPanels = buildInactivePanels();
|
||||||
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 };
|
|
||||||
} else {
|
|
||||||
summary = packet.summary;
|
|
||||||
}
|
}
|
||||||
|
summary = packet.summary;
|
||||||
pressureMatrix = packet.pressureMatrix;
|
pressureMatrix = packet.pressureMatrix;
|
||||||
hasSignalData = signalPanels.length > 0 || packet.summary.points.length > 0;
|
hasSignalData = signalPanels.length > 0 || packet.summary.points.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getFrameClock(): number {
|
||||||
|
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelPendingHudPacket(): void {
|
||||||
|
pendingHudPacket = null;
|
||||||
|
if (hudFrameRequestId != null && typeof window !== "undefined") {
|
||||||
|
window.cancelAnimationFrame(hudFrameRequestId);
|
||||||
|
hudFrameRequestId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleHudPacketFlush(): void {
|
||||||
|
if (hudFrameRequestId != null || typeof window === "undefined") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hudFrameRequestId = window.requestAnimationFrame(flushPendingHudPacket);
|
||||||
|
}
|
||||||
|
|
||||||
|
function flushPendingHudPacket(timestamp: number = getFrameClock()): void {
|
||||||
|
hudFrameRequestId = null;
|
||||||
|
|
||||||
|
if (!pendingHudPacket) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsed = timestamp - lastHudRenderAt;
|
||||||
|
if (lastHudRenderAt > 0 && elapsed < hudRealtimeRenderMs) {
|
||||||
|
scheduleHudPacketFlush();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const packet = pendingHudPacket;
|
||||||
|
pendingHudPacket = null;
|
||||||
|
lastHudRenderAt = timestamp;
|
||||||
|
applyPacket(packet);
|
||||||
|
}
|
||||||
|
|
||||||
|
function enqueueHudPacket(packet: HudPacket): void {
|
||||||
|
if (replayHasData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingHudPacket = packet;
|
||||||
|
scheduleHudPacketFlush();
|
||||||
|
}
|
||||||
|
|
||||||
function clearHudPanels(): void {
|
function clearHudPanels(): void {
|
||||||
|
cancelPendingHudPacket();
|
||||||
hasSignalData = false;
|
hasSignalData = false;
|
||||||
signalPanels = buildInactivePanels();
|
signalPanels = buildInactivePanels();
|
||||||
summary = buildEmptySummary();
|
summary = buildEmptySummary();
|
||||||
@@ -1906,7 +1938,7 @@
|
|||||||
void checkForAppUpdate();
|
void checkForAppUpdate();
|
||||||
void pollDevKitStatus();
|
void pollDevKitStatus();
|
||||||
devkitStatusTimer = window.setInterval(() => void pollDevKitStatus(), 3000);
|
devkitStatusTimer = window.setInterval(() => void pollDevKitStatus(), 3000);
|
||||||
void startTauriHudStream(applyPacket)
|
void startTauriHudStream(enqueueHudPacket)
|
||||||
.then((unlisten) => {
|
.then((unlisten) => {
|
||||||
if (disposed) {
|
if (disposed) {
|
||||||
unlisten();
|
unlisten();
|
||||||
@@ -1936,11 +1968,12 @@
|
|||||||
console.error("Failed to listen for devkit_pzt_angle:", error);
|
console.error("Failed to listen for devkit_pzt_angle:", error);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
stopMockFeed = startMockFeed(applyPacket);
|
stopMockFeed = startMockFeed(enqueueHudPacket);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
disposed = true;
|
disposed = true;
|
||||||
|
cancelPendingHudPacket();
|
||||||
pauseReplayPlayback();
|
pauseReplayPlayback();
|
||||||
stopMockFeed?.();
|
stopMockFeed?.();
|
||||||
unlistenHudStream?.();
|
unlistenHudStream?.();
|
||||||
|
|||||||
Reference in New Issue
Block a user