chore: rename repository to NODEDC MISSION CORE
This commit is contained in:
@@ -0,0 +1,766 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
AdminNavigationPanel,
|
||||
AppHeader,
|
||||
ApplicationPanel,
|
||||
ApplicationShell,
|
||||
Button,
|
||||
Checker,
|
||||
ColorField,
|
||||
ControlRow,
|
||||
HeaderAvatar,
|
||||
HeaderNavigation,
|
||||
HeaderProfile,
|
||||
HeaderProfileButton,
|
||||
HeaderWorkspace,
|
||||
Icon,
|
||||
Inspector,
|
||||
RangeControl,
|
||||
Select,
|
||||
StatusBadge,
|
||||
TextField,
|
||||
Window,
|
||||
WindowFooterActions,
|
||||
useApplicationWorkspace,
|
||||
type ApplicationPanelUtilityAction,
|
||||
} from "@nodedc/ui-react";
|
||||
|
||||
import { LandingStage } from "./components/LandingStage";
|
||||
import type { ViewerSettings } from "./api";
|
||||
import {
|
||||
rootById,
|
||||
roots,
|
||||
workspaceById,
|
||||
workspacesForRoot,
|
||||
type RootId,
|
||||
} from "./productModel";
|
||||
import { backendLabel, phaseLabel, phaseTone } from "./presentation";
|
||||
import { localizeRuntimeMessage } from "./messages";
|
||||
import {
|
||||
defaultSceneSettings,
|
||||
type PointColorMode,
|
||||
type PointPalette,
|
||||
type SceneSettings,
|
||||
} from "./sceneSettings";
|
||||
import { useK1Console } from "./useK1Console";
|
||||
import { DeviceWorkspace } from "./workspaces/DeviceWorkspace";
|
||||
import { WorkspaceRenderer } from "./workspaces/Workspaces";
|
||||
import "./styles/scene-windows.css";
|
||||
|
||||
type SceneToolWindowId = "sources" | "display" | "layers" | "layout";
|
||||
|
||||
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: "Один назначенный цвет" },
|
||||
];
|
||||
|
||||
function toViewerSettings(settings: SceneSettings): ViewerSettings {
|
||||
return {
|
||||
point_size: settings.pointSize,
|
||||
color_mode: settings.colorMode,
|
||||
palette: settings.palette,
|
||||
custom_color: settings.customColor,
|
||||
accumulation_seconds: settings.accumulationSeconds,
|
||||
show_points: settings.showPoints,
|
||||
show_trajectory: settings.showTrajectory,
|
||||
show_grid: settings.showGrid,
|
||||
};
|
||||
}
|
||||
|
||||
function mergeViewerSettings(
|
||||
current: SceneSettings,
|
||||
remote: ViewerSettings,
|
||||
): SceneSettings {
|
||||
const next = {
|
||||
...current,
|
||||
pointSize: remote.point_size,
|
||||
colorMode: remote.color_mode,
|
||||
palette: remote.palette,
|
||||
customColor: remote.custom_color,
|
||||
accumulationSeconds: remote.accumulation_seconds,
|
||||
showPoints: remote.show_points,
|
||||
showTrajectory: remote.show_trajectory,
|
||||
showGrid: remote.show_grid,
|
||||
};
|
||||
const unchanged =
|
||||
current.pointSize === next.pointSize &&
|
||||
current.colorMode === next.colorMode &&
|
||||
current.palette === next.palette &&
|
||||
current.customColor === next.customColor &&
|
||||
current.accumulationSeconds === next.accumulationSeconds &&
|
||||
current.showPoints === next.showPoints &&
|
||||
current.showTrajectory === next.showTrajectory &&
|
||||
current.showGrid === next.showGrid;
|
||||
return unchanged ? current : next;
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const console = useK1Console();
|
||||
const workspace = useApplicationWorkspace<string>({
|
||||
navigationOpen: false,
|
||||
contentExpanded: true,
|
||||
});
|
||||
|
||||
const [activeRoot, setActiveRoot] = useState<RootId | null>(null);
|
||||
const [sourceUrl, setSourceUrl] = useState("");
|
||||
const [sourceDraft, setSourceDraft] = useState("");
|
||||
const [sourceWindowOpen, setSourceWindowOpen] = useState(false);
|
||||
const [displayWindowOpen, setDisplayWindowOpen] = useState(false);
|
||||
const [layerInspectorOpen, setLayerInspectorOpen] = useState(false);
|
||||
const [sceneWindowOrder, setSceneWindowOrder] = useState<SceneToolWindowId[]>([]);
|
||||
const [layoutWindowOpen, setLayoutWindowOpen] = useState(false);
|
||||
const [layoutName, setLayoutName] = useState("Операторская сцена");
|
||||
const [layoutDraftSaved, setLayoutDraftSaved] = useState(false);
|
||||
const [sceneSettings, setSceneSettings] = useState<SceneSettings>(defaultSceneSettings);
|
||||
const [displayDraft, setDisplayDraft] = useState<SceneSettings>(defaultSceneSettings);
|
||||
|
||||
const currentRoot = rootById(activeRoot);
|
||||
const activeDefinition = workspaceById(workspace.activeView);
|
||||
const rootWorkspaces = workspacesForRoot(activeRoot);
|
||||
const activeSceneWindow = sceneWindowOrder[sceneWindowOrder.length - 1] ?? null;
|
||||
const automaticSourceUrl = console.state?.rerun_grpc_url?.trim() ?? "";
|
||||
const effectiveSourceUrl = sourceUrl || automaticSourceUrl;
|
||||
|
||||
useEffect(() => {
|
||||
const remote = console.state?.viewer_settings;
|
||||
if (!remote) return;
|
||||
setSceneSettings((current) => mergeViewerSettings(current, remote));
|
||||
if (!displayWindowOpen) {
|
||||
setDisplayDraft((current) => mergeViewerSettings(current, remote));
|
||||
}
|
||||
}, [console.state?.viewer_settings, displayWindowOpen]);
|
||||
|
||||
const activateSceneWindow = useCallback((windowId: SceneToolWindowId) => {
|
||||
setSceneWindowOrder((current) => [
|
||||
...current.filter((candidate) => candidate !== windowId),
|
||||
windowId,
|
||||
]);
|
||||
}, []);
|
||||
|
||||
const closeSceneWindow = useCallback((windowId: SceneToolWindowId) => {
|
||||
if (windowId === "sources") setSourceWindowOpen(false);
|
||||
if (windowId === "display") setDisplayWindowOpen(false);
|
||||
if (windowId === "layers") setLayerInspectorOpen(false);
|
||||
if (windowId === "layout") setLayoutWindowOpen(false);
|
||||
setSceneWindowOrder((current) => current.filter((candidate) => candidate !== windowId));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeSceneWindow) return;
|
||||
|
||||
const closeActiveWindow = (event: KeyboardEvent) => {
|
||||
if (event.key !== "Escape" || event.defaultPrevented) return;
|
||||
if (document.querySelector(".nodedc-dropdown-surface")) return;
|
||||
if (document.querySelector('.nodedc-overlay[data-placement="center"]')) return;
|
||||
event.preventDefault();
|
||||
closeSceneWindow(activeSceneWindow);
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", closeActiveWindow);
|
||||
return () => document.removeEventListener("keydown", closeActiveWindow);
|
||||
}, [activeSceneWindow, closeSceneWindow]);
|
||||
|
||||
const selectRoot = (rootId: RootId) => {
|
||||
setActiveRoot(rootId);
|
||||
workspace.closeView();
|
||||
workspace.openNavigation();
|
||||
};
|
||||
|
||||
const openView = (viewId: string) => {
|
||||
const definition = workspaceById(viewId);
|
||||
if (!definition) return;
|
||||
setActiveRoot(definition.root);
|
||||
workspace.openView(viewId);
|
||||
};
|
||||
|
||||
const openSource = () => {
|
||||
setSourceDraft(sourceUrl);
|
||||
setSourceWindowOpen(true);
|
||||
activateSceneWindow("sources");
|
||||
};
|
||||
|
||||
const openDisplay = () => {
|
||||
setDisplayDraft(sceneSettings);
|
||||
setDisplayWindowOpen(true);
|
||||
activateSceneWindow("display");
|
||||
};
|
||||
|
||||
const openLayers = () => {
|
||||
setLayerInspectorOpen(true);
|
||||
activateSceneWindow("layers");
|
||||
};
|
||||
|
||||
const openLayout = () => {
|
||||
setLayoutDraftSaved(false);
|
||||
setLayoutWindowOpen(true);
|
||||
activateSceneWindow("layout");
|
||||
};
|
||||
|
||||
const applyDisplaySettings = async () => {
|
||||
const applied = await console.updateViewerSettings(toViewerSettings(displayDraft));
|
||||
if (applied) setSceneSettings(displayDraft);
|
||||
};
|
||||
|
||||
const applyScenePatch = async (patch: Partial<SceneSettings>) => {
|
||||
const previous = sceneSettings;
|
||||
const next = { ...sceneSettings, ...patch };
|
||||
setSceneSettings(next);
|
||||
setDisplayDraft((current) => (displayWindowOpen ? { ...current, ...patch } : next));
|
||||
const applied = await console.updateViewerSettings(toViewerSettings(next));
|
||||
if (!applied) {
|
||||
setSceneSettings(previous);
|
||||
if (!displayWindowOpen) setDisplayDraft(previous);
|
||||
}
|
||||
};
|
||||
|
||||
const contentActions = useMemo<ApplicationPanelUtilityAction[]>(() => {
|
||||
const actions: ApplicationPanelUtilityAction[] = [];
|
||||
|
||||
if (activeDefinition?.kind === "device") {
|
||||
actions.push({
|
||||
label: "Обновить состояние локального контура",
|
||||
icon: "refresh",
|
||||
onClick: () => void console.refresh(),
|
||||
});
|
||||
}
|
||||
|
||||
if (activeDefinition?.kind === "spatial") {
|
||||
actions.push(
|
||||
{ label: "Настроить источник", icon: "network", onClick: openSource },
|
||||
{ label: "Настроить отображение", icon: "sliders", onClick: openDisplay },
|
||||
{ label: "Открыть слои", icon: "list", onClick: openLayers },
|
||||
);
|
||||
}
|
||||
|
||||
actions.push({
|
||||
label: "Сохранить компоновку",
|
||||
icon: "save",
|
||||
onClick: openLayout,
|
||||
});
|
||||
return actions;
|
||||
}, [activeDefinition?.kind, console, sceneSettings, sourceUrl]);
|
||||
|
||||
const header = (
|
||||
<AppHeader
|
||||
brand={<img src="/nodedc-logo.svg" alt="NODE.DC" />}
|
||||
brandLabel="NODEDC MISSION CORE"
|
||||
left={<span className="station-label">MISSION CORE</span>}
|
||||
center={
|
||||
<>
|
||||
<HeaderWorkspace kind="mark" label="Mission Core" imageUrl="/nodedc-mark.svg" />
|
||||
<HeaderNavigation
|
||||
label="Архитектурные блоки пункта управления"
|
||||
value={activeRoot ?? undefined}
|
||||
items={roots.map((root) => ({ value: root.id, label: root.label }))}
|
||||
onChange={selectRoot}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
right={
|
||||
<HeaderProfile>
|
||||
<HeaderProfileButton onClick={() => void console.refresh()} title="Обновить локальный контур">
|
||||
<span className="api-dot" data-status={console.backendStatus} aria-hidden="true" />
|
||||
{backendLabel(console.backendStatus)}
|
||||
</HeaderProfileButton>
|
||||
<HeaderAvatar label="DC" />
|
||||
</HeaderProfile>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ApplicationShell
|
||||
data-nodedc-ui
|
||||
className="control-station"
|
||||
navigationOpen={workspace.navigationOpen && activeRoot !== null}
|
||||
contentOpen={workspace.contentOpen && activeDefinition !== null}
|
||||
contentExpanded={workspace.contentExpanded}
|
||||
header={header}
|
||||
stage={
|
||||
<LandingStage
|
||||
root={currentRoot}
|
||||
backendStatus={console.backendStatus}
|
||||
phase={console.state?.phase}
|
||||
message={localizeRuntimeMessage(console.state?.message)}
|
||||
onOpenObservation={() => openView("spatial-scene")}
|
||||
onOpenDevice={() => openView("local-device")}
|
||||
/>
|
||||
}
|
||||
navigation={currentRoot ? (
|
||||
<AdminNavigationPanel
|
||||
eyebrow="MISSION CORE"
|
||||
title={currentRoot.title}
|
||||
closeLabel={`Закрыть раздел «${currentRoot.label}»`}
|
||||
navigationLabel={`Рабочие поверхности раздела «${currentRoot.label}»`}
|
||||
onClose={workspace.closeNavigation}
|
||||
contexts={[
|
||||
{
|
||||
id: "local-contour",
|
||||
label: "Локальный контур",
|
||||
description: console.state?.k1_ip || "Устройство не назначено",
|
||||
icon: <Icon name="network" />,
|
||||
active: console.backendStatus !== "offline",
|
||||
},
|
||||
]}
|
||||
items={rootWorkspaces.map((item) => ({
|
||||
id: item.id,
|
||||
label: item.label,
|
||||
icon: <Icon name={item.icon} />,
|
||||
}))}
|
||||
activeId={workspace.activeView ?? undefined}
|
||||
onItemChange={openView}
|
||||
footer={
|
||||
<>
|
||||
<span className="nodedc-admin-panel__nav-icon" aria-hidden="true">
|
||||
<Icon name="activity" />
|
||||
</span>
|
||||
<span>{rootWorkspaces.length} рабочих поверхностей</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
content={activeDefinition ? (
|
||||
<ApplicationPanel
|
||||
key={activeDefinition.id}
|
||||
eyebrow={activeDefinition.eyebrow}
|
||||
title={activeDefinition.title}
|
||||
description={activeDefinition.description}
|
||||
expanded={workspace.contentExpanded}
|
||||
onExpandedChange={workspace.setContentExpanded}
|
||||
headerTools={
|
||||
activeDefinition.kind === "device" ? (
|
||||
<StatusBadge tone={phaseTone(console.state?.phase)}>
|
||||
{phaseLabel(console.state?.phase)}
|
||||
</StatusBadge>
|
||||
) : activeDefinition.kind === "spatial" ? (
|
||||
<StatusBadge tone={effectiveSourceUrl ? "accent" : "warning"}>
|
||||
{effectiveSourceUrl ? "Источник назначен" : "Без источника"}
|
||||
</StatusBadge>
|
||||
) : (
|
||||
<StatusBadge tone="warning">Интерфейс готов</StatusBadge>
|
||||
)
|
||||
}
|
||||
utilityActions={contentActions}
|
||||
onClose={workspace.closeView}
|
||||
>
|
||||
{activeDefinition.kind === "device" ? (
|
||||
<DeviceWorkspace
|
||||
console={console}
|
||||
onOpenSpatialScene={() => openView("spatial-scene")}
|
||||
/>
|
||||
) : (
|
||||
<WorkspaceRenderer
|
||||
definition={activeDefinition}
|
||||
state={console.state}
|
||||
backendStatus={console.backendStatus}
|
||||
sourceUrl={effectiveSourceUrl}
|
||||
sceneSettings={sceneSettings}
|
||||
navigation={{
|
||||
openView,
|
||||
openSource,
|
||||
openDisplay,
|
||||
openLayers,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ApplicationPanel>
|
||||
) : null}
|
||||
/>
|
||||
|
||||
<Window
|
||||
open={sourceWindowOpen}
|
||||
title="Источники"
|
||||
subtitle="Потоки и записи пространственной сцены"
|
||||
placement="end"
|
||||
draggable
|
||||
closeOnBackdrop={false}
|
||||
closeOnEscape={false}
|
||||
lockBodyScroll={false}
|
||||
trapFocus={false}
|
||||
className="scene-tool-window scene-tool-window--sources"
|
||||
data-scene-window="sources"
|
||||
data-scene-active={activeSceneWindow === "sources" ? "true" : undefined}
|
||||
onPointerDown={() => activateSceneWindow("sources")}
|
||||
onClose={() => closeSceneWindow("sources")}
|
||||
footer={
|
||||
<WindowFooterActions>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setSourceDraft("");
|
||||
setSourceUrl("");
|
||||
}}
|
||||
>
|
||||
Сбросить адрес
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
shape="pill"
|
||||
disabled={!sourceDraft.trim()}
|
||||
onClick={() => {
|
||||
setSourceUrl(sourceDraft.trim());
|
||||
}}
|
||||
>
|
||||
Применить адрес
|
||||
</Button>
|
||||
</WindowFooterActions>
|
||||
}
|
||||
>
|
||||
<Inspector
|
||||
defaultOpen={["connection"]}
|
||||
singleOpen
|
||||
sections={[
|
||||
{
|
||||
id: "connection",
|
||||
label: "Подключение",
|
||||
description: "Адрес потока или записи",
|
||||
content: (
|
||||
<div className="inspector-control-stack">
|
||||
<ControlRow label="Состояние">
|
||||
<span className="scene-window-state">
|
||||
<i className="api-dot" data-status={effectiveSourceUrl ? "online" : "checking"} aria-hidden="true" />
|
||||
{effectiveSourceUrl ? "Источник назначен" : "Источник не назначен"}
|
||||
</span>
|
||||
</ControlRow>
|
||||
{effectiveSourceUrl ? (
|
||||
<ControlRow label="Активный адрес" layout="stack">
|
||||
<code className="scene-window-code">{effectiveSourceUrl}</code>
|
||||
</ControlRow>
|
||||
) : null}
|
||||
<ControlRow label="Автоматический" layout="stack">
|
||||
<code className="scene-window-code">
|
||||
{automaticSourceUrl || "Локальный источник ещё не опубликован"}
|
||||
</code>
|
||||
</ControlRow>
|
||||
<TextField
|
||||
label="Ручной адрес"
|
||||
hint="необязательно"
|
||||
value={sourceDraft}
|
||||
onChange={(event) => setSourceDraft(event.target.value)}
|
||||
spellCheck={false}
|
||||
placeholder="rerun+http://127.0.0.1:9876/proxy"
|
||||
description="Пустое значение использует автоматический локальный источник. Ручной адрес нужен для другого gRPC-потока или записи RRD."
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "formats",
|
||||
label: "Поддерживаемые адреса",
|
||||
description: "Rerun gRPC и RRD",
|
||||
content: (
|
||||
<div className="inspector-control-stack">
|
||||
<ControlRow label="Живой поток" layout="stack">
|
||||
<code className="scene-window-code">rerun+http://…/proxy</code>
|
||||
</ControlRow>
|
||||
<ControlRow label="Локальная запись" layout="stack">
|
||||
<code className="scene-window-code">http://…/recording.rrd</code>
|
||||
</ControlRow>
|
||||
<ControlRow label="Удалённая запись" layout="stack">
|
||||
<code className="scene-window-code">https://…/recording.rrd</code>
|
||||
</ControlRow>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "adapter",
|
||||
label: "Локальный адаптер",
|
||||
description: "Транспорт устройства",
|
||||
content: (
|
||||
<div className="inspector-control-stack">
|
||||
<ControlRow label="Контур">
|
||||
<span className="scene-window-state">
|
||||
<i className="api-dot" data-status={console.backendStatus} aria-hidden="true" />
|
||||
{backendLabel(console.backendStatus)}
|
||||
</span>
|
||||
</ControlRow>
|
||||
<p className="scene-window-note">
|
||||
Встроенный адаптер публикует локальный Rerun gRPC автоматически после запуска
|
||||
живого приёма или повтора записи. Ручной адрес для этого не требуется.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Window>
|
||||
|
||||
<Window
|
||||
open={displayWindowOpen}
|
||||
title="Отображение"
|
||||
subtitle="Параметры пространственной сцены"
|
||||
placement="end"
|
||||
draggable
|
||||
closeOnBackdrop={false}
|
||||
closeOnEscape={false}
|
||||
lockBodyScroll={false}
|
||||
trapFocus={false}
|
||||
className="scene-tool-window scene-tool-window--display"
|
||||
data-scene-window="display"
|
||||
data-scene-active={activeSceneWindow === "display" ? "true" : undefined}
|
||||
onPointerDown={() => activateSceneWindow("display")}
|
||||
onClose={() => closeSceneWindow("display")}
|
||||
footer={
|
||||
<WindowFooterActions>
|
||||
<Button variant="ghost" onClick={() => setDisplayDraft(defaultSceneSettings)}>
|
||||
Сбросить
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
shape="pill"
|
||||
disabled={console.pendingAction === "viewer"}
|
||||
onClick={() => void applyDisplaySettings()}
|
||||
>
|
||||
{console.pendingAction === "viewer" ? "Применяем…" : "Применить к сцене"}
|
||||
</Button>
|
||||
</WindowFooterActions>
|
||||
}
|
||||
>
|
||||
<Inspector
|
||||
defaultOpen={["points"]}
|
||||
singleOpen
|
||||
sections={[
|
||||
{
|
||||
id: "points",
|
||||
label: "Облако точек",
|
||||
description: "Размер и способ окрашивания",
|
||||
content: (
|
||||
<div className="inspector-control-stack">
|
||||
<RangeControl
|
||||
label="Размер точки"
|
||||
value={displayDraft.pointSize}
|
||||
min={0.5}
|
||||
max={12}
|
||||
step={0.5}
|
||||
formatValue={(value) => `${value.toFixed(1)} пкс`}
|
||||
onChange={(pointSize) => setDisplayDraft((current) => ({ ...current, pointSize }))}
|
||||
/>
|
||||
<ControlRow label="Атрибут цвета">
|
||||
<Select
|
||||
variant="split"
|
||||
label="Атрибут цвета"
|
||||
value={displayDraft.colorMode}
|
||||
options={colorModeOptions}
|
||||
onChange={(colorMode) => setDisplayDraft((current) => ({ ...current, colorMode }))}
|
||||
/>
|
||||
</ControlRow>
|
||||
<ControlRow label="Палитра">
|
||||
<Select
|
||||
variant="split"
|
||||
label="Палитра"
|
||||
value={displayDraft.palette}
|
||||
options={paletteOptions}
|
||||
onChange={(palette) => setDisplayDraft((current) => ({ ...current, palette }))}
|
||||
/>
|
||||
</ControlRow>
|
||||
{displayDraft.palette === "custom" ? (
|
||||
<ControlRow label="Цвет точек">
|
||||
<ColorField
|
||||
label="Цвет точек"
|
||||
value={displayDraft.customColor}
|
||||
onChange={(customColor) => setDisplayDraft((current) => ({ ...current, customColor }))}
|
||||
/>
|
||||
</ControlRow>
|
||||
) : null}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "history",
|
||||
label: "Накопление и время",
|
||||
description: "История облака и траектория",
|
||||
content: (
|
||||
<div className="inspector-control-stack">
|
||||
<RangeControl
|
||||
label="Окно накопления"
|
||||
value={displayDraft.accumulationSeconds}
|
||||
min={0}
|
||||
max={120}
|
||||
step={1}
|
||||
formatValue={(value) => (value === 0 ? "Только кадр" : `${value} с`)}
|
||||
onChange={(accumulationSeconds) => setDisplayDraft((current) => ({ ...current, accumulationSeconds }))}
|
||||
/>
|
||||
<div className="nodedc-field">
|
||||
<span className="nodedc-field__description">Линия пути устройства в координатах сцены</span>
|
||||
<Checker
|
||||
checked={displayDraft.showTrajectory}
|
||||
label="Показывать траекторию"
|
||||
onChange={(showTrajectory) => setDisplayDraft((current) => ({ ...current, showTrajectory }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "scene",
|
||||
label: "Окружение сцены",
|
||||
description: "Сетка, подписи и камеры",
|
||||
content: (
|
||||
<div className="inspector-control-stack">
|
||||
<Checker checked={displayDraft.showGrid} label="Сетка и оси" onChange={(showGrid) => setDisplayDraft((current) => ({ ...current, showGrid }))} />
|
||||
<div className="nodedc-field">
|
||||
<span className="nodedc-field__description">Появятся после подключения семантических сущностей</span>
|
||||
<Checker checked={false} disabled label="Подписи сущностей" onChange={() => undefined} />
|
||||
</div>
|
||||
<div className="nodedc-field">
|
||||
<span className="nodedc-field__description">Камерный канал пока не подключён</span>
|
||||
<Checker checked={false} disabled label="Области обзора камер" onChange={() => undefined} />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Window>
|
||||
|
||||
<Window
|
||||
open={layerInspectorOpen}
|
||||
title="Слои сцены"
|
||||
subtitle="Сущности пространственной сцены"
|
||||
placement="end"
|
||||
draggable
|
||||
closeOnBackdrop={false}
|
||||
closeOnEscape={false}
|
||||
lockBodyScroll={false}
|
||||
trapFocus={false}
|
||||
className="scene-tool-window scene-tool-window--layers"
|
||||
data-scene-window="layers"
|
||||
data-scene-active={activeSceneWindow === "layers" ? "true" : undefined}
|
||||
onPointerDown={() => activateSceneWindow("layers")}
|
||||
onClose={() => closeSceneWindow("layers")}
|
||||
>
|
||||
<div className="layer-inspector-intro">
|
||||
<span className="scene-window-state">
|
||||
<i className="api-dot" data-status={effectiveSourceUrl ? "online" : "checking"} aria-hidden="true" />
|
||||
{effectiveSourceUrl ? "Источник назначен" : "Источник не назначен"}
|
||||
</span>
|
||||
<p>Структура повторяет продуктовые сущности, а не внутренние панели визуального движка.</p>
|
||||
</div>
|
||||
<Inspector
|
||||
defaultOpen={["geometry"]}
|
||||
singleOpen
|
||||
sections={[
|
||||
{
|
||||
id: "geometry",
|
||||
label: "Геометрия",
|
||||
description: "Облако точек и путь",
|
||||
content: (
|
||||
<div className="inspector-control-stack">
|
||||
<div className="nodedc-field">
|
||||
<span className="nodedc-field__description">Основной поток геометрии лидара</span>
|
||||
<Checker disabled={console.pendingAction === "viewer"} checked={sceneSettings.showPoints} label="Облако точек" onChange={(showPoints) => void applyScenePatch({ showPoints })} />
|
||||
</div>
|
||||
<div className="nodedc-field">
|
||||
<span className="nodedc-field__description">Положение и ориентация устройства во времени</span>
|
||||
<Checker disabled={console.pendingAction === "viewer"} checked={sceneSettings.showTrajectory} label="Траектория" onChange={(showTrajectory) => void applyScenePatch({ showTrajectory })} />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "frames",
|
||||
label: "Координаты и камеры",
|
||||
description: "Преобразования и области обзора",
|
||||
content: (
|
||||
<div className="inspector-control-stack">
|
||||
<Checker disabled={console.pendingAction === "viewer"} checked={sceneSettings.showGrid} label="Сетка и оси" onChange={(showGrid) => void applyScenePatch({ showGrid })} />
|
||||
<div className="nodedc-field">
|
||||
<span className="nodedc-field__description">Камерный канал пока не подключён</span>
|
||||
<Checker checked={false} disabled label="Области обзора камер" onChange={() => undefined} />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "perception",
|
||||
label: "Объекты и маски",
|
||||
description: "Ожидают внешние обработчики",
|
||||
content: (
|
||||
<div className="inspector-control-stack">
|
||||
<div className="nodedc-field">
|
||||
<span className="nodedc-field__description">Внешний обработчик не подключён</span>
|
||||
<Checker checked={false} disabled label="Рамки 2D и 3D" onChange={() => undefined} />
|
||||
</div>
|
||||
<div className="nodedc-field">
|
||||
<span className="nodedc-field__description">Внешний обработчик не подключён</span>
|
||||
<Checker checked={false} disabled label="Маски сегментации" onChange={() => undefined} />
|
||||
</div>
|
||||
<div className="nodedc-field">
|
||||
<span className="nodedc-field__description">Внешний обработчик не подключён</span>
|
||||
<Checker checked={false} disabled label="Ключевые точки и треки" onChange={() => undefined} />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Window>
|
||||
|
||||
<Window
|
||||
open={layoutWindowOpen}
|
||||
title="Компоновка рабочей области"
|
||||
subtitle="КОМПОНОВКА / ЧЕРНОВИК"
|
||||
size="sm"
|
||||
placement="end"
|
||||
draggable
|
||||
closeOnBackdrop={false}
|
||||
closeOnEscape={false}
|
||||
lockBodyScroll={false}
|
||||
trapFocus={false}
|
||||
className="scene-tool-window scene-tool-window--layout"
|
||||
data-scene-window="layout"
|
||||
data-scene-active={activeSceneWindow === "layout" ? "true" : undefined}
|
||||
onPointerDown={() => activateSceneWindow("layout")}
|
||||
onClose={() => closeSceneWindow("layout")}
|
||||
footer={
|
||||
<WindowFooterActions>
|
||||
<Button onClick={() => closeSceneWindow("layout")}>Закрыть</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
shape="pill"
|
||||
disabled={!layoutName.trim()}
|
||||
onClick={() => setLayoutDraftSaved(true)}
|
||||
>
|
||||
Зафиксировать черновик
|
||||
</Button>
|
||||
</WindowFooterActions>
|
||||
}
|
||||
>
|
||||
<div className="modal-stack">
|
||||
<TextField
|
||||
label="Название компоновки"
|
||||
value={layoutName}
|
||||
onChange={(event) => {
|
||||
setLayoutName(event.target.value);
|
||||
setLayoutDraftSaved(false);
|
||||
}}
|
||||
placeholder="Название профиля"
|
||||
/>
|
||||
<div className="modal-contract-note" data-tone={layoutDraftSaved ? "success" : "warning"}>
|
||||
<Icon name={layoutDraftSaved ? "check" : "save"} />
|
||||
<div>
|
||||
<strong>{layoutDraftSaved ? "Черновик зафиксирован в текущем сеансе" : "Экспорт RBL ещё не подключён"}</strong>
|
||||
<p>
|
||||
{layoutDraftSaved
|
||||
? "Это состояние интерфейса без записи на диск. Будущий адаптер сохранит профиль Rerun рядом с кодом."
|
||||
: "Окно и контракт сохранения готовы; запись файла не имитируется."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Window>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user