/** * Railtrack Pro - Three.js Renderer * Handles 3D scene setup, camera, and rendering loop * @author Railtrack Pro Development Team */ class Renderer { constructor() { this.scene = null; this.camera = null; this.renderer = null; this.controls = null; this.gridHelper = null; this.trackMeshes = []; this.selectedObject = null; this.isDragging = false; this.raycaster = new THREE.Raycaster(); this.mouse = new THREE.Vector2(); this.init(); } /** * Initialize Three.js scene with camera, renderer, and controls */ init() { // Create scene this.scene = new THREE.Scene(); this.scene.background = new THREE.Color(0x1a1a2e); this.scene.fog = new THREE.Fog(0x1a1a2e, 50, 200); // Create camera this.camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.1, 1000 ); this.camera.position.set(20, 20, 20); this.camera.lookAt(0, 0, 0); // Create renderer this.renderer = new THREE.WebGLRenderer({ antialias: true }); this.renderer.setSize(window.innerWidth - 50, window.innerHeight - 150); this.renderer.shadowMap.enabled = true; this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; document.getElementById('viewport').appendChild(this.renderer.domElement); // Add orbit controls this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement); this.controls.enableDamping = true; this.controls.dampingFactor = 0.05; this.controls.maxPolarAngle = Math.PI / 2 - 0.1; this.controls.minDistance = 5; this.controls.maxDistance = 100; // Add lighting this.setupLighting(); // Add ground grid this.addGridHelper(); // Add event listeners this.addEventListeners(); // Start animation loop this.animate(); console.log('[Renderer] Initialized successfully'); } /** * Setup scene lighting */ setupLighting() { // Ambient light const ambientLight = new THREE.AmbientLight(0x404040, 1); this.scene.add(ambientLight); // Directional light (sun) const directionalLight = new THREE.DirectionalLight(0xffffff, 1); directionalLight.position.set(20, 30, 20); directionalLight.castShadow = true; directionalLight.shadow.mapSize.width = 2048; directionalLight.shadow.mapSize.height = 2048; directionalLight.shadow.camera.near = 0.5; directionalLight.shadow.camera.far = 100; directionalLight.shadow.camera.left = -50; directionalLight.shadow.camera.right = 50; directionalLight.shadow.camera.top = 50; directionalLight.shadow.camera.bottom = -50; this.scene.add(directionalLight); // Point light near center const pointLight = new THREE.PointLight(0xfb5607, 0.5, 30); pointLight.position.set(0, 10, 0); this.scene.add(pointLight); console.log('[Renderer] Lighting setup complete'); } /** * Add ground plane with grid helper */ addGridHelper() { // Ground plane const planeGeometry = new THREE.PlaneGeometry(100, 100); const planeMaterial = new THREE.MeshStandardMaterial({ color: 0x2a2a4e, roughness: 0.8, metalness: 0.2 }); const ground = new THREE.Mesh(planeGeometry, planeMaterial); ground.rotation.x = -Math.PI / 2; ground.receiveShadow = true; this.scene.add(ground); // Grid helper this.gridHelper = new THREE.GridHelper(100, 50, 0x404080, 0x202040); this.scene.add(this.gridHelper); // Axis helper const axesHelper = new THREE.AxesHelper(5); this.scene.add(axesHelper); console.log('[Renderer] Grid helper added'); } /** * Add event listeners for mouse interactions */ addEventListeners() { window.addEventListener('resize', () => this.onWindowResize()); this.renderer.domElement.addEventListener('mousedown', (e) => this.onMouseDown(e)); this.renderer.domElement.addEventListener('mousemove', (e) => this.onMouseMove(e)); this.renderer.domElement.addEventListener('mouseup', (e) => this.onMouseUp(e)); this.renderer.domElement.addEventListener('click', (e) => this.onClick(e)); this.renderer.domElement.addEventListener('contextmenu', (e) => e.preventDefault()); } /** * Handle window resize */ onWindowResize() { const viewport = document.getElementById('viewport'); this.camera.aspect = viewport.clientWidth / viewport.clientHeight; this.camera.updateProjectionMatrix(); this.renderer.setSize(viewport.clientWidth, viewport.clientHeight); } /** * Handle mouse down event */ onMouseDown(event) { this.isDragging = true; this.updateMouse(event); this.raycaster.setFromCamera(this.mouse, this.camera); // Track track meshes const intersects = this.raycaster.intersectObjects(this.trackMeshes); if (intersects.length > 0) { this.selectedObject = intersects[0].object; this.highlightSelection(this.selectedObject); } else { this.deselectAll(); } } /** * Handle mouse move event */ onMouseMove(event) { this.updateMouse(event); // Highlight object under cursor this.raycaster.setFromCamera(this.mouse, this.camera); const intersects = this.raycaster.intersectObjects(this.trackMeshes); if (intersects.length > 0 && intersects[0].object !== this.selectedObject) { this.highlightHover(intersects[0].object); } else { this.removeHoverHighlight(); } } /** * Handle mouse up event */ onMouseUp() { this.isDragging = false; } /** * Handle click event */ onClick(event) { if (!this.isDragging && this.selectedObject) { console.log('[Renderer] Track clicked:', this.selectedObject.userData); Game.onTrackClick(this.selectedObject); } } /** * Update mouse position from event */ updateMouse(event) { const rect = this.renderer.domElement.getBoundingClientRect(); this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; } /** * Highlight selected object */ highlightSelection(object) { // Remove previous highlight if (object.originalColor) { object.material.emissive.setHex(object.originalColor); } // Store original color and set highlight object.originalColor = object.material.emissive.getHex(); object.material.emissive.setHex(0xfb5607); this.selectedObject = object; // Update info panel Game.updateSelectedInfo(object); } /** * Highlight hover object */ highlightHover(object) { if (object.hoverColor) { object.material.emissive.setHex(object.hoverColor); } else { object.hoverColor = object.material.emissive.getHex(); object.material.emissive.setHex(0x404080); } } /** * Remove hover highlight */ removeHoverHighlight() { for (const mesh of this.trackMeshes) { if (mesh.hoverColor) { mesh.material.emissive.setHex(mesh.hoverColor); delete mesh.hoverColor; } } } /** * Deselect all objects */ deselectAll() { if (this.selectedObject) { if (this.selectedObject.originalColor) { this.selectedObject.material.emissive.setHex(this.selectedObject.originalColor); } this.selectedObject = null; } Game.updateSelectedInfo(null); } /** * Add track piece to scene */ addTrackPiece(geometry, material, position, rotation) { const mesh = new THREE.Mesh(geometry, material); mesh.position.copy(position); mesh.rotation.set(rotation.x, rotation.y, rotation.z); mesh.castShadow = true; mesh.receiveShadow = true; mesh.userData = { type: 'track', id: this.trackMeshes.length }; this.scene.add(mesh); this.trackMeshes.push(mesh); return mesh; } /** * Remove track piece from scene */ removeTrackPiece(mesh) { const index = this.trackMeshes.indexOf(mesh); if (index > -1) { this.trackMeshes.splice(index, 1); this.scene.remove(mesh); mesh.geometry.dispose(); mesh.material.dispose(); } } /** * Animation loop */ animate() { requestAnimationFrame(() => this.animate()); this.controls.update(); this.renderer.render(this.scene, this.camera); } } // Export for module usage if (typeof module !== 'undefined' && module.exports) { module.exports = Renderer; }