lijinghai commited on
Commit
0eea6ef
·
verified ·
1 Parent(s): 73739cf

Add static coverage planner demo

Browse files
Files changed (1) hide show
  1. index.html +416 -17
index.html CHANGED
@@ -1,19 +1,418 @@
1
  <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
  <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>RabbitRobot Coverage Path Demo</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: light;
10
+ --ink: #172033;
11
+ --muted: #5b667a;
12
+ --line: #d8dee8;
13
+ --panel: #f7f9fc;
14
+ --green: #138a4f;
15
+ --blue: #2563eb;
16
+ --orange: #f97316;
17
+ }
18
+
19
+ * { box-sizing: border-box; }
20
+
21
+ body {
22
+ margin: 0;
23
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
24
+ color: var(--ink);
25
+ background: #eef3f8;
26
+ }
27
+
28
+ header {
29
+ padding: 28px clamp(18px, 4vw, 48px) 18px;
30
+ background: #ffffff;
31
+ border-bottom: 1px solid var(--line);
32
+ }
33
+
34
+ h1 {
35
+ margin: 0 0 8px;
36
+ font-size: clamp(26px, 4vw, 42px);
37
+ line-height: 1.1;
38
+ letter-spacing: 0;
39
+ }
40
+
41
+ p {
42
+ margin: 0;
43
+ color: var(--muted);
44
+ line-height: 1.7;
45
+ }
46
+
47
+ main {
48
+ display: grid;
49
+ grid-template-columns: minmax(280px, 360px) minmax(0, 1fr);
50
+ gap: 18px;
51
+ padding: 18px clamp(14px, 3vw, 32px) 28px;
52
+ }
53
+
54
+ .panel {
55
+ background: #ffffff;
56
+ border: 1px solid var(--line);
57
+ border-radius: 8px;
58
+ }
59
+
60
+ .controls {
61
+ padding: 18px;
62
+ align-self: start;
63
+ }
64
+
65
+ .control {
66
+ display: grid;
67
+ grid-template-columns: 1fr auto;
68
+ gap: 8px 12px;
69
+ align-items: center;
70
+ margin-bottom: 16px;
71
+ }
72
+
73
+ label {
74
+ font-size: 14px;
75
+ font-weight: 650;
76
+ color: #253149;
77
+ }
78
+
79
+ output {
80
+ min-width: 52px;
81
+ text-align: right;
82
+ color: var(--muted);
83
+ font-variant-numeric: tabular-nums;
84
+ font-size: 13px;
85
+ }
86
+
87
+ input[type="range"] {
88
+ grid-column: 1 / -1;
89
+ width: 100%;
90
+ accent-color: var(--green);
91
+ }
92
+
93
+ .toggle {
94
+ display: flex;
95
+ gap: 8px;
96
+ align-items: center;
97
+ margin: 4px 0 18px;
98
+ color: var(--muted);
99
+ font-size: 14px;
100
+ }
101
+
102
+ button {
103
+ width: 100%;
104
+ border: 0;
105
+ border-radius: 7px;
106
+ padding: 12px 14px;
107
+ color: #fff;
108
+ background: var(--green);
109
+ font-weight: 750;
110
+ cursor: pointer;
111
+ }
112
+
113
+ button:hover { background: #0f7643; }
114
+
115
+ .stage {
116
+ padding: 14px;
117
+ min-width: 0;
118
+ }
119
+
120
+ canvas {
121
+ width: 100%;
122
+ height: auto;
123
+ display: block;
124
+ border: 1px solid var(--line);
125
+ border-radius: 6px;
126
+ background: #fff;
127
+ }
128
+
129
+ .summary {
130
+ display: grid;
131
+ grid-template-columns: repeat(4, minmax(0, 1fr));
132
+ gap: 10px;
133
+ margin-top: 12px;
134
+ }
135
+
136
+ .metric {
137
+ border: 1px solid var(--line);
138
+ border-radius: 7px;
139
+ padding: 10px 12px;
140
+ background: var(--panel);
141
+ }
142
+
143
+ .metric strong {
144
+ display: block;
145
+ color: var(--ink);
146
+ font-size: 19px;
147
+ line-height: 1.1;
148
+ font-variant-numeric: tabular-nums;
149
+ }
150
+
151
+ .metric span {
152
+ display: block;
153
+ margin-top: 6px;
154
+ color: var(--muted);
155
+ font-size: 12px;
156
+ line-height: 1.35;
157
+ }
158
+
159
+ .note {
160
+ margin-top: 12px;
161
+ border-left: 4px solid var(--orange);
162
+ padding: 10px 12px;
163
+ background: #fff8f1;
164
+ color: #5d4030;
165
+ border-radius: 5px;
166
+ line-height: 1.65;
167
+ font-size: 14px;
168
+ }
169
+
170
+ @media (max-width: 920px) {
171
+ main { grid-template-columns: 1fr; }
172
+ .summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
173
+ }
174
+
175
+ @media (max-width: 520px) {
176
+ .summary { grid-template-columns: 1fr; }
177
+ }
178
+ </style>
179
+ </head>
180
+ <body>
181
+ <header>
182
+ <h1>RabbitRobot Coverage Path Demo</h1>
183
+ <p>面向 AMR 清扫 / 巡检场景的公开静态演示:在合成占据栅格上做区域选择、车体安全膨胀、known free-space 裁剪和往返式覆盖路径生成。</p>
184
+ </header>
185
+
186
+ <main>
187
+ <section class="panel controls" aria-label="coverage controls">
188
+ <div class="control">
189
+ <label for="xMin">区域 x_min</label><output id="xMinOut"></output>
190
+ <input id="xMin" type="range" min="-2" max="48" step="0.5" value="3">
191
+ </div>
192
+ <div class="control">
193
+ <label for="xMax">区域 x_max</label><output id="xMaxOut"></output>
194
+ <input id="xMax" type="range" min="8" max="62" step="0.5" value="50">
195
+ </div>
196
+ <div class="control">
197
+ <label for="yMin">区域 y_min</label><output id="yMinOut"></output>
198
+ <input id="yMin" type="range" min="0" max="34" step="0.5" value="5">
199
+ </div>
200
+ <div class="control">
201
+ <label for="yMax">区域 y_max</label><output id="yMaxOut"></output>
202
+ <input id="yMax" type="range" min="8" max="40" step="0.5" value="32">
203
+ </div>
204
+ <div class="control">
205
+ <label for="lane">覆盖行距</label><output id="laneOut"></output>
206
+ <input id="lane" type="range" min="0.4" max="2.0" step="0.05" value="0.8">
207
+ </div>
208
+ <div class="control">
209
+ <label for="robotW">车体宽度</label><output id="robotWOut"></output>
210
+ <input id="robotW" type="range" min="0.25" max="1.0" step="0.01" value="0.486">
211
+ </div>
212
+ <div class="control">
213
+ <label for="robotL">车体长度</label><output id="robotLOut"></output>
214
+ <input id="robotL" type="range" min="0.25" max="1.2" step="0.01" value="0.45">
215
+ </div>
216
+ <div class="control">
217
+ <label for="margin">安全边距</label><output id="marginOut"></output>
218
+ <input id="margin" type="range" min="0" max="0.8" step="0.01" value="0.08">
219
+ </div>
220
+ <label class="toggle">
221
+ <input id="mapAware" type="checkbox" checked>
222
+ 启用 map-aware free-space clipping
223
+ </label>
224
+ <button id="run">Generate coverage path</button>
225
+ </section>
226
+
227
+ <section class="panel stage" aria-label="coverage map">
228
+ <canvas id="map" width="760" height="440"></canvas>
229
+ <div class="summary">
230
+ <div class="metric"><strong id="wp">0</strong><span>waypoints</span></div>
231
+ <div class="metric"><strong id="area">0.0</strong><span>estimated m²</span></div>
232
+ <div class="metric"><strong id="length">0.0</strong><span>path length m</span></div>
233
+ <div class="metric"><strong id="clearance">0.00</strong><span>center clearance m</span></div>
234
+ </div>
235
+ <div class="note" id="note"></div>
236
+ </section>
237
+ </main>
238
+
239
+ <script>
240
+ const W = 760, H = 440, RES = 0.10;
241
+ const controls = ["xMin","xMax","yMin","yMax","lane","robotW","robotL","margin"];
242
+ const canvas = document.getElementById("map");
243
+ const ctx = canvas.getContext("2d");
244
+
245
+ function val(id) { return Number(document.getElementById(id).value); }
246
+ function px(x, y) { return [Math.round(x / RES) + 50, H - (Math.round(y / RES) + 38)]; }
247
+
248
+ function buildMap() {
249
+ const grid = new Int8Array(W * H);
250
+ const setRect = (x0, y0, x1, y1, v) => {
251
+ for (let y = Math.max(0,y0); y < Math.min(H,y1); y++) {
252
+ for (let x = Math.max(0,x0); x < Math.min(W,x1); x++) grid[y * W + x] = v;
253
+ }
254
+ };
255
+ setRect(0, 0, W, 30, -1);
256
+ setRect(0, H - 28, W, H, -1);
257
+ setRect(0, 0, 34, H, -1);
258
+ setRect(W - 38, 0, W, H, -1);
259
+ setRect(540, 328, 730, 410, -1);
260
+ [[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));
261
+ setRect(64, 56, 690, 61, 1);
262
+ setRect(64, 374, 520, 379, 1);
263
+ setRect(64, 56, 69, 379, 1);
264
+ setRect(690, 56, 695, 328, 1);
265
+ return grid;
266
+ }
267
+
268
+ function freeMask(grid, clearancePx, mapAware) {
269
+ const mask = new Uint8Array(W * H);
270
+ mask.fill(1);
271
+ for (let i = 0; i < grid.length; i++) {
272
+ if (mapAware ? grid[i] !== 0 : grid[i] === 1) {
273
+ const x = i % W, y = Math.floor(i / W);
274
+ for (let yy = Math.max(0, y - clearancePx); yy <= Math.min(H - 1, y + clearancePx); yy++) {
275
+ for (let xx = Math.max(0, x - clearancePx); xx <= Math.min(W - 1, x + clearancePx); xx++) mask[yy * W + xx] = 0;
276
+ }
277
+ }
278
+ }
279
+ return mask;
280
+ }
281
+
282
+ function segments(row, minLen = 18) {
283
+ const out = [];
284
+ let start = -1;
285
+ for (let i = 0; i < row.length; i++) {
286
+ if (row[i] && start < 0) start = i;
287
+ if ((!row[i] || i === row.length - 1) && start >= 0) {
288
+ const end = row[i] && i === row.length - 1 ? i : i - 1;
289
+ if (end - start + 1 >= minLen) out.push([start, end]);
290
+ start = -1;
291
+ }
292
+ }
293
+ return out;
294
+ }
295
+
296
+ function lineFree(a, b, mask) {
297
+ const dx = b[0] - a[0], dy = b[1] - a[1];
298
+ const steps = Math.max(Math.abs(dx), Math.abs(dy), 1);
299
+ for (let i = 0; i <= steps; i++) {
300
+ const x = Math.round(a[0] + dx * i / steps);
301
+ const y = Math.round(a[1] + dy * i / steps);
302
+ if (x < 0 || x >= W || y < 0 || y >= H || !mask[y * W + x]) return false;
303
+ }
304
+ return true;
305
+ }
306
+
307
+ function connector(a, b, mask) {
308
+ if (lineFree(a, b, mask)) return [a, b];
309
+ for (const c of [[a[0], b[1]], [b[0], a[1]]]) {
310
+ if (lineFree(a, c, mask) && lineFree(c, b, mask)) return [a, c, b];
311
+ }
312
+ return null;
313
+ }
314
+
315
+ function plan() {
316
+ controls.forEach(id => document.getElementById(id + "Out").textContent = `${val(id).toFixed(id.includes("robot") || id === "margin" ? 2 : 1)} m`);
317
+ const grid = buildMap();
318
+ const clearanceM = Math.hypot(val("robotW"), val("robotL")) / 2 + val("margin");
319
+ const clearancePx = Math.max(1, Math.round(clearanceM / RES));
320
+ const mask = freeMask(grid, clearancePx, document.getElementById("mapAware").checked);
321
+ const [aX, aY] = px(Math.min(val("xMin"), val("xMax")), Math.min(val("yMin"), val("yMax")));
322
+ const [bX, bY] = px(Math.max(val("xMin"), val("xMax")), Math.max(val("yMin"), val("yMax")));
323
+ const x0 = Math.max(0, Math.min(aX, bX)), x1 = Math.min(W - 1, Math.max(aX, bX));
324
+ const y0 = Math.max(0, Math.min(aY, bY)), y1 = Math.min(H - 1, Math.max(aY, bY));
325
+ const laneStep = Math.max(8, Math.round(val("lane") / RES));
326
+ const waypoints = [], lines = [];
327
+ let previous = null, direction = 1, sampled = 0;
328
+ for (let y = y0; y <= y1; y += laneStep) {
329
+ const row = [];
330
+ for (let x = x0; x <= x1; x++) row.push(mask[y * W + x]);
331
+ for (const [s, e] of segments(row)) {
332
+ const sx = x0 + s, ex = x0 + e;
333
+ const start = direction > 0 ? [sx, y] : [ex, y];
334
+ const end = direction > 0 ? [ex, y] : [sx, y];
335
+ sampled += ex - sx + 1;
336
+ if (previous) {
337
+ const c = connector(previous, start, mask);
338
+ if (c) lines.push(c);
339
+ }
340
+ lines.push([start, end]);
341
+ waypoints.push(start, end);
342
+ previous = end;
343
+ direction *= -1;
344
+ }
345
+ }
346
+ draw(grid, mask, [x0, y0, x1, y1], lines, waypoints, clearancePx);
347
+ 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);
348
+ document.getElementById("wp").textContent = waypoints.length;
349
+ document.getElementById("area").textContent = (sampled * val("lane") * RES).toFixed(1);
350
+ document.getElementById("length").textContent = (lenPx * RES).toFixed(1);
351
+ document.getElementById("clearance").textContent = clearanceM.toFixed(2);
352
+ document.getElementById("note").textContent = "工程含义:先把选择区域裁剪到已知 free-space,再按车体 footprint 与安全边距膨胀障碍物。真实机器人执行时,覆盖路径仍应接入 Nav2、velocity smoother 与 collision monitor,而不是让规划节点直接控制底盘。";
353
+ }
354
+
355
+ function draw(grid, mask, selected, lines, waypoints, clearancePx) {
356
+ const img = ctx.createImageData(W, H);
357
+ for (let i = 0; i < grid.length; i++) {
358
+ let c = grid[i] === 0 ? [244,247,250] : grid[i] < 0 ? [199,205,214] : [31,41,55];
359
+ if (grid[i] === 0 && mask[i]) c = [236,252,242];
360
+ 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;
361
+ }
362
+ ctx.putImageData(img, 0, 0);
363
+ const [x0, y0, x1, y1] = selected;
364
+ ctx.fillStyle = "rgba(37,99,235,.12)";
365
+ ctx.strokeStyle = "rgba(37,99,235,.96)";
366
+ ctx.lineWidth = 3;
367
+ ctx.fillRect(x0, y0, x1 - x0, y1 - y0);
368
+ ctx.strokeRect(x0, y0, x1 - x0, y1 - y0);
369
+ ctx.strokeStyle = "#138a4f";
370
+ ctx.lineWidth = 4;
371
+ ctx.lineCap = "round";
372
+ lines.forEach(line => {
373
+ ctx.beginPath();
374
+ ctx.moveTo(line[0][0], line[0][1]);
375
+ line.slice(1).forEach(p => ctx.lineTo(p[0], p[1]));
376
+ ctx.stroke();
377
+ });
378
+ waypoints.forEach((p, i) => {
379
+ ctx.beginPath();
380
+ ctx.fillStyle = i === 0 ? "#166534" : i === waypoints.length - 1 ? "#dc2626" : "#15803d";
381
+ ctx.arc(p[0], p[1], i === 0 || i === waypoints.length - 1 ? 5 : 3.5, 0, Math.PI * 2);
382
+ ctx.fill();
383
+ });
384
+ ctx.fillStyle = "rgba(255,255,255,.92)";
385
+ ctx.strokeStyle = "#d8dee8";
386
+ ctx.lineWidth = 1;
387
+ roundRect(16, 16, 310, 96, 8);
388
+ ctx.fill();
389
+ ctx.stroke();
390
+ ctx.fillStyle = "#172033";
391
+ ctx.font = "14px system-ui";
392
+ ctx.fillText("RabbitRobot map-aware coverage", 30, 42);
393
+ [["safe free-space","#ecfcf2"],["unknown / off-map","#c7cdd6"],["obstacle source","#1f2937"],[`clearance radius ${clearancePx}px`,"#138a4f"]].forEach((item, i) => {
394
+ const y = 58 + i * 14;
395
+ ctx.fillStyle = item[1];
396
+ ctx.fillRect(30, y - 8, 14, 8);
397
+ ctx.fillStyle = "#5b667a";
398
+ ctx.font = "12px system-ui";
399
+ ctx.fillText(item[0], 52, y);
400
+ });
401
+ }
402
+
403
+ function roundRect(x, y, w, h, r) {
404
+ ctx.beginPath();
405
+ ctx.moveTo(x + r, y);
406
+ ctx.arcTo(x + w, y, x + w, y + h, r);
407
+ ctx.arcTo(x + w, y + h, x, y + h, r);
408
+ ctx.arcTo(x, y + h, x, y, r);
409
+ ctx.arcTo(x, y, x + w, y, r);
410
+ ctx.closePath();
411
+ }
412
+
413
+ document.getElementById("run").addEventListener("click", plan);
414
+ controls.concat(["mapAware"]).forEach(id => document.getElementById(id).addEventListener("input", plan));
415
+ plan();
416
+ </script>
417
+ </body>
418
  </html>