senangwebs-tour 1.0.3 → 1.0.6

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/dist/swt.js CHANGED
@@ -55829,8 +55829,19 @@ void main() {
55829
55829
  * @returns {Promise} - Resolves when the asset is loaded
55830
55830
  */
55831
55831
  async preloadImage(url, id) {
55832
- if (this.loadedAssets.has(id)) {
55833
- return Promise.resolve(this.loadedAssets.get(id));
55832
+ // Check if asset exists and if the URL is the same
55833
+ const existingAsset = this.loadedAssets.get(id);
55834
+ if (existingAsset) {
55835
+ const existingSrc = existingAsset.getAttribute('src');
55836
+ if (existingSrc === url) {
55837
+ // Same URL, return cached asset
55838
+ return Promise.resolve(existingAsset);
55839
+ }
55840
+ // URL changed - remove old asset and create new one
55841
+ if (existingAsset.parentNode) {
55842
+ existingAsset.parentNode.removeChild(existingAsset);
55843
+ }
55844
+ this.loadedAssets.delete(id);
55834
55845
  }
55835
55846
 
55836
55847
  // Ensure assets element is ready
@@ -55989,15 +56000,21 @@ void main() {
55989
56000
  * Transition to a new scene with fade effect
55990
56001
  * @param {string} sceneId - The target scene ID
55991
56002
  * @param {Object} sceneData - The scene configuration object
56003
+ * @param {Function} onSceneLoaded - Optional callback to run after scene loads but before fade-in
55992
56004
  * @returns {Promise} - Resolves when the transition is complete
55993
56005
  */
55994
- async transitionTo(sceneId, sceneData) {
56006
+ async transitionTo(sceneId, sceneData, onSceneLoaded = null) {
55995
56007
  // Fade out
55996
56008
  await this.fadeOut();
55997
56009
 
55998
56010
  // Load new scene
55999
56011
  await this.loadScene(sceneId, sceneData);
56000
56012
 
56013
+ // Call onSceneLoaded callback (e.g., to set camera position while screen is black)
56014
+ if (onSceneLoaded && typeof onSceneLoaded === 'function') {
56015
+ onSceneLoaded();
56016
+ }
56017
+
56001
56018
  // Fade in
56002
56019
  await this.fadeIn();
56003
56020
 
@@ -56092,13 +56109,17 @@ void main() {
56092
56109
  * HotspotManager - Creates, manages, and removes hotspot entities in the A-Frame scene
56093
56110
  */
56094
56111
  class HotspotManager {
56095
- constructor(sceneEl, assetManager, defaultHotspotSettings = {}) {
56112
+ constructor(sceneEl, assetManager, defaultHotspotSettings = {}, iconRenderer = null) {
56096
56113
  this.sceneEl = sceneEl;
56097
56114
  this.assetManager = assetManager;
56098
56115
  this.defaultSettings = defaultHotspotSettings;
56116
+ this.iconRenderer = iconRenderer;
56099
56117
  this.activeHotspots = [];
56100
56118
  this.tooltipEl = null;
56101
56119
  this.tooltipCreated = false;
56120
+ this.iconDataUrls = new Map(); // Cache for generated icon data URLs
56121
+ this.sceneLoadCounter = 0; // Unique counter to prevent asset ID collisions between scene loads
56122
+ this.currentAssetPrefix = ''; // Current asset ID prefix for this scene load
56102
56123
 
56103
56124
  // Listen for hover events
56104
56125
  this.sceneEl.addEventListener('swt-hotspot-hover', (evt) => {
@@ -56177,20 +56198,50 @@ void main() {
56177
56198
  this.createTooltip();
56178
56199
  }
56179
56200
 
56180
- // First, preload all hotspot icons (with error handling)
56181
- const iconPromises = hotspots.map((hotspot, index) => {
56201
+ // Increment scene load counter to ensure unique asset IDs for this scene load
56202
+ // This prevents A-Frame/THREE.js texture caching from reusing old icons
56203
+ this.sceneLoadCounter++;
56204
+ this.currentAssetPrefix = `hotspot-icon-${this.sceneLoadCounter}`;
56205
+
56206
+ // Clear previous icon data URLs cache
56207
+ this.iconDataUrls.clear();
56208
+
56209
+ // Process all hotspot icons SEQUENTIALLY to avoid race condition
56210
+ // (IconRenderer uses a shared renderContainer that would be corrupted by parallel generation)
56211
+ for (let index = 0; index < hotspots.length; index++) {
56212
+ const hotspot = hotspots[index];
56182
56213
  const icon = hotspot.appearance?.icon || this.defaultSettings.icon;
56183
- if (icon) {
56184
- const assetId = `hotspot-icon-${index}`;
56185
- return this.assetManager.preloadImage(icon, assetId).catch(err => {
56214
+ const color = hotspot.appearance?.color || '#ffffff';
56215
+
56216
+ if (!icon) continue;
56217
+
56218
+ // Check if it's an image URL
56219
+ const isImageUrl = icon.startsWith('http') || icon.startsWith('data:') || icon.startsWith('/');
56220
+
56221
+ // Use unique asset ID that includes scene load counter
56222
+ const assetId = `${this.currentAssetPrefix}-${index}`;
56223
+
56224
+ if (isImageUrl) {
56225
+ // Preload as image asset
56226
+ try {
56227
+ await this.assetManager.preloadImage(icon, assetId);
56228
+ } catch (err) {
56186
56229
  console.warn(`Failed to load icon for hotspot ${index}, will use color instead`);
56187
- return null; // Continue even if icon fails to load
56188
- });
56230
+ }
56231
+ } else if (this.iconRenderer) {
56232
+ // Generate icon data URL from SenangStart icon name
56233
+ try {
56234
+ const dataUrl = await this.iconRenderer.generateIconDataUrl(icon, color, 128);
56235
+ if (dataUrl) {
56236
+ this.iconDataUrls.set(index, dataUrl);
56237
+ // Preload the generated data URL as an asset
56238
+ await this.assetManager.preloadImage(dataUrl, assetId);
56239
+ }
56240
+ } catch (err) {
56241
+ console.warn(`Failed to generate icon for hotspot ${index}:`, err);
56242
+ }
56189
56243
  }
56190
- return Promise.resolve();
56191
- });
56192
-
56193
- await Promise.all(iconPromises);
56244
+ }
56194
56245
 
56195
56246
  // Then create the hotspot entities
56196
56247
  hotspots.forEach((hotspot, index) => {
@@ -56211,26 +56262,31 @@ void main() {
56211
56262
  const pos = hotspot.position;
56212
56263
  hotspotEl.setAttribute('position', `${pos.x} ${pos.y} ${pos.z}`);
56213
56264
 
56214
- // Set icon or fallback to plane with color
56265
+ // Get icon and color
56215
56266
  const icon = hotspot.appearance?.icon || this.defaultSettings.icon;
56216
- const assetId = `hotspot-icon-${index}`;
56217
- const assetEl = icon ? document.getElementById(assetId) : null;
56267
+ const color = hotspot.appearance?.color || '#4CC3D9';
56268
+
56269
+ // Check if we have a preloaded icon asset (either from URL or generated from icon name)
56270
+ // Use the unique asset prefix that includes scene load counter
56271
+ const assetId = `${this.currentAssetPrefix}-${index}`;
56272
+ const assetsEl = this.sceneEl.querySelector('a-assets');
56273
+ const assetEl = assetsEl ? assetsEl.querySelector(`#${assetId}`) : null;
56218
56274
 
56219
56275
  let visualEl;
56220
56276
 
56221
- // Check if icon was successfully loaded
56277
+ // Check if icon asset was successfully loaded/generated
56222
56278
  if (icon && assetEl) {
56223
56279
  visualEl = document.createElement('a-image');
56224
56280
  visualEl.setAttribute('src', `#${assetId}`);
56225
56281
  // Make images double-sided
56226
- visualEl.setAttribute('material', 'side', 'double');
56282
+ visualEl.setAttribute('material', 'side: double; transparent: true; alphaTest: 0.1');
56227
56283
  } else {
56228
- // Fallback to a plane with color
56284
+ // Fallback to a colored plane
56229
56285
  visualEl = document.createElement('a-plane');
56230
- visualEl.setAttribute('color', hotspot.appearance?.color || '#4CC3D9');
56286
+ visualEl.setAttribute('color', color);
56231
56287
  visualEl.setAttribute('width', '1');
56232
56288
  visualEl.setAttribute('height', '1');
56233
- // Make plane double-sided and always face camera
56289
+ // Make plane double-sided
56234
56290
  visualEl.setAttribute('material', 'side', 'double');
56235
56291
  }
56236
56292
 
@@ -56258,6 +56314,9 @@ void main() {
56258
56314
  * Remove all active hotspots from the scene
56259
56315
  */
56260
56316
  removeAllHotspots() {
56317
+ // Remove hotspot icon assets from a-assets to prevent icon mixup on scene navigation
56318
+ this.removeHotspotIconAssets();
56319
+
56261
56320
  this.activeHotspots.forEach(hotspot => {
56262
56321
  if (hotspot.parentNode) {
56263
56322
  hotspot.parentNode.removeChild(hotspot);
@@ -56265,12 +56324,43 @@ void main() {
56265
56324
  });
56266
56325
  this.activeHotspots = [];
56267
56326
 
56327
+ // Clear icon data URLs cache
56328
+ this.iconDataUrls.clear();
56329
+
56268
56330
  // Hide tooltip
56269
56331
  if (this.tooltipEl) {
56270
56332
  this.tooltipEl.setAttribute('visible', 'false');
56271
56333
  }
56272
56334
  }
56273
56335
 
56336
+ /**
56337
+ * Remove all hotspot icon assets from a-assets element
56338
+ * This prevents icon mixup when navigating between scenes
56339
+ */
56340
+ removeHotspotIconAssets() {
56341
+ const assetsEl = this.sceneEl.querySelector('a-assets');
56342
+ if (!assetsEl) return;
56343
+
56344
+ // Find and remove all assets with IDs matching hotspot-icon-* pattern
56345
+ const iconAssets = assetsEl.querySelectorAll('[id^="hotspot-icon-"]');
56346
+ iconAssets.forEach(asset => {
56347
+ if (asset.parentNode) {
56348
+ asset.parentNode.removeChild(asset);
56349
+ }
56350
+ });
56351
+
56352
+ // Also clear the asset manager's loaded assets for hotspot icons
56353
+ if (this.assetManager && this.assetManager.loadedAssets) {
56354
+ const keysToDelete = [];
56355
+ this.assetManager.loadedAssets.forEach((value, key) => {
56356
+ if (key.startsWith('hotspot-icon-')) {
56357
+ keysToDelete.push(key);
56358
+ }
56359
+ });
56360
+ keysToDelete.forEach(key => this.assetManager.loadedAssets.delete(key));
56361
+ }
56362
+ }
56363
+
56274
56364
  /**
56275
56365
  * Clean up the hotspot manager
56276
56366
  */
@@ -56282,6 +56372,130 @@ void main() {
56282
56372
  }
56283
56373
  }
56284
56374
 
56375
+ /**
56376
+ * IconRenderer - Converts SenangStart icon names to image data URLs for A-Frame
56377
+ */
56378
+ class IconRenderer {
56379
+ constructor() {
56380
+ this.iconCache = new Map();
56381
+ this.renderContainer = null;
56382
+ }
56383
+
56384
+ /**
56385
+ * Initialize the hidden render container
56386
+ */
56387
+ init() {
56388
+ if (this.renderContainer) return;
56389
+
56390
+ this.renderContainer = document.createElement('div');
56391
+ this.renderContainer.id = 'swt-icon-renderer';
56392
+ this.renderContainer.style.cssText = `
56393
+ position: absolute;
56394
+ left: -9999px;
56395
+ top: -9999px;
56396
+ width: 128px;
56397
+ height: 128px;
56398
+ pointer-events: none;
56399
+ visibility: hidden;
56400
+ `;
56401
+ document.body.appendChild(this.renderContainer);
56402
+ }
56403
+
56404
+ /**
56405
+ * Generate an image data URL from a SenangStart icon
56406
+ * @param {string} iconName - SenangStart icon name (e.g., 'arrow-right')
56407
+ * @param {string} color - Hex color for the icon
56408
+ * @param {number} size - Size in pixels (default 128)
56409
+ * @returns {Promise<string>} - Data URL of the icon image
56410
+ */
56411
+ async generateIconDataUrl(iconName, color = '#ffffff', size = 128) {
56412
+ const cacheKey = `${iconName}-${color}-${size}`;
56413
+
56414
+ if (this.iconCache.has(cacheKey)) {
56415
+ return this.iconCache.get(cacheKey);
56416
+ }
56417
+
56418
+ this.init();
56419
+
56420
+ return new Promise((resolve, reject) => {
56421
+ // Create the ss-icon element
56422
+ this.renderContainer.innerHTML = `
56423
+ <ss-icon
56424
+ icon="${iconName}"
56425
+ thickness="2.5"
56426
+ style="color: ${color}; width: ${size}px; height: ${size}px; display: block;"
56427
+ ></ss-icon>
56428
+ `;
56429
+
56430
+ // Wait for the custom element to render
56431
+ setTimeout(() => {
56432
+ try {
56433
+ const ssIcon = this.renderContainer.querySelector('ss-icon');
56434
+ if (!ssIcon || !ssIcon.shadowRoot) {
56435
+ console.warn(`Icon ${iconName} not rendered properly`);
56436
+ resolve(null);
56437
+ return;
56438
+ }
56439
+
56440
+ // Get the SVG from shadow root
56441
+ const svg = ssIcon.shadowRoot.querySelector('svg');
56442
+ if (!svg) {
56443
+ console.warn(`SVG not found for icon ${iconName}`);
56444
+ resolve(null);
56445
+ return;
56446
+ }
56447
+
56448
+ // Clone and prepare SVG
56449
+ const svgClone = svg.cloneNode(true);
56450
+ svgClone.setAttribute('width', size);
56451
+ svgClone.setAttribute('height', size);
56452
+ svgClone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
56453
+
56454
+ // Apply the color to all paths/elements
56455
+ svgClone.querySelectorAll('path, circle, rect, line, polyline, polygon').forEach(el => {
56456
+ el.setAttribute('stroke', color);
56457
+ // Keep fill as currentColor if it's set
56458
+ const fill = el.getAttribute('fill');
56459
+ if (fill && fill !== 'none') {
56460
+ el.setAttribute('fill', color);
56461
+ }
56462
+ });
56463
+
56464
+ // Convert SVG to data URL
56465
+ const svgString = new XMLSerializer().serializeToString(svgClone);
56466
+ const dataUrl = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgString)));
56467
+
56468
+ // Cache the result
56469
+ this.iconCache.set(cacheKey, dataUrl);
56470
+
56471
+ resolve(dataUrl);
56472
+ } catch (error) {
56473
+ console.error('Error generating icon data URL:', error);
56474
+ resolve(null);
56475
+ }
56476
+ }, 100); // Wait for custom element to render
56477
+ });
56478
+ }
56479
+
56480
+ /**
56481
+ * Clear the icon cache
56482
+ */
56483
+ clearCache() {
56484
+ this.iconCache.clear();
56485
+ }
56486
+
56487
+ /**
56488
+ * Destroy the renderer
56489
+ */
56490
+ destroy() {
56491
+ if (this.renderContainer && this.renderContainer.parentNode) {
56492
+ this.renderContainer.parentNode.removeChild(this.renderContainer);
56493
+ }
56494
+ this.renderContainer = null;
56495
+ this.iconCache.clear();
56496
+ }
56497
+ }
56498
+
56285
56499
  /**
56286
56500
  * SenangWebs Tour (SWT) - Main Library Entry Point
56287
56501
  * Version 1.0.3
@@ -56310,12 +56524,14 @@ void main() {
56310
56524
  // Initialize managers
56311
56525
  this.assetManager = new AssetManager(this.sceneEl);
56312
56526
  this.sceneManager = new SceneManager(this.sceneEl, this.assetManager);
56527
+ this.iconRenderer = new IconRenderer();
56313
56528
 
56314
56529
  const defaultHotspotSettings = this.config.settings?.defaultHotspot || {};
56315
56530
  this.hotspotManager = new HotspotManager(
56316
56531
  this.sceneEl,
56317
56532
  this.assetManager,
56318
- defaultHotspotSettings
56533
+ defaultHotspotSettings,
56534
+ this.iconRenderer
56319
56535
  );
56320
56536
 
56321
56537
  // Event listeners
@@ -56376,6 +56592,11 @@ void main() {
56376
56592
 
56377
56593
  this.isStarted = true;
56378
56594
 
56595
+ // Set camera to starting position if set
56596
+ if (initialSceneData.startingPosition) {
56597
+ this.setCameraToStartingPosition(initialSceneData.startingPosition);
56598
+ }
56599
+
56379
56600
  // Emit events
56380
56601
  this.emit("scene-loaded", { sceneId: initialSceneId });
56381
56602
  this.emit("tour-started", { sceneId: initialSceneId });
@@ -56411,8 +56632,13 @@ void main() {
56411
56632
  // Remove old hotspots
56412
56633
  this.hotspotManager.removeAllHotspots();
56413
56634
 
56414
- // Transition to new scene
56415
- await this.sceneManager.transitionTo(sceneId, sceneData);
56635
+ // Transition to new scene with callback to set camera position while screen is black
56636
+ await this.sceneManager.transitionTo(sceneId, sceneData, () => {
56637
+ // Set camera to starting position during transition (while screen is still black)
56638
+ if (sceneData.startingPosition) {
56639
+ this.setCameraToStartingPosition(sceneData.startingPosition);
56640
+ }
56641
+ });
56416
56642
 
56417
56643
  // Create new hotspots
56418
56644
  await this.hotspotManager.createHotspots(sceneData.hotspots || []);
@@ -56466,6 +56692,41 @@ void main() {
56466
56692
  }
56467
56693
  }
56468
56694
 
56695
+ /**
56696
+ * Set camera to a starting position immediately
56697
+ * Uses look-controls internal pitchObject/yawObject for proper A-Frame compatibility
56698
+ * @param {Object} startingPosition - Object with pitch and yaw in radians
56699
+ */
56700
+ setCameraToStartingPosition(startingPosition) {
56701
+ if (!startingPosition) return;
56702
+
56703
+ const camera = this.sceneEl.querySelector("[camera]");
56704
+ if (!camera) return;
56705
+
56706
+ // Get look-controls component
56707
+ camera.components?.["look-controls"];
56708
+
56709
+ // Set rotation using look-controls internal objects (already in radians)
56710
+ const setRotation = () => {
56711
+ const cam = this.sceneEl.querySelector("[camera]");
56712
+ if (!cam) return;
56713
+
56714
+ const lc = cam.components?.["look-controls"];
56715
+ if (lc && lc.pitchObject && lc.yawObject) {
56716
+ lc.pitchObject.rotation.x = startingPosition.pitch;
56717
+ lc.yawObject.rotation.y = startingPosition.yaw;
56718
+ } else if (cam.object3D) {
56719
+ cam.object3D.rotation.set(startingPosition.pitch, startingPosition.yaw, 0);
56720
+ }
56721
+ };
56722
+
56723
+ // Set immediately and retry a few times to ensure it sticks
56724
+ setRotation();
56725
+ setTimeout(setRotation, 100);
56726
+ setTimeout(setRotation, 300);
56727
+ setTimeout(setRotation, 500);
56728
+ }
56729
+
56469
56730
  /**
56470
56731
  * Get the current scene ID
56471
56732
  * @returns {string}
@@ -56520,6 +56781,11 @@ void main() {
56520
56781
  this.hotspotManager.destroy();
56521
56782
  this.sceneManager.destroy();
56522
56783
  this.assetManager.destroy();
56784
+
56785
+ // Clean up icon renderer
56786
+ if (this.iconRenderer) {
56787
+ this.iconRenderer.destroy();
56788
+ }
56523
56789
 
56524
56790
  this.isStarted = false;
56525
56791
  }