From fa8ac760a1c3e94078eab948f9045c34034a63c9 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Mon, 7 Sep 2026 16:38:39 +0300 Subject: [PATCH] Share the K1 live scene template and preserve idle media channels --- apps/control-station/src/App.tsx | 211 +----- .../components/FloatingObservationWindow.tsx | 138 +--- .../src/components/ObservationSources.tsx | 130 +--- .../src/components/ObservationTimeline.tsx | 198 +----- apps/control-station/src/sceneSettings.ts | 32 +- .../src/styles/observation.css | 660 +----------------- .../src/styles/scene-windows.css | 84 +-- apps/control-station/src/styles/spatial.css | 485 +------------ .../src/workspaces/Workspaces.tsx | 11 +- .../test/k1ManualControl.test.mjs | 10 +- .../test/k1PreviewFrames.test.mjs | 39 +- .../test/observationSessions.test.mjs | 2 +- .../test/recordedCameraBuffering.test.mjs | 2 +- .../rerunViewportAtomicAdmission.test.mjs | 2 +- apps/node-agent/packaging/build.py | 1 + apps/node-agent/packaging/build_deb.py | 2 +- ...2026-09-07-k1-onboard-live-template-r12.md | 81 +++ packages/sensor-ui/src/SensorWorkspace.tsx | 2 +- packages/sensor-ui/src/pluginSdk.ts | 1 + packages/sensor-ui/src/rerunHost.ts | 3 +- .../spatial-ui/src/FloatingMediaWindow.tsx | 130 ++++ .../src/ObservationSourcePicker.tsx | 127 ++++ .../spatial-ui/src/ObservationTimeline.tsx | 197 ++++++ .../spatial-ui/src/SceneDisplayControls.tsx | 150 ++++ .../spatial-ui/src/SceneLayerControls.tsx | 62 ++ .../spatial-ui/src/SpatialToolbarActions.tsx | 8 + packages/spatial-ui/src/index.ts | 10 + packages/spatial-ui/src/observation.css | 659 +++++++++++++++++ packages/spatial-ui/src/scene-windows.css | 85 +++ packages/spatial-ui/src/sceneSettings.ts | 31 + packages/spatial-ui/src/spatial.css | 484 +++++++++++++ .../frontend/src/sensors/K1Detail.tsx | 14 +- .../frontend/src/sensors/K1LiveSettings.tsx | 28 - .../frontend/src/sensors/K1LiveView.tsx | 89 ++- .../frontend/src/sensors/K1SceneWindows.tsx | 23 + .../frontend/src/sensors/livePreview.css | 10 +- .../frontend/src/sensors/previewCamera.ts | 105 ++- .../frontend/src/sensors/previewFrames.ts | 2 +- .../frontend/src/sensors/previewFreshness.ts | 25 + .../frontend/src/sensors/useK1Preview.ts | 277 +++++--- .../src/sensors/useK1SceneSettings.ts | 51 ++ plugins/xgrids-k1/packaging/build_deb.py | 4 +- .../device_plugins/xgrids_k1/node_sensor.py | 18 +- src/k1link/viewer/node_media.py | 6 +- src/k1link/viewer/node_rerun.py | 38 +- tests/test_k1_installer.py | 2 +- tests/test_node_k1_bridge.py | 38 + tests/test_node_media.py | 92 +++ tests/test_node_rerun.py | 80 +++ 49 files changed, 2813 insertions(+), 2126 deletions(-) create mode 100644 docs/audits/2026-09-07-k1-onboard-live-template-r12.md create mode 100644 packages/spatial-ui/src/FloatingMediaWindow.tsx create mode 100644 packages/spatial-ui/src/ObservationSourcePicker.tsx create mode 100644 packages/spatial-ui/src/ObservationTimeline.tsx create mode 100644 packages/spatial-ui/src/SceneDisplayControls.tsx create mode 100644 packages/spatial-ui/src/SceneLayerControls.tsx create mode 100644 packages/spatial-ui/src/SpatialToolbarActions.tsx create mode 100644 packages/spatial-ui/src/index.ts create mode 100644 packages/spatial-ui/src/observation.css create mode 100644 packages/spatial-ui/src/scene-windows.css create mode 100644 packages/spatial-ui/src/sceneSettings.ts create mode 100644 packages/spatial-ui/src/spatial.css delete mode 100644 plugins/xgrids-k1/frontend/src/sensors/K1LiveSettings.tsx create mode 100644 plugins/xgrids-k1/frontend/src/sensors/K1SceneWindows.tsx create mode 100644 plugins/xgrids-k1/frontend/src/sensors/previewFreshness.ts create mode 100644 plugins/xgrids-k1/frontend/src/sensors/useK1SceneSettings.ts create mode 100644 tests/test_node_rerun.py diff --git a/apps/control-station/src/App.tsx b/apps/control-station/src/App.tsx index 7bdfebe..8f3945f 100644 --- a/apps/control-station/src/App.tsx +++ b/apps/control-station/src/App.tsx @@ -1,3 +1,5 @@ +import {SceneDisplayControls} from "../../../packages/spatial-ui/src/SceneDisplayControls"; +import {SceneLayerControls} from "../../../packages/spatial-ui/src/SceneLayerControls"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { AdminNavigationPanel, @@ -5,15 +7,11 @@ import { ApplicationPanel, ApplicationShell, Button, - Checker, - ColorField, ControlRow, HeaderNavigation, HeaderProfile, Icon, Inspector, - RangeControl, - Select, StatusBadge, TextField, UserProfileMenu, @@ -66,8 +64,6 @@ import { import { backendLabel, localConnectionPhaseLabel, phaseTone } from "./presentation"; import { defaultSceneSettings, - type PointColorMode, - type PointPalette, type SceneSettings, } from "./sceneSettings"; import { DeviceWorkspace } from "./workspaces/DeviceWorkspace"; @@ -77,22 +73,6 @@ import "./styles/scene-windows.css"; type SceneToolWindowId = "sources" | "display" | "layers"; const viewerSettingsQuietPeriodMs = 750; -const colorModeOptions: Array<{ value: PointColorMode; label: string; description: string }> = [ - { value: "intensity", label: "Интенсивность", description: "Значение отражённого сигнала" }, - { value: "height", label: "Высота Z", description: "Градиент по вертикальной координате" }, - { value: "distance", label: "Расстояние", description: "Дальность точки от сенсора" }, - { value: "rgb", label: "RGB", description: "Цвет, если он присутствует в данных" }, - { value: "class", label: "Класс", description: "Категория внешнего модуля восприятия" }, -]; - -const paletteOptions: Array<{ value: PointPalette; label: string; description: string }> = [ - { value: "turbo", label: "Turbo", description: "Контрастный спектральный градиент" }, - { value: "viridis", label: "Viridis", description: "Равномерный перцептивный градиент" }, - { value: "plasma", label: "Plasma", description: "Тёплый контрастный градиент" }, - { value: "grayscale", label: "Серый", description: "Монохромное отображение" }, - { value: "custom", label: "Свой цвет", description: "Один назначенный цвет" }, -]; - interface LivePerceptionLayers { detections2d: boolean; segmentation: boolean; @@ -1029,132 +1009,8 @@ export default function App() { onPointerDown={() => activateSceneWindow("display")} onClose={() => closeSceneWindow("display")} > - -
- `${value.toFixed(1)} пкс`} - onChange={(pointSize) => stageDisplayPatch({ pointSize })} - /> -
-
- - stageDisplayPatch({ palette })} - /> - -
- {displayDraft.palette === "custom" || displayDraft.colorMode === "class" ? ( -
- - stageDisplayPatch({ customColor })} - /> - -
- ) : null} - {replayPresented ? ( -

- Первый новый режим читает индекс архивных точек. Следующие палитры - переключаются из подготовленного цветового кэша без переэкспорта геометрии. -

- ) : null} - - ), - }, - { - id: "history", - label: "Накопление и время", - description: "История облака и траектория", - content: ( -
-
- (value === 0 ? "Только кадр" : `${value} с`)} - onChange={(accumulationSeconds) => stageDisplayPatch({ accumulationSeconds })} - /> -
-
- Линия пути устройства в координатах сцены - commitDisplayPatch({ showTrajectory })} - /> -
-
- ), - }, - { - id: "scene", - label: "Окружение сцены", - description: "Сетка, подписи и камеры", - content: ( -
- commitDisplayPatch({ showGrid })} /> -
- Появятся после подключения семантических сущностей - undefined} /> -
-
- Камерный канал пока не подключён - undefined} /> -
-
- ), - }, - ]} - /> +

