Preserve onboard preview recordings across backpressure and share the complete spatial scene
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { ActivityIndicator, Button } from "@nodedc/ui-react";
|
||||
import {K1SpatialSession, k1SpatialPhasePresentation} from './K1SpatialSession';
|
||||
export {k1SpatialPhasePresentation} from './K1SpatialSession';
|
||||
import { Button } from "@nodedc/ui-react";
|
||||
|
||||
import type { DevicePluginConnectionProps } from "@mission-core/plugin-sdk";
|
||||
import type {
|
||||
AcquisitionState,
|
||||
XgridsAcquisition,
|
||||
XgridsK1State,
|
||||
} from "../api";
|
||||
import {
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
} from "../lifecycle";
|
||||
import {
|
||||
deviceTelemetry,
|
||||
formatNumber,
|
||||
spatialActionFailure,
|
||||
} from "../presentation";
|
||||
import {
|
||||
@@ -34,12 +33,6 @@ import {
|
||||
} from "../runtimeContext";
|
||||
import { ActiveStreamRecoverySurface } from "./ActiveStreamRecoverySurface";
|
||||
|
||||
interface PhasePresentation {
|
||||
label: string;
|
||||
detail: string;
|
||||
busy: boolean;
|
||||
}
|
||||
|
||||
export interface K1SpatialAuthorityState {
|
||||
controlAuthoritative: boolean;
|
||||
dataAuthoritative: boolean;
|
||||
@@ -64,85 +57,6 @@ export function k1SpatialAuthorityState(
|
||||
};
|
||||
}
|
||||
|
||||
export function k1SpatialPhasePresentation(
|
||||
acquisition: XgridsAcquisition,
|
||||
softwareCommanded: boolean,
|
||||
): PhasePresentation {
|
||||
const presentations: Record<AcquisitionState, PhasePresentation> = {
|
||||
preparing: {
|
||||
label: "Подготовка локального приёма",
|
||||
detail: "Проверка контура и создание сессии записи.",
|
||||
busy: true,
|
||||
},
|
||||
prepared: {
|
||||
label: "Приём подготовлен",
|
||||
detail: softwareCommanded
|
||||
? "Можно инициировать работу устройства из Mission Core."
|
||||
: "Программная команда K1 недоступна в текущем профиле.",
|
||||
busy: false,
|
||||
},
|
||||
awaiting_external_start: {
|
||||
label: softwareCommanded
|
||||
? "K1 калибруется и готовит облако точек"
|
||||
: "Ожидание запуска на устройстве",
|
||||
detail: softwareCommanded
|
||||
? "Первые данные могут появиться через десятки секунд. Не перемещайте устройство."
|
||||
: "Запустите сканирование физической кнопкой K1.",
|
||||
busy: true,
|
||||
},
|
||||
starting: {
|
||||
label: softwareCommanded
|
||||
? "K1 калибруется и готовит облако точек"
|
||||
: "Подготовка локального приёмника",
|
||||
detail: softwareCommanded
|
||||
? "Первые данные могут появиться через десятки секунд. Не перемещайте устройство."
|
||||
: "Mission Core запускает запись до физического старта K1.",
|
||||
busy: true,
|
||||
},
|
||||
acquiring: {
|
||||
label: softwareCommanded ? "K1 работает · запись активна" : "Локальная запись активна",
|
||||
detail: softwareCommanded
|
||||
? "Состояние получено из профилированного контура управления K1."
|
||||
: "Mission Core принимает данные; физическое состояние K1 не управляется программно.",
|
||||
busy: false,
|
||||
},
|
||||
awaiting_external_stop: {
|
||||
label: softwareCommanded ? "K1 завершает и сохраняет" : "Ожидание остановки на устройстве",
|
||||
detail: softwareCommanded
|
||||
? "STOP не повторяется. Mission Core завершит запись после READY от K1."
|
||||
: "Mission Core ждёт подтверждения физической остановки K1.",
|
||||
busy: true,
|
||||
},
|
||||
stopping: {
|
||||
label: softwareCommanded ? "Остановка K1 и записи" : "Остановка локального приёма",
|
||||
detail: softwareCommanded
|
||||
? "Команда отправлена; ожидаем подтверждённое состояние устройства."
|
||||
: "Физическое состояние K1 остаётся неизвестным.",
|
||||
busy: true,
|
||||
},
|
||||
finalizing: {
|
||||
label: "Сохранение локальной записи",
|
||||
detail: "Не закрывайте Mission Core до завершения финализации.",
|
||||
busy: true,
|
||||
},
|
||||
completed: { label: "Приём завершён", detail: "", busy: false },
|
||||
failed: { label: "Ошибка приёма", detail: "", busy: false },
|
||||
aborted: { label: "Приём прерван", detail: "", busy: false },
|
||||
interrupted: { label: "Приём прерван", detail: "", busy: false },
|
||||
};
|
||||
return presentations[acquisition.state];
|
||||
}
|
||||
|
||||
function formatDuration(seconds: number): string {
|
||||
const wholeSeconds = Math.max(0, Math.floor(seconds));
|
||||
const hours = Math.floor(wholeSeconds / 3_600);
|
||||
const minutes = Math.floor((wholeSeconds % 3_600) / 60);
|
||||
const remainder = wholeSeconds % 60;
|
||||
return hours > 0
|
||||
? `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(remainder).padStart(2, "0")}`
|
||||
: `${String(minutes).padStart(2, "0")}:${String(remainder).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
export function runSpatialActiveStreamForceFinish(
|
||||
controller: Pick<
|
||||
XgridsK1Controller,
|
||||
@@ -276,30 +190,7 @@ export function K1SpatialControlsView({
|
||||
);
|
||||
const actionFailure = runtimeActionFailure ?? spatialActionFailure(authorityFailure);
|
||||
return (
|
||||
<section
|
||||
className="xgrids-k1-spatial-controls"
|
||||
aria-label="Управление сессией XGRIDS K1"
|
||||
aria-busy={phase.busy}
|
||||
data-busy={phase.busy ? "true" : undefined}
|
||||
>
|
||||
<div className="xgrids-k1-spatial-controls__phase">
|
||||
{phase.busy ? <ActivityIndicator size="compact" /> : null}
|
||||
<span>
|
||||
<strong>{phase.label}</strong>
|
||||
<small>{phase.detail}</small>
|
||||
</span>
|
||||
</div>
|
||||
<div className="xgrids-k1-spatial-controls__telemetry" aria-label="Телеметрия маршрута K1">
|
||||
{telemetry.elapsedSeconds !== null ? (
|
||||
<span><small>Время сканирования</small><strong>{formatDuration(telemetry.elapsedSeconds)}</strong></span>
|
||||
) : null}
|
||||
{telemetry.routeDistanceMeters !== null ? (
|
||||
<span><small>Маршрут устройства</small><strong>{formatNumber(telemetry.routeDistanceMeters, 2)} м</strong></span>
|
||||
) : null}
|
||||
{telemetry.speedMetersPerSecond !== null ? (
|
||||
<span><small>Скорость</small><strong>{formatNumber(telemetry.speedMetersPerSecond, 2)} м/с</strong></span>
|
||||
) : null}
|
||||
</div>
|
||||
<K1SpatialSession phase={phase} telemetry={telemetry}>
|
||||
{actionFailure ? (
|
||||
<div className="xgrids-k1-spatial-controls__error" role="alert">
|
||||
<strong>{actionFailure.title}</strong>
|
||||
@@ -345,7 +236,7 @@ export function K1SpatialControlsView({
|
||||
</span>
|
||||
</Button>
|
||||
) : null}
|
||||
</section>
|
||||
</K1SpatialSession>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
.xgrids-k1-spatial-controls {
|
||||
display: flex;
|
||||
min-width: min(42rem, 100%);
|
||||
max-width: 100%;
|
||||
align-items: center;
|
||||
gap: 0.85rem;
|
||||
border: 1px solid rgb(255 255 255 / 0.1);
|
||||
border-radius: 1rem;
|
||||
background: rgb(9 10 13 / 0.88);
|
||||
padding: 0.55rem 0.65rem 0.55rem 0.75rem;
|
||||
color: var(--nodedc-text-primary);
|
||||
box-shadow: 0 0.9rem 2.4rem rgb(0 0 0 / 0.3);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.xgrids-k1-spatial-controls__phase {
|
||||
display: flex;
|
||||
min-width: 11rem;
|
||||
flex: 1 1 15rem;
|
||||
align-items: center;
|
||||
gap: 0.58rem;
|
||||
}
|
||||
|
||||
.xgrids-k1-spatial-controls__phase > span:last-child {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.xgrids-k1-spatial-controls__phase strong,
|
||||
.xgrids-k1-spatial-controls__phase small {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.xgrids-k1-spatial-controls__phase strong {
|
||||
font-size: 0.66rem;
|
||||
}
|
||||
|
||||
.xgrids-k1-spatial-controls__phase small {
|
||||
color: var(--nodedc-text-muted);
|
||||
font-size: 0.53rem;
|
||||
}
|
||||
|
||||
.xgrids-k1-spatial-controls__telemetry {
|
||||
display: flex;
|
||||
flex: 0 1 auto;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.xgrids-k1-spatial-controls__telemetry > span {
|
||||
display: grid;
|
||||
gap: 0.12rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.xgrids-k1-spatial-controls__telemetry small {
|
||||
color: var(--nodedc-text-muted);
|
||||
font-size: 0.48rem;
|
||||
}
|
||||
|
||||
.xgrids-k1-spatial-controls__telemetry strong {
|
||||
font-size: 0.61rem;
|
||||
}
|
||||
|
||||
.xgrids-k1-spatial-controls__error {
|
||||
display: grid;
|
||||
max-width: 17rem;
|
||||
gap: 0.12rem;
|
||||
color: rgb(var(--nodedc-danger-rgb));
|
||||
}
|
||||
|
||||
.xgrids-k1-spatial-controls__error strong {
|
||||
font-size: 0.61rem;
|
||||
}
|
||||
|
||||
.xgrids-k1-spatial-controls__error small {
|
||||
overflow: hidden;
|
||||
color: var(--nodedc-text-secondary);
|
||||
font-size: 0.51rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.xgrids-k1-spatial-controls__action-label {
|
||||
display: block;
|
||||
width: 9.75rem;
|
||||
font-size: 0.66rem;
|
||||
line-height: 1.08;
|
||||
text-align: center;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.xgrids-k1-spatial-controls__action-label--local {
|
||||
width: 8.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.xgrids-k1-spatial-controls {min-width:0}
|
||||
.xgrids-k1-spatial-controls__phase small,
|
||||
.xgrids-k1-spatial-controls__telemetry,
|
||||
.xgrids-k1-spatial-controls__error small {display:none}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import type {ReactNode} from 'react';
|
||||
import {ActivityIndicator} from '@nodedc/ui-react';
|
||||
import type {AcquisitionState} from '../spatialSessionModel';
|
||||
import {formatNumber, type K1DeviceTelemetry} from '../spatialSessionModel';
|
||||
import './K1SpatialSession.css';
|
||||
interface PhasePresentation {
|
||||
label: string;
|
||||
detail: string;
|
||||
busy: boolean;
|
||||
}
|
||||
|
||||
export function k1SpatialPhasePresentation(
|
||||
acquisition: {state:AcquisitionState},
|
||||
softwareCommanded: boolean,
|
||||
): PhasePresentation {
|
||||
const presentations: Record<AcquisitionState, PhasePresentation> = {
|
||||
preparing: {
|
||||
label: "Подготовка локального приёма",
|
||||
detail: "Проверка контура и создание сессии записи.",
|
||||
busy: true,
|
||||
},
|
||||
prepared: {
|
||||
label: "Приём подготовлен",
|
||||
detail: softwareCommanded
|
||||
? "Можно инициировать работу устройства из Mission Core."
|
||||
: "Программная команда K1 недоступна в текущем профиле.",
|
||||
busy: false,
|
||||
},
|
||||
awaiting_external_start: {
|
||||
label: softwareCommanded
|
||||
? "K1 калибруется и готовит облако точек"
|
||||
: "Ожидание запуска на устройстве",
|
||||
detail: softwareCommanded
|
||||
? "Первые данные могут появиться через десятки секунд. Не перемещайте устройство."
|
||||
: "Запустите сканирование физической кнопкой K1.",
|
||||
busy: true,
|
||||
},
|
||||
starting: {
|
||||
label: softwareCommanded
|
||||
? "K1 калибруется и готовит облако точек"
|
||||
: "Подготовка локального приёмника",
|
||||
detail: softwareCommanded
|
||||
? "Первые данные могут появиться через десятки секунд. Не перемещайте устройство."
|
||||
: "Mission Core запускает запись до физического старта K1.",
|
||||
busy: true,
|
||||
},
|
||||
acquiring: {
|
||||
label: softwareCommanded ? "K1 работает · запись активна" : "Локальная запись активна",
|
||||
detail: softwareCommanded
|
||||
? "Состояние получено из профилированного контура управления K1."
|
||||
: "Mission Core принимает данные; физическое состояние K1 не управляется программно.",
|
||||
busy: false,
|
||||
},
|
||||
awaiting_external_stop: {
|
||||
label: softwareCommanded ? "K1 завершает и сохраняет" : "Ожидание остановки на устройстве",
|
||||
detail: softwareCommanded
|
||||
? "STOP не повторяется. Mission Core завершит запись после READY от K1."
|
||||
: "Mission Core ждёт подтверждения физической остановки K1.",
|
||||
busy: true,
|
||||
},
|
||||
stopping: {
|
||||
label: softwareCommanded ? "Остановка K1 и записи" : "Остановка локального приёма",
|
||||
detail: softwareCommanded
|
||||
? "Команда отправлена; ожидаем подтверждённое состояние устройства."
|
||||
: "Физическое состояние K1 остаётся неизвестным.",
|
||||
busy: true,
|
||||
},
|
||||
finalizing: {
|
||||
label: "Сохранение локальной записи",
|
||||
detail: "Не закрывайте Mission Core до завершения финализации.",
|
||||
busy: true,
|
||||
},
|
||||
completed: { label: "Приём завершён", detail: "", busy: false },
|
||||
failed: { label: "Ошибка приёма", detail: "", busy: false },
|
||||
aborted: { label: "Приём прерван", detail: "", busy: false },
|
||||
interrupted: { label: "Приём прерван", detail: "", busy: false },
|
||||
};
|
||||
return presentations[acquisition.state];
|
||||
}
|
||||
|
||||
function formatDuration(seconds: number): string {
|
||||
const wholeSeconds = Math.max(0, Math.floor(seconds));
|
||||
const hours = Math.floor(wholeSeconds / 3_600);
|
||||
const minutes = Math.floor((wholeSeconds % 3_600) / 60);
|
||||
const remainder = wholeSeconds % 60;
|
||||
return hours > 0
|
||||
? `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(remainder).padStart(2, "0")}`
|
||||
: `${String(minutes).padStart(2, "0")}:${String(remainder).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
export function K1SpatialSession({phase,telemetry,children}:{phase:PhasePresentation;telemetry:K1DeviceTelemetry;children?:ReactNode}) {
|
||||
return (
|
||||
<section
|
||||
className="xgrids-k1-spatial-controls"
|
||||
aria-label="Управление сессией XGRIDS K1"
|
||||
aria-busy={phase.busy}
|
||||
data-busy={phase.busy ? "true" : undefined}
|
||||
>
|
||||
<div className="xgrids-k1-spatial-controls__phase">
|
||||
{phase.busy ? <ActivityIndicator size="compact" /> : null}
|
||||
<span>
|
||||
<strong>{phase.label}</strong>
|
||||
<small>{phase.detail}</small>
|
||||
</span>
|
||||
</div>
|
||||
<div className="xgrids-k1-spatial-controls__telemetry" aria-label="Телеметрия маршрута K1">
|
||||
{telemetry.elapsedSeconds !== null ? (
|
||||
<span><small>Время сканирования</small><strong>{formatDuration(telemetry.elapsedSeconds)}</strong></span>
|
||||
) : null}
|
||||
{telemetry.routeDistanceMeters !== null ? (
|
||||
<span><small>Маршрут устройства</small><strong>{formatNumber(telemetry.routeDistanceMeters, 2)} м</strong></span>
|
||||
) : null}
|
||||
{telemetry.speedMetersPerSecond !== null ? (
|
||||
<span><small>Скорость</small><strong>{formatNumber(telemetry.speedMetersPerSecond, 2)} м/с</strong></span>
|
||||
) : null}
|
||||
</div>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user