feat(lab): publish fail-closed TGS evidence

This commit is contained in:
DCCONSTRUCTIONS
2026-08-26 21:32:35 +03:00
parent 67d5d6fa05
commit 6544d9e918
27 changed files with 2167 additions and 73 deletions
@@ -41,8 +41,25 @@ export interface LaboratoryMetricCorridorVisual {
halfWidthM: number;
}
export type LaboratoryMetricCellState =
| "unobserved"
| "ground-support"
| "nonground-occupied"
| "unknown-rejected";
export interface LaboratoryMetricCellEvidence {
centerBodyXyM: readonly [number, number];
zBoundsM: readonly [number | null, number | null];
state: LaboratoryMetricCellState;
}
export interface LaboratoryMetricLegendEntry {
id: LaboratoryMetricDecision | "context" | "local-surface" | "rolling" | "low-step";
id: LaboratoryMetricDecision
| LaboratoryMetricCellState
| "context"
| "local-surface"
| "rolling"
| "low-step";
label: string;
}
@@ -197,6 +214,9 @@ LaboratoryMetricEvidenceSceneHandle,
pointSemanticClassIds?: readonly (number | null)[];
semanticClasses?: readonly RecordedEvidenceSemanticClass[];
semanticPalette?: readonly RecordedEvidenceSemanticPaletteEntry[];
classifiedCells?: readonly LaboratoryMetricCellEvidence[];
classifiedCellSizeM?: number;
showClassifiedCells?: boolean;
}
>(function LaboratoryMetricEvidenceScene({
pointCloudBodyXyzM,
@@ -214,6 +234,9 @@ LaboratoryMetricEvidenceSceneHandle,
pointSemanticClassIds,
semanticClasses,
semanticPalette,
classifiedCells = [],
classifiedCellSizeM = 0.45,
showClassifiedCells = true,
}, ref) {
const hostRef = useRef<HTMLDivElement | null>(null);
const sceneRef = useRef<THREE.Scene | null>(null);
@@ -374,6 +397,63 @@ LaboratoryMetricEvidenceSceneHandle,
));
}
if (showClassifiedCells && classifiedCells.length) {
const cellsByState = new Map<LaboratoryMetricCellState, LaboratoryMetricCellEvidence[]>();
for (const cell of classifiedCells) {
const cells = cellsByState.get(cell.state) ?? [];
cells.push(cell);
cellsByState.set(cell.state, cells);
}
for (const [state, cells] of cellsByState) {
const color = state === "ground-support"
? tokenColor(host, "--nodedc-success-rgb", [181, 255, 90])
: state === "nonground-occupied"
? tokenColor(host, "--nodedc-danger-rgb", [255, 104, 112])
: state === "unknown-rejected"
? tokenColor(host, "--nodedc-warning-rgb", [255, 197, 92])
: tokenColor(host, "--nodedc-text-muted", [96, 99, 106]);
const geometry = new THREE.BoxGeometry(
classifiedCellSizeM * 0.92,
1,
classifiedCellSizeM * 0.92,
);
const material = new THREE.MeshBasicMaterial({
color,
transparent: true,
opacity: state === "unobserved" ? 0.035 : state === "ground-support" ? 0.12 : 0.24,
depthWrite: false,
});
const mesh = new THREE.InstancedMesh(geometry, material, cells.length);
const matrix = new THREE.Matrix4();
const scale = new THREE.Vector3(1, 1, 1);
const rotation = new THREE.Quaternion();
cells.forEach((cell, index) => {
const minimum = cell.zBoundsM[0];
const maximum = cell.zBoundsM[1];
const height = minimum === null || maximum === null
? 0.018
: Math.max(0.018, maximum - minimum);
const centerZ = minimum === null || maximum === null
? -0.012
: (minimum + maximum) / 2;
const [sceneX, sceneY, sceneZ] = scenePoint([
cell.centerBodyXyM[0],
cell.centerBodyXyM[1],
centerZ,
]);
scale.set(1, height, 1);
matrix.compose(
new THREE.Vector3(sceneX, sceneY, sceneZ),
rotation,
scale,
);
mesh.setMatrixAt(index, matrix);
});
mesh.instanceMatrix.needsUpdate = true;
content.add(mesh);
}
}
for (const obstacle of obstacles) {
if (
(
@@ -451,7 +531,10 @@ LaboratoryMetricEvidenceSceneHandle,
pointSemanticClassIds,
semanticClasses,
semanticPalette,
classifiedCells,
classifiedCellSizeM,
showCurrentIncrement,
showClassifiedCells,
showLocalSurface,
showRollingMap,
showLowStep,
@@ -563,7 +646,9 @@ LaboratoryMetricEvidenceSceneHandle,
});
})();
const metricLegendEntries = laboratoryMetricLegendEntries({
pointCloudCount: pointCloudBodyXyzM.length,
pointCloudCount: pointSemanticClassIds?.every((item) => item !== null)
? 0
: pointCloudBodyXyzM.length,
localSurfaceCount: localSurfaceBodyXyzM.length,
obstacles,
showCurrentIncrement,
@@ -571,6 +656,28 @@ LaboratoryMetricEvidenceSceneHandle,
showRollingMap,
showLowStep,
});
const classifiedLegendEntries = (() => {
if (!showClassifiedCells || !classifiedCells.length) return [];
const states = new Set(classifiedCells.map((cell) => cell.state));
return [
states.has("ground-support")
? { id: "ground-support" as const, label: "Ground support" }
: null,
states.has("nonground-occupied")
? { id: "nonground-occupied" as const, label: "Non-ground occupied" }
: null,
states.has("unknown-rejected")
? { id: "unknown-rejected" as const, label: "Unknown / rejected" }
: null,
states.has("unobserved")
? { id: "unobserved" as const, label: "Unobserved" }
: null,
]
.filter((entry): entry is NonNullable<typeof entry> => entry !== null)
.filter((entry) => !semanticLegendEntries.some(
(semanticEntry) => semanticEntry.label === entry.label,
));
})();
return (
<div className="laboratory-metric-evidence-scene">
@@ -581,6 +688,9 @@ LaboratoryMetricEvidenceSceneHandle,
{metricLegendEntries.map((entry) => (
<span key={entry.id} data-decision={entry.id}>{entry.label}</span>
))}
{classifiedLegendEntries.map((entry) => (
<span key={entry.id} data-decision={entry.id}>{entry.label}</span>
))}
{semanticLegendEntries.map((entry) => (
<span
key={entry.id}