Compare commits

...

28 Commits

Author SHA1 Message Date
sebseb7
327627f194 Adjust lighting and emissive properties for improved scene illumination: reduced intensities for ambient, directional, point, spotlight, and hemisphere lights in lighting.js; modified emissive brightness in model-loader.js for better interior visibility; increased tone mapping exposure in scene-setup.js for a brighter appearance. 2025-08-26 09:34:00 +02:00
seb
bdcbdbf3c8 Refine texture coordinates in cube.obj for materials Tile_Interior_01_1K and BackColor, improving visual representation and consistency across multiple vertices and faces. 2025-08-26 09:03:27 +02:00
seb
5c16236165 Update cube.mtl and cube.obj: rename material Quartz_Light_Grey to M03_Pewter_Shine, adjust color values for materials, and refine vertex positions and texture coordinates in cube.obj for improved visual representation and organization. 2025-08-26 08:56:35 +02:00
sebseb7
6d34067fc8 Update camera configuration and FPV controls: adjusted camera position, target, and distance values for improved viewing experience; modified jump speed, gravity, crouch height, and crouch speed in FPV controls for enhanced movement dynamics; refined eye height calculation in model loader for better alignment with model dimensions. 2025-08-26 02:49:37 +02:00
seb
6e563e86e9 Update cube.mtl and cube.obj: rename materials, adjust color values, and refine texture mappings for improved visual representation. Added new materials WP, Wood_Floor_01_1K, Plywood_01_1K, and Tile_Interior_01_1K, while updating associated vertex and face definitions for better organization and clarity. 2025-08-26 02:49:17 +02:00
seb
0c24a245fd Update cube.obj: refine vertex positions, adjust texture coordinates, and enhance face definitions for materials including FrontColor and Cladding_Stucco_White, improving overall visual representation and organization. 2025-08-26 01:56:40 +02:00
seb
4830201d7a Update cube.obj: refine vertex positions, adjust texture coordinates, and enhance face definitions for materials including Carpet_Plush_Charcoal and BackColor, improving overall visual representation and organization. 2025-08-26 01:40:33 +02:00
seb
607ada8bd5 Update cube.obj: refine vertex positions, adjust texture coordinates, and enhance face definitions for materials including Cladding_Stucco_White and FrontColor, improving overall visual representation and organization. 2025-08-26 01:18:04 +02:00
seb
d689fee589 Update cube.obj: reintroduce and adjust materials including BackColor and Wood_Floor_Dark, refine vertex positions, and enhance face definitions for improved visual representation and organization. 2025-08-26 01:02:20 +02:00
sebseb7
e66e26764e Implement fly mode in FPV controls: added vertical movement capabilities, toggle functionality with 'G' key, and updated control logic for jumping and crouching. Enhanced user instructions in index.html. Improved lighting setup in lighting.js with additional directional light and adjustments to existing lights for better illumination. Refined material handling in model-loader.js to ensure proper rendering of glass and non-glass materials. 2025-08-26 00:56:42 +02:00
seb
60871b3963 Update cube.obj: reintroduce and adjust materials including BackColor and Wood_Floor_Dark, refine vertex positions, and enhance face definitions for improved visual representation and organization. 2025-08-26 00:56:15 +02:00
seb
7e9ceb7ce2 Refactor cube.mtl and cube.obj: reintroduce previously removed materials BackColor and Carpet_Plush_Charcoal, adjust vertex positions, and update face definitions for improved visual representation and organization. 2025-08-26 00:29:27 +02:00
seb
565218feb7 Remove unused materials from cube.mtl and update mesh group names in cube.obj for better organization and clarity. Adjusted vertex positions and face definitions for improved visual representation of models. 2025-08-25 23:30:36 +02:00
seb
e5032d78a5 Update cube.mtl and cube.obj: added new materials ForegroundColor and Helen_Phone, adjusted vertex positions for Helen_Boots, and refined face definitions for improved visual accuracy. 2025-08-25 23:26:52 +02:00
seb
4f64d94fd8 Update material definitions and geometry in cube files: renamed materials, adjusted color values, and modified vertex and face data for improved visual representation, including the addition of new materials such as Helen_Boots and Mirror_02. 2025-08-25 23:08:22 +02:00
seb
a7c674c892 Enhance geometry and material properties in cube.obj: updated vertex positions, texture coordinates, and face definitions for various materials, including adjustments to FrontColor and Cladding_Stucco_White for improved visual fidelity. 2025-08-25 22:57:21 +02:00
seb
4f9f696ad4 Refine material properties and geometry in cube.obj: updated vertex normals, texture coordinates, and face definitions for various materials, including adjustments to Cladding_Stucco_White and Carpet_Plush_Charcoal for enhanced visual accuracy. 2025-08-25 22:44:56 +02:00
sebseb7
f915bd29c0 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. 2025-08-25 22:32:39 +02:00
seb
51ad0743cf Update material definitions and geometry in cube files: renamed materials, adjusted color values, and modified vertex and face data for improved visual representation. 2025-08-25 22:32:14 +02:00
sebseb7
44736e8061 u 2025-08-25 10:30:11 +02:00
sebseb7
70b335c39a u 2025-08-25 10:13:35 +02:00
sebseb7
d0ea77f4b1 u 2025-08-25 10:04:04 +02:00
sebseb7
cdb8ad4624 move 2025-08-25 09:57:31 +02:00
seb
ffaf51db16 u 2025-08-25 09:45:29 +02:00
seb
292bb69ea8 u 2025-08-25 09:43:24 +02:00
sebseb7
3250291c67 g 2025-08-25 09:31:39 +02:00
seb
7f64c3ca74 u 2025-08-25 09:31:25 +02:00
seb
8028c8436e u 2025-08-25 09:19:41 +02:00
21 changed files with 19780 additions and 7333 deletions

