feat(lab): publish fail-closed TGS evidence
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user