SceneEditor / index.html
ThorAILabs's picture
add error handling for webgpu, so that you can still use the editor
a593e62 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Scene Editor with WebGPU Raytracing</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/STLLoader.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/OBJLoader.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/exporters/OBJExporter.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/exporters/STLExporter.min.js"></script>
<style>
#renderCanvas {
width: 100%;
height: 100%;
display: block;
}
.dragover {
border: 2px dashed #3b82f6 !important;
background-color: rgba(59, 130, 246, 0.1) !important;
}
.sidebar {
transition: all 0.3s ease;
}
.sidebar.collapsed {
transform: translateX(-100%);
}
.render-mode {
position: absolute;
top: 10px;
left: 10px;
background: rgba(0,0,0,0.5);
padding: 5px 10px;
border-radius: 5px;
color: white;
font-size: 12px;
}
.instruction-step {
counter-increment: step-counter;
position: relative;
padding-left: 2rem;
margin-bottom: 0.5rem;
}
.instruction-step::before {
content: counter(step-counter);
position: absolute;
left: 0;
background-color: #3b82f6;
color: white;
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.8rem;
}
.feature-disabled {
opacity: 0.5;
pointer-events: none;
cursor: not-allowed;
}
</style>
</head>
<body class="bg-gray-900 text-white h-screen flex overflow-hidden">
<!-- WebGPU Modal -->
<div id="webgpuModal" class="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 hidden">
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-2xl w-full mx-4">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold text-blue-400">
<i class="fas fa-info-circle mr-2"></i> WebGPU Setup Required
</h2>
<button id="closeWebgpuModal" class="text-gray-400 hover:text-white">
<i class="fas fa-times"></i>
</button>
</div>
<div class="bg-gray-700 p-4 rounded mb-4">
<p class="text-yellow-300 mb-2">
<i class="fas fa-exclamation-triangle mr-2"></i>
Raytracing requires WebGPU which is currently experimental in Chrome/Edge
</p>
<p class="text-sm text-gray-300">
Follow these steps to enable WebGPU in Chromium browsers (Chrome/Edge):
</p>
</div>
<div class="space-y-3 mb-6 pl-4" style="counter-reset: step-counter;">
<div class="instruction-step">
<strong>Type this in your address bar:</strong>
<code class="block bg-gray-900 p-2 rounded mt-1 text-sm">chrome://flags/#enable-unsafe-webgpu</code>
</div>
<div class="instruction-step">
<strong>Find "Unsafe WebGPU"</strong> in the list and change it from "Default" to <span class="bg-blue-600 px-2 py-1 rounded">Enabled</span>
</div>
<div class="instruction-step">
<strong>Restart your browser</strong> when prompted
</div>
<div class="instruction-step">
<strong>Reload this page</strong> after restarting
</div>
</div>
<div class="bg-gray-700 p-4 rounded">
<p class="text-sm text-gray-300">
<i class="fas fa-info-circle mr-2 text-blue-400"></i>
WebGPU is only available in Chrome 113+ and Edge 113+ on Windows/Linux/Mac.
Other browsers will fall back to traditional rasterization.
</p>
</div>
<div class="flex justify-end mt-4 space-x-2">
<button id="dontShowAgain" class="bg-gray-600 hover:bg-gray-500 px-4 py-2 rounded">
Don't Show Again
</button>
<button id="gotItButton" class="bg-blue-600 hover:bg-blue-500 px-4 py-2 rounded">
Got It!
</button>
</div>
</div>
</div>
<!-- WebGPU Error Modal -->
<div id="webgpuErrorModal" class="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 hidden">
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-2xl w-full mx-4">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold text-red-400">
<i class="fas fa-exclamation-triangle mr-2"></i> WebGPU Raytracing Unavailable
</h2>
<button id="closeWebgpuErrorModal" class="text-gray-400 hover:text-white">
<i class="fas fa-times"></i>
</button>
</div>
<div class="bg-gray-700 p-4 rounded mb-4">
<p class="text-yellow-300 mb-2">
<i class="fas fa-exclamation-circle mr-2"></i>
Failed to load WebGPU raytracing library
</p>
<p class="text-sm text-gray-300">
The application will continue with traditional rasterization rendering.
</p>
</div>
<div class="space-y-3 mb-6">
<p class="text-sm">
Possible reasons for this error:
</p>
<ul class="list-disc pl-5 space-y-1 text-sm text-gray-300">
<li>WebGPU is not enabled in your browser</li>
<li>Your browser doesn't support WebGPU</li>
<li>Network issues prevented loading the raytracing library</li>
<li>The raytracing library is temporarily unavailable</li>
</ul>
</div>
<div class="bg-gray-700 p-4 rounded">
<p class="text-sm text-gray-300">
<i class="fas fa-info-circle mr-2 text-blue-400"></i>
You can still use all other features of the 3D editor.
The raytracing option has been disabled.
</p>
</div>
<div class="flex justify-end mt-4">
<button id="continueButton" class="bg-blue-600 hover:bg-blue-500 px-4 py-2 rounded">
Continue
</button>
</div>
</div>
</div>
<!-- Sidebar -->
<div id="sidebar" class="sidebar w-64 bg-gray-800 h-full flex flex-col border-r border-gray-700">
<div class="p-4 border-b border-gray-700 flex justify-between items-center">
<h1 class="text-xl font-bold">3D Scene Editor</h1>
<button id="toggleSidebar" class="text-gray-400 hover:text-white">
<i class="fas fa-chevron-left"></i>
</button>
</div>
<div class="p-4 space-y-4 overflow-y-auto flex-grow">
<!-- Scene Controls -->
<div class="space-y-2">
<h2 class="font-semibold text-blue-400 flex items-center">
<i class="fas fa-cube mr-2"></i> Scene Controls
</h2>
<div class="flex space-x-2">
<button id="toggleRaytracing" class="bg-blue-600 hover:bg-blue-700 px-3 py-1 rounded flex items-center">
<i class="fas fa-lightbulb mr-1"></i> Toggle Raytracing
</button>
<button id="clearScene" class="bg-red-600 hover:bg-red-700 px-3 py-1 rounded flex items-center">
<i class="fas fa-trash mr-1"></i> Clear
</button>
</div>
<div id="raytraceSettings" class="bg-gray-700 p-2 rounded mt-2 hidden">
<h3 class="text-sm font-medium mb-1">Raytrace Settings</h3>
<div class="space-y-2 text-xs">
<div>
<label>Bounces: <span id="bounceValue">3</span></label>
<input type="range" id="bounceSlider" min="1" max="10" value="3" class="w-full">
</div>
<div>
<label>Samples: <span id="sampleValue">4</span></label>
<input type="range" id="sampleSlider" min="1" max="16" value="4" class="w-full">
</div>
<div class="flex items-center">
<input type="checkbox" id="denoiseCheck" class="mr-2" checked>
<label for="denoiseCheck">Denoise</label>
</div>
</div>
</div>
</div>
<!-- Lighting Controls -->
<div class="space-y-2">
<h2 class="font-semibold text-yellow-400 flex items-center">
<i class="fas fa-lightbulb mr-2"></i> Lighting
</h2>
<div class="grid grid-cols-2 gap-2">
<button id="addAmbientLight" class="bg-gray-700 hover:bg-gray-600 px-2 py-1 rounded text-sm flex items-center">
<i class="fas fa-sun mr-1"></i> Ambient
</button>
<button id="addDirectionalLight" class="bg-gray-700 hover:bg-gray-600 px-2 py-1 rounded text-sm flex items-center">
<i class="fas fa-sun mr-1"></i> Directional
</button>
<button id="addPointLight" class="bg-gray-700 hover:bg-gray-600 px-2 py-1 rounded text-sm flex items-center">
<i class="fas fa-lightbulb mr-1"></i> Point
</button>
<button id="addSpotLight" class="bg-gray-700 hover:bg-gray-600 px-2 py-1 rounded text-sm flex items-center">
<i class="fas fa-lightbulb mr-1"></i> Spot
</button>
</div>
<div id="lightControls" class="space-y-2 mt-2">
<!-- Light controls will be added here dynamically -->
</div>
</div>
<!-- Import/Export -->
<div class="space-y-2">
<h2 class="font-semibold text-green-400 flex items-center">
<i class="fas fa-file-import mr-2"></i> Import/Export
</h2>
<div class="grid grid-cols-2 gap-2">
<button id="importSTL" class="bg-gray-700 hover:bg-gray-600 px-2 py-1 rounded text-sm flex items-center">
<i class="fas fa-file-upload mr-1"></i> Import STL
</button>
<button id="importOBJ" class="bg-gray-700 hover:bg-gray-600 px-2 py-1 rounded text-sm flex items-center">
<i class="fas fa-file-upload mr-1"></i> Import OBJ
</button>
<button id="exportSTL" class="bg-gray-700 hover:bg-gray-600 px-2 py-1 rounded text-sm flex items-center">
<i class="fas fa-file-download mr-1"></i> Export STL
</button>
<button id="exportOBJ" class="bg-gray-700 hover:bg-gray-600 px-2 py-1 rounded text-sm flex items-center">
<i class="fas fa-file-download mr-1"></i> Export OBJ
</button>
</div>
<div id="dropZone" class="border-2 border-dashed border-gray-600 rounded p-4 text-center mt-2 cursor-pointer hover:border-blue-500 transition">
<i class="fas fa-cloud-upload-alt text-3xl mb-2 text-gray-400"></i>
<p class="text-sm text-gray-400">Drop 3D files here</p>
<p class="text-xs text-gray-500">(STL, OBJ)</p>
</div>
</div>
<!-- Object List -->
<div class="space-y-2">
<h2 class="font-semibold text-purple-400 flex items-center">
<i class="fas fa-shapes mr-2"></i> Scene Objects
</h2>
<div id="objectList" class="space-y-1 max-h-40 overflow-y-auto">
<!-- Objects will be listed here -->
</div>
</div>
</div>
<div class="p-4 border-t border-gray-700 text-xs text-gray-500">
<p>3D Scene Editor v1.0</p>
<p>With WebGPU Raytracing</p>
</div>
</div>
<!-- Main Content -->
<div class="flex-grow relative">
<div id="renderCanvas"></div>
<div id="renderMode" class="render-mode">Rasterization</div>
<!-- Stats Panel -->
<div id="stats" class="absolute top-2 right-2 bg-gray-800 bg-opacity-70 p-2 rounded text-xs">
<div>FPS: <span id="fps">0</span></div>
<div>Objects: <span id="objectCount">0</span></div>
<div>Vertices: <span id="vertexCount">0</span></div>
</div>
<!-- Loading Indicator -->
<div id="loadingIndicator" class="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden">
<div class="bg-gray-800 p-6 rounded-lg shadow-lg text-center">
<i class="fas fa-spinner fa-spin text-4xl text-blue-500 mb-4"></i>
<p class="text-xl">Loading...</p>
</div>
</div>
</div>
<!-- File Input (hidden) -->
<input type="file" id="fileInput" accept=".stl,.obj" class="hidden" multiple>
<script>
// Initialize Three.js scene
let scene, camera, renderer, controls;
let raytracingEnabled = false;
let raytracingRenderer = null;
let objects = [];
let lights = [];
let clock = new THREE.Clock();
let stats = {
fps: 0,
objectCount: 0,
vertexCount: 0
};
// Check if WebGPU is supported
function isWebGPUSupported() {
return navigator.gpu !== undefined;
}
// Check if this is a Chromium browser
function isChromium() {
return /Chrome|Edg/.test(navigator.userAgent) && !/Opera|OPR/.test(navigator.userAgent);
}
// Show WebGPU instructions if needed
function checkWebGPUSupport() {
// Check if user has previously dismissed the modal
if (localStorage.getItem('dontShowWebGPUModal') === 'true') {
return;
}
// Only show for Chromium browsers without WebGPU support
if (isChromium() && !isWebGPUSupported()) {
document.getElementById('webgpuModal').classList.remove('hidden');
}
}
// Disable raytracing UI when not available
function disableRaytracingUI() {
const toggleBtn = document.getElementById('toggleRaytracing');
toggleBtn.disabled = true;
toggleBtn.classList.add('feature-disabled');
toggleBtn.title = "WebGPU raytracing not available";
document.getElementById('raytraceSettings').classList.add('hidden');
// Show error modal
document.getElementById('webgpuErrorModal').classList.remove('hidden');
}
// Initialize the application
async function init() {
// Create scene
scene = new THREE.Scene();
scene.background = new THREE.Color(0x111111);
// Create camera
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(5, 5, 5);
camera.lookAt(0, 0, 0);
// Create renderer
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.getElementById('renderCanvas').appendChild(renderer.domElement);
// Try to initialize WebGPU raytracing if supported
if (isWebGPUSupported()) {
try {
// Dynamically load the WebGPU raytracing library
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/webgpu-raytracing@latest/dist/webgpu-raytracing.umd.js';
// Set timeout for library loading
const loadTimeout = setTimeout(() => {
console.warn("WebGPU raytracing library loading timed out");
disableRaytracingUI();
}, 5000);
script.onload = async () => {
clearTimeout(loadTimeout);
try {
if (typeof WebGPURaytracingRenderer !== 'undefined') {
raytracingRenderer = new WebGPURaytracingRenderer({
canvas: document.createElement('canvas'),
width: window.innerWidth,
height: window.innerHeight
});
await raytracingRenderer.init();
// Configure raytracing settings
raytracingRenderer.setBounces(3);
raytracingRenderer.setSamples(4);
raytracingRenderer.setDenoise(true);
console.log("WebGPU Raytracing initialized successfully");
} else {
console.warn("WebGPU Raytracing library not available");
disableRaytracingUI();
}
} catch (e) {
console.error("Failed to initialize WebGPU raytracing:", e);
disableRaytracingUI();
}
};
script.onerror = () => {
clearTimeout(loadTimeout);
console.warn("Failed to load WebGPU raytracing library");
disableRaytracingUI();
};
document.head.appendChild(script);
} catch (e) {
console.error("Error loading WebGPU raytracing:", e);
disableRaytracingUI();
}
} else {
console.warn("WebGPU not supported in this browser");
disableRaytracingUI();
}
// Add orbit controls
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
// Add axes helper
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
// Add grid helper
const gridHelper = new THREE.GridHelper(20, 20);
scene.add(gridHelper);
// Add default ambient light
addAmbientLight();
// Add default directional light
addDirectionalLight();
// Event listeners
setupEventListeners();
// Check WebGPU support
checkWebGPUSupport();
// Start animation loop
animate();
}
// Animation loop
function animate() {
requestAnimationFrame(animate);
// Update controls
controls.update();
// Calculate FPS
const delta = clock.getDelta();
stats.fps = Math.round(1 / delta);
// Update stats
updateStats();
// Render scene
if (raytracingEnabled && raytracingRenderer) {
try {
// Update raytracing renderer with current scene
raytracingRenderer.setCamera(camera);
raytracingRenderer.setScene(scene);
// Render with raytracing
raytracingRenderer.render();
// Display the raytraced result
renderer.domElement.style.display = 'none';
raytracingRenderer.canvas.style.display = 'block';
raytracingRenderer.canvas.style.width = '100%';
raytracingRenderer.canvas.style.height = '100%';
if (!document.getElementById('renderCanvas').contains(raytracingRenderer.canvas)) {
document.getElementById('renderCanvas').appendChild(raytracingRenderer.canvas);
}
} catch (e) {
console.error("Raytracing render error:", e);
raytracingEnabled = false;
document.getElementById('renderMode').textContent = 'Rasterization';
renderer.domElement.style.display = 'block';
if (raytracingRenderer && raytracingRenderer.canvas) {
raytracingRenderer.canvas.style.display = 'none';
}
}
} else {
// Regular rasterization rendering
renderer.render(scene, camera);
// Hide raytracing canvas if visible
if (raytracingRenderer && raytracingRenderer.canvas) {
raytracingRenderer.canvas.style.display = 'none';
}
renderer.domElement.style.display = 'block';
}
}
// Update statistics display
function updateStats() {
document.getElementById('fps').textContent = stats.fps;
document.getElementById('objectCount').textContent = objects.length;
// Calculate total vertices
let vertexCount = 0;
objects.forEach(obj => {
if (obj.geometry) {
vertexCount += obj.geometry.attributes.position.count;
}
});
document.getElementById('vertexCount').textContent = vertexCount;
}
// Setup event listeners
function setupEventListeners() {
// Window resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
if (raytracingRenderer) {
raytracingRenderer.setSize(window.innerWidth, window.innerHeight);
}
});
// Sidebar toggle
document.getElementById('toggleSidebar').addEventListener('click', () => {
document.getElementById('sidebar').classList.toggle('collapsed');
document.getElementById('toggleSidebar').innerHTML =
document.getElementById('sidebar').classList.contains('collapsed') ?
'<i class="fas fa-chevron-right"></i>' : '<i class="fas fa-chevron-left"></i>';
});
// Raytracing toggle
document.getElementById('toggleRaytracing').addEventListener('click', () => {
if (!isWebGPUSupported() || !raytracingRenderer) {
document.getElementById('webgpuModal').classList.remove('hidden');
return;
}
raytracingEnabled = !raytracingEnabled;
document.getElementById('raytraceSettings').classList.toggle('hidden', !raytracingEnabled);
document.getElementById('renderMode').textContent = raytracingEnabled ? 'Raytracing' : 'Rasterization';
if (raytracingEnabled && !raytracingRenderer) {
alert('WebGPU raytracing is not available in your browser');
raytracingEnabled = false;
document.getElementById('raytraceSettings').classList.add('hidden');
document.getElementById('renderMode').textContent = 'Rasterization';
}
});
// Raytracing settings
document.getElementById('bounceSlider').addEventListener('input', (e) => {
const value = parseInt(e.target.value);
document.getElementById('bounceValue').textContent = value;
if (raytracingRenderer) raytracingRenderer.setBounces(value);
});
document.getElementById('sampleSlider').addEventListener('input', (e) => {
const value = parseInt(e.target.value);
document.getElementById('sampleValue').textContent = value;
if (raytracingRenderer) raytracingRenderer.setSamples(value);
});
document.getElementById('denoiseCheck').addEventListener('change', (e) => {
if (raytracingRenderer) raytracingRenderer.setDenoise(e.target.checked);
});
// Clear scene
document.getElementById('clearScene').addEventListener('click', () => {
clearScene();
});
// Light buttons
document.getElementById('addAmbientLight').addEventListener('click', addAmbientLight);
document.getElementById('addDirectionalLight').addEventListener('click', addDirectionalLight);
document.getElementById('addPointLight').addEventListener('click', addPointLight);
document.getElementById('addSpotLight').addEventListener('click', addSpotLight);
// Import/Export buttons
document.getElementById('importSTL').addEventListener('click', () => {
document.getElementById('fileInput').accept = '.stl';
document.getElementById('fileInput').click();
});
document.getElementById('importOBJ').addEventListener('click', () => {
document.getElementById('fileInput').accept = '.obj';
document.getElementById('fileInput').click();
});
document.getElementById('exportSTL').addEventListener('click', exportSTL);
document.getElementById('exportOBJ').addEventListener('click', exportOBJ);
// File input handling
document.getElementById('fileInput').addEventListener('change', handleFileSelect);
// Drag and drop
const dropZone = document.getElementById('dropZone');
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('dragover');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('dragover');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('dragover');
if (e.dataTransfer.files.length > 0) {
handleFileSelect({ target: { files: e.dataTransfer.files } });
}
});
dropZone.addEventListener('click', () => {
document.getElementById('fileInput').accept = '.stl,.obj';
document.getElementById('fileInput').click();
});
// WebGPU modal controls
document.getElementById('closeWebgpuModal').addEventListener('click', () => {
document.getElementById('webgpuModal').classList.add('hidden');
});
document.getElementById('gotItButton').addEventListener('click', () => {
document.getElementById('webgpuModal').classList.add('hidden');
});
document.getElementById('dontShowAgain').addEventListener('click', () => {
localStorage.setItem('dontShowWebGPUModal', 'true');
document.getElementById('webgpuModal').classList.add('hidden');
});
// WebGPU error modal controls
document.getElementById('closeWebgpuErrorModal').addEventListener('click', () => {
document.getElementById('webgpuErrorModal').classList.add('hidden');
});
document.getElementById('continueButton').addEventListener('click', () => {
document.getElementById('webgpuErrorModal').classList.add('hidden');
});
}
// Add ambient light
function addAmbientLight(color = 0x404040, intensity = 0.5) {
const light = new THREE.AmbientLight(color, intensity);
scene.add(light);
lights.push(light);
addLightControl(light, 'Ambient Light');
}
// Add directional light
function addDirectionalLight(color = 0xffffff, intensity = 1, x = 5, y = 5, z = 5) {
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(x, y, z);
light.castShadow = true;
light.shadow.mapSize.width = 1024;
light.shadow.mapSize.height = 1024;
scene.add(light);
lights.push(light);
// Add helper
const helper = new THREE.DirectionalLightHelper(light, 1);
scene.add(helper);
addLightControl(light, 'Directional Light', helper);
}
// Add point light
function addPointLight(color = 0xffffff, intensity = 1, distance = 10, x = 0, y = 5, z = 0) {
const light = new THREE.PointLight(color, intensity, distance);
light.position.set(x, y, z);
light.castShadow = true;
scene.add(light);
lights.push(light);
// Add helper
const helper = new THREE.PointLightHelper(light, 1);
scene.add(helper);
addLightControl(light, 'Point Light', helper);
}
// Add spot light
function addSpotLight(color = 0xffffff, intensity = 1, distance = 10, angle = Math.PI / 4, penumbra = 0.1, x = 0, y = 5, z = 0) {
const light = new THREE.SpotLight(color, intensity, distance, angle, penumbra);
light.position.set(x, y, z);
light.castShadow = true;
scene.add(light);
lights.push(light);
// Add helper
const helper = new THREE.SpotLightHelper(light);
scene.add(helper);
addLightControl(light, 'Spot Light', helper);
}
// Add light control UI
function addLightControl(light, name, helper = null) {
const lightControls = document.getElementById('lightControls');
const lightId = lights.length - 1;
const controlDiv = document.createElement('div');
controlDiv.className = 'bg-gray-700 p-2 rounded';
controlDiv.id = `lightControl-${lightId}`;
controlDiv.innerHTML = `
<div class="flex justify-between items-center mb-1">
<span class="text-sm font-medium">${name}</span>
<button class="text-red-400 hover:text-red-300 text-xs" data-light-id="${lightId}">
<i class="fas fa-times"></i>
</button>
</div>
<div class="grid grid-cols-2 gap-1 text-xs">
<div>
<label>Intensity</label>
<input type="range" min="0" max="2" step="0.1" value="${light.intensity}"
class="w-full" data-light-id="${lightId}" data-property="intensity">
</div>
<div>
<label>Color</label>
<input type="color" value="#${light.color.getHexString()}"
class="w-full" data-light-id="${lightId}" data-property="color">
</div>
</div>
`;
lightControls.appendChild(controlDiv);
// Add event listeners for the controls
controlDiv.querySelectorAll('input').forEach(input => {
input.addEventListener('input', (e) => {
const id = parseInt(e.target.dataset.lightId);
const property = e.target.dataset.property;
const value = e.target.value;
if (property === 'color') {
lights[id].color.setHex(parseInt(value.substring(1), 16));
} else if (property === 'intensity') {
lights[id].intensity = parseFloat(value);
}
if (helper) helper.update();
});
});
// Add delete button event
controlDiv.querySelector('button').addEventListener('click', (e) => {
const id = parseInt(e.target.dataset.lightId || e.target.closest('button').dataset.lightId);
scene.remove(lights[id]);
if (helper) scene.remove(helper);
lights.splice(id, 1);
controlDiv.remove();
// Update remaining controls
document.querySelectorAll('[data-light-id]').forEach(el => {
const currentId = parseInt(el.dataset.lightId);
if (currentId > id) {
el.dataset.lightId = currentId - 1;
}
});
});
}
// Clear the scene
function clearScene() {
// Remove all objects
objects.forEach(obj => {
scene.remove(obj);
});
objects = [];
// Remove all lights except the first two (ambient and directional)
while (lights.length > 2) {
const light = lights.pop();
scene.remove(light);
// Remove helper if exists
light.children.forEach(child => {
if (child instanceof THREE.LightHelper) {
scene.remove(child);
}
});
}
// Clear light controls except the first two
const lightControls = document.getElementById('lightControls');
while (lightControls.children.length > 2) {
lightControls.removeChild(lightControls.lastChild);
}
// Reset the first two lights to default
if (lights.length > 0) {
lights[0].color.setHex(0x404040);
lights[0].intensity = 0.5;
}
if (lights.length > 1) {
lights[1].color.setHex(0xffffff);
lights[1].intensity = 1;
lights[1].position.set(5, 5, 5);
}
// Update UI controls
document.querySelectorAll('#lightControls input[data-property="color"]').forEach((input, index) => {
if (index === 0) input.value = '#404040';
if (index === 1) input.value = '#ffffff';
});
document.querySelectorAll('#lightControls input[data-property="intensity"]').forEach((input, index) => {
if (index === 0) input.value = 0.5;
if (index === 1) input.value = 1;
});
// Update object list
updateObjectList();
}
// Handle file selection
function handleFileSelect(event) {
const files = event.target.files;
if (!files || files.length === 0) return;
showLoading(true);
// Process each file
Array.from(files).forEach(file => {
const fileName = file.name.toLowerCase();
const reader = new FileReader();
reader.onload = function(e) {
if (fileName.endsWith('.stl')) {
loadSTL(e.target.result, file.name);
} else if (fileName.endsWith('.obj')) {
loadOBJ(e.target.result, file.name);
}
};
reader.onerror = function() {
console.error('Error reading file');
showLoading(false);
};
if (fileName.endsWith('.stl') || fileName.endsWith('.obj')) {
reader.readAsArrayBuffer(file);
} else {
alert('Unsupported file format. Please upload STL or OBJ files.');
showLoading(false);
}
});
// Reset file input
event.target.value = '';
}
// Load STL file
function loadSTL(buffer, name) {
try {
const loader = new THREE.STLLoader();
const geometry = loader.parse(buffer);
const material = new THREE.MeshStandardMaterial({
color: 0xaaaaaa,
metalness: 0.5,
roughness: 0.5
});
const mesh = new THREE.Mesh(geometry, material);
mesh.castShadow = true;
mesh.receiveShadow = true;
mesh.name = name;
// Center and scale the model
geometry.computeBoundingBox();
const boundingBox = geometry.boundingBox;
const center = new THREE.Vector3();
boundingBox.getCenter(center);
mesh.position.sub(center);
// Scale to reasonable size
const size = boundingBox.getSize(new THREE.Vector3());
const maxDim = Math.max(size.x, size.y, size.z);
const scale = 5 / maxDim;
mesh.scale.set(scale, scale, scale);
scene.add(mesh);
objects.push(mesh);
updateObjectList();
showLoading(false);
} catch (e) {
console.error('Error loading STL:', e);
alert('Error loading STL file');
showLoading(false);
}
}
// Load OBJ file
function loadOBJ(buffer, name) {
try {
const loader = new THREE.OBJLoader();
const obj = loader.parse(buffer);
// Apply material to all children
obj.traverse(child => {
if (child instanceof THREE.Mesh) {
child.material = new THREE.MeshStandardMaterial({
color: 0xaaaaaa,
metalness: 0.5,
roughness: 0.5
});
child.castShadow = true;
child.receiveShadow = true;
}
});
obj.name = name;
// Center and scale the model
const box = new THREE.Box3().setFromObject(obj);
const center = new THREE.Vector3();
box.getCenter(center);
obj.position.sub(center);
// Scale to reasonable size
const size = box.getSize(new THREE.Vector3());
const maxDim = Math.max(size.x, size.y, size.z);
const scale = 5 / maxDim;
obj.scale.set(scale, scale, scale);
scene.add(obj);
objects.push(obj);
updateObjectList();
showLoading(false);
} catch (e) {
console.error('Error loading OBJ:', e);
alert('Error loading OBJ file');
showLoading(false);
}
}
// Export STL
function exportSTL() {
if (objects.length === 0) {
alert('No objects to export');
return;
}
const exporter = new THREE.STLExporter();
let stlString = '';
// Export all objects
objects.forEach(obj => {
stlString += exporter.parse(obj);
});
// Create download link
const blob = new Blob([stlString], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'scene.stl';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
// Export OBJ
function exportOBJ() {
if (objects.length === 0) {
alert('No objects to export');
return;
}
const exporter = new THREE.OBJExporter();
let objString = '';
// Export all objects
objects.forEach(obj => {
objString += exporter.parse(obj);
});
// Create download link
const blob = new Blob([objString], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'scene.obj';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
// Update object list in UI
function updateObjectList() {
const objectList = document.getElementById('objectList');
objectList.innerHTML = '';
if (objects.length === 0) {
objectList.innerHTML = '<p class="text-gray-500 text-sm">No objects in scene</p>';
return;
}
objects.forEach((obj, index) => {
const item = document.createElement('div');
item.className = 'flex justify-between items-center bg-gray-700 px-2 py-1 rounded text-sm';
item.innerHTML = `
<span class="truncate">${obj.name || 'Unnamed Object'}</span>
<div class="flex space-x-1">
<button class="text-blue-400 hover:text-blue-300" data-action="focus" data-index="${index}">
<i class="fas fa-crosshairs"></i>
</button>
<button class="text-red-400 hover:text-red-300" data-action="remove" data-index="${index}">
<i class="fas fa-trash"></i>
</button>
</div>
`;
objectList.appendChild(item);
});
// Add event listeners
document.querySelectorAll('[data-action="focus"]').forEach(btn => {
btn.addEventListener('click', (e) => {
const index = parseInt(e.target.dataset.index || e.target.closest('button').dataset.index);
focusObject(objects[index]);
});
});
document.querySelectorAll('[data-action="remove"]').forEach(btn => {
btn.addEventListener('click', (e) => {
const index = parseInt(e.target.dataset.index || e.target.closest('button').dataset.index);
removeObject(index);
});
});
}
// Focus on object
function focusObject(obj) {
const box = new THREE.Box3().setFromObject(obj);
const size = box.getSize(new THREE.Vector3());
const center = box.getCenter(new THREE.Vector3());
// Position camera to view the entire object
const maxDim = Math.max(size.x, size.y, size.z);
const cameraDistance = maxDim * 2;
camera.position.copy(center);
camera.position.z += cameraDistance;
camera.lookAt(center);
controls.target.copy(center);
controls.update();
}
// Remove object
function removeObject(index) {
if (index >= 0 && index < objects.length) {
scene.remove(objects[index]);
objects.splice(index, 1);
updateObjectList();
}
}
// Show/hide loading indicator
function showLoading(show) {
document.getElementById('loadingIndicator').classList.toggle('hidden', !show);
}
// Initialize the app when DOM is loaded
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>