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 CHANGED
@@ -280,13 +280,21 @@ new SWT.Tour(aframeSceneElement, tourConfiguration);
280
280
 
281
281
  #### Configuration Structure
282
282
 
283
+ Both the editor and viewer use the same data format. Scenes are stored as an **array**:
284
+
283
285
  ```javascript
284
286
  {
285
287
  initialScene: "scene-id", // Required: Starting scene ID
286
- scenes: { // Required: Object (not array!)
287
- "scene-id": { // Key = scene ID
288
+ scenes: [ // Required: Array of scenes
289
+ {
290
+ id: "scene-id", // Required: Scene identifier
288
291
  name: "Scene Name", // Required: Display name
289
292
  panorama: "url-or-dataurl", // Required: Image URL or base64
293
+ thumbnail: "url", // Optional: Thumbnail for editor
294
+ startingPosition: { // Optional: Initial camera orientation
295
+ pitch: 0.1, // Vertical angle (radians)
296
+ yaw: 1.5 // Horizontal angle (radians)
297
+ },
290
298
  hotspots: [ // Optional: Array of hotspots
291
299
  {
292
300
  id: "hotspot-1", // Optional: Auto-generated if omitted
@@ -301,25 +309,29 @@ new SWT.Tour(aframeSceneElement, tourConfiguration);
301
309
  },
302
310
  appearance: { // Optional: Visual customization
303
311
  color: "#00ff00", // Default: "#00ff00"
304
- scale: 1.5, // Default: 1.0 (number or "x y z" string)
305
- icon: "arrow" // Default: sphere (future: custom icons)
312
+ scale: "1 1 1", // Default: "1 1 1"
313
+ icon: "arrow" // Optional: Icon name or URL
306
314
  },
307
315
  tooltip: { // Optional: Hover/focus text
308
- text: "Click to navigate" // Tooltip content
316
+ text: "Click here", // Tooltip title
317
+ description: "Details" // Optional: Extended description
318
+ },
319
+ cameraOrientation: { // Optional: Camera direction when created
320
+ pitch: -0.02, // Vertical angle (radians)
321
+ yaw: 2.06 // Horizontal angle (radians)
309
322
  }
310
323
  }
311
324
  ]
312
325
  }
313
- }
326
+ ]
314
327
  }
315
328
  ```
316
329
 
317
330
  **Important Notes:**
318
331
 
319
- - `scenes` is an **object** (keys are scene IDs), not an array
332
+ - `scenes` is an **array** (not an object)
333
+ - The library also accepts `scenes` as an object keyed by ID for backward compatibility
320
334
  - Hotspot `position` is in 3D space (typically on 10-unit sphere surface)
321
- - Editor stores `imageUrl`, library expects `panorama` (export handles conversion)
322
- - Images can be URLs or Data URLs (base64) for offline tours
323
335
 
324
336
  ---
325
337
 
@@ -300,8 +300,10 @@
300
300
  return false;
301
301
  }
302
302
 
