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.
- package/LICENSE.md +21 -0
- package/README.md +653 -0
- package/dist/swt-editor.css +869 -0
- package/dist/swt-editor.css.map +1 -0
- package/dist/swt-editor.js +2853 -0
- package/dist/swt-editor.js.map +1 -0
- package/dist/swt-editor.min.css +1 -0
- package/dist/swt-editor.min.js +1 -0
- package/dist/swt.js +873 -0
- package/dist/swt.js.map +1 -0
- package/dist/swt.min.js +1 -0
- package/package.json +39 -0
- package/src/AssetManager.js +153 -0
- package/src/HotspotManager.js +193 -0
- package/src/SceneManager.js +162 -0
- package/src/components/hotspot-listener.js +114 -0
- package/src/editor/css/main.css +1002 -0
- package/src/editor/editor-entry.css +4 -0
- package/src/editor/editor-entry.js +30 -0
- package/src/editor/js/editor.js +629 -0
- package/src/editor/js/export-manager.js +286 -0
- package/src/editor/js/hotspot-editor.js +237 -0
- package/src/editor/js/preview-controller.js +556 -0
- package/src/editor/js/scene-manager.js +202 -0
- package/src/editor/js/storage-manager.js +193 -0
- package/src/editor/js/ui-controller.js +387 -0
- package/src/editor/js/ui-init.js +164 -0
- package/src/editor/js/utils.js +217 -0
- package/src/index.js +245 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// Utility Functions
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate a unique ID
|
|
5
|
+
*/
|
|
6
|
+
function generateId(prefix = 'id') {
|
|
7
|
+
return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Sanitize string for use as ID
|
|
12
|
+
*/
|
|
13
|
+
function sanitizeId(str) {
|
|
14
|
+
return str.toLowerCase()
|
|
15
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
16
|
+
.replace(/^_+|_+$/g, '');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generate thumbnail from image file
|
|
21
|
+
*/
|
|
22
|
+
async function generateThumbnail(file, maxWidth = 200, maxHeight = 150) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const reader = new FileReader();
|
|
25
|
+
|
|
26
|
+
reader.onload = (e) => {
|
|
27
|
+
const img = new Image();
|
|
28
|
+
|
|
29
|
+
img.onload = () => {
|
|
30
|
+
const canvas = document.createElement('canvas');
|
|
31
|
+
const ctx = canvas.getContext('2d');
|
|
32
|
+
|
|
33
|
+
let width = img.width;
|
|
34
|
+
let height = img.height;
|
|
35
|
+
|
|
36
|
+
if (width > height) {
|
|
37
|
+
if (width > maxWidth) {
|
|
38
|
+
height *= maxWidth / width;
|
|
39
|
+
width = maxWidth;
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
if (height > maxHeight) {
|
|
43
|
+
width *= maxHeight / height;
|
|
44
|
+
height = maxHeight;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
canvas.width = width;
|
|
49
|
+
canvas.height = height;
|
|
50
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
51
|
+
|
|
52
|
+
resolve(canvas.toDataURL('image/jpeg', 0.7));
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
img.onerror = reject;
|
|
56
|
+
img.src = e.target.result;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
reader.onerror = reject;
|
|
60
|
+
reader.readAsDataURL(file);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Load image file as data URL
|
|
66
|
+
*/
|
|
67
|
+
async function loadImageAsDataUrl(file) {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
const reader = new FileReader();
|
|
70
|
+
reader.onload = (e) => resolve(e.target.result);
|
|
71
|
+
reader.onerror = reject;
|
|
72
|
+
reader.readAsDataURL(file);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Show toast notification
|
|
78
|
+
*/
|
|
79
|
+
function showToast(message, type = 'info', duration = 3000) {
|
|
80
|
+
const toast = document.getElementById('toast');
|
|
81
|
+
toast.textContent = message;
|
|
82
|
+
toast.className = `toast ${type}`;
|
|
83
|
+
toast.classList.add('show');
|
|
84
|
+
|
|
85
|
+
setTimeout(() => {
|
|
86
|
+
toast.classList.remove('show');
|
|
87
|
+
}, duration);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Show modal
|
|
92
|
+
*/
|
|
93
|
+
function showModal(modalId) {
|
|
94
|
+
const modal = document.getElementById(modalId);
|
|
95
|
+
if (modal) {
|
|
96
|
+
modal.classList.add('show');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Hide modal
|
|
102
|
+
*/
|
|
103
|
+
function hideModal(modalId) {
|
|
104
|
+
const modal = document.getElementById(modalId);
|
|
105
|
+
if (modal) {
|
|
106
|
+
modal.classList.remove('show');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Format file size
|
|
112
|
+
*/
|
|
113
|
+
function formatFileSize(bytes) {
|
|
114
|
+
if (bytes === 0) return '0 Bytes';
|
|
115
|
+
const k = 1024;
|
|
116
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
117
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
118
|
+
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Download text as file
|
|
123
|
+
*/
|
|
124
|
+
function downloadTextAsFile(text, filename) {
|
|
125
|
+
const blob = new Blob([text], { type: 'text/plain' });
|
|
126
|
+
const url = URL.createObjectURL(blob);
|
|
127
|
+
const a = document.createElement('a');
|
|
128
|
+
a.href = url;
|
|
129
|
+
a.download = filename;
|
|
130
|
+
document.body.appendChild(a);
|
|
131
|
+
a.click();
|
|
132
|
+
document.body.removeChild(a);
|
|
133
|
+
URL.revokeObjectURL(url);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Copy text to clipboard
|
|
138
|
+
*/
|
|
139
|
+
async function copyToClipboard(text) {
|
|
140
|
+
try {
|
|
141
|
+
await navigator.clipboard.writeText(text);
|
|
142
|
+
return true;
|
|
143
|
+
} catch (err) {
|
|
144
|
+
// Fallback for older browsers
|
|
145
|
+
const textarea = document.createElement('textarea');
|
|
146
|
+
textarea.value = text;
|
|
147
|
+
textarea.style.position = 'fixed';
|
|
148
|
+
textarea.style.opacity = '0';
|
|
149
|
+
document.body.appendChild(textarea);
|
|
150
|
+
textarea.select();
|
|
151
|
+
const success = document.execCommand('copy');
|
|
152
|
+
document.body.removeChild(textarea);
|
|
153
|
+
return success;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Debounce function
|
|
159
|
+
*/
|
|
160
|
+
function debounce(func, wait) {
|
|
161
|
+
let timeout;
|
|
162
|
+
return function executedFunction(...args) {
|
|
163
|
+
const later = () => {
|
|
164
|
+
clearTimeout(timeout);
|
|
165
|
+
func(...args);
|
|
166
|
+
};
|
|
167
|
+
clearTimeout(timeout);
|
|
168
|
+
timeout = setTimeout(later, wait);
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Convert position object to string
|
|
174
|
+
*/
|
|
175
|
+
function positionToString(pos) {
|
|
176
|
+
return `${pos.x.toFixed(1)} ${pos.y.toFixed(1)} ${pos.z.toFixed(1)}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Parse position string to object
|
|
181
|
+
*/
|
|
182
|
+
function parsePosition(str) {
|
|
183
|
+
const parts = str.split(' ').map(Number);
|
|
184
|
+
return { x: parts[0] || 0, y: parts[1] || 0, z: parts[2] || 0 };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Validate email
|
|
189
|
+
*/
|
|
190
|
+
function isValidEmail(email) {
|
|
191
|
+
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
192
|
+
return re.test(email);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Deep clone object
|
|
197
|
+
*/
|
|
198
|
+
function deepClone(obj) {
|
|
199
|
+
return JSON.parse(JSON.stringify(obj));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export {
|
|
203
|
+
generateId,
|
|
204
|
+
sanitizeId,
|
|
205
|
+
generateThumbnail,
|
|
206
|
+
loadImageAsDataUrl,
|
|
207
|
+
showToast,
|
|
208
|
+
showModal,
|
|
209
|
+
hideModal,
|
|
210
|
+
formatFileSize,
|
|
211
|
+
downloadTextAsFile,
|
|
212
|
+
debounce,
|
|
213
|
+
positionToString,
|
|
214
|
+
parsePosition,
|
|
215
|
+
isValidEmail,
|
|
216
|
+
deepClone
|
|
217
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangWebs Tour (SWT) - Main Library Entry Point
|
|
3
|
+
* Version 1.0.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import './components/hotspot-listener.js';
|
|
7
|
+
import { AssetManager } from './AssetManager.js';
|
|
8
|
+
import { SceneManager } from './SceneManager.js';
|
|
9
|
+
import { HotspotManager } from './HotspotManager.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Main Tour class - The public API for the SWT library
|
|
13
|
+
*/
|
|
14
|
+
class Tour {
|
|
15
|
+
constructor(aframeSceneEl, tourConfig) {
|
|
16
|
+
if (!aframeSceneEl) {
|
|
17
|
+
throw new Error('SWT.Tour requires an A-Frame scene element');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!tourConfig || !tourConfig.scenes || !tourConfig.initialScene) {
|
|
21
|
+
throw new Error('SWT.Tour requires a valid tour configuration with scenes and initialScene');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
this.sceneEl = aframeSceneEl;
|
|
25
|
+
this.config = tourConfig;
|
|
26
|
+
this.isStarted = false;
|
|
27
|
+
|
|
28
|
+
// Initialize managers
|
|
29
|
+
this.assetManager = new AssetManager(this.sceneEl);
|
|
30
|
+
this.sceneManager = new SceneManager(this.sceneEl, this.assetManager);
|
|
31
|
+
|
|
32
|
+
const defaultHotspotSettings = this.config.settings?.defaultHotspot || {};
|
|
33
|
+
this.hotspotManager = new HotspotManager(
|
|
34
|
+
this.sceneEl,
|
|
35
|
+
this.assetManager,
|
|
36
|
+
defaultHotspotSettings
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// Event listeners
|
|
40
|
+
this.boundHandleHotspotClick = this.handleHotspotClick.bind(this);
|
|
41
|
+
this.sceneEl.addEventListener('swt-hotspot-clicked', this.boundHandleHotspotClick);
|
|
42
|
+
|
|
43
|
+
// Ensure cursor exists for interaction
|
|
44
|
+
this.ensureCursor();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Ensure the scene has a cursor for interaction
|
|
49
|
+
*/
|
|
50
|
+
ensureCursor() {
|
|
51
|
+
const camera = this.sceneEl.querySelector('[camera]');
|
|
52
|
+
if (camera) {
|
|
53
|
+
let cursor = camera.querySelector('[cursor]');
|
|
54
|
+
if (!cursor) {
|
|
55
|
+
cursor = document.createElement('a-cursor');
|
|
56
|
+
cursor.setAttribute('fuse', 'true');
|
|
57
|
+
cursor.setAttribute('fuse-timeout', '1500');
|
|
58
|
+
camera.appendChild(cursor);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Start the tour - Load the initial scene
|
|
65
|
+
* @returns {Promise}
|
|
66
|
+
*/
|
|
67
|
+
async start() {
|
|
68
|
+
if (this.isStarted) {
|
|
69
|
+
console.warn('Tour has already been started');
|
|
70
|
+
return Promise.resolve();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const initialSceneId = this.config.initialScene;
|
|
74
|
+
const initialSceneData = this.config.scenes[initialSceneId];
|
|
75
|
+
|
|
76
|
+
if (!initialSceneData) {
|
|
77
|
+
throw new Error(`Initial scene "${initialSceneId}" not found in tour configuration`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
// Emit scene-loading event
|
|
82
|
+
this.emit('scene-loading', { sceneId: initialSceneId });
|
|
83
|
+
|
|
84
|
+
// Load the scene
|
|
85
|
+
await this.sceneManager.loadScene(initialSceneId, initialSceneData);
|
|
86
|
+
|
|
87
|
+
// Create hotspots
|
|
88
|
+
await this.hotspotManager.createHotspots(initialSceneData.hotspots || []);
|
|
89
|
+
|
|
90
|
+
this.isStarted = true;
|
|
91
|
+
|
|
92
|
+
// Emit events
|
|
93
|
+
this.emit('scene-loaded', { sceneId: initialSceneId });
|
|
94
|
+
this.emit('tour-started', { sceneId: initialSceneId });
|
|
95
|
+
|
|
96
|
+
return Promise.resolve();
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error('Failed to start tour:', error);
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Navigate to a specific scene
|
|
105
|
+
* @param {string} sceneId - The ID of the scene to navigate to
|
|
106
|
+
* @returns {Promise}
|
|
107
|
+
*/
|
|
108
|
+
async navigateTo(sceneId) {
|
|
109
|
+
const sceneData = this.config.scenes[sceneId];
|
|
110
|
+
|
|
111
|
+
if (!sceneData) {
|
|
112
|
+
throw new Error(`Scene "${sceneId}" not found in tour configuration`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (this.sceneManager.getCurrentSceneId() === sceneId) {
|
|
116
|
+
console.warn(`Already at scene "${sceneId}"`);
|
|
117
|
+
return Promise.resolve();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
// Emit scene-loading event
|
|
122
|
+
this.emit('scene-loading', { sceneId: sceneId });
|
|
123
|
+
|
|
124
|
+
// Remove old hotspots
|
|
125
|
+
this.hotspotManager.removeAllHotspots();
|
|
126
|
+
|
|
127
|
+
// Transition to new scene
|
|
128
|
+
await this.sceneManager.transitionTo(sceneId, sceneData);
|
|
129
|
+
|
|
130
|
+
// Create new hotspots
|
|
131
|
+
await this.hotspotManager.createHotspots(sceneData.hotspots || []);
|
|
132
|
+
|
|
133
|
+
// Emit scene-loaded event
|
|
134
|
+
this.emit('scene-loaded', { sceneId: sceneId });
|
|
135
|
+
|
|
136
|
+
return Promise.resolve();
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error(`Failed to navigate to scene "${sceneId}":`, error);
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Handle hotspot click events
|
|
145
|
+
* @param {CustomEvent} evt - The hotspot click event
|
|
146
|
+
*/
|
|
147
|
+
handleHotspotClick(evt) {
|
|
148
|
+
const { hotspotData } = evt.detail;
|
|
149
|
+
const currentSceneId = this.sceneManager.getCurrentSceneId();
|
|
150
|
+
|
|
151
|
+
// Emit hotspot-activated event
|
|
152
|
+
this.emit('hotspot-activated', {
|
|
153
|
+
hotspotData: hotspotData,
|
|
154
|
+
sceneId: currentSceneId
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Handle the action
|
|
158
|
+
if (hotspotData.action) {
|
|
159
|
+
this.handleAction(hotspotData.action);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Handle a hotspot action
|
|
165
|
+
* @param {Object} action - The action object
|
|
166
|
+
*/
|
|
167
|
+
handleAction(action) {
|
|
168
|
+
switch (action.type) {
|
|
169
|
+
case 'navigateTo':
|
|
170
|
+
if (action.target) {
|
|
171
|
+
this.navigateTo(action.target).catch(err => {
|
|
172
|
+
console.error('Navigation failed:', err);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
|
|
177
|
+
default:
|
|
178
|
+
console.warn(`Unknown action type: ${action.type}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get the current scene ID
|
|
184
|
+
* @returns {string}
|
|
185
|
+
*/
|
|
186
|
+
getCurrentSceneId() {
|
|
187
|
+
return this.sceneManager.getCurrentSceneId();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Emit a custom event
|
|
192
|
+
* @param {string} eventName - The event name
|
|
193
|
+
* @param {Object} detail - Event detail object
|
|
194
|
+
*/
|
|
195
|
+
emit(eventName, detail = {}) {
|
|
196
|
+
const event = new CustomEvent(eventName, {
|
|
197
|
+
detail: detail,
|
|
198
|
+
bubbles: true,
|
|
199
|
+
cancelable: true
|
|
200
|
+
});
|
|
201
|
+
this.sceneEl.dispatchEvent(event);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Add an event listener
|
|
206
|
+
* @param {string} eventName - The event name
|
|
207
|
+
* @param {Function} handler - The event handler
|
|
208
|
+
*/
|
|
209
|
+
addEventListener(eventName, handler) {
|
|
210
|
+
this.sceneEl.addEventListener(eventName, handler);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Remove an event listener
|
|
215
|
+
* @param {string} eventName - The event name
|
|
216
|
+
* @param {Function} handler - The event handler
|
|
217
|
+
*/
|
|
218
|
+
removeEventListener(eventName, handler) {
|
|
219
|
+
this.sceneEl.removeEventListener(eventName, handler);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Destroy the tour and clean up resources
|
|
224
|
+
*/
|
|
225
|
+
destroy() {
|
|
226
|
+
// Remove event listeners
|
|
227
|
+
this.sceneEl.removeEventListener('swt-hotspot-clicked', this.boundHandleHotspotClick);
|
|
228
|
+
|
|
229
|
+
// Clean up managers
|
|
230
|
+
this.hotspotManager.destroy();
|
|
231
|
+
this.sceneManager.destroy();
|
|
232
|
+
this.assetManager.destroy();
|
|
233
|
+
|
|
234
|
+
this.isStarted = false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Export the Tour class as the main API
|
|
239
|
+
export { Tour };
|
|
240
|
+
|
|
241
|
+
// Also attach to window for UMD usage
|
|
242
|
+
if (typeof window !== 'undefined') {
|
|
243
|
+
window.SWT = window.SWT || {};
|
|
244
|
+
window.SWT.Tour = Tour;
|
|
245
|
+
}
|