aaurelions commited on
Commit
026a19d
·
verified ·
1 Parent(s): 32b887e

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +217 -81
index.html CHANGED
@@ -12,11 +12,16 @@
12
  body { background-color: #111827; }
13
 
14
  .glass-ui {
15
- background-color: rgba(17, 24, 39, 0.75);
16
  backdrop-filter: blur(16px);
17
  border-color: rgba(255, 255, 255, 0.1);
18
  }
19
 
 
 
 
 
 
20
  #loading-overlay {
21
  transition: opacity 0.3s ease-in-out;
22
  }
@@ -37,7 +42,7 @@
37
  .panel-trigger:hover { background-color: rgba(59, 130, 246, 0.5); }
38
 
39
  .list-item {
40
- transition: background-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
41
  border: 2px solid transparent;
42
  }
43
  .list-item:hover {
@@ -52,13 +57,22 @@
52
  .panel-content::-webkit-scrollbar { width: 6px; }
53
  .panel-content::-webkit-scrollbar-track { background: transparent; }
54
  .panel-content::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.2); border-radius: 10px; }
 
 
 
 
 
 
 
 
 
55
  </style>
56
  </head>
57
  <body class="text-gray-100 select-none overflow-hidden">
58
 
59
  <canvas id="bg-canvas" class="absolute top-0 left-0 w-full h-full outline-none z-10"></canvas>
60
 
61
- <div id="loading-overlay" class="glass-ui absolute inset-0 z-40 flex items-center justify-center opacity-0 pointer-events-none">
62
  <div class="spinner w-12 h-12 border-4 border-gray-600 rounded-full animate-spin"></div>
63
  </div>
64
  <div id="main-loader" class="absolute inset-0 bg-gray-900 flex flex-col items-center justify-center z-50 transition-opacity duration-500">
@@ -74,30 +88,73 @@
74
  <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
75
  </button>
76
 
77
- <aside id="model-panel" class="side-panel left glass-ui fixed top-0 left-0 h-full w-80 max-w-[80vw] z-30">
78
- <div class="h-full flex flex-col">
79
- <div class="flex-shrink-0 flex justify-between items-center p-4 border-b border-gray-700">
80
- <h2 class="text-xl font-bold text-white">Models</h2>
 
 
 
 
81
  <button id="close-model-panel" class="p-2 rounded-full hover:bg-gray-700">
82
  <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="M6 18L18 6M6 6l12 12" /></svg>
83
  </button>
84
  </div>
85
- <div id="model-selector" class="panel-content flex-grow overflow-y-auto p-4 grid grid-cols-2 gap-4"></div>
86
  </div>
 
87
  </aside>
88
 
89
- <aside id="panorama-panel" class="side-panel right glass-ui fixed top-0 right-0 h-full w-80 max-w-[80vw] z-30">
90
- <div class="h-full flex flex-col">
91
- <div class="flex-shrink-0 flex justify-between items-center p-4 border-b border-gray-700">
92
- <h2 class="text-xl font-bold text-white">Panoramas</h2>
93
- <button id="close-panorama-panel" class="p-2 rounded-full hover:bg-gray-700">
 
 
 
94
  <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="M6 18L18 6M6 6l12 12" /></svg>
95
  </button>
96
  </div>
97
- <div id="panorama-gallery" class="panel-content flex-grow overflow-y-auto p-4 grid grid-cols-2 gap-4"></div>
98
  </div>
 
99
  </aside>
100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  <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>
102
 
103
  <script type="module">
@@ -105,8 +162,8 @@
105
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
106
  import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
107
 
108
- const panoramaFiles = Array.from({ length: 10 }, (_, i) => `bg${i + 1}.jpg`);
109
- const modelFiles = [ 'gerbera.glb', 'shahed1.glb', 'shahed2.glb', 'shahed3.glb', 'supercam.glb', 'zala.glb', 'beaver.glb' ];
110
 
111
  let scene, camera, renderer, controls;
112
  const loadedModels = new Map();
@@ -139,8 +196,8 @@
139
  setupUI();
140
  setupEventListeners();
141
 
142
- await setPanorama(panoramaFiles[0]);
143
- await loadAndSwitchModel(modelFiles[0]);
144
 
145
  mainLoader.style.opacity = '0';
146
  setTimeout(() => mainLoader.style.display = 'none', 500);
@@ -149,22 +206,24 @@
149
  animate();
150
  }
151
 
