/** * Train Renderer * Handles Three.js visualization of train models * Works in browser environment with Three.js global * @module trainRenderer */ /** * TrainRenderer class - manages 3D train visualization */ class TrainRenderer { /** * Create a train renderer * @param {THREE.Scene} scene - Three.js scene * @param {Object} options - Configuration options * @param {string} options.color - Train body color hex */ constructor(scene, options = {}) { this.scene = scene; this.trainMeshes = new Map(); this.trainController = null; // Default train colors this.colors = { body: new THREE.Color(options.color || '#FF6B00'), windows: new THREE.Color('#87CEEB'), wheels: new THREE.Color('#333333'), details: new THREE.Color('#FFFFFF') }; this.trains = {}; } /** * Create a 3D train mesh * @param {string} trainId - Unique train identifier * @returns {THREE.Group} Train mesh group */ createTrainMesh(trainId) { const trainGroup = new THREE.Group(); trainGroup.userData.id = trainId; // Train body (main chassis) const bodyGeometry = new THREE.BoxGeometry(2, 1.2, 4); const bodyMaterial = new THREE.MeshPhongMaterial({ color: this.colors.body, shininess: 50 }); const body = new THREE.Mesh(bodyGeometry, bodyMaterial); body.position.y = 0.6; trainGroup.add(body); // Cab area (front) const cabGeometry = new THREE.BoxGeometry(1.8, 0.8, 1); const cabMaterial = new THREE.MeshPhongMaterial({ color: this.colors.windows, shininess: 30 }); const cab = new THREE.Mesh(cabGeometry, cabMaterial); cab.position.set(0.9, 1.1, 0); cab.rotation.y = Math.PI / 4; trainGroup.add(cab); // Wheels (4 visible wheels) const wheelGeometry = new THREE.CylinderGeometry(0.3, 0.3, 0.2, 16); const wheelMaterial = new THREE.MeshPhongMaterial({ color: this.colors.wheels, shininess: 20 }); const wheelPositions = [ { x: 0.3, y: -0.3, z: 1.2, rotZ: Math.PI / 2 }, { x: 0.3, y: -0.3, z: -1.2, rotZ: Math.PI / 2 }, { x: -0.3, y: -0.3, z: 1.2, rotZ: Math.PI / 2 }, { x: -0.3, y: -0.3, z: -1.2, rotZ: Math.PI / 2 } ]; wheelPositions.forEach(pos => { const wheel = new THREE.Mesh(wheelGeometry, wheelMaterial); wheel.position.set(pos.x, pos.y, pos.z); wheel.rotation.z = pos.rotZ; trainGroup.add(wheel); }); // Train front marker (red light) const lightGeometry = new THREE.SphereGeometry(0.15); const lightMaterial = new THREE.MeshBasicMaterial({ color: 0xFF0000 }); const frontLight = new THREE.Mesh(lightGeometry, lightMaterial); frontLight.position.set(1.01, 1.0, 0); trainGroup.add(frontLight); // Train rear marker (red tail light) const rearLight = new THREE.Mesh(lightGeometry, lightMaterial); rearLight.position.set(-1.01, 1.0, 0); trainGroup.add(rearLight); // Add train to scene this.scene.add(trainGroup); this.trainMeshes.set(trainId, trainGroup); return trainGroup; } /** * Remove a train from the scene * @param {string} trainId - Train identifier to remove */ removeTrain(trainId) { const mesh = this.trainMeshes.get(trainId); if (mesh) { this.scene.remove(mesh); mesh.traverse(child => { if (child.geometry) child.geometry.dispose(); if (child.material) { if (Array.isArray(child.material)) { child.material.forEach(mat => mat.dispose()); } else { child.material.dispose(); } } }); this.trainMeshes.delete(trainId); delete this.trains[trainId]; } } /** * Update train mesh position and rotation based on train state * @param {string} trainId - Train identifier * @param {Object} position - Position {x, y, z} * @param {number} rotation - Rotation angle in radians */ updateTrain(trainId, position, rotation) { const mesh = this.trainMeshes.get(trainId); if (mesh) { mesh.position.set(position.x, position.y, position.z); mesh.rotation.y = rotation; } } /** * Update all train meshes based on train controller progress * @param {number} deltaTime - Time delta in seconds * @param {number} speed - Train speed */ updateAll(deltaTime, speed) { const progress = this.trainController ? this.trainController.progress : 0; const currentPos = this.trainController ? this.trainController.currentPosition : { x: 0, y: 0, z: 0 }; // Calculate orientation based on direction let rotation = 0; if (this.trainController && this.trainController.trackPath.length >= 2) { const totalLength = this.trainController.totalPathLength; const targetDistance = progress * totalLength; let distance = 0; for (let i = 0; i < this.trainController.trackPath.length - 1; i++) { const p1 = this.trainController.trackPath[i]; const p2 = this.trainController.trackPath[i + 1]; const dist = this._distanceBetween(p1, p2); if (targetDistance >= distance && targetDistance <= distance + dist) { const segmentProgress = (targetDistance - distance) / dist; const x1 = p1.x, y1 = p1.y; const x2 = p2.x, y2 = p2.y; rotation = Math.atan2(y2 - y1, x2 - x1); break; } distance += dist; } } // Update mesh positions this.trainMeshes.forEach((mesh, trainId) => { this.updateTrain(trainId, currentPos, rotation); }); } /** * Calculate distance between two points * @param {Object} p1 - First point {x, y, z} * @param {Object} p2 - Second point {x, y, z} * @returns {number} Distance * @private */ _distanceBetween(p1, p2) { const dx = p2.x - p1.x; const dy = p2.y - p1.y; const dz = p2.z - p1.z; return Math.sqrt(dx * dx + dy * dy + dz * dz); } /** * Set train controller reference * @param {TrainController} controller - TrainController instance */ setController(controller) { this.trainController = controller; } /** * Get all train meshes * @returns {Map} Map of trainId to mesh */ getTrainMeshes() { return this.trainMeshes; } /** * Dispose resources */ dispose() { this.trainMeshes.forEach(mesh => { mesh.traverse(child => { if (child.geometry) child.geometry.dispose(); if (child.material) { if (Array.isArray(child.material)) { child.material.forEach(mat => mat.dispose()); } else { child.material.dispose(); } } }); }); this.trainMeshes.clear(); } } // Export for browser global scope if (typeof window !== 'undefined') { window.TrainRenderer = TrainRenderer; }