senangwebs-tour 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2853 @@
1
+ (function () {
2
+ 'use strict';
3
+
4
+ // Utility Functions
5
+
6
+ /**
7
+ * Generate a unique ID
8
+ */
9
+ function generateId(prefix = 'id') {
10
+ return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
11
+ }
12
+
13
+ /**
14
+ * Sanitize string for use as ID
15
+ */
16
+ function sanitizeId$1(str) {
17
+ return str.toLowerCase()
18
+ .replace(/[^a-z0-9]+/g, '_')
19
+ .replace(/^_+|_+$/g, '');
20
+ }
21
+
22
+ /**
23
+ * Generate thumbnail from image file
24
+ */
25
+ async function generateThumbnail(file, maxWidth = 200, maxHeight = 150) {
26
+ return new Promise((resolve, reject) => {
27
+ const reader = new FileReader();
28
+
29
+ reader.onload = (e) => {
30
+ const img = new Image();
31
+
32
+ img.onload = () => {
33
+ const canvas = document.createElement('canvas');
34
+ const ctx = canvas.getContext('2d');
35
+
36
+ let width = img.width;
37
+ let height = img.height;
38
+
39
+ if (width > height) {
40
+ if (width > maxWidth) {
41
+ height *= maxWidth / width;
42
+ width = maxWidth;
43
+ }
44
+ } else {
45
+ if (height > maxHeight) {
46
+ width *= maxHeight / height;
47
+ height = maxHeight;
48
+ }
49
+ }
50
+
51
+ canvas.width = width;
52
+ canvas.height = height;
53
+ ctx.drawImage(img, 0, 0, width, height);
54
+
55
+ resolve(canvas.toDataURL('image/jpeg', 0.7));
56
+ };
57
+
58
+ img.onerror = reject;
59
+ img.src = e.target.result;
60
+ };
61
+
62
+ reader.onerror = reject;
63
+ reader.readAsDataURL(file);
64
+ });
65
+ }
66
+
67
+ /**
68
+ * Load image file as data URL
69
+ */
70
+ async function loadImageAsDataUrl(file) {
71
+ return new Promise((resolve, reject) => {
72
+ const reader = new FileReader();
73
+ reader.onload = (e) => resolve(e.target.result);
74
+ reader.onerror = reject;
75
+ reader.readAsDataURL(file);
76
+ });
77
+ }
78
+
79
+ /**
80
+ * Show toast notification
81
+ */
82
+ function showToast$1(message, type = 'info', duration = 3000) {
83
+ const toast = document.getElementById('toast');
84
+ toast.textContent = message;
85
+ toast.className = `toast ${type}`;
86
+ toast.classList.add('show');
87
+
88
+ setTimeout(() => {
89
+ toast.classList.remove('show');
90
+ }, duration);
91
+ }
92
+
93
+ /**
94
+ * Show modal
95
+ */
96
+ function showModal(modalId) {
97
+ const modal = document.getElementById(modalId);
98
+ if (modal) {
99
+ modal.classList.add('show');
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Hide modal
105
+ */
106
+ function hideModal$1(modalId) {
107
+ const modal = document.getElementById(modalId);
108
+ if (modal) {
109
+ modal.classList.remove('show');
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Format file size
115
+ */
116
+ function formatFileSize(bytes) {
117
+ if (bytes === 0) return '0 Bytes';
118
+ const k = 1024;
119
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
120
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
121
+ return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
122
+ }
123
+
124
+ /**
125
+ * Download text as file
126
+ */
127
+ function downloadTextAsFile(text, filename) {
128
+ const blob = new Blob([text], { type: 'text/plain' });
129
+ const url = URL.createObjectURL(blob);
130
+ const a = document.createElement('a');
131
+ a.href = url;
132
+ a.download = filename;
133
+ document.body.appendChild(a);
134
+ a.click();
135
+ document.body.removeChild(a);
136
+ URL.revokeObjectURL(url);
137
+ }
138
+
139
+ /**
140
+ * Debounce function
141
+ */
142
+ function debounce(func, wait) {
143
+ let timeout;
144
+ return function executedFunction(...args) {
145
+ const later = () => {
146
+ clearTimeout(timeout);
147
+ func(...args);
148
+ };
149
+ clearTimeout(timeout);
150
+ timeout = setTimeout(later, wait);
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Convert position object to string
156
+ */
157
+ function positionToString(pos) {
158
+ return `${pos.x.toFixed(1)} ${pos.y.toFixed(1)} ${pos.z.toFixed(1)}`;
159
+ }
160
+
161
+ /**
162
+ * Parse position string to object
163
+ */
164
+ function parsePosition(str) {
165
+ const parts = str.split(' ').map(Number);
166
+ return { x: parts[0] || 0, y: parts[1] || 0, z: parts[2] || 0 };
167
+ }
168
+
169
+ /**
170
+ * Validate email
171
+ */
172
+ function isValidEmail(email) {
173
+ const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
174
+ return re.test(email);
175
+ }
176
+
177
+ /**
178
+ * Deep clone object
179
+ */
180
+ function deepClone$1(obj) {
181
+ return JSON.parse(JSON.stringify(obj));
182
+ }
183
+
184
+ var utils = /*#__PURE__*/Object.freeze({
185
+ __proto__: null,
186
+ debounce: debounce,
187
+ deepClone: deepClone$1,
188
+ downloadTextAsFile: downloadTextAsFile,
189
+ formatFileSize: formatFileSize,
190
+ generateId: generateId,
191
+ generateThumbnail: generateThumbnail,
192
+ hideModal: hideModal$1,
193
+ isValidEmail: isValidEmail,
194
+ loadImageAsDataUrl: loadImageAsDataUrl,
195
+ parsePosition: parsePosition,
196
+ positionToString: positionToString,
197
+ sanitizeId: sanitizeId$1,
198
+ showModal: showModal,
199
+ showToast: showToast$1
200
+ });
201
+
202
+ // Storage Manager - Handles LocalStorage operations
203
+
204
+ let ProjectStorageManager$1 = class ProjectStorageManager {
205
+ constructor() {
206
+ this.storageKey = "swt_project";
207
+ this.autoSaveInterval = null;
208
+ this.autoSaveDelay = 30000; // 30 seconds
209
+ }
210
+
211
+ /**
212
+ * Save project to localStorage
213
+ */
214
+ saveProject(projectData) {
215
+ try {
216
+ const json = JSON.stringify(projectData);
217
+ localStorage.setItem(this.storageKey, json);
218
+ localStorage.setItem(
219
+ this.storageKey + "_lastSaved",
220
+ new Date().toISOString()
221
+ );
222
+ return true;
223
+ } catch (error) {
224
+ console.error("Failed to save project:", error);
225
+ if (error.name === "QuotaExceededError") {
226
+ showToast$1("Storage quota exceeded. Project too large!", "error");
227
+ }
228
+ return false;
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Load project from localStorage
234
+ */
235
+ loadProject() {
236
+ try {
237
+ const json = localStorage.getItem(this.storageKey);
238
+ if (json) {
239
+ const projectData = JSON.parse(json);
240
+
241
+ // Validate and migrate data if needed
242
+ if (!this.validateProjectData(projectData)) {
243
+ console.error("Invalid project data structure");
244
+ return null;
245
+ }
246
+
247
+ return projectData;
248
+ }
249
+ return null;
250
+ } catch (error) {
251
+ console.error("Failed to load project:", error);
252
+ showToast$1("Failed to load project", "error");
253
+ return null;
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Validate project data structure
259
+ */
260
+ validateProjectData(projectData) {
261
+ if (!projectData || typeof projectData !== "object") {
262
+ return false;
263
+ }
264
+
265
+ // Check if scenes array exists and is valid
266
+ if (!projectData.scenes || !Array.isArray(projectData.scenes)) {
267
+ return false;
268
+ }
269
+
270
+ // Validate each scene has required properties
271
+ for (const scene of projectData.scenes) {
272
+ if (!scene || typeof scene !== "object") {
273
+ return false;
274
+ }
275
+
276
+ // Required scene properties
277
+ if (!scene.id || typeof scene.id !== "string") {
278
+ return false;
279
+ }
280
+
281
+ // imageUrl is required for scenes to be valid
282
+ if (!scene.imageUrl || typeof scene.imageUrl !== "string") {
283
+ return false;
284
+ }
285
+
286
+ // Hotspots array should exist (can be empty)
287
+ if (!Array.isArray(scene.hotspots)) {
288
+ scene.hotspots = []; // Auto-fix missing hotspots array
289
+ }
290
+ }
291
+
292
+ // Ensure config exists
293
+ if (!projectData.config || typeof projectData.config !== "object") {
294
+ projectData.config = { title: "My Virtual Tour" };
295
+ }
296
+
297
+ return true;
298
+ }
299
+
300
+ /**
301
+ * Clear project from localStorage
302
+ */
303
+ clearProject() {
304
+ try {
305
+ localStorage.removeItem(this.storageKey);
306
+ localStorage.removeItem(this.storageKey + "_lastSaved");
307
+ return true;
308
+ } catch (error) {
309
+ console.error("Failed to clear project:", error);
310
+ return false;
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Check if project exists in localStorage
316
+ */
317
+ hasProject() {
318
+ return localStorage.getItem(this.storageKey) !== null;
319
+ }
320
+
321
+ /**
322
+ * Get last saved date
323
+ */
324
+ getLastSavedDate() {
325
+ const dateStr = localStorage.getItem(this.storageKey + "_lastSaved");
326
+ return dateStr ? new Date(dateStr) : null;
327
+ }
328
+
329
+ /**
330
+ * Start auto-save
331
+ */
332
+ startAutoSave(callback) {
333
+ this.stopAutoSave();
334
+ this.autoSaveInterval = setInterval(() => {
335
+ callback();
336
+ }, this.autoSaveDelay);
337
+ }
338
+
339
+ /**
340
+ * Stop auto-save
341
+ */
342
+ stopAutoSave() {
343
+ if (this.autoSaveInterval) {
344
+ clearInterval(this.autoSaveInterval);
345
+ this.autoSaveInterval = null;
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Export project to file
351
+ */
352
+ exportToFile(projectData, filename = "tour.json") {
353
+ try {
354
+ const json = JSON.stringify(projectData, null, 2);
355
+ downloadTextAsFile(json, filename);
356
+ return true;
357
+ } catch (error) {
358
+ console.error("Failed to export project:", error);
359
+ showToast$1("Failed to export project", "error");
360
+ return false;
361
+ }
362
+ }
363
+
364
+ /**
365
+ * Import project from file
366
+ */
367
+ async importFromFile(file) {
368
+ return new Promise((resolve, reject) => {
369
+ const reader = new FileReader();
370
+
371
+ reader.onload = (e) => {
372
+ try {
373
+ const projectData = JSON.parse(e.target.result);
374
+ resolve(projectData);
375
+ } catch (error) {
376
+ console.error("Failed to parse project file:", error);
377
+ showToast$1("Invalid project file", "error");
378
+ reject(error);
379
+ }
380
+ };
381
+
382
+ reader.onerror = () => {
383
+ console.error("Failed to read file:", reader.error);
384
+ showToast$1("Failed to read file", "error");
385
+ reject(reader.error);
386
+ };
387
+
388
+ reader.readAsText(file);
389
+ });
390
+ }
391
+ };
392
+
393
+ // Scene Manager - Handles scene operations
394
+
395
+ let SceneManagerEditor$1 = class SceneManagerEditor {
396
+ constructor(editor) {
397
+ this.editor = editor;
398
+ this.scenes = [];
399
+ this.currentSceneIndex = -1;
400
+ }
401
+
402
+ /**
403
+ * Add new scene
404
+ */
405
+ async addScene(file) {
406
+ try {
407
+ // Generate thumbnail
408
+ const thumbnail = await generateThumbnail(file);
409
+
410
+ // Load full image
411
+ const imageDataUrl = await loadImageAsDataUrl(file);
412
+
413
+ const scene = {
414
+ id: sanitizeId$1(file.name.replace(/\.[^/.]+$/, "")),
415
+ name: file.name.replace(/\.[^/.]+$/, ""),
416
+ imageUrl: imageDataUrl,
417
+ thumbnail: thumbnail,
418
+ hotspots: [],
419
+ };
420
+
421
+ this.scenes.push(scene);
422
+ this.currentSceneIndex = this.scenes.length - 1;
423
+ showToast$1(`Scene "${scene.name}" added successfully`, "success");
424
+ return scene;
425
+ } catch (error) {
426
+ console.error("Failed to add scene:", error);
427
+ showToast$1("Failed to add scene", "error");
428
+ return null;
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Remove scene by index
434
+ */
435
+ removeScene(index) {
436
+ if (index >= 0 && index < this.scenes.length) {
437
+ const scene = this.scenes[index];
438
+
439
+ // Confirm deletion
440
+ if (!confirm(`Are you sure you want to delete scene "${scene.name}"?`)) {
441
+ return false;
442
+ }
443
+
444
+ this.scenes.splice(index, 1);
445
+
446
+ // Update current scene index
447
+ if (this.currentSceneIndex === index) {
448
+ this.currentSceneIndex = Math.min(
449
+ this.currentSceneIndex,
450
+ this.scenes.length - 1
451
+ );
452
+ } else if (this.currentSceneIndex > index) {
453
+ this.currentSceneIndex--;
454
+ }
455
+
456
+ showToast$1(`Scene "${scene.name}" removed`, "success");
457
+ return true;
458
+ }
459
+ return false;
460
+ }
461
+
462
+ /**
463
+ * Get scene by index
464
+ */
465
+ getScene(index) {
466
+ return this.scenes[index] || null;
467
+ }
468
+
469
+ /**
470
+ * Get scene by ID
471
+ */
472
+ getSceneById(id) {
473
+ return this.scenes.find((s) => s.id === id) || null;
474
+ }
475
+
476
+ /**
477
+ * Update scene property
478
+ */
479
+ updateScene(index, property, value) {
480
+ if (index >= 0 && index < this.scenes.length) {
481
+ this.scenes[index][property] = value;
482
+
483
+ // If updating ID, update all hotspot target references
484
+ if (property === "id") {
485
+ this.scenes.forEach((scene) => {
486
+ scene.hotspots.forEach((hotspot) => {
487
+ if (hotspot.targetSceneId === this.scenes[index].id) {
488
+ hotspot.targetSceneId = value;
489
+ }
490
+ });
491
+ });
492
+ }
493
+
494
+ return true;
495
+ }
496
+ return false;
497
+ }
498
+
499
+ /**
500
+ * Reorder scenes
501
+ */
502
+ reorderScenes(fromIndex, toIndex) {
503
+ if (
504
+ fromIndex >= 0 &&
505
+ fromIndex < this.scenes.length &&
506
+ toIndex >= 0 &&
507
+ toIndex < this.scenes.length
508
+ ) {
509
+ const scene = this.scenes.splice(fromIndex, 1)[0];
510
+ this.scenes.splice(toIndex, 0, scene);
511
+
512
+ // Update current scene index
513
+ if (this.currentSceneIndex === fromIndex) {
514
+ this.currentSceneIndex = toIndex;
515
+ } else if (
516
+ fromIndex < this.currentSceneIndex &&
517
+ toIndex >= this.currentSceneIndex
518
+ ) {
519
+ this.currentSceneIndex--;
520
+ } else if (
521
+ fromIndex > this.currentSceneIndex &&
522
+ toIndex <= this.currentSceneIndex
523
+ ) {
524
+ this.currentSceneIndex++;
525
+ }
526
+
527
+ return true;
528
+ }
529
+ return false;
530
+ }
531
+
532
+ /**
533
+ * Get current scene
534
+ */
535
+ getCurrentScene() {
536
+ return this.getScene(this.currentSceneIndex);
537
+ }
538
+
539
+ /**
540
+ * Set current scene by index
541
+ */
542
+ setCurrentScene(index) {
543
+ if (index >= 0 && index < this.scenes.length) {
544
+ this.currentSceneIndex = index;
545
+ return true;
546
+ }
547
+ return false;
548
+ }
549
+
550
+ /**
551
+ * Get all scenes
552
+ */
553
+ getAllScenes() {
554
+ return this.scenes;
555
+ }
556
+
557
+ /**
558
+ * Get all scenes (alias for getAllScenes)
559
+ */
560
+ getScenes() {
561
+ return this.scenes;
562
+ }
563
+
564
+ /**
565
+ * Clear all scenes
566
+ */
567
+ clearScenes() {
568
+ if (this.scenes.length > 0) {
569
+ if (!confirm("Are you sure you want to clear all scenes?")) {
570
+ return false;
571
+ }
572
+ }
573
+
574
+ this.scenes = [];
575
+ this.currentSceneIndex = -1;
576
+ return true;
577
+ }
578
+
579
+ /**
580
+ * Load scenes from data
581
+ */
582
+ loadScenes(scenesData) {
583
+ this.scenes = scenesData || [];
584
+ this.currentSceneIndex = this.scenes.length > 0 ? 0 : -1;
585
+ }
586
+ };
587
+
588
+ // Hotspot Editor - Handles hotspot placement and editing
589
+
590
+ let HotspotEditor$1 = class HotspotEditor {
591
+ constructor(editor) {
592
+ this.editor = editor;
593
+ this.currentHotspotIndex = -1;
594
+ this.placementMode = false;
595
+ }
596
+
597
+ /**
598
+ * Enable hotspot placement mode
599
+ */
600
+ enablePlacementMode() {
601
+ const scene = this.editor.sceneManager.getCurrentScene();
602
+ if (!scene) {
603
+ showToast$1('Please select a scene first', 'error');
604
+ return false;
605
+ }
606
+
607
+ this.placementMode = true;
608
+
609
+ // Visual feedback
610
+ document.body.style.cursor = 'crosshair';
611
+ const preview = document.getElementById('preview');
612
+ if (preview) {
613
+ preview.style.border = '3px solid #4CC3D9';
614
+ preview.style.boxShadow = '0 0 20px rgba(76, 195, 217, 0.5)';
615
+ }
616
+
617
+ // Update button state
618
+ const btn = document.getElementById('addHotspotBtn');
619
+ if (btn) {
620
+ btn.textContent = 'Click on Preview...';
621
+ btn.classList.add('btn-active');
622
+ }
623
+
624
+ showToast$1('Click on the 360° preview to place hotspot', 'info', 5000);
625
+ return true;
626
+ }
627
+
628
+ /**
629
+ * Disable hotspot placement mode
630
+ */
631
+ disablePlacementMode() {
632
+ this.placementMode = false;
633
+ document.body.style.cursor = 'default';
634
+
635
+ // Remove visual feedback
636
+ const preview = document.getElementById('preview');
637
+ if (preview) {
638
+ preview.style.border = '';
639
+ preview.style.boxShadow = '';
640
+ }
641
+
642
+ // Reset button state
643
+ const btn = document.getElementById('addHotspotBtn');
644
+ if (btn) {
645
+ btn.textContent = '+ Add Hotspot';
646
+ btn.classList.remove('btn-active');
647
+ }
648
+ }
649
+
650
+ /**
651
+ * Add hotspot at position
652
+ */
653
+ addHotspot(position, targetSceneId = '') {
654
+ const scene = this.editor.sceneManager.getCurrentScene();
655
+ if (!scene) {
656
+ showToast$1('No scene selected', 'error');
657
+ return null;
658
+ }
659
+
660
+ const hotspot = {
661
+ id: generateId('hotspot'),
662
+ type: 'navigation',
663
+ position: position,
664
+ targetSceneId: targetSceneId,
665
+ title: 'New Hotspot',
666
+ description: '',
667
+ color: '#00ff00',
668
+ icon: ''
669
+ };
670
+
671
+ scene.hotspots.push(hotspot);
672
+ this.currentHotspotIndex = scene.hotspots.length - 1;
673
+
674
+ this.disablePlacementMode();
675
+ showToast$1('Hotspot added', 'success');
676
+
677
+ return hotspot;
678
+ }
679
+
680
+ /**
681
+ * Remove hotspot
682
+ */
683
+ removeHotspot(index) {
684
+ const scene = this.editor.sceneManager.getCurrentScene();
685
+ if (!scene || index < 0 || index >= scene.hotspots.length) {
686
+ return false;
687
+ }
688
+
689
+ if (!confirm('Are you sure you want to delete this hotspot?')) {
690
+ return false;
691
+ }
692
+
693
+ scene.hotspots.splice(index, 1);
694
+
695
+ // Update current index
696
+ if (this.currentHotspotIndex === index) {
697
+ this.currentHotspotIndex = -1;
698
+ } else if (this.currentHotspotIndex > index) {
699
+ this.currentHotspotIndex--;
700
+ }
701
+
702
+ showToast$1('Hotspot removed', 'success');
703
+ return true;
704
+ }
705
+
706
+ /**
707
+ * Update hotspot property
708
+ */
709
+ updateHotspot(index, property, value) {
710
+ const scene = this.editor.sceneManager.getCurrentScene();
711
+ if (!scene || index < 0 || index >= scene.hotspots.length) {
712
+ return false;
713
+ }
714
+
715
+ scene.hotspots[index][property] = value;
716
+ return true;
717
+ }
718
+
719
+ /**
720
+ * Get hotspot by index
721
+ */
722
+ getHotspot(index) {
723
+ const scene = this.editor.sceneManager.getCurrentScene();
724
+ if (!scene || index < 0 || index >= scene.hotspots.length) {
725
+ return null;
726
+ }
727
+ return scene.hotspots[index];
728
+ }
729
+
730
+ /**
731
+ * Update hotspot position
732
+ */
733
+ updateHotspotPosition(index, position) {
734
+ return this.updateHotspot(index, 'position', position);
735
+ }
736
+
737
+ /**
738
+ * Get current hotspot
739
+ */
740
+ getCurrentHotspot() {
741
+ const scene = this.editor.sceneManager.getCurrentScene();
742
+ if (!scene || this.currentHotspotIndex < 0) {
743
+ return null;
744
+ }
745
+ return scene.hotspots[this.currentHotspotIndex] || null;
746
+ }
747
+
748
+ /**
749
+ * Set current hotspot
750
+ */
751
+ setCurrentHotspot(index) {
752
+ const scene = this.editor.sceneManager.getCurrentScene();
753
+ if (!scene || index < 0 || index >= scene.hotspots.length) {
754
+ this.currentHotspotIndex = -1;
755
+ return false;
756
+ }
757
+
758
+ this.currentHotspotIndex = index;
759
+ return true;
760
+ }
761
+
762
+ /**
763
+ * Get all hotspots for current scene
764
+ */
765
+ getAllHotspots() {
766
+ const scene = this.editor.sceneManager.getCurrentScene();
767
+ return scene ? scene.hotspots : [];
768
+ }
769
+
770
+ /**
771
+ * Duplicate hotspot
772
+ */
773
+ duplicateHotspot(index) {
774
+ const scene = this.editor.sceneManager.getCurrentScene();
775
+ if (!scene || index < 0 || index >= scene.hotspots.length) {
776
+ return null;
777
+ }
778
+
779
+ const original = scene.hotspots[index];
780
+ const duplicate = deepClone(original);
781
+ duplicate.id = generateId('hotspot');
782
+ duplicate.title = original.title + ' (Copy)';
783
+
784
+ // Offset position slightly
785
+ duplicate.position = {
786
+ x: original.position.x + 0.5,
787
+ y: original.position.y,
788
+ z: original.position.z
789
+ };
790
+
791
+ scene.hotspots.push(duplicate);
792
+ this.currentHotspotIndex = scene.hotspots.length - 1;
793
+
794
+ showToast$1('Hotspot duplicated', 'success');
795
+ return duplicate;
796
+ }
797
+
798
+ /**
799
+ * Clear all hotspots
800
+ */
801
+ clearAllHotspots() {
802
+ const scene = this.editor.sceneManager.getCurrentScene();
803
+ if (!scene) {
804
+ return false;
805
+ }
806
+
807
+ if (scene.hotspots.length === 0) {
808
+ return true;
809
+ }
810
+
811
+ if (!confirm('Are you sure you want to remove all hotspots from this scene?')) {
812
+ return false;
813
+ }
814
+
815
+ scene.hotspots = [];
816
+ this.currentHotspotIndex = -1;
817
+
818
+ showToast$1('All hotspots removed', 'success');
819
+ return true;
820
+ }
821
+ };
822
+
823
+ // Preview Controller - Manages A-Frame preview integration using SWT library
824
+
825
+ let PreviewController$1 = class PreviewController {
826
+ constructor(editor) {
827
+ this.editor = editor;
828
+ this.tour = null;
829
+ this.isInitialized = false;
830
+ this.previewContainer = null;
831
+ this.hasLoadedScene = false; // Track if we've ever loaded a scene
832
+ }
833
+
834
+ /**
835
+ * Initialize A-Frame preview
836
+ */
837
+ async init() {
838
+ this.previewContainer = document.getElementById("preview");
839
+ if (!this.previewContainer) {
840
+ console.error("Preview element not found");
841
+ return false;
842
+ }
843
+
844
+ // Wait for A-Frame to be loaded
845
+ if (typeof AFRAME === "undefined") {
846
+ await this.waitForLibrary("AFRAME", 5000);
847
+ }
848
+
849
+ // Wait for SWT library to be loaded
850
+ if (typeof SWT === "undefined") {
851
+ await this.waitForLibrary("SWT", 5000);
852
+ }
853
+ this.isInitialized = true;
854
+ return true;
855
+ }
856
+
857
+ /**
858
+ * Wait for a global library to be available
859
+ */
860
+ async waitForLibrary(libraryName, timeout = 5000) {
861
+ const startTime = Date.now();
862
+
863
+ while (typeof window[libraryName] === "undefined") {
864
+ if (Date.now() - startTime > timeout) {
865
+ throw new Error(`Timeout waiting for ${libraryName} to load`);
866
+ }
867
+ await new Promise((resolve) => setTimeout(resolve, 100));
868
+ }
869
+ }
870
+
871
+ /**
872
+ * Load scene into preview using SWT library
873
+ */
874
+ async loadScene(scene, preserveCameraRotation = true) {
875
+ if (!this.isInitialized || !scene) {
876
+ return;
877
+ }
878
+
879
+ // Validate scene has required data
880
+ if (!scene.imageUrl || !scene.id) {
881
+ console.error("Invalid scene data:", scene);
882
+ return;
883
+ }
884
+
885
+ // Show loading animation
886
+ this.showLoading();
887
+
888
+ // Save camera rotation before destroying scene
889
+ let savedRotation = null;
890
+ if (preserveCameraRotation && this.tour) {
891
+ savedRotation = this.getCameraRotation();
892
+ }
893
+
894
+ // Destroy existing tour if any
895
+ if (this.tour) {
896
+ try {
897
+ this.tour.destroy();
898
+ } catch (error) {
899
+ console.error("Error destroying tour:", error);
900
+ }
901
+ this.tour = null;
902
+ }
903
+
904
+ // Clear preview container carefully
905
+ // Only do complex cleanup if we've actually loaded a scene before
906
+ if (this.hasLoadedScene) {
907
+ const existingScene = this.previewContainer.querySelector("a-scene");
908
+ if (existingScene) {
909
+ try {
910
+ // Remove the scene element - A-Frame will handle cleanup if it's ready
911
+ this.previewContainer.removeChild(existingScene);
912
+ } catch (error) {
913
+ console.error("Error removing scene:", error);
914
+ }
915
+ }
916
+
917
+ // Clear any remaining children (loading overlays, empty state, etc)
918
+ while (this.previewContainer.firstChild) {
919
+ this.previewContainer.removeChild(this.previewContainer.firstChild);
920
+ }
921
+ } else {
922
+ // First load - only remove non-A-Frame elements (like empty state divs)
923
+ const children = Array.from(this.previewContainer.children);
924
+ children.forEach(child => {
925
+ // Only remove if it's NOT an a-scene (shouldn't be any, but be safe)
926
+ if (child.tagName.toLowerCase() !== 'a-scene') {
927
+ this.previewContainer.removeChild(child);
928
+ }
929
+ });
930
+ }
931
+
932
+ // Create loading overlay (will be removed after scene loads)
933
+ const loadingOverlay = this.createLoadingOverlay();
934
+ this.previewContainer.appendChild(loadingOverlay);
935
+
936
+ // Create A-Frame scene
937
+ const aframeScene = document.createElement("a-scene");
938
+ aframeScene.id = "preview-scene";
939
+ aframeScene.setAttribute("embedded", "");
940
+ aframeScene.setAttribute("vr-mode-ui", "enabled: false;");
941
+ this.previewContainer.appendChild(aframeScene);
942
+
943
+ // Give A-Frame a moment to start initializing before we proceed
944
+ await new Promise((resolve) => setTimeout(resolve, 100));
945
+
946
+ // Build tour config for this single scene
947
+ // Transform editor scene format to library format
948
+ (scene.hotspots || []).map((h) => ({
949
+ id: h.id,
950
+ position: h.position,
951
+ action: {
952
+ type: h.type === "navigation" ? "navigateTo" : h.type,
953
+ target: h.targetSceneId,
954
+ },
955
+ appearance: {
956
+ color: h.color || "#00ff00",
957
+ icon: h.icon || null,
958
+ scale: h.scale || "1 1 1",
959
+ },
960
+ tooltip: {
961
+ text: h.title || "Hotspot",
962
+ },
963
+ }));
964
+
965
+ ({
966
+ id: scene.id,
967
+ name: scene.name,
968
+ panorama: scene.imageUrl});
969
+
970
+ // Build scenes object with ALL scenes (for navigation to work)
971
+ const allScenes = {};
972
+ const editorScenes = this.editor.sceneManager.scenes || [];
973
+ editorScenes.forEach((s) => {
974
+ const sceneHotspots = (s.hotspots || []).map((h) => ({
975
+ id: h.id,
976
+ position: h.position,
977
+ action: {
978
+ type: h.type === "navigation" ? "navigateTo" : h.type,
979
+ target: h.targetSceneId,
980
+ },
981
+ appearance: {
982
+ color: h.color || "#00ff00",
983
+ icon: h.icon || null,
984
+ scale: h.scale || "1 1 1",
985
+ },
986
+ tooltip: {
987
+ text: h.title || "Hotspot",
988
+ },
989
+ }));
990
+
991
+ allScenes[s.id] = {
992
+ id: s.id,
993
+ name: s.name,
994
+ panorama: s.imageUrl,
995
+ hotspots: sceneHotspots,
996
+ };
997
+ });
998
+
999
+ const tourConfig = {
1000
+ title: scene.name,
1001
+ initialScene: scene.id,
1002
+ scenes: allScenes,
1003
+ settings: {
1004
+ autoRotate: false,
1005
+ showCompass: false,
1006
+ },
1007
+ };
1008
+
1009
+ try {
1010
+ // Create new tour instance
1011
+ this.tour = new SWT.Tour(aframeScene, tourConfig);
1012
+
1013
+ // Set up event listeners
1014
+ this.tour.addEventListener("tour-started", (e) => {});
1015
+
1016
+ this.tour.addEventListener("scene-loaded", (e) => {});
1017
+
1018
+ this.tour.addEventListener("hotspot-activated", (e) => {
1019
+ // Find the hotspot index by ID and select it
1020
+ const hotspotId = e.detail?.hotspotId;
1021
+ if (hotspotId) {
1022
+ const scene = this.editor.sceneManager.getCurrentScene();
1023
+ if (scene) {
1024
+ const hotspotIndex = scene.hotspots.findIndex(
1025
+ (h) => h.id === hotspotId
1026
+ );
1027
+ if (hotspotIndex >= 0) {
1028
+ this.editor.selectHotspot(hotspotIndex);
1029
+ }
1030
+ }
1031
+ }
1032
+ });
1033
+
1034
+ // Start the tour
1035
+ await this.tour.start();
1036
+
1037
+ // Mark that we've successfully loaded a scene
1038
+ this.hasLoadedScene = true;
1039
+
1040
+ // Hide loading animation after scene loads
1041
+ this.hideLoading();
1042
+
1043
+ // Restore camera rotation if preserved
1044
+ if (savedRotation && preserveCameraRotation) {
1045
+ this.setCameraRotation(savedRotation);
1046
+ }
1047
+
1048
+ // Setup click handler after a short delay to ensure A-Frame is ready
1049
+ setTimeout(() => {
1050
+ this.setupClickHandler();
1051
+ }, 500);
1052
+ } catch (error) {
1053
+ console.error("Failed to load preview:", error);
1054
+ showToast$1("Failed to load preview: " + error.message, "error");
1055
+ // Hide loading on error
1056
+ this.hideLoading();
1057
+ }
1058
+ }
1059
+
1060
+ /**
1061
+ * Setup click handler for hotspot placement
1062
+ */
1063
+ setupClickHandler() {
1064
+ if (!this.tour) {
1065
+ return;
1066
+ }
1067
+
1068
+ const aframeScene = this.previewContainer.querySelector("a-scene");
1069
+ if (!aframeScene) {
1070
+ setTimeout(() => this.setupClickHandler(), 200); // Retry
1071
+ return;
1072
+ }
1073
+
1074
+ // Remove any existing click handler to avoid duplicates
1075
+ if (this.clickHandler) {
1076
+ aframeScene.removeEventListener("click", this.clickHandler);
1077
+ }
1078
+
1079
+ // Create and store the click handler
1080
+ this.clickHandler = (evt) => {
1081
+ if (!this.editor.hotspotEditor.placementMode) {
1082
+ return;
1083
+ }
1084
+
1085
+ // Try to get intersection from event detail first
1086
+ let intersection = evt.detail?.intersection;
1087
+
1088
+ // If no intersection, perform manual raycasting
1089
+ if (!intersection) {
1090
+ const camera = aframeScene.querySelector("[camera]");
1091
+ const sky = aframeScene.querySelector("a-sky");
1092
+
1093
+ if (!camera || !sky) {
1094
+ showToast$1("Scene not ready, please try again", "warning");
1095
+ return;
1096
+ }
1097
+
1098
+ // Get mouse position relative to canvas
1099
+ const canvas = aframeScene.canvas;
1100
+ const rect = canvas.getBoundingClientRect();
1101
+ const mouse = {
1102
+ x: ((evt.clientX - rect.left) / rect.width) * 2 - 1,
1103
+ y: -((evt.clientY - rect.top) / rect.height) * 2 + 1,
1104
+ };
1105
+
1106
+ // Perform raycasting
1107
+ const raycaster = new THREE.Raycaster();
1108
+ const cameraEl = camera.object3D;
1109
+ raycaster.setFromCamera(mouse, cameraEl.children[0]); // Get the actual camera
1110
+
1111
+ // Raycast against the sky sphere
1112
+ const intersects = raycaster.intersectObject(sky.object3D, true);
1113
+
1114
+ if (intersects.length > 0) {
1115
+ intersection = intersects[0];
1116
+ } else {
1117
+ showToast$1("Click on the panorama image", "warning");
1118
+ return;
1119
+ }
1120
+ }
1121
+
1122
+ const point = intersection.point;
1123
+ const position = {
1124
+ x: parseFloat(point.x.toFixed(2)),
1125
+ y: parseFloat(point.y.toFixed(2)),
1126
+ z: parseFloat(point.z.toFixed(2)),
1127
+ };
1128
+ this.editor.addHotspotAtPosition(position);
1129
+ };
1130
+ aframeScene.addEventListener("click", this.clickHandler);
1131
+ }
1132
+
1133
+ /**
1134
+ * Get current camera rotation
1135
+ */
1136
+ getCameraRotation() {
1137
+ const aframeScene = this.previewContainer?.querySelector("a-scene");
1138
+ if (!aframeScene) {
1139
+ return null;
1140
+ }
1141
+
1142
+ const camera = aframeScene.querySelector("[camera]");
1143
+ if (!camera) {
1144
+ return null;
1145
+ }
1146
+
1147
+ // Get rotation from object3D which is more reliable
1148
+ const rotation = camera.object3D.rotation;
1149
+ const savedRotation = {
1150
+ x: rotation.x,
1151
+ y: rotation.y,
1152
+ z: rotation.z,
1153
+ };
1154
+ return savedRotation;
1155
+ }
1156
+
1157
+ /**
1158
+ * Set camera rotation
1159
+ */
1160
+ setCameraRotation(rotation) {
1161
+ if (!rotation) {
1162
+ return;
1163
+ }
1164
+
1165
+ const aframeScene = this.previewContainer?.querySelector("a-scene");
1166
+ if (!aframeScene) {
1167
+ return;
1168
+ }
1169
+
1170
+ const camera = aframeScene.querySelector("[camera]");
1171
+ if (!camera) {
1172
+ return;
1173
+ }
1174
+
1175
+ // Set rotation on object3D directly
1176
+ const setRotation = () => {
1177
+ if (camera.object3D) {
1178
+ camera.object3D.rotation.set(rotation.x, rotation.y, rotation.z);
1179
+ }
1180
+ };
1181
+
1182
+ // Try immediately and also after a delay to ensure it sticks
1183
+ setRotation();
1184
+ setTimeout(setRotation, 100);
1185
+ setTimeout(setRotation, 300);
1186
+ }
1187
+
1188
+ /**
1189
+ * Refresh preview (reload current scene while preserving camera rotation)
1190
+ */
1191
+ async refresh() {
1192
+ const scene = this.editor.sceneManager.getCurrentScene();
1193
+ if (scene) {
1194
+ // Save current camera rotation
1195
+ const savedRotation = this.getCameraRotation();
1196
+ // Reload scene
1197
+ await this.loadScene(scene);
1198
+
1199
+ // Restore camera rotation
1200
+ if (savedRotation) {
1201
+ this.setCameraRotation(savedRotation);
1202
+ }
1203
+ }
1204
+ }
1205
+
1206
+ /**
1207
+ * Reset camera
1208
+ */
1209
+ resetCamera() {
1210
+ const camera = this.previewContainer?.querySelector("[camera]");
1211
+ if (camera) {
1212
+ camera.setAttribute("rotation", "0 0 0");
1213
+ }
1214
+ }
1215
+
1216
+ /**
1217
+ * Point camera to hotspot position
1218
+ */
1219
+ pointCameraToHotspot(hotspotPosition) {
1220
+ if (!hotspotPosition) {
1221
+ return;
1222
+ }
1223
+
1224
+ const aframeScene = this.previewContainer?.querySelector("a-scene");
1225
+ if (!aframeScene) {
1226
+ return;
1227
+ }
1228
+
1229
+ const camera = aframeScene.querySelector("[camera]");
1230
+ if (!camera || !camera.object3D) {
1231
+ return;
1232
+ }
1233
+
1234
+ // Get camera position (usually at origin 0,0,0)
1235
+ const cameraPos = camera.object3D.position;
1236
+
1237
+ // Calculate direction vector from camera to hotspot
1238
+ const direction = new THREE.Vector3(
1239
+ hotspotPosition.x - cameraPos.x,
1240
+ hotspotPosition.y - cameraPos.y,
1241
+ hotspotPosition.z - cameraPos.z
1242
+ );
1243
+
1244
+ // Calculate spherical coordinates (yaw and pitch)
1245
+ const distance = direction.length();
1246
+
1247
+ // Pitch (up/down rotation around X-axis) - in degrees
1248
+ const pitch = Math.asin(direction.y / distance) * (180 / Math.PI);
1249
+
1250
+ // Yaw (left/right rotation around Y-axis) - in degrees
1251
+ // Using atan2 to get correct quadrant
1252
+ const yaw = Math.atan2(direction.x, direction.z) * (180 / Math.PI);
1253
+
1254
+ // Apply smooth rotation with animation
1255
+ this.animateCameraRotation(camera, { x: pitch, y: yaw, z: 0 });
1256
+ }
1257
+
1258
+ /**
1259
+ * Animate camera rotation smoothly
1260
+ */
1261
+ animateCameraRotation(camera, targetRotation, duration = 800) {
1262
+ if (!camera || !camera.object3D) return;
1263
+
1264
+ const startRotation = {
1265
+ x: camera.object3D.rotation.x * (180 / Math.PI),
1266
+ y: camera.object3D.rotation.y * (180 / Math.PI),
1267
+ z: camera.object3D.rotation.z * (180 / Math.PI),
1268
+ };
1269
+
1270
+ // Handle angle wrapping for smooth rotation
1271
+ let deltaY = targetRotation.y - startRotation.y;
1272
+
1273
+ // Normalize to -180 to 180 range
1274
+ while (deltaY > 180) deltaY -= 360;
1275
+ while (deltaY < -180) deltaY += 360;
1276
+
1277
+ const endRotationY = startRotation.y + deltaY;
1278
+
1279
+ const startTime = Date.now();
1280
+
1281
+ const animate = () => {
1282
+ const elapsed = Date.now() - startTime;
1283
+ const progress = Math.min(elapsed / duration, 1);
1284
+
1285
+ // Ease-in-out function for smooth animation
1286
+ const eased =
1287
+ progress < 0.5
1288
+ ? 2 * progress * progress
1289
+ : 1 - Math.pow(-2 * progress + 2, 2) / 2;
1290
+
1291
+ // Interpolate rotation
1292
+ const currentRotation = {
1293
+ x: startRotation.x + (targetRotation.x - startRotation.x) * eased,
1294
+ y: startRotation.y + (endRotationY - startRotation.y) * eased,
1295
+ z: startRotation.z + (targetRotation.z - startRotation.z) * eased,
1296
+ };
1297
+
1298
+ // Apply rotation (convert degrees to radians)
1299
+ camera.object3D.rotation.set(
1300
+ currentRotation.x * (Math.PI / 180),
1301
+ currentRotation.y * (Math.PI / 180),
1302
+ currentRotation.z * (Math.PI / 180)
1303
+ );
1304
+
1305
+ if (progress < 1) {
1306
+ requestAnimationFrame(animate);
1307
+ }
1308
+ };
1309
+
1310
+ animate();
1311
+ }
1312
+
1313
+ /**
1314
+ * Highlight hotspot (not needed with library, but keep for compatibility)
1315
+ */
1316
+ highlightHotspot(index) {
1317
+ // The library handles hotspot visualization
1318
+ // This is kept for API compatibility
1319
+ }
1320
+
1321
+ /**
1322
+ * Update hotspot marker (refresh scene while preserving camera rotation)
1323
+ */
1324
+ async updateHotspotMarker(index) {
1325
+ const scene = this.editor.sceneManager.getCurrentScene();
1326
+ if (!scene || !this.tour) return;
1327
+
1328
+ const hotspot = scene.hotspots[index];
1329
+ if (!hotspot) return;
1330
+
1331
+ // Refresh the preview to reflect changes, camera rotation will be preserved
1332
+ await this.refresh();
1333
+ }
1334
+
1335
+ /**
1336
+ * Create loading overlay element
1337
+ */
1338
+ createLoadingOverlay() {
1339
+ const overlay = document.createElement("div");
1340
+ overlay.className = "preview-loading";
1341
+ overlay.innerHTML = `
1342
+ <div class="loading-spinner"></div>
1343
+ <div class="loading-text">Loading scene...</div>
1344
+ `;
1345
+ return overlay;
1346
+ }
1347
+
1348
+ /**
1349
+ * Show loading animation
1350
+ */
1351
+ showLoading() {
1352
+ const existing = this.previewContainer?.querySelector(".preview-loading");
1353
+ if (existing) {
1354
+ existing.classList.remove("hidden");
1355
+ }
1356
+ }
1357
+
1358
+ /**
1359
+ * Hide loading animation
1360
+ */
1361
+ hideLoading() {
1362
+ const loading = this.previewContainer?.querySelector(".preview-loading");
1363
+ if (loading) {
1364
+ loading.classList.add("hidden");
1365
+ // Remove after transition
1366
+ setTimeout(() => {
1367
+ if (loading.parentNode) {
1368
+ loading.parentNode.removeChild(loading);
1369
+ }
1370
+ }, 300);
1371
+ }
1372
+ }
1373
+ };
1374
+
1375
+ // UI Controller - Handles DOM manipulation and rendering
1376
+
1377
+ let UIController$1 = class UIController {
1378
+ constructor(editor) {
1379
+ this.editor = editor;
1380
+ this.sceneList = document.getElementById('sceneList');
1381
+ this.hotspotList = document.getElementById('hotspotList');
1382
+ this.draggedElement = null;
1383
+ }
1384
+
1385
+ /**
1386
+ * Render scene list
1387
+ */
1388
+ renderSceneList() {
1389
+ if (!this.sceneList) return;
1390
+
1391
+ this.sceneList.innerHTML = '';
1392
+ const scenes = this.editor.sceneManager.getAllScenes();
1393
+ const currentIndex = this.editor.sceneManager.currentSceneIndex;
1394
+
1395
+ if (scenes.length === 0) {
1396
+ const empty = document.createElement('div');
1397
+ empty.className = 'empty-state';
1398
+ empty.innerHTML = `
1399
+ <p>No scenes yet</p>
1400
+ <p class="hint">Click "Add Scene" to upload a 360° panorama</p>
1401
+ `;
1402
+ this.sceneList.appendChild(empty);
1403
+ return;
1404
+ }
1405
+
1406
+ scenes.forEach((scene, index) => {
1407
+ const card = this.createSceneCard(scene, index, index === currentIndex);
1408
+ this.sceneList.appendChild(card);
1409
+ });
1410
+ }
1411
+
1412
+ /**
1413
+ * Create scene card element
1414
+ */
1415
+ createSceneCard(scene, index, isActive) {
1416
+ const card = document.createElement('div');
1417
+ card.className = 'scene-card' + (isActive ? ' active' : '');
1418
+ card.draggable = true;
1419
+ card.dataset.index = index;
1420
+
1421
+ // Drag handle
1422
+ const dragHandle = document.createElement('div');
1423
+ dragHandle.className = 'drag-handle';
1424
+ dragHandle.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free v6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M0 96C0 78.3 14.3 64 32 64l384 0c17.7 0 32 14.3 32 32s-14.3 32-32 32L32 128C14.3 128 0 113.7 0 96zM0 256c0-17.7 14.3-32 32-32l384 0c17.7 0 32 14.3 32 32s-14.3 32-32 32L32 288c-17.7 0-32-14.3-32-32zM448 416c0 17.7-14.3 32-32 32L32 448c-17.7 0-32-14.3-32-32s14.3-32 32-32l384 0c17.7 0 32 14.3 32 32z"/></svg>';
1425
+
1426
+ // Thumbnail
1427
+ const thumbnail = document.createElement('img');
1428
+ thumbnail.src = scene.thumbnail || scene.imageUrl;
1429
+ thumbnail.alt = scene.name;
1430
+
1431
+ // Info
1432
+ const info = document.createElement('div');
1433
+ info.className = 'scene-info';
1434
+
1435
+ const name = document.createElement('div');
1436
+ name.className = 'scene-name';
1437
+ name.textContent = scene.name;
1438
+
1439
+ const meta = document.createElement('div');
1440
+ meta.className = 'scene-meta';
1441
+ meta.textContent = `${scene.hotspots.length} hotspot${scene.hotspots.length !== 1 ? 's' : ''}`;
1442
+
1443
+ info.appendChild(name);
1444
+ info.appendChild(meta);
1445
+
1446
+ // Actions
1447
+ const actions = document.createElement('div');
1448
+ actions.className = 'scene-actions';
1449
+
1450
+ const deleteBtn = document.createElement('button');
1451
+ deleteBtn.className = 'btn-icon';
1452
+ deleteBtn.innerHTML = '🗑️';
1453
+ deleteBtn.title = 'Delete scene';
1454
+ deleteBtn.onclick = (e) => {
1455
+ e.stopPropagation();
1456
+ this.editor.removeScene(index);
1457
+ };
1458
+
1459
+ actions.appendChild(deleteBtn);
1460
+
1461
+ card.appendChild(dragHandle);
1462
+ card.appendChild(thumbnail);
1463
+ card.appendChild(info);
1464
+ card.appendChild(actions);
1465
+
1466
+ // Click handler
1467
+ card.onclick = () => {
1468
+ this.editor.selectScene(index);
1469
+ };
1470
+
1471
+ // Drag and drop handlers
1472
+ this.setupDragAndDrop(card);
1473
+
1474
+ return card;
1475
+ }
1476
+
1477
+ /**
1478
+ * Setup drag and drop for scene reordering
1479
+ */
1480
+ setupDragAndDrop(card) {
1481
+ card.addEventListener('dragstart', (e) => {
1482
+ this.draggedElement = card;
1483
+ card.classList.add('dragging');
1484
+ e.dataTransfer.effectAllowed = 'move';
1485
+ });
1486
+
1487
+ card.addEventListener('dragend', () => {
1488
+ card.classList.remove('dragging');
1489
+ this.draggedElement = null;
1490
+ });
1491
+
1492
+ card.addEventListener('dragover', (e) => {
1493
+ e.preventDefault();
1494
+ e.dataTransfer.dropEffect = 'move';
1495
+
1496
+ if (this.draggedElement && this.draggedElement !== card) {
1497
+ const bounding = card.getBoundingClientRect();
1498
+ const offset = bounding.y + bounding.height / 2;
1499
+
1500
+ if (e.clientY - offset > 0) {
1501
+ card.style.borderBottom = '2px solid var(--accent-color)';
1502
+ card.style.borderTop = '';
1503
+ } else {
1504
+ card.style.borderTop = '2px solid var(--accent-color)';
1505
+ card.style.borderBottom = '';
1506
+ }
1507
+ }
1508
+ });
1509
+
1510
+ card.addEventListener('dragleave', () => {
1511
+ card.style.borderTop = '';
1512
+ card.style.borderBottom = '';
1513
+ });
1514
+
1515
+ card.addEventListener('drop', (e) => {
1516
+ e.preventDefault();
1517
+ card.style.borderTop = '';
1518
+ card.style.borderBottom = '';
1519
+
1520
+ if (this.draggedElement && this.draggedElement !== card) {
1521
+ const fromIndex = parseInt(this.draggedElement.dataset.index);
1522
+ const toIndex = parseInt(card.dataset.index);
1523
+ this.editor.reorderScenes(fromIndex, toIndex);
1524
+ }
1525
+ });
1526
+ }
1527
+
1528
+ /**
1529
+ * Render hotspot list
1530
+ */
1531
+ renderHotspotList() {
1532
+ if (!this.hotspotList) return;
1533
+
1534
+ this.hotspotList.innerHTML = '';
1535
+ const hotspots = this.editor.hotspotEditor.getAllHotspots();
1536
+ const currentIndex = this.editor.hotspotEditor.currentHotspotIndex;
1537
+
1538
+ if (hotspots.length === 0) {
1539
+ const empty = document.createElement('div');
1540
+ empty.className = 'empty-state';
1541
+ empty.textContent = 'No hotspots. Click "Add Hotspot" to create one.';
1542
+ this.hotspotList.appendChild(empty);
1543
+ return;
1544
+ }
1545
+
1546
+ hotspots.forEach((hotspot, index) => {
1547
+ const item = this.createHotspotItem(hotspot, index, index === currentIndex);
1548
+ this.hotspotList.appendChild(item);
1549
+ });
1550
+ }
1551
+
1552
+ /**
1553
+ * Create hotspot list item
1554
+ */
1555
+ createHotspotItem(hotspot, index, isActive) {
1556
+ const item = document.createElement('div');
1557
+ item.className = 'hotspot-item' + (isActive ? ' active' : '');
1558
+
1559
+ const color = document.createElement('div');
1560
+ color.className = 'hotspot-color';
1561
+ color.style.backgroundColor = hotspot.color;
1562
+
1563
+ const info = document.createElement('div');
1564
+ info.className = 'hotspot-info';
1565
+
1566
+ const title = document.createElement('div');
1567
+ title.className = 'hotspot-title';
1568
+ title.textContent = hotspot.title || 'Untitled Hotspot';
1569
+
1570
+ const target = document.createElement('div');
1571
+ target.className = 'hotspot-target';
1572
+ if (hotspot.targetSceneId) {
1573
+ const targetScene = this.editor.sceneManager.getSceneById(hotspot.targetSceneId);
1574
+ target.textContent = targetScene ? `→ ${targetScene.name}` : `→ ${hotspot.targetSceneId}`;
1575
+ } else {
1576
+ target.textContent = 'No target';
1577
+ }
1578
+
1579
+ info.appendChild(title);
1580
+ info.appendChild(target);
1581
+
1582
+ const actions = document.createElement('div');
1583
+ actions.className = 'hotspot-actions';
1584
+
1585
+ const deleteBtn = document.createElement('button');
1586
+ deleteBtn.className = 'btn-delete';
1587
+ deleteBtn.innerHTML = '🗑️';
1588
+ deleteBtn.title = 'Delete';
1589
+ deleteBtn.onclick = (e) => {
1590
+ e.stopPropagation();
1591
+ this.editor.removeHotspot(index);
1592
+ };
1593
+
1594
+ actions.appendChild(deleteBtn);
1595
+
1596
+ item.appendChild(color);
1597
+ item.appendChild(info);
1598
+ item.appendChild(actions);
1599
+
1600
+ item.onclick = () => {
1601
+ this.editor.selectHotspot(index);
1602
+ };
1603
+
1604
+ return item;
1605
+ }
1606
+
1607
+ /**
1608
+ * Update properties panel for hotspot
1609
+ */
1610
+ updateHotspotProperties(hotspot) {
1611
+ const hotspotAll = document.getElementById('hotspotAll');
1612
+ const hotspotProperties = document.getElementById('hotspotProperties');
1613
+
1614
+ if (!hotspot) {
1615
+ // No hotspot selected - show list, hide properties
1616
+ if (hotspotAll) hotspotAll.style.display = 'block';
1617
+ if (hotspotProperties) hotspotProperties.style.display = 'none';
1618
+
1619
+ // Clear form
1620
+ document.getElementById('hotspotTitle').value = '';
1621
+ document.getElementById('hotspotDescription').value = '';
1622
+ document.getElementById('hotspotTarget').value = '';
1623
+ document.getElementById('hotspotColor').value = '#00ff00';
1624
+ const colorText = document.getElementById('hotspotColorText');
1625
+ if (colorText) colorText.value = '#00ff00';
1626
+ document.getElementById('hotspotPosX').value = '';
1627
+ document.getElementById('hotspotPosY').value = '';
1628
+ document.getElementById('hotspotPosZ').value = '';
1629
+ return;
1630
+ }
1631
+
1632
+ // Hotspot selected - show both list and properties
1633
+ if (hotspotAll) hotspotAll.style.display = 'block';
1634
+ if (hotspotProperties) hotspotProperties.style.display = 'block';
1635
+
1636
+ document.getElementById('hotspotTitle').value = hotspot.title || '';
1637
+ document.getElementById('hotspotDescription').value = hotspot.description || '';
1638
+ document.getElementById('hotspotTarget').value = hotspot.targetSceneId || '';
1639
+ document.getElementById('hotspotColor').value = hotspot.color || '#00ff00';
1640
+
1641
+ // Update color text input if it exists
1642
+ const colorText = document.getElementById('hotspotColorText');
1643
+ if (colorText) {
1644
+ colorText.value = hotspot.color || '#00ff00';
1645
+ }
1646
+
1647
+ // Update position inputs
1648
+ const pos = hotspot.position || { x: 0, y: 0, z: 0 };
1649
+ document.getElementById('hotspotPosX').value = pos.x;
1650
+ document.getElementById('hotspotPosY').value = pos.y;
1651
+ document.getElementById('hotspotPosZ').value = pos.z;
1652
+
1653
+ // Update target dropdown
1654
+ this.updateTargetSceneOptions();
1655
+ }
1656
+
1657
+ /**
1658
+ * Update properties panel for scene
1659
+ */
1660
+ updateSceneProperties(scene) {
1661
+ if (!scene) {
1662
+ document.getElementById('sceneId').value = '';
1663
+ document.getElementById('sceneName').value = '';
1664
+ document.getElementById('sceneImageUrl').value = '';
1665
+ return;
1666
+ }
1667
+
1668
+ document.getElementById('sceneId').value = scene.id || '';
1669
+ document.getElementById('sceneName').value = scene.name || '';
1670
+ document.getElementById('sceneImageUrl').value = scene.imageUrl || '';
1671
+ }
1672
+
1673
+ /**
1674
+ * Update properties panel for tour
1675
+ */
1676
+ updateTourProperties(config) {
1677
+ document.getElementById('tourTitle').value = config.title || '';
1678
+ document.getElementById('tourDescription').value = config.description || '';
1679
+ document.getElementById('tourInitialScene').value = config.initialSceneId || '';
1680
+ document.getElementById('tourAutoRotate').checked = config.autoRotate || false;
1681
+ document.getElementById('tourShowCompass').checked = config.showCompass || false;
1682
+
1683
+ // Also update project name in header if it exists
1684
+ const projectName = document.getElementById('project-name');
1685
+ if (projectName) {
1686
+ projectName.value = config.title || '';
1687
+ }
1688
+ }
1689
+
1690
+ /**
1691
+ * Update target scene options in hotspot properties
1692
+ */
1693
+ updateTargetSceneOptions() {
1694
+ const select = document.getElementById('hotspotTarget');
1695
+ if (!select) return;
1696
+
1697
+ const scenes = this.editor.sceneManager.getAllScenes();
1698
+ const currentValue = select.value;
1699
+
1700
+ select.innerHTML = '<option value="">Select target scene...</option>';
1701
+
1702
+ scenes.forEach(scene => {
1703
+ const option = document.createElement('option');
1704
+ option.value = scene.id;
1705
+ option.textContent = scene.name;
1706
+ select.appendChild(option);
1707
+ });
1708
+
1709
+ select.value = currentValue;
1710
+ }
1711
+
1712
+ /**
1713
+ * Update initial scene options in tour properties
1714
+ */
1715
+ updateInitialSceneOptions() {
1716
+ const select = document.getElementById('tourInitialScene');
1717
+ if (!select) return;
1718
+
1719
+ const scenes = this.editor.sceneManager.getAllScenes();
1720
+ const currentValue = select.value;
1721
+
1722
+ select.innerHTML = '<option value="">Select initial scene...</option>';
1723
+
1724
+ scenes.forEach(scene => {
1725
+ const option = document.createElement('option');
1726
+ option.value = scene.id;
1727
+ option.textContent = scene.name;
1728
+ select.appendChild(option);
1729
+ });
1730
+
1731
+ select.value = currentValue;
1732
+ }
1733
+
1734
+ /**
1735
+ * Show/hide loading indicator
1736
+ */
1737
+ setLoading(isLoading) {
1738
+ const indicator = document.querySelector('.loading-indicator');
1739
+ if (indicator) {
1740
+ indicator.style.display = isLoading ? 'block' : 'none';
1741
+ }
1742
+ }
1743
+
1744
+ /**
1745
+ * Switch properties tab
1746
+ */
1747
+ switchTab(tabName) {
1748
+ // Update tab buttons
1749
+ document.querySelectorAll('.tab-btn').forEach(btn => {
1750
+ btn.classList.toggle('active', btn.dataset.tab === tabName);
1751
+ });
1752
+
1753
+ // Update tab content
1754
+ document.querySelectorAll('.tab-content').forEach(content => {
1755
+ content.classList.toggle('active', content.id === tabName + 'Tab');
1756
+ });
1757
+ }
1758
+ };
1759
+
1760
+ // Export Manager - Handles JSON generation for SWT library
1761
+
1762
+ let ExportManager$1 = class ExportManager {
1763
+ constructor(editor) {
1764
+ this.editor = editor;
1765
+ }
1766
+
1767
+ /**
1768
+ * Generate JSON compatible with SWT library
1769
+ */
1770
+ generateJSON() {
1771
+ const scenes = this.editor.sceneManager.getAllScenes();
1772
+ const config = this.editor.config;
1773
+ // Build scenes array
1774
+ const scenesData = scenes.map(scene => ({
1775
+ id: scene.id,
1776
+ name: scene.name,
1777
+ imageUrl: scene.imageUrl,
1778
+ hotspots: scene.hotspots.map(hotspot => ({
1779
+ id: hotspot.id,
1780
+ type: hotspot.type || 'navigation',
1781
+ position: hotspot.position,
1782
+ targetSceneId: hotspot.targetSceneId || '',
1783
+ title: hotspot.title || '',
1784
+ description: hotspot.description || '',
1785
+ color: hotspot.color || '#00ff00',
1786
+ icon: hotspot.icon || ''
1787
+ }))
1788
+ }));
1789
+ // Determine initial scene
1790
+ let initialSceneId = config.initialSceneId;
1791
+ if (!initialSceneId && scenes.length > 0) {
1792
+ initialSceneId = scenes[0].id;
1793
+ }
1794
+
1795
+ // Build final JSON
1796
+ const jsonData = {
1797
+ title: config.title || 'Virtual Tour',
1798
+ description: config.description || '',
1799
+ initialSceneId: initialSceneId,
1800
+ scenes: scenesData,
1801
+ settings: {
1802
+ autoRotate: config.autoRotate || false,
1803
+ showCompass: config.showCompass || false
1804
+ }
1805
+ };
1806
+
1807
+ return jsonData;
1808
+ }
1809
+
1810
+ /**
1811
+ * Export as JSON file
1812
+ */
1813
+ exportJSON() {
1814
+ try {
1815
+ const jsonData = this.generateJSON();
1816
+ const json = JSON.stringify(jsonData, null, 2);
1817
+
1818
+ const filename = sanitizeId(jsonData.title || 'tour') + '.json';
1819
+ downloadTextAsFile(json, filename);
1820
+
1821
+ showToast('Tour exported successfully', 'success');
1822
+ return true;
1823
+ } catch (error) {
1824
+ console.error('Export failed:', error);
1825
+ showToast('Export failed', 'error');
1826
+ return false;
1827
+ }
1828
+ }
1829
+
1830
+ /**
1831
+ * Copy JSON to clipboard
1832
+ */
1833
+ async copyJSON() {
1834
+ try {
1835
+ const jsonData = this.generateJSON();
1836
+ const json = JSON.stringify(jsonData, null, 2);
1837
+
1838
+ const success = await copyToClipboard(json);
1839
+ if (success) {
1840
+ showToast('JSON copied to clipboard', 'success');
1841
+ } else {
1842
+ showToast('Failed to copy to clipboard', 'error');
1843
+ }
1844
+ return success;
1845
+ } catch (error) {
1846
+ console.error('Copy failed:', error);
1847
+ showToast('Copy failed', 'error');
1848
+ return false;
1849
+ }
1850
+ }
1851
+
1852
+ /**
1853
+ * Generate HTML viewer code
1854
+ */
1855
+ generateViewerHTML() {
1856
+ const jsonData = this.generateJSON();
1857
+
1858
+ return `<!DOCTYPE html>
1859
+ <html lang="en">
1860
+ <head>
1861
+ <meta charset="UTF-8">
1862
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1863
+ <title>${jsonData.title}</title>
1864
+ <script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
1865
+ <script src="dist/swt.min.js"></script>
1866
+ <style>
1867
+ body {
1868
+ margin: 0;
1869
+ overflow: hidden;
1870
+ font-family: Arial, sans-serif;
1871
+ }
1872
+
1873
+ #loading {
1874
+ position: fixed;
1875
+ top: 0;
1876
+ left: 0;
1877
+ width: 100%;
1878
+ height: 100%;
1879
+ background: #000;
1880
+ display: flex;
1881
+ align-items: center;
1882
+ justify-content: center;
1883
+ color: #fff;
1884
+ z-index: 1000;
1885
+ }
1886
+
1887
+ #loading.hidden {
1888
+ display: none;
1889
+ }
1890
+
1891
+ .spinner {
1892
+ border: 4px solid rgba(255,255,255,0.3);
1893
+ border-top: 4px solid #fff;
1894
+ border-radius: 50%;
1895
+ width: 40px;
1896
+ height: 40px;
1897
+ animation: spin 1s linear infinite;
1898
+ margin-right: 15px;
1899
+ }
1900
+
1901
+ @keyframes spin {
1902
+ 0% { transform: rotate(0deg); }
1903
+ 100% { transform: rotate(360deg); }
1904
+ }
1905
+
1906
+ #ui {
1907
+ position: fixed;
1908
+ bottom: 20px;
1909
+ left: 50%;
1910
+ transform: translateX(-50%);
1911
+ z-index: 100;
1912
+ display: flex;
1913
+ gap: 10px;
1914
+ }
1915
+
1916
+ .btn {
1917
+ background: rgba(0,0,0,0.7);
1918
+ color: #fff;
1919
+ border: none;
1920
+ padding: 10px 20px;
1921
+ border-radius: 5px;
1922
+ cursor: pointer;
1923
+ font-size: 14px;
1924
+ }
1925
+
1926
+ .btn:hover {
1927
+ background: rgba(0,0,0,0.9);
1928
+ }
1929
+ </style>
1930
+ </head>
1931
+ <body>
1932
+ <div id="loading">
1933
+ <div class="spinner"></div>
1934
+ <span>Loading Tour...</span>
1935
+ </div>
1936
+
1937
+ <div id="tour-container"></div>
1938
+
1939
+ <div id="ui" style="display: none;">
1940
+ <button class="btn" id="resetBtn">Reset View</button>
1941
+ <span class="btn" id="sceneInfo"></span>
1942
+ </div>
1943
+
1944
+ <script>
1945
+ // Tour configuration
1946
+ const tourConfig = ${JSON.stringify(jsonData, null, 8)};
1947
+
1948
+ // Initialize tour
1949
+ let tour;
1950
+
1951
+ document.addEventListener('DOMContentLoaded', async () => {
1952
+ try {
1953
+ // Create tour instance
1954
+ tour = new SenangWebsTour('tour-container', tourConfig);
1955
+
1956
+ // Listen to events
1957
+ tour.on('sceneChanged', (sceneId) => {
1958
+ updateSceneInfo();
1959
+ });
1960
+
1961
+ tour.on('ready', () => {
1962
+ document.getElementById('loading').classList.add('hidden');
1963
+ document.getElementById('ui').style.display = 'flex';
1964
+ updateSceneInfo();
1965
+ });
1966
+
1967
+ tour.on('error', (error) => {
1968
+ console.error('Tour error:', error);
1969
+ alert('Failed to load tour: ' + error.message);
1970
+ });
1971
+
1972
+ // Start tour
1973
+ await tour.start();
1974
+
1975
+ // Setup UI
1976
+ document.getElementById('resetBtn').addEventListener('click', () => {
1977
+ const camera = document.querySelector('[camera]');
1978
+ if (camera) {
1979
+ camera.setAttribute('rotation', '0 0 0');
1980
+ }
1981
+ });
1982
+
1983
+ } catch (error) {
1984
+ console.error('Failed to initialize tour:', error);
1985
+ alert('Failed to initialize tour: ' + error.message);
1986
+ }
1987
+ });
1988
+
1989
+ function updateSceneInfo() {
1990
+ const sceneId = tour.getCurrentSceneId();
1991
+ const scene = tourConfig.scenes.find(s => s.id === sceneId);
1992
+ if (scene) {
1993
+ document.getElementById('sceneInfo').textContent = scene.name;
1994
+ }
1995
+ }
1996
+ </script>
1997
+ </body>
1998
+ </html>`;
1999
+ }
2000
+
2001
+ /**
2002
+ * Export as standalone HTML viewer
2003
+ */
2004
+ exportViewerHTML() {
2005
+ try {
2006
+ const html = this.generateViewerHTML();
2007
+ const jsonData = this.generateJSON();
2008
+ const filename = sanitizeId(jsonData.title || 'tour') + '-viewer.html';
2009
+
2010
+ downloadTextAsFile(html, filename);
2011
+
2012
+ showToast('Viewer HTML exported successfully', 'success');
2013
+ return true;
2014
+ } catch (error) {
2015
+ console.error('Export viewer failed:', error);
2016
+ showToast('Export viewer failed', 'error');
2017
+ return false;
2018
+ }
2019
+ }
2020
+
2021
+ /**
2022
+ * Show export preview in modal
2023
+ */
2024
+ showExportPreview() {
2025
+ try {
2026
+ const jsonData = this.generateJSON();
2027
+ const json = JSON.stringify(jsonData, null, 2);
2028
+
2029
+ const preview = document.getElementById('exportPreview');
2030
+ if (preview) {
2031
+ preview.textContent = json;
2032
+ }
2033
+
2034
+ showModal('exportModal');
2035
+ return true;
2036
+ } catch (error) {
2037
+ console.error('Failed to show export preview:', error);
2038
+ showToast('Failed to generate preview', 'error');
2039
+ return false;
2040
+ }
2041
+ }
2042
+ };
2043
+
2044
+ // Main Editor Controller
2045
+
2046
+ let TourEditor$1 = class TourEditor {
2047
+ constructor(options = {}) {
2048
+ this.config = {
2049
+ title: options.projectName || 'My Virtual Tour',
2050
+ description: '',
2051
+ initialSceneId: '',
2052
+ autoRotate: false,
2053
+ showCompass: false
2054
+ };
2055
+
2056
+ // Store initialization options
2057
+ this.options = {
2058
+ sceneListElement: options.sceneListElement || null,
2059
+ previewElement: options.previewElement || null,
2060
+ propertiesElement: options.propertiesElement || null,
2061
+ autoSave: options.autoSave !== undefined ? options.autoSave : false,
2062
+ autoSaveInterval: options.autoSaveInterval || 30000,
2063
+ ...options
2064
+ };
2065
+
2066
+ this.storageManager = new ProjectStorageManager();
2067
+ this.sceneManager = new SceneManagerEditor(this);
2068
+ this.hotspotEditor = new HotspotEditor(this);
2069
+ this.previewController = new PreviewController(this);
2070
+ this.uiController = new UIController(this);
2071
+ this.exportManager = new ExportManager(this);
2072
+
2073
+ this.hasUnsavedChanges = false;
2074
+ this.lastRenderedSceneIndex = -1;
2075
+ this.listenersSetup = false;
2076
+ }
2077
+
2078
+ /**
2079
+ * Initialize editor
2080
+ * @param {Object} config - Optional configuration object for programmatic init
2081
+ */
2082
+ async init(config = {}) {
2083
+ // Merge config with existing options
2084
+ if (config && Object.keys(config).length > 0) {
2085
+ Object.assign(this.options, config);
2086
+ if (config.projectName) {
2087
+ this.config.title = config.projectName;
2088
+ }
2089
+ }
2090
+
2091
+ // Initialize preview
2092
+ const previewInit = await this.previewController.init();
2093
+ if (!previewInit) {
2094
+ console.error('Failed to initialize preview controller');
2095
+ showToast('Failed to initialize preview', 'error');
2096
+ return false;
2097
+ }
2098
+
2099
+ // Setup event listeners
2100
+ this.setupEventListeners();
2101
+
2102
+ // Load saved project if exists (but only if it has valid data)
2103
+ if (this.storageManager.hasProject()) {
2104
+ try {
2105
+ const projectData = this.storageManager.loadProject();
2106
+ if (projectData && projectData.scenes && projectData.scenes.length > 0) {
2107
+ this.loadProject();
2108
+ } else {
2109
+ // Invalid or empty project, clear it
2110
+ console.error('Invalid or empty project data, clearing storage');
2111
+ this.storageManager.clearProject();
2112
+ }
2113
+ } catch (error) {
2114
+ console.error('Error loading saved project:', error);
2115
+ this.storageManager.clearProject();
2116
+ }
2117
+ }
2118
+
2119
+ // Start auto-save if enabled
2120
+ if (this.options.autoSave) {
2121
+ this.storageManager.startAutoSave(() => {
2122
+ this.saveProject();
2123
+ }, this.options.autoSaveInterval);
2124
+ }
2125
+
2126
+ // Initial render (only if no project was loaded)
2127
+ if (this.sceneManager.getScenes().length === 0) {
2128
+ this.render();
2129
+ }
2130
+
2131
+ showToast('Editor ready', 'success');
2132
+
2133
+ return true;
2134
+ }
2135
+
2136
+ /**
2137
+ * Setup event listeners
2138
+ */
2139
+ setupEventListeners() {
2140
+ if (this.listenersSetup) {
2141
+ return;
2142
+ }
2143
+
2144
+ const addSceneBtns = document.querySelectorAll('#addSceneBtn');
2145
+ const sceneUploads = document.querySelectorAll('#sceneUpload');
2146
+ const importBtns = document.querySelectorAll('#importBtn');
2147
+ const importUploads = document.querySelectorAll('#importUpload');
2148
+ if (addSceneBtns.length > 1 || sceneUploads.length > 1 || importBtns.length > 1 || importUploads.length > 1) {
2149
+ console.error('Duplicate IDs found in DOM. This will cause double-trigger issues.');
2150
+ }
2151
+
2152
+ // Toolbar buttons
2153
+ document.getElementById('newBtn')?.addEventListener('click', () => this.newProject());
2154
+ document.getElementById('saveBtn')?.addEventListener('click', () => this.saveProject());
2155
+ document.getElementById('exportBtn')?.addEventListener('click', () => this.exportManager.showExportPreview());
2156
+ document.getElementById('importBtn')?.addEventListener('click', () => this.importProject());
2157
+ document.getElementById('helpBtn')?.addEventListener('click', () => showModal('helpModal'));
2158
+
2159
+ document.getElementById('addSceneBtn')?.addEventListener('click', () => {
2160
+ const sceneUpload = document.getElementById('sceneUpload');
2161
+ if (sceneUpload) {
2162
+ sceneUpload.click();
2163
+ }
2164
+ });
2165
+
2166
+ document.getElementById('sceneUpload')?.addEventListener('change', (e) => {
2167
+ if (e.target.files && e.target.files.length > 0) {
2168
+ this.handleSceneUpload(e.target.files);
2169
+ setTimeout(() => {
2170
+ e.target.value = '';
2171
+ }, 100);
2172
+ }
2173
+ });
2174
+
2175
+ document.getElementById('addHotspotBtn')?.addEventListener('click', () => {
2176
+ this.hotspotEditor.enablePlacementMode();
2177
+ });
2178
+
2179
+ document.getElementById('clearHotspotsBtn')?.addEventListener('click', () => {
2180
+ if (this.hotspotEditor.clearAllHotspots()) {
2181
+ this.render();
2182
+ }
2183
+ });
2184
+
2185
+ // Properties tabs
2186
+ document.querySelectorAll('.tab-btn').forEach(btn => {
2187
+ btn.addEventListener('click', () => {
2188
+ this.uiController.switchTab(btn.dataset.tab);
2189
+ });
2190
+ });
2191
+
2192
+ document.getElementById('hotspotTitle')?.addEventListener('input', debounce((e) => {
2193
+ this.updateCurrentHotspot('title', e.target.value);
2194
+ }, 300));
2195
+
2196
+ document.getElementById('hotspotDescription')?.addEventListener('input', debounce((e) => {
2197
+ this.updateCurrentHotspot('description', e.target.value);
2198
+ }, 300));
2199
+
2200
+ document.getElementById('hotspotTarget')?.addEventListener('change', (e) => {
2201
+ this.updateCurrentHotspot('targetSceneId', e.target.value);
2202
+ });
2203
+
2204
+ document.getElementById('hotspotColor')?.addEventListener('input', (e) => {
2205
+ this.updateCurrentHotspot('color', e.target.value);
2206
+ });
2207
+
2208
+ document.getElementById('hotspotPosX')?.addEventListener('input', debounce((e) => {
2209
+ this.updateCurrentHotspotPosition('x', parseFloat(e.target.value) || 0);
2210
+ }, 300));
2211
+
2212
+ document.getElementById('hotspotPosY')?.addEventListener('input', debounce((e) => {
2213
+ this.updateCurrentHotspotPosition('y', parseFloat(e.target.value) || 0);
2214
+ }, 300));
2215
+
2216
+ document.getElementById('hotspotPosZ')?.addEventListener('input', debounce((e) => {
2217
+ this.updateCurrentHotspotPosition('z', parseFloat(e.target.value) || 0);
2218
+ }, 300));
2219
+
2220
+ document.getElementById('sceneId')?.addEventListener('input', debounce((e) => {
2221
+ this.updateCurrentScene('id', sanitizeId(e.target.value));
2222
+ }, 300));
2223
+
2224
+ document.getElementById('sceneName')?.addEventListener('input', debounce((e) => {
2225
+ this.updateCurrentScene('name', e.target.value);
2226
+ }, 300));
2227
+
2228
+ document.getElementById('sceneImageUrl')?.addEventListener('input', debounce((e) => {
2229
+ this.updateCurrentSceneImage(e.target.value);
2230
+ }, 300));
2231
+
2232
+ document.getElementById('tourTitle')?.addEventListener('input', debounce((e) => {
2233
+ this.config.title = e.target.value;
2234
+ this.markUnsavedChanges();
2235
+ const projectName = document.getElementById('project-name');
2236
+ if (projectName && projectName.value !== e.target.value) {
2237
+ projectName.value = e.target.value;
2238
+ }
2239
+ }, 300));
2240
+
2241
+ document.getElementById('project-name')?.addEventListener('input', debounce((e) => {
2242
+ this.config.title = e.target.value;
2243
+ this.markUnsavedChanges();
2244
+ const tourTitle = document.getElementById('tourTitle');
2245
+ if (tourTitle && tourTitle.value !== e.target.value) {
2246
+ tourTitle.value = e.target.value;
2247
+ }
2248
+ }, 300));
2249
+
2250
+ document.getElementById('tourDescription')?.addEventListener('input', debounce((e) => {
2251
+ this.config.description = e.target.value;
2252
+ this.markUnsavedChanges();
2253
+ }, 300));
2254
+
2255
+ document.getElementById('tourInitialScene')?.addEventListener('change', (e) => {
2256
+ this.config.initialSceneId = e.target.value;
2257
+ this.markUnsavedChanges();
2258
+ });
2259
+
2260
+ document.getElementById('tourAutoRotate')?.addEventListener('change', (e) => {
2261
+ this.config.autoRotate = e.target.checked;
2262
+ this.markUnsavedChanges();
2263
+ });
2264
+
2265
+ document.getElementById('tourShowCompass')?.addEventListener('change', (e) => {
2266
+ this.config.showCompass = e.target.checked;
2267
+ this.markUnsavedChanges();
2268
+ });
2269
+
2270
+ document.getElementById('exportJsonBtn')?.addEventListener('click', () => {
2271
+ this.exportManager.exportJSON();
2272
+ });
2273
+
2274
+ document.getElementById('copyJsonBtn')?.addEventListener('click', () => {
2275
+ this.exportManager.copyJSON();
2276
+ });
2277
+
2278
+ document.getElementById('exportViewerBtn')?.addEventListener('click', () => {
2279
+ this.exportManager.exportViewerHTML();
2280
+ });
2281
+
2282
+ document.querySelectorAll('.modal-close').forEach(btn => {
2283
+ btn.addEventListener('click', () => {
2284
+ const modal = btn.closest('.modal');
2285
+ if (modal) {
2286
+ hideModal(modal.id);
2287
+ }
2288
+ });
2289
+ });
2290
+
2291
+ document.getElementById('importUpload')?.addEventListener('change', (e) => {
2292
+ if (e.target.files && e.target.files.length > 0) {
2293
+ this.handleImportFile(e.target.files[0]);
2294
+ setTimeout(() => {
2295
+ e.target.value = '';
2296
+ }, 100);
2297
+ }
2298
+ });
2299
+
2300
+ window.addEventListener('beforeunload', (e) => {
2301
+ if (this.hasUnsavedChanges) {
2302
+ e.preventDefault();
2303
+ e.returnValue = '';
2304
+ }
2305
+ });
2306
+
2307
+ this.listenersSetup = true;
2308
+ }
2309
+
2310
+ /**
2311
+ * Handle scene upload
2312
+ */
2313
+ async handleSceneUpload(files) {
2314
+ if (!files || files.length === 0) {
2315
+ return;
2316
+ }
2317
+
2318
+ this.uiController.setLoading(true);
2319
+
2320
+ for (const file of files) {
2321
+ if (!file.type.startsWith('image/')) {
2322
+ showToast(`${file.name} is not an image`, 'error');
2323
+ continue;
2324
+ }
2325
+
2326
+ await this.sceneManager.addScene(file);
2327
+ }
2328
+ this.uiController.setLoading(false);
2329
+ this.render();
2330
+ this.markUnsavedChanges();
2331
+ }
2332
+
2333
+ /**
2334
+ * Add hotspot at position
2335
+ */
2336
+ addHotspotAtPosition(position) {
2337
+ const distance = Math.sqrt(position.x * position.x + position.y * position.y + position.z * position.z);
2338
+ if (distance > 5) {
2339
+ const scale = 5 / distance;
2340
+ position.x *= scale;
2341
+ position.y *= scale;
2342
+ position.z *= scale;
2343
+ position.x = parseFloat(position.x.toFixed(2));
2344
+ position.y = parseFloat(position.y.toFixed(2));
2345
+ position.z = parseFloat(position.z.toFixed(2));
2346
+ }
2347
+ const hotspot = this.hotspotEditor.addHotspot(position);
2348
+ if (hotspot) {
2349
+ this.lastRenderedSceneIndex = -1;
2350
+ this.render();
2351
+ this.markUnsavedChanges();
2352
+ } else {
2353
+ console.error('Failed to add hotspot');
2354
+ }
2355
+ }
2356
+
2357
+ /**
2358
+ * Select scene by index
2359
+ */
2360
+ selectScene(index) {
2361
+ if (this.sceneManager.setCurrentScene(index)) {
2362
+ this.lastRenderedSceneIndex = -1;
2363
+ this.hotspotEditor.currentHotspotIndex = -1;
2364
+
2365
+ const scene = this.sceneManager.getCurrentScene();
2366
+ if (scene) {
2367
+ this.previewController.loadScene(scene, false);
2368
+ this.lastRenderedSceneIndex = index;
2369
+ }
2370
+
2371
+ this.uiController.renderSceneList();
2372
+ this.uiController.updateSceneProperties(scene);
2373
+ this.uiController.renderHotspotList();
2374
+ this.uiController.updateHotspotProperties(null);
2375
+ this.uiController.updateInitialSceneOptions();
2376
+ this.uiController.updateTargetSceneOptions();
2377
+ }
2378
+ }
2379
+
2380
+ /**
2381
+ * Select hotspot by index
2382
+ */
2383
+ selectHotspot(index) {
2384
+ if (this.hotspotEditor.setCurrentHotspot(index)) {
2385
+ const hotspot = this.hotspotEditor.getHotspot(index);
2386
+
2387
+ this.uiController.renderHotspotList();
2388
+ this.uiController.updateHotspotProperties(hotspot);
2389
+ this.uiController.updateTargetSceneOptions();
2390
+ this.uiController.switchTab('hotspot');
2391
+
2392
+ if (hotspot && hotspot.position) {
2393
+ this.previewController.pointCameraToHotspot(hotspot.position);
2394
+ }
2395
+ }
2396
+ }
2397
+
2398
+ /**
2399
+ * Remove scene
2400
+ */
2401
+ removeScene(index) {
2402
+ if (this.sceneManager.removeScene(index)) {
2403
+ this.render();
2404
+ this.markUnsavedChanges();
2405
+ }
2406
+ }
2407
+
2408
+ /**
2409
+ * Remove hotspot
2410
+ */
2411
+ removeHotspot(index) {
2412
+ if (this.hotspotEditor.removeHotspot(index)) {
2413
+ this.lastRenderedSceneIndex = -1;
2414
+ this.render();
2415
+ this.markUnsavedChanges();
2416
+ }
2417
+ }
2418
+
2419
+ /**
2420
+ * Duplicate hotspot
2421
+ */
2422
+ duplicateHotspot(index) {
2423
+ const hotspot = this.hotspotEditor.duplicateHotspot(index);
2424
+ if (hotspot) {
2425
+ this.lastRenderedSceneIndex = -1;
2426
+ this.render();
2427
+ this.markUnsavedChanges();
2428
+ }
2429
+ }
2430
+
2431
+ /**
2432
+ * Reorder scenes
2433
+ */
2434
+ reorderScenes(fromIndex, toIndex) {
2435
+ if (this.sceneManager.reorderScenes(fromIndex, toIndex)) {
2436
+ this.render();
2437
+ this.markUnsavedChanges();
2438
+ }
2439
+ }
2440
+
2441
+ /**
2442
+ * Update current hotspot property
2443
+ */
2444
+ async updateCurrentHotspot(property, value) {
2445
+ const index = this.hotspotEditor.currentHotspotIndex;
2446
+ if (this.hotspotEditor.updateHotspot(index, property, value)) {
2447
+ await this.previewController.updateHotspotMarker(index);
2448
+ this.uiController.renderHotspotList();
2449
+ this.markUnsavedChanges();
2450
+ }
2451
+ }
2452
+
2453
+ /**
2454
+ * Update current hotspot position (X, Y, or Z)
2455
+ */
2456
+ async updateCurrentHotspotPosition(axis, value) {
2457
+ const index = this.hotspotEditor.currentHotspotIndex;
2458
+ const hotspot = this.hotspotEditor.getHotspot(index);
2459
+
2460
+ if (hotspot) {
2461
+ if (!hotspot.position) {
2462
+ hotspot.position = { x: 0, y: 0, z: 0 };
2463
+ }
2464
+
2465
+ hotspot.position[axis] = value;
2466
+
2467
+ const pos = hotspot.position;
2468
+ const distance = Math.sqrt(pos.x * pos.x + pos.y * pos.y + pos.z * pos.z);
2469
+ if (distance > 10) {
2470
+ const scale = 10 / distance;
2471
+ pos.x *= scale;
2472
+ pos.y *= scale;
2473
+ pos.z *= scale;
2474
+
2475
+ document.getElementById(`hotspotPos${axis.toUpperCase()}`).value = pos[axis].toFixed(2);
2476
+ showToast('Position clamped to 10-unit radius', 'info');
2477
+ }
2478
+
2479
+ await this.previewController.updateHotspotMarker(index);
2480
+ this.uiController.renderHotspotList();
2481
+ this.markUnsavedChanges();
2482
+ }
2483
+ }
2484
+
2485
+ /**
2486
+ * Update current scene property
2487
+ */
2488
+ updateCurrentScene(property, value) {
2489
+ const index = this.sceneManager.currentSceneIndex;
2490
+ if (this.sceneManager.updateScene(index, property, value)) {
2491
+ this.uiController.renderSceneList();
2492
+ this.markUnsavedChanges();
2493
+ }
2494
+ }
2495
+
2496
+ /**
2497
+ * Update current scene image URL
2498
+ */
2499
+ async updateCurrentSceneImage(imageUrl) {
2500
+ const index = this.sceneManager.currentSceneIndex;
2501
+ if (index < 0) return;
2502
+
2503
+ if (this.sceneManager.updateScene(index, 'imageUrl', imageUrl)) {
2504
+ const scene = this.sceneManager.getCurrentScene();
2505
+ if (scene) {
2506
+ scene.thumbnail = imageUrl;
2507
+ }
2508
+
2509
+ this.uiController.renderSceneList();
2510
+ this.lastRenderedSceneIndex = -1;
2511
+
2512
+ if (scene) {
2513
+ await this.previewController.loadScene(scene);
2514
+ this.lastRenderedSceneIndex = index;
2515
+ showToast('Scene image updated', 'success');
2516
+ }
2517
+ this.markUnsavedChanges();
2518
+ }
2519
+ }
2520
+
2521
+ /**
2522
+ * Render all UI
2523
+ */
2524
+ render() {
2525
+ this.uiController.renderSceneList();
2526
+ this.uiController.renderHotspotList();
2527
+
2528
+ const currentScene = this.sceneManager.getCurrentScene();
2529
+ const currentHotspot = this.hotspotEditor.getCurrentHotspot();
2530
+
2531
+ this.uiController.updateSceneProperties(currentScene);
2532
+ this.uiController.updateHotspotProperties(currentHotspot);
2533
+ this.uiController.updateTourProperties(this.config);
2534
+ this.uiController.updateInitialSceneOptions();
2535
+ this.uiController.updateTargetSceneOptions();
2536
+
2537
+ if (currentScene) {
2538
+ const emptyState = document.querySelector('.preview-empty');
2539
+ if (emptyState) {
2540
+ emptyState.style.display = 'none';
2541
+ }
2542
+
2543
+ const currentSceneIndex = this.sceneManager.currentSceneIndex;
2544
+ if (currentSceneIndex !== this.lastRenderedSceneIndex) {
2545
+ this.previewController.loadScene(currentScene);
2546
+ this.lastRenderedSceneIndex = currentSceneIndex;
2547
+ }
2548
+
2549
+ if (currentHotspot) {
2550
+ this.previewController.highlightHotspot(this.hotspotEditor.currentHotspotIndex);
2551
+ }
2552
+ } else {
2553
+ const emptyState = document.querySelector('.preview-empty');
2554
+ if (emptyState) {
2555
+ emptyState.style.display = 'flex';
2556
+ }
2557
+ this.lastRenderedSceneIndex = -1;
2558
+ }
2559
+ }
2560
+
2561
+ /**
2562
+ * Save project
2563
+ */
2564
+ saveProject() {
2565
+ const projectData = {
2566
+ config: this.config,
2567
+ scenes: this.sceneManager.getAllScenes()
2568
+ };
2569
+
2570
+ if (this.storageManager.saveProject(projectData)) {
2571
+ this.hasUnsavedChanges = false;
2572
+ showToast('Project saved', 'success');
2573
+ return true;
2574
+ }
2575
+ return false;
2576
+ }
2577
+
2578
+ /**
2579
+ * Load project
2580
+ */
2581
+ loadProject() {
2582
+ const projectData = this.storageManager.loadProject();
2583
+ if (projectData) {
2584
+ this.config = projectData.config || this.config;
2585
+ this.sceneManager.loadScenes(projectData.scenes || []);
2586
+ this.hasUnsavedChanges = false;
2587
+ this.render();
2588
+ showToast('Project loaded', 'success');
2589
+ return true;
2590
+ }
2591
+ return false;
2592
+ }
2593
+
2594
+ /**
2595
+ * New project
2596
+ */
2597
+ newProject() {
2598
+ if (this.hasUnsavedChanges) {
2599
+ if (!confirm('You have unsaved changes. Create new project?')) {
2600
+ return false;
2601
+ }
2602
+ }
2603
+
2604
+ this.config = {
2605
+ title: 'My Virtual Tour',
2606
+ description: '',
2607
+ initialSceneId: '',
2608
+ autoRotate: false,
2609
+ showCompass: false
2610
+ };
2611
+
2612
+ this.sceneManager.clearScenes();
2613
+ this.hasUnsavedChanges = false;
2614
+ this.render();
2615
+
2616
+ showToast('New project created', 'success');
2617
+ return true;
2618
+ }
2619
+
2620
+ /**
2621
+ * Import project
2622
+ */
2623
+ importProject() {
2624
+ const importUpload = document.getElementById('importUpload');
2625
+ if (importUpload) {
2626
+ importUpload.click();
2627
+ }
2628
+ }
2629
+
2630
+ /**
2631
+ * Handle import file
2632
+ */
2633
+ async handleImportFile(file) {
2634
+ try {
2635
+ this.uiController.setLoading(true);
2636
+
2637
+ const projectData = await this.storageManager.importFromFile(file);
2638
+
2639
+ this.config = projectData.config || projectData;
2640
+ this.sceneManager.loadScenes(projectData.scenes || []);
2641
+ this.hasUnsavedChanges = true;
2642
+
2643
+ this.render();
2644
+ this.uiController.setLoading(false);
2645
+
2646
+ showToast('Project imported successfully', 'success');
2647
+ } catch (error) {
2648
+ this.uiController.setLoading(false);
2649
+ console.error('Import failed:', error);
2650
+ }
2651
+ }
2652
+
2653
+ /**
2654
+ * Mark unsaved changes
2655
+ */
2656
+ markUnsavedChanges() {
2657
+ this.hasUnsavedChanges = true;
2658
+ }
2659
+ };
2660
+
2661
+ // Initialize editor when DOM is ready
2662
+ document.addEventListener('DOMContentLoaded', async () => {
2663
+ if (document.querySelector('[data-swt-editor]')) {
2664
+ // Declarative initialization will handle it
2665
+ return;
2666
+ }
2667
+ window.editor = new TourEditor$1();
2668
+ await window.editor.init();
2669
+ });
2670
+
2671
+ // UI Initialization - Handles color picker sync, keyboard shortcuts, tab switching, and declarative init
2672
+
2673
+ /**
2674
+ * Initialize editor from declarative HTML attributes
2675
+ */
2676
+ function initDeclarativeEditor() {
2677
+ const editorElement = document.querySelector("[data-swt-editor]");
2678
+
2679
+ if (!editorElement) {
2680
+ return null; // No declarative editor found
2681
+ }
2682
+
2683
+ // Check if auto-init is enabled
2684
+ const autoInit = editorElement.getAttribute("data-swt-auto-init");
2685
+ if (autoInit !== "true") {
2686
+ return null; // Auto-init disabled
2687
+ }
2688
+
2689
+ // Find required elements by data attributes
2690
+ const sceneListElement = editorElement.querySelector("[data-swt-scene-list]");
2691
+ const previewElement = editorElement.querySelector("[data-swt-preview-area]");
2692
+ const propertiesElement = editorElement.querySelector(
2693
+ "[data-swt-properties-panel]"
2694
+ );
2695
+
2696
+ // Get optional configuration from attributes
2697
+ const projectName =
2698
+ editorElement.getAttribute("data-swt-project-name") || "My Virtual Tour";
2699
+ const autoSave = editorElement.getAttribute("data-swt-auto-save") === "true";
2700
+ const autoSaveInterval =
2701
+ parseInt(editorElement.getAttribute("data-swt-auto-save-interval")) ||
2702
+ 30000;
2703
+
2704
+ // Create and initialize editor
2705
+ const editor = new TourEditor({
2706
+ projectName,
2707
+ autoSave,
2708
+ autoSaveInterval,
2709
+ });
2710
+
2711
+ // Store element references for controllers
2712
+ if (sceneListElement) editor.options.sceneListElement = sceneListElement;
2713
+ if (previewElement) editor.options.previewElement = previewElement;
2714
+ if (propertiesElement) editor.options.propertiesElement = propertiesElement;
2715
+
2716
+ editor.init().catch((err) => {
2717
+ console.error("Failed to initialize declarative editor:", err);
2718
+ });
2719
+
2720
+ return editor;
2721
+ }
2722
+
2723
+ window.addEventListener("DOMContentLoaded", () => {
2724
+ // Try declarative initialization first
2725
+ const declarativeEditor = initDeclarativeEditor();
2726
+
2727
+ if (declarativeEditor) {
2728
+ // Store editor instance globally for declarative mode
2729
+ window.editor = declarativeEditor;
2730
+ }
2731
+
2732
+ // Setup color picker sync
2733
+ const colorPicker = document.getElementById("hotspotColor");
2734
+ const colorText = document.getElementById("hotspotColorText");
2735
+
2736
+ if (colorPicker && colorText) {
2737
+ colorPicker.addEventListener("input", (e) => {
2738
+ colorText.value = e.target.value;
2739
+ });
2740
+
2741
+ colorText.addEventListener("input", (e) => {
2742
+ if (/^#[0-9A-F]{6}$/i.test(e.target.value)) {
2743
+ colorPicker.value = e.target.value;
2744
+ }
2745
+ });
2746
+ }
2747
+
2748
+ // Keyboard shortcuts
2749
+ document.addEventListener("keydown", (e) => {
2750
+ // Ctrl/Cmd + S to save
2751
+ if ((e.ctrlKey || e.metaKey) && e.key === "s") {
2752
+ e.preventDefault();
2753
+ if (window.editor) window.editor.saveProject();
2754
+ }
2755
+
2756
+ // Ctrl/Cmd + E to export
2757
+ if ((e.ctrlKey || e.metaKey) && e.key === "e") {
2758
+ e.preventDefault();
2759
+ if (window.editor) window.editor.exportManager.showExportPreview();
2760
+ }
2761
+
2762
+ // ESC to close modals
2763
+ if (e.key === "Escape") {
2764
+ document.querySelectorAll(".modal.show").forEach((modal) => {
2765
+ modal.classList.remove("show");
2766
+ });
2767
+ }
2768
+ });
2769
+
2770
+ // Preview button - just focuses on the preview area
2771
+ const previewBtn = document.getElementById("previewBtn");
2772
+ if (previewBtn) {
2773
+ previewBtn.addEventListener("click", () => {
2774
+ const preview = document.getElementById("preview");
2775
+ const canvas = document.getElementById("canvasArea");
2776
+ if (preview && canvas) {
2777
+ canvas.classList.toggle("preview-active");
2778
+ // refresh preview
2779
+ if (window.editor && window.editor.previewController) {
2780
+ window.editor.previewController.refresh();
2781
+ }
2782
+ }
2783
+ });
2784
+ }
2785
+
2786
+ // Modal background click to close
2787
+ document.querySelectorAll(".modal").forEach((modal) => {
2788
+ modal.addEventListener("click", (e) => {
2789
+ if (e.target === modal) {
2790
+ modal.classList.remove("show");
2791
+ }
2792
+ });
2793
+ });
2794
+
2795
+ // Tab switching functionality
2796
+ const tabs = document.querySelectorAll(".tab");
2797
+ const tabContents = document.querySelectorAll(".tab-content");
2798
+
2799
+ tabs.forEach((tab) => {
2800
+ tab.addEventListener("click", () => {
2801
+ const targetTab = tab.dataset.tab;
2802
+
2803
+ // Update tab buttons
2804
+ tabs.forEach((t) => t.classList.remove("active"));
2805
+ tab.classList.add("active");
2806
+
2807
+ // Update tab content
2808
+ tabContents.forEach((content) => {
2809
+ content.style.display = "none";
2810
+ });
2811
+
2812
+ const targetContent = document.getElementById(targetTab + "Tab");
2813
+ if (targetContent) {
2814
+ targetContent.style.display = "block";
2815
+ }
2816
+
2817
+ // Update panel title (if exists)
2818
+ const panelTitle = document.getElementById("panelTitle");
2819
+ if (panelTitle) {
2820
+ switch (targetTab) {
2821
+ case "scene":
2822
+ panelTitle.textContent = "Scene Properties";
2823
+ break;
2824
+ case "hotspot":
2825
+ panelTitle.textContent = "Hotspot Properties";
2826
+ break;
2827
+ case "tour":
2828
+ panelTitle.textContent = "Tour Settings";
2829
+ break;
2830
+ }
2831
+ }
2832
+ });
2833
+ });
2834
+ });
2835
+
2836
+ // SenangWebs Tour Editor - Entry Point
2837
+ // This file bundles all editor modules for distribution
2838
+
2839
+ Object.assign(window, utils);
2840
+
2841
+ // Attach classes to window for global access
2842
+ window.ProjectStorageManager = ProjectStorageManager$1;
2843
+ window.SceneManagerEditor = SceneManagerEditor$1;
2844
+ window.HotspotEditor = HotspotEditor$1;
2845
+ window.PreviewController = PreviewController$1;
2846
+ window.UIController = UIController$1;
2847
+ window.ExportManager = ExportManager$1;
2848
+ window.TourEditor = TourEditor$1;
2849
+
2850
+ return TourEditor$1;
2851
+
2852
+ })();
2853
+ //# sourceMappingURL=swt-editor.js.map