Skip to content

Commit 84d7e7e

Browse files
authored
Merge pull request #2 from alphaonelabs/copilot/add-readme-contribute-button
Add README, Contribute button, and LiDAR shape/color detection panel
2 parents 1447b25 + 8f089ca commit 84d7e7e

3 files changed

Lines changed: 232 additions & 39 deletions

File tree

README.md

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ A browser-based virtual robotics simulation environment for building, configurin
88

99
- **Modular Robot Builder** — Start from a blank chassis and attach sensors, wheels, and a manipulator arm.
1010
- **Omni-Directional Movement** — Drive your robot with WASD or Arrow keys; adjust motor power and turning speed in real-time.
11-
- **LiDAR Scanning** — A rotating 360° LiDAR sensor casts a detection beam to detect nearby objects.
11+
- **LiDAR Scanning** — A rotating 360° LiDAR sensor casts a detection beam. When it passes over an object, the **Detection Panel** reports the object's **shape** and **color**.
1212
- **First-Person Camera** — Live FPV camera feed rendered from the robot's perspective directly in the browser.
1313
- **Robotic Arm** — Pick up and drop coloured blocks using the SPACE key.
1414
- **Telemetry HUD** — Real-time position, heading, and speed readout overlaid on the simulation canvas.
@@ -23,22 +23,9 @@ No build tools or server required — just open the files directly in your brows
2323
```bash
2424
git clone https://github.com/alphaonelabs/alphaonelabs-virtual-robotics-playground.git
2525
```
26-
2726
2. Open `index.html` in any modern web browser to view the landing page.
28-
2927
3. Click **Enter System** to launch the interactive playground (`home.html`).
3028

31-
**Optional: Run with local server**
32-
```bash
33-
# Using Python
34-
python3 -m http.server 8000
35-
36-
# Using Node.js
37-
npx http-server -p 8000
38-
39-
# Visit: http://localhost:8000
40-
```
41-
4229
---
4330

4431
## 🎮 Controls
@@ -61,12 +48,21 @@ Add or remove components from the **Toolbox** panel on the left:
6148
|---|---|
6249
| **Chassis** | Core robot body — must be added before any other part |
6350
| **Wheels (WASD)** | Enables keyboard-driven movement |
64-
| **LiDAR** | Rotating laser scanner with visual sweep animation |
51+
| **LiDAR** | Rotating laser scanner; detects nearby objects and reports shape & color |
6552
| **Camera (View)** | First-person camera feed shown in the top-left overlay |
6653
| **Arm (SPACE)** | Allows picking up and dropping blocks with the SPACE key |
6754

6855
---
6956

57+
## 📡 LiDAR Detection
58+
59+
When the LiDAR sensor is active, its beam sweeps 360° around the robot. When the beam intersects an object within range, the **Detection Panel** (bottom of the right sidebar) instantly displays:
60+
61+
- The **shape** of the detected object — `Square`, `Circle`, or `Triangle`
62+
- The **color** of the detected object — e.g. `Red`, `Orange`, `Green`, `Blue`
63+
64+
---
65+
7066
## ⚙️ Settings (Right Sidebar)
7167

7268
| Setting | Description |
@@ -149,4 +145,4 @@ This project is licensed under the [MIT License](LICENSE).
149145

150146
---
151147

