fix(control-station): align lidar viewer and expose polygon

This commit is contained in:
DCCONSTRUCTIONS 2026-07-25 13:23:34 +03:00
parent c01712c30d
commit 881e97312b
9 changed files with 51 additions and 71 deletions

View File

@ -47,7 +47,6 @@ import type {
import { useRecordedSessionAdmission } from "./core/observation/useRecordedSessionAdmission"; import { useRecordedSessionAdmission } from "./core/observation/useRecordedSessionAdmission";
import { useWorkspaceLayoutProfile } from "./core/observation/useWorkspaceLayoutProfile"; import { useWorkspaceLayoutProfile } from "./core/observation/useWorkspaceLayoutProfile";
import { resolvePolygonRunRoute } from "./core/polygon/runArchive"; import { resolvePolygonRunRoute } from "./core/polygon/runArchive";
import { usePolygonWorkerCapability } from "./core/polygon/usePolygonWorkerCapability";
import { import {
OBSERVATION_WORKSPACE_ID, OBSERVATION_WORKSPACE_ID,
OBSERVATION_WORKSPACE_LAYOUT_VERSION, OBSERVATION_WORKSPACE_LAYOUT_VERSION,
@ -55,7 +54,7 @@ import {
} from "./core/observation/workspaceLayout"; } from "./core/observation/workspaceLayout";
import { import {
rootById, rootById,
rootsForCapabilities, roots,
workspaceById, workspaceById,
workspacesForRoot, workspacesForRoot,
type RootId, type RootId,
@ -151,7 +150,6 @@ function mergeViewerSettings(
export default function App() { export default function App() {
const runtime = useMissionRuntime(); const runtime = useMissionRuntime();
const { selection } = useDevicePluginHost(); const { selection } = useDevicePluginHost();
const polygonWorker = usePolygonWorkerCapability();
const polygonRunRoute = useMemo( const polygonRunRoute = useMemo(
() => resolvePolygonRunRoute(typeof window === "undefined" ? "" : window.location.search), () => resolvePolygonRunRoute(typeof window === "undefined" ? "" : window.location.search),
[], [],
@ -199,10 +197,7 @@ export default function App() {
const polygonRunRouteOpenedRef = useRef(false); const polygonRunRouteOpenedRef = useRef(false);
const currentRoot = rootById(activeRoot); const currentRoot = rootById(activeRoot);
const visibleRoots = useMemo( const visibleRoots = roots;
() => rootsForCapabilities({ polygonWorkerAvailable: polygonWorker.available }),
[polygonWorker.available],
);
const activeDefinition = workspaceById(workspace.activeView); const activeDefinition = workspaceById(workspace.activeView);
const spatialWorkspaceActive = Boolean( const spatialWorkspaceActive = Boolean(
workspace.contentOpen && activeDefinition?.kind === "spatial", workspace.contentOpen && activeDefinition?.kind === "spatial",

View File

@ -796,14 +796,6 @@ export function rootById(id: RootId | null): RootDefinition | null {
return id ? roots.find((root) => root.id === id) ?? null : null; return id ? roots.find((root) => root.id === id) ?? null : null;
} }
export function rootsForCapabilities({
polygonWorkerAvailable,
}: {
polygonWorkerAvailable: boolean;
}): RootDefinition[] {
return roots.filter((root) => root.id !== "polygon" || polygonWorkerAvailable);
}
export function workspaceById(id: string | null): WorkspaceDefinition | null { export function workspaceById(id: string | null): WorkspaceDefinition | null {
return id ? workspaces.find((workspace) => workspace.id === id) ?? null : null; return id ? workspaces.find((workspace) => workspace.id === id) ?? null : null;
} }

View File

@ -1441,9 +1441,8 @@
.lidar-field-cloud { .lidar-field-cloud {
overflow: hidden; overflow: hidden;
min-width: 0; min-width: 0;
border: 1px solid rgb(255 255 255 / 0.09);
border-radius: 0.9rem; border-radius: 0.9rem;
background: #071018; background: #06070a;
} }
.lidar-field-camera { .lidar-field-camera {
@ -1461,7 +1460,6 @@
.lidar-field-camera > .lidar-field-stage__label, .lidar-field-camera > .lidar-field-stage__label,
.lidar-field-cloud > header { .lidar-field-cloud > header {
min-height: 3.45rem; min-height: 3.45rem;
border-bottom: 1px solid rgb(255 255 255 / 0.08);
padding: 0.66rem 0.72rem; padding: 0.66rem 0.72rem;
} }
@ -1487,7 +1485,6 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
gap: 0.5rem; gap: 0.5rem;
border-top: 1px solid rgb(255 255 255 / 0.08);
padding: 0.52rem 0.7rem; padding: 0.52rem 0.7rem;
} }
@ -1548,7 +1545,7 @@
overflow: hidden; overflow: hidden;
margin-top: 0.8rem; margin-top: 0.8rem;
border-radius: 0.9rem; border-radius: 0.9rem;
background: #071018; background: #06070a;
} }
.lidar-fallback-review > header { .lidar-fallback-review > header {
@ -1557,7 +1554,6 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 0.8rem; gap: 0.8rem;
border-bottom: 1px solid rgb(255 255 255 / 0.08);
padding: 0.65rem 0.75rem; padding: 0.65rem 0.75rem;
} }
@ -1569,7 +1565,6 @@
} }
.lidar-fallback-review > footer { .lidar-fallback-review > footer {
border-top: 1px solid rgb(255 255 255 / 0.08);
padding: 0.58rem 0.75rem; padding: 0.58rem 0.75rem;
color: var(--nodedc-text-muted); color: var(--nodedc-text-muted);
font-size: 0.58rem; font-size: 0.58rem;
@ -1646,11 +1641,8 @@
display: grid; display: grid;
gap: 0.75rem; gap: 0.75rem;
margin-top: 0.85rem; margin-top: 0.85rem;
border: 1px solid rgb(74 215 255 / 0.18);
border-radius: 1rem; border-radius: 1rem;
background: background: #06070a;
radial-gradient(circle at 18% 0%, rgb(56 124 255 / 0.12), transparent 34%),
rgb(4 12 19 / 0.72);
padding: 0.85rem; padding: 0.85rem;
} }
@ -1716,9 +1708,9 @@
.lidar-ground-modes button, .lidar-ground-modes button,
.lidar-ground-frame-control button, .lidar-ground-frame-control button,
.lidar-ground-scene__toolbar button { .lidar-ground-scene__toolbar button {
border: 1px solid var(--station-hairline); border: 0;
border-radius: 999px; border-radius: 999px;
background: rgb(255 255 255 / 0.035); background: transparent;
padding: 0.4rem 0.62rem; padding: 0.4rem 0.62rem;
color: var(--nodedc-text-secondary); color: var(--nodedc-text-secondary);
font: inherit; font: inherit;
@ -1730,9 +1722,16 @@
.lidar-ground-modes button[data-active="true"], .lidar-ground-modes button[data-active="true"],
.lidar-ground-frame-control button:hover:not(:disabled), .lidar-ground-frame-control button:hover:not(:disabled),
.lidar-ground-scene__toolbar button:hover { .lidar-ground-scene__toolbar button:hover {
border-color: rgb(74 215 255 / 0.48); background: rgb(255 255 255 / 0.11);
background: rgb(74 215 255 / 0.1);
color: var(--nodedc-text-primary); color: var(--nodedc-text-primary);
outline: none;
}
.lidar-ground-modes button:focus-visible,
.lidar-ground-frame-control button:focus-visible,
.lidar-ground-scene__toolbar button:focus-visible {
background: rgb(255 255 255 / 0.14);
outline: none;
} }
.lidar-ground-frame-control button { .lidar-ground-frame-control button {
@ -1751,16 +1750,15 @@
.lidar-ground-frame-control input { .lidar-ground-frame-control input {
width: min(16rem, 24vw); width: min(16rem, 24vw);
accent-color: #4ad7ff; accent-color: #f2f2f2;
} }
.lidar-ground-scene { .lidar-ground-scene {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
min-height: 30rem; min-height: 30rem;
border: 1px solid rgb(255 255 255 / 0.09);
border-radius: 0.9rem; border-radius: 0.9rem;
background: #071018; background: #06070a;
} }
.lidar-ground-scene__viewport { .lidar-ground-scene__viewport {
@ -1782,15 +1780,15 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
border: 1px solid rgb(255 255 255 / 0.08); border: 0;
border-radius: 999px; border-radius: 999px;
background: rgb(5 13 21 / 0.82); background: rgb(10 11 14 / 0.78);
padding: 0.28rem; padding: 0.28rem;
backdrop-filter: blur(14px); backdrop-filter: blur(14px);
} }
.lidar-ground-scene__toolbar button { .lidar-ground-scene__toolbar button {
background: rgb(74 215 255 / 0.08); background: rgb(255 255 255 / 0.06);
} }
.lidar-ground-scene__error { .lidar-ground-scene__error {
@ -1808,9 +1806,8 @@
place-content: center; place-content: center;
justify-items: center; justify-items: center;
gap: 0.55rem; gap: 0.55rem;
border: 1px solid var(--station-hairline);
border-radius: 0.9rem; border-radius: 0.9rem;
background: #071018; background: #06070a;
text-align: center; text-align: center;
} }

View File

@ -60,12 +60,13 @@ function frameColors(
const candidateAssigned = frame.masks.candidateAssigned[index] === 1; const candidateAssigned = frame.masks.candidateAssigned[index] === 1;
if (mode === "intensity") { if (mode === "intensity") {
const intensity = (frame.intensity0To255?.[index] ?? 96) / 255; const intensity = (frame.intensity0To255?.[index] ?? 96) / 255;
const neutral = 0.16 + intensity * 0.8;
setRgb( setRgb(
colors, colors,
offset, offset,
0.12 + intensity * 0.74, neutral,
0.24 + intensity * 0.68, neutral,
0.34 + intensity * 0.6, neutral,
); );
} else if (mode === "current") { } else if (mode === "current") {
setRgb( setRgb(
@ -131,7 +132,7 @@ export function LidarGroundPointCloud({
} }
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.outputColorSpace = THREE.SRGBColorSpace; renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.setClearColor(0x071018, 0.96); renderer.setClearColor(0x06070a, 1);
renderer.domElement.setAttribute( renderer.domElement.setAttribute(
"aria-label", "aria-label",
"Интерактивное облако LiDAR", "Интерактивное облако LiDAR",
@ -139,7 +140,7 @@ export function LidarGroundPointCloud({
host.prepend(renderer.domElement); host.prepend(renderer.domElement);
const scene = new THREE.Scene(); const scene = new THREE.Scene();
const fog = new THREE.FogExp2(0x071018, 0.035); const fog = new THREE.FogExp2(0x06070a, 0.035);
scene.fog = fog; scene.fog = fog;
fogRef.current = fog; fogRef.current = fog;
const camera = new THREE.PerspectiveCamera(48, 1, 0.01, 1_000); const camera = new THREE.PerspectiveCamera(48, 1, 0.01, 1_000);
@ -172,7 +173,7 @@ export function LidarGroundPointCloud({
materialRef.current = material; materialRef.current = material;
scene.add(new THREE.Points(geometry, material)); scene.add(new THREE.Points(geometry, material));
const grid = new THREE.GridHelper(10, 40, 0x71963d, 0x26312a); const grid = new THREE.GridHelper(10, 40, 0x454846, 0x252725);
gridRef.current = grid; gridRef.current = grid;
const gridMaterials = Array.isArray(grid.material) const gridMaterials = Array.isArray(grid.material)
? grid.material ? grid.material

View File

@ -378,7 +378,7 @@ export function PolygonRoverScene({
<span> <span>
{workerAvailable {workerAvailable
? "Запустите PX4/Gazebo — модель останется доступна для осмотра." ? "Запустите PX4/Gazebo — модель останется доступна для осмотра."
: олигон появится в шапке после подтверждения Simulation Worker."} : рогоны и датасеты доступны; запуск и управление откроются после подтверждения Simulation Worker."}
</span> </span>
</div> </div>
) : null} ) : null}

View File

@ -21,7 +21,7 @@ let stopPolygonWorker;
let PolygonWorkerContractError; let PolygonWorkerContractError;
let workspaceById; let workspaceById;
let workspacesForRoot; let workspacesForRoot;
let rootsForCapabilities; let roots;
before(async () => { before(async () => {
server = await createServer({ server = await createServer({
@ -51,7 +51,7 @@ before(async () => {
({ ({
workspaceById, workspaceById,
workspacesForRoot, workspacesForRoot,
rootsForCapabilities, roots,
} = await server.ssrLoadModule("/src/productModel.ts")); } = await server.ssrLoadModule("/src/productModel.ts"));
}); });
@ -290,7 +290,7 @@ function commandAcceptance(overrides = {}) {
}; };
} }
test("Polygon is a worker-gated product root and keeps a compatible direct route", () => { test("Polygon stays visible while worker capability gates live actions", () => {
assert.deepEqual(resolvePolygonRunRoute("?workspace=polygon-run"), { assert.deepEqual(resolvePolygonRunRoute("?workspace=polygon-run"), {
active: true, active: true,
runId: null, runId: null,
@ -320,16 +320,7 @@ test("Polygon is a worker-gated product root and keeps a compatible direct route
workspacesForRoot("system").some(({ id }) => id === "polygon-run"), workspacesForRoot("system").some(({ id }) => id === "polygon-run"),
false, false,
); );
assert.equal( assert.equal(roots.some(({ id }) => id === "polygon"), true);
rootsForCapabilities({ polygonWorkerAvailable: false })
.some(({ id }) => id === "polygon"),
false,
);
assert.equal(
rootsForCapabilities({ polygonWorkerAvailable: true })
.some(({ id }) => id === "polygon"),
true,
);
}); });
test("Polygon contracts decode path-free evidence and preserve the safety boundary", () => { test("Polygon contracts decode path-free evidence and preserve the safety boundary", () => {

View File

@ -164,8 +164,8 @@ fresh loopback-only namespace, the run reached `completed`, and both provider
groups stopped without residue. S1C commit `b7ccca3` adds a persistent groups stopped without residue. S1C commit `b7ccca3` adds a persistent
unprivileged worker agent in the same isolation boundary, a bounded Unix-socket unprivileged worker agent in the same isolation boundary, a bounded Unix-socket
gateway, an explicit `internal-virtual-only` backend gate and a browser-native gateway, an explicit `internal-virtual-only` backend gate and a browser-native
live panel. UI-2 commit `60e7916` promotes that panel into a worker-gated live panel. UI-2 commit `60e7916` promotes that panel into a top-level product
top-level product root and replaces the diagnostic 2D marker with a root and replaces the diagnostic 2D marker with a
browser-native Three.js Ackermann Rover scene. The exact D-only generation browser-native Three.js Ackermann Rover scene. The exact D-only generation
passed 35 target tests and one UI-driven provider run with diagnostic Gazebo passed 35 target tests and one UI-driven provider run with diagnostic Gazebo
`VehicleState`, eight persisted lifecycle events, terminal `completed` and zero `VehicleState`, eight persisted lifecycle events, terminal `completed` and zero

View File

@ -834,15 +834,16 @@ safety policy.
## 20. UI gate ## 20. UI gate
Polygon is implemented as a capability-gated seventh top-level bounded context: Polygon is implemented as a permanent seventh top-level bounded context:
```text ```text
Center | Fleet | Observation | Missions | Data | Polygon* | System Center | Fleet | Observation | Missions | Data | Polygon | System
* visible only while Mission Core confirms an admitted available Simulation Worker
``` ```
Two UI gates are intentionally distinct. The product surface and execution capability are intentionally distinct.
`Полигон` and its dataset catalog remain visible without a worker. Worker
capability gates only live lifecycle and command actions, which fail closed and
show an explicit offline state.
The Polygon navigation is task-oriented: The Polygon navigation is task-oriented:
@ -886,7 +887,7 @@ read-only evidence contract backed by the server-owned run repository and is
composed into UI-2. A shared review deployment requires a reviewed read-only D composed into UI-2. A shared review deployment requires a reviewed read-only D
mount or a co-located backend; SSH is not a product data plane. mount or a co-located backend; SSH is not a product data plane.
### UI-2 — worker-gated top-level Polygon ### UI-2 — permanent Polygon with worker-gated live actions
The full UI-2 control/scenario gate begins only after: The full UI-2 control/scenario gate begins only after:
@ -894,8 +895,10 @@ The full UI-2 control/scenario gate begins only after:
2. S1 target backend lifecycle and command authority are proven; 2. S1 target backend lifecycle and command authority are proven;
3. run history is persisted independently of a browser session. 3. run history is persisted independently of a browser session.
Commit `60e7916` accepts the presentation/lifecycle slice before all three Commit `60e7916` accepted the presentation/lifecycle slice before all three
conditions are complete: conditional header root, direct workspace, conditions were complete. The root is now permanent so datasets and persisted
evidence remain reachable while the worker is offline; live start/stop and
commands remain capability-gated. The slice includes the direct workspace,
virtual-only start/stop, 3D rover inspection, diagnostic ENU pose and read-only virtual-only start/stop, 3D rover inspection, diagnostic ENU pose and read-only
history. It does not claim command authority. Scenarios, pause/step/reset, history. It does not claim command authority. Scenarios, pause/step/reset,
canonical rover commands, Compare and Report remain planned. UI-2 cannot own canonical rover commands, Compare and Report remain planned. UI-2 cannot own

View File

@ -55,9 +55,10 @@ reports easy to misuse.
13. S1 uses exact `px4_msgs` directly. The experimental 13. S1 uses exact `px4_msgs` directly. The experimental
`px4-ros2-interface-lib` is not a mandatory S1 dependency. `px4-ros2-interface-lib` is not a mandatory S1 dependency.
14. The read-only direct UI-0 run view consumes proven target S1B persisted 14. The read-only direct UI-0 run view consumes proven target S1B persisted
history through GET-only server routes. It is hidden from navigation and history through GET-only server routes and cannot mutate a run. The
cannot mutate a run. Top-level Polygon UI and control remain gated on top-level Polygon product surface remains visible so datasets and persisted
accepted S1 target command/lifecycle behavior. evidence are reachable offline; live lifecycle and command actions remain
gated on admitted worker capability and accepted S1 target behavior.
15. Real actuator authority requires a new decision and physical safety gate. 15. Real actuator authority requires a new decision and physical safety gate.
16. S0 processes run inside an ephemeral loopback-only Linux network namespace. 16. S0 processes run inside an ephemeral loopback-only Linux network namespace.
Micro XRCE-DDS Agent's UDP wildcard bind is acceptable only inside that Micro XRCE-DDS Agent's UDP wildcard bind is acceptable only inside that