feat: introduce device plugin runtime boundary

This commit is contained in:
DCCONSTRUCTIONS
2026-07-16 13:49:10 +03:00
parent 9225227421
commit 27bf7527df
45 changed files with 3748 additions and 1062 deletions
+34 -31
View File
@@ -26,7 +26,9 @@ import {
} from "@nodedc/ui-react";
import { LandingStage } from "./components/LandingStage";
import type { ViewerSettings } from "./api";
import { useDevicePluginHost } from "./core/device-plugins/DevicePluginHost";
import { useMissionRuntime } from "./core/runtime/MissionRuntimeContext";
import type { ViewerSettings } from "./core/runtime/contracts";
import {
rootById,
roots,
@@ -35,14 +37,12 @@ import {
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";
@@ -106,7 +106,8 @@ function mergeViewerSettings(
}
export default function App() {
const console = useK1Console();
const runtime = useMissionRuntime();
const { selection } = useDevicePluginHost();
const workspace = useApplicationWorkspace<string>({
navigationOpen: false,
contentExpanded: true,
@@ -129,17 +130,17 @@ export default function App() {
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 automaticSourceUrl = runtime.state?.spatialSource?.url.trim() ?? "";
const effectiveSourceUrl = sourceUrl || automaticSourceUrl;
useEffect(() => {
const remote = console.state?.viewer_settings;
const remote = runtime.state?.viewerSettings;
if (!remote) return;
setSceneSettings((current) => mergeViewerSettings(current, remote));
if (!displayWindowOpen) {
setDisplayDraft((current) => mergeViewerSettings(current, remote));
}
}, [console.state?.viewer_settings, displayWindowOpen]);
}, [runtime.state?.viewerSettings, displayWindowOpen]);
const activateSceneWindow = useCallback((windowId: SceneToolWindowId) => {
setSceneWindowOrder((current) => [
@@ -208,7 +209,7 @@ export default function App() {
};
const applyDisplaySettings = async () => {
const applied = await console.updateViewerSettings(toViewerSettings(displayDraft));
const applied = await runtime.updateViewerSettings(toViewerSettings(displayDraft));
if (applied) setSceneSettings(displayDraft);
};
@@ -217,7 +218,7 @@ export default function App() {
const next = { ...sceneSettings, ...patch };
setSceneSettings(next);
setDisplayDraft((current) => (displayWindowOpen ? { ...current, ...patch } : next));
const applied = await console.updateViewerSettings(toViewerSettings(next));
const applied = await runtime.updateViewerSettings(toViewerSettings(next));
if (!applied) {
setSceneSettings(previous);
if (!displayWindowOpen) setDisplayDraft(previous);
@@ -231,7 +232,7 @@ export default function App() {
actions.push({
label: "Обновить состояние локального контура",
icon: "refresh",
onClick: () => void console.refresh(),
onClick: () => void runtime.refresh(),
});
}
@@ -249,7 +250,7 @@ export default function App() {
onClick: openLayout,
});
return actions;
}, [activeDefinition?.kind, console, sceneSettings, sourceUrl]);
}, [activeDefinition?.kind, runtime, sceneSettings, sourceUrl]);
const header = (
<AppHeader
@@ -269,9 +270,9 @@ export default function App() {
}
right={
<HeaderProfile>
<HeaderProfileButton onClick={() => void console.refresh()} title="Обновить локальный контур">
<span className="api-dot" data-status={console.backendStatus} aria-hidden="true" />
{backendLabel(console.backendStatus)}
<HeaderProfileButton onClick={() => void runtime.refresh()} title="Обновить локальный контур">
<span className="api-dot" data-status={runtime.backendStatus} aria-hidden="true" />
{backendLabel(runtime.backendStatus)}
</HeaderProfileButton>
<HeaderAvatar label="DC" />
</HeaderProfile>
@@ -291,9 +292,9 @@ export default function App() {
stage={
<LandingStage
root={currentRoot}
backendStatus={console.backendStatus}
phase={console.state?.phase}
message={localizeRuntimeMessage(console.state?.message)}
backendStatus={runtime.backendStatus}
phase={runtime.state?.phase}
message={runtime.state?.message}
onOpenObservation={() => openView("spatial-scene")}
onOpenDevice={() => openView("local-device")}
/>
@@ -309,9 +310,12 @@ export default function App() {
{
id: "local-contour",
label: "Локальный контур",
description: console.state?.k1_ip || "Устройство не назначено",
description:
runtime.state?.activeDevice?.endpointLabel ||
selection?.model.displayName ||
"Модель не выбрана",
icon: <Icon name="network" />,
active: console.backendStatus !== "offline",
active: runtime.backendStatus !== "offline" && runtime.backendStatus !== "unconfigured",
},
]}
items={rootWorkspaces.map((item) => ({
@@ -341,8 +345,8 @@ export default function App() {
onExpandedChange={workspace.setContentExpanded}
headerTools={
activeDefinition.kind === "device" ? (
<StatusBadge tone={phaseTone(console.state?.phase)}>
{phaseLabel(console.state?.phase)}
<StatusBadge tone={phaseTone(runtime.state?.phase)}>
{phaseLabel(runtime.state?.phase)}
</StatusBadge>
) : activeDefinition.kind === "spatial" ? (
<StatusBadge tone={effectiveSourceUrl ? "accent" : "warning"}>
@@ -357,14 +361,13 @@ export default function App() {
>
{activeDefinition.kind === "device" ? (
<DeviceWorkspace
console={console}
onOpenSpatialScene={() => openView("spatial-scene")}
/>
) : (
<WorkspaceRenderer
definition={activeDefinition}
state={console.state}
backendStatus={console.backendStatus}
state={runtime.state}
backendStatus={runtime.backendStatus}
sourceUrl={effectiveSourceUrl}
sceneSettings={sceneSettings}
navigation={{
@@ -482,8 +485,8 @@ export default function App() {
<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)}
<i className="api-dot" data-status={runtime.backendStatus} aria-hidden="true" />
{backendLabel(runtime.backendStatus)}
</span>
</ControlRow>
<p className="scene-window-note">
@@ -520,10 +523,10 @@ export default function App() {
<Button
variant="primary"
shape="pill"
disabled={console.pendingAction === "viewer"}
disabled={runtime.pendingAction === "viewer"}
onClick={() => void applyDisplaySettings()}
>
{console.pendingAction === "viewer" ? "Применяем…" : "Применить к сцене"}
{runtime.pendingAction === "viewer" ? "Применяем…" : "Применить к сцене"}
</Button>
</WindowFooterActions>
}
@@ -660,11 +663,11 @@ export default function App() {
<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 })} />
<Checker disabled={runtime.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 })} />
<Checker disabled={runtime.pendingAction === "viewer"} checked={sceneSettings.showTrajectory} label="Траектория" onChange={(showTrajectory) => void applyScenePatch({ showTrajectory })} />
</div>
</div>
),
@@ -675,7 +678,7 @@ export default function App() {
description: "Преобразования и области обзора",
content: (
<div className="inspector-control-stack">
<Checker disabled={console.pendingAction === "viewer"} checked={sceneSettings.showGrid} label="Сетка и оси" onChange={(showGrid) => void applyScenePatch({ showGrid })} />
<Checker disabled={runtime.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} />