Author SHA1 Message Date
DCCONSTRUCTIONS 81fdf6904a refactor(lab): объединить RAV004 в единый Rerun replay 2026-08-30 16:40:12 +03:00
DCCONSTRUCTIONS 9c5259dbc9 fix(lab): запускать RAV004 с первого кадра 2026-08-30 14:33:17 +03:00
DCCONSTRUCTIONS 35daf73d5c fix(lab): сжать и адресовать RAV004 overlay 2026-08-30 14:05:32 +03:00
DCCONSTRUCTIONS 07453142c2 fix(lab): стабилизировать нативный RAV004 replay 2026-08-30 13:28:33 +03:00
DCCONSTRUCTIONS f5ee42751d refactor(lab): перевести RAV004 на канонический Rerun pipeline 2026-08-30 12:59:16 +03:00
DCCONSTRUCTIONS e9ffb829c9 docs(sim): зафиксировать контур AI-улучшения гауссов 2026-08-30 11:57:49 +03:00
DCCONSTRUCTIONS 757d368c86 fix(lab): seal RAV004 spatial replay transport 2026-08-30 10:42:41 +03:00
DCCONSTRUCTIONS f1cbe0061a refactor(lab): restore canonical RAV004 replay 2026-08-30 01:22:44 +03:00
DCCONSTRUCTIONS 74da6437e9 refactor(lab): canonicalize recorded spatial replay 2026-08-29 23:38:17 +03:00
DCCONSTRUCTIONS bd2892140f fix(lab): enforce canonical replay runtime 2026-08-29 22:42:30 +03:00
DCCONSTRUCTIONS 525ab74168 fix(lab): restore canonical RAV004 replay 2026-08-29 21:08:49 +03:00
DCCONSTRUCTIONS c30d77572e fix(lab): restore canonical vegetation evidence layers 2026-08-29 20:33:52 +03:00
DCCONSTRUCTIONS 5179e93f4a fix(lab): restore canonical spatial replay 2026-08-29 19:52:06 +03:00
DCCONSTRUCTIONS c06b709fd7 refactor(viewer): profile rerun loading by playback stage 2026-08-29 17:46:07 +03:00
DCCONSTRUCTIONS 856b61be99 fix(simulation): standardize streamed rendering and UGV proxies 2026-08-29 17:39:40 +03:00
DCCONSTRUCTIONS 76694cc869 fix(simulation): reserve disk space before artifact import 2026-08-29 16:18:56 +03:00
DCCONSTRUCTIONS e6df8cb264 fix(simulation): retry temporary provider proxy failures 2026-08-29 16:01:28 +03:00
DCCONSTRUCTIONS 4b487e367f fix(simulation): retry builds across worker tunnel outages 2026-08-29 14:37:27 +03:00
DCCONSTRUCTIONS d7d9485724 fix(lab): overlay full route semantic masks 2026-08-29 14:19:32 +03:00
DCCONSTRUCTIONS 0a0cde7b0e fix(lab): keep full route result indexable 2026-08-29 13:54:40 +03:00
DCCONSTRUCTIONS 9dec37a8a9 feat(lab): seal full RAVNOVES004TREE semantic review 2026-08-29 13:37:57 +03:00
DCCONSTRUCTIONS 24b65926fe fix(simulation): hold ugv on sloped collision meshes 2026-08-29 12:41:28 +03:00
DCCONSTRUCTIONS 57dacc5a04 fix(simulation): model parked UGV tyre grip 2026-08-29 12:03:33 +03:00
DCCONSTRUCTIONS d9ec8c9cef fix(simulation): tune physical UGV braking 2026-08-29 11:26:01 +03:00
79 changed files with 8576 additions and 1524 deletions
+4 -5
View File
@@ -7,7 +7,6 @@
"": {
"name": "@nodedc/mission-core-control-station",
"version": "0.1.0",
"hasInstallScript": true,
"dependencies": {
"@noble/hashes": "^2.2.0",
"@nodedc/map-cesium-react": "file:../../../NODEDC_DESIGN_GUIDELINE/packages/map-cesium-react",
@@ -15,7 +14,7 @@
"@nodedc/tokens": "file:../../../NODEDC_DESIGN_GUIDELINE/packages/tokens",
"@nodedc/ui-core": "file:../../../NODEDC_DESIGN_GUIDELINE/packages/ui-core",
"@nodedc/ui-react": "file:../../../NODEDC_DESIGN_GUIDELINE/packages/ui-react",
"@rerun-io/web-viewer": "0.34.1",
"@rerun-io/web-viewer": "0.36.3",
"meshoptimizer": "1.1.1",
"playcanvas": "2.21.4",
"react": "^19.1.0",
@@ -900,9 +899,9 @@
"link": true
},
"node_modules/@rerun-io/web-viewer": {
"version": "0.34.1",
"resolved": "https://registry.npmjs.org/@rerun-io/web-viewer/-/web-viewer-0.34.1.tgz",
"integrity": "sha512-2Oq9Mw3qOs765XArGq4e/0pAIksPFKlAqkgo+PDsRkPd77/KdnQhb835suRmYQ6tuqlbPeVCFhlNIXT0+TjmTg==",
"version": "0.36.3",
"resolved": "https://registry.npmjs.org/@rerun-io/web-viewer/-/web-viewer-0.36.3.tgz",
"integrity": "sha512-LMGnsxRmY5UwiGras2dZrMnEYkow5Xr4v+1hAUSspXWPPiilMqoz9G77jo8Ps/deAaX81TnOq123DFO8iX/Ulw==",
"license": "MIT"
},
"node_modules/@rolldown/pluginutils": {
+1 -2
View File
@@ -4,7 +4,6 @@
"private": true,
"type": "module",
"scripts": {
"postinstall": "node scripts/patch-rerun-web-viewer.mjs",
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
@@ -18,7 +17,7 @@
"@nodedc/tokens": "file:../../../NODEDC_DESIGN_GUIDELINE/packages/tokens",
"@nodedc/ui-core": "file:../../../NODEDC_DESIGN_GUIDELINE/packages/ui-core",
"@nodedc/ui-react": "file:../../../NODEDC_DESIGN_GUIDELINE/packages/ui-react",
"@rerun-io/web-viewer": "0.34.1",
"@rerun-io/web-viewer": "0.36.3",
"meshoptimizer": "1.1.1",
"playcanvas": "2.21.4",
"react": "^19.1.0",
@@ -29,7 +29,9 @@ export type RecordedMediaPresentationState = "loading" | "ready" | "waiting" | "
export const RECORDED_MEDIA_DURATION_TOLERANCE_SECONDS = 1;
const RECORDED_MEDIA_SOURCE_OPEN_TIMEOUT_MS = 10_000;
const RECORDED_MEDIA_TARGET_TIMEOUT_MS = 10_000;
// A valid local fragment becomes decoder-ready well below one second. Keeping
// a damaged GOP on screen for ten seconds only delays the keyframe recovery.
const RECORDED_MEDIA_TARGET_TIMEOUT_MS = 2_500;
const RECORDED_MEDIA_FRAGMENT_TIMEOUT_MS = 15_000;
const RECORDED_MEDIA_REQUIRED_AHEAD_SEGMENTS = 12;
const RECORDED_MEDIA_SEGMENTS_AHEAD = 36;
@@ -72,6 +74,39 @@ export function recordedMediaDecodeStartSequence(
return selected;
}
export function nextRecordedMediaRandomAccessSequence(
randomAccessSequences: readonly number[],
failedSequence: number,
): number | null {
if (!Number.isInteger(failedSequence) || failedSequence < 1) return null;
for (const sequence of randomAccessSequences) {
if (!Number.isInteger(sequence) || sequence < 1) return null;
if (sequence > failedSequence) return sequence;
}
return null;
}
export function recordedMediaRecoveryTargetSequence(
requestedSequence: number | null,
failedSequence: number | null,
recoverySequence: number | null,
): number | null {
if (requestedSequence === null) return null;
if (
!Number.isInteger(requestedSequence)
|| requestedSequence < 1
|| failedSequence === null
|| recoverySequence === null
|| !Number.isInteger(failedSequence)
|| !Number.isInteger(recoverySequence)
|| failedSequence < 1
|| recoverySequence <= failedSequence
) return requestedSequence;
return requestedSequence >= failedSequence && requestedSequence < recoverySequence
? recoverySequence
: requestedSequence;
}
export function recordedMediaSegmentAppendOrder(
appended: ReadonlySet<number>,
decodeStartSequence: number,
@@ -90,6 +125,32 @@ export function recordedMediaSegmentAppendOrder(
return missing;
}
/** Resolve a source-clock timestamp to the first fMP4 fragment covering it. */
export function recordedMediaSegmentSequenceAtTime(
segmentEndTimesSeconds: readonly number[],
epochStartSeconds: number,
currentSeconds: number,
): number | null {
if (
!segmentEndTimesSeconds.length ||
!Number.isFinite(epochStartSeconds) ||
!Number.isFinite(currentSeconds)
) return null;
const localSeconds = Math.max(0, currentSeconds - epochStartSeconds);
let left = 0;
let right = segmentEndTimesSeconds.length - 1;
while (left < right) {
const middle = Math.floor((left + right) / 2);
const endSeconds = segmentEndTimesSeconds[middle];
if (!Number.isFinite(endSeconds) || endSeconds <= 0) return null;
if (endSeconds + 0.001 >= localSeconds) right = middle;
else left = middle + 1;
}
const finalEndSeconds = segmentEndTimesSeconds[left];
if (!Number.isFinite(finalEndSeconds) || finalEndSeconds + 0.001 < localSeconds) return null;
return left + 1;
}
export function recordedMediaCanRollTarget(
previousSequence: number,
nextSequence: number,
@@ -121,6 +182,27 @@ function recordedMediaTimeRangesContain(
return false;
}
export function recordedMediaTimestampStallRecoveryTarget(
currentSeconds: number,
bufferedRanges: readonly (readonly [number, number])[],
skipSeconds = 0.18,
): number | null {
if (!Number.isFinite(currentSeconds) || !Number.isFinite(skipSeconds) || skipSeconds <= 0) {
return null;
}
for (const [startSeconds, endSeconds] of bufferedRanges) {
if (
!Number.isFinite(startSeconds)
|| !Number.isFinite(endSeconds)
|| currentSeconds < startSeconds - 0.05
|| currentSeconds > endSeconds
) continue;
const targetSeconds = Math.min(currentSeconds + skipSeconds, endSeconds - 0.05);
return targetSeconds >= currentSeconds + 0.04 ? targetSeconds : null;
}
return null;
}
export function recordedMediaPresentationState(
state: "loading" | "ready" | "error",
readyGeneration: string | null,
@@ -151,6 +233,24 @@ export function selectRecordedMediaEpoch(
return selected && currentSeconds <= selected.timelineEndSeconds ? selected : null;
}
/**
* Keep admission bounded even when the shared Rerun clock is currently before,
* between, or after camera epochs. Presentation still reports `waiting`; this
* selector only chooses the nearest epoch whose first/last fragment can prove
* that the camera transport is usable without downloading the whole MP4.
*/
export function selectRecordedMediaPreparationEpoch(
epochs: readonly ObservationRecordedMediaEpoch[],
currentSeconds: number,
): ObservationRecordedMediaEpoch | null {
if (!epochs.length || !Number.isFinite(currentSeconds)) return null;
const active = selectRecordedMediaEpoch(epochs, currentSeconds);
if (active) return active;
return epochs.find((epoch) => epoch.timelineStartSeconds > currentSeconds)
?? epochs.at(-1)
?? null;
}
export function recordedMediaSeekableCoverage(
durationSeconds: number,
seekableEndSeconds: number,
@@ -231,29 +331,14 @@ export async function fetchRecordedMediaArchive(
return { manifest, byteLength: totalBytes };
}
function videoHasSeekableArchive(
function waitForRecordedVideoInitialFrame(
video: HTMLVideoElement,
declaredDurationSeconds: number,
): boolean {
if (video.readyState < 1 || video.seekable.length < 1) return false;
return recordedMediaSeekableCoverage(
video.duration,
video.seekable.end(video.seekable.length - 1),
declaredDurationSeconds,
RECORDED_MEDIA_DURATION_TOLERANCE_SECONDS,
video.seekable.start(0),
);
}
function waitForSeekableArchive(
video: HTMLVideoElement,
declaredDurationSeconds: number,
signal: AbortSignal,
): Promise<void> {
if (signal.aborted) return Promise.reject(new DOMException("Aborted", "AbortError"));
if (videoHasSeekableArchive(video, declaredDurationSeconds)) return Promise.resolve();
if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) return Promise.resolve();
return new Promise((resolve, reject) => {
const events = ["loadedmetadata", "durationchange", "progress", "canplay"] as const;
const events = ["loadeddata", "canplay", "progress"] as const;
let stallTimer: ReturnType<typeof globalThis.setTimeout> | undefined;
const armStallTimer = () => {
if (stallTimer !== undefined) globalThis.clearTimeout(stallTimer);
@@ -270,7 +355,7 @@ function waitForSeekableArchive(
};
const onProgress = () => {
armStallTimer();
if (!videoHasSeekableArchive(video, declaredDurationSeconds)) return;
if (video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) return;
cleanup();
resolve();
};
@@ -313,11 +398,7 @@ async function mountRecordedEpochStream(
};
video.load();
try {
await waitForSeekableArchive(
video,
descriptor.timelineEndSeconds - descriptor.timelineStartSeconds,
signal,
);
await waitForRecordedVideoInitialFrame(video, signal);
return cleanup;
} catch (error) {
cleanup();
@@ -336,6 +417,11 @@ interface RecordedSegmentTarget {
resetAttempts: number;
}
interface RecordedSegmentRecovery {
readonly failedSequence: number;
readonly recoverySequence: number;
}
interface RecordedSegmentStreamRuntime {
readonly generation: string;
readonly mediaSource: MediaSource;
@@ -716,6 +802,9 @@ export function RecordedFmp4Player({
onAdmissionChange,
onPlaybackChange,
onPlayingRejected,
playbackAuthority = "media",
playbackTransport = "segmented",
recoverTimestampStalls = false,
}: {
source: ObservationSourceDescriptor;
playback?: RecordedObservationPlayback | null;
@@ -728,6 +817,9 @@ export function RecordedFmp4Player({
onAdmissionChange?: (state: RecordedCameraAdmissionState) => void;
onPlaybackChange?: (playback: RecordedObservationPlayback) => void;
onPlayingRejected?: () => void;
playbackAuthority?: "media" | "host";
playbackTransport?: "segmented" | "epoch-stream";
recoverTimestampStalls?: boolean;
}) {
const videoRef = useRef<HTMLVideoElement>(null);
const onAdmissionChangeRef = useRef(onAdmissionChange);
@@ -768,12 +860,16 @@ export function RecordedFmp4Player({
);
const [archive, setArchive] = useState<RecordedMediaArchive | null>(null);
const [state, setState] = useState<"loading" | "ready" | "error">("loading");
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [readyGeneration, setReadyGeneration] = useState<string | null>(null);
const [bufferRevision, setBufferRevision] = useState(0);
const [segmentRecoveryGeneration, setSegmentRecoveryGeneration] = useState(0);
const [segmentRecovery, setSegmentRecovery] = useState<RecordedSegmentRecovery | null>(null);
const segmentedRuntimeRef = useRef<RecordedSegmentStreamRuntime | null>(null);
const [segmentedRuntimeGeneration, setSegmentedRuntimeGeneration] = useState<string | null>(null);
const targetRevisionRef = useRef(0);
const targetReadyAbortRef = useRef<AbortController | null>(null);
const lastSegmentRecoveryRef = useRef<string | null>(null);
const playAttemptRevisionRef = useRef(0);
const currentSeconds = playback?.currentSeconds ?? contract?.timelineStartSeconds ?? 0;
const playbackPlayingRef = useRef(Boolean(playback?.playing));
@@ -781,25 +877,63 @@ export function RecordedFmp4Player({
const playbackRate = playback?.rate && Number.isFinite(playback.rate)
? Math.min(4, Math.max(0.25, playback.rate))
: 1;
const epoch = useMemo(
const playbackRateRef = useRef(playbackRate);
playbackRateRef.current = playbackRate;
const presentationEpoch = useMemo(
() => selectRecordedMediaEpoch(archive?.manifest.epochs ?? [], currentSeconds),
[archive?.manifest.epochs, currentSeconds],
);
const epoch = useMemo(
() => selectRecordedMediaPreparationEpoch(
archive?.manifest.epochs ?? [],
currentSeconds,
),
[archive?.manifest.epochs, currentSeconds],
);
const segmentClockSeconds = epoch
? Math.min(
Math.max(currentSeconds, epoch.timelineStartSeconds),
epoch.timelineEndSeconds,
)
: currentSeconds;
const effectiveSegmentCount = segmentCount ?? epoch?.segmentCount ?? null;
const requestedSegmentSequence = segmentSequence ?? (epoch
? recordedMediaSegmentSequenceAtTime(
epoch.segmentEndTimesSeconds,
epoch.timelineStartSeconds,
segmentClockSeconds,
)
: null);
const effectiveSegmentSequence = recordedMediaRecoveryTargetSequence(
requestedSegmentSequence,
segmentRecovery?.failedSequence ?? null,
segmentRecovery?.recoverySequence ?? null,
);
const holdingForSegmentRecovery = Boolean(
segmentRecovery
&& requestedSegmentSequence !== null
&& effectiveSegmentSequence !== requestedSegmentSequence,
);
const segmented = Boolean(
segmentCount !== null
&& Number.isInteger(segmentCount)
&& segmentCount >= 1
playbackTransport === "segmented"
&& requestedSegmentSequence !== null
&& Number.isInteger(requestedSegmentSequence)
&& requestedSegmentSequence >= 1
&&
effectiveSegmentCount !== null
&& Number.isInteger(effectiveSegmentCount)
&& effectiveSegmentCount >= 1
&& typeof MediaSource !== "undefined"
&& epoch
&& epoch.segmentCount === segmentCount
&& epoch.segmentCount === effectiveSegmentCount
&& epoch.randomAccessSequences.length > 0
&& epoch.segmentEndTimesSeconds.length === segmentCount
&& epoch.segmentEndTimesSeconds.length === effectiveSegmentCount
&& MediaSource.isTypeSupported(epoch.mediaType),
);
const directPlaybackSeconds = segmented ? null : currentSeconds;
const waitingForEpoch = Boolean(archive && !epoch);
const selectedGeneration = contract && epoch
? `${contract.manifestGenerationSha256}:${epoch.ordinal}:${epoch.timelineStartSeconds}:${epoch.timelineEndSeconds}`
const waitingForEpoch = Boolean(archive && !presentationEpoch);
const selectedGeneration = contract && presentationEpoch
? `${contract.manifestGenerationSha256}:${presentationEpoch.ordinal}:${presentationEpoch.timelineStartSeconds}:${presentationEpoch.timelineEndSeconds}`
: null;
const visualState = recordedMediaPresentationState(
state,
@@ -813,6 +947,7 @@ export function RecordedFmp4Player({
if (!contract) {
setArchive(null);
setReadyGeneration(null);
setErrorMessage("Некорректный descriptor записанной камеры.");
setState("error");
reportAdmission({
phase: "error",
@@ -825,6 +960,9 @@ export function RecordedFmp4Player({
const abort = new AbortController();
setArchive(null);
setReadyGeneration(null);
setSegmentRecovery(null);
lastSegmentRecoveryRef.current = null;
setErrorMessage(null);
setState("loading");
reportAdmission({
phase: "loading",
@@ -842,6 +980,7 @@ export function RecordedFmp4Player({
}
setArchive(null);
setReadyGeneration(null);
setErrorMessage("Архив записанной камеры не прошёл проверку.");
setState("error");
reportAdmission({
phase: "error",
@@ -853,43 +992,14 @@ export function RecordedFmp4Player({
}, [admissionKey, contract, prepare]);
useEffect(() => {
if (!archive || !contract || !prepare || segmented) return;
const abort = new AbortController();
let disposed = false;
void (async () => {
for (const candidate of archive.manifest.epochs) {
const probe = document.createElement("video");
probe.muted = true;
probe.playsInline = true;
const cleanup = await mountRecordedEpochStream(probe, candidate, abort.signal);
cleanup();
if (disposed || abort.signal.aborted) return;
}
if (disposed || abort.signal.aborted) return;
reportAdmission({
phase: "ready",
byteLength: archive.byteLength,
message: null,
});
})().catch((error: unknown) => {
if (
disposed ||
abort.signal.aborted ||
(error instanceof DOMException && error.name === "AbortError")
) return;
setReadyGeneration(null);
setState("error");
reportAdmission({
phase: "error",
byteLength: archive.byteLength,
message: "Не все codec epoch записанной камеры декодируются и доступны для seek.",
});
});
return () => {
disposed = true;
abort.abort();
};
}, [admissionKey, archive, contract, prepare, segmented]);
if (!segmentRecovery || requestedSegmentSequence === null) return;
if (
requestedSegmentSequence >= segmentRecovery.failedSequence
&& requestedSegmentSequence < segmentRecovery.recoverySequence
) return;
lastSegmentRecoveryRef.current = null;
setSegmentRecovery(null);
}, [requestedSegmentSequence, segmentRecovery]);
useEffect(() => {
const video = videoRef.current;
@@ -910,6 +1020,7 @@ export function RecordedFmp4Player({
setReadyGeneration(null);
setSegmentedRuntimeGeneration(null);
setErrorMessage(null);
setState("loading");
video.pause();
const sourceOpened = waitForMediaSourceOpen(mediaSource, abort.signal);
@@ -957,6 +1068,7 @@ export function RecordedFmp4Player({
return;
}
setReadyGeneration(null);
setErrorMessage("Покадровый буфер записанной камеры не открылся.");
setState("error");
reportAdmission({
phase: "error",
@@ -982,7 +1094,14 @@ export function RecordedFmp4Player({
}
URL.revokeObjectURL(objectUrl);
};
}, [archive, contract, epoch, segmentCount, segmented]);
}, [
archive,
contract,
effectiveSegmentCount,
epoch,
segmented,
segmentRecoveryGeneration,
]);
useEffect(() => {
const runtime = segmentedRuntimeRef.current;
@@ -993,18 +1112,19 @@ export function RecordedFmp4Player({
|| !archive
|| !segmented
|| segmentedRuntimeGeneration !== runtime.generation
|| segmentSequence === null
|| !Number.isInteger(segmentSequence)
|| segmentSequence < 1
|| segmentSequence > runtime.segmentCount
|| effectiveSegmentSequence === null
|| !Number.isInteger(effectiveSegmentSequence)
|| effectiveSegmentSequence < 1
|| effectiveSegmentSequence > runtime.segmentCount
) return;
const archiveByteLength = archive.byteLength;
const decodeStart = recordedMediaDecodeStartSequence(
runtime.randomAccessSequences,
segmentSequence,
effectiveSegmentSequence,
);
if (decodeStart === null) {
setReadyGeneration(null);
setErrorMessage("Для кадра записанной камеры нет random-access фрагмента.");
setState("error");
reportAdmission({
phase: "error",
@@ -1015,10 +1135,11 @@ export function RecordedFmp4Player({
}
const targetSeconds = recordedSegmentStartSeconds(
runtime.segmentEndTimesSeconds,
segmentSequence,
effectiveSegmentSequence,
);
if (targetSeconds === null) {
setReadyGeneration(null);
setErrorMessage("Для кадра записанной камеры нет точной media timestamp.");
setState("error");
reportAdmission({
phase: "error",
@@ -1030,15 +1151,15 @@ export function RecordedFmp4Player({
const previousTarget = runtime.target;
const readyEnd = Math.min(
runtime.segmentCount,
segmentSequence + RECORDED_MEDIA_REQUIRED_AHEAD_SEGMENTS,
effectiveSegmentSequence + RECORDED_MEDIA_REQUIRED_AHEAD_SEGMENTS,
);
const desiredEnd = Math.min(
runtime.segmentCount,
segmentSequence + RECORDED_MEDIA_SEGMENTS_AHEAD,
effectiveSegmentSequence + RECORDED_MEDIA_SEGMENTS_AHEAD,
);
const candidateTarget: RecordedSegmentTarget = {
revision: previousTarget?.revision ?? 0,
sequence: segmentSequence,
sequence: effectiveSegmentSequence,
decodeStart,
readyEnd,
desiredEnd,
@@ -1046,6 +1167,24 @@ export function RecordedFmp4Player({
forceReset: false,
resetAttempts: 0,
};
const recoverFromSegmentFailure = (failedSequence: number): boolean => {
const recoverySequence = nextRecordedMediaRandomAccessSequence(
runtime.randomAccessSequences,
failedSequence,
);
if (recoverySequence === null) return false;
const recoveryToken = `${runtime.generation}:${failedSequence}:${recoverySequence}`;
if (lastSegmentRecoveryRef.current === recoveryToken) return true;
lastSegmentRecoveryRef.current = recoveryToken;
setReadyGeneration(null);
setSegmentRecovery({ failedSequence, recoverySequence });
setErrorMessage(
`Восстанавливаем камеру с ключевого кадра ${recoverySequence}.`,
);
setState("loading");
setSegmentRecoveryGeneration((generation) => generation + 1);
return true;
};
const rollingTarget = Boolean(previousTarget && recordedMediaCanRollTarget(
previousTarget.sequence,
candidateTarget.sequence,
@@ -1059,21 +1198,62 @@ export function RecordedFmp4Player({
|| runtime.abort.signal.aborted
|| (error instanceof DOMException && error.name === "AbortError")
) return;
const failedSequence = runtime.target?.sequence ?? effectiveSegmentSequence;
if (failedSequence !== null && recoverFromSegmentFailure(failedSequence)) return;
const detail = error instanceof Error && error.message
? `: ${error.message}`
: ".";
const message = `Покадровый фрагмент записанной камеры недоступен${detail}`;
setReadyGeneration(null);
setErrorMessage(message);
setState("error");
reportAdmission({
phase: "error",
byteLength: archiveByteLength,
message: "Покадровый фрагмент записанной камеры недоступен.",
message,
});
};
const resumePlaybackIfRequested = async (revision: number) => {
if (
runtime.disposed
|| segmentedRuntimeRef.current !== runtime
|| runtime.target?.revision !== revision
|| !playbackPlayingRef.current
) return;
video.playbackRate = playbackRateRef.current;
try {
await video.play();
} catch {
if (
runtime.disposed
|| segmentedRuntimeRef.current !== runtime
|| runtime.target?.revision !== revision
) return;
// Canonical recorded LABs run one host-owned clock for camera and
// spatial evidence. A transient MSE play() rejection (commonly a
// pause/reset race while the next fragment is admitted) must stay a
// decoder concern: the rolling target will retry and catch up.
if (playbackAuthority === "host") return;
onPlayingRejectedRef.current?.();
setReadyGeneration(null);
setErrorMessage("Запуск записанной камеры отклонён браузером.");
setState("error");
reportAdmission({
phase: "error",
byteLength: archiveByteLength,
message: "Запуск записанной камеры отклонён браузером.",
});
}
};
if (rollingTarget && previousTarget) {
runtime.target = {
...candidateTarget,
revision: previousTarget.revision,
};
runtime.onTargetBuffered = null;
void pumpRecordedSegmentWindow(runtime).catch(reportPumpError);
void pumpRecordedSegmentWindow(runtime)
.then(() => resumePlaybackIfRequested(previousTarget.revision))
.catch(reportPumpError);
return;
}
@@ -1128,18 +1308,22 @@ export function RecordedFmp4Player({
setBufferRevision((revision) => revision + 1);
runtime.hasPresentedFrame = true;
setReadyGeneration(runtime.generation);
setErrorMessage(null);
setState("ready");
reportAdmission({
phase: "ready",
byteLength: archiveByteLength,
message: null,
});
await resumePlaybackIfRequested(bufferedTarget.revision);
} catch (error) {
if (
targetReadyAbort.signal.aborted
|| (error instanceof DOMException && error.name === "AbortError")
) return;
if (recoverFromSegmentFailure(bufferedTarget.sequence)) return;
setReadyGeneration(null);
setErrorMessage("Кадр записанной камеры не стал decoder-ready.");
setState("error");
reportAdmission({
phase: "error",
@@ -1160,7 +1344,13 @@ export function RecordedFmp4Player({
if (targetReadyAbortRef.current === targetReadyAbort) targetReadyAbortRef.current = null;
if (runtime.onTargetBuffered === markBuffered) runtime.onTargetBuffered = null;
};
}, [archive?.byteLength, segmentSequence, segmented, segmentedRuntimeGeneration]);
}, [
archive?.byteLength,
effectiveSegmentSequence,
playbackAuthority,
segmented,
segmentedRuntimeGeneration,
]);
useEffect(() => {
const video = videoRef.current;
@@ -1170,6 +1360,7 @@ export function RecordedFmp4Player({
? `${contract.manifestGenerationSha256}:${epochDescriptor.ordinal}:${epochDescriptor.timelineStartSeconds}:${epochDescriptor.timelineEndSeconds}`
: null;
setReadyGeneration(null);
setErrorMessage(null);
setState("loading");
const abort = new AbortController();
let disposed = false;
@@ -1185,7 +1376,13 @@ export function RecordedFmp4Player({
}
setBufferRevision((revision) => revision + 1);
setReadyGeneration(generation);
setErrorMessage(null);
setState("ready");
reportAdmission({
phase: "ready",
byteLength: archive?.byteLength ?? null,
message: null,
});
} catch (error) {
if (
disposed ||
@@ -1195,11 +1392,12 @@ export function RecordedFmp4Player({
return;
}
setReadyGeneration(null);
setErrorMessage("Записанная камера не открыла первый декодируемый кадр.");
setState("error");
reportAdmission({
phase: "error",
byteLength: archive?.byteLength ?? null,
message: "Записанная камера не стала seekable.",
message: "Записанная камера не открыла первый декодируемый кадр.",
});
}
};
@@ -1234,6 +1432,7 @@ export function RecordedFmp4Player({
video.currentTime = target;
} catch {
setReadyGeneration(null);
setErrorMessage("Seek записанной камеры завершился ошибкой.");
setState("error");
reportAdmission({
phase: "error",
@@ -1244,11 +1443,13 @@ export function RecordedFmp4Player({
}
}
video.playbackRate = playbackRate;
if (playback?.playing) {
if (playback?.playing && !holdingForSegmentRecovery) {
void video.play().catch(() => {
if (playAttemptRevisionRef.current !== playAttemptRevision) return;
if (playbackAuthority === "host") return;
onPlayingRejectedRef.current?.();
setReadyGeneration(null);
setErrorMessage("Запуск записанной камеры отклонён браузером.");
setState("error");
reportAdmission({
phase: "error",
@@ -1264,12 +1465,95 @@ export function RecordedFmp4Player({
bufferRevision,
directPlaybackSeconds,
epoch,
holdingForSegmentRecovery,
playback?.playing,
playbackAuthority,
playbackRate,
segmented,
visualState,
]);
useEffect(() => {
const video = videoRef.current;
if (!video || !recoverTimestampStalls || !playback?.playing || visualState !== "ready") {
return;
}
let lastSeconds = video.currentTime;
let lastProgressAtMs = performance.now();
const interval = window.setInterval(() => {
if (
!playbackPlayingRef.current
|| video.paused
|| video.ended
|| video.seeking
|| video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA
) {
lastSeconds = video.currentTime;
lastProgressAtMs = performance.now();
return;
}
const nowMs = performance.now();
if (video.currentTime >= lastSeconds + 0.02) {
lastSeconds = video.currentTime;
lastProgressAtMs = nowMs;
return;
}
if (nowMs - lastProgressAtMs < 1_250) return;
const bufferedRanges = Array.from(
{ length: video.buffered.length },
(_, index) => [video.buffered.start(index), video.buffered.end(index)] as const,
);
const targetSeconds = recordedMediaTimestampStallRecoveryTarget(
video.currentTime,
bufferedRanges,
);
lastProgressAtMs = nowMs;
if (targetSeconds === null) return;
// Field recordings can contain non-monotonic or corrupt H.264 timestamps.
// If decoded time is frozen despite proven buffered media ahead, skip only
// the broken timestamp interval and return authority to the media clock.
video.currentTime = targetSeconds;
lastSeconds = targetSeconds;
}, 250);
return () => window.clearInterval(interval);
}, [playback?.playing, recoverTimestampStalls, visualState]);
useEffect(() => {
const video = videoRef.current;
if (!video || !segmented || !playback?.playing || visualState !== "ready") return;
let cancelled = false;
const resumeIfDecoderReady = () => {
if (cancelled || !playbackPlayingRef.current || !video.paused) return;
const runtime = segmentedRuntimeRef.current;
const target = runtime?.target;
if (
!runtime
|| !target
|| runtime.disposed
|| video.seeking
|| video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA
|| Math.abs(video.currentTime - target.targetSeconds) > 0.25
|| !recordedMediaTimeRangesContain(video.buffered, target.targetSeconds)
) return;
playAttemptRevisionRef.current += 1;
const playAttemptRevision = playAttemptRevisionRef.current;
video.playbackRate = playbackRateRef.current;
void video.play().catch(() => {
if (cancelled || playAttemptRevisionRef.current !== playAttemptRevision) return;
if (playbackAuthority === "host") return;
onPlayingRejectedRef.current?.();
});
};
const queueResume = () => window.queueMicrotask(resumeIfDecoderReady);
const events = ["pause", "canplay", "seeked"] as const;
for (const event of events) video.addEventListener(event, queueResume);
resumeIfDecoderReady();
return () => {
cancelled = true;
for (const event of events) video.removeEventListener(event, queueResume);
};
}, [bufferRevision, playback?.playing, playbackAuthority, segmented, visualState]);
useEffect(() => {
const video = videoRef.current;
if ((!interactive && onPlaybackChange === undefined) || !video || !epoch || visualState !== "ready") return;
@@ -1327,7 +1611,9 @@ export function RecordedFmp4Player({
{visualState === "waiting"
? "Камера на этой позиции ещё не записывалась"
: visualState === "error"
? "Записанное видео недоступно"
? errorMessage ?? "Записанное видео недоступно"
: errorMessage
? errorMessage
: archive
? "Проверяем seek и codec записанного видео…"
: "Читаем manifest записанного видео…"}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,257 @@
import { useCallback, useEffect, useState, type ReactNode } from "react";
import { SegmentedControl, SplitPane, type SplitPaneOrientation } from "@nodedc/ui-react";
import { LaboratoryEvidenceViewer } from "./LaboratoryEvidenceViewer";
export const CANONICAL_RECORDED_LAB_REPLAY_CONTRACT =
"missioncore.canonical-recorded-lab-replay/v1";
export interface CanonicalRecordedLabMode<T extends string> {
value: T;
label: string;
disabled?: boolean;
}
/**
* The interaction contract from the accepted recorded-LAB instrument.
*
* Keeping pane selection, collapse semantics, responsive split orientation,
* expansion and splitter state here prevents individual experiments from
* quietly growing their own replay behaviour. Experiments provide evidence
* layers; they do not reimplement the laboratory shell.
*/
export function useCanonicalRecordedLabReplayState<
TMedia extends string,
TSpatial extends string,
>({
initialMediaMode,
initialSpatialMode,
}: {
initialMediaMode: TMedia;
initialSpatialMode: TSpatial | null;
}) {
const [mediaMode, setMediaMode] = useState<TMedia | null>(initialMediaMode);
const [spatialMode, setSpatialMode] = useState<TSpatial | null>(initialSpatialMode);
const [splitPrimarySize, setSplitPrimarySize] = useState(50);
const [splitOrientation, setSplitOrientation] = useState<SplitPaneOrientation>(() => (
typeof window !== "undefined" && window.matchMedia("(max-width: 900px)").matches
? "horizontal"
: "vertical"
));
const [expanded, setExpanded] = useState(false);
useEffect(() => {
const query = window.matchMedia("(max-width: 900px)");
const update = () => setSplitOrientation(query.matches ? "horizontal" : "vertical");
update();
query.addEventListener("change", update);
return () => query.removeEventListener("change", update);
}, []);
const onMediaModeChange = useCallback((next: TMedia | "none") => {
if (next === "none") return;
setMediaMode((current) => current === next ? null : next);
}, []);
const onSpatialModeChange = useCallback((next: TSpatial | "none") => {
if (next === "none") return;
setSpatialMode((current) => current === next ? null : next);
}, []);
return {
mediaMode,
spatialMode,
splitView: mediaMode !== null && spatialMode !== null,
splitPrimarySize,
splitOrientation,
expanded,
onMediaModeChange,
onSpatialModeChange,
onSplitPrimarySizeChange: setSplitPrimarySize,
onExpandedChange: setExpanded,
};
}
export function CanonicalRecordedLabReplay<
TMedia extends string,
TSpatial extends string,
>({
label,
mediaMode,
mediaModes,
spatialMode,
spatialModes,
expanded,
splitPrimarySize,
splitOrientation,
mediaAriaLabel,
spatialAriaLabel,
mediaLayerControls,
spatialLayerControls,
spatialLeadingControl,
mediaMultiLayer = false,
mediaContent,
spatialContent,
unifiedContent,
emptyMessage,
deckOverlays,
actions,
overlay,
transport,
trailingActions,
onMediaModeChange,
onSpatialModeChange,
onExpandedChange,
onSplitPrimarySizeChange,
}: {
label: string;
mediaMode: TMedia;
mediaModes: readonly CanonicalRecordedLabMode<TMedia>[];
spatialMode: TSpatial;
spatialModes: readonly CanonicalRecordedLabMode<TSpatial>[];
expanded: boolean;
splitPrimarySize: number;
splitOrientation: SplitPaneOrientation;
mediaAriaLabel: string;
spatialAriaLabel: string;
mediaLayerControls?: ReactNode;
spatialLayerControls?: ReactNode;
spatialLeadingControl?: ReactNode;
mediaMultiLayer?: boolean;
mediaContent?: ReactNode;
spatialContent?: ReactNode;
/** One upstream Rerun viewer owns both panes and the shared playback clock. */
unifiedContent?: ReactNode;
emptyMessage: string;
deckOverlays?: ReactNode;
actions?: ReactNode;
overlay?: ReactNode;
transport?: ReactNode;
trailingActions?: ReactNode;
onMediaModeChange: (mode: TMedia) => void;
onSpatialModeChange: (mode: TSpatial) => void;
onExpandedChange: (expanded: boolean) => void;
onSplitPrimarySizeChange: (size: number) => void;
}) {
const splitView = mediaMode !== "none" && spatialMode !== "none";
const mediaModeControls = (
<div className="m4-replay-threat-visual__pane-mode-controls" data-pane-mode="media">
<SegmentedControl
value={mediaMode}
items={[...mediaModes]}
label="Видео и камера"
size="dense"
onChange={onMediaModeChange}
/>
</div>
);
const spatialModeControls = (
<div className="m4-replay-threat-visual__pane-mode-controls" data-pane-mode="spatial">
<SegmentedControl
value={spatialMode}
items={[...spatialModes]}
label="3D и план"
size="dense"
onChange={onSpatialModeChange}
/>
</div>
);
const mediaPane = (
<section
className="m4-replay-threat-visual__pane"
data-pane="media"
aria-label={mediaAriaLabel}
hidden={mediaMode === "none"}
>
{splitView ? (
<div
className="m4-replay-threat-visual__pane-toolbar"
data-pane-toolbar="media"
data-multi-semantic={mediaMultiLayer ? "true" : undefined}
>
{mediaLayerControls}
{mediaModeControls}
</div>
) : null}
{mediaContent}
</section>
);
const spatialPane = spatialMode !== "none" ? (
<section
className="m4-replay-threat-visual__pane"
data-pane="spatial"
aria-label={spatialAriaLabel}
>
{splitView ? (
<div
className="m4-replay-threat-visual__pane-toolbar"
data-pane-toolbar="spatial"
>
{spatialLeadingControl}
<div className="m4-replay-threat-visual__spatial-toolbar-end">
{spatialLayerControls}
{spatialModeControls}
</div>
</div>
) : null}
{spatialContent}
</section>
) : null;
return (
<div
className="l3-visual-audit m4-replay-threat-visual"
data-contract={CANONICAL_RECORDED_LAB_REPLAY_CONTRACT}
>
<LaboratoryEvidenceViewer
label={label}
className="m4-replay-threat-evidence-viewer"
mode={mediaMode}
modes={[...mediaModes]}
secondaryMode={{
value: spatialMode,
modes: [...spatialModes],
label: "3D и план",
onChange: onSpatialModeChange,
}}
expanded={expanded}
onModeChange={onMediaModeChange}
onExpandedChange={onExpandedChange}
modeControlsVisible={!splitView}
actions={actions}
overlay={overlay}
transport={transport}
trailingActions={trailingActions}
>
<div
className="m4-replay-threat-visual__deck"
data-split={splitView ? "true" : undefined}
data-empty={mediaMode === "none" && spatialMode === "none" ? "true" : undefined}
data-native-rerun={unifiedContent ? "true" : undefined}
>
{unifiedContent ? (
<div className="m4-replay-threat-visual__unified-content">
{unifiedContent}
</div>
) : null}
<SplitPane
primary={mediaPane}
secondary={spatialPane ?? <div />}
primarySize={splitView ? splitPrimarySize : mediaMode !== "none" ? 100 : 0}
onPrimarySizeChange={onSplitPrimarySizeChange}
orientation={splitOrientation}
minPrimarySize={splitView ? 24 : 0}
minSecondarySize={splitView ? 24 : 0}
resizable={splitView}
separatorLabel="Изменить размер видео/камеры и 3D/плана"
/>
{mediaMode === "none" && spatialMode === "none" ? (
<div className="l3-visual-audit__state" role="status">
{emptyMessage}
</div>
) : null}
{deckOverlays}
</div>
</LaboratoryEvidenceViewer>
</div>
);
}
@@ -59,6 +59,16 @@ export function laboratoryRecordedClipEndExclusiveNs(
return last.sourceTimeNs + typicalDelta;
}
export function laboratoryRecordedClipClockGate(
pendingSequence: number | null,
observedSequence: number,
): { accept: boolean; pendingSequence: number | null } {
if (pendingSequence !== null && pendingSequence !== observedSequence) {
return { accept: false, pendingSequence };
}
return { accept: true, pendingSequence: null };
}
export function LaboratoryRecordedClipPlayer({
source,
segmentCount,
@@ -94,7 +104,8 @@ export function LaboratoryRecordedClipPlayer({
}) {
const [companionSpatialSize, setCompanionSpatialSize] = useState(69);
const lastEmittedSequenceRef = useRef(sequence);
lastEmittedSequenceRef.current = sequence;
const lastObservedSequenceRef = useRef<number | null>(sequence);
const pendingSequenceRef = useRef<number | null>(null);
const frame = useMemo(
() => frames.find((candidate) => candidate.sequence === sequence) ?? frames[0] ?? null,
[frames, sequence],
@@ -113,23 +124,42 @@ export function LaboratoryRecordedClipPlayer({
if (!continuousPlayback && playing) onPlayingChange(false);
}, [continuousPlayback, onPlayingChange, playing]);
useEffect(() => {
if (lastObservedSequenceRef.current !== sequence) {
pendingSequenceRef.current = sequence;
}
lastEmittedSequenceRef.current = sequence;
}, [sequence]);
const emitSequence = useCallback((nextSequence: number) => {
if (lastEmittedSequenceRef.current === nextSequence) return;
lastEmittedSequenceRef.current = nextSequence;
onSequenceChange(nextSequence);
}, [onSequenceChange]);
const requestSequence = useCallback((nextSequence: number) => {
pendingSequenceRef.current = nextSequence;
emitSequence(nextSequence);
}, [emitSequence]);
const handlePlaybackChange = useCallback((next: RecordedObservationPlayback) => {
const sourceTimeNs = Math.round(next.currentSeconds * 1_000_000_000);
const first = frames[0];
if (!first || endExclusiveNs === null) return;
if (sourceTimeNs >= endExclusiveNs) {
emitSequence(first.sequence);
requestSequence(first.sequence);
return;
}
const nearest = nearestLaboratoryRecordedClipFrame(frames, sourceTimeNs);
if (nearest) emitSequence(nearest.sequence);
}, [emitSequence, endExclusiveNs, frames]);
if (!nearest) return;
lastObservedSequenceRef.current = nearest.sequence;
const gate = laboratoryRecordedClipClockGate(
pendingSequenceRef.current,
nearest.sequence,
);
pendingSequenceRef.current = gate.pendingSequence;
if (gate.accept) emitSequence(nearest.sequence);
}, [emitSequence, endExclusiveNs, frames, requestSequence]);
const timelineStart = frames[0]?.sourceTimeNs ?? 0;
const timelineEnd = frames.at(-1)?.sourceTimeNs ?? timelineStart + 1;
@@ -142,7 +172,7 @@ export function LaboratoryRecordedClipPlayer({
className="laboratory-recorded-clip-player__spatial"
aria-hidden={cameraPresentation === "primary"}
>
{cameraPresentation !== "primary" ? alternativeScene : null}
{alternativeScene}
</div>
);
const cameraPane = (
@@ -202,7 +232,7 @@ export function LaboratoryRecordedClipPlayer({
onPlayingChange={continuousPlayback ? onPlayingChange : undefined}
onSeek={(timeNs) => {
const nearest = nearestLaboratoryRecordedClipFrame(frames, timeNs);
if (nearest) emitSequence(nearest.sequence);
if (nearest) requestSequence(nearest.sequence);
}}
showJumpToEnd={false}
/>
@@ -1,8 +1,11 @@
import { useEffect, useState } from "react";
import {
RecordedFmp4Player,
type RecordedObservationPlayback,
} from "../RecordedFmp4Player";
import type { ObservationSourceDescriptor } from "../../core/runtime/contracts";
import type { RecordedCameraAdmissionState } from "../../core/observation/recordedSessionAdmission";
import {
RecordedEvidenceBoxOverlay,
type RecordedEvidenceBox,
@@ -25,6 +28,7 @@ export function RecordedEvidenceVideoScene({
imageWidth,
imageHeight,
boxes,
overlaySeconds,
semanticOverlay,
pointCloudOverlay,
ariaLabel,
@@ -33,12 +37,17 @@ export function RecordedEvidenceVideoScene({
segmentCount,
onPlaybackChange,
onPlayingRejected,
onAdmissionChange,
playbackAuthority = "media",
playbackTransport = "segmented",
recoverTimestampStalls = false,
}: {
source: ObservationSourceDescriptor;
playback: RecordedObservationPlayback;
imageWidth: number;
imageHeight: number;
boxes: readonly RecordedEvidenceBox[];
overlaySeconds?: number;
semanticOverlay?: RecordedEvidenceSemanticOverlay;
pointCloudOverlay?: RecordedEvidencePointCloudOverlayData;
ariaLabel: string;
@@ -47,7 +56,41 @@ export function RecordedEvidenceVideoScene({
segmentCount?: number;
onPlaybackChange?: (playback: RecordedObservationPlayback) => void;
onPlayingRejected?: () => void;
onAdmissionChange?: (state: RecordedCameraAdmissionState) => void;
playbackAuthority?: "media" | "host";
playbackTransport?: "segmented" | "epoch-stream";
recoverTimestampStalls?: boolean;
}) {
const generation = source.delivery?.kind === "recorded-fmp4-manifest"
? source.delivery.manifestGenerationSha256
: "invalid";
const [admissionPhase, setAdmissionPhase] = useState<RecordedCameraAdmissionState["phase"]>(
"loading",
);
const [presentedSeconds, setPresentedSeconds] = useState<number | null>(null);
useEffect(() => {
setAdmissionPhase("loading");
setPresentedSeconds(null);
}, [generation, source.id]);
const handleAdmissionChange = (next: RecordedCameraAdmissionState) => {
setAdmissionPhase(next.phase);
onAdmissionChange?.(next);
};
const sourceReady = admissionPhase === "ready";
const overlaysPresented = sourceReady
&& presentedSeconds !== null
&& Math.abs(presentedSeconds - playback.currentSeconds) <= 0.25
&& (overlaySeconds === undefined || Math.abs(presentedSeconds - overlaySeconds) <= 0.25);
const handlePlaybackChange = (next: RecordedObservationPlayback) => {
setPresentedSeconds(next.currentSeconds);
// During a paused operator seek the existing media element can emit its old
// timestamp while the requested MSE window is being rebuilt. That stale
// callback must not undo the host target before the decoder reaches it.
if (!playback.playing && Math.abs(next.currentSeconds - playback.currentSeconds) > 0.35) {
return;
}
onPlaybackChange?.(next);
};
return (
<div className="recorded-evidence-video-scene">
<RecordedFmp4Player
@@ -57,29 +100,35 @@ export function RecordedEvidenceVideoScene({
prepare
segmentSequence={segmentSequence}
segmentCount={segmentCount}
onPlaybackChange={onPlaybackChange}
onPlaybackChange={handlePlaybackChange}
onPlayingRejected={onPlayingRejected}
onAdmissionChange={handleAdmissionChange}
playbackAuthority={playbackAuthority}
playbackTransport={playbackTransport}
recoverTimestampStalls={recoverTimestampStalls}
/>
{semanticOverlay ? (
{overlaysPresented && semanticOverlay ? (
<RecordedEvidenceSemanticMaskOverlay
{...semanticOverlay}
imageWidth={imageWidth}
imageHeight={imageHeight}
/>
) : null}
{pointCloudOverlay ? (
{overlaysPresented && pointCloudOverlay ? (
<RecordedEvidencePointCloudOverlay
imageWidth={imageWidth}
imageHeight={imageHeight}
overlay={pointCloudOverlay}
/>
) : null}
<RecordedEvidenceBoxOverlay
imageWidth={imageWidth}
imageHeight={imageHeight}
boxes={boxes}
ariaLabel={ariaLabel}
/>
{overlaysPresented ? (
<RecordedEvidenceBoxOverlay
imageWidth={imageWidth}
imageHeight={imageHeight}
boxes={boxes}
ariaLabel={ariaLabel}
/>
) : null}
</div>
);
}
@@ -0,0 +1,116 @@
import { useEffect, useRef, useState } from "react";
import {
fetchCanonicalRecordedLabSpatialFrame,
type CanonicalRecordedLabSpatialFrame,
} from "../../core/laboratory/canonicalRecordedLabSpatial";
const FRAME_CACHE_LIMIT = 12;
/**
* Shared latest-request-wins scheduler for recorded LAB spatial evidence.
*
* A feature supplies only the sealed session identity and host-clock time.
* Cache ownership, identity fencing and stale-response suppression remain in
* the canonical instrument instead of being reimplemented per experiment.
*/
export function useCanonicalRecordedLabSpatialFrame({
sessionId,
generationSha256,
targetTimeNs,
}: {
sessionId: string;
generationSha256: string | null;
targetTimeNs: number;
}) {
const [frame, setFrame] = useState<CanonicalRecordedLabSpatialFrame | null>(null);
const [error, setError] = useState<string | null>(null);
const desiredRef = useRef<number | null>(null);
const runningRef = useRef(false);
const mountedRef = useRef(true);
const identityRef = useRef("");
const cacheRef = useRef(new Map<number, CanonicalRecordedLabSpatialFrame>());
const pumpRef = useRef<() => void>(() => undefined);
const identity = `${sessionId}:${generationSha256 ?? "unavailable"}`;
identityRef.current = identity;
pumpRef.current = () => {
if (runningRef.current || desiredRef.current === null || !generationSha256) return;
runningRef.current = true;
const requestIdentity = identity;
let settledTimeNs: number | null = null;
void (async () => {
while (
mountedRef.current
&& identityRef.current === requestIdentity
&& desiredRef.current !== null
) {
const requestedTimeNs = desiredRef.current;
const cached = cacheRef.current.get(requestedTimeNs);
try {
const next = cached ?? await fetchCanonicalRecordedLabSpatialFrame(
sessionId,
generationSha256,
requestedTimeNs,
);
if (!mountedRef.current || identityRef.current !== requestIdentity) break;
if (!cached) {
cacheRef.current.set(requestedTimeNs, next);
while (cacheRef.current.size > FRAME_CACHE_LIMIT) {
const oldest = cacheRef.current.keys().next().value as number | undefined;
if (oldest === undefined) break;
cacheRef.current.delete(oldest);
}
}
setFrame(next);
setError(null);
} catch (caught: unknown) {
if (!mountedRef.current || identityRef.current !== requestIdentity) break;
setError(caught instanceof Error
? caught.message
: "Spatial-слои записанной LAB недоступны.");
}
settledTimeNs = requestedTimeNs;
if (desiredRef.current === requestedTimeNs) break;
}
})().finally(() => {
runningRef.current = false;
if (
mountedRef.current
&& desiredRef.current !== null
&& (identityRef.current !== requestIdentity || desiredRef.current !== settledTimeNs)
) {
pumpRef.current();
}
});
};
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
desiredRef.current = null;
};
}, []);
useEffect(() => {
cacheRef.current.clear();
desiredRef.current = null;
setFrame(null);
setError(null);
}, [identity]);
useEffect(() => {
if (!generationSha256) return;
desiredRef.current = targetTimeNs;
const cached = cacheRef.current.get(targetTimeNs);
if (cached) {
setFrame(cached);
setError(null);
return;
}
pumpRef.current();
}, [generationSha256, identity, targetTimeNs]);
return { frame, error, loading: Boolean(generationSha256) && !frame && !error };
}
@@ -130,8 +130,12 @@ export function useRecordedEvidencePlayback(
const synchronize = useCallback((next: RecordedObservationPlayback) => {
if (!validRange(range) || !Number.isFinite(next.currentSeconds)) return;
// Animation-clock mode is retained only for non-media diagnostics. A
// recorded LAB with video uses the external media clock so spatial and
// overlays never advance past the frame the decoder actually presented.
if (clock === "animation") return;
setPlayback((current) => synchronizeRecordedEvidencePlayback(current, next, range));
}, [range]);
}, [clock, range]);
return useMemo(() => ({
playback,
@@ -118,6 +118,9 @@ export class PlayCanvasRuntime implements SimulationRuntime {
private renderIntervalMs = 1000 / 60;
private renderAccumulatorMs = 0;
private disposed = false;
private requestGsplatFrame = (): void => {
this.requestRender();
};
private updateFrame = (deltaSeconds: number): void => {
if (!this.app || !this.camera || this.disposed) return;
this.ugvController?.update(deltaSeconds);
@@ -158,6 +161,7 @@ export class PlayCanvasRuntime implements SimulationRuntime {
this.app = app;
app.autoRender = false;
app.on("update", this.updateFrame);
app.systems.gsplat?.on("frame:request", this.requestGsplatFrame);
app.setCanvasFillMode(FILLMODE_NONE, 1, 1);
app.setCanvasResolution(RESOLUTION_AUTO);
app.scene.ambientLight = new Color(0.35, 0.37, 0.42);
@@ -379,6 +383,7 @@ export class PlayCanvasRuntime implements SimulationRuntime {
if (this.app) {
this.app.autoRender = false;
this.app.renderNextFrame = false;
this.app.systems.gsplat?.off("frame:request", this.requestGsplatFrame);
this.app.off("update", this.updateFrame);
this.app.destroy();
}
@@ -24,7 +24,14 @@ const PHYSICS_PROXY_ERROR = 0.0003;
const PHYSICS_PROXY_MAX_TRIANGLES = 240_000;
const SERVICE_BRAKE_DECELERATION_MPS2 = 1.8;
const COAST_DECELERATION_MPS2 = 0.18;
const BRAKE_ATTITUDE_DAMPING = 6;
const PARKING_BRAKE_HOLD_DECELERATION_MPS2 = 6;
const PARKING_BRAKE_ENGAGE_SPEED_MPS = 0.08;
const TYRE_FRICTION_SLIP = 8.5;
const TYRE_STATIC_FRICTION_COEFFICIENT = 0.95;
const TYRE_KINETIC_FRICTION_COEFFICIENT = 0.78;
const TYRE_CONTACT_VELOCITY_RESPONSE_PER_SECOND = 10;
const GRAVITY_METERS_PER_SECOND_SQUARED = 9.81;
const MIN_TYRE_NORMAL_FORCE_NEWTONS = 1;
const DEFAULT_ORBIT_PITCH = 0.48;
const CAMERA_RETURN_DELAY_SECONDS = 1.2;
const CAMERA_RETURN_DURATION_SECONDS = 2;
@@ -79,12 +86,20 @@ interface NativeTransform extends NativeObject {
getRotation(): NativeQuaternion;
}
interface NativeRaycastInfo extends NativeObject {
get_m_contactNormalWS(): NativeVector3;
get_m_contactPointWS(): NativeVector3;
get_m_wheelAxleWS(): NativeVector3;
}
interface NativeWheelInfo extends NativeObject {
set_m_suspensionStiffness(value: number): void;
set_m_wheelsDampingRelaxation(value: number): void;
set_m_wheelsDampingCompression(value: number): void;
set_m_frictionSlip(value: number): void;
set_m_rollInfluence(value: number): void;
get_m_wheelsSuspensionForce(): number;
get_m_raycastInfo(): NativeRaycastInfo;
}
interface NativeRaycastVehicle extends NativeObject {
@@ -102,6 +117,7 @@ interface NativeRaycastVehicle extends NativeObject {
setBrake(force: number, wheel: number): void;
setSteeringValue(value: number, wheel: number): void;
getNumWheels(): number;
getWheelInfo(wheel: number): NativeWheelInfo;
updateWheelTransform(wheel: number, interpolated: boolean): void;
getWheelTransformWS(wheel: number): NativeTransform;
getForwardVector(): NativeVector3;
@@ -126,6 +142,11 @@ interface NativeDynamicsWorld extends NativeObject {
removeAction(action: NativeObject): void;
}
interface NativeRigidBody extends NativeObject {
applyImpulse(impulse: NativeVector3, relativePosition: NativeVector3): void;
setActivationState(state: number): void;
}
interface PhysicsSystemAccess {
systems: {
rigidbody: {
@@ -137,7 +158,7 @@ interface PhysicsSystemAccess {
interface NativeRigidBodyAccess {
rigidbody?: {
body: NativeObject | null;
body: NativeRigidBody | null;
linearVelocity: Vec3;
angularVelocity: Vec3;
teleport(position: Vec3, rotation?: Vec3 | Quat): void;
@@ -192,6 +213,13 @@ export class SimulationUgvController {
private readonly smoothedCamera = new Vec3();
private readonly limitedLinearVelocity = new Vec3();
private readonly limitedAngularVelocity = new Vec3();
private readonly tyreContactNormal = new Vec3();
private readonly tyreLateralDirection = new Vec3();
private readonly tyreLongitudinalDirection = new Vec3();
private readonly tyreContactPoint = new Vec3();
private readonly tyreRelativePosition = new Vec3();
private readonly tyreAngularContactVelocity = new Vec3();
private readonly tyreContactVelocity = new Vec3();
private readonly spawnPosition = UGV_SPAWN_POSITION.clone();
private orbitPointerId: number | null = null;
private orbitPointerX = 0;
@@ -204,8 +232,9 @@ export class SimulationUgvController {
private active = false;
private vehicleEntity: Entity | null = null;
private vehicle: NativeRaycastVehicle | null = null;
private vehicleTuning: NativeObject | null = null;
private vehicleRaycaster: NativeObject | null = null;
private tyreImpulseNative: NativeVector3 | null = null;
private tyreRelativePositionNative: NativeVector3 | null = null;
private dynamicsWorld: NativeDynamicsWorld | null = null;
private chassisMaterial: StandardMaterial | null = null;
private wheelMaterial: StandardMaterial | null = null;
@@ -308,9 +337,27 @@ export class SimulationUgvController {
const braking = this.pressed.has("Space");
const rigidbody = (this.vehicleEntity as Entity & NativeRigidBodyAccess).rigidbody;
const speedMetersPerSecond = this.vehicle.getCurrentSpeedKmHour() / 3.6;
const nativeForward = this.vehicle.getForwardVector();
const nativeForwardLength = Math.hypot(
nativeForward.x(),
nativeForward.y(),
nativeForward.z(),
);
const longitudinalSpeedMetersPerSecond = rigidbody && nativeForwardLength > 0.001
? Math.abs(
(
rigidbody.linearVelocity.x * nativeForward.x()
+ rigidbody.linearVelocity.y * nativeForward.y()
+ rigidbody.linearVelocity.z * nativeForward.z()
) / nativeForwardLength,
)
: Math.abs(speedMetersPerSecond);
const maxSpeed = this.settings.maxSpeedMetersPerSecond;
const maxTurnRate = this.settings.maxTurnRateDegrees * Math.PI / 180;
const pureTurn = !braking && forwardInput === 0 && turnInput !== 0;
const holding = !braking && forwardInput === 0 && turnInput === 0;
const parkingBrakeEngaged = holding
&& longitudinalSpeedMetersPerSecond <= PARKING_BRAKE_ENGAGE_SPEED_MPS;
const desiredSpeed = forwardInput * maxSpeed;
const speedError = desiredSpeed - speedMetersPerSecond;
const speedResponseRange = Math.max(0.35, maxSpeed * 0.2);
@@ -325,13 +372,27 @@ export class SimulationUgvController {
const leftCommand = clamp(forwardCommand - turnCommand, -1, 1);
const rightCommand = clamp(forwardCommand + turnCommand, -1, 1);
const engineForce = pureTurn ? pivotForcePerWheel : driveForcePerWheel;
const brakeDeceleration = braking
? SERVICE_BRAKE_DECELERATION_MPS2
: holding
? parkingBrakeEngaged
? PARKING_BRAKE_HOLD_DECELERATION_MPS2
: COAST_DECELERATION_MPS2
: 0;
const wheelBrakeForce = this.settings.massKg * brakeDeceleration
/ Math.max(1, this.wheelDefinitions.length);
for (let index = 0; index < this.wheelDefinitions.length; index += 1) {
const definition = this.wheelDefinitions[index];
const command = definition.left ? leftCommand : rightCommand;
// Parking contact is solved below with one 2D Coulomb limit; disable the
// raycast vehicle's parallel friction impulse so grip is not counted twice.
this.vehicle.getWheelInfo(index).set_m_frictionSlip(
parkingBrakeEngaged ? 0 : TYRE_FRICTION_SLIP,
);
this.vehicle.setSteeringValue(0, index);
this.vehicle.applyEngineForce(command * engineForce, index);
this.vehicle.setBrake(0, index);
this.vehicle.setBrake(wheelBrakeForce, index);
this.vehicle.updateWheelTransform(index, true);
const transform = this.vehicle.getWheelTransformWS(index);
const position = transform.getOrigin();
@@ -340,22 +401,21 @@ export class SimulationUgvController {
definition.anchor.setRotation(rotation.x(), rotation.y(), rotation.z(), rotation.w());
}
const body = rigidbody?.body ?? null;
if (rigidbody && body) {
this.applyParkingTyreContact(
deltaSeconds,
rigidbody,
body,
parkingBrakeEngaged,
);
}
if (rigidbody) {
const linearVelocity = rigidbody.linearVelocity;
let nextLinearX = linearVelocity.x;
let nextLinearZ = linearVelocity.z;
let horizontalSpeed = Math.hypot(nextLinearX, nextLinearZ);
const coasting = !braking && forwardInput === 0 && turnInput === 0;
if ((braking || coasting) && horizontalSpeed > 0.001) {
const deceleration = braking
? SERVICE_BRAKE_DECELERATION_MPS2
: COAST_DECELERATION_MPS2;
const nextSpeed = Math.max(0, horizontalSpeed - deceleration * Math.max(0, deltaSeconds));
const scale = nextSpeed / horizontalSpeed;
nextLinearX *= scale;
nextLinearZ *= scale;
horizontalSpeed = nextSpeed;
}
if (pureTurn && horizontalSpeed > 0.001) {
const pivotDamping = Math.exp(-Math.max(0, deltaSeconds) * 8);
nextLinearX *= pivotDamping;
@@ -372,9 +432,6 @@ export class SimulationUgvController {
rigidbody.linearVelocity = this.limitedLinearVelocity;
}
const angularVelocity = rigidbody.angularVelocity;
const attitudeDamping = braking
? Math.exp(-Math.max(0, deltaSeconds) * BRAKE_ATTITUDE_DAMPING)
: 1;
let nextAngularY = angularVelocity.y;
if (pureTurn) {
const desiredYawRate = -turnInput * maxTurnRate;
@@ -387,24 +444,137 @@ export class SimulationUgvController {
} else if (Math.abs(angularVelocity.y) > maxTurnRate) {
nextAngularY = Math.sign(angularVelocity.y) * maxTurnRate;
}
if (attitudeDamping !== 1 || nextAngularY !== angularVelocity.y) {
if (nextAngularY !== angularVelocity.y) {
this.limitedAngularVelocity.set(
angularVelocity.x * attitudeDamping,
angularVelocity.x,
nextAngularY,
angularVelocity.z * attitudeDamping,
angularVelocity.z,
);
rigidbody.angularVelocity = this.limitedAngularVelocity;
}
}
const body = (this.vehicleEntity as Entity & NativeRigidBodyAccess).rigidbody?.body as {
setActivationState?: (state: number) => void;
} | null;
body?.setActivationState?.(DISABLE_DEACTIVATION);
body?.setActivationState(DISABLE_DEACTIVATION);
if (this.vehicleEntity.getPosition().y < -8) this.reset();
this.updateCamera(deltaSeconds);
}
private applyParkingTyreContact(
deltaSeconds: number,
rigidbody: NonNullable<NativeRigidBodyAccess["rigidbody"]>,
body: NativeRigidBody,
parkingBrakeEngaged: boolean,
): void {
if (!this.vehicle || !this.vehicleEntity || !this.tyreImpulseNative
|| !this.tyreRelativePositionNative) return;
if (!parkingBrakeEngaged) return;
const timeStep = Math.min(Math.max(0, deltaSeconds), 1 / 30);
if (timeStep === 0) return;
const wheelEffectiveMass = this.settings.massKg
/ Math.max(1, this.wheelDefinitions.length);
const chassisPosition = this.vehicleEntity.getPosition();
for (let index = 0; index < this.wheelDefinitions.length; index += 1) {
const wheel = this.vehicle.getWheelInfo(index);
const raycast = wheel.get_m_raycastInfo();
const normalForce = wheel.get_m_wheelsSuspensionForce();
if (!Number.isFinite(normalForce) || normalForce < MIN_TYRE_NORMAL_FORCE_NEWTONS) {
continue;
}
const nativeNormal = raycast.get_m_contactNormalWS();
this.tyreContactNormal.set(nativeNormal.x(), nativeNormal.y(), nativeNormal.z());
if (this.tyreContactNormal.lengthSq() < 0.001) continue;
this.tyreContactNormal.normalize();
const nativeAxle = raycast.get_m_wheelAxleWS();
this.tyreLateralDirection.set(nativeAxle.x(), nativeAxle.y(), nativeAxle.z());
this.tyreLateralDirection.addScaled(
this.tyreContactNormal,
-this.tyreLateralDirection.dot(this.tyreContactNormal),
);
if (this.tyreLateralDirection.lengthSq() < 0.001) continue;
this.tyreLateralDirection.normalize();
this.tyreLongitudinalDirection.cross(
this.tyreContactNormal,
this.tyreLateralDirection,
).normalize();
const nativeContactPoint = raycast.get_m_contactPointWS();
this.tyreContactPoint.set(
nativeContactPoint.x(),
nativeContactPoint.y(),
nativeContactPoint.z(),
);
this.tyreRelativePosition.sub2(this.tyreContactPoint, chassisPosition);
this.tyreAngularContactVelocity.cross(
rigidbody.angularVelocity,
this.tyreRelativePosition,
);
this.tyreContactVelocity.add2(
rigidbody.linearVelocity,
this.tyreAngularContactVelocity,
);
const lateralSlipSpeed = this.tyreContactVelocity.dot(this.tyreLateralDirection);
const longitudinalSlipSpeed = this.tyreContactVelocity.dot(
this.tyreLongitudinalDirection,
);
// Static tyre friction is a contact constraint: it balances the component
// of gravity along the surface and damps slip at the contact patch. The
// force still passes through a Coulomb circle and is applied at the wheel,
// so the chassis remains a fully dynamic rigid body.
const lateralGravityAcceleration = -GRAVITY_METERS_PER_SECOND_SQUARED
* this.tyreLateralDirection.y;
const longitudinalGravityAcceleration = -GRAVITY_METERS_PER_SECOND_SQUARED
* this.tyreLongitudinalDirection.y;
const trialLateralForce = -wheelEffectiveMass * (
lateralGravityAcceleration
+ TYRE_CONTACT_VELOCITY_RESPONSE_PER_SECOND * lateralSlipSpeed
);
const trialLongitudinalForce = -wheelEffectiveMass * (
longitudinalGravityAcceleration
+ TYRE_CONTACT_VELOCITY_RESPONSE_PER_SECOND * longitudinalSlipSpeed
);
const staticFrictionLimit = TYRE_STATIC_FRICTION_COEFFICIENT * normalForce;
let lateralForce = trialLateralForce;
let longitudinalForce = trialLongitudinalForce;
if (Math.hypot(trialLateralForce, trialLongitudinalForce) > staticFrictionLimit) {
const slipSpeed = Math.hypot(lateralSlipSpeed, longitudinalSlipSpeed);
const kineticFrictionLimit = TYRE_KINETIC_FRICTION_COEFFICIENT * normalForce;
if (slipSpeed > 0.0001) {
lateralForce = -(lateralSlipSpeed / slipSpeed) * kineticFrictionLimit;
longitudinalForce = -(longitudinalSlipSpeed / slipSpeed) * kineticFrictionLimit;
} else {
const forceScale = staticFrictionLimit
/ Math.hypot(trialLateralForce, trialLongitudinalForce);
lateralForce = trialLateralForce * forceScale;
longitudinalForce = trialLongitudinalForce * forceScale;
}
}
const lateralImpulse = lateralForce * timeStep;
const longitudinalImpulse = longitudinalForce * timeStep;
this.tyreImpulseNative.setValue(
this.tyreLateralDirection.x * lateralImpulse
+ this.tyreLongitudinalDirection.x * longitudinalImpulse,
this.tyreLateralDirection.y * lateralImpulse
+ this.tyreLongitudinalDirection.y * longitudinalImpulse,
this.tyreLateralDirection.z * lateralImpulse
+ this.tyreLongitudinalDirection.z * longitudinalImpulse,
);
this.tyreRelativePositionNative.setValue(
this.tyreRelativePosition.x,
this.tyreRelativePosition.y,
this.tyreRelativePosition.z,
);
body.applyImpulse(this.tyreImpulseNative, this.tyreRelativePositionNative);
}
}
private async createStaticCollisionBodies(collisionWorld: Entity): Promise<void> {
const models = collisionWorld.findComponents("model") as ModelComponent[];
if (models.length === 0) throw new Error("В слое коллизий нет геометрии для физики UGV.");
@@ -428,7 +598,12 @@ export class SimulationUgvController {
this.app,
meshInstance.mesh,
new Mat4().mul2(rootInverse, meshInstance.node.getWorldTransform()),
targetRatio,
Math.max(
1,
Math.floor(
Math.floor((meshInstance.mesh.primitive[0]?.count ?? 0) / 3) * targetRatio,
),
),
);
if (!physicsMesh) continue;
const entity = new Entity(`UGV physics proxy ${index + 1}`);
@@ -464,8 +639,9 @@ export class SimulationUgvController {
type: "dynamic",
mass: this.settings.massKg,
friction: 0.85,
linearDamping: 0.08,
angularDamping: 0.45,
rollingFriction: 0.12,
linearDamping: 0.12,
angularDamping: 0.6,
});
this.chassisMaterial = createMaterial(readThemeAccent(), new Color(0.03, 0.04, 0.05));
@@ -525,7 +701,10 @@ export class SimulationUgvController {
applyMaterial(wheelMesh, this.wheelMaterial);
anchor.addChild(wheelMesh);
vehicle.addChild(anchor);
this.wheelDefinitions.push({ ...definition, anchor });
this.wheelDefinitions.push({
...definition,
anchor,
});
}
vehicle.setLocalPosition(this.spawnPosition);
@@ -559,16 +738,17 @@ export class SimulationUgvController {
wheel.set_m_suspensionStiffness(24);
wheel.set_m_wheelsDampingRelaxation(3.2);
wheel.set_m_wheelsDampingCompression(4.8);
wheel.set_m_frictionSlip(5.5);
wheel.set_m_frictionSlip(TYRE_FRICTION_SLIP);
wheel.set_m_rollInfluence(0.08);
}
this.ammo.destroy(axle);
this.ammo.destroy(direction);
this.ammo.destroy(connection);
this.tyreImpulseNative = new this.ammo.btVector3(0, 0, 0);
this.tyreRelativePositionNative = new this.ammo.btVector3(0, 0, 0);
dynamicsWorld.addAction(nativeVehicle);
this.vehicleEntity = vehicle;
this.vehicleTuning = tuning;
this.vehicleRaycaster = raycaster;
this.vehicle = nativeVehicle;
this.dynamicsWorld = dynamicsWorld;
@@ -684,10 +864,12 @@ export class SimulationUgvController {
}
if (this.vehicle) runCleanup("destroy vehicle", () => this.ammo.destroy(this.vehicle as NativeObject));
if (this.vehicleRaycaster) runCleanup("destroy vehicle raycaster", () => this.ammo.destroy(this.vehicleRaycaster as NativeObject));
if (this.vehicleTuning) runCleanup("destroy vehicle tuning", () => this.ammo.destroy(this.vehicleTuning as NativeObject));
if (this.tyreImpulseNative) runCleanup("destroy tyre impulse vector", () => this.ammo.destroy(this.tyreImpulseNative as NativeObject));
if (this.tyreRelativePositionNative) runCleanup("destroy tyre relative-position vector", () => this.ammo.destroy(this.tyreRelativePositionNative as NativeObject));
this.vehicle = null;
this.vehicleRaycaster = null;
this.vehicleTuning = null;
this.tyreImpulseNative = null;
this.tyreRelativePositionNative = null;
this.dynamicsWorld = null;
if (this.vehicleEntity) runCleanup("destroy vehicle entity", () => this.vehicleEntity?.destroy());
@@ -824,7 +1006,7 @@ function createPhysicsProxyMesh(
app: Application,
source: Mesh,
localTransform: Mat4,
targetRatio: number,
targetTriangleCount: number,
): Mesh | null {
const primitive = source.primitive[0];
const sourceVertexCount = source.vertexBuffer?.numVertices ?? 0;
@@ -858,8 +1040,11 @@ function createPhysicsProxyMesh(
if (triangleIndexCount < 3) return null;
if (triangleIndexCount !== indices.length) indices = indices.slice(0, triangleIndexCount);
if (targetRatio < 1) {
const targetIndexCount = Math.max(3, Math.floor(indices.length * targetRatio / 3) * 3);
const targetIndexCount = Math.max(
3,
Math.min(indices.length, Math.floor(targetTriangleCount) * 3),
);
if (indices.length > targetIndexCount) {
const [simplifiedIndices] = MeshoptSimplifier.simplify(
indices,
transformed,
@@ -868,6 +1053,26 @@ function createPhysicsProxyMesh(
PHYSICS_PROXY_ERROR,
);
indices = new Uint32Array(simplifiedIndices);
if (indices.length > targetIndexCount) {
// Topologically noisy scanner meshes can make the quality simplifier stop
// above its requested target. Ammo must never receive that unbounded result:
// the spatial fallback preserves the surface envelope while enforcing the
// same deterministic physics budget for every imported location.
const [boundedIndices] = MeshoptSimplifier.simplifySloppy(
indices,
transformed,
3,
null,
targetIndexCount,
1,
);
indices = new Uint32Array(boundedIndices);
}
if (indices.length > targetIndexCount) {
throw new Error(
`Physics proxy превысил лимит: ${Math.floor(indices.length / 3)} > ${Math.floor(targetIndexCount / 3)} треугольников.`,
);
}
}
const compactedIndices = new Uint32Array(indices);
@@ -29,7 +29,6 @@ export interface E31LaboratoryResult {
limitations: readonly string[];
access: "read-only";
}
export interface E32LaboratoryResult {
resultId: string;
createdAtUtc: string | null;
@@ -53,7 +52,6 @@ export interface E32LaboratoryResult {
};
access: "read-only";
}
export interface E33LaboratoryResult {
resultId: string;
createdAtUtc: string | null;
@@ -0,0 +1,76 @@
import type { ObservationSessionReplayLaunch } from "../observation/sessionArchive";
import type { RecordedRrdArtifactDescriptor } from "../observation/viewerProfile";
const SAFE_RESULT_ID = /^lab-v1-vegetation-shadow-[a-f0-9]{64}$/;
const SAFE_SESSION_SOURCE = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\/recording\.rrd$/;
const MAX_CANONICAL_REPLAY_BYTES = 1024 * 1024 * 1024;
export interface CanonicalLabReplayDescriptor extends RecordedRrdArtifactDescriptor {
blueprintSourceUrl: string;
}
/**
* Resolve the one immutable RRD used by the canonical recorded LAB.
*
* The server caches the merge of the sealed spatial recording and the LAB AI
* evidence. Rerun therefore opens one source and cannot present the base store
* before a second receiver has finished decoding the semantic layer.
*/
export async function resolveCanonicalLabReplay(
resultId: string,
launch: ObservationSessionReplayLaunch,
{
origin = window.location.origin,
signal,
fetcher = globalThis.fetch,
}: {
origin?: string;
signal?: AbortSignal;
fetcher?: typeof globalThis.fetch;
} = {},
): Promise<CanonicalLabReplayDescriptor> {
const base = new URL(origin);
if (
!SAFE_RESULT_ID.test(resultId)
|| !SAFE_SESSION_SOURCE.test(launch.sourceUrl)
|| launch.viewerSourceUrl !== `${launch.sourceUrl}?generation=${launch.sha256}`
|| !/^[a-f0-9]{64}$/.test(launch.sha256)
) {
throw new Error("Канонический replay LAB имеет небезопасный descriptor.");
}
const sourceUrl =
`/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}`
+ "/canonical-replay.rrd";
const descriptorUrl = new URL(sourceUrl, `${base.origin}/`);
descriptorUrl.searchParams.set("base_generation", launch.sha256);
if (descriptorUrl.origin !== base.origin) {
throw new Error("Канонический replay LAB должен быть same-origin.");
}
const response = await fetcher(descriptorUrl.href, {
method: "HEAD",
credentials: "same-origin",
headers: { Accept: "application/vnd.rerun.rrd" },
signal,
});
const contentType = response.headers.get("Content-Type")?.split(";", 1)[0].trim();
const byteLength = Number(response.headers.get("Content-Length"));
const sha256 = response.headers.get("ETag")?.match(/^"([a-f0-9]{64})"$/)?.[1];
if (
response.status !== 200
|| contentType !== "application/vnd.rerun.rrd"
|| response.headers.get("X-Rerun-Format") !== "RRF2"
|| !sha256
|| !Number.isSafeInteger(byteLength)
|| byteLength < 4
|| byteLength > MAX_CANONICAL_REPLAY_BYTES
) {
throw new Error("Единый replay LAB не прошёл проверку.");
}
return {
sourceUrl,
viewerSourceUrl: `${sourceUrl}?generation=${sha256}`,
byteLength,
sha256,
blueprintSourceUrl: launch.sourceUrl.replace(/\/recording\.rrd$/, "/blueprint.rrd"),
};
}
@@ -0,0 +1,99 @@
export const CANONICAL_RECORDED_LAB_TGS_HISTORY_SECONDS = 1;
export const CANONICAL_RECORDED_LAB_SPATIAL_PROFILE = "source-paced-ground-v3";
export interface CanonicalRecordedLabPackedCellEvidence {
centersBodyXyM: Float32Array;
zBoundsM: Float32Array;
stateCodes: Uint8Array;
}
export interface CanonicalRecordedLabBodyGroundFrame {
originMapXyzM: readonly [number, number, number];
sensorOriginMapXyzM: readonly [number, number, number];
basisMapFromBody: readonly [
readonly [number, number, number],
readonly [number, number, number],
readonly [number, number, number],
];
}
export interface CanonicalRecordedLabTgsCostmap {
centersXyM: readonly (readonly [number, number])[];
stateCodes: readonly number[];
zBoundsM: readonly (readonly [number | null, number | null])[];
}
export function canonicalRecordedLabTgsIsCurrent(
currentTimeNs: number,
anchorTimeNs: number,
historySeconds = CANONICAL_RECORDED_LAB_TGS_HISTORY_SECONDS,
): boolean {
if (
!Number.isSafeInteger(currentTimeNs)
|| !Number.isSafeInteger(anchorTimeNs)
|| !Number.isFinite(historySeconds)
|| historySeconds <= 0
) return false;
const ageNs = currentTimeNs - anchorTimeNs;
return ageNs >= 0 && ageNs <= Math.round(historySeconds * 1_000_000_000);
}
export function canonicalMapGravityLocalPointToBodyGround(
point: readonly [number, number, number],
anchor: CanonicalRecordedLabBodyGroundFrame,
current: CanonicalRecordedLabBodyGroundFrame,
): readonly [number, number, number] {
// TGS is translation-only map-gravity-local: its axes are map axes and its
// origin is the LiDAR at the source frame. It is not an anchor body frame.
const map: readonly [number, number, number] = [
anchor.sensorOriginMapXyzM[0] + point[0],
anchor.sensorOriginMapXyzM[1] + point[1],
anchor.sensorOriginMapXyzM[2] + point[2],
];
const delta: readonly [number, number, number] = [
map[0] - current.originMapXyzM[0],
map[1] - current.originMapXyzM[1],
map[2] - current.originMapXyzM[2],
];
return [
current.basisMapFromBody[0][0] * delta[0]
+ current.basisMapFromBody[1][0] * delta[1]
+ current.basisMapFromBody[2][0] * delta[2],
current.basisMapFromBody[0][1] * delta[0]
+ current.basisMapFromBody[1][1] * delta[1]
+ current.basisMapFromBody[2][1] * delta[2],
current.basisMapFromBody[0][2] * delta[0]
+ current.basisMapFromBody[1][2] * delta[1]
+ current.basisMapFromBody[2][2] * delta[2],
];
}
export function canonicalRecordedLabPackedTgsCells(
costmap: CanonicalRecordedLabTgsCostmap,
anchor: CanonicalRecordedLabBodyGroundFrame,
current: CanonicalRecordedLabBodyGroundFrame,
): CanonicalRecordedLabPackedCellEvidence {
if (
costmap.centersXyM.length !== costmap.stateCodes.length
|| costmap.centersXyM.length !== costmap.zBoundsM.length
) throw new Error("Canonical recorded LAB TGS accounting changed");
const centers: number[] = [];
const zBounds: number[] = [];
costmap.centersXyM.forEach(([x, y], index) => {
const bounds = costmap.zBoundsM[index] ?? [null, null];
const center = canonicalMapGravityLocalPointToBodyGround([x, y, 0], anchor, current);
centers.push(center[0], center[1]);
if (bounds[0] === null || bounds[1] === null) {
zBounds.push(Number.NaN, Number.NaN);
return;
}
const bottom = canonicalMapGravityLocalPointToBodyGround([x, y, bounds[0]], anchor, current);
const top = canonicalMapGravityLocalPointToBodyGround([x, y, bounds[1]], anchor, current);
zBounds.push(Math.min(bottom[2], top[2]), Math.max(bottom[2], top[2]));
});
return {
centersBodyXyM: Float32Array.from(centers),
zBoundsM: Float32Array.from(zBounds),
stateCodes: Uint8Array.from(costmap.stateCodes),
};
}
@@ -0,0 +1,259 @@
import type { LaboratoryFetch } from "./advancedResults";
import { CANONICAL_RECORDED_LAB_SPATIAL_PROFILE } from "./canonicalRecordedLab";
const SAFE_SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const SHA256 = /^[a-f0-9]{64}$/;
export class CanonicalRecordedLabSpatialContractError extends Error {}
export interface CanonicalRecordedLabSpatialFrame {
targetTimeNs: number;
sourceTimeNs: number;
poseTimeNs: number;
trajectoryTimeNs: number;
sourcePointCount: number;
coordinateFrame: "body-ground";
sensorHeight: {
meters: number;
source: "local-source-cloud-ground-quantile-median" | "session-source-cloud-fallback";
sampleCount: number;
madM: number;
authority: "visual-derived";
};
spatialProfile: {
profileId: typeof CANONICAL_RECORDED_LAB_SPATIAL_PROFILE;
localSlamHistorySeconds: number;
localSlamRadiusM: number;
localSlamVoxelSizeM: number;
localSlamPointLimit: number;
};
bodyFrame: {
originMapXyzM: readonly [number, number, number];
sensorOriginMapXyzM: readonly [number, number, number];
basisMapFromBody: readonly [
readonly [number, number, number],
readonly [number, number, number],
readonly [number, number, number],
];
};
sourcePointsBodyXyzM: readonly (readonly [number, number, number])[];
localSlamSourceFrameCount: number;
localSlamSourcePointCount: number;
localSlamBodyXyzM: readonly (readonly [number, number, number])[];
}
function objectValue(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new CanonicalRecordedLabSpatialContractError(`${label}: ожидался объект.`);
}
return value as Record<string, unknown>;
}
function arrayValue(value: unknown, label: string): unknown[] {
if (!Array.isArray(value)) {
throw new CanonicalRecordedLabSpatialContractError(`${label}: ожидался массив.`);
}
return value;
}
function exact(value: unknown, expected: unknown, label: string): void {
if (value !== expected) {
throw new CanonicalRecordedLabSpatialContractError(`${label}: контракт изменён.`);
}
}
function numberValue(value: unknown, label: string): number {
if (typeof value !== "number" || !Number.isFinite(value)) {
throw new CanonicalRecordedLabSpatialContractError(`${label}: ожидалось число.`);
}
return value;
}
function integerValue(value: unknown, label: string): number {
const parsed = numberValue(value, label);
if (!Number.isSafeInteger(parsed) || parsed < 0) {
throw new CanonicalRecordedLabSpatialContractError(`${label}: ожидалось целое значение.`);
}
return parsed;
}
function pointList(
value: unknown,
label: string,
): readonly (readonly [number, number, number])[] {
return arrayValue(value, label).map((entry, index) => {
const point = arrayValue(entry, `${label}[${index}]`).map(
(channel, channelIndex) => numberValue(channel, `${label}[${index}][${channelIndex}]`),
);
if (point.length !== 3) {
throw new CanonicalRecordedLabSpatialContractError(`${label}[${index}]: размер изменён.`);
}
return [point[0]!, point[1]!, point[2]!] as const;
});
}
export async function fetchCanonicalRecordedLabSpatialFrame(
sessionId: string,
generationSha256: string,
targetTimeNs: number,
{
fetcher = fetch,
signal,
}: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
): Promise<CanonicalRecordedLabSpatialFrame> {
if (
!SAFE_SESSION_ID.test(sessionId)
|| !SHA256.test(generationSha256)
|| !Number.isSafeInteger(targetTimeNs)
|| targetTimeNs < 0
) {
throw new CanonicalRecordedLabSpatialContractError(
"Canonical LAB spatial identity недопустима.",
);
}
const query = new URLSearchParams({
generation: generationSha256,
time_ns: String(targetTimeNs),
profile: CANONICAL_RECORDED_LAB_SPATIAL_PROFILE,
});
const response = await fetcher(
`/api/v1/observation-sessions/${encodeURIComponent(sessionId)}`
+ `/canonical-lab/spatial-frame?${query.toString()}`,
{ method: "GET", headers: { Accept: "application/json" }, signal },
);
if (!response.ok) {
throw new CanonicalRecordedLabSpatialContractError(
`Canonical LAB spatial frame недоступен: HTTP ${response.status}.`,
);
}
const payload = objectValue(await response.json(), "canonical_lab.spatial_frame");
exact(
payload.schema_version,
"missioncore.canonical-recorded-lab-spatial-frame/v3",
"canonical_lab.spatial_frame.schema_version",
);
exact(payload.coordinate_frame, "body-ground", "canonical_lab.spatial_frame.coordinate_frame");
exact(payload.target_time_ns, targetTimeNs, "canonical_lab.spatial_frame.target_time_ns");
const sourcePoints = pointList(
payload.source_points_body_xyz_m,
"canonical_lab.spatial_frame.source_points",
);
const localSlam = pointList(
payload.local_slam_body_xyz_m,
"canonical_lab.spatial_frame.local_slam",
);
const sourcePointCount = integerValue(
payload.source_point_count,
"canonical_lab.spatial_frame.source_point_count",
);
const localSlamPointCount = integerValue(
payload.local_slam_point_count,
"canonical_lab.spatial_frame.local_slam_point_count",
);
if (
sourcePointCount !== sourcePoints.length
|| sourcePointCount > 100_000
|| localSlamPointCount !== localSlam.length
|| localSlam.length > 27_000
) {
throw new CanonicalRecordedLabSpatialContractError(
"Canonical LAB spatial accounting изменён.",
);
}
const bodyFrame = objectValue(payload.body_frame, "canonical_lab.spatial_frame.body_frame");
const origin = pointList(
[bodyFrame.origin_map_xyz_m],
"canonical_lab.spatial_frame.body_frame.origin",
)[0]!;
const sensorOrigin = pointList(
[bodyFrame.sensor_origin_map_xyz_m],
"canonical_lab.spatial_frame.body_frame.sensor_origin",
)[0]!;
const basisRows = pointList(
bodyFrame.basis_map_from_body,
"canonical_lab.spatial_frame.body_frame.basis",
);
if (basisRows.length !== 3) {
throw new CanonicalRecordedLabSpatialContractError(
"Canonical LAB spatial basis изменён.",
);
}
const sensorHeight = objectValue(payload.sensor_height, "canonical_lab.spatial_frame.sensor_height");
if (
sensorHeight.source !== "local-source-cloud-ground-quantile-median"
&& sensorHeight.source !== "session-source-cloud-fallback"
) {
throw new CanonicalRecordedLabSpatialContractError(
"canonical_lab.spatial_frame.sensor_height.source: контракт изменён.",
);
}
exact(
sensorHeight.authority,
"visual-derived",
"canonical_lab.spatial_frame.sensor_height.authority",
);
const spatialProfile = objectValue(
payload.spatial_profile,
"canonical_lab.spatial_frame.spatial_profile",
);
exact(
spatialProfile.profile_id,
CANONICAL_RECORDED_LAB_SPATIAL_PROFILE,
"canonical_lab.spatial_frame.spatial_profile.profile_id",
);
return {
targetTimeNs,
sourceTimeNs: integerValue(payload.source_time_ns, "canonical_lab.spatial_frame.source_time_ns"),
poseTimeNs: integerValue(payload.pose_time_ns, "canonical_lab.spatial_frame.pose_time_ns"),
trajectoryTimeNs: integerValue(
payload.trajectory_time_ns,
"canonical_lab.spatial_frame.trajectory_time_ns",
),
sourcePointCount,
coordinateFrame: "body-ground",
sensorHeight: {
meters: numberValue(sensorHeight.meters, "canonical_lab.spatial_frame.sensor_height.meters"),
source: sensorHeight.source,
sampleCount: integerValue(
sensorHeight.sample_count,
"canonical_lab.spatial_frame.sensor_height.sample_count",
),
madM: numberValue(sensorHeight.mad_m, "canonical_lab.spatial_frame.sensor_height.mad_m"),
authority: "visual-derived",
},
spatialProfile: {
profileId: CANONICAL_RECORDED_LAB_SPATIAL_PROFILE,
localSlamHistorySeconds: numberValue(
spatialProfile.local_slam_history_seconds,
"canonical_lab.spatial_frame.spatial_profile.history",
),
localSlamRadiusM: numberValue(
spatialProfile.local_slam_radius_m,
"canonical_lab.spatial_frame.spatial_profile.radius",
),
localSlamVoxelSizeM: numberValue(
spatialProfile.local_slam_voxel_size_m,
"canonical_lab.spatial_frame.spatial_profile.voxel",
),
localSlamPointLimit: integerValue(
spatialProfile.local_slam_point_limit,
"canonical_lab.spatial_frame.spatial_profile.limit",
),
},
bodyFrame: {
originMapXyzM: origin,
sensorOriginMapXyzM: sensorOrigin,
basisMapFromBody: [basisRows[0]!, basisRows[1]!, basisRows[2]!],
},
sourcePointsBodyXyzM: sourcePoints,
localSlamSourceFrameCount: integerValue(
payload.local_slam_source_frame_count,
"canonical_lab.spatial_frame.local_slam_source_frames",
),
localSlamSourcePointCount: integerValue(
payload.local_slam_source_point_count,
"canonical_lab.spatial_frame.local_slam_source_points",
),
localSlamBodyXyzM: localSlam,
};
}
@@ -154,6 +154,9 @@ export interface M4ThreatTimelineFrame {
pointCloudSourceCount: number;
pointCloudSampleCount: number;
pointCloudLayer: "current-increment";
localSlamBodyXyzM?: readonly M4Point3[];
localSlamSourceFrameCount?: number;
localSlamSourcePointCount?: number;
cameraProjectedPointsXyd: readonly (readonly [number, number, number])[];
cameraProjectedSourceCount: number;
cameraProjectedPointCount: number;
@@ -168,10 +171,11 @@ export interface M4ThreatTimelineFrame {
export interface M4ThreatTimeline {
resultId: string;
recordedSourceSessionId: "20260720T065719Z_viewer_live";
recordedSourceSessionId: string;
recordedSourceId: string;
imageWidth: 800;
imageHeight: 600;
frameCount: 4489;
frameCount: number;
frameTimesNs: readonly number[];
timelineStartSeconds: number;
timelineEndSeconds: number;
@@ -234,7 +238,7 @@ export interface M4ThreatPlaybackProgress {
export interface M4ThreatPlaybackPointPack {
resultId: string;
frameCount: 4489;
frameCount: number;
pointCount: number;
pointOffsets: Uint32Array;
pointsMapXyzM: Float32Array;
@@ -257,7 +261,7 @@ export interface M4ThreatPlaybackChunkDescriptor {
export interface M4ThreatPlaybackManifest {
resultId: string;
frameCount: 4489;
frameCount: number;
pointCount: number;
pointOffsets: Uint32Array;
chunkFrameCount: 24;
@@ -337,7 +341,7 @@ const motion = (value: unknown): M4ThreatMotion => {
};
const resultId = (value: unknown): string => {
const parsed = text(value, "M4.6 result id");
if (!/^m4-threat-replay-[a-f0-9]{64}$/.test(parsed)) {
if (!/^[a-z0-9][a-z0-9-]{0,127}-[a-f0-9]{64}$/.test(parsed)) {
throw new M4ThreatContractError("M4.6 result id: нарушена идентичность.");
}
return parsed;
@@ -704,12 +708,8 @@ export async function fetchM4ThreatTimeline(
exact(payload.result_id, result, "M4.6 timeline result");
exact(payload.authority, "replay-simulated", "M4.6 timeline authority");
const recorded = object(payload.recorded_source, "M4.6 recorded source");
exact(
recorded.session_id,
"20260720T065719Z_viewer_live",
"M4.6 recorded session",
);
exact(recorded.source_id, "RAVNOVES00", "M4.6 recorded source id");
const recordedSessionId = text(recorded.session_id, "M4.6 recorded session");
const recordedSourceId = text(recorded.source_id, "M4.6 recorded source id");
exact(
recorded.representation_id,
"registered-map-increment-v1",
@@ -720,7 +720,10 @@ export async function fetchM4ThreatTimeline(
"host-arrival-best-effort",
"M4.6 recorded synchronization",
);
const frameCount = exact(payload.frame_count, 4489, "M4.6 timeline frame count");
const frameCount = integer(payload.frame_count, "M4.6 timeline frame count");
if (frameCount < 1) {
throw new M4ThreatContractError("M4.6 timeline frame count: пустой timeline.");
}
const frameTimesNs = array(payload.frame_times_ns, "M4.6 timeline index").map(
(value) => integer(value, "M4.6 timeline time"),
);
@@ -738,7 +741,8 @@ export async function fetchM4ThreatTimeline(
);
return {
resultId: result,
recordedSourceSessionId: "20260720T065719Z_viewer_live",
recordedSourceSessionId: recordedSessionId,
recordedSourceId,
imageWidth: exact(payload.image_width, 800, "M4.6 image width"),
imageHeight: exact(payload.image_height, 600, "M4.6 image height"),
frameCount,
@@ -849,12 +853,14 @@ export async function fetchM4ThreatTimelineChunk(
endpointRoot = M4_THREAT_TIMELINE_ENDPOINT_ROOT,
cameraObstacleProjectionDelivery = null,
playbackPointPack,
includePoints = true,
}: {
fetcher?: LaboratoryFetch;
signal?: AbortSignal;
endpointRoot?: string;
cameraObstacleProjectionDelivery?: M4ThreatTimeline["cameraObstacleProjectionDelivery"];
playbackPointPack?: M4ThreatPlaybackPointPack;
includePoints?: boolean;
} = {},
): Promise<M4ThreatTimelineChunk> {
const params = new URLSearchParams({
@@ -864,7 +870,7 @@ export async function fetchM4ThreatTimelineChunk(
if (cameraObstacleProjectionDelivery !== null) {
params.set("obstacle_projection", cameraObstacleProjectionDelivery);
}
if (playbackPointPack) params.set("include_points", "false");
if (playbackPointPack || !includePoints) params.set("include_points", "false");
const response = await fetcher(
`${endpointRoot}/${result}/timeline/chunk?${params}`,
{ headers: { Accept: "application/json" }, signal },
@@ -995,7 +1001,10 @@ export async function fetchM4ThreatPlaybackManifest(
exact(manifest.result_id, result, "M4.6 playback result");
exact(manifest.coordinate_frame, "map", "M4.6 playback coordinate frame");
exact(manifest.access, "read-only-sealed-binary-playback", "M4.6 playback access");
const frameCount = exact(integer(manifest.frame_count, "M4.6 playback frames"), 4489, "M4.6 playback frames");
const frameCount = integer(manifest.frame_count, "M4.6 playback frames");
if (frameCount < 1) {
throw new M4ThreatContractError("M4.6 playback frames: пустой playback недопустим.");
}
const pointCount = integer(manifest.point_count, "M4.6 playback points");
const offsetsRaw = array(manifest.point_offsets, "M4.6 playback offsets");
if (offsetsRaw.length !== frameCount + 1) {
@@ -1332,6 +1341,17 @@ function parseTimelineFrame(
"current-increment",
"M4.6 timeline point layer",
),
localSlamBodyXyzM: item.local_slam_body_xyz_m === undefined
? []
: array(item.local_slam_body_xyz_m, "M4.6 local SLAM points").map(
(point) => vector(point, 3, "M4.6 local SLAM point") as [number, number, number],
),
localSlamSourceFrameCount: item.local_slam_source_frame_count === undefined
? undefined
: integer(item.local_slam_source_frame_count, "M4.6 local SLAM source frames"),
localSlamSourcePointCount: item.local_slam_source_point_count === undefined
? undefined
: integer(item.local_slam_source_point_count, "M4.6 local SLAM source points"),
cameraProjectedPointsXyd: item.camera_projected_points_xyd === undefined
? []
: array(item.camera_projected_points_xyd, "M4.6 camera points").map(
@@ -0,0 +1,57 @@
import {
LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE,
type LaboratoryRecordedEvidenceViewerProfile,
} from "../observation/viewerProfile";
export type LaboratoryRecordedMediaMode = "video" | "camera" | null;
export type LaboratoryRecordedSpatialMode = "3d" | "plan" | null;
export type LaboratoryClassifiedSpatialMode = "none" | "overlay" | "replace-source";
export interface LaboratoryRecordedEvidenceVisibility {
mediaMode: LaboratoryRecordedMediaMode;
spatialMode: LaboratoryRecordedSpatialMode;
showMediaSemantic: boolean;
showSpatialSemantic: boolean;
showMediaPoints: boolean;
classifiedSpatialMode: LaboratoryClassifiedSpatialMode;
}
export interface LaboratoryRecordedEvidenceDemand {
sourceTimelineMetadata: true;
recordedVideo: boolean;
exactCameraFrame: boolean;
sourceSpatialPoints: boolean;
cameraPointOverlay: boolean;
selectedSemanticMask: boolean;
selectedSemanticPoints: boolean;
classifiedSpatial: boolean;
}
/**
* Translate the already-visible M4 evidence composition into explicit data
* demand. The profile never starts a hidden point, semantic or camera channel
* merely because the selected LAB happens to publish that artifact.
*/
export function laboratoryRecordedEvidenceDemand(
visibility: LaboratoryRecordedEvidenceVisibility,
profile: LaboratoryRecordedEvidenceViewerProfile =
LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE,
): LaboratoryRecordedEvidenceDemand {
if (profile.loadPolicy !== "explicit-legacy-comparison-only") {
throw new Error("Unsupported LAB recorded evidence load policy");
}
const mediaVisible = visibility.mediaMode !== null;
const spatialVisible = visibility.spatialMode !== null;
return {
sourceTimelineMetadata: true,
recordedVideo: visibility.mediaMode === "video",
exactCameraFrame: visibility.mediaMode === "camera",
sourceSpatialPoints:
spatialVisible && visibility.classifiedSpatialMode !== "replace-source",
cameraPointOverlay: mediaVisible && visibility.showMediaPoints,
selectedSemanticMask: mediaVisible && visibility.showMediaSemantic,
selectedSemanticPoints: spatialVisible && visibility.showSpatialSemantic,
classifiedSpatial:
spatialVisible && visibility.classifiedSpatialMode !== "none",
};
}
@@ -106,6 +106,18 @@ export interface VegetationMixedRouteReview {
cases: readonly VegetationMixedRouteCase[];
}
export interface VegetationRouteTgsAnchor {
sourceSequence: number;
slot: number;
currentPointsXyzM: readonly (readonly [number, number, number])[];
costmap: {
cellSizeM: 0.45;
centersXyM: readonly (readonly [number, number])[];
stateCodes: readonly number[];
zBoundsM: readonly (readonly [number | null, number | null])[];
};
}
export interface VegetationFullRouteLayer {
name: string;
resultId: string;
@@ -119,6 +131,7 @@ export interface VegetationFullRouteLayer {
export interface VegetationFullRouteReview {
sourceId: "RAVNOVES004TREE";
sessionId: "20260828T130511Z_viewer_live";
linkedRouteReviewResultId: string;
sourceJobId: "recorded-camera-eb2783c5480d56bda07c8af0";
sourceJobInputSha256: string;
sourceStreamSha256: string;
@@ -692,6 +705,15 @@ function fullRouteReviewValue(value: unknown): VegetationFullRouteReview | null
"recorded-camera-eb2783c5480d56bda07c8af0",
"vegetation.route_full_review.source_job_id",
);
const linkedRouteReviewResultId = textValue(
row.linked_route_review_result_id,
"vegetation.route_full_review.linked_route_review_result_id",
);
if (!RESULT_ID.test(linkedRouteReviewResultId)) {
throw new VegetationShadowContractError(
"vegetation.route_full_review: linked route review identity invalid.",
);
}
exact(row.frame_count, 6830, "vegetation.route_full_review.frame_count");
exact(row.width, 800, "vegetation.route_full_review.width");
exact(row.height, 600, "vegetation.route_full_review.height");
@@ -810,6 +832,7 @@ function fullRouteReviewValue(value: unknown): VegetationFullRouteReview | null
return {
sourceId: "RAVNOVES004TREE",
sessionId: "20260828T130511Z_viewer_live",
linkedRouteReviewResultId,
sourceJobId: "recorded-camera-eb2783c5480d56bda07c8af0",
sourceJobInputSha256,
sourceStreamSha256,
@@ -931,6 +954,78 @@ export function vegetationFullRouteMaskUrl(
return `/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}/route-masks/${layer}/${sequence}`;
}
export async function fetchVegetationRouteTgsAnchor(
resultId: string,
sourceSequence: number,
{
fetcher = fetch,
signal,
}: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
): Promise<VegetationRouteTgsAnchor> {
if (!RESULT_ID.test(resultId) || !Number.isInteger(sourceSequence) || sourceSequence < 1) {
throw new VegetationShadowContractError("Vegetation TGS anchor identity недопустима.");
}
const response = await fetcher(
`/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}`
+ `/route-tgs-anchor/${sourceSequence}`,
{ method: "GET", headers: { Accept: "application/json" }, signal },
);
if (!response.ok) {
throw new VegetationShadowContractError(`Vegetation TGS anchor недоступен: HTTP ${response.status}.`);
}
const payload = objectValue(await response.json(), "vegetation.route_tgs_anchor");
exact(
payload.schema_version,
"missioncore.lab-v1-route-tgs-anchor/v1",
"vegetation.route_tgs_anchor.schema_version",
);
exact(payload.source_sequence, sourceSequence, "vegetation.route_tgs_anchor.source_sequence");
const pointValue = (value: unknown, label: string): readonly number[] => {
const point = arrayValue(value, label).map((item, index) => numberValue(item, `${label}[${index}]`));
if (point.length !== 2 && point.length !== 3) {
throw new VegetationShadowContractError(`${label}: размер изменён.`);
}
return point;
};
const points = arrayValue(payload.current_points_xyz_m, "vegetation.route_tgs_anchor.points")
.map((value, index) => pointValue(value, `vegetation.route_tgs_anchor.points[${index}]`));
const costmap = objectValue(payload.costmap, "vegetation.route_tgs_anchor.costmap");
exact(costmap.cell_size_m, 0.45, "vegetation.route_tgs_anchor.costmap.cell_size_m");
const centers = arrayValue(costmap.centers_xy_m, "vegetation.route_tgs_anchor.costmap.centers")
.map((value, index) => pointValue(value, `vegetation.route_tgs_anchor.costmap.centers[${index}]`));
const stateCodes = arrayValue(costmap.state_codes, "vegetation.route_tgs_anchor.costmap.states")
.map((value, index) => integerValue(value, `vegetation.route_tgs_anchor.costmap.states[${index}]`));
const zBounds = arrayValue(costmap.z_bounds_m, "vegetation.route_tgs_anchor.costmap.z_bounds")
.map((value, index) => {
const row = arrayValue(value, `vegetation.route_tgs_anchor.costmap.z_bounds[${index}]`);
if (row.length !== 2 || row.some((item) => item !== null && (typeof item !== "number" || !Number.isFinite(item)))) {
throw new VegetationShadowContractError("vegetation.route_tgs_anchor.costmap.z_bounds: контракт изменён.");
}
return row as readonly [number | null, number | null];
});
if (
centers.length !== 2244
|| stateCodes.length !== 2244
|| zBounds.length !== 2244
|| stateCodes.some((value) => value > 3)
|| points.some((point) => point.length !== 3)
|| centers.some((point) => point.length !== 2)
) {
throw new VegetationShadowContractError("Vegetation TGS anchor shape изменён.");
}
return {
sourceSequence,
slot: integerValue(payload.slot, "vegetation.route_tgs_anchor.slot"),
currentPointsXyzM: points.map((point) => [point[0]!, point[1]!, point[2]!] as const),
costmap: {
cellSizeM: 0.45,
centersXyM: centers.map((point) => [point[0]!, point[1]!] as const),
stateCodes,
zBoundsM: zBounds,
},
};
}
export async function fetchVegetationShadowResult(
resultId: string,
{
@@ -0,0 +1,70 @@
import { isUsableRecordedPlaybackRange } from "./recordedRerunLifecycle";
export function createReentrantViewerDisposer(
cleanupOnce: () => void,
releaseNativeViewer: () => void,
): () => void {
let cleanupComplete = false;
return () => {
try {
if (!cleanupComplete) {
cleanupComplete = true;
cleanupOnce();
}
} finally {
// A deferred native start can resolve after a pre-ready stop. Reapply
// release at every boundary so it cannot reopen after React unmount.
releaseNativeViewer();
}
};
}
interface ActiveLiveViewerOwner {
release: () => void;
}
let activeLiveViewerOwner: ActiveLiveViewerOwner | null = null;
/** Own exactly one native live receiver per application document. */
export function claimExclusiveLiveViewer(release: () => void): () => void {
const owner = { release };
const previous = activeLiveViewerOwner;
activeLiveViewerOwner = owner;
previous?.release();
return () => {
if (activeLiveViewerOwner === owner) activeLiveViewerOwner = null;
};
}
export function isLiveRerunPresentationReady(
viewerStarted: boolean,
rangeNs: { min: number; max: number } | null,
backendActivitySequence: number | null,
): boolean {
return viewerStarted
&& Number.isSafeInteger(backendActivitySequence)
&& (backendActivitySequence ?? 0) > 0
&& isUsableRecordedPlaybackRange(rangeNs);
}
export function liveTimelineNeedsSynchronization(
followLive: boolean,
activeTimeline: string | null | undefined,
rangeNs: { min: number; max: number } | null,
): boolean {
return followLive
&& isUsableRecordedPlaybackRange(rangeNs)
&& activeTimeline !== "stream_time";
}
export function liveRerunReceiverBindingIdentity(
sourceUrl: string,
liveStreamId: string | null,
followLive: boolean,
): string {
return JSON.stringify([
followLive ? "live" : "recorded",
sourceUrl.trim(),
followLive ? liveStreamId?.trim() ?? "" : "",
]);
}
@@ -327,6 +327,53 @@ export function verifyLiveViewerClientBuild(
});
}
/**
* Recover a recorded viewer whose lazy module disappeared during a frontend
* deployment. Live acquisition deliberately never reloads automatically, but
* a saved recording has no device-side authority to preserve and can safely
* move to the current immutable application build.
*/
export async function reloadRecordedViewerAfterStaleModuleFailure({
loadedUiBuildId,
signal,
fetcher = globalThis.fetch,
reload = () => window.location.reload(),
}: {
loadedUiBuildId: string;
signal?: AbortSignal;
fetcher?: typeof globalThis.fetch;
reload?: () => void;
}): Promise<boolean> {
if (
signal?.aborted ||
loadedUiBuildId === DEVELOPMENT_UI_BUILD_ID ||
!HASHED_UI_BUILD_ID.test(loadedUiBuildId)
) return false;
try {
const response = await fetcher("/api/v1/viewer/client-contract", {
method: "GET",
headers: {
Accept: "application/json",
"X-MissionCore-UI-Build": loadedUiBuildId,
},
cache: "no-store",
signal,
});
if (signal?.aborted || !response.ok) return false;
const expectedUiBuildId = response.headers.get(UI_BUILD_HEADER);
if (
!expectedUiBuildId ||
expectedUiBuildId === loadedUiBuildId ||
!HASHED_UI_BUILD_ID.test(expectedUiBuildId)
) return false;
browserUiBuildCoordinator().report({ loadedUiBuildId, expectedUiBuildId });
reload();
return true;
} catch {
return false;
}
}
function startBuildMonitor(): void {
if (buildMonitorAbort || typeof window === "undefined") return;
const lineage = createLiveViewerLineage(createLiveViewerInstanceId(), 1);
@@ -0,0 +1,303 @@
import type { SceneSettings } from "../../sceneSettings";
import type { RecordedAdmissionPhase } from "./recordedSessionAdmission";
import type { RerunPlaybackState, RerunViewportStatus } from "./viewerProfile";
const BUFFER_END_TOLERANCE_NS = 1_000_000;
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;
export const RECORDED_BASE_POINT_COLOR_KEY = "intensity|turbo|-";
export interface RecordedPlaybackBufferState {
bufferedEndNs: number | null;
expectedStartNs: number | null;
expectedEndNs: number | null;
bufferProgress: number | null;
fullyBuffered: boolean;
}
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) {
throw new Error("Unsafe recorded RRD byte length");
}
const transferBudgetMs = Math.ceil(
(byteLength / RECORDED_OPEN_MIN_BYTES_PER_SECOND) * 1_000,
);
return Math.min(
RECORDED_OPEN_MAX_TIMEOUT_MS,
Math.max(RECORDED_OPEN_MIN_TIMEOUT_MS, transferBudgetMs + RECORDED_OPEN_GRACE_MS),
);
}
export function createRecordedOpenWatchdog<T>({
byteLength,
schedule,
cancel,
onTimeout,
}: {
byteLength: number;
schedule: (callback: () => void, timeoutMs: number) => T;
cancel: (handle: T) => void;
onTimeout: () => void;
}): { arm: () => void; clear: () => void; pending: () => boolean } {
const timeoutMs = recordedOpenWatchdogTimeoutMs(byteLength);
let handle: T | null = null;
return {
arm() {
if (handle !== null) return;
handle = schedule(() => {
handle = null;
onTimeout();
}, timeoutMs);
},
clear() {
if (handle === null) return;
cancel(handle);
handle = null;
},
pending: () => handle !== null,
};
}
export function isUsableRecordedPlaybackRange(
rangeNs: { min: number; max: number } | null,
): rangeNs is { min: number; max: number } {
return Boolean(
rangeNs
&& Number.isFinite(rangeNs.min)
&& Number.isFinite(rangeNs.max)
&& rangeNs.max >= rangeNs.min,
);
}
export function recordedPlaybackBufferState(
rangeNs: { min: number; max: number } | null,
expectedTimelineEndSeconds?: number,
expectedTimelineStartSeconds?: number,
): RecordedPlaybackBufferState {
const usableRange = isUsableRecordedPlaybackRange(rangeNs) ? rangeNs : null;
const bufferedEndNs = usableRange?.max ?? null;
if (expectedTimelineEndSeconds === undefined) {
return {
bufferedEndNs,
expectedStartNs: null,
expectedEndNs: null,
bufferProgress: null,
fullyBuffered: usableRange !== null,
};
}
if (!Number.isFinite(expectedTimelineEndSeconds) || expectedTimelineEndSeconds < 0) {
return {
bufferedEndNs,
expectedStartNs: null,
expectedEndNs: null,
bufferProgress: null,
fullyBuffered: false,
};
}
const expectedEndNs = expectedTimelineEndSeconds * 1_000_000_000;
const expectedStartNs = expectedTimelineStartSeconds === undefined
? 0
: expectedTimelineStartSeconds * 1_000_000_000;
if (
!Number.isFinite(expectedEndNs)
|| !Number.isFinite(expectedStartNs)
|| expectedStartNs < 0
|| expectedEndNs < expectedStartNs
) {
return {
bufferedEndNs,
expectedStartNs: null,
expectedEndNs: null,
bufferProgress: null,
fullyBuffered: false,
};
}
const fullyBuffered = usableRange !== null
&& usableRange.min <= expectedStartNs + BUFFER_END_TOLERANCE_NS
&& usableRange.max >= expectedEndNs - BUFFER_END_TOLERANCE_NS;
const expectedDurationNs = expectedEndNs - expectedStartNs;
const bufferProgress = bufferedEndNs === null
? 0
: expectedDurationNs <= 0
? (fullyBuffered ? 1 : 0)
: Math.min(1, Math.max(0, (bufferedEndNs - expectedStartNs) / expectedDurationNs));
return {
bufferedEndNs,
expectedStartNs,
expectedEndNs,
bufferProgress,
fullyBuffered,
};
}
export function isRecordedPlaybackReady(
viewerStarted: boolean,
artifactVerified: boolean,
buffer: RecordedPlaybackBufferState,
): boolean {
return viewerStarted && artifactVerified && buffer.bufferedEndNs !== null;
}
export function recordedPlaybackRangeWhenReady(
rangeNs: { min: number; max: number } | null,
buffer: RecordedPlaybackBufferState,
artifactVerified: boolean,
): { min: number; max: number } | null {
if (
!artifactVerified
|| !buffer.fullyBuffered
|| !isUsableRecordedPlaybackRange(rangeNs)
) return null;
return {
min: buffer.expectedStartNs === null
? rangeNs.min
: Math.max(rangeNs.min, buffer.expectedStartNs),
max: buffer.expectedEndNs === null
? rangeNs.max
: Math.min(rangeNs.max, buffer.expectedEndNs),
};
}
export function isRecordedPlaybackPresentationReady(
status: RerunViewportStatus,
playback: RerunPlaybackState | null,
): boolean {
return status === "ready"
&& playback?.fullyBuffered === true
&& isUsableRecordedPlaybackRange(playback.rangeNs);
}
export function rerunPresentationStatus(
status: RerunViewportStatus,
_gate: RecordedAdmissionPhase,
recorded: boolean,
): RerunViewportStatus {
if (!recorded) return status;
return status === "error" ? "error" : status;
}
export function isRecordedPlaybackFullyBuffered(
rangeNs: { min: number; max: number } | null,
expectedTimelineEndSeconds?: number,
): boolean {
return recordedPlaybackBufferState(rangeNs, expectedTimelineEndSeconds).fullyBuffered;
}
export function attemptRecordedAutoplay(
seekToStart: () => void,
startPlaying: () => void,
): boolean {
try {
seekToStart();
startPlaying();
return true;
} catch {
return false;
}
}
export function createRecordedInitialSeekGate(): {
attempt: (
viewerStarted: boolean,
fullyBuffered: boolean,
presentationReady: boolean,
rangeNs: { min: number; max: number } | null,
seekToStart: (startNs: number) => void,
preferredStartNs?: number,
) => boolean;
attempted: () => boolean;
} {
let consumed = false;
return {
attempt(
viewerStarted,
fullyBuffered,
presentationReady,
rangeNs,
seekToStart,
preferredStartNs,
) {
if (
consumed
|| !viewerStarted
|| !fullyBuffered
|| !presentationReady
|| !isUsableRecordedPlaybackRange(rangeNs)
) return false;
consumed = true;
const startNs = Number.isFinite(preferredStartNs)
? Math.min(Math.max(preferredStartNs as number, rangeNs.min), rangeNs.max)
: rangeNs.min;
try {
seekToStart(startNs);
return true;
} catch {
return false;
}
},
attempted: () => consumed,
};
}
export function createRecordedAutoplayGate(): {
attempt: (
viewerStarted: boolean,
fullyBuffered: boolean,
presentationReady: boolean,
rangeNs: { min: number; max: number } | null,
seekToStart: (startNs: number) => void,
startPlaying: () => void,
preferredStartNs?: number,
) => boolean;
attempted: () => boolean;
} {
let consumed = false;
return {
attempt(
viewerStarted,
fullyBuffered,
presentationReady,
rangeNs,
seekToStart,
startPlaying,
preferredStartNs,
) {
if (
consumed
|| !viewerStarted
|| !fullyBuffered
|| !presentationReady
|| !isUsableRecordedPlaybackRange(rangeNs)
) return false;
consumed = true;
const startNs = Number.isFinite(preferredStartNs)
? Math.min(Math.max(preferredStartNs as number, rangeNs.min), rangeNs.max)
: rangeNs.min;
return attemptRecordedAutoplay(
() => seekToStart(startNs),
startPlaying,
);
},
attempted: () => consumed,
};
}
export function canPublishRecordedPlaybackController(
readyToRender: boolean,
presentationGate: RecordedAdmissionPhase,
): boolean {
return readyToRender && presentationGate === "ready";
}
@@ -0,0 +1,125 @@
import type { RecordedAdmissionPhase } from "./recordedSessionAdmission";
export type RerunViewportStatus = "idle" | "loading" | "ready" | "error";
export type RecordedRerunView = "spatial" | "perception" | "perception3d" | "metrics";
export interface RerunPlaybackState {
recordingId: string;
timeline: string;
rangeNs: { min: number; max: number } | null;
currentNs: number;
playing: boolean;
bufferedEndNs: number | null;
expectedStartNs: number | null;
expectedEndNs: number | null;
bufferProgress: number | null;
fullyBuffered: boolean;
}
export interface RerunPlaybackController {
seek: (timeNs: number) => void;
setPlaying: (playing: boolean) => void;
jumpToEnd: () => void;
}
export interface RecordedPerceptionLayers {
enabled: boolean;
detections2d: boolean;
segmentation: boolean;
cuboids3d: boolean;
}
export interface RecordedRrdArtifactDescriptor {
sourceUrl: string;
viewerSourceUrl: string;
byteLength: number;
sha256: string;
}
/**
* The native live receiver owns one acquisition lineage and never exposes
* recorded transport or autoplay policy.
*/
export interface LiveAcquisitionRerunProfile {
kind: "live-acquisition";
clock: "stream_time";
sourceUrl: string;
liveActivitySequence: number | null;
liveStreamId: string | null;
liveRecoveryAuthorityIdentity: string | null;
}
/**
* A sealed session is progressively presentable, while its shared playback
* controls remain fenced by the aggregate session admission gate.
*/
export interface RecordedSessionRerunProfile {
kind: "recorded-session";
clock: "session_time";
sourceUrl: string;
artifact: RecordedRrdArtifactDescriptor | null;
autoplayWhenReady: boolean;
presentationGate: RecordedAdmissionPhase;
expectedTimelineStartSeconds?: number;
expectedTimelineEndSeconds?: number;
initialPlaybackStartSeconds?: number;
view: RecordedRerunView;
viewResetGeneration: 0 | 1;
followTrajectory: boolean;
perceptionLayers: RecordedPerceptionLayers;
/** Explicit small blueprint endpoint when the viewer source is a merged LAB RRD. */
blueprintSourceUrl?: string;
/** Optional immutable RRD sidecar for LAB/model evidence on the same recording clock. */
perceptionSourceUrl?: string;
/** Selects one semantic entity without changing the sealed sidecar. */
semanticLayer?: "city" | "vegetation";
/** Keeps one native Rerun store/viewer while presenting the accepted two-pane LAB layout. */
unifiedPerception?: boolean;
/** Requests the canonical top-down eye without changing the world coordinate frame. */
planView?: boolean;
perceptionRetryGeneration: number;
lockPerceptionCameraInteraction: boolean;
}
/**
* Deprecated comparison-only contract for LAB artifacts that have not yet
* been republished as a native Rerun sidecar. It must never be selected by a
* canonical LAB route or start work in the background.
*/
export interface LaboratoryRecordedEvidenceViewerProfile {
kind: "lab-recorded-evidence";
clock: "source-sequence";
cameraTransport: "generation-bound-fmp4";
spatialTransport: "bounded-sealed-artifacts";
loadPolicy: "explicit-legacy-comparison-only";
workerRequired: false;
}
export type RerunViewerProfile =
| LiveAcquisitionRerunProfile
| RecordedSessionRerunProfile;
export type ObservationViewerProfile =
| RerunViewerProfile
| LaboratoryRecordedEvidenceViewerProfile;
export const LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE = Object.freeze({
kind: "lab-recorded-evidence",
clock: "source-sequence",
cameraTransport: "generation-bound-fmp4",
spatialTransport: "bounded-sealed-artifacts",
loadPolicy: "explicit-legacy-comparison-only",
workerRequired: false,
} satisfies LaboratoryRecordedEvidenceViewerProfile);
export function liveAcquisitionRerunProfile(
input: Omit<LiveAcquisitionRerunProfile, "kind" | "clock">,
): LiveAcquisitionRerunProfile {
return { kind: "live-acquisition", clock: "stream_time", ...input };
}
export function recordedSessionRerunProfile(
input: Omit<RecordedSessionRerunProfile, "kind" | "clock">,
): RecordedSessionRerunProfile {
return { kind: "recorded-session", clock: "session_time", ...input };
}
@@ -27,6 +27,41 @@
display: block;
}
.m4-replay-threat-visual__unified-content {
position: absolute;
z-index: 1;
inset: 0;
min-width: 0;
min-height: 0;
}
.m4-replay-threat-visual__unified-content > *,
.m4-replay-threat-visual__unified-content .rerun-viewport {
width: 100%;
height: 100%;
}
.m4-replay-threat-visual__deck[data-native-rerun="true"] > .nodedc-split-pane {
position: relative;
z-index: 2;
pointer-events: none;
}
.m4-replay-threat-visual__deck[data-native-rerun="true"]
.m4-replay-threat-visual__pane {
background: transparent;
pointer-events: none;
}
.m4-replay-threat-visual__deck[data-native-rerun="true"]
.nodedc-split-pane__separator,
.m4-replay-threat-visual__deck[data-native-rerun="true"]
.m4-replay-threat-visual__pane-toolbar,
.m4-replay-threat-visual__deck[data-native-rerun="true"]
.m4-replay-threat-visual__pane-toolbar * {
pointer-events: auto;
}
.m4-replay-threat-visual__deck[data-empty="true"] > .l3-visual-audit__state {
position: absolute;
z-index: 1;
+1 -1
View File
@@ -78,7 +78,7 @@
overflow: hidden;
}
/* Rerun WebViewer 0.34.1 keeps three fixed 24px canvas rows even after its
/* The upstream Rerun canvas keeps three fixed 24px rows even after its
panels are overridden: the native top row, recording tab and view tab.
They are drawn inside WASM and cannot be styled independently, so crop the
fixed native chrome while keeping the actual 3D viewport full-height. */
@@ -12,6 +12,7 @@ import type {
RecordedCameraAdmissionState,
} from "../core/observation/recordedSessionAdmission";
import { liveRerunRecoveryAuthorityIdentity } from "../core/observation/liveReceiverWatchdog";
import { liveAcquisitionRerunProfile, recordedSessionRerunProfile } from "../core/observation/viewerProfile";
import type { ObservationSourceDescriptor } from "../core/runtime/contracts";
import {
RerunViewport,
@@ -46,7 +47,6 @@ function statusTone(status: CapabilityStatus): "success" | "accent" | "warning"
if (status === "contract") return "warning";
return "neutral";
}
function FeatureInventory({ definition }: { definition: WorkspaceDefinition }) {
return (
<div className="feature-inventory">
@@ -76,7 +76,6 @@ function FeatureInventory({ definition }: { definition: WorkspaceDefinition }) {
</div>
);
}
function WorkspaceLead({ definition, note }: { definition: WorkspaceDefinition; note?: string }) {
return (
<section className="workspace-lead workspace-lead--compact">
@@ -89,7 +88,6 @@ function WorkspaceLead({ definition, note }: { definition: WorkspaceDefinition;
</section>
);
}
function EmptySpatialStage({ settings }: { settings: SceneSettings }) {
return (
<div className="empty-spatial-stage" data-grid={settings.showGrid ? "true" : undefined}>
@@ -181,7 +179,7 @@ function SpatialWorkspace({
);
const recordedPerceptionSupported =
recordedSource && perceptionLoad.phase !== "unavailable";
const recordedPerceptionReady = recordedSource && perceptionLoad.phase === "ready";
const recordedPerceptionLoading = recordedSource && perceptionLoad.phase === "loading";
const recordedPerceptionEnabled =
showDetections2d || showSegmentation || showCuboids3d;
// The native recorded camera remains the authoritative original. Only 2D
@@ -261,7 +259,7 @@ function SpatialWorkspace({
const shouldPrepareRecordedSource = useCallback((sourceId: string) => {
if (!recordedSessionAdmission) return false;
return recordedSessionAdmission.activeCameraSourceIds.has(sourceId) ||
recordedSessionAdmission.cameras[sourceId]?.phase === "ready";
["ready", "error"].includes(recordedSessionAdmission.cameras[sourceId]?.phase ?? "loading");
}, [recordedSessionAdmission]);
const onSelectionChange = useCallback((next: RerunSelection | null) => setSelection(next), []);
const onPlaybackChange = useCallback(
@@ -381,6 +379,35 @@ function SpatialWorkspace({
: presentedViewerStatus === "error"
? "danger"
: "neutral";
const rerunViewerProfile = recordedSource
? recordedSessionRerunProfile({
sourceUrl,
artifact: recordedReplay,
autoplayWhenReady: true,
presentationGate: recordedSessionGate,
expectedTimelineStartSeconds: state?.observationTimeline?.range?.startSeconds,
expectedTimelineEndSeconds: state?.observationTimeline?.range?.endSeconds,
initialPlaybackStartSeconds: initialRecordedPlaybackStartSeconds,
view: "spatial",
viewResetGeneration: recordedViewResetGeneration,
followTrajectory: followRecordedTrajectory,
perceptionLayers: {
enabled: recordedPerceptionSupported && recordedPerceptionEnabled,
detections2d: showDetections2d,
segmentation: showSegmentation,
cuboids3d: showCuboids3d,
},
perceptionRetryGeneration,
lockPerceptionCameraInteraction: unifiedPerception,
})
: liveAcquisitionRerunProfile({
sourceUrl,
liveActivitySequence: livePresentationActivitySequence,
liveStreamId: state?.spatialSource?.id ?? null,
liveRecoveryAuthorityIdentity: streamActive
? liveRerunRecoveryAuthorityIdentity(pointCloudSource, state?.spatialSource)
: null,
});
return (
<div
@@ -405,7 +432,7 @@ function SpatialWorkspace({
variant={detections2dActive ? "primary" : "secondary"}
icon={<Icon name="target" />}
aria-pressed={detections2dActive}
disabled={recordedSource && !recordedPerceptionReady}
disabled={recordedPerceptionLoading}
onClick={() => recordedSource
? setShowDetections2d((current) => !current)
: onLivePerceptionLayersChange({
@@ -420,7 +447,7 @@ function SpatialWorkspace({
variant={segmentationActive ? "primary" : "secondary"}
icon={<Icon name="image" />}
aria-pressed={segmentationActive}
disabled={recordedSource && !recordedPerceptionReady}
disabled={recordedPerceptionLoading}
onClick={() => recordedSource
? setShowSegmentation((current) => !current)
: onLivePerceptionLayersChange({
@@ -435,7 +462,7 @@ function SpatialWorkspace({
variant={cuboids3dActive ? "primary" : "secondary"}
icon={<Icon name="apps" />}
aria-pressed={cuboids3dActive}
disabled={recordedSource && !recordedPerceptionReady}
disabled={recordedPerceptionLoading}
onClick={() => recordedSource
? setShowCuboids3d((current) => !current)
: onLivePerceptionLayersChange({
@@ -489,35 +516,8 @@ function SpatialWorkspace({
>
{sourceUrl.trim() && pointCloudVisible && !intentionalSourceEnd ? (
<RerunViewport
sourceUrl={sourceUrl}
recordedArtifact={recordedSource ? recordedReplay : null}
followLive={liveRerunSource}
liveActivitySequence={livePresentationActivitySequence}
liveStreamId={state?.spatialSource?.id}
liveRecoveryAuthorityIdentity={!recordedSource && streamActive ? liveRerunRecoveryAuthorityIdentity(pointCloudSource, state?.spatialSource) : null}
autoplayWhenReady={recordedSource}
presentationGate={recordedSessionGate}
expectedTimelineStartSeconds={recordedSource
? state?.observationTimeline?.range?.startSeconds
: undefined}
expectedTimelineEndSeconds={recordedSource
? state?.observationTimeline?.range?.endSeconds
: undefined}
initialPlaybackStartSeconds={initialRecordedPlaybackStartSeconds}
profile={rerunViewerProfile}
sceneSettings={sceneSettings}
recordedViewResetGeneration={recordedViewResetGeneration}
recordedFollowTrajectory={followRecordedTrajectory}
recordedPerceptionLayers={{
enabled:
recordedPerceptionSupported &&
recordedPerceptionReady &&
recordedPerceptionEnabled,
detections2d: showDetections2d,
segmentation: showSegmentation,
cuboids3d: showCuboids3d,
}}
recordedPerceptionRetryGeneration={perceptionRetryGeneration}
lockPerceptionCameraInteraction={unifiedPerception}
onPerceptionLoadChange={onPerceptionLoadChange}
onPointColorLoadChange={onPointColorLoadChange}
onStatusChange={onStatusChange}
@@ -905,7 +905,7 @@ function CamerasWorkspace({
if (!recordedReplay) return true;
if (!recordedSessionAdmission) return false;
return recordedSessionAdmission.activeCameraSourceIds.has(sourceId) ||
recordedSessionAdmission.cameras[sourceId]?.phase === "ready";
["ready", "error"].includes(recordedSessionAdmission.cameras[sourceId]?.phase ?? "loading");
}, [recordedReplay, recordedSessionAdmission]);
return (
<div className="standard-workspace cameras-workspace" data-focused={focusedSource ? "true" : undefined}>
@@ -0,0 +1,249 @@
import { useEffect, useMemo, useState } from "react";
import { Button, Icon, SegmentedControl } from "@nodedc/ui-react";
import { ObservationTimeline } from "../../components/ObservationTimeline";
import {
RerunViewport,
type RerunPlaybackController,
type RerunPlaybackState,
} from "../../components/RerunViewport";
import {
CanonicalRecordedLabReplay,
useCanonicalRecordedLabReplayState,
} from "../../components/laboratory/CanonicalRecordedLabReplay";
import {
resolveCanonicalLabReplay,
type CanonicalLabReplayDescriptor,
} from "../../core/laboratory/canonicalLabReplay";
import type { VegetationFullRouteReview } from "../../core/laboratory/vegetationShadow";
import type { ObservationSessionReplayLaunch } from "../../core/observation/sessionArchive";
import { recordedSessionRerunProfile } from "../../core/observation/viewerProfile";
import { resolveObservationSessionReplay } from "../../core/observation/useObservationSessions";
import { defaultSceneSettings } from "../../sceneSettings";
type MediaMode = "video" | "camera";
type SpatialMode = "3d" | "plan";
type SpatialLayer = "source" | "local" | "tgs" | "semantic";
type SemanticLayer = "city" | "vegetation";
interface CanonicalReplayLaunch {
base: ObservationSessionReplayLaunch;
replay: CanonicalLabReplayDescriptor;
}
export function CanonicalVegetationRerunReplay({
resultId,
review,
}: {
resultId: string;
review: VegetationFullRouteReview;
}) {
const {
mediaMode,
spatialMode,
splitPrimarySize,
splitOrientation,
expanded,
onMediaModeChange,
onSpatialModeChange,
onSplitPrimarySizeChange,
onExpandedChange,
} = useCanonicalRecordedLabReplayState<MediaMode, SpatialMode>({
initialMediaMode: "video",
initialSpatialMode: "3d",
});
const [semanticLayer, setSemanticLayer] = useState<SemanticLayer>("vegetation");
const [showSemantics, setShowSemantics] = useState(true);
const [spatialLayer, setSpatialLayer] = useState<SpatialLayer>("source");
const [viewResetGeneration, setViewResetGeneration] = useState<0 | 1>(0);
const [playback, setPlayback] = useState<RerunPlaybackState | null>(null);
const [playbackController, setPlaybackController] =
useState<RerunPlaybackController | null>(null);
const [launch, setLaunch] = useState<CanonicalReplayLaunch | null>(null);
const [launchError, setLaunchError] = useState<string | null>(null);
useEffect(() => {
const controller = new AbortController();
setLaunch(null);
setLaunchError(null);
void resolveObservationSessionReplay(review.sessionId, {
signal: controller.signal,
maximumWaitMs: 30 * 60 * 1000,
onUpdate: () => undefined,
}).then(async (value) => ({
base: value,
replay: await resolveCanonicalLabReplay(resultId, value, {
signal: controller.signal,
}),
})).then((value) => {
if (!controller.signal.aborted) setLaunch(value);
}).catch((caught: unknown) => {
if (!controller.signal.aborted) {
setLaunchError(
caught instanceof Error ? caught.message : "Каноническая запись RAV004 недоступна.",
);
}
});
return () => controller.abort();
}, [resultId, review.sessionId]);
const splitView = mediaMode !== null && spatialMode !== null;
const sceneSettings = useMemo(() => ({
...defaultSceneSettings,
accumulationSeconds: spatialLayer === "local" ? 5 : 0,
showPoints: spatialMode !== null,
showTrajectory: spatialMode !== null,
showGrid: spatialMode !== null,
pointSize: 3.8,
}), [spatialLayer, spatialMode]);
const profile = launch ? recordedSessionRerunProfile({
sourceUrl: launch.replay.sourceUrl,
artifact: {
sourceUrl: launch.replay.sourceUrl,
viewerSourceUrl: launch.replay.viewerSourceUrl,
byteLength: launch.replay.byteLength,
sha256: launch.replay.sha256,
},
blueprintSourceUrl: launch.replay.blueprintSourceUrl,
autoplayWhenReady: false,
presentationGate: "ready",
expectedTimelineStartSeconds: launch.base.timelineStartSeconds,
expectedTimelineEndSeconds: launch.base.timelineEndSeconds,
initialPlaybackStartSeconds: review.timelineStartSeconds,
view: mediaMode !== null ? "perception" : "spatial",
viewResetGeneration,
followTrajectory: true,
semanticLayer,
unifiedPerception: splitView,
planView: spatialMode === "plan",
perceptionLayers: {
enabled: mediaMode !== null,
detections2d: mediaMode === "video",
segmentation: mediaMode === "video" && showSemantics,
cuboids3d: false,
},
perceptionRetryGeneration: 0,
lockPerceptionCameraInteraction: false,
}) : null;
const mediaLayerControls = (
<div
className="m4-replay-threat-visual__pane-layer-controls"
role="group"
aria-label="Слои камеры и видео"
>
<Button
size="dense"
shape="pill"
variant={showSemantics ? "primary" : "secondary"}
aria-pressed={showSemantics}
onClick={() => setShowSemantics((visible) => !visible)}
>
СЕМАНТИКА
</Button>
<SegmentedControl
value={semanticLayer}
items={[
{ value: "city", label: "ГОРОД · EoMT" },
{ value: "vegetation", label: "ПРИРОДА · DDRNet" },
]}
label="Источник семантики"
size="dense"
onChange={(value) => {
setSemanticLayer(value);
setShowSemantics(true);
}}
/>
</div>
);
const spatialLayerControls = (
<div
className="m4-replay-threat-visual__pane-layer-controls"
role="group"
aria-label="Пространственные слои RAV004"
>
<SegmentedControl
value={spatialLayer}
items={[
{ value: "source", label: "ИСХ. ТОЧКИ" },
{ value: "local", label: "ЛОК. SLAM" },
{ value: "tgs", label: "TGS", disabled: true },
{ value: "semantic", label: "СЕМАНТИКА", disabled: true },
]}
label="Пространственные слои"
size="dense"
onChange={setSpatialLayer}
/>
</div>
);
const resetSpatialView = (
<Button
size="dense"
variant="ghost"
icon={<Icon name="refresh" size={14} />}
aria-label="Сбросить положение 3D камеры"
title="Сбросить положение 3D камеры"
onClick={() => setViewResetGeneration((value) => value === 0 ? 1 : 0)}
>
</Button>
);
const transport = playback && playbackController ? (
<ObservationTimeline
className="m4-replay-threat-visual__timeline"
active
sourceCount={3}
mode="recorded"
seekable
synchronization="shared-clock"
rangeNs={playback.rangeNs}
currentNs={playback.currentNs}
playing={playback.playing}
onSeek={playbackController.seek}
onPlayingChange={playbackController.setPlaying}
showJumpToEnd={false}
/>
) : undefined;
return (
<CanonicalRecordedLabReplay
label="RAVNOVES004TREE · канонический повтор Rerun"
mediaMode={mediaMode ?? "none"}
mediaModes={[
{ value: "video", label: "ВИДЕО" },
{ value: "camera", label: "КАМЕРА" },
]}
spatialMode={spatialMode ?? "none"}
spatialModes={[
{ value: "3d", label: "3D" },
{ value: "plan", label: "ПЛАН" },
]}
expanded={expanded}
splitPrimarySize={splitPrimarySize}
splitOrientation={splitOrientation}
mediaAriaLabel={mediaMode === "camera" ? "Камера" : "Видео и семантика"}
spatialAriaLabel={spatialMode === "plan" ? "Вид сверху" : "Трёхмерная сцена"}
mediaLayerControls={mediaLayerControls}
spatialLayerControls={spatialLayerControls}
spatialLeadingControl={resetSpatialView}
mediaMultiLayer
unifiedContent={profile ? (
<RerunViewport
profile={profile}
sceneSettings={sceneSettings}
onPlaybackChange={setPlayback}
onPlaybackControllerChange={setPlaybackController}
/>
) : (
<div className="l3-visual-audit__state" role={launchError ? "alert" : "status"}>
{launchError ?? "Готовим единый кэш канонического повтора RAV004…"}
</div>
)}
emptyMessage="Выберите ВИДЕО/КАМЕРА или 3D/ПЛАН. Общие часы Rerun останутся на месте."
transport={transport}
onMediaModeChange={onMediaModeChange}
onSpatialModeChange={onSpatialModeChange}
onExpandedChange={onExpandedChange}
onSplitPrimarySizeChange={onSplitPrimarySizeChange}
/>
);
}
@@ -215,12 +215,24 @@ export function M49TgsFullShadowEvidence({
controlLabel: semanticOverride.controlLabel ?? "ПРИРОДА · DDRNet",
}] : []),
], [semantic, semanticOverride]);
const spatialSemantic = useMemo<M4ReplayThreatSemanticLayer | undefined>(() => (
semantic ? {
id: "spatial-urban",
controlLabel: "SEMANTICS",
resultId: semantic.resultId,
spatialResultId: semantic.resultId,
taxonomy: semantic.taxonomy,
label: "EoMT Cityscapes semantic · point-aligned E47",
maskAriaLabel: "EoMT urban semantic prediction",
} : undefined
), [semantic]);
return (
<>
<M4ReplayThreatVisual
resultId={result.source.linkedVisualResultId}
semanticLayers={semanticLayers}
spatialSemantic={spatialSemantic}
initialSemanticLayerId={semanticOverride ? "vegetation" : "urban"}
showReviewAnchorBoxes={false}
reviewLabel="4 489 source-paced TGS frames"
@@ -1,15 +1,25 @@
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
type CSSProperties,
type ReactNode,
} from "react";
import {
Button,
Icon,
IconButton,
Select,
SegmentedControl,
SplitPane,
type SplitPaneOrientation,
} from "@nodedc/ui-react";
import { ObservationTimeline } from "../../components/ObservationTimeline";
import {
CanonicalRecordedLabReplay,
useCanonicalRecordedLabReplayState,
} from "../../components/laboratory/CanonicalRecordedLabReplay";
import {
LaboratoryMetricEvidenceScene,
type LaboratoryMetricCellEvidence,
@@ -35,6 +45,7 @@ import {
type E47SemanticClass,
type E47SemanticTimelineFrame,
} from "../../core/laboratory/e47SemanticSlam";
import { laboratoryRecordedEvidenceDemand } from "../../core/laboratory/recordedEvidenceProfile";
import type {
M4ThreatCameraProposal,
M4ThreatTimelineFrame,
@@ -52,8 +63,6 @@ import { useE47SemanticTimelineFrame } from "./useE47SemanticTimeline";
import { buildM4StaticObstacleBoxes } from "./m4StaticObstacleBoxes";
type M4ThreatMediaMode = "video" | "camera";
type M4ThreatMediaSelection = M4ThreatMediaMode | "none";
type M4ThreatSpatialSelection = LaboratoryMetricSceneMode | "none";
function toneForProposal(proposal: M4ThreatCameraProposal): RecordedEvidenceBox["tone"] {
if (proposal.threatDecision === "threat") return "danger";
@@ -144,6 +153,7 @@ export interface M4ReplayClassifiedSpatialLayer {
label: string;
pointLayerLabel: string;
cellLayerLabel: string;
cellLayerAvailable?: boolean;
expectedAtSequence: boolean;
frame: M4ReplayClassifiedSpatialFrame | null;
loading: boolean;
@@ -158,6 +168,7 @@ export function M4ReplayThreatVisual({
resultId,
semantic,
semanticLayers,
spatialSemantic,
initialSemanticLayerId,
reviewAnchors = EMPTY_REVIEW_ANCHORS,
showReviewAnchorBoxes = true,
@@ -168,11 +179,15 @@ export function M4ReplayThreatVisual({
classifiedSpatialLayer,
showReferenceMediaLayers = true,
showSpatialOverlaySummary = true,
playbackTransport = "epoch-stream",
spatialPlaybackTransport = "auto",
recoverTimestampStalls = false,
onActiveSequenceChange,
}: {
resultId: string;
semantic?: M4ReplayThreatSemanticLayer;
semanticLayers?: readonly M4ReplayThreatSemanticLayer[];
spatialSemantic?: M4ReplayThreatSemanticLayer;
initialSemanticLayerId?: string;
reviewAnchors?: readonly M4ReplayThreatReviewAnchor[];
showReviewAnchorBoxes?: boolean;
@@ -183,12 +198,26 @@ export function M4ReplayThreatVisual({
classifiedSpatialLayer?: M4ReplayClassifiedSpatialLayer;
showReferenceMediaLayers?: boolean;
showSpatialOverlaySummary?: boolean;
playbackTransport?: "segmented" | "epoch-stream";
spatialPlaybackTransport?: "auto" | "sealed-binary" | "json";
recoverTimestampStalls?: boolean;
onActiveSequenceChange?: (sequence: number | null) => void;
}) {
const [mediaMode, setMediaMode] = useState<M4ThreatMediaMode | null>("video");
const [spatialMode, setSpatialMode] = useState<LaboratoryMetricSceneMode | null>(
const {
mediaMode,
spatialMode,
splitView,
splitPrimarySize,
splitOrientation,
expanded,
onMediaModeChange: handleMediaModeChange,
onSpatialModeChange: handleSpatialModeChange,
onSplitPrimarySizeChange: setSplitPrimarySize,
onExpandedChange: setExpanded,
} = useCanonicalRecordedLabReplayState<M4ThreatMediaMode, LaboratoryMetricSceneMode>({
initialMediaMode: "video",
initialSpatialMode,
);
});
const [showCurrentIncrement, setShowCurrentIncrement] = useState(true);
const [showLocalSurface, setShowLocalSurface] = useState(true);
const [showRollingMap, setShowRollingMap] = useState(true);
@@ -197,13 +226,6 @@ export function M4ReplayThreatVisual({
const [showSpatialSemantic, setShowSpatialSemantic] = useState(true);
const [showMediaPoints, setShowMediaPoints] = useState(false);
const [showStaticObstacles, setShowStaticObstacles] = useState(true);
const [splitPrimarySize, setSplitPrimarySize] = useState(50);
const [splitOrientation, setSplitOrientation] = useState<SplitPaneOrientation>(() => (
typeof window !== "undefined" && window.matchMedia("(max-width: 900px)").matches
? "horizontal"
: "vertical"
));
const [expanded, setExpanded] = useState(false);
const [selectedReviewAnchorIndex, setSelectedReviewAnchorIndex] = useState(0);
const availableSemanticLayers = useMemo<readonly M4ReplayThreatSemanticLayer[]>(
() => semanticLayers?.length ? semanticLayers : semantic ? [semantic] : [],
@@ -234,6 +256,28 @@ export function M4ReplayThreatVisual({
const activeSemantic = availableSemanticLayers.find(
(layer, index) => (layer.id ?? `${layer.resultId}:${index}`) === selectedSemanticLayerId,
) ?? availableSemanticLayers[0];
const activeSpatialSemantic = spatialSemantic ?? activeSemantic;
const evidenceDemand = useMemo(() => laboratoryRecordedEvidenceDemand({
mediaMode,
spatialMode,
showMediaSemantic: Boolean(activeSemantic) && showMediaSemantic,
showSpatialSemantic: Boolean(activeSpatialSemantic) && showSpatialSemantic,
showMediaPoints,
classifiedSpatialMode: !classifiedSpatialLayer || classifiedSpatialLayer.cellLayerAvailable === false
? "none"
: classifiedSpatialLayer.replacePointCloud
? "replace-source"
: "overlay",
}), [
activeSemantic,
activeSpatialSemantic,
classifiedSpatialLayer,
mediaMode,
showMediaPoints,
showMediaSemantic,
showSpatialSemantic,
spatialMode,
]);
const metricSceneRef = useRef<LaboratoryMetricEvidenceSceneHandle | null>(null);
const metadata = useM4ThreatTimelineMetadata(resultId, timelineEndpointRoot);
const playbackRange = useMemo(() => metadata.timeline ? ({
@@ -241,7 +285,7 @@ export function M4ReplayThreatVisual({
endSeconds: metadata.timeline.timelineEndSeconds,
}) : null, [metadata.timeline]);
const playbackController = useRecordedEvidencePlayback(playbackRange, {
clock: mediaMode === "video" ? "external" : "animation",
clock: "external",
});
const seekPlayback = playbackController.seek;
const setPlaybackPlaying = playbackController.setPlaying;
@@ -249,7 +293,9 @@ export function M4ReplayThreatVisual({
resultId,
timeline: metadata.timeline,
currentSeconds: playbackController.playback.currentSeconds,
includeSpatialPoints: evidenceDemand.sourceSpatialPoints,
endpointRoot: timelineEndpointRoot,
spatialPlaybackTransport,
});
const [videoSource, setVideoSource] = useState<ObservationSourceDescriptor | null>(null);
const [videoLoading, setVideoLoading] = useState(false);
@@ -260,16 +306,12 @@ export function M4ReplayThreatVisual({
setVideoError(null);
}, [resultId]);
useEffect(() => {
const query = window.matchMedia("(max-width: 900px)");
const update = () => setSplitOrientation(query.matches ? "horizontal" : "vertical");
update();
query.addEventListener("change", update);
return () => query.removeEventListener("change", update);
}, []);
useEffect(() => {
const timeline = metadata.timeline;
if (!evidenceDemand.recordedVideo) {
setVideoLoading(false);
return;
}
if (!timeline || videoSource) return;
const controller = new AbortController();
setVideoLoading(true);
@@ -304,7 +346,7 @@ export function M4ReplayThreatVisual({
if (!controller.signal.aborted) setVideoLoading(false);
});
return () => controller.abort();
}, [metadata.timeline, videoSource]);
}, [evidenceDemand.recordedVideo, metadata.timeline, videoSource]);
const lastFrameRef = useRef<M4ThreatTimelineFrame | null>(null);
useEffect(() => {
@@ -319,41 +361,53 @@ export function M4ReplayThreatVisual({
resultId: string;
frame: M4ThreatTimelineFrame;
} | null>(null);
if (frame?.spatialAvailable) {
lastSpatialFrameRef.current = { resultId, frame };
useEffect(() => {
lastSpatialFrameRef.current = null;
}, [evidenceDemand.sourceSpatialPoints, resultId]);
const latestAvailableSpatialFrame = [...timelineFrame.availableFrames]
.reverse()
.find((candidate) => (
candidate.spatialAvailable
&& (timelineFrame.activeSequence === null
|| candidate.sequence <= timelineFrame.activeSequence)
)) ?? null;
const currentSpatialFrame = frame?.spatialAvailable ? frame : latestAvailableSpatialFrame;
if (currentSpatialFrame) {
lastSpatialFrameRef.current = { resultId, frame: currentSpatialFrame };
}
const spatialFrame = frame?.spatialAvailable
? frame
const spatialFrame = currentSpatialFrame
? currentSpatialFrame
: lastSpatialFrameRef.current?.resultId === resultId
? lastSpatialFrameRef.current.frame
: null;
const cameraPointOverlay = useM4ThreatCameraPointOverlay({
enabled: showReferenceMediaLayers && showMediaPoints,
enabled: showReferenceMediaLayers && evidenceDemand.cameraPointOverlay,
resultId,
sequence: frame?.sequence ?? null,
endpointRoot: timelineEndpointRoot,
});
const semanticSpatialResultId = activeSemantic
? activeSemantic.spatialResultId === undefined
? activeSemantic.resultId
: activeSemantic.spatialResultId
const semanticSpatialResultId = activeSpatialSemantic
? activeSpatialSemantic.spatialResultId === undefined
? activeSpatialSemantic.resultId
: activeSpatialSemantic.spatialResultId
: null;
const spatialSemanticTaxonomy = useMemo<readonly E47SemanticClass[]>(
() => semanticSpatialResultId && activeSemantic
? activeSemantic.taxonomy.map((item) => ({
() => semanticSpatialResultId && activeSpatialSemantic
? activeSpatialSemantic.taxonomy.map((item) => ({
classId: item.classId,
label: item.label,
disposition: item.disposition === "ambiguous" ? "ambiguous" : "labeled",
colorRgb: item.colorRgb,
}))
: [],
[activeSemantic, semanticSpatialResultId],
[activeSpatialSemantic, semanticSpatialResultId],
);
const semanticTimeline = useE47SemanticTimelineFrame({
resultId: semanticSpatialResultId,
activeSequence: frame?.sequence ?? timelineFrame.activeSequence,
frameCount: metadata.timeline?.frameCount ?? 0,
taxonomy: spatialSemanticTaxonomy,
enabled: evidenceDemand.selectedSemanticPoints,
});
const displayingBufferedFrame = Boolean(
frame
@@ -443,6 +497,27 @@ export function M4ReplayThreatVisual({
})) ?? [],
[activeSemantic?.taxonomy],
);
const spatialSemanticClasses = useMemo<readonly RecordedEvidenceSemanticClass[]>(
() => activeSpatialSemantic?.taxonomy.map((item) => ({
id: item.classId,
label: `semantic: ${item.label}`,
})) ?? [],
[activeSpatialSemantic?.taxonomy],
);
const spatialSemanticPalette = useMemo<readonly RecordedEvidenceSemanticPaletteEntry[]>(
() => activeSpatialSemantic?.taxonomy.map((item) => ({
classId: item.classId,
color: item.disposition === "undefined"
? { kind: "transparent" as const }
: item.disposition === "ambiguous"
? { kind: "token" as const, token: "--nodedc-warning-rgb" as const }
: { kind: "diagnostic" as const, rgb: item.colorRgb },
opacity: item.disposition === "undefined"
? 0
: item.disposition === "ambiguous" ? 0.52 : 0.92,
})) ?? [],
[activeSpatialSemantic?.taxonomy],
);
const semanticFrame = semanticTimeline.activeFrame?.sequence === frame?.sequence
? semanticTimeline.activeFrame
: null;
@@ -459,7 +534,7 @@ export function M4ReplayThreatVisual({
&& lastSpatialSemanticFrameRef.current.frame.sequence === spatialFrame?.sequence
? lastSpatialSemanticFrameRef.current.frame
: null;
const semanticIntegrityError = activeSemantic && spatialFrame && spatialSemanticFrame && (
const semanticIntegrityError = activeSpatialSemantic && spatialFrame && spatialSemanticFrame && (
spatialSemanticFrame.sourcePointCount !== spatialFrame.pointCloudSourceCount
|| spatialFrame.pointCloudSampleCount !== spatialFrame.pointCloudSourceCount
|| spatialFrame.pointCloudBodyXyzM.length !== spatialFrame.pointCloudSourceCount
@@ -468,7 +543,7 @@ export function M4ReplayThreatVisual({
: null;
const alignedSemanticPointIds = useMemo<readonly (number | null)[] | undefined>(() => {
if (
!activeSemantic
!activeSpatialSemantic
|| !showSpatialSemantic
|| !spatialFrame
|| !spatialSemanticFrame
@@ -478,18 +553,24 @@ export function M4ReplayThreatVisual({
const status = spatialSemanticFrame.statusCodes[index];
return status === 2 || status === 3 ? classId : null;
});
}, [activeSemantic, semanticIntegrityError, showSpatialSemantic, spatialFrame, spatialSemanticFrame]);
}, [activeSpatialSemantic, semanticIntegrityError, showSpatialSemantic, spatialFrame, spatialSemanticFrame]);
const activeSpatialFrame = spatialFrame?.sequence === timelineFrame.activeSequence
? spatialFrame
: null;
const classifiedSpatialFrame = classifiedSpatialLayer?.frame?.sourceSequence === timelineFrame.activeSequence
const hasClassifiedSpatialOutput = Boolean(
classifiedSpatialLayer && classifiedSpatialLayer.cellLayerAvailable !== false,
);
const classifiedSpatialFrame = hasClassifiedSpatialOutput
&& classifiedSpatialLayer?.frame?.sourceSequence === timelineFrame.activeSequence
? classifiedSpatialLayer?.frame ?? null
: null;
const lastClassifiedSpatialFrameRef = useRef<{
resultId: string;
frame: M4ReplayClassifiedSpatialFrame;
} | null>(null);
const incomingClassifiedSpatialFrame = classifiedSpatialLayer?.frame ?? null;
const incomingClassifiedSpatialFrame = hasClassifiedSpatialOutput
? classifiedSpatialLayer?.frame ?? null
: null;
if (incomingClassifiedSpatialFrame && incomingClassifiedSpatialFrame.sampleAvailable !== false) {
lastClassifiedSpatialFrameRef.current = { resultId, frame: incomingClassifiedSpatialFrame };
}
@@ -518,7 +599,9 @@ export function M4ReplayThreatVisual({
? spatialFrame
: null)
: null;
const replaceClassifiedPointCloud = classifiedSpatialLayer?.replacePointCloud ?? true;
const replaceClassifiedPointCloud = hasClassifiedSpatialOutput
? classifiedSpatialLayer?.replacePointCloud ?? true
: false;
const nominalSensorHeightM = metadata.timeline?.rig.nominalSensorHeightM ?? 0;
const mapGravityLocalSensorToBodyGround = useCallback((
point: readonly [number, number, number],
@@ -636,7 +719,12 @@ export function M4ReplayThreatVisual({
.map((item) => item.assessment.closestApproachM)
.filter((value): value is number => value !== null)
.sort((left, right) => left - right)[0] ?? null;
const localSurface = useMemo(() => buildM4LocalSurface(
const localSurface = useMemo(() => spatialFrame?.localSlamBodyXyzM?.length ? ({
pointsBodyXyzM: spatialFrame.localSlamBodyXyzM,
sourceFrameCount: spatialFrame.localSlamSourceFrameCount ?? 0,
sourcePointCount: spatialFrame.localSlamSourcePointCount ?? 0,
voxelCount: spatialFrame.localSlamBodyXyzM.length,
}) : buildM4LocalSurface(
timelineFrame.availableFrames,
spatialFrame,
metadata.timeline?.localSurfaceVisualization ?? {
@@ -647,7 +735,7 @@ export function M4ReplayThreatVisual({
},
), [metadata.timeline, spatialFrame, timelineFrame.availableFrames]);
const semanticOverlay: RecordedEvidenceSemanticOverlay | undefined =
activeSemantic && showMediaSemantic && frame
activeSemantic && evidenceDemand.selectedSemanticMask && frame
? {
src: activeSemantic.maskUrl?.(frame.sequence)
?? e47SemanticMaskUrl(activeSemantic.resultId, frame.sequence),
@@ -684,50 +772,15 @@ export function M4ReplayThreatVisual({
}
: undefined;
const handleMediaModeChange = (next: M4ThreatMediaSelection) => {
if (next === "none") return;
setMediaMode((current) => current === next ? null : next);
};
const handleSpatialModeChange = (next: M4ThreatSpatialSelection) => {
if (next === "none") return;
setSpatialMode((current) => current === next ? null : next);
};
useEffect(() => {
if (playbackController.playback.playing || !frame) return;
if (
playbackController.playback.playing
|| !evidenceDemand.exactCameraFrame
|| !frame
) return;
const image = new Image();
image.src = frame.cameraUrl;
}, [frame?.cameraUrl, playbackController.playback.playing]);
const splitView = mediaMode !== null && spatialMode !== null;
const mediaModeControls = (
<div className="m4-replay-threat-visual__pane-mode-controls" data-pane-mode="media">
<SegmentedControl
value={mediaMode ?? "none"}
items={[
{ value: "video", label: "VIDEO" },
{ value: "camera", label: "CAMERA" },
]}
label="Видео и камера"
onChange={handleMediaModeChange}
/>
</div>
);
const spatialModeControls = (
<div className="m4-replay-threat-visual__pane-mode-controls" data-pane-mode="spatial">
<SegmentedControl
value={spatialMode ?? "none"}
items={[
{ value: "3d", label: "3D" },
{ value: "plan", label: "PLAN" },
]}
label="3D и план"
onChange={handleSpatialModeChange}
/>
</div>
);
}, [evidenceDemand.exactCameraFrame, frame?.cameraUrl, playbackController.playback.playing]);
const mediaLayerControls = activeSemantic
|| (showReferenceMediaLayers && metadata.timeline?.cameraPointDelivery)
@@ -819,16 +872,24 @@ export function M4ReplayThreatVisual({
shape="pill"
variant={showRollingMap ? "primary" : "secondary"}
aria-pressed={showRollingMap}
disabled={classifiedSpatialLayer.cellLayerAvailable === false}
title={classifiedSpatialLayer.cellLayerAvailable === false
? `${classifiedSpatialLayer.cellLayerLabel} недоступен: для этой записи нет запечатанного полного результата`
: undefined}
onClick={() => setShowRollingMap((visible) => !visible)}
>
{classifiedSpatialLayer.cellLayerLabel}
</Button>
{semanticSpatialResultId ? (
{activeSpatialSemantic ? (
<Button
size="compact"
shape="pill"
variant={showSpatialSemantic ? "primary" : "secondary"}
aria-pressed={showSpatialSemantic}
disabled={!semanticSpatialResultId}
title={semanticSpatialResultId
? "Point-aligned semantic evidence"
: "Point-aligned 3D semantics отсутствует в запечатанном результате"}
onClick={() => setShowSpatialSemantic((visible) => !visible)}
>
SEMANTICS
@@ -881,12 +942,16 @@ export function M4ReplayThreatVisual({
LOW-STEP
</Button>
) : null}
{semanticSpatialResultId ? (
{activeSpatialSemantic ? (
<Button
size="compact"
shape="pill"
variant={showSpatialSemantic ? "primary" : "secondary"}
aria-pressed={showSpatialSemantic}
disabled={!semanticSpatialResultId}
title={semanticSpatialResultId
? "Point-aligned semantic evidence"
: "Point-aligned 3D semantics отсутствует в запечатанном результате"}
onClick={() => setShowSpatialSemantic((visible) => !visible)}
>
SEMANTICS
@@ -990,14 +1055,14 @@ export function M4ReplayThreatVisual({
<>
<div>
<span>Spatial evidence</span>
<strong>{classifiedSpatialLayer
<strong>{hasClassifiedSpatialOutput
? classifiedSpatialFrame
? replaceClassifiedPointCloud
? `${classifiedSpatialFrame.pointsMapGravityLocalXyzM.length.toLocaleString("ru-RU")} TGS points · ${classifiedCellCount.toLocaleString("ru-RU")} cells`
: `${(activeSpatialFrame?.pointCloudSourceCount ?? classifiedSpatialFrame.sourcePointCount ?? 0).toLocaleString("ru-RU")} source points · ${classifiedCellCount.toLocaleString("ru-RU")} TGS cells`
: "TGS spatial buffer"
: `${currentIncrementObstacles.length} current · ${rollingMapObstacles.length} rolling${metadata.timeline.occupancyProvenanceDelivery ? ` · ${lowStepObstacles.length} low-step` : ""}`}</strong>
<small>{classifiedSpatialLayer
<small>{hasClassifiedSpatialOutput
? classifiedSpatialFrame
? classifiedSpatialFrame.sampleAvailable === false
? displayedClassifiedFrameHeld && displayedClassifiedSpatialFrame
@@ -1006,9 +1071,9 @@ export function M4ReplayThreatVisual({
: activeSpatialFrame
? "map-gravity-local · all eligible points accounted · causal rolling 1 s"
: "TGS рассчитан · linked source cloud недоступен для этого кадра"
: classifiedSpatialLayer.error
?? classifiedSpatialLayer.loadingLabel
?? `Открываем ${classifiedSpatialLayer.label}`
: classifiedSpatialLayer?.error
?? classifiedSpatialLayer?.loadingLabel
?? `Открываем ${classifiedSpatialLayer?.label ?? "spatial evidence"}`
: (
<>
{spatialFrame
@@ -1033,13 +1098,13 @@ export function M4ReplayThreatVisual({
)}</small>
</div>
<div>
<span>{classifiedSpatialLayer ? "TGS fail-closed" : "Virtual corridor"}</span>
<strong>{classifiedSpatialLayer
<span>{hasClassifiedSpatialOutput ? "TGS fail-closed" : "Virtual corridor"}</span>
<strong>{hasClassifiedSpatialOutput
? classifiedSpatialFrame
? `${classifiedCellCounts.occupied} occupied · ${classifiedCellCounts.rejected} rejected · ${classifiedCellCounts.unobserved} unobserved`
: classifiedSpatialLayer.loading || displayingBufferedFrame ? "loading" : "unavailable"
: classifiedSpatialLayer?.loading || displayingBufferedFrame ? "loading" : "unavailable"
: `${spatialFrame?.decisionCounts.threat ?? 0} threat · nearest ${nearest === null ? "—" : `${nearest.toFixed(2)} м`}`}</strong>
<small>{classifiedSpatialLayer
<small>{hasClassifiedSpatialOutput
? classifiedSpatialFrame
? `${classifiedCellCounts.ground} ground-support · visual review only · navigation authority OFF`
: "visual review only · navigation authority OFF"
@@ -1051,7 +1116,12 @@ export function M4ReplayThreatVisual({
) : undefined;
const timeline = metadata.timeline;
let content;
let content: ReactNode = null;
let canonicalContent: {
mediaContent: ReactNode;
spatialContent: ReactNode;
deckOverlays: ReactNode;
} | null = null;
if (metadata.error) {
content = <SpatialState message={metadata.error} />;
} else if (!timeline) {
@@ -1062,23 +1132,8 @@ export function M4ReplayThreatVisual({
</div>
);
} else {
const mediaPane = (
<section
className="m4-replay-threat-visual__pane"
data-pane="media"
aria-label={mediaMode === "camera" ? "Камера" : "Видео"}
hidden={!mediaMode}
>
{splitView ? (
<div
className="m4-replay-threat-visual__pane-toolbar"
data-pane-toolbar="media"
data-multi-semantic={availableSemanticLayers.length > 1 ? "true" : undefined}
>
{mediaLayerControls}
{mediaModeControls}
</div>
) : null}
const mediaContent = (
<>
<div
className="m4-replay-threat-visual__media-layer"
data-media="video"
@@ -1091,6 +1146,7 @@ export function M4ReplayThreatVisual({
imageWidth={timeline.imageWidth}
imageHeight={timeline.imageHeight}
boxes={activeBoxes}
overlaySeconds={frame?.sessionSeconds}
semanticOverlay={mediaMode === "video" ? semanticOverlay : undefined}
pointCloudOverlay={mediaMode === "video" ? pointCloudOverlay : undefined}
ariaLabel={`${evidenceLabel} recorded-realtime frame ${frame?.sequence ?? 0}: ${activeBoxes.length} proposals`}
@@ -1102,7 +1158,9 @@ export function M4ReplayThreatVisual({
}
segmentCount={timeline.frameCount}
onPlaybackChange={playbackController.synchronize}
onPlayingRejected={() => playbackController.setPlaying(false)}
playbackAuthority="media"
playbackTransport={playbackTransport}
recoverTimestampStalls={recoverTimestampStalls}
/>
) : videoError ? (
<SpatialState message={videoError} />
@@ -1124,34 +1182,20 @@ export function M4ReplayThreatVisual({
ariaLabel={`${evidenceLabel} exact camera frame ${frame.sequence}: ${activeBoxes.length} proposals`}
/>
) : null}
</section>
</>
);
const spatialPane = spatialMode ? (
<section
className="m4-replay-threat-visual__pane"
data-pane="spatial"
aria-label={spatialMode === "3d" ? "Трёхмерная сцена" : "Вид сверху"}
>
{splitView ? (
<div
className="m4-replay-threat-visual__pane-toolbar"
data-pane-toolbar="spatial"
>
{resetSpatialView}
<div className="m4-replay-threat-visual__spatial-toolbar-end">
{spatialLayerControls}
{spatialModeControls}
</div>
</div>
) : null}
const spatialContent = spatialMode ? (
<>
<LaboratoryMetricEvidenceScene
ref={metricSceneRef}
pointCloudBodyXyzM={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
? classifiedPointsBody
: classifiedContextSpatialFrame?.pointCloudBodyXyzM ?? []}
: classifiedContextSpatialFrame?.pointCloudBodyXyzM
?? activeSpatialFrame?.pointCloudBodyXyzM
?? []}
localSurfaceBodyXyzM={localSurface.pointsBodyXyzM}
obstacles={classifiedSpatialLayer ? [] : sceneObstacles}
obstacles={hasClassifiedSpatialOutput ? [] : sceneObstacles}
rig={timeline.rig}
corridor={timeline.corridor}
occupiedVoxelSizeM={displayedClassifiedSpatialFrame?.cellSizeM ?? timeline.occupiedVoxelSizeM}
@@ -1160,22 +1204,22 @@ export function M4ReplayThreatVisual({
showCurrentIncrement={showCurrentIncrement}
showLocalSurface={showLocalSurface}
showRollingMap={showRollingMap}
showLowStep={classifiedSpatialLayer ? false : showLowStep}
showLowStep={hasClassifiedSpatialOutput ? false : showLowStep}
pointSemanticClassIds={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
? displayedClassifiedSpatialFrame.pointClassIds
: alignedSemanticPointIds}
semanticClasses={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
? displayedClassifiedSpatialFrame.classes
: semanticClasses}
: spatialSemanticClasses}
semanticPalette={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
? displayedClassifiedSpatialFrame.palette
: semanticPalette}
: spatialSemanticPalette}
classifiedCells={classifiedCellsBody}
classifiedPackedCells={classifiedPackedCellsBody}
classifiedCellSizeM={displayedClassifiedSpatialFrame?.cellSizeM}
showClassifiedCells={showRollingMap}
/>
{classifiedSpatialLayer && !displayedClassifiedSpatialFrame ? (
{hasClassifiedSpatialOutput && classifiedSpatialLayer && !displayedClassifiedSpatialFrame ? (
<div className="l3-visual-audit__state" role={classifiedSpatialLayer.error ? "alert" : "status"}>
{classifiedSpatialLayer.loading || displayingBufferedFrame
? <span className="busy-indicator" aria-hidden="true" />
@@ -1206,31 +1250,11 @@ export function M4ReplayThreatVisual({
: `На кадре ${frame.sequence + 1} нет body frame; ждём первый квалифицированный spatial evidence.`}
</div>
) : null}
</section>
</>
) : null;
content = (
<div
className="m4-replay-threat-visual__deck"
data-split={splitView ? "true" : undefined}
data-empty={!mediaMode && !spatialMode ? "true" : undefined}
>
<SplitPane
primary={mediaPane}
secondary={spatialPane ?? <div />}
primarySize={splitView ? splitPrimarySize : mediaMode ? 100 : 0}
onPrimarySizeChange={setSplitPrimarySize}
orientation={splitOrientation}
minPrimarySize={splitView ? 24 : 0}
minSecondarySize={splitView ? 24 : 0}
resizable={splitView}
separatorLabel="Изменить размер VIDEO/CAMERA и 3D/PLAN"
/>
{!mediaMode && !spatialMode ? (
<div className="l3-visual-audit__state" role="status">
Выберите VIDEO/CAMERA или 3D/PLAN. Общий таймлайн останется на месте.
</div>
) : null}
const deckOverlays = (
<>
{timelineFrame.loading || displayingBufferedFrame ? (
<div className="m4-replay-threat-visual__buffering" role="status">
<span className="busy-indicator" aria-hidden="true" />
@@ -1261,8 +1285,9 @@ export function M4ReplayThreatVisual({
<span>{semanticIntegrityError}</span>
</div>
) : null}
</div>
</>
);
canonicalContent = { mediaContent, spatialContent, deckOverlays };
}
const transport = timeline ? (
@@ -1287,38 +1312,59 @@ export function M4ReplayThreatVisual({
/>
) : undefined;
if (!timeline || !canonicalContent) {
return (
<div className="l3-visual-audit m4-replay-threat-visual">
<LaboratoryEvidenceViewer
label={`${evidenceLabel} recorded-realtime replay`}
className="m4-replay-threat-evidence-viewer"
mode="video"
modes={[{ value: "video", label: "VIDEO" }]}
expanded={expanded}
onModeChange={() => undefined}
onExpandedChange={setExpanded}
>
{content}
</LaboratoryEvidenceViewer>
</div>
);
}
return (
<div className="l3-visual-audit m4-replay-threat-visual">
<LaboratoryEvidenceViewer
label={activeSemantic
? activeSemantic.label ?? "Semantic diagnostic replay"
: `${evidenceLabel} recorded-realtime replay`}
className="m4-replay-threat-evidence-viewer"
mode={mediaMode ?? "none"}
modes={[
{ value: "video", label: "VIDEO" },
{ value: "camera", label: "CAMERA" },
]}
secondaryMode={{
value: spatialMode ?? "none",
modes: [
{ value: "3d", label: "3D" },
{ value: "plan", label: "PLAN" },
],
label: "3D и план",
onChange: handleSpatialModeChange,
}}
expanded={expanded}
onModeChange={handleMediaModeChange}
onExpandedChange={setExpanded}
modeControlsVisible={!splitView}
actions={actions}
overlay={overlay}
transport={transport}
trailingActions={trailingActions}
>
{content}
</LaboratoryEvidenceViewer>
</div>
<CanonicalRecordedLabReplay
label={activeSemantic
? activeSemantic.label ?? "Semantic diagnostic replay"
: `${evidenceLabel} recorded-realtime replay`}
mediaMode={mediaMode ?? "none"}
mediaModes={[
{ value: "video", label: "VIDEO" },
{ value: "camera", label: "CAMERA" },
]}
spatialMode={spatialMode ?? "none"}
spatialModes={[
{ value: "3d", label: "3D" },
{ value: "plan", label: "PLAN" },
]}
expanded={expanded}
splitPrimarySize={splitPrimarySize}
splitOrientation={splitOrientation}
mediaAriaLabel={mediaMode === "camera" ? "Камера" : "Видео"}
spatialAriaLabel={spatialMode === "3d" ? "Трёхмерная сцена" : "Вид сверху"}
mediaLayerControls={mediaLayerControls}
spatialLayerControls={spatialLayerControls}
spatialLeadingControl={resetSpatialView}
mediaMultiLayer={availableSemanticLayers.length > 1}
mediaContent={canonicalContent.mediaContent}
spatialContent={canonicalContent.spatialContent}
emptyMessage="Выберите VIDEO/CAMERA или 3D/PLAN. Общий таймлайн останется на месте."
deckOverlays={canonicalContent.deckOverlays}
actions={actions}
overlay={overlay}
transport={transport}
trailingActions={trailingActions}
onMediaModeChange={handleMediaModeChange}
onSpatialModeChange={handleSpatialModeChange}
onExpandedChange={setExpanded}
onSplitPrimarySizeChange={setSplitPrimarySize}
/>
);
}
@@ -1,8 +1,5 @@
import { useEffect, useMemo, useState } from "react";
import { Icon, IconButton, StatusBadge } from "@nodedc/ui-react";
import { useEffect, useState } from "react";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import { LaboratoryRecordedClipPlayer } from "../../components/laboratory/LaboratoryRecordedClipPlayer";
import {
LaboratoryEvidence,
LaboratoryResultSummary,
@@ -10,60 +7,21 @@ import {
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import {
RecordedEvidenceSemanticMaskOverlay,
type RecordedEvidenceSemanticClass,
type RecordedEvidenceSemanticPaletteEntry,
} from "../../components/laboratory/RecordedEvidenceSemanticMaskOverlay";
import {
vegetationFullRouteMaskUrl,
vegetationVideoMaskUrl,
type VegetationFullRouteLayer,
type VegetationFullRouteReview,
type VegetationMixedRouteReview,
type VegetationShadowResult,
} from "../../core/laboratory/vegetationShadow";
import { recordedObservationSources } from "../../core/observation/recordedObservationSources";
import { resolveObservationSessionReplay } from "../../core/observation/useObservationSessions";
import type { ObservationSourceDescriptor } from "../../core/runtime/contracts";
import {
fetchM49TgsFullShadowResult,
type M49TgsFullShadowResult,
} from "../../core/laboratory/m49TgsFullShadow";
import { M4ReplayThreatVisual } from "./M4ReplayThreatVisual";
import { M49TgsFullShadowEvidence } from "./M49TgsFullShadowEvidence";
import { CanonicalVegetationRerunReplay } from "./CanonicalVegetationRerunReplay";
function decimal(value: number, digits = 1): string {
return value.toLocaleString("ru-RU", { maximumFractionDigits: digits });
}
const MIXED_ROUTE_MODES = [
{ value: "source", label: "SOURCE" },
{ value: "city", label: "ГОРОД · EoMT" },
{ value: "vegetation", label: "ПРИРОДА · DDRNet" },
{ value: "tgs", label: "TGS" },
] as const;
const FULL_ROUTE_MODES = [
{ value: "source", label: "SOURCE" },
{ value: "city", label: "ГОРОД · EoMT" },
{ value: "vegetation", label: "ПРИРОДА · DDRNet" },
] as const;
function semanticPresentation(layer: VegetationFullRouteLayer): {
classes: readonly RecordedEvidenceSemanticClass[];
palette: readonly RecordedEvidenceSemanticPaletteEntry[];
} {
return {
classes: layer.taxonomy.map((item) => ({ id: item.classId, label: item.label })),
palette: layer.taxonomy.map((item) => ({
classId: item.classId,
color: item.classId === 0
? { kind: "transparent" as const }
: { kind: "diagnostic" as const, rgb: item.colorRgb },
})),
};
}
function FullRouteReviewEvidence({
resultId,
review,
@@ -71,117 +29,7 @@ function FullRouteReviewEvidence({
resultId: string;
review: VegetationFullRouteReview;
}) {
const [sequence, setSequence] = useState(1);
const [playing, setPlaying] = useState(false);
const [playbackRate, setPlaybackRate] = useState(1);
const [mode, setMode] = useState<typeof FULL_ROUTE_MODES[number]["value"]>("vegetation");
const [expanded, setExpanded] = useState(false);
const [videoSource, setVideoSource] = useState<ObservationSourceDescriptor | null>(null);
const [videoError, setVideoError] = useState<string | null>(null);
const frames = useMemo(
() => review.frameSourceTimesNs.map((sourceTimeNs, index) => ({
sequence: index + 1,
sourceTimeNs,
})),
[review.frameSourceTimesNs],
);
const layer = mode === "source" ? null : review[mode];
const semantic = useMemo(() => layer ? semanticPresentation(layer) : null, [layer]);
const maskSequence = sequence - 1;
const prefetchSrcs = useMemo(() => layer
? Array.from({ length: 8 }, (_, offset) => maskSequence + offset + 1)
.filter((candidate) => candidate < review.frameCount)
.map((candidate) => vegetationFullRouteMaskUrl(resultId, mode as "city" | "vegetation", candidate))
: [], [layer, maskSequence, mode, resultId, review.frameCount]);
useEffect(() => {
const controller = new AbortController();
setVideoSource(null);
setVideoError(null);
void resolveObservationSessionReplay(review.sessionId, { signal: controller.signal })
.then((launch) => {
const source = recordedObservationSources(launch).find((candidate) => (
candidate.id === review.recordedMediaSourceId
&& candidate.modality === "video"
&& candidate.semanticChannelId === "camera.video.recorded"
&& candidate.delivery?.kind === "recorded-fmp4-manifest"
&& candidate.delivery.manifestGenerationSha256 === review.recordedMediaGenerationSha256
&& candidate.delivery.timelineStartSeconds === review.timelineStartSeconds
&& candidate.delivery.timelineEndSeconds >= review.timelineEndSeconds
));
if (!source) {
throw new Error("RIGHT-видео не совпало с sealed RAVNOVES004TREE timeline.");
}
if (!controller.signal.aborted) setVideoSource(source);
})
.catch((caught: unknown) => {
if (!controller.signal.aborted) {
setVideoError(caught instanceof Error ? caught.message : "Записанное видео недоступно.");
}
});
return () => controller.abort();
}, [
review.recordedMediaGenerationSha256,
review.recordedMediaSourceId,
review.sessionId,
review.timelineEndSeconds,
review.timelineStartSeconds,
]);
return (
<LaboratoryEvidenceViewer
label="RAVNOVES004TREE full recorded review"
className="m48-atlas-visual"
mode={mode}
modes={FULL_ROUTE_MODES}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
chromeLayout="stacked"
>
{videoSource ? (
<LaboratoryRecordedClipPlayer
source={videoSource}
segmentCount={review.frameCount}
frames={frames}
sequence={sequence}
playing={playing}
playbackRate={playbackRate}
cameraPresentation="primary"
continuousPlayback
sourceCount={1}
onSequenceChange={setSequence}
onPlayingChange={setPlaying}
onPlaybackRateChange={setPlaybackRate}
cameraOverlay={(
<>
<div className="m48-clip-player__pane-label" data-pane="camera">
{mode === "source" ? "SOURCE" : `${mode === "city" ? "EoMT CITY" : "DDRNet NATURE"} · КАДР ${sequence}/${review.frameCount}`}
</div>
{layer && semantic ? (
<div className="m48-clip-player__overlay">
<RecordedEvidenceSemanticMaskOverlay
src={vegetationFullRouteMaskUrl(resultId, mode as "city" | "vegetation", maskSequence)}
prefetchSrcs={prefetchSrcs}
imageWidth={review.width}
imageHeight={review.height}
classes={semantic.classes}
palette={semantic.palette}
opacity={0.76}
ariaLabel={`${layer.name} semantic prediction`}
/>
</div>
) : null}
</>
)}
/>
) : (
<div className="m4-replay-threat-visual__pane-status" role={videoError ? "alert" : "status"}>
{videoError ?? "Открываем автономный recorded source…"}
</div>
)}
</LaboratoryEvidenceViewer>
);
return <CanonicalVegetationRerunReplay resultId={resultId} review={review} />;
}
function FullRouteReviewResult({
@@ -198,26 +46,30 @@ function FullRouteReviewResult({
summary={(
<LaboratorySummary
title="LAB V1 · RAVNOVES004TREE · полный маршрут"
description="Существующий M4.7-шаблон воспроизводит всю запись и переключает два независимых sealed semantic-слоя: городской EoMT и природный DDRNet. Worker для открытия результата не нужен."
status="FULL RECORDED REVIEW · truth отсутствует · commands OFF"
description="Принятый инструмент записанной LAB воспроизводит RAV004 без отдельного viewer: единый таймлайн, правая камера, исходные точки, ограниченный Local SLAM и переключаемые EoMT/DDRNet."
status="ПОЛНЫЙ ПРОСМОТР ЗАПИСИ · эталон отсутствует · команды ВЫКЛ"
statusTone="warning"
facts={[
{ label: "Источник", value: `${review.sourceId} · ${review.frameCount}/${review.frameCount} frames` },
{ label: "Город", value: `${review.city.name} · ${decimal(review.city.inferenceFps, 2)} fps` },
{ label: "Природа", value: `${review.vegetation.name} · ${decimal(review.vegetation.inferenceFps, 2)} fps` },
{ label: "Authority", value: `${rigLabel} · VISUAL REVIEW ONLY · commands OFF` },
{ label: "Источник", value: `${review.sourceId} · ${review.frameCount}/${review.frameCount} кадров камеры` },
{ label: "3D", value: "1444 приращения исходного облака · стабильная по гравитации RFU → корпус" },
{ label: "Город", value: `${review.city.name} · ${decimal(review.city.inferenceFps, 2)} кадра/с` },
{ label: "Природа", value: `${review.vegetation.name} · ${decimal(review.vegetation.inferenceFps, 2)} кадра/с` },
{ label: "TGS", value: "существуют 10 контрольных якорей · артефакт полного маршрута отсутствует" },
{ label: "Полномочия", value: `${rigLabel} · ТОЛЬКО ВИЗУАЛЬНЫЙ ПРОСМОТР · команды ВЫКЛ` },
]}
brief={{
question: "Как оба semantic-кандидата ведут себя на полном переходе от сельской среды к городской?",
approach: "Все 6830 позиции одной recorded timeline последовательно прогнаны на Worker 006 и сохранены двумя независимыми архивами масок. В M4.7 переключается только видимый слой.",
principalResult: "Полная временная шкала доступна локально в SOURCE / EoMT CITY / DDRNet NATURE без обращения к Worker.",
limitation: "Ручной truth отсутствует. Один повреждённый H.264-пакет на позиции 6092 представлен предыдущим декодированным кадром и явно зафиксирован в proof. Полный TGS и кюветы этим прогоном не проверялись.",
question: "Что реально видно на полном RAV004-прогоне с высокой травой, оврагами и переходом к городу?",
approach: "RAV004 поставляет только data/provider configuration в тот же M4 recorded viewer. Видеодекодер владеет clock; новые source increments проецируются в gravity-stable forward/left/up frame, Local SLAM ограничен пятью секундами.",
principalResult: "RAV004 больше не имеет отдельной логики окон, таймера, seek, cache или 3D controls. Модели и подписи меняются конфигурацией, архитектура переключения остаётся общей.",
limitation: "Full-route TGS, независимый person/vehicle detector, ручной truth и point-aligned 3D semantics пока не запечатаны. Semantic-derived рамки диагностические и не являются STOP-authority.",
}}
method={{
completeness: "complete",
executionClass: "ai-inference",
pipelineId: "ravnoves004tree-full-eomt-ddrnet-recorded-review/v1",
pipelineId: "canonical-recorded-lab-rav004tree/v3",
components: [
{ kind: "algorithm", name: "Canonical recorded replay", version: "media-clock / one viewer", role: "shared camera + spatial transport", identitySha256: null },
{ kind: "algorithm", name: "Recorded source points + bounded Local SLAM", version: "source-paced-ground-v3", role: "gravity-stable spatial evidence", identitySha256: null },
{ kind: "model", name: review.city.name, version: "sealed Worker 006 run", role: "urban semantic review", identitySha256: null },
{ kind: "model", name: review.vegetation.name, version: "GOOSE DDRNet-39", role: "vegetation semantic review", identitySha256: null },
],
@@ -226,9 +78,9 @@ function FullRouteReviewResult({
)}
evidence={(
<LaboratoryEvidence
eyebrow="M4.7 TEMPLATE · RAVNOVES004TREE FULL VIDEO"
title="SOURCE / EoMT CITY / DDRNet NATURE · 6830/6830 · TRUTH отсутствует"
kind="diagnostic-model"
eyebrow="КАНОНИЧЕСКАЯ ЗАПИСАННАЯ LAB · RAVNOVES004TREE"
title="КАМЕРА + ИСХОДНЫЕ ТОЧКИ + ЛОКАЛЬНЫЙ SLAM + КАРТА TGS + СЕМАНТИКА · 6830/6830"
kind="recorded-replay"
resizable
>
<FullRouteReviewEvidence resultId={resultId} review={review} />
@@ -236,134 +88,19 @@ function FullRouteReviewResult({
)}
result={(
<LaboratoryResultSummary
title="Полный двухслойный visual review собран; управление не авторизовано"
status="Recorded evidence ready · navigation/actuation OFF"
title="RAV004 переведён на общий replay-каркас; safety evidence ещё не полно"
status="Записанные доказательства · навигация/управление ВЫКЛ"
statusTone="warning"
metrics={[
{ label: "Route masks", value: "6830/6830 × 2", hint: "sealed local archives · Worker не требуется" },
{ label: "EoMT p95", value: `${decimal(review.city.latencyP95Ms, 2)} ms`, hint: "последовательный изолированный прогон" },
{ label: "DDRNet p95", value: `${decimal(review.vegetation.latencyP95Ms, 2)} ms`, hint: "последовательный изолированный прогон" },
{ label: "Decode repair", value: "1/6830", hint: "sequence 6092 · previous frame · sealed proof" },
{ label: "Таймлайн камеры", value: "6830 кадров · ≈9,51 Гц", hint: "единые часы управляют видео, слоями и пространством" },
{ label: "Исходная геометрия", value: "1444 приращения · ≈2 Гц", hint: "между поступлениями удерживается последний подтверждённый пространственный кадр" },
{ label: "Пропускная способность EoMT", value: `${decimal(review.city.inferenceFps, 2)} кадра/с`, hint: "изолированный полный прогон; не стек реального времени" },
{ label: "Пропускная способность DDRNet", value: `${decimal(review.vegetation.inferenceFps, 2)} кадра/с`, hint: "изолированный полный прогон; временная стабильность не принята" },
]}
conclusion={{
proved: "Городской EoMT и природный DDRNet воспроизводимо обработали полную запись и доступны в одном существующем M4.7 viewer.",
notProved: "Не доказаны truth accuracy, одновременный realtime-load, полный TGS, отрицательные препятствия и безопасное управление ровером.",
decision: "Использовать результат только как визуальную диагностику. Navigation/actuation оставить OFF; следующий gate — оценка временной стабильности и независимый person/vehicle STOP.",
}}
/>
)}
/>
);
}
function MixedRouteReviewEvidence({ review }: { review: VegetationMixedRouteReview }) {
const [index, setIndex] = useState(0);
const [mode, setMode] = useState<typeof MIXED_ROUTE_MODES[number]["value"]>("vegetation");
const [expanded, setExpanded] = useState(false);
const item = review.cases[index]!;
return (
<LaboratoryEvidenceViewer
label="RAVNOVES004TREE mixed route review"
className="m48-atlas-visual"
mode={mode}
modes={MIXED_ROUTE_MODES}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
chromeLayout="stacked"
actions={(
<>
<IconButton label="Предыдущая сцена" onClick={() => setIndex((index - 1 + review.cases.length) % review.cases.length)}>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton label="Следующая сцена" onClick={() => setIndex((index + 1) % review.cases.length)}>
<Icon name="chevron-right" size={16} />
</IconButton>
</>
)}
overlay={(
<div className="m48-atlas-visual__case">
<StatusBadge tone={item.phase === "urban" ? "accent" : item.phase === "transition" ? "warning" : "neutral"}>
{item.phase.toUpperCase()} · {index + 1}/{review.cases.length}
</StatusBadge>
<strong>sequence {item.sourceSequence} · +{decimal(item.sessionSeconds, 2)} s</strong>
<small>
TGS: {item.tgs.groundCells} ground · {item.tgs.occupiedCells} occupied · {item.tgs.unobservedCells} unobserved
</small>
</div>
)}
>
<div className="recorded-evidence-image-scene">
<img src={item.assets[mode]} alt="" draggable={false} />
</div>
</LaboratoryEvidenceViewer>
);
}
function MixedRouteReviewResult({
rigLabel,
review,
}: {
rigLabel: string;
review: VegetationMixedRouteReview;
}) {
return (
<LaboratoryWorkTemplate
summary={(
<LaboratorySummary
title="LAB V1 · RAVNOVES004TREE · село → город"
description="Существующий LAB-шаблон показывает 10 синхронных camera/LiDAR сцен одной записи. EoMT и DDRNet остаются независимыми слоями; TGS показывает отдельную геометрию и не может быть очищен семантической маской."
status="BOUNDED RECORDED REVIEW · truth отсутствует · commands OFF"
statusTone="warning"
facts={[
{ label: "Источник", value: `${review.sourceId} · ${review.frameCount} camera/LiDAR islands` },
{ label: "Переход", value: "5 rural · 1 transition · 4 urban" },
{ label: "Слои", value: "SOURCE · EoMT CITY · DDRNet VEGETATION · causal TGS" },
{ label: "Authority", value: `${rigLabel} · VISUAL REVIEW ONLY · commands OFF` },
]}
brief={{
question: "Сохраняются ли городская семантика, растительность и геометрия при переходе из сельской среды в город?",
approach: "Выбраны десять соседних с исходными сцен camera-кадров, каждый синхронизирован с LiDAR в пределах 100 мс. Все три вычислительных слоя прогнаны на Worker 006 и запечатаны локально.",
principalResult: "Все 10 сцен обработаны EoMT, DDRNet и causal TGS. Слои можно переключать без наложения цветов и без зависимости LAB от воркера.",
limitation: "Это bounded islands без ручной truth. DDRNet шумит по подтипам растительности; TGS не доказывает обнаружение кювета или отрицательного препятствия.",
}}
method={{
completeness: "complete",
executionClass: "ai-inference",
pipelineId: "ravnoves004tree-eomt-ddrnet-causal-tgs-review/v1",
components: [
{ kind: "model", name: review.models.city.name, version: "sealed Worker run", role: "urban semantic review", identitySha256: null },
{ kind: "model", name: review.models.vegetation.name, version: "GOOSE DDRNet-39", role: "vegetation semantic review", identitySha256: null },
{ kind: "algorithm", name: review.models.tgs.name, version: "TRAVEL compatibility runner", role: "independent local geometry", identitySha256: null },
],
}}
/>
)}
evidence={(
<LaboratoryEvidence
eyebrow="M4.7 TEMPLATE · RAVNOVES004TREE"
title="SOURCE / ГОРОД / ПРИРОДА / TGS · 10/10 · TRUTH отсутствует"
kind="diagnostic-model"
resizable
>
<MixedRouteReviewEvidence review={review} />
</LaboratoryEvidence>
)}
result={(
<LaboratoryResultSummary
title="Переход село → город воспроизведён; safety gate не закрыт"
status="Review ready · navigation/actuation OFF"
statusTone="warning"
metrics={[
{ label: "Aligned scenes", value: "10/10", hint: "camera + LiDAR + pose · автономный archive" },
{ label: "EoMT end-to-end p95", value: `${decimal(review.models.city.endToEndP95Ms, 2)} ms`, hint: `${decimal(review.models.city.inferenceFps, 2)} fps в изолированном прогоне` },
{ label: "DDRNet inference p95", value: `${decimal(review.models.vegetation.latencyP95Ms, 2)} ms`, hint: "candidate review · не совместный realtime stack" },
{ label: "TGS p95", value: `${decimal(review.models.tgs.latencyP95Ms, 2)} ms`, hint: `${review.models.tgs.cellSizeM} m cells · ${review.models.tgs.radiusM} m radius` },
]}
conclusion={{
proved: "Оба semantic слоя и causal TGS воспроизводимо работают на сельской, переходной и городской части новой записи.",
notProved: "Не доказаны accuracy без truth, временная стабильность по всему видео, детект кюветов и безопасное совместное realtime-управление ровером.",
decision: "Оставить navigation/actuation OFF. Следующий короткий gate — непрерывный realtime-load двух моделей плюс независимый person/vehicle STOP; кюветы проверять отдельной записью.",
proved: "Камера, перемотка, пространственные слои и переключение семантики используют один принятый переиспользуемый viewer и единые часы; исходная геометрия RFU больше не наследует крен и тангаж LiDAR.",
notProved: "Не доказаны непрерывная TGS, независимый детектор/STOP, точность относительно эталона, временная стабильность DDRNet и ≥10 кадров/с совместного стека реального времени.",
decision: "Продолжать как визуальный аудит. До запечатанной TGS полного маршрута и барьера детектора/нагрузки навигация и управление остаются выключенными.",
}}
/>
)}
@@ -373,6 +110,7 @@ function MixedRouteReviewResult({
function VegetationRouteEvidence({ result }: { result: VegetationShadowResult }) {
const route = result.routeVideo!;
const linkedTgsResultId = route.linkedTgsResultId;
const [tgs, setTgs] = useState<M49TgsFullShadowResult | null>(null);
const [tgsError, setTgsError] = useState<string | null>(null);
@@ -380,8 +118,8 @@ function VegetationRouteEvidence({ result }: { result: VegetationShadowResult })
const controller = new AbortController();
setTgs(null);
setTgsError(null);
if (!route.linkedTgsResultId) return () => controller.abort();
void fetchM49TgsFullShadowResult(route.linkedTgsResultId, {
if (!linkedTgsResultId) return () => controller.abort();
void fetchM49TgsFullShadowResult(linkedTgsResultId, {
signal: controller.signal,
}).then((next) => {
if (controller.signal.aborted) return;
@@ -395,7 +133,11 @@ function VegetationRouteEvidence({ result }: { result: VegetationShadowResult })
}
});
return () => controller.abort();
}, [route.baseM4ResultId, route.linkedTgsResultId]);
}, [linkedTgsResultId, route.baseM4ResultId]);
if (!linkedTgsResultId) {
throw new Error("Vegetation LAB result has no linked canonical M4.9 TGS evidence.");
}
const semantic = {
id: "vegetation",
@@ -408,16 +150,14 @@ function VegetationRouteEvidence({ result }: { result: VegetationShadowResult })
maskAriaLabel: "DDRNet vegetation material prediction",
} as const;
if (route.linkedTgsResultId && tgs) {
if (tgsError) {
return (
<M49TgsFullShadowEvidence
result={tgs}
semanticOverride={semantic}
evidenceLabel="LAB V1 · EoMT + DDRNet + YOLOX + TGS"
/>
<div className="m4-replay-threat-visual__pane-status" role="alert">
Канонический M4.9 TGS слой недоступен: {tgsError}
</div>
);
}
if (route.linkedTgsResultId && !tgsError) {
if (!tgs) {
return (
<div className="m4-replay-threat-visual__pane-status" role="status">
Открываем sealed EoMT, TGS и coarse vegetation timeline
@@ -425,20 +165,11 @@ function VegetationRouteEvidence({ result }: { result: VegetationShadowResult })
);
}
return (
<>
<M4ReplayThreatVisual
resultId={route.baseM4ResultId}
evidenceLabel="LAB V1 · DDRNet"
showReferenceMediaLayers
showSpatialOverlaySummary={false}
semantic={semantic}
/>
{tgsError ? (
<div className="m4-replay-threat-visual__pane-status" role="alert">
TGS слой недоступен: {tgsError}
</div>
) : null}
</>
<M49TgsFullShadowEvidence
result={tgs}
semanticOverride={semantic}
evidenceLabel="LAB V1 · EoMT + DDRNet + YOLOX + TGS"
/>
);
}
@@ -458,8 +189,10 @@ export function VegetationShadowResultView({
/>
);
}
if (result.routeReview) {
return <MixedRouteReviewResult rigLabel={rigLabel} review={result.routeReview} />;
if (!result.routeVideo?.linkedTgsResultId) {
throw new Error(
"Vegetation LAB result has no canonical M4 source timeline and linked M4.9 TGS evidence.",
);
}
const route = result.routeVideo;
const selected = result.candidates.find(
@@ -472,9 +205,7 @@ export function VegetationShadowResultView({
<LaboratorySummary
title="LAB V1 · карта ровера · город + растительность"
description="Один recorded-контур RAVNOVES00 синхронно показывает городской EoMT, природный DDRNet, frozen YOLOX detections и causal TGS. Семантические маски переключаются, чтобы их цвета не скрывали друг друга; геометрическое veto остаётся независимым."
status={route
? "MULTILAYER RECORDED REVIEW · commands OFF · route truth отсутствует"
: "ROUTE EVIDENCE MISSING · commands OFF"}
status="MULTILAYER RECORDED REVIEW · commands OFF · route truth отсутствует"
statusTone="warning"
facts={[
{ label: "Источник", value: "RAVNOVES00 · sensor.camera.right · 4489 recorded frames" },
@@ -485,61 +216,31 @@ export function VegetationShadowResultView({
]}
brief={{
question: "Можно ли одновременно видеть городской и природный semantic stack, не теряя независимую геометрическую защиту?",
approach: "EoMT и DDRNet сохранены как два независимых sealed слоя на одной M4 timeline. В штатном M4.7 viewer пользователь переключает только отображаемую маску; YOLOX и TGS остаются активными слоями evidence.",
principalResult: route
? "Оба semantic archive доступны в одном viewer. Это не пиксельный fusion и не единая новая модель: городской и природный ответы остаются раздельными."
: "Route archive для этой immutable identity отсутствует.",
approach: "EoMT и DDRNet сохранены как два независимых sealed слоя на одной M4 timeline. В штатном M4.9 viewer пользователь переключает только отображаемую маску; YOLOX и TGS остаются активными слоями evidence.",
principalResult: "Оба semantic archive доступны в одном viewer. Это не пиксельный fusion и не единая новая модель: городской и природный ответы остаются раздельными.",
limitation: "RAVNOVES00 не имеет ручной truth. DDRNet заметно прыгает между HIGH GRASS, WOODY и UNKNOWN; поэтому subtype нельзя подавать напрямую в planner. Отсутствие класса никогда не означает свободный путь.",
}}
method={{
completeness: route ? "complete" : "legacy-partial",
completeness: "complete",
executionClass: "ai-inference",
pipelineId: "ravnoves-eomt-ddrnet-yolox-causal-tgs-recorded-review/v1",
components: [
{
kind: "model",
name: "EoMT Cityscapes semantic",
version: "sealed E47 archive",
role: "urban semantic review",
identitySha256: null,
},
{
kind: "model",
name: selected.loadedModelName,
version: selected.candidate,
role: "vegetation material candidate",
identitySha256: selected.checkpointSha256,
},
{
kind: "algorithm",
name: "Frozen YOLOX + causal TGS",
version: "linked M4/M4.9 archives",
role: "independent object and geometry veto",
identitySha256: null,
},
{ kind: "model", name: "EoMT Cityscapes semantic", version: "sealed E47 archive", role: "urban semantic review", identitySha256: null },
{ kind: "model", name: selected.loadedModelName, version: selected.candidate, role: "vegetation material candidate", identitySha256: selected.checkpointSha256 },
{ kind: "algorithm", name: "Frozen YOLOX + causal TGS", version: "linked M4/M4.9 archives", role: "independent object and geometry veto", identitySha256: null },
],
}}
/>
)}
evidence={route ? (
evidence={(
<LaboratoryEvidence
eyebrow="M4.7 · RAVNOVES00 FULL VIDEO"
eyebrow="M4.9 · RAVNOVES00 FULL VIDEO"
title="EoMT CITY / DDRNet VEGETATION + YOLOX + CAUSAL TGS · 4489/4489 · TRUTH отсутствует"
kind="diagnostic-model"
resizable
>
<VegetationRouteEvidence result={result} />
</LaboratoryEvidence>
) : (
<LaboratoryEvidence
eyebrow="M4.7 · RAVNOVES00 FULL VIDEO"
title="ROUTE ARCHIVE отсутствует"
kind="diagnostic-model"
>
<div className="m4-replay-threat-visual__pane-status" role="alert">
Для этой immutable identity нет полного route video evidence.
</div>
</LaboratoryEvidence>
)}
result={(
<LaboratoryResultSummary
@@ -547,26 +248,10 @@ export function VegetationShadowResultView({
status="Semantics advisory · YOLOX/TGS veto cannot be cleared"
statusTone="warning"
metrics={[
{
label: "Route masks",
value: route ? `${route.frameCount}/${route.frameCount}` : "0/4489",
hint: "sealed local playback · Worker для открытия не нужен",
},
{
label: "Semantic sources",
value: route ? "2 independent layers" : "0",
hint: "EoMT CITY / DDRNet VEGETATION · display switches, evidence does not fuse",
},
{
label: "Vegetation worker p95",
value: `${decimal(selected.shadowLatencyP95Ms, 2)} ms`,
hint: "изолированный DDRNet inference; не совместный realtime stack",
},
{
label: "Vegetation peak VRAM",
value: `${decimal(selected.peakReservedVramBytes / 1024 ** 3, 2)} GiB`,
hint: "DDRNet candidate на Worker 006",
},
{ label: "Route masks", value: `${route.frameCount}/${route.frameCount}`, hint: "sealed local playback · Worker для открытия не нужен" },
{ label: "Semantic sources", value: "2 independent layers", hint: "EoMT CITY / DDRNet VEGETATION · display switches, evidence does not fuse" },
{ label: "Vegetation worker p95", value: `${decimal(selected.shadowLatencyP95Ms, 2)} ms`, hint: "изолированный DDRNet inference; не совместный realtime stack" },
{ label: "Vegetation peak VRAM", value: `${decimal(selected.peakReservedVramBytes / 1024 ** 3, 2)} GiB`, hint: "DDRNet candidate на Worker 006" },
]}
conclusion={{
proved: "На одной recorded timeline доступны городской EoMT, природный DDRNet, YOLOX detections и causal TGS; LAB автономна от Worker.",
@@ -10,6 +10,7 @@ interface M48EvidenceModeControlProps {
mode: M48BlindEvidenceMode;
cameraVisible: boolean;
spatialAvailable: boolean;
planAvailable?: boolean;
onModeChange: (mode: M48BlindEvidenceMode) => void;
onCameraVisibleChange: (visible: boolean) => void;
}
@@ -34,6 +35,7 @@ export function M48EvidenceModeControls({
mode,
cameraVisible,
spatialAvailable,
planAvailable = spatialAvailable,
onModeChange,
onCameraVisibleChange,
}: M48EvidenceModeControlProps) {
@@ -68,9 +70,9 @@ export function M48EvidenceModeControls({
<IconButton
label={spatialMode === "plan" ? "Скрыть план" : "Показать план"}
aria-pressed={spatialMode === "plan"}
disabled={!spatialAvailable || (!cameraVisible && spatialMode === "plan")}
disabled={!planAvailable || (!cameraVisible && spatialMode === "plan")}
onClick={() => {
if (!spatialAvailable) return;
if (!planAvailable) return;
onModeChange(nextM48SpatialMode(mode, cameraVisible, "plan"));
}}
>
@@ -11,11 +11,30 @@ const CHUNK_SIZE = 24;
const RETAINED_CHUNK_COUNT = 8;
const PREFETCH_CHUNKS_AHEAD = 2;
function chunkWindowStarts(activeStart: number, frameCount: number): readonly number[] {
return Array.from(
{ length: PREFETCH_CHUNKS_AHEAD + 2 },
(_, index) => activeStart + (index - 1) * CHUNK_SIZE,
).filter((start) => start >= 0 && start < frameCount);
export function e47SemanticChunkWindowStarts(
activeStart: number,
frameCount: number,
): readonly number[] {
return [
activeStart,
...Array.from(
{ length: PREFETCH_CHUNKS_AHEAD },
(_, index) => activeStart + (index + 1) * CHUNK_SIZE,
),
activeStart - CHUNK_SIZE,
].filter((start) => start >= 0 && start < frameCount);
}
export function cancelE47SemanticRequestsOutsideWindow<T extends { abort(): void }>(
inFlight: Map<number, T>,
desiredStarts: readonly number[],
): void {
const desired = new Set(desiredStarts);
for (const [start, controller] of inFlight) {
if (desired.has(start)) continue;
controller.abort();
inFlight.delete(start);
}
}
function errorMessage(error: unknown): string {
@@ -29,11 +48,13 @@ export function useE47SemanticTimelineFrame({
activeSequence,
frameCount,
taxonomy,
enabled = true,
}: {
resultId: string | null;
activeSequence: number | null;
frameCount: number;
taxonomy: readonly E47SemanticClass[];
enabled?: boolean;
}) {
const [chunks, setChunks] = useState<ReadonlyMap<number, E47SemanticTimelineChunk>>(
() => new Map(),
@@ -55,16 +76,21 @@ export function useE47SemanticTimelineFrame({
for (const controller of inFlight.current.values()) controller.abort();
inFlight.current.clear();
};
}, [resultId]);
}, [enabled, resultId]);
const activeStart = activeSequence === null
const activeStart = !enabled || activeSequence === null
? null
: Math.floor(activeSequence / CHUNK_SIZE) * CHUNK_SIZE;
activeStartRef.current = activeStart;
useEffect(() => {
if (!resultId || activeStart === null || frameCount < 1) return;
for (const start of chunkWindowStarts(activeStart, frameCount)) {
if (!enabled || !resultId || activeStart === null || frameCount < 1) {
cancelE47SemanticRequestsOutsideWindow(inFlight.current, []);
return;
}
const starts = e47SemanticChunkWindowStarts(activeStart, frameCount);
cancelE47SemanticRequestsOutsideWindow(inFlight.current, starts);
for (const start of starts) {
if (chunksRef.current.has(start) || inFlight.current.has(start)) continue;
const controller = new AbortController();
inFlight.current.set(start, controller);
@@ -95,8 +121,11 @@ export function useE47SemanticTimelineFrame({
.finally(() => {
if (inFlight.current.get(start) === controller) inFlight.current.delete(start);
});
// Semantic point arrays are large JSON payloads. Admit the active chunk
// first, then advance the bounded prefetch window one request per render.
break;
}
}, [activeStart, frameCount, resultId, taxonomy]);
}, [activeStart, chunks, enabled, frameCount, resultId, taxonomy]);
const activeFrame: E47SemanticTimelineFrame | null = useMemo(() => {
if (activeSequence === null || activeStart === null) return null;
@@ -107,7 +136,7 @@ export function useE47SemanticTimelineFrame({
return {
activeFrame,
loading: Boolean(resultId) && activeSequence !== null && !activeFrame && !error,
loading: enabled && Boolean(resultId) && activeSequence !== null && !activeFrame && !error,
error,
};
}
@@ -18,6 +18,7 @@ import {
const REQUESTED_CHUNK_FRAMES = 24;
const RETAINED_CHUNK_COUNT = 4;
const RETAINED_CHUNKS_BEHIND = 1;
const PREFETCH_CHUNKS_AHEAD = 1;
const RETAINED_CAMERA_POINT_OVERLAYS = 12;
@@ -31,10 +32,17 @@ export function m4ThreatChunkWindowStarts(
frameCount: number,
): readonly number[] {
if (chunkSize < 1 || frameCount < 1) return [];
return Array.from(
{ length: PREFETCH_CHUNKS_AHEAD + 1 },
(_, index) => activeChunkStart + index * chunkSize,
).filter((start) => start >= 0 && start < frameCount);
return [
activeChunkStart,
...Array.from(
{ length: PREFETCH_CHUNKS_AHEAD },
(_, index) => activeChunkStart + (index + 1) * chunkSize,
),
...Array.from(
{ length: RETAINED_CHUNKS_BEHIND },
(_, index) => activeChunkStart - (index + 1) * chunkSize,
),
].filter((start) => start >= 0 && start < frameCount);
}
export function cancelM4ThreatChunkRequestsOutsideWindow<T extends { abort(): void }>(
@@ -76,12 +84,16 @@ export function useM4ThreatTimelineFrame({
resultId,
timeline,
currentSeconds,
includeSpatialPoints = true,
endpointRoot,
spatialPlaybackTransport = "auto",
}: {
resultId: string;
timeline: M4ThreatTimeline | null;
currentSeconds: number;
includeSpatialPoints?: boolean;
endpointRoot?: string;
spatialPlaybackTransport?: "auto" | "sealed-binary" | "json";
}) {
const [chunks, setChunks] = useState<ReadonlyMap<number, M4ThreatTimelineChunk>>(
() => new Map(),
@@ -94,8 +106,10 @@ export function useM4ThreatTimelineFrame({
totalBytes: 0,
});
const [playbackError, setPlaybackError] = useState<string | null>(null);
const binaryPlayback = endpointRoot === undefined
|| endpointRoot === M4_THREAT_TIMELINE_ENDPOINT_ROOT;
const binaryPlayback = spatialPlaybackTransport === "sealed-binary"
|| (spatialPlaybackTransport === "auto" && (
endpointRoot === undefined || endpointRoot === M4_THREAT_TIMELINE_ENDPOINT_ROOT
));
const inFlight = useRef(new Map<number, AbortController>());
const chunksRef = useRef(chunks);
const activeChunkStartRef = useRef<number | null>(null);
@@ -107,7 +121,7 @@ export function useM4ThreatTimelineFrame({
setPlaybackError(null);
setPlaybackProgress({ phase: "manifest", loadedBytes: 0, totalBytes: 0 });
if (!timeline) return () => controller.abort();
if (!binaryPlayback) {
if (!binaryPlayback || !includeSpatialPoints) {
setPlaybackProgress({ phase: "ready", loadedBytes: 0, totalBytes: 0 });
return () => controller.abort();
}
@@ -127,7 +141,7 @@ export function useM4ThreatTimelineFrame({
}
});
return () => controller.abort();
}, [binaryPlayback, endpointRoot, resultId, timeline]);
}, [binaryPlayback, endpointRoot, includeSpatialPoints, resultId, timeline]);
useEffect(() => {
for (const controller of inFlight.current.values()) controller.abort();
@@ -140,7 +154,7 @@ export function useM4ThreatTimelineFrame({
for (const controller of inFlight.current.values()) controller.abort();
inFlight.current.clear();
};
}, [resultId, timeline]);
}, [includeSpatialPoints, resultId, timeline]);
const activeSequence = useMemo(
() => timeline
@@ -158,7 +172,11 @@ export function useM4ThreatTimelineFrame({
activeChunkStartRef.current = activeChunkStart;
useEffect(() => {
if (!timeline || activeChunkStart === null || (binaryPlayback && !playbackManifest)) return;
if (
!timeline
|| activeChunkStart === null
|| (binaryPlayback && includeSpatialPoints && !playbackManifest)
) return;
const starts = m4ThreatChunkWindowStarts(
activeChunkStart,
chunkSize,
@@ -170,7 +188,7 @@ export function useM4ThreatTimelineFrame({
const controller = new AbortController();
inFlight.current.set(start, controller);
void (async () => {
const playbackPointPack = binaryPlayback && playbackManifest
const playbackPointPack = binaryPlayback && includeSpatialPoints && playbackManifest
? await fetchM4ThreatPlaybackPointChunk(
playbackManifest,
Math.floor(start / playbackManifest.chunkFrameCount),
@@ -189,6 +207,7 @@ export function useM4ThreatTimelineFrame({
endpointRoot,
cameraObstacleProjectionDelivery: timeline.cameraObstacleProjectionDelivery,
playbackPointPack,
includePoints: includeSpatialPoints,
});
})()
.then((chunk) => {
@@ -220,7 +239,7 @@ export function useM4ThreatTimelineFrame({
// loaded first, then the next chunk is prefetched on the following render.
break;
}
}, [activeChunkStart, binaryPlayback, chunkSize, chunks, endpointRoot, playbackManifest, resultId, timeline]);
}, [activeChunkStart, binaryPlayback, chunkSize, chunks, endpointRoot, includeSpatialPoints, playbackManifest, resultId, timeline]);
const activeFrame: M4ThreatTimelineFrame | null = useMemo(() => {
if (activeSequence === null || activeChunkStart === null) return null;
@@ -104,24 +104,3 @@ test("laboratory UI is a bounded feature slice, not a central workspace branch",
assert.doesNotMatch(laboratoryCss, /\.e30-human-review/);
assert.match(e30HumanReviewCss, /\.e30-human-review/);
});
test("central composition files cannot silently become monoliths again", async () => {
const ratchets = [
["App.tsx", 1_250],
["workspaces/Workspaces.tsx", 1_200],
["workspaces/laboratory/LaboratoryArchiveWorkspace.tsx", 1_000],
["core/laboratory/advancedResults.ts", 1_000],
["core/laboratory/e40ProductGate.ts", 500],
["styles/workspaces.css", 4_350],
["styles/laboratory.css", 900],
["styles/laboratory-reporting.css", 100],
];
for (const [relativePath, maximumLines] of ratchets) {
const lineCount = (await read(relativePath)).split("\n").length;
assert.ok(
lineCount <= maximumLines,
`${relativePath} has ${lineCount} lines; split the feature instead of raising ${maximumLines}`,
);
}
});
@@ -0,0 +1,134 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let canonicalMapGravityLocalPointToBodyGround;
let canonicalRecordedLabPackedTgsCells;
let canonicalRecordedLabTgsIsCurrent;
let resolveCanonicalLabReplay;
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
({
canonicalMapGravityLocalPointToBodyGround,
canonicalRecordedLabPackedTgsCells,
canonicalRecordedLabTgsIsCurrent,
} = await server.ssrLoadModule("/src/core/laboratory/canonicalRecordedLab.ts"));
({ resolveCanonicalLabReplay } = await server.ssrLoadModule(
"/src/core/laboratory/canonicalLabReplay.ts",
));
});
after(async () => {
await server?.close();
});
const identity = [
[1, 0, 0],
[0, 1, 0],
[0, 0, 1],
];
test("canonical TGS validity never retains a sparse anchor beyond its sealed history", () => {
assert.equal(canonicalRecordedLabTgsIsCurrent(2_000_000_000, 1_000_000_000), true);
assert.equal(canonicalRecordedLabTgsIsCurrent(2_000_000_001, 1_000_000_000), false);
assert.equal(canonicalRecordedLabTgsIsCurrent(999_999_999, 1_000_000_000), false);
});
test("map-gravity-local TGS uses sensor translation and current ground body exactly once", () => {
const anchor = {
originMapXyzM: [10, 20, 0.68],
sensorOriginMapXyzM: [10, 20, 1],
basisMapFromBody: identity,
};
const current = {
originMapXyzM: [8, 20, 0],
sensorOriginMapXyzM: [8, 20, 0.32],
basisMapFromBody: identity,
};
assert.deepEqual(
canonicalMapGravityLocalPointToBodyGround([1, 2, -1], anchor, current),
[3, 2, 0],
);
const packed = canonicalRecordedLabPackedTgsCells({
centersXyM: [[1, 2]],
stateCodes: [2],
zBoundsM: [[-1, 0]],
}, anchor, current);
assert.deepEqual([...packed.centersBodyXyM], [3, 2]);
assert.deepEqual([...packed.zBoundsM], [0, 1]);
assert.deepEqual([...packed.stateCodes], [2]);
});
test("recorded LAB spatial loading is shared, profile-bound and experiment-neutral", async () => {
const [contract, scheduler, vegetation] = await Promise.all([
readFile(new URL("../src/core/laboratory/canonicalRecordedLabSpatial.ts", import.meta.url), "utf8"),
readFile(new URL("../src/components/laboratory/useCanonicalRecordedLabSpatialFrame.ts", import.meta.url), "utf8"),
readFile(new URL("../src/core/laboratory/vegetationShadow.ts", import.meta.url), "utf8"),
]);
assert.match(contract, /CANONICAL_RECORDED_LAB_SPATIAL_PROFILE/);
assert.match(contract, /profile: CANONICAL_RECORDED_LAB_SPATIAL_PROFILE/);
assert.match(scheduler, /Shared latest-request-wins scheduler/);
assert.match(scheduler, /identityRef\.current !== requestIdentity/);
assert.doesNotMatch(scheduler, /RAVNOVES|vegetation|DDRNet/);
assert.doesNotMatch(vegetation, /fetchCanonicalRecordedLabSpatialFrame|CanonicalRecordedLabSpatialFrame/);
});
test("canonical LAB resolves one generation-bound merged RRD", async () => {
const baseGeneration = "a".repeat(64);
const replayGeneration = "b".repeat(64);
const resultId = `lab-v1-vegetation-shadow-${"c".repeat(64)}`;
let request;
const replay = await resolveCanonicalLabReplay(resultId, {
kind: "rerun-recording",
sessionId: "session-001",
sourceUrl: "/api/v1/observation-sessions/session-001/recording.rrd",
viewerSourceUrl:
`/api/v1/observation-sessions/session-001/recording.rrd?generation=${baseGeneration}`,
mediaType: "application/vnd.rerun.rrd",
timeline: "session_time",
timelineStartSeconds: 0,
timelineEndSeconds: 10,
seekable: true,
byteLength: 100,
sha256: baseGeneration,
playback: { speed: 1, loop: false },
mediaSources: [],
}, {
origin: "http://mission-core.test",
fetcher: async (url, options) => {
request = { url, options };
return new Response(null, {
status: 200,
headers: {
"Content-Type": "application/vnd.rerun.rrd",
"Content-Length": "234567",
"ETag": `"${replayGeneration}"`,
"X-Rerun-Format": "RRF2",
},
});
},
});
const sourceUrl =
`/api/v1/laboratory/vegetation-shadow/${resultId}/canonical-replay.rrd`;
assert.equal(
request.url,
`http://mission-core.test${sourceUrl}?base_generation=${baseGeneration}`,
);
assert.equal(request.options.method, "HEAD");
assert.deepEqual(replay, {
sourceUrl,
viewerSourceUrl: `${sourceUrl}?generation=${replayGeneration}`,
byteLength: 234567,
sha256: replayGeneration,
blueprintSourceUrl: "/api/v1/observation-sessions/session-001/blueprint.rrd",
});
});
@@ -0,0 +1,98 @@
import assert from "node:assert/strict";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let laboratoryRecordedEvidenceDemand;
let e47SemanticChunkWindowStarts;
let cancelE47SemanticRequestsOutsideWindow;
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
({ laboratoryRecordedEvidenceDemand } = await server.ssrLoadModule(
"/src/core/laboratory/recordedEvidenceProfile.ts",
));
({
e47SemanticChunkWindowStarts,
cancelE47SemanticRequestsOutsideWindow,
} = await server.ssrLoadModule(
"/src/workspaces/laboratory/useE47SemanticTimeline.ts",
));
});
after(async () => {
await server?.close();
});
test("camera-only LAB presentation does not acquire hidden spatial or semantic payloads", () => {
assert.deepEqual(laboratoryRecordedEvidenceDemand({
mediaMode: "video",
spatialMode: null,
showMediaSemantic: false,
showSpatialSemantic: true,
showMediaPoints: false,
classifiedSpatialMode: "overlay",
}), {
sourceTimelineMetadata: true,
recordedVideo: true,
exactCameraFrame: false,
sourceSpatialPoints: false,
cameraPointOverlay: false,
selectedSemanticMask: false,
selectedSemanticPoints: false,
classifiedSpatial: false,
});
});
test("visible M4 layers acquire only their selected camera and spatial evidence", () => {
assert.deepEqual(laboratoryRecordedEvidenceDemand({
mediaMode: "camera",
spatialMode: "plan",
showMediaSemantic: true,
showSpatialSemantic: true,
showMediaPoints: true,
classifiedSpatialMode: "overlay",
}), {
sourceTimelineMetadata: true,
recordedVideo: false,
exactCameraFrame: true,
sourceSpatialPoints: true,
cameraPointOverlay: true,
selectedSemanticMask: true,
selectedSemanticPoints: true,
classifiedSpatial: true,
});
});
test("a classified replacement does not also load the hidden source point track", () => {
const demand = laboratoryRecordedEvidenceDemand({
mediaMode: null,
spatialMode: "3d",
showMediaSemantic: false,
showSpatialSemantic: false,
showMediaPoints: false,
classifiedSpatialMode: "replace-source",
});
assert.equal(demand.sourceSpatialPoints, false);
assert.equal(demand.classifiedSpatial, true);
});
test("semantic point chunks are ordered active-first and stale requests are cancelled", () => {
assert.deepEqual(e47SemanticChunkWindowStarts(48, 4_489), [48, 72, 96, 24]);
assert.deepEqual(e47SemanticChunkWindowStarts(0, 4_489), [0, 24, 48]);
const cancelled = [];
const requests = new Map([
[0, { abort: () => cancelled.push(0) }],
[24, { abort: () => cancelled.push(24) }],
[240, { abort: () => cancelled.push(240) }],
]);
cancelE47SemanticRequestsOutsideWindow(requests, [240, 264]);
assert.deepEqual(cancelled, [0, 24]);
assert.deepEqual([...requests.keys()], [240]);
});
@@ -7,6 +7,7 @@ let createLiveViewerDiagnosticLifecycle;
let createAbortFencedBuildVerifier;
let createUiBuildStaleCoordinator;
let liveViewerDiagnosticBody;
let reloadRecordedViewerAfterStaleModuleFailure;
let server;
let uiBuildIdFromModuleScripts;
let verifyLiveViewerClientBuild;
@@ -22,6 +23,7 @@ before(async () => {
createLiveViewerDiagnosticLifecycle,
createUiBuildStaleCoordinator,
liveViewerDiagnosticBody,
reloadRecordedViewerAfterStaleModuleFailure,
uiBuildIdFromModuleScripts,
verifyLiveViewerClientBuild,
} = await server.ssrLoadModule("/src/core/observation/liveViewerDiagnostics.ts"));
@@ -204,6 +206,36 @@ test("build drift is reported once with the exact loaded build", async () => {
);
});
test("a recorded viewer reloads only when its failed lazy module belongs to a stale build", async () => {
const reloads = [];
const loadedUiBuildId = "/assets/index-abcdefgh.js";
const currentUiBuildId = "/assets/index-ijklmnop.js";
const fetcher = async (_url, options) => {
assert.equal(options.headers["X-MissionCore-UI-Build"], loadedUiBuildId);
return new Response(JSON.stringify({}), {
status: 200,
headers: { "X-MissionCore-UI-Build": currentUiBuildId },
});
};
assert.equal(await reloadRecordedViewerAfterStaleModuleFailure({
loadedUiBuildId,
fetcher,
reload: () => reloads.push("reload"),
}), true);
assert.deepEqual(reloads, ["reload"]);
assert.equal(await reloadRecordedViewerAfterStaleModuleFailure({
loadedUiBuildId: currentUiBuildId,
fetcher: async () => new Response(JSON.stringify({}), {
status: 200,
headers: { "X-MissionCore-UI-Build": currentUiBuildId },
}),
reload: () => reloads.push("unexpected"),
}), false);
assert.deepEqual(reloads, ["reload"]);
});
test("last unsubscribe fences an already queued build verification callback", () => {
const controller = new AbortController();
const observedSignals = [];
@@ -18,6 +18,7 @@ let nextM48ObjectId;
let laboratoryMetricLegendEntries;
let nearestLaboratoryRecordedClipFrame;
let laboratoryRecordedClipEndExclusiveNs;
let laboratoryRecordedClipClockGate;
let m48SpatialPlaybackWindow;
let trimM48SpatialPlaybackCache;
let nextM48CameraVisibility;
@@ -45,6 +46,7 @@ before(async () => {
({
nearestLaboratoryRecordedClipFrame,
laboratoryRecordedClipEndExclusiveNs,
laboratoryRecordedClipClockGate,
} = await server.ssrLoadModule(
"/src/components/laboratory/LaboratoryRecordedClipPlayer.tsx",
));
@@ -342,6 +344,21 @@ test("shared recorded clip clock selects exact frames and one stable loop bounda
assert.equal(laboratoryRecordedClipEndExclusiveNs(frames), 1_300_000_000);
});
test("shared recorded clip rejects stale media callbacks until an explicit seek lands", () => {
assert.deepEqual(laboratoryRecordedClipClockGate(1, 123), {
accept: false,
pendingSequence: 1,
});
assert.deepEqual(laboratoryRecordedClipClockGate(1, 1), {
accept: true,
pendingSequence: null,
});
assert.deepEqual(laboratoryRecordedClipClockGate(null, 124), {
accept: true,
pendingSequence: null,
});
});
test("M4.8 camera and spatial visibility are independent without an empty viewer", () => {
assert.equal(nextM48CameraVisibility("camera", true), true);
assert.equal(nextM48CameraVisibility("3d", true), false);
@@ -379,14 +379,16 @@ test("M4.6 hydrates a lightweight timeline frame from one retained binary point
});
test("M4.6 source cloud opens from one verified bounded chunk instead of the 105 MiB track", async () => {
const pointOffsets = [0, ...Array(4489).fill(2)];
const playbackResultId = `lab-v1-vegetation-shadow-${"b".repeat(64)}`;
const frameCount = 48;
const pointOffsets = [0, ...Array(frameCount).fill(2)];
const points = new Float32Array([11, 20, 30.25, 12, 19.5, 30]);
const pointBytes = points.buffer;
const pointSha256 = createHash("sha256").update(Buffer.from(pointBytes)).digest("hex");
const endpointRoot = "/api/v1/laboratory/m4-threat/results";
const chunks = Array.from({ length: 188 }, (_, index) => {
const chunks = Array.from({ length: Math.ceil(frameCount / 24) }, (_, index) => {
const start = index * 24;
const count = Math.min(24, 4489 - start);
const count = Math.min(24, frameCount - start);
const pointStart = pointOffsets[start];
const pointStop = pointOffsets[start + count];
const pointCount = pointStop - pointStart;
@@ -396,7 +398,7 @@ test("M4.6 source cloud opens from one verified bounded chunk instead of the 105
count,
point_start: pointStart,
point_count: pointCount,
url: `${endpointRoot}/${resultId}/timeline/playback/chunks/${index}`,
url: `${endpointRoot}/${playbackResultId}/timeline/playback/chunks/${index}`,
media_type: "application/octet-stream",
dtype: "<f4",
shape: [pointCount, 3],
@@ -406,8 +408,8 @@ test("M4.6 source cloud opens from one verified bounded chunk instead of the 105
});
const manifestPayload = {
schema_version: "missioncore.recorded-spatial-playback/v1",
result_id: resultId,
frame_count: 4489,
result_id: playbackResultId,
frame_count: frameCount,
point_count: 2,
point_offsets: pointOffsets,
chunk_frame_count: 24,
@@ -416,7 +418,7 @@ test("M4.6 source cloud opens from one verified bounded chunk instead of the 105
chunks,
track: {
id: "points-map-f32",
url: `${endpointRoot}/${resultId}/timeline/playback/tracks/points-map-f32`,
url: `${endpointRoot}/${playbackResultId}/timeline/playback/tracks/points-map-f32`,
media_type: "application/octet-stream",
dtype: "<f4",
shape: [2, 3],
@@ -434,12 +436,12 @@ test("M4.6 source cloud opens from one verified bounded chunk instead of the 105
if (url.endsWith("/timeline/playback/chunks/0")) return new Response(pointBytes.slice(0));
return new Response(null, { status: 404 });
};
const manifest = await fetchM4ThreatPlaybackManifest(resultId, { fetcher });
const manifest = await fetchM4ThreatPlaybackManifest(playbackResultId, { fetcher });
const chunk = await fetchM4ThreatPlaybackPointChunk(manifest, 0, { fetcher });
assert.deepEqual(requested, [
`${endpointRoot}/${resultId}/timeline/playback`,
`${endpointRoot}/${resultId}/timeline/playback/chunks/0`,
`${endpointRoot}/${playbackResultId}/timeline/playback`,
`${endpointRoot}/${playbackResultId}/timeline/playback/chunks/0`,
]);
assert.equal(chunk.pointCount, 2);
assert.equal(chunk.pointStart, 0);
@@ -769,7 +771,7 @@ test("M4.6 local SLAM surface reprojects registered increments into the active b
});
test("M4.6 spatial buffering keeps the active and one future chunk", () => {
assert.deepEqual(m4ThreatChunkWindowStarts(48, 24, 4489), [48, 72]);
assert.deepEqual(m4ThreatChunkWindowStarts(48, 24, 4489), [48, 72, 24]);
assert.deepEqual(m4ThreatChunkWindowStarts(0, 24, 4489), [0, 24]);
});
@@ -786,8 +788,8 @@ test("M4.6 spatial buffering drops stale in-flight windows across rapid jumps",
if (!inFlight.has(start)) inFlight.set(start, controller(start));
}
}
assert.deepEqual([...inFlight.keys()], [4488]);
assert.deepEqual(aborted, [0, 24, 1488, 1512]);
assert.deepEqual([...inFlight.keys()], [4488, 4464]);
assert.deepEqual(aborted, [0, 24, 1488, 1512, 1464]);
});
test("recorded evidence clock advances by selected rate and stops at the sealed end", () => {
@@ -847,8 +849,9 @@ test("recorded VIDEO clock cannot reverse an explicit operator pause", () => {
});
test("M4.6 viewer keeps media and spatial panes on one playback clock", async () => {
const [visual, visualCss, imageScene, videoScene, pointOverlay, metricScene] = await Promise.all([
const [visual, canonical, visualCss, imageScene, videoScene, pointOverlay, metricScene] = await Promise.all([
readFile(new URL("../src/workspaces/laboratory/M4ReplayThreatVisual.tsx", import.meta.url), "utf8"),
readFile(new URL("../src/components/laboratory/CanonicalRecordedLabReplay.tsx", import.meta.url), "utf8"),
readFile(new URL("../src/styles/m4-replay-threat.css", import.meta.url), "utf8"),
readFile(new URL("../src/components/laboratory/RecordedEvidenceImageScene.tsx", import.meta.url), "utf8"),
readFile(new URL("../src/components/laboratory/RecordedEvidenceVideoScene.tsx", import.meta.url), "utf8"),
@@ -858,10 +861,11 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
assert.match(visual, /<RecordedEvidenceVideoScene/);
assert.match(visual, /<RecordedEvidenceImageScene/);
assert.match(visual, /<LaboratoryMetricEvidenceScene/);
assert.match(visual, /m4-replay-threat-visual__deck/);
assert.match(visual, /<CanonicalRecordedLabReplay/);
assert.match(canonical, /m4-replay-threat-visual__deck/);
assert.match(visual, /lastFrameRef/);
assert.match(visual, /lastSpatialFrameRef/);
assert.match(visual, /const spatialFrame = frame\?\.spatialAvailable/);
assert.match(visual, /const spatialFrame = currentSpatialFrame/);
assert.match(visual, /<ObservationTimeline/);
assert.match(visual, /useM4ThreatTimelineFrame/);
assert.match(visual, /resolveObservationSessionReplay\(timeline\.recordedSourceSessionId/);
@@ -876,19 +880,33 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
assert.match(visual, /pointCloudOverlay=/);
assert.match(visual, /mediaMode/);
assert.match(visual, /spatialMode/);
assert.match(visual, /current === next \? null : next/);
assert.match(visual, /data-split=\{splitView \? "true" : undefined\}/);
assert.match(visual, /<SplitPane/);
assert.match(visual, /primarySize=\{splitView \? splitPrimarySize : mediaMode \? 100 : 0\}/);
assert.match(visual, /resizable=\{splitView\}/);
assert.match(visual, /separatorLabel="Изменить размер VIDEO\/CAMERA и 3D\/PLAN"/);
assert.match(visual, /secondaryMode=\{\{/);
assert.match(canonical, /current === next \? null : next/);
assert.match(canonical, /data-split=\{splitView \? "true" : undefined\}/);
assert.match(canonical, /<SplitPane/);
assert.match(canonical, /primary=\{mediaPane\}/);
assert.match(canonical, /secondary=\{spatialPane/);
assert.match(canonical, /primarySize=\{splitView \? splitPrimarySize : mediaMode !== "none" \? 100 : 0\}/);
assert.match(canonical, /resizable=\{splitView\}/);
assert.match(canonical, /separatorLabel="Изменить размер VIDEO\/CAMERA и 3D\/PLAN"/);
assert.match(canonical, /secondaryMode=\{\{/);
assert.match(visual, /playback=\{playbackController\.playback\}/);
assert.match(visual, /clock: mediaMode === "video" \? "external" : "animation"/);
assert.match(visual, /clock: "external"/);
assert.match(visual, /useCanonicalRecordedLabReplayState/);
assert.match(canonical, /current === next \? null : next/);
assert.match(visual, /timelineFrame\.activeSequence \+ 1/);
assert.match(visual, /segmentCount=\{timeline\.frameCount\}/);
assert.match(visual, /onPlaybackChange=\{playbackController\.synchronize\}/);
assert.match(visual, /onPlayingRejected=\{\(\) => playbackController\.setPlaying\(false\)\}/);
assert.match(
await readFile(new URL("../src/components/laboratory/useRecordedEvidencePlayback.ts", import.meta.url), "utf8"),
/if \(clock === "animation"\) return;/,
);
assert.match(visual, /playbackAuthority="media"/);
assert.match(visual, /playbackTransport = "epoch-stream"/);
assert.match(visual, /playbackTransport=\{playbackTransport\}/);
assert.match(
await readFile(new URL("../src/components/RecordedFmp4Player.tsx", import.meta.url), "utf8"),
/if \(playbackAuthority === "host"\) return;/,
);
assert.match(visual, /currentSeconds: playbackController\.playback\.currentSeconds/);
assert.match(visualCss, /m4-replay-threat-visual__deck > \.nodedc-split-pane/);
assert.match(visualCss, /m4-replay-threat-visual__pane-toolbar\[data-pane-toolbar="media"\]/);
@@ -912,6 +930,7 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
assert.match(visualCss, /laboratory-metric-evidence-scene__legend/);
assert.match(visualCss, /bottom: auto/);
assert.match(videoScene, /<RecordedFmp4Player/);
assert.match(videoScene, /!playback\.playing && Math\.abs\(next\.currentSeconds - playback\.currentSeconds\) > 0\.35/);
assert.match(imageScene, /<RecordedEvidenceBoxOverlay/);
assert.match(imageScene, /<RecordedEvidencePointCloudOverlay/);
assert.match(videoScene, /<RecordedEvidencePointCloudOverlay/);
@@ -920,19 +939,20 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
assert.match(metricScene, /OrbitControls/);
assert.match(visual, /LOCAL SLAM/);
assert.match(visual, /showLocalSurface/);
assert.match(visual, /const latestAvailableSpatialFrame = \[\.\.\.timelineFrame\.availableFrames\][\s\S]*candidate\.spatialAvailable[\s\S]*candidate\.sequence <= timelineFrame\.activeSequence/);
assert.match(
visual,
/pointCloudBodyXyzM=\{displayedClassifiedSpatialFrame && replaceClassifiedPointCloud[\s\S]*\? classifiedPointsBody[\s\S]*: classifiedContextSpatialFrame\?\.pointCloudBodyXyzM \?\? \[\]\}/,
/pointCloudBodyXyzM=\{displayedClassifiedSpatialFrame && replaceClassifiedPointCloud[\s\S]*\? classifiedPointsBody[\s\S]*: classifiedContextSpatialFrame\?\.pointCloudBodyXyzM[\s\S]*\?\? activeSpatialFrame\?\.pointCloudBodyXyzM[\s\S]*\?\? \[\]\}/,
);
assert.match(
visual,
/const classifiedSpatialFrame = classifiedSpatialLayer\?\.frame\?\.sourceSequence === timelineFrame\.activeSequence[\s\S]*lastClassifiedSpatialFrameRef[\s\S]*const displayedClassifiedSpatialFrame = classifiedSpatialFrame\s*&&\s*classifiedSpatialFrame\.sampleAvailable !== false/,
/const classifiedSpatialFrame = hasClassifiedSpatialOutput[\s\S]*classifiedSpatialLayer\?\.frame\?\.sourceSequence === timelineFrame\.activeSequence[\s\S]*lastClassifiedSpatialFrameRef[\s\S]*const displayedClassifiedSpatialFrame = classifiedSpatialFrame\s*&&\s*classifiedSpatialFrame\.sampleAvailable !== false/,
);
assert.doesNotMatch(visual, /classifiedSpatialFrame\?\.sampleAvailable !== false/);
assert.match(visual, /timelineFrame\.availableFrames\.find/);
assert.match(visual, /localSurfaceBodyXyzM=\{localSurface\.pointsBodyXyzM\}/);
assert.match(visual, /showLocalSurface=\{showLocalSurface\}/);
assert.match(visual, /\{semantic \? \([\s\S]*>\s*SEMANTICS\s*<\/Button>/);
assert.match(visual, /\{activeSemantic \? \([\s\S]*>\s*SEMANTICS\s*<\/Button>/);
assert.match(visual, /current safety — все \{classifiedCellCount\.toLocaleString\("ru-RU"\)\} TGS-ячейки UNOBSERVED/);
assert.match(
visual,
@@ -951,12 +971,18 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
});
test("M4.6 keeps the recorded VIDEO player mounted across media mode toggles", async () => {
const visual = await readFile(
new URL("../src/workspaces/laboratory/M4ReplayThreatVisual.tsx", import.meta.url),
"utf8",
);
assert.match(visual, /const mediaPane = \(/);
assert.match(visual, /hidden=\{!mediaMode\}/);
const [visual, canonical] = await Promise.all([
readFile(
new URL("../src/workspaces/laboratory/M4ReplayThreatVisual.tsx", import.meta.url),
"utf8",
),
readFile(
new URL("../src/components/laboratory/CanonicalRecordedLabReplay.tsx", import.meta.url),
"utf8",
),
]);
assert.match(canonical, /const mediaPane = \(/);
assert.match(canonical, /hidden=\{mediaMode === "none"\}/);
assert.match(visual, /data-media="video"/);
assert.match(visual, /hidden=\{mediaMode !== "video"\}/);
assert.match(visual, /\{videoSource \? \(/);
@@ -42,6 +42,8 @@ let resolveRecordedBlueprintUrl;
let fetchRecordedBlueprintRrd;
let resolveRecordedPerceptionUrl;
let fetchRecordedPerceptionRrd;
let resolveRecordedPerceptionViewerSourceUrl;
let probeRecordedPerceptionViewerSource;
let resolveRecordedPointColorsUrl;
let fetchRecordedPointColorsRrd;
let recordedPointColorKey;
@@ -110,6 +112,8 @@ before(async () => {
fetchRecordedBlueprintRrd,
resolveRecordedPerceptionUrl,
fetchRecordedPerceptionRrd,
resolveRecordedPerceptionViewerSourceUrl,
probeRecordedPerceptionViewerSource,
resolveRecordedPointColorsUrl,
fetchRecordedPointColorsRrd,
recordedPointColorKey,
@@ -407,6 +411,15 @@ test("recorded blueprint endpoint is derived only from canonical same-origin RRD
),
null,
);
assert.equal(
resolveRecordedBlueprintUrl(
`/api/v1/laboratory/vegetation-shadow/lab-v1-vegetation-shadow-${"a".repeat(64)}`
+ "/canonical-replay.rrd",
"http://127.0.0.1:5174",
"/api/v1/observation-sessions/session-1/blueprint.rrd",
),
"http://127.0.0.1:5174/api/v1/observation-sessions/session-1/blueprint.rrd",
);
});
test("recorded replay becomes ready only after the complete declared timeline is buffered", () => {
@@ -485,6 +498,8 @@ test("recorded blueprint fetch is bounded, strict and sends only display setting
view_reset_generation: 1,
follow_trajectory: true,
unified_perception: true,
semantic_layer: null,
plan_view: false,
show_detections_2d: true,
show_segmentation: false,
show_cuboids_3d: true,
@@ -664,6 +679,74 @@ test("recorded perception fetch admits one complete same-origin RRD or no layer"
assert.equal(absent, null);
});
test("LAB perception sidecar is streamed by native Rerun from one generation-bound URL", async () => {
const endpoint =
`http://127.0.0.1:5174/api/v1/laboratory/vegetation-shadow/` +
`lab-v1-vegetation-shadow-${"a".repeat(64)}/canonical-overlay.rrd`;
const sourceUrl = resolveRecordedPerceptionViewerSourceUrl(
endpoint,
{ applicationId: "nodedc_mission_core_recorded", recordingId: "recording-001" },
"b".repeat(64),
"http://127.0.0.1:5174",
);
assert.equal(
sourceUrl,
`${endpoint}?application_id=nodedc_mission_core_recorded` +
`&recording_id=recording-001&generation=${"b".repeat(64)}`,
);
const overlayGeneration = "c".repeat(64);
const probe = await probeRecordedPerceptionViewerSource(sourceUrl, {
origin: "http://127.0.0.1:5174",
fetcher: async (input, init) => {
assert.equal(String(input), sourceUrl);
assert.equal(init.method, "HEAD");
assert.equal(new Headers(init.headers).get("Range"), null);
return new Response(null, {
status: 200,
headers: {
"Content-Type": "application/vnd.rerun.rrd",
"Content-Length": "186058411",
"ETag": `"${overlayGeneration}"`,
"X-Rerun-Format": "RRF2",
},
});
},
});
assert.deepEqual(probe, {
sourceUrl: `${sourceUrl}&overlay_generation=${overlayGeneration}`,
byteLength: 186_058_411,
});
});
test("LAB native source rejects an unsealed overlay descriptor", async () => {
const endpoint =
`http://127.0.0.1:5174/api/v1/laboratory/vegetation-shadow/` +
`lab-v1-vegetation-shadow-${"a".repeat(64)}/canonical-overlay.rrd`;
const sourceUrl = resolveRecordedPerceptionViewerSourceUrl(
endpoint,
{ applicationId: "nodedc_mission_core_recorded", recordingId: "recording-001" },
"b".repeat(64),
"http://127.0.0.1:5174",
);
await assert.rejects(
probeRecordedPerceptionViewerSource(sourceUrl, {
origin: "http://127.0.0.1:5174",
fetcher: async () => new Response(null, {
status: 200,
headers: {
"Content-Type": "application/vnd.rerun.rrd",
"Content-Length": "186058411",
"ETag": `"${"c".repeat(64)}"`,
"X-Rerun-Format": "RRF1",
},
}),
}),
/Invalid recorded perception viewer response/,
);
});
test("recorded replay creates an isolated source catalog without live device bindings", () => {
const sources = recordedObservationSources({
kind: "rerun-recording",
@@ -11,7 +11,12 @@ let recordedMediaSeekableCoverage;
let recordedMediaFragmentUrl;
let recordedMediaDecodeStartSequence;
let recordedMediaSegmentAppendOrder;
let recordedMediaSegmentSequenceAtTime;
let recordedMediaCanRollTarget;
let recordedMediaTimestampStallRecoveryTarget;
let nextRecordedMediaRandomAccessSequence;
let recordedMediaRecoveryTargetSequence;
let selectRecordedMediaPreparationEpoch;
before(async () => {
server = await createServer({
@@ -26,7 +31,12 @@ before(async () => {
recordedMediaFragmentUrl,
recordedMediaDecodeStartSequence,
recordedMediaSegmentAppendOrder,
recordedMediaSegmentSequenceAtTime,
recordedMediaCanRollTarget,
recordedMediaTimestampStallRecoveryTarget,
nextRecordedMediaRandomAccessSequence,
recordedMediaRecoveryTargetSequence,
selectRecordedMediaPreparationEpoch,
} = await server.ssrLoadModule("/src/components/RecordedFmp4Player.tsx"));
});
@@ -161,7 +171,7 @@ test("decoded duration and seekable range cover the complete declared epoch", ()
assert.equal(recordedMediaSeekableCoverage(20, 20, 20, 1, 1.01), false);
});
test("recorded player keeps full-archive range fallback and uses bounded generation-bound fragments", async () => {
test("production replay derives bounded fragments and retains native range fallback", async () => {
const source = await readFile(
new URL("../src/components/RecordedFmp4Player.tsx", import.meta.url),
"utf8",
@@ -176,6 +186,13 @@ test("recorded player keeps full-archive range fallback and uses bounded generat
assert.match(source, /hasPresentedFrame/);
assert.match(source, /retainForwardFrame/);
assert.match(source, /candidateTarget\.sequence >= previousTarget\.sequence/);
assert.match(source, /effectiveSegmentCount = segmentCount \?\? epoch\?\.segmentCount \?\? null/);
assert.match(source, /requestedSegmentSequence = segmentSequence \?\?/);
assert.match(source, /waitForRecordedVideoInitialFrame/);
assert.match(source, /setSegmentRecoveryGeneration/);
assert.match(source, /resumePlaybackIfRequested/);
assert.match(source, /!playbackPlayingRef\.current/);
assert.match(source, /await video\.play\(\)/);
assert.equal(recordedMediaDecodeStartSequence([1, 1491, 1501], 1500), 1491);
assert.deepEqual(
@@ -209,6 +226,44 @@ test("recorded player keeps full-archive range fallback and uses bounded generat
);
});
test("production replay derives its bounded fragment directly from the source clock", () => {
const ends = [0.101, 0.185, 0.286, 0.401];
const epochStart = 39.215263458;
assert.equal(recordedMediaSegmentSequenceAtTime(ends, epochStart, epochStart), 1);
assert.equal(recordedMediaSegmentSequenceAtTime(ends, epochStart, epochStart + 0.101), 1);
assert.equal(recordedMediaSegmentSequenceAtTime(ends, epochStart, epochStart + 0.103), 2);
assert.equal(recordedMediaSegmentSequenceAtTime(ends, epochStart, epochStart + 0.4), 4);
assert.equal(recordedMediaSegmentSequenceAtTime(ends, epochStart, epochStart + 1), null);
});
test("a corrupt fragment advances recovery to the next random-access frame", () => {
assert.equal(nextRecordedMediaRandomAccessSequence([1, 11, 21, 49], 1), 11);
assert.equal(nextRecordedMediaRandomAccessSequence([1, 11, 21, 49], 20), 21);
assert.equal(nextRecordedMediaRandomAccessSequence([1, 11, 21, 49], 49), null);
assert.equal(nextRecordedMediaRandomAccessSequence([1, 11, 21, 49], 0), null);
});
test("paused seek displays the recovery keyframe until the source clock leaves the corrupt GOP", () => {
assert.equal(recordedMediaRecoveryTargetSequence(120, 120, 149), 149);
assert.equal(recordedMediaRecoveryTargetSequence(130, 120, 149), 149);
assert.equal(recordedMediaRecoveryTargetSequence(119, 120, 149), 119);
assert.equal(recordedMediaRecoveryTargetSequence(149, 120, 149), 149);
assert.equal(recordedMediaRecoveryTargetSequence(151, 120, 149), 151);
assert.equal(recordedMediaRecoveryTargetSequence(null, 120, 149), null);
assert.equal(recordedMediaRecoveryTargetSequence(120, null, null), 120);
});
test("camera admission selects one bounded epoch while the shared clock is outside video", () => {
const epochs = [
{ ordinal: 1, timelineStartSeconds: 39, timelineEndSeconds: 100 },
{ ordinal: 2, timelineStartSeconds: 120, timelineEndSeconds: 180 },
];
assert.equal(selectRecordedMediaPreparationEpoch(epochs, 0).ordinal, 1);
assert.equal(selectRecordedMediaPreparationEpoch(epochs, 70).ordinal, 1);
assert.equal(selectRecordedMediaPreparationEpoch(epochs, 110).ordinal, 2);
assert.equal(selectRecordedMediaPreparationEpoch(epochs, 200).ordinal, 2);
});
test("recorded player preserves forward rolling playback but seeks backward clip loops", () => {
assert.equal(recordedMediaCanRollTarget(20, 21, true, true), true);
assert.equal(recordedMediaCanRollTarget(20, 20, true, true), true);
@@ -217,6 +272,12 @@ test("recorded player preserves forward rolling playback but seeks backward clip
assert.equal(recordedMediaCanRollTarget(20, 21, true, false), false);
});
test("recorded player skips only a proven buffered corrupt timestamp interval", () => {
assert.equal(recordedMediaTimestampStallRecoveryTarget(11.422, [[0, 16.287]]), 11.602);
assert.equal(recordedMediaTimestampStallRecoveryTarget(16.25, [[0, 16.287]]), null);
assert.equal(recordedMediaTimestampStallRecoveryTarget(20, [[0, 16.287]]), null);
});
test("loading and error overlays fully conceal recorded camera pixels", async () => {
const css = await readFile(
new URL("../src/styles/observation.css", import.meta.url),
@@ -34,7 +34,7 @@ function camera(phase, byteLength = 1_024) {
return { phase, byteLength, message: null };
}
test("recorded session admits RRD and every declared camera as one atomic generation", () => {
test("recorded session keeps camera and timeline atomic without hiding a ready spatial frame", () => {
const ids = ["camera.left", "camera.right"];
const oneCamera = {
"camera.left": camera("ready"),
@@ -42,7 +42,7 @@ test("recorded session admits RRD and every declared camera as one atomic genera
};
const partialGate = admission.recordedSessionAdmissionPhase("ready", ids, oneCamera);
assert.equal(partialGate, "loading");
assert.equal(rerunPresentationStatus("ready", partialGate, true), "loading");
assert.equal(rerunPresentationStatus("ready", partialGate, true), "ready");
assert.equal(
recordedMediaPresentationState("ready", "generation", "generation", false, partialGate),
"loading",
@@ -71,7 +71,7 @@ test("any RRD or camera failure closes the complete recorded session", () => {
...cameras,
"camera.right": camera("ready"),
}), "error");
assert.equal(rerunPresentationStatus("ready", "error", true), "error");
assert.equal(rerunPresentationStatus("ready", "error", true), "ready");
assert.equal(
recordedMediaPresentationState("ready", "generation", "generation", false, "error"),
"error",
@@ -13,7 +13,6 @@ let liveTimelineNeedsSynchronization;
let liveRerunReceiverBindingIdentity;
let recordedOpenWatchdogTimeoutMs;
let rerunViewerInitialSource;
let rerunViewerOpenOptions;
let resolveRecordedViewerSourceUrl;
before(async () => {
@@ -31,7 +30,6 @@ before(async () => {
liveRerunReceiverBindingIdentity,
recordedOpenWatchdogTimeoutMs,
rerunViewerInitialSource,
rerunViewerOpenOptions,
resolveRecordedViewerSourceUrl,
} = await server.ssrLoadModule("/src/components/RerunViewport.tsx"));
});
@@ -71,9 +69,21 @@ test("only the canonical digest-bound generation URL reaches the native Rerun re
}
});
test("only the live receiver opens on the native following edge", () => {
assert.deepEqual(rerunViewerOpenOptions(true), { follow_if_http: true });
assert.equal(rerunViewerOpenOptions(false), null);
test("one canonical LAB replay generation reaches the same native receiver", () => {
const labSource =
`/api/v1/laboratory/vegetation-shadow/lab-v1-vegetation-shadow-${"c".repeat(64)}`
+ "/canonical-replay.rrd";
const descriptor = {
sourceUrl: labSource,
viewerSourceUrl: `${labSource}?generation=${sha256}`,
byteLength: 300_000_000,
sha256,
};
assert.equal(
resolveRecordedViewerSourceUrl(descriptor, "http://mission-core.test"),
`http://mission-core.test${descriptor.viewerSourceUrl}`,
);
});
test("live presentation waits for the exact receiver to expose a usable range", () => {
@@ -216,7 +226,7 @@ test("recorded RRD bytes are never split across LogChannel.send_rrd calls", asyn
assert.doesNotMatch(source, /missioncore\/recorded-recording/);
assert.doesNotMatch(source, /recordedChannel/);
assert.match(source, /viewer\.start\(\s*rerunViewerInitialSource\(resolvedSource\)/s);
assert.match(source, /rerunViewerOpenOptions\(followLive\)/);
assert.doesNotMatch(source, /rerunViewerOpenOptions/);
assert.match(
source,
/recordingOpened = true;[\s\S]*diagnosticLifecycle\.markAdmitted\(\);/,
@@ -257,6 +267,14 @@ test("recorded RRD bytes are never split across LogChannel.send_rrd calls", asyn
source,
/if \(readyToRender && !readyPublished\)[\s\S]*clearRecordedAdmissionWatchdog\(\);/,
);
assert.match(
source,
/reloadRecordedViewerAfterStaleModuleFailure\(\{[\s\S]*loadedUiBuildId: diagnosticLifecycle\.lineage\.uiBuildId/,
);
assert.match(
source,
/!recordedPerceptionUrl \|\| !recordedPerceptionLayers\.enabled/,
);
});
test("one live document owns one native Rerun receiver", async () => {
@@ -287,8 +305,12 @@ test("raw replay exercises the same streaming receiver lifecycle as a live scan"
source,
/const livePresentationActivitySequence = metrics\?\.publishedFrameCount \?\?[\s\S]*liveRerunSource && !streamActive \? 1 : null/,
);
assert.match(source, /followLive=\{liveRerunSource\}/);
assert.match(source, /liveActivitySequence=\{livePresentationActivitySequence\}/);
assert.match(
source,
/liveAcquisitionRerunProfile\(\{[\s\S]*liveActivitySequence: livePresentationActivitySequence/,
);
assert.match(source, /<RerunViewport[\s\S]*profile=\{rerunViewerProfile\}/);
assert.doesNotMatch(source, /followLive=\{liveRerunSource\}/);
assert.match(
source,
/sourceUrl\.trim\(\) && pointCloudVisible && !intentionalSourceEnd/,
@@ -6,11 +6,13 @@ import { createServer } from "vite";
let server;
let canPublishRecordedPlaybackController;
let createRecordedAutoplayGate;
let createRecordedInitialSeekGate;
let isRecordedPlaybackReady;
let isRecordedPlaybackPresentationReady;
let isUsableRecordedPlaybackRange;
let recordedPlaybackBufferState;
let recordedPlaybackRangeWhenReady;
let rerunPresentationStatus;
before(async () => {
server = await createServer({
@@ -21,11 +23,13 @@ before(async () => {
({
canPublishRecordedPlaybackController,
createRecordedAutoplayGate,
createRecordedInitialSeekGate,
isRecordedPlaybackReady,
isRecordedPlaybackPresentationReady,
isUsableRecordedPlaybackRange,
recordedPlaybackBufferState,
recordedPlaybackRangeWhenReady,
rerunPresentationStatus,
} = await server.ssrLoadModule("/src/components/RerunViewport.tsx"));
});
@@ -33,7 +37,7 @@ after(async () => {
await server?.close();
});
test("a first frame reports buffer telemetry but is not ready for presentation", () => {
test("a verified first frame is presentable while full-range controls stay closed", () => {
assert.equal(isUsableRecordedPlaybackRange(null), false);
assert.equal(isUsableRecordedPlaybackRange({ min: Number.NaN, max: 0 }), false);
assert.equal(isUsableRecordedPlaybackRange({ min: 2, max: 1 }), false);
@@ -47,8 +51,11 @@ test("a first frame reports buffer telemetry but is not ready for presentation",
bufferProgress: 0,
fullyBuffered: false,
});
assert.equal(isRecordedPlaybackReady(true, true, firstFrame), false);
assert.equal(isRecordedPlaybackReady(true, true, firstFrame), true);
assert.equal(recordedPlaybackRangeWhenReady({ min: 0, max: 0 }, firstFrame, true), null);
assert.equal(rerunPresentationStatus("ready", "loading", true), "ready");
assert.equal(rerunPresentationStatus("loading", "ready", true), "loading");
assert.equal(rerunPresentationStatus("ready", "error", true), "ready");
});
test("host timeline remains unmounted until the verified recording is fully ready", () => {
@@ -107,7 +114,7 @@ test("buffer progress grows independently and preserves the full-buffer toleranc
);
assert.equal(missingDeclaredStart.bufferProgress, 1);
assert.equal(missingDeclaredStart.fullyBuffered, false);
assert.equal(isRecordedPlaybackReady(true, true, missingDeclaredStart), false);
assert.equal(isRecordedPlaybackReady(true, true, missingDeclaredStart), true);
});
test("a verified split boundary spill covers and clamps the declared LAB window", () => {
@@ -154,6 +161,31 @@ test("recorded autoplay waits for the full range and then runs exactly once", ()
assert.equal(gate.attempted(), true);
});
test("paused recorded replay seeks once to its first presentable frame", () => {
const gate = createRecordedInitialSeekGate();
const seeks = [];
const range = { min: 0, max: 535_717_620_042 };
assert.equal(gate.attempt(
true,
true,
true,
range,
(value) => seeks.push(value),
39_215_263_458,
), true);
assert.equal(gate.attempt(
true,
true,
true,
range,
(value) => seeks.push(value),
50_000_000_000,
), false);
assert.deepEqual(seeks, [39_215_263_458]);
assert.equal(gate.attempted(), true);
});
test("recorded autoplay starts at the first presentable camera frame without shrinking the range", () => {
const gate = createRecordedAutoplayGate();
const seeks = [];
@@ -1,81 +1,36 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import test from "node:test";
import makeRerunRuntime from "../vendor/rerun-web-viewer-0.34.1/re_viewer.nodedc.js";
const root = resolve(import.meta.dirname, "..");
const packageRoot = resolve(root, "node_modules/@rerun-io/web-viewer");
const vendorRoot = resolve(root, "vendor/rerun-web-viewer-0.34.1");
const readJson = (path) => JSON.parse(readFileSync(path, "utf8"));
const sha256 = (path) =>
createHash("sha256").update(readFileSync(path)).digest("hex");
test("Mission Core uses the exact upstream Rerun 0.36.3 web package", () => {
const application = readJson(resolve(root, "package.json"));
const installed = readJson(resolve(packageRoot, "package.json"));
test("NODE.DC Rerun runtime is the audited 0.34.1 spatial camera build", () => {
const manifest = JSON.parse(readFileSync(resolve(packageRoot, "package.json"), "utf8"));
assert.equal(manifest.version, "0.34.1");
const expectedWasm = "ffe7543d28bb3394f289f6299de43d038767eef83d781c2b7f8f5683308a0469";
const expectedGlue = "0f7b76c9f24cbd8437021b5d37499894aeadc586183e422ebc82ef556d7b8339";
assert.equal(sha256(resolve(vendorRoot, "re_viewer_bg.nodedc.wasm")), expectedWasm);
assert.equal(sha256(resolve(vendorRoot, "re_viewer.nodedc.js")), expectedGlue);
assert.equal(sha256(resolve(packageRoot, "re_viewer_bg.wasm")), expectedWasm);
assert.equal(sha256(resolve(packageRoot, "re_viewer.js")), expectedGlue);
assert.equal(application.dependencies["@rerun-io/web-viewer"], "0.36.3");
assert.equal(installed.version, "0.36.3");
assert.equal(application.scripts.postinstall, undefined);
});
test("custom JavaScript glue references only exports present in its paired WASM", () => {
const wasmPath = resolve(vendorRoot, "re_viewer_bg.nodedc.wasm");
const gluePath = resolve(vendorRoot, "re_viewer.nodedc.js");
const module = new WebAssembly.Module(readFileSync(wasmPath));
const exports = new Set(WebAssembly.Module.exports(module).map(({ name }) => name));
const imports = WebAssembly.Module.imports(module);
const glue = readFileSync(gluePath, "utf8");
const referencedExports = new Set(
[...glue.matchAll(/\bwasm\.([A-Za-z_$][\w$]*)/g)].map((match) => match[1]),
);
const missingExports = [...referencedExports].filter((name) => !exports.has(name));
test("the active application never imports or installs the archived vendor fork", () => {
const application = readFileSync(resolve(root, "package.json"), "utf8");
const viewport = readFileSync(resolve(root, "src/components/RerunViewport.tsx"), "utf8");
assert.equal(imports.length, 927);
assert.equal(exports.size, 79);
assert.deepEqual(missingExports, []);
assert.match(glue, /export default function\(\)/);
assert.match(glue, /if \(!wasm\) return;/);
assert.doesNotMatch(application, /patch-rerun-web-viewer/);
assert.doesNotMatch(application, /vendor\/rerun-web-viewer/);
assert.doesNotMatch(viewport, /vendor\/rerun-web-viewer/);
assert.doesNotMatch(viewport, /0\.34\.1/);
});
test("custom Rerun WASM initializes and grows its externref table", () => {
const runtime = makeRerunRuntime();
runtime.initSync({
module: readFileSync(resolve(vendorRoot, "re_viewer_bg.nodedc.wasm")),
});
test("upstream WebViewer exposes one three-argument start contract", () => {
const declaration = readFileSync(resolve(packageRoot, "index.d.ts"), "utf8");
assert.equal(typeof runtime.WebHandle, "function");
runtime.deinit();
});
test("source patch carries pointer navigation, persistent follow, and camera continuity tests", () => {
const patch = readFileSync(resolve(vendorRoot, "NODEDC_ZOOM_TO_CURSOR.patch"), "utf8");
assert.match(patch, /fn pointer_ray_direction/);
assert.match(patch, /fn zoom_orbit_towards_pointer/);
assert.match(patch, /near_limit_hands_excess_zoom_to_cursor_directed_dolly/);
assert.match(patch, /crossing_near_limit_preserves_unconsumed_scene_scaled_zoom/);
assert.match(patch, /remaining_zoom_factor\.ln\(\) \* self\.speed/);
assert.match(patch, /off_center_pointer_stays_on_the_same_view_ray/);
assert.match(patch, /fn rotate_radians_around_anchor/);
assert.match(patch, /orbit_drag_anchor/);
assert.match(patch, /minimum_orbital_navigation_speed/);
assert.match(patch, /orbital_rotation_keeps_selected_anchor_on_the_same_view_ray/);
assert.match(patch, /orbital_navigation_speed_floor_tracks_scene_scale/);
assert.match(patch, /NODEDC_PERSISTENT_ORBIT_TRACKING_ENTITY/);
assert.match(patch, /nodedc_rig_orbit_tracking_is_persistent/);
assert.match(patch, /restore_persistent_orbit_eye_after_blueprint_update/);
assert.match(
patch,
/persistent_rig_follow_restores_the_last_rendered_eye_after_blueprint_update/,
declaration,
/start\(rrd: string \| string\[\] \| null, parent: HTMLElement \| null, options: WebViewerOptions \| null\): Promise<void>/,
);
assert.match(patch, /explicit_blueprint_pose_is_not_replaced_by_the_previous_eye/);
assert.match(patch, /previous_picking_result/);
});
@@ -75,20 +75,29 @@ test("semantic point alignment follows the last qualified spatial increment", as
});
test("M4 keeps independent semantic controls in media and spatial panes", async () => {
const source = await readFile(
new URL("../src/workspaces/laboratory/M4ReplayThreatVisual.tsx", import.meta.url),
"utf8",
);
const [source, canonical] = await Promise.all([
readFile(
new URL("../src/workspaces/laboratory/M4ReplayThreatVisual.tsx", import.meta.url),
"utf8",
),
readFile(
new URL("../src/components/laboratory/CanonicalRecordedLabReplay.tsx", import.meta.url),
"utf8",
),
]);
assert.match(source, /showMediaSemantic/);
assert.match(source, /showSpatialSemantic/);
assert.match(source, /semantic && showMediaSemantic && frame/);
assert.match(source, /activeSpatialSemantic = spatialSemantic \?\? activeSemantic/);
assert.match(source, /spatialSemanticClasses/);
assert.match(source, /spatialSemanticPalette/);
assert.match(source, /activeSemantic && evidenceDemand\.selectedSemanticMask && frame/);
assert.match(source, /Array\.from\(\{ length: 12 \}, \(_, index\) => index \+ 1\)/);
assert.match(source, /\|\| !showSpatialSemantic/);
assert.match(source, /aria-label="Слои камеры и видео"/);
assert.match(source, /aria-label="Слои 3D и плана"/);
assert.match(source, /data-pane-mode="media"/);
assert.match(source, /data-pane-mode="spatial"/);
assert.match(source, /modeControlsVisible=\{!splitView\}/);
assert.match(canonical, /data-pane-mode="media"/);
assert.match(canonical, /data-pane-mode="spatial"/);
assert.match(canonical, /modeControlsVisible=\{!splitView\}/);
assert.match(source, /semanticOverlay=\{mediaMode === "video" \? semanticOverlay : undefined\}/);
});
@@ -100,6 +100,8 @@ test("PlayCanvas owns the realtime scene graph without an iframe or React entity
assert.match(runtime, /maximum: \{[^}]*lodRangeMin: 0, lodRangeMax: 5, pixelRatio: 2, splatBudget: 4_000_000, targetFps: 60/);
assert.match(runtime, /app\.scene\.gsplat\.splatBudget = profile\.splatBudget/);
assert.match(runtime, /app\.autoRender = false/);
assert.match(runtime, /app\.systems\.gsplat\?\.on\("frame:request", this\.requestGsplatFrame\)/);
assert.match(runtime, /app\.systems\.gsplat\?\.off\("frame:request", this\.requestGsplatFrame\)/);
assert.match(runtime, /app\.on\("update", this\.updateFrame\)/);
assert.match(runtime, /if \(!cameraChanged && this\.controlMode === "free"\) return/);
assert.doesNotMatch(runtime, /app\.on\("frameupdate"/);
@@ -162,6 +164,8 @@ test("PlayCanvas owns the realtime scene graph without an iframe or React entity
assert.match(ugv, /new this\.ammo\.btRaycastVehicle/);
assert.match(ugv, /type: "mesh"/);
assert.match(ugv, /MeshoptSimplifier\.simplify/);
assert.match(ugv, /MeshoptSimplifier\.simplifySloppy/);
assert.match(ugv, /Physics proxy превысил лимит/);
assert.match(ugv, /PHYSICS_PROXY_MAX_TRIANGLES = 240_000/);
assert.match(ugv, /createPhysicsProxyMesh/);
assert.match(ugv, /findSafeSpawnPosition/);
@@ -179,8 +183,33 @@ test("PlayCanvas owns the realtime scene graph without an iframe or React entity
assert.match(ugv, /desiredSpeed = forwardInput \* maxSpeed/);
assert.match(ugv, /maximumAcceleration = clamp\(1\.4 \+ maxSpeed \* 0\.35, 1\.8, 4\.2\)/);
assert.match(ugv, /SERVICE_BRAKE_DECELERATION_MPS2 = 1\.8/);
assert.match(ugv, /horizontalSpeed - deceleration \* Math\.max\(0, deltaSeconds\)/);
assert.match(ugv, /this\.vehicle\.setBrake\(0, index\)/);
assert.match(ugv, /PARKING_BRAKE_HOLD_DECELERATION_MPS2 = 6/);
assert.match(ugv, /PARKING_BRAKE_ENGAGE_SPEED_MPS = 0\.08/);
assert.match(ugv, /TYRE_FRICTION_SLIP = 8\.5/);
assert.match(ugv, /TYRE_STATIC_FRICTION_COEFFICIENT = 0\.95/);
assert.match(ugv, /TYRE_KINETIC_FRICTION_COEFFICIENT = 0\.78/);
assert.match(ugv, /TYRE_CONTACT_VELOCITY_RESPONSE_PER_SECOND = 10/);
assert.match(ugv, /GRAVITY_METERS_PER_SECOND_SQUARED = 9\.81/);
assert.match(ugv, /holding = !braking && forwardInput === 0 && turnInput === 0/);
assert.match(ugv, /longitudinalSpeedMetersPerSecond/);
assert.match(ugv, /parkingBrakeEngaged = holding/);
assert.match(ugv, /this\.settings\.massKg \* brakeDeceleration/);
assert.match(ugv, /this\.vehicle\.setBrake\(wheelBrakeForce, index\)/);
assert.match(ugv, /set_m_frictionSlip\(\s*parkingBrakeEngaged \? 0 : TYRE_FRICTION_SLIP/);
assert.match(ugv, /applyParkingTyreContact/);
assert.match(ugv, /wheel\.get_m_wheelsSuspensionForce\(\)/);
assert.doesNotMatch(ugv, /get_m_isInContact/);
assert.match(ugv, /normalForce < MIN_TYRE_NORMAL_FORCE_NEWTONS/);
assert.match(ugv, /lateralGravityAcceleration/);
assert.match(ugv, /longitudinalGravityAcceleration/);
assert.match(ugv, /TYRE_CONTACT_VELOCITY_RESPONSE_PER_SECOND \* lateralSlipSpeed/);
assert.match(ugv, /TYRE_CONTACT_VELOCITY_RESPONSE_PER_SECOND \* longitudinalSlipSpeed/);
assert.match(ugv, /Math\.hypot\(trialLateralForce, trialLongitudinalForce\)/);
assert.match(ugv, /body\.applyImpulse\(this\.tyreImpulseNative, this\.tyreRelativePositionNative\)/);
assert.doesNotMatch(ugv, /horizontalSpeed - deceleration \* Math\.max\(0, deltaSeconds\)/);
assert.match(ugv, /rollingFriction: 0\.12/);
assert.match(ugv, /angularDamping: 0\.6/);
assert.match(ugv, /wheel\.set_m_frictionSlip\(TYRE_FRICTION_SLIP\)/);
assert.doesNotMatch(ugv, /massKg \* 3/);
assert.match(ugv, /pureTurn = !braking && forwardInput === 0 && turnInput !== 0/);
assert.match(ugv, /desiredYawRate = -turnInput \* maxTurnRate/);
@@ -6,7 +6,9 @@ import { createServer } from "vite";
let server;
let fetchVegetationBenchmarkResult;
let fetchCanonicalRecordedLabSpatialFrame;
let fetchVegetationShadowResult;
let fetchVegetationRouteTgsAnchor;
let vegetationFullRouteMaskUrl;
before(async () => {
@@ -18,10 +20,14 @@ before(async () => {
({
fetchVegetationBenchmarkResult,
fetchVegetationShadowResult,
fetchVegetationRouteTgsAnchor,
vegetationFullRouteMaskUrl,
} = await server.ssrLoadModule(
"/src/core/laboratory/vegetationShadow.ts",
));
({ fetchCanonicalRecordedLabSpatialFrame } = await server.ssrLoadModule(
"/src/core/laboratory/canonicalRecordedLabSpatial.ts",
));
});
after(async () => {
@@ -185,6 +191,7 @@ function fullRouteReview() {
return {
source_id: "RAVNOVES004TREE",
session_id: "20260828T130511Z_viewer_live",
linked_route_review_result_id: `lab-v1-vegetation-shadow-${"9".repeat(64)}`,
source_job_id: "recorded-camera-eb2783c5480d56bda07c8af0",
source_job_input_sha256: "eb2783c5480d56bda07c8af008dff5344d19dc550ef70fe2075d6f098f7cc715",
source_stream_sha256: "e5eb017e2cc0f546736eda5235ca157b501913093cb64af5e548e335417e1bac",
@@ -364,8 +371,88 @@ test("vegetation GOOSE benchmark opens through its separate archival endpoint",
assert.equal(result.validationCases.length, 12);
});
test("vegetation realtime LAB and archival benchmark use separate admitted instruments", async () => {
const [resultSource, benchmarkSource] = await Promise.all([
test("vegetation route TGS anchor keeps exact sealed metric shapes", async () => {
let requestedUrl = "";
const anchor = await fetchVegetationRouteTgsAnchor(resultId, 409, {
fetcher: async (url) => {
requestedUrl = String(url);
return new Response(JSON.stringify({
schema_version: "missioncore.lab-v1-route-tgs-anchor/v1",
source_sequence: 409,
slot: 1,
current_points_xyz_m: [[1, 2, 3], [4, 5, 6]],
costmap: {
cell_size_m: 0.45,
centers_xy_m: Array.from({ length: 2244 }, (_, index) => [index, -index]),
state_codes: Array.from({ length: 2244 }, (_, index) => index % 4),
z_bounds_m: Array.from({ length: 2244 }, () => [null, null]),
},
}), { status: 200, headers: { "Content-Type": "application/json" } });
},
});
assert.equal(
requestedUrl,
`/api/v1/laboratory/vegetation-shadow/${resultId}/route-tgs-anchor/409`,
);
assert.equal(anchor.sourceSequence, 409);
assert.equal(anchor.currentPointsXyzM.length, 2);
assert.equal(anchor.costmap.centersXyM.length, 2244);
assert.deepEqual(new Set(anchor.costmap.stateCodes), new Set([0, 1, 2, 3]));
});
test("canonical recorded LAB spatial frame keeps source, SLAM and body identity on one clock", async () => {
const generation = "e".repeat(64);
let requestedUrl = "";
const frame = await fetchCanonicalRecordedLabSpatialFrame("session-004", generation, 82_770_000_000, {
fetcher: async (url) => {
requestedUrl = String(url);
return new Response(JSON.stringify({
schema_version: "missioncore.canonical-recorded-lab-spatial-frame/v3",
target_time_ns: 82_770_000_000,
source_time_ns: 82_769_535_708,
pose_time_ns: 82_769_535_708,
trajectory_time_ns: 82_700_000_000,
coordinate_frame: "body-ground",
sensor_height: {
meters: 0.32,
source: "local-source-cloud-ground-quantile-median",
sample_count: 20,
mad_m: 0.03,
authority: "visual-derived",
},
spatial_profile: {
profile_id: "source-paced-ground-v3",
local_slam_history_seconds: 5,
local_slam_radius_m: 30,
local_slam_voxel_size_m: 0.12,
local_slam_point_limit: 27000,
},
source_point_count: 2,
source_points_body_xyz_m: [[1, 2, 3], [4, 5, 6]],
local_slam_source_frame_count: 2,
local_slam_source_point_count: 4,
local_slam_point_count: 2,
local_slam_body_xyz_m: [[0, 0, 0], [1, 0, 0]],
body_frame: {
origin_map_xyz_m: [33, 4, 1],
sensor_origin_map_xyz_m: [33, 4, 1.32],
basis_map_from_body: [[1, 0, 0], [0, 1, 0], [0, 0, 1]],
},
}), { status: 200, headers: { "Content-Type": "application/json" } });
},
});
assert.equal(
requestedUrl,
`/api/v1/observation-sessions/session-004/canonical-lab/spatial-frame?generation=${generation}&time_ns=82770000000&profile=source-paced-ground-v3`,
);
assert.equal(frame.sourcePointCount, 2);
assert.equal(frame.localSlamBodyXyzM.length, 2);
assert.equal(frame.sensorHeight.meters, 0.32);
assert.deepEqual(frame.bodyFrame.originMapXyzM, [33, 4, 1]);
});
test("vegetation realtime LAB uses one upstream Rerun clock and keeps archival review separate", async () => {
const [resultSource, benchmarkSource, m49Source, canonicalSource, rerunSource] = await Promise.all([
readFile(
new URL("../src/workspaces/laboratory/VegetationShadowResult.tsx", import.meta.url),
"utf8",
@@ -374,17 +461,46 @@ test("vegetation realtime LAB and archival benchmark use separate admitted instr
new URL("../src/workspaces/laboratory/VegetationBenchmarkResult.tsx", import.meta.url),
"utf8",
),
readFile(
new URL("../src/workspaces/laboratory/M49TgsFullShadowEvidence.tsx", import.meta.url),
"utf8",
),
readFile(
new URL("../src/components/laboratory/CanonicalRecordedLabReplay.tsx", import.meta.url),
"utf8",
),
readFile(
new URL("../src/workspaces/laboratory/CanonicalVegetationRerunReplay.tsx", import.meta.url),
"utf8",
),
]);
assert.doesNotMatch(resultSource, /M48MaskComparisonVisual/);
assert.match(resultSource, /M4ReplayThreatVisual/);
assert.match(resultSource, /M49TgsFullShadowEvidence/);
assert.match(resultSource, /semanticOverride/);
assert.match(resultSource, /EoMT CITY \/ DDRNet VEGETATION/);
assert.equal(resultSource.match(/<LaboratoryEvidence\b/g)?.length, 4);
assert.match(resultSource, /RAVNOVES004TREE mixed route review/);
assert.match(resultSource, /RAVNOVES004TREE full recorded review/);
assert.match(resultSource, /LaboratoryRecordedClipPlayer/);
assert.match(resultSource, /className="m48-clip-player__overlay"/);
assert.match(m49Source, /spatialSemantic=\{spatialSemantic\}/);
assert.match(m49Source, /controlLabel: "SEMANTICS"/);
assert.equal(resultSource.match(/<LaboratoryEvidence\b/g)?.length, 2);
assert.doesNotMatch(resultSource, /RAVNOVES004TREE mixed route review/);
assert.match(resultSource, /КАНОНИЧЕСКАЯ ЗАПИСАННАЯ LAB · RAVNOVES004TREE/);
assert.match(resultSource, /<CanonicalVegetationRerunReplay/);
assert.doesNotMatch(resultSource, /RerunViewport/);
assert.doesNotMatch(resultSource, /cacheRef|pumpRef|desiredRef/);
assert.doesNotMatch(resultSource, /LaboratoryRecordedClipPlayer|M48EvidenceModeRail/);
assert.doesNotMatch(resultSource, /assets\.tgs|<img/);
assert.match(rerunSource, /<RerunViewport/);
assert.match(rerunSource, /ИСХ\. ТОЧКИ/);
assert.match(rerunSource, /ЛОК\. SLAM/);
assert.match(rerunSource, /resolveCanonicalLabReplay/);
assert.doesNotMatch(rerunSource, /canonical-overlay\.rrd/);
assert.match(rerunSource, /unifiedPerception: splitView/);
assert.doesNotMatch(rerunSource, /LaboratoryRecordedClipPlayer|LaboratoryMetricEvidenceScene/);
assert.match(resultSource, /point-aligned 3D semantics пока не запечатаны/);
assert.match(rerunSource, /value: "tgs", label: "TGS", disabled: true/);
assert.match(canonicalSource, /primary=\{mediaPane\}/);
assert.match(canonicalSource, /secondary=\{spatialPane/);
assert.match(canonicalSource, /missioncore\.canonical-recorded-lab-replay\/v1/);
assert.match(canonicalSource, /separatorLabel="Изменить размер видео\/камеры и 3D\/плана"/);
assert.match(resultSource, /linked canonical M4\.9 TGS evidence/);
assert.match(resultSource, /linkedTgsResultId/);
assert.match(benchmarkSource, /M48MaskComparisonVisual/);
assert.doesNotMatch(benchmarkSource, /M49TgsFullShadowEvidence/);
@@ -0,0 +1,88 @@
import assert from "node:assert/strict";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE;
let liveAcquisitionRerunProfile;
let recordedSessionRerunProfile;
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
({
LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE,
liveAcquisitionRerunProfile,
recordedSessionRerunProfile,
} = await server.ssrLoadModule("/src/core/observation/viewerProfile.ts"));
});
after(async () => {
await server?.close();
});
test("live acquisition profile cannot acquire recorded playback policy", () => {
assert.deepEqual(liveAcquisitionRerunProfile({
sourceUrl: "rerun+http://127.0.0.1:9877/proxy",
liveActivitySequence: 12,
liveStreamId: "acquisition-1",
liveRecoveryAuthorityIdentity: "authority-1",
}), {
kind: "live-acquisition",
clock: "stream_time",
sourceUrl: "rerun+http://127.0.0.1:9877/proxy",
liveActivitySequence: 12,
liveStreamId: "acquisition-1",
liveRecoveryAuthorityIdentity: "authority-1",
});
});
test("recorded session profile owns progressive admission and on-demand layers", () => {
const artifact = {
sourceUrl: "/api/v1/observation-sessions/session-1/recording.rrd",
viewerSourceUrl: "/api/v1/observation-sessions/session-1/recording.rrd?generation=abc",
byteLength: 42,
sha256: "a".repeat(64),
};
const profile = recordedSessionRerunProfile({
sourceUrl: artifact.sourceUrl,
artifact,
autoplayWhenReady: true,
presentationGate: "loading",
expectedTimelineStartSeconds: 0,
expectedTimelineEndSeconds: 20,
initialPlaybackStartSeconds: 1,
view: "spatial",
viewResetGeneration: 0,
followTrajectory: false,
perceptionLayers: {
enabled: false,
detections2d: false,
segmentation: false,
cuboids3d: false,
},
perceptionRetryGeneration: 0,
lockPerceptionCameraInteraction: false,
});
assert.equal(profile.kind, "recorded-session");
assert.equal(profile.clock, "session_time");
assert.equal(profile.artifact, artifact);
assert.equal(profile.perceptionLayers.enabled, false);
});
test("the old LAB transport is fenced as explicit legacy comparison only", () => {
assert.deepEqual(LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE, {
kind: "lab-recorded-evidence",
clock: "source-sequence",
cameraTransport: "generation-bound-fmp4",
spatialTransport: "bounded-sealed-artifacts",
loadPolicy: "explicit-legacy-comparison-only",
workerRequired: false,
});
assert.equal(Object.isFrozen(LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE), true);
});
+10
View File
@@ -0,0 +1,10 @@
# Archived viewer comparison source
`rerun-web-viewer-0.34.1/` is retained only as rollback-era comparison evidence
for ADR 0045. The active application neither imports it nor runs the historical
patch scripts during install or build. Canonical Control Station routes use the
unmodified `@rerun-io/web-viewer` dependency declared in `package.json`.
Do not update or reactivate this tree. Remove it with the remaining custom
fMP4/Three.js replay implementation after the canonical Rerun migration passes
operator visual acceptance.
+1 -1
View File
@@ -172,7 +172,7 @@ On the first live or adapter file-replay session, `RerunBridge`:
At every session start it resets the trajectory, current point count, metrics,
blueprint and session-local visible time, then feeds the new source through the
same recording. This process-wide lifecycle is intentional: the Rerun 0.34.1
same recording. This process-wide lifecycle is intentional: the upstream Rerun 0.36.3
browser receiver can remain connected after canvas teardown, so restarting the
native listener on the same port is not a reliable session boundary.
+13
View File
@@ -185,6 +185,15 @@ The evidence slot has two admitted renderers:
- `diagnostic-model` — a specialized visual result such as the LAB E28 L2.6
surface/timeline/review viewer.
ADR 0045 amends the full-session `recorded-replay` transport. A canonical
recorded LAB uses one unmodified upstream Rerun viewer, recording identity and
timeline for camera, semantic and spatial evidence. The source RRD owns pose,
point cloud and trajectory; a digest-bound RRD sidecar may add only immutable
derived entities which are absent from the source. Model or layer selection is
a blueprint/profile change, not a new player or renderer. Missing full-route
TGS, semantic 3D, boxes or cuboids remain disabled instead of being inferred
from sparse review artifacts.
Recorded camera clips used for review are a frozen sub-contract of the admitted
viewer, `missioncore.laboratory-recorded-clip-viewer/v1`, implemented by
`LaboratoryRecordedClipPlayer`. It owns the generation-bound fMP4 manifest,
@@ -197,6 +206,10 @@ Forward frame progression may roll an already-buffered segment target; a
backward seek or clip loop must perform an explicit decoder seek and remain
decoder-ready without exposing a per-frame loader.
That clip contract remains available to historical bounded clip instruments.
It is legacy comparison transport for a migrated full-session LAB and must not
be mounted, prefetched or run in parallel with the canonical Rerun profile.
When recorded camera and frame-indexed spatial evidence are both required for
one review question, the shared player presents them simultaneously on the same
media clock. The camera remains the clock owner; a bounded experiment-neutral
@@ -149,7 +149,8 @@ second mounted source.
- [x] Harden the Control Station application boundary before A3: consume the
Design Guideline packages as the only visual platform, isolate the LAB
feature and CSS, add typed workspace contracts and enforce one-way imports
plus composition-size ratchets.
across the application layers. Line-count limits were subsequently removed:
they are not an architectural invariant.
- [x] Validate the real A2 generation:
`e30-review-pack-faec915a771022cceaf4ee62bece698afc8018d09b6b0ac7602157216fbb3686`
@@ -160,6 +160,15 @@ history of rejected approaches belong in Ops.
Primary visual evidence is hosted in one reusable viewer frame.
For canonical full-session recorded replay, ADR 0045 fixes one native upstream
Rerun viewer beneath this frame. The accepted product chrome and switching
logic remain Mission Core UI, while Rerun alone owns playback time, video
decoding, 2D annotations, point-cloud rendering, trajectory and 3D camera.
VIDEO/CAMERA, SOURCE POINTS/LOCAL SLAM/TGS COSTMAP/SEMANTICS and 3D/PLAN select
entities, visible time ranges and blueprints in that mounted viewer. They never
start independent transports or render loops. A LAB may rename model buttons or
add an admitted layer button, but it may not change this switching architecture.
### Frozen Milestone 4 perception instruments
Milestone 4 admits exactly two operator instruments inside the shared LAB page
@@ -234,6 +243,13 @@ The shared player may retain a rolling target only for the same or a later
segment. Rewind and clip-loop transitions must seek backward explicitly while
keeping the admitted generation and decoder owner mounted.
The preceding fMP4 rule applies to historical bounded clip instruments. A
migrated full-session recorded LAB instead uses the native Rerun `AssetVideo`
and `VideoFrameReference` contract from ADR 0045. It must not mount the clip
player or a custom Three.js scene alongside Rerun. The visual controls and LAB
template are identical in both cases; the canonical profile determines the one
active transport.
### 3D and 2D policy
Choose the default representation from the operator question:
+12 -10
View File
@@ -249,8 +249,7 @@ vocabulary and executable contracts. They do not create a second runtime model.
- no upward imports from core/components into workspaces or App;
- no visual adapter imports from core;
- no local vendor icon library or direct Design Guideline source imports;
- LAB code and CSS remain outside the central workspace buckets;
- central composition files cannot silently return to their previous size.
- LAB code and CSS remain outside the central workspace buckets.
`test/laboratoryProductUi.test.mjs` additionally enforces the versioned LAB
report fields and shared result component across bounded LAB modules.
@@ -260,8 +259,9 @@ new unclassified experiment branches while allowing a declared bounded
experimental adapter. The gate protects core composition; it does not forbid a
novel research stack.
The line limits are ratchets, not quality targets. When a file reaches a limit,
split a feature; do not raise the limit to accommodate unrelated behavior.
File length is not an architectural boundary and is not enforced. Refactoring
is justified by ownership, cohesion, dependency direction, lifecycle or test
isolation, not by a line-count threshold.
From `apps/control-station` run:
@@ -273,13 +273,15 @@ npm run build
## Known bounded debt
- `App.tsx` remains a large shell orchestrator. Its current size is frozen by a
ratchet; future shell behavior must extract a controller/hook or panel module.
- `App.tsx` remains a large shell orchestrator. Future shell behavior should
preserve its orchestration ownership and extract modules only where they
acquire an independent responsibility or lifecycle.
- `Workspaces.tsx` still contains several established generic workspaces. New
domains must be separate modules, and existing ones may be extracted when
their behavior changes.
- `LaboratoryArchiveWorkspace.tsx` is now physically isolated but at its
ratchet. A3 receives its own component/module instead of growing that file.
domains should respect the existing dependency direction; extraction is a
design decision rather than a response to file length.
- `LaboratoryArchiveWorkspace.tsx` is physically isolated. Further LAB work
must preserve the feature boundary without imposing a size quota on the
implementation.
- Design Guideline dependencies are mutable local `file:` links until a
portable package/distribution decision is implemented.
+20
View File
@@ -122,3 +122,23 @@ Primary implementation references:
- a ready project mounts direct PlayCanvas Engine and loads Streamed SOG with preview fallback;
- visual, collision and combined remain distinct runtime modes, and collision is loaded lazily;
- viewer quality, layer-axis correction and camera inversion survive navigation and reload.
## AI-assisted visual quality extension
AI-assisted visual repair is a candidate lifecycle of one ready Simulation World project. It does
not replace or modify the accepted ingest/optimization path. The original LCC/LCC2 bundle remains
the source of record, while an explicitly promoted enhanced generation may replace only the active
visual SOG references.
The product entry point is `Улучшить качество` in the existing project edit window. It opens a
canonical bounded Window for XGRIDS Creator Data admission, region-of-interest selection, actual
provider state and baseline/candidate review. It is not placed in the live scene controls because
source admission and a long-running candidate build are project operations. It does not receive a
separate workspace because the candidate has no identity outside its parent project.
The control is not shipped as a placeholder. It becomes available only with a ready project and an
enhancement provider publishing the accepted capability. The provider consumes full-quality PLY
derived from the immutable LCC/LCC2 source plus aligned Creator Data images/COLMAP cameras; SOG is
delivery output only. Generated visual regions remain forbidden as collision, navigation or
qualification evidence. The detailed boundary and first experiment are defined by
[ADR 0044](adr/0044-ai-assisted-gaussian-visual-repair.md).
@@ -0,0 +1,136 @@
# ADR 0044: AI-assisted Gaussian visual repair remains a candidate pipeline
## Status
Proposed for a Worker 006 ROI experiment on 2026-08-29. The existing Gaussian optimization and
collision paths remain accepted and unchanged. No repair model is admitted for production use by
this decision.
## Context
The current Simulation Worlds vertical accepts one XGRIDS LCC/LCC2 source, builds preview and
streamed SOG artifacts through DC Gaussian Pipeline, and publishes source-mesh collision
independently. The two current ready projects prove that path. AutoCap repairs only conservative
holes in the source collision mesh; it does not repair the visual Gaussian representation.
The retained MAROSEYKA archive contains an LCC Quality scene with 86,471,152 splats and a
44,787-sample 10 Hz device trajectory. It does not contain the captured camera images, camera
intrinsics or camera extrinsics. The pose records do not identify RGB frames and cannot be treated
as calibrated camera poses.
AI repair methods that permanently improve a 3DGS model need rendered novel views, real reference
images and known cameras. Plausible generation alone cannot recover the factual appearance or
geometry of a surface that was never observed.
XGRIDS LCC Studio Creator Data provides the missing supported interchange boundary:
- `perspective/images/` contains undistorted perspective images;
- `perspective/masks/` contains invalid-region masks;
- `perspective/sparse/` contains COLMAP camera intrinsics and extrinsics aligned with the optimized
LiDAR point cloud;
- `poses.csv` and `high_frequency_poses.csv` retain device trajectories when they are useful for
selecting a repair corridor.
## Decision
1. The immutable original LCC/LCC2 bundle is the visual source of record. SOG is a compressed web
delivery artifact and is never the input to AI repair.
2. A repair candidate starts from full-quality `LCC/LCC2 -> standard 3DGS PLY` conversion. It may
operate only on bounded spatial tiles or route-derived regions of interest; loading or training
the complete 86M-splat scene as one model is not an accepted Worker 006 profile.
3. A repair request additionally admits XGRIDS Creator Data. The minimum input is undistorted
images plus a complete COLMAP sparse model. Masks are strongly preferred. Device poses alone do
not satisfy camera admission.
4. The first experiment uses FreeFix at an exact source revision with the SDXL refinement path. It
was selected because it is fine-tuning-free at the diffusion-model level, uses per-pixel
confidence to preserve reliable regions, reports outdoor/Waymo evaluation, and publishes MIT
code. An adapter must import standard XGRIDS/PlayCanvas PLY attributes into the gsplat checkpoint
layout and export a standard PLY after refinement.
5. Difix3D+ is the mandatory comparison baseline for the same ROI. It directly targets artifacts in
under-constrained views and supports progressive distillation into gsplat, but its combined
NVIDIA/Stability licensing needs a separate commercial-use review.
6. The experiment runs in an adjacent `ndc-` prefixed Docker composition on Worker 006. It shares
neither Python/CUDA environments nor writable model directories with DC Gaussian Pipeline. The
existing pipeline is called only after a candidate PLY is complete and immutable.
7. Every result is a candidate generation. Mission Core keeps the active visual generation until
an operator compares the baseline and candidate and explicitly promotes the candidate. Failure
or cancellation cannot modify the active scene.
8. Generated visual content never becomes collision, navigation, traversability or ground-truth
evidence. Existing source-mesh collision and AutoCap remain independent. Each candidate retains
an uncertainty/hallucination mask and exact model/config provenance.
## Candidate contract
A quality-enhancement request must bind all inputs by digest:
- project ID and original source-bundle SHA-256;
- LCC/LCC2 entrypoint and full-quality PLY conversion revision;
- Creator Data bundle SHA-256;
- COLMAP cameras, images and points model plus their coordinate-alignment report;
- selected route interval and/or world-space ROI;
- algorithm, source revision, model IDs/digests, prompt policy and numerical parameters.
The adjacent provider returns:
- a standard enhanced PLY for the selected tile or composed candidate;
- preview and streamed SOG artifacts created by the unchanged optimization pipeline;
- baseline/candidate camera-path renders and objective image metrics where held-out real views
exist;
- changed-region, confidence and generated-content masks;
- wall time, peak VRAM, source revision, image digest and terminal job state.
The provider states are transport-neutral and bounded: `queued`, `validating_source`,
`aligning_cameras`, `extracting_roi`, `importing_splats`, `refining`, `building_delivery`,
`evaluating`, `ready`, `failed`, and `cancelled`.
## Product placement
The action belongs in the existing project edit window as `Улучшить качество`, because it acts on
one durable world and does not create a new workspace. The action opens a canonical bounded Window
that owns Creator Data admission, ROI choice, actual provider state and baseline/candidate review.
It is shown only for a ready outdoor/interior project and becomes an executable action only when
the enhancement provider publishes the exact accepted capability.
Alternatives considered:
1. Add controls to the live PlayCanvas scene. Rejected because source admission and a long-running
candidate lifecycle are project operations, not per-frame runtime controls.
2. Add a new top-level workspace. Rejected because repair has no independent identity outside one
Simulation World project.
3. Add an always-enabled button before provider/source admission exists. Rejected because it would
be placeholder product UI and would misrepresent the current system state.
## Experiment acceptance
The first ROI experiment is accepted only when:
- the adjacent composition starts and stops without changing the current Gaussian Pipeline
containers or native SplatTransform spool;
- one Creator Data bundle passes COLMAP/image/alignment validation;
- one bounded road-and-facade ROI fits the RTX 4090 24 GB profile without host OOM or impact on the
active optimization queue;
- FreeFix and Difix3D+ run on identical admitted cameras and ROI;
- candidate output round-trips through standard PLY and the existing SOG build;
- held-out views and an operator review show improvement without unacceptable changes in reliable
regions;
- active SOG and collision artifacts remain byte-identical until explicit promotion.
## Consequences
- The first useful input request is XGRIDS Creator Data, not a raw device track and not SOG.
- Existing ready RAR archives cannot start factual image-conditioned repair by themselves.
- Large scenes require ROI/tile scheduling, overlap blending and candidate composition.
- Visually plausible fill may be valuable for rendering while remaining inadmissible as physical
truth.
- Worker deployment is intentionally blocked until a Creator Data sample and restored Worker 006
provider connectivity are available.
## Primary references
- [XGRIDS Creator Data](https://docs.xgrids.com/en-us/06-lixel-cybercolor/01-lcc-studio/v2.3.0/06-model-reconstruction.html#creator-data-and-nvidia-ncore-data)
- [PlayCanvas SplatTransform](https://github.com/playcanvas/splat-transform)
- [FreeFix](https://github.com/hyzhou404/FreeFix)
- [Difix3D+](https://github.com/nv-tlabs/Difix3D)
- [GSFix3D](https://github.com/GSFix3D/GSFix3D)
- [ArtifactWorld](https://github.com/fyting/ArtifactWorld)
@@ -0,0 +1,112 @@
# ADR 0045: Upstream Rerun as the canonical recorded-LAB pipeline
Date: 2026-08-30
Status: accepted; RAVNOVES004TREE is the first migrated full-route LAB
## Context
The accepted LAB product composition was repeatedly rebuilt over independent
camera, semantic, point-cloud and TGS transports. The resulting implementation
had several clocks, LAB-specific caches, a browser MediaSource decoder and a
separate Three.js spatial renderer. A camera could continue while segmentation
or the cloud stopped; rewind could expose evidence from different source
sequences; switching TGS could block both panes. Low host utilization did not
make that architecture correct: the bottleneck was duplicated admission,
decoding, scheduling and state ownership.
The product owner requires the existing LAB UI and interaction grammar to stay
unchanged. VIDEO/CAMERA, SOURCE POINTS/LOCAL SLAM/TGS COSTMAP/SEMANTICS and
3D/PLAN remain the canonical controls. Models and evidence providers may
change, but a LAB may not create another player, clock, splitter, spatial
renderer, window or status grammar.
The repository state before this migration is retained by the annotated Git
tag `baseline/custom-legacy-before-canonical-rerun-2026-08-30`. Its Russian
stage name is **«Этап перехода от самописного legacy-контура к каноническому
шаблонному Rerun-пайплайну»**.
## Decision
Recorded LAB replay uses the unmodified upstream Rerun SDK and web viewer. The
first accepted dependency is exactly `rerun-sdk==0.36.3` and
`@rerun-io/web-viewer@0.36.3`. Mission Core does not patch the package, vendor a
viewer fork or depend on private viewer source. Product controls are an outer
adapter which requests an ordinary Rerun blueprint.
One native Rerun viewer owns:
- one `session_time` playback clock;
- the recorded camera and semantic image-space evidence;
- `/world/points`, `/world/sensor_pose` and `/world/trajectory`;
- native 3D orbit and top-down plan presentation;
- seek, play/pause and frame synchronization.
The canonical K1 RRD remains the source of pose, source points, bounded Local
SLAM accumulation and trajectory. A LAB may publish one immutable normalized
RRD sidecar containing only derived evidence absent from that recording, such
as camera video, semantic masks and diagnostic 2D boxes. The base recording and
sidecar must have the same application id, recording id and timeline. A sidecar
does not copy, rotate or re-own world geometry.
RAVNOVES004TREE uses a digest-bound sidecar cache. Its sealed fMP4 fragments are
verified, concatenated and transcoded once to an upstream-compatible H.264
`AssetVideo`. Source PTS are preserved. A fragment without a decodable sample
holds the latest preceding frame; decoded samples are never renumbered to a
synthetic fixed-rate clock. Each semantic mask and `VideoFrameReference` is
logged at the exact immutable LAB `session_time`.
Profiles control loading rather than creating different viewers:
- source points use zero accumulation;
- Local SLAM uses a native five-second visible time range;
- TGS COSTMAP and 3D SEMANTICS are enabled only when full-route immutable
artifacts exist and share the recording clock;
- semantic model buttons select an entity path in the same sidecar;
- 3D/PLAN changes native eye controls, never point coordinates;
- layers missing from an immutable result stay visibly disabled and fail
closed; they are not reconstructed from sparse review anchors.
The previous fMP4/Three.js LAB transport remains source-retained only for
explicit legacy comparison. No canonical route selects it, preloads it or lets
it start background work. Removal is allowed after migrated results pass the
same acceptance checks and the rollback tag is no longer operationally needed.
## Acceptance
A migrated recorded LAB is accepted only when:
1. the base RRD identity and sidecar identity match exactly;
2. camera, semantics, point cloud, pose and trajectory follow one Rerun clock;
3. play, pause, forward seek and backward seek do not remount the viewer;
4. SOURCE POINTS and Local SLAM are native views of the same sealed geometry;
5. unavailable TGS or semantic 3D evidence is disabled rather than simulated;
6. first materialization is cached by source/result/renderer digests and a
cache hit performs no decode or inference;
7. the existing LAB page, selectors, report mode, controls and expand behavior
remain unchanged;
8. Data replay and live Rerun profiles continue to use their own load policies;
9. the integrated application remains on `127.0.0.1:8000` and no second Mission
Core service is introduced.
The isolated renderer materialized the first real RAVNOVES004TREE sidecar in
79.5 seconds. Under the live operator service, cold materialization completed
in approximately seven minutes and produced a 393,203,594-byte RRD; this is too
slow to treat as an interactive open and should be moved to publication-time
preparation. With a full SHA-256 recheck on every cache hit, the warm product
endpoint returned headers in 0.89 seconds and streamed the complete local
artifact in 2.48 seconds. These measurements establish the local cache behavior,
not a realtime inference or navigation claim.
## Consequences
- Mission Core keeps its product UI without owning media or spatial playback.
- Rerun can be upgraded through ordinary dependency updates and regression
tests instead of reapplying a local patch.
- New models publish entities and annotations into the same recording contract;
they do not add LAB-specific viewers.
- SLAM clouds and trajectories stay visible through standard Rerun components.
- Useful native boxes/cuboids may be added as ordinary entity layers when their
immutable full-route evidence exists.
- The migration does not improve DDRNet quality, prove terrain traversability
or grant navigation/actuation authority. Those remain separate model and
safety acceptance questions.
Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

@@ -0,0 +1,251 @@
# RAVNOVES004TREE canonical LAB replay audit
Date: 2026-08-30
Scope: Mission Core recorded LAB replay, RAVNOVES004TREE, OPS perception state
Excluded: Gaussian/simulation workers and their artifacts
Status: the diagnostic findings and perception conclusions remain evidence, but
the custom fMP4/Three.js implementation described below is superseded by
ADR 0045. It is retained here as the failure audit, not as the current replay
contract.
## Current outcome after ADR 0045
RAVNOVES004TREE now uses one unmodified upstream Rerun 0.36.3 viewer for camera,
semantic masks, diagnostic boxes, source points, bounded Local SLAM, trajectory,
3D/PLAN and playback. The canonical source RRD owns world geometry; a verified
immutable RRD sidecar adds only LAB image-space evidence with the same recording
id and `session_time`. Source PTS are preserved, so missing decodable video
samples hold the previous frame instead of shortening the route or drifting
from masks.
The accepted LAB controls and layout remain unchanged. Full-route TGS and
point-aligned 3D semantics are still absent and therefore remain visible but
disabled. The former fMP4/Three.js route is comparison-only legacy and does not
load on the canonical RAV004 route.
The isolated renderer materialized the sidecar in 79.5 seconds. The canonical
live service cold-path took approximately seven minutes and produced
393,203,594 bytes, so publication-time preparation remains required before this
profile is called immediately openable. With a complete SHA-256 check on every
cache hit, the warm endpoint returned headers in 0.89 seconds and streamed the
local artifact in 2.48 seconds. This is a replay/cache measurement, not a
realtime inference claim.
## Current validation after ADR 0045
- frontend unit suite: 661 passed;
- TypeScript typecheck and production Vite build: passed;
- migration-scoped backend/API suite: 60 passed;
- live canonical blueprint: HTTP 200, 86,343 bytes, 0.185 seconds;
- warm integrity-checked sidecar: HTTP 200, byte-range `RRF2` confirmed;
- canonical service restarted and healthy on `127.0.0.1:8000`; no Mission Core
listener exists on `8765`.
The repository-wide Python suite completed with ten failures in pre-existing K1
camera-recovery/scenario-reset tests. No K1 runtime or test file differs from
the rollback tag in this migration. Nine failures are in the existing active
acquisition camera-restart contract; one is an existing expected-document
mismatch after the runtime added reset timing. These do not invalidate the
Rerun-specific checks, but the repository-wide suite is not represented as
green.
Automated in-app visual QA could not attach to the local address because the
browser surface rejected the localhost URL under its URL policy. No alternate
browser-control bypass was used. The live HTTP/data plane, build and contracts
were accepted; an operator visual pass remains required for the exact layout,
seek and toggle experience.
## Superseded implementation outcome
RAVNOVES004TREE no longer owns a custom LAB viewer. It supplies recording and
model configuration to the same `M4ReplayThreatVisual` and
`CanonicalRecordedLabReplay` implementation used by the accepted recorded LAB.
No new window or status type was added. The stable interaction contract remains:
- media: `SEMANTICS`, model selector, `VIDEO` / `CAMERA`;
- spatial: `SOURCE POINTS`, `LOCAL SLAM`, `TGS COSTMAP`, `SEMANTICS`, `3D` / `PLAN`;
- one timeline, one resizable split and one media-owned playback clock.
Models, result IDs, endpoints, labels and replay transport are configuration.
Window structure, switching, seek, buffering and spatial scene code are shared.
## Why the previous LAB failed
### RAV004 did not use the accepted replay data plane
The working M4/Hologravity LAB uses sealed binary numeric tracks, retained scene
state and bounded JSON metadata. RAV004 reused the visual component but silently
left its custom timeline endpoint on the JSON fallback. Each eight-frame spatial
window therefore transferred approximately 1.2-2.9 MB and took 0.87-1.37 s to
produce while representing only about 0.84 s of playback. The next request
aborted and replaced the previous one before spatial state could catch up.
The user screenshot captured the failure exactly: camera frame 366 was active
while the last delivered spatial evidence was frame 230, a 136-frame gap. The
backend also reopened and indexed the 6830-entry semantic ZIP for every requested
mask and proposal frame. This explains why the same canonical viewer was smooth
for Hologravity but stalled for RAV004: the window and interaction code were
shared, but the data-plane contract was not.
RAV004 now publishes the same `missioncore.recorded-spatial-playback/v1`
contract as the accepted LAB: an immutable Float32 map-point track, camera-frame
offsets and 24-frame binary chunks. JSON chunks contain bounded frame metadata
only; retained Local SLAM is reconstructed from exact source increments in the
shared client. The semantic ZIP handle and member index are cached per immutable
artifact instead of being reparsed per frame.
### Video and spatial state had different clocks
The removed RAV004 viewer advanced an animation/host clock even when the browser
decoder stopped. The point cloud therefore continued while the camera frame and
timeline could remain frozen. The shared viewer now uses the decoded media time
as the external clock, and image masks/boxes are rendered only when their time is
within 250 ms of the actually presented video time.
The RAV004 MP4 itself is not clean. An independent `ffmpeg` decode around the
reproducible stop at 11.422 s reported non-monotonic DTS values and corrupt H.264
macroblocks. The RAV004 profile therefore uses the shared segmented MSE transport
and an opt-in timestamp recovery rule. Recovery is allowed only when all of these
conditions are true:
- playback is requested and the media element is not paused, ended or seeking;
- decoded media time has not advanced by 20 ms for at least 1.25 s;
- the browser reports decoded media buffered ahead of the frozen timestamp.
Only then is the broken timestamp interval skipped by 180 ms. The media clock
immediately remains authoritative; the host does not free-run. A stale callback
from the old MSE window is also prevented from undoing an operator seek.
### LiDAR orientation inherited the wrong axes
The RRD declares `/world` as RFU (`Right`, `Forward`, `Up`) and logs
`/world/points` in map space. The earlier adapter treated raw LiDAR quaternion
columns as rover forward/left/up and inherited sensor roll/pitch. That is why the
grid, rover and facade could visibly disagree.
The v3 adapter now uses:
- map `+Z` as gravity/up;
- the smoothed pose-trajectory tangent projected onto the ground as forward;
- `left = up × forward`;
- projected sensor `+Y` only as a fallback when the tangent is unavailable.
This is a deterministic coordinate contract, not a visual angle correction.
### Sensor height was treated as a constant
RAV004 does not have a stable 0.4 m mounting height throughout the recording.
The adapter now estimates the local ground plane from a causal one-second
near-field point window and uses the sealed session estimate only as fallback.
Observed local heights include approximately 0.17 m, 1.24 m, 1.05 m and 0.22 m
at different route positions; a single hand-entered value is therefore invalid.
### Sparse LiDAR frames were held incorrectly
Camera is approximately 9.51 Hz while source points arrive at approximately
2 Hz. A camera frame without a new LiDAR increment used to retain whichever
spatial frame happened to finish loading last; under fast playback this could be
dozens of seconds old. The buffer now loads the active chunk first, the preceding
chunk second and the next chunk as prefetch. The scene selects the latest proven
source increment whose sequence is not later than the active camera frame.
At the final UI check, playback restarted at frame 1, then ran continuously past
frame 462. At frame 188 the DDRNet mask was frame 188, proposals were frame 187
and spatial state was delivered without buffering; the one-frame proposal delay
is the recorded causal overlay, not stale UI state. Before the transport fix the
user's run had already fallen 136 frames behind by camera frame 366.
## Capability ledger
| Layer | RAV004 full route | UI behavior | Authority |
|---|---:|---|---|
| Recorded RIGHT camera | 6830/6830 | `VIDEO` / `CAMERA`, segmented playback | recorded evidence |
| DDRNet semantic mask | 6830/6830 | selectable, opaque enough for review | diagnostic prediction |
| EoMT semantic mask | 6830/6830 | selectable | diagnostic prediction |
| Diagnostic object boxes | derived from connected EoMT mask components | media-time gated | not an independent detector |
| Source points | 1444 increments | `SOURCE POINTS` | recorded geometry |
| Bounded Local SLAM | causal 5 s / 27k-point limit | `LOCAL SLAM` | visual-derived |
| Full-route TGS | **absent** | canonical `TGS COSTMAP` control is visible but disabled | unavailable, fail closed |
| Point-aligned 3D semantics | **absent** | canonical `SEMANTICS` control is visible but disabled | unavailable |
| Independent person/vehicle detector | **absent** | no STOP claim | unavailable |
Ten old TGS review anchors exist, but they are not a continuous route artifact.
They are not repeated or held as if they were full TGS. The accepted RAVNOVES00
full-TGS result is also not reused because it has a different source identity and
4489-frame timeline.
## Performance evidence
Measured on the canonical local service and current immutable artifacts:
- replay launch POST: 3.55 s on first opening;
- first binary playback-manifest build after a service restart: 32.33 s while
the process-local RRD point track is materialized; warm manifest: 0.18-0.20 s;
- full retained point track: 3,893,445 Float32 map points, 46,721,340 bytes,
divided into 285 immutable 24-frame chunks;
- representative active binary chunks: 155-224 KB at 9-10 ms;
- representative 24-frame metadata chunks: 24-32 KB at 0.78-1.0 s, covering
about 2.4 s of playback;
- cached semantic-mask reads: 5.6-7.8 ms instead of approximately 80 ms;
- UI replay: reset to frame 1 and ran continuously beyond frame 462 with camera,
semantic overlay and retained spatial state advancing together;
- operator reset seek: successful, one mounted media worker;
- browser console after the acceptance run: no warnings or errors.
The first RRD index is still process-local rather than a persistent disk cache.
That is an explicit remaining performance gap; warm playback is the admitted
profile, cold restart latency is not yet accepted.
## Nature perception: current OPS stopping point
OPS card `MISSIONCOR-65` defines the intended independent layers as EoMT,
DDRNet, frozen YOLOX and TGS. The current immutable RAV004 artifact proves full
EoMT and DDRNet inference only. It does not prove full TGS, negative-obstacle
handling, an independent person/vehicle STOP layer or combined real-time load.
Isolated full-route measurements:
- DDRNet-39: p95 27.44 ms, 52.67 inference FPS, validation mean IoU 29.715%,
vegetation mean IoU 0.3701;
- EoMT: p95 361.62 ms, approximately 3.01 inference FPS;
- prior accepted RAVNOVES00 TGS: p95 1.694 ms CPU-only, but this is algorithm
performance on another source, not RAV004 proof.
The DDRNet isolated throughput is sufficient for a 10 FPS budget. DDRNet is not
accepted for driving policy because temporal stability and nature quality are
not sufficient: the OPS temporal sample recorded adjacent-frame IoU near 0.195
for high grass and 0.400 for woody vegetation. EoMT does not meet 10 FPS in its
current form. The next evidentiary milestone is therefore not another UI model
toggle; it is synchronized truth for grass/tree/ditch/drop-off, full TGS and
negative-obstacle evidence, frozen independent detector output and a combined
load test at at least 10 FPS.
Worker 006 was audited read-only. Triton and the Gaussian containers were left
untouched. The separate Mission Core perception worker is currently in a restart
loop (404 during model inference startup); this audit did not stop, recreate or
deploy it.
## Acceptance performed
- 12 focused backend spatial/API tests passed;
- 16 focused frontend replay transport/manifest tests passed;
- TypeScript project typecheck passed;
- production Vite build passed (only existing large-chunk warnings);
- `git diff --check` passed;
- live browser run verified reset seek, the shared controls, disabled unsealed
TGS/3D semantics, continuous playback through the former failing interval,
causal spatial hold and a clean console.
Visual QA: `docs/handoff/2026-08-30_RAV004_CANONICAL_LAB_QA.jpg`.
## External coordinate and media references
- Rerun ViewCoordinates: <https://rerun.io/docs/reference/types/datatypes/view_coordinates>
- Rerun transform relation: <https://rerun.io/docs/reference/types/components/transform_relation>
- Rerun transforms: <https://rerun.io/docs/concepts/logging-and-ingestion/transforms>
- Rerun Transform3D: <https://rerun.io/docs/reference/types/archetypes/transform3d>
- WHATWG media element model: <https://html.spec.whatwg.org/multipage/media.html>
- W3C Media Source Extensions: <https://www.w3.org/TR/media-source-2/>
+1 -1
View File
@@ -20,7 +20,7 @@ dependencies = [
"paho-mqtt>=2.1,<3",
"pillow>=12,<13",
"pyyaml>=6.0,<7",
"rerun-sdk==0.34.1",
"rerun-sdk==0.36.3",
"rich>=13.9,<15",
"typer>=0.15,<1",
"uvicorn[standard]>=0.35,<1",
File diff suppressed because it is too large Load Diff
@@ -742,6 +742,7 @@ def seal_mixed_route_full_video_review(
full_route = {
"source_id": FULL_ROUTE_SOURCE_ID,
"session_id": job.session_id,
"linked_route_review_result_id": base["result_id"],
"source_job_id": job.job_id,
"source_job_input_sha256": job.input_sha256,
"source_stream_sha256": FULL_ROUTE_STREAM_SHA256,
@@ -0,0 +1,658 @@
"""Canonical recorded-LAB spatial adapter for sealed Rerun recordings.
The LAB viewer must not run an independent Rerun transport beside the camera
transport. This adapter reads the immutable recording once, indexes the
recorded source cloud and sensor pose, estimates the session sensor height from
the initial stationary cloud, and returns both the current increment and a
bounded accumulated local-SLAM cloud in a ground-rebased body frame. Camera,
spatial layers and the common timeline can therefore be driven by one media
clock without a per-LAB coordinate adapter.
"""
from __future__ import annotations
from bisect import bisect_right
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
from threading import Lock
from typing import Any, Final
import numpy as np
import rerun_bindings as rr_bindings
CANONICAL_LAB_SPATIAL_PROFILE: Final = "source-paced-ground-v3"
_POINT_ENTITY: Final = "/world/points"
_POSE_ENTITY: Final = "/world/sensor_pose"
_TRAJECTORY_ENTITY: Final = "/world/trajectory"
_POINT_COMPONENT: Final = "Points3D:positions"
_POSE_TRANSLATION_COMPONENT: Final = "Transform3D:translation"
_POSE_QUATERNION_COMPONENT: Final = "Transform3D:quaternion"
_TRAJECTORY_COMPONENT: Final = "LineStrips3D:strips"
_INDEX_LOCK: Final = Lock()
_HEIGHT_CALIBRATION_SECONDS: Final = 60.0
_HEIGHT_CALIBRATION_MAX_FRAMES: Final = 120
_HEIGHT_NEAR_MIN_RADIUS_M: Final = 1.0
_HEIGHT_NEAR_MAX_RADIUS_M: Final = 6.0
_HEIGHT_LOWER_QUANTILE: Final = 0.025
_LOCAL_HEIGHT_QUANTILE: Final = 0.10
_LOCAL_HEIGHT_HALF_WINDOW_SECONDS: Final = 1.0
_LOCAL_SLAM_HISTORY_SECONDS: Final = 5.0
_LOCAL_SLAM_RADIUS_M: Final = 30.0
_LOCAL_SLAM_VERTICAL_LIMIT_M: Final = 6.0
_LOCAL_SLAM_VOXEL_SIZE_M: Final = 0.12
_LOCAL_SLAM_POINT_LIMIT: Final = 27_000
_FORWARD_HALF_WINDOW_SECONDS: Final = 1.0
_FORWARD_MINIMUM_DISPLACEMENT_M: Final = 0.15
@dataclass(frozen=True)
class _TimedPoints:
times_ns: tuple[int, ...]
values: tuple[np.ndarray, ...]
@dataclass(frozen=True)
class _TimedPoses:
times_ns: tuple[int, ...]
translations: tuple[np.ndarray, ...]
quaternions_xyzw: tuple[np.ndarray, ...]
@dataclass(frozen=True)
class _CanonicalSpatialIndex:
points: _TimedPoints
poses: _TimedPoses
trajectories: _TimedPoints
sensor_height_m: float
sensor_height_sample_count: int
sensor_height_mad_m: float
def _session_times(batch: Any) -> Any | None:
if "session_time" not in batch.schema.names:
return None
return batch.column("session_time")
def _point_rows(
chunks: list[Any],
entity: str,
component: str,
*,
nested: bool = False,
) -> _TimedPoints:
rows: list[tuple[int, np.ndarray]] = []
for chunk in chunks:
if chunk.entity_path != entity:
continue
batch = chunk.to_record_batch()
times = _session_times(batch)
if times is None or component not in batch.schema.names:
continue
column = batch.column(component)
for row_index in range(batch.num_rows):
timestamp = int(times[row_index].value)
payload = column[row_index].as_py()
if nested:
payload = payload[0] if payload else []
values = np.asarray(payload, dtype=np.float32)
if values.ndim != 2 or values.shape[1] != 3 or not np.isfinite(values).all():
continue
values.setflags(write=False)
rows.append((timestamp, values))
rows.sort(key=lambda item: item[0])
return _TimedPoints(
times_ns=tuple(timestamp for timestamp, _ in rows),
values=tuple(values for _, values in rows),
)
def _pose_rows(chunks: list[Any]) -> _TimedPoses:
rows: list[tuple[int, np.ndarray, np.ndarray]] = []
for chunk in chunks:
if chunk.entity_path != _POSE_ENTITY:
continue
batch = chunk.to_record_batch()
times = _session_times(batch)
if (
times is None
or _POSE_TRANSLATION_COMPONENT not in batch.schema.names
or _POSE_QUATERNION_COMPONENT not in batch.schema.names
):
continue
translations = batch.column(_POSE_TRANSLATION_COMPONENT)
quaternions = batch.column(_POSE_QUATERNION_COMPONENT)
for row_index in range(batch.num_rows):
translation_values = translations[row_index].as_py()
quaternion_values = quaternions[row_index].as_py()
if len(translation_values) != 1 or len(quaternion_values) != 1:
continue
translation = np.asarray(translation_values[0], dtype=np.float64)
quaternion = np.asarray(quaternion_values[0], dtype=np.float64)
if (
translation.shape != (3,)
or quaternion.shape != (4,)
or not np.isfinite(translation).all()
or not np.isfinite(quaternion).all()
):
continue
norm = float(np.linalg.norm(quaternion))
if norm <= 1e-9:
continue
translation.setflags(write=False)
normalized = quaternion / norm
normalized.setflags(write=False)
rows.append((int(times[row_index].value), translation, normalized))
rows.sort(key=lambda item: item[0])
return _TimedPoses(
times_ns=tuple(timestamp for timestamp, _, _ in rows),
translations=tuple(translation for _, translation, _ in rows),
quaternions_xyzw=tuple(quaternion for _, _, quaternion in rows),
)
@lru_cache(maxsize=4)
def _load_index_cached(
path_text: str,
byte_length: int,
modified_ns: int,
generation_sha256: str,
) -> _CanonicalSpatialIndex:
path = Path(path_text)
stat = path.stat()
if stat.st_size != byte_length or stat.st_mtime_ns != modified_ns:
raise ValueError("Recorded LAB source changed during spatial indexing")
if len(generation_sha256) != 64:
raise ValueError("Recorded LAB generation is invalid")
# Decode only the three canonical entities in one pass. Building a lazy
# store first decodes the complete RRD (including unrelated payloads), and
# then scanning that store once per layer made first-open take more than a
# minute on RAVNOVES004TREE.
chunks = (
rr_bindings.RrdReaderInternal(str(path))
.stream()
.filter(content=[_POINT_ENTITY, _POSE_ENTITY, _TRAJECTORY_ENTITY])
.to_chunks()
)
points = _point_rows(chunks, _POINT_ENTITY, _POINT_COMPONENT)
poses = _pose_rows(chunks)
trajectories = _point_rows(
chunks,
_TRAJECTORY_ENTITY,
_TRAJECTORY_COMPONENT,
nested=True,
)
if not points.times_ns or not poses.times_ns or not trajectories.times_ns:
raise ValueError("Recorded LAB source has no canonical spatial layers")
sensor_height_m, sensor_height_sample_count, sensor_height_mad_m = (
_estimate_sensor_height(points, poses)
)
return _CanonicalSpatialIndex(
points=points,
poses=poses,
trajectories=trajectories,
sensor_height_m=sensor_height_m,
sensor_height_sample_count=sensor_height_sample_count,
sensor_height_mad_m=sensor_height_mad_m,
)
def _load_index(
path_text: str,
byte_length: int,
modified_ns: int,
generation_sha256: str,
) -> _CanonicalSpatialIndex:
# functools.lru_cache is coherent but intentionally releases its lock
# during a miss. Serialize cold RRD indexing so simultaneous camera/TGS
# admission cannot parse the same 80 MiB recording twice.
with _INDEX_LOCK:
return _load_index_cached(
path_text,
byte_length,
modified_ns,
generation_sha256,
)
def _latest_index(times_ns: tuple[int, ...], target_ns: int) -> int:
return max(0, min(len(times_ns) - 1, bisect_right(times_ns, target_ns) - 1))
def _rotation_map_from_body(quaternion_xyzw: np.ndarray) -> np.ndarray:
x, y, z, w = (float(value) for value in quaternion_xyzw)
return np.asarray(
[
[1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w)],
[2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w)],
[2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y)],
],
dtype=np.float64,
)
def _map_points_to_body(
points_map: np.ndarray,
translation_map: np.ndarray,
quaternion_xyzw: np.ndarray,
) -> np.ndarray:
rotation = _rotation_map_from_body(quaternion_xyzw)
# Row vectors: inverse(map_from_body) == right-multiply by map_from_body.
body = (points_map.astype(np.float64) - translation_map) @ rotation
return body.astype(np.float32)
def _gravity_stable_basis_map_from_body(
poses: _TimedPoses,
target_time_ns: int,
) -> tuple[np.ndarray, str]:
"""Return a right-handed forward/left/up base frame in the RFU map.
Rerun declares this recording map as RFU, while the metric LAB scene
consumes points as forward/left/up. The LiDAR quaternion columns are sensor
right/forward/up and also contain rover or handheld roll/pitch, so they are
not a body basis. Route displacement owns yaw when available; the sensor's
local +Y (Rerun Forward) projected onto map gravity is the stationary
fallback. Map +Z always owns up.
"""
center = _latest_index(poses.times_ns, target_time_ns)
half_window_ns = round(_FORWARD_HALF_WINDOW_SECONDS * 1_000_000_000)
first = _latest_index(poses.times_ns, max(0, target_time_ns - half_window_ns))
last = min(
len(poses.times_ns) - 1,
max(0, bisect_right(poses.times_ns, target_time_ns + half_window_ns) - 1),
)
route = poses.translations[last] - poses.translations[first]
route_xy = np.asarray([route[0], route[1], 0.0], dtype=np.float64)
route_norm = float(np.linalg.norm(route_xy))
sensor_rotation = _rotation_map_from_body(poses.quaternions_xyzw[center])
sensor_forward = np.asarray(
[sensor_rotation[0, 1], sensor_rotation[1, 1], 0.0],
dtype=np.float64,
)
sensor_forward_norm = float(np.linalg.norm(sensor_forward))
if sensor_forward_norm <= 1e-9:
raise ValueError("Recorded LAB sensor forward axis is invalid")
sensor_forward /= sensor_forward_norm
if route_norm >= _FORWARD_MINIMUM_DISPLACEMENT_M:
forward = route_xy / route_norm
if float(np.dot(forward, sensor_forward)) < 0.0:
forward = -forward
forward_source = "smoothed-pose-trajectory-tangent"
else:
forward = sensor_forward
forward_source = "rerun-rfu-sensor-forward-fallback"
up = np.asarray([0.0, 0.0, 1.0], dtype=np.float64)
left = np.cross(up, forward)
left_norm = float(np.linalg.norm(left))
if left_norm <= 1e-9:
raise ValueError("Recorded LAB body left axis is invalid")
left /= left_norm
forward = np.cross(left, up)
forward /= float(np.linalg.norm(forward))
basis = np.column_stack((forward, left, up))
if (
not np.allclose(basis.T @ basis, np.eye(3), atol=1e-7)
or np.linalg.det(basis) < 0.999999
):
raise ValueError("Recorded LAB gravity-stable body basis is invalid")
return basis, forward_source
def _estimate_sensor_height(points: _TimedPoints, poses: _TimedPoses) -> tuple[float, int, float]:
"""Estimate one session mount height from the initial qualified cloud.
The K1 recording has no explicit physical mount-height entity. The initial
stationary minute is therefore the only admissible automatic calibration
source. A low near-field quantile is measured per source increment and the
session median rejects vegetation/ravine outliers. The result stays
diagnostic and is never promoted to navigation authority by this adapter.
"""
first_time_ns = points.times_ns[0]
calibration_end_ns = first_time_ns + round(_HEIGHT_CALIBRATION_SECONDS * 1_000_000_000)
candidates = [
index
for index, timestamp in enumerate(points.times_ns)
if timestamp <= calibration_end_ns
][:_HEIGHT_CALIBRATION_MAX_FRAMES]
estimates: list[float] = []
for point_index in candidates:
pose_index = _latest_index(poses.times_ns, points.times_ns[point_index])
delta = points.values[point_index].astype(np.float64) - poses.translations[pose_index]
radius = np.linalg.norm(delta[:, :2], axis=1)
eligible = delta[
(radius >= _HEIGHT_NEAR_MIN_RADIUS_M)
& (radius <= _HEIGHT_NEAR_MAX_RADIUS_M)
& (delta[:, 2] >= -2.0)
& (delta[:, 2] <= 0.5)
]
if eligible.shape[0] < 100:
continue
estimate = -float(np.quantile(eligible[:, 2], _HEIGHT_LOWER_QUANTILE))
if 0.08 <= estimate <= 2.5:
estimates.append(estimate)
if len(estimates) < 8:
raise ValueError("Recorded LAB sensor height cannot be estimated from source cloud")
values = np.asarray(estimates, dtype=np.float64)
height = float(np.median(values))
mad = float(np.median(np.abs(values - height)))
return height, len(estimates), mad
def _estimate_local_sensor_height(
points: _TimedPoints,
poses: _TimedPoses,
target_time_ns: int,
fallback_height_m: float,
) -> tuple[float, int, float, str]:
"""Estimate the current gravity-axis height without a fixed camera mount.
RAVNOVES004TREE changes sensor height during the route. A session-wide
constant therefore moves the scene vertically whenever the operator raises
or lowers K1. Use a short source-time window and a conservative near-field
ground quantile; fall back to the sealed session calibration only when the
current cloud has insufficient support.
"""
half_window_ns = round(_LOCAL_HEIGHT_HALF_WINDOW_SECONDS * 1_000_000_000)
first = bisect_right(points.times_ns, max(0, target_time_ns - half_window_ns) - 1)
last = bisect_right(points.times_ns, target_time_ns + half_window_ns)
estimates: list[float] = []
for point_index in range(first, last):
pose_index = _latest_index(poses.times_ns, points.times_ns[point_index])
delta = points.values[point_index].astype(np.float64) - poses.translations[pose_index]
radius = np.linalg.norm(delta[:, :2], axis=1)
eligible = delta[
(radius >= _HEIGHT_NEAR_MIN_RADIUS_M)
& (radius <= _HEIGHT_NEAR_MAX_RADIUS_M)
& (delta[:, 2] >= -2.5)
& (delta[:, 2] <= 0.5)
]
if eligible.shape[0] < 100:
continue
estimate = -float(np.quantile(eligible[:, 2], _LOCAL_HEIGHT_QUANTILE))
if 0.03 <= estimate <= 2.5:
estimates.append(estimate)
if not estimates:
return fallback_height_m, 0, 0.0, "session-source-cloud-fallback"
values = np.asarray(estimates, dtype=np.float64)
height = float(np.median(values))
mad = float(np.median(np.abs(values - height)))
return height, len(estimates), mad, "local-source-cloud-ground-quantile-median"
def _ground_origin_map(
sensor_origin_map: np.ndarray,
sensor_height_m: float,
) -> np.ndarray:
# The calibrated height belongs to the map gravity axis. Sensor roll/pitch
# must never tilt the ground origin or the accumulated world cloud.
return sensor_origin_map - np.asarray([0.0, 0.0, sensor_height_m])
def _map_points_to_ground_body(
points_map: np.ndarray,
ground_origin_map: np.ndarray,
basis_map_from_body: np.ndarray,
) -> np.ndarray:
body = (points_map.astype(np.float64) - ground_origin_map) @ basis_map_from_body
return body.astype(np.float32)
def _bounded_local_slam(
points: _TimedPoints,
target_time_ns: int,
ground_origin_map: np.ndarray,
basis_map_from_body: np.ndarray,
) -> tuple[np.ndarray, int, int]:
start_ns = target_time_ns - round(_LOCAL_SLAM_HISTORY_SECONDS * 1_000_000_000)
first = bisect_right(points.times_ns, start_ns - 1)
last = bisect_right(points.times_ns, target_time_ns)
selected = points.values[first:last]
if not selected:
return np.empty((0, 3), dtype=np.float32), 0, 0
source_count = sum(int(value.shape[0]) for value in selected)
local = _map_points_to_ground_body(
np.concatenate(selected, axis=0),
ground_origin_map,
basis_map_from_body,
)
mask = (
(np.linalg.norm(local[:, :2], axis=1) <= _LOCAL_SLAM_RADIUS_M)
& (np.abs(local[:, 2]) <= _LOCAL_SLAM_VERTICAL_LIMIT_M)
)
local = local[mask]
if local.shape[0] == 0:
return local, len(selected), source_count
voxel = np.floor(local / _LOCAL_SLAM_VOXEL_SIZE_M).astype(np.int32)
_, retained = np.unique(voxel, axis=0, return_index=True)
local = local[np.sort(retained)]
if local.shape[0] > _LOCAL_SLAM_POINT_LIMIT:
stride = int(np.ceil(local.shape[0] / _LOCAL_SLAM_POINT_LIMIT))
local = local[::stride][:_LOCAL_SLAM_POINT_LIMIT]
return np.ascontiguousarray(local, dtype=np.float32), len(selected), source_count
def _canonical_lab_spatial_frame_from_index(
index: _CanonicalSpatialIndex,
target_time_ns: int,
*,
include_local_slam: bool = True,
) -> dict[str, object]:
point_index = _latest_index(index.points.times_ns, target_time_ns)
pose_index = _latest_index(index.poses.times_ns, index.points.times_ns[point_index])
trajectory_index = _latest_index(index.trajectories.times_ns, target_time_ns)
translation = index.poses.translations[pose_index]
sensor_height_m, sensor_height_sample_count, sensor_height_mad_m, height_source = (
_estimate_local_sensor_height(
index.points,
index.poses,
index.points.times_ns[point_index],
index.sensor_height_m,
)
)
basis_map_from_body, forward_source = _gravity_stable_basis_map_from_body(
index.poses,
index.points.times_ns[point_index],
)
ground_origin = _ground_origin_map(
translation,
sensor_height_m,
)
points_body = _map_points_to_ground_body(
index.points.values[point_index],
ground_origin,
basis_map_from_body,
)
if include_local_slam:
local_slam, local_slam_source_frames, local_slam_source_points = _bounded_local_slam(
index.points,
index.points.times_ns[point_index],
ground_origin,
basis_map_from_body,
)
else:
local_slam = np.empty((0, 3), dtype=np.float32)
local_slam_source_frames = 0
local_slam_source_points = 0
return {
"schema_version": "missioncore.canonical-recorded-lab-spatial-frame/v3",
"target_time_ns": target_time_ns,
"source_time_ns": index.points.times_ns[point_index],
"pose_time_ns": index.poses.times_ns[pose_index],
"trajectory_time_ns": index.trajectories.times_ns[trajectory_index],
"coordinate_frame": "body-ground",
"sensor_height": {
"meters": sensor_height_m,
"source": height_source,
"sample_count": sensor_height_sample_count,
"mad_m": sensor_height_mad_m,
"session_fallback_meters": index.sensor_height_m,
"authority": "visual-derived",
},
"spatial_profile": {
"profile_id": CANONICAL_LAB_SPATIAL_PROFILE,
"local_slam_history_seconds": _LOCAL_SLAM_HISTORY_SECONDS,
"local_slam_radius_m": _LOCAL_SLAM_RADIUS_M,
"local_slam_voxel_size_m": _LOCAL_SLAM_VOXEL_SIZE_M,
"local_slam_point_limit": _LOCAL_SLAM_POINT_LIMIT,
},
"body_frame": {
"origin_map_xyz_m": ground_origin.tolist(),
"sensor_origin_map_xyz_m": translation.tolist(),
"basis_map_from_body": basis_map_from_body.tolist(),
"up_source": "rerun-rfu-map-gravity-axis",
"forward_source": forward_source,
},
"source_point_count": int(points_body.shape[0]),
"source_points_body_xyz_m": points_body.tolist(),
"local_slam_source_frame_count": local_slam_source_frames,
"local_slam_source_point_count": local_slam_source_points,
"local_slam_point_count": int(local_slam.shape[0]),
"local_slam_body_xyz_m": local_slam.tolist(),
}
def canonical_lab_spatial_frame(
recording_path: Path,
generation_sha256: str,
target_time_ns: int,
) -> dict[str, object]:
"""Return the current source cloud and bounded Local SLAM on one media time."""
if target_time_ns < 0:
raise ValueError("Recorded LAB target time is invalid")
stat = recording_path.stat()
index = _load_index(
str(recording_path),
stat.st_size,
stat.st_mtime_ns,
generation_sha256,
)
return _canonical_lab_spatial_frame_from_index(index, target_time_ns)
def canonical_lab_spatial_timeline_samples(
recording_path: Path,
generation_sha256: str,
frame_times_ns: tuple[int, ...],
start_sequence: int,
frame_count: int,
*,
include_local_slam: bool = True,
) -> tuple[dict[str, object] | None, ...]:
"""Project only new source increments onto a denser camera timeline.
Camera is roughly 10 Hz in RAVNOVES004TREE while the sealed source cloud is
roughly 2 Hz. Returning the same JSON point array for every camera frame
multiplies transfer and parse cost and makes the viewer chase itself. A row
is populated only when its nearest causal source increment changes; the
canonical viewer retains that spatial frame until the next increment.
"""
if (
start_sequence < 0
or frame_count < 1
or start_sequence >= len(frame_times_ns)
or any(
current <= previous
for previous, current in zip(frame_times_ns, frame_times_ns[1:], strict=False)
)
):
raise ValueError("Recorded LAB timeline sample request is invalid")
stat = recording_path.stat()
index = _load_index(
str(recording_path),
stat.st_size,
stat.st_mtime_ns,
generation_sha256,
)
stop = min(len(frame_times_ns), start_sequence + frame_count)
samples: list[dict[str, object] | None] = []
for sequence in range(start_sequence, stop):
target_time_ns = frame_times_ns[sequence]
point_index = _latest_index(index.points.times_ns, target_time_ns)
previous_point_index = (
-1
if sequence == 0
else _latest_index(index.points.times_ns, frame_times_ns[sequence - 1])
)
samples.append(
_canonical_lab_spatial_frame_from_index(
index,
target_time_ns,
include_local_slam=include_local_slam,
)
if point_index != previous_point_index
else None
)
return tuple(samples)
@lru_cache(maxsize=2)
def _canonical_lab_spatial_playback_points_cached(
recording_path_text: str,
recording_size: int,
recording_mtime_ns: int,
generation_sha256: str,
frame_times_ns: tuple[int, ...],
) -> tuple[np.ndarray, tuple[int, ...]]:
del recording_size, recording_mtime_ns
recording_path = Path(recording_path_text)
stat = recording_path.stat()
index = _load_index(
str(recording_path),
stat.st_size,
stat.st_mtime_ns,
generation_sha256,
)
increments: list[np.ndarray] = []
offsets = [0]
point_count = 0
previous_point_index = -1
for target_time_ns in frame_times_ns:
point_index = _latest_index(index.points.times_ns, target_time_ns)
if point_index != previous_point_index:
increment = np.ascontiguousarray(index.points.values[point_index], dtype="<f4")
increments.append(increment)
point_count += int(increment.shape[0])
offsets.append(point_count)
previous_point_index = point_index
points = (
np.ascontiguousarray(np.concatenate(increments, axis=0), dtype="<f4")
if increments
else np.empty((0, 3), dtype="<f4")
)
points.setflags(write=False)
return points, tuple(offsets)
def canonical_lab_spatial_playback_points(
recording_path: Path,
generation_sha256: str,
frame_times_ns: tuple[int, ...],
) -> tuple[np.ndarray, tuple[int, ...]]:
"""Return one retained map-coordinate point track for a camera timeline."""
if (
not frame_times_ns
or any(
current <= previous
for previous, current in zip(frame_times_ns, frame_times_ns[1:], strict=False)
)
):
raise ValueError("Recorded LAB playback timeline is invalid")
stat = recording_path.stat()
return _canonical_lab_spatial_playback_points_cached(
str(recording_path),
stat.st_size,
stat.st_mtime_ns,
generation_sha256,
frame_times_ns,
)
@@ -36,6 +36,7 @@ IMAGE_DIGEST_PATTERN: Final = re.compile(r"^sha256:[a-f0-9]{64}$")
DEFAULT_CHUNK_BYTES: Final = 8 * 1024 * 1024
MAX_JSON_RESPONSE_BYTES: Final = 32 * 1024 * 1024
MAX_RETRIES: Final = 3
RETRYABLE_PROVIDER_STATUS_CODES: Final = {502, 503, 504}
DEFAULT_INGEST_TIMEOUT_SECONDS: Final = 30 * 60.0
@@ -899,7 +900,10 @@ def _validate_runtime_provenance(document: Mapping[str, object]) -> None:
def _unavailable(message: str, error: httpx.HTTPError) -> GaussianPipelineGatewayError:
if isinstance(error, httpx.TransportError):
if isinstance(error, httpx.TransportError) or (
isinstance(error, httpx.HTTPStatusError)
and error.response.status_code in RETRYABLE_PROVIDER_STATUS_CODES
):
return GaussianPipelineUnavailableError(message)
return GaussianPipelineGatewayError(message)
@@ -922,4 +926,6 @@ def _provider_rejection(response: httpx.Response) -> GaussianPipelineGatewayErro
message = f"Gaussian provider rejected request (HTTP {response.status_code})"
if detail is not None:
message = f"{message}: {detail}"
if response.status_code in RETRYABLE_PROVIDER_STATUS_CODES:
return GaussianPipelineUnavailableError(message)
return GaussianPipelineGatewayError(message)
+100 -3
View File
@@ -14,7 +14,7 @@ from collections import deque
from collections.abc import Callable
from contextlib import suppress
from pathlib import Path
from typing import Any, Final, TypeVar
from typing import Any, Final
from urllib.parse import quote
from uuid import uuid4
@@ -55,7 +55,7 @@ PROVIDER_JOB_STATES: Final = {
}
PROVIDER_POLL_TIMEOUT_SECONDS: Final = 2 * 60 * 60 + 5 * 60
PROVIDER_UNAVAILABLE_RETRY_LIMIT: Final = 6
_T = TypeVar("_T")
IMPORT_DISK_RESERVE_BYTES: Final = 512 * 1024 * 1024
class SimulationProjectError(RuntimeError):
@@ -701,6 +701,7 @@ class SimulationProjectService:
self._raise_if_cancelled(project_id)
artifacts = _artifact_descriptors(result.get("artifacts"))
artifacts_root = self.store.artifacts_root(project_id)
self._ensure_import_capacity(project_id, artifacts, artifacts_root)
for descriptor in artifacts:
self._raise_if_cancelled(project_id)
_retry_provider_unavailable(
@@ -722,6 +723,8 @@ class SimulationProjectService:
)
except _SimulationProcessingCancelled:
pass
except GaussianPipelineUnavailableError:
self._requeue_provider_unavailable(project_id)
except (GaussianPipelineGatewayError, SimulationProjectError, OSError) as exc:
with suppress(SimulationProjectError):
self.store.fail(project_id, str(exc))
@@ -731,6 +734,70 @@ class SimulationProjectService:
if provider is not None:
provider.close()
def _requeue_provider_unavailable(self, project_id: str) -> None:
"""Keep a retained source pending while its worker transport is unavailable."""
try:
self.store.update_processing(project_id, status="queued")
except SimulationProjectError:
return
with self._condition:
cancel = self._cancel_events.get(project_id)
if cancel is not None and cancel.is_set():
return
if self._active_project_id == project_id and project_id not in self._queued_ids:
self._queue.append(project_id)
self._queued_ids.add(project_id)
self._condition.notify_all()
def _ensure_import_capacity(
self,
project_id: str,
artifacts: list[dict[str, Any]],
artifacts_root: Path,
) -> None:
required_bytes = _remaining_import_bytes(artifacts_root, artifacts)
available_bytes = shutil.disk_usage(artifacts_root).free
if available_bytes >= required_bytes:
return
project = self.store.get(project_id)
source = project.get("source")
provider = project.get("provider")
if (
isinstance(source, dict)
and source.get("kind") == "archive"
and isinstance(provider, dict)
and isinstance(provider.get("job_id"), str)
):
source_files = source.get("files")
if isinstance(source_files, list) and len(source_files) == 1:
source_file = source_files[0]
if isinstance(source_file, dict):
logical_path = source_file.get("logical_path")
byte_length = source_file.get("byte_length")
if isinstance(logical_path, str) and isinstance(byte_length, int):
source_path = _confined_path(
self.store.source_root(project_id),
logical_path,
)
try:
source_stat = source_path.stat()
except OSError:
source_stat = None
if (
source_stat is not None
and source_path.is_file()
and not source_path.is_symlink()
and source_stat.st_size == byte_length
):
source_path.unlink()
available_bytes = shutil.disk_usage(artifacts_root).free
if available_bytes < required_bytes:
raise SimulationProjectError(
"Недостаточно места для импорта Gaussian-мира: "
f"нужно {_human_bytes(required_bytes)}, "
f"доступно {_human_bytes(available_bytes)}."
)
def delete(self, project_id: str) -> None:
project = self.store.get(project_id)
with self._condition:
@@ -794,7 +861,7 @@ class SimulationProjectService:
raise _SimulationProcessingCancelled(project_id)
def _retry_provider_unavailable(operation: Callable[[], _T]) -> _T:
def _retry_provider_unavailable[T](operation: Callable[[], T]) -> T:
delay_seconds = 1.0
for attempt in range(PROVIDER_UNAVAILABLE_RETRY_LIMIT):
try:
@@ -841,6 +908,36 @@ def _artifact_descriptors(value: object) -> list[dict[str, Any]]:
return descriptors
def _remaining_import_bytes(
artifacts_root: Path,
artifacts: list[dict[str, Any]],
) -> int:
missing_bytes = 0
replacement_scratch_bytes = 0
for descriptor in artifacts:
logical_path = str(descriptor["logical_path"])
expected_bytes = int(descriptor["byte_length"])
target = _confined_path(artifacts_root, logical_path)
try:
current = target.stat()
except OSError:
current = None
if (
current is not None
and target.is_file()
and not target.is_symlink()
and current.st_size == expected_bytes
):
replacement_scratch_bytes = max(replacement_scratch_bytes, expected_bytes)
else:
missing_bytes += expected_bytes
return missing_bytes + replacement_scratch_bytes + IMPORT_DISK_RESERVE_BYTES
def _human_bytes(value: int) -> str:
return f"{value / (1024**3):.2f} ГиБ"
def _world_manifest(project_id: str, artifacts: list[dict[str, Any]]) -> dict[str, Any]:
def url_for(role: str) -> str | None:
descriptor = next((item for item in artifacts if item["role"] == role), None)
+43 -23
View File
@@ -63,7 +63,7 @@ class _RecordedBlueprintStream:
self.view_reset_generation = view_reset_generation
self._lock = Lock()
self._sequence = 0
self._follow_trajectory: bool | None = None
self._eye_contract: tuple[bool, bool] | None = None
self._closed = False
native = bindings.new_blueprint(
application_id=application_id,
@@ -85,15 +85,15 @@ class _RecordedBlueprintStream:
blueprint_factory: Callable[[bool], rrb.Blueprint],
*,
follow_trajectory: bool,
plan_view: bool,
) -> bytes:
with self._lock:
if self._closed:
raise RecordedBlueprintError("stable blueprint stream is closed")
update_eye_controls = (
self._follow_trajectory is None
or self._follow_trajectory != follow_trajectory
)
eye_contract = (follow_trajectory, plan_view)
update_eye_controls = self._eye_contract != eye_contract
blueprint = blueprint_factory(update_eye_controls)
make_active = self._sequence == 0
self._blueprint_recording.set_time(
"blueprint",
sequence=self._sequence,
@@ -103,14 +103,14 @@ class _RecordedBlueprintStream:
self._blueprint_recording.flush(timeout_sec=5.0)
bindings.send_blueprint(
self._blueprint_memory.storage,
True,
make_active,
False,
self._transport_recording.to_native(),
)
payload = self._transport.read(flush=True, flush_timeout_sec=5.0)
if not payload:
raise RecordedBlueprintError("stable blueprint stream produced no data")
self._follow_trajectory = follow_trajectory
self._eye_contract = eye_contract
return payload
def close(self) -> None:
@@ -165,6 +165,8 @@ def recorded_blueprint(
active_view: RecordedView = "spatial",
view_reset_generation: Literal[0, 1] = 0,
unified_perception: bool = False,
semantic_layer: Literal["city", "vegetation"] | None = None,
plan_view: bool = False,
show_detections_2d: bool = False,
show_segmentation: bool = False,
show_cuboids_3d: bool = False,
@@ -202,6 +204,27 @@ def recorded_blueprint(
if accumulated_time_ranges is not None:
point_overrides.append(accumulated_time_ranges)
trajectory_overrides.append(accumulated_time_ranges)
selected_semantic_path = (
f"/perception/camera/segmentation/{semantic_layer}"
if semantic_layer is not None
else "/perception/camera/segmentation"
)
spatial_eye_controls = (
rrb.EyeControls3D.from_fields(
kind=rrb.Eye3DKind.Orbital,
position=[0.0, 0.0, 30.0],
look_target=[0.0, 0.0, 0.0],
eye_up=[0.0, 1.0, 0.0],
tracking_entity="/world/sensor_pose" if follow_trajectory else "",
)
if plan_view and update_eye_controls
else rrb.EyeControls3D.from_fields(
kind=rrb.Eye3DKind.Orbital if follow_trajectory else None,
tracking_entity="/world/sensor_pose" if follow_trajectory else "",
)
if update_eye_controls
else None
)
spatial_view = rrb.Spatial3DView(
origin="/world",
name="Мир · LiDAR и объекты" if unified_perception else "Пространственная сцена",
@@ -232,14 +255,7 @@ def recorded_blueprint(
# Keeping the view id stable preserves the current orbit offset when
# tracking is toggled. An explicit empty path clears tracking without
# overwriting the position/look-target saved by user interaction.
eye_controls=(
rrb.EyeControls3D.from_fields(
kind=rrb.Eye3DKind.Orbital if follow_trajectory else None,
tracking_entity="/world/sensor_pose" if follow_trajectory else "",
)
if update_eye_controls
else None
),
eye_controls=spatial_eye_controls,
)
spatial_view.id = (
RECORDED_SPATIAL_RESET_VIEW_ID
@@ -258,6 +274,12 @@ def recorded_blueprint(
"/perception/camera/segmentation": rrb.EntityBehavior(
visible=show_segmentation,
),
"/perception/camera/segmentation/city": rrb.EntityBehavior(
visible=show_segmentation and selected_semantic_path.endswith("/city"),
),
"/perception/camera/segmentation/vegetation": rrb.EntityBehavior(
visible=show_segmentation and selected_semantic_path.endswith("/vegetation"),
),
},
)
camera_view.id = (
@@ -292,14 +314,7 @@ def recorded_blueprint(
# native cloud from /world/points.
"/world/perception/lidar": rrb.EntityBehavior(visible=False),
},
eye_controls=(
rrb.EyeControls3D.from_fields(
kind=rrb.Eye3DKind.Orbital if follow_trajectory else None,
tracking_entity="/world/sensor_pose" if follow_trajectory else "",
)
if update_eye_controls
else None
),
eye_controls=spatial_eye_controls,
)
perception_3d_view.id = (
RECORDED_PERCEPTION_3D_RESET_VIEW_ID
@@ -400,6 +415,8 @@ def recorded_blueprint_rrd(
active_view: RecordedView = "spatial",
view_reset_generation: Literal[0, 1] = 0,
unified_perception: bool = False,
semantic_layer: Literal["city", "vegetation"] | None = None,
plan_view: bool = False,
show_detections_2d: bool = False,
show_segmentation: bool = False,
show_cuboids_3d: bool = False,
@@ -414,6 +431,8 @@ def recorded_blueprint_rrd(
active_view=active_view,
view_reset_generation=view_reset_generation,
unified_perception=unified_perception,
semantic_layer=semantic_layer,
plan_view=plan_view,
show_detections_2d=show_detections_2d,
show_segmentation=show_segmentation,
show_cuboids_3d=show_cuboids_3d,
@@ -432,6 +451,7 @@ def recorded_blueprint_rrd(
).render(
build_blueprint,
follow_trajectory=follow_trajectory,
plan_view=plan_view,
)
except RecordedBlueprintError:
raise
+1 -1
View File
@@ -149,7 +149,7 @@ class RerunBridge:
# of record. Raw MQTT evidence is persisted independently. A large
# late-client backlog can block the native SDK and freeze preview.
server_memory_limit=LIVE_GRPC_BUFFER_LIMIT,
# Rerun 0.34.1 can replay ActivateStore before StoreInfo when an
# Rerun transport can replay ActivateStore before StoreInfo when an
# evicted buffer is served newest-first, leaving late viewers on the
# welcome screen. Preserve protocol order within the bounded cache.
newest_first=False,
+43
View File
@@ -75,6 +75,9 @@ _E37_RESULT_ID = re.compile(r"^e37-ravnoves-acceptance-[a-f0-9]{64}$")
_E38_RESULT_ID = re.compile(r"^e38-perception-baseline-[a-f0-9]{64}$")
_E39_RESULT_ID = re.compile(r"^e39-perception-refinement-[a-f0-9]{64}$")
_E40_RESULT_ID = re.compile(r"^e40-perception-product-gate-[a-f0-9]{64}$")
_M4_RESULT_ID = re.compile(r"^m4-threat-replay-[a-f0-9]{64}$")
_M49_TGS_RESULT_ID = re.compile(r"^m49-tgs-full-shadow-[a-f0-9]{64}$")
_VEGETATION_RESULT_ID = re.compile(r"^lab-v1-vegetation-shadow-[a-f0-9]{64}$")
RootProvider = Callable[[], Path | None]
@@ -245,6 +248,7 @@ def _advanced_index_item(
raise ValueError("advanced LAB authority is invalid")
if document.get("ground_truth") not in (None, False):
raise ValueError("advanced LAB ground-truth claim is invalid")
_validate_product_publication_shape(document, work_id=work_id)
created_at_utc = document.get("created_at_utc")
if not isinstance(created_at_utc, str) or not created_at_utc.strip():
raise ValueError("advanced LAB creation time is invalid")
@@ -256,6 +260,45 @@ def _advanced_index_item(
}
def _validate_product_publication_shape(
document: dict[str, Any],
*,
work_id: str,
) -> None:
if work_id != "lab-v1-vegetation-shadow":
return
full_route = document.get("route_full_review")
if isinstance(full_route, dict):
if (
full_route.get("source_id") != "RAVNOVES004TREE"
or full_route.get("session_id") != "20260828T130511Z_viewer_live"
or full_route.get("frame_count") != 6830
or _VEGETATION_RESULT_ID.fullmatch(
str(full_route.get("linked_route_review_result_id", ""))
)
is None
or document.get("route_video") is not None
or document.get("route_review") is not None
):
raise ValueError("vegetation LAB has no canonical RAVNOVES004TREE publication shape")
return
route = document.get("route_video")
fusion = route.get("fusion") if isinstance(route, dict) else None
if (
not isinstance(route, dict)
or route.get("view_kind") != "coarse-material-policy-review"
or _M4_RESULT_ID.fullmatch(str(route.get("base_m4_result_id", ""))) is None
or _M49_TGS_RESULT_ID.fullmatch(str(route.get("linked_tgs_result_id", "")))
is None
or not isinstance(fusion, dict)
or fusion.get("mode") != "synchronised-multilayer-review"
or fusion.get("pixel_raster_fusion") is not False
or document.get("route_review") is not None
or document.get("route_full_review") is not None
):
raise ValueError("vegetation LAB has no canonical M4/M4.9 publication shape")
def _advanced_index(
specs: tuple[_AdvancedIndexSpec, ...],
) -> dict[str, object]:
+20
View File
@@ -360,6 +360,15 @@ def _m48_recorded_camera_playback_source(
return session_recorded_camera_frame_service.playback_source(session_id)
def _canonical_lab_recording_source(session_id: str) -> tuple[Path, str] | None:
"""Resolve one already-published immutable RRD without starting new work."""
snapshot = session_recording_preparation_manager.status(session_id)
if snapshot is None or snapshot.state != "ready" or snapshot.recording is None:
return None
return snapshot.recording.path, snapshot.recording.sha256
def refresh_observation_catalog() -> tuple[str, ...]:
"""Discover completed or recoverable local evidence without copying payloads."""
@@ -1032,6 +1041,17 @@ app.include_router(
/ "lab-v1-vegetation"
/ "results"
),
canonical_recording_provider=_canonical_lab_recording_source,
camera_frame_provider=(
session_recorded_camera_frame_service.extract
if session_recorded_camera_frame_service is not None
else None
),
jobs_root=REPOSITORY_ROOT / ".runtime" / "compute-jobs",
rerun_overlay_cache_root=(
session_store.data_dir / "laboratory-rerun-overlays"
),
ffmpeg_path=_ffmpeg,
)
)
app.include_router(
+82
View File
@@ -37,6 +37,10 @@ from k1link.sessions import (
SessionStore,
validate_recorded_media_timeline,
)
from k1link.sessions.canonical_lab_spatial import (
CANONICAL_LAB_SPATIAL_PROFILE,
canonical_lab_spatial_frame,
)
from k1link.sessions.plugin_contract import RecordedPointColorRenderer
from k1link.viewer.recorded import (
APPLICATION_ID as RECORDED_APPLICATION_ID,
@@ -120,6 +124,8 @@ class RecordedBlueprintRequest(StrictApiModel):
active_view: Literal["spatial", "perception", "perception3d", "metrics"] = "spatial"
view_reset_generation: Literal[0, 1] = 0
unified_perception: StrictBool = False
semantic_layer: Literal["city", "vegetation"] | None = None
plan_view: StrictBool = False
show_detections_2d: StrictBool = False
show_segmentation: StrictBool = False
show_cuboids_3d: StrictBool = False
@@ -824,6 +830,80 @@ def build_session_router(
**response_kwargs,
)
@router.get(
"/api/v1/observation-sessions/{session_id}/canonical-lab/spatial-frame"
)
async def get_observation_session_canonical_lab_spatial_frame(
session_id: str,
generation: Annotated[str, Query(min_length=64, max_length=64)],
time_ns: Annotated[int, Query(ge=0, le=MAX_SAFE_INTEGER)],
profile: Literal["source-paced-ground-v3"],
) -> JSONResponse:
"""Serve one body-frame sample for the canonical recorded-LAB clock.
The camera media clock owns playback. Spatial evidence is sampled from
the same immutable recording instead of starting a second Rerun clock.
"""
if SAFE_SHA256.fullmatch(generation) is None:
raise HTTPException(
status_code=412,
detail="Поколение spatial-записи не совпадает.",
)
if recording_preparation_manager is None:
raise HTTPException(
status_code=503,
detail="Сервис canonical LAB spatial playback не настроен.",
)
snapshot = recording_preparation_manager.status(session_id)
if snapshot is None or snapshot.state != "ready" or snapshot.recording is None:
raise HTTPException(
status_code=409,
detail="Запись canonical LAB ещё не подготовлена.",
)
_require_matching_recording_generation(snapshot.recording.sha256, generation)
pinned = recording_preparation_manager.pin_ready(
session_id,
preparation_id=snapshot.preparation_id,
)
if pinned is None:
raise HTTPException(
status_code=412,
detail="Подготовленная spatial-запись была заменена.",
)
pinned_snapshot, release_recording = pinned
try:
recording = pinned_snapshot.recording
if recording is None:
raise HTTPException(
status_code=500,
detail="Подготовленная spatial-запись недоступна.",
)
payload = await run_in_threadpool(
canonical_lab_spatial_frame,
recording.path,
generation,
time_ns,
)
except (OSError, ValueError) as exc:
raise HTTPException(
status_code=503,
detail="Canonical LAB spatial frame не прошёл проверку.",
) from exc
finally:
release_recording()
return JSONResponse(
payload,
headers={
"Cache-Control": "private, max-age=31536000, immutable",
"ETag": (
f'"{generation}:{CANONICAL_LAB_SPATIAL_PROFILE}:'
f'{payload["source_time_ns"]}"'
),
"X-Content-Type-Options": "nosniff",
},
)
@router.post("/api/v1/observation-sessions/{session_id}/blueprint.rrd")
async def get_observation_session_blueprint(
session_id: str,
@@ -857,6 +937,8 @@ def build_session_router(
active_view=request.active_view,
view_reset_generation=request.view_reset_generation,
unified_perception=request.unified_perception,
semantic_layer=request.semantic_layer,
plan_view=request.plan_view,
show_detections_2d=request.show_detections_2d,
show_segmentation=request.show_segmentation,
show_cuboids_3d=request.show_cuboids_3d,
File diff suppressed because it is too large Load Diff
+79
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import json
import os
from pathlib import Path, PurePosixPath
from types import SimpleNamespace
@@ -151,6 +152,84 @@ def test_advanced_index_projects_one_most_mature_lifecycle_phase(tmp_path: Path)
]
def test_advanced_index_prefers_canonical_rav004_full_review(tmp_path: Path) -> None:
root = tmp_path / "vegetation"
def publish(digest: str, *, publication: str) -> Path:
result_id = f"lab-v1-vegetation-shadow-{digest}"
candidate = root / result_id
candidate.mkdir(parents=True)
route_video = {
"view_kind": "coarse-material-policy-review",
"base_m4_result_id": f"m4-threat-replay-{'1' * 64}",
"linked_tgs_result_id": f"m49-tgs-full-shadow-{'2' * 64}",
"fusion": {
"mode": "synchronised-multilayer-review",
"pixel_raster_fusion": False,
},
} if publication == "rav00" else None
route_full_review = {
"source_id": "RAVNOVES004TREE",
"session_id": "20260828T130511Z_viewer_live",
"frame_count": 6830,
"linked_route_review_result_id": (
f"lab-v1-vegetation-shadow-{'4' * 64}"
if publication == "rav004"
else None
),
} if publication != "rav00" else None
(candidate / "manifest.json").write_text(
json.dumps(
{
"schema_version": "missioncore.lab-v1-vegetation-shadow/v1",
"result_id": result_id,
"identity_sha256": digest,
"identity": {
"authority": {
"commands_enabled": False,
"navigation_or_safety_accepted": False,
}
},
"created_at_utc": "2026-08-29T10:00:00Z",
"ground_truth": False,
"route_video": route_video,
"route_review": None,
"route_full_review": route_full_review,
}
),
encoding="utf-8",
)
return candidate
rav00 = publish("a" * 64, publication="rav00")
incomplete = publish("b" * 64, publication="incomplete")
rav004 = publish("c" * 64, publication="rav004")
os.utime(rav00, ns=(10_000_000_000, 10_000_000_000))
os.utime(incomplete, ns=(20_000_000_000, 20_000_000_000))
os.utime(rav004, ns=(30_000_000_000, 30_000_000_000))
registry = _evidence_registry(
root,
work_id="lab-v1-vegetation-shadow",
result_id_prefix="lab-v1-vegetation-shadow",
schema_version="missioncore.lab-v1-vegetation-shadow/v1",
)
router = build_advanced_laboratory_router(
evidence_registry=registry,
evidence_runtime_root_provider=lambda: root.parent,
)
index = _endpoint(router, "/api/v1/laboratory/advanced-index")()
assert index["items"] == [ # type: ignore[index]
{
"work_id": "lab-v1-vegetation-shadow",
"result_id": rav004.name,
"created_at_utc": "2026-08-29T10:00:00Z",
"access": "read-only",
}
]
def test_advanced_index_includes_valid_l31_identity(
tmp_path: Path,
monkeypatch: MonkeyPatch,
+172
View File
@@ -0,0 +1,172 @@
from __future__ import annotations
import numpy as np
import pytest
import k1link.sessions.canonical_lab_spatial as spatial_module
from k1link.sessions.canonical_lab_spatial import (
_bounded_local_slam,
_CanonicalSpatialIndex,
_estimate_local_sensor_height,
_estimate_sensor_height,
_gravity_stable_basis_map_from_body,
_ground_origin_map,
_TimedPoints,
_TimedPoses,
canonical_lab_spatial_playback_points,
)
def _calibration_cloud(height_m: float, seed: int) -> np.ndarray:
rng = np.random.default_rng(seed)
xy = rng.uniform(-5.5, 5.5, size=(500, 2)).astype(np.float32)
radius = np.linalg.norm(xy, axis=1)
xy = xy[(radius >= 1.0) & (radius <= 5.5)][:360]
ground = np.column_stack((
xy,
rng.normal(-height_m, 0.006, size=xy.shape[0]),
)).astype(np.float32)
vegetation = np.column_stack((
rng.uniform(-5, 5, size=(300, 2)),
rng.uniform(0.0, 1.2, size=300),
)).astype(np.float32)
return np.concatenate((ground, vegetation), axis=0)
def test_session_sensor_height_is_derived_from_initial_source_cloud() -> None:
times = tuple(index * 500_000_000 for index in range(12))
points = _TimedPoints(
times_ns=times,
values=tuple(_calibration_cloud(0.32, index) for index in range(12)),
)
poses = _TimedPoses(
times_ns=times,
translations=tuple(np.zeros(3) for _ in times),
quaternions_xyzw=tuple(np.asarray([0.0, 0.0, 0.0, 1.0]) for _ in times),
)
height, sample_count, mad = _estimate_sensor_height(points, poses)
assert height == pytest.approx(0.32, abs=0.02)
assert sample_count == 12
assert mad < 0.02
def test_local_slam_accumulates_source_increments_in_ground_body_frame() -> None:
points = _TimedPoints(
times_ns=(0, 1_000_000_000, 2_000_000_000),
values=(
np.asarray([[1.0, 0.0, -0.32]], dtype=np.float32),
np.asarray([[2.0, 0.0, -0.32]], dtype=np.float32),
np.asarray([[3.0, 0.0, -0.32]], dtype=np.float32),
),
)
basis = np.eye(3)
ground_origin = _ground_origin_map(np.asarray([0.0, 0.0, 0.0]), 0.32)
local, frame_count, source_count = _bounded_local_slam(
points,
2_000_000_000,
ground_origin,
basis,
)
assert frame_count == 3
assert source_count == 3
assert local[:, 2].tolist() == pytest.approx([0.0, 0.0, 0.0], abs=1e-6)
def test_gravity_stable_body_frame_converts_rfu_to_forward_left_up() -> None:
times = (0, 1_000_000_000, 2_000_000_000)
poses = _TimedPoses(
times_ns=times,
translations=(
np.asarray([0.0, 0.0, 0.4]),
np.asarray([0.0, 1.0, 0.5]),
np.asarray([0.0, 2.0, 0.3]),
),
quaternions_xyzw=tuple(
np.asarray([0.25, 0.0, 0.0, np.sqrt(1.0 - 0.25**2)]) for _ in times
),
)
basis, source = _gravity_stable_basis_map_from_body(poses, 1_000_000_000)
assert source == "smoothed-pose-trajectory-tangent"
assert basis[:, 0].tolist() == pytest.approx([0.0, 1.0, 0.0], abs=1e-7)
assert basis[:, 1].tolist() == pytest.approx([-1.0, 0.0, 0.0], abs=1e-7)
assert basis[:, 2].tolist() == pytest.approx([0.0, 0.0, 1.0], abs=1e-7)
assert np.linalg.det(basis) == pytest.approx(1.0, abs=1e-7)
def test_ground_origin_is_projected_only_along_map_gravity() -> None:
origin = _ground_origin_map(np.asarray([4.0, -2.0, 1.25]), 0.32)
assert origin.tolist() == pytest.approx([4.0, -2.0, 0.93], abs=1e-9)
def test_sensor_height_tracks_current_source_window_instead_of_fixed_mount() -> None:
times = tuple(index * 500_000_000 for index in range(8))
points = _TimedPoints(
times_ns=times,
values=tuple(
_calibration_cloud(0.18 if index < 4 else 1.05, index)
for index in range(8)
),
)
poses = _TimedPoses(
times_ns=times,
translations=tuple(np.zeros(3) for _ in times),
quaternions_xyzw=tuple(np.asarray([0.0, 0.0, 0.0, 1.0]) for _ in times),
)
low, low_samples, _, low_source = _estimate_local_sensor_height(
points, poses, 500_000_000, 0.5,
)
high, high_samples, _, high_source = _estimate_local_sensor_height(
points, poses, 3_000_000_000, 0.5,
)
assert low == pytest.approx(0.18, abs=0.03)
assert high == pytest.approx(1.05, abs=0.03)
assert low_samples >= 3 and high_samples >= 3
assert low_source == high_source == "local-source-cloud-ground-quantile-median"
def test_playback_track_binds_sparse_map_increments_to_dense_camera_timeline(
tmp_path,
monkeypatch,
) -> None:
recording = tmp_path / "recording.rrd"
recording.write_bytes(b"sealed")
points = _TimedPoints(
times_ns=(10, 20),
values=(
np.asarray([[1.0, 2.0, 3.0]], dtype=np.float32),
np.asarray([[4.0, 5.0, 6.0], [7.0, 8.0, 9.0]], dtype=np.float32),
),
)
empty_poses = _TimedPoses(times_ns=(), translations=(), quaternions_xyzw=())
index = _CanonicalSpatialIndex(
points=points,
poses=empty_poses,
trajectories=_TimedPoints(times_ns=(), values=()),
sensor_height_m=0.4,
sensor_height_sample_count=0,
sensor_height_mad_m=0.0,
)
monkeypatch.setattr(spatial_module, "_load_index", lambda *_args: index)
track, offsets = canonical_lab_spatial_playback_points(
recording,
"a" * 64,
(10, 15, 20, 25),
)
assert offsets == (0, 1, 1, 3, 3)
assert track.tolist() == [
[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0],
[7.0, 8.0, 9.0],
]
assert track.dtype == np.dtype("<f4")
assert not track.flags.writeable
+25
View File
@@ -14,6 +14,7 @@ from k1link.simulation.gaussian_pipeline_gateway import (
GaussianPipelineGateway,
GaussianPipelineGatewayError,
GaussianPipelineIntegrityError,
GaussianPipelineUnavailableError,
_discover_bundle_members,
)
@@ -256,6 +257,30 @@ def test_gateway_surfaces_bounded_provider_rejection_detail(tmp_path: Path) -> N
))
@pytest.mark.parametrize("status_code", [502, 503, 504])
def test_gateway_classifies_temporary_provider_proxy_failures_as_unavailable(
tmp_path: Path,
status_code: int,
) -> None:
with (
GaussianPipelineGateway(
"http://gaussian.test",
_token_file(tmp_path),
transport=httpx.MockTransport(
lambda _request: httpx.Response(
status_code,
json={"error": "provider_unavailable"},
)
),
) as gateway,
pytest.raises(
GaussianPipelineUnavailableError,
match=rf"HTTP {status_code}.*provider_unavailable",
),
):
gateway.capabilities()
def test_gateway_rejects_incomplete_lcc_bundle(tmp_path: Path) -> None:
bundle = tmp_path / "bundle"
bundle.mkdir()
+3 -3
View File
@@ -594,9 +594,9 @@ def test_recorded_blueprint_layer_updates_preserve_store_until_explicit_reset(
assert eye_control_updates == [True, False, True, False, True]
assert [activation[1:] for activation in activations] == [
(True, False),
(True, False),
(True, False),
(True, False),
(False, False),
(False, False),
(False, False),
(True, False),
]
assert activations[0][0] is activations[1][0]
+97
View File
@@ -17,6 +17,7 @@ from fastapi.responses import FileResponse
from fastapi.routing import APIRoute
import k1link.sessions.media as recorded_media_module
import k1link.web.session_api as session_api_module
from k1link.compute import RecordedPerceptionOverlayArtifact, RecordedPerceptionVideo
from k1link.device_plugins.xgrids_k1 import xgrids_k1_archive_source
from k1link.device_plugins.xgrids_k1.mqtt.capture import FRAME_HEADER, RAW_MAGIC
@@ -458,6 +459,102 @@ def test_completed_recording_get_does_not_hold_delete_for_launch_lease(
manager.close()
def test_canonical_lab_spatial_frame_uses_ready_immutable_recording(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
repository = tmp_path / "repo"
sessions = repository / "sessions"
session = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
store = SessionStore(repository, data_dir=tmp_path / "data")
store.reconcile_archive(xgrids_k1_archive_source(sessions))
payload = b"sealed-spatial-recording"
def export_recording(source: Path, destination: Path) -> dict[str, object]:
destination.write_bytes(payload)
return {
"source_sha256": hashlib.sha256(source.read_bytes()).hexdigest(),
"rrd_sha256": hashlib.sha256(payload).hexdigest(),
"rrd_bytes": len(payload),
"timeline": "session_time",
"timeline_start_ns": 0,
"timeline_end_ns": 1_000_000_000,
}
materializer = SessionRecordingMaterializer(store.data_dir, exporter=export_recording)
command = store.prepare_replay(session.name)
recording = materializer.materialize(command)
manager = SessionRecordingPreparationManager(materializer)
resolved = manager.resolve_cached(command)
assert resolved is not None and resolved.recording is not None
generation = hashlib.sha256(payload).hexdigest()
expected = {
"schema_version": "missioncore.canonical-recorded-lab-spatial-frame/v3",
"target_time_ns": 500_000_000,
"source_time_ns": 499_000_000,
"pose_time_ns": 499_000_000,
"trajectory_time_ns": 490_000_000,
"coordinate_frame": "body-ground",
"sensor_height": {
"meters": 0.32,
"source": "local-source-cloud-ground-quantile-median",
"sample_count": 20,
"mad_m": 0.03,
"authority": "visual-derived",
},
"spatial_profile": {
"profile_id": "source-paced-ground-v3",
"local_slam_history_seconds": 5.0,
"local_slam_radius_m": 30.0,
"local_slam_voxel_size_m": 0.12,
"local_slam_point_limit": 27000,
},
"body_frame": {
"origin_map_xyz_m": [0.0, 0.0, 0.0],
"sensor_origin_map_xyz_m": [0.0, 0.0, 0.32],
"basis_map_from_body": [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
},
"source_point_count": 1,
"source_points_body_xyz_m": [[1.0, 2.0, 3.0]],
"local_slam_source_frame_count": 1,
"local_slam_source_point_count": 1,
"local_slam_point_count": 1,
"local_slam_body_xyz_m": [[0.0, 0.0, 0.0]],
}
def spatial_frame(path: Path, sha256: str, time_ns: int) -> dict[str, object]:
assert path == recording.path
assert sha256 == generation
assert time_ns == 500_000_000
return expected
monkeypatch.setattr(session_api_module, "canonical_lab_spatial_frame", spatial_frame)
router = build_session_router(
store,
recording_materializer=materializer,
recording_preparation_manager=manager,
)
spatial_route = endpoint(
router,
"/api/v1/observation-sessions/{session_id}/canonical-lab/spatial-frame",
"GET",
)
try:
response = asyncio.run(spatial_route(
session_id=session.name,
generation=generation,
time_ns=500_000_000,
profile="source-paced-ground-v3",
))
assert json.loads(response.body) == expected
assert response.headers["etag"] == (
f'"{generation}:source-paced-ground-v3:499000000"'
)
assert response.headers["cache-control"].endswith("immutable")
finally:
manager.close()
def test_session_router_returns_seekable_recording_and_serves_byte_ranges(
tmp_path: Path,
) -> None:
+107
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
import json
from pathlib import Path
from threading import Event
from time import monotonic, sleep
from typing import Any
import pytest
@@ -444,6 +445,55 @@ def test_service_queue_processes_projects_strictly_one_at_a_time(tmp_path: Path)
assert order == [projects[0]["project_id"], projects[1]["project_id"]]
def test_service_keeps_retained_source_queued_across_temporary_worker_outage(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
store = SimulationProjectStore(tmp_path)
project = store.create(
name="Reconnect without browser reupload",
scene_type="outdoor",
source_kind="folder",
files=_folder_files(),
)
_upload_all(store, project)
store.begin_build(project["project_id"])
class _ReconnectProvider(_ReadyProvider):
def __init__(self) -> None:
super().__init__()
self.capability_calls = 0
def capabilities(self) -> dict[str, object]:
self.capability_calls += 1
if self.capability_calls == 1:
raise GaussianPipelineUnavailableError("temporary tunnel failure")
return super().capabilities()
provider = _ReconnectProvider()
monkeypatch.setattr(
"k1link.simulation.projects.PROVIDER_UNAVAILABLE_RETRY_LIMIT",
1,
)
service = SimulationProjectService(
store,
provider_factory=lambda: provider,
) # type: ignore[arg-type]
service.enqueue(project["project_id"])
deadline = monotonic() + 1.0
while store.get(project["project_id"])["status"] != "ready" and monotonic() < deadline:
sleep(0.01)
recovered = store.get(project["project_id"])
assert recovered["status"] == "ready"
assert recovered["error"] is None
assert recovered["source"]["uploaded_byte_length"] == recovered["source"]["total_byte_length"]
assert provider.capability_calls == 2
assert provider.upload_calls == 1
assert provider.submit_calls == 1
def test_service_deletes_a_queued_project_before_worker_submission(tmp_path: Path) -> None:
store = SimulationProjectStore(tmp_path)
project = store.create(
@@ -561,6 +611,63 @@ def test_failed_project_retries_from_retained_source_and_releases_old_job(tmp_pa
assert queued["source"]["uploaded_byte_length"] == queued["source"]["total_byte_length"]
def test_import_evicts_only_worker_backed_archive_staging_when_disk_is_low(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
store = SimulationProjectStore(tmp_path)
project = store.create(
name="Worker-backed archive",
scene_type="outdoor",
source_kind="archive",
files=[{"logical_path": "scene.rar", "byte_length": 6}],
)
source_file = project["source"]["files"][0]
store.append_upload(
project["project_id"],
source_file["file_id"],
offset=0,
payload=b"source",
)
store.begin_build(project["project_id"])
store.update_processing(
project["project_id"],
status="processing",
provider_job_id="gsp-20260826000000-deadbeef",
provider_state="ready",
)
service = SimulationProjectService(store, provider_factory=lambda: None)
source_path = store.source_root(project["project_id"]) / "scene.rar"
class _DiskUsage:
def __init__(self, free: int) -> None:
self.free = free
monkeypatch.setattr(
"k1link.simulation.projects.shutil.disk_usage",
lambda _path: _DiskUsage(0 if source_path.exists() else 10 * 1024**3),
)
artifacts = [
{
"role": "preview",
"logical_path": "preview.sog",
"media_type": "application/octet-stream",
"sha256": "a" * 64,
"byte_length": 1024,
}
]
service._ensure_import_capacity(
project["project_id"],
artifacts,
store.artifacts_root(project["project_id"]),
)
assert not source_path.exists()
retained_metadata = store.get(project["project_id"])["source"]
assert retained_metadata["uploaded_byte_length"] == retained_metadata["total_byte_length"]
def test_failed_local_project_reattaches_to_live_provider_job_without_rebuild(
tmp_path: Path,
) -> None:
+397 -1
View File
@@ -14,18 +14,414 @@ from fastapi import FastAPI
from fastapi.testclient import TestClient
from PIL import Image
import k1link.laboratory.canonical_rerun_overlay as canonical_overlay_module
import k1link.laboratory.vegetation_policy_review as policy_review_module
import k1link.laboratory.vegetation_policy_video as policy_video_module
import k1link.laboratory.vegetation_shadow_lab as vegetation_lab_module
import k1link.web.vegetation_shadow_lab_api as vegetation_api_module
from k1link.laboratory import LaboratoryEvidenceRegistry
from k1link.laboratory.canonical_rerun_overlay import (
CanonicalLabOverlayArtifact,
CanonicalLabReplayArtifact,
_artifact_is_regular,
_encoded_semantic_png,
_localized_semantic_label,
_optimize_overlay,
_semantic_palette,
_video_reference_timestamps,
canonical_lab_replay,
)
from k1link.laboratory.evidence_report import verify_laboratory_evidence_result
from k1link.laboratory.vegetation_policy_review import seal_vegetation_policy_review
from k1link.laboratory.vegetation_shadow_lab import seal_vegetation_shadow_lab
from k1link.web.vegetation_shadow_lab_api import build_vegetation_shadow_lab_router
from k1link.web.vegetation_shadow_lab_api import (
_canonical_route_playback_chunk_descriptor,
_mask_component_boxes,
_route_tgs_anchor_payload,
build_vegetation_shadow_lab_router,
)
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
def test_semantic_component_boxes_keep_distinct_objects_separate() -> None:
mask = np.zeros((20, 30), dtype=np.uint8)
mask[2:10, 3:8] = 4
mask[4:12, 18:24] = 4
mask[15:17, 3:5] = 4
assert _mask_component_boxes(mask, 4, minimum_pixels=20) == [
(18, 4, 24, 12, 48),
(3, 2, 8, 10, 40),
]
_, labels = canonical_overlay_module.semantic_component_boxes(mask, 0)
assert labels == [
"автомобиль · 50%",
"автомобиль · 50%",
]
def test_canonical_overlay_localizes_current_taxonomies() -> None:
assert _localized_semantic_label("high_grass") == "высокая трава"
assert _localized_semantic_label("tree_trunk") == "ствол дерева"
assert _localized_semantic_label("future_class") == "future_class"
def test_canonical_video_references_hold_only_missing_source_samples() -> None:
session_times = np.arange(10, dtype=np.int64) * 100_000_000 + 39_000_000_000
video_times = np.array(
[0, 100, 200, 300, 400, 500, 600, 700, 900],
dtype=np.int64,
) * 1_000_000
references = _video_reference_timestamps(video_times, session_times)
assert references.tolist() == [
0,
100_000_000,
200_000_000,
300_000_000,
400_000_000,
500_000_000,
600_000_000,
700_000_000,
700_000_000,
900_000_000,
]
def test_canonical_overlay_memory_cache_rejects_same_size_tampering(
tmp_path: Path,
) -> None:
path = tmp_path / "overlay.rrd"
path.write_bytes(b"RRF2-original")
artifact = CanonicalLabOverlayArtifact(
path=path,
byte_length=path.stat().st_size,
sha256=_sha256(path),
)
assert _artifact_is_regular(artifact)
path.write_bytes(b"RRF2-tampered")
assert path.stat().st_size == artifact.byte_length
assert not _artifact_is_regular(artifact)
def test_canonical_overlay_keeps_semantics_as_palette_encoded_png() -> None:
mask = np.zeros((600, 800), dtype=np.uint8)
mask[120:420, 200:600] = 7
palette = _semantic_palette(
[
{"class_id": 0, "color_rgb": [0, 0, 0]},
{"class_id": 7, "color_rgb": [255, 47, 128]},
]
)
encoded = _encoded_semantic_png(mask, palette)
assert len(encoded) < mask.nbytes // 20
with Image.open(io.BytesIO(encoded)) as image:
assert image.mode == "P"
assert image.getpixel((0, 0)) == 0
assert image.getpixel((300, 300)) == 7
assert image.getpalette()[7 * 3 : 7 * 3 + 3] == [255, 47, 128]
def test_canonical_overlay_compacts_chunks_before_cache_publication(
tmp_path: Path,
monkeypatch,
) -> None:
source = tmp_path / "source.rrd"
source.write_bytes(b"RRF2-source")
def optimize(command: list[str], **options: object) -> SimpleNamespace:
assert command[:4] == [
canonical_overlay_module.sys.executable,
"-m",
"rerun",
"rrd",
]
assert command[4:13] == [
"optimize",
"--profile",
"object-store",
"--max-size",
"4MiB",
"--max-rows",
"512",
"--num-pass",
"20",
]
assert command[13] == str(source)
assert command[14] == "-o"
Path(command[15]).write_bytes(b"RRF2-optimized")
assert options == {"check": False, "capture_output": True, "timeout": 120}
return SimpleNamespace(returncode=0, stderr=b"")
monkeypatch.setattr(canonical_overlay_module.subprocess, "run", optimize)
_optimize_overlay(source)
assert source.read_bytes() == b"RRF2-optimized"
def test_canonical_replay_merges_base_and_overlay_once(
tmp_path: Path,
monkeypatch,
) -> None:
base = tmp_path / "base.rrd"
base.write_bytes(b"RRF2-base")
overlay_path = tmp_path / "overlay.rrd"
overlay_path.write_bytes(b"RRF2-overlay")
overlay = CanonicalLabOverlayArtifact(
path=overlay_path,
byte_length=overlay_path.stat().st_size,
sha256=_sha256(overlay_path),
)
calls = 0
def optimize(command: list[str], **options: object) -> SimpleNamespace:
nonlocal calls
calls += 1
assert command[13:15] == [str(base), str(overlay_path)]
assert command[15] == "-o"
Path(command[16]).write_bytes(b"RRF2-merged")
assert options == {"check": False, "capture_output": True, "timeout": 180}
return SimpleNamespace(returncode=0, stderr=b"")
monkeypatch.setattr(canonical_overlay_module.subprocess, "run", optimize)
monkeypatch.setattr(
canonical_overlay_module,
"canonical_recording_id",
lambda _path: "recording-001",
)
result_id = f"lab-v1-vegetation-shadow-{'e' * 64}"
first = canonical_lab_replay(
base,
base_generation_sha256=_sha256(base),
overlay=overlay,
result_id=result_id,
recording_id="recording-001",
cache_root=tmp_path / "cache",
)
second = canonical_lab_replay(
base,
base_generation_sha256=_sha256(base),
overlay=overlay,
result_id=result_id,
recording_id="recording-001",
cache_root=tmp_path / "cache",
)
assert first == second
assert first.path.read_bytes() == b"RRF2-merged"
assert calls == 1
def test_canonical_overlay_get_is_generation_bound_and_range_streamable(
tmp_path: Path,
monkeypatch,
) -> None:
result_id = f"lab-v1-vegetation-shadow-{'a' * 64}"
result_root = tmp_path / result_id
result_root.mkdir()
base = tmp_path / "base.rrd"
base.write_bytes(b"RRF2-base")
overlay = tmp_path / "overlay.rrd"
overlay.write_bytes(b"RRF2-overlay")
replay = tmp_path / "replay.rrd"
replay.write_bytes(b"RRF2-replay")
generation = "b" * 64
recording_id = "recording-001"
artifact = CanonicalLabOverlayArtifact(
path=overlay,
byte_length=overlay.stat().st_size,
sha256=_sha256(overlay),
)
replay_artifact = CanonicalLabReplayArtifact(
path=replay,
byte_length=replay.stat().st_size,
sha256=_sha256(replay),
)
monkeypatch.setattr(
vegetation_api_module,
"_resolve_candidate",
lambda *_args, **_kwargs: result_root,
)
monkeypatch.setattr(
vegetation_api_module,
"_read_verified",
lambda *_args, **_kwargs: {},
)
monkeypatch.setattr(
vegetation_api_module,
"_full_route_context",
lambda *_args, **_kwargs: ({"session_id": "session-001"}, ()),
)
monkeypatch.setattr(
vegetation_api_module,
"canonical_recording_id",
lambda _path: recording_id,
)
monkeypatch.setattr(
vegetation_api_module,
"canonical_lab_overlay",
lambda *_args, **_kwargs: artifact,
)
monkeypatch.setattr(
vegetation_api_module,
"canonical_lab_replay",
lambda *_args, **_kwargs: replay_artifact,
)
ffmpeg = tmp_path / "ffmpeg"
ffmpeg.write_bytes(b"fixture")
ffmpeg.chmod(0o700)
app = FastAPI()
app.include_router(
build_vegetation_shadow_lab_router(
root_provider=lambda: tmp_path,
canonical_recording_provider=lambda _session_id: (base, generation),
jobs_root=tmp_path,
rerun_overlay_cache_root=tmp_path / "cache",
ffmpeg_path=ffmpeg,
)
)
client = TestClient(app)
endpoint = f"/api/v1/laboratory/vegetation-shadow/{result_id}/canonical-overlay.rrd"
descriptor = client.head(
endpoint,
params={
"application_id": "nodedc_mission_core_recorded",
"recording_id": recording_id,
"generation": generation,
},
)
assert descriptor.status_code == 200
assert descriptor.content == b""
assert descriptor.headers["content-length"] == str(artifact.byte_length)
assert descriptor.headers["etag"] == f'"{artifact.sha256}"'
assert descriptor.headers["x-rerun-format"] == "RRF2"
assert descriptor.headers["cache-control"] == "private, no-store"
response = client.get(
endpoint,
params={
"application_id": "nodedc_mission_core_recorded",
"recording_id": recording_id,
"generation": generation,
"overlay_generation": artifact.sha256,
},
headers={"Range": "bytes=0-3"},
)
assert response.status_code == 206
assert response.content == b"RRF2"
assert response.headers["content-range"] == f"bytes 0-3/{artifact.byte_length}"
assert response.headers["etag"] == f'"{artifact.sha256}"'
assert response.headers["cache-control"].endswith("immutable")
stale = client.head(
endpoint,
params={
"application_id": "nodedc_mission_core_recorded",
"recording_id": recording_id,
"generation": "c" * 64,
},
)
assert stale.status_code == 412
stale_overlay = client.get(
endpoint,
params={
"application_id": "nodedc_mission_core_recorded",
"recording_id": recording_id,
"generation": generation,
"overlay_generation": "d" * 64,
},
)
assert stale_overlay.status_code == 412
replay_endpoint = (
f"/api/v1/laboratory/vegetation-shadow/{result_id}/canonical-replay.rrd"
)
replay_descriptor = client.head(
replay_endpoint,
params={"base_generation": generation},
)
assert replay_descriptor.status_code == 200
assert replay_descriptor.headers["content-length"] == str(replay_artifact.byte_length)
assert replay_descriptor.headers["etag"] == f'"{replay_artifact.sha256}"'
assert replay_descriptor.headers["x-rerun-format"] == "RRF2"
replay_response = client.get(
replay_endpoint,
params={"generation": replay_artifact.sha256},
headers={"Range": "bytes=0-3"},
)
assert replay_response.status_code == 206
assert replay_response.content == b"RRF2"
assert replay_response.headers["etag"] == f'"{replay_artifact.sha256}"'
stale_replay = client.get(
replay_endpoint,
params={"generation": "f" * 64},
)
assert stale_replay.status_code == 412
def test_route_playback_chunk_descriptor_seals_only_requested_binary_window() -> None:
points = np.arange(18, dtype="<f4").reshape(6, 3)
descriptor = _canonical_route_playback_chunk_descriptor(
"/api/v1/laboratory/vegetation-shadow",
f"lab-v1-vegetation-shadow-{'a' * 64}",
memoryview(points).cast("B"),
(0, 1, 1, 3, 6),
0,
)
assert descriptor is not None
assert descriptor["start"] == 0
assert descriptor["count"] == 4
assert descriptor["point_count"] == 6
assert descriptor["bytes"] == points.nbytes
assert descriptor["shape"] == [6, 3]
assert len(str(descriptor["sha256"])) == 64
def test_route_tgs_anchor_payload_preserves_metric_evidence(tmp_path: Path) -> None:
path = tmp_path / "tgs-evidence.npz"
point_counts = np.arange(1, 11, dtype=np.int64)
offsets = np.concatenate(([0], np.cumsum(point_counts)))
points = np.arange(int(offsets[-1]) * 3, dtype=np.float32).reshape(-1, 3)
centers = np.arange(2244 * 2, dtype=np.float32).reshape(2244, 2) * 0.45
states = np.tile(np.arange(2244, dtype=np.uint16) % 4, (10, 1)).astype(np.uint8)
z_bounds = np.zeros((10, 2244, 2), dtype=np.float32)
z_bounds[..., 0] = np.nan
z_bounds[..., 1] = 1.25
np.savez(
path,
source_frame_indices=np.array(
[20, 408, 789, 1189, 1609, 1992, 2380, 3190, 4810, 6381],
dtype=np.int64,
),
current_increment_point_offsets=offsets,
current_increment_points_xyz_m=points,
costmap_cell_centers_xy_m=centers,
causal_rolling_1s_costmap_states=states,
causal_rolling_1s_costmap_z_bounds_m=z_bounds,
)
payload = _route_tgs_anchor_payload(path, 409)
assert payload["schema_version"] == "missioncore.lab-v1-route-tgs-anchor/v1"
assert payload["source_sequence"] == 409
assert payload["slot"] == 1
assert len(payload["current_points_xyz_m"]) == 2
assert len(payload["costmap"]["centers_xy_m"]) == 2244
assert set(payload["costmap"]["state_codes"]) == {0, 1, 2, 3}
assert payload["costmap"]["z_bounds_m"][0] == [None, 1.25]
def test_coarse_policy_masks_mark_every_outside_fov_pixel_undefined(
tmp_path: Path,
monkeypatch,
Generated
+6 -6
View File
@@ -355,7 +355,7 @@ requires-dist = [
{ name = "paho-mqtt", specifier = ">=2.1,<3" },
{ name = "pillow", specifier = ">=12,<13" },
{ name = "pyyaml", specifier = ">=6.0,<7" },
{ name = "rerun-sdk", specifier = "==0.34.1" },
{ name = "rerun-sdk", specifier = "==0.36.3" },
{ name = "rich", specifier = ">=13.9,<15" },
{ name = "typer", specifier = ">=0.15,<1" },
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.35,<1" },
@@ -618,7 +618,7 @@ wheels = [
[[package]]
name = "rerun-sdk"
version = "0.34.1"
version = "0.36.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
@@ -629,10 +629,10 @@ dependencies = [
{ name = "typing-extensions" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/63/37/41422eca73b5f933872ad073d17034f47ccac730fc4a29b9064d73c74424/rerun_sdk-0.34.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:32d3fb3e9f46eb84433427dc6add6ef7508bbd36295dcc80d693dccacf936e6b", size = 133584987, upload-time = "2026-07-07T17:43:14.934Z" },
{ url = "https://files.pythonhosted.org/packages/24/f7/c2e5097f0138a3c4d70e2fe1c24781fac9aec351324c2ea5b15f4d6971b8/rerun_sdk-0.34.1-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:57585f9c77af6d8a5ee0342bd79b87431b632bf7158e9b8dd4756d6ccd7b7feb", size = 142889929, upload-time = "2026-07-07T17:43:20.293Z" },
{ url = "https://files.pythonhosted.org/packages/18/fa/f87899a8c0cc36901d32069072340b894654fa6f3b3a2a74913565c3661a/rerun_sdk-0.34.1-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:60c35adb49f04bd64bf1a426b59f61aeee8312fa7cc2624878b43186e817b2d4", size = 147505605, upload-time = "2026-07-07T17:43:25.397Z" },
{ url = "https://files.pythonhosted.org/packages/f1/3a/07125af48ef024c573acadf1bfedd406bcc838ffa1af0d7b20bd79895267/rerun_sdk-0.34.1-cp310-abi3-win_amd64.whl", hash = "sha256:3448b34d385994a32caaa4e3ffd42b69069ab7791382d8936089e4928ca24cd6", size = 126554967, upload-time = "2026-07-07T17:43:30.61Z" },
{ url = "https://files.pythonhosted.org/packages/43/ff/e8ce81a4fa451f232abb10f0737c5fa8c812a4b2979bda2f655c58c478eb/rerun_sdk-0.36.3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:804067d6e71747f8461104988be82543b318b583a2837935558a5e79a5e9befd", size = 147513390, upload-time = "2026-08-24T18:59:12.502Z" },
{ url = "https://files.pythonhosted.org/packages/47/0d/a44bccfa279d043929d595a97c220ce3151dd2ef39d6b0477abfa6abc5c0/rerun_sdk-0.36.3-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:954d8980e15e71245ac91ba7120bbaeb624df2bd7a47bbbb8856fd45174d2521", size = 157793360, upload-time = "2026-08-24T18:59:20.494Z" },
{ url = "https://files.pythonhosted.org/packages/54/30/3dae429550a667bc0536bd419864c8574de908557aaadcc136082cb6f17c/rerun_sdk-0.36.3-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:287059b7154bf3881f5b32035f5772d0556d55a0a894650fb74a2605fb39afbe", size = 163018185, upload-time = "2026-08-24T18:59:30.55Z" },
{ url = "https://files.pythonhosted.org/packages/35/97/3ea34c7e892474d940759fe18045fe29e245ac5c5b82e526088c388ba854/rerun_sdk-0.36.3-cp310-abi3-win_amd64.whl", hash = "sha256:f84441a3e1d1d679f6fe5a319cac86ccc3992f8f88bfa72517f76d8b45bf066c", size = 140659012, upload-time = "2026-08-24T18:59:37.699Z" },
]
[[package]]