- 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>
217 lines
6.8 KiB
JavaScript
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;
|
|
}
|
|
} |