fix: clarify EM overview path layers

This commit is contained in:
梁薄云
2026-08-09 19:17:22 +08:00
parent 76fa2900ce
commit bc76ff45c8
3 changed files with 126 additions and 53 deletions
@@ -102,46 +102,86 @@
}
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 lineKind(line) { return String(line && line.kind || "").toLowerCase(); }
function markerTransform(position, unitsPerPixel, headingRadians = 0) {
const headingDegrees = (finite(headingRadians) ? headingRadians : 0) * 180 / Math.PI;
return `translate(${position.x} ${-position.y}) rotate(${-headingDegrees}) scale(${unitsPerPixel})`;
}
function appendWorldMarker(marker, frame, metrics) {
if (!marker.position || !finite(marker.position.x) || !finite(marker.position.y)) return;
const kind = String(marker.kind || "").toLowerCase();
const pose = frame.vehiclePose;
const heading = kind === "vehicle" && pose && finite(pose.headingRadians) ? pose.headingRadians : 0;
const group = svg("g", {
class: `world-marker-group marker-${kind || "generic"}`,
transform: markerTransform(marker.position, metrics.unitsPerPixel, heading)
});
if (kind === "vehicle") {
group.append(svg("path", { d: "M -9 -5 L 5 -5 L 9 0 L 5 5 L -9 5 Z", class: "world-marker-shape vehicle-outline" }));
group.append(svg("line", { x1: -4, y1: 0, x2: 7, y2: 0, class: "vehicle-heading" }));
} else if (kind === "smooth-start") {
group.append(svg("circle", { cx: 0, cy: 0, r: 5, class: "world-marker-shape marker-smooth-start" }));
} else if (kind === "plan-start") {
group.append(svg("circle", { cx: 0, cy: 0, r: 5, class: "world-marker-shape marker-plan-start" }));
} else if (kind === "gear-switch" || kind === "gear-switch-end") {
group.append(svg("path", { d: "M 0 -6 L 6 0 L 0 6 L -6 0 Z", class: "world-marker-shape marker-gear-switch" }));
} else if (kind === "final-goal") {
group.append(svg("path", { d: "M -6 -6 L 6 -6 L 6 6 L -6 6 Z", class: "world-marker-shape marker-final-goal" }));
} else {
group.append(svg("path", { d: "M 0 -6 L 6 0 L 0 6 L -6 0 Z", class: "world-marker-shape world-marker" }));
}
overlay.append(group);
const labelY = kind === "plan-start" ? 17 : -9;
const labelGroup = svg("g", { transform: `translate(${marker.position.x} ${-marker.position.y}) scale(${metrics.unitsPerPixel})` });
const label = svg("text", { x: 10, y: labelY, class: "world-marker-label" });
label.textContent = marker.labelChinese || "";
labelGroup.append(label);
overlay.append(labelGroup);
}
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"; };
const dynamicClass = line => { const kind = String(line.kind || "").toLowerCase(); if (kind === "current") return "world-current"; if (kind === "previous" || line.lineStyle === 1) return "world-previous"; return "world-current"; };
safeArray(snapshot.staticPolylines).forEach(line => addLine(line, staticClass(line)));
safeArray(snapshot.directionSegments).forEach(segment => addLine(segment, segment.segmentIndex === frame.activeSegmentIndex ? "world-segment-active" : "world-direction-inactive"));
safeArray(frame.dynamicPolylines).forEach(line => addLine(line, dynamicClass(line)));
const hasPath = safeArray(snapshot.staticPolylines).concat(safeArray(snapshot.directionSegments), safeArray(frame.dynamicPolylines))
.some(line => safeArray(line && line.points).some(point => point && finite(point.x) && finite(point.y)));
const appendMarker = marker => {
if (!marker.position || !finite(marker.position.x) || !finite(marker.position.y)) return;
const x = marker.position.x, y = -marker.position.y; const kind = String(marker.kind || "").toLowerCase();
if (kind === "vehicle") {
const pose = frame.vehiclePose; const heading = pose && finite(pose.headingRadians) ? pose.headingRadians : 0;
const cos = Math.cos(heading), sin = Math.sin(heading);
const corners = [[.38, .18], [-.38, .18], [-.38, -.18], [.38, -.18]].map(([dx, dy]) => {
const wx = marker.position.x + dx * cos - dy * sin; const wy = marker.position.y + dx * sin + dy * cos; return `${wx} ${-wy}`;
});
const group = svg("g", { class: "marker-vehicle" });
group.append(svg("path", { d: `M ${corners[0]} L ${corners[1]} L ${corners[2]} L ${corners[3]} Z`, class: "vehicle-outline" }));
group.append(svg("line", { x1: x, y1: y, x2: x + .22 * cos, y2: y - .22 * sin, class: "vehicle-heading" }));
overlay.append(group);
} else if (kind === "plan-start") {
overlay.append(svg("circle", { cx: x, cy: y, r: ".12", class: "marker-plan-start" }));
} else if (kind === "gear-switch" || kind === "gear-switch-end") {
const size = ".14"; overlay.append(svg("path", { d: `M ${x} ${y - size} L ${x + size} ${y} L ${x} ${y + size} L ${x - size} ${y} Z`, class: "marker-gear-switch" }));
} else if (kind === "final-goal") {
const size = ".13"; overlay.append(svg("path", { d: `M ${x - size} ${y - size} L ${x + size} ${y - size} L ${x + size} ${y + size} L ${x - size} ${y + size} Z`, class: "marker-final-goal" }));
} else {
const size = ".18"; overlay.append(svg("path", { d: `M ${x} ${y - size} L ${x + size} ${y} L ${x} ${y + size} L ${x - size} ${y} Z`, class: "world-marker" }));
}
const label = svg("text", { x: x + .2, y: y - .2, fill: "#20252b", "font-size": ".32" }); label.textContent = marker.labelChinese || ""; overlay.append(label);
const bounds = getBounds();
const metrics = worldMetrics(bounds);
drawOccupancy(bounds);
overlay.replaceChildren();
overlay.setAttribute("viewBox", `${bounds.xMin} ${-bounds.yMax} ${bounds.xMax - bounds.xMin} ${bounds.yMax - bounds.yMin}`);
overlay.setAttribute("preserveAspectRatio", "xMidYMid meet");
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 }));
};
safeArray(snapshot.staticMarkers).concat(safeArray(frame.dynamicMarkers)).forEach(appendMarker);
const staticLines = safeArray(snapshot.staticPolylines);
staticLines.filter(line => lineKind(line) === "global" || lineKind(line) === "coarse")
.forEach(line => addLine(line, "world-coarse"));
staticLines.filter(line => lineKind(line).includes("local-g2") || lineKind(line).includes("g2"))
.forEach(line => addLine(line, "world-local-g2"));
staticLines.filter(line => lineKind(line) !== "global" && lineKind(line) !== "coarse" && !lineKind(line).includes("local-g2") && !lineKind(line).includes("g2"))
.forEach(line => addLine(line, "world-static"));
safeArray(snapshot.directionSegments).forEach(segment => addLine(segment,
segment.segmentIndex === frame.activeSegmentIndex ? "world-segment-active" : "world-direction-inactive"));
const dynamicLines = safeArray(frame.dynamicPolylines);
dynamicLines.filter(line => lineKind(line) === "active-segment")
.forEach(line => addLine(line, "world-segment-active"));
dynamicLines.filter(line => lineKind(line) === "previous" || line.lineStyle === 1)
.forEach(line => addLine(line, "world-previous"));
dynamicLines.filter(line => lineKind(line) !== "active-segment" && lineKind(line) !== "previous" && line.lineStyle !== 1)
.forEach(line => addLine(line, "world-current"));
const allLines = staticLines.concat(safeArray(snapshot.directionSegments), dynamicLines);
const hasPath = allLines.some(line => safeArray(line && line.points)
.some(point => point && finite(point.x) && finite(point.y)));
safeArray(snapshot.staticMarkers).concat(safeArray(frame.dynamicMarkers))
.forEach(marker => appendWorldMarker(marker, frame, metrics));
if (!hasPath) {
const empty = svg("text", { x: (bounds.xMin + bounds.xMax) / 2, y: -((bounds.yMin + bounds.yMax) / 2), class: "world-empty-state", "text-anchor": "middle" });
empty.textContent = "未收到 Local G2 路径"; overlay.append(empty);
const emptyGroup = svg("g", { transform: `translate(${(bounds.xMin + bounds.xMax) / 2} ${-((bounds.yMin + bounds.yMax) / 2)}) scale(${metrics.unitsPerPixel})` });
const empty = svg("text", { x: 0, y: 0, class: "world-empty-state", "text-anchor": "middle" });
empty.textContent = "未收到 Local G2 路径";
emptyGroup.append(empty);
overlay.append(emptyGroup);
}
}
@@ -323,6 +363,14 @@
function receiveFrame(payload) { state.frame = payload.snapshot || null; state.history = safeArray(payload.history); state.lastFrameAt = Date.now(); if (!state.ended) setLiveState(state.frame && state.frame.sessionStateChinese || "运行中"); renderAll(); }
function startEvents() { const events = new EventSource(authorized("/api/events")); events.addEventListener("frame", event => { try { receiveFrame(JSON.parse(event.data)); } catch (error) { state.redrawEnabled[state.activeTab] = false; setLiveState("页面绘图异常", "failure"); } }); events.addEventListener("end", () => { state.ended = true; events.close(); setLiveState("会话已结束"); }); events.onerror = () => { if (!state.ended) setLiveState("连接中断,正在重连", "notice"); }; }
async function boot() { installTabs(); ["overview", "ls-st", "kinematics", "history-config"].forEach(tab => { state.redrawEnabled[tab] = true; }); try { const response = await fetch(authorized("/api/bootstrap"), { cache: "no-store" }); if (!response.ok) throw new Error("bootstrap failed"); const bootstrap = await response.json(); state.staticSnapshot = bootstrap.staticSnapshot || bootstrap; state.refreshRateHz = finite(bootstrap.refreshRateHz) && bootstrap.refreshRateHz > 0 ? bootstrap.refreshRateHz : 10; renderAll(); startEvents(); window.setInterval(updateConnectionState, 250); } catch (error) { setLiveState("页面绘图异常", "failure"); } }
let overviewResizeFrame = 0;
window.addEventListener("resize", () => {
if (overviewResizeFrame || state.activeTab !== "overview" || !state.redrawEnabled.overview) return;
overviewResizeFrame = window.requestAnimationFrame(() => {
overviewResizeFrame = 0;
renderWorld();
});
});
window.addEventListener("error", () => { state.redrawEnabled[state.activeTab] = false; setLiveState("页面绘图异常", "failure"); }); window.addEventListener("unhandledrejection", () => { state.redrawEnabled[state.activeTab] = false; setLiveState("页面绘图异常", "failure"); });
if (window.__TRAJECTORY_VISUALIZATION_TEST__ === true) {
["overview", "ls-st", "kinematics", "history-config"].forEach(tab => { state.redrawEnabled[tab] = true; });