refactor(control-station): separate lidar diagnostics from datasets
This commit is contained in:
parent
f57d64bf52
commit
c01712c30d
|
|
@ -421,6 +421,7 @@ export default function App() {
|
|||
setActiveRoot(rootId);
|
||||
if (rootId === "polygon") {
|
||||
workspace.openView("polygon-run");
|
||||
workspace.openNavigation();
|
||||
return;
|
||||
}
|
||||
workspace.closeView();
|
||||
|
|
@ -652,7 +653,7 @@ export default function App() {
|
|||
className="control-station"
|
||||
data-observation-fullscreen={observationFullscreenActive ? "true" : undefined}
|
||||
navigationOpen={
|
||||
workspace.navigationOpen && activeRoot !== null && activeRoot !== "polygon"
|
||||
workspace.navigationOpen && activeRoot !== null
|
||||
}
|
||||
contentOpen={workspace.contentOpen && activeDefinition !== null}
|
||||
contentExpanded={workspace.contentExpanded}
|
||||
|
|
@ -667,7 +668,7 @@ export default function App() {
|
|||
onOpenDevice={() => openView("local-device")}
|
||||
/>
|
||||
}
|
||||
navigation={currentRoot && activeRoot !== "polygon" ? (
|
||||
navigation={currentRoot ? (
|
||||
<AdminNavigationPanel
|
||||
eyebrow="MISSION CORE"
|
||||
title={currentRoot.title}
|
||||
|
|
@ -746,6 +747,8 @@ export default function App() {
|
|||
</div>
|
||||
) : activeDefinition.kind === "polygon-run" ? (
|
||||
<StatusBadge tone="accent">Virtual only</StatusBadge>
|
||||
) : activeDefinition.kind === "polygon-datasets" ? (
|
||||
<StatusBadge tone="neutral">Offline evaluation</StatusBadge>
|
||||
) : (
|
||||
<StatusBadge tone="warning">Интерфейс готов</StatusBadge>
|
||||
)
|
||||
|
|
@ -775,6 +778,7 @@ export default function App() {
|
|||
onLivePerceptionLayersChange={changeLivePerceptionLayers}
|
||||
observationLayout={observationLayout}
|
||||
polygonRunRoute={polygonRunRoute}
|
||||
deviceLabel={selection?.model.displayName ?? null}
|
||||
spatialControls={selection?.SpatialControlsView
|
||||
? {
|
||||
View: selection.SpatialControlsView,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export type WorkspaceKind =
|
|||
| "missions"
|
||||
| "catalog"
|
||||
| "lidar-quality"
|
||||
| "polygon-datasets"
|
||||
| "polygon-run";
|
||||
|
||||
export type CapabilityStatus = "active" | "ready" | "contract" | "later";
|
||||
|
|
@ -133,9 +134,9 @@ export const roots: RootDefinition[] = [
|
|||
label: "Полигон",
|
||||
title: "Полигон",
|
||||
eyebrow: "СИМУЛЯЦИЯ И КВАЛИФИКАЦИЯ",
|
||||
description: "Живые PX4/Gazebo прогоны, сценарии, траектории и доказательства.",
|
||||
statement: "Проверять поведение машины в воспроизводимой виртуальной среде.",
|
||||
accent: "СИМУЛЯЦИЯ И ДОКАЗАТЕЛЬСТВА",
|
||||
description: "Симуляционные, replay и shadow-прогоны с воспроизводимыми доказательствами.",
|
||||
statement: "Проверять алгоритмы и поведение машины на версионированных входах.",
|
||||
accent: "КВАЛИФИКАЦИЯ И ДОКАЗАТЕЛЬСТВА",
|
||||
},
|
||||
{
|
||||
id: "system",
|
||||
|
|
@ -152,14 +153,25 @@ export const workspaces: WorkspaceDefinition[] = [
|
|||
{
|
||||
id: "polygon-run",
|
||||
root: "polygon",
|
||||
label: "Полигон",
|
||||
title: "Полигон",
|
||||
eyebrow: "ПОЛИГОН / LIVE",
|
||||
description: "Live Simulation Worker и доказательства прогонов PX4/Gazebo.",
|
||||
label: "Прогоны",
|
||||
title: "Прогоны",
|
||||
eyebrow: "ПОЛИГОН / ПРОГОНЫ",
|
||||
description: "Live Simulation Worker, история и доказательства квалификационных прогонов.",
|
||||
icon: "activity",
|
||||
kind: "polygon-run",
|
||||
groups: [],
|
||||
},
|
||||
{
|
||||
id: "polygon-datasets",
|
||||
root: "polygon",
|
||||
label: "Датасеты",
|
||||
title: "Датасеты",
|
||||
eyebrow: "ПОЛИГОН / ДАТАСЕТЫ",
|
||||
description: "Версионированные входы для replay-регрессии и независимой оценки алгоритмов.",
|
||||
icon: "grid",
|
||||
kind: "polygon-datasets",
|
||||
groups: [],
|
||||
},
|
||||
{
|
||||
id: "command-overview",
|
||||
root: "center",
|
||||
|
|
@ -589,12 +601,12 @@ export const workspaces: WorkspaceDefinition[] = [
|
|||
},
|
||||
{
|
||||
id: "lidar-quality",
|
||||
root: "data",
|
||||
label: "Качество LiDAR",
|
||||
title: "Качество LiDAR",
|
||||
eyebrow: "ДАННЫЕ / LIDAR EVIDENCE",
|
||||
root: "fleet",
|
||||
label: "Диагностика LiDAR",
|
||||
title: "Диагностика LiDAR",
|
||||
eyebrow: "ПАРК / СЕНСОРЫ / LIDAR",
|
||||
description:
|
||||
"Проверенные поля сканера, частота, плотность, intensity, pose coverage и допуск следующих алгоритмов.",
|
||||
"Состояние данных и качество записей выбранного LiDAR.",
|
||||
icon: "activity",
|
||||
kind: "lidar-quality",
|
||||
groups: [],
|
||||
|
|
|
|||
|
|
@ -30,6 +30,16 @@
|
|||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.lidar-field-cloud {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.lidar-field-camera {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.lidar-field-camera img {
|
||||
min-height: 0;
|
||||
aspect-ratio: 4 / 3;
|
||||
|
|
@ -106,6 +116,24 @@
|
|||
gap: 0.7rem;
|
||||
}
|
||||
|
||||
.lidar-device-context,
|
||||
.dataset-purpose,
|
||||
.dataset-entry {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.lidar-device-context__verdict {
|
||||
border-top: 1px solid var(--station-hairline);
|
||||
border-left: 0;
|
||||
padding-top: 0.75rem;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.dataset-purpose {
|
||||
align-items: start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.polygon-live-heading {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
|
|
@ -137,15 +165,45 @@
|
|||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.dataset-gateway__representations,
|
||||
.dataset-gateway__grid,
|
||||
.dataset-gateway__footer {
|
||||
.dataset-purpose ol {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dataset-entry dl {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.dataset-entry dl div + div {
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.dataset-entry dl div:nth-child(even) {
|
||||
border-left: 1px solid var(--station-hairline);
|
||||
}
|
||||
|
||||
.dataset-entry dl div:nth-child(n + 3) {
|
||||
border-top: 1px solid var(--station-hairline);
|
||||
}
|
||||
|
||||
.dataset-entry footer {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dataset-contract__content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dataset-gateway__representations > div + div {
|
||||
border-top: 1px solid rgb(255 255 255 / 0.07);
|
||||
border-left: 0;
|
||||
.dataset-contract summary small,
|
||||
.lidar-technical-details > summary small {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.lidar-device-context__verdict {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.control-station .nodedc-header__profile-button {
|
||||
|
|
@ -200,7 +258,8 @@
|
|||
}
|
||||
|
||||
.lidar-ground-review > header,
|
||||
.lidar-ground-review__controls {
|
||||
.lidar-ground-review__controls,
|
||||
.lidar-fallback-review > header {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -807,144 +807,378 @@
|
|||
gap: 1rem;
|
||||
}
|
||||
|
||||
.dataset-gateway {
|
||||
.lidar-device-context {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
overflow: hidden;
|
||||
border: 1px solid color-mix(in srgb, var(--nodedc-accent) 24%, transparent);
|
||||
background:
|
||||
radial-gradient(circle at 10% 0%, color-mix(in srgb, var(--nodedc-accent) 10%, transparent), transparent 32%),
|
||||
rgb(255 255 255 / 0.025);
|
||||
grid-template-columns: minmax(14rem, 0.8fr) minmax(20rem, 1.2fr) auto;
|
||||
align-items: center;
|
||||
gap: 1.1rem;
|
||||
border-radius: 0.9rem;
|
||||
background: var(--station-panel);
|
||||
padding: 0.85rem 1rem;
|
||||
}
|
||||
|
||||
.dataset-gateway__heading p,
|
||||
.dataset-gateway__grid p,
|
||||
.dataset-gateway__pending {
|
||||
margin: 0.4rem 0 0;
|
||||
.lidar-device-context__identity {
|
||||
display: grid;
|
||||
gap: 0.22rem;
|
||||
}
|
||||
|
||||
.lidar-device-context__identity strong {
|
||||
color: var(--nodedc-text-primary);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.lidar-device-context__identity small {
|
||||
overflow: hidden;
|
||||
color: var(--nodedc-text-muted);
|
||||
font-size: 0.6rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.lidar-device-context__verdict {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.7rem;
|
||||
min-width: 0;
|
||||
border-left: 1px solid var(--station-hairline);
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.lidar-device-context__verdict p {
|
||||
margin: 0;
|
||||
color: var(--nodedc-text-muted);
|
||||
font-size: 0.62rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.lidar-technical-details {
|
||||
overflow: hidden;
|
||||
border-radius: 0.9rem;
|
||||
background: var(--station-panel);
|
||||
}
|
||||
|
||||
.lidar-technical-details > summary {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.95rem 1rem;
|
||||
color: var(--nodedc-text-primary);
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.lidar-technical-details > summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.lidar-technical-details > summary::after {
|
||||
content: "+";
|
||||
color: var(--nodedc-text-muted);
|
||||
}
|
||||
|
||||
.lidar-technical-details[open] > summary::after {
|
||||
content: "−";
|
||||
}
|
||||
|
||||
.lidar-technical-details > summary span {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.lidar-technical-details > summary small {
|
||||
color: var(--nodedc-text-muted);
|
||||
font-size: 0.58rem;
|
||||
}
|
||||
|
||||
.lidar-technical-details__content {
|
||||
display: grid;
|
||||
gap: 0.85rem;
|
||||
border-top: 1px solid var(--station-hairline);
|
||||
padding: 0.85rem;
|
||||
}
|
||||
|
||||
.dataset-workspace {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.dataset-purpose {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(20rem, 1fr) auto;
|
||||
align-items: end;
|
||||
gap: 2rem;
|
||||
border-bottom: 1px solid var(--station-hairline);
|
||||
padding: 0.2rem 0.1rem 1.1rem;
|
||||
}
|
||||
|
||||
.dataset-purpose h2,
|
||||
.dataset-purpose p,
|
||||
.dataset-library h2,
|
||||
.dataset-entry h3,
|
||||
.dataset-entry p,
|
||||
.dataset-entry dl,
|
||||
.dataset-contract h3,
|
||||
.dataset-contract p,
|
||||
.dataset-contract ul {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dataset-purpose h2 {
|
||||
margin-top: 0.28rem;
|
||||
color: var(--nodedc-text-primary);
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
.dataset-purpose p {
|
||||
max-width: 54rem;
|
||||
margin-top: 0.45rem;
|
||||
color: var(--nodedc-text-muted);
|
||||
font-size: 0.72rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.dataset-purpose ol {
|
||||
display: flex;
|
||||
gap: 0.9rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.dataset-purpose li {
|
||||
display: grid;
|
||||
gap: 0.22rem;
|
||||
min-width: 6.8rem;
|
||||
color: var(--nodedc-text-secondary);
|
||||
font-size: 0.62rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dataset-purpose li span {
|
||||
color: var(--nodedc-accent);
|
||||
font-size: 0.56rem;
|
||||
letter-spacing: 0.12em;
|
||||
}
|
||||
|
||||
.dataset-state {
|
||||
display: grid;
|
||||
min-height: 12rem;
|
||||
align-content: center;
|
||||
justify-items: start;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.dataset-state h2,
|
||||
.dataset-state p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dataset-state p {
|
||||
color: var(--nodedc-text-muted);
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.dataset-library {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
border: 0;
|
||||
background: var(--station-panel);
|
||||
}
|
||||
|
||||
.dataset-library__heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.dataset-library h2 {
|
||||
margin-top: 0.25rem;
|
||||
color: var(--nodedc-text-primary);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.dataset-entry {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(20rem, 1.2fr) minmax(22rem, 0.8fr);
|
||||
gap: 1.25rem 2rem;
|
||||
border-top: 1px solid var(--station-hairline);
|
||||
padding-top: 1.15rem;
|
||||
}
|
||||
|
||||
.dataset-entry__identity {
|
||||
display: flex;
|
||||
gap: 0.9rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dataset-entry__mark {
|
||||
display: grid;
|
||||
flex: 0 0 auto;
|
||||
width: 2.8rem;
|
||||
height: 2.8rem;
|
||||
place-items: center;
|
||||
border-radius: 0.8rem;
|
||||
background: color-mix(in srgb, var(--nodedc-accent) 13%, transparent);
|
||||
color: var(--nodedc-accent);
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dataset-entry__identity > div > span {
|
||||
color: var(--nodedc-text-muted);
|
||||
font-size: 0.55rem;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.dataset-entry h3 {
|
||||
margin-top: 0.25rem;
|
||||
color: var(--nodedc-text-primary);
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.dataset-entry__identity p {
|
||||
max-width: 48rem;
|
||||
margin-top: 0.45rem;
|
||||
color: var(--nodedc-text-muted);
|
||||
font-size: 0.68rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.dataset-gateway__representations {
|
||||
.dataset-entry dl {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
align-self: start;
|
||||
background: rgb(255 255 255 / 0.025);
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.dataset-entry dl div {
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
padding: 0.78rem;
|
||||
}
|
||||
|
||||
.dataset-entry dl div + div {
|
||||
border-left: 1px solid var(--station-hairline);
|
||||
}
|
||||
|
||||
.dataset-entry dt {
|
||||
color: var(--nodedc-text-muted);
|
||||
font-size: 0.55rem;
|
||||
}
|
||||
|
||||
.dataset-entry dd {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
color: var(--nodedc-text-primary);
|
||||
font-size: 0.64rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dataset-entry footer {
|
||||
display: flex;
|
||||
grid-column: 1 / -1;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
border-top: 1px solid var(--station-hairline);
|
||||
padding-top: 0.9rem;
|
||||
}
|
||||
|
||||
.dataset-entry footer p {
|
||||
color: var(--nodedc-text-muted);
|
||||
font-size: 0.64rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.dataset-entry footer > div {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.dataset-contract {
|
||||
overflow: hidden;
|
||||
background: var(--station-panel);
|
||||
border-radius: 0.9rem;
|
||||
}
|
||||
|
||||
.dataset-contract summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 0.9rem 1rem;
|
||||
color: var(--nodedc-text-primary);
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.dataset-contract summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dataset-contract summary::after {
|
||||
content: "+";
|
||||
color: var(--nodedc-text-muted);
|
||||
}
|
||||
|
||||
.dataset-contract[open] summary::after {
|
||||
content: "−";
|
||||
}
|
||||
|
||||
.dataset-contract summary span {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dataset-contract summary small {
|
||||
color: var(--nodedc-text-muted);
|
||||
font-size: 0.58rem;
|
||||
}
|
||||
|
||||
.dataset-contract__content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
overflow: hidden;
|
||||
border: 1px solid rgb(255 255 255 / 0.07);
|
||||
border-radius: 0.9rem;
|
||||
}
|
||||
|
||||
.dataset-gateway__representations > div {
|
||||
position: relative;
|
||||
display: grid;
|
||||
min-height: 7.4rem;
|
||||
align-content: center;
|
||||
gap: 0.3rem;
|
||||
padding: 1rem 1.1rem 1rem 3.6rem;
|
||||
background: rgb(255 255 255 / 0.025);
|
||||
}
|
||||
|
||||
.dataset-gateway__representations > div + div {
|
||||
border-left: 1px solid rgb(255 255 255 / 0.07);
|
||||
}
|
||||
|
||||
.dataset-gateway__representations > div > span {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
left: 1rem;
|
||||
color: var(--nodedc-accent);
|
||||
font-size: 0.62rem;
|
||||
letter-spacing: 0.14em;
|
||||
}
|
||||
|
||||
.dataset-gateway__representations strong,
|
||||
.dataset-gateway__representations small,
|
||||
.dataset-gateway__representations em {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dataset-gateway__representations strong {
|
||||
color: var(--nodedc-text-primary);
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
.dataset-gateway__representations small,
|
||||
.dataset-gateway__representations em {
|
||||
color: var(--nodedc-text-muted);
|
||||
font-size: 0.58rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.dataset-gateway__representations em {
|
||||
color: color-mix(in srgb, var(--nodedc-accent) 72%, white);
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.dataset-gateway__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.7rem;
|
||||
}
|
||||
|
||||
.dataset-gateway__grid > section {
|
||||
min-width: 0;
|
||||
gap: 1.5rem;
|
||||
border-top: 1px solid var(--station-hairline);
|
||||
padding: 1rem;
|
||||
border: 1px solid rgb(255 255 255 / 0.07);
|
||||
border-radius: 0.9rem;
|
||||
background: rgb(255 255 255 / 0.02);
|
||||
}
|
||||
|
||||
.dataset-gateway__grid > section[data-warning="true"] {
|
||||
border-color: rgb(255 181 71 / 0.2);
|
||||
}
|
||||
|
||||
.dataset-gateway__grid h3 {
|
||||
margin: 0.28rem 0 0;
|
||||
color: var(--nodedc-text-primary);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.dataset-gateway__facts {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
margin-top: 0.8rem;
|
||||
}
|
||||
|
||||
.dataset-gateway__facts span {
|
||||
padding: 0.32rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
background: rgb(255 255 255 / 0.045);
|
||||
color: var(--nodedc-text-secondary);
|
||||
font-size: 0.56rem;
|
||||
}
|
||||
|
||||
.dataset-gateway__footer {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr);
|
||||
gap: 1rem;
|
||||
padding-top: 0.85rem;
|
||||
border-top: 1px solid rgb(255 255 255 / 0.07);
|
||||
}
|
||||
|
||||
.dataset-gateway__footer > div {
|
||||
display: grid;
|
||||
gap: 0.2rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dataset-gateway__footer span,
|
||||
.dataset-gateway__footer small {
|
||||
color: var(--nodedc-text-muted);
|
||||
font-size: 0.58rem;
|
||||
}
|
||||
|
||||
.dataset-gateway__footer strong {
|
||||
overflow-wrap: anywhere;
|
||||
.dataset-contract h3 {
|
||||
color: var(--nodedc-text-primary);
|
||||
font-size: 0.68rem;
|
||||
}
|
||||
|
||||
.dataset-contract p,
|
||||
.dataset-contract li {
|
||||
color: var(--nodedc-text-muted);
|
||||
font-size: 0.6rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.dataset-contract section > p:first-of-type,
|
||||
.dataset-contract ul {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.dataset-contract ul {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.dataset-contract li {
|
||||
display: grid;
|
||||
gap: 0.12rem;
|
||||
}
|
||||
|
||||
.dataset-contract li strong {
|
||||
color: var(--nodedc-text-secondary);
|
||||
}
|
||||
|
||||
.lidar-quality-message {
|
||||
display: grid;
|
||||
min-height: 12rem;
|
||||
|
|
@ -1093,11 +1327,11 @@
|
|||
}
|
||||
|
||||
.lidar-field-review {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
border-color: rgb(74 215 255 / 0.22);
|
||||
background:
|
||||
radial-gradient(circle at 8% 0%, rgb(56 124 255 / 0.13), transparent 30rem),
|
||||
var(--station-panel);
|
||||
gap: 0.22rem;
|
||||
border: 0;
|
||||
background: var(--station-panel);
|
||||
}
|
||||
|
||||
.lidar-field-review__heading p,
|
||||
|
|
@ -1121,8 +1355,6 @@
|
|||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 0.28rem;
|
||||
border: 1px solid var(--station-hairline);
|
||||
border-radius: 0.72rem;
|
||||
background: rgb(255 255 255 / 0.025);
|
||||
padding: 0.64rem 0.7rem;
|
||||
}
|
||||
|
|
@ -1169,8 +1401,8 @@
|
|||
|
||||
.lidar-field-window-list > button:hover,
|
||||
.lidar-field-window-list > button[data-selected="true"] {
|
||||
border-color: rgb(74 215 255 / 0.48);
|
||||
background: rgb(74 215 255 / 0.08);
|
||||
border-color: color-mix(in srgb, var(--nodedc-accent) 45%, transparent);
|
||||
background: color-mix(in srgb, var(--nodedc-accent) 7%, transparent);
|
||||
}
|
||||
|
||||
.lidar-field-window-list > button > span {
|
||||
|
|
@ -1180,8 +1412,8 @@
|
|||
height: 1.35rem;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
background: rgb(74 215 255 / 0.12);
|
||||
color: #78e3ff;
|
||||
background: color-mix(in srgb, var(--nodedc-accent) 12%, transparent);
|
||||
color: var(--nodedc-accent);
|
||||
font-size: 0.59rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
|
@ -1200,7 +1432,7 @@
|
|||
|
||||
.lidar-field-stage {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(18rem, 0.68fr) minmax(0, 1.32fr);
|
||||
grid-template-columns: minmax(0, 1.45fr) minmax(18rem, 0.55fr);
|
||||
gap: 0.65rem;
|
||||
margin-top: 0.7rem;
|
||||
}
|
||||
|
|
@ -1216,9 +1448,16 @@
|
|||
|
||||
.lidar-field-camera {
|
||||
display: grid;
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.lidar-field-cloud {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.lidar-field-camera > .lidar-field-stage__label,
|
||||
.lidar-field-cloud > header {
|
||||
min-height: 3.45rem;
|
||||
|
|
@ -1304,6 +1543,39 @@
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.lidar-fallback-review {
|
||||
display: grid;
|
||||
overflow: hidden;
|
||||
margin-top: 0.8rem;
|
||||
border-radius: 0.9rem;
|
||||
background: #071018;
|
||||
}
|
||||
|
||||
.lidar-fallback-review > header {
|
||||
display: flex;
|
||||
min-height: 3.6rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.8rem;
|
||||
border-bottom: 1px solid rgb(255 255 255 / 0.08);
|
||||
padding: 0.65rem 0.75rem;
|
||||
}
|
||||
|
||||
.lidar-fallback-review .lidar-ground-scene,
|
||||
.lidar-fallback-review .lidar-ground-scene-placeholder {
|
||||
min-height: 34rem;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.lidar-fallback-review > footer {
|
||||
border-top: 1px solid rgb(255 255 255 / 0.08);
|
||||
padding: 0.58rem 0.75rem;
|
||||
color: var(--nodedc-text-muted);
|
||||
font-size: 0.58rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.lidar-ground-benchmark {
|
||||
min-width: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,133 +0,0 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { GlassSurface, StatusBadge } from "@nodedc/ui-react";
|
||||
|
||||
import {
|
||||
fetchDatasetGatewayCatalog,
|
||||
type DatasetGatewayCatalog,
|
||||
} from "../core/lidar/datasetGateway";
|
||||
|
||||
const representationLabels: Record<string, string> = {
|
||||
"native-scan": "Один оборот / скан",
|
||||
"normalized-scan": "Deskew + bounded cleanup",
|
||||
"rolling-local-map": "Pose + TTL + voxel map",
|
||||
};
|
||||
|
||||
export function DatasetGatewayPanel() {
|
||||
const [catalog, setCatalog] = useState<DatasetGatewayCatalog | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
void fetchDatasetGatewayCatalog({ signal: controller.signal })
|
||||
.then((value) => {
|
||||
if (!controller.signal.aborted) setCatalog(value);
|
||||
})
|
||||
.catch((loadError: unknown) => {
|
||||
if (controller.signal.aborted) return;
|
||||
setError(
|
||||
loadError instanceof Error
|
||||
? loadError.message
|
||||
: "Dataset Gateway недоступен",
|
||||
);
|
||||
});
|
||||
return () => controller.abort();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<GlassSurface className="dataset-gateway" padding="lg">
|
||||
<header className="panel-heading dataset-gateway__heading">
|
||||
<div>
|
||||
<span className="section-eyebrow">DATASET GATEWAY · S0</span>
|
||||
<h2>Три разных LiDAR-продукта</h2>
|
||||
<p>
|
||||
Кольцевой одиночный скан, очищенный sensor-frame и накопленная карта
|
||||
больше не считаются одним облаком.
|
||||
</p>
|
||||
</div>
|
||||
<StatusBadge
|
||||
tone={error ? "danger" : catalog?.storage.admitted ? "success" : "warning"}
|
||||
>
|
||||
{error
|
||||
? "Gateway недоступен"
|
||||
: catalog?.storage.admitted
|
||||
? "D: допущен"
|
||||
: "Ожидает D:"}
|
||||
</StatusBadge>
|
||||
</header>
|
||||
|
||||
{catalog ? (
|
||||
<>
|
||||
<div className="dataset-gateway__representations">
|
||||
{catalog.representations.map((representation, index) => (
|
||||
<div key={representation.id}>
|
||||
<span>0{index + 1}</span>
|
||||
<strong>
|
||||
{representationLabels[representation.id] ?? representation.title}
|
||||
</strong>
|
||||
<small>{representation.title}</small>
|
||||
<em>
|
||||
{representation.accumulation
|
||||
? "накопление включено явно"
|
||||
: "без накопления"}
|
||||
</em>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="dataset-gateway__grid">
|
||||
<section>
|
||||
<span className="section-eyebrow">ПЕРВЫЙ BASELINE</span>
|
||||
<h3>{catalog.source.displayName}</h3>
|
||||
<p>
|
||||
Один оборот VLS-128 в SemanticKITTI XYZI + point-wise semantic и
|
||||
instance labels. Это то самое разреженное кольцевое облако,
|
||||
которое корректно сравнивать с алгоритмами.
|
||||
</p>
|
||||
<div className="dataset-gateway__facts">
|
||||
<span>{catalog.source.platforms.join(" · ")}</span>
|
||||
<span>{catalog.source.superclasses.length} superclasses</span>
|
||||
<span>val {catalog.source.validationArchiveGb} ГБ</span>
|
||||
<span>{catalog.source.license}</span>
|
||||
</div>
|
||||
</section>
|
||||
<section data-warning="true">
|
||||
<span className="section-eyebrow">ТЕКУЩИЙ DEVICE INPUT</span>
|
||||
<h3>Mapped feed ≠ raw scan</h3>
|
||||
<p>
|
||||
Внешний MQTT содержит vendor-mapped increment после LIO. В нём
|
||||
нет per-point time и line/ring, поэтому из него нельзя честно
|
||||
восстановить один исходный скан или выполнить deskew.
|
||||
</p>
|
||||
<div className="dataset-gateway__facts">
|
||||
<span>Patchwork++: diagnostic only</span>
|
||||
<span>rolling map: возможно</span>
|
||||
<span>raw reconstruction: невозможно</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<footer className="dataset-gateway__footer">
|
||||
<div>
|
||||
<span>Storage gate</span>
|
||||
<strong>{catalog.storage.requiredWindowsRoot}</strong>
|
||||
<small>{catalog.storage.requiredWslRoot}</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>Следующий исполнимый шаг</span>
|
||||
<strong>
|
||||
{catalog.storage.admitted
|
||||
? "Скачать GOOSE validation и импортировать первый кадр"
|
||||
: "Подключить Dataset Root на D: worker"}
|
||||
</strong>
|
||||
<small>Автозагрузка 3.3 ГБ намеренно запрещена</small>
|
||||
</div>
|
||||
</footer>
|
||||
</>
|
||||
) : (
|
||||
<p className="dataset-gateway__pending">
|
||||
{error ?? "Читаем входные контракты Dataset Gateway…"}
|
||||
</p>
|
||||
)}
|
||||
</GlassSurface>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
GlassSurface,
|
||||
StatusBadge,
|
||||
} from "@nodedc/ui-react";
|
||||
|
||||
import {
|
||||
fetchDatasetGatewayCatalog,
|
||||
type DatasetGatewayCatalog,
|
||||
} from "../core/lidar/datasetGateway";
|
||||
|
||||
const representationLabels: Record<string, string> = {
|
||||
"native-scan": "Исходный скан",
|
||||
"normalized-scan": "Нормализованный скан",
|
||||
"rolling-local-map": "Локальная карта",
|
||||
};
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
return error instanceof Error && error.message.trim()
|
||||
? error.message
|
||||
: "Каталог датасетов недоступен.";
|
||||
}
|
||||
|
||||
export function DatasetGatewayWorkspace() {
|
||||
const [catalog, setCatalog] = useState<DatasetGatewayCatalog | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [reloadGeneration, setReloadGeneration] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
void fetchDatasetGatewayCatalog({ signal: controller.signal })
|
||||
.then((value) => {
|
||||
if (!controller.signal.aborted) setCatalog(value);
|
||||
})
|
||||
.catch((loadError: unknown) => {
|
||||
if (controller.signal.aborted) return;
|
||||
setCatalog(null);
|
||||
setError(errorMessage(loadError));
|
||||
})
|
||||
.finally(() => {
|
||||
if (!controller.signal.aborted) setLoading(false);
|
||||
});
|
||||
return () => controller.abort();
|
||||
}, [reloadGeneration]);
|
||||
|
||||
return (
|
||||
<div className="standard-workspace dataset-workspace">
|
||||
<section className="dataset-purpose" aria-label="Назначение датасетов">
|
||||
<div>
|
||||
<span className="section-eyebrow">ЗАЧЕМ ЭТО НУЖНО</span>
|
||||
<h2>Независимая проверка алгоритмов</h2>
|
||||
<p>
|
||||
Датасет — это вход квалификационного прогона. Мы подаём известные
|
||||
размеченные данные в выбранный pipeline, измеряем ошибки и сохраняем
|
||||
воспроизводимое решение. Он не подмешивается в диагностику
|
||||
реального сенсора и не
|
||||
заменяет closed-loop симуляцию.
|
||||
</p>
|
||||
</div>
|
||||
<ol aria-label="Последовательность квалификации">
|
||||
<li><span>01</span>Выбрать данные</li>
|
||||
<li><span>02</span>Запустить pipeline</li>
|
||||
<li><span>03</span>Сравнить с разметкой</li>
|
||||
<li><span>04</span>Принять решение</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
{loading && !catalog ? (
|
||||
<GlassSurface className="dataset-state" padding="lg">
|
||||
<StatusBadge tone="accent">Проверяем каталог</StatusBadge>
|
||||
<h2>Читаем допущенные источники</h2>
|
||||
<p>Ни один большой архив не загружается автоматически.</p>
|
||||
</GlassSurface>
|
||||
) : error ? (
|
||||
<GlassSurface className="dataset-state" padding="lg">
|
||||
<StatusBadge tone="danger">Каталог недоступен</StatusBadge>
|
||||
<h2>Датасеты не открыты</h2>
|
||||
<p>{error}</p>
|
||||
<Button
|
||||
size="compact"
|
||||
variant="secondary"
|
||||
onClick={() => setReloadGeneration((value) => value + 1)}
|
||||
>
|
||||
Повторить
|
||||
</Button>
|
||||
</GlassSurface>
|
||||
) : catalog ? (
|
||||
<>
|
||||
<GlassSurface className="dataset-library" padding="lg">
|
||||
<header className="dataset-library__heading">
|
||||
<div>
|
||||
<span className="section-eyebrow">КАТАЛОГ</span>
|
||||
<h2>Доступные источники</h2>
|
||||
</div>
|
||||
<StatusBadge tone={catalog.storage.admitted ? "success" : "warning"}>
|
||||
{catalog.storage.admitted ? "Worker готов" : "Worker не настроен"}
|
||||
</StatusBadge>
|
||||
</header>
|
||||
|
||||
<article className="dataset-entry">
|
||||
<div className="dataset-entry__identity">
|
||||
<span className="dataset-entry__mark">G</span>
|
||||
<div>
|
||||
<span>PUBLIC · OFF-ROAD · LABELED</span>
|
||||
<h3>{catalog.source.displayName}</h3>
|
||||
<p>
|
||||
Один оборот VLS-128 с point-wise semantic и instance labels.
|
||||
Первый baseline для ground segmentation и последующих
|
||||
LiDAR-native моделей.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<dl>
|
||||
<div>
|
||||
<dt>Состояние</dt>
|
||||
<dd>Не загружен</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Validation</dt>
|
||||
<dd>{catalog.source.validationArchiveGb} ГБ</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Разметка</dt>
|
||||
<dd>{catalog.source.superclasses.length} классов</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Лицензия</dt>
|
||||
<dd>{catalog.source.license}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<footer>
|
||||
<p>
|
||||
{catalog.storage.admitted
|
||||
? "Следующий шаг: загрузить validation archive на worker и проверить hash/license."
|
||||
: "Сначала нужно допустить Dataset Root на диске D worker. Сейчас открывать нечего."}
|
||||
</p>
|
||||
<div>
|
||||
<Button
|
||||
size="compact"
|
||||
variant="secondary"
|
||||
disabled
|
||||
title="Первый реальный кадр ещё не импортирован"
|
||||
>
|
||||
Открыть
|
||||
</Button>
|
||||
<Button
|
||||
size="compact"
|
||||
disabled
|
||||
title="Прогон станет доступен после импорта датасета"
|
||||
>
|
||||
Создать прогон
|
||||
</Button>
|
||||
</div>
|
||||
</footer>
|
||||
</article>
|
||||
</GlassSurface>
|
||||
|
||||
<details className="dataset-contract">
|
||||
<summary>
|
||||
<span>Технический контракт</span>
|
||||
<small>Представления, storage gate и граница реального сенсора</small>
|
||||
</summary>
|
||||
<div className="dataset-contract__content">
|
||||
<section>
|
||||
<h3>Представления</h3>
|
||||
<ul>
|
||||
{catalog.representations.map((representation) => (
|
||||
<li key={representation.id}>
|
||||
<strong>
|
||||
{representationLabels[representation.id] ?? representation.title}
|
||||
</strong>
|
||||
<span>{representation.title}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
<section>
|
||||
<h3>Storage gate</h3>
|
||||
<p>{catalog.storage.requiredWindowsRoot}</p>
|
||||
<p>{catalog.storage.requiredWslRoot}</p>
|
||||
</section>
|
||||
<section>
|
||||
<h3>Граница реального сенсора</h3>
|
||||
<p>
|
||||
Текущий MQTT-вход — vendor-mapped increment, а не native scan.
|
||||
Отсутствующие per-point time и ring/line не восстанавливаются.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</details>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -26,6 +26,16 @@ interface LidarGroundPointCloudProps {
|
|||
mode: LidarGroundViewMode;
|
||||
}
|
||||
|
||||
function percentile(values: number[], fraction: number): number {
|
||||
if (!values.length) return 0;
|
||||
const sorted = [...values].sort((left, right) => left - right);
|
||||
const index = Math.min(
|
||||
sorted.length - 1,
|
||||
Math.max(0, Math.floor((sorted.length - 1) * fraction)),
|
||||
);
|
||||
return sorted[index];
|
||||
}
|
||||
|
||||
function setRgb(
|
||||
target: Float32Array,
|
||||
offset: number,
|
||||
|
|
@ -101,6 +111,7 @@ export function LidarGroundPointCloud({
|
|||
const controlsRef = useRef<OrbitControls | null>(null);
|
||||
const fogRef = useRef<THREE.FogExp2 | null>(null);
|
||||
const gridRef = useRef<THREE.GridHelper | null>(null);
|
||||
const viewRadiusRef = useRef(1);
|
||||
const [renderError, setRenderError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -123,7 +134,7 @@ export function LidarGroundPointCloud({
|
|||
renderer.setClearColor(0x071018, 0.96);
|
||||
renderer.domElement.setAttribute(
|
||||
"aria-label",
|
||||
"Интерактивное облако ground segmentation",
|
||||
"Интерактивное облако LiDAR",
|
||||
);
|
||||
host.prepend(renderer.domElement);
|
||||
|
||||
|
|
@ -161,7 +172,7 @@ export function LidarGroundPointCloud({
|
|||
materialRef.current = material;
|
||||
scene.add(new THREE.Points(geometry, material));
|
||||
|
||||
const grid = new THREE.GridHelper(24, 48, 0x3c7cff, 0x233747);
|
||||
const grid = new THREE.GridHelper(10, 40, 0x71963d, 0x26312a);
|
||||
gridRef.current = grid;
|
||||
const gridMaterials = Array.isArray(grid.material)
|
||||
? grid.material
|
||||
|
|
@ -216,6 +227,7 @@ export function LidarGroundPointCloud({
|
|||
controlsRef.current = null;
|
||||
fogRef.current = null;
|
||||
gridRef.current = null;
|
||||
viewRadiusRef.current = 1;
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
|
@ -229,20 +241,22 @@ export function LidarGroundPointCloud({
|
|||
if (!geometry || !material || !camera || !controls || !fog || !grid) return;
|
||||
|
||||
const positions = new Float32Array(frame.pointCount * 3);
|
||||
let minimumX = Number.POSITIVE_INFINITY;
|
||||
let maximumX = Number.NEGATIVE_INFINITY;
|
||||
let minimumY = Number.POSITIVE_INFINITY;
|
||||
let maximumY = Number.NEGATIVE_INFINITY;
|
||||
let minimumZ = Number.POSITIVE_INFINITY;
|
||||
let maximumZ = Number.NEGATIVE_INFINITY;
|
||||
frame.pointsXyzM.forEach(([x, y, z]) => {
|
||||
minimumX = Math.min(minimumX, x);
|
||||
maximumX = Math.max(maximumX, x);
|
||||
minimumY = Math.min(minimumY, y);
|
||||
maximumY = Math.max(maximumY, y);
|
||||
minimumZ = Math.min(minimumZ, z);
|
||||
maximumZ = Math.max(maximumZ, z);
|
||||
const sampleStep = Math.max(1, Math.floor(frame.pointCount / 5_000));
|
||||
const sampleX: number[] = [];
|
||||
const sampleY: number[] = [];
|
||||
const sampleZ: number[] = [];
|
||||
frame.pointsXyzM.forEach(([x, y, z], index) => {
|
||||
if (index % sampleStep !== 0) return;
|
||||
sampleX.push(x);
|
||||
sampleY.push(y);
|
||||
sampleZ.push(z);
|
||||
});
|
||||
const minimumX = percentile(sampleX, 0.01);
|
||||
const maximumX = percentile(sampleX, 0.99);
|
||||
const minimumY = percentile(sampleY, 0.01);
|
||||
const maximumY = percentile(sampleY, 0.99);
|
||||
const minimumZ = percentile(sampleZ, 0.01);
|
||||
const maximumZ = percentile(sampleZ, 0.99);
|
||||
const centerX = (minimumX + maximumX) / 2;
|
||||
const centerY = (minimumY + maximumY) / 2;
|
||||
frame.pointsXyzM.forEach(([x, y, z], index) => {
|
||||
|
|
@ -253,13 +267,21 @@ export function LidarGroundPointCloud({
|
|||
});
|
||||
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
|
||||
geometry.computeBoundingSphere();
|
||||
const radius = Math.max(geometry.boundingSphere?.radius ?? 1, 0.2);
|
||||
const radius = Math.max(
|
||||
Math.hypot(
|
||||
(maximumX - minimumX) / 2,
|
||||
(maximumY - minimumY) / 2,
|
||||
maximumZ - minimumZ,
|
||||
),
|
||||
0.2,
|
||||
);
|
||||
viewRadiusRef.current = radius;
|
||||
material.size = THREE.MathUtils.clamp(radius / 155, 0.014, 0.075);
|
||||
fog.density = THREE.MathUtils.clamp(0.18 / radius, 0.0008, 0.035);
|
||||
grid.scale.setScalar(Math.max(radius / 12, 1));
|
||||
grid.scale.setScalar(Math.max(radius / 5, 0.25));
|
||||
|
||||
const targetHeight = Math.max((maximumZ - minimumZ) * 0.35, 0.15);
|
||||
const distance = Math.max(radius * 1.8, 1.2);
|
||||
const targetHeight = Math.max((maximumZ - minimumZ) * 0.42, 0.15);
|
||||
const distance = Math.max(radius * 1.15, 0.9);
|
||||
controls.target.set(0, targetHeight, 0);
|
||||
camera.position.set(distance, distance * 0.72, distance);
|
||||
camera.near = Math.max(distance / 1_000, 0.005);
|
||||
|
|
@ -283,10 +305,10 @@ export function LidarGroundPointCloud({
|
|||
const camera = cameraRef.current;
|
||||
const controls = controlsRef.current;
|
||||
if (!geometry || !camera || !controls) return;
|
||||
const radius = Math.max(geometry.boundingSphere?.radius ?? 1, 0.2);
|
||||
const distance = Math.max(radius * 1.8, 1.2);
|
||||
const radius = Math.max(viewRadiusRef.current, 0.2);
|
||||
const distance = Math.max(radius * 1.15, 0.9);
|
||||
camera.position.set(distance, distance * 0.72, distance);
|
||||
controls.target.set(0, radius * 0.18, 0);
|
||||
controls.target.set(0, radius * 0.25, 0);
|
||||
controls.update();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -21,12 +21,10 @@ import {
|
|||
type LidarStageReadiness,
|
||||
} from "../core/lidar/replayQuality";
|
||||
import { MetricCard } from "../components/MetricCard";
|
||||
import type { WorkspaceDefinition } from "../productModel";
|
||||
import {
|
||||
LidarGroundPointCloud,
|
||||
type LidarGroundViewMode,
|
||||
} from "./LidarGroundPointCloud";
|
||||
import { DatasetGatewayPanel } from "./DatasetGatewayPanel";
|
||||
|
||||
function formatNumber(value: number | null, digits = 1): string {
|
||||
if (value === null) return "—";
|
||||
|
|
@ -60,9 +58,11 @@ function errorMessage(error: unknown): string {
|
|||
}
|
||||
|
||||
export function LidarQualityWorkspace({
|
||||
definition,
|
||||
deviceLabel,
|
||||
onOpenObservation,
|
||||
}: {
|
||||
definition: WorkspaceDefinition;
|
||||
deviceLabel: string | null;
|
||||
onOpenObservation: () => void;
|
||||
}) {
|
||||
const [catalog, setCatalog] = useState<LidarReplayCatalog | null>(null);
|
||||
const [detail, setDetail] = useState<LidarReplayDetail | null>(null);
|
||||
|
|
@ -73,7 +73,7 @@ export function LidarQualityWorkspace({
|
|||
const [groundFrameLoading, setGroundFrameLoading] = useState(false);
|
||||
const [groundFrameError, setGroundFrameError] = useState<string | null>(null);
|
||||
const [groundViewMode, setGroundViewMode] =
|
||||
useState<LidarGroundViewMode>("disagreement");
|
||||
useState<LidarGroundViewMode>("intensity");
|
||||
const [fieldReview, setFieldReview] = useState<LidarFieldReview | null>(null);
|
||||
const [fieldWindow, setFieldWindow] =
|
||||
useState<LidarFieldReviewWindow | null>(null);
|
||||
|
|
@ -81,7 +81,7 @@ export function LidarQualityWorkspace({
|
|||
const [fieldLoading, setFieldLoading] = useState(true);
|
||||
const [fieldError, setFieldError] = useState<string | null>(null);
|
||||
const [fieldViewMode, setFieldViewMode] =
|
||||
useState<LidarGroundViewMode>("disagreement");
|
||||
useState<LidarGroundViewMode>("intensity");
|
||||
const [selectedPackId, setSelectedPackId] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
|
@ -116,8 +116,11 @@ export function LidarQualityWorkspace({
|
|||
if (controller.signal.aborted) return;
|
||||
setSelectedPackId(target);
|
||||
setDetail(nextDetail);
|
||||
setGroundBenchmark(groundCatalog.items[0] ?? null);
|
||||
setGroundFrameIndex(0);
|
||||
const nextGroundBenchmark = groundCatalog.items[0] ?? null;
|
||||
setGroundBenchmark(nextGroundBenchmark);
|
||||
setGroundFrameIndex(
|
||||
nextGroundBenchmark ? Math.floor(nextGroundBenchmark.frames / 2) : 0,
|
||||
);
|
||||
} catch (loadError) {
|
||||
if (controller.signal.aborted) return;
|
||||
setDetail(null);
|
||||
|
|
@ -212,17 +215,6 @@ export function LidarQualityWorkspace({
|
|||
|
||||
return (
|
||||
<div className="standard-workspace lidar-quality-workspace">
|
||||
<section className="workspace-lead workspace-lead--compact">
|
||||
<div>
|
||||
<span className="section-eyebrow">{definition.eyebrow}</span>
|
||||
<h2>{definition.title}</h2>
|
||||
<p>{definition.description}</p>
|
||||
</div>
|
||||
<span className="workspace-lead__note">Только проверенные replay-артефакты</span>
|
||||
</section>
|
||||
|
||||
<DatasetGatewayPanel />
|
||||
|
||||
{loading && !detail ? (
|
||||
<GlassSurface className="lidar-quality-message" padding="lg">
|
||||
<StatusBadge tone="accent">Проверка evidence</StatusBadge>
|
||||
|
|
@ -255,22 +247,49 @@ export function LidarQualityWorkspace({
|
|||
</GlassSurface>
|
||||
) : (
|
||||
<>
|
||||
<section className="lidar-device-context" aria-label="Выбранный LiDAR и запись">
|
||||
<div className="lidar-device-context__identity">
|
||||
<span className="section-eyebrow">ВЫБРАННЫЙ СЕНСОР</span>
|
||||
<strong>{deviceLabel ?? "Архивный источник"} · LiDAR</strong>
|
||||
<small>
|
||||
Запись {fieldReview?.sessionId ?? detail.pack.sessionId}
|
||||
</small>
|
||||
</div>
|
||||
<div className="lidar-device-context__verdict">
|
||||
<StatusBadge tone="warning">Диагностическая запись</StatusBadge>
|
||||
<p>
|
||||
Данные воспроизводятся и доступны для визуального разбора.
|
||||
Физическая точность сенсора этой записью не аттестована.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
size="compact"
|
||||
variant="secondary"
|
||||
onClick={onOpenObservation}
|
||||
>
|
||||
Открыть наблюдение
|
||||
</Button>
|
||||
</section>
|
||||
|
||||
<GlassSurface className="lidar-field-review" padding="lg">
|
||||
<header className="panel-heading lidar-field-review__heading">
|
||||
<div>
|
||||
<span className="section-eyebrow">ПОЛЕВОЙ REVIEW · RAVNOVES00</span>
|
||||
<span className="section-eyebrow">
|
||||
{fieldReview ? "ОБЛАКО LIDAR · RAVNOVES00" : "ОБЛАКО LIDAR · ЗАПИСЬ"}
|
||||
</span>
|
||||
<h2>
|
||||
{fieldReview?.displayName
|
||||
?? "Центральный городской интервал"}
|
||||
{fieldReview
|
||||
? `Городская сцена: ${selectedFieldWindow?.label ?? fieldReview.displayName}`
|
||||
: `Кадр ${groundFrameIndex + 1} из ${groundBenchmark?.frames ?? 0}`}
|
||||
</h2>
|
||||
<p>
|
||||
Дорога, дома, автомобили и растительность. Облако накоплено
|
||||
по исходным LiDAR-сэмплам выбранного окна, а кадр камеры
|
||||
фиксирует контекст сцены.
|
||||
{fieldReview
|
||||
? "Выберите один из пяти интервалов, вращайте облако мышью и при необходимости включите экспериментальные маски обработки."
|
||||
: "Полевая запись на этом хосте не подключена. Показываем доступную запись выбранного LiDAR без подмешивания публичных датасетов."}
|
||||
</p>
|
||||
</div>
|
||||
<StatusBadge tone={fieldError ? "danger" : "warning"}>
|
||||
{fieldError ? "Review недоступен" : "Visual review only"}
|
||||
{fieldError ? "Облако недоступно" : "Только собственные данные"}
|
||||
</StatusBadge>
|
||||
</header>
|
||||
|
||||
|
|
@ -389,6 +408,7 @@ export function LidarQualityWorkspace({
|
|||
aria-label="Режим окраски полевого LiDAR"
|
||||
>
|
||||
{([
|
||||
["intensity", "Облако"],
|
||||
["current", "Current"],
|
||||
["candidate", "Patchwork++"],
|
||||
["disagreement", "Расхождения"],
|
||||
|
|
@ -441,6 +461,87 @@ export function LidarQualityWorkspace({
|
|||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : groundBenchmark ? (
|
||||
<section
|
||||
className="lidar-fallback-review"
|
||||
aria-label="Доступная запись LiDAR"
|
||||
>
|
||||
<header>
|
||||
<div
|
||||
className="lidar-ground-modes"
|
||||
role="group"
|
||||
aria-label="Режим окраски LiDAR"
|
||||
>
|
||||
{([
|
||||
["intensity", "Облако"],
|
||||
["current", "Current"],
|
||||
["candidate", "Patchwork++"],
|
||||
["disagreement", "Расхождения"],
|
||||
] as const).map(([mode, label]) => (
|
||||
<button
|
||||
type="button"
|
||||
key={mode}
|
||||
data-active={groundViewMode === mode ? "true" : undefined}
|
||||
onClick={() => setGroundViewMode(mode)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="lidar-ground-frame-control">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Предыдущий LiDAR кадр"
|
||||
disabled={groundFrameIndex === 0}
|
||||
onClick={() =>
|
||||
setGroundFrameIndex((value) => Math.max(0, value - 1))
|
||||
}
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<input
|
||||
type="range"
|
||||
aria-label="Номер LiDAR кадра"
|
||||
min={0}
|
||||
max={Math.max(groundBenchmark.frames - 1, 0)}
|
||||
step={1}
|
||||
value={groundFrameIndex}
|
||||
onChange={(event) =>
|
||||
setGroundFrameIndex(Number(event.currentTarget.value))
|
||||
}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Следующий LiDAR кадр"
|
||||
disabled={groundFrameIndex >= groundBenchmark.frames - 1}
|
||||
onClick={() =>
|
||||
setGroundFrameIndex((value) =>
|
||||
Math.min(groundBenchmark.frames - 1, value + 1)
|
||||
)
|
||||
}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
{groundFrame ? (
|
||||
<LidarGroundPointCloud
|
||||
frame={groundFrame}
|
||||
mode={groundViewMode}
|
||||
/>
|
||||
) : (
|
||||
<div className="lidar-ground-scene-placeholder">
|
||||
<StatusBadge tone={groundFrameError ? "danger" : "accent"}>
|
||||
{groundFrameError ? "Кадр недоступен" : "Загрузка"}
|
||||
</StatusBadge>
|
||||
<p>{groundFrameError ?? "Читаем облако LiDAR…"}</p>
|
||||
</div>
|
||||
)}
|
||||
<footer>
|
||||
Это техническая запись из 66 кадров. Она позволяет проверить
|
||||
геометрию и управление viewer, но не заменяет полевую сцену.
|
||||
</footer>
|
||||
</section>
|
||||
) : (
|
||||
<div className="lidar-field-review__empty">
|
||||
<StatusBadge tone={fieldError ? "danger" : "accent"}>
|
||||
|
|
@ -481,6 +582,14 @@ export function LidarQualityWorkspace({
|
|||
/>
|
||||
</section>
|
||||
|
||||
<details className="lidar-technical-details">
|
||||
<summary>
|
||||
<span>Технические детали и эксперименты</span>
|
||||
<small>
|
||||
Целостность записи, допуски стадий, Patchwork++ и contract slice
|
||||
</small>
|
||||
</summary>
|
||||
<div className="lidar-technical-details__content">
|
||||
<div className="lidar-quality-grid">
|
||||
<GlassSurface className="lidar-quality-panel" padding="lg">
|
||||
<header className="panel-heading">
|
||||
|
|
@ -813,6 +922,8 @@ export function LidarQualityWorkspace({
|
|||
))}
|
||||
</footer>
|
||||
</GlassSurface>
|
||||
</div>
|
||||
</details>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ import { finiteMetric, formatNumber, pipelineLatency, sourceModeLabel } from "..
|
|||
import type { SceneSettings } from "../sceneSettings";
|
||||
import { PolygonRunWorkspace } from "./PolygonRunWorkspace";
|
||||
import { LidarQualityWorkspace } from "./LidarQualityWorkspace";
|
||||
import { DatasetGatewayWorkspace } from "./DatasetGatewayWorkspace";
|
||||
|
||||
function statusTone(status: CapabilityStatus): "success" | "accent" | "warning" | "neutral" {
|
||||
if (status === "active") return "success";
|
||||
|
|
@ -140,6 +141,7 @@ export interface WorkspaceRendererProps {
|
|||
}) => void;
|
||||
observationLayout: ObservationLayoutController;
|
||||
polygonRunRoute: PolygonRunRoute;
|
||||
deviceLabel: string | null;
|
||||
navigation: WorkspaceNavigation;
|
||||
spatialControls: {
|
||||
View: ComponentType<DevicePluginConnectionProps>;
|
||||
|
|
@ -1308,7 +1310,14 @@ export function WorkspaceRenderer(props: WorkspaceRendererProps) {
|
|||
case "catalog":
|
||||
return <CatalogWorkspace {...props} />;
|
||||
case "lidar-quality":
|
||||
return <LidarQualityWorkspace definition={props.definition} />;
|
||||
return (
|
||||
<LidarQualityWorkspace
|
||||
deviceLabel={props.deviceLabel}
|
||||
onOpenObservation={() => props.navigation.openView("spatial-scene")}
|
||||
/>
|
||||
);
|
||||
case "polygon-datasets":
|
||||
return <DatasetGatewayWorkspace />;
|
||||
case "polygon-run":
|
||||
return <PolygonRunWorkspace route={props.polygonRunRoute} />;
|
||||
case "device":
|
||||
|
|
|
|||
|
|
@ -610,6 +610,6 @@ test("LiDAR fetchers use read-only endpoints and workspace is registered", async
|
|||
method: "GET",
|
||||
},
|
||||
]);
|
||||
assert.equal(workspaceById("lidar-quality").root, "data");
|
||||
assert.equal(workspaceById("lidar-quality").root, "fleet");
|
||||
assert.equal(workspaceById("lidar-quality").kind, "lidar-quality");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -314,6 +314,8 @@ test("Polygon is a worker-gated product root and keeps a compatible direct route
|
|||
workspacesForRoot("polygon").some(({ id }) => id === "polygon-run"),
|
||||
true,
|
||||
);
|
||||
assert.equal(workspaceById("polygon-datasets").root, "polygon");
|
||||
assert.equal(workspaceById("polygon-datasets").kind, "polygon-datasets");
|
||||
assert.equal(
|
||||
workspacesForRoot("system").some(({ id }) => id === "polygon-run"),
|
||||
false,
|
||||
|
|
|
|||
|
|
@ -77,9 +77,11 @@ Polygon is now a parallel product branch. As of this document:
|
|||
Simulation Worker agent in a fresh loopback-only namespace, a bounded
|
||||
Unix-socket Mission Core gateway and an explicit `internal-virtual-only`
|
||||
lifecycle gate;
|
||||
- UI-2 exact generation `60e7916` exposes `Полигон` as a dedicated header root
|
||||
only while Mission Core confirms an available Simulation Worker. The header
|
||||
opens one direct workspace without a secondary launcher panel;
|
||||
- UI-2 exact generation `60e7916` established `Полигон` as a dedicated header
|
||||
root only while Mission Core confirms an available Simulation Worker. The
|
||||
current product surface keeps that direct entry and exposes a bounded
|
||||
two-item navigation: `Прогоны` and `Датасеты`; it has no launcher or link
|
||||
directory;
|
||||
- the browser-native Three.js scene renders a recognisable procedural Ackermann
|
||||
Rover, ground grid and ENU trajectory. Orbit supports 360° azimuth, bounded
|
||||
vertical inspection from overhead to horizon, zoom, pan, follow and camera
|
||||
|
|
@ -842,6 +844,18 @@ Center | Fleet | Observation | Missions | Data | Polygon* | System
|
|||
|
||||
Two UI gates are intentionally distinct.
|
||||
|
||||
The Polygon navigation is task-oriented:
|
||||
|
||||
- `Прогоны` owns live simulation lifecycle, history and qualification evidence;
|
||||
- `Датасеты` owns admitted replay inputs and the entry point for a future
|
||||
qualification run.
|
||||
|
||||
Dataset bytes and source artifacts remain in Data storage, but algorithm
|
||||
evaluation does not live under `Данные`. Device-specific scanner diagnostics
|
||||
remain under Fleet. A dataset preview is not a qualification result; the
|
||||
result is a versioned run with pipeline, sensor profile, metrics, provenance
|
||||
and decision.
|
||||
|
||||
### UI-0 — early read-only run view
|
||||
|
||||
Target S1B proves persisted start/health/stop history. UI-0 is implemented as a
|
||||
|
|
|
|||
|
|
@ -30,7 +30,9 @@ Implemented now:
|
|||
- a fail-closed K1 `lio_pcl` boundary;
|
||||
- worker storage admission for `D:\NDC_MISSIONCORE\datasets` and
|
||||
`/mnt/d/NDC_MISSIONCORE/datasets`;
|
||||
- a visible Dataset Gateway panel in **Данные → Качество LiDAR**.
|
||||
- a dedicated, honest dataset catalog in **Полигон → Датасеты**;
|
||||
- a separate **Парк → Диагностика LiDAR** surface containing only real sensor
|
||||
recordings and their operational evidence.
|
||||
|
||||
Not implemented in S0:
|
||||
|
||||
|
|
@ -40,6 +42,23 @@ Not implemented in S0:
|
|||
- no model training or production promotion;
|
||||
- no rolling-map implementation yet.
|
||||
|
||||
## Product surface boundary
|
||||
|
||||
The gateway is not part of device quality diagnostics.
|
||||
|
||||
- **Парк → Диагностика LiDAR** answers whether a selected real sensor recording
|
||||
is present, reproducible and operationally usable. It never mixes public
|
||||
dataset frames into the selected device evidence.
|
||||
- **Наблюдение** opens a concrete live or recorded spatial scene.
|
||||
- **Данные** owns source-of-record, retention, replay preparation and export.
|
||||
- **Полигон → Датасеты** lists admitted evaluation inputs.
|
||||
- **Полигон → Прогоны** owns the resulting algorithm comparison, metrics,
|
||||
provenance and decision.
|
||||
|
||||
Before real dataset bytes exist, the catalog shows `Не загружен`, explains the
|
||||
blocked storage gate and keeps `Открыть` / `Создать прогон` disabled. An
|
||||
architecture contract must not look like an executable dataset tool.
|
||||
|
||||
## Why public recordings look different
|
||||
|
||||
GOOSE stores one VLS-128 revolution per annotated `.bin` file. A rotating
|
||||
|
|
@ -130,7 +149,9 @@ time; TTL/dynamic filtering prevents stale ghosts.
|
|||
- [x] GOOSE XYZI and labels remain point aligned.
|
||||
- [x] Invalid length and non-finite frames fail closed.
|
||||
- [x] K1 mapped increments cannot claim raw-scan fields.
|
||||
- [x] React exposes the architectural truth before dataset bytes exist.
|
||||
- [x] React exposes an honest catalog/empty state before dataset bytes exist.
|
||||
- [x] Dataset Gateway is isolated from real-sensor diagnostics and placed under
|
||||
Polygon qualification.
|
||||
- [ ] Worker D root configured.
|
||||
- [ ] GOOSE validation archive hash recorded.
|
||||
- [ ] First real labeled frame visible in React.
|
||||
|
|
|
|||
|
|
@ -69,6 +69,12 @@ unavailable rather than inventing timestamps.
|
|||
## Consequences
|
||||
|
||||
- The UI must label accumulated K1 evidence as a map product, not a scan.
|
||||
- Real-sensor diagnostics live under Fleet and never mix public dataset frames
|
||||
into device evidence.
|
||||
- Dataset catalog, preview and qualification-run entry live under Polygon;
|
||||
Data remains the source-of-record and artifact store.
|
||||
- Until bytes are installed, the UI exposes an explicit empty state rather
|
||||
than presenting this architecture contract as an interactive tool.
|
||||
- Dataset and device inputs can share downstream algorithms only after their
|
||||
normalized contracts match.
|
||||
- Sensor adaptation may change range, FOV, point density, noise and dropout for
|
||||
|
|
|
|||
Loading…
Reference in New Issue