Refactor index.js to modularize functionality and improve readability. Removed unused variables and functions, integrated scene setup, lighting, and model loading into dedicated functions. Enhanced FPV controls and camera updates for better performance and maintainability.
This commit is contained in:
24
src/camera-config.js
Normal file
24
src/camera-config.js
Normal file
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
328
src/fpv-controls.js
Normal file
328
src/fpv-controls.js
Normal file
@@ -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;
|
||||
}
|
||||
1128
src/index.js
1128
src/index.js
File diff suppressed because it is too large
Load Diff
131
src/lighting.js
Normal file
131
src/lighting.js
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
316
src/model-loader.js
Normal file
316
src/model-loader.js
Normal file
@@ -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 =
|
||||
'<div style="color: #ff6b6b;">Error loading model. Please check console for details.</div>';
|
||||
});
|
||||
},
|
||||
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 =
|
||||
'<div style="color: #ff6b6b;">Failed to load model. Check if cube.obj exists.</div>';
|
||||
});
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
235
src/print-handlers.js
Normal file
235
src/print-handlers.js
Normal file
@@ -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');
|
||||
}
|
||||
97
src/scene-setup.js
Normal file
97
src/scene-setup.js
Normal file
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user