lijinghai's picture
Update coverage Space hero copy
a064d46 verified
Raw History Blame Contribute Delete
14.4 kB
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Map-aware AMR coverage planning for RabbitRobot, supporting real-robot navigation and VLN/VLR evaluation with footprint clearance and known-free clipping.">
<title>RabbitRobot Coverage Path Planning</title>
<style>
:root {
color-scheme: light;
--ink: #172033;
--muted: #5b667a;
--line: #d8dee8;
--panel: #f7f9fc;
--green: #138a4f;
--blue: #2563eb;
--orange: #f97316;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: var(--ink);
background: #eef3f8;
}
header {
padding: 28px clamp(18px, 4vw, 48px) 18px;
background: #ffffff;
border-bottom: 1px solid var(--line);
}
h1 {
margin: 0 0 8px;
font-size: clamp(26px, 4vw, 42px);
line-height: 1.1;
letter-spacing: 0;
}
p {
margin: 0;
color: var(--muted);
line-height: 1.7;
}
main {
display: grid;
grid-template-columns: minmax(280px, 360px) minmax(0, 1fr);
gap: 18px;
padding: 18px clamp(14px, 3vw, 32px) 28px;
}
.panel {
background: #ffffff;
border: 1px solid var(--line);
border-radius: 8px;
}
.controls {
padding: 18px;
align-self: start;
}
.control {
display: grid;
grid-template-columns: 1fr auto;
gap: 8px 12px;
align-items: center;
margin-bottom: 16px;
}
label {
font-size: 14px;
font-weight: 650;
color: #253149;
}
output {
min-width: 52px;
text-align: right;
color: var(--muted);
font-variant-numeric: tabular-nums;
font-size: 13px;
}
input[type="range"] {
grid-column: 1 / -1;
width: 100%;
accent-color: var(--green);
}
.toggle {
display: flex;
gap: 8px;
align-items: center;
margin: 4px 0 18px;
color: var(--muted);
font-size: 14px;
}
button {
width: 100%;
border: 0;
border-radius: 7px;
padding: 12px 14px;
color: #fff;
background: var(--green);
font-weight: 750;
cursor: pointer;
}
button:hover { background: #0f7643; }
.stage {
padding: 14px;
min-width: 0;
}
canvas {
width: 100%;
height: auto;
display: block;
border: 1px solid var(--line);
border-radius: 6px;
background: #fff;
}
.summary {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
margin-top: 12px;
}
.metric {
border: 1px solid var(--line);
border-radius: 7px;
padding: 10px 12px;
background: var(--panel);
}
.metric strong {
display: block;
color: var(--ink);
font-size: 19px;
line-height: 1.1;
font-variant-numeric: tabular-nums;
}
.metric span {
display: block;
margin-top: 6px;
color: var(--muted);
font-size: 12px;
line-height: 1.35;
}
.note {
margin-top: 12px;
border-left: 4px solid var(--orange);
padding: 10px 12px;
background: #fff8f1;
color: #5d4030;
border-radius: 5px;
line-height: 1.65;
font-size: 14px;
}
@media (max-width: 920px) {
main { grid-template-columns: 1fr; }
.summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 520px) {
.summary { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<header>
<h1>RabbitRobot Coverage Path Planning</h1>
<p>Map-aware AMR coverage planning for RabbitRobot, supporting real-robot navigation and VLN/VLR evaluation with footprint clearance, known-free clipping and safe waypoint generation.</p>
</header>
<main>
<section class="panel controls" aria-label="coverage controls">
<div class="control">
<label for="xMin">区域 x_min</label><output id="xMinOut"></output>
<input id="xMin" type="range" min="-2" max="48" step="0.5" value="3">
</div>
<div class="control">
<label for="xMax">区域 x_max</label><output id="xMaxOut"></output>
<input id="xMax" type="range" min="8" max="62" step="0.5" value="50">
</div>
<div class="control">
<label for="yMin">区域 y_min</label><output id="yMinOut"></output>
<input id="yMin" type="range" min="0" max="34" step="0.5" value="5">
</div>
<div class="control">
<label for="yMax">区域 y_max</label><output id="yMaxOut"></output>
<input id="yMax" type="range" min="8" max="40" step="0.5" value="32">
</div>
<div class="control">
<label for="lane">覆盖行距</label><output id="laneOut"></output>
<input id="lane" type="range" min="0.4" max="2.0" step="0.05" value="0.8">
</div>
<div class="control">
<label for="robotW">车体宽度</label><output id="robotWOut"></output>
<input id="robotW" type="range" min="0.25" max="1.0" step="0.01" value="0.486">
</div>
<div class="control">
<label for="robotL">车体长度</label><output id="robotLOut"></output>
<input id="robotL" type="range" min="0.25" max="1.2" step="0.01" value="0.45">
</div>
<div class="control">
<label for="margin">安全边距</label><output id="marginOut"></output>
<input id="margin" type="range" min="0" max="0.8" step="0.01" value="0.08">
</div>
<label class="toggle">
<input id="mapAware" type="checkbox" checked>
启用 map-aware free-space clipping
</label>
<button id="run">Generate coverage path</button>
</section>
<section class="panel stage" aria-label="coverage map">
<canvas id="map" width="760" height="440"></canvas>
<div class="summary">
<div class="metric"><strong id="wp">0</strong><span>waypoints</span></div>
<div class="metric"><strong id="area">0.0</strong><span>estimated m²</span></div>
<div class="metric"><strong id="length">0.0</strong><span>path length m</span></div>
<div class="metric"><strong id="clearance">0.00</strong><span>center clearance m</span></div>
</div>
<div class="note" id="note"></div>
</section>
</main>
<script>
const W = 760, H = 440, RES = 0.10;
const controls = ["xMin","xMax","yMin","yMax","lane","robotW","robotL","margin"];
const canvas = document.getElementById("map");
const ctx = canvas.getContext("2d");
function val(id) { return Number(document.getElementById(id).value); }
function px(x, y) { return [Math.round(x / RES) + 50, H - (Math.round(y / RES) + 38)]; }
function buildMap() {
const grid = new Int8Array(W * H);
const setRect = (x0, y0, x1, y1, v) => {
for (let y = Math.max(0,y0); y < Math.min(H,y1); y++) {
for (let x = Math.max(0,x0); x < Math.min(W,x1); x++) grid[y * W + x] = v;
}
};
setRect(0, 0, W, 30, -1);
setRect(0, H - 28, W, H, -1);
setRect(0, 0, 34, H, -1);
setRect(W - 38, 0, W, H, -1);
setRect(540, 328, 730, 410, -1);
[[95,78,182,124],[258,70,303,220],[420,62,464,156],[558,102,668,148],[155,250,255,300],[348,264,432,312],[510,225,603,270],[638,215,682,326]].forEach(r => setRect(...r, 1));
setRect(64, 56, 690, 61, 1);
setRect(64, 374, 520, 379, 1);
setRect(64, 56, 69, 379, 1);
setRect(690, 56, 695, 328, 1);
return grid;
}
function freeMask(grid, clearancePx, mapAware) {
const mask = new Uint8Array(W * H);
mask.fill(1);
for (let i = 0; i < grid.length; i++) {
if (mapAware ? grid[i] !== 0 : grid[i] === 1) {
const x = i % W, y = Math.floor(i / W);
for (let yy = Math.max(0, y - clearancePx); yy <= Math.min(H - 1, y + clearancePx); yy++) {
for (let xx = Math.max(0, x - clearancePx); xx <= Math.min(W - 1, x + clearancePx); xx++) mask[yy * W + xx] = 0;
}
}
}
return mask;
}
function segments(row, minLen = 18) {
const out = [];
let start = -1;
for (let i = 0; i < row.length; i++) {
if (row[i] && start < 0) start = i;
if ((!row[i] || i === row.length - 1) && start >= 0) {
const end = row[i] && i === row.length - 1 ? i : i - 1;
if (end - start + 1 >= minLen) out.push([start, end]);
start = -1;
}
}
return out;
}
function lineFree(a, b, mask) {
const dx = b[0] - a[0], dy = b[1] - a[1];
const steps = Math.max(Math.abs(dx), Math.abs(dy), 1);
for (let i = 0; i <= steps; i++) {
const x = Math.round(a[0] + dx * i / steps);
const y = Math.round(a[1] + dy * i / steps);
if (x < 0 || x >= W || y < 0 || y >= H || !mask[y * W + x]) return false;
}
return true;
}
function connector(a, b, mask) {
if (lineFree(a, b, mask)) return [a, b];
for (const c of [[a[0], b[1]], [b[0], a[1]]]) {
if (lineFree(a, c, mask) && lineFree(c, b, mask)) return [a, c, b];
}
return null;
}
function plan() {
controls.forEach(id => document.getElementById(id + "Out").textContent = `${val(id).toFixed(id.includes("robot") || id === "margin" ? 2 : 1)} m`);
const grid = buildMap();
const clearanceM = Math.hypot(val("robotW"), val("robotL")) / 2 + val("margin");
const clearancePx = Math.max(1, Math.round(clearanceM / RES));
const mask = freeMask(grid, clearancePx, document.getElementById("mapAware").checked);
const [aX, aY] = px(Math.min(val("xMin"), val("xMax")), Math.min(val("yMin"), val("yMax")));
const [bX, bY] = px(Math.max(val("xMin"), val("xMax")), Math.max(val("yMin"), val("yMax")));
const x0 = Math.max(0, Math.min(aX, bX)), x1 = Math.min(W - 1, Math.max(aX, bX));
const y0 = Math.max(0, Math.min(aY, bY)), y1 = Math.min(H - 1, Math.max(aY, bY));
const laneStep = Math.max(8, Math.round(val("lane") / RES));
const waypoints = [], lines = [];
let previous = null, direction = 1, sampled = 0;
for (let y = y0; y <= y1; y += laneStep) {
const row = [];
for (let x = x0; x <= x1; x++) row.push(mask[y * W + x]);
for (const [s, e] of segments(row)) {
const sx = x0 + s, ex = x0 + e;
const start = direction > 0 ? [sx, y] : [ex, y];
const end = direction > 0 ? [ex, y] : [sx, y];
sampled += ex - sx + 1;
if (previous) {
const c = connector(previous, start, mask);
if (c) lines.push(c);
}
lines.push([start, end]);
waypoints.push(start, end);
previous = end;
direction *= -1;
}
}
draw(grid, mask, [x0, y0, x1, y1], lines, waypoints, clearancePx);
const lenPx = lines.reduce((sum, line) => sum + line.slice(1).reduce((s, p, i) => s + Math.hypot(p[0] - line[i][0], p[1] - line[i][1]), 0), 0);
document.getElementById("wp").textContent = waypoints.length;
document.getElementById("area").textContent = (sampled * val("lane") * RES).toFixed(1);
document.getElementById("length").textContent = (lenPx * RES).toFixed(1);
document.getElementById("clearance").textContent = clearanceM.toFixed(2);
document.getElementById("note").textContent = "工程含义:先把选择区域裁剪到已知 free-space,再按车体 footprint 与安全边距膨胀障碍物。真实机器人执行时,覆盖路径仍应接入 Nav2、velocity smoother 与 collision monitor,而不是让规划节点直接控制底盘。";
}
function draw(grid, mask, selected, lines, waypoints, clearancePx) {
const img = ctx.createImageData(W, H);
for (let i = 0; i < grid.length; i++) {
let c = grid[i] === 0 ? [244,247,250] : grid[i] < 0 ? [199,205,214] : [31,41,55];
if (grid[i] === 0 && mask[i]) c = [236,252,242];
img.data[i * 4] = c[0]; img.data[i * 4 + 1] = c[1]; img.data[i * 4 + 2] = c[2]; img.data[i * 4 + 3] = 255;
}
ctx.putImageData(img, 0, 0);
const [x0, y0, x1, y1] = selected;
ctx.fillStyle = "rgba(37,99,235,.12)";
ctx.strokeStyle = "rgba(37,99,235,.96)";
ctx.lineWidth = 3;
ctx.fillRect(x0, y0, x1 - x0, y1 - y0);
ctx.strokeRect(x0, y0, x1 - x0, y1 - y0);
ctx.strokeStyle = "#138a4f";
ctx.lineWidth = 4;
ctx.lineCap = "round";
lines.forEach(line => {
ctx.beginPath();
ctx.moveTo(line[0][0], line[0][1]);
line.slice(1).forEach(p => ctx.lineTo(p[0], p[1]));
ctx.stroke();
});
waypoints.forEach((p, i) => {
ctx.beginPath();
ctx.fillStyle = i === 0 ? "#166534" : i === waypoints.length - 1 ? "#dc2626" : "#15803d";
ctx.arc(p[0], p[1], i === 0 || i === waypoints.length - 1 ? 5 : 3.5, 0, Math.PI * 2);
ctx.fill();
});
ctx.fillStyle = "rgba(255,255,255,.92)";
ctx.strokeStyle = "#d8dee8";
ctx.lineWidth = 1;
roundRect(16, 16, 310, 96, 8);
ctx.fill();
ctx.stroke();
ctx.fillStyle = "#172033";
ctx.font = "14px system-ui";
ctx.fillText("RabbitRobot map-aware coverage", 30, 42);
[["safe free-space","#ecfcf2"],["unknown / off-map","#c7cdd6"],["obstacle source","#1f2937"],[`clearance radius ${clearancePx}px`,"#138a4f"]].forEach((item, i) => {
const y = 58 + i * 14;
ctx.fillStyle = item[1];
ctx.fillRect(30, y - 8, 14, 8);
ctx.fillStyle = "#5b667a";
ctx.font = "12px system-ui";
ctx.fillText(item[0], 52, y);
});
}
function roundRect(x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
}
document.getElementById("run").addEventListener("click", plan);
controls.concat(["mapAware"]).forEach(id => document.getElementById(id).addEventListener("input", plan));
plan();
</script>
</body>
</html>