senangwebs-tour 1.0.7 → 1.0.9
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.
- package/README.md +2 -1
- package/dist/swt-editor.js +445 -37
- package/dist/swt-editor.js.map +1 -1
- package/dist/swt-editor.min.js +1 -1
- package/package.json +1 -1
- package/src/editor/js/editor.js +95 -2
- package/src/editor/js/event-emitter.js +257 -0
- package/src/editor/js/export-manager.js +47 -1
- package/src/editor/js/ui-controller.js +18 -2
package/package.json
CHANGED
package/src/editor/js/editor.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Main Editor Controller
|
|
2
2
|
import { debounce, showModal, showToast } from './utils.js';
|
|
3
|
+
import EventEmitter, { EditorEvents } from './event-emitter.js';
|
|
3
4
|
|
|
4
5
|
class TourEditor {
|
|
5
6
|
constructor(options = {}) {
|
|
@@ -29,6 +30,47 @@ class TourEditor {
|
|
|
29
30
|
this.hasUnsavedChanges = false;
|
|
30
31
|
this.lastRenderedSceneIndex = -1;
|
|
31
32
|
this.listenersSetup = false;
|
|
33
|
+
|
|
34
|
+
// Initialize event emitter
|
|
35
|
+
this.events = new EventEmitter();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Subscribe to editor events
|
|
40
|
+
* @param {string} event - Event name (use EditorEvents constants)
|
|
41
|
+
* @param {Function} callback - Callback function
|
|
42
|
+
* @returns {Function} Unsubscribe function
|
|
43
|
+
*/
|
|
44
|
+
on(event, callback) {
|
|
45
|
+
return this.events.on(event, callback);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Subscribe to an event once
|
|
50
|
+
* @param {string} event - Event name
|
|
51
|
+
* @param {Function} callback - Callback function
|
|
52
|
+
* @returns {Function} Unsubscribe function
|
|
53
|
+
*/
|
|
54
|
+
once(event, callback) {
|
|
55
|
+
return this.events.once(event, callback);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Unsubscribe from an event
|
|
60
|
+
* @param {string} event - Event name
|
|
61
|
+
* @param {Function} callback - Callback to remove
|
|
62
|
+
*/
|
|
63
|
+
off(event, callback) {
|
|
64
|
+
this.events.off(event, callback);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Emit an event
|
|
69
|
+
* @param {string} event - Event name
|
|
70
|
+
* @param {Object} data - Event data
|
|
71
|
+
*/
|
|
72
|
+
emit(event, data = {}) {
|
|
73
|
+
this.events.emit(event, data);
|
|
32
74
|
}
|
|
33
75
|
|
|
34
76
|
/**
|
|
@@ -55,8 +97,8 @@ class TourEditor {
|
|
|
55
97
|
// Setup event listeners
|
|
56
98
|
this.setupEventListeners();
|
|
57
99
|
|
|
58
|
-
// Populate icon grid
|
|
59
|
-
this.uiController.populateIconGrid();
|
|
100
|
+
// Populate icon grid (async to wait for custom element registration)
|
|
101
|
+
await this.uiController.populateIconGrid();
|
|
60
102
|
|
|
61
103
|
// Load saved project if exists (but only if it has valid data)
|
|
62
104
|
if (this.storageManager.hasProject()) {
|
|
@@ -89,6 +131,9 @@ class TourEditor {
|
|
|
89
131
|
|
|
90
132
|
showToast('Editor ready', 'success');
|
|
91
133
|
|
|
134
|
+
// Emit ready event
|
|
135
|
+
this.emit(EditorEvents.READY, { config: this.options });
|
|
136
|
+
|
|
92
137
|
return true;
|
|
93
138
|
}
|
|
94
139
|
|
|
@@ -294,6 +339,9 @@ class TourEditor {
|
|
|
294
339
|
}
|
|
295
340
|
|
|
296
341
|
const scene = await this.sceneManager.addScene(file);
|
|
342
|
+
if (scene) {
|
|
343
|
+
this.emit(EditorEvents.SCENE_ADD, { scene, file });
|
|
344
|
+
}
|
|
297
345
|
}
|
|
298
346
|
this.uiController.setLoading(false);
|
|
299
347
|
this.render();
|
|
@@ -327,6 +375,11 @@ class TourEditor {
|
|
|
327
375
|
this.lastRenderedSceneIndex = -1;
|
|
328
376
|
this.render();
|
|
329
377
|
this.markUnsavedChanges();
|
|
378
|
+
this.emit(EditorEvents.HOTSPOT_ADD, {
|
|
379
|
+
hotspot,
|
|
380
|
+
position,
|
|
381
|
+
sceneId: this.sceneManager.getCurrentScene()?.id
|
|
382
|
+
});
|
|
330
383
|
} else {
|
|
331
384
|
console.error('Failed to add hotspot');
|
|
332
385
|
}
|
|
@@ -372,6 +425,8 @@ class TourEditor {
|
|
|
372
425
|
this.uiController.updateHotspotProperties(null);
|
|
373
426
|
this.uiController.updateInitialSceneOptions();
|
|
374
427
|
this.uiController.updateTargetSceneOptions();
|
|
428
|
+
|
|
429
|
+
this.emit(EditorEvents.SCENE_SELECT, { scene, index });
|
|
375
430
|
}
|
|
376
431
|
}
|
|
377
432
|
|
|
@@ -390,6 +445,8 @@ class TourEditor {
|
|
|
390
445
|
if (hotspot) {
|
|
391
446
|
this.previewController.pointCameraToHotspot(hotspot);
|
|
392
447
|
}
|
|
448
|
+
|
|
449
|
+
this.emit(EditorEvents.HOTSPOT_SELECT, { hotspot, index });
|
|
393
450
|
}
|
|
394
451
|
}
|
|
395
452
|
|
|
@@ -397,9 +454,11 @@ class TourEditor {
|
|
|
397
454
|
* Remove scene
|
|
398
455
|
*/
|
|
399
456
|
removeScene(index) {
|
|
457
|
+
const scene = this.sceneManager.getScene(index);
|
|
400
458
|
if (this.sceneManager.removeScene(index)) {
|
|
401
459
|
this.render();
|
|
402
460
|
this.markUnsavedChanges();
|
|
461
|
+
this.emit(EditorEvents.SCENE_REMOVE, { scene, index });
|
|
403
462
|
}
|
|
404
463
|
}
|
|
405
464
|
|
|
@@ -407,10 +466,12 @@ class TourEditor {
|
|
|
407
466
|
* Remove hotspot
|
|
408
467
|
*/
|
|
409
468
|
removeHotspot(index) {
|
|
469
|
+
const hotspot = this.hotspotEditor.getHotspot(index);
|
|
410
470
|
if (this.hotspotEditor.removeHotspot(index)) {
|
|
411
471
|
this.lastRenderedSceneIndex = -1;
|
|
412
472
|
this.render();
|
|
413
473
|
this.markUnsavedChanges();
|
|
474
|
+
this.emit(EditorEvents.HOTSPOT_REMOVE, { hotspot, index });
|
|
414
475
|
}
|
|
415
476
|
}
|
|
416
477
|
|
|
@@ -423,6 +484,7 @@ class TourEditor {
|
|
|
423
484
|
this.lastRenderedSceneIndex = -1;
|
|
424
485
|
this.render();
|
|
425
486
|
this.markUnsavedChanges();
|
|
487
|
+
this.emit(EditorEvents.HOTSPOT_DUPLICATE, { hotspot, originalIndex: index });
|
|
426
488
|
}
|
|
427
489
|
}
|
|
428
490
|
|
|
@@ -433,6 +495,7 @@ class TourEditor {
|
|
|
433
495
|
if (this.sceneManager.reorderScenes(fromIndex, toIndex)) {
|
|
434
496
|
this.render();
|
|
435
497
|
this.markUnsavedChanges();
|
|
498
|
+
this.emit(EditorEvents.SCENE_REORDER, { fromIndex, toIndex });
|
|
436
499
|
}
|
|
437
500
|
}
|
|
438
501
|
|
|
@@ -445,6 +508,12 @@ class TourEditor {
|
|
|
445
508
|
await this.previewController.updateHotspotMarker(index);
|
|
446
509
|
this.uiController.renderHotspotList();
|
|
447
510
|
this.markUnsavedChanges();
|
|
511
|
+
this.emit(EditorEvents.HOTSPOT_UPDATE, {
|
|
512
|
+
hotspot: this.hotspotEditor.getHotspot(index),
|
|
513
|
+
index,
|
|
514
|
+
property,
|
|
515
|
+
value
|
|
516
|
+
});
|
|
448
517
|
}
|
|
449
518
|
}
|
|
450
519
|
|
|
@@ -481,6 +550,13 @@ class TourEditor {
|
|
|
481
550
|
await this.previewController.updateHotspotMarker(index);
|
|
482
551
|
this.uiController.renderHotspotList();
|
|
483
552
|
this.markUnsavedChanges();
|
|
553
|
+
this.emit(EditorEvents.HOTSPOT_POSITION_CHANGE, {
|
|
554
|
+
hotspot,
|
|
555
|
+
index,
|
|
556
|
+
axis,
|
|
557
|
+
value,
|
|
558
|
+
position: hotspot.position
|
|
559
|
+
});
|
|
484
560
|
}
|
|
485
561
|
}
|
|
486
562
|
|
|
@@ -492,6 +568,12 @@ class TourEditor {
|
|
|
492
568
|
if (this.sceneManager.updateScene(index, property, value)) {
|
|
493
569
|
this.uiController.renderSceneList();
|
|
494
570
|
this.markUnsavedChanges();
|
|
571
|
+
this.emit(EditorEvents.SCENE_UPDATE, {
|
|
572
|
+
scene: this.sceneManager.getScene(index),
|
|
573
|
+
index,
|
|
574
|
+
property,
|
|
575
|
+
value
|
|
576
|
+
});
|
|
495
577
|
}
|
|
496
578
|
}
|
|
497
579
|
|
|
@@ -518,6 +600,7 @@ class TourEditor {
|
|
|
518
600
|
showToast('Scene image updated', 'success');
|
|
519
601
|
}
|
|
520
602
|
this.markUnsavedChanges();
|
|
603
|
+
this.emit(EditorEvents.SCENE_IMAGE_CHANGE, { scene, index, imageUrl });
|
|
521
604
|
}
|
|
522
605
|
}
|
|
523
606
|
|
|
@@ -545,6 +628,7 @@ class TourEditor {
|
|
|
545
628
|
this.uiController.updateSceneProperties(scene);
|
|
546
629
|
this.markUnsavedChanges();
|
|
547
630
|
showToast('Starting position set', 'success');
|
|
631
|
+
this.emit(EditorEvents.SCENE_STARTING_POSITION_SET, { scene, startingPosition: scene.startingPosition });
|
|
548
632
|
}
|
|
549
633
|
|
|
550
634
|
/**
|
|
@@ -562,6 +646,7 @@ class TourEditor {
|
|
|
562
646
|
this.uiController.updateSceneProperties(scene);
|
|
563
647
|
this.markUnsavedChanges();
|
|
564
648
|
showToast('Starting position cleared', 'success');
|
|
649
|
+
this.emit(EditorEvents.SCENE_STARTING_POSITION_CLEAR, { scene });
|
|
565
650
|
}
|
|
566
651
|
|
|
567
652
|
/**
|
|
@@ -570,6 +655,7 @@ class TourEditor {
|
|
|
570
655
|
render() {
|
|
571
656
|
this.uiController.renderSceneList();
|
|
572
657
|
this.uiController.renderHotspotList();
|
|
658
|
+
this.uiController.populateIconGrid(); // Re-render icon grid to ensure icons display
|
|
573
659
|
|
|
574
660
|
const currentScene = this.sceneManager.getCurrentScene();
|
|
575
661
|
const currentHotspot = this.hotspotEditor.getCurrentHotspot();
|
|
@@ -602,6 +688,8 @@ class TourEditor {
|
|
|
602
688
|
}
|
|
603
689
|
this.lastRenderedSceneIndex = -1;
|
|
604
690
|
}
|
|
691
|
+
|
|
692
|
+
this.emit(EditorEvents.UI_RENDER);
|
|
605
693
|
}
|
|
606
694
|
|
|
607
695
|
/**
|
|
@@ -616,6 +704,7 @@ class TourEditor {
|
|
|
616
704
|
if (this.storageManager.saveProject(projectData)) {
|
|
617
705
|
this.hasUnsavedChanges = false;
|
|
618
706
|
showToast('Project saved', 'success');
|
|
707
|
+
this.emit(EditorEvents.PROJECT_SAVE, { projectData });
|
|
619
708
|
return true;
|
|
620
709
|
}
|
|
621
710
|
return false;
|
|
@@ -632,6 +721,7 @@ class TourEditor {
|
|
|
632
721
|
this.hasUnsavedChanges = false;
|
|
633
722
|
this.render();
|
|
634
723
|
showToast('Project loaded', 'success');
|
|
724
|
+
this.emit(EditorEvents.PROJECT_LOAD, { projectData });
|
|
635
725
|
return true;
|
|
636
726
|
}
|
|
637
727
|
return false;
|
|
@@ -658,6 +748,7 @@ class TourEditor {
|
|
|
658
748
|
this.render();
|
|
659
749
|
|
|
660
750
|
showToast('New project created', 'success');
|
|
751
|
+
this.emit(EditorEvents.PROJECT_NEW, { config: this.config });
|
|
661
752
|
return true;
|
|
662
753
|
}
|
|
663
754
|
|
|
@@ -688,6 +779,7 @@ const importUpload = document.getElementById('importUpload');
|
|
|
688
779
|
this.uiController.setLoading(false);
|
|
689
780
|
|
|
690
781
|
showToast('Project imported successfully', 'success');
|
|
782
|
+
this.emit(EditorEvents.PROJECT_IMPORT, { projectData, file });
|
|
691
783
|
} catch (error) {
|
|
692
784
|
this.uiController.setLoading(false);
|
|
693
785
|
console.error('Import failed:', error);
|
|
@@ -713,3 +805,4 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
|
713
805
|
});
|
|
714
806
|
|
|
715
807
|
export default TourEditor;
|
|
808
|
+
export { EditorEvents } from './event-emitter.js';
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event Emitter for TourEditor
|
|
3
|
+
*
|
|
4
|
+
* Provides a comprehensive event system for the editor with:
|
|
5
|
+
* - Specific events for all editor operations
|
|
6
|
+
* - A unified 'change' event that fires for any modification
|
|
7
|
+
* - Support for wildcards and namespaced events
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Event types for the Tour Editor
|
|
12
|
+
* These are the available events that can be listened to
|
|
13
|
+
*/
|
|
14
|
+
export const EditorEvents = {
|
|
15
|
+
// Lifecycle
|
|
16
|
+
INIT: 'init',
|
|
17
|
+
READY: 'ready',
|
|
18
|
+
DESTROY: 'destroy',
|
|
19
|
+
|
|
20
|
+
// Scene events
|
|
21
|
+
SCENE_ADD: 'scene:add',
|
|
22
|
+
SCENE_REMOVE: 'scene:remove',
|
|
23
|
+
SCENE_SELECT: 'scene:select',
|
|
24
|
+
SCENE_UPDATE: 'scene:update',
|
|
25
|
+
SCENE_REORDER: 'scene:reorder',
|
|
26
|
+
SCENE_CLEAR: 'scene:clear',
|
|
27
|
+
SCENE_IMAGE_CHANGE: 'scene:imageChange',
|
|
28
|
+
SCENE_STARTING_POSITION_SET: 'scene:startingPositionSet',
|
|
29
|
+
SCENE_STARTING_POSITION_CLEAR: 'scene:startingPositionClear',
|
|
30
|
+
|
|
31
|
+
// Hotspot events
|
|
32
|
+
HOTSPOT_ADD: 'hotspot:add',
|
|
33
|
+
HOTSPOT_REMOVE: 'hotspot:remove',
|
|
34
|
+
HOTSPOT_SELECT: 'hotspot:select',
|
|
35
|
+
HOTSPOT_UPDATE: 'hotspot:update',
|
|
36
|
+
HOTSPOT_DUPLICATE: 'hotspot:duplicate',
|
|
37
|
+
HOTSPOT_POSITION_CHANGE: 'hotspot:positionChange',
|
|
38
|
+
|
|
39
|
+
// Project events
|
|
40
|
+
PROJECT_NEW: 'project:new',
|
|
41
|
+
PROJECT_SAVE: 'project:save',
|
|
42
|
+
PROJECT_LOAD: 'project:load',
|
|
43
|
+
PROJECT_IMPORT: 'project:import',
|
|
44
|
+
PROJECT_EXPORT: 'project:export',
|
|
45
|
+
|
|
46
|
+
// Config events
|
|
47
|
+
CONFIG_UPDATE: 'config:update',
|
|
48
|
+
INITIAL_SCENE_CHANGE: 'config:initialSceneChange',
|
|
49
|
+
|
|
50
|
+
// Preview events
|
|
51
|
+
PREVIEW_START: 'preview:start',
|
|
52
|
+
PREVIEW_STOP: 'preview:stop',
|
|
53
|
+
PREVIEW_SCENE_CHANGE: 'preview:sceneChange',
|
|
54
|
+
|
|
55
|
+
// UI events
|
|
56
|
+
UI_RENDER: 'ui:render',
|
|
57
|
+
UI_LOADING_START: 'ui:loadingStart',
|
|
58
|
+
UI_LOADING_END: 'ui:loadingEnd',
|
|
59
|
+
MODAL_OPEN: 'ui:modalOpen',
|
|
60
|
+
MODAL_CLOSE: 'ui:modalClose',
|
|
61
|
+
|
|
62
|
+
// Data events
|
|
63
|
+
DATA_CHANGE: 'data:change', // Fires when any data changes
|
|
64
|
+
UNSAVED_CHANGES: 'data:unsavedChanges',
|
|
65
|
+
|
|
66
|
+
// Unified change event - fires for ANY modification
|
|
67
|
+
CHANGE: 'change'
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Event Emitter class
|
|
72
|
+
* Provides pub/sub functionality for editor events
|
|
73
|
+
*/
|
|
74
|
+
class EventEmitter {
|
|
75
|
+
constructor() {
|
|
76
|
+
this.listeners = new Map();
|
|
77
|
+
this.onceListeners = new Map();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Register an event listener
|
|
82
|
+
* @param {string} event - Event name or 'change' for all changes
|
|
83
|
+
* @param {Function} callback - Function to call when event fires
|
|
84
|
+
* @returns {Function} Unsubscribe function
|
|
85
|
+
*/
|
|
86
|
+
on(event, callback) {
|
|
87
|
+
if (!this.listeners.has(event)) {
|
|
88
|
+
this.listeners.set(event, new Set());
|
|
89
|
+
}
|
|
90
|
+
this.listeners.get(event).add(callback);
|
|
91
|
+
|
|
92
|
+
// Return unsubscribe function
|
|
93
|
+
return () => this.off(event, callback);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Register a one-time event listener
|
|
98
|
+
* @param {string} event - Event name
|
|
99
|
+
* @param {Function} callback - Function to call once when event fires
|
|
100
|
+
* @returns {Function} Unsubscribe function
|
|
101
|
+
*/
|
|
102
|
+
once(event, callback) {
|
|
103
|
+
if (!this.onceListeners.has(event)) {
|
|
104
|
+
this.onceListeners.set(event, new Set());
|
|
105
|
+
}
|
|
106
|
+
this.onceListeners.get(event).add(callback);
|
|
107
|
+
|
|
108
|
+
return () => {
|
|
109
|
+
if (this.onceListeners.has(event)) {
|
|
110
|
+
this.onceListeners.get(event).delete(callback);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Remove an event listener
|
|
117
|
+
* @param {string} event - Event name
|
|
118
|
+
* @param {Function} callback - Function to remove
|
|
119
|
+
*/
|
|
120
|
+
off(event, callback) {
|
|
121
|
+
if (this.listeners.has(event)) {
|
|
122
|
+
this.listeners.get(event).delete(callback);
|
|
123
|
+
}
|
|
124
|
+
if (this.onceListeners.has(event)) {
|
|
125
|
+
this.onceListeners.get(event).delete(callback);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Remove all listeners for an event, or all listeners if no event specified
|
|
131
|
+
* @param {string} [event] - Optional event name
|
|
132
|
+
*/
|
|
133
|
+
removeAllListeners(event) {
|
|
134
|
+
if (event) {
|
|
135
|
+
this.listeners.delete(event);
|
|
136
|
+
this.onceListeners.delete(event);
|
|
137
|
+
} else {
|
|
138
|
+
this.listeners.clear();
|
|
139
|
+
this.onceListeners.clear();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Emit an event
|
|
145
|
+
* @param {string} event - Event name
|
|
146
|
+
* @param {Object} data - Event data
|
|
147
|
+
*/
|
|
148
|
+
emit(event, data = {}) {
|
|
149
|
+
const eventData = {
|
|
150
|
+
type: event,
|
|
151
|
+
timestamp: Date.now(),
|
|
152
|
+
...data
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Call specific event listeners
|
|
156
|
+
if (this.listeners.has(event)) {
|
|
157
|
+
this.listeners.get(event).forEach(callback => {
|
|
158
|
+
try {
|
|
159
|
+
callback(eventData);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.error(`Error in event listener for "${event}":`, error);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Call once listeners and remove them
|
|
167
|
+
if (this.onceListeners.has(event)) {
|
|
168
|
+
const onceCallbacks = this.onceListeners.get(event);
|
|
169
|
+
this.onceListeners.delete(event);
|
|
170
|
+
onceCallbacks.forEach(callback => {
|
|
171
|
+
try {
|
|
172
|
+
callback(eventData);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.error(`Error in once listener for "${event}":`, error);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Also emit to wildcard listeners (namespace:*)
|
|
180
|
+
const namespace = event.split(':')[0];
|
|
181
|
+
const wildcardEvent = `${namespace}:*`;
|
|
182
|
+
if (this.listeners.has(wildcardEvent)) {
|
|
183
|
+
this.listeners.get(wildcardEvent).forEach(callback => {
|
|
184
|
+
try {
|
|
185
|
+
callback(eventData);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.error(`Error in wildcard listener for "${wildcardEvent}":`, error);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Emit unified 'change' event for data-modifying events
|
|
193
|
+
if (this.isDataModifyingEvent(event) && event !== EditorEvents.CHANGE) {
|
|
194
|
+
this.emit(EditorEvents.CHANGE, {
|
|
195
|
+
originalEvent: event,
|
|
196
|
+
...data
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Check if an event modifies data (should trigger 'change' event)
|
|
203
|
+
* @param {string} event - Event name
|
|
204
|
+
* @returns {boolean}
|
|
205
|
+
*/
|
|
206
|
+
isDataModifyingEvent(event) {
|
|
207
|
+
const dataEvents = [
|
|
208
|
+
EditorEvents.SCENE_ADD,
|
|
209
|
+
EditorEvents.SCENE_REMOVE,
|
|
210
|
+
EditorEvents.SCENE_UPDATE,
|
|
211
|
+
EditorEvents.SCENE_REORDER,
|
|
212
|
+
EditorEvents.SCENE_CLEAR,
|
|
213
|
+
EditorEvents.SCENE_IMAGE_CHANGE,
|
|
214
|
+
EditorEvents.SCENE_STARTING_POSITION_SET,
|
|
215
|
+
EditorEvents.SCENE_STARTING_POSITION_CLEAR,
|
|
216
|
+
EditorEvents.HOTSPOT_ADD,
|
|
217
|
+
EditorEvents.HOTSPOT_REMOVE,
|
|
218
|
+
EditorEvents.HOTSPOT_UPDATE,
|
|
219
|
+
EditorEvents.HOTSPOT_DUPLICATE,
|
|
220
|
+
EditorEvents.HOTSPOT_POSITION_CHANGE,
|
|
221
|
+
EditorEvents.CONFIG_UPDATE,
|
|
222
|
+
EditorEvents.INITIAL_SCENE_CHANGE,
|
|
223
|
+
EditorEvents.PROJECT_LOAD,
|
|
224
|
+
EditorEvents.PROJECT_IMPORT,
|
|
225
|
+
EditorEvents.PROJECT_NEW,
|
|
226
|
+
EditorEvents.DATA_CHANGE
|
|
227
|
+
];
|
|
228
|
+
return dataEvents.includes(event);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get the number of listeners for an event
|
|
233
|
+
* @param {string} event - Event name
|
|
234
|
+
* @returns {number}
|
|
235
|
+
*/
|
|
236
|
+
listenerCount(event) {
|
|
237
|
+
let count = 0;
|
|
238
|
+
if (this.listeners.has(event)) {
|
|
239
|
+
count += this.listeners.get(event).size;
|
|
240
|
+
}
|
|
241
|
+
if (this.onceListeners.has(event)) {
|
|
242
|
+
count += this.onceListeners.get(event).size;
|
|
243
|
+
}
|
|
244
|
+
return count;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Get all event names that have listeners
|
|
249
|
+
* @returns {string[]}
|
|
250
|
+
*/
|
|
251
|
+
eventNames() {
|
|
252
|
+
const names = new Set([...this.listeners.keys(), ...this.onceListeners.keys()]);
|
|
253
|
+
return Array.from(names);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export default EventEmitter;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Export Manager - Handles JSON generation for SWT library
|
|
2
|
-
import { downloadTextAsFile, showModal, copyToClipboard } from "./utils.js";
|
|
2
|
+
import { downloadTextAsFile, showModal, copyToClipboard, showToast } from "./utils.js";
|
|
3
3
|
import { IconRenderer } from "../../IconRenderer.js";
|
|
4
4
|
import { buildTourConfig } from "./data-transform.js";
|
|
5
|
+
import { EditorEvents } from "./event-emitter.js";
|
|
5
6
|
|
|
6
7
|
class ExportManager {
|
|
7
8
|
constructor(editor) {
|
|
@@ -21,6 +22,51 @@ class ExportManager {
|
|
|
21
22
|
return buildTourConfig(config, scenes);
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Load tour data from JSON (inverse of generateJSON)
|
|
27
|
+
* This loads the entire tour configuration including initialScene and all scenes
|
|
28
|
+
* @param {Object} tourData - Tour configuration object with initialScene and scenes
|
|
29
|
+
* @param {string} tourData.initialScene - Initial scene ID
|
|
30
|
+
* @param {Array} tourData.scenes - Array of scene objects
|
|
31
|
+
* @returns {boolean} Success status
|
|
32
|
+
*/
|
|
33
|
+
loadJSON(tourData) {
|
|
34
|
+
try {
|
|
35
|
+
if (!tourData || typeof tourData !== 'object') {
|
|
36
|
+
console.error('Invalid tour data: expected object');
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Load scenes into scene manager
|
|
41
|
+
const scenes = tourData.scenes || [];
|
|
42
|
+
this.editor.sceneManager.loadScenes(scenes);
|
|
43
|
+
|
|
44
|
+
// Set initial scene in config
|
|
45
|
+
if (tourData.initialScene) {
|
|
46
|
+
this.editor.config.initialSceneId = tourData.initialScene;
|
|
47
|
+
} else if (scenes.length > 0) {
|
|
48
|
+
this.editor.config.initialSceneId = scenes[0].id;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Mark as having unsaved changes
|
|
52
|
+
this.editor.hasUnsavedChanges = true;
|
|
53
|
+
|
|
54
|
+
// Re-render the editor UI
|
|
55
|
+
this.editor.render();
|
|
56
|
+
|
|
57
|
+
showToast('Tour loaded successfully', 'success');
|
|
58
|
+
|
|
59
|
+
// Emit event
|
|
60
|
+
this.editor.emit(EditorEvents.PROJECT_LOAD, { tourData, source: 'loadJSON' });
|
|
61
|
+
|
|
62
|
+
return true;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error('Failed to load tour data:', error);
|
|
65
|
+
showToast('Failed to load tour', 'error');
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
24
70
|
/**
|
|
25
71
|
* Generate JSON with icons baked in as SVG data URLs
|
|
26
72
|
* This ensures the exported HTML doesn't need the SenangStart icons library
|
|
@@ -281,11 +281,27 @@ class UIController {
|
|
|
281
281
|
|
|
282
282
|
/**
|
|
283
283
|
* Populate icon grid from SenangStart icons (baked in at build time)
|
|
284
|
+
* Waits for ss-icon custom element to be defined to avoid race conditions
|
|
284
285
|
*/
|
|
285
|
-
populateIconGrid() {
|
|
286
|
+
async populateIconGrid() {
|
|
286
287
|
const grid = document.getElementById("hotspotIconGrid");
|
|
287
288
|
if (!grid) return;
|
|
288
289
|
|
|
290
|
+
// Wait for ss-icon custom element to be defined before populating
|
|
291
|
+
// This prevents race conditions where icons don't render if the
|
|
292
|
+
// custom element isn't registered yet when this method runs
|
|
293
|
+
try {
|
|
294
|
+
if (customElements.get('ss-icon') === undefined) {
|
|
295
|
+
// Give a reasonable timeout to avoid infinite waiting
|
|
296
|
+
await Promise.race([
|
|
297
|
+
customElements.whenDefined('ss-icon'),
|
|
298
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('ss-icon timeout')), 5000))
|
|
299
|
+
]);
|
|
300
|
+
}
|
|
301
|
+
} catch (err) {
|
|
302
|
+
console.warn('ss-icon custom element not available, icon grid may not render properly:', err.message);
|
|
303
|
+
}
|
|
304
|
+
|
|
289
305
|
// Clear existing content
|
|
290
306
|
grid.innerHTML = "";
|
|
291
307
|
|
|
@@ -309,7 +325,7 @@ class UIController {
|
|
|
309
325
|
grid.appendChild(btn);
|
|
310
326
|
});
|
|
311
327
|
|
|
312
|
-
console.log(`Loaded ${iconsData.length} icons`);
|
|
328
|
+
// console.log(`Loaded ${iconsData.length} icons`);
|
|
313
329
|
}
|
|
314
330
|
|
|
315
331
|
/**
|