STL-Editor / index.html
namelessai's picture
Update index.html
6cc0f49 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>3D STL & OBJ Editor v.0.0.5</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/loaders/STLLoader.min.js"></script>
<!-- Added OBJLoader script -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/loaders/OBJLoader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/exporters/STLExporter.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" />
<style>
/* Keep the render canvas full size */
#renderCanvas {
width: 100%;
height: 100%;
display: block;
}
.transform-controls {
transition: all 0.3s ease;
}
.transform-controls:hover {
transform: translateY(-2px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.sidebar {
transition: transform 0.3s ease;
}
.sidebar.collapsed {
transform: translateX(-90%);
}
.model-list-item {
transition: all 0.2s ease;
}
.model-list-item:hover {
background-color: rgba(59, 130, 246, 0.1);
}
.tab-button {
transition: all 0.2s ease;
}
.tab-button.active {
border-bottom: 2px solid #3b82f6;
color: #3b82f6;
}
.color-picker {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
width: 30px;
height: 30px;
border: none;
cursor: pointer;
border-radius: 50%;
}
.color-picker::-webkit-color-swatch {
border-radius: 50%;
border: 2px solid #fff;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
}
.export-options {
display: none;
position: absolute;
right: 10px;
top: 40px;
background: white;
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
z-index: 100;
padding: 8px;
min-width: 200px;
}
.export-options.show {
display: block;
}
.export-option {
padding: 8px 12px;
cursor: pointer;
border-radius: 4px;
}
.export-option:hover {
background-color: #f3f4f6;
}
/* New CSS for the transform handle */
#transformHandle {
width: 100%;
height: 6px;
background-color: #ccc;
cursor: ns-resize;
margin-bottom: 4px;
}
/* Make sure the transform controls have a default height and hidden overflow */
#transformControls {
height: 40px;
overflow: hidden;
}
</style>
</head>
<body class="bg-gray-100 h-screen flex flex-col">
<!-- Header -->
<header class="bg-white shadow-sm py-3 px-4 flex items-center justify-between">
<div class="flex items-center space-x-2">
<i class="fas fa-cube text-blue-500 text-2xl"></i>
<h1 class="text-xl font-bold text-gray-800">3D STL & OBJ Editor v.0.0.5 (Broken, fix is being worked on.)</h1>
</div>
<div class="flex space-x-4">
<button id="toggleSidebar" class="p-2 rounded-full hover:bg-gray-200 transition">
<i class="fas fa-bars text-gray-600"></i>
</button>
</div>
</header>
<div class="flex flex-1 overflow-hidden">
<!-- Sidebar -->
<div id="sidebar" class="sidebar w-64 bg-white shadow-md flex flex-col transition-all duration-300">
<div class="p-4 border-b">
<h2 class="font-semibold text-gray-700">Project</h2>
<div class="mt-2 flex space-x-2">
<button id="loadModelBtn" class="flex-1 bg-blue-500 hover:bg-blue-600 text-white py-2 px-3 rounded text-sm flex items-center justify-center">
<i class="fas fa-folder-open mr-2"></i> Load Model
</button>
<!-- Updated accept attribute to support both .stl and .obj -->
<input type="file" id="fileInput" accept=".stl,.obj" class="hidden" />
<div class="relative">
<button id="exportModelBtn" class="flex-1 bg-gray-200 hover:bg-gray-300 text-gray-700 py-2 px-3 rounded text-sm flex items-center justify-center">
<i class="fas fa-download mr-2"></i> Export
</button>
<div id="exportOptions" class="export-options">
<div class="export-option" id="exportSelectedBtn">
<i class="fas fa-cube mr-2"></i> Export Selected
</div>
<div class="export-option" id="exportAllBtn">
<i class="fas fa-cubes mr-2"></i> Export All
</div>
<div class="border-t my-1"></div>
<div class="export-option" id="exportSceneBtn">
<i class="fas fa-scene mr-2"></i> Export Scene
</div>
</div>
</div>
</div>
</div>
<div class="flex border-b">
<button id="modelsTab" class="tab-button flex-1 py-2 text-sm font-medium active">Models</button>
<button id="settingsTab" class="tab-button flex-1 py-2 text-sm font-medium">Settings</button>
</div>
<div id="modelsPanel" class="flex-1 overflow-y-auto p-4">
<div class="mb-4">
<h3 class="text-sm font-medium text-gray-700 mb-2">Loaded Models</h3>
<div id="modelList" class="space-y-2">
<!-- Models will be added here dynamically -->
</div>
</div>
</div>
<div id="settingsPanel" class="hidden flex-1 overflow-y-auto p-4">
<div class="mb-4">
<h3 class="text-sm font-medium text-gray-700 mb-2">Scene Settings</h3>
<div class="space-y-3">
<div>
<label class="block text-xs text-gray-500 mb-1">Background Color</label>
<input type="color" id="bgColorPicker" class="color-picker" value="#f3f4f6" />
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Grid Visibility</label>
<div class="flex items-center">
<input type="checkbox" id="gridToggle" class="mr-2" checked />
<span class="text-sm">Show Grid</span>
</div>
</div>
</div>
</div>
<div class="mb-4">
<h3 class="text-sm font-medium text-gray-700 mb-2">Camera Controls</h3>
<div class="space-y-3">
<div>
<label class="block text-xs text-gray-500 mb-1">Rotation Speed</label>
<input type="range" id="rotationSpeed" min="0.1" max="2" step="0.1" value="1" class="w-full" />
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Zoom Speed</label>
<input type="range" id="zoomSpeed" min="0.1" max="2" step="0.1" value="1" class="w-full" />
</div>
</div>
</div>
</div>
</div>
<!-- Main Content -->
<div class="flex-1 flex flex-col">
<!-- Toolbar -->
<div class="bg-white border-b p-2 flex items-center justify-between">
<div class="flex space-x-2">
<button id="selectTool" class="p-2 rounded hover:bg-gray-200 text-blue-500">
<i class="fas fa-mouse-pointer"></i>
</button>
<button id="moveTool" class="p-2 rounded hover:bg-gray-200">
<i class="fas fa-arrows-alt"></i>
</button>
<button id="rotateTool" class="p-2 rounded hover:bg-gray-200">
<i class="fas fa-sync-alt"></i>
</button>
<button id="scaleTool" class="p-2 rounded hover:bg-gray-200">
<i class="fas fa-expand-alt"></i>
</button>
</div>
<div class="flex space-x-2">
<button id="resetViewBtn" class="p-2 rounded hover:bg-gray-200">
<i class="fas fa-home"></i> Reset View
</button>
</div>
</div>
<!-- 3D Viewport -->
<div id="viewportContainer" class="flex-1 relative">
<div id="renderCanvas"></div>
<div id="loadingOverlay" class="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden">
<div class="bg-white p-6 rounded-lg shadow-lg flex flex-col items-center">
<div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500 mb-4"></div>
<p class="text-gray-700">Loading model...</p>
</div>
</div>
</div>
<!-- Transform Controls with draggable handle -->
<div id="transformControls" class="bg-white border-t p-4 transform-controls hidden">
<!-- Draggable handle -->
<div id="transformHandle"></div>
<h3 class="text-sm font-medium text-gray-700 mb-3">Transform</h3>
<div class="grid grid-cols-3 gap-4">
<div>
<label class="block text-xs text-gray-500 mb-1">Position X</label>
<input type="number" id="posX" class="w-full p-2 border rounded text-sm" step="0.1" />
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Position Y</label>
<input type="number" id="posY" class="w-full p-2 border rounded text-sm" step="0.1" />
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Position Z</label>
<input type="number" id="posZ" class="w-full p-2 border rounded text-sm" step="0.1" />
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Rotation X</label>
<input type="number" id="rotX" class="w-full p-2 border rounded text-sm" step="1" />
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Rotation Y</label>
<input type="number" id="rotY" class="w-full p-2 border rounded text-sm" step="1" />
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Rotation Z</label>
<input type="number" id="rotZ" class="w-full p-2 border rounded text-sm" step="1" />
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Scale X</label>
<input type="number" id="scaleX" class="w-full p-2 border rounded text-sm" value="1" step="0.1" />
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Scale Y</label>
<input type="number" id="scaleY" class="w-full p-2 border rounded text-sm" value="1" step="0.1" />
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Scale Z</label>
<input type="number" id="scaleZ" class="w-full p-2 border rounded text-sm" value="1" step="0.1" />
</div>
</div>
<div class="mt-3 flex justify-between">
<div>
<label class="block text-xs text-gray-500 mb-1">Model Color</label>
<input type="color" id="modelColorPicker" class="color-picker" value="#3b82f6" />
</div>
<button id="deleteModelBtn" class="bg-red-500 hover:bg-red-600 text-white py-1 px-3 rounded text-sm">
<i class="fas fa-trash mr-1"></i> Delete
</button>
</div>
</div>
<!-- Status Bar -->
<div class="bg-gray-800 text-gray-300 text-xs p-1 px-3 flex justify-between">
<div>
<span id="cameraPosition">Camera: (0, 0, 0)</span>
</div>
<div>
<span id="fpsCounter">FPS: 0</span>
</div>
<div>
<span id="selectedModelInfo">No model selected</span>
</div>
</div>
</div>
</div>
<script>
// Main application
document.addEventListener('DOMContentLoaded', function() {
// UI Elements
const toggleSidebarBtn = document.getElementById('toggleSidebar');
const sidebar = document.getElementById('sidebar');
const loadModelBtn = document.getElementById('loadModelBtn');
const fileInput = document.getElementById('fileInput');
const exportModelBtn = document.getElementById('exportModelBtn');
const exportOptions = document.getElementById('exportOptions');
const exportSelectedBtn = document.getElementById('exportSelectedBtn');
const exportAllBtn = document.getElementById('exportAllBtn');
const exportSceneBtn = document.getElementById('exportSceneBtn');
const modelsTab = document.getElementById('modelsTab');
const settingsTab = document.getElementById('settingsTab');
const modelsPanel = document.getElementById('modelsPanel');
const settingsPanel = document.getElementById('settingsPanel');
const modelList = document.getElementById('modelList');
const transformControls = document.getElementById('transformControls');
const transformHandle = document.getElementById('transformHandle'); // New handle element
const selectTool = document.getElementById('selectTool');
const moveTool = document.getElementById('moveTool');
const rotateTool = document.getElementById('rotateTool');
const scaleTool = document.getElementById('scaleTool');
const resetViewBtn = document.getElementById('resetViewBtn');
const loadingOverlay = document.getElementById('loadingOverlay');
const bgColorPicker = document.getElementById('bgColorPicker');
const gridToggle = document.getElementById('gridToggle');
const rotationSpeed = document.getElementById('rotationSpeed');
const zoomSpeed = document.getElementById('zoomSpeed');
const cameraPosition = document.getElementById('cameraPosition');
const fpsCounter = document.getElementById('fpsCounter');
const selectedModelInfo = document.getElementById('selectedModelInfo');
// Transform controls elements
const posX = document.getElementById('posX');
const posY = document.getElementById('posY');
const posZ = document.getElementById('posZ');
const rotX = document.getElementById('rotX');
const rotY = document.getElementById('rotY');
const rotZ = document.getElementById('rotZ');
const scaleX = document.getElementById('scaleX');
const scaleY = document.getElementById('scaleY');
const scaleZ = document.getElementById('scaleZ');
const modelColorPicker = document.getElementById('modelColorPicker');
const deleteModelBtn = document.getElementById('deleteModelBtn');
// Three.js variables
let scene, camera, renderer, controls, gridHelper;
let stlLoader = new THREE.STLLoader();
let stlExporter = new THREE.STLExporter();
let selectedModel = null;
let models = [];
let lastTime = 0;
let frameCount = 0;
let fps = 0;
// Get the viewport container element
const viewportContainer = document.getElementById('viewportContainer');
// Initialize the 3D scene
function initScene() {
// Create scene
scene = new THREE.Scene();
scene.background = new THREE.Color(0xf3f4f6);
// Create camera
camera = new THREE.PerspectiveCamera(75, viewportContainer.clientWidth / viewportContainer.clientHeight, 0.1, 1000);
camera.position.set(0, 0, 50);
// Create renderer
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(viewportContainer.clientWidth, viewportContainer.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.getElementById('renderCanvas').appendChild(renderer.domElement);
// Add lights
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
// Add grid helper
gridHelper = new THREE.GridHelper(100, 100);
scene.add(gridHelper);
// Add orbit controls
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.rotateSpeed = 1.0;
controls.zoomSpeed = 1.0;
// Handle window resize
window.addEventListener('resize', onWindowResize);
// Start animation loop
animate();
}
// Animation loop
function animate() {
requestAnimationFrame(animate);
// Update controls
controls.update();
// Update camera position display
updateCameraPosition();
// Calculate FPS
const now = performance.now();
frameCount++;
if (now >= lastTime + 1000) {
fps = Math.round((frameCount * 1000) / (now - lastTime));
fpsCounter.textContent = `FPS: ${fps}`;
frameCount = 0;
lastTime = now;
}
renderer.render(scene, camera);
}
// Handle window resize
function onWindowResize() {
camera.aspect = viewportContainer.clientWidth / viewportContainer.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(viewportContainer.clientWidth, viewportContainer.clientHeight);
}
// Update camera position display
function updateCameraPosition() {
const pos = camera.position;
cameraPosition.textContent = `Camera: (${pos.x.toFixed(1)}, ${pos.y.toFixed(1)}, ${pos.z.toFixed(1)})`;
}
// Load model (STL or OBJ)
function loadModel(file) {
loadingOverlay.classList.remove('hidden');
const fileName = file.name.toLowerCase();
const url = URL.createObjectURL(file);
if (fileName.endsWith('.stl')) {
stlLoader.load(
url,
function (geometry) {
const material = new THREE.MeshPhongMaterial({
color: 0x3b82f6,
specular: 0x111111,
shininess: 50,
flatShading: true
});
const mesh = new THREE.Mesh(geometry, material);
// Center the model
geometry.computeBoundingBox();
const boundingBox = geometry.boundingBox;
const center = new THREE.Vector3();
boundingBox.getCenter(center);
mesh.position.sub(center);
// Scale the model to a reasonable size
const size = boundingBox.getSize(new THREE.Vector3());
const maxDim = Math.max(size.x, size.y, size.z);
const scale = 10 / maxDim;
mesh.scale.set(scale, scale, scale);
scene.add(mesh);
const modelId = Date.now();
models.push({
id: modelId,
name: file.name,
mesh: mesh,
originalColor: 0x3b82f6
});
addModelToList(modelId, file.name);
// Automatically select the loaded model so the toolbar shows up
selectModel(modelId);
loadingOverlay.classList.add('hidden');
},
function (xhr) {
console.log((xhr.loaded / xhr.total * 100) + '% loaded');
},
function (error) {
console.error('Error loading STL file:', error);
loadingOverlay.classList.add('hidden');
alert('Error loading STL file. Please try another file.');
}
);
} else if (fileName.endsWith('.obj')) {
const objLoader = new THREE.OBJLoader();
objLoader.load(
url,
function (object) {
// Apply a default material to all mesh children
object.traverse(function(child) {
if (child instanceof THREE.Mesh) {
child.material = new THREE.MeshPhongMaterial({
color: 0x3b82f6,
specular: 0x111111,
shininess: 50,
flatShading: true
});
}
});
// Center the model
const boundingBox = new THREE.Box3().setFromObject(object);
const center = new THREE.Vector3();
boundingBox.getCenter(center);
object.position.sub(center);
// Scale the model to a reasonable size
const size = boundingBox.getSize(new THREE.Vector3());
const maxDim = Math.max(size.x, size.y, size.z);
const scale = 10 / maxDim;
object.scale.set(scale, scale, scale);
scene.add(object);
const modelId = Date.now();
// Retrieve the original color (assumes at least one mesh child exists)
let originalColor = 0x3b82f6;
object.traverse(function(child) {
if (child instanceof THREE.Mesh && child.material && child.material.color) {
originalColor = child.material.color.getHex();
}
});
models.push({
id: modelId,
name: file.name,
mesh: object,
originalColor: originalColor
});
addModelToList(modelId, file.name);
// Automatically select the loaded model so the toolbar shows up
selectModel(modelId);
loadingOverlay.classList.add('hidden');
},
function (xhr) {
console.log((xhr.loaded / xhr.total * 100) + '% loaded');
},
function (error) {
console.error('Error loading OBJ file:', error);
loadingOverlay.classList.add('hidden');
alert('Error loading OBJ file. Please try another file.');
}
);
} else {
loadingOverlay.classList.add('hidden');
alert('Unsupported file type');
}
}
// Add model to the UI list
function addModelToList(id, name) {
const listItem = document.createElement('div');
listItem.className = 'model-list-item bg-gray-50 p-2 rounded cursor-pointer flex items-center justify-between';
listItem.dataset.id = id;
listItem.innerHTML = `
<div class="flex items-center">
<i class="fas fa-cube text-blue-500 mr-2"></i>
<span class="text-sm truncate" style="max-width: 160px;">${name}</span>
</div>
<div class="flex items-center space-x-1">
<button class="p-1 text-gray-500 hover:text-blue-500 model-visibility-btn">
<i class="fas fa-eye"></i>
</button>
</div>
`;
listItem.addEventListener('click', () => selectModel(id));
const visibilityBtn = listItem.querySelector('.model-visibility-btn');
visibilityBtn.addEventListener('click', (e) => {
e.stopPropagation();
toggleModelVisibility(id);
});
modelList.appendChild(listItem);
}
// Select a model
function selectModel(id) {
const model = models.find(m => m.id === id);
if (!model) return;
// Deselect previous model
if (selectedModel) {
if (selectedModel.mesh.material && selectedModel.mesh.material.color)
selectedModel.mesh.material.color.setHex(selectedModel.originalColor);
else {
selectedModel.mesh.traverse(child => {
if (child instanceof THREE.Mesh && child.material && child.material.color)
child.material.color.setHex(selectedModel.originalColor);
});
}
}
// Select new model
selectedModel = model;
if (model.mesh.material && model.mesh.material.color) {
selectedModel.originalColor = model.mesh.material.color.getHex();
model.mesh.material.color.setHex(0xff0000);
} else {
model.mesh.traverse(child => {
if (child instanceof THREE.Mesh && child.material && child.material.color) {
selectedModel.originalColor = child.material.color.getHex();
child.material.color.setHex(0xff0000);
}
});
}
// Update UI
updateTransformControls();
transformControls.classList.remove('hidden');
selectedModelInfo.textContent = `Selected: ${model.name}`;
// Highlight in model list
document.querySelectorAll('.model-list-item').forEach(item => {
item.classList.remove('bg-blue-50', 'border-blue-200', 'border');
});
document.querySelector(`.model-list-item[data-id="${id}"]`).classList.add('bg-blue-50', 'border-blue-200', 'border');
}
// Toggle model visibility
function toggleModelVisibility(id) {
const model = models.find(m => m.id === id);
if (!model) return;
model.mesh.visible = !model.mesh.visible;
const btn = document.querySelector(`.model-list-item[data-id="${id}"] .model-visibility-btn i`);
if (model.mesh.visible) {
btn.classList.remove('fa-eye-slash');
btn.classList.add('fa-eye');
} else {
btn.classList.remove('fa-eye');
btn.classList.add('fa-eye-slash');
}
}
// Update transform controls with selected model values
function updateTransformControls() {
if (!selectedModel) return;
const mesh = selectedModel.mesh;
posX.value = mesh.position.x.toFixed(2);
posY.value = mesh.position.y.toFixed(2);
posZ.value = mesh.position.z.toFixed(2);
rotX.value = THREE.MathUtils.radToDeg(mesh.rotation.x).toFixed(1);
rotY.value = THREE.MathUtils.radToDeg(mesh.rotation.y).toFixed(1);
rotZ.value = THREE.MathUtils.radToDeg(mesh.rotation.z).toFixed(1);
scaleX.value = mesh.scale.x.toFixed(2);
scaleY.value = mesh.scale.y.toFixed(2);
scaleZ.value = mesh.scale.z.toFixed(2);
let colorHex = selectedModel.originalColor;
if (mesh.material && mesh.material.color)
colorHex = mesh.material.color.getHex();
else {
mesh.traverse(child => {
if (child instanceof THREE.Mesh && child.material && child.material.color)
colorHex = child.material.color.getHex();
});
}
modelColorPicker.value = `#${colorHex.toString(16).padStart(6, '0')}`;
}
// Apply transform changes to selected model
function applyTransformChanges() {
if (!selectedModel) return;
const mesh = selectedModel.mesh;
mesh.position.set(
parseFloat(posX.value) || 0,
parseFloat(posY.value) || 0,
parseFloat(posZ.value) || 0
);
mesh.rotation.set(
THREE.MathUtils.degToRad(parseFloat(rotX.value) || 0),
THREE.MathUtils.degToRad(parseFloat(rotY.value) || 0),
THREE.MathUtils.degToRad(parseFloat(rotZ.value) || 0)
);
mesh.scale.set(
parseFloat(scaleX.value) || 1,
parseFloat(scaleY.value) || 1,
parseFloat(scaleZ.value) || 1
);
}
// Delete selected model
function deleteSelectedModel() {
if (!selectedModel) return;
scene.remove(selectedModel.mesh);
models = models.filter(m => m.id !== selectedModel.id);
const listItem = document.querySelector(`.model-list-item[data-id="${selectedModel.id}"]`);
if (listItem) listItem.remove();
selectedModel = null;
transformControls.classList.add('hidden');
selectedModelInfo.textContent = 'No model selected';
}
// Export functions
function exportSTL(mesh, name) {
const stlString = stlExporter.parse(mesh, { binary: true });
const blob = new Blob([stlString], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = name || 'model.stl';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
setTimeout(() => URL.revokeObjectURL(url), 100);
}
function exportSelectedModel() {
if (!selectedModel) {
alert('No model selected');
return;
}
const mesh = selectedModel.mesh.clone();
mesh.traverse(child => {
if (child instanceof THREE.Mesh && child.material) {
child.material = child.material.clone();
}
});
exportSTL(mesh, selectedModel.name);
}
function exportAllModels() {
if (models.length === 0) {
alert('No models to export');
return;
}
const group = new THREE.Group();
models.forEach(model => {
const mesh = model.mesh.clone();
mesh.traverse(child => {
if (child instanceof THREE.Mesh && child.material) {
child.material = child.material.clone();
}
});
group.add(mesh);
});
exportSTL(group, 'all_models.stl');
}
function exportScene() {
const group = new THREE.Group();
scene.children.forEach(child => {
if (child instanceof THREE.Mesh && child.visible) {
const mesh = child.clone();
if (mesh.material) mesh.material = mesh.material.clone();
group.add(mesh);
}
});
if (group.children.length === 0) {
alert('No visible objects to export');
return;
}
exportSTL(group, 'scene.stl');
}
// Initialize UI event listeners
function initUIListeners() {
// Toggle sidebar
toggleSidebarBtn.addEventListener('click', () => {
sidebar.classList.toggle('collapsed');
toggleSidebarBtn.innerHTML = sidebar.classList.contains('collapsed') ?
'<i class="fas fa-chevron-right"></i>' : '<i class="fas fa-bars"></i>';
});
// Load model button
loadModelBtn.addEventListener('click', () => fileInput.click());
// File input change
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
loadModel(e.target.files[0]);
fileInput.value = '';
}
});
// Export model button
exportModelBtn.addEventListener('click', (e) => {
e.stopPropagation();
exportOptions.classList.toggle('show');
});
// Export options
exportSelectedBtn.addEventListener('click', exportSelectedModel);
exportAllBtn.addEventListener('click', exportAllModels);
exportSceneBtn.addEventListener('click', exportScene);
document.addEventListener('click', (e) => {
if (!exportModelBtn.contains(e.target) && !exportOptions.contains(e.target)) {
exportOptions.classList.remove('show');
}
});
// Tab switching
modelsTab.addEventListener('click', () => {
modelsTab.classList.add('active');
settingsTab.classList.remove('active');
modelsPanel.classList.remove('hidden');
settingsPanel.classList.add('hidden');
});
settingsTab.addEventListener('click', () => {
settingsTab.classList.add('active');
modelsTab.classList.remove('active');
settingsPanel.classList.remove('hidden');
modelsPanel.classList.add('hidden');
});
// Tool buttons
selectTool.addEventListener('click', () => {
selectTool.classList.add('text-blue-500');
moveTool.classList.remove('text-blue-500');
rotateTool.classList.remove('text-blue-500');
scaleTool.classList.remove('text-blue-500');
});
moveTool.addEventListener('click', () => {
moveTool.classList.add('text-blue-500');
selectTool.classList.remove('text-blue-500');
rotateTool.classList.remove('text-blue-500');
scaleTool.classList.remove('text-blue-500');
});
rotateTool.addEventListener('click', () => {
rotateTool.classList.add('text-blue-500');
selectTool.classList.remove('text-blue-500');
moveTool.classList.remove('text-blue-500');
scaleTool.classList.remove('text-blue-500');
});
scaleTool.addEventListener('click', () => {
scaleTool.classList.add('text-blue-500');
selectTool.classList.remove('text-blue-500');
moveTool.classList.remove('text-blue-500');
rotateTool.classList.remove('text-blue-500');
});
// Reset view
resetViewBtn.addEventListener('click', () => {
camera.position.set(0, 0, 50);
camera.lookAt(0, 0, 0);
controls.reset();
});
// Transform controls
[posX, posY, posZ, rotX, rotY, rotZ, scaleX, scaleY, scaleZ].forEach(input => {
input.addEventListener('change', applyTransformChanges);
});
// Model color picker
modelColorPicker.addEventListener('input', (e) => {
if (!selectedModel) return;
const color = new THREE.Color(e.target.value);
if (selectedModel.mesh.material && selectedModel.mesh.material.color) {
selectedModel.mesh.material.color.copy(color);
} else {
selectedModel.mesh.traverse(child => {
if (child instanceof THREE.Mesh && child.material && child.material.color)
child.material.color.copy(color);
});
}
selectedModel.originalColor = color.getHex();
});
// Delete model button
deleteModelBtn.addEventListener('click', deleteSelectedModel);
// Background color picker
bgColorPicker.addEventListener('input', (e) => {
scene.background = new THREE.Color(e.target.value);
});
// Grid toggle
gridToggle.addEventListener('change', (e) => {
gridHelper.visible = e.target.checked;
});
// Rotation and zoom speed
rotationSpeed.addEventListener('input', (e) => {
controls.rotateSpeed = parseFloat(e.target.value);
});
zoomSpeed.addEventListener('input', (e) => {
controls.zoomSpeed = parseFloat(e.target.value);
});
// Draggable Transform Controls Handle
transformHandle.addEventListener('mousedown', function(e) {
let startY = e.clientY;
let initialHeight = transformControls.clientHeight;
function onMouseMove(e) {
// Calculate how much the pointer has moved upward (dragging upward increases height)
let delta = startY - e.clientY;
let newHeight = initialHeight + delta;
// Clamp the height between 40px and half of the viewport height
newHeight = Math.max(newHeight, 40);
newHeight = Math.min(newHeight, window.innerHeight * 0.5);
transformControls.style.height = newHeight + 'px';
}
function onMouseUp() {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
}
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
}
// Initialize everything
function init() {
initScene();
initUIListeners();
// Set initial tool selection
selectTool.classList.add('text-blue-500');
}
// Start the application
init();
});
</script>
</body>
</html>