fix(viewer): gate recorded perception readiness

This commit is contained in:
DCCONSTRUCTIONS
2026-07-23 12:27:48 +03:00
parent ee15160862
commit 9f712bf353
6 changed files with 347 additions and 40 deletions
@@ -45,6 +45,7 @@ import {
type RerunPlaybackState,
type RerunSelection,
type RerunViewportStatus,
type RecordedPerceptionLoadState,
} from "../components/RerunViewport";
import {
capabilityStatusLabel,
@@ -307,9 +308,14 @@ function SpatialWorkspace({
const [playbackState, setPlaybackState] = useState<RerunPlaybackState | null>(null);
const [playbackController, setPlaybackController] = useState<RerunPlaybackController | null>(null);
const [recordedViewResetGeneration, setRecordedViewResetGeneration] = useState<0 | 1>(0);
const [perceptionAvailability, setPerceptionAvailability] = useState<
"unknown" | "available" | "unavailable"
>("unknown");
const [perceptionLoad, setPerceptionLoad] = useState<RecordedPerceptionLoadState>({
phase: "idle",
receivedBytes: 0,
totalBytes: null,
progress: null,
message: "",
});
const [perceptionRetryGeneration, setPerceptionRetryGeneration] = useState(0);
const [showDetections2d, setShowDetections2d] = useState(false);
const [showSegmentation, setShowSegmentation] = useState(false);
const [showCuboids3d, setShowCuboids3d] = useState(false);
@@ -335,7 +341,9 @@ function SpatialWorkspace({
const mediaSources = observationSources.filter(
(source) => source.capabilities.overlay && source.modality !== "point-cloud",
);
const recordedPerceptionSupported = recordedSource && perceptionAvailability !== "unavailable";
const recordedPerceptionSupported =
recordedSource && perceptionLoad.phase !== "unavailable";
const recordedPerceptionReady = recordedSource && perceptionLoad.phase === "ready";
const recordedPerceptionEnabled =
showDetections2d || showSegmentation || showCuboids3d;
// The native recorded camera remains the authoritative original. Only 2D
@@ -421,9 +429,9 @@ function SpatialWorkspace({
(next: RerunPlaybackController | null) => setPlaybackController(next),
[],
);
const onPerceptionAvailabilityChange = useCallback((available: boolean) => {
setPerceptionAvailability(available ? "available" : "unavailable");
if (!available) {
const onPerceptionLoadChange = useCallback((next: RecordedPerceptionLoadState) => {
setPerceptionLoad(next);
if (next.phase === "unavailable" || next.phase === "error") {
setShowDetections2d(false);
setShowSegmentation(false);
setShowCuboids3d(false);
@@ -431,12 +439,19 @@ function SpatialWorkspace({
}, []);
useEffect(() => {
setPerceptionAvailability("unknown");
setPerceptionLoad({
phase: recordedSource ? "loading" : "idle",
receivedBytes: 0,
totalBytes: null,
progress: null,
message: recordedSource ? "Ожидаем канал AI-слоёв." : "",
});
setPerceptionRetryGeneration(0);
setShowDetections2d(false);
setShowSegmentation(false);
setShowCuboids3d(false);
setRecordedViewResetGeneration(0);
}, [sourceUrl]);
}, [recordedSource, sourceUrl]);
useEffect(() => {
if (pointCloudVisible && sourceUrl.trim()) return;
@@ -445,7 +460,13 @@ function SpatialWorkspace({
setSelection(null);
setPlaybackState(null);
setPlaybackController(null);
setPerceptionAvailability("unknown");
setPerceptionLoad({
phase: "idle",
receivedBytes: 0,
totalBytes: null,
progress: null,
message: "",
});
setShowDetections2d(false);
setShowSegmentation(false);
setShowCuboids3d(false);
@@ -507,6 +528,7 @@ function SpatialWorkspace({
variant={detections2dActive ? "primary" : "secondary"}
icon={<Icon name="target" />}
aria-pressed={detections2dActive}
disabled={recordedSource && !recordedPerceptionReady}
onClick={() => recordedSource
? setShowDetections2d((current) => !current)
: onLivePerceptionLayersChange({
@@ -521,6 +543,7 @@ function SpatialWorkspace({
variant={segmentationActive ? "primary" : "secondary"}
icon={<Icon name="image" />}
aria-pressed={segmentationActive}
disabled={recordedSource && !recordedPerceptionReady}
onClick={() => recordedSource
? setShowSegmentation((current) => !current)
: onLivePerceptionLayersChange({
@@ -535,6 +558,7 @@ function SpatialWorkspace({
variant={cuboids3dActive ? "primary" : "secondary"}
icon={<Icon name="apps" />}
aria-pressed={cuboids3dActive}
disabled={recordedSource && !recordedPerceptionReady}
onClick={() => recordedSource
? setShowCuboids3d((current) => !current)
: onLivePerceptionLayersChange({
@@ -546,6 +570,22 @@ function SpatialWorkspace({
</Button>
</div>
) : null}
{recordedSource && perceptionLoad.phase === "loading" ? (
<div className="spatial-toolbar__perception-status" role="status">
<span className="busy-indicator" aria-hidden="true" />
<span>{perceptionLoad.message || "Готовим AI-слои."}</span>
</div>
) : null}
{recordedSource && perceptionLoad.phase === "error" ? (
<button
type="button"
className="spatial-toolbar__perception-retry"
onClick={() => setPerceptionRetryGeneration((generation) => generation + 1)}
>
<Icon name="refresh" size={14} />
Повторить AI
</button>
) : null}
{recordedSource && presentedViewerStatus === "ready" ? (
<Button
size="compact"
@@ -591,12 +631,17 @@ function SpatialWorkspace({
sceneSettings={sceneSettings}
recordedViewResetGeneration={recordedViewResetGeneration}
recordedPerceptionLayers={{
enabled: recordedPerceptionSupported && recordedPerceptionEnabled,
enabled:
recordedPerceptionSupported &&
recordedPerceptionReady &&
recordedPerceptionEnabled,
detections2d: showDetections2d,
segmentation: showSegmentation,
cuboids3d: showCuboids3d,
}}
onPerceptionAvailabilityChange={onPerceptionAvailabilityChange}
recordedPerceptionRetryGeneration={perceptionRetryGeneration}
lockPerceptionCameraInteraction={unifiedPerception}
onPerceptionLoadChange={onPerceptionLoadChange}
onStatusChange={onStatusChange}
onSelectionChange={onSelectionChange}
onPlaybackChange={onPlaybackChange}