152-
*Built by [Alpha One Labs](https://github.com/alphaonelabs) — Advancing open science through education and robotics.*
148+
*Built by [Alpha One Labs](https://github.com/alphaonelabs) — Advancing open science through education and robotics.*

home.html

Lines changed: 190 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@
8787
class="mt-3 w-full bg-gray-700 hover:bg-gray-600 p-2 rounded text-gray-300 text-xs flex items-center justify-center gap-2 transition">
8888
<i class="fas fa-redo"></i> Reset All
8989
</button>
90+
<a href="https://github.com/alphaonelabs/alphaonelabs-virtual-robotics-playground"
91+
target="_blank"
92+
rel="noopener noreferrer"
93+
class="mt-2 w-full bg-indigo-700 hover:bg-indigo-600 p-2 rounded text-white text-xs flex items-center justify-center gap-2 transition font-medium">
94+
<i class="fas fa-code-branch"></i> Contribute
95+
</a>
9096
</div>
9197
<div class="flex-1 relative bg-gray-950" id="viewport-wrapper">
9298
<canvas id="sim-canvas"
@@ -176,6 +182,16 @@
176182
Blocks: <span id="block-count">0</span>
177183
</div>
178184
</div>
185+
<!-- LiDAR Detection Panel -->
186+
<div class="mt-3 pt-3 border-t border-gray-700">
187+
<div class="text-gray-400 text-xs mb-1 flex items-center gap-1.5">
188+
<span class="w-1.5 h-1.5 rounded-full bg-red-500 animate-pulse hidden" id="detection-indicator"></span>
189+
📡 Detection:
190+
</div>
191+
<div id="detection-info" class="text-[10px] space-y-0.5">
192+
<div class="text-gray-500">LiDAR not installed</div>
193+
</div>
194+
</div>
179195
</div>
180196
</div>
181197
</div>
@@ -230,13 +246,15 @@
230246

231247
function generateBlocks() {
232248
blocks = [];
249+
const shapes = ['square', 'circle', 'triangle'];
233250
for (let i = 0; i < 12; i++) {
234251
blocks.push({
235252
id: i,
236253
x: 80 + Math.random() * (canvas.width - 160),
237254
y: 80 + Math.random() * (canvas.height - 160),
238255
size: 25,
239256
color: `hsl(${i * 30}, 70%, 55%)`,
257+
shape: shapes[Math.floor(Math.random() * shapes.length)],
240258
held: false,
241259
});
242260
}
@@ -331,6 +349,10 @@
331349
if (type === 'camera') {
332350
document.getElementById('camera-feed').classList.remove('hidden');
333351
}
352+
if (type === 'lidar') {
353+
lastDetectedId = null;
354+
updateDetectionUI(null);
355+
}
334356
updatePartsList();
335357
showMsg(`${type.charAt(0).toUpperCase() + type.slice(1)} added!`);
336358
}
@@ -360,6 +382,13 @@
360382
robot.holdingBlock = null;
361383
}
362384

385+
// Handle lidar removal: clear detection
386+
if (type === 'lidar') {
387+
lastDetectedId = null;
388+
document.getElementById('detection-info').innerHTML = '<div class="text-gray-500">LiDAR not installed</div>';
389+
document.getElementById('detection-indicator').classList.add('hidden');
390+
}
391+
363392
// Handle wheels removal: stop the robot
364393
if (type === 'wheels') {
365394
robot.speed = 0;
@@ -389,6 +418,9 @@
389418
document.getElementById('start-prompt').classList.remove('hidden');
390419
document.getElementById('camera-feed').classList.add('hidden');
391420
document.getElementById('block-count').textContent = '0';
421+
lastDetectedId = null;
422+
document.getElementById('detection-info').innerHTML = '<div class="text-gray-500">LiDAR not installed</div>';
423+
document.getElementById('detection-indicator').classList.add('hidden');
392424
updatePartsList();
393425
}
394426

@@ -442,6 +474,16 @@
442474
document.getElementById('pos-display').textContent = `${Math.round(robot.x)}, ${Math.round(robot.y)}`;
443475
document.getElementById('angle-display').textContent = `${Math.round((robot.angle * 180) / Math.PI)}°`;
444476
document.getElementById('speed-display').textContent = robot.speed.toFixed(1);
477+
478+
// LiDAR object detection
479+
if (robot.parts.lidar) {
480+
const detected = getLidarDetection();
481+
const newId = detected ? detected.id : null;
482+
if (newId !== lastDetectedId) {
483+
lastDetectedId = newId;
484+
updateDetectionUI(detected);
485+
}
486+
}
445487
}
446488

