aaurelions commited on
Commit
8026f5a
·
verified ·
1 Parent(s): 026a19d

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +109 -102
index.html CHANGED
@@ -12,9 +12,9 @@
12
  body { background-color: #111827; }
13
 
14
  .glass-ui {
15
- background-color: rgba(17, 24, 39, 0.85);
16
  backdrop-filter: blur(16px);
17
- border-color: rgba(255, 255, 255, 0.1);
18
  }
19
 
20
  .modal-overlay {
@@ -22,9 +22,6 @@
22
  backdrop-filter: blur(8px);
23
  }
24
 
25
- #loading-overlay {
26
- transition: opacity 0.3s ease-in-out;
27
- }
28
  #loading-overlay .spinner {
29
  border-top-color: #3b82f6;
30
  }
@@ -42,7 +39,7 @@
42
  .panel-trigger:hover { background-color: rgba(59, 130, 246, 0.5); }
43
 
44
  .list-item {
45
- transition: all 0.2s ease;
46
  border: 2px solid transparent;
47
  }
48
  .list-item:hover {
@@ -50,7 +47,7 @@
50
  transform: scale(1.03);
51
  }
52
  .list-item.active {
53
- background-color: rgba(59, 130, 246, 0.25);
54
  border-color: #3b82f6;
55
  }
56
 
@@ -63,7 +60,7 @@
63
  transition: background-color 0.2s, border-color 0.2s;
64
  }
65
  .drop-zone.drag-over {
66
- background-color: rgba(59, 130, 246, 0.2);
67
  border-color: #3b82f6;
68
  }
69
  </style>
@@ -72,7 +69,7 @@
72
 
73
  <canvas id="bg-canvas" class="absolute top-0 left-0 w-full h-full outline-none z-10"></canvas>
74
 
75
- <div id="loading-overlay" class="glass-ui absolute inset-0 z-50 flex items-center justify-center opacity-0 pointer-events-none">
76
  <div class="spinner w-12 h-12 border-4 border-gray-600 rounded-full animate-spin"></div>
77
  </div>
78
  <div id="main-loader" class="absolute inset-0 bg-gray-900 flex flex-col items-center justify-center z-50 transition-opacity duration-500">
@@ -90,7 +87,7 @@
90
 
91
  <!-- Panels -->
92
  <aside id="model-panel" class="side-panel left glass-ui fixed top-0 left-0 h-full w-80 max-w-[80vw] z-30 flex flex-col">
93
- <div class="flex-shrink-0 flex justify-between items-center p-4 border-b border-gray-700">
94
  <h2 class="text-xl font-bold text-white">Models</h2>
95
  <div>
96
  <button id="add-model-btn" class="p-2 rounded-full hover:bg-blue-500/50" title="Add new model">
@@ -105,7 +102,7 @@
105
  </aside>
106
 
107
  <aside id="panorama-panel" class="side-panel right glass-ui fixed top-0 right-0 h-full w-80 max-w-[80vw] z-30 flex flex-col">
108
- <div class="flex-shrink-0 flex justify-between items-center p-4 border-b border-gray-700">
109
  <h2 class="text-xl font-bold text-white">Panoramas</h2>
110
  <div>
111
  <button id="add-panorama-btn" class="p-2 rounded-full hover:bg-blue-500/50" title="Add new panorama">
@@ -121,40 +118,39 @@
121
 
122
  <!-- Upload Modals -->
123
  <div id="upload-model-modal" class="modal-overlay fixed inset-0 z-40 hidden items-center justify-center">
124
- <div class="glass-ui p-6 rounded-lg w-full max-w-md mx-4">
125
- <h3 class="text-2xl font-bold mb-4">Upload New Model</h3>
126
- <div id="model-drop-zone" class="drop-zone p-6 rounded-lg text-center mb-4 cursor-pointer">
127
- <p class="text-gray-400">Drag & Drop .glb file here or</p>
128
- <button id="browse-model-btn" class="mt-2 bg-blue-600 px-4 py-2 rounded-md font-semibold hover:bg-blue-700">Browse</button>
129
  <input type="file" id="model-file-input" class="hidden" accept=".glb,.gltf">
130
  </div>
131
- <p class="text-center text-gray-400 my-2">OR</p>
132
  <input type="text" id="model-url-input" class="w-full bg-gray-900/50 p-2 rounded-md border-2 border-gray-600 focus:border-blue-500 outline-none" placeholder="Enter model URL (.glb, .gltf)">
