stl-storage/public/js/stl-viewer.js
kris 3dff6b00d4 Initial commit: STL Storage Application
- Complete web-based STL file storage and 3D viewer
- Express.js backend with SQLite database
- Interactive Three.js 3D viewer with orbit controls
- File upload with drag-and-drop support
- Security features: rate limiting, input validation, helmet
- Container deployment with Docker/Podman
- Production-ready configuration management
- Comprehensive logging and monitoring

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-07 16:18:58 +00:00

217 lines
6.8 KiB
JavaScript

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;
}
}