447489
function drawGrid() {
@@ -465,10 +507,26 @@
465507
blocks.forEach((b) => {
466508
if (b.held) return; // draw held block with robot
467509
ctx.fillStyle = b.color;
468-
ctx.fillRect(b.x, b.y, b.size, b.size);
469510
ctx.strokeStyle = '#fff';
470511
ctx.lineWidth = 2;
471-
ctx.strokeRect(b.x, b.y, b.size, b.size);
512+
513+
if (b.shape === 'circle') {
514+
ctx.beginPath();
515+
ctx.arc(b.x + b.size / 2, b.y + b.size / 2, b.size / 2, 0, Math.PI * 2);
516+
ctx.fill();
517+
ctx.stroke();
518+
} else if (b.shape === 'triangle') {
519+
ctx.beginPath();
520+
ctx.moveTo(b.x + b.size / 2, b.y);
521+
ctx.lineTo(b.x, b.y + b.size);
522+
ctx.lineTo(b.x + b.size, b.y + b.size);
523+
ctx.closePath();
524+
ctx.fill();
525+
ctx.stroke();
526+
} else {
527+
ctx.fillRect(b.x, b.y, b.size, b.size);
528+
ctx.strokeRect(b.x, b.y, b.size, b.size);
529+
}
472530

473531
// Glow if near gripper and arm attached
474532
if (robot.parts.arm && robot.holdingBlock === null) {
@@ -477,7 +535,20 @@
477535
if (Math.hypot(gx - (b.x + b.size / 2), gy - (b.y + b.size / 2)) < 45) {
478536
ctx.shadowColor = '#fff';
479537
ctx.shadowBlur = 12;
480-
ctx.strokeRect(b.x, b.y, b.size, b.size);
538+
if (b.shape === 'circle') {
539+
ctx.beginPath();
540+
ctx.arc(b.x + b.size / 2, b.y + b.size / 2, b.size / 2, 0, Math.PI * 2);
541+
ctx.stroke();
542+
} else if (b.shape === 'triangle') {
543+
ctx.beginPath();
544+
ctx.moveTo(b.x + b.size / 2, b.y);
545+
ctx.lineTo(b.x, b.y + b.size);
546+
ctx.lineTo(b.x + b.size, b.y + b.size);
547+
ctx.closePath();
548+
ctx.stroke();
549+
} else {
550+
ctx.strokeRect(b.x, b.y, b.size, b.size);
551+
}
481552
ctx.shadowBlur = 0;
482553
}
483554
}
@@ -527,7 +598,7 @@
527598
const sw = (((Date.now() / 15) % 360) * Math.PI) / 180;
528599
ctx.beginPath();
529600
ctx.moveTo(0, -32);
530-
ctx.lineTo(Math.cos(sw - Math.PI / 2) * 70, -32 + Math.sin(sw - Math.PI / 2) * 70);
601+
ctx.lineTo(Math.cos(sw - Math.PI / 2) * 100, -32 + Math.sin(sw - Math.PI / 2) * 100);
531602
ctx.stroke();
532603
}
533604

@@ -567,10 +638,25 @@
567638
const b = blocks.find((bl) => bl.id === robot.holdingBlock);
568639
if (b) {
569640
ctx.fillStyle = b.color;
570-
ctx.fillRect(-b.size / 2, 55, b.size, b.size);
571641
ctx.strokeStyle = '#fff';
572642
ctx.lineWidth = 2;
573-
ctx.strokeRect(-b.size / 2, 55, b.size, b.size);
643+
if (b.shape === 'circle') {
644+
ctx.beginPath();
645+
ctx.arc(0, 55 + b.size / 2, b.size / 2, 0, Math.PI * 2);
646+
ctx.fill();
647+
ctx.stroke();
648+
} else if (b.shape === 'triangle') {
649+
ctx.beginPath();
650+
ctx.moveTo(0, 55);
651+
ctx.lineTo(-b.size / 2, 55 + b.size);
652+
ctx.lineTo(b.size / 2, 55 + b.size);
653+
ctx.closePath();
654+
ctx.fill();
655+
ctx.stroke();
656+
} else {
657+
ctx.fillRect(-b.size / 2, 55, b.size, b.size);
658+
ctx.strokeRect(-b.size / 2, 55, b.size, b.size);
659+
}
574660
}
575661
}
576662
}
@@ -647,7 +733,105 @@
647733
render();
648734
requestAnimationFrame(loop);
649735
}
736+
737+
// ========== LIDAR DETECTION ==========
738+
let lastDetectedId = null;
739+
650740
loop();
741+
742+
function segmentsIntersect(x1, y1, x2, y2, x3, y3, x4, y4) {
743+
const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
744+
if (denom === 0) return false;
745+
const t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom;
746+
const u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denom;
747+
return t >= 0 && t <= 1 && u >= 0 && u <= 1;
748+
}
749+
750+
function segmentIntersectsRect(x1, y1, x2, y2, rx, ry, rw, rh) {
751+
if (x2 >= rx && x2 <= rx + rw && y2 >= ry && y2 <= ry + rh) return true;
752+
return (
753+
segmentsIntersect(x1, y1, x2, y2, rx, ry, rx + rw, ry) ||
754+
segmentsIntersect(x1, y1, x2, y2, rx + rw, ry, rx + rw, ry + rh) ||
755+
segmentsIntersect(x1, y1, x2, y2, rx, ry + rh, rx + rw, ry + rh) ||
756+
segmentsIntersect(x1, y1, x2, y2, rx, ry, rx, ry + rh)
757+
);
758+
}
759+
760+
function pointToSegmentDist(px, py, x1, y1, x2, y2) {
761+
const A = px - x1, B = py - y1, C = x2 - x1, D = y2 - y1;
762+
const lenSq = C * C + D * D;
763+
const param = lenSq !== 0 ? Math.max(0, Math.min(1, (A * C + B * D) / lenSq)) : 0;
764+
return Math.hypot(px - (x1 + param * C), py - (y1 + param * D));
765+
}
766+
767+
function beamIntersectsBlock(x1, y1, x2, y2, b) {
768+
const cx = b.x + b.size / 2, cy = b.y + b.size / 2;
769+
if (b.shape === 'circle') {
770+
return pointToSegmentDist(cx, cy, x1, y1, x2, y2) < b.size / 2;
771+
} else if (b.shape === 'triangle') {
772+
const tx1 = b.x + b.size / 2, ty1 = b.y;
773+
const tx2 = b.x, ty2 = b.y + b.size;
774+
const tx3 = b.x + b.size, ty3 = b.y + b.size;
775+
// Check all three edges, or if beam endpoint is inside triangle
776+
return (
777+
segmentsIntersect(x1, y1, x2, y2, tx1, ty1, tx2, ty2) ||
778+
segmentsIntersect(x1, y1, x2, y2, tx2, ty2, tx3, ty3) ||
779+
segmentsIntersect(x1, y1, x2, y2, tx3, ty3, tx1, ty1)
780+
);
781+
} else {
782+
return segmentIntersectsRect(x1, y1, x2, y2, b.x, b.y, b.size, b.size);
783+
}
784+
}
785+
786+
function getLidarDetection() {
787+
if (!robot.parts.lidar || blocks.length === 0) return null;
788+
const sw = (((Date.now() / 15) % 360) * Math.PI) / 180;
789+
// Lidar center in world coordinates (local offset: 0, -32)
790+
const lidarX = robot.x + 32 * Math.sin(robot.angle);
791+
const lidarY = robot.y - 32 * Math.cos(robot.angle);
792+
// World beam direction angle
793+
const worldBeamAngle = sw - Math.PI / 2 + robot.angle;
794+
const beamRange = 100;
795+
const beamEndX = lidarX + Math.cos(worldBeamAngle) * beamRange;
796+
const beamEndY = lidarY + Math.sin(worldBeamAngle) * beamRange;
797+
for (const b of blocks) {
798+
if (b.held) continue;
799+
if (beamIntersectsBlock(lidarX, lidarY, beamEndX, beamEndY, b)) return b;
800+
}
801+
return null;
802+
}
803+
804+
function getColorName(hslColor) {
805+
const match = hslColor.match(/hsl\((\d+)/);
806+
if (!match) return hslColor;
807+
const hue = parseInt(match[1]);
808+
const names = [
809+
[15, 'Red'], [45, 'Orange'], [75, 'Yellow'], [105, 'Yellow-Green'],
810+
[135, 'Green'], [165, 'Teal'], [195, 'Cyan'], [225, 'Sky Blue'],
811+
[255, 'Blue'], [285, 'Purple'], [315, 'Magenta'], [345, 'Pink'],
812+
];
813+
for (const [max, name] of names) {
814+
if (hue < max) return name;
815+
}
816+
return 'Red';
817+
}
818+
819+
function updateDetectionUI(detected) {
820+
const el = document.getElementById('detection-info');
821+
const indicator = document.getElementById('detection-indicator');
822+
if (detected) {
823+
const colorName = getColorName(detected.color);
824+
const shapeName = detected.shape.charAt(0).toUpperCase() + detected.shape.slice(1);
825+
el.innerHTML =
826+
'<div class="text-green-400 font-semibold">● OBJECT DETECTED</div>' +
827+
'<div class="text-gray-300 mt-0.5">Shape: <span class="text-yellow-300">' + shapeName + '</span></div>' +
828+
'<div class="text-gray-300">Color: <span style="color:' + detected.color + ';font-weight:600">' + colorName + '</span></div>';
829+
indicator.classList.remove('hidden');
830+
} else {
831+
el.innerHTML = '<div class="text-gray-500">No objects detected</div>';
832+
indicator.classList.add('hidden');
833+
}
834+
}
651835
</script>
652836
</body>
653837
</html>

0 commit comments

Comments
 (0)