import * as THREE from 'three'; import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'; import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader.js'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; import './styles.css'; // Global variables let scene, camera, renderer, controls; let cube; let cameraConfig = null; // Load camera configuration from config file async function loadCameraConfig() { try { const response = await fetch('./camera-config.json'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } cameraConfig = await response.json(); console.log('โœ… Camera config loaded successfully:', cameraConfig); return cameraConfig; } catch (error) { console.warn('โš ๏ธ Could not load camera-config.json, using original defaults.'); console.log('๐Ÿ“ To use custom camera settings, ensure camera-config.json is accessible from your web server.'); // Return original simple defaults return { camera: { position: { x: 20, y: 20, z: 20 }, target: { x: 0, y: 0, z: 0 }, distance: 34.64 } }; } } // Initialize the 3D scene async function init() { // Load camera configuration first cameraConfig = await loadCameraConfig(); // Create container const container = document.getElementById('container'); // Create scene scene = new THREE.Scene(); scene.background = new THREE.Color(0x2c3e50); // Create camera with extended far clipping plane for better zoom out camera = new THREE.PerspectiveCamera( 75, // Field of view window.innerWidth / window.innerHeight, // Aspect ratio 0.1, // Near clipping plane 10000 // Extended far clipping plane for better zoom out ); // Use camera config values or defaults const camPos = cameraConfig.camera.position; const camTarget = cameraConfig.camera.target; camera.position.set(camPos.x, camPos.y, camPos.z); camera.lookAt(camTarget.x, camTarget.y, camTarget.z); // Create renderer renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; renderer.outputColorSpace = THREE.SRGBColorSpace; renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.5; // Enable proper transparency rendering renderer.sortObjects = true; renderer.autoClear = false; container.appendChild(renderer.domElement); // Add orbit controls with enhanced touch support controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.dampingFactor = 0.05; controls.enableZoom = true; controls.enablePan = true; controls.enableRotate = true; // Touch-specific settings controls.touches = { ONE: THREE.TOUCH.ROTATE, // Single finger touch for rotation TWO: THREE.TOUCH.DOLLY_PAN // Two fingers for zoom and pan }; // Enhanced touch sensitivity and limits controls.rotateSpeed = 1.0; // Rotation speed controls.zoomSpeed = 2.0; // Increased zoom speed for faster mouse wheel controls.panSpeed = 0.8; // Pan speed // Sync zoom limits with camera clipping planes controls.minDistance = 0.1; // Match camera near plane controls.maxDistance = 8000; // Stay within camera far plane (10000) with buffer // Smooth touch interactions controls.enableDamping = true; controls.dampingFactor = 0.08; // Slightly higher for smoother touch // Add custom touch event handlers setupCustomTouchControls(); // Add lighting setupLighting(); // Load the cube model loadCubeModel(); // Handle window resize window.addEventListener('resize', onWindowResize, false); // Handle print events setupPrintHandlers(); // Setup manual print button setupPrintButton(); // Start render loop animate(); } function setupCustomTouchControls() { const canvas = renderer.domElement; // Add touch-friendly CSS for better interaction canvas.style.touchAction = 'none'; canvas.style.userSelect = 'none'; canvas.style.webkitUserSelect = 'none'; canvas.style.mozUserSelect = 'none'; canvas.style.msUserSelect = 'none'; // Prevent context menu on long press canvas.addEventListener('contextmenu', function(event) { event.preventDefault(); }, { passive: false }); console.log('Touch controls enabled - OrbitControls handles all touch gestures natively'); } function setupPrintHandlers() { const originalBackgroundColor = scene.background; const originalRendererClearColor = renderer.getClearColor(new THREE.Color()); const originalRendererClearAlpha = renderer.getClearAlpha(); let isTestMode = false; let testTimeout = null; // Before print - change background to white and make renderer opaque const beforePrint = () => { if (isTestMode) { console.log('๐Ÿšซ Print event ignored - test mode is active'); return; } console.log('๐Ÿ–จ๏ธ PRINT MODE ACTIVATED - Changing background to white'); // Force scene background to white scene.background = new THREE.Color(0xffffff); // Set renderer clear color to white renderer.setClearColor(0xffffff, 1.0); // Force canvas background to white with multiple methods const canvas = renderer.domElement; canvas.style.setProperty('background', 'white', 'important'); canvas.style.setProperty('background-color', 'white', 'important'); canvas.style.setProperty('background-image', 'none', 'important'); // Also set body and html backgrounds document.body.style.setProperty('background', 'white', 'important'); document.body.style.setProperty('background-color', 'white', 'important'); document.documentElement.style.setProperty('background', 'white', 'important'); document.documentElement.style.setProperty('background-color', 'white', 'important'); // Force a render to apply changes immediately renderer.render(scene, camera); console.log('โœ… Print background forcibly set to white'); }; // After print - restore original background const afterPrint = () => { if (isTestMode) { console.log('๐Ÿšซ Print restore ignored - test mode is active'); return; } console.log('๐Ÿ–จ๏ธ PRINT MODE DEACTIVATED - Restoring original background'); // Restore original settings scene.background = originalBackgroundColor; renderer.setClearColor(originalRendererClearColor, originalRendererClearAlpha); // Remove forced styles const canvas = renderer.domElement; canvas.style.removeProperty('background'); canvas.style.removeProperty('background-color'); canvas.style.removeProperty('background-image'); document.body.style.removeProperty('background'); document.body.style.removeProperty('background-color'); document.documentElement.style.removeProperty('background'); document.documentElement.style.removeProperty('background-color'); console.log('โœ… Original background restored'); }; // Apply white background (used by both test and print modes) const applyWhiteBackground = () => { // Force scene background to white scene.background = new THREE.Color(0xffffff); // Set renderer clear color to white renderer.setClearColor(0xffffff, 1.0); // Force canvas background to white with multiple methods const canvas = renderer.domElement; canvas.style.setProperty('background', 'white', 'important'); canvas.style.setProperty('background-color', 'white', 'important'); canvas.style.setProperty('background-image', 'none', 'important'); // Also set body and html backgrounds document.body.style.setProperty('background', 'white', 'important'); document.body.style.setProperty('background-color', 'white', 'important'); document.documentElement.style.setProperty('background', 'white', 'important'); document.documentElement.style.setProperty('background-color', 'white', 'important'); // Force a render to apply changes immediately renderer.render(scene, camera); }; // Restore original background (used by both test and print modes) const restoreBackground = () => { // Restore original settings scene.background = originalBackgroundColor; renderer.setClearColor(originalRendererClearColor, originalRendererClearAlpha); // Remove forced styles const canvas = renderer.domElement; canvas.style.removeProperty('background'); canvas.style.removeProperty('background-color'); canvas.style.removeProperty('background-image'); document.body.style.removeProperty('background'); document.body.style.removeProperty('background-color'); document.documentElement.style.removeProperty('background'); document.documentElement.style.removeProperty('background-color'); }; // Test the print handler immediately (for debugging) window.testPrintMode = () => { console.log('๐Ÿงช Testing print mode (print events disabled during test)...'); isTestMode = true; if (testTimeout) { clearTimeout(testTimeout); } applyWhiteBackground(); console.log('โœ… Test mode background set to white'); testTimeout = setTimeout(() => { console.log('๐Ÿงช Test complete, restoring...'); restoreBackground(); isTestMode = false; testTimeout = null; console.log('โœ… Test mode complete, print events re-enabled'); }, 6000); }; // Cancel test mode early window.cancelTestMode = () => { if (testTimeout) { clearTimeout(testTimeout); testTimeout = null; } if (isTestMode) { restoreBackground(); isTestMode = false; console.log('๐Ÿงช Test mode cancelled, print events re-enabled'); } }; // Listen for print events if (window.matchMedia) { const mediaQueryList = window.matchMedia('print'); mediaQueryList.addEventListener('change', (mql) => { console.log('๐Ÿ“ฑ Media query change detected:', mql.matches ? 'PRINT' : 'SCREEN'); if (mql.matches) { beforePrint(); } else { afterPrint(); } }); } // Fallback for older browsers window.addEventListener('beforeprint', () => { console.log('๐Ÿ–จ๏ธ beforeprint event fired'); beforePrint(); }); window.addEventListener('afterprint', () => { console.log('๐Ÿ–จ๏ธ afterprint event fired'); afterPrint(); }); console.log('๐Ÿ”ง Print event handlers set up.'); console.log(' Use window.testPrintMode() to test for 6 seconds'); console.log(' Use window.cancelTestMode() to end test early'); } function setupPrintButton() { const printBtn = document.getElementById('print-btn'); if (!printBtn) { console.warn('Print button not found'); return; } const originalBackgroundColor = scene.background; const originalRendererClearColor = renderer.getClearColor(new THREE.Color()); const originalRendererClearAlpha = renderer.getClearAlpha(); printBtn.addEventListener('click', () => { console.log('๐Ÿ–จ๏ธ Manual print button clicked - forcing white background'); // Force white background immediately scene.background = new THREE.Color(0xffffff); renderer.setClearColor(0xffffff, 1.0); // Force canvas and page backgrounds to white const canvas = renderer.domElement; canvas.style.setProperty('background', 'white', 'important'); canvas.style.setProperty('background-color', 'white', 'important'); canvas.style.setProperty('background-image', 'none', 'important'); document.body.style.setProperty('background', 'white', 'important'); document.body.style.setProperty('background-color', 'white', 'important'); document.documentElement.style.setProperty('background', 'white', 'important'); document.documentElement.style.setProperty('background-color', 'white', 'important'); // Force a render to apply changes renderer.render(scene, camera); console.log('โœ… Background forced to white, opening print dialog...'); // Small delay to ensure changes are applied, then open print dialog setTimeout(() => { window.print(); // Restore background after a delay to account for print dialog setTimeout(() => { console.log('๐Ÿ”„ Restoring original background...'); scene.background = originalBackgroundColor; renderer.setClearColor(originalRendererClearColor, originalRendererClearAlpha); canvas.style.removeProperty('background'); canvas.style.removeProperty('background-color'); canvas.style.removeProperty('background-image'); document.body.style.removeProperty('background'); document.body.style.removeProperty('background-color'); document.documentElement.style.removeProperty('background'); document.documentElement.style.removeProperty('background-color'); console.log('โœ… Original background restored'); }, 1000); // Wait 1 second after print dialog opens }, 100); // Small delay to apply changes }); console.log('๐Ÿ–จ๏ธ Manual print button set up'); } function setupLighting() { // Ambient light for overall illumination - increased intensity const ambientLight = new THREE.AmbientLight(0x404040, 1.2); scene.add(ambientLight); // Directional light (like sunlight) - increased intensity const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2); directionalLight.position.set(10, 10, 5); directionalLight.castShadow = true; directionalLight.shadow.mapSize.width = 2048; directionalLight.shadow.mapSize.height = 2048; scene.add(directionalLight); // Point light for additional illumination - increased intensity const pointLight = new THREE.PointLight(0xffffff, 0.8, 100); pointLight.position.set(-10, 10, 10); scene.add(pointLight); // Camera light - follows the camera to illuminate from viewing angle window.cameraLight = new THREE.PointLight(0xffffff, 1.0, 200); window.cameraLight.position.copy(camera.position); scene.add(window.cameraLight); // Additional camera spotlight for focused illumination window.cameraSpotlight = new THREE.SpotLight(0xffffff, 0.8, 100, Math.PI / 4, 0.3); window.cameraSpotlight.position.copy(camera.position); window.cameraSpotlight.target.position.set(0, 0, 0); // Point at origin initially scene.add(window.cameraSpotlight); scene.add(window.cameraSpotlight.target); // Interior point lights (will be repositioned when model loads) - increased intensity window.interiorLight = new THREE.PointLight(0xffffff, 0.8, 50); window.interiorLight.position.set(0, 0, 0); // Will be moved to model center scene.add(window.interiorLight); // Additional interior light for better coverage - increased intensity window.interiorLight2 = new THREE.PointLight(0xffffff, 0.5, 30); window.interiorLight2.position.set(5, 5, -5); // Will be adjusted relative to model scene.add(window.interiorLight2); // Hemisphere light for natural lighting - increased intensity const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.8); scene.add(hemiLight); console.log('โœจ Camera lights added - they will follow your view'); // Create environment map for glass reflections setupEnvironmentMap(); } function setupEnvironmentMap() { // Create a simple cube texture for reflections (makes glass more visible) const cubeTextureLoader = new THREE.CubeTextureLoader(); // Create a procedural cube map for reflections const envMapRenderTarget = new THREE.WebGLCubeRenderTarget(256, { generateMipmaps: true, minFilter: THREE.LinearMipmapLinearFilter }); // Simple gradient environment for reflections const canvas = document.createElement('canvas'); canvas.width = 256; canvas.height = 256; const ctx = canvas.getContext('2d'); // Create gradient for reflections const gradient = ctx.createLinearGradient(0, 0, 0, 256); gradient.addColorStop(0, '#87CEEB'); // Sky blue gradient.addColorStop(0.5, '#F0F8FF'); // Alice blue gradient.addColorStop(1, '#4682B4'); // Steel blue ctx.fillStyle = gradient; ctx.fillRect(0, 0, 256, 256); // Convert to texture const texture = new THREE.CanvasTexture(canvas); texture.mapping = THREE.EquirectangularReflectionMapping; // Store for glass materials window.environmentMap = texture; } function loadCubeModel() { // Create loaders const mtlLoader = new MTLLoader(); const objLoader = new OBJLoader(); // Load material file first mtlLoader.load('cube.mtl', function(materials) { materials.preload(); // Configure materials for double-sided rendering with proper transparency Object.keys(materials.materials).forEach(function(key) { const material = materials.materials[key]; material.side = THREE.DoubleSide; // Handle glass materials specially if (key.toLowerCase().includes('glass')) { material.transparent = true; // Preserve MTL file opacity values for glass materials // Only apply default opacity if not already set in MTL if (material.opacity === undefined || material.opacity === 1.0) { material.opacity = 0.7; // Fallback for visibility } // Enhanced glass properties for better visibility material.shininess = 100; material.reflectivity = 0.8; // Glass-like color with slight tint for visibility material.color = new THREE.Color(0.9, 0.95, 1.0); // Add specular highlights to make glass more apparent material.specular = new THREE.Color(0.8, 0.9, 1.0); // Slight emissive to make glass edges more visible if (material.emissive) { material.emissive.setHex(0x001122); // Very subtle blue glow } // Apply environment map for reflections if (window.environmentMap) { material.envMap = window.environmentMap; material.envMapIntensity = 0.9; } console.log(`Glass material '${key}' configured with opacity: ${material.opacity}`); } else { // Non-glass materials material.transparent = false; material.opacity = 1.0; // Add emissive for interior visibility on solid materials if (material.emissive) { material.emissive.setHex(0x222222); // Increased emissive brightness } else { material.emissive = new THREE.Color(0x222222); } } material.needsUpdate = true; }); // Set materials to OBJ loader objLoader.setMaterials(materials); // Load the OBJ file objLoader.load('cube.obj', function(object) { // Scale and position the model object.scale.set(0.5, 0.5, 0.5); object.position.set(0, 0, 0); // Enable shadows and fix materials for interior faces object.traverse(function(child) { if (child instanceof THREE.Mesh) { child.castShadow = true; child.receiveShadow = true; // Special handling for glass materials to improve visibility if (child.material && child.material.name && child.material.name.toLowerCase().includes('glass')) { // Glass-specific settings child.material.side = THREE.DoubleSide; child.material.transparent = true; // Preserve original opacity from MTL if available if (child.material.opacity === undefined || child.material.opacity === 1.0) { child.material.opacity = 0.7; } child.material.alphaTest = 0.1; // Enhanced reflective properties child.material.envMapIntensity = 0.9; // Apply environment map for reflections if (window.environmentMap) { child.material.envMap = window.environmentMap; } // Disable shadow casting for glass (more realistic) child.castShadow = false; child.receiveShadow = true; console.log('Applied glass-specific settings to mesh'); // Add subtle wireframe to glass edges for better visibility if (child.geometry) { const wireframe = new THREE.EdgesGeometry(child.geometry); const wireframeMaterial = new THREE.LineBasicMaterial({ color: 0x88aaff, transparent: true, opacity: 0.3, linewidth: 2 }); const wireframeLines = new THREE.LineSegments(wireframe, wireframeMaterial); child.add(wireframeLines); } } else { // Non-glass materials child.material.side = THREE.DoubleSide; child.material.transparent = false; child.material.opacity = 1.0; // Ensure proper lighting on both sides if (child.material.type === 'MeshLambertMaterial' || child.material.type === 'MeshPhongMaterial') { child.material.emissive = new THREE.Color(0x222222); // Increased self-illumination for brightness } } child.material.needsUpdate = true; } }); // Store reference and add to scene cube = object; scene.add(cube); // Calculate model center and adjust controls centerModelAndControls(cube); // Hide loading indicator document.getElementById('loading').style.display = 'none'; console.log('Cube loaded successfully!'); }, function(progress) { console.log('Loading progress:', (progress.loaded / progress.total * 100) + '%'); }, function(error) { console.error('Error loading OBJ file:', error); document.getElementById('loading').innerHTML = '
Error loading model. Please check console for details.
'; }); }, function(progress) { console.log('Loading MTL progress:', (progress.loaded / progress.total * 100) + '%'); }, function(error) { console.error('Error loading MTL file:', error); // Try to load OBJ without materials loadObjWithoutMaterials(); }); } function loadObjWithoutMaterials() { const objLoader = new OBJLoader(); objLoader.load('cube.obj', function(object) { // Apply default material with better interior visibility const defaultMaterial = new THREE.MeshPhongMaterial({ color: 0xffffff, side: THREE.DoubleSide, transparent: false, opacity: 1.0, emissive: new THREE.Color(0x222222), // Increased self-illumination for brightness shininess: 30 }); object.traverse(function(child) { if (child instanceof THREE.Mesh) { child.material = defaultMaterial.clone(); // Clone to avoid sharing child.castShadow = true; child.receiveShadow = true; } }); // Scale and position object.scale.set(0.5, 0.5, 0.5); object.position.set(0, 0, 0); cube = object; scene.add(cube); // Calculate model center and adjust controls centerModelAndControls(cube); document.getElementById('loading').style.display = 'none'; console.log('Cube loaded without materials!'); }, function(progress) { console.log('Loading progress:', (progress.loaded / progress.total * 100) + '%'); }, function(error) { console.error('Error loading OBJ file:', error); document.getElementById('loading').innerHTML = '
Failed to load model. Check if cube.obj exists.
'; }); } function centerModelAndControls(object) { // Calculate the bounding box of the model const boundingBox = new THREE.Box3().setFromObject(object); // Get the center point of the bounding box const center = boundingBox.getCenter(new THREE.Vector3()); // Get the size of the bounding box const size = boundingBox.getSize(new THREE.Vector3()); // Set the orbit controls target to the model's center controls.target.copy(center); // Calculate optimal camera distance based on model size const maxDimension = Math.max(size.x, size.y, size.z); const fov = camera.fov * (Math.PI / 180); const distance = Math.abs(maxDimension / Math.sin(fov / 2)) * 1.2; // 1.2 for some padding // Position camera at optimal distance from model center const direction = camera.position.clone().sub(center).normalize(); camera.position.copy(center).add(direction.multiplyScalar(distance)); // Update camera to look at the model center camera.lookAt(center); // Reposition interior lights relative to model center if (window.interiorLight) { window.interiorLight.position.copy(center); // Adjust light range based on model size window.interiorLight.distance = maxDimension * 2; } if (window.interiorLight2) { window.interiorLight2.position.copy(center).add(new THREE.Vector3( size.x * 0.3, size.y * 0.3, -size.z * 0.3 )); window.interiorLight2.distance = maxDimension * 1.5; } // Update camera lights to target the model center if (window.cameraLight) { window.cameraLight.distance = maxDimension * 3; // Adjust range based on model size } if (window.cameraSpotlight) { window.cameraSpotlight.target.position.copy(center); window.cameraSpotlight.distance = maxDimension * 2; } // Update controls controls.update(); // Add rotation logging setupRotationLogging(); console.log('Model centered at:', center); console.log('Model size:', size); console.log('Camera positioned at:', camera.position); } function setupRotationLogging() { let debounceTimer = null; // Log rotation and zoom changes with 1-second debounce controls.addEventListener('change', function() { // Clear existing timer if (debounceTimer) { clearTimeout(debounceTimer); } // Set new timer for 1 second delay debounceTimer = setTimeout(() => { const distance = camera.position.distanceTo(controls.target); const position = camera.position.clone(); const target = controls.target.clone(); // Calculate spherical coordinates for rotation const spherical = new THREE.Spherical().setFromVector3( position.clone().sub(target) ); console.log('=== CAMERA STATE ==='); console.log('Position:', { x: Math.round(position.x * 100) / 100, y: Math.round(position.y * 100) / 100, z: Math.round(position.z * 100) / 100 }); console.log('Target:', { x: Math.round(target.x * 100) / 100, y: Math.round(target.y * 100) / 100, z: Math.round(target.z * 100) / 100 }); console.log('Distance (Zoom):', Math.round(distance * 100) / 100); console.log('Spherical Rotation:'); console.log(' - Radius:', Math.round(spherical.radius * 100) / 100); console.log(' - Theta (horizontal):', Math.round(spherical.theta * 100) / 100, 'radians'); console.log(' - Phi (vertical):', Math.round(spherical.phi * 100) / 100, 'radians'); console.log('=================='); debounceTimer = null; }, 1000); }); console.log('Rotation logging enabled - camera state will be logged 1 second after movement stops'); } function animate() { requestAnimationFrame(animate); // Update controls controls.update(); // Update camera lights to follow camera position if (window.cameraLight) { window.cameraLight.position.copy(camera.position); } if (window.cameraSpotlight) { window.cameraSpotlight.position.copy(camera.position); // Make spotlight point towards the orbit controls target (model center) if (controls && controls.target) { window.cameraSpotlight.target.position.copy(controls.target); } } // Clear and render with proper transparency sorting renderer.clear(); renderer.render(scene, camera); } function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } // Initialize when page loads window.addEventListener('load', async () => { await init(); });