Files
ParkingRobot/ClumsyPilot/ParkrobTrajplanner/Trajplanner_output/trajectory-planning-flow-demo.html
T

414 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>轨迹规划全链路 Demo</title>
<style>
:root {
color-scheme: light dark;
--ink: light-dark(#172033, #e8eefb);
--muted: light-dark(#516078, #a9b6cc);
--surface: light-dark(#ffffff, #151c2a);
--canvas: light-dark(#f6f8fc, #0d1420);
--grid: light-dark(#d8dfeb, #2a374b);
--line: light-dark(#c9d2e1, #36445b);
--obstacle: light-dark(#67748a, #64748b);
--coarse: light-dark(#ca5a14, #ff9b5a);
--smooth: light-dark(#21835b, #64d99a);
--trajectory: light-dark(#1664c0, #71b5ff);
--selected: light-dark(#a33d95, #f58ce9);
--gear: light-dark(#a05800, #ffc65b);
--goal: light-dark(#a92f3b, #ff8995);
--start: light-dark(#2d7c4f, #74dd9b);
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--canvas);
color: var(--ink);
font-family: "Segoe UI", "Microsoft YaHei", sans-serif;
}
main { max-width: 1120px; margin: 0 auto; padding: 22px; }
h1 { margin: 0 0 6px; font-size: clamp(1.4rem, 3vw, 2rem); }
h2 { margin: 0; font-size: 1.05rem; }
p { margin: 8px 0; line-height: 1.6; }
.muted { color: var(--muted); }
.stage-nav { display: flex; flex-wrap: wrap; gap: 8px; margin: 18px 0; }
button {
border: 1px solid var(--line);
border-radius: 7px;
background: var(--surface);
color: var(--ink);
padding: 8px 12px;
cursor: pointer;
font: inherit;
}
button[aria-pressed="true"] { background: var(--trajectory); color: white; border-color: var(--trajectory); }
.layout { display: grid; grid-template-columns: minmax(0, 2fr) minmax(260px, 1fr); gap: 16px; }
.panel { background: var(--surface); border: 1px solid var(--line); border-radius: 10px; padding: 14px; }
.map-panel { min-width: 0; }
#planning-map { display: block; width: 100%; height: auto; background: var(--canvas); border: 1px solid var(--line); border-radius: 7px; }
.legend { display: flex; flex-wrap: wrap; gap: 8px 14px; margin-top: 10px; font-size: .85rem; }
.legend span { display: inline-flex; align-items: center; gap: 5px; }
.swatch { width: 20px; height: 0; border-top: 3px solid currentColor; display: inline-block; }
.dot { width: 11px; height: 11px; border-radius: 50%; display: inline-block; background: currentColor; }
.coarse { color: var(--coarse); } .smooth { color: var(--smooth); } .trajectory { color: var(--trajectory); }
.gear { color: var(--gear); } .goal { color: var(--goal); } .obstacle { color: var(--obstacle); }
.timeline-row { display: flex; align-items: center; gap: 10px; margin-top: 14px; }
#timeline { flex: 1; accent-color: var(--trajectory); }
.cards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin: 16px 0; }
.metric { background: var(--surface); border: 1px solid var(--line); border-radius: 8px; padding: 10px; }
.metric b { display: block; font-size: 1.05rem; margin-top: 4px; overflow-wrap: anywhere; }
.bottom { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
pre { margin: 10px 0 0; padding: 12px; overflow: auto; background: var(--canvas); border: 1px solid var(--line); border-radius: 7px; font-size: .78rem; line-height: 1.45; }
.boundary { border-left: 4px solid var(--trajectory); padding-left: 10px; }
.publish-rule { border-left: 4px solid var(--goal); padding-left: 10px; font-size: .9rem; }
.flow { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; font-size: .9rem; margin: 12px 0; }
.flow span { padding: 4px 7px; border: 1px solid var(--line); border-radius: 5px; background: var(--surface); }
.flow i { color: var(--muted); font-style: normal; }
.map-label { font-size: 12px; fill: var(--ink); paint-order: stroke; stroke: var(--canvas); stroke-width: 4px; stroke-linejoin: round; }
.axis-label { font-size: 12px; fill: var(--muted); }
@media (max-width: 760px) {
main { padding: 14px; }
.layout, .bottom { grid-template-columns: 1fr; }
.cards { grid-template-columns: 1fr; }
.timeline-row { align-items: stretch; flex-direction: column; }
}
</style>
</head>
<body>
<main>
<header>
<h1>轨迹规划全链路 Demo</h1>
<p class="muted">内置说明数据:同一米制地图中的地图、粗路径、平滑路径、EM 轨迹与控制输出。它用于理解数据边界,不调用 OSQP、定位、硬件或真实控制器。</p>
</header>
<div class="flow" aria-label="规划数据流">
<span>PlanningGridMap</span><i></i><span>Hybrid A*</span><i></i><span>Local G2</span><i></i><span>EmTrajectory</span><i></i><span>TrajectoryControlCommand</span>
</div>
<nav class="stage-nav" aria-label="规划阶段">
<button type="button" data-stage="map" aria-pressed="false">1. 地图</button>
<button type="button" data-stage="coarse" aria-pressed="false">2. 粗路径</button>
<button type="button" data-stage="smooth" aria-pressed="false">3. 平滑路径</button>
<button type="button" data-stage="em" aria-pressed="true">4. EM 轨迹</button>
<button type="button" data-stage="control" aria-pressed="false">5. 控制接口</button>
</nav>
<section class="layout" aria-label="规划地图与数据">
<section class="panel map-panel">
<h2 id="stage-title">EM 最终轨迹:可发布的时间参数化参考</h2>
<p id="stage-description" class="muted"></p>
<svg id="planning-map" viewBox="0 0 800 520" role="img" aria-label="停车场地图与轨迹"></svg>
<div class="legend" aria-label="路径图例">
<span class="obstacle"><i class="dot"></i>静态障碍物</span>
<span class="coarse coarse-path"><i class="swatch"></i>Hybrid A* 粗路径</span>
<span class="smooth smooth-path"><i class="swatch"></i>Local G2 平滑路径</span>
<span class="trajectory"><i class="swatch"></i>EmTrajectory</span>
<span class="gear"><i class="dot"></i>换挡点</span>
<span class="goal"><i class="dot"></i>最终 Goal</span>
</div>
<div class="timeline-row">
<label for="timeline">时间采样点</label>
<input id="timeline" type="range" min="0" max="5" value="0" aria-label="EM 轨迹时间点">
<output id="time-output" for="timeline">t = 0.0 s</output>
</div>
</section>
<aside class="panel">
<h2>规划身份</h2>
<div class="cards">
<div class="metric"><span class="muted">地图版本</span><b id="map-id"></b></div>
<div class="metric"><span class="muted">路径版本</span><b id="path-id"></b></div>
<div class="metric"><span class="muted">车辆状态序号</span><b id="state-id"></b></div>
</div>
<div class="metric"><span class="muted">当前轨迹 ID</span><b id="trajectory-id"></b></div>
<div class="metric"><span class="muted">effectiveAtUtc</span><b id="effective-at"></b></div>
<p class="publish-rule">仅当状态为 <code>Success</code><code>SuccessWithFallback</code>,且结果携带完整 <code>EmTrajectory</code> 时,才允许发布给执行层;其他状态只能输出诊断。</p>
</aside>
</section>
<section class="bottom" aria-label="阶段数据和控制边界">
<section class="panel">
<h2 id="data-title">EmTrajectory 点数据</h2>
<p id="data-note" class="muted"></p>
<pre id="data-sample" aria-live="polite"></pre>
</section>
<section class="panel">
<h2>控制层消费边界</h2>
<p id="control-note" class="boundary"></p>
<pre id="control-sample" aria-live="polite"></pre>
</section>
</section>
</main>
<script>
(() => {
"use strict";
const svgNamespace = "http:" + "//www.w3.org/2000/svg";
const identity = Object.freeze({
mapSnapshotId: 2026081001,
referencePathId: "parking-path-v17",
vehicleStateSequenceId: 42,
trajectoryId: "trajectory-42-parking-demo",
effectiveAtUtc: "2026-08-10T09:00:00.000Z"
});
const map = Object.freeze({
width: 4, height: 3,
start: { x: 0.55, y: 0.55, yaw: 0 },
goal: { x: 2.72, y: 2.18, yaw: 1.57 },
obstacles: [
{ x: 0.10, y: 1.82, width: 1.42, height: 0.40, label: "车位隔离带" },
{ x: 2.45, y: 0.18, width: 1.20, height: 0.48, label: "静态车辆" },
{ x: 3.08, y: 1.50, width: 0.64, height: 1.18, label: "墙体" }
]
});
const coarsePath = Object.freeze([
{ x: 0.55, y: 0.55, direction: "Forward" },
{ x: 1.05, y: 0.56, direction: "Forward" },
{ x: 1.55, y: 0.78, direction: "Forward" },
{ x: 2.05, y: 1.42, direction: "Forward", gear: true },
{ x: 2.24, y: 1.63, direction: "Reverse" },
{ x: 2.50, y: 1.91, direction: "Reverse" },
{ x: 2.72, y: 2.18, direction: "Reverse", goal: true }
]);
const smoothPath = Object.freeze([
{ x: 0.55, y: 0.55, curvature: 0.00, segment: 0 },
{ x: 0.95, y: 0.56, curvature: 0.02, segment: 0 },
{ x: 1.32, y: 0.68, curvature: 0.18, segment: 0 },
{ x: 1.68, y: 0.95, curvature: 0.31, segment: 0 },
{ x: 2.05, y: 1.42, curvature: 0.00, segment: 0, gear: true },
{ x: 2.20, y: 1.58, curvature: -0.10, segment: 1 },
{ x: 2.42, y: 1.78, curvature: -0.16, segment: 1 },
{ x: 2.58, y: 1.99, curvature: -0.09, segment: 1 },
{ x: 2.72, y: 2.18, curvature: 0.00, segment: 1, goal: true }
]);
const trajectory = Object.freeze([
{ timeFromStart: 0.0, x: 0.55, y: 0.55, yaw: 0.00, signedVelocity: 0.45, acceleration: 0.22, yawRate: 0.00, direction: "Forward", segmentIndex: 0, boundaryType: "None" },
{ timeFromStart: 1.0, x: 1.18, y: 0.63, yaw: 0.20, signedVelocity: 0.38, acceleration: -0.08, yawRate: 0.13, direction: "Forward", segmentIndex: 0, boundaryType: "None" },
{ timeFromStart: 2.0, x: 2.05, y: 1.42, yaw: 0.64, signedVelocity: 0.00, acceleration: 0.00, yawRate: 0.00, direction: "Forward", segmentIndex: 0, boundaryType: "GearSwitchApproach" },
{ timeFromStart: 2.2, x: 2.05, y: 1.42, yaw: 0.64, signedVelocity: 0.00, acceleration: 0.00, yawRate: 0.00, direction: "Reverse", segmentIndex: 1, boundaryType: "GearSwitchDeparture" },
{ timeFromStart: 3.2, x: 2.42, y: 1.78, yaw: 0.88, signedVelocity: -0.28, acceleration: -0.04, yawRate: -0.09, direction: "Reverse", segmentIndex: 1, boundaryType: "None" },
{ timeFromStart: 4.3, x: 2.72, y: 2.18, yaw: 1.57, signedVelocity: 0.00, acceleration: 0.00, yawRate: 0.00, direction: "Reverse", segmentIndex: 1, boundaryType: "Goal" }
]);
const stages = Object.freeze({
map: {
title: "地图:规划前冻结的世界模型",
description: "PlanningGridMap、车辆外廓、起点与终点属于同一业务快照;规划服务不自行读取传感器或系统时钟。",
dataTitle: "地图与状态输入",
dataNote: "这些输入绑定 mapSnapshotId 与车辆状态序号,是所有下游数据的一致性基础。"
},
coarse: {
title: "Hybrid A* 粗路径:可行驶的离散搜索结果",
description: "粗路径以离散姿态节点给出拓扑可行解,并在方向变化处标出换挡点。",
dataTitle: "CoarsePathResult(离散节点)",
dataNote: "粗路径用于后续平滑和诊断;它不是控制器按时间执行的命令。"
},
smooth: {
title: "Local G2 平滑路径:连续几何与方向段",
description: "平滑阶段把离散节点变成连续、曲率可控的参考路径,并明确前进段和倒车段边界。",
dataTitle: "PathSmoothingResult(连续几何)",
dataNote: "平滑路径提供 Frenet 参考和走廊构建依据;它仍不携带控制执行所需的时间信息。"
},
em: {
title: "EM 最终轨迹:可发布的时间参数化参考",
description: "EM 将平滑几何与纵向速度规划组合为按时间排序的 EmTrajectory 点;蓝色车辆为当前选中点。",
dataTitle: "EmTrajectoryPoint(当前时间采样)",
dataNote: "位置单位 m,航向 rad,速度 m/s,纵向加速度 m/s²,yaw rate rad/s。"
},
control: {
title: "控制接口:仅消费已验证轨迹的当前采样",
description: "执行层从完整 EmTrajectory 采样并映射为控制器中立 TrajectoryControlCommand;换挡和终点强制零运动。",
dataTitle: "EmTrajectory → 执行层采样",
dataNote: "轨迹身份用于安全交接与追溯;控制器并不以粗路径、平滑路径或优化中间量作为逐周期命令。"
}
});
const state = { stage: "em", selectedIndex: 0 };
const svg = document.getElementById("planning-map");
const timeline = document.getElementById("timeline");
const stageButtons = Array.from(document.querySelectorAll("[data-stage]"));
const stageTitle = document.getElementById("stage-title");
const stageDescription = document.getElementById("stage-description");
const dataTitle = document.getElementById("data-title");
const dataNote = document.getElementById("data-note");
const dataSample = document.getElementById("data-sample");
const controlSample = document.getElementById("control-sample");
const controlNote = document.getElementById("control-note");
const timeOutput = document.getElementById("time-output");
document.getElementById("map-id").textContent = identity.mapSnapshotId;
document.getElementById("path-id").textContent = identity.referencePathId;
document.getElementById("state-id").textContent = identity.vehicleStateSequenceId;
document.getElementById("trajectory-id").textContent = identity.trajectoryId;
document.getElementById("effective-at").textContent = identity.effectiveAtUtc;
function element(name, attributes, text) {
const node = document.createElementNS(svgNamespace, name);
Object.entries(attributes || {}).forEach(([key, value]) => node.setAttribute(key, String(value)));
if (text !== undefined) node.textContent = text;
return node;
}
function world(point) {
return { x: 85 + point.x * 155, y: 455 - point.y * 125 };
}
function appendText(x, y, text, extra) {
svg.appendChild(element("text", Object.assign({ x, y, class: "map-label" }, extra || {}), text));
}
function drawPolyline(points, stroke, width, dash) {
const coordinates = points.map(point => {
const p = world(point);
return `${p.x},${p.y}`;
}).join(" ");
svg.appendChild(element("polyline", {
points: coordinates, fill: "none", stroke, "stroke-width": width,
"stroke-linecap": "round", "stroke-linejoin": "round", "stroke-dasharray": dash || ""
}));
}
function drawCircle(point, radius, fill, label) {
const p = world(point);
svg.appendChild(element("circle", { cx: p.x, cy: p.y, r: radius, fill, stroke: "var(--surface)", "stroke-width": 2 }));
if (label) appendText(p.x + 9, p.y - 8, label);
}
function drawVehicle(point) {
const p = world(point);
const angle = -point.yaw * 180 / Math.PI;
const vehicle = element("g", { transform: `translate(${p.x} ${p.y}) rotate(${angle})` });
vehicle.appendChild(element("rect", { x: -21, y: -12, width: 42, height: 24, rx: 4, fill: "var(--trajectory)", stroke: "var(--surface)", "stroke-width": 2 }));
vehicle.appendChild(element("path", { d: "M 18 0 L 8 -6 L 8 6 Z", fill: "var(--surface)" }));
svg.appendChild(vehicle);
}
function drawMapBase() {
for (let x = 0; x <= map.width; x += 1) {
const a = world({ x, y: 0 }); const b = world({ x, y: map.height });
svg.appendChild(element("line", { x1: a.x, y1: a.y, x2: b.x, y2: b.y, stroke: "var(--grid)", "stroke-width": 1 }));
appendText(a.x - 4, a.y + 20, `${x} m`, { class: "axis-label" });
}
for (let y = 0; y <= map.height; y += 1) {
const a = world({ x: 0, y }); const b = world({ x: map.width, y });
svg.appendChild(element("line", { x1: a.x, y1: a.y, x2: b.x, y2: b.y, stroke: "var(--grid)", "stroke-width": 1 }));
appendText(a.x - 35, a.y + 4, `${y} m`, { class: "axis-label" });
}
const origin = world({ x: 0, y: 0 });
svg.appendChild(element("rect", { x: origin.x, y: origin.y - map.height * 125, width: map.width * 155, height: map.height * 125, fill: "none", stroke: "var(--line)", "stroke-width": 2 }));
map.obstacles.forEach(obstacle => {
const topLeft = world({ x: obstacle.x, y: obstacle.y + obstacle.height });
svg.appendChild(element("rect", { x: topLeft.x, y: topLeft.y, width: obstacle.width * 155, height: obstacle.height * 125, rx: 4, fill: "var(--obstacle)", opacity: .85 }));
appendText(topLeft.x + 4, topLeft.y + 16, obstacle.label, { fill: "white", stroke: "var(--obstacle)" });
});
drawCircle(map.start, 8, "var(--start)", "起点");
drawCircle(map.goal, 9, "var(--goal)", "Goal");
}
function drawCoarse() {
drawPolyline(coarsePath, "var(--coarse)", 3, "7 5");
coarsePath.forEach((point, index) => {
drawCircle(point, 4.5, "var(--coarse)", index === 0 ? "" : String(index));
if (index > 0 && index < coarsePath.length - 1) {
const p = world(point);
const sign = point.direction === "Forward" ? "↑ 前进" : "↓ 倒车";
appendText(p.x + 7, p.y + 17, sign, { class: "axis-label" });
}
});
}
function drawSmooth() {
drawPolyline(smoothPath, "var(--smooth)", 4);
const forward = smoothPath.filter(point => point.segment === 0);
const reverse = smoothPath.filter(point => point.segment === 1);
const f = world(forward[2]); const r = world(reverse[1]);
appendText(f.x - 4, f.y - 19, "方向段 0Forward,κmax = 0.31 1/m", { class: "axis-label" });
appendText(r.x + 9, r.y + 23, "方向段 1Reverse,κmax = 0.16 1/m", { class: "axis-label" });
}
function drawTrajectory() {
drawPolyline(trajectory, "var(--trajectory)", 4);
trajectory.forEach((point, index) => {
const selected = index === state.selectedIndex;
drawCircle(point, selected ? 8 : 4.5, selected ? "var(--selected)" : "var(--trajectory)", selected ? `t=${point.timeFromStart.toFixed(1)} s` : "");
});
}
function drawGearAndGoal() {
const gear = trajectory[2];
drawCircle(gear, 10, "var(--gear)", "停车换挡:s_end");
const goal = trajectory[trajectory.length - 1];
drawCircle(goal, 10, "var(--goal)", "最终终端:Goal");
}
function stageData(selectedPoint) {
if (state.stage === "map") {
return { PlanningGridMap: { boundsMeters: [0, map.width, 0, map.height], obstacleCount: map.obstacles.length, mapSnapshotId: identity.mapSnapshotId }, VehicleMotionState: { pose: map.start, signedLongitudinalSpeedMetersPerSecond: 0, sequenceId: identity.vehicleStateSequenceId } };
}
if (state.stage === "coarse") {
return { CoarsePathResult: { nodes: coarsePath, gearSwitchNodeIndex: 3, startDirection: "Forward", goalDirection: "Reverse" } };
}
if (state.stage === "smooth") {
return { PathSmoothingResult: { referencePathId: identity.referencePathId, directionSegments: [{ segmentIndex: 0, direction: "Forward" }, { segmentIndex: 1, direction: "Reverse" }], samples: smoothPath } };
}
return { EmTrajectoryMetadata: identity, EmTrajectoryPoint: selectedPoint };
}
function controlCommandForPoint(point) {
const holding = point.boundaryType === "GearSwitchApproach" ||
point.boundaryType === "GearSwitchDeparture" || point.boundaryType === "Goal";
return {
SignedLongitudinalVelocity: holding ? 0 : point.signedVelocity,
YawRate: holding ? 0 : point.yawRate,
Direction: point.direction,
RequestDirectionChange: point.boundaryType === "GearSwitchApproach",
HoldBrake: holding,
IsTrajectoryComplete: point.boundaryType === "Goal"
};
}
function render() {
const selectedPoint = trajectory[state.selectedIndex];
const stage = stages[state.stage];
svg.replaceChildren();
drawMapBase();
if (state.stage === "coarse" || state.stage === "smooth" || state.stage === "em" || state.stage === "control") drawCoarse();
if (state.stage === "smooth" || state.stage === "em" || state.stage === "control") drawSmooth();
if (state.stage === "em" || state.stage === "control") drawTrajectory();
if (state.stage !== "map") drawGearAndGoal();
drawVehicle(state.stage === "map" ? map.start : selectedPoint);
stageTitle.textContent = stage.title;
stageDescription.textContent = stage.description;
dataTitle.textContent = stage.dataTitle;
dataNote.textContent = stage.dataNote;
dataSample.textContent = JSON.stringify(stageData(selectedPoint), null, 2);
timeOutput.textContent = `t = ${selectedPoint.timeFromStart.toFixed(1)} s`;
timeline.disabled = state.stage === "map" || state.stage === "coarse" || state.stage === "smooth";
if (state.stage === "control") {
controlNote.textContent = "控制层当前只消费完整轨迹的采样结果或执行层导出的 TrajectoryControlCommand。粗路径、平滑路径、地图和优化诊断属于观测、回放与问题定位证据链,不是逐周期底盘命令。";
controlSample.textContent = JSON.stringify({ trajectoryId: identity.trajectoryId, effectiveAtUtc: identity.effectiveAtUtc, command: controlCommandForPoint(selectedPoint) }, null, 2);
} else {
controlNote.textContent = "此阶段尚未形成控制命令。只有完整且已验证的 EmTrajectory 才能进入执行层采样。";
controlSample.textContent = "等待控制接口阶段:TrajectoryControlCommand 未生成。";
}
}
stageButtons.forEach(button => {
button.addEventListener("click", () => {
state.stage = button.dataset.stage;
stageButtons.forEach(candidate => candidate.setAttribute("aria-pressed", String(candidate === button)));
render();
});
});
timeline.addEventListener("input", () => {
state.selectedIndex = Number(timeline.value);
render();
});
render();
})();
</script>
</body>
</html>