133
- <div class="flex justify-end gap-3 mt-5">
134
- <button id="cancel-model-upload" class="bg-gray-600 px-4 py-2 rounded-md font-semibold hover:bg-gray-700">Cancel</button>
135
- <button id="load-model-url-btn" class="bg-blue-600 px-4 py-2 rounded-md font-semibold hover:bg-blue-700">Load</button>
136
  </div>
137
  </div>
138
  </div>
139
 
140
  <div id="upload-panorama-modal" class="modal-overlay fixed inset-0 z-40 hidden items-center justify-center">
141
- <div class="glass-ui p-6 rounded-lg w-full max-w-md mx-4">
142
- <h3 class="text-2xl font-bold mb-4">Upload New Panorama</h3>
143
- <div id="panorama-drop-zone" class="drop-zone p-6 rounded-lg text-center mb-4 cursor-pointer">
144
- <p class="text-gray-400">Drag & Drop image here or</p>
145
- <button id="browse-panorama-btn" class="mt-2 bg-blue-600 px-4 py-2 rounded-md font-semibold hover:bg-blue-700">Browse</button>
146
  <input type="file" id="panorama-file-input" class="hidden" accept="image/*">
147
  </div>
148
- <p class="text-center text-gray-400 my-2">OR</p>
149
  <input type="text" id="panorama-url-input" class="w-full bg-gray-900/50 p-2 rounded-md border-2 border-gray-600 focus:border-blue-500 outline-none" placeholder="Enter panorama image URL">
150
- <div class="flex justify-end gap-3 mt-5">
151
- <button id="cancel-panorama-upload" class="bg-gray-600 px-4 py-2 rounded-md font-semibold hover:bg-gray-700">Cancel</button>
152
- <button id="load-panorama-url-btn" class="bg-blue-600 px-4 py-2 rounded-md font-semibold hover:bg-blue-700">Load</button>
153
  </div>
154
  </div>
155
  </div>
156
 
157
-
158
  <script type="importmap">{ "imports": { "three": "https://unpkg.com/three@0.160.0/build/three.module.js", "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/" } }</script>
159
 
160
  <script type="module">
@@ -162,8 +158,8 @@
162
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
163
  import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
164
 
165
- const defaultPanoramaFiles = Array.from({ length: 10 }, (_, i) => `bg${i + 1}.jpg`);
166
- const defaultModelFiles = [ 'gerbera.glb', 'shahed1.glb', 'shahed2.glb', 'shahed3.glb', 'supercam.glb', 'zala.glb', 'beaver.glb' ];
167
 
168
  let scene, camera, renderer, controls;
169
  const loadedModels = new Map();
@@ -196,8 +192,8 @@
196
  setupUI();
197
  setupEventListeners();
198
 
199
- await setPanorama({ name: defaultPanoramaFiles[0], url: `/jpg/${defaultPanoramaFiles[0]}` });
200
- await loadAndSwitchModel({ name: defaultModelFiles[0], url: `/glb/${defaultModelFiles[0]}` });
201
 
202
  mainLoader.style.opacity = '0';
203
  setTimeout(() => mainLoader.style.display = 'none', 500);
@@ -205,26 +201,27 @@
205
  window.addEventListener('resize', onWindowResize);
206
  animate();
207
  }
208
-
209
- function setPanorama(panorama) {
210
- return new Promise(resolve => {
211
  showLoadingOverlay(true);
212
- textureLoader.load(panorama.url, texture => {
213
  texture.encoding = THREE.sRGBEncoding;
214
  texture.mapping = THREE.EquirectangularReflectionMapping;
215
  scene.background = texture;
216
  scene.environment = texture;
217
 
218
  document.querySelectorAll('#panorama-gallery .list-item').forEach(c => c.classList.remove('active'));
219
- const activeItem = document.querySelector(`[data-name="${panorama.name}"]`);
220
  if(activeItem) activeItem.classList.add('active');
221
 
222
  showLoadingOverlay(false);
223
  resolve();
224
  }, undefined, (err) => {
225
  console.error('Error loading panorama:', err);
226
- alert(`Error: Could not load panorama "${panorama.name}".`);
227
  showLoadingOverlay(false);
 
228
  });
229
  });
230
  }
