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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "senangwebs-tour",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "type": "module",
5
5
  "description": "VR 360° virtual tour system with visual editor and viewer library",
6
6
  "main": "dist/swt.js",
@@ -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
+ }
@@ -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('title', e.target.value);
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('targetSceneId', e.target.value);
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
- if (this.sceneManager.updateScene(index, 'imageUrl', imageUrl)) {
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
- // Build scenes object (keyed by scene ID)
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
- for (const sceneId of Object.keys(jsonData.scenes)) {
94
- const scene = jsonData.scenes[sceneId];
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, // Store camera pitch/yaw for reliable pointing
28
- targetSceneId: targetSceneId,
29
- title: 'New Hotspot',
30
- description: '',
31
- color: '#00ff00',
32
- icon: '',
33
- scale: '1 1 1'
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][property] = value;
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.title = original.title + ' (Copy)';
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
- if (!scene.imageUrl || !scene.id) {
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 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 = {};
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.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
- 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.imageUrl - Image URL (if config object)
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
- imageUrl: imageDataUrl,
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 with URL strings
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
- imageUrl: fileOrConfig.imageUrl || "",
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.targetSceneId === this.scenes[index].id) {
123
- hotspot.targetSceneId = value;
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
- if (!scene.imageUrl || typeof scene.imageUrl !== "string") {
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 = hotspot.color;
204
+ colorIndicator.style.backgroundColor = color;
198
205
 
199
206
  // If hotspot has an icon, show it with the color applied
200
- if (hotspot.icon) {
201
- colorIndicator.innerHTML = `<ss-icon icon="${hotspot.icon}" thickness="2.2" style="color: ${hotspot.color}; width: 20px; height: 20px;"></ss-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 title = document.createElement("div");
212
- title.className = "hotspot-title";
213
- title.textContent = hotspot.title || "Untitled Hotspot";
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 (hotspot.targetSceneId) {
224
+ if (targetSceneId) {
218
225
  const targetScene = this.editor.sceneManager.getSceneById(
219
- hotspot.targetSceneId
226
+ targetSceneId
220
227
  );
221
228
  target.textContent = targetScene
222
229
  ? `→ ${targetScene.name}`
223
- : `→ ${hotspot.targetSceneId}`;
230
+ : `→ ${targetSceneId}`;
224
231
  } else {
225
232
  target.textContent = "No target";
226
233
  }
227
234
 
228
- info.appendChild(title);
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
- document.getElementById("hotspotTitle").value = hotspot.title || "";
340
- document.getElementById("hotspotDescription").value =
341
- hotspot.description || "";
342
- document.getElementById("hotspotTarget").value =
343
- hotspot.targetSceneId || "";
344
- document.getElementById("hotspotColor").value = hotspot.color || "#00ff00";
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 = hotspot.color || "#00ff00";
378
+ colorText.value = color;
350
379
  }
351
380
 
352
381
  // Update icon grid active button
353
- this.setActiveIconButton(hotspot.icon || "");
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
- document.getElementById("sceneImageUrl").value = scene.imageUrl || "";
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.config.scenes[initialSceneId];
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.config.scenes[sceneId];
155
+ const sceneData = this.getSceneById(sceneId);
127
156
 
128
157
  if (!sceneData) {
129
158
  throw new Error(`Scene "${sceneId}" not found in tour configuration`);