senangwebs-tour 1.0.2 → 1.0.3

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.
@@ -1,286 +1,199 @@
1
1
  // Export Manager - Handles JSON generation for SWT library
2
- import { downloadTextAsFile, showModal } from './utils.js';
2
+ import { downloadTextAsFile, showModal } from "./utils.js";
3
3
 
4
4
  class ExportManager {
5
- constructor(editor) {
6
- this.editor = editor;
7
- }
5
+ constructor(editor) {
6
+ this.editor = editor;
7
+ }
8
+
9
+ /**
10
+ * Generate JSON compatible with SWT library
11
+ * Follows the tourConfig structure from example-simple.html
12
+ */
13
+ generateJSON() {
14
+ const scenes = this.editor.sceneManager.getAllScenes();
15
+ const config = this.editor.config;
16
+
17
+ // Build scenes object (keyed by scene ID)
18
+ const scenesData = {};
19
+ scenes.forEach((scene) => {
20
+ scenesData[scene.id] = {
21
+ name: scene.name,
22
+ panorama: scene.imageUrl,
23
+ hotspots: scene.hotspots.map((hotspot) => {
24
+ const hotspotData = {
25
+ position: hotspot.position,
26
+ };
27
+
28
+ // Add action based on hotspot type
29
+ if (hotspot.type === "navigation" && hotspot.targetSceneId) {
30
+ hotspotData.action = {
31
+ type: "navigateTo",
32
+ target: hotspot.targetSceneId,
33
+ };
34
+ } else if (hotspot.type === "info") {
35
+ hotspotData.action = {
36
+ type: "showInfo",
37
+ };
38
+ }
39
+
40
+ // Add appearance
41
+ hotspotData.appearance = {
42
+ color: hotspot.color || "#FF6B6B",
43
+ scale: "2 2 2",
44
+ };
8
45
 
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
- }
46
+ // Add tooltip if title exists
47
+ if (hotspot.title) {
48
+ hotspotData.tooltip = {
49
+ text: hotspot.title,
50
+ };
51
+ }
36
52
 
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
- };
53
+ return hotspotData;
54
+ }),
55
+ };
56
+ });
48
57
 
49
- return jsonData;
58
+ // Determine initial scene
59
+ let initialScene = config.initialSceneId;
60
+ if (!initialScene && scenes.length > 0) {
61
+ initialScene = scenes[0].id;
50
62
  }
