feat: SaltyBot 3D robot model in web UI (#37) #41

Merged
seb merged 1 commits from sl-firmware/robot-3d-model into main 2026-02-28 21:57:56 -05:00
Showing only changes of commit 91fc54c3d7 - Show all commits

View File

@ -86,8 +86,8 @@ import * as THREE from 'three';
// --- Three.js scene ---
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 2, 5);
camera.lookAt(0, 0, 0);
camera.position.set(0, 1.2, 5);
camera.lookAt(0, 0.8, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
@ -107,70 +107,67 @@ scene.add(pointLight);
// Grid
scene.add(new THREE.GridHelper(10, 20, 0x222244, 0x111122));
// FC Board model
// SaltyBot robot model — two-wheeled self-balancing robot
// Pivot at wheel axle (y=0); body rises upward; pitches ±pitch_deg around X axis.
const boardGroup = new THREE.Group();
// PCB
// Main body (vertical rectangle, ~1.2 tall, center above wheel axle)
boardGroup.add(new THREE.Mesh(
new THREE.BoxGeometry(1.6, 0.08, 1.6),
new THREE.MeshPhongMaterial({ color: 0x1a1a1a, specular: 0x333333 })
new THREE.BoxGeometry(0.7, 1.2, 0.3),
new THREE.MeshPhongMaterial({ color: 0x1a2a4a, specular: 0x334466 })
));
boardGroup.children[boardGroup.children.length - 1].position.set(0, 0.7, 0);
// Copper traces
const traceMat = new THREE.MeshPhongMaterial({ color: 0xcc8833, specular: 0xffaa44 });
for (let i = -0.6; i <= 0.6; i += 0.3) {
const trace = new THREE.Mesh(new THREE.BoxGeometry(1.4, 0.005, 0.02), traceMat);
trace.position.set(0, 0.043, i);
boardGroup.add(trace);
}
// Left wheel
const wheelGeo = new THREE.CylinderGeometry(0.4, 0.4, 0.14, 20);
const wheelMat = new THREE.MeshPhongMaterial({ color: 0x1a1a1a, specular: 0x333333 });
const leftWheel = new THREE.Mesh(wheelGeo, wheelMat);
leftWheel.rotation.z = Math.PI / 2;
leftWheel.position.set(-0.47, 0, 0);
boardGroup.add(leftWheel);
// MCU chip
const mcu = new THREE.Mesh(
new THREE.BoxGeometry(0.5, 0.06, 0.5),
new THREE.MeshPhongMaterial({ color: 0x222222, specular: 0x444444 })
);
mcu.position.set(0, 0.07, 0);
boardGroup.add(mcu);
// Right wheel
const rightWheel = new THREE.Mesh(wheelGeo, wheelMat);
rightWheel.rotation.z = Math.PI / 2;
rightWheel.position.set(0.47, 0, 0);
boardGroup.add(rightWheel);
// IMU chip
const imuChip = new THREE.Mesh(
new THREE.BoxGeometry(0.2, 0.04, 0.2),
new THREE.MeshPhongMaterial({ color: 0x331111, specular: 0x442222 })
);
imuChip.position.set(-0.4, 0.06, -0.3);
boardGroup.add(imuChip);
// Wheel rims (blue accent)
const rimGeo = new THREE.TorusGeometry(0.37, 0.025, 8, 20);
const rimMat = new THREE.MeshPhongMaterial({ color: 0x0055cc, specular: 0x0088ff });
[[-0.54], [0.54]].forEach(([x]) => {
const rim = new THREE.Mesh(rimGeo, rimMat);
rim.rotation.z = Math.PI / 2;
rim.position.set(x, 0, 0);
boardGroup.add(rim);
});
// USB connector
const usb = new THREE.Mesh(
new THREE.BoxGeometry(0.35, 0.1, 0.15),
new THREE.MeshPhongMaterial({ color: 0x888888, specular: 0xaaaaaa })
);
usb.position.set(0, 0.06, -0.85);
boardGroup.add(usb);
// Display panel on front face
boardGroup.add(new THREE.Mesh(
new THREE.BoxGeometry(0.48, 0.36, 0.02),
new THREE.MeshPhongMaterial({ color: 0x001133, specular: 0x003366, emissive: 0x000a1a })
));
boardGroup.children[boardGroup.children.length - 1].position.set(0, 0.75, 0.16);
// Status LED
const ledMat = new THREE.MeshBasicMaterial({ color: 0xff0000 });
const led = new THREE.Mesh(new THREE.SphereGeometry(0.03, 8, 8), ledMat);
led.position.set(0.5, 0.06, 0.5);
const led = new THREE.Mesh(new THREE.SphereGeometry(0.04, 8, 8), ledMat);
led.position.set(0.22, 1.1, 0.16);
boardGroup.add(led);
// Mounting holes
const holeMat = new THREE.MeshPhongMaterial({ color: 0x444444 });
[[-0.55, -0.55], [-0.55, 0.55], [0.55, -0.55], [0.55, 0.55]].forEach(([x, z]) => {
const ring = new THREE.Mesh(new THREE.TorusGeometry(0.06, 0.015, 8, 16), holeMat);
ring.rotation.x = Math.PI / 2;
ring.position.set(x, 0.05, z);
boardGroup.add(ring);
});
// Sensor stem (thin post above body)
boardGroup.add(new THREE.Mesh(
new THREE.BoxGeometry(0.1, 0.28, 0.1),
new THREE.MeshPhongMaterial({ color: 0x2a2a2a, specular: 0x444444 })
));
boardGroup.children[boardGroup.children.length - 1].position.set(0, 1.44, 0);
// Forward arrow
const arrow = new THREE.Mesh(
new THREE.ConeGeometry(0.08, 0.2, 8),
new THREE.MeshBasicMaterial({ color: 0xff4444, transparent: true, opacity: 0.8 })
);
arrow.rotation.x = -Math.PI / 2;
arrow.position.set(0, 0.06, -0.65);
boardGroup.add(arrow);
// Sensor head (camera/LIDAR box at top)
boardGroup.add(new THREE.Mesh(
new THREE.BoxGeometry(0.28, 0.16, 0.28),
new THREE.MeshPhongMaterial({ color: 0x111122, specular: 0x333355 })
));
boardGroup.children[boardGroup.children.length - 1].position.set(0, 1.66, 0);
scene.add(boardGroup);