152
- function setPanorama(imageName) {
153
  return new Promise(resolve => {
154
  showLoadingOverlay(true);
155
- textureLoader.load(`/jpg/${imageName}`, texture => {
156
  texture.encoding = THREE.sRGBEncoding;
157
  texture.mapping = THREE.EquirectangularReflectionMapping;
158
  scene.background = texture;
159
  scene.environment = texture;
160
 
161
  document.querySelectorAll('#panorama-gallery .list-item').forEach(c => c.classList.remove('active'));
162
- document.querySelector(`[data-image="${imageName}"]`).classList.add('active');
163
-
 
164
  showLoadingOverlay(false);
165
  resolve();
166
- }, undefined, () => {
167
- alert(`Error: Could not load panorama "${imageName}".`);
 
168
  showLoadingOverlay(false);
169
  });
170
  });
@@ -174,99 +233,176 @@
174
  scene.children.forEach(child => {
175
  if (child.isGroup) child.visible = (child.name === modelName);
176
  });
177
- document.querySelectorAll('#model-selector .list-item').forEach(b => b.classList.toggle('active', b.dataset.model === modelName));
178
  }
179
 
180
- function loadAndSwitchModel(modelName) {
181
  return new Promise(resolve => {
182
- if (loadedModels.has(modelName)) {
183
- switchActiveModel(modelName);
184
  resolve();
185
  return;
186
  }
187
  showLoadingOverlay(true);
188
- gltfLoader.load(`/glb/${modelName}`, gltf => {
189
  const model = gltf.scene;
190
- model.name = modelName;
 
191
  const box = new THREE.Box3().setFromObject(model);
192
  const center = box.getCenter(new THREE.Vector3());
193
- model.position.sub(center);
194
  const size = box.getSize(new THREE.Vector3());
195
  const maxDim = Math.max(size.x, size.y, size.z);
196
  const scale = 3.0 / maxDim;
197
  model.scale.set(scale, scale, scale);
198
  model.position.y += size.y * scale / 2;
 
199
  scene.add(model);
200
- loadedModels.set(modelName, model);
201
- switchActiveModel(modelName);
202
  showLoadingOverlay(false);
203
  resolve();
204
- }, undefined, () => {
205
- alert(`Error: Could not load model "${modelName}".`);
 
206
  showLoadingOverlay(false);
207
  });
208
  });
209
  }
 
 
 
 
 
 
210
 
211
- function setupUI() {
212
- const modelSelector = document.getElementById('model-selector');
213
- modelFiles.forEach(fileName => {
214
- const container = document.createElement('div');
215
- container.className = 'list-item flex flex-col rounded-lg cursor-pointer overflow-hidden bg-gray-800/50';
216
- container.dataset.model = fileName;
217
- container.onclick = () => loadAndSwitchModel(fileName);
218
-
219
- const thumb = document.createElement('img');
220
- thumb.src = `/glb/thumbnails/${fileName.replace('.glb', '.png')}`;
221
- thumb.alt = `Thumbnail for ${fileName}`;
222
- thumb.className = 'w-full h-auto aspect-square object-cover';
223
- thumb.onerror = () => { thumb.classList.add('bg-gray-700'); } // Add bg on error
224
-
225
- const nameWrapper = document.createElement('div');
226
- nameWrapper.className = 'p-2 w-full';
227
-
228
- const name = document.createElement('span');
229
- name.textContent = fileName.replace('.glb', '').replace(/(\d)/, ' $1').toUpperCase();
230
- name.className = 'text-white text-sm font-medium text-center block';
231
-
232
- nameWrapper.appendChild(name);
233
- container.appendChild(thumb);
234
- container.appendChild(nameWrapper);
235
- modelSelector.appendChild(container);
236
- });
237
 
238
- const panoramaGallery = document.getElementById('panorama-gallery');
239
- panoramaFiles.forEach(fileName => {
240
- const container = document.createElement('div');
241
- container.className = 'list-item aspect-video rounded-lg overflow-hidden cursor-pointer bg-gray-800/50';
242
- container.dataset.image = fileName;
243
- container.onclick = () => setPanorama(fileName);
244
-
245
- const thumb = document.createElement('img');
246
- thumb.src = `/jpg/thumbnails/${fileName}`;
247
- thumb.className = 'w-full h-full object-cover';
248
- thumb.alt = `Thumbnail for ${fileName}`;
249
- thumb.onerror = () => { thumb.classList.add('bg-gray-700'); } // Add bg on error
250
-
251
- container.appendChild(thumb);
252
- panoramaGallery.appendChild(container);
253
- });
 
 
 
 
 
 
 
 
 
254
  }
255
 
