diff --git a/README.md b/README.md index 029a8a1..a778e73 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A browser-based virtual robotics simulation environment for building, configurin - **Modular Robot Builder** — Start from a blank chassis and attach sensors, wheels, and a manipulator arm. - **Omni-Directional Movement** — Drive your robot with WASD or Arrow keys; adjust motor power and turning speed in real-time. -- **LiDAR Scanning** — A rotating 360° LiDAR sensor casts a detection beam to detect nearby objects. +- **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**. - **First-Person Camera** — Live FPV camera feed rendered from the robot's perspective directly in the browser. - **Robotic Arm** — Pick up and drop coloured blocks using the SPACE key. - **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 ```bash git clone https://github.com/alphaonelabs/alphaonelabs-virtual-robotics-playground.git ``` - 2. Open `index.html` in any modern web browser to view the landing page. - 3. Click **Enter System** to launch the interactive playground (`home.html`). -**Optional: Run with local server** -```bash -# Using Python -python3 -m http.server 8000 - -# Using Node.js -npx http-server -p 8000 - -# Visit: http://localhost:8000 -``` - --- ## 🎮 Controls @@ -61,12 +48,21 @@ Add or remove components from the **Toolbox** panel on the left: |---|---| | **Chassis** | Core robot body — must be added before any other part | | **Wheels (WASD)** | Enables keyboard-driven movement | -| **LiDAR** | Rotating laser scanner with visual sweep animation | +| **LiDAR** | Rotating laser scanner; detects nearby objects and reports shape & color | | **Camera (View)** | First-person camera feed shown in the top-left overlay | | **Arm (SPACE)** | Allows picking up and dropping blocks with the SPACE key | --- +## 📡 LiDAR Detection + +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: + +- The **shape** of the detected object — `Square`, `Circle`, or `Triangle` +- The **color** of the detected object — e.g. `Red`, `Orange`, `Green`, `Blue` + +--- + ## ⚙️ Settings (Right Sidebar) | Setting | Description | @@ -149,4 +145,4 @@ This project is licensed under the [MIT License](LICENSE). --- -*Built by [Alpha One Labs](https://github.com/alphaonelabs) — Advancing open science through education and robotics.* \ No newline at end of file +*Built by [Alpha One Labs](https://github.com/alphaonelabs) — Advancing open science through education and robotics.* diff --git a/home.html b/home.html index 1ee8960..18ca614 100644 --- a/home.html +++ b/home.html @@ -87,6 +87,12 @@ 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"> Reset All + + Contribute +
0
+ +
+
+ + 📡 Detection: +
+
+
LiDAR not installed
+
+
@@ -230,6 +246,7 @@ function generateBlocks() { blocks = []; + const shapes = ['square', 'circle', 'triangle']; for (let i = 0; i < 12; i++) { blocks.push({ id: i, @@ -237,6 +254,7 @@ y: 80 + Math.random() * (canvas.height - 160), size: 25, color: `hsl(${i * 30}, 70%, 55%)`, + shape: shapes[Math.floor(Math.random() * shapes.length)], held: false, }); } @@ -331,6 +349,10 @@ if (type === 'camera') { document.getElementById('camera-feed').classList.remove('hidden'); } + if (type === 'lidar') { + lastDetectedId = null; + updateDetectionUI(null); + } updatePartsList(); showMsg(`${type.charAt(0).toUpperCase() + type.slice(1)} added!`); } @@ -360,6 +382,13 @@ robot.holdingBlock = null; } + // Handle lidar removal: clear detection + if (type === 'lidar') { + lastDetectedId = null; + document.getElementById('detection-info').innerHTML = '
LiDAR not installed
'; + document.getElementById('detection-indicator').classList.add('hidden'); + } + // Handle wheels removal: stop the robot if (type === 'wheels') { robot.speed = 0; @@ -389,6 +418,9 @@ document.getElementById('start-prompt').classList.remove('hidden'); document.getElementById('camera-feed').classList.add('hidden'); document.getElementById('block-count').textContent = '0'; + lastDetectedId = null; + document.getElementById('detection-info').innerHTML = '
LiDAR not installed
'; + document.getElementById('detection-indicator').classList.add('hidden'); updatePartsList(); } @@ -442,6 +474,16 @@ document.getElementById('pos-display').textContent = `${Math.round(robot.x)}, ${Math.round(robot.y)}`; document.getElementById('angle-display').textContent = `${Math.round((robot.angle * 180) / Math.PI)}°`; document.getElementById('speed-display').textContent = robot.speed.toFixed(1); + + // LiDAR object detection + if (robot.parts.lidar) { + const detected = getLidarDetection(); + const newId = detected ? detected.id : null; + if (newId !== lastDetectedId) { + lastDetectedId = newId; + updateDetectionUI(detected); + } + } } function drawGrid() { @@ -465,10 +507,26 @@ blocks.forEach((b) => { if (b.held) return; // draw held block with robot ctx.fillStyle = b.color; - ctx.fillRect(b.x, b.y, b.size, b.size); ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; - ctx.strokeRect(b.x, b.y, b.size, b.size); + + if (b.shape === 'circle') { + ctx.beginPath(); + ctx.arc(b.x + b.size / 2, b.y + b.size / 2, b.size / 2, 0, Math.PI * 2); + ctx.fill(); + ctx.stroke(); + } else if (b.shape === 'triangle') { + ctx.beginPath(); + ctx.moveTo(b.x + b.size / 2, b.y); + ctx.lineTo(b.x, b.y + b.size); + ctx.lineTo(b.x + b.size, b.y + b.size); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + } else { + ctx.fillRect(b.x, b.y, b.size, b.size); + ctx.strokeRect(b.x, b.y, b.size, b.size); + } // Glow if near gripper and arm attached if (robot.parts.arm && robot.holdingBlock === null) { @@ -477,7 +535,20 @@ if (Math.hypot(gx - (b.x + b.size / 2), gy - (b.y + b.size / 2)) < 45) { ctx.shadowColor = '#fff'; ctx.shadowBlur = 12; - ctx.strokeRect(b.x, b.y, b.size, b.size); + if (b.shape === 'circle') { + ctx.beginPath(); + ctx.arc(b.x + b.size / 2, b.y + b.size / 2, b.size / 2, 0, Math.PI * 2); + ctx.stroke(); + } else if (b.shape === 'triangle') { + ctx.beginPath(); + ctx.moveTo(b.x + b.size / 2, b.y); + ctx.lineTo(b.x, b.y + b.size); + ctx.lineTo(b.x + b.size, b.y + b.size); + ctx.closePath(); + ctx.stroke(); + } else { + ctx.strokeRect(b.x, b.y, b.size, b.size); + } ctx.shadowBlur = 0; } } @@ -527,7 +598,7 @@ const sw = (((Date.now() / 15) % 360) * Math.PI) / 180; ctx.beginPath(); ctx.moveTo(0, -32); - ctx.lineTo(Math.cos(sw - Math.PI / 2) * 70, -32 + Math.sin(sw - Math.PI / 2) * 70); + ctx.lineTo(Math.cos(sw - Math.PI / 2) * 100, -32 + Math.sin(sw - Math.PI / 2) * 100); ctx.stroke(); } @@ -567,10 +638,25 @@ const b = blocks.find((bl) => bl.id === robot.holdingBlock); if (b) { ctx.fillStyle = b.color; - ctx.fillRect(-b.size / 2, 55, b.size, b.size); ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; - ctx.strokeRect(-b.size / 2, 55, b.size, b.size); + if (b.shape === 'circle') { + ctx.beginPath(); + ctx.arc(0, 55 + b.size / 2, b.size / 2, 0, Math.PI * 2); + ctx.fill(); + ctx.stroke(); + } else if (b.shape === 'triangle') { + ctx.beginPath(); + ctx.moveTo(0, 55); + ctx.lineTo(-b.size / 2, 55 + b.size); + ctx.lineTo(b.size / 2, 55 + b.size); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + } else { + ctx.fillRect(-b.size / 2, 55, b.size, b.size); + ctx.strokeRect(-b.size / 2, 55, b.size, b.size); + } } } } @@ -647,7 +733,105 @@ render(); requestAnimationFrame(loop); } + + // ========== LIDAR DETECTION ========== + let lastDetectedId = null; + loop(); + + function segmentsIntersect(x1, y1, x2, y2, x3, y3, x4, y4) { + const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); + if (denom === 0) return false; + const t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom; + const u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denom; + return t >= 0 && t <= 1 && u >= 0 && u <= 1; + } + + function segmentIntersectsRect(x1, y1, x2, y2, rx, ry, rw, rh) { + if (x2 >= rx && x2 <= rx + rw && y2 >= ry && y2 <= ry + rh) return true; + return ( + segmentsIntersect(x1, y1, x2, y2, rx, ry, rx + rw, ry) || + segmentsIntersect(x1, y1, x2, y2, rx + rw, ry, rx + rw, ry + rh) || + segmentsIntersect(x1, y1, x2, y2, rx, ry + rh, rx + rw, ry + rh) || + segmentsIntersect(x1, y1, x2, y2, rx, ry, rx, ry + rh) + ); + } + + function pointToSegmentDist(px, py, x1, y1, x2, y2) { + const A = px - x1, B = py - y1, C = x2 - x1, D = y2 - y1; + const lenSq = C * C + D * D; + const param = lenSq !== 0 ? Math.max(0, Math.min(1, (A * C + B * D) / lenSq)) : 0; + return Math.hypot(px - (x1 + param * C), py - (y1 + param * D)); + } + + function beamIntersectsBlock(x1, y1, x2, y2, b) { + const cx = b.x + b.size / 2, cy = b.y + b.size / 2; + if (b.shape === 'circle') { + return pointToSegmentDist(cx, cy, x1, y1, x2, y2) < b.size / 2; + } else if (b.shape === 'triangle') { + const tx1 = b.x + b.size / 2, ty1 = b.y; + const tx2 = b.x, ty2 = b.y + b.size; + const tx3 = b.x + b.size, ty3 = b.y + b.size; + // Check all three edges, or if beam endpoint is inside triangle + return ( + segmentsIntersect(x1, y1, x2, y2, tx1, ty1, tx2, ty2) || + segmentsIntersect(x1, y1, x2, y2, tx2, ty2, tx3, ty3) || + segmentsIntersect(x1, y1, x2, y2, tx3, ty3, tx1, ty1) + ); + } else { + return segmentIntersectsRect(x1, y1, x2, y2, b.x, b.y, b.size, b.size); + } + } + + function getLidarDetection() { + if (!robot.parts.lidar || blocks.length === 0) return null; + const sw = (((Date.now() / 15) % 360) * Math.PI) / 180; + // Lidar center in world coordinates (local offset: 0, -32) + const lidarX = robot.x + 32 * Math.sin(robot.angle); + const lidarY = robot.y - 32 * Math.cos(robot.angle); + // World beam direction angle + const worldBeamAngle = sw - Math.PI / 2 + robot.angle; + const beamRange = 100; + const beamEndX = lidarX + Math.cos(worldBeamAngle) * beamRange; + const beamEndY = lidarY + Math.sin(worldBeamAngle) * beamRange; + for (const b of blocks) { + if (b.held) continue; + if (beamIntersectsBlock(lidarX, lidarY, beamEndX, beamEndY, b)) return b; + } + return null; + } + + function getColorName(hslColor) { + const match = hslColor.match(/hsl\((\d+)/); + if (!match) return hslColor; + const hue = parseInt(match[1]); + const names = [ + [15, 'Red'], [45, 'Orange'], [75, 'Yellow'], [105, 'Yellow-Green'], + [135, 'Green'], [165, 'Teal'], [195, 'Cyan'], [225, 'Sky Blue'], + [255, 'Blue'], [285, 'Purple'], [315, 'Magenta'], [345, 'Pink'], + ]; + for (const [max, name] of names) { + if (hue < max) return name; + } + return 'Red'; + } + + function updateDetectionUI(detected) { + const el = document.getElementById('detection-info'); + const indicator = document.getElementById('detection-indicator'); + if (detected) { + const colorName = getColorName(detected.color); + const shapeName = detected.shape.charAt(0).toUpperCase() + detected.shape.slice(1); + el.innerHTML = + '
● OBJECT DETECTED
' + + '
Shape: ' + shapeName + '
' + + '
Color: ' + colorName + '
'; + indicator.classList.remove('hidden'); + } else { + el.innerHTML = '
No objects detected
'; + indicator.classList.add('hidden'); + } + } diff --git a/index.html b/index.html index 7029729..fb9d0c0 100644 --- a/index.html +++ b/index.html @@ -552,23 +552,35 @@

{ - e.preventDefault(); - const overlay = document.getElementById('page-transition'); - overlay.classList.add('active'); - setTimeout(() => { window.location.href = 'home.html'; }, 1200); - }}> - - - - - - - Enter System - - - - +
+ { + e.preventDefault(); + const overlay = document.getElementById('page-transition'); + overlay.classList.add('active'); + setTimeout(() => { window.location.href = 'home.html'; }, 1200); + }}> + + + + + + + Enter System + + + + + + + + + + Contribute + + + + +
SCROLL TO INITIATE SEQUENCE @@ -760,6 +772,7 @@

Terms & Conditions Privacy Policy Cookie Policy + Contribute