feat(ui): separate live data and lab surfaces
This commit is contained in:
@@ -26,7 +26,6 @@ import {
|
||||
} from "@nodedc/ui-react";
|
||||
|
||||
import { LandingStage } from "./components/LandingStage";
|
||||
import { ObservationSessionSelect } from "./components/ObservationSessionSelect";
|
||||
import { useDevicePluginHost } from "./core/device-plugins/DevicePluginHost";
|
||||
import { useMissionRuntime } from "./core/runtime/MissionRuntimeContext";
|
||||
import type { ViewerSettings } from "./core/runtime/contracts";
|
||||
@@ -160,7 +159,7 @@ export default function App() {
|
||||
});
|
||||
|
||||
const [activeRoot, setActiveRoot] = useState<RootId | null>(
|
||||
polygonDatasetRoute.active ? "polygon" : null,
|
||||
polygonDatasetRoute.active ? "data" : null,
|
||||
);
|
||||
const [sourceUrl, setSourceUrl] = useState("");
|
||||
const [recordedReplay, setRecordedReplay] = useState<ObservationSessionReplayLaunch | null>(null);
|
||||
@@ -199,24 +198,33 @@ export default function App() {
|
||||
const currentRoot = rootById(activeRoot);
|
||||
const visibleRoots = roots;
|
||||
const activeDefinition = workspaceById(workspace.activeView);
|
||||
const spatialWorkspaceActive = Boolean(
|
||||
workspace.contentOpen && activeDefinition?.kind === "spatial",
|
||||
const sceneWorkspaceActive = Boolean(
|
||||
workspace.contentOpen
|
||||
&& activeDefinition
|
||||
&& ["spatial", "recordings"].includes(activeDefinition.kind),
|
||||
);
|
||||
const archiveWorkspaceActive = Boolean(
|
||||
activeDefinition
|
||||
&& ["recordings", "lab-archive"].includes(activeDefinition.kind),
|
||||
);
|
||||
const rootWorkspaces = workspacesForRoot(activeRoot);
|
||||
const activeSceneWindow = sceneWindowOrder[sceneWindowOrder.length - 1] ?? null;
|
||||
const automaticSourceUrl = runtime.state?.spatialSource?.url.trim() ?? "";
|
||||
const effectiveSourceUrl = replayTransitioning ? "" : sourceUrl || automaticSourceUrl;
|
||||
const effectiveSourceUrl = archiveWorkspaceActive
|
||||
? replayTransitioning ? "" : sourceUrl
|
||||
: automaticSourceUrl;
|
||||
const replayActive = Boolean(recordedReplay && sourceUrl === recordedReplay.sourceUrl);
|
||||
const replayPresented = archiveWorkspaceActive && replayActive;
|
||||
const recordedSessionAdmission = useRecordedSessionAdmission(
|
||||
replayActive ? recordedReplay : null,
|
||||
replayPresented ? recordedReplay : null,
|
||||
);
|
||||
runtimeUpdateViewerSettingsRef.current = runtime.updateViewerSettings;
|
||||
replayActiveRef.current = replayActive;
|
||||
replayActiveRef.current = replayPresented;
|
||||
|
||||
useEffect(() => {
|
||||
if (!polygonDatasetRoute.active || polygonDatasetRouteOpenedRef.current) return;
|
||||
polygonDatasetRouteOpenedRef.current = true;
|
||||
workspace.openView("polygon-datasets");
|
||||
workspace.openView("datasets");
|
||||
}, [polygonDatasetRoute.active, workspace]);
|
||||
|
||||
if (!sceneSettingsCommitterRef.current) {
|
||||
@@ -245,11 +253,11 @@ export default function App() {
|
||||
() => recordedObservationSources(recordedReplay),
|
||||
[recordedReplay],
|
||||
);
|
||||
const activeObservationSources = replayActive
|
||||
const activeObservationSources = replayPresented
|
||||
? replaySources
|
||||
: runtime.state?.observationSources ?? [];
|
||||
const activeRuntimeState = useMemo(() => {
|
||||
if (!replayActive || !recordedReplay) return runtime.state;
|
||||
if (!replayPresented || !recordedReplay) return runtime.state;
|
||||
return {
|
||||
...(runtime.state ?? { phase: "replaying" as const }),
|
||||
phase: "replaying" as const,
|
||||
@@ -272,7 +280,7 @@ export default function App() {
|
||||
},
|
||||
},
|
||||
};
|
||||
}, [recordedReplay, replayActive, replaySources, runtime.state]);
|
||||
}, [recordedReplay, replayPresented, replaySources, runtime.state]);
|
||||
const viewerSettingsTargetIdentity = [
|
||||
runtime.state?.activeDevice?.pluginId,
|
||||
runtime.state?.activeDevice?.modelId,
|
||||
@@ -283,7 +291,7 @@ export default function App() {
|
||||
].filter(Boolean).join(":") || "local-runtime";
|
||||
const observationLayout = useObservationLayout(
|
||||
activeObservationSources,
|
||||
replayActive ? undefined : runtime.setObservationSourceActive,
|
||||
replayPresented ? undefined : runtime.setObservationSourceActive,
|
||||
);
|
||||
const workspaceLayoutProfile = useWorkspaceLayoutProfile();
|
||||
const focusedObservationSource = activeObservationSources.find(
|
||||
@@ -335,14 +343,14 @@ export default function App() {
|
||||
}, [observationLayout.restore, workspaceLayoutProfile.profile]);
|
||||
|
||||
useEffect(() => {
|
||||
if (spatialWorkspaceActive) return;
|
||||
if (sceneWorkspaceActive) return;
|
||||
// Scene tools are transient children of the spatial workspace. They must
|
||||
// never survive a route/root/content close or appear over the landing page.
|
||||
setSourceWindowOpen(false);
|
||||
setDisplayWindowOpen(false);
|
||||
setLayerInspectorOpen(false);
|
||||
setSceneWindowOrder([]);
|
||||
}, [spatialWorkspaceActive]);
|
||||
}, [sceneWorkspaceActive]);
|
||||
|
||||
useEffect(() => {
|
||||
sceneSettingsCommitterActiveRef.current = true;
|
||||
@@ -398,7 +406,7 @@ export default function App() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!spatialWorkspaceActive || !activeSceneWindow) return;
|
||||
if (!sceneWorkspaceActive || !activeSceneWindow) return;
|
||||
|
||||
const closeActiveWindow = (event: KeyboardEvent) => {
|
||||
if (event.key !== "Escape" || event.defaultPrevented) return;
|
||||
@@ -410,12 +418,12 @@ export default function App() {
|
||||
|
||||
document.addEventListener("keydown", closeActiveWindow);
|
||||
return () => document.removeEventListener("keydown", closeActiveWindow);
|
||||
}, [activeSceneWindow, closeSceneWindow, spatialWorkspaceActive]);
|
||||
}, [activeSceneWindow, closeSceneWindow, sceneWorkspaceActive]);
|
||||
|
||||
const selectRoot = (rootId: RootId) => {
|
||||
setActiveRoot(rootId);
|
||||
if (rootId === "polygon") {
|
||||
workspace.openView("polygon-datasets");
|
||||
workspace.openView("lab-archive");
|
||||
workspace.openNavigation();
|
||||
return;
|
||||
}
|
||||
@@ -431,7 +439,7 @@ export default function App() {
|
||||
};
|
||||
|
||||
const openSource = () => {
|
||||
if (!spatialWorkspaceActive) return;
|
||||
if (!sceneWorkspaceActive) return;
|
||||
setSourceDraft(sourceUrl);
|
||||
setSourceWindowOpen(true);
|
||||
activateSceneWindow("sources");
|
||||
@@ -472,13 +480,13 @@ export default function App() {
|
||||
}, [commitDisplaySettings]);
|
||||
|
||||
const openDisplay = () => {
|
||||
if (!spatialWorkspaceActive) return;
|
||||
if (!sceneWorkspaceActive) return;
|
||||
setDisplayWindowOpen(true);
|
||||
activateSceneWindow("display");
|
||||
};
|
||||
|
||||
const openLayers = () => {
|
||||
if (!spatialWorkspaceActive) return;
|
||||
if (!sceneWorkspaceActive) return;
|
||||
setLayerInspectorOpen(true);
|
||||
activateSceneWindow("layers");
|
||||
};
|
||||
@@ -547,12 +555,9 @@ export default function App() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// ObservationSessionSelect owns the cancellable request and unmounts when
|
||||
// the operator leaves the spatial workspace. Its unmount cannot safely
|
||||
// call back into this owner, so clear the transient blanking state here.
|
||||
// Returning to Observation then starts from a deterministic idle source
|
||||
// instead of an orphaned `replayTransitioning=true` state.
|
||||
if (activeDefinition?.kind !== "spatial") setReplayTransitioning(false);
|
||||
if (!["recordings", "lab-archive"].includes(activeDefinition?.kind ?? "")) {
|
||||
setReplayTransitioning(false);
|
||||
}
|
||||
}, [activeDefinition?.kind]);
|
||||
|
||||
const applyScenePatch = (patch: Partial<SceneSettings>) => commitDisplayPatch(patch);
|
||||
@@ -598,7 +603,10 @@ export default function App() {
|
||||
});
|
||||
}
|
||||
|
||||
if (activeDefinition?.kind === "spatial") {
|
||||
if (
|
||||
activeDefinition
|
||||
&& ["spatial", "recordings"].includes(activeDefinition.kind)
|
||||
) {
|
||||
actions.push(
|
||||
{
|
||||
label: workspaceLayoutProfile.state === "saving"
|
||||
@@ -709,7 +717,9 @@ export default function App() {
|
||||
key={activeDefinition.id}
|
||||
eyebrow={activeDefinition.eyebrow}
|
||||
title={
|
||||
activeDefinition.kind === "spatial" && replayActive && recordedReplayLabel
|
||||
activeDefinition.kind === "recordings"
|
||||
&& replayPresented
|
||||
&& recordedReplayLabel
|
||||
? `${activeDefinition.title}: ${recordedReplayLabel}`
|
||||
: activeDefinition.title
|
||||
}
|
||||
@@ -723,13 +733,9 @@ export default function App() {
|
||||
</StatusBadge>
|
||||
) : activeDefinition.kind === "spatial" ? (
|
||||
<div className="observation-header-tools">
|
||||
<ObservationSessionSelect
|
||||
disabled={runtime.pendingAction !== null || sourceSwitchBlocked}
|
||||
blockedReason={sourceSwitchBlockedReason}
|
||||
onReplayBegin={beginRecordedReplaySwitch}
|
||||
onReplayAccepted={acceptRecordedReplay}
|
||||
onReplaySettled={(_session, outcome) => settleRecordedReplaySwitch(outcome)}
|
||||
/>
|
||||
<StatusBadge tone={runtime.state?.sourceMode === "live" ? "success" : "neutral"}>
|
||||
{runtime.state?.sourceMode === "live" ? "Эфир" : "Ожидание эфира"}
|
||||
</StatusBadge>
|
||||
{layoutSaveNotice || workspaceLayoutProfile.error ? (
|
||||
<span
|
||||
className="workspace-layout-feedback"
|
||||
@@ -745,8 +751,12 @@ export default function App() {
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
) : activeDefinition.kind === "polygon-datasets" ? (
|
||||
) : activeDefinition.kind === "recordings" ? (
|
||||
<StatusBadge tone="accent">Архив записей</StatusBadge>
|
||||
) : activeDefinition.kind === "datasets" ? (
|
||||
<StatusBadge tone="neutral">Offline evaluation</StatusBadge>
|
||||
) : activeDefinition.kind === "lab-archive" ? (
|
||||
<StatusBadge tone="accent">Лаборатория</StatusBadge>
|
||||
) : (
|
||||
<StatusBadge tone="warning">Интерфейс готов</StatusBadge>
|
||||
)
|
||||
@@ -765,7 +775,7 @@ export default function App() {
|
||||
state={activeRuntimeState}
|
||||
backendStatus={runtime.backendStatus}
|
||||
sourceUrl={effectiveSourceUrl}
|
||||
recordedReplay={replayActive ? recordedReplay : null}
|
||||
recordedReplay={replayPresented ? recordedReplay : null}
|
||||
recordedSessionAdmission={recordedSessionAdmission}
|
||||
sceneSettings={sceneSettings}
|
||||
accumulationSeconds={displayDraft.accumulationSeconds}
|
||||
@@ -782,6 +792,14 @@ export default function App() {
|
||||
model: selection.model,
|
||||
}
|
||||
: null}
|
||||
sessionArchive={{
|
||||
disabled: runtime.pendingAction !== null || sourceSwitchBlocked,
|
||||
blockedReason: sourceSwitchBlockedReason,
|
||||
onReplayBegin: beginRecordedReplaySwitch,
|
||||
onReplayAccepted: acceptRecordedReplay,
|
||||
onReplaySettled: (_session, outcome) =>
|
||||
settleRecordedReplaySwitch(outcome),
|
||||
}}
|
||||
navigation={{
|
||||
openView,
|
||||
openSource,
|
||||
@@ -796,7 +814,7 @@ export default function App() {
|
||||
/>
|
||||
|
||||
<Window
|
||||
open={spatialWorkspaceActive && sourceWindowOpen}
|
||||
open={sceneWorkspaceActive && sourceWindowOpen}
|
||||
title="Визуальный движок"
|
||||
subtitle="Rerun gRPC и записи пространственной сцены"
|
||||
placement="end"
|
||||
@@ -924,7 +942,7 @@ export default function App() {
|
||||
</Window>
|
||||
|
||||
<Window
|
||||
open={spatialWorkspaceActive && displayWindowOpen}
|
||||
open={sceneWorkspaceActive && displayWindowOpen}
|
||||
title="Отображение"
|
||||
subtitle="Параметры пространственной сцены"
|
||||
placement="end"
|
||||
@@ -1003,7 +1021,7 @@ export default function App() {
|
||||
</ControlRow>
|
||||
</div>
|
||||
) : null}
|
||||
{replayActive ? (
|
||||
{replayPresented ? (
|
||||
<p className="scene-window-note">
|
||||
Первый новый режим читает индекс архивных точек. Следующие палитры
|
||||
переключаются из подготовленного цветового кэша без переэкспорта геометрии.
|
||||
@@ -1068,7 +1086,7 @@ export default function App() {
|
||||
</Window>
|
||||
|
||||
<Window
|
||||
open={spatialWorkspaceActive && layerInspectorOpen}
|
||||
open={sceneWorkspaceActive && layerInspectorOpen}
|
||||
title="Слои сцены"
|
||||
subtitle="Сущности пространственной сцены"
|
||||
placement="end"
|
||||
|
||||
Reference in New Issue
Block a user