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,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
+ }