feat(viewer): add recorded point colors and trajectory follow
This commit is contained in:
@@ -201,18 +201,6 @@ export default function App() {
|
||||
const recordedSessionAdmission = useRecordedSessionAdmission(
|
||||
replayActive ? recordedReplay : null,
|
||||
);
|
||||
const displayPaletteOptions = replayActive
|
||||
? paletteOptions.map((option) => {
|
||||
if (option.value === "turbo") {
|
||||
return {
|
||||
...option,
|
||||
label: "Цвет записи",
|
||||
description: "Вернуть цвета, сохранённые внутри RRD",
|
||||
};
|
||||
}
|
||||
return option.value === "custom" ? option : { ...option, disabled: true };
|
||||
})
|
||||
: paletteOptions;
|
||||
runtimeUpdateViewerSettingsRef.current = runtime.updateViewerSettings;
|
||||
replayActiveRef.current = replayActive;
|
||||
|
||||
@@ -954,7 +942,6 @@ export default function App() {
|
||||
label="Атрибут цвета"
|
||||
value={displayDraft.colorMode}
|
||||
options={colorModeOptions}
|
||||
disabled={replayActive}
|
||||
onChange={(colorMode) => stageDisplayPatch({ colorMode })}
|
||||
/>
|
||||
</ControlRow>
|
||||
@@ -965,12 +952,12 @@ export default function App() {
|
||||
variant="split"
|
||||
label="Палитра"
|
||||
value={displayDraft.palette}
|
||||
options={displayPaletteOptions}
|
||||
options={paletteOptions}
|
||||
onChange={(palette) => stageDisplayPatch({ palette })}
|
||||
/>
|
||||
</ControlRow>
|
||||
</div>
|
||||
{displayDraft.palette === "custom" ? (
|
||||
{displayDraft.palette === "custom" || displayDraft.colorMode === "class" ? (
|
||||
<div
|
||||
className="scene-settings-commit-field"
|
||||
onPointerUp={flushDisplaySettings}
|
||||
@@ -988,8 +975,8 @@ export default function App() {
|
||||
) : null}
|
||||
{replayActive ? (
|
||||
<p className="scene-window-note">
|
||||
В архиве градиентные цвета уже записаны в RRD. Без переэкспорта
|
||||
можно вернуть цвет записи или назначить один свой цвет.
|
||||
Первый новый режим читает индекс архивных точек. Следующие палитры
|
||||
переключаются из подготовленного цветового кэша без переэкспорта геометрии.
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,11 @@ export interface RecordedPerceptionLoadState {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type RecordedPointColorLoadState = Pick<
|
||||
RecordedPerceptionLoadState,
|
||||
"phase" | "receivedBytes" | "totalBytes" | "progress" | "message"
|
||||
>;
|
||||
|
||||
export interface RecordedPerceptionLayers {
|
||||
enabled: boolean;
|
||||
detections2d: boolean;
|
||||
@@ -75,15 +80,18 @@ export interface RerunViewportProps {
|
||||
| "showPoints"
|
||||
| "showTrajectory"
|
||||
| "pointSize"
|
||||
| "colorMode"
|
||||
| "palette"
|
||||
| "customColor"
|
||||
>;
|
||||
recordedView?: RecordedRerunView;
|
||||
recordedViewResetGeneration?: 0 | 1;
|
||||
recordedFollowTrajectory?: boolean;
|
||||
recordedPerceptionLayers?: RecordedPerceptionLayers;
|
||||
recordedPerceptionRetryGeneration?: number;
|
||||
lockPerceptionCameraInteraction?: boolean;
|
||||
onPerceptionLoadChange?: (state: RecordedPerceptionLoadState) => void;
|
||||
onPointColorLoadChange?: (state: RecordedPointColorLoadState) => void;
|
||||
}
|
||||
|
||||
export interface RecordedRrdArtifactDescriptor {
|
||||
@@ -110,6 +118,7 @@ interface RecordedRerunIdentity {
|
||||
const RECORDED_RRD_PATH = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\/recording\.rrd$/;
|
||||
const RECORDED_BLUEPRINT_PATH = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\/blueprint\.rrd$/;
|
||||
const RECORDED_PERCEPTION_PATH = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\/perception\.rrd$/;
|
||||
const RECORDED_POINT_COLORS_PATH = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\/point-colors\.rrd$/;
|
||||
const MAX_BLUEPRINT_BYTES = 1_048_576;
|
||||
const MAX_PERCEPTION_BYTES = 512 * 1024 * 1024;
|
||||
const BUFFER_END_TOLERANCE_NS = 1_000_000;
|
||||
@@ -117,6 +126,16 @@ const RECORDED_OPEN_MIN_TIMEOUT_MS = 120_000;
|
||||
const RECORDED_OPEN_MAX_TIMEOUT_MS = 1_800_000;
|
||||
const RECORDED_OPEN_GRACE_MS = 30_000;
|
||||
const RECORDED_OPEN_MIN_BYTES_PER_SECOND = 2 * 1024 * 1024;
|
||||
const RECORDED_BASE_POINT_COLOR_KEY = "intensity|turbo|-";
|
||||
|
||||
export function recordedPointColorKey(
|
||||
settings: Pick<SceneSettings, "colorMode" | "palette" | "customColor">,
|
||||
): string {
|
||||
const custom = settings.palette === "custom" || settings.colorMode === "class"
|
||||
? settings.customColor.toLowerCase()
|
||||
: "-";
|
||||
return `${settings.colorMode}|${settings.palette}|${custom}`;
|
||||
}
|
||||
|
||||
export function recordedOpenWatchdogTimeoutMs(byteLength: number): number {
|
||||
if (!Number.isSafeInteger(byteLength) || byteLength < 4) {
|
||||
@@ -459,6 +478,84 @@ export function resolveRecordedPerceptionUrl(sourceUrl: string, origin: string):
|
||||
return endpoint.origin === base.origin ? endpoint.href : null;
|
||||
}
|
||||
|
||||
export function resolveRecordedPointColorsUrl(sourceUrl: string, origin: string): string | null {
|
||||
const normalized = sourceUrl.trim();
|
||||
if (!RECORDED_RRD_PATH.test(normalized)) return null;
|
||||
const base = new URL(origin);
|
||||
const endpoint = new URL(
|
||||
normalized.replace(/\/recording\.rrd$/, "/point-colors.rrd"),
|
||||
`${base.origin}/`,
|
||||
);
|
||||
return endpoint.origin === base.origin ? endpoint.href : null;
|
||||
}
|
||||
|
||||
export async function fetchRecordedPointColorsRrd(
|
||||
endpointUrl: string,
|
||||
settings: Pick<SceneSettings, "colorMode" | "palette" | "customColor">,
|
||||
identity: RecordedRerunIdentity,
|
||||
{
|
||||
origin,
|
||||
signal,
|
||||
fetcher = globalThis.fetch,
|
||||
}: {
|
||||
origin: string;
|
||||
signal?: AbortSignal;
|
||||
fetcher?: typeof globalThis.fetch;
|
||||
},
|
||||
): Promise<Uint8Array> {
|
||||
const base = new URL(origin);
|
||||
const endpoint = new URL(endpointUrl, base.origin);
|
||||
if (
|
||||
endpoint.origin !== base.origin ||
|
||||
endpoint.search ||
|
||||
endpoint.hash ||
|
||||
!RECORDED_POINT_COLORS_PATH.test(endpoint.pathname) ||
|
||||
!["intensity", "height", "distance", "rgb", "class"].includes(settings.colorMode) ||
|
||||
!["turbo", "viridis", "plasma", "grayscale", "custom"].includes(settings.palette) ||
|
||||
!/^#[0-9A-Fa-f]{6}$/.test(settings.customColor) ||
|
||||
identity.applicationId !== "nodedc_mission_core_recorded" ||
|
||||
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(identity.recordingId)
|
||||
) {
|
||||
throw new Error("Unsafe recorded point-color request");
|
||||
}
|
||||
const response = await fetcher(endpoint.href, {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
Accept: "application/vnd.rerun.rrd",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
application_id: identity.applicationId,
|
||||
recording_id: identity.recordingId,
|
||||
color_mode: settings.colorMode,
|
||||
palette: settings.palette,
|
||||
custom_color: settings.customColor,
|
||||
}),
|
||||
signal,
|
||||
});
|
||||
const contentType = response.headers.get("Content-Type")?.split(";", 1)[0].trim();
|
||||
const declaredLength = Number(response.headers.get("Content-Length"));
|
||||
if (
|
||||
!response.ok ||
|
||||
contentType !== "application/vnd.rerun.rrd" ||
|
||||
(Number.isFinite(declaredLength) && declaredLength < 4)
|
||||
) {
|
||||
throw new Error("Invalid recorded point-color response");
|
||||
}
|
||||
const payload = new Uint8Array(await response.arrayBuffer());
|
||||
if (
|
||||
payload.byteLength < 4 ||
|
||||
payload[0] !== 0x52 ||
|
||||
payload[1] !== 0x52 ||
|
||||
payload[2] !== 0x46 ||
|
||||
payload[3] !== 0x32
|
||||
) {
|
||||
throw new Error("Invalid recorded point-color RRD");
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
export async function fetchRecordedBlueprintRrd(
|
||||
endpointUrl: string,
|
||||
settings: Pick<
|
||||
@@ -468,6 +565,7 @@ export async function fetchRecordedBlueprintRrd(
|
||||
| "showPoints"
|
||||
| "showTrajectory"
|
||||
| "pointSize"
|
||||
| "colorMode"
|
||||
| "palette"
|
||||
| "customColor"
|
||||
>,
|
||||
@@ -478,6 +576,7 @@ export async function fetchRecordedBlueprintRrd(
|
||||
signal,
|
||||
activeView = "spatial",
|
||||
viewResetGeneration = 0,
|
||||
followTrajectory = false,
|
||||
perceptionLayers = {
|
||||
enabled: false,
|
||||
detections2d: false,
|
||||
@@ -491,6 +590,7 @@ export async function fetchRecordedBlueprintRrd(
|
||||
signal?: AbortSignal;
|
||||
activeView?: RecordedRerunView;
|
||||
viewResetGeneration?: 0 | 1;
|
||||
followTrajectory?: boolean;
|
||||
perceptionLayers?: RecordedPerceptionLayers;
|
||||
fetcher?: typeof globalThis.fetch;
|
||||
},
|
||||
@@ -508,6 +608,7 @@ export async function fetchRecordedBlueprintRrd(
|
||||
!Number.isFinite(settings.pointSize) ||
|
||||
settings.pointSize < 0.1 ||
|
||||
settings.pointSize > 32 ||
|
||||
!["intensity", "height", "distance", "rgb", "class"].includes(settings.colorMode) ||
|
||||
!["turbo", "viridis", "plasma", "grayscale", "custom"].includes(settings.palette) ||
|
||||
!/^#[0-9A-Fa-f]{6}$/.test(settings.customColor) ||
|
||||
!["spatial", "perception", "perception3d", "metrics"].includes(activeView) ||
|
||||
@@ -540,10 +641,12 @@ export async function fetchRecordedBlueprintRrd(
|
||||
show_points: settings.showPoints,
|
||||
show_trajectory: settings.showTrajectory,
|
||||
point_size: settings.pointSize,
|
||||
color_mode: settings.colorMode,
|
||||
palette: settings.palette,
|
||||
custom_color: settings.customColor,
|
||||
active_view: activeView,
|
||||
view_reset_generation: viewResetGeneration,
|
||||
follow_trajectory: followTrajectory,
|
||||
unified_perception:
|
||||
perceptionLayers.detections2d || perceptionLayers.segmentation,
|
||||
show_detections_2d: perceptionLayers.detections2d,
|
||||
@@ -679,6 +782,7 @@ export function RerunViewport({
|
||||
sceneSettings,
|
||||
recordedView = "spatial",
|
||||
recordedViewResetGeneration = 0,
|
||||
recordedFollowTrajectory = false,
|
||||
recordedPerceptionLayers = {
|
||||
enabled: false,
|
||||
detections2d: false,
|
||||
@@ -688,6 +792,7 @@ export function RerunViewport({
|
||||
recordedPerceptionRetryGeneration = 0,
|
||||
lockPerceptionCameraInteraction = false,
|
||||
onPerceptionLoadChange,
|
||||
onPointColorLoadChange,
|
||||
}: RerunViewportProps) {
|
||||
const hostRef = useRef<HTMLDivElement>(null);
|
||||
const [status, setStatus] = useState<RerunViewportStatus>(sourceUrl ? "loading" : "idle");
|
||||
@@ -696,6 +801,7 @@ export function RerunViewport({
|
||||
const blueprintChannelRef = useRef<RerunBlueprintChannel | null>(null);
|
||||
const perceptionChannelRef = useRef<RerunBlueprintChannel | null>(null);
|
||||
const loadedPerceptionChannelRef = useRef<RerunBlueprintChannel | null>(null);
|
||||
const appliedPointColorKeyRef = useRef<string | null>(null);
|
||||
const recordedIdentityRef = useRef<RecordedRerunIdentity | null>(null);
|
||||
const blueprintSessionIdRef = useRef(crypto.randomUUID().replaceAll("-", ""));
|
||||
const presentationGateRef = useRef(presentationGate);
|
||||
@@ -708,6 +814,9 @@ export function RerunViewport({
|
||||
const recordedPerceptionUrl = sourceUrl
|
||||
? resolveRecordedPerceptionUrl(sourceUrl, window.location.origin)
|
||||
: null;
|
||||
const recordedPointColorsUrl = sourceUrl
|
||||
? resolveRecordedPointColorsUrl(sourceUrl, window.location.origin)
|
||||
: null;
|
||||
const presentationStatus = rerunPresentationStatus(
|
||||
status,
|
||||
presentationGate,
|
||||
@@ -822,6 +931,7 @@ export function RerunViewport({
|
||||
};
|
||||
|
||||
host.replaceChildren();
|
||||
appliedPointColorKeyRef.current = null;
|
||||
setStatus("loading");
|
||||
setRecordingBufferProgress(null);
|
||||
onStatusChange?.("loading");
|
||||
@@ -1213,7 +1323,7 @@ export function RerunViewport({
|
||||
autoplayWhenReady,
|
||||
expectedTimelineEndSeconds,
|
||||
expectedTimelineStartSeconds,
|
||||
followLive,
|
||||
followLive,
|
||||
initialPlaybackStartSeconds,
|
||||
onPlaybackChange,
|
||||
onPlaybackControllerChange,
|
||||
@@ -1231,6 +1341,10 @@ export function RerunViewport({
|
||||
loadedPerceptionChannelRef.current = null;
|
||||
}, [recordedPerceptionUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
appliedPointColorKeyRef.current = null;
|
||||
}, [recordedPointColorsUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!recordedPerceptionUrl) return;
|
||||
const active = perceptionChannelRef.current;
|
||||
@@ -1327,6 +1441,89 @@ export function RerunViewport({
|
||||
recordedPerceptionUrl,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!recordedPointColorsUrl || !sceneSettings) return;
|
||||
const active = blueprintChannelRef.current;
|
||||
const identity = recordedIdentityRef.current;
|
||||
if (
|
||||
!active ||
|
||||
!identity ||
|
||||
!active.channel.ready
|
||||
) return;
|
||||
const desiredKey = recordedPointColorKey(sceneSettings);
|
||||
if (
|
||||
appliedPointColorKeyRef.current === null &&
|
||||
desiredKey === RECORDED_BASE_POINT_COLOR_KEY
|
||||
) {
|
||||
appliedPointColorKeyRef.current = desiredKey;
|
||||
onPointColorLoadChange?.({
|
||||
phase: "ready",
|
||||
receivedBytes: 0,
|
||||
totalBytes: 0,
|
||||
progress: 1,
|
||||
message: "Цвет записи готов.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (appliedPointColorKeyRef.current === desiredKey) {
|
||||
onPointColorLoadChange?.({
|
||||
phase: "ready",
|
||||
receivedBytes: 0,
|
||||
totalBytes: null,
|
||||
progress: 1,
|
||||
message: "Окраска точек готова.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const abort = new AbortController();
|
||||
onPointColorLoadChange?.({
|
||||
phase: "loading",
|
||||
receivedBytes: 0,
|
||||
totalBytes: null,
|
||||
progress: null,
|
||||
message: "Готовим архивную окраску точек.",
|
||||
});
|
||||
void fetchRecordedPointColorsRrd(recordedPointColorsUrl, sceneSettings, identity, {
|
||||
origin: window.location.origin,
|
||||
signal: abort.signal,
|
||||
}).then((payload) => {
|
||||
if (
|
||||
abort.signal.aborted ||
|
||||
blueprintChannelRef.current !== active ||
|
||||
recordedIdentityRef.current !== identity ||
|
||||
!active.channel.ready
|
||||
) {
|
||||
return;
|
||||
}
|
||||
active.channel.send_rrd(payload);
|
||||
appliedPointColorKeyRef.current = desiredKey;
|
||||
onPointColorLoadChange?.({
|
||||
phase: "ready",
|
||||
receivedBytes: payload.byteLength,
|
||||
totalBytes: payload.byteLength,
|
||||
progress: 1,
|
||||
message: "Окраска точек готова.",
|
||||
});
|
||||
}).catch(() => {
|
||||
if (abort.signal.aborted) return;
|
||||
onPointColorLoadChange?.({
|
||||
phase: "error",
|
||||
receivedBytes: 0,
|
||||
totalBytes: null,
|
||||
progress: null,
|
||||
message: "Окраска точек не загрузилась.",
|
||||
});
|
||||
});
|
||||
return () => abort.abort();
|
||||
}, [
|
||||
blueprintChannelRevision,
|
||||
onPointColorLoadChange,
|
||||
recordedPointColorsUrl,
|
||||
sceneSettings?.colorMode,
|
||||
sceneSettings?.customColor,
|
||||
sceneSettings?.palette,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!recordedBlueprintUrl || !sceneSettings) return;
|
||||
const active = blueprintChannelRef.current;
|
||||
@@ -1344,6 +1541,7 @@ export function RerunViewport({
|
||||
signal: abort.signal,
|
||||
activeView: recordedView,
|
||||
viewResetGeneration: recordedViewResetGeneration,
|
||||
followTrajectory: recordedFollowTrajectory,
|
||||
perceptionLayers: recordedPerceptionLayers,
|
||||
}).then((payload) => {
|
||||
if (
|
||||
@@ -1365,12 +1563,14 @@ export function RerunViewport({
|
||||
recordedBlueprintUrl,
|
||||
recordedView,
|
||||
recordedViewResetGeneration,
|
||||
recordedFollowTrajectory,
|
||||
recordedPerceptionLayers.enabled,
|
||||
recordedPerceptionLayers.detections2d,
|
||||
recordedPerceptionLayers.segmentation,
|
||||
recordedPerceptionLayers.cuboids3d,
|
||||
sceneSettings?.accumulationSeconds,
|
||||
sceneSettings?.customColor,
|
||||
sceneSettings?.colorMode,
|
||||
sceneSettings?.palette,
|
||||
sceneSettings?.pointSize,
|
||||
sceneSettings?.showPoints,
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
type RerunSelection,
|
||||
type RerunViewportStatus,
|
||||
type RecordedPerceptionLoadState,
|
||||
type RecordedPointColorLoadState,
|
||||
} from "../components/RerunViewport";
|
||||
import {
|
||||
capabilityStatusLabel,
|
||||
@@ -308,6 +309,7 @@ function SpatialWorkspace({
|
||||
const [playbackState, setPlaybackState] = useState<RerunPlaybackState | null>(null);
|
||||
const [playbackController, setPlaybackController] = useState<RerunPlaybackController | null>(null);
|
||||
const [recordedViewResetGeneration, setRecordedViewResetGeneration] = useState<0 | 1>(0);
|
||||
const [followRecordedTrajectory, setFollowRecordedTrajectory] = useState(false);
|
||||
const [perceptionLoad, setPerceptionLoad] = useState<RecordedPerceptionLoadState>({
|
||||
phase: "idle",
|
||||
receivedBytes: 0,
|
||||
@@ -316,6 +318,13 @@ function SpatialWorkspace({
|
||||
message: "",
|
||||
});
|
||||
const [perceptionRetryGeneration, setPerceptionRetryGeneration] = useState(0);
|
||||
const [pointColorLoad, setPointColorLoad] = useState<RecordedPointColorLoadState>({
|
||||
phase: "idle",
|
||||
receivedBytes: 0,
|
||||
totalBytes: null,
|
||||
progress: null,
|
||||
message: "",
|
||||
});
|
||||
const [showDetections2d, setShowDetections2d] = useState(false);
|
||||
const [showSegmentation, setShowSegmentation] = useState(false);
|
||||
const [showCuboids3d, setShowCuboids3d] = useState(false);
|
||||
@@ -437,6 +446,9 @@ function SpatialWorkspace({
|
||||
setShowCuboids3d(false);
|
||||
}
|
||||
}, []);
|
||||
const onPointColorLoadChange = useCallback((next: RecordedPointColorLoadState) => {
|
||||
setPointColorLoad(next);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setPerceptionLoad({
|
||||
@@ -451,6 +463,14 @@ function SpatialWorkspace({
|
||||
setShowSegmentation(false);
|
||||
setShowCuboids3d(false);
|
||||
setRecordedViewResetGeneration(0);
|
||||
setFollowRecordedTrajectory(false);
|
||||
setPointColorLoad({
|
||||
phase: "idle",
|
||||
receivedBytes: 0,
|
||||
totalBytes: null,
|
||||
progress: null,
|
||||
message: "",
|
||||
});
|
||||
}, [recordedSource, sourceUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -470,6 +490,14 @@ function SpatialWorkspace({
|
||||
setShowDetections2d(false);
|
||||
setShowSegmentation(false);
|
||||
setShowCuboids3d(false);
|
||||
setFollowRecordedTrajectory(false);
|
||||
setPointColorLoad({
|
||||
phase: "idle",
|
||||
receivedBytes: 0,
|
||||
totalBytes: null,
|
||||
progress: null,
|
||||
message: "",
|
||||
});
|
||||
}, [pointCloudVisible, sourceUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -586,6 +614,29 @@ function SpatialWorkspace({
|
||||
Повторить AI
|
||||
</button>
|
||||
) : null}
|
||||
{recordedSource && pointColorLoad.phase === "loading" ? (
|
||||
<div className="spatial-toolbar__perception-status" role="status">
|
||||
<span className="busy-indicator" aria-hidden="true" />
|
||||
<span>{pointColorLoad.message}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{recordedSource && pointColorLoad.phase === "error" ? (
|
||||
<div className="spatial-toolbar__perception-status" role="status">
|
||||
<span>{pointColorLoad.message}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{recordedSource && presentedViewerStatus === "ready" ? (
|
||||
<Button
|
||||
size="compact"
|
||||
variant={followRecordedTrajectory ? "primary" : "secondary"}
|
||||
icon={<Icon name="target" />}
|
||||
aria-pressed={followRecordedTrajectory}
|
||||
title="Привязать orbital-пивот к текущему положению рига"
|
||||
onClick={() => setFollowRecordedTrajectory((current) => !current)}
|
||||
>
|
||||
Следовать
|
||||
</Button>
|
||||
) : null}
|
||||
{recordedSource && presentedViewerStatus === "ready" ? (
|
||||
<Button
|
||||
size="compact"
|
||||
@@ -630,6 +681,7 @@ function SpatialWorkspace({
|
||||
initialPlaybackStartSeconds={initialRecordedPlaybackStartSeconds}
|
||||
sceneSettings={sceneSettings}
|
||||
recordedViewResetGeneration={recordedViewResetGeneration}
|
||||
recordedFollowTrajectory={followRecordedTrajectory}
|
||||
recordedPerceptionLayers={{
|
||||
enabled:
|
||||
recordedPerceptionSupported &&
|
||||
@@ -642,6 +694,7 @@ function SpatialWorkspace({
|
||||
recordedPerceptionRetryGeneration={perceptionRetryGeneration}
|
||||
lockPerceptionCameraInteraction={unifiedPerception}
|
||||
onPerceptionLoadChange={onPerceptionLoadChange}
|
||||
onPointColorLoadChange={onPointColorLoadChange}
|
||||
onStatusChange={onStatusChange}
|
||||
onSelectionChange={onSelectionChange}
|
||||
onPlaybackChange={onPlaybackChange}
|
||||
|
||||
Reference in New Issue
Block a user