diff --git a/src/camera-config.js b/src/camera-config.js
new file mode 100644
index 0000000..26f15a8
--- /dev/null
+++ b/src/camera-config.js
@@ -0,0 +1,24 @@
+// Load camera configuration from config file
+export async function loadCameraConfig() {
+ try {
+ const response = await fetch('./camera-config.json');
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const 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
+ }
+ };
+ }
+}
diff --git a/src/fpv-controls.js b/src/fpv-controls.js
new file mode 100644
index 0000000..034da61
--- /dev/null
+++ b/src/fpv-controls.js
@@ -0,0 +1,328 @@
+import * as THREE from 'three';
+
+// FPV Mode variables
+let fpvMode = false;
+let fpvControls = {
+ moveForward: false,
+ moveBackward: false,
+ moveLeft: false,
+ moveRight: false,
+ jump: false,
+ crouch: false,
+ canJump: true,
+ velocity: new THREE.Vector3(),
+ direction: new THREE.Vector3(),
+ rotation: { x: 0, y: 0 },
+ verticalVelocity: 0,
+ isJumping: false,
+ isCrouching: false,
+ currentHeight: 0,
+ targetHeight: 0
+};
+
+export function setupFPVButton(renderer) {
+ const fpvBtn = document.getElementById('fpv-btn');
+ if (!fpvBtn) {
+ console.warn('FPV button not found');
+ return;
+ }
+
+ fpvBtn.addEventListener('click', () => {
+ if (!fpvMode) {
+ enterFPVMode(renderer);
+ } else {
+ exitFPVMode();
+ }
+ });
+
+ console.log('๐ถ FPV button set up');
+}
+
+export function setupFPVControls() {
+ // Keyboard event handlers
+ document.addEventListener('keydown', onKeyDown);
+ document.addEventListener('keyup', onKeyUp);
+
+ // Mouse event handlers for FPV mode
+ document.addEventListener('mousemove', onMouseMove);
+ document.addEventListener('click', onMouseClick);
+
+ console.log('FPV controls setup complete. Press F to enter FPV mode');
+}
+
+function onKeyDown(event) {
+ switch (event.code) {
+ case 'KeyF':
+ event.preventDefault();
+ event.stopPropagation();
+ if (!fpvMode) {
+ enterFPVMode();
+ } else {
+ exitFPVMode();
+ }
+ return false;
+ case 'Escape':
+ if (fpvMode) {
+ event.preventDefault();
+ event.stopPropagation();
+ exitFPVMode();
+ return false;
+ }
+ break;
+ case 'KeyW':
+ if (fpvMode) fpvControls.moveForward = true;
+ break;
+ case 'KeyA':
+ if (fpvMode) fpvControls.moveLeft = true;
+ break;
+ case 'KeyS':
+ if (fpvMode) fpvControls.moveBackward = true;
+ break;
+ case 'KeyD':
+ if (fpvMode) fpvControls.moveRight = true;
+ break;
+ case 'Space':
+ if (fpvMode) {
+ event.preventDefault();
+ fpvControls.jump = true;
+ }
+ break;
+ case 'ShiftLeft':
+ case 'ShiftRight':
+ if (fpvMode) {
+ event.preventDefault();
+ fpvControls.crouch = true;
+ fpvControls.isCrouching = true;
+ }
+ break;
+ }
+}
+
+function onKeyUp(event) {
+ switch (event.code) {
+ case 'KeyW':
+ if (fpvMode) fpvControls.moveForward = false;
+ break;
+ case 'KeyA':
+ if (fpvMode) fpvControls.moveLeft = false;
+ break;
+ case 'KeyS':
+ if (fpvMode) fpvControls.moveBackward = false;
+ break;
+ case 'KeyD':
+ if (fpvMode) fpvControls.moveRight = false;
+ break;
+ case 'Space':
+ if (fpvMode) fpvControls.jump = false;
+ break;
+ case 'ShiftLeft':
+ case 'ShiftRight':
+ if (fpvMode) {
+ fpvControls.crouch = false;
+ fpvControls.isCrouching = false;
+ }
+ break;
+ }
+}
+
+function onMouseMove(event) {
+ if (!fpvMode) return;
+
+ const movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0;
+ const movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0;
+
+ fpvControls.rotation.y -= movementX * 0.002; // Horizontal rotation (yaw)
+ fpvControls.rotation.x -= movementY * 0.002; // Vertical rotation (pitch)
+
+ // Limit vertical look angle
+ fpvControls.rotation.x = Math.max(-Math.PI/2, Math.min(Math.PI/2, fpvControls.rotation.x));
+}
+
+function onMouseClick(event) {
+ if (fpvMode && event.target && event.target.requestPointerLock) {
+ // Request pointer lock for mouse control
+ event.target.requestPointerLock();
+ }
+}
+
+function enterFPVMode(renderer) {
+ if (!window.modelBounds) {
+ console.warn('Model not loaded yet, cannot enter FPV mode');
+ return;
+ }
+
+ console.log('๐ถ Entering FPV mode');
+ fpvMode = true;
+
+ // Disable orbit controls
+ if (window.controls) {
+ window.controls.enabled = false;
+ }
+
+ // Keep current camera position instead of forcing to model center
+ // Just ensure the height is reasonable for walking
+ fpvControls.currentHeight = Math.max(window.camera.position.y, window.eyeHeight);
+ fpvControls.targetHeight = fpvControls.currentHeight;
+ window.camera.position.y = fpvControls.currentHeight;
+
+ // Calculate rotation based on current camera orientation
+ const euler = new THREE.Euler().setFromQuaternion(window.camera.quaternion, 'YXZ');
+ fpvControls.rotation.x = euler.x;
+ fpvControls.rotation.y = euler.y;
+
+ // Reset movement state
+ fpvControls.velocity.set(0, 0, 0);
+ fpvControls.verticalVelocity = 0;
+ fpvControls.isJumping = false;
+ fpvControls.isCrouching = false;
+
+ // Show crosshair
+ const crosshair = document.getElementById('fpv-crosshair');
+ if (crosshair) {
+ crosshair.style.display = 'block';
+ }
+
+ // Update button text
+ const fpvBtn = document.getElementById('fpv-btn');
+ if (fpvBtn) {
+ fpvBtn.textContent = 'Exit FPV Mode';
+ fpvBtn.style.background = '#FF5722';
+ }
+
+ // Request pointer lock
+ if (renderer && renderer.domElement) {
+ renderer.domElement.requestPointerLock();
+ }
+
+ console.log('FPV mode active - WASD to move, mouse to look, ESC to exit');
+}
+
+function exitFPVMode() {
+ console.log('๐ Exiting FPV mode');
+
+ // Exit pointer lock first, before changing fpvMode
+ if (document.pointerLockElement) {
+ document.exitPointerLock();
+ }
+
+ // Small delay to ensure pointer lock has been released
+ setTimeout(() => {
+ fpvMode = false;
+
+ // Re-enable orbit controls
+ if (window.controls) {
+ window.controls.enabled = true;
+ }
+
+ // Hide crosshair
+ const crosshair = document.getElementById('fpv-crosshair');
+ if (crosshair) {
+ crosshair.style.display = 'none';
+ }
+
+ // Update button text
+ const fpvBtn = document.getElementById('fpv-btn');
+ if (fpvBtn) {
+ fpvBtn.textContent = 'Enter FPV Mode';
+ fpvBtn.style.background = '#2196F3';
+ }
+
+ // Reset movement controls
+ fpvControls.moveForward = false;
+ fpvControls.moveBackward = false;
+ fpvControls.moveLeft = false;
+ fpvControls.moveRight = false;
+ fpvControls.jump = false;
+ fpvControls.crouch = false;
+ fpvControls.verticalVelocity = 0;
+ fpvControls.isJumping = false;
+ fpvControls.isCrouching = false;
+ fpvControls.currentHeight = window.eyeHeight;
+ fpvControls.targetHeight = window.eyeHeight;
+
+ console.log('Orbit controls restored');
+ }, 50);
+}
+
+export function updateFPVMovement(camera) {
+ if (!fpvMode) return;
+
+ const delta = 0.016; // Approximate 60fps
+ const moveSpeed = 100.0; // Units per second (1000x faster)
+ const jumpSpeed = 220.0; // Jump initial velocity (4x higher)
+ const gravity = 420.0; // Gravity strength (4x faster)
+ const crouchHeight = window.eyeHeight * 0.5; // Crouch height (60% of normal eye height)
+ const crouchSpeed = 100.0; // Speed of crouching transition
+
+ // Reset direction
+ fpvControls.direction.set(0, 0, 0);
+
+ // Calculate movement direction based on current rotation
+ const forward = new THREE.Vector3(0, 0, -1);
+ const right = new THREE.Vector3(1, 0, 0);
+
+ // Apply horizontal rotation to movement vectors
+ forward.applyAxisAngle(new THREE.Vector3(0, 1, 0), fpvControls.rotation.y);
+ right.applyAxisAngle(new THREE.Vector3(0, 1, 0), fpvControls.rotation.y);
+
+ // Apply movement inputs
+ if (fpvControls.moveForward) fpvControls.direction.add(forward);
+ if (fpvControls.moveBackward) fpvControls.direction.sub(forward);
+ if (fpvControls.moveLeft) fpvControls.direction.sub(right);
+ if (fpvControls.moveRight) fpvControls.direction.add(right);
+
+ // Normalize and apply speed
+ if (fpvControls.direction.length() > 0) {
+ fpvControls.direction.normalize();
+ fpvControls.direction.multiplyScalar(moveSpeed * delta);
+
+ // Apply movement to camera position
+ camera.position.add(fpvControls.direction);
+ }
+
+ // Handle jumping
+ if (fpvControls.jump && fpvControls.canJump && !fpvControls.isJumping) {
+ fpvControls.verticalVelocity = jumpSpeed;
+ fpvControls.isJumping = true;
+ fpvControls.canJump = false;
+ }
+
+ // Update target height based on crouching state
+ fpvControls.targetHeight = fpvControls.isCrouching ? crouchHeight : window.eyeHeight;
+
+ // Apply gravity and vertical movement
+ if (fpvControls.isJumping) {
+ fpvControls.verticalVelocity -= gravity * delta;
+ camera.position.y += fpvControls.verticalVelocity * delta;
+
+ // Check if landed back on ground
+ if (camera.position.y <= fpvControls.targetHeight) {
+ camera.position.y = fpvControls.targetHeight;
+ fpvControls.currentHeight = fpvControls.targetHeight;
+ fpvControls.verticalVelocity = 0;
+ fpvControls.isJumping = false;
+ fpvControls.canJump = true;
+ }
+ } else {
+ // Smooth crouching transition when not jumping
+ if (Math.abs(fpvControls.currentHeight - fpvControls.targetHeight) > 0.01) {
+ const direction = fpvControls.targetHeight > fpvControls.currentHeight ? 1 : -1;
+ fpvControls.currentHeight += direction * crouchSpeed * delta;
+
+ // Clamp to target
+ if (direction > 0 && fpvControls.currentHeight >= fpvControls.targetHeight) {
+ fpvControls.currentHeight = fpvControls.targetHeight;
+ } else if (direction < 0 && fpvControls.currentHeight <= fpvControls.targetHeight) {
+ fpvControls.currentHeight = fpvControls.targetHeight;
+ }
+ }
+ camera.position.y = fpvControls.currentHeight;
+ }
+
+ // Apply rotation to camera
+ camera.rotation.set(fpvControls.rotation.x, fpvControls.rotation.y, 0, 'YXZ');
+}
+
+export function isFPVMode() {
+ return fpvMode;
+}
diff --git a/src/index.js b/src/index.js
index dabd952..1fa826a 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,151 +1,51 @@
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';
+import { loadCameraConfig } from './camera-config.js';
+import { setupLighting, updateCameraLights } from './lighting.js';
+import { loadCubeModel } from './model-loader.js';
+import { setupFPVButton, setupFPVControls, updateFPVMovement, isFPVMode } from './fpv-controls.js';
+import { setupPrintHandlers, setupPrintButton } from './print-handlers.js';
+import { createScene, onWindowResize } from './scene-setup.js';
// Global variables
let scene, camera, renderer, controls;
-let cube;
let cameraConfig = null;
-// FPV Mode variables
-let fpvMode = false;
-let fpvControls = {
- moveForward: false,
- moveBackward: false,
- moveLeft: false,
- moveRight: false,
- jump: false,
- crouch: false,
- canJump: true,
- velocity: new THREE.Vector3(),
- direction: new THREE.Vector3(),
- rotation: { x: 0, y: 0 },
- verticalVelocity: 0,
- isJumping: false,
- isCrouching: false,
- currentHeight: 0,
- targetHeight: 0
-};
-let modelBounds = null;
-let groundLevel = 0;
-let ceilingLevel = 0;
-let eyeHeight = 0;
-
-// 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 scene, camera, renderer, and controls
+ const sceneComponents = createScene(cameraConfig);
+ scene = sceneComponents.scene;
+ camera = sceneComponents.camera;
+ renderer = sceneComponents.renderer;
+ controls = sceneComponents.controls;
- // 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();
+ // Make scene components globally accessible
+ window.scene = scene;
+ window.camera = camera;
+ window.renderer = renderer;
+ window.controls = controls;
// Add lighting
- setupLighting();
+ setupLighting(scene, camera);
// Load the cube model
- loadCubeModel();
+ loadCubeModel(scene, camera, controls);
// Handle window resize
- window.addEventListener('resize', onWindowResize, false);
+ window.addEventListener('resize', () => onWindowResize(camera, renderer), false);
// Handle print events
- setupPrintHandlers();
+ setupPrintHandlers(scene, renderer);
// Setup manual print button
- setupPrintButton();
+ setupPrintButton(scene, renderer);
// Setup FPV button
- setupFPVButton();
+ setupFPVButton(renderer);
// Setup FPV controls
setupFPVControls();
@@ -154,1002 +54,26 @@ async function init() {
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 setupFPVButton() {
- const fpvBtn = document.getElementById('fpv-btn');
- if (!fpvBtn) {
- console.warn('FPV button not found');
- return;
- }
-
- fpvBtn.addEventListener('click', () => {
- if (!fpvMode) {
- enterFPVMode();
- } else {
- exitFPVMode();
- }
- });
-
- console.log('๐ถ FPV 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());
-
- // Store model bounds for FPV mode
- modelBounds = { boundingBox, center, size };
- groundLevel = boundingBox.min.y;
- ceilingLevel = boundingBox.max.y;
- eyeHeight = groundLevel + (ceilingLevel - groundLevel) * 0.5;
-
- console.log('FPV bounds calculated:');
- console.log('Ground level:', groundLevel);
- console.log('Ceiling level:', ceilingLevel);
- console.log('Eye height (50%):', eyeHeight);
-
- // 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 setupFPVControls() {
- // Keyboard event handlers
- document.addEventListener('keydown', onKeyDown);
- document.addEventListener('keyup', onKeyUp);
-
- // Mouse event handlers for FPV mode
- document.addEventListener('mousemove', onMouseMove);
- document.addEventListener('click', onMouseClick);
-
- console.log('FPV controls setup complete. Press F to enter FPV mode');
-}
-
-function onKeyDown(event) {
- switch (event.code) {
- case 'KeyF':
- event.preventDefault();
- event.stopPropagation();
- if (!fpvMode) {
- enterFPVMode();
- } else {
- exitFPVMode();
- }
- return false;
- case 'Escape':
- if (fpvMode) {
- event.preventDefault();
- event.stopPropagation();
- exitFPVMode();
- return false;
- }
- break;
- case 'KeyW':
- if (fpvMode) fpvControls.moveForward = true;
- break;
- case 'KeyA':
- if (fpvMode) fpvControls.moveLeft = true;
- break;
- case 'KeyS':
- if (fpvMode) fpvControls.moveBackward = true;
- break;
- case 'KeyD':
- if (fpvMode) fpvControls.moveRight = true;
- break;
- case 'Space':
- if (fpvMode) {
- event.preventDefault();
- fpvControls.jump = true;
- }
- break;
- case 'ShiftLeft':
- case 'ShiftRight':
- if (fpvMode) {
- event.preventDefault();
- fpvControls.crouch = true;
- fpvControls.isCrouching = true;
- }
- break;
- }
-}
-
-function onKeyUp(event) {
- switch (event.code) {
- case 'KeyW':
- if (fpvMode) fpvControls.moveForward = false;
- break;
- case 'KeyA':
- if (fpvMode) fpvControls.moveLeft = false;
- break;
- case 'KeyS':
- if (fpvMode) fpvControls.moveBackward = false;
- break;
- case 'KeyD':
- if (fpvMode) fpvControls.moveRight = false;
- break;
- case 'Space':
- if (fpvMode) fpvControls.jump = false;
- break;
- case 'ShiftLeft':
- case 'ShiftRight':
- if (fpvMode) {
- fpvControls.crouch = false;
- fpvControls.isCrouching = false;
- }
- break;
- }
-}
-
-function onMouseMove(event) {
- if (!fpvMode) return;
-
- const movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0;
- const movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0;
-
- fpvControls.rotation.y -= movementX * 0.002; // Horizontal rotation (yaw)
- fpvControls.rotation.x -= movementY * 0.002; // Vertical rotation (pitch)
-
- // Limit vertical look angle
- fpvControls.rotation.x = Math.max(-Math.PI/2, Math.min(Math.PI/2, fpvControls.rotation.x));
-}
-
-function onMouseClick(event) {
- if (fpvMode) {
- // Request pointer lock for mouse control
- renderer.domElement.requestPointerLock();
- }
-}
-
-function enterFPVMode() {
- if (!modelBounds) {
- console.warn('Model not loaded yet, cannot enter FPV mode');
- return;
- }
-
- console.log('๐ถ Entering FPV mode');
- fpvMode = true;
-
- // Disable orbit controls
- controls.enabled = false;
-
- // Keep current camera position instead of forcing to model center
- // Just ensure the height is reasonable for walking
- fpvControls.currentHeight = Math.max(camera.position.y, eyeHeight);
- fpvControls.targetHeight = fpvControls.currentHeight;
- camera.position.y = fpvControls.currentHeight;
-
- // Calculate rotation based on current camera orientation
- const euler = new THREE.Euler().setFromQuaternion(camera.quaternion, 'YXZ');
- fpvControls.rotation.x = euler.x;
- fpvControls.rotation.y = euler.y;
-
- // Reset movement state
- fpvControls.velocity.set(0, 0, 0);
- fpvControls.verticalVelocity = 0;
- fpvControls.isJumping = false;
- fpvControls.isCrouching = false;
-
- // Show crosshair
- const crosshair = document.getElementById('fpv-crosshair');
- if (crosshair) {
- crosshair.style.display = 'block';
- }
-
- // Update button text
- const fpvBtn = document.getElementById('fpv-btn');
- if (fpvBtn) {
- fpvBtn.textContent = 'Exit FPV Mode';
- fpvBtn.style.background = '#FF5722';
- }
-
- // Request pointer lock
- renderer.domElement.requestPointerLock();
-
- console.log('FPV mode active - WASD to move, mouse to look, ESC to exit');
-}
-
-function exitFPVMode() {
- console.log('๐ Exiting FPV mode');
-
- // Exit pointer lock first, before changing fpvMode
- if (document.pointerLockElement) {
- document.exitPointerLock();
- }
-
- // Small delay to ensure pointer lock has been released
- setTimeout(() => {
- fpvMode = false;
-
- // Re-enable orbit controls
- controls.enabled = true;
-
- // Hide crosshair
- const crosshair = document.getElementById('fpv-crosshair');
- if (crosshair) {
- crosshair.style.display = 'none';
- }
-
- // Update button text
- const fpvBtn = document.getElementById('fpv-btn');
- if (fpvBtn) {
- fpvBtn.textContent = 'Enter FPV Mode';
- fpvBtn.style.background = '#2196F3';
- }
-
- // Reset movement controls
- fpvControls.moveForward = false;
- fpvControls.moveBackward = false;
- fpvControls.moveLeft = false;
- fpvControls.moveRight = false;
- fpvControls.jump = false;
- fpvControls.crouch = false;
- fpvControls.verticalVelocity = 0;
- fpvControls.isJumping = false;
- fpvControls.isCrouching = false;
- fpvControls.currentHeight = eyeHeight;
- fpvControls.targetHeight = eyeHeight;
-
- console.log('Orbit controls restored');
- }, 50);
-}
-
-function updateFPVMovement() {
- if (!fpvMode) return;
-
- const delta = 0.016; // Approximate 60fps
- const moveSpeed = 100.0; // Units per second (1000x faster)
- const jumpSpeed = 220.0; // Jump initial velocity (4x higher)
- const gravity = 420.0; // Gravity strength (4x faster)
- const crouchHeight = eyeHeight * 0.5; // Crouch height (60% of normal eye height)
- const crouchSpeed = 100.0; // Speed of crouching transition
-
- // Reset direction
- fpvControls.direction.set(0, 0, 0);
-
- // Calculate movement direction based on current rotation
- const forward = new THREE.Vector3(0, 0, -1);
- const right = new THREE.Vector3(1, 0, 0);
-
- // Apply horizontal rotation to movement vectors
- forward.applyAxisAngle(new THREE.Vector3(0, 1, 0), fpvControls.rotation.y);
- right.applyAxisAngle(new THREE.Vector3(0, 1, 0), fpvControls.rotation.y);
-
- // Apply movement inputs
- if (fpvControls.moveForward) fpvControls.direction.add(forward);
- if (fpvControls.moveBackward) fpvControls.direction.sub(forward);
- if (fpvControls.moveLeft) fpvControls.direction.sub(right);
- if (fpvControls.moveRight) fpvControls.direction.add(right);
-
- // Normalize and apply speed
- if (fpvControls.direction.length() > 0) {
- fpvControls.direction.normalize();
- fpvControls.direction.multiplyScalar(moveSpeed * delta);
-
- // Apply movement to camera position
- camera.position.add(fpvControls.direction);
- }
-
- // Handle jumping
- if (fpvControls.jump && fpvControls.canJump && !fpvControls.isJumping) {
- fpvControls.verticalVelocity = jumpSpeed;
- fpvControls.isJumping = true;
- fpvControls.canJump = false;
- }
-
- // Update target height based on crouching state
- fpvControls.targetHeight = fpvControls.isCrouching ? crouchHeight : eyeHeight;
-
- // Apply gravity and vertical movement
- if (fpvControls.isJumping) {
- fpvControls.verticalVelocity -= gravity * delta;
- camera.position.y += fpvControls.verticalVelocity * delta;
-
- // Check if landed back on ground
- if (camera.position.y <= fpvControls.targetHeight) {
- camera.position.y = fpvControls.targetHeight;
- fpvControls.currentHeight = fpvControls.targetHeight;
- fpvControls.verticalVelocity = 0;
- fpvControls.isJumping = false;
- fpvControls.canJump = true;
- }
- } else {
- // Smooth crouching transition when not jumping
- if (Math.abs(fpvControls.currentHeight - fpvControls.targetHeight) > 0.01) {
- const direction = fpvControls.targetHeight > fpvControls.currentHeight ? 1 : -1;
- fpvControls.currentHeight += direction * crouchSpeed * delta;
-
- // Clamp to target
- if (direction > 0 && fpvControls.currentHeight >= fpvControls.targetHeight) {
- fpvControls.currentHeight = fpvControls.targetHeight;
- } else if (direction < 0 && fpvControls.currentHeight <= fpvControls.targetHeight) {
- fpvControls.currentHeight = fpvControls.targetHeight;
- }
- }
- camera.position.y = fpvControls.currentHeight;
- }
-
- // Apply rotation to camera
- camera.rotation.set(fpvControls.rotation.x, fpvControls.rotation.y, 0, 'YXZ');
-}
-
function animate() {
requestAnimationFrame(animate);
// Update FPV movement if in FPV mode
- updateFPVMovement();
+ updateFPVMovement(camera);
// Update controls (only if not in FPV mode)
- if (!fpvMode) {
+ if (!isFPVMode()) {
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);
- }
- }
+ updateCameraLights(camera, controls);
// 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();
-});
+});
\ No newline at end of file
diff --git a/src/lighting.js b/src/lighting.js
new file mode 100644
index 0000000..570d949
--- /dev/null
+++ b/src/lighting.js
@@ -0,0 +1,131 @@
+import * as THREE from 'three';
+
+export function setupLighting(scene, camera) {
+ // 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();
+}
+
+export 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;
+}
+
+export function updateCameraLights(camera, controls) {
+ // 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);
+ }
+ }
+}
+
+export function adjustLightsForModel(modelBounds) {
+ const { center, size } = modelBounds;
+ const maxDimension = Math.max(size.x, size.y, size.z);
+
+ // 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;
+ }
+}
diff --git a/src/model-loader.js b/src/model-loader.js
new file mode 100644
index 0000000..3623f04
--- /dev/null
+++ b/src/model-loader.js
@@ -0,0 +1,316 @@
+import * as THREE from 'three';
+import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
+import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader.js';
+import { adjustLightsForModel } from './lighting.js';
+
+export function loadCubeModel(scene, camera, controls, onLoadComplete) {
+ // 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
+ window.cube = object;
+ scene.add(object);
+
+ // Calculate model center and adjust controls
+ centerModelAndControls(object, camera, controls);
+
+ // Hide loading indicator
+ document.getElementById('loading').style.display = 'none';
+
+ console.log('Cube loaded successfully!');
+
+ if (onLoadComplete) {
+ onLoadComplete(object);
+ }
+ },
+ 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(scene, camera, controls, onLoadComplete);
+ });
+}
+
+export function loadObjWithoutMaterials(scene, camera, controls, onLoadComplete) {
+ 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);
+
+ window.cube = object;
+ scene.add(object);
+
+ // Calculate model center and adjust controls
+ centerModelAndControls(object, camera, controls);
+
+ document.getElementById('loading').style.display = 'none';
+ console.log('Cube loaded without materials!');
+
+ if (onLoadComplete) {
+ onLoadComplete(object);
+ }
+ },
+ 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, camera, controls) {
+ // 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());
+
+ // Store model bounds for FPV mode
+ window.modelBounds = { boundingBox, center, size };
+ window.groundLevel = boundingBox.min.y;
+ window.ceilingLevel = boundingBox.max.y;
+ window.eyeHeight = window.groundLevel + (window.ceilingLevel - window.groundLevel) * 0.5;
+
+ console.log('FPV bounds calculated:');
+ console.log('Ground level:', window.groundLevel);
+ console.log('Ceiling level:', window.ceilingLevel);
+ console.log('Eye height (50%):', window.eyeHeight);
+
+ // 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);
+
+ // Adjust lights based on model size
+ adjustLightsForModel({ center, size });
+
+ // Update controls
+ controls.update();
+
+ // Add rotation logging
+ setupRotationLogging(camera, controls);
+
+ console.log('Model centered at:', center);
+ console.log('Model size:', size);
+ console.log('Camera positioned at:', camera.position);
+}
+
+function setupRotationLogging(camera, controls) {
+ 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');
+}
diff --git a/src/print-handlers.js b/src/print-handlers.js
new file mode 100644
index 0000000..d70699c
--- /dev/null
+++ b/src/print-handlers.js
@@ -0,0 +1,235 @@
+import * as THREE from 'three';
+
+export function setupPrintHandlers(scene, renderer) {
+ 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, window.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, window.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');
+}
+
+export function setupPrintButton(scene, renderer) {
+ 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, window.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');
+}
diff --git a/src/scene-setup.js b/src/scene-setup.js
new file mode 100644
index 0000000..bcf72fb
--- /dev/null
+++ b/src/scene-setup.js
@@ -0,0 +1,97 @@
+import * as THREE from 'three';
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
+
+export function createScene(cameraConfig) {
+ // Create container
+ const container = document.getElementById('container');
+
+ // Create scene
+ const scene = new THREE.Scene();
+ scene.background = new THREE.Color(0x2c3e50);
+
+ // Create camera with extended far clipping plane for better zoom out
+ const 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
+ const 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
+ const 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(renderer);
+
+ return { scene, camera, renderer, controls };
+}
+
+export function setupCustomTouchControls(renderer) {
+ 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');
+}
+
+export function onWindowResize(camera, renderer) {
+ camera.aspect = window.innerWidth / window.innerHeight;
+ camera.updateProjectionMatrix();
+ renderer.setSize(window.innerWidth, window.innerHeight);
+}