atropos/environments/community/padres_spatial/visualization/main.js
Shannon Sands e7e747a396 linting
2025-05-27 15:43:12 +10:00

185 lines
7.3 KiB
JavaScript

// Make sure OrbitControls is loaded from CDN or included if you use it
// If OrbitControls is loaded globally via script tag: const OrbitControls = window.OrbitControls;
// const TWEEN = window.TWEEN; // If using TWEEN CDN
// --- Global Variables ---
let scene, camera, renderer, controls;
const objectsInScene = new Map(); // Stores THREE.Mesh objects by their ID
const websocketUrl = "ws://localhost:8765";
let socket;
// --- DOM Elements ---
const statusElement = document.getElementById('status');
const taskDescriptionElement = document.getElementById('taskDescription');
const visualizationContainer = document.getElementById('visualizationContainer');
// --- Initialization ---
function init() {
// Scene
scene = new THREE.Scene();
scene.background = new THREE.Color(0xdddddd);
// Camera
camera = new THREE.PerspectiveCamera(75, visualizationContainer.clientWidth / visualizationContainer.clientHeight, 0.1, 1000);
camera.position.set(3, 4, 5); // Adjusted camera position
camera.lookAt(0, 0, 0);
// Renderer
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(visualizationContainer.clientWidth, visualizationContainer.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.shadowMap.enabled = true; // Enable shadows
visualizationContainer.appendChild(renderer.domElement);
// Lights
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(5, 10, 7);
directionalLight.castShadow = true; // Enable shadow casting for this light
// Configure shadow properties for better quality (optional)
directionalLight.shadow.mapSize.width = 1024;
directionalLight.shadow.mapSize.height = 1024;
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 50;
scene.add(directionalLight);
// Ground Plane
const planeGeometry = new THREE.PlaneGeometry(20, 20);
const planeMaterial = new THREE.MeshStandardMaterial({ color: 0xaaaaaa, roughness: 0.8 });
const groundPlane = new THREE.Mesh(planeGeometry, planeMaterial);
groundPlane.rotation.x = -Math.PI / 2;
groundPlane.receiveShadow = true; // Allow plane to receive shadows
scene.add(groundPlane);
// Controls (Optional, if OrbitControls is loaded)
if (typeof OrbitControls !== 'undefined') {
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.screenSpacePanning = false;
controls.minDistance = 2;
controls.maxDistance = 20;
controls.maxPolarAngle = Math.PI / 2 - 0.05; // Prevent camera from going below ground
}
// Handle window resize
window.addEventListener('resize', onWindowResize, false);
// Start animation loop
animate();
// Connect to WebSocket
connectWebSocket();
}
// --- WebSocket Handling ---
function connectWebSocket() {
socket = new WebSocket(websocketUrl);
statusElement.textContent = "Connecting to WebSocket...";
socket.onopen = () => {
statusElement.textContent = "Connected to Physics Server!";
console.log("WebSocket connected.");
// You could send a "client_ready" message if needed
};
socket.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
// console.log("Message from server:", message);
if (message.type === "scene_update" || message.type === "initial_scene") {
updateScene(message.payload); // payload is List[ObjectData]
if (message.type === "initial_scene" && message.task_description) {
taskDescriptionElement.textContent = message.task_description;
}
} else if (message.type === "task_info") { // Example for updating task description
taskDescriptionElement.textContent = message.description || "N/A";
}
} catch (e) {
console.error("Error processing message from server:", e, event.data);
}
};
socket.onerror = (error) => {
statusElement.textContent = "WebSocket Error!";
console.error("WebSocket Error:", error);
};
socket.onclose = () => {
statusElement.textContent = "Disconnected. Attempting to reconnect in 3s...";
console.log("WebSocket disconnected. Reconnecting in 3 seconds...");
setTimeout(connectWebSocket, 3000); // Simple reconnect logic
};
}
// --- Three.js Scene Updates ---
function updateScene(objectStates) { // objectStates is List of Dicts from server
const receivedIds = new Set();
objectStates.forEach(objState => {
receivedIds.add(objState.id);
let threeObject = objectsInScene.get(objState.id);
if (!threeObject) { // Object doesn't exist, create it
let geometry;
const scale = objState.scale || [1,1,1];
if (objState.type === "cube") {
geometry = new THREE.BoxGeometry(scale[0], scale[1], scale[2]);
} else if (objState.type === "sphere") {
geometry = new THREE.SphereGeometry(scale[0] / 2, 32, 16); // Assume scale[0] is diameter
} else {
console.warn("Unsupported object type for visualization:", objState.type);
geometry = new THREE.BoxGeometry(1, 1, 1); // Default placeholder
}
const color = new THREE.Color(...(objState.color_rgba ? objState.color_rgba.slice(0,3) : [0.5, 0.5, 0.5]));
const material = new THREE.MeshStandardMaterial({ color: color, roughness: 0.5, metalness: 0.1 });
threeObject = new THREE.Mesh(geometry, material);
threeObject.name = objState.id; // Useful for debugging
threeObject.castShadow = true; // Object casts shadows
threeObject.receiveShadow = false; // Usually objects don't receive shadows on themselves unless complex
scene.add(threeObject);
objectsInScene.set(objState.id, threeObject);
}
// Update position and orientation
if (objState.position) {
threeObject.position.set(...objState.position);
}
if (objState.orientation_quaternion) {
threeObject.quaternion.set(...objState.orientation_quaternion);
}
// TODO: Update color or other properties if they can change dynamically
});
// Remove objects that are in Three.js scene but not in the new state
objectsInScene.forEach((obj, id) => {
if (!receivedIds.has(id)) {
scene.remove(obj);
obj.geometry.dispose(); // Dispose geometry
obj.material.dispose(); // Dispose material
objectsInScene.delete(id);
console.log(`Removed object ${id} from scene.`);
}
});
}
// --- Animation Loop & Resize ---
function animate() {
requestAnimationFrame(animate);
if (controls) {
controls.update(); // Only if OrbitControls is used
}
renderer.render(scene, camera);
}
function onWindowResize() {
camera.aspect = visualizationContainer.clientWidth / visualizationContainer.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(visualizationContainer.clientWidth / visualizationContainer.clientHeight);
}
// --- Start Everything ---
init();