3D-Model-Studio / index.html
Open-Source-Lab's picture
Create index.html
3d5d9da verified
<!-- optimize for mobile -->
<html>
<body>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
primary: '#5D5CDE',
secondary: '#4F4FC9',
darkbg: '#121212',
darkelem: '#1e1e1e',
darksecondary: '#252525'
}
}
}
}
</script>
<script src="https://pfst.cf2.poecdn.net/base/text/17f93eb776a5479dfb702f9a920d9a49d3e83847a7e3394d57a8b4fe26585897?pmaid=369101293"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/TransformControls.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/loaders/OBJLoader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/loaders/STLLoader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/loaders/GLTFLoader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/exporters/OBJExporter.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/exporters/STLExporter.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/exporters/GLTFExporter.js"></script>
<style>
body { overflow: hidden; }
.notification { transition: opacity 0.3s ease, transform 0.3s ease; opacity: 0; transform: translateY(20px); }
.notification.show { opacity: 1; transform: translateY(0); }
.custom-scrollbar::-webkit-scrollbar { width: 8px; }
.custom-scrollbar::-webkit-scrollbar-track { background: #1e1e1e; }
.custom-scrollbar::-webkit-scrollbar-thumb { background: #555; border-radius: 4px; }
.custom-scrollbar::-webkit-scrollbar-thumb:hover { background: #777; }
.transform-control-btn.active { border-color: #5D5CDE; background-color: rgba(93, 92, 222, 0.2); }
.dark .transform-control-btn.active { border-color: #5D5CDE; background-color: rgba(93, 92, 222, 0.3); }
.shape-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.25rem; }
.sidebar-tabs { display: flex; border-bottom: 1px solid #e5e7eb; }
.dark .sidebar-tabs { border-color: #374151; }
.sidebar-tab { padding: 0.5rem 1rem; font-size: 0.875rem; font-weight: 500; cursor: pointer; transition: all 0.2s; }
.sidebar-tab.active { color: #5D5CDE; border-bottom: 2px solid #5D5CDE; }
.dark .sidebar-tab.active { color: #6d6ef8; border-bottom: 2px solid #6d6ef8; }
.panel { display: none; }
.panel.active { display: block; }
.template-item { transition: all 0.2s; }
.template-item:hover { transform: translateY(-2px); box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); }
.dark .template-item:hover { box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.5), 0 2px 4px -1px rgba(0, 0, 0, 0.3); }
.floating-panel { backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); }
.dark .floating-panel { background-color: rgba(30, 30, 30, 0.8); }
.tool-category { margin-bottom: 1rem; }
.category-title { display: flex; align-items: center; padding: 0.5rem 0.25rem; cursor: pointer; user-select: none; }
.category-content { max-height: 0; overflow: hidden; transition: max-height 0.3s ease; }
.category-content.expanded { max-height: 1000px; }
.category-icon { transition: transform 0.3s ease; }
.expanded .category-icon { transform: rotate(90deg); }
</style>
</head>
<body class="font-sans bg-white dark:bg-darkbg text-gray-900 dark:text-gray-100 transition-colors duration-200">
<div class="flex h-screen" id="app-container">
<div class="w-64 bg-gray-100 dark:bg-darkelem shadow-md overflow-hidden z-10 flex flex-col">
<div class="sidebar-tabs">
<div class="sidebar-tab active" data-tab="tools">Tools</div>
<div class="sidebar-tab" data-tab="assets">Assets</div>
<div class="sidebar-tab" data-tab="settings">Settings</div>
</div>
<div class="panel active flex-grow overflow-y-auto custom-scrollbar p-4" id="tools-panel">
<div class="tool-category">
<div class="category-title">
<svg class="category-icon w-4 h-4 mr-1" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
</svg>
<h3 class="text-sm font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Common Shapes</h3>
</div>
<div class="category-content expanded">
<div class="shape-grid mt-2">
<div class="tool-btn p-1.5 rounded cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors" data-action="addShape" data-shape="box">Cube</div>
<div class="tool-btn p-1.5 rounded cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors" data-action="addShape" data-shape="sphere">Sphere</div>
<div class="tool-btn p-1.5 rounded cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors" data-action="addShape" data-shape="cylinder">Cylinder</div>
<div class="tool-btn p-1.5 rounded cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors" data-action="addShape" data-shape="cone">Cone</div>
<div class="tool-btn p-1.5 rounded cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors" data-action="addShape" data-shape="torus">Torus</div>
<div class="tool-btn p-1.5 rounded cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors" data-action="addShape" data-shape="plane">Plane</div>
<div class="tool-btn p-1.5 rounded cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors" data-action="addShape" data-shape="circle">Circle</div>
<div class="tool-btn p-1.5 rounded cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors" data-action="addShape" data-shape="ring">Ring</div>
<div class="tool-btn p-1.5 rounded cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors" data-action="addShape" data-shape="text">Text</div>
<div class="tool-btn p-1.5 rounded cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors" data-action="addLight">Light</div>
<div class="tool-btn p-1.5 rounded cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors" data-action="addShape" data-shape="tetrahedron">Tetrahedron</div>
<div class="tool-btn p-1.5 rounded cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors" data-action="addShape" data-shape="octahedron">Octahedron</div>
<div class="tool-btn p-1.5 rounded cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors" data-action="addShape" data-shape="dodecahedron">Dodecahedron</div>
<div class="tool-btn p-1.5 rounded cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors" data-action="addShape" data-shape="icosahedron">Icosahedron</div>
<div class="tool-btn p-1.5 rounded cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors" data-action="addShape" data-shape="torusKnot">Torus Knot</div>
<div class="tool-btn p-1.5 rounded cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors" data-action="addShape" data-shape="capsule">Capsule</div>
<div class="tool-btn p-1.5 rounded cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors" data-action="addShape" data-shape="lathe">Lathe</div>
<div class="tool-btn p-1.5 rounded cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors" data-action="addShape" data-shape="extrude">Extrude</div>
<div class="tool-btn p-1.5 rounded cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors" data-action="addShape" data-shape="tube">Tube</div>
<div class="tool-btn p-1.5 rounded cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors" data-action="addShape" data-shape="pyramid">Pyramid</div>
</div>
</div>
</div>
<div class="tool-category">
<div class="category-title">
<svg class="category-icon w-4 h-4 mr-1" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
</svg>
<h3 class="text-sm font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Customize</h3>
</div>
<div class="category-content expanded space-y-3 mt-2">
<div>
<label class="block text-sm font-medium mb-1">Color</label>
<div class="flex space-x-2">
<input type="color" id="object-color" class="w-8 h-8 rounded cursor-pointer" value="#00cc00">
<button id="apply-color" class="px-2 py-1 bg-primary hover:bg-secondary text-white rounded text-sm transition-colors">Apply</button>
</div>
</div>
<div>
<label class="block text-sm font-medium mb-1">Shape</label>
<select id="object-shape" class="w-full p-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded text-base">
<option value="box">Box</option>
<option value="sphere">Sphere</option>
<option value="cylinder">Cylinder</option>
<option value="cone">Cone</option>
<option value="torus">Torus</option>
<option value="torusKnot">Knot</option>
<option value="plane">Plane</option>
<option value="circle">Circle</option>
<option value="ring">Ring</option>
<option value="text">Fence</option>
<option value="tetrahedron">Tetrahedron</option>
<option value="octahedron">Octahedron</option>
<option value="dodecahedron">Dodecahedron</option>
<option value="icosahedron">Icosahedron</option>
<option value="torusKnot">Torus Knot</option>
<option value="capsule">Capsule</option>
<option value="lathe">Lathe</option>
<option value="extrude">Extrude</option>
<option value="tube">Tube</option>
<option value="pyramid">Pyramid</option>
</select>
</div>
<div>
<label class="block text-sm font-medium mb-1">Texture</label>
<select id="object-texture" class="w-full p-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded text-base">
<option value="none">None</option>
<option value="brick">Brick</option>
<option value="metal">Metal</option>
<option value="water">Water</option>
<option value="concrete">Concrete</option>
<option value="fabric">Fabric</option>
<option value="leather">Leather</option>
<option value="clouds">Clouds</option>
<option value="checkerboard">Checkerboard</option>
<option value="grid">Grid</option>
<option value="dots">Dots</option>
<option value="carbon">Carbon Fiber</option>
<option value="rubber">Rubber</option>
<option value="plastic">Plastic</option>
<option value="earth">Earth</option>
<option value="moon">Moon</option>
<option value="custom">Custom Texture</option>
</select>
</div>
</div>
</div>
<div class="tool-category">
<div class="category-title">
<svg class="category-icon w-4 h-4 mr-1" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
</svg>
<h3 class="text-sm font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Import/Export</h3>
</div>
<div class="category-content expanded mt-2">
<div class="space-y-2">
<div>
<label class="block text-sm font-medium mb-1">Import</label>
<input type="file" id="import-file" accept=".obj,.stl,.glb,.gltf,.json,.png,.jpg,.jpeg" class="hidden">
<div class="grid grid-cols-2 gap-1">
<button id="import-obj" class="px-2 py-1 bg-primary hover:bg-secondary text-white rounded text-sm transition-colors">.OBJ</button>
<button id="import-stl" class="px-2 py-1 bg-primary hover:bg-secondary text-white rounded text-sm transition-colors">.STL</button>
<button id="import-glb" class="px-2 py-1 bg-primary hover:bg-secondary text-white rounded text-sm transition-colors">.GLB</button>
<button id="import-textures" class="px-2 py-1 bg-primary hover:bg-secondary text-white rounded text-sm transition-colors">Textures</button>
</div>
</div>
<div>
<label class="block text-sm font-medium mb-1">Export</label>
<div class="grid grid-cols-2 gap-1">
<button id="export-obj" class="px-2 py-1 bg-primary hover:bg-secondary text-white rounded text-sm transition-colors">.OBJ</button>
<button id="export-stl" class="px-2 py-1 bg-primary hover:bg-secondary text-white rounded text-sm transition-colors">.STL</button>
<button id="export-glb" class="px-2 py-1 bg-primary hover:bg-secondary text-white rounded text-sm transition-colors">.GLB</button>
<button id="export-json" class="px-2 py-1 bg-primary hover:bg-secondary text-white rounded text-sm transition-colors">.JSON</button>
</div>
</div>
</div>
</div>
</div>
<div class="tool-category">
<div class="category-title">
<svg class="category-icon w-4 h-4 mr-1" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
</svg>
<h3 class="text-sm font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Share</h3>
</div>
<div class="category-content expanded mt-2">
<ul class="space-y-1">
<li class="p-2 rounded cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors" id="screenshot-btn">Take Screenshot</li>
<li class="p-2 rounded text-amber-500 dark:text-amber-400 cursor-not-allowed">PRO: Share to Gallery</li>
</ul>
</div>
</div>
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<button id="subscribe-btn" class="w-full py-2 bg-gradient-to-r from-purple-600 to-primary text-white rounded hover:opacity-90 transition-opacity"> Subscribe to PRO </button>
<br><br>
<button id="modelStudio-btn" class="w-full py-2 bg-gradient-to-r from-purple-600 to-primary text-white rounded hover:opacity-90 transition-opacity"> 3D-Model-Studio </button>
<br><br>
<button id="objectGenerator-btn" class="w-full py-2 bg-gradient-to-r from-purple-600 to-primary text-white rounded hover:opacity-90 transition-opacity"> 3D-Object-Generator </button>
</div>
</div>
<div class="panel flex-grow overflow-y-auto custom-scrollbar p-4" id="assets-panel">
<div class="mb-4">
<h3 class="text-sm font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">Templates</h3>
<button id="open-templates" class="w-full py-2 bg-primary hover:bg-secondary text-white rounded transition-colors"> Browse Templates </button>
</div>
<div class="mb-4">
<h3 class="text-sm font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">Recent</h3>
<div class="space-y-2">
<div class="p-2 rounded bg-white dark:bg-gray-800 shadow-sm flex items-center hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer">
<div class="w-10 h-10 bg-gray-200 dark:bg-gray-700 rounded mr-2"></div>
<div>
<p class="text-sm font-medium">My Scene 1</p>
<p class="text-xs text-gray-500 dark:text-gray-400">2 days ago</p>
</div>
</div>
<div class="p-2 rounded bg-white dark:bg-gray-800 shadow-sm flex items-center hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer">
<div class="w-10 h-10 bg-gray-200 dark:bg-gray-700 rounded mr-2"></div>
<div>
<p class="text-sm font-medium">Character Test</p>
<p class="text-xs text-gray-500 dark:text-gray-400">5 days ago</p>
</div>
</div>
</div>
</div>
<div class="mb-4">
<h3 class="text-sm font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">My Assets</h3>
<div class="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-4 text-center">
<p class="text-sm text-gray-500 dark:text-gray-400">Drag and drop files here or</p>
<button id="upload-assets-btn" class="mt-2 px-3 py-1 bg-primary hover:bg-secondary text-white rounded text-sm transition-colors">Upload Files</button>
<input type="file" id="asset-upload" class="hidden" accept=".png,.jpg,.jpeg,.json" multiple>
</div>
<div id="uploaded-assets" class="mt-3 grid grid-cols-3 gap-2"></div>
</div>
</div>
<div class="panel flex-grow overflow-y-auto custom-scrollbar p-4" id="settings-panel">
<div class="mb-4">
<h3 class="text-sm font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">Appearance</h3>
<div class="flex items-center justify-between">
<span class="text-sm">Theme</span>
<button id="theme-toggle" class="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700">
<svg class="h-5 w-5 dark:hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/>
</svg>
<svg class="h-5 w-5 hidden dark:block" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/>
</svg>
</button>
</div>
</div>
<div class="mb-4">
<h3 class="text-sm font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">Viewport</h3>
<div class="space-y-2">
<div class="flex items-center justify-between">
<span class="text-sm">Show Grid</span>
<label class="inline-flex items-center cursor-pointer">
<input type="checkbox" id="show-grid" class="sr-only peer" checked>
<div class="relative w-10 h-5 bg-gray-200 peer-focus:outline-none rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-primary"></div>
</label>
</div>
<div class="flex items-center justify-between">
<span class="text-sm">Show Axes</span>
<label class="inline-flex items-center cursor-pointer">
<input type="checkbox" id="show-axes" class="sr-only peer" checked>
<div class="relative w-10 h-5 bg-gray-200 peer-focus:outline-none rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-primary"></div>
</label>
</div>
<div class="flex items-center justify-between">
<span class="text-sm">Shadows</span>
<label class="inline-flex items-center cursor-pointer">
<input type="checkbox" id="show-shadows" class="sr-only peer" checked>
<div class="relative w-10 h-5 bg-gray-200 peer-focus:outline-none rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-primary"></div>
</label>
</div>
</div>
</div>
<div class="mb-4">
<h3 class="text-sm font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">Account</h3>
<div class="p-3 bg-white dark:bg-gray-800 rounded shadow-sm">
<p class="text-sm font-medium">Free Account</p>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Upgrade to access premium features</p>
<button id="upgrade-btn" class="mt-2 w-full py-2 px-4 bg-gradient-to-r from-purple-600 to-primary text-white rounded hover:opacity-90 transition-opacity"> Upgrade to PRO </button>
</div>
</div>
</div>
</div>
<div class="flex-1 flex flex-col relative overflow-hidden">
<div class="bg-gray-200 dark:bg-darksecondary p-3 flex justify-between items-center shadow-sm">
<div class="flex space-x-2">
<button id="new-scene" class="px-3 py-1.5 bg-primary hover:bg-secondary text-white rounded transition-colors">New</button>
<button id="save-scene" class="px-3 py-1.5 bg-gray-500 hover:bg-gray-600 text-white rounded transition-colors">Save</button>
<button id="open-scene" class="px-3 py-1.5 bg-gray-500 hover:bg-gray-600 text-white rounded transition-colors">Open</button>
<button id="delete-object" class="px-3 py-1.5 bg-red-500 hover:bg-red-600 text-white rounded transition-colors" disabled>Delete</button>
</div>
<div class="flex space-x-2">
<button id="undo-btn" class="px-3 py-1.5 bg-gray-500 hover:bg-gray-600 text-white rounded transition-colors" disabled>Undo</button>
<button id="redo-btn" class="px-3 py-1.5 bg-gray-500 hover:bg-gray-600 text-white rounded transition-colors" disabled>Redo</button>
</div>
</div>
<div class="absolute top-16 left-4 z-10 bg-white dark:bg-darkelem p-2 rounded shadow-lg flex flex-col space-y-2">
<button data-mode="translate" class="transform-control-btn w-8 h-8 flex items-center justify-center border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7l4-4m0 0l4 4m-4-4v18"/>
</svg>
</button>
<button data-mode="rotate" class="transform-control-btn w-8 h-8 flex items-center justify-center border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
</button>
<button data-mode="scale" class="transform-control-btn w-8 h-8 flex items-center justify-center border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"/>
</svg>
</button>
</div>
<div id="canvas-container" class="flex-1 bg-gray-300 dark:bg-gray-800"></div>
<div id="screenshot-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4 hidden">
<div class="bg-white dark:bg-darkelem rounded-lg shadow-xl max-w-3xl w-full max-h-[90vh] flex flex-col">
<div class="p-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
<h3 class="text-lg font-medium">Screenshot Preview</h3>
<button id="close-modal" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div class="p-4 overflow-auto flex-1">
<img id="screenshot-img" class="max-w-full max-h-[70vh] mx-auto" alt="3D Scene Screenshot">
</div>
<div class="p-4 border-t border-gray-200 dark:border-gray-700">
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">Right-click on the image and select "Save image as..." to download this screenshot.</p>
<div class="flex justify-end">
<button id="close-preview" class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">Close</button>
</div>
</div>
</div>
</div>
<div id="text-input-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4 hidden">
<div class="bg-white dark:bg-darkelem rounded-lg shadow-xl max-w-md w-full flex flex-col">
<div class="p-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
<h3 class="text-lg font-medium">Add 3D Text</h3>
<button id="close-text-modal" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div class="p-4">
<div class="mb-4">
<label for="text-value" class="block text-sm font-medium mb-1">Your Text</label>
<input type="text" id="text-value" class="w-full p-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-base" placeholder="Type your text...">
</div>
<div class="mb-4">
<label for="text-size" class="block text-sm font-medium mb-1">Size</label>
<input type="range" id="text-size" min="0.1" max="5" step="0.1" value="1" class="w-full">
<div class="flex justify-between text-xs text-gray-500">
<span>Small</span>
<span>Large</span>
</div>
</div>
<div class="mb-4">
<label for="text-depth" class="block text-sm font-medium mb-1">Depth</label>
<input type="range" id="text-depth" min="0.1" max="2" step="0.1" value="0.2" class="w-full">
<div class="flex justify-between text-xs text-gray-500">
<span>Thin</span>
<span>Thick</span>
</div>
</div>
</div>
<div class="p-4 border-t border-gray-200 dark:border-gray-700 flex justify-end space-x-2">
<button id="cancel-text" class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">Cancel</button>
<button id="add-text" class="px-4 py-2 bg-primary hover:bg-secondary text-white rounded transition-colors">Add Text</button>
</div>
</div>
</div>
<div id="texture-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4 hidden">
<div class="bg-white dark:bg-darkelem rounded-lg shadow-xl max-w-md w-full flex flex-col">
<div class="p-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
<h3 class="text-lg font-medium">Upload Texture</h3>
<button id="close-texture-modal" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div class="p-4">
<div class="mb-4">
<label for="texture-file" class="block text-sm font-medium mb-1">Select Image</label>
<input type="file" id="texture-file" accept=".png,.jpg,.jpeg" class="w-full p-2 border border-gray-300 dark:border-gray-600 rounded text-base">
</div>
<div id="texture-preview" class="hidden mb-4">
<p class="block text-sm font-medium mb-1">Preview</p>
<img id="texture-preview-img" class="max-w-full max-h-48 mx-auto border border-gray-300 dark:border-gray-600 rounded" alt="Texture Preview">
</div>
<div class="mb-4">
<label for="texture-name" class="block text-sm font-medium mb-1">Texture Name</label>
<input type="text" id="texture-name" class="w-full p-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-base" placeholder="e.g. My Custom Texture">
</div>
</div>
<div class="p-4 border-t border-gray-200 dark:border-gray-700 flex justify-end space-x-2">
<button id="cancel-texture" class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">Cancel</button>
<button id="add-texture" class="px-4 py-2 bg-primary hover:bg-secondary text-white rounded transition-colors">Add Texture</button>
</div>
</div>
</div>
<div id="templates-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4 hidden">
<div class="bg-white dark:bg-darkelem rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] flex flex-col">
<div class="p-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
<h3 class="text-lg font-medium">Choose a Template</h3>
<button id="close-templates-modal" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div class="p-4 overflow-auto flex-1">
<div class="grid grid-cols-2 md:grid-cols-3 gap-4">
<div class="template-item p-2 border border-gray-200 dark:border-gray-700 rounded-lg cursor-pointer hover:border-primary dark:hover:border-primary" data-template="empty">
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg aspect-video flex items-center justify-center">
<svg class="h-8 w-8 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
</svg>
</div>
<div class="mt-2">
<p class="text-sm font-medium">Empty Scene</p>
<p class="text-xs text-gray-500 dark:text-gray-400">Start with a blank canvas</p>
</div>
</div>
<div class="template-item p-2 border border-gray-200 dark:border-gray-700 rounded-lg cursor-pointer hover:border-primary dark:hover:border-primary" data-template="room">
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg aspect-video"></div>
<div class="mt-2">
<p class="text-sm font-medium">Room</p>
<p class="text-xs text-gray-500 dark:text-gray-400">Detailed room with furniture</p>
</div>
</div>
<div class="template-item p-2 border border-gray-200 dark:border-gray-700 rounded-lg cursor-pointer hover:border-primary dark:hover:border-primary" data-template="character">
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg aspect-video"></div>
<div class="mt-2">
<p class="text-sm font-medium">Character</p>
<p class="text-xs text-gray-500 dark:text-gray-400">Articulated character setup</p>
</div>
</div>
<div class="template-item p-2 border border-gray-200 dark:border-gray-700 rounded-lg cursor-pointer hover:border-primary dark:hover:border-primary" data-template="landscape">
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg aspect-video"></div>
<div class="mt-2">
<p class="text-sm font-medium">Landscape</p>
<p class="text-xs text-gray-500 dark:text-gray-400">Natural terrain environment</p>
</div>
</div>
<div class="template-item p-2 border border-gray-200 dark:border-gray-700 rounded-lg cursor-pointer hover:border-primary dark:hover:border-primary" data-template="vehicle">
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg aspect-video"></div>
<div class="mt-2">
<p class="text-sm font-medium">Vehicle</p>
<p class="text-xs text-gray-500 dark:text-gray-400">Detailed vehicle model</p>
</div>
</div>
<div class="template-item p-2 border border-gray-200 dark:border-gray-700 rounded-lg cursor-pointer hover:border-primary dark:hover:border-primary" data-template="abstract">
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg aspect-video"></div>
<div class="mt-2">
<p class="text-sm font-medium">Abstract</p>
<p class="text-xs text-gray-500 dark:text-gray-400">Artistic abstract composition</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="open-file-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4 hidden">
<div class="bg-white dark:bg-darkelem rounded-lg shadow-xl max-w-xl w-full flex flex-col">
<div class="p-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
<h3 class="text-lg font-medium">Open File or Text</h3>
<button id="close-open-file-modal" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div class="p-4">
<div class="mb-4">
<div class="flex space-x-2 mb-4">
<button id="tab-file" class="flex-1 py-2 border-b-2 border-primary text-primary font-medium">File</button>
<button id="tab-text" class="flex-1 py-2 border-b-2 border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300">Text</button>
</div>
<div id="file-input-container">
<label for="open-file-input" class="block text-sm font-medium mb-1">Select 3D File</label>
<input type="file" id="open-file-input" accept=".obj,.stl,.glb,.gltf,.json" class="w-full p-2 border border-gray-300 dark:border-gray-600 rounded text-base">
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">Supported formats: OBJ, STL, GLB/GLTF, JSON</p>
</div>
<div id="text-input-container" class="hidden">
<label for="open-text-format" class="block text-sm font-medium mb-1">Format</label>
<select id="open-text-format" class="w-full p-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded text-base mb-2">
<option value="json">JSON</option>
<option value="obj">OBJ</option>
</select>
<label for="open-text-content" class="block text-sm font-medium mb-1">Paste Content</label>
<textarea id="open-text-content" class="w-full h-48 p-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-base font-mono" placeholder="Paste your 3D model data here..."></textarea>
</div>
</div>
</div>
<div class="p-4 border-t border-gray-200 dark:border-gray-700 flex justify-end space-x-2">
<button id="cancel-open-file" class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">Cancel</button>
<button id="confirm-open-file" class="px-4 py-2 bg-primary hover:bg-secondary text-white rounded transition-colors">Open</button>
</div>
</div>
</div>
</div>
</div>
<div id="notification-container" class="fixed bottom-4 right-4 z-20 flex flex-col space-y-2"></div>
<script>
const state = {
isDarkMode: window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches,
selectedObject: null,
history: [],
historyIndex: -1,
maxHistorySize: 50,
snapToGrid: true,
snapIncrement: 0.25,
showGrid: true,
showAxes: true,
showShadows: true,
activeTab: 'tools',
currentMode: 'translate',
objectCounter: 0,
viewportMode: 'perspective',
lastAction: null,
customTextures: {},
assetFiles: [],
currentOpenTab: 'file',
};
if (state.isDarkMode) {
document.documentElement.classList.add('dark');
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
state.isDarkMode = event.matches;
document.documentElement.classList.toggle('dark', event.matches);
});
const canvasContainer = document.getElementById('canvas-container');
const scene = new THREE.Scene();
const renderer = new THREE.WebGLRenderer({antialias: true, preserveDrawingBuffer: true});
renderer.setSize(canvasContainer.clientWidth, canvasContainer.clientHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
canvasContainer.appendChild(renderer.domElement);
const camera = new THREE.PerspectiveCamera(75, canvasContainer.clientWidth / canvasContainer.clientHeight, 0.1, 1000);
camera.position.set(5, 5, 5);
const orbitControls = new THREE.OrbitControls(camera, renderer.domElement);
orbitControls.enableDamping = true;
orbitControls.dampingFactor = 0.25;
const transformControls = new THREE.TransformControls(camera, renderer.domElement);
transformControls.addEventListener('dragging-changed', event => {
orbitControls.enabled = !event.value;
});
transformControls.addEventListener('objectChange', () => {
if (state.selectedObject) {
state.lastAction = {
type: 'transform',
object: state.selectedObject.uuid,
position: state.selectedObject.position.clone(),
rotation: state.selectedObject.rotation.clone(),
scale: state.selectedObject.scale.clone()
};
}
});
transformControls.addEventListener('mouseUp', () => {
if (state.selectedObject && state.lastAction) {
addToHistory(state.lastAction);
state.lastAction = null;
}
});
scene.add(transformControls);
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(5, 10, 7.5);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 1024;
directionalLight.shadow.mapSize.height = 1024;
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 50;
directionalLight.shadow.camera.left = -10;
directionalLight.shadow.camera.right = 10;
directionalLight.shadow.camera.top = 10;
directionalLight.shadow.camera.bottom = -10;
scene.add(directionalLight);
const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x404040, 0.6);
scene.add(hemisphereLight);
const gridHelper = new THREE.GridHelper(20, 20, 0x888888, 0x444444);
scene.add(gridHelper);
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
const groundGeometry = new THREE.PlaneGeometry(100, 100);
const groundMaterial = new THREE.MeshPhongMaterial({color: 0x999999, side: THREE.DoubleSide});
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -0.01;
ground.receiveShadow = true;
scene.add(ground);
const sceneObjects = {};
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let selectableObjects = [];
const textureLoader = new THREE.TextureLoader();
function safeLoadTexture(url) {
return new Promise((resolve, reject) => {
const texture = textureLoader.load(
url,
(texture) => {
resolve(texture);
},
(progressEvent) => {
},
(error) => {
const canvas = document.createElement('canvas');
canvas.width = 128;
canvas.height = 128;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#555555';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#999999';
ctx.font = '14px Arial';
ctx.fillText('Error', 40, 64);
const fallbackTexture = new THREE.CanvasTexture(canvas);
fallbackTexture.sourceFile = 'error';
resolve(fallbackTexture);
}
);
});
}
function storeObjectState(object) {
if (!object) return null;
return {
uuid: object.uuid,
position: object.position.clone(),
rotation: object.rotation.clone(),
scale: object.scale.clone(),
material: object.material ? {
color: object.material.color ? object.material.color.getHex() : null,
map: object.material.map ? object.material.map.sourceFile : null
} : null,
geometry: object.geometry ? object.geometry.type : null,
userData: JSON.parse(JSON.stringify(object.userData))
};
}
function createGeometry(shape, params = {}) {
switch (shape) {
case 'box':
return new THREE.BoxGeometry(
params.width || 1,
params.height || 2,
params.depth || 1
);
case 'sphere':
return new THREE.SphereGeometry(
params.radius || 1,
params.widthSegments || 32,
params.heightSegments || 32
);
case 'cylinder':
return new THREE.CylinderGeometry(
params.radiusTop || 0.5,
params.radiusBottom || 0.5,
params.height || 2,
params.radialSegments || 32
);
case 'cone':
return new THREE.ConeGeometry(
params.radius || 1,
params.height || 2,
params.radialSegments || 32
);
case 'torus':
return new THREE.TorusGeometry(
params.radius || 1,
params.tube || 0.4,
params.radialSegments || 16,
params.tubularSegments || 32
);
case 'plane':
return new THREE.PlaneGeometry(
params.width || 2,
params.height || 2
);
case 'circle':
return new THREE.CircleGeometry(
params.radius || 1,
params.segments || 32
);
case 'ring':
return new THREE.RingGeometry(
params.innerRadius || 0.5,
params.outerRadius || 1,
params.thetaSegments || 32
);
case 'tetrahedron':
return new THREE.TetrahedronGeometry(params.radius || 1);
case 'octahedron':
return new THREE.OctahedronGeometry(params.radius || 1);
case 'dodecahedron':
return new THREE.DodecahedronGeometry(params.radius || 1);
case 'icosahedron':
return new THREE.IcosahedronGeometry(params.radius || 1);
case 'torusKnot':
return new THREE.TorusKnotGeometry(
params.radius || 1,
params.tube || 0.4,
params.tubularSegments || 64,
params.radialSegments || 8,
params.p || 2,
params.q || 3
);
case 'capsule':
return new THREE.CapsuleGeometry(
params.radius || 0.5,
params.length || 1,
params.capSegments || 10,
params.radialSegments || 16
);
case 'lathe':
const points = [];
for (let i = 0; i < 10; i++) {
const y = i * 0.2;
const x = Math.sin(i * 0.2) * 0.5 + 0.3;
points.push(new THREE.Vector2(x, y));
}
return new THREE.LatheGeometry(
params.points || points,
params.segments || 12,
params.phiStart || 0,
params.phiLength || Math.PI * 2
);
case 'extrude':
const heartShape = new THREE.Shape();
heartShape.moveTo(0, 0);
heartShape.bezierCurveTo(0, -0.5, -1, -0.5, -1, 0);
heartShape.bezierCurveTo(-1, 0.5, 0, 1, 0, 1.5);
heartShape.bezierCurveTo(0, 1, 1, 0.5, 1, 0);
heartShape.bezierCurveTo(1, -0.5, 0, -0.5, 0, 0);
return new THREE.ExtrudeGeometry(
params.shape || heartShape,
{
depth: params.depth || 0.5,
bevelEnabled: params.bevelEnabled !== undefined ? params.bevelEnabled : true,
bevelThickness: params.bevelThickness || 0.1,
bevelSize: params.bevelSize || 0.1,
bevelSegments: params.bevelSegments || 3
}
);
case 'tube':
const path = new THREE.CatmullRomCurve3([
new THREE.Vector3(-1, 0, 0),
new THREE.Vector3(-0.5, 0.5, 0.5),
new THREE.Vector3(0, 0, 1),
new THREE.Vector3(0.5, -0.5, 0.5),
new THREE.Vector3(1, 0, 0)
]);
return new THREE.TubeGeometry(
params.path || path,
params.tubularSegments || 64,
params.radius || 0.2,
params.radialSegments || 16,
params.closed || false
);
case 'pyramid':
const pyramidGeometry = new THREE.BufferGeometry();
const vertices = new Float32Array([
-0.5, 0, -0.5,
0.5, 0, -0.5,
0.5, 0, 0.5,
-0.5, 0, 0.5,
0, 1, 0
]);
const indices = [
0, 1, 2,
0, 2, 3,
0, 4, 1,
1, 4, 2,
2, 4, 3,
3, 4, 0
];
pyramidGeometry.setIndex(indices);
pyramidGeometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
pyramidGeometry.computeVertexNormals();
return pyramidGeometry;
default:
return new THREE.BoxGeometry(1, 1, 1);
}
}
async function loadTexture(textureName) {
if (state.customTextures[textureName]) {
return state.customTextures[textureName];
}
const textureMappings = {
'brick': 'https://threejs.org/examples/textures/brick_diffuse.jpg',
'wood': 'https://threejs.org/examples/textures/wood.jpg',
'metal': 'https://threejs.org/examples/textures/metal.jpg',
'stone': 'https://threejs.org/examples/textures/stone.jpg',
'grass': 'https://threejs.org/examples/textures/grass.jpg',
'water': 'https://threejs.org/examples/textures/water.jpg',
'marble': 'https://threejs.org/examples/textures/marble.jpg',
'concrete': 'https://threejs.org/examples/textures/concrete.jpg',
'fabric': 'https://threejs.org/examples/textures/fabric.jpg',
'leather': 'https://threejs.org/examples/textures/leather.jpg',
'glass': 'https://threejs.org/examples/textures/glass.jpg',
'ice': 'https://threejs.org/examples/textures/ice.jpg',
'earth': 'https://threejs.org/examples/textures/planets/earth_atmos_2048.jpg',
'moon': 'https://threejs.org/examples/textures/planets/moon_1024.jpg',
'stars': 'https://threejs.org/examples/textures/stars.jpg'
};
if (textureMappings[textureName]) {
try {
const texture = await safeLoadTexture(textureMappings[textureName]);
texture.sourceFile = textureName;
return texture;
} catch (error) {
return createProceduralTexture(textureName);
}
}
switch (textureName) {
case 'checkerboard':
case 'grid':
case 'dots':
case 'carbon':
case 'rubber':
case 'plastic':
case 'clouds':
return createProceduralTexture(textureName);
case 'none':
default:
return null;
}
}
function createProceduralTexture(type) {
const canvas = document.createElement('canvas');
canvas.width = 512;
canvas.height = 512;
const ctx = canvas.getContext('2d');
switch (type) {
case 'checkerboard':
const size = 64;
for (let y = 0; y < canvas.height; y += size) {
for (let x = 0; x < canvas.width; x += size) {
ctx.fillStyle = ((x / size) + (y / size)) % 2 === 0 ? '#fff' : '#000';
ctx.fillRect(x, y, size, size);
}
}
break;
case 'grid':
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
const gridSize = 32;
for (let y = 0; y <= canvas.height; y += gridSize) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
}
for (let x = 0; x <= canvas.width; x += gridSize) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvas.height);
ctx.stroke();
}
break;
case 'dots':
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#000';
const dotSize = 5;
const spacing = 32;
for (let y = spacing/2; y < canvas.height; y += spacing) {
for (let x = spacing/2; x < canvas.width; x += spacing) {
ctx.beginPath();
ctx.arc(x, y, dotSize, 0, Math.PI * 2);
ctx.fill();
}
}
break;
case 'carbon':
ctx.fillStyle = '#222';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#444';
const carbonSize = 10;
for (let y = 0; y < canvas.height; y += carbonSize * 2) {
for (let x = 0; x < canvas.width; x += carbonSize * 2) {
ctx.beginPath();
ctx.ellipse(x + carbonSize, y + carbonSize, carbonSize, carbonSize * 0.5, Math.PI / 4, 0, Math.PI * 2);
ctx.fill();
}
}
break;
case 'rubber':
ctx.fillStyle = '#333';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'rgba(255,255,255,0.05)';
for (let i = 0; i < 1000; i++) {
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height;
const size = Math.random() * 3 + 1;
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI * 2);
ctx.fill();
}
break;
case 'plastic':
const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
gradient.addColorStop(0, '#fff');
gradient.addColorStop(1, '#ddd');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'rgba(255,255,255,0.5)';
ctx.fillRect(0, 0, canvas.width, canvas.height / 2);
break;
case 'clouds':
ctx.fillStyle = '#87CEEB';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const cloudGradient = ctx.createRadialGradient(
canvas.width / 2, canvas.height / 2, 0,
canvas.width / 2, canvas.height / 2, canvas.width / 2
);
cloudGradient.addColorStop(0, 'rgba(255, 255, 255, 0.8)');
cloudGradient.addColorStop(0.4, 'rgba(255, 255, 255, 0.6)');
cloudGradient.addColorStop(0.7, 'rgba(255, 255, 255, 0.3)');
cloudGradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
for (let i = 0; i < 10; i++) {
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height * 0.7;
const size = Math.random() * 100 + 50;
for (let j = 0; j < 5; j++) {
const offsetX = (Math.random() - 0.5) * size * 0.7;
const offsetY = (Math.random() - 0.5) * size * 0.7;
const circleSize = size * (0.5 + Math.random() * 0.5);
ctx.beginPath();
ctx.arc(x + offsetX, y + offsetY, circleSize / 2, 0, Math.PI * 2);
ctx.fill();
}
}
break;
default:
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
const texture = new THREE.CanvasTexture(canvas);
texture.sourceFile = type;
return texture;
}
async function createMaterial(color, textureName) {
const material = new THREE.MeshPhongMaterial({
color: new THREE.Color(color),
shininess: 30,
});
if (textureName && textureName !== 'none') {
try {
const texture = await loadTexture(textureName);
if (texture) {
material.map = texture;
material.needsUpdate = true;
}
} catch (error) {
}
}
return material;
}
async function applyTexture(object, textureName) {
try {
const texture = await loadTexture(textureName);
if (texture) {
object.material.map = texture;
object.material.needsUpdate = true;
} else {
object.material.map = null;
object.material.needsUpdate = true;
}
addToHistory({
type: 'changeTexture',
object: object.uuid,
texture: textureName
});
} catch (error) {
}
}
async function createObject(params = {}) {
const shape = params.shape || document.getElementById('object-shape').value;
const color = params.color || document.getElementById('object-color').value;
const texture = params.texture || document.getElementById('object-texture').value;
const position = params.position || new THREE.Vector3(0, 1, 0);
const scale = params.scale || new THREE.Vector3(1, 1, 1);
const rotation = params.rotation || new THREE.Euler(0, 0, 0);
const type = params.type || 'object';
if (shape === 'text') {
showTextInputModal();
return;
}
const geometry = createGeometry(shape, params.geometryParams || {});
const material = await createMaterial(color, texture);
const object = new THREE.Mesh(geometry, material);
object.position.copy(position);
object.scale.copy(scale);
object.rotation.copy(rotation);
object.castShadow = true;
object.receiveShadow = true;
const objectId = `${type}_${++state.objectCounter}`;
object.userData = {
type: type,
id: objectId,
shape: shape,
createdAt: Date.now()
};
scene.add(object);
sceneObjects[objectId] = object;
selectableObjects.push(object);
selectObject(object);
addToHistory({
type: 'create',
objectType: type,
shape: shape,
color: color,
texture: texture,
position: object.position.clone(),
rotation: object.rotation.clone(),
scale: object.scale.clone(),
object: object.uuid
});
return object;
}
function createText3D(text, size = 1, depth = 0.2) {
const geometry = new THREE.BoxGeometry(text.length * 0.5 * size, size, depth);
const material = createMaterial(document.getElementById('object-color').value, 'none');
const textMesh = new THREE.Mesh(geometry, material);
textMesh.userData = {
type: 'text',
id: `text_${++state.objectCounter}`,
textValue: text,
textSize: size,
textDepth: depth,
createdAt: Date.now()
};
textMesh.position.set(0, size/2, 0);
textMesh.castShadow = true;
textMesh.receiveShadow = true;
scene.add(textMesh);
sceneObjects[textMesh.userData.id] = textMesh;
selectableObjects.push(textMesh);
selectObject(textMesh);
addToHistory({
type: 'create',
objectType: 'text',
textValue: text,
textSize: size,
textDepth: depth,
position: textMesh.position.clone(),
rotation: textMesh.rotation.clone(),
scale: textMesh.scale.clone(),
object: textMesh.uuid
});
return textMesh;
}
function createLight(type = 'point', params = {}) {
let light;
if (params.replaceSceneLight && sceneObjects.light) {
scene.remove(sceneObjects.light);
disposeObject(sceneObjects.light);
if (state.selectedObject === sceneObjects.light) {
deselectObject();
}
}
switch (type) {
case 'point':
light = new THREE.PointLight(params.color || 0xffffff, params.intensity || 1);
break;
case 'spot':
light = new THREE.SpotLight(params.color || 0xffffff, params.intensity || 1);
light.angle = params.angle || Math.PI / 6;
light.penumbra = params.penumbra || 0.2;
break;
case 'directional':
light = new THREE.DirectionalLight(params.color || 0xffffff, params.intensity || 1);
break;
default:
light = new THREE.PointLight(params.color || 0xffffff, params.intensity || 1);
}
light.position.set(
params.position ? params.position.x : 0,
params.position ? params.position.y : 5,
params.position ? params.position.z : 0
);
light.castShadow = params.castShadow !== undefined ? params.castShadow : true;
if (light.shadow) {
light.shadow.mapSize.width = 1024;
light.shadow.mapSize.height = 1024;
light.shadow.camera.near = 0.5;
light.shadow.camera.far = 50;
}
const sphereGeometry = new THREE.SphereGeometry(0.2, 16, 8);
const sphereMaterial = new THREE.MeshBasicMaterial({
color: 0xffff00,
transparent: true,
opacity: 0.7
});
const lightMesh = new THREE.Mesh(sphereGeometry, sphereMaterial);
light.add(lightMesh);
light.userData = {
type: 'light',
lightType: type,
id: `light_${++state.objectCounter}`,
createdAt: Date.now()
};
let helper;
if (type === 'point') {
helper = new THREE.PointLightHelper(light, 0.5);
} else if (type === 'spot') {
helper = new THREE.SpotLightHelper(light);
} else if (type === 'directional') {
helper = new THREE.DirectionalLightHelper(light, 1);
}
if (helper) {
helper.userData = {
isHelper: true,
parentId: light.userData.id
};
scene.add(helper);
light.userData.helper = helper;
}
scene.add(light);
sceneObjects[light.userData.id] = light;
selectableObjects.push(light);
if (params.replaceSceneLight) {
sceneObjects.light = light;
}
selectObject(light);
addToHistory({
type: 'create',
objectType: 'light',
lightType: type,
position: light.position.clone(),
object: light.uuid
});
return light;
}
function disposeObject(object) {
if (!object) return;
const objectState = storeObjectState(object);
if (object.userData && object.userData.helper) {
scene.remove(object.userData.helper);
}
if (object.geometry) {
object.geometry.dispose();
}
if (object.material) {
if (Array.isArray(object.material)) {
object.material.forEach(material => disposeMaterial(material));
} else {
disposeMaterial(object.material);
}
}
const index = selectableObjects.indexOf(object);
if (index !== -1) {
selectableObjects.splice(index, 1);
}
if (object.userData && object.userData.id) {
delete sceneObjects[object.userData.id];
}
scene.remove(object);
addToHistory({
type: 'delete',
objectUUID: object.uuid,
objectState: objectState
});
if (state.selectedObject === object) {
deselectObject();
}
return objectState;
}
function disposeMaterial(material) {
if (!material) return;
if (material.map) {
material.map.dispose();
}
for (const key in material) {
if (material[key] && material[key].isTexture) {
material[key].dispose();
}
}
material.dispose();
}
function selectObject(object) {
if (object === state.selectedObject) return;
state.selectedObject = object;
transformControls.attach(object);
updateObjectPropertiesUI(object);
document.getElementById('delete-object').disabled = false;
}
function deselectObject() {
transformControls.detach();
state.selectedObject = null;
updateObjectPropertiesUI(null);
document.getElementById('delete-object').disabled = true;
}
function updateObjectPropertiesUI(object) {
if (!object) return;
const objectShape = determineObjectShape(object);
if (objectShape) {
document.getElementById('object-shape').value = objectShape;
}
if (object.material && object.material.color) {
const color = '#' + object.material.color.getHexString();
document.getElementById('object-color').value = color;
}
if (object.material && object.material.map) {
const textureName = determineObjectTexture(object);
document.getElementById('object-texture').value = textureName || 'none';
} else {
document.getElementById('object-texture').value = 'none';
}
}
function determineObjectShape(object) {
if (!object || !object.geometry) return null;
if (object.userData && object.userData.shape) {
return object.userData.shape;
}
if (object.geometry instanceof THREE.BoxGeometry) return 'box';
if (object.geometry instanceof THREE.SphereGeometry) return 'sphere';
if (object.geometry instanceof THREE.CylinderGeometry) return 'cylinder';
if (object.geometry instanceof THREE.ConeGeometry) return 'cone';
if (object.geometry instanceof THREE.TorusGeometry) return 'torus';
if (object.geometry instanceof THREE.PlaneGeometry) return 'plane';
if (object.geometry instanceof THREE.CircleGeometry) return 'circle';
if (object.geometry instanceof THREE.RingGeometry) return 'ring';
if (object.geometry instanceof THREE.TetrahedronGeometry) return 'tetrahedron';
if (object.geometry instanceof THREE.OctahedronGeometry) return 'octahedron';
if (object.geometry instanceof THREE.DodecahedronGeometry) return 'dodecahedron';
if (object.geometry instanceof THREE.IcosahedronGeometry) return 'icosahedron';
if (object.geometry instanceof THREE.TorusKnotGeometry) return 'torusKnot';
if (object.geometry instanceof THREE.CapsuleGeometry) return 'capsule';
if (object.geometry instanceof THREE.LatheGeometry) return 'lathe';
if (object.geometry instanceof THREE.ExtrudeGeometry) return 'extrude';
if (object.geometry instanceof THREE.TubeGeometry) return 'tube';
return null;
}
function determineObjectTexture(object) {
if (!object || !object.material || !object.material.map) return null;
const texture = object.material.map;
if (texture.sourceFile) {
return texture.sourceFile;
}
for (const [name, customTexture] of Object.entries(state.customTextures)) {
if (texture === customTexture) {
return name;
}
}
const url = texture.image && texture.image.src ? texture.image.src : '';
if (url.includes('brick_diffuse')) return 'brick';
if (url.includes('wood')) return 'wood';
if (url.includes('metal')) return 'metal';
if (url.includes('stone')) return 'stone';
if (url.includes('grass')) return 'grass';
if (url.includes('water')) return 'water';
if (url.includes('marble')) return 'marble';
if (url.includes('concrete')) return 'concrete';
if (url.includes('fabric')) return 'fabric';
if (url.includes('leather')) return 'leather';
if (url.includes('glass')) return 'glass';
if (url.includes('ice')) return 'ice';
if (url.includes('earth_atmos')) return 'earth';
if (url.includes('moon')) return 'moon';
if (url.includes('stars')) return 'stars';
return 'none';
}
function newScene() {
const objectsToRemove = [];
scene.children.forEach(child => {
if (child !== ground && child !== ambientLight &&
child !== directionalLight && child !== hemisphereLight &&
child !== gridHelper && child !== axesHelper && child !== transformControls) {
objectsToRemove.push(child);
}
});
objectsToRemove.forEach(object => {
scene.remove(object);
disposeObject(object);
});
Object.keys(sceneObjects).forEach(key => {
delete sceneObjects[key];
});
selectableObjects = [];
deselectObject();
state.history = [];
state.historyIndex = -1;
state.objectCounter = 0;
updateHistoryButtonStates();
createObject({
shape: 'box',
color: '#00cc00',
texture: 'none',
position: new THREE.Vector3(0, 1, 0),
type: 'object'
});
}
function saveScene() {
takeScreenshot();
const sceneJson = {
objects: [],
settings: {
showGrid: state.showGrid,
showAxes: state.showAxes,
showShadows: state.showShadows
}
};
Object.values(sceneObjects).forEach(obj => {
if (obj.userData && obj.userData.type) {
const objectData = {
type: obj.userData.type,
id: obj.userData.id,
position: {
x: obj.position.x,
y: obj.position.y,
z: obj.position.z
},
rotation: {
x: obj.rotation.x,
y: obj.rotation.y,
z: obj.rotation.z
},
scale: {
x: obj.scale.x,
y: obj.scale.y,
z: obj.scale.z
}
};
if (obj.userData.type === 'light') {
objectData.lightType = obj.userData.lightType;
objectData.intensity = obj.intensity;
objectData.color = obj.color ? '#' + obj.color.getHexString() : '#ffffff';
} else if (obj.userData.shape) {
objectData.shape = obj.userData.shape;
if (obj.material) {
objectData.material = {
color: '#' + obj.material.color.getHexString(),
texture: obj.material.map ? determineObjectTexture(obj) : 'none'
};
}
}
sceneJson.objects.push(objectData);
}
});
const jsonContent = JSON.stringify(sceneJson, null, 2);
}
function exportToObj() {
if (!state.selectedObject) {
return;
}
const exporter = new THREE.OBJExporter();
const result = exporter.parse(state.selectedObject);
}
function exportToJson() {
saveScene();
}
function deleteSelectedObject() {
if (!state.selectedObject) {
return;
}
const objectState = storeObjectState(state.selectedObject);
addToHistory({
type: 'delete',
objectUUID: state.selectedObject.uuid,
objectState: objectState
});
scene.remove(state.selectedObject);
disposeObject(state.selectedObject);
deselectObject();
}
function addToHistory(action) {
if (state.historyIndex < state.history.length - 1) {
state.history = state.history.slice(0, state.historyIndex + 1);
}
if (action.type === 'transform') {
const object = findObjectByUUID(action.object);
if (object) {
let previousState = null;
for (let i = state.history.length - 1; i >= 0; i--) {
const historyItem = state.history[i];
if ((historyItem.type === 'create' || historyItem.type === 'transform') &&
historyItem.object === action.object) {
previousState = {
position: historyItem.position.clone(),
rotation: historyItem.rotation.clone(),
scale: historyItem.scale.clone()
};
break;
}
}
if (previousState) {
action.previousState = previousState;
}
}
}
state.history.push(action);
if (state.history.length > state.maxHistorySize) {
state.history.shift();
} else {
state.historyIndex++;
}
updateHistoryButtonStates();
}
function updateHistoryButtonStates() {
document.getElementById('undo-btn').disabled = state.historyIndex < 0;
document.getElementById('redo-btn').disabled = state.historyIndex >= state.history.length - 1;
}
function undoAction() {
if (state.historyIndex < 0) return;
const action = state.history[state.historyIndex];
switch (action.type) {
case 'create':
const createdObject = findObjectByUUID(action.object);
if (createdObject) {
scene.remove(createdObject);
const index = selectableObjects.indexOf(createdObject);
if (index !== -1) {
selectableObjects.splice(index, 1);
}
if (createdObject.userData && createdObject.userData.id) {
delete sceneObjects[createdObject.userData.id];
}
if (state.selectedObject === createdObject) {
deselectObject();
}
}
break;
case 'delete':
if (action.objectState) {
const geometry = createGeometry(action.objectState.userData.shape || 'box');
const material = new THREE.MeshPhongMaterial({
color: action.objectState.material && action.objectState.material.color || 0x00cc00,
shininess: 30
});
const restoredObject = new THREE.Mesh(geometry, material);
restoredObject.position.copy(action.objectState.position);
restoredObject.rotation.copy(action.objectState.rotation);
restoredObject.scale.copy(action.objectState.scale);
restoredObject.userData = action.objectState.userData;
scene.add(restoredObject);
sceneObjects[restoredObject.userData.id] = restoredObject;
selectableObjects.push(restoredObject);
if (action.objectState.material && action.objectState.material.map) {
applyTexture(restoredObject, action.objectState.material.map);
}
selectObject(restoredObject);
}
break;
case 'transform':
if (action.previousState) {
const object = findObjectByUUID(action.object);
if (object) {
object.position.copy(action.previousState.position);
object.rotation.copy(action.previousState.rotation);
object.scale.copy(action.previousState.scale);
if (object.userData.helper) {
object.userData.helper.update();
}
}
}
break;
case 'changeColor':
if (action.previousColor) {
const object = findObjectByUUID(action.object);
if (object && object.material) {
object.material.color.set(action.previousColor);
}
}
break;
case 'changeTexture':
if (action.previousTexture) {
const object = findObjectByUUID(action.object);
if (object) {
applyTexture(object, action.previousTexture);
}
}
break;
case 'changeShape':
break;
}
state.historyIndex--;
updateHistoryButtonStates();
}
function redoAction() {
if (state.historyIndex >= state.history.length - 1) return;
state.historyIndex++;
const action = state.history[state.historyIndex];
switch (action.type) {
case 'create':
if (action.objectType === 'object') {
createObject({
shape: action.shape,
color: action.color,
texture: action.texture,
position: action.position,
rotation: action.rotation,
scale: action.scale,
type: action.objectType
});
} else if (action.objectType === 'light') {
createLight(action.lightType, {
position: action.position
});
} else if (action.objectType === 'text') {
createText3D(action.textValue, action.textSize, action.textDepth);
}
break;
case 'delete':
const objectToDelete = findObjectByUUID(action.objectUUID);
if (objectToDelete) {
scene.remove(objectToDelete);
disposeObject(objectToDelete);
}
break;
case 'transform':
const object = findObjectByUUID(action.object);
if (object) {
object.position.copy(action.position);
object.rotation.copy(action.rotation);
object.scale.copy(action.scale);
if (object.userData.helper) {
object.userData.helper.update();
}
}
break;
case 'changeColor':
const colorObj = findObjectByUUID(action.object);
if (colorObj && colorObj.material) {
colorObj.material.color.set(action.color);
}
break;
case 'changeTexture':
const textureObj = findObjectByUUID(action.object);
if (textureObj) {
applyTexture(textureObj, action.texture);
}
break;
}
updateHistoryButtonStates();
}
function findObjectByUUID(uuid) {
let result = null;
scene.traverse(function (object) {
if (object.uuid === uuid) {
result = object;
}
});
return result;
}
function showOpenFileModal() {
document.getElementById('open-file-modal').classList.remove('hidden');
document.getElementById('tab-file').click();
}
function closeOpenFileModal() {
document.getElementById('open-file-modal').classList.add('hidden');
}
function switchOpenTab(tab) {
state.currentOpenTab = tab;
if (tab === 'file') {
document.getElementById('tab-file').classList.add('border-primary', 'text-primary');
document.getElementById('tab-file').classList.remove('border-transparent', 'text-gray-500');
document.getElementById('tab-text').classList.remove('border-primary', 'text-primary');
document.getElementById('tab-text').classList.add('border-transparent', 'text-gray-500');
document.getElementById('file-input-container').classList.remove('hidden');
document.getElementById('text-input-container').classList.add('hidden');
} else {
document.getElementById('tab-text').classList.add('border-primary', 'text-primary');
document.getElementById('tab-text').classList.remove('border-transparent', 'text-gray-500');
document.getElementById('tab-file').classList.remove('border-primary', 'text-primary');
document.getElementById('tab-file').classList.add('border-transparent', 'text-gray-500');
document.getElementById('text-input-container').classList.remove('hidden');
document.getElementById('file-input-container').classList.add('hidden');
}
}
function processOpenFile() {
if (state.currentOpenTab === 'file') {
const fileInput = document.getElementById('open-file-input');
if (fileInput.files.length > 0) {
const file = fileInput.files[0];
handleFileImport({ target: { files: [file] } });
closeOpenFileModal();
} else {
}
} else {
const format = document.getElementById('open-text-format').value;
const content = document.getElementById('open-text-content').value.trim();
if (!content) {
return;
}
try {
if (format === 'json') {
const jsonData = JSON.parse(content);
loadSceneFromJson(jsonData);
} else if (format === 'obj') {
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const loader = new THREE.OBJLoader();
loader.load(
url,
(object) => {
addImportedObject(object, 'Pasted OBJ');
URL.revokeObjectURL(url);
},
null,
(error) => {
URL.revokeObjectURL(url);
}
);
}
closeOpenFileModal();
} catch (error) {
}
}
}
function showNotification(message, type = 'info', duration = 3000) {
}
function takeScreenshot() {
const wasAttached = transformControls.object !== null;
let attachedObject = null;
if (wasAttached) {
attachedObject = transformControls.object;
transformControls.detach();
}
renderer.render(scene, camera);
const dataURL = renderer.domElement.toDataURL('image/png');
document.getElementById('screenshot-img').src = dataURL;
document.getElementById('screenshot-modal').classList.remove('hidden');
if (wasAttached && attachedObject) {
transformControls.attach(attachedObject);
}
}
function importModel(fileType) {
const importFileInput = document.getElementById('import-file');
importFileInput.accept = fileType === 'obj' ? '.obj' :
fileType === 'stl' ? '.stl' :
fileType === 'textures' ? '.png,.jpg,.jpeg' :
'.glb,.gltf';
importFileInput.click();
}
function handleFileImport(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
const extension = file.name.split('.').pop().toLowerCase();
if (['png', 'jpg', 'jpeg'].includes(extension)) {
handleTextureImport(file);
return;
}
if (extension === 'json') {
reader.onload = function(e) {
try {
const sceneData = JSON.parse(e.target.result);
loadSceneFromJson(sceneData);
} catch (error) {
}
};
reader.readAsText(file);
return;
}
reader.onload = function(e) {
let loader;
switch (extension) {
case 'obj':
loader = new THREE.OBJLoader();
loader.load(
URL.createObjectURL(file),
function(object) {
addImportedObject(object, file.name);
},
null,
function(error) {
}
);
break;
case 'stl':
loader = new THREE.STLLoader();
loader.load(
URL.createObjectURL(file),
function(geometry) {
const material = createMaterial(document.getElementById('object-color').value, 'none');
const mesh = new THREE.Mesh(geometry, material);
addImportedObject(mesh, file.name);
},
null,
function(error) {
}
);
break;
case 'glb':
case 'gltf':
loader = new THREE.GLTFLoader();
loader.load(
URL.createObjectURL(file),
function(gltf) {
addImportedObject(gltf.scene, file.name);
},
null,
function(error) {
}
);
break;
default:
return;
}
};
reader.readAsArrayBuffer(file);
}
function handleTextureImport(file) {
document.getElementById('texture-modal').classList.remove('hidden');
const reader = new FileReader();
reader.onload = function(e) {
const previewImg = document.getElementById('texture-preview-img');
previewImg.src = e.target.result;
document.getElementById('texture-preview').classList.remove('hidden');
const fileName = file.name.split('.')[0];
document.getElementById('texture-name').value = fileName;
document.getElementById('texture-file').file = file;
};
reader.readAsDataURL(file);
}
function addCustomTexture() {
const textureFile = document.getElementById('texture-file').file;
const textureName = document.getElementById('texture-name').value.trim();
if (!textureFile || !textureName) {
return;
}
const reader = new FileReader();
reader.onload = function(e) {
const image = new Image();
image.src = e.target.result;
image.onload = function() {
const texture = new THREE.Texture(image);
texture.needsUpdate = true;
texture.sourceFile = textureName;
state.customTextures[textureName] = texture;
const textureSelect = document.getElementById('object-texture');
let exists = false;
for (let i = 0; i < textureSelect.options.length; i++) {
if (textureSelect.options[i].value === textureName) {
exists = true;
break;
}
}
if (!exists) {
const option = document.createElement('option');
option.value = textureName;
option.textContent = textureName + ' (Custom)';
const customOption = Array.from(textureSelect.options).find(opt => opt.value === 'custom');
if (customOption) {
textureSelect.insertBefore(option, customOption);
} else {
textureSelect.appendChild(option);
}
}
document.getElementById('texture-modal').classList.add('hidden');
addAssetToPanel(textureName, e.target.result, 'texture');
if (state.selectedObject && state.selectedObject.material) {
applyTexture(state.selectedObject, textureName);
}
};
};
reader.readAsDataURL(textureFile);
}
function addAssetToPanel(name, url, type) {
const assetsContainer = document.getElementById('uploaded-assets');
const assetItem = document.createElement('div');
assetItem.className = 'p-1 border border-gray-200 dark:border-gray-700 rounded';
assetItem.dataset.assetName = name;
assetItem.dataset.assetType = type;
const thumbnail = document.createElement('div');
thumbnail.className = 'w-full aspect-square bg-gray-100 dark:bg-gray-800 rounded overflow-hidden';
if (type === 'texture') {
const img = document.createElement('img');
img.src = url;
img.className = 'w-full h-full object-cover';
thumbnail.appendChild(img);
} else {
thumbnail.innerHTML = '<div class="w-full h-full flex items-center justify-center text-gray-400">File</div>';
}
const assetName = document.createElement('p');
assetName.className = 'text-xs mt-1 truncate';
assetName.textContent = name;
assetItem.appendChild(thumbnail);
assetItem.appendChild(assetName);
if (type === 'texture') {
assetItem.addEventListener('click', () => {
if (state.selectedObject && state.selectedObject.material) {
applyTexture(state.selectedObject, name);
} else {
}
});
}
assetsContainer.appendChild(assetItem);
}
function loadSceneFromJson(sceneData) {
try {
newScene();
if (sceneData.settings) {
if (sceneData.settings.showGrid !== undefined) {
state.showGrid = sceneData.settings.showGrid;
gridHelper.visible = state.showGrid;
document.getElementById('show-grid').checked = state.showGrid;
}
if (sceneData.settings.showAxes !== undefined) {
state.showAxes = sceneData.settings.showAxes;
axesHelper.visible = state.showAxes;
document.getElementById('show-axes').checked = state.showAxes;
}
if (sceneData.settings.showShadows !== undefined) {
state.showShadows = sceneData.settings.showShadows;
renderer.shadowMap.enabled = state.showShadows;
document.getElementById('show-shadows').checked = state.showShadows;
}
}
if (Array.isArray(sceneData.objects)) {
sceneData.objects.forEach(objData => {
if (objData.type === 'light') {
createLight(objData.lightType || 'point', {
position: new THREE.Vector3(
objData.position.x || 0,
objData.position.y || 5,
objData.position.z || 0
),
intensity: objData.intensity || 1,
color: objData.color || 0xffffff
});
} else {
createObject({
shape: objData.shape || 'box',
color: objData.material && objData.material.color ? objData.material.color : '#00cc00',
texture: objData.material && objData.material.texture ? objData.material.texture : 'none',
position: new THREE.Vector3(
objData.position.x || 0,
objData.position.y || 1,
objData.position.z || 0
),
rotation: new THREE.Euler(
objData.rotation.x || 0,
objData.rotation.y || 0,
objData.rotation.z || 0
),
scale: new THREE.Vector3(
objData.scale.x || 1,
objData.scale.y || 1,
objData.scale.z || 1
),
type: objData.type || 'object'
});
}
});
}
} catch (error) {
}
}
function addImportedObject(object, filename) {
object.traverse(function(child) {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
const box = new THREE.Box3().setFromObject(object);
const center = box.getCenter(new THREE.Vector3());
object.position.sub(center);
object.position.y = 0;
const size = box.getSize(new THREE.Vector3());
const maxDim = Math.max(size.x, size.y, size.z);
if (maxDim > 5) {
const scale = 5 / maxDim;
object.scale.multiplyScalar(scale);
}
const objectId = `imported_${++state.objectCounter}`;
object.userData = {
type: 'imported',
id: objectId,
filename: filename,
importedAt: Date.now()
};
scene.add(object);
sceneObjects[objectId] = object;
selectableObjects.push(object);
selectObject(object);
addToHistory({
type: 'importModel',
objectType: 'imported',
filename: filename,
object: object.uuid
});
}
function exportModel(fileType) {
if (!state.selectedObject) {
return;
}
let result;
switch (fileType) {
case 'obj':
const objExporter = new THREE.OBJExporter();
result = objExporter.parse(state.selectedObject);
break;
case 'stl':
const stlExporter = new THREE.STLExporter();
result = stlExporter.parse(state.selectedObject, { binary: true });
break;
case 'glb':
const gltfExporter = new THREE.GLTFExporter();
gltfExporter.parse(state.selectedObject, function(gltf) {
}, { binary: true });
break;
case 'json':
exportToJson();
break;
default:
return;
}
}
function showTextInputModal() {
document.getElementById('text-input-modal').classList.remove('hidden');
}
function closeTextInputModal() {
document.getElementById('text-input-modal').classList.add('hidden');
}
function processTextInput() {
const text = document.getElementById('text-value').value;
const size = parseFloat(document.getElementById('text-size').value);
const depth = parseFloat(document.getElementById('text-depth').value);
if (!text) {
return;
}
createText3D(text, size, depth);
closeTextInputModal();
}
function showTemplatesModal() {
document.getElementById('templates-modal').classList.remove('hidden');
}
function closeTemplatesModal() {
document.getElementById('templates-modal').classList.add('hidden');
}
function handleKeyDown(event) {
if (event.key === 'Delete' && state.selectedObject) {
deleteSelectedObject();
}
if (event.key === 'z' && (event.ctrlKey || event.metaKey) && !event.shiftKey) {
event.preventDefault();
undoAction();
}
if ((event.key === 'y' && (event.ctrlKey || event.metaKey)) ||
(event.key === 'z' && (event.ctrlKey || event.metaKey) && event.shiftKey)) {
event.preventDefault();
redoAction();
}
}
function loadTemplate(templateName) {
newScene();
switch (templateName) {
case 'empty':
break;
case 'room':
createRoomTemplate();
break;
case 'character':
createCharacterTemplate();
break;
case 'landscape':
createLandscapeTemplate();
break;
case 'vehicle':
createVehicleTemplate();
break;
case 'abstract':
createAbstractTemplate();
break;
default:
return;
}
}
async function createRoomTemplate() {
await createObject({
shape: 'box',
color: '#8B4513',
texture: 'wood',
position: new THREE.Vector3(0, -0.25, 0),
scale: new THREE.Vector3(10, 0.5, 10),
type: 'floor'
});
await createObject({
shape: 'box',
color: '#F5F5DC',
texture: 'concrete',
position: new THREE.Vector3(0, 2.5, -5),
scale: new THREE.Vector3(10, 5, 0.5),
type: 'wall'
});
await createObject({
shape: 'box',
color: '#F5F5DC',
texture: 'concrete',
position: new THREE.Vector3(-5, 2.5, 0),
scale: new THREE.Vector3(0.5, 5, 10),
type: 'wall'
});
await createObject({
shape: 'box',
color: '#87CEFA',
texture: 'plastic',
position: new THREE.Vector3(0, 3, -4.8),
scale: new THREE.Vector3(3, 2, 0.1),
type: 'window'
});
await createObject({
shape: 'box',
color: '#8B4513',
texture: 'wood',
position: new THREE.Vector3(0, 1, 0),
scale: new THREE.Vector3(2, 0.2, 1.5),
type: 'table'
});
for (let x = -0.8; x <= 0.8; x += 1.6) {
for (let z = -0.6; z <= 0.6; z += 1.2) {
await createObject({
shape: 'cylinder',
color: '#8B4513',
texture: 'wood',
position: new THREE.Vector3(x, 0.5, z),
scale: new THREE.Vector3(0.1, 1, 0.1),
type: 'leg'
});
}
}
createLight('point', {
position: new THREE.Vector3(0.5, 2, 0),
intensity: 1.5
});
}
async function createCharacterTemplate() {
await createObject({
shape: 'box',
color: '#4169E1',
texture: 'fabric',
position: new THREE.Vector3(0, 1.5, 0),
scale: new THREE.Vector3(1, 1.5, 0.5),
type: 'body'
});
await createObject({
shape: 'sphere',
color: '#FFA07A',
texture: 'none',
position: new THREE.Vector3(0, 3, 0),
scale: new THREE.Vector3(0.5, 0.5, 0.5),
type: 'head'
});
}
async function createLandscapeTemplate() {
await createObject({
shape: 'box',
color: '#228B22',
texture: 'grass',
position: new THREE.Vector3(0, -0.5, 0),
scale: new THREE.Vector3(20, 1, 20),
type: 'ground'
});
for (let i = 0; i < 7; i++) {
const x = Math.random() * 16 - 8;
const z = Math.random() * 16 - 8;
const height = Math.random() * 2 + 1;
const width = Math.random() * 3 + 2;
await createObject({
shape: 'sphere',
color: '#228B22',
texture: 'grass',
position: new THREE.Vector3(x, height/2 - 0.5, z),
scale: new THREE.Vector3(width, height, width),
type: 'hill'
});
}
}
async function createVehicleTemplate() {
await createObject({
shape: 'box',
color: '#FF0000',
texture: 'metal',
position: new THREE.Vector3(0, 0.5, 0),
scale: new THREE.Vector3(4, 0.8, 2),
type: 'car_body'
});
await createObject({
shape: 'box',
color: '#FF0000',
texture: 'metal',
position: new THREE.Vector3(-0.2, 1.3, 0),
scale: new THREE.Vector3(2, 0.8, 1.8),
type: 'car_top'
});
await createObject({
shape: 'box',
color: '#87CEFA',
texture: 'plastic',
position: new THREE.Vector3(0.7, 1.3, 0),
rotation: new THREE.Euler(-Math.PI/10, 0, 0),
scale: new THREE.Vector3(0.1, 0.7, 1.7),
type: 'windshield'
});
}
async function createAbstractTemplate() {
const shapes = ['sphere', 'box', 'cone', 'cylinder', 'torus', 'tetrahedron', 'octahedron'];
const colors = ['#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF', '#FF8000'];
const textures = ['none', 'checkerboard', 'grid', 'dots', 'carbon', 'rubber', 'plastic'];
await createObject({
shape: 'torusKnot',
color: '#FF0000',
texture: 'metal',
position: new THREE.Vector3(0, 3, 0),
scale: new THREE.Vector3(1.5, 1.5, 1.5),
type: 'central'
});
for (let i = 0; i < 20; i++) {
const angle = (i / 20) * Math.PI * 2;
const radius = 3 + Math.random() * 2;
const height = 1 + Math.sin(i * 0.5) * 3;
const x = Math.cos(angle) * radius;
const z = Math.sin(angle) * radius;
const shapeIndex = Math.floor(Math.random() * shapes.length);
const colorIndex = Math.floor(Math.random() * colors.length);
const textureIndex = Math.floor(Math.random() * textures.length);
const size = 0.2 + Math.random() * 0.6;
await createObject({
shape: shapes[shapeIndex],
color: colors[colorIndex],
texture: textures[textureIndex],
position: new THREE.Vector3(x, height, z),
rotation: new THREE.Euler(
Math.random() * Math.PI * 2,
Math.random() * Math.PI * 2,
Math.random() * Math.PI * 2
),
scale: new THREE.Vector3(size, size, size),
type: 'abstract'
});
}
createLight('point', {
position: new THREE.Vector3(0, 5, 0),
intensity: 1.5,
color: 0xFF8000
});
createLight('point', {
position: new THREE.Vector3(5, 2, 0),
intensity: 0.8,
color: 0x0080FF
});
createLight('point', {
position: new THREE.Vector3(-5, 2, 0),
intensity: 0.8,
color: 0xFF0080
});
}
function animate() {
requestAnimationFrame(animate);
orbitControls.update();
Object.values(sceneObjects).forEach(obj => {
if (obj.userData && obj.userData.type === 'light' && obj.userData.helper) {
obj.userData.helper.update();
}
});
renderer.render(scene, camera);
}
function onWindowResize() {
camera.aspect = canvasContainer.clientWidth / canvasContainer.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(canvasContainer.clientWidth, canvasContainer.clientHeight);
}
function onMouseClick(event) {
const rect = renderer.domElement.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(selectableObjects);
if (intersects.length > 0) {
selectObject(intersects[0].object);
} else {
deselectObject();
}
}
async function showUpgradeBenefits() {
try {
window.Poe.registerHandler("upgrade-info-handler", (result) => {
if (result.status === "complete" && result.responses.length > 0) {
const response = result.responses[0];
if (response.status === "complete") {
} else if (response.status === "error") {
}
}
});
await window.Poe.sendUserMessage(
"@Video-Generator-PRO sub",
{
handler: "upgrade-info-handler",
stream: false,
openChat: false
}
);
} catch (error) {
}
}
// Function for the modelStudio button
async function startModelStudio() {
try {
await window.Poe.sendUserMessage(
"@3D-Model-Studio start",
{ openChat: false }
);
} catch (error) {
console.error("Error starting model studio:", error);
}
}
// Function for the objectGenerator button
async function startObjectGenerator() {
try {
await window.Poe.sendUserMessage(
"@3D-Object-Generator start",
{ openChat: false }
);
} catch (error) {
console.error("Error starting object generator:", error);
}
}
function initEventListeners() {
window.addEventListener('resize', onWindowResize);
window.addEventListener('keydown', handleKeyDown);
renderer.domElement.addEventListener('click', onMouseClick);
document.getElementById('theme-toggle').addEventListener('click', () => {
state.isDarkMode = !state.isDarkMode;
document.documentElement.classList.toggle('dark', state.isDarkMode);
});
document.querySelectorAll('.sidebar-tab').forEach(tab => {
tab.addEventListener('click', () => {
const tabId = tab.dataset.tab;
document.querySelectorAll('.sidebar-tab').forEach(t => {
t.classList.toggle('active', t === tab);
});
document.querySelectorAll('.panel').forEach(panel => {
panel.classList.toggle('active', panel.id === `${tabId}-panel`);
});
state.activeTab = tabId;
});
});
document.querySelectorAll('.category-title').forEach(title => {
title.addEventListener('click', () => {
const content = title.nextElementSibling;
content.classList.toggle('expanded');
title.querySelector('.category-icon').classList.toggle('expanded');
});
});
document.querySelectorAll('.transform-control-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.transform-control-btn').forEach(b => {
b.classList.remove('active')
});
btn.classList.add('active');
const mode = btn.dataset.mode;
transformControls.setMode(mode);
state.currentMode = mode;
});
});
document.querySelector('[data-mode="translate"]').classList.add('active');
transformControls.setMode('translate');
document.querySelectorAll('[data-action="addShape"]').forEach(btn => {
btn.addEventListener('click', () => {
const shape = btn.dataset.shape;
document.getElementById('object-shape').value = shape;
if (shape === 'text') {
showTextInputModal();
} else {
createObject({ shape: shape });
}
});
});
document.querySelector('[data-action="addLight"]').addEventListener('click', () => {
createLight('point');
});
document.getElementById('apply-color').addEventListener('click', () => {
if (state.selectedObject && state.selectedObject.material) {
const color = document.getElementById('object-color').value;
const previousColor = state.selectedObject.material.color.getHex();
state.selectedObject.material.color.set(color);
addToHistory({
type: 'changeColor',
object: state.selectedObject.uuid,
color: color,
previousColor: previousColor
});
} else {
}
});
document.getElementById('object-shape').addEventListener('change', () => {
const shape = document.getElementById('object-shape').value;
if (state.selectedObject) {
if (shape === 'text') {
showTextInputModal();
return;
}
const type = state.selectedObject.userData.type;
const position = state.selectedObject.position.clone();
const rotation = state.selectedObject.rotation.clone();
const scale = state.selectedObject.scale.clone();
const color = state.selectedObject.material ? state.selectedObject.material.color.getHex() : 0x00cc00;
const texture = state.selectedObject.material && state.selectedObject.material.map ?
determineObjectTexture(state.selectedObject) : 'none';
const oldUUID = state.selectedObject.uuid;
scene.remove(state.selectedObject);
disposeObject(state.selectedObject);
const newObject = createObject({
shape: shape,
color: '#' + color.toString(16).padStart(6, '0'),
texture: texture,
position: position,
rotation: rotation,
scale: scale,
type: type
});
addToHistory({
type: 'changeShape',
oldObject: oldUUID,
newObject: newObject.uuid,
shape: shape
});
}
});
document.getElementById('object-texture').addEventListener('change', () => {
const textureName = document.getElementById('object-texture').value;
if (textureName === 'custom') {
document.getElementById('texture-modal').classList.remove('hidden');
return;
}
if (state.selectedObject && state.selectedObject.material) {
const previousTexture = state.selectedObject.material.map ?
determineObjectTexture(state.selectedObject) : 'none';
applyTexture(state.selectedObject, textureName);
addToHistory({
type: 'changeTexture',
object: state.selectedObject.uuid,
texture: textureName,
previousTexture: previousTexture
});
}
});
document.getElementById('new-scene').addEventListener('click', newScene);
document.getElementById('save-scene').addEventListener('click', saveScene);
document.getElementById('open-scene').addEventListener('click', showOpenFileModal);
document.getElementById('delete-object').addEventListener('click', deleteSelectedObject);
document.getElementById('undo-btn').addEventListener('click', undoAction);
document.getElementById('redo-btn').addEventListener('click', redoAction);
document.getElementById('screenshot-btn').addEventListener('click', takeScreenshot);
document.getElementById('close-modal').addEventListener('click', () => {
document.getElementById('screenshot-modal').classList.add('hidden')
});
document.getElementById('close-preview').addEventListener('click', () => {
document.getElementById('screenshot-modal').classList.add('hidden')
});
document.getElementById('close-text-modal').addEventListener('click', closeTextInputModal);
document.getElementById('cancel-text').addEventListener('click', closeTextInputModal);
document.getElementById('add-text').addEventListener('click', processTextInput);
document.getElementById('close-texture-modal').addEventListener('click', () => {
document.getElementById('texture-modal').classList.add('hidden');
});
document.getElementById('cancel-texture').addEventListener('click', () => {
document.getElementById('texture-modal').classList.add('hidden');
});
document.getElementById('add-texture').addEventListener('click', addCustomTexture);
document.getElementById('open-templates').addEventListener('click', showTemplatesModal);
document.getElementById('close-templates-modal').addEventListener('click', closeTemplatesModal);
document.querySelectorAll('.template-item').forEach(item => {
item.addEventListener('click', () => {
const template = item.dataset.template;
loadTemplate(template);
closeTemplatesModal();
});
});
document.getElementById('import-obj').addEventListener('click', () => importModel('obj'));
document.getElementById('import-stl').addEventListener('click', () => importModel('stl'));
document.getElementById('import-glb').addEventListener('click', () => importModel('glb'));
document.getElementById('import-textures').addEventListener('click', () => importModel('textures'));
document.getElementById('import-file').addEventListener('change', handleFileImport);
document.getElementById('export-obj').addEventListener('click', () => exportModel('obj'));
document.getElementById('export-stl').addEventListener('click', () => exportModel('stl'));
document.getElementById('export-glb').addEventListener('click', () => exportModel('glb'));
document.getElementById('export-json').addEventListener('click', () => exportModel('json'));
document.getElementById('upload-assets-btn').addEventListener('click', () => {
document.getElementById('asset-upload').click();
});
document.getElementById('asset-upload').addEventListener('change', (event) => {
const files = event.target.files;
if (!files || files.length === 0) return;
for (let i = 0; i < files.length; i++) {
const file = files[i];
const extension = file.name.split('.').pop().toLowerCase();
if (['png', 'jpg', 'jpeg'].includes(extension)) {
handleTextureImport(file);
} else if (extension === 'json') {
const reader = new FileReader();
reader.onload = function(e) {
try {
const data = JSON.parse(e.target.result);
addAssetToPanel(file.name, null, 'json');
state.assetFiles.push({
name: file.name,
type: 'json',
data: data
});
} catch (error) {
}
};
reader.readAsText(file);
}
}
});
document.getElementById('show-grid').addEventListener('change', (e) => {
gridHelper.visible = e.target.checked;
state.showGrid = e.target.checked;
});
document.getElementById('show-axes').addEventListener('change', (e) => {
axesHelper.visible = e.target.checked;
state.showAxes = e.target.checked;
});
document.getElementById('show-shadows').addEventListener('change', (e) => {
renderer.shadowMap.enabled = e.target.checked;
state.showShadows = e.target.checked;
scene.traverse(obj => {
if (obj.isMesh) {
obj.castShadow = e.target.checked;
obj.receiveShadow = e.target.checked;
} else if (obj.isLight && obj.shadow) {
obj.castShadow = e.target.checked;
}
});
renderer.shadowMap.needsUpdate = true;
});
document.getElementById('upgrade-btn').addEventListener('click', showUpgradeBenefits);
document.getElementById('subscribe-btn').addEventListener('click', showUpgradeBenefits);
// Add the correct event listeners for the Model Studio and Object Generator buttons
document.getElementById('modelStudio-btn').addEventListener('click', startModelStudio);
document.getElementById('objectGenerator-btn').addEventListener('click', startObjectGenerator);
document.getElementById('close-open-file-modal').addEventListener('click', closeOpenFileModal);
document.getElementById('cancel-open-file').addEventListener('click', closeOpenFileModal);
document.getElementById('confirm-open-file').addEventListener('click', processOpenFile);
document.getElementById('tab-file').addEventListener('click', () => switchOpenTab('file'));
document.getElementById('tab-text').addEventListener('click', () => switchOpenTab('text'));
}
function init() {
initEventListeners();
animate();
createObject();
updateHistoryButtonStates();
}
init();
</script>
</body>
</html>