@@ -237,7 +234,7 @@
237
  }
238
 
239
  function loadAndSwitchModel(modelData) {
240
- return new Promise(resolve => {
241
  if (loadedModels.has(modelData.name)) {
242
  switchActiveModel(modelData.name);
243
  resolve();
@@ -266,28 +263,32 @@
266
  console.error('Error loading model:', err);
267
  alert(`Error: Could not load model "${modelData.name}".`);
268
  showLoadingOverlay(false);
 
269
  });
270
  });
271
  }
272
 
273
  function createModelCard(modelData) {
274
  const container = document.createElement('div');
275
- container.className = 'list-item flex flex-col rounded-lg cursor-pointer overflow-hidden bg-gray-800/50';
276
  container.dataset.name = modelData.name;
277
  container.onclick = () => loadAndSwitchModel(modelData);
278
 
279
  const thumb = document.createElement('img');
280
- thumb.src = modelData.thumbnail || `glb/thumbnails/${modelData.name.replace('.glb', '.png')}`;
281
  thumb.alt = `Thumbnail for ${modelData.name}`;
282
- thumb.className = 'w-full h-auto aspect-square object-cover bg-gray-700';
283
- thumb.onerror = () => { thumb.src = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIiB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2E1YjRjYyIgc3Ryb2tlLXdpZHRoPSIwLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PHBhdGggZD0iTTUgMyBsIDE0IDAgYSBUbyAwIDAgMSAyIDIgTCAyMSAxOSBhIDIgMiAwIDAgMSAtMiAyIEwgNSAyMSBhIDIgMiAwIDAgMSAtMiAtMiBMIDMgNSBhIDIgMiAwIDAgMSAyIC0yIFoiPjwvcGF0aD48Y2lyY2xlIGN4PSIxMiIgY3k9IjEyIiByPSI0Ij48L2NpcmNsZT48L3N2Zz4='; };
 
 
 
284
 
285
  const nameWrapper = document.createElement('div');
286
- nameWrapper.className = 'p-2 w-full';
287
 
288
  const name = document.createElement('span');
289
  name.textContent = modelData.name.replace(/\.[^/.]+$/, "").replace(/(\d)/, ' $1').toUpperCase();
290
- name.className = 'text-white text-sm font-medium text-center block truncate';
291
 
292
  nameWrapper.appendChild(name);
293
  container.appendChild(thumb);
@@ -297,32 +298,39 @@
297
 
298
  function createPanoramaCard(panoData) {
299
  const container = document.createElement('div');
300
- container.className = 'list-item aspect-video rounded-lg overflow-hidden cursor-pointer bg-gray-800/50';
301
  container.dataset.name = panoData.name;
302
  container.onclick = () => setPanorama(panoData);
303
 
304
  const thumb = document.createElement('img');
305
- thumb.src = panoData.url;
306
  thumb.className = 'w-full h-full object-cover';
307
  thumb.alt = `Thumbnail for ${panoData.name}`;
308
- thumb.onerror = () => { thumb.classList.add('bg-gray-700'); }
309
 
310
  container.appendChild(thumb);
311
  document.getElementById('panorama-gallery').appendChild(container);
312
  }
313
 
314
  function setupUI() {
315
- defaultModelFiles.forEach(name => createModelCard({ name, url: `/glb/${name}` }));
316
- defaultPanoramaFiles.forEach(name => createPanoramaCard({ name, url: `/jpg/thumbnails/${name}` }));
 
 
 
 
 
 
 
 
 
 
317
  }
318
 
319
  function setupEventListeners() {
 
320
  const modelPanel = document.getElementById('model-panel');
321
  const panoramaPanel = document.getElementById('panorama-panel');
322
- const uploadModelModal = document.getElementById('upload-model-modal');
323
- const uploadPanoramaModal = document.getElementById('upload-panorama-modal');
324
-
325
- // Panel Controls
326
  const closeModelPanel = () => modelPanel.classList.remove('is-open');
327
  const closePanoramaPanel = () => panoramaPanel.classList.remove('is-open');
328
  document.getElementById('model-panel-trigger').addEventListener('click', e => { e.stopPropagation(); modelPanel.classList.toggle('is-open'); closePanoramaPanel(); });
@@ -331,76 +339,75 @@
331
  document.getElementById('close-panorama-panel').addEventListener('click', closePanoramaPanel);
332
  document.getElementById('bg-canvas').addEventListener('click', () => { closeModelPanel(); closePanoramaPanel(); });
333
 
334
- // Modal Controls
 
 
335
  document.getElementById('add-model-btn').addEventListener('click', () => uploadModelModal.style.display = 'flex');
336
  document.getElementById('add-panorama-btn').addEventListener('click', () => uploadPanoramaModal.style.display = 'flex');
337
- document.getElementById('cancel-model-upload').addEventListener('click', () => uploadModelModal.style.display = 'none');
338
- document.getElementById('cancel-panorama-upload').addEventListener('click', () => uploadPanoramaModal.style.display = 'none');
339
 
340
- // Model Upload
341
- document.getElementById('browse-model-btn').addEventListener('click', () => document.getElementById('model-file-input').click());
342
- document.getElementById('model-file-input').addEventListener('change', e => handleModelFile(e.target.files[0]));
343
- document.getElementById('load-model-url-btn').addEventListener('click', () => {
 
 
 
 
 
344
  const url = document.getElementById('model-url-input').value.trim();
345
- if(url) handleModelFile(url);
 
 
 
346
  });
347
- setupDragDrop('model-drop-zone', handleModelFile);
348
-
349
- // Panorama Upload
350
- document.getElementById('browse-panorama-btn').addEventListener('click', () => document.getElementById('panorama-file-input').click());
351
- document.getElementById('panorama-file-input').addEventListener('change', e => handlePanoramaFile(e.target.files[0]));
352
- document.getElementById('load-panorama-url-btn').addEventListener('click', () => {
353
  const url = document.getElementById('panorama-url-input').value.trim();
354
- if(url) handlePanoramaFile(url);
 
 
 
355
  });
356
- setupDragDrop('panorama-drop-zone', handlePanoramaFile);
 
 
 
357
  }
358
 
359
- function handleModelFile(file) {
360
- if(!file) return;
361
- const url = (typeof file === 'string') ? file : URL.createObjectURL(file);
362
- const name = (typeof file === 'string') ? file.split('/').pop() : file.name;
363
- const thumbnail = (typeof file === 'string') ? null : URL.createObjectURL(file);
364
- const modelData = { name, url, thumbnail };
365
 
366
  createModelCard(modelData);
367
- loadAndSwitchModel(modelData);
368
  document.getElementById('upload-model-modal').style.display = 'none';
 
369
  }
370
 
371
- function handlePanoramaFile(file) {
372
- if(!file) return;
373
- const url = (typeof file === 'string') ? file : URL.createObjectURL(file);
374
- const name = (typeof file === 'string') ? file.split('/').pop() : file.name;
375
- const panoData = { name, url };
 
376
 
377
  createPanoramaCard(panoData);
378
- setPanorama(panoData);
379
  document.getElementById('upload-panorama-modal').style.display = 'none';
 
380
  }
381
 
382
- function setupDragDrop(zoneId, callback) {
383
  const dropZone = document.getElementById(zoneId);
384
- dropZone.addEventListener('dragover', e => {
385
- e.preventDefault();
386
- dropZone.classList.add('drag-over');
387
- });
388
- dropZone.addEventListener('dragleave', e => {
389
- e.preventDefault();
390
- dropZone.classList.remove('drag-over');
391
- });
392
  dropZone.addEventListener('drop', e => {
393
  e.preventDefault();
394
  dropZone.classList.remove('drag-over');
395
  if(e.dataTransfer.files.length) {
396
- callback(e.dataTransfer.files[0]);
397
- }
398
- });
399
- dropZone.addEventListener('click', () => {
400
- if (zoneId.includes('model')) {
401
- document.getElementById('model-file-input').click();
402
- } else {
403
- document.getElementById('panorama-file-input').click();
404
  }
405
  });
406
  }
 
12
  body { background-color: #111827; }
13
 
14
  .glass-ui {
15
+ background-color: rgba(23, 31, 47, 0.75);
16
  backdrop-filter: blur(16px);
17
+ border: 1px solid rgba(255, 255, 255, 0.1);
18
  }
19
 
20
  .modal-overlay {
 
22
  backdrop-filter: blur(8px);
23
  }
24
 
 
 
 
25
  #loading-overlay .spinner {
26
  border-top-color: #3b82f6;
27
  }
 
39
  .panel-trigger:hover { background-color: rgba(59, 130, 246, 0.5); }
40
 
41
  .list-item {
42
+ transition: all 0.2s ease-in-out;
43
  border: 2px solid transparent;
44
  }
45
  .list-item:hover {
 
47
  transform: scale(1.03);
48
  }
49
  .list-item.active {
50
+ background-color: rgba(59, 130, 246, 0.2);
51
  border-color: #3b82f6;
52
  }
53
 
 
60
  transition: background-color 0.2s, border-color 0.2s;
61
  }
62
  .drop-zone.drag-over {
63
+ background-color: rgba(59, 130, 246, 0.15);
64
  border-color: #3b82f6;
65
  }
66
  </style>
 
69
 
70
  <canvas id="bg-canvas" class="absolute top-0 left-0 w-full h-full outline-none z-10"></canvas>
71
 
72
+ <div id="loading-overlay" class="glass-ui absolute inset-0 z-[60] flex items-center justify-center opacity-0 pointer-events-none transition-opacity duration-300">
73
  <div class="spinner w-12 h-12 border-4 border-gray-600 rounded-full animate-spin"></div>
74
  </div>
75
  <div id="main-loader" class="absolute inset-0 bg-gray-900 flex flex-col items-center justify-center z-50 transition-opacity duration-500">
 
87
 
88
  <!-- Panels -->
89
  <aside id="model-panel" class="side-panel left glass-ui fixed top-0 left-0 h-full w-80 max-w-[80vw] z-30 flex flex-col">
90
+ <div class="flex-shrink-0 flex justify-between items-center p-4 border-b border-white/10">
91
  <h2 class="text-xl font-bold text-white">Models</h2>
92
  <div>
93
  <button id="add-model-btn" class="p-2 rounded-full hover:bg-blue-500/50" title="Add new model">
 
102
  </aside>
103
 
104
  <aside id="panorama-panel" class="side-panel right glass-ui fixed top-0 right-0 h-full w-80 max-w-[80vw] z-30 flex flex-col">
105
+ <div class="flex-shrink-0 flex justify-between items-center p-4 border-b border-white/10">
106
  <h2 class="text-xl font-bold text-white">Panoramas</h2>
107
  <div>
108
  <button id="add-panorama-btn" class="p-2 rounded-full hover:bg-blue-500/50" title="Add new panorama">
 
118
 
119
  <!-- Upload Modals -->
120
  <div id="upload-model-modal" class="modal-overlay fixed inset-0 z-40 hidden items-center justify-center">
121
+ <div class="glass-ui p-6 rounded-lg w-full max-w-md mx-4 flex flex-col gap-4">
122
+ <h3 class="text-2xl font-bold text-center">Upload New Model</h3>
123
+ <div id="model-drop-zone" class="drop-zone p-6 rounded-lg text-center cursor-pointer">
124
+ <p class="text-gray-300 pointer-events-none">Drag & Drop .glb file</p>
125
+ <p class="text-gray-400 text-sm pointer-events-none">or click to browse</p>
126
  <input type="file" id="model-file-input" class="hidden" accept=".glb,.gltf">
127
  </div>
128
+ <div class="flex items-center text-gray-400"><hr class="flex-grow border-white/10"><span class="mx-2 text-sm">OR</span><hr class="flex-grow border-white/10"></div>
129
  <input type="text" id="model-url-input" class="w-full bg-gray-900/50 p-2 rounded-md border-2 border-gray-600 focus:border-blue-500 outline-none" placeholder="Enter model URL (.glb, .gltf)">
130
+ <div class="flex justify-end gap-3 mt-2">
131
+ <button onclick="document.getElementById('upload-model-modal').style.display='none'" class="bg-gray-600 px-4 py-2 rounded-md font-semibold hover:bg-gray-700">Cancel</button>
132
+ <button id="load-model-url-btn" class="bg-blue-600 px-4 py-2 rounded-md font-semibold hover:bg-blue-700 min-w-[80px]">Load</button>
133
  </div>
134
  </div>
135
  </div>
136
 
137
  <div id="upload-panorama-modal" class="modal-overlay fixed inset-0 z-40 hidden items-center justify-center">
138
+ <div class="glass-ui p-6 rounded-lg w-full max-w-md mx-4 flex flex-col gap-4">
139
+ <h3 class="text-2xl font-bold text-center">Upload New Panorama</h3>
140
+ <div id="panorama-drop-zone" class="drop-zone p-6 rounded-lg text-center cursor-pointer">
141
+ <p class="text-gray-300 pointer-events-none">Drag & Drop image</p>
142
+ <p class="text-gray-400 text-sm pointer-events-none">or click to browse</p>
143
  <input type="file" id="panorama-file-input" class="hidden" accept="image/*">
144
  </div>
145
+ <div class="flex items-center text-gray-400"><hr class="flex-grow border-white/10"><span class="mx-2 text-sm">OR</span><hr class="flex-grow border-white/10"></div>
146
  <input type="text" id="panorama-url-input" class="w-full bg-gray-900/50 p-2 rounded-md border-2 border-gray-600 focus:border-blue-500 outline-none" placeholder="Enter panorama image URL">
147
+ <div class="flex justify-end gap-3 mt-2">
148
+ <button onclick="document.getElementById('upload-panorama-modal').style.display='none'" class="bg-gray-600 px-4 py-2 rounded-md font-semibold hover:bg-gray-700">Cancel</button>
149
+ <button id="load-panorama-url-btn" class="bg-blue-600 px-4 py-2 rounded-md font-semibold hover:bg-blue-700 min-w-[80px]">Load</button>
150
  </div>
151
  </div>
152
  </div>
153
 
 
154
  <script type="importmap">{ "imports": { "three": "https://unpkg.com/three@0.160.0/build/three.module.js", "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/" } }</script>
155
 
156
  <script type="module">
 
158
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
159
  import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
160
 
161
+ const defaultPanoramaFiles = Array.from({ length: 10 }, (_, i) => ({ name: `bg${i + 1}.jpg`, url: `/jpg/bg${i + 1}.jpg`, thumb: `/jpg/thumbnails/bg${i + 1}.jpg` }));
162
+ const defaultModelFiles = [ 'gerbera.glb', 'shahed1.glb', 'shahed2.glb', 'shahed3.glb', 'supercam.glb', 'zala.glb', 'beaver.glb' ].map(name => ({ name, url: `/glb/${name}`, thumb: `/glb/thumbnails/${name.replace('.glb', '.png')}` }));
163
 
164
  let scene, camera, renderer, controls;
165
  const loadedModels = new Map();
 
192
  setupUI();
193
  setupEventListeners();
194
 
195
+ await setPanorama(defaultPanoramaFiles[0]);
196
+ await loadAndSwitchModel(defaultModelFiles[0]);
197
 
198
  mainLoader.style.opacity = '0';
199
  setTimeout(() => mainLoader.style.display = 'none', 500);
 
201
  window.addEventListener('resize', onWindowResize);
202
  animate();
203
  }
