feat: add metric axes to trajectory overview
This commit is contained in:
@@ -39,6 +39,10 @@ main > section[hidden] { display: none !important; }
|
|||||||
.chart-annotation-point { fill: var(--handoff); stroke: #ffffff; stroke-width: .6; }
|
.chart-annotation-point { fill: var(--handoff); stroke: #ffffff; stroke-width: .6; }
|
||||||
.chart-annotation-label { fill: var(--handoff); font: 10px sans-serif; }
|
.chart-annotation-label { fill: var(--handoff); font: 10px sans-serif; }
|
||||||
.world-line { fill: none; vector-effect: non-scaling-stroke; }
|
.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-static { stroke: #8d959d; stroke-width: .9; }
|
||||||
.world-future { stroke: #b6bec6; stroke-dasharray: 5 4; stroke-width: .85; }
|
.world-future { stroke: #b6bec6; stroke-dasharray: 5 4; stroke-width: .85; }
|
||||||
.world-active { stroke: #1769aa; stroke-width: 1.1; }
|
.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); } }
|
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 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() {
|
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 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 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 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"; };
|
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"; };
|
||||||
|
|||||||
@@ -29,6 +29,22 @@ internal static class WebAssetChecks
|
|||||||
Verification.True(css.Contains(".world-local-g2"), "Local G2 semantic class");
|
Verification.True(css.Contains(".world-local-g2"), "Local G2 semantic class");
|
||||||
Verification.True(css.Contains(".world-segment-active"), "active direction semantic class");
|
Verification.True(css.Contains(".world-segment-active"), "active direction semantic class");
|
||||||
Verification.True(css.Contains(".world-current"), "current trajectory semantic class");
|
Verification.True(css.Contains(".world-current"), "current trajectory semantic class");
|
||||||
|
Verification.True(css.Contains(".world-coordinate-grid"),
|
||||||
|
"overview exposes a metric grid semantic class");
|
||||||
|
Verification.True(css.Contains(".world-axis-zero"),
|
||||||
|
"overview exposes a zero-axis semantic class");
|
||||||
|
Verification.True(css.Contains(".world-axis-tick"),
|
||||||
|
"overview exposes coordinate tick semantics");
|
||||||
|
Verification.True(css.Contains(".world-axis-label"),
|
||||||
|
"overview exposes coordinate axis-label semantics");
|
||||||
|
Verification.True(js.Contains("function niceWorldStep("),
|
||||||
|
"overview selects readable metric tick steps");
|
||||||
|
Verification.True(js.Contains("function worldMetrics("),
|
||||||
|
"overview derives stable world-to-screen metrics");
|
||||||
|
Verification.True(js.Contains("function renderWorldCoordinates("),
|
||||||
|
"overview renders its own metric coordinate system");
|
||||||
|
Verification.True(js.Contains("X (m)") && js.Contains("Y (m)"),
|
||||||
|
"overview labels both metric axes");
|
||||||
Verification.True(css.Contains(".marker-vehicle"), "vehicle marker semantic class");
|
Verification.True(css.Contains(".marker-vehicle"), "vehicle marker semantic class");
|
||||||
Verification.True(css.Contains(".marker-plan-start"), "plan start marker semantic class");
|
Verification.True(css.Contains(".marker-plan-start"), "plan start marker semantic class");
|
||||||
Verification.True(css.Contains(".marker-gear-switch"), "gear-switch marker semantic class");
|
Verification.True(css.Contains(".marker-gear-switch"), "gear-switch marker semantic class");
|
||||||
|
|||||||
Reference in New Issue
Block a user