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/dist/swt-editor.js
CHANGED
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
/**
|
|
80
80
|
* Show toast notification
|
|
81
81
|
*/
|
|
82
|
-
function showToast
|
|
82
|
+
function showToast(message, type = 'info', duration = 3000) {
|
|
83
83
|
const toast = document.getElementById('toast');
|
|
84
84
|
toast.textContent = message;
|
|
85
85
|
toast.className = `toast ${type}`;
|
|
@@ -218,7 +218,7 @@
|
|
|
218
218
|
positionToString: positionToString,
|
|
219
219
|
sanitizeId: sanitizeId$1,
|
|
220
220
|
showModal: showModal,
|
|
221
|
-
showToast: showToast
|
|
221
|
+
showToast: showToast
|
|
222
222
|
});
|
|
223
223
|
|
|
224
224
|
// Storage Manager - Handles LocalStorage operations
|
|
@@ -245,7 +245,7 @@
|
|
|
245
245
|
} catch (error) {
|
|
246
246
|
console.error("Failed to save project:", error);
|
|
247
247
|
if (error.name === "QuotaExceededError") {
|
|
248
|
-
showToast
|
|
248
|
+
showToast("Storage quota exceeded. Project too large!", "error");
|
|
249
249
|
}
|
|
250
250
|
return false;
|
|
251
251
|
}
|
|
@@ -271,7 +271,7 @@
|
|
|
271
271
|
return null;
|
|
272
272
|
} catch (error) {
|
|
273
273
|
console.error("Failed to load project:", error);
|
|
274
|
-
showToast
|
|
274
|
+
showToast("Failed to load project", "error");
|
|
275
275
|
return null;
|
|
276
276
|
}
|
|
277
277
|
}
|
|
@@ -380,7 +380,7 @@
|
|
|
380
380
|
return true;
|
|
381
381
|
} catch (error) {
|
|
382
382
|
console.error("Failed to export project:", error);
|
|
383
|
-
showToast
|
|
383
|
+
showToast("Failed to export project", "error");
|
|
384
384
|
return false;
|
|
385
385
|
}
|
|
386
386
|
}
|
|
@@ -398,14 +398,14 @@
|
|
|
398
398
|
resolve(projectData);
|
|
399
399
|
} catch (error) {
|
|
400
400
|
console.error("Failed to parse project file:", error);
|
|
401
|
-
showToast
|
|
401
|
+
showToast("Invalid project file", "error");
|
|
402
402
|
reject(error);
|
|
403
403
|
}
|
|
404
404
|
};
|
|
405
405
|
|
|
406
406
|
reader.onerror = () => {
|
|
407
407
|
console.error("Failed to read file:", reader.error);
|
|
408
|
-
showToast
|
|
408
|
+
showToast("Failed to read file", "error");
|
|
409
409
|
reject(reader.error);
|
|
410
410
|
};
|
|
411
411
|
|
|
@@ -465,11 +465,11 @@
|
|
|
465
465
|
|
|
466
466
|
this.scenes.push(scene);
|
|
467
467
|
this.currentSceneIndex = this.scenes.length - 1;
|
|
468
|
-
showToast
|
|
468
|
+
showToast(`Scene "${scene.name}" added successfully`, "success");
|
|
469
469
|
return scene;
|
|
470
470
|
} catch (error) {
|
|
471
471
|
console.error("Failed to add scene:", error);
|
|
472
|
-
showToast
|
|
472
|
+
showToast("Failed to add scene", "error");
|
|
473
473
|
return null;
|
|
474
474
|
}
|
|
475
475
|
}
|
|
@@ -498,7 +498,7 @@
|
|
|
498
498
|
this.currentSceneIndex--;
|
|
499
499
|
}
|
|
500
500
|
|
|
501
|
-
showToast
|
|
501
|
+
showToast(`Scene "${scene.name}" removed`, "success");
|
|
502
502
|
return true;
|
|
503
503
|
}
|
|
504
504
|
return false;
|
|
@@ -648,7 +648,7 @@
|
|
|
648
648
|
addHotspot(position, targetSceneId = '', cameraOrientation = null) {
|
|
649
649
|
const scene = this.editor.sceneManager.getCurrentScene();
|
|
650
650
|
if (!scene) {
|
|
651
|
-
showToast
|
|
651
|
+
showToast('No scene selected', 'error');
|
|
652
652
|
return null;
|
|
653
653
|
}
|
|
654
654
|
|
|
@@ -675,7 +675,7 @@
|
|
|
675
675
|
scene.hotspots.push(hotspot);
|
|
676
676
|
this.currentHotspotIndex = scene.hotspots.length - 1;
|
|
677
677
|
|
|
678
|
-
showToast
|
|
678
|
+
showToast('Hotspot added', 'success');
|
|
679
679
|
|
|
680
680
|
return hotspot;
|
|
681
681
|
}
|
|
@@ -702,7 +702,7 @@
|
|
|
702
702
|
this.currentHotspotIndex--;
|
|
703
703
|
}
|
|
704
704
|
|
|
705
|
-
showToast
|
|
705
|
+
showToast('Hotspot removed', 'success');
|
|
706
706
|
return true;
|
|
707
707
|
}
|
|
708
708
|
|
|
@@ -816,7 +816,7 @@
|
|
|
816
816
|
scene.hotspots.push(duplicate);
|
|
817
817
|
this.currentHotspotIndex = scene.hotspots.length - 1;
|
|
818
818
|
|
|
819
|
-
showToast
|
|
819
|
+
showToast('Hotspot duplicated', 'success');
|
|
820
820
|
return duplicate;
|
|
821
821
|
}
|
|
822
822
|
|
|
@@ -840,7 +840,7 @@
|
|
|
840
840
|
scene.hotspots = [];
|
|
841
841
|
this.currentHotspotIndex = -1;
|
|
842
842
|
|
|
843
|
-
showToast
|
|
843
|
+
showToast('All hotspots removed', 'success');
|
|
844
844
|
return true;
|
|
845
845
|
}
|
|
846
846
|
};
|
|
@@ -1066,7 +1066,7 @@
|
|
|
1066
1066
|
}
|
|
1067
1067
|
} catch (error) {
|
|
1068
1068
|
console.error("Failed to load preview:", error);
|
|
1069
|
-
showToast
|
|
1069
|
+
showToast("Failed to load preview: " + error.message, "error");
|
|
1070
1070
|
// Hide loading on error
|
|
1071
1071
|
this.hideLoading();
|
|
1072
1072
|
}
|
|
@@ -4269,11 +4269,27 @@
|
|
|
4269
4269
|
|
|
4270
4270
|
/**
|
|
4271
4271
|
* Populate icon grid from SenangStart icons (baked in at build time)
|
|
4272
|
+
* Waits for ss-icon custom element to be defined to avoid race conditions
|
|
4272
4273
|
*/
|
|
4273
|
-
populateIconGrid() {
|
|
4274
|
+
async populateIconGrid() {
|
|
4274
4275
|
const grid = document.getElementById("hotspotIconGrid");
|
|
4275
4276
|
if (!grid) return;
|
|
4276
4277
|
|
|
4278
|
+
// Wait for ss-icon custom element to be defined before populating
|
|
4279
|
+
// This prevents race conditions where icons don't render if the
|
|
4280
|
+
// custom element isn't registered yet when this method runs
|
|
4281
|
+
try {
|
|
4282
|
+
if (customElements.get('ss-icon') === undefined) {
|
|
4283
|
+
// Give a reasonable timeout to avoid infinite waiting
|
|
4284
|
+
await Promise.race([
|
|
4285
|
+
customElements.whenDefined('ss-icon'),
|
|
4286
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('ss-icon timeout')), 5000))
|
|
4287
|
+
]);
|
|
4288
|
+
}
|
|
4289
|
+
} catch (err) {
|
|
4290
|
+
console.warn('ss-icon custom element not available, icon grid may not render properly:', err.message);
|
|
4291
|
+
}
|
|
4292
|
+
|
|
4277
4293
|
// Clear existing content
|
|
4278
4294
|
grid.innerHTML = "";
|
|
4279
4295
|
|
|
@@ -4297,7 +4313,7 @@
|
|
|
4297
4313
|
grid.appendChild(btn);
|
|
4298
4314
|
});
|
|
4299
4315
|
|
|
4300
|
-
console.log(`Loaded ${iconsData.length} icons`);
|
|
4316
|
+
// console.log(`Loaded ${iconsData.length} icons`);
|
|
4301
4317
|
}
|
|
4302
4318
|
|
|
4303
4319
|
/**
|
|
@@ -4608,6 +4624,262 @@
|
|
|
4608
4624
|
}
|
|
4609
4625
|
}
|
|
4610
4626
|
|
|
4627
|
+
/**
|
|
4628
|
+
* Event Emitter for TourEditor
|
|
4629
|
+
*
|
|
4630
|
+
* Provides a comprehensive event system for the editor with:
|
|
4631
|
+
* - Specific events for all editor operations
|
|
4632
|
+
* - A unified 'change' event that fires for any modification
|
|
4633
|
+
* - Support for wildcards and namespaced events
|
|
4634
|
+
*/
|
|
4635
|
+
|
|
4636
|
+
/**
|
|
4637
|
+
* Event types for the Tour Editor
|
|
4638
|
+
* These are the available events that can be listened to
|
|
4639
|
+
*/
|
|
4640
|
+
const EditorEvents = {
|
|
4641
|
+
// Lifecycle
|
|
4642
|
+
INIT: 'init',
|
|
4643
|
+
READY: 'ready',
|
|
4644
|
+
DESTROY: 'destroy',
|
|
4645
|
+
|
|
4646
|
+
// Scene events
|
|
4647
|
+
SCENE_ADD: 'scene:add',
|
|
4648
|
+
SCENE_REMOVE: 'scene:remove',
|
|
4649
|
+
SCENE_SELECT: 'scene:select',
|
|
4650
|
+
SCENE_UPDATE: 'scene:update',
|
|
4651
|
+
SCENE_REORDER: 'scene:reorder',
|
|
4652
|
+
SCENE_CLEAR: 'scene:clear',
|
|
4653
|
+
SCENE_IMAGE_CHANGE: 'scene:imageChange',
|
|
4654
|
+
SCENE_STARTING_POSITION_SET: 'scene:startingPositionSet',
|
|
4655
|
+
SCENE_STARTING_POSITION_CLEAR: 'scene:startingPositionClear',
|
|
4656
|
+
|
|
4657
|
+
// Hotspot events
|
|
4658
|
+
HOTSPOT_ADD: 'hotspot:add',
|
|
4659
|
+
HOTSPOT_REMOVE: 'hotspot:remove',
|
|
4660
|
+
HOTSPOT_SELECT: 'hotspot:select',
|
|
4661
|
+
HOTSPOT_UPDATE: 'hotspot:update',
|
|
4662
|
+
HOTSPOT_DUPLICATE: 'hotspot:duplicate',
|
|
4663
|
+
HOTSPOT_POSITION_CHANGE: 'hotspot:positionChange',
|
|
4664
|
+
|
|
4665
|
+
// Project events
|
|
4666
|
+
PROJECT_NEW: 'project:new',
|
|
4667
|
+
PROJECT_SAVE: 'project:save',
|
|
4668
|
+
PROJECT_LOAD: 'project:load',
|
|
4669
|
+
PROJECT_IMPORT: 'project:import',
|
|
4670
|
+
PROJECT_EXPORT: 'project:export',
|
|
4671
|
+
|
|
4672
|
+
// Config events
|
|
4673
|
+
CONFIG_UPDATE: 'config:update',
|
|
4674
|
+
INITIAL_SCENE_CHANGE: 'config:initialSceneChange',
|
|
4675
|
+
|
|
4676
|
+
// Preview events
|
|
4677
|
+
PREVIEW_START: 'preview:start',
|
|
4678
|
+
PREVIEW_STOP: 'preview:stop',
|
|
4679
|
+
PREVIEW_SCENE_CHANGE: 'preview:sceneChange',
|
|
4680
|
+
|
|
4681
|
+
// UI events
|
|
4682
|
+
UI_RENDER: 'ui:render',
|
|
4683
|
+
UI_LOADING_START: 'ui:loadingStart',
|
|
4684
|
+
UI_LOADING_END: 'ui:loadingEnd',
|
|
4685
|
+
MODAL_OPEN: 'ui:modalOpen',
|
|
4686
|
+
MODAL_CLOSE: 'ui:modalClose',
|
|
4687
|
+
|
|
4688
|
+
// Data events
|
|
4689
|
+
DATA_CHANGE: 'data:change', // Fires when any data changes
|
|
4690
|
+
UNSAVED_CHANGES: 'data:unsavedChanges',
|
|
4691
|
+
|
|
4692
|
+
// Unified change event - fires for ANY modification
|
|
4693
|
+
CHANGE: 'change'
|
|
4694
|
+
};
|
|
4695
|
+
|
|
4696
|
+
/**
|
|
4697
|
+
* Event Emitter class
|
|
4698
|
+
* Provides pub/sub functionality for editor events
|
|
4699
|
+
*/
|
|
4700
|
+
class EventEmitter {
|
|
4701
|
+
constructor() {
|
|
4702
|
+
this.listeners = new Map();
|
|
4703
|
+
this.onceListeners = new Map();
|
|
4704
|
+
}
|
|
4705
|
+
|
|
4706
|
+
/**
|
|
4707
|
+
* Register an event listener
|
|
4708
|
+
* @param {string} event - Event name or 'change' for all changes
|
|
4709
|
+
* @param {Function} callback - Function to call when event fires
|
|
4710
|
+
* @returns {Function} Unsubscribe function
|
|
4711
|
+
*/
|
|
4712
|
+
on(event, callback) {
|
|
4713
|
+
if (!this.listeners.has(event)) {
|
|
4714
|
+
this.listeners.set(event, new Set());
|
|
4715
|
+
}
|
|
4716
|
+
this.listeners.get(event).add(callback);
|
|
4717
|
+
|
|
4718
|
+
// Return unsubscribe function
|
|
4719
|
+
return () => this.off(event, callback);
|
|
4720
|
+
}
|
|
4721
|
+
|
|
4722
|
+
/**
|
|
4723
|
+
* Register a one-time event listener
|
|
4724
|
+
* @param {string} event - Event name
|
|
4725
|
+
* @param {Function} callback - Function to call once when event fires
|
|
4726
|
+
* @returns {Function} Unsubscribe function
|
|
4727
|
+
*/
|
|
4728
|
+
once(event, callback) {
|
|
4729
|
+
if (!this.onceListeners.has(event)) {
|
|
4730
|
+
this.onceListeners.set(event, new Set());
|
|
4731
|
+
}
|
|
4732
|
+
this.onceListeners.get(event).add(callback);
|
|
4733
|
+
|
|
4734
|
+
return () => {
|
|
4735
|
+
if (this.onceListeners.has(event)) {
|
|
4736
|
+
this.onceListeners.get(event).delete(callback);
|
|
4737
|
+
}
|
|
4738
|
+
};
|
|
4739
|
+
}
|
|
4740
|
+
|
|
4741
|
+
/**
|
|
4742
|
+
* Remove an event listener
|
|
4743
|
+
* @param {string} event - Event name
|
|
4744
|
+
* @param {Function} callback - Function to remove
|
|
4745
|
+
*/
|
|
4746
|
+
off(event, callback) {
|
|
4747
|
+
if (this.listeners.has(event)) {
|
|
4748
|
+
this.listeners.get(event).delete(callback);
|
|
4749
|
+
}
|
|
4750
|
+
if (this.onceListeners.has(event)) {
|
|
4751
|
+
this.onceListeners.get(event).delete(callback);
|
|
4752
|
+
}
|
|
4753
|
+
}
|
|
4754
|
+
|
|
4755
|
+
/**
|
|
4756
|
+
* Remove all listeners for an event, or all listeners if no event specified
|
|
4757
|
+
* @param {string} [event] - Optional event name
|
|
4758
|
+
*/
|
|
4759
|
+
removeAllListeners(event) {
|
|
4760
|
+
if (event) {
|
|
4761
|
+
this.listeners.delete(event);
|
|
4762
|
+
this.onceListeners.delete(event);
|
|
4763
|
+
} else {
|
|
4764
|
+
this.listeners.clear();
|
|
4765
|
+
this.onceListeners.clear();
|
|
4766
|
+
}
|
|
4767
|
+
}
|
|
4768
|
+
|
|
4769
|
+
/**
|
|
4770
|
+
* Emit an event
|
|
4771
|
+
* @param {string} event - Event name
|
|
4772
|
+
* @param {Object} data - Event data
|
|
4773
|
+
*/
|
|
4774
|
+
emit(event, data = {}) {
|
|
4775
|
+
const eventData = {
|
|
4776
|
+
type: event,
|
|
4777
|
+
timestamp: Date.now(),
|
|
4778
|
+
...data
|
|
4779
|
+
};
|
|
4780
|
+
|
|
4781
|
+
// Call specific event listeners
|
|
4782
|
+
if (this.listeners.has(event)) {
|
|
4783
|
+
this.listeners.get(event).forEach(callback => {
|
|
4784
|
+
try {
|
|
4785
|
+
callback(eventData);
|
|
4786
|
+
} catch (error) {
|
|
4787
|
+
console.error(`Error in event listener for "${event}":`, error);
|
|
4788
|
+
}
|
|
4789
|
+
});
|
|
4790
|
+
}
|
|
4791
|
+
|
|
4792
|
+
// Call once listeners and remove them
|
|
4793
|
+
if (this.onceListeners.has(event)) {
|
|
4794
|
+
const onceCallbacks = this.onceListeners.get(event);
|
|
4795
|
+
this.onceListeners.delete(event);
|
|
4796
|
+
onceCallbacks.forEach(callback => {
|
|
4797
|
+
try {
|
|
4798
|
+
callback(eventData);
|
|
4799
|
+
} catch (error) {
|
|
4800
|
+
console.error(`Error in once listener for "${event}":`, error);
|
|
4801
|
+
}
|
|
4802
|
+
});
|
|
4803
|
+
}
|
|
4804
|
+
|
|
4805
|
+
// Also emit to wildcard listeners (namespace:*)
|
|
4806
|
+
const namespace = event.split(':')[0];
|
|
4807
|
+
const wildcardEvent = `${namespace}:*`;
|
|
4808
|
+
if (this.listeners.has(wildcardEvent)) {
|
|
4809
|
+
this.listeners.get(wildcardEvent).forEach(callback => {
|
|
4810
|
+
try {
|
|
4811
|
+
callback(eventData);
|
|
4812
|
+
} catch (error) {
|
|
4813
|
+
console.error(`Error in wildcard listener for "${wildcardEvent}":`, error);
|
|
4814
|
+
}
|
|
4815
|
+
});
|
|
4816
|
+
}
|
|
4817
|
+
|
|
4818
|
+
// Emit unified 'change' event for data-modifying events
|
|
4819
|
+
if (this.isDataModifyingEvent(event) && event !== EditorEvents.CHANGE) {
|
|
4820
|
+
this.emit(EditorEvents.CHANGE, {
|
|
4821
|
+
originalEvent: event,
|
|
4822
|
+
...data
|
|
4823
|
+
});
|
|
4824
|
+
}
|
|
4825
|
+
}
|
|
4826
|
+
|
|
4827
|
+
/**
|
|
4828
|
+
* Check if an event modifies data (should trigger 'change' event)
|
|
4829
|
+
* @param {string} event - Event name
|
|
4830
|
+
* @returns {boolean}
|
|
4831
|
+
*/
|
|
4832
|
+
isDataModifyingEvent(event) {
|
|
4833
|
+
const dataEvents = [
|
|
4834
|
+
EditorEvents.SCENE_ADD,
|
|
4835
|
+
EditorEvents.SCENE_REMOVE,
|
|
4836
|
+
EditorEvents.SCENE_UPDATE,
|
|
4837
|
+
EditorEvents.SCENE_REORDER,
|
|
4838
|
+
EditorEvents.SCENE_CLEAR,
|
|
4839
|
+
EditorEvents.SCENE_IMAGE_CHANGE,
|
|
4840
|
+
EditorEvents.SCENE_STARTING_POSITION_SET,
|
|
4841
|
+
EditorEvents.SCENE_STARTING_POSITION_CLEAR,
|
|
4842
|
+
EditorEvents.HOTSPOT_ADD,
|
|
4843
|
+
EditorEvents.HOTSPOT_REMOVE,
|
|
4844
|
+
EditorEvents.HOTSPOT_UPDATE,
|
|
4845
|
+
EditorEvents.HOTSPOT_DUPLICATE,
|
|
4846
|
+
EditorEvents.HOTSPOT_POSITION_CHANGE,
|
|
4847
|
+
EditorEvents.CONFIG_UPDATE,
|
|
4848
|
+
EditorEvents.INITIAL_SCENE_CHANGE,
|
|
4849
|
+
EditorEvents.PROJECT_LOAD,
|
|
4850
|
+
EditorEvents.PROJECT_IMPORT,
|
|
4851
|
+
EditorEvents.PROJECT_NEW,
|
|
4852
|
+
EditorEvents.DATA_CHANGE
|
|
4853
|
+
];
|
|
4854
|
+
return dataEvents.includes(event);
|
|
4855
|
+
}
|
|
4856
|
+
|
|
4857
|
+
/**
|
|
4858
|
+
* Get the number of listeners for an event
|
|
4859
|
+
* @param {string} event - Event name
|
|
4860
|
+
* @returns {number}
|
|
4861
|
+
*/
|
|
4862
|
+
listenerCount(event) {
|
|
4863
|
+
let count = 0;
|
|
4864
|
+
if (this.listeners.has(event)) {
|
|
4865
|
+
count += this.listeners.get(event).size;
|
|
4866
|
+
}
|
|
4867
|
+
if (this.onceListeners.has(event)) {
|
|
4868
|
+
count += this.onceListeners.get(event).size;
|
|
4869
|
+
}
|
|
4870
|
+
return count;
|
|
4871
|
+
}
|
|
4872
|
+
|
|
4873
|
+
/**
|
|
4874
|
+
* Get all event names that have listeners
|
|
4875
|
+
* @returns {string[]}
|
|
4876
|
+
*/
|
|
4877
|
+
eventNames() {
|
|
4878
|
+
const names = new Set([...this.listeners.keys(), ...this.onceListeners.keys()]);
|
|
4879
|
+
return Array.from(names);
|
|
4880
|
+
}
|
|
4881
|
+
}
|
|
4882
|
+
|
|
4611
4883
|
// Export Manager - Handles JSON generation for SWT library
|
|
4612
4884
|
|
|
4613
4885
|
let ExportManager$1 = class ExportManager {
|
|
@@ -4628,6 +4900,51 @@
|
|
|
4628
4900
|
return buildTourConfig(config, scenes);
|
|
4629
4901
|
}
|
|
4630
4902
|
|
|
4903
|
+
/**
|
|
4904
|
+
* Load tour data from JSON (inverse of generateJSON)
|
|
4905
|
+
* This loads the entire tour configuration including initialScene and all scenes
|
|
4906
|
+
* @param {Object} tourData - Tour configuration object with initialScene and scenes
|
|
4907
|
+
* @param {string} tourData.initialScene - Initial scene ID
|
|
4908
|
+
* @param {Array} tourData.scenes - Array of scene objects
|
|
4909
|
+
* @returns {boolean} Success status
|
|
4910
|
+
*/
|
|
4911
|
+
loadJSON(tourData) {
|
|
4912
|
+
try {
|
|
4913
|
+
if (!tourData || typeof tourData !== 'object') {
|
|
4914
|
+
console.error('Invalid tour data: expected object');
|
|
4915
|
+
return false;
|
|
4916
|
+
}
|
|
4917
|
+
|
|
4918
|
+
// Load scenes into scene manager
|
|
4919
|
+
const scenes = tourData.scenes || [];
|
|
4920
|
+
this.editor.sceneManager.loadScenes(scenes);
|
|
4921
|
+
|
|
4922
|
+
// Set initial scene in config
|
|
4923
|
+
if (tourData.initialScene) {
|
|
4924
|
+
this.editor.config.initialSceneId = tourData.initialScene;
|
|
4925
|
+
} else if (scenes.length > 0) {
|
|
4926
|
+
this.editor.config.initialSceneId = scenes[0].id;
|
|
4927
|
+
}
|
|
4928
|
+
|
|
4929
|
+
// Mark as having unsaved changes
|
|
4930
|
+
this.editor.hasUnsavedChanges = true;
|
|
4931
|
+
|
|
4932
|
+
// Re-render the editor UI
|
|
4933
|
+
this.editor.render();
|
|
4934
|
+
|
|
4935
|
+
showToast('Tour loaded successfully', 'success');
|
|
4936
|
+
|
|
4937
|
+
// Emit event
|
|
4938
|
+
this.editor.emit(EditorEvents.PROJECT_LOAD, { tourData, source: 'loadJSON' });
|
|
4939
|
+
|
|
4940
|
+
return true;
|
|
4941
|
+
} catch (error) {
|
|
4942
|
+
console.error('Failed to load tour data:', error);
|
|
4943
|
+
showToast('Failed to load tour', 'error');
|
|
4944
|
+
return false;
|
|
4945
|
+
}
|
|
4946
|
+
}
|
|
4947
|
+
|
|
4631
4948
|
/**
|
|
4632
4949
|
* Generate JSON with icons baked in as SVG data URLs
|
|
4633
4950
|
* This ensures the exported HTML doesn't need the SenangStart icons library
|
|
@@ -4818,6 +5135,47 @@
|
|
|
4818
5135
|
this.hasUnsavedChanges = false;
|
|
4819
5136
|
this.lastRenderedSceneIndex = -1;
|
|
4820
5137
|
this.listenersSetup = false;
|
|
5138
|
+
|
|
5139
|
+
// Initialize event emitter
|
|
5140
|
+
this.events = new EventEmitter();
|
|
5141
|
+
}
|
|
5142
|
+
|
|
5143
|
+
/**
|
|
5144
|
+
* Subscribe to editor events
|
|
5145
|
+
* @param {string} event - Event name (use EditorEvents constants)
|
|
5146
|
+
* @param {Function} callback - Callback function
|
|
5147
|
+
* @returns {Function} Unsubscribe function
|
|
5148
|
+
*/
|
|
5149
|
+
on(event, callback) {
|
|
5150
|
+
return this.events.on(event, callback);
|
|
5151
|
+
}
|
|
5152
|
+
|
|
5153
|
+
/**
|
|
5154
|
+
* Subscribe to an event once
|
|
5155
|
+
* @param {string} event - Event name
|
|
5156
|
+
* @param {Function} callback - Callback function
|
|
5157
|
+
* @returns {Function} Unsubscribe function
|
|
5158
|
+
*/
|
|
5159
|
+
once(event, callback) {
|
|
5160
|
+
return this.events.once(event, callback);
|
|
5161
|
+
}
|
|
5162
|
+
|
|
5163
|
+
/**
|
|
5164
|
+
* Unsubscribe from an event
|
|
5165
|
+
* @param {string} event - Event name
|
|
5166
|
+
* @param {Function} callback - Callback to remove
|
|
5167
|
+
*/
|
|
5168
|
+
off(event, callback) {
|
|
5169
|
+
this.events.off(event, callback);
|
|
5170
|
+
}
|
|
5171
|
+
|
|
5172
|
+
/**
|
|
5173
|
+
* Emit an event
|
|
5174
|
+
* @param {string} event - Event name
|
|
5175
|
+
* @param {Object} data - Event data
|
|
5176
|
+
*/
|
|
5177
|
+
emit(event, data = {}) {
|
|
5178
|
+
this.events.emit(event, data);
|
|
4821
5179
|
}
|
|
4822
5180
|
|
|
4823
5181
|
/**
|
|
@@ -4837,15 +5195,15 @@
|
|
|
4837
5195
|
const previewInit = await this.previewController.init();
|
|
4838
5196
|
if (!previewInit) {
|
|
4839
5197
|
console.error('Failed to initialize preview controller');
|
|
4840
|
-
showToast
|
|
5198
|
+
showToast('Failed to initialize preview', 'error');
|
|
4841
5199
|
return false;
|
|
4842
5200
|
}
|
|
4843
5201
|
|
|
4844
5202
|
// Setup event listeners
|
|
4845
5203
|
this.setupEventListeners();
|
|
4846
5204
|
|
|
4847
|
-
// Populate icon grid
|
|
4848
|
-
this.uiController.populateIconGrid();
|
|
5205
|
+
// Populate icon grid (async to wait for custom element registration)
|
|
5206
|
+
await this.uiController.populateIconGrid();
|
|
4849
5207
|
|
|
4850
5208
|
// Load saved project if exists (but only if it has valid data)
|
|
4851
5209
|
if (this.storageManager.hasProject()) {
|
|
@@ -4876,7 +5234,10 @@
|
|
|
4876
5234
|
this.render();
|
|
4877
5235
|
}
|
|
4878
5236
|
|
|
4879
|
-
showToast
|
|
5237
|
+
showToast('Editor ready', 'success');
|
|
5238
|
+
|
|
5239
|
+
// Emit ready event
|
|
5240
|
+
this.emit(EditorEvents.READY, { config: this.options });
|
|
4880
5241
|
|
|
4881
5242
|
return true;
|
|
4882
5243
|
}
|
|
@@ -5078,11 +5439,14 @@
|
|
|
5078
5439
|
|
|
5079
5440
|
for (const file of files) {
|
|
5080
5441
|
if (!file.type.startsWith('image/')) {
|
|
5081
|
-
showToast
|
|
5442
|
+
showToast(`${file.name} is not an image`, 'error');
|
|
5082
5443
|
continue;
|
|
5083
5444
|
}
|
|
5084
5445
|
|
|
5085
|
-
await this.sceneManager.addScene(file);
|
|
5446
|
+
const scene = await this.sceneManager.addScene(file);
|
|
5447
|
+
if (scene) {
|
|
5448
|
+
this.emit(EditorEvents.SCENE_ADD, { scene, file });
|
|
5449
|
+
}
|
|
5086
5450
|
}
|
|
5087
5451
|
this.uiController.setLoading(false);
|
|
5088
5452
|
this.render();
|
|
@@ -5116,6 +5480,11 @@
|
|
|
5116
5480
|
this.lastRenderedSceneIndex = -1;
|
|
5117
5481
|
this.render();
|
|
5118
5482
|
this.markUnsavedChanges();
|
|
5483
|
+
this.emit(EditorEvents.HOTSPOT_ADD, {
|
|
5484
|
+
hotspot,
|
|
5485
|
+
position,
|
|
5486
|
+
sceneId: this.sceneManager.getCurrentScene()?.id
|
|
5487
|
+
});
|
|
5119
5488
|
} else {
|
|
5120
5489
|
console.error('Failed to add hotspot');
|
|
5121
5490
|
}
|
|
@@ -5128,13 +5497,13 @@
|
|
|
5128
5497
|
addHotspotAtCursor() {
|
|
5129
5498
|
const scene = this.sceneManager.getCurrentScene();
|
|
5130
5499
|
if (!scene) {
|
|
5131
|
-
showToast
|
|
5500
|
+
showToast('Please select a scene first', 'error');
|
|
5132
5501
|
return;
|
|
5133
5502
|
}
|
|
5134
5503
|
|
|
5135
5504
|
const position = this.previewController.getCursorIntersection();
|
|
5136
5505
|
if (!position) {
|
|
5137
|
-
showToast
|
|
5506
|
+
showToast('Could not get cursor position. Please ensure the preview is loaded.', 'error');
|
|
5138
5507
|
return;
|
|
5139
5508
|
}
|
|
5140
5509
|
|
|
@@ -5161,6 +5530,8 @@
|
|
|
5161
5530
|
this.uiController.updateHotspotProperties(null);
|
|
5162
5531
|
this.uiController.updateInitialSceneOptions();
|
|
5163
5532
|
this.uiController.updateTargetSceneOptions();
|
|
5533
|
+
|
|
5534
|
+
this.emit(EditorEvents.SCENE_SELECT, { scene, index });
|
|
5164
5535
|
}
|
|
5165
5536
|
}
|
|
5166
5537
|
|
|
@@ -5179,6 +5550,8 @@
|
|
|
5179
5550
|
if (hotspot) {
|
|
5180
5551
|
this.previewController.pointCameraToHotspot(hotspot);
|
|
5181
5552
|
}
|
|
5553
|
+
|
|
5554
|
+
this.emit(EditorEvents.HOTSPOT_SELECT, { hotspot, index });
|
|
5182
5555
|
}
|
|
5183
5556
|
}
|
|
5184
5557
|
|
|
@@ -5186,9 +5559,11 @@
|
|
|
5186
5559
|
* Remove scene
|
|
5187
5560
|
*/
|
|
5188
5561
|
removeScene(index) {
|
|
5562
|
+
const scene = this.sceneManager.getScene(index);
|
|
5189
5563
|
if (this.sceneManager.removeScene(index)) {
|
|
5190
5564
|
this.render();
|
|
5191
5565
|
this.markUnsavedChanges();
|
|
5566
|
+
this.emit(EditorEvents.SCENE_REMOVE, { scene, index });
|
|
5192
5567
|
}
|
|
5193
5568
|
}
|
|
5194
5569
|
|
|
@@ -5196,10 +5571,12 @@
|
|
|
5196
5571
|
* Remove hotspot
|
|
5197
5572
|
*/
|
|
5198
5573
|
removeHotspot(index) {
|
|
5574
|
+
const hotspot = this.hotspotEditor.getHotspot(index);
|
|
5199
5575
|
if (this.hotspotEditor.removeHotspot(index)) {
|
|
5200
5576
|
this.lastRenderedSceneIndex = -1;
|
|
5201
5577
|
this.render();
|
|
5202
5578
|
this.markUnsavedChanges();
|
|
5579
|
+
this.emit(EditorEvents.HOTSPOT_REMOVE, { hotspot, index });
|
|
5203
5580
|
}
|
|
5204
5581
|
}
|
|
5205
5582
|
|
|
@@ -5212,6 +5589,7 @@
|
|
|
5212
5589
|
this.lastRenderedSceneIndex = -1;
|
|
5213
5590
|
this.render();
|
|
5214
5591
|
this.markUnsavedChanges();
|
|
5592
|
+
this.emit(EditorEvents.HOTSPOT_DUPLICATE, { hotspot, originalIndex: index });
|
|
5215
5593
|
}
|
|
5216
5594
|
}
|
|
5217
5595
|
|
|
@@ -5222,6 +5600,7 @@
|
|
|
5222
5600
|
if (this.sceneManager.reorderScenes(fromIndex, toIndex)) {
|
|
5223
5601
|
this.render();
|
|
5224
5602
|
this.markUnsavedChanges();
|
|
5603
|
+
this.emit(EditorEvents.SCENE_REORDER, { fromIndex, toIndex });
|
|
5225
5604
|
}
|
|
5226
5605
|
}
|
|
5227
5606
|
|
|
@@ -5234,6 +5613,12 @@
|
|
|
5234
5613
|
await this.previewController.updateHotspotMarker(index);
|
|
5235
5614
|
this.uiController.renderHotspotList();
|
|
5236
5615
|
this.markUnsavedChanges();
|
|
5616
|
+
this.emit(EditorEvents.HOTSPOT_UPDATE, {
|
|
5617
|
+
hotspot: this.hotspotEditor.getHotspot(index),
|
|
5618
|
+
index,
|
|
5619
|
+
property,
|
|
5620
|
+
value
|
|
5621
|
+
});
|
|
5237
5622
|
}
|
|
5238
5623
|
}
|
|
5239
5624
|
|
|
@@ -5264,12 +5649,19 @@
|
|
|
5264
5649
|
pos.z *= scale;
|
|
5265
5650
|
|
|
5266
5651
|
document.getElementById(`hotspotPos${axis.toUpperCase()}`).value = pos[axis].toFixed(2);
|
|
5267
|
-
showToast
|
|
5652
|
+
showToast('Position clamped to 10-unit radius', 'info');
|
|
5268
5653
|
}
|
|
5269
5654
|
|
|
5270
5655
|
await this.previewController.updateHotspotMarker(index);
|
|
5271
5656
|
this.uiController.renderHotspotList();
|
|
5272
5657
|
this.markUnsavedChanges();
|
|
5658
|
+
this.emit(EditorEvents.HOTSPOT_POSITION_CHANGE, {
|
|
5659
|
+
hotspot,
|
|
5660
|
+
index,
|
|
5661
|
+
axis,
|
|
5662
|
+
value,
|
|
5663
|
+
position: hotspot.position
|
|
5664
|
+
});
|
|
5273
5665
|
}
|
|
5274
5666
|
}
|
|
5275
5667
|
|
|
@@ -5281,6 +5673,12 @@
|
|
|
5281
5673
|
if (this.sceneManager.updateScene(index, property, value)) {
|
|
5282
5674
|
this.uiController.renderSceneList();
|
|
5283
5675
|
this.markUnsavedChanges();
|
|
5676
|
+
this.emit(EditorEvents.SCENE_UPDATE, {
|
|
5677
|
+
scene: this.sceneManager.getScene(index),
|
|
5678
|
+
index,
|
|
5679
|
+
property,
|
|
5680
|
+
value
|
|
5681
|
+
});
|
|
5284
5682
|
}
|
|
5285
5683
|
}
|
|
5286
5684
|
|
|
@@ -5304,9 +5702,10 @@
|
|
|
5304
5702
|
if (scene) {
|
|
5305
5703
|
await this.previewController.loadScene(scene);
|
|
5306
5704
|
this.lastRenderedSceneIndex = index;
|
|
5307
|
-
showToast
|
|
5705
|
+
showToast('Scene image updated', 'success');
|
|
5308
5706
|
}
|
|
5309
5707
|
this.markUnsavedChanges();
|
|
5708
|
+
this.emit(EditorEvents.SCENE_IMAGE_CHANGE, { scene, index, imageUrl });
|
|
5310
5709
|
}
|
|
5311
5710
|
}
|
|
5312
5711
|
|
|
@@ -5316,13 +5715,13 @@
|
|
|
5316
5715
|
setSceneStartingPosition() {
|
|
5317
5716
|
const scene = this.sceneManager.getCurrentScene();
|
|
5318
5717
|
if (!scene) {
|
|
5319
|
-
showToast
|
|
5718
|
+
showToast('No scene selected', 'error');
|
|
5320
5719
|
return;
|
|
5321
5720
|
}
|
|
5322
5721
|
|
|
5323
5722
|
const rotation = this.previewController.getCameraRotation();
|
|
5324
5723
|
if (!rotation) {
|
|
5325
|
-
showToast
|
|
5724
|
+
showToast('Could not get camera rotation', 'error');
|
|
5326
5725
|
return;
|
|
5327
5726
|
}
|
|
5328
5727
|
|
|
@@ -5333,7 +5732,8 @@
|
|
|
5333
5732
|
|
|
5334
5733
|
this.uiController.updateSceneProperties(scene);
|
|
5335
5734
|
this.markUnsavedChanges();
|
|
5336
|
-
showToast
|
|
5735
|
+
showToast('Starting position set', 'success');
|
|
5736
|
+
this.emit(EditorEvents.SCENE_STARTING_POSITION_SET, { scene, startingPosition: scene.startingPosition });
|
|
5337
5737
|
}
|
|
5338
5738
|
|
|
5339
5739
|
/**
|
|
@@ -5342,7 +5742,7 @@
|
|
|
5342
5742
|
clearSceneStartingPosition() {
|
|
5343
5743
|
const scene = this.sceneManager.getCurrentScene();
|
|
5344
5744
|
if (!scene) {
|
|
5345
|
-
showToast
|
|
5745
|
+
showToast('No scene selected', 'error');
|
|
5346
5746
|
return;
|
|
5347
5747
|
}
|
|
5348
5748
|
|
|
@@ -5350,7 +5750,8 @@
|
|
|
5350
5750
|
|
|
5351
5751
|
this.uiController.updateSceneProperties(scene);
|
|
5352
5752
|
this.markUnsavedChanges();
|
|
5353
|
-
showToast
|
|
5753
|
+
showToast('Starting position cleared', 'success');
|
|
5754
|
+
this.emit(EditorEvents.SCENE_STARTING_POSITION_CLEAR, { scene });
|
|
5354
5755
|
}
|
|
5355
5756
|
|
|
5356
5757
|
/**
|
|
@@ -5359,6 +5760,7 @@
|
|
|
5359
5760
|
render() {
|
|
5360
5761
|
this.uiController.renderSceneList();
|
|
5361
5762
|
this.uiController.renderHotspotList();
|
|
5763
|
+
this.uiController.populateIconGrid(); // Re-render icon grid to ensure icons display
|
|
5362
5764
|
|
|
5363
5765
|
const currentScene = this.sceneManager.getCurrentScene();
|
|
5364
5766
|
const currentHotspot = this.hotspotEditor.getCurrentHotspot();
|
|
@@ -5391,6 +5793,8 @@
|
|
|
5391
5793
|
}
|
|
5392
5794
|
this.lastRenderedSceneIndex = -1;
|
|
5393
5795
|
}
|
|
5796
|
+
|
|
5797
|
+
this.emit(EditorEvents.UI_RENDER);
|
|
5394
5798
|
}
|
|
5395
5799
|
|
|
5396
5800
|
/**
|
|
@@ -5404,7 +5808,8 @@
|
|
|
5404
5808
|
|
|
5405
5809
|
if (this.storageManager.saveProject(projectData)) {
|
|
5406
5810
|
this.hasUnsavedChanges = false;
|
|
5407
|
-
showToast
|
|
5811
|
+
showToast('Project saved', 'success');
|
|
5812
|
+
this.emit(EditorEvents.PROJECT_SAVE, { projectData });
|
|
5408
5813
|
return true;
|
|
5409
5814
|
}
|
|
5410
5815
|
return false;
|
|
@@ -5420,7 +5825,8 @@
|
|
|
5420
5825
|
this.sceneManager.loadScenes(projectData.scenes || []);
|
|
5421
5826
|
this.hasUnsavedChanges = false;
|
|
5422
5827
|
this.render();
|
|
5423
|
-
showToast
|
|
5828
|
+
showToast('Project loaded', 'success');
|
|
5829
|
+
this.emit(EditorEvents.PROJECT_LOAD, { projectData });
|
|
5424
5830
|
return true;
|
|
5425
5831
|
}
|
|
5426
5832
|
return false;
|
|
@@ -5446,7 +5852,8 @@
|
|
|
5446
5852
|
this.hasUnsavedChanges = false;
|
|
5447
5853
|
this.render();
|
|
5448
5854
|
|
|
5449
|
-
showToast
|
|
5855
|
+
showToast('New project created', 'success');
|
|
5856
|
+
this.emit(EditorEvents.PROJECT_NEW, { config: this.config });
|
|
5450
5857
|
return true;
|
|
5451
5858
|
}
|
|
5452
5859
|
|
|
@@ -5476,7 +5883,8 @@
|
|
|
5476
5883
|
this.render();
|
|
5477
5884
|
this.uiController.setLoading(false);
|
|
5478
5885
|
|
|
5479
|
-
showToast
|
|
5886
|
+
showToast('Project imported successfully', 'success');
|
|
5887
|
+
this.emit(EditorEvents.PROJECT_IMPORT, { projectData, file });
|
|
5480
5888
|
} catch (error) {
|
|
5481
5889
|
this.uiController.setLoading(false);
|
|
5482
5890
|
console.error('Import failed:', error);
|