303
- // imageUrl is required for scenes to be valid
304
- if (!scene.imageUrl || typeof scene.imageUrl !== "string") {
303
+ // panorama or imageUrl is required for scenes to be valid (support both formats)
304
+ const hasImage = (scene.panorama && typeof scene.panorama === "string") ||
305
+ (scene.imageUrl && typeof scene.imageUrl === "string");
306
+ if (!hasImage) {
305
307
  return false;
306
308
  }
307
309
 
@@ -426,7 +428,7 @@
426
428
  * @param {File|Object} fileOrConfig - Either a File object or a scene config object
427
429
  * @param {string} fileOrConfig.id - Scene ID (if config object)
428
430
  * @param {string} fileOrConfig.name - Scene name (if config object)
429
- * @param {string} fileOrConfig.imageUrl - Image URL (if config object)
431
+ * @param {string} fileOrConfig.panorama - Panorama URL (if config object)
430
432
  * @param {string} fileOrConfig.thumbnail - Thumbnail URL (if config object)
431
433
  * @param {Array} fileOrConfig.hotspots - Hotspots array (if config object)
432
434
  */
@@ -443,17 +445,17 @@
443
445
  scene = {
444
446
  id: sanitizeId$1(fileOrConfig.name.replace(/\.[^/.]+$/, "")),
445
447
  name: fileOrConfig.name.replace(/\.[^/.]+$/, ""),
446
- imageUrl: imageDataUrl,
448
+ panorama: imageDataUrl,
447
449
  thumbnail: thumbnail,
448
450
  hotspots: [],
449
451
  };
450
452
  } else if (typeof fileOrConfig === "object" && fileOrConfig !== null) {
451
- // Handle config object with URL strings
453
+ // Handle config object - support both panorama and imageUrl for backward compatibility
452
454
  scene = {
453
455
  id: fileOrConfig.id || sanitizeId$1(`scene-${Date.now()}`),
454
456
  name: fileOrConfig.name || "Untitled Scene",
455
- imageUrl: fileOrConfig.imageUrl || "",
456
- thumbnail: fileOrConfig.thumbnail || fileOrConfig.imageUrl || "",
457
+ panorama: fileOrConfig.panorama || fileOrConfig.imageUrl || "",
458
+ thumbnail: fileOrConfig.thumbnail || fileOrConfig.panorama || fileOrConfig.imageUrl || "",
457
459
  hotspots: fileOrConfig.hotspots || [],
458
460
  ...(fileOrConfig.startingPosition && { startingPosition: fileOrConfig.startingPosition }),
459
461
  };
@@ -523,12 +525,13 @@
523
525
  if (index >= 0 && index < this.scenes.length) {
524
526
  this.scenes[index][property] = value;
525
527
 
526
- // If updating ID, update all hotspot target references
528
+ // If updating ID, update all hotspot target references (uses nested action.target)
527
529
  if (property === "id") {
530
+ const oldId = this.scenes[index].id;
528
531
  this.scenes.forEach((scene) => {
529
532
  scene.hotspots.forEach((hotspot) => {
530
- if (hotspot.targetSceneId === this.scenes[index].id) {
531
- hotspot.targetSceneId = value;
533
+ if (hotspot.action?.target === oldId) {
534
+ hotspot.action.target = value;
532
535
  }
533
536
  });
534
537
  });
@@ -649,17 +652,24 @@
649
652
  return null;
650
653
  }
651
654
 
655
+ // Use unified library format with nested structure
652
656
  const hotspot = {
653
657
  id: generateId('hotspot'),
654
- type: 'navigation',
655
658
  position: position,
656
- cameraOrientation: cameraOrientation, // Store camera pitch/yaw for reliable pointing
657
- targetSceneId: targetSceneId,
658
- title: 'New Hotspot',
659
- description: '',
660
- color: '#00ff00',
661
- icon: '',
662
- scale: '1 1 1'
659
+ cameraOrientation: cameraOrientation,
660
+ action: {
661
+ type: 'navigateTo',
662
+ target: targetSceneId
663
+ },
664
+ appearance: {
665
+ color: '#00ff00',
666
+ scale: '1 1 1',
667
+ icon: ''
668
+ },
669
+ tooltip: {
670
+ text: 'New Hotspot',
671
+ description: ''
672
+ }
663
673
  };
664
674
 
665
675
  scene.hotspots.push(hotspot);
@@ -698,6 +708,7 @@
698
708
 
699
709
  /**
700
710
  * Update hotspot property
711
+ * Supports nested properties like 'appearance.color', 'action.target', 'tooltip.text'
701
712
  */
702
713
  updateHotspot(index, property, value) {
703
714
  const scene = this.editor.sceneManager.getCurrentScene();
@@ -705,7 +716,23 @@
705
716
  return false;
706
717
  }
707
718
 
708
- scene.hotspots[index][property] = value;
719
+ const hotspot = scene.hotspots[index];
720
+
721
+ // Handle nested properties (e.g., 'appearance.color')
722
+ if (property.includes('.')) {
723
+ const parts = property.split('.');
724
+ let obj = hotspot;
725
+ for (let i = 0; i < parts.length - 1; i++) {
726
+ if (!obj[parts[i]]) {
727
+ obj[parts[i]] = {};
728
+ }
729
+ obj = obj[parts[i]];
730
+ }
731
+ obj[parts[parts.length - 1]] = value;
732
+ } else {
733
+ hotspot[property] = value;
734
+ }
735
+
709
736
  return true;
710
737
  }
711
738
 
@@ -772,7 +799,9 @@
772
799
  const original = scene.hotspots[index];
773
800
  const duplicate = deepClone(original);
774
801
  duplicate.id = generateId('hotspot');
775
- duplicate.title = original.title + ' (Copy)';
802
+ if (duplicate.tooltip) {
803
+ duplicate.tooltip.text = (original.tooltip?.text || 'Hotspot') + ' (Copy)';
804
+ }
776
805
 
777
806
  // Offset position slightly
778
807
  duplicate.position = {
@@ -816,6 +845,49 @@
816
845
  }
817
846
  };
818
847
 
848
+ /**
849
+ * Data Transform Utilities
850
+ *
851
+ * With the unified format, editor and library use the same structure:
852
+ * - scenes: Array of scene objects with `panorama`
853
+ * - hotspots: nested properties (action, appearance, tooltip)
854
+ *
855
+ * These utilities handle building the tour config for the SWT library.
856
+ */
857
+
858
+ /**
859
+ * Build a complete tour configuration for the SWT library
860
+ * Since unified format is used, this just wraps the scenes array with config
861
+ * @param {Object} config - Config with initialSceneId, etc.
862
+ * @param {Array} scenes - Array of scenes (already in unified format)
863
+ * @returns {Object} Complete tour config
864
+ */
865
+ function buildTourConfig(config, scenes) {
866
+ // Determine initial scene
867
+ let initialScene = config.initialSceneId;
868
+ if (!initialScene && scenes && scenes.length > 0) {
869
+ initialScene = scenes[0].id;
870
+ }
871
+
872
+ return {
873
+ initialScene: initialScene,
874
+ scenes: scenes || [],
875
+ };
876
+ }
877
+
878
+ /**
879
+ * Build tour config for preview (sets initial scene to current scene)
880
+ * @param {Object} currentScene - The scene to show initially
881
+ * @param {Array} allScenes - All scenes for navigation support
882
+ * @returns {Object} Complete tour config
883
+ */
884
+ function buildPreviewTourConfig(currentScene, allScenes) {
885
+ return buildTourConfig(
886
+ { initialSceneId: currentScene.id },
887
+ allScenes
888
+ );
889
+ }
890
+
819
891
  // Preview Controller - Manages A-Frame preview integration using SWT library
820
892
 
821
893
  let PreviewController$1 = class PreviewController {
@@ -866,14 +938,16 @@
866
938
 
867
939
  /**
868
940
  * Load scene into preview using SWT library
941
+ * Scenes and hotspots now use unified library format directly
869
942
  */
870
943
  async loadScene(scene, preserveCameraRotation = true) {
871
944
  if (!this.isInitialized || !scene) {
872
945
  return;
873
946
  }
874
947
 
875
- // Validate scene has required data
876
- if (!scene.imageUrl || !scene.id) {
948
+ // Validate scene has required data (panorama or imageUrl for backward compatibility)
949
+ const panoramaUrl = scene.panorama || scene.imageUrl;
950
+ if (!panoramaUrl || !scene.id) {
877
951
  console.error("Invalid scene data:", scene);
878
952
  return;
879
953
  }
@@ -939,65 +1013,10 @@
939
1013
  // Give A-Frame a moment to start initializing before we proceed
940
1014
  await new Promise((resolve) => setTimeout(resolve, 100));
941
1015
 
942
- // Build tour config for this single scene
943
- // Transform editor scene format to library format
944
- (scene.hotspots || []).map((h) => ({
945
- id: h.id,
946
- position: h.position,
947
- action: {
948
- type: h.type === "navigation" ? "navigateTo" : h.type,
949
- target: h.targetSceneId,
950
- },
951
- appearance: {
952
- color: h.color || "#00ff00",
953
- icon: h.icon || null,
954
- scale: h.scale || "1 1 1",
955
- },
956
- tooltip: {
957
- text: h.title || "Hotspot",
958
- },
959
- }));
960
-
961
- ({
962
- id: scene.id,
963
- name: scene.name,
964
- panorama: scene.imageUrl});
965
-
966
- // Build scenes object with ALL scenes (for navigation to work)
967
- const allScenes = {};
1016
+ // Build tour config using shared transform utilities
1017
+ // This includes ALL scenes for navigation support
968
1018
  const editorScenes = this.editor.sceneManager.scenes || [];
969
- editorScenes.forEach((s) => {
970
- const sceneHotspots = (s.hotspots || []).map((h) => ({
971
- id: h.id,
972
- position: h.position,
973
- action: {
974
- type: h.type === "navigation" ? "navigateTo" : h.type,
975
- target: h.targetSceneId,
976
- },
977
- appearance: {
978
- color: h.color || "#00ff00",
979
- icon: h.icon || null,
980
- scale: h.scale || "1 1 1",
981
- },
982
- tooltip: {
983
- text: h.title || "Hotspot",
984
- },
985
- }));
986
-
987
- allScenes[s.id] = {
988
- id: s.id,
989
- name: s.name,
990
- panorama: s.imageUrl,
991
- hotspots: sceneHotspots,
992
- startingPosition: s.startingPosition || null,
993
- };
994
- });
995
-
996
- const tourConfig = {
997
- title: scene.name,
998
- initialScene: scene.id,
999
- scenes: allScenes,
1000
- };
1019
+ const tourConfig = buildPreviewTourConfig(scene, editorScenes);
1001
1020
 
1002
1021
  try {
1003
1022
  // Create new tour instance
@@ -4022,9 +4041,9 @@
4022
4041
  dragHandle.innerHTML =
4023
4042
  '<ss-icon icon="arrow-up-down-left-right" thickness="2.2"></ss-icon>';
4024
4043
 
4025
- // Thumbnail
4044
+ // Thumbnail - use thumbnail, panorama, or imageUrl (backward compatibility)
4026
4045
  const thumbnail = document.createElement("img");
4027
- thumbnail.src = scene.thumbnail || scene.imageUrl;
4046
+ thumbnail.src = scene.thumbnail || scene.panorama || scene.imageUrl;
4028
4047
  thumbnail.alt = scene.name;
4029
4048
 
4030
4049
  // Info
@@ -4156,18 +4175,25 @@
4156
4175
 
4157
4176
  /**
4158
4177
  * Create hotspot list item
4178
+ * Uses unified format with nested appearance, action, and tooltip
4159
4179
  */
4160
4180
  createHotspotItem(hotspot, index, isActive) {
4161
4181
  const item = document.createElement("div");
4162
4182
  item.className = "hotspot-item" + (isActive ? " active" : "");
4163
4183
 
4184
+ // Get values from nested structure
4185
+ const color = hotspot.appearance?.color || "#00ff00";
4186
+ const icon = hotspot.appearance?.icon || "";
4187
+ const title = hotspot.tooltip?.text || "Untitled Hotspot";
4188
+ const targetSceneId = hotspot.action?.target || "";
4189
+
4164
4190
  const colorIndicator = document.createElement("div");
4165
4191
  colorIndicator.className = "hotspot-color";
4166
- colorIndicator.style.backgroundColor = hotspot.color;
4192
+ colorIndicator.style.backgroundColor = color;
4167
4193
 
4168
4194
  // If hotspot has an icon, show it with the color applied
4169
- if (hotspot.icon) {
4170
- colorIndicator.innerHTML = `<ss-icon icon="${hotspot.icon}" thickness="2.2" style="color: ${hotspot.color}; width: 20px; height: 20px;"></ss-icon>`;
4195
+ if (icon) {
4196
+ colorIndicator.innerHTML = `<ss-icon icon="${icon}" thickness="2.2" style="color: ${color}; width: 20px; height: 20px;"></ss-icon>`;
4171
4197
  colorIndicator.style.backgroundColor = "transparent";
4172
4198
  colorIndicator.style.display = "flex";
4173
4199
  colorIndicator.style.alignItems = "center";
@@ -4177,24 +4203,24 @@
4177
4203
  const info = document.createElement("div");
4178
4204
  info.className = "hotspot-info";
4179
4205
 
4180
- const title = document.createElement("div");
4181
- title.className = "hotspot-title";
4182
- title.textContent = hotspot.title || "Untitled Hotspot";
4206
+ const titleEl = document.createElement("div");
4207
+ titleEl.className = "hotspot-title";
4208
+ titleEl.textContent = title;
4183
4209
 
4184
4210
  const target = document.createElement("div");
4185
4211
  target.className = "hotspot-target";
4186
- if (hotspot.targetSceneId) {
4212
+ if (targetSceneId) {
4187
4213
  const targetScene = this.editor.sceneManager.getSceneById(
4188
- hotspot.targetSceneId
4214
+ targetSceneId
4189
4215
  );
4190
4216
  target.textContent = targetScene
4191
4217
  ? `→ ${targetScene.name}`
4192
- : `→ ${hotspot.targetSceneId}`;
4218
+ : `→ ${targetSceneId}`;
4193
4219
  } else {
4194
4220
  target.textContent = "No target";
4195
4221
  }
4196
4222
 
4197
- info.appendChild(title);
4223
+ info.appendChild(titleEl);
4198
4224
  info.appendChild(target);
4199
4225
 
4200
4226
  const actions = document.createElement("div");
@@ -4243,11 +4269,27 @@
4243
4269
 
4244
4270
  /**
4245
4271
  * Populate icon grid from SenangStart icons (baked in at build time)
4272
+ * Waits for ss-icon custom element to be defined to avoid race conditions
4246
4273
  */
4247
- populateIconGrid() {
4274
+ async populateIconGrid() {
4248
4275
  const grid = document.getElementById("hotspotIconGrid");
4249
4276
  if (!grid) return;
4250
4277
 
4278
+ // Wait for ss-icon custom element to be defined before populating
4279
+ // This prevents race conditions where icons don't render if the
4280
+ // custom element isn't registered yet when this method runs
4281
+ try {
4282
+ if (customElements.get('ss-icon') === undefined) {
4283
+ // Give a reasonable timeout to avoid infinite waiting
4284
+ await Promise.race([
4285
+ customElements.whenDefined('ss-icon'),
4286
+ new Promise((_, reject) => setTimeout(() => reject(new Error('ss-icon timeout')), 5000))
4287
+ ]);
4288
+ }
4289
+ } catch (err) {
4290
+ console.warn('ss-icon custom element not available, icon grid may not render properly:', err.message);
4291
+ }
4292
+
4251
4293
  // Clear existing content
4252
4294
  grid.innerHTML = "";
4253
4295
 
@@ -4276,6 +4318,7 @@
4276
4318
 
4277
4319
  /**
4278
4320
  * Update properties panel for hotspot
4321
+ * Uses unified format with nested appearance, action, and tooltip
4279
4322
  */
4280
4323
  updateHotspotProperties(hotspot) {
4281
4324
  const hotspotAll = document.getElementById("hotspotAll");
@@ -4305,21 +4348,26 @@
4305
4348
  if (hotspotAll) hotspotAll.style.display = "block";
4306
4349
  if (hotspotProperties) hotspotProperties.style.display = "block";
4307
4350
 
4308
- document.getElementById("hotspotTitle").value = hotspot.title || "";
4309
- document.getElementById("hotspotDescription").value =
4310
- hotspot.description || "";
4311
- document.getElementById("hotspotTarget").value =
4312
- hotspot.targetSceneId || "";
4313
- document.getElementById("hotspotColor").value = hotspot.color || "#00ff00";
4351
+ // Get values from nested structure
4352
+ const title = hotspot.tooltip?.text || "";
4353
+ const description = hotspot.tooltip?.description || "";
4354
+ const targetSceneId = hotspot.action?.target || "";
4355
+ const color = hotspot.appearance?.color || "#00ff00";
4356
+ const icon = hotspot.appearance?.icon || "";
4357
+
4358
+ document.getElementById("hotspotTitle").value = title;
4359
+ document.getElementById("hotspotDescription").value = description;
4360
+ document.getElementById("hotspotTarget").value = targetSceneId;
4361
+ document.getElementById("hotspotColor").value = color;
4314
4362
 
4315
4363
  // Update color text input if it exists
4316
4364
  const colorText = document.getElementById("hotspotColorText");
4317
4365
  if (colorText) {
4318
- colorText.value = hotspot.color || "#00ff00";
4366
+ colorText.value = color;
4319
4367
  }
4320
4368
 
4321
4369
  // Update icon grid active button
4322
- this.setActiveIconButton(hotspot.icon || "");
4370
+ this.setActiveIconButton(icon);
4323
4371
 
4324
4372
  // Update position inputs
4325
4373
  const pos = hotspot.position || { x: 0, y: 0, z: 0 };
@@ -4349,7 +4397,8 @@
4349
4397
 
4350
4398
  document.getElementById("sceneId").value = scene.id || "";
4351
4399
  document.getElementById("sceneName").value = scene.name || "";
4352
- document.getElementById("sceneImageUrl").value = scene.imageUrl || "";
4400
+ // Support both panorama (unified) and imageUrl (legacy)
4401
+ document.getElementById("sceneImageUrl").value = scene.panorama || scene.imageUrl || "";
4353
4402
 
4354
4403
  // Update starting position display
4355
4404
  if (startingPosDisplay) {
@@ -4586,75 +4635,13 @@
4586
4635
  /**
4587
4636
  * Generate JSON compatible with SWT library
4588
4637
  * Follows the tourConfig structure from example-simple.html
4638
+ * Uses shared data-transform utilities for consistent transformation
4589
4639
  */
4590
4640
  generateJSON() {
4591
4641
  const scenes = this.editor.sceneManager.getAllScenes();
4592
4642
  const config = this.editor.config;
4593
4643
 
4594
- // Build scenes object (keyed by scene ID)
4595
- const scenesData = {};
4596
- scenes.forEach((scene) => {
4597
- scenesData[scene.id] = {
4598
- name: scene.name,
4599
- panorama: scene.imageUrl,
4600
- hotspots: scene.hotspots.map((hotspot) => {
4601
- const hotspotData = {
4602
- position: hotspot.position,
4603
- };
4604
-
4605
- // Add action based on hotspot type
4606
- if (hotspot.type === "navigation" && hotspot.targetSceneId) {
4607
- hotspotData.action = {
4608
- type: "navigateTo",
4609
- target: hotspot.targetSceneId,
4610
- };
4611
- } else if (hotspot.type === "info") {
4612
- hotspotData.action = {
4613
- type: "showInfo",
4614
- };
4615
- }
4616
-
4617
- // Add appearance
4618
- hotspotData.appearance = {
4619
- color: hotspot.color || "#FF6B6B",
4620
- scale: hotspot.scale || "2 2 2",
4621
- };
4622
-
4623
- // Add icon if set
4624
- if (hotspot.icon) {
4625
- hotspotData.appearance.icon = hotspot.icon;
4626
- }
4627
-
4628
- // Add tooltip if title exists
4629
- if (hotspot.title) {
4630
- hotspotData.tooltip = {
4631
- text: hotspot.title,
4632
- };
4633
- }
4634
-
4635
- return hotspotData;
4636
- }),
4637
- };
4638
-
4639
- // Add starting position if set
4640
- if (scene.startingPosition) {
4641
- scenesData[scene.id].startingPosition = scene.startingPosition;
4642
- }
4643
- });
4644
-
4645
- // Determine initial scene
4646
- let initialScene = config.initialSceneId;
4647
- if (!initialScene && scenes.length > 0) {
4648
- initialScene = scenes[0].id;
4649
- }
4650
-
4651
- // Build final JSON matching SWT tourConfig format
4652
- const jsonData = {
4653
- initialScene: initialScene,
4654
- scenes: scenesData,
4655
- };
4656
-
4657
- return jsonData;
4644
+ return buildTourConfig(config, scenes);
4658
4645
  }
4659
4646
 
4660
4647
  /**
@@ -4665,9 +4652,8 @@
4665
4652
  const jsonData = this.generateJSON();
4666
4653
 
4667
4654
  // Process all scenes and convert icon names to data URLs
4668
- for (const sceneId of Object.keys(jsonData.scenes)) {
4669
- const scene = jsonData.scenes[sceneId];
4670
-
4655
+ // Scenes are an array in unified format
4656
+ for (const scene of jsonData.scenes) {
4671
4657
  for (let i = 0; i < scene.hotspots.length; i++) {
4672
4658
  const hotspot = scene.hotspots[i];
4673
4659
  const icon = hotspot.appearance?.icon;
@@ -4874,8 +4860,8 @@
4874
4860
  // Setup event listeners
4875
4861
  this.setupEventListeners();
4876
4862
 
4877
- // Populate icon grid
4878
- this.uiController.populateIconGrid();
4863
+ // Populate icon grid (async to wait for custom element registration)
4864
+ await this.uiController.populateIconGrid();
4879
4865
 
4880
4866
  // Load saved project if exists (but only if it has valid data)
4881
4867
  if (this.storageManager.hasProject()) {
@@ -4968,19 +4954,19 @@
4968
4954
  });
4969
4955
 
4970
4956
  document.getElementById('hotspotTitle')?.addEventListener('input', debounce((e) => {
4971
- this.updateCurrentHotspot('title', e.target.value);
4957
+ this.updateCurrentHotspot('tooltip.text', e.target.value);
4972
4958
  }, 300));
4973
4959
 
4974
4960
  document.getElementById('hotspotDescription')?.addEventListener('input', debounce((e) => {
4975
- this.updateCurrentHotspot('description', e.target.value);
4961
+ this.updateCurrentHotspot('tooltip.description', e.target.value);
4976
4962
  }, 300));
4977
4963
 
4978
4964
  document.getElementById('hotspotTarget')?.addEventListener('change', (e) => {
4979
- this.updateCurrentHotspot('targetSceneId', e.target.value);
4965
+ this.updateCurrentHotspot('action.target', e.target.value);
4980
4966
  });
4981
4967
 
4982
4968
  document.getElementById('hotspotColor')?.addEventListener('input', (e) => {
4983
- this.updateCurrentHotspot('color', e.target.value);
4969
+ this.updateCurrentHotspot('appearance.color', e.target.value);
4984
4970
  });
4985
4971
 
4986
4972
  // Icon grid button clicks
@@ -4992,7 +4978,7 @@
4992
4978
  document.querySelectorAll('#hotspotIconGrid .icon-btn').forEach(b => b.classList.remove('active'));
4993
4979
  btn.classList.add('active');
4994
4980
  // Update hotspot
4995
- this.updateCurrentHotspot('icon', iconValue);
4981
+ this.updateCurrentHotspot('appearance.icon', iconValue);
4996
4982
  }
4997
4983
  });
4998
4984
 
@@ -5321,7 +5307,8 @@
5321
5307
  const index = this.sceneManager.currentSceneIndex;
5322
5308
  if (index < 0) return;
5323
5309
 
5324
- if (this.sceneManager.updateScene(index, 'imageUrl', imageUrl)) {
5310
+ // Use panorama for unified format
5311
+ if (this.sceneManager.updateScene(index, 'panorama', imageUrl)) {
5325
5312
  const scene = this.sceneManager.getCurrentScene();
5326
5313
  if (scene) {
5327
5314
  scene.thumbnail = imageUrl;
@@ -5388,6 +5375,7 @@
5388
5375
  render() {
5389
5376
  this.uiController.renderSceneList();
5390
5377
  this.uiController.renderHotspotList();
5378
+ this.uiController.populateIconGrid(); // Re-render icon grid to ensure icons display
5391
5379
 
5392
5380
  const currentScene = this.sceneManager.getCurrentScene();
5393
5381
  const currentHotspot = this.hotspotEditor.getCurrentHotspot();