View File

@@ -1,20 +1,20 @@
{
"camera": {
"position": {
"x": 514,
"y": 228,
"z": -403
"x": 248.7,
"y": 93.5,
"z": 139.16
},
"target": {
"x": 50,
"y": 85,
"z": -140
"x": 156.1,
"y": 63.43,
"z": -186.15
},
"distance": 552,
"distance": 339.56,
"spherical": {
"radius": 552,
"theta": 2.09,
"phi": 1.31
"radius": 339.56,
"theta": 0.28,
"phi": 1.48
}
},
"description": "Default camera configuration for 3D model viewer. Position values can be copied from console logs and pasted here to set new defaults."

View File

@@ -2,43 +2,80 @@
## Alias OBJ Material File
# Exported from SketchUp, (c) 2000-2012 Trimble Navigation Limited
newmtl Helen_Boots
Ka 0.000000 0.000000 0.000000
Kd 0.184314 0.184314 0.184314
Ks 0.330000 0.330000 0.330000
newmtl Color_M01
Ka 0.000000 0.000000 0.000000
Kd 0.886275 0.886275 0.886275
Ks 0.330000 0.330000 0.330000
newmtl Color_M02
Ka 0.000000 0.000000 0.000000
Kd 0.776471 0.776471 0.776471
Ks 0.330000 0.330000 0.330000
newmtl Mirror_02
Ka 0.000000 0.000000 0.000000
Kd 0.898039 0.898039 0.898039
Ks 0.330000 0.330000 0.330000
map_Kd cube/Mirror_02.jpg
newmtl Helen_Phone
Ka 0.000000 0.000000 0.000000
Kd 0.247059 0.247059 0.247059
Ks 0.330000 0.330000 0.330000
newmtl FrontColor
Ka 0.000000 0.000000 0.000000
Kd 1.000000 1.000000 1.000000
Ks 0.330000 0.330000 0.330000
newmtl WP
Ka 0.000000 0.000000 0.000000
Kd 0.752941 0.749020 0.737255
Ks 0.330000 0.330000 0.330000
map_Kd cube/WP.jpg
newmtl M03_Pewter_Shine
Ka 0.000000 0.000000 0.000000
Kd 0.658824 0.658824 0.658824
Ks 0.330000 0.330000 0.330000
newmtl Wood_Floor_Dark
Ka 0.000000 0.000000 0.000000
Kd 0.600000 0.368627 0.164706
Ks 0.330000 0.330000 0.330000
map_Kd cube/Wood_Floor_Dark.jpg
newmtl BackColor
Ka 0.000000 0.000000 0.000000
Kd 0.643137 0.698039 0.733333
Ks 0.330000 0.330000 0.330000
newmtl Carpet_01_1K
newmtl Wood_Floor_01_1K
Ka 0.000000 0.000000 0.000000
Kd 0.768627 0.764706 0.756863
Kd 0.572549 0.435294 0.298039
Ks 0.330000 0.330000 0.330000
map_Kd cube/Carpet_01_1K.png
map_Kd cube/Wood_Floor_01_1K.png
newmtl Marble_01_1K
newmtl Plywood_01_1K
Ka 0.000000 0.000000 0.000000
Kd 0.901961 0.866667 0.827451
Kd 0.490196 0.376471 0.250980
Ks 0.330000 0.330000 0.330000
map_Kd cube/Marble_01_1K.png
map_Kd cube/Plywood_01_1K.png
newmtl Wood_Veneer_15_1K
newmtl Carpet_Plush_Charcoal
Ka 0.000000 0.000000 0.000000
Kd 0.403922 0.270588 0.176471
Kd 0.341176 0.341176 0.341176
Ks 0.330000 0.330000 0.330000
map_Kd cube/Wood_Veneer_15_1K.png
map_Kd cube/Carpet_Plush_Charcoal.jpg
newmtl Denim_03_1K
newmtl Tile_Interior_01_1K
Ka 0.000000 0.000000 0.000000
Kd 0.364706 0.415686 0.466667
Kd 0.749020 0.749020 0.749020
Ks 0.330000 0.330000 0.330000
map_Kd cube/Denim_03_1K.png
newmtl _
Ka 0.000000 0.000000 0.000000
Kd 0.458824 0.333333 0.117647
Ks 0.330000 0.330000 0.330000
map_Kd cube/_.png
map_Kd cube/Tile_Interior_01_1K.png

25002
cube.obj

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
cube/Mirror_02.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
cube/Plywood_01_1K.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
cube/Quartz_Light_Grey.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB

BIN
cube/WP.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

BIN
cube/Wood_Floor_01_1K.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

BIN
cube/Wood_Floor_Dark.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
cube/Wood_OSB.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

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
}
};
}
}