256
  function setupEventListeners() {
257
  const modelPanel = document.getElementById('model-panel');
258
  const panoramaPanel = document.getElementById('panorama-panel');
259
-
 
 
 
260
  const closeModelPanel = () => modelPanel.classList.remove('is-open');
261
  const closePanoramaPanel = () => panoramaPanel.classList.remove('is-open');
262
-
263
  document.getElementById('model-panel-trigger').addEventListener('click', e => { e.stopPropagation(); modelPanel.classList.toggle('is-open'); closePanoramaPanel(); });
264
  document.getElementById('panorama-panel-trigger').addEventListener('click', e => { e.stopPropagation(); panoramaPanel.classList.toggle('is-open'); closeModelPanel(); });
265
-
266
  document.getElementById('close-model-panel').addEventListener('click', closeModelPanel);
267
  document.getElementById('close-panorama-panel').addEventListener('click', closePanoramaPanel);
268
-
269
  document.getElementById('bg-canvas').addEventListener('click', () => { closeModelPanel(); closePanoramaPanel(); });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  }
271
 
272
  function showLoadingOverlay(show) {
 
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 {
21
+ background-color: rgba(0, 0, 0, 0.6);
22
+ backdrop-filter: blur(8px);
23
+ }
24
+
25
  #loading-overlay {
26
  transition: opacity 0.3s ease-in-out;
27
  }
 
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 {
 
57
  .panel-content::-webkit-scrollbar { width: 6px; }
58
  .panel-content::-webkit-scrollbar-track { background: transparent; }
59
  .panel-content::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.2); border-radius: 10px; }
60
+
61
+ .drop-zone {
62
+ border: 2px dashed #4b5563;
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>
70
  </head>
71
  <body class="text-gray-100 select-none overflow-hidden">
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">
 
88
  <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
89
  </button>
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">
97
+ <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="M12 4v16m8-8H4" /></svg>
98
+ </button>
99
  <button id="close-model-panel" class="p-2 rounded-full hover:bg-gray-700">
100
  <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="M6 18L18 6M6 6l12 12" /></svg>
101
  </button>
102
  </div>
 
103
  </div>
104
+ <div id="model-selector" class="panel-content flex-grow overflow-y-auto p-4 grid grid-cols-2 gap-4"></div>
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">
112
+ <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="M12 4v16m8-8H4" /></svg>
113
+ </button>
114
+ <button id="close-panorama-panel" class="p-2 rounded-full hover:bg-gray-700">
115
  <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="M6 18L18 6M6 6l12 12" /></svg>
116
  </button>
117
  </div>
 
118
  </div>
119
+ <div id="panorama-gallery" class="panel-content flex-grow overflow-y-auto p-4 grid grid-cols-2 gap-4"></div>
120
  </aside>
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
  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
  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);
 
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
  });
 
233
  scene.children.forEach(child => {
234
  if (child.isGroup) child.visible = (child.name === modelName);
235
  });
236
+ document.querySelectorAll('#model-selector .list-item').forEach(b => b.classList.toggle('active', b.dataset.name === modelName));
237
  }
238
 
239
+ function loadAndSwitchModel(modelData) {
240
  return new Promise(resolve => {
241
+ if (loadedModels.has(modelData.name)) {
242
+ switchActiveModel(modelData.name);
243
  resolve();
244
  return;
245
  }
246
  showLoadingOverlay(true);
247
+ gltfLoader.load(modelData.url, gltf => {
248
  const model = gltf.scene;
249
+ model.name = modelData.name;
250
+
251
  const box = new THREE.Box3().setFromObject(model);
252
  const center = box.getCenter(new THREE.Vector3());
253
+ model.position.sub(center);
254
  const size = box.getSize(new THREE.Vector3());
255
  const maxDim = Math.max(size.x, size.y, size.z);
256
  const scale = 3.0 / maxDim;
257
  model.scale.set(scale, scale, scale);
258
  model.position.y += size.y * scale / 2;
259
+
260
  scene.add(model);
261
+ loadedModels.set(modelData.name, model);
262
+ switchActiveModel(modelData.name);
263
  showLoadingOverlay(false);
264
  resolve();
265
+ }, undefined, (err) => {
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);
294
+ container.appendChild(nameWrapper);
295
+ document.getElementById('model-selector').appendChild(container);
296
+ }
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(); });
329
  document.getElementById('panorama-panel-trigger').addEventListener('click', e => { e.stopPropagation(); panoramaPanel.classList.toggle('is-open'); closeModelPanel(); });
 
330
  document.getElementById('close-model-panel').addEventListener('click', closeModelPanel);
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
  }
407
 
408
  function showLoadingOverlay(show) {