fix(lab): make replay pause authoritative
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user