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,286 @@
1
+ // Export Manager - Handles JSON generation for SWT library
2
+ import { downloadTextAsFile, showModal } from './utils.js';
3
+
4
+ class ExportManager {
5
+ constructor(editor) {
6
+ this.editor = editor;
7
+ }
8
+
9
+ /**
10
+ * Generate JSON compatible with SWT library
11
+ */
12
+ generateJSON() {
13
+ const scenes = this.editor.sceneManager.getAllScenes();
14
+ const config = this.editor.config;
15
+ // Build scenes array
16
+ const scenesData = scenes.map(scene => ({
17
+ id: scene.id,
18
+ name: scene.name,
19
+ imageUrl: scene.imageUrl,
20
+ hotspots: scene.hotspots.map(hotspot => ({
21
+ id: hotspot.id,
22
+ type: hotspot.type || 'navigation',
23
+ position: hotspot.position,
24
+ targetSceneId: hotspot.targetSceneId || '',
25
+ title: hotspot.title || '',
26
+ description: hotspot.description || '',
27
+ color: hotspot.color || '#00ff00',
28
+ icon: hotspot.icon || ''
29
+ }))
30
+ }));
31
+ // Determine initial scene
32
+ let initialSceneId = config.initialSceneId;
33
+ if (!initialSceneId && scenes.length > 0) {
34
+ initialSceneId = scenes[0].id;
35
+ }
36
+
37
+ // Build final JSON
38
+ const jsonData = {
39
+ title: config.title || 'Virtual Tour',
40
+ description: config.description || '',
41
+ initialSceneId: initialSceneId,
42
+ scenes: scenesData,
43
+ settings: {
44
+ autoRotate: config.autoRotate || false,
45
+ showCompass: config.showCompass || false
46
+ }
47
+ };
48
+
49
+ return jsonData;
50
+ }
51
+
52
+ /**
53
+ * Export as JSON file
54
+ */
55
+ exportJSON() {
56
+ try {
57
+ const jsonData = this.generateJSON();
58
+ const json = JSON.stringify(jsonData, null, 2);
59
+
60
+ const filename = sanitizeId(jsonData.title || 'tour') + '.json';
61
+ downloadTextAsFile(json, filename);
62
+
63
+ showToast('Tour exported successfully', 'success');
64
+ return true;
65
+ } catch (error) {
66
+ console.error('Export failed:', error);
67
+ showToast('Export failed', 'error');
68
+ return false;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Copy JSON to clipboard
74
+ */
75
+ async copyJSON() {
76
+ try {
77
+ const jsonData = this.generateJSON();
78
+ const json = JSON.stringify(jsonData, null, 2);
79
+
80
+ const success = await copyToClipboard(json);
81
+ if (success) {
82
+ showToast('JSON copied to clipboard', 'success');
83
+ } else {
84
+ showToast('Failed to copy to clipboard', 'error');
85
+ }
86
+ return success;
87
+ } catch (error) {
88
+ console.error('Copy failed:', error);
89
+ showToast('Copy failed', 'error');
90
+ return false;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Generate HTML viewer code
96
+ */
97
+ generateViewerHTML() {
98
+ const jsonData = this.generateJSON();
99
+
100
+ return `<!DOCTYPE html>
101
+ <html lang="en">
102
+ <head>
103
+ <meta charset="UTF-8">
104
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
105
+ <title>${jsonData.title}</title>
106
+ <script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
107
+ <script src="dist/swt.min.js"></script>
108
+ <style>
109
+ body {
110
+ margin: 0;
111
+ overflow: hidden;
112
+ font-family: Arial, sans-serif;
113
+ }
114
+
115
+ #loading {
116
+ position: fixed;
117
+ top: 0;
118
+ left: 0;
119
+ width: 100%;
120
+ height: 100%;
121
+ background: #000;
122
+ display: flex;
123
+ align-items: center;
124
+ justify-content: center;
125
+ color: #fff;
126
+ z-index: 1000;
127
+ }
128
+
129
+ #loading.hidden {
130
+ display: none;
131
+ }
132
+
133
+ .spinner {
134
+ border: 4px solid rgba(255,255,255,0.3);
135
+ border-top: 4px solid #fff;
136
+ border-radius: 50%;
137
+ width: 40px;
138
+ height: 40px;
139
+ animation: spin 1s linear infinite;
140
+ margin-right: 15px;
141
+ }
142
+
143
+ @keyframes spin {
144
+ 0% { transform: rotate(0deg); }
145
+ 100% { transform: rotate(360deg); }
146
+ }
147
+
148
+ #ui {
149
+ position: fixed;
150
+ bottom: 20px;
151
+ left: 50%;
152
+ transform: translateX(-50%);
153
+ z-index: 100;
154
+ display: flex;
155
+ gap: 10px;
156
+ }
157
+
158
+ .btn {
159
+ background: rgba(0,0,0,0.7);
160
+ color: #fff;
161
+ border: none;
162
+ padding: 10px 20px;
163
+ border-radius: 5px;
164
+ cursor: pointer;
165
+ font-size: 14px;
166
+ }
167
+
168
+ .btn:hover {
169
+ background: rgba(0,0,0,0.9);
170
+ }
171
+ </style>
172
+ </head>
173
+ <body>
174
+ <div id="loading">
175
+ <div class="spinner"></div>
176
+ <span>Loading Tour...</span>
177
+ </div>
178
+
179
+ <div id="tour-container"></div>
180
+
181
+ <div id="ui" style="display: none;">
182
+ <button class="btn" id="resetBtn">Reset View</button>
183
+ <span class="btn" id="sceneInfo"></span>
184
+ </div>
185
+
186
+ <script>
187
+ // Tour configuration
188
+ const tourConfig = ${JSON.stringify(jsonData, null, 8)};
189
+
190
+ // Initialize tour
191
+ let tour;
192
+
193
+ document.addEventListener('DOMContentLoaded', async () => {
194
+ try {
195
+ // Create tour instance
196
+ tour = new SenangWebsTour('tour-container', tourConfig);
197
+
198
+ // Listen to events
199
+ tour.on('sceneChanged', (sceneId) => {
200
+ updateSceneInfo();
201
+ });
202
+
203
+ tour.on('ready', () => {
204
+ document.getElementById('loading').classList.add('hidden');
205
+ document.getElementById('ui').style.display = 'flex';
206
+ updateSceneInfo();
207
+ });
208
+
209
+ tour.on('error', (error) => {
210
+ console.error('Tour error:', error);
211
+ alert('Failed to load tour: ' + error.message);
212
+ });
213
+
214
+ // Start tour
215
+ await tour.start();
216
+
217
+ // Setup UI
218
+ document.getElementById('resetBtn').addEventListener('click', () => {
219
+ const camera = document.querySelector('[camera]');
220
+ if (camera) {
221
+ camera.setAttribute('rotation', '0 0 0');
222
+ }
223
+ });
224
+
225
+ } catch (error) {
226
+ console.error('Failed to initialize tour:', error);
227
+ alert('Failed to initialize tour: ' + error.message);
228
+ }
229
+ });
230
+
231
+ function updateSceneInfo() {
232
+ const sceneId = tour.getCurrentSceneId();
233
+ const scene = tourConfig.scenes.find(s => s.id === sceneId);
234
+ if (scene) {
235
+ document.getElementById('sceneInfo').textContent = scene.name;
236
+ }
237
+ }
238
+ </script>
239
+ </body>
240
+ </html>`;
241
+ }
242
+
243
+ /**
244
+ * Export as standalone HTML viewer
245
+ */
246
+ exportViewerHTML() {
247
+ try {
248
+ const html = this.generateViewerHTML();
249
+ const jsonData = this.generateJSON();
250
+ const filename = sanitizeId(jsonData.title || 'tour') + '-viewer.html';
251
+
252
+ downloadTextAsFile(html, filename);
253
+
254
+ showToast('Viewer HTML exported successfully', 'success');
255
+ return true;
256
+ } catch (error) {
257
+ console.error('Export viewer failed:', error);
258
+ showToast('Export viewer failed', 'error');
259
+ return false;
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Show export preview in modal
265
+ */
266
+ showExportPreview() {
267
+ try {
268
+ const jsonData = this.generateJSON();
269
+ const json = JSON.stringify(jsonData, null, 2);
270
+
271
+ const preview = document.getElementById('exportPreview');
272
+ if (preview) {
273
+ preview.textContent = json;
274
+ }
275
+
276
+ showModal('exportModal');
277
+ return true;
278
+ } catch (error) {
279
+ console.error('Failed to show export preview:', error);
280
+ showToast('Failed to generate preview', 'error');
281
+ return false;
282
+ }
283
+ }
284
+ }
285
+
286
+ export default ExportManager;
@@ -0,0 +1,237 @@
1
+ // Hotspot Editor - Handles hotspot placement and editing
2
+ import { generateId, showToast } from './utils.js';
3
+
4
+ class HotspotEditor {
5
+ constructor(editor) {
6
+ this.editor = editor;
7
+ this.currentHotspotIndex = -1;
8
+ this.placementMode = false;
9
+ }
10
+
11
+ /**
12
+ * Enable hotspot placement mode
13
+ */
14
+ enablePlacementMode() {
15
+ const scene = this.editor.sceneManager.getCurrentScene();
16
+ if (!scene) {
17
+ showToast('Please select a scene first', 'error');
18
+ return false;
19
+ }
20
+
21
+ this.placementMode = true;
22
+
23
+ // Visual feedback
24
+ document.body.style.cursor = 'crosshair';
25
+ const preview = document.getElementById('preview');
26
+ if (preview) {
27
+ preview.style.border = '3px solid #4CC3D9';
28
+ preview.style.boxShadow = '0 0 20px rgba(76, 195, 217, 0.5)';
29
+ }
30
+
31
+ // Update button state
32
+ const btn = document.getElementById('addHotspotBtn');
33
+ if (btn) {
34
+ btn.textContent = 'Click on Preview...';
35
+ btn.classList.add('btn-active');
36
+ }
37
+
38
+ showToast('Click on the 360° preview to place hotspot', 'info', 5000);
39
+ return true;
40
+ }
41
+
42
+ /**
43
+ * Disable hotspot placement mode
44
+ */
45
+ disablePlacementMode() {
46
+ this.placementMode = false;
47
+ document.body.style.cursor = 'default';
48
+
49
+ // Remove visual feedback
50
+ const preview = document.getElementById('preview');
51
+ if (preview) {
52
+ preview.style.border = '';
53
+ preview.style.boxShadow = '';
54
+ }
55
+
56
+ // Reset button state
57
+ const btn = document.getElementById('addHotspotBtn');
58
+ if (btn) {
59
+ btn.textContent = '+ Add Hotspot';
60
+ btn.classList.remove('btn-active');
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Add hotspot at position
66
+ */
67
+ addHotspot(position, targetSceneId = '') {
68
+ const scene = this.editor.sceneManager.getCurrentScene();
69
+ if (!scene) {
70
+ showToast('No scene selected', 'error');
71
+ return null;
72
+ }
73
+
74
+ const hotspot = {
75
+ id: generateId('hotspot'),
76
+ type: 'navigation',
77
+ position: position,
78
+ targetSceneId: targetSceneId,
79
+ title: 'New Hotspot',
80
+ description: '',
81
+ color: '#00ff00',
82
+ icon: ''
83
+ };
84
+
85
+ scene.hotspots.push(hotspot);
86
+ this.currentHotspotIndex = scene.hotspots.length - 1;
87
+
88
+ this.disablePlacementMode();
89
+ showToast('Hotspot added', 'success');
90
+
91
+ return hotspot;
92
+ }
93
+
94
+ /**
95
+ * Remove hotspot
96
+ */
97
+ removeHotspot(index) {
98
+ const scene = this.editor.sceneManager.getCurrentScene();
99
+ if (!scene || index < 0 || index >= scene.hotspots.length) {
100
+ return false;
101
+ }
102
+
103
+ if (!confirm('Are you sure you want to delete this hotspot?')) {
104
+ return false;
105
+ }
106
+
107
+ scene.hotspots.splice(index, 1);
108
+
109
+ // Update current index
110
+ if (this.currentHotspotIndex === index) {
111
+ this.currentHotspotIndex = -1;
112
+ } else if (this.currentHotspotIndex > index) {
113
+ this.currentHotspotIndex--;
114
+ }
115
+
116
+ showToast('Hotspot removed', 'success');
117
+ return true;
118
+ }
119
+
120
+ /**
121
+ * Update hotspot property
122
+ */
123
+ updateHotspot(index, property, value) {
124
+ const scene = this.editor.sceneManager.getCurrentScene();
125
+ if (!scene || index < 0 || index >= scene.hotspots.length) {
126
+ return false;
127
+ }
128
+
129
+ scene.hotspots[index][property] = value;
130
+ return true;
131
+ }
132
+
133
+ /**
134
+ * Get hotspot by index
135
+ */
136
+ getHotspot(index) {
137
+ const scene = this.editor.sceneManager.getCurrentScene();
138
+ if (!scene || index < 0 || index >= scene.hotspots.length) {
139
+ return null;
140
+ }
141
+ return scene.hotspots[index];
142
+ }
143
+
144
+ /**
145
+ * Update hotspot position
146
+ */
147
+ updateHotspotPosition(index, position) {
148
+ return this.updateHotspot(index, 'position', position);
149
+ }
150
+
151
+ /**
152
+ * Get current hotspot
153
+ */
154
+ getCurrentHotspot() {
155
+ const scene = this.editor.sceneManager.getCurrentScene();
156
+ if (!scene || this.currentHotspotIndex < 0) {
157
+ return null;
158
+ }
159
+ return scene.hotspots[this.currentHotspotIndex] || null;
160
+ }
161
+
162
+ /**
163
+ * Set current hotspot
164
+ */
165
+ setCurrentHotspot(index) {
166
+ const scene = this.editor.sceneManager.getCurrentScene();
167
+ if (!scene || index < 0 || index >= scene.hotspots.length) {
168
+ this.currentHotspotIndex = -1;
169
+ return false;
170
+ }
171
+
172
+ this.currentHotspotIndex = index;
173
+ return true;
174
+ }
175
+
176
+ /**
177
+ * Get all hotspots for current scene
178
+ */
179
+ getAllHotspots() {
180
+ const scene = this.editor.sceneManager.getCurrentScene();
181
+ return scene ? scene.hotspots : [];
182
+ }
183
+
184
+ /**
185
+ * Duplicate hotspot
186
+ */
187
+ duplicateHotspot(index) {
188
+ const scene = this.editor.sceneManager.getCurrentScene();
189
+ if (!scene || index < 0 || index >= scene.hotspots.length) {
190
+ return null;
191
+ }
192
+
193
+ const original = scene.hotspots[index];
194
+ const duplicate = deepClone(original);
195
+ duplicate.id = generateId('hotspot');
196
+ duplicate.title = original.title + ' (Copy)';
197
+
198
+ // Offset position slightly
199
+ duplicate.position = {
200
+ x: original.position.x + 0.5,
201
+ y: original.position.y,
202
+ z: original.position.z
203
+ };
204
+
205
+ scene.hotspots.push(duplicate);
206
+ this.currentHotspotIndex = scene.hotspots.length - 1;
207
+
208
+ showToast('Hotspot duplicated', 'success');
209
+ return duplicate;
210
+ }
211
+
212
+ /**
213
+ * Clear all hotspots
214
+ */
215
+ clearAllHotspots() {
216
+ const scene = this.editor.sceneManager.getCurrentScene();
217
+ if (!scene) {
218
+ return false;
219
+ }
220
+
221
+ if (scene.hotspots.length === 0) {
222
+ return true;
223
+ }
224
+
225
+ if (!confirm('Are you sure you want to remove all hotspots from this scene?')) {
226
+ return false;
227
+ }
228
+
229
+ scene.hotspots = [];
230
+ this.currentHotspotIndex = -1;
231
+
232
+ showToast('All hotspots removed', 'success');
233
+ return true;
234
+ }
235
+ }
236
+
237
+ export default HotspotEditor;