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,556 @@
|
|
|
1
|
+
// Preview Controller - Manages A-Frame preview integration using SWT library
|
|
2
|
+
import { showToast } from "./utils.js";
|
|
3
|
+
|
|
4
|
+
class PreviewController {
|
|
5
|
+
constructor(editor) {
|
|
6
|
+
this.editor = editor;
|
|
7
|
+
this.tour = null;
|
|
8
|
+
this.isInitialized = false;
|
|
9
|
+
this.previewContainer = null;
|
|
10
|
+
this.hasLoadedScene = false; // Track if we've ever loaded a scene
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Initialize A-Frame preview
|
|
15
|
+
*/
|
|
16
|
+
async init() {
|
|
17
|
+
this.previewContainer = document.getElementById("preview");
|
|
18
|
+
if (!this.previewContainer) {
|
|
19
|
+
console.error("Preview element not found");
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Wait for A-Frame to be loaded
|
|
24
|
+
if (typeof AFRAME === "undefined") {
|
|
25
|
+
await this.waitForLibrary("AFRAME", 5000);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Wait for SWT library to be loaded
|
|
29
|
+
if (typeof SWT === "undefined") {
|
|
30
|
+
await this.waitForLibrary("SWT", 5000);
|
|
31
|
+
}
|
|
32
|
+
this.isInitialized = true;
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Wait for a global library to be available
|
|
38
|
+
*/
|
|
39
|
+
async waitForLibrary(libraryName, timeout = 5000) {
|
|
40
|
+
const startTime = Date.now();
|
|
41
|
+
|
|
42
|
+
while (typeof window[libraryName] === "undefined") {
|
|
43
|
+
if (Date.now() - startTime > timeout) {
|
|
44
|
+
throw new Error(`Timeout waiting for ${libraryName} to load`);
|
|
45
|
+
}
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Load scene into preview using SWT library
|
|
52
|
+
*/
|
|
53
|
+
async loadScene(scene, preserveCameraRotation = true) {
|
|
54
|
+
if (!this.isInitialized || !scene) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Validate scene has required data
|
|
59
|
+
if (!scene.imageUrl || !scene.id) {
|
|
60
|
+
console.error("Invalid scene data:", scene);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Show loading animation
|
|
65
|
+
this.showLoading();
|
|
66
|
+
|
|
67
|
+
// Save camera rotation before destroying scene
|
|
68
|
+
let savedRotation = null;
|
|
69
|
+
if (preserveCameraRotation && this.tour) {
|
|
70
|
+
savedRotation = this.getCameraRotation();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Destroy existing tour if any
|
|
74
|
+
if (this.tour) {
|
|
75
|
+
try {
|
|
76
|
+
this.tour.destroy();
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error("Error destroying tour:", error);
|
|
79
|
+
}
|
|
80
|
+
this.tour = null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Clear preview container carefully
|
|
84
|
+
// Only do complex cleanup if we've actually loaded a scene before
|
|
85
|
+
if (this.hasLoadedScene) {
|
|
86
|
+
const existingScene = this.previewContainer.querySelector("a-scene");
|
|
87
|
+
if (existingScene) {
|
|
88
|
+
try {
|
|
89
|
+
// Remove the scene element - A-Frame will handle cleanup if it's ready
|
|
90
|
+
this.previewContainer.removeChild(existingScene);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error("Error removing scene:", error);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Clear any remaining children (loading overlays, empty state, etc)
|
|
97
|
+
while (this.previewContainer.firstChild) {
|
|
98
|
+
this.previewContainer.removeChild(this.previewContainer.firstChild);
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
// First load - only remove non-A-Frame elements (like empty state divs)
|
|
102
|
+
const children = Array.from(this.previewContainer.children);
|
|
103
|
+
children.forEach(child => {
|
|
104
|
+
// Only remove if it's NOT an a-scene (shouldn't be any, but be safe)
|
|
105
|
+
if (child.tagName.toLowerCase() !== 'a-scene') {
|
|
106
|
+
this.previewContainer.removeChild(child);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Create loading overlay (will be removed after scene loads)
|
|
112
|
+
const loadingOverlay = this.createLoadingOverlay();
|
|
113
|
+
this.previewContainer.appendChild(loadingOverlay);
|
|
114
|
+
|
|
115
|
+
// Create A-Frame scene
|
|
116
|
+
const aframeScene = document.createElement("a-scene");
|
|
117
|
+
aframeScene.id = "preview-scene";
|
|
118
|
+
aframeScene.setAttribute("embedded", "");
|
|
119
|
+
aframeScene.setAttribute("vr-mode-ui", "enabled: false;");
|
|
120
|
+
this.previewContainer.appendChild(aframeScene);
|
|
121
|
+
|
|
122
|
+
// Give A-Frame a moment to start initializing before we proceed
|
|
123
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
124
|
+
|
|
125
|
+
// Build tour config for this single scene
|
|
126
|
+
// Transform editor scene format to library format
|
|
127
|
+
const transformedHotspots = (scene.hotspots || []).map((h) => ({
|
|
128
|
+
id: h.id,
|
|
129
|
+
position: h.position,
|
|
130
|
+
action: {
|
|
131
|
+
type: h.type === "navigation" ? "navigateTo" : h.type,
|
|
132
|
+
target: h.targetSceneId,
|
|
133
|
+
},
|
|
134
|
+
appearance: {
|
|
135
|
+
color: h.color || "#00ff00",
|
|
136
|
+
icon: h.icon || null,
|
|
137
|
+
scale: h.scale || "1 1 1",
|
|
138
|
+
},
|
|
139
|
+
tooltip: {
|
|
140
|
+
text: h.title || "Hotspot",
|
|
141
|
+
},
|
|
142
|
+
}));
|
|
143
|
+
|
|
144
|
+
const libraryScene = {
|
|
145
|
+
id: scene.id,
|
|
146
|
+
name: scene.name,
|
|
147
|
+
panorama: scene.imageUrl, // Editor uses 'imageUrl', library expects 'panorama'
|
|
148
|
+
hotspots: transformedHotspots,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// Build scenes object with ALL scenes (for navigation to work)
|
|
152
|
+
const allScenes = {};
|
|
153
|
+
const editorScenes = this.editor.sceneManager.scenes || [];
|
|
154
|
+
editorScenes.forEach((s) => {
|
|
155
|
+
const sceneHotspots = (s.hotspots || []).map((h) => ({
|
|
156
|
+
id: h.id,
|
|
157
|
+
position: h.position,
|
|
158
|
+
action: {
|
|
159
|
+
type: h.type === "navigation" ? "navigateTo" : h.type,
|
|
160
|
+
target: h.targetSceneId,
|
|
161
|
+
},
|
|
162
|
+
appearance: {
|
|
163
|
+
color: h.color || "#00ff00",
|
|
164
|
+
icon: h.icon || null,
|
|
165
|
+
scale: h.scale || "1 1 1",
|
|
166
|
+
},
|
|
167
|
+
tooltip: {
|
|
168
|
+
text: h.title || "Hotspot",
|
|
169
|
+
},
|
|
170
|
+
}));
|
|
171
|
+
|
|
172
|
+
allScenes[s.id] = {
|
|
173
|
+
id: s.id,
|
|
174
|
+
name: s.name,
|
|
175
|
+
panorama: s.imageUrl,
|
|
176
|
+
hotspots: sceneHotspots,
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const tourConfig = {
|
|
181
|
+
title: scene.name,
|
|
182
|
+
initialScene: scene.id,
|
|
183
|
+
scenes: allScenes,
|
|
184
|
+
settings: {
|
|
185
|
+
autoRotate: false,
|
|
186
|
+
showCompass: false,
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
// Create new tour instance
|
|
192
|
+
this.tour = new SWT.Tour(aframeScene, tourConfig);
|
|
193
|
+
|
|
194
|
+
// Set up event listeners
|
|
195
|
+
this.tour.addEventListener("tour-started", (e) => {});
|
|
196
|
+
|
|
197
|
+
this.tour.addEventListener("scene-loaded", (e) => {});
|
|
198
|
+
|
|
199
|
+
this.tour.addEventListener("hotspot-activated", (e) => {
|
|
200
|
+
// Find the hotspot index by ID and select it
|
|
201
|
+
const hotspotId = e.detail?.hotspotId;
|
|
202
|
+
if (hotspotId) {
|
|
203
|
+
const scene = this.editor.sceneManager.getCurrentScene();
|
|
204
|
+
if (scene) {
|
|
205
|
+
const hotspotIndex = scene.hotspots.findIndex(
|
|
206
|
+
(h) => h.id === hotspotId
|
|
207
|
+
);
|
|
208
|
+
if (hotspotIndex >= 0) {
|
|
209
|
+
this.editor.selectHotspot(hotspotIndex);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Start the tour
|
|
216
|
+
await this.tour.start();
|
|
217
|
+
|
|
218
|
+
// Mark that we've successfully loaded a scene
|
|
219
|
+
this.hasLoadedScene = true;
|
|
220
|
+
|
|
221
|
+
// Hide loading animation after scene loads
|
|
222
|
+
this.hideLoading();
|
|
223
|
+
|
|
224
|
+
// Restore camera rotation if preserved
|
|
225
|
+
if (savedRotation && preserveCameraRotation) {
|
|
226
|
+
this.setCameraRotation(savedRotation);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Setup click handler after a short delay to ensure A-Frame is ready
|
|
230
|
+
setTimeout(() => {
|
|
231
|
+
this.setupClickHandler();
|
|
232
|
+
}, 500);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
console.error("Failed to load preview:", error);
|
|
235
|
+
showToast("Failed to load preview: " + error.message, "error");
|
|
236
|
+
// Hide loading on error
|
|
237
|
+
this.hideLoading();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Setup click handler for hotspot placement
|
|
243
|
+
*/
|
|
244
|
+
setupClickHandler() {
|
|
245
|
+
if (!this.tour) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const aframeScene = this.previewContainer.querySelector("a-scene");
|
|
250
|
+
if (!aframeScene) {
|
|
251
|
+
setTimeout(() => this.setupClickHandler(), 200); // Retry
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Remove any existing click handler to avoid duplicates
|
|
256
|
+
if (this.clickHandler) {
|
|
257
|
+
aframeScene.removeEventListener("click", this.clickHandler);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Create and store the click handler
|
|
261
|
+
this.clickHandler = (evt) => {
|
|
262
|
+
if (!this.editor.hotspotEditor.placementMode) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Try to get intersection from event detail first
|
|
267
|
+
let intersection = evt.detail?.intersection;
|
|
268
|
+
|
|
269
|
+
// If no intersection, perform manual raycasting
|
|
270
|
+
if (!intersection) {
|
|
271
|
+
const camera = aframeScene.querySelector("[camera]");
|
|
272
|
+
const sky = aframeScene.querySelector("a-sky");
|
|
273
|
+
|
|
274
|
+
if (!camera || !sky) {
|
|
275
|
+
showToast("Scene not ready, please try again", "warning");
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Get mouse position relative to canvas
|
|
280
|
+
const canvas = aframeScene.canvas;
|
|
281
|
+
const rect = canvas.getBoundingClientRect();
|
|
282
|
+
const mouse = {
|
|
283
|
+
x: ((evt.clientX - rect.left) / rect.width) * 2 - 1,
|
|
284
|
+
y: -((evt.clientY - rect.top) / rect.height) * 2 + 1,
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// Perform raycasting
|
|
288
|
+
const raycaster = new THREE.Raycaster();
|
|
289
|
+
const cameraEl = camera.object3D;
|
|
290
|
+
raycaster.setFromCamera(mouse, cameraEl.children[0]); // Get the actual camera
|
|
291
|
+
|
|
292
|
+
// Raycast against the sky sphere
|
|
293
|
+
const intersects = raycaster.intersectObject(sky.object3D, true);
|
|
294
|
+
|
|
295
|
+
if (intersects.length > 0) {
|
|
296
|
+
intersection = intersects[0];
|
|
297
|
+
} else {
|
|
298
|
+
showToast("Click on the panorama image", "warning");
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const point = intersection.point;
|
|
304
|
+
const position = {
|
|
305
|
+
x: parseFloat(point.x.toFixed(2)),
|
|
306
|
+
y: parseFloat(point.y.toFixed(2)),
|
|
307
|
+
z: parseFloat(point.z.toFixed(2)),
|
|
308
|
+
};
|
|
309
|
+
this.editor.addHotspotAtPosition(position);
|
|
310
|
+
};
|
|
311
|
+
aframeScene.addEventListener("click", this.clickHandler);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Get current camera rotation
|
|
316
|
+
*/
|
|
317
|
+
getCameraRotation() {
|
|
318
|
+
const aframeScene = this.previewContainer?.querySelector("a-scene");
|
|
319
|
+
if (!aframeScene) {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const camera = aframeScene.querySelector("[camera]");
|
|
324
|
+
if (!camera) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Get rotation from object3D which is more reliable
|
|
329
|
+
const rotation = camera.object3D.rotation;
|
|
330
|
+
const savedRotation = {
|
|
331
|
+
x: rotation.x,
|
|
332
|
+
y: rotation.y,
|
|
333
|
+
z: rotation.z,
|
|
334
|
+
};
|
|
335
|
+
return savedRotation;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Set camera rotation
|
|
340
|
+
*/
|
|
341
|
+
setCameraRotation(rotation) {
|
|
342
|
+
if (!rotation) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const aframeScene = this.previewContainer?.querySelector("a-scene");
|
|
347
|
+
if (!aframeScene) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const camera = aframeScene.querySelector("[camera]");
|
|
352
|
+
if (!camera) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Set rotation on object3D directly
|
|
357
|
+
const setRotation = () => {
|
|
358
|
+
if (camera.object3D) {
|
|
359
|
+
camera.object3D.rotation.set(rotation.x, rotation.y, rotation.z);
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
// Try immediately and also after a delay to ensure it sticks
|
|
364
|
+
setRotation();
|
|
365
|
+
setTimeout(setRotation, 100);
|
|
366
|
+
setTimeout(setRotation, 300);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Refresh preview (reload current scene while preserving camera rotation)
|
|
371
|
+
*/
|
|
372
|
+
async refresh() {
|
|
373
|
+
const scene = this.editor.sceneManager.getCurrentScene();
|
|
374
|
+
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
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Reset camera
|
|
389
|
+
*/
|
|
390
|
+
resetCamera() {
|
|
391
|
+
const camera = this.previewContainer?.querySelector("[camera]");
|
|
392
|
+
if (camera) {
|
|
393
|
+
camera.setAttribute("rotation", "0 0 0");
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Point camera to hotspot position
|
|
399
|
+
*/
|
|
400
|
+
pointCameraToHotspot(hotspotPosition) {
|
|
401
|
+
if (!hotspotPosition) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const aframeScene = this.previewContainer?.querySelector("a-scene");
|
|
406
|
+
if (!aframeScene) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const camera = aframeScene.querySelector("[camera]");
|
|
411
|
+
if (!camera || !camera.object3D) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Get camera position (usually at origin 0,0,0)
|
|
416
|
+
const cameraPos = camera.object3D.position;
|
|
417
|
+
|
|
418
|
+
// Calculate direction vector from camera to hotspot
|
|
419
|
+
const direction = new THREE.Vector3(
|
|
420
|
+
hotspotPosition.x - cameraPos.x,
|
|
421
|
+
hotspotPosition.y - cameraPos.y,
|
|
422
|
+
hotspotPosition.z - cameraPos.z
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
// Calculate spherical coordinates (yaw and pitch)
|
|
426
|
+
const distance = direction.length();
|
|
427
|
+
|
|
428
|
+
// Pitch (up/down rotation around X-axis) - in degrees
|
|
429
|
+
const pitch = Math.asin(direction.y / distance) * (180 / Math.PI);
|
|
430
|
+
|
|
431
|
+
// Yaw (left/right rotation around Y-axis) - in degrees
|
|
432
|
+
// Using atan2 to get correct quadrant
|
|
433
|
+
const yaw = Math.atan2(direction.x, direction.z) * (180 / Math.PI);
|
|
434
|
+
|
|
435
|
+
// Apply smooth rotation with animation
|
|
436
|
+
this.animateCameraRotation(camera, { x: pitch, y: yaw, z: 0 });
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Animate camera rotation smoothly
|
|
441
|
+
*/
|
|
442
|
+
animateCameraRotation(camera, targetRotation, duration = 800) {
|
|
443
|
+
if (!camera || !camera.object3D) return;
|
|
444
|
+
|
|
445
|
+
const startRotation = {
|
|
446
|
+
x: camera.object3D.rotation.x * (180 / Math.PI),
|
|
447
|
+
y: camera.object3D.rotation.y * (180 / Math.PI),
|
|
448
|
+
z: camera.object3D.rotation.z * (180 / Math.PI),
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
// Handle angle wrapping for smooth rotation
|
|
452
|
+
let deltaY = targetRotation.y - startRotation.y;
|
|
453
|
+
|
|
454
|
+
// Normalize to -180 to 180 range
|
|
455
|
+
while (deltaY > 180) deltaY -= 360;
|
|
456
|
+
while (deltaY < -180) deltaY += 360;
|
|
457
|
+
|
|
458
|
+
const endRotationY = startRotation.y + deltaY;
|
|
459
|
+
|
|
460
|
+
const startTime = Date.now();
|
|
461
|
+
|
|
462
|
+
const animate = () => {
|
|
463
|
+
const elapsed = Date.now() - startTime;
|
|
464
|
+
const progress = Math.min(elapsed / duration, 1);
|
|
465
|
+
|
|
466
|
+
// Ease-in-out function for smooth animation
|
|
467
|
+
const eased =
|
|
468
|
+
progress < 0.5
|
|
469
|
+
? 2 * progress * progress
|
|
470
|
+
: 1 - Math.pow(-2 * progress + 2, 2) / 2;
|
|
471
|
+
|
|
472
|
+
// Interpolate rotation
|
|
473
|
+
const currentRotation = {
|
|
474
|
+
x: startRotation.x + (targetRotation.x - startRotation.x) * eased,
|
|
475
|
+
y: startRotation.y + (endRotationY - startRotation.y) * eased,
|
|
476
|
+
z: startRotation.z + (targetRotation.z - startRotation.z) * eased,
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
// Apply rotation (convert degrees to radians)
|
|
480
|
+
camera.object3D.rotation.set(
|
|
481
|
+
currentRotation.x * (Math.PI / 180),
|
|
482
|
+
currentRotation.y * (Math.PI / 180),
|
|
483
|
+
currentRotation.z * (Math.PI / 180)
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
if (progress < 1) {
|
|
487
|
+
requestAnimationFrame(animate);
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
animate();
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Highlight hotspot (not needed with library, but keep for compatibility)
|
|
496
|
+
*/
|
|
497
|
+
highlightHotspot(index) {
|
|
498
|
+
// The library handles hotspot visualization
|
|
499
|
+
// This is kept for API compatibility
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Update hotspot marker (refresh scene while preserving camera rotation)
|
|
504
|
+
*/
|
|
505
|
+
async updateHotspotMarker(index) {
|
|
506
|
+
const scene = this.editor.sceneManager.getCurrentScene();
|
|
507
|
+
if (!scene || !this.tour) return;
|
|
508
|
+
|
|
509
|
+
const hotspot = scene.hotspots[index];
|
|
510
|
+
if (!hotspot) return;
|
|
511
|
+
|
|
512
|
+
// Refresh the preview to reflect changes, camera rotation will be preserved
|
|
513
|
+
await this.refresh();
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Create loading overlay element
|
|
518
|
+
*/
|
|
519
|
+
createLoadingOverlay() {
|
|
520
|
+
const overlay = document.createElement("div");
|
|
521
|
+
overlay.className = "preview-loading";
|
|
522
|
+
overlay.innerHTML = `
|
|
523
|
+
<div class="loading-spinner"></div>
|
|
524
|
+
<div class="loading-text">Loading scene...</div>
|
|
525
|
+
`;
|
|
526
|
+
return overlay;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Show loading animation
|
|
531
|
+
*/
|
|
532
|
+
showLoading() {
|
|
533
|
+
const existing = this.previewContainer?.querySelector(".preview-loading");
|
|
534
|
+
if (existing) {
|
|
535
|
+
existing.classList.remove("hidden");
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Hide loading animation
|
|
541
|
+
*/
|
|
542
|
+
hideLoading() {
|
|
543
|
+
const loading = this.previewContainer?.querySelector(".preview-loading");
|
|
544
|
+
if (loading) {
|
|
545
|
+
loading.classList.add("hidden");
|
|
546
|
+
// Remove after transition
|
|
547
|
+
setTimeout(() => {
|
|
548
|
+
if (loading.parentNode) {
|
|
549
|
+
loading.parentNode.removeChild(loading);
|
|
550
|
+
}
|
|
551
|
+
}, 300);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export default PreviewController;
|