class STLViewer { constructor(containerId) { console.log('STLViewer constructor called with containerId:', containerId); this.container = document.getElementById(containerId); console.log('Container element:', this.container); if (!this.container) { throw new Error(`Container element with id "${containerId}" not found`); } this.scene = null; this.camera = null; this.renderer = null; this.controls = null; this.mesh = null; this.animationId = null; console.log('Initializing STLViewer...'); this.init(); console.log('STLViewer initialization complete'); } init() { const width = this.container.clientWidth || 800; const height = this.container.clientHeight || 600; console.log('Container dimensions:', { width, height }); // Scene console.log('Creating scene...'); this.scene = new THREE.Scene(); this.scene.background = new THREE.Color(0xf0f0f0); // Camera console.log('Creating camera...'); this.camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000); this.camera.position.set(0, 0, 50); // Renderer console.log('Creating renderer...'); this.renderer = new THREE.WebGLRenderer({ antialias: true }); this.renderer.setSize(width, height); this.renderer.shadowMap.enabled = true; this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; console.log('Appending renderer to container...'); this.container.appendChild(this.renderer.domElement); // Controls console.log('Creating controls...'); if (typeof THREE.OrbitControls === 'undefined') { throw new Error('OrbitControls not available'); } this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement); this.controls.enableDamping = true; this.controls.dampingFactor = 0.05; // Lighting console.log('Setting up lighting...'); this.setupLighting(); // Handle window resize window.addEventListener('resize', () => this.onWindowResize()); console.log('Starting animation loop...'); this.animate(); } setupLighting() { // Ambient light const ambientLight = new THREE.AmbientLight(0x404040, 0.6); this.scene.add(ambientLight); // Directional light const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); directionalLight.position.set(50, 50, 50); directionalLight.castShadow = true; directionalLight.shadow.mapSize.width = 2048; directionalLight.shadow.mapSize.height = 2048; this.scene.add(directionalLight); // Point lights for better illumination const pointLight1 = new THREE.PointLight(0xffffff, 0.4, 100); pointLight1.position.set(-50, 25, 25); this.scene.add(pointLight1); const pointLight2 = new THREE.PointLight(0xffffff, 0.4, 100); pointLight2.position.set(50, -25, -25); this.scene.add(pointLight2); } async loadSTL(url) { return new Promise((resolve, reject) => { console.log('Loading STL from:', url); const loader = new THREE.STLLoader(); loader.load( url, (geometry) => { console.log('STL loaded successfully, vertices:', geometry.attributes.position.count); this.displayGeometry(geometry); resolve(); }, (progress) => { console.log('Loading progress:', (progress.loaded / progress.total * 100) + '%'); }, (error) => { console.error('Error loading STL:', error); reject(error); } ); }); } displayGeometry(geometry) { // Remove existing mesh if (this.mesh) { this.scene.remove(this.mesh); } // Center geometry geometry.center(); geometry.computeVertexNormals(); // Material const material = new THREE.MeshPhongMaterial({ color: 0x00aa88, shininess: 100, side: THREE.DoubleSide }); // Create mesh this.mesh = new THREE.Mesh(geometry, material); this.mesh.castShadow = true; this.mesh.receiveShadow = true; this.scene.add(this.mesh); // Auto-fit camera to object this.fitCameraToObject(); } fitCameraToObject() { if (!this.mesh) return; const box = new THREE.Box3().setFromObject(this.mesh); const center = box.getCenter(new THREE.Vector3()); const size = box.getSize(new THREE.Vector3()); const maxDim = Math.max(size.x, size.y, size.z); const fov = this.camera.fov * (Math.PI / 180); let cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2)); cameraZ *= 1.5; // Add some padding this.camera.position.set(center.x, center.y, center.z + cameraZ); this.controls.target.copy(center); this.controls.update(); } setWireframe(enabled) { if (this.mesh) { this.mesh.material.wireframe = enabled; } } setColor(color) { if (this.mesh) { this.mesh.material.color.setHex(color); } } resetView() { if (this.mesh) { this.fitCameraToObject(); } } onWindowResize() { const width = this.container.clientWidth; const height = this.container.clientHeight; this.camera.aspect = width / height; this.camera.updateProjectionMatrix(); this.renderer.setSize(width, height); } animate() { this.animationId = requestAnimationFrame(() => this.animate()); this.controls.update(); this.renderer.render(this.scene, this.camera); } dispose() { if (this.animationId) { cancelAnimationFrame(this.animationId); } if (this.mesh) { this.scene.remove(this.mesh); this.mesh.geometry.dispose(); this.mesh.material.dispose(); } if (this.renderer) { this.container.removeChild(this.renderer.domElement); this.renderer.dispose(); } this.controls?.dispose(); } screenshot() { if (this.renderer) { return this.renderer.domElement.toDataURL('image/png'); } return null; } }