404
src/fpv-controls.js Normal file
View File

@@ -0,0 +1,404 @@
import * as THREE from 'three';
// FPV Mode variables
let fpvMode = false;
let flyMode = false; // New fly mode state
let fpvControls = {
moveForward: false,
moveBackward: false,
moveLeft: false,
moveRight: false,
jump: false,
crouch: false,
moveUp: false, // For fly mode
moveDown: false, // For fly mode
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 'KeyG':
if (fpvMode) {
event.preventDefault();
event.stopPropagation();
toggleFlyMode();
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();
if (flyMode) {
fpvControls.moveUp = true;
} else {
fpvControls.jump = true;
}
}
break;
case 'ShiftLeft':
case 'ShiftRight':
if (fpvMode) {
event.preventDefault();
if (flyMode) {
fpvControls.moveDown = true;
} else {
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) {
if (flyMode) {
fpvControls.moveUp = false;
} else {
fpvControls.jump = false;
}
}
break;
case 'ShiftLeft':
case 'ShiftRight':
if (fpvMode) {
if (flyMode) {
fpvControls.moveDown = false;
} else {
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, G to toggle fly mode, ESC to exit');
}
function toggleFlyMode() {
flyMode = !flyMode;
// Reset vertical states when switching modes
fpvControls.verticalVelocity = 0;
fpvControls.isJumping = false;
fpvControls.isCrouching = false;
fpvControls.moveUp = false;
fpvControls.moveDown = false;
fpvControls.jump = false;
fpvControls.crouch = false;
if (flyMode) {
console.log('🚁 Fly mode enabled - SPACE to ascend, SHIFT to descend, no gravity');
} else {
console.log('🚶 Walk mode enabled - SPACE to jump, SHIFT to crouch, with gravity');
// Set current height to maintain position when switching back to walk mode
fpvControls.currentHeight = window.camera.position.y;
fpvControls.targetHeight = fpvControls.currentHeight;
}
}
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.moveUp = false;
fpvControls.moveDown = false;
fpvControls.verticalVelocity = 0;
fpvControls.isJumping = false;
fpvControls.isCrouching = false;
fpvControls.currentHeight = window.eyeHeight;
fpvControls.targetHeight = window.eyeHeight;
// Reset fly mode
flyMode = false;
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
const jumpSpeed = 500.0; // Jump initial velocity
const gravity = 2000.0; // Gravity strength
const crouchHeight = window.eyeHeight * 0.3; // Crouch height
const crouchSpeed = 300.0; // Speed of crouching transition
const flySpeed = 80.0; // Vertical fly speed
// 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);
const up = new THREE.Vector3(0, 1, 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 horizontal 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 horizontal speed
if (fpvControls.direction.length() > 0) {
fpvControls.direction.normalize();
fpvControls.direction.multiplyScalar(moveSpeed * delta);
// Apply horizontal movement to camera position
camera.position.add(fpvControls.direction);
}
if (flyMode) {
// FLY MODE - No gravity, direct vertical control
let verticalMovement = 0;
if (fpvControls.moveUp) {
verticalMovement += flySpeed * delta;
}
if (fpvControls.moveDown) {
verticalMovement -= flySpeed * delta;
}
if (verticalMovement !== 0) {
camera.position.y += verticalMovement;
}
} else {
// WALK MODE - Gravity, jumping, crouching
// 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;
}

View File

@@ -22,8 +22,17 @@
<p>🤏 Pinch to zoom: Zoom in/out</p>
<p>✌️ Two finger drag: Pan around scene</p>
<hr style="margin: 10px 0; border: 1px solid #555;">
<p>🚶 <button id="fpv-btn" style="background: #2196F3; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-size: 12px; margin-right: 10px;">Enter FPV Mode</button> <span style="font-size: 11px; color: #aaa;">(or press F key to toggle)</span></p>
<p style="font-size: 11px; color: #aaa; margin: 5px 0;">In FPV: WASD to move, mouse to look, G to toggle fly mode, F or ESC to exit</p>
<p style="font-size: 10px; color: #888; margin: 2px 0;">Walk mode: SPACE jump, SHIFT crouch | Fly mode: SPACE ascend, SHIFT descend</p>
<button id="print-btn" style="background: #4CAF50; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">🖨️ Print</button>
</div>
<!-- FPV Crosshair -->
<div id="fpv-crosshair" style="display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); pointer-events: none; z-index: 1000;">
<div style="position: absolute; width: 20px; height: 2px; background: rgba(255, 255, 255, 0.8); top: -1px; left: -10px; border: 1px solid rgba(0, 0, 0, 0.5);"></div>
<div style="position: absolute; width: 2px; height: 20px; background: rgba(255, 255, 255, 0.8); top: -10px; left: -1px; border: 1px solid rgba(0, 0, 0, 0.5);"></div>
</div>
</div>
</body>
</html>

View File

@@ -1,774 +1,78 @@
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;
// 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(renderer);
// Setup FPV controls
setupFPVControls();
// Start render loop
animate();
}
function setupCustomTouchControls() {
const canvas = renderer.domElement;
// Add touch-friendly CSS for better interaction
canvas.style.touchAction = 'none';
canvas.style.userSelect = 'none';
canvas.style.webkitUserSelect = 'none';
canvas.style.mozUserSelect = 'none';
canvas.style.msUserSelect = 'none';
// Prevent context menu on long press
canvas.addEventListener('contextmenu', function(event) {
event.preventDefault();
}, { passive: false });
console.log('Touch controls enabled - OrbitControls handles all touch gestures natively');
}
function setupPrintHandlers() {
const originalBackgroundColor = scene.background;
const originalRendererClearColor = renderer.getClearColor(new THREE.Color());
const originalRendererClearAlpha = renderer.getClearAlpha();
let isTestMode = false;
let testTimeout = null;
// Before print - change background to white and make renderer opaque
const beforePrint = () => {
if (isTestMode) {
console.log('🚫 Print event ignored - test mode is active');
return;
}
console.log('🖨️ PRINT MODE ACTIVATED - Changing background to white');
// Force scene background to white
scene.background = new THREE.Color(0xffffff);
// Set renderer clear color to white
renderer.setClearColor(0xffffff, 1.0);
// Force canvas background to white with multiple methods
const canvas = renderer.domElement;
canvas.style.setProperty('background', 'white', 'important');
canvas.style.setProperty('background-color', 'white', 'important');
canvas.style.setProperty('background-image', 'none', 'important');
// Also set body and html backgrounds
document.body.style.setProperty('background', 'white', 'important');
document.body.style.setProperty('background-color', 'white', 'important');
document.documentElement.style.setProperty('background', 'white', 'important');
document.documentElement.style.setProperty('background-color', 'white', 'important');
// Force a render to apply changes immediately
renderer.render(scene, camera);
console.log('✅ Print background forcibly set to white');
};
// After print - restore original background
const afterPrint = () => {
if (isTestMode) {
console.log('🚫 Print restore ignored - test mode is active');
return;
}
console.log('🖨️ PRINT MODE DEACTIVATED - Restoring original background');
// Restore original settings
scene.background = originalBackgroundColor;
renderer.setClearColor(originalRendererClearColor, originalRendererClearAlpha);
// Remove forced styles
const canvas = renderer.domElement;
canvas.style.removeProperty('background');
canvas.style.removeProperty('background-color');
canvas.style.removeProperty('background-image');
document.body.style.removeProperty('background');
document.body.style.removeProperty('background-color');
document.documentElement.style.removeProperty('background');
document.documentElement.style.removeProperty('background-color');
console.log('✅ Original background restored');
};
// Apply white background (used by both test and print modes)
const applyWhiteBackground = () => {
// Force scene background to white
scene.background = new THREE.Color(0xffffff);
// Set renderer clear color to white
renderer.setClearColor(0xffffff, 1.0);
// Force canvas background to white with multiple methods
const canvas = renderer.domElement;
canvas.style.setProperty('background', 'white', 'important');
canvas.style.setProperty('background-color', 'white', 'important');
canvas.style.setProperty('background-image', 'none', 'important');
// Also set body and html backgrounds
document.body.style.setProperty('background', 'white', 'important');
document.body.style.setProperty('background-color', 'white', 'important');
document.documentElement.style.setProperty('background', 'white', 'important');
document.documentElement.style.setProperty('background-color', 'white', 'important');
// Force a render to apply changes immediately
renderer.render(scene, camera);
};
// Restore original background (used by both test and print modes)
const restoreBackground = () => {
// Restore original settings
scene.background = originalBackgroundColor;
renderer.setClearColor(originalRendererClearColor, originalRendererClearAlpha);
// Remove forced styles
const canvas = renderer.domElement;
canvas.style.removeProperty('background');
canvas.style.removeProperty('background-color');
canvas.style.removeProperty('background-image');
document.body.style.removeProperty('background');
document.body.style.removeProperty('background-color');
document.documentElement.style.removeProperty('background');
document.documentElement.style.removeProperty('background-color');
};
// Test the print handler immediately (for debugging)
window.testPrintMode = () => {
console.log('🧪 Testing print mode (print events disabled during test)...');
isTestMode = true;
if (testTimeout) {
clearTimeout(testTimeout);
}
applyWhiteBackground();
console.log('✅ Test mode background set to white');
testTimeout = setTimeout(() => {
console.log('🧪 Test complete, restoring...');
restoreBackground();
isTestMode = false;
testTimeout = null;
console.log('✅ Test mode complete, print events re-enabled');
}, 6000);
};
// Cancel test mode early
window.cancelTestMode = () => {
if (testTimeout) {
clearTimeout(testTimeout);
testTimeout = null;
}
if (isTestMode) {
restoreBackground();
isTestMode = false;
console.log('🧪 Test mode cancelled, print events re-enabled');
}
};
// Listen for print events
if (window.matchMedia) {
const mediaQueryList = window.matchMedia('print');
mediaQueryList.addEventListener('change', (mql) => {
console.log('📱 Media query change detected:', mql.matches ? 'PRINT' : 'SCREEN');
if (mql.matches) {
beforePrint();
} else {
afterPrint();
}
});
}
// Fallback for older browsers
window.addEventListener('beforeprint', () => {
console.log('🖨️ beforeprint event fired');
beforePrint();
});
window.addEventListener('afterprint', () => {
console.log('🖨️ afterprint event fired');
afterPrint();
});
console.log('🔧 Print event handlers set up.');
console.log(' Use window.testPrintMode() to test for 6 seconds');
console.log(' Use window.cancelTestMode() to end test early');
}
function setupPrintButton() {
const printBtn = document.getElementById('print-btn');
if (!printBtn) {
console.warn('Print button not found');
return;
}
const originalBackgroundColor = scene.background;
const originalRendererClearColor = renderer.getClearColor(new THREE.Color());
const originalRendererClearAlpha = renderer.getClearAlpha();
printBtn.addEventListener('click', () => {
console.log('🖨️ Manual print button clicked - forcing white background');
// Force white background immediately
scene.background = new THREE.Color(0xffffff);
renderer.setClearColor(0xffffff, 1.0);
// Force canvas and page backgrounds to white
const canvas = renderer.domElement;
canvas.style.setProperty('background', 'white', 'important');
canvas.style.setProperty('background-color', 'white', 'important');
canvas.style.setProperty('background-image', 'none', 'important');
document.body.style.setProperty('background', 'white', 'important');
document.body.style.setProperty('background-color', 'white', 'important');
document.documentElement.style.setProperty('background', 'white', 'important');
document.documentElement.style.setProperty('background-color', 'white', 'important');
// Force a render to apply changes
renderer.render(scene, camera);
console.log('✅ Background forced to white, opening print dialog...');
// Small delay to ensure changes are applied, then open print dialog
setTimeout(() => {
window.print();
// Restore background after a delay to account for print dialog
setTimeout(() => {
console.log('🔄 Restoring original background...');
scene.background = originalBackgroundColor;
renderer.setClearColor(originalRendererClearColor, originalRendererClearAlpha);
canvas.style.removeProperty('background');
canvas.style.removeProperty('background-color');
canvas.style.removeProperty('background-image');
document.body.style.removeProperty('background');
document.body.style.removeProperty('background-color');
document.documentElement.style.removeProperty('background');
document.documentElement.style.removeProperty('background-color');
console.log('✅ Original background restored');
}, 1000); // Wait 1 second after print dialog opens
}, 100); // Small delay to apply changes
});
console.log('🖨️ Manual print button set up');
}
function setupLighting() {
// Ambient light for overall illumination - increased intensity
const ambientLight = new THREE.AmbientLight(0x404040, 1.2);
scene.add(ambientLight);
// Directional light (like sunlight) - increased intensity
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
directionalLight.position.set(10, 10, 5);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
scene.add(directionalLight);
// Point light for additional illumination - increased intensity
const pointLight = new THREE.PointLight(0xffffff, 0.8, 100);
pointLight.position.set(-10, 10, 10);
scene.add(pointLight);
// 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);
// 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 =
'<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();
});
}
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 =
'<div style="color: #ff6b6b;">Failed to load model. Check if cube.obj exists.</div>';
});
}
function centerModelAndControls(object) {
// Calculate the bounding box of the model
const boundingBox = new THREE.Box3().setFromObject(object);
// Get the center point of the bounding box
const center = boundingBox.getCenter(new THREE.Vector3());
// Get the size of the bounding box
const size = boundingBox.getSize(new THREE.Vector3());
// Set the orbit controls target to the model's center
controls.target.copy(center);
// Calculate optimal camera distance based on model size
const maxDimension = Math.max(size.x, size.y, size.z);
const fov = camera.fov * (Math.PI / 180);
const distance = Math.abs(maxDimension / Math.sin(fov / 2)) * 1.2; // 1.2 for some padding
// Position camera at optimal distance from model center
const direction = camera.position.clone().sub(center).normalize();
camera.position.copy(center).add(direction.multiplyScalar(distance));
// Update camera to look at the model center
camera.lookAt(center);
// Reposition interior lights relative to model center
if (window.interiorLight) {
window.interiorLight.position.copy(center);
// Adjust light range based on model size
window.interiorLight.distance = maxDimension * 2;
}
if (window.interiorLight2) {
window.interiorLight2.position.copy(center).add(new THREE.Vector3(
size.x * 0.3,
size.y * 0.3,
-size.z * 0.3
));
window.interiorLight2.distance = maxDimension * 1.5;
}
// Update controls
controls.update();
// Add rotation logging
setupRotationLogging();
console.log('Model centered at:', center);
console.log('Model size:', size);
console.log('Camera positioned at:', camera.position);
}
function setupRotationLogging() {
let debounceTimer = null;
// Log rotation and zoom changes with 1-second debounce
controls.addEventListener('change', function() {
// Clear existing timer
if (debounceTimer) {
clearTimeout(debounceTimer);
}
// Set new timer for 1 second delay
debounceTimer = setTimeout(() => {
const distance = camera.position.distanceTo(controls.target);
const position = camera.position.clone();
const target = controls.target.clone();
// Calculate spherical coordinates for rotation
const spherical = new THREE.Spherical().setFromVector3(
position.clone().sub(target)
);
console.log('=== CAMERA STATE ===');
console.log('Position:', {
x: Math.round(position.x * 100) / 100,
y: Math.round(position.y * 100) / 100,
z: Math.round(position.z * 100) / 100
});
console.log('Target:', {
x: Math.round(target.x * 100) / 100,
y: Math.round(target.y * 100) / 100,
z: Math.round(target.z * 100) / 100
});
console.log('Distance (Zoom):', Math.round(distance * 100) / 100);
console.log('Spherical Rotation:');
console.log(' - Radius:', Math.round(spherical.radius * 100) / 100);
console.log(' - Theta (horizontal):', Math.round(spherical.theta * 100) / 100, 'radians');
console.log(' - Phi (vertical):', Math.round(spherical.phi * 100) / 100, 'radians');
console.log('==================');
debounceTimer = null;
}, 1000);
});
console.log('Rotation logging enabled - camera state will be logged 1 second after movement stops');
}
function animate() {
requestAnimationFrame(animate);
// Update controls
controls.update();
// Update FPV movement if in FPV mode
updateFPVMovement(camera);
// Update controls (only if not in FPV mode)
if (!isFPVMode()) {
controls.update();
}
// Update camera lights to follow camera position
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();

158
src/lighting.js Normal file
View File

@@ -0,0 +1,158 @@
import * as THREE from 'three';
export function setupLighting(scene, camera) {
// Ambient light for overall illumination - reduced intensity
const ambientLight = new THREE.AmbientLight(0x404040, 0.4);
scene.add(ambientLight);
// Directional light (like sunlight) - reduced intensity
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.6);
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);
// Enhanced camera point light - follows the camera to illuminate from viewing angle
window.cameraLight = new THREE.PointLight(0xffffff, 0.8, 300);
window.cameraLight.position.copy(camera.position);
scene.add(window.cameraLight);
// Camera spotlight for focused illumination (like a headlamp)
window.cameraSpotlight = new THREE.SpotLight(0xffffff, 0.5, 150, Math.PI / 6, 0.2);
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);
// Additional camera directional light (like a flashlight)
window.cameraDirectionalLight = new THREE.DirectionalLight(0xffffff, 0.3);
window.cameraDirectionalLight.position.copy(camera.position);
window.cameraDirectionalLight.target.position.set(0, 0, -1);
scene.add(window.cameraDirectionalLight);
scene.add(window.cameraDirectionalLight.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 - reduced intensity
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.4);
scene.add(hemiLight);
console.log('✨ Enhanced camera lights added - headlamp-style illumination 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);
// Calculate where the camera is looking
const lookDirection = new THREE.Vector3(0, 0, -1);
lookDirection.applyQuaternion(camera.quaternion);
// Set spotlight target based on camera direction
const targetPosition = camera.position.clone().add(lookDirection.multiplyScalar(10));
window.cameraSpotlight.target.position.copy(targetPosition);
}
if (window.cameraDirectionalLight) {
window.cameraDirectionalLight.position.copy(camera.position);
// Calculate where the camera is looking for directional light
const lookDirection = new THREE.Vector3(0, 0, -1);
lookDirection.applyQuaternion(camera.quaternion);
// Set directional light target based on camera direction
const targetPosition = camera.position.clone().add(lookDirection.multiplyScalar(5));
window.cameraDirectionalLight.target.position.copy(targetPosition);
}
}
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 based on model size
if (window.cameraLight) {
window.cameraLight.distance = maxDimension * 4; // Adjust range based on model size
}
if (window.cameraSpotlight) {
window.cameraSpotlight.distance = maxDimension * 3;
}
if (window.cameraDirectionalLight) {
// Directional lights don't have distance, but we can adjust intensity
window.cameraDirectionalLight.intensity = 1.0;
}
}

317
src/model-loader.js Normal file
View File

@@ -0,0 +1,317 @@
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 with proper transparency
Object.keys(materials.materials).forEach(function(key) {
const material = materials.materials[key];
// Only set DoubleSide for glass materials, others use FrontSide
material.side = key.toLowerCase().includes('glass') ? THREE.DoubleSide : THREE.FrontSide;
// 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 subtle emissive for interior visibility on solid materials
if (material.emissive) {
material.emissive.setHex(0x111111); // Reduced emissive brightness
} else {
material.emissive = new THREE.Color(0x111111);
}
}
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 - fix texture appearing on both sides
child.material.side = THREE.FrontSide;
child.material.transparent = false;
child.material.opacity = 1.0;
// Ensure proper lighting
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.FrontSide, // Fix texture appearing on both sides
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.55;
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.8; // Higher exposure for brighter gamma-corrected appearance
// 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);
}