fix(control-station): align lidar viewer and expose polygon
This commit is contained in:
parent
c01712c30d
commit
881e97312b
|
|
@ -47,7 +47,6 @@ import type {
|
|||
import { useRecordedSessionAdmission } from "./core/observation/useRecordedSessionAdmission";
|
||||
import { useWorkspaceLayoutProfile } from "./core/observation/useWorkspaceLayoutProfile";
|
||||
import { resolvePolygonRunRoute } from "./core/polygon/runArchive";
|
||||
import { usePolygonWorkerCapability } from "./core/polygon/usePolygonWorkerCapability";
|
||||
import {
|
||||
OBSERVATION_WORKSPACE_ID,
|
||||
OBSERVATION_WORKSPACE_LAYOUT_VERSION,
|
||||
|
|
@ -55,7 +54,7 @@ import {
|
|||
} from "./core/observation/workspaceLayout";
|
||||
import {
|
||||
rootById,
|
||||
rootsForCapabilities,
|
||||
roots,
|
||||
workspaceById,
|
||||
workspacesForRoot,
|
||||
type RootId,
|
||||
|
|
@ -151,7 +150,6 @@ function mergeViewerSettings(
|
|||
export default function App() {
|
||||
const runtime = useMissionRuntime();
|
||||
const { selection } = useDevicePluginHost();
|
||||
const polygonWorker = usePolygonWorkerCapability();
|
||||
const polygonRunRoute = useMemo(
|
||||
() => resolvePolygonRunRoute(typeof window === "undefined" ? "" : window.location.search),
|
||||
[],
|
||||
|
|
@ -199,10 +197,7 @@ export default function App() {
|
|||
const polygonRunRouteOpenedRef = useRef(false);
|
||||
|
||||
const currentRoot = rootById(activeRoot);
|
||||
const visibleRoots = useMemo(
|
||||
() => rootsForCapabilities({ polygonWorkerAvailable: polygonWorker.available }),
|
||||
[polygonWorker.available],
|
||||
);
|
||||
const visibleRoots = roots;
|
||||
const activeDefinition = workspaceById(workspace.activeView);
|
||||
const spatialWorkspaceActive = Boolean(
|
||||
workspace.contentOpen && activeDefinition?.kind === "spatial",
|
||||
|
|
|
|||
|
|
@ -796,14 +796,6 @@ export function rootById(id: RootId | null): RootDefinition | 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 {
|
||||
return id ? workspaces.find((workspace) => workspace.id === id) ?? null : null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1441,9 +1441,8 @@
|
|||
.lidar-field-cloud {
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
border: 1px solid rgb(255 255 255 / 0.09);
|
||||
border-radius: 0.9rem;
|
||||
background: #071018;
|
||||
background: #06070a;
|
||||
}
|
||||
|
||||
.lidar-field-camera {
|
||||
|
|
@ -1461,7 +1460,6 @@
|
|||
.lidar-field-camera > .lidar-field-stage__label,
|
||||
.lidar-field-cloud > header {
|
||||
min-height: 3.45rem;
|
||||
border-bottom: 1px solid rgb(255 255 255 / 0.08);
|
||||
padding: 0.66rem 0.72rem;
|
||||
}
|
||||
|
||||
|
|
@ -1487,7 +1485,6 @@
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
border-top: 1px solid rgb(255 255 255 / 0.08);
|
||||
padding: 0.52rem 0.7rem;
|
||||
}
|
||||
|
||||
|
|
@ -1548,7 +1545,7 @@
|
|||
overflow: hidden;
|
||||
margin-top: 0.8rem;
|
||||
border-radius: 0.9rem;
|
||||
background: #071018;
|
||||
background: #06070a;
|
||||
}
|
||||
|
||||
.lidar-fallback-review > header {
|
||||
|
|
@ -1557,7 +1554,6 @@
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
@ -1569,7 +1565,6 @@
|
|||
}
|
||||
|
||||
.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;
|
||||
|
|
@ -1646,11 +1641,8 @@
|
|||
display: grid;
|
||||
gap: 0.75rem;
|
||||
margin-top: 0.85rem;
|
||||
border: 1px solid rgb(74 215 255 / 0.18);
|
||||
border-radius: 1rem;
|
||||
background:
|
||||
radial-gradient(circle at 18% 0%, rgb(56 124 255 / 0.12), transparent 34%),
|
||||
rgb(4 12 19 / 0.72);
|
||||
background: #06070a;
|
||||
padding: 0.85rem;
|
||||
}
|
||||
|
||||
|
|
@ -1716,9 +1708,9 @@
|
|||
.lidar-ground-modes button,
|
||||
.lidar-ground-frame-control button,
|
||||
.lidar-ground-scene__toolbar button {
|
||||
border: 1px solid var(--station-hairline);
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: rgb(255 255 255 / 0.035);
|
||||
background: transparent;
|
||||
padding: 0.4rem 0.62rem;
|
||||
color: var(--nodedc-text-secondary);
|
||||
font: inherit;
|
||||
|
|
@ -1730,9 +1722,16 @@
|
|||
.lidar-ground-modes button[data-active="true"],
|
||||
.lidar-ground-frame-control button:hover:not(:disabled),
|
||||
.lidar-ground-scene__toolbar button:hover {
|
||||
border-color: rgb(74 215 255 / 0.48);
|
||||
background: rgb(74 215 255 / 0.1);
|
||||
background: rgb(255 255 255 / 0.11);
|
||||
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 {
|
||||
|
|
@ -1751,16 +1750,15 @@
|
|||
|
||||
.lidar-ground-frame-control input {
|
||||
width: min(16rem, 24vw);
|
||||
accent-color: #4ad7ff;
|
||||
accent-color: #f2f2f2;
|
||||
}
|
||||
|
||||
.lidar-ground-scene {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 30rem;
|
||||
border: 1px solid rgb(255 255 255 / 0.09);
|
||||
border-radius: 0.9rem;
|
||||
background: #071018;
|
||||
background: #06070a;
|
||||
}
|
||||
|
||||
.lidar-ground-scene__viewport {
|
||||
|
|
@ -1782,15 +1780,15 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
border: 1px solid rgb(255 255 255 / 0.08);
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: rgb(5 13 21 / 0.82);
|
||||
background: rgb(10 11 14 / 0.78);
|
||||
padding: 0.28rem;
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.lidar-ground-scene__toolbar button {
|
||||
background: rgb(74 215 255 / 0.08);
|
||||
background: rgb(255 255 255 / 0.06);
|
||||
}
|
||||
|
||||
.lidar-ground-scene__error {
|
||||
|
|
@ -1808,9 +1806,8 @@
|
|||
place-content: center;
|
||||
justify-items: center;
|
||||
gap: 0.55rem;
|
||||
border: 1px solid var(--station-hairline);
|
||||
border-radius: 0.9rem;
|
||||
background: #071018;
|
||||
background: #06070a;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -60,12 +60,13 @@ function frameColors(
|
|||
const candidateAssigned = frame.masks.candidateAssigned[index] === 1;
|
||||
if (mode === "intensity") {
|
||||
const intensity = (frame.intensity0To255?.[index] ?? 96) / 255;
|
||||
const neutral = 0.16 + intensity * 0.8;
|
||||
setRgb(
|
||||
colors,
|
||||
offset,
|
||||
0.12 + intensity * 0.74,
|
||||
0.24 + intensity * 0.68,
|
||||
0.34 + intensity * 0.6,
|
||||
neutral,
|
||||
neutral,
|
||||
neutral,
|
||||
);
|
||||
} else if (mode === "current") {
|
||||
setRgb(
|
||||
|
|
@ -131,7 +132,7 @@ export function LidarGroundPointCloud({
|
|||
}
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||
renderer.setClearColor(0x071018, 0.96);
|
||||
renderer.setClearColor(0x06070a, 1);
|
||||
renderer.domElement.setAttribute(
|
||||
"aria-label",
|
||||
"Интерактивное облако LiDAR",
|
||||
|
|
@ -139,7 +140,7 @@ export function LidarGroundPointCloud({
|
|||
host.prepend(renderer.domElement);
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
const fog = new THREE.FogExp2(0x071018, 0.035);
|
||||
const fog = new THREE.FogExp2(0x06070a, 0.035);
|
||||
scene.fog = fog;
|
||||
fogRef.current = fog;
|
||||
const camera = new THREE.PerspectiveCamera(48, 1, 0.01, 1_000);
|
||||
|
|
@ -172,7 +173,7 @@ export function LidarGroundPointCloud({
|
|||
materialRef.current = 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;
|
||||
const gridMaterials = Array.isArray(grid.material)
|
||||
? grid.material
|
||||
|
|
|
|||
|
|
@ -378,7 +378,7 @@ export function PolygonRoverScene({
|
|||
<span>
|
||||
{workerAvailable
|
||||
? "Запустите PX4/Gazebo — модель останется доступна для осмотра."
|
||||
: "Полигон появится в шапке после подтверждения Simulation Worker."}
|
||||
: "Прогоны и датасеты доступны; запуск и управление откроются после подтверждения Simulation Worker."}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ let stopPolygonWorker;
|
|||
let PolygonWorkerContractError;
|
||||
let workspaceById;
|
||||
let workspacesForRoot;
|
||||
let rootsForCapabilities;
|
||||
let roots;
|
||||
|
||||
before(async () => {
|
||||
server = await createServer({
|
||||
|
|
@ -51,7 +51,7 @@ before(async () => {
|
|||
({
|
||||
workspaceById,
|
||||
workspacesForRoot,
|
||||
rootsForCapabilities,
|
||||
roots,
|
||||
} = 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"), {
|
||||
active: true,
|
||||
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"),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
rootsForCapabilities({ polygonWorkerAvailable: false })
|
||||
.some(({ id }) => id === "polygon"),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
rootsForCapabilities({ polygonWorkerAvailable: true })
|
||||
.some(({ id }) => id === "polygon"),
|
||||
true,
|
||||
);
|
||||
assert.equal(roots.some(({ id }) => id === "polygon"), true);
|
||||
});
|
||||
|
||||
test("Polygon contracts decode path-free evidence and preserve the safety boundary", () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
unprivileged worker agent in the same isolation boundary, a bounded Unix-socket
|
||||
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
|
||||
top-level product root and replaces the diagnostic 2D marker with a
|
||||
live panel. UI-2 commit `60e7916` promotes that panel into a top-level product
|
||||
root and replaces the diagnostic 2D marker with a
|
||||
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
|
||||
`VehicleState`, eight persisted lifecycle events, terminal `completed` and zero
|
||||
|
|
|
|||
|
|
@ -834,15 +834,16 @@ safety policy.
|
|||
|
||||
## 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
|
||||
Center | Fleet | Observation | Missions | Data | Polygon* | System
|
||||
|
||||
* visible only while Mission Core confirms an admitted available Simulation Worker
|
||||
Center | Fleet | Observation | Missions | Data | Polygon | System
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
|
|
@ -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
|
||||
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:
|
||||
|
||||
|
|
@ -894,8 +895,10 @@ The full UI-2 control/scenario gate begins only after:
|
|||
2. S1 target backend lifecycle and command authority are proven;
|
||||
3. run history is persisted independently of a browser session.
|
||||
|
||||
Commit `60e7916` accepts the presentation/lifecycle slice before all three
|
||||
conditions are complete: conditional header root, direct workspace,
|
||||
Commit `60e7916` accepted the presentation/lifecycle slice before all three
|
||||
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
|
||||
history. It does not claim command authority. Scenarios, pause/step/reset,
|
||||
canonical rover commands, Compare and Report remain planned. UI-2 cannot own
|
||||
|
|
|
|||
|
|
@ -55,9 +55,10 @@ reports easy to misuse.
|
|||
13. S1 uses exact `px4_msgs` directly. The experimental
|
||||
`px4-ros2-interface-lib` is not a mandatory S1 dependency.
|
||||
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
|
||||
cannot mutate a run. Top-level Polygon UI and control remain gated on
|
||||
accepted S1 target command/lifecycle behavior.
|
||||
history through GET-only server routes and cannot mutate a run. The
|
||||
top-level Polygon product surface remains visible so datasets and persisted
|
||||
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.
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in New Issue