Compare commits
30 Commits
e967f52f1b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
327627f194 | ||
|
|
bdcbdbf3c8 | ||
|
|
5c16236165 | ||
|
|
6d34067fc8 | ||
|
|
6e563e86e9 | ||
|
|
0c24a245fd | ||
|
|
4830201d7a | ||
|
|
607ada8bd5 | ||
|
|
d689fee589 | ||
|
|
e66e26764e | ||
|
|
60871b3963 | ||
|
|
7e9ceb7ce2 | ||
|
|
565218feb7 | ||
|
|
e5032d78a5 | ||
|
|
4f64d94fd8 | ||
|
|
a7c674c892 | ||
|
|
4f9f696ad4 | ||
|
|
f915bd29c0 | ||
|
|
51ad0743cf | ||
|
|
44736e8061 | ||
|
|
70b335c39a | ||
|
|
d0ea77f4b1 | ||
|
|
cdb8ad4624 | ||
|
|
ffaf51db16 | ||
|
|
292bb69ea8 | ||
|
|
3250291c67 | ||
|
|
7f64c3ca74 | ||
|
|
8028c8436e | ||
|
|
f548139c1a | ||
|
|
ba8eb581c9 |
@@ -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."
|
||||
|
||||
73
cube.mtl
@@ -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
|
||||
|
||||
|
||||
BIN
cube/Carpet_Plush_Charcoal.jpg
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
cube/Cladding_Stucco_White.jpg
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
cube/Mirror_02.jpg
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
cube/Plywood_01_1K.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
cube/Quartz_Light_Grey.jpg
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
cube/Tile_Interior_01_1K.png
Normal file
|
After Width: | Height: | Size: 331 KiB |
BIN
cube/WP.jpg
Normal file
|
After Width: | Height: | Size: 322 KiB |
BIN
cube/Wood_Floor_01_1K.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
cube/Wood_Floor_Dark.jpg
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
cube/Wood_OSB.jpg
Normal file
|
After Width: | Height: | Size: 11 KiB |
24
src/camera-config.js
Normal file
@@ -0,0 +1,24 @@
|
||||
// Load camera configuration from config file
|
||||
export async function loadCameraConfig() {
|
||||
try {
|
||||
const response = await fetch('./camera-config.json');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const cameraConfig = await response.json();
|
||||
console.log('✅ Camera config loaded successfully:', cameraConfig);
|
||||
return cameraConfig;
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Could not load camera-config.json, using original defaults.');
|
||||
console.log('📝 To use custom camera settings, ensure camera-config.json is accessible from your web server.');
|
||||
|
||||
// Return original simple defaults
|
||||
return {
|
||||
camera: {
|
||||
position: { x: 20, y: 20, z: 20 },
|
||||
target: { x: 0, y: 0, z: 0 },
|
||||
distance: 34.64
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
404
src/fpv-controls.js
Normal 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;
|
||||
}
|
||||
@@ -21,6 +21,17 @@
|
||||
<p>👆 Single finger drag: Rotate around model</p>
|
||||
<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>
|
||||
|
||||
542
src/index.js
@@ -1,535 +1,79 @@
|
||||
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(scene, renderer);
|
||||
|
||||
// Setup manual print button
|
||||
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 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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -51,6 +51,12 @@ body {
|
||||
margin: 3px 0 !important;
|
||||
border: 0.5px solid #555;
|
||||
}
|
||||
|
||||
#print-btn {
|
||||
font-size: 9px !important;
|
||||
padding: 4px 6px !important;
|
||||
border-radius: 3px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Very small screens (phones in portrait) */
|
||||
@@ -92,3 +98,71 @@ body {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Print styles - make background transparent and model visible */
|
||||
@media print {
|
||||
* {
|
||||
background: white !important;
|
||||
background-image: none !important;
|
||||
background-color: white !important;
|
||||
}
|
||||
|
||||
body {
|
||||
background: white !important;
|
||||
background-image: none !important;
|
||||
background-color: white !important;
|
||||
}
|
||||
|
||||
html {
|
||||
background: white !important;
|
||||
background-image: none !important;
|
||||
background-color: white !important;
|
||||
}
|
||||
|
||||
#container {
|
||||
background: white !important;
|
||||
background-image: none !important;
|
||||
background-color: white !important;
|
||||
}
|
||||
|
||||
/* Hide info box when printing to focus on the model */
|
||||
#info {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Hide print button in print output */
|
||||
#print-btn {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Hide loading indicator when printing */
|
||||
#loading {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Ensure the canvas is visible and takes full page */
|
||||
canvas {
|
||||
background: white !important;
|
||||
background-image: none !important;
|
||||
background-color: white !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
max-width: none !important;
|
||||
max-height: none !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* Remove any WebGL background */
|
||||
canvas[data-engine*="three"], canvas[data-engine*="webgl"] {
|
||||
background: white !important;
|
||||
background-color: white !important;
|
||||
}
|
||||
|
||||
/* Ensure proper print sizing */
|
||||
@page {
|
||||
margin: 0.5in;
|
||||
size: auto;
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
|
||||