feat(perception): add E34 temporal occupied layer

This commit is contained in:
DCCONSTRUCTIONS
2026-07-27 16:55:40 +03:00
parent 95c6540691
commit 621084fcd6
22 changed files with 4732 additions and 104 deletions
@@ -0,0 +1,146 @@
import type { ComponentType } from "react";
import type { LaboratoryOption } from "../../components/laboratory/LaboratoryPresentation";
import type { AdvancedLaboratoryResults } from "../../core/laboratory/advancedResults";
import type { ObservationSessionSummary } from "../../core/observation/sessionArchive";
import type { WorkspaceRendererProps } from "../contracts";
import { E31Result } from "./E31Result";
import { E32Result } from "./E32Result";
import { E33Result } from "./E33Result";
import { E34Result } from "./E34Result";
import { RecordedReplayEvidence } from "./RecordedReplayEvidence";
export type AdvancedLaboratoryWorkId =
| "e31-source-binding"
| "e32-track-geometry"
| "e33-worker-shadow"
| "e34-temporal-layer";
type LaboratoryWorkspaceProps = WorkspaceRendererProps & {
SpatialView: ComponentType<WorkspaceRendererProps>;
};
export function isAdvancedLaboratoryWorkId(
value: string,
): value is AdvancedLaboratoryWorkId {
return (
value === "e31-source-binding"
|| value === "e32-track-geometry"
|| value === "e33-worker-shadow"
|| value === "e34-temporal-layer"
);
}
export function advancedLaboratoryWorkOptions(
results: AdvancedLaboratoryResults,
sourceSessions: ReadonlyMap<string, ObservationSessionSummary>,
): readonly LaboratoryOption<AdvancedLaboratoryWorkId>[] {
const options: LaboratoryOption<AdvancedLaboratoryWorkId>[] = [];
if (results.e31 && sourceSessions.has(results.e31.sourceSessionId)) {
options.push({
id: "e31-source-binding",
label: "LAB E31 · source binding",
});
}
if (results.e32 && sourceSessions.has(results.e32.sourceSessionId)) {
options.push({
id: "e32-track-geometry",
label: "LAB E32 · TrackGeometry v1",
});
}
if (results.e33 && sourceSessions.has(results.e33.sourceSessionId)) {
options.push({
id: "e33-worker-shadow",
label: "LAB E33 · worker shadow 1×",
});
}
if (results.e34) {
options.push({
id: "e34-temporal-layer",
label: "LAB E34 · temporal occupied/unknown",
});
}
return options;
}
export function advancedLaboratorySourceSession(
workId: AdvancedLaboratoryWorkId,
results: AdvancedLaboratoryResults,
sourceSessions: ReadonlyMap<string, ObservationSessionSummary>,
): ObservationSessionSummary | null {
const sourceSessionId = workId === "e31-source-binding"
? results.e31?.sourceSessionId
: workId === "e32-track-geometry"
? results.e32?.sourceSessionId
: workId === "e33-worker-shadow"
? results.e33?.sourceSessionId
: null;
return sourceSessionId ? sourceSessions.get(sourceSessionId) ?? null : null;
}
export function AdvancedLaboratoryResult({
props,
rigLabel,
workId,
results,
sourceSessions,
replayingSessionId,
failedSessionId,
replayError,
}: {
props: LaboratoryWorkspaceProps;
rigLabel: string;
workId: AdvancedLaboratoryWorkId;
results: AdvancedLaboratoryResults;
sourceSessions: ReadonlyMap<string, ObservationSessionSummary>;
replayingSessionId: string | null;
failedSessionId: string | null;
replayError: string | null;
}) {
if (workId === "e34-temporal-layer" && results.e34) {
return <E34Result rigLabel={rigLabel} result={results.e34} />;
}
const sourceSession = advancedLaboratorySourceSession(
workId,
results,
sourceSessions,
);
if (!sourceSession) return null;
const evidence = (
<RecordedReplayEvidence
props={props}
sourceSession={sourceSession}
loading={replayingSessionId === sourceSession.id}
error={failedSessionId === sourceSession.id ? replayError : null}
/>
);
if (workId === "e31-source-binding" && results.e31) {
return (
<E31Result
rigLabel={rigLabel}
result={results.e31}
evidence={evidence}
/>
);
}
if (workId === "e32-track-geometry" && results.e32) {
return (
<E32Result
rigLabel={rigLabel}
result={results.e32}
evidence={evidence}
/>
);
}
if (workId === "e33-worker-shadow" && results.e33) {
return (
<E33Result
rigLabel={rigLabel}
result={results.e33}
evidence={evidence}
/>
);
}
return null;
}
@@ -0,0 +1,236 @@
import { useMemo, useState } from "react";
import { Select } from "@nodedc/ui-react";
import {
LaboratoryEvidence,
LaboratoryResultSummary,
LaboratorySummary,
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import type {
E34TemporalLayerResult,
E34TemporalReviewFrame,
} from "../../core/laboratory/e34TemporalLayer";
import { formatNumber } from "../../presentation";
import {
E34TemporalLayerScene,
type E34TemporalViewMode,
} from "./E34TemporalLayerScene";
function percent(value: number): string {
return `${(value * 100).toLocaleString("ru-RU", {
maximumFractionDigits: 1,
})}%`;
}
function defaultFrame(
frames: readonly E34TemporalReviewFrame[],
): E34TemporalReviewFrame | null {
return frames.find((frame) => (
frame.counts.current > 0 && frame.counts.held > 0
)) ?? frames.find((frame) => frame.counts.expired > 0) ?? frames[0] ?? null;
}
function frameLabel(frame: E34TemporalReviewFrame): string {
const state = frame.counts.expired
? `истекло ${frame.counts.expired}`
: frame.counts.held
? `удерживается ${frame.counts.held}`
: `наблюдается ${frame.counts.current}`;
return `Кадр ${formatNumber(frame.frameIndex, 0)} · ${state}`;
}
function E34Evidence({
result,
}: {
result: E34TemporalLayerResult;
}) {
const initial = useMemo(
() => defaultFrame(result.reviewFrames),
[result.reviewFrames],
);
const [frameIndex, setFrameIndex] = useState(initial?.frameIndex ?? 0);
const [mode, setMode] = useState<E34TemporalViewMode>("3d");
const [expanded, setExpanded] = useState(false);
const frame = result.reviewFrames.find(
(item) => item.frameIndex === frameIndex,
) ?? initial;
if (!frame) {
return (
<div className="laboratory-result-pending" role="status">
Контрольные состояния временного слоя не опубликованы.
</div>
);
}
return (
<div className="e34-temporal-evidence">
<LaboratoryEvidenceViewer
label="Временной occupied/unknown слой E34"
mode={mode}
modes={[
{ value: "3d", label: "3D" },
{ value: "plan", label: "План" },
]}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
actions={(
<Select
label="Контрольное состояние E34"
value={String(frame.frameIndex)}
options={result.reviewFrames.map((item) => ({
value: String(item.frameIndex),
label: frameLabel(item),
}))}
variant="split"
menuWidth="anchor"
onChange={(value) => setFrameIndex(Number(value))}
/>
)}
overlay={(
<dl className="e34-temporal-evidence__telemetry">
<div>
<dt>Кадр / время</dt>
<dd>
{formatNumber(frame.frameIndex, 0)}
{" · "}
{frame.sessionSeconds.toLocaleString("ru-RU", {
maximumFractionDigits: 3,
})}
{" с"}
</dd>
</div>
<div>
<dt>Вход</dt>
<dd>{frame.sourceAvailable ? "Есть текущие точки" : "Текущих точек нет"}</dd>
</div>
<div>
<dt>Состояние слоя</dt>
<dd>
{frame.counts.current} current · {frame.counts.held} held · {frame.counts.expired} expired
</dd>
</div>
</dl>
)}
>
<E34TemporalLayerScene frame={frame} mode={mode} />
</LaboratoryEvidenceViewer>
</div>
);
}
export function E34Result({
rigLabel,
result,
}: {
rigLabel: string;
result: E34TemporalLayerResult;
}) {
const metrics = result.metrics;
const config = result.configuration;
return (
<LaboratoryWorkTemplate
summary={(
<LaboratorySummary
title="LAB E34 · короткоживущий occupied/unknown слой"
description="Проверяли, может ли TrackGeometry поддерживать ограниченную во времени карту занятости: подтверждённые точки публикуются как occupied, исчезнувшее наблюдение кратко удерживается как unknown и затем удаляется строго по TTL."
status="Временной слой принят"
statusTone="success"
facts={[
{ label: "Источник", value: `${rigLabel} · TrackGeometry E32` },
{
label: "Объём проверки",
value: `${formatNumber(metrics.processedFrames, 0)} / ${formatNumber(metrics.sourceFrames, 0)} кадров`,
},
{
label: "Параметры слоя",
value: `${config.voxelSizeM.toLocaleString("ru-RU")} м · TTL ${config.occupiedTtlSeconds.toLocaleString("ru-RU")} с`,
},
{ label: "Полномочия", value: "Диагностика · команды и safety выключены" },
]}
brief={{
question: "Можно ли сохранить краткую пространственную непрерывность объектов между наблюдениями, не объявляя ненаблюдаемое пространство свободным и не загрязняя постоянную карту?",
approach: "Полная запись E32 воспроизведена через hit-only voxel layer в map-frame. Camera-track связывались только по точной идентичности, geometry-only компоненты — по ограниченному пространственно-временному сопоставлению. Первый immutable replay был отклонён; после исправления логических часов TTL и детектора глобального сдвига тот же замороженный профиль выполнен повторно.",
principalResult: `Да, в диагностическом контуре. Обработаны все ${formatNumber(metrics.processedFrames, 0)} кадров и все ${formatNumber(metrics.e34ConsumedCurrentPointRows, 0)} текущих точек; непрерывность наблюдений составила ${percent(metrics.continuityFraction)}, задержка логического истечения — ${metrics.maximumExpiryDelaySeconds.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} с.`,
limitation: "Слой не вычисляет свободное пространство, не назначает dynamic/static, не меняет persistent reconstruction и не имеет навигационных или safety-полномочий. Непрерывность — диагностическая метрика, не ground truth.",
}}
method={{
completeness: "complete",
executionClass: "deterministic",
pipelineId: "TrackGeometry → short-TTL hit-only occupied/unknown layer",
components: [
{
kind: "source",
name: "Принятый TrackGeometry из LAB E32",
version: `${formatNumber(metrics.e32CurrentPointRows, 0)} квалифицированных точек`,
role: "неизменяемый map-frame вход с явной принадлежностью точек",
identitySha256: null,
},
{
kind: "algorithm",
name: "Bounded temporal component association",
version: `voxel ${config.voxelSizeM} м · TTL ${config.occupiedTtlSeconds} с`,
role: "точное camera-связывание, geometry-only reassociation, hold и независимое expiry",
identitySha256: null,
},
{
kind: "runtime",
name: "Fail-closed diagnostic replay",
version: `${metrics.frameProcessingP95Ms.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} мс/frame p95`,
role: "полный учёт кадров, bounds, контроль map-frame и неизменность upstream",
identitySha256: null,
},
],
}}
/>
)}
evidence={(
<LaboratoryEvidence
eyebrow="ВРЕМЕННОЕ ДОКАЗАТЕЛЬСТВО"
title="Контрольные состояния: наблюдение, удержание unknown и точное истечение"
kind="diagnostic-model"
resizable
>
<E34Evidence result={result} />
</LaboratoryEvidence>
)}
result={(
<LaboratoryResultSummary
title="Краткая непрерывность доказана; свободное пространство намеренно не выводится"
status="12 / 12 gate"
statusTone="success"
metrics={[
{
label: "Полный replay",
value: `${formatNumber(metrics.processedFrames, 0)} / ${formatNumber(metrics.sourceFrames, 0)}`,
hint: "все входные точки учтены",
},
{
label: "Связность компонентов",
value: percent(metrics.continuityFraction),
hint: `${formatNumber(metrics.exactCameraAssociations + metrics.geometrySpatialReassociations, 0)} повторных связей`,
},
{
label: "TTL / истечение",
value: `${metrics.maximumHeldAgeSeconds.toLocaleString("ru-RU", { maximumFractionDigits: 2 })} с`,
hint: `${metrics.maximumExpiryDelaySeconds.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} с задержка`,
},
{
label: "Ограниченность",
value: `${formatNumber(metrics.peakActiveComponents, 0)} / ${formatNumber(config.maximumActiveComponents, 0)}`,
hint: `${formatNumber(metrics.peakCellsPerComponent, 0)} / ${formatNumber(config.maximumCellsPerComponent, 0)} ячеек`,
},
]}
conclusion={{
proved: "На данной полной записи hit-backed компоненты можно детерминированно удерживать до 0,75 с и удалять по независимому deadline, сохраняя bounds, полный учёт и неизменность E32/E33.",
notProved: "Работа не доказывает свободное пространство, динамический класс, качество планирования, переносимость порогов на другой сенсорный риг или корректность при реальном глобальном скачке map-frame.",
decision: "E34 принимается как read-only диагностический слой. Следующий критический gate — E35: детерминированно проверить деградацию и восстановление при потере источника, нарушении сроков и map-frame fault.",
}}
/>
)}
/>
);
}
@@ -0,0 +1,363 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { Button, Icon } from "@nodedc/ui-react";
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import type {
E34Point3,
E34TemporalComponent,
E34TemporalReviewFrame,
} from "../../core/laboratory/e34TemporalLayer";
export type E34TemporalViewMode = "3d" | "plan";
function tokenColor(
host: HTMLElement,
token: string,
fallback: readonly [number, number, number],
): THREE.Color {
const value = getComputedStyle(host).getPropertyValue(token).trim();
if (value.startsWith("#")) {
return new THREE.Color(value);
}
const channels = value.match(/[\d.]+/g)?.slice(0, 3).map(Number);
const [red, green, blue] = channels?.length === 3
? channels
: fallback;
return new THREE.Color(red / 255, green / 255, blue / 255);
}
function disposeRenderable(object: THREE.Object3D): void {
const renderable = object as THREE.Object3D & {
geometry?: THREE.BufferGeometry;
material?: THREE.Material | THREE.Material[];
};
renderable.geometry?.dispose();
const materials = Array.isArray(renderable.material)
? renderable.material
: renderable.material
? [renderable.material]
: [];
materials.forEach((material) => material.dispose());
}
function scenePoint(
point: E34Point3,
origin: E34Point3,
): readonly [number, number, number] {
return [
point[0] - origin[0],
point[2] - origin[2],
-(point[1] - origin[1]),
];
}
function positions(
points: readonly E34Point3[],
origin: E34Point3,
): Float32Array {
const result = new Float32Array(points.length * 3);
points.forEach((point, index) => {
const [x, y, z] = scenePoint(point, origin);
const offset = index * 3;
result[offset] = x;
result[offset + 1] = y;
result[offset + 2] = z;
});
return result;
}
function median(values: readonly number[]): number {
if (!values.length) return 0;
const sorted = [...values].sort((left, right) => left - right);
return sorted[Math.floor(sorted.length / 2)] ?? 0;
}
function frameOrigin(frame: E34TemporalReviewFrame): E34Point3 {
const anchors = frame.components.length
? frame.components.map((component) => component.centroidMapXyzM)
: frame.cellCentersMapXyzM;
return [
median(anchors.map((point) => point[0])),
median(anchors.map((point) => point[1])),
median(anchors.map((point) => point[2])),
];
}
function componentColor(
host: HTMLElement,
component: E34TemporalComponent,
): THREE.Color {
if (component.state === "expired") {
return tokenColor(host, "--nodedc-danger-rgb", [255, 104, 112]);
}
if (component.state === "held") {
return tokenColor(host, "--nodedc-warning-rgb", [255, 209, 102]);
}
return tokenColor(host, "--nodedc-accent-rgb", [247, 248, 244]);
}
function fitRadius(frame: E34TemporalReviewFrame, origin: E34Point3): number {
const points = [
...frame.cellCentersMapXyzM,
...frame.components.map((component) => component.centroidMapXyzM),
];
if (!points.length) return 4;
const distances = points
.map((point) => {
const [x, y, z] = scenePoint(point, origin);
return Math.hypot(x, y, z);
})
.sort((left, right) => left - right);
const p95 = distances[Math.floor((distances.length - 1) * 0.95)] ?? 4;
return THREE.MathUtils.clamp(p95, 3, 32);
}
export function E34TemporalLayerScene({
frame,
mode,
}: {
frame: E34TemporalReviewFrame;
mode: E34TemporalViewMode;
}) {
const hostRef = useRef<HTMLDivElement | null>(null);
const sceneRef = useRef<THREE.Scene | null>(null);
const cameraRef = useRef<THREE.PerspectiveCamera | null>(null);
const controlsRef = useRef<OrbitControls | null>(null);
const contentRef = useRef<THREE.Group | null>(null);
const viewRadiusRef = useRef(5);
const [renderError, setRenderError] = useState<string | null>(null);
const origin = useMemo(() => frameOrigin(frame), [frame]);
useEffect(() => {
const host = hostRef.current;
if (!host) return;
let renderer: THREE.WebGLRenderer;
try {
renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: false,
powerPreference: "high-performance",
});
} catch {
setRenderError("Браузер не смог создать 3D-сцену временного слоя.");
return;
}
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.setClearColor(
tokenColor(host, "--nodedc-canvas", [5, 5, 6]),
1,
);
renderer.domElement.setAttribute(
"aria-label",
"Интерактивная 3D-сцена временного occupied/unknown слоя E34",
);
renderer.domElement.setAttribute("role", "img");
host.prepend(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(48, 1, 0.01, 500);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.08;
controls.enablePan = true;
controls.enableZoom = true;
controls.screenSpacePanning = true;
controls.minDistance = 0.5;
controls.maxDistance = 160;
controls.target.set(0, 0, 0);
const content = new THREE.Group();
scene.add(content);
sceneRef.current = scene;
cameraRef.current = camera;
controlsRef.current = controls;
contentRef.current = content;
const resize = () => {
const width = Math.max(host.clientWidth, 1);
const height = Math.max(host.clientHeight, 1);
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height, false);
};
const observer = new ResizeObserver(resize);
observer.observe(host);
resize();
let animationFrame = 0;
const render = () => {
animationFrame = window.requestAnimationFrame(render);
controls.update();
renderer.render(scene, camera);
};
render();
return () => {
window.cancelAnimationFrame(animationFrame);
observer.disconnect();
controls.dispose();
scene.traverse(disposeRenderable);
renderer.dispose();
renderer.domElement.remove();
sceneRef.current = null;
cameraRef.current = null;
controlsRef.current = null;
contentRef.current = null;
};
}, []);
useEffect(() => {
const host = hostRef.current;
const content = contentRef.current;
if (!host || !content) return;
while (content.children.length) {
const child = content.children[0];
if (!child) continue;
content.remove(child);
child.traverse(disposeRenderable);
}
const contextGeometry = new THREE.BufferGeometry();
contextGeometry.setAttribute(
"position",
new THREE.BufferAttribute(
positions(frame.cellCentersMapXyzM, origin),
3,
),
);
const context = new THREE.Points(
contextGeometry,
new THREE.PointsMaterial({
color: tokenColor(host, "--nodedc-text-muted", [147, 151, 159]),
size: 2.4,
sizeAttenuation: false,
transparent: true,
opacity: 0.32,
depthWrite: false,
}),
);
content.add(context);
for (const component of frame.components) {
const color = componentColor(host, component);
const markerGeometry = new THREE.BufferGeometry();
markerGeometry.setAttribute(
"position",
new THREE.BufferAttribute(
positions([component.centroidMapXyzM], origin),
3,
),
);
const marker = new THREE.Points(
markerGeometry,
new THREE.PointsMaterial({
color,
size: component.state === "expired" ? 8 : 6,
sizeAttenuation: false,
transparent: true,
opacity: component.state === "expired" ? 0.72 : 1,
depthWrite: false,
}),
);
content.add(marker);
if (component.history.length > 1) {
const trailGeometry = new THREE.BufferGeometry();
trailGeometry.setAttribute(
"position",
new THREE.BufferAttribute(
positions(
component.history.map((item) => item.centroidMapXyzM),
origin,
),
3,
),
);
content.add(new THREE.Line(
trailGeometry,
new THREE.LineBasicMaterial({
color,
transparent: true,
opacity: component.state === "expired" ? 0.28 : 0.54,
}),
));
}
}
const radius = fitRadius(frame, origin);
viewRadiusRef.current = radius;
const grid = new THREE.GridHelper(
radius * 2.4,
20,
tokenColor(host, "--nodedc-text-muted", [96, 99, 106]),
tokenColor(host, "--nodedc-glass-outline", [48, 50, 56]),
);
const materials = Array.isArray(grid.material)
? grid.material
: [grid.material];
materials.forEach((material) => {
material.transparent = true;
material.opacity = 0.15;
material.depthWrite = false;
});
content.add(grid);
}, [frame, origin]);
const resetView = () => {
const camera = cameraRef.current;
const controls = controlsRef.current;
if (!camera || !controls) return;
const radius = viewRadiusRef.current;
controls.target.set(0, 0, 0);
if (mode === "plan") {
camera.position.set(0, radius * 2.8, 0.001);
camera.up.set(0, 0, -1);
} else {
camera.position.set(radius * 1.35, radius * 0.9, radius * 1.35);
camera.up.set(0, 1, 0);
}
camera.near = Math.max(radius / 2_000, 0.005);
camera.far = Math.max(radius * 20, 120);
camera.updateProjectionMatrix();
controls.maxDistance = Math.max(radius * 8, 40);
controls.update();
};
useEffect(resetView, [frame, mode]);
return (
<div className="e34-temporal-scene">
<div ref={hostRef} className="e34-temporal-scene__viewport">
{renderError ? (
<p className="e34-temporal-scene__error">{renderError}</p>
) : null}
</div>
<div className="e34-temporal-scene__toolbar">
<Button
variant="secondary"
size="compact"
icon={<Icon name="refresh" size={14} />}
onClick={resetView}
>
Сбросить ракурс
</Button>
<div className="e34-temporal-scene__gestures">
<span>ЛКМ · вращение</span>
<span>Колесо · масштаб</span>
<span>ПКМ · панорама</span>
</div>
</div>
<div className="e34-temporal-scene__legend">
<span data-state="cells">
Ячейки · {frame.cellCentersMapXyzM.length}
</span>
<span data-state="current">Наблюдается · {frame.counts.current}</span>
<span data-state="held">Удерживается · {frame.counts.held}</span>
<span data-state="expired">Истекло · {frame.counts.expired}</span>
</div>
</div>
);
}
@@ -29,9 +29,7 @@ import {
} from "../../core/laboratory/e30Review";
import {
fetchAdvancedLaboratoryResults,
type E31LaboratoryResult,
type E32LaboratoryResult,
type E33LaboratoryResult,
type AdvancedLaboratoryResults,
} from "../../core/laboratory/advancedResults";
import {
fetchLidarLocalSurfaces,
@@ -41,14 +39,17 @@ import { formatNumber } from "../../presentation";
import { E30ReviewWorkspace } from "../E30ReviewWorkspace";
import { LidarQualityWorkspace } from "../LidarQualityWorkspace";
import type { WorkspaceRendererProps } from "../contracts";
import { E31Result } from "./E31Result";
import { E32Result } from "./E32Result";
import { E33Result } from "./E33Result";
import {
AdvancedLaboratoryResult,
advancedLaboratorySourceSession,
advancedLaboratoryWorkOptions,
isAdvancedLaboratoryWorkId,
type AdvancedLaboratoryWorkId,
} from "./AdvancedLaboratoryResult";
import {
e28LaboratoryBrief, e29LaboratoryBrief,
e30LaboratoryBrief, PUBLISHED_LABORATORY_BRIEF,
} from "./laboratoryArchiveBriefs";
import { RecordedReplayEvidence } from "./RecordedReplayEvidence";
type LaboratoryWorkspaceProps = WorkspaceRendererProps & {
SpatialView: ComponentType<WorkspaceRendererProps>;
};
@@ -58,11 +59,16 @@ type LaboratoryWorkId =
| "e28-local-surface"
| "e29-camera-geometry"
| "e30-evidence-review"
| "e31-source-binding"
| "e32-track-geometry"
| "e33-worker-shadow"
| AdvancedLaboratoryWorkId
| `session:${string}`;
const EMPTY_ADVANCED_RESULTS: AdvancedLaboratoryResults = {
e31: null,
e32: null,
e33: null,
e34: null,
};
function digestFromContentId(value: string | null | undefined): string | null {
const digest = value?.split("-").at(-1) ?? "";
return /^[a-f0-9]{64}$/.test(digest) ? digest : null;
@@ -554,9 +560,9 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
const [e28Model, setE28Model] = useState<LidarLocalSurfaceModel | null>(null);
const [e29Result, setE29Result] = useState<E29EvidenceResult | null>(null);
const [e30Result, setE30Result] = useState<E30ReviewResult | null>(null);
const [e31Result, setE31Result] = useState<E31LaboratoryResult | null>(null);
const [e32Result, setE32Result] = useState<E32LaboratoryResult | null>(null);
const [e33Result, setE33Result] = useState<E33LaboratoryResult | null>(null);
const [advancedResults, setAdvancedResults] = useState(
EMPTY_ADVANCED_RESULTS,
);
const [evidenceLoading, setEvidenceLoading] = useState(true);
const [evidenceError, setEvidenceError] = useState<string | null>(null);
const sessions = useObservationSessions({
@@ -598,14 +604,12 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
setE28Model(nextE28);
setE29Result(nextE29);
setE30Result(nextE30);
setE31Result(nextAdvanced?.e31 ?? null);
setE32Result(nextAdvanced?.e32 ?? null);
setE33Result(nextAdvanced?.e33 ?? null);
setAdvancedResults(nextAdvanced ?? EMPTY_ADVANCED_RESULTS);
const failures = [
e28.status === "rejected" ? "E28" : null,
e29.status === "rejected" ? "E29" : null,
e30.status === "rejected" ? "E30" : null,
advanced.status === "rejected" ? "E31E33" : null,
advanced.status === "rejected" ? "E31E34" : null,
].filter(Boolean);
setEvidenceError(
failures.length
@@ -651,32 +655,16 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
label: "LAB E30 · evidence review A2",
});
}
if (e31Result && sourceSessions.has(e31Result.sourceSessionId)) {
items.push({
id: "e31-source-binding",
label: "LAB E31 · source binding",
});
}
if (e32Result && sourceSessions.has(e32Result.sourceSessionId)) {
items.push({
id: "e32-track-geometry",
label: "LAB E32 · TrackGeometry v1",
});
}
if (e33Result && sourceSessions.has(e33Result.sourceSessionId)) {
items.push({
id: "e33-worker-shadow",
label: "LAB E33 · worker shadow 1×",
});
}
items.push(...advancedLaboratoryWorkOptions(
advancedResults,
sourceSessions,
));
return items;
}, [
advancedResults,
e28Model,
e29Result,
e30Result,
e31Result,
e32Result,
e33Result,
sourceSessions,
]);
const profiles = useMemo(() => {
@@ -714,15 +702,6 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
const e30SourceSession = e30Result
? sourceSessions.get(e30Result.sourceSessionId) ?? null
: null;
const e31SourceSession = e31Result
? sourceSessions.get(e31Result.sourceSessionId) ?? null
: null;
const e32SourceSession = e32Result
? sourceSessions.get(e32Result.sourceSessionId) ?? null
: null;
const e33SourceSession = e33Result
? sourceSessions.get(e33Result.sourceSessionId) ?? null
: null;
useEffect(() => {
if (
@@ -786,13 +765,13 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
void sessions.replay(e30SourceSession.id);
return;
}
const advancedSession = next === "e31-source-binding"
? e31SourceSession
: next === "e32-track-geometry"
? e32SourceSession
: next === "e33-worker-shadow"
? e33SourceSession
: null;
const advancedSession = isAdvancedLaboratoryWorkId(next)
? advancedLaboratorySourceSession(
next,
advancedResults,
sourceSessions,
)
: null;
if (advancedSession) {
void sessions.replay(advancedSession.id);
}
@@ -940,44 +919,16 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
result={e30Result}
sourceSession={e30SourceSession}
/>
) : workId === "e31-source-binding" && e31Result && e31SourceSession ? (
<E31Result
) : isAdvancedLaboratoryWorkId(workId) ? (
<AdvancedLaboratoryResult
props={props}
rigLabel={rigLabel}
result={e31Result}
evidence={(
<RecordedReplayEvidence
props={props}
sourceSession={e31SourceSession}
loading={sessions.replayingSessionId === e31SourceSession.id}
error={sessions.failedSessionId === e31SourceSession.id ? sessions.error : null}
/>
)}
/>
) : workId === "e32-track-geometry" && e32Result && e32SourceSession ? (
<E32Result
rigLabel={rigLabel}
result={e32Result}
evidence={(
<RecordedReplayEvidence
props={props}
sourceSession={e32SourceSession}
loading={sessions.replayingSessionId === e32SourceSession.id}
error={sessions.failedSessionId === e32SourceSession.id ? sessions.error : null}
/>
)}
/>
) : workId === "e33-worker-shadow" && e33Result && e33SourceSession ? (
<E33Result
rigLabel={rigLabel}
result={e33Result}
evidence={(
<RecordedReplayEvidence
props={props}
sourceSession={e33SourceSession}
loading={sessions.replayingSessionId === e33SourceSession.id}
error={sessions.failedSessionId === e33SourceSession.id ? sessions.error : null}
/>
)}
workId={workId}
results={advancedResults}
sourceSessions={sourceSessions}
replayingSessionId={sessions.replayingSessionId}
failedSessionId={sessions.failedSessionId}
replayError={sessions.error}
/>
) : selectedSession ? (
<PublishedLaboratoryResult