feat: add metric axes to trajectory overview

This commit is contained in:
梁薄云
2026-08-09 19:11:56 +08:00
parent 40a28138b9
commit 76fa2900ce
3 changed files with 73 additions and 0 deletions
@@ -39,6 +39,10 @@ main > section[hidden] { display: none !important; }
.chart-annotation-point { fill: var(--handoff); stroke: #ffffff; stroke-width: .6; }
.chart-annotation-label { fill: var(--handoff); font: 10px sans-serif; }
.world-line { fill: none; vector-effect: non-scaling-stroke; }
.world-coordinate-grid { stroke: #dfe4e8; stroke-opacity: .62; stroke-width: .7; vector-effect: non-scaling-stroke; }
.world-axis-zero { stroke: #7c8791; stroke-opacity: .72; stroke-width: 1.1; vector-effect: non-scaling-stroke; }
.world-axis-tick { fill: #68737d; font-family: ui-monospace, Consolas, monospace; }
.world-axis-label { fill: #48535d; font-family: "Microsoft YaHei", "Noto Sans CJK SC", sans-serif; font-weight: 600; }
.world-static { stroke: #8d959d; stroke-width: .9; }
.world-future { stroke: #b6bec6; stroke-dasharray: 5 4; stroke-width: .85; }
.world-active { stroke: #1769aa; stroke-width: 1.1; }
@@ -49,9 +49,62 @@
for (let row = 0; row < grid.rows; row += 1) for (let column = 0; column < grid.columns; column += 1) { const index = row * grid.columns + column; if ((bits[index >> 3] & (1 << (index & 7))) !== 0) { const x = offsetX + (grid.bounds.xMin + column * grid.resolutionMeters - bounds.xMin) * factor; const y = offsetY + (bounds.yMax - (grid.bounds.yMin + (row + 1) * grid.resolutionMeters)) * factor; context.fillRect(x, y, grid.resolutionMeters * factor, grid.resolutionMeters * factor); } }
}
function niceWorldStep(span, targetTicks = 7) {
if (!finite(span) || span <= 0) return 1;
const rough = span / Math.max(2, targetTicks);
const magnitude = Math.pow(10, Math.floor(Math.log10(rough)));
const normalized = rough / magnitude;
const factor = normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10;
return factor * magnitude;
}
function worldMetrics(bounds) {
const box = overlay.getBoundingClientRect();
const pixelWidth = box.width > 0 ? box.width : 800;
const pixelHeight = box.height > 0 ? box.height : 520;
const spanX = Math.max(bounds.xMax - bounds.xMin, 1e-9);
const spanY = Math.max(bounds.yMax - bounds.yMin, 1e-9);
return { pixelWidth, pixelHeight, spanX, spanY,
unitsPerPixel: Math.max(spanX / pixelWidth, spanY / pixelHeight, 1e-9) };
}
function renderWorldCoordinates(bounds, metrics) {
const group = svg("g", { class: "world-coordinate-system", "aria-hidden": "true" });
const xStep = niceWorldStep(metrics.spanX, 8);
const yStep = niceWorldStep(metrics.spanY, 8);
const epsilon = Math.max(xStep, yStep) * 1e-9;
let count = 0;
for (let x = Math.ceil(bounds.xMin / xStep) * xStep; x <= bounds.xMax + epsilon && count < 100; x += xStep, count += 1) {
const normalized = Math.abs(x) < epsilon ? 0 : x;
group.append(svg("line", { x1: normalized, y1: -bounds.yMax, x2: normalized, y2: -bounds.yMin, class: "world-coordinate-grid" }));
const tick = svg("text", { x: normalized, y: -bounds.yMin - 7 * metrics.unitsPerPixel, class: "world-axis-tick", "font-size": 11 * metrics.unitsPerPixel, "text-anchor": "middle" });
tick.textContent = formatTick(normalized);
group.append(tick);
}
count = 0;
for (let y = Math.ceil(bounds.yMin / yStep) * yStep; y <= bounds.yMax + epsilon && count < 100; y += yStep, count += 1) {
const normalized = Math.abs(y) < epsilon ? 0 : y;
group.append(svg("line", { x1: bounds.xMin, y1: -normalized, x2: bounds.xMax, y2: -normalized, class: "world-coordinate-grid" }));
const tick = svg("text", { x: bounds.xMin + 6 * metrics.unitsPerPixel, y: -normalized - 3 * metrics.unitsPerPixel, class: "world-axis-tick", "font-size": 11 * metrics.unitsPerPixel, "text-anchor": "start" });
tick.textContent = formatTick(normalized);
group.append(tick);
}
if (bounds.xMin <= 0 && bounds.xMax >= 0)
group.append(svg("line", { x1: 0, y1: -bounds.yMax, x2: 0, y2: -bounds.yMin, class: "world-axis-zero" }));
if (bounds.yMin <= 0 && bounds.yMax >= 0)
group.append(svg("line", { x1: bounds.xMin, y1: 0, x2: bounds.xMax, y2: 0, class: "world-axis-zero" }));
const xLabel = svg("text", { x: bounds.xMax - 8 * metrics.unitsPerPixel, y: -bounds.yMin - 8 * metrics.unitsPerPixel, class: "world-axis-label", "font-size": 12 * metrics.unitsPerPixel, "text-anchor": "end" });
xLabel.textContent = "X (m)";
const yLabel = svg("text", { x: bounds.xMin + 8 * metrics.unitsPerPixel, y: -bounds.yMax + 16 * metrics.unitsPerPixel, class: "world-axis-label", "font-size": 12 * metrics.unitsPerPixel, "text-anchor": "start" });
yLabel.textContent = "Y (m)";
group.append(xLabel, yLabel);
overlay.append(group);
}
function worldPath(points) { return safeArray(points).filter(point => finite(point.x) && finite(point.y)).map((point, index) => (index ? "L" : "M") + point.x + " " + (-point.y)).join(" "); }
function renderWorld() {
const bounds = getBounds(); drawOccupancy(bounds); overlay.replaceChildren(); overlay.setAttribute("viewBox", `${bounds.xMin} ${-bounds.yMax} ${bounds.xMax - bounds.xMin} ${bounds.yMax - bounds.yMin}`); overlay.setAttribute("preserveAspectRatio", "xMidYMid meet");
const metrics = worldMetrics(bounds); renderWorldCoordinates(bounds, metrics);
const snapshot = state.staticSnapshot || {}; const frame = state.frame || {};
const addLine = (line, css) => { const path = worldPath(line.points); if (path) overlay.append(svg("path", { d: path, class: "world-line " + css })); };
const staticClass = line => { const kind = String(line.kind || "").toLowerCase(); if (kind === "global" || kind === "coarse") return "world-coarse"; if (kind.includes("local-g2") || kind.includes("g2")) return "world-local-g2"; return "world-static"; };