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 ( - -