fix(lab): make replay pause authoritative

This commit is contained in:
DCCONSTRUCTIONS
2026-08-24 00:12:49 +03:00
parent a7caed70c1
commit 8877ee947f
4 changed files with 72 additions and 13 deletions
@@ -40,6 +40,23 @@ export function advanceRecordedEvidencePlayback(
return { ...playback, currentSeconds: clampRecordedEvidenceSeconds(next, range) };
}
export function synchronizeRecordedEvidencePlayback(
current: RecordedObservationPlayback,
observed: RecordedObservationPlayback,
range: RecordedEvidencePlaybackRange,
): RecordedObservationPlayback {
const currentSeconds = clampRecordedEvidenceSeconds(observed.currentSeconds, range);
return {
...current,
currentSeconds,
// The transport button owns play/pause intent. A native frame callback may
// still report the previous media state immediately after an operator
// command, so it must not restart playback. The sealed end is the only
// media-clock transition allowed to stop the shared LAB clock.
playing: currentSeconds >= range.endSeconds ? false : current.playing,
};
}
export function useRecordedEvidencePlayback(
range: RecordedEvidencePlaybackRange | null,
{ clock = "animation" }: { clock?: "animation" | "external" } = {},
@@ -106,11 +123,7 @@ export function useRecordedEvidencePlayback(
const synchronize = useCallback((next: RecordedObservationPlayback) => {
if (!validRange(range) || !Number.isFinite(next.currentSeconds)) return;
setPlayback((current) => ({
...current,
currentSeconds: clampRecordedEvidenceSeconds(next.currentSeconds, range),
playing: next.playing,
}));
setPlayback((current) => synchronizeRecordedEvidencePlayback(current, next, range));
}, [range]);
return useMemo(() => ({
@@ -219,10 +219,13 @@
.m4-replay-threat-visual__overlay {
box-sizing: border-box;
min-width: 0;
align-items: stretch;
width: min(22rem, calc(33.333333% - 0.8rem));
grid-template-columns: minmax(0, 1fr);
width: max-content;
max-width: calc(var(--m4-replay-threat-overlay-pane-width, 100%) - 1.2rem);
grid-template-columns: minmax(0, max-content);
gap: 0.3rem;
overflow: hidden;
background: transparent;
padding: 0;
backdrop-filter: none;
@@ -235,9 +238,17 @@
backdrop-filter: blur(var(--nodedc-blur-control));
}
.m4-replay-threat-visual__overlay span,
.m4-replay-threat-visual__overlay strong,
.m4-replay-threat-visual__overlay small {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (max-width: 900px) {
.m4-replay-threat-visual__overlay {
width: min(22rem, calc(100% - 1.2rem));
max-width: calc(100% - 1.2rem);
}
}
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
import {
Button,
Icon,
@@ -442,8 +442,17 @@ export function M4ReplayThreatVisual({
const trailingActions = !splitView && spatialMode ? resetSpatialView : null;
const overlayPanePercent = splitView && splitOrientation === "vertical"
? splitPrimarySize
: 100;
const overlay = metadata.timeline && frame ? (
<div className="l3-visual-audit__overlay m4-replay-threat-visual__overlay">
<div
className="l3-visual-audit__overlay m4-replay-threat-visual__overlay"
style={{
"--m4-replay-threat-overlay-pane-width": `${overlayPanePercent}%`,
} as CSSProperties}
>
<div>
<span>RAVNOVES00 · recorded realtime</span>
<strong>frame {frame.sequence + 1}/{metadata.timeline.frameCount}</strong>