310 lines
9.2 KiB
JavaScript
310 lines
9.2 KiB
JavaScript
/**
|
|
* 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;
|
|
}
|