From b8fb4ebcba39a8b0ec582ccecb2098d18692dd76 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Thu, 6 Aug 2026 09:14:20 +0300 Subject: [PATCH] fix(lab): align spatial replay controls --- .../src/components/ObservationTimeline.tsx | 22 +++--- .../laboratory/LaboratoryEvidenceViewer.tsx | 3 + .../LaboratoryMetricEvidenceScene.tsx | 72 ++++++++----------- .../styles/l3-pointpillars-visual-audit.css | 8 +-- .../src/styles/m4-replay-threat.css | 49 +++++++------ .../laboratory/M4ReplayThreatVisual.tsx | 45 +++++++++++- .../test/m4ReplayThreat.test.mjs | 6 +- 7 files changed, 119 insertions(+), 86 deletions(-) diff --git a/apps/control-station/src/components/ObservationTimeline.tsx b/apps/control-station/src/components/ObservationTimeline.tsx index d365337..4f2d2e3 100644 --- a/apps/control-station/src/components/ObservationTimeline.tsx +++ b/apps/control-station/src/components/ObservationTimeline.tsx @@ -14,6 +14,7 @@ export function ObservationTimeline({ onSeek, onPlayingChange, onJumpToEnd, + showJumpToEnd = true, playbackRate, onPlaybackRateChange, accumulationSeconds, @@ -32,6 +33,7 @@ export function ObservationTimeline({ onSeek?: (timeNs: number) => void; onPlayingChange?: (playing: boolean) => void; onJumpToEnd?: () => void; + showJumpToEnd?: boolean; playbackRate?: number; onPlaybackRateChange?: (rate: number) => void; accumulationSeconds?: number; @@ -144,15 +146,17 @@ export function ObservationTimeline({ {buffered ? `${sourceCount} каналов` : `${synchronizationLabel} · буфер не включён`} - + {showJumpToEnd ? ( + + ) : null} ); diff --git a/apps/control-station/src/components/laboratory/LaboratoryEvidenceViewer.tsx b/apps/control-station/src/components/laboratory/LaboratoryEvidenceViewer.tsx index 402cb2c..6834a9f 100644 --- a/apps/control-station/src/components/laboratory/LaboratoryEvidenceViewer.tsx +++ b/apps/control-station/src/components/laboratory/LaboratoryEvidenceViewer.tsx @@ -30,6 +30,7 @@ export function LaboratoryEvidenceViewer< secondaryMode, overlay, transport, + trailingActions, children, }: { label: string; @@ -48,6 +49,7 @@ export function LaboratoryEvidenceViewer< }; overlay?: ReactNode; transport?: ReactNode; + trailingActions?: ReactNode; children: ReactNode; }) { const expandButtonRef = useRef(null); @@ -98,6 +100,7 @@ export function LaboratoryEvidenceViewer< label={`${label}: режим представления`} onChange={onModeChange} /> + {trailingActions} void; +} + +export const LaboratoryMetricEvidenceScene = forwardRef< +LaboratoryMetricEvidenceSceneHandle, +{ pointCloudBodyXyzM: readonly LaboratoryMetricPoint3[]; obstacles: readonly LaboratoryMetricObstacleVisual[]; rig: LaboratoryMetricRigVisual; @@ -107,7 +110,20 @@ export function LaboratoryMetricEvidenceScene({ occupiedVoxelSizeM: number; mode: LaboratoryMetricSceneMode; label: string; -}) { + showCurrentIncrement: boolean; + showRollingMap: boolean; +} +>(function LaboratoryMetricEvidenceScene({ + pointCloudBodyXyzM, + obstacles, + rig, + corridor, + occupiedVoxelSizeM, + mode, + label, + showCurrentIncrement, + showRollingMap, +}, ref) { const hostRef = useRef(null); const sceneRef = useRef(null); const cameraRef = useRef(null); @@ -115,8 +131,6 @@ export function LaboratoryMetricEvidenceScene({ const staticContentRef = useRef(null); const dynamicContentRef = useRef(null); const [renderError, setRenderError] = useState(null); - const [showCurrentIncrement, setShowCurrentIncrement] = useState(true); - const [showRollingMap, setShowRollingMap] = useState(true); useEffect(() => { const host = hostRef.current; @@ -370,39 +384,13 @@ export function LaboratoryMetricEvidenceScene({ }; useEffect(resetView, [corridor.forwardLengthM, mode]); + useImperativeHandle(ref, () => ({ resetView })); return (
{renderError ?

{renderError}

: null}
-
- - - - ЛКМ · вращение · колесо · масштаб · ПКМ · панорама -
Угроза Вне коридора @@ -412,4 +400,4 @@ export function LaboratoryMetricEvidenceScene({
); -} +}); diff --git a/apps/control-station/src/styles/l3-pointpillars-visual-audit.css b/apps/control-station/src/styles/l3-pointpillars-visual-audit.css index f97fb61..5bcd2b8 100644 --- a/apps/control-station/src/styles/l3-pointpillars-visual-audit.css +++ b/apps/control-station/src/styles/l3-pointpillars-visual-audit.css @@ -88,15 +88,9 @@ inset: 0; } -.m4-replay-threat-evidence-viewer .laboratory-metric-evidence-scene__toolbar { - top: 5.4rem; - max-width: calc(100% - 1.2rem); - flex-wrap: wrap; -} - .m4-replay-threat-evidence-viewer .laboratory-metric-evidence-scene__legend, .m4-replay-threat-evidence-viewer .m4-replay-threat-visual__overlay { - bottom: 5.3rem; + bottom: 6.2rem; } .m4-replay-threat-visual__timeline { diff --git a/apps/control-station/src/styles/m4-replay-threat.css b/apps/control-station/src/styles/m4-replay-threat.css index c881dbf..fbed7a0 100644 --- a/apps/control-station/src/styles/m4-replay-threat.css +++ b/apps/control-station/src/styles/m4-replay-threat.css @@ -54,6 +54,30 @@ transform: translateX(-50%); } +.m4-replay-threat-evidence-viewer .laboratory-evidence-viewer__controls { + width: calc(100% - 1.2rem); +} + +.m4-replay-threat-evidence-viewer .l3-visual-audit__actions { + justify-content: space-between; +} + +.m4-replay-threat-visual__layer-controls { + margin-left: auto; +} + +.m4-replay-threat-evidence-viewer .laboratory-evidence-viewer__transport { + bottom: 0.3rem; +} + +.m4-replay-threat-visual__timeline { + padding: 0.5rem 0.55rem; +} + +.m4-replay-threat-visual__timeline .observation-timeline__playback { + grid-template-columns: auto auto auto minmax(0, 1fr) auto; +} + .laboratory-metric-evidence-scene__viewport { position: absolute; inset: 0; @@ -81,30 +105,11 @@ font-size: 0.62rem; } -.laboratory-metric-evidence-scene__toolbar { - position: absolute; - z-index: 3; - top: 0.6rem; - left: 0.6rem; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.laboratory-metric-evidence-scene__toolbar .nodedc-button:not([data-variant="primary"]), -.laboratory-metric-evidence-scene__toolbar > span, .laboratory-metric-evidence-scene__legend { background: var(--nodedc-floating-surface); backdrop-filter: blur(var(--nodedc-blur-control)); } -.laboratory-metric-evidence-scene__toolbar > span { - border-radius: var(--nodedc-radius-control-compact); - padding: 0.43rem 0.55rem; - color: var(--nodedc-text-secondary); - font-size: 0.5rem; -} - .laboratory-metric-evidence-scene__legend { position: absolute; z-index: 3; @@ -150,9 +155,3 @@ border: 1px solid rgb(var(--nodedc-accent-rgb)); background: transparent; } - -@media (max-width: 900px) { - .laboratory-metric-evidence-scene__toolbar > span { - display: none; - } -} diff --git a/apps/control-station/src/workspaces/laboratory/M4ReplayThreatVisual.tsx b/apps/control-station/src/workspaces/laboratory/M4ReplayThreatVisual.tsx index 8bc415c..eeb3b24 100644 --- a/apps/control-station/src/workspaces/laboratory/M4ReplayThreatVisual.tsx +++ b/apps/control-station/src/workspaces/laboratory/M4ReplayThreatVisual.tsx @@ -4,6 +4,7 @@ import { Icon, IconButton } from "@nodedc/ui-react"; import { ObservationTimeline } from "../../components/ObservationTimeline"; import { LaboratoryMetricEvidenceScene, + type LaboratoryMetricEvidenceSceneHandle, type LaboratoryMetricSceneMode, } from "../../components/laboratory/LaboratoryMetricEvidenceScene"; import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer"; @@ -66,7 +67,10 @@ function SpatialState({ message: text }: { message: string }) { export function M4ReplayThreatVisual({ resultId }: { resultId: string }) { const [mode, setMode] = useState("video"); const [spatialMode, setSpatialMode] = useState("3d"); + const [showCurrentIncrement, setShowCurrentIncrement] = useState(true); + const [showRollingMap, setShowRollingMap] = useState(true); const [expanded, setExpanded] = useState(false); + const metricSceneRef = useRef(null); const metadata = useM4ThreatTimelineMetadata(resultId); const playbackRange = useMemo(() => metadata.timeline ? ({ startSeconds: metadata.timeline.timelineStartSeconds, @@ -189,9 +193,44 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) {
+ {mode === "3d" || mode === "plan" ? ( +
+ + +
+ ) : null} ); + const trailingActions = mode === "3d" || mode === "plan" ? ( + metricSceneRef.current?.resetView()} + > + + + ) : null; + const overlay = metadata.timeline && frame ? (
@@ -287,6 +326,7 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) { > {frame ? ( ) : null}
@@ -336,7 +378,7 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) { onSeek={(timeNs) => playbackController.seek(timeNs / 1_000_000_000)} onPlayingChange={playbackController.setPlaying} onPlaybackRateChange={playbackController.setRate} - onJumpToEnd={() => playbackController.seek(timeline.timelineEndSeconds)} + showJumpToEnd={false} /> ) : undefined; @@ -358,6 +400,7 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) { actions={actions} overlay={overlay} transport={transport} + trailingActions={trailingActions} > {content} diff --git a/apps/control-station/test/m4ReplayThreat.test.mjs b/apps/control-station/test/m4ReplayThreat.test.mjs index c034722..003ef94 100644 --- a/apps/control-station/test/m4ReplayThreat.test.mjs +++ b/apps/control-station/test/m4ReplayThreat.test.mjs @@ -345,6 +345,8 @@ test("M4.6 viewer reuses shared camera, video and metric evidence renderers", as assert.match(videoScene, /