Download index.html from lijinghai/RabbitRobot-CoveragePath-Demo: direct link, hf CLI and curl.
- Browser
- Download file 14.4 kB
-
https://huggingface.co/spaces/lijinghai/RabbitRobot-CoveragePath-Demo/resolve/main/index.html
- Command line
-
hf download hf://spaces/lijinghai/RabbitRobot-CoveragePath-Demo/index.html
-
curl -L -o index.html https://huggingface.co/spaces/lijinghai/RabbitRobot-CoveragePath-Demo/resolve/main/index.html
14.4 kB
| <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> | |