51
63
 
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
- }
64
+ // Build final JSON matching SWT tourConfig format
65
+ const jsonData = {
66
+ initialScene: initialScene,
67
+ scenes: scenesData,
68
+ };
69
+
70
+ return jsonData;
71
+ }
72
+
73
+ /**
74
+ * Export as JSON file
75
+ */
76
+ exportJSON() {
77
+ try {
78
+ const jsonData = this.generateJSON();
79
+ const json = JSON.stringify(jsonData, null, 2);
80
+
81
+ const title = this.editor.config.title || "tour";
82
+ const filename = sanitizeId(title) + ".json";
83
+ downloadTextAsFile(json, filename);
84
+
85
+ showToast("Tour exported successfully", "success");
86
+ return true;
87
+ } catch (error) {
88
+ console.error("Export failed:", error);
89
+ showToast("Export failed", "error");
90
+ return false;
70
91
  }
92
+ }
71
93
 
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
- }
94
+ /**
95
+ * Copy JSON to clipboard
96
+ */
97
+ async copyJSON() {
98
+ try {
99
+ const jsonData = this.generateJSON();
100
+ const json = JSON.stringify(jsonData, null, 2);
101
+
102
+ const success = await copyToClipboard(json);
103
+ if (success) {
104
+ showToast("JSON copied to clipboard", "success");
105
+ } else {
106
+ showToast("Failed to copy to clipboard", "error");
107
+ }
108
+ return success;
109
+ } catch (error) {
110
+ console.error("Copy failed:", error);
111
+ showToast("Copy failed", "error");
112
+ return false;
92
113
  }
114
+ }
115
+
116
+ /**
117
+ * Generate HTML viewer code
118
+ */
119
+ generateViewerHTML() {
120
+ const jsonData = this.generateJSON();
121
+ const title = this.editor.config.title || "Virtual Tour";
93
122
 
94
- /**
95
- * Generate HTML viewer code
96
- */
97
- generateViewerHTML() {
98
- const jsonData = this.generateJSON();
99
-
100
- return `<!DOCTYPE html>
123
+ return `<!DOCTYPE html>
101
124
  <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>
125
+ <head>
126
+ <meta charset="UTF-8" />
127
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
128
+ <title>${title}</title>
129
+ <script src="https://unpkg.com/senangwebs-tour@latest/dist/swt.min.js"></script>
130
+ </head>
131
+ <body>
132
+ <a-scene id="tour-container">
133
+ <a-camera>
134
+ <a-cursor></a-cursor>
135
+ </a-camera>
136
+ </a-scene>
185
137
 
186
138
  <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
- }
139
+ // Tour configuration
140
+ const tourConfig = ${JSON.stringify(jsonData, null, 8)
141
+ .split("\n")
142
+ .map((line, i) => (i === 0 ? line : " " + line))
143
+ .join("\n")};
144
+
145
+ // Initialize tour when scene is loaded
146
+ const sceneEl = document.querySelector("#tour-container");
147
+ sceneEl.addEventListener("loaded", () => {
148
+ const tour = new SWT.Tour(sceneEl, tourConfig);
149
+ tour.start();
150
+ });
238
151
  </script>
239
- </body>
152
+ </body>
240
153
  </html>`;
241
- }
154
+ }
242
155
 
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
- }
156
+ /**
157
+ * Export as standalone HTML viewer
158
+ */
159
+ exportViewerHTML() {
160
+ try {
161
+ const html = this.generateViewerHTML();
162
+ const title = this.editor.config.title || "tour";
163
+ const filename = sanitizeId(title) + "-viewer.html";
164
+
165
+ downloadTextAsFile(html, filename);
166
+
167
+ showToast("Viewer HTML exported successfully", "success");
168
+ return true;
169
+ } catch (error) {
170
+ console.error("Export viewer failed:", error);
171
+ showToast("Export viewer failed", "error");
172
+ return false;
261
173
  }
174
+ }
175
+
176
+ /**
177
+ * Show export preview in modal
178
+ */
179
+ showExportPreview() {
180
+ try {
181
+ const jsonData = this.generateJSON();
182
+ const json = JSON.stringify(jsonData, null, 2);
183
+
184
+ const preview = document.getElementById("exportPreview");
185
+ if (preview) {
186
+ preview.textContent = json;
187
+ }
262
188
 
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
- }
189
+ showModal("exportModal");
190
+ return true;
191
+ } catch (error) {
192
+ console.error("Failed to show export preview:", error);
193
+ showToast("Failed to generate preview", "error");
194
+ return false;
283
195
  }
196
+ }
284
197
  }
285
198
 
286
199
  export default ExportManager;
@@ -92,7 +92,7 @@ class PreviewController {
92
92
  console.error("Error removing scene:", error);
93
93
  }
94
94
  }
95
-
95
+
96
96
  // Clear any remaining children (loading overlays, empty state, etc)
97
97
  while (this.previewContainer.firstChild) {
98
98
  this.previewContainer.removeChild(this.previewContainer.firstChild);
@@ -100,9 +100,9 @@ class PreviewController {
100
100
  } else {
101
101
  // First load - only remove non-A-Frame elements (like empty state divs)
102
102
  const children = Array.from(this.previewContainer.children);
103
- children.forEach(child => {
103
+ children.forEach((child) => {
104
104
  // Only remove if it's NOT an a-scene (shouldn't be any, but be safe)
105
- if (child.tagName.toLowerCase() !== 'a-scene') {
105
+ if (child.tagName.toLowerCase() !== "a-scene") {
106
106
  this.previewContainer.removeChild(child);
107
107
  }
108
108
  });
@@ -313,6 +313,7 @@ class PreviewController {
313
313
 
314
314
  /**
315
315
  * Get current camera rotation
316
+ * Works with A-Frame's look-controls component by reading its internal state
316
317
  */
317
318
  getCameraRotation() {
318
319
  const aframeScene = this.previewContainer?.querySelector("a-scene");
@@ -325,7 +326,18 @@ class PreviewController {
325
326
  return null;
326
327
  }
327
328
 
328
- // Get rotation from object3D which is more reliable
329
+ // Try to get rotation from look-controls internal state (more reliable)
330
+ const lookControls = camera.components?.["look-controls"];
331
+ if (lookControls && lookControls.pitchObject && lookControls.yawObject) {
332
+ const savedRotation = {
333
+ x: lookControls.pitchObject.rotation.x,
334
+ y: lookControls.yawObject.rotation.y,
335
+ z: 0,
336
+ };
337
+ return savedRotation;
338
+ }
339
+
340
+ // Fallback to object3D rotation
329
341
  const rotation = camera.object3D.rotation;
330
342
  const savedRotation = {
331
343
  x: rotation.x,
@@ -337,6 +349,7 @@ class PreviewController {
337
349
 
338
350
  /**
339
351
  * Set camera rotation
352
+ * Works with A-Frame's look-controls component by setting its internal state
340
353
  */
341
354
  setCameraRotation(rotation) {
342
355
  if (!rotation) {
@@ -353,17 +366,30 @@ class PreviewController {
353
366
  return;
354
367
  }
355
368
 
356
- // Set rotation on object3D directly
369
+ // Set rotation via look-controls internal pitchObject/yawObject
370
+ // This is required because look-controls overrides object3D.rotation on each tick
357
371
  const setRotation = () => {
358
- if (camera.object3D) {
359
- camera.object3D.rotation.set(rotation.x, rotation.y, rotation.z);
372
+ const cam = this.previewContainer?.querySelector("[camera]");
373
+ if (!cam) return;
374
+
375
+ const lookControls = cam.components?.["look-controls"];
376
+ if (lookControls && lookControls.pitchObject && lookControls.yawObject) {
377
+ // Set pitch (x rotation) on pitchObject
378
+ lookControls.pitchObject.rotation.x = rotation.x;
379
+ // Set yaw (y rotation) on yawObject
380
+ lookControls.yawObject.rotation.y = rotation.y;
381
+ } else if (cam.object3D) {
382
+ // Fallback to direct object3D if look-controls not ready
383
+ cam.object3D.rotation.set(rotation.x, rotation.y, rotation.z);
360
384
  }
361
385
  };
362
386
 
363
- // Try immediately and also after a delay to ensure it sticks
387
+ // Try immediately and also after delays to ensure it sticks
388
+ // A-Frame look-controls may not be fully initialized immediately
364
389
  setRotation();
365
390
  setTimeout(setRotation, 100);
366
391
  setTimeout(setRotation, 300);
392
+ setTimeout(setRotation, 500);
367
393
  }
368
394
 
369
395
  /**
@@ -372,15 +398,8 @@ class PreviewController {
372
398
  async refresh() {
373
399
  const scene = this.editor.sceneManager.getCurrentScene();
374
400
  if (scene) {
375
- // Save current camera rotation
376
- const savedRotation = this.getCameraRotation();
377
- // Reload scene
378
- await this.loadScene(scene);
379
-
380
- // Restore camera rotation
381
- if (savedRotation) {
382
- this.setCameraRotation(savedRotation);
383
- }
401
+ // loadScene with preserveCameraRotation=true handles save/restore internally
402
+ await this.loadScene(scene, true);
384
403
  }
385
404
  }
386
405