204
+
205
+ function setPanorama(panoData) {
206
+ return new Promise((resolve, reject) => {
207
  showLoadingOverlay(true);
208
+ textureLoader.load(panoData.url, texture => {
209
  texture.encoding = THREE.sRGBEncoding;
210
  texture.mapping = THREE.EquirectangularReflectionMapping;
211
  scene.background = texture;
212
  scene.environment = texture;
213
 
214
  document.querySelectorAll('#panorama-gallery .list-item').forEach(c => c.classList.remove('active'));
215
+ const activeItem = document.querySelector(`#panorama-gallery [data-name="${panoData.name}"]`);
216
  if(activeItem) activeItem.classList.add('active');
217
 
218
  showLoadingOverlay(false);
219
  resolve();
220
  }, undefined, (err) => {
221
  console.error('Error loading panorama:', err);
222
+ alert(`Error: Could not load panorama "${panoData.name}".`);
223
  showLoadingOverlay(false);
224
+ reject(err);
225
  });
226
  });
227
  }
 
234
  }
235
 
236
  function loadAndSwitchModel(modelData) {
237
+ return new Promise((resolve, reject) => {
238
  if (loadedModels.has(modelData.name)) {
239
  switchActiveModel(modelData.name);
240
  resolve();
 
263
  console.error('Error loading model:', err);
264
  alert(`Error: Could not load model "${modelData.name}".`);
265
  showLoadingOverlay(false);
266
+ reject(err);
267
  });
268
  });
