/** * Train Module * Handles train model and controller for driving along tracks * Works in browser environment with Three.js global * @module train */ /** * Train class - represents a train that can move along tracks */ class Train { /** * Create a train instance * @param {Object} options - Train configuration options * @param {string} options.id - Unique identifier for the train * @param {number} options.speed - Initial speed * @param {number} options.maxSpeed - Maximum speed * @param {number} options.direction - Direction (1 or -1) * @param {number} options.length - Train length for display * @param {string} options.color - Train body color */ constructor(options = {}) { this.id = options.id || `train-${Date.now()}`; this.speed = options.speed || 0; this.maxSpeed = options.maxSpeed || 5; this.acceleration = options.acceleration || 0.5; this.brakeDeceleration = options.brakeDeceleration || 0.3; this.direction = options.direction || 1; this.length = options.length || 3; this.isMoving = false; this.threeMesh = null; this.color = options.color || '#FF6B00'; } /** * Accelerate the train */ accelerate() { this.speed = Math.min(this.speed + this.acceleration * this.direction, this.maxSpeed * this.direction); this.isMoving = this.speed !== 0; } /** * Brake the train */ brake() { const brakeAmount = this.brakeDeceleration * this.direction; const newSpeed = this.speed - brakeAmount; if (Math.abs(newSpeed) < Math.abs(this.brakeDeceleration)) { this.speed = 0; this.isMoving = false; } else { this.speed = newSpeed; } } /** * Reverse direction */ reverse() { this.direction *= -1; } /** * Stop the train */ stop() { this.speed = 0; this.isMoving = false; } /** * Update train physics based on speed * @param {number} deltaTime - Time delta in seconds */ update(deltaTime) { if (this.isMoving) { this.speed = this.speed > 0 ? Math.max(this.speed - 0.1 * this.direction, 0) : Math.min(this.speed + 0.1 * this.direction, 0); this.isMoving = this.speed !== 0; } } /** * Get train velocity * @returns {number} Train velocity */ getVelocity() { return this.speed; } } /** * TrainController class - manages train position along track path */ class TrainController { /** * Create a train controller * @param {Array} trackPath - Array of {x, y, z} points defining the track path */ constructor(trackPath = []) { this.trackPath = trackPath || []; this.progress = 0; // 0.0 to 1.0 along the path this.distance = 0; // Distance traveled along path this.currentPosition = { x: 0, y: 0, z: 0 }; this.totalPathLength = this._calculatePathLength(); } /** * Calculate total path length * @returns {number} Total length of the track path * @private */ _calculatePathLength() { if (this.trackPath.length < 2) return 0; let totalLength = 0; for (let i = 1; i < this.trackPath.length; i++) { const p1 = this.trackPath[i - 1]; const p2 = this.trackPath[i]; const dx = p2.x - p1.x; const dy = p2.y - p1.y; const dz = p2.z - p1.z; totalLength += Math.sqrt(dx * dx + dy * dy + dz * dz); } return totalLength; } /** * Set distance traveled along the path * @param {number} distance - Distance to set */ setDistance(distance) { this.distance = Math.max(0, Math.min(distance, this.totalPathLength)); this.progress = this.totalPathLength > 0 ? this.distance / this.totalPathLength : 0; this.updateCurrentPosition(); } /** * Get current position on the track path * @returns {Object} Position {x, y, z} */ getCurrentPosition() { return this.currentPosition; } /** * Update current position based on progress * @private */ updateCurrentPosition() { if (this.trackPath.length < 2) { this.currentPosition = this.trackPath[0] || { x: 0, y: 0, z: 0 }; return; } const totalLength = this.totalPathLength; const targetDistance = this.progress * totalLength; let distance = 0; for (let i = 0; i < this.trackPath.length - 1; i++) { const p1 = this.trackPath[i]; const p2 = this.trackPath[i + 1]; const p1Length = distance; const p2Length = distance + this._distanceBetween(p1, p2); if (targetDistance >= p1Length && targetDistance <= p2Length) { const segmentProgress = (targetDistance - p1Length) / (p2Length - p1Length); this.currentPosition = { x: p1.x + (p2.x - p1.x) * segmentProgress, y: p1.y + (p2.y - p1.y) * segmentProgress, z: p1.z + (p2.z - p1.z) * segmentProgress }; return; } distance += this._distanceBetween(p1, p2); } // End of path this.currentPosition = this.trackPath[this.trackPath.length - 1]; } /** * 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); } /** * Update controller based on train speed * @param {number} deltaTime - Time delta in seconds * @param {number} speed - Train speed */ update(deltaTime, speed) { const distanceDelta = speed * deltaTime; this.setDistance(this.distance + distanceDelta); } /** * Set track path * @param {Array} path - Array of {x, y, z} points */ setTrackPath(path) { this.trackPath = path; this.totalPathLength = this._calculatePathLength(); this.progress = 0; this.distance = 0; this.updateCurrentPosition(); } } // Export for browser global scope if (typeof window !== 'undefined') { window.Train = Train; window.TrainController = TrainController; } // Export for Node.js/Jest module.exports = { Train, TrainController };