feat: SaltyBot 3D robot model in web UI (#37) #41
101
ui/index.html
101
ui/index.html
@ -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);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user