269
  }
270
 
271
  function createModelCard(modelData) {
272
  const container = document.createElement('div');
273
+ container.className = 'list-item flex flex-col rounded-lg cursor-pointer overflow-hidden bg-white/5';
274
  container.dataset.name = modelData.name;
275
  container.onclick = () => loadAndSwitchModel(modelData);
276
 
277
  const thumb = document.createElement('img');
278
+ thumb.src = modelData.thumb;
279
  thumb.alt = `Thumbnail for ${modelData.name}`;
280
+ thumb.className = 'w-full h-auto aspect-square object-contain'; // Use object-contain for PNGs
281
+ thumb.onerror = () => {
282
+ thumb.src = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIiB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2E1YjRjYyIgc3Ryb2tlLXdpZHRoPSIwLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PHBhdGggZD0iTTUgMyBsIDE0IDAgYSBUbyAwIDAgMSAyIDIgTCAyMSAxOSBhIDIgMiAwIDAgMSAtMiAyIEwgNSAyMSBhIDIgMiAwIDAgMSAtMiAtMiBMIDMgNSBhIDIgMiAwIDAgMSAyIC0yIFoiPjwvcGF0aD48Y2lyY2xlIGN4PSIxMiIgY3k9IjEyIiByPSI0Ij48L2NpcmNsZT48L3N2Zz4=';
283
+ thumb.classList.add('bg-gray-800', 'p-4');
284
+ };
285
 
286
  const nameWrapper = document.createElement('div');
287
+ nameWrapper.className = 'p-2 w-full bg-black/20';
288
 
289
  const name = document.createElement('span');
290
  name.textContent = modelData.name.replace(/\.[^/.]+$/, "").replace(/(\d)/, ' $1').toUpperCase();
291
+ name.className = 'text-white text-xs font-medium text-center block truncate';
292
 
293
  nameWrapper.appendChild(name);
294
  container.appendChild(thumb);
 
298
 
299
  function createPanoramaCard(panoData) {
300
  const container = document.createElement('div');
301
+ container.className = 'list-item aspect-video rounded-lg overflow-hidden cursor-pointer bg-white/5';
302
  container.dataset.name = panoData.name;
303
  container.onclick = () => setPanorama(panoData);
304
 
305
  const thumb = document.createElement('img');
306
+ thumb.src = panoData.thumb;
307
  thumb.className = 'w-full h-full object-cover';
308
  thumb.alt = `Thumbnail for ${panoData.name}`;
309
+ thumb.onerror = () => { thumb.classList.add('bg-gray-800'); }
310
 
311
  container.appendChild(thumb);
312
  document.getElementById('panorama-gallery').appendChild(container);
313
  }
314
 
315
  function setupUI() {
316
+ defaultModelFiles.forEach(createModelCard);
317
+ defaultPanoramaFiles.forEach(createPanoramaCard);
318
+ }
319
+
320
+ function toggleButtonLoading(button, isLoading) {
321
+ if (isLoading) {
322
+ button.disabled = true;
323
+ button.innerHTML = `<svg class="animate-spin h-5 w-5 mx-auto" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>`;
324
+ } else {
325
+ button.disabled = false;
326
+ button.innerHTML = 'Load';
327
+ }
328
  }
329
 
330
  function setupEventListeners() {
331
+ // Panels
332
  const modelPanel = document.getElementById('model-panel');
333
  const panoramaPanel = document.getElementById('panorama-panel');
 
 
 
 
334
  const closeModelPanel = () => modelPanel.classList.remove('is-open');
335
  const closePanoramaPanel = () => panoramaPanel.classList.remove('is-open');
336
  document.getElementById('model-panel-trigger').addEventListener('click', e => { e.stopPropagation(); modelPanel.classList.toggle('is-open'); closePanoramaPanel(); });
 
339
  document.getElementById('close-panorama-panel').addEventListener('click', closePanoramaPanel);
340
  document.getElementById('bg-canvas').addEventListener('click', () => { closeModelPanel(); closePanoramaPanel(); });
341
 
342
+ // Modals
343
+ const uploadModelModal = document.getElementById('upload-model-modal');
344
+ const uploadPanoramaModal = document.getElementById('upload-panorama-modal');
345
  document.getElementById('add-model-btn').addEventListener('click', () => uploadModelModal.style.display = 'flex');
346
  document.getElementById('add-panorama-btn').addEventListener('click', () => uploadPanoramaModal.style.display = 'flex');
 
 
347
 
348
+ // File Handling
349
+ const modelFileInput = document.getElementById('model-file-input');
350
+ const panoramaFileInput = document.getElementById('panorama-file-input');
351
+ modelFileInput.addEventListener('change', e => handleModelFile(e.target.files[0]));
352
+ panoramaFileInput.addEventListener('change', e => handlePanoramaFile(e.target.files[0]));
353
+
354
+ // URL Loading
355
+ const loadModelUrlBtn = document.getElementById('load-model-url-btn');
356
+ loadModelUrlBtn.addEventListener('click', () => {
357
  const url = document.getElementById('model-url-input').value.trim();
358
+ if(url) {
359
+ toggleButtonLoading(loadModelUrlBtn, true);
360
+ handleModelFile(url).finally(() => toggleButtonLoading(loadModelUrlBtn, false));
361
+ }
362
  });
363
+ const loadPanoramaUrlBtn = document.getElementById('load-panorama-url-btn');
364
+ loadPanoramaUrlBtn.addEventListener('click', () => {
 
 
 
 
365
  const url = document.getElementById('panorama-url-input').value.trim();
366
+ if(url) {
367
+ toggleButtonLoading(loadPanoramaUrlBtn, true);
368
+ handlePanoramaFile(url).finally(() => toggleButtonLoading(loadPanoramaUrlBtn, false));
369
+ }
370
  });
371
+
372
+ // Drag and Drop
373
+ setupDragDrop('model-drop-zone', handleModelFile, modelFileInput);
374
+ setupDragDrop('panorama-drop-zone', handlePanoramaFile, panoramaFileInput);
375
  }
376
 
377
+ async function handleModelFile(fileOrUrl) {
378
+ if(!fileOrUrl) return;
379
+ const isUrl = typeof fileOrUrl === 'string';
380
+ const url = isUrl ? fileOrUrl : URL.createObjectURL(fileOrUrl);
381
+ const name = isUrl ? fileOrUrl.split('/').pop() : fileOrUrl.name;
382
+ const modelData = { name, url, thumb: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIiB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2E1YjRjYyIgc3Ryb2tlLXdpZHRoPSIwLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PHBhdGggZD0iTTUgMyBsIDE0IDAgYSBUbyAwIDAgMSAyIDIgTCAyMSAxOSBhIDIgMiAwIDAgMSAtMiAyIEwgNSAyMSBhIDIgMiAwIDAgMSAtMiAtMiBMIDMgNSBhIDIgMiAwIDAgMSAyIC0yIFoiPjwvcGF0aD48Y2lyY2xlIGN4PSIxMiIgY3k9IjEyIiByPSI0Ij48L2NpcmNsZT48L3N2Zz4=' }; // Placeholder thumb
383
 
384
  createModelCard(modelData);
 
385
  document.getElementById('upload-model-modal').style.display = 'none';
386
+ await loadAndSwitchModel(modelData);
387
  }
388
 
389
+ async function handlePanoramaFile(fileOrUrl) {
390
+ if(!fileOrUrl) return;
391
+ const isUrl = typeof fileOrUrl === 'string';
392
+ const url = isUrl ? fileOrUrl : URL.createObjectURL(fileOrUrl);
393
+ const name = isUrl ? fileOrUrl.split('/').pop() : fileOrUrl.name;
394
+ const panoData = { name, url, thumb: url };
395
 
396
  createPanoramaCard(panoData);
 
397
  document.getElementById('upload-panorama-modal').style.display = 'none';
398
+ await setPanorama(panoData);
399
  }
400
 
401
+ function setupDragDrop(zoneId, fileHandler, fileInput) {
402
  const dropZone = document.getElementById(zoneId);
403
+ dropZone.addEventListener('click', () => fileInput.click());
404
+ dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); });
405
+ dropZone.addEventListener('dragleave', e => { e.preventDefault(); dropZone.classList.remove('drag-over'); });
 
 
 
 
 
406
  dropZone.addEventListener('drop', e => {
407
  e.preventDefault();
408
  dropZone.classList.remove('drag-over');
409
  if(e.dataTransfer.files.length) {
410
+ fileHandler(e.dataTransfer.files[0]);
 
 
 
 
 
 
 
411
  }
412
  });
413
  }