Структура повторяет продуктовые сущности, а не внутренние панели визуального движка.

- -
- Основной поток геометрии лидара - void applyScenePatch({ showPoints })} /> -
-
- Положение и ориентация устройства во времени - void applyScenePatch({ showTrajectory })} /> -
- - ), - }, - { - id: "frames", - label: "Координаты и камеры", - description: "Преобразования и области обзора", - content: ( -
- void applyScenePatch({ showGrid })} /> -
- Камерный канал пока не подключён - undefined} /> -
-
- ), - }, - { - id: "perception", - label: "Объекты и маски", - description: "Ожидают внешние обработчики", - content: ( -
-
- Внешний обработчик не подключён - undefined} /> -
-
- Внешний обработчик не подключён - undefined} /> -
-
- Внешний обработчик не подключён - undefined} /> -
-
- ), - }, - ]} - /> +
); diff --git a/apps/control-station/src/components/FloatingObservationWindow.tsx b/apps/control-station/src/components/FloatingObservationWindow.tsx index 4db59a0..d9fa391 100644 --- a/apps/control-station/src/components/FloatingObservationWindow.tsx +++ b/apps/control-station/src/components/FloatingObservationWindow.tsx @@ -1,5 +1,6 @@ -import { useLayoutEffect, useState, type RefObject } from "react"; -import { WorkspaceWindow } from "@nodedc/ui-react"; +import type {RefObject} from "react"; +import {FloatingMediaWindow} from "../../../../packages/spatial-ui/src/FloatingMediaWindow"; +export {initialObservationWindowRect,shouldCaptureWorkspacePointer} from "../../../../packages/spatial-ui/src/FloatingMediaWindow"; import type { ObservationWindowRect } from "../core/observation/useObservationLayout"; import type { ObservationSourceDescriptor } from "../core/runtime/contracts"; @@ -10,60 +11,6 @@ import type { RecordedCameraAdmissionState, } from "../core/observation/recordedSessionAdmission"; -const WINDOW_WIDTH = 336; -const WINDOW_HEIGHT = 210; -const WINDOW_GAP = 16; -const WINDOW_INSET = 18; -const TIMELINE_CLEARANCE = 64; - -type ClosestTarget = { closest: (selectors: string) => unknown }; - -export function shouldCaptureWorkspacePointer( - button: number, - target: ClosestTarget | null, -): boolean { - if (button !== 0 || !target) return false; - if (target.closest(".nodedc-workspace-window__resize")) return true; - if (!target.closest(".nodedc-workspace-window__head")) return false; - return !target.closest("button, input, select, textarea, a"); -} - -export function initialObservationWindowRect( - index: number, - count = 1, - bounds: { width: number; height: number } = { width: 1280, height: 720 }, -): ObservationWindowRect { - const availableWidth = Math.max(0, bounds.width - WINDOW_INSET * 2); - const width = Math.max(1, Math.min(WINDOW_WIDTH, availableWidth || WINDOW_WIDTH)); - const availableHeight = Math.max(0, bounds.height - WINDOW_INSET - TIMELINE_CLEARANCE); - const maxColumns = Math.max( - 1, - Math.floor((availableWidth + WINDOW_GAP) / (width + WINDOW_GAP)), - ); - const columns = Math.max(1, Math.min(Math.max(1, count), maxColumns)); - const rows = Math.max(1, Math.ceil(Math.max(1, count) / columns)); - const rowHeight = Math.max( - 1, - (availableHeight - Math.max(0, rows - 1) * WINDOW_GAP) / rows, - ); - const height = Math.max(1, Math.min(WINDOW_HEIGHT, rowHeight)); - const row = Math.floor(index / columns); - const column = index % columns; - const rowItemCount = Math.min(columns, Math.max(1, count - row * columns)); - const rowWidth = rowItemCount * width + Math.max(0, rowItemCount - 1) * WINDOW_GAP; - const rowStart = Math.max(0, bounds.width - WINDOW_INSET - rowWidth); - - return { - x: rowStart + Math.min(column, rowItemCount - 1) * (width + WINDOW_GAP), - y: Math.max( - 0, - bounds.height - TIMELINE_CLEARANCE - height - row * (height + WINDOW_GAP), - ), - width, - height, - }; -} - export function FloatingObservationWindow({ source, index, @@ -104,76 +51,12 @@ export function FloatingObservationWindow({ state: RecordedCameraAdmissionState, ) => void; }) { - const [bounds, setBounds] = useState<{ width: number; height: number } | null>(null); - - useLayoutEffect(() => { - const element = boundsRef.current; - if (!element) return; - - const measure = () => { - const next = { width: element.clientWidth, height: element.clientHeight }; - setBounds((current) => ( - current?.width === next.width && current.height === next.height ? current : next - )); - }; - - measure(); - const observer = new ResizeObserver(measure); - observer.observe(element); - return () => observer.disconnect(); - }, [boundsRef]); - - // Camera media admission must not wait for the viewport observer. A source - // can arrive in the same commit as a workspace transition; mounting it with - // the canonical fallback geometry keeps the browser transport alive until - // the real bounds are measured immediately afterward. - const initialRect = initialObservationWindowRect(index, count, bounds ?? undefined); - const windowRect = rect ?? initialRect; - - return ( - -