senangwebs-tour 1.0.6 → 1.0.8
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/README.md +21 -9
- package/dist/swt-editor.js +165 -177
- package/dist/swt-editor.js.map +1 -1
- package/dist/swt-editor.min.js +1 -1
- package/dist/swt.js +31 -2
- package/dist/swt.js.map +1 -1
- package/dist/swt.min.js +1 -1
- package/package.json +1 -1
- package/src/editor/js/data-transform.js +42 -0
- package/src/editor/js/editor.js +10 -8
- package/src/editor/js/export-manager.js +5 -67
- package/src/editor/js/hotspot-editor.js +36 -10
- package/src/editor/js/preview-controller.js +8 -62
- package/src/editor/js/scene-manager.js +9 -8
- package/src/editor/js/storage-manager.js +4 -2
- package/src/editor/js/ui-controller.js +52 -22
- package/src/index.js +31 -2
package/package.json
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data Transform Utilities
|
|
3
|
+
*
|
|
4
|
+
* With the unified format, editor and library use the same structure:
|
|
5
|
+
* - scenes: Array of scene objects with `panorama`
|
|
6
|
+
* - hotspots: nested properties (action, appearance, tooltip)
|
|
7
|
+
*
|
|
8
|
+
* These utilities handle building the tour config for the SWT library.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build a complete tour configuration for the SWT library
|
|
13
|
+
* Since unified format is used, this just wraps the scenes array with config
|
|
14
|
+
* @param {Object} config - Config with initialSceneId, etc.
|
|
15
|
+
* @param {Array} scenes - Array of scenes (already in unified format)
|
|
16
|
+
* @returns {Object} Complete tour config
|
|
17
|
+
*/
|
|
18
|
+
export function buildTourConfig(config, scenes) {
|
|
19
|
+
// Determine initial scene
|
|
20
|
+
let initialScene = config.initialSceneId;
|
|
21
|
+
if (!initialScene && scenes && scenes.length > 0) {
|
|
22
|
+
initialScene = scenes[0].id;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
initialScene: initialScene,
|
|
27
|
+
scenes: scenes || [],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build tour config for preview (sets initial scene to current scene)
|
|
33
|
+
* @param {Object} currentScene - The scene to show initially
|
|
34
|
+
* @param {Array} allScenes - All scenes for navigation support
|
|
35
|
+
* @returns {Object} Complete tour config
|
|
36
|
+
*/
|
|
37
|
+
export function buildPreviewTourConfig(currentScene, allScenes) {
|
|
38
|
+
return buildTourConfig(
|
|
39
|
+
{ initialSceneId: currentScene.id },
|
|
40
|
+
allScenes
|
|
41
|
+
);
|
|
42
|
+
}
|
package/src/editor/js/editor.js
CHANGED
|
@@ -55,8 +55,8 @@ class TourEditor {
|
|
|
55
55
|
// Setup event listeners
|
|
56
56
|
this.setupEventListeners();
|
|
57
57
|
|
|
58
|
-
// Populate icon grid
|
|
59
|
-
this.uiController.populateIconGrid();
|
|
58
|
+
// Populate icon grid (async to wait for custom element registration)
|
|
59
|
+
await this.uiController.populateIconGrid();
|
|
60
60
|
|
|
61
61
|
// Load saved project if exists (but only if it has valid data)
|
|
62
62
|
if (this.storageManager.hasProject()) {
|
|
@@ -149,19 +149,19 @@ class TourEditor {
|
|
|
149
149
|
});
|
|
150
150
|
|
|
151
151
|
document.getElementById('hotspotTitle')?.addEventListener('input', debounce((e) => {
|
|
152
|
-
this.updateCurrentHotspot('
|
|
152
|
+
this.updateCurrentHotspot('tooltip.text', e.target.value);
|
|
153
153
|
}, 300));
|
|
154
154
|
|
|
155
155
|
document.getElementById('hotspotDescription')?.addEventListener('input', debounce((e) => {
|
|
156
|
-
this.updateCurrentHotspot('description', e.target.value);
|
|
156
|
+
this.updateCurrentHotspot('tooltip.description', e.target.value);
|
|
157
157
|
}, 300));
|
|
158
158
|
|
|
159
159
|
document.getElementById('hotspotTarget')?.addEventListener('change', (e) => {
|
|
160
|
-
this.updateCurrentHotspot('
|
|
160
|
+
this.updateCurrentHotspot('action.target', e.target.value);
|
|
161
161
|
});
|
|
162
162
|
|
|
163
163
|
document.getElementById('hotspotColor')?.addEventListener('input', (e) => {
|
|
164
|
-
this.updateCurrentHotspot('color', e.target.value);
|
|
164
|
+
this.updateCurrentHotspot('appearance.color', e.target.value);
|
|
165
165
|
});
|
|
166
166
|
|
|
167
167
|
// Icon grid button clicks
|
|
@@ -173,7 +173,7 @@ class TourEditor {
|
|
|
173
173
|
document.querySelectorAll('#hotspotIconGrid .icon-btn').forEach(b => b.classList.remove('active'));
|
|
174
174
|
btn.classList.add('active');
|
|
175
175
|
// Update hotspot
|
|
176
|
-
this.updateCurrentHotspot('icon', iconValue);
|
|
176
|
+
this.updateCurrentHotspot('appearance.icon', iconValue);
|
|
177
177
|
}
|
|
178
178
|
});
|
|
179
179
|
|
|
@@ -502,7 +502,8 @@ class TourEditor {
|
|
|
502
502
|
const index = this.sceneManager.currentSceneIndex;
|
|
503
503
|
if (index < 0) return;
|
|
504
504
|
|
|
505
|
-
|
|
505
|
+
// Use panorama for unified format
|
|
506
|
+
if (this.sceneManager.updateScene(index, 'panorama', imageUrl)) {
|
|
506
507
|
const scene = this.sceneManager.getCurrentScene();
|
|
507
508
|
if (scene) {
|
|
508
509
|
scene.thumbnail = imageUrl;
|
|
@@ -569,6 +570,7 @@ class TourEditor {
|
|
|
569
570
|
render() {
|
|
570
571
|
this.uiController.renderSceneList();
|
|
571
572
|
this.uiController.renderHotspotList();
|
|
573
|
+
this.uiController.populateIconGrid(); // Re-render icon grid to ensure icons display
|
|
572
574
|
|
|
573
575
|
const currentScene = this.sceneManager.getCurrentScene();
|
|
574
576
|
const currentHotspot = this.hotspotEditor.getCurrentHotspot();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Export Manager - Handles JSON generation for SWT library
|
|
2
2
|
import { downloadTextAsFile, showModal, copyToClipboard } from "./utils.js";
|
|
3
3
|
import { IconRenderer } from "../../IconRenderer.js";
|
|
4
|
+
import { buildTourConfig } from "./data-transform.js";
|
|
4
5
|
|
|
5
6
|
class ExportManager {
|
|
6
7
|
constructor(editor) {
|
|
@@ -11,75 +12,13 @@ class ExportManager {
|
|
|
11
12
|
/**
|
|
12
13
|
* Generate JSON compatible with SWT library
|
|
13
14
|
* Follows the tourConfig structure from example-simple.html
|
|
15
|
+
* Uses shared data-transform utilities for consistent transformation
|
|
14
16
|
*/
|
|
15
17
|
generateJSON() {
|
|
16
18
|
const scenes = this.editor.sceneManager.getAllScenes();
|
|
17
19
|
const config = this.editor.config;
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
const scenesData = {};
|
|
21
|
-
scenes.forEach((scene) => {
|
|
22
|
-
scenesData[scene.id] = {
|
|
23
|
-
name: scene.name,
|
|
24
|
-
panorama: scene.imageUrl,
|
|
25
|
-
hotspots: scene.hotspots.map((hotspot) => {
|
|
26
|
-
const hotspotData = {
|
|
27
|
-
position: hotspot.position,
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
// Add action based on hotspot type
|
|
31
|
-
if (hotspot.type === "navigation" && hotspot.targetSceneId) {
|
|
32
|
-
hotspotData.action = {
|
|
33
|
-
type: "navigateTo",
|
|
34
|
-
target: hotspot.targetSceneId,
|
|
35
|
-
};
|
|
36
|
-
} else if (hotspot.type === "info") {
|
|
37
|
-
hotspotData.action = {
|
|
38
|
-
type: "showInfo",
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Add appearance
|
|
43
|
-
hotspotData.appearance = {
|
|
44
|
-
color: hotspot.color || "#FF6B6B",
|
|
45
|
-
scale: hotspot.scale || "2 2 2",
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
// Add icon if set
|
|
49
|
-
if (hotspot.icon) {
|
|
50
|
-
hotspotData.appearance.icon = hotspot.icon;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Add tooltip if title exists
|
|
54
|
-
if (hotspot.title) {
|
|
55
|
-
hotspotData.tooltip = {
|
|
56
|
-
text: hotspot.title,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return hotspotData;
|
|
61
|
-
}),
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
// Add starting position if set
|
|
65
|
-
if (scene.startingPosition) {
|
|
66
|
-
scenesData[scene.id].startingPosition = scene.startingPosition;
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
// Determine initial scene
|
|
71
|
-
let initialScene = config.initialSceneId;
|
|
72
|
-
if (!initialScene && scenes.length > 0) {
|
|
73
|
-
initialScene = scenes[0].id;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Build final JSON matching SWT tourConfig format
|
|
77
|
-
const jsonData = {
|
|
78
|
-
initialScene: initialScene,
|
|
79
|
-
scenes: scenesData,
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
return jsonData;
|
|
21
|
+
return buildTourConfig(config, scenes);
|
|
83
22
|
}
|
|
84
23
|
|
|
85
24
|
/**
|
|
@@ -90,9 +29,8 @@ class ExportManager {
|
|
|
90
29
|
const jsonData = this.generateJSON();
|
|
91
30
|
|
|
92
31
|
// Process all scenes and convert icon names to data URLs
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
32
|
+
// Scenes are an array in unified format
|
|
33
|
+
for (const scene of jsonData.scenes) {
|
|
96
34
|
for (let i = 0; i < scene.hotspots.length; i++) {
|
|
97
35
|
const hotspot = scene.hotspots[i];
|
|
98
36
|
const icon = hotspot.appearance?.icon;
|
|
@@ -20,17 +20,24 @@ class HotspotEditor {
|
|
|
20
20
|
return null;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// Use unified library format with nested structure
|
|
23
24
|
const hotspot = {
|
|
24
25
|
id: generateId('hotspot'),
|
|
25
|
-
type: 'navigation',
|
|
26
26
|
position: position,
|
|
27
|
-
cameraOrientation: cameraOrientation,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
cameraOrientation: cameraOrientation,
|
|
28
|
+
action: {
|
|
29
|
+
type: 'navigateTo',
|
|
30
|
+
target: targetSceneId
|
|
31
|
+
},
|
|
32
|
+
appearance: {
|
|
33
|
+
color: '#00ff00',
|
|
34
|
+
scale: '1 1 1',
|
|
35
|
+
icon: ''
|
|
36
|
+
},
|
|
37
|
+
tooltip: {
|
|
38
|
+
text: 'New Hotspot',
|
|
39
|
+
description: ''
|
|
40
|
+
}
|
|
34
41
|
};
|
|
35
42
|
|
|
36
43
|
scene.hotspots.push(hotspot);
|
|
@@ -69,6 +76,7 @@ class HotspotEditor {
|
|
|
69
76
|
|
|
70
77
|
/**
|
|
71
78
|
* Update hotspot property
|
|
79
|
+
* Supports nested properties like 'appearance.color', 'action.target', 'tooltip.text'
|
|
72
80
|
*/
|
|
73
81
|
updateHotspot(index, property, value) {
|
|
74
82
|
const scene = this.editor.sceneManager.getCurrentScene();
|
|
@@ -76,7 +84,23 @@ class HotspotEditor {
|
|
|
76
84
|
return false;
|
|
77
85
|
}
|
|
78
86
|
|
|
79
|
-
scene.hotspots[index]
|
|
87
|
+
const hotspot = scene.hotspots[index];
|
|
88
|
+
|
|
89
|
+
// Handle nested properties (e.g., 'appearance.color')
|
|
90
|
+
if (property.includes('.')) {
|
|
91
|
+
const parts = property.split('.');
|
|
92
|
+
let obj = hotspot;
|
|
93
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
94
|
+
if (!obj[parts[i]]) {
|
|
95
|
+
obj[parts[i]] = {};
|
|
96
|
+
}
|
|
97
|
+
obj = obj[parts[i]];
|
|
98
|
+
}
|
|
99
|
+
obj[parts[parts.length - 1]] = value;
|
|
100
|
+
} else {
|
|
101
|
+
hotspot[property] = value;
|
|
102
|
+
}
|
|
103
|
+
|
|
80
104
|
return true;
|
|
81
105
|
}
|
|
82
106
|
|
|
@@ -143,7 +167,9 @@ class HotspotEditor {
|
|
|
143
167
|
const original = scene.hotspots[index];
|
|
144
168
|
const duplicate = deepClone(original);
|
|
145
169
|
duplicate.id = generateId('hotspot');
|
|
146
|
-
duplicate.
|
|
170
|
+
if (duplicate.tooltip) {
|
|
171
|
+
duplicate.tooltip.text = (original.tooltip?.text || 'Hotspot') + ' (Copy)';
|
|
172
|
+
}
|
|
147
173
|
|
|
148
174
|
// Offset position slightly
|
|
149
175
|
duplicate.position = {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Preview Controller - Manages A-Frame preview integration using SWT library
|
|
2
2
|
import { showToast } from "./utils.js";
|
|
3
|
+
import { buildPreviewTourConfig } from "./data-transform.js";
|
|
3
4
|
|
|
4
5
|
class PreviewController {
|
|
5
6
|
constructor(editor) {
|
|
@@ -49,14 +50,16 @@ class PreviewController {
|
|
|
49
50
|
|
|
50
51
|
/**
|
|
51
52
|
* Load scene into preview using SWT library
|
|
53
|
+
* Scenes and hotspots now use unified library format directly
|
|
52
54
|
*/
|
|
53
55
|
async loadScene(scene, preserveCameraRotation = true) {
|
|
54
56
|
if (!this.isInitialized || !scene) {
|
|
55
57
|
return;
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
// Validate scene has required data
|
|
59
|
-
|
|
60
|
+
// Validate scene has required data (panorama or imageUrl for backward compatibility)
|
|
61
|
+
const panoramaUrl = scene.panorama || scene.imageUrl;
|
|
62
|
+
if (!panoramaUrl || !scene.id) {
|
|
60
63
|
console.error("Invalid scene data:", scene);
|
|
61
64
|
return;
|
|
62
65
|
}
|
|
@@ -122,67 +125,10 @@ class PreviewController {
|
|
|
122
125
|
// Give A-Frame a moment to start initializing before we proceed
|
|
123
126
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
124
127
|
|
|
125
|
-
// Build tour config
|
|
126
|
-
//
|
|
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 = {};
|
|
128
|
+
// Build tour config using shared transform utilities
|
|
129
|
+
// This includes ALL scenes for navigation support
|
|
153
130
|
const editorScenes = this.editor.sceneManager.scenes || [];
|
|
154
|
-
editorScenes
|
|
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
|
-
startingPosition: s.startingPosition || null,
|
|
178
|
-
};
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
const tourConfig = {
|
|
182
|
-
title: scene.name,
|
|
183
|
-
initialScene: scene.id,
|
|
184
|
-
scenes: allScenes,
|
|
185
|
-
};
|
|
131
|
+
const tourConfig = buildPreviewTourConfig(scene, editorScenes);
|
|
186
132
|
|
|
187
133
|
try {
|
|
188
134
|
// Create new tour instance
|
|
@@ -18,7 +18,7 @@ class SceneManagerEditor {
|
|
|
18
18
|
* @param {File|Object} fileOrConfig - Either a File object or a scene config object
|
|
19
19
|
* @param {string} fileOrConfig.id - Scene ID (if config object)
|
|
20
20
|
* @param {string} fileOrConfig.name - Scene name (if config object)
|
|
21
|
-
* @param {string} fileOrConfig.
|
|
21
|
+
* @param {string} fileOrConfig.panorama - Panorama URL (if config object)
|
|
22
22
|
* @param {string} fileOrConfig.thumbnail - Thumbnail URL (if config object)
|
|
23
23
|
* @param {Array} fileOrConfig.hotspots - Hotspots array (if config object)
|
|
24
24
|
*/
|
|
@@ -35,17 +35,17 @@ class SceneManagerEditor {
|
|
|
35
35
|
scene = {
|
|
36
36
|
id: sanitizeId(fileOrConfig.name.replace(/\.[^/.]+$/, "")),
|
|
37
37
|
name: fileOrConfig.name.replace(/\.[^/.]+$/, ""),
|
|
38
|
-
|
|
38
|
+
panorama: imageDataUrl,
|
|
39
39
|
thumbnail: thumbnail,
|
|
40
40
|
hotspots: [],
|
|
41
41
|
};
|
|
42
42
|
} else if (typeof fileOrConfig === "object" && fileOrConfig !== null) {
|
|
43
|
-
// Handle config object
|
|
43
|
+
// Handle config object - support both panorama and imageUrl for backward compatibility
|
|
44
44
|
scene = {
|
|
45
45
|
id: fileOrConfig.id || sanitizeId(`scene-${Date.now()}`),
|
|
46
46
|
name: fileOrConfig.name || "Untitled Scene",
|
|
47
|
-
|
|
48
|
-
thumbnail: fileOrConfig.thumbnail || fileOrConfig.imageUrl || "",
|
|
47
|
+
panorama: fileOrConfig.panorama || fileOrConfig.imageUrl || "",
|
|
48
|
+
thumbnail: fileOrConfig.thumbnail || fileOrConfig.panorama || fileOrConfig.imageUrl || "",
|
|
49
49
|
hotspots: fileOrConfig.hotspots || [],
|
|
50
50
|
...(fileOrConfig.startingPosition && { startingPosition: fileOrConfig.startingPosition }),
|
|
51
51
|
};
|
|
@@ -115,12 +115,13 @@ class SceneManagerEditor {
|
|
|
115
115
|
if (index >= 0 && index < this.scenes.length) {
|
|
116
116
|
this.scenes[index][property] = value;
|
|
117
117
|
|
|
118
|
-
// If updating ID, update all hotspot target references
|
|
118
|
+
// If updating ID, update all hotspot target references (uses nested action.target)
|
|
119
119
|
if (property === "id") {
|
|
120
|
+
const oldId = this.scenes[index].id;
|
|
120
121
|
this.scenes.forEach((scene) => {
|
|
121
122
|
scene.hotspots.forEach((hotspot) => {
|
|
122
|
-
if (hotspot.
|
|
123
|
-
hotspot.
|
|
123
|
+
if (hotspot.action?.target === oldId) {
|
|
124
|
+
hotspot.action.target = value;
|
|
124
125
|
}
|
|
125
126
|
});
|
|
126
127
|
});
|
|
@@ -78,8 +78,10 @@ class ProjectStorageManager {
|
|
|
78
78
|
return false;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
// imageUrl is required for scenes to be valid
|
|
82
|
-
|
|
81
|
+
// panorama or imageUrl is required for scenes to be valid (support both formats)
|
|
82
|
+
const hasImage = (scene.panorama && typeof scene.panorama === "string") ||
|
|
83
|
+
(scene.imageUrl && typeof scene.imageUrl === "string");
|
|
84
|
+
if (!hasImage) {
|
|
83
85
|
return false;
|
|
84
86
|
}
|
|
85
87
|
|
|
@@ -53,9 +53,9 @@ class UIController {
|
|
|
53
53
|
dragHandle.innerHTML =
|
|
54
54
|
'<ss-icon icon="arrow-up-down-left-right" thickness="2.2"></ss-icon>';
|
|
55
55
|
|
|
56
|
-
// Thumbnail
|
|
56
|
+
// Thumbnail - use thumbnail, panorama, or imageUrl (backward compatibility)
|
|
57
57
|
const thumbnail = document.createElement("img");
|
|
58
|
-
thumbnail.src = scene.thumbnail || scene.imageUrl;
|
|
58
|
+
thumbnail.src = scene.thumbnail || scene.panorama || scene.imageUrl;
|
|
59
59
|
thumbnail.alt = scene.name;
|
|
60
60
|
|
|
61
61
|
// Info
|
|
@@ -187,18 +187,25 @@ class UIController {
|
|
|
187
187
|
|
|
188
188
|
/**
|
|
189
189
|
* Create hotspot list item
|
|
190
|
+
* Uses unified format with nested appearance, action, and tooltip
|
|
190
191
|
*/
|
|
191
192
|
createHotspotItem(hotspot, index, isActive) {
|
|
192
193
|
const item = document.createElement("div");
|
|
193
194
|
item.className = "hotspot-item" + (isActive ? " active" : "");
|
|
194
195
|
|
|
196
|
+
// Get values from nested structure
|
|
197
|
+
const color = hotspot.appearance?.color || "#00ff00";
|
|
198
|
+
const icon = hotspot.appearance?.icon || "";
|
|
199
|
+
const title = hotspot.tooltip?.text || "Untitled Hotspot";
|
|
200
|
+
const targetSceneId = hotspot.action?.target || "";
|
|
201
|
+
|
|
195
202
|
const colorIndicator = document.createElement("div");
|
|
196
203
|
colorIndicator.className = "hotspot-color";
|
|
197
|
-
colorIndicator.style.backgroundColor =
|
|
204
|
+
colorIndicator.style.backgroundColor = color;
|
|
198
205
|
|
|
199
206
|
// If hotspot has an icon, show it with the color applied
|
|
200
|
-
if (
|
|
201
|
-
colorIndicator.innerHTML = `<ss-icon icon="${
|
|
207
|
+
if (icon) {
|
|
208
|
+
colorIndicator.innerHTML = `<ss-icon icon="${icon}" thickness="2.2" style="color: ${color}; width: 20px; height: 20px;"></ss-icon>`;
|
|
202
209
|
colorIndicator.style.backgroundColor = "transparent";
|
|
203
210
|
colorIndicator.style.display = "flex";
|
|
204
211
|
colorIndicator.style.alignItems = "center";
|
|
@@ -208,24 +215,24 @@ class UIController {
|
|
|
208
215
|
const info = document.createElement("div");
|
|
209
216
|
info.className = "hotspot-info";
|
|
210
217
|
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
218
|
+
const titleEl = document.createElement("div");
|
|
219
|
+
titleEl.className = "hotspot-title";
|
|
220
|
+
titleEl.textContent = title;
|
|
214
221
|
|
|
215
222
|
const target = document.createElement("div");
|
|
216
223
|
target.className = "hotspot-target";
|
|
217
|
-
if (
|
|
224
|
+
if (targetSceneId) {
|
|
218
225
|
const targetScene = this.editor.sceneManager.getSceneById(
|
|
219
|
-
|
|
226
|
+
targetSceneId
|
|
220
227
|
);
|
|
221
228
|
target.textContent = targetScene
|
|
222
229
|
? `→ ${targetScene.name}`
|
|
223
|
-
: `→ ${
|
|
230
|
+
: `→ ${targetSceneId}`;
|
|
224
231
|
} else {
|
|
225
232
|
target.textContent = "No target";
|
|
226
233
|
}
|
|
227
234
|
|
|
228
|
-
info.appendChild(
|
|
235
|
+
info.appendChild(titleEl);
|
|
229
236
|
info.appendChild(target);
|
|
230
237
|
|
|
231
238
|
const actions = document.createElement("div");
|
|
@@ -274,11 +281,27 @@ class UIController {
|
|
|
274
281
|
|
|
275
282
|
/**
|
|
276
283
|
* Populate icon grid from SenangStart icons (baked in at build time)
|
|
284
|
+
* Waits for ss-icon custom element to be defined to avoid race conditions
|
|
277
285
|
*/
|
|
278
|
-
populateIconGrid() {
|
|
286
|
+
async populateIconGrid() {
|
|
279
287
|
const grid = document.getElementById("hotspotIconGrid");
|
|
280
288
|
if (!grid) return;
|
|
281
289
|
|
|
290
|
+
// Wait for ss-icon custom element to be defined before populating
|
|
291
|
+
// This prevents race conditions where icons don't render if the
|
|
292
|
+
// custom element isn't registered yet when this method runs
|
|
293
|
+
try {
|
|
294
|
+
if (customElements.get('ss-icon') === undefined) {
|
|
295
|
+
// Give a reasonable timeout to avoid infinite waiting
|
|
296
|
+
await Promise.race([
|
|
297
|
+
customElements.whenDefined('ss-icon'),
|
|
298
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('ss-icon timeout')), 5000))
|
|
299
|
+
]);
|
|
300
|
+
}
|
|
301
|
+
} catch (err) {
|
|
302
|
+
console.warn('ss-icon custom element not available, icon grid may not render properly:', err.message);
|
|
303
|
+
}
|
|
304
|
+
|
|
282
305
|
// Clear existing content
|
|
283
306
|
grid.innerHTML = "";
|
|
284
307
|
|
|
@@ -307,6 +330,7 @@ class UIController {
|
|
|
307
330
|
|
|
308
331
|
/**
|
|
309
332
|
* Update properties panel for hotspot
|
|
333
|
+
* Uses unified format with nested appearance, action, and tooltip
|
|
310
334
|
*/
|
|
311
335
|
updateHotspotProperties(hotspot) {
|
|
312
336
|
const hotspotAll = document.getElementById("hotspotAll");
|
|
@@ -336,21 +360,26 @@ class UIController {
|
|
|
336
360
|
if (hotspotAll) hotspotAll.style.display = "block";
|
|
337
361
|
if (hotspotProperties) hotspotProperties.style.display = "block";
|
|
338
362
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
363
|
+
// Get values from nested structure
|
|
364
|
+
const title = hotspot.tooltip?.text || "";
|
|
365
|
+
const description = hotspot.tooltip?.description || "";
|
|
366
|
+
const targetSceneId = hotspot.action?.target || "";
|
|
367
|
+
const color = hotspot.appearance?.color || "#00ff00";
|
|
368
|
+
const icon = hotspot.appearance?.icon || "";
|
|
369
|
+
|
|
370
|
+
document.getElementById("hotspotTitle").value = title;
|
|
371
|
+
document.getElementById("hotspotDescription").value = description;
|
|
372
|
+
document.getElementById("hotspotTarget").value = targetSceneId;
|
|
373
|
+
document.getElementById("hotspotColor").value = color;
|
|
345
374
|
|
|
346
375
|
// Update color text input if it exists
|
|
347
376
|
const colorText = document.getElementById("hotspotColorText");
|
|
348
377
|
if (colorText) {
|
|
349
|
-
colorText.value =
|
|
378
|
+
colorText.value = color;
|
|
350
379
|
}
|
|
351
380
|
|
|
352
381
|
// Update icon grid active button
|
|
353
|
-
this.setActiveIconButton(
|
|
382
|
+
this.setActiveIconButton(icon);
|
|
354
383
|
|
|
355
384
|
// Update position inputs
|
|
356
385
|
const pos = hotspot.position || { x: 0, y: 0, z: 0 };
|
|
@@ -380,7 +409,8 @@ class UIController {
|
|
|
380
409
|
|
|
381
410
|
document.getElementById("sceneId").value = scene.id || "";
|
|
382
411
|
document.getElementById("sceneName").value = scene.name || "";
|
|
383
|
-
|
|
412
|
+
// Support both panorama (unified) and imageUrl (legacy)
|
|
413
|
+
document.getElementById("sceneImageUrl").value = scene.panorama || scene.imageUrl || "";
|
|
384
414
|
|
|
385
415
|
// Update starting position display
|
|
386
416
|
if (startingPosDisplay) {
|
package/src/index.js
CHANGED
|
@@ -30,6 +30,9 @@ class Tour {
|
|
|
30
30
|
this.config = tourConfig;
|
|
31
31
|
this.isStarted = false;
|
|
32
32
|
|
|
33
|
+
// Normalize scenes to array format (backward compatibility with object format)
|
|
34
|
+
this.scenesArray = this.normalizeScenesArray(tourConfig.scenes);
|
|
35
|
+
|
|
33
36
|
// Initialize managers
|
|
34
37
|
this.assetManager = new AssetManager(this.sceneEl);
|
|
35
38
|
this.sceneManager = new SceneManager(this.sceneEl, this.assetManager);
|
|
@@ -54,6 +57,32 @@ class Tour {
|
|
|
54
57
|
this.ensureCursor();
|
|
55
58
|
}
|
|
56
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Normalize scenes to array format
|
|
62
|
+
* Supports both array and object (keyed by ID) formats for backward compatibility
|
|
63
|
+
* @param {Array|Object} scenes - Scenes in either format
|
|
64
|
+
* @returns {Array} - Scenes as array with id property
|
|
65
|
+
*/
|
|
66
|
+
normalizeScenesArray(scenes) {
|
|
67
|
+
if (Array.isArray(scenes)) {
|
|
68
|
+
return scenes;
|
|
69
|
+
}
|
|
70
|
+
// Convert object format to array format
|
|
71
|
+
return Object.entries(scenes).map(([id, sceneData]) => ({
|
|
72
|
+
id,
|
|
73
|
+
...sceneData,
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get scene data by ID
|
|
79
|
+
* @param {string} sceneId - The scene ID to find
|
|
80
|
+
* @returns {Object|undefined} - Scene data or undefined if not found
|
|
81
|
+
*/
|
|
82
|
+
getSceneById(sceneId) {
|
|
83
|
+
return this.scenesArray.find((scene) => scene.id === sceneId);
|
|
84
|
+
}
|
|
85
|
+
|
|
57
86
|
/**
|
|
58
87
|
* Ensure the scene has a cursor for interaction
|
|
59
88
|
*/
|
|
@@ -81,7 +110,7 @@ class Tour {
|
|
|
81
110
|
}
|
|
82
111
|
|
|
83
112
|
const initialSceneId = this.config.initialScene;
|
|
84
|
-
const initialSceneData = this.
|
|
113
|
+
const initialSceneData = this.getSceneById(initialSceneId);
|
|
85
114
|
|
|
86
115
|
if (!initialSceneData) {
|
|
87
116
|
throw new Error(
|
|
@@ -123,7 +152,7 @@ class Tour {
|
|
|
123
152
|
* @returns {Promise}
|
|
124
153
|
*/
|
|
125
154
|
async navigateTo(sceneId) {
|
|
126
|
-
const sceneData = this.
|
|
155
|
+
const sceneData = this.getSceneById(sceneId);
|
|
127
156
|
|
|
128
157
|
if (!sceneData) {
|
|
129
158
|
throw new Error(`Scene "${sceneId}" not found in tour configuration`);
|