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:
sebseb7
2025-08-25 22:32:36 +02:00
parent 51ad0743cf
commit f915bd29c0
7 changed files with 1158 additions and 1103 deletions

24
src/camera-config.js Normal file
View 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
View 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;
}

File diff suppressed because it is too large Load Diff

131
src/lighting.js Normal file
View 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
View 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
View 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
View 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);
}