diff --git a/apps/control-station/src/App.tsx b/apps/control-station/src/App.tsx index a7bc5fb..04da62a 100644 --- a/apps/control-station/src/App.tsx +++ b/apps/control-station/src/App.tsx @@ -315,6 +315,8 @@ export default function App() { const observationFullscreenActive = Boolean( activeDefinition?.kind === "spatial" ? observationLayout.maximizedFloatingSourceId || focusedObservationSource?.modality === "point-cloud" + : activeDefinition?.kind === "lab-archive" + ? observationLayout.maximizedFloatingSourceId || focusedObservationSource?.modality === "point-cloud" : activeDefinition?.kind === "cameras" && focusedObservationSource, ); diff --git a/apps/control-station/src/styles/workspaces.css b/apps/control-station/src/styles/workspaces.css index d58b18b..ee6d16d 100644 --- a/apps/control-station/src/styles/workspaces.css +++ b/apps/control-station/src/styles/workspaces.css @@ -2385,7 +2385,8 @@ .recordings-workspace, .lab-archive-workspace, -.lab-result-surface { +.lab-result-surface, +.laboratory-work-template { display: grid; gap: 0.85rem; } @@ -2542,12 +2543,52 @@ white-space: nowrap; } -.lab-result-surface { +.laboratory-evidence-frame { + position: relative; + min-width: 0; + min-height: 0; + overflow: hidden; + border-radius: 1rem; +} + +.lab-result-surface[data-resizable="true"] .laboratory-evidence-frame { + height: clamp(42rem, 68vh, 58rem); + min-height: 42rem; + resize: vertical; +} + +.laboratory-evidence-frame > .spatial-workspace { + height: 100%; min-height: 0; } -.lab-result-surface .spatial-workspace { - min-height: 34rem; +.laboratory-evidence-frame > .lidar-quality-workspace { + min-height: 42rem; +} + +.lab-archive-workspace[data-viewer-focused="true"] { + height: 100%; + min-height: 0; + padding: 0; +} + +.lab-archive-workspace[data-viewer-focused="true"] > .laboratory-selector, +.lab-archive-workspace[data-viewer-focused="true"] + .laboratory-work-template > :not(.lab-result-surface), +.lab-archive-workspace[data-viewer-focused="true"] .lab-result-surface > header { + display: none; +} + +.lab-archive-workspace[data-viewer-focused="true"] > .laboratory-work-output, +.lab-archive-workspace[data-viewer-focused="true"] .laboratory-work-template, +.lab-archive-workspace[data-viewer-focused="true"] .lab-result-surface, +.lab-archive-workspace[data-viewer-focused="true"] .laboratory-evidence-frame { + height: 100%; + min-height: 0; +} + +.lab-archive-workspace[data-viewer-focused="true"] .laboratory-evidence-frame { + resize: none; } .laboratory-result-pending { diff --git a/apps/control-station/src/workspaces/Workspaces.tsx b/apps/control-station/src/workspaces/Workspaces.tsx index 3c5fc37..5bc1318 100644 --- a/apps/control-station/src/workspaces/Workspaces.tsx +++ b/apps/control-station/src/workspaces/Workspaces.tsx @@ -5,6 +5,7 @@ import { useRef, useState, type ComponentType, + type ReactNode, } from "react"; import { Button, @@ -1281,6 +1282,54 @@ function LaboratoryTask({ ); } +function LaboratoryEvidence({ + eyebrow, + title, + resizable = false, + children, +}: { + eyebrow: string; + title: string; + resizable?: boolean; + children: ReactNode; +}) { + return ( +
+
+ {eyebrow} + {title} +
+
+ {children} +
+
+ ); +} + +function LaboratoryWorkTemplate({ + task, + evidence, + result = null, + details = null, +}: { + task: ReactNode; + evidence: ReactNode; + result?: ReactNode; + details?: ReactNode; +}) { + return ( +
+ {task} + {evidence} + {result} + {details} +
+ ); +} + function formatSeconds(value: number): string { return `${value.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} с`; } @@ -1334,8 +1383,9 @@ function E29LaboratoryResult({ }, [result.resultId, selectedFrameIndex]); return ( - <> - - -
-
- ИСХОДНЫЕ ДАННЫЕ - LiDAR, траектория и камера RAVNOVES00 -
+ /> + )} + evidence={( + {replayReady ? ( ) : ( @@ -1374,9 +1425,10 @@ function E29LaboratoryResult({

)} -
- -
+ + )} + result={( +
РЕЗУЛЬТАТ И ВЫВОД @@ -1409,9 +1461,10 @@ function E29LaboratoryResult({

{result.decision.nextGate}

-
- -
+
+ )} + details={( +
КАДРЫ С КОНФЛИКТОМ @@ -1475,8 +1528,9 @@ function E29LaboratoryResult({
)} -
- +
+ )} + /> ); } @@ -1524,8 +1578,9 @@ function PublishedLaboratoryResult({ const replayReady = props.recordedReplay?.sessionId === session.id; return ( - <> - - -
-
- ВИЗУАЛЬНОЕ ДОКАЗАТЕЛЬСТВО - Исходная запись выбранной лабораторной работы -
+ /> + )} + evidence={( + {replayReady ? ( ) : ( @@ -1555,9 +1611,10 @@ function PublishedLaboratoryResult({

)} -
- -
+ + )} + result={( +
ЗАФИКСИРОВАННЫЕ МЕТРИКИ @@ -1584,8 +1641,9 @@ function PublishedLaboratoryResult({
)} -
- +
+ )} + /> ); } @@ -1792,8 +1850,16 @@ function LabArchiveWorkspace(props: WorkspaceRendererProps) { ); } + const viewerFocused = Boolean( + props.observationLayout.focusedSourceId + || props.observationLayout.maximizedFloatingSourceId, + ); + return ( -
+
profile.id === profileId)?.label ?? rigLabel} @@ -1817,8 +1883,9 @@ function LabArchiveWorkspace(props: WorkspaceRendererProps) {
{workId === "e28-local-surface" ? ( - <> - - props.navigation.openView("spatial-scene")} - /> - + /> + )} + evidence={( + + props.navigation.openView("spatial-scene")} + /> + + )} + /> ) : workId === "e29-camera-geometry" && e29Result && e29SourceSession ? ( { + server = await createServer({ + appType: "custom", + logLevel: "silent", + server: { middlewareMode: true }, + }); + productModel = await server.ssrLoadModule("/src/productModel.ts"); +}); + +after(async () => { + await server?.close(); +}); + +test("top navigation has no Center and Park owns contour health first", () => { + assert.deepEqual( + productModel.roots.map((root) => root.id), + ["fleet", "observation", "missions", "data", "system", "polygon"], + ); + assert.equal(productModel.workspacesForRoot("fleet")[0]?.id, "contour-health"); + assert.equal(productModel.workspaceById("contour-health")?.root, "fleet"); +}); + +test("every laboratory result uses the shared evidence template", async () => { + const source = await readFile( + new URL("../src/workspaces/Workspaces.tsx", import.meta.url), + "utf8", + ); + const css = await readFile( + new URL("../src/styles/workspaces.css", import.meta.url), + "utf8", + ); + const app = await readFile( + new URL("../src/App.tsx", import.meta.url), + "utf8", + ); + + assert.match(source, /function LaboratoryWorkTemplate\(/); + assert.match(source, /function LaboratoryEvidence\(/); + assert.match(source, /data-viewer-focused=/); + assert.match(css, /height:\s*clamp\(42rem,\s*68vh,\s*58rem\)/); + assert.match(css, /resize:\s*vertical/); + assert.match(app, /activeDefinition\?\.kind === "lab-archive"/); +});